'); + if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(` { + if (ctx.method === 'GET' && /^recent$/i.test(String(ctx.query.filter || '')) && ['q', 'search'].some(k => String(ctx.query[k] || '').trim())) ctx.query = { ...ctx.query, filter: ctx.query.filter === 'RECENT' ? 'ALL' : 'all' }; await next(); }, - async (ctx, next) => { setLanguage(ctx.cookies.get("language") || getConfig().language || "en"); await next(); }, + async (ctx, next) => { await require('../models/typed_log').requestScope.run({ capped: false, limit: 0, path: ctx.path, query: ctx.querystring, method: ctx.method }, next); }, async (ctx, next) => { - const isBinary = ctx.path.startsWith('/qr') || ctx.path.startsWith('/image/') || ctx.path.startsWith('/blob/') || ctx.path.startsWith('/assets/'); + if (ctx.method !== 'POST') return next(); + const forget = () => { + censusCache.clear(); + lifetime.forget(); + try { activityModel.invalidateCache(); } catch (_) {} + clearnetDecisionCache = { at: 0, data: null }; + clearnetIndexCache = { ts: 0, data: null, building: null }; + clearnetBlobCache = { ts: 0, set: null, forcedAt: clearnetBlobCache.forcedAt }; + clearnetTribesCache = { at: 0, data: null }; + }; + forget(); + try { await next(); } finally { forget(); await settleLogWrites(); } + }, + publicModeGuard({ + isPublic: !!config.public, + onBlocked: (ctx) => sendErrorPage(ctx, "Sorry, many actions are unavailable when Oasis is running in public mode. Please run Oasis in the default mode and try again.", { status: 403 }) + }), + async (ctx, next) => { + if (isLoopbackOnlyPath(ctx.path) && !isLoopbackRequest(ctx)) { sendErrorPage(ctx, 'Forbidden', { status: 403 }); return; } + await next(); + }, + syncTribeModuleExposure, + async (ctx, next) => { applyFirstRunLanguage(ctx); setLanguage(isClearnetPath(ctx.request) ? clearnetLanguage(ctx) : (ctx.cookies.get("language") || getConfig().language || "en")); await next(); }, + async (ctx, next) => { + try { require('../views/comments_view').setCommentsOpen(ctx.method === 'GET' && String(ctx.query.comments || '') === 'open'); } catch (_) {} + await next(); + }, + async (ctx, next) => { + if (ctx.method === 'GET' && WALLET_CHIP_PATHS.some(p => ctx.path === p || ctx.path.startsWith(`${p}/`))) { + await settleWithin(refreshWalletReady(), WALLET_CHECK_WAIT_MS); + const kind = (ctx.path === '/transfers' || ctx.path.startsWith('/transfers/')) ? 'transfers' + : (ctx.path === '/banking' || ctx.path.startsWith('/banking/')) ? 'banking' : null; + if (kind) require('../models/typed_log').requestScope.run({ capped: false, limit: 0, path: '/', query: '' }, () => runWalletWork(kind)).catch(() => {}); + } + await next(); + }, + async (ctx, next) => { + await next(); + const flash = String((ctx.state && ctx.state.inlineError) || (ctx.method === 'GET' ? signedErrorOf(ctx) : '')).trim(); + if (!flash || typeof ctx.body !== 'string' || !/html/.test(String(ctx.type || ''))) return; + const { renderInlineError } = require('../views/main_views'); + const marker = ''; + const at = ctx.body.indexOf(marker); + if (at < 0) return; + const cleanUrl = ctx.method === 'GET' ? ctx.path + (() => { const q = new URLSearchParams(ctx.querystring); q.delete('error'); q.delete('errsig'); const s = q.toString(); return s ? `?${s}` : ''; })() : null; + ctx.body = ctx.body.slice(0, at + marker.length) + renderInlineError(flash, cleanUrl) + ctx.body.slice(at + marker.length); + }, + async (ctx, next) => { + const isBinary = ctx.path.startsWith('/qr') || ctx.path.startsWith('/wallet/qr/') || ctx.path.startsWith('/image/') || ctx.path.startsWith('/blob/') || ctx.path.startsWith('/assets/'); if (isBinary) { try { await next(); } catch (err) { ctx.status = err.status || 500; @@ -8007,112 +15547,217 @@ const middleware = [ } return; } - const ssb = await cooler.open(), status = await ssb.status(), values = Object.values(status.sync.plugins); - const totalCurrent = values.reduce((acc, cur) => acc + cur, 0), totalTarget = status.sync.since * values.length; - if (totalTarget - totalCurrent > 1024 * 1024) ctx.response.body = indexingView({ percent: Math.floor((totalCurrent / totalTarget) * 1000) / 10 }); + const migration = typeof SSBconfig.migrationStatus === 'function' ? SSBconfig.migrationStatus() : null; + if (migration && migration.running) { ctx.response.body = indexingView({ percent: migration.percent }); return; } + const until = Date.now() + STARTUP_WAIT_MS; + const ssb = await Promise.race([cooler.open(), new Promise((resolve) => setTimeout(() => resolve(null), STARTUP_WAIT_MS))]); + let status = ssb ? await ssb.status() : { ready: false }; + while (ssb && status && status.ready === false && Date.now() < until) { + await new Promise((resolve) => setTimeout(resolve, 150)); + status = await ssb.status(); + } + if (status && status.ready === false) ctx.response.body = indexingView({ percent: startupProgress(status), refresh: 5 }); + else if (indexingLag(status) > INDEXING_LAG_BYTES) ctx.response.body = indexingView({ percent: Math.floor(indexingProgress(status) * 1000) / 10 }); else { try { await next(); } catch (err) { const { i18n } = require('../views/main_views'); if (err.name === 'FileTooLargeError' || (err.message && err.message.includes('maxFileSize'))) { sendErrorPage(ctx, i18n.fileTooLargeMessage, { title: i18n.fileTooLargeTitle, status: 413 }); - } else { - sendErrorPage(ctx, err.message || 'Internal Server Error', { status: err.status || 500 }); + return; } + const moduleHome = moduleHomeFor(ctx); + if (moduleHome && isMissingContentError(err)) { ctx.redirect(moduleHome); return; } + sendErrorPage(ctx, err.message || 'Internal Server Error', { status: err.status || 500 }); } } }, async (ctx, next) => { if (!ctx.path.startsWith('/assets/') && !ctx.path.startsWith('/image/') && !ctx.path.startsWith('/blob/') && !ctx.path.startsWith('/qr') && !ctx.path.startsWith('/c/')) { - const now = Date.now(); - if (now - sharedState.getLastRefresh() > 60000) { - sharedState.setLastRefresh(now); - try { - const stats = await statsModel.getStats('ALL'); - const totalMB = parseSizeMB(stats.statsBlobsSize) + parseSizeMB(stats.statsBlockchainSize); - const hcT = parseFloat((totalMB * 0.0002 * 475).toFixed(2)); - const inhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 1; - const hcH = inhabitants > 0 ? parseFloat((hcT / inhabitants).toFixed(2)) : 0; - sharedState.setCarbonHcT(hcT); - sharedState.setCarbonHcH(hcH); - } catch (_) {} - try { await refreshInboxCount(); } catch (_) {} - try { await calendarsModel.checkDueReminders(); } catch (_) {} - try { await tasksModel.checkDueReminders(); } catch (_) {} - try { - const peers = await meta.connectedPeers(); - sharedState.setOnlinePeerCount(Array.isArray(peers) ? peers.length : 0); - } catch (_) {} - try { - sharedState.setInboxUnreadCount(sharedState.getInboxCount()); - sharedState.setLastSyncTs(now); - } catch (_) {} - try { - const ex = await bankingModel.calculateEcoinValue(); - if (ex && ex.isSynced) sharedState.setEcoValue(Number(ex.ecoValue).toFixed(4)); - } catch (_) {} - try { - const me = getViewerId(); - const actions = await activityModel.listFeed('all').catch(() => []); - const mine = (actions || []).filter(a => a && a.author === me && a.type !== 'tombstone' && a.type !== 'post'); - mine.sort((a, b) => (b.ts || 0) - (a.ts || 0)); - const prev = mine[1] || mine[0]; - if (prev) { - const hrefByType = { - vote: prev.content?.vote?.link ? `/thread/${encodeURIComponent(prev.content.vote.link)}#${encodeURIComponent(prev.content.vote.link)}` : null, - transfer: `/transfers/${encodeURIComponent(prev.id)}`, - tribe: `/tribe/${encodeURIComponent(prev.id)}`, - shop: `/shops/${encodeURIComponent(prev.id)}`, - job: `/jobs/${encodeURIComponent(prev.id)}`, - event: `/events/${encodeURIComponent(prev.id)}`, - project: `/projects/${encodeURIComponent(prev.id)}`, - image: `/images/${encodeURIComponent(prev.id)}`, - audio: `/audios/${encodeURIComponent(prev.id)}`, - video: `/videos/${encodeURIComponent(prev.id)}`, - document: `/documents/${encodeURIComponent(prev.id)}`, - torrent: `/torrents/${encodeURIComponent(prev.id)}`, - forum: `/forum/${encodeURIComponent(prev.content?.key || prev.id)}`, - bookmark: `/bookmarks/${encodeURIComponent(prev.id)}`, - task: `/tasks/${encodeURIComponent(prev.id)}`, - event_: `/events/${encodeURIComponent(prev.id)}`, - about: `/author/${encodeURIComponent(prev.author)}`, - map: `/maps/${encodeURIComponent(prev.id)}`, - chat: `/chats/${encodeURIComponent(prev.id)}`, - pad: `/pads/${encodeURIComponent(prev.id)}`, - pixelia: `/pixelia` - }; - sharedState.setLastActivity({ - id: prev.id, - type: prev.type, - ts: prev.ts, - href: hrefByType[prev.type] || `/activity?filter=mine` - }); - } - } catch (_) {} - } + if (Date.now() - sharedState.getLastRefresh() > 60000) kickRefresh(); } await next(); }, + // la rama movil: su router y su capa de backend (ver GUIA_DE_DESARROLLO.md) + require('./fork_routes')({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, about }), routes, + async (ctx) => { + if (ctx.body != null || (ctx.method !== 'GET' && ctx.method !== 'HEAD')) return; + if (isClearnetPath(ctx.request) || ctx.path === '/clearnet' || ctx.path.startsWith('/clearnet/')) { + ctx.status = 404; + ctx.type = 'text/html'; + ctx.body = require('../views/clearnet_view').renderClearnetNotFound(); + return; + } + if (/^\/(blob|image|assets|qr|qr-invite)(\/|$)/.test(ctx.path)) return; + ctx.redirect('/'); + }, ]; const app = http({ host, port, middleware, allowHost: config.allowHost }); +if (ADMIN_TOKEN) console.log(`- Admin access (open it on this device): http://localhost:${port}/admin-session/${ADMIN_TOKEN}`); +const startDesktopNotices = async () => { + const { notify: desktopNotify, i18nNow, displayName } = require('./desktopNotify'); + const ssb = await cooler.open(); + if (!ssb || !ssb.db || typeof ssb.db.onMsgAdded !== 'function') return; + const me = ssb.id; + let lastEmergency = null; + try { const f = await emergenciesModel.featured(); lastEmergency = f && f.id; } catch (_) {} + const nameFor = async (id) => { try { return displayName(id, await about.name(id)); } catch (_) { return String(id); } }; + const isMutual = async (id) => { try { const rel = await friend.getRelationship(id); return !!(rel && rel.following && rel.followsMe); } catch (_) { return false; } }; + ssb.db.onMsgAdded((ev) => { + const kvt = ev && ev.kvt; + const v = kvt && kvt.value; + if (!v || v.author === me || Date.now() - (Number(v.timestamp) || 0) > 15 * 60 * 1000) return; + (async () => { + const i18n = i18nNow(); + const cfg = getConfig(); + if (typeof v.content === 'string') { + let c = null; + try { const dec = ssb.private.unbox({ key: kvt.key, value: v, timestamp: kvt.timestamp }); c = dec && dec.value && dec.value.content; } catch (_) {} + if (!c || c.private !== true || !Array.isArray(c.to) || !c.to.includes(me)) return; + if (cfg.pmVisibility === 'mutuals' && !(await isMutual(v.author))) return; + if (cfg.pmVisibility === 'following') { + let rel = null; + try { rel = await friend.getRelationship(v.author); } catch (_) { rel = null; } + if (!(rel && rel.following)) return; + } + if (c.type === 'pam') { + if (cfg.modules.phoneMod !== 'off') desktopNotify(i18n.phoneTitle, `${i18n.notifyNewPamFrom} ${await nameFor(v.author)}`); + return; + } + if (c.type !== 'post') return; + const bot = pmModel.botOf({ subject: String(c.subject || '') }); + if (bot && pmModel.mutedBots(cfg).has(bot)) return; + desktopNotify(i18n.inbox, `${i18n.notifyNewPmFrom} ${bot || await nameFor(v.author)}`); + return; + } + const t = v.content && v.content.type; + if (typeof t === 'string' && t.startsWith('emergency') && cfg.modules.emergenciesMod === 'on') { + const f = await emergenciesModel.featured().catch(() => null); + if (!f || !f.id || f.id === lastEmergency) return; + lastEmergency = f.id; + if (sharedState.getDismissedEmergency && sharedState.getDismissedEmergency() === f.id) return; + desktopNotify(i18n.emergenciesTitle, f.title); + } + })().catch(() => {}); + }, false); +}; +if (!config.public) setTimeout(() => { phoneModel.start().catch(() => {}); startDesktopNotices().catch(() => {}); syncClearnetSince().catch(() => {}); }, 3000); let pubEngineTimer = null; +let pubAddressEnsured = false; +let pubEngineRunning = false; async function runPubEngineTick() { - if (!bankingModel.isPubNode()) return; - try { await bankingModel.executeEpoch({}); } catch (_) {} - try { await bankingModel.processPendingClaims(); } catch (_) {} - try { await bankingModel.publishPubAvailability(); } catch (_) {} + if (!bankingModel.isPubNode() || pubEngineRunning) return; + pubEngineRunning = true; + try { + if (!pubAddressEnsured) { try { const r = await bankingModel.ensureSelfAddressPublished(); pubAddressEnsured = r && r.status !== 'skipped'; } catch (_) {} } + try { await bankingModel.executeEpoch({}); } catch (_) {} + try { await bankingModel.processPendingClaims(); } catch (_) {} + try { await bankingModel.confirmIncomingTransfers(); } catch (_) {} + try { await bankingModel.rebalanceUbiPools(); } catch (_) {} + try { await bankingModel.publishPubAvailability(); } catch (_) {} + } finally { + pubEngineRunning = false; + } +} +const ubiNoticePath = process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, 'banking-ubi-notice.json') : stateFilePath('banking-ubi-notice.json'); +const confirmNoticePath = process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, 'banking-confirm-notice.json') : stateFilePath('banking-confirm-notice.json'); +async function notifyPendingConfirmations() { + if (getConfig().modules?.transfersMod === 'off') return; + try { + const me = getViewerId(); + const list = await transfersModel.listAll('all', me).catch(() => []); + const pending = (list || []).filter(t => { + const tags = Array.isArray(t.tags) ? t.tags.map(x => String(x).toUpperCase()) : []; + const settledUbi = tags.includes('UBI') && /^[0-9a-f]{64}$/i.test(String(t.txid || '')); + return String(t.status || '').toUpperCase() === 'UNCONFIRMED' + && String(t.to) === String(me) + && !(Array.isArray(t.confirmedBy) ? t.confirmedBy : []).includes(me) + && !tags.includes('PENDING') + && !settledUbi; + }); + if (!pending.length) return; + let notified = []; + try { notified = JSON.parse(fs.readFileSync(confirmNoticePath, 'utf8')) || []; } catch (_) {} + const fresh = pending.filter(t => !notified.includes(t.id)); + if (!fresh.length) return; + const { i18n: i18nB } = require('../views/main_views'); + const lines = fresh.slice(0, 5).map(t => `· [${t.concept || t.id}](/transfers/${encodeURIComponent(t.id)}) — ${Number(t.amount || 0).toFixed(6)} ECO`).join('\n'); + await notifyBot('BANKING_CONFIRM_PENDING', [me], `${i18nB.bankingBotConfirmText} (${fresh.length}):\n${lines}\n\n[${i18nB.transfersFilterPending}](/transfers?filter=pending)`); + fs.writeFileSync(confirmNoticePath, JSON.stringify([...notified, ...fresh.map(t => t.id)].slice(-500))); + } catch (_) {} +} + +const lastWalletWork = { transfers: 0, banking: 0 }; +async function runWalletWork(kind) { + if (bankingModel.isPubNode()) return; + if (Date.now() - (lastWalletWork[kind] || 0) < 60000) return; + lastWalletWork[kind] = Date.now(); + try { await bankingModel.confirmIncomingTransfers(); } catch (_) {} + if (kind === 'transfers') { try { await notifyPendingConfirmations(); } catch (_) {} return; } + try { + const avail = await bankingModel.claimAvailability(getViewerId()); + if (!avail.available) return; + let notified = {}; + try { notified = JSON.parse(fs.readFileSync(ubiNoticePath, 'utf8')) || {}; } catch (_) {} + if (notified.epochId === avail.epochId) return; + const { i18n: i18nB } = require('../views/main_views'); + await notifyBot('BANKING_UBI_AVAILABLE', [getViewerId()], `${i18nB.bankingBotUbiAvailableText}: [${i18nB.bankClaimUBI}](/banking?filter=exchange) · ${avail.epochId}`); + fs.writeFileSync(ubiNoticePath, JSON.stringify({ epochId: avail.epochId, at: new Date().toISOString() })); + } catch (_) {} +} +const CLEARNET_ROOMS_SYNC_MS = 60 * 1000; +const CLEARNET_ROOM_WATCHERS_MAX = 50; +const SOUND_RATE = require('../server/phone_module').SOUND_RATE; +const SOUND_BACKLOG_MAX = SOUND_RATE * 2 * 2; +const clearnetRoomWatchers = new Map(); +const clearnetRoomOk = async (room) => { + if (!room || room.isClosed || !CLEARNET_REACH_OK.rooms(room)) return false; + const prefs = await about.visibilityPrefs(room.author).catch(() => null); + return clearnetPublicSync('rooms', room, room.author, prefs, await clearnetDecisions()); +}; +const clearnetRoomPreview = async (ctx, room) => { + if (!room || !isLoopbackRequest(ctx) || !checkMod(ctx, 'phoneMod')) return false; + const state = await roomsModel.liveState().catch(() => null); + return !!state && state.phase === 'live' && state.rid === room.rid && Array.isArray(state.gridOrder) && state.gridOrder.length > 0; +}; +let clearnetRoomsSyncing = false; +const syncClearnetRooms = async () => { + if (clearnetRoomsSyncing || !config.public) return; + clearnetRoomsSyncing = true; + try { + const rooms = await roomsModel.listAll({ filter: 'all', viewerId: getViewerId() }).catch(() => []); + const wanted = []; + for (const room of rooms || []) if (await clearnetRoomOk(room)) wanted.push(room); + const rids = new Set(wanted.map(r => r.rid)); + for (const rid of await roomsModel.viewerRooms()) if (!rids.has(rid)) await roomsModel.viewerLeave(rid).catch(() => null); + const inside = new Set(await roomsModel.viewerRooms()); + for (const room of wanted) if (!inside.has(room.rid)) await roomsModel.viewerJoin(room).catch(() => null); + } catch (_) {} + finally { clearnetRoomsSyncing = false; } +}; +if (config.public) { + setTimeout(syncClearnetRooms, 20000); + const clearnetRoomsTimer = setInterval(syncClearnetRooms, CLEARNET_ROOMS_SYNC_MS); + if (clearnetRoomsTimer.unref) clearnetRoomsTimer.unref(); +} +const PUB_INVITES_REFRESH_MS = 24 * 60 * 60 * 1000; +if (ssbConfig.pub === true) { + const announcePubInvites = async () => { try { await pubInvites.announce(await cooler.open()); } catch (_) {} }; + setTimeout(announcePubInvites, 60000); + setInterval(announcePubInvites, PUB_INVITES_REFRESH_MS); } if (bankingModel.isPubNode()) { + console.log(`[UBI] PUB engine on: paying UBI through ecoind at ${getConfig().wallet.url}`); setTimeout(() => { runPubEngineTick(); }, 15000); pubEngineTimer = setInterval(runPubEngineTick, 30 * 60 * 1000); } -setTimeout(() => { larpModel.init().catch(() => {}); }, 10000); +setTimeout(() => { if (getConfig().modules?.larpMod === 'on') larpModel.init().catch(() => {}); }, 10000); let welcomePmAttempted = false; async function sendWelcomePmIfFirstLaunch() { if (welcomePmAttempted) return; welcomePmAttempted = true; - const flagPath = path.join(ssbConfig.path, 'oasis-first-contact'); + const flagPath = stateFilePath('oasis-first-contact'); try { const ssbClient = await cooler.open(); if (!ssbClient || !ssbClient.id) { welcomePmAttempted = false; return; } @@ -8137,7 +15782,9 @@ async function sendWelcomePmIfFirstLaunch() { welcomePmAttempted = false; return; } - try { fs.writeFileSync(flagPath, ownId + '\n' + new Date().toISOString() + '\n'); } catch (e) { + try { + if (!onboardingModel.begin(ownId)) console.error('[welcome-pm] flag write failed'); + } catch (e) { console.error('[welcome-pm] flag write failed:', e && e.message ? e.message : e); } } catch (err) { @@ -8145,38 +15792,245 @@ async function sendWelcomePmIfFirstLaunch() { welcomePmAttempted = false; } } -setTimeout(() => { sendWelcomePmIfFirstLaunch(); }, 20000); +let firstRunLanguageSettled = false; +const supportedLanguages = () => Object.keys(require('../client/assets/translations/i18n')); +function clearnetLanguage(ctx) { + const supported = supportedLanguages(); + const wanted = String(ctx.query.lang || '').trim().toLowerCase(); + if (supported.includes(wanted)) { + const store = require('../models/typed_log').requestScope.getStore(); + if (store) store.cnLang = wanted; + return wanted; + } + const detected = require('../models/onboarding_model').browserLanguage(ctx.get('accept-language'), supported); + return detected || getConfig().language || 'en'; +} +function applyFirstRunLanguage(ctx) { + if (firstRunLanguageSettled || isClearnetPath(ctx.request)) return; + const cfg = getConfig(); + const decision = require('../models/onboarding_model').firstRunLanguage({ + fresh: !onboardingModel.firstContactSeen(), + isPublic: !!config.public, + configured: cfg.language, + cookie: ctx.cookies.get('language'), + header: ctx.get('accept-language'), + supported: supportedLanguages() + }); + if (!decision.settled) return; + firstRunLanguageSettled = true; + if (decision.language && decision.language !== cfg.language) { + cfg.language = decision.language; + saveConfig(cfg); + } + setTimeout(welcomePmTick, 0); +} +let welcomePmRetries = 0; +const welcomePmTick = async () => { + if (onboardingModel.firstContactSeen()) return; + if (!firstRunLanguageSettled) { + const cfg = getConfig(); + const decision = require('../models/onboarding_model').firstRunLanguage({ fresh: true, isPublic: !!config.public, configured: cfg.language, supported: supportedLanguages() }); + if (!decision.settled) return; + firstRunLanguageSettled = true; + } + await sendWelcomePmIfFirstLaunch(); + if (onboardingModel.firstContactSeen()) return; + if (welcomePmRetries++ >= 30) return; + setTimeout(welcomePmTick, 4000); +}; +setTimeout(welcomePmTick, 3000); -async function logClearnetStatus() { +const pbFmtDate = (iso) => iso ? moment(iso).format('YYYY-MM-DD HH:mm:ss') : ''; +const pbFmtRemaining = (endIso) => { + if (!endIso) return ''; + const ms = moment(endIso).diff(moment()); + if (ms <= 0) return '0d 00:00:00'; + const d = Math.floor(ms / 86400000); + const h = Math.floor((ms % 86400000) / 3600000); + const mi = Math.floor((ms % 3600000) / 60000); + const se = Math.floor((ms % 60000) / 1000); + const pad = n => String(n).padStart(2, '0'); + return `${d}d ${pad(h)}:${pad(mi)}:${pad(se)}`; +}; +const pbFill = (tpl, vars) => String(tpl || '').replace(/\{(\w+)\}/g, (m, k) => (k in vars ? vars[k] : m)); +const pbTitleCase = (s) => { const t = String(s || ''); return t ? t.charAt(0).toUpperCase() + t.slice(1).toLowerCase() : ''; }; +const pbAtId = (id) => id ? '@' + String(id).replace(/^@/, '') : ''; +function buildLarpRulingMsg(data = {}, t = {}) { + const house = data.house || {}; + const name = house.name || data.houseKey || ''; + const intro = pbFill(t.politicalBotLarpIntro || 'Now {house} rules during this cycle: {cycle}', { + house: `[${name}](/larp/${encodeURIComponent(data.houseKey || '')})`, + cycle: `[${data.cycleFormatted || ''}](/larp)` + }); + const blocks = [ + [intro], + [name, house.description || ''], + [ + `${t.larpRolesLabel || 'Roles'}: ${house.roles || ''}`, + `${t.larpFunctionLabel || 'Function'}: ${house.function || ''}`, + `${t.larpMonthLabel || 'Governance cycle'}: ${house.month != null ? house.month : ''}`, + `${t.larpMembersCount || 'Members'}: ${data.memberCount || 0}` + ], + [house.motto ? `“${house.motto}”` : ''] + ]; + const body = blocks + .map(block => block.filter(l => l != null && String(l).trim() !== '').join('\n')) + .filter(Boolean) + .join('\n\n'); + return { subject: 'LARP_RULING', body }; +} +function pbGovBlock(gov = {}, t = {}) { + const lines = [`${t.parliamentGovernmentCard || 'Current Government'}: ${String(gov.method || '').toUpperCase()}`]; + if (gov.leaderId) lines.push(`${t.politicalBotLeaderLabel || 'Leader'}: ${pbAtId(gov.leaderId)}`); + lines.push(`${t.parliamentLegSince || 'CYCLE SINCE'}: ${pbFmtDate(gov.since)}`); + lines.push(`${t.parliamentLegEnd || 'CYCLE END'}: ${pbFmtDate(gov.end)}`); + lines.push(`${t.parliamentTimeRemaining || 'Time remaining'}: ${pbFmtRemaining(gov.end)}`); + lines.push(`${t.parliamentPopulation || 'Population'}: ${gov.population || 0}`); + return lines; +} +function buildParliamentGovMsg(gov = {}, t = {}) { + const govLink = `[${pbTitleCase(gov.method)}](/parliament)`; + const intro = gov.leaderId + ? pbFill(t.politicalBotParliamentIntroLeader || 'The current government is {gov}, led by {leader}', { gov: govLink, leader: pbAtId(gov.leaderId) }) + : pbFill(t.politicalBotParliamentIntro || 'The current government is {gov}', { gov: govLink }); + return { subject: 'PARLIAMENT_GOV', body: [intro, ''].concat(pbGovBlock(gov, t)).join('\n') }; +} +function buildTribeGovMsg(gov = {}, t = {}) { + const govLink = `[${pbTitleCase(gov.method)}](/tribe/${encodeURIComponent(gov.tribeId || '')}?section=governance)`; + const vars = { tribe: gov.tribeName || '', gov: govLink, leader: pbAtId(gov.leaderId) }; + const intro = gov.leaderId + ? pbFill(t.politicalBotTribeIntroLeader || 'The current government in the Tribe {tribe} is {gov}, led by {leader}', vars) + : pbFill(t.politicalBotTribeIntro || 'The current government in the Tribe {tribe} is {gov}', vars); + return { subject: 'TRIBE_GOV', body: [intro, ''].concat(pbGovBlock(gov, t)).join('\n') }; +} +let jobsBotRunning = false; +async function checkJobMatches() { + if (jobsBotRunning) return; + jobsBotRunning = true; try { - const ssbClient = await cooler.open(); - if (!ssbClient || !ssbClient.id) return; - const prefs = await about.visibilityPrefs(ssbClient.id).catch(() => null); - const modules = [ - ['Shops', 'clearnetShops'], - ['Jobs', 'clearnetJobs'], - ['Events', 'clearnetEvents'], - ['Projects', 'clearnetProjects'], - ['Blogs', 'clearnetPosts'], - ['Audios', 'clearnetAudios'], - ['Videos', 'clearnetVideos'], - ['Images', 'clearnetImages'], - ['Documents', 'clearnetDocuments'], - ['Torrents', 'clearnetTorrents'] - ]; - const active = prefs ? modules.filter(([_, k]) => prefs[k] === true).map(([label]) => label) : []; - try { - const { setClearnetModules } = require('../server/ssb_metadata'); - setClearnetModules(active); - } catch (_) {} - } catch (_) { - try { - const { setClearnetModules } = require('../server/ssb_metadata'); - setClearnetModules([]); - } catch (_) {} + const viewerId = getViewerId(); + if (!viewerId) return; + const cv = await cvModel.getCVByUserId().catch(() => null); + if (!cv || cv.aiManaged === false) return; + const threshold = Math.min(100, Math.max(0, Number(cv.matchThreshold) || 80)) / 100; + const matches = await dataModel.jobMatchesFor(viewerId, { minScore: threshold }).catch(() => []); + if (!matches.length) return; + const announced = await pmModel.sentRefs().catch(() => new Set()); + const i18nAll = require('../client/assets/translations/i18n'); + const lang = (getConfig() && getConfig().language) || 'en'; + const t = i18nAll[lang] || i18nAll.en; + for (const m of matches.slice(0, 5)) { + const ref = `${m.id}|${Math.round(m.score * 100)}`; + if (announced.has(`JOB_MATCH|${ref}`)) continue; + const pctText = `${Math.round(m.score * 100)}%`; + const body = [ + (t.jobsBotMatchIntro || 'A job matches your curriculum.'), + '', + `${t.jobsBotMatchJob || 'Job'}: [${m.title || m.id}](${m.href})`, + `${t.matchScore || 'Match'}: ${pctText}`, + m.common.length ? `${t.commonSkills || 'Common skills'}: ${m.common.slice(0, 10).join(', ')}` : '', + '', + (t.jobsBotMatchHint || 'You receive this because your curriculum is AI managed. You can turn it off in your CV.') + ].filter(Boolean).join('\n'); + try { + await notifyBot('JOB_MATCH', [], body, false, ref); + announced.add(`JOB_MATCH|${ref}`); + } catch (e) { + if (config.debug) console.error('[jobs-bot] send failed:', e && e.message); + } + } + } catch (e) { + if (config.debug) console.error('[jobs-bot] failed:', e && e.message); + } finally { + jobsBotRunning = false; + } +} + +let politicalCheckRunning = false; +async function checkPoliticalChanges() { + if (politicalCheckRunning) return; + politicalCheckRunning = true; + try { + const ssbClient = await cooler.open().catch(() => null); + if (!ssbClient || !ssbClient.id) return; + const ownId = ssbClient.id; + const i18nAll = require('../client/assets/translations/i18n'); + const lang = (getConfig() && getConfig().language) || 'en'; + const t = i18nAll[lang] || i18nAll.en; + const announced = await pmModel.sentRefs().catch(() => new Set()); + const alreadyAnnounced = (subject, ref) => announced.has(`${subject}|${ref}`); + const seen = pmPolicy.readAnnounceSeen(); + const send = async (subject, body, ref) => { + const { send: shouldSend, remember } = pmPolicy.decideAnnouncement({ subject, ref, announced, seen }); + if (!shouldSend) { + if (remember) { seen[subject] = String(ref); pmPolicy.writeAnnounceSeen(seen); } + return; + } + try { + await notifyBot(subject, [], body, { ref }); + announced.add(`${subject}|${ref}`); + seen[subject] = String(ref); + pmPolicy.writeAnnounceSeen(seen); + } catch (e) { console.error('[political-bot] send failed:', e && e.message); } + }; + + try { + const houseKey = larpModel.getGoverningHouseKey(); + if (houseKey) { + let cycleFormatted = ''; + try { cycleFormatted = larpModel.computeCycle().formatted; } catch (_) {} + const ref = larpModel.getGoverningPeriodId(); + if (!alreadyAnnounced('LARP_RULING', ref)) { + const house = (larpModel.getHouse && larpModel.getHouse(houseKey)) || {}; + let memberCount = 0; + try { const hc = await larpModel.listHousesWithCounts(); const h = (hc || []).find(x => x.key === houseKey); memberCount = h ? (h.memberCount || 0) : 0; } catch (_) {} + const { subject, body } = buildLarpRulingMsg({ houseKey, house, memberCount, cycleFormatted }, t); + await send(subject, body, ref); + } + } + } catch (e) { console.error('[political-bot] larp:', e && e.message); } + + try { + const card = await parliamentModel.getLatestGovernmentCard().catch(() => null); + if (card) { + const ref = [String(card.id || card.method || ''), String(card.since || '')].filter(Boolean).join(':'); + if (ref && !alreadyAnnounced('PARLIAMENT_GOV', ref)) { + let population = 0; + try { population = Number(await inhabitantsModel.countInhabitants()) || 0; } catch (_) {} + const leaderId = (card.powerType === 'inhabitant' && card.powerId) ? card.powerId : null; + const { subject, body } = buildParliamentGovMsg({ method: card.method, since: card.since, end: card.end, population, leaderId }, t); + await send(subject, body, ref); + } + } + } catch (e) { console.error('[political-bot] parliament:', e && e.message); } + + try { + let tribes = []; + try { tribes = await tribesModel.listAll(); } catch (_) { tribes = []; } + for (const tribe of (tribes || [])) { + if (!tribe || !tribe.id) continue; + if (!Array.isArray(tribe.members) || !tribe.members.includes(ownId)) continue; + if (tribe.parentTribeId) continue; + let term = null; + try { term = await parliamentModel.tribe.getCurrentTerm(tribe.id); } catch (_) { term = null; } + if (!term) continue; + const ref = `${tribe.id}:${String(term.id || term.startAt || term.method || '')}`; + if (!alreadyAnnounced('TRIBE_GOV', ref)) { + const lead = Array.isArray(term.leaders) && term.leaders.length ? term.leaders[0] : null; + const leaderId = lead && typeof lead === 'object' ? (lead.id || lead.powerId || null) : lead; + const population = Array.isArray(tribe.members) ? tribe.members.length : 0; + const { subject, body } = buildTribeGovMsg({ method: term.method, since: term.startAt, end: term.endAt, population, leaderId, tribeId: tribe.id, tribeName: tribe.title || tribe.name || tribe.id }, t); + await send(subject, body, ref); + } + } + } catch (e) { console.error('[political-bot] tribe:', e && e.message); } + + } catch (err) { + console.error('[political-bot] unexpected:', err && err.message); + } finally { + politicalCheckRunning = false; } } -setTimeout(() => { logClearnetStatus(); }, 8000); app._close = () => { if (pubEngineTimer) clearInterval(pubEngineTimer); diff --git a/src/backend/blobHandler.js b/src/backend/blobHandler.js index 95c00b8..cb2c3c2 100644 --- a/src/backend/blobHandler.js +++ b/src/backend/blobHandler.js @@ -5,19 +5,202 @@ const ssb = require("../client/gui"); const config = require("../server/SSB_server").config; const cooler = ssb({ offline: config.offline }); -let sharp; -try { - sharp = require("sharp"); -} catch (e) { -} +let sharpLib; +const getSharp = () => { + if (sharpLib === undefined) { + try { sharpLib = require('../server/node_modules/sharp'); } catch (_) { sharpLib = null; } + } + return sharpLib; +}; + +/** + * Quitar metadatos de una imagen SIN sharp. + * + * sharp no se empaqueta en Android (lleva binarios nativos y el bundle del movil + * va sin ninguno), asi que alli esta funcion devolvia la imagen intacta y las + * fotos subian con su EXIF entero, ubicacion GPS incluida. + * + * Esto recorre el contenedor y tira los bloques de metadatos, sin decodificar ni + * recomprimir la imagen: los pixeles se quedan byte a byte como estaban. + * + * De la orientacion se guarda solo eso, la orientacion. Sin sharp no se pueden + * girar los pixeles, y tirar la etiqueta dejaria las fotos verticales tumbadas; + * asi que se reconstruye un bloque EXIF minimo con esa unica etiqueta, que no + * dice nada de quien hizo la foto ni donde. + */ + +// JPEG: cada segmento es FF . Se tiran los que llevan +// metadatos y se dejan JFIF (APP0) y el perfil de color (APP2), que afectan a como +// se ve la imagen. +const JPEG_DROP = new Set([ + 0xE1, // APP1 Exif y XMP + 0xE5, // APP5 + 0xE6, // APP6 + 0xEC, // APP12 Ducky/Picture Info + 0xED, // APP13 IPTC / Photoshop + 0xEE, // APP14 Adobe + 0xFE // COM comentario +]); + +const jpegOrientation = (seg) => { + // seg empieza en "Exif\0\0"; detras va una cabecera TIFF con la IFD0 + if (seg.length < 14 || seg.toString("latin1", 0, 6) !== "Exif\0\0") return 0; + const tiff = seg.subarray(6); + const le = tiff.toString("latin1", 0, 2) === "II"; + const u16 = (o) => (le ? tiff.readUInt16LE(o) : tiff.readUInt16BE(o)); + const u32 = (o) => (le ? tiff.readUInt32LE(o) : tiff.readUInt32BE(o)); + if (u16(2) !== 0x002A) return 0; + const ifd0 = u32(4); + if (ifd0 + 2 > tiff.length) return 0; + const n = u16(ifd0); + for (let i = 0; i < n; i++) { + const e = ifd0 + 2 + i * 12; + if (e + 12 > tiff.length) break; + if (u16(e) === 0x0112) { // Orientation + const v = u16(e + 8); + return v >= 1 && v <= 8 ? v : 0; + } + } + return 0; +}; + +/** Bloque APP1 de 32 bytes con la orientacion y nada mas. */ +const exifOnlyOrientation = (orientation) => { + const b = Buffer.alloc(32); + b.write("Exif\0\0", 0, "latin1"); + b.write("II", 6, "latin1"); + b.writeUInt16LE(0x002A, 8); + b.writeUInt32LE(8, 10); // la IFD0 empieza justo detras de la cabecera TIFF + b.writeUInt16LE(1, 14); // una sola entrada + b.writeUInt16LE(0x0112, 16); // Orientation + b.writeUInt16LE(3, 18); // SHORT + b.writeUInt32LE(1, 20); // un valor + b.writeUInt16LE(orientation, 24); + b.writeUInt32LE(0, 28); // no hay mas IFDs + return b; +}; + +const stripJpeg = (buf) => { + if (buf.length < 4 || buf[0] !== 0xFF || buf[1] !== 0xD8) return null; + const out = [buf.subarray(0, 2)]; + let orientation = 0; + let i = 2; + let scan = false; + while (i + 4 <= buf.length) { + if (buf[i] !== 0xFF) return null; // no es un JPEG sano: no tocarlo + const marker = buf[i + 1]; + if (marker === 0xD8 || (marker >= 0xD0 && marker <= 0xD9)) { i += 2; continue; } + if (marker === 0xDA) { out.push(buf.subarray(i)); scan = true; break; } // datos comprimidos: hasta el final + const len = buf.readUInt16BE(i + 2); + if (len < 2 || i + 2 + len > buf.length) return null; + const seg = buf.subarray(i + 4, i + 2 + len); + if (marker === 0xE1 && !orientation) orientation = jpegOrientation(seg); + if (!JPEG_DROP.has(marker)) out.push(buf.subarray(i, i + 2 + len)); + i += 2 + len; + } + // sin datos de imagen (SOS) es un JPEG cortado: devolver la cabecera sola subiria una foto vacia + if (!scan) return null; + if (orientation > 1) { + const exif = exifOnlyOrientation(orientation); + const head = Buffer.alloc(4); + head.writeUInt16BE(0xFFE1, 0); + head.writeUInt16BE(exif.length + 2, 2); + out.splice(1, 0, head, exif); + } + return Buffer.concat(out); +}; + +// PNG: cadena de bloques . +const PNG_DROP = new Set(["tEXt", "zTXt", "iTXt", "eXIf", "tIME", "dSIG"]); + +const stripPng = (buf) => { + const SIG = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + if (buf.length < 8 || !buf.subarray(0, 8).equals(SIG)) return null; + const out = [buf.subarray(0, 8)]; + let i = 8; + while (i + 8 <= buf.length) { + const len = buf.readUInt32BE(i); + const type = buf.toString("latin1", i + 4, i + 8); + const end = i + 12 + len; + if (end > buf.length) return null; + if (!PNG_DROP.has(type)) out.push(buf.subarray(i, end)); + i = end; + if (type === "IEND") break; + } + return Buffer.concat(out); +}; + +// WebP: contenedor RIFF con bloques (+1 de relleno +// si la longitud es impar). +const stripWebp = (buf) => { + if (buf.length < 12) return null; + if (buf.toString("latin1", 0, 4) !== "RIFF" || buf.toString("latin1", 8, 12) !== "WEBP") return null; + const out = []; + let i = 12; + while (i + 8 <= buf.length) { + const type = buf.toString("latin1", i, i + 4); + const len = buf.readUInt32LE(i + 4); + const end = i + 8 + len + (len % 2); + if (end > buf.length) return null; + if (type !== "EXIF" && type !== "XMP ") out.push(buf.subarray(i, end)); + i = end; + } + const body = Buffer.concat(out); + const head = Buffer.alloc(12); + head.write("RIFF", 0, "latin1"); + head.writeUInt32LE(body.length + 4, 4); + head.write("WEBP", 8, "latin1"); + return Buffer.concat([head, body]); +}; + +// Formatos que pueden llevar EXIF (GPS incluido) y que aqui no se saben limpiar: +// HEIC/HEIF/AVIF (caja ftyp) y TIFF. Antes subian tal cual, con la ubicacion. +const isUncleanable = (buf) => { + if (buf.length >= 12 && buf.toString("latin1", 4, 8) === "ftyp") { + return /^(heic|heix|hevc|hevx|heim|heis|mif1|msf1|avif|avis)$/.test(buf.toString("latin1", 8, 12)); + } + return buf.length >= 4 && (buf.toString("latin1", 0, 4) === "II*\0" || buf.toString("latin1", 0, 4) === "MM\0*"); +}; +const looksCleanable = (buf) => + (buf.length >= 2 && buf[0] === 0xFF && buf[1] === 0xD8) || + (buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))) || + (buf.length >= 12 && buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP"); + +const unsafe = () => { + let msg = "This image could not be cleaned of its metadata, so it has not been uploaded."; + try { msg = require("../views/main_views").i18n.imageMetadataUnsafe || msg; } catch (_) {} + const err = new Error(msg); + err.status = 400; + return err; +}; + +/** + * Limpia segun el formato. Si es JPEG/PNG/WebP y no se puede limpiar, o es un + * formato con EXIF que no sabemos limpiar, LANZA: es mejor no subir la foto que + * subirla con la ubicacion. Formatos sin metadatos de ese tipo (BMP, SVG, ICO...) + * pasan tal cual. + */ +const stripMetadataPure = (buffer) => { + if (isUncleanable(buffer)) throw unsafe(); + if (!looksCleanable(buffer)) return buffer; + let out = null; + try { out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer); } catch (_) { out = null; } + if (!out || !out.length) throw unsafe(); + return out; +}; const stripImageMetadata = async (buffer) => { - if (typeof sharp !== "function") return buffer; - try { - return await sharp(buffer).rotate().toBuffer(); - } catch { - return buffer; + // Con sharp se prefiere su camino: ademas de quitar metadatos, aplica la + // orientacion girando los pixeles de verdad. + const sharp = getSharp(); + if (sharp) { + try { + return await sharp(buffer).rotate().toBuffer(); + } catch { + return stripMetadataPure(buffer); + } } + return stripMetadataPure(buffer); }; const PDF_METADATA_KEYS = [ @@ -47,7 +230,7 @@ const stripPdfMetadata = (buffer) => { } }; -const MAX_BLOB_SIZE = 50 * 1024 * 1024; +const MAX_BLOB_SIZE = 75 * 1024 * 1024; class FileTooLargeError extends Error { constructor(fileName, fileSize) { @@ -58,25 +241,32 @@ class FileTooLargeError extends Error { } } -const handleBlobUpload = async function (ctx, fileFieldName) { - if (!ctx.request.files || !ctx.request.files[fileFieldName]) { - return null; - } +const OGG_VIDEO_MARKERS = ['theora', 'VP80', 'OggDS', 'Dirac']; +const OGG_AUDIO_MARKERS = ['vorbis', 'OpusHead', 'FLAC', 'Speex']; - const blobUpload = ctx.request.files[fileFieldName]; - if (!blobUpload) return null; +const oggMime = (buffer) => { + const head = buffer.slice(0, 65536); + for (const marker of OGG_VIDEO_MARKERS) if (head.includes(Buffer.from(marker))) return 'video/ogg'; + for (const marker of OGG_AUDIO_MARKERS) if (head.includes(Buffer.from(marker))) return 'audio/ogg'; + return 'video/ogg'; +}; + +const sealedRef = (sealed, mime, name) => `/blob/sealed/${encodeURIComponent(sealed.manifestBlobId)}?k=${encodeURIComponent(sealed.key)}&t=${encodeURIComponent(mime)}&n=${encodeURIComponent(name)}`; + +const storeUploadedFile = async function (blobUpload, seal = null) { + if (!blobUpload || !blobUpload.filepath) return null; let data = await promisesFs.readFile(blobUpload.filepath); if (data.length === 0) return null; if (data.length > MAX_BLOB_SIZE) { - throw new FileTooLargeError(blobUpload.originalFilename || blobUpload.name || fileFieldName, data.length); + throw new FileTooLargeError(blobUpload.originalFilename || blobUpload.name || 'file', data.length); } const EXTENSION_MIME_MAP = { '.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', - '.ogv': 'video/ogg', '.avi': 'video/x-msvideo', '.mov': 'video/quicktime', - '.mkv': 'video/x-matroska', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', + '.ogv': 'video/ogg', '.oga': 'audio/ogg', '.mkv': 'video/x-matroska', '.avi': 'video/x-msvideo', '.mov': 'video/quicktime', + '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac', '.aac': 'audio/aac', '.opus': 'audio/opus', '.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', @@ -93,9 +283,12 @@ const handleBlobUpload = async function (ctx, fileFieldName) { blob.mime = null; } - if (!blob.mime && blob.name) { + if (blob.mime === 'application/ogg') blob.mime = oggMime(data); + + const GENERIC_MIMES = new Set(['application/octet-stream', 'video/x-matroska']); + if ((!blob.mime || GENERIC_MIMES.has(blob.mime)) && blob.name) { const ext = (blob.name.match(/\.[^.]+$/) || [''])[0].toLowerCase(); - blob.mime = EXTENSION_MIME_MAP[ext] || 'application/octet-stream'; + blob.mime = EXTENSION_MIME_MAP[ext] || blob.mime || 'application/octet-stream'; } if (!blob.mime) { @@ -108,14 +301,19 @@ const handleBlobUpload = async function (ctx, fileFieldName) { data = stripPdfMetadata(data); } - const ssbClient = await cooler.open(); - - blob.id = await new Promise((resolve, reject) => { - pull( - pull.values([data]), - ssbClient.blobs.add((err, ref) => (err ? reject(err) : resolve(ref))) - ); - }); + if (typeof seal === 'function') { + await promisesFs.writeFile(blobUpload.filepath, data); + const sealed = await seal({ filepath: blobUpload.filepath, size: data.length, name: blob.name, mime: blob.mime }); + blob.id = sealedRef(sealed, blob.mime, blob.name); + } else { + const ssbClient = await cooler.open(); + blob.id = await new Promise((resolve, reject) => { + pull( + pull.values([data]), + ssbClient.blobs.add((err, ref) => (err ? reject(err) : resolve(ref))) + ); + }); + } if (blob.mime.startsWith("image/")) return `\n![image:${blob.name}](${blob.id})`; if (blob.mime.startsWith("audio/")) return `\n[audio:${blob.name}](${blob.id})`; @@ -126,6 +324,25 @@ const handleBlobUpload = async function (ctx, fileFieldName) { return `\n[${blob.name}](${blob.id})`; }; +const handleBlobUpload = async function (ctx, fileFieldName, seal = null) { + if (!ctx.request.files || !ctx.request.files[fileFieldName]) return null; + const entry = ctx.request.files[fileFieldName]; + const first = Array.isArray(entry) ? entry[0] : entry; + return storeUploadedFile(first, seal); +}; + +const handleBlobUploads = async function (ctx, fileFieldName, max = 8, seal = null) { + if (!ctx.request.files || !ctx.request.files[fileFieldName]) return []; + const entry = ctx.request.files[fileFieldName]; + const files = (Array.isArray(entry) ? entry : [entry]).slice(0, Math.max(0, max)); + const out = []; + for (const file of files) { + const stored = await storeUploadedFile(file, seal); + if (stored) out.push(stored); + } + return out; +}; + function waitForBlob(ssbClient, blobId, timeoutMs = 8000) { return new Promise((resolve, reject) => { let done = false; @@ -168,6 +385,52 @@ function waitForBlob(ssbClient, blobId, timeoutMs = 8000) { }); } +const contentDisposition = (type, name) => { + const clean = String(name || 'file').replace(/["\r\n\\/]/g, '').trim() || 'file'; + const ascii = clean.replace(/[^\x20-\x7e]/g, '_'); + if (ascii === clean) return `${type}; filename="${clean}"`; + return `${type}; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(clean).replace(/['()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())}`; +}; + +const sendBlobBuffer = (ctx, buffer) => { + const size = buffer.length; + const range = ctx.headers.range; + + if (range) { + const match = /^bytes=(\d*)-(\d*)$/.exec(range); + if (!match) { + ctx.status = 416; + ctx.set('Content-Range', `bytes */${size}`); + return; + } + + let start = match[1] ? parseInt(match[1], 10) : 0; + let end = match[2] ? parseInt(match[2], 10) : size - 1; + + if (Number.isNaN(start) || start < 0) start = 0; + if (Number.isNaN(end) || end >= size) end = size - 1; + + if (start > end || start >= size) { + ctx.status = 416; + ctx.set('Content-Range', `bytes */${size}`); + return; + } + + const chunk = buffer.slice(start, end + 1); + + ctx.status = 206; + ctx.set('Content-Range', `bytes ${start}-${end}/${size}`); + ctx.set('Accept-Ranges', 'bytes'); + ctx.set('Content-Length', String(chunk.length)); + ctx.body = chunk; + } else { + ctx.status = 200; + ctx.set('Accept-Ranges', 'bytes'); + ctx.set('Content-Length', String(size)); + ctx.body = buffer; + } +}; + const serveBlob = async function (ctx) { const encodedParam = (ctx.params.id || ctx.params.blobId || '').trim(); const raw = decodeURIComponent(encodedParam); @@ -207,8 +470,6 @@ const serveBlob = async function (ctx) { return; } - const size = buffer.length; - let mime = 'application/octet-stream'; try { const ft = await FileType.fromBuffer(buffer); @@ -216,7 +477,7 @@ const serveBlob = async function (ctx) { } catch {} if (mime === 'application/octet-stream' && buffer.length > 10 && buffer[0] === 0x64) { - const head = buffer.slice(0, 128).toString('ascii'); + const head = buffer.slice(0, 512).toString('ascii'); if (head.includes('announce') || head.includes('8:announce') || head.includes('4:info')) mime = 'application/x-bittorrent'; } @@ -229,51 +490,22 @@ const serveBlob = async function (ctx) { } } + if (mime === 'application/ogg') mime = oggMime(buffer); + const isSvg = mime === 'image/svg+xml'; const qName = ctx.query.name ? String(ctx.query.name).replace(/["\r\n\\]/g, '').trim() : ''; const safeRaw = String(raw).replace(/["\r\n\\]/g, ''); - const filename = qName || (mime === 'application/x-bittorrent' ? 'download.torrent' : safeRaw); - const disposition = isSvg ? 'attachment' : 'inline'; + const wantsDownload = String(ctx.query.download || '') === '1'; + const extByMime = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg', 'video/mp4': 'mp4', 'video/webm': 'webm', 'audio/mpeg': 'mp3', 'audio/ogg': 'ogg', 'application/pdf': 'pdf', 'application/x-bittorrent': 'torrent' }; + const ext = extByMime[mime] || String(mime.split('/')[1] || 'bin').replace(/[^a-z0-9]/gi, '').slice(0, 8) || 'bin'; + const filename = qName || (mime === 'application/x-bittorrent' ? 'download.torrent' : (wantsDownload ? `oasis-${safeRaw.replace(/^&/, '').slice(0, 8)}.${ext}` : safeRaw)); + const disposition = isSvg || wantsDownload ? 'attachment' : 'inline'; ctx.type = mime; - ctx.set('Content-Disposition', `${disposition}; filename="${filename}"`); + ctx.set('Content-Disposition', contentDisposition(disposition, filename)); ctx.set('Cache-Control', 'public, max-age=31536000, immutable'); - const range = ctx.headers.range; - - if (range) { - const match = /^bytes=(\d*)-(\d*)$/.exec(range); - if (!match) { - ctx.status = 416; - ctx.set('Content-Range', `bytes */${size}`); - return; - } - - let start = match[1] ? parseInt(match[1], 10) : 0; - let end = match[2] ? parseInt(match[2], 10) : size - 1; - - if (Number.isNaN(start) || start < 0) start = 0; - if (Number.isNaN(end) || end >= size) end = size - 1; - - if (start > end || start >= size) { - ctx.status = 416; - ctx.set('Content-Range', `bytes */${size}`); - return; - } - - const chunk = buffer.slice(start, end + 1); - - ctx.status = 206; - ctx.set('Content-Range', `bytes ${start}-${end}/${size}`); - ctx.set('Accept-Ranges', 'bytes'); - ctx.set('Content-Length', String(chunk.length)); - ctx.body = chunk; - } else { - ctx.status = 200; - ctx.set('Accept-Ranges', 'bytes'); - ctx.set('Content-Length', String(size)); - ctx.body = buffer; - } + sendBlobBuffer(ctx, buffer); }; -module.exports = { handleBlobUpload, serveBlob, FileTooLargeError }; +module.exports = { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, oggMime, FileTooLargeError, stripImageMetadata }; diff --git a/src/backend/media-favorites.js b/src/backend/content_favorites.js similarity index 70% rename from src/backend/media-favorites.js rename to src/backend/content_favorites.js index 78a8923..1d7540c 100644 --- a/src/backend/media-favorites.js +++ b/src/backend/content_favorites.js @@ -1,22 +1,43 @@ const fs = require("fs"); const path = require("path"); -const FILE = path.join(__dirname, "../configs/media-favorites.json"); +const storePath = () => require("../configs/state-manager").statePath("content_favorites.json"); const DEFAULT = { + emergencies: [], audios: [], + blogs: [], bookmarks: [], calendars: [], + campaigns: [], chats: [], documents: [], + events: [], + forum: [], + housing: [], images: [], + jobs: [], + logistics: [], + logs: [], + mailing: [], maps: [], pads: [], + rooms: [], + podcasts: [], + polls: [], + projects: [], + reports: [], + school: [], shops: [], market: [], shopProducts: [], + tasks: [], torrents: [], - videos: [] + files: [], + transfers: [], + videos: [], + votes: [], + wiki: [] }; const safeArr = (v) => (Array.isArray(v) ? v : []); @@ -39,32 +60,32 @@ const normalize = (raw) => { const ensureFile = async () => { try { - await fs.promises.access(FILE); + await fs.promises.access(storePath()); } catch (e) { - const dir = path.dirname(FILE); + const dir = path.dirname(storePath()); await fs.promises.mkdir(dir, { recursive: true }); - await fs.promises.writeFile(FILE, JSON.stringify(DEFAULT, null, 2), "utf8"); + await fs.promises.writeFile(storePath(), JSON.stringify(DEFAULT, null, 2), "utf8"); } }; const readAll = async () => { await ensureFile(); try { - const txt = await fs.promises.readFile(FILE, "utf8"); + const txt = await fs.promises.readFile(storePath(), "utf8"); return normalize(JSON.parse(txt || "{}")); } catch (e) { const fixed = normalize(DEFAULT); - await fs.promises.writeFile(FILE, JSON.stringify(fixed, null, 2), "utf8"); + await fs.promises.writeFile(storePath(), JSON.stringify(fixed, null, 2), "utf8"); return fixed; } }; const writeAll = async (data) => { - const dir = path.dirname(FILE); - const tmp = path.join(dir, `.media-favorites.${process.pid}.${Date.now()}.tmp`); + const dir = path.dirname(storePath()); + const tmp = path.join(dir, `.content_favorites.${process.pid}.${Date.now()}.tmp`); const txt = JSON.stringify(normalize(data), null, 2); await fs.promises.writeFile(tmp, txt, "utf8"); - await fs.promises.rename(tmp, FILE); + await fs.promises.rename(tmp, storePath()); }; const assertKind = (kind) => { @@ -88,6 +109,15 @@ exports.getFavoriteSet = async (kind) => { return new Set(safeArr(data[k]).map(String)); }; +exports.getFavoriteIndex = async () => { + const data = await readAll(); + const index = new Map(); + for (const kind of Object.keys(DEFAULT)) { + for (const id of safeArr(data[kind])) index.set(String(id), kind); + } + return index; +}; + exports.addFavorite = async (kind, id) => withLock(async () => { const k = assertKind(kind); @@ -123,4 +153,4 @@ exports.removeFromFavorites = async (...args) => { const id = idFromArgs(args); return exports.removeFavorite(kind, id); }; - +exports.storePath = storePath; diff --git a/src/backend/dedupe.js b/src/backend/dedupe.js index c5d9e97..c0d3d5b 100644 --- a/src/backend/dedupe.js +++ b/src/backend/dedupe.js @@ -49,4 +49,79 @@ const dedupeByPreferring = (items, keyFn, scoreFn) => { return order.map(o => (o.solo !== undefined ? o.solo : o.slot.v)); }; -module.exports = { dedupeBy, mergeDuplicatesBy, dedupeByPreferring, norm }; +const versionGroups = (entries) => { + const byKey = new Map(); + for (const e of (entries || [])) if (e && e.key) byKey.set(e.key, e); + const memo = new Map(); + const rootOf = (key) => { + if (memo.has(key)) return memo.get(key); + const path = []; + const seen = new Set([key]); + let cur = key; + let root = null; + while (root === null) { + if (memo.has(cur)) { root = memo.get(cur); break; } + const e = byKey.get(cur); + const p = e && e.replaces; + if (!p || !byKey.has(p) || seen.has(p)) { root = cur; break; } + path.push(cur); + seen.add(p); + cur = p; + } + memo.set(cur, root); + for (const k of path) memo.set(k, root); + return root; + }; + const isOrphan = (key) => { + const e = byKey.get(key); + return !!(e && e.replaces && !byKey.has(e.replaces)); + }; + const isForeignCopy = (key) => { + const e = byKey.get(key); + return !!(e && isOrphan(key) && typeof e.claimedAuthor === 'string' && e.claimedAuthor && e.claimedAuthor !== e.author); + }; + const groups = new Map(); + for (const k of byKey.keys()) { + const r = rootOf(k); + if (!groups.has(r)) groups.set(r, []); + groups.get(r).push(k); + } + const later = (a, b) => (a.seq || 0) > (b.seq || 0) || ((a.seq || 0) === (b.seq || 0) && (a.ts || 0) >= (b.ts || 0)); + for (const [r, keys] of Array.from(groups.entries())) { + if (!isForeignCopy(r)) continue; + const owner = byKey.get(r).claimedAuthor; + let first = null; + for (const k of keys) { const e = byKey.get(k); if (e.author === owner && (!first || later(byKey.get(first), e))) first = k; } + if (!first) continue; + groups.delete(r); + groups.set(first, keys); + for (const k of keys) memo.set(k, first); + } + const ownerTip = new Map(); + for (const [r, keys] of groups) { + const re = byKey.get(r); + for (const k of keys) { + const e = byKey.get(k); + if (!re || e.author !== re.author) continue; + const prev = ownerTip.has(r) ? byKey.get(ownerTip.get(r)) : null; + if (!prev || later(e, prev)) ownerTip.set(r, k); + } + } + const isForeign = (key) => { + const e = byKey.get(key); + const re = byKey.get(rootOf(key)); + return !!(e && re && e.author !== re.author); + }; + const roots = new Set(); + for (const r of groups.keys()) if (!isForeignCopy(r)) roots.add(r); + const tipOf = (key) => { const r = rootOf(key); return ownerTip.get(r) || r; }; + return { rootOf, tipOf, roots, groups, isOrphan, isForeign, isForeignCopy }; +}; + +const versionEntry = (m) => { + const v = (m && m.value) || {}; + const c = v.content || {}; + return { key: m && m.key, author: v.author, seq: v.sequence || 0, ts: v.timestamp || (m && m.timestamp) || 0, replaces: typeof c.replaces === 'string' ? c.replaces : null, claimedAuthor: typeof c.author === 'string' ? c.author : null }; +}; + +module.exports = { dedupeBy, mergeDuplicatesBy, dedupeByPreferring, norm, versionGroups, versionEntry }; diff --git a/src/backend/desktopNotify.js b/src/backend/desktopNotify.js new file mode 100644 index 0000000..c12a6ea --- /dev/null +++ b/src/backend/desktopNotify.js @@ -0,0 +1,80 @@ +const fs = require('fs'); +const path = require('path'); +const { spawn } = require('child_process'); +const { getConfig } = require('../configs/config-manager.js'); + +const REPEAT_MS = 60000; +const ICON = path.join(__dirname, '..', 'client', 'assets', 'images', 'snh-oasis.jpg'); +const recent = new Map(); + +const onPath = (bin) => (process.env.PATH || '').split(path.delimiter).some(d => d && fs.existsSync(path.join(d, bin))); + +const i18nNow = () => { + const all = require('../client/assets/translations/i18n'); + let lang = 'en'; + try { lang = getConfig().language || 'en'; } catch (_) {} + return { ...all.en, ...(all[lang] || {}) }; +}; + +const displayName = (id, name) => { + const n = String(name || '').trim(); + if (!n || n === 'Redacted' || n === String(id || '').replace(/^@/, '').slice(0, 8)) return String(id || ''); + return n.startsWith('@') ? n : `@${n}`; +}; + +const MOBILE_QUEUE = path.join(__dirname, '..', '..', '.notifications.json'); +const queueForAndroid = (keyword, body, line, key) => { + try { + let list = []; + try { list = JSON.parse(fs.readFileSync(MOBILE_QUEUE, 'utf8')); } catch (_) { list = []; } + if (!Array.isArray(list)) list = []; + const t = i18nNow(); + const k = String(keyword || ''); + const route = k === t.phoneTitle ? '/phone' : k === t.inbox ? '/inbox' : k === t.emergenciesTitle ? '/emergencies' : '/'; + const entry = { id: Date.now() + '-' + Math.random().toString(36).slice(2, 8), keyword: k || 'Oasis', body, line, route, key: key ? String(key) : null, ts: Date.now() }; + if (entry.key && entry.key.indexOf('call:') === 0) { + const ringing = String(t.phoneIsCalling || ''); + entry.caller = ringing && body.endsWith(ringing) ? body.slice(0, body.length - ringing.length).trim() : body; + entry.accept = String(t.phoneAnswer || ''); + entry.reject = String(t.phoneReject || ''); + } + list.push(entry); + while (list.length > 20) list.shift(); + fs.writeFileSync(MOBILE_QUEUE, JSON.stringify(list)); + return true; + } catch (_) { + return false; + } +}; + +const notify = (keyword, text, key = null, opts = {}) => { + const onMobile = process.env.OASIS_MOBILE === '1'; + if (!onMobile && !onPath('notify-send')) return false; + const body = String(text || '').replace(/\s+/g, ' ').trim().replace(/[.。!…]+$/, '').slice(0, 200); + const line = `${String(keyword || 'Oasis').toUpperCase()}: ${body}...`; + const now = Date.now(); + const dedupe = key || line; + for (const [k, t] of recent) if (now - t > REPEAT_MS) recent.delete(k); + if (recent.has(dedupe)) return false; + recent.set(dedupe, now); + if (onMobile) return queueForAndroid(keyword, body, line, key); + const actions = Array.isArray(opts.actions) ? opts.actions.filter(x => x && x.key && x.label) : []; + const extra = [ + ...(opts.timeoutMs > 0 ? ['-t', String(Math.round(opts.timeoutMs))] : []), + ...actions.flatMap(x => ['-A', `${x.key}=${x.label}`]) + ]; + try { + const p = spawn('notify-send', ['-a', 'Oasis', ...(fs.existsSync(ICON) ? ['-i', ICON] : []), ...extra, line], { stdio: ['ignore', actions.length ? 'pipe' : 'ignore', 'ignore'] }); + p.on('error', () => {}); + if (actions.length && typeof opts.onAction === 'function') { + let out = ''; + p.stdout.on('data', (d) => { out += d; }); + p.on('close', () => { const chosen = out.trim(); if (actions.some(x => x.key === chosen)) opts.onAction(chosen); }); + } + return true; + } catch (_) { + return false; + } +}; + +module.exports = { notify, i18nNow, displayName }; diff --git a/src/backend/fileshare_crypto.js b/src/backend/fileshare_crypto.js new file mode 100644 index 0000000..d6bef73 --- /dev/null +++ b/src/backend/fileshare_crypto.js @@ -0,0 +1,74 @@ +const crypto = require('crypto'); + +const ALG = 'aes-256-gcm'; +const IV_LEN = 12; +const TAG_LEN = 16; +const KEY_LEN = 32; +const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024; + +const generateFileKey = () => crypto.randomBytes(KEY_LEN); + +const keyToHex = (key) => Buffer.isBuffer(key) ? key.toString('hex') : String(key); + +const keyFromHex = (hex) => { + const buf = Buffer.from(String(hex), 'hex'); + if (buf.length !== KEY_LEN) throw new Error('Invalid file key'); + return buf; +}; + +const encryptChunk = (plaintext, key) => { + const iv = crypto.randomBytes(IV_LEN); + const cipher = crypto.createCipheriv(ALG, key, iv); + const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]); + const tag = cipher.getAuthTag(); + return Buffer.concat([iv, tag, ct]); +}; + +const decryptChunk = (payload, key) => { + if (!Buffer.isBuffer(payload) || payload.length < IV_LEN + TAG_LEN) throw new Error('Corrupt chunk'); + const iv = payload.subarray(0, IV_LEN); + const tag = payload.subarray(IV_LEN, IV_LEN + TAG_LEN); + const ct = payload.subarray(IV_LEN + TAG_LEN); + const decipher = crypto.createDecipheriv(ALG, key, iv); + decipher.setAuthTag(tag); + return Buffer.concat([decipher.update(ct), decipher.final()]); +}; + +const sha256Hex = (buffer) => crypto.createHash('sha256').update(buffer).digest('hex'); + +const splitBuffer = (buffer, chunkSize = DEFAULT_CHUNK_SIZE) => { + const size = Math.max(1, Number(chunkSize) || DEFAULT_CHUNK_SIZE); + const out = []; + for (let off = 0; off < buffer.length; off += size) { + out.push(buffer.subarray(off, Math.min(off + size, buffer.length))); + } + if (!out.length) out.push(Buffer.alloc(0)); + return out; +}; + +const buildManifest = ({ filename, mime, size, chunkSize, chunkHashes, plainSha256 }) => ({ + v: 1, + alg: ALG, + filename: String(filename || 'file'), + mime: String(mime || 'application/octet-stream'), + size: Number(size) || 0, + chunkSize: Number(chunkSize) || DEFAULT_CHUNK_SIZE, + chunks: Array.isArray(chunkHashes) ? chunkHashes.slice() : [], + plainSha256: plainSha256 || null +}); + +const encryptManifest = (manifest, key) => encryptChunk(Buffer.from(JSON.stringify(manifest), 'utf8'), key); + +const decryptManifest = (payload, key) => { + const json = decryptChunk(payload, key).toString('utf8'); + const m = JSON.parse(json); + if (!m || m.v !== 1 || !Array.isArray(m.chunks)) throw new Error('Invalid manifest'); + return m; +}; + +module.exports = { + ALG, IV_LEN, TAG_LEN, KEY_LEN, DEFAULT_CHUNK_SIZE, + generateFileKey, keyToHex, keyFromHex, + encryptChunk, decryptChunk, sha256Hex, splitBuffer, + buildManifest, encryptManifest, decryptManifest +}; diff --git a/src/backend/fork_routes.js b/src/backend/fork_routes.js new file mode 100644 index 0000000..368ef0c --- /dev/null +++ b/src/backend/fork_routes.js @@ -0,0 +1,152 @@ +"use strict"; +// Router propio de la rama movil. +// +// Va en la cadena justo delante de las rutas del dev (backend.js lo monta con una +// sola linea). Lo que no atiende pasa al del dev con next(). Aqui viven: +// - mobile_layer.js: lanzador, filtro y avatar de la barra de arriba +// - wrapUpstream: mira lo que contesta el dev (seccion apagada, guardar modulos) +// - las rutas propias: tema y Modulos en el movil, barra de abajo, identidades +// +// backend.js es del dev y cambia en 34 de cada 39 versiones: nada de esto va alli. +// Ver GUIA_DE_DESARROLLO.md. + +const koaRouter = require("../server/node_modules/@koa/router"); +const { koaBody } = require("../server/node_modules/koa-body"); +const compose = require("../server/node_modules/koa-compose"); +const { isLocalPath } = require("./request_guards"); +const { ALL_MODULES } = require("../models/workflows_model"); +const { mobileModulesView, STEMS } = require("../views/mobile_modules_view"); +require("../models/fork_workflows").register(); + +const isMobile = () => process.env.OASIS_MOBILE === '1'; + +// Seccion apagada: upstream responde con una redireccion a /modules sin decir de +// donde venia. Este envoltorio la ve pasar y anota que modulo era, para que la +// pantalla de Modulos del movil lo diga y ofrezca activarlo. La app es de una +// persona, asi que basta con recordar el ultimo unos segundos. +const SEGMENT_MOD = { tribe: 'tribes', vote: 'votes', document: 'docs', documents: 'docs', blog: 'blogs', event: 'events', + task: 'tasks', calendar: 'calendars', image: 'images', video: 'videos', audio: 'audios', bookmark: 'bookmarks', + file: 'files', poll: 'polls', report: 'reports', project: 'projects', shop: 'shops', job: 'jobs', campaign: 'campaigns', + podcast: 'podcasts', room: 'rooms', map: 'maps', transfer: 'transfers', pad: 'pads', chat: 'chats', torrent: 'torrents' }; +const modOfPath = (p) => { + const seg = String(p || '').split('/')[1] || ''; + const mod = STEMS[seg] ? seg : SEGMENT_MOD[seg]; + return mod && ALL_MODULES.includes(mod) ? mod : null; +}; +let lastBlocked = null; +const takeBlocked = () => { + const b = lastBlocked && Date.now() - lastBlocked.at < 15000 ? lastBlocked : null; + lastBlocked = null; + return b; +}; +const wrapUpstream = async (ctx, next) => { + await next(); + if (!isMobile() || ctx.status < 300 || ctx.status >= 400) return; + const to = String(ctx.response.get('Location') || ''); + // guardar en Modulos: volver con el aviso de guardado + if (ctx.method === 'POST' && ctx.path === '/save-modules') { ctx.redirect('/modules?saved=1'); return; } + if (ctx.method === 'GET' && ctx.path !== '/modules' && /^(https?:\/\/[^/]+)?\/modules$/.test(to)) { + const mod = modOfPath(ctx.path); + if (mod) lastBlocked = { mod, path: ctx.path, at: Date.now() }; + } +}; + +module.exports = ({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, about }) => { +const mobileLayer = require('./mobile_layer')({ getViewerId, about }); + + const forkRouter = new koaRouter(); + forkRouter + // tema en el movil: solo los skins de movil (uno de escritorio deja la app sin + // maqueta). Hace lo mismo que la ruta del dev, con esa comprobacion delante + .post('/settings/theme', koaBody(), async (ctx, next) => { + if (!isMobile()) return next(); + const theme = String((ctx.request.body || {}).theme || '').trim(); + const { MOBILE_THEMES } = require('../views/main_views'); + if (!MOBILE_THEMES.some(([value]) => value === theme)) { ctx.redirect('/settings#theme'); return; } + const cfg = getConfig(); + cfg.themes.current = theme; + saveConfig(cfg); + ctx.cookies.set('theme', theme, { httpOnly: true, sameSite: 'strict', secure: ctx.secure }); + ctx.redirect('/settings#theme'); + }) + // Modulos en el movil: la vista propia (mobile_modules_view.js); en escritorio, la de upstream + .get('/modules', async (ctx, next) => { + if (!isMobile()) return next(); + ctx.body = mobileModulesView({ blocked: takeBlocked(), saved: ctx.query.saved === '1' }); + }) + // activar un solo modulo desde el aviso de Modulos y volver a donde se queria ir + .post('/modules/enable', koaBody(), async (ctx) => { + if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } + const mod = String((ctx.request.body || {}).mod || '').replace(/Mod$/, ''); + if (!ALL_MODULES.includes(mod)) { ctx.redirect('/modules'); return; } + const cfg = getConfig(); + cfg.modules[`${mod}Mod`] = 'on'; + saveConfig(cfg); + const back = String((ctx.request.body || {}).returnTo || ''); + ctx.redirect(isLocalPath(back) ? back : `/${mod}`); + }) + .get("/settings/bottombar", async (ctx) => { + ctx.body = require("../views/main_views").bottomBarPickerView(); + }) + .post("/settings/bottombar", koaBody(), async (ctx) => { + const cfg = getConfig(); + const MAX = 4; + const body = ctx.request.body || {}; + const action = String(body.action || '').trim(); + const key = String(body.key || '').trim(); + const catalog = require("../views/main_views").PINNABLE_MODULES || []; + let pins = Array.isArray(cfg.bottomBarPins) ? cfg.bottomBarPins.slice() : []; + if (action === 'add') { + if (key && catalog.some(m => m.key === key) && !pins.includes(key) && pins.length < MAX) pins.push(key); + } else if (action === 'remove') { + pins = pins.filter(k => k !== key); + } else if (action === 'clear') { + pins = []; + } + cfg.bottomBarPins = pins.slice(0, MAX); + saveConfig(cfg); + ctx.redirect('/settings/bottombar'); + }) + .post("/settings/identity/use", koaBody(), async (ctx) => { + // solo anota cual sera la del proximo arranque: cambiarla en caliente dejaria + // media aplicacion hablando con el sbot anterior + const accounts = require('./accounts.js'); + const name = String((ctx.request.body || {}).account || '').trim(); + if (!accounts.isValid(name) || !accounts.list().includes(name)) { + sendErrorPage(ctx, require('../views/main_views').i18n.identitiesInvalidName, { status: 400 }); + return; + } + accounts.setActive(name); + ctx.redirect('/settings'); + }) + .post("/settings/identity/create", koaBody(), async (ctx) => { + const accounts = require('./accounts.js'); + const name = String((ctx.request.body || {}).account || '').trim(); + if (!accounts.isValid(name)) { + sendErrorPage(ctx, require('../views/main_views').i18n.identitiesInvalidName, { status: 400 }); + return; + } + // no se genera el secret aqui: lo crea ssb-config al arrancar con esa cuenta, + // que es quien sabe como hacerlo. Basta con anotarla y reiniciar. + accounts.setActive(name); + ctx.redirect('/settings'); + }) + .post("/settings/identity/restart", koaBody(), async (ctx) => { + // Solo en movil: alli el backend vive en un servicio en primer plano que + // sobrevive a cerrar la app, asi que la unica forma de arrancar con otra + // identidad es terminar el proceso. nodejs-mobile no permite volver a + // levantar el runtime dentro del mismo proceso: hay que salir y que la app + // lo relance al abrirse. + if (process.env.OASIS_MOBILE !== '1') { ctx.status = 404; ctx.body = ''; return; } + ctx.body = require("../views/main_views").template( + require('../views/main_views').i18n.identitiesTitle, + require("../server/node_modules/hyperaxe").section( + require("../server/node_modules/hyperaxe").p(require('../views/main_views').i18n.identitiesRestarting) + ) + ); + // se responde primero: si se sale antes, el navegador se queda sin pagina + setTimeout(() => process.exit(0), 500); + }); + + return compose([mobileLayer, wrapUpstream, forkRouter.routes()]); +}; diff --git a/src/backend/logsPdf.js b/src/backend/logsPdf.js deleted file mode 100644 index ca25ec3..0000000 --- a/src/backend/logsPdf.js +++ /dev/null @@ -1,206 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const LOGO_PATH = path.join(__dirname, '..', 'client', 'assets', 'images', 'snh-oasis.jpg'); - -const escapePdf = s => String(s || '').replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); - -const linkPattern = /(?:https?:\/\/[^\s]+|www\.[^\s]+|@[A-Za-z0-9+/=.\-]+\.ed25519|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g; - -const splitSegments = (line) => { - const segs = []; - let last = 0; - const re = new RegExp(linkPattern.source, 'g'); - let m; - while ((m = re.exec(line)) !== null) { - if (m.index > last) segs.push({ t: line.slice(last, m.index), l: false }); - segs.push({ t: m[0], l: true }); - last = m.index + m[0].length; - } - if (last < line.length) segs.push({ t: line.slice(last), l: false }); - return segs; -}; - -const wrap = (txt, max = 82) => { - const out = []; - for (const raw of String(txt || '').split('\n')) { - let line = raw; - while (line.length > max) { - let cut = line.lastIndexOf(' ', max); - if (cut <= 0) cut = max; - out.push(line.slice(0, cut)); - line = line.slice(cut).replace(/^\s+/, ''); - } - out.push(line); - } - return out; -}; - -const readJpegDims = (buf) => { - let i = 2; - while (i < buf.length) { - if (buf[i] !== 0xFF) return null; - const marker = buf[i + 1]; - if (marker === 0xD8 || marker === 0xD9) { i += 2; continue; } - const len = buf.readUInt16BE(i + 2); - if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) { - const h = buf.readUInt16BE(i + 5); - const w = buf.readUInt16BE(i + 7); - const c = buf[i + 9]; - return { w, h, c }; - } - i += 2 + len; - } - return null; -}; - -function buildLogsPdf(entries, oasisId, opts = {}) { - const pageW = 612; - const pageH = 792; - const marginX = 50; - const headerH = 90; - const footerH = 40; - const bodyTop = pageH - headerH - 22; - const bodyBottom = footerH + 10; - const lineH = 12; - const maxBodyLines = Math.floor((bodyTop - bodyBottom) / lineH); - - let logoBuf = null; - let logoDims = null; - try { - logoBuf = fs.readFileSync(LOGO_PATH); - logoDims = readJpegDims(logoBuf); - } catch (_) {} - - const allLines = []; - for (const e of entries) { - const ts = new Date(e.ts); - const when = ts.toISOString().replace('T', ' ').slice(0, 19); - allLines.push({ kind: 'header', text: `[${when}]:` }); - allLines.push({ kind: 'blank', text: '' }); - for (const l of wrap(e.text, 82)) allLines.push({ kind: 'text', text: l }); - allLines.push({ kind: 'blank', text: '' }); - } - if (!allLines.length) allLines.push({ kind: 'text', text: '(no entries)' }); - - const pages = []; - for (let i = 0; i < allLines.length; i += maxBodyLines) { - pages.push(allLines.slice(i, i + maxBodyLines)); - } - if (!pages.length) pages.push([{ kind: 'text', text: '(no entries)' }]); - - const objects = []; - const addObj = body => { objects.push(body); return objects.length; }; - - const catalogId = addObj(null); - const pagesId = addObj(null); - const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>'); - const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold >>'); - - let logoXObjId = null; - if (logoBuf && logoDims) { - const colorSpace = logoDims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; - const dict = `<< /Type /XObject /Subtype /Image /Width ${logoDims.w} /Height ${logoDims.h} /ColorSpace ${colorSpace} /BitsPerComponent 8 /Filter /DCTDecode /Length ${logoBuf.length} >>`; - logoXObjId = addObj({ dict, stream: logoBuf }); - } - - const exportDate = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; - const footerLeft = `Generated: ${exportDate}`; - - const pageIds = []; - const contentIds = []; - - pages.forEach((pg, pgIdx) => { - const parts = []; - - if (logoXObjId) { - const logoH = 60; - const logoW = Math.round((logoDims.w / logoDims.h) * logoH); - const logoX = marginX; - const logoY = pageH - headerH + 15; - parts.push(`q\n${logoW} 0 0 ${logoH} ${logoX} ${logoY} cm\n/Logo Do\nQ`); - } - - const titleX = (logoXObjId ? marginX + 80 : marginX); - const titleY = pageH - 45; - parts.push(`BT\n/F2 16 Tf\n${titleX} ${titleY} Td\n(${escapePdf('OASIS - Experience logs')}) Tj\nET`); - const inhabitantPrefix = 'Inhabitant: '; - const inhabitantPrefixW = inhabitantPrefix.length * 5.4; - parts.push(`BT\n/F1 9 Tf\n${titleX} ${titleY - 16} Td\n(${escapePdf(inhabitantPrefix)}) Tj\nET`); - parts.push(`BT\n/F2 9 Tf\n${titleX + inhabitantPrefixW} ${titleY - 16} Td\n(${escapePdf(String(oasisId || ''))}) Tj\nET`); - - parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${pageH - headerH} m\n${pageW - marginX} ${pageH - headerH} l\nS\nQ`); - - let y = bodyTop; - const charW = 6; - for (const ln of pg) { - if (ln.kind === 'header') { - parts.push(`BT\n/F2 10 Tf\n1 0.647 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - } else if (ln.text) { - const segs = splitSegments(ln.text); - let x = marginX; - for (const s of segs) { - if (!s.t) continue; - const color = s.l ? '0 0 1 rg' : '0 0 0 rg'; - parts.push(`BT\n/F1 10 Tf\n${color}\n${x} ${y} Td\n(${escapePdf(s.t)}) Tj\nET`); - x += s.t.length * charW; - } - } - y -= lineH; - } - - parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${footerH + 5} m\n${pageW - marginX} ${footerH + 5} l\nS\nQ`); - parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 10} Td\n(${escapePdf(footerLeft)}) Tj\nET`); - const pageLabel = `Page ${pgIdx + 1} of ${pages.length}`; - const pageLabelW = pageLabel.length * 4.8; - parts.push(`BT\n/F1 8 Tf\n${pageW - marginX - pageLabelW} ${footerH - 10} Td\n(${escapePdf(pageLabel)}) Tj\nET`); - - const content = parts.join('\n'); - const stream = `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}\nendstream`; - const cid = addObj(stream); - contentIds.push(cid); - const pid = addObj(null); - pageIds.push(pid); - }); - - for (let i = 0; i < pageIds.length; i++) { - const resources = logoXObjId - ? `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> /XObject << /Logo ${logoXObjId} 0 R >> >>` - : `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> >>`; - objects[pageIds[i] - 1] = `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${pageW} ${pageH}] /Contents ${contentIds[i]} 0 R /Resources ${resources} >>`; - } - - objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; - objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageIds.map(id => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`; - - const chunks = []; - const offsets = [0]; - let byteLen = 0; - const push = (buf) => { chunks.push(buf); byteLen += buf.length; }; - - push(Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary')); - - for (let i = 0; i < objects.length; i++) { - offsets.push(byteLen); - const obj = objects[i]; - if (obj && typeof obj === 'object' && obj.dict && obj.stream) { - push(Buffer.from(`${i + 1} 0 obj\n${obj.dict}\nstream\n`, 'binary')); - push(obj.stream); - push(Buffer.from('\nendstream\nendobj\n', 'binary')); - } else { - push(Buffer.from(`${i + 1} 0 obj\n${obj}\nendobj\n`, 'binary')); - } - } - - const xrefStart = byteLen; - let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; - for (let i = 1; i <= objects.length; i++) { - xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; - } - xref += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xrefStart}\n%%EOF`; - push(Buffer.from(xref, 'binary')); - - return Buffer.concat(chunks); -} - -module.exports = { buildLogsPdf }; diff --git a/src/backend/long_text.js b/src/backend/long_text.js new file mode 100644 index 0000000..c4095ad --- /dev/null +++ b/src/backend/long_text.js @@ -0,0 +1,195 @@ +const crypto = require('crypto'); + +const CHUNK_TYPE = 'textChunk'; +const TEXT_CAP = 60000; +const CONTENT_BYTES = { plain: 6500, boxed: 4500, sealed: 3300 }; +const MIN_HEAD_BYTES = 200; +const CACHE_MAX = 400; +const KEY_RE = /^%[A-Za-z0-9+/]{43}=?\.sha256$/; +const KEY_SAMPLE = '%' + 'A'.repeat(44) + '.sha256'; + +const byteLength = (s) => Buffer.byteLength(String(s == null ? '' : s), 'utf8'); +const contentBytes = (content) => { try { return byteLength(JSON.stringify(content)); } catch (_) { return Infinity; } }; +const isChunkKey = (k) => typeof k === 'string' && KEY_RE.test(k); +const tooLong = (text) => String(text == null ? '' : text).length > TEXT_CAP; + +const cutAt = (text, maxBytes) => { + let i = 0, bytes = 0; + while (i < text.length) { + const cp = text.codePointAt(i); + const len = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4; + if (bytes + len > maxBytes) break; + bytes += len; + i += cp >= 0x10000 ? 2 : 1; + } + if (i >= text.length) return text.length; + const min = Math.floor(i / 2); + const para = text.lastIndexOf('\n\n', i); + if (para > min) return para + 2; + const line = text.lastIndexOf('\n', i); + if (line > min) return line + 1; + const space = text.lastIndexOf(' ', i); + if (space > min) return space + 1; + return Math.max(i, 1); +}; + +const splitText = (text, maxBytes, firstMax = maxBytes) => { + const s = String(text == null ? '' : text); + const out = []; + let rest = s; + let limit = Math.max(1, firstMax); + while (rest.length) { + const cut = cutAt(rest, limit); + out.push(rest.slice(0, cut)); + rest = rest.slice(cut); + limit = Math.max(1, maxBytes); + } + return out.length ? out : ['']; +}; + +const chunkTemplate = (extra) => ({ type: CHUNK_TYPE, text: '', index: 999, total: 999, group: 'g'.repeat(16), ...extra }); +const chunkBudget = (extra, maxBytes) => maxBytes - contentBytes(chunkTemplate(extra)); +const headBudget = (content, field, maxBytes, count) => maxBytes - contentBytes({ ...content, [field]: '', chunks: new Array(count).fill(KEY_SAMPLE) }); + +const needsChunking = (content, maxBytes = CONTENT_BYTES.plain) => contentBytes(content) > maxBytes; + +const planPieces = (content, field, { maxBytes = CONTENT_BYTES.plain, extra = {} } = {}) => { + const text = String(content[field] == null ? '' : content[field]); + if (tooLong(text)) throw new Error('Text too long'); + if (!needsChunking(content, maxBytes)) return null; + const perChunk = chunkBudget(extra, maxBytes); + if (perChunk < MIN_HEAD_BYTES) throw new Error('Text does not fit'); + let count = 0; + let pieces = null; + for (let guard = 0; guard < 12; guard++) { + const head = headBudget(content, field, maxBytes, count); + if (head < MIN_HEAD_BYTES) throw new Error('Text does not fit'); + pieces = splitText(text, perChunk, head); + if (pieces.length - 1 <= count) return pieces; + count = pieces.length - 1; + } + return pieces; +}; + +const publishChunks = async (pieces, publish, { extra = {}, group } = {}) => { + const g = group || crypto.randomBytes(8).toString('hex'); + const total = pieces.length; + const keys = []; + for (let i = 0; i < pieces.length; i++) { + const r = await publish({ type: CHUNK_TYPE, text: pieces[i], index: i + 1, total, group: g, ...extra }); + const key = r && typeof r === 'object' ? r.key : r; + if (!isChunkKey(key)) throw new Error('Chunk publish failed'); + keys.push(key); + } + return keys; +}; + +const chunkContent = async (content, field, { maxBytes = CONTENT_BYTES.plain, extra = {}, publish } = {}) => { + const base = { ...content }; + delete base.chunks; + const pieces = planPieces(base, field, { maxBytes, extra }); + if (!pieces) return base; + const keys = await publishChunks(pieces.slice(1), publish, { extra }); + return { ...base, [field]: pieces[0], chunks: keys }; +}; + +const chunkOf = (author, c) => { + if (!c || typeof c !== 'object' || typeof c.text !== 'string') return null; + if (c.type !== CHUNK_TYPE && c.k !== CHUNK_TYPE) return null; + return { author, text: c.text, index: Number(c.index) || 0, total: Number(c.total) || 0, group: String(c.group || '') }; +}; + +const joinText = (head, keys, lookup, author) => { + let out = String(head == null ? '' : head); + if (!Array.isArray(keys) || !keys.length) return out; + let group = null; + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + if (!isChunkKey(k)) break; + const ch = lookup(k); + if (!ch || (author && ch.author !== author)) break; + if (group === null) group = ch.group; else if (ch.group !== group) break; + if (ch.index !== i + 1 || ch.total !== keys.length) break; + out += ch.text; + } + return out; +}; + +const hasChunks = (content) => !!content && Array.isArray(content.chunks) && content.chunks.length > 0; + +const resolveField = (content, field, author, lookup) => { + if (!hasChunks(content)) return content; + return { ...content, [field]: joinText(content[field], content.chunks, lookup, author) }; +}; + +const indexChunks = (messages, decode) => { + const index = new Map(); + for (const m of messages || []) { + const v = m && m.value; + if (!v || !m.key) continue; + const raw = v.content; + const c = decode ? decode(raw, m) : raw; + const ch = chunkOf(v.author, c); + if (ch) index.set(m.key, ch); + } + return index; +}; + +const lookupIn = (index) => (k) => index.get(k) || null; + +const caches = new WeakMap(); +const cacheFor = (ssb) => { + let c = caches.get(ssb); + if (!c) { c = new Map(); caches.set(ssb, c); } + return c; +}; +const remember = (cache, key, value) => { + if (cache.has(key)) cache.delete(key); + cache.set(key, value); + while (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value); + return value; +}; + +const fetchChunk = async (ssb, key, unbox) => { + const cache = cacheFor(ssb); + if (cache.has(key)) { const v = cache.get(key); cache.delete(key); cache.set(key, v); return v; } + const raw = await new Promise((resolve) => { try { ssb.get(key, (err, m) => resolve(err ? null : m)); } catch (_) { resolve(null); } }); + if (!raw) return null; + let content = raw.content; + if (typeof content === 'string' && unbox) { try { content = unbox(content); } catch (_) { content = null; } } + const ch = chunkOf(raw.author, content); + return ch ? remember(cache, key, ch) : null; +}; + +const joinChunks = async (ssb, content, author, { field = 'text', unbox = null } = {}) => { + if (!hasChunks(content)) return content; + const found = new Map(); + for (const k of content.chunks) { + if (!isChunkKey(k)) break; + const ch = await fetchChunk(ssb, k, unbox); + if (!ch) break; + found.set(k, ch); + } + return resolveField(content, field, author, lookupIn(found)); +}; + +module.exports = { + CHUNK_TYPE, + TEXT_CAP, + CONTENT_BYTES, + byteLength, + contentBytes, + tooLong, + splitText, + needsChunking, + planPieces, + publishChunks, + chunkContent, + chunkOf, + joinText, + hasChunks, + resolveField, + indexChunks, + lookupIn, + joinChunks +}; diff --git a/src/backend/mobile_layer.js b/src/backend/mobile_layer.js new file mode 100644 index 0000000..611e3c7 --- /dev/null +++ b/src/backend/mobile_layer.js @@ -0,0 +1,40 @@ +"use strict"; +// Lo que la interfaz de la rama necesita del backend, fuera de backend.js. +// +// Prepara lo que leen la barra de arriba y el lanzador de hive_views.js: el filtro +// Personal/Social, que el lanzador no va en la portada y el avatar propio. Lo +// monta el router propio (fork_routes.js) delante de las rutas del dev. Nada de +// esto corre en escritorio. Ver GUIA_DE_DESARROLLO.md. +// +// Lo demas que el movil necesita del backend (modulos que no van, rutas que se +// cortan, retoques del HTML, descargas de la copia de seguridad) lo hace el propio +// dev en la version de movil de su backend.js, la que va dentro de su APK. + +const NULL_IMG = '&' + '0'.repeat(43) + '=.sha256'; +const isMobile = () => process.env.OASIS_MOBILE === '1'; +const isPage = (p) => !/^\/(assets|image|blob|qr|c)(\/|$)/.test(p); + +module.exports = ({ getViewerId, about }) => { + let avatarAt = 0; + const refreshAvatar = async () => { + avatarAt = Date.now(); + try { + const me = getViewerId(); + const img = me ? await about.image(me) : null; + if (typeof img === 'string' && img[0] === '&' && img !== NULL_IMG) global.__OASIS_SELF_AVATAR__ = `/image/256/${encodeURIComponent(img)}`; + else if (typeof img === 'string' && img[0] === '/') global.__OASIS_SELF_AVATAR__ = img; + else global.__OASIS_SELF_AVATAR__ = null; + } catch (_) {} + }; + + return async (ctx, next) => { + if (!isMobile() || !isPage(ctx.path)) return next(); + global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : ''; + // el lanzador ya no va en la portada: la portada es la actividad y los modulos + // estan en la hoja de la pestaña Menu (docs/ESTUDIO_NAVEGACION.md) + global.__OASIS_SHOW_HIVE__ = false; + if (!avatarAt) await refreshAvatar(); + else if (Date.now() - avatarAt > 60000) refreshAvatar(); + return next(); + }; +}; diff --git a/src/backend/opinion_categories.js b/src/backend/opinion_categories.js index a6e314d..0779724 100644 --- a/src/backend/opinion_categories.js +++ b/src/backend/opinion_categories.js @@ -46,5 +46,40 @@ const all = [...positive, ...constructive, ...moderation]; all.positive = positive; all.constructive = constructive; all.moderation = moderation; +const karmaNeutral = ["adultOnly", "nsfw", "triggering", "duplicate", "offTopic", "needsSources"]; +const karmaModeration = [...moderation.filter(c => !karmaNeutral.includes(c)), "propaganda"]; +all.karma = { + positive, + neutral: karmaNeutral, + constructive: constructive.filter(c => !karmaNeutral.includes(c) && !karmaModeration.includes(c)), + moderation: karmaModeration +}; +all.messageTypes = { + bookmark: 'bookmarkOpinion', + votes: 'votesOpinion', + poll: 'pollOpinion', + transfer: 'transferOpinion', + feed: 'feedOpinion', + post: 'blogOpinion', + image: 'imageOpinion', + audio: 'audioOpinion', + video: 'videoOpinion', + document: 'documentOpinion', + torrent: 'torrentOpinion', + file: 'fileOpinion', + industry: 'industryOpinion', + project: 'projectOpinion', + report: 'reportOpinion', + task: 'taskOpinion', + event: 'eventOpinion', + shopProduct: 'shopOpinion', + housing: 'housingOpinion', + market: 'marketOpinion', + schoolCourse: 'schoolOpinion', + podcast: 'podcastOpinion', + podcastEpisode: 'podcastOpinion', + campaign: 'campaignOpinion', + logisticsRoute: 'logisticsOpinion' +}; module.exports = all; diff --git a/src/backend/pdf.js b/src/backend/pdf.js new file mode 100644 index 0000000..10da631 --- /dev/null +++ b/src/backend/pdf.js @@ -0,0 +1,1485 @@ +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); +const { plainText } = require('./renderStyledText'); +const { MAP_W, MAP_H, makeView, fitView, toScreen, tileCoverage } = require('../maps/map_renderer'); + +const LOGO_PATH = path.join(__dirname, '..', 'client', 'assets', 'images', 'snh-oasis.jpg'); +const TILES_DIR = path.join(__dirname, '..', 'maps', 'tiles'); + +const WIN_ANSI_HIGH = { + '\u20AC': '\x80', '\u201A': '\x82', '\u0192': '\x83', '\u201E': '\x84', + '\u2026': '\x85', '\u2020': '\x86', '\u2021': '\x87', '\u02C6': '\x88', + '\u2030': '\x89', '\u0160': '\x8A', '\u2039': '\x8B', '\u0152': '\x8C', + '\u017D': '\x8E', '\u2018': '\x91', '\u2019': '\x92', '\u201C': '\x93', + '\u201D': '\x94', '\u2022': '\x95', '\u2013': '\x96', '\u2014': '\x97', + '\u02DC': '\x98', '\u2122': '\x99', '\u0161': '\x9A', '\u203A': '\x9B', + '\u0153': '\x9C', '\u017E': '\x9E', '\u0178': '\x9F' +}; + +const escapePdf = s => String(s == null ? '' : s) + .replace(/[\r\n\t]+/g, ' ') + .replace(/[\u0152\u0153\u0160\u0161\u0178\u017D\u017E\u0192\u02C6\u02DC\u2013\u2014\u2018\u2019\u201A\u201C\u201D\u201E\u2020\u2021\u2022\u2026\u2030\u2039\u203A\u20AC\u2122]/g, + (c) => WIN_ANSI_HIGH[c] || '?') + .replace(/[^\x20-\xFF\x80-\x9F]/g, '?') + .replace(/\\/g, '\\\\') + .replace(/\(/g, '\\(') + .replace(/\)/g, '\\)'); + +const wrap = (txt, max = 82) => { + const out = []; + for (const raw of String(txt == null ? '' : txt).split('\n')) { + let line = raw; + while (line.length > max) { + let cut = line.lastIndexOf(' ', max); + if (cut <= 0) cut = max; + out.push(line.slice(0, cut)); + line = line.slice(cut).replace(/^\s+/, ''); + } + out.push(line); + } + return out; +}; + +const readJpegDims = (buf) => { + let i = 2; + while (i < buf.length) { + if (buf[i] !== 0xFF) return null; + const marker = buf[i + 1]; + if (marker === 0xD8 || marker === 0xD9) { i += 2; continue; } + const len = buf.readUInt16BE(i + 2); + if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) { + const h = buf.readUInt16BE(i + 5); + const w = buf.readUInt16BE(i + 7); + const c = buf[i + 9]; + return { w, h, c }; + } + i += 2 + len; + } + return null; +}; + +const decodePng = (buf) => { + if (!Buffer.isBuffer(buf) || buf.length < 33 || buf.readUInt32BE(0) !== 0x89504e47) return null; + let pos = 8; + let w = 0, h = 0, depth = 0, colorType = 0, interlace = 0; + let palette = null; + const idat = []; + while (pos + 8 <= buf.length) { + const len = buf.readUInt32BE(pos); + const type = buf.toString('ascii', pos + 4, pos + 8); + const data = buf.slice(pos + 8, pos + 8 + len); + if (type === 'IHDR') { w = data.readUInt32BE(0); h = data.readUInt32BE(4); depth = data[8]; colorType = data[9]; interlace = data[12]; } + else if (type === 'PLTE') palette = data; + else if (type === 'IDAT') idat.push(data); + else if (type === 'IEND') break; + pos += 12 + len; + } + if (!w || !h || depth !== 8 || interlace !== 0 || !idat.length) return null; + if (w > 8192 || h > 8192 || w * h > 20 * 1024 * 1024) return null; + const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[colorType]; + if (!channels) return null; + let raw; + try { raw = zlib.inflateSync(Buffer.concat(idat), { maxOutputLength: (w * channels + 1) * h }); } catch (_) { return null; } + const stride = w * channels; + const out = Buffer.alloc(w * h * 3); + let prev = Buffer.alloc(stride); + let inPos = 0; + for (let y = 0; y < h; y++) { + const filter = raw[inPos++]; + const cur = Buffer.from(raw.slice(inPos, inPos + stride)); + inPos += stride; + for (let i = 0; i < stride; i++) { + const a = i >= channels ? cur[i - channels] : 0; + const b = prev[i]; + const c = i >= channels ? prev[i - channels] : 0; + let v = cur[i]; + if (filter === 1) v += a; + else if (filter === 2) v += b; + else if (filter === 3) v += Math.floor((a + b) / 2); + else if (filter === 4) { const p = a + b - c; const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c); v += (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c); } + cur[i] = v & 0xff; + } + for (let x = 0; x < w; x++) { + const o = (y * w + x) * 3; + const s = x * channels; + if (colorType === 2 || colorType === 6) { out[o] = cur[s]; out[o + 1] = cur[s + 1]; out[o + 2] = cur[s + 2]; } + else if (colorType === 0 || colorType === 4) { out[o] = out[o + 1] = out[o + 2] = cur[s]; } + else if (colorType === 3 && palette) { const pi = cur[s] * 3; out[o] = palette[pi]; out[o + 1] = palette[pi + 1]; out[o + 2] = palette[pi + 2]; } + if (colorType === 6 || colorType === 4) { + const alpha = cur[s + channels - 1] / 255; + out[o] = Math.round(out[o] * alpha + 255 * (1 - alpha)); + out[o + 1] = Math.round(out[o + 1] * alpha + 255 * (1 - alpha)); + out[o + 2] = Math.round(out[o + 2] * alpha + 255 * (1 - alpha)); + } + } + prev = cur; + } + return { w, h, rgb: out }; +}; + +const imageXObject = (buf) => { + if (!Buffer.isBuffer(buf) || buf.length < 4) return null; + if (buf[0] === 0xff && buf[1] === 0xd8) { + const dims = readJpegDims(buf); + if (!dims) return null; + const cs = dims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; + return { w: dims.w, h: dims.h, dict: `<< /Type /XObject /Subtype /Image /Width ${dims.w} /Height ${dims.h} /ColorSpace ${cs} /BitsPerComponent 8 /Filter /DCTDecode /Length ${buf.length} >>`, stream: buf }; + } + const png = decodePng(buf); + if (!png) return null; + const compressed = zlib.deflateSync(png.rgb); + return { w: png.w, h: png.h, dict: `<< /Type /XObject /Subtype /Image /Width ${png.w} /Height ${png.h} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length ${compressed.length} >>`, stream: compressed }; +}; + +const rasterXObject = (raster) => { + if (!raster || !raster.w || !raster.h || !Buffer.isBuffer(raster.rgb) || raster.rgb.length !== raster.w * raster.h * 3) return null; + const compressed = zlib.deflateSync(raster.rgb); + return { w: raster.w, h: raster.h, dict: `<< /Type /XObject /Subtype /Image /Width ${raster.w} /Height ${raster.h} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Interpolate false /Filter /FlateDecode /Length ${compressed.length} >>`, stream: compressed }; +}; + +const jpegXObject = (buf) => { + if (!Buffer.isBuffer(buf) || buf[0] !== 0xff || buf[1] !== 0xd8) return null; + const dims = readJpegDims(buf); + if (!dims) return null; + return { w: dims.w, h: dims.h, dict: `<< /Type /XObject /Subtype /Image /Width ${dims.w} /Height ${dims.h} /ColorSpace ${dims.c === 1 ? '/DeviceGray' : '/DeviceRGB'} /BitsPerComponent 8 /Filter /DCTDecode /Length ${buf.length} >>`, stream: buf }; +}; + +const tileXObject = (t) => { + try { return jpegXObject(fs.readFileSync(path.join(TILES_DIR, String(t.z), `${t.x}_${t.y}.jpg`))); } catch (_) { return null; } +}; + +const MAP_PIN_R = 4.5; +const MAP_LABEL_MAX = 28; + +const mapBlock = (pins) => { + const pts = (Array.isArray(pins) ? pins : []).filter(p => p && isFinite(Number(p.lat)) && isFinite(Number(p.lng))); + if (!pts.length) return null; + const fit = fitView(pts, { singleZoom: 8 }); + const view = makeView(fit.lat, fit.lng, fit.zoom); + const tiles = tileCoverage(view).map(t => ({ ...t, xo: tileXObject(t) })).filter(t => t.xo); + if (!tiles.length) return null; + const marks = pts.map(p => { + const s = toScreen(view, Number(p.lat), Number(p.lng)); + const line = String(p.label || '').split(/\r?\n/)[0].trim(); + return { x: s.x, y: s.y, main: !!p.main, label: line.length > MAP_LABEL_MAX ? line.slice(0, MAP_LABEL_MAX - 1) + '…' : line }; + }); + return { kind: 'map', w: MAP_W, h: MAP_H, zoom: view.zoom, tiles, pins: marks }; +}; + +const circlePath = (cx, cy, r) => { + const k = 0.5523 * r; + return `${cx + r} ${cy} m ${cx + r} ${cy + k} ${cx + k} ${cy + r} ${cx} ${cy + r} c ${cx - k} ${cy + r} ${cx - r} ${cy + k} ${cx - r} ${cy} c ${cx - r} ${cy - k} ${cx - k} ${cy - r} ${cx} ${cy - r} c ${cx + k} ${cy - r} ${cx + r} ${cy - k} ${cx + r} ${cy} c`; +}; + +const mapContent = (ln, x0, y0, box, tileIds) => { + const s = box.w / ln.w; + const f = v => Math.round(v * 100) / 100; + const parts = [`q ${f(x0)} ${f(y0)} ${f(box.w)} ${f(box.h)} re W n`]; + for (const t of ln.tiles) { + parts.push(`q ${f(t.size * s + 0.5)} 0 0 ${f(t.size * s + 0.5)} ${f(x0 + t.sx * s)} ${f(y0 + (ln.h - t.sy - t.size) * s)} cm /Im${tileIds.get(t)} Do Q`); + } + const at = p => ({ x: x0 + p.x * s, y: y0 + (ln.h - p.y) * s }); + for (const p of ln.pins) { + const c = at(p); + parts.push(`q ${p.main ? '0.95 0.55 0' : '0.85 0.15 0.15'} rg 1 1 1 RG 1 w ${circlePath(f(c.x), f(c.y), MAP_PIN_R)} h B Q`); + } + for (const p of ln.pins) { + if (!p.label) continue; + const c = at(p); + const tw = p.label.length * 4.2; + parts.push(`q 1 1 1 rg ${f(c.x + MAP_PIN_R + 2)} ${f(c.y - 5)} ${f(tw + 4)} 10 re f Q`); + parts.push(`BT /F1 7 Tf 0 0 0 rg ${f(c.x + MAP_PIN_R + 4)} ${f(c.y - 2.5)} Td (${escapePdf(p.label)}) Tj ET`); + } + parts.push(`q 0.4 0.4 0.4 RG 0.5 w ${f(x0)} ${f(y0)} ${f(box.w)} ${f(box.h)} re S Q`); + parts.push('Q'); + return parts.join('\n'); +}; + +const flattenSections = (sections) => { + const lines = []; + for (const s of Array.isArray(sections) ? sections : []) { + if (!s) continue; + if (s.kind === 'image') { + const xo = s.raster ? rasterXObject(s.raster) : imageXObject(s.buffer); + if (xo) lines.push({ kind: 'image', xo, caption: s.caption || '', maxW: s.maxW, maxH: s.maxH }); + else if (s.caption) lines.push({ kind: 'kv', text: `[image: ${s.caption}]` }); + } else if (s.kind === 'map') { + const block = mapBlock(s.pins); + if (block) lines.push(block); + } else if (s.kind === 'kv') { + const txt = `${s.label}: ${s.value == null ? '' : s.value}`; + for (const w of wrap(txt, 82)) lines.push({ kind: 'kv', text: w }); + } else if (s.kind === 'text') { + for (const w of wrap(plainText(s.text), 82)) lines.push({ kind: 'kv', text: w }); + } else { + lines.push({ kind: s.kind, text: s.text }); + } + } + return lines; +}; + +const licenseFooter = (id) => { + const l = require('../views/clearnet_view').licenseInfo(id); + return l ? `License: ${l.name}` : ''; +}; + +function buildDocumentPdf({ title, issuedToLabel, issuedTo, sections, license = '' } = {}) { + const footerNote = licenseFooter(license); + const pageW = 612; + const pageH = 792; + const marginX = 50; + const headerH = 90; + const footerH = 40; + const bodyTop = pageH - headerH - 24; + const bodyBottom = footerH + 10; + const lineH = 14; + + let logoBuf = null; + let logoDims = null; + try { + logoBuf = fs.readFileSync(LOGO_PATH); + logoDims = readJpegDims(logoBuf); + } catch (_) {} + + const lines = flattenSections(sections); + const maxImgW = 240; + const maxImgH = 160; + const imageBox = (ln) => { + const scale = Math.min(1, (ln.maxW || maxImgW) / ln.xo.w, (ln.maxH || maxImgH) / ln.xo.h); + return { w: Math.round(ln.xo.w * scale), h: Math.round(ln.xo.h * scale) }; + }; + const mapBox = (ln) => { + const scale = Math.min(1, (pageW - 2 * marginX) / ln.w); + return { w: Math.round(ln.w * scale), h: Math.round(ln.h * scale) }; + }; + const heightOf = (ln) => ln.kind === 'image' ? imageBox(ln).h + lineH : ln.kind === 'map' ? mapBox(ln).h + lineH : lineH; + + const bodyHeight = bodyTop - bodyBottom; + const pages = []; + let current = []; + let used = 0; + for (const ln of lines) { + const hgt = Math.min(heightOf(ln), bodyHeight); + if (used + hgt > bodyHeight && current.length) { pages.push(current); current = []; used = 0; } + current.push(ln); + used += hgt; + } + pages.push(current); + + const objects = []; + const addObj = body => { objects.push(body); return objects.length; }; + + const catalogId = addObj(null); + const pagesId = addObj(null); + const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>'); + const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>'); + let logoXObjId = null; + if (logoBuf && logoDims) { + const cs = logoDims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; + const dict = `<< /Type /XObject /Subtype /Image /Width ${logoDims.w} /Height ${logoDims.h} /ColorSpace ${cs} /BitsPerComponent 8 /Filter /DCTDecode /Length ${logoBuf.length} >>`; + logoXObjId = addObj({ dict, stream: logoBuf }); + } + + const imageIds = new Map(); + for (const ln of lines) { + if (ln.kind === 'map') { + for (const t of ln.tiles) imageIds.set(t, addObj({ dict: t.xo.dict, stream: t.xo.stream })); + continue; + } + if (ln.kind !== 'image') continue; + const id = addObj({ dict: ln.xo.dict, stream: ln.xo.stream }); + imageIds.set(ln, id); + } + + const exportDate = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; + const footerLeft = `Generated: ${exportDate}`; + + const pageIds = []; + const contentIds = []; + + pages.forEach((pg, pgIdx) => { + const parts = []; + if (logoXObjId) { + const logoH = 60; + const logoW = Math.round((logoDims.w / logoDims.h) * logoH); + parts.push(`q\n${logoW} 0 0 ${logoH} ${marginX} ${pageH - headerH + 15} cm\n/Logo Do\nQ`); + } + const titleX = (logoXObjId ? marginX + 80 : marginX); + const titleY = pageH - 45; + parts.push(`BT\n/F2 16 Tf\n${titleX} ${titleY} Td\n(${escapePdf(title || 'OASIS')}) Tj\nET`); + if (issuedTo) { + const prefix = `${issuedToLabel || 'Issued to'}: `; + const prefixW = prefix.length * 5.4; + parts.push(`BT\n/F1 9 Tf\n${titleX} ${titleY - 16} Td\n(${escapePdf(prefix)}) Tj\nET`); + parts.push(`BT\n/F2 9 Tf\n${titleX + prefixW} ${titleY - 16} Td\n(${escapePdf(String(issuedTo))}) Tj\nET`); + } + + parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${pageH - headerH} m\n${pageW - marginX} ${pageH - headerH} l\nS\nQ`); + + let y = bodyTop; + for (const ln of pg) { + if (ln.kind === 'title') { + parts.push(`BT\n/F2 14 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + } else if (ln.kind === 'subtitle') { + parts.push(`BT\n/F1 11 Tf\n0.2 0.2 0.2 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + } else if (ln.kind === 'section') { + parts.push(`BT\n/F2 11 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + parts.push(`q\n0 0 0 RG\n0.5 w\n${marginX} ${y - 3} m\n${pageW - marginX} ${y - 3} l\nS\nQ`); + } else if (ln.kind === 'kv') { + parts.push(`BT\n/F1 10 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + } else if (ln.kind === 'image') { + const box = imageBox(ln); + parts.push(`q\n${box.w} 0 0 ${box.h} ${marginX} ${y - box.h + lineH - 4} cm\n/Im${imageIds.get(ln)} Do\nQ`); + y -= box.h; + } else if (ln.kind === 'map') { + const box = mapBox(ln); + parts.push(mapContent(ln, marginX, y - box.h + lineH - 4, box, imageIds)); + y -= box.h; + } + y -= lineH; + } + + parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${footerH + 5} m\n${pageW - marginX} ${footerH + 5} l\nS\nQ`); + parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 10} Td\n(${escapePdf(footerLeft)}) Tj\nET`); + if (footerNote) parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 21} Td\n(${escapePdf(footerNote)}) Tj\nET`); + const pageLabel = `Page ${pgIdx + 1} of ${pages.length}`; + const pageLabelW = pageLabel.length * 4.8; + parts.push(`BT\n/F1 8 Tf\n${pageW - marginX - pageLabelW} ${footerH - 10} Td\n(${escapePdf(pageLabel)}) Tj\nET`); + + const content = parts.join('\n'); + const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`; + const cid = addObj(stream); + contentIds.push(cid); + const pid = addObj(null); + pageIds.push(pid); + }); + + const imageRefs = Array.from(imageIds.values()).map(id => `/Im${id} ${id} 0 R`).join(' '); + for (let i = 0; i < pageIds.length; i++) { + const xobjects = [logoXObjId ? `/Logo ${logoXObjId} 0 R` : '', imageRefs].filter(Boolean).join(' '); + const resources = xobjects + ? `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> /XObject << ${xobjects} >> >>` + : `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> >>`; + objects[pageIds[i] - 1] = `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${pageW} ${pageH}] /Contents ${contentIds[i]} 0 R /Resources ${resources} >>`; + } + objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; + objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageIds.map(id => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`; + + const chunks = []; + const offsets = [0]; + let byteLen = 0; + const push = (buf) => { chunks.push(buf); byteLen += buf.length; }; + push(Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary')); + for (let i = 0; i < objects.length; i++) { + offsets.push(byteLen); + const obj = objects[i]; + if (obj && typeof obj === 'object' && obj.dict && obj.stream) { + push(Buffer.from(`${i + 1} 0 obj\n${obj.dict}\nstream\n`, 'binary')); + push(obj.stream); + push(Buffer.from('\nendstream\nendobj\n', 'binary')); + } else { + push(Buffer.from(`${i + 1} 0 obj\n${obj}\nendobj\n`, 'binary')); + } + } + const xrefStart = byteLen; + let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; + for (let i = 1; i <= objects.length; i++) xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; + xref += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xrefStart}\n%%EOF`; + push(Buffer.from(xref, 'binary')); + return Buffer.concat(chunks); +} + +const fmtDate = v => { + if (!v) return ''; + const d = new Date(v); + return isNaN(d.getTime()) ? String(v) : d.toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; +}; + +const fmtDay = v => { + if (!v) return ''; + const d = new Date(v); + return isNaN(d.getTime()) ? String(v) : d.toISOString().slice(0, 10); +}; + +const asList = v => (Array.isArray(v) ? v : []); +const txt = v => (v == null ? '' : String(v)); + +const humanLabel = (key) => String(key || '') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/^./, c => c.toUpperCase()); + +const pushMeta = (out, item) => { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'METADATA' }); + if (item.id) out.push({ kind: 'kv', label: 'Content ID', value: item.id }); + const author = item.author || item.organizer || item.createdBy || item.from || ''; + if (author) out.push({ kind: 'kv', label: 'Author', value: author }); + if (item.createdAt) out.push({ kind: 'kv', label: 'Created At', value: fmtDate(item.createdAt) }); + if (item.updatedAt) out.push({ kind: 'kv', label: 'Updated At', value: fmtDate(item.updatedAt) }); +}; + +const pushOpinions = (out, item) => { + const op = item.opinions && typeof item.opinions === 'object' ? item.opinions : {}; + const entries = Object.entries(op).filter(([, n]) => Number(n) > 0); + if (!entries.length) return; + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'OPINIONS' }); + for (const [cat, n] of entries) out.push({ kind: 'kv', label: humanLabel(cat), value: String(n) }); +}; + +const pushTags = (out, item) => { + const tags = asList(item.tags).filter(Boolean); + if (tags.length) out.push({ kind: 'kv', label: 'Tags', value: tags.join(', ') }); +}; + +const reportSections = (report) => { + const out = []; + out.push({ kind: 'title', text: `Report: ${txt(report.title) || '-'}` }); + out.push({ kind: 'blank' }); + + out.push({ kind: 'section', text: 'CLASSIFICATION' }); + out.push({ kind: 'kv', label: 'Category', value: txt(report.category).toUpperCase() }); + out.push({ kind: 'kv', label: 'Severity', value: txt(report.severity).toUpperCase() }); + out.push({ kind: 'kv', label: 'Status', value: txt(report.status).toUpperCase() }); + pushTags(out, report); + + if (txt(report.description).trim()) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DESCRIPTION' }); + out.push({ kind: 'text', text: report.description }); + } + + const tpl = report.template && typeof report.template === 'object' ? report.template : {}; + const tplEntries = Object.entries(tpl).filter(([, v]) => txt(v).trim()); + if (tplEntries.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DETAILS' }); + for (const [k, v] of tplEntries) out.push({ kind: 'kv', label: humanLabel(k), value: v }); + } + + const confirmations = asList(report.confirmations); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'CONFIRMATIONS' }); + out.push({ kind: 'kv', label: 'Confirmed', value: String(confirmations.length) }); + for (const c of confirmations) out.push({ kind: 'kv', label: 'Confirmed by', value: c }); + + pushOpinions(out, report); + pushMeta(out, report); + return out; +}; + +const voteSections = (vote) => { + const out = []; + out.push({ kind: 'title', text: `Votation: ${txt(vote.question) || '-'}` }); + out.push({ kind: 'blank' }); + + out.push({ kind: 'section', text: 'TERMS' }); + out.push({ kind: 'kv', label: 'Status', value: txt(vote.status).toUpperCase() }); + if (vote.deadline) out.push({ kind: 'kv', label: 'Deadline', value: fmtDate(vote.deadline) }); + pushTags(out, vote); + + const votes = vote.votes && typeof vote.votes === 'object' ? vote.votes : {}; + const total = Number(vote.totalVotes) || asList(vote.voters).length; + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'RESULTS' }); + out.push({ kind: 'kv', label: 'Total votes', value: String(total) }); + for (const [choice, n] of Object.entries(votes)) { + const count = Number(n) || 0; + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + out.push({ kind: 'kv', label: choice, value: `${count} (${pct}%)` }); + } + + pushOpinions(out, vote); + pushMeta(out, vote); + return out; +}; + +const eventSections = (event) => { + const out = []; + out.push({ kind: 'title', text: `Event: ${txt(event.title) || '-'}` }); + out.push({ kind: 'blank' }); + + out.push({ kind: 'section', text: 'SCHEDULE' }); + out.push({ kind: 'kv', label: 'Date', value: fmtDate(event.date) }); + out.push({ kind: 'kv', label: 'Status', value: txt(event.status).toUpperCase() }); + if (event.location) out.push({ kind: 'kv', label: 'Location', value: event.location }); + if (event.mapUrl) out.push({ kind: 'kv', label: 'Map', value: event.mapUrl }); + if (Number(event.price) > 0) out.push({ kind: 'kv', label: 'Price', value: `${Number(event.price)} ECO` }); + out.push({ kind: 'kv', label: 'Privacy', value: txt(event.isPublic).toUpperCase() }); + if (event.url) out.push({ kind: 'kv', label: 'Url', value: event.url }); + pushTags(out, event); + + if (txt(event.description).trim()) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DESCRIPTION' }); + out.push({ kind: 'text', text: event.description }); + } + + const attendees = asList(event.attendees); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'ATTENDEES' }); + out.push({ kind: 'kv', label: 'Total', value: String(attendees.length) }); + for (const a of attendees) out.push({ kind: 'kv', label: 'Attendee', value: a }); + + pushOpinions(out, event); + pushMeta(out, event); + return out; +}; + +const taskSections = (task) => { + const out = []; + out.push({ kind: 'title', text: `Task: ${txt(task.title) || '-'}` }); + out.push({ kind: 'blank' }); + + out.push({ kind: 'section', text: 'SCHEDULE' }); + out.push({ kind: 'kv', label: 'Status', value: txt(task.status).toUpperCase() }); + out.push({ kind: 'kv', label: 'Priority', value: txt(task.priority).toUpperCase() }); + if (task.startTime) out.push({ kind: 'kv', label: 'Starts', value: fmtDate(task.startTime) }); + if (task.endTime) out.push({ kind: 'kv', label: 'Ends', value: fmtDate(task.endTime) }); + if (task.location) out.push({ kind: 'kv', label: 'Location', value: task.location }); + out.push({ kind: 'kv', label: 'Privacy', value: txt(task.isPublic).toUpperCase() }); + pushTags(out, task); + + if (txt(task.description).trim()) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DESCRIPTION' }); + out.push({ kind: 'text', text: task.description }); + } + + const assignees = asList(task.assignees); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'ASSIGNEES' }); + out.push({ kind: 'kv', label: 'Total', value: String(assignees.length) }); + for (const a of assignees) out.push({ kind: 'kv', label: 'Assignee', value: a }); + + pushOpinions(out, task); + pushMeta(out, task); + return out; +}; + +const calendarSections = (calendar, extra = {}) => { + const dates = asList(extra.dates); + const notesByDate = extra.notesByDate && typeof extra.notesByDate === 'object' ? extra.notesByDate : {}; + + const out = []; + out.push({ kind: 'title', text: `Calendar: ${txt(calendar.title) || '-'}` }); + out.push({ kind: 'blank' }); + + out.push({ kind: 'section', text: 'SUMMARY' }); + out.push({ kind: 'kv', label: 'Status', value: calendar.isClosed ? 'CLOSED' : txt(calendar.status).toUpperCase() }); + if (calendar.deadline) out.push({ kind: 'kv', label: 'Deadline', value: fmtDate(calendar.deadline) }); + if (calendar.mapUrl) out.push({ kind: 'kv', label: 'Map', value: calendar.mapUrl }); + out.push({ kind: 'kv', label: 'Participants', value: String(asList(calendar.participants).length) }); + out.push({ kind: 'kv', label: 'Dates', value: String(dates.length) }); + const noteTotal = Object.values(notesByDate).reduce((n, arr) => n + asList(arr).length, 0); + out.push({ kind: 'kv', label: 'Notes', value: String(noteTotal) }); + pushTags(out, calendar); + + const participants = asList(calendar.participants); + if (participants.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'PARTICIPANTS' }); + for (const p of participants) out.push({ kind: 'kv', label: 'Participant', value: p }); + } + + if (dates.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DATES' }); + for (const d of dates) { + const day = fmtDay(d && d.date) || txt(d && d.date); + out.push({ kind: 'kv', label: day, value: txt(d && d.label) }); + for (const note of asList(notesByDate[d && d.key])) { + const noteText = txt(note && note.text); + if (noteText) out.push({ kind: 'text', text: ` - ${noteText}` }); + } + } + } + + pushMeta(out, calendar); + return out; +}; + +const cvSections = (cv) => { + const out = []; + out.push({ kind: 'title', text: `Curriculum: ${txt(cv.name) || txt(cv.author) || '-'}` }); + out.push({ kind: 'blank' }); + + out.push({ kind: 'section', text: 'PROFILE' }); + if (cv.location) out.push({ kind: 'kv', label: 'Location', value: cv.location }); + if (cv.status) out.push({ kind: 'kv', label: 'Status', value: txt(cv.status).toUpperCase() }); + if (cv.preferences) out.push({ kind: 'kv', label: 'Preferences', value: txt(cv.preferences).toUpperCase() }); + if (cv.languages) out.push({ kind: 'kv', label: 'Languages', value: cv.languages }); + + if (txt(cv.description).trim()) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DESCRIPTION' }); + out.push({ kind: 'text', text: cv.description }); + } + + const blocks = [ + ['PERSONAL', cv.personalExperiences, cv.personalSkills], + ['EDUCATION', cv.educationExperiences, cv.educationalSkills], + ['PROFESSIONAL', cv.professionalExperiences, cv.professionalSkills], + ['OASIS', cv.oasisExperiences, cv.oasisSkills] + ]; + + for (const [heading, experiences, skills] of blocks) { + const skillList = asList(skills).filter(Boolean); + const body = txt(experiences).trim(); + if (!body && !skillList.length) continue; + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: heading }); + if (body) out.push({ kind: 'text', text: body }); + if (skillList.length) out.push({ kind: 'kv', label: 'Skills', value: skillList.join(', ') }); + } + + pushMeta(out, cv); + return out; +}; + +const IMAGE_MD = /^\s*!\[image:([^\]]*)\]\((&[^)]+)\)\s*$/; +const MEDIA_MD = /^\s*\[(audio|video|pdf|torrent):([^\]]*)\]\((&[^)]+)\)\s*$/; +const inlineMarkdown = (line) => String(line || '') + .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '$2') + .replace(/\[\[([^\]]+)\]\]/g, (m, t) => t.replace(/^.*:/, '')) + .replace(/!\[image:([^\]]*)\]\((&[^)]+)\)/g, '[image: $1]') + .replace(/\[(audio|video|pdf|torrent):([^\]]*)\]\((&[^)]+)\)/g, '[$1: $2]') + .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)') + .replace(/(\*\*|__)(.*?)\1/g, '$2') + .replace(/(\*|_)(.*?)\1/g, '$2') + .replace(/`([^`]*)`/g, '$1') + .replace(/^\s{0,3}>\s?/, '| '); + +const pushWikiBody = (out, body, images = {}) => { + const lines = String(body || '').replace(/\r\n/g, '\n').split('\n'); + let paragraph = []; + const flush = () => { if (paragraph.length) { out.push({ kind: 'text', text: paragraph.join('\n') }); paragraph = []; } }; + for (const raw of lines) { + const img = raw.match(IMAGE_MD); + if (img) { + flush(); + const buf = images && images[img[2]]; + if (buf) out.push({ kind: 'image', buffer: buf, caption: txt(img[1]) }); + else out.push({ kind: 'text', text: `[image: ${txt(img[1])}]` }); + continue; + } + const media = raw.match(MEDIA_MD); + if (media) { flush(); out.push({ kind: 'text', text: `[${media[1]}: ${txt(media[2])}]` }); continue; } + const heading = raw.match(/^\s{0,3}#{1,6}\s+(.*)$/); + if (heading) { flush(); out.push({ kind: 'blank' }); out.push({ kind: 'section', text: inlineMarkdown(heading[1]).toUpperCase() }); continue; } + if (!raw.trim()) { flush(); out.push({ kind: 'blank' }); continue; } + paragraph.push(inlineMarkdown(raw)); + } + flush(); +}; + +const padSections = (pad, extra = {}) => { + const out = []; + out.push({ kind: 'title', text: txt(pad.title) || '-' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: 'Author', value: txt(pad.author) }); + out.push({ kind: 'kv', label: 'Created', value: fmtDate(pad.createdAt) }); + out.push({ kind: 'kv', label: 'Status', value: txt(pad.status).toUpperCase() }); + out.push({ kind: 'kv', label: 'Members', value: String(Array.isArray(pad.members) ? pad.members.length : 0) }); + out.push({ kind: 'kv', label: 'Versions', value: String(Array.isArray(pad.versions) ? pad.versions.length : 0) }); + pushTags(out, pad); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'CONTENT' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'text', text: String(pad.text || '').replace(/\r\n/g, '\n') }); + const versions = Array.isArray(pad.versions) ? pad.versions : []; + if (versions.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'VERSION HISTORY' }); + out.push({ kind: 'blank' }); + for (const v of versions.slice().reverse().slice(0, 50)) out.push({ kind: 'kv', label: fmtDate(v.createdAt), value: txt(v.author) }); + } + return out; +}; + +const wikiSections = (page, extra = {}) => { + const out = []; + out.push({ kind: 'title', text: txt(page.title) || '-' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: 'Author', value: txt(page.author) }); + out.push({ kind: 'kv', label: 'Updated', value: fmtDate(page.updatedAt) }); + out.push({ kind: 'kv', label: 'Versions', value: String(page.versionCount || 1) }); + pushTags(out, page); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'CONTENT' }); + out.push({ kind: 'blank' }); + pushWikiBody(out, page.body, (extra && extra.images) || {}); + return out; +}; + +const emergencySections = (item, extra = {}) => { + const out = []; + out.push({ kind: 'title', text: txt(item.title) || '-' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: 'Status', value: txt(item.status).toUpperCase() }); + out.push({ kind: 'kv', label: 'Severity', value: `${txt(item.severity).toUpperCase()} (${Number(item.confirmationCount) || 0} confirmations)` }); + out.push({ kind: 'kv', label: 'Category', value: txt(item.category).toUpperCase() }); + if (item.expiresAt) out.push({ kind: 'kv', label: 'Expires', value: fmtDate(item.expiresAt) }); + out.push({ kind: 'kv', label: 'Author', value: txt(item.author) }); + out.push({ kind: 'kv', label: 'Created', value: fmtDate(item.createdAt) }); + if (item.mapUrl) out.push({ kind: 'kv', label: 'Map', value: txt(item.mapUrl) }); + pushTags(out, item); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'CONTENT' }); + out.push({ kind: 'blank' }); + pushWikiBody(out, item.text, (extra && extra.images) || {}); + const updates = asList(item.updates); + if (updates.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'LAST UPDATES' }); + for (const u of updates) { + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: fmtDate(u.createdAt), value: txt(u.author) }); + pushWikiBody(out, u.text, (extra && extra.images) || {}); + } + } + return out; +}; + +const campaignSections = (item, extra = {}) => { + const out = []; + out.push({ kind: 'title', text: txt(item.title) || '-' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: 'Status', value: txt(item.status).toUpperCase() }); + out.push({ kind: 'kv', label: 'Category', value: txt(item.category).toUpperCase() }); + out.push({ kind: 'kv', label: 'Signatures', value: `${Number(item.signatureCount) || 0} / ${Number(item.goal) || 0} (${Number(item.progress) || 0}%)` }); + if (item.deadline) out.push({ kind: 'kv', label: 'Deadline', value: fmtDate(item.deadline) }); + out.push({ kind: 'kv', label: 'Promoter', value: txt(item.author) }); + out.push({ kind: 'kv', label: 'Created', value: fmtDate(item.createdAt) }); + if (item.mapUrl) out.push({ kind: 'kv', label: 'Map', value: txt(item.mapUrl) }); + pushTags(out, item); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'CONTENT' }); + out.push({ kind: 'blank' }); + pushWikiBody(out, item.text, (extra && extra.images) || {}); + const updates = asList(item.updates); + if (updates.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'LAST UPDATES' }); + for (const u of updates) { + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: fmtDate(u.createdAt), value: txt(u.author) }); + pushWikiBody(out, u.text, (extra && extra.images) || {}); + } + } + const signatures = asList(item.signatures); + if (signatures.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: `SUPPORTERS (${signatures.length})` }); + for (const s of signatures) out.push({ kind: 'kv', label: fmtDate(s.createdAt), value: `${txt(s.author)}${s.text ? ' - ' + txt(s.text) : ''}` }); + } + return out; +}; + +const logisticsSections = (item, extra = {}) => { + const out = []; + out.push({ kind: 'title', text: txt(item.title) || '-' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: 'Type', value: `${txt(item.kind).toUpperCase()} - ${txt(item.mode).toUpperCase()}` }); + out.push({ kind: 'kv', label: 'Status', value: txt(item.status).toUpperCase() }); + out.push({ kind: 'kv', label: 'Route', value: `${txt(item.origin)} -> ${txt(item.destination)}` }); + if (item.date) out.push({ kind: 'kv', label: 'Date', value: `${fmtDate(item.date)}${item.recurrence && item.recurrence !== 'NONE' ? ' (' + txt(item.recurrence) + ')' : ''}` }); + if (item.kind === 'TRIP' && item.seats) out.push({ kind: 'kv', label: 'Seats', value: `${Number(item.seatsLeft) || 0} / ${Number(item.seats) || 0}` }); + if (item.size) out.push({ kind: 'kv', label: 'Size', value: txt(item.size) }); + if (item.weight) out.push({ kind: 'kv', label: 'Weight', value: txt(item.weight) }); + out.push({ kind: 'kv', label: 'Price', value: item.priceType === 'FREE' || !(Number(item.price) > 0) ? 'FREE' : `${item.price} ${txt(item.priceType)}` }); + if (item.orderRef) out.push({ kind: 'kv', label: 'Order', value: txt(item.orderRef) }); + if (item.mapUrl) out.push({ kind: 'kv', label: 'Map', value: txt(item.mapUrl) }); + out.push({ kind: 'kv', label: 'Author', value: txt(item.author) }); + out.push({ kind: 'kv', label: 'Created', value: fmtDate(item.createdAt) }); + pushTags(out, item); + if (txt(item.description).trim()) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DESCRIPTION' }); + out.push({ kind: 'blank' }); + pushWikiBody(out, item.description, (extra && extra.images) || {}); + } + const bookings = asList(item.bookings); + if (bookings.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: `BOOKINGS (${bookings.length})` }); + for (const b of bookings) out.push({ kind: 'kv', label: `${fmtDate(b.createdAt)} ${txt(b.status)}`, value: `${txt(b.booker)}${b.seats ? ' x' + b.seats : ''}${b.notes ? ' - ' + txt(b.notes) : ''}${b.receipt ? ' [' + txt(b.receipt) + ']' : ''}` }); + } + const ratings = asList(item.ratings); + if (ratings.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: `RATINGS (${ratings.length}) - ${Number(item.ratingAvg) || 0}/5` }); + for (const r of ratings) out.push({ kind: 'kv', label: `${'*'.repeat(Number(r.score) || 0)} ${fmtDate(r.createdAt)}`, value: `${txt(r.author)}${r.text ? ' - ' + txt(r.text) : ''}` }); + } + return out; +}; + +const mailingSections = (item, extra = {}) => { + const out = []; + out.push({ kind: 'title', text: txt(item.title) || '-' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: 'Type', value: txt(item.listType).toUpperCase() }); + out.push({ kind: 'kv', label: 'Status', value: txt(item.status).toUpperCase() }); + out.push({ kind: 'kv', label: 'Subscribers', value: String(Number(item.participantCount) || 0) }); + out.push({ kind: 'kv', label: 'Messages', value: String(Number(item.messageCount) || 0) }); + out.push({ kind: 'kv', label: 'Author', value: txt(item.author) }); + out.push({ kind: 'kv', label: 'Created', value: fmtDate(item.createdAt) }); + pushTags(out, item); + if (txt(item.description).trim()) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DESCRIPTION' }); + out.push({ kind: 'blank' }); + pushWikiBody(out, item.description, (extra && extra.images) || {}); + } + const history = asList(item.history); + if (history.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: `HISTORY (${history.length})` }); + for (const m of history) { + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: fmtDate(m.sentAt), value: `${txt(m.author)} - ${txt(m.subject)}` }); + pushWikiBody(out, m.text, {}); + } + } + return out; +}; + +const PIXELIA_EMPTY = [0x1a, 0x1a, 0x1a]; +const PIXELIA_GRID = [0x33, 0x33, 0x33]; +const PIXELIA_PAGE_W = 512; +const PIXELIA_PAGE_H = 520; +const PIXELIA_MAX_CELL = 24; + +const pixeliaCells = (pixels, cols, rows) => { + const cells = new Map(); + for (const px of asList(pixels)) { + const key = `${px.x},${px.y}`; + const hex = /^#[0-9a-f]{6}$/i.test(String(px.color || '')) ? String(px.color).slice(1) : null; + if (!hex || cells.has(key) || px.x < 1 || px.x > cols || px.y < 1 || px.y > rows) continue; + cells.set(key, [0, 2, 4].map(i => parseInt(hex.slice(i, i + 2), 16))); + } + return cells; +}; + +const pixeliaRaster = (cells, x0, y0, cols, rows, block) => { + const w = cols * block; + const h = rows * block; + const rgb = Buffer.alloc(w * h * 3); + const segment = (c, edge) => { + const seg = Buffer.alloc(block * 3); + for (let i = 0; i < block; i++) { + const col = edge || i === block - 1 ? PIXELIA_GRID : c; + seg[i * 3] = col[0]; seg[i * 3 + 1] = col[1]; seg[i * 3 + 2] = col[2]; + } + return seg; + }; + const gridSeg = segment(PIXELIA_GRID, true); + const segs = new Map(); + for (let cy = 0; cy < rows; cy++) { + for (let cx = 0; cx < cols; cx++) { + const c = cells.get(`${x0 + cx},${y0 + cy}`) || PIXELIA_EMPTY; + const key = c.join(','); + if (!segs.has(key)) segs.set(key, segment(c, false)); + const seg = segs.get(key); + for (let py = 0; py < block; py++) { + rgb.set(py === block - 1 ? gridSeg : seg, (((cy * block + py) * w) + cx * block) * 3); + } + } + } + return { w, h, rgb }; +}; + +const pixeliaSections = (canvas) => { + const out = []; + const pixels = asList(canvas.pixels); + const cols = Number(canvas.width) || 1; + const rows = Number(canvas.height) || 1; + const contributors = [...new Set(pixels.flatMap(px => asList(px.contributors_inhabitants)))]; + const cells = pixeliaCells(pixels, cols, rows); + const cell = Math.min(PIXELIA_MAX_CELL, PIXELIA_PAGE_W / cols); + const block = Math.max(6, Math.ceil(cell * 2)); + const perPage = Math.max(1, Math.floor(PIXELIA_PAGE_H / cell)); + out.push({ kind: 'title', text: 'Pixelia' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: 'Total pixels', value: String(pixels.length) }); + out.push({ kind: 'kv', label: 'Contributors', value: String(contributors.length) }); + out.push({ kind: 'blank' }); + for (let y = 1; y <= rows; y += perPage) { + const n = Math.min(perPage, rows - y + 1); + out.push({ kind: 'image', raster: pixeliaRaster(cells, 1, y, cols, n, block), maxW: cols * cell, maxH: n * cell }); + } + if (contributors.length) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'CONTRIBUTORS' }); + out.push({ kind: 'blank' }); + for (const id of contributors) out.push({ kind: 'text', text: txt(id) }); + } + return out; +}; + +const mapPins = (item) => [{ + lat: item.lat, lng: item.lng, main: true, + label: txt(item.markerLabel) || txt(item.description) || txt(item.title) || 'Marker', + image: txt(item.image), author: item.author, createdAt: item.createdAt +}].concat(asList(item.markers).map(mk => ({ lat: mk.lat, lng: mk.lng, main: false, label: txt(mk.label), image: txt(mk.image), author: mk.author, createdAt: mk.createdAt }))); + +const mapSections = (item, extra = {}) => { + const names = (extra && extra.names) || {}; + const images = (extra && extra.images) || {}; + const who = id => names[id] ? `${names[id]} (${id})` : txt(id); + const coords = (lat, lng) => `${(Number(lat) || 0).toFixed(4)}, ${(Number(lng) || 0).toFixed(4)}`; + const out = []; + out.push({ kind: 'title', text: txt(item.title) || '-' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: 'Type', value: txt(item.mapType).toUpperCase() }); + out.push({ kind: 'kv', label: 'Location', value: coords(item.lat, item.lng) }); + out.push({ kind: 'kv', label: 'Author', value: who(item.author) }); + out.push({ kind: 'kv', label: 'Created', value: fmtDate(item.createdAt) }); + if (item.updatedAt && item.updatedAt !== item.createdAt) out.push({ kind: 'kv', label: 'Updated', value: fmtDate(item.updatedAt) }); + pushTags(out, item); + if (txt(item.description)) { + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'DESCRIPTION' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'text', text: String(item.description).replace(/\r\n/g, '\n') }); + } + const pins = mapPins(item); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: 'MAP' }); + out.push({ kind: 'blank' }); + out.push({ kind: 'map', pins }); + out.push({ kind: 'blank' }); + out.push({ kind: 'section', text: `MARKERS (${pins.length})` }); + pins.forEach((p, i) => { + out.push({ kind: 'blank' }); + out.push({ kind: 'kv', label: i === 0 ? 'Main marker' : `Marker ${i}`, value: p.label }); + out.push({ kind: 'kv', label: 'Coordinates', value: coords(p.lat, p.lng) }); + out.push({ kind: 'kv', label: 'Author', value: who(p.author) }); + if (p.createdAt) out.push({ kind: 'kv', label: 'Date', value: fmtDate(p.createdAt) }); + if (p.image && images[p.image]) out.push({ kind: 'image', buffer: images[p.image], caption: p.label, maxW: 200, maxH: 140 }); + }); + return out; +}; + +const BUILDERS = { + maps: { title: 'OASIS - Map', sections: mapSections, name: item => item.title }, + wiki: { title: 'OASIS - Wiki', sections: wikiSections, name: item => item.title }, + pads: { title: 'OASIS - Pad', sections: padSections, name: item => item.title }, + campaigns: { title: 'OASIS - Campaign', sections: campaignSections, name: item => item.title }, + logistics: { title: 'OASIS - Route', sections: logisticsSections, name: item => item.title }, + mailing: { title: 'OASIS - Mailing List', sections: mailingSections, name: item => item.title }, + emergencies: { title: 'OASIS - Emergency', sections: emergencySections, name: item => item.title }, + reports: { title: 'OASIS - Report', sections: reportSections, name: item => item.title }, + votes: { title: 'OASIS - Votation', sections: voteSections, name: item => item.question }, + events: { title: 'OASIS - Event', sections: eventSections, name: item => item.title }, + tasks: { title: 'OASIS - Task', sections: taskSections, name: item => item.title }, + calendars: { title: 'OASIS - Calendar', sections: calendarSections, name: item => item.title }, + cv: { title: 'OASIS - Curriculum', sections: cvSections, name: item => item.name || item.author }, + pixelia: { title: 'OASIS - Pixelia', sections: pixeliaSections, name: () => 'canvas' } +}; + +const isSupported = kind => Object.prototype.hasOwnProperty.call(BUILDERS, kind); + +const pdfFilename = (kind, item) => { + const b = BUILDERS[kind]; + const raw = b ? txt(b.name(item || {})) : ''; + const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40); + return `oasis-${kind}-${slug || 'document'}.pdf`; +}; + +const buildContentPdf = (kind, item, extra = {}, viewerId = null) => { + const b = BUILDERS[kind]; + if (!b) throw new Error('Unsupported pdf kind'); + return buildDocumentPdf({ + title: b.title, + issuedToLabel: 'Issued to', + issuedTo: viewerId || null, + sections: b.sections(item || {}, extra), + license: (item && item.license) || '' + }); +}; + +function buildSmartContractPdf({ transfer, block, viewerId }) { + const pageW = 612; + const pageH = 792; + const marginX = 50; + const headerH = 90; + const footerH = 40; + const bodyTop = pageH - headerH - 24; + const bodyBottom = footerH + 10; + const lineH = 14; + + let logoBuf = null; + let logoDims = null; + try { + logoBuf = fs.readFileSync(LOGO_PATH); + logoDims = readJpegDims(logoBuf); + } catch (_) {} + + const t = transfer || {}; + const b = block || {}; + const fmt = v => (v === undefined || v === null) ? '' : String(v); + const fmtAmount = () => { + const cat = String(t.category || 'ECONOMIC').toUpperCase(); + const unit = cat === 'TIME' ? 'h' : cat === 'TRUST' ? 'trust' : 'ECO'; + return `${Number(t.amount || 0).toFixed(6)} ${unit}`; + }; + const fmtDate = v => v ? new Date(v).toISOString().replace('T', ' ').slice(0, 19) + ' UTC' : ''; + const confirmedBy = Array.isArray(t.confirmedBy) ? t.confirmedBy : []; + const settledUbi = (Array.isArray(t.tags) ? t.tags : []).some(x => String(x).toUpperCase() === 'UBI') && /^[0-9a-f]{64}$/i.test(String(t.txid || '')); + const required = (t.from === t.to || settledUbi) ? 1 : 2; + const confirmedCount = confirmedBy.length; + const tags = Array.isArray(t.tags) ? t.tags.join(', ') : ''; + + const sections = []; + sections.push({ kind: 'title', text: `Concept: ${t.concept || '-'}` }); + sections.push({ kind: 'blank' }); + + sections.push({ kind: 'section', text: 'OASIS IDs' }); + sections.push({ kind: 'kv', label: 'From', value: fmt(t.from) }); + sections.push({ kind: 'kv', label: 'To', value: fmt(t.to) }); + sections.push({ kind: 'blank' }); + + sections.push({ kind: 'section', text: 'TERMS' }); + sections.push({ kind: 'kv', label: 'Category', value: String(t.category || 'ECONOMIC').toUpperCase() }); + if (String(t.category || '').toUpperCase() !== 'TRUST') sections.push({ kind: 'kv', label: 'Amount', value: fmtAmount() }); + sections.push({ kind: 'kv', label: 'Status', value: fmt(t.status) }); + if (t.deadline) sections.push({ kind: 'kv', label: 'Deadline', value: fmtDate(t.deadline) }); + if (tags) sections.push({ kind: 'kv', label: 'Tags', value: tags }); + sections.push({ kind: 'blank' }); + + sections.push({ kind: 'section', text: 'CONFIRMATIONS' }); + sections.push({ kind: 'kv', label: 'Required', value: String(required) }); + sections.push({ kind: 'kv', label: 'Confirmed', value: String(confirmedCount) }); + if (confirmedBy.length) { + for (const f of confirmedBy) sections.push({ kind: 'kv', label: 'Signed by', value: fmt(f) }); + } + sections.push({ kind: 'blank' }); + + sections.push({ kind: 'section', text: 'BLOCK VERIFICATION' }); + if (b && b.id) { + sections.push({ kind: 'kv', label: 'Block ID', value: fmt(b.id) }); + if (b.ts) sections.push({ kind: 'kv', label: 'Block Timestamp', value: fmtDate(b.ts) }); + if (b.type) sections.push({ kind: 'kv', label: 'Block Type', value: String(b.type).toUpperCase() }); + if (b.author) sections.push({ kind: 'kv', label: 'Block Author', value: fmt(b.author) }); + if (b.size) sections.push({ kind: 'kv', label: 'Block Size', value: `${b.size} bytes` }); + } else { + sections.push({ kind: 'kv', label: 'Block ID', value: fmt(t.id) }); + } + sections.push({ kind: 'blank' }); + + sections.push({ kind: 'section', text: 'METADATA' }); + sections.push({ kind: 'kv', label: 'Transfer ID', value: fmt(t.id) }); + if (t.createdAt) sections.push({ kind: 'kv', label: 'Created At', value: fmtDate(t.createdAt) }); + if (t.updatedAt) sections.push({ kind: 'kv', label: 'Updated At', value: fmtDate(t.updatedAt) }); + + const lines = []; + for (const s of sections) { + if (s.kind === 'kv') { + const txt = `${s.label}: ${s.value}`; + for (const w of wrap(txt, 82)) lines.push({ kind: 'kv', text: w }); + } else { + lines.push(s); + } + } + + const maxBodyLines = Math.floor((bodyTop - bodyBottom) / lineH); + const pages = []; + for (let i = 0; i < lines.length; i += maxBodyLines) pages.push(lines.slice(i, i + maxBodyLines)); + if (!pages.length) pages.push([]); + + const objects = []; + const addObj = body => { objects.push(body); return objects.length; }; + + const catalogId = addObj(null); + const pagesId = addObj(null); + const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>'); + const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>'); + let logoXObjId = null; + if (logoBuf && logoDims) { + const cs = logoDims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; + const dict = `<< /Type /XObject /Subtype /Image /Width ${logoDims.w} /Height ${logoDims.h} /ColorSpace ${cs} /BitsPerComponent 8 /Filter /DCTDecode /Length ${logoBuf.length} >>`; + logoXObjId = addObj({ dict, stream: logoBuf }); + } + + const exportDate = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; + const footerLeft = `Generated: ${exportDate}`; + + const pageIds = []; + const contentIds = []; + + pages.forEach((pg, pgIdx) => { + const parts = []; + if (logoXObjId) { + const logoH = 60; + const logoW = Math.round((logoDims.w / logoDims.h) * logoH); + parts.push(`q\n${logoW} 0 0 ${logoH} ${marginX} ${pageH - headerH + 15} cm\n/Logo Do\nQ`); + } + const titleX = (logoXObjId ? marginX + 80 : marginX); + const titleY = pageH - 45; + parts.push(`BT\n/F2 16 Tf\n${titleX} ${titleY} Td\n(${escapePdf('OASIS - Smart Contract')}) Tj\nET`); + const prefix = 'Issued to: '; + const prefixW = prefix.length * 5.4; + parts.push(`BT\n/F1 9 Tf\n${titleX} ${titleY - 16} Td\n(${escapePdf(prefix)}) Tj\nET`); + parts.push(`BT\n/F2 9 Tf\n${titleX + prefixW} ${titleY - 16} Td\n(${escapePdf(String(viewerId || ''))}) Tj\nET`); + + parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${pageH - headerH} m\n${pageW - marginX} ${pageH - headerH} l\nS\nQ`); + + let y = bodyTop; + for (const ln of pg) { + if (ln.kind === 'title') { + parts.push(`BT\n/F2 14 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + } else if (ln.kind === 'subtitle') { + parts.push(`BT\n/F1 11 Tf\n0.2 0.2 0.2 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + } else if (ln.kind === 'section') { + parts.push(`BT\n/F2 11 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + parts.push(`q\n0 0 0 RG\n0.5 w\n${marginX} ${y - 3} m\n${pageW - marginX} ${y - 3} l\nS\nQ`); + } else if (ln.kind === 'kv') { + parts.push(`BT\n/F1 10 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + } + y -= lineH; + } + + parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${footerH + 5} m\n${pageW - marginX} ${footerH + 5} l\nS\nQ`); + parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 10} Td\n(${escapePdf(footerLeft)}) Tj\nET`); + parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 21} Td\n(${escapePdf(licenseFooter(''))}) Tj\nET`); + const pageLabel = `Page ${pgIdx + 1} of ${pages.length}`; + const pageLabelW = pageLabel.length * 4.8; + parts.push(`BT\n/F1 8 Tf\n${pageW - marginX - pageLabelW} ${footerH - 10} Td\n(${escapePdf(pageLabel)}) Tj\nET`); + + const content = parts.join('\n'); + const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`; + const cid = addObj(stream); + contentIds.push(cid); + const pid = addObj(null); + pageIds.push(pid); + }); + + for (let i = 0; i < pageIds.length; i++) { + const resources = logoXObjId + ? `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> /XObject << /Logo ${logoXObjId} 0 R >> >>` + : `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> >>`; + objects[pageIds[i] - 1] = `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${pageW} ${pageH}] /Contents ${contentIds[i]} 0 R /Resources ${resources} >>`; + } + objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; + objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageIds.map(id => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`; + + const chunks = []; + const offsets = [0]; + let byteLen = 0; + const push = (buf) => { chunks.push(buf); byteLen += buf.length; }; + push(Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary')); + for (let i = 0; i < objects.length; i++) { + offsets.push(byteLen); + const obj = objects[i]; + if (obj && typeof obj === 'object' && obj.dict && obj.stream) { + push(Buffer.from(`${i + 1} 0 obj\n${obj.dict}\nstream\n`, 'binary')); + push(obj.stream); + push(Buffer.from('\nendstream\nendobj\n', 'binary')); + } else { + push(Buffer.from(`${i + 1} 0 obj\n${obj}\nendobj\n`, 'binary')); + } + } + const xrefStart = byteLen; + let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; + for (let i = 1; i <= objects.length; i++) xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; + xref += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xrefStart}\n%%EOF`; + push(Buffer.from(xref, 'binary')); + return Buffer.concat(chunks); +} + +function buildCertificatePdf({ cert, course, studentName, teacherName }) { + const pageW = 792; + const pageH = 612; + const margin = 40; + + let logoBuf = null; + let logoDims = null; + try { + logoBuf = fs.readFileSync(LOGO_PATH); + logoDims = readJpegDims(logoBuf); + } catch (_) {} + + const c = cert || {}; + const courseTitle = String((course && course.title) || c.courseTitle || 'Oasis Course'); + const issued = c.createdAt ? new Date(c.createdAt) : new Date(); + const issuedStr = issued.toISOString().slice(0, 10); + + const charW = (size) => size * 0.6; + const centered = (text, size, font, y, rgb = '0 0 0') => { + const t = String(text || ''); + const x = Math.max(margin, (pageW - t.length * charW(size)) / 2); + return `BT\n/${font} ${size} Tf\n${rgb} rg\n${x} ${y} Td\n(${escapePdf(t)}) Tj\nET`; + }; + + const parts = []; + + parts.push(`q\n0.85 0.65 0.1 RG\n3 w\n${margin} ${margin} ${pageW - margin * 2} ${pageH - margin * 2} re\nS\nQ`); + parts.push(`q\n0.3 0.3 0.3 RG\n1 w\n${margin + 8} ${margin + 8} ${pageW - (margin + 8) * 2} ${pageH - (margin + 8) * 2} re\nS\nQ`); + + if (logoBuf && logoDims) { + const logoH = 64; + const logoW = Math.round((logoDims.w / logoDims.h) * logoH); + parts.push(`q\n${logoW} 0 0 ${logoH} ${(pageW - logoW) / 2} ${pageH - 140} cm\n/Logo Do\nQ`); + } + + parts.push(centered('OASIS', 26, 'F2', pageH - 175)); + parts.push(centered('CERTIFICATE OF COMPLETION', 16, 'F2', pageH - 205, '0.55 0.4 0.05')); + + parts.push(centered('This certifies that', 11, 'F1', pageH - 250, '0.25 0.25 0.25')); + parts.push(centered(studentName || c.student || '', 18, 'F2', pageH - 280)); + if (studentName && c.student && studentName !== c.student) { + parts.push(centered(c.student, 7, 'F1', pageH - 296, '0.45 0.45 0.45')); + } + + parts.push(centered('has successfully completed the course', 11, 'F1', pageH - 330, '0.25 0.25 0.25')); + parts.push(centered(courseTitle, 16, 'F2', pageH - 360)); + + parts.push(centered(`Issued on ${issuedStr}`, 10, 'F1', pageH - 400, '0.25 0.25 0.25')); + + const sigY = 135; + parts.push(`q\n0.3 0.3 0.3 RG\n0.7 w\n${margin + 60} ${sigY + 14} m\n${margin + 280} ${sigY + 14} l\nS\nQ`); + parts.push(`BT\n/F2 11 Tf\n0 0 0 rg\n${margin + 60} ${sigY} Td\n(${escapePdf(teacherName || c.author || '')}) Tj\nET`); + parts.push(`BT\n/F1 8 Tf\n0.35 0.35 0.35 rg\n${margin + 60} ${sigY - 13} Td\n(${escapePdf('Teacher')}) Tj\nET`); + if (teacherName && c.author && teacherName !== c.author) { + parts.push(`BT\n/F1 6 Tf\n0.45 0.45 0.45 rg\n${margin + 60} ${sigY - 24} Td\n(${escapePdf(c.author)}) Tj\nET`); + } + + parts.push(centered('Cryptographically signed on the Oasis P2P network — verifiable by anyone', 8, 'F1', margin + 30, '0.35 0.35 0.35')); + if (c.id) parts.push(centered(`Certificate ID: ${c.id}`, 6, 'F1', margin + 18, '0.45 0.45 0.45')); + parts.push(centered(licenseFooter(''), 6, 'F1', margin + 42, '0.45 0.45 0.45')); + + const content = parts.join('\n'); + + const objects = []; + const addObj = body => { objects.push(body); return objects.length; }; + + const catalogId = addObj(null); + const pagesId = addObj(null); + const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>'); + const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>'); + let logoXObjId = null; + if (logoBuf && logoDims) { + const cs = logoDims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; + const dict = `<< /Type /XObject /Subtype /Image /Width ${logoDims.w} /Height ${logoDims.h} /ColorSpace ${cs} /BitsPerComponent 8 /Filter /DCTDecode /Length ${logoBuf.length} >>`; + logoXObjId = addObj({ dict, stream: logoBuf }); + } + + const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`; + const contentId = addObj(stream); + const pageId = addObj(null); + + const resources = logoXObjId + ? `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> /XObject << /Logo ${logoXObjId} 0 R >> >>` + : `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> >>`; + objects[pageId - 1] = `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${pageW} ${pageH}] /Contents ${contentId} 0 R /Resources ${resources} >>`; + objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; + objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageId} 0 R] /Count 1 >>`; + + const chunks = []; + const offsets = [0]; + let byteLen = 0; + const push = (buf) => { chunks.push(buf); byteLen += buf.length; }; + push(Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary')); + for (let i = 0; i < objects.length; i++) { + offsets.push(byteLen); + const obj = objects[i]; + if (obj && typeof obj === 'object' && obj.dict && obj.stream) { + push(Buffer.from(`${i + 1} 0 obj\n${obj.dict}\nstream\n`, 'binary')); + push(obj.stream); + push(Buffer.from('\nendstream\nendobj\n', 'binary')); + } else { + push(Buffer.from(`${i + 1} 0 obj\n${obj}\nendobj\n`, 'binary')); + } + } + const xrefStart = byteLen; + let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; + for (let i = 1; i <= objects.length; i++) xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; + xref += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xrefStart}\n%%EOF`; + push(Buffer.from(xref, 'binary')); + return Buffer.concat(chunks); +} + +const linkPattern = /(?:https?:\/\/[^\s]+|www\.[^\s]+|@[A-Za-z0-9+/=.\-]+\.ed25519|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g; + +const splitSegments = (line) => { + const segs = []; + let last = 0; + const re = new RegExp(linkPattern.source, 'g'); + let m; + while ((m = re.exec(line)) !== null) { + if (m.index > last) segs.push({ t: line.slice(last, m.index), l: false }); + segs.push({ t: m[0], l: true }); + last = m.index + m[0].length; + } + if (last < line.length) segs.push({ t: line.slice(last), l: false }); + return segs; +}; + +function buildLogsPdf(entries, oasisId, opts = {}) { + const pageW = 612; + const pageH = 792; + const marginX = 50; + const headerH = 90; + const footerH = 40; + const bodyTop = pageH - headerH - 22; + const bodyBottom = footerH + 10; + const lineH = 12; + const maxBodyLines = Math.floor((bodyTop - bodyBottom) / lineH); + + let logoBuf = null; + let logoDims = null; + try { + logoBuf = fs.readFileSync(LOGO_PATH); + logoDims = readJpegDims(logoBuf); + } catch (_) {} + + const allLines = []; + for (const e of entries) { + const ts = new Date(e.ts); + const when = ts.toISOString().replace('T', ' ').slice(0, 19); + allLines.push({ kind: 'header', text: `[${when}]:` }); + allLines.push({ kind: 'blank', text: '' }); + for (const l of wrap(e.text, 82)) allLines.push({ kind: 'text', text: l }); + allLines.push({ kind: 'blank', text: '' }); + } + if (!allLines.length) allLines.push({ kind: 'text', text: '(no entries)' }); + + const pages = []; + for (let i = 0; i < allLines.length; i += maxBodyLines) { + pages.push(allLines.slice(i, i + maxBodyLines)); + } + if (!pages.length) pages.push([{ kind: 'text', text: '(no entries)' }]); + + const objects = []; + const addObj = body => { objects.push(body); return objects.length; }; + + const catalogId = addObj(null); + const pagesId = addObj(null); + const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>'); + const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>'); + + let logoXObjId = null; + if (logoBuf && logoDims) { + const colorSpace = logoDims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; + const dict = `<< /Type /XObject /Subtype /Image /Width ${logoDims.w} /Height ${logoDims.h} /ColorSpace ${colorSpace} /BitsPerComponent 8 /Filter /DCTDecode /Length ${logoBuf.length} >>`; + logoXObjId = addObj({ dict, stream: logoBuf }); + } + + const exportDate = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; + const footerLeft = `Generated: ${exportDate}`; + + const pageIds = []; + const contentIds = []; + + pages.forEach((pg, pgIdx) => { + const parts = []; + + if (logoXObjId) { + const logoH = 60; + const logoW = Math.round((logoDims.w / logoDims.h) * logoH); + const logoX = marginX; + const logoY = pageH - headerH + 15; + parts.push(`q\n${logoW} 0 0 ${logoH} ${logoX} ${logoY} cm\n/Logo Do\nQ`); + } + + const titleX = (logoXObjId ? marginX + 80 : marginX); + const titleY = pageH - 45; + parts.push(`BT\n/F2 16 Tf\n${titleX} ${titleY} Td\n(${escapePdf('OASIS - Experience logs')}) Tj\nET`); + const inhabitantPrefix = 'Inhabitant: '; + const inhabitantPrefixW = inhabitantPrefix.length * 5.4; + parts.push(`BT\n/F1 9 Tf\n${titleX} ${titleY - 16} Td\n(${escapePdf(inhabitantPrefix)}) Tj\nET`); + parts.push(`BT\n/F2 9 Tf\n${titleX + inhabitantPrefixW} ${titleY - 16} Td\n(${escapePdf(String(oasisId || ''))}) Tj\nET`); + + parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${pageH - headerH} m\n${pageW - marginX} ${pageH - headerH} l\nS\nQ`); + + let y = bodyTop; + const charW = 6; + for (const ln of pg) { + if (ln.kind === 'header') { + parts.push(`BT\n/F2 10 Tf\n1 0.647 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); + } else if (ln.text) { + const segs = splitSegments(ln.text); + let x = marginX; + for (const s of segs) { + if (!s.t) continue; + const color = s.l ? '0 0 1 rg' : '0 0 0 rg'; + parts.push(`BT\n/F1 10 Tf\n${color}\n${x} ${y} Td\n(${escapePdf(s.t)}) Tj\nET`); + x += s.t.length * charW; + } + } + y -= lineH; + } + + parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${footerH + 5} m\n${pageW - marginX} ${footerH + 5} l\nS\nQ`); + parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 10} Td\n(${escapePdf(footerLeft)}) Tj\nET`); + parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 21} Td\n(${escapePdf(licenseFooter(''))}) Tj\nET`); + const pageLabel = `Page ${pgIdx + 1} of ${pages.length}`; + const pageLabelW = pageLabel.length * 4.8; + parts.push(`BT\n/F1 8 Tf\n${pageW - marginX - pageLabelW} ${footerH - 10} Td\n(${escapePdf(pageLabel)}) Tj\nET`); + + const content = parts.join('\n'); + const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`; + const cid = addObj(stream); + contentIds.push(cid); + const pid = addObj(null); + pageIds.push(pid); + }); + + for (let i = 0; i < pageIds.length; i++) { + const resources = logoXObjId + ? `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> /XObject << /Logo ${logoXObjId} 0 R >> >>` + : `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> >>`; + objects[pageIds[i] - 1] = `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${pageW} ${pageH}] /Contents ${contentIds[i]} 0 R /Resources ${resources} >>`; + } + + objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; + objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageIds.map(id => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`; + + const chunks = []; + const offsets = [0]; + let byteLen = 0; + const push = (buf) => { chunks.push(buf); byteLen += buf.length; }; + + push(Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary')); + + for (let i = 0; i < objects.length; i++) { + offsets.push(byteLen); + const obj = objects[i]; + if (obj && typeof obj === 'object' && obj.dict && obj.stream) { + push(Buffer.from(`${i + 1} 0 obj\n${obj.dict}\nstream\n`, 'binary')); + push(obj.stream); + push(Buffer.from('\nendstream\nendobj\n', 'binary')); + } else { + push(Buffer.from(`${i + 1} 0 obj\n${obj}\nendobj\n`, 'binary')); + } + } + + const xrefStart = byteLen; + let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; + for (let i = 1; i <= objects.length; i++) { + xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; + } + xref += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xrefStart}\n%%EOF`; + push(Buffer.from(xref, 'binary')); + + return Buffer.concat(chunks); +} + +const buildRecoveryKitPdf = (kit, { qr = null, labels = {} } = {}) => { + const sections = []; + sections.push({ kind: 'title', text: labels.title || 'RECOVERY' }); + sections.push({ kind: 'blank' }); + sections.push({ kind: 'kv', label: labels.id || 'Oasis ID', value: kit.id || '' }); + sections.push({ kind: 'kv', label: labels.date || 'Generated', value: fmtDate(kit.createdAt) }); + if (qr) { sections.push({ kind: 'blank' }); sections.push({ kind: 'image', buffer: qr, caption: '' }); } + sections.push({ kind: 'blank' }); + sections.push({ kind: 'section', text: 'SECRET' }); + sections.push({ kind: 'blank' }); + for (const line of String(kit.secret || '').split('\n')) sections.push({ kind: 'text', text: line }); + return buildDocumentPdf({ title: 'OASIS - Recovery', issuedToLabel: labels.id || 'Oasis ID', issuedTo: kit.id || '', sections }); +}; + +module.exports = { buildDocumentPdf, escapePdf, wrap, buildContentPdf, pdfFilename, isSupported, buildSmartContractPdf, buildCertificatePdf, buildLogsPdf, buildRecoveryKitPdf }; diff --git a/src/backend/pm_policy.js b/src/backend/pm_policy.js new file mode 100644 index 0000000..54f89fe --- /dev/null +++ b/src/backend/pm_policy.js @@ -0,0 +1,51 @@ +const fs = require('fs'); + +const ANNOUNCE_SEEN_FILE = 'oasis-political-seen'; + +const isMutual = (relationship) => !!(relationship && relationship.following && relationship.followsMe); + +const isRecipientAllowed = ({ pmVisibility, viewerId, recipientId, relationship } = {}) => { + if (recipientId && viewerId && recipientId === viewerId) return true; + if (pmVisibility === 'mutuals') return isMutual(relationship); + if (pmVisibility === 'following') return !!(relationship && relationship.following); + return true; +}; + +const announceSeenPath = () => { + try { return require('../configs/state-manager').statePath(ANNOUNCE_SEEN_FILE); } catch (_) { return null; } +}; + +const readAnnounceSeen = () => { + const file = announceSeenPath(); + if (!file) return {}; + try { + const parsed = JSON.parse(fs.readFileSync(file, 'utf8')); + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch (_) { return {}; } +}; + +const writeAnnounceSeen = (state) => { + const file = announceSeenPath(); + if (!file) return false; + try { fs.writeFileSync(file, JSON.stringify(state || {})); return true; } catch (_) { return false; } +}; + +const everAnnounced = (announced, subject) => { + for (const entry of (announced || [])) { + if (String(entry).startsWith(`${subject}|`)) return true; + } + return false; +}; + +const decideAnnouncement = ({ subject, ref, announced = new Set(), seen = {} } = {}) => { + if (!subject || !ref) return { send: false, remember: false }; + const key = String(ref); + if (announced.has(`${subject}|${key}`)) return { send: false, remember: false }; + if (seen[subject] === key) return { send: false, remember: false }; + if (!everAnnounced(announced, subject) && seen[subject] === undefined) { + return { send: false, remember: true }; + } + return { send: true, remember: true }; +}; + +module.exports = { isRecipientAllowed, isMutual, announceSeenPath, readAnnounceSeen, writeAnnounceSeen, decideAnnouncement }; diff --git a/src/backend/renderStyledText.js b/src/backend/renderStyledText.js new file mode 100644 index 0000000..40945d0 --- /dev/null +++ b/src/backend/renderStyledText.js @@ -0,0 +1,325 @@ +const { a, img, video, audio, span, strong, em, u, s: strike, code, div, textarea } = require("../server/node_modules/hyperaxe"); +const i18nBase = require("../client/assets/translations/i18n"); +const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model"); + +let zoomSeq = 0; + +function getI18n() { + try { + const { i18n } = require("../views/main_views"); + return i18n; + } catch (_) { + return i18nBase['en'] || {}; + } +} + +const ESCAPE_RE = /\\([\\*_~`#>\-\[\]!])/g; +const CODE_BLOCK_RE = /```([\s\S]*?)```[ \t]*(?:\n(?:[ \t]*\n)*|$)/g; +const RULE_RE = /^[ \t]*---[ \t]*(?:\n(?:[ \t]*\n)*|$)/gm; +const HEADER_CLOSED_RE = /^[ \t]*(#{1,6})[ \t]*([^\n]{1,200}?)[ \t]*\1[ \t]*(?:\n(?:[ \t]*\n)*|$)/gm; +const HEADER_SPACED_RE = /^[ \t]*(#{1,6})[ \t]+([^\n]{1,200}?)[ \t]*#*[ \t]*(?:\n(?:[ \t]*\n)*|$)/gm; +const QUOTE_RE = /^[ \t]*>[ \t]?([^\n]*)(?:\n(?:[ \t]*\n)*|$)/gm; +const BULLET_RE = /^([ \t]*)-[ \t]+([^\n]+)(?:\n(?:[ \t]*\n)*|$)/gm; +const NUMBER_RE = /^([ \t]*)(\d{1,3})([.)])[ \t]+([^\n]+)(?:\n(?:[ \t]*\n)*|$)/gm; +const BOLD_RE = /\*\*(?!\s)((?:[^*\n]|\*(?!\*))+?)(? { mentionResolver = typeof fn === 'function' ? fn : null; }; +const bareMentionName = (token) => (/\.ed25519$/i.test(token) ? '' : token.replace(/[.\-]+$/, '')); +const MSG_REF_RE = /%[A-Za-z0-9+/=]{44}\.sha256/g; +const MD_LINK_RE = /!?\[([^\]\n]{1,160})\]\((https?:\/\/[^)\s]+|\/(?![\/\\])[^)\s]*)\)/g; +const URL_RE = /\b(?:https?:\/\/|www\.)[^\s<>"'`]+/gi; +const EMAIL_RE = /\b[A-Za-z0-9][A-Za-z0-9._%+-]{0,63}@(?:[A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,24}\b/g; +const HASHTAG_RE = /#[\w\u0080-\uFFFF]{1,32}(?![\w\u0080-\uFFFF])/g; +const URL_TAIL_RE = /[.,;:!?»"')\]}>]+$/; +const SELF_HOST_RE = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:[/?#]|$)/i; +const INTERNAL_PATHS = [ + 'agenda','ai','audios','author','backup','banking','blockexplorer','blogs','bookmarks','calendars','campaigns', + 'chats','cipher','courts','cv','dev','docs','emergencies','events','favorites','feed','files','forum','games','graphos', + 'hashtag','housing','images','industry','inbox','inhabitants','invites','jobs','larp','logistics','logs','mailing', + 'maps','market','melody','mentions','modules','multiverse','opinions','pads','parliament','rooms','peers','pixelia','pm', + 'podcasts','polls','popular','profile','projects','publish','reports','school','search','settings','shops','spread', + 'stats','tags','tasks','thread','torrents','transfers','trending','tribes','tribe','videos','votes','votations','wallet','wiki' +]; +const INTERNAL_PATH_SET = new Set(INTERNAL_PATHS); +const SSB_ID_RE = /^(?:%[A-Za-z0-9+/=]{20,}\.sha256|@[A-Za-z0-9+/=]{20,}\.ed25519|&[A-Za-z0-9+/=]{20,}\.sha256)$/; + +const internalTargetOf = (url) => { + let parsed = null; + try { parsed = new URL(url); } catch (_) { return null; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null; + const segments = parsed.pathname.split('/').filter(Boolean).map(seg => { try { return decodeURIComponent(seg); } catch (_) { return seg; } }); + if (!segments.length || !INTERNAL_PATH_SET.has(segments[0].toLowerCase())) return null; + const sameHost = SELF_HOST_RE.test(url); + const hasSsbId = segments.slice(1).some(seg => SSB_ID_RE.test(seg)); + if (!sameHost && !hasSsbId) return null; + const id = segments.slice(1).find(seg => SSB_ID_RE.test(seg)) || segments[1] || ''; + return { href: `${parsed.pathname}${parsed.search}${parsed.hash}`, kind: segments[0].toLowerCase(), id }; +}; + +const shortInternalId = (id) => { + const value = String(id || ''); + if (!value) return ''; + const sigil = /^[%@&]/.test(value) ? value[0] : ''; + const body = sigil ? value.slice(1) : value; + return body.length > 8 ? `${sigil}${body.slice(0, 8)}…` : `${sigil}${body}`; +}; +const MAX_DEPTH = 8; + +const headerLevel = (hashes) => Math.min(3, String(hashes || '#').length); + +const indentLevel = (indent) => { + const width = String(indent || '').replace(/\t/g, ' ').length; + return Math.min(4, Math.floor(width / 2) + 1); +}; + +const codeBlockBody = (raw) => { + const s = String(raw); + const nl = s.indexOf('\n'); + if (nl === -1) return s; + return s.slice(nl + 1).replace(/\n[ \t]*$/, ''); +}; + +function renderStyledText(value, opts = {}) { + if (typeof value !== 'string') return value === null || value === undefined ? [] : [String(value)]; + const text = value.indexOf('\r') === -1 ? value : value.replace(/\r\n?/g, '\n'); + const depth = Number(opts.depth) || 0; + if (depth > MAX_DEPTH) return [text]; + const allMatches = []; + const push = (m, extra) => allMatches.push({ index: m.index, length: m[0].length, ...extra }); + for (const m of text.matchAll(ESCAPE_RE)) push(m, { type: 'escaped', body: m[1] }); + for (const m of text.matchAll(WIKILINK_RE)) push(m, { type: 'wikilink', target: linkTarget(m[1]), label: (m[2] || linkTarget(m[1])).trim() }); + for (const m of text.matchAll(CODE_BLOCK_RE)) push(m, { type: 'code-block', body: codeBlockBody(m[1]) }); + for (const m of text.matchAll(RULE_RE)) push(m, { type: 'rule' }); + for (const m of text.matchAll(HEADER_CLOSED_RE)) push(m, { type: 'header', level: headerLevel(m[1]), body: m[2] }); + for (const m of text.matchAll(HEADER_SPACED_RE)) push(m, { type: 'header', level: headerLevel(m[1]), body: m[2] }); + for (const m of text.matchAll(QUOTE_RE)) push(m, { type: 'quote', body: m[1] }); + for (const m of text.matchAll(BULLET_RE)) push(m, { type: 'bullet', level: indentLevel(m[1]), body: m[2] }); + for (const m of text.matchAll(NUMBER_RE)) push(m, { type: 'number', level: indentLevel(m[1]), marker: `${m[2]}${m[3]}`, body: m[4] }); + for (const m of text.matchAll(BLOB_IMAGE_RE)) push(m, { type: 'blob-image', name: m[1], blob: m[2] }); + for (const m of text.matchAll(BLOB_VIDEO_RE)) push(m, { type: 'blob-video', name: m[1], blob: m[2] }); + for (const m of text.matchAll(BLOB_AUDIO_RE)) push(m, { type: 'blob-audio', name: m[1], blob: m[2] }); + for (const m of text.matchAll(BLOB_PDF_RE)) push(m, { type: 'blob-pdf', name: m[1], blob: m[2] }); + for (const m of text.matchAll(BLOB_TORRENT_RE)) push(m, { type: 'blob-file', name: m[1], blob: m[2] }); + for (const m of text.matchAll(BLOB_FILE_RE)) push(m, { type: 'blob-file', name: m[1], blob: m[2] }); + for (const m of text.matchAll(MD_MENTION_RE)) push(m, { type: 'md-mention', name: m[1], feedId: m[2] }); + for (const m of text.matchAll(RAW_MENTION_RE)) push(m, { type: 'raw-mention', feedId: m[1] }); + for (const m of text.matchAll(BARE_MENTION_RE)) { + const name = bareMentionName(m[2]); + if (name) allMatches.push({ index: m.index + m[1].length, length: name.length + 1, type: 'bare-mention', name }); + } + for (const m of text.matchAll(MSG_REF_RE)) push(m, { type: 'msg-ref', id: m[0] }); + for (const m of text.matchAll(MD_LINK_RE)) push(m, { type: 'md-link', label: m[1], href: m[2] }); + for (const m of text.matchAll(BOLD_RE)) push(m, { type: 'bold', body: m[1] }); + for (const m of text.matchAll(UNDERLINE_RE)) push(m, { type: 'underline', body: m[1] }); + for (const m of text.matchAll(STRIKE_RE)) push(m, { type: 'strike', body: m[1] }); + for (const m of text.matchAll(ITALIC_RE)) push(m, { type: 'italic', body: m[1] }); + for (const m of text.matchAll(INLINE_CODE_RE)) push(m, { type: 'inline-code', body: m[1] }); + for (const m of text.matchAll(URL_RE)) { + const trimmed = m[0].replace(URL_TAIL_RE, ''); + if (trimmed) allMatches.push({ index: m.index, length: trimmed.length, type: 'url', text: trimmed }); + } + for (const m of text.matchAll(EMAIL_RE)) push(m, { type: 'email', text: m[0] }); + for (const m of text.matchAll(HASHTAG_RE)) push(m, { type: 'hashtag', tag: m[0].slice(1) }); + allMatches.sort((x, y) => x.index - y.index); + const filtered = []; + let lastEnd = 0; + for (const m of allMatches) { + if (m.index < lastEnd) continue; + filtered.push(m); + lastEnd = m.index + m.length; + } + const blobHref = (id) => String(id).startsWith('/blob/sealed/') ? String(id) : `${opts.blobPrefix || '/blob/'}${encodeURIComponent(id)}`; + const linksOn = opts.links !== false; + const internalOn = opts.internalLinks !== false; + const plain = (value) => opts.plainUrlClass ? span({ class: opts.plainUrlClass }, value) : value; + const inner = (body) => renderStyledText(body, { ...opts, depth: depth + 1 }); + const result = []; + let cursor = 0; + for (const m of filtered) { + if (cursor < m.index) result.push(text.slice(cursor, m.index)); + if (m.type === 'escaped') { + result.push(m.body); + } else if (m.type === 'msg-ref') { + result.push(internalOn ? a({ href: `/thread/${encodeURIComponent(m.id)}`, class: 'styled-link' }, m.id.slice(0, 10) + '...') : m.id); + } else if (m.type === 'wikilink') { + result.push(typeof opts.wikiLink === 'function' + ? opts.wikiLink(m.target, slugify(m.target), m.label) + : internalOn + ? a({ href: `/wiki/${encodeURIComponent(slugify(m.target))}`, class: 'wiki-link' }, m.label) + : m.label); + } else if (m.type === 'code-block') { + result.push(span({ class: 'rt-code-block' }, m.body)); + } else if (m.type === 'rule') { + result.push(span({ class: 'rt-rule' })); + } else if (m.type === 'header') { + result.push(span({ class: `rt-header rt-header-${m.level}` }, ...inner(m.body))); + } else if (m.type === 'quote') { + result.push(span({ class: 'rt-quote' }, ...inner(m.body))); + } else if (m.type === 'bullet') { + result.push(span({ class: `rt-item rt-item-${m.level}` }, ...inner(m.body))); + } else if (m.type === 'number') { + result.push(span({ class: `rt-item rt-item-number rt-item-${m.level}` }, `${m.marker} `, ...inner(m.body))); + } else if (m.type === 'bold') { + result.push(strong(...inner(m.body))); + } else if (m.type === 'underline') { + result.push(u(...inner(m.body))); + } else if (m.type === 'strike') { + result.push(strike(...inner(m.body))); + } else if (m.type === 'italic') { + result.push(em(...inner(m.body))); + } else if (m.type === 'inline-code') { + result.push(code({ class: 'rt-code' }, m.body)); + } else if (m.type === 'blob-image') { + const imageSrc = blobHref(m.blob); + if (opts.zoomImages) { + zoomSeq = (zoomSeq + 1) % 1000000; + const zoomId = `rtzoom-${zoomSeq}-${String(m.blob).replace(/[^a-zA-Z0-9]/g, '').slice(-10)}`; + result.push(a({ href: `#${zoomId}`, id: `${zoomId}-src`, class: 'zoom-link' }, img({ src: imageSrc, alt: m.name || '', class: 'post-image' }))); + result.push(span({ id: zoomId, class: 'lightbox' }, + a({ href: `${imageSrc}${imageSrc.includes('?') ? '&' : '?'}download=1`, class: 'lightbox-download', title: getI18n().fileShareDownload || 'Download' }, '\u2913'), + a({ href: `#${zoomId}-src`, class: 'lightbox-close' }, '\u00d7'), + img({ src: imageSrc, alt: m.name || '', class: 'lightbox-image' }) + )); + } else { + result.push(img({ src: imageSrc, alt: m.name || '', class: 'post-image' })); + } + } else if (m.type === 'blob-video') { + result.push(video({ controls: true, class: 'post-video', src: blobHref(m.blob) })); + } else if (m.type === 'blob-audio') { + result.push(audio({ controls: true, class: 'post-audio', src: blobHref(m.blob) })); + } else if (m.type === 'blob-pdf') { + const i18n = getI18n(); + result.push(a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, m.name || i18n.pdfFallbackLabel || 'PDF')); + } else if (m.type === 'blob-file') { + result.push(a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, m.name || m.blob.slice(1, 9))); + } else if (m.type === 'md-mention') { + result.push(internalOn ? a({ href: `/author/${encodeURIComponent('@' + m.feedId)}`, class: 'mention' }, '@' + m.name) : '@' + m.name); + } else if (m.type === 'raw-mention') { + result.push(internalOn ? a({ href: `/author/${encodeURIComponent('@' + m.feedId)}`, class: 'mention' }, '@' + m.feedId.slice(0, 8) + '...') : '@' + m.feedId.slice(0, 8) + '...'); + } else if (m.type === 'bare-mention') { + const feedId = internalOn && mentionResolver ? mentionResolver(m.name) : null; + result.push(feedId ? a({ href: `/author/${encodeURIComponent(feedId)}`, class: 'mention' }, '@' + m.name) : '@' + m.name); + } else if (m.type === 'md-link') { + const external = /^https?:\/\//.test(m.href) && !SELF_HOST_RE.test(m.href); + result.push(external + ? (linksOn ? a({ href: m.href, class: 'styled-link', target: '_blank', rel: 'noopener noreferrer' }, ...inner(m.label)) : plain(m.label)) + : (internalOn ? a({ href: m.href, class: 'styled-link' }, ...inner(m.label)) : m.label)); + } else if (m.type === 'url') { + const schemeMatch = m.text.match(/^(https?):\/\//i); + const href = schemeMatch ? `${schemeMatch[1].toLowerCase()}://${m.text.slice(schemeMatch[0].length)}` : `https://${m.text}`; + const internalTarget = internalTargetOf(href); + if (!linksOn) result.push(plain(m.text)); + else if (internalTarget) { + result.push(internalOn + ? a({ href: internalTarget.href, class: 'internal-link' }, + span({ class: 'internal-link-kind' }, internalTarget.kind.toUpperCase()), + internalTarget.id ? span({ class: 'internal-link-id' }, shortInternalId(internalTarget.id)) : null + ) + : plain(internalTarget.href)); + } + else if (SELF_HOST_RE.test(href)) result.push(a({ href }, m.text)); + else result.push(a({ href, target: '_blank', rel: 'noopener noreferrer' }, m.text)); + } else if (m.type === 'email') { + result.push(linksOn ? a({ href: `mailto:${m.text}` }, m.text) : plain(m.text)); + } else if (m.type === 'hashtag') { + const tagHref = typeof opts.hashtagHref === 'function' ? opts.hashtagHref(m.tag) : (internalOn ? `/search?query=%23${encodeURIComponent(m.tag)}` : null); + result.push(tagHref ? a({ href: tagHref, class: 'tag-link tag-inline' }, `#${m.tag}`) : `#${m.tag}`); + } + cursor = m.index + m.length; + } + if (cursor < text.length) result.push(text.slice(cursor)); + return result; +} + +const safeExternalHref = (url) => { + const value = String(url || '').trim(); + const lower = value.toLowerCase(); + if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('mailto:')) return value; + return ''; +}; + +const escapeHtml = (value) => String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +function renderStyledHtml(text, opts = {}) { + if (text === null || text === undefined || text === '') return ''; + return renderStyledText(String(text), opts) + .map(node => (node && typeof node === 'object' && node.outerHTML) ? node.outerHTML : escapeHtml(node)) + .join(''); +} + +function plainText(text) { + if (text === null || text === undefined) return ''; + return String(text) + .replace(/```[\s\S]*?```/g, '') + .replace(/^[ \t]*>[ \t]?/gm, '') + .replace(/^[ \t]*---[ \t]*$/gm, '') + .replace(/^[ \t]*#{1,6}[ \t]*/gm, '') + .replace(/[ \t]*#{1,6}[ \t]*$/gm, '') + .replace(/^[ \t]*- /gm, '') + .replace(/^[ \t]*\d{1,3}[.)] /gm, '') + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/__(.*?)__/g, '$1') + .replace(/~~(.*?)~~/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/!?\[[^\]\n]{0,160}\]\(\s*&[^)\s\n]{1,120}\.sha256\s*\)/g, '') + .replace(/\[(?:video|audio|pdf):[^\]\n]{0,160}\]\([^)\n]{0,200}\)/g, '') + .replace(/\[\[([^\]|\n]{1,120})(?:\|([^\]\n]{1,120}))?\]\]/g, (m, target, label) => label || target) + .replace(/!?\[([^\]\n]{1,160})\]\(([^)\s\n]{0,200})\)/g, (m, label, href) => href ? `${label} (${href})` : label) + .replace(/\\([\\*_~`#>\-\[\]!])/g, '$1') + .trim(); +} + +function renderTextPreview(text, maxLength = 220) { + const preview = plainText(text).replace(/\n+/g, ' ').trim(); + return preview.length > maxLength ? preview.slice(0, maxLength) + '...' : preview; +} + +const FORMAT_MARKS = [ + { mark: '**bold**', style: 'bold' }, + { mark: '*italic*', style: 'italic' }, + { mark: '__underline__', style: 'underline' }, + { mark: '~~strike~~', style: 'strike' }, + { mark: '`code`', style: 'code' }, + { mark: '### title ###', style: 'header' }, + { mark: '- list', style: 'item' }, + { mark: '> quote', style: 'quote' }, + { mark: '[[wiki]]', style: 'wiki' } +]; + +function renderFormatBar() { + const i18n = getI18n(); + const marks = []; + FORMAT_MARKS.forEach((m, i) => { + if (i) marks.push(span({ class: 'rt-sep' }, '|')); + marks.push(span({ class: `rt-mark rt-mark-${m.style}` }, m.mark)); + }); + return div({ class: 'rt-toolbar' }, + span({ class: 'rt-toolbar-label' }, `${i18n.formatBarLabel || 'Format'}:`), + ...marks + ); +} + +function richTextarea(attrs, ...children) { + return [renderFormatBar(), textarea(attrs, ...children)]; +} + +module.exports = { renderStyledText, renderStyledHtml, renderTextPreview, plainText, escapeHtml, safeExternalHref, renderFormatBar, richTextarea, setMentionResolver, INTERNAL_PATHS }; diff --git a/src/backend/renderTextWithStyles.js b/src/backend/renderTextWithStyles.js deleted file mode 100644 index c403a45..0000000 --- a/src/backend/renderTextWithStyles.js +++ /dev/null @@ -1,139 +0,0 @@ -const i18nBase = require("../client/assets/translations/i18n"); - -function getI18n() { - try { - const { i18n } = require("../views/main_views"); - return i18n; - } catch (_) { - return i18nBase['en'] || {}; - } -} - -function renderTextPreview(text, maxLength = 220) { - if (!text) return '' - - let preview = String(text) - - preview = preview - .replace(/```[\s\S]*?```/g, '') - .replace(/^>.*$/gm, '') - .replace(/^#{1,6}\s*/gm, '') - .replace(/^- /gm, '') - .replace(/^\d+\. /gm, '') - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/\*(.*?)\*/g, '$1') - .replace(/`([^`]+)`/g, '$1') - .replace(/!\[.*?\]\(.*?\)/g, '') - .replace(/\[.*?\]\(.*?\)/g, '') - .replace(/\n+/g, ' ') - .trim() - - if (preview.length > maxLength) { - preview = preview.slice(0, maxLength) + '...' - } - - return preview -} - -function renderTextWithStyles(text) { - if (!text) return '' - const i18n = getI18n() - - let html = String(text) - - html = html - .replace(/&/g, '&') - .replace(//g, '>') - - html = html - .replace(/```([\s\S]*?)```/gim, '
$1
') - .replace(/^> (.*)$/gim, '
$1
') - .replace(/^---$/gim, '
') - .replace(/^### (.*)$/gim, '

$1

') - .replace(/^## (.*)$/gim, '

$1

') - .replace(/^# (.*)$/gim, '

$1

') - - html = html - .replace(/\*\*(.*?)\*\*/gim, '$1') - .replace(/\*(.*?)\*/gim, '$1') - .replace(/`([^`]+)`/gim, '$1') - - html = html - .replace(/!\[([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, (_, alt, blob) => - `${alt}` - ) - .replace(/\[video:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, (_, _name, blob) => - `` - ) - .replace(/\[audio:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, (_, _name, blob) => - `` - ) - .replace(/\[pdf:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, (_, name, blob) => - `
${name || i18n.pdfFallbackLabel || 'PDF'}` - ) - - html = html - .replace(/\[@([^\]]+)\]\(@?([A-Za-z0-9+/=.\-]+\.ed25519)\)/g, (_, name, id) => - `@${name}` - ) - .replace(/@([A-Za-z0-9+/=.\-]+\.ed25519)/g, (_, id) => - `@${id}` - ) - - const escAttr = (s) => String(s).replace(/"/g, '"').replace(/'/g, ''') - html = html - .replace(/#(\w+)/g, (_, tag) => - `#${escAttr(tag)}` - ) - .replace(/(https?:\/\/[^\s"'<>]+)/g, url => - `${escAttr(url)}` - ) - .replace(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, email => - `${escAttr(email)}` - ) - - const lines = html.split('\n') - let result = '' - let inUL = false - let inOL = false - - for (let line of lines) { - if (/^- /.test(line)) { - if (!inUL) { - result += '
    ' - inUL = true - } - result += `
  • ${line.replace(/^- /, '')}
  • ` - continue - } - - if (/^\d+\. /.test(line)) { - if (!inOL) { - result += '
      ' - inOL = true - } - result += `
    1. ${line.replace(/^\d+\. /, '')}
    2. ` - continue - } - - if (inUL) { - result += '
' - inUL = false - } - - if (inOL) { - result += '' - inOL = false - } - - result += line + '
' - } - - if (inUL) result += '' - if (inOL) result += '' - - return result -} - -module.exports = { renderTextWithStyles, renderTextPreview } diff --git a/src/backend/renderUrl.js b/src/backend/renderUrl.js deleted file mode 100644 index 9267768..0000000 --- a/src/backend/renderUrl.js +++ /dev/null @@ -1,92 +0,0 @@ -const { a, img, video, audio } = require("../server/node_modules/hyperaxe"); -const i18nBase = require("../client/assets/translations/i18n"); - -function getI18n() { - try { - const { i18n } = require("../views/main_views"); - return i18n; - } catch (_) { - return i18nBase['en'] || {}; - } -} - -function renderUrl(text) { - if (typeof text !== 'string') return [text]; - const blobImageRegex = /!\[([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g; - const blobVideoRegex = /\[video:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g; - const blobAudioRegex = /\[audio:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g; - const blobPdfRegex = /\[pdf:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g; - const mdMentionRegex = /\[@([^\]]+)\]\(@?([A-Za-z0-9+/=.\-]+\.ed25519)\)/g; - const rawMentionRegex = /@([A-Za-z0-9+/=.\-]+\.ed25519)/g; - const urlRegex = /\b(?:https?:\/\/|www\.)[^\s]+/g; - const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/gi; - const allMatches = []; - for (const m of text.matchAll(blobImageRegex)) { - allMatches.push({ index: m.index, length: m[0].length, type: 'blob-image', name: m[1], blob: m[2] }); - } - for (const m of text.matchAll(blobVideoRegex)) { - allMatches.push({ index: m.index, length: m[0].length, type: 'blob-video', name: m[1], blob: m[2] }); - } - for (const m of text.matchAll(blobAudioRegex)) { - allMatches.push({ index: m.index, length: m[0].length, type: 'blob-audio', name: m[1], blob: m[2] }); - } - for (const m of text.matchAll(blobPdfRegex)) { - allMatches.push({ index: m.index, length: m[0].length, type: 'blob-pdf', name: m[1], blob: m[2] }); - } - for (const m of text.matchAll(mdMentionRegex)) { - allMatches.push({ index: m.index, length: m[0].length, type: 'md-mention', name: m[1], feedId: m[2] }); - } - for (const m of text.matchAll(rawMentionRegex)) { - allMatches.push({ index: m.index, length: m[0].length, type: 'raw-mention', feedId: m[1] }); - } - for (const m of text.matchAll(urlRegex)) { - allMatches.push({ index: m.index, length: m[0].length, type: 'url', text: m[0] }); - } - for (const m of text.matchAll(emailRegex)) { - allMatches.push({ index: m.index, length: m[0].length, type: 'email', text: m[0] }); - } - allMatches.sort((a, b) => a.index - b.index); - const filtered = []; - let lastEnd = 0; - for (const m of allMatches) { - if (m.index < lastEnd) continue; - filtered.push(m); - lastEnd = m.index + m.length; - } - const result = []; - let cursor = 0; - for (const m of filtered) { - if (cursor < m.index) { - result.push(text.slice(cursor, m.index)); - } - if (m.type === 'blob-image') { - result.push(img({ src: `/blob/${encodeURIComponent(m.blob)}`, alt: m.name || '', class: 'post-image' })); - } else if (m.type === 'blob-video') { - result.push(video({ controls: true, class: 'post-video', src: `/blob/${encodeURIComponent(m.blob)}` })); - } else if (m.type === 'blob-audio') { - result.push(audio({ controls: true, class: 'post-audio', src: `/blob/${encodeURIComponent(m.blob)}` })); - } else if (m.type === 'blob-pdf') { - const i18n = getI18n(); - const label = m.name || i18n.pdfFallbackLabel || 'PDF'; - result.push(a({ href: `/blob/${encodeURIComponent(m.blob)}`, class: 'post-pdf', target: '_blank' }, label)); - } else if (m.type === 'md-mention') { - const feedWithAt = '@' + m.feedId; - result.push(a({ href: `/author/${encodeURIComponent(feedWithAt)}`, class: 'mention' }, '@' + m.name)); - } else if (m.type === 'raw-mention') { - const feedWithAt = '@' + m.feedId; - result.push(a({ href: `/author/${encodeURIComponent(feedWithAt)}`, class: 'mention' }, '@' + m.feedId.slice(0, 8) + '...')); - } else if (m.type === 'url') { - const href = m.text.startsWith('http') ? m.text : `https://${m.text}`; - result.push(a({ href, target: '_blank', rel: 'noopener noreferrer' }, m.text)); - } else if (m.type === 'email') { - result.push(a({ href: `mailto:${m.text}` }, m.text)); - } - cursor = m.index + m.length; - } - if (cursor < text.length) { - result.push(text.slice(cursor)); - } - return result; -} - -module.exports = { renderUrl }; diff --git a/src/backend/request_guards.js b/src/backend/request_guards.js new file mode 100644 index 0000000..bc62e3e --- /dev/null +++ b/src/backend/request_guards.js @@ -0,0 +1,99 @@ +const MSG_KEY_RE = /^%[A-Za-z0-9+/]{43}=\.sha256$/; + +const isMsgKey = (value) => typeof value === 'string' && MSG_KEY_RE.test(value); + +const pickMsgKeys = (value) => { + const list = Array.isArray(value) ? value : (value == null ? [] : [value]); + return Array.from(new Set(list.filter(isMsgKey))); +}; + +const isLocalPath = (value) => { + if (typeof value !== 'string' || !value.startsWith('/')) return false; + if (value.startsWith('//') || value.startsWith('/\\')) return false; + if (/[\r\n\0]/.test(value)) return false; + return true; +}; + +const SHARED_RETURN_PAGES = ['/activity', '/agenda', '/search', '/trending', '/favorites', '/opinions', '/mentions', '/melody', '/author', '/inhabitant', '/tribe']; +const isSharedReturnPage = (rt) => SHARED_RETURN_PAGES.some(p => rt === p || (rt.startsWith(p) && /^[?#/]/.test(rt.slice(p.length)))); +const safeReturnTo = (ctx, fallback, allowedPrefixes) => { + const rt = ctx?.request?.body?.returnTo || ctx?.query?.returnTo; + if (!isLocalPath(rt)) return fallback; + if (isSharedReturnPage(rt)) return rt; + return Array.isArray(allowedPrefixes) && allowedPrefixes.some(p => rt.startsWith(p)) ? rt : fallback; +}; + +const refererPath = (referer, host) => { + if (!referer) return null; + try { + const u = new URL(referer); + if ((u.protocol !== 'http:' && u.protocol !== 'https:') || u.host !== host) return null; + const target = u.pathname + u.search + u.hash; + return isLocalPath(target) ? target : null; + } catch (_) { + return null; + } +}; + +const safeRefererRedirect = (ctx, fallback = '/') => { + const target = refererPath(ctx.request.header.referer, ctx.host); + ctx.redirect(target || fallback); +}; + +const isClearnetPath = (request) => { + const url = String((request && request.url) || ''); + return url === '/c' || url.startsWith('/c/') || url.startsWith('/c?'); +}; + +const isTrustedRequest = (request, validHosts) => { + if (isClearnetPath(request)) return request.method === 'GET'; + if (!Array.isArray(validHosts) || !validHosts.includes(request.hostname)) return false; + if (request.method === 'GET') return true; + const origin = request.header && request.header.origin; + const source = origin && origin !== 'null' ? origin : (request.header && request.header.referer); + if (source == null) return false; + try { + const u = new URL(source); + if ((u.protocol !== 'http:' && u.protocol !== 'https:') || !validHosts.includes(u.hostname)) return false; + if (u.host !== String(request.host || '')) return false; + if (u.pathname.startsWith('/blob/')) return false; + } catch (_) { + return false; + } + return true; +}; + +const publicModeGuard = ({ isPublic, onBlocked }) => async (ctx, next) => { + if (isPublic && ctx.method !== 'GET') { onBlocked(ctx); return; } + await next(); +}; + +const buildCsp = (isClearnet, { frames = false } = {}) => (isClearnet + ? [ + "default-src 'self'", + "script-src 'none'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data:", + "media-src 'self' blob:", + "connect-src 'self'", + "frame-src 'none'", + "form-action 'self'", + "object-src 'none'", + "base-uri 'none'", + "frame-ancestors 'none'" + ] + : [ + "default-src 'self'", + "script-src 'self' http://localhost:3000/js", + "style-src 'self'", + "img-src 'self'", + "media-src 'self' blob:", + "worker-src 'self' blob:", + frames ? "frame-src 'self'" : "frame-src 'none'", + "form-action 'self'", + "object-src 'none'", + "base-uri 'none'", + "frame-ancestors 'none'" + ]).join('; '); + +module.exports = { isMsgKey, pickMsgKeys, isLocalPath, safeReturnTo, refererPath, safeRefererRedirect, isClearnetPath, isTrustedRequest, publicModeGuard, buildCsp }; diff --git a/src/backend/sanitizeHtml.js b/src/backend/sanitizeHtml.js index 85bb4b4..f23d66d 100644 --- a/src/backend/sanitizeHtml.js +++ b/src/backend/sanitizeHtml.js @@ -1,14 +1,32 @@ "use strict"; -const { JSDOM } = require('../server/node_modules/jsdom'); -const DOMPurify = require('../server/node_modules/dompurify'); -const window = new JSDOM('').window; -const purify = DOMPurify(window); +const SAFE_SRC = /^\/(?:blob|image)\/[^\s"'<>]+$/i; +const SAFE_HREF = /^(?:https?:\/\/|\/(?!\/)|#|mailto:)/i; +const SAFE_CLASS = /^(?:rt-[a-z0-9-]+|styled-link|mention|tag-link|tag-inline|zoom-link|pm-quote|wiki-link(?:-missing)?|internal-link(?:-id|-kind)?|lightbox(?:-[a-z]+)?|post-(?:audio|image|pdf|video))$/; +let purifier = null; +const getPurify = () => { + if (purifier) return purifier; + const { JSDOM } = require('../server/node_modules/jsdom'); + const DOMPurify = require('../server/node_modules/dompurify'); + const purify = DOMPurify(new JSDOM('').window); + purify.addHook('uponSanitizeAttribute', (node, data) => { + const name = String(data.attrName || '').toLowerCase(); + const value = String(data.attrValue || '').trim(); + if (name === 'src' && !SAFE_SRC.test(value)) data.keepAttr = false; + if (name === 'href' && !SAFE_HREF.test(value)) data.keepAttr = false; + if (name === 'class') { + const kept = value.split(/\s+/).filter(c => SAFE_CLASS.test(c)); + if (kept.length) data.attrValue = kept.join(' '); else data.keepAttr = false; + } + }); + purifier = purify; + return purifier; +}; +const dropControlChars = (html) => String(html).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ''); const stripDangerousTags = (input) => { if (typeof input !== 'string') return ''; - return purify.sanitize(input, { - USE_PROFILES: { html: true }, + return dropControlChars(getPurify().sanitize(input, { ALLOWED_TAGS: [ 'p', 'br', 'b', 'strong', 'i', 'em', 'u', @@ -19,13 +37,12 @@ const stripDangerousTags = (input) => { ALLOWED_ATTR: [], FORBID_TAGS: ['svg', 'math', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button', 'script'], FORBID_ATTR: ['style'] - }); + })); }; const sanitizeHtml = (input) => { if (typeof input !== 'string') return ''; - return purify.sanitize(input, { - USE_PROFILES: { html: true }, + return dropControlChars(getPurify().sanitize(input, { ALLOWED_TAGS: [ 'p', 'br', 'hr', 'b', 'strong', 'i', 'em', 'u', 's', 'del', @@ -39,7 +56,7 @@ const sanitizeHtml = (input) => { ALLOWED_ATTR: ['href', 'class', 'target', 'rel', 'src', 'alt', 'title', 'controls'], FORBID_TAGS: ['svg', 'math', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button', 'script', 'style', 'link', 'meta'], FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onsubmit', 'onchange', 'style'] - }); + })); }; module.exports = { stripDangerousTags, sanitizeHtml }; diff --git a/src/backend/smartContractPdf.js b/src/backend/smartContractPdf.js deleted file mode 100644 index 66b8baf..0000000 --- a/src/backend/smartContractPdf.js +++ /dev/null @@ -1,228 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const LOGO_PATH = path.join(__dirname, '..', 'client', 'assets', 'images', 'snh-oasis.jpg'); - -const escapePdf = s => String(s || '').replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); - -const wrap = (txt, max = 82) => { - const out = []; - for (const raw of String(txt || '').split('\n')) { - let line = raw; - while (line.length > max) { - let cut = line.lastIndexOf(' ', max); - if (cut <= 0) cut = max; - out.push(line.slice(0, cut)); - line = line.slice(cut).replace(/^\s+/, ''); - } - out.push(line); - } - return out; -}; - -const readJpegDims = (buf) => { - let i = 2; - while (i < buf.length) { - if (buf[i] !== 0xFF) return null; - const marker = buf[i + 1]; - if (marker === 0xD8 || marker === 0xD9) { i += 2; continue; } - const len = buf.readUInt16BE(i + 2); - if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) { - const h = buf.readUInt16BE(i + 5); - const w = buf.readUInt16BE(i + 7); - const c = buf[i + 9]; - return { w, h, c }; - } - i += 2 + len; - } - return null; -}; - -function buildSmartContractPdf({ transfer, block, viewerId }) { - const pageW = 612; - const pageH = 792; - const marginX = 50; - const headerH = 90; - const footerH = 40; - const bodyTop = pageH - headerH - 24; - const bodyBottom = footerH + 10; - const lineH = 14; - - let logoBuf = null; - let logoDims = null; - try { - logoBuf = fs.readFileSync(LOGO_PATH); - logoDims = readJpegDims(logoBuf); - } catch (_) {} - - const t = transfer || {}; - const b = block || {}; - const fmt = v => (v === undefined || v === null) ? '' : String(v); - const fmtAmount = () => { - const cat = String(t.category || 'ECONOMIC').toUpperCase(); - const unit = cat === 'TIME' ? 'h' : cat === 'TRUST' ? 'trust' : 'ECO'; - return `${Number(t.amount || 0).toFixed(6)} ${unit}`; - }; - const fmtDate = v => v ? new Date(v).toISOString().replace('T', ' ').slice(0, 19) + ' UTC' : ''; - const confirmedBy = Array.isArray(t.confirmedBy) ? t.confirmedBy : []; - const required = t.from === t.to ? 1 : 2; - const confirmedCount = confirmedBy.length; - const tags = Array.isArray(t.tags) ? t.tags.join(', ') : ''; - - const sections = []; - sections.push({ kind: 'title', text: `Concept: ${t.concept || '-'}` }); - sections.push({ kind: 'blank' }); - - sections.push({ kind: 'section', text: 'OASIS IDs' }); - sections.push({ kind: 'kv', label: 'From', value: fmt(t.from) }); - sections.push({ kind: 'kv', label: 'To', value: fmt(t.to) }); - sections.push({ kind: 'blank' }); - - sections.push({ kind: 'section', text: 'TERMS' }); - sections.push({ kind: 'kv', label: 'Category', value: String(t.category || 'ECONOMIC').toUpperCase() }); - if (String(t.category || '').toUpperCase() !== 'TRUST') sections.push({ kind: 'kv', label: 'Amount', value: fmtAmount() }); - sections.push({ kind: 'kv', label: 'Status', value: fmt(t.status) }); - if (t.deadline) sections.push({ kind: 'kv', label: 'Deadline', value: fmtDate(t.deadline) }); - if (tags) sections.push({ kind: 'kv', label: 'Tags', value: tags }); - sections.push({ kind: 'blank' }); - - sections.push({ kind: 'section', text: 'CONFIRMATIONS' }); - sections.push({ kind: 'kv', label: 'Required', value: String(required) }); - sections.push({ kind: 'kv', label: 'Confirmed', value: String(confirmedCount) }); - if (confirmedBy.length) { - for (const f of confirmedBy) sections.push({ kind: 'kv', label: 'Signed by', value: fmt(f) }); - } - sections.push({ kind: 'blank' }); - - sections.push({ kind: 'section', text: 'BLOCK VERIFICATION' }); - if (b && b.id) { - sections.push({ kind: 'kv', label: 'Block ID', value: fmt(b.id) }); - if (b.ts) sections.push({ kind: 'kv', label: 'Block Timestamp', value: fmtDate(b.ts) }); - if (b.type) sections.push({ kind: 'kv', label: 'Block Type', value: String(b.type).toUpperCase() }); - if (b.author) sections.push({ kind: 'kv', label: 'Block Author', value: fmt(b.author) }); - if (b.size) sections.push({ kind: 'kv', label: 'Block Size', value: `${b.size} bytes` }); - } else { - sections.push({ kind: 'kv', label: 'Block ID', value: fmt(t.id) }); - } - sections.push({ kind: 'blank' }); - - sections.push({ kind: 'section', text: 'METADATA' }); - sections.push({ kind: 'kv', label: 'Transfer ID', value: fmt(t.id) }); - if (t.createdAt) sections.push({ kind: 'kv', label: 'Created At', value: fmtDate(t.createdAt) }); - if (t.updatedAt) sections.push({ kind: 'kv', label: 'Updated At', value: fmtDate(t.updatedAt) }); - - const lines = []; - for (const s of sections) { - if (s.kind === 'kv') { - const txt = `${s.label}: ${s.value}`; - for (const w of wrap(txt, 82)) lines.push({ kind: 'kv', text: w }); - } else { - lines.push(s); - } - } - - const maxBodyLines = Math.floor((bodyTop - bodyBottom) / lineH); - const pages = []; - for (let i = 0; i < lines.length; i += maxBodyLines) pages.push(lines.slice(i, i + maxBodyLines)); - if (!pages.length) pages.push([]); - - const objects = []; - const addObj = body => { objects.push(body); return objects.length; }; - - const catalogId = addObj(null); - const pagesId = addObj(null); - const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>'); - const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold >>'); - let logoXObjId = null; - if (logoBuf && logoDims) { - const cs = logoDims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; - const dict = `<< /Type /XObject /Subtype /Image /Width ${logoDims.w} /Height ${logoDims.h} /ColorSpace ${cs} /BitsPerComponent 8 /Filter /DCTDecode /Length ${logoBuf.length} >>`; - logoXObjId = addObj({ dict, stream: logoBuf }); - } - - const exportDate = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; - const footerLeft = `Generated: ${exportDate}`; - - const pageIds = []; - const contentIds = []; - - pages.forEach((pg, pgIdx) => { - const parts = []; - if (logoXObjId) { - const logoH = 60; - const logoW = Math.round((logoDims.w / logoDims.h) * logoH); - parts.push(`q\n${logoW} 0 0 ${logoH} ${marginX} ${pageH - headerH + 15} cm\n/Logo Do\nQ`); - } - const titleX = (logoXObjId ? marginX + 80 : marginX); - const titleY = pageH - 45; - parts.push(`BT\n/F2 16 Tf\n${titleX} ${titleY} Td\n(${escapePdf('OASIS - Smart Contract')}) Tj\nET`); - const prefix = 'Issued to: '; - const prefixW = prefix.length * 5.4; - parts.push(`BT\n/F1 9 Tf\n${titleX} ${titleY - 16} Td\n(${escapePdf(prefix)}) Tj\nET`); - parts.push(`BT\n/F2 9 Tf\n${titleX + prefixW} ${titleY - 16} Td\n(${escapePdf(String(viewerId || ''))}) Tj\nET`); - - parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${pageH - headerH} m\n${pageW - marginX} ${pageH - headerH} l\nS\nQ`); - - let y = bodyTop; - for (const ln of pg) { - if (ln.kind === 'title') { - parts.push(`BT\n/F2 14 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - } else if (ln.kind === 'subtitle') { - parts.push(`BT\n/F1 11 Tf\n0.2 0.2 0.2 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - } else if (ln.kind === 'section') { - parts.push(`BT\n/F2 11 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - parts.push(`q\n0 0 0 RG\n0.5 w\n${marginX} ${y - 3} m\n${pageW - marginX} ${y - 3} l\nS\nQ`); - } else if (ln.kind === 'kv') { - parts.push(`BT\n/F1 10 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - } - y -= lineH; - } - - parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${footerH + 5} m\n${pageW - marginX} ${footerH + 5} l\nS\nQ`); - parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 10} Td\n(${escapePdf(footerLeft)}) Tj\nET`); - const pageLabel = `Page ${pgIdx + 1} of ${pages.length}`; - const pageLabelW = pageLabel.length * 4.8; - parts.push(`BT\n/F1 8 Tf\n${pageW - marginX - pageLabelW} ${footerH - 10} Td\n(${escapePdf(pageLabel)}) Tj\nET`); - - const content = parts.join('\n'); - const stream = `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}\nendstream`; - const cid = addObj(stream); - contentIds.push(cid); - const pid = addObj(null); - pageIds.push(pid); - }); - - for (let i = 0; i < pageIds.length; i++) { - const resources = logoXObjId - ? `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> /XObject << /Logo ${logoXObjId} 0 R >> >>` - : `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> >>`; - objects[pageIds[i] - 1] = `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${pageW} ${pageH}] /Contents ${contentIds[i]} 0 R /Resources ${resources} >>`; - } - objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; - objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageIds.map(id => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`; - - const chunks = []; - const offsets = [0]; - let byteLen = 0; - const push = (buf) => { chunks.push(buf); byteLen += buf.length; }; - push(Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary')); - for (let i = 0; i < objects.length; i++) { - offsets.push(byteLen); - const obj = objects[i]; - if (obj && typeof obj === 'object' && obj.dict && obj.stream) { - push(Buffer.from(`${i + 1} 0 obj\n${obj.dict}\nstream\n`, 'binary')); - push(obj.stream); - push(Buffer.from('\nendstream\nendobj\n', 'binary')); - } else { - push(Buffer.from(`${i + 1} 0 obj\n${obj}\nendobj\n`, 'binary')); - } - } - const xrefStart = byteLen; - let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; - for (let i = 1; i <= objects.length; i++) xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; - xref += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xrefStart}\n%%EOF`; - push(Buffer.from(xref, 'binary')); - return Buffer.concat(chunks); -} - -module.exports = { buildSmartContractPdf }; diff --git a/src/backend/updater.js b/src/backend/updater.js index bb2d660..2f6f3b3 100644 --- a/src/backend/updater.js +++ b/src/backend/updater.js @@ -1,12 +1,50 @@ -const fetch = require('../server/node_modules/node-fetch'); const { existsSync, readFileSync, writeFileSync, unlinkSync } = require('fs'); const { join } = require('path'); const localpackage = join(__dirname, '../server/package.json'); -const remoteUrl = 'https://code.03c8.net/KrakensLab/oasis/raw/master/src/server/package.json'; // Official SNH-Oasis -const remoteUrl2 = 'https://raw.githubusercontent.com/epsylon/oasis/refs/heads/main/src/server/package.json'; // Mirror SNH-Oasis +const remoteUrl = 'https://code.03c8.net/KrakensLab/oasis/raw/master/src/server/package.json'; +const remoteUrl2 = 'https://raw.githubusercontent.com/epsylon/oasis/refs/heads/main/src/server/package.json'; let printed = false; +const repoRoot = join(__dirname, '..', '..'); +const updateFlagFile = join(__dirname, '../server/.update_required'); + +const git = (args, cwd = repoRoot) => new Promise((resolve) => { + require('child_process').execFile('git', args, { cwd, timeout: 60000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } }, (err, out) => resolve(err ? '' : String(out || '').trim())); +}); +const commitOf = (line) => { + const [hash, date, ...rest] = String(line || '').split('\t'); + return hash ? { hash, date: date || '', subject: rest.join('\t') } : null; +}; +const cleanOrigin = (url) => String(url || '').replace(/\/\/[^@\/]*@/, '//').replace(/\.git$/, ''); + +async function collectDetails(root = repoRoot) { + if (!existsSync(join(root, '.git'))) return null; + const installed = commitOf(await git(['log', '-1', '--format=%h%x09%cI%x09%s'], root)); + await git(['fetch', '--quiet'], root); + const upstream = (await git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], root)) || 'origin/master'; + const available = commitOf(await git(['log', '-1', '--format=%h%x09%cI%x09%s', upstream], root)); + const commits = (await git(['log', '--format=%h%x09%cI%x09%s', '-n', '10', `HEAD..${upstream}`], root)).split('\n').map(commitOf).filter(Boolean); + const count = Number(await git(['rev-list', '--count', `HEAD..${upstream}`], root)) || commits.length; + const stat = await git(['diff', '--shortstat', 'HEAD', upstream], root); + const num = (re) => { const m = stat.match(re); return m ? Number(m[1]) : 0; }; + return { + installed, available, commits, count, + files: num(/(\d+) files? changed/), insertions: num(/(\d+) insertions?/), deletions: num(/(\d+) deletions?/), + origin: cleanOrigin(await git(['remote', 'get-url', 'origin'], root)) + }; +} +exports.collectDetails = collectDetails; + +exports.readUpdateInfo = () => { + if (!existsSync(updateFlagFile)) return null; + try { + const info = JSON.parse(readFileSync(updateFlagFile, 'utf8')); + return info && info.required ? info : { required: true }; + } catch (_) { + return { required: true }; + } +}; async function extractVersionFromText(text) { try { @@ -33,11 +71,22 @@ async function diffVersion(body, callback) { const updateFlagPath = join(__dirname, "../server/.update_required"); if (remoteVersion !== localVersion) { - writeFileSync(updateFlagPath, JSON.stringify({ required: true })); + const alreadyKnown = existsSync(updateFlagPath); + writeFileSync(updateFlagPath, JSON.stringify({ required: true, version: remoteVersion, installedVersion: localVersion })); + collectDetails().then((details) => { + if (details && existsSync(updateFlagPath)) writeFileSync(updateFlagPath, JSON.stringify({ required: true, version: remoteVersion, installedVersion: localVersion, details })); + }).catch(() => {}); + if (!alreadyKnown) { + try { + const { notify, i18nNow } = require('./desktopNotify'); + const i18n = i18nNow(); + notify(i18n.notifyUpdateLabel, `${i18n.updateBannerText} (${remoteVersion})`); + } catch (_) {} + } callback("required"); } else { if (existsSync(updateFlagPath)) unlinkSync(updateFlagPath); - callback(""); // no updates required + callback(""); } } catch (error) { console.error("Error comparing versions:", error.message); @@ -49,15 +98,8 @@ async function checkMirror(callback) { try { const response = await fetch(remoteUrl2, { method: 'GET', - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br', - 'Connection': 'keep-alive', - 'Referer': 'https://raw.githubusercontent.com', - 'Origin': 'https://raw.githubusercontent.com' - } + headers: { 'Accept': 'application/json, text/plain, */*' }, + signal: AbortSignal.timeout(20000) }); if (!response.ok) { @@ -72,20 +114,154 @@ async function checkMirror(callback) { } } +const networkAllowed = () => { + if (process.argv.includes('--offline') || process.env.OASIS_NETWORK_PAUSED === '1') return false; + try { if (require('../server/ssb_config').offline === true) return false; } catch (_) {} + try { if (require('../configs/config-manager.js').getConfig().networkPaused === true) return false; } catch (_) {} + return true; +}; + +const MOBILE_RELEASE_SOURCES = [ + { url: 'https://code.03c8.net/KrakensLab/oasis/releases', type: 'html' }, + { url: 'https://api.github.com/repos/epsylon/oasis/releases?per_page=20', type: 'json' } +]; +const MOBILE_APK_RE = /^oasis-v(\d+\.\d+\.\d+)\.apk$/i; +const MOBILE_APK_HTML_RE = /oasis-v(\d+\.\d+\.\d+)\.apk/i; +const ECOIN_RELEASES_API = 'https://api.github.com/repos/epsylon/ecoin/releases?per_page=20'; +const ECOIN_RELEASES_PAGE = 'https://github.com/epsylon/ecoin/releases'; +const ECOIN_APK_RE = /^ecoin-wallet-v(\d+(?:\.\d+)*)\.apk$/i; +const MOBILE_FETCH_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9' +}; + +let ecoinWalletApk = ''; + +const versionParts = (v) => String(v || '').split('.').map(n => parseInt(n, 10) || 0); + +const isNewerVersion = (remote, local) => { + const a = versionParts(remote), b = versionParts(local); + const len = Math.max(a.length, b.length, 3); + for (let i = 0; i < len; i++) { + if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0); + } + return false; +}; + +const localMobileVersion = () => { + const fromEnv = String(process.env.OASIS_ANDROID_VERSION || '').split('-')[0].trim(); + if (fromEnv) return fromEnv; + try { + return JSON.parse(readFileSync(localpackage, 'utf8')).version || ''; + } catch (error) { + return ''; + } +}; + +const pickReleaseFromJson = (releases) => { + for (const rel of Array.isArray(releases) ? releases : []) { + if (!rel || rel.draft) continue; + for (const asset of Array.isArray(rel.assets) ? rel.assets : []) { + const match = MOBILE_APK_RE.exec(String((asset && asset.name) || '')); + if (match) { + return { + version: match[1], + page: String(rel.html_url || ''), + apk: String(asset.browser_download_url || '') + }; + } + } + } + return null; +}; + +const pickReleaseFromHtml = (html, pageUrl) => { + const match = MOBILE_APK_HTML_RE.exec(String(html || '')); + return match ? { version: match[1], page: pageUrl, apk: '' } : null; +}; + +const fetchMobileRelease = async (source) => { + const isHtml = source.type === 'html'; + const response = await fetch(source.url, { + method: 'GET', + headers: { ...MOBILE_FETCH_HEADERS, 'Accept': isHtml ? 'text/html,application/xhtml+xml' : 'application/json, text/plain, */*' } + }); + if (!response.ok) throw new Error(`Request failed with status ${response.status}`); + return isHtml + ? pickReleaseFromHtml(await response.text(), source.url) + : pickReleaseFromJson(await response.json()); +}; + +const refreshEcoinWalletApk = async () => { + try { + const response = await fetch(ECOIN_RELEASES_API, { + method: 'GET', + headers: { ...MOBILE_FETCH_HEADERS, 'Accept': 'application/json, text/plain, */*' } + }); + if (!response.ok) return; + const releases = await response.json(); + let best = null; + for (const rel of Array.isArray(releases) ? releases : []) { + if (!rel || rel.draft) continue; + for (const asset of Array.isArray(rel.assets) ? rel.assets : []) { + const match = ECOIN_APK_RE.exec(String((asset && asset.name) || '')); + if (!match || !asset.browser_download_url) continue; + if (!best || isNewerVersion(match[1], best.version)) best = { version: match[1], apk: String(asset.browser_download_url) }; + } + } + if (best) ecoinWalletApk = best.apk; + } catch (error) {} +}; + +exports.ecoinWalletDownloadUrl = () => { + if (String(process.env.OASIS_ANDROID_INSTALLER || '') === 'com.android.vending') return ECOIN_RELEASES_PAGE; + if (!ecoinWalletApk) refreshEcoinWalletApk(); + return ecoinWalletApk || ECOIN_RELEASES_PAGE; +}; + +exports.getMobileRemoteVersion = async () => { + await refreshEcoinWalletApk(); + if (String(process.env.OASIS_ANDROID_INSTALLER || '') === 'com.android.vending') return; + const localVersion = localMobileVersion(); + if (!localVersion) return; + const updateFlagPath = join(__dirname, '../server/.update_required'); + const found = []; + for (const source of MOBILE_RELEASE_SOURCES) { + try { + const candidate = await fetchMobileRelease(source); + if (candidate) found.push(candidate); + } catch (error) {} + } + if (!found.length) return; + const top = found.reduce((best, r) => (isNewerVersion(r.version, best.version) ? r : best), found[0]); + const sameVersion = found.filter(r => r.version === top.version); + const withApk = sameVersion.find(r => r.apk); + const release = { version: top.version, page: sameVersion[0].page, apk: withApk ? withApk.apk : '' }; + if (isNewerVersion(release.version, localVersion)) { + try { + writeFileSync(updateFlagPath, JSON.stringify({ + required: true, + version: release.version, + installedVersion: localVersion, + page: release.page || release.apk, + apk: release.apk + })); + } catch (error) {} + console.log(`\noasis@version: Oasis ${release.version} is available at ${release.apk || release.page}\n`); + } else if (existsSync(updateFlagPath)) { + try { unlinkSync(updateFlagPath); } catch (error) {} + } +}; + exports.getRemoteVersion = async () => { - if (existsSync('../../.git')) { + if (process.env.OASIS_MOBILE === '1') return exports.getMobileRemoteVersion(); + if (!networkAllowed()) return; + if (existsSync(join(__dirname, '..', '..', '.git'))) { try { const response = await fetch(remoteUrl, { method: 'GET', - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br', - 'Connection': 'keep-alive', - 'Referer': 'https://code.03c8.net', - 'Origin': 'https://code.03c8.net' - } + headers: { 'Accept': 'application/json, text/plain, */*' }, + signal: AbortSignal.timeout(20000) }); if (!response.ok) { diff --git a/src/backend/vote_tally.js b/src/backend/vote_tally.js index 54f02af..d8e4599 100644 --- a/src/backend/vote_tally.js +++ b/src/backend/vote_tally.js @@ -1,116 +1,119 @@ -const buildVoteTally = (messages) => { - const nodes = new Map(); - const ballots = []; +const INSTITUTIONAL_VOTE_TAG = /^(gov|govMethod|courtsCase|courtsMethod):/; - for (const m of messages) { +const msOf = (v) => { + const t = Date.parse(v); + return Number.isFinite(t) ? t : null; +}; + +const isLater = (a, b) => a.ts > b.ts || (a.ts === b.ts && a.seq > b.seq); + +const legacyChoice = (kid, parent, options) => { + const kVoters = Array.isArray(kid.content.voters) ? kid.content.voters : []; + const pVoters = Array.isArray(parent.content.voters) ? parent.content.voters : []; + if (!kVoters.includes(kid.author) || pVoters.includes(kid.author)) return null; + const kVotes = kid.content.votes || {}; + const pVotes = parent.content.votes || {}; + let choice = null; + for (const o of options) { + const d = (Number(kVotes[o]) || 0) - (Number(pVotes[o]) || 0); + if (d === 0) continue; + if (d !== 1 || choice !== null) return null; + choice = o; + } + return choice; +}; + +const buildVoteResults = (messages) => { + const nodes = new Map(); + const ballotsByTarget = new Map(); + + for (const m of messages || []) { const v = m && m.value; const c = v && v.content; if (!c) continue; - if (c.type === 'votesVote') { if (c.target) ballots.push({ target: c.target, author: v.author, choice: c.choice }); continue; } + const ts = Number(v.timestamp || m.timestamp || 0) || 0; + const seq = Number(v.sequence || 0) || 0; + if (c.type === 'votesVote') { + if (typeof c.target !== 'string' || !v.author) continue; + if (!ballotsByTarget.has(c.target)) ballotsByTarget.set(c.target, []); + ballotsByTarget.get(c.target).push({ author: v.author, choice: c.choice, ts, seq }); + continue; + } if (c.type !== 'votes') continue; - nodes.set(m.key, { key: m.key, author: v.author, content: c, ts: v.timestamp || m.timestamp || 0 }); + nodes.set(m.key, { key: m.key, author: v.author, content: c, ts, seq }); } const prev = new Map(); for (const [k, n] of nodes) { const t = n.content.replaces; - if (typeof t === 'string' && nodes.has(t)) prev.set(k, t); - } - const childrenOf = new Map(); - for (const [k, p] of prev) { - if (!childrenOf.has(p)) childrenOf.set(p, []); - childrenOf.get(p).push(k); + if (typeof t === 'string' && t !== k && nodes.has(t)) prev.set(k, t); } const rootOf = (k) => { let x = k, g = 0; while (prev.has(x) && g++ < 100000) x = prev.get(x); return x; }; const groups = new Map(); for (const k of nodes.keys()) { const r = rootOf(k); if (!groups.has(r)) groups.set(r, []); groups.get(r).push(k); } - const tallyByKey = new Map(); + const results = new Map(); for (const [root, keys] of groups.entries()) { const rootNode = nodes.get(root); if (!rootNode) continue; - const options = Array.isArray(rootNode.content.options) ? rootNode.content.options : []; - let votesMap = Object.assign({}, rootNode.content.votes || {}); - let voters = Array.isArray(rootNode.content.voters) ? rootNode.content.voters.slice() : []; - for (const o of options) if (!(o in votesMap)) votesMap[o] = 0; - - let cur = root; - let g = 0; - let creatorChoice = null; - let creatorCounted = false; - while (g++ < 100000) { - const kids = (childrenOf.get(cur) || []).map(k => nodes.get(k)).filter(Boolean).sort((a, b) => a.ts - b.ts); - if (!kids.length) break; - let advanced = false; - for (const kid of kids) { - const kVotes = Object.assign({}, kid.content.votes || {}); - const kVoters = Array.isArray(kid.content.voters) ? kid.content.voters.slice() : []; - for (const o of options) if (!(o in kVotes)) kVotes[o] = 0; - if (kid.author === rootNode.author) { - if (!creatorCounted && kVoters.includes(rootNode.author) && !voters.includes(rootNode.author)) { - creatorCounted = true; - let inc = null; - let incCount = 0; - for (const o of options) { - const d = (Number(kVotes[o]) || 0) - (Number(votesMap[o]) || 0); - if (d >= 1) { inc = o; incCount++; } - } - if (incCount === 1) creatorChoice = inc; - } - votesMap = kVotes; - voters = kVoters; - cur = kid.key; - advanced = true; - break; - } - if (voters.includes(kid.author)) continue; - if (kVoters.length !== voters.length + 1) continue; - const added = kVoters.filter(x => !voters.includes(x)); - if (added.length !== 1 || added[0] !== kid.author) continue; - let deltas = 0; - let deltaOk = true; - for (const o of options) { - const d = (Number(kVotes[o]) || 0) - (Number(votesMap[o]) || 0); - if (d === 0) continue; - if (d === 1) { deltas++; continue; } - deltaOk = false; - break; - } - if (!deltaOk || deltas !== 1) continue; - votesMap = kVotes; - voters = kVoters; - cur = kid.key; - advanced = true; - break; + const creator = rootNode.author; + const tags = Array.isArray(rootNode.content.tags) ? rootNode.content.tags : []; + const frozen = tags.some(t => INSTITUTIONAL_VOTE_TAG.test(String(t))); + let current = rootNode; + if (!frozen) { + for (const k of keys) { + const n = nodes.get(k); + if (n.author === creator && isLater(n, current)) current = n; } - if (!advanced) break; + } + const options = Array.isArray(current.content.options) ? current.content.options.filter(o => typeof o === 'string') : []; + const deadline = typeof current.content.deadline === 'string' ? current.content.deadline : null; + const opensAt = rootNode.ts; + const closesAt = deadline ? msOf(deadline) : null; + + const cast = []; + for (const k of keys) { + for (const b of ballotsByTarget.get(k) || []) cast.push(b); + const n = nodes.get(k); + if (n.author === creator || !prev.has(k)) continue; + const choice = legacyChoice(n, nodes.get(prev.get(k)), options); + if (choice !== null) cast.push({ author: n.author, choice, ts: n.ts, seq: n.seq }); } - if (voters.includes(rootNode.author)) { - voters = voters.filter(x => x !== rootNode.author); - if (creatorChoice && (Number(votesMap[creatorChoice]) || 0) > 0) votesMap[creatorChoice] = votesMap[creatorChoice] - 1; - } - - const voterSet = new Set(voters); - let totalVotes = voters.length; - const chainKeys = new Set(keys); - for (const b of ballots) { - if (!chainKeys.has(b.target)) continue; - if (b.author === rootNode.author) continue; - if (!b.author || voterSet.has(b.author)) continue; + const latest = new Map(); + for (const b of cast) { + if (!b.author || b.author === creator) continue; if (!options.includes(b.choice)) continue; - voterSet.add(b.author); - voters.push(b.author); - votesMap[b.choice] = (votesMap[b.choice] || 0) + 1; - totalVotes += 1; + if (b.ts < opensAt) continue; + if (closesAt !== null && b.ts > closesAt) continue; + const known = latest.get(b.author); + if (!known || isLater(b, known)) latest.set(b.author, b); } - const tally = { votes: votesMap, voters, totalVotes }; - for (const k of keys) tallyByKey.set(k, tally); + const votes = {}; + for (const o of options) votes[o] = 0; + const voters = []; + for (const b of [...latest.values()].sort((a, b) => a.ts - b.ts || a.seq - b.seq)) { + votes[b.choice] = (votes[b.choice] || 0) + 1; + voters.push(b.author); + } + + const result = { votes, voters, totalVotes: voters.length, creator, rootId: root, deadline, options }; + for (const k of keys) results.set(k, result); } + return results; +}; + +const buildVoteTally = (messages) => { + const shared = new Map(); + const tallyByKey = new Map(); + for (const [k, r] of buildVoteResults(messages)) { + if (!shared.has(r)) shared.set(r, { votes: r.votes, voters: r.voters, totalVotes: r.totalVotes }); + tallyByKey.set(k, shared.get(r)); + } return tallyByKey; }; -module.exports = { buildVoteTally }; +module.exports = { buildVoteTally, buildVoteResults }; diff --git a/src/backend/wallet_addresses.js b/src/backend/wallet_addresses.js deleted file mode 100644 index ada6f20..0000000 --- a/src/backend/wallet_addresses.js +++ /dev/null @@ -1,17 +0,0 @@ -const fs = require("fs"); -const path = require("path"); - -const STORAGE_DIR = path.join(__dirname, "..", "configs"); -const ADDR_PATH = path.join(STORAGE_DIR, "wallet-addresses.json"); - -function ensure() { - if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true }); - if (!fs.existsSync(ADDR_PATH)) fs.writeFileSync(ADDR_PATH, "{}"); -} -function readAll() { ensure(); return JSON.parse(fs.readFileSync(ADDR_PATH, "utf8")); } -function writeAll(m) { fs.writeFileSync(ADDR_PATH, JSON.stringify(m, null, 2)); } - -async function getAddress(userId) { const m = readAll(); return m[userId] || null; } -async function setAddress(userId, address) { const m = readAll(); m[userId] = address; writeAll(m); return true; } - -module.exports = { getAddress, setAddress }; diff --git a/src/client/assets/fonts/Anton-Regular.ttf b/src/client/assets/fonts/Anton-Regular.ttf new file mode 100644 index 0000000..4d65707 Binary files /dev/null and b/src/client/assets/fonts/Anton-Regular.ttf differ diff --git a/src/client/assets/fonts/OFL.txt b/src/client/assets/fonts/OFL.txt new file mode 100644 index 0000000..93fedde --- /dev/null +++ b/src/client/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Anton Project Authors (https://github.com/googlefonts/AntonFont.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/client/assets/images/theme-acampada.png b/src/client/assets/images/theme-acampada.png new file mode 100644 index 0000000..c635277 Binary files /dev/null and b/src/client/assets/images/theme-acampada.png differ diff --git a/src/client/assets/images/theme-clear-snh.png b/src/client/assets/images/theme-clear-snh.png new file mode 100644 index 0000000..b759e6b Binary files /dev/null and b/src/client/assets/images/theme-clear-snh.png differ diff --git a/src/client/assets/images/theme-dark-snh.png b/src/client/assets/images/theme-dark-snh.png new file mode 100644 index 0000000..76c9fa0 Binary files /dev/null and b/src/client/assets/images/theme-dark-snh.png differ diff --git a/src/client/assets/images/theme-matrix-snh.png b/src/client/assets/images/theme-matrix-snh.png new file mode 100644 index 0000000..a3b8d41 Binary files /dev/null and b/src/client/assets/images/theme-matrix-snh.png differ diff --git a/src/client/assets/images/theme-oasismobile.png b/src/client/assets/images/theme-oasismobile.png new file mode 100644 index 0000000..2f9bfaf Binary files /dev/null and b/src/client/assets/images/theme-oasismobile.png differ diff --git a/src/client/assets/images/theme-purple-snh.png b/src/client/assets/images/theme-purple-snh.png new file mode 100644 index 0000000..ab9fffc Binary files /dev/null and b/src/client/assets/images/theme-purple-snh.png differ diff --git a/src/client/assets/images/ux-ainav.png b/src/client/assets/images/ux-ainav.png new file mode 100644 index 0000000..022f127 Binary files /dev/null and b/src/client/assets/images/ux-ainav.png differ diff --git a/src/client/assets/images/ux-blocks.png b/src/client/assets/images/ux-blocks.png new file mode 100644 index 0000000..cfbae8c Binary files /dev/null and b/src/client/assets/images/ux-blocks.png differ diff --git a/src/client/assets/images/ux-chats.png b/src/client/assets/images/ux-chats.png new file mode 100644 index 0000000..a7ccde5 Binary files /dev/null and b/src/client/assets/images/ux-chats.png differ diff --git a/src/client/assets/images/ux-empty.svg b/src/client/assets/images/ux-empty.svg new file mode 100644 index 0000000..3237726 --- /dev/null +++ b/src/client/assets/images/ux-empty.svg @@ -0,0 +1 @@ + diff --git a/src/client/assets/images/ux-feed.png b/src/client/assets/images/ux-feed.png new file mode 100644 index 0000000..cdcb882 Binary files /dev/null and b/src/client/assets/images/ux-feed.png differ diff --git a/src/client/assets/images/ux-phone.png b/src/client/assets/images/ux-phone.png new file mode 100644 index 0000000..f160c47 Binary files /dev/null and b/src/client/assets/images/ux-phone.png differ diff --git a/src/client/assets/images/ux-rooms.png b/src/client/assets/images/ux-rooms.png new file mode 100644 index 0000000..cdf711e Binary files /dev/null and b/src/client/assets/images/ux-rooms.png differ diff --git a/src/client/assets/images/world-map.svg b/src/client/assets/images/world-map.svg deleted file mode 100644 index cf308be..0000000 --- a/src/client/assets/images/world-map.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/src/client/assets/images/worldmap-z2.png b/src/client/assets/images/worldmap-z2.png deleted file mode 100644 index ce8e6ac..0000000 Binary files a/src/client/assets/images/worldmap-z2.png and /dev/null differ diff --git a/src/client/assets/styles/hive_acampada.css b/src/client/assets/styles/hive_acampada.css new file mode 100644 index 0000000..588cf6d --- /dev/null +++ b/src/client/assets/styles/hive_acampada.css @@ -0,0 +1,507 @@ +/* Lanzador, barras, tarjetas e identidades con el tema Acampada (rama movil). + * + * OasisMobile.css (cargado antes que themes/Acampada.css) esta pensado para fondo + * oscuro; esta hoja va detras del tema, solo con Acampada, y cambia lo que alli + * es amarillo sobre negro por negro sobre blanco con el cian neon de + * acampada26s.net. Las variables --a26-* las define themes/Acampada.css. */ + +body { background: var(--a26-bg) !important; } + +/* ---------- barra de arriba ---------- */ +.oasis-mobile-topbar { + background: var(--a26-bg) !important; + border: 0 !important; + border-bottom: var(--a26-bd) !important; + box-shadow: none !important; +} +.oasis-mobile-topbar .omt-menus { background: transparent !important; border: 0 !important; box-shadow: none !important; gap: 10px; } +.oasis-mobile-topbar .omt-menus .oasis-menu-btn { + font-family: var(--a26-tit) !important; + font-weight: 400 !important; + text-transform: uppercase; + font-size: 20px !important; + letter-spacing: .02em; + min-height: 44px !important; + padding: 4px 16px !important; + background: var(--a26-bg) !important; + color: var(--a26-fg) !important; + border: var(--a26-bd) !important; + border-radius: var(--a26-radius) !important; + box-shadow: 0 4px 0 var(--a26-fg) !important; +} +.oasis-mobile-topbar .omt-menus .oasis-menu-btn span { color: var(--a26-fg) !important; } +.oasis-mobile-topbar .omt-menus .oasis-menu-btn.active { background: var(--a26-neon) !important; } +.oasis-mobile-topbar .omt-menus .oasis-menu-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--a26-fg) !important; } +.oasis-mobile-topbar .omt-logo img, .oasis-mobile-topbar .omt-avatar img { + border: var(--a26-bd) !important; + border-radius: 50%; +} + +/* la cabecera de escritorio de upstream no pinta nada en el movil */ +.header { background: transparent !important; border: 0 !important; box-shadow: none !important; padding: 0 !important; } +.header .top-bar-left, .header .top-bar-right { display: none !important; } + +/* ---------- lanzador de modulos (views/hive_views.js) ---------- */ +.launcher, .ln-group, .ln-grid { background: transparent !important; border: 0 !important; box-shadow: none !important; } +.header .launcher { margin: 18px auto 10px !important; } +.ln-group + .ln-group { margin-top: 22px !important; } +.ln-title { + font-family: var(--a26-tit) !important; + font-weight: 400 !important; + font-size: 17px !important; + letter-spacing: .03em !important; + color: var(--a26-fg) !important; + border-bottom: var(--a26-bd) !important; + padding: 0 0 4px !important; + margin: 0 2px 14px !important; +} +.ln-grid { gap: 16px 10px !important; } +.ln-ico { + width: 58px !important; height: 58px !important; + background: var(--a26-bg) !important; + color: var(--a26-fg) !important; + border: var(--a26-bd) !important; + border-radius: var(--a26-radius) !important; + box-shadow: 0 4px 0 var(--a26-fg) !important; + font-size: 24px !important; + transition: transform .12s ease-out, box-shadow .12s ease-out; +} +.ln-app:active .ln-ico, .ln-app:focus-visible .ln-ico { + transform: translateY(3px); + box-shadow: 0 1px 0 var(--a26-fg) !important; + background: var(--a26-neon) !important; + border-color: var(--a26-fg) !important; +} +.ln-app:hover, .ln-app:focus-visible { background: transparent !important; } +.ln-lbl { color: var(--a26-fg) !important; font-size: 12px !important; font-weight: 600 !important; } + +/* ---------- barra de abajo ---------- */ +.oasis-bottombar { + background: var(--a26-bg) !important; + border: 0 !important; + border-top: var(--a26-bd) !important; + box-shadow: none !important; +} +.oasis-bottombar { padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px)) !important; gap: 2px !important; } +.oasis-bottombar .bb-item { gap: 2px !important; } +.oasis-bottombar .bb-ico { color: var(--a26-fg) !important; font-size: 21px !important; } +.oasis-bottombar .bb-lbl { + color: var(--a26-fg) !important; + font-size: 10.5px !important; + font-weight: 600; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} +.oasis-bottombar a.bb-item.current .bb-ico { background: var(--a26-neon) !important; color: var(--a26-fg) !important; } +.oasis-bottombar a.bb-item.current .bb-lbl { color: var(--a26-fg) !important; } +.oasis-bottombar .bb-badge { background: var(--a26-neon) !important; color: var(--a26-fg) !important; border: 2px solid var(--a26-fg) !important; } +/* Nodos e Invitaciones: juntos y estrechos { + background: var(--a26-bg) !important; + border: var(--a26-bd-fino) !important; + border-radius: var(--a26-radius) !important; + padding: 2px !important; + gap: 0 !important; +} + +/* ---------- personalizar la barra ---------- */ +.bb-manage-btn, .bb-pick-btn { + background: var(--a26-bg) !important; + color: var(--a26-fg) !important; + border: var(--a26-bd) !important; + border-radius: var(--a26-radius) !important; + box-shadow: 0 4px 0 var(--a26-fg) !important; + font-family: var(--a26-txt) !important; + text-transform: none; +} +.bb-manage-ico, .bb-manage-x, .bb-pick-ico { color: var(--a26-fg) !important; } +.bb-manage-lbl, .bb-pick-lbl { color: var(--a26-fg) !important; font-weight: 600; } +.bb-manage h3, .bb-manage-add h3 { color: var(--a26-fg) !important; } +.bb-manage-empty, .bb-manage-full { color: var(--a26-muted) !important; } + +/* ---------- menu desplegable de upstream (si se abre) ---------- */ +.oasis-nav-header { background: var(--a26-bg) !important; color: var(--a26-fg) !important; border: var(--a26-bd-fino) !important; } +.oasis-nav-list li a, .oasis-nav-list li a .emoji { color: var(--a26-fg) !important; } +.oasis-nav-list li a.current { background: var(--a26-neon) !important; } + +/* ---------- foro y chat: los colores de hive.css pensados para fondo negro ---------- */ +.forum-comment.level-1, .forum-comment.level-2, .forum-comment.level-3, .forum-comment.level-4 { border-left: 3px solid var(--a26-fg) !important; } +.forum-comment.highlighted-reply { background: #e5fcff !important; border-left: 5px solid var(--a26-neon) !important; } +.chat-message-form .filter-btn::before, .forum-send-btn::before { color: var(--a26-fg) !important; } + +/* ---------- lo que mobile.css de upstream pinta en amarillo con !important ---------- */ +/* mobile.css va detras del tema, asi que estas reglas tienen que ir aqui */ +.create-button, button.create-button, a.create-button { + background-color: var(--a26-fg) !important; + border-color: var(--a26-fg) !important; + color: var(--a26-bg) !important; +} +.filter-btn.active { background-color: var(--a26-neon) !important; border-color: var(--a26-fg) !important; color: var(--a26-fg) !important; } +.peers-conn-actions button, .filter-box__button { background-color: var(--a26-bg) !important; color: var(--a26-fg) !important; } +.oasis-id-copy { background: var(--a26-soft) !important; } +.gov-overview tbody tr { border: var(--a26-bd-fino) !important; } +button, input[type="submit"], input[type="button"], .filter-btn, .create-button { min-height: 48px !important; font-size: 18px !important; } + +/* aviso de bienvenida: el boton de empezar en negro y la X sin caja */ +.welcome-banner .welcome-banner-start { + background: var(--a26-fg) !important; + color: var(--a26-bg) !important; + box-shadow: 0 4px 0 var(--a26-bg) !important; + border: var(--a26-bd) !important; +} +.welcome-banner .welcome-banner-close-btn { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; + color: var(--a26-fg) !important; + font-size: 22px !important; + opacity: 1 !important; +} + +/* pie: gris y subrayado, sin amarillos */ +.oasis-footer .oasis-footer-center a, .oasis-footer .oasis-footer-center span, +.oasis-footer .oasis-footer-center a span { color: var(--a26-muted) !important; } + +/* la columna de contenido sin marco: el marco lo llevan las tarjetas */ +.main-column { border: 0 !important; box-shadow: none !important; border-radius: 0 !important; } + +/* la sombra de Personal/Social no se corta contra la raya de la barra */ +.oasis-mobile-topbar { padding-bottom: 12px !important; } + +/* ---------- asistente de bienvenida: botones grandes de un toque ---------- */ +.welcome-step { border: var(--a26-bd) !important; border-radius: var(--a26-radius) !important; } +.welcome-banner .welcome-banner-start { font-size: 20px !important; min-height: 48px !important; padding: 8px 22px !important; } + +/* mobile.css pinta en naranja los botones sin clase y todos los .filter-btn */ +button:not([class]), input[type="submit"]:not([class]), input[type="button"]:not([class]), +.filter-btn, a.filter-btn, .conn-actions button, .peers-conn-actions button, .filter-box__button { + background-color: var(--a26-bg) !important; + color: var(--a26-fg) !important; + border: var(--a26-bd) !important; + border-radius: var(--a26-radius) !important; + font-weight: 400 !important; + font-size: 18px !important; + height: auto !important; + min-height: 48px !important; +} +.filter-btn.active, a.filter-btn.active { background-color: var(--a26-neon) !important; } + +/* pantallas estrechas (360px): Nodos e Invitaciones solo con icono, para que + los cuatro atajos no se corten */ +@media (max-width: 380px) { + .oasis-bottombar .bb-pair .bb-lbl { display: none !important; } + .oasis-bottombar .bb-pair .bb-ico { font-size: 22px !important; } + .oasis-bottombar .bb-pair .bb-item { padding: 2px 6px !important; } + .oasis-bottombar .bb-lbl { font-size: 10px !important; } + .welcome-banner .update-banner-icon { display: none !important; } +} + +/* ---------- identidades (views/identities_view.js): una tarjeta por identidad ---------- */ +.identities .identity-list { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 10px; } +.identities .identity-row { display: flex; flex-direction: column; align-items: stretch; gap: 10px; padding: 12px 14px; } +.identities .identity-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; border: 0; background: transparent; padding: 0; margin: 0; box-shadow: none; } +.identities .identity-feed { font-family: monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.identities .identity-warn { font-size: 12px; } +.identities .identity-row form { margin: 0; } +.identities .identity-row button { width: 100%; margin: 0; } +.identities .identity-active { text-align: center; font-weight: bold; padding: 10px 12px; border-radius: var(--a26-radius); } +.identities .identity-create { display: flex; flex-direction: column; gap: 10px; margin: 0 0 16px; } +.identities .identity-create input { width: 100%; box-sizing: border-box; margin: 0; } +.identities .identity-create button { width: 100%; margin: 0; } +.identities .identity-actions { display: flex; flex-direction: column; gap: 12px; align-items: stretch; border: 0; background: transparent; padding: 0; } +.identities .identity-actions form { margin: 0; } +.identities .identity-actions button { width: 100%; } +.identities .identity-legacy { text-align: center; text-decoration: underline; } +.identities .identity-row { border: var(--a26-bd) !important; border-radius: var(--a26-radius) !important; background: var(--a26-bg) !important; } +.identities .identity-name { font-family: var(--a26-tit) !important; font-weight: 400 !important; text-transform: uppercase; font-size: 22px !important; } +.identities .identity-feed { color: var(--a26-muted) !important; } +.identities .identity-active { background: var(--a26-neon) !important; border: var(--a26-bd-fino) !important; color: var(--a26-fg) !important; } +.identities .identity-warn { color: #c4003b !important; } + +/* las barras de filtros de cada modulo: upstream las encoge (.75em) y les pone + borde amarillo con selectores compuestos; aqui, del mismo tamaño que el resto */ +.tribe-mode-buttons .create-button, .ui-toolbar--filters .create-button, .mode-buttons .create-button, +.mode-buttons-row .create-button, .filters .create-button, .filter-group .create-button, +.activity-sub-filter .create-button, .activity-filter-grid .create-button { + border: var(--a26-bd) !important; + border-color: var(--a26-fg) !important; + background: var(--a26-fg) !important; + color: var(--a26-bg) !important; + font-size: 18px !important; + padding: 8px 16px !important; + min-height: 48px !important; + border-radius: var(--a26-radius) !important; + line-height: 1.2 !important; +} +.tribe-mode-buttons .filter-btn, .ui-toolbar--filters .filter-btn, .mode-buttons .filter-btn, +.mode-buttons-row .filter-btn, .mode-buttons-cols .filter-btn, .filters .filter-btn, +.filter-group .filter-btn, .activity-sub-filter .filter-btn, .activity-filter-grid .filter-btn { + border: var(--a26-bd) !important; + font-size: 16px !important; + padding: 6px 14px !important; + min-height: 44px !important; + border-radius: var(--a26-radius) !important; + line-height: 1.2 !important; +} + +/* ---------- lo que tiene que seguir siendo pequeño ---------- */ +/* los iconos de accion de cada tarjeta (borrar, fijar, enlace...): en fila y redondos */ +.content-actions { + display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; + justify-content: flex-end !important; align-items: center !important; gap: 6px !important; +} +.main-column .content-actions form, .content-actions .content-action-form { display: inline-flex !important; margin: 0 !important; width: auto !important; } +.btn-singleview, button.btn-singleview, a.btn-singleview { + width: 34px !important; height: 34px !important; min-width: 34px !important; min-height: 34px !important; + padding: 0 !important; margin: 0 !important; + display: inline-flex !important; align-items: center !important; justify-content: center !important; + font-family: var(--a26-txt) !important; font-size: 15px !important; text-transform: none !important; + background: var(--a26-bg) !important; color: var(--a26-fg) !important; + border: var(--a26-bd-fino) !important; border-radius: 50% !important; box-shadow: none !important; +} +.btn-singleview.btn-delete { color: #c4003b !important; border-color: #c4003b !important; } + +/* barras de filtros: chips en fila que se reparten el ancho, no un boton por linea */ +.mode-buttons, .mode-buttons-row, .mode-buttons-cols, .tribe-mode-buttons, .ui-toolbar--filters, .filters { + display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; + justify-content: center !important; gap: 8px !important; margin: 0 0 14px !important; +} +.mode-buttons form, .mode-buttons-row form, .mode-buttons-cols form, .tribe-mode-buttons form, .filters > form:not(.filter-box) { + display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; + justify-content: center !important; gap: 8px !important; width: auto !important; max-width: 100% !important; margin: 0 !important; +} +.mode-buttons .filter-btn, .mode-buttons-row .filter-btn, .mode-buttons-cols .filter-btn, .tribe-mode-buttons .filter-btn, +.ui-toolbar--filters .filter-btn, .filters .filter-btn, .activity-filter-grid .filter-btn, .activity-sub-filter .filter-btn { + width: auto !important; flex: 0 0 auto !important; + min-height: 38px !important; height: auto !important; + padding: 4px 12px !important; margin: 0 !important; + font-size: 15px !important; box-shadow: 0 3px 0 var(--a26-fg) !important; + border: var(--a26-bd-fino) !important; +} +.mode-buttons .create-button, .mode-buttons-row .create-button, .tribe-mode-buttons .create-button, .filters .create-button { + flex: 1 1 100% !important; width: 100% !important; margin-top: 4px !important; +} +/* chips de la actividad (activity-filter-*) */ +.activity-filter-chips button, .activity-filter-grid button, .activity-sub-filter button, .activity-filter-col button { + background: var(--a26-bg) !important; color: var(--a26-fg) !important; border: var(--a26-bd-fino) !important; +} +.activity-filter-chips button.active, .activity-filter-grid button.active, .activity-sub-filter button.active, .activity-filter-col button.active { + background: var(--a26-neon) !important; +} +/* fechas y autores al pie de cada tarjeta: texto, no botones */ +.card-footer .date-link, .card-footer a.user-link, a.user-link, .date-link { + font-family: var(--a26-txt) !important; text-transform: none !important; letter-spacing: 0 !important; + font-size: 13px !important; box-shadow: none !important; border: 0 !important; + background: transparent !important; padding: 0 !important; min-height: 0 !important; +} +a.user-link { font-weight: 700 !important; text-decoration: underline !important; } +.card-footer { display: flex !important; flex-wrap: wrap; gap: 10px; align-items: center; color: var(--a26-muted) !important; } +/* buscador de cada modulo */ +.filter-box { display: flex !important; gap: 8px !important; align-items: stretch !important; } +.filter-box__input::placeholder, input::placeholder, textarea::placeholder { color: #777 !important; } +.filter-box__button { font-size: 15px !important; padding: 4px 14px !important; box-shadow: none !important; } +/* el pie de cada pagina y el id del perfil, en gris */ +.main-column [class*="feed-id"], .profile .feed-id, code { color: var(--a26-muted) !important; } + +/* una sola caja por tarjeta: lo de dentro (cabecera, titulo, texto) sin marco */ +.main-column section, .main-column [class*="-card-"], .main-column .card-section, .main-column .card-header, +.main-column .card-meta-row, .main-column .shop-title-row, .main-column [class*="-grid"], +.main-column .feed-container, .main-column [class$="-card"] [class$="-card"] { + border: 0 !important; box-shadow: none !important; background: transparent !important; border-radius: 0 !important; +} +.main-column [class$="-card"], .main-column [class*="-card "] { + border: var(--a26-bd) !important; border-radius: var(--a26-radius) !important; + background: var(--a26-bg) !important; padding: 12px !important; margin: 0 0 14px !important; +} +.main-column .card-header { padding: 0 0 8px !important; border-bottom: var(--a26-bd-fino) !important; margin-bottom: 8px !important; } +.main-column [class$="-card"] .card-footer { border-top: var(--a26-bd-fino) !important; padding-top: 8px !important; margin-top: 8px !important; } +.main-column hr { border: 0 !important; border-top: var(--a26-bd-fino) !important; } + +/* titulos de las tarjetas: enlaces en negro */ +.main-column h1 a, .main-column h2 a, .main-column h3 a, .main-column [class*="title"] a { + color: var(--a26-fg) !important; text-decoration: none !important; +} +.main-column [class*="-card"] .tribe-card-title, .main-column [class*="-card"] h2 { font-size: 22px !important; margin: 4px 0 8px !important; } + +/* la etiqueta del tipo (IMAGENES, FOROS...) y los chips de estado */ +.pm-exposition-chip, .card-chips-row > span { + background: var(--a26-fg) !important; color: var(--a26-bg) !important; + border: 0 !important; border-radius: 8px !important; padding: 2px 8px !important; +} +.pm-exposition-chip *, .card-chips-row > span * { color: var(--a26-bg) !important; } +.pm-exposition-text { font-family: var(--a26-tit) !important; letter-spacing: .04em; font-size: 13px !important; } +.content-actions .btn-singleview.btn-content, .content-actions a.btn-content { border-color: var(--a26-fg) !important; color: var(--a26-fg) !important; } + +/* opiniones y votos */ +.opinions-summary, .vote-btn, .score-btn { + background: var(--a26-bg) !important; color: var(--a26-fg) !important; + border: var(--a26-bd-fino) !important; border-radius: 10px !important; box-shadow: none !important; + font-family: var(--a26-txt) !important; text-transform: none !important; font-size: 14px !important; +} +.post-image { border: var(--a26-bd-fino) !important; border-radius: 10px; } + +/* tablas de detalle (fecha, estado, lugar...): filas claras, la etiqueta en gris */ +.main-column table, .main-column table tr, .main-column table td, .main-column table th, .main-column table tbody { + background: var(--a26-bg) !important; color: var(--a26-fg) !important; border-color: var(--a26-fg) !important; +} +.main-column table { border: var(--a26-bd-fino) !important; border-radius: 10px !important; } +.main-column table tr + tr td { border-top: 1px solid #ccc !important; } +.main-column table td:first-child, .main-column table th, .main-column [class*="-info-label"] { + color: var(--a26-muted) !important; font-weight: 700 !important; font-size: 13px !important; + text-transform: uppercase; letter-spacing: .03em; +} + +/* perfil: los datos del habitante como lineas claras, sin cajas negras */ +.profile-sensors-box, .profile-sensors-box > span, .karma-line, .inhabitant-last-activity, .gpg-line, +.inhabitant-stats-box, .inhabitant-stat, .profile-side-relationship .status { + background: transparent !important; color: var(--a26-fg) !important; border: 0 !important; box-shadow: none !important; +} +.profile-sensors-box { display: flex !important; flex-direction: column !important; gap: 4px !important; align-items: center !important; } +.profile-sensors-box strong, .profile-sensors-box a, .device-mobile { color: var(--a26-fg) !important; } +.inhabitant-stat { border: var(--a26-bd-fino) !important; border-radius: 10px !important; padding: 4px 10px !important; } +.profile-side-relationship .status { background: var(--a26-neon) !important; border: var(--a26-bd-fino) !important; border-radius: 8px !important; padding: 2px 10px !important; } +.main-column [class*="feed-id"], .main-column a[href^="/author/"]:not(.user-link) { color: var(--a26-muted) !important; } +.profile img.avatar, .inhabitant-photo { border: var(--a26-bd) !important; } + +/* etiquetas (#asamblea) y la tasa ECO: chips claros */ +.card-tags { background: transparent !important; border: 0 !important; padding: 0 !important; display: flex !important; flex-wrap: wrap; gap: 6px; } +.card-tags .tag-link, a.tag-link { + background: var(--a26-bg) !important; color: var(--a26-fg) !important; + border: var(--a26-bd-fino) !important; border-radius: 8px !important; padding: 2px 8px !important; + text-decoration: none !important; font-weight: 700; +} +.eco-tax-chip, [class*="eco-tax-chip-"] { + background: var(--a26-soft) !important; color: var(--a26-muted) !important; + border: 1px solid #bbb !important; border-radius: 8px !important; +} + +/* los filtros de la actividad son enlaces .activity-chip */ +.activity-chip { + background: var(--a26-bg) !important; color: var(--a26-fg) !important; + border: var(--a26-bd-fino) !important; border-radius: 10px !important; + padding: 4px 10px !important; font-weight: 700 !important; text-decoration: none !important; +} +.activity-chip.active { background: var(--a26-neon) !important; } +.activity-chip:hover { color: var(--a26-fg) !important; border-color: var(--a26-fg) !important; } + +/* ---------- fichas de filtro de 1.3.3 (OasisMobile.css las pinta en amarillo y gris) ---------- */ +/* mismo selector que alli: gana por ir detras */ +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) { + justify-content: center !important; + gap: 8px !important; + padding: 4px 0 !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) .filter-btn:not(.create-button) { + font-family: var(--a26-tit) !important; + font-weight: 400 !important; + font-size: 15px !important; + letter-spacing: .02em !important; + padding: 4px 12px !important; + min-height: 38px !important; + border: var(--a26-bd-fino) !important; + border-radius: var(--a26-radius) !important; + background: var(--a26-bg) !important; + color: var(--a26-fg) !important; + box-shadow: 0 3px 0 var(--a26-fg) !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) .filter-btn.active:not(.create-button) { + background: var(--a26-neon) !important; + border-color: var(--a26-fg) !important; + color: var(--a26-fg) !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) .create-button { min-height: 48px !important; } + +/* acciones de la tarjeta: iconos SVG en negro dentro del circulo */ +.main-column .content-actions .btn-singleview { width: 36px !important; height: 36px !important; } +.btn-singleview .btn-icon { width: 18px !important; height: 18px !important; } +.btn-singleview.btn-pin-on { background: var(--a26-neon) !important; } + +/* hilos del foro (lista tipo archivo de 1.3.3): tarjeta negra sin la raya naranja */ +.main-column .mailing-archive-item { + border: var(--a26-bd-fino) !important; + border-radius: var(--a26-radius) !important; + background: var(--a26-bg) !important; + padding: 12px !important; + margin: 0 0 14px !important; +} +.main-column .mailing-archive-item a { color: var(--a26-fg) !important; } +.main-column a.forum-category { + background: var(--a26-fg) !important; color: var(--a26-bg) !important; + border: 0 !important; border-radius: 8px !important; padding: 2px 8px !important; + font-family: var(--a26-tit) !important; text-decoration: none !important; letter-spacing: .03em; +} + +/* restos de naranja de style.css y mobile.css */ +.oasis-id-copy, input.oasis-id-copy, input.oasis-id-copy[readonly] { + background: var(--a26-soft) !important; border: var(--a26-bd-fino) !important; color: var(--a26-muted) !important; +} +.oasis-menu-btn.oasis-menu-open, .oasis-menu-btn.oasis-menu-close { + background: var(--a26-bg) !important; border: var(--a26-bd-fino) !important; color: var(--a26-fg) !important; +} +img.inhabitant-photo-details { border: var(--a26-bd) !important; } +.welcome-action { color: var(--a26-fg) !important; } +label.welcome-ux-option { + background: var(--a26-bg) !important; border: var(--a26-bd-fino) !important; + border-radius: var(--a26-radius) !important; box-shadow: 0 4px 0 var(--a26-fg) !important; +} +label.welcome-ux-option:has(input:checked) { background: var(--a26-neon) !important; } + +/* chat: burbujas con marco negro y las reacciones como chips pequeños + (el estilo general de botones de Acampada las hacia de 48px a ancho completo) */ +.chat-bubble-row .chat-message { + background: var(--a26-bg) !important; color: var(--a26-fg) !important; + border: var(--a26-bd-fino) !important; border-radius: 14px 14px 14px 4px !important; +} +.chat-bubble-row-self .chat-message { background: #e5fcff !important; border-radius: 14px 14px 4px 14px !important; } +.chat-bubble-sender, .chat-bubble-sender-owner { color: var(--a26-fg) !important; } +.chat-bubble-time { color: var(--a26-muted) !important; } +.main-column .chat-msg-actions { display: flex !important; flex-wrap: wrap !important; gap: 4px !important; } +.main-column .chat-msg-actions form, .main-column form.chat-react-form { display: inline-flex !important; width: auto !important; margin: 0 !important; } +.main-column button.chat-react-btn, button.chat-react-btn { + width: auto !important; min-height: 0 !important; height: auto !important; + padding: 1px 8px !important; margin: 0 !important; + font-size: 13px !important; line-height: 1.4 !important; + font-family: "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", sans-serif !important; + text-transform: none !important; letter-spacing: 0 !important; + background: var(--a26-bg) !important; color: var(--a26-fg) !important; + border: 1px solid var(--a26-fg) !important; border-radius: 10px !important; box-shadow: none !important; +} +/* el 📌 va en la esquina: el texto deja su hueco (el padding general de la burbuja lo pisaba) */ +.chat-bubble-row .chat-message.chat-has-pin-toggle { position: relative !important; padding-right: 44px !important; } +.chat-day-chip { background: var(--a26-fg) !important; color: var(--a26-bg) !important; border: 0 !important; } +.chat-reply-cancel, +.chat-message-meta .chat-reply-btn { color: var(--a26-fg) !important; } + +/* ---------- skins en Ajustes ---------- */ +.skin-card { + background: var(--a26-bg) !important; color: var(--a26-fg) !important; + border: var(--a26-bd) !important; border-radius: var(--a26-radius) !important; + box-shadow: 0 5px 0 var(--a26-fg) !important; +} +.skin-card .skin-shot { border: var(--a26-bd-fino); } +.skin-card .skin-name { + font-family: var(--a26-tit) !important; font-weight: 400 !important; + text-transform: uppercase; letter-spacing: .02em; font-size: 21px !important; +} +.skin-card.is-current { background: var(--a26-neon) !important; color: var(--a26-fg) !important; border: var(--a26-bd) !important; } +.skin-card:active { transform: translateY(4px); box-shadow: 0 1px 0 var(--a26-fg) !important; background: var(--a26-neon) !important; } + +/* ---------- letra y botones mas grandes, para leer sin gafas ---------- */ +body { font-size: 18px; } +.ln-ico { width: 64px !important; height: 64px !important; font-size: 28px !important; } +.ln-lbl { font-size: 13px !important; font-weight: 700 !important; white-space: normal !important; overflow-wrap: normal !important; word-break: normal !important; line-height: 1.15 !important; letter-spacing: -.02em; overflow: visible !important; text-overflow: clip !important; max-width: calc(100% + 10px) !important; } +.ln-title { font-size: 20px !important; } +.ln-grid { gap: 20px 8px !important; } +.oasis-bottombar .bb-ico { font-size: 25px !important; } +.oasis-bottombar .bb-lbl { font-size: 12.5px !important; font-weight: 700 !important; } +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) .filter-btn:not(.create-button) { + font-size: 18px !important; min-height: 46px !important; padding: 6px 16px !important; +} +.activity-chip { font-size: 16px !important; padding: 6px 12px !important; } +.main-column .content-actions .btn-singleview { width: 44px !important; height: 44px !important; min-width: 44px !important; } +.btn-singleview .btn-icon { width: 22px !important; height: 22px !important; } +.main-column button.chat-react-btn, button.chat-react-btn { font-size: 17px !important; padding: 3px 10px !important; } +.oasis-mobile-topbar .omt-menus .oasis-menu-btn { font-size: 22px !important; min-height: 48px !important; } +.tags-header p { font-size: 16px !important; } +.card-footer .date-link, .card-footer a.user-link, a.user-link, .date-link { font-size: 15px !important; } +input, textarea, select { font-size: 18px !important; min-height: 50px; } diff --git a/src/client/assets/styles/mobile.css b/src/client/assets/styles/mobile.css index 18c1720..d2d2cae 100644 --- a/src/client/assets/styles/mobile.css +++ b/src/client/assets/styles/mobile.css @@ -89,7 +89,6 @@ code { } .search-input, -.feed-search-input, .activity-search-input { width: 100% !important; max-width: 100% !important; @@ -262,7 +261,7 @@ h3 { font-size: 1em !important; } display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; - justify-content: center !important; + justify-content: flex-start !important; align-items: center !important; width: 100% !important; max-width: 100% !important; @@ -289,8 +288,8 @@ h3 { font-size: 1em !important; } } .inhabitant-action .filter-btn { min-height: 0 !important; - padding: 7px 12px !important; - font-size: 13px !important; + padding: 2px 9px !important; + font-size: .75em !important; white-space: nowrap !important; } .profile-sensors-box { @@ -316,8 +315,8 @@ h3 { font-size: 1em !important; } .activity-filter-col .filter-btn, .activity-sub-filter .filter-btn { min-height: 0 !important; - padding: 7px 12px !important; - font-size: 13px !important; + padding: 2px 9px !important; + font-size: .75em !important; white-space: nowrap !important; } @@ -451,10 +450,6 @@ h3 { font-size: 1em !important; } grid-template-columns: repeat(2, 1fr) !important; } -.tribe-media-grid { - grid-template-columns: repeat(2, 1fr) !important; -} - .tribe-overview-grid { grid-template-columns: 1fr !important; } @@ -467,24 +462,11 @@ h3 { font-size: 1em !important; } height: 180px !important; } -.tribe-votation-option { - flex-direction: column !important; - align-items: flex-start !important; -} - .tribe-content-header { flex-direction: column !important; gap: 8px !important; } -.tribe-content-filters { - flex-wrap: wrap !important; -} - -.tribe-media-filters { - flex-wrap: wrap !important; -} - .forum-card { flex-direction: column !important; } @@ -510,28 +492,28 @@ h3 { font-size: 1em !important; } } .forum-comment { - margin-left: 0 !important; - padding-left: 8px !important; + margin-left: 0; + padding-left: 8px; } .comment-body-row { - flex-direction: column !important; + flex-direction: column; } .comment-vote-col, .comment-text-col { - width: 100% !important; + width: 100%; } .forum-score-box, .forum-score-form { - flex-direction: row !important; - justify-content: center !important; + flex-direction: row; + justify-content: center; } .new-message-form textarea, .comment-textarea { - width: 100% !important; + width: 100%; } [style*="grid-template-columns: repeat(6"] { @@ -627,6 +609,7 @@ a.filter-btn, width: 100% !important; max-width: 100% !important; min-width: 0 !important; + flex: 0 0 auto !important; height: 42px !important; font-size: 16px !important; box-sizing: border-box !important; @@ -673,7 +656,7 @@ a.filter-btn, padding: 9px 16px !important; } .filters .ui-toolbar--filters { - justify-content: center !important; + justify-content: flex-start !important; align-items: center !important; gap: 6px !important; } @@ -729,6 +712,15 @@ a.filter-btn, max-width: 100% !important; overflow-wrap: anywhere !important; } +.card-field .card-value.user-link, +.card-field .card-value .user-link { + display: inline-block !important; + vertical-align: bottom !important; + max-width: 100% !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; +} .market-grid, .shop-products-grid, @@ -818,14 +810,14 @@ a.filter-btn, margin-left: 0 !important; } -.oasis-id-copy { +input.oasis-id-copy[readonly] { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; font-family: monospace !important; - font-size: 12px !important; + font-size: clamp(8px, 2.5vw, 12px) !important; text-align: center !important; - padding: 8px !important; + padding: 8px 6px !important; border: 1px solid #FFA500 !important; border-radius: 8px !important; background: rgba(255, 165, 0, 0.08) !important; @@ -850,6 +842,8 @@ a.create-button { .peers-list .block-info-table, .peers-technical-block .block-info-table, +.viewer-hidden-block .block-info-table, +.parliament-rows-table, .gov-overview { display: block !important; width: 100% !important; @@ -860,17 +854,23 @@ a.create-button { } .peers-list .block-info-table tbody, .peers-technical-block .block-info-table tbody, +.viewer-hidden-block .block-info-table tbody, +.parliament-rows-table tbody, .gov-overview tbody { display: block !important; width: 100% !important; } -.gov-overview thead { display: none !important; } +.gov-overview thead, +.parliament-rows-table thead { display: none !important; } .peers-list .block-info-table tr:first-child, -.peers-technical-block .block-info-table tr:first-child { +.peers-technical-block .block-info-table tr:first-child, +.viewer-hidden-block .block-info-table tr:first-child { display: none !important; } .peers-list .block-info-table tr, .peers-technical-block .block-info-table tr, +.viewer-hidden-block .block-info-table tr, +.parliament-rows-table tbody tr, .gov-overview tbody tr { display: block !important; width: 100% !important; @@ -883,6 +883,8 @@ a.create-button { } .peers-list .block-info-table td, .peers-technical-block .block-info-table td, +.viewer-hidden-block .block-info-table td, +.parliament-rows-table tbody td, .gov-overview tbody td { display: flex !important; justify-content: space-between !important; @@ -898,6 +900,8 @@ a.create-button { } .peers-list .block-info-table td::before, .peers-technical-block .block-info-table td::before, +.viewer-hidden-block .block-info-table td::before, +.parliament-rows-table tbody td::before, .gov-overview tbody td::before { content: attr(data-label); flex: 0 0 auto !important; @@ -910,7 +914,606 @@ a.create-button { white-space: nowrap !important; word-break: normal !important; } -.peers-technical-block .block-info-table td[data-label=""] { justify-content: center !important; } -.peers-technical-block .block-info-table td[data-label=""]::before { content: none !important; } +.peers-technical-block .block-info-table td[data-label=""], +.viewer-hidden-block .block-info-table td[data-label=""] { justify-content: center !important; } +.peers-technical-block .block-info-table td[data-label=""]::before, +.viewer-hidden-block .block-info-table td[data-label=""]::before { content: none !important; } .peers-list .block-info-table td .peer-key, .peers-technical-block .block-info-table td .peer-key { text-align: right !important; } +.phone-history td.phone-dir, +.phone-contact .phone-dir, +.phone-dir { + display: none !important; +} +.phone-contact { + gap: 6px !important; + padding: 8px !important; +} +.phone-contact .user-link { + flex: 1 1 100% !important; + min-width: 0 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +.phone-contact .date-link { + flex: 0 1 auto !important; + font-size: 12px !important; +} +.phone-history, +.phone-history tbody { + display: block !important; + width: 100% !important; +} +.phone-history tr:first-child { + display: none !important; +} +.phone-history tr { + display: block !important; + width: 100% !important; + box-sizing: border-box !important; + border: 1px solid #444 !important; + border-radius: 10px !important; + margin: 0 0 8px 0 !important; + padding: 4px 10px !important; +} +.phone-history td { + display: flex !important; + justify-content: space-between !important; + align-items: baseline !important; + gap: 10px !important; + width: 100% !important; + border: 0 !important; + padding: 3px 0 !important; + font-size: 12px !important; + text-align: right !important; +} +.phone-history td::before { + content: attr(data-label); + flex: 0 0 auto !important; + font-weight: 700 !important; + text-transform: uppercase; + font-size: 10px !important; + letter-spacing: .03em; + opacity: .7 !important; + text-align: left !important; + white-space: nowrap !important; +} +.phone-history td.phone-row-actions { + justify-content: flex-end !important; + padding: 0 !important; + margin-top: -6px !important; +} +.phone-history .phone-row-delete .btn-singleview, +.phone-latest-row .phone-row-delete .btn-singleview { + width: 32px !important; + height: 32px !important; + min-width: 0 !important; + min-height: 0 !important; + line-height: 1 !important; + padding: 0 !important; + font-size: 14px !important; +} +.phone-history td.phone-row-actions::before { + content: none !important; +} +.phone-latest-row .phone-row-delete { + margin-left: auto !important; +} +.phone-history td .user-link { + min-width: 0 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +.peers-list .block-info-table td .ubi-tick-ok, +.peers-list .block-info-table td .peer-tor-no, +.peers-technical-block .block-info-table td .ubi-tick-ok, +.peers-technical-block .block-info-table td .peer-tor-no { + padding: 0 !important; + line-height: 1.2 !important; +} + +.conn-actions, +.peers-conn-actions { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + gap: 8px !important; + width: 100% !important; +} +.conn-actions form, +.peers-conn-actions form { + flex: 1 1 45% !important; + min-width: 130px !important; + max-width: 100% !important; + margin: 0 !important; + display: block !important; +} +.conn-actions form button, +.peers-conn-actions form button { + width: 100% !important; + min-width: 0 !important; +} + +.relationship-status .relationship-actions { flex-direction: column !important; align-items: stretch !important; gap: 8px !important; } +.relationship-status .relationship-actions form, +.relationship-status .relationship-actions button { width: 100% !important; } + +.profile-sensors-box { align-items: center !important; text-align: center !important; } +.profile-sensors-box > * { margin-left: auto !important; margin-right: auto !important; justify-content: center !important; } + +.profile-layout, +.profile-layout .tribe-side, +.profile-layout .tribe-main { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + box-sizing: border-box !important; +} +.profile-side-description, +.profile-side-description p, +.trending-card .card-section, +.feed-text { + word-break: normal !important; + overflow-wrap: break-word !important; +} + +.invite-qr-card { display: flex !important; justify-content: center !important; border: none !important; background: transparent !important; padding: 0 !important; box-shadow: none !important; margin: 0 auto !important; } +.invite-qr-img { display: block; margin: 0 auto; } + +.stats-kpi { display: flex !important; flex-direction: column !important; align-items: center !important; text-align: center !important; gap: 2px !important; } +.stats-kpi-label, +.stats-kpi-value { width: 100% !important; text-align: center !important; } + +.main-column audio, +.main-column video { width: 100% !important; max-width: 100% !important; } + +.main-column section:has(.chat-messages-list) { border: none !important; background: transparent !important; padding: 0 !important; box-shadow: none !important; } +.tribe-details:has(.chat-messages-list) { display: block !important; border: none !important; background: transparent !important; padding: 6px !important; margin: 0 !important; box-shadow: none !important; } +.chat-full-width { border: none !important; background: transparent !important; padding: 0 !important; } +.chat-messages-list { padding: 0 !important; } +.chat-message { max-width: 92% !important; } +.chat-message.chat-poll { max-width: 97% !important; } +.feed-workspace { + display: block !important; +} +.feed-side { + display: none !important; +} +.tribe-info-table, +.tribe-info-table tbody, +.tribe-info-table tr, +.tribe-info-table td { + display: block !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + text-align: left !important; + overflow: visible !important; +} +.tribe-info-table td .user-link { + display: inline-block !important; + max-width: 100% !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; +} +.school-lesson-summary { + flex-wrap: wrap !important; +} +.school-lesson-summary-title { + flex: 1 1 100% !important; + min-width: 0 !important; + white-space: normal !important; + overflow-wrap: anywhere !important; +} +.school-lesson-summary-meta { + flex: 1 1 100% !important; + white-space: normal !important; +} +.feed-votes-summary { + flex-wrap: wrap !important; + max-width: 100% !important; + min-width: 0 !important; +} +.feed-votes-summary .vote-category { + white-space: nowrap !important; + flex: 0 0 auto !important; +} +.tribe-open-invite-code { + font-size: .85em !important; +} + +.school-progress-row { + flex-direction: column !important; + align-items: flex-start !important; + gap: 6px !important; +} +.school-progress-issue { + width: 100% !important; +} +.school-progress-issue input[type="text"] { + max-width: 100% !important; + flex: 1 1 100% !important; +} + +.subscription-box-block { + display: flex !important; + flex-wrap: wrap !important; + align-items: center !important; + gap: 8px !important; +} +.subscription-box-block .subscription-form { margin: 0 !important; } + +.profile-module-section, +.profile-module-section .trending-card, +.profile-module-section .card-body, +.profile-module-section .card-section { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + box-sizing: border-box !important; +} +.card-section-action { + display: flex !important; + justify-content: center !important; + width: 100% !important; +} +.card-section-action .keep-reading-link { + text-align: center !important; +} +.card-field.image-container,.card-field.video-container,.card-field.audio-container { justify-content: center !important; } +.card-field.media-left { justify-content: flex-start !important; } + +.card-footer .date-link, +.card-footer .user-link { padding: 0 10px !important; font-size: 12px !important; } +.card-meta-row .opinions-summary, +.card-meta-row .comments-summary-link { padding: 0 10px !important; font-size: 12px !important; } + +.parliament-rows-table tbody td[data-label=""] { justify-content: center !important; } +.parliament-rows-table tbody td[data-label=""]::before { content: none !important; } +.parliament-rows-table tbody td .parliament-avatar-cell { + display: flex !important; + align-items: center !important; + gap: 8px !important; + justify-content: flex-end !important; +} +.parliament-rows-table tbody td .parliament-leader-avatar { width: 32px !important; height: 32px !important; } +.parliament-rows-table tbody td .parliament-method-img { max-height: 28px !important; width: auto !important; } +.parliament-rows-table tbody td form { margin: 0 !important; } + +.hemi-legend { + flex-direction: column !important; + align-items: flex-start !important; + gap: 6px !important; + justify-content: flex-start !important; +} +.hemi-legend-item { + width: 100% !important; + flex-wrap: nowrap !important; + justify-content: flex-start !important; +} +.hemi-legend-count { margin-left: auto !important; } +.hemi-modes { flex-wrap: wrap !important; } +.room-item .mailing-archive-head{flex-wrap:wrap} + +.oasis-nav-side-toggle { + position: absolute !important; + width: 1px !important; + height: 1px !important; + opacity: 0 !important; + pointer-events: none !important; +} +.oasis-nav-side-toggle ~ .header { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: center !important; + gap: 8px 6px !important; + padding: 8px 8px 4px !important; + min-height: 60px !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + z-index: 900 !important; + box-sizing: border-box !important; +} +.oasis-nav-side-toggle ~ .header + * , +.oasis-nav-side-toggle ~ .header ~ .update-banner:first-of-type { + margin-top: 0 !important; +} +body:has(.oasis-nav-side-toggle) { + padding-top: 64px !important; +} +body:has(:is(input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, select):focus) .oasis-nav-side-toggle ~ .header { + position: absolute !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left, +.oasis-nav-side-toggle ~ .header .top-bar-right, +.oasis-nav-side-toggle ~ .header .top-bar-mid { + display: contents !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left a.logo-icon { + order: 1 !important; + flex: 0 0 auto !important; + margin: 0 !important; + padding: 0 !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left a.logo-icon img.logo-icon { + width: 44px !important; + height: 44px !important; + margin: 0 !important; +} +.oasis-nav-side-toggle ~ .header .oasis-mobile-menus { + order: 2 !important; + display: flex !important; + flex: 1 1 auto !important; + flex-wrap: nowrap !important; + justify-content: flex-end !important; + align-items: center !important; + gap: 5px !important; + margin: 0 !important; + padding: 0 !important; +} +.oasis-nav-side-toggle ~ .header .oasis-menu-slot { + display: inline-flex !important; +} +.oasis-nav-side-toggle ~ .header .oasis-menu-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + gap: 4px !important; + min-height: 44px !important; + padding: 6px 9px !important; + white-space: nowrap !important; + cursor: pointer !important; +} +.oasis-nav-side-toggle ~ .header .oasis-menu-slot .oasis-menu-close { + display: none !important; +} +#nav-left-toggle:checked ~ .header .oasis-menu-slot-left .oasis-menu-open, +#nav-right-toggle:checked ~ .header .oasis-menu-slot-right .oasis-menu-open { + display: none !important; +} +#nav-left-toggle:checked ~ .header .oasis-menu-slot-left .oasis-menu-close, +#nav-right-toggle:checked ~ .header .oasis-menu-slot-right .oasis-menu-close { + display: inline-flex !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left nav { + order: 3 !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-right nav { + order: 4 !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left nav, +.oasis-nav-side-toggle ~ .header .top-bar-right nav { + flex: 1 1 calc(50% - 4px) !important; + width: auto !important; + min-width: 0 !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left nav ul, +.oasis-nav-side-toggle ~ .header .top-bar-right nav ul { + display: grid !important; + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; + gap: 4px !important; + list-style: none !important; + padding: 0 !important; + margin: 0 !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left nav ul li, +.oasis-nav-side-toggle ~ .header .top-bar-right nav ul li { + margin: 0 !important; + min-width: 0 !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left nav ul li a, +.oasis-nav-side-toggle ~ .header .top-bar-right nav ul li a { + position: relative !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 100% !important; + min-height: 50px !important; + padding: 0 !important; + font-size: 0 !important; + text-decoration: none !important; + box-sizing: border-box !important; +} +.oasis-nav-side-toggle ~ .header nav ul li a .nav-text { + display: none !important; +} +.oasis-nav-side-toggle ~ .header nav ul li a .emoji { + font-size: 1.6rem !important; + line-height: 1 !important; + margin: 0 !important; +} +.oasis-nav-side-toggle ~ .header nav ul li a .inbox-badge { + position: absolute !important; + top: 3px !important; + right: 3px !important; + margin: 0 !important; + font-size: 0.66rem !important; +} +.oasis-nav-side-toggle ~ .main-content { + display: flex !important; + flex-direction: column !important; +} +.oasis-nav-side-toggle ~ .main-content .main-column { + order: 0 !important; +} +.oasis-nav-side-toggle ~ .main-content .sidebar-left, +.oasis-nav-side-toggle ~ .main-content .sidebar-right { + display: none !important; +} +#nav-left-toggle:checked ~ .main-content .sidebar-left, +#nav-right-toggle:checked ~ .main-content .sidebar-right { + display: block !important; + order: -1 !important; + width: 100% !important; + max-width: 100% !important; + padding: 0 4px !important; + margin: 0 0 8px 0 !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-header { + margin: 0 0 6px 0 !important; + padding: 12px !important; +} +.oasis-nav-side-toggle ~ .main-content .sidebar-left .oasis-nav-list, +.oasis-nav-side-toggle ~ .main-content .sidebar-right .oasis-nav-list { + display: grid !important; + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; + gap: 8px !important; + margin: 0 !important; + padding: 0 !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-list li { + margin: 0 !important; + padding: 0 !important; + min-width: 0 !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-list li a { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + gap: 6px !important; + min-height: 74px !important; + height: 100% !important; + padding: 8px 4px !important; + margin: 0 !important; + text-align: center !important; + font-size: 0.72rem !important; + opacity: 1 !important; + box-sizing: border-box !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-list li a .emoji { + width: auto !important; + font-size: 1.5rem !important; + line-height: 1 !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-list li a .nav-text { + line-height: 1.15 !important; + overflow-wrap: anywhere !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left nav, +.oasis-nav-side-toggle ~ .header .top-bar-right nav { + position: fixed !important; + bottom: 0 !important; + height: 66px !important; + width: 50% !important; + flex: none !important; + z-index: 900 !important; + margin: 0 !important; + box-sizing: border-box !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left nav { + left: 0 !important; + right: auto !important; + padding: 6px 2px 6px 6px !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-right nav { + right: 0 !important; + left: auto !important; + padding: 6px 6px 6px 2px !important; +} +.oasis-nav-side-toggle ~ .oasis-footer { + padding-bottom: 84px !important; +} +body:has(:is(input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, select):focus) .oasis-nav-side-toggle ~ .header .top-bar-left nav, +body:has(:is(input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, select):focus) .oasis-nav-side-toggle ~ .header .top-bar-right nav { + display: none !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-left nav { + width: calc(300% / 7) !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-right nav { + width: calc(400% / 7) !important; +} +.oasis-nav-side-toggle ~ .header .top-bar-right nav ul { + grid-template-columns: repeat(4, minmax(0, 1fr)) !important; +} +.oasis-nav-side-toggle ~ .main-content .sidebar-left nav > ul, +.oasis-nav-side-toggle ~ .main-content .sidebar-right nav > ul { + display: grid !important; + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; + grid-auto-flow: row dense !important; + gap: 8px !important; + list-style: none !important; + padding: 0 !important; + margin: 0 !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-group { + display: contents !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-group > .oasis-nav-header { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + gap: 6px !important; + min-height: 74px !important; + margin: 0 !important; + padding: 8px 4px !important; + text-align: center !important; + font-size: 0.68rem !important; + box-sizing: border-box !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-group > .oasis-nav-header .emoji { + width: auto !important; + font-size: 1.5rem !important; + line-height: 1 !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-group > .oasis-nav-header .nav-text { + flex: 0 0 auto !important; + line-height: 1.15 !important; + overflow-wrap: anywhere !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-group > .oasis-nav-header .oasis-nav-arrow { + display: none !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-group > .oasis-nav-list { + grid-column: 1 / -1 !important; + max-height: none !important; + overflow: visible !important; +} +.oasis-nav-side-toggle ~ .main-content .oasis-nav-toggle:not(:checked) + .oasis-nav-header + .oasis-nav-list { + display: none !important; +} +.oasis-nav-side-toggle ~ .header .oasis-phone-state { + order: 1 !important; + flex: 1 1 0 !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; + align-self: center !important; + margin: 0 !important; + padding: 0 !important; + min-width: 0 !important; +} +.oasis-nav-side-toggle ~ .header:has(.oasis-phone-state) .oasis-mobile-menus { + flex: 0 1 auto !important; +} +.oasis-nav-side-toggle ~ .header .oasis-phone-state-btn { + appearance: none !important; + -webkit-appearance: none !important; + font: inherit !important; + display: inline-flex !important; + align-items: center !important; + gap: 3px !important; + min-height: 0 !important; + justify-content: center !important; + width: 38px !important; + height: 38px !important; + padding: 0 !important; + font-size: 8.5px !important; + line-height: 1.2 !important; + white-space: nowrap !important; + border-radius: 999px !important; + cursor: pointer !important; +} +.oasis-nav-side-toggle ~ .header .oasis-phone-state-btn .pm-exposition-icon { + font-size: 22px !important; + line-height: 1 !important; + margin: 0 !important; +} diff --git a/src/client/assets/styles/mobile_base.css b/src/client/assets/styles/mobile_base.css new file mode 100644 index 0000000..e3c89ba --- /dev/null +++ b/src/client/assets/styles/mobile_base.css @@ -0,0 +1,1002 @@ +/* Maqueta de movil de la rama (lanzador, barras, tarjetas, formularios). + * + * Hasta el 19/10/2026 vivia dentro de themes/OasisMobile.css, que es un tema del dev + * y cambia en 21 de cada 39 versiones: cada integracion era elegir entre su fichero + * y el nuestro. Ahora ese tema vuelve a ser el suyo y esto va aparte. En el movil se + * carga siempre, antes del skin (Acampada.css o el OasisMobile.css del dev). + * Ver GUIA_DE_DESARROLLO.md. */ + +html { overflow-x: clip; overflow-y: visible; height: auto; } +body { overflow-x: clip; overflow-y: visible; } + +.oasis-bottombar { + display: flex; + position: fixed; + left: 0; right: 0; bottom: 0; + z-index: 1000; + justify-content: space-around; + align-items: center; + gap: 4px; + background: #1F1F1F; + border-top: 1px solid #333; + padding: 8px 6px calc(8px + env(safe-area-inset-bottom, 0px)); +} +.oasis-bottombar .bb-item { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + flex: 1 1 0; + min-width: 0; + padding: 2px 0; + text-decoration: none; +} +.oasis-bottombar .bb-ico { font-size: 18px; line-height: 1; color: #FFD700; } +.oasis-bottombar .bb-lbl { font-size: 9px; color: #FFD700; white-space: nowrap; } +.oasis-bottombar .bb-badge { + position: absolute; + top: -4px; + left: calc(50% + 6px); + min-width: 15px; height: 15px; + padding: 0 3px; + border-radius: 8px; + background: #FFD700; + color: #121212; + font-size: 9px; font-weight: bold; + display: flex; align-items: center; justify-content: center; +} + +body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); } + +/* topbar FIJA arriba: [logo] [Personal] [Community] [avatar]; el lanzador scrollea por debajo */ +.oasis-mobile-topbar { + position: fixed; top: 0; left: 0; right: 0; z-index: 100; + display: flex; align-items: center; gap: 8px; + padding: 8px 12px; + background: #121212; + border-bottom: 1px solid #333; +} +body { padding-top: 74px; background: #121212; } + +.oasis-mobile-topbar .omt-logo, .oasis-mobile-topbar .omt-avatar { flex: 0 0 auto; } +.oasis-mobile-topbar .omt-logo img, +.oasis-mobile-topbar .omt-avatar img { + width: 42px; height: 42px; + border-radius: 50%; + border: 2px solid #FFD700; + object-fit: cover; display: block; +} + +.oasis-mobile-topbar .omt-menus { + flex: 1 1 auto; min-width: 0; + display: flex; justify-content: center; gap: 8px; +} +.oasis-mobile-topbar .omt-menus .oasis-menu-btn { + display: flex; align-items: center; justify-content: center; + padding: 7px 14px; + border: 1px solid #FFD700; border-radius: 8px; + background: #161616; color: #FFD700; + font-weight: 600; letter-spacing: .5px; text-decoration: none; + font-size: 13px; white-space: nowrap; cursor: pointer; +} +.oasis-mobile-topbar .omt-menus .oasis-menu-btn.active { + background: #FFD700; color: #121212; border-color: #FFD700; +} +.oasis-mobile-topbar .omt-menus .oasis-menu-btn span { text-decoration: none; } + +/* ANULAR el "card" global de style.css `div {background:#222;border-radius:10px;box-shadow;padding:20px;margin-bottom:20px}` + en nuestro chrome móvil propio: era el 2º color (caja mas clara) detras de los botones, + la caja redondeada de la topbar y el fondo #222 detras del lanzador. */ +.header, .launcher, .ln-group, .ln-grid, +.oasis-mobile-topbar, .oasis-mobile-topbar .omt-menus { + background-color: transparent; box-shadow: none; border-radius: 0; +} +.oasis-mobile-topbar { background-color: #121212; } /* fija: fondo opaco (el contenido scrollea por debajo) */ +.oasis-mobile-topbar .omt-menus { padding: 0; margin: 0; } /* sin caja ni relleno detras de los botones */ + +.header .top-bar-left > a.logo-icon { display: none; } + +.oasis-mobile-menus { display: flex; gap: 8px; padding: 0 10px 6px; } +.oasis-mobile-menus .oasis-menu-ico { display: none; } +.oasis-mobile-menus .oasis-menu-btn { + flex: 1 1 0; + display: flex; align-items: center; justify-content: center; + padding: 10px 12px; + border: 1px solid #FFD700; border-radius: 8px; + background: #161616; color: #FFD700; + font-weight: 600; letter-spacing: 1px; text-decoration: none; cursor: pointer; +} +.oasis-mobile-menus .oasis-menu-btn span { text-decoration: none; } + +.oasis-mobile-menus .oasis-menu-btn.active { + background: #FFD700; color: #121212; border-color: #FFD700; +} + +:root { + --space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px; --space-5: 32px; + --fs-caption: 12px; --fs-body: 14px; --fs-body-lg: 16px; --fs-title: 20px; + --lh-body: 1.5; --measure: 66ch; + --c-muted: #8c8c8c; --radius-md: 12px; +} + +.main-column, .main-column p, .main-column li, .main-column a, +.main-column span, .main-column td, .main-column h1, .main-column h2, .main-column h3 { + overflow-wrap: anywhere; + word-break: break-word; +} +.main-column { padding: var(--space-3); } +.main-column p { line-height: var(--lh-body); } + +.main-column > * + * { margin-top: var(--space-3); } + +.main-column img, .main-column video, .main-column table, .main-column pre, .main-column iframe { + max-width: 100%; height: auto; +} +.main-column pre { white-space: pre-wrap; } + +.oasis-nav-group { + list-style: none; + background: #1c1c1c; + border: 1px solid #333; + border-radius: var(--radius-md); + overflow: hidden; + margin-bottom: var(--space-2); +} +.oasis-nav-header { + display: flex; align-items: center; gap: var(--space-2); + min-height: 48px; padding: 0 var(--space-3); + cursor: pointer; + color: #FFD700; font-weight: bold; + text-transform: uppercase; letter-spacing: .04em; + font-size: var(--fs-caption); +} +.oasis-nav-header .nav-text { flex: 1 1 auto; } +.oasis-nav-header .oasis-nav-arrow { color: var(--c-muted); transition: transform 150ms ease; } +.oasis-nav-toggle:checked + .oasis-nav-header .oasis-nav-arrow { transform: rotate(180deg); } +.oasis-nav-header:active { background: #262626; } +.oasis-nav-list { list-style: none; padding: 0; margin: 0; } +.oasis-nav-list li a { + display: flex; align-items: center; gap: var(--space-3); + min-height: 48px; padding: 0 var(--space-3); + color: #FFD700; text-decoration: none; + border-top: 1px solid #262626; +} +.oasis-nav-list li a .emoji { flex: 0 0 auto; width: 22px; text-align: center; color: #FFD700; } +.oasis-nav-list li a .nav-text { font-size: var(--fs-body); } +.oasis-nav-list li a:active { background: #262626; } +.oasis-nav-list li a.current { color: #FFD700; font-weight: bold; } + +.main-column h1 { font-size: var(--fs-title); font-weight: 700; line-height: 1.25; margin: 0 0 var(--space-2); } +.main-column h2 { font-size: 18px; font-weight: 700; line-height: 1.3; margin: 0 0 var(--space-2); } +.main-column h3 { font-size: var(--fs-body-lg); font-weight: 600; margin: 0 0 var(--space-1); } +.main-column p { font-size: var(--fs-body-lg); } +.main-column .created-at, .main-column .time, +.main-column .meta, .main-column .subtitle { + color: var(--c-muted); font-size: var(--fs-caption); +} + +/* solo las tarjetas (clase acabada en -card), no sus piezas: -card-header, + -card-hero-image... heredaban fondo, borde y relleno y salian como cajas */ +.main-column article, +.main-column :is([class$="-card"], [class*="-card "]) { + background: #1c1c1c; + border: 1px solid #333; + border-radius: var(--radius-md); + padding: var(--space-3); +} +.main-column article:active, +.main-column :is([class$="-card"], [class*="-card "]):active { background: #202020; } + +.btn, button, input[type="submit"], input[type="button"], .submit-button, +.filter-btn, .create-button, .delete-btn, .buy-btn { + min-height: 48px; +} +button:active, input[type="submit"]:active, .submit-button:active, +.filter-btn:active, .create-button:active, +.delete-btn:active, .buy-btn:active { + filter: brightness(.85); +} +.main-column a:active { opacity: .6; } + +* { -webkit-tap-highlight-color: transparent; } +a, button, label, .oasis-menu-btn, .bb-item, .oasis-nav-header, .oasis-nav-list li a { + touch-action: manipulation; +} +body { overscroll-behavior: none; } + +.main-content, .main-column, .header { overflow: visible; } + +.bb-manage { margin: 8px 0 16px; } +.bb-manage h3, .bb-manage-add h3 { + font-size: 13px; text-transform: uppercase; letter-spacing: .05em; + color: #FFD700; margin: 14px 0 8px; +} +.bb-manage-list { display: flex; flex-direction: column; gap: 8px; } +.bb-manage-form { margin: 0; width: 100%; } +.bb-manage-btn { + width: 100%; + display: flex; align-items: center; gap: 12px; + min-height: 52px; padding: 0 14px; + background: #1c1c1c; border: 1px solid #333; border-radius: 10px; + color: #FFD700; cursor: pointer; text-align: left; +} +.bb-manage-ico { font-size: 20px; color: #FFD700; width: 26px; text-align: center; } +.bb-manage-lbl { flex: 1 1 auto; font-size: 15px; } +.bb-manage-x { font-size: 16px; color: #FFD700; font-weight: bold; } +.bb-manage-empty, .bb-manage-full { color: #999; font-size: 14px; padding: 8px 2px; } +.bb-manage-done { + display: block; text-align: center; + margin-top: 16px; min-height: 48px; line-height: 48px; +} + +/* Lanzador de modulos de la home (sustituye a los hexagonos): secciones con un + rotulo discreto y una rejilla de baldosas, como el cajon de apps del movil */ +.launcher { margin: 6px 0 20px; padding: 0; border: 0; } +.ln-group { margin: 0; padding: 0; border: 0; } +.ln-group + .ln-group { margin-top: 20px; } +.ln-title { + margin: 0 2px 10px; padding: 0; border: 0; + font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; + color: var(--c-muted); +} +.ln-grid { + display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px 8px; margin: 0; padding: 0; +} +.ln-app { + display: flex; flex-direction: column; align-items: center; gap: 6px; + min-width: 0; text-decoration: none; -webkit-tap-highlight-color: transparent; +} +.ln-ico { + width: 54px; height: 54px; box-sizing: border-box; + display: flex; align-items: center; justify-content: center; + border-radius: 16px; background: #1c1c1c; border: 1px solid #333; + color: #FFD700; font-size: 22px; line-height: 1; + transition: background-color .12s, border-color .12s; +} +.ln-app:active .ln-ico, .ln-app:focus-visible .ln-ico { background: #2a2412; border-color: #FFD700; } +.ln-lbl { + max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-size: 11px; line-height: 1.2; text-align: center; color: #e6e6e6; +} + +.header .top-bar-left nav, +.header .top-bar-right nav { display: none; } + +.oasis-mobile-topbar { padding-top: calc(env(safe-area-inset-top, 0px) + 10px); } + +.oasis-bottombar a.bb-item.current .bb-ico { + background: #FFD700; color: #121212; + border-radius: 999px; padding: 4px 12px; +} +.oasis-bottombar a.bb-item.current .bb-lbl { color: #FFD700; font-weight: bold; } +.oasis-bottombar a.bb-item.current .bb-badge { background: #121212; color: #FFD700; } + +.main-column, .main-column p, .main-column li, .main-column a, +.main-column span, .main-column td, .main-column th, .main-column label, +.main-column h1, .main-column h2, .main-column h3, .main-column h4, +.main-column strong, .main-column em, .main-column small, +.created-at, .feed-row .time, .feed-row .created-at, +.profile-sensors-box > *, +.card-field .card-value, .card-field .card-label { + overflow-wrap: break-word; + word-break: normal; + -webkit-hyphens: none; + hyphens: none; +} + +.main-column pre, .main-column code, kbd, samp, +.peer-key, .user-link.peer-key, .oasis-id-copy, .search-oasis-id, +.pm-sent-key-value, .kpi__value, +.block-info-table td .peer-key, .block-info-table td .user-link, +.pm-info-table td .user-link, +.main-column a[href*="ed25519"], .main-column a[href^="ssb:"], +.main-column a[href^="/author/"] { + overflow-wrap: anywhere; + word-break: break-all; +} + +button, input[type="submit"], input[type="button"], +.filter-btn, a.filter-btn, .create-button, .delete-btn, .buy-btn, .submit-button, .btn, +.conn-actions button, .peers-conn-actions button, +.mode-buttons button, .mode-buttons .filter-btn, +.filter-group button, .filter-group .filter-btn, +.main-column button, .main-column input[type="submit"] { + white-space: normal; + word-break: normal; + overflow-wrap: break-word; + -webkit-hyphens: none; + hyphens: none; +} + +.conn-actions, .peers-conn-actions { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: stretch; + gap: 8px; + width: 100%; + max-width: 100%; + overflow-x: hidden; +} +.conn-actions > form, .peers-conn-actions > form { + flex: 0 1 auto; + width: auto; + min-width: 0; + margin: 0; + display: flex; +} + +.pm-info-table, .pm-info-table tbody, +.block-info-table, .block-info-table tbody { + display: block; + width: 100%; + max-width: 100%; + border: 0; + background: transparent; + box-shadow: none; + table-layout: auto; +} +.pm-info-table thead, .block-info-table thead { display: none; } +.pm-info-table tr, .block-info-table tr { + display: block; + width: 100%; + box-sizing: border-box; + border: 0; + border-bottom: 1px solid rgba(255,215,0,0.18); + border-radius: 0; + margin: 0 0 8px 0; + padding: 6px 0; + background: transparent; +} +.pm-info-table td, .block-info-table td { + display: block; + width: 100%; + max-width: 100%; + box-sizing: border-box; + border: 0; + background: transparent; + padding: 1px 0; + text-align: left; + overflow-wrap: break-word; + word-break: normal; +} + +.pm-info-table td.card-label, .block-info-table td.card-label { + color: var(--c-muted, #8c8c8c); + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .03em; + margin-bottom: 2px; + padding-bottom: 0; +} +.pm-info-table td.card-value, .block-info-table td.card-value { + font-size: 15px; + line-height: 1.5; + margin-bottom: 6px; +} + +.block-info-table tr:first-child:has(> td.card-label) { display: none; } +.block-info-table td[data-label]:not([data-label=""])::before { + content: attr(data-label); + display: block; + color: var(--c-muted, #8c8c8c); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .03em; + margin-bottom: 1px; + white-space: nowrap; + word-break: normal; +} +.block-info-table td[data-label=""]::before { content: none; } + +.peers-list .block-info-table td, +.peers-technical-block .block-info-table td, +.gov-overview tbody td { + word-break: normal; + overflow-wrap: break-word; +} + +.pm-info-table td .user-link, .pm-info-table td .peer-key, .pm-info-table td a[href], +.block-info-table td .peer-key, .block-info-table td .user-link, .block-info-table td a[href] { + display: inline-block; + max-width: 100%; + overflow-wrap: anywhere; + word-break: break-all; +} + +.pm-info-table td form, .block-info-table td form, +.pm-info-table td button, .block-info-table td button { + width: 100%; + margin: 0; +} + +.main-content { width: 100%; max-width: 100%; } +.main-column { + width: 100%; max-width: 100%; + padding: 16px; box-sizing: border-box; +} +.main-column .card-field { + flex-direction: column; + align-items: stretch; + gap: 2px; +} +.main-column .card-field .card-value { + width: 100%; max-width: 100%; font-size: 15px; +} +.main-column .card-field .card-label { + color: var(--c-muted, #8c8c8c); font-size: 11px; +} +.main-column img, .main-column video, .main-column iframe, .main-column canvas { + max-width: 100%; height: auto; +} +.main-column hr { + width: 100%; border: 0; + border-top: 1px solid #333; margin: 16px 0; +} + +.message-list { display: flex; flex-direction: column; gap: 4px; width: 100%; } +.message-list .pm-thread { display: flex; flex-direction: column; width: 100%; align-self: stretch; } +.pm-thread-replies { display: flex; flex-direction: column; gap: 4px; } + +.pm-thread .pm-thread-details { width: 100%; } +.pm-thread .pm-thread-toggle { font-size: 12px; color: var(--c-muted, #8c8c8c); padding: 4px 0; cursor: pointer; } + +.main-column h1, .main-column h2, .main-column h3, .main-column h4, +.tags-header h2, .tags-header h3, .title-with-chip h2, .pm-title, h2.pm-title, +.cn-name, h1.cn-name, .cn-section, h2.cn-section, .profile-side-name, h2.profile-side-name { + text-wrap: balance; + overflow-wrap: break-word; + word-break: keep-all; + -webkit-hyphens: none; hyphens: none; + text-align: left; +} +.main-column h1, .tags-header h2, .cn-name { + font-size: var(--fs-title, 20px); font-weight: 700; + line-height: 1.22; letter-spacing: -0.01em; + margin: 0 0 var(--space-2, 8px); +} +.main-column h2, .pm-title, .title-with-chip h2, .cn-section { + font-size: 18px; font-weight: 700; + line-height: 1.28; margin: var(--space-1, 4px) 0 var(--space-2, 8px); +} +.main-column h3 { + font-size: var(--fs-body-lg, 16px); font-weight: 600; + line-height: 1.35; margin: var(--space-1, 4px) 0; +} +.main-column h1, .main-column h2, .main-column h3, +.tags-header h2, .pm-title, .cn-name, .cn-section { + max-width: 40ch; width: auto; min-width: 0; +} + +.main-column h1 code, .main-column h2 code, .main-column h3 code, +.tags-header h2 code, .pm-title code, +.main-column h1 a[href*="ed25519"], .main-column h2 a[href*="ed25519"], +.main-column h1 a[href^="ssb:"], .main-column h2 a[href^="ssb:"] { + word-break: break-all; overflow-wrap: anywhere; +} + +.main-column .tags-header { + display: flex; flex-direction: column; align-items: flex-start; + gap: var(--space-1, 4px); width: 100%; max-width: 100%; + margin: 0 0 var(--space-2, 8px); +} +.main-column .tags-header h2 { margin: 0; } + +.main-column .title-with-chip { + display: flex; flex-wrap: wrap; align-items: center; + gap: var(--space-2, 8px); width: 100%; max-width: 100%; +} +.main-column .title-with-chip h2 { flex: 1 1 auto; min-width: 0; margin: 0; max-width: none; } +.main-column .title-with-chip > :not(h2) { flex: 0 0 auto; } + +.main-column p, .main-column li, +.tags-header p, .tags-header .prefs-help, .indexing-note, .indexing-percent, +.error-page-message, .message-text { + text-wrap: pretty; + line-height: 1.55; + max-width: min(66ch, 100%); + overflow-wrap: anywhere; + word-break: normal; + min-width: 0; + -webkit-hyphens: none; hyphens: none; +} +.main-column p + p { margin-top: var(--space-3, 16px); } +.tags-header p, .tags-header .prefs-help, .indexing-note { line-height: 1.5; margin: 4px 0 0 0; } + +[style*="auto-fill"], +[style*="minmax(240px"], [style*="minmax(280px"], [style*="minmax(300px"], +[style*="minmax( 240px"], [style*="minmax( 280px"], [style*="minmax( 300px"], +.trending-container, .cn-hub-grid, .cn-products { + display: grid !important; grid-template-columns: 1fr !important; gap: 12px !important; + width: 100% !important; max-width: 100% !important; +} +.trending-container > *, .market-grid > *, .tribe-grid > *, +.feed-container, .feed-card, .trending-card, +.main-column article, .main-column [class*="-card"], .main-column [class*="-item"], +.main-column [class*="-entry"], .main-column .card { + width: 100%; max-width: 100%; min-width: 0; + box-sizing: border-box; margin-left: 0; margin-right: 0; +} +.feed-card .feed-text, .trending-card .feed-text, +.main-column article p, .main-column [class*="-card"] p { max-width: var(--measure, 66ch); } +.main-column form, .comment-form, .comment-form-wrapper, .market-form, +.new-message-form, .project-control-form, .filter-box { + display: block; width: 100%; max-width: 100%; + min-width: 0; box-sizing: border-box; +} +.main-column input:not([type="checkbox"]):not([type="radio"]):not(.oasis-nav-toggle):not(.oasis-nav-side-toggle), +.main-column textarea, .main-column select { + width: 100%; max-width: 100%; min-width: 0; box-sizing: border-box; +} +.main-column form > button:only-of-type, .main-column form > .create-button, +.comment-form button[type="submit"], .market-form button[type="submit"], +.new-message-form button[type="submit"] { + display: block; width: 100%; max-width: 100%; + box-sizing: border-box; margin-top: 8px; +} +[style*="width:33%"], [style*="width: 33%"], [style*="width:25%"], [style*="width: 25%"], +[style*="width:60%"], [style*="width: 60%"], [style*="width:40%"], [style*="width: 40%"] { + width: 100% !important; max-width: 100% !important; +} +.main-column [style*="max-width: 800px"], .main-column [style*="max-width:800px"] { + max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; box-sizing: border-box !important; +} +.message-list, .pm-thread, .pm-thread-replies { width: 100%; max-width: 100%; box-sizing: border-box; } + +.main-column p .user-link, .main-column p a[href^="/author/"], +.main-column li .user-link, .main-column span .user-link, +.main-column .message-text .user-link, .main-column .message-text a[href^="/author/"], +.pm-quote a[href^="/author/"] { + display: inline; white-space: normal; + overflow-wrap: anywhere; word-break: keep-all; + -webkit-hyphens: none; hyphens: none; +} + +.peer-key, .user-link.peer-key, .block-info-table td .peer-key, .pm-info-table td .peer-key, +.pm-info-table td .user-link, .block-info-table td .user-link, +.search-oasis-id, .pm-sent-key-value, .kpi__value { + display: inline-block; max-width: 100%; + overflow-wrap: anywhere; word-break: break-all; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-variant-numeric: tabular-nums; line-height: 1.45; +} + +.oasis-id-copy { + display: block; width: 100%; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} +.peers-list .block-info-table td .peer-key { + display: block; max-width: 100%; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; word-break: normal; +} + +.main-column .message-text a[href^="http"], .main-column p a[href^="http"], .main-column .message-text a.oasis-path-link, +.main-column .message-text a.job-link, .main-column .message-text a.project-link, +.main-column .message-text a.market-link { + overflow-wrap: anywhere; word-break: keep-all; + -webkit-hyphens: none; hyphens: none; +} +.main-column code:not(pre code) { + display: inline; max-width: 100%; + overflow-wrap: anywhere; word-break: break-word; white-space: pre-wrap; +} + +.main-column .tags-header { margin-bottom: 6px; gap: 2px; } +.main-column .tags-header h2, .tags-header h2 { + font-size: 17px; line-height: 1.2; letter-spacing: 0; +} +.main-column .tags-header p { font-size: 13px; line-height: 1.35; margin: 0; } + +.main-column { padding: 10px 12px 16px 12px; } + +.oasis-bottombar .bb-item { min-height: 48px; } +.oasis-mobile-menus .oasis-menu-btn { min-height: 48px; } +.score-btn, .vote-btn { + min-height: 44px; min-width: 44px; + display: inline-flex; align-items: center; justify-content: center; +} +.score-btn:active, .vote-btn:active { filter: brightness(.85); } +.bb-pick-grid { + display: grid; grid-template-columns: repeat(3, 1fr); + gap: 8px; width: 100%; margin-top: 8px; +} +.bb-pick-form { margin: 0; display: block; } +.bb-pick-btn { + display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 6px; width: 100%; + min-height: 76px; padding: 10px 6px; box-sizing: border-box; + background: #1c1c1c; border: 1px solid #333; border-radius: 12px; color: #FFD700; +} +.bb-pick-btn.active { border-color: #FFD700; background: #2a2412; } +.bb-pick-ico { font-size: 26px; color: #FFD700; line-height: 1; } +.bb-pick-lbl { font-size: 11px; text-align: center; line-height: 1.2; } + +/* Ocultar los checkboxes nativos del drawer (nav-left/right-toggle) — si no, salen 2 cuadrados blancos arriba */ +.oasis-nav-side-toggle { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; + margin: 0; +} + +/* ========================================================================== + Usabilidad 0.9.1 — Chats estilo Signal + widgets compactos + foros con hilos + Todo en NUESTRO tema (carga tras mobile.css → gana por cascada, sin !important) + ========================================================================== */ + +/* --- Chats: burbujas tipo Signal (los hooks .chat-message-self/-author ya existen) --- */ +.chat-messages-list { display: flex; flex-direction: column; gap: 4px; width: 100%; } +.chat-message { + max-width: 82%; width: fit-content; min-width: 0; box-sizing: border-box; + padding: 8px 12px; margin: 2px 0; + border: 1px solid #343434; background: #1e1e1e; + border-radius: 16px; border-bottom-left-radius: 4px; + align-self: flex-start; +} +.chat-message.chat-message-self { + align-self: flex-end; margin-left: auto; margin-right: 0; + background: #2a2413; border-color: #5c4c1f; + border-bottom-left-radius: 16px; border-bottom-right-radius: 4px; +} +.chat-message-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 3px; font-size: 10px; color: var(--c-muted, #8c8c8c); } +.chat-message.chat-message-self .chat-message-meta { justify-content: flex-end; } +.chat-message-sender, .chat-message-sender a { font-size: 11px; font-weight: 700; color: #FFD700; } +.chat-message-date { opacity: .8; } +.chat-message-text { + display: block; font-size: 15px; line-height: 1.42; color: #FFD700; margin: 0; + word-break: break-word; overflow-wrap: anywhere; +} +.chat-message-text a { color: #FFD700; } +.chat-message-image-wrap { margin-top: 6px; } +.chat-message-image { max-width: 100%; border-radius: 10px; } + +/* --- Compose de chat: caja redondeada + enviar como pill (no bloque naranja de ancho completo) --- */ +.chat-message-form textarea { + width: 100%; box-sizing: border-box; border-radius: 14px; + padding: 10px 14px; resize: vertical; font-size: 15px; +} +.chat-message-form .filter-btn { + width: 46px !important; height: 46px !important; min-width: 46px !important; min-height: 46px !important; + padding: 0 !important; border-radius: 50% !important; + display: inline-flex !important; align-items: center !important; justify-content: center !important; + font-size: 0 !important; line-height: 0 !important; +} +.chat-message-form .filter-btn::before { content: "\27A4"; font-size: 18px; line-height: 1; color: #121212; } +.chat-message-form input[type="file"] { font-size: 12px; } + +/* --- Reply a mensaje en chats (server-side ?replyTo=, sin JS) --- */ +.chat-message-meta .chat-reply-btn { margin-left: auto; padding: 0 4px; color: #FFD700; text-decoration: none; font-size: 15px; opacity: .85; } +.chat-message-self .chat-message-meta .chat-reply-btn { margin-left: 8px; } +.chat-reply-cancel { margin-left: auto; color: #FFD700; text-decoration: none; font-weight: 700; font-size: 14px; } +.new-message-form { flex-wrap: wrap; } + +/* --- #1: chat estilo Signal — compose ABAJO + mensajes arriba (el reply deja de "saltar") --- */ +.tribe-main.chat-full-width { display: flex; flex-direction: column-reverse; } +#chatbox { scroll-margin-top: 8px; scroll-margin-bottom: 8px; } + +/* --- Botón spread → pill compacto (vence button {min-height:48px} + +/* --- Caja de votos (▲ n ▼): compacta y HORIZONTAL. + El problema era una cadena de flex-stretch (comment-vote-col flex → estiraba la caja a 135px). + Rompemos el estiramiento con height:auto + inline-flex en toda la cadena, con !important + para vencer el min-height:44px de .score-btn (762) y el !important de epsylon en mobile.css. --- */ +.comment-vote-col { display: block; height: auto; min-height: 0; width: auto; } +.forum-score-box { + display: inline-flex; height: auto; min-height: 0; + min-width: 0; width: auto; + padding: 4px 8px; border-radius: 10px; + flex-direction: row; flex-wrap: nowrap; gap: 12px; align-items: center; +} +.forum-score-box .forum-score-form { + display: inline-flex; height: auto; width: auto; + flex-direction: row; flex-wrap: nowrap; gap: 12px; align-items: center; +} +.score-btn { + flex: 0 0 auto !important; + width: 30px !important; height: 30px !important; min-width: 30px !important; min-height: 30px !important; + font-size: 0.95em !important; border-radius: 8px !important; padding: 0 !important; +} +.score-total { flex: 0 0 auto; font-size: 1.15em; min-height: 0; height: auto; margin: 0; white-space: nowrap; } +.comment-votes .votes-count { font-size: 11px; } + +/* Aplanar el comentario: quitar las cajas/recuadros internos para que sea UN bloque limpio + (cada sección salía como una tarjeta con padding enorme → mucho hueco muerto) */ +.forum-comment .comment-header, +.forum-comment .comment-body-row, +.forum-comment .comment-vote-col, +.forum-comment .comment-text-col, +.forum-comment .new-reply { + background: transparent; border: none; box-shadow: none; + padding: 0; margin: 6px 0 0; border-radius: 0; +} +.forum-comment .comment-header { margin-top: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; } +/* la barra de votos de abajo ya muestra el score → ocultar el ▲:0 ▼:0 redundante de la cabecera */ +.forum-comment .comment-header .comment-votes { display: none; } +.forum-comment .comment-text-col p { margin: 4px 0; } +.forum-comment .comment-text-col > div { background: transparent; border: none; padding: 0; } +.forum-comment { padding: 10px 12px; margin-bottom: 8px; } +/* barra de votos alineada a la izquierda, no centrada, para que ocupe poco. + `display:flex !important` es DE PELEA: vence a mobile.css:164 `.comment-body-row {display:block!important}` */ +.forum-comment .comment-body-row { display: flex !important; flex-direction: column; align-items: flex-start; } + +/* --- Reply/compose del foro estilo Signal: fila [textarea que crece] + [botón REDONDO con icono ↩] --- */ +/* especificidad extra (.main-column …) para ganar a nuestra propia regla `.main-column form {display:block}` sin !important */ +.main-column .new-reply .comment-form, +.main-column .new-message-form { display: flex; flex-direction: row; align-items: flex-end; gap: 8px; margin: 6px 0 0; } +.main-column .new-message-form br { display: none; } +.main-column .comment-textarea, .main-column .new-message-form textarea { + flex: 1 1 auto; box-sizing: border-box; border-radius: 14px; + padding: 9px 14px; min-height: 42px; resize: none; font-size: 14px; margin: 0; +} +.forum-send-btn { + flex: 0 0 auto !important; + width: 44px !important; height: 44px !important; min-width: 44px !important; min-height: 44px !important; + padding: 0 !important; margin: 0 !important; border-radius: 50% !important; + font-size: 0 !important; line-height: 0 !important; + display: inline-flex !important; align-items: center !important; justify-content: center !important; +} +.forum-send-btn::before { content: "\21A9"; font-size: 20px; line-height: 1; color: #121212; } +.new-reply { margin-top: 6px; } + +/* --- Fase 2: hilos anidados. mobile.css fuerza margin-left:0 !important → necesitamos !important para ganar --- */ +.forum-comment.level-1 { margin-left: 10px; border-left: 2px solid #333; } +.forum-comment.level-2 { margin-left: 20px; border-left: 2px solid #3a3a3a; } +.forum-comment.level-3 { margin-left: 28px; border-left: 2px solid #404040; } +.forum-comment.level-4 { margin-left: 34px; border-left: 2px solid #454545; } +/* comentario destacado: acento sutil a la izquierda en vez del marco naranja enorme */ +.forum-comment.highlighted-reply { border: 1px solid #3a3a3a; border-left: 3px solid #FFD700; background: #1b1a16; } + +/* --- Reply del foro SIN tocar la vista de epsylon: la cajita se agranda al enfocarla; + el botón redondo ↩ está SIEMPRE visible (el usuario pidió icono de reply, no gesto/JS). --- */ +.new-reply .comment-textarea { min-height: 42px; } +.new-reply:focus-within .comment-textarea { min-height: 64px; } + +/* ========================================================================== + Mejoras UI 0.9.1: fuente única + barra de búsqueda en topbar + hive global + bottombar fija + ========================================================================== */ + +/* --- fuente única en todo el UI móvil (quitar el lío monospace/sans) --- */ +.oasis-mobile-topbar, .oasis-mobile-menus, .launcher, .oasis-bottombar, .main-column, +.oasis-mobile-topbar input, .main-column input, .main-column textarea, .main-column select, button { + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +/* (topbar sin barra de búsqueda: vuelve el título "OASIS" — CSS .omt-title más arriba) */ + +/* --- el lanzador va en el header (debajo de los 2 botones), solo en la home --- */ +.header .launcher { width: 100%; max-width: 440px; box-sizing: border-box; margin: 4px auto 10px; padding: 0 14px; } + +/* holgura bajo la topbar fija */ +body { padding-top: 74px; } /* espejo { display: contents; } + +/* ========================================================================== + 1.3.x: upstream trae en mobile.css su propio chrome movil (cabecera fija, cajones + laterales y botones Personal/Community que los abren). La rama ya tiene el suyo + (topbar, lanzador y barra inferior), asi que aqui se apaga el de upstream sin + tocar mobile.css. Va con !important porque las reglas que anula lo llevan. + ========================================================================== */ +.oasis-nav-side-toggle ~ .header { + position: static !important; + display: flex !important; flex-direction: column !important; flex-wrap: nowrap !important; + align-items: stretch !important; + min-height: 0 !important; padding: 0 !important; gap: 0 !important; + z-index: auto !important; +} +body:has(.oasis-nav-side-toggle) { padding-top: 84px !important; } +.oasis-nav-side-toggle ~ .header .oasis-mobile-menus, +.oasis-nav-side-toggle ~ .header .top-bar-left nav, +.oasis-nav-side-toggle ~ .header .top-bar-right nav, +.oasis-nav-side-toggle ~ .header .top-bar-left a.logo-icon { display: none !important; } + +/* ========================================================================== + Upstream 1.3.3: piezas nuevas (karma del perfil, directo a pantalla completa, + salas en vivo, graphos). Copiado tal cual del tema oficial: el resto de ese + tema es otro aspecto y no se trae. + ========================================================================== */ +.main-column .live-lightbox.live-fullscreen { + top: 0 !important; + bottom: 0 !important; + z-index: 10050 !important; + background: #000 !important; +} +.main-column .live-lightbox .live-fullscreen-btn { + top: 8px !important; + right: 64px !important; + z-index: 2 !important; + color: #FFD700 !important; +} + +.profile-sensors-box span.karma-line.karma-line-stacked { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + gap: 1px !important; +} +.profile-sensors-box span.karma-line .karma-line-main { + display: block !important; + line-height: 1.25 !important; +} +.profile-sensors-box span.karma-line .karma-received-row { + display: flex !important; + justify-content: center !important; + align-items: center !important; + gap: 14px !important; + margin-top: 0 !important; + line-height: 1.25 !important; +} +.profile-sensors-box span.karma-line .karma-received { + display: inline-flex !important; + align-items: center !important; + gap: 4px !important; +} + +.main-column .room-live-panel .room-live-head { + gap: 6px !important; +} +.main-column .room-live-head .room-live-controls { + gap: 6px !important; +} +.main-column .room-live-controls .tribe-action-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + min-height: 0 !important; + height: auto !important; + padding: 5px 10px !important; + font-size: 12px !important; + line-height: 1.2 !important; + letter-spacing: 0 !important; + border-radius: 8px !important; +} +.main-column .room-live-controls .room-stream-input:invalid ~ .room-stream-go, +.main-column .room-live-controls .room-stream-input:valid ~ .room-stream-pick { + display: none !important; +} +.main-column .room-stream-chip { + display: inline-flex !important; + align-items: center !important; + gap: 4px !important; + max-width: 100% !important; + padding: 2px 10px !important; + border-radius: 12px !important; + font-size: 12px !important; + line-height: 1.4 !important; + background: rgba(255, 163, 0, 0.1) !important; +} +.main-column .room-live-panel .video-grid { + margin: 10px 0 !important; +} +.main-column .room-live-panel #room-video { + scroll-margin-top: 80px !important; +} +.main-column .room-live-panel .phone-contacts { + gap: 4px !important; +} +.main-column .phone-contact.room-peer { + min-height: 0 !important; + padding: 4px 6px !important; + gap: 6px !important; +} +.main-column .phone-contact.room-peer .user-link { + padding: 4px 10px !important; + font-size: 11px !important; +} +.main-column .phone-contact.room-peer :is(.room-peer-you, .room-peer-cam, .room-peer-muted, .room-peer-hand) { + padding: 1px 5px !important; + font-size: 10px !important; +} +.main-column .phone-contact.room-peer :is(button, .tribe-action-btn) { + min-height: 0 !important; + padding: 3px 8px !important; + font-size: 11px !important; +} +.main-column .live-lightbox.live-fullscreen { + top: 0 !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + width: 100vw !important; + height: 100vh !important; +} +.main-column .video-grid-solo .video-tile { width: 100% !important; } +.main-column .inhabitants-grid .inhabitant-card { padding-bottom: 12px !important; } + +/* ========================================================================== + Filtros como fichas. Las reglas de botones grandes (48px, ancho completo) + tambien pillaban a los filtros de chats, feed, foros, pads...: en el foro + las 20 categorias llenaban la pantalla y el contenido quedaba debajo. + Ahora van en fila como los de Actividad; "Crear ..." sigue grande, aparte. + ========================================================================== */ +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: center !important; + gap: 6px !important; + padding: 10px 12px !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) > form { + display: block !important; + width: auto !important; + flex: 0 0 auto !important; + margin: 0 !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) .filter-btn:not(.create-button) { + min-height: 0 !important; + width: auto !important; + padding: 6px 12px !important; + font-size: 12px !important; + line-height: 1.3 !important; + letter-spacing: .3px !important; + border-radius: 16px !important; + border: 1px solid #555 !important; + background: transparent !important; + color: #ddd !important; + box-shadow: none !important; + white-space: nowrap !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) .filter-btn.active:not(.create-button) { + background: #FFD700 !important; + border-color: #FFD700 !important; + color: #121212 !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) > form:has(.create-button) { + flex: 1 1 100% !important; + margin-top: 6px !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group) .create-button { + width: 100% !important; + min-height: 44px !important; +} + +/* fichas de filtro vacias (el foro solo pinta las categorias con hilos) */ +.main-column .filter-group:empty { display: none !important; } + +/* acciones de la tarjeta (borrar, fijar, enlace, abrir): una fila a la derecha. + ".main-column form" las ponia a ancho completo y partia la fila en dos */ +.main-column .content-actions { + display: flex !important; + flex-wrap: nowrap !important; + justify-content: flex-end !important; + align-items: center !important; + gap: 6px !important; + width: auto !important; + margin-left: auto !important; +} +.main-column .content-actions > form { + display: inline-flex !important; + width: auto !important; + flex: 0 0 auto !important; + margin: 0 !important; +} +.main-column .content-actions .btn-singleview { + min-height: 0 !important; + width: 36px !important; + height: 36px !important; + padding: 0 !important; + border-radius: 50% !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; +} + +/* Skins y workflows en Ajustes (views/settings_view.js): tarjetas de un toque */ +.skin-pick { display: grid !important; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin: 0; } +.skin-card { + display: flex !important; flex-direction: column; align-items: stretch; gap: 8px; + width: 100% !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 8px !important; + background: #1c1c1c !important; color: #e6e6e6 !important; + border: 1px solid #444 !important; border-radius: 16px !important; cursor: pointer; +} +.skin-card .skin-shot { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; object-position: top; border-radius: 10px; display: block; } +.skin-card .skin-name { font-size: 17px; font-weight: 700; text-align: center; } +.skin-card.is-current { border: 2px solid #FFD700 !important; background: #2a2412 !important; } +.skin-card.is-current .skin-name::after { content: " \2713"; } diff --git a/src/client/assets/styles/mobile_ux.css b/src/client/assets/styles/mobile_ux.css new file mode 100644 index 0000000..7408f0a --- /dev/null +++ b/src/client/assets/styles/mobile_ux.css @@ -0,0 +1,648 @@ +/* Piezas de usabilidad de la rama movil, para los dos skins (Acampada y OasisMobile). + * + * Va la ultima, detras del skin. Los colores salen de las variables de + * Acampada (--a26-*) si el tema las define y, si no, de la paleta oscura con oro + * de OasisMobile; asi cada pieza nueva se ve bien en los dos sin duplicarla. + * + * Letra: Anton (--ux-title) solo para titulos y para el boton principal de cada + * pantalla; todo lo demas (fichas, botones secundarios, etiquetas) en la letra del + * sistema en negrita. Antes habia Anton en botones, fichas, cabeceras de tabla y + * pestañas, y la pantalla parecia un cartel. + * + * Espacios: rejilla de 4/8. Zonas tactiles de 44px como minimo. */ + +:root { + --ux-bg: var(--a26-bg, #121212); + --ux-fg: var(--a26-fg, #f2f2f2); + --ux-accent: var(--a26-neon, #ffd700); + --ux-on-accent: #000; + --ux-soft: var(--a26-soft, #1e1e1e); + --ux-muted: var(--a26-muted, #a8a8a8); + --ux-line: var(--a26-fg, #3a3a3a); + --ux-radius: var(--a26-radius, 12px); + --ux-title: var(--a26-tit, inherit); + --ux-text: var(--a26-txt, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif); + --ux-danger: #c4003b; + --ux-ok: #0a8f3c; + --ux-warn: #c98a00; +} + +/* ---------- letra unificada ---------- */ +/* fichas de filtro, pestañas y botones secundarios: letra del sistema */ +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group, .filters) .filter-btn:not(.create-button), +.main-column .filter-box__button, .main-column .doc-export-actions .filter-btn, +.main-column .update-btn, .main-column .tribe-action-btn, .lang-chip, +.main-column table th, .card-chips-row > span, .pm-exposition-text { + font-family: var(--ux-text) !important; + font-weight: 700 !important; + letter-spacing: .02em !important; +} +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group, .filters) .filter-btn:not(.create-button) { + font-size: 15px !important; + text-transform: uppercase; +} +/* los formularios de cada modulo (crear tribu, votacion, sala...): etiquetas como + texto, no como titulos; el h2 de la pregunta de una votacion era Anton de 28px */ +.main-column .vote-form h2, .main-column form > label:not(.module-switch):not(.vote-kind-option), .main-column form label[for]:not(.module-switch) { + font-family: var(--ux-text) !important; + font-size: 15px !important; + font-weight: 700 !important; + text-transform: none !important; + letter-spacing: 0 !important; + color: var(--ux-fg) !important; +} +.form-hint { + margin: 6px 0 14px !important; + font-size: 14px !important; + line-height: 1.4; + color: var(--ux-muted) !important; +} + +/* el boton de enviar de los formularios: a todo lo ancho, abajo, que se vea que es + el final. Antes unos centrados, +.main-column form[action$="/tribes/create"][method="post"] > button[type="submit"], +.main-column form[action*="/tribes/update/"] > button[type="submit"] { + display: block !important; + width: 100% !important; + margin: 8px 0 0 !important; +} + +/* ---------- iconos SVG (lanzador, barra, modulos) ---------- */ +.has-svg { display: inline-flex !important; align-items: center; justify-content: center; } +.has-svg svg { width: 100%; height: 100%; display: block; } +.ln-ico.has-svg { padding: 15px !important; } +.ln-ico.has-svg svg { width: 30px; height: 30px; } +.oasis-bottombar .bb-ico.has-svg { + width: 56px !important; height: 30px !important; padding: 3px 0 !important; + border-radius: 16px !important; font-size: 0 !important; +} +.oasis-bottombar .bb-ico.has-svg svg { width: 24px; height: 24px; } +.bb-manage-ico.has-svg, .bb-pick-ico.has-svg { width: 24px; height: 24px; } + +/* ---------- barra de abajo: la pestaña actual con pastilla, como en Android ---------- */ +.oasis-bottombar a.bb-item.current .bb-ico.has-svg { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; } +.oasis-bottombar .bb-lbl { font-family: var(--ux-text) !important; } +/* con el teclado abierto la ventana encoge: la barra se esconde y el formulario + recupera ese tercio de pantalla */ +@media (max-height: 520px) { + .oasis-bottombar { display: none !important; } + body { padding-bottom: 0 !important; } +} + +/* ---------- aviso de bienvenida: una linea, no un bloque ---------- */ +.welcome-banner { + display: flex !important; align-items: center !important; gap: 10px !important; + padding: 8px 8px 8px 14px !important; min-height: 0 !important; +} +.welcome-banner .update-banner-icon { display: none !important; } +.welcome-banner .update-banner-text { flex: 1 1 auto; font-size: 15px !important; line-height: 1.3 !important; margin: 0 !important; } +.welcome-banner .welcome-banner-start { + flex: 0 0 auto; font-size: 16px !important; min-height: 40px !important; + padding: 6px 14px !important; margin: 0 !important; width: auto !important; +} +.welcome-banner .welcome-banner-close { margin: 0 !important; flex: 0 0 auto; } +.welcome-banner .welcome-banner-close-btn { min-height: 40px !important; width: 36px !important; margin: 0 !important; } + +/* ---------- pie de una linea ---------- */ +.oasis-footer.oasis-footer-mobile { + display: flex !important; flex-direction: column; align-items: center; gap: 4px; + padding: 14px 16px 18px !important; margin-top: 24px !important; text-align: center; + font-size: 13px !important; +} +.oasis-footer-mobile .footer-sync { + display: inline-flex; align-items: center; gap: 8px; min-height: 36px; + font-weight: 700; text-decoration: none !important; color: var(--ux-fg) !important; +} +.oasis-footer-mobile .footer-sync-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ux-muted); } +.oasis-footer-mobile .footer-sync.is-on .footer-sync-dot { background: var(--ux-ok); } +.oasis-footer-mobile .footer-version { color: var(--ux-muted) !important; } + +/* ---------- acciones de tarjeta: borrar con confirmacion ---------- */ +.content-action-confirm { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; } +.content-action-confirm > summary { list-style: none; cursor: pointer; } +.content-action-confirm > summary::-webkit-details-marker { display: none; } +.content-action-confirm[open] { flex-basis: 100%; order: 99; } +.content-action-confirm[open] > summary { background: var(--ux-danger) !important; color: #fff !important; border-color: var(--ux-danger) !important; } +.content-action-confirm .confirm-bar { + flex-basis: 100%; display: flex; align-items: center; justify-content: flex-end; gap: 10px; + margin-top: 8px; padding: 8px 8px 8px 12px; border: 2px solid var(--ux-danger); border-radius: var(--ux-radius); +} +.content-action-confirm .confirm-question { flex: 1 1 auto; font-weight: 700; font-size: 15px; text-align: left; } +.content-action-confirm form { margin: 0 !important; width: auto !important; } +.main-column .content-action-confirm .confirm-yes, .content-action-confirm .confirm-yes { + background: var(--ux-danger) !important; color: #fff !important; border-color: var(--ux-danger) !important; + box-shadow: none !important; min-height: 44px !important; margin: 0 !important; width: auto !important; + font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important; +} + +/* ---------- lista de tribus: una fila por tribu ---------- */ +.main-column .tribe-list { display: flex !important; flex-direction: column; gap: 10px; padding: 0 !important; } +.main-column a.tribe-row { + display: flex !important; flex-wrap: nowrap !important; align-items: center; gap: 12px; width: auto !important; + padding: 10px 12px !important; min-height: 72px; + border: 2px solid var(--ux-line) !important; border-radius: var(--ux-radius) !important; + background: var(--ux-bg) !important; color: var(--ux-fg) !important; text-decoration: none !important; +} +.main-column a.tribe-row:active { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; } +.tribe-row .tribe-row-thumb { + flex: 0 0 52px; width: 52px !important; height: 52px !important; border-radius: 10px !important; + object-fit: cover; border: 2px solid var(--ux-line) !important; margin: 0 !important; +} +.tribe-row .tribe-row-icon { padding: 11px; background: var(--ux-soft); } +.tribe-row .tribe-row-text { flex: 1 1 0; min-width: 0; overflow: hidden; display: flex; flex-direction: column; gap: 2px; } +.tribe-row .tribe-row-text > span { display: block; max-width: 100%; margin: 0 !important; padding: 0 !important; line-height: 1.3; } +.tribe-row .tribe-row-title { + font-family: var(--ux-title) !important; font-size: 21px; line-height: 1.1; text-transform: uppercase; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.tribe-row .tribe-row-facts { font-size: 13px; font-weight: 700; color: var(--ux-muted); } +.tribe-row .tribe-row-desc { font-size: 14px; color: var(--ux-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.tribe-row .tribe-row-go { flex: 0 0 auto; font-size: 28px; line-height: 1; color: var(--ux-muted); } + +/* ---------- votaciones ---------- */ +.vote-bars { display: flex; flex-direction: column; gap: 10px; margin: 8px 0 4px; } +.vote-bar { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 4px 10px; } +.vote-bar-label { font-weight: 700; font-size: 15px; } +.vote-bar-count { font-weight: 700; font-size: 15px; min-width: 2ch; text-align: right; } +.vote-bar-meter { + grid-column: 1 / -1; width: 100%; height: 12px; appearance: none; -webkit-appearance: none; + border: 2px solid var(--ux-line); border-radius: 8px; overflow: hidden; background: var(--ux-soft); +} +.vote-bar-meter::-webkit-progress-bar { background: var(--ux-soft); } +.vote-bar-meter::-webkit-progress-value { background: var(--ux-fg); } +.vote-bar-meter::-moz-progress-bar { background: var(--ux-fg); } +.vote-bar-yes .vote-bar-meter::-webkit-progress-value, .vote-bar-green .vote-bar-meter::-webkit-progress-value { background: var(--ux-ok); } +.vote-bar-no .vote-bar-meter::-webkit-progress-value, .vote-bar-red .vote-bar-meter::-webkit-progress-value { background: var(--ux-danger); } +.vote-bar-yellow .vote-bar-meter::-webkit-progress-value { background: var(--ux-warn); } +.vote-own-note { + padding: 12px 14px !important; border-left: 4px solid var(--ux-accent) !important; + background: var(--ux-soft) !important; border-radius: 0 10px 10px 0; font-size: 15px !important; +} +/* botones de votar: rejilla de dos columnas, cada opcion un boton grande */ +.main-column .vote-buttons-row-single { display: grid !important; grid-template-columns: 1fr 1fr; gap: 10px !important; } +.main-column .vote-buttons-row-single form { margin: 0 !important; width: 100% !important; } +.main-column .vote-buttons-row-single button { + width: 100% !important; min-height: 52px !important; margin: 0 !important; + font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 16px !important; +} +.main-column .vote-buttons-row-single button[value="YES"], .main-column .vote-buttons-row-single button[value="GREEN"] { border-color: var(--ux-ok) !important; box-shadow: 0 4px 0 var(--ux-ok) !important; } +.main-column .vote-buttons-row-single button[value="NO"], .main-column .vote-buttons-row-single button[value="RED"] { border-color: var(--ux-danger) !important; box-shadow: 0 4px 0 var(--ux-danger) !important; } +.main-column .vote-buttons-row-single button[value="YELLOW"] { border-color: var(--ux-warn) !important; box-shadow: 0 4px 0 var(--ux-warn) !important; } +/* tipo de votacion: tarjetas de radio */ +.vote-kind { display: flex; flex-direction: column; gap: 10px; margin: 4px 0 18px; } +.vote-kind-title { font-weight: 700; font-size: 15px; } +.vote-kind-option { + display: grid !important; grid-template-columns: auto 1fr; gap: 2px 12px; align-items: center; + padding: 12px 14px; border: 2px solid var(--ux-line); border-radius: var(--ux-radius); cursor: pointer; +} +.main-column .vote-kind-option input[type="radio"] { grid-row: 1 / span 2; width: 22px !important; height: 22px !important; min-height: 0 !important; margin: 0 !important; accent-color: var(--ux-fg); } +.vote-kind-option:has(input:checked) { background: var(--ux-accent); color: var(--ux-on-accent); } +.vote-kind-option:has(input:checked) .vote-kind-hint { color: var(--ux-on-accent); } +.vote-kind-name { font-weight: 700; font-size: 17px; } +.vote-kind-hint { font-size: 14px; color: var(--ux-muted); } + +/* ---------- modulos: interruptores agrupados ---------- */ +.module-switches { display: flex; flex-direction: column; gap: 22px; padding-bottom: 8px; } +.module-group { display: flex; flex-direction: column; gap: 8px; } +.module-group-title { + font-family: var(--ux-title) !important; font-weight: 400 !important; font-size: 20px !important; + text-transform: uppercase; margin: 0 0 4px !important; padding-bottom: 4px; border-bottom: 3px solid var(--ux-line); +} +.main-column label.module-switch { + display: grid !important; grid-template-columns: 40px minmax(0, 1fr) 52px; align-items: center; gap: 12px; + padding: 10px 12px; min-height: 64px; cursor: pointer; + border: 2px solid var(--ux-line); border-radius: var(--ux-radius); + font-family: var(--ux-text) !important; text-transform: none !important; letter-spacing: 0 !important; +} +.module-switch-ico { width: 40px; height: 40px; padding: 8px; border-radius: 10px; background: var(--ux-soft); color: var(--ux-fg); } +.module-switch-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.module-switch-name { font-weight: 700; font-size: 17px; } +.module-switch-desc { font-size: 13px; color: var(--ux-muted); line-height: 1.3; } +.module-switch:has(.switch-input:checked) { border-color: var(--ux-fg); } +/* el interruptor: la casilla de siempre con forma de pastilla */ +.main-column label.module-switch input.switch-input[type="checkbox"] { + appearance: none; -webkit-appearance: none; flex: 0 0 auto; + width: 52px !important; height: 30px !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important; + border: 2px solid var(--ux-line) !important; border-radius: 16px !important; background: var(--ux-soft) !important; + position: relative; cursor: pointer; transition: background .15s ease-out; +} +.main-column label.module-switch input.switch-input[type="checkbox"]::after { + content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; + background: var(--ux-muted); transition: transform .15s ease-out, background .15s ease-out; +} +.main-column label.module-switch input.switch-input[type="checkbox"]:checked { background: var(--ux-accent) !important; } +.main-column label.module-switch input.switch-input[type="checkbox"]:checked::after { transform: translateX(22px); background: var(--ux-on-accent); } +/* Guardar siempre a la vista, encima de la barra de abajo */ +.module-save-bar { + position: sticky; bottom: calc(76px + env(safe-area-inset-bottom, 0px)); z-index: 5; + padding: 10px 0; background: var(--ux-bg); +} +.module-save-bar .create-button { width: 100% !important; margin: 0 !important; } +.module-saved { + padding: 10px 14px !important; margin: 0 0 14px !important; font-weight: 700; + border-left: 4px solid var(--ux-ok) !important; background: var(--ux-soft) !important; border-radius: 0 10px 10px 0; +} +/* la seccion apagada a la que se queria entrar */ +.main-column div.module-blocked { + display: grid; grid-template-columns: 48px 1fr; gap: 12px; align-items: center; + padding: 14px; margin: 0 0 20px; border: 3px solid var(--ux-line); border-radius: var(--ux-radius); background: var(--ux-soft); +} +.module-blocked-ico { width: 48px; height: 48px; padding: 10px; border-radius: 12px; background: var(--ux-bg); } +.module-blocked-text { margin: 0 !important; font-weight: 700; font-size: 17px; } +.module-blocked form { grid-column: 1 / -1; margin: 0 !important; } +.module-blocked .create-button { width: 100% !important; margin: 0 !important; } + +/* ---------- Ajustes: idiomas como fichas de un toque ---------- */ +.lang-pick { display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; } +.main-column .lang-pick .lang-chip, .lang-pick .lang-chip { + width: 100% !important; min-height: 48px !important; margin: 0 !important; padding: 6px 10px !important; + font-size: 16px !important; text-transform: none !important; box-shadow: none !important; + border: 2px solid var(--ux-line) !important; background: var(--ux-bg) !important; color: var(--ux-fg) !important; +} +.main-column .lang-pick .lang-chip.is-current, .lang-pick .lang-chip.is-current { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; } + +/* ---------- foco visible para teclado y lectores ---------- */ +a:focus-visible, button:focus-visible, summary:focus-visible, label.module-switch:focus-within { + outline: 3px solid var(--ux-accent) !important; outline-offset: 2px; +} + +@media (prefers-reduced-motion: reduce) { + .main-column label.module-switch input.switch-input[type="checkbox"], .main-column label.module-switch input.switch-input[type="checkbox"]::after { transition: none; } +} + +/* ---------- buscador de cada modulo: campo y lupa en la misma linea ---------- */ +/* solo cuando el unico control es el boton: los de Transferencias llevan rangos y + desplegables y siguen en columna */ +.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) { + display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; + align-items: stretch !important; gap: 8px !important; width: 100% !important; margin: 0 0 12px !important; +} +.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) .filter-box__input { + flex: 1 1 auto !important; min-width: 0 !important; width: 0 !important; height: 48px !important; margin: 0 !important; +} +.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) .filter-box__controls { + flex: 0 0 auto !important; width: auto !important; margin: 0 !important; +} +.main-column form.filter-box .filter-box__button { min-height: 48px !important; margin: 0 !important; } +/* el recuento (TOTAL: 12 · PUBLICA: 12) como una linea discreta, no un bloque */ +.main-column .activity-toolbar-row { display: flex !important; flex-direction: column !important; align-items: stretch !important; gap: 6px !important; } +.main-column [class*="module-stats"] { font-size: 13px !important; color: var(--ux-muted) !important; text-align: left !important; margin: 0 !important; } + +/* el boton de crear de cada modulo a todo lo ancho, tambien cuando va solo en su + formulario (en Votaciones salia estrecho y en Tribus entero) */ +.main-column .filters > form:has(.create-button), .main-column .mode-buttons > form:has(.create-button) { width: 100% !important; } + +/* jerga fuera del movil: la tasa ECO (microgramos de CO2) no se entendia y su ficha + salia ilegible; sigue en Estadisticas */ +.main-column .eco-tax-chip, .main-column [class*="eco-tax-chip"] { display: none !important; } +/* etiquetas (#asamblea) sin caja negra detras */ +.main-column .card-tags, .main-column .tribe-side .card-tags { background: transparent !important; padding: 0 !important; margin: 8px 0 !important; } + +/* acciones secundarias de una ficha (actualizar, PDF, compartir): una rejilla de + dos columnas, sin los huecos de 80px que dejaban los formularios sueltos */ +.main-column :is(.tribe-side-actions, .doc-export-actions) { + display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px !important; margin: 12px 0 !important; padding: 0 !important; +} +.main-column :is(.tribe-side-actions, .doc-export-actions) form { margin: 0 !important; width: 100% !important; padding: 0 !important; } +.main-column :is(.tribe-side-actions, .doc-export-actions) :is(button, .filter-btn, a) { + width: 100% !important; min-height: 44px !important; margin: 0 !important; padding: 6px 8px !important; + font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 14px !important; box-shadow: none !important; +} +.main-column .tribe-side-actions + .doc-export-actions { margin-top: 0 !important; } +/* resultado de la votacion: ficha de color legible en los dos skins */ +.main-column .vote-outcome { + display: inline-block; padding: 2px 10px !important; border-radius: 8px; + font-family: var(--ux-text) !important; font-size: 15px !important; font-weight: 700 !important; + background: var(--ux-soft) !important; color: var(--ux-fg) !important; vertical-align: middle; +} +.main-column .vote-outcome-yes { background: var(--ux-ok) !important; color: #fff !important; } +.main-column .vote-outcome-no { background: var(--ux-danger) !important; color: #fff !important; } +.main-column .job-section-title:has(.vote-outcome) { display: flex !important; align-items: center; gap: 8px; flex-wrap: wrap; } + +/* ---------- margenes: uno solo, de 14px ---------- */ +/* se sumaban capa a capa (contenido 20 + columna 10 + ficha 24 + lateral 20 = 74px + por lado) y el texto de una votacion o una tribu se quedaba en 264 de 412px */ +body .main-content { padding-left: 14px !important; padding-right: 14px !important; } +body .main-content > .main-column { padding-left: 0 !important; padding-right: 0 !important; } +.main-column :is(.tribe-details, .tribe-side, .tribe-main, .job-section, .vote-form, .tribe-content) { + padding-left: 0 !important; padding-right: 0 !important; margin-left: 0 !important; margin-right: 0 !important; + width: 100% !important; max-width: 100% !important; box-sizing: border-box; +} +.main-column .tribe-details { display: flex !important; flex-direction: column !important; gap: 18px; } +/* dentro de una ficha, un solo ritmo: 10px entre bloques (antes hueco de 16 + los + margenes de cada bloque + 20 de relleno, unos 70px entre secciones) */ +.main-column .tribe-side { gap: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; } +.main-column .tribe-side > *, .main-column .tribe-main > * { margin-top: 0 !important; margin-bottom: 0 !important; } +.main-column .tribe-main { display: flex !important; flex-direction: column; gap: 14px !important; } +.main-column .tribe-card-members { text-align: left !important; padding: 0 !important; font-weight: 700; } +.main-column :is(.tribe-side-actions, .doc-export-actions) > :only-child { grid-column: 1 / -1; } +/* la imagen generica de tribu no aporta nada y ocupaba 300px: solo se ve la propia */ +.main-column img[src*="default-tribe"] { display: none !important; } +/* etiquetas del lanzador largas (Configuracion): se parten con guion */ +.ln-lbl { hyphens: auto; -webkit-hyphens: auto; font-size: 12.5px !important; } + +/* ---------- secciones de una tribu: tira de fichas deslizable + "Mas" ---------- */ +.main-column .tribe-tabs { margin: 4px 0 12px !important; } +.tribe-tabs-strip { + display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; + margin: 0; padding: 4px 0 8px; scroll-snap-type: x proximity; +} +.tribe-tabs-strip::-webkit-scrollbar { display: none; } +.main-column a.tribe-tab, .tribe-tabs-more > summary { + flex: 0 0 auto; scroll-snap-align: start; display: inline-flex; align-items: center; justify-content: center; + min-height: 40px; padding: 4px 14px; border: 2px solid var(--ux-line) !important; border-radius: 20px !important; + background: var(--ux-bg) !important; color: var(--ux-fg) !important; text-decoration: none !important; + font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 14px !important; white-space: nowrap; + text-transform: uppercase; letter-spacing: .02em; +} +.main-column a.tribe-tab.is-current { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; } +.tribe-tabs-more > summary { list-style: none; cursor: pointer; margin-top: 2px; } +.tribe-tabs-more > summary::-webkit-details-marker { display: none; } +.tribe-tabs-more > summary::after { content: " ▾"; } +.tribe-tabs-more[open] > summary::after { content: " ▴"; } +.tribe-tabs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; } +.tribe-tabs-grid .tribe-tab { width: 100%; } +/* filas etiqueta / valor de las fichas (plazo, estado, creado el): en dos columnas */ +.main-column table.tribe-info-table tr { display: grid !important; grid-template-columns: minmax(96px, 38%) 1fr; align-items: center; } +.main-column table.tribe-info-table td { display: block !important; border: 0 !important; padding: 8px 10px !important; min-width: 0; overflow: hidden; text-overflow: ellipsis; } +.main-column table.tribe-info-table tr + tr { border-top: 1px solid #ccc !important; } + +/* bienvenida: "Ahora no" al final, como enlace discreto */ +.main-column .welcome-skip-end { text-align: center; margin: 8px 0 0 !important; } +.main-column .welcome-skip-end .welcome-skip-btn { + background: transparent !important; border: 0 !important; box-shadow: none !important; + font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important; + text-transform: none !important; text-decoration: underline; color: var(--ux-muted) !important; width: auto !important; +} +/* buzon y otros: los botones de filtro sueltos (Mas recientes, Marcar como leido) + en la letra de texto, como el resto de fichas */ +.main-column [class*="inbox"] .filter-btn:not(.create-button), +.main-column form[action^="/inbox"] button:not(.create-button) { + font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important; letter-spacing: .02em !important; +} + +/* ---------- regla general de letra para botones ---------- */ +/* todo boton en letra del sistema; Anton solo en el principal de cada pantalla + (el negro, .create-button) y en las tarjetas de aspecto */ +.main-column :is(button, input[type="submit"], input[type="button"], .filter-btn, a.filter-btn):not(.create-button):not(.welcome-banner-start):not(.skin-card):not(.ux-pick-card):not(.btn-singleview) { + font-family: var(--ux-text) !important; + font-weight: 700 !important; + letter-spacing: .02em !important; +} +.main-column form.welcome-skip-end { display: flex !important; justify-content: center !important; } +/* la barra de confirmar borrado con su marco rojo (Acampada quita el borde a los div) */ +.main-column div.confirm-bar { border: 2px solid var(--ux-danger) !important; } +.main-column .content-action-confirm[open] > summary { opacity: 1 !important; } + +/* ---------- boton principal de cada modulo: el mismo en todos ---------- */ +/* en Chats y Pads salia entero, en Foros estrecho y en Feed con otra letra (lleva + tambien .filter-btn y le tocaban las reglas de las fichas) */ +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filters, .forum-create-col) :is(form, div):has(> .create-button), +.main-column .forum-create-col { width: 100% !important; flex: 1 1 100% !important; } +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filters, .forum-create-col) .create-button.create-button { + width: 100% !important; min-height: 52px !important; margin: 4px 0 0 !important; padding: 8px 16px !important; + font-family: var(--ux-title) !important; font-weight: 400 !important; font-size: 20px !important; + letter-spacing: .02em !important; text-transform: uppercase; +} + +/* ---------- estados vacios: icono, texto centrado, sin cajas ---------- */ +.main-column :is(.no-content, .no-results, .no-content-box), +.main-column .tribe-grid > p:only-child { + display: flex !important; flex-direction: column; align-items: center; gap: 12px; + margin: 24px 0 !important; padding: 28px 16px !important; text-align: center !important; + border: 2px dashed var(--ux-muted) !important; border-radius: var(--ux-radius) !important; + color: var(--ux-muted) !important; font-size: 16px !important; font-weight: 700; background: transparent !important; +} +.main-column :is(.no-content, .no-results, .no-content-box)::before, +.main-column .tribe-grid > p:only-child::before { + content: ""; width: 44px; height: 44px; background: var(--ux-muted); + -webkit-mask: url("../images/ux-empty.svg") center / contain no-repeat; mask: url("../images/ux-empty.svg") center / contain no-repeat; +} +.main-column .no-content-box p { margin: 0 !important; color: inherit !important; } + +/* ---------- el pie abajo del todo aunque la pagina sea corta ---------- */ +body .main-content { min-height: calc(100dvh - 300px); } +.main-column .tribe-grid:has(> p:only-child), .main-column .filters, .main-column .tribe-mode-buttons { padding-left: 0 !important; padding-right: 0 !important; margin-left: 0 !important; margin-right: 0 !important; } +.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filters, .forum-create-col) .create-button.create-button { box-shadow: 0 5px 0 var(--ux-accent) !important; } + +/* ---------- buzon: cada mensaje en pocas lineas ---------- */ +/* era una tabla de seis filas (De, Fecha, Para, Asunto, Tasa ECO, Cifrado) dentro de + dos cajas: un aviso ocupaba una pantalla. Para es siempre una misma, el asunto ya + es el titulo, la tasa ECO no se entiende y el cifrado es igual en todos */ +.main-column .pm-thread { border: 0 !important; padding: 0 !important; margin: 0 0 10px !important; background: transparent !important; } +.main-column div.pm-card { padding: 12px !important; margin: 0 !important; border: 2px solid var(--ux-line) !important; border-radius: var(--ux-radius) !important; } +.main-column .pm-card .pm-info-table { display: flex !important; flex-wrap: wrap; align-items: center; gap: 4px 10px; border: 0 !important; margin: 0 0 6px !important; width: 100%; } +.main-column .pm-card .pm-info-table tbody { display: contents !important; } +.main-column .pm-card .pm-info-table tr { display: none !important; } +.main-column .pm-card .pm-info-table tr:nth-child(1), .main-column .pm-card .pm-info-table tr:nth-child(2), +.main-column .pm-card .pm-info-table tr:has(.pm-unread-chip) { display: flex !important; align-items: center; border: 0 !important; } +.main-column .pm-card .pm-info-table td { display: block !important; padding: 0 !important; border: 0 !important; background: transparent !important; } +.main-column .pm-card .pm-info-table td.card-label { display: none !important; } +.main-column .pm-card .pm-info-table tr:nth-child(1) { flex: 1 1 60%; min-width: 0; } +.main-column .pm-card .pm-info-table tr:nth-child(1) a { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px !important; } +.main-column .pm-card .pm-info-table tr:nth-child(2) td { font-size: 13px !important; color: var(--ux-muted) !important; } +.main-column .pm-card .pm-subject-cell > span:not(.pm-unread-chip) { display: none !important; } +.main-column .pm-card h2.pm-title { font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 17px !important; text-transform: none !important; line-height: 1.3 !important; margin: 0 0 6px !important; } +.main-column .pm-card .message-text { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; font-size: 15px; } + +/* ---------- busqueda: primero el campo de buscar, los filtros despues ---------- */ +.main-column .search-filters-row { display: flex !important; flex-direction: column-reverse !important; gap: 12px; } +.main-column table.search-filters-table, .main-column table.search-filters-table tbody { display: block !important; border: 0 !important; width: 100% !important; } +.main-column table.search-filters-table tr { display: block !important; border: 0 !important; margin: 0 0 10px; } +.main-column table.search-filters-table td { display: block !important; padding: 0 !important; border: 0 !important; background: transparent !important; } +.main-column table.search-filters-table td.card-label { margin-bottom: 4px; } +.main-column .search-submit-row button { width: 100% !important; } + +/* ---------- nodos: acciones en rejilla ---------- */ +.main-column .peers-conn-actions.filter-box { display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px !important; } +.main-column .peers-conn-actions form { margin: 0 !important; width: 100% !important; } +.main-column .peers-conn-actions button { width: 100% !important; margin: 0 !important; font-size: 15px !important; } +.main-column .message-list { padding: 0 !important; } +.main-column .pm-card .pm-info-table tr { width: auto !important; flex: 0 1 auto; } +.main-column .pm-card .pm-info-table tr:nth-child(1) { flex: 1 1 0 !important; } +.main-column .pm-card .pm-info-table tr:has(.pm-unread-chip) { order: 3; flex-basis: 100% !important; } +.main-column .pm-card .pm-actions { display: inline-flex !important; width: auto !important; margin: 8px 8px 0 0 !important; } +.main-column .pm-card .pm-actions button { min-height: 40px !important; padding: 4px 14px !important; font-size: 14px !important; box-shadow: none !important; width: auto !important; } +.main-column table.search-filters-table { height: auto !important; min-height: 0 !important; margin: 0 !important; } +.main-column .peers-conn-actions > form:last-child:nth-child(odd) { grid-column: 1 / -1; } +.main-column .search-filters-row { height: auto !important; } +.main-column .search-filters-row > table.search-filters-table { flex: 0 0 auto !important; } +.main-column .pm-card div.pm-actions.pm-actions { width: auto !important; max-width: max-content; display: inline-flex !important; vertical-align: top; } +.main-column .pm-card .pm-info-table tr:nth-child(1) td, .main-column .pm-card .pm-info-table tr:nth-child(1) a { white-space: nowrap !important; overflow: hidden; text-overflow: ellipsis; min-width: 0; max-width: 100%; text-align: left !important; } + +/* ---------- el interruptor Abierto / No molestar del Telefono del dev ---------- */ +/* salia centrado en una fila propia arriba de cada pagina (unos 60px); va en la + barra de arriba, junto al avatar */ +form.oasis-phone-state { + position: fixed !important; top: calc(env(safe-area-inset-top, 0px) + 14px); right: 62px; z-index: 101; /* encima de la barra de arriba (100), debajo de la hoja del menu (1000) */ + margin: 0 !important; padding: 0 !important; width: auto !important; +} +form.oasis-phone-state .oasis-phone-state-btn { + width: 40px !important; height: 40px !important; min-height: 40px !important; padding: 0 !important; margin: 0 !important; + display: inline-flex !important; align-items: center; justify-content: center; + border-radius: 50% !important; box-shadow: none !important; font-size: 20px !important; +} +.oasis-mobile-topbar .omt-menus { padding-right: 46px; } + +/* barras de la votacion: juntas, y las etiquetas del dev (CONFUNDIDO, SEGUIR + MAYORIA) en el mismo tipo que las nuestras (Si, No) */ +.main-column div.vote-bar { margin: 0 !important; padding: 0 !important; min-height: 0 !important; gap: 4px 10px !important; } +.main-column .vote-bar > * { margin: 0 !important; } +.main-column .vote-bar-meter { height: 12px !important; min-height: 0 !important; } +.main-column .vote-bar-label { text-transform: lowercase; } +.main-column .vote-bar-label::first-letter { text-transform: uppercase; } + +/* personalizar la barra de abajo: una fila por atajo, como el resto de listas */ +.main-column .bb-manage-list, .main-column .bb-pick-grid { display: flex !important; flex-direction: column !important; gap: 8px !important; align-items: stretch !important; } +.main-column .bb-manage-list form, .main-column .bb-pick-grid form { margin: 0 !important; width: 100% !important; } +.main-column button.bb-manage-btn, .main-column button.bb-pick-btn { + width: 100% !important; min-height: 52px !important; margin: 0 !important; padding: 8px 14px !important; + display: flex !important; align-items: center; gap: 12px; justify-content: flex-start !important; + font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 16px !important; + text-transform: none !important; letter-spacing: 0 !important; box-shadow: none !important; +} +.main-column .bb-manage-btn .bb-manage-lbl, .main-column .bb-pick-btn .bb-pick-lbl { flex: 1 1 auto; text-align: left; } +.main-column .bb-manage-btn .bb-manage-x { color: var(--ux-danger) !important; font-size: 18px; } +.main-column .bb-manage h3, .main-column .bb-manage-add h3 { font-size: 20px !important; margin: 18px 0 10px !important; } +.main-column a.bb-manage-done { display: block !important; width: 100% !important; margin-top: 18px !important; text-align: center; } +.main-column div.bb-manage, .main-column div.bb-manage-add, .main-column div.bb-manage-list, .main-column div.bb-pick-grid { padding: 0 !important; margin: 0 !important; border: 0 !important; } + +/* el aviso de bienvenida, encima del lanzador: debajo quedaba a 1.300px y quien + abria la app por primera vez no lo veia */ +body > .welcome-banner { order: -1; margin-top: 10px !important; } + +/* ---------- dentro de una sala: los controles a mano ---------- */ +/* silenciar, camara, levantar la mano, salir... quedaban debajo de toda la ficha de + la sala (a 1.100px). Estando dentro, el panel en directo va primero y los + controles en rejilla de dos */ +.main-column .tribe-details:has(.room-live-panel) > .tribe-main { order: -1; } +.main-column div.room-live-controls { display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px !important; } +.main-column .room-live-controls form { margin: 0 !important; width: 100% !important; } +.main-column .room-live-controls button { + width: 100% !important; min-height: 48px !important; margin: 0 !important; padding: 6px 8px !important; + font-size: 15px !important; box-shadow: none !important; +} +.main-column .room-live-controls form[action="/rooms/leave"] { grid-column: 1 / -1; } +.main-column .room-live-controls form[action="/rooms/leave"] button { background: var(--ux-danger) !important; color: #fff !important; border-color: var(--ux-danger) !important; } +.main-column .room-live-controls :is(label, a.tribe-action-btn, .room-stream-form label) { + display: flex !important; align-items: center; justify-content: center; width: 100% !important; min-height: 48px !important; + margin: 0 !important; padding: 6px 8px !important; box-sizing: border-box; + border: var(--a26-bd-fino, 2px solid var(--ux-line)) !important; border-radius: var(--ux-radius) !important; + background: var(--ux-bg) !important; color: var(--ux-fg) !important; box-shadow: none !important; + font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important; text-transform: uppercase; text-decoration: none !important; +} + +/* ---------- pestaña Menu y su hoja con todos los modulos ---------- */ +/* la portada es la actividad; los modulos se abren desde abajo, como el "Mas" de + Android (docs/ESTUDIO_NAVEGACION.md). Una casilla oculta abre y cierra la hoja */ +.oasis-bottombar input.menu-toggle { position: absolute; opacity: 0; width: 0 !important; height: 0 !important; min-height: 0 !important; pointer-events: none; } +.oasis-bottombar label.bb-menu { cursor: pointer; } +.oasis-bottombar .menu-toggle:checked ~ label.bb-menu .bb-ico.has-svg { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; } +.oasis-bottombar .menu-sheet, .oasis-bottombar .menu-sheet-backdrop { display: none; } +.oasis-bottombar .menu-toggle:checked ~ .menu-sheet-backdrop { + display: block; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1002; background: rgba(0, 0, 0, .45); +} +.oasis-bottombar .menu-toggle:checked ~ .menu-sheet { + display: block; position: fixed; left: 0; right: 0; z-index: 1003; + bottom: 0; max-height: 82vh; max-height: 82dvh; overflow-y: auto; overscroll-behavior: contain; + padding: 8px 14px calc(18px + env(safe-area-inset-bottom, 0px)); + background: var(--ux-bg); color: var(--ux-fg); + border-top: 3px solid var(--ux-line); border-radius: 22px 22px 0 0; + box-shadow: 0 -6px 24px rgba(0, 0, 0, .25); +} +.menu-sheet-head { + position: sticky; top: -8px; z-index: 1; display: flex; align-items: center; justify-content: space-between; + margin: 0 -14px 8px; padding: 12px 14px 10px; background: var(--ux-bg); border-bottom: 2px solid var(--ux-line); +} +.menu-sheet-head::before { + content: ""; position: absolute; top: 4px; left: 50%; width: 44px; height: 5px; margin-left: -22px; + border-radius: 3px; background: var(--ux-muted); +} +.menu-sheet-title { font-family: var(--ux-title); font-size: 26px; text-transform: uppercase; line-height: 1; } +.menu-sheet-close { + display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; cursor: pointer; + border: 2px solid var(--ux-line); border-radius: 50%; font-size: 20px; background: var(--ux-bg); +} +.menu-sheet .launcher { margin: 0 !important; padding: 0 !important; } +/* cinco pestañas: que quepan a 360px */ +.oasis-bottombar .bb-item { flex: 1 1 0 !important; min-width: 0 !important; } +@media (max-width: 380px) { .oasis-bottombar .bb-lbl { font-size: 11px !important; } } +.oasis-bottombar .menu-toggle:checked ~ div.menu-sheet { background: var(--ux-bg) !important; bottom: calc(66px + env(safe-area-inset-bottom, 0px)); border-radius: 22px 22px 0 0 !important; border: 0 !important; border-top: 3px solid var(--ux-line) !important; } +.oasis-bottombar div.menu-sheet-head { background: var(--ux-bg) !important; border: 0 !important; border-bottom: 2px solid var(--ux-line) !important; } +.oasis-bottombar .menu-toggle:checked ~ .menu-sheet-backdrop { bottom: calc(66px + env(safe-area-inset-bottom, 0px)); } +/* en la hoja todo tiene que caber sin deslizar: cinco iconos por fila y mas pequeños + (con 20 modulos la rejilla del lanzador medía 1.200px para 690 de hoja) */ +.menu-sheet .launcher .ln-grid { display: grid !important; grid-template-columns: repeat(5, 1fr) !important; gap: 8px 2px !important; } +.menu-sheet .launcher .ln-app { min-width: 0 !important; padding: 0 !important; gap: 4px !important; } +.menu-sheet .launcher .ln-ico.has-svg { width: 48px !important; height: 48px !important; padding: 11px !important; box-shadow: 0 3px 0 var(--ux-line) !important; } +.menu-sheet .launcher .ln-lbl { font-size: 11px !important; line-height: 1.1 !important; } +.menu-sheet .launcher .ln-title { font-size: 15px !important; margin: 0 0 6px !important; padding: 0 0 2px !important; border-bottom-width: 2px !important; } +.menu-sheet .launcher .ln-group + .ln-group { margin-top: 10px !important; } +/* con el menu abierto, solo Menu lleva la pastilla */ +.oasis-bottombar .menu-toggle:checked ~ a.bb-item.current .bb-ico.has-svg { background: transparent !important; color: var(--ux-fg) !important; } +/* los grupos de uno o dos modulos (Biblioteca, Oficina, Gobernanza...) van de dos en + dos, uno al lado del otro; los grandes ocupan el ancho. Hace falta :has(): en los + WebView viejos (Android 9-11 sin actualizar) cada grupo va a lo ancho */ +.menu-sheet .launcher .ln-group, .menu-sheet .launcher .ln-group + .ln-group { margin: 0 0 10px !important; min-width: 0; } +@supports selector(:has(a)) { + .menu-sheet nav.launcher { display: grid !important; grid-template-columns: 1fr 1fr; column-gap: 14px; row-gap: 10px; } + .menu-sheet .launcher .ln-group, .menu-sheet .launcher .ln-group + .ln-group { margin: 0 !important; } + .menu-sheet .launcher .ln-group:has(.ln-app:nth-child(3)) { grid-column: 1 / -1; } + .menu-sheet .launcher .ln-group:not(:has(.ln-app:nth-child(3))) .ln-grid { grid-template-columns: 1fr 1fr !important; } +} +/* con el menu abierto, el boton de No molestar no se monta encima de la hoja */ +body:has(#oasis-menu-toggle:checked) form.oasis-phone-state { display: none !important; } + +/* ---------- tipo de portada: un toque y guardado ---------- */ +/* bienvenida y Ajustes: cada opcion es un boton. En el movil, una fila por opcion con + la miniatura a la izquierda (en rejilla de tres, "Conversaciones" se partia a 360px); + en tableta, rejilla de tres */ +.main-column form.ux-pick { margin: 0 !important; padding: 0 !important; border: 0 !important; background: transparent !important; box-shadow: none !important; } +.main-column .ux-pick-hint { margin: 0 0 10px !important; color: var(--ux-muted); font-size: 14px; } +.main-column .ux-pick-grid { display: block !important; border: 0 !important; background: transparent !important; padding: 0 !important; } +.main-column button.ux-pick-card { + display: flex !important; flex-direction: row; align-items: center; text-align: left; + width: 100% !important; min-width: 0 !important; min-height: 64px !important; height: auto !important; margin: 0 0 10px !important; padding: 8px 12px 8px 8px !important; + background: var(--ux-bg) !important; color: var(--ux-fg) !important; border: 2px solid var(--ux-line) !important; border-radius: 14px !important; + box-shadow: 0 3px 0 var(--ux-line) !important; text-transform: none !important; font-family: inherit !important; cursor: pointer; +} +.main-column button.ux-pick-card.is-current { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; } +.main-column .ux-pick-shot { display: block; flex: 0 0 76px; width: 76px; height: auto; border-radius: 8px; margin: 0 12px 0 0; border: 1px solid rgba(0, 0, 0, .2); } +.main-column .ux-pick-label { flex: 1 1 auto; min-width: 0; text-align: left; font-size: 16px; font-weight: 700; line-height: 1.2; } +.main-column .ux-pick-now { flex: 0 0 auto; margin-left: 8px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; } +/* perfil de uso: sin miniatura, nombre y una linea de para que sirve */ +.main-column .ux-pick-text { flex: 1 1 auto; min-width: 0; display: block; text-align: left; } +.main-column .ux-pick-text .ux-pick-label { display: block; } +.main-column .ux-pick-sub { display: block; margin-top: 2px; font-size: 13px; font-weight: 400; line-height: 1.3; opacity: .8; } +.main-column .wf-pick button.ux-pick-card { padding: 10px 14px !important; } +.main-column .wf-pick .ux-pick-grid { display: block !important; } +@media (min-width: 700px) { + .main-column .ux-pick-grid { display: grid !important; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-gap: 12px; } + .main-column button.ux-pick-card { flex-direction: column; text-align: center; margin: 0 !important; padding: 10px !important; } + .main-column .ux-pick-shot { width: 100%; flex: 0 0 auto; margin: 0 0 8px; } + .main-column .ux-pick-now { margin: 4px 0 0; } +} +@media (min-width: 700px) { .main-column .wf-pick button.ux-pick-card { flex-direction: row; text-align: left; margin: 0 0 10px !important; } } + +/* ---------- tabletas y pantallas anchas ---------- */ +/* a 800dp todo se estiraba: lineas larguisimas y la hoja del menu con cinco iconos + repartidos en 800px. Contenido a 720px como mucho, centrado; la hoja, igual */ +@media (min-width: 700px) { + body .main-content { max-width: 720px !important; margin-left: auto !important; margin-right: auto !important; width: 100% !important; box-sizing: border-box; } + body > .welcome-banner { width: calc(100% - 28px) !important; max-width: 692px !important; margin-left: auto !important; margin-right: auto !important; box-sizing: border-box; } + .oasis-bottombar .menu-toggle:checked ~ div.menu-sheet { left: 50%; right: auto; width: 640px; margin-left: -320px; } +} + +/* ---------- WebView viejos ---------- */ +/* Android 9-11 sin actualizar traen WebView de 2018-2020: no saben gap en flex (las + fichas salian pegadas) ni :has() (el buscador partia el boton a otra linea). inset + llego a la vez que esas cosas, asi que sirve para reconocerlos */ +@supports not (inset: 0) { + .main-column .filter-btn, .main-column .activity-chip, .main-column .tribe-tab, .oasis-mobile-topbar .oasis-menu-btn { margin: 0 6px 8px 0 !important; } + .main-column form.filter-box { display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; } + .main-column form.filter-box .filter-box__input { flex: 1 1 60% !important; width: auto !important; margin: 0 8px 8px 0 !important; } + .main-column form.filter-box .filter-box__controls { flex: 0 0 auto !important; width: auto !important; } + .main-column .lang-pick .lang-chip, .main-column .vote-buttons-row-single form { margin-bottom: 8px !important; } + /* sticky dentro de un fixed se pintaba en blanco */ + .menu-sheet-head { position: relative !important; top: 0 !important; } +} diff --git a/src/client/assets/styles/style.css b/src/client/assets/styles/style.css index eca02f8..f3c8297 100644 --- a/src/client/assets/styles/style.css +++ b/src/client/assets/styles/style.css @@ -80,12 +80,6 @@ a:hover { .update-banner-icon { font-size: 1.15em; - animation: update-spin 2.5s linear infinite; -} - -@keyframes update-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } } .update-banner-text { @@ -400,6 +394,9 @@ nav ul li a:hover { min-width: 0; max-width: 100vw; } +.main-content > .sidebar-left { order: 1; } +.main-content > .main-column { order: 2; } +.main-content > .sidebar-right { order: 3; } .main-column pre, .main-column code { @@ -671,6 +668,49 @@ table th { background-color: #222; } +.main-column table th { + overflow-wrap: normal; + word-break: normal; + hyphens: auto; + white-space: normal; + line-height: 1.25; +} + +.main-column table td { + word-break: normal; + overflow-wrap: anywhere; +} + +.table-wrap { + max-width: 100%; + overflow-x: auto; +} + +.gov-overview { + table-layout: fixed; + width: 100%; +} + +.gov-overview th, +.gov-overview td { + padding: 8px 3px; + overflow-wrap: break-word; +} + +.gov-overview th { + font-size: 10.5px; + letter-spacing: 0; +} + +.gov-overview th:first-child, +.gov-overview td:first-child { + width: 15%; +} + +.gov-overview td:first-child { + font-size: 14px; +} + table tr:nth-child(even) { background-color: #444; } @@ -972,9 +1012,18 @@ article img { border-radius: 8px; margin: 8px 0; } +.trending-card .post-image { + width: auto; + max-height: 480px; + margin-left: auto; + margin-right: auto; +} .post-video { + width: 100%; max-width: 100%; + max-height: 480px; + height: auto; display: block; border-radius: 8px; margin: 8px 0; @@ -986,11 +1035,22 @@ article img { margin: 8px 0; } +.trending-card .post-image,.trending-card .post-video{margin-left:auto;margin-right:auto} +.trending-card .zoom-link:not(.zoom-inline){display:block;text-align:center} +.trending-card .zoom-link:not(.zoom-inline) img{display:inline-block} + .feed-container .post-image{max-width:100%;width:auto;max-height:480px;height:auto} .feed-container .post-video,.feed-container .video-container video{width:100%;max-width:720px;max-height:480px;height:auto} -.feed-container .post-audio,.feed-container .audio-container audio{width:100%;max-width:640px} +.feed-container .post-audio,.feed-container .audio-container audio{width:100%} +.feed-detail-card .post-image{width:auto;max-height:480px;margin-left:auto;margin-right:auto} +.feed-detail-card .post-video{max-height:480px;margin-left:auto;margin-right:auto} +.feed-detail-card .post-audio{width:100%} +.feed-detail-card .zoom-link:not(.zoom-inline){display:block;text-align:center} +.feed-detail-card .zoom-link:not(.zoom-inline) img{display:inline-block} .fediverse-network{border-radius:8px;padding:18px 22px;margin-top:10px;display:flex;flex-direction:column;gap:16px} +.fediverse-network > p,.fediverse-network form > p{margin:0} +.fediverse-network form{margin:0} .fediverse-network h3{margin:0} .fediverse-profile{display:flex;flex-direction:column;gap:24px} .fediverse-namerow{display:flex;align-items:center;gap:16px;flex-wrap:wrap} @@ -1019,6 +1079,13 @@ article img { .fediverse-profile,.fediverse-profile-id,.fediverse-namerow,.fediverse-stat,.fediverse-name,.fediverse-compact-actions,.fediverse-author,.fediverse-head,.fediverse-bio,.fediverse-stats,.fediverse-field,.fediverse-field-value,.fediverse-actions,.fediverse-compose,.fediverse-compose-actions,.fediverse-media,.fediverse-reach,.fediverse-preview-card,.fediverse-preview-head,.fediverse-disconnected{border:none !important;background:none !important;padding:0 !important;margin:0 !important;box-shadow:none !important;border-radius:0 !important} .fediverse-empty{text-align:center;padding:24px} .fediverse-error{color:#ff6b6b;margin:8px 0} +.telegram-unread-badge{background:#ffa300;color:#111;border-radius:10px;padding:1px 8px;font-size:.8em;font-weight:bold} +.telegram-preview{margin:0;opacity:.85} +.telegram-dialog .fediverse-avatar{width:44px;height:44px;border-radius:50%} +.telegram-message.telegram-out{border-left:3px solid #ffa300} +.telegram-text{white-space:pre-wrap} +.telegram-chat-title{display:flex;align-items:center;gap:10px;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;margin:0 0 10px 0} +.telegram-chat-title h3{margin:0} .fediverse-compose{display:flex;flex-direction:column;gap:8px;margin-bottom:18px} .fediverse-compose textarea{width:100%;box-sizing:border-box;resize:vertical} .fediverse-compose-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap} @@ -1133,16 +1200,82 @@ button.create-button:hover { .bookmark-url:hover { text-decoration: underline; } +.card-section.bookmark .bookmark-url{font-size:.95em;font-weight:600;margin:0;overflow-wrap:anywhere} .filter-box { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; padding: 16px 18px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(0, 0, 0, 0.18); } +.filter-box input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), +.filter-box select, +.filter-box button { + height: 42px !important; + box-sizing: border-box !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + margin: 0 !important; + vertical-align: middle; +} + +.filter-box div { + border: none !important; + background: transparent !important; + box-shadow: none !important; + padding: 0 !important; + margin: 0 !important; +} + +.filter-box__controls .filter-box__input, +.filter-box__controls input[type="text"], +.filter-box__controls input[type="number"], +.filter-box__controls input[type="date"], +.filter-box__controls input[type="datetime-local"], +.filter-box__controls select { + width: auto; + flex: 0 1 220px; + min-width: 140px; +} + +.filter-box .transfer-range, +.filter-box__controls .transfer-range { + display: flex; + gap: 8px; + flex: 1 1 100% !important; +} + +.filter-box select { + flex: 1 1 100% !important; + max-width: 340px; +} + +.filter-box .filter-box__button, +.filter-box button { + flex: 0 0 auto; +} + +.filter-box input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]), +.filter-box select { + box-sizing: border-box; + height: 42px; + padding: 10px 12px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.12); + color: #ffd700; + outline: none; +} + .filter-box__input { - width: 100%; + width: auto; + flex: 0 1 320px; + min-width: 160px; box-sizing: border-box; height: 42px; padding: 10px 12px; @@ -1161,15 +1294,16 @@ button.create-button:hover { display: flex; gap: 12px; align-items: center; - margin-top: 14px; + margin-top: 0; + flex: 0 1 auto; flex-wrap: wrap; } .filter-box__select { box-sizing: border-box; - height: 36px; + height: 42px; padding: 6px 10px; - border-radius: 6px; + border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.12); color: inherit; @@ -1177,14 +1311,26 @@ button.create-button:hover { } .filter-box__button { - height: 36px; - padding: 0 14px; - border-radius: 6px; + height: 42px; + padding: 0 18px; + border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.16); color: #ffd700; } +.filter-box__controls input:not([type="hidden"]), +.filter-box__controls select, +.filter-box__controls button { + height: 42px !important; + box-sizing: border-box !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + margin: 0 !important; + align-self: center !important; + line-height: normal; +} + .bookmark-topbar { display: flex; gap: 8px; @@ -1217,6 +1363,10 @@ button.create-button:hover { .error-page-message{margin:8px 0 16px;white-space:pre-wrap;word-break:break-word} .error-page-actions{margin-top:16px} +.inline-error-box{border:1px solid #d9534f !important;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap} +.inline-error-box .error-page-message{margin:0;color:#d9534f;font-weight:bold} +.inline-error-box .inline-error-dismiss{display:inline-flex;align-items:center;padding:4px 14px;border:1px solid #d9534f;border-radius:6px;background:transparent;color:#d9534f;font-weight:700;font-size:.9em;text-decoration:none;cursor:pointer} +.inline-error-box .inline-error-dismiss:hover{background:#d9534f;color:#1a1a1a} .tags-container { display: flex; @@ -1232,6 +1382,12 @@ button.create-button:hover { margin-right: 1em; margin-bottom: 0.5em; } +.tag-inline { + display: inline; + padding: 0; + margin: 0; + font-size: inherit; +} .tag-checkboxes-table { margin-top: 1rem; @@ -1266,14 +1422,36 @@ button.create-button:hover { width: 300px; flex: 0 0 300px; } +.inhabitants-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px} +@media (max-width: 900px){.inhabitants-grid{grid-template-columns:1fr}} +.tribe-main .inhabitants-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,260px),1fr))} +.inhabitants-grid > .inhabitants-card{margin:0;min-width:0} +.inhabitants-grid .inhabitants-card-body,.inhabitants-grid .inhabitant-card{padding:0 !important;margin:0 !important;border:0 !important;background:transparent !important;box-shadow:none !important} +.inhabitants-grid .inhabitant-card{flex-direction:column;align-items:stretch} +.inhabitants-grid .inhabitant-left{width:auto;flex:1 1 auto;min-width:0} +.inhabitants-grid .inhabitant-left > a:first-of-type{width:160px;height:160px} +.inhabitants-grid .inhabitant-photo-details{width:160px !important;height:160px !important} .inhabitant-left > a:first-of-type { margin-bottom: 0 !important; } .inhabitant-left > .status.you { margin: 0; } .inhabitant-left .profile-sensors-box { margin: 0; } .inhabitant-left .profile-sensors-box .profile-reach { margin: 0; } +.inhabitant-left .profile-reach { + margin: 0; + padding: 0 !important; + border: 0 !important; + background: transparent !important; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + width: auto; +} +.inhabitant-left .profile-reach .invite-qr-card { margin: 0; } .inhabitant-left > a:first-of-type { display: block; - width: 100%; + width: 256px; + height: 256px; } .inhabitant-left .profile-sensors-box a { display: inline; @@ -1294,6 +1472,7 @@ button.create-button:hover { margin: 0 auto; } +.inhabitant-left-name{margin:6px 0 0 0;text-align:center;font-weight:bold} .inhabitant-photo-details { width: 100% !important; height: 100% !important; @@ -1347,6 +1526,11 @@ button.create-button:hover { color: #e74c3c; } +.karma-line-main{display:block;line-height:1.25}.profile-sensors-box .karma-line.karma-line-stacked,.inhabitant-left .karma-line.karma-line-stacked,.inhabitant-karma-ubi .karma-line.karma-line-stacked,.karma-line.karma-line-stacked{display:flex;flex-direction:column;align-items:center;gap:1px}.karma-received-row{display:flex;justify-content:center;align-items:center;gap:14px;margin-top:0;line-height:1.25}.karma-received{display:inline-flex;align-items:center;gap:4px;font-weight:600;white-space:nowrap;line-height:1}.karma-arrow{font-size:.85em;line-height:1;display:inline-block}.karma-received-up{color:#2ecc71}.karma-received-down{color:#e74c3c}.karma-value-positive{color:#2ecc71 !important}.karma-value-neutral{color:#ffd700 !important}.karma-value-negative{color:#e74c3c !important} +.module-stats-line .stat-label-green{color:#2ecc71} +.module-stats-line .stat-label-orange{color:#f39c12} +.module-stats-line .stat-label-red{color:#e74c3c} + .profile-last-seen, .profile-device { display: block; @@ -1376,7 +1560,12 @@ button.create-button:hover { .activity-search-input{min-width:240px;max-width:520px;width:100%} .post-text-pre{white-space:pre-wrap} .activity-image-thumb{width:100%;max-width:720px;height:auto;border-radius:14px;object-fit:cover} -.activity-avatar{width:256px;height:256px;} +.activity-avatar{width:256px;height:256px;object-fit:cover} +.about-card-cols{display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.about-card-cols > div{background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.about-card-col-img{flex:0 0 auto} +.about-card-col-desc{flex:1 1 240px;min-width:200px} +.about-card-header{margin:0 0 8px 0} .activity-pub{display:flex;flex-direction:column;gap:10px;align-items:flex-start} .activity-pub-title{margin:0} .activity-pub-title .user-link{display:inline} @@ -1423,6 +1612,11 @@ button.create-button:hover { box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); } +.pdf-canvas-wrap { + height: 420px; + overflow: auto; +} + .pdf-viewer-container canvas { display: block; margin: 0 auto; @@ -1430,7 +1624,7 @@ button.create-button:hover { background-color: #fff; } -.pdf-viewer-container div { +.pdf-viewer-container .pdf-controls { margin-top: 1rem; } @@ -1473,11 +1667,6 @@ button.create-button:hover { padding: 20px; } -.no-cv { - padding: 20px; - text-align: center; -} - .checkbox-oasisContributor { display: table-cell; align-items: center; @@ -1510,6 +1699,21 @@ button.create-button:hover { } .opinions-voting-collapse { margin: 8px 0; } +.card-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:8px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;overflow:visible} +.card-meta-row .opinions-voting-collapse:not([open]){display:flex} +.card-meta-row .opinions-voting-collapse:not([open]) summary{display:inline-flex;align-items:center} +.card-meta-row .card-footer{display:flex;flex-wrap:wrap;align-items:center;gap:8px} +.card-footer .date-link, +.card-footer .user-link{display:inline-flex;align-items:center;box-sizing:border-box;height:34px;max-width:100%;min-width:0;margin:0;padding:0 14px;border:1px solid transparent;border-radius:5px;font-size:13px;line-height:32px;white-space:nowrap;flex:0 1 auto} +.card-footer .user-link{overflow:hidden;text-overflow:ellipsis} +.card-meta-row .opinions-voting-collapse{margin:0;width:auto;flex:0 1 auto;background:transparent;border:0;padding:0} +.card-meta-row .opinions-voting-collapse[open]{flex:1 1 100%;width:100%;margin-bottom:8px} +.card-meta-row .voting-buttons{margin:10px 0 0 0;padding:0} +.card-meta-row .voting-buttons form{margin:0;padding:0;background:transparent;border:0} +.card-meta-row .card-footer{margin:0 !important;padding:0 !important;border:0 !important;flex:1 1 auto;min-width:0} +.card-meta-row .opinions-summary{display:inline-flex !important;align-items:center;box-sizing:border-box;height:34px;padding:0 14px !important;border-radius:5px;font-size:13px;line-height:32px;flex:0 0 auto} +.card-meta-row .comments-summary-link{display:inline-flex;align-items:center;gap:4px;box-sizing:border-box;height:34px;padding:0 14px;border:1px solid rgba(128,128,128,.5);border-radius:5px;background:rgba(128,128,128,.12);font-size:13px;line-height:32px;text-decoration:none;flex:0 0 auto} +.card-meta-row .comments-summary-link:hover{background:rgba(128,128,128,.25);text-decoration:none} .opinions-summary { display: inline-block !important; width: auto !important; @@ -1518,18 +1722,59 @@ button.create-button:hover { cursor: pointer; font: inherit; padding: 10px 20px; - border: 1px solid #ff3300; + border: 1px solid rgba(128, 128, 128, .5) !important; border-radius: 5px; - background-color: #ff6600; - color: #fff; + background: rgba(128, 128, 128, .12) !important; transition: background-color .2s, border-color .2s; list-style: none; } -.opinions-summary:hover { background-color: #ff9900; border-color: #e68a00; color: #000; } +.opinions-summary:hover { background: rgba(128, 128, 128, .25) !important; } .opinions-summary::-webkit-details-marker { display: none; } .opinions-summary::after { content: " \25BE"; opacity: 0.85; } details[open] > .opinions-summary::after { content: " \25B4"; } +.comments-collapse { margin: 8px 0; } +.comments-summary { + display: inline-block !important; + width: auto !important; + max-width: max-content; + box-sizing: border-box; + cursor: pointer; + font: inherit; + padding: 10px 20px; + border: 1px solid rgba(128, 128, 128, .5); + border-radius: 5px; + background: rgba(128, 128, 128, .12) !important; + list-style: none; +} +.comments-summary:hover { background: rgba(128, 128, 128, .25) !important; } +.keep-reading-link { text-decoration: none; color: inherit; } +.keep-reading-link::after { content: ""; } +.comments-summary::-webkit-details-marker { display: none; } +.comments-summary::after { content: " \25BE"; opacity: 0.85; } +details[open] > .comments-summary::after { content: " \25B4"; } +.comments-summary-icon { margin-right: 6px; } + +.comments-link { + text-decoration: none; + text-align: center; +} + +.comments-link::after { content: ""; } + +.engage-on { + border-color: #e68a00 !important; + background: #ff9900 !important; + color: #000 !important; +} + +.engage-on:hover { + background: #ffb733 !important; + border-color: #ff9900 !important; + color: #000 !important; +} +.opinions-summary-icon { margin-right: 6px; } + .message-banner { margin: 12px 0; padding: 12px 16px; @@ -1539,27 +1784,6 @@ details[open] > .opinions-summary::after { content: " \25B4"; } } .message-banner p { margin: 0; } -.feed-search-form { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; -} - -.feed-search-input { - flex: 1 1 260px; - min-width: 220px; - height: 40px; - box-sizing: border-box; -} - -.feed-search-btn { - height: 40px; - display: inline-flex; - align-items: center; - box-sizing: border-box; -} - .type-label { font-weight: bold; margin-top: 0.5em; @@ -1592,39 +1816,12 @@ display:flex; gap:8px; margin-top:16px; margin-bottom: 8px; } -.tribe-feed { - background-color: #111; - padding: 16px; - border-radius: 8px; -} - -.tribe-feed-full { - width: 100%; - background-color: #222; - padding: 16px; - border-radius: 8px; - box-sizing: border-box; -} - -.feed-actions { - display: flex; - gap: 8px; - margin-bottom: 16px; -} - .vote-actions { display: flex; gap: 8px; margin-bottom: 16px; } -.feed-list { - display: flex; - flex-direction: column; - gap: 12px; - margin-bottom: 16px; -} - .feed-item { background-color: #1c1c1c; padding: 12px; @@ -1654,50 +1851,38 @@ display:flex; gap:8px; margin-top:16px; cursor: not-allowed; } -.tribe-feed .feed-row, -.tribe-feed-full .feed-row { - display: flex; - align-items: flex-start; - gap: 12px; -} - -.tribe-feed .refeed-column, -.tribe-feed-full .refeed-column { - flex: 0 0 68px; - min-width: 68px; - max-width: 68px; +.refeed-column { + flex: 0 0 80px; + min-width: 80px; + max-width: 80px; padding: 10px 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 88px; + box-sizing: border-box; + gap: 6px; } -.tribe-feed .refeed-column h1, -.tribe-feed-full .refeed-column h1 { - margin: 0 0 8px 0; +.refeed-column h1 { + margin: 0; font-size: 24px; } -.tribe-feed .refeed-btn, -.tribe-feed-full .refeed-btn { - padding: 6px 8px; +.refeed-btn { + padding: 6px 4px; font-size: 12px; width: 100%; -} - -.tribe-feed .feed-main, -.tribe-feed-full .feed-main { - flex: 1 1 auto; - min-width: 0; + white-space: normal; overflow-wrap: anywhere; - word-break: break-word; + line-height: 1.15; } @media (max-width: 720px) { - .tribe-feed .feed-row, - .tribe-feed-full .feed-row { - flex-direction: column; - } + - .tribe-feed .refeed-column, - .tribe-feed-full .refeed-column { + .refeed-column { width: 100%; min-width: 0; max-width: none; @@ -1705,60 +1890,10 @@ display:flex; gap:8px; margin-top:16px; } } -.tribe-feed .feed-actions, -.tribe-feed-full .feed-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 8px; -} - -.tribe-feed .feed-actions form, -.tribe-feed-full .feed-actions form, -.tribe-feed .refeed-column form, -.tribe-feed-full .refeed-column form { +.refeed-column form { margin: 0; } -.tribe-feed-compose { - display: grid; - grid-template-columns: 1fr auto; - gap: 12px; - margin-top: 14px; - align-items: end; -} - -.tribe-feed-compose textarea { - width: 100%; - min-width: 0; - padding: 12px; - border-radius: 10px; - border: 1px solid #444; - background: #222; - color: #eee; - resize: vertical; - min-height: 120px; - line-height: 1.45; - box-sizing: border-box; -} - -.tribe-feed-compose .tribe-feed-send { - height: 40px; - padding: 0 14px; - border-radius: 10px; - white-space: nowrap; -} - -@media (max-width: 720px) { - .tribe-feed-compose { - grid-template-columns: 1fr; - } - - .tribe-feed-compose .tribe-feed-send { - width: 100%; - } -} - .tribes-container{ display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:20px; margin-top:16px; } @@ -1818,13 +1953,19 @@ background:#ffcc00; color:#000; } .buy-btn { - display: inline-block; - background: #ffcc00; + display: block; + width: 100%; + padding: 14px 20px; + font-size: 1.2em; + font-weight: bold; + background: #ffa300; color: #000; text-decoration: none; text-align: center; + border-radius: 8px; } .buy-btn:hover { background: #ffcc00; color: #000; } +form:has(> .buy-btn) { width: 100%; } .gallery { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:10px; padding:10px; @@ -1922,20 +2063,41 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; border-radius: 6px; } -.lightbox-close { +.zoom-link{display:inline-block;border:none !important;background:transparent !important;padding:0 !important} +.internal-link{display:inline-flex;align-items:center;gap:6px;padding:2px 8px;border:1px solid rgba(128,128,128,.5);border-radius:10px;background:rgba(128,128,128,.12);text-decoration:none;vertical-align:baseline} +.internal-link:hover{background:rgba(128,128,128,.25);text-decoration:none} +.internal-link-kind{font-weight:700;letter-spacing:1px;font-size:.8em} +.internal-link-id{font-family:monospace;opacity:.75;font-size:.8em} +.zoom-link img{cursor:zoom-in} +.link-lightbox .link-box{display:flex;flex-direction:column;gap:10px;width:min(92vw,640px);background:#1e1f23;border:1px solid #333;border-radius:10px;padding:18px;box-sizing:border-box} +.link-box-row{display:flex;flex-direction:column;gap:4px} +.link-box-label{font-weight:700;font-size:.8em;letter-spacing:.5px;text-transform:uppercase} +.link-box-input{width:100%;box-sizing:border-box;font-family:monospace;margin:0} + +.lightbox-close, +.lightbox-download { position: absolute; top: 15px; - right: 60px; width: 42px; height: 42px; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; background: rgba(0, 0, 0, 0.6); border-radius: 50%; color: #fff; font-size: 1.5em; - text-align: center; - line-height: 32px; + line-height: 1; text-decoration: none; } +.lightbox-close { + right: 60px; +} +.lightbox-download { + right: 112px; + font-size: 1.15em; +} /* Opinions */ .voting-buttons { @@ -1994,9 +2156,13 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .conn-actions { display: flex; + flex-wrap: wrap; + align-items: center; gap: 10px; margin-top: 10px; } +.conn-actions form { margin: 0; padding: 0; border: none; background: transparent; box-shadow: none; } +.peers-paused-note { margin: 0; color: #ffa500; } .inhabitant-action { display: flex; @@ -2102,13 +2268,41 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-top: 10px; } -.event-actions { +.doc-export-actions { display: flex; + flex-wrap: wrap; gap: 10px; - margin-top: 10px; + justify-content: center; + margin-top: 12px; + border: none !important; + background: transparent !important; } -.feed-actions { +.doc-export-actions.doc-export-left { justify-content: flex-start; } + +.doc-export-actions form { + border: none !important; + background: transparent !important; + margin: 0; +} + +.vote-outcome { + font-weight: bold; +} + +.vote-outcome-yes { + color: #4caf50 !important; +} + +.vote-outcome-no { + color: #e53935 !important; +} + +.vote-outcome-pending { + color: #ffcc00 !important; +} + +.event-actions { display: flex; gap: 10px; margin-top: 10px; @@ -2185,6 +2379,24 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-top: 12px; } +.cv-threshold-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.cv-card-fields { + display: flex; + flex-direction: column; + gap: 4px; + margin: 6px 0; +} + +.cv-threshold-row label { + margin: 0; +} + .card-salary { display: inline-block; font-weight: 700; @@ -2192,6 +2404,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; letter-spacing: 0.02em; } +.transfer-amount-centered { + display: block; + text-align: center; +} + .mode-buttons { display: flex; gap: 10px; @@ -2261,7 +2478,9 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-top: 10px; background-color: #FFA500; border: none; - padding: 6px 12px; + padding: 4px 12px; + font-size: 20px; + line-height: 1.2; border-radius: 6px; font-weight: bold; color: #000; @@ -2295,6 +2514,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; text-decoration: underline; } +.feed-main a.tag-link, +.feed-main a.tag-link:hover { + text-decoration: none; +} + .votes-wrapper { margin-top: 16px; padding-top: 12px; @@ -2376,7 +2600,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; } .market-tribe-card .market-card-kind{display:inline-block;font-size:10px;letter-spacing:2px;color:#888;text-transform:uppercase;font-weight:600;margin-bottom:4px} -.market-tribe-card .market-card-price{font-size:16px;color:#FFD700;font-weight:600;margin-top:6px} .market-tribe-card .market-card-seller{font-size:13px;color:#bbb;margin-top:4px;word-break:break-all;border:none;background:transparent;padding:0} .market-tribe-card .market-card-seller-label{color:#ff9800;font-weight:600} @@ -2493,6 +2716,8 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; border: 0 !important; padding: 0 !important; width: 100%; + align-items: center; + justify-content: center; } .inhabitant-left .cv-actions form, @@ -2500,11 +2725,42 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; background: transparent !important; border: 0 !important; padding: 0 !important; - width: 100%; + width: auto; } -.inhabitant-left .cv-actions button.btn { +.inhabitant-user-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; width: 100%; + margin-top: 0; + padding: 0 !important; + border: 0 !important; + background: transparent !important; +} +.inhabitant-user-actions .filter-btn{margin-right:0} + +.inhabitant-user-actions form { + background: transparent !important; + border: 0 !important; + padding: 0 !important; + margin: 0; +} + +.inhabitant-relationship { + background: transparent !important; + border: 0 !important; + padding: 0 !important; +} + +.inhabitant-relationship .relationship-status { + justify-content: center; + text-align: center; +} + +.inhabitant-relationship .relationship-actions { + justify-content: center; } .inhabitant-left .cv-actions p { @@ -2542,6 +2798,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; + justify-content: center; } .status { @@ -2604,11 +2861,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; gap: 1.1em; background: none; } -.visit-forum-form { - display: flex; - justify-content: center; - margin-top: 10px; -} .forum-score-col { min-width: 78px; display: flex; @@ -2844,7 +3096,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; display: flex; flex-direction: row; align-items: flex-start; - margin-bottom: 1.5em; + margin-bottom: 10px; } .mode-buttons-cols { flex: 1; @@ -2867,7 +3119,12 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .comment-header { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; + gap: 10px; +} + +.comment-header .comment-votes { + margin-left: auto; } .comment-body-row { @@ -2898,6 +3155,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .comment-textarea { resize: vertical; width: 100%; + box-sizing: border-box; padding: 8px; background: #1e1e1e; color: #ffd740; @@ -2997,6 +3255,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; font-weight: bold; } +.forum-negative-votes { + font-weight: bold; + margin-left: 12px; +} + .forum-messages { font-weight: 500; } @@ -3009,10 +3272,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-bottom: 16px; } -.tribe-forum-reply-form { - margin-top: 12px; - margin-bottom: 16px; -} .forum-comment.level-4 { margin-left: 80px; } /*blockexplorer*/ @@ -3213,7 +3472,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; background: #1e1f23; border: none; border-radius: 50%; - padding: 7px 12px; + min-width: 36px; + height: 36px; + box-sizing: border-box; + line-height: 1; + padding: 0 11px; cursor: pointer; font-size: 1.14em; color: #ffa300; @@ -3228,6 +3491,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .btn-singleview:hover { background: #2d2e34; color: #ffd740; + text-decoration: none; } .btn-datagram { @@ -3310,6 +3574,8 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .pm-card.normal-pm { padding: 10px 12px; border-radius: 8px; margin-bottom: 10px; border: 1px solid #ffa300; background: transparent } .pm-btn { padding: 6px 16px; border-radius: 6px; font-size: 12px; font-weight: 700; letter-spacing: 0.5px; cursor: pointer; border: 1px solid #ffa300; background: transparent; color: #ffa300; transition: background 0.15s, color 0.15s } .pm-btn:hover { background: #ffa300; color: #000 } +a.pm-btn { text-decoration: none; display: inline-block; line-height: 1.2 } +a.pm-btn:hover { text-decoration: none } .pm-thread { margin-bottom: 12px } .pm-thread-details { margin-top: 0 } .pm-thread-details[open] .pm-thread-icon { display: inline-block; transform: rotate(90deg) } @@ -3333,7 +3599,10 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .pm-actions { display: flex; gap: 10px; + align-items: center; + flex-wrap: wrap; } +.pm-actions .pm-btn { margin: 0 } .pm-crypter-row { border: none; background: transparent; @@ -3429,7 +3698,8 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; display: flex; align-items: center; gap: 10px; - flex-wrap: nowrap; + flex-wrap: wrap; + max-width: 100%; } .project-control-select, @@ -3440,7 +3710,10 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .project-control-select, .project-control-input { - min-width: 220px; + min-width: 0; + flex: 1 1 160px; + max-width: 100%; + box-sizing: border-box; } .project-control-btn { @@ -3455,7 +3728,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; @media (max-width: 820px) { .project-control-select, .project-control-input { - min-width: 180px; + flex-basis: 100%; } .project-progress-input { @@ -3535,68 +3808,9 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; } /*banking*/ -.addr-form-card, .addr-list-card { - border: 1px solid rgba(255,255,255,0.1); - border-radius: 12px; - background: rgba(255,255,255,0.03); - padding: 20px; -} - -.addr-form-card.wide { - padding: 22px; -} - -.addr-form .form-row { - display: grid; - grid-template-columns: 140px 1fr; - gap: 10px; - align-items: center; - margin-bottom: 12px; -} - -.addr-form .form-input { - padding: 0 14px; -} - -.addr-form .form-input.xl { - height: 50px; - font-size: 16px; -} - -.addr-form .form-actions { - display: flex; - margin-top: 8px; -} - -.addr-list-card .form-row { - display: grid; - grid-template-columns: 150px 1fr; - gap: 10px; - align-items: center; -} - -.addr-list-card .form-input { - padding: 0 14px; -} - -.addr-search input[type="text"]:focus, -.addr-form .form-input:focus { - outline: none; - border-color: #ffa300; -} - -.addr-form .form-actions .mini-btn { - background: #ffa300; - border: none; - color: #fff; - padding: 9px 14px; - border-radius: 8px; - cursor: pointer; -} - -.addr-form .form-actions .mini-btn:hover { - background: #ff7f00; -} +.bank-form{max-width:560px} +.bank-form input[type="text"]{width:100%;box-sizing:border-box} +.no-content-box{border:1px solid #333;border-radius:10px;padding:18px;margin:12px 0;opacity:.85} .bank-summary .bank-info-table tbody tr .synced { color: green; @@ -3609,11 +3823,21 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; } .bank-claim-ubi{margin:16px 0} -.bank-claim-card{border:1px solid #FFA500;border-radius:8px;padding:16px;margin:8px 0} +.bank-claim-card{background:transparent;border:0;box-shadow:none;padding:0;margin:0 0 14px} .bank-claim-card p{margin:4px 0} .bank-claim-btn{font-size:16px;padding:10px 24px;margin-top:8px} +.bank-claim-actions{display:flex;flex-wrap:wrap;gap:12px;align-items:center;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.bank-claim-actions form{margin:0;padding:0;background:transparent !important;border:0 !important;box-shadow:none} .bank-eco-chart-block{margin:24px 0;border:1px solid #2a2a2a;border-radius:8px;padding:16px;background:#111} +.bank-eco-chart-zoom-link{display:block;cursor:zoom-in;text-decoration:none;border:0 !important;background:transparent !important;padding:0 !important} +.bank-range-tabs{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 12px;background:transparent !important;border:0 !important;padding:0 !important} +.bank-chart-empty-note{margin:6px 0 0;font-size:.9em;opacity:.75} +.bank-rules-list{margin:6px 0 14px 18px;padding:0} +.bank-rules-list li{margin:4px 0} +.bank-eco-chart-lightbox .bank-eco-chart-canvas-zoom{width:92vw;max-width:1400px;background:#111;border:1px solid #2a2a2a;border-radius:8px;padding:16px;box-sizing:border-box} +.bank-eco-chart-lightbox .bank-eco-chart-svg{width:100%;height:auto;display:block} +.addr-card-head .content-actions{margin:0;padding:0 !important} .bank-eco-chart-title{font-size:16px;margin:0 0 12px 0;color:#FFA500} .bank-eco-chart-canvas{width:100%;overflow:hidden} .bank-eco-chart-svg{width:100%;height:auto;display:block} @@ -3635,7 +3859,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .melody-player-card{border:1px solid #2a2a2a;border-radius:8px;padding:16px;background:#111;margin:16px 0} .melody-meta{margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:6px} .melody-meta-sep{color:#555} -.melody-audio{width:100%;max-width:520px} +.melody-audio{width:100%} .melody-section-title{font-size:16px;color:#FFA500;margin:16px 0 8px 0} .melody-help{color:#888;font-size:13px;margin:0 0 12px 0} .melody-notes-grid{display:flex;flex-wrap:wrap;gap:6px} @@ -3651,6 +3875,10 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .eco-tax-chip-low .eco-tax-chip-value{color:#9aff9a} .eco-tax-chip-low:hover{background:#143820;border-color:#5aa066} .eco-tax-chip-mid{background:#2a2410;border-color:#a39134;color:#ffd95a} +.eco-value-chip{margin-left:8px;vertical-align:middle} +.log-window-note{display:flex;justify-content:center;margin:16px 0 8px 0} +.mobile-counters{display:none} +.log-window-chip .eco-tax-chip-value{font-family:inherit} .eco-tax-chip-mid .eco-tax-chip-label{color:#ffe9a0} .eco-tax-chip-mid .eco-tax-chip-value{color:#ffd95a} .eco-tax-chip-mid:hover{background:#3a3416;border-color:#c5b14e} @@ -3681,12 +3909,26 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .melody-upload-submit{width:auto;display:inline-block;padding:6px 18px;align-self:flex-start} .melody-upload-label{color:#FFD700;font-size:13px;font-weight:600;display:inline-block;margin-top:6px} .melody-bcs-list{display:flex;flex-direction:column;gap:14px;margin-top:14px;background:transparent !important;border:0 !important;padding:0 !important} -.audio-container{display:flex;gap:8px;align-items:center;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important} +.video-container{display:flex;flex-direction:column;align-items:center;width:100%;background:transparent !important;border:0 !important;padding:0 !important} +.video-container video{width:100%;max-width:720px;max-height:480px;height:auto} .audio-transcode-form{display:inline-block;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.audio-container{display:flex;flex-direction:row;align-items:center;gap:10px;width:100%;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;margin:0 0 8px 0} +.audio-container audio{flex:1 1 auto;min-width:0;width:100%} +.card-section .card-field.audio-container, +.trending-card .card-field.audio-container, +.opinions-card .card-field.audio-container{display:flex} +.card-section .card-field.video-container, +.trending-card .card-field.video-container, +.opinions-card .card-field.video-container{display:block} +.audio-transcode-form .filter-btn{background:transparent;color:inherit;font-size:.75em;padding:3px 10px;border:1px solid #555;border-radius:12px;white-space:nowrap;line-height:1.5} +.audio-transcode-form .filter-btn:hover{border-color:#ffa300;color:#ffa300;background:transparent} +.audio-transcode-form .create-button{margin:0;font-size:.75em;padding:3px 12px;border-radius:12px;border:1px solid #ffcc00;background:#ffcc00;color:#111;font-weight:bold;line-height:1.5;cursor:pointer} +.audio-transcode-run-form{margin:6px 0 16px 0 !important} .melody-bcs-card{background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} .melody-bcs-head{display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important;padding:0 !important} .melody-bcs-title{color:#FFA500;margin:0;font-size:17px} -.melody-bcs-meta{color:#bbb;font-size:13px;background:transparent !important;border:0 !important;padding:0 !important} +.melody-bcs-meta{color:#bbb;font-size:13px;background:transparent !important;border:0 !important;padding:0 !important;display:flex;align-items:center;gap:10px;margin:6px 0 0 0} +.melody-bcs-date{opacity:.75} .melody-bcs-player{margin:10px 0;background:transparent !important;border:0 !important;padding:0 !important} .melody-bcs-desc{color:#ccc;font-size:13px;margin:6px 0 0 0;line-height:1.5} .melody-bcs-actions{margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important} @@ -3703,11 +3945,10 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .pub-item{border:1;border-radius:10px;background:none} .snh-invite-box{border:1px solid currentColor;border-radius:8px;padding:16px;margin:12px 0;font-family:monospace} .snh-invite-name{margin:0 0 8px 0} +.snh-invite-code{word-break:break-all} .snh-invite-date{font-size:13px;opacity:0.7;margin:0 0 10px 0} .snh-invite-btn{display:block;word-break:break-all;font-size:14px;margin:0;cursor:pointer;background:none;border:1px solid #FFA500;color:#FFA500;padding:10px;border-radius:5px;width:auto;text-align:left;font-family:monospace} .error-box{background:#222326;border:none;color:#f5c242;padding:12px;border-radius:8px;margin-top:8px} -.error-title{margin:0 0 6px 0;font-weight:600} -.error-pre{margin:0;white-space:pre-wrap;font-family:monospace} .shop-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin:8px 0} .shop-card{border:1px solid #333;border-radius:8px;overflow:hidden;display:flex;flex-direction:column} @@ -3728,14 +3969,15 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .shop-detail-media img{max-width:100%;border-radius:6px} .shop-detail-desc{margin:8px 0} .shop-share{margin:8px 0;display:flex;align-items:center;gap:8px} -.shop-share-input{flex:1;background:#222;border:1px solid #555;color:#ccc;padding:6px 10px;border-radius:4px;font-family:monospace;font-size:13px} +.shop-share-input{flex:1;background:rgba(128,128,128,.12);border:1px solid rgba(255,163,0,.6);color:#ffa300;font-weight:bold;padding:6px 10px;border-radius:4px;font-family:monospace;font-size:13px;margin:0} .shop-products-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin:12px 0} .shop-product-card{border:1px solid #333;border-radius:8px;overflow:hidden;display:flex;flex-direction:column} .shop-product-media{max-height:180px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#111} .shop-product-media img{width:100%;max-height:180px;object-fit:cover} .shop-product-body{padding:10px;display:flex;flex-direction:column;gap:6px} -.shop-product-price{font-size:18px;font-weight:bold;color:#FFA500} -.shop-product-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px} +.price-chip{display:inline-flex;align-items:center;width:auto;max-width:max-content;box-sizing:border-box;border-radius:10px;font-size:14px;font-weight:700;border:1px solid #a39134;background:#3a2300;color:#ffd700;padding:4px 12px;margin:6px 0 0 0;box-shadow:none;white-space:nowrap} +.time-chip{display:inline-flex;align-items:center;width:auto;max-width:100%;box-sizing:border-box;border-radius:10px;font-size:14px;font-weight:700;line-height:1.4;border:1px solid #5a5a5a;background:#3a3a3a;color:#ffd740;padding:4px 12px;margin:6px 0 0 0;box-shadow:none;white-space:normal;overflow-wrap:anywhere} +.shop-product-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;padding:0;border:0;background:transparent;box-shadow:none} .shop-add-product{border:1px dashed #555;border-radius:8px;padding:16px;margin:12px 0} .shop-featured-products{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0} .shop-featured-item{display:flex;flex-direction:column;align-items:center;width:72px;text-decoration:none;border:1px solid #333;border-radius:6px;overflow:hidden;background:#111} @@ -3755,13 +3997,15 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; /* carbon footprint chart */ .carbon-section-title{font-size:18px;color:#555;margin:8px 0;font-weight:600} +#carbon{scroll-margin-top:16px} +#wallet,#language,#ux,#theme,#wish,#workflows,#home-page,#ai,#logstream,#replication,#lan,#pm-visibility,#multiverse,#verification,#indexes,#export,#panic{scroll-margin-top:16px} .carbon-chart{margin:12px 0;background:transparent !important;border:none !important} .carbon-bar-label{display:flex;justify-content:space-between;font-size:14px;margin:4px 0 2px;background:transparent !important;border:none !important} .carbon-bar-track{background:#3a3a3a !important;border-radius:6px;height:22px;overflow:hidden;border:none !important} .carbon-bar-fill{height:100%;border-radius:6px;min-width:2px;transition:width 0.4s ease;border:none !important} .carbon-bar-network{background:#2ecc71 !important} .carbon-bar-mine{background:#3498db !important} -.carbon-bar-max{background:#555 !important;border:none !important} +.carbon-bar-max{background:#ffa300 !important;border:none !important} .carbon-bar-note{font-size:13px;color:#ffd700;margin:6px 0 2px} .carbon-bar-formula{font-size:12px;color:#ffd700;margin:2px 0 10px} @@ -3992,8 +4236,28 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .tribe-card-title { margin: 0; font-size: 1.15em; + overflow-wrap: anywhere; } +.jobs-grid .tribe-card-title, +.tribe-grid .tribe-card-title, +.trending-card .tribe-card-title, +.trending-card .card-section > h2, +.data-card-title, +.job-section-title, +.melody-bcs-title { + overflow-wrap: anywhere; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} +.data-reasons-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;background:transparent !important;border:0 !important;padding:0 !important} +.data-section{margin-top:18px;background:transparent !important;border:0 !important;padding:0 !important} +.data-section-title{margin:0 0 8px 0;font-size:.95em;letter-spacing:.06em;color:#ffa500} +.data-top-best{margin-top:6px} +.data-reason-chip{opacity:1;border-color:rgba(255,163,0,.6)} + .tribe-card-footer { display: flex; align-items: center; @@ -4055,10 +4319,31 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; gap: 6px; } .tribe-side-actions form > * { margin: 0 } +.tribe-side-center { flex-direction: column; justify-content: center; } +.tribe-enter-invite { border: none !important; background: transparent !important; box-shadow: none !important; padding: 0; } .tribe-side-actions select, .tribe-side-actions input[type="number"], .tribe-side-actions input[type="text"] { height: 36px; box-sizing: border-box } -.tribe-side-actions button { height: 36px; box-sizing: border-box; display: inline-flex; align-items: center } +.tribe-side-actions button, +.tribe-side-actions > a, +.tribe-side-actions form > a { + height: 36px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.tribe-side-actions a > button { + height: auto; + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + letter-spacing: inherit; + text-transform: inherit; +} .tribe-side-subtribes { margin: 8px 0; @@ -4095,7 +4380,8 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .tribe-mode-buttons { display: flex; gap: 8px; - margin-top: 16px; + margin-top: 0; + margin-bottom: 10px; } .tribe-card-image-wrapper { @@ -4160,18 +4446,30 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; } .tribe-open-invite { - border: none; + display: block; + border: none !important; border-radius: 4px; - padding: 8px; + padding: 0 !important; margin: 6px 0; - background: #1e1f23; - text-align: center; + background: transparent !important; + box-shadow: none; + text-align: left; } .tribe-open-invite-code { - font-family: monospace; + display: block; + font-family: 'JetBrains Mono', monospace; + font-size: .95em; + color: #FFD700; + margin: 8px 0 10px; + user-select: all; + overflow-wrap: anywhere; word-break: break-all; - font-weight: bold; - margin-left: 6px; + background: #111 !important; + border: 1px dashed rgba(255, 163, 0, .5) !important; + border-radius: 6px; + padding: 8px 10px !important; + text-align: center; + box-shadow: none; } .tribe-open-invite-panel { margin: 6px 0; } .profile-side-version { margin: 4px 0 8px; font-size: 0.9em; text-align: center; } @@ -4220,80 +4518,61 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .tribe-action-btn { padding: 6px 16px; border-radius: 6px; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.5px; - cursor: pointer; - border: 1px solid #ffa300; - background: transparent; - color: #ffa300; - transition: background 0.15s, color 0.15s; -} -.tribe-action-btn, -.tribe-side-actions button, -.tribe-side-actions a, -.map-detail-actions button, -.map-detail-actions a { - display: inline-block; - padding: 6px 16px; - border-radius: 6px; + font-family: inherit; font-size: 12px; font-weight: 700; letter-spacing: 0.5px; line-height: 1.2; - text-transform: uppercase; - text-decoration: none; - text-align: center; cursor: pointer; border: 1px solid #ffa300; background: transparent; color: #ffa300; + text-decoration: none; + display: inline-block; + appearance: none; + -webkit-appearance: none; transition: background 0.15s, color 0.15s; } -.tribe-side-actions button:hover, -.tribe-side-actions a:hover, -.map-detail-actions button:hover, -.map-detail-actions a:hover { - background: #ffa300; - color: #000; -} -.tribe-side-actions .tribe-action-btn.danger-btn, -.tribe-side-actions .delete-btn, -.map-detail-actions .delete-btn { - border-color: #d9534f; - color: #d9534f; -} -.tribe-side-actions .tribe-action-btn.danger-btn:hover, -.tribe-side-actions .delete-btn:hover, -.map-detail-actions .delete-btn:hover { - background: #d9534f; - color: #000; +.tribe-action-btn:hover { + text-decoration: none; } .tribe-action-btn.danger-btn, .tribe-card-actions .tribe-action-btn.danger-btn, -.tribe-side-actions .tribe-action-btn.danger-btn { +.tribe-side-actions .tribe-action-btn.danger-btn, +.tribe-side-actions .delete-btn { border: 1px solid #d9534f !important; color: #d9534f !important; background: transparent !important; } .tribe-action-btn.danger-btn:hover, .tribe-card-actions .tribe-action-btn.danger-btn:hover, -.tribe-side-actions .tribe-action-btn.danger-btn:hover { +.tribe-side-actions .tribe-action-btn.danger-btn:hover, +.tribe-side-actions .delete-btn:hover { background: #d9534f !important; color: #000 !important; } +.tribe-side-actions .update-btn { + border: 1px solid #ffa300; + color: #ffa300; + background: transparent; +} +.tribe-side-actions .update-btn:hover { + background: #ffa300; + color: #000; +} .tribe-action-btn:hover { background: #ffa300; color: #000; } .tribe-action-btn.danger-btn { border-color: #d9534f; color: #d9534f; } +.tribe-action-btn.success-btn { border-color: #2ecc71; color: #2ecc71; } +.tribe-action-btn.success-btn:hover { background: #2ecc71; color: #000; } .tribe-action-btn.danger-btn:hover { background: #d9534f; color: #000; } .calendar-interval-block { margin: 8px 0 16px; } .calendar-interval-label { display: block; margin-bottom: 6px; } -.calendar-interval-until { margin-top: 10px; } -.calendar-interval-row { display: flex; gap: 16px; flex-wrap: wrap; } -.calendar-interval-option { display: flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; } +.calendar-interval-until { margin-top: 14px; } +.calendar-interval-select { margin: 0; } .tribe-thumb-grid { display: grid; @@ -4319,22 +4598,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; object-fit: cover; } -.tribe-feed-compose { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 16px; -} - -.tribe-feed-compose textarea { - width: 100%; - box-sizing: border-box; -} - -.tribe-feed-compose .tribe-feed-send { - align-self: flex-end; -} - .tribe-parent-box { margin-bottom: 12px; text-align: center; @@ -4387,9 +4650,12 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .tribe-grid { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; } +@media (max-width: 900px) { + .tribe-grid { grid-template-columns: 1fr; } +} .tribe-section-nav { display: flex; @@ -4426,16 +4692,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; border-right: none; } -.tribe-section-nav .filter-btn { - padding: 8px 12px; - text-transform: uppercase; -} - -.tribe-section-nav .filter-btn.active { - background: #ffcc00; - color: #000; -} - .tribe-content-card { background: #1c1c1c; border: 1px solid #333; @@ -4450,7 +4706,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .tribe-content-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; } @@ -4466,13 +4722,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; align-items: center; margin-bottom: 16px; } -.tribe-search-form{display:flex;gap:10px;align-items:center} - -.tribe-content-filters { - display: flex; - gap: 8px; - margin-bottom: 12px; -} .tribe-content-status { display: inline-block; @@ -4536,68 +4785,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; object-fit: cover; } -.tribe-votation-options { - display: flex; - flex-direction: column; - gap: 8px; - margin: 12px 0; -} - -.tribe-votation-option { - display: flex; - align-items: center; - gap: 12px; -} - -.tribe-votation-bar { - background: #333; - border-radius: 4px; - height: 24px; - position: relative; - overflow: hidden; - flex: 1; -} - -.tribe-votation-fill { - background: #ff6600; - height: 100%; - border-radius: 4px; - transition: width 0.3s ease; -} - -.tribe-fill-0 { width: 0%; } -.tribe-fill-5 { width: 5%; } -.tribe-fill-10 { width: 10%; } -.tribe-fill-15 { width: 15%; } -.tribe-fill-20 { width: 20%; } -.tribe-fill-25 { width: 25%; } -.tribe-fill-30 { width: 30%; } -.tribe-fill-35 { width: 35%; } -.tribe-fill-40 { width: 40%; } -.tribe-fill-45 { width: 45%; } -.tribe-fill-50 { width: 50%; } -.tribe-fill-55 { width: 55%; } -.tribe-fill-60 { width: 60%; } -.tribe-fill-65 { width: 65%; } -.tribe-fill-70 { width: 70%; } -.tribe-fill-75 { width: 75%; } -.tribe-fill-80 { width: 80%; } -.tribe-fill-85 { width: 85%; } -.tribe-fill-90 { width: 90%; } -.tribe-fill-95 { width: 95%; } -.tribe-fill-100 { width: 100%; } - -.tribe-votation-label { - min-width: 120px; - color: #ccc; -} - -.tribe-votation-count { - min-width: 40px; - text-align: right; - color: #9aa3b2; -} - .tribe-forum-thread { background: #1c1c1c; border: 1px solid #333; @@ -4614,33 +4801,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; border-radius: 0 8px 8px 0; } -.tribe-forum-reply-form { - margin-top: 16px; -} - -.tribe-media-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 18px; -} - -.tribe-media-item { - background: #1c1c1c; - border-radius: 8px; - overflow: hidden; - border: 1px solid #333; - transition: border-color .2s; - padding-bottom: 8px; -} - -.tribe-media-item:hover { - border-color: #ffa500; -} - -.tribe-media-item a { - display: block; -} - .tribe-media-thumb { width: 100%; height: 160px; @@ -4655,50 +4815,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; box-sizing: border-box; } -.tribe-media-item img { - width: 100%; - height: 160px; - object-fit: cover; -} - -.tribe-media-item video { - width: 100%; - height: 160px; - object-fit: cover; -} - -.tribe-media-title-link { - color: #ffa500; - font-weight: bold; - text-decoration: none; - display: block; - margin-bottom: 4px; -} - -.tribe-media-title-link:hover { - text-decoration: underline; -} - -.tribe-media-item-info { - padding: 12px; -} - -.tribe-media-date { - margin: 6px 0 2px 0; - font-size: 0.85em; -} - -.tribe-media-author { - margin: 0 0 6px 0; - font-size: 0.85em; -} - -.tribe-media-filters { - display: flex; - gap: 8px; - margin-bottom: 12px; -} - .tribe-overview-grid { display: grid; grid-template-columns: 1fr 1fr; @@ -4736,22 +4852,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; color: #88cc88; } -.tribe-feed-compose { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 16px; -} - -.tribe-feed-compose textarea { - width: 100%; - box-sizing: border-box; -} - -.tribe-feed-compose .tribe-feed-send { - align-self: flex-end; -} - .comment-submit-btn { width: auto; max-width: 200px; @@ -4873,6 +4973,19 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; font-size: 1.15rem; font-weight: 600; } +.tag-cloud-item[data-weight="5"] { + font-size: 1.3rem; + font-weight: 700; +} +.tag-cloud-item[data-weight="1"] { color: hsl(200,70%,60%); } +.tag-cloud-item[data-weight="2"] { color: hsl(230,70%,60%); } +.tag-cloud-item[data-weight="3"] { color: hsl(260,70%,60%); } +.tag-cloud-item[data-weight="4"] { color: hsl(290,70%,60%); } +.tag-cloud-item[data-weight="5"] { color: hsl(320,70%,60%); } +.vote-outcome{font-weight:bold} +.vote-outcome-yes{color:#4caf50} +.vote-outcome-no{color:#e53935} +.vote-outcome-none{color:#ffcc00} .stats-karma-block { background-color: #222; @@ -4890,14 +5003,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-bottom: 16px; } -.map-card { - background-color: #222; - border: 1px solid #444; - border-radius: 8px; - padding: 16px; - margin-bottom: 16px; -} - .map-card-info { display: flex; gap: 12px; @@ -4948,28 +5053,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; word-break: break-all; } -.map-detail-info { - display: flex; - gap: 16px; - align-items: center; - flex-wrap: wrap; - margin-bottom: 12px; -} - -.map-wrap { - margin-right: 30px; - position: relative; - border: 0px; -} - -.map-img { - display: block; - width: 100% !important; - height: 100% !important; - max-width: none !important; - border: none !important; -} - .map-bar { display: flex; align-items: center; @@ -5016,10 +5099,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; font-size: 1.2rem; } -.map-create-layout { - width: 100%; -} - .map-form { margin-top: 12px; } @@ -5043,86 +5122,29 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; display: flex; border: 0px; gap: 12px; + align-items: center; +} + +.map-form-row .filter-btn { + height: 36px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; + text-decoration: none; } .map-form-field { flex: 1; } -.map-card { - background-color: #222; - border: 1px solid #444; - border-radius: 8px; - padding: 12px 16px; - margin-bottom: 12px; - display: flex; - gap: 16px; - align-items: flex-start; -} - -.map-card-thumb-link { - flex-shrink: 0; -} - -.map-card-thumb { - width: 120px !important; - height: 120px !important; - max-width: none !important; - border-radius: 6px !important; - border: 1px solid #555 !important; - object-fit: cover; -} - -.map-card-body { - flex: 1; - min-width: 0; -} - -.map-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 8px; -} - -.map-card-actions { - display: flex; - gap: 6px; - flex-wrap: wrap; - align-items: center; -} - .map-click-hint { color: #FFA500; font-size: 0.85rem; padding: 6px 0; } -.map-detail { - background-color: #222; - border: 1px solid #444; - border-radius: 8px; - padding: 16px; -} - -.map-detail-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - flex-wrap: wrap; - margin-bottom: 12px; -} - -.map-detail-actions { - display: flex; - gap: 6px; - flex-wrap: wrap; - align-items: center; -} - .map-marker-form { margin-top: 20px; padding-top: 16px; @@ -5234,6 +5256,31 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; font-size: 0.9rem; } +.search-card { + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; +} + +.search-card .card-value, +.search-card .styled-text, +.search-card p { + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; +} + +.search-card img, +.search-card video { + max-width: 100%; + height: auto; +} + +.search-card audio { + width: 100%; + max-width: 100%; +} + .search-result-image { max-width: 100%; max-height: 300px; @@ -5268,12 +5315,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin: 16px 0; } -.maps-list { - display: flex; - flex-direction: column; - gap: 16px; -} - .maps-search { margin-bottom: 16px; } @@ -5335,90 +5376,552 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .map-markers-list h3 { margin-bottom: 4px; } -.map-form-full { - width: 100%; +.map-viewer, +.map-stage, +.map-stage div, +.map-hero, +.map-toolbar, +.map-toolbar div, +.map-place-results, +.map-markers-list, +.map-markers-list div, +.map-detail-grid, +.map-picked, +.map-card-thumb .map-viewer { + margin: 0; + padding: 0; + background: transparent; + border: 0; + box-shadow: none; + border-radius: 0; + overflow: visible; } -.map-form-full form { - width: 100%; +.map-marker-meta { + display: inline-flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; + font-size: 0.75rem; } -.map-osm-embed { - width: 100%; - height: 450px; - border: 1px solid #444; - border-radius: 8px; - margin: 12px 0; +.map-marker-meta a.user-link { + padding: 2px 8px; + font-size: 0.7rem; + border-width: 1px; } .map-viewer { width: 100%; - overflow-x: auto; - overflow-y: hidden; border: 0px; } -.map-form-full, .map-form-full form { - box-sizing: border-box; - overflow: hidden; +.map-toolbar label { + display: inline-block; + width: auto; + white-space: nowrap; } -.map-popup { - display: none; - position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0,0,0,0.65); - z-index: 1000; - align-items: center; - justify-content: center; +.map-toolbar .filter-btn, +.map-place-result, +.map-center-link { + white-space: nowrap; } -.map-popup:target { - display: flex; -} -.map-popup-box { - background: #222; - border: 1px solid #555; - border-radius: 10px; - padding: 20px 24px; - max-width: 360px; - width: 90%; - color: #ddd; +.map-stage { position: relative; + width: 100%; + background: #16232e; + border: 1px solid #333; + border-radius: 8px; + overflow: hidden; + line-height: 0; } -.map-popup-close { +.map-stage-thumb { + border: 0; + border-radius: 0; +} +.map-svg { + display: block; + width: 100%; + height: auto; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} +.map-pins-layer { position: absolute; - top: 10px; right: 14px; - color: #FFA500; - text-decoration: none; - font-size: 1.2rem; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 4; + pointer-events: none; +} +.map-pins-layer a { + pointer-events: auto; +} +.map-ocean { fill: #a7c6de; } +.map-tile { image-rendering: auto; } +.map-grat { fill: none; stroke: rgba(255, 255, 255, 0.4); stroke-width: 0.6; } +.map-border { fill: none; stroke: rgba(72, 52, 32, 0.65); stroke-width: 0.8; stroke-linejoin: round; } +.map-lake { fill: none; stroke: rgba(36, 86, 128, 0.7); stroke-width: 0.7; stroke-linejoin: round; } +.map-state { fill: none; stroke: rgba(72, 52, 32, 0.5); stroke-width: 0.6; stroke-dasharray: 3 2; } +.map-coast { fill: none; stroke: rgba(36, 86, 128, 0.75); stroke-width: 0.8; stroke-linejoin: round; } +.map-label-country { fill: #262c31; font-size: 14px; font-weight: 600; letter-spacing: 0.6px; text-anchor: middle; paint-order: stroke; stroke: rgba(255, 255, 255, 0.85); stroke-width: 3px; stroke-linejoin: round; } +.map-place-dot { fill: #1f252a; stroke: #ffffff; stroke-width: 1; } +.map-place-capital { fill: #d35400; } +.map-place-label { fill: #1f252a; font-size: 12px; paint-order: stroke; stroke: rgba(255, 255, 255, 0.85); stroke-width: 3px; stroke-linejoin: round; } +.map-place-label-capital { font-weight: 700; } +.map-pin-body { fill: #3498db; stroke: #ffffff; stroke-width: 1.5; } +.map-pin-main .map-pin-body { fill: #e74c3c; } +.map-pin-focus .map-pin-body { fill: #FFA500; stroke: #ffffff; stroke-width: 2.2; } +.map-pin-eye { fill: #ffffff; } +.map-pin-label-bg { fill: rgba(8, 12, 16, 0.86); stroke: rgba(255, 255, 255, 0.35); stroke-width: 0.6; } +.map-pin-focus .map-pin-label-bg { stroke: #FFA500; stroke-width: 1.2; } +.map-pin-label { fill: #ffffff; font-size: 12px; font-weight: 700; text-anchor: middle; } +.map-pin-img-frame { fill: #ffffff; } +.map-pins-layer a:hover .map-pin-body, .map-pins a:hover .map-pin-body { fill: #FFA500; } +.map-cluster-ring { fill: rgba(255, 165, 0, 0.28); } +.map-cluster-dot { fill: #FFA500; stroke: #ffffff; stroke-width: 1.6; } +.map-cluster-count { fill: #000000; font-size: 13px; font-weight: 700; text-anchor: middle; } +a:hover .map-cluster-dot { fill: #ffffff; } +.map-pick-ring { fill: none; stroke: #FFA500; stroke-width: 2; } +.map-pick-cross { fill: none; stroke: #FFA500; stroke-width: 2; stroke-linecap: round; } +.map-pick-center { fill: #ffffff; } +.map-scale-line { fill: none; stroke: #1f252a; stroke-width: 1.6; } +.map-scale-text { fill: #1f252a; font-size: 11px; font-weight: 600; paint-order: stroke; stroke: rgba(255, 255, 255, 0.85); stroke-width: 3px; } +.map-attrib { fill: #3b434a; font-size: 9px; text-anchor: end; paint-order: stroke; stroke: rgba(255, 255, 255, 0.75); stroke-width: 2px; } +.map-pick-grid { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: grid; + grid-template-columns: repeat(24, 1fr); + grid-template-rows: repeat(15, 1fr); +} +.map-pick-cell { + display: block; + background: transparent; + border: 0; + margin: 0; + padding: 0; + min-width: 0; + min-height: 0; + width: 100%; + height: 100%; + cursor: crosshair; + border-radius: 0; + box-shadow: none; +} +.map-pick-cell:hover { + background: rgba(255, 165, 0, 0.22); + outline: 1px solid rgba(255, 165, 0, 0.7); + outline-offset: -1px; +} +.map-controls { + position: absolute; + z-index: 3; + display: flex; + flex-direction: column; + gap: 4px; line-height: 1; } -.map-popup-label { - font-weight: 600; - font-size: 1rem; - color: #eee; - margin-bottom: 6px; +.map-controls-zoom { + top: 10px; + left: 10px; } -.map-popup-coords { - font-family: monospace; - font-size: 0.85rem; - color: white; +.map-controls-pan { + top: 10px; + right: 10px; + display: grid; + grid-template-columns: repeat(3, 30px); + grid-template-rows: repeat(3, 30px); + gap: 2px; } -.map-popup-link { +.map-ctrl { + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(18, 18, 18, 0.88); color: #FFA500; - text-decoration: underline; - word-break: break-all; + border: 1px solid #555; + border-radius: 6px; + text-decoration: none; + font-size: 17px; + font-weight: 700; + font-family: inherit; + margin: 0; + padding: 0; + cursor: pointer; + box-sizing: border-box; +} +.map-ctrl:hover { + background: #FFA500; + color: #000; + border-color: #FFA500; +} +.map-ctrl-off { + opacity: 0.35; + cursor: default; +} +.map-ctrl-off:hover { + background: rgba(18, 18, 18, 0.88); + color: #FFA500; + border-color: #555; +} +.map-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin: 10px 0; +} +.map-toolbar form { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 0; +} +.map-toolbar label { + margin: 0; +} +.map-toolbar select, +.map-toolbar input[type="text"] { + height: 34px; + box-sizing: border-box; + margin: 0; +} +.map-toolbar .filter-btn { + height: 34px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + margin: 0; +} +.map-place-search input[type="text"] { + min-width: 220px; +} +.map-place-results { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 6px 0; + align-items: center; +} +.map-place-result { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border: 1px solid #555; + border-radius: 14px; + color: #FFA500; + text-decoration: none; + font-size: 0.85rem; + background: transparent; + font-family: inherit; + cursor: pointer; + line-height: 1.3; +} +.map-place-result:hover { + background: #FFA500; + color: #000; + border-color: #FFA500; +} +.map-place-result-country { + opacity: 0.75; + font-size: 0.8em; +} +.map-place-none { + color: #888; + font-size: 0.85rem; +} +.map-hero { + margin: 0 0 12px 0; +} +.map-detail-grid { + display: flex; + gap: 24px; + align-items: flex-start; +} +.map-detail-side { + width: 320px; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 16px; +} +.map-detail-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 16px; +} +@media (max-width: 900px) { + .map-detail-grid { + flex-direction: column; + } + .map-detail-side { + width: 100%; + } +} +.map-markers-list .map-marker-row { + display: grid; + grid-template-columns: 28px minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-bottom: 1px solid #333; + text-align: left; +} +.map-markers-list .map-marker-row:last-child { + border-bottom: 0; +} +.map-marker-body { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} +.map-marker-date { + color: #888; +} +.map-marker-meta a.user-link { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + box-sizing: border-box; +} +.map-form-actions { + margin-top: 14px; +} +.map-form-actions .create-button, +.map-form-actions .filter-btn { + height: 36px; + margin: 0; + padding: 0 18px; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + font-size: 0.9em; + line-height: 1; + white-space: nowrap; + text-decoration: none; +} +.map-popup { + pointer-events: auto; +} +.map-popup-pointer { + fill: #1e1f23; + stroke: #555; + stroke-width: 1; +} +.map-stage .map-popup-wrap { + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + line-height: 1.35; + font-family: inherit; +} +.map-stage .map-popup--below .map-popup-wrap { + justify-content: flex-start; +} +.map-stage .map-popup-card { + background: #1e1f23; + border: 1px solid #555; + border-radius: 8px; + padding: 10px 12px; + color: #fff; + font-size: 13px; + box-sizing: border-box; + max-height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + gap: 6px; + text-align: left; +} +.map-stage .map-popup-card > * { + flex-shrink: 0; +} +.map-stage .map-popup-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} +.map-popup-close { + color: #aaa; + text-decoration: none; + font-size: 20px; + line-height: 1; +} +.map-popup-close:hover { + color: #FFA500; + text-decoration: none; } .map-popup-img { - display: block; - max-width: 180px; + width: 100%; max-height: 120px; object-fit: cover; border-radius: 4px; - margin: 6px auto; -} -.map-marker-img { display: block; - max-width: 120px; - max-height: 80px; +} +.map-stage .map-popup-text { + color: #ddd; + word-break: break-word; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; +} +.map-popup-text img { + max-width: 100%; + max-height: 100px; +} +.map-popup-coords { + font-family: monospace; + font-size: 12px; + color: #fff; +} +.map-stage .map-popup-meta { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + font-size: 12px; + color: #aaa; +} +.map-popup-meta a.user-link { + padding: 2px 8px; + font-size: 0.7rem; + border-width: 1px; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + box-sizing: border-box; +} +.map-stage .map-popup-actions { + display: flex; + gap: 6px; + align-items: center; +} +.map-stage .map-popup-actions form { + margin: 0; +} +.map-popup-actions .btn-singleview { + min-width: 30px; + height: 30px; + font-size: 1em; +} +.map-markers-list .map-marker-row--focus { + background: rgba(255, 165, 0, 0.08); + box-shadow: inset 3px 0 0 #FFA500; +} +.map-marker-idx { + min-width: 24px; + height: 24px; + border-radius: 50%; + background: #3498db; + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 700; + flex-shrink: 0; +} +.map-marker-idx--main { + background: #e74c3c; +} +.map-marker-row--focus .map-marker-idx { + background: #FFA500; + color: #000; +} +.map-marker-thumb { + width: 44px; + height: 44px; object-fit: cover; border-radius: 4px; - margin: 4px 0; + flex-shrink: 0; +} +.map-marker-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; +} +.map-marker-label { + font-weight: 600; + color: #ffa300; + word-break: break-word; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} +.map-marker-actions { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 6px; + justify-self: end; +} +.map-marker-actions .btn-singleview { + align-self: center; +} +.map-marker-actions form { + margin: 0; +} +.map-marker-actions .btn-singleview { + min-width: 32px; + height: 32px; + font-size: 1em; +} +.map-center-link { + color: #FFA500; + text-decoration: none; + font-size: 0.85rem; + border: 1px solid #555; + border-radius: 6px; + padding: 0 10px; + height: 32px; + display: inline-flex; + align-items: center; + box-sizing: border-box; + white-space: nowrap; +} +.map-center-link:hover { + background: #FFA500; + color: #000; + border-color: #FFA500; +} +.map-picked { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 8px 0; + padding: 8px 12px; + background: #222; + border-radius: 6px; + color: #FFA500; + font-family: monospace; + font-size: 0.9rem; +} +.map-picked-near { + color: #ddd; + font-family: inherit; +} +.map-pick-hint { + color: #888; + font-size: 0.85rem; + margin: 6px 0; } .map-zoom-info { display: block; @@ -5432,10 +5935,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .map-embed-url .map-location-link { font-size: 0.8rem; } -.map-zone { - display: block; -} -.map-popup-container { height: 0; overflow: hidden; border: 0px; } .project-maploc { margin: 6px 0; } @@ -5474,14 +5973,88 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .chat-participants-list li{padding:4px 0;font-size:0.85rem} .chat-message-form{background:#1a1a1a;border:1px solid #333;border-radius:4px;padding:10px} .chat-message-form textarea{width:100%;box-sizing:border-box;background:#111;color:#eee;border:1px solid #555;border-radius:4px;padding:8px;resize:vertical;font-family:inherit} -.chat-messages-list{display:flex;flex-direction:column;gap:4px} -.chat-message{padding:6px 10px;border-radius:4px;background:#1a1a1a;word-break:break-word;border:none} -.chat-message-author{border-left-color:#FFA500;background:#1f1800} -.chat-message-self{border-left-color:#888;background:#181818} -.chat-message-meta{font-size:0.8rem;color:#888;border:none} -.chat-message-date{color:#666} -.chat-message-sender a{color:#aaa} -.chat-message-text{display:block;margin-top:2px;font-size:0.9rem;white-space:pre-wrap;word-break:break-word} +.main-content.chatsux-only .main-column{max-width:100%;width:100%} +.chat-workspace{display:flex;gap:14px;align-items:stretch;min-height:72vh} +.chat-topics{width:280px;flex-shrink:0;display:flex;flex-direction:column;gap:4px;background:#161616;border:1px solid #2a2a2a;border-radius:8px;padding:8px;max-height:82vh;overflow-y:auto} +.chat-topics-head{display:flex;justify-content:space-between;align-items:center;font-weight:bold;padding:4px 6px;border:none !important;background:transparent !important} +.chat-topics-new{padding:2px 10px;line-height:1.3} +.chat-topics-title-link{font-weight:bold;color:#ffa300;text-decoration:none} +.chat-topics-title-link:hover{text-decoration:underline} +.main-column .chat-workspace-section{background:transparent !important;border:none !important;padding:0 !important;box-shadow:none !important;margin-bottom:0} +.tribe-details.chat-workspace-details{background:transparent !important;border:none !important;padding:0 !important;box-shadow:none !important} +.chat-topic{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:8px;text-decoration:none;border:none;background:transparent} +.chat-topic:hover{background:#222} +.chat-topic-active{background:#2f2510} +.chat-topic-title{font-weight:bold;color:#ffa300;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.chat-topic-meta{font-size:0.75rem;color:#888} +.chat-workspace-main{flex:1;min-width:0} +.chat-workspace-empty{display:flex;align-items:center;justify-content:center;border:1px dashed #333;border-radius:8px} +@media (max-width: 900px){.chat-workspace{flex-direction:column}.chat-topics{width:100%;max-height:220px}} +.chat-messages-list{display:flex;flex-direction:column;gap:6px} +.chat-bubble-row{display:flex;justify-content:flex-start;border:none !important;background:transparent !important;padding:0 !important} +.chat-bubble-row-self{justify-content:flex-end} +.chat-message{position:relative;max-width:85%;padding:7px 12px 5px;border-radius:14px 14px 14px 4px;background:#23242a;word-break:break-word;border:none} +.chat-message-author{background:#1f1800} +.chat-bubble-row-self .chat-message{border-radius:14px 14px 4px 14px;background:#2f2510} +.chat-bubble-sender{display:block;font-size:0.78rem;font-weight:bold;color:#7fb3ff;text-decoration:none;margin-bottom:2px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.chat-bubble-sender:hover{text-decoration:underline} +.chat-bubble-sender-owner{color:#ffa300} +.chat-bubble-time{display:block;font-size:0.68rem;color:#888;text-align:right;margin-top:2px;line-height:1} +.chat-reply-quote{display:block;border-left:3px solid #FFA500;background:rgba(255,163,0,.08);border-radius:4px;padding:2px 8px;margin-bottom:4px;font-size:.78rem;color:#ccc;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:420px} +.chat-reply-quote:hover{background:rgba(255,163,0,.16)} +.chat-reply-author{color:#7fb3ff;font-weight:bold} +.chat-replying-banner{display:flex;align-items:center;gap:6px;border:0 !important;border-left:3px solid #FFA500 !important;background:rgba(255,163,0,.08) !important;border-radius:4px;padding:4px 10px !important;margin:0 0 6px;box-shadow:none;font-size:.8rem;color:#ccc;overflow:hidden} +.chat-replying-banner .chat-reply-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1} +.chat-reply-cancel{color:#ff6b6b;text-decoration:none;font-weight:bold;margin-left:auto} +.chat-msg-actions{display:flex;flex-wrap:wrap;gap:4px;align-items:center;margin:4px 0 0;padding:0 !important;background:transparent !important;border:0 !important;border-radius:0;box-shadow:none} +.chat-react-form{display:inline;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.chat-react-btn{display:inline-flex;align-items:center;justify-content:center;font-size:.72rem;line-height:1.4;padding:1px 7px;border:1px solid rgba(128,128,128,.4);border-radius:10px;background:rgba(128,128,128,.12);color:#eee;cursor:pointer;text-decoration:none;font-family:"Noto Color Emoji","Apple Color Emoji","Segoe UI Emoji",sans-serif} +.chat-react-btn:hover{background:rgba(128,128,128,.3)} +.chat-reply-btn{margin-left:auto} +.chat-react-active{border-color:#FFA500;color:#FFA500} +.chat-message-pinned{border:1px solid #FFA500 !important;background:#332a14 !important} +.chat-pin-toggle{position:absolute;top:4px;right:6px} +.chat-has-pin-toggle{padding-right:38px} +.chat-pinned-link{margin-right:8px} +.btn-singleview .btn-icon{display:flex;align-items:center;justify-content:center;width:1em;height:1em;line-height:0} +.btn-singleview .btn-icon svg{display:block;width:100%;height:100%} +.btn-singleview .btn-count{margin-left:5px;line-height:1;font-size:.9em} +.btn-singleview:has(> .btn-icon:only-child){width:36px;min-width:36px;height:36px;padding:0;flex-shrink:0} +.btn-singleview.btn-content{background:#ffa300;color:#1e1f23;border:1px solid #ffa300} +.btn-singleview.btn-content:hover{background:#FFD700;color:#1e1f23;border-color:#FFD700} +.btn-singleview.btn-delete{color:#ff6b6b !important;border-color:rgba(255,107,107,.55) !important} +.btn-singleview.btn-delete:hover{background:rgba(255,107,107,.15) !important} +.feed-opinions-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important;margin:6px 0;box-shadow:none} +.feed-opinions-count{font-weight:bold;color:#FFA500} +.feed-votes-summary{display:flex;gap:6px;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important;margin:6px 0;box-shadow:none} +.feed-votes-summary .vote-category{font-size:.75em;padding:2px 9px;border:1px solid rgba(255,163,0,.45);border-radius:10px;background:rgba(255,163,0,.08);color:#FFD700;white-space:nowrap} +.school-invite-code-block{display:block} +.school-lesson-details{background:transparent !important;border:0 !important;padding:0 !important;margin:0;box-shadow:none} +.school-lesson-summary{cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:10px;list-style:none;font-weight:bold} +.school-lesson-summary-title{flex:1 1 auto;min-width:0} +.school-lesson-summary::-webkit-details-marker{display:none} +.school-lesson-summary-title{color:#ffa300} +.school-lesson-summary-title::after{content:" \25BE";opacity:.7} +details[open] > .school-lesson-summary .school-lesson-summary-title::after{content:" \25B4"} +.school-lesson-summary-meta{font-size:.78em;color:#888;white-space:nowrap;font-weight:normal} +.school-lesson-body{background:transparent !important;border:0 !important;padding:10px 0 0 !important;margin:0;box-shadow:none} +.school-lesson-actions{display:flex;gap:8px;align-items:center;margin-top:8px;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.feed-workspace{display:grid;grid-template-columns:230px minmax(0,1fr) 230px;gap:18px;align-items:start;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;margin:0;overflow:visible} +.feed-workspace-center{min-width:0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;margin:0;overflow:visible} +.feed-side{position:sticky;top:12px;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;margin:0;overflow:visible} +.feed-side-box{background:#1c1c1c;border:1px solid #333;border-radius:12px;padding:14px 16px;margin:0;box-shadow:none;overflow:hidden} +.feed-side-title{font-size:1em;margin:0 0 10px;color:#FFA500} +.feed-side-tags{display:flex;flex-wrap:wrap;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;margin:0;box-shadow:none} +.feed-side-users{display:flex;flex-direction:column;gap:8px;background:transparent !important;border:0 !important;padding:0 !important;margin:0;box-shadow:none} +.feed-side-user{display:flex;align-items:center;gap:8px;background:transparent !important;border:0 !important;padding:0 !important;margin:0;box-shadow:none;overflow:hidden} +.feed-side-avatar{width:28px;height:28px;border-radius:50%;object-fit:cover;flex:0 0 auto} +.feed-side-user .user-link{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px} +.chat-time-inline{margin:0 0 0 6px;line-height:1.4} +.chat-time-push{margin-left:auto} +.chat-day-separator{display:flex;justify-content:center;margin:10px 0 4px;border:none !important;background:transparent !important;padding:0 !important} +.chat-day-chip{font-size:0.72rem;padding:2px 12px;border-radius:10px;background:#1a1a1a;color:#aaa;border:1px solid #333} +.chat-message.chat-poll{max-width:92%} +.chat-message-text{display:block;font-size:0.9rem;white-space:pre-wrap;word-break:break-word} .calendar-note-card{position:relative;padding:10px 80px 10px 10px;min-height:56px} .calendar-note-delete{position:absolute;top:8px;right:8px;margin:0} .calendar-note-delete button{padding:4px 10px;font-size:0.8rem} @@ -5529,6 +6102,73 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .pad-readonly-colored{background:#fff;color:#111;padding:12px;border-radius:4px;min-height:120px;white-space:pre-wrap;word-break:break-word;font-family:inherit;font-size:.95rem;border:1px solid #aaa;margin-bottom:10px} .pad-author-span{padding:1px 2px;border-radius:2px} .pad-members-section{margin-top:12px;border:none} +.feed-error-msg{margin:8px 0 12px;padding:10px 14px;border-left:4px solid #e74c3c;background:rgba(231,76,60,.12);border-radius:6px} +.feed-error-msg p{margin:0} +.pad-workspace{display:flex;flex-direction:column;gap:14px;width:100%} +.pad-workspace-main{min-width:0;width:100%} +.pad-workspace-side{display:flex;flex-direction:column;gap:14px} +.pad-editor{width:100%;box-sizing:border-box;min-height:62vh;background:#fff;color:#111;border:1px solid #aaa;border-radius:4px;padding:12px;font-family:monospace;font-size:.95rem;line-height:1.45;resize:vertical} +.pad-editor-bar{display:flex;justify-content:flex-end;align-items:center;gap:10px;margin-top:8px} +.pad-page .tribe-main .pad-document{width:100%;box-sizing:border-box} +.pad-editor-hint{color:#888;font-size:.8rem} +.pad-attribution{margin-top:10px} +.pad-attribution summary{cursor:pointer;color:#aaa;font-size:.8rem;text-transform:uppercase;letter-spacing:1px;padding:4px 0} +.pad-document{background:#fff;color:#111;padding:14px;border-radius:4px;min-height:40vh;white-space:pre-wrap;word-break:break-word;font-family:monospace;font-size:.95rem;line-height:1.45} +.pad-users h4,.pad-history h4{margin:0 0 6px 0;color:#aaa;font-size:.8rem;text-transform:uppercase;letter-spacing:1px} +.pad-user-item{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:.9rem;min-width:0} +.pad-user-item a{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.pad-history{max-height:40vh;overflow:auto} +.pad-history-item{width:100%;box-sizing:border-box} +.pad-line-placeholder{color:#888;font-style:italic} +.pad-page .tribe-side .pad-meta{flex-basis:100%} +.pad-history-item{display:flex;align-items:center;gap:6px;padding:4px 0;font-size:.85rem;border-bottom:1px solid rgba(128,128,128,.15);min-width:0} +.pad-history-item .pad-version-link{color:#aaa;text-decoration:none;font-family:monospace;white-space:nowrap} +.pad-history-item .pad-version-link.active{color:#FFA500} +.pad-history-item a.user-link{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.tribe-details.pad-page{flex-direction:column;gap:16px} +.pad-page .tribe-side{width:100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:10px 18px;padding:12px 16px;box-sizing:border-box} +.pad-page .tribe-side > *{margin:0} +.pad-page .tribe-side > div:not(.tribe-info-table),.pad-page .tribe-side > form{border:none;background:transparent;padding:2px} +.pad-page .tribe-side .tribe-side-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap} +.pad-page .tribe-side .card-header{border:none;background:transparent;padding:0} +.pad-page .tribe-side .shop-title-row{border:none;background:transparent;padding:0} +.pad-page .tribe-side .tribe-card-title{margin:0} +.pad-page .tribe-side .tribe-info-table{margin:0} +.pad-page .tribe-side form{display:inline-block} +.pad-page .tribe-main{width:100%;border:none;background:transparent;padding:0} +.pad-page .tribe-main,.pad-page .pad-workspace,.pad-page .pad-workspace div{padding:0;margin:0;border:none;border-radius:0;background:transparent;box-shadow:none;overflow:visible;box-sizing:border-box} +.pad-page .pad-workspace .pad-editor-wrap{display:flex;width:100%;background:#fff;border:1px solid #aaa;border-radius:4px;overflow:hidden} +.pad-page .pad-workspace .pad-gutter{padding:12px 8px 12px 10px;background:#f4f4f4;border-right:1px solid #ddd} +.pad-page .pad-workspace .pad-document{width:100%;padding:12px 14px 12px 4px;background:#fff;border-radius:4px;min-height:40vh} +.pad-page .pad-workspace .pad-readonly-colored{padding:12px 14px 12px 4px;background:#fff;border-radius:4px} +.pad-page .pad-workspace .pad-workspace-side > div{border:1px solid #333;background:#1A1A1A;border-radius:8px;padding:10px 12px} +.pad-page .pad-workspace .pad-history-item{padding:4px 0;border-bottom:1px solid rgba(128,128,128,.15)} +.pad-page .pad-workspace .pad-editor-bar{margin-top:8px} +.pad-page .pad-workspace .pad-editor-wrap .pad-editor{margin:0;min-height:0;padding:12px;line-height:1.45;font-size:.95rem} +.pad-page .pad-workspace .pad-attribution{margin-top:10px} +.pad-workspace .pad-document,.pad-workspace .pad-readonly-colored{background:#fff} +.pad-meta{display:flex;align-items:center;gap:6px 10px;flex-wrap:wrap;font-size:.9em} +.pad-meta .user-link{display:inline-block;width:auto} +.pad-page .tribe-side .pad-members-details{flex-basis:100%;border:none;background:transparent;padding:0} +.pad-members-details summary{cursor:pointer;display:inline-block} +.pad-members-chips{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0 2px;max-height:150px;overflow:auto;border:none;background:transparent} +.pad-page .tribe-side div form{border:none;background:transparent;padding:2px} +.pad-member-chip{display:inline-flex;align-items:center;gap:6px;font-size:.9em;min-width:0} +.supporters-list{display:flex;flex-wrap:wrap;gap:8px 12px;margin:0;padding:0 !important;background:transparent !important;border:0 !important;box-shadow:none} +.pad-member-chip .user-link{display:inline-block;width:auto;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.pad-workspace-main form{display:block;width:100%;margin:0} +.pad-editor-wrap{display:flex;align-items:stretch;width:100%;box-sizing:border-box;background:#fff;border:1px solid #aaa;border-radius:4px;overflow:hidden} +.pad-gutter{flex:0 0 auto;padding:12px 8px 12px 10px;font-family:monospace;font-size:.95rem;line-height:1.45;color:#999;text-align:right;white-space:pre;user-select:none;border-right:1px solid #ddd;background:#f4f4f4} +.pad-editor-wrap .pad-editor{flex:1 1 0;min-width:0;width:100%;border:none;border-radius:0;min-height:62vh;resize:vertical;overflow:auto} +.pad-lines,.pad-document table.pad-lines,.pad-readonly-colored table.pad-lines{border-collapse:collapse;width:100%;background:transparent;border:none;margin:0;box-shadow:none} +.pad-lines tr,.pad-lines tr:nth-child(even),.pad-lines tr:nth-child(odd),.pad-lines td,.pad-lines th{background:transparent !important;border:none !important;padding:0;vertical-align:top;color:#111} +.pad-lines .pad-line-num{text-align:right;padding:0 10px 0 2px;width:1%;min-width:40px;white-space:nowrap;user-select:none;border-right:1px solid #ddd;font-family:monospace;line-height:1.45} +.pad-lines .pad-line-num a{color:#999;text-decoration:none} +.pad-lines tr:target .pad-line-num a{color:#FFA500;font-weight:700} +.pad-lines .pad-line-text{padding-left:12px;white-space:pre-wrap;word-break:break-word;font-family:monospace;line-height:1.45;min-height:1.45em} +.pad-workspace .pad-document{padding:12px 14px 12px 4px} +.pad-workspace .pad-readonly-colored{padding:12px 14px 12px 4px;color:#111} +@media (max-width:720px){.pad-workspace{grid-template-columns:1fr}.pad-editor{min-height:50vh}.pad-history{max-height:30vh}} .pad-members-below-tags{margin-top:12px;border:none} .pad-member-card{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:0.85rem} .pad-members-grid{display:flex;flex-direction:column;gap:4px;margin-bottom:12px} @@ -5558,6 +6198,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .game-iframe-ecoinflow{height:95vh} .game-iframe-neoninfiltrator{height:95vh} .game-iframe-audiopendulum{height:95vh} +.game-iframe-visualdj{height:95vh} .game-iframe-flipflop{height:720px} .game-iframe-rockpaperscissors{height:580px} .game-iframe-tiktaktoe{height:580px} @@ -5584,20 +6225,24 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .torrent-table{width:100%;border-collapse:collapse;margin-top:12px} .torrent-table th,.torrent-table td{padding:8px 12px;text-align:left} +.torrent-table td:has(.filter-btn){width:1%;white-space:nowrap} +.torrent-table td:nth-child(2){width:auto;overflow-wrap:anywhere} +.torrent-table th:nth-child(1),.torrent-table td:nth-child(1),.torrent-table td:nth-child(3),.torrent-table td:nth-child(4),.torrent-table td:nth-child(5){white-space:nowrap;width:1%} +.torrent-table td .filter-btn{margin:0} .torrent-table th{background:#1a1a1a;color:#FFA500;font-weight:bold} -.torrent-download{margin:12px 0} -.torrent-download .filter-btn{border:none} +.torrent-download{margin:12px 0;background:transparent;border:0;padding:0;box-shadow:none} .logs-table{width:100%;border-collapse:collapse;margin-top:12px} .logs-table th,.logs-table td{padding:8px 12px;text-align:left;vertical-align:middle;border-bottom:1px solid #222} -.logs-col-actions{vertical-align:middle;text-align:center;white-space:nowrap;background:transparent} -.logs-col-actions form{margin:0;display:inline-block} -.logs-col-actions .filter-btn{min-width:110px} +.logs-col-actions{vertical-align:middle;text-align:right;white-space:nowrap;background:transparent;width:1%} +.logs-row-actions{display:flex;gap:8px;justify-content:flex-end;align-items:center;border:0 !important;background:transparent !important;padding:0 !important} +.logs-col-actions form{margin:0;display:inline-flex} +.logs-col-actions .filter-btn{min-width:0;white-space:nowrap} .logs-table th{background:#1a1a1a;color:#FFA500;font-weight:bold} -.logs-col-date{white-space:nowrap;text-align:center;font-family:monospace;background:transparent} +.logs-col-date{white-space:nowrap;text-align:left;font-family:monospace;background:transparent;width:1%} .logs-date-day{color:#FFA500;font-weight:bold;font-size:0.9rem} .logs-date-time{color:#ddd;font-size:0.85rem} -.logs-col-log{max-width:560px;vertical-align:middle;background:transparent;padding:8px 12px} +.logs-col-log{width:100%;vertical-align:middle;background:transparent;padding:8px 12px;overflow-wrap:anywhere} .logs-col-log>*{background:transparent;border:0;padding:0;margin:0} .logs-entry-label{color:#FFA500;font-weight:bold;margin-bottom:4px !important;background:transparent;border:0} .logs-entry-meta{color:#888;font-size:0.85rem;margin-bottom:6px} @@ -5608,7 +6253,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .logs-mode-form{margin:12px 0} .logs-mode-group{display:flex;gap:10px;align-items:center;flex-wrap:wrap} .logs-ai-disabled{color:#e74c3c;font-style:italic;margin:8px 0} -.logs-col-type{white-space:nowrap;text-align:center;font-family:monospace;background:transparent} +.logs-col-type{white-space:nowrap;text-align:left;font-family:monospace;background:transparent;width:1%} .logs-type-text{font-weight:bold;letter-spacing:0.5px;text-transform:uppercase;font-family:monospace;font-size:0.85rem} .logs-type-ai{color:#00b4ff} .logs-type-manual{color:#FFA500} @@ -5652,7 +6297,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .stats-link-break{color:#ffa500;text-decoration:none;word-break:break-all} .stats-muted{color:#aaa} .stats-strong{color:#ffd700;font-weight:600} -.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:16px;margin-bottom:24px} +.stats-grid{display:flex;flex-wrap:wrap;gap:6px 28px;margin-bottom:6px;background:transparent !important;border:0 !important;box-shadow:none;padding:0 !important} .stats-mode-row{justify-content:flex-start;margin-bottom:24px} .stats-section-h{font-size:18px;color:#ffa500;margin:8px 0;font-weight:600} .stats-list-reset{list-style-type:none;padding:0;margin:0} @@ -5672,6 +6317,8 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .invites-pubs-table td:nth-child(4){width:30%} .invites-pubs-table tr:first-child td{white-space:nowrap} .stats-block{background:none;padding:0;border:none;border-radius:0;margin-bottom:16px} +.stats-block.stats-block-boxed{background:#1c1c1c;border:1px solid #333;border-radius:10px;padding:14px 16px;margin-bottom:16px} +.stats-block.stats-block-boxed h2{margin-top:0} .stats-block h2{margin:0 0 10px;font-size:16px;color:#ffa500;font-weight:600} .stats-block ul{margin:6px 0 0;padding-left:18px} .stats-block ul li{margin:3px 0;color:#ddd;font-size:14px} @@ -5687,8 +6334,18 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .stats-toplist .stats-toplist-name{flex:0 0 auto;color:#FFDD44;font-size:13px;min-width:120px} .stats-toplist .stats-toplist-num{flex:0 0 auto;color:#FFDD44;font-size:13px;min-width:36px;text-align:right} .peer-key{word-break:break-all;font-size:12px} +.peers-list .block-info-table{table-layout:fixed;width:100%} +.viewer-hidden-block .block-info-table{width:100%}.viewer-hidden-block .block-info-table td{overflow-wrap:anywhere}.viewer-hidden-block .block-info-table td:nth-child(2),.viewer-hidden-block .block-info-table td:nth-child(3){white-space:nowrap;width:1%} +.peers-list .block-info-table td{overflow-wrap:anywhere} +.peers-list .block-info-table td:nth-child(1){width:18%} +.peers-list .block-info-table td:nth-child(2){width:47%} +.peers-list .block-info-table td:nth-child(3){width:12%} +.peers-list .block-info-table td:nth-child(4){width:8%} +.peers-list .block-info-table td:nth-child(5){width:15%} .peers-technical-block{margin-top:16px} .peers-conn-actions{margin-bottom:12px} +.activity-toolbar-row .peers-conn-actions{margin:0;padding:0;border:0;background:transparent;box-shadow:none;display:flex;gap:6px;flex-wrap:wrap;margin-left:auto} +.activity-toolbar-row .peers-conn-actions form{margin:0} .invites-pubs-actions{margin:12px 0} .larp-cycle-banner{display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:#1A1A1A;border:1px solid #FFA500;border-radius:6px;padding:8px 14px;margin:12px 0;font-family:monospace} .larp-post-submit{width:auto;display:inline-block;padding:6px 18px;align-self:flex-start} @@ -5699,12 +6356,14 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .larp-my-house{display:flex;align-items:center;gap:8px;margin:8px 0 16px 0;background:transparent !important;border:0 !important;padding:0 !important} .larp-my-house-label{color:#FFD700;font-size:12px;text-transform:uppercase;letter-spacing:1px} .larp-my-house-link{color:#FFA500;font-weight:700} -.larp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;margin-top:12px} +.larp-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-top:12px;align-items:stretch} +.larp-card{display:flex;flex-direction:column} +.larp-card .larp-card-body{flex:1 1 auto;display:flex;flex-direction:column} .larp-card{background:#1A1A1A;border:1px solid #333;border-radius:8px;overflow:hidden;display:flex;flex-direction:column} .larp-card-mine{border:3px solid #FFA500} .larp-card-ruling{border:3px solid #ff6b6b} -.larp-card-image-link{display:block} -.larp-card-image{width:100%;height:160px;object-fit:cover;display:block;background:#0a0a0a} +.larp-card-image-link{display:block;width:100%} +.larp-card-image{width:100%;aspect-ratio:16/10;height:auto;object-fit:cover;object-position:center;display:block;background:#0a0a0a} .larp-card-body{padding:10px 12px;display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important} .larp-card-title-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:transparent !important;border:0 !important;padding:0 !important} .larp-card-title{color:#FFA500;font-weight:700;font-size:16px;letter-spacing:0.5px} @@ -5722,8 +6381,10 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .larp-detail-body{flex:1 1 320px;display:flex;flex-direction:column;gap:10px;background:transparent !important;border:0 !important;padding:0 !important} .larp-info-table{width:100%;border-collapse:collapse} .larp-info-table td{padding:4px 8px;border-bottom:1px solid #2a2a2a;vertical-align:top} -.larp-detail-description{color:#ccc;line-height:1.5;margin:0} -.larp-actions{margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-wrap:wrap;gap:8px;align-items:center} +.larp-detail-description{color:#FFDD44;line-height:1.6;font-size:15px;margin:0} +.larp-actions{margin-top:12px;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-wrap:wrap;gap:8px;align-items:center} +.larp-actions form{margin:0;display:inline-flex;align-items:center} +.larp-actions .filter-btn,.larp-actions button{height:36px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;text-decoration:none;margin:0;padding:6px 16px;font-family:inherit;font-size:inherit;line-height:1.2;vertical-align:middle} .larp-members-block{margin-top:24px} .larp-members-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .larp-member-row{padding:4px 8px;background:#1A1A1A;border:1px solid #333;border-radius:4px} @@ -5748,9 +6409,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .larp-post-badge{display:inline-block;padding:2px 8px;border-radius:10px;background:#3a2300;color:#FFD700;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px} .larp-post-text{color:#ddd;line-height:1.5;margin:0;white-space:pre-wrap;word-break:break-word} .larp-rules{margin-top:14px;display:flex;flex-direction:column;gap:18px;background:transparent !important;border:0 !important;padding:0 !important} -.larp-rules-section{background:#1A1A1A;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} -.larp-rules-section h3{color:#FFA500;margin:0 0 6px 0;font-size:15px} -.larp-rules-section p{color:#ccc;line-height:1.55;margin:6px 0} .larp-rules-list{display:flex;flex-direction:column;gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .larp-rules-list strong{color:#FFD700} .lan-checkbox-label{display:inline-flex;align-items:center;gap:10px;color:#FFD700;font-size:14px;margin:10px 0;background:transparent !important;border:0 !important;padding:0 !important;cursor:pointer} @@ -5758,6 +6416,50 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .lan-checkbox-text{line-height:1} .larp-sign-block{display:block;line-height:0;margin:0 auto;background:transparent !important;border:0 !important;padding:0 !important;width:180px} .profile-sensors-box .larp-sign-block{margin-top:14px} +.wallet-line{display:flex;flex-direction:row;align-items:center;justify-content:center;flex-wrap:wrap;gap:6px;padding:0.2rem 0.6rem !important;border:1px solid #555 !important;border-radius:4px;background-color:#1A1A1A !important;font-size:0.9rem;color:#FFDD44;margin:0;box-shadow:none;overflow:visible} +.profile-sensors-box .wallet-line a,.inhabitant-left .wallet-line a{color:#FFDD44;text-decoration:none} +.profile-sensors-box .wallet-line a:hover,.inhabitant-left .wallet-line a:hover{color:#ffa500;text-decoration:underline} +.wallet-line-head{display:inline-flex;align-items:center;font-weight:700;font-size:1em} +.wallet-icon{color:#4fc3f7;font-size:1.1em} +.phone-number-icon{color:#ffa500;font-size:1.1em} +.wallet-address{display:inline;font-family:monospace;font-size:.85em;font-weight:700;color:#FFDD44;word-break:break-all;background:transparent;border-radius:0;padding:0;cursor:copy;-webkit-user-select:all;user-select:all} +.wallet-line-link{text-decoration:none;color:inherit} +.wallet-transfer-ctx{display:flex;flex-direction:column;gap:6px;background:transparent !important;border:0 !important;border-radius:0;padding:0 !important;margin:10px 0;box-shadow:none} +.wallet-transfer-ctx-line{display:flex;flex-wrap:wrap;align-items:center;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.wallet-transfer-ctx-label{font-weight:700;font-size:.85em;letter-spacing:.5px;text-transform:uppercase} +.ubi-status{display:flex;flex-direction:column;gap:8px;background:#1e1f23 !important;border:1px solid #333 !important;border-radius:10px;padding:12px 14px !important;margin:0 0 14px} +.ubi-status-line{display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.ubi-status-note{font-size:.85em;opacity:.75} +.ubi-status a{text-decoration:none} +.wallet-chip-link,.wallet-chip-link:hover{text-decoration:none} +.module-header-line .wallet-chip-link{display:inline-flex} +.addr-cards.ubi-pub-cards{grid-template-columns:repeat(3,1fr)} +.ubi-pub-summary{margin:0;padding:0 !important;background:transparent !important;border:0 !important;box-shadow:none} +.ubi-pub-summary .bank-info-table{width:100%;margin:0} +.ubi-pub-summary .bank-info-table td{padding:6px 8px;font-size:.9em;letter-spacing:0;white-space:nowrap} +.ubi-pub-summary .bank-info-table td.card-value{width:1%} +.ubi-tick-ok{color:#7fff7f;font-weight:700} +.ubi-tick-bad{color:#ff8a8a;font-weight:700} +.bank-addr-search{margin-bottom:14px} +.bank-addresses{width:100%;border-collapse:collapse} +.bank-addresses th,.bank-addresses td{text-align:left;padding:8px 10px;border-bottom:1px solid #333;vertical-align:middle} +.bank-addresses th{font-size:.85em;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap} +.bank-addresses .delete-btn{white-space:nowrap} +.bank-addresses td:last-child,.bank-addresses td:nth-child(4){white-space:nowrap} +.bank-addresses td:nth-child(3){min-width:290px} +.bank-address-code{font-family:monospace;font-size:1em;word-break:break-all} +.bank-summary .module-stats-line{margin:0 0 10px;padding:0 !important;background:transparent !important;border:0 !important;box-shadow:none;display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:.72em;white-space:nowrap;text-transform:uppercase} +.bank-addresses .addr-del{margin:0;padding:0;background:transparent !important;border:0 !important;box-shadow:none} +.ubi-pub-card form{margin:0;padding:0;background:transparent !important;border:0 !important;box-shadow:none} +@media (max-width:900px){.addr-cards.ubi-pub-cards{grid-template-columns:1fr}} +.ubi-pub-card .ubi-status-line{gap:6px} +.wallet-line-link:hover .wallet-address{color:#ffa500;text-decoration:underline} +.addr-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px} +.addr-card{display:flex;flex-direction:column;gap:10px;background:#1e1f23 !important;border:1px solid #333 !important;border-radius:10px;padding:14px !important;margin:0;box-shadow:none;overflow:visible} +.addr-card-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.addr-card .wallet-line{margin:0} +.addr-card-owner{margin:0} +.addr-card .addr-del{margin:0;padding:0;background:transparent;border:0;box-shadow:none;display:flex;justify-content:flex-end} .larp-sign-large{width:180px;height:180px;object-fit:cover;border-radius:8px;border:2px solid #FFA500;display:block} .larp-sign-mini-block{display:inline-block;line-height:0;margin:6px 0;background:transparent !important;border:0 !important;padding:0 !important;width:64px} .larp-sign-mini{width:64px;height:64px;object-fit:cover;border-radius:50%;border:2px solid #FFA500;display:block} @@ -5771,9 +6473,9 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .larp-last-attempt-fail{color:#ff8a8a;font-weight:700} .larp-test-form-compact{margin:10px 0 0 0;gap:8px} .larp-test-form-compact .larp-test-question{padding:8px 12px} -.larp-test-form-compact .larp-test-q-text{font-size:13px;margin:0 0 6px 0} -.larp-test-form-compact .larp-test-option{font-size:12px;padding:2px 6px} -.larp-will-test-hint{color:#bbb;font-size:13px;line-height:1.4;margin:4px 0 10px 0} +.larp-test-form-compact .larp-test-q-text{font-size:15px;margin:0 0 8px 0} +.larp-test-form-compact .larp-test-option{font-size:14px;padding:4px 6px} +.larp-will-test-hint{color:#FFA500;opacity:.85;font-size:14px;line-height:1.5;margin:4px 0 12px 0} .larp-academia-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:12px;background:transparent !important;border:0 !important;padding:0 !important} .larp-invite-redeem-form,.larp-invite-create-form,.larp-card-invite-form{display:inline-flex;gap:6px;align-items:center;background:transparent !important;border:0 !important;padding:0 !important;margin:0} .larp-invite-redeem-form input[type=text],.larp-card-invite-form input[type=text]{padding:4px 8px;font-family:monospace;font-size:12px;min-width:280px;width:100%;max-width:360px;box-sizing:border-box} @@ -5784,11 +6486,15 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .larp-invite-banner-code{color:#9aff9a;font-family:monospace;font-size:15px;letter-spacing:1px;margin:0;word-break:break-all} .larp-test-ranking{margin-top:14px;background:transparent !important;border:0 !important;padding:0 !important} .larp-test-ranking-title{color:#FFA500;font-size:15px;margin:0 0 8px 0} -.larp-academia-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;background:transparent !important;border:0 !important;padding:0 !important} -.larp-academia-thumb{display:flex;flex-direction:column;align-items:center;gap:6px;text-decoration:none;background:#1A1A1A;border:1px solid #333;border-radius:8px;padding:10px;transition:border-color 0.2s} +.larp-academia-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;background:transparent !important;border:0 !important;padding:0 !important} +.larp-academia-thumb{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:6px;text-decoration:none;background:#1A1A1A;border:1px solid #333;border-radius:8px;padding:10px;transition:border-color 0.2s} .larp-academia-thumb:hover{border-color:#FFA500} .larp-academia-thumb-mine{border:3px solid #FFA500} -.larp-academia-thumb-image{width:100%;max-width:128px;height:128px;object-fit:cover;border-radius:6px;border:1px solid #FFA500;display:block} +.larp-academia-thumb-image{width:100%;max-width:72px;aspect-ratio:1;height:auto;object-fit:cover;border-radius:6px;border:1px solid #FFA500;display:block;margin:0 auto} +.larp-academia-thumb-table{margin-top:2px} +.larp-academia-thumb-table td{padding:2px 6px;font-size:12px} +.larp-academia-thumb-table .card-label{color:#FFA500;white-space:nowrap} +.larp-academia-thumb-table .card-value{color:#ccc} .larp-academia-thumb-name{color:#FFA500;font-weight:700;font-size:14px;text-transform:uppercase;letter-spacing:0.5px;text-align:center} .larp-test-header{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start;margin:14px 0;background:transparent !important;border:0 !important;padding:0 !important} .larp-test-image{width:180px;height:180px;object-fit:cover;border-radius:8px;border:2px solid #FFA500;display:block;flex:0 0 180px} @@ -5800,10 +6506,10 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .larp-test-cooldown p{color:#FFD700;margin:0 0 8px 0;font-size:14px} .larp-test-form{margin:14px 0;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-direction:column;gap:14px} .larp-test-question{background:#1A1A1A;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} -.larp-test-q-text{color:#ddd;line-height:1.5;margin:0 0 10px 0;font-size:14px} -.larp-test-q-text strong{color:#FFA500} +.larp-test-q-text{color:#FFDD44;line-height:1.5;margin:0 0 10px 0;font-size:15px;font-weight:600} +.larp-test-q-text strong{color:#FFDD44} .larp-test-options{display:flex;flex-direction:column;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;align-items:flex-start} -.larp-test-option{color:#ccc;font-size:13px;display:inline-flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;cursor:pointer;padding:4px 6px;border-radius:3px;width:auto;max-width:100%} +.larp-test-option{color:#FFA500;font-size:14px;display:inline-flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;cursor:pointer;padding:4px 6px;border-radius:3px;width:auto;max-width:100%} .larp-test-option:hover{background:#222} .larp-test-option input[type=radio]{accent-color:#FFA500;margin:0;flex:none;width:16px;height:16px} .larp-test-result{background:#1A1A1A;border:1px solid #FFA500;border-radius:8px;padding:18px 22px;margin:14px 0} @@ -5818,10 +6524,17 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .inbox-exposition{margin:12px 0 4px 0;display:flex;flex-wrap:wrap;align-items:center;gap:8px} .inbox-filters-label{color:#FFDD44;font-size:13px;font-weight:600;align-self:center} .inbox-vis-toggle{background:transparent !important;border:0 !important;padding:0 !important;display:inline-block} +.inbox-bot-filters{margin-top:-4px} +.pm-subject-cell{display:flex;align-items:center;gap:8px;flex-wrap:wrap} +.pm-unread-chip{background:#3a2a00;color:#FFA500;border-color:#FFA500} +.inbox-bot-label{display:flex;margin:6px 0} +.pm-title-link{color:inherit;text-decoration:none} +.mention-actions{margin-top:8px} .suggested-badge{display:inline-block;background:#FFA500;color:#000;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;width:fit-content} .peers-import-form{margin-top:16px;display:flex;flex-direction:column;gap:8px;align-items:flex-start} -.peers-import-form textarea{width:100%;max-width:720px;font-family:monospace;font-size:12px} +.peers-import-form textarea{width:100%;max-width:720px;box-sizing:border-box;font-family:monospace;font-size:12px} .peers-import-label{color:#FFDD44;font-size:13px;font-weight:600} +.invites-used-table td:nth-child(1),.invites-used-table td:nth-child(3){width:1%;white-space:nowrap} .graphos-canvas{width:100%;max-width:1100px;margin:12px auto;padding:8px;background:transparent} .graphos-svg{width:100%;height:auto;min-height:480px;display:block} .graphos-edge{stroke:#666;stroke-width:1;opacity:.55} @@ -5829,7 +6542,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .graphos-edge-discovered{stroke:#ffd700;stroke-width:1.5;opacity:.75} .graphos-edge-unknown{stroke:#888;stroke-width:1;stroke-dasharray:4 4;opacity:.55} .graphos-node-circle{stroke:#1a1a1a;stroke-width:2;transition:stroke-width .15s,filter .15s} -.graphos-node-circle-me{fill:#ffa500;stroke:#cc7700;stroke-width:3} +.graphos-node-circle-me{fill:#3498db;stroke:#1f6391;stroke-width:3} .graphos-node-circle-online{fill:#2ecc71;stroke:#1e8449} .graphos-node-circle-discovered{fill:#ffd700;stroke:#b8860b} .graphos-node-circle-unknown{fill:#888;stroke:#555} @@ -5837,17 +6550,51 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .graphos-node-link:hover .graphos-node-circle{stroke-width:4;filter:brightness(1.15)} .graphos-node-link:hover .graphos-node-label{font-weight:600} .graphos-node-label{font-size:12px;fill:#ddd;text-anchor:middle;pointer-events:none} -.graphos-node-label-me{font-weight:600;fill:#ffa500} -.graphos-legend{display:flex;gap:18px;flex-wrap:wrap;align-items:center;font-size:13px;color:#ddd;margin:0 0 8px} +.graphos-node-label-me{font-weight:600;fill:#5dade2} +.graphos-legend{display:flex;gap:18px;flex-wrap:wrap;align-items:center;justify-content:center;font-size:14px;color:#ddd;margin:14px 0 8px} +.graphos-stats .stats-grid{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:18px} +.graphos-stats .stats-kpi{background:#1c1c1c;border:1px solid #444;border-radius:8px;padding:12px 14px;box-shadow:0 1px 3px rgba(0,0,0,0.35)} +.graphos-stats .stats-kpi-label{font-size:14px} +.graphos-stats .stats-kpi-inline .stats-kpi-value{font-size:20px} .graphos-legend-item{display:flex;align-items:center;gap:6px} .graphos-legend-dot{display:inline-block;width:12px;height:12px;border-radius:50%;border:2px solid #1a1a1a} -.graphos-legend-dot.graphos-node-circle-me{background:#ffa500;border-color:#cc7700} +.graphos-legend-dot.graphos-node-circle-me{background:#3498db;border-color:#1f6391} .graphos-legend-dot.graphos-node-circle-online{background:#2ecc71;border-color:#1e8449} .graphos-legend-dot.graphos-node-circle-discovered{background:#ffd700;border-color:#b8860b} .graphos-legend-dot.graphos-node-circle-unknown{background:#888;border-color:#555} -.stats-kpi{padding:8px 4px;display:flex;flex-direction:column;gap:4px;background:transparent;border:none} -.stats-kpi-label{color:#ffa500;font-size:12px;font-weight:600;letter-spacing:.3px} -.stats-kpi-value{color:#ffd700;font-weight:600;font-size:22px;line-height:1.1;word-break:break-word} +.graphos-fed-connected{fill:#2ecc71;stroke:#1e8449} +.graphos-fed-available{fill:#ffd700;stroke:#b8860b} +.graphos-fed-unreachable{fill:#7f8c8d;stroke:#4d5656} +.graphos-fed-unknown{fill:#444;stroke:#666} +.graphos-fed-outdated-ring{fill:none;stroke:#e74c3c;stroke-width:3} +.graphos-fed-isolated-ring{fill:none;stroke:#e74c3c;stroke-width:1.5;stroke-dasharray:3 3} +.graphos-fed-link{stroke-width:1.5;opacity:.8} +.graphos-fed-mutual{stroke:#ffa500} +.graphos-fed-oneway{stroke:#ffa500;stroke-dasharray:5 5;opacity:.5} +.graphos-fed-me-live{stroke:#2ecc71;stroke-width:2.5;opacity:.9} +.graphos-fed-me-follow{stroke:#888;stroke-width:1;stroke-dasharray:2 4;opacity:.6} +.graphos-fed-tor{font-size:10px;fill:#c9a0ff;font-weight:700;pointer-events:none} +.graphos-legend-dot.graphos-fed-dot-connected{background:#2ecc71;border-color:#1e8449} +.graphos-legend-dot.graphos-fed-dot-available{background:#ffd700;border-color:#b8860b} +.graphos-legend-dot.graphos-fed-dot-unreachable{background:#7f8c8d;border-color:#4d5656} +.graphos-legend-dot.graphos-fed-dot-outdated{background:transparent;border:3px solid #e74c3c} +.graphos-legend-line{display:inline-block;width:22px;height:0;border-top:2px solid #ffa500} +.graphos-legend-line.graphos-fed-line-oneway{border-top-style:dashed;opacity:.6} +.graphos-fed-table{margin-top:16px} +.stats-kpi{padding:4px 0 !important;margin:0 !important;display:inline-flex !important;flex-direction:row !important;flex-wrap:nowrap;align-items:baseline !important;gap:6px;background:transparent !important;border:none !important;width:auto !important} +.stats-kpi-label{color:#ffa500;font-size:15px;font-weight:600;letter-spacing:.3px;padding:0 !important;margin:0 !important;background:transparent !important;border:0 !important;width:auto !important;flex:0 0 auto;white-space:nowrap} +.stats-kpi-label::after{content:':'} +.stats-kpi-value{color:#ffd700;font-weight:600;font-size:15px;line-height:1.1;word-break:break-word;padding:0 !important;margin:0 !important;background:transparent !important;border:0 !important;width:auto !important;flex:0 0 auto} +.ai-ask-row{display:flex;flex-wrap:nowrap;align-items:center;justify-content:center;gap:18px;width:100%;max-width:960px;min-width:0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.ai-ask-row .ai-ask-nav{flex:0 0 auto} +.ai-ask-row .ai-ask-form{flex:1 1 260px;min-width:180px;max-width:480px} +.ai-ask-row .ai-suggestion-banner{flex:0 1 auto;min-width:0;justify-content:flex-start;border:0 !important;background:transparent !important;padding:0 !important;margin:0;white-space:nowrap;overflow:hidden} +.ai-ask-row .ai-suggestion-banner .update-banner-link{max-width:28vw} +.ai-ask-nav ul{display:flex;margin:0;padding:0;list-style:none} +.ai-ask-nav ul li{margin:0} +.header .top-bar-center{align-self:center;margin:0 0 20px 0 !important;padding:0 !important;box-shadow:none;overflow:visible} +.header .ai-ask-row{margin:0 !important;overflow:visible} +.header .ai-ask-row .ai-suggestion-banner{margin:0 !important} .stats-kpi-bar{margin-top:6px} .stats-activity-totals{display:flex;gap:24px;flex-wrap:wrap;color:#ffa500;font-size:13px;margin-top:8px} .stats-activity-totals strong{color:#ffd700} @@ -5870,7 +6617,9 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .prefs-card .tags-header{margin:0 0 10px 0} .prefs-card .tags-header h2{font-size:16px;color:#FFA500;margin:0 0 4px 0} .prefs-help{color:#FFD700;font-size:12px;margin:0} -.pref-pill-row{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px} +.pref-pill-row{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:8px;margin-top:8px} +@media (max-width:1100px){.pref-pill-row{grid-template-columns:repeat(4,minmax(0,1fr))}} +@media (max-width:640px){.pref-pill-row{grid-template-columns:repeat(2,minmax(0,1fr))}} .pref-pill{display:inline-flex;align-items:center;border:1px solid #333;padding:6px 12px;cursor:pointer;background:#161616;color:#888;font-size:13px;user-select:none} .pref-pill:hover{border-color:#555;color:#bbb} .pref-pill-input{position:absolute;opacity:0;width:0;height:0;pointer-events:none} @@ -5929,25 +6678,34 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; } .inhabitant-stats-grid{ display:grid; - grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); + grid-template-columns:repeat(2,minmax(0,1fr)); gap:6px; } .inhabitant-stat{ display:flex; justify-content:space-between; align-items:center; - gap:8px; - padding:5px 10px; + gap:4px; + min-width:0; + padding:4px 7px; border:1px solid rgba(128,128,128,.35); border-radius:6px; + white-space:nowrap; } .inhabitant-stat-label{ - font-size:12px; + font-size:11px; opacity:.85; + overflow:hidden; + text-overflow:ellipsis; +} +.inhabitant-stat-value{ + flex:0 0 auto; + font-size:13px; } .inhabitant-stat-value{ font-weight:700; } +.profile-side-actions{align-items:center} .profile-side-actions .btn{ background-color:#FFA500; color:#000; @@ -6003,7 +6761,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .inhabitant-left .gpg-line a{color:#FFDD44;text-decoration:none} a.chip-link{text-decoration:none;display:inline-block} a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} -.gpg-edit-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:4px} +.gpg-edit-row{display:flex;flex-direction:column;align-items:flex-start;gap:10px;flex-wrap:wrap;margin-top:4px} .gpg-edit-row .gpg-remove-btn{background:transparent;border:1px solid #FF6B6B;color:#FF6B6B;cursor:pointer;padding:4px 10px;border-radius:4px;font-family:inherit;font-size:0.9rem} .gpg-edit-row .gpg-remove-btn:hover{background:#FF6B6B;color:#000} .profile-sensors-box .inhabitant-last-activity a:hover, @@ -6027,32 +6785,17 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .search-filters-table td{padding:6px 10px;border:none;vertical-align:middle;background:transparent} .search-filters-table td.card-label{white-space:nowrap;font-weight:600;color:#ffa500} .search-filters-table input[type="datetime-local"], -.search-filters-table input.search-oasis-id{min-width:280px} +.search-filters-table input.search-oasis-id, +.search-filters-table input[type="search"], +.search-filters-table select{min-width:280px;width:100%;box-sizing:border-box} .transfer-category-select{width:auto;min-width:160px;max-width:200px;padding:6px 10px} .spread-row{margin:8px 0;display:flex;justify-content:flex-start} .spread-form{display:inline-block} -.spread-btn{cursor:pointer;font:inherit;padding:4px 10px;border:1px solid #555;border-radius:4px;background:#1a1a1a;color:#ffa500;font-size:14px} -.spread-btn:hover{background:#2a2a2a;border-color:#ffa500} -.spread-btn-on{background:#ffa500;color:#1a1a1a;border-color:#ff7300} -.spread-btn-on:hover{background:#ffb733;color:#000} -.welcome-header{margin-bottom:18px} -.welcome-chat{max-width:760px;margin:0 auto;padding:8px} -.welcome-ai-header{display:flex;align-items:center;gap:10px;margin-bottom:10px;color:#ffa500;font-weight:600} -.welcome-ai-avatar{font-size:24px} -.welcome-ai-name{font-size:16px} -.welcome-ai-step{margin-left:auto;font-size:12px;color:#888} -.welcome-bubble{background:#1a1a1a;border:1px solid #333;border-radius:12px;padding:14px 18px;margin-bottom:14px;line-height:1.5} -.welcome-bubble-ai{border-left:3px solid #ffa500} -.welcome-bubble-title{margin:0 0 8px;color:#ffd700;font-size:18px} -.welcome-bubble p{margin:6px 0;color:#ddd} -.welcome-actions{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0;justify-content:flex-end} -.welcome-action-primary{background:#ff6600;color:#fff;border-color:#ff3300} -.welcome-progress{display:flex;justify-content:center;gap:10px;margin:18px 0 6px} -.welcome-dot{font-size:14px;color:#444;text-decoration:none} -.welcome-dot-active{color:#ffa500} -.welcome-skip{text-align:center;margin-top:6px} -.welcome-skip-link{color:#666;font-size:12px} +.btn-spread-on{background:#ffa300;color:#000} +.btn-spread-on:hover{background:#ffb733;color:#000} +.spread-form .btn-singleview{border-radius:16px;padding:7px 12px} .indexing-progress-block{max-width:640px;margin:24px auto;text-align:center} +.indexing-message{margin:0 0 14px;font-size:16px;line-height:1.4;color:#ffd700} .indexing-progress{width:100%;height:18px} .indexing-percent{margin:8px 0 4px;font-size:22px;color:#ffd700} .indexing-note{color:#888;font-size:12px;margin:0} @@ -6071,6 +6814,108 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .pm-exposition-whole{background:#3a2300;color:#ffd700;border-color:#a39134} .pm-exposition-mutuals{background:#1a3a1a;color:#7fff7f;border-color:#3a7f4c} .pm-exposition-encrypted{background:#0e3320;color:#7fff7f;border-color:#3a7f4c} +.pm-exposition-transport{background:#132433;color:#8fc7ff;border-color:#35648f} +.pm-exposition-license{background:#251f33;color:#c8b8ec;border-color:#6a5a96} +.pm-exposition-tribe{background:#2b1d0a;color:#ffb347;border-color:#a8692a} +.phone-action-form{display:inline-block;margin:0;padding:0} +.phone-call-panel{border:1px solid #ffa500;border-radius:8px;padding:14px;margin:12px 0} +.phone-call-panel div{background:transparent} +.phone-call-who{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:16px;font-weight:600;padding:0;margin:8px 0;border:none;box-shadow:none} +.phone-call-icon{font-size:22px} +.phone-call-clock{font-family:monospace;color:#ffa500} +.phone-call-actions{display:flex;gap:10px;flex-wrap:wrap;padding:0;margin:8px 0 0 0;border:none;box-shadow:none} +.phone-block{padding:14px 18px;margin:0 0 14px 0;background:transparent} +.phone-block > h2{margin:0 0 12px 0} +.phone-block > .phone-empty{margin:0} +.phone-contacts{display:flex;flex-direction:column;gap:8px;padding:0;margin:8px 0 0 0;background:transparent;border:none !important;box-shadow:none} +.phone-contact{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 10px;margin:0;border:1px solid #333;border-radius:6px} +.phone-contact .phone-action-form{margin-left:auto} +.phone-record audio{width:100%;max-width:520px;display:block;margin:8px 0} +.phone-record .phone-record-pending{display:flex;align-items:center;min-height:40px;margin:8px 0} +.phone-number-chooser{margin:0 0 12px 0;padding:0;border:none;background:transparent} +.phone-number-chooser > p{margin:0 0 8px 0} +.phone-call-ringing{font-size:14px;color:#888;padding:0;margin:4px 0} +.phone-history{width:100%;border-collapse:collapse} +.phone-history th,.phone-history td{padding:6px 8px;border-bottom:1px solid #333;text-align:left} +.phone-dir{font-size:16px;width:24px} +.phone-empty{color:#888} +.phone-banner{display:flex;align-items:center;gap:10px;flex-wrap:wrap} +.phone-banner{gap:10px 12px;padding:8px 16px} +.phone-banner .update-banner-link{font-size:1.05em;text-decoration:none;border-bottom:1px dotted currentColor} +.phone-banner-dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:#2ecc71;box-shadow:0 0 6px #2ecc71;flex:0 0 auto} +.phone-banner-chip{white-space:nowrap} +.phone-banner-chip .live-clock{line-height:1.2em} +.phone-banner .phone-action-form{margin:0;display:inline-flex;align-items:center} +.phone-banner .tribe-action-btn{padding:4px 12px;font-size:11px} +.phone-banner button{margin:0} +.live-clock{display:inline-flex;align-items:flex-end;white-space:nowrap;line-height:1.2em} +.live-clock-win{display:inline-block;height:1.2em;line-height:1.2em;overflow:hidden;vertical-align:bottom;font-variant-numeric:tabular-nums;padding:0} +.live-clock-roll{display:block;animation:live-clock-roll 60s steps(60) infinite;padding:0} +.live-clock-roll>span{display:block;height:1.2em;line-height:1.2em;padding:0} +.live-clock-min{animation-duration:3600s;animation-iteration-count:1;animation-fill-mode:forwards} +.live-clock-once{animation-iteration-count:1;animation-fill-mode:forwards} +@keyframes live-clock-roll{to{transform:translateY(-100%)}} +.lc-d1{animation-delay:-1s} +.lc-d2{animation-delay:-2s} +.lc-d3{animation-delay:-3s} +.lc-d4{animation-delay:-4s} +.lc-d5{animation-delay:-5s} +.lc-d6{animation-delay:-6s} +.lc-d7{animation-delay:-7s} +.lc-d8{animation-delay:-8s} +.lc-d9{animation-delay:-9s} +.lc-d10{animation-delay:-10s} +.lc-d11{animation-delay:-11s} +.lc-d12{animation-delay:-12s} +.lc-d13{animation-delay:-13s} +.lc-d14{animation-delay:-14s} +.lc-d15{animation-delay:-15s} +.lc-d16{animation-delay:-16s} +.lc-d17{animation-delay:-17s} +.lc-d18{animation-delay:-18s} +.lc-d19{animation-delay:-19s} +.lc-d20{animation-delay:-20s} +.lc-d21{animation-delay:-21s} +.lc-d22{animation-delay:-22s} +.lc-d23{animation-delay:-23s} +.lc-d24{animation-delay:-24s} +.lc-d25{animation-delay:-25s} +.lc-d26{animation-delay:-26s} +.lc-d27{animation-delay:-27s} +.lc-d28{animation-delay:-28s} +.lc-d29{animation-delay:-29s} +.lc-d30{animation-delay:-30s} +.lc-d31{animation-delay:-31s} +.lc-d32{animation-delay:-32s} +.lc-d33{animation-delay:-33s} +.lc-d34{animation-delay:-34s} +.lc-d35{animation-delay:-35s} +.lc-d36{animation-delay:-36s} +.lc-d37{animation-delay:-37s} +.lc-d38{animation-delay:-38s} +.lc-d39{animation-delay:-39s} +.lc-d40{animation-delay:-40s} +.lc-d41{animation-delay:-41s} +.lc-d42{animation-delay:-42s} +.lc-d43{animation-delay:-43s} +.lc-d44{animation-delay:-44s} +.lc-d45{animation-delay:-45s} +.lc-d46{animation-delay:-46s} +.lc-d47{animation-delay:-47s} +.lc-d48{animation-delay:-48s} +.lc-d49{animation-delay:-49s} +.lc-d50{animation-delay:-50s} +.lc-d51{animation-delay:-51s} +.lc-d52{animation-delay:-52s} +.lc-d53{animation-delay:-53s} +.lc-d54{animation-delay:-54s} +.lc-d55{animation-delay:-55s} +.lc-d56{animation-delay:-56s} +.lc-d57{animation-delay:-57s} +.lc-d58{animation-delay:-58s} +.lc-d59{animation-delay:-59s} +.update-changes{margin:0;padding:0 0 0 18px} +.update-changes li{margin:0 0 4px 0} .pm-double-enc-chip{background:#2a1633;border-color:#7a4b9c} .pm-double-enc-chip,.pm-double-enc-chip .pm-exposition-icon,.pm-double-enc-chip .pm-exposition-text{color:#d59bff} .pm-exposition-closed{background:#3a0e0e;color:#ff8a8a;border-color:#b04040} @@ -6079,7 +6924,8 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .pm-exposition-lifespan-green{background:#0e2a16;color:#9aff9a;border-color:#3a7f4c} .pm-exposition-lifespan-orange{background:#2a1f10;color:#ffb84d;border-color:#a37a34} .pm-exposition-lifespan-red{background:#2a0e0e;color:#ff7f7f;border-color:#b04040} -.jobs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:20px} +.jobs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(max(300px,calc((100% - 40px)/3)),1fr));gap:20px} +.blogs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(max(300px,calc((100% - 40px)/3)),1fr));gap:20px} .jobs-grid .tribe-card,.tribe-grid .tribe-card{margin-bottom:0} .tribe-card .card-comments-summary{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px} .tribe-card .card-comments-summary .inline-form{margin:0} @@ -6088,7 +6934,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .jobs-info-table .tribe-info-value{white-space:normal;word-break:normal;overflow-wrap:anywhere} .job-section{margin-bottom:12px;padding:0} .job-section-title{margin:0 0 8px 0;color:#ffa500;font-size:1.05em} -.job-card .tribe-card-members{background:transparent;padding:4px 0;text-align:left} .job-card .card-field{border-bottom:none} .job-card .confirmations-block{margin:6px 0} .job-meta-line{margin:0;color:#9aa3b2;font-size:12px;letter-spacing:1px;text-transform:uppercase} @@ -6109,7 +6954,7 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .pm-exposition-text{line-height:1;} .shop-title-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px;padding:0;background:transparent;border-radius:0;box-shadow:none} .shop-title-row h2{margin:0} -.card-chips-row{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px 0;padding:0;border:none;background:transparent;border-radius:0;box-shadow:none} +.card-chips-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 10px 0;padding:0;border:none;background:transparent;border-radius:0;box-shadow:none} .tribe-card-body .job-price-line,.tribe-card-body .job-meta-line,.tribe-card-body .job-card-view-btn,.tribe-card-body .card-comments-summary,.tribe-card-body .tribe-card-members{padding:0;background:transparent;border-radius:0;box-shadow:none;border:none} .tribe-card-body .job-price-line{margin:4px 0} .tribe-card-body .job-card-view-btn{margin-top:8px;margin-bottom:0} @@ -6117,20 +6962,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .tribe-main .job-section{border:none;background:transparent} .profile-reach{display:flex;flex-direction:column;align-items:center;gap:8px;margin:12px 0;text-align:center;border:none !important;background:none !important} .profile-reach .shop-clearnet-url{margin:0;text-align:center;border:none !important;background:none !important} -.profile-reach-toggle{margin:0} -.profile-reach-toggle .btn{ - background-color:#FFA500; - color:#000; - padding:8px 16px; - border-radius:5px; - text-align:center; - text-decoration:none; - border:none; - cursor:pointer; - font-family:inherit; - font-size:inherit; -} -.profile-reach-toggle .btn:hover{background-color:#FFD700} /* Unified item card aesthetic — applied to per-module containers that previously had no card styling. Existing rules (.market-card, .feed-card, .tribe-card, etc.) keep their own look via cascade. */ .card, @@ -6156,11 +6987,8 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .chat-card, .opinions-card, .post-card, -.map-card, .mention-card, .trending-card, -.bank-claim-card, -.addr-list-card, .shop-product-card, .cv-item, .transfer-item, @@ -6320,7 +7148,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .chat-card .card-field, .opinions-card .card-field, .post-card .card-field, -.map-card .card-field, .mention-card .card-field, .trending-card .card-field, .bank-claim-card .card-field, @@ -6335,6 +7162,8 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} border-bottom: 1px solid #2a2a2a; align-items: baseline; } +.card-field > .card-value:only-child { grid-column: 1 / -1; min-width: 0; } + .card .card-field:last-child, .card-section .card-field:last-child, .card-fields-container .card-field:last-child, @@ -6393,8 +7222,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .card-date-highlight{color:#ffd740;font-size:18px;font-weight:700;margin:6px 0;letter-spacing:.5px;background:transparent !important;border:0 !important;padding:0 !important} .vote-buttons-row-single{display:flex;flex-wrap:wrap;gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .vote-buttons-row-single form{margin:0;display:inline-block} -.card-visit-btn-centered{display:flex;justify-content:center;margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important} -.card-visit-btn-centered form{margin:0} .tribe-card-body .voting-buttons{padding:0;background:transparent !important;border:0 !important;margin:8px 0 0 0} .card-assigned-list{display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important;padding:0 !important;margin:6px 0} .card-assigned-list a{display:block} @@ -6413,7 +7240,9 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .activity-card-header{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px;border:none;background:transparent;padding:0} .activity-card-header .card-chips-row{margin:0} .content-actions{display:flex;justify-content:right;align-items:center;gap:12px;flex-wrap:wrap;border:none;background:transparent;padding:0} -.trending-card.own-content{box-shadow:inset 3px 0 0 #ffa300} +.tribe-side .content-actions{flex-wrap:wrap;gap:6px} +.tribe-side .content-actions .btn-singleview{padding:6px 9px;font-size:1em} +.trending-card.own-content,.addr-card.ubi-pub-card.own-content{box-shadow:inset 3px 0 0 #ffa300} .trending-dominant-sep{margin:0 8px;opacity:0.5} .trending-dominant-text,.trending-total-count{font-weight:700} .comment-file-upload{display:flex;flex-direction:column;align-items:flex-start;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;margin:8px 0} @@ -6429,13 +7258,1669 @@ div .header-content{width:100%} .card-section{border:none;padding:10px 0 0 16px} .card-header{display:flex;justify-content:space-between;align-items:center;padding-bottom:0;margin-top:8px;padding-top:0;border:none} .card-label{font-weight:bold;letter-spacing:1.5px;line-height:1.2;margin-bottom:0} -.card-footer{margin-top:6px;font-weight:500;font-size:1.07em;display:flex;align-items:center;gap:8px;background:none;border:none;padding-top:0} +.card-footer{margin-top:6px;font-weight:500;font-size:1.07em;display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:none;border:none;padding-top:0} .card-body{margin-top:0;padding:0} .card-field{display:flex;align-items:baseline;padding:0;margin-bottom:0;border:none;background:none} .card-tags{margin:5px 0 3px 0;display:flex;flex-wrap:wrap;gap:9px} -.card-tags a.tag-link{text-decoration:none;padding:5px 13px 4px 13px;border-radius:7px;font-size:.98em;border:none;font-weight:bold} +.tribe-side .card-tags{background-color:#222 !important;border:none !important;border-radius:10px;padding:20px !important;margin:0 0 20px 0;box-shadow:0 4px 6px rgba(0,0,0,.1);gap:8px} a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bold;text-decoration:none;display:inline-block;border:2px solid transparent;font-size:.8em} .date-link{padding:8px 16px;border-radius:5px;margin-left:8px} .activitySpreadInhabitant2,.activityVotePost{padding:8px 16px;border-radius:5px;font-weight:bold;text-decoration:none;display:inline-block;border:2px solid transparent} .danger-btn,.delete-btn{border:1px solid #d9534f;color:#d9534f;background:transparent} .danger-btn:hover,.delete-btn:hover{background:#d9534f;color:#000} +.pm-fileshare-form-wrap{margin-top:16px} +.pm-fileshare-form input[type=file]{display:block;margin:10px 0 15px} +.pm-fileshare-info{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:8px 0} +.pm-fileshare-icon{font-size:1.2em} +.pm-fileshare-name{font-weight:bold;word-break:break-all} +.pm-fileshare-meta{font-size:.85em;opacity:.75} +.pm-fileshare-download-form{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px;border:0;background:transparent;padding:0} +.pm-fileshare-download{text-decoration:none;display:inline-block} +.industry-form{margin:0 auto} +.industry-governance{margin-top:16px} +.industry-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0} +.industry-build-vote{display:flex;flex-wrap:wrap;gap:8px;align-items:center} +.industry-estimate,.industry-materials,.industry-blueprint-meta,.industry-meta{border:none;background:transparent;padding:0;box-shadow:none;border-radius:0;margin:8px 0} +.industry-estimate p{margin:2px 0} +.industry-estimate-total{font-weight:700;margin-top:6px} +.industry-section{margin-top:20px} +.industry-applicants,.industry-members-list{list-style:none;padding:0;margin:8px 0} +.industry-applicants li,.industry-members-list li{padding:6px 0;display:flex;flex-wrap:wrap;gap:8px;align-items:center} +.industry-vote-form,.industry-invite-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;border:0;background:transparent;padding:0} +.industry-invite-form input{flex:1 1 240px} +.industry-vote-count{opacity:.85} +.industry-hint{opacity:.75;font-style:italic} +.industry-meta{margin:12px 0} +.industry-meta p{margin:4px 0} +.industry-meta-label{font-weight:bold} +.industry-card-desc,.industry-detail-desc{margin:8px 0} +.industry-side-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px} +.search-industry .card-field{margin:4px 0} +.industry-form select,.industry-form input[type="number"],.industry-form input[type="file"],.industry-form input[type="date"]{margin-bottom:15px} +.industry-form-media,.industry-detail-media{margin:8px 0} +.industry-hero-image,.tribe-card-hero-image{max-width:100%;height:auto;border-radius:6px} +.industry-search{margin:8px 0} +.industry-card-noimage{display:flex;align-items:center;justify-content:center;min-height:120px;font-size:48px;opacity:.5} +.industry-bot-notification .pm-title,.housing-bot-notification .pm-title,.banking-bot-notification .pm-title{margin-top:4px} +.industry-table{width:100%;border-collapse:collapse;margin:8px 0} +.industry-table th,.industry-table td{text-align:left;padding:6px 8px;border-bottom:1px solid rgba(128,128,128,.3)} +.industry-contrib-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0} +.industry-contrib-form-single label{display:block;margin:10px 0 4px 0} +.industry-contrib-form-single input,.industry-contrib-form-single textarea,.industry-contrib-form-single select{width:100%;box-sizing:border-box;display:block;margin:0} +.industry-contrib-form-single .industry-input-num{width:auto;max-width:220px} +.industry-contrib-form-single button{margin-top:14px} +.industry-contrib-kind{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:0 0 6px 0;border:none;background:transparent;padding:0;box-shadow:none} +.industry-contrib-kind label{margin:0} +.industry-contrib-kind select{width:auto;min-width:190px;margin:0;height:38px;box-sizing:border-box} +.industry-contrib-kind button{margin:0;height:38px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center} +.industry-blueprint-card{border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0} +.industry-card-wrap{position:relative;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0;background:transparent;box-shadow:none} +.industry-card-wrap .industry-blueprint-card{border:none;background:transparent;padding:0;margin:0;box-shadow:none;border-radius:0} +.industry-card-wrap > .card-header{position:absolute;top:8px;right:10px;margin:0;padding:0;border:none;background:transparent;box-shadow:none;z-index:1} +.industry-card-wrap > .card-header > span:empty{display:none} +.industry-card-wrap > .industry-blueprint-card{padding:0 96px 0 0} +.industry-actions form,.industry-actions .project-control-form--status{margin:0;padding:0;border:none;background:transparent;display:flex;align-items:center;gap:10px} +.industry-actions .project-control-select,.industry-actions .project-control-btn,.industry-actions select,.industry-actions button{margin:0} +.industry-actions button,.industry-actions .project-control-select{display:inline-flex;align-items:center;justify-content:center;height:38px;box-sizing:border-box} +.card-chips-row .spread-form{margin:0;padding:0;border:none;background:transparent;display:flex;align-items:center} +.card-chips-row .spread-form .btn-singleview{margin:0} +.industry-build-notes{margin:22px 0} +.industry-dates-row{border:none;background:transparent;padding:0;box-shadow:none;border-radius:0;margin:8px 0} +.industry-dates-row p{margin:0} +.dev-breadcrumb,.dev-file-meta,.dev-file-head,.dev-listing,.dev-pager,.dev-actions,.dev-result-text,.dev-result-head,.dev-desktop{border:none;background:transparent;padding:0;box-shadow:none;border-radius:0} +.dev-file-meta p{margin:4px 0;line-height:1.9} +.dev-label{font-weight:bold} +.dev-sep{opacity:.45} +.dev-hint{opacity:.75;font-style:italic} +.dev-breadcrumb{margin:14px 0;word-break:break-all} +.dev-breadcrumb-sep{opacity:.45} +.dev-breadcrumb-current{font-weight:bold} +.dev-desktop{display:grid;grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:12px;margin:16px 0} +.dev-icon{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;text-align:center;gap:5px;padding:12px 6px;border:1px solid rgba(128,128,128,.28);border-radius:8px;text-decoration:none;word-break:break-word;min-height:104px} +.dev-icon:hover{border-color:rgba(128,128,128,.7)} +.dev-icon-glyph{font-size:30px;line-height:1} +.dev-icon-name{font-size:.9em;line-height:1.25} +.dev-icon-meta{font-size:.76em;opacity:.6} +.dev-icon-off{opacity:.5;cursor:default} +.dev-icon-off:hover{border-color:rgba(128,128,128,.28)} +.dev-file-head{display:flex;align-items:center;gap:10px;margin:14px 0 4px 0} +.dev-file-glyph{font-size:26px} +.dev-file-name{margin:0;word-break:break-all} +.dev-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:14px 0} +.dev-actions form{margin:0;padding:0;border:none;background:transparent} +.dev-actions button{margin:0} +.dev-code{overflow-x:auto;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:8px;margin:12px 0} +.dev-code-table{border-collapse:collapse;width:100%} +.dev-code-num a{text-decoration:none} +.dev-code-line pre{margin:0;white-space:pre} +.dev-code-line code{font-family:monospace;font-size:.92em} +.dev-pager{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0} +.dev-list{list-style:none;padding:0;margin:14px 0} +.dev-entry{padding:9px 0;border-bottom:1px solid rgba(128,128,128,.2);word-break:break-all} +.dev-result-head{display:flex;align-items:center;gap:8px} +.dev-result-glyph{font-size:18px} +.dev-result-text{margin:5px 0 0 26px;overflow-x:auto} +.dev-result-text pre{margin:0} +.dev-table{width:100%;border-collapse:collapse;margin:8px 0} +.dev-table th,.dev-table td{text-align:left;padding:6px 8px;border-bottom:1px solid rgba(128,128,128,.3);word-break:break-all} +.filters .dev-toolbar{border:none !important;background:transparent !important;padding:0 !important;margin:0 !important;box-shadow:none !important;border-radius:0 !important} +.dev-search{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;border-radius:0 !important;margin:14px 0} +.dev-search .filter-box{margin:0} +.dev-search .filter-box__controls{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;border-radius:0 !important;margin-top:12px;display:flex;flex-wrap:wrap;gap:10px;align-items:center} +.dev-search .filter-box__select{width:auto;min-width:170px;margin:0;height:38px} +.dev-search .filter-btn{margin:0;height:38px;padding:0 20px;display:inline-flex;align-items:center;justify-content:center} +.dev-code-row td{border:none;vertical-align:top} +.dev-code-num{text-align:right;padding:0 14px 0 4px;width:1%;min-width:52px;white-space:nowrap;opacity:.5;user-select:none;border-right:1px solid rgba(128,128,128,.25) !important} +.dev-code-line{padding-left:16px} +.ai-suggestion-banner .update-banner-link{display:inline-block;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60vw} +.welcome-banner .welcome-banner-start{display:inline-flex;align-items:center;margin:0 10px;padding:6px 18px;font-size:1em;text-decoration:none;white-space:nowrap} +.welcome-banner .welcome-banner-close{margin:0;padding:0;border:none;background:transparent;box-shadow:none;display:inline-flex;align-items:center} +.welcome-banner-close-btn{background:transparent;border:none;color:inherit;cursor:pointer;font-size:1em;line-height:1;padding:0 4px;margin:0;opacity:.6} +.welcome-banner-close-btn:hover{opacity:1} +.welcome-ux-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;border:none !important;background:transparent !important;max-width:680px} +.welcome-ux-option{display:flex;flex-direction:column;gap:6px;align-items:center;border:1px solid rgba(128,128,128,.35);border-radius:8px;padding:10px;cursor:pointer;background:transparent} +.welcome-ux-option:has(input:checked){border-color:#ffa300;background:rgba(255,163,0,.07)} +.welcome-ux-option input{margin:0} +.welcome-ux-shot{width:100%;max-width:320px;border-radius:6px;border:1px solid rgba(128,128,128,.25)} +.welcome-ux-label{font-weight:bold} +@media (max-width: 700px){.welcome-ux-grid{grid-template-columns:1fr}} +.welcome-complete,.welcome-action{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;border-radius:0 !important} +.welcome-step{border:1px solid rgba(128,128,128,.3) !important;border-radius:8px;padding:14px 16px !important;margin:0} +.welcome-step summary{cursor:pointer} +.welcome-step summary h2{display:inline;margin:0 10px 0 0} +.welcome-step-text{font-size:.95em;opacity:.85} +.welcome-step[open] summary{margin-bottom:10px} +.welcome-action{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0} +.welcome-action form{margin:0;padding:0;border:none;background:transparent} +.welcome-action .filter-btn{margin:0;height:38px;padding:0 20px;display:inline-flex;align-items:center;justify-content:center} +.welcome-complete{margin:16px 0} +.welcome-profile-form,.welcome-greeting-form,.welcome-ux-form,.welcome-select-form,.welcome-community-form{margin:0;padding:0;border:none;background:transparent;box-shadow:none} +.welcome-pick{display:flex;align-items:flex-start;gap:10px;cursor:pointer} +.welcome-pick input[type="checkbox"]{margin:18px 0 0 0;flex:0 0 auto;width:auto !important;min-width:0 !important} +.welcome-pick .snh-invite-box{flex:1 1 auto;min-width:0;margin:6px 0} +.welcome-select-form select{width:auto;min-width:220px;margin:0 0 14px 0} +.welcome-profile-form label{display:block;margin:10px 0 4px 0} +.welcome-profile-form input[type="text"],.welcome-profile-form textarea,.welcome-greeting-form textarea{width:100%;box-sizing:border-box;display:block;margin:0} +.welcome-profile-form input[type="file"]{margin:0;width:auto} +.welcome-profile-form input[type="text"]{max-width:420px} +.welcome-profile-form .welcome-action,.welcome-greeting-form .welcome-action{margin-top:14px} +.welcome-profile-avatar{display:block;width:96px;height:96px;object-fit:cover;border-radius:6px;margin:0 0 10px 0} +.housing-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:20px} +.housing-grid .tribe-card{margin-bottom:0} +.housing-card .card-field{border-bottom:none} +.housing-info-table{table-layout:auto} +.housing-info-table .tribe-info-label{width:1%;white-space:nowrap} +.housing-info-table .tribe-info-value{white-space:normal;word-break:normal;overflow-wrap:anywhere} +.housing-stars{color:#FFA500;font-size:14px;letter-spacing:1px} +.media-gallery{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;margin:0 0 12px 0} +.media-gallery .gallery{padding:0} +.housing-search .filter-box{margin:0} +.housing-place-input{height:36px;min-width:170px;width:auto;flex:1 1 170px} +.media-form-gallery{padding:0;margin:10px 0} +.media-form-gallery .gallery-item{cursor:default} +.housing-keep-photos{display:inline-flex;align-items:center;gap:6px;margin:0 0 10px 0} +.housing-keep-photos input[type="checkbox"]{width:auto;margin:0} +.tribe-main .housing-opinions{border:none;background:transparent} +.housing-form input[type="file"]{width:auto} +.vote-form-error{margin:0 0 14px 0} +.vote-form-error p{margin:0} +.media-file-row{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 6px 0} +.media-file-row input[type="file"]{width:auto;margin:0} +.media-add-btn{margin:0} +.media-video{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;margin:0 0 10px 0} +.media-video-player{max-width:100%;border-radius:6px;display:block} +.media-form-video{max-width:320px} +.media-video-centered{display:flex;justify-content:center;margin-top:12px} +.media-video-centered .media-video-player{max-width:720px;width:100%} +.housing-card-video{display:flex;justify-content:center} +.housing-card-hero-video{width:100%;max-height:220px;border-radius:6px;display:block} +.housing-status-row,.housing-visibility-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px} +.housing-status-row form,.housing-visibility-row form{flex-basis:100%;margin:4px 0 0 0} +.housing-stars{display:block;margin:2px 0 6px 0} +.media-form-gallery .gallery-item{display:flex;flex-direction:column;align-items:center;gap:6px} +.media-remove-btn{height:28px;padding:0 10px;border:1px solid rgba(128,128,128,.5);border-radius:4px;background:transparent;color:inherit;cursor:pointer;font-size:.8em} +.media-form-video{display:flex;flex-direction:column;align-items:flex-start;gap:8px} +.spread-edit-warning{margin:0 0 14px 0} +.spread-edit-warning p{margin:0} +.market-opinions{border:none !important;background:transparent !important;padding:0 !important;margin:12px 0} +.housing-status-row .project-control-form{flex-basis:100%;margin:4px 0 0 0} +.task-assign-row{margin:8px 0} +.task-assign-row form{margin:0} +.reports-subfilters{margin-top:8px;align-items:center} +.reports-subfilters .card-label{margin:0 4px 0 8px;white-space:nowrap} +.stats-kpi-inline{display:flex;flex-direction:row;align-items:baseline;gap:6px;flex-wrap:wrap} +.stats-kpi-inline .stats-kpi-value{font-size:16px} +.stats-kpi-inline .stats-kpi-label{white-space:nowrap} +.content-actions .content-action-form{margin:0;display:inline-flex} +.content-actions .btn-donate{font-family:inherit;line-height:1;text-decoration:none} +.btn-singleview>*{line-height:1} +.content-actions .btn-pin-on{background:#ffa300;color:#000} +.content-actions .btn-pin-on:hover{background:#ffb733;color:#000} +.content-actions .spread-form{margin:0;display:inline-flex} + +.blog-card-body, +.blog-detail-body, +.mention-card-body { + border: none !important; + background: transparent !important; +} + +.blog-card-text, +.blog-detail-text, +.mention-text { + line-height: 1.6; + word-wrap: break-word; + overflow-wrap: anywhere; + border: none !important; + background: transparent !important; +} + +.blog-card-text img, +.blog-detail-text img { + max-width: 100%; + height: auto; + display: block; + margin: 10px 0; +} + +.blog-card-text pre, +.blog-detail-text pre { + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; +} + +.blog-card-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 6px 0; + border: none !important; + background: transparent !important; +} + +.blog-allow-comments { + display: inline-flex; + align-items: center; + gap: 6px; + border: none !important; + background: transparent !important; +} + +.blog-allow-comments input[type="checkbox"] { + width: auto; + margin: 0; +} + +.logs-public-list { + display: flex; + flex-direction: column; + gap: 12px; + border: none !important; + background: transparent !important; +} + +.logs-public-body { + border: none !important; + background: transparent !important; +} + +.logs-public-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 6px 0; + border: none !important; + background: transparent !important; +} + +.logs-published-hint { + margin-top: 10px; + opacity: .8; + font-size: .9em; +} + +.poll-card-body, +.poll-detail-body, +.poll-card-meta, +.poll-switch, +.poll-ballot, +.poll-option-row { + border: none !important; + background: transparent !important; +} + +.poll-results { + width: 100%; + border-collapse: collapse; + margin: 8px 0; +} + +.poll-results td { + padding: 4px 6px; + vertical-align: middle; +} + +.poll-result-option { + white-space: normal; + overflow-wrap: anywhere; +} + +.poll-result-count { + text-align: right; + white-space: nowrap; +} + +.poll-bar { + width: 100%; + min-width: 60px; + height: 10px; + border: 1px solid rgba(128, 128, 128, .5); + border-radius: 5px; + overflow: hidden; + background: transparent !important; +} + +.poll-bar-fill { + height: 100%; + background: rgba(128, 128, 128, .55) !important; + border: none !important; +} + +.poll-bar-fill-0 { width: 0%; } +.poll-bar-fill-5 { width: 5%; } +.poll-bar-fill-10 { width: 10%; } +.poll-bar-fill-15 { width: 15%; } +.poll-bar-fill-20 { width: 20%; } +.poll-bar-fill-25 { width: 25%; } +.poll-bar-fill-30 { width: 30%; } +.poll-bar-fill-35 { width: 35%; } +.poll-bar-fill-40 { width: 40%; } +.poll-bar-fill-45 { width: 45%; } +.poll-bar-fill-50 { width: 50%; } +.poll-bar-fill-55 { width: 55%; } +.poll-bar-fill-60 { width: 60%; } +.poll-bar-fill-65 { width: 65%; } +.poll-bar-fill-70 { width: 70%; } +.poll-bar-fill-75 { width: 75%; } +.poll-bar-fill-80 { width: 80%; } +.poll-bar-fill-85 { width: 85%; } +.poll-bar-fill-90 { width: 90%; } +.poll-bar-fill-95 { width: 95%; } +.poll-bar-fill-100 { width: 100%; } + +.poll-option-row { + margin: 4px 0; +} + +.poll-option-row input[type="radio"], +.poll-option-row input[type="checkbox"] { + width: auto; + margin-right: 6px; +} + +.poll-switch { + display: flex; + align-items: center; + margin: 6px 0; + padding: 0 !important; + border: none !important; + background: transparent !important; +} + +.poll-switch input[type="checkbox"] { + width: auto; + margin: 0 6px 0 0; +} + +.chat-poll-form form { + display: flex; + flex-direction: column; + align-items: flex-start; + background: transparent !important; + border: 0 !important; + padding: 0 !important; + margin: 0; +} + +.chat-poll-form input[type="text"], +.chat-poll-form textarea { + width: 100%; + max-width: 640px; + margin: 0; +} + +.chat-poll { + display: block; +} + +.chat-poll-head { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 6px; +} + +.chat-poll-tag { + font-size: .75em; + text-transform: uppercase; + border: 1px solid rgba(128, 128, 128, .5); + border-radius: 4px; + padding: 1px 6px; +} + +.chat-poll-question { + font-weight: bold; + margin: 4px 0 8px 0; +} + +.chat-poll-meta { + margin: 6px 0; +} + +.chat-poll-form { + border: 1px solid rgba(128, 128, 128, .4); + border-radius: 6px; + padding: 8px 12px; + margin-bottom: 12px; + background: transparent !important; +} + +.chat-poll-summary { + display: inline-block !important; + width: auto !important; + max-width: max-content; + box-sizing: border-box; + cursor: pointer; + font: inherit; + font-weight: bold; + padding: 10px 20px; + margin-bottom: 10px; + border: 1px solid rgba(128, 128, 128, .5) !important; + border-radius: 5px; + background: rgba(128, 128, 128, .12) !important; + transition: background-color .2s, border-color .2s; + list-style: none; +} + +.chat-poll-summary:hover { background: rgba(128, 128, 128, .25) !important; } +.chat-poll-summary::-webkit-details-marker { display: none; } +.chat-poll-summary::after { content: " \25BE"; opacity: 0.85; } +details[open] > .chat-poll-summary::after { content: " \25B4"; } + +.poll-form-fields { + display: flex; + flex-direction: column; + gap: 4px; +} + +.larp-poll { + display: block; +} + +.reply-context { + border-left: 3px solid rgba(128, 128, 128, .6); + padding-left: 10px; + margin-bottom: 8px; + opacity: .85; +} + +.reply-context-meta { + font-size: .85em; +} + +.reply-context-text { + font-size: .85em; + max-height: 60px; + overflow: hidden; + margin-top: 4px; + font-weight: bold; +} + +.forum-reply-body, +.trending-card .forum-reply-body { + display: block; + margin-bottom: 12px; +} + +.forum-reply-text { + margin: 0 0 8px 0; + overflow-wrap: anywhere; +} + +.forum-reply-text img { + display: block; + max-width: 100%; + height: auto; +} + +.trending-card .card-field > .forum-reply-text { + grid-column: 1 / -1; + min-width: 0; +} + +.chat-thread-details, +.chat-thread-meta { + border: none !important; + background: transparent !important; +} + +.chat-thread-summary { + cursor: pointer; + font-weight: bold; + margin: 6px 0 6px 16px; +} + +.thread-reply-text { + white-space: pre-wrap; +} + +.event-recurrence-block, +.event-occurrences { + border: none !important; + background: transparent !important; + margin: 8px 0; +} + +.event-occurrence-list { + padding-left: 22px; + margin: 6px 0; +} + +.cv-ai-box, +.cv-match-row, +.inhabitant-subfilters { + border: none !important; + background: transparent !important; +} + +.cv-ai-box { + border: 1px solid rgba(128, 128, 128, .4) !important; + border-radius: 6px; + padding: 10px 14px; + margin-bottom: 12px; +} + +.cv-ai-box input[type="number"] { + width: auto; + min-width: 90px; +} + +.inhabitant-subfilters { + margin-top: 4px; +} + +.pads-grid { + grid-template-columns: repeat(auto-fill, minmax(min(100%, 520px), 1fr)); +} + +.poll-hue-0 { background: rgba(76,175,80,.75) !important; } +.poll-hue-1 { background: rgba(33,150,243,.75) !important; } +.poll-hue-2 { background: rgba(255,152,0,.75) !important; } +.poll-hue-3 { background: rgba(156,39,176,.75) !important; } +.poll-hue-4 { background: rgba(0,188,212,.75) !important; } +.poll-hue-5 { background: rgba(233,30,99,.75) !important; } +.poll-hue-6 { background: rgba(139,195,74,.75) !important; } +.poll-hue-7 { background: rgba(121,85,72,.75) !important; } + +.poll-result-mine .poll-result-option { + font-weight: bold; +} + +.poll-deadline { + margin: 6px 0; + border: none !important; + background: transparent !important; +} + +.chat-poll-actions { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 10px; + border: none !important; + background: transparent !important; +} + +.poll-ballot-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-top: 6px; +} + +.chat-poll-close-form:empty, +.chat-poll-close-form:not(:has(button)) { + margin: 0; + padding: 0; +} + +.report-category-select { + width: auto; + max-width: 260px; +} +.apply-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:8px 0 4px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.apply-row select{margin:0} +.apply-row .create-button{margin:0} +.form-sep{border:0;border-top:1px solid #444;margin:14px 0} + +.data-search { + margin-top: 10px; +} + +.data-cohesion { + display: flex; + flex-direction: column; + gap: 10px; + border: none !important; + background: transparent !important; +} + +.data-cohesion-value { + font-size: 2em; + font-weight: bold; + margin: 0; +} + +.data-cohesion-hint { + opacity: .75; + margin: 0; +} + +.data-cohesion-figures { + display: flex; + flex-wrap: wrap; + gap: 24px; + border: none !important; + background: transparent !important; +} + +.data-figure { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 6px 14px; + border-radius: 6px; +} + +.data-figure-value { + font-size: 1.4em; + font-weight: bold; +} + +.data-figure-label { + font-size: .85em; + opacity: .7; + text-transform: uppercase; +} + +.bookmark-subtitle{color:#FFDD44;margin:2px 0 6px 0;font-size:15px} +.data-common-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0;background:transparent !important;border:0 !important;padding:0 !important} +.data-common-row .card-label{margin:0} +.data-common-row .tag-link{margin:0;padding:0.15em 0.55em;line-height:1.4} +.data-you-chip{margin-left:8px} +.data-top-terms { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + border: none !important; + background: transparent !important; +} + +.data-top-terms .tag-link { + font-size: 1.15em; +} + +.data-scale-0 { background: rgba(233, 30, 99, .75) !important; } +.data-scale-1 { background: rgba(255, 152, 0, .75) !important; } +.data-scale-2 { background: rgba(255, 214, 0, .75) !important; } +.data-scale-3 { background: rgba(139, 195, 74, .75) !important; } +.data-scale-4 { background: rgba(76, 175, 80, .75) !important; } + +.data-list { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + border: none !important; + background: transparent !important; +} + +@media (max-width: 900px) { + .data-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 600px) { + .data-list { grid-template-columns: 1fr; } +} + +.data-card { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 14px; + border-radius: 8px; + position: relative; +} + +.data-score-value, +.data-links-count { + font-weight: bold; + white-space: nowrap; +} + +.data-side-top { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + padding-right: 40px; + border: none !important; + background: transparent !important; +} + +.data-side-top .data-score-label { + margin-left: auto; + margin-bottom: 0; + white-space: nowrap; +} + +.data-side-top .data-kind-chip { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.data-card .data-side-visit { + position: absolute; + top: 10px; + right: 12px; + margin: 0; +} + +.data-card .data-side-visit { + background: transparent !important; + border: 1px solid rgba(128, 128, 128, .45) !important; + border-radius: 10px; + padding: 2px 9px !important; + font-size: .9em; + line-height: 1.4; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; +} + +.data-kind-chip { + display: inline-block; + font-size: .68em; + letter-spacing: .04em; + padding: 2px 7px; + border: 1px solid rgba(128, 128, 128, .45); + border-radius: 10px; + opacity: .85; + white-space: nowrap; +} + +.data-card-title { + font-weight: bold; + font-size: .95em; + line-height: 1.25; + min-width: 0; + overflow-wrap: anywhere; +} + +.data-best-card { + background: #26200f !important; + border: 1px solid rgba(255, 163, 0, .35) !important; +} + +.data-best-score { + font-size: 1.15em; +} + +.data-card-author { + font-size: .78em; + opacity: .7; + max-width: 100%; + padding-left: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.data-cohesion-label { + font-size: .5em; + vertical-align: middle; + opacity: .75; +} + +.card-engage { + display: grid; + grid-template-columns: 1fr 1fr; + align-items: start; + gap: 8px; + margin: 8px 0; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.card-engage > * { + min-width: 0; +} + +.card-engage > .feed-votes-summary { + grid-column: 1 / -1; + margin: 0; +} + +.card-engage > details[open] { + grid-column: 1 / -1; +} + +.card-engage > details[open] ~ details { + grid-column: 1 / -1; +} + +.card-engage .opinions-voting-collapse, +.card-engage .comments-collapse { + margin: 0; +} + +.card-engage .opinions-summary, +.card-engage .comments-summary { + width: 100% !important; + max-width: none; + text-align: center; +} + +@media (max-width: 600px) { + .card-engage { grid-template-columns: 1fr; } +} + +.activity-search input[type="text"], +.blockexplorer-search-pair input[type="text"], +.blockexplorer-search-dates input[type="text"], +.blockexplorer-search-actions input[type="text"], +.logs-toolbar input[type="text"], +.search-filters-row input[type="text"], +.filter-box input[type="text"], +.ui-toolbar input[type="text"] { + margin: 0; +} + +.calendar-invite-actions, +.calendar-owner-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; +} + +.calendar-open-invite-block { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.image-container { + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.activity-sub-filter { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 4px 0 14px 0; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.activity-sub-filter .sub-filter-form { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0; +} + +.larp-academia-hint .larp-my-house-label { + opacity: .85; +} + +.larp-academia-hint .larp-my-house-link { + font-weight: bold; +} + +.oasis-footer-version { + font-weight: bold; +} + +.larp-test-panel { + border: 1px solid rgba(128, 128, 128, .4) !important; + border-radius: 6px; + padding: 12px 16px; + margin-top: 16px; + background: transparent !important; +} + +.larp-test-panel-hint { + color: #FFA500; + opacity: .85; + font-size: 14px; + line-height: 1.5; + margin: 4px 0 12px 0; +} + +@media (max-width: 900px) { + .larp-academia-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 560px) { + .larp-academia-grid { grid-template-columns: 1fr; } +} + +@media (max-width: 900px) { + .larp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 560px) { + .larp-grid { grid-template-columns: 1fr; } +} + +.chat-jump-row { + display: flex; + justify-content: flex-end; + margin-bottom: 8px; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.chat-latest-anchor { + scroll-margin-top: 16px; + border: none !important; + background: transparent !important; + padding: 0 !important; +} + +.ai-prompt-form{margin-bottom:0} +.ai-submit-row{display:flex;gap:1.5em;justify-content:flex-end;align-items:center;margin-top:.7em} +.ai-clear-form{display:inline} +.ai-clear-btn{background:transparent;color:#d9534f;border:1px solid #d9534f;padding:.4em 1.2em;border-radius:6px;cursor:pointer;font-size:1em} +.ai-clear-btn:hover{background:#d9534f;color:#fff} +.user-prompt{margin-right:auto;font-size:.85em;color:#888;align-self:center} +.ai-workspace{display:flex;flex-direction:column;gap:12px;background:transparent;border:0;padding:0} +.ai-submit-row{background:transparent;border:0;padding:8px 0 0 0} +.ai-messages-list{padding:10px 14px;gap:14px} +.ai-row .chat-message{padding:10px 14px 8px} +.ai-row.chat-bubble-row-self .chat-message{max-width:85%;margin-left:auto;background:#23242a} +.ai-row:not(.chat-bubble-row-self) .chat-message{max-width:70%;margin-right:auto} +.ai-bubble-user{background:#2f2510} +.ai-chip{background:#1a2a3a;color:#9fd0ff;border-color:#3a6a8f;align-self:center} +.ai-submit-row .ai-chip-waiting{margin-right:auto} +.ai-messages-list .ai-chip-thinking{background:#2a2410;color:#ffd95a;border-color:#a39134;align-self:flex-end;margin-top:4px} +.ai-messages-list .ai-chip-thinking:not(:last-child){display:none} +.ai-bubble-user .chat-message-text,.ai-bubble-42 .chat-message-text{background:transparent;border:0;padding:0;margin:0} +.ai-bubble-42 .ai-answer-line{background:transparent;border:0;padding:0} +.ai-bubble-42{max-width:92%} +.ai-bubble-data{border-left:3px solid #7fb3ff} +.ai-bubble-network{border-left:3px solid #3a7f4c} +.ai-bubble-error{border-left:3px solid #d9534f} +.ai-answer-line{margin:0 0 4px 0} +.ai-rating{display:flex;align-items:center;gap:6px;margin-top:4px;background:transparent;border:0;padding:0} +.ai-stars{flex-direction:row-reverse;justify-content:flex-end} +.ai-star-btn{background:transparent;border:0;color:#555;font-size:18px;line-height:1;padding:0 1px;cursor:pointer} +.ai-star-btn:hover,.ai-star-btn:hover ~ .ai-star-btn{color:#ffa300} +.ai-stars-static{color:#ffa300;font-size:16px;letter-spacing:1px} +.ai-rating-label{font-size:11px;color:#888} +.ai-status{padding:8px 12px;border-radius:6px;font-size:13px;margin-bottom:8px} +.ai-status-loading{background:#2a2410;color:#ffd95a;border:1px solid #a39134} +.ai-status-missing{background:#3a1a1a;color:#ffb3b3;border:1px solid #d9534f} +.ai-export-btn{margin-left:8px} +.blob-cache-clean-btn{margin-left:8px} +.blob-cache-usage{margin:4px 0 10px 0} +.blob-cache-notice{margin:0 0 10px 0;font-weight:bold} +.peers-connect-error{margin:0 0 12px 0;padding:8px 12px;border:1px solid #d9534f;border-radius:6px;color:#d9534f;font-weight:bold} +.user-prompt-text{font-style:italic} +.chat-entry{margin-bottom:2em;position:relative;background:#191919;border-radius:10px;box-shadow:0 0 8px #0004;padding-top:1.8em} +.chat-entry-timestamp{position:absolute;top:.5em;right:1.3em;font-size:.92em;color:#888} +.user-question{margin-bottom:.75em} +.ai-response{max-width:800px;margin:auto;background:#111;padding:1.25em;border-radius:6px;font-family:sans-serif;line-height:1.6;color:#ffcc00} +.ai-response p{margin-bottom:1.2em} +.ai-train-bar{display:flex;align-items:center;gap:12px;margin:12px auto 8px auto;max-width:800px;padding:8px 0;border-top:1px solid #2a2a2a;flex-wrap:wrap} +.ai-snippets-used{color:#9aa;font-size:.95em} +.ai-train-approved{color:#5ad25a;font-weight:600} +.ai-train-rejected{color:#ff6b6b;font-weight:600} + +.modules-counts{display:flex;flex-wrap:wrap;gap:18px;align-items:baseline;padding:10px 0 !important;margin-bottom:10px;border:0 !important} +.modules-count-value{color:#ffa300} +.modules-count-list{list-style-type:none;padding:0;margin:0} +.modules-count-item{margin:0} +.modules-preset-form{display:inline;margin:0} +.module-status-cell{text-align:center} +.save-button-container{margin-top:20px;text-align:left} + +.votes-count-negative{margin-left:12px} +.forum-thread-header{display:flex;align-items:flex-start} +.root-vote-col{width:60px;text-align:center} +.forum-thread-header .forum-main-col{flex:1;padding-left:10px} +.new-message-wrapper{margin-top:12px} +.new-message-textarea{width:100%} +.new-message-form .forum-send-btn{margin-top:4px} + +.card-field-row{display:flex;gap:.6em;flex-wrap:wrap} +.card-section-action{margin-top:6px} +.card-section-text{margin:.4rem 0} +.post-text-clamped{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:16;overflow:hidden;max-height:none} +.post-text-clamped:has(video,audio,iframe,embed,object){display:block;-webkit-line-clamp:unset;max-height:none;overflow:visible} +.reply-context{border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:.85} +.reply-context-label{font-size:.85em} +.reply-context-text{font-size:.85em;max-height:80px;overflow:hidden;margin-top:4px} +.activity-spread-clamped{max-height:200px;overflow:hidden} +.activity-spread-missing{opacity:.6;font-style:italic} + +.search-select{position:relative;z-index:10} +.card-field-stacked{display:flex;flex-direction:column} +.card-value-pre{white-space:pre-wrap;margin-top:4px} + +.trending-container{display:grid;grid-template-columns:1fr;gap:20px} +.feed-vote-sep{margin:0 8px;opacity:.5} +.feed-vote-dominant{font-weight:700} +.image-container-row,.video-container-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} +.image-card .image-container-row, +.card-section .card-field.image-container, +.trending-card .card-field.image-container, +.opinions-card .card-field.image-container{display:block;text-align:center} +.video-container-row{flex-direction:column;align-items:center;width:100%} +.video-container-row video{width:100%;max-height:640px;margin:0 auto} +.vote-no-quorum{color:#ffcc00;font-weight:bold} + +.pixelia-swatch-000000{background-color:#000000} +.pixelia-swatch-ffffff{background-color:#ffffff} +.pixelia-swatch-17f018{background-color:#17f018} +.pixelia-swatch-ffbb00{background-color:#ffbb00} +.pixelia-swatch-ff0000{background-color:#ff0000} +.pixelia-swatch-0000ff{background-color:#0000ff} +.pixelia-swatch-ffff00{background-color:#ffff00} +.pixelia-swatch-00ff00{background-color:#00ff00} +.pixelia-swatch-00ffff{background-color:#00ffff} +.pixelia-swatch-ff00ff{background-color:#ff00ff} +.pixelia-swatch-a52a2a{background-color:#a52a2a} +.pixelia-swatch-800080{background-color:#800080} +.pixelia-swatch-808000{background-color:#808000} +.pixelia-swatch-00bfff{background-color:#00bfff} +.pixelia-swatch-d3d3d3{background-color:#d3d3d3} +.pixelia-swatch-ff6347{background-color:#ff6347} + +.school-lesson-header{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;flex-wrap:wrap} +.school-lesson-header h3{margin:0} +.school-lesson-header form{margin:0} +.school-complete-btn--done{border-color:#2ecc71;color:#2ecc71} + +.school-invited-list{border:none !important;background:transparent !important;box-shadow:none !important;padding:0;margin:0} +.stacked-action-row{flex-direction:column;align-items:flex-start;gap:6px} +.school-invite-students form{flex-direction:column;align-items:stretch;width:100%} +.school-invite-students form br{display:none} +.school-invite-students input[type="text"]{width:100%} +.school-invite-students form button{align-self:flex-start} +.stacked-action-row .subscription-form{margin:0} +.subscription-actions{display:flex;align-items:center;gap:8px;background:transparent !important;border:0 !important;padding:0 !important;margin:0;box-shadow:none !important} +.subscription-label-row{display:flex;align-items:center;gap:8px;background:transparent !important;border:0 !important;padding:0 !important;margin:0;box-shadow:none !important} +.pm-list-row{background:transparent !important;border:0 !important;padding:0 !important;margin:8px 0;box-shadow:none !important} +.school-progress-row{border:none !important;background:transparent !important;box-shadow:none !important;padding:0;margin:0 0 10px 0;display:flex;flex-wrap:wrap;align-items:center;gap:10px} +.school-progress-row p{margin:0} +.school-progress-issue{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0} +.school-progress-issue input[type="text"]{max-width:220px} +.school-progress-failed{font-size:0.85em;color:#e57373;cursor:help} +.school-lessons .school-lesson,.school-exams .school-lesson{background:transparent !important;border:0 !important;box-shadow:none;padding:0 !important;margin:0 0 16px 0} +.school-exam-question{background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;margin:0 0 14px 0} +.top-bar-center .ai-ask-form{flex:1 1 auto;min-width:0} +.top-bar-center:not(.top-bar-center-ainav){container-type:inline-size} +@container (max-width:180px){.ai-ask-row .ai-ask-form{display:none}} +.ai-suggestion-inline{display:inline-flex;align-items:center;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;margin:0 0 0 12px !important;box-shadow:none;font-size:.85em;white-space:nowrap;flex:0 1 auto;min-width:0;overflow:hidden} +.ai-suggestion-inline .update-banner-link{display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle} +.ai-suggestion-inline .welcome-banner-close{margin:0;padding:0;background:transparent;border:0;box-shadow:none;display:inline} +.activity-update-title{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap} +.tags-header:not(.module-header-line):not(.activity-header-line) > h2{display:inline-block;margin:0 12px 8px 0;vertical-align:middle} +.tags-header:not(.module-header-line):not(.activity-header-line) > h2 + p{display:inline-block;margin:0 0 8px 0;font-size:.9em;opacity:.8;vertical-align:middle} +.activity-header-line,.module-header-line{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 20px} +.activity-header-line h2,.module-header-line h2{margin:0;line-height:1.1} +.activity-header-line p,.module-header-line p{margin:0;font-size:.9em;opacity:.8;line-height:1.2;padding-top:2px} +.module-header-line .pm-exposition-chip,.module-header-line .pm-exposition-chip-link{align-self:center} +.tribe-mode-buttons,.ui-toolbar--filters,.mode-buttons,.mode-buttons-row,.mode-buttons-cols,.filters,.inhabitant-action{display:flex;flex-wrap:wrap;gap:5px !important;align-items:center} +.tribe-mode-buttons .filter-btn,.ui-toolbar--filters .filter-btn,.mode-buttons .filter-btn,.mode-buttons-row .filter-btn,.mode-buttons-cols .filter-btn,.filters .filter-btn,.inhabitant-action .filter-btn,.tribe-section-nav .filter-btn{background:transparent;color:inherit;font-size:.75em;padding:2px 9px;border:1px solid #555;border-radius:12px;white-space:nowrap;line-height:1.5} +.tribe-mode-buttons .filter-btn:hover,.ui-toolbar--filters .filter-btn:hover,.mode-buttons .filter-btn:hover,.mode-buttons-row .filter-btn:hover,.mode-buttons-cols .filter-btn:hover,.filters .filter-btn:hover,.inhabitant-action .filter-btn:hover,.tribe-section-nav .filter-btn:hover{border-color:#ffa300;color:#ffa300;background:transparent} +.tribe-mode-buttons .filter-btn.active,.ui-toolbar--filters .filter-btn.active,.mode-buttons .filter-btn.active,.mode-buttons-row .filter-btn.active,.mode-buttons-cols .filter-btn.active,.filters .filter-btn.active,.inhabitant-action .filter-btn.active,.tribe-section-nav .filter-btn.active{background:#ffa300;border-color:#ffa300;color:#111;font-weight:bold} +.tribe-mode-buttons .create-button,.ui-toolbar--filters .create-button,.mode-buttons .create-button,.mode-buttons-row .create-button,.filters .create-button{font-size:.75em;padding:2px 10px;border-radius:12px;border:1px solid #ffcc00;line-height:1.5;background:#ffcc00;color:#111;font-weight:bold} +.filter-box{padding:0;justify-content:flex-end;border:none;background:transparent} +.filter-box input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]),.filter-box select,.filter-box button{height:32px !important} +.filter-box input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]),.filter-box select{height:32px;padding:0 12px} +.activity-filter-chips{display:flex;flex-wrap:wrap;align-items:center;gap:5px;padding:10px 14px !important;margin-bottom:10px} +.activity-chip{text-decoration:none;font-size:.75em;padding:2px 9px;border:1px solid #555;border-radius:12px;color:inherit;white-space:nowrap;line-height:1.5} +.activity-chip:hover{border-color:#ffa300;color:#ffa300} +.activity-chip.active{background:#ffa300;border-color:#ffa300;color:#111;font-weight:bold} +.activity-toolbar-row{margin-top:-4px;max-width:100%;box-sizing:border-box} +.activity-toolbar-row .filter-box{margin-left:auto;flex:0 1 auto;flex-wrap:nowrap;max-width:100%;min-width:0} +.activity-toolbar-row .filter-box select{flex:0 1 auto !important;width:auto !important;max-width:220px;height:32px !important} +.activity-toolbar-row .filter-box .transfer-range{flex:0 1 auto !important;width:auto !important;min-width:0 !important;display:flex;gap:8px} +.activity-toolbar-row .filter-box .transfer-range input{flex:0 1 130px;min-width:90px;width:auto !important} +.activity-toolbar-row .filter-box__controls{gap:8px !important;justify-content:flex-end !important} +.activity-toolbar-row .filter-box__controls > *{margin:0 !important;flex:0 1 auto !important;width:auto !important} +.activity-toolbar-row .filter-box__controls > .transfer-range{min-width:0 !important} +.activity-toolbar-row .inbox-exposition{margin:0 !important;padding:0 !important;background:transparent !important;border:0 !important;box-shadow:none;display:flex;align-items:center;gap:8px} +.activity-toolbar-row .module-stats-line{margin:0 !important;padding:0 !important;background:transparent !important;border:0 !important;box-shadow:none;display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:.72em;white-space:nowrap;text-transform:uppercase} +.module-stats-line strong{text-transform:none} +.activity-toolbar-row .inbox-exposition .btn{margin:0;padding:2px 10px;font-size:.75em;border-radius:12px;line-height:1.5} +.activity-toolbar-row .inbox-exposition form{margin:0;padding:0;background:transparent;border:0;box-shadow:none} +.activity-toolbar-row .filter-box > input[type="number"]{flex:0 0 150px;min-width:150px} +.activity-toolbar-row .filter-box > .housing-place-input{flex:0 1 220px;min-width:140px} +.activity-toolbar-row .filter-box__controls{flex-wrap:nowrap;flex:0 0 auto} +.activity-toolbar-row .filter-box .filter-box__input{min-width:0} +.activity-toolbar-row .filter-box__controls{flex:0 0 auto} +.filters{padding:10px 14px;margin-bottom:10px} +.filters,.mode-buttons,.mode-buttons-row,.tribe-mode-buttons,.inhabitant-action,.activity-filter-chips,.activity-sub-filter,.filter-group,.activity-filter-grid,.logs-toolbar-wrap{border:0 !important} +.mode-buttons-row,.mode-buttons,.tribe-mode-buttons,.inhabitant-action{padding:10px 14px !important;margin-bottom:10px !important} +.mode-buttons-cols{display:contents !important} +.filters > form:not(.filter-box),.inhabitant-action > form,.tribe-mode-buttons > form,.mode-buttons > form,.mode-buttons-row > form,.filter-group > form,.activity-filter-grid > form{display:contents} +.mode-buttons .column,.mode-buttons-row .column,.mode-buttons-row .filter-column,.mode-buttons-cols .filter-column,.mode-buttons-row .filter-group,.mode-buttons .filter-group,.forum-create-col,.activity-filter-col{display:contents !important} +.activity-filter-grid,.filter-group,.activity-sub-filter{display:flex !important;flex-wrap:wrap;gap:5px !important;align-items:center;padding:10px 14px !important;margin-bottom:10px !important;grid-template-columns:none !important} +.activity-filter-grid form,.filter-group form,.activity-sub-filter form,.sub-filter-form{margin:0 !important;padding:0 !important;background:transparent !important;border:none !important;box-shadow:none !important} +.activity-filter-grid .filter-btn,.filter-group .filter-btn,.activity-sub-filter .filter-btn{margin:0;background:transparent;color:inherit;font-size:.75em;padding:2px 9px;border:1px solid #555;border-radius:12px;white-space:nowrap;line-height:1.5} +.activity-filter-grid .filter-btn.active,.filter-group .filter-btn.active,.activity-sub-filter .filter-btn.active{background:#ffa300;border-color:#ffa300;color:#111;font-weight:bold} +.filter-group .create-button,.activity-sub-filter .create-button,.activity-filter-grid .create-button{margin:0;font-size:.75em;padding:2px 10px;border-radius:12px;border:1px solid #ffcc00;background:#ffcc00;color:#111;font-weight:bold;line-height:1.5} +.mode-buttons-row form,.mode-buttons form,.tribe-mode-buttons form{margin:0 !important;padding:0 !important;background:transparent !important;border:none !important;box-shadow:none !important} +.mode-buttons-row .filter-btn,.mode-buttons-row .create-button,.mode-buttons .filter-btn,.mode-buttons .create-button,.tribe-mode-buttons .filter-btn,.tribe-mode-buttons .create-button{margin:0} +.filters form{margin:0;padding:0;background:transparent;border:none;box-shadow:none} +.filters .filter-btn,.filters .create-button,.ui-toolbar--filters .filter-btn,.ui-toolbar--filters .create-button{margin:0} +.activity-subchip-label{color:#ffa300;font-weight:bold} +.transfers-subfilters{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:8px 0 0;background:transparent !important;border:0 !important;padding:0 !important} +.activity-update-counts{white-space:nowrap} +.activity-update-msg{margin:10px 15px 0 0} +.activity-update-msg .post-text{margin:0} +.school-exam-question p{margin:2px 0} +.school-exam-correct{color:#2ecc71} +.school-exam-option-row{color:#999} +.school-panel-box{border:1px solid #444;border-radius:8px;margin:0 0 14px 0} +.school-panel-box h3{margin:0 0 10px 0;color:#ffa300;font-size:.9em;text-transform:uppercase;letter-spacing:1px} +.school-students-table{width:100%;border-collapse:collapse} +.school-students-table th{text-align:left;color:#ffa300;font-size:.85em;text-transform:uppercase;letter-spacing:1px;padding:6px 10px;border-bottom:1px solid #444} +.school-students-table td{padding:6px 10px;border-bottom:1px solid #333;vertical-align:middle} +.school-students-table tr.school-row-passed td{background:rgba(46,204,113,.08)} +.school-students-table .school-progress-issue input[type="text"]{max-width:160px} +.school-progress-bar{flex:1 1 auto;min-width:100px;height:12px;accent-color:#ffa300} +.school-progress-count{font-size:.85em;white-space:nowrap} +.school-unit{background:transparent !important;border:0 !important;padding:0 !important;margin:0 0 14px 0;box-shadow:none} +.school-unit-summary{cursor:pointer;list-style:none;font-weight:bold;color:#ffa300;text-transform:uppercase;letter-spacing:1px;margin:0 0 8px 0} +.school-unit-summary::-webkit-details-marker{display:none} +.school-unit-summary::after{content:" \25BE";opacity:.7} +details[open] > .school-unit-summary::after{content:" \25B4"} +.school-lessons .school-lesson-next{border-left:3px solid #ffa300 !important;padding-left:10px !important} +.school-pending-block{border:none !important;background:transparent !important;box-shadow:none !important;padding:0;margin:0} +.school-pending-row{justify-content:center;border:none !important;background:transparent !important;box-shadow:none !important;padding:0;margin:0} +.school-pending-block .doc-export-actions{margin-top:8px;margin-bottom:0} + +.wiki-body{line-height:1.55} +.wiki-paragraph{margin:0 0 12px 0} +.wiki-link{color:#ffd740;text-decoration:underline dotted} +.wiki-link-missing{color:#e74c3c;border-bottom:1px dashed #e74c3c;text-decoration:none} +.wiki-page-title{margin:0 0 8px 0;overflow-wrap:anywhere} +.wiki-version-notice{font-size:.9em;opacity:.85;margin:6px 0 10px 0} +.wiki-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0 4px 0;background:transparent !important;border:0 !important;padding:0 !important} +.wiki-actions form{margin:0;padding:0;background:transparent !important;border:0 !important} +.wiki-backlinks ul{margin:6px 0 0 18px;padding:0} +.wiki-backlinks li{margin:2px 0} +.wiki-diff{font-family:monospace;font-size:.9em;white-space:pre-wrap;overflow-x:auto;background:transparent !important;border:0 !important;padding:0 !important} +.wiki-diff-line{display:flex;gap:8px;padding:1px 6px;background:transparent !important;border:0 !important;margin:0} +.wiki-diff-sign{width:1em;flex:0 0 1em;opacity:.7} +.wiki-diff-add{background:rgba(46,204,113,.16) !important} +.wiki-diff-del{background:rgba(231,76,60,.16) !important;text-decoration:line-through;opacity:.85} +.wiki-history .wiki-version,.wiki-changes .wiki-change{margin-bottom:10px} +.wiki-version-current{border-left:3px solid #ffa500} +.wiki-change-summary{margin:4px 0;opacity:.9} + +.emergency-severity{display:inline-block;font-size:.72em;font-weight:bold;letter-spacing:.06em;padding:2px 9px;border-radius:12px;border:1px solid transparent;white-space:nowrap} +.emergency-severity-unverified{color:#fff;border-color:#c0392b;background:#c0392b} +.emergency-severity-low{color:#111;background:#f1c40f;border-color:#f1c40f} +.emergency-severity-medium{color:#111;background:#e67e22;border-color:#e67e22} +.emergency-severity-high{color:#fff;background:#e74c3c;border-color:#e74c3c} +.emergency-card-low{border-left:3px solid #f1c40f} +.emergency-card-medium{border-left:3px solid #e67e22} +.emergency-card-high{border-left:3px solid #e74c3c} +.emergency-title{margin:0 0 8px 0;overflow-wrap:anywhere} +.emergency-meta{margin:6px 0;font-size:.9em;opacity:.85} +.emergency-body{line-height:1.5;margin:8px 0} +.emergency-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0 4px 0;background:transparent !important;border:0 !important;padding:0 !important} +.emergency-actions form{margin:0;padding:0;background:transparent !important;border:0 !important} +.emergency-updates{margin-top:14px} +.emergency-update{margin:8px 0;padding:8px 12px !important;border-left:2px solid #ffa500} +.emergency-update-form{margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important} +.emergency-update-form textarea{width:100%;max-width:640px} +.emergency-banner-high{border-color:#e74c3c} +.emergency-banner-medium{border-color:#e67e22} +.emergency-banner .update-banner-icon{color:#e74c3c} + +.mailing-counts{margin:0;opacity:.85} +.mailing-archive{list-style:none;margin:0;padding:0;background:transparent !important;border:0 !important} +.mailing-archive-item{margin:0 0 12px 0;padding:12px 14px !important;border-left:2px solid #ffa300} +.mailing-archive-head{margin:0 0 8px 0} +.mailing-archive-item>div{margin:0;padding:0 !important;background:transparent !important;border:0 !important;box-shadow:none} +.mailing-archive-head>.content-actions,.mailing-archive-head>.card-header{margin:0;padding:0 !important} +.mailing-archive-meta{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.mailing-archive-row{margin:0} +.mailing-archive-meta>.user-link{max-width:100%;overflow-wrap:anywhere} +.mailing-archive-meta>*{margin:0} +.mailing-description{margin:6px 0;overflow-wrap:anywhere} +.mailing-title{margin:0 0 8px 0;overflow-wrap:anywhere} +.mailing-body{line-height:1.5;margin:8px 0} +.mailing-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0 4px 0;background:transparent !important;border:0 !important;padding:0 !important} +.mailing-actions form{margin:0;padding:0;background:transparent !important;border:0 !important} +.mailing-participants{margin:8px 0;background:transparent !important;border:0 !important;padding:0 !important} +.mailing-participants-list{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:6px;background:transparent !important;border:0 !important;padding:0 !important} +.mailing-participants-list div{background:transparent !important;border:0 !important;padding:0 !important} +.mailing-history{margin-top:14px} +.mailing-history .filter-box{margin:8px 0} +.mailing-message{margin:10px 0;padding:10px 12px !important;border-left:2px solid #8e44ad} +.mailing-message-head{display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;margin-bottom:6px;background:transparent !important;border:0 !important;padding:0 !important} +.mailing-message-subject{font-weight:bold;overflow-wrap:anywhere} +.mailing-message-text{line-height:1.5;overflow-wrap:anywhere;background:transparent !important;border:0 !important;padding:0 !important} +.mailing-reply-form{margin:6px 0 0 0;padding:0;background:transparent !important;border:0 !important} +.mailing-thread{margin:8px 0;background:transparent !important;border:0 !important;padding:0 !important} +.mailing-thread-details{margin:0 0 0 16px;background:transparent !important;border:0 !important;padding:0 !important} +.mailing-thread-details summary{cursor:pointer;font-size:.9em;opacity:.85} +.mailing-author-group{margin:10px 0;background:transparent !important;border:0 !important;padding:0 !important} +.mailing-author-title{margin:0 0 4px 0;font-size:1em} +.mailing-author-count{opacity:.7} +.mailing-compose{margin:14px 0} +.mailing-compose textarea{width:100%;max-width:640px} +.mailing-inbox-list{font-weight:bold} +.mailing-inbox-reply{display:flex;flex-direction:column;gap:6px;align-items:flex-start;width:100%} +.mailing-inbox-reply-text{width:100%;max-width:640px} + +.logistics-route-line{margin:6px 0;font-weight:bold;overflow-wrap:anywhere} +.logistics-zone{overflow-wrap:anywhere} +.logistics-zones{align-items:center} +.logistics-price-unit{width:auto;margin-left:6px} +.logistics-short{width:120px} +.logistics-title{margin:0 0 8px 0;overflow-wrap:anywhere} +.logistics-body{line-height:1.5;margin:8px 0} +.logistics-rating-line{color:#f1c40f} +.logistics-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0 4px 0;background:transparent !important;border:0 !important;padding:0 !important} +.logistics-actions form{margin:0;padding:0;background:transparent !important;border:0 !important} +.logistics-book-form{background:transparent !important;border:0 !important;padding:0 !important} +.logistics-book-form textarea{width:100%;max-width:640px} +.logistics-bookings{margin-top:14px} +.logistics-booking{margin:8px 0;padding:8px 12px !important;border-left:2px solid #16a085} +.logistics-booking-head{display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;background:transparent !important;border:0 !important;padding:0 !important} +.logistics-booking-notes{margin:6px 0;overflow-wrap:anywhere} +.logistics-receipt{margin:6px 0;font-size:.9em} +.logistics-booking-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;background:transparent !important;border:0 !important;padding:0 !important} +.logistics-booking-actions form,.logistics-receipt-form,.logistics-rate-form{margin:6px 0 0 0;padding:0;background:transparent !important;border:0 !important} +.logistics-receipt-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center} +.logistics-ratings{margin-top:14px} +.logistics-rating{margin:8px 0;padding:8px 12px !important;border-left:2px solid #f1c40f} +.logistics-rating-head{display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;background:transparent !important;border:0 !important;padding:0 !important} +.logistics-stars{color:#f1c40f;letter-spacing:.1em} +.logistics-rate-form textarea{width:100%;max-width:640px} + +.podcast-card-cover{width:100%;max-height:220px;object-fit:cover;border-radius:6px;display:block;margin:0 0 8px 0} +.podcast-cover{width:120px;height:120px;object-fit:cover;border-radius:6px;display:block} +.podcast-cover-large{width:220px;height:220px} +.podcast-page-body{display:flex;gap:18px;align-items:flex-start;flex-wrap:wrap} +.podcast-page-main{flex:1 1 320px;min-width:0;background:transparent !important;border:0 !important;padding:0 !important} +.podcast-description{margin:6px 0;overflow-wrap:anywhere} +.podcast-counts{margin:6px 0;opacity:.85} +.podcast-title{margin:0 0 8px 0;overflow-wrap:anywhere} +.podcast-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0 4px 0;background:transparent !important;border:0 !important;padding:0 !important} +.podcast-actions form{margin:0;padding:0;background:transparent !important;border:0 !important} +.podcast-episodes{margin-top:14px} +.podcast-episode-list{background:transparent !important;border:0 !important;padding:0 !important} +.podcast-episode-row{display:flex;gap:12px;align-items:flex-start;margin:8px 0;padding:8px 12px !important;border-left:2px solid #d35400} +.podcast-episode-number{font-weight:bold;opacity:.8;flex:0 0 auto} +.podcast-episode-main{flex:1 1 auto;min-width:0;background:transparent !important;border:0 !important;padding:0 !important} +.podcast-episode-title{font-weight:bold;overflow-wrap:anywhere;display:inline-block;margin:0} +.podcast-episode-main .tribe-side-description{margin:0 0 8px 0} +.podcast-episode-main .card-footer{margin:0} +.podcast-episode-head{margin:0 0 10px 0} +.podcast-featured{margin:0 0 16px 0;background:transparent !important;border:0 !important;padding:0 !important} +.podcast-cover-link{display:inline-block;border:0 !important;background:transparent !important;padding:0 !important} +.tribe-side .card-tags + .tribe-side-actions{margin-top:0} +.podcast-episode-head .tribe-card-title{margin:0} +.podcast-episode-head-left{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important;margin:0} +.podcast-player{margin:10px 0;background:transparent !important;border:0 !important;padding:0 !important} +.podcast-player audio{width:100%;max-width:100%} +.podcast-player.video-container .podcast-video{width:100%;max-width:100%;max-height:640px;display:block} +@media (max-width:680px){.podcast-cover-large{width:140px;height:140px}} + +.campaign-progress{margin:8px 0} +.campaign-title{margin:0 0 8px 0;overflow-wrap:anywhere} +.campaign-body{line-height:1.5;margin:8px 0} +.campaign-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0 4px 0;background:transparent !important;border:0 !important;padding:0 !important} +.campaign-actions form{margin:0;padding:0;background:transparent !important;border:0 !important} +.campaign-page-body{display:flex;gap:18px;align-items:flex-start;flex-wrap:wrap} +.campaign-page-main{flex:1 1 320px;min-width:0;background:transparent !important;border:0 !important;padding:0 !important} +.campaign-signed{text-align:center;margin:8px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.campaign-qr{flex:0 0 auto;text-align:center;background:transparent !important;border:0 !important;padding:0 !important} +.campaign-qr-img{width:160px;height:160px;display:block;margin:0 auto;background:#fff;border-radius:6px} +.campaign-qr-caption{font-size:.8em;opacity:.8;max-width:180px;margin:6px auto 0 auto} +.campaign-sign-form{background:transparent !important;border:0 !important;padding:0 !important} +.campaign-sign-form textarea,.campaign-update-form textarea{width:100%;max-width:640px} +.campaign-updates,.campaign-signatures{margin-top:14px} +.campaign-update{margin:8px 0;padding:8px 12px !important;border-left:2px solid #c0392b} +.campaign-update-form{margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important} +.campaign-signature{margin:6px 0;padding:6px 12px !important;border-left:2px solid #27ae60} +.campaign-signature-head{display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;background:transparent !important;border:0 !important;padding:0 !important} +.campaign-signature-text{margin:4px 0 0 0;overflow-wrap:anywhere} +.campaign-signature-list{background:transparent !important;border:0 !important;padding:0 !important;margin-bottom:8px} +.campaign-achieved{border-left:3px solid #27ae60} + +.backup-section{background:transparent !important;border:0 !important;padding:0 !important} +.backup-section-title{margin:0 0 10px 0} +.backup-estimate{margin:0 0 12px 0;background:transparent !important;border:0 !important;padding:0 !important} +.backup-hint{font-size:.85em;opacity:.8;margin:4px 0} +.backup-warning{color:#e74c3c;font-weight:bold} +.backup-verification ul{margin:6px 0;padding-left:18px} +.backup-restored{margin-bottom:12px;background:transparent !important;border:0 !important;padding:0 !important} +.backup-kit{max-width:none;margin:0;background:transparent !important;border:0 !important;padding:0 !important} +.backup-wrap{background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.backup-kit-line{margin:6px 0;overflow-wrap:anywhere} +.backup-kit-qr{display:flex;justify-content:center;margin:12px 0;background:transparent !important;border:0 !important;padding:0 !important} +.backup-kit-qr-img{width:260px;height:260px;background:#fff;border-radius:6px;padding:8px} +.backup-kit-secret{white-space:pre;overflow-x:auto;font-size:1em;line-height:1.6;padding:10px;border:1px dashed currentColor;border-radius:6px} +.backup-kit-steps{margin:10px 0 0 0;padding-left:18px} +.backup-kit .doc-export-actions{margin-top:6px} +.backup-verification{margin-top:10px} +.backup-check-ok{color:#27ae60} +.backup-check-bad{color:#e74c3c} +.backup-check-warn{color:#f1c40f} +.backup-fork{overflow-wrap:anywhere;font-size:.9em} +@media print{nav,.sidebar-left,.sidebar-right,.filters,.report-form,.module-header-line p{display:none !important}.backup-kit{max-width:none}.backup-kit-secret{color:#000;border-color:#000}body{background:#fff;color:#000}} +.wiki-card-cover{width:100%;max-height:220px;object-fit:cover;border-radius:6px;display:block;margin:0 0 8px 0} +.wiki-actions-top{justify-content:flex-end;margin:0 0 10px 0} +.wiki-actions-top .wiki-actions-left{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-right:auto} +.wiki-version-actions{justify-content:flex-start;margin:6px 0} +.wiki-preview,.blog-preview{margin:0 0 14px 0} +.stats-kpi{margin:0 !important;padding:0 !important;box-shadow:none !important;overflow:visible} + +.stats-kpi-label{white-space:nowrap} +.stats-block,.stats-container{box-shadow:none !important} +.emergency-card-cover{width:100%;max-height:220px;object-fit:cover;border-radius:6px;display:block;margin:0 0 8px 0} +.trending-card .card-field.media-left{text-align:left;align-items:flex-start} +.trending-card .media-left .zoom-link:not(.zoom-inline){display:inline-block;text-align:left} +.trending-card .media-left .post-image{margin-left:0;margin-right:0} +.campaign-card-cover,.logistics-card-cover{width:100%;max-height:220px;object-fit:cover;border-radius:6px;display:block;margin:0 0 8px 0} +.campaign-progress .confirmations-progress{width:100%;margin-top:6px} +.logistics-booking,.logistics-rating,.mailing-message{margin:8px 0} +.emergency-update-head{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 6px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.emergency-update-head .card-chips-row{margin:0} +.emergency-update-actions{margin:0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.emergency-update-head .card-chips-row{margin:0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.backup-kit-id{font-family:monospace;font-size:1.1em} + +.preview-rendered{white-space:pre-wrap} +.blog-card-text, +.blog-detail-text, +.feed-text, +.emergency-body, +.mailing-message-text{white-space:pre-wrap} +.rt-header{display:block;font-weight:bold;margin:10px 0 4px;line-height:1.3} +.rt-header-1{font-size:1.5em} +.rt-header-2{font-size:1.3em} +.rt-header-3{font-size:1.15em} +.rt-item{display:block;padding-left:1.4em;text-indent:-0.7em} +.rt-item-2{padding-left:3em} +.rt-item-3{padding-left:4.6em} +.rt-item-4{padding-left:6.2em} +.rt-item::before{content:"• ";opacity:.7} +.rt-item-number{text-indent:-1.2em} +.rt-item-number::before{content:none} +.rt-item .tag-link{text-indent:0} +.rt-quote{display:block;border-left:3px solid currentColor;padding-left:10px;margin:6px 0;opacity:.85} +.rt-rule{display:block;border-top:1px solid currentColor;opacity:.4;margin:10px 0} +.rt-code{font-family:monospace;background:rgba(128,128,128,.18);padding:1px 4px;border-radius:3px;word-break:break-word} +.rt-code-block{display:block;font-family:monospace;white-space:pre-wrap;background:rgba(128,128,128,.18);padding:10px;border-radius:5px;margin:8px 0;overflow-x:auto;word-break:break-word} +.rt-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;width:fit-content;max-width:100%;margin:14px 0 0 !important;padding:5px 10px !important;font-size:.95em;background:rgba(128,128,128,.13) !important;box-shadow:none !important;border:none !important;border-radius:5px} +.rt-toolbar-label{font-weight:bold} +.rt-sep{opacity:.4} +.rt-mark{font-family:monospace;white-space:nowrap} +.rt-mark-bold{font-weight:bold} +.rt-mark-italic{font-style:italic} +.rt-mark-underline{text-decoration:underline} +.rt-mark-strike{text-decoration:line-through} + +.qr-code{display:flex;justify-content:center;align-items:center;width:100%} +.wallet-qr-img{max-width:320px;width:100%;height:auto} +label.wallet-create-transfer{display:flex !important;flex-direction:row !important;align-items:center !important;justify-content:flex-start !important;gap:8px;text-align:left !important;width:100% !important;margin:8px 0} +label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0 0 auto !important;margin:0 !important} +.wallet-status{margin:0 0 12px 0} +.wallet-history .wallet-col-confirmations{width:10%} +.wallet-history .wallet-col-date{width:34%;white-space:nowrap} +.torrent-file-info{margin:0 0 10px 0} +.hemicycle{background:transparent;border:0;padding:0;margin:12px 0} +.hemicycle-canvas{background:transparent;border:0;padding:0;margin:0 auto;max-width:760px} +.hemicycle-svg{width:100%;height:auto;display:block} +.hemi-seat{fill:#444;stroke:#1a1a1a;stroke-width:1} +.hemi-seat.hemi-leader{stroke:#fff;stroke-width:2.5} +.hemi-seat.hemi-power{stroke:#ffd700;stroke-width:2} +.hemi-bloc-gov{fill:#ffa300} +.hemi-bloc-opp0{fill:#ab47bc}.hemi-bloc-opp1{fill:#ef5350}.hemi-bloc-opp2{fill:#26a69a}.hemi-bloc-opp3{fill:#ec407a}.hemi-bloc-opp4{fill:#8d6e63}.hemi-bloc-opp5{fill:#ffca28}.hemi-bloc-opp6{fill:#4fc3f7}.hemi-bloc-opp7{fill:#9e9e9e} +.hemi-bloc-none{fill:#2f6fb5} +.hemi-house-academia{fill:#5c6bc0}.hemi-house-solaris{fill:#ffb300}.hemi-house-arrakis{fill:#d2691e}.hemi-house-terraverde{fill:#43a047}.hemi-house-unsystem{fill:#9e9e9e}.hemi-house-dogma{fill:#8e24aa}.hemi-house-helix{fill:#00acc1}.hemi-house-quark{fill:#e53935}.hemi-house-hermandad{fill:#c0a000} +.hemi-threshold{stroke:#ddd;stroke-width:1.5;stroke-dasharray:4 3} +.hemi-threshold-quorum{stroke:#66bb6a} +.hemi-threshold-pass{stroke:#ffa300} +.hemi-threshold-label{font-size:11px;font-weight:400;fill:#ddd} +.hemi-threshold-label.hemi-threshold-quorum{fill:#66bb6a} +.hemi-threshold-label.hemi-threshold-pass{fill:#ffa300} +.hemi-modes{padding:0 !important;margin:0 0 8px 0;display:flex;gap:6px;justify-content:center;background:transparent !important;border:0 !important} +.hemi-legend{list-style:none;margin:10px auto 0;padding:0;display:flex;flex-wrap:wrap;gap:6px 18px;justify-content:center} +.hemi-legend-item{display:inline-flex;align-items:center;gap:6px;font-size:13px} +.hemi-swatch{display:inline-block;width:12px;height:12px;border-radius:50%;background:#444} +.hemi-swatch.hemi-bloc-gov{background:#ffa300} +.hemi-swatch.hemi-bloc-opp0{background:#ab47bc}.hemi-swatch.hemi-bloc-opp1{background:#ef5350}.hemi-swatch.hemi-bloc-opp2{background:#26a69a}.hemi-swatch.hemi-bloc-opp3{background:#ec407a}.hemi-swatch.hemi-bloc-opp4{background:#8d6e63}.hemi-swatch.hemi-bloc-opp5{background:#ffca28}.hemi-swatch.hemi-bloc-opp6{background:#4fc3f7}.hemi-swatch.hemi-bloc-opp7{background:#9e9e9e} +.hemi-swatch.hemi-bloc-none{background:#2f6fb5} +.hemi-swatch.hemi-house-academia{background:#5c6bc0}.hemi-swatch.hemi-house-solaris{background:#ffb300}.hemi-swatch.hemi-house-arrakis{background:#d2691e}.hemi-swatch.hemi-house-terraverde{background:#43a047}.hemi-swatch.hemi-house-unsystem{background:#9e9e9e}.hemi-swatch.hemi-house-dogma{background:#8e24aa}.hemi-swatch.hemi-house-helix{background:#00acc1}.hemi-swatch.hemi-house-quark{background:#e53935}.hemi-swatch.hemi-house-hermandad{background:#c0a000} +.hemi-legend-link{color:#ffa300;text-decoration:none} +.hemi-legend-count{color:#ffd700;font-size:12px} +.hemi-legend-item{color:#ffd700} +.hemi-note{text-align:center;color:#ffd700;font-size:12px;margin:6px 0 0} +.torrent-oasis{margin:8px 0 14px} +.torrent-progress{width:160px;max-width:100%;vertical-align:middle} +.torrent-progress-text{font-family:monospace;font-size:.85em} +.torrent-progress-cell{white-space:nowrap} +.torrent-from-notice p{margin:0 0 10px} +.btn-torrent{font-weight:700} +.peertube-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:14px} +.peertube-card{display:flex;flex-direction:column;gap:8px;padding:10px} +.peertube-thumb{position:relative;display:block;border-radius:6px;overflow:hidden;background:#000} +.peertube-thumb img{display:block;width:100%;aspect-ratio:16/9;object-fit:cover} +.peertube-thumb-empty{width:100%;aspect-ratio:16/9;background:#222} +.peertube-duration{position:absolute;right:6px;bottom:6px;background:rgba(0,0,0,.8);color:#fff;font-size:.78em;padding:1px 6px;border-radius:4px;font-family:monospace} +.peertube-title{font-weight:700;display:block} +.peertube-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:.85em;opacity:.9} +.peertube-channel-avatar{width:22px;height:22px;border-radius:50%;object-fit:cover} +.peertube-player{width:100%;max-height:70vh;background:#000;border-radius:6px} +.peertube-watch{gap:10px} +.peertube-description{white-space:pre-wrap;word-break:break-word} +.peertube-nostream p{margin:8px 0 0} +.torrent-table td{vertical-align:middle} +.torrent-spread-cell,.torrent-table th.torrent-spread-cell,.torrent-table td.torrent-spread-cell{text-align:center;white-space:nowrap} +.torrent-spread-cell .spread-form{display:inline-block;margin:0;vertical-align:middle} +.torrent-spread-cell .btn-singleview{margin:0;vertical-align:middle} +.torrent-detail-section{margin:0 0 18px} +.torrent-section-title{margin:0 0 8px;font-size:1em;text-transform:uppercase;letter-spacing:1px;opacity:.85} +.torrent-detail-section .tribe-info-table{margin:0 0 10px} +.pad-page .tribe-side .pad-actions-top{flex-basis:100%;display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin:0} +.pad-page .tribe-side .pad-actions-top .card-header{margin:0;padding:0;border:none;background:transparent;box-shadow:none} +.pad-page .tribe-side .pad-actions-top form{margin:0;padding:0} +.pad-page .tribe-side .shop-title-row{flex-basis:auto} +.torrent-card-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:0;margin:0;border:none;background:transparent;box-shadow:none} +.feed-preview{margin:14px 0} +.feed-preview-title{font-size:1em;margin:0 0 8px;text-transform:uppercase;letter-spacing:1px;opacity:.85} +.feed-compose-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px} +.peer-tor-no{color:#888;font-weight:700} +.room-join-form{display:inline-flex;margin:0;padding:0;background:transparent !important;border:0 !important;box-shadow:none} +.room-join-btn,.room-join-form .room-join-btn{display:inline-flex;align-items:center;gap:4px;border-radius:10px;font-size:12px;font-weight:700;line-height:1.4;padding:2px 10px;margin:0;width:auto;height:auto;min-height:0;border:1px solid #ffa500;background:transparent;color:#ffa500;box-shadow:none;cursor:pointer;text-decoration:none} +.room-join-btn:hover,.room-join-form .room-join-btn:hover{background:#ffa500;color:#1a1a1a} +.room-join-form .room-join-btn:disabled{opacity:.5;cursor:default} +.room-item .mailing-archive-head{flex-wrap:nowrap;align-items:flex-start} +.room-item .mailing-archive-meta{flex:1 1 0;min-width:0} +.room-peer{flex-wrap:nowrap;min-width:0} +.room-peer .user-link{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.room-peer-you{flex:0 0 auto;color:#888;font-size:12px;background:transparent;border:0;padding:0} +.room-peer-muted{flex:0 0 auto;color:#ff6b6b;font-size:11px;font-weight:700;border:1px solid #ff6b6b;border-radius:10px;padding:0 8px;line-height:1.5} +.room-peer-hand{flex:0 0 auto;color:#FFA500;font-size:11px;font-weight:700;border:1px solid #FFA500;border-radius:10px;padding:0 5px;line-height:1.5;white-space:nowrap} +.room-peer-raised{border-left:3px solid #FFA500} +.tribe-action-btn.room-hand-btn-on{border-color:#FFA500;color:#FFA500} +.room-peer .phone-action-form{flex:0 0 auto;margin-left:auto} +.room-live-controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end;margin:0;padding:0;border:none;background:transparent;flex:1 0 100%;min-width:0} +.room-live-controls>form,.room-live-controls>a,.room-live-controls>span{display:inline-flex;align-items:center;margin:0;padding:0;flex:0 0 auto} +.room-live-controls .tribe-action-btn,.room-live-controls .room-refresh-btn{margin:0;display:inline-flex;align-items:center;justify-content:center;height:28px;line-height:1;padding:0 12px;font-family:inherit;font-size:12px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;white-space:nowrap;width:auto;min-width:0;max-width:none;overflow:visible;box-sizing:border-box;border-width:1px;border-style:solid;border-radius:6px;box-shadow:none;flex:0 0 auto} +.room-live-controls>*{min-width:0;flex:0 0 auto} +.room-live-head,.room-live-controls,.room-live-controls>form{overflow:visible} +.room-live-controls .room-rec-chip{min-width:max-content;padding:2px 8px} +.room-live-head{flex-wrap:wrap;align-items:center} +.room-live-head-main{flex:1 1 160px} +.room-rec-chip{flex:0 0 auto;color:#ff4d4d;font-size:11px;font-weight:700;border:1px solid #ff4d4d;border-radius:10px;padding:0 8px;line-height:1.5;white-space:nowrap} +.room-notify-off{opacity:.6} +.room-events{list-style:none;margin:10px 0 0 0;padding:0;display:flex;flex-direction:column;gap:4px;font-size:12px} +.room-event{display:flex;gap:8px;align-items:center;flex-wrap:nowrap;min-width:0} +.room-event .date-link{flex:0 0 auto;margin:0} +.room-event-text{display:flex;gap:6px;align-items:center;flex-wrap:nowrap;min-width:0;white-space:nowrap} +.room-event-text .user-link{flex:0 1 auto;min-width:0;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block} +.room-recordings{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px} +.room-recording{display:flex;gap:8px;align-items:center;flex-wrap:wrap} +.room-peer .phone-action-form .tribe-action-btn{padding:2px 8px;font-size:11px} +.phone-call-peers{margin:8px 0 0 0} +.block-info-table .pm-exposition-chip{white-space:nowrap} +.torrent-table td > .pm-exposition-chip-link,.torrent-table td > .pm-exposition-chip,.file-table td > .pm-exposition-chip-link,.file-table td > .pm-exposition-chip{margin-left:10px;vertical-align:middle;position:relative;top:-3px} +.peers-technical-block .inline-form .filter-btn{white-space:nowrap} +.peers-technical-block .peer-port{white-space:nowrap} +.peers-technical-block .peer-host{overflow-wrap:normal;word-break:normal} +.phone-chip-form{display:inline-flex;margin:0;padding:0;background:transparent !important;border:0 !important;box-shadow:none} +.phone-chip-btn{display:inline-flex;align-items:center;background:transparent !important;border:0 !important;padding:0 !important;margin:0;width:auto;height:auto;min-height:0;box-shadow:none;cursor:pointer;font:inherit;line-height:inherit;color:inherit} +.phone-chip-btn:hover .pm-exposition-chip,.phone-chip-btn:hover .phone-number-value{filter:brightness(1.2)} +.phone-flash-anchor{display:none} +.clearnet-choice{display:block;margin:0 0 10px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.clearnet-choice-options{display:flex;align-items:center;gap:18px;flex-wrap:wrap;margin:6px 0 4px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important} +.clearnet-choice-option{display:inline-flex;align-items:center;gap:8px;cursor:pointer;white-space:nowrap;width:auto;margin:0} +.clearnet-choice-option input{width:auto;min-width:0;margin:0;flex:0 0 auto} +.clearnet-choice-option .pm-exposition-chip{margin:0} + +.clearnet-switch{display:inline-flex;margin:0;padding:0;border:1px solid #555;border-radius:6px;overflow:hidden;background:transparent !important;box-shadow:none} +.clearnet-switch-btn{margin:0;border:0;border-radius:0;padding:5px 14px;font-size:12px;font-weight:700;letter-spacing:.5px;background:transparent;color:#bbb;cursor:pointer;min-height:0;width:auto} +.clearnet-switch-btn+.clearnet-switch-btn{border-left:1px solid #555} +.clearnet-switch-btn.active{background:#ffa500;color:#000;cursor:default} +.clearnet-switch-btn:disabled{opacity:1} +.votations-comment-card,.larp-post{position:relative} +.larp-post.own-comment > .larp-post-head{padding-right:48px;box-sizing:border-box} +.comment-delete-form{position:absolute;top:12px;right:12px;margin:0;padding:0;background:transparent !important;border:0 !important;box-shadow:none} +.votations-comment-card.own-comment > .created-at{display:block;padding-right:48px;box-sizing:border-box} +.comment-form .comment-textarea{display:block;max-width:100%;margin:0} +.comments-latest-anchor{background:transparent !important;border:0 !important;padding:0 !important;margin:0;box-shadow:none} +.phone-dial-call .pm-btn{font-size:1.25em;padding:14px 48px;min-width:220px} +.card-section .card-field.activity-room-title{display:flex;align-items:center;gap:10px} +.card-section .card-field.activity-room-title>.card-value{flex:0 1 auto} +.room-live-head,.room-live-head-main{padding:0;margin:0;background:transparent;border:none !important;box-shadow:none;border-radius:0} +.room-live-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px} +.room-live-head-main{flex:1 1 auto;min-width:0} +.room-live-head-main>.card-chips-row{margin:0} +.room-live-head-main>.phone-empty{margin:4px 0 0 0} +.room-refresh-btn{flex:0 0 auto} +.phone-dialer{display:flex;flex-direction:column;align-items:center;gap:12px;margin:10px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.phone-screen{order:1;width:100%;max-width:320px;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.phone-screen input{width:100%;text-align:center;font-size:22px;letter-spacing:2px;margin:0} +.phone-dialpad{order:2;display:grid;grid-template-columns:repeat(3,64px);gap:12px;justify-content:center;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.phone-dialpad .phone-key{width:64px;height:64px;min-height:0;border-radius:50%;border:1px solid #ffa500;background:transparent;color:#ffa500;font-size:22px;font-weight:700;padding:0;margin:0;display:flex;align-items:center;justify-content:center;cursor:pointer} +.phone-dialpad .phone-key:hover{background:#ffa500;color:#1a1a1a} +.phone-dialpad .phone-key-fn{font-size:18px;color:#ccc;border-color:#555} +.phone-dialer .phone-dial-add{order:3} +.phone-recipients{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;margin:8px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.phone-recipients>.card-label{flex-basis:100%;text-align:center} +.phone-row-delete{display:inline-flex;margin:0 0 0 auto;padding:0 !important;background:transparent !important;border:0 !important;box-shadow:none} +.phone-contact .phone-action-form+.phone-row-delete{margin-left:6px} +.phone-latest-row{flex-wrap:nowrap;min-width:0} +.phone-latest-row .user-link{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.phone-latest-row .date-link,.phone-latest-row .phone-action-form,.phone-latest-row .phone-row-delete{flex:0 0 auto} +.phone-history td.phone-row-actions{text-align:right;width:1%;white-space:nowrap} +.phone-pam-hint{color:#ffa500;font-weight:600;margin:6px 0} +.phone-refused{color:#ffa500;margin:8px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none;display:flex;flex-wrap:wrap;align-items:center;gap:6px} +.phone-dnd-form{display:inline-flex;margin:0;padding:0;background:transparent !important;border:0 !important;box-shadow:none} +.phone-recipient-list{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin:0;padding:10px 14px;border:1px solid #ffa500 !important;border-radius:10px;background:transparent !important;box-shadow:none} +.phone-recipient{display:inline-flex;align-items:center;gap:6px;border:none;padding:0;font-size:13px;background:transparent !important} +.phone-recipient form{display:inline-flex;margin:0;padding:0;background:transparent !important;border:0 !important} +.phone-recipient form .phone-recipient-remove{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;min-height:0;padding:0;margin:0;border:1px solid #ff8a8a;border-radius:50%;background:transparent;color:#ff8a8a;font-size:11px;line-height:1;cursor:pointer;box-shadow:none} +.phone-recipient form .phone-recipient-remove:hover{background:#ff8a8a;color:#1a1a1a} +.phone-dial-hint{text-align:center;margin:0 0 8px 0} +.phone-dial-call{display:flex;justify-content:center;margin:0;background:transparent !important;border:0 !important;padding:0 !important} +.peers-pause-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 12px 0;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none} +.peers-pause-row form{margin:0;padding:0;border:0;background:transparent;box-shadow:none} +.peers-pause-hint{margin:0;color:#ccc;font-size:.9em} +.tribe-main .tribe-content-list{padding:0;margin:0;background:transparent;border:0;box-shadow:none;gap:0;overflow:visible} +.tribe-main .jobs-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr))} +.tribe-main .feed-row{flex-wrap:wrap} +.tribe-main .feed-main{flex:1 1 180px;min-width:0} +.tribe-main .clearnet-switch{flex-wrap:wrap} +.tribe-main .forum-back-form{margin:0 0 10px 0} +.tribe-main .feed-container,.tribe-main .jobs-grid,.tribe-main .images-list,.tribe-main .audios-list,.tribe-main .videos-list,.tribe-main .documents-list,.tribe-main .bookmark-list,.tribe-main .inhabitants-list,.tribe-main .forum-thread-container{padding:0;margin:0;background:transparent;border:0;box-shadow:none;overflow:visible} +.tribe-main .feed-text{margin-right:0} +.tribe-main .forum-card{flex-wrap:wrap} +.tribe-main .forum-main-col{flex:1 1 220px;min-width:0} +.tribe-main .comment-body-row{flex-wrap:wrap} +.tribe-main .comment-text-col{flex:1 1 200px;min-width:0} +.tribe-main .tribe-grid{padding:0;margin:0;background:transparent;border:0;box-shadow:none;grid-template-columns:repeat(auto-fill,minmax(min(100%,260px),1fr))} +.tribe-main .audios-list{overflow-x:auto} +.tribe-main .torrent-table .user-link,.tribe-main .file-table .user-link{display:inline-block;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom} +.tribe-main .forum-footer,.tribe-main .forum-meta,.tribe-main .comment-header{flex-wrap:wrap} +.tribe-main .forum-footer .date-link,.tribe-main .comment-header .date-link{flex:0 0 auto;white-space:nowrap} +.tribe-main .forum-footer .user-link,.tribe-main .comment-header .user-link{flex:1 1 120px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.tribe-main .room-item .mailing-archive-head{flex-wrap:wrap} +.tribe-main .room-item .mailing-archive-meta{flex:1 1 200px} + +.agenda-card-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;padding:0;border:0;background:transparent;box-shadow:none} +.card-section>.card-footer.favorites-meta{border-top:0;padding-top:0;margin-top:6px} +.agenda-list.jobs-grid{margin-top:12px} +.oasis-pager{display:flex;justify-content:flex-end;align-items:center;flex-wrap:wrap;gap:8px;margin:24px 0 8px 0} +.oasis-pager-info{color:#9a8a2e;font-size:12px;margin-right:4px} +.oasis-pager-sizes{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-right:auto;padding:0} +.oasis-pager-btn{display:inline-block;padding:6px 14px;background:#1C1C1C;color:#E6C200;border:1px solid #333;border-radius:14px;font-size:13px;text-decoration:none;transition:border-color .15s ease,color .15s ease,background .15s ease} +.oasis-pager-btn:hover,.oasis-pager-btn.active{border-color:#FFD700;color:#FFD700;text-decoration:none} +.card-title-link{color:inherit;text-decoration:none} +.card-title-link:hover{text-decoration:underline} +.graphos-statsbar{margin:12px 0 14px}.graphos-canvas{position:relative}.graphos-canvas-svg{margin:0;padding:0;border:0 !important;background:transparent !important;box-shadow:none}.graphos-canvas .graphos-zoom-btn{position:absolute;top:8px;right:8px;margin:0;white-space:nowrap;z-index:1}.graphos-kpi .stats-kpi-label{text-transform:uppercase;white-space:nowrap}.graphos-kpi .stats-kpi-value{font-weight:bold} +.graphos-lightbox .graphos-canvas-zoom{width:96vw;height:92vh;overflow:auto;background:#111;border:1px solid #2a2a2a;border-radius:8px;box-sizing:border-box;padding:0;margin:0} +.graphos-lightbox .graphos-zoom-stage,.graphos-lightbox .graphos-zoom-svg{padding:0;margin:0;border:0;border-radius:0;background:none;box-shadow:none} +.graphos-lightbox .graphos-svg{width:100%;height:auto;max-height:calc(92vh - 4px);min-height:0;display:block} +.graphos-lightbox .lightbox-close{position:fixed;top:14px;right:22px;z-index:10002;font-size:28px;text-decoration:none} +.live-lightbox .lightbox-image-link{display:flex;align-items:center;justify-content:center;max-width:100%;max-height:100%;padding:0;margin:0;border:0;background:none} +.shop-buy-form,.market-item.actions,.market-bid-form,.market-buy-form{border:0;background:transparent;box-shadow:none;padding:0} +.project-progress-block .confirmations-progress{width:100%} +.tags-list .tag-table th:last-child,.tags-list .tag-table td:last-child{width:1%;white-space:nowrap;text-align:right} +.stats-block .tag-table,.stats-card .tag-table{width:100%;border-collapse:collapse;table-layout:fixed} +.stats-block .tag-table th:first-child,.stats-block .tag-table td:first-child,.stats-card .tag-table th:first-child,.stats-card .tag-table td:first-child{width:70%} +.stats-block .tag-table th:last-child,.stats-block .tag-table td:last-child,.stats-card .tag-table th:last-child,.stats-card .tag-table td:last-child{width:30%;text-align:right} +.video-grid{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin:18px 0 0;padding:0;border:0;background:transparent;box-shadow:none} +.video-tile{display:flex;flex-direction:column;gap:0;padding:0;margin:0;width:420px;max-width:100%;border:1px solid #333;border-radius:6px;overflow:hidden;background:#000} +.video-grid-solo .video-tile{width:800px} +.video-feed{display:block;width:100%;aspect-ratio:4/3;object-fit:cover;background:#000} +.video-tile .user-link,.video-tile-name{display:block;margin:0;padding:4px 8px;font-size:12px;text-align:left;background:#1a1a1a} +.video-tile-me .video-feed{transform:scaleX(-1)} +.room-peer-cam{flex:0 0 auto;color:#ffa300;font-size:11px;font-weight:700;border:1px solid #ffa300;border-radius:4px;padding:1px 6px} +.tribe-action-btn.room-cam-btn-on{background:#ffa500 !important;color:#000 !important;border-color:#ffa500 !important} +.room-stream-chip{color:#ffa300;font-size:11px;font-weight:700;border:1px solid #ffa300;border-radius:4px;padding:1px 6px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.room-public-warning{margin:8px 0 0;font-size:12px;color:#ffa300} +.video-tile-film{color:#ffa300} +.room-stream-form{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;position:relative} +.room-live-controls>.room-stream-form{flex:0 1 auto;max-width:100%} +.room-stream-input{position:absolute;left:0;top:0;width:1px;height:1px;opacity:0;pointer-events:none;clip-path:inset(50%)} +.room-stream-pick{cursor:pointer} +.room-stream-input:invalid ~ .room-stream-go,.room-stream-input:valid ~ .room-stream-pick{display:none} +.room-stream-go{order:2} +.room-stream-form .room-stream-input:valid{position:static !important;width:auto !important;height:auto !important;max-width:160px !important;opacity:1 !important;pointer-events:auto !important;clip-path:none !important;order:1;margin:0 !important;padding:1px 6px !important;border:1px solid #ffa300 !important;border-radius:4px !important;background:transparent !important;color:#ffa300 !important;font-size:11px !important;font-weight:700;line-height:1.6;cursor:pointer} +.room-stream-input::file-selector-button{display:none} +.room-stream-input::-webkit-file-upload-button{display:none} +.stream-picker-list{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-direction:column;gap:6px} +.stream-picker-item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;padding:6px 10px;border:1px solid rgba(128,128,128,.4);border-radius:6px} +.stream-picker-link{display:flex;align-items:center;gap:8px;min-width:0;text-decoration:none} +.stream-picker-link .btn-icon{display:flex;flex:0 0 auto;width:18px;height:18px} +.stream-picker-link .btn-icon svg{display:block;width:100%;height:100%} +.stream-picker-name{overflow-wrap:anywhere} +.stream-picker-size{flex:0 0 auto;font-size:12px;opacity:.7;white-space:nowrap} +.stream-picker-item .phone-action-form{flex:0 0 auto;margin:0} +.video-grid-one{flex-direction:column;align-items:center} +.video-feed-grid{width:100%;max-width:100%;aspect-ratio:auto;border:1px solid #333;border-radius:6px} +.video-grid-solo .video-feed-grid{max-width:800px;margin:0 auto} +.video-grid-names{display:flex;flex-wrap:wrap;gap:6px;padding:0;margin:0;border:0;background:transparent;box-shadow:none} +.video-grid-names .user-link,.video-grid-names .video-tile-name{padding:4px 8px;font-size:12px;background:#1a1a1a;border-radius:4px} +.file-table{width:100%;border-collapse:collapse;margin-top:12px} +.file-table th,.file-table td{padding:8px 12px;text-align:left;vertical-align:middle} +.file-table th{background:#1a1a1a;color:#FFA500;font-weight:bold} +.file-table th:nth-child(1),.file-table td:nth-child(1),.file-table td:nth-child(3),.file-table td:nth-child(4),.file-table td:nth-child(5),.file-table td:nth-child(6){white-space:nowrap;width:1%} +.file-table td:nth-child(2){width:auto;overflow-wrap:anywhere} +.file-table td .filter-btn{margin:0} +.file-table th.torrent-spread-cell,.file-table td.torrent-spread-cell{text-align:center} +.forum-inline-reply{padding:0;margin:6px 0 0;border:0;background:transparent;box-shadow:none} +.forum-inline-reply-toggle{cursor:pointer;display:inline-block;padding:3px 10px;border:1px solid #ffa500;border-radius:10px;background:#1a1a1a;color:#ffa500;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;list-style:none;width:max-content} +.forum-inline-reply-toggle::-webkit-details-marker{display:none} +.forum-inline-reply[open] .forum-inline-reply-toggle{margin-bottom:6px} +.video-tile .zoom-link,.video-grid-one .zoom-link{display:block;width:100%;border:0 !important;background:transparent !important;padding:0 !important} +.video-tile-me .lightbox-image{transform:scaleX(-1)} +.phone-call-panel .live-lightbox{background:rgba(0,0,0,.85)} +.live-lightbox .lightbox-image{width:min(80vw,880px);height:auto;max-width:none;max-height:80vh;object-fit:contain;cursor:zoom-out} +.live-fullscreen-btn{position:absolute;top:15px;right:112px;width:42px;height:42px;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background:rgba(0,0,0,.6);border-radius:50%;color:#fff;text-decoration:none} +.live-fullscreen-btn .btn-icon{display:flex;width:20px;height:20px} +.live-fullscreen-btn .btn-icon svg{display:block;width:100%;height:100%} +.live-lightbox.live-fullscreen{background:#000;z-index:10050} +.live-fullscreen .lightbox-image-link{display:flex;align-items:center;justify-content:center;width:100vw;height:100vh} +.live-fullscreen .lightbox-image{width:100vw;height:100vh;max-width:100vw;max-height:100vh;object-fit:contain;border-radius:0} diff --git a/src/client/assets/themes/Acampada.css b/src/client/assets/themes/Acampada.css new file mode 100644 index 0000000..8c3211f --- /dev/null +++ b/src/client/assets/themes/Acampada.css @@ -0,0 +1,195 @@ +/* Tema Acampada (rama movil): blanco, negro y cian neon, titulos en Anton. + * + * Los colores y las formas salen de acampada26s.net: bordes negros gruesos, + * botones con sombra solida que se hunden al pulsar y titulos en mayusculas. + * + * La maqueta de movil es styles/mobile_base.css, que main_views.js carga justo antes + * con su propio versionado (no con @import: se quedaba en la cache del + * WebView); aqui solo se cambian colores, letra y formas encima. Por eso casi todo lleva + * !important: la maqueta tambien lo usa. Las piezas propias de la rama + * (panal, barras) se ajustan en styles/hive_acampada.css. */ +@font-face { + font-family: "Anton"; + src: url("../fonts/Anton-Regular.ttf") format("truetype"); + font-display: swap; +} + +:root { + --a26-bg: #fff; + --a26-fg: #000; + --a26-neon: #00e5ff; + --a26-soft: #f2f2f2; + --a26-muted: #555; + --a26-bd: 3px solid var(--a26-fg); + --a26-bd-fino: 2px solid var(--a26-fg); + --a26-radius: 14px; + --a26-tit: "Anton", Impact, "Arial Narrow", sans-serif; + --a26-txt: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + color-scheme: light; +} + +/* --- fondo y texto --- */ +html, body { + background: var(--a26-bg) !important; + color: var(--a26-fg) !important; + font-family: var(--a26-txt) !important; +} +body { font-size: 17px; line-height: 1.5; } + +header, footer, .header, .main-content, .main-column, .sidebar-left, .sidebar-right, +section, article, div, table, .profile { + background-color: transparent !important; + color: var(--a26-fg) !important; + border-color: var(--a26-fg) !important; +} +div { border: 0 !important; box-shadow: none !important; } + +span, p, li, td, th, label, small, strong, em, h4, h5, h6 { color: inherit !important; } + +a { color: var(--a26-fg) !important; } +a:hover, a:focus-visible { color: var(--a26-fg) !important; background-color: var(--a26-neon); } + +/* --- titulos: Anton en mayusculas --- */ +h1, h2, h3, .card-label, .tags-header h2, .oasis-nav-header { + font-family: var(--a26-tit) !important; + font-weight: 400 !important; + text-transform: uppercase; + letter-spacing: .01em; + line-height: 1.05 !important; + color: var(--a26-fg) !important; +} +h1 { font-size: 34px !important; } +h2 { font-size: 28px !important; } +h3 { font-size: 21px !important; } +.card-label { font-size: 15px !important; } + +/* cabecera de cada modulo: titulo con raya gorda debajo, como los h2 de la web */ +.tags-header { + background: transparent !important; + border: 0 !important; + border-bottom: var(--a26-bd) !important; + border-radius: 0 !important; + padding: 0 0 8px !important; + margin: 0 0 16px !important; + text-align: left !important; +} +.tags-header p { color: var(--a26-muted) !important; font-size: 15px !important; margin: 4px 0 0 !important; } + +/* --- tarjetas: recuadro negro, sin sombras difusas --- */ +.main-column article, .main-column [class$="-card"], .main-column [class*="-card "], +.main-column .card, .forum-comment, .feed-row { + background: var(--a26-bg) !important; + border: var(--a26-bd-fino) !important; + border-radius: var(--a26-radius) !important; + box-shadow: none !important; +} +.main-column > section { border: 0 !important; padding: 0 !important; } + +/* --- botones: los .bt de acampada --- */ +button, input[type="submit"], input[type="button"], +.filter-btn, .create-button, .buy-btn, .delete-btn, +a.user-link, button.user-link, .date-link, .welcome-banner-start, .update-download-link { + font-family: var(--a26-tit) !important; + font-weight: 400 !important; + text-transform: uppercase; + letter-spacing: .02em; + background: var(--a26-bg) !important; + color: var(--a26-fg) !important; + border: var(--a26-bd) !important; + border-radius: var(--a26-radius) !important; + box-shadow: 0 5px 0 var(--a26-fg) !important; + text-decoration: none !important; + transition: transform .12s ease-out, box-shadow .12s ease-out; +} +button, input[type="submit"], input[type="button"], .filter-btn, .create-button { + min-height: 48px; + padding: 8px 16px !important; + font-size: 18px !important; + margin-bottom: 6px; +} +button:active, input[type="submit"]:active, input[type="button"]:active, +.filter-btn:active, .create-button:active, a.user-link:active { + transform: translateY(4px); + box-shadow: 0 1px 0 var(--a26-fg) !important; + background: var(--a26-neon) !important; +} +/* el boton elegido (filtros, pestañas): relleno cian */ +.filter-btn.active, .mode-buttons .filter-btn.active, .mode-buttons-row .filter-btn.active, +.tribe-mode-buttons .filter-btn.active, .filters .filter-btn.active, +.activity-filter-grid .filter-btn.active, .activity-sub-filter .filter-btn.active, +.bank-range-tabs .filter-btn.active { + background: var(--a26-neon) !important; + border-color: var(--a26-fg) !important; + color: var(--a26-fg) !important; +} +/* el boton de crear: negro, para que se vea primero */ +.create-button, .mode-buttons .create-button, .mode-buttons-row .create-button, +.tribe-mode-buttons .create-button, .filters .create-button { + background: var(--a26-fg) !important; + color: var(--a26-bg) !important; + box-shadow: 0 5px 0 var(--a26-neon) !important; + font-size: 18px !important; + padding: 8px 16px !important; + min-height: 48px !important; +} +.delete-btn { box-shadow: 0 5px 0 #e0004d !important; } + +/* --- formularios --- */ +input, textarea, select { + background: var(--a26-bg) !important; + color: var(--a26-fg) !important; + border: var(--a26-bd-fino) !important; + border-radius: 10px !important; + font-family: var(--a26-txt) !important; + font-size: 16px !important; + min-height: 44px; +} +input:focus, textarea:focus, select:focus { + outline: 3px solid var(--a26-neon) !important; + outline-offset: 1px; +} +input[type="checkbox"], input[type="radio"] { min-height: 0; accent-color: var(--a26-fg); } + +/* --- tablas --- */ +table { border: var(--a26-bd-fino) !important; border-radius: 10px; overflow: hidden; } +table th { background: var(--a26-fg) !important; color: var(--a26-bg) !important; } +table td { border-color: var(--a26-fg) !important; } + +/* --- avisos de upstream (bienvenida, actualizaciones) --- */ +.update-banner, .welcome-banner { + background: var(--a26-neon) !important; + color: var(--a26-fg) !important; + border: var(--a26-bd) !important; + border-radius: var(--a26-radius) !important; + margin: 8px 10px !important; +} +.update-banner-link, .update-banner-text { color: var(--a26-fg) !important; } +.welcome-banner-close-btn { + background: var(--a26-bg) !important; + box-shadow: none !important; + min-height: 40px; width: 40px; padding: 0 !important; +} + +/* --- colores sueltos de OasisMobile que se quedaban en amarillo o naranja --- */ +.avatar, .article img, .post-preview img { border-color: var(--a26-fg) !important; } +.profile .name, .snh-invite-code, .stats-kpi-label, .stats-kpi-value, .carbon-bar-formula, +a.hashtag, .chat-topic-title, .chat-topic-active .chat-topic-title, +.chat-topic-meta, .chat-topic-last-author, .chat-workspace-details > .tribe-side .tribe-card-members { + color: var(--a26-fg) !important; +} +.chat-topic-active { background: var(--a26-neon) !important; } +a.hashtag { text-decoration: underline !important; } +.pdf-maximized { background: var(--a26-bg) !important; } + +/* --- pie --- */ +.oasis-footer, .oasis-footer * { color: var(--a26-muted) !important; background: transparent !important; } +.oasis-footer { border-top: var(--a26-bd) !important; margin-top: 32px; } +.oasis-footer a { text-decoration: underline !important; } + +::-webkit-scrollbar-thumb { background-color: var(--a26-fg) !important; } +::-webkit-scrollbar-track { background-color: var(--a26-soft) !important; } + +@media (prefers-reduced-motion: reduce) { + button, .filter-btn, .create-button { transition: none; } + button:active, .filter-btn:active, .create-button:active { transform: none; } +} diff --git a/src/client/assets/themes/Clear-SNH.css b/src/client/assets/themes/Clear-SNH.css new file mode 100644 index 0000000..c64f10b --- /dev/null +++ b/src/client/assets/themes/Clear-SNH.css @@ -0,0 +1,411 @@ +body { + background-color: #F9F9F9 !important; + color: #2C2C2C !important; + font-family: 'Roboto', sans-serif !important; +} + +.main-column { + background-color: #FFFFFF !important; + border: 1px solid #E0E0E0 !important; +} + +button, input[type="submit"], input[type="button"] { + background-color: #FF6F00 !important; + color: #FFFFFF !important; + border: none !important; + border-radius: 6px !important; + padding: 10px 20px !important; + cursor: pointer !important; + font-weight: 600 !important; +} + +button:hover, input[type="submit"]:hover, input[type="button"]:hover { + background-color: #FF8F00 !important; +} + +input, textarea, select { + background-color: #FFFFFF !important; + color: #2C2C2C !important; + border: 1px solid #E0E0E0 !important; + border-radius: 4px !important; + padding: 8px !important; + font-size: 16px !important; +} + +a { + color: #007BFF !important; + text-decoration: none !important; +} + +a:hover { + text-decoration: underline !important; +} + +p { + color: black !important; + text-decoration: none !important; +} + +.created-at, .about-time, .time { + font-size: 0.9rem; + color: black; +} + +table { + background-color: #FFFFFF !important; + color: #2C2C2C !important; + width: 100% !important; + border-collapse: collapse !important; +} + +table th { + background-color: #F8F8F8 !important; + padding: 12px 15px !important; + text-align: left !important; + font-weight: 600 !important; +} + +table tr:nth-child(even) { + background-color: #FAFAFA !important; +} + +table td { + padding: 12px 15px !important; +} + +.profile { + background-color: #FFFFFF !important; + padding: 20px !important; + border-radius: 8px !important; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important; +} + +.profile .name { + color: #FF6F00 !important; + font-size: 20px !important; + font-weight: 700 !important; +} + +.avatar { + border: 3px solid #FF6F00 !important; + border-radius: 50% !important; + width: 60px !important; + height: 60px !important; +} + +article, section { + background-color: #FFFFFF !important; + color: #2C2C2C !important; + padding: 20px !important; + border-radius: 8px !important; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important; +} + +.post-preview img { + border-radius: 8px !important; + max-width: 100% !important; + height: auto !important; +} + +div { + background-color: #FFFFFF !important; + border: 1px solid #E0E0E0 !important; +} + +::-webkit-scrollbar-thumb { + background-color: #B0B0B0 !important; +} + +::-webkit-scrollbar-track { + background-color: #F9F9F9 !important; +} + +.action-container { + background-color: #FFFFFF !important; + border-color: #E0E0E0 !important; + color: #2C2C2C !important; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important; +} + +footer { + background-color: #FFFFFF !important; + border-top: 1px solid #E0E0E0 !important; + padding: 15px 0 !important; +} + +footer a { + background-color: #007BFF !important; + color: #FFFFFF !important; + padding: 10px 20px !important; + border-radius: 6px !important; + text-decoration: none !important; + font-weight: 600 !important; +} + +footer a:hover { + background-color: #0056b3 !important; +} + +.sidebar-left nav ul, +.sidebar-right nav ul { + display: flex !important; + flex-direction: column !important; +} + +.sidebar-left nav ul, +.sidebar-right nav ul { + display: flex; + flex-direction: column; + margin: 0; +} + +.sidebar-left nav ul li, +.sidebar-right nav ul li { + width: 100%; + display: block; +} + +.sidebar-left nav ul li a, +.sidebar-right nav ul li a, +.header nav ul li a { + display: block; + width: 100%; + padding: 12px 16px; + font-size: 15px; + font-weight: 500; + border-radius: 6px; + background-color: #ffffff !important; + color: #2C2C2C !important; + border: 1px solid #D0D0D0 !important; + text-align: left; + box-sizing: border-box; +} + +.sidebar-left nav ul li a:hover, +.sidebar-right nav ul li a:hover, +.header nav ul li a:hover { + background-color: #f0f0f0 !important; +} + +.filter-btn, +.create-button, +.edit-btn, +.delete-btn, +.join-btn, +.leave-btn, +.buy-btn { + background-color: #FF6F00 !important; + color: #FFFFFF !important; + border: none !important; +} + +.filter-btn:hover, +.create-button:hover { + background-color: #FF8F00 !important; + color: #FFFFFF !important; +} + +.card { + color: #4A4A4A; + box-shadow: 0 4px 30px 0 rgba(0, 0, 0, 0.1); + background-color: #F4F4F4; +} + +.card-label { + color: #2D2D2D; +} + +.card-footer { + color: #6C6C6C; + background: none; +} + +.card-field { + background: none; +} + + + +a.user-link, button.user-link { + background-color: #FFD600; + color: #FFFFFF; + border-color: #FFD600; + transition: background-color 0.3s, color 0.3s, border-color 0.3s; +} + +a.user-link:hover, button.user-link:hover { + background-color: #FFD600; + border-color: #FFD600; + color: #FFFFFF; + cursor: pointer; +} + +a.user-link:focus, button.user-link:focus { + background-color: #9A2F2F; + border-color: #9A2F2F; + color: #FFFFFF; +} + +.date-link { + background-color: #2F3C32; + color: #fff; +} + +.date-link:hover { + background-color: #3E4A3D; + color: #fff; +} + +.activitySpreadInhabitant2 { + background-color: #3E4A3D; + color: #fff; +} + +.activityVotePost { + background-color: #3B5C42; + color: #fff; +} + + +.update-banner { + background-color: #FFF3E0 !important; + border-bottom-color: #FFE0B2 !important; + color: #E65100 !important; +} + +.update-banner-link { + color: #FF6F00 !important; +} + +.snh-invite-code { + color: #007BFF !important; +} + +.carbon-bar-track { + background: #e0e0e0 !important; +} + +.carbon-bar-max { + background: #cc7700 !important; +} + +.stats-kpi-label { color: #2D2D2D !important; } +.stats-kpi-value { color: #007BFF !important; } +.carbon-bar-note, .carbon-bar-formula { color: #007BFF !important; } +.graphos-node-label { fill: #2C2C2C !important; } +.graphos-node-label-me { fill: #1f6391 !important; } +.data-best-label { color: #cc7700 !important; } +.data-best-card { background: #FFF3E0 !important; border: 1px solid rgba(204, 119, 0, .4) !important; } +.graphos-legend { color: #2C2C2C !important; } +.graphos-edge { stroke: #BBB !important; } +.graphos-edge-discovered { stroke: #b8860b !important; } +.graphos-edge-unknown { stroke: #999 !important; } +.graphos-node-circle-discovered { fill: #ffd700 !important; stroke: #b8860b !important; } +.graphos-legend-dot.graphos-node-circle-discovered { background: #ffd700 !important; border-color: #b8860b !important; } +/* Blockexplorer */ +.blockchain-view { background-color: #F4F4F4 !important; color: #2C2C2C !important; } +.block { background: #FFFFFF !important; box-shadow: 0 2px 12px rgba(0,0,0,0.06) !important; } +.block:hover { box-shadow: 0 8px 32px rgba(0,0,0,0.10) !important; } +.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #2D2D2D !important; } +.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #007BFF !important; } +.block-content-preview, .block-content { background: #F8F8F8 !important; color: #2C2C2C !important; } +.block-author { color: #FF6F00 !important; background: rgba(255,111,0,0.08) !important; } +.block-author:hover { color: #FF8F00 !important; } +.block-url { color: #007BFF !important; } +.block-row--details .block-url { background: #F0F0F0 !important; } +.block-row--details .block-url:hover { background: #E0E0E0 !important; color: #FF6F00 !important; } +.btn-singleview { background: #F0F0F0 !important; color: #FF6F00 !important; } +.btn-singleview:hover { background: #E0E0E0 !important; color: #FF8F00 !important; } +.btn-spread-on, .content-actions .btn-pin-on { background: #FF6F00 !important; color: #FFFFFF !important; } +.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #FF8F00 !important; color: #FFFFFF !important; } +.btn-singleview.btn-content { background: #FF6F00 !important; color: #F0F0F0 !important; border: 1px solid #FF6F00 !important; } +.btn-singleview.btn-content:hover { background: #FF8F00 !important; color: #FFFFFF !important; border-color: #FF8F00 !important; } +.btn-back { background: #FF6F00 !important; color: #FFFFFF !important; } +.btn-back:hover { background: #FF8F00 !important; color: #FFFFFF !important; } +.block-info-table td, .pm-info-table td { border-color: #E0E0E0 !important; } +.block-diagram { border-color: #E0E0E0 !important; background: #FFFFFF !important; } +.block-diagram-ruler { color: #2D2D2D !important; background: #F8F8F8 !important; border-bottom-color: #E0E0E0 !important; } +.block-diagram-ruler span { color: #2D2D2D !important; } +.block-diagram-cell { border-color: #E0E0E0 !important; background: #FFFFFF !important; } +.bd-label { color: #2D2D2D !important; } +.bd-value { color: #007BFF !important; } +.deleted-label { color: #D32F2F !important; } + +/* Tribes */ +.tribe-card { background: #FFFFFF !important; border-color: #E0E0E0 !important; } +.tribe-card:hover { border-color: #007BFF !important; } +.tribe-card-title { color: #2D2D2D !important; } +.tribe-card-description { color: #555 !important; } +.tribe-info-table td { border-color: #E0E0E0 !important; } +.tribe-info-label { color: #2D2D2D !important; background: #FFFFFF !important; } +.tribe-info-value { color: #007BFF !important; background: #FFFFFF !important; } +.tribe-info-empty { color: #999 !important; } +.tribe-card-members { border-color: #E0E0E0 !important; background: #F8F8F8 !important; } +.tribe-members-count { color: #FF6F00 !important; } +.tribe-card-actions { border-color: #E0E0E0 !important; background: #F8F8F8 !important; } +.tribe-action-btn { border-color: #FF6F00 !important; color: #FF6F00 !important; } +button.tribe-action-btn { background: transparent !important; border: 1px solid #FF6F00 !important; } +.tribe-action-btn:hover { background: #FF6F00 !important; color: #fff !important; } +.tribe-subtribe-link { background: #F0F0F0 !important; border-color: #E0E0E0 !important; color: #FF6F00 !important; } +.tribe-thumb-link { border-color: #E0E0E0 !important; } +.tribe-thumb-link:hover { border-color: #FF6F00 !important; } +.tribe-subtribe-link:hover { background: #E0E0E0 !important; } +.tribe-parent-image { border-color: #E0E0E0 !important; } +.tribe-parent-box { background: #FFFFFF !important; } +.chat-message { background: #FFFFFF !important; border: 1px solid #E0E0E0 !important; } +.chat-bubble-row-self .chat-message { background: #FFF3E0 !important; border-color: rgba(204, 119, 0, .35) !important; } +.chat-message-author { background: #FDF6EC !important; } +.chat-bubble-sender { color: #1565C0 !important; } +.chat-bubble-sender-owner { color: #cc7700 !important; } +.chat-bubble-time { color: #999 !important; } +.chat-day-chip { background: #F0F0F0 !important; color: #666 !important; border-color: #DDD !important; } +.chat-topics { background: #FAFAFA !important; border-color: #E0E0E0 !important; } +.chat-topic:hover { background: #F0F0F0 !important; } +.chat-topic-active { background: #FFF3E0 !important; } +.chat-topic-title { color: #2D2D2D !important; } +.chat-workspace-empty { border-color: #DDD !important; } +.map-viewer, .map-stage div, .map-hero, .map-toolbar, .map-toolbar div, .map-place-results, .map-markers-list, .map-markers-list div, .map-detail-grid, .map-card-thumb .map-viewer { background: transparent !important; border: 0 !important; } +.map-pick-cell, .map-pick-cell:hover { border: 0 !important; padding: 0 !important; border-radius: 0 !important; box-shadow: none !important; } +.map-pick-cell { background: transparent !important; } +.map-pick-cell:hover { background: rgba(255, 111, 0, 0.18) !important; } +.map-ctrl, .map-place-result { padding: 0 !important; border-radius: 6px !important; box-shadow: none !important; } +.map-place-result { padding: 4px 10px !important; border-radius: 14px !important; } +.map-stage { background: #cfe0ec !important; border: 1px solid #E0E0E0 !important; } +.map-marker-row { border-bottom: 1px solid #E0E0E0 !important; } +.map-label-country { fill: #4a4a4a; stroke: #ffffff; } +.map-place-dot { fill: #333333; stroke: #ffffff; } +.map-place-capital { fill: #E65100; } +.map-place-label { fill: #222222; stroke: #ffffff; } +.map-pin-label-bg { fill: rgba(255, 255, 255, 0.92); stroke: rgba(0, 0, 0, 0.3); } +.map-pin-label { fill: #222222; } +.map-pin-focus .map-pin-body, .map-cluster-dot { fill: #FF6F00; } +.map-pin-focus .map-pin-label-bg { stroke: #FF6F00; } +.map-cluster-ring { fill: rgba(255, 111, 0, 0.25); } +.map-pick-ring, .map-pick-cross { stroke: #FF6F00; } +.map-pins-layer a:hover .map-pin-body, .map-pins a:hover .map-pin-body { fill: #FF6F00; } +.map-scale-line { stroke: #333333; } +.map-scale-text { fill: #333333; stroke: #ffffff; } +.map-attrib { fill: #777777; } +.map-ctrl { background: rgba(255, 255, 255, 0.92) !important; color: #FF6F00 !important; border-color: #cccccc !important; } +.map-ctrl:hover { background: #FF6F00 !important; color: #ffffff !important; border-color: #FF6F00 !important; } +.map-ctrl-off:hover { background: rgba(255, 255, 255, 0.92) !important; color: #FF6F00 !important; border-color: #cccccc !important; } +.map-pick-cell:hover { background: rgba(255, 111, 0, 0.18); outline-color: rgba(255, 111, 0, 0.7); } +.map-place-result, .map-center-link { color: #FF6F00 !important; border-color: #cccccc !important; } +.map-place-result:hover, .map-center-link:hover { background: #FF6F00 !important; color: #ffffff !important; border-color: #FF6F00 !important; } +.map-picked { background: #F0F0F0 !important; color: #FF6F00 !important; } +.map-picked-near { color: #555555 !important; } +.map-markers-list .map-marker-row--focus { background: rgba(255, 111, 0, 0.08) !important; box-shadow: inset 3px 0 0 #FF6F00 !important; } +.map-marker-row--focus .map-marker-idx { background: #FF6F00 !important; color: #ffffff !important; } +.map-marker-label { color: #E65100 !important; } +.map-marker-date { color: #777777 !important; } +.map-marker-coords { color: #222222 !important; } +.map-popup-pointer { fill: #ffffff; stroke: #cccccc; } +.map-stage .map-popup-card { background: #ffffff !important; border: 1px solid #cccccc !important; color: #222222; } +.map-stage .map-popup-text { color: #333333; } +.map-popup-coords { color: #222222; } +.map-stage .map-popup-meta { color: #777777; } +.map-popup-close { color: #777777; } +.map-popup-close:hover { color: #FF6F00; } +.oasis-pager-info { color: #888888; } +.oasis-pager-btn { background: #FFFFFF; color: #555555 !important; border: 1px solid #E0E0E0; text-decoration: none !important; } +.oasis-pager-btn:hover, .oasis-pager-btn.active { border-color: #2C2C2C; color: #2C2C2C !important; text-decoration: none !important; } diff --git a/src/client/assets/themes/Dark-SNH.css b/src/client/assets/themes/Dark-SNH.css new file mode 100644 index 0000000..6fe0855 --- /dev/null +++ b/src/client/assets/themes/Dark-SNH.css @@ -0,0 +1,351 @@ +body { + background-color: #121212; + color: #FFD700; +} + +header, footer { + background-color: #1F1F1F; +} + +.sidebar-left, .sidebar-right { + background-color: #1A1A1A; + border: 1px solid #333; +} + +.main-column { + background-color: #1C1C1C; +} + +button, input[type="submit"], input[type="button"] { + background-color: #444; + color: #FFD700; + border: 1px solid #444; +} + +button:hover, input[type="submit"]:hover, input[type="button"]:hover { + background-color: #333; + border-color: #666; +} + +input, textarea, select { + background-color: #333; + color: #FFD700; + border: 1px solid #555; +} + +a { + color: #FFD700; +} + +a:hover { + color: #FFDD44; +} + +table { + background-color: #222; + color: #FFD700; +} + +table th { + background-color: #333; +} + +table tr:nth-child(even) { + background-color: #2A2A2A; +} + +nav ul li a:hover { + color: #FFDD44; + text-decoration: underline; +} + +.profile { + background-color: #222; + padding: 15px; + border-radius: 8px; +} + +.profile .name { + color: #FFD700; +} + +.avatar { + border: 3px solid #FFD700; +} + +article, section { + background-color: #1C1C1C; + color: #FFD700; +} + +.article img { + border: 3px solid #FFD700; +} + +.post-preview img { + border: 3px solid #FFD700; +} + +.post-preview .image-container { + max-width: 100%; + overflow: hidden; + display: block; + margin: 0 auto; +} + +div { + background-color: #1A1A1A; + border: 1px solid #333; +} + +div .header-content { + width: 100%; +} + +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-thumb { + background-color: #444; + border-radius: 10px; +} + +::-webkit-scrollbar-track { + background-color: #222; +} + +.action-container { + background-color: #1A1A1A; + border: 1px solid #333; + padding: 10px; + border-radius: 8px; + color: #FFD700; +} + +footer { + background-color: #1F1F1F; + padding: 10px 0; +} + +footer a { + background-color: #444; + color: #FFD700; + text-align: center; + padding: 8px 16px; + border-radius: 5px; + text-decoration: none; +} + +footer a:hover { + background-color: #333; + color: #FFDD44; +} + +.card { + border-radius: 16px; + padding: 0px 24px 10px 24px; + margin-bottom: 16px; + color: #FFD600; + font-family: inherit; + box-shadow: 0 2px 20px 0 #FFD60024; +} + +.card-section { + border:none; + padding: 10px 0 0 16px; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0px; + margin-top: 8px; + padding-top: 0px; + border: none; +} + +.card-label { + color: #ffa300; + font-weight: bold; + letter-spacing: 1.5px; + line-height: 1.2; + margin-bottom: 0; +} + +.card-footer { + margin-top: 6px; + font-weight: 500; + color: #ff9900; + font-size: 1.07em; + display: flex; + align-items: center; + gap: 8px; + background: none; + border: none; + padding-top: 0; + margin-bottom: 6; +} + +.card-body { + margin-top: 0; + margin-bottom: 4; + padding: 0; +} + +.card-field { + display: flex; + align-items: baseline; + padding: 0; + margin-bottom: 0; + border: none; + background: none; +} + +.card-tags { + margin: 5px 0 3px 0; + display: flex; + flex-wrap: wrap; + gap: 9px; +} + + + +a.user-link, button.user-link { + background-color: #FFA500; + color: #000; + padding: 8px 16px; + border-radius: 5px; + text-align: center; + font-weight: bold; + text-decoration: none; + display: inline-block; + border: 2px solid transparent; + transition: background-color 0.3s, color 0.3s, border-color 0.3s; + font-size: 0.8em; +} + +a.user-link:hover, button.user-link:hover { + background-color: #FFD700; + border-color: #FFD700; + color: #000; + cursor: pointer; +} + +a.user-link:focus, button.user-link:focus { + background-color: #007B9F; + border-color: #007B9F; + color: #fff; +} + +.date-link { + background-color: #444; + color: #FFD600; + padding: 8px 16px; + border-radius: 5px; + margin-left: 8px; +} + +.date-link:hover { + background-color: #555; + color: #FFD700; +} + +.activitySpreadInhabitant2 { + background-color: #007B9F; + color: #fff; + padding: 8px 16px; + border-radius: 5px; + font-weight: bold; + text-decoration: none; + display: inline-block; + border: 2px solid transparent; +} + +.activityVotePost { + background-color: #557d3b; + color: #fff; + padding: 8px 16px; + border-radius: 5px; + font-weight: bold; + text-decoration: none; + display: inline-block; + border: 2px solid transparent; +} + +.update-banner { + background-color: #1a1400; + border-bottom-color: #3a2e00; + color: #FFD700; +} + +.update-banner-link { + color: #FFD700; +} + +.oasis-footer-center a { + color: #FFA500; +} + +.oasis-footer-center a:hover { + color: #FFD700; +} + +.snh-invite-code { + color: #FFA500 !important; +} + +/* Blockexplorer */ +.stats-kpi-label { color: #ffa300 !important; } +.stats-kpi-value { color: #FFD700 !important; } +.carbon-bar-note, .carbon-bar-formula { color: #FFD700 !important; } +.graphos-node-label { fill: #ddd !important; } +.graphos-node-label-me { fill: #5dade2 !important; } +.graphos-legend { color: #ddd !important; } +.blockchain-view { background-color: #191b20 !important; color: #FFD700 !important; } +.block { background: #23242a !important; } +.block:hover { box-shadow: 0 8px 32px rgba(35,40,50,0.18) !important; } +.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #ffa300 !important; } +.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #FFD700 !important; } +.block-content-preview, .block-content { background: #222326 !important; color: #FFD700 !important; } +.block-author { color: #FFD700 !important; background: rgba(255,163,0,0.08) !important; } +.block-author:hover { color: #FFDD44 !important; } +.block-url { color: #FFD700 !important; } +.block-row--details .block-url { background: #1f2023 !important; } +.block-row--details .block-url:hover { background: #292b36 !important; color: #ffa300 !important; } +.btn-singleview { background: #1e1f23 !important; color: #ffa300 !important; } +.btn-singleview:hover { background: #2d2e34 !important; color: #FFD700 !important; } +.btn-spread-on, .content-actions .btn-pin-on { background: #ffa300 !important; color: #000000 !important; } +.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #ffb733 !important; color: #000000 !important; } +.btn-singleview.btn-content { background: #ffa300 !important; color: #1e1f23 !important; border: 1px solid #ffa300 !important; } +.btn-singleview.btn-content:hover { background: #FFD700 !important; color: #1e1f23 !important; border-color: #FFD700 !important; } +.btn-back { background: #21232b !important; color: #ffa300 !important; } +.btn-back:hover { background: #292b36 !important; color: #FFD700 !important; } +.block-info-table td, .pm-info-table td { border-color: #444 !important; } +.block-diagram { border-color: #555 !important; background: #191b20 !important; } +.block-diagram-ruler { color: #ffa300 !important; background: #111 !important; border-bottom-color: #555 !important; } +.block-diagram-ruler span { color: #ffa300 !important; } +.block-diagram-cell { border-color: #555 !important; background: #1e1f23 !important; } +.bd-label { color: #ffa300 !important; } +.bd-value { color: #FFD700 !important; } + +/* Tribes */ +.tribe-card { background: #23242a !important; border-color: #444 !important; } +.tribe-card:hover { border-color: #ffa300 !important; } +.tribe-card-description { color: #cfd3e1 !important; } +.tribe-info-table td { border-color: #444 !important; } +.tribe-info-label { color: #ffa300 !important; background: #1e1f23 !important; } +.tribe-info-value { color: #FFD700 !important; background: #1e1f23 !important; } +.tribe-info-empty { color: #9aa3b2 !important; } +.tribe-card-members { border-color: #444 !important; background: #1e1f23 !important; } +.tribe-members-count { color: #ffa300 !important; } +.tribe-card-actions { border-color: #444 !important; background: #1e1f23 !important; } +.tribe-action-btn { border-color: #ffa300 !important; color: #ffa300 !important; } +.tribe-action-btn:hover { background: #ffa300 !important; color: #000 !important; } +.tribe-subtribe-link { background: #191b20 !important; border-color: #444 !important; color: #ffa300 !important; } +.tribe-thumb-link { border-color: #444 !important; } +.tribe-thumb-link:hover { border-color: #ffa300 !important; } +.tribe-subtribe-link:hover { background: #333 !important; } +.tribe-parent-image { border-color: #ffa300 !important; } +.tribe-parent-box { background: #1e1f23 !important; } diff --git a/src/client/assets/themes/Matrix-SNH.css b/src/client/assets/themes/Matrix-SNH.css new file mode 100644 index 0000000..1daec99 --- /dev/null +++ b/src/client/assets/themes/Matrix-SNH.css @@ -0,0 +1,418 @@ +body { + background-color: #000000 !important; + color: #00FF00 !important; + font-family: 'Courier New', monospace; +} + +header, footer { + background-color: #000000; + color: #00FF00; + padding: 20px; + text-align: center; + font-size: 18px; +} + +footer { + border-top: 2px solid #00FF00; +} + +.sidebar-left, .sidebar-right { + background-color: #000000; + color: #00FF00; + border-right: 2px solid #00FF00; + padding: 15px; + margin-bottom: 10px; +} + +.main-column { + background-color: #000000; + color: #00FF00; + padding: 20px; + border-left: 2px solid #00FF00; + margin-bottom: 20px; +} + +button, input[type="submit"], input[type="button"] { + background-color: #000000; + color: #00FF00; + border: 2px solid #00FF00; + border-radius: 8px; + padding: 10px 20px; + cursor: pointer; + font-family: 'Courier New', monospace; + text-transform: none; + font-size: 16px; +} + +button:hover, input[type="submit"]:hover, input[type="button"]:hover { + background-color: #00FF00; + color: #000000; + border-color: #00FF00; + box-shadow: 0 0 10px rgba(0, 255, 0, 0.8); +} + +input, textarea, select { + background-color: #1A1A1A; + color: #00FF00; + border: 1px solid #00FF00; + border-radius: 5px; + padding: 10px; + font-family: 'Courier New', monospace; + font-size: 16px; +} + +a { + color: #00FF00; + text-decoration: none; + font-weight: normal; + font-size: 16px; +} + +table { + background-color: #000000; + color: #00FF00; + width: 100%; + border: 1px solid #00FF00; + font-size: 16px; +} + +table th { + background-color: #00FF00; + color: #000000; +} + +table tr:nth-child(even) { + background-color: #1A1A1A; +} + +nav ul { + background-color: #000000; + padding: 0; + margin: 0; +} + +nav ul li { + display: inline-block; + margin-right: 10px; +} + +nav ul li a { + color: #00FF00; + padding: 10px; + display: inline-block; + font-family: 'Courier New', monospace; + text-transform: none; + font-size: 16px; +} + +.profile { + background-color: #1A1A1A; + color: #00FF00; + padding: 20px; + border-radius: 8px; + border: 1px solid #00FF00; + box-shadow: 0 2px 5px rgba(0,255,0,0.5); +} + +.profile .name { + color: #00FF00; + font-size: 18px; + font-weight: bold; +} + +.avatar { + border: 4px solid #00FF00; + border-radius: 50%; + width: 80px; + height: 80px; + margin-bottom: 10px; +} + +article, section { + background-color: #1A1A1A; + color: #00FF00; + padding: 20px; + border-radius: 10px; + border: 1px solid #00FF00; + box-shadow: 0 4px 10px rgba(0, 255, 0, 0.5); +} + +.post-preview { + background-color: #00FF00; + padding: 15px; + border-radius: 8px; + color: #000000; +} + +.post-preview img { + border-radius: 8px; + max-width: 100%; +} + +::-webkit-scrollbar-thumb { + background-color: #00FF00; +} + +::-webkit-scrollbar-track { + background-color: #000000; +} + +.action-container { + background-color: #1A1A1A; + border-color: #00FF00; + color: #00FF00; +} + +footer a { + background-color: #00FF00; + color: #000000; + padding: 8px 20px; + border-radius: 5px; + text-decoration: none; +} + +.top-bar-left, +.top-bar-mid, +.top-bar-right { + background-color: #000000 !important; + border: 2px solid #00FF00 !important; + padding: 12px 16px; + box-shadow: 0 0 12px #00FF00; + border-radius: 8px; + display: flex; + gap: 12px; +} + +.sidebar-left, +.sidebar-right { + background-color: #000000 !important; + border: 2px solid #00FF00 !important; + box-shadow: 0 0 15px #00FF00; + padding: 16px; +} + +.sidebar-left nav ul, +.sidebar-right nav ul { + display: flex; + flex-direction: column; + gap: 10px; + margin: 0; + padding: 0; +} + +.sidebar-left nav ul li, +.sidebar-right nav ul li { + width: 100%; +} + +.sidebar-left nav ul li a, +.sidebar-right nav ul li a, +.header nav ul li a { + background-color: #000000 !important; + color: #00FF00 !important; + border: 1px solid #00FF00 !important; + font-weight: bold; + border-radius: 6px; + padding: 10px 14px; + display: flex; + justify-content: flex-start; + box-shadow: 0 0 6px #00FF00; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.sidebar-left nav ul li a:hover, +.sidebar-right nav ul li a:hover, +.header nav ul li a:hover { + background-color: #00FF00 !important; + color: #000000 !important; +} + +.card { + color: #00FF00; + font-family: 'Courier New', monospace; + background-color: #1A1A1A; +} + +.card-label { + color: #00FF00; +} + +.card-footer { + color: #00FF00; +} + + + +a.user-link, button.user-link { + background-color: #00FF00; + color: #000000; + border-color: #00FF00; + transition: background-color 0.3s, color 0.3s, border-color 0.3s; +} + +a.user-link:hover, button.user-link:hover { + background-color: #000000; + border-color: #00FF00; + color: #00FF00; + cursor: pointer; +} + +a.user-link:focus, button.user-link:focus { + background-color: #00FF00; + border-color: #00FF00; + color: #000000; +} + +.date-link { + background-color: #00FF00; + color: #000000; +} + +.date-link:hover { + background-color: #000000; + color: #00FF00; +} + +.activitySpreadInhabitant2 { + background-color: #1A1A1A; + color: #00FF00; +} + +.activityVotePost { + background-color: #00FF00; + color: #000000; +} + +.update-banner { + background-color: #001a00; + border-bottom-color: #003300; + color: #00FF00; +} + +.update-banner-link { + color: #00FF00; +} + +.snh-invite-code { + color: #00FF00 !important; +} + +.stats-kpi-label { color: #00FF00 !important; } +.stats-kpi-value { color: #00FF00 !important; } +.carbon-bar-note, .carbon-bar-formula { color: #00FF00 !important; } +.graphos-node-label { fill: #00FF00 !important; } +.graphos-node-label-me { fill: #5dade2 !important; font-weight: bold !important; } +.graphos-legend { color: #00FF00 !important; } +.graphos-edge { stroke: #003300 !important; } +/* Blockexplorer */ +.blockchain-view { background-color: #000000 !important; color: #00FF00 !important; } +.block { background: #1A1A1A !important; border: 1px solid #00FF00 !important; } +.block:hover { box-shadow: 0 0 15px rgba(0,255,0,0.3) !important; } +.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #00FF00 !important; } +.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #00FF00 !important; } +.block-content-preview, .block-content { background: #1A1A1A !important; color: #00FF00 !important; } +.block-author { color: #00FF00 !important; background: rgba(0,255,0,0.08) !important; } +.block-author:hover { color: #00FF00 !important; } +.block-url { color: #00FF00 !important; } +.block-row--details .block-url { background: #1A1A1A !important; } +.block-row--details .block-url:hover { background: #00FF00 !important; color: #000000 !important; } +.btn-singleview { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; } +.btn-singleview:hover { background: #00FF00 !important; color: #000000 !important; } +.btn-spread-on, .content-actions .btn-pin-on { background: #00FF00 !important; color: #000000 !important; } +.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #66FF66 !important; color: #000000 !important; } +.btn-singleview.btn-content { background: #00FF00 !important; color: #000000 !important; border: 1px solid #00FF00 !important; } +.btn-singleview.btn-content:hover { background: #000000 !important; color: #00FF00 !important; border-color: #00FF00 !important; } +.btn-back { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; } +.btn-back:hover { background: #00FF00 !important; color: #000000 !important; } +.block-info-table td, .pm-info-table td { border-color: #00FF00 !important; } +.block-diagram { border-color: #00FF00 !important; background: #000000 !important; } +.block-diagram-ruler { color: #00FF00 !important; background: #000000 !important; border-bottom-color: #00FF00 !important; } +.block-diagram-ruler span { color: #00FF00 !important; } +.block-diagram-cell { border-color: #00FF00 !important; background: #1A1A1A !important; } +.bd-label { color: #00FF00 !important; } +.bd-value { color: #00FF00 !important; } +.deleted-label { color: #ff3333 !important; } + +/* Tribes */ +.tribe-card { background: #1A1A1A !important; border-color: #00FF00 !important; } +.tribe-card:hover { box-shadow: 0 0 15px rgba(0,255,0,0.3) !important; } +.tribe-card-title { color: #00FF00 !important; } +.tribe-card-description { color: #00FF00 !important; } +.tribe-info-table td { border-color: #00FF00 !important; } +.tribe-info-label { color: #00FF00 !important; background: #1A1A1A !important; } +.tribe-info-value { color: #00FF00 !important; background: #1A1A1A !important; } +.tribe-info-empty { color: #006600 !important; } +.tribe-card-members { border-color: #00FF00 !important; background: #1A1A1A !important; } +.tribe-members-count { color: #00FF00 !important; } +.tribe-card-actions { border-color: #00FF00 !important; background: #1A1A1A !important; } +.tribe-action-btn { border-color: #00FF00 !important; color: #00FF00 !important; } +.tribe-action-btn:hover { background: #00FF00 !important; color: #000 !important; } +.tribe-subtribe-link { background: #000000 !important; border-color: #00FF00 !important; color: #00FF00 !important; } +.tribe-thumb-link { border-color: #00FF00 !important; } +.tribe-thumb-link:hover { box-shadow: 0 0 10px rgba(0,255,0,0.3) !important; } +.tribe-subtribe-link:hover { background: #00FF00 !important; color: #000 !important; } +.tribe-parent-image { border-color: #00FF00 !important; } +.tribe-parent-box { background: #1A1A1A !important; } + +.oasis-nav-header { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; } +.oasis-nav-header:hover { color: #000000 !important; background: #00FF00 !important; } +.oasis-nav-header .emoji, .oasis-nav-header .nav-text, .oasis-nav-arrow { color: inherit !important; } +.oasis-nav-list li a { color: #00FF00 !important; } +.oasis-nav-list li a:hover { color: #00FF00 !important; opacity: 1 !important; } +.oasis-nav-list .emoji, .oasis-nav-list .nav-text { color: inherit !important; } +nav, .sidebar-left nav, .sidebar-right nav, .sidebar-left ul, .sidebar-right ul { color: #00FF00 !important; } +.sidebar-left a, .sidebar-right a { color: #00FF00 !important; } + +.ai-ask-form .ai-ask-input, .ai-ask-form .ai-ask-btn { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; } +.ai-ask-form .ai-ask-input::placeholder { color: rgba(0,255,0,0.55) !important; } +.ai-ask-form .ai-ask-input:focus { border-color: #00FF00 !important; background: #1A1A1A !important; box-shadow: 0 0 6px rgba(0,255,0,0.4) !important; } +.ai-ask-form .ai-ask-btn:hover { background: #00FF00 !important; color: #000000 !important; border-color: #00FF00 !important; } +.ai-nav-results, .ai-nav-result-card { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; } +.ai-nav-result-card .card-label { color: #00FF00 !important; } +.ai-nav-result-card a { color: #00FF00 !important; } +.ai-nav-result-card a:hover { background: #00FF00 !important; color: #000000 !important; } +.ai-nav-query { color: #00FF00 !important; } +.trending-card.own-content, .addr-card.ubi-pub-card.own-content { box-shadow: inset 3px 0 0 #00FF00; } +.map-stage { background: #000000 !important; border-color: #00FF00 !important; } +.map-label-country { fill: #00FF00; stroke: #000000; } +.map-place-dot { fill: #00FF00; stroke: #000000; } +.map-place-capital { fill: #66FF66; } +.map-place-label { fill: #00FF00; stroke: #000000; } +.map-pin-body { fill: #00aa00; stroke: #00FF00; } +.map-pin-main .map-pin-body { fill: #00FF00; stroke: #000000; } +.map-pin-focus .map-pin-body { fill: #ffffff; stroke: #00FF00; } +.map-pin-eye { fill: #000000; } +.map-pin-label-bg { fill: rgba(0, 0, 0, 0.9); stroke: #00FF00; } +.map-pin-focus .map-pin-label-bg { stroke: #ffffff; } +.map-pin-label { fill: #00FF00; } +.map-pins-layer a:hover .map-pin-body, .map-pins a:hover .map-pin-body { fill: #ffffff; } +.map-cluster-ring { fill: rgba(0, 255, 0, 0.25); } +.map-cluster-dot { fill: #00FF00; stroke: #000000; } +.map-pick-ring, .map-pick-cross { stroke: #00FF00; } +.map-scale-line { stroke: #00FF00; } +.map-scale-text { fill: #00FF00; stroke: #000000; } +.map-attrib { fill: #008800; } +.map-ctrl { background: rgba(0, 0, 0, 0.9) !important; color: #00FF00 !important; border-color: #00FF00 !important; } +.map-ctrl:hover { background: #00FF00 !important; color: #000000 !important; } +.map-ctrl-off:hover { background: rgba(0, 0, 0, 0.9) !important; color: #00FF00 !important; } +.map-pick-cell:hover { background: rgba(0, 255, 0, 0.18); outline-color: rgba(0, 255, 0, 0.7); } +.map-place-result, .map-center-link { color: #00FF00 !important; border-color: #00FF00 !important; } +.map-place-result:hover, .map-center-link:hover { background: #00FF00 !important; color: #000000 !important; } +.map-picked { background: #050505 !important; color: #00FF00 !important; } +.map-picked-near { color: #00CC00 !important; } +.map-markers-list .map-marker-row { border-color: #003300 !important; } +.map-markers-list .map-marker-row--focus { background: rgba(0, 255, 0, 0.08) !important; box-shadow: inset 3px 0 0 #00FF00 !important; } +.map-marker-idx { background: #004400 !important; color: #00FF00 !important; } +.map-marker-idx--main, .map-marker-row--focus .map-marker-idx { background: #00FF00 !important; color: #000000 !important; } +.map-marker-label { color: #00FF00 !important; } +.map-marker-date { color: #00AA00 !important; } +.map-popup-pointer { fill: #000000; stroke: #00FF00; } +.map-stage .map-popup-card { background: #000000; border-color: #00FF00; color: #00FF00; } +.map-stage .map-popup-text { color: #00FF00; } +.map-popup-coords { color: #66FF66; } +.map-stage .map-popup-meta { color: #00AA00; } +.map-popup-close { color: #00AA00; } +.map-popup-close:hover { color: #ffffff; } +.oasis-pager-info { color: #008800; } +.oasis-pager-btn { background: #0a0a0a; color: #00CC00; border: 1px solid #00FF00; } +.oasis-pager-btn:hover, .oasis-pager-btn.active { border-color: #00FF00; color: #00FF00; } diff --git a/src/client/assets/themes/OasisMobile.css b/src/client/assets/themes/OasisMobile.css index 7726265..889157e 100644 --- a/src/client/assets/themes/OasisMobile.css +++ b/src/client/assets/themes/OasisMobile.css @@ -94,8 +94,10 @@ article, section { } div { - background-color: #1A1A1A; - border: 1px solid #333; + background-color: transparent; + border: 0; + box-shadow: none; + transition: none !important; } div .header-content { @@ -210,24 +212,9 @@ footer a:hover { gap: 9px; } -.card-tags a.tag-link { - text-decoration: none; - color: #181818; - background: #FFD600; - padding: 5px 13px 4px 13px; - border-radius: 7px; - font-size: .98em; - border: none; - font-weight: bold; -} -.card-tags a.tag-link:hover { - background: #ffe86a; - color: #111; - cursor: pointer; -} -a.user-link { +a.user-link, button.user-link { background-color: #FFA500; color: #000; padding: 8px 16px; @@ -241,14 +228,14 @@ a.user-link { font-size: 0.8em; } -a.user-link:hover { +a.user-link:hover, button.user-link:hover { background-color: #FFD700; border-color: #FFD700; color: #000; cursor: pointer; } -a.user-link:focus { +a.user-link:focus, button.user-link:focus { background-color: #007B9F; border-color: #007B9F; color: #fff; @@ -538,6 +525,7 @@ h3 { font-size: 1em !important; } .filter-group, .activity-filter-grid, .activity-sub-filter, +.bank-range-tabs, .filters .ui-toolbar { display: flex !important; flex-direction: row !important; @@ -753,7 +741,7 @@ h3 { font-size: 1em !important; } } .graphos-node-label-me { - fill: #ffa500 !important; + fill: #5dade2 !important; } .graphos-legend { @@ -825,6 +813,64 @@ h3 { font-size: 1em !important; } } .oasis-menu-btn .oasis-menu-ico { font-size: 1.05rem !important; } +.room-item .mailing-archive-head { + flex-direction: column !important; + flex-wrap: nowrap !important; + align-items: stretch !important; + gap: 8px !important; +} +.room-item .mailing-archive-head > .content-actions { + order: -1 !important; + align-self: flex-end !important; +} +.room-item .mailing-archive-meta { + width: 100% !important; + flex: 1 1 auto !important; + align-items: center !important; + gap: 6px !important; +} +.room-item .mailing-archive-meta > .pm-exposition-chip, +.room-item .mailing-archive-meta > span { + white-space: nowrap !important; + flex: 0 0 auto !important; + width: auto !important; +} +.card-section .card-field.activity-room-title { + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: center !important; + justify-content: flex-start !important; + gap: 8px !important; +} +.card-section .card-field.activity-room-title > .card-value { + text-align: left !important; +} +.oasis-pager-sizes { + flex: 1 1 100% !important; + margin-right: 0 !important; +} +.room-live-controls .room-refresh-btn { + font-size: 16px !important; + min-height: 44px !important; +} + +.oasis-menu-slot { + display: inline-flex !important; +} +.oasis-menu-slot .oasis-menu-close { + display: none !important; +} +#nav-left-toggle:checked ~ .header .oasis-menu-slot-left .oasis-menu-open, +#nav-right-toggle:checked ~ .header .oasis-menu-slot-right .oasis-menu-open { + display: none !important; +} +#nav-left-toggle:checked ~ .header .oasis-menu-slot-left .oasis-menu-close, +#nav-right-toggle:checked ~ .header .oasis-menu-slot-right .oasis-menu-close { + display: inline-flex !important; + background: #FFD700 !important; + color: #161616 !important; +} + .header .top-bar-left nav { order: 3 !important; flex: 1 1 100% !important; @@ -839,7 +885,7 @@ h3 { font-size: 1em !important; } .header .top-bar-right nav ul, .header .top-bar-mid nav ul { display: grid !important; - grid-template-columns: repeat(3, 1fr) !important; + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; gap: 6px !important; overflow: visible !important; list-style: none !important; @@ -865,9 +911,13 @@ h3 { font-size: 1em !important; } width: 100% !important; min-height: 42px !important; box-sizing: border-box !important; - padding: 6px 8px !important; - font-size: 0.85rem !important; - white-space: nowrap !important; + padding: 6px 4px !important; + font-size: 0.78rem !important; + line-height: 1.15 !important; + text-align: center !important; + white-space: normal !important; + overflow-wrap: break-word !important; + hyphens: auto !important; border: 1px solid #FFD700 !important; border-radius: 8px !important; background: #161616 !important; @@ -919,6 +969,7 @@ h3 { font-size: 1em !important; } .activity-filter-grid, .activity-filters, .activity-sub-filter, +.bank-range-tabs, .filters .ui-toolbar { display: flex !important; flex-direction: row !important; @@ -1003,3 +1054,2989 @@ a.hashtag { .profile-reach { max-width: 100% !important; overflow: hidden !important; text-align: center !important; } .feed-card .card-footer { display: flex !important; flex-wrap: wrap !important; align-items: center !important; gap: 6px !important; row-gap: 4px !important; } .feed-card .date-link { max-width: 100% !important; overflow-wrap: anywhere !important; font-size: 0.85em !important; padding: 6px 10px !important; } + +.data-cohesion-figures { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)) !important; gap: 8px !important; max-width: 100% !important; } +.data-figure { border: 1px solid rgba(128, 128, 128, .25) !important; border-radius: 8px !important; padding: 8px 6px !important; text-align: center !important; max-width: 100% !important; min-width: 0 !important; } +.data-figure-value { font-size: 1.2em !important; overflow-wrap: anywhere !important; } +.data-figure-label { font-size: .7em !important; overflow-wrap: anywhere !important; } +.data-top-terms { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; max-width: 100% !important; } +.data-card { max-width: 100% !important; overflow: hidden !important; } +.data-card-title, .data-card-author { overflow-wrap: anywhere !important; } +.data-common-row { max-width: 100% !important; } +.data-search { width: 100% !important; max-width: 100% !important; } +.data-search input { width: 100% !important; max-width: 100% !important; } +.data-cohesion { max-width: 100% !important; overflow: hidden !important; } +.chat-message { max-width: 92% !important; } +.chat-message.chat-poll { max-width: 97% !important; } +.data-pair { grid-template-columns: 1fr !important; gap: 6px !important; } +.data-pair-side { width: 100% !important; max-width: 100% !important; min-width: 0 !important; } +.data-pair-link { max-width: 100% !important; overflow-wrap: anywhere !important; } + +.main-content.chatsux-only { padding: 4px !important; } +.chat-workspace { + display: grid !important; + grid-template-columns: minmax(92px, 34%) 1fr !important; + grid-template-areas: "info info" "list chat" !important; + grid-template-rows: min-content 1fr !important; + align-content: start !important; + gap: 8px !important; + min-height: 72vh !important; +} +.chat-topics { grid-area: list !important; width: 100% !important; max-width: 100% !important; max-height: none !important; overflow: hidden !important; } +.chat-topics a { overflow-wrap: anywhere !important; } +.chat-topics-head { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; justify-content: space-between !important; gap: 6px !important; } +.chat-topics-head .chat-topics-title-link { overflow-wrap: normal !important; white-space: nowrap !important; } +.chat-topics-new { padding: 2px 8px !important; font-size: .9em !important; flex-shrink: 0 !important; line-height: 1.4 !important; } +.chat-workspace-main.chat-workspace-empty { grid-area: chat !important; display: flex !important; align-items: center !important; justify-content: center !important; } +.chat-workspace-main:not(.chat-workspace-empty) { display: contents !important; } +.chat-workspace-details { display: contents !important; } +.chat-workspace-details > .tribe-side { + grid-area: info !important; + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: center !important; + gap: 8px !important; + padding: 6px 8px !important; + width: 100% !important; + max-width: 100% !important; + border: 1px solid rgba(128, 128, 128, .25) !important; + border-radius: 8px !important; +} +.chat-workspace-details > .tribe-side .shop-title-row { margin: 0 !important; } +.chat-workspace-details > .tribe-side .tribe-card-title { font-size: 1em !important; margin: 0 !important; } +.chat-workspace-details .tribe-detail-image { max-width: 36px !important; max-height: 36px !important; height: auto !important; border-radius: 50% !important; } +.chat-workspace-details .tribe-info-table { display: none !important; } +.chat-workspace-details > .tribe-main { grid-area: chat !important; width: 100% !important; max-width: 100% !important; min-width: 0 !important; } +.chat-list-toggle { display: none !important; } +.chat-list-hide-btn { display: inline-flex !important; align-items: center !important; padding: 2px 8px !important; border: 1px solid rgba(128, 128, 128, .25) !important; border-radius: 8px !important; cursor: pointer !important; font-size: .8em !important; user-select: none !important; flex-shrink: 0 !important; } +.chat-list-show-btn { display: none !important; } +#chat-list-toggle:checked ~ .chat-list-show-btn { display: inline-flex !important; align-items: center !important; width: fit-content !important; padding: 3px 10px !important; margin: 0 0 6px 0 !important; border: 1px solid rgba(128, 128, 128, .25) !important; border-radius: 8px !important; cursor: pointer !important; font-size: .8em !important; user-select: none !important; } +#chat-list-toggle:checked ~ .chat-workspace { grid-template-columns: 1fr !important; grid-template-areas: "info" "chat" !important; } +#chat-list-toggle:checked ~ .chat-workspace .chat-topics { display: none !important; } +.chat-workspace-details > .tribe-side > div, +.chat-workspace-details > .tribe-side > form { + display: inline-flex !important; flex-direction: row !important; flex-wrap: wrap !important; + align-items: center !important; gap: 6px !important; + background: transparent !important; border: none !important; box-shadow: none !important; + padding: 0 !important; margin: 0 !important; width: auto !important; min-width: 0 !important; +} +.chat-workspace-details > .tribe-side div { background: transparent !important; border: none !important; box-shadow: none !important; } +.chat-workspace-details > .tribe-side > .tribe-detail-image { order: -2 !important; } +.chat-workspace-details > .tribe-side > .shop-title-row { order: -1 !important; } +.chat-workspace-details > .tribe-side .tribe-side-actions form, .chat-workspace-details > .tribe-side .inline-form { display: inline-flex !important; margin: 0 !important; } +.chat-workspace-details > .tribe-side .tribe-action-btn { padding: 3px 10px !important; font-size: .85em !important; } +.chat-workspace-details > .tribe-side .card-label { margin: 0 !important; } +.chat-workspace-details > .tribe-side .tribe-members-count { font-size: .85em !important; } +.chat-workspace-details > .tribe-side { padding: 4px 8px !important; } +.chat-workspace-details > .tribe-side .card-chips-row, +.chat-workspace-details > .tribe-side .tribe-side-tags, +.chat-workspace-details > .tribe-side .tribe-open-invite, +.chat-workspace-details > .tribe-side .housing-status-row > span { display: none !important; } +.chat-workspace-details > .tribe-side .tribe-card-title { font-size: .95em !important; } +.chat-workspace-details > .tribe-side .tribe-action-btn { padding: 2px 8px !important; font-size: .8em !important; } +.chat-workspace-details > .tribe-side .card-header { display: none !important; } +.chat-workspace-details > .tribe-side .tribe-detail-image { display: inline-block !important; max-width: 28px !important; max-height: 28px !important; width: auto !important; height: auto !important; border-radius: 50% !important; order: -3 !important; } +.chat-workspace-details > .tribe-side > .shop-title-row { order: -2 !important; } +.chat-workspace-details > .tribe-side .tribe-card-description { order: -1 !important; margin: 2px 0 0 0 !important; font-size: .8em !important; opacity: .85 !important; white-space: normal !important; overflow: visible !important; text-overflow: unset !important; max-width: 100% !important; flex-basis: 100% !important; } +.chat-workspace-details > .tribe-side .tribe-card-members { flex-basis: 100% !important; font-size: .85em !important; } +.chat-topic { display: flex !important; align-items: center !important; gap: 6px !important; flex-wrap: wrap !important; } +.chat-topic-thumb { width: 28px !important; height: 28px !important; border-radius: 50% !important; object-fit: cover !important; flex-shrink: 0 !important; } +.chat-topic-title { flex: 1 1 auto !important; min-width: 0 !important; } +.chat-topic-meta { flex-basis: 100% !important; font-size: .75em !important; opacity: .7 !important; } +.chat-topics { min-width: 0 !important; justify-content: flex-start !important; gap: 2px !important; padding: 6px !important; } +.chat-topic { padding: 6px 6px !important; min-height: 0 !important; } +.chat-topic-title { white-space: normal !important; display: -webkit-box !important; -webkit-line-clamp: 2 !important; -webkit-box-orient: vertical !important; overflow: hidden !important; font-size: .85em !important; font-weight: 600 !important; color: #ddd !important; } +.chat-topic-active .chat-topic-title { color: #ffa300 !important; } +.chat-topic-meta { display: none !important; } +.chat-topics-head { padding: 2px 4px !important; } +.chat-topics-title-link { font-size: .95em !important; } +.chat-list-hide-btn { color: #ffa300 !important; font-size: 1em !important; line-height: 1.2 !important; padding: 2px 9px !important; } +.chat-topic-meta { display: block !important; flex-basis: 100% !important; font-size: .7em !important; color: #ffa300 !important; opacity: .9 !important; } +.chat-topic-last { flex-basis: 100% !important; font-size: .75em !important; color: #999 !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; min-width: 0 !important; max-width: 100% !important; } +.chat-workspace-details > .tribe-side .tribe-card-members { font-size: .9em !important; color: #ffa300 !important; font-weight: bold !important; } +.chat-workspace-details > .tribe-side .tribe-action-btn { padding: 1px 7px !important; font-size: .75em !important; } +.chat-topic-last-author { font-weight: bold !important; color: #bbb !important; } +.chat-topic { display: grid !important; grid-template-columns: 32px minmax(0, 1fr) !important; grid-auto-rows: min-content !important; column-gap: 8px !important; row-gap: 2px !important; align-items: center !important; padding: 6px !important; } +.chat-topic-thumb { grid-column: 1 !important; grid-row: 1 / span 2 !important; width: 32px !important; height: 32px !important; } +.chat-topic-title { grid-column: 2 !important; grid-row: 1 !important; line-height: 1.25 !important; } +.chat-topic-meta { grid-column: 2 !important; grid-row: 2 !important; } +.chat-topic-last { grid-column: 1 / -1 !important; grid-row: 3 !important; display: block !important; } +.chat-workspace { grid-template-columns: minmax(140px, 42%) 1fr !important; } +.chat-topic { display: grid !important; grid-template-columns: 38px minmax(0, 1fr) !important; grid-template-rows: auto auto auto !important; column-gap: 8px !important; row-gap: 1px !important; align-items: center !important; padding: 6px !important; } +.chat-topic-thumb { grid-column: 1 !important; grid-row: 1 / span 3 !important; width: 38px !important; height: 38px !important; align-self: center !important; } +.chat-topic-title { grid-column: 2 !important; grid-row: 1 !important; display: block !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; font-size: .85em !important; font-weight: 700 !important; color: #eee !important; } +.chat-topic-meta { grid-column: 2 !important; grid-row: 2 !important; display: block !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; font-size: .7em !important; } +.chat-topic-last { grid-column: 2 !important; grid-row: 3 !important; display: block !important; font-size: .75em !important; } +.chat-workspace-list .chat-list-hide-btn { display: none !important; } +.chat-topics-new { min-width: auto !important; min-height: auto !important; width: auto !important; height: auto !important; padding: 2px 10px !important; line-height: 1.3 !important; font-size: 1em !important; } +.chat-topic-last { font-size: .7em !important; } +.chat-topic-last-author { font-size: 1em !important; } +.chat-workspace-list .chat-workspace { grid-template-columns: 1fr !important; grid-template-areas: "list" !important; } +.chat-workspace-list .chat-workspace-empty { display: none !important; } +.chat-workspace-list .chat-topics { border: none !important; background: transparent !important; } +.chat-topic-title { color: #ffa300 !important; } +.chat-topic-last-author { color: #ffa300 !important; } +.chat-topic-last-author { color: #ffdd44 !important; } +.chat-topic-meta { color: #ffdd44 !important; } +.chat-topic-last-author { color: #ddd !important; } +.chat-jump-row { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: 8px !important; } +#chat-list-toggle:checked ~ .chat-workspace .chat-list-show-btn { display: inline-flex !important; align-items: center !important; padding: 3px 10px !important; border: 1px solid rgba(128, 128, 128, .25) !important; border-radius: 8px !important; cursor: pointer !important; font-size: .8em !important; user-select: none !important; } +.chat-workspace-section:has(.chat-jump-row) > .chat-list-show-btn { display: none !important; } +.chat-workspace-details > .tribe-side .tribe-action-btn { padding: 1px 6px !important; font-size: .7em !important; } +.chat-workspace { grid-template-columns: 1fr !important; grid-template-areas: "info" "chat" !important; } +.chat-back-btn { text-decoration: none !important; } +.chat-workspace-details .tribe-card-description { white-space: normal !important; overflow: visible !important; text-overflow: unset !important; max-width: 100% !important; flex-basis: 100% !important; margin: 2px 0 0 0 !important; font-size: .8em !important; opacity: .85 !important; } + +.relationship-actions { display: flex !important; flex-direction: column !important; gap: 6px !important; width: 100% !important; } +.relationship-actions form { width: 100% !important; margin: 0 !important; display: block !important; } +.relationship-actions .filter-btn { width: 100% !important; white-space: nowrap !important; } +.inhabitant-activity-group { display: flex !important; flex-direction: column !important; align-items: center !important; text-align: center !important; gap: 4px !important; width: 100% !important; } +.inhabitant-last-activity { text-align: center !important; } +.profile-sensors-box { display: flex !important; flex-direction: column !important; align-items: center !important; text-align: center !important; width: 100% !important; } +.profile-sensors-box > * { margin-left: auto !important; margin-right: auto !important; text-align: center !important; } +.feed-row { align-items: center !important; } +.refeed-column { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; gap: 4px !important; } +.refeed-column h1 { margin: 0 !important; } +.feed-card .card-footer { justify-content: center !important; align-items: center !important; text-align: center !important; } +.invite-qr-card { border: none !important; background: transparent !important; box-shadow: none !important; padding: 0 !important; margin: 8px auto !important; display: flex !important; justify-content: center !important; width: fit-content !important; } +.invite-qr-img { display: block !important; margin: 0 auto !important; } +.stats-kpi { display: flex !important; flex-direction: row !important; justify-content: space-between !important; align-items: center !important; gap: 8px !important; width: 100% !important; } +.stats-kpi-label { text-align: left !important; margin: 0 !important; } +.stats-kpi-value { text-align: right !important; margin: 0 !important; } +audio, video { max-width: 100% !important; } +.main-column audio { width: 100% !important; display: block !important; } +.chat-workspace-details > .tribe-side .housing-status-row:not(:has(form)) { display: none !important; } +.buy-btn { display: block !important; width: 100% !important; padding: 10px 16px !important; font-size: 1.05em !important; font-weight: bold !important; background: #ffa300 !important; color: #111 !important; border: none !important; border-radius: 8px !important; text-align: center !important; margin: 8px auto 4px auto !important; cursor: pointer !important; } +.chat-messages-list { gap: 3px !important; } +.chat-messages-list > div, .chat-bubble-row { margin: 0 !important; padding: 0 !important; } +.chat-message { margin: 0 !important; } +.chat-message p { margin: 0 !important; } +.chat-day-separator { margin: 8px 0 2px 0 !important; } +.module-table { width: 100% !important; table-layout: fixed !important; border-collapse: collapse !important; } +.module-table td:first-child, .module-table th:first-child { width: 27% !important; } +.module-table td:last-child, .module-table th:last-child { width: 19% !important; text-align: center !important; } +.module-table th { white-space: nowrap !important; font-size: .9em !important; } +.module-table td:first-child a { white-space: normal !important; word-break: keep-all !important; overflow-wrap: normal !important; hyphens: none !important; } +.module-table td, .module-table th { padding: 8px 6px !important; vertical-align: middle !important; } +.module-table td:nth-child(2) p { font-size: .85em !important; margin: 0 !important; } +.pdf-canvas-wrap { height: auto !important; max-height: 70vh !important; overflow: auto !important; } +.pdf-viewer-container { padding: 8px !important; } +.pdf-viewer-container .pdf-controls { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; justify-content: center !important; align-items: center !important; margin-top: 8px !important; } +.pdf-viewer-container button { font-size: 1em !important; padding: 7px 10px !important; min-width: 42px !important; } +.pdf-viewer-container span { margin: 0 4px !important; } +.pdf-viewer-container { margin-bottom: 8px !important; } +.pdf-maximized { position: fixed !important; inset: 0 !important; z-index: 9999 !important; margin: 0 !important; border-radius: 0 !important; background: #111 !important; display: flex !important; flex-direction: column !important; padding: 6px !important; overflow: hidden !important; } +.pdf-maximized .pdf-canvas-wrap { flex: 1 1 auto !important; max-height: none !important; height: auto !important; overflow: auto !important; } +.pdf-maximized .pdf-controls { flex-shrink: 0 !important; margin: 6px 0 !important; } +.pdf-maximized-body { overflow: hidden !important; } + +.main-content .feed-workspace { display: flex !important; flex-direction: column !important; gap: 10px !important; } +.feed-workspace .feed-side { display: block !important; position: static !important; } +.feed-workspace .feed-side-left { order: 1 !important; } +.feed-workspace .feed-side-right { order: 2 !important; } +.feed-workspace .feed-workspace-center { order: 3 !important; } +.feed-workspace .feed-side-users { flex-direction: row !important; overflow-x: auto !important; gap: 12px !important; padding: 2px 0 6px 0 !important; } +.feed-workspace .feed-side-user { flex-direction: column !important; flex: 0 0 auto !important; width: 64px !important; align-items: center !important; gap: 4px !important; } +.feed-workspace .feed-side-avatar { width: 44px !important; height: 44px !important; } +.feed-workspace .feed-side-user a { font-size: .65em !important; max-width: 62px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } +.feed-workspace .feed-side-box { padding: 8px 10px !important; } +.feed-workspace .feed-side-title { font-size: .85em !important; margin: 0 0 6px 0 !important; } +.feed-card .feed-row { display: flex !important; flex-direction: column !important; gap: 6px !important; align-items: stretch !important; } +.feed-card .refeed-column { order: 2 !important; display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; gap: 10px !important; align-items: center !important; justify-content: center !important; } +.feed-card .refeed-column h1 { font-size: 1.1em !important; margin: 0 !important; } +.feed-card .refeed-column .muted { font-size: .7em !important; margin: 0 !important; } +.feed-card .refeed-btn { padding: 4px 14px !important; font-size: .85em !important; width: auto !important; } +.feed-card .feed-main { order: 1 !important; width: 100% !important; max-width: 100% !important; min-width: 0 !important; } +.feed-card .feed-main h2 { font-size: .85em !important; } +.feed-card .date-link { font-size: .7em !important; padding: 2px 6px !important; } +.feed-sides-row { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 8px !important; margin: 8px 0 !important; align-items: start !important; width: 100% !important; } +.feed-sides-row .feed-side { display: block !important; position: static !important; width: 100% !important; min-width: 0 !important; } +.feed-sides-row .feed-side-box { padding: 8px !important; width: 100% !important; box-sizing: border-box !important; } +.feed-sides-row .feed-side-title { font-size: .8em !important; margin: 0 0 6px 0 !important; } +.feed-sides-row .feed-side-users { display: flex !important; flex-direction: column !important; gap: 6px !important; overflow: visible !important; padding: 0 !important; } +.feed-sides-row .feed-side-user { flex-direction: row !important; width: 100% !important; gap: 6px !important; align-items: center !important; } +.feed-sides-row .feed-side-avatar { width: 24px !important; height: 24px !important; } +.feed-sides-row .feed-side-user a { font-size: .75em !important; max-width: 100% !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } +.feed-sides-row .feed-side-tags { gap: 4px !important; } +.feed-sides-row .tag-link { font-size: .75em !important; } +.feed-card { padding: 8px !important; } +.feed-card .activity-card-header { padding: 0 !important; margin: 0 !important; min-height: 0 !important; } +.feed-card .card-section { padding: 4px !important; margin: 0 !important; } +.feed-card .feed-card-body { padding: 4px !important; } +.feed-card .feed-main { padding: 0 !important; width: 100% !important; } +.feed-card .feed-text { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } +.feed-card .refeed-column { justify-content: center !important; align-items: center !important; gap: 10px !important; margin: 4px 0 0 0 !important; } +.feed-card .refeed-column h1 { font-size: 1.5em !important; margin: 0 !important; padding: 6px 16px !important; border: 1px solid rgba(128, 128, 128, .35) !important; border-radius: 8px !important; line-height: 1 !important; } +.feed-card .refeed-column .muted { font-size: .75em !important; } +.feed-card .card-footer { gap: 4px !important; margin: 4px 0 0 0 !important; } +.feed-card .card-footer a { font-size: .75em !important; padding: 2px 8px !important; } +.feed-card .date-link { font-size: .7em !important; padding: 2px 6px !important; } +.feed-card .activity-card-header { justify-content: flex-end !important; gap: 4px !important; margin-bottom: 4px !important; } +.feed-card .activity-card-header button, .feed-card .activity-card-header a { width: 34px !important; height: 34px !important; min-width: 34px !important; padding: 4px !important; font-size: .9em !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; } +.feed-card .feed-main h2 { text-align: center !important; margin: 6px 0 2px 0 !important; } +.feed-card .card-footer { justify-content: center !important; flex-wrap: nowrap !important; } +.feed-card .date-link { display: inline-flex !important; align-items: center !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: 55% !important; font-size: .65em !important; } +.feed-card .card-footer a { max-width: 42% !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; display: inline-block !important; } +.feed-card .card-engage { display: flex !important; flex-direction: column !important; gap: 4px !important; margin-top: 6px !important; } +.feed-card .card-engage summary { padding: 6px 10px !important; font-size: .85em !important; min-height: 0 !important; } +.feed-card .card-engage details, .feed-card .card-engage .comments-link { padding: 0 !important; margin: 0 !important; } +.feed-card .comments-link { padding: 6px 10px !important; font-size: .85em !important; display: block !important; text-align: center !important; } +.feed-card .card-footer { flex-wrap: wrap !important; justify-content: center !important; max-width: 100% !important; } +.feed-card .date-link { white-space: normal !important; overflow: visible !important; text-overflow: unset !important; max-width: 100% !important; font-size: .75em !important; } +.feed-card .card-footer a { max-width: 100% !important; white-space: normal !important; overflow: visible !important; overflow-wrap: anywhere !important; } +.feed-card .refeed-column { border: 1px solid rgba(128, 128, 128, .3) !important; border-radius: 8px !important; padding: 8px !important; width: 100% !important; box-sizing: border-box !important; } +.feed-card { overflow: hidden !important; } +.feed-card .refeed-column { flex-wrap: wrap !important; } +.feed-card .refeed-column .muted { flex-basis: 100% !important; text-align: center !important; margin: 4px 0 0 0 !important; } +.feed-card .card-engage summary, .feed-card .comments-link { min-height: 40px !important; padding: 8px 10px !important; box-sizing: border-box !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: .85em !important; } +.comments-collapse .comments-inline-form { display: flex !important; flex-direction: column !important; gap: 6px !important; padding: 8px !important; } +.comments-collapse textarea { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; } +.comments-collapse .filter-btn { width: auto !important; align-self: center !important; padding: 6px 18px !important; } +.comments-collapse .comments-view-all { display: block !important; text-align: center !important; padding: 6px !important; font-size: .8em !important; } +.comments-collapse .comment-form { display: flex !important; flex-direction: column !important; gap: 6px !important; width: 100% !important; } +.comments-collapse .comment-textarea { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; } +.comments-collapse .comment-file-upload { display: flex !important; flex-direction: column !important; gap: 4px !important; width: 100% !important; } +.comments-collapse .comment-file-upload input { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; } +.comments-collapse .comment-form-title { font-size: .9em !important; margin: 4px 0 !important; } +.comments-collapse .comment-submit-btn { align-self: center !important; padding: 6px 20px !important; } +.comments-collapse .vote-comments-section { padding: 8px !important; } +.feed-card .refeed-column h1 { min-height: 44px !important; min-width: 44px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; padding: 0 16px !important; font-size: 1.3em !important; box-sizing: border-box !important; } +.feed-card .refeed-btn { min-height: 44px !important; box-sizing: border-box !important; display: inline-flex !important; align-items: center !important; } +.feed-card .refeed-column h1, .feed-card .refeed-column .refeed-btn { height: 48px !important; min-height: 48px !important; margin: 0 !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; box-sizing: border-box !important; vertical-align: middle !important; } +.feed-card .refeed-column form { margin: 0 !important; display: inline-flex !important; align-items: center !important; } +.school-lessons { width: 100% !important; padding: 8px !important; box-sizing: border-box !important; } +.school-lesson { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; padding: 8px !important; margin: 0 0 8px 0 !important; } +.school-lesson-header { display: flex !important; flex-wrap: wrap !important; align-items: center !important; justify-content: space-between !important; gap: 6px !important; width: 100% !important; padding: 0 !important; margin: 0 !important; background: transparent !important; border: none !important; } +.school-lesson-header h3 { margin: 0 !important; flex: 1 1 auto !important; min-width: 0 !important; font-size: 1em !important; } +.school-lesson-header h3 a { display: block !important; width: 100% !important; overflow-wrap: anywhere !important; } +.school-lesson > p { width: 100% !important; max-width: 100% !important; margin: 6px 0 !important; padding: 0 !important; } +.school-lesson-meta { display: flex !important; flex-wrap: wrap !important; gap: 4px !important; width: 100% !important; max-width: 100% !important; padding: 4px 0 0 0 !important; margin: 0 !important; background: transparent !important; border: none !important; font-size: .8em !important; opacity: .8 !important; } +.school-lesson-meta span { white-space: normal !important; overflow-wrap: anywhere !important; } +.school-teacher-panel { width: 100% !important; box-sizing: border-box !important; padding: 8px !important; } +.school-teacher-panel input, .school-teacher-panel textarea, .school-teacher-panel select { width: 100% !important; box-sizing: border-box !important; } +textarea, select, input:not([type="checkbox"]):not([type="radio"]) { width: 100% !important; box-sizing: border-box !important; } +.create-tribe-form { width: 100% !important; box-sizing: border-box !important; padding: 8px !important; } +.create-tribe-form form { width: 100% !important; } +.tribe-card { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } +.tribe-card-body { width: 100% !important; box-sizing: border-box !important; padding: 8px !important; } +.filter-box { width: 100% !important; box-sizing: border-box !important; } +.filter-box__input { width: 100% !important; } + +.main-content { padding: 6px !important; gap: 8px !important; } +.main-column { padding: 4px !important; } +.tribe-details { padding: 8px !important; gap: 12px !important; } +.tribe-side, .tribe-main { padding: 10px !important; width: 100% !important; box-sizing: border-box !important; } +.feed-container, [class*="-module-section"] { padding: 8px !important; } +.trending-card { padding: 10px !important; margin: 0 0 10px 0 !important; } +.card-section { padding: 8px !important; } +.main-column section { padding-left: 0 !important; padding-right: 0 !important; } +.card-section-action { padding: 0 !important; border: none !important; background: transparent !important; box-shadow: none !important; justify-content: center !important; } +.card-section-action .keep-reading-link { margin: 6px auto !important; } + +.bank-range-tabs .filter-btn, +.tribe-mode-buttons .filter-btn, .ui-toolbar--filters .filter-btn, .mode-buttons .filter-btn, .mode-buttons-row .filter-btn, .mode-buttons-cols .filter-btn, .filters .filter-btn, .inhabitant-action .filter-btn, .activity-filter-grid .filter-btn, .activity-filter-col .filter-btn, .activity-sub-filter .filter-btn { + min-height: 0 !important; + height: auto !important; + font-size: .75em !important; + padding: 2px 9px !important; + border: 1px solid #555 !important; + border-radius: 12px !important; + background: transparent !important; + color: inherit !important; + line-height: 1.5 !important; + font-weight: normal !important; +} +.bank-range-tabs .filter-btn.active, +.tribe-mode-buttons .filter-btn.active, .ui-toolbar--filters .filter-btn.active, .mode-buttons .filter-btn.active, .mode-buttons-row .filter-btn.active, .mode-buttons-cols .filter-btn.active, .filters .filter-btn.active, .inhabitant-action .filter-btn.active, .activity-filter-grid .filter-btn.active, .activity-filter-col .filter-btn.active, .activity-sub-filter .filter-btn.active { + background: #ffa300 !important; + border-color: #ffa300 !important; + color: #111 !important; + font-weight: bold !important; +} +.tribe-mode-buttons .create-button, .ui-toolbar--filters .create-button, .mode-buttons .create-button, .mode-buttons-row .create-button, .filters .create-button { + min-height: 0 !important; + height: auto !important; + font-size: .75em !important; + padding: 2px 10px !important; + border: 1px solid #ffcc00 !important; + border-radius: 12px !important; + background: #ffcc00 !important; + color: #111 !important; + font-weight: bold !important; + line-height: 1.5 !important; +} +.tribe-mode-buttons, .ui-toolbar--filters, .mode-buttons, .mode-buttons-row, .mode-buttons-cols, .filters, .inhabitant-action { + gap: 5px !important; + margin-top: 8px !important; +} + +.tags-header.module-header-line, .tags-header.activity-header-line { + display: flex !important; + flex-wrap: wrap !important; + align-items: center !important; + gap: 4px 10px !important; + padding: 8px 10px !important; +} +.module-header-line h2, .activity-header-line h2 { margin: 0 !important; flex: 0 0 auto !important; } +.module-header-line p, .activity-header-line p { margin: 0 !important; flex: 1 1 auto !important; min-width: 0 !important; font-size: .85em !important; } +.module-header-line .pm-exposition-chip, .module-header-line .pm-exposition-chip-link, .activity-header-line .pm-exposition-chip, .activity-header-line .pm-exposition-chip-link { + margin-left: auto !important; + flex: 0 0 auto !important; +} +.filter-box button, .filter-box .filter-box__button, .filter-box input[type="submit"] { + min-height: 0 !important; + height: 32px !important; + width: auto !important; + flex: 0 0 auto !important; + font-size: .85em !important; + padding: 0 14px !important; + border-radius: 4px !important; + border: 1px solid rgba(255, 255, 255, 0.12) !important; + background: rgba(255, 255, 255, 0.16) !important; + color: inherit !important; + font-weight: normal !important; +} + +.module-header-line + .shop-title-row, .activity-header-line + .shop-title-row { + display: flex !important; + justify-content: flex-end !important; + width: 100% !important; + margin: 0 !important; + padding: 0 10px !important; +} + +.feed-card .card-footer { + flex-direction: column !important; + align-items: stretch !important; + gap: 4px !important; + width: 100% !important; +} +.feed-card .card-footer .date-link, +.feed-card .card-footer .user-link { + display: block !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + min-height: 40px !important; + height: auto !important; + margin: 0 !important; + padding: 8px 12px !important; + font-size: 14px !important; + line-height: 24px !important; + text-align: center !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +.feed-card .card-footer .user-link > * { + display: inline !important; + max-width: 100% !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +.feed-card .card-footer .user-link { + font-weight: bold !important; +} +.update-download-link { + display: inline-block !important; + flex: 1 1 100% !important; + width: auto !important; + max-width: fit-content !important; + margin: 14px 0 12px 0 !important; + padding: 11px 18px !important; + background: #FFA500 !important; + border: 0 !important; + border-radius: 6px !important; + color: #000000 !important; + font-size: 14px !important; + font-weight: bold !important; + text-align: left !important; + text-decoration: none !important; + white-space: nowrap !important; +} +.update-banner { + justify-content: flex-start !important; + gap: 10px !important; + padding: 8px 12px !important; + text-align: left !important; +} +.update-banner:not(.welcome-banner) { + padding-right: 44px !important; +} +.update-banner .update-banner-icon { + flex: 0 0 24px !important; + width: 24px !important; + text-align: center !important; +} +.update-banner .update-banner-text { + flex: 1 1 auto !important; + min-width: 0 !important; +} +.update-banner .update-banner-link { + flex: 0 0 auto !important; + margin-left: auto !important; + text-align: right !important; + white-space: nowrap !important; +} +.update-banner .welcome-banner-close { + flex: 0 0 auto !important; + margin-left: 0 !important; +} +.update-banner.ai-suggestion-banner { + gap: 6px !important; +} +.update-banner.ai-suggestion-banner .update-banner-icon { + flex: 0 0 auto !important; + width: auto !important; +} +.update-banner.ai-suggestion-banner .update-banner-link { + flex: 1 1 auto !important; + margin-left: 0 !important; + min-width: 0 !important; + text-align: left !important; + overflow: hidden !important; + text-overflow: ellipsis !important; +} +.wallet-error-actions { + display: flex !important; + flex-wrap: wrap !important; + gap: 10px !important; + margin: 4px 0 !important; + padding: 2px !important; + border: 0 !important; + background: none !important; + box-shadow: none !important; +} +.wallet-error-actions .update-download-link { + flex: 0 0 auto !important; + margin: 0 !important; +} +.update-download-link.wallet-settings-link { + background: transparent !important; + border: 1px solid #FFA500 !important; + color: #FFA500 !important; + padding: 10px 17px !important; +} +.wallet-history, +.bank-info-table { + display: table !important; + width: 100% !important; + table-layout: fixed !important; + border-collapse: collapse !important; +} +.wallet-history th, +.wallet-history td, +.bank-info-table th, +.bank-info-table td { + padding: 8px 6px !important; + font-size: 13px !important; + letter-spacing: normal !important; + word-break: normal !important; + overflow-wrap: normal !important; + vertical-align: middle !important; +} +.wallet-history th:nth-child(1), .wallet-history td:nth-child(1) { display: none !important; } +.wallet-history th:nth-child(2), .wallet-history td:nth-child(2) { width: 33% !important; } +.wallet-history th:nth-child(3), .wallet-history td:nth-child(3) { width: 39% !important; } +.wallet-history th:nth-child(4), .wallet-history td:nth-child(4) { width: 28% !important; text-align: right !important; } +.wallet-history td.tcell-ellipsis { + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +.bank-summary { + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + background: none !important; + box-shadow: none !important; +} +.bank-info-table th:first-child, +.bank-info-table td:first-child { + width: 54% !important; + font-weight: bold !important; +} +.bank-info-table td:last-child { + text-align: right !important; +} +.bank-allocs, +.bank-allocs tbody, +.bank-allocs tr, +.bank-allocs td, +.bank-epochs, +.bank-epochs tbody, +.bank-epochs tr, +.bank-epochs td { + display: block !important; + width: 100% !important; + box-sizing: border-box !important; +} +.bank-allocs thead, +.bank-epochs thead { + display: none !important; +} +.bank-allocs tr, +.bank-epochs tr { + margin: 0 0 8px 0 !important; + padding: 10px !important; + border: 1px solid #444444 !important; + border-radius: 6px !important; + background: #222222 !important; +} +.bank-allocs td, +.bank-epochs td { + padding: 3px 0 !important; + border: 0 !important; + background: none !important; + font-size: 13px !important; + white-space: normal !important; + text-align: left !important; +} +.bank-allocs td:last-child, +.bank-epochs td:last-child { + padding: 8px 1px 1px 1px !important; + overflow: visible !important; +} +.bank-allocs td form, +.bank-epochs td form { + display: inline-block !important; +} +.wallet-history .pm-exposition-chip { + white-space: nowrap !important; + font-size: .78em !important; + padding: 2px 7px !important; + gap: 3px !important; +} +.wallet-qr-img { + display: block !important; + width: 100% !important; + max-width: 280px !important; + height: auto !important; + margin: 0 auto !important; +} +.wallet-section { + padding: 12px !important; +} +.wallet-section > h2 { + margin: 14px 0 6px 0 !important; + color: #FFA300 !important; + font-size: 15px !important; +} +.wallet-section > h2:first-child { + margin-top: 0 !important; +} +.wallet-section .wallet-address, +.wallet-section .div-center { + margin: 0 !important; + padding: 12px !important; +} +.wallet-section .wallet-address h2.element { + display: block !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + margin: 0 !important; + padding: 0 !important; + font-family: monospace !important; + font-size: 13px !important; + letter-spacing: 0 !important; + line-height: 1.4 !important; + text-align: center !important; + white-space: normal !important; + word-break: break-all !important; + overflow-wrap: anywhere !important; + overflow: visible !important; +} +.wallet-section .wallet-address { + overflow: visible !important; +} +.wallet-section .div-center > h2 { + margin: 0 !important; + font-size: 22px !important; + text-align: center !important; +} +.wallet-section .div-center + .div-center { + margin-top: 12px !important; + padding: 0 !important; + border: 0 !important; + background: none !important; + box-shadow: none !important; +} +.wallet-form-button-group-center { + display: flex !important; + gap: 8px !important; + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + background: none !important; + box-shadow: none !important; + box-sizing: border-box !important; +} +.wallet-form-button-group-center form { + flex: 1 1 0 !important; + min-width: 0 !important; + margin: 0 !important; +} +.wallet-form-button-group-center button { + width: 100% !important; +} +.bank-eco-chart-zoom-link { + display: block !important; + width: 100% !important; +} +.bank-eco-chart-lightbox .bank-eco-chart-canvas-zoom { + flex: 0 0 auto !important; + width: 96vh !important; + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + background: none !important; + box-shadow: none !important; + overflow: visible !important; + transform: rotate(90deg) !important; +} +.bank-eco-chart-lightbox .bank-eco-chart-svg { + display: block !important; + width: 96vh !important; + height: auto !important; + max-height: 96vw !important; +} +.bank-eco-chart-lightbox .lightbox-close { + top: 40px !important; + right: 12px !important; + z-index: 2 !important; +} +.bank-eco-chart-lightbox .bank-eco-chart-canvas-zoom { + position: relative !important; + z-index: 1 !important; +} +.subscription-actions .tribe-action-btn { + white-space: nowrap !important; + font-size: .8em !important; + padding: 4px 10px !important; + width: auto !important; + min-height: 0 !important; + height: 32px !important; +} +.subscription-actions .btn-pm { + min-height: 0 !important; + height: 32px !important; + width: 32px !important; + padding: 0 !important; + font-size: 14px !important; +} +.bank-addresses .delete-btn { + display: block !important; + width: 100% !important; + margin: 0 !important; +} +.bank-addresses, +.bank-addresses tbody, +.bank-addresses tr, +.bank-addresses td { + display: block !important; + width: 100% !important; + box-sizing: border-box !important; +} +.bank-addresses thead { + display: none !important; +} +.bank-addresses tr { + margin: 0 0 8px 0 !important; + padding: 10px !important; + border: 1px solid #444444 !important; + border-radius: 6px !important; + background: #222222 !important; +} +.bank-addresses td { + padding: 3px 0 !important; + border: 0 !important; + background: none !important; + font-size: 13px !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +.bank-addresses td:nth-child(2) { + font-family: monospace !important; +} +.bank-addresses td:last-child { + padding: 8px 1px 1px 1px !important; + overflow: visible !important; + white-space: normal !important; +} +.bank-addresses td a, +.bank-addresses td .user-link { + display: inline !important; + padding: 0 !important; + border: 0 !important; + background: none !important; + color: #FFA300 !important; + font-size: 13px !important; +} +.bank-addresses { + border: 0 !important; + background: none !important; +} +.bank-addresses .row-actions, +.bank-addresses .addr-del { + overflow: visible !important; + padding: 1px !important; +} +.bank-range-tabs { + gap: 6px !important; + justify-content: flex-start !important; + margin: 0 0 10px 0 !important; + padding: 0 !important; + border: 0 !important; + background: none !important; +} +.bank-range-tabs .filter-btn { + width: auto !important; + flex: 0 0 auto !important; + text-decoration: none !important; +} +.ubi-pub-summary .bank-info-table td { + white-space: normal !important; + overflow-wrap: anywhere !important; +} +.ubi-pub-summary .bank-info-table td.card-value { + width: auto !important; + text-align: right !important; +} +.ubi-pub-card, +.ubi-pub-summary, +.addr-card { + overflow: visible !important; + max-width: 100% !important; +} +.addr-cards { + display: block !important; +} +.addr-cards .addr-card { + margin: 0 0 12px 0 !important; +} +.pm-info-table, +.pm-info-table tbody, +.pm-info-table tr, +.pm-info-table td { + display: block !important; + width: 100% !important; + box-sizing: border-box !important; +} +.pm-info-table tr { + display: flex !important; + flex-wrap: wrap !important; + align-items: center !important; + gap: 6px !important; + border-bottom: 1px solid #333 !important; +} +.pm-info-table td { + border: 0 !important; + padding: 4px 6px !important; + font-size: 13px !important; + width: auto !important; +} +.pm-info-table td.card-label { + flex: 0 0 auto !important; + opacity: .8 !important; +} +.pm-info-table td.card-value { + flex: 1 1 auto !important; + min-width: 0 !important; + overflow-wrap: anywhere !important; +} +.pm-card .pm-title { + font-size: 15px !important; + margin: 8px 0 !important; +} +.pm-actions { + gap: 6px !important; +} +.pm-actions .pm-btn { + padding: 4px 8px !important; + font-size: 11px !important; +} +.bank-info-table td, +.tribe-info-table td, +.larp-info-table td, +.block-info-table td, +.pm-info-table td { + white-space: normal !important; + overflow-wrap: anywhere !important; + word-break: break-word !important; +} +.invites-used-table td:nth-child(1), +.invites-used-table td:nth-child(3) { + white-space: nowrap !important; + overflow-wrap: normal !important; + word-break: normal !important; +} +.bank-info-table td.card-value, +.tribe-info-table td.card-value, +.block-info-table td.card-value { + width: auto !important; +} +.main-column > *, +.main-column section, +.main-column .tags-header, +.main-column .filters { + max-width: 100% !important; + box-sizing: border-box !important; +} +.main-column img, +.main-column svg, +.main-column video, +.main-column canvas { + max-width: 100% !important; + height: auto !important; +} +.mobile-counters { + position: fixed !important; + right: 10px !important; + bottom: 10px !important; + display: flex !important; + gap: 6px !important; + z-index: 60 !important; +} +.mobile-counter { + display: inline-flex !important; + align-items: center !important; + gap: 4px !important; + background: #e74c3c !important; + color: #fff !important; + border-radius: 14px !important; + padding: 5px 10px !important; + font-weight: 700 !important; + font-size: 0.9rem !important; + text-decoration: none !important; + box-shadow: 0 2px 6px rgba(0,0,0,.4) !important; +} + +.activity-toolbar-row .inbox-exposition { + margin: 0 0 12px 0 !important; +} + +.hemicycle-canvas { + position: relative !important; +} +.hemi-zoom-link { + position: absolute !important; + inset: 0 !important; + display: block !important; + border: 0 !important; + background: transparent !important; + padding: 0 !important; + cursor: zoom-in !important; +} +.bank-eco-chart-lightbox .hemicycle-svg { + display: block !important; + width: 96vh !important; + height: auto !important; + max-height: 96vw !important; +} +.bank-eco-chart-lightbox .hemi-zoom-canvas circle { + r: 9 !important; +} + +.inhabitant-card .inhabitant-photo-details { + width: 150px !important; + height: 150px !important; + object-fit: cover !important; + border-radius: 50% !important; +} +.profile-sensors-box span span { + display: inline !important; +} +.profile-sensors-box .inhabitant-last-activity { + white-space: nowrap !important; +} + +.torrent-table, +.torrent-table tbody, +.torrent-table tr, +.torrent-table td { + display: block !important; + width: 100% !important; + box-sizing: border-box !important; +} +.torrent-table { + border: 0 !important; + background: transparent !important; +} +.torrent-table tr:first-child, +.torrent-table th { + display: none !important; +} +.torrent-table tr { + margin: 0 0 12px 0 !important; + padding: 12px !important; + border: 0 !important; + border-radius: 14px !important; + background: #1e1e1e !important; +} +.torrent-table td { + padding: 3px 0 !important; + border: 0 !important; + background: none !important; + font-size: 13px !important; + white-space: normal !important; + text-align: left !important; + overflow-wrap: anywhere !important; + height: auto !important; +} +.torrent-table td .user-link { + display: inline-block !important; + max-width: 100% !important; + overflow-wrap: anywhere !important; + white-space: normal !important; + font-size: 12px !important; +} +.torrent-table td.row-chips { + display: flex !important; + flex-wrap: wrap !important; + gap: 4px !important; + padding: 0 0 6px 0 !important; +} +.torrent-table td.row-title { + font-weight: 700 !important; +} +.torrent-table td.torrent-spread-cell .spread-form { + display: inline-flex !important; + vertical-align: middle !important; +} +.torrent-table td > form { + display: block !important; + margin: 6px 0 0 0 !important; +} +.torrent-table td > form .filter-btn { + width: 100% !important; +} +.torrent-table tr { + display: flex !important; + flex-direction: column !important; +} +.torrent-table td.row-actions { + order: -1 !important; + padding: 0 0 8px 0 !important; +} +.torrent-table td.torrent-spread-cell, +.file-table td.torrent-spread-cell { + display: flex !important; + align-items: center !important; + gap: 6px !important; +} +.torrent-table .torrent-card-actions, +.file-table .torrent-card-actions { + display: flex !important; + gap: 6px !important; + margin: 6px 0 0 0 !important; + padding: 0 !important; +} +.torrent-table .torrent-card-actions .filter-btn, +.file-table .torrent-card-actions .filter-btn { + flex: 1 1 0 !important; +} + +.stats-grid { + display: block !important; + width: 100% !important; +} +.stats-grid .stats-kpi { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: baseline !important; + justify-content: flex-start !important; + gap: 6px !important; + width: 100% !important; + box-sizing: border-box !important; + padding: 6px 0 !important; + margin: 0 !important; + border: 0 !important; + border-bottom: 1px solid #3a3a3a !important; + border-radius: 0 !important; + background: none !important; + text-align: left !important; +} +.stats-grid .stats-kpi:last-child { border-bottom: 0 !important; } +.stats-grid .stats-kpi .stats-kpi-label { + text-align: left !important; + width: auto !important; + flex: 0 1 auto !important; + font-size: 13px !important; + margin: 0 !important; + color: #FFD700 !important; + font-weight: bold !important; + letter-spacing: 1.5px !important; +} +.stats-grid .stats-kpi .stats-kpi-value { + text-align: left !important; + width: auto !important; + flex: 0 0 auto !important; + font-weight: 700 !important; + margin: 0 !important; +} +.stats-block .stats-toplist { + padding-left: 0 !important; + margin-left: 0 !important; + list-style: none !important; +} +.stats-block .stats-toplist li { + justify-content: flex-start !important; + gap: 8px !important; + padding: 5px 0 !important; +} +.stats-block .stats-toplist .stats-toplist-name { + min-width: 92px !important; + text-align: left !important; + flex: 0 0 auto !important; +} +.stats-block .stats-toplist .stats-bar-track { + height: 8px !important; + min-height: 0 !important; + flex: 1 1 auto !important; + align-self: center !important; +} +.stats-block .stats-toplist .stats-toplist-num { + margin-left: auto !important; + text-align: right !important; +} + +.mailing-archive-meta > .user-link, +.mailing-archive-meta > .forum-category { + flex: 0 0 100% !important; +} +.mailing-archive-meta > .mailing-counts { + flex: 0 1 auto !important; + width: auto !important; + font-size: .85em !important; +} + +.forum-thread-container { + padding: 0 !important; + border: 0 !important; + background: none !important; + margin: 0 0 12px 0 !important; +} +.forum-thread-container .forum-card { + padding: 10px !important; + margin: 0 0 10px 0 !important; + width: 100% !important; + box-sizing: border-box !important; +} +.forum-thread-container .forum-card.forum-thread-header { + flex-direction: row !important; + flex-wrap: nowrap !important; + align-items: stretch !important; + gap: 8px !important; +} +.forum-thread-header .root-vote-col { + flex: 0 0 52px !important; + width: 52px !important; + padding: 0 !important; + border: 0 !important; + margin: 0 !important; +} +.forum-thread-header .forum-score-box, +.forum-thread-header .forum-score-form, +.forum-comment .comment-vote-col .forum-score-box, +.forum-comment .comment-vote-col .forum-score-form { + flex-direction: column !important; + justify-content: flex-start !important; + align-items: center !important; + gap: 2px !important; + min-height: 0 !important; + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + width: 100% !important; +} +.forum-thread-header .score-btn, +.forum-comment .score-btn { + min-height: 30px !important; + padding: 2px 8px !important; + font-size: 14px !important; +} +.forum-thread-header .score-total, +.forum-comment .score-total { + font-size: 18px !important; + padding: 0 !important; + min-height: 0 !important; +} +.forum-thread-header .forum-main-col { + flex: 1 1 auto !important; + min-width: 0 !important; + padding: 0 !important; + border: 0 !important; + margin: 0 !important; + width: auto !important; +} +.forum-card .forum-header-row { + align-items: flex-start !important; + text-align: left !important; +} +.forum-card .forum-header-row, +.forum-card .forum-body, +.forum-card .forum-meta, +.forum-card .forum-footer { + padding: 0 !important; + border: 0 !important; + width: 100% !important; + box-sizing: border-box !important; +} +.forum-card .forum-meta { + flex-direction: row !important; + flex-wrap: wrap !important; + gap: 4px 14px !important; +} +.forum-card .forum-footer { + flex-wrap: wrap !important; + align-items: center !important; + gap: 6px !important; +} +.forum-card .forum-footer .date-link, +.forum-card .forum-footer .user-link { + font-size: .72em !important; + padding: 3px 8px !important; + text-align: left !important; + max-width: 100% !important; + overflow-wrap: anywhere !important; +} +.main-column .forum-comment .comment-body-row { + display: flex !important; + overflow-x: visible !important; + flex-direction: row !important; + flex-wrap: nowrap !important; + align-items: flex-start !important; + gap: 8px !important; +} +.main-column .forum-comment .comment-vote-col { + flex: 0 0 52px !important; + width: 52px !important; +} +.main-column .forum-comment .comment-text-col { + flex: 1 1 auto !important; + min-width: 0 !important; + width: auto !important; +} + +.forum-thread-header .content-actions { + width: 100% !important; + flex-wrap: wrap !important; +} +.content-actions .content-action-delete { + order: -1 !important; + margin-left: 0 !important; + margin-right: auto !important; +} + +.main-column .forum-comment { + padding: 8px !important; +} +.main-column .forum-comment .comment-header { + flex-wrap: wrap !important; + align-items: center !important; + gap: 6px !important; + padding: 0 0 6px 0 !important; + border: 0 !important; +} +.main-column .forum-comment .comment-header .date-link { + white-space: nowrap !important; + font-size: .72em !important; + padding: 3px 8px !important; + margin: 0 !important; +} +.main-column .forum-comment .comment-header .user-link { + font-size: .72em !important; + padding: 3px 8px !important; + flex: 0 1 auto !important; + min-width: 0 !important; + max-width: 100% !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + text-align: left !important; +} +.main-column .forum-comment .comment-header .comment-votes { + margin-left: 0 !important; + padding: 0 !important; + border: 0 !important; + display: flex !important; + gap: 14px !important; + flex: 0 0 100% !important; + width: 100% !important; + justify-content: flex-start !important; + margin: 2px 0 0 0 !important; +} +.main-column .forum-comment .comment-text-col > div { + padding: 0 !important; + border: 0 !important; + background: none !important; +} + +.main-column .forum-comment { + display: flex !important; + flex-direction: column !important; +} +.main-column .forum-comment .comment-header { + order: 99 !important; + padding: 16px 8px 10px 8px !important; + margin: 0 !important; +} +.main-column .forum-comment .comment-vote-col { + padding: 0 !important; + align-self: flex-start !important; +} + +.main-column .forum-thread-header .root-vote-col { + align-self: stretch !important; + display: flex !important; +} +.main-column .forum-thread-header .root-vote-col .forum-score-box { + height: 100% !important; + justify-content: center !important; +} + +.main-column .forum-card .forum-meta { + display: flex !important; + flex-wrap: wrap !important; + justify-content: flex-start !important; + gap: 4px 18px !important; +} +.main-column .forum-card .forum-meta > .forum-positive-votes, +.main-column .forum-card .forum-meta > .forum-negative-votes { + flex: 0 0 auto !important; + order: 0 !important; +} +.main-column .forum-card .forum-meta::before { + content: "" !important; + flex: 0 0 100% !important; + height: 0 !important; + order: 1 !important; +} +.main-column .forum-card .forum-meta > .forum-participants, +.main-column .forum-card .forum-meta > .forum-messages { + flex: 0 0 auto !important; + order: 2 !important; +} + +.activity-card-header { + align-items: center !important; + flex-wrap: nowrap !important; + gap: 8px !important; +} +.activity-card-header .card-chips-row { + flex: 0 1 auto !important; + min-width: 0 !important; +} +.activity-card-header .content-actions { + flex: 0 0 auto !important; + margin-bottom: 0 !important; + justify-content: flex-end !important; + gap: 4px !important; +} +.activity-card-header .content-actions .btn-singleview { + min-width: 30px !important; + width: 30px !important; + height: 30px !important; + min-height: 30px !important; + padding: 0 !important; + font-size: 13px !important; +} + +.btn-singleview.btn-content, +.main-column .content-actions .btn-singleview.btn-content { + background: #ffa300 !important; + color: #1e1f23 !important; + border: 1px solid #ffa300 !important; +} +.btn-singleview.btn-content:hover, +.main-column .content-actions .btn-singleview.btn-content:hover { + background: #FFD700 !important; + color: #1e1f23 !important; + border-color: #FFD700 !important; +} + +.tribe-details.pad-page { padding: 8px !important; gap: 10px !important; } +.pad-page > .tribe-side { padding: 8px 10px !important; gap: 6px 12px !important; } +.pad-page > .tribe-side .tribe-info-table { font-size: .85em !important; } +.pad-page > .tribe-side .tribe-side-actions form, .pad-page > .tribe-side .inline-form { display: inline-flex !important; margin: 0 !important; } +.pad-page > .tribe-main { padding: 0 !important; border: none !important; background: transparent !important; } +.pad-workspace { grid-template-columns: 1fr !important; gap: 10px !important; } +.pad-workspace-main form { margin: 0 !important; } +.pad-editor { min-height: 45vh !important; font-size: 16px !important; padding: 10px !important; } +.pad-document { min-height: 30vh !important; padding: 10px !important; } +.pad-workspace-side { flex-direction: column !important; } +.pad-history { max-height: 32vh !important; } +.pad-gutter { padding: 10px 6px 10px 6px !important; font-size: 16px !important; } +.pad-editor-wrap .pad-editor { min-height: 45vh !important; } +.pad-member-chip .user-link { max-width: 150px !important; } + +.file-table, +.file-table tbody, +.file-table tr, +.file-table td { + display: block !important; + width: 100% !important; + box-sizing: border-box !important; +} +.file-table { + border: 0 !important; + background: transparent !important; +} +.file-table tr:first-child, +.file-table th { + display: none !important; +} +.file-table tr { + margin: 0 0 12px 0 !important; + padding: 12px !important; + border: 0 !important; + border-radius: 14px !important; + background: #1e1e1e !important; +} +.file-table td { + padding: 3px 0 !important; + border: 0 !important; + background: none !important; + font-size: 13px !important; + white-space: normal !important; + text-align: left !important; + overflow-wrap: anywhere !important; + height: auto !important; +} +.file-table td .user-link { + display: inline-block !important; + max-width: 100% !important; + overflow-wrap: anywhere !important; + white-space: normal !important; + font-size: 12px !important; +} +.file-table td.row-chips { + display: flex !important; + flex-wrap: wrap !important; + gap: 4px !important; + padding: 0 0 6px 0 !important; +} +.file-table td.row-title { + font-weight: 700 !important; +} +.file-table td.torrent-spread-cell .spread-form { + display: inline-flex !important; + vertical-align: middle !important; +} +.file-table td > form { + display: block !important; + margin: 6px 0 0 0 !important; +} +.file-table td > form .filter-btn { + width: 100% !important; +} + +.file-table td[data-label]::before, +.torrent-table td[data-label]::before { + content: attr(data-label) ": " !important; + color: #ffa300 !important; + font-weight: 600 !important; +} +.file-table td:empty, +.torrent-table td:empty { + display: none !important; +} +.file-table td .user-link, +.torrent-table td .user-link { + font-size: 10px !important; + padding: 2px 6px !important; + line-height: 1.25 !important; +} + +body, +.main-content, +.main-column, +.main-column section, +article { + background: #121212 !important; + border: 0 !important; + box-shadow: none !important; +} +.main-column div, +.main-column section, +.main-column article { + border: 0 !important; + box-shadow: none !important; +} +.main-column section, +.main-column article, +.main-column :is(div:not([class]), [class$="-list"], [class*="-list "], [class$="-grid"], [class*="-grid "], [class$="-wrap"]:not(.pad-editor-wrap), [class*="-wrap "], [class$="-section"], [class*="-section "], [class$="-container"], [class*="-container "], [class$="-header"], [class*="-header "], [class$="-body"], [class*="-body "], [class$="-row"], [class*="-row "], .tribe-side, .tribe-main, .filters, .card-field, .card-engage, .card-footer) { + background-color: transparent !important; +} +.main-column :is([class$="-card"], [class*="-card "], .card) { + background: #1e1e1e !important; + border: 0 !important; + border-radius: 14px !important; + padding: 12px !important; + margin: 0 0 12px 0 !important; + box-shadow: none !important; +} +.main-column :is([class$="-card"], [class*="-card "], .card) :is([class$="-card"], [class*="-card "], .card, div:not([class]), [class^="card-"], [class*=" card-"], [class$="-body"], [class*="-body "], [class$="-section"], [class*="-section "], [class$="-row"], [class*="-row "], [class$="-header"], [class*="-header "]) { + background: transparent !important; + border-radius: 0 !important; + margin-left: 0 !important; + margin-right: 0 !important; + padding-left: 0 !important; + padding-right: 0 !important; +} +.main-column :is([class$="-card"], [class*="-card "], .card).own-content { + box-shadow: inset 3px 0 0 #ffa300 !important; +} + +.header { + background: #121212 !important; + border: 0 !important; + box-shadow: none !important; +} +.oasis-mobile-menus { + background: transparent !important; + border: 0 !important; + padding: 0 !important; + margin: 0 !important; +} +.oasis-menu-btn { + border: 0 !important; + background: #1e1e1e !important; + border-radius: 14px !important; + min-height: 44px !important; + padding: 6px 18px !important; +} +body > .header { + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: center !important; + gap: 8px 6px !important; + padding: 8px 8px 4px !important; +} +.header .top-bar-left a.logo-icon { + order: 1 !important; + flex: 0 0 auto !important; + margin: 0 !important; + padding: 0 !important; +} +.header .top-bar-left a.logo-icon img.logo-icon { + width: 44px !important; + height: 44px !important; + margin: 0 !important; +} +.oasis-mobile-menus { + flex: 1 1 auto !important; + justify-content: flex-end !important; + flex-wrap: nowrap !important; +} +.header .top-bar-left nav, +.header .top-bar-right nav { + flex: 1 1 calc(50% - 4px) !important; + width: auto !important; + min-width: 0 !important; +} +.header .top-bar-left nav ul, +.header .top-bar-right nav ul { + gap: 4px !important; +} +.header .top-bar-left nav ul li a, +.header .top-bar-right nav ul li a { + position: relative !important; + min-height: 50px !important; + padding: 0 !important; + border: 0 !important; + border-radius: 14px !important; + background: #1e1e1e !important; + font-size: 0 !important; + gap: 0 !important; +} +.header .top-bar-left nav ul li a .nav-text, +.header .top-bar-right nav ul li a .nav-text { + display: none !important; +} +.header .top-bar-left nav ul li a .emoji, +.header .top-bar-right nav ul li a .emoji { + font-size: 1.6rem !important; + line-height: 1 !important; + margin: 0 !important; +} +.header .top-bar-left nav ul li a .inbox-badge { + position: absolute !important; + top: 3px !important; + right: 3px !important; + margin: 0 !important; + font-size: 0.66rem !important; + line-height: 1.35 !important; + padding: 0 4px !important; + min-width: 15px !important; +} +.oasis-footer { + border-top: 0 !important; + padding: 0 10px calc(24px + env(safe-area-inset-bottom, 0px)) !important; +} +.oasis-footer-center { + background: #1e1e1e !important; + border: 0 !important; + border-radius: 14px !important; + box-shadow: none !important; + padding: 14px 18px !important; +} +.oasis-footer .oasis-footer-center { + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; +} +.oasis-footer .oasis-footer-center input.oasis-id-copy[readonly] { + display: block !important; + width: 100% !important; + margin: 6px 0 6px 0 !important; + padding: 9px 6px !important; + border: 0 !important; + border-radius: 10px !important; + background: #2a2a2a !important; + color: #FFD700 !important; + font-family: monospace !important; + font-size: clamp(8px, 2.5vw, 11px) !important; + letter-spacing: 0 !important; + text-align: center !important; + text-overflow: clip !important; +} +.oasis-footer .oasis-id-copy + br, +.oasis-footer .oasis-footer-logo-link + br { + display: none !important; +} +.content-actions { + flex-wrap: wrap !important; + justify-content: flex-end !important; + gap: 4px !important; + min-width: 0 !important; +} +.main-column .content-actions .btn-singleview { + background: #2a2a2a !important; +} +.main-column .content-actions .btn-singleview.btn-pin-on { + background: #ffa300 !important; + color: #000 !important; +} +.activity-card-header { + flex-wrap: wrap !important; +} +.activity-card-header .content-actions { + flex: 1 1 auto !important; +} +.main-column .content-actions .btn-singleview, +.main-column .activity-card-header .content-actions .btn-singleview { + width: 32px !important; + min-width: 32px !important; + height: 32px !important; + min-height: 32px !important; + padding: 0 !important; + margin: 0 !important; + font-size: 14px !important; + line-height: 1 !important; + border-radius: 50% !important; +} +.main-column .content-actions .spread-form .btn-singleview { + width: auto !important; + padding: 0 10px !important; + border-radius: 16px !important; +} +.main-column .content-actions .content-action-form, +.main-column .content-actions .spread-form { + margin: 0 !important; + padding: 0 !important; + display: inline-flex !important; +} +.main-column .mailing-archive { + padding: 0 !important; + margin: 0 !important; + list-style: none !important; +} +.main-column .mailing-archive-item { + background: #1e1e1e !important; + border: 0 !important; + border-radius: 14px !important; + padding: 12px !important; + margin: 0 0 12px 0 !important; +} +.main-column .mailing-archive-head { + display: flex !important; + flex-direction: column !important; + flex-wrap: nowrap !important; + align-items: stretch !important; + gap: 8px !important; + margin: 0 !important; +} +.main-column .mailing-archive-head > .content-actions { + order: -1 !important; + align-self: stretch !important; + justify-content: flex-end !important; +} +.main-column .mailing-archive-meta > a.user-link:first-child { + background: transparent !important; + color: #FFD700 !important; + border: 0 !important; + padding: 0 !important; + font-size: 1.15rem !important; + font-weight: 700 !important; + text-align: left !important; + text-decoration: underline !important; + display: block !important; +} +.main-column .mailing-archive-meta > .forum-category { + background: transparent !important; + padding: 0 !important; +} +.main-column .mailing-archive-meta .tribe-action-btn { + background: #ffa300 !important; + color: #111 !important; + border: 0 !important; + border-radius: 10px !important; + padding: 8px 16px !important; + min-height: 0 !important; + font-size: 0.85rem !important; + font-weight: 700 !important; +} +.main-column section > div:not(:is([class$="-card"], [class*="-card "], .card, .tribe-details)) { + padding-left: 0 !important; + padding-right: 0 !important; +} +.main-column .tribe-details:not(.profile-layout) { + background: #1e1e1e !important; + border: 0 !important; + border-radius: 14px !important; + padding: 12px !important; + margin: 0 0 12px 0 !important; + gap: 10px !important; +} +.main-column .tribe-details .tribe-side, +.main-column .tribe-details .tribe-main { + background: transparent !important; + border: 0 !important; + padding: 0 !important; + margin: 0 !important; + box-shadow: none !important; +} +.main-column .card-tags { + background: transparent !important; + padding: 0 !important; + margin: 6px 0 !important; +} +.main-column .tribe-info-table, +.main-column .bank-info-table, +.main-column .block-info-table { + border: 0 !important; + border-collapse: collapse !important; + background: transparent !important; + margin: 0 0 10px 0 !important; +} +.main-column .tribe-info-table tr, +.main-column .tribe-info-table td { + border: 0 !important; + background: transparent !important; +} +.main-column .tribe-info-table td.tribe-info-label { + padding: 8px 0 2px 0 !important; + font-size: 0.75rem !important; + color: #ffa300 !important; +} +.main-column .tribe-info-table td.tribe-info-value { + padding: 0 0 6px 0 !important; + border-bottom: 1px solid #2a2a2a !important; +} +.main-column .card-engage { + grid-template-columns: 1fr 1fr !important; + gap: 8px !important; +} +.main-column .card-engage .opinions-summary, +.main-column .card-engage .comments-summary { + border: 0 !important; + border-radius: 12px !important; + background: #2a2a2a !important; + padding: 8px 10px !important; + min-height: 38px !important; +} +.main-column .card-footer { + border: 0 !important; +} +.main-column .card-title-link { + color: inherit !important; + text-decoration: underline !important; +} +#nav-left-toggle:checked ~ .main-content .sidebar-left, +#nav-right-toggle:checked ~ .main-content .sidebar-right { + background: transparent !important; + border: 0 !important; + padding: 0 4px !important; + margin: 0 0 8px 0 !important; +} +.main-content .sidebar-left nav > ul, +.main-content .sidebar-right nav > ul { + gap: 2px !important; +} +.main-content .oasis-nav-group { + margin: 0 0 6px 0 !important; + padding: 0 !important; +} +.main-content .oasis-nav-header { + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + margin: 0 !important; + padding: 10px 4px 8px !important; + font-size: 0.78rem !important; + color: #ffa300 !important; +} +.main-content .sidebar-left .oasis-nav-list, +.main-content .sidebar-right .oasis-nav-list { + display: grid !important; + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; + gap: 8px !important; + margin: 0 !important; + padding: 0 !important; +} +.main-content .oasis-nav-header + .oasis-nav-list { + margin-top: 0 !important; +} +.main-content .oasis-nav-list li { + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + background: transparent !important; + min-width: 0 !important; +} +.main-content .oasis-nav-list li a { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + gap: 6px !important; + min-height: 74px !important; + height: 100% !important; + padding: 8px 4px !important; + margin: 0 !important; + border: 0 !important; + border-radius: 14px !important; + background: #1e1e1e !important; + color: #FFD700 !important; + text-align: center !important; + font-size: 0.72rem !important; + font-weight: 600 !important; + opacity: 1 !important; + text-decoration: none !important; +} +.main-content .oasis-nav-list li a .emoji { + width: auto !important; + font-size: 1.5rem !important; + line-height: 1 !important; +} +.main-content .oasis-nav-list li a .nav-text { + line-height: 1.15 !important; + overflow-wrap: anywhere !important; +} +.main-content .oasis-nav-header { + background: #1a1a1a !important; + border-radius: 12px !important; + padding: 12px !important; + margin: 0 0 6px 0 !important; +} +.update-banner { + border: 0 !important; + border-radius: 14px !important; + margin: 4px 8px 8px !important; +} +.main-column .card-meta-row .opinions-summary, +.main-column .card-meta-row .comments-summary-link, +.main-column .card-meta-row .comments-summary { + border: 0 !important; + border-radius: 10px !important; + background: #2a2a2a !important; +} +.wallet-line { + flex-wrap: nowrap !important; + justify-content: center !important; + max-width: 100% !important; + border: 0 !important; + background: transparent !important; +} +.wallet-line .wallet-line-head { + flex: 0 0 auto !important; +} +.wallet-line .wallet-address, +.wallet-line .wallet-line-link { + min-width: 0 !important; + font-size: 11px !important; + overflow-wrap: anywhere !important; + word-break: break-all !important; +} +.main-column .invites-pubs-table, +.main-column .invites-pubs-table tbody, +.main-column .invites-pubs-table tr, +.main-column .invites-pubs-table td { + display: block !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + border: 0 !important; + background: transparent !important; +} +.main-column .invites-pubs-table { + overflow: visible !important; + margin: 0 0 10px 0 !important; +} +.main-column .invites-pubs-table tr:first-child { + display: none !important; +} +.main-column .invites-pubs-table tr { + background: #1e1e1e !important; + border-radius: 14px !important; + padding: 12px !important; + margin: 0 0 10px 0 !important; +} +.main-column .invites-pubs-table td { + padding: 0 !important; + text-align: left !important; +} +.main-column .invites-pubs-table td:nth-child(1), +.main-column .invites-pubs-table td:nth-child(2) { + display: inline !important; + width: auto !important; + font-weight: 700 !important; + font-size: 1rem !important; + color: #FFD700 !important; + overflow-wrap: anywhere !important; +} +.main-column .invites-pubs-table td:nth-child(2)::before { + content: ":" !important; +} +.main-column .invites-pubs-table td:nth-child(3) { + margin: 8px 0 0 0 !important; +} +.main-column .invites-pubs-table td:nth-child(3) .user-link { + display: block !important; + font-size: 10px !important; + text-align: left !important; + word-break: break-all !important; + padding: 6px 8px !important; +} +.main-column .invites-pubs-table td:nth-child(4) { + margin: 8px 0 0 0 !important; +} +.main-column .invites-pubs-table td:nth-child(4) button { + width: 100% !important; +} +.main-column .table-wrap { + overflow: visible !important; +} +.main-column .table-wrap table.table, +.main-column .table-wrap table.table tbody, +.main-column .table-wrap table.table tr, +.main-column .table-wrap table.table td { + display: block !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + border: 0 !important; + background: transparent !important; + text-align: left !important; +} +.main-column .table-wrap table.table thead { + display: none !important; +} +.main-column .table-wrap table.table tr { + background: #1e1e1e !important; + border-radius: 14px !important; + padding: 12px !important; + margin: 0 0 10px 0 !important; +} +.main-column .table-wrap table.table td { + padding: 3px 0 !important; + font-size: 13px !important; + overflow-wrap: anywhere !important; +} +.main-column .table-wrap table.table td[data-label]::before { + content: attr(data-label) ": " !important; + color: #ffa300 !important; + font-weight: 600 !important; +} +.main-column .table-wrap table.table td .user-link { + display: inline-block !important; + max-width: 100% !important; + font-size: 10px !important; + padding: 2px 6px !important; + word-break: break-all !important; +} +.main-column .table-wrap table.table td.table-row-action { + padding: 8px 0 0 0 !important; +} +.main-column .table-wrap table.table td.table-row-action button { + width: 100% !important; +} +.main-column .table-wrap table.table td.table-row-action .link-button { + background: #ffa300 !important; + color: #111 !important; + border: 0 !important; + border-radius: 10px !important; + font-weight: 700 !important; + min-height: 42px !important; +} +.main-column .housing-grid, +.main-column .shop-products-grid { + display: grid !important; + grid-template-columns: minmax(0, 1fr) !important; + gap: 12px !important; + margin: 12px 0 !important; +} +.main-column .tribe-details .shop-product-card { + background: #262626 !important; + margin: 0 !important; + min-width: 0 !important; +} +.main-column :is([class$="-card"], [class*="-card "], .card) > * { + max-width: 100% !important; + min-width: 0 !important; + box-sizing: border-box !important; +} +.main-column :is(div:not([class]), [class$="-list"], [class*="-list "], [class$="-grid"], [class*="-grid "], [class$="-wrap"], [class*="-wrap "], [class$="-section"]:not(.card-section), [class$="-container"], .tribe-main, .tribe-side) { + padding-left: 0 !important; + padding-right: 0 !important; +} +.main-column .event-card-body .time-chip { + display: inline-flex !important; + margin: 6px 0 0 0 !important; +} +.main-column .pledge-box button.btn { + display: block !important; + width: 100% !important; + padding: 10px 16px !important; + font-size: 1.05em !important; + font-weight: bold !important; + background: #ffa300 !important; + color: #111 !important; + border: none !important; + border-radius: 8px !important; + text-align: center !important; + margin: 8px auto 4px auto !important; + cursor: pointer !important; + text-transform: uppercase !important; +} +.main-column .calendar-nav { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + gap: 12px !important; + padding: 0 !important; + margin: 8px 0 !important; +} +.main-column .calendar-nav .filter-btn { + min-width: 52px !important; + font-size: 20px !important; + padding: 4px 14px !important; +} +.main-column .calendar-nav .tribe-info-label { + flex: 1 1 auto !important; + text-align: center !important; + font-size: 1rem !important; +} +.main-column .calendar-grid { + grid-template-columns: repeat(7, minmax(0, 1fr)) !important; + gap: 4px !important; + padding: 0 !important; +} +.main-column .calendar-grid .calendar-day-header { + padding: 6px 0 !important; + text-align: center !important; + font-size: 0.85rem !important; + border: 0 !important; + background: transparent !important; +} +.main-column .calendar-grid .calendar-day { + padding: 8px 0 !important; + margin: 0 !important; + text-align: center !important; + border-radius: 8px !important; + background: #262626 !important; + min-width: 0 !important; +} +.main-column .calendar-grid .calendar-day-empty { + background: transparent !important; +} +.main-column .calendar-grid .calendar-day-marked { + background: #ffa300 !important; +} +.main-column .calendar-grid .calendar-day-marked a { + color: #111 !important; + font-weight: 700 !important; +} +.main-column .graphos-statsbar { + margin: 0 0 8px 0 !important; +} +.main-column .graphos-canvas .graphos-zoom-btn { + min-height: 36px !important; + padding: 6px 14px !important; + font-size: 13px !important; +} +.graphos-lightbox .graphos-canvas-zoom { + width: 100vw !important; + height: 100vh !important; + overflow: auto !important; + border: 0 !important; + border-radius: 0 !important; + padding: 48px 0 0 0 !important; + background: #0d0d0d !important; +} +.graphos-lightbox .graphos-svg { + width: 200vw !important; + max-width: none !important; + max-height: none !important; + height: auto !important; + min-height: 0 !important; +} +.graphos-lightbox .lightbox-close { + position: fixed !important; + top: 8px !important; + right: 14px !important; + z-index: 10001 !important; + font-size: 32px !important; + color: #FFD700 !important; +} +.main-column .pad-document { + padding: 8px 8px 8px 0 !important; +} +.main-column .pad-lines .pad-line-num { + min-width: 0 !important; + width: 1% !important; + padding: 0 6px 0 4px !important; + background: #f1f1f1 !important; + border-right: 1px solid #ddd !important; +} +.main-column .pad-lines .pad-line-num a, +.main-column .pad-lines .pad-line-num a:not([class]) { + color: #888 !important; + text-decoration: none !important; + font-size: 13px !important; +} +.main-column .pad-lines tr:target .pad-line-num a { + color: #c77700 !important; + font-weight: 700 !important; +} +.main-column .pad-lines .pad-line-text { + padding-left: 8px !important; + color: #111 !important; +} +.main-column .pad-editor-wrap { + background: #fff !important; + border: 1px solid #ccc !important; + border-radius: 10px !important; +} +.main-column .pad-editor-wrap .pad-gutter { + padding: 10px 6px 10px 4px !important; + background: #f1f1f1 !important; + color: #888 !important; + font-size: 16px !important; + line-height: 1.45 !important; + border-right: 1px solid #ddd !important; +} +.main-column .pad-editor-wrap .pad-editor { + line-height: 1.45 !important; + padding: 10px 8px !important; + color: #111 !important; + background: #fff !important; + border: 0 !important; +} +.main-column .pad-history-item { + flex-wrap: nowrap !important; + gap: 6px !important; + min-width: 0 !important; +} +.main-column .pad-history-item .pad-version-link { + flex: 0 0 auto !important; + font-size: 11px !important; + letter-spacing: -0.2px !important; +} +.main-column .pad-history-item .user-link { + flex: 0 1 auto !important; + min-width: 0 !important; + max-width: 100% !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + font-size: 11px !important; + padding: 3px 8px !important; +} +.main-column .backup-kit-line { + display: flex !important; + flex-direction: column !important; + gap: 2px !important; + margin: 0 0 8px 0 !important; +} +.main-column .backup-kit-id { + font-family: monospace !important; + font-size: 9.5px !important; + letter-spacing: -0.2px !important; + white-space: nowrap !important; + overflow-wrap: normal !important; + word-break: normal !important; +} +.main-column .backup-kit-secret { + font-size: 11px !important; + line-height: 1.4 !important; + padding: 10px !important; +} +.main-column .melody-regen-form { + display: flex !important; + flex-wrap: nowrap !important; + gap: 12px !important; + align-items: stretch !important; + margin: 10px 0 !important; +} +.main-column .melody-regen-form > form, +.main-column .melody-regen-form > a { + flex: 1 1 0 !important; + margin: 0 !important; +} +.main-column .melody-regen-form > form .filter-btn, +.main-column .melody-regen-form > a.filter-btn { + width: 100% !important; + height: 46px !important; + min-height: 46px !important; + margin: 0 !important; + box-sizing: border-box !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + text-decoration: none !important; +} +.main-column .confirm-actions, +.main-column .error-page-actions { + display: flex !important; + flex-wrap: wrap !important; + gap: 10px !important; + align-items: stretch !important; + margin: 12px 0 0 0 !important; +} +.main-column .confirm-actions > input[type="hidden"] { + display: none !important; +} +.main-column .confirm-actions > .create-button, +.main-column .confirm-actions > .filter-btn, +.main-column .error-page-actions > .filter-btn { + flex: 1 1 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + min-height: 46px !important; + margin: 0 !important; + padding: 10px 14px !important; + background: #ffa300 !important; + color: #111 !important; + border: 0 !important; + border-radius: 10px !important; + font-size: 15px !important; + font-weight: 700 !important; + text-decoration: none !important; + text-transform: uppercase !important; +} +.main-column .card-section table, +.main-column [class*="-card"] table:not(.pad-lines) { + border: 0 !important; + border-collapse: collapse !important; + background: transparent !important; +} +.main-column [class*="-card"] table:not(.pad-lines) th, +.main-column [class*="-card"] table:not(.pad-lines) td { + border: 0 !important; + border-bottom: 1px solid #2a2a2a !important; + background: transparent !important; +} +.main-column .search-filters-row { + padding: 0 !important; +} +.main-column .search-filters-table, +.main-column .search-filters-table tr, +.main-column .search-filters-table td { + border: 0 !important; + background: transparent !important; +} +.main-column .search-filters-table td { + padding: 0 0 6px 0 !important; +} +.header .top-bar-left nav, +.header .top-bar-right nav { + background: #161616 !important; + border-top: 1px solid #2a2a2a !important; +} +.header .top-bar-left nav ul li a, +.header .top-bar-right nav ul li a { + background: transparent !important; + min-height: 46px !important; + border-radius: 12px !important; +} +.header .top-bar-left nav ul li a[aria-current="page"], +.header .top-bar-right nav ul li a[aria-current="page"] { + background: rgba(255, 215, 0, 0.16) !important; +} +.main-content .oasis-nav-group > .oasis-nav-header { + background: #1e1e1e !important; + border-radius: 14px !important; + color: #ffa300 !important; +} +.main-content .oasis-nav-toggle:checked + .oasis-nav-header { + background: rgba(255, 215, 0, 0.18) !important; + color: #FFD700 !important; +} +.main-content .oasis-nav-group > .oasis-nav-list { + background: #181818 !important; + border-radius: 14px !important; + padding: 8px !important; +} +.main-column .content-actions .content-action-form.content-action-delete { + order: -1 !important; + margin: 0 auto 0 0 !important; +} +.main-column .inhabitants-grid { + display: grid !important; + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + gap: 10px !important; +} +.main-column .inhabitants-grid > .inhabitants-card { + margin: 0 !important; + padding: 10px 8px !important; + min-width: 0 !important; +} +.main-column .inhabitants-grid .content-actions { + justify-content: center !important; + gap: 3px !important; +} +.main-column .inhabitants-grid .content-actions .btn-singleview { + width: 28px !important; + min-width: 28px !important; + height: 28px !important; + min-height: 28px !important; + font-size: 12px !important; +} +.main-column .inhabitants-grid .inhabitant-card, +.main-column .inhabitants-grid .inhabitant-left { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + gap: 6px !important; + width: 100% !important; + min-width: 0 !important; + margin: 0 !important; + padding: 0 !important; +} +.main-column .inhabitants-grid .inhabitant-left > a:first-of-type, +.main-column .inhabitants-grid .inhabitant-photo-details { + width: 92px !important; + height: 92px !important; + min-width: 0 !important; + margin: 4px auto 0 !important; +} +.main-column .inhabitants-grid .inhabitant-left-name { + margin: 0 !important; + max-width: 100% !important; +} +.main-column .inhabitants-grid .inhabitant-left-name .user-link { + display: block !important; + max-width: 100% !important; + font-size: 11px !important; + padding: 4px 8px !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; +} +.main-column .inhabitants-grid .profile-sensors-box { + display: flex !important; + flex-direction: column !important; + align-items: stretch !important; + gap: 4px !important; + width: 100% !important; + margin: 0 !important; + padding: 0 !important; +} +.main-column .inhabitants-grid .profile-sensors-box > span { + display: block !important; + margin: 0 !important; + padding: 3px 4px !important; + font-size: 10.5px !important; + line-height: 1.3 !important; + text-align: center !important; + white-space: normal !important; + border-radius: 8px !important; +} +.main-column .inhabitants-grid .wallet-line { + justify-content: center !important; + gap: 4px !important; + padding: 0 !important; + min-width: 0 !important; +} +.main-column .inhabitants-grid .wallet-line .wallet-address { + font-size: 9px !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + word-break: normal !important; + overflow-wrap: normal !important; + min-width: 0 !important; + max-width: 100% !important; +} +.main-column .inhabitants-grid .phone-number-line .pm-exposition-chip { + font-size: 9px !important; + padding: 1px 6px !important; +} +.main-column .inhabitants-grid .inhabitant-left-name .status.you { + font-size: 11px !important; + display: inline-block !important; + line-height: 1.3 !important; +} +.main-column .inhabitants-grid .inhabitant-card { + padding-bottom: 12px !important; +} +:where(.main-column) div { + margin-bottom: 8px; + padding: 0; +} +:where(.main-column) p { + margin-top: 6px; + margin-bottom: 6px; +} +:where(.main-column) section { + margin-bottom: 10px; +} +.main-column :is(form, section, div) > br { + display: none !important; +} +.main-column .card-field > a.card-value.user-link { + display: inline-block !important; + max-width: 100% !important; + background: #FFA500 !important; + color: #000 !important; + border: 0 !important; + border-radius: 6px !important; + padding: 6px 12px !important; + font-size: 14px !important; + font-weight: 700 !important; + line-height: 1.3 !important; + white-space: normal !important; + text-decoration: none !important; +} +.main-column .content-actions .btn-singleview.btn-spread-on { + background: #ffa300 !important; + color: #000 !important; +} +.main-column .engage-on, +.main-column .card-engage .engage-on, +.main-column .card-meta-row .engage-on { + background: #ff9900 !important; + color: #000 !important; + border: 1px solid #e68a00 !important; +} +.main-column .engage-on *, +.main-column .content-actions .btn-singleview.btn-spread-on * { + color: inherit !important; +} +.main-column .tribe-card-members { + margin: 8px 0 !important; + padding: 0 !important; + text-align: center !important; +} +.main-column .tribe-side-actions, +.main-column .tribe-card-members + form { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + gap: 8px !important; + margin: 4px 0 10px !important; +} +.main-column .card-tags { + display: flex !important; + flex-wrap: wrap !important; + gap: 6px 14px !important; + margin: 6px 0 !important; +} +.main-column .doc-export-actions { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + gap: 8px !important; + margin: 6px 0 !important; +} +.main-column .doc-export-actions form { + margin: 0 !important; +} +.main-column .calendar-details { + display: flex !important; + flex-direction: column !important; +} +.main-column .calendar-details > .tribe-side, +.main-column .calendar-details > .tribe-main { + display: contents !important; +} +.main-column .calendar-details .card-header.activity-card-header { + order: -1 !important; + margin: 0 !important; +} +.main-column .calendar-details .calendar-nav { + margin: 8px 0 4px !important; +} +.main-column .calendar-details .calendar-grid { + margin: 4px 0 8px !important; +} +.main-column .tag-table { + display: table !important; + width: 100% !important; + table-layout: auto !important; +} +.main-column .tag-table th:last-child, +.main-column .tag-table td:last-child { + width: 1% !important; + white-space: nowrap !important; + text-align: right !important; + padding-right: 10px !important; +} +.main-column .tag-table th:first-child, +.main-column .tag-table td:first-child { + width: auto !important; + overflow-wrap: anywhere !important; +} +.main-column .peers-conn-actions { + display: flex !important; + flex-wrap: nowrap !important; + gap: 6px !important; + margin: 8px 0 !important; +} +.main-column .peers-conn-actions form { + flex: 1 1 0 !important; + min-width: 0 !important; + margin: 0 !important; + display: flex !important; +} +.main-column .peers-conn-actions .filter-btn { + width: 100% !important; + min-width: 0 !important; + min-height: 40px !important; + padding: 6px 4px !important; + font-size: 12px !important; + line-height: 1.15 !important; + white-space: normal !important; + overflow-wrap: normal !important; + border-radius: 10px !important; + background: #ffa300 !important; + color: #111 !important; + border: 0 !important; +} +.main-column .peers-import-form { + display: flex !important; + flex-direction: column !important; + gap: 8px !important; +} +.main-column .peers-import-form .filter-btn { + align-self: flex-start !important; +} +.main-column .graphos-canvas { + margin: 0 !important; + padding: 0 !important; +} +.main-column .graphos-canvas .graphos-svg { + min-height: 0 !important; + height: auto !important; +} +.main-column .graphos-statsbar { + margin: 4px 0 6px 0 !important; +} +.main-column .graphos-fed-table, +.main-column .graphos-fed-table tbody, +.main-column .graphos-fed-table tr, +.main-column .graphos-fed-table td { + display: block !important; + width: 100% !important; + border: 0 !important; + background: transparent !important; + text-align: left !important; +} +.main-column .graphos-fed-table tr:first-child { + display: none !important; +} +.main-column .graphos-fed-table tr { + background: #1e1e1e !important; + border-radius: 14px !important; + padding: 10px 12px !important; + margin: 0 0 10px 0 !important; +} +.main-column .graphos-fed-table td { + padding: 2px 0 !important; + font-size: 13px !important; +} +.main-column .graphos-fed-table td[data-label]::before { + content: attr(data-label) ": " !important; + color: #ffa300 !important; + font-weight: 600 !important; +} +.main-column .card-field:has(> .card-label) { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: baseline !important; + gap: 2px 6px !important; +} +.main-column .card-field:has(> .card-label) > .card-label, +.main-column .card-field:has(> .card-label) > .card-value { + display: inline !important; + width: auto !important; + margin: 0 !important; +} +.main-column .room-live-panel .room-live-head { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + gap: 8px !important; + text-align: center !important; +} +.main-column .room-live-panel .room-live-head-main { + width: 100% !important; + text-align: center !important; +} +.main-column .tribe-main:has(> .room-live-panel) { + display: flex !important; + flex-direction: column !important; +} +.main-column .tribe-main:has(> .room-live-panel) > .pad-members-details { + order: 1 !important; + text-align: center !important; +} +.main-column .tribe-main:has(> .room-live-panel) > .pm-form-error-msg { + order: -2 !important; +} +.torrent-table td.torrent-spread-cell, +.file-table td.torrent-spread-cell { + display: flex !important; + align-items: center !important; + gap: 8px !important; + min-height: 0 !important; + padding: 0 !important; +} +.torrent-table td.torrent-spread-cell > form, +.file-table td.torrent-spread-cell > form { + display: inline-flex !important; + margin: 0 !important; +} +.torrent-table td.torrent-spread-cell .btn-singleview, +.file-table td.torrent-spread-cell .btn-singleview { + width: auto !important; + min-width: 0 !important; + height: 20px !important; + min-height: 20px !important; + padding: 0 !important; + justify-content: flex-start !important; + font-size: 15px !important; +} +.main-column .tribe-side, +.main-column .tribe-main { + gap: 8px !important; +} +.main-column .tribe-side > .tribe-card-members, +.main-column .tribe-main > .tribe-card-members { + margin: 0 !important; +} +.main-column .tribe-side > .tribe-info-table, +.main-column .tribe-main > .tribe-info-table { + margin-bottom: 0 !important; +} +.main-column .tribe-side-actions { + padding: 2px 0 !important; + margin: 0 !important; +} +.main-column :is(.card-tags, .tribe-side-tags) { + gap: 4px 12px !important; + margin: 0 !important; + padding: 0 !important; +} +.main-column .calendar-details :is(.div-center, .calendar-interval-block) { + padding: 0 !important; + margin: 0 !important; +} +.main-column :is(.card-tags, .tribe-side-tags) .tag-link { + margin: 0 !important; + padding: 2px 0 !important; +} +.main-column form :is(textarea, select, input:not([type="hidden"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"])) { + margin-top: 4px !important; + margin-bottom: 6px !important; +} +.main-column .peers-import-form > :is(label, textarea, input) { + margin: 0 !important; +} +.main-column .tribe-details:has(> .tribe-main > .card-header .content-actions) > :is(.tribe-side, .tribe-main) { + display: contents !important; +} +.main-column .tribe-details:has(> .tribe-main > .card-header .content-actions) > .tribe-main > .card-header { + order: -1 !important; +} +.graphos-lightbox .graphos-zoom-stage { + position: relative !important; + width: 200vw !important; + margin: 0 !important; + padding: 0 !important; +} +.graphos-lightbox .graphos-zoom-svg { + margin: 0 !important; + padding: 0 !important; +} +.graphos-lightbox .graphos-zoom-focus { + position: absolute !important; + left: calc(50% - 50vw) !important; + top: calc(50% - 50vh) !important; + width: 100vw !important; + height: 100vh !important; + pointer-events: none !important; +} +.graphos-lightbox:has(.graphos-zoom-focus:target) { + display: flex !important; +} +.graphos-lightbox .graphos-canvas-zoom { + display: flex !important; + flex-direction: column !important; + justify-content: safe center !important; + align-items: flex-start !important; +} +.graphos-lightbox .graphos-zoom-stage { + flex: 0 0 auto !important; +} +.main-column .tribe-info-table tr:has(> td.tribe-info-label + td.tribe-info-value) { + display: flex !important; + flex-wrap: wrap !important; + align-items: baseline !important; + gap: 2px 6px !important; + padding: 6px 0 !important; + border-bottom: 1px solid #2a2a2a !important; +} +.main-column .tribe-info-table tr:has(> td.tribe-info-label + td.tribe-info-value) > td { + display: inline !important; + width: auto !important; + padding: 0 !important; + border: 0 !important; +} +.main-column .tribe-info-table tr:has(> td.tribe-info-label + td.tribe-info-value) > td.tribe-info-label::after { + content: ":"; +} +.main-column .ui-toolbar--filters { + justify-content: flex-start !important; +} +.main-column .phone-call-panel .phone-call-who > .phone-call-icon { + display: none !important; +} +.main-column .phone-call-actions { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: center !important; + gap: 10px !important; +} +.main-column .phone-call-actions a.phone-speaker-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + box-sizing: border-box !important; + height: 44px !important; + min-height: 44px !important; + margin: 0 !important; + padding: 6px 16px !important; + font-size: 16px !important; + line-height: 19.2px !important; + text-align: center !important; + white-space: nowrap !important; +} +.main-column .phone-call-actions > form { + display: flex !important; + margin: 0 !important; +} +.main-column .phone-call-actions > form > button { + margin: 0 !important; +} +.main-column .phone-call-panel .phone-call-who { + justify-content: center !important; +} +.main-column .phone-speaker-btn .phone-ico { + color: transparent !important; + text-shadow: 0 0 0 #ffa300 !important; +} +.main-column .phone-call-actions:has(> a.phone-speaker-btn) { + display: grid !important; + grid-template-columns: 1fr 1fr !important; + gap: 10px !important; +} +.main-column .phone-call-actions:has(> a.phone-speaker-btn) > * { + width: 100% !important; + min-width: 0 !important; +} +.main-column .phone-call-actions:has(> a.phone-speaker-btn) > form > button, +.main-column .phone-call-actions:has(> a.phone-speaker-btn) > a { + width: 100% !important; + padding-left: 6px !important; + padding-right: 6px !important; +} +.main-column .phone-call-actions:has(> a.phone-speaker-btn) > :last-child:nth-child(odd) { + grid-column: 1 / -1 !important; + justify-self: center !important; + width: calc(50% - 5px) !important; +} +.main-column .wiki-page > .card-section { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; +} +.main-column .wiki-page > .card-section > * { + margin-top: 0 !important; + margin-bottom: 0 !important; +} +.main-column .wiki-page > .card-section > .shop-title-row { + order: -1 !important; +} +.main-column .wiki-page .wiki-actions-top { + flex-wrap: wrap !important; + justify-content: center !important; + gap: 8px !important; + padding: 0 !important; +} +.main-column .wiki-page .wiki-actions-top > .wiki-actions-left { + flex: 0 0 100% !important; + justify-content: flex-start !important; + margin: 0 !important; +} +.main-column .card-engage .comments-summary.engage-on, +.main-column .card-engage .opinions-summary.engage-on, +.main-column .card-meta-row .comments-summary-link.engage-on { + background: #ff9900 !important; + color: #000 !important; + border: 1px solid #e68a00 !important; +} +.main-column .video-grid { gap: 6px !important; margin-top: 18px !important; } +.main-column .video-tile { width: calc(50% - 3px) !important; } +.main-column .video-grid-solo .video-tile { width: 100% !important; } +.main-column .forum-comment.highlighted-reply { + background-color: transparent !important; + border-left: 2px solid rgba(255, 164, 0, 0.55) !important; +} +.main-column .video-tile .video-tile-name, +.main-column .video-tile .user-link, +.main-column .video-grid-names { + display: none !important; +} +.main-column .tribe-side-actions.housing-status-row { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: center !important; + gap: 8px !important; + padding: 0 !important; + margin: 0 !important; +} +.main-column .tribe-side-actions.housing-status-row > .card-label { + width: auto !important; + flex: 0 0 auto !important; + margin: 0 !important; + letter-spacing: 0 !important; +} +.main-column .tribe-side-actions.housing-status-row > .inline-form { + display: inline-flex !important; + width: auto !important; + flex: 0 0 auto !important; + margin: 0 !important; +} +.main-column .tribe-main > .pad-members-details .pad-members-chips { + justify-content: center !important; +} +.main-column .tribe-side-actions.housing-status-row > .inline-form .tribe-action-btn { + min-height: 32px !important; + padding: 4px 12px !important; + font-size: 13px !important; +} +.main-column .tribe-side-actions.owner-actions { + padding: 0 !important; + margin: 0 !important; +} +.main-column .tribe-main > .room-live-panel { + padding: 8px 10px !important; + margin: 0 !important; +} +.main-column .tribe-main > .pad-members-details { + margin: 0 !important; +} +.main-column .tribe-main > .pad-members-details > summary { + margin: 0 !important; +} +.main-column .room-live-panel .phone-empty { + margin: 0 0 8px !important; +} +.main-column .tribe-side:has(> .owner-actions) { + display: flex !important; + flex-direction: column !important; +} +.main-column .tribe-side:has(> .owner-actions) > :is(.tribe-side-description, .tribe-info-table, .card-tags, .tribe-side-tags, .tribe-card-members, .tribe-side-actions:not(.owner-actions):not(.housing-status-row)) { + order: 1 !important; +} +.main-column .tribe-side:has(> .owner-actions) > .housing-status-row { + order: -1 !important; +} +.main-column .tribe-side:has(> .owner-actions) > :is(.card-header, .shop-title-row, .card-chips-row) { + order: -2 !important; +} +.main-column .room-live-head .room-live-controls { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + gap: 8px !important; +} +.main-column .live-lightbox { + top: 60px !important; + bottom: 66px !important; + margin: 0 !important; + left: 0 !important; + right: 0 !important; + width: auto !important; + height: auto !important; + padding: 0 !important; + z-index: 800 !important; + background: #0d0d0d !important; +} +.main-column .live-lightbox .lightbox-image { + width: 100% !important; + height: 100% !important; + max-width: 100% !important; + max-height: 100% !important; + object-fit: contain !important; + margin: 0 !important; + border: 0 !important; + border-radius: 0 !important; +} +.main-column .live-lightbox .lightbox-close { + position: absolute !important; + top: 8px !important; + right: 14px !important; + z-index: 2 !important; + font-size: 32px !important; + line-height: 1 !important; + color: #FFD700 !important; +} +.main-column .live-lightbox .lightbox-image-link { + display: block !important; + width: 100% !important; + height: 100% !important; + padding: 0 !important; + margin: 0 !important; + border: 0 !important; + background: none !important; +} +.main-column .live-lightbox.live-fullscreen { + top: 0 !important; + bottom: 0 !important; + z-index: 10050 !important; + background: #000 !important; +} +.main-column .live-lightbox .live-fullscreen-btn { + top: 8px !important; + right: 64px !important; + z-index: 2 !important; + color: #FFD700 !important; +} + +.profile-sensors-box span.karma-line.karma-line-stacked { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + gap: 1px !important; +} +.profile-sensors-box span.karma-line .karma-line-main { + display: block !important; + line-height: 1.25 !important; +} +.profile-sensors-box span.karma-line .karma-received-row { + display: flex !important; + justify-content: center !important; + align-items: center !important; + gap: 14px !important; + margin-top: 0 !important; + line-height: 1.25 !important; +} +.profile-sensors-box span.karma-line .karma-received { + display: inline-flex !important; + align-items: center !important; + gap: 4px !important; +} + +.main-column .room-live-panel .room-live-head { + gap: 6px !important; +} +.main-column .room-live-head .room-live-controls { + gap: 6px !important; +} +.main-column .room-live-controls .tribe-action-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + min-height: 0 !important; + height: auto !important; + padding: 5px 10px !important; + font-size: 12px !important; + line-height: 1.2 !important; + letter-spacing: 0 !important; + border-radius: 8px !important; +} +.main-column .room-live-controls .room-stream-input:invalid ~ .room-stream-go, +.main-column .room-live-controls .room-stream-input:valid ~ .room-stream-pick { + display: none !important; +} +.main-column .room-stream-chip { + display: inline-flex !important; + align-items: center !important; + gap: 4px !important; + max-width: 100% !important; + padding: 2px 10px !important; + border-radius: 12px !important; + font-size: 12px !important; + line-height: 1.4 !important; + background: rgba(255, 163, 0, 0.1) !important; +} +.main-column .room-live-panel .video-grid { + margin: 10px 0 !important; +} +.main-column .room-live-panel #room-video { + scroll-margin-top: 80px !important; +} +.main-column .room-live-panel .phone-contacts { + gap: 4px !important; +} +.main-column .phone-contact.room-peer { + min-height: 0 !important; + padding: 4px 6px !important; + gap: 6px !important; +} +.main-column .phone-contact.room-peer .user-link { + padding: 4px 10px !important; + font-size: 11px !important; +} +.main-column .phone-contact.room-peer :is(.room-peer-you, .room-peer-cam, .room-peer-muted, .room-peer-hand) { + padding: 1px 5px !important; + font-size: 10px !important; +} +.main-column .phone-contact.room-peer :is(button, .tribe-action-btn) { + min-height: 0 !important; + padding: 3px 8px !important; + font-size: 11px !important; +} +.main-column .live-lightbox.live-fullscreen { + top: 0 !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + width: 100vw !important; + height: 100vh !important; +} diff --git a/src/client/assets/themes/OasisMobileUX.css b/src/client/assets/themes/OasisMobileUX.css deleted file mode 100644 index a43279c..0000000 --- a/src/client/assets/themes/OasisMobileUX.css +++ /dev/null @@ -1,777 +0,0 @@ -html { overflow-x: clip !important; overflow-y: visible !important; height: auto !important; } -body { overflow-x: clip !important; overflow-y: visible !important; } - - -.oasis-bottombar { - display: flex !important; - position: fixed !important; - left: 0 !important; right: 0 !important; bottom: 0 !important; - z-index: 1000 !important; - justify-content: space-around !important; - align-items: center !important; - gap: 4px !important; - background: #1F1F1F !important; - border-top: 1px solid #333 !important; - padding: 8px 6px calc(8px + env(safe-area-inset-bottom, 0px)) !important; -} -.oasis-bottombar .bb-item { - position: relative !important; - display: flex !important; - flex-direction: column !important; - align-items: center !important; - gap: 3px !important; - flex: 1 1 0 !important; - min-width: 0 !important; - padding: 2px 0 !important; - text-decoration: none !important; -} -.oasis-bottombar .bb-ico { font-size: 18px !important; line-height: 1 !important; color: #FFA500 !important; } -.oasis-bottombar .bb-lbl { font-size: 9px !important; color: #ccc !important; white-space: nowrap !important; } -.oasis-bottombar .bb-badge { - position: absolute !important; - top: -4px !important; - left: calc(50% + 6px) !important; - min-width: 15px !important; height: 15px !important; - padding: 0 3px !important; - border-radius: 8px !important; - background: #FFA500 !important; - color: #121212 !important; - font-size: 9px !important; font-weight: bold !important; - display: flex !important; align-items: center !important; justify-content: center !important; -} - -body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)) !important; } - -.oasis-mobile-topbar { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - width: 100% !important; - padding: 8px 12px !important; - background: transparent !important; - box-shadow: none !important; border: 0 !important; -} - -.oasis-mobile-topbar .omt-logo img, -.oasis-mobile-topbar .omt-avatar img { - width: 32px !important; height: 32px !important; - border-radius: 50% !important; - border: 2px solid #FFA500 !important; - object-fit: cover !important; display: block !important; -} - -.oasis-mobile-topbar .omt-title { - flex: 1 1 auto !important; - text-align: center !important; - font-size: 26px !important; font-weight: bold !important; - letter-spacing: 10px !important; text-indent: 10px !important; - color: #FFD700 !important; text-decoration: none !important; - white-space: nowrap !important; - background: transparent !important; border: 0 !important; box-shadow: none !important; padding: 0 !important; -} - -.header .top-bar-left > a.logo-icon { display: none !important; } - -.oasis-mobile-menus { display: flex !important; gap: 8px !important; padding: 0 10px 6px !important; } -.oasis-mobile-menus .oasis-menu-ico { display: none !important; } -.oasis-mobile-menus .oasis-menu-btn { - flex: 1 1 0 !important; - display: flex !important; align-items: center !important; justify-content: center !important; - padding: 12px !important; - border: 1.5px solid #333 !important; border-radius: 10px !important; - background: #121212 !important; color: #ccc !important; - font-weight: bold !important; letter-spacing: 1px !important; cursor: pointer !important; -} - -#nav-left-toggle:checked ~ .header .oasis-menu-btn[for="nav-left-toggle"] { - background: #FFA500 !important; color: #121212 !important; border-color: #FFA500 !important; -} - -#nav-right-toggle:checked ~ .header .oasis-menu-btn[for="nav-right-toggle"] { - background: #FFD700 !important; color: #121212 !important; border-color: #FFD700 !important; -} - -:root { - --space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px; --space-5: 32px; - --fs-caption: 12px; --fs-body: 14px; --fs-body-lg: 16px; --fs-title: 20px; - --lh-body: 1.5; --measure: 66ch; - --c-muted: #8c8c8c; --radius-md: 12px; -} - -.oasis-bottombar .bb-fab .bb-ico { - width: 46px !important; height: 46px !important; - margin-top: -22px !important; - border-radius: 50% !important; - background: #FFA500 !important; color: #121212 !important; - display: flex !important; align-items: center !important; justify-content: center !important; - font-size: 22px !important; - box-shadow: 0 2px 8px rgba(0,0,0,.4) !important; -} -.oasis-bottombar .bb-fab .bb-lbl { color: #FFA500 !important; font-weight: bold !important; } - -.main-column, .main-column p, .main-column li, .main-column a, -.main-column span, .main-column td, .main-column h1, .main-column h2, .main-column h3 { - overflow-wrap: anywhere !important; - word-break: break-word !important; -} -.main-column { padding: var(--space-3) !important; } -.main-column p { line-height: var(--lh-body) !important; } - -.main-column > * + * { margin-top: var(--space-3) !important; } - -.main-column img, .main-column video, .main-column table, .main-column pre, .main-column iframe { - max-width: 100% !important; height: auto !important; -} -.main-column pre { white-space: pre-wrap !important; } - -.oasis-nav-group { - list-style: none !important; - background: #1c1c1c !important; - border: 1px solid #333 !important; - border-radius: var(--radius-md) !important; - overflow: hidden !important; - margin-bottom: var(--space-2) !important; -} -.oasis-nav-header { - display: flex !important; align-items: center !important; gap: var(--space-2) !important; - min-height: 48px !important; padding: 0 var(--space-3) !important; - cursor: pointer !important; - color: #FFD700 !important; font-weight: bold !important; - text-transform: uppercase !important; letter-spacing: .04em !important; - font-size: var(--fs-caption) !important; -} -.oasis-nav-header .nav-text { flex: 1 1 auto !important; } -.oasis-nav-header .oasis-nav-arrow { color: var(--c-muted) !important; transition: transform 150ms ease !important; } -.oasis-nav-toggle:checked + .oasis-nav-header .oasis-nav-arrow { transform: rotate(180deg) !important; } -.oasis-nav-header:active { background: #262626 !important; } -.oasis-nav-list { list-style: none !important; padding: 0 !important; margin: 0 !important; } -.oasis-nav-list li a { - display: flex !important; align-items: center !important; gap: var(--space-3) !important; - min-height: 48px !important; padding: 0 var(--space-3) !important; - color: #e8e8e8 !important; text-decoration: none !important; - border-top: 1px solid #262626 !important; -} -.oasis-nav-list li a .emoji { flex: 0 0 auto !important; width: 22px !important; text-align: center !important; color: #FFA500 !important; } -.oasis-nav-list li a .nav-text { font-size: var(--fs-body) !important; } -.oasis-nav-list li a:active { background: #262626 !important; } -.oasis-nav-list li a.current { color: #FFA500 !important; font-weight: bold !important; } - -.main-column h1 { font-size: var(--fs-title) !important; font-weight: 700 !important; line-height: 1.25 !important; margin: 0 0 var(--space-2) !important; } -.main-column h2 { font-size: 18px !important; font-weight: 700 !important; line-height: 1.3 !important; margin: 0 0 var(--space-2) !important; } -.main-column h3 { font-size: var(--fs-body-lg) !important; font-weight: 600 !important; margin: 0 0 var(--space-1) !important; } -.main-column p { font-size: var(--fs-body-lg) !important; } -.main-column .created-at, .main-column .small, .main-column .time, -.main-column .meta, .main-column .author-meta, .main-column .subtitle { - color: var(--c-muted) !important; font-size: var(--fs-caption) !important; -} - -.main-column article, -.main-column [class*="-card"] { - background: #1c1c1c !important; - border: 1px solid #333 !important; - border-radius: var(--radius-md) !important; - padding: var(--space-3) !important; -} -.main-column article:active, -.main-column [class*="-card"]:active { background: #202020 !important; } - -.btn, button, input[type="submit"], input[type="button"], .submit-button, -.filter-btn, .create-button, .edit-btn, .delete-btn, .join-btn, .leave-btn, .buy-btn { - min-height: 48px !important; -} -button:active, input[type="submit"]:active, .submit-button:active, -.filter-btn:active, .create-button:active, .edit-btn:active, -.delete-btn:active, .join-btn:active, .leave-btn:active, .buy-btn:active { - filter: brightness(.85) !important; -} -.main-column a:active { opacity: .6 !important; } - -* { -webkit-tap-highlight-color: transparent !important; } -a, button, label, .tap, .oasis-menu-btn, .bb-item, .oasis-nav-header, .oasis-nav-list li a { - touch-action: manipulation !important; -} -body { overscroll-behavior: none !important; } - -.main-content, .main-column, .header, -.sidebar-left, .sidebar-right { overflow: visible !important; } - -.sidebar-left, .sidebar-right { display: none !important; } -#nav-left-toggle:checked ~ .main-content .sidebar-left { display: block !important; } -#nav-right-toggle:checked ~ .main-content .sidebar-right { display: block !important; } - -.bb-manage { margin: 8px 0 16px !important; } -.bb-manage h3, .bb-manage-add h3 { - font-size: 13px !important; text-transform: uppercase !important; letter-spacing: .05em !important; - color: #FFA500 !important; margin: 14px 0 8px !important; -} -.bb-manage-list { display: flex !important; flex-direction: column !important; gap: 8px !important; } -.bb-manage-form { margin: 0 !important; width: 100% !important; } -.bb-manage-btn { - width: 100% !important; - display: flex !important; align-items: center !important; gap: 12px !important; - min-height: 52px !important; padding: 0 14px !important; - background: #1c1c1c !important; border: 1px solid #333 !important; border-radius: 10px !important; - color: #eee !important; cursor: pointer !important; text-align: left !important; -} -.bb-manage-ico { font-size: 20px !important; color: #FFA500 !important; width: 26px !important; text-align: center !important; } -.bb-manage-lbl { flex: 1 1 auto !important; font-size: 15px !important; } -.bb-manage-x { font-size: 16px !important; color: #ff6b6b !important; font-weight: bold !important; } -.bb-manage-empty, .bb-manage-full { color: #999 !important; font-size: 14px !important; padding: 8px 2px !important; } -.bb-manage-done { - display: block !important; text-align: center !important; - margin-top: 16px !important; min-height: 48px !important; line-height: 48px !important; -} - -.hive-nav { --hx: min(64px, calc((100vw - 40px) / 4)); --hxm: min(58px, calc((100vw - 36px) / 5)); margin: 8px 0 16px !important; overflow: visible !important; background: transparent !important; padding: 0 !important; border: 0 !important; box-shadow: none !important; } -.hive-radio { position: absolute !important; opacity: 0 !important; width: 0 !important; height: 0 !important; pointer-events: none !important; } -.hive-grid { display: flex !important; flex-direction: column !important; align-items: center !important; overflow: visible !important; background: transparent !important; padding: 0 !important; margin: 0 !important; box-shadow: none !important; } -.hive-row { display: flex !important; justify-content: center !important; gap: 3px !important; overflow: visible !important; background: transparent !important; padding: 0 !important; margin: 0 !important; box-shadow: none !important; } -.hive-cats .hive-row + .hive-row { margin-top: calc(var(--hx) * -0.30) !important; } -.hex-cat, .hex-mod { - position: relative !important; - -webkit-clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; - clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; - display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; gap: 2px !important; - text-decoration: none !important; cursor: pointer !important; box-sizing: border-box !important; - background: #D9A400 !important; - filter: drop-shadow(0 1px 2px rgba(0,0,0,.6)) !important; -} -.hex-cat::before, .hex-mod::before { - content: '' !important; position: absolute !important; inset: 1.5px !important; z-index: 0 !important; - -webkit-clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; - clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; - background: #161616 !important; -} -.hex-cat { width: var(--hx) !important; height: calc(var(--hx) * 1.15) !important; } -.hex-mod { width: var(--hxm) !important; height: calc(var(--hxm) * 1.15) !important; } -.hex-cat .hx-ico, .hex-mod .hx-ico { color: #FFA500 !important; line-height: 1 !important; position: relative !important; z-index: 1 !important; } -.hex-cat .hx-ico { font-size: 17px !important; } -.hex-mod .hx-ico { font-size: 16px !important; } -.hex-cat .hx-lbl, .hex-mod .hx-lbl { color: #d8d8d8 !important; text-align: center !important; line-height: 1 !important; font-weight: bold !important; overflow: hidden !important; position: relative !important; z-index: 1 !important; } -.hex-cat .hx-lbl { font-size: 8px !important; max-width: 88% !important; } -.hex-mod .hx-lbl { font-size: 8px !important; max-width: 92% !important; } -.hex-cat:active::before, .hex-mod:active::before { background: #242424 !important; } -#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"], -#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"], -#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"], -#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"], -#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"], -#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"], -#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"], -#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"], -#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"], -#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] { background: #FFA500 !important; } -#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"]::before, -#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"]::before, -#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"]::before, -#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"]::before, -#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"]::before, -#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"]::before, -#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"]::before, -#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"]::before, -#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"]::before, -#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"]::before { background: #FFA500 !important; } -#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"] :is(.hx-ico,.hx-lbl), -#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"] :is(.hx-ico,.hx-lbl), -#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"] :is(.hx-ico,.hx-lbl), -#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"] :is(.hx-ico,.hx-lbl), -#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"] :is(.hx-ico,.hx-lbl), -#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"] :is(.hx-ico,.hx-lbl), -#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"] :is(.hx-ico,.hx-lbl), -#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"] :is(.hx-ico,.hx-lbl), -#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"] :is(.hx-ico,.hx-lbl), -#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] :is(.hx-ico,.hx-lbl) { color: #121212 !important; } -.hive-panel { display: none !important; margin: 12px auto 0 !important; max-width: 340px !important; background: #101010 !important; border: 1px solid #242424 !important; border-radius: 16px !important; padding: 12px 8px !important; overflow: visible !important; box-shadow: none !important; } -#hive-personal:checked ~ #hive-panel-personal, -#hive-governance:checked ~ #hive-panel-governance, -#hive-office:checked ~ #hive-panel-office, -#hive-economy:checked ~ #hive-panel-economy, -#hive-tools:checked ~ #hive-panel-tools, -#hive-network:checked ~ #hive-panel-network, -#hive-blogs:checked ~ #hive-panel-blogs, -#hive-creative:checked ~ #hive-panel-creative, -#hive-media:checked ~ #hive-panel-media, -#hive-fediverse:checked ~ #hive-panel-fediverse { display: block !important; } -.hive-mods { display: flex !important; flex-wrap: wrap !important; justify-content: center !important; gap: 4px !important; margin: 0 !important; padding: 0 !important; background: transparent !important; overflow: visible !important; } - -.header .top-bar-left nav, -.header .top-bar-right nav { display: none !important; } - -.oasis-mobile-topbar { padding-top: calc(env(safe-area-inset-top, 0px) + 10px) !important; } - -.oasis-bottombar .bb-pair { - display: flex !important; align-items: center !important; - flex: 0 0 auto !important; - gap: 2px !important; padding: 3px !important; - background: #262626 !important; - border: 1px solid #3a3a3a !important; - border-radius: 999px !important; -} -.oasis-bottombar .bb-pair .bb-item { flex: 0 0 auto !important; padding: 2px 8px !important; } -.oasis-bottombar .bb-pair .bb-lbl { font-size: 8px !important; } - -.oasis-bottombar a.bb-item.current .bb-ico { - background: #FFA500 !important; color: #121212 !important; - border-radius: 999px !important; padding: 4px 12px !important; -} -.oasis-bottombar a.bb-item.current .bb-lbl { color: #FFA500 !important; font-weight: bold !important; } -.oasis-bottombar a.bb-item.current .bb-badge { background: #121212 !important; color: #FFA500 !important; } - -.main-column, .main-column p, .main-column li, .main-column a, -.main-column span, .main-column td, .main-column th, .main-column label, -.main-column h1, .main-column h2, .main-column h3, .main-column h4, -.main-column strong, .main-column em, .main-column small, -.created-at, .post-meta, .feed-post-meta, -.feed-row .small, .feed-row .time, .feed-row .created-at, -.profile-sensors-box > *, -.card-field .card-value, .card-field .card-label { - overflow-wrap: break-word !important; - word-break: normal !important; - -webkit-hyphens: none !important; - hyphens: none !important; -} - -.main-column pre, .main-column code, kbd, samp, -.peer-key, .user-link.peer-key, .oasis-id-copy, .search-oasis-id, -.pm-sent-key-value, .kpi__value, -.block-info-table td .peer-key, .block-info-table td .user-link, -.pm-info-table td .user-link, -.main-column a[href*="ed25519"], .main-column a[href^="ssb:"], -.main-column a[href^="/author/"] { - overflow-wrap: anywhere !important; - word-break: break-all !important; -} - -button, input[type="submit"], input[type="button"], -.filter-btn, a.filter-btn, .create-button, .edit-btn, .delete-btn, -.join-btn, .leave-btn, .buy-btn, .submit-button, .btn, -.conn-actions button, .peers-conn-actions button, -.mode-buttons button, .mode-buttons .filter-btn, -.filter-group button, .filter-group .filter-btn, -.main-column button, .main-column input[type="submit"] { - white-space: normal !important; - word-break: normal !important; - overflow-wrap: break-word !important; - -webkit-hyphens: none !important; - hyphens: none !important; -} - -.conn-actions, .peers-conn-actions { - display: flex !important; - flex-direction: row !important; - flex-wrap: wrap !important; - justify-content: center !important; - align-items: stretch !important; - gap: 8px !important; - width: 100% !important; - max-width: 100% !important; - overflow-x: hidden !important; -} -.conn-actions > form, .peers-conn-actions > form { - flex: 0 1 auto !important; - width: auto !important; - min-width: 0 !important; - margin: 0 !important; - display: flex !important; -} - -.pm-info-table, .pm-info-table tbody, -.block-info-table, .block-info-table tbody { - display: block !important; - width: 100% !important; - max-width: 100% !important; - border: 0 !important; - background: transparent !important; - box-shadow: none !important; - table-layout: auto !important; -} -.pm-info-table thead, .block-info-table thead { display: none !important; } -.pm-info-table tr, .block-info-table tr { - display: block !important; - width: 100% !important; - box-sizing: border-box !important; - border: 0 !important; - border-bottom: 1px solid rgba(255,165,0,0.18) !important; - border-radius: 0 !important; - margin: 0 0 8px 0 !important; - padding: 6px 0 !important; - background: transparent !important; -} -.pm-info-table td, .block-info-table td { - display: block !important; - width: 100% !important; - max-width: 100% !important; - box-sizing: border-box !important; - border: 0 !important; - background: transparent !important; - padding: 1px 0 !important; - text-align: left !important; - overflow-wrap: break-word !important; - word-break: normal !important; -} - -.pm-info-table td.card-label, .block-info-table td.card-label { - color: var(--c-muted, #8c8c8c) !important; - font-size: 11px !important; - font-weight: 700 !important; - text-transform: uppercase !important; - letter-spacing: .03em !important; - margin-bottom: 2px !important; - padding-bottom: 0 !important; -} -.pm-info-table td.card-value, .block-info-table td.card-value { - font-size: 15px !important; - line-height: 1.5 !important; - margin-bottom: 6px !important; -} - -.block-info-table tr:first-child:has(> td.card-label) { display: none !important; } -.block-info-table td[data-label]:not([data-label=""])::before { - content: attr(data-label); - display: block !important; - color: var(--c-muted, #8c8c8c) !important; - font-size: 10px !important; - font-weight: 700 !important; - text-transform: uppercase !important; - letter-spacing: .03em !important; - margin-bottom: 1px !important; - white-space: nowrap !important; - word-break: normal !important; -} -.block-info-table td[data-label=""]::before { content: none !important; } - -.peers-list .block-info-table td, -.peers-technical-block .block-info-table td, -.gov-overview tbody td { - word-break: normal !important; - overflow-wrap: break-word !important; -} - -.pm-info-table td .user-link, .pm-info-table td .peer-key, .pm-info-table td a[href], -.block-info-table td .peer-key, .block-info-table td .user-link, .block-info-table td a[href] { - display: inline-block !important; - max-width: 100% !important; - overflow-wrap: anywhere !important; - word-break: break-all !important; -} - -.pm-info-table td form, .block-info-table td form, -.pm-info-table td button, .block-info-table td button { - width: 100% !important; - margin: 0 !important; -} - -.main-content { width: 100% !important; max-width: 100% !important; } -.main-column { - width: 100% !important; max-width: 100% !important; - padding: 16px !important; box-sizing: border-box !important; -} -.main-column .card-field { - flex-direction: column !important; - align-items: stretch !important; - gap: 2px !important; -} -.main-column .card-field .card-value { - width: 100% !important; max-width: 100% !important; font-size: 15px !important; -} -.main-column .card-field .card-label { - color: var(--c-muted, #8c8c8c) !important; font-size: 11px !important; -} -.main-column img, .main-column video, .main-column iframe, .main-column canvas { - max-width: 100% !important; height: auto !important; -} -.main-column hr { - width: 100% !important; border: 0 !important; - border-top: 1px solid #333 !important; margin: 16px 0 !important; -} - -.activity-filter-acc { width: 100% !important; margin-bottom: 8px !important; } - -.activity-quick-modes { - display: flex !important; flex-wrap: nowrap !important; gap: 6px !important; margin-bottom: 8px !important; -} -.activity-quick-modes form { flex: 1 1 0 !important; min-width: 0 !important; margin: 0 !important; display: flex !important; } -.activity-quick-modes .filter-btn { - flex: 1 1 0 !important; width: 100% !important; min-height: 44px !important; - white-space: nowrap !important; word-break: keep-all !important; overflow-wrap: normal !important; - padding-left: 6px !important; padding-right: 6px !important; font-size: 14px !important; -} - -.activity-groups { list-style: none !important; padding: 0 !important; margin: 0 !important; } - -.activity-groups .oasis-nav-header .emoji { display: none !important; } -.activity-groups .oasis-nav-header .nav-text { flex: 1 1 auto !important; } -.activity-groups .act-count { - flex: 0 0 auto !important; min-width: 20px !important; text-align: center !important; - font-size: 11px !important; font-weight: 700 !important; color: #121212 !important; - background: #FFA500 !important; border-radius: 999px !important; padding: 1px 7px !important; - margin: 0 8px 0 0 !important; -} - -.activity-groups .oasis-nav-list li { list-style: none !important; margin: 0 !important; padding: 0 !important; } -.activity-groups .oasis-nav-list form { margin: 0 !important; display: block !important; width: 100% !important; } -.activity-groups .oasis-nav-list .filter-btn { - display: block !important; width: 100% !important; text-align: left !important; - min-height: 44px !important; margin: 2px 0 !important; box-sizing: border-box !important; -} -.activity-groups .oasis-nav-list .filter-btn.active { - outline: 2px solid #FFA500 !important; outline-offset: -2px !important; -} - -.message-list { display: flex !important; flex-direction: column !important; gap: 4px !important; width: 100% !important; } -.message-list .pm-thread { display: flex !important; flex-direction: column !important; width: 100% !important; align-self: stretch !important; } -.pm-thread-replies { display: flex !important; flex-direction: column !important; gap: 4px !important; } - -.pm-card.pm-bubble { - max-width: 82% !important; width: fit-content !important; min-width: 0 !important; - box-sizing: border-box !important; padding: 8px 12px !important; margin: 2px 0 !important; - border-radius: 16px !important; border: 1px solid #333 !important; - background: #1e1e1e !important; box-shadow: none !important; cursor: default !important; -} - -.pm-card.pm-bubble.pm-recv { - align-self: flex-start !important; margin-left: 0 !important; margin-right: auto !important; - border-bottom-left-radius: 4px !important; background: #1e1e1e !important; border-color: #343434 !important; -} - -.pm-card.pm-bubble.pm-sent { - align-self: flex-end !important; margin-left: auto !important; margin-right: 0 !important; - border-bottom-right-radius: 4px !important; background: #2a2413 !important; border-color: #5c4c1f !important; -} - -.pm-bubble .pm-bubble-sender { font-size: 11px !important; font-weight: 700 !important; color: #FFA500 !important; margin-bottom: 3px !important; } -.pm-bubble .pm-bubble-sender a { color: #FFA500 !important; } - -.pm-bubble .message-text { font-size: 15px !important; line-height: 1.42 !important; color: #ececec !important; margin: 0 !important; } -.pm-bubble .message-text a { color: #FFD700 !important; } - -.pm-bubble .pm-bubble-meta { - display: flex !important; flex-wrap: wrap !important; align-items: center !important; - gap: 6px !important; margin-top: 4px !important; font-size: 10px !important; color: var(--c-muted, #8c8c8c) !important; -} -.pm-card.pm-sent .pm-bubble-meta { justify-content: flex-end !important; } -.pm-card.pm-recv .pm-bubble-meta { justify-content: flex-start !important; } -.pm-bubble .pm-bubble-subject { font-style: italic !important; opacity: .75 !important; max-width: 50% !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } -.pm-bubble .pm-bubble-lock { opacity: .85 !important; } -.pm-bubble .pm-bubble-eco { font-size: 9px !important; opacity: .9 !important; } -.pm-bubble .pm-bubble-eco * { font-size: 9px !important; } - -.pm-bubble .pm-actions { margin-top: 6px !important; display: flex !important; gap: 6px !important; } -.pm-card.pm-sent .pm-actions { justify-content: flex-end !important; } -.pm-bubble .pm-actions .pm-btn { font-size: 10px !important; padding: 3px 8px !important; min-height: 28px !important; } -.pm-bubble .pm-actions form { margin: 0 !important; } - -.pm-bubble .pm-info-table { display: none !important; } - -.pm-thread .pm-thread-details { width: 100% !important; } -.pm-thread .pm-thread-toggle { font-size: 12px !important; color: var(--c-muted, #8c8c8c) !important; padding: 4px 0 !important; cursor: pointer !important; } - -.main-column h1, .main-column h2, .main-column h3, .main-column h4, -.tags-header h2, .tags-header h3, .title-with-chip h2, .pm-title, h2.pm-title, -.cn-name, h1.cn-name, .cn-section, h2.cn-section, .profile-side-name, h2.profile-side-name { - text-wrap: balance !important; - overflow-wrap: break-word !important; - word-break: keep-all !important; - -webkit-hyphens: none !important; hyphens: none !important; - text-align: left !important; -} -.main-column h1, .tags-header h2, .cn-name { - font-size: var(--fs-title, 20px) !important; font-weight: 700 !important; - line-height: 1.22 !important; letter-spacing: -0.01em !important; - margin: 0 0 var(--space-2, 8px) !important; -} -.main-column h2, .pm-title, .title-with-chip h2, .cn-section { - font-size: 18px !important; font-weight: 700 !important; - line-height: 1.28 !important; margin: var(--space-1, 4px) 0 var(--space-2, 8px) !important; -} -.main-column h3 { - font-size: var(--fs-body-lg, 16px) !important; font-weight: 600 !important; - line-height: 1.35 !important; margin: var(--space-1, 4px) 0 !important; -} -.main-column h1, .main-column h2, .main-column h3, -.tags-header h2, .pm-title, .cn-name, .cn-section { - max-width: 40ch !important; width: auto !important; min-width: 0 !important; -} - -.main-column h1 code, .main-column h2 code, .main-column h3 code, -.tags-header h2 code, .pm-title code, -.main-column h1 a[href*="ed25519"], .main-column h2 a[href*="ed25519"], -.main-column h1 a[href^="ssb:"], .main-column h2 a[href^="ssb:"] { - word-break: break-all !important; overflow-wrap: anywhere !important; -} - -.main-column .tags-header { - display: flex !important; flex-direction: column !important; align-items: flex-start !important; - gap: var(--space-1, 4px) !important; width: 100% !important; max-width: 100% !important; - margin: 0 0 var(--space-2, 8px) !important; -} -.main-column .tags-header h2 { margin: 0 !important; } - -.main-column .title-with-chip { - display: flex !important; flex-wrap: wrap !important; align-items: center !important; - gap: var(--space-2, 8px) !important; width: 100% !important; max-width: 100% !important; -} -.main-column .title-with-chip h2 { flex: 1 1 auto !important; min-width: 0 !important; margin: 0 !important; max-width: none !important; } -.main-column .title-with-chip > :not(h2) { flex: 0 0 auto !important; } - -.main-column p, .main-column li, -.tags-header p, .tags-header .prefs-help, .indexing-note, .indexing-percent, -.error-page-message, .welcome-text, .message-text, .pm-bubble .message-text { - text-wrap: pretty !important; - line-height: 1.55 !important; - max-width: min(66ch, 100%) !important; - overflow-wrap: anywhere !important; - word-break: normal !important; - min-width: 0 !important; - -webkit-hyphens: none !important; hyphens: none !important; -} -.main-column p + p { margin-top: var(--space-3, 16px) !important; } -.tags-header p, .tags-header .prefs-help, .indexing-note { line-height: 1.5 !important; margin: 4px 0 0 0 !important; } -.pm-bubble .message-text { line-height: 1.45 !important; max-width: 100% !important; } - -[style*="auto-fill"], -[style*="minmax(240px"], [style*="minmax(280px"], [style*="minmax(300px"], -[style*="minmax( 240px"], [style*="minmax( 280px"], [style*="minmax( 300px"], -.trending-container, .cn-hub-grid, .cn-products { - display: grid !important; grid-template-columns: 1fr !important; gap: 12px !important; - width: 100% !important; max-width: 100% !important; -} -.trending-container > *, .games-grid > *, .market-grid > *, .tribe-grid > *, -.feed-container, .feed-card, .trending-card, -.main-column article, .main-column [class*="-card"], .main-column [class*="-item"], -.main-column [class*="-entry"], .main-column .card { - width: 100% !important; max-width: 100% !important; min-width: 0 !important; - box-sizing: border-box !important; margin-left: 0 !important; margin-right: 0 !important; -} -.feed-card .feed-text, .trending-card .feed-text, -.main-column article p, .main-column [class*="-card"] p { max-width: var(--measure, 66ch) !important; } -.main-column form, .comment-form, .comment-form-wrapper, .market-form, -.new-message-form, .project-control-form, .filter-box { - display: block !important; width: 100% !important; max-width: 100% !important; - min-width: 0 !important; box-sizing: border-box !important; -} -.main-column input:not([type="checkbox"]):not([type="radio"]):not(.oasis-nav-toggle):not(.oasis-nav-side-toggle), -.main-column textarea, .main-column select { - width: 100% !important; max-width: 100% !important; min-width: 0 !important; box-sizing: border-box !important; -} -.main-column form > button:only-of-type, .main-column form > .create-button, -.comment-form button[type="submit"], .market-form button[type="submit"], -.new-message-form button[type="submit"] { - display: block !important; width: 100% !important; max-width: 100% !important; - box-sizing: border-box !important; margin-top: 8px !important; -} -[style*="width:33%"], [style*="width: 33%"], [style*="width:25%"], [style*="width: 25%"], -[style*="width:60%"], [style*="width: 60%"], [style*="width:40%"], [style*="width: 40%"] { - width: 100% !important; max-width: 100% !important; -} -.main-column [style*="max-width: 800px"], .main-column [style*="max-width:800px"], -.ai-response, .ai-train-bar { - max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; box-sizing: border-box !important; -} -.message-list, .pm-thread, .pm-thread-replies { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } - -.main-column p .user-link, .main-column p a[href^="/author/"], -.main-column li .user-link, .main-column span .user-link, -.main-column .message-text .user-link, .main-column .message-text a[href^="/author/"], -.pm-bubble .message-text .user-link, .pm-bubble .message-text a[href^="/author/"], -.pm-quote a[href^="/author/"] { - display: inline !important; white-space: normal !important; - overflow-wrap: anywhere !important; word-break: keep-all !important; - -webkit-hyphens: none !important; hyphens: none !important; -} - -.peer-key, .user-link.peer-key, .block-info-table td .peer-key, .pm-info-table td .peer-key, -.pm-info-table td .user-link, .block-info-table td .user-link, -.search-oasis-id, .pm-sent-key-value, .kpi__value { - display: inline-block !important; max-width: 100% !important; - overflow-wrap: anywhere !important; word-break: break-all !important; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; - font-variant-numeric: tabular-nums !important; line-height: 1.45 !important; -} - -.oasis-id-copy { - display: block !important; width: 100% !important; white-space: nowrap !important; - overflow: hidden !important; text-overflow: ellipsis !important; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; -} -.invites-list .user-link.peer-key, .peers-list .block-info-table td .peer-key { - display: block !important; max-width: 100% !important; white-space: nowrap !important; - overflow: hidden !important; text-overflow: ellipsis !important; word-break: normal !important; -} - -.main-column .message-text a[href^="http"], .main-column p a[href^="http"], -.pm-bubble .message-text a[href^="http"], .main-column .message-text a.oasis-path-link, -.main-column .message-text a.job-link, .main-column .message-text a.project-link, -.main-column .message-text a.market-link { - overflow-wrap: anywhere !important; word-break: keep-all !important; - -webkit-hyphens: none !important; hyphens: none !important; -} -.main-column code:not(pre code), .pm-bubble .message-text code { - display: inline !important; max-width: 100% !important; - overflow-wrap: anywhere !important; word-break: break-word !important; white-space: pre-wrap !important; -} - -.main-column .tags-header { margin-bottom: 6px !important; gap: 2px !important; } -.main-column .tags-header h2, .tags-header h2 { - font-size: 17px !important; line-height: 1.2 !important; letter-spacing: 0 !important; -} -.main-column .tags-header p { font-size: 13px !important; line-height: 1.35 !important; margin: 0 !important; } - -.main-column { padding: 10px 12px 16px 12px !important; } -.activity-filter-acc { margin-bottom: 4px !important; } - -.empty-state { - display: flex !important; flex-direction: column !important; align-items: center !important; - justify-content: center !important; text-align: center !important; - padding: 40px 16px !important; gap: 8px !important; width: 100% !important; box-sizing: border-box !important; -} -.empty-state-icon { font-size: 44px !important; line-height: 1 !important; opacity: 0.55 !important; } -.empty-state-title { font-size: 16px !important; font-weight: 700 !important; margin: 4px 0 0 0 !important; color: #eee !important; } -.empty-state-text { color: var(--c-muted, #8c8c8c) !important; font-size: 14px !important; line-height: 1.4 !important; max-width: 40ch !important; margin: 0 !important; } -.empty-state-cta { margin-top: 10px !important; min-height: 48px !important; display: inline-flex !important; align-items: center !important; } - -.activity-filter-collapsed { - display: flex !important; align-items: center !important; gap: 10px !important; - flex-wrap: wrap !important; margin-bottom: 8px !important; width: 100% !important; -} -.activity-back { - display: inline-flex !important; align-items: center !important; gap: 4px !important; - min-height: 44px !important; padding: 6px 14px 6px 10px !important; flex: 0 0 auto !important; -} -.activity-back-arrow { font-size: 22px !important; line-height: 1 !important; font-weight: bold !important; } -.activity-active-filter { - font-weight: 700 !important; color: #FFA500 !important; text-transform: uppercase !important; - font-size: 13px !important; letter-spacing: .03em !important; -} - -.oasis-bottombar .bb-item { min-height: 48px !important; } -.oasis-bottombar .bb-pair .bb-item { min-height: 44px !important; padding: 4px 10px !important; } -.oasis-mobile-menus .oasis-menu-btn { min-height: 48px !important; } -.pm-bubble .pm-actions .pm-btn { min-height: 40px !important; padding: 6px 12px !important; } -.score-btn, .vote-btn, .feed-action-btn { - min-height: 44px !important; min-width: 44px !important; - display: inline-flex !important; align-items: center !important; justify-content: center !important; -} -.score-btn:active, .vote-btn:active, .feed-action-btn:active { filter: brightness(.85) !important; } - -.oasis-bottombar .bb-add .bb-ico { - background: #FFA500 !important; color: #121212 !important; border-radius: 999px !important; - width: 30px !important; height: 30px !important; display: inline-flex !important; - align-items: center !important; justify-content: center !important; - font-weight: bold !important; font-size: 20px !important; line-height: 1 !important; -} -.bb-pick-grid { - display: grid !important; grid-template-columns: repeat(3, 1fr) !important; - gap: 8px !important; width: 100% !important; margin-top: 8px !important; -} -.bb-pick-form { margin: 0 !important; display: block !important; } -.bb-pick-btn { - display: flex !important; flex-direction: column !important; align-items: center !important; - justify-content: center !important; gap: 6px !important; width: 100% !important; - min-height: 76px !important; padding: 10px 6px !important; box-sizing: border-box !important; - background: #1c1c1c !important; border: 1px solid #333 !important; border-radius: 12px !important; color: #eee !important; -} -.bb-pick-btn.active { border-color: #FFA500 !important; background: #2a2412 !important; } -.bb-pick-ico { font-size: 26px !important; color: #FFA500 !important; line-height: 1 !important; } -.bb-pick-lbl { font-size: 11px !important; text-align: center !important; line-height: 1.2 !important; } - diff --git a/src/client/assets/themes/Purple-SNH.css b/src/client/assets/themes/Purple-SNH.css new file mode 100644 index 0000000..0f50ff7 --- /dev/null +++ b/src/client/assets/themes/Purple-SNH.css @@ -0,0 +1,429 @@ +body { + background-color: #4B0A6D; + color: #E5E5E5; + font-family: 'Arial', sans-serif; +} + +footer { + border-top: 2px solid #9B1C96; +} + +.sidebar-left, .sidebar-right { + background-color: #39006D; + border: 1px solid #9B1C96; + color: #E5E5E5; + padding: 15px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); +} + +.main-column { + background-color: #1A1A1A; + border: 1px solid #9B1C96; + padding: 20px; + color: #E5E5E5; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); +} + +button, input[type="submit"], input[type="button"] { + background-color: #9B1C96; + color: #E5E5E5; + border: 2px solid #6A0066; + border-radius: 10px; + padding: 12px 24px; + cursor: pointer; +} + +button:hover, input[type="submit"]:hover, input[type="button"]:hover { + background-color: #6A0066; + border-color: #9B1C96; + box-shadow: 0 0 15px rgba(155, 28, 150, 0.8); +} + +input, textarea, select { + background-color: #333333; + color: #E5E5E5; + border: 1px solid #9B1C96; + border-radius: 5px; + padding: 12px; +} + +input:focus, textarea:focus, select:focus { + background-color: #39006D; + outline: none; +} + +a { + color: #9B1C96; + text-decoration: none; +} + +a:hover { + color: #E5E5E5; +} + +table { + background-color: #1A1A1A; + color: #E5E5E5; + width: 100%; + border-collapse: collapse; +} + +table th { + background-color: #6A0066; +} + +table tr:nth-child(even) { + background-color: #333333; +} + +nav ul { + background-color: #39006D; + padding: 0; + margin: 0; + list-style: none; +} + +nav ul li { + display: inline-block; + margin-right: 10px; +} + +nav ul li a { + color: #E5E5E5; + padding: 15px; + display: inline-block; + text-transform: none; + font-weight: bold; + letter-spacing: 0; +} + +.profile { + background-color: #333333; + color: #E5E5E5; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0,0,0,0.3); +} + +.profile .name { + color: #9B1C96; + font-size: 20px; + font-weight: bold; +} + +.avatar { + border: 4px solid #9B1C96; + border-radius: 50%; + width: 80px; + height: 80px; + margin-bottom: 10px; +} + +article, section { + background-color: #333333; + color: #E5E5E5; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0,0,0,0.3); +} + +.post-preview { + background-color: #39006D; + padding: 15px; + border-radius: 8px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); +} + +.post-preview img { + border-radius: 8px; + max-width: 100%; +} + +::-webkit-scrollbar-thumb { + background-color: #9B1C96; +} + +::-webkit-scrollbar-track { + background-color: #1A1A1A; +} + +.action-container { + background-color: #333333; + border-color: #9B1C96; + color: #E5E5E5; +} + +footer a { + background-color: #9B1C96; + color: #FFFFFF; + padding: 8px 20px; + border-radius: 5px; + text-decoration: none; +} + +footer a:hover { + background-color: #6A0066; +} + + +.sidebar-left nav ul li a, +.sidebar-right nav ul li a, +.header nav ul li a { + background-color: #682B94 !important; + color: #FFE082 !important; + border: 1px solid #B86ADE !important; + font-weight: 600; + border-radius: 6px; + padding: 12px 16px; + display: flex; + justify-content: flex-start; + box-sizing: border-box; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.sidebar-left nav ul li a:hover, +.sidebar-right nav ul li a:hover, +.header nav ul li a:hover { + background-color: #682B94 !important; + border-color: #FFD54F !important; + color: #FFFFFF !important; +} + +body { + background-color: #2D0B47 !important; +} + +.main-column, +article, +section, +.action-container, +.profile, +.post-preview { + background-color: #3C1360 !important; + border-color: #B86ADE !important; + color: #FFEEDB !important; +} + +input, +textarea, +select { + background-color: #4B1A72 !important; + color: #FFFFFF !important; + border-color: #BB5EFF !important; +} + +button, +input[type="submit"], +input[type="button"] { + background-color: #A34AD8 !important; + color: #FFFFFF !important; + border-color: #751E9F !important; +} + +header { + background-color: #5A1A85 !important; + border-bottom: 1px solid #B86ADE !important; + box-shadow: none !important; +} + +.header { + background-color: #5A1A85 !important; +} + +.top-bar-left, +.top-bar-mid, +.top-bar-right { + background-color: #39006D !important; + border: 1px solid #9B1C96 !important; + padding: 15px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + display: flex; + gap: 12px; + border-radius: 10px; +} + +.sidebar-left, +.sidebar-right { + padding: 16px; + box-sizing: border-box; +} + +.sidebar-left nav ul, +.sidebar-right nav ul { + display: flex; + flex-direction: column; + gap: 10px; + margin: 0; + padding: 0; +} + +.sidebar-left nav ul li, +.sidebar-right nav ul li { + width: 100%; +} + +.card { + color: #E5E5E5; + box-shadow: 0 4px 30px 0 rgba(0, 0, 0, 0.2); + background-color: #3C1360; + border-color: #B86ADE; +} + +.card-label { + color: #9B1C96; +} + +.card-footer { + color: #B86ADE; +} + + + +a.user-link, button.user-link { + background-color: #FFD600; + color: #3C1360; + border-color: #FFD600; + transition: background-color 0.3s, color 0.3s, border-color 0.3s; +} + +a.user-link:hover, button.user-link:hover { + background-color: #FFB600; + border-color: #FFB600; + color: #3C1360; + cursor: pointer; +} + +a.user-link:focus, button.user-link:focus { + background-color: #9A2F2F; + border-color: #9A2F2F; + color: #E5E5E5; +} + +.date-link { + background-color: #2F3C32; + color: #fff; +} + +.date-link:hover { + background-color: #3E4A3D; + color: #fff; +} + +.activitySpreadInhabitant2 { + background-color: #3E4A3D; + color: #fff; +} + +.activityVotePost { + background-color: #3B5C42; + color: #fff; +} + +.update-banner { + background-color: #1a0025; + border-bottom-color: #3d004d; + color: #E5E5E5; +} + +.update-banner-link { + color: #c44bc1; +} + +.snh-invite-code { + color: #9B1C96 !important; +} + +.stats-kpi-label { color: #B86ADE !important; } +.stats-kpi-value { color: #FFEEDB !important; } +.carbon-bar-note, .carbon-bar-formula { color: #FFEEDB !important; } +.graphos-node-label { fill: #FFEEDB !important; } +.graphos-node-label-me { fill: #5dade2 !important; } +.graphos-legend { color: #FFEEDB !important; } +/* Blockexplorer */ +.blockchain-view { background-color: #2D0B47 !important; color: #FFEEDB !important; } +.block { background: #3C1360 !important; border: 1px solid #B86ADE !important; } +.block:hover { box-shadow: 0 0 15px rgba(184,106,222,0.3) !important; } +.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #B86ADE !important; } +.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #FFEEDB !important; } +.block-content-preview, .block-content { background: #4B1A72 !important; color: #FFEEDB !important; } +.block-author { color: #FFD600 !important; background: rgba(255,214,0,0.08) !important; } +.block-author:hover { color: #FFB600 !important; } +.block-url { color: #B86ADE !important; } +.block-row--details .block-url { background: #4B1A72 !important; } +.block-row--details .block-url:hover { background: #5A1A85 !important; color: #FFD600 !important; } +.btn-singleview { background: #4B1A72 !important; color: #B86ADE !important; } +.btn-singleview:hover { background: #5A1A85 !important; color: #FFD600 !important; } +.btn-spread-on, .content-actions .btn-pin-on { background: #B86ADE !important; color: #2A0E42 !important; } +.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #CE8CE9 !important; color: #2A0E42 !important; } +.btn-singleview.btn-content { background: #B86ADE !important; color: #4B1A72 !important; border: 1px solid #B86ADE !important; } +.btn-singleview.btn-content:hover { background: #FFD600 !important; color: #4B1A72 !important; border-color: #FFD600 !important; } +.btn-back { background: #A34AD8 !important; color: #FFFFFF !important; } +.btn-back:hover { background: #751E9F !important; color: #FFFFFF !important; } +.block-info-table td, .pm-info-table td { border-color: #B86ADE !important; } +.block-diagram { border-color: #B86ADE !important; background: #2D0B47 !important; } +.block-diagram-ruler { color: #B86ADE !important; background: #1A0030 !important; border-bottom-color: #B86ADE !important; } +.block-diagram-ruler span { color: #B86ADE !important; } +.block-diagram-cell { border-color: #B86ADE !important; background: #3C1360 !important; } +.bd-label { color: #B86ADE !important; } +.bd-value { color: #FFEEDB !important; } +.deleted-label { color: #ff5555 !important; } + +/* Tribes */ +.tribe-card { background: #3C1360 !important; border-color: #B86ADE !important; } +.tribe-card:hover { box-shadow: 0 0 15px rgba(184,106,222,0.3) !important; } +.tribe-card-title { color: #FFEEDB !important; } +.tribe-card-description { color: #FFEEDB !important; } +.tribe-info-table td { border-color: #B86ADE !important; } +.tribe-info-label { color: #B86ADE !important; background: #3C1360 !important; } +.tribe-info-value { color: #FFEEDB !important; background: #3C1360 !important; } +.tribe-info-empty { color: #8844aa !important; } +.tribe-card-members { border-color: #B86ADE !important; background: #2D0B47 !important; } +.tribe-members-count { color: #FFD600 !important; } +.tribe-card-actions { border-color: #B86ADE !important; background: #2D0B47 !important; } +.tribe-action-btn { border-color: #B86ADE !important; color: #B86ADE !important; } +button.tribe-action-btn { background: transparent !important; } +.tribe-action-btn:hover { background: #B86ADE !important; color: #000 !important; } +.tribe-subtribe-link { background: #4B1A72 !important; border-color: #B86ADE !important; color: #FFD600 !important; } +.tribe-thumb-link { border-color: #B86ADE !important; } +.tribe-thumb-link:hover { border-color: #FFD600 !important; } +.tribe-subtribe-link:hover { background: #5A1A85 !important; } +.tribe-parent-image { border-color: #B86ADE !important; } +.tribe-parent-box { background: #3C1360 !important; } +.trending-card.own-content, .addr-card.ubi-pub-card.own-content { box-shadow: inset 3px 0 0 #B86ADE; } +.map-stage { background: #2a0640 !important; border-color: #9B1C96 !important; } +.map-label-country { fill: #E5E5E5; stroke: #2a0640; } +.map-place-dot { fill: #ffffff; stroke: #2a0640; } +.map-place-capital { fill: #B86ADE; } +.map-place-label { fill: #E5E5E5; stroke: #2a0640; } +.map-pin-focus .map-pin-body, .map-cluster-dot { fill: #B86ADE; } +.map-pin-focus .map-pin-label-bg { stroke: #B86ADE; } +.map-pins-layer a:hover .map-pin-body, .map-pins a:hover .map-pin-body { fill: #B86ADE; } +.map-cluster-ring { fill: rgba(184, 106, 222, 0.28); } +.map-pick-ring, .map-pick-cross { stroke: #B86ADE; } +.map-scale-text { stroke: #2a0640; } +.map-attrib { fill: #9B7CAA; } +.map-ctrl { background: rgba(43, 10, 70, 0.9) !important; color: #E5E5E5 !important; border-color: #9B1C96 !important; } +.map-ctrl:hover { background: #B86ADE !important; color: #000000 !important; border-color: #B86ADE !important; } +.map-ctrl-off:hover { background: rgba(43, 10, 70, 0.9) !important; color: #E5E5E5 !important; border-color: #9B1C96 !important; } +.map-pick-cell, .map-pick-cell:hover { border: 0 !important; padding: 0 !important; border-radius: 0 !important; box-shadow: none !important; } +.map-pick-cell { background: transparent !important; } +.map-pick-cell:hover { background: rgba(184, 106, 222, 0.22) !important; outline-color: rgba(184, 106, 222, 0.7); } +.map-place-result, .map-center-link { color: #E5E5E5 !important; border-color: #9B1C96 !important; } +.map-place-result:hover, .map-center-link:hover { background: #B86ADE !important; color: #000000 !important; border-color: #B86ADE !important; } +.map-picked { background: #3C1360 !important; color: #E5E5E5 !important; } +.map-picked-near { color: #C8C8C8 !important; } +.map-markers-list .map-marker-row { border-color: #5A1A85 !important; } +.map-markers-list .map-marker-row--focus { background: rgba(184, 106, 222, 0.1) !important; box-shadow: inset 3px 0 0 #B86ADE !important; } +.map-marker-row--focus .map-marker-idx { background: #B86ADE !important; color: #000000 !important; } +.map-marker-label { color: #B86ADE !important; } +.map-marker-date { color: #C8C8C8 !important; } +.map-popup-pointer { fill: #3C1360; stroke: #9B1C96; } +.map-stage .map-popup-card { background: #3C1360; border-color: #9B1C96; color: #E5E5E5; } +.map-stage .map-popup-text { color: #E5E5E5; } +.map-popup-coords { color: #ffffff; } +.map-stage .map-popup-meta { color: #C8C8C8; } +.map-popup-close { color: #C8C8C8; } +.map-popup-close:hover { color: #B86ADE; } +.oasis-pager-info { color: #9B7CAA; } +.oasis-pager-btn { background: #39006D; color: #C8C8C8; border: 1px solid #9B1C96; } +.oasis-pager-btn:hover, .oasis-pager-btn.active { border-color: #E5E5E5; color: #E5E5E5; } diff --git a/src/client/assets/translations/i18n.js b/src/client/assets/translations/i18n.js index 129e820..34ad3a0 100644 --- a/src/client/assets/translations/i18n.js +++ b/src/client/assets/translations/i18n.js @@ -1,15 +1,27 @@ const path = require('path'); -let i18n = {}; -const languages = ['en', 'es', 'fr', 'eu', 'de', 'it', 'pt', 'zh', 'ar', 'hi', 'ru']; +const i18n = {}; +const languages = ['en', 'es', 'ca', 'gl', 'fr', 'eu', 'de', 'it', 'pt', 'zh', 'ar', 'hi', 'ru']; languages.forEach(language => { - try { - const languagePath = path.join(__dirname, `oasis_${language}.js`); - const languageData = require(languagePath); - i18n[language] = languageData[language]; - } catch (error) { - console.error(`Failed to load language file for ${language}:`, error); - } + Object.defineProperty(i18n, language, { + enumerable: true, + configurable: true, + get() { + let languageData; + try { + languageData = require(path.join(__dirname, `oasis_${language}.js`))[language]; + } catch (error) { + console.error(`Failed to load language file for ${language}:`, error); + } + Object.defineProperty(i18n, language, { value: languageData, enumerable: true, configurable: true, writable: true }); + return languageData; + } + }); }); +try { + const overlay = require('./i18n_fork.js'); + languages.forEach(l => Object.assign(i18n[l] = i18n[l] || {}, overlay[l] || overlay.en || {})); +} catch (e) {} + module.exports = i18n; diff --git a/src/client/assets/translations/i18n_fork.js b/src/client/assets/translations/i18n_fork.js new file mode 100644 index 0000000..f5f3904 --- /dev/null +++ b/src/client/assets/translations/i18n_fork.js @@ -0,0 +1,1059 @@ +// Claves de traduccion propias de la rama experimental. +// Se fusionan sobre las de upstream desde i18n.js, para no editar los 11 +// ficheros oasis_*.js (upstream los reescribe entero en cada release). +// +// Contiene tres grupos: +// 1. Claves que anade la rama: el selector de identidades, +// la barra inferior (bb*) y el filtro Personal/Social de la topbar. +// 2. Claves que upstream tenia en 0.9.1 y elimino en 0.9.5, pero que el +// codigo de la rama sigue usando (menuBlogs, spreadHint, chatShareUrl...). +// 3. Textos de upstream que la rama corrige (el grupo spread en castellano). +// +// Los nombres de las categorias NO se tocan: son los de epsylon y se leen de +// upstream, para que la rama no se separe de el en algo tan visible. +// +// Los 11 idiomas estan completos. Si anades una clave, anadela en los 11. + + +module.exports = { + en: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "Menu", + bbMenuClose: "Close", + uxPickHint: "Tap one and it is saved. You can change it any time in Settings.", + uxPickCurrent: "In use", + welcomeStepUseMobile: "Profile", + welcomeHintUseMobile: "What you use Oasis for. It turns on the modules you need.", + wfHint_activists: "Assemblies, tribes, votes, forums and shared documents", + wfHint_acampada: "Just what a camp needs: chats, rooms, committees and voting", + wfHint_full: "All modules", + wfHint_default: "All modules", + wfHint_social: "Feed, photos, events and friends", + wfHint_kids: "School, games and making things", + wfHint_night: "Blogs, music, videos and talk", + wfHint_jobs: "Work, projects, market and office", + wfHint_business: "Economy, banking, office and documents", + wfHint_mobile: "The basics for a phone", + workflow_acampada: "Camp", + tribeSectionsMore: "More", + tribeSectionsLabel: "Sections", + voteYes: "Yes", + voteNo: "No", + voteAbstention: "Abstain", + voteGreen: "Green · agree", + voteYellow: "Yellow · doubts", + voteRed: "Red · against", + voteOwnNote: "You created this vote, so you cannot vote in it: whoever proposes stays neutral.", + voteKindLabel: "Type of vote", + voteKind_classic: "Yes / No", + voteKindHint_classic: "Yes, no, abstain and the usual options", + voteKind_traffic: "Traffic light", + voteKindHint_traffic: "Green agree · yellow doubts · red against, like in the assembly", + voteKind_scale: "Thermometer", + voteKindHint_scale: "From 1 (cold) to 5 (hot)", + deleteConfirmQuestion: "Delete it for good?", + deleteConfirmYes: "Yes, delete", + welcomeBannerTextMobile: "Add your name and photo so people know it is you.", + welcomeBannerActionMobile: "Set up", + footerPeersNone: "Not syncing with anyone yet", + footerPeersSome: "Syncing with {n}", + tribeMapUrlLabel: "Map link (optional)", + tribeHintPrivacy: "Private: only members see it, and it is encrypted. Public: the whole network sees it.", + tribeHintMode: "Open: any member can invite people. Strict: only whoever created it can invite.", + moduleBlockedText: "{name} is turned off.", + moduleBlockedAction: "Turn on {name}", + modulesOnCount: "{on} of {total} sections on", + modulesSaved: "Saved.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "Pick how Oasis looks on your phone. It applies as soon as you tap it.", + workflowsDescriptionMobile: "A workflow sets which modules are on. On the phone your skin stays as it is.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "This image could not be cleaned of its metadata (location, camera…), so it has not been uploaded. Save it as JPEG or PNG and try again.", + // Identidades + identitiesActive: "In use", + identitiesBackup: "Export or import an identity", + identitiesCreate: "Create identity", + identitiesDescription: "Each identity lives in its own folder, with its own key, database and blobs. The change applies on restart.", + identitiesInvalidName: "Invalid name. Letters, numbers, hyphen and underscore, up to 32.", + identitiesNewName: "Name for the new identity", + identitiesRestart: "Restart now", + identitiesRestartNeeded: "Selected. Restart Oasis to enter with this identity.", + identitiesRestarting: "Restarting. Open Oasis again in a few seconds.", + identitiesSameFeed: "Shares key with another identity: never publish from both.", + identitiesTitle: "Identities", + identitiesUse: "Use this one", + // Barra inferior + bbAdd: "Add", + bbDone: "Done", + bbFull: "Maximum reached", + bbManage: "Edit", + bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.", + bbPickNone: "None", + bbPickTitle: "Customize the bar", + bbQuickActions: "Quick actions", + bbRemove: "Remove", + bbYourBar: "Your bar", + // Menu y filtros + menuAll: "All", + menuBlogs: "Blogs", + menuSocial: "Social", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "Category", + bookmarkCategoryPlaceholder: "Optional", + chatReply: "Reply", + chatShareUrl: "Share URL", + filter: "Filter", + forumFilterHot: "Hot", + peerLastChange: "Last change", + profileVisibilityDevice: "Device", + publishBlog: "Publish Blog", + spreadHint: "Spread this to your supporters (replicates via your feed).", + spreadMore: "more" + }, + es: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "Menú", + bbMenuClose: "Cerrar", + uxPickHint: "Toca uno y queda guardado. Puedes cambiarlo cuando quieras en Ajustes.", + uxPickCurrent: "En uso", + welcomeStepUseMobile: "Perfil", + welcomeHintUseMobile: "Para qué usas Oasis. Enciende los módulos que hacen falta.", + wfHint_activists: "Asambleas, tribus, votaciones, foros y documentos compartidos", + wfHint_acampada: "Lo justo para una acampada: chats, salas, comisiones y votar", + wfHint_full: "Todos los módulos", + wfHint_default: "Todos los módulos", + wfHint_social: "Feed, fotos, eventos y amistades", + wfHint_kids: "Escuela, juegos y hacer cosas", + wfHint_night: "Blogs, música, vídeos y charla", + wfHint_jobs: "Trabajo, proyectos, mercado y oficina", + wfHint_business: "Economía, banca, oficina y documentos", + wfHint_mobile: "Lo básico para el móvil", + workflow_acampada: "Acampada", + tribeSectionsMore: "Más", + tribeSectionsLabel: "Secciones", + voteYes: "Sí", + voteNo: "No", + voteAbstention: "Abstención", + voteGreen: "Verde · de acuerdo", + voteYellow: "Amarillo · dudas", + voteRed: "Rojo · en contra", + voteOwnNote: "Has creado esta votación, así que no puedes votar en ella: quien propone se queda neutral.", + voteKindLabel: "Tipo de votación", + voteKind_classic: "Sí / No", + voteKindHint_classic: "Sí, no, abstención y las opciones de siempre", + voteKind_traffic: "Semáforo", + voteKindHint_traffic: "Verde de acuerdo · amarillo dudas · rojo en contra, como en asamblea", + voteKind_scale: "Termómetro", + voteKindHint_scale: "Del 1 (frío) al 5 (caliente)", + deleteConfirmQuestion: "¿Borrar para siempre?", + deleteConfirmYes: "Sí, borrar", + welcomeBannerTextMobile: "Pon tu nombre y tu foto para que te reconozcan.", + welcomeBannerActionMobile: "Ponerlos", + footerPeersNone: "Aún no sincronizas con nadie", + footerPeersSome: "Sincronizando con {n}", + tribeMapUrlLabel: "Enlace del mapa (opcional)", + tribeHintPrivacy: "Privada: solo la ven sus miembros y va cifrada. Pública: la ve toda la red.", + tribeHintMode: "Abierta: cualquier miembro puede invitar. Estricta: solo invita quien la creó.", + moduleBlockedText: "{name} está desactivado.", + moduleBlockedAction: "Activar {name}", + modulesOnCount: "{on} de {total} secciones activas", + modulesSaved: "Guardado.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "Elige cómo se ve Oasis en tu móvil. Se aplica al tocarlo.", + workflowsDescriptionMobile: "Un workflow fija qué módulos están activos. En el móvil tu skin se queda como está.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "No se han podido quitar los metadatos de esta imagen (ubicación, cámara…), así que no se ha subido. Guárdala como JPEG o PNG y vuelve a intentarlo.", + // Identidades + identitiesActive: "En uso", + identitiesBackup: "Exportar o importar una identidad", + identitiesCreate: "Crear identidad", + identitiesDescription: "Cada identidad vive en su propia carpeta, con su clave, su base de datos y sus blobs. El cambio se aplica al reiniciar.", + identitiesInvalidName: "Nombre no válido. Letras, números, guion y guion bajo, hasta 32.", + identitiesNewName: "Nombre de la nueva identidad", + identitiesRestart: "Reiniciar ahora", + identitiesRestartNeeded: "Seleccionada. Reinicia Oasis para entrar con esta identidad.", + identitiesRestarting: "Reiniciando. Vuelve a abrir Oasis en unos segundos.", + identitiesSameFeed: "Comparte clave con otra identidad: no publiques nunca desde las dos.", + identitiesTitle: "Identidades", + identitiesUse: "Usar esta", + // Barra inferior + bbAdd: "Añadir", + bbDone: "Hecho", + bbFull: "Máximo alcanzado", + bbManage: "Editar", + bbPickDesc: "Elige hasta 4 accesos directos para la barra inferior.", + bbPickNone: "Ninguno", + bbPickTitle: "Personalizar la barra", + bbQuickActions: "Acciones rápidas", + bbRemove: "Quitar", + bbYourBar: "Tu barra", + // Menu y filtros + menuAll: "Todo", + menuBlogs: "Blogs", + menuSocial: "Social", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "Categoría", + bookmarkCategoryPlaceholder: "Opcional", + chatReply: "Responder", + chatShareUrl: "Compartir enlace", + filter: "Filtrar", + forumFilterHot: "Candente", + peerLastChange: "Último cambio", + profileVisibilityDevice: "Dispositivo", + publishBlog: "Publicar blog", + spreadHint: "Difunde esto entre quienes te siguen (se replica por tu feed).", + spreadMore: "más", + // Correcciones al castellano de upstream + spreadChron: "Difusión", + spreadLabel: "Difundir", + spreaded: "Difundido", + spreadedDescription: "Lista de posts replicados por habitante.", + totalspreads: "Réplicas totales" + }, + fr: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "Menu", + bbMenuClose: "Fermer", + uxPickHint: "Touchez-en un et c'est enregistré. Vous pouvez le changer à tout moment dans les Paramètres.", + uxPickCurrent: "Actuel", + welcomeStepUseMobile: "Profil", + welcomeHintUseMobile: "À quoi vous sert Oasis. Active les modules nécessaires.", + wfHint_activists: "Assemblées, tribus, votes, forums et documents partagés", + wfHint_acampada: "Le nécessaire pour un campement : chats, salons, commissions et votes", + wfHint_full: "Tous les modules", + wfHint_default: "Tous les modules", + wfHint_social: "Fil, photos, événements et amis", + wfHint_kids: "École, jeux et création", + wfHint_night: "Blogs, musique, vidéos et discussions", + wfHint_jobs: "Travail, projets, marché et bureau", + wfHint_business: "Économie, banque, bureau et documents", + wfHint_mobile: "L'essentiel pour le téléphone", + workflow_acampada: "Campement", + tribeSectionsMore: "Plus", + tribeSectionsLabel: "Sections", + voteYes: "Oui", + voteNo: "Non", + voteAbstention: "Abstention", + voteGreen: "Vert · d'accord", + voteYellow: "Jaune · doutes", + voteRed: "Rouge · contre", + voteOwnNote: "Vous avez créé ce vote, vous ne pouvez donc pas y voter : qui propose reste neutre.", + voteKindLabel: "Type de vote", + voteKind_classic: "Oui / Non", + voteKindHint_classic: "Oui, non, abstention et les options habituelles", + voteKind_traffic: "Feu tricolore", + voteKindHint_traffic: "Vert d'accord · jaune doutes · rouge contre, comme en assemblée", + voteKind_scale: "Thermomètre", + voteKindHint_scale: "De 1 (froid) à 5 (chaud)", + deleteConfirmQuestion: "Supprimer définitivement ?", + deleteConfirmYes: "Oui, supprimer", + welcomeBannerTextMobile: "Ajoutez votre nom et votre photo pour qu'on vous reconnaisse.", + welcomeBannerActionMobile: "Ajouter", + footerPeersNone: "Aucune synchronisation pour l'instant", + footerPeersSome: "Synchronisé avec {n}", + tribeMapUrlLabel: "Lien de la carte (facultatif)", + tribeHintPrivacy: "Privée : seuls ses membres la voient, et elle est chiffrée. Publique : tout le réseau la voit.", + tribeHintMode: "Ouverte : tout membre peut inviter. Stricte : seule la personne qui l'a créée invite.", + moduleBlockedText: "{name} est désactivé.", + moduleBlockedAction: "Activer {name}", + modulesOnCount: "{on} sections actives sur {total}", + modulesSaved: "Enregistré.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "Choisis l'apparence d'Oasis sur ton téléphone. Elle s'applique dès que tu touches.", + workflowsDescriptionMobile: "Un workflow fixe les modules actifs. Sur le téléphone, ton thème ne change pas.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Impossible de retirer les métadonnées de cette image (position, appareil…), elle n'a donc pas été envoyée. Enregistrez-la en JPEG ou PNG et réessayez.", + // Identidades + identitiesActive: "En cours d'utilisation", + identitiesBackup: "Exporter ou importer une identité", + identitiesCreate: "Créer une identité", + identitiesDescription: "Chaque identité vit dans son propre dossier, avec sa clé, sa base de données et ses blobs. Le changement s'applique au redémarrage.", + identitiesInvalidName: "Nom invalide. Lettres, chiffres, tiret et tiret bas, jusqu'à 32.", + identitiesNewName: "Nom de la nouvelle identité", + identitiesRestart: "Redémarrer maintenant", + identitiesRestartNeeded: "Sélectionnée. Redémarrez Oasis pour entrer avec cette identité.", + identitiesRestarting: "Redémarrage. Rouvrez Oasis dans quelques secondes.", + identitiesSameFeed: "Partage sa clé avec une autre identité : ne publiez jamais depuis les deux.", + identitiesTitle: "Identités", + identitiesUse: "Utiliser celle-ci", + // Barra inferior + bbAdd: "Ajouter", + bbDone: "Terminé", + bbFull: "Maximum atteint", + bbManage: "Modifier", + bbPickDesc: "Choisissez jusqu'à 4 raccourcis pour la barre du bas.", + bbPickNone: "Aucun", + bbPickTitle: "Personnaliser la barre", + bbQuickActions: "Actions rapides", + bbRemove: "Retirer", + bbYourBar: "Votre barre", + // Menu y filtros + menuAll: "Tout", + menuBlogs: "Blogs", + menuSocial: "Social", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "Catégorie", + bookmarkCategoryPlaceholder: "Facultatif", + chatReply: "Répondre", + chatShareUrl: "Partager le lien", + filter: "Filtrer", + forumFilterHot: "Populaire", + peerLastChange: "Dernier changement", + profileVisibilityDevice: "Appareil", + publishBlog: "Publier le blog", + spreadHint: "Diffusez ceci auprès de vos soutiens (se réplique via votre flux).", + spreadMore: "plus" + }, + eu: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "Menua", + bbMenuClose: "Itxi", + uxPickHint: "Bat ukitu eta gordeta geratzen da. Nahi duzunean alda dezakezu Ezarpenetan.", + uxPickCurrent: "Erabiltzen", + welcomeStepUseMobile: "Profila", + welcomeHintUseMobile: "Oasis zertarako erabiltzen duzun. Behar diren moduluak pizten ditu.", + wfHint_activists: "Batzarrak, tribuak, bozketak, foroak eta dokumentu partekatuak", + wfHint_acampada: "Kanpaldi baterako behar dena: txatak, gelak, batzordeak eta bozkatzea", + wfHint_full: "Modulu guztiak", + wfHint_default: "Modulu guztiak", + wfHint_social: "Jarioa, argazkiak, ekitaldiak eta lagunak", + wfHint_kids: "Eskola, jolasak eta sortzea", + wfHint_night: "Blogak, musika, bideoak eta solasa", + wfHint_jobs: "Lana, proiektuak, merkatua eta bulegoa", + wfHint_business: "Ekonomia, banka, bulegoa eta dokumentuak", + wfHint_mobile: "Mugikorrerako oinarrizkoa", + workflow_acampada: "Kanpaldia", + tribeSectionsMore: "Gehiago", + tribeSectionsLabel: "Atalak", + voteYes: "Bai", + voteNo: "Ez", + voteAbstention: "Abstentzioa", + voteGreen: "Berdea · ados", + voteYellow: "Horia · zalantzak", + voteRed: "Gorria · aurka", + voteOwnNote: "Bozketa hau zuk sortu duzu, beraz ezin duzu bozkatu: proposatzen duena neutral geratzen da.", + voteKindLabel: "Bozketa mota", + voteKind_classic: "Bai / Ez", + voteKindHint_classic: "Bai, ez, abstentzioa eta ohiko aukerak", + voteKind_traffic: "Semaforoa", + voteKindHint_traffic: "Berdea ados · horia zalantzak · gorria aurka, batzarrean bezala", + voteKind_scale: "Termometroa", + voteKindHint_scale: "1etik (hotza) 5era (beroa)", + deleteConfirmQuestion: "Betiko ezabatu?", + deleteConfirmYes: "Bai, ezabatu", + welcomeBannerTextMobile: "Jarri zure izena eta argazkia ezagutu zaitzaten.", + welcomeBannerActionMobile: "Jarri", + footerPeersNone: "Oraindik ez zaude inorekin sinkronizatzen", + footerPeersSome: "{n}-rekin sinkronizatzen", + tribeMapUrlLabel: "Maparen esteka (aukerakoa)", + tribeHintPrivacy: "Pribatua: kideek bakarrik ikusten dute, eta zifratuta dago. Publikoa: sare osoak ikusten du.", + tribeHintMode: "Irekia: edozein kidek gonbida dezake. Zorrotza: sortu zuenak bakarrik gonbidatzen du.", + moduleBlockedText: "{name} desaktibatuta dago.", + moduleBlockedAction: "Aktibatu {name}", + modulesOnCount: "{total} ataletatik {on} aktibo", + modulesSaved: "Gordeta.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "Aukeratu nolakoa den Oasis zure mugikorrean. Ukitu bezain laster aplikatzen da.", + workflowsDescriptionMobile: "Workflow batek zein modulu dauden aktibo finkatzen du. Mugikorrean zure itxura ez da aldatzen.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Ezin izan dira irudi honen metadatuak kendu (kokapena, kamera…), beraz ez da igo. Gorde JPEG edo PNG gisa eta saiatu berriro.", + // Identidades + identitiesActive: "Erabiltzen", + identitiesBackup: "Esportatu edo inportatu nortasun bat", + identitiesCreate: "Sortu nortasuna", + identitiesDescription: "Nortasun bakoitza bere karpetan bizi da, bere gakoarekin, datu-basearekin eta blob-ekin. Aldaketa berrabiaraztean aplikatzen da.", + identitiesInvalidName: "Izen baliogabea. Letrak, zenbakiak, marratxoa eta azpimarra, 32 arte.", + identitiesNewName: "Nortasun berriaren izena", + identitiesRestart: "Berrabiarazi orain", + identitiesRestartNeeded: "Hautatuta. Berrabiarazi Oasis nortasun honekin sartzeko.", + identitiesRestarting: "Berrabiarazten. Ireki Oasis berriro segundo batzuk barru.", + identitiesSameFeed: "Beste nortasun batekin gakoa partekatzen du: ez argitaratu inoiz bietatik.", + identitiesTitle: "Nortasunak", + identitiesUse: "Erabili hau", + // Barra inferior + bbAdd: "Gehitu", + bbDone: "Eginda", + bbFull: "Gehienekora iritsi zara", + bbManage: "Editatu", + bbPickDesc: "Aukeratu 4 lasterbide arte beheko barrarako.", + bbPickNone: "Bat ere ez", + bbPickTitle: "Pertsonalizatu barra", + bbQuickActions: "Ekintza bizkorrak", + bbRemove: "Kendu", + bbYourBar: "Zure barra", + // Menu y filtros + menuAll: "Dena", + menuBlogs: "Blogak", + menuSocial: "Soziala", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "Kategoria", + bookmarkCategoryPlaceholder: "Aukerakoa", + chatReply: "Erantzun", + chatShareUrl: "Partekatu esteka", + filter: "Iragazi", + forumFilterHot: "Bero", + peerLastChange: "Azken aldaketa", + profileVisibilityDevice: "Gailua", + publishBlog: "Argitaratu bloga", + spreadHint: "Zabaldu hau zure jarraitzaileen artean (zure jarioaren bidez errepikatzen da).", + spreadMore: "gehiago" + }, + de: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "Menü", + bbMenuClose: "Schließen", + uxPickHint: "Tippe eins an und es ist gespeichert. Du kannst es jederzeit in den Einstellungen ändern.", + uxPickCurrent: "Aktiv", + welcomeStepUseMobile: "Profil", + welcomeHintUseMobile: "Wofür du Oasis nutzt. Schaltet die nötigen Module ein.", + wfHint_activists: "Versammlungen, Stämme, Abstimmungen, Foren und gemeinsame Dokumente", + wfHint_acampada: "Was ein Camp braucht: Chats, Räume, Arbeitsgruppen und Abstimmen", + wfHint_full: "Alle Module", + wfHint_default: "Alle Module", + wfHint_social: "Feed, Fotos, Termine und Freunde", + wfHint_kids: "Schule, Spiele und Selbermachen", + wfHint_night: "Blogs, Musik, Videos und Gespräche", + wfHint_jobs: "Arbeit, Projekte, Markt und Büro", + wfHint_business: "Wirtschaft, Bank, Büro und Dokumente", + wfHint_mobile: "Das Wichtigste fürs Handy", + workflow_acampada: "Camp", + tribeSectionsMore: "Mehr", + tribeSectionsLabel: "Bereiche", + voteYes: "Ja", + voteNo: "Nein", + voteAbstention: "Enthaltung", + voteGreen: "Grün · einverstanden", + voteYellow: "Gelb · Zweifel", + voteRed: "Rot · dagegen", + voteOwnNote: "Du hast diese Abstimmung erstellt und kannst daher nicht abstimmen: wer vorschlägt, bleibt neutral.", + voteKindLabel: "Art der Abstimmung", + voteKind_classic: "Ja / Nein", + voteKindHint_classic: "Ja, nein, Enthaltung und die üblichen Optionen", + voteKind_traffic: "Ampel", + voteKindHint_traffic: "Grün einverstanden · gelb Zweifel · rot dagegen, wie im Plenum", + voteKind_scale: "Thermometer", + voteKindHint_scale: "Von 1 (kalt) bis 5 (heiß)", + deleteConfirmQuestion: "Endgültig löschen?", + deleteConfirmYes: "Ja, löschen", + welcomeBannerTextMobile: "Füge deinen Namen und dein Foto hinzu, damit man dich erkennt.", + welcomeBannerActionMobile: "Einrichten", + footerPeersNone: "Noch mit niemandem synchronisiert", + footerPeersSome: "Synchronisiert mit {n}", + tribeMapUrlLabel: "Kartenlink (optional)", + tribeHintPrivacy: "Privat: nur Mitglieder sehen sie, und sie ist verschlüsselt. Öffentlich: das ganze Netz sieht sie.", + tribeHintMode: "Offen: jedes Mitglied kann einladen. Streng: nur wer sie erstellt hat, lädt ein.", + moduleBlockedText: "{name} ist ausgeschaltet.", + moduleBlockedAction: "{name} einschalten", + modulesOnCount: "{on} von {total} Bereichen aktiv", + modulesSaved: "Gespeichert.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "Wähle, wie Oasis auf deinem Handy aussieht. Es wird beim Antippen übernommen.", + workflowsDescriptionMobile: "Ein Workflow legt fest, welche Module aktiv sind. Auf dem Handy bleibt dein Skin, wie er ist.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Die Metadaten dieses Bildes (Standort, Kamera…) konnten nicht entfernt werden, daher wurde es nicht hochgeladen. Speichere es als JPEG oder PNG und versuche es erneut.", + // Identidades + identitiesActive: "In Benutzung", + identitiesBackup: "Identität exportieren oder importieren", + identitiesCreate: "Identität erstellen", + identitiesDescription: "Jede Identität liegt in einem eigenen Ordner, mit eigenem Schlüssel, eigener Datenbank und eigenen Blobs. Die Änderung greift beim Neustart.", + identitiesInvalidName: "Ungültiger Name. Buchstaben, Ziffern, Bindestrich und Unterstrich, bis zu 32.", + identitiesNewName: "Name der neuen Identität", + identitiesRestart: "Jetzt neu starten", + identitiesRestartNeeded: "Ausgewählt. Starte Oasis neu, um mit dieser Identität einzutreten.", + identitiesRestarting: "Neustart. Öffne Oasis in ein paar Sekunden erneut.", + identitiesSameFeed: "Teilt den Schlüssel mit einer anderen Identität: niemals von beiden veröffentlichen.", + identitiesTitle: "Identitäten", + identitiesUse: "Diese verwenden", + // Barra inferior + bbAdd: "Hinzufügen", + bbDone: "Fertig", + bbFull: "Maximum erreicht", + bbManage: "Bearbeiten", + bbPickDesc: "Wähle bis zu 4 Verknüpfungen für die untere Leiste.", + bbPickNone: "Keine", + bbPickTitle: "Leiste anpassen", + bbQuickActions: "Schnellzugriff", + bbRemove: "Entfernen", + bbYourBar: "Deine Leiste", + // Menu y filtros + menuAll: "Alle", + menuBlogs: "Blogs", + menuSocial: "Sozial", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "Kategorie", + bookmarkCategoryPlaceholder: "Optional", + chatReply: "Antworten", + chatShareUrl: "Link teilen", + filter: "Filtern", + forumFilterHot: "Angesagt", + peerLastChange: "Letzte Änderung", + profileVisibilityDevice: "Gerät", + publishBlog: "Blog veröffentlichen", + spreadHint: "Verbreite dies an deine Unterstützer (repliziert über deinen Feed).", + spreadMore: "mehr" + }, + it: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "Menu", + bbMenuClose: "Chiudi", + uxPickHint: "Toccane uno ed è salvato. Puoi cambiarlo quando vuoi nelle Impostazioni.", + uxPickCurrent: "In uso", + welcomeStepUseMobile: "Profilo", + welcomeHintUseMobile: "A cosa ti serve Oasis. Attiva i moduli necessari.", + wfHint_activists: "Assemblee, tribù, votazioni, forum e documenti condivisi", + wfHint_acampada: "Il necessario per un accampamento: chat, stanze, commissioni e voto", + wfHint_full: "Tutti i moduli", + wfHint_default: "Tutti i moduli", + wfHint_social: "Feed, foto, eventi e amicizie", + wfHint_kids: "Scuola, giochi e creatività", + wfHint_night: "Blog, musica, video e chiacchiere", + wfHint_jobs: "Lavoro, progetti, mercato e ufficio", + wfHint_business: "Economia, banca, ufficio e documenti", + wfHint_mobile: "L'essenziale per il telefono", + workflow_acampada: "Accampata", + tribeSectionsMore: "Altro", + tribeSectionsLabel: "Sezioni", + voteYes: "Sì", + voteNo: "No", + voteAbstention: "Astensione", + voteGreen: "Verde · d'accordo", + voteYellow: "Giallo · dubbi", + voteRed: "Rosso · contrario", + voteOwnNote: "Hai creato tu questa votazione, quindi non puoi votare: chi propone resta neutrale.", + voteKindLabel: "Tipo di votazione", + voteKind_classic: "Sì / No", + voteKindHint_classic: "Sì, no, astensione e le opzioni di sempre", + voteKind_traffic: "Semaforo", + voteKindHint_traffic: "Verde d'accordo · giallo dubbi · rosso contrario, come in assemblea", + voteKind_scale: "Termometro", + voteKindHint_scale: "Da 1 (freddo) a 5 (caldo)", + deleteConfirmQuestion: "Eliminare per sempre?", + deleteConfirmYes: "Sì, elimina", + welcomeBannerTextMobile: "Aggiungi nome e foto perché ti riconoscano.", + welcomeBannerActionMobile: "Aggiungi", + footerPeersNone: "Non sincronizzi ancora con nessuno", + footerPeersSome: "In sincronia con {n}", + tribeMapUrlLabel: "Link della mappa (facoltativo)", + tribeHintPrivacy: "Privata: la vedono solo i membri ed è cifrata. Pubblica: la vede tutta la rete.", + tribeHintMode: "Aperta: qualsiasi membro può invitare. Rigida: invita solo chi l'ha creata.", + moduleBlockedText: "{name} è disattivato.", + moduleBlockedAction: "Attiva {name}", + modulesOnCount: "{on} sezioni attive su {total}", + modulesSaved: "Salvato.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "Scegli come appare Oasis sul tuo telefono. Si applica appena lo tocchi.", + workflowsDescriptionMobile: "Un workflow stabilisce quali moduli sono attivi. Sul telefono la tua skin resta com'è.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Non è stato possibile rimuovere i metadati di questa immagine (posizione, fotocamera…), quindi non è stata caricata. Salvala come JPEG o PNG e riprova.", + // Identidades + identitiesActive: "In uso", + identitiesBackup: "Esporta o importa un'identità", + identitiesCreate: "Crea identità", + identitiesDescription: "Ogni identità vive nella sua cartella, con la sua chiave, il suo database e i suoi blob. Il cambio si applica al riavvio.", + identitiesInvalidName: "Nome non valido. Lettere, numeri, trattino e trattino basso, fino a 32.", + identitiesNewName: "Nome della nuova identità", + identitiesRestart: "Riavvia ora", + identitiesRestartNeeded: "Selezionata. Riavvia Oasis per entrare con questa identità.", + identitiesRestarting: "Riavvio in corso. Riapri Oasis tra qualche secondo.", + identitiesSameFeed: "Condivide la chiave con un'altra identità: non pubblicare mai da entrambe.", + identitiesTitle: "Identità", + identitiesUse: "Usa questa", + // Barra inferior + bbAdd: "Aggiungi", + bbDone: "Fatto", + bbFull: "Massimo raggiunto", + bbManage: "Modifica", + bbPickDesc: "Scegli fino a 4 scorciatoie per la barra in basso.", + bbPickNone: "Nessuno", + bbPickTitle: "Personalizza la barra", + bbQuickActions: "Azioni rapide", + bbRemove: "Rimuovi", + bbYourBar: "La tua barra", + // Menu y filtros + menuAll: "Tutto", + menuBlogs: "Blog", + menuSocial: "Sociale", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "Categoria", + bookmarkCategoryPlaceholder: "Facoltativo", + chatReply: "Rispondi", + chatShareUrl: "Condividi link", + filter: "Filtra", + forumFilterHot: "Caldo", + peerLastChange: "Ultima modifica", + profileVisibilityDevice: "Dispositivo", + publishBlog: "Pubblica blog", + spreadHint: "Diffondi questo tra chi ti segue (si replica tramite il tuo feed).", + spreadMore: "altro" + }, + pt: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "Menu", + bbMenuClose: "Fechar", + uxPickHint: "Toque num e fica guardado. Pode mudá-lo quando quiser nas Definições.", + uxPickCurrent: "Em uso", + welcomeStepUseMobile: "Perfil", + welcomeHintUseMobile: "Para que usa o Oasis. Liga os módulos necessários.", + wfHint_activists: "Assembleias, tribos, votações, fóruns e documentos partilhados", + wfHint_acampada: "O necessário para um acampamento: chats, salas, comissões e votar", + wfHint_full: "Todos os módulos", + wfHint_default: "Todos os módulos", + wfHint_social: "Feed, fotos, eventos e amizades", + wfHint_kids: "Escola, jogos e criar coisas", + wfHint_night: "Blogues, música, vídeos e conversa", + wfHint_jobs: "Trabalho, projetos, mercado e escritório", + wfHint_business: "Economia, banca, escritório e documentos", + wfHint_mobile: "O básico para o telemóvel", + workflow_acampada: "Acampada", + tribeSectionsMore: "Mais", + tribeSectionsLabel: "Secções", + voteYes: "Sim", + voteNo: "Não", + voteAbstention: "Abstenção", + voteGreen: "Verde · de acordo", + voteYellow: "Amarelo · dúvidas", + voteRed: "Vermelho · contra", + voteOwnNote: "Criaste esta votação, por isso não podes votar nela: quem propõe fica neutro.", + voteKindLabel: "Tipo de votação", + voteKind_classic: "Sim / Não", + voteKindHint_classic: "Sim, não, abstenção e as opções de sempre", + voteKind_traffic: "Semáforo", + voteKindHint_traffic: "Verde de acordo · amarelo dúvidas · vermelho contra, como em assembleia", + voteKind_scale: "Termómetro", + voteKindHint_scale: "De 1 (frio) a 5 (quente)", + deleteConfirmQuestion: "Apagar para sempre?", + deleteConfirmYes: "Sim, apagar", + welcomeBannerTextMobile: "Põe o teu nome e a tua foto para que te reconheçam.", + welcomeBannerActionMobile: "Pôr", + footerPeersNone: "Ainda não sincronizas com ninguém", + footerPeersSome: "A sincronizar com {n}", + tribeMapUrlLabel: "Ligação do mapa (opcional)", + tribeHintPrivacy: "Privada: só os membros a veem e vai cifrada. Pública: toda a rede a vê.", + tribeHintMode: "Aberta: qualquer membro pode convidar. Estrita: só convida quem a criou.", + moduleBlockedText: "{name} está desativado.", + moduleBlockedAction: "Ativar {name}", + modulesOnCount: "{on} de {total} secções ativas", + modulesSaved: "Guardado.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "Escolhe como o Oasis aparece no teu telemóvel. Aplica-se ao tocar.", + workflowsDescriptionMobile: "Um workflow define que módulos estão ativos. No telemóvel o teu tema fica como está.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Não foi possível remover os metadados desta imagem (localização, câmara…), por isso não foi enviada. Guarda-a como JPEG ou PNG e tenta de novo.", + // Identidades + identitiesActive: "Em uso", + identitiesBackup: "Exportar ou importar uma identidade", + identitiesCreate: "Criar identidade", + identitiesDescription: "Cada identidade vive na sua própria pasta, com a sua chave, base de dados e blobs. A mudança aplica-se ao reiniciar.", + identitiesInvalidName: "Nome inválido. Letras, números, hífen e sublinhado, até 32.", + identitiesNewName: "Nome da nova identidade", + identitiesRestart: "Reiniciar agora", + identitiesRestartNeeded: "Selecionada. Reinicia o Oasis para entrar com esta identidade.", + identitiesRestarting: "A reiniciar. Volta a abrir o Oasis dentro de alguns segundos.", + identitiesSameFeed: "Partilha a chave com outra identidade: nunca publiques a partir das duas.", + identitiesTitle: "Identidades", + identitiesUse: "Usar esta", + // Barra inferior + bbAdd: "Adicionar", + bbDone: "Concluído", + bbFull: "Máximo atingido", + bbManage: "Editar", + bbPickDesc: "Escolhe até 4 atalhos para a barra inferior.", + bbPickNone: "Nenhum", + bbPickTitle: "Personalizar a barra", + bbQuickActions: "Ações rápidas", + bbRemove: "Remover", + bbYourBar: "A tua barra", + // Menu y filtros + menuAll: "Tudo", + menuBlogs: "Blogues", + menuSocial: "Social", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "Categoria", + bookmarkCategoryPlaceholder: "Opcional", + chatReply: "Responder", + chatShareUrl: "Partilhar ligação", + filter: "Filtrar", + forumFilterHot: "Em alta", + peerLastChange: "Última alteração", + profileVisibilityDevice: "Dispositivo", + publishBlog: "Publicar blogue", + spreadHint: "Difunde isto entre quem te segue (replica-se pelo teu feed).", + spreadMore: "mais" + }, + zh: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "菜单", + bbMenuClose: "关闭", + uxPickHint: "点一下即保存。随时可以在设置中更改。", + uxPickCurrent: "使用中", + welcomeStepUseMobile: "用途", + welcomeHintUseMobile: "你用 Oasis 做什么。会打开需要的模块。", + wfHint_activists: "集会、部落、投票、论坛和共享文档", + wfHint_acampada: "营地所需:聊天、房间、小组和投票", + wfHint_full: "全部模块", + wfHint_default: "全部模块", + wfHint_social: "动态、照片、活动和朋友", + wfHint_kids: "学校、游戏和创作", + wfHint_night: "博客、音乐、视频和闲聊", + wfHint_jobs: "工作、项目、市场和办公", + wfHint_business: "经济、银行、办公和文档", + wfHint_mobile: "手机基本功能", + workflow_acampada: "营地", + tribeSectionsMore: "更多", + tribeSectionsLabel: "板块", + voteYes: "赞成", + voteNo: "反对", + voteAbstention: "弃权", + voteGreen: "绿 · 同意", + voteYellow: "黄 · 有疑问", + voteRed: "红 · 反对", + voteOwnNote: "这是你发起的投票,所以你不能投票:发起人保持中立。", + voteKindLabel: "投票类型", + voteKind_classic: "是 / 否", + voteKindHint_classic: "赞成、反对、弃权及常用选项", + voteKind_traffic: "红绿灯", + voteKindHint_traffic: "绿同意 · 黄有疑问 · 红反对,和集会上一样", + voteKind_scale: "温度计", + voteKindHint_scale: "从 1(冷)到 5(热)", + deleteConfirmQuestion: "永久删除?", + deleteConfirmYes: "是,删除", + welcomeBannerTextMobile: "添加你的名字和照片,让大家认出你。", + welcomeBannerActionMobile: "设置", + footerPeersNone: "尚未与任何人同步", + footerPeersSome: "正在与 {n} 个节点同步", + tribeMapUrlLabel: "地图链接(可选)", + tribeHintPrivacy: "私密:只有成员能看到,并且加密。公开:整个网络都能看到。", + tribeHintMode: "开放:任何成员都可以邀请。严格:只有创建者可以邀请。", + moduleBlockedText: "{name} 已关闭。", + moduleBlockedAction: "开启 {name}", + modulesOnCount: "已开启 {on}/{total} 个板块", + modulesSaved: "已保存。", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "选择 Oasis 在手机上的外观。点一下就会生效。", + workflowsDescriptionMobile: "工作流决定启用哪些模块。在手机上,你的外观保持不变。", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "无法清除这张图片的元数据(位置、相机等),因此没有上传。请另存为 JPEG 或 PNG 后重试。", + // Identidades + identitiesActive: "使用中", + identitiesBackup: "导出或导入身份", + identitiesCreate: "创建身份", + identitiesDescription: "每个身份都有自己的文件夹,包含各自的密钥、数据库和 blob。更改在重启后生效。", + identitiesInvalidName: "名称无效。字母、数字、连字符和下划线,最多 32 个字符。", + identitiesNewName: "新身份的名称", + identitiesRestart: "立即重启", + identitiesRestartNeeded: "已选择。重启 Oasis 以使用该身份进入。", + identitiesRestarting: "正在重启。请过几秒再打开 Oasis。", + identitiesSameFeed: "与另一个身份共用密钥:切勿从两者同时发布。", + identitiesTitle: "身份", + identitiesUse: "使用这个", + // Barra inferior + bbAdd: "添加", + bbDone: "完成", + bbFull: "已达上限", + bbManage: "编辑", + bbPickDesc: "为底部工具栏选择最多 4 个快捷方式。", + bbPickNone: "无", + bbPickTitle: "自定义工具栏", + bbQuickActions: "快捷操作", + bbRemove: "移除", + bbYourBar: "你的工具栏", + // Menu y filtros + menuAll: "全部", + menuBlogs: "博客", + menuSocial: "社群", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "分类", + bookmarkCategoryPlaceholder: "可选", + chatReply: "回复", + chatShareUrl: "分享链接", + filter: "筛选", + forumFilterHot: "热门", + peerLastChange: "最后变更", + profileVisibilityDevice: "设备", + publishBlog: "发布博客", + spreadHint: "把它扩散给支持你的人(通过你的 feed 复制传播)。", + spreadMore: "更多" + }, + ar: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "القائمة", + bbMenuClose: "إغلاق", + uxPickHint: "المس واحدًا ويُحفظ. يمكنك تغييره في أي وقت من الإعدادات.", + uxPickCurrent: "قيد الاستخدام", + welcomeStepUseMobile: "الملف", + welcomeHintUseMobile: "لماذا تستخدم Oasis. يفعّل الوحدات التي تحتاجها.", + wfHint_activists: "جمعيات وقبائل وتصويت ومنتديات ووثائق مشتركة", + wfHint_acampada: "ما يلزم للمخيم: دردشات وغرف ولجان وتصويت", + wfHint_full: "كل الوحدات", + wfHint_default: "كل الوحدات", + wfHint_social: "الموجز والصور والفعاليات والأصدقاء", + wfHint_kids: "المدرسة والألعاب والإبداع", + wfHint_night: "مدونات وموسيقى وفيديو ودردشة", + wfHint_jobs: "عمل ومشاريع وسوق ومكتب", + wfHint_business: "اقتصاد ومصرف ومكتب ووثائق", + wfHint_mobile: "الأساسيات للهاتف", + workflow_acampada: "مخيم", + tribeSectionsMore: "المزيد", + tribeSectionsLabel: "الأقسام", + voteYes: "نعم", + voteNo: "لا", + voteAbstention: "امتناع", + voteGreen: "أخضر · موافق", + voteYellow: "أصفر · شكوك", + voteRed: "أحمر · ضد", + voteOwnNote: "أنت من أنشأ هذا التصويت، لذا لا يمكنك التصويت فيه: من يقترح يبقى محايدًا.", + voteKindLabel: "نوع التصويت", + voteKind_classic: "نعم / لا", + voteKindHint_classic: "نعم، لا، امتناع والخيارات المعتادة", + voteKind_traffic: "إشارة المرور", + voteKindHint_traffic: "أخضر موافق · أصفر شكوك · أحمر ضد، كما في الجمعية", + voteKind_scale: "مقياس الحرارة", + voteKindHint_scale: "من 1 (بارد) إلى 5 (ساخن)", + deleteConfirmQuestion: "حذف نهائيًا؟", + deleteConfirmYes: "نعم، احذف", + welcomeBannerTextMobile: "أضف اسمك وصورتك ليتعرّف عليك الآخرون.", + welcomeBannerActionMobile: "إضافة", + footerPeersNone: "لا مزامنة مع أحد بعد", + footerPeersSome: "مزامنة مع {n}", + tribeMapUrlLabel: "رابط الخريطة (اختياري)", + tribeHintPrivacy: "خاصة: يراها أعضاؤها فقط وهي مشفّرة. عامة: تراها الشبكة كلها.", + tribeHintMode: "مفتوحة: يمكن لأي عضو أن يدعو. صارمة: لا يدعو إلا من أنشأها.", + moduleBlockedText: "{name} مُعطّل.", + moduleBlockedAction: "تفعيل {name}", + modulesOnCount: "{on} من {total} أقسام مفعّلة", + modulesSaved: "تم الحفظ.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "اختر مظهر Oasis على هاتفك. يُطبَّق بمجرد اللمس.", + workflowsDescriptionMobile: "يحدد سير العمل الوحدات المفعّلة. على الهاتف يبقى مظهرك كما هو.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "تعذّر إزالة البيانات الوصفية من هذه الصورة (الموقع، الكاميرا…)، لذلك لم تُرفع. احفظها بصيغة JPEG أو PNG وحاول مجددًا.", + // Identidades + identitiesActive: "قيد الاستخدام", + identitiesBackup: "تصدير هوية أو استيرادها", + identitiesCreate: "إنشاء هوية", + identitiesDescription: "كل هوية تعيش في مجلدها الخاص، بمفتاحها وقاعدة بياناتها وملفاتها. يسري التغيير عند إعادة التشغيل.", + identitiesInvalidName: "اسم غير صالح. حروف وأرقام وشرطة وشرطة سفلية، حتى 32 حرفًا.", + identitiesNewName: "اسم الهوية الجديدة", + identitiesRestart: "أعد التشغيل الآن", + identitiesRestartNeeded: "تم الاختيار. أعد تشغيل Oasis للدخول بهذه الهوية.", + identitiesRestarting: "جارٍ إعادة التشغيل. افتح Oasis مجددًا بعد ثوانٍ.", + identitiesSameFeed: "تشترك في المفتاح مع هوية أخرى: لا تنشر أبدًا من كلتيهما.", + identitiesTitle: "الهويات", + identitiesUse: "استخدم هذه", + // Barra inferior + bbAdd: "إضافة", + bbDone: "تم", + bbFull: "بلغت الحد الأقصى", + bbManage: "تحرير", + bbPickDesc: "اختر حتى 4 اختصارات للشريط السفلي.", + bbPickNone: "لا شيء", + bbPickTitle: "تخصيص الشريط", + bbQuickActions: "إجراءات سريعة", + bbRemove: "إزالة", + bbYourBar: "شريطك", + // Menu y filtros + menuAll: "الكل", + menuBlogs: "المدونات", + menuSocial: "اجتماعي", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "الفئة", + bookmarkCategoryPlaceholder: "اختياري", + chatReply: "رد", + chatShareUrl: "مشاركة الرابط", + filter: "تصفية", + forumFilterHot: "الأكثر تفاعلًا", + peerLastChange: "آخر تغيير", + profileVisibilityDevice: "الجهاز", + publishBlog: "نشر المدونة", + spreadHint: "انشر هذا بين مؤيديك (يُنسخ عبر تدفقك).", + spreadMore: "المزيد" + }, + hi: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "मेनू", + bbMenuClose: "बंद करें", + uxPickHint: "एक पर टैप करें और वह सेव हो जाएगा। सेटिंग्स में इसे कभी भी बदल सकते हैं।", + uxPickCurrent: "चालू", + welcomeStepUseMobile: "प्रोफ़ाइल", + welcomeHintUseMobile: "आप Oasis किस लिए इस्तेमाल करते हैं। ज़रूरी मॉड्यूल चालू करता है।", + wfHint_activists: "सभाएँ, ट्राइब, मतदान, फ़ोरम और साझा दस्तावेज़", + wfHint_acampada: "शिविर के लिए ज़रूरी: चैट, कमरे, समितियाँ और मतदान", + wfHint_full: "सभी मॉड्यूल", + wfHint_default: "सभी मॉड्यूल", + wfHint_social: "फ़ीड, फ़ोटो, कार्यक्रम और दोस्त", + wfHint_kids: "स्कूल, खेल और रचना", + wfHint_night: "ब्लॉग, संगीत, वीडियो और बातचीत", + wfHint_jobs: "काम, प्रोजेक्ट, बाज़ार और दफ़्तर", + wfHint_business: "अर्थव्यवस्था, बैंकिंग, दफ़्तर और दस्तावेज़", + wfHint_mobile: "फ़ोन के लिए बुनियादी", + workflow_acampada: "शिविर", + tribeSectionsMore: "और", + tribeSectionsLabel: "खंड", + voteYes: "हाँ", + voteNo: "नहीं", + voteAbstention: "तटस्थ", + voteGreen: "हरा · सहमत", + voteYellow: "पीला · संदेह", + voteRed: "लाल · विरोध", + voteOwnNote: "यह मतदान आपने बनाया है, इसलिए आप इसमें वोट नहीं दे सकते: प्रस्ताव रखने वाला तटस्थ रहता है।", + voteKindLabel: "मतदान का प्रकार", + voteKind_classic: "हाँ / नहीं", + voteKindHint_classic: "हाँ, नहीं, तटस्थ और सामान्य विकल्प", + voteKind_traffic: "ट्रैफ़िक लाइट", + voteKindHint_traffic: "हरा सहमत · पीला संदेह · लाल विरोध, सभा की तरह", + voteKind_scale: "थर्मामीटर", + voteKindHint_scale: "1 (ठंडा) से 5 (गरम) तक", + deleteConfirmQuestion: "हमेशा के लिए हटाएँ?", + deleteConfirmYes: "हाँ, हटाएँ", + welcomeBannerTextMobile: "अपना नाम और फ़ोटो जोड़ें ताकि लोग आपको पहचानें।", + welcomeBannerActionMobile: "जोड़ें", + footerPeersNone: "अभी किसी से सिंक नहीं", + footerPeersSome: "{n} से सिंक हो रहा है", + tribeMapUrlLabel: "नक्शे का लिंक (वैकल्पिक)", + tribeHintPrivacy: "निजी: केवल सदस्य देखते हैं, और यह एन्क्रिप्टेड है। सार्वजनिक: पूरा नेटवर्क देखता है।", + tribeHintMode: "खुला: कोई भी सदस्य आमंत्रित कर सकता है। सख़्त: केवल बनाने वाला आमंत्रित करता है।", + moduleBlockedText: "{name} बंद है।", + moduleBlockedAction: "{name} चालू करें", + modulesOnCount: "{total} में से {on} खंड चालू", + modulesSaved: "सहेजा गया।", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "चुनें कि आपके फ़ोन पर Oasis कैसा दिखे। छूते ही लागू हो जाता है।", + workflowsDescriptionMobile: "वर्कफ़्लो तय करता है कि कौन-से मॉड्यूल चालू हैं। फ़ोन पर आपकी स्किन वैसी ही रहती है।", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "इस छवि का मेटाडेटा (स्थान, कैमरा…) हटाया नहीं जा सका, इसलिए इसे अपलोड नहीं किया गया। इसे JPEG या PNG के रूप में सहेजें और फिर कोशिश करें।", + // Identidades + identitiesActive: "उपयोग में", + identitiesBackup: "पहचान निर्यात या आयात करें", + identitiesCreate: "पहचान बनाएँ", + identitiesDescription: "हर पहचान अपने अलग फ़ोल्डर में रहती है, अपनी कुंजी, डेटाबेस और ब्लॉब्स के साथ। बदलाव पुनः आरंभ करने पर लागू होता है।", + identitiesInvalidName: "अमान्य नाम। अक्षर, अंक, हाइफ़न और अंडरस्कोर, अधिकतम 32।", + identitiesNewName: "नई पहचान का नाम", + identitiesRestart: "अभी पुनः आरंभ करें", + identitiesRestartNeeded: "चयनित। इस पहचान से प्रवेश करने के लिए Oasis पुनः आरंभ करें।", + identitiesRestarting: "पुनः आरंभ हो रहा है। कुछ सेकंड बाद Oasis फिर खोलें।", + identitiesSameFeed: "किसी अन्य पहचान से कुंजी साझा करती है: दोनों से कभी प्रकाशित न करें।", + identitiesTitle: "पहचानें", + identitiesUse: "इसका उपयोग करें", + // Barra inferior + bbAdd: "जोड़ें", + bbDone: "हो गया", + bbFull: "अधिकतम सीमा", + bbManage: "संपादित करें", + bbPickDesc: "नीचे की बार के लिए अधिकतम 4 शॉर्टकट चुनें।", + bbPickNone: "कोई नहीं", + bbPickTitle: "बार अनुकूलित करें", + bbQuickActions: "त्वरित क्रियाएँ", + bbRemove: "हटाएँ", + bbYourBar: "आपकी बार", + // Menu y filtros + menuAll: "सभी", + menuBlogs: "ब्लॉग", + menuSocial: "सामाजिक", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "श्रेणी", + bookmarkCategoryPlaceholder: "वैकल्पिक", + chatReply: "उत्तर दें", + chatShareUrl: "लिंक साझा करें", + filter: "छानें", + forumFilterHot: "चर्चित", + peerLastChange: "अंतिम बदलाव", + profileVisibilityDevice: "डिवाइस", + publishBlog: "ब्लॉग प्रकाशित करें", + spreadHint: "इसे अपने समर्थकों तक फैलाएँ (आपकी फ़ीड से प्रतिकृति होती है)।", + spreadMore: "और" + }, + ru: { + // usabilidad (19/10/2026): votaciones, borrar, modulos, pie + bbMenu: "Меню", + bbMenuClose: "Закрыть", + uxPickHint: "Нажмите на вариант, и он сохранится. Изменить можно в любой момент в Настройках.", + uxPickCurrent: "Сейчас", + welcomeStepUseMobile: "Профиль", + welcomeHintUseMobile: "Для чего вам Oasis. Включает нужные модули.", + wfHint_activists: "Собрания, племена, голосования, форумы и общие документы", + wfHint_acampada: "Всё для лагеря: чаты, комнаты, комиссии и голосование", + wfHint_full: "Все модули", + wfHint_default: "Все модули", + wfHint_social: "Лента, фото, события и друзья", + wfHint_kids: "Школа, игры и творчество", + wfHint_night: "Блоги, музыка, видео и общение", + wfHint_jobs: "Работа, проекты, рынок и офис", + wfHint_business: "Экономика, банк, офис и документы", + wfHint_mobile: "Основное для телефона", + workflow_acampada: "Лагерь", + tribeSectionsMore: "Ещё", + tribeSectionsLabel: "Разделы", + voteYes: "Да", + voteNo: "Нет", + voteAbstention: "Воздержаться", + voteGreen: "Зелёный · за", + voteYellow: "Жёлтый · сомнения", + voteRed: "Красный · против", + voteOwnNote: "Вы создали это голосование, поэтому не можете в нём голосовать: кто предлагает, остаётся нейтральным.", + voteKindLabel: "Тип голосования", + voteKind_classic: "Да / Нет", + voteKindHint_classic: "Да, нет, воздержаться и обычные варианты", + voteKind_traffic: "Светофор", + voteKindHint_traffic: "Зелёный за · жёлтый сомнения · красный против, как на собрании", + voteKind_scale: "Термометр", + voteKindHint_scale: "От 1 (холодно) до 5 (горячо)", + deleteConfirmQuestion: "Удалить навсегда?", + deleteConfirmYes: "Да, удалить", + welcomeBannerTextMobile: "Добавьте имя и фото, чтобы вас узнавали.", + welcomeBannerActionMobile: "Добавить", + footerPeersNone: "Пока ни с кем не синхронизировано", + footerPeersSome: "Синхронизация с {n}", + tribeMapUrlLabel: "Ссылка на карту (необязательно)", + tribeHintPrivacy: "Закрытое: видят только участники, и оно зашифровано. Открытое: видит вся сеть.", + tribeHintMode: "Открытое: приглашать может любой участник. Строгое: приглашает только создатель.", + moduleBlockedText: "{name} выключен.", + moduleBlockedAction: "Включить {name}", + modulesOnCount: "Включено {on} из {total} разделов", + modulesSaved: "Сохранено.", + // skins y workflows en el movil (settings_view.js) + skinIntroMobile: "Выберите, как Oasis выглядит на телефоне. Применяется сразу по нажатию.", + workflowsDescriptionMobile: "Рабочий процесс задаёт, какие модули включены. На телефоне ваше оформление не меняется.", + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Не удалось удалить метаданные этого изображения (местоположение, камера…), поэтому оно не загружено. Сохраните его как JPEG или PNG и попробуйте снова.", + // Identidades + identitiesActive: "Используется", + identitiesBackup: "Экспортировать или импортировать личность", + identitiesCreate: "Создать личность", + identitiesDescription: "Каждая личность живёт в своей папке — со своим ключом, базой данных и блобами. Изменение вступает в силу после перезапуска.", + identitiesInvalidName: "Недопустимое имя. Буквы, цифры, дефис и подчёркивание, до 32 символов.", + identitiesNewName: "Имя новой личности", + identitiesRestart: "Перезапустить сейчас", + identitiesRestartNeeded: "Выбрано. Перезапустите Oasis, чтобы войти с этой личностью.", + identitiesRestarting: "Перезапуск. Откройте Oasis снова через несколько секунд.", + identitiesSameFeed: "Ключ совпадает с другой личностью: никогда не публикуйте с обеих.", + identitiesTitle: "Личности", + identitiesUse: "Использовать эту", + // Barra inferior + bbAdd: "Добавить", + bbDone: "Готово", + bbFull: "Достигнут максимум", + bbManage: "Изменить", + bbPickDesc: "Выберите до 4 ярлыков для нижней панели.", + bbPickNone: "Нет", + bbPickTitle: "Настроить панель", + bbQuickActions: "Быстрые действия", + bbRemove: "Убрать", + bbYourBar: "Ваша панель", + // Menu y filtros + menuAll: "Все", + menuBlogs: "Блоги", + menuSocial: "Социальное", + // Rescatadas de 0.9.1 y otras + bookmarkCategoryLabel: "Категория", + bookmarkCategoryPlaceholder: "Необязательно", + chatReply: "Ответить", + chatShareUrl: "Поделиться ссылкой", + filter: "Фильтр", + forumFilterHot: "Горячее", + peerLastChange: "Последнее изменение", + profileVisibilityDevice: "Устройство", + publishBlog: "Опубликовать блог", + spreadHint: "Распространите это среди тех, кто вас читает (реплицируется через вашу ленту).", + spreadMore: "ещё" + } +}; diff --git a/src/client/assets/translations/oasis_ar.js b/src/client/assets/translations/oasis_ar.js index 3319b82..c706560 100644 --- a/src/client/assets/translations/oasis_ar.js +++ b/src/client/assets/translations/oasis_ar.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { ar: { - bbAdd: "إضافة", - bbQuickActions: "إجراءات سريعة", - bbPickTitle: "تثبيت وحدة", - bbPickDesc: "اختر وحدة لخانة + في الشريط السفلي.", - bbPickNone: "لا شيء", - bbManage: "تعديل", - bbYourBar: "شريطك", - bbRemove: "إزالة", - bbFull: "تم بلوغ الحد الأقصى", - bbDone: "تم", - activityChangeFilter: "تغيير التصفية", - emptyConnectHint: "اتصل بـ pub للمزامنة مع الشبكة.", - emptyConnectCta: "الاتصال بـ pub", - menuCommunity: "المجتمع", - activityGroupCommunity: "المجتمع والحوكمة", - activityGroupOrg: "التنظيم", - activityGroupComm: "التواصل", - activityGroupEconomy: "الاقتصاد", - activityGroupMedia: "المحتوى والوسائط", - activityGroupOther: "أخرى", - languageName: "العربية", shopOrderStatusPaid: "تم استلام الدفع", shopOrderStatusReceived: "تم الاستلام", shopOrderMarkPaid: "تأكيد استلام الدفع", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "المشتريات", shopMyOrdersDescription: "طلبات الشراء الخاصة بك.", shopMyOrdersEmpty: "ليس لديك مشتريات بعد.", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PM البائع", shopOrderSeller: "البائع", - shopOrderPaymentConcept: "دفعة", shopOrderInvoice: "فاتورة", shopOrderInvoiceConcept: "فاتورة", shopOrderStatusPending: "قيد الانتظار", shopOrderStatusAccepted: "مقبول", shopOrderStatusRejected: "مرفوض", shopOrderStatusShipped: "تم الشحن", - shopOrderStatusDelivered: "تم التسليم", shopOrderAccept: "قبول", shopOrderReject: "رفض", shopOrderMarkShipped: "وضع علامة شُحن", - shopOrderMarkDelivered: "وضع علامة سُلّم", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "عقد ذكي", + shopOrderPmBuyer: "PM المشتري", shopOrderContractConcept: "طلب متجر", shopOrdersPending: "طلبات معلّقة", all: "الكل", @@ -75,18 +50,17 @@ module.exports = { viewJson: "عرض JSON", matchScore: "درجة التطابق", preferencesLabel: "التفضيلات", - inhabitantProfileTitle: "ملف الساكن", + inhabitantProfileTitle: "الساكن", contentWarningLabel: "تحذير المحتوى", invalidPost: "محتوى غير صالح", invalidPostHint: "هذه الرسالة تحتوي على نص فارغ/غير صالح.", - spreadBy: "بواسطة", spreadChron: "نشر", spreaded: "تم النشر", - spreadMore: "المزيد", spreadContentUnavailable: "المحتوى غير متاح بعد (في انتظار النسخ)", filteredByTag: "مُصفّى حسب الوسم", filteredByTagTitle: "مُصفّى حسب الوسم", feedPublishedSuccess: "تم نشر التغذية بنجاح!", + feedMastodonFailed: "نُشر في Oasis، لكن تعذّر إرساله إلى Mastodon.", tribeFeedSent: "تم إرسال الرسالة بنجاح!", tribeContentEncrypted: "محتوى مشفّر", tribeTorrentsEmpty: "لا توجد تورنتات بعد.", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "بدون خطورة", calendarDeleteDate: "حذف التاريخ", calendarIntervalUntil: "حتى", - bookmarkCategory: "الفئة", bookmarkLastVisit: "آخر زيارة", profileClearnetBookmarksLabel: "الإشارات المرجعية", - forumFilterHot: "رائج", invitesPort: "المنفذ", - currentlyUnrecheable: "خطأ!", - peerHostValidation: "عنوان IPv4 صالح (مثل 192.168.1.100) أو اسم مضيف (مثل pub.example.com)", + peerHostValidation: "عنوان IPv4 صالح (مثل 192.168.1.100) أو اسم مضيف (مثل pub.solarnethub.com)", peerPortValidation: "المنفذ 1-65535", peerKeyValidation: "مفتاح SSB ed25519 العام (@<44 حرف base64>=.ed25519)", inboxJobUnsubscribedTitle: "إلغاء الاشتراk في عرض عملك", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "وصف المهمة", exportPasswordLabel: "كلمة مرور التصدير", importPasswordPlaceholder: "أدخل كلمة المرور", - searchCasesPlaceholder: "البحث عن القضايا…", + searchCasesPlaceholder: "ابحث في المحاكم...", shopBuyDeliveryAddressPlaceholder: "عنوان التسليم", shopBuyNotesPlaceholder: "ملاحظات", bankTaxesUserNoteBold: "تخفيض ضريبة ECO مباشرة", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "دعم القضية", courtsSettlementAcceptBtn: "قبول التسوية", courtsSupportCount: "المؤيدون", - courtsVerdictVoteTitle: "التصويت على الحكم", - courtsVerdictVoteLabel: "تصويت الحكم", - courtsVerdictVoteAccept: "قبول", - courtsVerdictVoteReject: "رفض", - courtsVerdictVoteSubmit: "إرسال التصويت", reportsSetStatus: "تعيين الحالة", reportsStatusClosed: "مغلق", statsAvgPerInhabitant: "المتوسط لكل ساكن", statsCarbonMaxAnnual: "أقصى تقدير سنوي", statsCarbonNetwork: "إجمالي الشبكة", statsCarbonOfEstMax: "من السعة القصوى المقدّرة", + statsCarbonYearProgress: "من السنة المنقضية", + statsNetworkTitle: "الشبكة", + statsStorageTitle: "التخزين", + statsEcoTaxTitle: "ضريبة ECO", + statsAccountTitle: "الحساب", + statsAveragesTitle: "المتوسطات", statsCarbonOfNetwork: "من إجمالي الشبكة", statsCarbonUser: "بصمتك", statsContentCountColumn: "العدد", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "أبرز الوسوم", statsTopTypesTitle: "أبرز أنواع المحتوى", statsTotalMsgs: "إجمالي الرسائل", - feedViewDetails: "عرض التفاصيل", - eventFileLabel: "إرفاق صورة", - taskFileLabel: "إرفاق صورة", - courtsRespondentRequired: "معرّف المدعى عليه مطلوب", extended: "الكون المتعدد", extendedDescription: [ "عندما تدعم شخصًا ما، قد تقوم بتنزيل منشورات من السكان الذين يدعمونهم، وتظهر تلك المنشورات هنا، مرتبة حسب الأحدث.", @@ -215,47 +183,76 @@ module.exports = { ], profile: "الصورة الرمزية", inhabitants: "السكان", - peersReplicatedFeeds: "الموجزات المنسوخة", + peersReplicatedFeeds: "المنسوخة", + peersTorColumn: "TOR", + peersTorYes: "يمكن الوصول إليه عبر Tor", + peersTorNo: "لا يمكن الوصول إليه عبر Tor", + peersOasisId: "ID", + peersOasisVersion: "الإصدار", graphos: "غرافوس", graphosDescription: "خريطة تفاعلية للشبكة من حولك.", + graphosFederation: "الاتحاد", + graphosFedPubs: "خوادم PUB", + graphosFedUnreachable: "غير قابلة للوصول", + graphosFedLinks: "روابط متبادلة", + graphosFedIsolated: "معزولة", + graphosFedNoData: "لا بيانات", + graphosFedMutual: "متبادل", + graphosFedOneWay: "باتجاه واحد", + graphosFedTransport: "الشبكة", + graphosFedWith: "متحد مع", + graphosZoom: "تكبير", + graphosFedInhabitants: "السكان", + graphosFedClearnet: "الإنترنت", + graphosFedBoth: "الإنترنت + TOR", + graphosFedEmpty: "لا تعرف عقدتك أي PUB بعد.", graphosViewingGraphOf: "عرض", graphosBackToMine: "← شبكتي", graphosShowing: "عرض", graphosYou: "أنت", graphosTotalNodes: "إجمالي العقد", - manualMode: "الوضع اليدوي", mentions: "الإشارات", mentionsDescription: [ - strong("منشورات تُشير إليك @"), - "، مرتبة حسب الأحدث.", + strong("كل رسالة تذكرك بـ @"), + ".", ], - nextPage: "التالي", - previousPage: "السابق", noMentions: "لم تتلقَّ إشارات @ بعد.", + mentionsSearchPlaceholder: "ابحث في الإشارات...", privateReminders: "تذكيرات", + inboxFilterPms: "رسائل خاصة", + inboxFilterNotifications: "إشعارات", + inboxMarkRead: "وضع علامة كمقروء", + inboxMarkUnread: "وضع علامة كغير مقروء", + inboxUnreadChip: "غير مقروء", + inboxMarkAllRead: "وضع علامة مقروء على الكل", + inboxDeleteShown: "حذف الكل", + inboxArchive: "أرشفة", + inboxUnarchive: "إلغاء الأرشفة", + inboxFilterArchived: "مؤرشف", + inboxEarlierMessages: "رسائل سابقة", + inboxSortThread: "حسب المحادثة", + inboxSortRecent: "الأحدث أولاً", + inboxAllBots: "الروبوتات", + settingsInboxBotsTitle: "إشعارات صندوق الوارد", + settingsInboxBotsDesc: "اختر الروبوتات التي يمكنها إشعارك في صندوق الوارد. الروبوتات غير المحددة تُخفى ولا تُحتسب إشعاراتها.", inboxFiltersLabel: "المرشحات:", inboxToggleToMutuals: "التبديل إلى المتبادلين", inboxToggleToWhole: "التبديل إلى الجميع", - privateFrom: "من", - privateTo: "إلى", privateDate: "التاريخ", pmReply: "رد", pmReplies: "ردود", - pmNew: "جديد", - pmMarkRead: "تعيين كمقروء", inReplyTo: "ردًا على", + forumInThread: "في الموضوع", pmPreview: "معاينة", pmPreviewTitle: "معاينة الرسالة", peers: "الأقران", searchDescription: "الوصف", - imageSearch: "بحث عن الصور", searchFromLabel: "من", searchToLabel: "إلى", - searchMinOpinionsLabel: "أدنى الآراء", - searchInhabitantLabel: "ساكن (Oasis ID)", searchQueryLabel: "استعلام", - searchPerPageLabel: "نتائج لكل صفحة", + searchPerPageLabel: "النتائج", settings: "الإعدادات", + settingsDescription: "اضبط بيئة شبكتك.", continueReading: "متابعة القراءة", moreComments: "تعليق إضافي", readThread: "اقرأ بقية المحادثة", @@ -265,68 +262,47 @@ module.exports = { melodyDescription: 'شغّل لحن سلسلة الكتل خاصتك — كل كتلة تصبح نغمة.', melodyEmpty: 'لا توجد نغمات بعد — لم تنتج سلسلة الكتل خاصتك أي كتلة.', melodyCompositionTitle: 'التأليف', - melodyCompositionHelp: 'كل كتلة في سلسلة الكتل خاصتك تصبح نغمة موسيقية حسب نوعها وحجمها.', - melodyStatsTitle: 'التوزيع حسب النوع', - melodyInhabitantLabel: 'الساكن', melodyTotalBlocks: 'نغمات', - melodyType: 'النوع', - melodyCount: 'كتل', melodyFilterMine: 'لي', melodyFilterAll: 'الكل', - melodyFilterShare: 'تحميل لحن', - melodyShareTitle: 'شارك لحنك', - melodyShareHelp: 'انشر لقطة من لحنك الحالي ليتمكن الآخرون من الاستماع وإعادة المزج.', - melodyShareTitleLabel: 'العنوان (اختياري)', - melodyShareTitlePlaceholder: 'شبح بلوكتشين', - melodyShareSubmit: 'نشر اللحن', - melodyNoShared: 'لا توجد ألحان بلوكتشين بعد.', melodyRegenerate: 'إعادة التوليد', melodyDownload: "تنزيل BCS", - profileActivityTitle: 'النشاط', - profileActivityMore: 'عرض كل النشاط', profileContentSectionTitle: 'محتوى الصورة الرمزية', profileContentHelp: 'اختر أي وحدات تُعرض في صورتك الرمزية.', profileSensorsHelp: 'مقاييس اختيارية معروضة في صورتك الرمزية.', profileClearnetHelp: 'الوحدات التي يمكن الوصول إليها من خارج أوازيس.', profileHubAll: 'الكل', - profileHubTitle: 'المحتوى العام', - footerPulseTitle: 'النبض', - footerPulsePeers: 'الأقران', - footerPulseInbox: 'الوارد', - footerPulseSync: 'آخر مزامنة', - footerPulseEcoValue: 'ECO/ساعة', - footerPulseLastActivity: 'آخر نشاط', footerLicenseLabel: 'الترخيص', - profileSwitchToOasis: 'العودة إلى Oasis', - profileSwitchToClearnet: 'الغوص في Clearnet', - profileVisibilityFediverse: "فيديفيرس", - profileSwitchToFediverse: "انغمس في الفيديفيرس", - coordLabel: 'الإحداثي (مثال: A3)', - coordPlaceholder: 'أدخل الإحداثي', + profileVisibilityFediverse: "الكون المتعدد", contributorsTitle: "المساهمون", - pixeliaBy: "بواسطة", colorLabel: 'اختر لونًا', paintButton: 'ارسم!', - invalidCoordinate: 'إحداثي غير صحيح', - goToMuralButton: "عرض الجدارية", totalPixels: 'إجمالي البكسلات', modules: "الوحدات", modulesViewTitle: "الوحدات", modulesViewDescription: "اضبط بيئتك بتفعيل أو تعطيل الوحدات.", inbox: "صندوق الوارد", + inboxSearchPlaceholder: "ابحث في صندوق الوارد...", multiverse: "الكون المتعدد", - fediverse: "فيديفيرس", - fediverseDescription: "أدر حساباتك الأخرى في الفيديفيرس، بما في ذلك إرسال واستقبال المحتوى.", + fediverse: "الكون المتعدد", + fediverseDescription: "أدر حساباتك الأخرى في الكون المتعدد، بما في ذلك إرسال واستقبال المحتوى.", fediverseStatus: "الحالة", - fediverseDisconnected: "الفيديفيرس غير متصل. تحقق من %LINK% أو حالة الاتصال.", - fediverseSettingsTitle: "فيديفيرس", + fediverseDisconnected: "الكون المتعدد غير متصل. تحقق من %LINK% أو حالة الاتصال.", + fediverseSettingsTitle: "الكون المتعدد", fediverseInstanceLabel: "العنوان", fediverseTokenLabel: "رمز الوصول", - fediverseTokenHelp: "اضبط رمز الوصول الخاص بك. سيُستخدم لإدارة حساب ماستودون من داخل أوازيس.", fediverseConnect: "ربطه", fediverseConnectedAs: "متصل باسم", + nextcloudServerLabel: "الخادم", + nextcloudUserLabel: "المستخدم", + nextcloudFolderLabel: "المجلد", + nextcloudUploadButton: "رفع", + nextcloudFileLabel: "اختر ملفًا", + fileExternalLink: "رابط خارجي", + nextcloudErrConnect: "تعذر الوصول إلى Nextcloud بهذه البيانات.", + nextcloudErrUpload: "فشل الرفع إلى Nextcloud.", + nextcloudErrShare: "لم يعطِ Nextcloud رابطًا عامًا للملف.", fediverseDisconnect: "قطع الاتصال", - fediverseEmpty: "عند ربط حسابات أخرى في الفيديفيرس من %LINK%، يمكنك إدارتها من هنا.", fediverseEmptyLink: "الإعدادات", fediverseComposePlaceholder: "بماذا تفكر؟", fediverseReplyPlaceholder: "اكتب ردك…", @@ -335,21 +311,15 @@ module.exports = { fediverseReply: "رد", fediverseBoosted: "قام بالترقية", fediverseNoPosts: "خطك الزمني فارغ.", - fediverseThread: "سلسلة", fediverseTimeline: "الخطوط الزمنية", - fediverseManageTimeline: "إدارة الخط الزمني", fediverseFollowers: "المتابعون", fediverseFollowing: "يُتابِع", fediversePosts: "المنشورات", fediverseJoined: "انضم", - fediverseOverview: "نظرة عامة", fediverseRefresh: "تحديث", fediverseWrite: "اكتب", fediversePreview: "معاينة", - fediverseEdit: "تعديل", - fediverseOpenFeed: "زيارة الخلاصة", fediverseManage: "إدارة", - fediverseStatusNotConnected: "غير متصل", fediverseError: "تعذّر الاتصال بماستودون.", fediverseErrInstance: "رابط خادم غير صالح.", fediverseErrAuth: "رمز غير صالح أو منتهٍ الصلاحية.", @@ -360,17 +330,76 @@ module.exports = { fediverseErrPost: "تعذّر النشر.", fediverseErrMedia: "تعذّر رفع الملف.", fediverseErrEmpty: "اكتب شيئاً أو أرفق ملفاً.", - popularLabel: "أبرز المنشورات", - topicsLabel: "المواضيع", - latestLabel: "الأحدث", - summariesLabel: "الملخصات", - threadsLabel: "المحادثات", - multiverseLabel: "الكون المتعدد", - inboxLabel: "صندوق الوارد", - invitesLabel: "الدعوات", - walletLabel: "المحفظة", - legacyLabel: "المفاتيح", - cipherLabel: "التشفير", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "رقم الهاتف", + telegramSendCode: "إرسال الرمز", + telegramCodeHelp: "أدخل رمز الدخول الذي أرسله لك تيليغرام للتو (التطبيق أو الرسائل).", + telegramCodeLabel: "الرمز", + telegramPasswordHelp: "هذا الحساب يستخدم التحقق بخطوتين. أدخل كلمة المرور.", + telegramPasswordLabel: "كلمة المرور", + telegramVerify: "تحقق", + telegramCancel: "إلغاء", + telegramChats: "المحادثات", + telegramUnread: "غير مقروءة", + telegramOpen: "فتح", + telegramSend: "إرسال", + telegramYou: "أنت", + telegramNoChats: "لا توجد محادثات بعد.", + telegramNoMessages: "لا توجد رسائل بعد.", + telegramComposePlaceholder: "اكتب رسالة…", + telegramChannel: "قناة", + telegramGroup: "مجموعة", + telegramPrivate: "خاص", + telegramErrConnect: "تعذر الاتصال بتيليغرام.", + telegramErrTimeout: "استغرق تيليغرام وقتاً طويلاً للرد.", + telegramErrAuth: "جلسة تيليغرام غير صالحة أو منتهية. أعد الاتصال.", + telegramErrFetch: "تعذر تحميل المحادثات.", + telegramErrSend: "تعذر إرسال الرسالة.", + telegramErrMissing: "مطلوب API ID و API hash ورقم الهاتف.", + telegramErrCode: "رمز غير صحيح. حاول مرة أخرى.", + telegramErrCodeExpired: "انتهت صلاحية الرمز. ابدأ من جديد.", + telegramErrPassword: "كلمة مرور خاطئة.", + telegramErrPhone: "رقم هاتف غير صالح.", + telegramErrApi: "API ID أو API hash غير صالح.", + telegramErrFlood: "محاولات كثيرة جداً. انتظر قليلاً ثم حاول مجدداً.", + telegramErrNoLogin: "لا يوجد تسجيل دخول جارٍ. ابدأ من جديد.", + peertubeUsernameLabel: "اسم المستخدمة", + peertubePasswordLabel: "كلمة المرور", + peertubeSubscriptions: "الاشتراكات", + peertubeMyVideos: "فيديوهاتي", + peertubeVideos: "فيديوهات", + peertubeFollowers: "المتابِعات", + peertubeViews: "مشاهدة", + peertubeLikes: "إعجاب", + peertubeQuota: "الحصة", + peertubeUpload: "رفع فيديو", + peertubeUploadHelp: "اختاري ملف فيديو. يُرفع إلى قناتك ثم يعالجه PeerTube.", + peertubeTitleLabel: "العنوان", + peertubeDescriptionLabel: "الوصف", + peertubePrivacyLabel: "الخصوصية", + peertubePrivacyPublic: "عام", + peertubePrivacyUnlisted: "غير مدرج", + peertubePrivacyPrivate: "خاص", + peertubeNoVideos: "لا توجد فيديوهات بعد.", + peertubeComments: "التعليقات", + peertubeComment: "تعليق", + peertubeCommentPlaceholder: "اكتبي تعليقًا…", + peertubeNoComments: "لا توجد تعليقات بعد.", + peertubeOpen: "فتح على PeerTube", + peertubeWatch: "مشاهدة", + peertubeChannel: "القناة", + peertubeNoStream: "لا يمكن تشغيل هذا الفيديو هنا بعد. افتحيه على PeerTube.", + peertubeUploaded: "تم رفع الفيديو. PeerTube يعالجه الآن.", + peertubeErrConnect: "تعذر الاتصال بـ PeerTube.", + peertubeErrAuth: "اسم المستخدمة أو كلمة المرور غير صحيحة، أو انتهت الجلسة. أعيدي الاتصال.", + peertubeErrInstance: "عنوان الخادم غير صالح.", + peertubeErrMissing: "يلزم الخادم واسم المستخدمة وكلمة المرور.", + peertubeErrFetch: "تعذر تحميل الفيديوهات.", + peertubeErrUpload: "تعذر رفع الفيديو.", + peertubeErrTitle: "العنوان مطلوب.", + peertubeErrComment: "تعذر نشر التعليق.", + peertubeErrNoFile: "اختاري ملف فيديو.", bookmarksLabel: "العلامات المرجعية", videosLabel: "الفيديوهات", torrentsLabel: "التورنت", @@ -381,34 +410,22 @@ module.exports = { inhabitantsLabel: "السكان", trendingLabel: "الرائج", eventsLabel: "الأحداث", - tasksLabel: "المهام", apply: "تطبيق", menuPersonal: "شخصي", - menuContent: "المحتوى", - menuBlogs: "مدونات", menuGovernance: "الحوكمة", menuOffice: "المكتب", - menuMultiverse: "الكون المتعدد", - menuNetwork: "الشبكة", + menuNetwork: "المجتمع", menuCreative: "إبداعي", menuEconomy: "الاقتصاد", - menuMedia: "الوسائط", + menuMedia: "المكتبة", menuTools: "الأدوات", comment: "تعليق", subtopic: "موضوع فرعي", json: "JSON", - createdBy: "بواسطة", unfollow: "إلغاء الدعم", follow: "دعم", block: "حظر", unblock: "إلغاء الحظر", - newerPosts: "منشورات أحدث", - olderPosts: "منشورات أقدم", - feedRangeEmpty: "النطاق المحدد فارغ لهذا التغذية. جرّب عرض ", - seeFullFeed: "التغذية الكاملة", - feedEmpty: "لم تشهد شبكة Oasis أي منشورات من هذا الحساب.", - beginningOfFeed: "هذه بداية التغذية", - noNewerPosts: "لم يتم استقبال منشورات أحدث بعد.", relationshipNotFollowing: "أنت غير مدعوم", relationshipTheyFollow: "يدعمك", relationshipMutuals: "دعم متبادل", @@ -418,25 +435,23 @@ module.exports = { relationshipBlockedBy: "أنت محظور", relationshipMutualBlock: "حظر متبادل", relationshipNone: "أنت لا تدعم", - relationshipConflict: "تعارض", relationshipBlockingPost: "منشور محظور", viewLikes: "عرض الانتشارات", + viewSupporters: "عرض الداعمين", spreadedDescription: "قائمة المنشورات التي نشرها الساكن.", - totalspreads: "إجمالي الانتشارات", - attachFiles: "إرفاق ملفات", preview: "معاينة", publish: "كتابة", contentWarningPlaceholder: "أضف موضوعًا للمنشور (اختياري)", - privateWarningPlaceholder: "أضف سكانًا لإرسال منشور خاص (اختياري)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "اكتب نص مقالتك.", + publishTooLong: "منشورك طويل جدًا. الرجاء تقصيره.", + keepTextRetry: "إرسال مرة أخرى", + dateInPastError: "لا يمكن أن يكون التاريخ في الماضي", publishCustomDescription: [ "تذكّر: بسبب تقنية البلوكتشين، بمجرد نشر المنشور لا يمكن تعديله أو حذفه.", ], commentWarning: [ "تذكّر: بسبب تقنية البلوكتشين، بمجرد نشر المنشور لا يمكن تعديله أو حذفه.", ], - commentPublic: "عام", - commentPrivate: "خاص", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -456,14 +471,12 @@ module.exports = { ".", ], publishCustom: "كتابة منشور متقدم", - subtopicLabel: "إنشاء موضوع فرعي من هذا المنشور", messagePreview: "معاينة المنشور", mentionsMatching: "الإشارات المطابقة", mentionsName: "الاسم", mentionsRelationship: "العلاقة", - updateit: "احصل على التحديثات!", updateBannerText: "يتوفر إصدار جديد من Oasis.", - updateBannerAction: "حدّث الآن →", + updateBannerAction: "حدّث الآن!", info: "معلومات", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -471,29 +484,94 @@ module.exports = { timeAgo: "مضت", sendTime: "حوالي ", theme: "السمة", - legacy: "المفاتيح", - legacyTitle: "المفاتيح", - legacyDescription: "إدارة مفتاحك السري (المفتاح الخاص) بسرعة وأمان.", - legacyExportButton: "تصدير", - legacyImportButton: "استيراد", + backupKeysExportButton: "تصدير", + backupKeysImportButton: "استيراد", ssbLogStream: "البلوكتشين", ssbLogStreamDescription: "تكوين حد الرسائل لتدفقات البلوكتشين.", + logWindowChip: "آخر {n} تدفقات من البلوكتشين", saveSettings: "حفظ الإعدادات", - exportTitle: "تصدير البيانات", exportDescription: "عيّن كلمة مرور (32 حرفًا على الأقل) لتشفير مفتاحك", - exportDataTitle: "نسخة احتياطية", + exportDataTitle: "نسخة فورية", exportDataDescription: "تنزيل بياناتك (بدون المفتاح السري!)", exportDataButton: "تنزيل قاعدة البيانات", - pubWallet: "محفظة PUB", - pubWalletDescription: "عيّن عنوان محفظة PUB. سيُستخدم لمعاملات PUB (بما في ذلك الدخل الأساسي الشامل).", - pubWalletConfiguration: "حفظ الإعدادات", - importTitle: "استيراد البيانات", importDescription: "استورد مفتاحك السري المشفر (المفتاح الخاص) لتفعيل صورتك الرمزية", - importAttach: "إرفاق ملف مشفر (.enc)", - passwordLengthInfo: "يجب أن تكون كلمة المرور 32 حرفًا على الأقل.", passwordImport: "اكتب كلمة المرور لفك تشفير البيانات التي سيتم حفظها في مجلد النظام الرئيسي (الاسم: secret)", exportPasswordPlaceholder: "استخدم أحرفًا صغيرة وكبيرة وأرقامًا ورموزًا", - fileInfo: "سيتم حفظ مفتاحك السري المشفر في مجلد النظام الرئيسي (الاسم: oasis.enc)", + fileInfo: "سيتم تنزيل مفتاحك السري المشفر إلى جهازك (الاسم: oasis.enc)", + developer: "التطوير", + devTitle: "التطوير", + welcomeBannerText: "لم يتم إعداد صورتك الرمزية بعد.", + aiSuggestionBanner: "اقتراح:", + aiSuggestionsEnable: "اقتراحات الذكاء الاصطناعي", + welcomeBannerAction: "ابدأ", + welcomeTitle: "أهلًا بك", + welcomeStepIdentity: "الهوية", + welcomeStepType: "النوع", + welcomeStepUse: "الاستخدام", + welcomeStepLook: "المظهر", + welcomeStepCommunity: "المجتمع", + welcomeStepIntro: "ترحيب", + welcomeHintIdentity: "اسمك وصورتك.", + welcomeHintType: "كيف يُفتح Oasis لك.", + welcomeHintUse: "الوحدات التي تبقى مفعّلة.", + welcomeHintLook: "السمة التي يرتديها Oasis.", + welcomeHintCommunity: "انضم إلى PUB للتعرّف على سكان آخرين.", + welcomeHintIntro: "أرسل تحية أولى إلى المجتمع.", + welcomeStepThemeAction: "استخدم هذه السمة", + welcomeJoinPubs: "انضم إلى PUBs", + welcomeNoFederation: "بدون اتحاد", + welcomeStepGreetingAction: "إرسال", + welcomeDescription: "بعض الأمور التي يُستحسن فعلها قبل البدء.", + welcomeSkip: "ليس الآن", + welcomeStepProfileAction: "حفظ الملف", + welcomeBootstrapRunning: "جارٍ تحميل لقطة الـpub. عند الانتهاء يتم الانضمام إلى الـpub تلقائيًا؛ يمكنك متابعة استخدام Oasis في هذه الأثناء.", + welcomeCompleteTitle: "كل شيء جاهز.", + welcomeCompleteText: "عقدتك جاهزة. من هنا تنمو الشبكة مع السكان الذين تدعمهم.", + welcomeFindPeople: "ابحث عن السكان", + devFilterFiles: "الملفات", + devFilterSearch: "بحث", + devFilterModules: "الوحدات", + devFilterTranslations: "الترجمات", + devFilterStyles: "الأنماط", + devFilterTests: "الاختبارات", + devVersion: "الإصدار", + devNodeVersion: "Node", + devStatsTests: "اختبارات", + devParentFolder: "المجلد الأعلى", + devOpenFolder: "فتح", + devDescription: "استكشف الشيفرة المصدرية التي تعمل على هذه العقدة.", + devRoot: "الجذر", + devSearchPlaceholder: "النص المطلوب البحث عنه في الشيفرة", + devAnyExtension: "أي ملف", + devStatsFiles: "الملفات", + devStatsLines: "الأسطر", + devStatsModules: "الوحدات", + devStatsLanguages: "اللغات", + devNotViewable: "غير قابل للعرض", + devEmptyFolder: "هذا المجلد فارغ.", + devSize: "الحجم", + devShowing: "يعرض", + devLine: "سطر", + devReportBug: "الإبلاغ عن خلل", + devCreateTask: "إنشاء مهمة", + devRaw: "RAW", + devReportContext: "وُجد أثناء قراءة الشيفرة المصدرية لأواسيس", + devTaskPrefix: "مراجعة", + devPrevPage: "الأسطر السابقة", + devNextPage: "الأسطر التالية", + devMatches: "تطابقات", + devFilesScanned: "ملفات مفحوصة", + devNoResults: "لا توجد تطابقات بعد.", + devColModule: "الوحدة", + devColState: "الحالة", + devColModel: "النموذج", + devColView: "العرض", + devColRoutes: "المسارات", + devColTests: "الاختبارات", + devOn: "مفعّل", + devOff: "معطّل", + modulesDevLabel: "التطوير", + modulesDevDescription: "وحدة لإدارة شيفرة أواسيس المصدرية.", themeIntro: "اختر سمة.", setTheme: "تعيين السمة", @@ -501,16 +579,49 @@ module.exports = { languageDescription: "إذا كنت ترغب في استخدام لغة أخرى، اخترها هنا.", setLanguage: "تعيين اللغة", - peerConnections: "الأقران", peerConnectionsIntro: "إدارة جميع اتصالاتك مع الأقران الآخرين.", + peersConnectFailed: "تعذّر الاتصال: {reason}.", + peerStateLabel: "الحالة", + peerAddressLabel: "العنوان", + peerLastChangeLabel: "آخر تغيير", + peerStateConnected: "متصل", + peerStateConnecting: "جارٍ الاتصال", + peerStateStaged: "متاح", + peerErrSilent: "لا نشاط", + peerErrTimeout: "لا يستجيب", + peerErrRefused: "يرفض الاتصال", + peerErrNotFound: "عنوان غير معروف", + peerErrUnreachable: "الشبكة غير قابلة للوصول", + peerErrKeys: "يستخدم مفاتيح مختلفة", + peerErrIdentity: "لا يقبل اتصالك", + peerErrTor: "TOR فقط", + peerErrDropped: "انقطع الاتصال", + peerErrPaused: "الشبكة متوقفة مؤقتًا", + peerErrOther: "خطأ في الاتصال", + peerOutdated: "قديم", + peerVersionUnannounced: "لا يعلن عن إصداره: Oasis قديم", + peersTorMissing: "هذا الـPUB لا يسمح إلا بالاتصالات عبر TOR.", peerConnectionsTitle: "الاتصالات", + peersOnlineHint: "العقد المتصلة بك الآن.", + peersDiscoveredHint: "عقد ذات عنوان معروف غير متصلة حاليًا.", + peersUnknownHint: "عقد في شبكتك الاجتماعية بدون عنوان معروف.", + peersConnectionsHint: "العناوين التي يمكن لهذه العقدة الاتصال بها وحالة كل اتصال.", + peersTabLan: "الشبكة المحلية", + peersTabHealth: "الحالة", + peersHealthHint: "حال كل pub أو نظير معروف، داخل الشبكة المحلية وخارجها.", + peersHealthReachable: "يمكن الوصول إليه", + peersHealthFailing: "يفشل", + peersHealthFailures: "الإخفاقات", + peersFailingSince: "يفشل منذ", + peersHealthForget: "نسيان غير القابلة للوصول", + peersLanHint: "العقد التي تم العثور عليها في شبكتك المحلية.", + peersLanLastSeen: "آخر ظهور", + peersLanEmpty: "لم يُعثر على أي عقدة في الشبكة المحلية بعد.", online: "متصل", offline: "غير متصل", discovered: 'مُكتشَف', unknown: 'غير معروف', - lanBroadcastLabel: "بث LAN", lanBroadcastActive: "نشط (بث UDP المتعدد عبر LAN)", - lanBroadcastDisabled: "معطل (وضع pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -523,127 +634,102 @@ module.exports = { noConnections: "لا يوجد أقران متصلون.", noDiscovered: "لم يتم اكتشاف أقران.", noUnknownPeers: "لا توجد أقران غير معروفة في رسم بياني الأصدقاء.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "لا توجد عقد مسجلة بعد.", + peerConnect: "اتصال", + peerDisconnect: "قطع الاتصال", peerRefresh: "تحديث", peerPruneIdle: "إزالة الخاملة", + peersPause: "إيقاف مؤقت", + peersResume: "استئناف", + peersPausedNote: "متوقف مؤقتًا: لا يتصل Oasis بأحد ولا يمكن لأحد الاتصال به.", + peersPauseHint: "يوقف كل الاتصالات: لا يتصل Oasis بعُقد أخرى ولا يقبلها حتى تستأنف.", peerExport: "تصدير", peerImport: "استيراد", peerImportTitle: "استيراد قائمة الأقران", peerImportPlaceholder: "ألصق عنوان multiserver واحدًا لكل سطر، أو حمّل ملف .txt…", - noSupportedConnections: "لا يوجد أقران مدعومون.", - noBlockedConnections: "لا يوجد أقران محظورون.", - noRecommendedConnections: "لا يوجد أقران مُوصى بهم.", connectionActionIntro: "", - startNetworking: "بدء الشبكة", - stopNetworking: "إيقاف الشبكة", - restartNetworking: "إعادة تشغيل الشبكة", - sync: "مزامنة الشبكة", indexes: "الفهارس", + indexesRebuilt: "أُعيد بناء الفهارس", + indexesRebuildFailed: "تعذّر إعادة بناء الفهارس", + indexesFiles: "ملفات الفهرس", indexesDescription: "إعادة بناء الفهارس آمنة، وقد تصلح بعض أنواع الأخطاء.", homePageTitle: "الصفحة الرئيسية", homePageDescription: "اختر الوحدة التي تريدها كصفحتك الرئيسية.", saveHomePage: "تعيين الرئيسية", invites: "الدعوات", - invitesTitle: "الدعوات", - invitesInvites: "الدعوات", invitesDescription: "إدارة وتطبيق رموز الدعوة في شبكتك.", + invitesPubsHint: "ألصق رمز دعوة لخادم pub للاتحاد معه والبدء بنسخ سكانه.", + invitesFederationsHint: "الخوادم المتحدة معك: حدّثها، أزل غير المتاحة أو صدّر القائمة.", invitesTribesTitle: "القبائل", - invitesTribeInviteCodePlaceholder: "أدخل رمز دعوة القبيلة", - invitesTribeJoinButton: "انضم إلى القبيلة", invitesChatsTitle: "المحادثات", - invitesChatInviteCodePlaceholder: "أدخل رمز دعوة المحادثة", - invitesChatJoinButton: "انضم إلى المحادثة", - invitesPadsTitle: "الوسادات", - invitesPadInviteCodePlaceholder: "أدخل رمز دعوة الوسادة", - invitesPadJoinButton: "انضم إلى الوسادة", + invitesPadsTitle: "المستندات التعاونية", invitesCalendarsTitle: "التقاويم", - invitesCalendarInviteCodePlaceholder: "أدخل رمز دعوة التقويم", - invitesCalendarJoinButton: "انضم إلى التقويم", invitesEventsTitle: "الأحداث", - invitesEventInviteCodePlaceholder: "أدخل رمز دعوة الحدث", - invitesEventJoinButton: "انضم إلى الحدث", invitesForumsTitle: "المنتديات", - invitesForumInviteCodePlaceholder: "أدخل رمز دعوة المنتدى", - invitesForumJoinButton: "انضم إلى المنتدى", invitesMapsTitle: "خرائط", - invitesMapInviteCodePlaceholder: "أدخل رمز دعوة الخريطة", - invitesMapJoinButton: "انضم إلى الخريطة", invitesShopsTitle: "المتاجر", - invitesShopInviteCodePlaceholder: "أدخل رمز دعوة المتجر", - invitesShopJoinButton: "الانضمام إلى المتجر", invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "PUBs مقترحة", + invitesFilterOthers: "أخرى", + invitesJoinTitle: "الدعوات", + invitesJoinHint: "ألصق أي رمز دعوة، أيًّا كان غرضه، أو معرّف Oasis لأحد السكان.", + invitesJoinPlaceholder: "رمز دعوة أو معرّف Oasis", + invitesJoinButton: "انضمام", + invitesUsedTitle: "أحدث الدعوات", + invitesUsedEmpty: "لم تستعمل أي دعوة بعد.", + invitesUsedType: "النوع", + invitesUsedName: "الاسم", + invitesUsedDate: "التاريخ", invitesPubInviteCodePlaceholder: "أدخل رمز دعوة PUB", invitesAcceptInvite: "انضم إلى PUB", + invitesPubUses: "الدعوات المستخدمة", + invitesPubExhausted: "لم يتبقَّ لدى هذا الـ PUB أي دعوات.", invitesAlreadyFederated: "أنت متحد بالفعل مع هذا الخادم.", + invitesJoinPubMode: "يعمل Oasis هذا كـ PUB في وضع القراءة فقط. لربطه بـ PUB آخر، استخدم في الطرفية: ./oasis.sh join <رمز الدعوة>", + invitesJoinLocalOnly: "لا تُقبل الدعوات إلا من الجهاز الذي يعمل عليه Oasis، أو باستخدام رابط الإدارة الذي يظهر عند التشغيل.", invitesFederationsTitle: "الاتحادات", invitesAcceptedInvites: "متحدة", - invitesNoInvites: "لم يتم قبول أي دعوات بعد.", invitesUnfollow: "إلغاء المتابعة", invitesFollow: "متابعة", invitesUnfollowedInvites: "غير متحدة", invitesNoFederatedPubs: "لا توجد pubs متحدة.", invitesNoUnfollowed: "لا توجد pubs غير متحدة.", - invitesUnreachablePubs: "يتعذر الوصول", - invitesNoUnreachablePubs: "لا توجد pubs يتعذر الوصول إليها.", invitesPubsRefresh: "تحديث", - invitesPubsClearUnreachable: "إزالة المتعذر الوصول إليها", + invitesPubsClearUnreachable: "تنظيف", invitesPubsExport: "تصدير", invitesPubsImport: "استيراد", invitesPubsImportTitle: "استيراد pubs", invitesPubsImportPlaceholder: "ألصق عنوان multiserver أو رمز دعوة لكل سطر، أو حمّل ملف .txt…", - currentlyUnreachable: "خطأ!", - errorDetails: "تفاصيل الخطأ", genericError: "حدث خطأ.", panicMode: "وضع الطوارئ!", encryptData: "عيّن كلمة مرور (32 حرفًا على الأقل) لتشفير البلوكتشين الخاص بك", decryptData: "أدخل كلمة المرور لفك تشفير البلوكتشين الخاص بك", - panicModeDescription: "تشفير/فك تشفير أو حذف البلوكتشين الخاص بك", removeDataDescription: "تحذير: لا يمكن التراجع عن هذه العملية.", - encryptPanicButton: "تشفير البلوكتشين", - decryptPanicButton: "فك تشفير البلوكتشين", removePanicButton: "حذف جميع بياناتك!", searchTitle: "بحث", searchDescriptionLabel: "ابحث عن محتوى في شبكتك.", - searchLanguagesLabel: "اللغات", - searchSkillsLabel: "المهارات", searchPlaceholder:"ابحث عن محتوى...", - searchDateLabel:"التاريخ", searchLocationLabel:"الموقع", searchPriceLabel:"السعر", - searchUrlLabel:"الرابط", searchCategoryLabel:"الفئة", - searchStartLabel:"وقت البدء", - searchEndLabel:"وقت الانتهاء", searchPriorityLabel:"الأولوية", searchStatusLabel:"الحالة", statusLabel:"الحالة", - totalVotesLabel:"إجمالي الأصوات", noResultsFound:"لم يتم العثور على نتائج.", votesOption:"خيارات التصويت", voteYesLabel:"نعم", voteNoLabel:"لا", allTypesLabel: "غير محدود", - ABSTENTIONLabel:"امتناع", YESLabel:"نعم", NOLabel:"لا", - FOLLOW_MAJORITYLabel: "اتباع الأغلبية", - CONFUSEDLabel: "مرتبك", - NOT_INTERESTEDLabel: "غير مهتم", - StatusLabel:"الحالة", votesOptionYesLabel:"نعم", votesOptionNoLabel:"لا", votesOptionAbstentionLabel:"امتناع", votesQuestionLabel:"السؤال", - votesCreatedByLabel:"أنشأه", voteStatusOpen:"مفتوح", voteStatusClosed:"مغلق", - imageSearchLabel: "أدخل كلمات للبحث عن صور مُعلَّمة بها.", commentDescription: ({ parentUrl }) => [ " علّق على ", a({ href: parentUrl }, " المحادثة"), @@ -654,61 +740,27 @@ module.exports = { a({ href: parentUrl }, " منشور"), ], subtopicTitle: ({ authorName }) => [`موضوع فرعي على منشور @${authorName}`], - mysteryDescription: "نشر منشورًا غامضًا", oasisDescription: "شبكة مشروع OASIS", searchSubmit: "لنبحث...", - postLabel: "المنشورات", - aboutLabel: "السكان", - feedLabel: "التغذيات", votesLabel: "التصويتات", - reportLabel: "التقارير", - imageLabel: "الصور", - videoLabel: "الفيديوهات", - audioLabel: "الملفات الصوتية", - documentLabel: "المستندات", - torrentLabel: "التورنت", pdfFallbackLabel: "مستند PDF", - eventLabel: "الأحداث", - taskLabel: "المهام", - transferLabel: "التحويلات", - curriculumLabel: "السيرة الذاتية", - bookmarkLabel: "العلامات المرجعية", - tribeLabel: "القبائل", - marketLabel: "السوق", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "المحافظ", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + shopLabel: "المتاجر", + voteLabel: "الأصوات", submit: "إرسال", - subjectLabel: "الموضوع", editProfile: "تعديل الصورة الرمزية", - profileQrAlt: "رمز QR للملف الشخصي", - profileQrHint: "امسح ضوئيًا لنسخ معرّف Oasis هذا.", oasisIdLabel: "OasisID", - spreadLabel: "نشر", - spreadHint: "نشر هذا لداعميك (يتم النسخ عبر تغذيتك).", + spreadContent: "نسخ", welcomePmSubject: "Hello World!", - welcomePmBody: "مرحبًا بك في أوازيس — شبكة اجتماعية حرة، نِدّ لِنِد، اتحادية ومشفّرة، ذات بنية موزّعة بالدعوة فقط.\n\nبعض الأماكن المثيرة للاهتمام للبدء:\n\n- /profile — عدّل صورتك الرمزية واسمك ووصفك والوحدات التي تظهر في جانبك العام.\n- /invites — أضف pub للاتحاد مع بقية الشبكة.\n- /peers — اطّلع على من هو متصل، أعد مسح شبكتك المحلية، صدّر أو استورد قوائم الأقران.\n- /inhabitants — اكتشف الصور الرمزية للآخرين وزرها.\n- /tribes — أنشئ مجموعات خاصة بتشفير من طرف إلى طرف أو انضم إليها.\n- /inbox — اقرأ وأرسل الرسائل الخاصة.\n- /activity — اطّلع على النشاط الأخير، نشاطك ونشاط شبكتك.\n- /publish — اكتب منشورًا أو شارك صورة أو صوتًا أو فيديو أو مستندًا.\n\nبعض الأماكن المثيرة للاهتمام للاتصال:\n\n- /fediverse — أدر حساباتك الأخرى في الفيديفيرس، بما في ذلك إرسال واستقبال المحتوى.\n\nأُرسلت هذه الرسالة بشكل خاص منك إلى نفسك، لذا لم يلزم أي اتصال لاستلامها.", - profileVisibilityTitle: "ظهور الملف الشخصي العام", - profileVisibilityHint: "اختر الحقول التي يراها السكان الآخرون في ملفك الشخصي. افتراضيًا، يظهر Karma فقط.", + welcomePmBody: "مرحبًا بك في أوازيس — شبكة اجتماعية حرة، نِدّ لِنِد، اتحادية ومشفّرة، ذات بنية موزّعة بالدعوة فقط.\n\nبعض الأماكن المثيرة للاهتمام للبدء:\n\n- [/avatar](/profile) — عدّل صورتك الرمزية واسمك ووصفك والوحدات التي تظهر في جانبك العام.\n- /settings — اضبط نسختك المحلية: اللغة والسمة والوحدات والخصوصية والمزامنة.\n- /invites — أضف pub للاتحاد مع بقية الشبكة.\n- /peers — اطّلع على من هو متصل، أعد مسح شبكتك المحلية، صدّر أو استورد قوائم الأقران.\n- /inhabitants — اكتشف الصور الرمزية للسكان الآخرين وزرها.\n- /tribes — أنشئ مجموعات خاصة بتشفير من طرف إلى طرف أو انضم إليها.\n- /search — ابحث في محتوى الشبكة حسب النوع.\n\nبعض الأماكن المثيرة للاهتمام للاتصال:\n\n- /multiverse — أدر حساباتك الأخرى في الكون المتعدد، بما في ذلك إرسال واستقبال المحتوى.\n\nأُرسلت هذه الرسالة بشكل خاص منك إلى نفسك، لذا لم يلزم أي اتصال لاستلامها.", profileSensorsSectionTitle: "المستشعرات", profileVisibilityActivity: "مستوى النشاط", - profileVisibilityDevice: "الجهاز", profileDeviceLockedHint: "هذا المستشعر مفعّل دائمًا: يتيح للآخرين معرفة الجهاز الذي تتصل منه ولا يمكن تعطيله.", profileVisibilityKarma: "نقاط KARMA", - profileVisibilityUbi: "UBI", - profileVisibilityWallet: "محفظة ECOIN", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityUbi: "الدخل الأساسي", + profileVisibilityWallet: "محفظة ECOin", + profileVisibilityEcoTax: "ضريبة ECO", + profileVisibilityLarpSign: "ختم L.A.R.P.", profileVisibilityGpg: "مفتاح GPG", - profileVisibilityClearnet: "نشر ملفي الشخصي", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "المتاجر", profileClearnetJobsLabel: "الوظائف", @@ -720,6 +772,10 @@ module.exports = { profileClearnetImagesLabel: "الصور", profileClearnetDocumentsLabel: "المستندات", profileClearnetTorrentsLabel: "التورنت", + profileClearnetPodcastsLabel: "البودكاست", + profileClearnetMarketLabel: "السوق", + profileClearnetFeedLabel: "التدفق", + profileClearnetWikiLabel: "ويكي", editProfileDescription: "", profileName: "الاسم", @@ -745,7 +801,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "تأكيدات", walletConfirm: "تأكيد", - walletDescription: "إدارة أصولك الرقمية، بما في ذلك إرسال واستقبال ECOin، وعرض رصيدك، والوصول إلى سجل معاملاتك.", + walletDescription: "أدر محفظة ECOin الخاصة بك.", walletDate: "التاريخ", walletFee: "الرسوم (كلما ارتفعت الرسوم، زادت سرعة معالجة معاملتك)", walletFeeLine: ({ fee }) => `الرسوم: ECO ${fee}`, @@ -753,6 +809,86 @@ module.exports = { walletReceive: "استقبال", walletReset: "إعادة تعيين", walletSend: "إرسال", + walletPayingTransfer: "دفع التحويل", + walletCreateTransferLabel: "إنشاء عقد ذكي (تحويل)", + walletTransferNotifiedNote: "تم إخطار الطرف الآخر بهذه الدفعة", + walletTransferCreatedNote: "تم إنشاء تحويل (إيصال) لهذه الدفعة", + walletTransferNoPayeeNote: "لم ينشر أي ساكن هذا العنوان، فلم يُنشأ تحويل", + walletTransferReceiptConcept: "دفعة من المحفظة", + walletPaymentPmText: "أرسل لك دفعة", + walletPayeeLabel: "المستلم", + walletConceptLabel: "البيان", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "لقد استلمت دخلك الأساسي.", + bankingBotPaymentTitle: "لقد استلمت دفعة.", + bankingBotUbiPaidText: "تم دفع دخلك الأساسي الشامل", + bankUbiPubThisNode: "هذه العقدة تدفع الدخل الأساسي", + bankUbiPubConnected: "PUB الدخل الأساسي", + bankUbiPubNone: "لا يوجد PUB يعلن عن الدخل الأساسي في شبكتك بعد", + bankUbiAddressPublished: "تم نشر عنوان ECOin", + bankUbiAddressMissing: "انشر عنوان ECOin الخاص بك لتلقي الدخل الأساسي", + bankUbiClaimedThisMonth: "تمت المطالبة هذا الشهر", + bankUbiPubBalance: "محفظة PUB المتاحة للدخل الأساسي", + bankNoPubAddress: "لم ينشر PUB الدخل الأساسي عنوان ECOin بعد.", + bankUbiTab: "الدخل الأساسي", + bankDonateEco: "تبرّع!", + donateTooltip: "تبرّع بـ ECOin", + bankNoUserAddress: "هذا الساكن لا يشارك عنوان ECOin.", + bankUbiPubDefault: "افتراضي", + bankUbiPool: "الرصيد", + bankUbiLastSeen: "آخر ظهور", + bankUbiLastPayout: "آخر إجراء", + bankUbiPaidOut: "تم توزيعه", + bankRangeToday: "اليوم", + bankRangeWeek: "أسبوع", + bankRangeMonth: "شهر", + bankRangeYear: "سنة", + bankRangeFiveYears: "5 سنوات", + bankRangeAll: "الكل", + bankChartValueTitle: "تطور قيمة ECOin", + bankChartValueHours: "المعادل الزمني (ساعة)", + bankChartWealthTitle: "إعادة توزيع ثروة ECOin", + bankChartDistributed: "الدخل الأساسي الموزّع (ECO)", + bankChartRebalanceTitle: "إعادة توزيع الثروة بين الـ PUBs", + bankChartRebalanced: "ECO المنقولة بين الـ PUBs", + bankChartTaxes: "الضرائب المحتجزة (ECO)", + bankChartNoDataRange: "لا توجد بيانات في هذا النطاق.", + bankChartNoDataYet: "لا توجد بيانات بعد", + bankChartPerHour: "في الساعة", + bankChartPerDay: "في اليوم", + bankChartPerMonth: "في الشهر", + bankTotalUbiDistributed: "إجمالي الدخل الأساسي المعاد توزيعه (الشبكة)", + bankTotalTaxesWithheld: "إجمالي الضرائب المحتجزة (الشبكة)", + bankInflationIssuance: "معدل الإصدار (سنوي، من مكافأة الكتلة)", + bankRulesEligibilityTitle: "من يمكنه تلقي الدخل الأساسي", + bankRulesEligibility1: "عنوان ECOin منشور في ملفك (الملف → المستشعرات → محفظة ECOin).", + bankRulesEligibility2: "تغذية عمرها أكثر من 30 يوماً ولها نشاط في الشبكة: كارما فوق الصفر.", + bankRulesEligibility3: "مطالبة واحدة في الشهر (الحقبة)، من Banking بزر المطالبة بالدخل الأساسي. يمكن رفض الشهر الحالي بزر رفض الدخل الأساسي.", + bankRulesEligibility4: "الحد الأدنى 1 ECO يُدفع دائماً؛ الضرائب تقلّل فقط الجزء الذي يتجاوزه ولا تُحصَّل أبداً بشكل منفصل.", + bankRulesEligibility5: "يدفع الـ PUB في دورته التالية وينشر تحويل دخل أساسي يمكنك تأكيده.", + bankRulesEligibility6: "إذا غاب عنوانك أو كانت التغذية حديثة جداً أو رفضت الشهر أو لم يكن لدى الـ PUB أموال، تُتخطى المطالبة وتُسجَّل على الـ PUB.", + copyLinkTitle: "نسخ الرابط", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "رفض الدخل الأساسي", + bankUbiRefusedThisMonth: "لقد رفضت دخل هذا الشهر الأساسي.", + bankingBotUbiAvailableTitle: "يمكنك المطالبة بدخلك الأساسي.", + bankingBotUbiAvailableText: "دخلك الأساسي لهذا الشهر متاح", + bankingBotConfirmTitle: "لديك عقد ذكي بانتظار التأكيد", + bankingBotConfirmText: "عقود ذكية بانتظار تأكيدك", + bankChartZoomHint: "انقر للتكبير", + bankChartPoolTitle: "رصيد الدخل الأساسي (عبر الزمن)", + bankChartNetworkUbiTitle: "إجمالي الدخل الأساسي المعاد توزيعه (الشبكة)", + bankChartSupplyTitle: "المعروض المتداول من ECOin (عبر الزمن)", + bankChartInflationTitle: "تضخم ECOin (عبر الزمن)", + bankChartInflationSeries: "المعدل السنوي (%)", + bankChartPool: "إجمالي الرصيد (ECO)", + bankChartKarmaTitle: "KARMA Scoring (عبر الزمن)", + bankChartFundsTitle: "أموالك ECOin (عبر الزمن)", + bankChartKarma: "كارما", + bankWalletReady: "ECOin متصل", + bankWalletNotConfigured: "ECOin غير متصل", + transfersPayWithWallet: "ادفع!", walletStatus: "الحالة", walletDisconnected: [ "محفظة ECOin ", @@ -762,9 +898,11 @@ module.exports = { " أو حالة الاتصال.", ], walletSentToLine: ({ destination, amount }) => `تم إرسال ECO ${amount} إلى ${destination}`, - walletSettingsTitle: "المحفظة", + walletNotConfigured: "محفظة ECOin الخاصة بك غير مُهيأة بعد.", walletSettingsDescription: "دمج Oasis مع محفظة ECOin الخاصة بك.", - walletSettingsDocLink: "دليل تثبيت ECOin", + walletSettingsMobileDescription: "اربط Oasis بتطبيق ECOin Wallet على هذا الجهاز.", + walletDownloadEcoin: "نزّل ECOin!", + walletRpcUrl: "عنوان RPC", walletStatusMessages: { invalid_amount: "مبلغ غير صالح", invalid_dest: "عنوان وجهة غير صالح", @@ -780,41 +918,24 @@ module.exports = { walletUser: "اسم المستخدم", walletPass: "كلمة المرور", walletConfiguration: "تعيين المحفظة", + walletDisconnectButton: "فصل المحفظة", cipher: "التشفير", cipherTitle: "التشفير", cipherDescription: "تشفير وفك تشفير نصك بشكل متماثل (باستخدام كلمة مرور مشتركة).", randomPassword: "كلمة مرور عشوائية", - cipherEncryptTitle: "تشفير النص", - cipherEncryptDescription: "أدخل النص للتشفير", - cipherTextLabel: "النص للتشفير", cipherTextPlaceholder: "أدخل النص للتشفير...", cipherPasswordLabel: "عيّن كلمة مرور (32 حرفًا على الأقل) لتشفير نصك", cipherPasswordDecryptLabel: "عيّن كلمة مرور (32 حرفًا على الأقل) لفك تشفير نصك", cipherPasswordPlaceholder: "أدخل كلمة مرور...", cipherEncryptButton: "تشفير", - cipherDecryptTitle: "فك تشفير النص", - cipherDecryptDescription: "أدخل النص لفك التشفير", cipherEncryptedMessageLabel: "النص المشفر", cipherDecryptedMessageLabel: "النص المفكوك", - cipherPasswordUsedLabel: "كلمة المرور المستخدمة للتشفير (احتفظ بها!)", cipherEncryptedTextPlaceholder: "أدخل النص المشفر...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "أدخل متجه التهيئة...", cipherDecryptButton: "فك التشفير", password: "كلمة المرور", text: "النص", encryptedText: "النص المشفر", iv: "متجه التهيئة (IV)", - encryptTitle: "تشفير نصك", - encryptDescription: "أدخل النص الذي تريد تشفيره وقدّم كلمة مرور.", - encryptButton: "تشفير", - decryptTitle: "فك تشفير نصك", - decryptDescription: "أدخل النص المشفر وقدّم نفس كلمة المرور المستخدمة للتشفير.", - decryptButton: "فك التشفير", - passwordLengthError: "يجب أن تكون كلمة المرور 32 حرفًا على الأقل.", - missingFieldsError: "لم يتم تقديم النص أو كلمة المرور أو IV.", - encryptionError: "خطأ في تشفير النص.", - decryptionError: "خطأ في فك تشفير النص.", bookmarkTitle: "العلامات المرجعية", bookmarkDescription: "اكتشف وأدِر العلامات المرجعية في شبكتك.", bookmarkAllSectionTitle: "العلامات المرجعية", @@ -831,19 +952,16 @@ module.exports = { bookmarkFilterRecent: "الأخيرة", bookmarkCreateButton: "إنشاء علامة مرجعية", bookmarkUpdateButton: "تحديث", - bookmarkDeleteButton: "حذف", bookmarkAddFavoriteButton: "إضافة للمفضلة", bookmarkRemoveFavoriteButton: "إزالة من المفضلة", bookmarkUrlLabel: "الرابط", - bookmarkUrlPlaceholder: "https://example.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "الوصف", bookmarkDescriptionPlaceholder: "اختياري", bookmarkTagsLabel: "الوسوم", bookmarkTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", - bookmarkCategoryLabel: "الفئة", - bookmarkCategoryPlaceholder: "اختياري", bookmarkLastVisitLabel: "آخر زيارة", - bookmarkSearchPlaceholder: "ابحث عن رابط، وسوم، فئة، مؤلف...", + bookmarkSearchPlaceholder: "ابحث في العلامات المرجعية...", bookmarkSortRecent: "الأحدث", bookmarkSortOldest: "الأقدم", bookmarkSortTop: "الأكثر تصويتًا", @@ -856,8 +974,6 @@ module.exports = { noLastVisit: "لا توجد زيارة أخيرة", videoTitle: "الفيديوهات", videoDescription: "استكشف وأدِر محتوى الفيديو في شبكتك.", - videoPluginTitle: "العنوان", - videoPluginDescription: "الوصف", videoMineSectionTitle: "فيديوهاتك", videoCreateSectionTitle: "رفع فيديو", videoUpdateSectionTitle: "تحديث الفيديو", @@ -884,12 +1000,11 @@ module.exports = { videoDescriptionPlaceholder: "اختياري", videoNoFile: "لم يتم تقديم ملف فيديو", noVideos: "لا توجد فيديوهات متاحة.", - videoSearchPlaceholder: "ابحث عن عنوان، وسوم، مؤلف...", + videoSearchPlaceholder: "ابحث في الفيديوهات...", videoSortRecent: "الأحدث", videoSortOldest: "الأقدم", videoSortTop: "الأكثر تصويتًا", videoSearchButton: "بحث", - videoMessageAuthorButton: "رسالة خاصة", videoUpdatedAt: "تم التحديث", videoNoMatch: "لا توجد فيديوهات تطابق بحثك.", documentTitle: "المستندات", @@ -908,7 +1023,6 @@ module.exports = { documentFilterFavorites: "المفضلة", documentCreateButton: "رفع مستند", documentUpdateButton: "تحديث", - documentDeleteButton: "حذف", documentAddFavoriteButton: "إضافة للمفضلة", documentRemoveFavoriteButton: "إزالة من المفضلة", documentMessageAuthorButton: "رسالة خاصة", @@ -921,7 +1035,7 @@ module.exports = { documentDescriptionPlaceholder: "اختياري", documentNoFile: "لا يوجد ملف.", noDocuments: "لا توجد مستندات متاحة.", - documentSearchPlaceholder: "ابحث عن عنوان، وسوم، وصف، مؤلف...", + documentSearchPlaceholder: "ابحث في المستندات...", documentSortRecent: "الأحدث", documentSortOldest: "الأقدم", documentSortTop: "الأكثر تصويتًا", @@ -930,8 +1044,6 @@ module.exports = { documentUpdatedAt: "تم التحديث", audioTitle: "الملفات الصوتية", audioDescription: "استكشف وأدِر المحتوى الصوتي في شبكتك.", - audioPluginTitle: "العنوان", - audioPluginDescription: "الوصف", audioMineSectionTitle: "ملفاتك الصوتية", audioCreateSectionTitle: "رفع ملف صوتي", audioUpdateSectionTitle: "تحديث الملف الصوتي", @@ -942,22 +1054,21 @@ module.exports = { audioFilterMine: "خاصتي", audioFilterRecent: "الأخيرة", audioFilterTop: "الأفضل", - audioFilterBlockchain: "بلوكتشين", audioCreateButton: "رفع ملف صوتي", audioUpdateButton: "تحديث", - audioDeleteButton: "حذف", audioAddFavoriteButton: "إضافة للمفضلة", audioRemoveFavoriteButton: "إزالة من المفضلة", audioFileLabel: "اختر ملفًا صوتيًا (.mp3, .wav, .ogg)", audioTagsLabel: "الوسوم", audioTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + tagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", audioTitleLabel: "العنوان", audioTitlePlaceholder: "اختياري", audioDescriptionLabel: "الوصف", audioDescriptionPlaceholder: "اختياري", audioNoFile: "لم يتم تقديم ملف صوتي", noAudios: "لا توجد ملفات صوتية متاحة.", - audioSearchPlaceholder: "ابحث عن عنوان، وسوم، مؤلف...", + audioSearchPlaceholder: "ابحث في الملفات الصوتية...", audioSortRecent: "الأحدث", audioSortOldest: "الأقدم", audioSortTop: "الأكثر تصويتًا", @@ -969,6 +1080,7 @@ module.exports = { audioFilterFavorites: "المفضلة", favoritesTitle: "المفضلة", favoritesDescription: "كل المحتوى المفضل لديك في مكان واحد.", + favoritesSearchPlaceholder: "ابحث في المفضلة...", favoritesFilterAll: "الكل", favoritesFilterRecent: "الأخيرة", favoritesFilterAudios: "الملفات الصوتية", @@ -980,22 +1092,16 @@ module.exports = { favoritesFilterChats: "الدردشات", favoritesFilterCalendars: "التقويمات", favoritesFilterVideos: "الفيديوهات", - favoritesRemoveButton: "إزالة من المفضلة", favoritesNoItems: "لا توجد مفضلات بعد.", yourContacts: "جهات اتصالك", allInhabitants: "السكان", - allCVs: "جميع السير الذاتية", + allCVs: "السير الذاتية", discoverPeople: "اكتشف السكان في شبكتك.", - allInhabitantsButton: "الكل", - contactsButton: "المدعومون", - CVsButton: "السير الذاتية", - matchSkills: "مطابقة المهارات", - matchSkillsButton: "مطابقة المهارات", - suggestedButton: "المقترحون", - searchInhabitantsPlaceholder: "تصفية السكان حسب الاسم …", + searchInhabitantsPlaceholder: "ابحث في السكان...", filterLocation: "تصفية السكان حسب الموقع …", filterLanguage: "تصفية السكان حسب اللغة …", filterSkills: "تصفية السكان حسب المهارات …", + cvSkillsCount: "المهارات", applyFilters: "تطبيق الفلاتر", locationLabel: "الموقع", languagesLabel: "اللغات", @@ -1004,15 +1110,15 @@ module.exports = { mutualFollowers: "المتابعون المتبادلون", suggestedFollowsYou: "يتابعك", latestInteractions: "آخر التفاعلات", - viewAvatar: "عرض الصورة الرمزية", - viewCV: "عرض السيرة الذاتية", suggestedSectionTitle: "المقترحون", topkarmaSectionTitle: "أفضل كارما", topecoSectionTitle: "أفضل إيكو", topactivitySectionTitle: "الأكثر نشاطًا", + "TOP INACTIVITYButton": "الأقل نشاطاً", + topinactivitySectionTitle: "السكان الأقل نشاطاً", blockedSectionTitle: "المحظورون", gallerySectionTitle: "المعرض", - blockedButton: "المحظورون", + topSectionTitle: "الأعلى", blockedLabel: "مستخدم محظور", inhabitantviewDetails: "عرض التفاصيل", keepReading: "متابعة القراءة...", @@ -1027,10 +1133,23 @@ module.exports = { parliamentFilterCandidatures: "الترشيحات", parliamentFilterProposals: "المقترحات", parliamentFilterLaws: "القوانين", - parliamentFilterHistorical: "السجل التاريخي", + parliamentFilterRevocations: "الإقالات", + parliamentFilterHistorical: "السجل", parliamentFilterLeaders: "القادة", parliamentFilterRules: "القواعد", parliamentGovernmentCard: "الحكومة الحالية", + parliamentSeatsGovernment: "الحكومة", + parliamentSeatsOpposition: "المعارضة", + parliamentSeatsAbstention: "امتناع", + parliamentSeatsQuorum: "نصاب الانتخاب", + parliamentSeatsPass: "عتبة الموافقة", + parliamentSeatsByElection: "حسب الانتخاب", + parliamentSeatsByHouses: "حسب بيت LARP", + parliamentSeatsSeats: "مقاعد", + parliamentSeatsSeat: "مقعد", + parliamentSeatsLeader: "القائد", + parliamentSeatsVotesFor: "أصوات لـ", + parliamentSeatsNoHouse: "بلا بيت", parliamentActorInPowerInhabitant: 'الساكن الحاكم', parliamentActorInPowerTribe: 'القبيلة الحاكمة', parliamentHistoricalGovernmentsTitle: 'الحكومات', @@ -1052,10 +1171,8 @@ module.exports = { parliamentPoliciesDeclined: "القوانين المرفوضة", parliamentPoliciesDiscarded: "القوانين الملغاة", parliamentEfficiency: "% الكفاءة", - parliamentFilterRevocations: 'الإلغاءات', parliamentRevocationFormTitle: 'إلغاء قانون', parliamentRevocationLaw: 'القانون', - parliamentRevocationTitle: 'العنوان', parliamentRevocationReasons: 'الأسباب', parliamentRevocationPublish: 'نشر الإلغاء', parliamentCurrentRevocationsTitle: 'الإلغاءات الحالية', @@ -1068,23 +1185,12 @@ module.exports = { parliamentNoGovernments: "لا توجد حكومات بعد.", parliamentCandidatureFormTitle: "اقتراح ترشيح", parliamentCandidatureIdPh: "معرّف Oasis (@...) أو اسم القبيلة", - parliamentCandidatureSlogan: "الشعار (140 حرفًا كحد أقصى)", - parliamentCandidatureSloganPh: "شعار قصير", parliamentCandidatureMethod: "الطريقة", parliamentCandidatureProposeBtn: "نشر الترشيح", - parliamentThDate: "تاريخ الاقتراح", - parliamentThSlogan: "الشعار", - parliamentThSince: "تاريخ الملف الشخصي", - parliamentThVotes: "الأصوات المُستلَمة", - parliamentThVoteAction: "تصويت", - parliamentTypeUser: "ساكن", - parliamentTypeTribe: "قبيلة", parliamentVoteBtn: "تصويت", parliamentProposalFormTitle: "اقتراح قانون", parliamentProposalDescription: "الوصف (≤1000)", parliamentProposalPublish: "نشر الاقتراح", - parliamentFinalize: "إنهاء", - parliamentDeadline: "الموعد النهائي", parliamentNoProposals: "لا توجد مقترحات قوانين بعد.", parliamentNoLaws: "لا توجد قوانين معتمدة بعد.", parliamentMethodDEMOCRACY: "ديمقراطية", @@ -1102,29 +1208,22 @@ module.exports = { parliamentVotesSlashTotal: "الأصوات/الإجمالي", parliamentVotesNeeded: "الأصوات المطلوبة", parliamentFutureLawsTitle: "القوانين المستقبلية", - parliamentNoFutureLaws: "لا توجد قوانين مستقبلية بعد.", parliamentVoteAction: "تصويت", - parliamentLeadersTitle: "القادة", parliamentThLeader: "الصورة الرمزية", parliamentPopulation: "عدد السكان", - parliamentThType: "النوع", - parliamentThInPower: "في السلطة", parliamentThPresented: "الترشيحات", parliamentNoLeaders: "لا يوجد قادة بعد.", parliamentCandidaturesListTitle: "قائمة الترشيحات", parliamentTimeRemaining: "الوقت المتبقي", - parliamentNoLeader: "لا يوجد ترشيح فائز بعد.", parliamentElectionsStatusTitle: "الحكومة القادمة", parliamentProposalDeadlineLabel: "الموعد النهائي", + parliamentViewReport: "عرض التقرير", parliamentProposalTimeLeft: "الوقت المتبقي", - parliamentProposalOnTrack: "في طريقه للتمرير", - parliamentProposalOffTrack: "لا يوجد دعم كافٍ بعد", parliamentRulesTitle: "كيف يعمل البرلمان", parliamentRulesIntro: "تُحسم الانتخابات كل شهرين؛ الترشيحات مستمرة وتُعاد عند اختيار حكومة.", parliamentRulesCandidates: "يمكن لأي ساكن ترشيح نفسه أو ساكن آخر أو أي قبيلة. يمكن لكل ساكن تقديم 3 ترشيحات كحد أقصى لكل دورة؛ تُرفض التكرارات في نفس الدورة.", parliamentRulesElection: "الفائز هو الترشيح الذي يحصل على أكبر عدد من الأصوات وقت الحسم.", parliamentRulesTies: "ترتيب كسر التعادل: أعلى كارما للساكن؛ إذا تعادلوا، أقدم ملف شخصي؛ إذا لا يزال متعادلاً، أقدم ترشيح؛ ثم ترتيب أبجدي حسب المعرّف.", - parliamentRulesFallback: "إذا لم يصوّت أحد، يفوز آخر ترشيح مقدّم. إذا لم تكن هناك ترشيحات، تُختار قبيلة عشوائية.", parliamentRulesTerm: "يعرض تبويب الحكومة الحكومة الحالية والإحصائيات.", parliamentRulesMethods: "الأشكال: فوضوية (أغلبية بسيطة)، ديمقراطية (50%+1)، أغلبية (80%)، أقلية (20%)، كارماتوقراطية (مقترحات أعلى كارما)، ديكتاتورية (موافقة فورية).", parliamentRulesAnarchy: "الفوضوية هي الوضع الافتراضي: إذا لم يُنتخب أي ترشيح عند الحسم، تُعلن الفوضوية. في ظل الفوضوية، يمكن لأي ساكن اقتراح قوانين.", @@ -1133,6 +1232,7 @@ module.exports = { parliamentRulesLaws: "عندما يصل اقتراح إلى حده الأدنى، يصبح قانونًا ويظهر في تبويب القوانين مع تاريخ سنّه.", parliamentRulesHistorical: "في تبويب السجل التاريخي يمكنك رؤية كل دورة حكومية حدثت وبيانات عن إدارتها.", parliamentRulesLeaders: "في تبويب القادة يمكنك رؤية ترتيب السكان/القبائل التي حكمت (أو ترشحت)، مرتبة حسب الكفاءة.", + parliamentRulesHemicycle: "يمنح نصف الدائرة مقعداً لكل ساكن: كتلة الحكومة (ناخبو الترشيح الفائز والقائد)، وكتلة معارضة لكل ترشيح آخر في الدورة، والامتناع. تُظهر العلامات المتقطعة نصاب الانتخاب (25٪ من التعداد) وعتبة موافقة الطريقة الحالية. «حسب بيت LARP» يلوّن المقاعد نفسها حسب البيت.", parliamentProposalVoteStatusLabel: "حالة التصويت", parliamentProposalOnTrackYes: "تم الوصول للحد الأدنى", parliamentProposalOnTrackNo: "دون الحد الأدنى", @@ -1146,11 +1246,8 @@ module.exports = { courtsCaseFormTitle: "فتح قضية", courtsCaseRespondent: "المتهم / المدعى عليه", courtsCaseRespondentPh: "معرّف Oasis (@...) أو اسم القبيلة", - courtsCaseMediatorsAccuser: "الوسطاء (المدعي)", + courtsCaseSubjectPlaceholder: "الموضوع أو وصف قصير", courtsCaseMethod: "طريقة الحل", - courtsCaseDescription: "الوصف (1000 حرف كحد أقصى)", - courtsCaseEvidenceTitle: "أدلة القضية", - courtsCaseEvidenceHelp: "أرفق صورًا أو ملفات صوتية أو مستندات (PDF) أو فيديوهات تدعم قضيتك.", courtsCaseSubmit: "رفع القضية", courtsNominateJudge: "ترشيح قاضٍ", courtsJudgeId: "القاضي", @@ -1183,34 +1280,33 @@ module.exports = { courtsMethodDICTATOR: "ديكتاتور", courtsMethodPOPULAR: "شعبي", courtsMethodKARMATOCRACY: "كارماتوقراطية", + courtsCaseType: "نوع القضية", + courtsPresetMinorConflict: "نزاع بسيط", + courtsPresetModerateConflict: "نزاع متوسط", + courtsPresetSevereConflict: "نزاع خطير", + courtsPresetHarassment: "تحرش أو إساءة", + courtsPresetContentModeration: "إشراف على المحتوى", + courtsPresetBanRestriction: "حظر أو تقييد", + courtsPresetGovernanceDispute: "نزاع حوكمة", + courtsStatusOPEN: "مفتوحة", + courtsStatusIN_PROGRESS: "قيد النظر", + courtsStatusDECIDED: "تم البت فيها", + courtsStatusCLOSED: "مغلقة", + courtsStatusSOLVED: "محلولة", + courtsStatusUNSOLVED: "غير محلولة", + courtsStatusDISCARDED: "مرفوضة", courtsRulesTitle: "كيف تعمل المحاكم", courtsRulesIntro: "المحاكم هي عملية يديرها المجتمع لحل النزاعات وتعزيز العدالة التصالحية. يُعطى الأولوية للحوار والأدلة الواضحة والعلاجات المتناسبة.", courtsRulesLifecycle: "العملية: 1) فتح قضية 2) اختيار الطريقة 3) تقديم الأدلة 4) الاستماع والمداولة 5) الحكم والعلاج 6) الامتثال والإغلاق 7) الاستئناف (إن وُجد).", - courtsRulesRoles: "المدعي: يفتح القضية. الدفاع: الشخص أو القبيلة المتهمة. الطريقة: الآلية التي يختارها المجتمع لتسهيل وتقييم الأدلة وإصدار الحكم. الشاهد: يقدم شهادة أو دليلاً. الوسطاء: أشخاص محايدون يدعوهم المدعي و/أو الدفاع، لديهم حق الوصول لجميع التفاصيل، يساعدون في تهدئة النزاع والمشاركة في صنع الاتفاقيات.", + courtsRulesRoles: "المدعي: يفتح القضية. الدفاع: الساكن أو القبيلة المتهمة. الطريقة: الآلية التي يختارها المجتمع لتسهيل وتقييم الأدلة وإصدار الحكم. الشاهد: يقدم شهادة أو دليلاً. الوسطاء: سكان محايدون يدعوهم المدعي و/أو الدفاع، لديهم حق الوصول لجميع التفاصيل، يساعدون في تهدئة النزاع والمشاركة في صنع الاتفاقيات.", courtsRulesEvidence: "الوصف حتى 1000 حرف. أرفق صورًا وملفات صوتية وفيديوهات ومستندات PDF ذات صلة وقانونية. لا تشارك بيانات خاصة حساسة دون موافقة.", courtsRulesDeliberation: "قد تكون الجلسات علنية أو خاصة. يضمن القضاة الاحترام ويطلبون التوضيحات وقد يتجاهلون المواد غير ذات الصلة أو غير القانونية.", courtsRulesVerdict: "يُعطى الأولوية للتصالح: الاعتذارات، اتفاقيات الوساطة، إدارة المحتوى، القيود المؤقتة، أو تدابير متناسبة أخرى. يجب تسجيل الأسباب.", courtsRulesAppeals: "الاستئناف: مسموح عند وجود أدلة جديدة أو خطأ إجرائي واضح. يجب تقديمه خلال 7 أيام ما لم يُذكر خلاف ذلك.", - courtsRulesPrivacy: "احترم الخصوصية والأمان. يتم إزالة التشهير والكراهية والتهديدات. قد يعدل القضاة أو يختمون أجزاء من السجل لحماية الأشخاص المعرضين للخطر.", + courtsRulesPrivacy: "احترم الخصوصية والأمان. يتم إزالة التشهير والكراهية والتهديدات. قد يعدل القضاة أو يختمون أجزاء من السجل لحماية السكان المعرضين للخطر.", courtsRulesMisconduct: "قد يؤدي التحرش أو التلاعب أو تلفيق الأدلة إلى حكم سلبي فوري.", courtsRulesGlossary: "القضية: سجل نزاع. الدليل: مواد تدعم الادعاءات. الحكم: قرار مع علاج. الاستئناف: طلب مراجعة الحكم.", courtsFilterActions: "الإجراءات", - courtsNoActions: "لا توجد إجراءات معلقة لدورك.", - courtsCaseTitlePlaceholder: "وصف مختصر للنزاع", - courtsCaseSeverity: "الخطورة", - courtsCaseSeverityNone: "بدون تصنيف خطورة", - courtsCaseSeverityLOW: "منخفضة", - courtsCaseSeverityMEDIUM: "متوسطة", - courtsCaseSeverityHIGH: "عالية", - courtsCaseSeverityCRITICAL: "حرجة", - courtsCaseSubject: "الموضوع", - courtsCaseSubjectNone: "بدون تصنيف موضوع", - courtsCaseSubjectBEHAVIOUR: "سلوك", - courtsCaseSubjectCONTENT: "محتوى", - courtsCaseSubjectGOVERNANCE: "حوكمة / قواعد", - courtsCaseSubjectFINANCIAL: "مالي / موارد", - courtsCaseSubjectOTHER: "أخرى", - courtsHiddenRespondent: "مخفي (مرئي فقط للأدوار المعنية).", courtsThRole: "الدور", courtsRoleAccuser: "المدعي", courtsRoleDefence: "الدفاع", @@ -1221,51 +1317,23 @@ module.exports = { courtsAssignJudgeBtn: "اختيار قاضٍ", trendingTitle: "الرائج", exploreTrending: "استكشف المحتوى الأكثر شعبية في شبكتك.", - bookmarkButton: "العلامات المرجعية", - transferButton: "التحويلات", - eventButton: "الأحداث", - taskButton: "المهام", votesButton: "التصويتات", - reportButton: "التقارير", - feedButton: "التغذية", - marketButton: "السوق", - imageButton: "الصور", - audioButton: "الملفات الصوتية", - videoButton: "الفيديوهات", - documentButton: "المستندات", - torrentButton: "التورنت", - noTrendingFound: "لم يتم العثور على محتوى رائج.", - noContentMessage: "لا يتوفر محتوى رائج بعد.", - trendingDescription: "الوصف", - trendingDate: "التاريخ", - trendingLocation: "الموقع", - trendingPrice: "السعر", - trendingUrl: "الرابط", - trendingCategory: "الفئة", - trendingStart: "البداية", - trendingEnd: "النهاية", - trendingPriority: "الأولوية", - trendingStatus: "الحالة", - trendingFrom: "من", - trendingTo: "إلى", - trendingConcept: "المفهوم", - trendingAmount: "المبلغ", - trendingDeadline: "الموعد النهائي", - trendingItemStatus: "حالة العنصر", - trendingTotalVotes: "إجمالي الأصوات", + shopProductButton: "متجر", + podcastButton: "بودكاست", + podcastEpisodeButton: "حلقة", + campaignButton: "حملة", + logisticsRouteButton: "مسار", trendingTotalOpinions: "إجمالي الآراء", - trendingNoContentMessage: "لا يتوفر محتوى رائج بعد.", - trendingAuthor: "بواسطة", - trendingCreatedAtLabel: "أُنشئ في", + trendingNoContentMessage: "لا توجد اتجاهات بعد.", trendingTotalCount: "العدد الإجمالي", tasksTitle: "المهام", tasksDescription: "اكتشف وأدِر المهام في شبكتك.", + taskSearchPlaceholder: "ابحث في المهام...", taskTitleLabel: "العنوان", taskDescriptionLabel: "الوصف", taskStartTimeLabel: "وقت البدء", taskEndTimeLabel: "وقت الانتهاء", taskPriorityLabel: "الأولوية", - taskPrioritySelect: "اختر الأولوية", taskPriorityUrgent: "عاجل", taskPriorityHigh: "عالية", taskPriorityMedium: "متوسطة", @@ -1275,13 +1343,13 @@ module.exports = { taskVisibilityLabel: "الظهور", taskPublic: "عام", taskPrivate: "خاص", - taskCreatedAt: "أُنشئ في", - taskBy: "بواسطة", taskStatus: "الحالة", taskStatusOpen: "مفتوح", taskStatusInProgress: "قيد التنفيذ", taskStatusClosed: "مغلق", - taskAssignedTo: "مُسنَد إلى", + taskAssignedTo: "مُسنَد", + taskAssignedChip: "مُسندة", + taskUnassignedChip: "غير مُسندة", taskAssignees: "المُكلَّفون", taskAssignButton: "إسناد لي", taskUnassignButton: "إلغاء الإسناد", @@ -1289,12 +1357,12 @@ module.exports = { taskUpdateButton: "تحديث", taskDeleteButton: "حذف", taskFilterAll: "الكل", + taskFilterRecent: "الأخيرة", taskFilterMine: "خاصتي", taskFilterOpen: "مفتوح", taskFilterInProgress: "قيد التنفيذ", taskFilterClosed: "مغلق", taskFilterAssigned: "مُسنَد", - taskFilterArchived: "مؤرشف", taskFilterUrgent: "عاجل", taskFilterHigh: "عالية", taskFilterMedium: "متوسطة", @@ -1307,35 +1375,47 @@ module.exports = { taskInProgressTitle: "المهام قيد التنفيذ", taskClosedTitle: "المهام المغلقة", taskAssignedTitle: "المهام المُسنَدة", - taskArchivedTitle: "المهام المؤرشفة", - taskPublicTitle: "المهام العامة", - taskPrivateTitle: "المهام الخاصة", notasks: "لا توجد مهام متاحة.", noLocation: "لم يتم تحديد الموقع", taskSetStatus: "تعيين الحالة", - eventTitle: "الأحداث", eventDateLabel: "التاريخ", + eventRecurrenceLabel: "التكرار", + eventRecurrenceUntil: "كرر حتى", + eventRecurrenceHint: "اترك تاريخ الانتهاء فارغًا لفعالية لمرة واحدة.", + eventUpcomingDates: "التواريخ القادمة", eventsTitle: "الأحداث", eventsDescription: "اكتشف وأدِر الأحداث في شبكتك.", - eventDescription: "الوصف", + eventSearchPlaceholder: "ابحث في الأحداث...", eventPrice: "السعر", eventStatus: "الحالة", - eventOrganizer: "المنظم", eventAllSectionTitle: "الأحداث", eventMineSectionTitle: "أحداثك", - eventArchivedTitle: "الأحداث المؤرشفة", eventCreateSectionTitle: "إنشاء حدث", eventUpdateSectionTitle: "تحديث الحدث", - eventDeleteButton: "حذف", eventCreateButton: "إنشاء حدث", eventAddToCalendar: "إضافة إلى التقويم", eventVisitCalendar: "عرض التقويم", viewTask: "عرض المهمة", + viewEvent: "عرض الحدث", + viewPad: "عرض المستند", + viewMap: "عرض الخريطة", + viewCalendar: "عرض التقويم", viewReport: "عرض التقرير", viewTransfer: "عرض التحويل", viewItem: "عرض العنصر", viewShop: "عرض المتجر", viewProject: "عرض المشروع", + viewJob: "عرض الوظيفة", + viewPlace: "عرض المكان", + generatePdf: "إنشاء PDF", + sharePm: "مشاركة برسالة خاصة", + galleryImages: "الصور (الحجم الأقصى: 50 ميجابايت لكل صورة)", + galleryPhotos: "الصور", + galleryAddPhoto: "إضافة صورة", + galleryRemovePhoto: "إزالة", + galleryVideo: "فيديو (الحجم الأقصى: 50 ميجابايت)", + galleryAddVideo: "إضافة فيديو", + galleryRemoveVideo: "إزالة الفيديو", viewVotation: "عرض التصويت", voteCastTitle: "أدلِ بصوتك", voteResults: "النتائج", @@ -1343,48 +1423,26 @@ module.exports = { eventDescriptionLabel: "الوصف", eventDescriptionPlaceholder: "أدخل وصف الحدث...", eventUpdateButton: "تحديث", - eventAttendeesLabel: "الحاضرون", - eventAttendeesPlaceholder: "أدخل الحاضرين، مفصولين بفواصل...", eventTagsLabel: "الوسوم", - eventTagsPlaceholder: "أدخل وسوم الحدث، مفصولة بفواصل...", - eventTags: "الوسوم", eventPriceLabel: "السعر", eventUrlLabel: "الرابط", eventAttendees: "الحاضرون", - noAttendees: "لا يوجد حاضرون بعد", - eventCreatedAt: "أُنشئ في", eventLocation: "الموقع", eventLocationLabel: "الموقع", - eventNoLocation: "لم يتم تحديد الموقع", - eventNoURL: "لم يتم تحديد رابط", - eventBy: "بواسطة", noevents: "لا توجد أحداث متاحة.", eventDate: "التاريخ", - eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "حضور الحدث", - eventUnattendButton: "إلغاء الحضور", - eventCreatedBy: "أُنشئ بواسطة", - eventAttendeesCount: "عدد الحاضرين", - eventCreatedByYou: "أنت أنشأت هذا الحدث", eventFilterAll: "الكل", + eventFilterRecent: "الأخيرة", eventFilterMine: "خاصتي", eventFilterToday: "اليوم", eventFilterWeek: "هذا الأسبوع", eventFilterMonth: "هذا الشهر", eventFilterYear: "هذه السنة", eventFilterArchived: "مؤرشف", - eventTodayTitle: "أحداث اليوم", - eventThisWeekTitle: "أحداث هذا الأسبوع", - eventThisMonthTitle: "أحداث هذا الشهر", - eventThisYearTitle: "أحداث هذه السنة", eventPrivacyLabel: "الظهور", eventPublic: "عام", eventPrivate: "خاص", - eventPublicTitle: "الأحداث العامة", - eventNoPrice: "حدث مجاني", - eventNoImage: "لم يتم رفع صورة", - eventAttendConfirmation: "أنت الآن تحضر هذا الحدث", - eventUnattendConfirmation: "لم تعد تحضر هذا الحدث", eventAttended: "حضر", eventUnattended: "لم يحضر", eventStatusOpen: "مفتوح", @@ -1395,9 +1453,14 @@ module.exports = { tagsTopSectionTitle: "أفضل الوسوم", tagsCloudSectionTitle: "سحابة الوسوم", tagsFilterAll: "الكل", + tagsFilterMine: "لي", + tagsFilterRecent: "حديثة", + tagsMineSectionTitle: "وسومي", + tagsRecentSectionTitle: "وسوم حديثة", tagsFilterTop: "الأفضل", tagsFilterCloud: "السحابة", tagsNoItems: "لا توجد وسوم متاحة.", + tagsSearchPlaceholder: "ابحث في الوسوم...", tagsTableHeaderTag: "الوسم/الرابط:", tagsTableHeaderCount: "العداد:", transfersTitle: "التحويلات", @@ -1405,8 +1468,11 @@ module.exports = { transfersFrom: "من", transfersTo: "إلى", transfersFilterAll: "الكل", + transfersFilterRecent: "الأخيرة", transfersFilterMine: "خاصتي", - transfersFilterUBI: "UBI", + transfersFilterMinePending: "قيد الانتظار", + transfersFilterMineConfirmed: "موقَّعة", + transfersFilterUBI: "الدخل الأساسي", transfersFilterMarket: "السوق", transfersFilterTop: "الأفضل", transfersFilterPending: "معلّق", @@ -1420,7 +1486,6 @@ module.exports = { transfersCategoryEconomic: "اقتصادي", transfersCategoryTime: "وقت", transfersCategoryTrust: "ثقة", - transfersCategoryApply: "تطبيق", transfersUnitEco: "ECO", transfersUnitHours: "س", transfersUnitTrust: "ثقة", @@ -1437,18 +1502,12 @@ module.exports = { transfersDeadline: "الموعد النهائي", transfersTags: "الوسوم", transfersStatus: "الحالة", - transfersStatusUnconfirmed: "غير مؤكد", - transfersStatusClosed: "مغلق", - transfersStatusDiscarded: "ملغى", - transfersCreatedAt: "أُنشئ في", transfersConfirmations: "التأكيدات", - transfersContainingBlock: "الكتلة المحتوية", - transfersExportContract: "إنشاء العقد", + transfersExportContract: "إنشاء فاتورة", transfersConfirmButton: "تأكيد التحويل", transfersNoItems: "لم يتم العثور على تحويلات.", transfersMineSectionTitle: "تحويلاتك", transfersUBISectionTitle: "تحويلات UBI", - transfersMarketSectionTitle: "تحويلات السوق", transfersTopSectionTitle: "أفضل التحويلات", transfersPendingSectionTitle: "التحويلات المعلّقة", transfersUnconfirmedSectionTitle: "التحويلات غير المؤكدة", @@ -1456,21 +1515,13 @@ module.exports = { transfersDiscardedSectionTitle: "التحويلات الملغاة", transfersCreateSectionTitle: "إنشاء عملية نقل", transfersAllSectionTitle: "التحويلات", - transfersFilterFavs: "المفضلة", - transfersFavsSectionTitle: "التحويلات المفضلة", - transfersSearchLabel: "بحث", - transfersSearchPlaceholder: "ابحث عن مفهوم، وسوم، مستخدمين...", + transfersSearchPlaceholder: "ابحث في التحويلات...", transfersMinAmountLabel: "الحد الأدنى للمبلغ", transfersMaxAmountLabel: "الحد الأقصى للمبلغ", - transfersSortLabel: "ترتيب حسب", transfersSortRecent: "الأحدث", transfersSortAmount: "الأعلى مبلغًا", transfersSortDeadline: "الأقرب موعدًا", transfersSearchButton: "بحث", - transfersFavoriteButton: "إضافة للمفضلة", - transfersUnfavoriteButton: "إزالة من المفضلة", - transfersMessageUserButton: "رسالة", - transfersExpiringSoonBadge: "على وشك الانتهاء", transfersExpiredBadge: "منتهي الصلاحية", transfersUpdatedAt: "تم التحديث", transfersNoMatch: "لا توجد تحويلات تطابق بحثك.", @@ -1483,6 +1534,10 @@ module.exports = { voteClosedTitle: "التصويتات المغلقة", voteAllSectionTitle: "التصويتات", voteCreateButton: "إنشاء تصويت", + voteErrorTitle: "راجع النموذج", + voteErrorDeadlineMin: "يجب أن يكون الموعد النهائي بعد {days} أيام على الأقل من الآن.", + voteErrorGeneric: "تعذّر حفظ التصويت.", + voteLocked: "تلقّى هذا التصويت أصواتًا أو آراء بالفعل، أو يتبع البرلمان أو المحاكم: لم يعد بالإمكان تعديله أو حذفه.", voteUpdateButton: "تحديث", voteDeleteButton: "حذف", voteOptionYes: "نعم", @@ -1492,6 +1547,7 @@ module.exports = { voteFollowMajority: "اتباع الأغلبية", voteNotInterested: "غير مهتم", voteFilterAll: "الكل", + voteFilterRecent: "الأخيرة", voteFilterMine: "خاصتي", voteFilterOpen: "مفتوح", voteFilterClosed: "مغلق", @@ -1507,37 +1563,28 @@ module.exports = { voteTags: "الوسوم", voteOpinions: "الآراء", novotes: "لا توجد مقترحات تصويت متاحة.", - voteBy: "بواسطة", voteCreatedAt: "أُنشئ في", voteNoQuestion: "لم يتم تقديم سؤال", voteUnknownCreator: "مُنشئ غير معروف", voteUnknownDate: "تاريخ غير معروف", - errorVoteNotFound: "لم يتم العثور على التصويت", - errorAlreadyVoted: "لقد أبديت رأيك بالفعل.", - errorVoteClosedCannotEdit: "لا يمكنك تعديل تصويت مغلق", - errorVoteDeadlinePassed: "انتهى الموعد النهائي لهذا التصويت", - errorRetrievingVote: "خطأ في استرجاع التصويت", - errorCreatingVote: "خطأ في إنشاء التصويت", - errorVoteAlreadyVoted: "لا يمكن التعديل بعد الإدلاء بالرأي", - errorDeletingOldVote: "خطأ في حذف الرأي القديم", - errorCreatingUpdatedVote: "خطأ في إنشاء الرأي المحدّث", - errorCreatingTombstone: "خطأ في إنشاء شاهد القبر", voteDetailSectionTitle: 'تفاصيل التصويت', voteCommentsLabel: 'التعليقات', voteCommentsForumButton: 'فتح النقاش', voteCommentsSectionTitle: 'فتح النقاش', voteNoCommentsYet: 'لا توجد تعليقات بعد. كن أول من يرد.', + commentsGoToLast: "اذهب إلى آخر تعليق", voteNewCommentPlaceholder: 'اكتب تعليقك هنا…', voteNewCommentButton: 'نشر التعليق', voteNewCommentLabel: 'إضافة تعليق', cvTitle: "السيرة الذاتية", - cvLabel: "السيرة الذاتية (CV)", cvEditSectionTitle: "تعديل السيرة الذاتية", cvCreateSectionTitle: "إنشاء سيرة ذاتية", cvDescription: "إدارة ومشاركة مهاراتك ومعلوماتك المهنية.", cvNameLabel: "الاسم الكامل", cvDescriptionLabel: "الملخص", cvPhotoLabel: "الصورة", + cvPdfLabel: "السيرة الذاتية (PDF)", + cvVisitButton: "زيارة السيرة الذاتية", cvPersonalExperiencesLabel: "الخبرات الشخصية", cvPersonalSkillsLabel: "المهارات الشخصية (مفصولة بفواصل)", cvOasisExperiencesLabel: "خبرات المساهمة في Oasis", @@ -1550,29 +1597,130 @@ module.exports = { cvLocationLabel: "الموقع", cvStatusLabel: "الحالة", cvPreferencesLabel: "التفضيلات", - cvOasisContributorLabel: "مساهم في Oasis", cvPersonal: "شخصي", cvOasis: "مساهم في Oasis (اختياري)", - cvOasisContributorView: "مساهمة في Oasis", + cvOasisContributorView: "المساهمة", cvEducational: "تعليمي (اختياري)", cvEducationalView: "تعليمي", cvProfessional: "مهني (اختياري)", cvProfessionalView: "مهني", cvAvailability: "التوفر (اختياري)", - cvAvailabilityView: "التوفر", cvUpdateButton: "تحديث", cvCreateButton: "إنشاء سيرة ذاتية", - cvContactLabel: "التواصل", cvCreatedAt: "أُنشئ في", cvUpdatedAt: "تم التحديث في", cvEditButton: "تحديث", cvDeleteButton: "حذف", cvNoCV: "لم يتم العثور على سيرة ذاتية.", blogSubject: "الموضوع", - blogMessage: "النص محدود بـ 8096 حرفًا.", + blogMessage: "الرسالة", blogImage: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", - blogPublish: "معاينة", - noPopularMessages: "لم تُنشر رسائل شائعة بعد", + blogMedia: "المرفقات (حتى 8 صور ومقطع فيديو)", + blogPublish: "نشر", + pollsTitle: "الاستطلاعات", + dataTitle: "التطابقات", + dataDescription: "استكشف تطابقات شبكتك وشاهدها.", + dataFilterAll: "الكل", + dataFilterMine: "بياناتي", + dataFilterRecent: "الأحدث", + dataFilterTop: "الأعلى", + dataKindInhabitants: "السكان", + dataKindJobs: "الوظائف", + dataKindProjects: "المشاريع", + dataKindEvents: "الفعاليات", + dataKindTribes: "القبائل", + dataKindMarket: "السوق", + dataKindHousing: "السكن", + dataKindIndustry: "الصناعة", + dataKindTasks: "المهام", + dataKindReports: "التقارير", + dataKindVotes: "التصويتات", + dataSearchPlaceholder: "ابحث في التطابقات...", + dataCohesionTitle: "معامل التماسك (CC)", + dataCcShort: "CC", + dataCohesionHint: "متوسط التقارب بين كل ما نشرته الشبكة.", + dataAffinity: "التوافق", + dataCommonTerms: "كلمة مفتاحية", + dataReasonFollowing: "أنت تدعم هذا الساكن", + dataReasonTribe: "يشاركك قبيلة", + dataReasonPinned: "يطابق شيئًا ثبّتّه", + dataReasonCv: "يطابق مهارات سيرتك الذاتية", + dataReasonContent: "مرتبط بما تنشره", + dataReasonRelated: "مرتبط باهتماماتك", + dataReasonAlike: "نشره ساكن يشبهك", + dataReasonSupportsYou: "يدعمك", + dataReasonMutual: "دعم متبادل", + dataReasonRated: "حاصل على تقييم جيد من الشبكة", + dataReasonNear: "بالقرب منك", + dataStatPeople: "السير الذاتية", + dataStatConnected: "متصلون", + dataStatSkills: "المهارات", + dataBestMatch: "أفضل تطابق", + dataStatIsolated: "معزولون", + dataStatEntities: "الكيانات المقارَنة", + dataStatTerms: "مصطلحات مختلفة", + dataStatPairs: "الأزواج المرتبطة", + dataMatchesTitle: "التطابقات", + dataMatchesHint: "اقتراحات مخصصة من الخوارزم.", + dataTermsTitle: "الكلمات المفتاحية", + dataTermsHint: "المصطلحات التي تظهر في أكثر المحتويات.", + dataConnections: "تطابقات مرتبطة", + dataNoMatches: "لا توجد بيانات متقاطعة لعرضها.", + dataNoProfile: "انشر محتوى أو اكتب سيرتك الذاتية ليتعلم الخوارزم ما يهمك.", + pollsDescription: "وحدة لسؤال الشبكة وعدّ الإجابات.", + pollFilterAll: "الكل", + pollFilterMine: "استطلاعاتي", + pollFilterRecent: "الأحدث", + pollFilterTop: "الأعلى", + pollFilterVoted: "صوّتت", + pollFilterOpen: "مفتوحة", + pollFilterClosed: "مغلقة", + pollCreateButton: "إنشاء استطلاع", + pollCreateTitle: "إنشاء استطلاع", + pollEditTitle: "تعديل الاستطلاع", + pollQuestion: "السؤال", + pollQuestionPlaceholder: "ماذا تريد أن تسأل؟", + pollOptions: "الخيارات (خيار في كل سطر)", + pollOutcome: "النتيجة", + pollNoVotesYet: "لا أصوات بعد", + pollTie: "تعادل", + pollOptionsPlaceholder: "الساحة\nالحديقة\nالمقهى", + pollAnonymous: "مجهول", + pollAnonymousLabel: "استطلاع مجهول", + pollMultiple: "اختيار متعدد", + pollMultipleLabel: "السماح بعدة إجابات", + pollDeadline: "الموعد النهائي", + pollTags: "الوسوم", + pollPublishButton: "نشر الاستطلاع", + pollUpdateButton: "تحديث", + pollCloseButton: "إغلاق", + pollDeleteButton: "حذف", + pollVoteButton: "صوّت", + pollChangeVote: "تغيير الصوت", + pollVoters: "المصوّتون", + pollComments: "التعليقات", + pollStatusLabel: "الحالة", + pollStatusOpen: "مفتوح", + pollStatusClosed: "مغلق", + pollSearchPlaceholder: "ابحث في الاستطلاعات...", + pollsNoItems: "لم يتم العثور على استطلاعات.", + viewPoll: "عرض الاستطلاع", + pollChatCreate: "إنشاء استطلاع", + pollInChat: "استطلاع", + blogTitle: "المدونات", + blogDescription: "وحدة لاكتشاف المدونات وإدارتها.", + blogFilterAll: "الكل", + blogFilterMine: "مدوناتي", + blogFilterRecent: "الأحدث", + blogFilterFavorites: "المفضلة", + blogFilterTop: "الأعلى", + blogCreateButton: "إنشاء مدونة", + blogSearchPlaceholder: "ابحث في المدونات...", + blogComments: "التعليقات", + blogAllowComments: "السماح بالتعليقات", + blogCommentsClosed: "أغلق الكاتب التعليقات على هذه المدونة.", + blogNoItems: "لم يتم العثور على مدونات.", + viewBlog: "عرض المدونة", forumTitle: "المنتديات", forumCategoryLabel: "الفئة", forumTitleLabel: "العنوان", @@ -1580,43 +1728,21 @@ module.exports = { forumCreateButton: "إنشاء منتدى", forumCreateSectionTitle: "إنشاء منتدى", forumDescription: "تحدث بصراحة مع السكان الآخرين في شبكتك.", + forumSearchPlaceholder: "ابحث في المنتديات...", forumFilterAll: "الكل", forumFilterMine: "خاصتي", forumFilterRecent: "الأخيرة", forumFilterTop: "الأفضل", - forumMineSectionTitle: "منتدياتك", - forumRecentSectionTitle: "المنتديات الأخيرة", - forumAllSectionTitle: "المنتديات", - forumDeleteButton: "حذف", forumParticipants: "المشاركون", forumMessages: "الرسائل", - forumLastMessage: "آخر رسالة", forumMessageLabel: "الرسالة", forumMessagePlaceholder: "اكتب رسالتك...", forumSendButton: "إرسال", - forumVisitForum: "زيارة المنتدى", noForums: "لم يتم العثور على منتديات.", - forumVisitButton: "زيارة المنتدى", - forumCatGENERAL: "عام", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "سياسة", - forumCatTECH: "تقنية", - forumCatSCIENCE: "علوم", - forumCatMUSIC: "موسيقى", - forumCatART: "فن", - forumCatGAMING: "ألعاب", - forumCatBOOKS: "كتب", - forumCatFILMS: "أفلام", - forumCatPHILOSOPHY: "فلسفة", - forumCatSOCIETY: "مجتمع", - forumCatPRIVACY: "خصوصية", - forumCatCYBERWARFARE: "حرب سيبرانية", - forumCatSURVIVALISM: "البقائية", + forumVisitButton: "عرض المنتدى", + forumTopicLabel: "الموضوع", imageTitle: "الصور", imageDescription: "استكشف وأدِر محتوى الصور في شبكتك.", - imagePluginTitle: "العنوان", - imagePluginDescription: "الوصف", imageMineSectionTitle: "صورك", imageCreateSectionTitle: "رفع صورة", imageUpdateSectionTitle: "تحديث الصورة", @@ -1625,17 +1751,14 @@ module.exports = { imageTopSectionTitle: "أفضل الصور", imageFavoritesSectionTitle: "المفضلة", imageGallerySectionTitle: "المعرض", - imageMemeSectionTitle: "ميمز", imageFilterAll: "الكل", imageFilterMine: "خاصتي", imageFilterRecent: "الأخيرة", imageFilterTop: "الأفضل", imageFilterFavorites: "المفضلة", imageFilterGallery: "المعرض", - imageFilterMeme: "ميمز", imageCreateButton: "رفع صورة", imageUpdateButton: "تحديث", - imageDeleteButton: "حذف", imageAddFavoriteButton: "إضافة للمفضلة", imageRemoveFavoriteButton: "إزالة من المفضلة", imageFileLabel: "اختر ملف صورة (.jpeg, .jpg, .png, .gif)", @@ -1645,23 +1768,21 @@ module.exports = { imageTitlePlaceholder: "اختياري", imageDescriptionLabel: "الوصف", imageDescriptionPlaceholder: "اختياري", - imageMemeLabel: "تعيين كميم", imageNoFile: "لم يتم تقديم ملف صورة", noImages: "لا توجد صور متاحة.", - imageSearchPlaceholder: "ابحث عن عنوان، وسوم، وصف، مؤلف...", + imageSearchPlaceholder: "ابحث في الصور...", imageSortRecent: "الأحدث", imageSortOldest: "الأقدم", imageSortTop: "الأكثر تصويتًا", imageSearchButton: "بحث", - imageMessageAuthorButton: "رسالة", imageUpdatedAt: "تم التحديث", imageNoMatch: "لا توجد صور تطابق بحثك.", + feedSearchPlaceholder: "ابحث في التغذية...", feedTitle: "التغذية", createFeedTitle: "إنشاء تغذية", createFeedButton: "أرسل التغذية!", feedPlaceholder: "ماذا يحدث؟ (280 حرفًا كحد أقصى)", TODAYButton: "اليوم", - CREATEButton: "إنشاء تغذية", totalOpinions: "إجمالي الآراء", moreVoted: "الأكثر تصويتًا", noFeedsFound: "لم يتم العثور على تغذيات.", @@ -1671,7 +1792,7 @@ module.exports = { alreadyRefeeded: "لقد أعدت نشر هذا بالفعل.", activityTitle: "النشاط", yourActivity: "نشاطك", - globalActivity: "النشاط العام", + globalActivity: "النشاط", activityList: "النشاط", activityDesc: "شاهد آخر نشاط في شبكتك.", allButton: "الكل", @@ -1684,20 +1805,13 @@ module.exports = { concept: "المفهوم", description: "الوصف", meme: "ميم", - activityContact: "جهة اتصال", - activityBy: "الاسم", - activityPixelia: "تمت إضافة بكسل جديد", - viewImage: "عرض الصورة", - playAudio: "تشغيل الصوت", - playVideo: "تشغيل الفيديو", typeRecent: "الأخيرة", - errorActivity: "خطأ في استرجاع النشاط", - typePost: "مدونات", - recentButton: "الأحدث", + typeTop: "الأعلى", + typePost: "مدونة", + activityUpdateLabel: "تحديث", typeTribe: "القبائل", typeLarp: "L.A.R.P.", typeInhabitants: "السكان", - activityProfileUpdated: "حدّث ملفه الشخصي", typeAbout: "السكان", typeCurriculum: "السير الذاتية", typeImage: "الصور", @@ -1741,8 +1855,6 @@ module.exports = { typeCourtsSettlementAccepted: "المحاكم · تسوية مقبولة", typeCourtsNomination: "المحاكم · ترشيح", typeCourtsNominationVote: "المحاكم · تصويت ترشيح", - activitySupport: "تحالف جديد أُقيم", - activityJoin: "تم الانضمام إلى PUB جديد", question: "السؤال", deadline: "الموعد النهائي", status: "الحالة", @@ -1756,12 +1868,8 @@ module.exports = { date: "التاريخ", category: "الفئة", attendees: "الحاضرون", - activitySpread: "->", - visitLink: "زيارة الرابط", - viewDocument: "عرض المستند", location: "الموقع", contentWarning: "الموضوع", - personName: "اسم الساكن", bankWalletConnected: "محفظة ECOin", bankUbiReceived: "تم استلام الدخل الأساسي", bankTx: "معاملة", @@ -1771,7 +1879,6 @@ module.exports = { link: "رابط", activityProjectFollow: "%OASIS% الآن %ACTION% هذا المشروع %PROJECT%", activityProjectUnfollow: "%OASIS% الآن %ACTION% هذا المشروع %PROJECT%", - activityProjectPledged: "%OASIS% قد %ACTION% %AMOUNT% للمشروع %PROJECT%", following: "يتابع", unfollowing: "ألغى المتابعة", pledged: "تبرع", @@ -1817,26 +1924,17 @@ module.exports = { courtsVotesSlashTotal: "نعم/الإجمالي", courtsOpenVote: "تصويت مفتوح", courtsAnswerTitle: "الرد", - courtsStanceADMIT: "اعتراف", - courtsStanceDENY: "إنكار", - courtsStancePARTIAL: "جزئي", - courtsStanceCOUNTERCLAIM: "دعوى مضادة", - courtsStanceNEUTRAL: "محايد", courtsVerdictResult: "النتيجة", courtsVerdictOrders: "الأوامر", courtsSettlementText: "التسوية", courtsSettlementAccepted: "مقبولة", - courtsSettlementPending: "معلّقة", courtsJudge: "القاضي", courtsThSupports: "الداعمون", courtsFilterOpenCase: 'فتح قضية', - courtsEvidenceFileLabel: 'ملف الدليل (صورة، صوت، فيديو أو PDF)', - courtsCaseMediators: 'الوسطاء', courtsCaseMediatorsPh: 'معرّفات Oasis للوسطاء، مفصولة بفاصلة', courtsMediatorsLabel: 'الوسطاء', courtsThCase: 'القضية', courtsThCreatedAt: 'تاريخ البدء', - courtsThActions: 'الإجراءات', courtsPublicPrefLabel: 'الظهور بعد الحل', courtsPublicPrefYes: 'أوافق على أن تكون هذه القضية علنية بالكامل', courtsPublicPrefNo: 'أفضل إبقاء التفاصيل خاصة', @@ -1844,8 +1942,11 @@ module.exports = { courtsMethodMEDIATION: 'وساطة', courtsNoCases: 'لا توجد قضايا.', reportsTitle: "التقارير", - reportsDescription: "إدارة وتتبع التقارير المتعلقة بالمشاكل والأخطاء والإساءات وتحذيرات المحتوى في شبكتك.", + reportsDescription: "أدر تقارير شبكتك وتابعها.", + reportsSearchPlaceholder: "ابحث في التقارير...", reportsFilterAll: "الكل", + reportsFilterRecent: "حديثة", + reportsFilterTop: "الأعلى", reportsFilterMine: "خاصتي", reportsFilterFeatures: "الميزات", reportsFilterBugs: "الأخطاء", @@ -1858,18 +1959,13 @@ module.exports = { reportsFilterInvalid: "غير صالح", reportsCreateButton: "إنشاء تقرير", reportsTitleLabel: "العنوان", - reportsDescriptionLabel: "الوصف", - reportsDescriptionPlaceholder: "يرجى تقديم وصف مفصّل.", reportsCategory: "الفئة", - reportsCategoryLabel: "الفئة", reportsCategoryFeatures: "الميزات", reportsCategoryBugs: "الأخطاء", reportsCategoryAbuse: "الإساءة", - reportsCategoryContent: "مشاكل المحتوى", + reportsCategoryContent: "المحتوى", reportsUpdateButton: "تحديث", reportsDeleteButton: "حذف", - reportsDateLabel: "التاريخ", - reportsUploadFile: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", reportsMineSectionTitle: "تقاريرك", reportsFeaturesSectionTitle: "طلبات الميزات", reportsBugsSectionTitle: "الأخطاء", @@ -1877,11 +1973,6 @@ module.exports = { reportsContentSectionTitle: "مشاكل المحتوى", reportsAllSectionTitle: "التقارير", reportsNoItems: "لا توجد تقارير متاحة.", - reportsValidationTitle: "يرجى إدخال عنوان صالح.", - reportsValidationDescription: "لا يمكن أن يكون الوصف فارغًا.", - reportsValidationCategory: "يرجى اختيار فئة.", - reportsCreatedAt: "أُنشئ في", - reportsCreatedBy: "بواسطة", reportsSeverity: "الخطورة", reportsSeverityLow: "منخفضة", reportsSeverityMedium: "متوسطة", @@ -1892,9 +1983,6 @@ module.exports = { reportsStatusUnderReview: "قيد المراجعة", reportsStatusResolved: "محلول", reportsStatusInvalid: "غير صالح", - reportsUpdateStatusButton: "تحديث الحالة", - reportsAnonymityOption: "إرسال بشكل مجهول", - reportsAnonymousAuthor: "مجهول", reportsConfirmButton: "تأكيد التقرير!", reportsConfirmations: "التأكيدات", reportsConfirmedSectionTitle: "التقارير المؤكدة", @@ -1903,7 +1991,6 @@ module.exports = { reportsUnderReviewSectionTitle: "التقارير قيد المراجعة", reportsResolvedSectionTitle: "التقارير المحلولة", reportsInvalidSectionTitle: "التقارير غير الصالحة", - reportsTemplateSectionTitle: 'قالب التقرير', reportsBugTemplateTitle: 'تفاصيل إعادة الإنتاج (الأخطاء)', reportsFeatureTemplateTitle: 'التفاصيل (الميزات)', reportsAbuseTemplateTitle: 'التفاصيل (الإساءة)', @@ -1942,6 +2029,20 @@ module.exports = { reportsRequestedActionLabel: 'الإجراء المطلوب', reportsRequestedActionPlaceholder: 'إزالة، إخفاء، وسم، تحذير، إلخ.', tribesTitle: "القبائل", + tribeFilterAll: "الكل", + tribeFilterRecent: "الأحدث", + tribeFilterMine: "لي", + tribeFilterMembership: "العضوية", + tribeFilterSubtribes: "القبائل الفرعية", + tribeFilterTop: "الأعلى", + tribeFilterGallery: "المعرض", + tribeFeedFilterTOP: "الأعلى", + tribeFeedFilterMINE: "لي", + tribeFeedFilterALL: "الكل", + tribeFeedFilterRECENT: "الأحدث", + courtsStanceDENY: "إنكار", + courtsStanceADMIT: "إقرار", + courtsStancePARTIAL: "إقرار جزئي", tribeAllSectionTitle: "القبائل", tribeMineSectionTitle: "قبائلك", tribeCreateSectionTitle: "إنشاء قبيلة", @@ -1949,24 +2050,15 @@ module.exports = { tribeGallerySectionTitle: "معرض القبائل", tribeRecentSectionTitle: "القبائل الأخيرة", tribeTopSectionTitle: "القبائل الشائعة", - tribeviewTribeButton: "زيارة القبيلة", - tribeviewSubTribeButton: "زيارة القبيلة الفرعية", tribeRootLabel: "الجذر", tribeDescription: "استكشف أو أنشئ قبائل في شبكتك.", - tribeFilterAll: "الكل", - tribeFilterMine: "خاصتي", - tribeFilterMembership: "العضوية", - tribeFilterRecent: "الأخيرة", - tribeFilterTop: "الأفضل", - tribeFilterSubtribes: "القبائل الفرعية", - tribeFilterGallery: "المعرض", tribeMainTribeLabel: "القبيلة الرئيسية", tribeCreateButton: "إنشاء قبيلة", tribeUpdateButton: "تحديث", tribeDeleteButton: "حذف", tribeImageLabel: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", tribeTitleLabel: "العنوان", - searchTribesPlaceholder: "تصفية القبائل حسب الاسم …", + searchTribesPlaceholder: "ابحث في القبائل...", tribeTitlePlaceholder: "اسم القبيلة", tribeDescriptionLabel: "الوصف", tribeDescriptionPlaceholder: "صف هذه القبيلة", @@ -1978,13 +2070,17 @@ module.exports = { tribeModeLabel: "الوضع", tribeIsAnonymousLabel: "الحالة", tribeMembersCount: "الأعضاء", - tribeInviteCodePlaceholder: "أدخل رمز الدعوة", - tribeJoinByCodeButton: "الانضمام بالرمز", - tribeJoinButton: "انضمام", tribeEnterInvite: "إدخال الرمز", + subscriptionTitle: "الاشتراكات", + subscriptionOn: "مشترك", + subscriptionOff: "غير مشترك", + subscriptionSubscribe: "اشترك", + subscriptionUnsubscribe: "إلغاء الاشتراك", + subscriptionSubscribers: "المشتركون", + pmListLabel: "إرسال إلى", + pmListNone: "— بدون قائمة —", + pmListNoSubscribers: "لا مشتركين", tribeLeaveButton: "مغادرة", - tribeYes: "نعم", - tribeNo: "لا", tribePublic: "عامة", tribePrivate: "خاصة", tribeGenerateInvite: "دعوة فردية", @@ -1993,13 +2089,8 @@ module.exports = { tribeRemoveInvitation: "إزالة الدعوة", tribeCreatedAt: "أُنشئت في", tribeAuthor: "بواسطة", - tribeAuthorLabel: "المؤلف", tribeStrict: "صارم", tribeOpen: "مفتوح", - tribeFeedFilterRECENT: "الأخيرة", - tribeFeedFilterMINE: "خاصتي", - tribeFeedFilterALL: "الكل", - tribeFeedFilterTOP: "الأفضل", tribeFeedRefeeds: "إعادات النشر", tribeFeedRefeed: "إعادة نشر", tribeFeedMessagePlaceholder: "اكتب تغذية…", @@ -2009,33 +2100,28 @@ module.exports = { tribeNotFound: "لم يتم العثور على القبيلة!", createTribeTitle: "إنشاء قبيلة", updateTribeTitle: "تحديث القبيلة", - tribeSectionOverview: "نظرة عامة", tribeSectionInhabitants: "السكان", tribeSectionVotations: "التصويتات", tribeSectionEvents: "الأحداث", - tribeSectionReports: "التقارير", tribeSectionTasks: "المهام", tribeSectionFeed: "التغذية", tribeSectionForum: "المنتدى", - tribeSectionMarket: "السوق", - tribeSectionJobs: "الوظائف", - tribeSectionProjects: "المشاريع", tribeSectionMedia: "الوسائط", tribeSectionImages: "الصور", tribeSectionAudios: "الملفات الصوتية", tribeSectionVideos: "الفيديوهات", tribeSectionDocuments: "المستندات", tribeSectionBookmarks: "العلامات المرجعية", - tribeSectionMaps: "MAPS", - tribeSectionPads: "PADS", - tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionMaps: "الخرائط", + tribeSectionPads: "المستندات التعاونية", + tribeSectionChats: "المحادثات", + tribeSectionCalendars: "التقاويم", + tribePadCreate: "إنشاء مستند تعاوني", + tribeChatCreate: "إنشاء محادثة", + tribeCalendarCreate: "إنشاء تقويم", + tribePadsEmpty: "لا توجد مستندات تعاونية بعد.", + tribeChatsEmpty: "لا توجد محادثات بعد.", + tribeCalendarsEmpty: "لا توجد تقاويم بعد.", tribeInhabitantsEmpty: "لا يوجد سكان في هذه القبيلة بعد.", tribeEventCreate: "إنشاء حدث", tribeEventsEmpty: "لا توجد أحداث بعد.", @@ -2057,11 +2143,6 @@ module.exports = { tribeTaskStatusClosed: "إغلاق", tribeTaskAssign: "إسناد", tribeTaskUnassign: "إلغاء الإسناد", - tribeReportCreate: "إنشاء تقرير", - tribeReportsEmpty: "لا توجد تقارير بعد.", - tribeReportTitle: "العنوان", - tribeReportDescription: "الوصف", - tribeReportCategory: "الفئة", tribeVotationCreate: "إنشاء تصويت", tribeVotationsEmpty: "لا توجد تصويتات بعد.", tribeVotationTitle: "العنوان", @@ -2079,27 +2160,6 @@ module.exports = { tribeForumCategory: "الفئة", tribeForumReply: "رد", tribeForumReplies: "الردود", - tribeMarketCreate: "إنشاء عرض", - tribeMarketEmpty: "لا توجد عروض بعد.", - tribeMarketTitle: "العنوان", - tribeMarketDescription: "الوصف", - tribeMarketPrice: "السعر", - tribeMarketImage: "الصورة", - tribeMarketCategory: "الفئة", - tribeJobCreate: "إنشاء وظيفة", - tribeJobsEmpty: "لا توجد وظائف بعد.", - tribeJobTitle: "العنوان", - tribeJobDescription: "الوصف", - tribeJobLocation: "الموقع", - tribeJobSalary: "الراتب", - tribeJobDeadline: "الموعد النهائي", - tribeProjectCreate: "إنشاء مشروع", - tribeProjectsEmpty: "لا توجد مشاريع بعد.", - tribeProjectTitle: "العنوان", - tribeProjectDescription: "الوصف", - tribeProjectGoal: "الهدف", - tribeProjectFunded: "مُموَّل", - tribeProjectDeadline: "الموعد النهائي", tribeMediaUpload: "رفع وسائط", readDocument: "قراءة المستند", tribeCreateImage: "إنشاء صورة", @@ -2110,21 +2170,20 @@ module.exports = { tribeMediaEmpty: "لا توجد وسائط بعد.", tribeMediaTitle: "العنوان", tribeMediaDescription: "الوصف", - tribeMediaType: "النوع", tribeMediaTypeImage: "صورة", tribeMediaTypeVideo: "فيديو", tribeMediaTypeAudio: "صوت", tribeMediaTypeDocument: "مستند", tribeMediaTypeBookmark: "علامة مرجعية", tribeContentDelete: "حذف", + tribeReachTribe: "القبيلة", + tribeReachNotice: "يعرضه OASIS لجميع السكان ويعرضه CLEARNET أيضًا على الويب المفتوح. فتحه يترك نسخة مقروءة خارج القبيلة: إعادته إلى القبيلة تخفيه، لكن لا يمكن مسح النسخ التي تمت مشاركتها.", + tribeReachPrivate: "محتوى القبيلة الخاصة لا يمكن أن يبقى إلا داخل القبيلة.", + tribeReachOpenedBy: "فتحه", + tribeReachChange: "مكان النشر", tribeInviteCodeText: "رمز الدعوة: ", oasisVersionLabel: "إصدار Oasis", - tribeInviteCodeHint: "شارك هذا الرمز مع من تريد دعوته. يمكنه الانضمام من /invites → Tribes.", - tribeGroupTribe: "القبيلة", - tribeGroupOffice: "المكتب", - tribeGroupNetwork: "الشبكة", - tribeGroupEconomy: "الاقتصاد", - tribeGroupMedia: "الوسائط", + tribeInviteCodeHint: "شارك هذا الرمز مع من تريد دعوته. يمكنه الانضمام من /invites.", tribeStatusOpen: "مفتوح", tribeStatusClosed: "مغلق", tribeStatusInProgress: "قيد التنفيذ", @@ -2134,33 +2193,17 @@ module.exports = { tribePriorityCritical: "حرجة", tribeTaskFilterAll: "الكل", tribeMediaFilterAll: "الكل", - tribeReportCatBug: "خطأ", - tribeReportCatAbuse: "إساءة", - tribeReportCatContent: "محتوى", - tribeReportCatOther: "أخرى", tribeForumCatGeneral: "عام", tribeForumCatProposal: "اقتراح", tribeForumCatQuestion: "سؤال", tribeForumCatAnnouncement: "إعلان", - tribeMarketCatGoods: "سلع", - tribeMarketCatServices: "خدمات", - tribeMarketCatFood: "طعام", - tribeMarketCatOther: "أخرى", tribeStatusLabel: "الحالة", tribeSubTribes: "القبائل الفرعية", tribeSubTribesCreate: "إنشاء قبيلة فرعية", tribeSubTribesStrictDenied: "الوضع الصارم للقبيلة لا يسمح لك بإنشاء قبائل فرعية جديدة. يرجى الاتصال بالمسؤول.", - tribeSubTribesEmpty: "لم يتم إنشاء قبائل فرعية بعد.", - tribeActivityJoined: "انضم", - tribeActivityLeft: "غادر", - tribeActivityFeed: "تغذية", tribeActivityRefeed: "إعادة نشر", - tribeGroupAnalytics: "التحليلات", - tribeGroupCreative: "إبداعي", tribeSectionActivity: "النشاط", tribeSectionTrending: "الرائج", - tribeSectionOpinions: "الآراء", - tribeSectionPixelia: "بكسيليا", tribeSectionTags: "الوسوم", tribeSectionSearch: "بحث", tribeActivityEmpty: "لا يوجد نشاط بعد.", @@ -2172,25 +2215,13 @@ module.exports = { tribeTrendingPeriodWeek: "هذا الأسبوع", tribeTrendingPeriodAll: "كل الأوقات", tribeTrendingEngagement: "التفاعل", - tribeOpinionsEmpty: "لا توجد آراء بعد.", - tribeOpinionsCast: "تصويت", - tribeOpinionsRankings: "الترتيب", - tribeOpinionsAlreadyVoted: "تم التصويت بالفعل", tribeTopCategory: "الأكثر تصويتًا", - tribePixeliaTitle: "بكسيليا", - tribePixeliaDescription: "لوحة فن بكسل تعاونية.", - tribePixeliaPaint: "رسم", - tribePixeliaContributors: "المساهمون", - tribePixeliaTotalPixels: "البكسلات المرسومة", tribeTagsEmpty: "لم يتم العثور على وسوم.", - tribeTagsCloud: "سحابة الوسوم", - tribeTagsContentWith: "محتوى بالوسم", - tribeSearchPlaceholder: "ابحث في محتوى القبيلة...", tribeSearchEmpty: "لم يتم العثور على نتائج.", - tribeSearchResults: "النتائج", tribeSearchMinChars: "أدخل حرفين على الأقل للبحث.", agendaTitle: "الأجندة", agendaDescription: "هنا يمكنك العثور على جميع العناصر المسندة إليك.", + agendaSearchPlaceholder: "ابحث في الأجندة...", agendaFilterAll: "الكل", agendaFilterToday: "اليوم", agendaFilterUpcoming: "القادمة", @@ -2204,23 +2235,15 @@ module.exports = { agendaFilterReports: "التقارير", agendaFilterTransfers: "التحويلات", agendaFilterJobs: "الوظائف", + agendaFilterIndustry: "الصناعة", + agendaFilterHousing: "السكن", agendaFilterProjects: "المشاريع", agendaFilterCalendars: "التقويمات", agendaNoItems: "لم يتم العثور على إسنادات.", - agendaAuthor: "بواسطة", agendaDiscardButton: "تجاهل", agendaRestoreButton: "استعادة", - agendaCreatedAt: "أُنشئ في", - agendaTitleLabel: "العنوان", + agendaRemoveButton: "إزالة", agendaMembersCount: "الأعضاء", - agendaDescriptionLabel: "الوصف", - agendaStatus: "الحالة", - agendaVisibility: "الظهور", - agendaEventDate: "تاريخ الحدث", - agendaEventLocation: "الموقع", - agendaEventPrice: "السعر", - agendaEventUrl: "الرابط", - agendaTaskStart: "وقت البدء", agendaLocationLabel: "الموقع", agendaYes: "نعم", agendaNo: "لا", @@ -2230,11 +2253,6 @@ module.exports = { agendareportCategory: "الفئة", agendareportSeverity: "الخطورة", agendareportStatus: "الحالة", - agendareportDescription: "الوصف", - agendaTaskEnd: "وقت الانتهاء", - agendaTaskPriority: "الأولوية", - agendaTransferFrom: "من", - agendaTransferTo: "إلى", agendaTransferConcept: "المفهوم", agendaTransferAmount: "المبلغ", agendaTransferDeadline: "الموعد النهائي", @@ -2244,47 +2262,7 @@ module.exports = { voteNow: "صوّت الآن", alreadyVoted: "لقد أبديت رأيك بالفعل.", noOpinionsFound: "لم يتم العثور على آراء.", - RECENTButton: "الأخيرة", TOPButton: "الأفضل", - interestingButton: "مثير للاهتمام", - necessaryButton: "ضروري", - funnyButton: "مضحك", - disgustingButton: "مقزز", - sensibleButton: "حساس", - propagandaButton: "دعاية", - adultOnlyButton: "للبالغين فقط", - boringButton: "ممل", - confusingButton: "مربك", - usefulButton: "مفيد", - informativeButton: "إعلامي", - wellResearchedButton: "بحث جيد", - accurateButton: "دقيق", - needsSourcesButton: "يحتاج مصادر", - wrongButton: "خاطئ", - lowQualityButton: "جودة منخفضة", - creativeButton: "إبداعي", - insightfulButton: "ثاقب", - actionableButton: "قابل للتنفيذ", - inspiringButton: "ملهم", - loveButton: "حب", - clearButton: "واضح", - upliftingButton: "مُحفّز", - unnecessaryButton: "غير ضروري", - rejectedButton: "مرفوض", - misleadingButton: "مضلل", - offTopicButton: "خارج الموضوع", - duplicateButton: "مكرر", - clickbaitButton: "طُعم نقرات", - spamButton: "رسائل مزعجة", - trollButton: "تصيّد", - nsfwButton: "NSFW", - violentButton: "عنيف", - toxicButton: "سام", - harassmentButton: "تحرش", - hateButton: "كراهية", - scamButton: "احتيال", - triggeringButton: "مُحفّز سلبيًا", - opinionsCreatedAt: "أُنشئ في", opinionsTotalCount: "إجمالي الآراء", voteInteresting: "مثير للاهتمام", voteNecessary: "ضروري", @@ -2321,29 +2299,41 @@ module.exports = { voteCreative: "إبداعي", voteSpam: "رسائل مزعجة", voteAdultOnly: "للبالغين فقط", - publishBlog: "نشر مدونة", privateMessage: "رسالة خاصة", pmSendTitle: "الرسائل الخاصة", pmSend: "إرسال!", - pmDescription: "استخدم هذا النموذج لإرسال رسالة مشفرة إلى سكان آخرين.", + pmCancel: "إلغاء", + pmReset: "إعادة تعيين", + pmSharedKeyHint: "شارك هذا المفتاح مع المستلم عبر قناة أخرى. فهو ضروري لفك التشفير.", + pmDescription: "استخدم هذا النموذج لإرسال رسالة/ملف مشفّر إلى سكان آخرين.", + pmComposeTitle: "إرسال رسالة", pmRecipients: "المستلمون", pmRecipientsHint: "أدخل معرّفات Oasis مفصولة بفواصل", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "حتى 7 من السكان لكل رسالة.", + pmTextPlaceholder: "اكتب رسالتك.", pmSubject: "الموضوع", pmSubjectHint: "أدخل موضوع الرسالة", pmText: "الرسالة", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "فك التشفير", + fileShareTitle: "مشاركة ملف", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "ملف", + fileShareDownload: "تنزيل", + fileShareMutualError: "يمكنك مشاركة الملفات فقط مع السكان ذوي الدعم المتبادل.", + fileShareNoFile: "لم يتم اختيار أي ملف.", + fileShareTooLarge: "الملف يتجاوز الحجم المسموح.", + fileShareFailed: "تعذّر تحضير الملف.", + fileShareSendError: "تعذّر إرسال الملف.", + fileShareUnavailable: "هذا الملف غير متاح حاليًا. حاول لاحقًا.", pmCrypterBadKey: "المفتاح المشترك غير صحيح!", pmCrypterTooLong: "الرسالة طويلة جدًا للتشفير باستخدام Crypter. يُرجى اختصارها والمحاولة مرة أخرى.", pmCrypterCipherLabel: "الرسالة المعاد تشفيرها", pmCrypterCharsLabel: "حرفًا", pmInvalidRecipients: "معرّف Oasis غير صالح (يجب أن يكون بالشكل @…=.ed25519).", pmEncryptionLabel: "التشفير", - pmFile: "المرفق", - private: "خاص", - privateDescription: "رسائلك المشفرة.", + private: "صندوق الوارد", + privateDescription: "أدر رسائلك الخاصة.", privateInbox: "الوارد", privateSent: "المُرسَل", privateDelete: "حذف", @@ -2351,19 +2341,27 @@ module.exports = { noPrivateMessages: "لا توجد رسائل خاصة.", pmFromLabel: "من:", pmToLabel: "إلى:", - pmInvalidMessage: "رسالة غير صالحة", pmNoSubject: "(بدون موضوع)", pmSubjectLabel: "الموضوع:", - pmBodyLabel: "النص", - pmBotJobs: "42-JobsBOT", - pmBotProjects: "42-ProjectsBOT", - pmBotMarket: "42-MarketBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "لقد تغيّر البيت الحاكم في L.A.R.P.", + politicalBotParliamentTitle: "بدأت دورة حكم جديدة في البرلمان.", + politicalBotTribeTitle: "بدأت دورة حكم جديدة في إحدى قبائلك.", + politicalBotLarpIntro: "الآن يحكم {house} خلال هذه الدورة: {cycle}", + politicalBotParliamentIntro: "الحكومة الحالية هي {gov}", + politicalBotParliamentIntroLeader: "الحكومة الحالية هي {gov} يمارسها {leader}", + politicalBotTribeIntro: "الحكومة الحالية في قبيلة {tribe} هي {gov}", + politicalBotTribeIntroLeader: "الحكومة الحالية في قبيلة {tribe} هي {gov} يمارسها {leader}", + politicalBotLeaderLabel: "القائد", inboxJobSubscribedTitle: "اشتراك جديد في عرض عملك", pmInhabitantWithId: "ساكن بمعرّف OASIS:", pmHasSubscribedToYourJobOffer: "اشترك في عرض عملك", - inboxProjectCreatedTitle: "تم إنشاء مشروع جديد", - pmHasCreatedAProject: "أنشأ مشروعًا", inboxMarketItemSoldTitle: "تم بيع عنصر", + inboxShopSoldTitle: "تم بيع المنتج", pmYourItem: "عنصرك", pmHasBeenSoldTo: "تم بيعه إلى", pmFor: "مقابل", @@ -2371,7 +2369,6 @@ module.exports = { pmHasPledged: "تبرع بـ", pmToYourProject: "لمشروعك", blockchain: 'مستكشف الكتل', - blockchainTitle: 'مستكشف الكتل', blockchainDescription: 'استكشف وتصوّر الكتل في البلوكتشين.', blockchainNoBlocks: 'لم يتم العثور على كتل في البلوكتشين.', blockchainStatsTitle: 'الإحصاءات', @@ -2383,42 +2380,36 @@ module.exports = { blockchainBlockCarbon: 'البصمة الكربونية', blockchainBlockType: 'النوع', blockchainBlockTimestamp: 'الطابع الزمني', - blockchainBlockContent: 'الكتلة', - blockchainBlockURL: 'الرابط:', - blockchainContent: 'الكتلة', - blockchainContentPreview: 'معاينة محتوى الكتلة', blockchainLatestDatagram: 'آخر بيانات', - blockchainDatagram: 'البيانات', - blockchainDetails: 'عرض تفاصيل الكتلة', - blockchainViewBlockexplorer: "عرض في مستكشف الكتل", - blockchainBlockInfo: 'معلومات الكتلة', - blockchainBlockDetails: 'تفاصيل الكتلة المحددة', + blockchainDatagram: "مخطط البيانات", + blockchainDetails: "عرض تفاصيل الكتلة", + blockchainViewBlockexplorer: "العرض في Blockexplorer", blockchainBack: 'العودة إلى مستكشف الكتل', blockchainContentDeleted: "تم حذف هذا المحتوى", visitContent: "زيارة المحتوى", + favoriteAdd: "تثبيت في المفضلة", + pmContentTooltip: "إرسال رسالة خاصة", + phoneCallTooltip: "إجراء مكالمة هاتفية", + favoriteRemove: "إزالة من المفضلة", + reportContent: "الإبلاغ عن المحتوى", banking: 'المصرفية', bankingTitle: 'المصرفية', - bankingDescription: 'استكشف القيمة الحالية لـ ECOin وتخصيص الدخل الأساسي الشامل المقابل، الموزّع لكل حقبة بناءً على المشاركة والثقة.', + bankingDescription: "اقتصاد ECOin الجماعي.", bankOverview: 'نظرة عامة', bankEpochs: 'الحقب', bankRules: 'القواعد', pending: 'معلّق', closed: 'مغلق', - bankBack: 'العودة إلى المصرفية', - bankViewTx: 'عرض المعاملة', bankClaimNow: 'طالب الآن', - bankClaimUBI: 'طالب بـ UBI!', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'لا توجد تخصيصات UBI معلقة لهذه الحقبة.', + bankClaimUBI: 'طالب بـ UBI', + bankClaimAndPay: "المطالبة والدفع", + bankClaimedPending: "المطالبة قيد الانتظار...", + bankStatusClaimed: "تمت المطالبة", + bankStatusExpired: "منتهية الصلاحية", + bankPubOnly: "عملية خاصة بـ PUB فقط", bankEpoch: 'الحقبة', bankPool: 'المجمع (هذه الحقبة)', bankWeightsSum: 'مجموع الأوزان', - bankAllocations: 'التخصيصات', bankNoAllocations: 'لم يتم العثور على تخصيصات.', bankNoEpochs: 'لم يتم العثور على حقب.', bankEpochAllocations: 'تخصيصات الحقبة', @@ -2432,16 +2423,21 @@ module.exports = { bankEpochId: 'معرّف الحقبة', bankRuleHash: 'بصمة لقطة القواعد', bankViewEpoch: 'عرض الحقبة', + bankYourIndustryBalance: "حصتك الصناعية", + bankYourUbiMonth: "دخلك الأساسي (هذا الشهر)", + bankYourFundsMonth: "إجمالي الأموال", + bankIndustryBalance: "الإنتاج الصناعي", bankUserBalance: 'رصيدك', - ecoWalletNotConfigured: 'محفظة ECOin غير مُعدّة', - editWallet: 'تعديل المحفظة', - addWallet: 'إضافة محفظة', bankAddresses: 'العناوين', bankNoAddresses: 'لم يتم العثور على عناوين.', bankUser: 'معرّف Oasis', bankAddress: 'العنوان', bankAddAddressTitle: 'إضافة عنوان ECOIN', bankAddAddressUser: 'معرّف Oasis', + bankAddAddressLabel: "تسمية (اختياري)", + bankAddAddressLabelPlaceholder: "محفظة بوب، متجر أليس…", + bankAddAddressUserOptional: "معرّف Oasis (اختياري)", + bankAddressUnnamed: "عنوان بدون اسم", bankAddAddressAddress: 'عنوان ECOIN', bankAddAddressSave: 'حفظ', bankAddressAdded: 'تمت إضافة العنوان', @@ -2459,19 +2455,11 @@ module.exports = { bankAddressDeleteConfirm: 'حذف هذا العنوان؟', search: 'ابحث!', bankLocal: 'محلي', - bankFromOasis: 'Oasis', - bankMyAddress: 'عنوانك', - bankRemoveMyAddress: 'إزالة عنواني', - bankNotRemovableOasis: 'لا يمكن إزالة العناوين محليًا', - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "الشبكة", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "لا توجد أموال!", bankUbiAvailableOk: "متوفر!", - bankUbiAvailability: "توفر UBI", + bankUbiAvailability: "الدخل الأساسي", bankAlreadyClaimedThisMonth: "تم المطالبة بالفعل هذا الشهر", bankUbiThisMonth: "الدخل الأساسي (هذا الشهر)", bankUbiLastClaimed: "الدخل الأساسي (آخر مطالبة)", @@ -2485,13 +2473,6 @@ module.exports = { shopsTitle: "متاجر", shopDescription: "اكتشف وأدر المتاجر في الشبكة.", shopTitle: "متجر", - shopFilterAll: "الكل", - shopFilterMine: "متاجري", - shopFilterRecent: "الأحدث", - shopFilterTop: "الأفضل", - shopFilterProducts: "المنتجات", - shopFilterPrices: "الأسعار", - shopFilterFavorites: "المفضلة", shopUpload: "إنشاء متجر", shopAllSectionTitle: "جميع المتاجر", shopMineSectionTitle: "متاجري", @@ -2508,19 +2489,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "النشر على Clearnet", - shopClearnetUnpublish: "إلغاء النشر من Clearnet", - shopClearnetUrlLabel: "رابط Clearnet", - shopClearnetWarning: "النشر على Clearnet يجعل هذا المتجر قابلاً للفهرسة بواسطة محركات البحث والزواحف الخارجية.", + transportChipLabel: "مشفّر أثناء النقل", shopAddFavorite: "إضافة مفضلة", shopRemoveFavorite: "إزالة مفضلة", shopOpen: "مفتوح", shopClosed: "مغلق", - shopOpenShop: "فتح المتجر", - shopCloseShop: "إغلاق المتجر", shopMakePrivate: "جعلها خاصة (E2E)", shopMakePublic: "جعلها عامة", shopProducts: "المنتجات", + shopFilterAll: "الكل", + shopFilterRecent: "الأحدث", + shopFilterMine: "خاصتي", + shopFilterTop: "الأفضل", + shopFilterProducts: "المنتجات", + shopFilterPrices: "الأسعار", + shopFilterFavorites: "المفضلة", shopProductAdd: "إضافة منتج", shopProductTitle: "منتج", shopProductUpdate: "تحديث منتج", @@ -2545,85 +2528,63 @@ module.exports = { shopOrderPrice: "السعر", shopOrderBuyer: "المشتري", shopBackToShop: "العودة إلى المتجر", - shopShareUrl: "رابط المشاركة", - shopVisitShop: "زيارة المتجر", marketVisitItem: "زيارة العنصر", shopStatus: "الحالة", shopCreatedAt: "تاريخ الإنشاء", - shopSearchPlaceholder: "البحث عن متاجر...", + shopSearchPlaceholder: "ابحث في متاجر...", shopUrl: "الرابط", shopLocation: "الموقع", shopTags: "الوسوم", shopVisibility: "الرؤية", - shopImage: "صورة", shopShortDescription: "وصف قصير", shopShortDescriptionPlaceholder: "وصف مختصر لبطاقات المتجر (160 حرف كحد أقصى)", shopTitlePlaceholder: "اسم متجرك", shopDescriptionPlaceholder: "وصف تفصيلي لمتجرك", - shopUrlPlaceholder: "https://رابط-متجرك.com", shopLocationPlaceholder: "المدينة، البلد", - shopTagsPlaceholder: "وسم1، وسم2، وسم3", + shopTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", mapTitlePlaceholder: "عنوان الخريطة", shopProductFeatured: "منتج مميز", - shopSendToMarket: "Send to Market", + shopSendToMarket: "إرسال إلى السوق", typeShop: "متاجر", typeShopProduct: "منتج متجر", bankExchange: 'الصرف', bankExchangeCurrentValue: 'قيمة ECOin (1 ساعة)', - bankTotalSupply: 'إجمالي معروض ECOin', - bankEcoinHours: "معادلة ECOin بالوقت", + bankTotalSupply: "إجمالي معروض ECOin", + bankPubsSupply: "معروض ECOin لرصيد PUBs", + bankHoldingSupply: "معروض ECOin خارج UBI", + bankEcoinHours: "معادلة ECOin بالوقت (1 ECO)", bankHoursOfWork: 'ساعات', bankUnitMs: "مللي ثانية", bankUnitSeconds: "ثواني", bankUnitMinutes: "دقائق", bankUnitDays: "أيام", bankExchangeNoData: 'لا توجد بيانات متاحة', - bankExchangeIndex: 'قيمة ECOin (1 ساعة)', - bankInflation: 'تضخم ECOin (anual)', - bankInflationMonthly: 'تضخم ECOin (mensual)', + bankInflation: "تضخم ECOin (سنوي)", + bankInflationMonthly: "تضخم ECOin (شهري)", bankExchangeChartTitle: 'تطور قيمة ECOin عبر الزمن', bankExchangeChartValue: 'القيمة (ECO/ساعة)', bankExchangeChartSupply: 'العرض', bankExchangeChartInflation: 'التضخم %', bankExchangeChartEmpty: 'لا توجد عينات كافية بعد — ارجع لاحقًا', - bankCurrentSupply: 'المعروض الحالي من ECOin', + bankCurrentSupply: "معروض ECOin الحالي", bankingSyncStatus: 'حالة ECOin', bankingSyncStatusSynced: 'متزامن', - bankingSyncStatusOutdated: 'قديم', + bankingSyncStatusOutdated: "غير متزامن", statsTitle: 'الإحصائيات', statistics: "الإحصائيات", - statsInhabitant: "إحصائيات الساكن", statsDescription: "اكتشف إحصائيات عن شبكتك.", ALLButton: "الكل", + RECENTButton: "الأخيرة", MINEButton: "خاصتي", TOMBSTONEButton: "شواهد القبور", - statsYou: "أنت", - statsUserId: "معرّف Oasis", statsCreatedAt: "أُنشئ في", statsYourContent: "المحتوى", statsYourOpinions: "الآراء", - statsYourTombstone: "شواهد القبور", - statsNetwork: "الشبكة", - statsTotalInhabitants: "السكان", - statsDiscoveredTribes: "القبائل (العامة)", - statsPrivateDiscoveredTribes: "القبائل (الخاصة)", statsNetworkContent: "المحتوى", - statsYourMarket: "السوق", - statsYourJob: "الوظائف", - statsYourProject: "المشاريع", - statsYourTransfer: "التحويلات", - statsYourForum: "المنتديات", statsNetworkOpinions: "الآراء", - statsDiscoveredMarket: "السوق", - statsDiscoveredJob: "الوظائف", - statsDiscoveredProject: "المشاريع", - statsBankingTitle: "المصرفية", - statsEcoWalletLabel: "محفظة ECOIN", + statsEcoWalletLabel: "محفظة ECOin", + walletAddressCopyHint: "انقر للتحديد ثم انسخ", statsEcoWalletNotConfigured: "غير مُعدّة!", - statsTotalEcoAddresses: "إجمالي العناوين", - statsDiscoveredTransfer: "التحويلات", - statsDiscoveredForum: "المنتديات", - statsNetworkTombstone: "شواهد القبور", statsBookmark: "العلامات المرجعية", statsEvent: "الأحداث", statsTask: "المهام", @@ -2644,17 +2605,15 @@ module.exports = { statsTransfer: "التحويلات", statsAiExchange: "الذكاء الاصطناعي", statsPUBs: 'PUBs', + statsSyncedPeers: "الأقران", + statsTotalLabel: "الإجمالي", statsPost: "المنشورات", - statsOasisID: "معرّف Oasis", statsSize: "الإجمالي (الحجم)", statsBlockchainSize: "البلوكتشين (الحجم)", statsBlobsSize: "الملفات (الحجم)", statsActivity7d: "النشاط (آخر 7 أيام)", statsActivity7dTotal: "إجمالي 7 أيام", statsActivity30dTotal: "إجمالي 30 يومًا", - statsKarmaScore: "نقاط الكارما", - statsPublic: "عام", - statsPrivate: "خاص", day: "يوم", messages: "الرسائل", statsProject: "المشاريع", @@ -2666,30 +2625,14 @@ module.exports = { statsProjectsCancelled: "ملغى", statsProjectsGoalTotal: "الهدف الإجمالي", statsProjectsPledgedTotal: "إجمالي التبرعات", - statsProjectsSuccessRate: "نسبة النجاح", - statsProjectsAvgProgress: "متوسط التقدم", - statsProjectsMedianProgress: "متوسط التقدم (الوسيط)", - statsProjectsActiveFundingAvg: "متوسط التمويل النشط", - statsJobsTitle: "الوظائف", - statsJobsTotal: "إجمالي الوظائف", - statsJobsOpen: "مفتوح", - statsJobsClosed: "مغلق", - statsJobsOpenVacants: "الشواغر المفتوحة", - statsJobsSubscribersTotal: "إجمالي المشتركين", - statsJobsAvgSalary: "متوسط الراتب", - statsJobsMedianSalary: "متوسط الراتب (الوسيط)", statsMarketTitle: "السوق", statsMarketTotal: "إجمالي العناصر", statsMarketForSale: "للبيع", statsMarketReserved: "محجوز", statsMarketClosed: "مغلق", statsMarketSold: "مُباع", - statsMarketRevenue: "الإيرادات", - statsMarketAvgSoldPrice: "متوسط سعر البيع", statsUsersTitle: "السكان", user: "ساكن", - statsTombstoneTitle: "شواهد القبور", - statsNetworkTombstones: "شواهد قبور الشبكة", statsTombstoneRatio: "نسبة شواهد القبور (%)", statsParliamentCandidature: "ترشيحات البرلمان", statsParliamentTerm: "فترات البرلمان", @@ -2715,31 +2658,28 @@ module.exports = { aiPrompt: "قدّم ردًا إعلاميًا ودقيقًا.", aiConfiguration: "تعيين التوجيه", aiPromptUsed: "التوجيه", - aiClearHistory: "مسح سجل المحادثة", - aiSharePrompt: "إضافة هذه الإجابة للتدريب الجماعي؟", - aiShareYes: "نعم", - aiShareNo: "لا", - aiSharedLabel: "تمت الإضافة للتدريب", - aiRejectedLabel: "لم تتم الإضافة للتدريب", - aiServerError: "لم يتمكن الذكاء الاصطناعي من الرد. يرجى المحاولة مرة أخرى.", + aiClearHistory: "مسح السجل", + aiServerError: "لا أستطيع الإجابة بشكل صحيح. حاول مرة أخرى.", + aiStatusLoading: "يقوم 42 بتحميل النموذج. اسأل مرة أخرى بعد لحظة.", + aiThinking: "يفكر", + aiWaiting: "في الانتظار", + aiStatusMissingModel: "نموذج الذكاء الاصطناعي غير مثبت: الملف src/AI/oasis-42-1-chat.Q4_K_M.gguf مفقود.", + aiExportFineTuning: "تصدير Fine-Tuning", + aiFactsTitle: "بيانات من شبكتك", aiInputPlaceholder: "ما هو Oasis؟", typeAiExchange: "الذكاء الاصطناعي", aiApproveTrain: "إضافة للتدريب الجماعي", aiRejectTrain: "عدم التدريب", - aiTrainPending: "بانتظار الموافقة", aiTrainApproved: "تمت الموافقة للتدريب", aiTrainRejected: "تم الرفض للتدريب", aiSnippetsUsed: "المقتطفات المُستخدَمة", aiSnippetsLearned: "المقتطفات المُتعلَّمة", statsAITraining: "تدريب الذكاء الاصطناعي", - aiApproveCustomTrain: "التدريب باستخدام هذه الإجابة المخصصة", aiCustomAnswerPlaceholder: "اكتب إجابتك المخصصة…", - statsAIExchanges: "تبادلات النموذج", marketMineSectionTitle: "عناصرك", marketCreateSectionTitle: "إنشاء عنصر", marketUpdateSectionTitle: "تحديث", marketAllSectionTitle: "السوق", - marketRecentSectionTitle: "السوق الأخير", marketTitle: "السوق", marketDescription: "سوق لتبادل السلع أو الخدمات في شبكتك.", marketFilterAll: "الكل", @@ -2754,7 +2694,7 @@ module.exports = { marketFilterDiscarded: "ملغى", marketFilterRecent: "الأخيرة", marketFilterMyBids: "العروض", - marketCreateButton: "إنشاء عنصر", + marketCreateButton: "بِع شيئًا", marketItemType: "النوع", marketItemTitle: "العنوان", marketItemAvailable: "الموعد النهائي", @@ -2769,14 +2709,11 @@ module.exports = { marketItemDeadline: "الموعد النهائي", marketItemIncludesShipping: "يشمل الشحن؟", marketItemHighestBid: "أعلى عرض", - marketItemHighestBidder: "صاحب أعلى عرض", marketItemStock: "المخزون", marketOutOfStock: "نفد المخزون", - marketItemBidTime: "وقت العرض", marketActionsUpdate: "تحديث", marketUpdateButton: "تحديث", marketActionsDelete: "حذف", - marketActionsSold: "تعيين كمُباع", marketActionsChangeStatus: "تغيير الحالة", marketActionsBuy: "شراء!", marketAuctionBids: "العروض الحالية", @@ -2785,19 +2722,81 @@ module.exports = { marketNoItems: "لا توجد عناصر متاحة بعد.", marketYourBid: "عرضك", marketCreateFormImageLabel: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", - marketSearchLabel: "بحث", - marketSearchPlaceholder: "ابحث عن عنوان أو وسوم", + marketSearchPlaceholder: "ابحث في السوق...", marketMinPriceLabel: "الحد الأدنى للسعر", marketMaxPriceLabel: "الحد الأقصى للسعر", - marketSortLabel: "ترتيب حسب", marketSortRecent: "الأحدث", marketSortPrice: "السعر", marketSortDeadline: "الموعد النهائي", marketSearchButton: "بحث", marketAuctionEndsIn: "ينتهي", marketAuctionEnded: "انتهى", - marketMyBidBadge: "عرضتَ", marketNoItemsMatch: "لا توجد عناصر تطابق بحثك.", + housingTitle: "السكن", + housingDescriptionText: "اكتشف وأدر الأماكن في شبكتك.", + modulesHousingLabel: "السكن", + modulesHousingDescription: "وحدة لاكتشاف الأماكن وإدارتها.", + typeHousing: "السكن", + housingFilterAll: "الكل", + housingFilterMine: "لي", + housingFilterRequested: "المطلوبة", + housingFilterSale: "للبيع", + housingFilterRent: "للإيجار", + housingFilterCouchsurfing: "الاستضافة", + housingFilterOpen: "المتاحة", + housingFilterClosed: "غير المتاحة", + housingFilterRecent: "حديثة", + housingFilterTop: "الأعلى", + housingType: "العرض", + housingTypeSALE: "للبيع", + housingTypeRENT: "للإيجار", + housingTypeCOUCHSURFING: "الاستضافة المجانية", + housingProperty: "العقار", + housingPropertyAPARTMENT: "شقة", + housingPropertyHOUSE: "منزل", + housingPropertyROOM: "غرفة", + housingPropertyLAND: "أرض", + housingPropertyOTHER: "أخرى", + housingTitleLabel: "العنوان", + housingTitlePlaceholder: "اسم المكان", + housingDescription: "الوصف", + housingDescriptionPlaceholder: "صف المكان والحي وما هو مشمول: الحمامات، الحديقة، الأسِرّة…", + housingRules: "القواعد", + housingRulesPlaceholder: "الحيوانات، التدخين، ساعات الهدوء، أقل مدة…", + housingPlace: "المكان", + housingPlacePlaceholder: "البلد، المدينة، العنوان", + housingPrice: "السعر", + housingPriceLabel: "السعر (ECO)", + housingSizeLabel: "المساحة (م²)", + housingFree: "مجاني", + housingPerMonth: "/شهر", + housingRooms: "الغرف", + housingSize: "المساحة", + housingCapacity: "الضيوف", + housingAvailableFrom: "متاح من", + housingAvailableTo: "متاح حتى", + housingTags: "الوسوم", + spreadEditWarning: "سيفقد هذا المحتوى نشراته بعد تعديله", + housingRequests: "الطلبات", + housingRequestButton: "طلب", + housingCancelButton: "إلغاء الطلب", + housingRequestedBadge: "مطلوب", + housingStatus: "الحالة", + housingStatusOPEN: "متاح", + housingStatusCLOSED: "غير متاح", + housingSetOpen: "اجعله متاحاً", + housingSetClosed: "اجعله غير متاح", + housingCreateButton: "إنشاء مكان", + housingUpdateButton: "تحديث", + housingSearchPlaceholder: "ابحث في السكن...", + housingSearchButton: "بحث", + housingMinPrice: "أقل سعر", + housingMaxPrice: "أعلى سعر", + housingSortRecent: "الأحدث", + housingSortPrice: "الأقل سعراً", + housingSortRequests: "الأكثر طلباً", + housingSortRating: "الأعلى تقييماً", + housingNoItems: "لم يتم العثور على أماكن.", jobsTitle: "الوظائف", jobsCandidatesTitle: "المرشحون المقترحون", jobsCandidatesDescription: "السكان الذين تتطابق مهاراتهم مع هذه الوظيفة. أرسل لهم رسالة خاصة لدعوتهم.", @@ -2845,7 +2844,6 @@ module.exports = { jobLocationPresencial: "حضوري", jobLocationRemote: "عن بُعد", jobVacantsPlaceholder: "عدد المناصب", - jobSalaryPlaceholder: "الراتب بـ ECO لساعة عمل واحدة", jobImage: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", jobTasks: "المهام", jobType: "نوع الوظيفة", @@ -2855,7 +2853,6 @@ module.exports = { jobsFilterTop: "الأفضل", jobsTopTitle: "الوظائف الأعلى راتبًا", createJobButton: "نشر الوظيفة", - viewDetailsButton: "عرض التفاصيل", noJobsFound: "لم يتم العثور على عروض وظيفية.", jobAuthor: "بواسطة", jobTypeFreelance: "مستقل", @@ -2867,42 +2864,208 @@ module.exports = { jobsHoursOffered: "الساعات المعروضة", jobsHoursRequested: "الساعات المطلوبة في المقابل", jobsExchangeSkill: "المهارة المطلوبة في المقابل", - jobsHoursOfferedPlaceholder: "مثال: 4", - jobsHoursRequestedPlaceholder: "مثال: 4", - jobsExchangeSkillPlaceholder: "مثال: النجارة، التصميم", - jobExchangeHint: "بالنسبة لوظائف تبادل الساعات، املأ الحقول أدناه بدلاً من الراتب.", jobTimePartial: "دوام جزئي", jobTimeComplete: "دوام كامل", - jobsDeleteButton: "حذف", jobsUpdateButton: "تحديث", jobsFilterApplied: "مُتقدَّم", jobsAppliedTitle: "طلباتي", jobsAppliedBadge: "مُتقدَّم", - jobsFilterFavs: "المفضلة", - jobsFavsTitle: "المفضلة", - jobsFilterNeeds: "يحتاج مساعدة", - jobsNeedsTitle: "يحتاج مساعدة", - jobsSearchLabel: "بحث", - jobsSearchPlaceholder: "ابحث عن عنوان، وسوم، وصف...", + jobsSearchPlaceholder: "ابحث في الوظائف...", jobsMinSalaryLabel: "الحد الأدنى للراتب", jobsMaxSalaryLabel: "الحد الأقصى للراتب", - jobsSortLabel: "ترتيب حسب", jobsSortRecent: "الأحدث", jobsSortSalary: "الأعلى راتبًا", jobsSortSubscribers: "الأكثر متقدمين", jobsSearchButton: "بحث", - jobsFavoriteButton: "إضافة للمفضلة", - jobsUnfavoriteButton: "إزالة من المفضلة", - jobsMessageAuthorButton: "رسالة خاصة", jobsApplicants: "المتقدمون", jobsUpdatedAt: "تم التحديث", jobSetOpen: "تعيين كمفتوح", - jobNewBadge: "جديد", jobsTagsLabel: "الوسوم", - jobsTagsPlaceholder: "وسم1، وسم2، وسم3", noJobsMatch: "لا توجد وظائف تطابق بحثك.", + industrySearchPlaceholder: "ابحث في الصناعة...", + industryAllSectors: "جميع القطاعات", + industryVisitButton: "زيارة الصناعة", + industryAdvanceTo: "تعيين إلى", + industryAmount: "المبلغ", + industryApproveBuild: "الموافقة", + industryBlueprint: "مخطط", + industryBlueprints: "مخططات", + industryBuild: "بناء", + industryBuildNotes: "ملاحظات", + industryBuildStart: "تاريخ البدء", + industryVoteUpdateButton: "التصويت للتحديث", + industryVoteDeleteButton: "التصويت للحذف", + industryRevokeVote: "سحب", + industryTimeLeft: "الوقت المتبقي", + industryBuildEnd: "تاريخ الانتهاء", + industryBuilds: "عمليات البناء", + industryBuildTitle: "العنوان", + industryContribute: "المساهمة", + industryContributeButton: "ساهم", + industryContributions: "المساهمات", + industryCreateBlueprintButton: "إنشاء مخطط", + industryDistribute: "توزيع", + industryDistributeButton: "التوزيع حسب الحصص", + industryDistribution: "التوزيع", + industryEcoAmount: "المبلغ (ECO)", + industryEditBlueprint: "تعديل المخطط", + industryFacility: "المصنع", + industryKindLabel: "النوع", + industryKind_labor: "العمل", + industryKind_material: "مادة", + industryKind_eco: "أموال", + industryLaborHours: "العمل (ساعات)", + industryHours: "ساعات", + industryMaterialItem: "مادة", + industryMaterials: "المواد", + industryEstMaterials: "إجمالي المواد", + industryEstLabor: "إجمالي العمل", + industryEstTotal: "السعر المقدر", + industryBuildingPrice: "سعر التصنيع", + industryFinalPrice: "السعر النهائي", + industryBlueprintDescPlaceholder: "المواصفات والأبعاد والوزن والوثائق…", + industryMaterialValue: "القيمة (ECO)", + industryMember: "عضو", + industryNewBlueprint: "مخطط جديد", + industryNewBuild: "اقتراح بناء", + industryEditBuild: "تحرير الإنتاج", + industryNeedBlueprint: "أنشئ مخططًا أولاً: كل إنتاج يصنع واحدًا.", + industryNoBlueprints: "لا توجد مخططات بعد.", + industryNoBuilds: "لا توجد عمليات بناء بعد.", + industryNote: "ملاحظة", + industryOutput: "المخرجات", + industryOutputKind: "نوع المنتج", + industryOutputValue: "قيمة المخرجات (ECO، مثلاً من البيع)", + industryPostJob: "إنشاء وظيفة", + industryPot: "الوعاء", + industryProposeBuildButton: "اقتراح البناء", + industryProposer: "المقترح", + industryAuthor: "المؤلف", + industrySellOnMarket: "إرسال إلى السوق", + industrySendToProjects: "إنشاء مشروع", + industrySkills: "المهارات", + industryTreasury: "الخزينة", + industryKind_physical: "مادي", + industryKind_digital: "رقمي", + industryBuildStatus_PROPOSED: "مقترح", + industryBuildStatus_REJECTED: "مرفوض", + industryBuildStatus_APPROVED: "معتمد", + industryBuildStatus_STOCKING: "تجهيز", + industryBuildStatus_IN_PRODUCTION: "قيد الإنتاج", + industryBuildStatus_COMPLETED: "مكتمل", + industryBuildStatus_FAILED: "فاشل", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "وظيفة تطابق سيرتك الذاتية", + jobsBotMatchIntro: "وظيفة تطابق سيرتك الذاتية.", + jobsBotMatchJob: "الوظيفة", + jobsBotMatchHint: "تصلك هذه الرسالة لأن سيرتك الذاتية مُدارة بالذكاء الاصطناعي. يمكنك تعطيلها من سيرتك.", + cvAiManaged: "مُدار بالذكاء الاصطناعي", + switchOn: "مُفعّل", + switchOff: "معطّل", + cvAiManagedHint: "عند التفعيل، ينبهك JobsBot برسالة خاصة عندما تطابق وظيفة سيرتك الذاتية.", + cvMatchThreshold: "نبّهني بدءًا من هذا المستوى من التطابق", + housingBotRequestedTitle: "طلب أحدهم أحد أماكنك.", + housingBotCancelledTitle: "تم إلغاء طلب على أحد أماكنك.", + housingBotYouRequestedTitle: "لقد طلبت مكاناً.", + housingBotYouCancelledTitle: "لقد ألغيت طلباً.", + housingBotUnavailableTitle: "المكان الذي طلبته لم يعد متاحاً.", + housingBotUnavailableSentTitle: "لقد أبلغت من طلبوا هذا المكان.", + industryBotAdmittedTitle: "تم قبولك في مصنع.", + industryBotApplicationTitle: "طلب عضوية جديد في مصنعك.", + industryBotInvitedTitle: "تمت دعوتك إلى مصنع.", + industryBotDissolvedTitle: "تم حل مصنع تنتمي إليه.", + industryBotBuildApprovedTitle: "تمت الموافقة على بناء.", + industryBotDistributedTitle: "تم توزيع مخرجات بناء.", + industryFilterRules: "القواعد", + industryRulesTitle: "القواعد", + industryRulesIntro: "تتيح Industry للسكان امتلاك وسائل الإنتاج جماعيًا: المصنع ورشة مملوكة للشبكة لا يملكها أحد بشكل خاص.", + industryRulesSteward: "المؤسس هو القيّم — راعٍ لا مالك: لا يمكنه بيع المصنع أو خصخصته أو حله بمفرده.", + industryRulesMembership: "تحدد سياسة العضوية كيفية الانضمام: مفتوح (فورًا)، بالتصويت (بقبول أصوات الأعضاء) أو بالدعوة فقط (يجب أن يدعوك عضو أولاً).", + industryRulesQuorum: "النصاب هو الحد الأدنى من الأعضاء الذين يجب أن يصوّتوا لتُحتسب القرار، حتى لا يقرر ساكن واحد في مصنع صغير.", + industryRulesMajority: "الأغلبية هي نسبة الأصوات اللازمة (0.5 = نصف، 1 = إجماع)؛ يمر القرار عندما تبلغ أصوات نعم على الأقل max(النصاب، الأعضاء × الأغلبية).", + industryRulesDecisions: "يصوّت الأعضاء لقبول الجدد والموافقة على عمليات البناء وحل المصنع؛ لا يمكن للقيّم تجاوز هذه القرارات الجماعية.", + industryRulesBlueprints: "المخططات وصفات حرة (COPYLEFT) — المدخلات (مواد، ساعات عمل، مهارات) والمخرجات (سلعة مادية أو رقمية) — ويمكن لأي شخص نسخها.", + industryRulesBuilds: "البناء أمر إنتاج: مقترح ← معتمد (بالتصويت) ← تجهيز ← قيد الإنتاج ← مكتمل، ولا يقبل مساهمات إلا بعد الاعتماد.", + industryRulesContributions: "يساهم الأعضاء بالعمل (ساعات) أو المواد (قيمة ECO معلنة) أو ECO في البناء؛ كل مساهمة تكسب Karma.", + industryRulesLaborRate: "أجر العمل هو قيمة ECO لساعة عمل في هذا المصنع؛ يحوّل الساعات إلى Karma لمقارنة الجهد ورأس المال بعدل: Karma = ساعات × الأجر + قيمة المواد + ECO.", + industryRulesShares: "حصتك في البناء هي Karma الخاص بك ÷ إجمالي Karma، وتحدد مقدار قيمة المخرجات التي تحصل عليها.", + industryRulesDistribution: "عند اكتمال الإنتاج، يوزّع الوصي المبلغ (أموال الإنتاج بالإضافة إلى قيمة البيع) على المساهمين بنسبة حصصهم.", + industryRulesTreasury: "يحتفظ القيّم بمساهمات ECO كأمين لخزينة البناء حتى التوزيع؛ تُسجَّل جميع الحركات على الشبكة ويمكن أن تذهب النزاعات إلى Courts.", + industryJobs: "وظائف", + industryReportButton: "إبلاغ", + industryCreateTask: "إنشاء مهمة", + industryOpenDispute: "فتح نزاع", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "إنشاء دعوة", + industryPauseVotes: "أصوات الحالة", + industryTitle: "الصناعة", + industryDescription: "وحدة لإدارة وسائل الإنتاج بشكل جماعي.", + typeIndustryBuild: "بناء", + typeIndustryBlueprint: "مخطط", + typeIndustryAllocation: "توزيع", + typeIndustry: "الصناعة", + modulesIndustryLabel: "الصناعة", + modulesIndustryDescription: "وحدة لإدارة وسائل الإنتاج بشكل جماعي.", + industryFilterAll: "الكل", + industryFilterRecent: "الأخيرة", + industryFilterMember: "عضو", + industryFilterBlueprints: "مخططات", + industryFilterBuilds: "إنتاجات", + industryFilterMine: "لي", + industryFilterActive: "يعمل", + industryFilterPaused: "موقوفة", + industryFilterDissolved: "منحلة", + industryAllTitle: "الصناعة", + industryRecentTitle: "المصانع الأخيرة", + industryMemberTitle: "المصانع التي أنتمي إليها", + industryMineTitle: "مصانعي", + industryActiveTitle: "يعمل", + industryPausedTitle: "المصانع الموقوفة", + industryDissolvedTitle: "المصانع المنحلة", + industryNoFacilitiesFound: "لم يتم العثور على مصانع.", + industryCreateFacility: "إنشاء مصنع", + industryMembers: "الأعضاء", + industryMemberBadge: "عضو", + industryName: "الاسم", + industryNamePlaceholder: "اسم المصنع", + industryDescriptionLabel: "الوصف", + industryDescriptionPlaceholder: "ماذا ينتج هذا المصنع؟", + industrySector: "القطاع", + industryMembershipPolicy: "سياسة العضوية", + industryQuorum: "النصاب", + industryMajority: "الأغلبية", + industryLaborRate: "أجر العمل", + industryCreateButton: "إنشاء مصنع", + industryUpdateButton: "تحديث", + industrySteward: "القيم", + industryStatusActive: "يعمل", + industryStatusPaused: "موقوف", + industryStatusDissolved: "منحل", + industryStatusLabel: "الحالة", + industryJoinButton: "انضمام", + industryApplyButton: "طلب الانضمام", + industryLeaveButton: "مغادرة", + industryInviteButton: "دعوة", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "تحتاج إلى دعوة للانضمام.", + industryPauseButton: "صوّت للإيقاف", + industryResumeButton: "صوّت للاستئناف", + industryDeleteButton: "حذف", + industryPendingApplicants: "طلبات معلقة", + industryVotesYes: "نعم", + industryVotesNo: "لا", + industryAlreadyVoted: "تم التصويت", + industryAdmit: "قبول", + industryReject: "رفض", + industryDissolveTitle: "حل المصنع", + industryDissolveVote: "التصويت للحل", + industryPolicy_open: "مفتوح", + industryPolicy_vote: "بالتصويت", + industryPolicy_invite: "بالدعوة فقط", projectsTitle: "المشاريع", projectsDescription: "أنشئ ومُوّل وتابع مشاريع يقودها المجتمع في شبكتك.", + projectSearchPlaceholder: "ابحث في المشاريع...", projectCreateProject: "إنشاء مشروع", projectCreateButton: "إنشاء مشروع", projectUpdateButton: "تحديث", @@ -2946,14 +3109,8 @@ module.exports = { projectPledgeTitle: "ادعم هذا المشروع", projectPledgePlaceholder: "المبلغ بـ ECO", projectBounties: "المكافآت", - projectBountiesInputLabel: "المكافآت (واحدة في كل سطر: العنوان|المبلغ [ECO]|الوصف)", - projectBountiesPlaceholder: "إصلاح خطأ واجهة|100|رابط المشكلة\nكتابة التوثيق|250|أمثلة الاستخدام", projectNoBounties: "لم يتم العثور على مكافآت.", projectTitle: "العنوان", - projectAddBountyTitle: "مكافأة جديدة", - projectBountyTitle: "عنوان المكافأة", - projectBountyAmount: "المبلغ (ECO)", - projectBountyDescription: "الوصف", projectMilestoneSelect: "اختيار مرحلة", projectBountyCreateButton: "إنشاء مكافأة", projectBountyStatus: "حالة المكافأة", @@ -2968,19 +3125,15 @@ module.exports = { projectMilestoneTitle: "عنوان المرحلة", projectMilestoneTargetPercent: "النسبة المئوية (%)", projectMilestoneDueDate: "التاريخ", - projectMilestoneCreateButton: "إنشاء مرحلة", projectMilestoneStatus: "حالة المرحلة", projectMilestoneOpen: "مفتوحة", projectMilestoneDone: "مكتملة", projectMilestoneDue: "مستحقة", - projectNoMilestones: "لم يتم العثور على مراحل.", projectMilestoneMarkDone: "تعيين كمنجزة", projectMilestoneTitlePlaceholder: "أدخل عنوان المرحلة", projectMilestoneDescriptionPlaceholder: "أدخل وصف هذه المرحلة", projectMilestoneDescription: "وصف المرحلة", - projectBudgetGoal: "الميزانية (الهدف)", projectBudgetAssigned: "المخصص للمكافآت", - projectBudgetRemaining: "المتبقي", projectBudgetOver: "تجاوز الميزانية: المخصص يتجاوز الهدف", projectFollowers: "المتابعون", projectFollowersTitle: "المتابعون", @@ -2993,7 +3146,6 @@ module.exports = { projectBackersTotalPledged: "إجمالي التبرعات", projectBackersYourPledge: "تبرعك", projectBackersNone: "لا توجد تبرعات بعد.", - projectNoRemainingBudget: "لا توجد ميزانية متبقية.", projectFilterBackers: "الداعمون", projectFilterApplied: "مُتقدَّم", projectAppliedTitle: "مُتقدَّم", @@ -3002,36 +3154,19 @@ module.exports = { projectBackerAmount: "إجمالي المساهمة", projectBackerPledges: "التبرعات", projectBackerProjects: "المشاريع", - projectPledgeAmount: "المبلغ", projectSelectMilestoneOrBounty: "اختر مرحلة أو مكافأة", projectPledgeButton: "تبرّع", footerLicense: "GPLv3", - footerPackage: "الحزمة", - footerVersion: "الإصدار", modulesModuleName: "الاسم", modulesModuleDescription: "الوصف", modulesModuleStatus: "الحالة", - modulesTotalModulesLabel: "الوحدات المُحمَّلة", + modulesTotalModulesLabel: "الإجمالي", modulesEnabledModulesLabel: "مُفعَّلة", modulesDisabledModulesLabel: "مُعطَّلة", - modulesPopularLabel: "أبرز المنشورات", - modulesPopularDescription: "وحدة لاستقبال المنشورات الرائجة أو الأكثر مشاهدة أو الأكثر تعليقًا.", - modulesTopicsLabel: "المواضيع", - modulesTopicsDescription: "وحدة لاستقبال فئات النقاش القائمة على الاهتمامات المشتركة.", - modulesSummariesLabel: "الملخصات", - modulesSummariesDescription: "وحدة لاستقبال ملخصات النقاشات أو المنشورات الطويلة.", - modulesLatestLabel: "الأحدث", - modulesLatestDescription: "وحدة لاستقبال أحدث المنشورات والنقاشات.", - modulesThreadsLabel: "المحادثات", - modulesThreadsDescription: "وحدة لاستقبال المحادثات المجمعة حسب الموضوع أو السؤال.", - modulesMultiverseLabel: "الكون المتعدد", - modulesMultiverseDescription: "وحدة لاستقبال المحتوى من الأقران المتحدين الآخرين.", modulesInvitesLabel: "الدعوات", modulesInvitesDescription: "وحدة لإدارة وتطبيق رموز الدعوة.", modulesWalletLabel: "المحفظة", modulesWalletDescription: "وحدة لإدارة أصولك الرقمية (ECOin).", - modulesLegacyLabel: "القديم", - modulesLegacyDescription: "وحدة لإدارة مفتاحك السري (المفتاح الخاص) بسرعة وأمان.", modulesCipherLabel: "التشفير", modulesCipherDescription: "وحدة لتشفير وفك تشفير نصك بشكل متماثل (باستخدام كلمة مرور مشتركة).", modulesBookmarksLabel: "العلامات المرجعية", @@ -3066,8 +3201,12 @@ module.exports = { modulesOpinionsDescription: "وحدة لاكتشاف والتصويت على الآراء.", modulesTransfersLabel: "التحويلات", modulesTransfersDescription: "وحدة لاكتشاف وإدارة العقود الذكية (التحويلات).", - modulesFediverseLabel: "فيديفيرس", - modulesFediverseDescription: "أدر حساباتك الأخرى في الفيديفيرس، بما في ذلك إرسال واستقبال المحتوى.", + modulesFediverseLabel: "الكون المتعدد", + modulesBlogsLabel: "المدونات", + modulesBlogsDescription: "وحدة لاكتشاف المدونات وإدارتها.", + modulesPollsLabel: "الاستطلاعات", + modulesPollsDescription: "وحدة لسؤال الشبكة وعدّ الإجابات.", + modulesFediverseDescription: "أدر حساباتك الأخرى في الكون المتعدد، بما في ذلك إرسال واستقبال المحتوى.", modulesFeedLabel: "التغذية", modulesFeedDescription: "وحدة لاكتشاف ومشاركة النصوص القصيرة (التغذيات).", modulesParliamentLabel: "البرلمان", @@ -3089,51 +3228,50 @@ module.exports = { modulesProjectsLabel: "المشاريع", modulesProjectsDescription: "وحدة لاستكشاف وتمويل جماعي وإدارة المشاريع.", modulesBankingLabel: "المصرفية", - modulesBankingDescription: "وحدة لتحديد القيمة الحقيقية لـ ECOIN وتوزيع الدخل الأساسي الشامل باستخدام الخزينة المشتركة.", + modulesBankingDescription: "وحدة لإدارة الخزينة المشتركة: قيمة ECOIN والضرائب والدخل الأساسي الشامل.", modulesFavoritesLabel: "المفضلة", modulesFavoritesDescription: "وحدة لإدارة المحتوى المفضل لديك.", fileTooLargeTitle: "الملف كبير جدًا", fileTooLargeMessage: "يتجاوز الملف الحجم الأقصى المسموح به (50 ميغابايت). يرجى اختيار ملف أصغر.", goBack: "رجوع", + errorDismiss: "حسنًا", errorPageTitle: "حدث خطأ", + errorNoAllocation: "لم يتم العثور على مخصصات لهذه المطالبة.", + errorInvalidClaim: "لا يمكن المطالبة بهذه المخصصات.", visibilityLabel: "الرؤية", + clearnetPrivateConflict: "لا يمكن نشر محتوى خاص أو مخفي أو مغلق أو بدعوة فقط أو مدفوع على CLEARNET. اختر OASIS أو اجعله عامًا.", visibilityPublic: "عام", visibilityHidden: "مخفي", visibilityMakePublic: "جعله عامًا", visibilityMakeHidden: "إخفاؤه", invitesInhabitantsTitle: "السكان", - invitesInhabitantsFollow: "متابعة", - aiNavPlaceholder: "إلى أين تريد الذهاب؟", + aiNavPlaceholder: "صف ما تحتاجه...", aiNavDisabled: "تعطيل التنقل بالذكاء الاصطناعي.", - aiNavResultsTitle: "اقتراحات التنقل بالذكاء الاصطناعي", + aiNavResultsTitle: "اقتراحات AINav", aiNavQueryLabel: "الاستفسار", - aiNavResultsDescription: "اختر المسار الأنسب لما تبحث عنه.", - aiNavResultPath: "المسار", - aiNavResultDescription: "ما يمكنك فعله", aiNavResultMatch: "التطابق", aiNavResultsEmpty: "لا توجد مسارات مطابقة. جرّب /search.", - aiNavSearchInstead: "ابحث بدلًا من ذلك", modulesAINavLabel: "AINav", modulesAINavDescription: "وحدة لإجراء استعلامات بلغة طبيعية حول محتوى الشبكة.", - directConnect: "اتصال مباشر", directConnectDescription: "اتصل مباشرة بقرين عن طريق إدخال عنوان IP والمنفذ والمفتاح العام. سيتم إضافة القرين كاتصال مُتابَع.", peerHost: "IP", - peerPort: "المنفذ (افتراضي: 8008)", + peerPort: "المنفذ", peerPublicKey: "المفتاح العام (@...ed25519)", connectAndFollow: "اتصال", - deviceSourceLabel: "مصدر الجهاز", modulesPresetTitle: "الإعدادات الشائعة", - modulesPreset_minimal: "الحد الأدنى", - modulesPreset_basic: "أساسي", - modulesPreset_social: "اجتماعي", - modulesPreset_economy: "اقتصادي", - modulesPreset_full: "كامل", + workflowsTitle: "مسارات العمل", + workflowsDescription: "يحدد مسار العمل السمة والوحدات المفعّلة.", + workflowsSet: "تطبيق مسار العمل", + workflow_default: "الافتراضي", + workflow_jobs: "العمل", + workflow_social: "اجتماعي", + workflow_business: "الأعمال", + workflow_night: "ليل", + workflow_kids: "الأطفال", + workflow_activists: "الناشطون", + workflow_mobile: "الهاتف", statsCarbonFootprintTitle: "البصمة الكربونية", - statsCarbonFootprintNetwork: "البصمة الكربونية للشبكة", - statsCarbonFootprintYours: "بصمتك الكربونية", statsCarbonTombstone: "بصمة الحذف", - feedSuccessMsg: "تم نشر التغذية بنجاح!", - dominantOpinionLabel: "الرأي السائد", uploadMedia: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", feedOpenDiscussion: "فتح النقاش", feedDetailTitle: "التغذية", @@ -3142,7 +3280,7 @@ module.exports = { noComments: "لا توجد تعليقات بعد", mapsLabel: "خرائط", mapTitle: "خرائط", - mapDescription: "استكشف وأدر الخرائط غير المتصلة في شبكتك.", + mapDescription: "أدر الخرائط غير المتصلة وشاركها في شبكتك.", mapMineSectionTitle: "خرائطك", mapCreateSectionTitle: "إنشاء خريطة", mapUpdateSectionTitle: "تحديث الخريطة", @@ -3166,14 +3304,13 @@ module.exports = { mapDescriptionLabel: "الوصف", mapDescriptionPlaceholder: "صف الخريطة أو الموقع...", mapTypeLabel: "نوع الخريطة", - mapTypeSingle: "فردي (الموقع الأولي فقط)", - mapTypeOpen: "مفتوح (يمكن لأي شخص إضافة علامات)", - mapTypeClosed: "مغلق (المنشئ فقط يضيف علامات)", + mapInviteMode: "دعوة", + mapTypeSingle: "فردي", + mapTypeOpen: "مفتوح", + mapTypeClosed: "مغلق", mapTagsLabel: "الوسوم", mapTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", mapUrlLabel: "رابط الخريطة", - mapPickCoordLabel: "أو اختر موقعاً على الشبكة:", - mapMarkersLabel: "علامات", mapMarkersTitle: "العلامات", mapMarkerDefault: "علامة", mapMarkerLatLabel: "خط عرض العلامة", @@ -3183,9 +3320,22 @@ module.exports = { markerImageLabel: "صورة العلامة", mapAddMarkerTitle: "إضافة علامة", mapAddMarkerButton: "إضافة علامة", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "مسح العلامة", mapApplyZoom: "تطبيق التكبير", - mapSearchPlaceholder: "البحث في الوصف، الوسوم، المؤلف...", + mapPlaceSearchPlaceholder: "ابحث عن مكان أو بلد...", + mapNoPlaces: "لم يتم العثور على أماكن.", + mapClusterTitle: "علامات هنا، انقر للتكبير", + mapDeleteMarker: "حذف العلامة", + mapZoomIn: "تكبير", + mapZoomOut: "تصغير", + mapFitMarkers: "ملاءمة كل العلامات", + mapPanLabel: "تحريك الخريطة", + mapCenterHere: "توسيط هنا", + mapNear: "بالقرب من", + mapPickedSpot: "النقطة المختارة", + mapPickHint: "انقر على الخريطة لاختيار نقطة.", + mapClearPick: "مسح", + mapSearchPlaceholder: "ابحث في خرائط...", mapSearchButton: "بحث", mapUpdatedAt: "محدّث", mapNoMatch: "لا توجد خرائط مطابقة لبحثك.", @@ -3196,118 +3346,820 @@ module.exports = { typeMapMarker: "علامة خريطة", modulesMapLabel: "خرائط", modulesMapDescription: "وحدة لإدارة ومشاركة الخرائط غير المتصلة.", - padsTitle: "الوسادات", - padTitle: "وسادة", - modulesPadsLabel: "الوسادات", + emergenciesTitle: "الطوارئ", + modulesEmergenciesLabel: "الطوارئ", + modulesEmergenciesDescription: "وحدة لإطلاق حالات الطوارئ وتأكيدها ومتابعتها في الشبكة.", + emergenciesDescription: "وحدة لإطلاق حالات الطوارئ وتأكيدها ومتابعتها في الشبكة.", + emergencyCreate: "إنشاء حالة طوارئ", + emergencyUpdate: "تحديث", + emergencyDelete: "حذف", + emergencyTitleLabel: "العنوان", + emergencyTitlePlaceholder: "ماذا يحدث؟", + emergencyTextLabel: "التفاصيل", + emergencyTextPlaceholder: "أين، منذ متى، ماذا نفعل...", + emergencyCategoryLabel: "الفئة", + emergencyCategoryWeather: "الطقس", + emergencyCategoryInfrastructure: "البنية التحتية", + emergencyCategoryHealth: "الصحة", + emergencyCategorySecurity: "الأمن", + emergencyCategoryLost: "المفقودات", + emergencyCategoryNeighborhood: "الحي", + emergencyExpiresLabel: "ينتهي خلال", + emergencyExpires1d: "يوم واحد", + emergencyExpires3d: "3 أيام", + emergencyExpires7d: "أسبوع واحد", + emergencyExpires30d: "شهر واحد", + emergencyExpiresNone: "غير محدد", + emergencyExpiresKeep: "الاحتفاظ بالتاريخ الحالي", + emergencyTagsLabel: "الوسوم", + emergencyTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + emergencyFilterAll: "الكل", + emergencyFilterMine: "تنبيهاتي", + emergencyFilterRecent: "الأحدث", + emergencySeverityUnverified: "غير مؤكد", + emergencySeverityLow: "منخفضة", + emergencySeverityMedium: "متوسطة", + emergencySeverityHigh: "عالية", + emergencyStatusActive: "نشط", + emergencyStatusResolved: "تم الحل", + emergencyStatusExpired: "منتهٍ", + emergencyConfirmations: "التأكيدات", + emergencyConfirmButton: "تأكيد", + emergencyConfirmed: "لقد أكدت حالة الطوارئ هذه", + emergencyResolveButton: "حل", + emergencyReopenButton: "إعادة فتح", + emergencyGenerateReport: "إنشاء التقرير", + emergencyUpdateEdited: "مُعدَّل", + emergencyLastUpdate: "آخر تحديث", + emergencyUpdatesTitle: "آخر التحديثات:", + emergencyNoUpdates: "لا تحديثات بعد.", + emergencyAddUpdate: "نشر تحديث", + emergencyUpdatePlaceholder: "ما الذي تغيّر؟", + emergencyNoItems: "لم يتم العثور على حالات طوارئ.", + emergencyExpiresAt: "ينتهي", + emergencyBannerLabel: "حالة طوارئ نشطة", + emergencyCreateSectionTitle: "إنشاء حالة طوارئ", + emergencyUpdateSectionTitle: "تحديث حالة الطوارئ", + emergencySearchPlaceholder: "البحث في الطوارئ...", + typeEmergency: "الطوارئ", + typeEmergencyConfirm: "تأكيد حالة طوارئ", + typeEmergencyUpdate: "تحديث حالة طوارئ", + statsEmergency: "الطوارئ", + statsEmergencyConfirm: "تأكيدات الطوارئ", + statsEmergencyUpdate: "تحديثات الطوارئ", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "حالة طوارئ تتابعها لديها تحديث.", + emergencyBotResolvedTitle: "تم حل حالة طوارئ تتابعها.", + dataKindEmergencies: "الطوارئ", + mailingTitle: "القوائم البريدية", + modulesMailingLabel: "القوائم البريدية", + modulesMailingDescription: "اكتشف وأدر القوائم البريدية في شبكتك.", + modulesPhoneLabel: "الهاتف", + modulesPhoneDescription: "وحدة للاتصال بالسكان الآخرين عبر مكالمات فيديو مشفّرة من طرف إلى طرف.", + mailingDescription: "اكتشف وأدر القوائم البريدية في شبكتك.", + mailingCreate: "إنشاء قائمة بريدية", + mailingUpdate: "تحديث", + mailingArchive: "أرشفة", + mailingActivate: "تفعيل", + mailingCreateSectionTitle: "إنشاء قائمة بريدية", + mailingUpdateSectionTitle: "تحديث القائمة البريدية", + mailingTitleLabel: "العنوان", + mailingTitlePlaceholder: "اسم القائمة البريدية", + mailingDescriptionLabel: "الوصف", + mailingDescriptionPlaceholder: "ما موضوع هذه القائمة؟", + mailingTypeLabel: "نوع القائمة البريدية", + mailingTypeOpen: "مفتوحة", + mailingTypeClosed: "مغلقة", + mailingStatusLabel: "الحالة", + mailingStatusActive: "نشطة", + mailingStatusArchived: "مؤرشفة", + mailingMembersLabel: "دعوة السكان", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "المشتركون", + mailingTagsLabel: "الوسوم", + mailingTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + mailingFilterAll: "الكل", + mailingFilterMine: "قوائمي", + mailingFilterSubscribed: "مشترك بها", + mailingFilterRecent: "الأحدث", + mailingHistoryDate: "التاريخ", + mailingHistoryThreads: "المواضيع", + mailingHistoryInhabitants: "السكان", + mailingHistorySearchPlaceholder: "البحث في السجل...", + mailingNoMessages: "لا توجد رسائل بعد.", + mailingNoItems: "لم يتم العثور على قوائم بريدية.", + mailingSearchPlaceholder: "البحث في القوائم البريدية...", + mailingWriteButton: "كتابة رسالة", + mailingWriteTitle: "الكتابة إلى القائمة", + mailingReplyTitle: "الرد على القائمة", + mailingSend: "إرسال", + mailingTextPlaceholder: "تصل رسالتك إلى جميع المشاركين في القائمة.", + mailingParticipants: "المشاركون", + inboxFilterMailing: "القوائم البريدية", + mailingReplyPlaceholder: "الرد على القائمة...", + mailingReplyToList: "الرد على القائمة", + typeMailingList: "قائمة بريدية", + statsMailingList: "القوائم البريدية", + mailingListLabel: "قائمة بريدية", + logisticsTitle: "اللوجستيات", + modulesLogisticsLabel: "اللوجستيات", + modulesLogisticsDescription: "اكتشف وأدر الخطط اللوجستية في شبكتك.", + logisticsDescription: "اكتشف وأدر الخطط اللوجستية في شبكتك.", + logisticsCreate: "إنشاء مسار", + logisticsUpdate: "تحديث", + logisticsClose: "إغلاق", + logisticsReopen: "إعادة فتح", + logisticsCreateSectionTitle: "إنشاء مسار", + logisticsUpdateSectionTitle: "تحديث المسار", + logisticsTitleLabel: "العنوان", + logisticsTitlePlaceholder: "رحلة إلى السوق، شاحنة متاحة...", + logisticsKindLabel: "نوع اللوجستيات", + logisticsKindTrip: "رحلة", + logisticsKindShipment: "شحنة", + logisticsKindTrips: "الرحلات", + logisticsKindShipments: "الشحنات", + logisticsModeLabel: "الوضع", + logisticsModeOffer: "أعرض", + logisticsModeRequest: "أبحث", + logisticsModeOffers: "العروض", + logisticsModeRequests: "الطلبات", + logisticsDescriptionLabel: "الوصف", + logisticsDescriptionPlaceholder: "نقطة اللقاء، ما تحمله، الشروط...", + logisticsOriginLabel: "نقطة الانطلاق", + logisticsOriginPlaceholder: "من أين يبدأ", + logisticsDestinationLabel: "الوجهة", + logisticsDestinationPlaceholder: "أين ينتهي", + logisticsDateLabel: "التاريخ والوقت", + logisticsRecurrenceLabel: "التكرار", + logisticsFrequencyLabel: "التكرار", + logisticsRecurrenceNone: "مرة واحدة", + logisticsRecurrenceDaily: "يومي", + logisticsRecurrenceWeekly: "أسبوعي", + logisticsRecurrenceMonthly: "شهري", + logisticsSeatsLabel: "المقاعد", + logisticsSizeLabel: "الحجم", + logisticsSizePlaceholder: "مثلاً 2m x 1m", + logisticsWeightLabel: "الوزن", + logisticsWeightPlaceholder: "مثلاً 40kg", + logisticsPriceFree: "مجاني", + logisticsPriceEco: "ECO", + logisticsPriceTime: "وقت", + logisticsPriceLabel: "السعر", + logisticsHours: "س", + logisticsOrderRefLabel: "رابط الطلب", + logisticsOrderRefPlaceholder: "/shops/... أو /market/... للطلب المراد تسليمه", + logisticsOrderLink: "الطلب المرتبط", + logisticsTagsLabel: "الوسوم", + logisticsTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + logisticsFilterAll: "الكل", + logisticsFilterMine: "مساراتي", + logisticsFilterBooked: "المحجوزة", + logisticsFilterRecent: "الأحدث", + logisticsFilterHistory: "السجل", + logisticsFilterTop: "الأفضل", + logisticsStatusOpen: "مفتوح", + logisticsStatusUpcoming: "القادمة", + logisticsStatusPast: "السابقة", + logisticsStatusClosed: "المغلقة", + logisticsZoneLabel: "المنطقة", + logisticsSearchPlaceholder: "البحث في اللوجستيات...", + logisticsNoItems: "لم يتم العثور على مسارات.", + logisticsBookingPending: "قيد الانتظار", + logisticsBookingConfirmed: "مؤكد", + logisticsBookingRejected: "مرفوض", + logisticsBookingCancelled: "ملغى", + logisticsBookingDelivered: "تم التسليم", + logisticsConfirm: "تأكيد", + logisticsReject: "رفض", + logisticsDeliver: "تسليم", + logisticsReceiptLabel: "إيصال", + logisticsReceiptPlaceholder: "ملاحظة التسليم (اختياري)", + logisticsCancelBooking: "إلغاء الحجز", + logisticsBookTitle: "احجز هذا المسار", + logisticsNotesLabel: "ملاحظات", + logisticsNotesPlaceholder: "ما يجب أن يعرفه المسؤول", + logisticsBook: "حجز", + logisticsBookingsTitle: "الحجوزات", + logisticsMyBookingTitle: "حجزك", + logisticsRatingsTitle: "التقييمات", + logisticsNoRatings: "لا توجد تقييمات بعد.", + logisticsRatingsWhenClosed: "تُفتح التقييمات بعد إغلاق المسار.", + logisticsScoreLabel: "الدرجة", + logisticsRatingPlaceholder: "كيف سارت الأمور؟", + logisticsRate: "تقييم", + typeLogisticsRoute: "مسار", + typeLogisticsRating: "تقييم مسار", + statsLogisticsRoute: "المسارات", + statsLogisticsRating: "تقييمات المسارات", + logisticsRouteLabel: "مسار", + podcastsTitle: "البودكاست", + modulesPodcastsLabel: "البودكاست", + modulesPodcastsDescription: "وحدة لنشر قنوات البودكاست.", + podcastsDescription: "وحدة لنشر قنوات البودكاست.", + podcastCreate: "إنشاء بودكاست", + podcastUpdate: "تحديث", + podcastDelete: "حذف", + podcastCreateSectionTitle: "إنشاء بودكاست", + podcastUpdateSectionTitle: "تحديث البودكاست", + podcastTitleLabel: "العنوان", + podcastTitlePlaceholder: "اسم القناة", + podcastDescriptionLabel: "الوصف", + podcastDescriptionPlaceholder: "ما موضوع هذا البودكاست؟", + podcastCategoryLabel: "الفئة", + podcastCategoryNews: "أخبار", + podcastCategoryMusic: "موسيقى", + podcastCategoryTalk: "حوار", + podcastCategoryEducation: "تعليم", + podcastCategoryTech: "تقنية", + podcastCategoryCulture: "ثقافة", + podcastCategoryCommunity: "مجتمع", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "أخرى", + podcastTagsLabel: "الوسوم", + podcastCoverLabel: "الغلاف (صورة أو فيديو)", + podcastEpisodeAudioLabel: "ملف صوتي أو فيديو (الحد الأقصى: 75MB)", + podcastTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + podcastFilterAll: "الكل", + podcastFilterMine: "قنواتي", + podcastFilterRecent: "الأحدث", + podcastFilterTop: "الأفضل", + podcastFilterViewers: "المستمعون", + podcastSearchPlaceholder: "البحث في البودكاست...", + podcastNoItems: "لم يتم العثور على بودكاست.", + podcastEpisodesLabel: "الحلقات", + podcastPlaysLabel: "المشاهدات", + podcastLastEpisode: "آخر حلقة", + podcastEpisodesTitle: "الحلقات", + podcastNoEpisodes: "لا توجد حلقات بعد.", + podcastEpisodeAdd: "إضافة حلقة", + podcastEpisodeCreateTitle: "نشر حلقة", + podcastEpisodeUpdateTitle: "تحديث الحلقة", + podcastEpisodeTitleLabel: "عنوان الحلقة", + podcastEpisodeTitlePlaceholder: "عنوان الحلقة", + podcastEpisodeDescriptionLabel: "ملاحظات", + podcastEpisodeDescriptionPlaceholder: "ما يحدث في هذه الحلقة، روابط، شكر...", + podcastEpisodePublish: "نشر", + podcastPlayed: "تم الاستماع", + podcastMarkPlayed: "وضع علامة كمستمَع", + typePodcast: "بودكاست", + typePodcastEpisode: "حلقة", + typePodcastOpinion: "رأي في الحلقة", + typePodcastPlay: "تشغيل الحلقة", + statsPodcast: "البودكاست", + statsPodcastEpisode: "الحلقات", + statsPodcastPlay: "مرات تشغيل الحلقات", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "مدونة تتابعها فيها مقال جديد.", + podcastBotEpisodeTitle: "بودكاست تتابعه لديه حلقة جديدة.", + podcastLabel: "بودكاست", + podcastEpisodeLabel: "حلقة", + campaignsTitle: "الحملات", + modulesCampaignsLabel: "الحملات", + modulesCampaignsDescription: "اكتشف وأدر الحملات في شبكتك.", + campaignsDescription: "اكتشف وأدر الحملات في شبكتك.", + campaignCreate: "إنشاء حملة", + campaignUpdate: "تحديث", + campaignDelete: "حذف", + campaignClose: "إغلاق", + campaignReopen: "إعادة فتح", + campaignCreateSectionTitle: "إنشاء حملة", + campaignUpdateSectionTitle: "تحديث الحملة", + campaignTitleLabel: "العنوان", + campaignTitlePlaceholder: "ماذا تريد أن تحقق؟", + campaignTextLabel: "النص", + campaignTextPlaceholder: "اشرح القضية، من يقرر وماذا تطلب...", + campaignCategoryLabel: "الفئة", + campaignCategoryEnvironment: "البيئة", + campaignCategoryRights: "الحقوق", + campaignCategoryHealth: "الصحة", + campaignCategoryEducation: "التعليم", + campaignCategoryInfrastructure: "البنية التحتية", + campaignCategoryCulture: "الثقافة", + campaignCategoryEconomy: "الاقتصاد", + campaignCategoryOther: "أخرى", + campaignGoalLabel: "هدف التوقيعات", + campaignDeadlineLabel: "الموعد النهائي", + campaignTagsLabel: "الوسوم", + campaignTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + campaignFilterAll: "الكل", + campaignFilterMine: "حملاتي", + campaignFilterSigned: "الموقّعة", + campaignFilterRecent: "الأحدث", + campaignFilterTop: "الأفضل", + campaignStatusOpen: "مفتوحة", + campaignStatusAchieved: "محققة", + campaignStatusUnachieved: "غير محقَّقة", + campaignStatusClosed: "مغلقة", + campaignSearchPlaceholder: "البحث في الحملات...", + campaignNoItems: "لم يتم العثور على حملات.", + campaignSignaturesLabel: "التوقيعات", + campaignSignaturesTitle: "الداعمون", + campaignNoSignatures: "لا توجد توقيعات بعد.", + campaignSignTitle: "وقّع هذه الحملة", + campaignSignPlaceholder: "لماذا تدعمها؟ (اختياري)", + campaignSign: "توقيع", + campaignSigned: "لقد وقّعت بالفعل!", + campaignNotSigned: "ما زال توقيعك ناقصًا", + campaignElevate: "رفعها إلى البرلمان", + campaignElevated: "رُفعت إلى البرلمان", + campaignUpdatesTitle: "التحديثات", + campaignNoUpdates: "لا توجد تحديثات بعد.", + campaignUpdatePlaceholder: "ماذا حدث؟", + campaignAddUpdate: "نشر تحديث", + campaignQrAlt: "رمز QR لتوقيع هذه الحملة", + campaignQrCaption: "امسح للتوقيع", + typeCampaign: "حملة", + typeCampaignSignature: "توقيع", + typeCampaignUpdate: "تحديث حملة", + statsCampaign: "الحملات", + statsCampaignSignature: "توقيعات الحملات", + statsCampaignUpdate: "تحديثات الحملات", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "حملة تتابعها لديها تحديث.", + campaignBotAchievedTitle: "حملة تتابعها حققت هدفها.", + campaignBotRaisedTitle: "حملة وقّعت عليها رُفعت إلى البرلمان.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "تغيّر مسار حجزته.", + logisticsBotClosedTitle: "أُغلق مسار حجزته.", + logisticsBotBookedTitle: "حجز شخص ما مسارك.", + logisticsBotCancelledTitle: "أُلغي حجز على مسارك.", + logisticsBotConfirmedTitle: "تأكد حجزك: الطرد في الطريق.", + logisticsBotRejectedTitle: "رُفض حجزك.", + logisticsBotDeliveredTitle: "وصل طردك.", + campaignLabel: "حملة", + campaignGenerateReport: "إنشاء التقرير", + logisticsGenerateReport: "إنشاء التقرير", + mailingGenerateReport: "إنشاء التقرير", + campaignUpdateEdited: "مُعدَّل", + mailingMessagesLabel: "الرسائل", + logisticsBookingsLabel: "الحجوزات", + backupTitle: "النسخ الاحتياطي", + modulesBackupLabel: "النسخ الاحتياطي", + modulesBackupDescription: "وحدة لتصدير مفاتيحك واستيرادها وإنشاء نسخ احتياطية مشفّرة.", + backupDescription: "وحدة لتصدير مفاتيحك واستيرادها وإنشاء نسخ احتياطية مشفّرة.", + backupTypeRecovery: "الاستعادة", + backupTypeKeys: "تصدير المفاتيح", + backupTypeFull: "نسخة كاملة", + backupTypeRestore: "استعادة النسخة", + backupTypeInstant: "فورية", + backupInstantDescription: "ملف .zip عادي يضم سجلك وملفاتك كاملة، بلا مفتاحك السري وبلا كلمة سر. سريع، لكن ", + backupInstantRisk: "من يحصل على الملف يقرأ كل ما فيه.", + backupKitPageDescription: "لا يمكن فتح حزمة الاستعادة إلا من هذا الجهاز.", + backupKitId: "معرّف Oasis", + backupKitDate: "تاريخ الإنشاء", + backupKitQrAlt: "رمز QR لمفتاحك السري", + backupKitStep1: "اطبعه أو انسخه يدوياً واحتفظ به في مكان آمن ودون اتصال.", + backupKitStep2: "للاستعادة، ثبّت Oasis على جهاز جديد واستورد هذا السر من النسخ الاحتياطي › المفاتيح.", + backupKitStep3: "أتلف هذه الصفحة إذا تعرّضت للكشف.", + backupKitPdfButton: "إنشاء PDF", + backupScopeLabel: "النطاق", + backupScopeAll: "كل شيء", + backupScopeMine: "محتواي فقط", + backupSinceLabel: "منذ", + backupEstimateTitle: "التقدير", + backupFileInfo: "سيتم تنزيل نسختك المشفّرة إلى جهازك (الاسم: oasis-backup-التاريخ.oasisbk)", + backupExportButton: "تنزيل النسخة", + backupRestorePassword: "اكتب كلمة المرور المستخدمة عند إنشاء النسخة", + backupRestoreButton: "استعادة النسخة", + backupRestoredLine: "تمت استعادة النسخة.", + backupRestoredMessages: "الرسائل المضافة", + backupRestoredSkipped: "موجودة مسبقاً", + backupRestoredBlobs: "الملفات المضافة", + backupRestoredState: "الإعدادات المستعادة", + backupRestoredFailed: "فشلت", + backupRestoredForked: "متباينة", + backupRestoreRunning: "جارٍ استعادة النسخة. تتحدث هذه الصفحة تلقائيًا؛ يمكنك متابعة استخدام Oasis في هذه الأثناء.", + backupRestoreElapsed: "المدة المنقضية", + backupRestoreFailedLine: "تعذّر استعادة النسخة.", + backupRestoreForkMine: "نشر هذا الجهاز بالفعل رسائل بهويتك غير موجودة في النسخة، لذا تعذّر تطبيقها فوقها. استعد النسخة على تثبيت نظيف مباشرة بعد استيراد مفاتيحك وقبل القيام بأي شيء آخر.", + verificationTitle: "التحقق", + verificationDescription: "افحص سلسلة خلاصتك، واكتشف التفرعات، وراجع الملفات اليتيمة أو المفقودة.", + verificationRun: "تشغيل التحقق", + verificationTotalMessages: "رسالة في السجل", + verificationFeed: "خلاصتك", + verificationMessages: "رسالة", + verificationLastSequence: "آخر تسلسل", + verificationGaps: "الفجوات", + verificationBrokenLinks: "روابط مكسورة", + verificationBadHashes: "تجزئات خاطئة", + verificationBadSignatures: "توقيعات خاطئة", + verificationDuplicates: "تسلسلات مكررة", + verificationForks: "التفرعات", + verificationBlobs: "الملفات", + verificationBlobsPresent: "موجودة", + verificationBlobsReferenced: "مُشار إليها", + verificationBlobsOrphan: "يتيمة", + verificationBlobsMissing: "مفقودة", + verificationBlobsMissingOwn: "ملفاتك المفقودة من هذا الجهاز", + verificationBlobsMissingOthers: "ملفات سكان آخرين لم تُنزَّل بعد", + verificationBlobsUnavailable: "تعذّر سرد الملفات على هذا الجهاز.", + wikiTitle: "ويكي", + modulesWikiLabel: "ويكي", + modulesWikiDescription: "وحدة لاكتشاف وإدارة الويكي.", + wikiDescription: "وحدة لاكتشاف وإدارة الويكي.", + wikiCreate: "إنشاء ويكي", + wikiUpdate: "تحديث", + wikiDelete: "حذف", + wikiEdit: "تعديل", + wikiTitleLabel: "العنوان", + wikiTitlePlaceholder: "عنوان الويكي...", + wikiBodyLabel: "المحتوى", + wikiBodyPlaceholder: "اربط ويكي أخرى عبر [[عنوان الويكي]] أو [[عنوان الويكي|نص مخصص]].\n\nاذكر ساكنًا عبر @id.\n\nضع وسمًا مثل #oasis.\n\nألصق روابط مثل https://solarnethub.com\n\nالسطر الفارغ يبدأ فقرة جديدة؛ وتُحفظ فواصل الأسطر المفردة.\n\nأرفق أي وسائط مدعومة وسيتم دمجها هنا.", + formatBarLabel: "التنسيق", + wikiStatusLabel: "نوع الويكي", + wikiStatusOpen: "مفتوحة", + wikiStatusClosed: "مغلقة", + wikiTagsLabel: "الوسوم", + wikiTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + wikiSummaryLabel: "ملخص التغيير", + wikiSummaryPlaceholder: "ما الذي تغيّر؟", + wikiFilterAll: "الكل", + wikiFilterMine: "صفحاتي", + wikiFilterRecent: "الأحدث", + wikiFilterLinked: "المرتبطة", + wikiChanges: "آخر التغييرات", + wikiNoPages: "لم يتم العثور على ويكي.", + wikiNoChanges: "لا تغييرات بعد.", + wikiHistory: "السجل", + wikiVersion: "الإصدار", + wikiVersions: "إصدارات", + wikiRestore: "استعادة", + wikiDiff: "التغييرات", + wikiViewVersion: "عرض", + wikiLinkedPages: "الصفحات المرتبطة", + wikiNoLinkedPages: "هذه الصفحة غير مرتبطة بأي صفحة أخرى بعد.", + wikiMissingLink: "الصفحة غير موجودة بعد", + wikiDuplicateTitle: "توجد صفحة بهذا العنوان بالفعل. غيّر العنوان لإنشاء صفحة مختلفة.", + wikiCreateSectionTitle: "إنشاء ويكي", + wikiUpdateSectionTitle: "تحديث الويكي", + wikiSearchPlaceholder: "البحث في الويكي...", + wikiCurrentVersion: "الإصدار الحالي", + wikiViewingVersion: "أنت تعرض إصدارًا قديمًا", + wikiBackToPage: "العودة إلى الصفحة", + wikiLinkedChip: "مرتبطة", + wikiRestoredSummary: "إصدار مستعاد", + wikiExportPdf: "إنشاء الصفحة", + wikiPreview: "معاينة", + tribeSectionWiki: "ويكي", + tribeWikiCreate: "إنشاء ويكي", + tribeWikiEmpty: "لا ويكي بعد.", + typeWiki: "ويكي", + padsTitle: "المستندات التعاونية", + padTitle: "مستند تعاوني", + modulesPadsLabel: "المستندات التعاونية", modulesPadsDescription: "وحدة لإدارة محررات النصوص التعاونية.", - padFilterAll: "الكل", - padFilterMine: "خاصتي", - padFilterRecent: "الأخيرة", - padFilterOpen: "مفتوح", - padFilterClosed: "مغلق", - padCreate: "إنشاء وسادة", - padUpdate: "تحديث الوسادة", - padDelete: "حذف الوسادة", + padCreate: "إنشاء مستند تعاوني", + padUpdate: "تحديث", + padDelete: "حذف", padTitleLabel: "العنوان", - padTitlePlaceholder: "أدخل عنوان الوسادة...", + padTitlePlaceholder: "أدخل عنوان المستند التعاوني...", padStatusLabel: "الحالة", + padTypeLabel: "نوع اللوح", padStatusOpen: "مفتوح", padStatusInviteOnly: "بدعوة فقط", padStatusClosed: "مغلق", padDeadlineLabel: "الموعد النهائي", padTagsLabel: "الوسوم", - padTagsPlaceholder: "وسم1، وسم2، ...", + padTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", padMembersLabel: "الأعضاء", - padVisitPad: "زيارة الوسادة", - padShareUrl: "مشاركة الرابط", padCreated: "تاريخ الإنشاء", - padAuthor: "المؤلف", - padGenerateCode: "إنشاء رمز", - padInviteCodeLabel: "رمز الدعوة", - padInviteCodePlaceholder: "أدخل رمز الدعوة...", - padValidateInvite: "تحقق", - padStartEditing: "ابدأ التحرير!", + padStartEditing: "تحرير", padEditorPlaceholder: "ابدأ الكتابة...", padSubmitEntry: "إرسال", padNoEntries: "لا توجد مدخلات بعد.", - padAllSectionTitle: "جميع الوسادات", - padMineSectionTitle: "وساداتي", + padAllSectionTitle: "جميع المستندات التعاونية", + padMineSectionTitle: "مستنداتي التعاونية", padsDescription: "إدارة محرّرات النصوص التعاونية المشفّرة في شبكتك.", - padRecentSectionTitle: "الوسادات الأخيرة", - padOpenSectionTitle: "الوسادات المفتوحة", - padClosedSectionTitle: "الوسادات المغلقة", - padCreateSectionTitle: "إنشاء وسادة جديدة", - padUpdateSectionTitle: "تحديث الوسادة", - padInviteGenerated: "تم إنشاء رمز الدعوة", - typePad: "وسادات", + roomsTitle: "الغرف", + roomsDescription: "اكتشف غرف مؤتمرات فيديو مشفّرة في شبكتك وانضم إليها.", + modulesRoomsLabel: "الغرف", + modulesRoomsDescription: "وحدة لاكتشاف غرف مؤتمرات الفيديو المشفّرة والانضمام إليها.", + roomFilterRecent: "الأخيرة", + roomFilterMine: "غرفي", + roomFilterAll: "الكل", + roomFilterLive: "مباشر", + roomFilterOpen: "مفتوحة", + roomFilterInvite: "بدعوة فقط", + roomFilterClosed: "مغلقة", + roomCreate: "إنشاء غرفة", + roomUpdate: "تحديث", + roomDelete: "حذف", + roomClose: "إغلاق", + roomReopen: "إعادة فتح", + roomCreateSectionTitle: "إنشاء غرفة", + roomUpdateSectionTitle: "تحديث الغرفة", + roomTitlePlaceholder: "عنوان الغرفة", + roomDescriptionLabel: "الوصف", + roomDescriptionPlaceholder: "عمّ ستتحدثون؟", + roomTypeLabel: "نوع الغرفة", + roomTagsLabel: "الوسوم", + roomStatusOpen: "مفتوحة", + roomStatusInviteOnly: "بدعوة فقط", + roomStatusClosed: "مغلقة", + roomParticipants: "المشاركون", + roomJoin: "انضمام", + roomLeave: "مغادرة", + roomFull: "الغرفة ممتلئة", + roomLiveChip: "مباشر", + roomMuted: "مكتوم", + roomRec: "تسجيل الصوت", + roomRecStop: "إيقاف التسجيل", + roomStreamFile: "بثّ", + roomStreamStop: "إيقاف", + roomStreaming: "جارٍ البث", + liveFullscreen: "ملء الشاشة", + roomStreamPickTitle: "اختر فيديو", + roomStreamPickHint: "مقاطع الفيديو على هذا الجهاز. يُبث الفيديو الذي تختاره من مكانه دون نسخه أو نشره.", + roomStreamGo: "بثّه في الغرفة", + roomStreamNotFound: "هذا الفيديو ليس في مجلداتك.", + roomPublicWarning: "تُبث هذه الغرفة على الويب العام: يمكن لأي شخص رؤية وسماع ما يحدث فيها.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} دخل الغرفة", + roomEventLeft: "{name} غادر الغرفة", + roomEventRecStart: "{name} بدأ التسجيل", + roomEventRecStop: "{name} أوقف التسجيل", + roomEventMuted: "{name} كتم صوته", + roomEventUnmuted: "{name} ألغى كتم صوته", + roomEventHand: "{name} رفع يده", + roomRaiseHand: "ارفع يدك", + roomLowerHand: "أنزل يدك", + confirmActionTitle: "أكّد هذا الإجراء", + confirmActionButton: "تأكيد", + confirmJoinText: "أنت على وشك الانضمام باستخدام هذه الدعوة.", + confirmFollowText: "أنت على وشك دعم هذا الساكن.", + confirmSignText: "أنت على وشك توقيع هذه الحملة.", + confirmDownloadText: "أنت على وشك تنزيل هذا الملف إلى جهازك.", + roomRecordings: "التسجيلات", + roomClearNotices: "مسح", + roomLineSwitchboard: "مقسم", + roomLineDndError: "رقم هذه الغرفة لا يستقبل المكالمات.", + roomNumberHint: "اطلب هذا الرقم من الهاتف للانضمام إلى الغرفة.", + liveRefresh: "تحديث", + roomNobody: "لا أحد في الغرفة الآن.", + roomSomeone: "{n} في الغرفة الآن.", + roomYou: "أنت", + roomYouAreIn: "أنت في الداخل", + roomNobodyInside: "لا يوجد أحد في هذه الغرفة الآن.", + roomErrorFull: "الغرفة ممتلئة. يمكنك الانضمام عندما يغادر أحدهم.", + roomErrorUnreachable: "لا يمكن الوصول إلى الغرفة الآن. حاول لاحقًا.", + roomErrorBusy: "أنت بالفعل في مكالمة أو في غرفة أخرى.", + roomErrorClosed: "هذه الغرفة مغلقة.", + roomAccessDenied: "هذه الغرفة بدعوة فقط. تحتاج إلى دعوة للانضمام.", + roomsNoItems: "لم يُعثر على غرف.", + roomSearchPlaceholder: "ابحث في الغرف...", + roomInviteMode: "دعوة", + tribeSectionRooms: "الغرف", + tribeRoomCreate: "إنشاء غرفة", + tribeRoomsEmpty: "لا توجد غرف بعد.", + typeRoom: "غرف", + typeRoomMember: "مشارك في غرفة", + statsRoom: "الغرف", + statsRoomMember: "المشاركون في الغرف", + favoritesFilterRooms: "الغرف", + invitesRoomsTitle: "الغرف", + inviteCodeInvalid: "رمز الدعوة هذا غير صالح أو استُخدم من قبل.", + inviteAlreadyMember: "أنت منضم بالفعل. يمكنك فتحه من وحدته.", + actionFailed: "تعذّر تنفيذ ذلك. حاول مرة أخرى.", + publishTooShort: "منشورك قصير جدًا.", + walletSendFailed: "تعذّر إرسال الدفعة: {reason}", + pixeliaCoordsError: "هذه الإحداثيات خارج الشبكة.", + courtsErrTitle: "العنوان مطلوب.", + courtsErrRespondent: "حدّد ضد من تُرفع القضية.", + courtsErrRespondentId: "هذا ليس معرّف Oasis صالحًا.", + courtsErrMethod: "اختر طريقة حل القضية.", + courtsErrCaseNotFound: "تعذّر العثور على هذه القضية.", + courtsErrBrief: "اكتب ردّك.", + courtsErrStance: "اختر موقفك.", + courtsErrResult: "اكتب النتيجة.", + courtsErrTerms: "اكتب شروط الاتفاق.", + courtsErrMediator: "أضف وسيطًا واحدًا على الأقل.", + courtsErrSelfMediator: "لا يمكنك أن تكون وسيطًا في قضيتك.", + courtsErrJudge: "اختر قاضيًا.", + courtsErrSelfJudge: "لا يمكنك أن تكون قاضيًا في قضيتك.", + courtsErrVisibility: "اختر من يمكنه رؤية القضية.", + courtsErrNomination: "تعذّر العثور على هذا الترشيح.", + opinionAlreadyGiven: "لقد أبديت رأيك بالفعل.", + cipherPasswordShort: "يجب ألا تقل كلمة المرور عن 32 حرفًا.", + errorForbidden: "ليس لديك صلاحية الوصول إلى هذا.", + errorNotFound: "تعذّر العثور عليه.", + errorClosed: "هذا مغلق.", + errorInviteOnly: "هذا بدعوة فقط. تحتاج إلى دعوة.", + errorTooLong: "هذا النص طويل جدًا.", + errorRequired: "هناك معلومات مطلوبة ناقصة.", + errorInvalid: "بعض المعلومات غير صالحة.", + errorNotAuthor: "لا يمكن القيام بهذا إلا لصاحب المحتوى.", + panicRemoved: "تم حذف سلسلة الكتل الخاصة بك.", + padRecentSectionTitle: "المستندات التعاونية الأخيرة", + padOpenSectionTitle: "المستندات التعاونية المفتوحة", + padClosedSectionTitle: "المستندات التعاونية المغلقة", + padCreateSectionTitle: "إنشاء مستند تعاوني جديد", + padUpdateSectionTitle: "تحديث المستند التعاوني", + typePad: "مستندات تعاونية", padNew: "جديد", padAddFavorite: "إضافة إلى المفضلة", padRemoveFavorite: "إزالة من المفضلة", - padClose: "إغلاق اللوحة", + padClose: "إغلاق", padBackToEditor: "العودة إلى المحرر", - padSearchPlaceholder: "البحث عن الوسادات...", + padSearchPlaceholder: "ابحث في المستندات التعاونية...", modulesChatsLabel: "المحادثات", - modulesChatsDescription: "وحدة لاكتشاف وإدارة المحادثات المشفرة.", + modulesChatsDescription: "وحدة لاكتشاف وإدارة الدردشات.", typeChat: "محادثات", typeChatMessage: "رسالة محادثة", - chatLabel: "المحادثات", - chatMessageLabel: "رسائل المحادثة", chatsTitle: "المحادثات", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "محادثاتك", + chatRecentTitle: "المحادثات الأخيرة", chatFavoritesTitle: "المفضلة", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "المحادثات المفتوحة", + chatClosedTitle: "المحادثات المغلقة", chatDescription: "الوصف", chatCategory: "الفئة", chatStatus: "الحالة", - chatFilterAll: "الكل", - chatFilterMine: "لي", - chatFilterRecent: "الأخيرة", - chatFilterFavorites: "المفضلة", - chatFilterOpen: "مفتوح", - chatFilterClosed: "مغلق", chatCreate: "إنشاء محادثة", - chatUpdate: "تحديث المحادثة", - chatDelete: "حذف المحادثة", + chatUpdate: "تحديث", + chatDelete: "حذف", chatClose: "إغلاق المحادثة", chatVisitChat: "زيارة المحادثة", chatUntitled: "محادثة بدون عنوان", chatNoItems: "لا توجد محادثات.", chatParticipants: "المشاركون", - chatStartChatting: "ابدأ المحادثة!", - chatGenerateCode: "إنشاء رمز", - chatShareUrl: "مشاركة الرابط", + chatMessagesLabel: "الرسائل", + chatThreadsLabel: "المواضيع", chatCreatedAt: "تم الإنشاء", - chatSearchPlaceholder: "بحث في المحادثات...", + chatSearchPlaceholder: "ابحث في المحادثات...", chatStatusOpen: "مفتوح", chatStatusInviteOnly: "بالدعوة فقط", chatStatusClosed: "مغلق", chatSendMessage: "إرسال", + chatJumpLatest: "آخر رسالة", + chatReply: "رد", + chatReplyCancel: "إلغاء الرد", + chatPin: "تثبيت", + chatUnpin: "إلغاء التثبيت", + chatPinned: "المثبتة", + chatPickChat: "اختر محادثة لفتحها", + chatNoneYet: "لا توجد محادثة متاحة بعد.", + activitySearchPlaceholder: "ابحث في النشاط...", + trendingSearchPlaceholder: "ابحث في الرائج...", + opinionsSearchPlaceholder: "ابحث في الآراء...", + votesSearchPlaceholder: "ابحث في التصويتات...", + gamesSearchPlaceholder: "ابحث في الألعاب...", + schoolTitle: "المدرسة", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "تم تعديل صفحة ويكي تتابعها.", + wikiBotRestoredTitle: "تمت استعادة إصدار سابق من صفحة ويكي تتابعها.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "انضم طالب جديد إلى دورتك.", + schoolBotInvitedTitle: "تمت دعوتك إلى دورة.", + schoolBotAdmittedTitle: "تم قبولك في دورة.", + schoolBotCertificateTitle: "لقد حصلت على شهادة.", + schoolBotPassedTitle: "نجح طالب في دورتك.", + schoolBotLessonTitle: "تم نشر درس جديد.", + invitesSchoolTitle: "المدرسة", + schoolEnrollLabel: "الانضمام إلى الدورة", + schoolExamRetryLater: "يمكنك المحاولة مجددًا بعد 24 ساعة.", + schoolApproved: "ناجح", + schoolExamPassMark: "درجة النجاح (0-10)", + schoolExamPassed: "ناجح", + schoolExamPending: "قيد الانتظار", + schoolExamFailed: "راسب", + schoolExamAddQuestion: "إضافة سؤال", + schoolExamCreateQuestion: "إنشاء سؤال", + schoolExamOption: "خيار", + schoolExamCorrect: "الخيار الصحيح", + schoolExamPoints: "النقاط", + schoolMaterialTextPlaceholder: "ملاحظات لهذا الدرس...", + schoolMaterials: "المواد", + schoolAddMaterial: "إضافة مادة", + schoolNoMaterials: "لا توجد مواد بعد.", + bankYourSchoolBalance: "أرباحك من المدرسة", + schoolProgress: "التقدّم", + schoolProgressMark: "وضع علامة مكتمل", + schoolProgressDone: "مكتمل", + schoolExams: "الاختبارات", + schoolExamCreate: "إنشاء اختبار", + schoolExamTitlePlaceholder: "عنوان الاختبار", + schoolExamHelp: "سؤال واحد في كل سطر: سؤال؟ ; خيار ; الخيار الصحيح* ; خيار", + schoolExamQuestionsPlaceholder: "ما هو SSB؟ ; قاعدة بيانات ; بروتوكول P2P* ; متصفح", + schoolExamSubmit: "إرسال الإجابات", + schoolExamScore: "نتيجتك", + schoolNoScoresYet: "لا توجد نتائج بعد.", + schoolLessonUnit: "الوحدة / المرحلة", + schoolLessonUnitPlaceholder: "مثال: المرحلة 1", + schoolLessonOrder: "الترتيب", + profileClearnetSchoolLabel: "المدرسة", + schoolStartDate: "تاريخ البدء", + schoolSessionDate: "تاريخ الجلسة", + schoolCourseChat: "دردشة الدورة", + agendaFilterSchool: "المدرسة", + agendaFilterCampaigns: "الحملات", + agendaFilterLogistics: "اللوجستيات", + schoolCourseType: "نوع الدورة", + schoolTypeOpen: "مفتوح", + schoolTypePaid: "مدفوع", + schoolTypeInvite: "بالدعوة فقط", + schoolLessonLocked: "الدرس مشفّر — اطلب مفتاح الدورة من المعلم.", + schoolGrantAccess: "إرسال مفتاح الدورة", + schoolKeySent: "تم إرسال المفتاح", + schoolOngoing: "جارٍ", + schoolFilterApplied: "مسجّل", + schoolFilterOpen: "مفتوحة", + schoolFilterFavorites: "المفضلة", + schoolPendingPayment: "الدفع معلّق", + schoolPendingStudents: "تسجيلات معلّقة", + schoolViewBill: "عرض الفاتورة", + schoolPaymentNote: "التسجيل يُنشئ فاتورة ECO (تحويل). يتأكد تسجيلك بعد توقيع التحويل.", + schoolCloseCourse: "إغلاق الدورة", + schoolReopenCourse: "إعادة فتح الدورة", + schoolDescription: "علّم وتعلّم الدورات في شبكتك.", + schoolFilterAll: "الكل", + schoolFilterMine: "دوراتي", + schoolFilterTop: "الأفضل", + schoolFilterRecent: "الأحدث", + schoolCreateButton: "إنشاء دورة", + schoolCreateSectionTitle: "إنشاء دورة", + schoolUpdateSectionTitle: "تحديث الدورة", + schoolCourseTitlePlaceholder: "اسم دورتك", + schoolCourseDescriptionPlaceholder: "ماذا ستعلّم في هذه الدورة؟", + schoolCourseImage: "صورة الدورة (الحجم الأقصى: 75MB)", + schoolTags: "الوسوم", + schoolTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + schoolPrice: "السعر (ECO)", + schoolVisibility: "الظهور", + schoolVisibilityPublic: "عام", + schoolVisibilityInvite: "بدعوة", + schoolStatus: "الحالة", + schoolClosed: "مغلق", + schoolFree: "مجاني", + schoolCreate: "إنشاء الدورة", + schoolUpdate: "تحديث الدورة", + schoolSearchPlaceholder: "ابحث في المدرسة...", + schoolSortTitle: "العنوان", + schoolNoCourses: "لم يتم العثور على دورات.", + schoolStudents: "الطلاب", + schoolTeacher: "المعلم", + schoolCreatedAt: "تاريخ الإنشاء", + schoolLessons: "الدروس", + schoolNoLessons: "لا توجد دروس بعد.", + schoolEnrollToSee: "سجّل لرؤية دروس هذه الدورة.", + schoolEnrollButton: "التسجيل", + schoolUnenrollButton: "مغادرة الدورة", + schoolInviteRequired: "هذه الدورة بدعوة فقط.", + schoolTeacherPanel: "لوحة المعلم", + schoolStudentPanel: "لوحة الطالب", + schoolAddLesson: "إنشاء درس", + schoolLessonTitlePlaceholder: "عنوان الدرس", + schoolLessonTextPlaceholder: "صف المفاهيم التي يغطيها هذا الدرس...", + schoolInviteStudents: "دعوة الطلاب", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "دعوة", + schoolInvited: "المدعوون", + schoolIssueCertificate: "إنشاء شهادة", + schoolCertified: "معتمد", + schoolCertificateTextPlaceholder: "ملاحظة الشهادة (اختياري)", + schoolLessonOpen: "افتح الدرس", + schoolCertificatePdf: "PDF", + schoolCertificates: "الشهادات", + schoolNoCertificates: "لم يتم إصدار شهادات بعد.", + schoolEditButton: "تعديل", + schoolDeleteButton: "حذف", + typeSchool: "المدرسة", + schoolCourseLabel: "دورة", + emergencyLabel: "طوارئ", + wikiPageLabel: "ويكي", + statsSchoolCourse: "الدورات", + statsSchoolLesson: "الدروس", + statsSchoolEnroll: "التسجيلات", + statsSchoolCertificate: "شهادات المدرسة", + modulesSchoolLabel: "المدرسة", + modulesSchoolDescription: "وحدة لتبادل المعرفة.", + welcomeStepUxAction: "حفظ العرض", + welcomeStepWorkflowTitle: "ما الاستخدام الذي تفكر فيه لـ OASIS؟", + welcomeStepWorkflowText: "في Oasis وحدات كثيرة. اختر الإعداد الذي يناسبك ولن يبقى مفعّلًا إلا المفيد منها. يمكنك تعديله متى شئت من «الوحدات».", + welcomeStepWorkflowAction: "حفظ الإعداد", + welcomeWorkflow_full: "الافتراضي", + chatRateLimitTitle: "رسائل كثيرة جدًا", + chatRateLimitMessage: "لقد بلغت حد الرسائل الذي تقبله هذه الغرفة في الساعة. حاول لاحقًا.", chatMessagePlaceholder: "اكتب رسالتك...", chatNoMessages: "لا توجد رسائل بعد.", - chatLeave: "مغادرة المحادثة", - chatInviteCodeLabel: "أدخل رمز الدعوة", - chatJoinByInvite: "انضم", chatTitlePlaceholder: "عنوان المحادثة", chatDescriptionPlaceholder: "وصف المحادثة", - chatTagsPlaceholder: "وسم1، وسم2", + chatTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", chatImageLabel: "اختر ملف صورة (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "رمز الدعوة", chatAuthor: "المؤلف", - chatCreated: "تم الإنشاء", chatAddFavorite: "إضافة للمفضلة", chatRemoveFavorite: "إزالة من المفضلة", chatPM: "رسالة", chatStatusLabel: "الحالة", + chatTypeLabel: "نوع الدردشة", chatCategoryLabel: "الفئة", - chatParticipantsLabel: "المشاركون", gamesTitle: "الألعاب", gamesDescription: "اكتشف وألعب بعض الألعاب.", gamesFilterAll: "الكل", gamesPlayButton: "العب!", - gamesBackToGames: "العودة إلى الألعاب", modulesGamesLabel: "الألعاب", modulesGamesDescription: "وحدة لاكتشاف وتشغيل بعض الألعاب.", modulesGraphosLabel: "غرافوس", @@ -3324,8 +4176,6 @@ module.exports = { gamesArkanoidDesc: "اكسر جميع الطوب بمضربك وكرتك. تحدي أركيد كلاسيكي.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "بينج بونج كلاسيكي ضد ذكاء اصطناعي. أول من يصل إلى 5 نقاط يفوز.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "سباق ضد الوقت! تجنب السيارات وصل إلى خط النهاية قبل انتهاء الوقت.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "قد مركبتك عبر حقل الكويكبات. دمرها قبل أن تصطدم بك.", gamesRockPaperScissorsTitle: "حجر ورقة مقص", @@ -3335,68 +4185,57 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "اقلب عملة معدنية وراهن على الوجه أو الظهر. كم أنت محظوظ?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "بلياردو من منظور علوي. انقر للتصويب واضغط باستمرار لشحن القوة. أدخل جميع الكرات بأقل عدد من الضربات.", + gamesArtilleryTitle: "المدفعية", + gamesArtilleryDesc: "صوّب مدفعك، وخذ الرياح في الحسبان، وأصب الهدف. 5 جولات، ويفوز صاحب أقل عدد من الطلقات.", + gamesLabyrinthTitle: "المتاهة", + gamesLabyrinthDesc: "اهرب من المتاهة قبل أن تنفد حركاتك. كل مستوى أكبر وأصعب.", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "كُل كل النقاط وتجنّب الأشباح. تتحرك الأشباح في الوقت الحقيقي — استمر في الحركة!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesAudioPendulumTitle: "البندول الصوتي", + gamesAudioPendulumDesc: "محاكي فيزياء فوضوية مع توليف صوتي في الوقت الحقيقي. تتحول السرعات الزاوية إلى ترددات، والقمم إلى ضربات طبول. لا توجد محاكاتان تبدوان متشابهتين.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "امزج المرئيات مباشرة على منصتين: اختر نمطاً لكل منهما، واضبط السرعة واللون والتكبير، وانتقل بينهما وأطلق الومضات على الإيقاع. انقر لضبط الإيقاع أو دع الميكروفون يقود النبض.", + gamesTetrisDesc: "المكعبات الساقطة الكلاسيكية. أكمل الصفوف لتسجل النقاط. إلى متى يمكنك الصمود؟", + gamesMinesweeperTitle: "كاسحة الألغام", + gamesMinesweeperDesc: "صيد الألغام الكلاسيكي. نظّف اللوحة لترتقي مستوى — شبكات أكبر وألغام أكثر ونقاط أعلى.", + gamesFilterScoring: "النقاط", + gamesHallOfFame: "قاعة المشاهير", + gamesHallPlayer: "اللاعب", + gamesHallScore: "النتيجة", + gamesNoScores: "لا توجد نتائج بعد.", calendarsTitle: "التقاويم", calendarTitle: "التقويم", modulesCalendarsLabel: "التقاويم", modulesCalendarsDescription: "وحدة لاكتشاف وإدارة التقاويم.", typeCalendar: "تقويمات", - calendarFilterAll: "الكل", - calendarFilterMine: "تقاويمي", - calendarFilterOpen: "مفتوح", - calendarFilterClosed: "مغلق", - calendarFilterRecent: "الأحدث", - calendarFilterFavorites: "المفضلة", calendarCreate: "إنشاء تقويم", calendarUpdate: "تحديث", - calendarDelete: "حذف", calendarTitleLabel: "العنوان", calendarTitlePlaceholder: "عنوان التقويم...", calendarStatusLabel: "الحالة", + calendarTypeLabel: "نوع التقويم", calendarStatusOpen: "مفتوح", calendarStatusClosed: "مغلق", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "الموعد النهائي", calendarTagsLabel: "الوسوم", - calendarTagsPlaceholder: "وسم1، وسم2...", + calendarTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", calendarParticipantsLabel: "المشاركون", - calendarParticipantsCount: "المشاركون", - calendarVisitCalendar: "زيارة التقويم", calendarCreated: "تم الإنشاء", - calendarAuthor: "المؤلف", calendarJoin: "انضمام", calendarGenerateInvite: "إنشاء دعوة", - calendarInviteCodePlaceholder: "أدخل رمز الدعوة...", - calendarValidateInvite: "التحقق من الرمز", - calendarJoined: "منضم", - calendarAddDate: "إضافة تاريخ", - calendarAddNote: "إضافة ملاحظة", calendarDateLabel: "التاريخ", calendarDatePlaceholder: "وصف هذا التاريخ...", - calendarNoteLabel: "ملاحظة", + calendarNoteLabel: "ملاحظات", calendarNotePlaceholder: "إضافة ملاحظة...", calendarFirstDateLabel: "التاريخ", calendarFirstNoteLabel: "ملاحظات", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "الفاصل الزمني", + calendarIntervalNone: "بدون تكرار", + calendarIntervalWeekly: "أسبوعيًا", + calendarIntervalMonthly: "شهريًا", + calendarIntervalYearly: "سنويًا", calendarFormDescription: "الوصف", calendarNoDates: "لا توجد تواريخ.", calendarNoNotes: "لا توجد ملاحظات.", @@ -3404,8 +4243,8 @@ module.exports = { calendarsDescription: "اكتشف وأدر التقويمات في شبكتك.", calendarMonthPrev: "← السابق", calendarMonthNext: "التالي →", - calendarMonthLabel: "التواريخ", - calendarsShareUrl: "رابط المشاركة", + calendarMonthNames: "يناير,فبراير,مارس,أبريل,مايو,يونيو,يوليو,أغسطس,سبتمبر,أكتوبر,نوفمبر,ديسمبر", + calendarDayInitials: "ن,ث,ر,خ,ج,س,ح", calendarAllSectionTitle: "التقاويم", calendarRecentSectionTitle: "التقويمات الأحدث", calendarFavoritesSectionTitle: "المفضلة", @@ -3415,21 +4254,160 @@ module.exports = { calendarCreateSectionTitle: "إنشاء تقويم جديد", calendarUpdateSectionTitle: "تحديث التقويم", calendarAddFavorite: "إضافة إلى المفضلة", - calendarDeleteNote: "Delete", + calendarDeleteNote: "حذف", calendarRemoveFavorite: "إزالة من المفضلة", - calendarSearchPlaceholder: "البحث عن تقاويم...", + calendarSearchPlaceholder: "ابحث في التقويم...", calendarAddEntry: "إضافة إدخال", - calendarLeave: "مغادرة التقويم", statsCalendar: "التقاويم", statsCalendarDate: "تواريخ التقويم", statsCalendarNote: "ملاحظات التقويم", chatAccessDenied: "ليس لديك حق الوصول إلى هذه المحادثة. اطلب دعوة للوصول إلى المحتوى.", - padAccessDenied: "ليس لديك حق الوصول إلى هذا الوسادة. اطلب دعوة للوصول إلى المحتوى.", + padAccessDenied: "ليس لديك حق الوصول إلى هذا المستند التعاوني. اطلب دعوة للوصول إلى المحتوى.", contentAccessDenied: "ليس لديك حق الوصول إلى هذا المحتوى.", blockAccessRestricted: "الوصول مقيد", tribeContentAccessDenied: "تم رفض الوصول", tribeContentAccessDeniedMsg: "هذا المحتوى ينتمي إلى قبيلة. يجب أن تكون عضواً للوصول إليه.", tribeViewTribes: "عرض القبائل", + tribeFilesEmpty: "لا توجد تورنتات بعد.", + filesLabel: "التورنت", + profileClearnetFilesLabel: "التورنت", + filesTitle: "التورنت", + filesDescription: "استكشف وأدِر التورنت في شبكتك.", + fileAllSectionTitle: "التورنت", + fileMineSectionTitle: "تورنتاتك", + fileRecentSectionTitle: "تورنتات حديثة", + fileTopSectionTitle: "أفضل التورنتات", + fileFavoritesSectionTitle: "المفضلة", + fileFilterAll: "الكل", + fileFilterMine: "خاصتي", + fileFilterRecent: "الحديثة", + fileFilterTop: "الأفضل", + fileFilterFavorites: "المفضلة", + fileCreateSectionTitle: "رفع تورنت", + fileUpdateSectionTitle: "تحديث التورنت", + fileCreateButton: "رفع تورنت", + fileUpdateButton: "تحديث", + fileAddFavoriteButton: "إضافة إلى المفضلة", + fileRemoveFavoriteButton: "إزالة من المفضلة", + fileFileLabel: "اختاري ملفًا (الحد الأقصى: 50 ميغابايت)", + calendarFilterRecent: "الأخيرة", + calendarFilterAll: "الكل", + calendarFilterMine: "خاصتي", + calendarFilterFavorites: "المفضلة", + calendarFilterOpen: "مفتوح", + calendarFilterClosed: "مغلق", + padFilterRecent: "الأخيرة", + padFilterAll: "الكل", + padFilterMine: "خاصتي", + padFilterOpen: "مفتوح", + padFilterClosed: "مغلق", + chatFilterRecent: "الأخيرة", + chatFilterAll: "الكل", + chatFilterMine: "خاصتي", + chatFilterFavorites: "المفضلة", + chatFilterOpen: "مفتوح", + chatFilterClosed: "مغلق", + licenseLabel: "الترخيص", + phoneTitle: "الهاتف", + phoneDescription: "أدر مكالمات الفيديو المشفّرة من طرف إلى طرف.", + phoneUnavailable: "المكالمات غير متاحة على هذا الجهاز.", + phoneCallButton: "اتصال", + profileVisibilityPhone: "رقم الهاتف", + phoneNumberUnknown: "لا أحد في شبكتك لديه الرقم {number}.", + phoneNumberChoose: "تحقق ممن تريد الاتصال به على الرقم {number}:", + phoneAnswer: "رد", + phoneSpeaker: "مكبر الصوت", + phoneEarpiece: "سماعة الأذن", + phoneReject: "رفض", + phoneHangup: "إنهاء المكالمة", + phoneMute: "كتم الصوت", + phoneUnmute: "إلغاء الكتم", + phoneCameraOn: "الكاميرا: تشغيل", + phoneCameraOff: "الكاميرا: إيقاف", + settingsPhoneVideo: "الفيديو", + settingsPhoneVideoOn: "مسموح في المكالمات والغرف", + settingsPhoneVideoOff: "صوت فقط", + phoneSilence: "عدم السماع", + phoneUnsilence: "السماع مجدداً", + phoneCalling: "جارٍ الاتصال بـ", + phoneIsCalling: "يتصل بك", + notifyNewPmFrom: "رسالة خاصة جديدة من", + notifyNewPamFrom: "رسالة PaM جديدة من", + notifyUpdateLabel: "تحديث", + updateInstalledLabel: "الإصدار المثبت", + updateAvailableLabel: "الإصدار المتاح", + updateChangesLabel: "التغييرات", + updateFilesLabel: "الملفات", + updateOriginLabel: "المصدر", + phoneRecording: "جارٍ تسجيل PaM لـ", + phoneLeaveMessage: "لم يرد أحد: اترك رسالتك بعد الصفارة.", + phoneLeaveMessageBusy: "في مكالمة أخرى: اترك رسالتك بعد الإشارة.", + phoneLeaveMessageUnreachable: "تعذّر الوصول: اترك رسالتك بعد الإشارة.", + phoneConnecting: "جارٍ الاتصال بـ", + phoneSendPam: "إرسال PaM", + phoneCancel: "إلغاء", + phoneSearchPlaceholder: "ابحث في الهاتف...", + phonePamTitle: "تسجيلات صوتية", + phoneNew: "جديد", + phoneDelete: "حذف", + phoneNoPams: "ليست لديك تسجيلات صوتية.", + phoneHistoryTitle: "السجل", + phoneLatestTitle: "آخر المكالمات", + phoneCreateButton: "مكالمة جديدة", + phoneLimitsHint: "حتى {n} من السكان لكل مكالمة؛ أكثر من ساكن واحد يجعلها مكالمة مشتركة.", + phoneErrorPub: "لا يمكن الاتصال بخادم pub.", + phoneJointCall: "مكالمة مشتركة", + phoneWith: "مع", + phoneResult: "النتيجة", + phoneWhen: "متى", + phoneDuration: "المدة", + phoneOutgoing: "صادرة", + phoneIncoming: "واردة", + phoneNoHistory: "لا توجد سجلات بعد.", + phoneOutcomeEnded: "تم الرد", + phoneOutcomeMissed: "فائتة", + phoneOutcomeRejected: "مرفوضة", + phoneOutcomeNoanswer: "لا رد", + phoneOutcomeCancelled: "ملغاة", + phoneOutcomePam: "تم إرسال PaM", + phoneOutcomeFailed: "فشلت", + phoneDialAdd: "إضافة", + phoneDialBack: "حذف", + phoneDialClear: "مسح", + phoneDialPlaceholder: "رقم أو معرّف Oasis", + phoneDialInhabitants: "السكان", + phoneDialRemove: "إزالة", + phoneOwnNumber: "هذا رقمك أنت.", + phoneSettingsDesc: "اختر من يمكنه الاتصال بك.", + phoneDnd: "عدم الإزعاج", + phoneOpen: "مفتوح", + phoneRecipientRefuses: "الساكن الذي تحاول الاتصال به لا يقبل المكالمات.", + phoneRefusedNotice: "هؤلاء السكان لا يقبلون المكالمات ولم يتم الاتصال بهم:", + fileEmptyError: "الملف فارغ. اختاري ملفًا آخر.", + fileTitleLabel: "العنوان", + fileTitlePlaceholder: "العنوان", + fileDescriptionLabel: "الوصف", + fileDescriptionPlaceholder: "الوصف", + fileTagsLabel: "الوسوم", + fileTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + fileSizeLabel: "الحجم", + fileDownloadButton: "تحميل", + fileDetailsButton: "التفاصيل", + fileNoFile: "لا يوجد ملف تورنت", + fileSearchPlaceholder: "ابحث في التورنت...", + fileSearchButton: "بحث", + fileSortRecent: "الأحدث", + fileSortOldest: "الأقدم", + fileSortTop: "الأكثر تصويتاً", + fileNoMatch: "لا توجد تورنتات مطابقة.", + fileUpdatedAt: "تم التحديث", + typeFile: "التورنت", + modulesFilesLabel: "الملفات", + modulesFilesDescription: "وحدة لاكتشاف وإدارة الملفات.", + tribeSectionFiles: "التورنت", + fileDescription: "الوصف", + fileDownload: "تنزيل", + fileTypeLabel: "النوع", torrentsTitle: "التورنت", torrentsDescription: "استكشف وأدِر التورنت في شبكتك.", torrentAllSectionTitle: "التورنت", @@ -3457,47 +4435,91 @@ module.exports = { torrentTagsLabel: "الوسوم", torrentTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", torrentSizeLabel: "الحجم", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "إنشاء تورنت", + torrentFromContentMissing: "الملف غير متاح بعد على هذه العقدة. حاولي لاحقًا.", + torrentDownloads: "التنزيلات", + torrentFetchOasis: "تنزيل داخل Oasis", + torrentSaveFile: "حفظ الملف", + torrentSeeds: "بذور", + seedsLabel: "البذور", + seedAction: "دعم", + torrentSourceFile: "الملف", + torrentStateQueued: "في الانتظار", + torrentStateProbing: "البحث عن بذور", + torrentStateDownloading: "جارٍ التنزيل", + torrentStateDone: "اكتمل", + torrentStateFailed: "فشل", + torrentStateNoSource: "لا توجد بذور متصلة بعد، في الانتظار", + torrentStateCancelled: "أُلغي", + torrentNoDownloads: "لا توجد تنزيلات بعد.", + torrentOriginalSection: "الأصل", + torrentCancel: "إلغاء", + torrentDownloadButton: "تحميل", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "التفاصيل", torrentNoFile: "لا يوجد ملف تورنت", noTorrents: "لم يتم العثور على تورنتات.", - torrentSearchPlaceholder: "بحث بالعنوان، الوسوم، المؤلف...", + noFiles: "لم يتم العثور على تورنتات.", + torrentSearchPlaceholder: "ابحث في التورنت...", torrentSearchButton: "بحث", torrentSortRecent: "الأحدث", torrentSortOldest: "الأقدم", torrentSortTop: "الأكثر تصويتاً", torrentNoMatch: "لا توجد تورنتات مطابقة.", - torrentMessageAuthorButton: "مراسلة المؤلف", torrentUpdatedAt: "تم التحديث", statsTorrent: "التورنت", + statsFile: "الملفات", typeTorrent: "التورنت", modulesTorrentsLabel: "التورنت", modulesTorrentsDescription: "وحدة لاكتشاف وإدارة التورنت.", favoritesFilterTorrents: "التورنت", + favoritesFilterFiles: "الملفات", favoritesFilterMarket: "السوق", + favoritesFilterEvents: "الفعاليات", + favoritesFilterForum: "المنتدى", + favoritesFilterHousing: "السكن", + favoritesFilterJobs: "الوظائف", + favoritesFilterProjects: "المشاريع", + favoritesFilterReports: "التقارير", + favoritesFilterTasks: "المهام", + favoritesFilterTransfers: "التحويلات", + favoritesFilterVotes: "التصويتات", favoritesFilterShopProducts: "منتجات المتجر", tribeSectionTorrents: "التورنت", tribeCreateTorrent: "رفع تورنت", - tribeMediaTypeTorrent: "تورنت", + tribeCreateFile: "رفع ملف", settingsWishTitle: "الرغبة", - settingsWishDesc: "قم بتكوين رغبة الأفاتار الخاص بك.", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "يحدد كيفية وصولك إلى كل المحتوى وتجربتك في شبكة Oasis.", + settingsWishWhole: "الكون المتعدد", + settingsWishMutuals: "الدعم المتبادل فقط", + settingsWishOnlyLan: "الشبكة المحلية فقط", + settingsWishLocal: "محلي", settingsPmVisibilityTitle: "الرسائل الخاصة", settingsPmVisibilityDesc: "قم بتكوين مستوى التعرض للرسائل الخاصة في الشبكة.", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "الكون المتعدد", + settingsPmVisibilityMutuals: "الدعم المتبادل فقط", + mute: "كتم", + unmute: "إلغاء الكتم", + relationshipMuted: "مكتوم من قبلك", + hideContent: "إخفاء عني", + unhideContent: "إظهار مجددًا", + hiddenByYou: "مخفي من قبلك", + settingsTabContent: "المحتوى", + settingsHiddenTitle: "المحتوى المخفي", + settingsHiddenDesc: "السكان الذين كتمتهم والمحتوى الذي أخفيته.", + settingsMutedLabel: "السكان المكتومون", + settingsHiddenLabel: "المحتوى المخفي", + settingsMutedEmpty: "لا أحد مكتوم.", + settingsHiddenEmpty: "لا شيء مخفي.", + settingsPmVisibilityFollowing: "فقط من أتابعهم", + settingsMentionsVisibilityTitle: "الإشارات", + settingsMentionsVisibilityDesc: "اختر من تصلك إشاراته.", pmMutualNotice: "الإرسال حاجز UX. الاستقبال فلتر انتباه.", - pmBlockedNonMutual: "يمكنك إرسال الرسائل الخاصة فقط للسكان ذوي الدعم المتبادل.", inhabitantsPendingFollowsTitle: "طلبات الدعم المعلقة", inhabitantsPendingAccept: "قبول", inhabitantsPendingReject: "رفض", - bxEncrypted: "مشفر", - bxEncryptedHexLabel: "النص المشفر (معاينة)", tribeSectionGovernance: "الحوكمة", - tribeSubStatusPublic: "عامة", - tribeSubStatusPrivate: "خاصة", - tribeSubInheritedPrivate: "خاصة (موروثة من القبيلة الرئيسية)", tribePadInviteRequired: "لا يمكنك الوصول. اطلب دعوة.", tribeChatInviteRequired: "لا يمكنك الوصول للدردشة. اطلب دعوة.", tribeGovernanceDesc: "الحوكمة الداخلية لهذه القبيلة.", @@ -3536,209 +4558,195 @@ module.exports = { tribeGovNoRevocations: "لا يوجد أي إلغاء بعد", tribeGovNoHistorical: "لا يوجد أي سجل بعد", logsTitle: "السجل", - logsDescription: "سجل تجربتك في الشبكة.", - logsReadMore: "اقرأ المزيد", + logsDescription: "احتفظ بمذكرات خاصة لتجاربك.", logsViewTitle: "السجل", logsColumnType: "النوع", logsView: "عرض", - logsManualPrompt: "اكتب سجلك", logsUpdateButton: "تحديث", - logsEditTitle: "تعديل الإدخال", - logsCreateDescription: "سجل تجربتك...", + logsEditTitle: "تحديث السجل", logsCreateTitle: "إنشاء إدخال", logsWriteButton: "اكتب", logsTextPlaceholder: "صف تجاربك...", - logsTextField: "النص", - logsLabelPlaceholder: "تسمية...", - logsLabelField: "اكتب سجلك (تسمية)", - logsAiDisabledWarn: "فعّل وحدة الذكاء الاصطناعي في /modules لاستخدام السجلات المكتوبة بواسطة الذكاء.", - logsAiContextValue: "يوم blockchain", - logsAiContext: "السياق", - logsAiModStatus: "AImod", - logsModeApply: "تطبيق!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "مساعد الذكاء الاصطناعي", logsModeManual: "يدوي", logsModeAI: "ذكاء", - logsColumnDelete: "حذف", - logsColumnEdit: "تعديل", logsColumnLog: "السجل", logsColumnDate: "التاريخ", logsDelete: "حذف", - logsEdit: "تعديل", + logsEdit: "تحديث", logsFilterAlways: "دائمًا", logsFilterYear: "السنة الماضية", logsFilterMonth: "الشهر الماضي", logsFilterWeek: "الأسبوع الماضي", logsFilterToday: "اليوم", - logsFilterRecent: "الأحدث", - logsFilterAll: "الكل", + logsComments: "التعليقات", + logsAuthorEntries: "المدخلات", logsCreate: "إنشاء إدخال", logsExport: "تصدير السجل", - logsExportOne: "تصدير", + logsExportOne: "تصدير السجل", logsViewDetails: "عرض التفاصيل", logsGenerateButton: "توليد نص", - logsSearchText: "ابحث في السجلات...", + logsSearchText: "ابحث في السجل...", logsSearchAnyType: "أي نوع", logsSearchButton: "بحث", logsEmpty: "لا توجد إدخالات بعد.", + logsNothingNew: "لا توجد إجراءات جديدة لتسجيلها.", modulesLogsLabel: "السجل", - modulesLogsDescription: "وحدة لتسجيل تجاربك (عبر مساعد ذكاء اصطناعي).", + modulesLogsDescription: "وحدة للاحتفاظ بمذكرات خاصة لتجاربك.", statsLogsTitle: "السجل", - statsLogsEntries: "إدخالات", typeLog: "سجل", - blockchainCycle: "دورة", larpTitle: "L.A.R.P.", larpDescription: "طبقة لعب أدوار حية للتجربة التعاونية.", + larpNoHousesMatch: "لا توجد بيوت تطابق بحثك.", + larpSearchPlaceholder: "ابحث في L.A.R.P....", larpAllHouses: "البيوت:", larpFilterRuling: "يحكم", larpFilterHouses: "البيوت", - larpFilterRules: "FAQ", + larpFilterRules: "الأسئلة الشائعة", larpRulesTitle: "أسئلة شائعة عن L.A.R.P.", - larpRulesEntryTitle: "بيت البداية", larpRulesEntryText: "يبدأ كل ساكن في ACADEMIA افتراضياً. من ACADEMIA، اختر بيتاً من لوحة «الانضمام إلى بيت»: ستفتح اختبار الدخول الخاص به. أجب عن الأسئلة العشرة وأرسلها. إذا تجاوزت الحد (7/10) فستُقبل آلياً في ذلك البيت؛ وإلا تُرفض وتبقى في ACADEMIA. في كل الأحوال يسجّل النظام OasisID الخاص بك ودورة المحاولة، ويمنع محاولة جديدة حتى انتهاء فترة الانتظار وهي 30 دورة.", - larpRulesLeaveText: "يستطيع أعضاء أي بيت العودة إلى ACADEMIA في أي وقت من صفحة بيتهم؛ يعيد ذلك ضبط عضويتهم لكنه لا يلغي فترة انتظار الاختبار.", - larpRulesGovernanceTitle: "حائط الحوكمة", larpRulesGovernanceText: "خلال دورته، يحمل البيت الحاكم شارة «يحكم» وهو الوحيد الذي يُعرض حائطه علنياً ضمن تبويب «يحكم».", - larpRulesSignTitle: "شعار L.A.R.P.", + larpRulesWallText: "لكل بيت جدار يكتب فيه أعضاؤه. جدار البيت خاص به، عدا جدار البيت الحاكم وجدار ACADEMIA، فيمكن لأي شخص قراءتهما.", larpRulesSignText: "يستطيع السكان تفعيله (في /profile/edit > المستشعرات) لعرض صورة بيتهم الحالي كختم في الملف الشخصي وبطاقة المؤلف والساكن. يربط الختم بصفحة البيت.", larpPostsTitle: "الحائط", larpPostsEmpty: "لا توجد منشورات بعد.", - larpPostLabel: "منشور جديد", larpPostPlaceholder: "ماذا يريد هذا البيت أن يقول؟", - larpPostPublic: "عام (يراه أي شخص، وإلا فالأعضاء فقط)", larpPostSubmit: "انشر", - larpPostMembersOnly: "للأعضاء فقط", larpCycleLabel: "الدورة:", - audioBackToBcs: "Back to BCS", + audioBackToBcs: "العودة إلى BCS", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", - audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", - audioTranscodeDetailTitle: "Transcode", - audioTranscodeStegoEmpty: "(none)", - audioTranscodeStegoMessage: "TEXT", - audioTranscodeStegoNotFound: "No steganographic payload could be decoded from this audio.", - audioTranscodeStegoOasisId: "By", - audioTranscodeStegoTimestamp: "Generated at", + audioTranscodeButton: "تحويل الترميز", + audioTranscodeCompositionEmpty: "لا يتضمن هذا الملف الصوتي تأليفًا محفوظًا لسلسلة الكتل.", + audioTranscodeDetailDescription: "فك ترميز الحمولة المضمنة وخريطة التأليف الأصلية لسلسلة الكتل.", + audioTranscodeDetailTitle: "تحويل الترميز", + audioTranscodeStegoEmpty: "(لا شيء)", + audioTranscodeStegoMessage: "النص", + audioTranscodeStegoNotFound: "تعذّر فك ترميز أي حمولة مخفية من هذا الملف الصوتي.", + audioTranscodeStegoTimestamp: "تاريخ الإنشاء", audioTranscodeStegoUnknown: "—", - bankExchangeEcoTaxAnnual: "ECOin Taxes (annual)", - bankExchangeEcoTaxMonthly: "ECOin Taxes (monthly)", - bankOverviewYourTaxes: "Your taxes", - bankRulesAlpha: "Alpha (max share of pub balance per epoch)", + bankExchangeEcoTaxAnnual: "ضرائب ECOin (سنويًا)", + bankExchangeEcoTaxMonthly: "ضرائب ECOin (شهريًا)", + bankOverviewYourTaxes: "إجمالي الضرائب", + bankRulesAlpha: "Alpha (الحصة القصوى من رصيد الـ pub لكل حقبة)", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", - bankRulesArchTaxRate: "Rate", - bankRulesArchTaxTitle: "ARCH Tax", - bankRulesCapPerEpoch: "Cap per epoch (absolute)", - bankRulesCapUser: "Cap per user per epoch", - bankRulesCarbonFactor: "Carbon factor", + bankRulesArchTaxNote: "تعكس ضريبة ARCH التكلفة طويلة الأمد لتوسيع أرشيف الشبكة وصيانته: كلما كانت بياناتك أقدم في الشبكة، زادت حصتك من تكلفة الصيانة.", + bankRulesArchTaxRate: "المعدل", + bankRulesArchTaxTitle: "ضريبة ARCH", + bankRulesCapPerEpoch: "الحد الأقصى لكل حقبة (مطلق)", + bankRulesCapUser: "الحد الأقصى لكل مستخدم في كل حقبة", + bankRulesCarbonFactor: "معامل الكربون", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", - bankRulesChipTitle: "ECO Tax chip color band", + bankRulesChipNote: "تُلوَّن شارة كل عنصر بالنسبة إلى أكبر رسالة رُصدت في الشبكة، وتُلطَّف بعدد السكان النشطين — فالشبكة الأكبر توزّع الحمل وتدفع مزيدًا من العناصر إلى النطاق الأخضر.", + bankRulesChipTitle: "نطاق لون شارة ضريبة ECO", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", - bankRulesEcoTaxRate: "Rate", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesEpochKind: "Epoch granularity", - bankRulesFloor: "Floor per user (gross)", - bankRulesGraceDays: "Claim grace period", + bankRulesEcoTaxNote: "تُخصم ضريبة ECO من الدخل الأساسي الإجمالي عند حساب تخصيص الحقبة. ولا يُعاد توزيع وحدات ECO المخصومة في هذه الحقبة — بل تبقى في رصيد الـ pub وتغذّي صندوق الحقبة التالية عبر حدود reserveMin / alpha.", + bankRulesEcoTaxRate: "المعدل", + bankRulesEcoTaxTitle: "ضريبة ECO", + bankRulesEpochKind: "دقة الحقبة", + bankRulesFloor: "الحد الأدنى لكل مستخدم (إجمالي)", + bankRulesGraceDays: "فترة السماح للمطالبة", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", - bankRulesKarmaTitle: "Karma (user engagement score)", + bankRulesKarmaNote: "تُشتق الكارما من أفعالك (المنشورات، والأصوات، والمشاركة في البرلمان/المحاكم، وغير ذلك) مع معامل تناقص بمرور الوقت، مطروحًا منها غرامات الكربون التي تولّدها تغذيتك. ما يتلقاه محتواك يُحتسب أيضًا: الآراء والأصوات والتفاعلات والتشغيلات الإيجابية تضيف كارما، والآراء أو الأصوات السلبية تخصمها عندما يتفق عدة سكان.", + bankRulesKarmaTitle: "الكارما (درجة تفاعل المستخدم)", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", - bankRulesPoolTitle: "Monthly pool", - bankRulesReserveMin: "Reserve minimum (kept in pub balance)", + bankRulesPoolTitle: "الصندوق الشهري", + bankRulesReserveMin: "الحد الأدنى للاحتياطي (يبقى في رصيد الـ pub)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", - bankRulesShareTitle: "Per-user share", + bankRulesShareTitle: "حصة كل مستخدم", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankRulesWMax: "Maximum weight (w_max)", - bankRulesWMin: "Minimum weight (w_min)", - bankTaxes: "Taxes", - bankTaxesAnnualEcoin: "ECO Tax (annual)", - bankTaxesArchTaxAnnual: "ARCH Tax (annual)", - bankTaxesArchTaxFirstBlock: "Your first block age", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxLifetime: "ARCH Tax (lifetime)", - bankTaxesArchTaxMonthly: "ARCH Tax (monthly)", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxRate: "Rate", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesArchTaxUserAmount: "Your ARCH tax (price to return)", - bankTaxesEcoTaxLifetime: "ECO Tax (lifetime)", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesLookupAuthor: "Author", - bankTaxesLookupButton: "Inspect", - bankTaxesLookupCarbon: "Carbon footprint", - bankTaxesLookupEcoin: "ECO Tax", - bankTaxesLookupNotFound: "No block found in your local feed with that ID.", - bankTaxesLookupPlaceholder: "Block ID (e.g. %abc...=.sha256)", - bankTaxesLookupSize: "Size", - bankTaxesLookupTitle: "Inspect a block", - bankTaxesLookupType: "Type", - bankTaxesMonthlyEcoin: "ECO Tax (monthly)", - bankTaxesRate: "Rate", - bankTaxesSpan: "Sampling span", - bankTaxesTotalAnnual: "Total (annual)", - bankTaxesTotalBlocks: "Total blocks", - bankTaxesTotalBytes: "Total bytes", - bankTaxesTotalCarbon: "Total carbon", - bankTaxesTotalEcoin: "Total ECO tax (lifetime)", - bankTaxesTotalLifetime: "Total (lifetime)", - bankTaxesTotalMonthly: "Total (monthly)", - bankTaxesTypesApply: "Set my taxes", - bankTaxesTypesLabel: "Select which taxes you want to pay", - bankTaxesUserAmount: "Your ECO tax (price to return)", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", - blockchainBackToBlockexplorer: "Back to blockexplorer", - blockchainBlockNotAvailable: "This block is not available in your local feed. It may belong to a peer you are not yet replicating from.", - ecoTaxLabel: "ECO Tax", - gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - melodyAllEmpty: "No BCS compositions from other inhabitants yet.", - melodyByLabel: "By", - melodyNoteUnavailable: "block not in your local feed", - melodyUploadBcsButton: "Publish", - melodyUploadBcsNameNote: "The audio will be published with the auto-generated name", - melodyUploadBcsTitle: "Publish", - melodyUploadStegoLabel: "Hidden message (steganography)", - melodyUploadStegoMaxLabel: "max 280 characters", - melodyUploadStegoPlaceholder: "Optional.", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", - statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", - statsPad: "Pads", - statsPadEntry: "Pad entries", - statsTombstoneEmpty: "No tombstones in the network yet.", - typeGameScore: "GAME SCORES", + bankRulesWMax: "الوزن الأقصى (w_max)", + bankRulesWMin: "الوزن الأدنى (w_min)", + bankTaxes: "الضرائب", + bankTaxesAnnualEcoin: "ضريبة ECO (سنويًا)", + bankTaxesArchTaxAnnual: "ضريبة ARCH (سنويًا)", + bankTaxesArchTaxFirstBlock: "عمر أول كتلة لك", + bankTaxesArchTaxFootprintNote: "تعكس ضريبة ARCH تكلفة توسيع أرشيف الشبكة وصيانته. وتُحسب من الفارق الزمني بين أول كتلة نشرتها وأحدث كتلة في الشبكة — فكلما طال بقاء تاريخك في الأرشيف، زادت حصتك من تكلفة الصيانة.", + bankTaxesArchTaxLifetime: "ضريبة ARCH (مدى الحياة)", + bankTaxesArchTaxMonthly: "ضريبة ARCH (شهريًا)", + bankTaxesArchTaxNoteBold: "خفض ضريبة ARCH مباشرةً", + bankTaxesArchTaxNoteIntro: "تُخصم ضريبة ARCH الخاصة بك من الفائض الذي تولّده الشبكة فوق دخلك الأساسي؛ وتُحدَّد القيمة الأساسية للدخل الأساسي كحد أدنى ثابت. ولا تُخصم الضريبة أبدًا من المبلغ الثابت الذي يستحقه كل ساكن بوصفه دخلًا أساسيًا. تُغذّي الأموال المخصومة خوارزمية إعادة توزيع الثروة وتُعاد إلى سكان آخرين عبر مطالباتهم بالدخل الأساسي. ومن المرجح أن يكون لدى السكان الأحدث، ولدى المتفرغين لمهام الأرشفة والنسخ والصيانة، مهمة مخصصة للحفاظ على سلامة أرشيف الشبكة نيابةً عنك. أو قد يكرّسون أنفسهم لـ", + bankTaxesArchTaxNoteOutro: " عبر اتفاقيات التقليم، وإزالة تكرار البيانات الزائدة، وبنية صيانة مشتركة، وقد يطلب إجماع الشبكة مساهمتهم المستمرة.", + bankTaxesArchTaxRate: "المعدل", + bankTaxesArchTaxTitle: "ضريبة ARCH", + bankTaxesArchTaxUserAmount: "ضريبة ARCH الخاصة بك (المبلغ الواجب إرجاعه)", + bankTaxesEcoTaxLifetime: "ضريبة ECO (مدى الحياة)", + bankTaxesEcoTaxTitle: "ضريبة ECO", + bankTaxesLookupAuthor: "المؤلف", + bankTaxesLookupButton: "فحص", + bankTaxesLookupCarbon: "البصمة الكربونية", + bankTaxesLookupEcoin: "ضريبة ECO", + bankTaxesLookupNotFound: "لم يُعثر في تغذيتك المحلية على كتلة بهذا المعرّف.", + bankTaxesLookupPlaceholder: "معرّف الكتلة (مثل %abc...=.sha256)", + bankTaxesLookupSize: "الحجم", + bankTaxesLookupTitle: "فحص كتلة", + bankTaxesLookupType: "النوع", + bankTaxesMonthlyEcoin: "ضريبة ECO (شهريًا)", + bankTaxesRate: "المعدل", + bankTaxesSpan: "فترة أخذ العينات", + bankTaxesTotalAnnual: "الإجمالي (سنويًا)", + bankTaxesTotalBlocks: "إجمالي الكتل", + bankTaxesTotalBytes: "إجمالي البايتات", + bankTaxesTotalCarbon: "إجمالي الكربون", + bankTaxesTotalEcoin: "إجمالي ضريبة ECO (مدى الحياة)", + bankTaxesTotalLifetime: "الإجمالي (مدى الحياة)", + bankTaxesTotalMonthly: "الإجمالي (شهريًا)", + bankTaxesTypesApply: "تعيين ضرائبي", + bankTaxesTypesLabel: "اختر الضرائب التي تريد دفعها", + bankTaxesUserAmount: "ضريبة ECO الخاصة بك (المبلغ الواجب إرجاعه)", + bankTaxesUserNoteChips: "تتغير شارة ضريبة ECO المعروضة بجانب كل عنصر لونها (أخضر / أصفر / أحمر) بحسب حجم العنصر مقارنةً بأكبر كتلة شوهدت في الشبكة، مع تلطيفها بعدد السكان النشطين — فالشبكة الشمسية الأكبر توزّع الحمل وتدفع مزيدًا من العناصر إلى النطاق الأخضر.", + bankTaxesUserNoteOtherParams: "تُشتق ضريبة ECO حاليًا من البصمة الكربونية لكل كتلة، لكنها قد تتضمن مع الوقت معايير أخرى — الطاقة المستهلكة في النسخ، والتخزين المكرر بين العقد، وعرض نطاق الملفات، والتكلفة الحسابية لفك التشفير، وبصمة تعدين المعاملات المرتبطة، أو أي حمل آخر قابل للقياس تتفق الشبكة على تقييمه.", + blockchainBackToBlockexplorer: "العودة إلى مستكشف الكتل", + blockchainBlockNotAvailable: "هذه الكتلة غير متوفرة في تغذيتك المحلية. قد تنتمي إلى عقدة لا تنسخ منها بعد.", + ecoTaxLabel: "ضريبة ECO", + gamesHallDate: "التاريخ", + gamesNewRecord: "رقم قياسي جديد!", + indexingRefreshNote: "تُحدَّث هذه الصفحة كل 10 ثوانٍ.", + indexingMessage: "يقوم Oasis بمزامنة شبكة العقد الموزعة. يرجى الانتظار.", + indexingTitle: "جارٍ المزامنة", + melodyAllEmpty: "لا توجد بعد مؤلفات BCS من سكان آخرين.", + melodyNoteUnavailable: "الكتلة ليست في تغذيتك المحلية", + melodyUploadBcsButton: "نشر", + melodyUploadBcsNameNote: "سيُنشر الملف الصوتي بالاسم المُنشأ تلقائيًا", + melodyUploadBcsTitle: "نشر", + melodyUploadStegoLabel: "رسالة مخفية (إخفاء المعلومات)", + melodyUploadStegoMaxLabel: "280 حرفًا كحد أقصى", + melodyUploadStegoPlaceholder: "اختياري.", + padsNoItems: "لم يتم العثور على مستندات تعاونية.", + padVersionHistory: "سجل الإصدارات", + padAttributionView: "حسب المؤلفة", + padVersionView: "عرض", + statsChat: "المحادثات", + statsChatMessage: "رسائل المحادثة", + statsGameScore: "الألعاب", + statsWiki: "صفحات الويكي", + statsPad: "المستندات التعاونية", + statsPadEntry: "مدخلات المستندات التعاونية", + statsTombstoneEmpty: "لا توجد عناصر محذوفة (tombstones) في الشبكة بعد.", + typeGameScore: "نتائج الألعاب", bankTaxesLookupNote: "أدخل معرف كتلة للبحث عنه في خلاصتك المحلية.", bankTaxesUserNoteChipsClick: "انقر على أي شريحة لفحص كتلتها في blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "رمز الدعوة", larpRulesInviteEntryText: "يمكن لأعضاء البيوت إنشاء رموز دعوة تمنح الوصول المباشر إلى البيت.", larpRulesOneHouseText: "يمكنك الانتماء إلى بيت واحد فقط في أي وقت. إذا لم تنتم إلى أي بيت، فأنت في ACADEMIA. كل صفحة بيت (غير ACADEMIA) تعرض لأعضائها زر «مغادرة البيت» الذي يعيد العضوية إلى ACADEMIA. المغادرة لا تلغي فترة انتظار الاختبار.", - larpRulesOneHouseTitle: "بيت واحد في كل مرة", - larpRulesTestEntry: "اختبار WILL", - larpRulesTestEntryText: "كل خيار يرجح بيتاً أو أكثر؛ عند الإرسال، يجمع النظام النقاط ويضمك تلقائياً إلى البيت صاحب أعلى نتيجة.", + larpRulesTestEntryText: "في اختبار WILL يمنح كل خيار وزنًا لبيت أو أكثر؛ وعند الإرسال يجمع النظام النقاط ويضمّك تلقائيًا إلى البيت الأعلى نقاطًا.", larpWillTestHint: "بمجرد الانتهاء، ستُسند إليك البيت الذي يتطابق بشكل أفضل مع إجاباتك. تتم معالجة إجاباتك الفردية محلياً ولا يتم تخزينها أبداً — يُنشر في خلاصتك فقط نتيجة تعيين البيت.", larpWillTestTitle: "اختبار WILL", - settingsLanDesc: "الإعلان بشكل دوري عن هذا النظير لمثيلات Oasis الأخرى على نفس الشبكة المحلية. تعطيل لإيقاف بث UDP.", + settingsLanDesc: "الإعلان بشكل دوري عن هذا النظير لمثيلات Oasis الأخرى على نفس الشبكة المحلية.", settingsLanEnable: "تفعيل بث الشبكة المحلية", settingsLanTitle: "بث الشبكة المحلية", settingsReplicationTitle: "النسخ", settingsReplicationDesc: "اضبط عدد القفزات التي يتبعها نظيرك انطلاقًا من خلاصتك عند نسخ المحتوى.", settingsReplicationHopsLabel: "قفزات", + settingsBlobCacheTitle: "ذاكرة الوسائط المؤقتة", + settingsBlobCacheDesc: "يُحفظ محتوى السكان الآخرين على هذا الجهاز. اختر المساحة التي يمكنه شغلها.", + settingsBlobCacheUsage: "الاستخدام الحالي", + settingsBlobCacheFiles: "ملفات", + settingsBlobCacheLimit: "الحد", + settingsBlobCacheUnlimited: "بلا حد", + settingsBlobCacheCleanNow: "تنظيف الآن", + settingsBlobCacheCleaned: "تمت إزالة {n} ملفات وتحرير {size}.", aiApproveTagsLabel: "وسوم (مفصولة بفواصل)", aiApproveTagsPlaceholder: "مثلاً oasis، حوكمة، بيئة", aiApproveRatingLabel: "التقييم", @@ -3749,31 +4757,25 @@ module.exports = { aiExchangeMarkHelpful: "+1 مفيد", larpCyclesUntilRuling: "دورة الحكم القادمة", larpVisitTribe: "زيارة القبيلة", + larpStartJourney: "ابدأ الرحلة", larpGoverning: "يحكم:", + larpStartHere: "ابدأ من هنا:", larpMyHouse: "بيتي:", larpMembersCount: "الأعضاء", - larpMembersTitle: "الأعضاء", - larpNoMembers: "لا يوجد أعضاء بعد.", larpRolesLabel: "الأدوار", larpFunctionLabel: "الوظيفة", larpMonthLabel: "دورة الحكم", larpLeaveToAcademia: "العودة إلى ACADEMIA", - larpAcademiaJoinTitle: "الانضمام إلى بيت", - larpAcademiaJoinHint: "أجرِ اختبار الشخصية النفسي ليُسنَد إليك البيت الأنسب لك، أو استخدم رمز دعوة تشاركَه أحد أعضاء البيوت.", - larpTakeTestButton: "أجرِ اختبار الشخصية", + larpAcademiaJoinTitle: "بيوت L.A.R.P.", larpInviteRedeem: "الانضمام إلى البيت", larpInviteCreate: "إنشاء رمز دعوة", larpInvitePlaceholder: "رمز الدعوة", larpInviteBannerTitle: "رمز دعوة جديد", larpInviteBannerHint: "شارك هذا الرمز مع شخص في ACADEMIA. ينتهي خلال 30 دورة أو بعد استخدام واحد.", - larpTestAssignedHouse: "البيت المُسنَد", larpTestRankingTitle: "النقاط لكل بيت", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "رمز دعوة بيت", - invitesHouseJoinButton: "الانضمام إلى البيت", larpLastAttemptTitle: "آخر محاولاتك", larpLastAttemptHouse: "البيت", - larpLastAttemptResult: "النتيجة", larpLastAttemptWhen: "متى", larpLastAttemptCooldown: "المحاولة التالية خلال", larpTestTitle: "اختبار الدخول", @@ -3782,37 +4784,14 @@ module.exports = { larpTestCooldownActive: "الاختبار التالي متاح خلال", larpTestCooldownDays: "دورات", larpTestResultTitle: "نتيجة الاختبار", - larpTestPassed: "اجتزت الاختبار — أهلاً!", - larpTestFailed: "لم تجتز الاختبار", larpTestScore: "الدرجة", larpTestNextAttempt: "يمكنك إجراء اختبار آخر بعد 30 دورة.", larpGoToHouse: "اذهب إلى بيتك", larpBackToAcademia: "العودة إلى ACADEMIA", - larpBackToHouses: "◀ البيوت", larpBadgeYou: "أنت", larpBadgeRuling: "يحكم", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "وحدة طبقة لعب الأدوار الحية في Oasis (البيوت التسعة).", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - ecoinTaxLabel: "ECOin Tax", - bankTaxesNetworkTitle: "Network Taxes", - bankTaxesUserTitle: "Your taxes", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - statsEcoTaxTitle: "ECO Tax", - audioTranscodeTitle: "BCS Transcode", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - audioTranscodeStegoTitle: "Embedded in the waveform", - audioBackToAudio: "Back to audio", - audioAuthorLabel: "Author", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "وحدة لطبقة لعب الأدوار الحيّ.", larpProfileQ1: "عند مواجهة مشكلة معقدة، ما الذي تفعله أولاً؟", larpProfileQ1O1: "التحدث مع الآخرين للوصول إلى توافق", larpProfileQ1O2: "بناء نموذج أولي لاختباره", @@ -3886,7 +4865,52 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "اختر وضع التنقل UX الذي تريده لواجهتك.", uxModeMenus: "الكتل", - uxModeAINav: "AI", + uxModeChats: "المحادثات", + uxModeFeed: "التدوين المصغر", + feedTrendingTitle: "الوسوم الرائجة", + feedActiveTitle: "السكان النشطون", + cnBrandSub: "حر · P2P · اتحادي", + cnSyncedPeers: "العقد المتزامنة", + cnLicense: "الرخصة", + cnNotAccessible: "المحتوى غير متاح في هذه اللحظة.", + cnUntitled: "بلا عنوان", + cnSearchPlaceholder: "بحث…", + cnHubTitle: "مركز Clearnet", + cnHubDescription: "محتوى عام يشاركه سكان شبكة Oasis هذه.", + cnHubEmpty: "لم تتم مشاركة أي محتوى عام على Clearnet بعد.", + cnPublicContent: "المحتوى", + cnTribesContent: "القبائل", + cnNetworkContent: "الشبكة", + cnCategoryEmpty: "لا يوجد محتوى في هذه الفئة.", + cnInhabitantEmpty: "لم ينشر هذا الساكن أي محتوى على Clearnet بعد.", + cnPrevPage: "← السابق", + cnNextPage: "التالي →", + cnPageOf: "صفحة {page} من {pages}", + cnMoreInfo: "مزيد من المعلومات ←", + cnOpenCourse: "دورة مفتوحة", + cnAuction: "مزاد", + cnExchange: "تبادل", + cnRssDescription: "نشرها سكان هذا المركز", + cnKindAudio: "صوت", + cnKindBlog: "مدونة", + cnKindBookmark: "إشارة مرجعية", + cnKindDocument: "مستند", + cnKindEvent: "حدث", + cnKindFeed: "تغذية", + cnKindImage: "صورة", + cnKindJob: "وظيفة", + cnKindMarket: "سوق", + cnKindPodcast: "بودكاست", + cnKindProject: "مشروع", + cnKindCourse: "دورة", + cnKindShop: "متجر", + cnKindTorrent: "تورنت", + cnKindFile: "ملف", + cnKindVideo: "فيديو", + cnKindWiki: "ويكي", + cnKindRoom: "غرفة", + cnKindMap: "خريطة", + uxModeAINav: "الذكاء الاصطناعي", } }; diff --git a/src/client/assets/translations/oasis_ca.js b/src/client/assets/translations/oasis_ca.js new file mode 100644 index 0000000..bc12bde --- /dev/null +++ b/src/client/assets/translations/oasis_ca.js @@ -0,0 +1,4917 @@ +const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); +module.exports = { + ca: { + shopOrderStatusPaid: "Pagament rebut", + shopOrderStatusReceived: "Rebut", + shopOrderMarkPaid: "Marcar pagament rebut", + shopOrderConfirmReceived: "Confirmar rebut", + shopMyOrdersTitle: "Les meves comandes", + shopPurchasesButton: "Compres", + shopMyOrdersDescription: "Les teves comandes de compra.", + shopMyOrdersEmpty: "Encara no tens compres.", + shopOrderPmSeller: "PM Venedor", + shopOrderSeller: "Venedor", + shopOrderInvoice: "Factura", + shopOrderInvoiceConcept: "Factura", + shopOrderStatusPending: "Pendent", + shopOrderStatusAccepted: "Acceptat", + shopOrderStatusRejected: "Rebutjat", + shopOrderStatusShipped: "Enviat", + shopOrderAccept: "Acceptar", + shopOrderReject: "Rebutjar", + shopOrderMarkShipped: "Marcar enviat", + shopOrderPmBuyer: "PM Comprador", + shopOrderContractConcept: "Comanda de botiga", + shopOrdersPending: "Comandes pendents", + all: "Tots", + mine: "Meus", + recent: "Recents", + anonymous: "Anònim", + unnamed: "Anònim", + authorLabel: "Autoria", + delete: "Eliminar", + save: "Desar", + vote: "Votar", + url: "URL", + price: "Preu", + priority: "Prioritat", + severity: "Gravetat", + organizer: "Organitzador", + edited: "editat", + isPublic: "Públic", + searchIsPublicLabel: "Públic", + privacyPrivate: "PRIVAT", + privacyPublic: "PÚBLIC", + notFound: "No trobat", + no_results: "Sense dades", + searchButton: "Cercar", + mapZoomLabel: "Zoom", + modulesTitle: "Mòduls", + viewJson: "Veure JSON", + matchScore: "Puntuació de coincidència", + preferencesLabel: "Preferències", + inhabitantProfileTitle: "Habitant", + contentWarningLabel: "Avís de contingut", + invalidPost: "Contingut no vàlid", + invalidPostHint: "Aquest missatge té el text buit o no vàlid.", + spreadChron: "Rèpliques", + spreaded: "Replicat", + spreadContentUnavailable: "Contingut encara no disponible (replicació pendent)", + filteredByTag: "Filtrat per etiqueta", + filteredByTagTitle: "Filtrat per etiqueta", + feedPublishedSuccess: "Publicació creada correctament!", + feedMastodonFailed: "S'ha publicat a Oasis, però no s'ha pogut enviar a Mastodon.", + tribeFeedSent: "Missatge enviat correctament!", + tribeContentEncrypted: "Contingut xifrat", + tribeTorrentsEmpty: "Encara no hi ha torrents.", + torrentDescription: "Descripció", + torrentDownload: "Baixar", + chatInviteMode: "Convidar", + padInviteMode: "Convidar", + noInviteMode: "Sense mode d'invitació", + noDeadline: "Sense data límit", + noStatus: "Sense estat", + noSeverity: "Sense gravetat", + calendarDeleteDate: "Eliminar data", + calendarIntervalUntil: "Fins a", + bookmarkLastVisit: "Darrera visita", + profileClearnetBookmarksLabel: "Marcadors", + invitesPort: "Port", + peerHostValidation: "IPv4 vàlida (p. ex. 192.168.1.100) o nom d'amfitrió (p. ex. pub.solarnethub.com)", + peerPortValidation: "Port 1-65535", + peerKeyValidation: "Clau pública SSB ed25519 (@<44 caràcters base64>=.ed25519)", + inboxJobUnsubscribedTitle: "Baixa de la teva oferta de feina", + inboxProjectFollowedTitle: "Nou seguidor del teu projecte", + inboxProjectUnfollowedTitle: "Ha deixat de seguir el teu projecte", + pmHasFollowedYourProject: "ha seguit el teu projecte", + pmHasUnfollowedYourProject: "ha deixat de seguir el teu projecte", + pmHasUnsubscribedFromYourJobOffer: "s'ha donat de baixa de la teva oferta de feina", + marketDeleteButton: "Eliminar", + marketNoBids: "Encara no s'han fet licitacions.", + marketAuctionBidAmount: "Import de la licitació", + marketAuctionBidTime: "Hora de la licitació", + marketAuctionUser: "Licitador", + parliamentActorInPower: "TRIBU GOVERNANT", + parliamentWinningCandidature: "Candidatura guanyadora", + projectBounty: "Recompensa", + projectFollowing: "SEGUINT", + projectBackerAuthor: "Mecenes", + projectBackerDate: "Data", + projectConfirmTransferButton: "Confirmar transferència", + projectPendingTransfersTitle: "Transferències pendents", + transfersUpdateSectionTitle: "Actualitzar transferència", + taskUnassignedFrom: "Desassignat de", + taskDescriptionPlaceholder: "Descripció de la tasca", + exportPasswordLabel: "Contrasenya d'exportació", + importPasswordPlaceholder: "Introdueix la contrasenya", + searchCasesPlaceholder: "Cercar a Tribunals...", + shopBuyDeliveryAddressPlaceholder: "Adreça de lliurament", + shopBuyNotesPlaceholder: "Notes", + bankTaxesUserNoteBold: "reduir directament l'impost ECO", + bankTaxesUserNoteIntro: "El teu impost ECO es dedueix de l'excedent que la xarxa genera per sobre de la teva RBU; el valor base de la RBU es fixa com a mínim inamovible. L'impost mai no es dedueix de la quantitat fixa que correspon a cada habitant com a RBU. Els fons deduïts alimenten l'algorisme de redistribució de la riquesa i es canalitzen de tornada cap a altres habitants mitjançant les seves reclamacions de RBU. Aquests altres habitants que reben més RBU per als seus projectes probablement tindran alguna tasca dedicada a ajudar-te a reduir el teu impost ECO. O poden dedicar-se a ", + bankTaxesUserNoteOutro: ", i el consens de la xarxa pot requerir-ne la contribució contínua.", + courtsViewDetails: "Veure", + courtsViewDetailsShort: "Detalls", + courtsThDetails: "Detalls", + courtsDetailsAnswersTitle: "Respostes", + courtsDetailsEvidenceTitle: "Proves", + courtsDetailsSettlementsTitle: "Acords", + courtsDetailsVerdictTitle: "Veredicte", + courtsDetailsNotPublic: "No públic", + courtsEvidenceSideAccuser: "Proves de l'acusador", + courtsEvidenceSideRespondent: "Proves de la defensa", + courtsEvidenceSideUnknown: "Desconegut", + courtsJudgeNotAssigned: "-", + courtsSupportCase: "Donar suport al cas", + courtsSettlementAcceptBtn: "Acceptar l'acord", + courtsSupportCount: "Suports", + reportsSetStatus: "Canviar l'estat", + reportsStatusClosed: "TANCAT", + statsAvgPerInhabitant: "Mitjana per habitant", + statsCarbonMaxAnnual: "Estimació màx. anual", + statsCarbonNetwork: "Total de la xarxa", + statsCarbonOfEstMax: "de la capacitat màx. estimada", + statsCarbonYearProgress: "de l'any transcorregut", + statsNetworkTitle: "Xarxa", + statsStorageTitle: "Emmagatzematge", + statsEcoTaxTitle: "Taxa ECO", + statsAccountTitle: "Compte", + statsAveragesTitle: "Mitjanes", + statsCarbonOfNetwork: "del total de la xarxa", + statsCarbonUser: "La teva petjada", + statsContentCountColumn: "Quantitat", + statsContentTypeColumn: "Tipus", + statsMsgsPerDay: "Missatges/dia (històric)", + statsMyShare: "La teva part de la xarxa", + statsNetworkSpan: "Antiguitat de la xarxa", + statsTombstoneRatioLabel: "Proporció d'esborrats", + statsTopTagsTitle: "Etiquetes principals", + statsTopTypesTitle: "Tipus de contingut principals", + statsTotalMsgs: "Missatges totals", + extended: "Multivers", + extendedDescription: [ + "Quan dones suport a algú, pots baixar les publicacions dels habitants a qui dona suport, i aquestes publicacions apareixeran aquí, ordenades per la més recent.", + ], + popular: "Destacades", + popularDescription: [ + "Publicacions d'habitants de la teva xarxa, ", + strong("ordenades per suports"), + ". Selecciona el període de temps per obtenir una llista.", + ], + week: "Setmana", + month: "Mes", + year: "Any", + latest: "Darrers", + latestDescription: [ + strong("Publicacions"), + " teves i d'habitants a qui dones suport, ordenades per les més recents.", + ], + topics: "Temes", + topicsDescription: [ + strong("Temes"), + " teus i d'habitants a qui dones suport, ordenats pels més recents.", + ], + summaries: "Resums", + summariesDescription: [ + strong("Temes i alguns comentaris"), + " teus i d'habitants a qui dones suport, ordenats pels més recents.", + ], + threads: "Fils", + threadsDescription: [ + strong("Publicacions que tenen comentaris"), + " d'habitants a qui dones suport i del teu multivers, ordenades per les més recents.", + ], + profile: "Avatar", + inhabitants: "Habitants", + peersReplicatedFeeds: "Replicats", + peersTorColumn: "TOR", + peersTorYes: "Accessible per Tor", + peersTorNo: "No accessible per Tor", + peersOasisId: "ID", + peersOasisVersion: "Versió", + graphos: "Graphos", + graphosDescription: "Mapa interactiu de la xarxa al teu voltant.", + graphosFederation: "Federació", + graphosFedPubs: "PUBs", + graphosFedUnreachable: "Inabastables", + graphosFedLinks: "Enllaços mutus", + graphosFedIsolated: "Aïllats", + graphosFedNoData: "Sense dades", + graphosFedMutual: "Mutu", + graphosFedOneWay: "En un sentit", + graphosFedTransport: "Xarxa", + graphosFedWith: "Federat amb", + graphosZoom: "Amplia", + graphosFedInhabitants: "Habitants", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "El teu node encara no coneix cap PUB.", + graphosViewingGraphOf: "Veient", + graphosBackToMine: "← La meva xarxa", + graphosShowing: "Mostrant", + graphosYou: "Tu", + graphosTotalNodes: "Nodes totals", + mentions: "Mencions", + mentionsDescription: [ + strong("Tots els missatges que et @mencionen"), + ".", + ], + noMentions: "Encara no has rebut @mencions.", + mentionsSearchPlaceholder: "Cercar a Mencions...", + privateReminders: "RECORDATORIS", + inboxFilterPms: "PMs", + inboxFilterNotifications: "Notificacions", + inboxMarkRead: "Marcar com a llegit", + inboxMarkUnread: "Marcar com a no llegit", + inboxUnreadChip: "No llegit", + inboxMarkAllRead: "Marcar-ho tot com a llegit", + inboxDeleteShown: "Esborrar-ho tot", + inboxArchive: "Arxivar", + inboxUnarchive: "Desarxivar", + inboxFilterArchived: "Arxivats", + inboxEarlierMessages: "missatges anteriors", + inboxSortThread: "Per fil", + inboxSortRecent: "Més recents primer", + inboxAllBots: "Bots", + settingsInboxBotsTitle: "Notificacions de l'Inbox", + settingsInboxBotsDesc: "Tria quins bots et poden avisar a l'Inbox. Els bots desmarcats s'amaguen i els seus avisos no es compten.", + inboxFiltersLabel: "Filtres:", + inboxToggleToMutuals: "Canviar a mutus", + inboxToggleToWhole: "Canviar a tots", + privateDate: "Data", + peers: "Nodes", + searchDescription: "Descripció", + searchFromLabel: "Des de", + searchToLabel: "Fins a", + searchQueryLabel: "Cerca", + searchPerPageLabel: "Resultats", + settings: "Configuració", + settingsDescription: "Configura l'entorn de la teva xarxa.", + continueReading: "Continuar llegint", + moreComments: "més comentaris", + readThread: "llegir la resta del fil", + pixeliaTitle: "Pixelia", + pixeliaDescription: "Dibuixa píxels en una graella i colabor-ART amb habitants de la teva xarxa.", + melodyTitle: "Melody", + melodyDescription: "Reprodueix la melodia de la teva blockchain — cada bloc es converteix en una nota.", + melodyEmpty: "Encara no hi ha notes — la teva blockchain encara no ha generat cap bloc.", + melodyTotalBlocks: "Notes", + melodyFilterAll: "TOTES", + melodyRegenerate: "Regenerar", + melodyDownload: "Baixar BCS", + profileContentSectionTitle: "Contingut de l'Avatar", + profileContentHelp: "Tria quins mòduls es mostraran al teu avatar.", + profileSensorsHelp: "Mètriques opcionals que es mostren al teu avatar.", + profileClearnetHelp: "Mòduls que es poden consultar des de fora d'Oasis.", + profileHubAll: "Tot", + footerLicenseLabel: "Llicència", + profileVisibilityFediverse: "Multivers", + contributorsTitle: "Contribucions", + colorLabel: "Tria un color", + paintButton: "Dibuixar!", + totalPixels: "Total de píxels", + modules: "Mòduls", + modulesViewTitle: "Mòduls", + modulesViewDescription: "Defineix el teu entorn ideal activant i desactivant mòduls.", + inbox: "Bústia", + inboxSearchPlaceholder: "Cercar a la Bústia...", + multiverse: "Multivers", + fediverse: "Multivers", + fediverseDescription: "Gestiona els teus altres comptes del multivers, incloent-hi enviar i rebre contingut.", + fediverseStatus: "Estat", + fediverseDisconnected: "Multivers desconnectat. Comprova %LINK% o l'estat de la connexió.", + fediverseSettingsTitle: "Multivers", + fediverseInstanceLabel: "Adreça", + fediverseTokenLabel: "Testimoni d'accés", + fediverseConnect: "Connectar", + fediverseConnectedAs: "Connectat com a", + nextcloudServerLabel: "Servidor", + nextcloudUserLabel: "Usuari", + nextcloudFolderLabel: "Carpeta", + nextcloudUploadButton: "Pujar", + nextcloudFileLabel: "Tria un fitxer", + fileExternalLink: "Enllaç extern", + nextcloudErrConnect: "No s'ha pogut arribar a Nextcloud amb aquestes dades.", + nextcloudErrUpload: "La pujada a Nextcloud ha fallat.", + nextcloudErrShare: "Nextcloud no ha donat cap enllaç públic per al fitxer.", + fediverseDisconnect: "Desconnectar", + fediverseEmptyLink: "la teva configuració", + fediverseComposePlaceholder: "Què estàs pensant?", + fediverseReplyPlaceholder: "Escriu la teva resposta…", + fediverseAttach: "Adjuntar multimèdia", + fediversePublish: "Publicar", + fediverseReply: "Respondre", + fediverseBoosted: "ha impulsat", + fediverseNoPosts: "La teva cronologia és buida.", + fediverseTimeline: "Cronologies", + fediverseFollowers: "Seguidors", + fediverseFollowing: "Seguint", + fediversePosts: "Publicacions", + fediverseJoined: "S'hi va unir", + fediverseRefresh: "Actualitzar", + fediverseWrite: "Escriure", + fediversePreview: "Previsualització", + fediverseManage: "Gestionar", + fediverseError: "No s'ha pogut contactar amb Mastodon.", + fediverseErrInstance: "URL d'instància no vàlid.", + fediverseErrAuth: "Testimoni no vàlid o caducat.", + fediverseErrConnect: "No s'ha pogut connectar amb la instància.", + fediverseErrToken: "Cal un testimoni.", + fediverseErrPublic: "No disponible en mode públic.", + fediverseErrFetch: "No s'ha pogut carregar la cronologia.", + fediverseErrPost: "No s'ha pogut publicar.", + fediverseErrMedia: "No s'ha pogut pujar el fitxer.", + fediverseErrEmpty: "Escriu alguna cosa o adjunta un fitxer.", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Número de telèfon", + telegramSendCode: "Enviar codi", + telegramCodeHelp: "Introdueix el codi que Telegram t'acaba d'enviar (app o SMS).", + telegramCodeLabel: "Codi", + telegramPasswordHelp: "Aquest compte té verificació en dos passos. Introdueix la teva contrasenya.", + telegramPasswordLabel: "Contrasenya", + telegramVerify: "Verificar", + telegramCancel: "Cancel·lar", + telegramChats: "Xats", + telegramUnread: "Sense llegir", + telegramOpen: "Obrir", + telegramSend: "Enviar", + telegramYou: "Tu", + telegramNoChats: "Encara no hi ha xats.", + telegramNoMessages: "Encara no hi ha missatges.", + telegramComposePlaceholder: "Escriu un missatge…", + telegramChannel: "Canal", + telegramGroup: "Grup", + telegramPrivate: "Privat", + telegramErrConnect: "No s'ha pogut contactar amb Telegram.", + telegramErrTimeout: "Telegram ha trigat massa a respondre.", + telegramErrAuth: "Sessió de Telegram no vàlida o caducada. Torna-la a connectar.", + telegramErrFetch: "No s'han pogut carregar els xats.", + telegramErrSend: "No s'ha pogut enviar el missatge.", + telegramErrMissing: "Calen l'API ID, l'API hash i el número de telèfon.", + telegramErrCode: "Codi incorrecte. Torna-ho a provar.", + telegramErrCodeExpired: "El codi ha caducat. Torna a començar.", + telegramErrPassword: "Contrasenya incorrecta.", + telegramErrPhone: "Número de telèfon no vàlid.", + telegramErrApi: "API ID o API hash no vàlids.", + telegramErrFlood: "Massa intents. Espera una estona i torna-ho a provar.", + telegramErrNoLogin: "No hi ha cap inici de sessió en curs. Torna a començar.", + peertubeUsernameLabel: "Usuària", + peertubePasswordLabel: "Contrasenya", + peertubeSubscriptions: "Subscripcions", + peertubeMyVideos: "Els meus vídeos", + peertubeVideos: "Vídeos", + peertubeFollowers: "Seguidores", + peertubeViews: "visualitzacions", + peertubeLikes: "m'agrada", + peertubeQuota: "Quota", + peertubeUpload: "Pujar vídeo", + peertubeUploadHelp: "Tria un fitxer de vídeo. Es puja al teu canal i PeerTube el processa després.", + peertubeTitleLabel: "Títol", + peertubeDescriptionLabel: "Descripció", + peertubePrivacyLabel: "Privacitat", + peertubePrivacyPublic: "Públic", + peertubePrivacyUnlisted: "No llistat", + peertubePrivacyPrivate: "Privat", + peertubeNoVideos: "Encara no hi ha vídeos.", + peertubeComments: "Comentaris", + peertubeComment: "Comentar", + peertubeCommentPlaceholder: "Escriu un comentari…", + peertubeNoComments: "Encara no hi ha comentaris.", + peertubeOpen: "Obrir a PeerTube", + peertubeWatch: "Veure", + peertubeChannel: "Canal", + peertubeNoStream: "Aquest vídeo encara no es pot reproduir aquí. Obre'l a PeerTube.", + peertubeUploaded: "Vídeo pujat. PeerTube l'està processant.", + peertubeErrConnect: "No s'ha pogut contactar amb PeerTube.", + peertubeErrAuth: "Usuària o contrasenya incorrectes, o la sessió ha caducat. Connecta-la de nou.", + peertubeErrInstance: "URL d'instància no vàlida.", + peertubeErrMissing: "Calen instància, usuària i contrasenya.", + peertubeErrFetch: "No s'han pogut carregar els vídeos.", + peertubeErrUpload: "No s'ha pogut pujar el vídeo.", + peertubeErrTitle: "Cal un títol.", + peertubeErrComment: "No s'ha pogut publicar el comentari.", + peertubeErrNoFile: "Tria un fitxer de vídeo.", + bookmarksLabel: "Marcadors", + videosLabel: "Vídeos", + torrentsLabel: "Torrents", + docsLabel: "Documents", + audiosLabel: "Àudios", + tagsLabel: "Etiquetes", + imagesLabel: "Imatges", + inhabitantsLabel: "Habitants", + trendingLabel: "Tendències", + eventsLabel: "Esdeveniments", + apply: "Aplicar", + menuPersonal: "Personal", + menuGovernance: "Governança", + menuOffice: "Oficina", + menuNetwork: "Comunitat", + menuCreative: "Creatiu", + menuEconomy: "Economia", + menuMedia: "Biblioteca", + menuTools: "Eines", + comment: "Comentar", + subtopic: "Subtema", + json: "JSON", + unfollow: "Deixar de donar suport", + follow: "Donar suport", + block: "Bloquejar", + unblock: "Desbloquejar", + relationshipNotFollowing: "No et dona suport", + relationshipTheyFollow: "Et dona suport", + relationshipMutuals: "Suport mutu", + relationshipFollowing: "Li dones suport", + relationshipYou: "Tu", + relationshipBlocking: "L'estàs bloquejant", + relationshipBlockedBy: "Et bloqueja", + relationshipMutualBlock: "Bloqueig mutu", + relationshipNone: "No li dones suport", + relationshipBlockingPost: "Post bloquejat", + viewLikes: "Veure rèpliques", + viewSupporters: "Veure suports", + spreadedDescription: "Llista de continguts replicats per habitant.", + preview: "Previsualitzar", + publish: "Publicar", + contentWarningPlaceholder: "Afegeix un títol al teu post (opcional)", + publishWarningPlaceholder: "Escriu el cos de la teva entrada.", + publishTooLong: "La teva publicació és massa llarga. Escurça-la, si us plau.", + keepTextRetry: "Envia de nou", + dateInPastError: "La data no pot ser anterior a avui", + publishCustomDescription: [ + "RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bé!", + ], + commentWarning: [ + "RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bé!", + ], + commentLabel: ({ publicOrPrivate, markdownUrl }) => [ + ], + publishLabel: ({ markdownUrl, linkTarget }) => [ + "RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bé!", + ], + replyLabel: ({ markdownUrl }) => [ + "RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bé!", + ], + publishCustomInfo: ({ href }) => [ + "Si tens experiència, també pots ", + a({ href }, "escriure un post avançat"), + ".", + ], + publishBasicInfo: ({ href }) => [ + "Si no tens experiència, hauries d'", + a({ href }, "escriure un post normal"), + ".", + ], + publishCustom: "Escriure un post avançat", + messagePreview: "Previsualització", + mentionsMatching: "Mencions", + mentionsName: "Nom", + mentionsRelationship: "Relació", + updateBannerText: "Hi ha una nova versió d'Oasis disponible.", + updateBannerAction: "Actualitzar ara!", + info: "Info", + settingsIntro: ({ version }) => [ + `[SNH] ꖒ OASIS [ v.${version} ]`, + ], + timeAgo: "fa", + sendTime: "escrit ", + theme: "Tema", + backupKeysExportButton: "Exportar", + backupKeysImportButton: "Importar", + ssbLogStream: "Blockchain", + ssbLogStreamDescription: "Configura el límit de missatges per als fluxos de la blockchain.", + logWindowChip: "Darrers {n} streams de la blockchain", + saveSettings: "Desar la configuració", + exportDescription: "Estableix una contrasenya (mínim 32 caràcters de llargada) per xifrar el teu secret", + exportDataTitle: "Còpia instantània", + exportDataDescription: "Baixar la teva blockchain (clau privada exclosa!)", + exportDataButton: "Baixar la blockchain", + importDescription: "Importa el teu secret xifrat (clau privada) per activar el teu avatar", + passwordImport: "Escriu la teva contrasenya per desxifrar les dades que es desaran al teu sistema (nom: secret)", + exportPasswordPlaceholder: "Utilitza minúscules, majúscules, números i símbols", + fileInfo: "El teu secret xifrat es baixarà al teu dispositiu (nom: oasis.enc)", + developer: "Desenvolupament", + devTitle: "Desenvolupament", + welcomeBannerText: "El teu avatar encara no està configurat.", + aiSuggestionBanner: "Suggeriment:", + aiSuggestionsEnable: "Suggeriments de la IA", + welcomeBannerAction: "Començar", + welcomeTitle: "Benvinguda", + welcomeStepIdentity: "Identitat", + welcomeStepType: "Tipus", + welcomeStepUse: "Ús", + welcomeStepLook: "Aparença", + welcomeStepCommunity: "Comunitat", + welcomeStepIntro: "Benvinguda", + welcomeHintIdentity: "El teu nom i la teva imatge.", + welcomeHintType: "Com s'obre Oasis per a tu.", + welcomeHintUse: "Quins mòduls queden actius.", + welcomeHintLook: "El tema que vesteix Oasis.", + welcomeHintCommunity: "Entra en un PUB per conèixer altres habitants.", + welcomeHintIntro: "Envia una primera salutació a la comunitat.", + welcomeStepThemeAction: "Fes servir aquest tema", + welcomeJoinPubs: "Entra als PUBs", + welcomeNoFederation: "Sense federar-se", + welcomeStepGreetingAction: "Enviar feed", + welcomeDescription: "Unes quantes coses que convé fer abans de començar.", + welcomeSkip: "Ara no", + welcomeStepProfileAction: "Desar el perfil", + welcomeBootstrapRunning: "Carregant la instantània del pub. En acabar s'uneix al pub automàticament; mentrestant pots continuar fent servir Oasis.", + welcomeCompleteTitle: "Tot a punt.", + welcomeCompleteText: "El teu node està preparat. A partir d'aquí la xarxa creix amb els habitants a qui dones suport.", + welcomeFindPeople: "Cercar habitants", + devFilterFiles: "FITXERS", + devFilterSearch: "CERCAR", + devFilterModules: "MÒDULS", + devFilterTranslations: "TRADUCCIONS", + devFilterStyles: "ESTILS", + devFilterTests: "TESTS", + devVersion: "Versió", + devNodeVersion: "Node", + devStatsTests: "Tests", + devParentFolder: "Carpeta superior", + devOpenFolder: "obrir", + devDescription: "Explora el codi que s'està executant en aquest node.", + devRoot: "arrel", + devSearchPlaceholder: "Text a cercar al codi", + devAnyExtension: "Qualsevol fitxer", + devStatsFiles: "Fitxers", + devStatsLines: "Línies", + devStatsModules: "Mòduls", + devStatsLanguages: "Idiomes", + devNotViewable: "no visualitzable", + devEmptyFolder: "Aquesta carpeta és buida.", + devSize: "Mida", + devShowing: "Mostrant", + devLine: "línia", + devReportBug: "Informar d'un error", + devCreateTask: "Crear tasca", + devRaw: "RAW", + devReportContext: "Trobat llegint el codi d'Oasis", + devTaskPrefix: "Revisar", + devPrevPage: "Línies anteriors", + devNextPage: "Línies següents", + devMatches: "coincidències", + devFilesScanned: "fitxers revisats", + devNoResults: "Encara no hi ha coincidències.", + devColModule: "Mòdul", + devColState: "Estat", + devColModel: "Model", + devColView: "Vista", + devColRoutes: "Rutes", + devColTests: "Tests", + devOn: "actiu", + devOff: "inactiu", + modulesDevLabel: "Desenvolupament", + modulesDevDescription: "Mòdul per gestionar el codi d'Oasis.", + themeIntro: + "Tria un tema.", + setTheme: "Establir el tema", + language: "Idioma", + languageDescription: + "Si vols utilitzar un altre idioma, selecciona'l aquí.", + setLanguage: "Establir l'idioma", + peerConnectionsIntro: "Gestiona totes les teves connexions amb altres nodes.", + peersConnectFailed: "No s'ha pogut connectar: {reason}.", + peerStateLabel: "Estat", + peerAddressLabel: "Adreça", + peerLastChangeLabel: "Darrer canvi", + peerStateConnected: "Connectat", + peerStateConnecting: "Connectant", + peerStateStaged: "Disponible", + peerErrSilent: "Sense activitat", + peerErrTimeout: "No respon", + peerErrRefused: "Rebutja la connexió", + peerErrNotFound: "Adreça desconeguda", + peerErrUnreachable: "Xarxa inaccessible", + peerErrKeys: "Fent servir claus diferents", + peerErrIdentity: "No accepta la teva connexió", + peerErrTor: "Només TOR", + peerErrDropped: "Connexió tallada", + peerErrPaused: "Xarxa en pausa", + peerErrOther: "Error de connexió", + peerOutdated: "Desactualitzat", + peerVersionUnannounced: "No anuncia la seva versió: un Oasis antic", + peersTorMissing: "Aquest PUB només permet connexions des de TOR.", + peerConnectionsTitle: "Connexions", + peersOnlineHint: "Nodes connectats amb tu ara mateix.", + peersDiscoveredHint: "Nodes amb adreça coneguda que no estan connectats ara.", + peersUnknownHint: "Nodes del teu graf social sense adreça coneguda.", + peersConnectionsHint: "Adreces a les quals pot connectar aquest node i l'estat de cada connexió.", + peersTabLan: "LAN", + peersTabHealth: "Salut", + peersHealthHint: "Com està cada pub o node conegut, a la xarxa local i fora.", + peersHealthReachable: "Accessible", + peersHealthFailing: "Fallant", + peersHealthFailures: "Errors", + peersFailingSince: "Falla des de", + peersHealthForget: "Oblidar inaccessibles", + peersLanHint: "Nodes trobats a la teva xarxa local.", + peersLanLastSeen: "Vist per última vegada", + peersLanEmpty: "Encara no s'ha trobat cap node a la xarxa local.", + online: "Connectat", + offline: "Desconnectat", + discovered: "Descoberts", + unknown: "Desconeguts", + lanBroadcastActive: "Activa (multicast UDP a la LAN)", + peerSourceRpc: "RPC", + peerSourceGossip: "Gossip", + peerSourceEbt: "EBT", + peerSourceRecent: "Recent", + peerSourceLan: "LAN", + pub: "PUB", + supported: "Amb suport", + recommended: "Recomanat", + blocked: "Bloquejat", + noConnections: "No hi ha nodes connectats.", + noDiscovered: "No has descobert cap node.", + noUnknownPeers: "No hi ha nodes desconeguts al graf d'amistats.", + peersTechnicalEmpty: "Encara no hi ha nodes registrats.", + peerConnect: "Connectar", + peerDisconnect: "Desconnectar", + peerRefresh: "Refrescar", + peerPruneIdle: "Treure els inactius", + peersPause: "Pausa", + peersResume: "Reprèn", + peersPausedNote: "En pausa: Oasis no es connecta amb ningú i ningú s'hi pot connectar.", + peersPauseHint: "Atura totes les connexions: Oasis no es connecta a altres nodes ni els accepta fins que reprenguis.", + peerExport: "Exportar", + peerImport: "Importar", + peerImportTitle: "Importar una llista de nodes", + peerImportPlaceholder: "Enganxa una adreça multiserver per línia, o puja un fitxer .txt…", + connectionActionIntro: + "", + indexes: "Índexs", + indexesRebuilt: "Índexs reconstruïts", + indexesRebuildFailed: "No s'han pogut reconstruir els índexs", + indexesFiles: "Fitxers d'índex", + indexesDescription: + "Reconstruir els índexs és segur i pot arreglar alguns errors de la teva blockchain.", + homePageTitle: "Pàgina d'inici", + homePageDescription: "Selecciona quin mòdul vols com a pàgina d'inici.", + saveHomePage: "Establir la pàgina d'inici", + invites: "Invitacions", + invitesDescription: "Gestiona i aplica codis d'invitació a la teva xarxa.", + invitesPubsHint: "Enganxa un codi d'invitació d'un pub per federar-t'hi i començar a replicar els seus habitants.", + invitesFederationsHint: "Pubs amb què estàs federat: actualitza'ls, treu els inabastables o exporta la llista.", + invitesTribesTitle: "Tribus", + invitesChatsTitle: "Xats", + invitesPadsTitle: "Pads", + invitesCalendarsTitle: "Calendaris", + invitesEventsTitle: "Esdeveniments", + invitesForumsTitle: "Fòrums", + invitesMapsTitle: "Mapes", + invitesShopsTitle: "Botigues", + invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "PUBs Suggerits", + invitesFilterOthers: "Altres", + invitesJoinTitle: "Invitacions", + invitesJoinHint: "Enganxa qualsevol codi d'invitació, sigui del que sigui, o l'Oasis ID d'un habitant.", + invitesJoinPlaceholder: "Codi d'invitació o Oasis ID", + invitesJoinButton: "Entra", + invitesUsedTitle: "Últimes invitacions", + invitesUsedEmpty: "Encara no has utilitzat cap invitació.", + invitesUsedType: "Tipus", + invitesUsedName: "Nom", + invitesUsedDate: "Data", + invitesPubInviteCodePlaceholder: "Introdueix el codi d'invitació del PUB", + invitesAcceptInvite: "Entrar al PUB", + invitesPubUses: "Invitacions usades", + invitesPubExhausted: "Aquest PUB no té invitacions disponibles.", + invitesAlreadyFederated: "Ja estàs federat amb aquest pub.", + invitesJoinPubMode: "Aquest Oasis funciona com a PUB, en mode només lectura. Per unir-lo a un altre PUB, fes servir al terminal: ./oasis.sh join ", + invitesJoinLocalOnly: "Les invitacions només s'accepten des de l'equip on funciona Oasis, o amb l'enllaç d'administració que mostra en arrencar.", + invitesFederationsTitle: "Federacions", + invitesAcceptedInvites: "Federades", + invitesNoFederatedPubs: "No hi ha pubs federats.", + invitesPubsRefresh: "Refrescar", + invitesPubsClearUnreachable: "Netejar", + invitesPubsExport: "Exportar", + invitesPubsImport: "Importar", + invitesPubsImportTitle: "Importar pubs", + invitesPubsImportPlaceholder: "Enganxa una adreça multiserver o un codi d'invitació per línia, o puja un fitxer .txt…", + genericError: "S'ha produït un error.", + panicMode: "Mode pànic!", + encryptData: "Estableix una contrasenya (mínim 32 caràcters de llargada) per xifrar la teva blockchain", + decryptData: "Introdueix la contrasenya per desxifrar la teva blockchain", + removeDataDescription: "COMPTE: Aquest procés no es pot desfer.", + removePanicButton: "ESBORRAR TOTES LES TEVES DADES!", + searchTitle: "Cercar", + searchDescriptionLabel: "Cerca contingut a la teva xarxa.", + searchPlaceholder:"Cercar contingut...", + searchLocationLabel:"Localització", + searchPriceLabel:"Preu", + searchCategoryLabel:"Categoria", + searchPriorityLabel:"Prioritat", + searchStatusLabel:"Estat", + statusLabel:"Estat", + noResultsFound:"No s'han trobat resultats.", + votesOption:"Opinions votades", + voteYesLabel:"Sí", + voteNoLabel:"No", + allTypesLabel: "SENSE LÍMITS", + YESLabel:"SÍ", + NOLabel:"NO", + votesOptionYesLabel:"Sí", + votesOptionNoLabel:"No", + votesOptionAbstentionLabel:"Abstenció", + votesQuestionLabel:"Pregunta", + voteStatusOpen:"OBERT", + voteStatusClosed:"TANCAT", + commentDescription: ({ parentUrl }) => [ + " ha comentat en ", + a({ href: parentUrl }, " fil"), + ], + commentTitle: ({ authorName }) => [`Ha comentat al post de @${authorName}`], + subtopicDescription: ({ parentUrl }) => [ + " ha creat un subtema des de ", + a({ href: parentUrl }, " un post"), + ], + subtopicTitle: ({ authorName }) => [`Ha creat un subtema al post de @${authorName}`], + oasisDescription: "OASIS Xarxa de Projectes", + searchSubmit: "Cercar...", + votesLabel: "GOVERN", + pdfFallbackLabel: "Document PDF", + shopLabel: "BOTIGUES", + voteLabel: "VOTS", + submit: "Aplicar", + editProfile: "Editar l'Avatar", + oasisIdLabel: "OasisID", + spreadContent: "Replicar", + welcomePmSubject: "Hello World!", + welcomePmBody: "Et donem la benvinguda a Oasis — una xarxa social lliure, peer-to-peer, federada i xifrada, amb una arquitectura distribuïda i només per invitació.\n\nAlguns llocs interessants per començar:\n\n- [/avatar](/profile) — Edita el teu avatar, el nom, la descripció i quins mòduls es mostren al teu avatar públic.\n- /settings — Ajusta la teva instància local: idioma, tema, mòduls, privadesa i sincronització.\n- /invites — Afegeix un pub per federar-te amb la resta de la xarxa.\n- /peers — Mira qui està connectat, torna a escanejar la teva LAN, exporta o importa llistes de nodes.\n- /inhabitants — Descobreix i visita els avatars d'altres habitants.\n- /tribes — Crea grups privats amb xifratge d'extrem a extrem o uneix-t'hi.\n- /search — Cerca el contingut de la xarxa per tipus.\n\nAlguns llocs interessants per connectar:\n\n- /multiverse — Gestiona els teus altres comptes del multivers, incloent-hi enviar i rebre contingut.\n\nAquest missatge s'ha enviat de manera privada de tu cap a tu, per això no ha calgut connexió per rebre'l.", + profileSensorsSectionTitle: "Sensors", + profileVisibilityActivity: "Nivell d'activitat", + profileDeviceLockedHint: "Aquest sensor està sempre actiu: permet que altres vegin des de quin dispositiu et connectes i no es pot desactivar.", + profileVisibilityKarma: "Puntuació KARMA", + profileVisibilityUbi: "RBU", + profileVisibilityWallet: "Moneder ECOin", + profileVisibilityEcoTax: "Taxa ECO", + profileVisibilityLarpSign: "Segell L.A.R.P.", + profileVisibilityGpg: "Clau GPG", + clearnetSectionTitle: "Clearnet", + profileClearnetShopsLabel: "Botigues", + profileClearnetJobsLabel: "Feina", + profileClearnetEventsLabel: "Esdeveniments", + profileClearnetProjectsLabel: "Projectes", + profileClearnetPostsLabel: "Blogs", + profileClearnetAudiosLabel: "Àudios", + profileClearnetVideosLabel: "Vídeos", + profileClearnetImagesLabel: "Imatges", + profileClearnetDocumentsLabel: "Documents", + profileClearnetTorrentsLabel: "Torrents", + profileClearnetPodcastsLabel: "Podcasts", + profileClearnetMarketLabel: "Mercat", + profileClearnetFeedLabel: "Feed", + profileClearnetWikiLabel: "Wikis", + editProfileDescription: + "", + profileName: "Nom", + profileImage: "Imatge de l'Avatar", + profileGpgKey: "Clau pública GPG (.asc)", + profileGpgChip: "GPG", + profileGpgDownload: "Baixar", + profileGpgRemove: "Eliminar", + profileDescription: "Descripció", + hashtagDescription: + "Posts d'habitants de la teva xarxa que fan referència a aquest #hashtag, ordenats pel més recent.", + rebuildName: "Reconstruir la blockchain", + wallet: "Moneder", + walletAddress: "Adreça", + walletAmount: "Quantitat", + walletAddressLine: ({ address }) => `Adreça: ${address}`, + walletAmountLine: ({ amount }) => `Quantitat: ${amount} ECO`, + walletBack: "Tornar", + walletBalanceTitle: "Saldo", + walletWalletSendTitle: "Enviar", + walletReceiveTitle: "Rebre", + walletHistoryTitle: "Historial", + walletBalanceLine: ({ balance }) => `${balance} ECO`, + walletCnfrs: "Conf.", + walletConfirm: "Confirmar", + walletDescription: "Gestiona el teu moneder ECOin.", + walletDate: "Data", + walletFee: "Comissió (com més alta sigui la comissió, més ràpid es processarà la teva transacció)", + walletFeeLine: ({ fee }) => `Comissió: ECO ${fee}`, + walletHistory: "Historial", + walletReceive: "Rebre", + walletReset: "Restablir", + walletSend: "Enviar", + walletPayingTransfer: "Pagant el transfer", + walletCreateTransferLabel: "Generar contracte (transfer)", + walletTransferNotifiedNote: "S'ha avisat del pagament a l'altra part", + walletTransferCreatedNote: "S'ha creat un transfer (rebut) d'aquest pagament", + walletTransferNoPayeeNote: "Cap habitant no ha publicat aquesta adreça, així que no s'ha creat cap transfer", + walletTransferReceiptConcept: "Pagament des del moneder", + walletPaymentPmText: "t'ha enviat un pagament", + walletPayeeLabel: "Destinatari", + walletConceptLabel: "Concepte", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Has rebut la teva RBU.", + bankingBotPaymentTitle: "Has rebut un pagament.", + bankingBotUbiPaidText: "La teva Renda Bàsica Universal s'ha pagat", + bankUbiPubThisNode: "Aquest node paga la RBU", + bankUbiPubConnected: "PUB de la RBU", + bankUbiPubNone: "De moment no hi ha PUBs que anunciïn una RBU.", + bankUbiAddressPublished: "Adreça ECOin publicada", + bankUbiAddressMissing: "Publica la teva adreça ECOin per rebre la RBU", + bankUbiClaimedThisMonth: "Reclamada aquest mes", + bankUbiPubBalance: "Moneder del PUB disponible per a la RBU", + bankNoPubAddress: "El PUB de la RBU encara no ha publicat cap adreça ECOin.", + bankUbiTab: "RBU", + bankDonateEco: "DONAR!", + donateTooltip: "Donar ECOin", + bankNoUserAddress: "Aquest habitant no comparteix cap adreça ECOin.", + bankUbiPubDefault: "Per defecte", + bankUbiPool: "Assignació", + bankUbiLastSeen: "Vist per última vegada", + bankUbiLastPayout: "Darrera acció", + bankUbiPaidOut: "Repartit", + bankRangeToday: "Avui", + bankRangeWeek: "Setmana", + bankRangeMonth: "Mes", + bankRangeYear: "Any", + bankRangeFiveYears: "5 anys", + bankRangeAll: "Tot", + bankChartValueTitle: "Evolució del valor d'ECOin", + bankChartValueHours: "Equivalència en temps (h)", + bankChartWealthTitle: "Redistribució de la riquesa ECOin", + bankChartDistributed: "RBU repartida (ECO)", + bankChartRebalanceTitle: "Redistribució de la riquesa entre PUBs", + bankChartRebalanced: "ECO moguts entre PUBs", + bankChartTaxes: "Impostos retinguts (ECO)", + bankChartNoDataRange: "No hi ha dades en aquest interval.", + bankChartNoDataYet: "Encara no hi ha dades", + bankChartPerHour: "per hora", + bankChartPerDay: "per dia", + bankChartPerMonth: "per mes", + bankTotalUbiDistributed: "RBU total redistribuïda (xarxa)", + bankTotalTaxesWithheld: "Impostos totals retinguts (xarxa)", + bankInflationIssuance: "Taxa d'emissió (anual, per recompensa de bloc)", + bankRulesEligibilityTitle: "Qui pot rebre la RBU", + bankRulesEligibility1: "Una adreça ECOin publicada al teu perfil (Perfil → Sensors → Moneder ECOin).", + bankRulesEligibility2: "Un feed de més de 30 dies i activitat a la xarxa: karma per sobre de zero.", + bankRulesEligibility3: "Una reclamació per mes (epoch), des de Banca amb el botó Reclamar RBU. Es pot rebutjar la del mes en curs amb Rebutjar RBU.", + bankRulesEligibility4: "El mínim d'1 ECO es paga sempre; els impostos només redueixen la part que el supera i mai no es cobren a part.", + bankRulesEligibility5: "El PUB paga en el seu següent cicle i publica un transfer RBU que pots confirmar.", + bankRulesEligibility6: "Si falta la teva adreça, el feed és massa jove, has rebutjat el mes o el PUB no té fons, la reclamació se salta i queda registrada al PUB.", + copyLinkTitle: "Copiar l'enllaç", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "Rebutjar RBU", + bankUbiRefusedThisMonth: "Has rebutjat la RBU d'aquest mes.", + bankingBotUbiAvailableTitle: "Pots reclamar la teva RBU.", + bankingBotUbiAvailableText: "La teva RBU d'aquest mes està disponible", + bankingBotConfirmTitle: "Tens un contracte pendent de confirmació", + bankingBotConfirmText: "Contractes pendents de confirmació", + bankChartZoomHint: "Fes clic per ampliar", + bankChartPoolTitle: "Assignació de la RBU (en el temps)", + bankChartNetworkUbiTitle: "RBU total redistribuïda (xarxa)", + bankChartSupplyTitle: "Oferta circulant d'ECOin (en el temps)", + bankChartInflationTitle: "Inflació d'ECOin (en el temps)", + bankChartInflationSeries: "Taxa anualitzada (%)", + bankChartPool: "Assignació total (ECO)", + bankChartKarmaTitle: "Puntuació KARMA (en el temps)", + bankChartFundsTitle: "Els teus fons ECOin (en el temps)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin connectat", + bankWalletNotConfigured: "ECOin desconnectat", + transfersPayWithWallet: "PAGAR!", + walletStatus: "Estat", + walletDisconnected: [ + "Tens el ", + strong("moneder d'ECOin desconnectat"), + ". Revisa ", + a({ href: '/settings#wallet' }, "la teva configuració"), + " o l'estat de la teva connexió.", + ], + walletSentToLine: ({ destination, amount }) => `Enviar ECO ${amount} a ${destination}`, + walletNotConfigured: "La teva cartera d'ECOin encara no està configurada.", + walletSettingsDescription: "Integrar Oasis amb el teu moneder d'ECOin.", + walletSettingsMobileDescription: "Vincula Oasis amb l'app ECOin Wallet d'aquest dispositiu.", + walletDownloadEcoin: "Baixar ECOin!", + walletRpcUrl: "URL del RPC", + walletStatusMessages: { + invalid_amount: "Quantitat no vàlida", + invalid_dest: "Adreça de destinació no vàlida", + invalid_fee: "Comissió no vàlida", + validation_errors: "Errors en la validació", + send_tx_success: "Transacció feta", + }, + walletTitle: "Moneder", + walletTotalCostLine: ({ totalCost }) => `Cost total: ECO ${totalCost}`, + walletTransactionId: "ID de la transacció", + walletTxId: "ID Tx", + walletType: "Tipus", + walletUser: "Usuari", + walletPass: "Contrasenya", + walletConfiguration: "Configurar el moneder", + walletDisconnectButton: "Desconnectar el moneder", + cipher: "Xifrador", + cipherTitle: "Xifrador", + cipherDescription: "Xifra i desxifra el teu text de manera simètrica (amb una contrasenya compartida).", + randomPassword: "Contrasenya aleatòria", + cipherTextPlaceholder: "Introdueix el text que vols xifrar...", + cipherPasswordLabel: "Estableix una contrasenya (mínim 32 caràcters) per xifrar el teu text", + cipherPasswordDecryptLabel: "Estableix una contrasenya (mínim 32 caràcters) per desxifrar el teu text", + cipherPasswordPlaceholder: "Introdueix una contrasenya...", + cipherEncryptButton: "Xifrar", + cipherEncryptedMessageLabel: "Text xifrat", + cipherDecryptedMessageLabel: "Text desxifrat", + cipherEncryptedTextPlaceholder: "Introdueix el text xifrat...", + cipherDecryptButton: "Desxifrar", + password: "Contrasenya", + text: "Text", + encryptedText: "Text xifrat", + iv: "Vector d'inicialització (IV)", + bookmarkTitle: "Marcadors", + bookmarkDescription: "Descobreix i gestiona marcadors a la teva xarxa.", + bookmarkAllSectionTitle: "Marcadors", + bookmarkMineSectionTitle: "Els teus marcadors", + bookmarkRecentSectionTitle: "Marcadors recents", + bookmarkTopSectionTitle: "Marcadors top", + bookmarkFavoritesSectionTitle: "Preferits", + bookmarkCreateSectionTitle: "Crear un marcador", + bookmarkUpdateSectionTitle: "Actualitzar el marcador", + bookmarkFilterAll: "TOTS", + bookmarkFilterMine: "MEUS", + bookmarkFilterTop: "TOP", + bookmarkFilterFavorites: "PREFERITS", + bookmarkFilterRecent: "RECENTS", + bookmarkCreateButton: "Crear un marcador", + bookmarkUpdateButton: "Actualitzar", + bookmarkAddFavoriteButton: "Afegir a preferits", + bookmarkRemoveFavoriteButton: "Treure de preferits", + bookmarkUrlLabel: "Enllaç", + bookmarkUrlPlaceholder: "https://solarnethub.com", + bookmarkDescriptionLabel: "Descripció", + bookmarkDescriptionPlaceholder: "Opcional", + bookmarkTagsLabel: "Etiquetes", + bookmarkTagsPlaceholder: "Introdueix etiquetes separades per comes", + bookmarkLastVisitLabel: "Darrera visita", + bookmarkSearchPlaceholder: "Cercar a Marcadors...", + bookmarkSortRecent: "Més recents", + bookmarkSortOldest: "Més antics", + bookmarkSortTop: "Més votats", + bookmarkSearchButton: "Cercar", + bookmarkUpdatedAt: "Actualitzat", + bookmarkNoMatch: "No hi ha marcadors que coincideixin amb la teva cerca.", + noBookmarks: "No hi ha marcadors disponibles.", + noUrl: "Sense enllaç", + noCategory: "Sense categoria", + noLastVisit: "Sense darrera visita", + videoTitle: "Vídeos", + videoDescription: "Explora i gestiona contingut de vídeo a la teva xarxa.", + videoMineSectionTitle: "Els teus vídeos", + videoCreateSectionTitle: "Pujar un vídeo", + videoUpdateSectionTitle: "Actualitzar el vídeo", + videoAllSectionTitle: "Vídeos", + videoRecentSectionTitle: "Vídeos recents", + videoTopSectionTitle: "Vídeos més votats", + videoFavoritesSectionTitle: "Preferits", + videoFilterAll: "TOTS", + videoFilterMine: "MEUS", + videoFilterRecent: "RECENTS", + videoFilterTop: "TOP", + videoFilterFavorites: "PREFERITS", + videoCreateButton: "Pujar un vídeo", + videoUpdateButton: "Actualitzar", + videoDeleteButton: "Eliminar", + videoAddFavoriteButton: "Afegir a preferits", + videoRemoveFavoriteButton: "Treure de preferits", + videoFileLabel: "Selecciona un fitxer de vídeo (.mp4, .webm, .ogv, .mov)", + videoTagsLabel: "Etiquetes", + videoTagsPlaceholder: "Introdueix etiquetes separades per comes", + videoTitleLabel: "Títol", + videoTitlePlaceholder: "Opcional", + videoDescriptionLabel: "Descripció", + videoDescriptionPlaceholder: "Opcional", + videoNoFile: "No s'ha proporcionat cap fitxer de vídeo", + noVideos: "No hi ha vídeos disponibles.", + videoSearchPlaceholder: "Cercar a Vídeos...", + videoSortRecent: "Més recents", + videoSortOldest: "Més antics", + videoSortTop: "Més votats", + videoSearchButton: "Cercar", + videoUpdatedAt: "Actualitzat", + videoNoMatch: "Cap vídeo no coincideix amb la teva cerca.", + documentTitle: "Documents", + documentDescription: "Descobreix i gestiona documents a la teva xarxa.", + documentAllSectionTitle: "Documents", + documentMineSectionTitle: "Els teus documents", + documentRecentSectionTitle: "Documents recents", + documentTopSectionTitle: "Documents top", + documentFavoritesSectionTitle: "Preferits", + documentCreateSectionTitle: "Pujar un document", + documentUpdateSectionTitle: "Editar el document", + documentFilterAll: "TOT", + documentFilterMine: "MEUS", + documentFilterRecent: "RECENTS", + documentFilterTop: "TOP", + documentFilterFavorites: "PREFERITS", + documentCreateButton: "Pujar un document", + documentUpdateButton: "Actualitzar", + documentAddFavoriteButton: "Afegir a preferits", + documentRemoveFavoriteButton: "Treure de preferits", + documentMessageAuthorButton: "MP", + documentFileLabel: "Pujar un document (.pdf)", + documentTagsLabel: "Etiquetes", + documentTagsPlaceholder: "Introdueix etiquetes separades per comes", + documentTitleLabel: "Títol", + documentTitlePlaceholder: "Opcional", + documentDescriptionLabel: "Descripció", + documentDescriptionPlaceholder: "Opcional", + documentNoFile: "Sense fitxer.", + noDocuments: "No hi ha documents disponibles.", + documentSearchPlaceholder: "Cercar a Documents...", + documentSortRecent: "Més recents", + documentSortOldest: "Més antics", + documentSortTop: "Més votats", + documentSearchButton: "Cercar", + documentNoMatch: "No hi ha documents que coincideixin amb la teva cerca.", + documentUpdatedAt: "Actualitzat", + audioTitle: "Àudios", + audioDescription: "Explora i gestiona contingut d'àudio a la teva xarxa.", + audioMineSectionTitle: "Els teus àudios", + audioCreateSectionTitle: "Pujar un àudio", + audioUpdateSectionTitle: "Actualitzar l'àudio", + audioAllSectionTitle: "Àudios", + audioRecentSectionTitle: "Àudios recents", + audioTopSectionTitle: "Àudios més votats", + audioFilterAll: "TOTS", + audioFilterMine: "MEUS", + audioFilterRecent: "RECENTS", + audioFilterTop: "TOP", + audioCreateButton: "Pujar un àudio", + audioUpdateButton: "Actualitzar", + audioAddFavoriteButton: "Afegir a preferits", + audioRemoveFavoriteButton: "Treure de preferits", + audioFileLabel: "Selecciona un fitxer d'àudio (.mp3, .wav, .ogg)", + audioTagsLabel: "Etiquetes", + audioTagsPlaceholder: "Introdueix etiquetes separades per comes", + tagsPlaceholder: "Introdueix etiquetes separades per comes", + audioTitleLabel: "Títol", + audioTitlePlaceholder: "Opcional", + audioDescriptionLabel: "Descripció", + audioDescriptionPlaceholder: "Opcional", + audioNoFile: "No s'ha proporcionat cap fitxer d'àudio", + noAudios: "No hi ha àudios disponibles.", + audioSearchPlaceholder: "Cercar a Àudios...", + audioSortRecent: "Més recents", + audioSortOldest: "Més antics", + audioSortTop: "Més votats", + audioSearchButton: "Cercar", + audioMessageAuthorButton: "MP", + audioUpdatedAt: "Actualitzat", + audioNoMatch: "Cap àudio no coincideix amb la teva cerca.", + audioFavoritesSectionTitle: "Preferits", + audioFilterFavorites: "PREFERITS", + favoritesTitle: "Preferits", + favoritesDescription: "Tots els teus elements marcats com a preferits en un sol lloc.", + favoritesSearchPlaceholder: "Cercar a Preferits...", + favoritesFilterAll: "TOTS", + favoritesFilterRecent: "RECENTS", + favoritesFilterAudios: "ÀUDIOS", + favoritesFilterBookmarks: "MARCADORS", + favoritesFilterDocuments: "DOCUMENTS", + favoritesFilterImages: "IMATGES", + favoritesFilterMaps: "MAPES", + favoritesFilterPads: "PADS", + favoritesFilterChats: "XATS", + favoritesFilterCalendars: "CALENDARIS", + favoritesFilterVideos: "VÍDEOS", + favoritesNoItems: "Encara no hi ha preferits.", + yourContacts: "Els teus contactes", + allInhabitants: "Habitants", + allCVs: "CV", + discoverPeople: "Descobreix habitants a la teva xarxa.", + searchInhabitantsPlaceholder: "Cercar a Habitants...", + filterLocation: "FILTRAR habitants PER UBICACIÓ …", + filterLanguage: "FILTRAR habitants PER IDIOMA …", + filterSkills: "FILTRAR habitants PER HABILITATS …", + cvSkillsCount: "Habilitats", + applyFilters: "Aplicar els filtres", + locationLabel: "Ubicació", + languagesLabel: "Idiomes", + skillsLabel: "Habilitats", + commonSkills: "Habilitats comunes", + mutualFollowers: "Seguidors mutus", + suggestedFollowsYou: "Et segueix", + latestInteractions: "Darreres interaccions", + suggestedSectionTitle: "Suggerits", + topkarmaSectionTitle: "Millor karma", + topecoSectionTitle: "Millor Eco", + topactivitySectionTitle: "Més activitat", + "TOP INACTIVITYButton": "MÉS INACTIUS", + topinactivitySectionTitle: "Habitants menys actius", + blockedSectionTitle: "Bloquejats", + gallerySectionTitle: "GALERIA", + topSectionTitle: "TOP", + blockedLabel: "Usuari bloquejat", + inhabitantviewDetails: "Veure els detalls", + keepReading: "Continuar llegint...", + oasisId: "ID", + noInhabitantsFound: "Encara no s'han trobat habitants.", + inhabitantActivityLevel: "Nivell d'activitat", + lifespanLabel: "Vigència", + deviceLabel: "Dispositiu", + parliamentTitle: "Parlament", + parliamentDescription: "Explora formes de govern i polítiques de gestió col·lectiva.", + parliamentFilterGovernment: "GOVERN", + parliamentFilterCandidatures: "CANDIDATURES", + parliamentFilterProposals: "PROPOSTES", + parliamentFilterLaws: "LLEIS", + parliamentFilterRevocations: "REVOCACIONS", + parliamentFilterHistorical: "HISTÒRIC", + parliamentFilterLeaders: "LÍDERS", + parliamentFilterRules: "REGLES", + parliamentGovernmentCard: "Govern actual", + parliamentSeatsGovernment: "Govern", + parliamentSeatsOpposition: "Oposició", + parliamentSeatsAbstention: "Abstenció", + parliamentSeatsQuorum: "Quòrum d'elecció", + parliamentSeatsPass: "Llindar d'aprovació", + parliamentSeatsByElection: "Per elecció", + parliamentSeatsByHouses: "Per casa del LARP", + parliamentSeatsSeats: "escons", + parliamentSeatsSeat: "escó", + parliamentSeatsLeader: "Líder", + parliamentSeatsVotesFor: "vots a", + parliamentSeatsNoHouse: "Sense casa", + parliamentActorInPowerInhabitant: "HABITANT AL PODER", + parliamentActorInPowerTribe: "TRIBU AL PODER", + parliamentHistoricalGovernmentsTitle: "GOVERNS", + parliamentHistoricalElectionsTitle: "CICLES ELECTORALS", + parliamentHistoricalLawsTitle: "HISTÒRIC", + parliamentHistoricalLeadersTitle: "LÍDERS", + parliamentThCycles: "CICLES", + parliamentThTimesInPower: "MANDATS", + parliamentThTotalCandidatures: "CANDIDATURES", + parliamentThProposed: "LLEIS PROPOSADES", + parliamentThApproved: "LLEIS APROVADES", + parliamentThDeclined: "LLEIS REBUTJADES", + parliamentThDiscarded: "LLEIS DESCARTADES", + parliamentMembers: "MEMBRES", + parliamentLegSince: "INICI DEL CICLE", + parliamentLegEnd: "FI DEL CICLE", + parliamentPoliciesProposal: "PROPOSTES DE LLEI", + parliamentPoliciesApproved: "LLEIS APROVADES", + parliamentPoliciesDeclined: "LLEIS REBUTJADES", + parliamentPoliciesDiscarded: "LLEIS DESCARTADES", + parliamentEfficiency: "% EFICIÈNCIA", + parliamentRevocationFormTitle: "Revocar una llei", + parliamentRevocationLaw: "Llei", + parliamentRevocationReasons: "Motius", + parliamentRevocationPublish: "Publicar la revocació", + parliamentCurrentRevocationsTitle: "Revocacions actuals", + parliamentFutureRevocationsTitle: "Revocacions futures", + parliamentPoliciesRevocated: "LLEIS REVOCADES", + parliamentPoliciesTitle: "HISTÒRIC", + parliamentLawsTitle: "LLEIS APROVADES", + parliamentRulesRevocations: "Qualsevol llei aprovada es pot revocar amb el mètode de govern vigent.", + parliamentNoStableGov: "Encara no hi ha cap govern elegit.", + parliamentNoGovernments: "Encara no hi ha governs a l'històric.", + parliamentCandidatureFormTitle: "Proposar una candidatura", + parliamentCandidatureIdPh: "Oasis ID (@...) o nom de la tribu", + parliamentCandidatureMethod: "Mètode", + parliamentCandidatureProposeBtn: "Publicar la candidatura", + parliamentVoteBtn: "Votar", + parliamentProposalFormTitle: "Proposar una llei", + parliamentProposalDescription: "Descripció (≤1000)", + parliamentProposalPublish: "Publicar la proposta", + parliamentNoProposals: "Encara no hi ha propostes de llei.", + parliamentNoLaws: "Encara no hi ha lleis aprovades.", + parliamentMethodDEMOCRACY: "Democràcia", + parliamentMethodMAJORITY: "Majoria (80%)", + parliamentMethodMINORITY: "Minoria (20%)", + parliamentMethodDICTATORSHIP: "Dictadura", + parliamentMethodKARMATOCRACY: "Karmatocràcia", + parliamentThId: "ID", + parliamentThProposalDate: "Data de la proposta", + parliamentThMethod: "Mètode", + parliamentThKarma: "Karma", + parliamentThSupports: "Suports", + parliamentThVote: "Votar", + parliamentCurrentProposalsTitle: "Propostes actuals", + parliamentVotesSlashTotal: "Vots/Total", + parliamentVotesNeeded: "Vots necessaris", + parliamentFutureLawsTitle: "Futures lleis", + parliamentVoteAction: "Votar", + parliamentThLeader: "AVATAR", + parliamentPopulation: "Població", + parliamentThPresented: "CANDIDATURES", + parliamentNoLeaders: "Encara no hi ha líders.", + parliamentCandidaturesListTitle: "Llista de candidatures", + parliamentTimeRemaining: "Temps restant", + parliamentElectionsStatusTitle: "Pròxim govern", + parliamentProposalDeadlineLabel: "Data límit", + parliamentViewReport: "Veure l'informe", + parliamentProposalTimeLeft: "Temps restant", + parliamentRulesTitle: "Com funciona el Parlament", + parliamentRulesIntro: "Les eleccions es resolen cada 2 mesos; les candidatures són contínues i es reinicien quan s'elegeix un govern.", + parliamentRulesCandidates: "Qualsevol habitant es pot proposar a si mateix, a un altre habitant o a qualsevol tribu. Cada habitant pot proposar fins a 3 candidatures per cicle; es rebutgen els duplicats dins el mateix cicle.", + parliamentRulesElection: "Guanya la candidatura amb més vots en el moment de la resolució.", + parliamentRulesTies: "Desempat: més karma de l'habitant; si persisteix, perfil més antic; si persisteix, proposta més primerenca; després, ordre lexicogràfic per ID.", + parliamentRulesTerm: "La pestanya Govern mostra el govern actual i les seves estadístiques.", + parliamentRulesMethods: "Formes: Anarquia (majoria simple), Democràcia (50%+1), Majoria (80%), Minoria (20%), Karmatocràcia (propostes amb més karma), Dictadura (aprovació instantània).", + parliamentRulesAnarchy: "L'Anarquia és el mode per defecte: si en resoldre no s'elegeix cap candidatura, es proclama l'Anarquia. En Anarquia, qualsevol habitant pot proposar lleis.", + parliamentRulesProposals: "Si ets l'habitant governant o membre de la tribu governant, pots publicar propostes de llei. Els mètodes no dictatorials creen una votació pública.", + parliamentRulesLimit: "Cada habitant pot publicar com a màxim 3 propostes de llei per cicle.", + parliamentRulesLaws: "Quan una proposta arriba al seu llindar, es converteix en Llei i apareix a la pestanya Lleis amb la seva data d'entrada en vigor.", + parliamentRulesHistorical: "A Històric es pot veure cada cicle de govern que hi ha hagut i dades sobre la seva gestió.", + parliamentRulesLeaders: "A Líders es pot veure un rànquing d'habitants/tribus que han governat (o s'han presentat), ordenats per eficàcia.", + parliamentRulesHemicycle: "L'hemicicle dona un escó per habitant: el bloc de govern (votants de la candidatura guanyadora i el líder), un bloc d'oposició per cada altra candidatura del cicle, i l'abstenció. Les marques discontínues assenyalen el quòrum d'elecció (25 % del cens) i el llindar d'aprovació del mètode vigent. \"Per casa del LARP\" acoloreix els mateixos escons per casa.", + parliamentProposalVoteStatusLabel: "Estat de la votació", + parliamentProposalOnTrackYes: "Llindar assolit", + parliamentProposalOnTrackNo: "Per sota del llindar", + courtsTitle: "Tribunals", + courtsDescription: "Explora formes de resolució de conflictes i de gestió col·lectiva de la justícia.", + courtsFilterCases: "CASOS", + courtsFilterMyCases: "MEUS", + courtsFilterJudges: "JUTGES", + courtsFilterHistory: "HISTORIAL", + courtsFilterRules: "REGLES", + courtsCaseFormTitle: "Obrir un cas", + courtsCaseRespondent: "Acusat / Part demandada", + courtsCaseRespondentPh: "ID d'Oasis (@...) o nom de la tribu", + courtsCaseSubjectPlaceholder: "Assumpte o descripció breu", + courtsCaseMethod: "Mètode de resolució", + courtsCaseSubmit: "Presentar el cas", + courtsNominateJudge: "Nominar un jutge", + courtsJudgeId: "Jutge", + courtsJudgeIdPh: "ID d'Oasis (@...) o nom de l'habitant", + courtsNominateBtn: "Nominar", + courtsAddEvidence: "Afegir proves", + courtsEvidenceText: "Text", + courtsEvidenceLink: "Enllaç", + courtsEvidenceLinkPh: "https://…", + courtsEvidenceSubmit: "Adjuntar", + courtsAnswerText: "Resum de la resposta", + courtsAnswerSubmit: "Enviar la resposta", + courtsVerdictTitle: "Emetre veredicte", + courtsVerdictOrdersPh: "Accions, terminis, passos restauratius...", + courtsIssueVerdict: "Emetre veredicte", + courtsMediationPropose: "Proposar un acord", + courtsSettlementProposeBtn: "Proposar", + courtsNominationsTitle: "Nominacions a la judicatura", + courtsThJudge: "Jutge", + courtsThDate: "Data", + courtsThVote: "Votar", + courtsNoNominations: "Encara no hi ha nominacions.", + courtsRespondentInvalid: "Ha de ser un ID SSB vàlid (@...ed25519)", + courtsCaseMediatorsRespondentTitle: "Afegir mediadors de la defensa", + courtsCaseMediatorsRespondent: "Mediadors (defensa)", + courtsMediatorsAccuserLabel: "Mediadors (acusació)", + courtsMediatorsRespondentLabel: "Mediadors (defensa)", + courtsMediatorsSubmit: "Desar els mediadors", + courtsNoMyCases: "Encara no tens conflictes.", + courtsNoHistory: "Encara no hi ha judicis registrats.", + courtsMethodDICTATOR: "Dictador", + courtsMethodPOPULAR: "Popular", + courtsMethodKARMATOCRACY: "Karmatocràcia", + courtsCaseType: "Tipus de cas", + courtsPresetMinorConflict: "Conflicte lleu", + courtsPresetModerateConflict: "Conflicte moderat", + courtsPresetSevereConflict: "Conflicte greu", + courtsPresetHarassment: "Assetjament o abús", + courtsPresetContentModeration: "Moderació de contingut", + courtsPresetBanRestriction: "Expulsió o restricció", + courtsPresetGovernanceDispute: "Disputa de governança", + courtsStatusOPEN: "Obert", + courtsStatusIN_PROGRESS: "En curs", + courtsStatusDECIDED: "Decidit", + courtsStatusCLOSED: "Tancat", + courtsStatusSOLVED: "Resolt", + courtsStatusUNSOLVED: "Sense resoldre", + courtsStatusDISCARDED: "Descartat", + courtsRulesTitle: "Com funcionen els Tribunals", + courtsRulesIntro: "Els Tribunals són un procés gestionat per la comunitat per resoldre conflictes i promoure la justícia restaurativa. Es prioritzen el diàleg, les proves clares i els remeis proporcionals.", + courtsRulesLifecycle: "Procés: 1) Obrir el cas 2) Seleccionar el mètode 3) Presentar proves 4) Audiència i deliberació 5) Veredicte i remei 6) Compliment i tancament 7) Apel·lació (si escau).", + courtsRulesRoles: "Acusació: obre el cas. Defensa: habitant o tribu acusada. Mètode: mecanisme triat per la comunitat per facilitar, avaluar les proves i emetre veredicte. Testimoni: aporta testimoniatge o proves. Mediadors: habitants neutrals convidats per l'acusació i/o la defensa, amb accés a tots els detalls, que ajuden a desescalar el conflicte i a cocrear acords.", + courtsRulesEvidence: "Descripció de fins a 1000 caràcters. Adjunta imatges, àudio, vídeo i documents PDF rellevants i legals. No comparteixis dades privades sensibles sense consentiment.", + courtsRulesDeliberation: "Les audiències poden ser públiques o privades. Els jutges garanteixen el respecte, demanen aclariments i poden descartar material irrellevant o il·legal.", + courtsRulesVerdict: "Es prioritza la restauració: disculpes, acords de mediació, moderació de contingut, restriccions temporals o altres mesures proporcionals. El raonament ha de quedar registrat.", + courtsRulesAppeals: "Apel·lació: permesa quan hi ha proves noves o un error processal clar. S'ha de presentar en un termini de 7 dies llevat que s'indiqui el contrari.", + courtsRulesPrivacy: "Respecta la privadesa i la seguretat. El doxing, l'odi o les amenaces s'eliminen. Els jutges poden editar o segellar parts de l'expedient per protegir habitants en risc.", + courtsRulesMisconduct: "L'assetjament, la manipulació o les proves fabricades poden comportar una resolució negativa immediata.", + courtsRulesGlossary: "Cas: registre d'un conflicte. Proves: materials que avalen les afirmacions. Veredicte: decisió amb remei. Apel·lació: sol·licitud per revisar el veredicte.", + courtsFilterActions: "ACCIONS", + courtsThRole: "Rol", + courtsRoleAccuser: "Acusació", + courtsRoleDefence: "Defensa", + courtsRoleMediator: "Mediador", + courtsRoleJudge: "Jutge", + courtsRoleDictator: "Dictador", + courtsAssignJudgeTitle: "Triar un jutge", + courtsAssignJudgeBtn: "Triar un jutge", + trendingTitle: "Tendències", + exploreTrending: "Explora el contingut més popular de la teva xarxa.", + votesButton: "GOVERN", + shopProductButton: "BOTIGA", + podcastButton: "PODCAST", + podcastEpisodeButton: "EPISODI", + campaignButton: "CAMPANYA", + logisticsRouteButton: "RUTA", + trendingTotalOpinions: "Total d'opinions", + trendingNoContentMessage: "Encara no hi ha tendències.", + trendingTotalCount: "Total de comptadors", + tasksTitle: "Tasques", + tasksDescription: "Descobreix i gestiona tasques a la teva xarxa.", + taskSearchPlaceholder: "Cercar a Tasques...", + taskTitleLabel: "Títol", + taskDescriptionLabel: "Descripció", + taskStartTimeLabel: "Data d'inici", + taskEndTimeLabel: "Data de fi", + taskPriorityLabel: "Prioritat", + taskPriorityUrgent: "Urgent", + taskPriorityHigh: "Alta", + taskPriorityMedium: "Mitjana", + taskPriorityLow: "Baixa", + taskLocationLabel: "Ubicació", + taskTagsLabel: "Etiquetes", + taskVisibilityLabel: "Visibilitat", + taskPublic: "públic", + taskPrivate: "privat", + taskStatus: "Estat", + taskStatusOpen: "Oberta", + taskStatusInProgress: "En curs", + taskStatusClosed: "Tancada", + taskAssignedTo: "Assignada", + taskAssignedChip: "Assignada", + taskUnassignedChip: "Sense assignar", + taskAssignees: "Assignats", + taskAssignButton: "Assignar-me-la", + taskUnassignButton: "Desassignar", + taskCreateButton: "Crear una tasca", + taskUpdateButton: "Actualitzar", + taskDeleteButton: "Eliminar", + taskFilterAll: "TOTES", + taskFilterRecent: "RECENTS", + taskFilterMine: "MEVES", + taskFilterOpen: "OBERTES", + taskFilterInProgress: "EN CURS", + taskFilterClosed: "TANCADES", + taskFilterAssigned: "ASSIGNADES", + taskFilterUrgent: "URGENTS", + taskFilterHigh: "ALTES", + taskFilterMedium: "MITJANES", + taskFilterLow: "BAIXES", + taskAllSectionTitle: "Tasques", + taskMineSectionTitle: "Les teves tasques", + taskCreateSectionTitle: "Crear una tasca", + taskUpdateSectionTitle: "Actualitzar", + taskOpenTitle: "Tasques obertes", + taskInProgressTitle: "Tasques en curs", + taskClosedTitle: "Tasques tancades", + taskAssignedTitle: "Tasques assignades", + notasks: "No hi ha tasques disponibles.", + noLocation: "No s'ha especificat cap ubicació", + taskSetStatus: "Establir l'estat", + eventDateLabel: "Data", + eventRecurrenceLabel: "Recurrència", + eventRecurrenceUntil: "Repetir fins a", + eventRecurrenceHint: "Deixa buida la data de repetició per a un esdeveniment únic.", + eventUpcomingDates: "Pròximes dates", + eventsTitle: "Esdeveniments", + eventsDescription: "Descobreix i gestiona esdeveniments a la teva xarxa.", + eventSearchPlaceholder: "Cercar a Esdeveniments...", + eventPrice: "Preu", + eventStatus: "Estat", + eventAllSectionTitle: "Esdeveniments", + eventMineSectionTitle: "Els teus esdeveniments", + eventCreateSectionTitle: "Crear un esdeveniment", + eventUpdateSectionTitle: "Actualitzar l'esdeveniment", + eventCreateButton: "Crear un esdeveniment", + eventAddToCalendar: "Afegir al calendari", + eventVisitCalendar: "Visitar el calendari", + viewTask: "Veure la tasca", + viewEvent: "Veure l'esdeveniment", + viewPad: "Veure el pad", + viewMap: "Veure el mapa", + viewCalendar: "Veure el calendari", + viewReport: "Veure l'informe", + viewTransfer: "Veure la transferència", + viewItem: "Veure l'article", + viewShop: "Veure la botiga", + viewProject: "Veure el projecte", + viewJob: "Veure la feina", + viewPlace: "Veure el lloc", + generatePdf: "Generar PDF", + sharePm: "Compartir per PM", + galleryImages: "Imatges (mida màx.: 75MB cadascuna)", + galleryPhotos: "Imatges", + galleryAddPhoto: "Afegir una imatge", + galleryRemovePhoto: "Treure", + galleryVideo: "Vídeo (mida màx.: 75MB)", + galleryAddVideo: "Afegir un vídeo", + galleryRemoveVideo: "Treure el vídeo", + viewVotation: "Veure la votació", + voteCastTitle: "Emetre el vot", + voteResults: "Resultats", + eventTitleLabel: "Títol", + eventDescriptionLabel: "Descripció", + eventDescriptionPlaceholder: "Introdueix la descripció de l'esdeveniment...", + eventUpdateButton: "Actualitzar", + eventTagsLabel: "Etiquetes", + eventPriceLabel: "Preu", + eventUrlLabel: "URL", + eventAttendees: "Assistents", + eventLocation: "Ubicació", + eventLocationLabel: "Ubicació", + noevents: "No hi ha esdeveniments disponibles.", + eventDate: "Data", + eventAttendButton: "Assistir a l'esdeveniment", + eventFilterAll: "TOTS", + eventFilterRecent: "RECENTS", + eventFilterMine: "MEUS", + eventFilterToday: "AVUI", + eventFilterWeek: "AQUESTA SETMANA", + eventFilterMonth: "AQUEST MES", + eventFilterYear: "AQUEST ANY", + eventFilterArchived: "ARXIVATS", + eventPrivacyLabel: "Visibilitat", + eventPublic: "Públic", + eventPrivate: "Privat", + eventAttended: "Hi assisteixo", + eventUnattended: "No hi assisteixo", + eventStatusOpen: "Obert", + eventStatusClosed: "Tancat", + tagsTitle: "Etiquetes", + tagsDescription: "Descobreix i explora patrons de taxonomia a la teva xarxa.", + tagsAllSectionTitle: "Etiquetes", + tagsTopSectionTitle: "Etiquetes principals", + tagsCloudSectionTitle: "Núvol d'etiquetes", + tagsFilterAll: "TOTES", + tagsFilterMine: "MEVES", + tagsFilterRecent: "RECENTS", + tagsMineSectionTitle: "Les meves etiquetes", + tagsRecentSectionTitle: "Etiquetes recents", + tagsFilterTop: "TOP", + tagsFilterCloud: "NÚVOL", + tagsNoItems: "No hi ha etiquetes disponibles.", + tagsSearchPlaceholder: "Cercar a Etiquetes...", + tagsTableHeaderTag: "ETIQUETA/ENLLAÇ:", + tagsTableHeaderCount: "COMPTADOR:", + transfersTitle: "Transferències", + transfersDescription: "Descobreix i gestiona transferències a la teva xarxa.", + transfersFrom: "De", + transfersTo: "A", + transfersFilterAll: "TOTES", + transfersFilterRecent: "RECENTS", + transfersFilterMine: "MEVES", + transfersFilterMinePending: "PENDENTS", + transfersFilterMineConfirmed: "SIGNADES", + transfersFilterUBI: "RBU", + transfersFilterMarket: "MERCAT", + transfersFilterTop: "TOP", + transfersFilterPending: "PENDENTS", + transfersFilterUnconfirmed: "NO CONFIRMADES", + transfersFilterClosed: "TANCADES", + transfersFilterDiscarded: "DESCARTADES", + transfersFilterEconomic: "ECONÒMICA", + transfersFilterTime: "TEMPS", + transfersFilterTrust: "CONFIANÇA", + transfersCategory: "Categoria", + transfersCategoryEconomic: "Econòmica", + transfersCategoryTime: "Temps", + transfersCategoryTrust: "Confiança", + transfersUnitEco: "ECO", + transfersUnitHours: "h", + transfersUnitTrust: "confiança", + transfersEconomicSectionTitle: "Transferències econòmiques", + transfersTimeSectionTitle: "Transferències de temps", + transfersTrustSectionTitle: "Transferències de confiança", + transfersCreateButton: "Crear una transferència", + transfersUpdateButton: "Actualitzar", + transfersDeleteButton: "Eliminar", + transfersToUser: "ID d'Oasis", + transfersToUserValidation: "ID d'Oasis vàlid, p. ex. @…=.ed25519", + transfersConcept: "Concepte", + transfersAmount: "Quantitat", + transfersDeadline: "Termini", + transfersTags: "Etiquetes", + transfersStatus: "Estat", + transfersConfirmations: "CONFIRMACIONS", + transfersExportContract: "Generar la factura", + transfersConfirmButton: "Confirmar la transferència", + transfersNoItems: "No s'han trobat transferències.", + transfersMineSectionTitle: "Les teves transferències", + transfersUBISectionTitle: "Transferències RBU", + transfersTopSectionTitle: "Transferències principals", + transfersPendingSectionTitle: "Transferències pendents", + transfersUnconfirmedSectionTitle: "Transferències no confirmades", + transfersClosedSectionTitle: "Transferències tancades", + transfersDiscardedSectionTitle: "Transferències descartades", + transfersCreateSectionTitle: "Crear una transferència", + transfersAllSectionTitle: "Transferències", + transfersSearchPlaceholder: "Cercar a Transferències...", + transfersMinAmountLabel: "Import mín.", + transfersMaxAmountLabel: "Import màx.", + transfersSortRecent: "Més recents", + transfersSortAmount: "Import més alt", + transfersSortDeadline: "Termini més proper", + transfersSearchButton: "Cercar", + transfersExpiredBadge: "CADUCADA", + transfersUpdatedAt: "Actualitzat", + transfersNoMatch: "No hi ha transferències que coincideixin amb la teva cerca.", + votationsTitle: "Votacions", + votationsDescription: "Descobreix i gestiona votacions a la teva xarxa.", + voteMineSectionTitle: "Les teves votacions", + voteCreateSectionTitle: "Crear una votació", + voteUpdateSectionTitle: "Actualitzar", + voteOpenTitle: "Votacions obertes", + voteClosedTitle: "Votacions tancades", + voteAllSectionTitle: "Govern", + voteCreateButton: "Crear una votació", + voteErrorTitle: "Revisa el formulari", + voteErrorDeadlineMin: "La data límit ha de ser d'almenys {days} dies a partir d'ara.", + voteErrorGeneric: "No s'ha pogut desar la votació.", + voteLocked: "Aquesta votació ja ha rebut vots o opinions, o pertany al Parlament o als Tribunals: ja no es pot editar ni esborrar.", + voteUpdateButton: "Actualitzar", + voteDeleteButton: "Eliminar", + voteOptionYes: "SÍ", + voteOptionNo: "NO", + voteOptionAbstention: "ABSTENCIÓ", + voteConfused: "CONFÓS", + voteFollowMajority: "SEGUIR LA MAJORIA", + voteNotInterested: "SENSE INTERÈS", + voteFilterAll: "TOTES", + voteFilterRecent: "RECENTS", + voteFilterMine: "MEVES", + voteFilterOpen: "OBERTES", + voteFilterClosed: "TANCADES", + voteQuestionLabel: "Pregunta", + voteDeadlineLabel: "Termini", + voteOptionsLabel: "El teu vot", + voteBreakdown: "Desglossament", + voteFinalResult: "RESULTAT", + voteNoQuorum: "SENSE QUÒRUM", + voteTagsLabel: "Etiquetes", + voteDeadline: "Termini", + voteStatus: "Estat", + voteTags: "Etiquetes", + voteOpinions: "Opinions", + novotes: "No hi ha propostes de votació disponibles.", + voteCreatedAt: "Creat el", + voteNoQuestion: "No s'ha proporcionat cap pregunta", + voteUnknownCreator: "Creador desconegut", + voteUnknownDate: "Data desconeguda", + voteDetailSectionTitle: "Detalls de la votació", + voteCommentsLabel: "Comentaris", + voteCommentsForumButton: "Obrir la conversa", + voteCommentsSectionTitle: "Conversa oberta", + voteNoCommentsYet: "Encara no hi ha comentaris. Sigues la primera persona a opinar.", + commentsGoToLast: "Anar al darrer comentari", + voteNewCommentPlaceholder: "Escriu aquí el teu comentari…", + voteNewCommentButton: "Publicar el comentari", + voteNewCommentLabel: "Afegeix un comentari", + cvTitle: "CV", + cvEditSectionTitle: "Editar el CV", + cvCreateSectionTitle: "Crear un CV", + cvDescription: "Gestiona i comparteix les teves habilitats professionals i la teva informació.", + cvNameLabel: "Nom complet", + cvDescriptionLabel: "Resum", + cvPhotoLabel: "Foto", + cvPdfLabel: "CV (PDF)", + cvVisitButton: "Visitar el CV", + cvPersonalExperiencesLabel: "Experiències personals", + cvPersonalSkillsLabel: "Habilitats personals (separades per comes)", + cvOasisExperiencesLabel: "Experiències de contribució a Oasis", + cvOasisSkillsLabel: "Habilitats de contribució a Oasis (separades per comes)", + cvEducationExperiencesLabel: "Experiències educatives", + cvEducationalSkillsLabel: "Habilitats educatives (separades per comes)", + cvProfessionalExperiencesLabel: "Experiències professionals", + cvProfessionalSkillsLabel: "Habilitats professionals (separades per comes)", + cvLanguagesLabel: "Idiomes", + cvLocationLabel: "Ubicació", + cvStatusLabel: "Estat", + cvPreferencesLabel: "Preferències", + cvPersonal: "Personal", + cvOasis: "Contribuïdor d'Oasis (opcional)", + cvOasisContributorView: "Contribució", + cvEducational: "Educatiu (opcional)", + cvEducationalView: "Educatiu", + cvProfessional: "Professional (opcional)", + cvProfessionalView: "Professional", + cvAvailability: "Disponibilitat (opcional)", + cvUpdateButton: "Actualitzar", + cvCreateButton: "Crear un CV", + cvCreatedAt: "Creat el", + cvUpdatedAt: "Actualitzat el", + cvEditButton: "Actualitzar", + cvDeleteButton: "Eliminar", + cvNoCV: "No s'ha trobat cap CV.", + blogSubject: "Assumpte", + blogMessage: "Missatge", + blogImage: "Pujar contingut multimèdia (màx.: 75MB)", + blogMedia: "Adjunts (fins a 8 imatges i un vídeo)", + blogPublish: "Publicar", + pollsTitle: "Enquestes", + dataTitle: "Coincidències", + dataDescription: "Explora i visualitza les coincidències de la teva xarxa.", + dataFilterAll: "TOT", + dataFilterMine: "MEUS", + dataFilterRecent: "RECENTS", + dataFilterTop: "TOP", + dataKindInhabitants: "HABITANTS", + dataKindJobs: "FEINES", + dataKindProjects: "PROJECTES", + dataKindEvents: "ESDEVENIMENTS", + dataKindTribes: "TRIBUS", + dataKindMarket: "MERCAT", + dataKindHousing: "HABITATGE", + dataKindIndustry: "INDÚSTRIA", + dataKindTasks: "TASQUES", + dataKindReports: "INFORMES", + dataKindVotes: "VOTACIONS", + dataSearchPlaceholder: "Cercar a Coincidències...", + dataCohesionTitle: "Coeficient de cohesió (CC)", + dataCcShort: "CC", + dataCohesionHint: "Afinitat mitjana entre tot el que ha publicat la xarxa.", + dataAffinity: "Afinitat", + dataCommonTerms: "Paraula clau", + dataReasonFollowing: "Dones suport a aquest habitant", + dataReasonTribe: "Comparteix tribu amb tu", + dataReasonPinned: "Coincideix amb alguna cosa que has fixat", + dataReasonCv: "Coincideix amb les habilitats del teu CV", + dataReasonContent: "Relacionat amb el que publiques", + dataReasonRelated: "Relacionat amb els teus interessos", + dataReasonAlike: "Publicat per un habitant afí a tu", + dataReasonSupportsYou: "Et dona suport", + dataReasonMutual: "Suport mutu", + dataReasonRated: "Ben valorat per la xarxa", + dataReasonNear: "A prop teu", + dataStatPeople: "CV", + dataStatConnected: "Connectats", + dataStatSkills: "Habilitats", + dataBestMatch: "Millor coincidència", + dataStatIsolated: "Aïllats", + dataStatEntities: "Entitats comparades", + dataStatTerms: "Termes diferents", + dataStatPairs: "Parells connectats", + dataMatchesTitle: "Coincidències", + dataMatchesHint: "Suggeriments personalitzats de l'algorisme.", + dataTermsTitle: "Paraules clau", + dataTermsHint: "Els termes que apareixen en més continguts.", + dataConnections: "Coincidències enllaçades", + dataNoMatches: "No hi ha dades creuades per mostrar.", + dataNoProfile: "Publica contingut o escriu el teu CV perquè l'algorisme aprengui què t'interessa.", + pollsDescription: "Mòdul per preguntar a la xarxa i comptar les respostes.", + pollFilterAll: "TOTES", + pollFilterMine: "MEVES", + pollFilterRecent: "RECENTS", + pollFilterTop: "TOP", + pollFilterVoted: "VOTADES", + pollFilterOpen: "OBERTES", + pollFilterClosed: "TANCADES", + pollCreateButton: "Crear una enquesta", + pollCreateTitle: "Crear una enquesta", + pollEditTitle: "Editar l'enquesta", + pollQuestion: "Pregunta", + pollQuestionPlaceholder: "Què vols preguntar?", + pollOptions: "Opcions (una per línia)", + pollOutcome: "Resultat", + pollNoVotesYet: "Encara sense vots", + pollTie: "Empat", + pollOptionsPlaceholder: "Plaça\nParc\nBar", + pollAnonymous: "Anònima", + pollAnonymousLabel: "Enquesta anònima", + pollMultiple: "Resposta múltiple", + pollMultipleLabel: "Permetre diverses respostes", + pollDeadline: "Data límit", + pollTags: "Etiquetes", + pollPublishButton: "Publicar l'enquesta", + pollUpdateButton: "Actualitzar", + pollCloseButton: "Tancar", + pollDeleteButton: "Eliminar", + pollVoteButton: "Votar", + pollChangeVote: "Canviar el vot", + pollVoters: "Votants", + pollComments: "Comentaris", + pollStatusLabel: "Estat", + pollStatusOpen: "OBERTA", + pollStatusClosed: "TANCADA", + pollSearchPlaceholder: "Cercar a Enquestes...", + pollsNoItems: "No s'han trobat enquestes.", + viewPoll: "Veure l'enquesta", + pollChatCreate: "Crear una enquesta", + pollInChat: "Enquesta", + blogTitle: "Blogs", + blogDescription: "Mòdul per descobrir i gestionar blogs.", + blogFilterAll: "TOTS", + blogFilterMine: "MEUS", + blogFilterRecent: "RECENTS", + blogFilterFavorites: "PREFERITS", + blogFilterTop: "TOP", + blogCreateButton: "Crear un blog", + blogSearchPlaceholder: "Cercar a Blogs...", + blogComments: "Comentaris", + blogAllowComments: "Permetre comentaris", + blogCommentsClosed: "L'autor ha tancat els comentaris d'aquest blog.", + blogNoItems: "No s'han trobat blogs.", + viewBlog: "Veure el blog", + forumTitle: "Fòrums", + forumCategoryLabel: "Categoria", + forumTitleLabel: "Títol", + forumTitlePlaceholder: "Títol del fòrum...", + forumCreateButton: "Crear un fòrum", + forumCreateSectionTitle: "Crear un fòrum", + forumDescription: "Parla obertament amb habitants de la teva xarxa.", + forumSearchPlaceholder: "Cercar a Fòrums...", + forumFilterAll: "TOTS", + forumFilterMine: "MEUS", + forumFilterRecent: "RECENTS", + forumFilterTop: "TOP", + forumParticipants: "participants", + forumMessages: "missatges", + forumMessageLabel: "Missatge", + forumMessagePlaceholder: "Escriu el teu missatge...", + forumSendButton: "Enviar", + noForums: "Encara no hi ha fòrums disponibles.", + forumVisitButton: "Visitar el fòrum", + forumTopicLabel: "Tema", + imageTitle: "Imatges", + imageDescription: "Explora i gestiona contingut d'imatges a la teva xarxa.", + imageMineSectionTitle: "Les teves imatges", + imageCreateSectionTitle: "Pujar una imatge", + imageUpdateSectionTitle: "Actualitzar la imatge", + imageAllSectionTitle: "Imatges", + imageRecentSectionTitle: "Imatges recents", + imageTopSectionTitle: "Imatges top", + imageFavoritesSectionTitle: "Preferits", + imageGallerySectionTitle: "Galeria", + imageFilterAll: "TOTES", + imageFilterMine: "MEVES", + imageFilterRecent: "RECENTS", + imageFilterTop: "TOP", + imageFilterFavorites: "PREFERITS", + imageFilterGallery: "GALERIA", + imageCreateButton: "Pujar una imatge", + imageUpdateButton: "Actualitzar", + imageAddFavoriteButton: "Afegir a preferits", + imageRemoveFavoriteButton: "Treure de preferits", + imageFileLabel: "Selecciona un fitxer d'imatge (.jpeg, .jpg, .png, .gif)", + imageTagsLabel: "Etiquetes", + imageTagsPlaceholder: "Introdueix etiquetes separades per comes", + imageTitleLabel: "Títol", + imageTitlePlaceholder: "Opcional", + imageDescriptionLabel: "Descripció", + imageDescriptionPlaceholder: "Opcional", + imageNoFile: "No s'ha proporcionat cap fitxer d'imatge", + noImages: "No hi ha imatges disponibles.", + imageSearchPlaceholder: "Cercar a Imatges...", + imageSortRecent: "Més recents", + imageSortOldest: "Més antigues", + imageSortTop: "Més votades", + imageSearchButton: "Cercar", + imageUpdatedAt: "Actualitzat", + imageNoMatch: "Cap imatge no coincideix amb la teva cerca.", + feedSearchPlaceholder: "Cercar a Feed...", + feedTitle: "Feed", + feedDetailTitle: "Feed", + feedOpenDiscussion: "Obrir el debat", + feedPostComment: "Publicar el comentari", + noComments: "Encara no hi ha comentaris", + createFeedTitle: "Crear un feed", + createFeedButton: "Enviar el feed!", + feedPlaceholder: "Què està passant? (màxim 280 caràcters)", + TODAYButton: "AVUI", + totalOpinions: "Total d'opinions", + moreVoted: "Més votat", + noFeedsFound: "No s'han trobat feeds.", + createdAtLabel: "Creat el", + FeedshareYourOpinions: "Descobreix i comparteix textos breus a la teva xarxa.", + refeedButton: "Reenviar", + alreadyRefeeded: "Ja has realimentat això.", + activityTitle: "Activitat", + yourActivity: "La teva activitat", + globalActivity: "Activitat", + activityList: "Activitat", + activityDesc: "Consulta l'activitat més recent de la teva xarxa.", + allButton: "TOT", + mineButton: "MEVES", + noActions: "No hi ha activitat disponible.", + from: "De", + to: "A", + amount: "Quantitat", + concept: "Concepte", + description: "Descripció", + meme: "Mem", + typeRecent: "RECENTS", + typeTop: "TOP", + typePost: "BLOG", + activityUpdateLabel: "ACTUALITZACIÓ", + typeTribe: "TRIBUS", + typeLarp: "L.A.R.P.", + typeInhabitants: "HABITANTS", + typeAbout: "HABITANTS", + typeCurriculum: "CV", + typeImage: "IMATGES", + typeBookmark: "MARCADORS", + typeDocument: "DOCUMENTS", + typeVotes: "VOTACIONS", + typeAudio: "ÀUDIOS", + typeMarket: "MERCAT", + typeJob: "FEINES", + typeProject: "PROJECTES", + typeVideo: "VÍDEOS", + typeVote: "DIFUSIÓ", + typeEvent: "ESDEVENIMENTS", + typeTransfer: "TRANSFERÈNCIES", + typeTask: "TASQUES", + typePixelia: "PIXELIA", + typeForum: "FÒRUMS", + typeReport: "INFORMES", + typeFeed: "FEEDS", + typeContact: "CONTACTE", + typePub: "PUB", + typeTombstone: "ELIMINAT", + typeBanking: "BANCA", + typeBankWallet: "BANCA/MONEDER", + typeBankClaim: "BANCA/RBU", + typeKarmaScore: "KARMA", + typeParliament: "PARLAMENT", + typeSpread: "RÈPLIQUES", + typeParliamentCandidature: "Parlament · Candidatura", + typeParliamentTerm: "Parlament · Mandat", + typeParliamentProposal:"Parlament · Proposta", + typeParliamentRevocation:"Parlament · Revocació", + typeParliamentLaw: "Parlament · Nova llei", + typeCourts: "TRIBUNALS", + typeCourtsCase: "Tribunals · Cas", + typeCourtsEvidence: "Tribunals · Prova", + typeCourtsAnswer: "Tribunals · Resposta", + typeCourtsVerdict: "Tribunals · Veredicte", + typeCourtsSettlement: "Tribunals · Acord", + typeCourtsSettlementProposal: "Tribunals · Proposta d'acord", + typeCourtsSettlementAccepted: "Tribunals · Acord acceptat", + typeCourtsNomination: "Tribunals · Nominació", + typeCourtsNominationVote: "Tribunals · Votació de nominació", + question: "Pregunta", + deadline: "Data límit", + status: "Estat", + votes: "Vots", + totalVotes: "Vots totals", + voteTotalVotes: "Vots totals", + name: "Nom", + skills: "Habilitats", + tags: "Etiquetes", + title: "Títol", + date: "Data", + category: "Categoria", + attendees: "Assistents", + location: "Ubicació", + contentWarning: "Assumpte", + bankWalletConnected: "Moneder ECOin", + bankUbiReceived: "RBU rebuda", + bankTx: "Tx", + bankEpochShort: "Època", + viewDetails: "Veure els detalls", + link: "Enllaç", + activityProjectFollow: "%OASIS% ara està %ACTION% aquest projecte %PROJECT%", + activityProjectUnfollow: "%OASIS% ara està %ACTION% aquest projecte %PROJECT%", + following: "SEGUINT", + unfollowing: "DEIXANT DE SEGUIR", + pledged: "APORTAT", + parliamentCandidatureId: "Candidatura", + parliamentGovMethod: "Mètode", + parliamentVotesReceived: "Vots rebuts", + parliamentMethodANARCHY: "Anarquia", + parliamentMethodVOTE: "Votació comunitària", + parliamentMethodRANKED: "Vot preferencial", + parliamentMethodPLURALITY: "Pluralitat", + parliamentMethodCOUNCIL: "Consell", + parliamentMethodJURY: "Jurat", + parliamentAnarchy: "ANARQUIA", + parliamentElectionsStart: "Inici de les eleccions", + parliamentElectionsEnd: "Fi de les eleccions", + parliamentCurrentLeader: "Candidatura guanyadora", + parliamentProposalTitle: "Títol", + parliamentOpenVote: "Votació oberta", + parliamentStatus: "Estat", + parliamentLawQuestion: "Pregunta", + parliamentLawMethod: "Mètode", + parliamentLawProposer: "Proponent", + parliamentLawEnacted: "Promulgada", + parliamentLawVotes: "Vots", + createdAt: "Creat el", + courtsCaseTitle: "Cas", + courtsMethod: "Mètode", + courtsMethodJUDGE: "Jutge", + courtsMethodJUDGES: "Tribunal de jutges", + courtsMethodSINGLE_JUDGE: "Jutge únic", + courtsMethodJURY: "Jurat", + courtsMethodCOUNCIL: "Consell", + courtsMethodCOMMUNITY: "Comunitat", + courtsMethodARBITRATION: "Arbitratge", + courtsMethodVOTE: "Votació comunitària", + courtsAccuser: "Acusació", + courtsRespondent: "Defensa", + courtsThStatus: "Estat", + courtsThAnswerBy: "Respondre abans de", + courtsThEvidenceBy: "Proves fins a", + courtsThDecisionBy: "Decisió abans de", + courtsVotesNeeded: "Vots necessaris", + courtsVotesSlashTotal: "SÍ/TOTAL", + courtsOpenVote: "Votació oberta", + courtsAnswerTitle: "Resposta", + courtsVerdictResult: "Resultat", + courtsVerdictOrders: "Ordres", + courtsSettlementText: "Acord", + courtsSettlementAccepted: "Acceptat", + courtsJudge: "Jutge", + courtsThSupports: "Suports", + courtsFilterOpenCase: "Obrir un cas", + courtsCaseMediatorsPh: "IDs d'Oasis dels mediadors, separats per comes", + courtsMediatorsLabel: "Mediadors", + courtsThCase: "Cas", + courtsThCreatedAt: "Data d'inici", + courtsPublicPrefLabel: "Visibilitat després de la resolució", + courtsPublicPrefYes: "Accepto que aquest cas sigui totalment públic", + courtsPublicPrefNo: "Prefereixo mantenir privats els detalls", + courtsPublicPrefSubmit: "Desar la preferència de visibilitat", + courtsMethodMEDIATION: "Mediació", + courtsNoCases: "No hi ha casos.", + reportsTitle: "Informes", + reportsDescription: "Gestiona i segueix els informes de la teva xarxa.", + reportsSearchPlaceholder: "Cercar a Informes...", + reportsFilterAll: "TOTS", + reportsFilterRecent: "RECENTS", + reportsFilterTop: "TOP", + reportsFilterMine: "MEUS", + reportsFilterFeatures: "FUNCIONS", + reportsFilterBugs: "ERRORS", + reportsFilterAbuse: "ABUSOS", + reportsFilterContent: "CONTINGUT", + reportsFilterConfirmed: "CONFIRMATS", + reportsFilterResolved: "RESOLTS", + reportsFilterOpen: "OBERTS", + reportsFilterUnderReview: "EN REVISIÓ", + reportsFilterInvalid: "NO VÀLIDS", + reportsCreateButton: "Crear un informe", + reportsTitleLabel: "Títol", + reportsCategory: "Categoria", + reportsCategoryFeatures: "Funcions", + reportsCategoryBugs: "Errors", + reportsCategoryAbuse: "Abús", + reportsCategoryContent: "Contingut", + reportsUpdateButton: "Actualitzar", + reportsDeleteButton: "Eliminar", + reportsMineSectionTitle: "Els teus informes", + reportsFeaturesSectionTitle: "Sol·licituds de funcions", + reportsBugsSectionTitle: "Errors", + reportsAbuseSectionTitle: "Informes d'abús", + reportsContentSectionTitle: "Problemes de contingut", + reportsAllSectionTitle: "Informes", + reportsNoItems: "No hi ha informes disponibles.", + reportsSeverity: "Gravetat", + reportsSeverityLow: "Baixa", + reportsSeverityMedium: "Mitjana", + reportsSeverityHigh: "Alta", + reportsSeverityCritical: "Crítica", + reportsStatus: "Estat", + reportsStatusOpen: "Obert", + reportsStatusUnderReview: "En revisió", + reportsStatusResolved: "Resolt", + reportsStatusInvalid: "No vàlid", + reportsConfirmButton: "CONFIRMAR L'INFORME!", + reportsConfirmations: "Confirmacions", + reportsConfirmedSectionTitle: "Informes confirmats", + reportsCreateTaskButton: "Crear una tasca", + reportsOpenSectionTitle: "Informes oberts", + reportsUnderReviewSectionTitle: "Informes en revisió", + reportsResolvedSectionTitle: "Informes resolts", + reportsInvalidSectionTitle: "Informes no vàlids", + reportsBugTemplateTitle: "Dades per reproduir (errors)", + reportsFeatureTemplateTitle: "Detalls (funcions)", + reportsAbuseTemplateTitle: "Detalls (abús)", + reportsContentTemplateTitle: "Detalls (problemes de contingut)", + reportsStepsToReproduceLabel: "Passos per reproduir-ho", + reportsStepsToReproducePlaceholder: "1) ...\n2) ...\n3) ...", + reportsExpectedBehaviorLabel: "Resultat esperat", + reportsExpectedBehaviorPlaceholder: "Què hauria de passar?", + reportsActualBehaviorLabel: "Resultat real", + reportsActualBehaviorPlaceholder: "Què passa realment?", + reportsEnvironmentLabel: "Entorn", + reportsEnvironmentPlaceholder: "Versió, dispositiu, SO, navegador, configuració, registres, etc.", + reportsReproduceRateLabel: "Freqüència", + reportsReproduceRateAlways: "Sempre", + reportsReproduceRateOften: "Sovint", + reportsReproduceRateSometimes: "De vegades", + reportsReproduceRateRarely: "Rarament", + reportsReproduceRateUnable: "No es pot reproduir", + reportsReproduceRateUnknown: "No especificat", + reportsProblemStatementLabel: "Problema / necessitat", + reportsProblemStatementPlaceholder: "Quin problema resol aquesta funció?", + reportsUserStoryLabel: "Història d'habitant", + reportsUserStoryPlaceholder: "Com a , vull , per .", + reportsAcceptanceCriteriaLabel: "Criteris d'acceptació", + reportsAcceptanceCriteriaPlaceholder: "- Donat...\n- Quan...\n- Aleshores...", + reportsWhatHappenedLabel: "Què ha passat?", + reportsWhatHappenedPlaceholder: "Descriu l'incident amb context i dates aproximades.", + reportsReportedUserLabel: "Habitant / entitat denunciada", + reportsReportedUserPlaceholder: "@habitant, feed, ID, enllaç, etc.", + reportsEvidenceLinksLabel: "Proves / enllaços", + reportsEvidenceLinksPlaceholder: "Enganxa enllaços, IDs de missatges, captures (si escau), etc.", + reportsContentLocationLabel: "On és el contingut", + reportsContentLocationPlaceholder: "Enllaç, ID, canal, fil, autor, etc.", + reportsWhyInappropriatePlaceholder: "Explica el motiu i l'impacte.", + reportsRequestedActionLabel: "Acció sol·licitada", + reportsRequestedActionPlaceholder: "Eliminar, amagar, marcar, advertir, etc.", + tribesTitle: "Tribus", + tribeFilterAll: "TOTES", + tribeFilterRecent: "RECENTS", + tribeFilterMine: "MEVES", + tribeFilterMembership: "PERTINENÇA", + tribeFilterSubtribes: "SUBTRIBUS", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALERIA", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "MEUS", + tribeFeedFilterALL: "TOTS", + tribeFeedFilterRECENT: "RECENTS", + courtsStanceDENY: "Negar", + courtsStanceADMIT: "Admetre", + courtsStancePARTIAL: "Admetre en part", + tribeAllSectionTitle: "Tribus", + tribeMineSectionTitle: "Les teves tribus", + tribeCreateSectionTitle: "Crear una tribu", + tribeUpdateSectionTitle: "Actualitzar la tribu", + tribeGallerySectionTitle: "Galeria de tribus", + tribeRecentSectionTitle: "Tribus recents", + tribeTopSectionTitle: "Tribus populars", + tribeRootLabel: "ARREL", + tribeDescription: "Explora o crea tribus a la teva xarxa.", + tribeMainTribeLabel: "TRIBU PRINCIPAL", + tribeCreateButton: "Crear una tribu", + tribeUpdateButton: "Actualitzar", + tribeDeleteButton: "Eliminar", + tribeImageLabel: "Pujar contingut multimèdia (màx.: 75MB)", + tribeTitleLabel: "Títol", + searchTribesPlaceholder: "Cercar a Tribus...", + tribeTitlePlaceholder: "Nom de la tribu", + tribeDescriptionLabel: "Descripció", + tribeDescriptionPlaceholder: "Descriu aquesta tribu", + tribeLocationLabel: "Ubicació", + tribeLocationPlaceholder: "On és ubicada aquesta tribu?", + tribeTagsLabel: "Etiquetes", + tribeTagsPlaceholder: "Introdueix etiquetes separades per comes", + tribeInviteMode: "Mode d'invitació", + tribeModeLabel: "MODE", + tribeIsAnonymousLabel: "ESTAT", + tribeMembersCount: "Membres", + tribeEnterInvite: "INTRODUIR LA INVITACIÓ", + subscriptionTitle: "Subscripcions", + subscriptionOn: "Subscrit", + subscriptionOff: "No subscrit", + subscriptionSubscribe: "Subscriure-s'hi", + subscriptionUnsubscribe: "Cancel·lar la subscripció", + subscriptionSubscribers: "Subscriptors", + pmListLabel: "Enviar a", + pmListNone: "— Sense llista —", + pmListNoSubscribers: "Sense subscriptors", + tribeLeaveButton: "SORTIR", + tribePublic: "PÚBLICA", + tribePrivate: "PRIVADA", + tribeGenerateInvite: "INVITACIÓ ÚNICA", + tribeOpenInvitation: "INVITACIÓ OBERTA", + tribeJoinOpen: "UNIR-SE A LA TRIBU", + tribeRemoveInvitation: "ELIMINAR LA INVITACIÓ", + tribeCreatedAt: "Creat el", + tribeAuthor: "Per", + tribeStrict: "Estricte", + tribeOpen: "Oberta", + tribeFeedRefeeds: "Redifusions", + tribeFeedRefeed: "Redifondre", + tribeFeedMessagePlaceholder: "Escriu un feed…", + tribeFeedSend: "Enviar", + tribeFeedEmpty: "Encara no hi ha missatges de feed disponibles.", + noTribes: "Encara no s'han trobat tribus.", + tribeNotFound: "No s'ha trobat la tribu!", + createTribeTitle: "Crear una tribu", + updateTribeTitle: "Actualitzar la tribu", + tribeSectionInhabitants: "Habitants", + tribeSectionVotations: "VOTACIONS", + tribeSectionEvents: "ESDEVENIMENTS", + tribeSectionTasks: "TASQUES", + tribeSectionFeed: "FEED", + tribeSectionForum: "FÒRUM", + tribeSectionMedia: "Multimèdia", + tribeSectionImages: "IMATGES", + tribeSectionAudios: "ÀUDIOS", + tribeSectionVideos: "VÍDEOS", + tribeSectionDocuments: "DOCUMENTS", + tribeSectionBookmarks: "MARCADORS", + tribeSectionMaps: "MAPES", + tribeSectionPads: "PADS", + tribeSectionChats: "XATS", + tribeSectionCalendars: "CALENDARIS", + tribePadCreate: "Crear un pad", + tribeChatCreate: "Crear un xat", + tribeCalendarCreate: "Crear un calendari", + tribePadsEmpty: "Encara no hi ha pads.", + tribeChatsEmpty: "Encara no hi ha xats.", + tribeCalendarsEmpty: "Encara no hi ha calendaris.", + tribeInhabitantsEmpty: "Encara no hi ha habitants en aquesta tribu.", + tribeEventCreate: "Crear un esdeveniment", + tribeEventsEmpty: "Encara no hi ha esdeveniments.", + tribeEventTitle: "Títol", + tribeEventDescription: "Descripció", + tribeEventDate: "Data", + tribeEventLocation: "Ubicació", + tribeEventAttend: "ASSISTIR-HI", + tribeEventUnattend: "NO ASSISTIR-HI", + tribeEventAttendees: "Assistents", + tribeTaskCreate: "Crear una tasca", + tribeTasksEmpty: "Encara no hi ha tasques.", + tribeTaskTitle: "Títol", + tribeTaskDescription: "Descripció", + tribeTaskPriority: "Prioritat", + tribeTaskDeadline: "Data límit", + tribeTaskAssignees: "Assignats", + tribeTaskStatusInProgress: "EN CURS", + tribeTaskStatusClosed: "TANCAR", + tribeTaskAssign: "ASSIGNAR", + tribeTaskUnassign: "DESASSIGNAR", + tribeVotationCreate: "Crear una votació", + tribeVotationsEmpty: "Encara no hi ha votacions.", + tribeVotationTitle: "Títol", + tribeVotationDescription: "Descripció", + tribeVotationOptions: "Opcions", + tribeVotationDeadline: "Data límit", + tribeVotationVote: "VOTAR", + tribeVotationResults: "Vots", + tribeVotationClose: "TANCAR LA VOTACIÓ", + tribeVotationOptionPlaceholder: "Opció", + tribeForumCreate: "Crear un fòrum", + tribeForumEmpty: "Encara no hi ha fils.", + tribeForumTitle: "Títol", + tribeForumText: "Missatge", + tribeForumCategory: "Categoria", + tribeForumReply: "Respondre", + tribeForumReplies: "Respostes", + tribeMediaUpload: "Pujar multimèdia", + readDocument: "Llegir el document", + tribeCreateImage: "Crear una imatge", + tribeCreateAudio: "Crear un àudio", + tribeCreateVideo: "Crear un vídeo", + tribeCreateDocument: "Crear un document", + tribeCreateBookmark: "Crear un marcador", + tribeMediaEmpty: "Encara no hi ha multimèdia.", + tribeMediaTitle: "Títol", + tribeMediaDescription: "Descripció", + tribeMediaTypeImage: "Imatge", + tribeMediaTypeVideo: "Vídeo", + tribeMediaTypeAudio: "Àudio", + tribeMediaTypeDocument: "Document", + tribeMediaTypeBookmark: "Marcador", + tribeContentDelete: "ELIMINAR", + tribeReachTribe: "TRIBU", + tribeReachNotice: "OASIS ho mostra a tots els habitants i CLEARNET també a la web oberta. Obrir-ho deixa una còpia llegible fora de la tribu: tornar a TRIBU ho amaga, però les còpies ja compartides no es poden esborrar.", + tribeReachPrivate: "El contingut d'una tribu privada només es pot quedar dins de la tribu.", + tribeReachOpenedBy: "Obert per", + tribeReachChange: "On es publica", + tribeInviteCodeText: "Codi d'invitació: ", + oasisVersionLabel: "Versió d'Oasis", + tribeInviteCodeHint: "Comparteix aquest codi amb qui vulguis convidar. S'hi pot unir des de /invites.", + tribeStatusOpen: "OBERT", + tribeStatusClosed: "TANCAT", + tribeStatusInProgress: "EN CURS", + tribePriorityLow: "BAIXA", + tribePriorityMedium: "MITJANA", + tribePriorityHigh: "ALTA", + tribePriorityCritical: "CRÍTICA", + tribeTaskFilterAll: "TOTES", + tribeMediaFilterAll: "TOTS", + tribeForumCatGeneral: "GENERAL", + tribeForumCatProposal: "PROPOSTA", + tribeForumCatQuestion: "PREGUNTA", + tribeForumCatAnnouncement: "ANUNCI", + tribeStatusLabel: "Estat", + tribeSubTribes: "SUBTRIBUS", + tribeSubTribesCreate: "Crear una subtribu", + tribeSubTribesStrictDenied: "El mode estricte de la tribu no et permet crear noves subtribus. Posa't en contacte amb l'administrador.", + tribeActivityRefeed: "REFEED", + tribeSectionActivity: "ACTIVITAT", + tribeSectionTrending: "TENDÈNCIES", + tribeSectionTags: "ETIQUETES", + tribeSectionSearch: "CERCAR", + tribeActivityEmpty: "Encara no hi ha activitat.", + tribeActivityCreated: "ha creat", + tribeActivityPosted: "ha publicat", + tribeActivityReplied: "ha respost", + tribeTrendingEmpty: "Encara no hi ha contingut en tendència.", + tribeTrendingPeriodDay: "Avui", + tribeTrendingPeriodWeek: "Aquesta setmana", + tribeTrendingPeriodAll: "Tot", + tribeTrendingEngagement: "interacció", + tribeTopCategory: "Més votat", + tribeTagsEmpty: "No s'han trobat etiquetes.", + tribeSearchEmpty: "Sense resultats.", + tribeSearchMinChars: "Introdueix almenys 2 caràcters per cercar.", + agendaTitle: "Agenda", + agendaDescription: "Aquí pots trobar tots els teus elements assignats.", + agendaSearchPlaceholder: "Cercar a l'Agenda...", + agendaFilterAll: "TOTS", + agendaFilterToday: "AVUI", + agendaFilterUpcoming: "PROPERS", + agendaFilterOverdue: "VENÇUTS", + agendaFilterOpen: "OBERTS", + agendaFilterClosed: "TANCATS", + agendaFilterTasks: "TASQUES", + agendaFilterMarket: "MERCAT", + agendaFilterTribes: "TRIBUS", + agendaFilterEvents: "ESDEVENIMENTS", + agendaFilterReports: "INFORMES", + agendaFilterTransfers: "TRANSFERÈNCIES", + agendaFilterJobs: "FEINES", + agendaFilterIndustry: "INDÚSTRIA", + agendaFilterHousing: "HABITATGE", + agendaFilterProjects: "PROJECTES", + agendaFilterCalendars: "CALENDARIS", + agendaNoItems: "No s'han trobat assignacions.", + agendaDiscardButton: "Descartar", + agendaRestoreButton: "Restaurar", + agendaRemoveButton: "Treure", + agendaMembersCount: "Membres", + agendaLocationLabel: "Ubicació", + agendaYes: "SÍ", + agendaNo: "NO", + agendaInviteModeLabel: "Mode d'invitació", + agendaAnonymousLabel: "Mode anònim", + agendaMembersLabel: "Membres", + agendareportCategory: "Categoria", + agendareportSeverity: "Gravetat", + agendareportStatus: "Estat", + agendaTransferConcept: "Concepte", + agendaTransferAmount: "Quantitat", + agendaTransferDeadline: "Termini", + opinionsTitle: "Opinions", + shareYourOpinions: "Descobreix i vota opinions a la teva xarxa.", + author: "Per", + voteNow: "Vota ara", + alreadyVoted: "Ja has opinat.", + noOpinionsFound: "No s'han trobat opinions.", + TOPButton: "TOP", + opinionsTotalCount: "Total d'opinions", + voteInteresting: "Interessant", + voteNecessary: "Necessari", + voteUseful: "Útil", + voteInformative: "Informatiu", + voteWellResearched: "Ben documentat", + voteNeedsSources: "Necessita fonts", + voteWrong: "Incorrecte", + voteLowQuality: "Baixa qualitat", + voteLove: "Amor", + voteClear: "Clar", + voteMisleading: "Enganyós", + voteOffTopic: "Fora de tema", + voteDuplicate: "Duplicat", + voteClickbait: "Esquer de clics", + votePropaganda: "Propaganda", + voteFunny: "Divertit", + voteInspiring: "Inspirador", + voteUplifting: "Encoratjador", + voteUnnecessary: "Innecessari", + voteRejected: "Rebutjat", + voteConfusing: "Confús", + voteTroll: "Troll", + voteNsfw: "NSFW", + voteViolent: "Violent", + voteToxic: "Tòxic", + voteHarassment: "Assetjament", + voteHate: "Odi", + voteScam: "Estafa", + voteTriggering: "Provocador", + voteInsightful: "Perspicaç", + voteAccurate: "Precís", + voteActionable: "Aplicable", + voteCreative: "Creatiu", + voteSpam: "Spam", + voteAdultOnly: "Només adults", + privateMessage: "MP", + pmSendTitle: "Missatges privats", + pmSend: "Enviar!", + pmCancel: "Cancel·lar", + pmReset: "Restablir", + pmSharedKeyHint: "Comparteix aquesta clau amb el destinatari per un altre canal. Cal per desxifrar.", + pmDescription: "Fes servir aquest formulari per enviar un missatge/fitxer xifrat a altres habitants.", + pmComposeTitle: "Enviar un missatge", + pmRecipients: "Destinataris", + pmRecipientsHint: "Introdueix els IDs d'Oasis separats per comes", + pmLimitsHint: "Fins a 7 habitants per missatge.", + pmTextPlaceholder: "Escriu el teu missatge.", + pmSubject: "Assumpte", + pmSubjectHint: "Introdueix l'assumpte del missatge", + pmText: "Missatge", + pmCrypterChip: "2xE2E", + pmCrypterDecryptButton: "Desxifrar", + fileShareTitle: "Compartir un fitxer", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "Fitxer", + fileShareDownload: "Baixar", + fileShareMutualError: "Només pots compartir fitxers amb habitants amb qui us doneu suport mútuament.", + fileShareNoFile: "No s'ha seleccionat cap fitxer.", + fileShareTooLarge: "El fitxer supera la mida permesa.", + fileShareFailed: "No s'ha pogut preparar el fitxer.", + fileShareSendError: "No s'ha pogut enviar el fitxer.", + fileShareUnavailable: "Aquest fitxer no està disponible ara mateix. Torna-ho a provar més tard.", + pmCrypterBadKey: "La clau compartida és incorrecta!", + pmCrypterTooLong: "El missatge és massa llarg per xifrar-lo amb el Crypter. Escurça'l i torna-ho a provar.", + pmCrypterCipherLabel: "Missatge rexifrat", + pmCrypterCharsLabel: "caràcters", + pmInvalidRecipients: "Oasis ID no vàlid (hauria de ser com @…=.ed25519).", + pmEncryptionLabel: "Xifratge", + private: "Bústia", + privateDescription: "Gestiona els teus missatges privats.", + privateInbox: "BÚSTIA", + privateSent: "ENVIATS", + privateDelete: "Esborrar", + pmCreateButton: "Escriure un MP", + noPrivateMessages: "No hi ha missatges privats.", + pmReply: "Respondre", + pmReplies: "respostes", + inReplyTo: "EN RESPOSTA A", + forumInThread: "AL FIL", + pmPreview: "Previsualitzar", + pmPreviewTitle: "Previsualització", + performed: "→", + pmFromLabel: "De:", + pmToLabel: "Per a:", + pmNoSubject: "(sense assumpte)", + pmSubjectLabel: "Assumpte:", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "La casa governant del L.A.R.P ha canviat.", + politicalBotParliamentTitle: "Ha començat un nou cicle de govern al Parlament.", + politicalBotTribeTitle: "Ha començat un nou cicle de govern en una de les teves tribus.", + politicalBotLarpIntro: "Ara governa {house} durant aquest cicle: {cycle}", + politicalBotParliamentIntro: "El govern actual és {gov}", + politicalBotParliamentIntroLeader: "El govern actual és {gov}, exercit per {leader}", + politicalBotTribeIntro: "El govern actual a la tribu {tribe} és {gov}", + politicalBotTribeIntroLeader: "El govern actual a la tribu {tribe} és {gov}, exercit per {leader}", + politicalBotLeaderLabel: "Líder", + inboxJobSubscribedTitle: "Nova subscripció a la teva oferta de feina", + pmInhabitantWithId: "Habitant amb ID OASIS:", + pmHasSubscribedToYourJobOffer: "s'ha subscrit a la teva oferta de feina", + inboxMarketItemSoldTitle: "Article venut", + inboxShopSoldTitle: "Producte venut", + pmYourItem: "El teu article", + pmHasBeenSoldTo: "s'ha venut a", + pmFor: "per", + inboxProjectPledgedTitle: "Nova aportació al teu projecte", + pmHasPledged: "ha aportat", + pmToYourProject: "al teu projecte", + blockchain: "Explorador", + blockchainDescription: "Explora i visualitza els blocs de la cadena.", + blockchainNoBlocks: "No s'han trobat blocs a la cadena.", + blockchainStatsTitle: "Estadístiques", + blockchainStatsTypeBreakdown: "Desglossament per tipus", + blockchainStatsTopAuthors: "Autors principals", + blockchainStatsCount: "Blocs", + blockchainBlockID: "ID del bloc", + blockchainBlockAuthor: "Autoria", + blockchainBlockCarbon: "Petjada de carboni", + blockchainBlockType: "Tipus", + blockchainBlockTimestamp: "Marca de temps", + blockchainLatestDatagram: "Darrer datagrama", + blockchainDatagram: "Datagrama", + blockchainDetails: "Veure els detalls del bloc", + blockchainViewBlockexplorer: "Veure a Blockexplorer", + blockchainBack: "Tornar a Blockexplorer", + banking: "Banca", + bankingTitle: "Banca", + bankingDescription: "L'economia col·lectiva d'ECOin.", + bankOverview: "Resum", + bankEpochs: "Èpoques", + bankRules: "Regles", + pending: "Pendents", + closed: "Tancades", + bankClaimNow: "Reclamar", + bankClaimUBI: "Reclamar RBU", + bankClaimAndPay: "Reclamar i pagar", + bankClaimedPending: "Reclamació pendent...", + bankStatusClaimed: "Reclamat", + bankStatusExpired: "Caducat", + bankPubOnly: "Operació només per a PUBs", + bankEpoch: "Època", + bankPool: "Fons (aquesta època)", + bankWeightsSum: "Suma de pesos", + bankNoAllocations: "No s'han trobat assignacions.", + bankNoEpochs: "No s'han trobat èpoques.", + bankEpochAllocations: "Assignacions de l'època", + bankAllocId: "ID d'assignació", + bankAllocDate: "Data", + bankAllocConcept: "Concepte", + bankAllocFrom: "De", + bankAllocTo: "Per a", + bankAllocAmount: "Quantitat", + bankAllocStatus: "Estat", + bankEpochId: "ID d'època", + bankRuleHash: "Hash de la instantània de regles", + bankViewEpoch: "Veure l'època", + bankYourIndustryBalance: "La teva part de la indústria", + bankYourUbiMonth: "La teva RBU (aquest mes)", + bankYourFundsMonth: "Fons totals", + bankIndustryBalance: "Producció industrial", + bankUserBalance: "El teu saldo", + bankAddresses: "Adreces", + bankNoAddresses: "No s'han trobat adreces.", + bankUser: "ID d'Oasis", + bankAddress: "Adreça", + bankAddAddressTitle: "Afegir una adreça d'ECOin", + bankAddAddressUser: "ID d'Oasis", + bankAddAddressLabel: "Etiqueta (opcional)", + bankAddAddressLabelPlaceholder: "Moneder de Bob, botiga d'Alice…", + bankAddAddressUserOptional: "Oasis ID (opcional)", + bankAddressUnnamed: "Adreça sense nom", + bankAddAddressAddress: "Adreça d'ECOin", + bankAddAddressSave: "Desar", + bankAddressAdded: "Adreça afegida", + bankAddressUpdated: "Adreça actualitzada", + bankAddressExists: "L'adreça ja existeix", + bankAddressInvalid: "Adreça no vàlida", + bankAddressDeleted: "Adreça eliminada", + bankAddressNotFound: "No s'ha trobat l'adreça", + bankAddressForbidden: "Només pots configurar la teva pròpia adreça de cobrament", + bankAddressTotal: "Total d'adreces", + bankAddressSearch: "Cercar @habitant o adreça", + bankAddressActions: "Accions", + bankAddressDelete: "Eliminar", + bankAddressSource: "Font", + bankAddressDeleteConfirm: "Vols eliminar aquesta adreça?", + search: "Cercar!", + bankLocal: "Local", + bankFromOasis: "Xarxa", + pubIdLabel: "PUB ID", + bankUbiAvailableNo: "SENSE FONS!", + bankUbiAvailableOk: "DISPONIBLE!", + bankUbiAvailability: "RBU", + bankAlreadyClaimedThisMonth: "Ja reclamada aquest mes", + bankUbiThisMonth: "RBU (aquest mes)", + bankUbiLastClaimed: "RBU (darrera reclamada)", + bankUbiNeverClaimed: "Mai reclamada", + bankUbiTotalClaimed: "RBU (total reclamat)", + bankUbiPub: "PUB", + bankUbiInhabitant: "HABITANT", + bankUbiClaimedAmount: "RECLAMAT (ECO)", + typeBankUbiResult: "BANCA - RBU", + bankNoPubConfigured: "No hi ha cap PUB configurat. Estableix l'ID del teu PUB a Configuració.", + shopsTitle: "Botigues", + shopDescription: "Descobreix i gestiona botigues a la xarxa.", + shopTitle: "Botiga", + shopUpload: "Crear una botiga", + shopAllSectionTitle: "Botigues", + shopMineSectionTitle: "Les meves botigues", + shopRecentSectionTitle: "Botigues recents", + shopTopSectionTitle: "Botigues destacades", + shopProductsSectionTitle: "Productes destacats", + shopPricesSectionTitle: "Productes per preu", + shopFavoritesSectionTitle: "Preferides", + shopCreateSectionTitle: "Crear una botiga", + shopUpdateSectionTitle: "Actualitzar la botiga", + shopCreate: "Crear", + shopUpdate: "Actualitzar", + shopDelete: "Eliminar", + shopReachClearnet: "Clearnet", + shopReachOasis: "Oasis", + encryptedChipLabel: "E2E", + transportChipLabel: "Xifrat en trànsit", + shopAddFavorite: "Afegir a preferides", + shopRemoveFavorite: "Treure de preferides", + shopOpen: "OBERTA", + shopClosed: "TANCADA", + shopMakePrivate: "FER-LA PRIVADA (E2E)", + shopMakePublic: "FER-LA PÚBLICA", + shopProducts: "Productes", + shopFilterAll: "TOTES", + shopFilterRecent: "RECENTS", + shopFilterMine: "MEVES", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUCTES", + shopFilterPrices: "PREUS", + shopFilterFavorites: "PREFERITS", + shopProductAdd: "Afegir un producte", + shopProductTitle: "Producte", + shopProductUpdate: "Actualitzar el producte", + shopProductPrice: "Preu (ECO)", + shopProductStock: "Estoc", + shopProductUntitled: "Producte sense títol", + shopUntitled: "Botiga sense títol", + shopNoItems: "No s'han trobat botigues.", + shopNoProducts: "Encara no hi ha productes.", + shopOutOfStock: "Exhaurit", + shopBuy: "Comprar", + shopBuyEncryptedNote: "Les teves dades de lliurament s'envien xifrades; només el propietari de la botiga les pot veure.", + shopBuyDeliveryAddress: "Adreça de lliurament", + shopBuyContact: "Contacte", + shopBuyContactPlaceholder: "correu, telèfon, etc.", + shopBuyNotes: "Notes", + shopBuyDeliveryRequired: "L'adreça de lliurament és obligatòria.", + shopOrdersTitle: "Comandes", + shopOrdersDescription: "Comandes rebudes per aquesta botiga.", + shopOrdersEmpty: "Encara no hi ha comandes.", + shopOrderProduct: "Producte", + shopOrderPrice: "Preu", + shopOrderBuyer: "Comprador", + shopBackToShop: "Tornar a la botiga", + marketVisitItem: "VEURE L'ARTICLE", + shopStatus: "ESTAT", + shopCreatedAt: "CREADA", + shopSearchPlaceholder: "Cercar a Botigues...", + shopUrl: "URL", + shopLocation: "Ubicació", + shopTags: "Etiquetes", + shopVisibility: "Visibilitat", + shopShortDescription: "Descripció curta", + shopShortDescriptionPlaceholder: "Descripció breu per a les targetes (màx. 160 caràcters)", + shopTitlePlaceholder: "Nom de la teva botiga", + shopDescriptionPlaceholder: "Descripció detallada de la teva botiga", + shopLocationPlaceholder: "Ciutat, País", + shopTagsPlaceholder: "Introdueix etiquetes separades per comes", + mapTitlePlaceholder: "Títol del mapa", + shopProductFeatured: "Producte destacat", + shopSendToMarket: "Enviar al mercat", + typeShop: "BOTIGUES", + typeShopProduct: "PRODUCTE DE BOTIGA", + bankExchange: "Intercanvi", + bankExchangeCurrentValue: "Valor d'ECOin (1h)", + bankTotalSupply: "Oferta total d'ECOin", + bankPubsSupply: "Oferta d'ECOin del fons dels PUBs", + bankHoldingSupply: "Oferta d'ECOin fora de la RBU", + bankHoursOfWork: "hores", + bankUnitMs: "ms", + bankUnitSeconds: "segons", + bankUnitMinutes: "minuts", + bankUnitDays: "dies", + bankExchangeNoData: "No hi ha dades disponibles", + bankInflation: "Inflació d'ECOin (anual)", + bankInflationMonthly: "Inflació d'ECOin (mensual)", + bankExchangeChartTitle: "Evolució del valor d'ECOin al llarg del temps", + bankExchangeChartValue: "Valor (ECO/h)", + bankExchangeChartSupply: "Oferta", + bankExchangeChartInflation: "Inflació %", + bankExchangeChartEmpty: "Encara no hi ha prou mostres — torna més tard", + bankCurrentSupply: "Oferta actual d'ECOin", + bankingSyncStatus: "Estat d'ECOin", + bankingSyncStatusSynced: "Sincronitzat", + bankingSyncStatusOutdated: "Sense sincronitzar", + statsTitle: "Estadístiques", + statistics: "Estadístiques", + statsDescription: "Descobreix estadístiques sobre la teva xarxa.", + ALLButton: "TOT", + RECENTButton: "RECENTS", + MINEButton: "MEVES", + TOMBSTONEButton: "ELIMINATS", + statsCreatedAt: "Creat el", + statsYourContent: "Contingut", + statsYourOpinions: "Opinions", + statsNetworkContent: "Contingut", + statsNetworkOpinions: "Opinions", + statsEcoWalletLabel: "Moneder ECOin", + walletAddressCopyHint: "Prem per seleccionar i copiar", + statsEcoWalletNotConfigured: "No configurat!", + statsBookmark: "Marcadors", + statsEvent: "Esdeveniments", + statsTask: "Tasques", + statsVotes: "Vots", + statsMarket: "Mercat", + statsForum: "Fòrums", + statsJob: "Feines", + statsReport: "Informes", + statsFeed: "Feed", + statsTribe: "Tribus", + statsImage: "Imatges", + statsAudio: "Àudios", + statsVideo: "Vídeos", + statsDocument: "Documents", + statsMap: "Mapes", + statsShop: "Botigues", + statsShopProduct: "Productes de botiga", + statsTransfer: "Transferències", + statsAiExchange: "IA", + statsPUBs: "PUBs", + statsSyncedPeers: "Nodes", + statsTotalLabel: "Total", + statsPost: "Publicacions", + statsSize: "Total (mida)", + statsBlockchainSize: "Blockchain (mida)", + statsBlobsSize: "Blobs (mida)", + statsActivity7d: "Activitat (darrers 7 dies)", + statsActivity7dTotal: "Total 7 dies", + statsActivity30dTotal: "Total 30 dies", + day: "Dia", + messages: "Missatges", + statsProject: "Projectes", + statsProjectsTitle: "Projectes", + statsProjectsTotal: "Total de projectes", + statsProjectsActive: "Actius", + statsProjectsCompleted: "Completats", + statsProjectsPaused: "En pausa", + statsProjectsCancelled: "Cancel·lats", + statsProjectsGoalTotal: "Objectiu total", + statsProjectsPledgedTotal: "Aportació total", + statsMarketTitle: "Mercat", + statsMarketTotal: "Articles totals", + statsMarketForSale: "En venda", + statsMarketReserved: "Reservats", + statsMarketClosed: "Tancats", + statsMarketSold: "Venuts", + statsUsersTitle: "Habitants", + user: "Habitant", + statsTombstoneRatio: "Ràtio d'eliminats (%)", + bankingUserEngagementScore: "Puntuació de KARMA", + statsParliamentCandidature: "Candidatures del parlament", + statsParliamentTerm: "Mandats del parlament", + statsParliamentProposal: "Propostes del parlament", + statsParliamentRevocation: "Revocacions del parlament", + statsParliamentLaw: "Lleis del parlament", + statsCourtsCase: "Casos judicials", + statsCourtsEvidence: "Proves judicials", + statsCourtsAnswer: "Respostes judicials", + statsCourtsVerdict: "Veredictes judicials", + statsCourtsSettlement: "Acords judicials", + statsCourtsSettlementProposal: "Propostes d'acord", + statsCourtsSettlementAccepted: "Acords acceptats", + statsCourtsNomination: "Nominacions de jutges", + statsCourtsNominationVote: "Vots de nominació", + ai: "IA", + aiTitle: "IA", + aiDescription: "Una Intel·ligència Artificial Col·lectiva (IAC) anomenada '42' que aprèn de la teva xarxa.", + aiUserQuestion: "Pregunta", + aiResponseTitle: "Resposta", + aiSubmitButton: "Enviar!", + aiSettingsDescription: "Configura el teu prompt (màx. 128 caràcters) per al model d'IA.", + aiPrompt: "Proporciona una resposta informativa i precisa.", + aiConfiguration: "Configurar el prompt", + aiPromptUsed: "Indicació", + aiClearHistory: "Esborrar l'historial", + aiServerError: "No puc respondre bé. Torna-ho a provar.", + aiStatusLoading: "42 està carregant el model. Torna a preguntar d'aquí a un moment.", + aiThinking: "Pensant", + aiWaiting: "Esperant", + aiStatusMissingModel: "El model d'IA no està instal·lat: falta src/AI/oasis-42-1-chat.Q4_K_M.gguf.", + aiExportFineTuning: "Exportar Fine-Tuning", + aiFactsTitle: "Dades de la teva xarxa", + aiInputPlaceholder: "Què és Oasis?", + typeAiExchange: "IA", + aiApproveTrain: "Afegir a l'entrenament col·lectiu", + aiRejectTrain: "No entrenar", + aiTrainApproved: "Aprovat per a l'entrenament", + aiTrainRejected: "Rebutjat per a l'entrenament", + aiSnippetsUsed: "Fragments utilitzats", + aiSnippetsLearned: "Fragments apresos", + statsAITraining: "Entrenament d'IA", + aiCustomAnswerPlaceholder: "Escriu la teva resposta personalitzada…", + marketMineSectionTitle: "Els teus articles", + marketCreateSectionTitle: "Crear un article", + marketUpdateSectionTitle: "Actualitzar", + marketAllSectionTitle: "Mercat", + marketTitle: "Mercat", + marketDescription: "Un mercat per intercanviar béns o serveis a la teva xarxa.", + marketFilterAll: "TOTS", + marketFilterMine: "MEUS", + marketFilterAuctions: "SUBHASTES", + marketFilterItems: "INTERCANVI", + marketFilterNew: "NOU", + marketFilterUsed: "USAT", + marketFilterBroken: "TRENCAT", + marketFilterForSale: "EN VENDA", + marketFilterSold: "VENUT", + marketFilterDiscarded: "DESCARTAT", + marketFilterRecent: "RECENTS", + marketFilterMyBids: "LICITACIONS", + marketCreateButton: "Vendre alguna cosa", + marketItemType: "Tipus", + marketItemTitle: "Títol", + marketItemAvailable: "Data límit", + marketItemDescription: "Descripció", + marketItemDescriptionPlaceholder: "Descriu l'article que vens", + marketItemStatus: "Estat", + marketShopLabel: "Botiga", + marketItemCondition: "Condició", + marketItemPrice: "Preu", + marketItemTags: "Etiquetes", + marketItemTagsPlaceholder: "Introdueix etiquetes separades per comes", + marketItemDeadline: "Data límit", + marketItemIncludesShipping: "Inclou l'enviament?", + marketItemHighestBid: "Licitació més alta", + marketItemStock: "Existències", + marketOutOfStock: "Sense estoc", + marketActionsUpdate: "Actualitzar", + marketUpdateButton: "Actualitzar", + marketActionsDelete: "Eliminar", + marketActionsChangeStatus: "CANVIAR L'ESTAT", + marketActionsBuy: "COMPRAR!", + marketAuctionBids: "Licitacions actuals", + marketPlaceBidButton: "Licitar", + marketItemSeller: "Venedor", + marketNoItems: "Encara no hi ha articles disponibles.", + marketYourBid: "La teva licitació", + marketCreateFormImageLabel: "Pujar contingut multimèdia (màx.: 75MB)", + marketSearchPlaceholder: "Cercar al Mercat...", + marketMinPriceLabel: "Preu mínim", + marketMaxPriceLabel: "Preu màxim", + marketSortRecent: "Més recents", + marketSortPrice: "Preu", + marketSortDeadline: "Data límit", + marketSearchButton: "Cercar", + marketAuctionEndsIn: "Acaba", + marketAuctionEnded: "Ha acabat", + marketNoItemsMatch: "No hi ha articles que coincideixin amb la teva cerca.", + housingTitle: "Habitatge", + housingDescriptionText: "Descobreix i gestiona llocs a la teva xarxa.", + modulesHousingLabel: "Habitatge", + modulesHousingDescription: "Mòdul per descobrir i gestionar llocs.", + typeHousing: "HABITATGE", + housingFilterAll: "TOT", + housingFilterMine: "MEUS", + housingFilterRequested: "SOL·LICITATS", + housingFilterSale: "VENDA", + housingFilterRent: "LLOGUER", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "DISPONIBLES", + housingFilterClosed: "NO DISPONIBLES", + housingFilterRecent: "RECENTS", + housingFilterTop: "TOP", + housingType: "Oferta", + housingTypeSALE: "En venda", + housingTypeRENT: "En lloguer", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Immoble", + housingPropertyAPARTMENT: "Pis", + housingPropertyHOUSE: "Casa", + housingPropertyROOM: "Habitació", + housingPropertyLAND: "Terreny", + housingPropertyOTHER: "Altres", + housingTitleLabel: "Títol", + housingTitlePlaceholder: "Nom del lloc", + housingDescription: "Descripció", + housingDescriptionPlaceholder: "Descriu el lloc, el barri, què inclou: banys, jardí, llits…", + housingRules: "Normes", + housingRulesPlaceholder: "Mascotes, fumar, hores de silenci, estada mínima…", + housingPlace: "Lloc", + housingPlacePlaceholder: "País, ciutat, adreça", + housingPrice: "Preu", + housingPriceLabel: "Preu (ECO)", + housingSizeLabel: "Superfície (m²)", + housingFree: "GRATIS", + housingPerMonth: "/mes", + housingRooms: "Habitacions", + housingSize: "Superfície", + housingCapacity: "Hostes", + housingAvailableFrom: "Disponible des de", + housingAvailableTo: "Disponible fins a", + housingTags: "Etiquetes", + spreadEditWarning: "Aquest contingut perdrà les seves rèpliques si l'edites", + housingRequests: "Sol·licituds", + housingRequestButton: "Sol·licitar", + housingCancelButton: "Cancel·lar la sol·licitud", + housingRequestedBadge: "SOL·LICITAT", + housingStatus: "Estat", + housingStatusOPEN: "DISPONIBLE", + housingStatusCLOSED: "NO DISPONIBLE", + housingSetOpen: "Marcar com a disponible", + housingSetClosed: "Marcar com a no disponible", + housingCreateButton: "Crear un lloc", + housingUpdateButton: "Actualitzar", + housingSearchPlaceholder: "Cercar a Habitatge...", + housingSearchButton: "Cercar", + housingMinPrice: "Preu mín.", + housingMaxPrice: "Preu màx.", + housingSortRecent: "Més recents", + housingSortPrice: "Preu més baix", + housingSortRequests: "Més sol·licitats", + housingSortRating: "Més ben valorats", + housingNoItems: "No s'han trobat llocs.", + jobsTitle: "Feina", + jobsCandidatesTitle: "Candidats suggerits", + jobsCandidatesDescription: "Habitants amb competències que encaixen amb aquesta feina. Envia'ls un missatge privat per convidar-los.", + jobsCandidatesPmBody: "Hola, crec que el teu perfil encaixa amb la meva oferta de feina", + jobsDescription: "Descobreix i gestiona ofertes de feina a la teva xarxa.", + jobsFilterRecent: "RECENTS", + jobsFilterMine: "MEVES", + jobsFilterAll: "TOTES", + jobsFilterRemote: "REMOTES", + jobsFilterOpen: "OBERTES", + jobsFilterClosed: "TANCADES", + jobsCV: "CV", + jobsCreateJob: "Publicar una feina", + jobsRecentTitle: "Feines recents", + jobsMineTitle: "Les teves feines", + jobsAllTitle: "Ofertes de feina", + jobsRemoteTitle: "Feines remotes", + jobsOpenTitle: "Feines obertes", + jobsClosedTitle: "Feines tancades", + jobsCVTitle: "CV", + jobsFilterPresencial: "PRESENCIAL", + jobsFilterFreelancer: "FREELANCE", + jobsFilterEmployee: "ASSALARIAT", + jobsPresencialTitle: "Feines presencials", + jobsFreelancerTitle: "Feines freelance", + jobsEmployeeTitle: "Feines assalariades", + jobTitle: "Títol", + jobLocation: "Ubicació", + jobSalary: "Salari (ECO/1h)", + jobVacants: "Vacants", + jobDescription: "Descripció", + jobRequirements: "Requisits", + jobLanguages: "Idiomes", + jobStatus: "Estat", + jobStatusOPEN: "OBERTA", + jobStatusCLOSED: "TANCADA", + jobSetClosed: "Marcar com a TANCADA", + jobSubscribeButton: "Uneix-te a aquesta oferta!", + jobUnsubscribeButton: "Surt d'aquesta oferta!", + jobTitlePlaceholder: "Introdueix el títol de la feina", + jobDescriptionPlaceholder: "Descriu el lloc de treball", + jobRequirementsPlaceholder: "Introdueix els requisits", + jobLanguagesPlaceholder: "Anglès, francès, castellà, català...", + jobTasksPlaceholder: "Llista de tasques", + jobLocationPresencial: "Presencial", + jobLocationRemote: "Remot", + jobVacantsPlaceholder: "Nombre de vacants", + jobImage: "Pujar contingut multimèdia (màx.: 75MB)", + jobTasks: "Tasques", + jobType: "Tipus de feina", + jobTime: "Temps de feina", + jobSubscribers: "Subscriptors", + noSubscribers: "Sense subscriptors", + jobsFilterTop: "TOP", + jobsTopTitle: "Feines més ben pagades", + createJobButton: "Publicar una feina", + noJobsFound: "No s'han trobat ofertes de feina.", + jobAuthor: "Per", + jobTypeFreelance: "Autònom", + jobTypeSalary: "Assalariat", + jobTypeExchange: "Intercanvi d'hores", + jobTypeEXCHANGE: "INTERCANVI D'HORES", + jobsFilterExchange: "INTERCANVI", + jobsExchangeTitle: "Feines per intercanvi d'hores", + jobsHoursOffered: "Hores oferides", + jobsHoursRequested: "Hores demanades a canvi", + jobsExchangeSkill: "Habilitat demanada a canvi", + jobTimePartial: "Parcial", + jobTimeComplete: "Completa", + jobsUpdateButton: "ACTUALITZAR", + jobsFilterApplied: "CANDIDATURA", + jobsAppliedTitle: "Les meves candidatures", + jobsAppliedBadge: "Inscrit", + jobsSearchPlaceholder: "Cercar a Feina...", + jobsMinSalaryLabel: "Salari mín.", + jobsMaxSalaryLabel: "Salari màx.", + jobsSortRecent: "Més recents", + jobsSortSalary: "Salari més alt", + jobsSortSubscribers: "Més candidats", + jobsSearchButton: "Cercar", + jobsApplicants: "Candidats", + jobsUpdatedAt: "Actualitzat", + jobSetOpen: "Marcar com a oberta", + jobsTagsLabel: "Etiquetes", + noJobsMatch: "No hi ha ofertes que coincideixin amb la teva cerca.", + industrySearchPlaceholder: "Cercar a Indústria...", + industryAllSectors: "Tots els sectors", + industryVisitButton: "Visitar Indústria", + industryAdvanceTo: "Canviar a", + industryAmount: "Quantitat", + industryApproveBuild: "Aprovar", + industryBlueprint: "Plànol", + industryBlueprints: "Plànols", + industryBuild: "Producció", + industryBuildNotes: "Notes", + industryBuildStart: "Data d'inici", + industryVoteUpdateButton: "Votar l'actualització", + industryVoteDeleteButton: "Votar l'esborrament", + industryRevokeVote: "Revocar", + industryTimeLeft: "Temps restant", + industryBuildEnd: "Data de fi", + industryBuilds: "Produccions", + industryBuildTitle: "Títol", + industryContribute: "Contribuir", + industryContributeButton: "Contribuir", + industryContributions: "Contribucions", + industryCreateBlueprintButton: "Crear un plànol", + industryDistribute: "Distribuir", + industryDistributeButton: "Distribuir per quotes", + industryDistribution: "Distribució", + industryEcoAmount: "Quantitat (ECO)", + industryEditBlueprint: "Editar el plànol", + industryFacility: "Fàbrica", + industryKindLabel: "Tipus", + industryKind_labor: "Mà d'obra", + industryKind_material: "Material", + industryKind_eco: "Fons", + industryLaborHours: "Mà d'obra (hores)", + industryHours: "Hores", + industryMaterialItem: "Material", + industryMaterials: "Materials", + industryEstMaterials: "Total de materials", + industryEstLabor: "Total de mà d'obra", + industryEstTotal: "Preu estimat", + industryBuildingPrice: "Preu de construcció", + industryFinalPrice: "Preu final", + industryBlueprintDescPlaceholder: "Especificacions, dimensions, pes, documentació…", + industryMaterialValue: "Valor (ECO)", + industryMember: "Membre", + industryNewBlueprint: "Nou plànol", + industryNewBuild: "Proposar una producció", + industryEditBuild: "Editar la producció", + industryNeedBlueprint: "Crea primer un plànol: cada producció en fabrica un.", + industryNoBlueprints: "Encara no hi ha plànols.", + industryNoBuilds: "Encara no hi ha produccions.", + industryNote: "Nota", + industryOutput: "Sortida", + industryOutputKind: "Tipus de producte", + industryOutputValue: "Valor de sortida (ECO, p. ex. de la venda)", + industryPostJob: "Crear una feina", + industryPot: "Pot", + industryProposeBuildButton: "Proposar la producció", + industryProposer: "Proponent", + industryAuthor: "Autor", + industrySellOnMarket: "Enviar al mercat", + industrySendToProjects: "Crear un projecte", + industrySkills: "Habilitats", + industryTreasury: "Tresoreria", + industryKind_physical: "Físic", + industryKind_digital: "Digital", + industryBuildStatus_PROPOSED: "PROPOSADA", + industryBuildStatus_REJECTED: "REBUTJADA", + industryBuildStatus_APPROVED: "APROVADA", + industryBuildStatus_STOCKING: "PROVEINT-SE", + industryBuildStatus_IN_PRODUCTION: "EN PRODUCCIÓ", + industryBuildStatus_COMPLETED: "COMPLETADA", + industryBuildStatus_FAILED: "FALLIDA", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Una feina encaixa amb el teu currículum", + jobsBotMatchIntro: "Una feina encaixa amb el teu currículum.", + jobsBotMatchJob: "Feina", + jobsBotMatchHint: "Reps això perquè el teu currículum està gestionat per IA. Ho pots desactivar al teu CV.", + cvAiManaged: "Gestionat per IA", + switchOn: "ACTIVAT", + switchOff: "DESACTIVAT", + cvAiManagedHint: "Si està actiu, JobsBot t'avisa per missatge privat quan una feina encaixa amb el teu currículum.", + cvMatchThreshold: "Avisa'm a partir d'aquest nivell de coincidència", + housingBotRequestedTitle: "Algú ha sol·licitat un dels teus llocs.", + housingBotCancelledTitle: "S'ha cancel·lat una sol·licitud sobre un dels teus llocs.", + housingBotYouRequestedTitle: "Has sol·licitat un lloc.", + housingBotYouCancelledTitle: "Has cancel·lat una sol·licitud.", + housingBotUnavailableTitle: "Un lloc que vas sol·licitar ja no està disponible.", + housingBotUnavailableSentTitle: "Has avisat qui va sol·licitar aquest lloc.", + industryBotAdmittedTitle: "T'han admès en una fàbrica.", + industryBotApplicationTitle: "Nova sol·licitud d'ingrés a la teva fàbrica.", + industryBotInvitedTitle: "T'han convidat a una fàbrica.", + industryBotDissolvedTitle: "Una fàbrica de la qual formes part s'ha dissolt.", + industryBotBuildApprovedTitle: "S'ha aprovat una producció.", + industryBotDistributedTitle: "S'ha distribuït la sortida d'una producció.", + industryFilterRules: "REGLES", + industryRulesTitle: "Regles", + industryRulesIntro: "Indústria permet als habitants posseir col·lectivament els mitjans de producció: una Fàbrica és un taller de la xarxa que ningú no posseeix de manera privada.", + industryRulesSteward: "El fundador és el majordom — un custodi, no un amo: no pot vendre, privatitzar ni dissoldre la Fàbrica tot sol.", + industryRulesMembership: "La política de pertinença defineix com s'hi uneixen els habitants: Oberta (hi entres a l'instant), Per vot (t'admet el vot dels membres) o Només per invitació (un membre t'ha de convidar abans).", + industryRulesQuorum: "El quòrum és el nombre mínim de membres que han de votar perquè una decisió compti, perquè una Fàbrica petita no la decideixi un sol habitant.", + industryRulesMajority: "La majoria és la fracció de vots necessària per aprovar (0,5 = meitat, 1 = unanimitat); una decisió s'aprova quan els SÍ arriben almenys a max(quòrum, membres × majoria).", + industryRulesDecisions: "Els membres voten per admetre'n de nous, aprovar produccions i dissoldre la Fàbrica; el majordom no es pot saltar aquestes decisions col·lectives.", + industryRulesBlueprints: "Els plànols són receptes lliures (COPYLEFT) — les entrades (materials, hores de feina, habilitats) i la sortida (un bé físic o digital) — i qualsevol en pot fer un fork.", + industryRulesBuilds: "Una producció és una ordre de fabricació: PROPOSADA → APROVADA (per vot) → PROVEINT-SE → EN PRODUCCIÓ → COMPLETADA, i només accepta contribucions un cop aprovada.", + industryRulesContributions: "Els membres aporten feina (hores), materials (valor declarat en ECO) o ECO a una producció; cada aportació guanya karma.", + industryRulesLaborRate: "La tarifa de feina és el valor en ECO d'una hora de feina en aquesta Fàbrica; converteix hores en karma per comparar esforç i capital de manera justa: karma = hores × tarifa + valor material + ECO.", + industryRulesShares: "La teva quota en una producció és el teu karma ÷ karma total, i determina quina part del valor de sortida reps.", + industryRulesDistribution: "Quan una producció es completa, el majordom reparteix el pot (els fons de la producció més el valor de venda) entre qui hi va contribuir, en proporció a la seva participació.", + industryRulesTreasury: "El majordom custodia les aportacions en ECO com a tresoreria de la producció fins al repartiment; tots els moviments queden registrats a la xarxa i les disputes poden anar als Tribunals.", + industryJobs: "Feines", + industryReportButton: "Informar", + industryCreateTask: "Crear una tasca", + industryOpenDispute: "Obrir una disputa", + industryMaterialsPlaceholder: "placa-solar:2:120\nbateria:1:80\nkit:1:15", + industryCreateInvite: "Generar una invitació", + industryPauseVotes: "Vots d'estat", + industryTitle: "Indústria", + industryDescription: "Mòdul per gestionar col·lectivament els mitjans de producció.", + typeIndustryBuild: "PRODUCCIÓ", + typeIndustryBlueprint: "PLÀNOL", + typeIndustryAllocation: "DISTRIBUCIÓ", + typeIndustry: "INDÚSTRIA", + modulesIndustryLabel: "Indústria", + modulesIndustryDescription: "Mòdul per gestionar col·lectivament els mitjans de producció.", + industryFilterAll: "TOTES", + industryFilterRecent: "RECENTS", + industryFilterMember: "MEMBRE", + industryFilterBlueprints: "PLÀNOLS", + industryFilterBuilds: "PRODUCCIONS", + industryFilterMine: "MEVES", + industryFilterActive: "EN MARXA", + industryFilterPaused: "EN PAUSA", + industryFilterDissolved: "DISSOLTES", + industryAllTitle: "Indústria", + industryRecentTitle: "Fàbriques recents", + industryMemberTitle: "Fàbriques on soc", + industryMineTitle: "Les meves fàbriques", + industryActiveTitle: "En marxa", + industryPausedTitle: "Fàbriques en pausa", + industryDissolvedTitle: "Fàbriques dissoltes", + industryNoFacilitiesFound: "No s'han trobat fàbriques.", + industryCreateFacility: "Crear una fàbrica", + industryMembers: "Membres", + industryMemberBadge: "MEMBRE", + industryName: "Nom", + industryNamePlaceholder: "Nom de la fàbrica", + industryDescriptionLabel: "Descripció", + industryDescriptionPlaceholder: "Què produeix aquesta fàbrica?", + industrySector: "Sector", + industryMembershipPolicy: "Política de pertinença", + industryQuorum: "Quòrum", + industryMajority: "Majoria", + industryLaborRate: "Tarifa de feina", + industryCreateButton: "Crear la fàbrica", + industryUpdateButton: "Actualitzar", + industrySteward: "Majordom", + industryStatusActive: "EN MARXA", + industryStatusPaused: "EN PAUSA", + industryStatusDissolved: "DISSOLTA", + industryStatusLabel: "Estat", + industryJoinButton: "Unir-s'hi", + industryApplyButton: "Sol·licitar l'ingrés", + industryLeaveButton: "Sortir", + industryInviteButton: "Convidar", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Necessites una invitació per unir-t'hi.", + industryPauseButton: "Votar la pausa", + industryResumeButton: "Votar la represa", + industryDeleteButton: "Eliminar", + industryPendingApplicants: "Sol·licituds pendents", + industryVotesYes: "sí", + industryVotesNo: "no", + industryAlreadyVoted: "votat", + industryAdmit: "Admetre", + industryReject: "Rebutjar", + industryDissolveTitle: "Dissoldre la fàbrica", + industryDissolveVote: "Votar la dissolució", + industryPolicy_open: "Oberta", + industryPolicy_vote: "Per vot", + industryPolicy_invite: "Només per invitació", + projectsTitle: "Projectes", + projectsDescription: "Crea, finança i segueix projectes impulsats per la comunitat a la teva xarxa.", + projectSearchPlaceholder: "Cercar a Projectes...", + projectCreateProject: "Crear un projecte", + projectCreateButton: "Crear un projecte", + projectUpdateButton: "ACTUALITZAR", + projectDeleteButton: "ELIMINAR", + projectNoProjectsFound: "No s'han trobat projectes.", + projectFilterAll: "TOTS", + projectFilterMine: "ELS MEUS PROJECTES", + projectFilterActive: "ACTIUS", + projectFilterPaused: "EN PAUSA", + projectFilterCompleted: "COMPLETATS", + projectFilterFollowing: "SEGUITS", + projectFilterRecent: "RECENTS", + projectFilterTop: "TOP", + projectAllTitle: "Projectes", + projectMineTitle: "Els teus projectes", + projectActiveTitle: "Projectes actius", + projectPausedTitle: "Projectes en pausa", + projectCompletedTitle: "Projectes completats", + projectFollowingTitle: "Projectes seguits", + projectRecentTitle: "Projectes recents", + projectTopTitle: "Més ben finançats", + projectTitlePlaceholder: "Nom del projecte", + projectImage: "Pujar contingut multimèdia (màx.: 75MB)", + projectDescription: "Descripció", + projectDescriptionPlaceholder: "Explica la història i els objectius…", + projectGoal: "Objectiu (ECO)", + projectGoalPlaceholder: "50000", + projectDeadline: "Data límit", + projectProgress: "Progrés inicial (%)", + projectStatus: "Estat", + projectFunding: "Finançament", + projectPledged: "Compromès", + projectSetStatus: "Establir l'estat", + projectSetProgress: "Actualitzar el progrés", + projectFollowButton: "SEGUIR", + projectUnfollowButton: "DEIXAR DE SEGUIR", + projectStatusACTIVE: "ACTIU", + projectStatusPAUSED: "EN PAUSA", + projectStatusCOMPLETED: "COMPLETAT", + projectStatusCANCELLED: "CANCEL·LAT", + projectPledgeTitle: "Dona suport a aquest projecte", + projectPledgePlaceholder: "Quantitat en ECO", + projectBounties: "Recompenses", + projectNoBounties: "No s'han trobat recompenses.", + projectTitle: "Títol", + projectMilestoneSelect: "Seleccionar una fita", + projectBountyCreateButton: "Crear una recompensa", + projectBountyStatus: "Estat de la recompensa", + projectBountyOpen: "oberta", + projectBountyClaimed: "reclamada", + projectBountyDone: "completada", + projectBountyClaimedBy: "reclamada per", + projectBountyClaimButton: "reclamar", + projectBountyCompleteButton: "Marcar com a completada", + projectMilestones: "Fites", + projectAddMilestoneTitle: "Nova fita", + projectMilestoneTitle: "Títol de la fita", + projectMilestoneTargetPercent: "Percentatge (%)", + projectMilestoneDueDate: "Data", + projectMilestoneStatus: "Estat de la fita", + projectMilestoneOpen: "oberta", + projectMilestoneDone: "completada", + projectMilestoneDue: "venç", + projectMilestoneMarkDone: "Marcar com a feta", + projectMilestoneTitlePlaceholder: "Introdueix el títol de la fita", + projectMilestoneDescriptionPlaceholder: "Introdueix la descripció d'aquesta fita", + projectMilestoneDescription: "Descripció de la fita", + projectBudgetAssigned: "Assignat a recompenses", + projectBudgetOver: "⚠ Pressupost excedit: l'assignat supera l'objectiu", + projectFollowers: "Seguidors", + projectFollowersTitle: "Seguidors", + projectFollowersNone: "Encara no hi ha seguidors.", + projectMore: "més", + projectYouFollowHint: "Segueixes aquest projecte", + projectBackers: "Mecenes", + projectBackersTitle: "Mecenes", + projectBackersTotal: "Total de mecenes", + projectBackersTotalPledged: "Total aportat", + projectBackersYourPledge: "La teva aportació", + projectBackersNone: "Encara no hi ha aportacions.", + projectFilterBackers: "MECENES", + projectFilterApplied: "APORTANT", + projectAppliedTitle: "APORTANT", + projectBackersLeaderboardTitle: "Mecenes destacats", + projectNoBackersFound: "No hi ha mecenes.", + projectBackerAmount: "Total aportat", + projectBackerPledges: "Aportacions", + projectBackerProjects: "Projectes", + projectSelectMilestoneOrBounty: "Seleccionar una fita o una recompensa", + projectPledgeButton: "Aportar", + footerLicense: "GPLv3", + modulesModuleName: "Nom", + modulesModuleDescription: "Descripció", + modulesModuleStatus: "Estat", + modulesTotalModulesLabel: "Total", + modulesEnabledModulesLabel: "Habilitats", + modulesDisabledModulesLabel: "Deshabilitats", + modulesInvitesLabel: "Invitacions", + modulesInvitesDescription: "Mòdul per gestionar i aplicar codis d'invitació.", + modulesWalletLabel: "Moneder", + modulesWalletDescription: "Mòdul per gestionar els teus actius digitals (ECOin).", + modulesCipherLabel: "Xifratge", + modulesCipherDescription: "Mòdul per xifrar i desxifrar el teu text de manera simètrica (amb una contrasenya compartida).", + modulesBookmarksLabel: "Marcadors", + modulesBookmarksDescription: "Mòdul per descobrir i gestionar marcadors.", + modulesVideosLabel: "Vídeos", + modulesVideosDescription: "Mòdul per descobrir i gestionar vídeos.", + modulesDocsLabel: "Documents", + modulesDocsDescription: "Mòdul per descobrir i gestionar documents.", + modulesAudiosLabel: "Àudios", + modulesAudiosDescription: "Mòdul per descobrir i gestionar àudios.", + modulesTagsLabel: "Etiquetes", + modulesTagsDescription: "Mòdul per descobrir i explorar patrons de taxonomia (etiquetes).", + modulesImagesLabel: "Imatges", + modulesImagesDescription: "Mòdul per descobrir i gestionar imatges.", + modulesTrendingLabel: "Tendències", + modulesTrendingDescription: "Mòdul per explorar el contingut més popular.", + modulesEventsLabel: "Esdeveniments", + modulesEventsDescription: "Mòdul per descobrir i gestionar esdeveniments.", + modulesTasksLabel: "Tasques", + modulesTasksDescription: "Mòdul per descobrir i gestionar tasques.", + modulesMarketLabel: "Mercat", + modulesMarketDescription: "Mòdul per intercanviar béns o serveis.", + modulesShopsLabel: "Botigues", + modulesShopsDescription: "Mòdul per gestionar i descobrir botigues.", + modulesTribesLabel: "Tribus", + modulesTribesDescription: "Mòdul per explorar o crear tribus (grups).", + modulesVotationsLabel: "Votacions", + modulesVotationsDescription: "Mòdul per descobrir i gestionar votacions.", + modulesParliamentLabel: "Parlament", + modulesParliamentDescription: "Mòdul per elegir governs i votar lleis.", + modulesCourtsLabel: "Tribunals", + modulesCourtsDescription: "Mòdul per resoldre conflictes i emetre veredictes.", + modulesReportsLabel: "Informes", + modulesReportsDescription: "Mòdul per gestionar i fer el seguiment d'informes sobre problemes, errors, abusos i avisos de contingut.", + modulesOpinionsLabel: "Opinions", + modulesOpinionsDescription: "Mòdul per descobrir i votar opinions.", + modulesTransfersLabel: "Transferències", + modulesTransfersDescription: "Mòdul per descobrir i gestionar contractes intel·ligents (transferències).", + modulesFediverseLabel: "Multivers", + modulesBlogsLabel: "Blogs", + modulesBlogsDescription: "Mòdul per descobrir i gestionar blogs.", + modulesPollsLabel: "Enquestes", + modulesPollsDescription: "Mòdul per preguntar a la xarxa i comptar les respostes.", + modulesFediverseDescription: "Gestiona els teus altres comptes del multivers, incloent-hi enviar i rebre contingut.", + modulesFeedLabel: "Feed", + modulesFeedDescription: "Mòdul per descobrir i compartir textos breus (feeds).", + modulesPixeliaLabel: "Pixelia", + modulesPixeliaDescription: "Mòdul per dibuixar en una graella col·laborativa.", + modulesMelodyLabel: "Melody", + modulesMelodyDescription: "Mòdul per generar i compartir el \"so\" de la teva blockchain.", + modulesAgendaLabel: "Agenda", + modulesAgendaDescription: "Mòdul per gestionar tots els teus elements assignats.", + modulesAILabel: "IA", + modulesAIDescription: "Mòdul per parlar amb un LLM anomenat '42'.", + modulesForumLabel: "Fòrums", + modulesForumDescription: "Mòdul per descobrir i gestionar fòrums.", + modulesJobsLabel: "Feina", + modulesJobsDescription: "Mòdul per descobrir i gestionar ofertes de feina.", + modulesProjectsLabel: "Projectes", + modulesProjectsDescription: "Mòdul per explorar, finançar i gestionar projectes.", + modulesBankingLabel: "Banca", + modulesBankingDescription: "Mòdul per gestionar el tresor comú: valor d'ECOin, impostos i RBU.", + modulesFavoritesLabel: "Preferits", + modulesFavoritesDescription: "Mòdul per gestionar el teu contingut preferit.", + fileTooLargeTitle: "Fitxer massa gran", + fileTooLargeMessage: "El fitxer supera la mida màxima permesa (75 MB). Si us plau, selecciona un fitxer més petit.", + goBack: "Tornar", + errorDismiss: "D'acord", + errorPageTitle: "S'ha produït un error", + errorNoAllocation: "No s'ha trobat cap assignació per a aquesta reclamació.", + errorInvalidClaim: "Aquesta assignació no es pot reclamar.", + visibilityLabel: "Visibilitat", + clearnetPrivateConflict: "Un contingut privat, ocult, tancat, només per invitació o de pagament no es pot publicar a la CLEARNET. Tria OASIS o fes-lo públic.", + visibilityPublic: "Pública", + visibilityHidden: "Oculta", + visibilityMakePublic: "Fer-la pública", + visibilityMakeHidden: "Fer-la oculta", + invitesInhabitantsTitle: "Habitants", + aiNavPlaceholder: "Descriu què necessites...", + aiNavDisabled: "La navegació amb IA està desactivada.", + aiNavResultsTitle: "Suggeriments d'AINav", + aiNavQueryLabel: "Consulta", + aiNavResultMatch: "Coincidència", + aiNavResultsEmpty: "No hi ha rutes coincidents. Prova amb /search.", + modulesAINavLabel: "AINav", + modulesAINavDescription: "Mòdul per fer peticions en llenguatge natural sobre el contingut de la xarxa.", + directConnectDescription: "Connecta't directament a un node introduint-ne l'adreça IP, el port i la clau pública. El node s'afegirà com a connexió seguida.", + peerHost: "IP", + peerPort: "Port", + peerPublicKey: "Clau pública (@...ed25519)", + connectAndFollow: "Connectar", + modulesPresetTitle: "Configuracions comunes", + workflowsTitle: "Fluxos de treball", + workflowsDescription: "Un flux de treball fixa el tema i quins mòduls estan actius.", + workflowsSet: "Aplicar el flux de treball", + workflow_default: "Per defecte", + workflow_jobs: "Feina", + workflow_social: "Social", + workflow_business: "Negocis", + workflow_night: "Nit", + workflow_kids: "Infants", + workflow_activists: "Activisme", + workflow_mobile: "Mòbil", + statsCarbonFootprintTitle: "Petjada de carboni", + statsCarbonTombstone: "Petjada dels esborrats (tombstoning)", + uploadMedia: "Pujar contingut multimèdia (màx.: 75MB)", + invitesUnfollow: "Deixar de seguir", + invitesFollow: "Seguir", + invitesUnfollowedInvites: "No federades", + invitesNoUnfollowed: "No hi ha pubs no federats.", + reportsWhyInappropriateLabel: "Per què és inadequat?", + blockchainContentDeleted: "Aquest contingut s'ha eliminat", + visitContent: "Visitar el contingut", + favoriteAdd: "Fixar a preferits", + pmContentTooltip: "Enviar un missatge privat", + phoneCallTooltip: "Fer una trucada", + favoriteRemove: "Treure de preferits", + reportContent: "Denunciar el contingut", + bankEcoinHours: "Equivalència d'ECOin en temps (1 ECO)", + mapsLabel: "Mapes", + mapTitle: "Mapes", + mapDescription: "Gestiona i comparteix mapes fora de línia a la teva xarxa.", + mapMineSectionTitle: "Els teus mapes", + mapCreateSectionTitle: "Crear un mapa", + mapUpdateSectionTitle: "Actualitzar el mapa", + mapAllSectionTitle: "Mapes", + mapRecentSectionTitle: "Mapes recents", + mapFavoritesSectionTitle: "Preferits", + mapFilterAll: "TOTS", + mapFilterMine: "MEUS", + mapFilterRecent: "RECENTS", + mapFilterFavorites: "PREFERITS", + mapUploadButton: "Crear un mapa", + mapCreateButton: "Crear un mapa", + mapUpdateButton: "Actualitzar", + mapDeleteButton: "Eliminar", + mapAddFavoriteButton: "Afegir a preferits", + mapRemoveFavoriteButton: "Treure de preferits", + mapLatLabel: "Latitud", + mapLatPlaceholder: "p. ex. 41.3874", + mapLngLabel: "Longitud", + mapLngPlaceholder: "p. ex. 2.1686", + mapDescriptionLabel: "Descripció", + mapDescriptionPlaceholder: "Descriu el mapa o la ubicació...", + mapTypeLabel: "Tipus de mapa", + mapInviteMode: "Invitació", + mapTypeSingle: "ÚNIC", + mapTypeOpen: "OBERT", + mapTypeClosed: "TANCAT", + mapTagsLabel: "Etiquetes", + mapTagsPlaceholder: "Introdueix etiquetes separades per comes", + mapUrlLabel: "URL del mapa", + mapMarkersTitle: "Marcadors", + mapMarkerDefault: "Marcador", + mapMarkerLatLabel: "Latitud del marcador", + mapMarkerLngLabel: "Longitud del marcador", + mapMarkerLabelField: "Etiqueta del marcador", + mapMarkerLabelPlaceholder: "Descriu aquest marcador...", + markerImageLabel: "Imatge del marcador", + mapAddMarkerTitle: "Afegir un marcador", + mapAddMarkerButton: "Afegir un marcador", + mapCleanMarkerButton: "Netejar el marcador", + mapApplyZoom: "Aplicar el zoom", + mapPlaceSearchPlaceholder: "Cerca un lloc o país...", + mapNoPlaces: "No s'han trobat llocs.", + mapClusterTitle: "marcadors aquí, fes clic per apropar", + mapDeleteMarker: "Elimina el marcador", + mapZoomIn: "Apropa", + mapZoomOut: "Allunya", + mapFitMarkers: "Ajusta a tots els marcadors", + mapPanLabel: "Mou el mapa", + mapCenterHere: "Centra aquí", + mapNear: "prop de", + mapPickedSpot: "Punt triat", + mapPickHint: "Fes clic al mapa per triar un punt.", + mapClearPick: "Neteja", + mapSearchPlaceholder: "Cercar a Mapes...", + mapSearchButton: "Cercar", + mapUpdatedAt: "Actualitzat", + mapNoMatch: "Cap mapa no coincideix amb la teva cerca.", + noMaps: "No hi ha mapes disponibles.", + mapLocationTitle: "Ubicació", + mapVisitLabel: "Visitar el mapa", + typeMap: "MAPES", + typeMapMarker: "MARCADOR DE MAPA", + modulesMapLabel: "Mapes", + modulesMapDescription: "Mòdul per gestionar i compartir mapes fora de línia.", + emergenciesTitle: "Emergències", + modulesEmergenciesLabel: "Emergències", + modulesEmergenciesDescription: "Mòdul per llançar, confirmar i seguir emergències a la xarxa.", + emergenciesDescription: "Mòdul per llançar, confirmar i seguir emergències a la xarxa.", + emergencyCreate: "Crear una emergència", + emergencyUpdate: "Actualitzar", + emergencyDelete: "Esborrar", + emergencyTitleLabel: "Títol", + emergencyTitlePlaceholder: "Què està passant?", + emergencyTextLabel: "Detalls", + emergencyTextPlaceholder: "On, des de quan, què cal fer...", + emergencyCategoryLabel: "Categoria", + emergencyCategoryWeather: "METEOROLOGIA", + emergencyCategoryInfrastructure: "INFRAESTRUCTURA", + emergencyCategoryHealth: "SALUT", + emergencyCategorySecurity: "SEGURETAT", + emergencyCategoryLost: "PERDUTS", + emergencyCategoryNeighborhood: "VEÏNAT", + emergencyExpiresLabel: "Caduca d'aquí a", + emergencyExpires1d: "1 dia", + emergencyExpires3d: "3 dies", + emergencyExpires7d: "1 setmana", + emergencyExpires30d: "1 mes", + emergencyExpiresNone: "Indefinit", + emergencyExpiresKeep: "Mantenir la data actual", + emergencyTagsLabel: "Etiquetes", + emergencyTagsPlaceholder: "Introdueix etiquetes separades per comes", + emergencyFilterAll: "TOTES", + emergencyFilterMine: "MEVES", + emergencyFilterRecent: "RECENTS", + emergencySeverityUnverified: "SENSE VERIFICAR", + emergencySeverityLow: "BAIXA", + emergencySeverityMedium: "MITJANA", + emergencySeverityHigh: "ALTA", + emergencyStatusActive: "ACTIVA", + emergencyStatusResolved: "RESOLTA", + emergencyStatusExpired: "CADUCADA", + emergencyConfirmations: "Confirmacions", + emergencyConfirmButton: "Confirmar", + emergencyConfirmed: "Has confirmat aquesta emergència", + emergencyResolveButton: "Resoldre", + emergencyReopenButton: "Reobrir", + emergencyGenerateReport: "Generar un informe", + emergencyUpdateEdited: "Editada", + emergencyLastUpdate: "DARRERA ACTUALITZACIÓ", + emergencyUpdatesTitle: "Darreres actualitzacions:", + emergencyNoUpdates: "Encara no hi ha actualitzacions.", + emergencyAddUpdate: "Publicar una actualització", + emergencyUpdatePlaceholder: "Què ha canviat?", + emergencyNoItems: "No s'han trobat emergències.", + emergencyExpiresAt: "Caduca", + emergencyBannerLabel: "Emergència activa", + emergencyCreateSectionTitle: "Crear una emergència", + emergencyUpdateSectionTitle: "Actualitzar l'emergència", + emergencySearchPlaceholder: "Cercar a emergències...", + typeEmergency: "EMERGÈNCIES", + typeEmergencyConfirm: "CONFIRMACIÓ D'EMERGÈNCIA", + typeEmergencyUpdate: "ACTUALITZACIÓ D'EMERGÈNCIA", + statsEmergency: "Emergències", + statsEmergencyConfirm: "Confirmacions d'emergències", + statsEmergencyUpdate: "Actualitzacions d'emergències", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "Una emergència que segueixes té una actualització.", + emergencyBotResolvedTitle: "Una emergència que segueixes s'ha resolt.", + dataKindEmergencies: "EMERGÈNCIES", + mailingTitle: "Llistes de correu", + modulesMailingLabel: "Llistes de correu", + modulesMailingDescription: "Descobreix i gestiona llistes de correu a la teva xarxa.", + modulesPhoneLabel: "Telèfon", + modulesPhoneDescription: "Mòdul per trucar a altres habitants amb videotrucades xifrades d'extrem a extrem.", + mailingDescription: "Descobreix i gestiona llistes de correu a la teva xarxa.", + mailingCreate: "Crear una llista de correu", + mailingUpdate: "Actualitzar", + mailingArchive: "Arxivar", + mailingActivate: "Activar", + mailingCreateSectionTitle: "Crear una llista de correu", + mailingUpdateSectionTitle: "Actualitzar la llista de correu", + mailingTitleLabel: "Títol", + mailingTitlePlaceholder: "Nom de la llista de correu", + mailingDescriptionLabel: "Descripció", + mailingDescriptionPlaceholder: "De què tracta aquesta llista?", + mailingTypeLabel: "Tipus de llista de correu", + mailingTypeOpen: "OBERTA", + mailingTypeClosed: "TANCADA", + mailingStatusLabel: "Estat", + mailingStatusActive: "ACTIVA", + mailingStatusArchived: "ARXIVADA", + mailingMembersLabel: "Convidar habitants", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Subscriptors", + mailingTagsLabel: "Etiquetes", + mailingTagsPlaceholder: "Introdueix etiquetes separades per comes", + mailingFilterAll: "TOTES", + mailingFilterMine: "MEVES", + mailingFilterSubscribed: "SUBSCRITES", + mailingFilterRecent: "RECENTS", + mailingHistoryDate: "DATA", + mailingHistoryThreads: "FILS", + mailingHistoryInhabitants: "HABITANTS", + mailingHistorySearchPlaceholder: "Cercar a l'historial...", + mailingNoMessages: "Encara no hi ha missatges.", + mailingNoItems: "No s'han trobat llistes de correu.", + mailingSearchPlaceholder: "Cercar a llistes de correu...", + mailingWriteButton: "Escriure un missatge", + mailingWriteTitle: "Escriure a la llista", + mailingReplyTitle: "Respondre a la llista", + mailingSend: "Enviar", + mailingTextPlaceholder: "El teu missatge arriba a tots els participants de la llista.", + mailingParticipants: "Participants", + inboxFilterMailing: "Llistes de correu", + mailingReplyPlaceholder: "Respondre a la llista...", + mailingReplyToList: "Respondre a la llista", + typeMailingList: "LLISTA DE CORREU", + statsMailingList: "Llistes de correu", + mailingListLabel: "LLISTA DE CORREU", + logisticsTitle: "Logística", + modulesLogisticsLabel: "Logística", + modulesLogisticsDescription: "Descobreix i gestiona plans logístics a la teva xarxa.", + logisticsDescription: "Descobreix i gestiona plans logístics a la teva xarxa.", + logisticsCreate: "Crear una ruta", + logisticsUpdate: "Actualitzar", + logisticsClose: "Tancar", + logisticsReopen: "Reobrir", + logisticsCreateSectionTitle: "Crear una ruta", + logisticsUpdateSectionTitle: "Actualitzar la ruta", + logisticsTitleLabel: "Títol", + logisticsTitlePlaceholder: "Viatge al mercat, furgoneta disponible...", + logisticsKindLabel: "Tipus de logística", + logisticsKindTrip: "VIATGE", + logisticsKindShipment: "ENVIAMENT", + logisticsKindTrips: "VIATGES", + logisticsKindShipments: "ENVIAMENTS", + logisticsModeLabel: "Mode", + logisticsModeOffer: "OFEREIXO", + logisticsModeRequest: "BUSCO", + logisticsModeOffers: "OFERTES", + logisticsModeRequests: "PETICIONS", + logisticsDescriptionLabel: "Descripció", + logisticsDescriptionPlaceholder: "Punt de trobada, què portes, condicions...", + logisticsOriginLabel: "Origen", + logisticsOriginPlaceholder: "On comença", + logisticsDestinationLabel: "Destinació", + logisticsDestinationPlaceholder: "On acaba", + logisticsDateLabel: "Data i hora", + logisticsRecurrenceLabel: "Recurrència", + logisticsFrequencyLabel: "Freqüència", + logisticsRecurrenceNone: "ÚNIC", + logisticsRecurrenceDaily: "DIARI", + logisticsRecurrenceWeekly: "SETMANAL", + logisticsRecurrenceMonthly: "MENSUAL", + logisticsSeatsLabel: "Places", + logisticsSizeLabel: "Mida", + logisticsSizePlaceholder: "p. ex. 2m x 1m", + logisticsWeightLabel: "Pes", + logisticsWeightPlaceholder: "p. ex. 40kg", + logisticsPriceFree: "GRATIS", + logisticsPriceEco: "ECO", + logisticsPriceTime: "TEMPS", + logisticsPriceLabel: "Preu", + logisticsHours: "h", + logisticsOrderRefLabel: "Enllaç a la comanda", + logisticsOrderRefPlaceholder: "/shops/... o /market/... de la comanda que cal lliurar", + logisticsOrderLink: "Comanda enllaçada", + logisticsTagsLabel: "Etiquetes", + logisticsTagsPlaceholder: "Introdueix etiquetes separades per comes", + logisticsFilterAll: "TOTES", + logisticsFilterMine: "MEVES", + logisticsFilterBooked: "RESERVADES", + logisticsFilterRecent: "RECENTS", + logisticsFilterHistory: "HISTORIAL", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "OBERTA", + logisticsStatusUpcoming: "PROPERES", + logisticsStatusPast: "PASSADES", + logisticsStatusClosed: "TANCADES", + logisticsZoneLabel: "Zona", + logisticsSearchPlaceholder: "Cercar a logística...", + logisticsNoItems: "No s'han trobat rutes.", + logisticsBookingPending: "PENDENT", + logisticsBookingConfirmed: "CONFIRMADA", + logisticsBookingRejected: "REBUTJADA", + logisticsBookingCancelled: "CANCEL·LADA", + logisticsBookingDelivered: "LLIURADA", + logisticsConfirm: "Confirmar", + logisticsReject: "Rebutjar", + logisticsDeliver: "Lliurar", + logisticsReceiptLabel: "Rebut", + logisticsReceiptPlaceholder: "Nota de lliurament (opcional)", + logisticsCancelBooking: "Cancel·lar la reserva", + logisticsBookTitle: "Reservar aquesta ruta", + logisticsNotesLabel: "Notes", + logisticsNotesPlaceholder: "El que hagi de saber la persona responsable", + logisticsBook: "Reservar", + logisticsBookingsTitle: "Reserves", + logisticsMyBookingTitle: "La teva reserva", + logisticsRatingsTitle: "Valoracions", + logisticsNoRatings: "Encara no hi ha valoracions.", + logisticsRatingsWhenClosed: "Les valoracions s'obren en tancar la ruta.", + logisticsScoreLabel: "Puntuació", + logisticsRatingPlaceholder: "Com ha anat?", + logisticsRate: "Valorar", + typeLogisticsRoute: "RUTA", + typeLogisticsRating: "VALORACIÓ DE RUTA", + statsLogisticsRoute: "Rutes", + statsLogisticsRating: "Valoracions de rutes", + logisticsRouteLabel: "RUTA", + podcastsTitle: "Podcasts", + modulesPodcastsLabel: "Podcasts", + modulesPodcastsDescription: "Mòdul per publicar canals de podcast.", + podcastsDescription: "Mòdul per publicar canals de podcast.", + podcastCreate: "Crear un podcast", + podcastUpdate: "Actualitzar", + podcastDelete: "Esborrar", + podcastCreateSectionTitle: "Crear un podcast", + podcastUpdateSectionTitle: "Actualitzar el podcast", + podcastTitleLabel: "Títol", + podcastTitlePlaceholder: "Nom del canal", + podcastDescriptionLabel: "Descripció", + podcastDescriptionPlaceholder: "De què tracta aquest podcast?", + podcastCategoryLabel: "Categoria", + podcastCategoryNews: "NOTÍCIES", + podcastCategoryMusic: "MÚSICA", + podcastCategoryTalk: "XERRADA", + podcastCategoryEducation: "EDUCACIÓ", + podcastCategoryTech: "TECNOLOGIA", + podcastCategoryCulture: "CULTURA", + podcastCategoryCommunity: "COMUNITAT", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "ALTRES", + podcastTagsLabel: "Etiquetes", + podcastCoverLabel: "Portada (imatge o vídeo)", + podcastEpisodeAudioLabel: "Fitxer d'àudio o vídeo (màx.: 75MB)", + podcastTagsPlaceholder: "Introdueix etiquetes separades per comes", + podcastFilterAll: "TOTS", + podcastFilterMine: "MEUS", + podcastFilterRecent: "RECENTS", + podcastFilterTop: "TOP", + podcastFilterViewers: "OIENTS", + podcastSearchPlaceholder: "Cercar a podcasts...", + podcastNoItems: "No s'han trobat podcasts.", + podcastEpisodesLabel: "Episodis", + podcastPlaysLabel: "Reproduccions", + podcastLastEpisode: "Darrer episodi", + podcastEpisodesTitle: "Episodis", + podcastNoEpisodes: "Encara no hi ha episodis.", + podcastEpisodeAdd: "Afegir un episodi", + podcastEpisodeCreateTitle: "Publicar un episodi", + podcastEpisodeUpdateTitle: "Actualitzar l'episodi", + podcastEpisodeTitleLabel: "Títol de l'episodi", + podcastEpisodeTitlePlaceholder: "Títol de l'episodi", + podcastEpisodeDescriptionLabel: "Notes", + podcastEpisodeDescriptionPlaceholder: "Què passa en aquest episodi, enllaços, crèdits...", + podcastEpisodePublish: "Publicar", + podcastPlayed: "Reproduït", + podcastMarkPlayed: "Marcar com a reproduït", + typePodcast: "PODCAST", + typePodcastEpisode: "EPISODI", + typePodcastOpinion: "OPINIÓ D'EPISODI", + typePodcastPlay: "REPRODUCCIÓ", + statsPodcast: "Podcasts", + statsPodcastEpisode: "Episodis", + statsPodcastPlay: "Reproduccions", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "Un blog que segueixes té una entrada nova.", + podcastBotEpisodeTitle: "Un podcast que segueixes té un episodi nou.", + podcastLabel: "PODCAST", + podcastEpisodeLabel: "EPISODI", + campaignsTitle: "Campanyes", + modulesCampaignsLabel: "Campanyes", + modulesCampaignsDescription: "Descobreix i gestiona campanyes a la teva xarxa.", + campaignsDescription: "Descobreix i gestiona campanyes a la teva xarxa.", + campaignCreate: "Crear una campanya", + campaignUpdate: "Actualitzar", + campaignDelete: "Esborrar", + campaignClose: "Tancar", + campaignReopen: "Reobrir", + campaignCreateSectionTitle: "Crear una campanya", + campaignUpdateSectionTitle: "Actualitzar la campanya", + campaignTitleLabel: "Títol", + campaignTitlePlaceholder: "Què vols aconseguir?", + campaignTextLabel: "Text", + campaignTextPlaceholder: "Explica la causa, qui decideix i què demanes...", + campaignCategoryLabel: "Categoria", + campaignCategoryEnvironment: "MEDI AMBIENT", + campaignCategoryRights: "DRETS", + campaignCategoryHealth: "SALUT", + campaignCategoryEducation: "EDUCACIÓ", + campaignCategoryInfrastructure: "INFRAESTRUCTURA", + campaignCategoryCulture: "CULTURA", + campaignCategoryEconomy: "ECONOMIA", + campaignCategoryOther: "ALTRES", + campaignGoalLabel: "Objectiu de signatures", + campaignDeadlineLabel: "Termini", + campaignTagsLabel: "Etiquetes", + campaignTagsPlaceholder: "Introdueix etiquetes separades per comes", + campaignFilterAll: "TOTES", + campaignFilterMine: "MEVES", + campaignFilterSigned: "SIGNADES", + campaignFilterRecent: "RECENTS", + campaignFilterTop: "TOP", + campaignStatusOpen: "OBERTA", + campaignStatusAchieved: "ACONSEGUIDA", + campaignStatusUnachieved: "NO ACONSEGUIDA", + campaignStatusClosed: "TANCADA", + campaignSearchPlaceholder: "Cercar a campanyes...", + campaignNoItems: "No s'han trobat campanyes.", + campaignSignaturesLabel: "Signatures", + campaignSignaturesTitle: "Suports", + campaignNoSignatures: "Encara no hi ha signatures.", + campaignSignTitle: "Signar aquesta campanya", + campaignSignPlaceholder: "Per què hi dones suport? (opcional)", + campaignSign: "Signar", + campaignSigned: "Ja l'has signada!", + campaignNotSigned: "Encara hi falta la teva signatura", + campaignElevate: "Elevar al Parlament", + campaignElevated: "Elevada al Parlament", + campaignUpdatesTitle: "Actualitzacions", + campaignNoUpdates: "Encara no hi ha actualitzacions.", + campaignUpdatePlaceholder: "Què ha passat?", + campaignAddUpdate: "Publicar una actualització", + campaignQrAlt: "QR per signar aquesta campanya", + campaignQrCaption: "Escaneja per signar", + typeCampaign: "CAMPANYA", + typeCampaignSignature: "SIGNATURA", + typeCampaignUpdate: "ACTUALITZACIÓ DE CAMPANYA", + statsCampaign: "Campanyes", + statsCampaignSignature: "Signatures de campanyes", + statsCampaignUpdate: "Actualitzacions de campanyes", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "Una campanya que segueixes té una actualització.", + campaignBotAchievedTitle: "Una campanya que segueixes ha assolit el seu objectiu.", + campaignBotRaisedTitle: "Una campanya que has signat s'ha elevat al Parlament.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Una ruta que vas reservar ha canviat.", + logisticsBotClosedTitle: "Una ruta que vas reservar s'ha tancat.", + logisticsBotBookedTitle: "Algú ha reservat la teva ruta.", + logisticsBotCancelledTitle: "S'ha cancel·lat una reserva de la teva ruta.", + logisticsBotConfirmedTitle: "La teva reserva s'ha confirmat: el paquet és en camí.", + logisticsBotRejectedTitle: "La teva reserva s'ha rebutjat.", + logisticsBotDeliveredTitle: "El teu paquet ha arribat.", + campaignLabel: "CAMPANYA", + campaignGenerateReport: "Generar un informe", + logisticsGenerateReport: "Generar un informe", + mailingGenerateReport: "Generar un informe", + campaignUpdateEdited: "Editada", + mailingMessagesLabel: "Missatges", + logisticsBookingsLabel: "Reserves", + backupTitle: "Còpia de seguretat", + modulesBackupLabel: "Còpia de seguretat", + modulesBackupDescription: "Mòdul per exportar i importar les teves claus i fer còpies de seguretat xifrades.", + backupDescription: "Mòdul per exportar i importar les teves claus i fer còpies de seguretat xifrades.", + backupTypeRecovery: "RECUPERACIÓ", + backupTypeKeys: "EXPORTAR CLAUS", + backupTypeFull: "CÒPIA COMPLETA", + backupTypeRestore: "RESTAURAR UNA CÒPIA", + backupTypeInstant: "INSTANTÀNIA", + backupInstantDescription: "Un .zip pla amb tot el teu registre i els teus fitxers, sense la teva clau secreta i sense contrasenya. Ràpida, però ", + backupInstantRisk: "qui tingui el fitxer ho pot llegir tot.", + backupKitPageDescription: "El kit de recuperació només es pot obrir des d'aquest dispositiu.", + backupKitId: "ID d'Oasis", + backupKitDate: "Generat", + backupKitQrAlt: "Codi QR de la teva clau secreta", + backupKitStep1: "Imprimeix-lo o copia'l a mà i desa'l en un lloc segur i sense connexió.", + backupKitStep2: "Per recuperar-lo, instal·la Oasis en un dispositiu nou i importa aquest secret des de Còpia de seguretat › Claus.", + backupKitStep3: "Destrueix aquesta pàgina si mai queda exposada.", + backupKitPdfButton: "Generar PDF", + backupScopeLabel: "Abast", + backupScopeAll: "TOT", + backupScopeMine: "NOMÉS EL MEU CONTINGUT", + backupSinceLabel: "Des de", + backupEstimateTitle: "Estimació", + backupFileInfo: "La teva còpia xifrada es baixarà al teu dispositiu (nom: oasis-backup-DATA.oasisbk)", + backupExportButton: "Baixar la còpia", + backupRestorePassword: "Escriu la contrasenya que vas fer servir en fer la còpia", + backupRestoreButton: "Restaurar la còpia", + backupRestoredLine: "Còpia restaurada.", + backupRestoredMessages: "Missatges afegits", + backupRestoredSkipped: "Ja existents", + backupRestoredBlobs: "Fitxers afegits", + backupRestoredState: "Configuració restaurada", + backupRestoredFailed: "Fallits", + backupRestoredForked: "Divergents", + backupRestoreRunning: "S'està restaurant la còpia. Aquesta pàgina s'actualitza sola; mentrestant pots continuar fent servir Oasis.", + backupRestoreElapsed: "Transcorregut", + backupRestoreFailedLine: "No s'ha pogut restaurar la còpia.", + backupRestoreForkMine: "Aquest dispositiu ja ha publicat missatges amb la teva identitat que no són a la còpia, així que no s'hi ha pogut aplicar a sobre. Restaura-la en una instal·lació neta just després d'importar les teves claus, abans de fer res més.", + verificationTitle: "Verificació", + verificationDescription: "Comprova la cadena del teu feed, detecta bifurcacions i revisa fitxers orfes o absents.", + verificationRun: "Executar la verificació", + verificationTotalMessages: "missatges al registre", + verificationFeed: "El teu feed", + verificationMessages: "missatges", + verificationLastSequence: "darrera seqüència", + verificationGaps: "Buits", + verificationBrokenLinks: "Enllaços trencats", + verificationBadHashes: "Hashes incorrectes", + verificationBadSignatures: "Signatures incorrectes", + verificationDuplicates: "Seqüències duplicades", + verificationForks: "Bifurcacions", + verificationBlobs: "Fitxers", + verificationBlobsPresent: "presents", + verificationBlobsReferenced: "referenciats", + verificationBlobsOrphan: "orfes", + verificationBlobsMissing: "absents", + verificationBlobsMissingOwn: "Fitxers teus que falten en aquest dispositiu", + verificationBlobsMissingOthers: "Fitxers d'altres habitants encara sense baixar", + verificationBlobsUnavailable: "No s'han pogut llistar els fitxers d'aquest dispositiu.", + wikiTitle: "Wikis", + modulesWikiLabel: "Wikis", + modulesWikiDescription: "Mòdul per descobrir i gestionar wikis.", + wikiDescription: "Mòdul per descobrir i gestionar wikis.", + wikiCreate: "Crear una wiki", + wikiUpdate: "Actualitzar", + wikiDelete: "Esborrar", + wikiEdit: "Editar", + wikiTitleLabel: "Títol", + wikiTitlePlaceholder: "Títol de la wiki...", + wikiBodyLabel: "Contingut", + wikiBodyPlaceholder: "Enllaça altres wikis amb [[Títol de la wiki]] o [[Títol de la wiki|un text propi]].\n\nMenciona un habitant amb @id.\n\nEtiqueta amb #oasis.\n\nEnganxa enllaços com https://solarnethub.com\n\nUna línia en blanc obre un paràgraf nou; els salts de línia simples es conserven.\n\nAdjunta qualsevol mitjà compatible i s'incrustarà aquí.", + formatBarLabel: "Format", + wikiStatusLabel: "Tipus de wiki", + wikiStatusOpen: "OBERTA", + wikiStatusClosed: "TANCADA", + wikiTagsLabel: "Etiquetes", + wikiTagsPlaceholder: "Introdueix etiquetes separades per comes", + wikiSummaryLabel: "Resum del canvi", + wikiSummaryPlaceholder: "Què ha canviat?", + wikiFilterAll: "TOTES", + wikiFilterMine: "MEVES", + wikiFilterRecent: "RECENTS", + wikiFilterLinked: "ENLLAÇADES", + wikiChanges: "Canvis recents", + wikiNoPages: "No s'han trobat wikis.", + wikiNoChanges: "Encara no hi ha canvis.", + wikiHistory: "Historial", + wikiVersion: "Versió", + wikiVersions: "versions", + wikiRestore: "Restaurar", + wikiDiff: "Canvis", + wikiViewVersion: "Veure", + wikiLinkedPages: "Pàgines enllaçades", + wikiNoLinkedPages: "Aquesta pàgina encara no enllaça cap altra.", + wikiMissingLink: "La pàgina encara no existeix", + wikiDuplicateTitle: "Ja existeix una pàgina amb aquest títol. Canvia el títol per crear una pàgina diferent.", + wikiCreateSectionTitle: "Crear una wiki", + wikiUpdateSectionTitle: "Actualitzar la wiki", + wikiSearchPlaceholder: "Cercar a la Wiki...", + wikiCurrentVersion: "Versió actual", + wikiViewingVersion: "Estàs veient una versió antiga", + wikiBackToPage: "Tornar a la pàgina", + wikiLinkedChip: "Enllaçada", + wikiRestoredSummary: "Versió restaurada", + wikiExportPdf: "Generar la pàgina", + wikiPreview: "Previsualització", + tribeSectionWiki: "WIKI", + tribeWikiCreate: "Crear una wiki", + tribeWikiEmpty: "Encara no hi ha wikis.", + typeWiki: "WIKI", + padsTitle: "Pads", + padTitle: "Pad", + modulesPadsLabel: "Pads", + modulesPadsDescription: "Mòdul per gestionar editors de text col·laboratius.", + padCreate: "Crear un pad", + padUpdate: "Actualitzar", + padDelete: "Eliminar", + padTitleLabel: "Títol", + padTitlePlaceholder: "Escriu el títol del pad...", + padStatusLabel: "Estat", + padTypeLabel: "Tipus de pad", + padStatusOpen: "OBERT", + padStatusInviteOnly: "NOMÉS CONVIDATS", + padStatusClosed: "TANCAT", + padDeadlineLabel: "Data límit", + padTagsLabel: "Etiquetes", + padTagsPlaceholder: "Introdueix etiquetes separades per comes", + padMembersLabel: "Membres", + padCreated: "Creat", + padStartEditing: "Editar", + padEditorPlaceholder: "Comença a escriure...", + padSubmitEntry: "Enviar", + padNoEntries: "Encara no hi ha entrades.", + padAllSectionTitle: "Tots els pads", + padMineSectionTitle: "Els meus pads", + padsDescription: "Gestiona editors de text xifrats i col·laboratius a la teva xarxa.", + roomsTitle: "Sales", + roomsDescription: "Descobreix i uneix-te a sales de videoconferència xifrades a la teva xarxa.", + modulesRoomsLabel: "Sales", + modulesRoomsDescription: "Mòdul per descobrir i unir-se a sales de videoconferència xifrades.", + roomFilterRecent: "RECENTS", + roomFilterMine: "MEVES", + roomFilterAll: "TOTES", + roomFilterLive: "EN DIRECTE", + roomFilterOpen: "OBERTES", + roomFilterInvite: "NOMÉS CONVIDATS", + roomFilterClosed: "TANCADES", + roomCreate: "Crear sala", + roomUpdate: "Actualitzar", + roomDelete: "Eliminar", + roomClose: "Tancar", + roomReopen: "Reobrir", + roomCreateSectionTitle: "Crear sala", + roomUpdateSectionTitle: "Actualitzar sala", + roomTitlePlaceholder: "Títol de la sala", + roomDescriptionLabel: "Descripció", + roomDescriptionPlaceholder: "De què parlareu?", + roomTypeLabel: "Tipus de sala", + roomTagsLabel: "Etiquetes", + roomStatusOpen: "OBERTA", + roomStatusInviteOnly: "NOMÉS CONVIDATS", + roomStatusClosed: "TANCADA", + roomParticipants: "Participants", + roomJoin: "Entrar", + roomLeave: "Sortir", + roomFull: "Sala plena", + roomLiveChip: "EN DIRECTE", + roomMuted: "silenciat", + roomRec: "Gravar àudio", + roomRecStop: "Atura la gravació", + roomStreamFile: "Emet", + roomStreamStop: "Atura", + roomStreaming: "Emetent", + liveFullscreen: "Pantalla completa", + roomStreamPickTitle: "Tria un vídeo", + roomStreamPickHint: "Vídeos d'aquest dispositiu. El que triïs s'emet des d'on és, sense copiar-lo ni publicar-lo.", + roomStreamGo: "Emet a la sala", + roomStreamNotFound: "Aquest vídeo no és a les teves carpetes.", + roomPublicWarning: "Aquesta sala es retransmet a la web pública: qualsevol pot veure i sentir el que hi passa.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} ha entrat a la sala", + roomEventLeft: "{name} ha sortit de la sala", + roomEventRecStart: "{name} ha començat a gravar", + roomEventRecStop: "{name} ha deixat de gravar", + roomEventMuted: "{name} s'ha silenciat", + roomEventUnmuted: "{name} ha tret el silenci", + roomEventHand: "{name} ha aixecat la mà", + roomRaiseHand: "Aixecar la mà", + roomLowerHand: "Abaixar la mà", + confirmActionTitle: "Confirma aquesta acció", + confirmActionButton: "Confirmar", + confirmJoinText: "Estàs a punt d'unir-te amb aquesta invitació.", + confirmFollowText: "Estàs a punt de donar suport a aquest habitant.", + confirmSignText: "Estàs a punt de signar aquesta campanya.", + confirmDownloadText: "Estàs a punt de descarregar aquest fitxer al teu dispositiu.", + roomRecordings: "Gravacions", + roomClearNotices: "Netejar", + roomLineSwitchboard: "Centraleta", + roomLineDndError: "El número d'aquesta sala no accepta trucades.", + roomNumberHint: "Marca aquest número des de Telèfon per entrar a la sala.", + liveRefresh: "Refrescar", + roomNobody: "Ara mateix no hi ha ningú a la sala.", + roomSomeone: "{n} a la sala ara mateix.", + roomYou: "tu", + roomYouAreIn: "Hi ets dins", + roomNobodyInside: "Ara mateix no hi ha ningú en aquesta sala.", + roomErrorFull: "La sala és plena. Podràs entrar quan algú en surti.", + roomErrorUnreachable: "Ara mateix no es pot arribar a la sala. Torna-ho a provar més tard.", + roomErrorBusy: "Ja ets en una trucada o en una altra sala.", + roomErrorClosed: "Aquesta sala està tancada.", + roomAccessDenied: "Aquesta sala és només amb invitació. Necessites una invitació per entrar.", + roomsNoItems: "No s'han trobat sales.", + roomSearchPlaceholder: "Cercar sales...", + roomInviteMode: "Invitació", + tribeSectionRooms: "SALES", + tribeRoomCreate: "Crear sala", + tribeRoomsEmpty: "Encara no hi ha sales.", + typeRoom: "SALES", + typeRoomMember: "PARTICIPANT DE SALA", + statsRoom: "Sales", + statsRoomMember: "Participants de sales", + favoritesFilterRooms: "SALES", + invitesRoomsTitle: "Sales", + inviteCodeInvalid: "Aquest codi d'invitació no és vàlid o ja s'ha fet servir.", + inviteAlreadyMember: "Ja en formes part. Pots obrir-ho des del seu mòdul.", + actionFailed: "No s'ha pogut fer. Torna-ho a provar.", + publishTooShort: "La teva publicació és massa curta.", + walletSendFailed: "No s'ha pogut enviar el pagament: {reason}", + pixeliaCoordsError: "Aquestes coordenades són fora de la quadrícula.", + courtsErrTitle: "Cal un títol.", + courtsErrRespondent: "Indica contra qui és el cas.", + courtsErrRespondentId: "Aquest no és un Oasis ID vàlid.", + courtsErrMethod: "Tria com es resoldrà el cas.", + courtsErrCaseNotFound: "No s'ha trobat aquest cas.", + courtsErrBrief: "Escriu la teva resposta.", + courtsErrStance: "Tria la teva postura.", + courtsErrResult: "Escriu el resultat.", + courtsErrTerms: "Escriu els termes de l'acord.", + courtsErrMediator: "Afegeix almenys un mediador.", + courtsErrSelfMediator: "No pots ser mediador en el teu propi cas.", + courtsErrJudge: "Tria un jutge.", + courtsErrSelfJudge: "No pots ser jutge del teu propi cas.", + courtsErrVisibility: "Tria qui pot veure el cas.", + courtsErrNomination: "No s'ha trobat aquesta nominació.", + opinionAlreadyGiven: "Ja has donat la teva opinió.", + cipherPasswordShort: "La contrasenya ha de tenir almenys 32 caràcters.", + errorForbidden: "No hi tens accés.", + errorNotFound: "No s'ha trobat.", + errorClosed: "Està tancat.", + errorInviteOnly: "Això és només amb invitació. Necessites una invitació.", + errorTooLong: "Aquest text és massa llarg.", + errorRequired: "Falta informació obligatòria.", + errorInvalid: "Part de la informació no és vàlida.", + errorNotAuthor: "Només qui ho va crear pot fer això.", + panicRemoved: "La teva blockchain s'ha eliminat.", + padRecentSectionTitle: "Pads recents", + padOpenSectionTitle: "Pads oberts", + padClosedSectionTitle: "Pads tancats", + padCreateSectionTitle: "Crear un pad nou", + padUpdateSectionTitle: "Actualitzar el pad", + typePad: "PADS", + padNew: "NOU", + padAddFavorite: "Afegir a preferits", + padRemoveFavorite: "Treure de preferits", + padClose: "Tancar", + padBackToEditor: "Tornar a l'editor", + padSearchPlaceholder: "Cercar a Pads...", + + calendarsTitle: "Calendaris", + calendarTitle: "Calendari", + modulesCalendarsLabel: "Calendaris", + modulesCalendarsDescription: "Mòdul per descobrir i gestionar calendaris.", + typeCalendar: "CALENDARIS", + calendarCreate: "Crear un calendari", + calendarUpdate: "Actualitzar", + calendarTitleLabel: "Títol", + calendarTitlePlaceholder: "Títol del calendari...", + calendarStatusLabel: "Estat", + calendarTypeLabel: "Tipus de calendari", + calendarStatusOpen: "OBERT", + calendarStatusClosed: "TANCAT", + calendarDeadlineLabel: "Data límit", + calendarTagsLabel: "Etiquetes", + calendarTagsPlaceholder: "Introdueix etiquetes separades per comes", + calendarParticipantsLabel: "Participants", + calendarCreated: "Creat", + calendarJoin: "Unir-se al calendari", + calendarGenerateInvite: "Generar una invitació", + calendarDateLabel: "Data", + calendarDatePlaceholder: "Descriu aquesta data...", + calendarNoteLabel: "Notes", + calendarNotePlaceholder: "Afegir una nota...", + calendarFirstDateLabel: "Data", + calendarFirstNoteLabel: "Notes", + calendarIntervalLabel: "Interval", + calendarIntervalNone: "Sense repetició", + calendarIntervalWeekly: "Setmanal", + calendarIntervalMonthly: "Mensual", + calendarIntervalYearly: "Anual", + calendarFormDescription: "Descripció", + calendarNoDates: "No hi ha dates afegides.", + calendarNoNotes: "Sense notes.", + calendarsNoItems: "No s'han trobat calendaris.", + calendarsDescription: "Descobreix i gestiona calendaris a la teva xarxa.", + calendarMonthPrev: "← Anterior", + calendarMonthNext: "Següent →", + calendarMonthNames: "Gener,Febrer,Març,Abril,Maig,Juny,Juliol,Agost,Setembre,Octubre,Novembre,Desembre", + calendarDayInitials: "Dl,Dt,Dc,Dj,Dv,Ds,Dg", + calendarAllSectionTitle: "Calendaris", + calendarRecentSectionTitle: "Calendaris recents", + calendarFavoritesSectionTitle: "Preferits", + calendarMineSectionTitle: "Els teus calendaris", + calendarOpenSectionTitle: "Calendaris oberts", + calendarClosedSectionTitle: "Calendaris tancats", + calendarCreateSectionTitle: "Crear un calendari nou", + calendarUpdateSectionTitle: "Actualitzar el calendari", + calendarAddFavorite: "Afegir a preferits", + calendarDeleteNote: "Eliminar", + calendarRemoveFavorite: "Treure de preferits", + calendarSearchPlaceholder: "Cercar al calendari...", + calendarAddEntry: "Afegir una entrada", + statsCalendar: "Calendaris", + statsCalendarDate: "Dates de calendari", + statsCalendarNote: "Notes de calendari", + + modulesChatsLabel: "Xats", + modulesChatsDescription: "Mòdul per descobrir i gestionar xats.", + typeChat: "XATS", + typeChatMessage: "MISSATGE DE XAT", + chatsTitle: "Xats", + chatMineSectionTitle: "Els teus xats", + chatRecentTitle: "Xats recents", + chatFavoritesTitle: "Preferits", + chatOpenTitle: "Xats oberts", + chatClosedTitle: "Xats tancats", + chatDescription: "Descripció", + chatCategory: "Categoria", + chatStatus: "ESTAT", + chatCreate: "Crear un xat", + chatUpdate: "Actualitzar", + chatDelete: "Esborrar", + chatClose: "Tancar el xat", + chatVisitChat: "VEURE EL XAT", + chatUntitled: "Xat sense títol", + chatNoItems: "No s'han trobat xats.", + chatParticipants: "Participants", + chatMessagesLabel: "Missatges", + chatThreadsLabel: "Fils", + chatCreatedAt: "CREAT", + chatSearchPlaceholder: "Cercar a Xats...", + chatStatusOpen: "OBERT", + chatStatusInviteOnly: "NOMÉS CONVIDATS", + chatStatusClosed: "TANCAT", + chatSendMessage: "Enviar", + chatJumpLatest: "Darrer missatge", + chatReply: "Respondre", + chatReplyCancel: "Cancel·lar la resposta", + chatPin: "Fixar", + chatUnpin: "Desfixar", + chatPinned: "Fixats", + chatPickChat: "Tria un xat per obrir-lo", + chatNoneYet: "Encara no hi ha cap xat disponible.", + activitySearchPlaceholder: "Cercar a Activitat...", + trendingSearchPlaceholder: "Cercar a Tendències...", + opinionsSearchPlaceholder: "Cercar a Opinions...", + votesSearchPlaceholder: "Cercar a Votacions...", + gamesSearchPlaceholder: "Cercar a Jocs...", + schoolTitle: "Escola", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "S'ha editat una pàgina wiki que segueixes.", + wikiBotRestoredTitle: "S'ha restaurat una versió anterior d'una pàgina wiki que segueixes.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Un estudiant nou s'ha apuntat al teu curs.", + schoolBotInvitedTitle: "T'han convidat a un curs.", + schoolBotAdmittedTitle: "T'han admès en un curs.", + schoolBotCertificateTitle: "Has rebut un certificat.", + schoolBotPassedTitle: "Un estudiant ha aprovat el teu curs.", + schoolBotLessonTitle: "S'ha publicat una lliçó nova.", + invitesSchoolTitle: "Escola", + schoolEnrollLabel: "Unir-se al curs", + schoolExamRetryLater: "Ho podràs tornar a provar passades 24 hores.", + schoolApproved: "APROVAT", + schoolExamPassMark: "Nota d'aprovat (0-10)", + schoolExamPassed: "APROVAT", + schoolExamPending: "Pendent", + schoolExamFailed: "SUSPÈS", + schoolExamAddQuestion: "Afegir una pregunta", + schoolExamCreateQuestion: "Crear una pregunta", + schoolExamOption: "Opció", + schoolExamCorrect: "Opció correcta", + schoolExamPoints: "Punts", + schoolMaterialTextPlaceholder: "Notes per a aquesta lliçó...", + schoolMaterials: "Materials", + schoolAddMaterial: "Afegir material", + schoolNoMaterials: "Encara no hi ha materials.", + bankYourSchoolBalance: "Els teus ingressos de l'escola", + schoolProgress: "Progrés", + schoolProgressMark: "Marcar com a completada", + schoolProgressDone: "Completada", + schoolExams: "Exàmens", + schoolExamCreate: "Crear un examen", + schoolExamTitlePlaceholder: "Títol de l'examen", + schoolExamHelp: "Una pregunta per línia: Pregunta? ; opció ; opció correcta* ; opció", + schoolExamQuestionsPlaceholder: "Què és SSB? ; Una base de dades ; Un protocol P2P* ; Un navegador", + schoolExamSubmit: "Enviar les respostes", + schoolExamScore: "La teva nota", + schoolNoScoresYet: "Encara no hi ha resultats.", + schoolLessonUnit: "Unitat / fase", + schoolLessonUnitPlaceholder: "p. ex. Fase 1", + schoolLessonOrder: "Ordre", + profileClearnetSchoolLabel: "Escola", + schoolStartDate: "Data d'inici", + schoolSessionDate: "Data de la sessió", + schoolCourseChat: "Xat del curs", + agendaFilterSchool: "ESCOLA", + agendaFilterCampaigns: "CAMPANYES", + agendaFilterLogistics: "LOGÍSTICA", + schoolCourseType: "Tipus de curs", + schoolTypeOpen: "OBERT", + schoolTypePaid: "DE PAGAMENT", + schoolTypeInvite: "NOMÉS PER INVITACIÓ", + schoolLessonLocked: "Lliçó xifrada — demana la clau del curs al professor.", + schoolGrantAccess: "Enviar la clau del curs", + schoolKeySent: "Clau enviada", + schoolOngoing: "En curs", + schoolFilterApplied: "APUNTAT", + schoolFilterOpen: "OBERTS", + schoolFilterFavorites: "PREFERITS", + schoolPendingPayment: "Pagament pendent", + schoolPendingStudents: "Inscripcions pendents", + schoolViewBill: "Veure la factura", + schoolPaymentNote: "En inscriure't es genera una factura en ECO (una transferència). La teva inscripció es confirma quan la transferència queda signada.", + schoolCloseCourse: "Tancar el curs", + schoolReopenCourse: "Reobrir el curs", + schoolDescription: "Ensenya i aprèn cursos a la teva xarxa.", + schoolFilterAll: "TOTS", + schoolFilterMine: "MEUS", + schoolFilterTop: "TOP", + schoolFilterRecent: "RECENTS", + schoolCreateButton: "Crear un curs", + schoolCreateSectionTitle: "Crear un curs", + schoolUpdateSectionTitle: "Actualitzar el curs", + schoolCourseTitlePlaceholder: "Nom del teu curs", + schoolCourseDescriptionPlaceholder: "Què ensenyaràs en aquest curs?", + schoolCourseImage: "Imatge del curs (mida màx.: 75MB)", + schoolTags: "Etiquetes", + schoolTagsPlaceholder: "Introdueix etiquetes separades per comes", + schoolPrice: "Preu (ECO)", + schoolVisibility: "Visibilitat", + schoolVisibilityPublic: "Públic", + schoolVisibilityInvite: "Per invitació", + schoolStatus: "Estat", + schoolClosed: "Tancat", + schoolFree: "Gratis", + schoolCreate: "Crear el curs", + schoolUpdate: "Actualitzar el curs", + schoolSearchPlaceholder: "Cercar a l'Escola...", + schoolSortTitle: "Títol", + schoolNoCourses: "No s'han trobat cursos.", + schoolStudents: "Estudiants", + schoolTeacher: "Professor", + schoolCreatedAt: "CREAT", + schoolLessons: "Lliçons", + schoolNoLessons: "Encara no hi ha lliçons.", + schoolEnrollToSee: "Inscriu-te per veure les lliçons d'aquest curs.", + schoolEnrollButton: "INSCRIURE'S", + schoolUnenrollButton: "Abandonar el curs", + schoolInviteRequired: "Aquest curs és per invitació.", + schoolTeacherPanel: "Tauler del professor", + schoolStudentPanel: "Tauler de l'estudiant", + schoolAddLesson: "Crear una lliçó", + schoolLessonTitlePlaceholder: "Títol de la lliçó", + schoolLessonTextPlaceholder: "Descriu les nocions que cobreix aquesta lliçó...", + schoolInviteStudents: "Convidar estudiants", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "CONVIDAR", + schoolInvited: "Convidats", + schoolIssueCertificate: "Crear un certificat", + schoolCertified: "Certificat", + schoolCertificateTextPlaceholder: "Nota del certificat (opcional)", + schoolLessonOpen: "Obrir la lliçó", + schoolCertificatePdf: "PDF", + schoolCertificates: "Certificats", + schoolNoCertificates: "Encara no s'han emès certificats.", + schoolEditButton: "Editar", + schoolDeleteButton: "Esborrar", + typeSchool: "ESCOLA", + schoolCourseLabel: "CURS", + emergencyLabel: "EMERGÈNCIA", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Cursos", + statsSchoolLesson: "Lliçons", + statsSchoolEnroll: "Inscripcions", + statsSchoolCertificate: "Certificats de l'escola", + modulesSchoolLabel: "Escola", + modulesSchoolDescription: "Mòdul per intercanviar coneixement.", + welcomeStepUxAction: "Desar vista", + welcomeStepWorkflowTitle: "Quin ús d'OASIS tens al cap?", + welcomeStepWorkflowText: "Oasis té molts mòduls. Tria la configuració que s'adapti a tu i només quedaran activats els útils. Pots ajustar-ho quan vulguis a Mòduls.", + welcomeStepWorkflowAction: "Desar configuració", + welcomeWorkflow_full: "Per defecte", + chatRateLimitTitle: "Massa missatges", + chatRateLimitMessage: "Has arribat al límit de missatges que aquesta sala accepta en una hora. Torna-ho a provar més tard.", + chatMessagePlaceholder: "Escriu el teu missatge...", + chatNoMessages: "Encara no hi ha missatges.", + chatTitlePlaceholder: "Títol del xat", + chatDescriptionPlaceholder: "Descripció del xat", + chatTagsPlaceholder: "Introdueix etiquetes separades per comes", + chatImageLabel: "Selecciona un fitxer d'imatge (.jpeg, .jpg, .png, .gif)", + chatAuthor: "Autor", + chatAddFavorite: "Afegir a preferits", + chatRemoveFavorite: "Treure de preferits", + chatPM: "MP", + chatStatusLabel: "Estat", + chatTypeLabel: "Tipus de xat", + chatCategoryLabel: "Categoria", + gamesTitle: "Jocs", + gamesDescription: "Descobreix alguns jocs i juga-hi.", + gamesFilterAll: "TOTS", + gamesPlayButton: "JUGAR!", + modulesGamesLabel: "Jocs", + modulesGamesDescription: "Mòdul per descobrir alguns jocs i jugar-hi.", + modulesGraphosLabel: "Graphos", + modulesGraphosDescription: "Mòdul per explorar la xarxa com un mapa interactiu de nodes.", + gamesCocolandTitle: "Cocoland", + gamesCocolandDesc: "Un coco amb ulls que salta palmeres i col·lecciona ECOins. Fins on pots arribar?", + gamesTheFlowTitle: "ECOinflow", + gamesTheFlowDesc: "Connecta PUBs amb habitants mitjançant validadors, botigues i acumuladors. Sobreviu a l'amenaça CBDC!", + gamesNeonInfiltratorTitle: "Neon Infiltrator", + gamesNeonInfiltratorDesc: "Infiltra't a la graella, recull dades confidencials, esquiva els drons de seguretat i escapa. Quants nivells pots superar?", + gamesSpaceInvadersTitle: "Space Invaders", + gamesSpaceInvadersDesc: "Atura la invasió alienígena! Destrueix onades d'invasors abans que arribin a terra.", + gamesArkanoidTitle: "Arkanoid", + gamesArkanoidDesc: "Trenca tots els maons amb la pala i la pilota. Un clàssic desafiament arcade.", + gamesPingPongTitle: "PingPong", + gamesPingPongDesc: "Ping-pong clàssic contra una IA. Guanya qui arriba primer a 5 punts.", + gamesAsteroidsTitle: "Asteroids", + gamesAsteroidsDesc: "Pilota la teva nau per un camp d'asteroides mortal. Dispara'ls abans que t'atrapin.", + gamesRockPaperScissorsTitle: "Pedra, paper, tisores", + gamesRockPaperScissorsDesc: "Pedra, paper o tisores contra una IA. Guanya qui s'emporta dues de tres rondes.", + gamesTikTakToeTitle: "TikTakToe", + gamesTikTakToeDesc: "Tres en ratlla clàssic contra la IA. Fes tres en línia per guanyar.", + gamesFlipFlopTitle: "FlipFlop", + gamesFlipFlopDesc: "Llança una moneda i aposta cara o creu. Quanta sort tens?", + games8BallTitle: "8Ball Pool", + games8BallDesc: "Billar vist des de dalt. Fes clic per apuntar i mantén premut per carregar la potència. Entra totes les boles amb el mínim de tirs.", + gamesArtilleryTitle: "Artilleria", + gamesArtilleryDesc: "Apunta el canó, tingues en compte el vent i encerta el blanc. 5 rondes: guanya qui fa menys tirs.", + gamesLabyrinthTitle: "Laberint", + gamesLabyrinthDesc: "Surt del laberint abans que s'acabin els moviments. Cada nivell és més gran i més difícil.", + gamesCocomanTitle: "Cocoman", + gamesCocomanDesc: "Menja't tots els punts i esquiva els fantasmes. Els fantasmes es mouen en temps real: no t'aturis!", + gamesTetrisTitle: "Tetris", + gamesAudioPendulumTitle: "Pèndol sonor", + gamesAudioPendulumDesc: "Simulador de física caòtica amb síntesi d'àudio en temps real. Les velocitats angulars es converteixen en freqüències i els pics, en cops de percussió. No hi ha dues simulacions que sonin igual.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Punxa visuals en directe amb dos plats: tria un patró per a cadascun, ajusta velocitat, color i zoom, mescla entre tots dos i llança flaixos al ritme. Marca el tempo o deixa que el micròfon porti el pols.", + gamesTetrisDesc: "Els clàssics blocs que cauen. Completa línies per puntuar. Quant de temps aguantaràs?", + gamesMinesweeperTitle: "Pescamines", + gamesMinesweeperDesc: "La clàssica caça de mines. Neteja el tauler per pujar de nivell — graelles més grans, més mines, més puntuació.", + gamesFilterScoring: "PUNTUACIONS", + gamesHallOfFame: "Saló de la fama", + gamesHallPlayer: "Jugador", + gamesHallScore: "Puntuació", + gamesNoScores: "Encara no hi ha puntuacions.", + chatAccessDenied: "No tens accés a aquest xat. Demana una invitació per accedir al contingut.", + padAccessDenied: "No tens accés a aquest pad. Demana una invitació per accedir al contingut.", + contentAccessDenied: "No tens accés a aquest contingut.", + blockAccessRestricted: "Accés restringit", + tribeContentAccessDenied: "Accés denegat", + tribeContentAccessDeniedMsg: "Aquest contingut pertany a una tribu. N'has de ser membre per accedir-hi.", + tribeViewTribes: "Veure les tribus", + tribeFilesEmpty: "Encara no hi ha fitxers.", + filesLabel: "Fitxers", + profileClearnetFilesLabel: "Fitxers", + filesTitle: "Fitxers", + filesDescription: "Explora i gestiona fitxers a la teva xarxa.", + fileAllSectionTitle: "Fitxers", + fileMineSectionTitle: "Els teus fitxers", + fileRecentSectionTitle: "Fitxers recents", + fileTopSectionTitle: "Fitxers destacats", + fileFavoritesSectionTitle: "Preferits", + fileFilterAll: "TOTS", + fileFilterMine: "MEUS", + fileFilterRecent: "RECENTS", + fileFilterTop: "DESTACATS", + fileFilterFavorites: "PREFERITS", + fileCreateSectionTitle: "Pujar un fitxer", + fileUpdateSectionTitle: "Actualitzar el fitxer", + fileCreateButton: "Pujar un fitxer", + fileUpdateButton: "Actualitzar", + fileAddFavoriteButton: "Afegir a preferits", + fileRemoveFavoriteButton: "Treure de preferits", + fileFileLabel: "Selecciona un fitxer (màx.: 75MB)", + calendarFilterRecent: "RECENTS", + calendarFilterAll: "TOTS", + calendarFilterMine: "MEUS", + calendarFilterFavorites: "PREFERITS", + calendarFilterOpen: "OBERTS", + calendarFilterClosed: "TANCATS", + padFilterRecent: "RECENTS", + padFilterAll: "TOTS", + padFilterMine: "MEUS", + padFilterOpen: "OBERTS", + padFilterClosed: "TANCATS", + chatFilterRecent: "RECENTS", + chatFilterAll: "TOTS", + chatFilterMine: "MEUS", + chatFilterFavorites: "PREFERITS", + chatFilterOpen: "OBERTS", + chatFilterClosed: "TANCATS", + licenseLabel: "Llicència", + phoneTitle: "Telèfon", + phoneDescription: "Gestiona les teves videotrucades xifrades d'extrem a extrem.", + phoneUnavailable: "Les trucades no estan disponibles en aquest dispositiu.", + phoneCallButton: "Trucar", + profileVisibilityPhone: "Número de telèfon", + phoneNumberUnknown: "Ningú de la teva xarxa té el número {number}.", + phoneNumberChoose: "Comprova a qui vols trucar al {number}:", + phoneAnswer: "Respondre", + phoneSpeaker: "Altaveu", + phoneEarpiece: "Auricular", + phoneReject: "Rebutjar", + phoneHangup: "Penjar", + phoneMute: "Silenciar", + phoneUnmute: "Activar micròfon", + phoneCameraOn: "Càmera on", + phoneCameraOff: "Càmera off", + settingsPhoneVideo: "Vídeo", + settingsPhoneVideoOn: "Permès en trucades i sales", + settingsPhoneVideoOff: "Només àudio", + phoneSilence: "No sentir", + phoneUnsilence: "Tornar a sentir", + phoneCalling: "Trucant a", + phoneIsCalling: "t'està trucant", + notifyNewPmFrom: "Nou MP de", + notifyNewPamFrom: "Nou PaM de", + notifyUpdateLabel: "Actualització", + updateInstalledLabel: "Versió instal·lada", + updateAvailableLabel: "Versió disponible", + updateChangesLabel: "Canvis", + updateFilesLabel: "Fitxers", + updateOriginLabel: "Origen", + phoneRecording: "Gravant PaM per a", + phoneLeaveMessage: "Ningú respon: deixa el teu missatge després del senyal.", + phoneLeaveMessageBusy: "És en una altra trucada: deixa el teu missatge després del senyal.", + phoneLeaveMessageUnreachable: "No s'ha pogut contactar: deixa el teu missatge després del senyal.", + phoneConnecting: "Connectant amb", + phoneSendPam: "Enviar PaM", + phoneCancel: "Cancel·lar", + phoneSearchPlaceholder: "Cercar a Telèfon...", + phonePamTitle: "Enregistraments d'àudio", + phoneNew: "Nou", + phoneDelete: "Esborrar", + phoneNoPams: "No tens enregistraments d'àudio.", + phoneHistoryTitle: "Historial", + phoneLatestTitle: "Últimes trucades", + phoneCreateButton: "Nova trucada", + phoneLimitsHint: "Fins a {n} habitants per trucada; amb més d'un és una trucada conjunta.", + phoneErrorPub: "No es pot trucar a un pub.", + phoneJointCall: "Trucada conjunta", + phoneWith: "Amb", + phoneResult: "Resultat", + phoneWhen: "Quan", + phoneDuration: "Durada", + phoneOutgoing: "Sortint", + phoneIncoming: "Entrant", + phoneNoHistory: "Encara no hi ha registres.", + phoneOutcomeEnded: "Atesa", + phoneOutcomeMissed: "Perduda", + phoneOutcomeRejected: "Rebutjada", + phoneOutcomeNoanswer: "Sense resposta", + phoneOutcomeCancelled: "Cancel·lada", + phoneOutcomePam: "PaM enviat", + phoneOutcomeFailed: "Fallida", + phoneDialAdd: "Afegir", + phoneDialBack: "Esborrar", + phoneDialClear: "Netejar", + phoneDialPlaceholder: "Número o Oasis ID", + phoneDialInhabitants: "Habitants", + phoneDialRemove: "Treure", + phoneOwnNumber: "Aquest és el teu propi número.", + phoneSettingsDesc: "Tria qui et pot trucar.", + phoneDnd: "No molestar", + phoneOpen: "Obert", + phoneRecipientRefuses: "L'habitant a qui intentes trucar no permet rebre trucades.", + phoneRefusedNotice: "Aquests habitants no permeten rebre trucades i no se'ls ha trucat:", + fileEmptyError: "L'arxiu és buit. Tria un altre arxiu.", + fileTitleLabel: "Títol", + fileTitlePlaceholder: "Títol", + fileDescriptionLabel: "Descripció", + fileDescriptionPlaceholder: "Descripció", + fileTagsLabel: "Etiquetes", + fileTagsPlaceholder: "Introdueix etiquetes separades per comes", + fileSizeLabel: "Mida", + fileDownloadButton: "BAIXA", + fileDetailsButton: "DETALLS", + fileNoFile: "Sense fitxer fitxer", + fileSearchPlaceholder: "Cercar a Fitxers...", + fileSearchButton: "Cercar", + fileSortRecent: "Més recents", + fileSortOldest: "Més antics", + fileSortTop: "Més votats", + fileNoMatch: "No s'han trobat fitxers coincidents.", + fileUpdatedAt: "Actualitzat", + typeFile: "FITXERS", + modulesFilesLabel: "Fitxers", + modulesFilesDescription: "Mòdul per descobrir i gestionar àudios.", + tribeSectionFiles: "FITXERS", + fileDescription: "Descripció", + fileDownload: "Baixar", + fileTypeLabel: "Tipus", + torrentsTitle: "Torrents", + torrentsDescription: "Explora i gestiona torrents a la teva xarxa.", + torrentAllSectionTitle: "Torrents", + torrentMineSectionTitle: "Els teus torrents", + torrentRecentSectionTitle: "Torrents recents", + torrentTopSectionTitle: "Torrents destacats", + torrentFavoritesSectionTitle: "Preferits", + torrentFilterAll: "TOTS", + torrentFilterMine: "MEUS", + torrentFilterRecent: "RECENTS", + torrentFilterTop: "DESTACATS", + torrentFilterFavorites: "PREFERITS", + torrentCreateSectionTitle: "Pujar un torrent", + torrentUpdateSectionTitle: "Actualitzar el torrent", + torrentCreateButton: "Pujar un torrent", + torrentUpdateButton: "Actualitzar", + torrentDeleteButton: "Eliminar", + torrentAddFavoriteButton: "Afegir a preferits", + torrentRemoveFavoriteButton: "Treure de preferits", + torrentFileLabel: "Seleccionar un fitxer torrent (.torrent)", + torrentTitleLabel: "Títol", + torrentTitlePlaceholder: "Títol", + torrentDescriptionLabel: "Descripció", + torrentDescriptionPlaceholder: "Descripció", + torrentTagsLabel: "Etiquetes", + torrentTagsPlaceholder: "Introdueix etiquetes separades per comes", + torrentSizeLabel: "Mida", + torrentFromContent: "Generar torrent", + torrentFromContentMissing: "El fitxer encara no està disponible en aquest node. Torna-ho a provar d'aquí a una estona.", + torrentDownloads: "Baixades", + torrentFetchOasis: "Baixar a Oasis", + torrentSaveFile: "Desar fitxer", + torrentSeeds: "llavors", + seedsLabel: "Llavors", + seedAction: "Donar suport", + torrentSourceFile: "Fitxer", + torrentStateQueued: "En cua", + torrentStateProbing: "Cercant llavors", + torrentStateDownloading: "Baixant", + torrentStateDone: "Completada", + torrentStateFailed: "Fallida", + torrentStateNoSource: "Sense llavors connectades, esperant", + torrentStateCancelled: "Cancel·lada", + torrentNoDownloads: "Encara no hi ha baixades.", + torrentOriginalSection: "Original", + torrentCancel: "Cancel·lar", + torrentDownloadButton: "BAIXA", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "DETALLS", + torrentNoFile: "Sense fitxer torrent", + noTorrents: "No s'han trobat torrents.", + noFiles: "No s'han trobat fitxers.", + torrentSearchPlaceholder: "Cercar a Torrents...", + torrentSearchButton: "Cercar", + torrentSortRecent: "Més recents", + torrentSortOldest: "Més antics", + torrentSortTop: "Més votats", + torrentNoMatch: "No s'han trobat torrents coincidents.", + torrentUpdatedAt: "Actualitzat", + statsTorrent: "Torrents", + statsFile: "Fitxers", + typeTorrent: "TORRENTS", + modulesTorrentsLabel: "Torrents", + modulesTorrentsDescription: "Mòdul per descobrir i gestionar torrents.", + favoritesFilterTorrents: "TORRENTS", + favoritesFilterFiles: "FITXERS", + favoritesFilterMarket: "MERCAT", + favoritesFilterEvents: "ESDEVENIMENTS", + favoritesFilterForum: "FÒRUM", + favoritesFilterHousing: "HABITATGE", + favoritesFilterJobs: "FEINES", + favoritesFilterProjects: "PROJECTES", + favoritesFilterReports: "INFORMES", + favoritesFilterTasks: "TASQUES", + favoritesFilterTransfers: "TRANSFERÈNCIES", + favoritesFilterVotes: "VOTACIONS", + favoritesFilterShopProducts: "ARTICLES DE BOTIGA", + tribeSectionTorrents: "TORRENTS", + tribeCreateTorrent: "Pujar un torrent", + tribeCreateFile: "Pujar fitxer", + settingsWishTitle: "Desig", + settingsWishDesc: "Determina com accedeixes a tot el contingut i la teva experiència a la xarxa Oasis.", + settingsWishWhole: "Multivers", + settingsWishMutuals: "Només suport mutu", + settingsWishOnlyLan: "Només LAN", + settingsWishLocal: "Local", + settingsPmVisibilityTitle: "Missatges privats", + settingsPmVisibilityDesc: "Configura el nivell d'exposició a missatges privats que vols tenir a la xarxa.", + settingsPmVisibilityWhole: "Multivers", + settingsPmVisibilityMutuals: "Només suport mutu", + mute: "Silenciar", + unmute: "Deixar de silenciar", + relationshipMuted: "Silenciat per tu", + hideContent: "Amagar per a mi", + unhideContent: "Mostrar de nou", + hiddenByYou: "Amagat per tu", + settingsTabContent: "Contingut", + settingsHiddenTitle: "Contingut amagat", + settingsHiddenDesc: "Habitants que has silenciat i contingut que has amagat.", + settingsMutedLabel: "Habitants silenciats", + settingsHiddenLabel: "Contingut amagat", + settingsMutedEmpty: "Ningú silenciat.", + settingsHiddenEmpty: "Res amagat.", + settingsPmVisibilityFollowing: "Només gent que segueixo", + settingsMentionsVisibilityTitle: "Mencions", + settingsMentionsVisibilityDesc: "Tria de qui t'arriben les mencions.", + pmMutualNotice: "L'enviament és una barrera d'UX. La recepció és un filtre d'atenció (el xifratge ja està replicat).", + inhabitantsPendingFollowsTitle: "Sol·licituds de suport pendents", + inhabitantsPendingAccept: "Acceptar", + inhabitantsPendingReject: "Rebutjar", + tribeSectionGovernance: "GOVERN", + tribePadInviteRequired: "No tens accés al pad. Demana una invitació per accedir al contingut.", + tribeChatInviteRequired: "No tens accés al xat. Demana una invitació per accedir al contingut.", + tribeGovernanceDesc: "Govern intern d'aquesta tribu.", + tribeGovernanceNoGov: "Sense govern actiu", + tribeGovernanceNoGovDesc: "Aquesta tribu encara no té govern. Proposa candidatures per iniciar el procés.", + tribeGovCardTitle: "Govern actual", + tribeGovCycleSince: "INICI DEL CICLE", + tribeGovCycleEnd: "FI DEL CICLE", + tribeGovTimeRemaining: "TEMPS RESTANT", + tribeGovPopulation: "POBLACIÓ", + tribeGovMethod: "MÈTODE", + tribeGovVotesReceived: "VOTS REBUTS", + tribeGovLeader: "LÍDER", + tribeGovFilterGovernment: "GOVERN", + tribeGovFilterCandidatures: "CANDIDATURES", + tribeGovFilterLaws: "LLEIS", + tribeGovCandidatureId: "Candidatura", + tribeGovCandidatureMethod: "Mètode", + tribeGovCandidatureProposeBtn: "Publicar la candidatura", + tribeGovRuleTitle: "Títol de la regla", + tribeGovRuleBody: "Cos de la regla", + tribeGovProposals: "PROPOSTES", + tribeGovRevocations: "REVOCACIONS", + tribeGovHistorical: "HISTÒRIC", + tribeGovRules: "REGLES", + tribeGovernanceAlreadyPublished: "Aquesta tribu ja té una candidatura oberta en el cicle actual del parlament global.", + tribeGovernanceProposeInternal: "Proposar una candidatura interna", + tribeGovernanceInternalCandidatures: "Candidatures internes", + tribeGovernanceNoCandidatures: "No hi ha candidatures obertes.", + tribeGovernanceAddRule: "Afegir una regla", + tribeGovernanceNoRules: "Encara no hi ha regles.", + tribeGovernanceNoLeaders: "Encara no hi ha líders elegits.", + tribeGovernanceComingSoon: "Aviat al mòdul de govern d'aquesta tribu.", + tribeGovNoProposals: "Encara no hi ha cap proposta", + tribeGovNoLaws: "Encara no hi ha cap llei", + tribeGovNoRevocations: "Encara no hi ha cap revocació", + tribeGovNoHistorical: "Encara no hi ha cap registre", + logsTitle: "Bitàcola", + logsDescription: "Porta un diari privat de les teves experiències.", + logsViewTitle: "Bitàcola", + logsColumnType: "Tipus", + logsView: "Veure", + logsUpdateButton: "Actualitzar", + logsEditTitle: "Actualitzar el registre", + logsCreateTitle: "Crear una entrada", + logsWriteButton: "Escriure", + logsTextPlaceholder: "Descriu les teves experiències...", + logsModeAIWritten: "Assistent d'IA", + logsModeManual: "Manual", + logsModeAI: "IA", + logsColumnLog: "Bitàcola", + logsColumnDate: "Data", + logsDelete: "Eliminar", + logsEdit: "Actualitzar", + logsFilterAlways: "SEMPRE", + logsFilterYear: "DARRER ANY", + logsFilterMonth: "DARRER MES", + logsFilterWeek: "DARRERA SETMANA", + logsFilterToday: "AVUI", + logsComments: "Comentaris", + logsAuthorEntries: "Entrades", + logsCreate: "Crear una entrada", + logsExport: "Exportar la bitàcola", + logsExportOne: "Exportar el registre", + logsViewDetails: "Veure els detalls", + logsGenerateButton: "Generar el text", + logsSearchText: "Cercar a la Bitàcola...", + logsSearchAnyType: "Qualsevol tipus", + logsSearchButton: "Cercar", + logsEmpty: "Encara no hi ha entrades.", + logsNothingNew: "No hi ha accions noves per registrar.", + modulesLogsLabel: "Bitàcola", + modulesLogsDescription: "Mòdul per portar un diari privat de les teves experiències.", + statsLogsTitle: "Bitàcola", + typeLog: "BITÀCOLA", + + larpTitle: "L.A.R.P.", + larpDescription: "Una capa de joc de rol en viu per a l'experimentació col·laborativa.", + larpNoHousesMatch: "Cap casa no coincideix amb la teva cerca.", + larpSearchPlaceholder: "Cercar a L.A.R.P....", + larpAllHouses: "Cases:", + larpFilterRuling: "GOVERNA", + larpFilterHouses: "CASES", + larpFilterRules: "PMF", + larpRulesTitle: "PMF del L.A.R.P.", + larpRulesEntryText: "Tot habitant comença per defecte a ACADÈMIA. Des d'ACADÈMIA, tria una casa al tauler \"Unir-se a una casa\": això n'obre el test d'entrada. Respon les 10 preguntes i envia. Si superes el llindar (7/10), t'admetran automàticament en aquella casa; si no, seràs rebutjat i romandràs a ACADÈMIA. En tots dos casos el sistema registra el teu OasisID i el cicle de l'intent, i impedeix un altre intent fins que passi el període d'espera de 30 cicles.", + larpRulesGovernanceText: "Durant el seu cicle, la casa governant llueix la insígnia \"Governa\" i és l'única el Mur de la qual es mostra públicament a la pestanya GOVERNA.", + larpRulesWallText: "Cada casa té un Mur on escriuen els seus membres. El Mur d'una casa és privat per a la casa, excepte el de la casa governant i el d'ACADÈMIA, que pot llegir qualsevol.", + larpRulesSignText: "Els habitants el poden activar (a /profile/edit > Sensors) per mostrar la imatge de la seva casa actual com un segell al seu perfil, a la seva targeta d'autor i d'habitant. El segell enllaça a la pàgina de la casa.", + larpPostsTitle: "Mur", + larpPostsEmpty: "Encara no hi ha publicacions.", + larpPostPlaceholder: "Què té a dir aquesta casa?", + larpPostSubmit: "Publicar", + larpCycleLabel: "Cicle:", + bankRulesArchTaxFormula: "Taxa ARCH(usuari) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", + bankRulesArchTaxNote: "L'impost ARCH reflecteix el cost a llarg termini de fer créixer i mantenir l'arxiu de la xarxa: com més antigues siguin les teves dades a la xarxa, més gran serà la teva part del cost de manteniment.", + bankRulesChipFormula: "band = ratio / reducer, on ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). alt ≥ 0.66, mitjà ≥ 0.33, altrament baix.", + bankRulesChipNote: "El xip de cada element s'acoloreix respecte al missatge més gran vist a la xarxa i se suavitza segons el nombre d'habitants actius — una xarxa més gran reparteix la càrrega i empeny més elements cap a la banda verda.", + bankRulesEcoTaxFormula: "Taxa ECO(usuari) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", + bankRulesEcoTaxNote: "L'impost ECO es dedueix de la RBU bruta quan es calcula l'assignació de l'època. Els ECO deduïts no es redistribueixen en aquesta època — es queden al saldo del pub i alimenten el fons de la pròxima època a través dels límits reserveMin / alpha.", + bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation", + bankRulesKarmaNote: "El karma es deriva de les teves accions (publicacions, vots, participació al parlament/tribunals, etc.) amb un factor de decaïment temporal, menys els grams de carboni que genera el teu feed. El que rep el teu contingut també compta: les opinions, vots, reaccions i reproduccions positives sumen karma, i les opinions o vots negatius el resten quan diversos habitants coincideixen.", + bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax", + bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)", + bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)", + bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", + bankTaxesArchTaxFootprintNote: "L'impost ARCH reflecteix el cost de fer créixer i mantenir l'arxiu de la xarxa. Es calcula a partir del temps transcorregut entre el primer bloc que vas publicar i el bloc més nou de la xarxa — com més temps visqui la teva història a l'arxiu, més gran serà la teva part del cost de manteniment.", + bankTaxesArchTaxNoteBold: "reduir directament l'impost ARCH", + bankTaxesArchTaxNoteIntro: "El teu impost ARCH es dedueix de l'excedent que la xarxa genera per sobre de la teva RBU; el valor base de la RBU és un mínim inamovible. L'impost mai no es dedueix de l'import fix que correspon a cada habitant com a RBU. Els fons deduïts alimenten l'algorisme de redistribució de la riquesa i es canalitzen cap a altres habitants a través de les seves reclamacions de RBU. Els habitants més nous i els que es dediquin específicament a tasques d'arxiu, replicació i manteniment probablement tindran alguna tasca dedicada a mantenir sa l'arxiu de la xarxa en nom teu. O poden dedicar-se a ", + bankTaxesArchTaxNoteOutro: " mitjançant acords de poda, deduplicació de dades redundants i infraestructura de manteniment compartida, i el consens de la xarxa pot requerir-ne la contribució contínua.", + bankTaxesUserNoteChips: "El xip d'impost ECO que es mostra al costat de cada element canvia de color (verd / groc / vermell) segons la mida de l'element en relació amb el bloc més gran vist a la xarxa, suavitzat pel nombre d'habitants actius — una xarxa solar-punk més gran reparteix la càrrega i empeny més elements cap a la banda verda.", + bankTaxesUserNoteOtherParams: "Ara com ara l'impost ECO es deriva de la petjada de carboni de cada bloc, però amb el temps pot incorporar altres paràmetres — energia gastada en la replicació, emmagatzematge redundant entre nodes, amplada de banda dels blobs, cost computacional del desxifratge, petjada de mineria de les transaccions associades o qualsevol altra càrrega mesurable que la xarxa acordi valorar.", + gamesHallDate: "Data", + gamesNewRecord: "Nou rècord!", + indexingRefreshNote: "Aquesta pàgina es recarrega cada 10 segons.", + indexingMessage: "Oasis està sincronitzant la xarxa de nodes distribuïda. Si us plau, espera.", + indexingTitle: "Sincronitzant", + padsNoItems: "Encara no hi ha pads.", + padVersionHistory: "Historial", + padAttributionView: "Per autora", + padVersionView: "Veure la versió", + statsChat: "Xats", + statsChatMessage: "Missatges de xat", + statsGameScore: "Jocs", + statsWiki: "Pàgines wiki", + statsPad: "Pads", + statsPadEntry: "Entrades de pad", + typeGameScore: "PUNTUACIONS", + bankTaxesLookupNote: "Introdueix un ID de bloc per cercar-lo al teu feed local.", + bankTaxesUserNoteChipsClick: "Fes clic a qualsevol xip per inspeccionar-ne el bloc al blockexplorer.", + mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntryText: "Els membres de les cases poden generar codis d'invitació que donen accés directe a la casa.", + larpRulesOneHouseText: "Només pots pertànyer a una casa alhora. Si no pertanys a cap, ets a ACADÈMIA. Cada pàgina de casa (excepte ACADÈMIA) mostra als seus membres un botó \"Sortir de la casa\" que torna la pertinença a ACADÈMIA. Sortir NO anul·la el període d'espera del test.", + larpRulesTestEntryText: "Al test WILL cada opció pondera una o més cases; en enviar-lo, el sistema suma les puntuacions i t'admet automàticament a la casa amb més punts.", + larpWillTestHint: "Un cop completat, se t'assignarà la casa que més s'ajusti a les teves respostes. Les teves respostes individuals es processen localment i no s'emmagatzemen mai — només es publica al teu feed l'assignació de casa resultant.", + larpWillTestTitle: "Test WILL", + settingsLanDesc: "Anuncia periòdicament aquest node a altres instàncies d'Oasis de la mateixa xarxa local.", + settingsLanEnable: "Activar la difusió LAN", + settingsLanTitle: "Difusió LAN", + settingsReplicationTitle: "Replicació", + settingsReplicationDesc: "Configura el nombre de salts que el teu node segueix des del teu propi feed en replicar contingut.", + settingsReplicationHopsLabel: "Salts", + settingsBlobCacheTitle: "Memòria cau de mitjans", + settingsBlobCacheDesc: "El contingut d'altres habitants es desa en aquest dispositiu. Tria quant d'espai pot ocupar.", + settingsBlobCacheUsage: "Ús actual", + settingsBlobCacheFiles: "fitxers", + settingsBlobCacheLimit: "Límit", + settingsBlobCacheUnlimited: "Sense límit", + settingsBlobCacheCleanNow: "Netejar ara", + settingsBlobCacheCleaned: "{n} fitxers eliminats, {size} alliberats.", + aiApproveTagsLabel: "Etiquetes (separades per comes)", + aiApproveTagsPlaceholder: "p. ex. oasis, governança, ecologia", + aiApproveRatingLabel: "Valoració", + aiExchangeLang: "Idioma", + aiExchangeTags: "Etiquetes", + aiExchangeRating: "Valoració", + aiExchangeHelpful: "Útil", + aiExchangeMarkHelpful: "+1 útil", + larpCyclesUntilRuling: "Pròxim cicle de govern", + larpVisitTribe: "Visitar la tribu", + larpStartJourney: "Començar el viatge", + larpGoverning: "Governa:", + larpStartHere: "Comença aquí:", + larpMyHouse: "La meva casa:", + larpMembersCount: "Membres", + larpRolesLabel: "Rols", + larpFunctionLabel: "Funció", + larpMonthLabel: "Cicle de govern", + larpLeaveToAcademia: "Tornar a ACADÈMIA", + larpAcademiaJoinTitle: "Cases L.A.R.P.", + larpInviteRedeem: "Unir-se a la casa", + larpInviteCreate: "Generar un codi d'invitació", + larpInvitePlaceholder: "Codi d'invitació", + larpInviteBannerTitle: "Nou codi d'invitació", + larpInviteBannerHint: "Comparteix aquest codi amb algú que sigui a ACADÈMIA. Caduca d'aquí a 30 cicles o després d'un ús.", + larpTestRankingTitle: "Puntuació per casa", + invitesHousesTitle: "L.A.R.P.", + ecoTaxLabel: "Taxa ECO", + bankTaxes: "Impostos", + bankOverviewYourTaxes: "Impostos totals", + bankTaxesEcoTaxTitle: "Taxa ECO", + bankTaxesArchTaxTitle: "Taxa ARCH", + bankTaxesUserAmount: "La teva taxa ECO (import a retornar)", + bankTaxesArchTaxUserAmount: "La teva taxa ARCH (import a retornar)", + bankTaxesArchTaxFirstBlock: "Edat del teu primer bloc", + bankTaxesArchTaxRate: "Tipus", + bankTaxesTotalBlocks: "Blocs totals", + bankTaxesTotalBytes: "Bytes totals", + bankTaxesTotalCarbon: "Carboni total", + bankTaxesTotalEcoin: "Taxa ECO total (tota la vida)", + bankTaxesEcoTaxLifetime: "Taxa ECO (tota la vida)", + bankTaxesAnnualEcoin: "Taxa ECO (anual)", + bankTaxesMonthlyEcoin: "Taxa ECO (mensual)", + bankTaxesArchTaxLifetime: "Taxa ARCH (tota la vida)", + bankTaxesArchTaxAnnual: "Taxa ARCH (anual)", + bankTaxesArchTaxMonthly: "Taxa ARCH (mensual)", + bankTaxesTotalLifetime: "Total (tota la vida)", + bankTaxesTotalAnnual: "Total (anual)", + bankTaxesTotalMonthly: "Total (mensual)", + bankTaxesRate: "Tipus", + bankTaxesSpan: "Període de mostreig", + bankTaxesLookupTitle: "Inspeccionar un bloc", + bankTaxesLookupPlaceholder: "ID del bloc (p. ex. %abc...=.sha256)", + bankTaxesLookupButton: "Inspeccionar", + bankTaxesLookupNotFound: "No s'ha trobat cap bloc amb aquest ID al teu feed local.", + bankTaxesLookupAuthor: "Autor", + bankTaxesLookupType: "Tipus", + bankTaxesLookupSize: "Mida", + bankTaxesLookupCarbon: "Petjada de carboni", + bankTaxesLookupEcoin: "Taxa ECO", + bankExchangeEcoTaxAnnual: "Impostos d'ECOin (anual)", + bankExchangeEcoTaxMonthly: "Impostos d'ECOin (mensual)", + bankTaxesTypesLabel: "Selecciona quins impostos vols pagar", + bankTaxesTypesApply: "Establir els meus impostos", + bankRulesPoolTitle: "Fons mensual", + bankRulesShareTitle: "Repartiment per usuari", + bankRulesKarmaTitle: "Karma (puntuació d'implicació)", + bankRulesEcoTaxTitle: "Taxa ECO", + bankRulesArchTaxTitle: "Taxa ARCH", + bankRulesChipTitle: "Banda de color del xip de la taxa ECO", + bankRulesEpochKind: "Granularitat de l'època", + bankRulesAlpha: "Alpha (quota màx. del saldo del pub per època)", + bankRulesReserveMin: "Reserva mínima (es manté al saldo del pub)", + bankRulesCapPerEpoch: "Límit per època (absolut)", + bankRulesGraceDays: "Període de gràcia de la reclamació", + bankRulesWMin: "Pes mínim (w_min)", + bankRulesWMax: "Pes màxim (w_max)", + bankRulesFloor: "Mínim per usuari (brut)", + bankRulesCapUser: "Límit per usuari i època", + bankRulesEcoTaxRate: "Tipus", + bankRulesArchTaxRate: "Tipus", + bankRulesCarbonFactor: "Factor de carboni", + statsTombstoneEmpty: "Encara no hi ha esborrats (tombstones) a la xarxa.", + blockchainBlockNotAvailable: "Aquest bloc no és al teu feed local. Pot pertànyer a un node que encara no repliques.", + blockchainBackToBlockexplorer: "Tornar a Blockexplorer", + audioFilterBcs: "BCS", + audioTranscodeButton: "TRANSCODIFICAR", + audioTranscodeDetailTitle: "Transcodificar", + audioTranscodeDetailDescription: "Descodifica la càrrega útil incrustada i el mapa de composició original de la blockchain.", + audioTranscodeStegoTimestamp: "Generat el", + audioTranscodeStegoMessage: "TEXT", + audioTranscodeStegoEmpty: "(cap)", + audioTranscodeStegoUnknown: "—", + audioTranscodeStegoNotFound: "No s'ha pogut descodificar cap càrrega esteganogràfica en aquest àudio.", + audioTranscodeCompositionEmpty: "Aquest àudio no inclou cap composició de blockchain desada.", + audioBackToBcs: "Tornar a BCS", + melodyCompositionTitle: "Mapa de composició de la blockchain", + melodyFilterMine: "MEVES", + melodyAllEmpty: "Encara no hi ha composicions BCS d'altres habitants.", + melodyNoteUnavailable: "el bloc no és al teu feed local", + melodyUploadBcsTitle: "Publicar", + melodyUploadBcsButton: "Publicar", + melodyUploadStegoLabel: "Missatge ocult (esteganografia)", + melodyUploadStegoMaxLabel: "màx. 280 caràcters", + melodyUploadStegoPlaceholder: "Opcional.", + melodyUploadBcsNameNote: "L'àudio es publicarà amb el nom generat automàticament", + larpLastAttemptTitle: "El teu darrer intent", + larpLastAttemptHouse: "Casa", + larpLastAttemptWhen: "Quan", + larpLastAttemptCooldown: "Pròxim intent d'aquí a", + larpTestTitle: "Test d'entrada", + larpTestIntro: "Respon les 10 preguntes. Necessites almenys 7 respostes correctes per ser admès. Pots intentar un test cada 30 cicles sigui quin sigui el resultat.", + larpTestSubmit: "Unir-se a la casa", + larpTestCooldownActive: "Pròxim test disponible d'aquí a", + larpTestCooldownDays: "cicles", + larpTestResultTitle: "Resultat del test", + larpTestScore: "Puntuació", + larpTestNextAttempt: "Pots intentar un altre test d'aquí a 30 cicles.", + larpGoToHouse: "Anar a la teva casa", + larpBackToAcademia: "Tornar a ACADÈMIA", + larpBadgeYou: "Tu", + larpBadgeRuling: "Governa", + modulesLarpLabel: "L.A.R.P.", + modulesLarpDescription: "Mòdul per a la capa de joc de rol en viu.", + larpProfileQ1: "Quan t'enfrontes a un problema complex, què fas primer?", + larpProfileQ1O1: "Parlar amb altres persones per buscar consens", + larpProfileQ1O2: "Construir un prototip per provar-lo", + larpProfileQ1O3: "Investigar la bibliografia", + larpProfileQ1O4: "Sabotejar el sistema que el provoca", + larpProfileQ2: "Què dona sentit a la teva feina diària?", + larpProfileQ2O1: "Defensar els habitants que estimo", + larpProfileQ2O2: "Crear alguna cosa tangible", + larpProfileQ2O3: "Guarir o tenir cura de la vida", + larpProfileQ2O4: "Treballar amb paraules i idees", + larpProfileQ2O5: "Fer riure els altres", + larpProfileQ3: "Quan sorgeix un conflicte al teu grup, tu…", + larpProfileQ3O1: "Lideres el diàleg", + larpProfileQ3O2: "Prens partit i et mantens ferm", + larpProfileQ3O3: "Deixes anar una broma per rebaixar la tensió", + larpProfileQ3O4: "Qüestiones si el conflicte és real", + larpProfileQ3O5: "Busques les causes ecològiques d'arrel", + larpProfileQ4: "El teu projecte preferit a llarg termini seria…", + larpProfileQ4O1: "Construir una ciutat", + larpProfileQ4O2: "Restaurar un bosc", + larpProfileQ4O3: "Redactar una constitució", + larpProfileQ4O4: "Organitzar un festival", + larpProfileQ4O5: "Muntar una xarxa de defensa", + larpProfileQ4O6: "Dissenyar una màquina nova", + larpProfileQ4O7: "Curar un arxiu", + larpProfileQ4O8: "Desmuntar un ordre injust", + larpProfileQ5: "Què fa una bona líder?", + larpProfileQ5O1: "Algú que escolta i fa de mediadora", + larpProfileQ5O2: "Algú capaç de lluitar i protegir", + larpProfileQ5O3: "Algú que coneix la història", + larpProfileQ5O4: "Algú que et fa somriure", + larpProfileQ6: "La teva relació amb les regles:", + larpProfileQ6O1: "Les regles sorgeixen del diàleg i la llei", + larpProfileQ6O2: "Les regles s'han de complir estrictament", + larpProfileQ6O3: "Les regles s'han de trencar sovint", + larpProfileQ6O4: "Les regles han de servir la vida", + larpProfileQ6O5: "Les regles aixequen infraestructures sòlides", + larpProfileQ7: "Com gestiones la informació?", + larpProfileQ7O1: "La curo i la preservo", + larpProfileQ7O2: "La comparteixo a través d'històries", + larpProfileQ7O3: "En qüestiono els orígens", + larpProfileQ7O4: "N'extrec el que és útil", + larpProfileQ7O5: "La faig servir per guarir", + larpProfileQ8: "La teva idea d'èxit és…", + larpProfileQ8O1: "Una màquina que funciona", + larpProfileQ8O2: "Una comunitat en pau", + larpProfileQ8O3: "Un festival animat", + larpProfileQ8O4: "Una família segura", + larpProfileQ8O5: "Un arxiu intacte", + larpProfileQ8O6: "Un dogma esquerdat", + larpProfileQ8O7: "Una collita pròspera", + larpProfileQ8O8: "Un edifici acabat", + larpProfileQ9: "En llevar-te, el que vols és…", + larpProfileQ9O1: "Entrenar el cos", + larpProfileQ9O2: "Llegir o escriure", + larpProfileQ9O3: "Conrear o cuinar", + larpProfileQ9O4: "Remenar alguna cosa", + larpProfileQ9O5: "Qüestionar l'autoritat", + larpProfileQ9O6: "Planificar un projecte", + larpProfileQ9O7: "Parlar amb les teves amigues", + larpProfileQ9O8: "Fer art", + larpProfileQ10: "La teva debilitat podria ser:", + larpProfileQ10O1: "Parlar massa", + larpProfileQ10O2: "Ser massa pragmàtica", + larpProfileQ10O3: "Ser massa idealista", + larpProfileQ10O4: "Ser massa disruptiva", + larpProfileQ10O5: "Ser massa rígida", + larpProfileQ10O6: "Ser massa despreocupada", + larpProfileQ10O7: "Ser massa prudent", + larpProfileQ10O8: "Ser massa ambiciosa", + uxModeTitle: "UX", + uxModeDescription: "Selecciona quin mode de navegació UX vols per a la teva interfície.", + uxModeMenus: "Blocs", + uxModeChats: "Converses", + uxModeFeed: "Microblogging", + feedTrendingTitle: "Etiquetes en tendència", + feedActiveTitle: "Habitants actius", + cnBrandSub: "Lliure · P2P · Federada", + cnSyncedPeers: "Nodes sincronitzats", + cnLicense: "Llicència", + cnNotAccessible: "El contingut no és accessible en aquest moment.", + cnUntitled: "Sense títol", + cnSearchPlaceholder: "Cercar…", + cnHubTitle: "HUB Clearnet", + cnHubDescription: "Contingut públic compartit pels habitants d'aquesta xarxa Oasis.", + cnHubEmpty: "Encara no s'ha compartit contingut públic a la Clearnet.", + cnPublicContent: "Contingut", + cnTribesContent: "Tribus", + cnNetworkContent: "Xarxa", + cnCategoryEmpty: "No hi ha contingut en aquesta categoria.", + cnInhabitantEmpty: "Aquest habitant encara no ha publicat contingut a la Clearnet.", + cnPrevPage: "← Anterior", + cnNextPage: "Següent →", + cnPageOf: "Pàgina {page} de {pages}", + cnMoreInfo: "Més informació →", + cnOpenCourse: "Curs obert", + cnAuction: "SUBHASTA", + cnExchange: "INTERCANVI", + cnRssDescription: "publicats pels habitants d'aquest HUB", + cnKindAudio: "Àudio", + cnKindBlog: "Blog", + cnKindBookmark: "Marcador", + cnKindDocument: "Document", + cnKindEvent: "Esdeveniment", + cnKindFeed: "Feed", + cnKindImage: "Imatge", + cnKindJob: "Feina", + cnKindMarket: "Mercat", + cnKindPodcast: "Podcast", + cnKindProject: "Projecte", + cnKindCourse: "Curs", + cnKindShop: "Botiga", + cnKindTorrent: "Torrent", + cnKindFile: "Fitxer", + cnKindVideo: "Vídeo", + cnKindWiki: "Wiki", + cnKindRoom: "Sala", + cnKindMap: "Mapa", + uxModeAINav: "IA", + + } +}; diff --git a/src/client/assets/translations/oasis_de.js b/src/client/assets/translations/oasis_de.js index 0cc9ae8..7132912 100644 --- a/src/client/assets/translations/oasis_de.js +++ b/src/client/assets/translations/oasis_de.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { de: { - bbAdd: "Hinzufügen", - bbQuickActions: "Schnellaktionen", - bbPickTitle: "Modul anheften", - bbPickDesc: "Wähle ein Modul für den +-Platz in der unteren Leiste.", - bbPickNone: "Keins", - bbManage: "Bearbeiten", - bbYourBar: "Deine Leiste", - bbRemove: "Entfernen", - bbFull: "Maximum erreicht", - bbDone: "Fertig", - activityChangeFilter: "Filter ändern", - emptyConnectHint: "Verbinde dich mit einem Pub, um dich mit dem Netzwerk zu synchronisieren.", - emptyConnectCta: "Mit einem Pub verbinden", - menuCommunity: "Gemeinschaft", - activityGroupCommunity: "Gemeinschaft & Governance", - activityGroupOrg: "Organisation", - activityGroupComm: "Kommunikation", - activityGroupEconomy: "Wirtschaft", - activityGroupMedia: "Inhalte & Medien", - activityGroupOther: "Sonstiges", - languageName: "Deutsch", shopOrderStatusPaid: "Zahlung erhalten", shopOrderStatusReceived: "Erhalten", shopOrderMarkPaid: "Zahlung erhalten", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "Einkäufe", shopMyOrdersDescription: "Deine Bestellungen.", shopMyOrdersEmpty: "Du hast noch keine Käufe.", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PN Verkäufer", shopOrderSeller: "Verkäufer", - shopOrderPaymentConcept: "Zahlung", shopOrderInvoice: "Rechnung", shopOrderInvoiceConcept: "Rechnung", shopOrderStatusPending: "Ausstehend", shopOrderStatusAccepted: "Angenommen", shopOrderStatusRejected: "Abgelehnt", shopOrderStatusShipped: "Versandt", - shopOrderStatusDelivered: "Geliefert", shopOrderAccept: "Annehmen", shopOrderReject: "Ablehnen", shopOrderMarkShipped: "Als versandt markieren", - shopOrderMarkDelivered: "Als geliefert markieren", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "Vertrag", + shopOrderPmBuyer: "PN Käufer", shopOrderContractConcept: "Shop-Bestellung", shopOrdersPending: "Ausstehende Bestellungen", all: "Alle", @@ -53,7 +28,7 @@ module.exports = { recent: "Aktuell", anonymous: "Anonym", unnamed: "Anonym", - authorLabel: "AUTOR", + authorLabel: "Autor", delete: "Löschen", save: "Speichern", vote: "Abstimmen", @@ -75,18 +50,17 @@ module.exports = { viewJson: "JSON anzeigen", matchScore: "Übereinstimmung", preferencesLabel: "Einstellungen", - inhabitantProfileTitle: "Bewohnerprofil", + inhabitantProfileTitle: "Bewohner", contentWarningLabel: "Inhaltswarnung", invalidPost: "Ungültiger Inhalt", invalidPostHint: "Diese Nachricht hat ungültigen/leeren Text.", - spreadBy: "Von", spreadChron: "Verbreitung", spreaded: "Verbreitet", - spreadMore: "mehr", spreadContentUnavailable: "Inhalt noch nicht verfügbar (Replikation ausstehend)", filteredByTag: "Nach Tag gefiltert", filteredByTagTitle: "Nach Tag gefiltert", feedPublishedSuccess: "Beitrag erfolgreich veröffentlicht!", + feedMastodonFailed: "In Oasis veröffentlicht, konnte aber nicht an Mastodon gesendet werden.", tribeFeedSent: "Nachricht erfolgreich gesendet!", tribeContentEncrypted: "Verschlüsselter Inhalt", tribeTorrentsEmpty: "Noch keine Torrents.", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "Kein Schweregrad", calendarDeleteDate: "Datum löschen", calendarIntervalUntil: "Bis", - bookmarkCategory: "Kategorie", bookmarkLastVisit: "Letzter Besuch", profileClearnetBookmarksLabel: "Lesezeichen", - forumFilterHot: "Beliebt", invitesPort: "Port", - currentlyUnrecheable: "FEHLER!", - peerHostValidation: "Gültige IPv4 (z. B. 192.168.1.100) oder Hostname (z. B. pub.example.com)", + peerHostValidation: "Gültige IPv4 (z. B. 192.168.1.100) oder Hostname (z. B. pub.solarnethub.com)", peerPortValidation: "Port 1-65535", peerKeyValidation: "SSB ed25519 öffentlicher Schlüssel (@<44 Zeichen base64>=.ed25519)", inboxJobUnsubscribedTitle: "Abmeldung von deinem Stellenangebot", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "Aufgabenbeschreibung", exportPasswordLabel: "Export-Passwort", importPasswordPlaceholder: "Passwort eingeben", - searchCasesPlaceholder: "Fälle suchen…", + searchCasesPlaceholder: "In Gerichte suchen...", shopBuyDeliveryAddressPlaceholder: "Lieferadresse", shopBuyNotesPlaceholder: "Notizen", bankTaxesUserNoteBold: "die ECO-Steuer direkt senken", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "Fall unterstützen", courtsSettlementAcceptBtn: "Vergleich annehmen", courtsSupportCount: "Unterstützer", - courtsVerdictVoteTitle: "Über das Urteil abstimmen", - courtsVerdictVoteLabel: "Urteilsabstimmung", - courtsVerdictVoteAccept: "Annehmen", - courtsVerdictVoteReject: "Ablehnen", - courtsVerdictVoteSubmit: "Stimme abgeben", reportsSetStatus: "Status setzen", reportsStatusClosed: "GESCHLOSSEN", statsAvgPerInhabitant: "Durchschnitt pro Bewohner", statsCarbonMaxAnnual: "Jährliche Maximalschätzung", statsCarbonNetwork: "Netzwerk gesamt", statsCarbonOfEstMax: "der geschätzten Maximalkapazität", + statsCarbonYearProgress: "des Jahres vergangen", + statsNetworkTitle: "Netzwerk", + statsStorageTitle: "Speicher", + statsEcoTaxTitle: "ECO-Steuer", + statsAccountTitle: "Konto", + statsAveragesTitle: "Durchschnitte", statsCarbonOfNetwork: "des Netzwerk-Gesamtwerts", statsCarbonUser: "Dein Fußabdruck", statsContentCountColumn: "Anzahl", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "Top-Tags", statsTopTypesTitle: "Top-Inhaltstypen", statsTotalMsgs: "Nachrichten gesamt", - feedViewDetails: "Details anzeigen", - eventFileLabel: "Bild anhängen", - taskFileLabel: "Bild anhängen", - courtsRespondentRequired: "Beklagten-ID erforderlich", extended: "Multiversum", extendedDescription: [ "Wenn du jemanden unterstützt, kannst du Beiträge von dessen unterstützten Bewohnern herunterladen. Diese erscheinen hier, nach Aktualität sortiert.", @@ -215,47 +183,76 @@ module.exports = { ], profile: "Avatar", inhabitants: "Bewohner", - peersReplicatedFeeds: "Replizierte Feeds", + peersReplicatedFeeds: "Repliziert", + peersTorColumn: "TOR", + peersTorYes: "Über Tor erreichbar", + peersTorNo: "Nicht über Tor erreichbar", + peersOasisId: "ID", + peersOasisVersion: "Version", graphos: "Graphos", graphosDescription: "Interaktive Karte des Netzwerks um dich herum.", + graphosFederation: "Föderation", + graphosFedPubs: "PUBs", + graphosFedUnreachable: "Nicht erreichbar", + graphosFedLinks: "Gegenseitige Verbindungen", + graphosFedIsolated: "Isoliert", + graphosFedNoData: "Keine Daten", + graphosFedMutual: "Gegenseitig", + graphosFedOneWay: "Einseitig", + graphosFedTransport: "Netz", + graphosFedWith: "Föderiert mit", + graphosZoom: "Vergrößern", + graphosFedInhabitants: "Bewohner", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "Dein Knoten kennt noch keinen PUB.", graphosViewingGraphOf: "Ansicht", graphosBackToMine: "← Mein Netzwerk", graphosShowing: "Zeige", graphosYou: "Du", graphosTotalNodes: "Knoten insgesamt", - manualMode: "Manueller Modus", mentions: "Erwähnungen", mentionsDescription: [ - strong("Beiträge, die dich @erwähnen"), - ", nach Aktualität sortiert.", + strong("Jede Nachricht, die dich @erwähnt"), + ".", ], - nextPage: "Weiter", - previousPage: "Zurück", noMentions: "Du hast noch keine @Erwähnungen erhalten.", + mentionsSearchPlaceholder: "In Erwähnungen suchen...", privateReminders: "ERINNERUNGEN", + inboxFilterPms: "PNs", + inboxFilterNotifications: "Benachrichtigungen", + inboxMarkRead: "Als gelesen markieren", + inboxMarkUnread: "Als ungelesen markieren", + inboxUnreadChip: "Ungelesen", + inboxMarkAllRead: "Alle als gelesen markieren", + inboxDeleteShown: "Alle löschen", + inboxArchive: "Archivieren", + inboxUnarchive: "Dearchivieren", + inboxFilterArchived: "Archiviert", + inboxEarlierMessages: "frühere Nachrichten", + inboxSortThread: "Nach Thread", + inboxSortRecent: "Neueste zuerst", + inboxAllBots: "Bots", + settingsInboxBotsTitle: "Inbox-Benachrichtigungen", + settingsInboxBotsDesc: "Wähle, welche Bots dich in deiner Inbox benachrichtigen dürfen. Abgewählte Bots werden ausgeblendet und ihre Hinweise nicht gezählt.", inboxFiltersLabel: "Filter:", inboxToggleToMutuals: "Auf Gegenseitige umschalten", inboxToggleToWhole: "Auf Alle umschalten", - privateFrom: "Von", - privateTo: "An", privateDate: "Datum", pmReply: "Antworten", pmReplies: "Antworten", - pmNew: "neu", - pmMarkRead: "Als gelesen markieren", inReplyTo: "ALS ANTWORT AUF", + forumInThread: "IM THREAD", pmPreview: "Vorschau", pmPreviewTitle: "Nachrichtenvorschau", peers: "Verbindungen", searchDescription: "Beschreibung", - imageSearch: "Bildersuche", searchFromLabel: "Von", searchToLabel: "Bis", - searchMinOpinionsLabel: "Min. Meinungen", - searchInhabitantLabel: "Bewohner (Oasis ID)", searchQueryLabel: "Suche", - searchPerPageLabel: "Ergebnisse pro Seite", + searchPerPageLabel: "Ergebnisse", settings: "Einstellungen", + settingsDescription: "Richte deine Netzwerkumgebung ein.", continueReading: "Weiterlesen", moreComments: "weitere Kommentare", readThread: "den Rest des Threads lesen", @@ -264,67 +261,46 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Spiel die Melodie deiner Blockchain — jeder Block wird zu einer Note.', melodyEmpty: 'Noch keine Noten — deine Blockchain hat noch keinen Block erzeugt.', - melodyCompositionHelp: 'Jeder Block deiner Blockchain wird je nach Typ und Größe zu einer Note.', - melodyStatsTitle: 'Aufschlüsselung nach Typ', - melodyInhabitantLabel: 'Bewohner', melodyTotalBlocks: 'Noten', - melodyType: 'Typ', - melodyCount: 'Blöcke', melodyFilterAll: 'ALLE', - melodyFilterShare: 'Melodie hochladen', - melodyShareTitle: 'Teile deine Melodie', - melodyShareHelp: 'Veröffentliche eine Momentaufnahme deiner aktuellen Melodie, damit andere sie anhören und remixen können.', - melodyShareTitleLabel: 'Titel (optional)', - melodyShareTitlePlaceholder: 'Ein Blockchain-Geist', - melodyShareSubmit: 'Melodie veröffentlichen', - melodyNoShared: 'Noch keine Blockchain-Melodien.', melodyRegenerate: 'Neu erzeugen', melodyDownload: "BCS herunterladen", - profileActivityTitle: 'Aktivität', - profileActivityMore: 'Alle Aktivität anzeigen', profileContentSectionTitle: 'Avatar-Inhalt', profileContentHelp: 'Wähle, welche deiner Module auf deinem Avatar angezeigt werden.', profileSensorsHelp: 'Optionale Metriken, die auf deinem Avatar angezeigt werden.', profileClearnetHelp: 'Module, die von außerhalb von Oasis aufgerufen werden können.', profileHubAll: 'Alle', - profileHubTitle: 'Öffentlicher Inhalt', - footerPulseTitle: 'Puls', - footerPulsePeers: 'Peers', - footerPulseInbox: 'Posteingang', - footerPulseSync: 'Letzte Sync', - footerPulseEcoValue: 'ECO/h', - footerPulseLastActivity: 'Letzte Aktivität', footerLicenseLabel: 'Lizenz', - profileSwitchToOasis: 'Zurück zu Oasis', - profileSwitchToClearnet: 'Eintauchen ins Clearnet', - profileVisibilityFediverse: "Fediverse", - profileSwitchToFediverse: "Ins Fediverse eintauchen", - coordLabel: 'Koordinate (z.B. A3)', - coordPlaceholder: 'Koordinate eingeben', + profileVisibilityFediverse: "Multiversum", contributorsTitle: "Mitwirkende", - pixeliaBy: "von", colorLabel: 'Farbe wählen', paintButton: 'Malen!', - invalidCoordinate: 'Ungültige Koordinate', - goToMuralButton: "Wandbild anzeigen", totalPixels: 'Pixel gesamt', modules: "Module", modulesViewTitle: "Module", modulesViewDescription: "Konfiguriere deine Umgebung durch Aktivieren oder Deaktivieren von Modulen.", inbox: "Posteingang", + inboxSearchPlaceholder: "In Posteingang suchen...", multiverse: "Multiversum", - fediverse: "Fediverse", - fediverseDescription: "Verwalte deine anderen Fediverse-Konten, einschließlich Senden und Empfangen von Inhalten.", + fediverse: "Multiversum", + fediverseDescription: "Verwalte deine anderen Multiversum-Konten, einschließlich Senden und Empfangen von Inhalten.", fediverseStatus: "Status", - fediverseDisconnected: "Fediverse getrennt. Überprüfe %LINK% oder den Verbindungsstatus.", - fediverseSettingsTitle: "Fediverse", + fediverseDisconnected: "Multiversum getrennt. Überprüfe %LINK% oder den Verbindungsstatus.", + fediverseSettingsTitle: "Multiversum", fediverseInstanceLabel: "Adresse", fediverseTokenLabel: "Zugriffstoken", - fediverseTokenHelp: "Lege dein Zugriffstoken fest. Es wird verwendet, um dein Mastodon-Konto aus Oasis heraus zu verwalten.", fediverseConnect: "Verbinden", fediverseConnectedAs: "Verbunden als", + nextcloudServerLabel: "Server", + nextcloudUserLabel: "Benutzer", + nextcloudFolderLabel: "Ordner", + nextcloudUploadButton: "Hochladen", + nextcloudFileLabel: "Datei auswählen", + fileExternalLink: "Externer Link", + nextcloudErrConnect: "Nextcloud war mit diesen Angaben nicht erreichbar.", + nextcloudErrUpload: "Der Upload zu Nextcloud ist fehlgeschlagen.", + nextcloudErrShare: "Nextcloud hat keinen öffentlichen Link für die Datei geliefert.", fediverseDisconnect: "Trennen", - fediverseEmpty: "Wenn du weitere Fediverse-Konten in %LINK% verbindest, kannst du sie von hier aus verwalten.", fediverseEmptyLink: "deinen Einstellungen", fediverseComposePlaceholder: "Was denkst du gerade?", fediverseReplyPlaceholder: "Schreibe deine Antwort…", @@ -333,21 +309,15 @@ module.exports = { fediverseReply: "Antworten", fediverseBoosted: "teilte", fediverseNoPosts: "Deine Timeline ist leer.", - fediverseThread: "Thread", fediverseTimeline: "Zeitleisten", - fediverseManageTimeline: "Zeitleiste verwalten", fediverseFollowers: "Follower", fediverseFollowing: "Folge ich", fediversePosts: "Beiträge", fediverseJoined: "Beigetreten", - fediverseOverview: "Übersicht", fediverseRefresh: "Aktualisieren", fediverseWrite: "Schreiben", fediversePreview: "Vorschau", - fediverseEdit: "Bearbeiten", - fediverseOpenFeed: "Feed ansehen", fediverseManage: "Verwalten", - fediverseStatusNotConnected: "Nicht verbunden", fediverseError: "Mastodon konnte nicht erreicht werden.", fediverseErrInstance: "Ungültige Instanz-URL.", fediverseErrAuth: "Ungültiges oder abgelaufenes Token.", @@ -358,17 +328,76 @@ module.exports = { fediverseErrPost: "Konnte nicht veröffentlichen.", fediverseErrMedia: "Datei konnte nicht hochgeladen werden.", fediverseErrEmpty: "Schreibe etwas oder hänge eine Datei an.", - popularLabel: "Höhepunkte", - topicsLabel: "Themen", - latestLabel: "Neueste", - summariesLabel: "Zusammenfassungen", - threadsLabel: "Diskussionen", - multiverseLabel: "Multiversum", - inboxLabel: "Posteingang", - invitesLabel: "Einladungen", - walletLabel: "Geldbörse", - legacyLabel: "Schlüssel", - cipherLabel: "Verschlüsselung", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API-Hash", + telegramPhoneLabel: "Telefonnummer", + telegramSendCode: "Code senden", + telegramCodeHelp: "Gib den Code ein, den Telegram dir gerade geschickt hat (App oder SMS).", + telegramCodeLabel: "Code", + telegramPasswordHelp: "Dieses Konto nutzt die Zwei-Schritt-Bestätigung. Gib dein Passwort ein.", + telegramPasswordLabel: "Passwort", + telegramVerify: "Bestätigen", + telegramCancel: "Abbrechen", + telegramChats: "Chats", + telegramUnread: "Ungelesen", + telegramOpen: "Öffnen", + telegramSend: "Senden", + telegramYou: "Du", + telegramNoChats: "Noch keine Chats.", + telegramNoMessages: "Noch keine Nachrichten.", + telegramComposePlaceholder: "Nachricht schreiben…", + telegramChannel: "Kanal", + telegramGroup: "Gruppe", + telegramPrivate: "Privat", + telegramErrConnect: "Telegram konnte nicht erreicht werden.", + telegramErrTimeout: "Telegram hat zu lange nicht geantwortet.", + telegramErrAuth: "Telegram-Sitzung ungültig oder abgelaufen. Erneut verbinden.", + telegramErrFetch: "Chats konnten nicht geladen werden.", + telegramErrSend: "Nachricht konnte nicht gesendet werden.", + telegramErrMissing: "API-ID, API-Hash und Telefonnummer sind erforderlich.", + telegramErrCode: "Ungültiger Code. Erneut versuchen.", + telegramErrCodeExpired: "Der Code ist abgelaufen. Neu beginnen.", + telegramErrPassword: "Falsches Passwort.", + telegramErrPhone: "Ungültige Telefonnummer.", + telegramErrApi: "Ungültige API-ID oder API-Hash.", + telegramErrFlood: "Zu viele Versuche. Warte kurz und versuche es erneut.", + telegramErrNoLogin: "Keine Anmeldung aktiv. Neu beginnen.", + peertubeUsernameLabel: "Benutzername", + peertubePasswordLabel: "Passwort", + peertubeSubscriptions: "Abonnements", + peertubeMyVideos: "Meine Videos", + peertubeVideos: "Videos", + peertubeFollowers: "Follower", + peertubeViews: "Aufrufe", + peertubeLikes: "Likes", + peertubeQuota: "Kontingent", + peertubeUpload: "Video hochladen", + peertubeUploadHelp: "Wähle eine Videodatei. Sie wird in deinen Kanal hochgeladen und danach von PeerTube verarbeitet.", + peertubeTitleLabel: "Titel", + peertubeDescriptionLabel: "Beschreibung", + peertubePrivacyLabel: "Sichtbarkeit", + peertubePrivacyPublic: "Öffentlich", + peertubePrivacyUnlisted: "Nicht gelistet", + peertubePrivacyPrivate: "Privat", + peertubeNoVideos: "Noch keine Videos.", + peertubeComments: "Kommentare", + peertubeComment: "Kommentieren", + peertubeCommentPlaceholder: "Kommentar schreiben…", + peertubeNoComments: "Noch keine Kommentare.", + peertubeOpen: "Auf PeerTube öffnen", + peertubeWatch: "Ansehen", + peertubeChannel: "Kanal", + peertubeNoStream: "Dieses Video kann hier noch nicht abgespielt werden. Öffne es auf PeerTube.", + peertubeUploaded: "Video hochgeladen. PeerTube verarbeitet es.", + peertubeErrConnect: "PeerTube ist nicht erreichbar.", + peertubeErrAuth: "Falscher Benutzername oder falsches Passwort, oder die Sitzung ist abgelaufen. Verbinde dich erneut.", + peertubeErrInstance: "Ungültige Instanz-URL.", + peertubeErrMissing: "Instanz, Benutzername und Passwort sind erforderlich.", + peertubeErrFetch: "Videos konnten nicht geladen werden.", + peertubeErrUpload: "Das Video konnte nicht hochgeladen werden.", + peertubeErrTitle: "Titel erforderlich.", + peertubeErrComment: "Der Kommentar konnte nicht veröffentlicht werden.", + peertubeErrNoFile: "Wähle eine Videodatei.", bookmarksLabel: "Lesezeichen", videosLabel: "Videos", torrentsLabel: "Torrents", @@ -379,34 +408,22 @@ module.exports = { inhabitantsLabel: "Bewohner", trendingLabel: "Trends", eventsLabel: "Termine", - tasksLabel: "Aufgaben", apply: "Anwenden", menuPersonal: "Persönlich", - menuContent: "Inhalt", - menuBlogs: "Blogs", menuGovernance: "Regierung", menuOffice: "Büro", - menuMultiverse: "Multiversum", - menuNetwork: "Netzwerk", + menuNetwork: "Gemeinschaft", menuCreative: "Kreativ", menuEconomy: "Wirtschaft", - menuMedia: "Medien", + menuMedia: "Bibliothek", menuTools: "Werkzeuge", comment: "Kommentar", subtopic: "Unterthema", json: "JSON", - createdBy: "von", unfollow: "Nicht mehr unterstützen", follow: "Unterstützen", block: "Blockieren", unblock: "Entsperren", - newerPosts: "Neuere Beiträge", - olderPosts: "Ältere Beiträge", - feedRangeEmpty: "Der angegebene Bereich ist leer für diesen Feed. Versuchen Sie den ", - seeFullFeed: "gesamten Feed", - feedEmpty: "Das Oasis-Netzwerk hat nie Beiträge von diesem Konto gesehen.", - beginningOfFeed: "Dies ist der Anfang des Feeds", - noNewerPosts: "Es wurden noch keine neueren Beiträge empfangen.", relationshipNotFollowing: "Du wirst nicht unterstützt", relationshipTheyFollow: "Unterstützt dich", relationshipMutuals: "Gegenseitige Unterstützung", @@ -416,25 +433,23 @@ module.exports = { relationshipBlockedBy: "Du bist blockiert", relationshipMutualBlock: "Gegenseitige Blockierung", relationshipNone: "Du unterstützt nicht", - relationshipConflict: "Konflikt", relationshipBlockingPost: "Blockierter Beitrag", viewLikes: "Verbreitungen ansehen", + viewSupporters: "Unterstützer ansehen", spreadedDescription: "Liste der vom Bewohner verbreiteten Beiträge.", - totalspreads: "Verbreitungen gesamt", - attachFiles: "Dateien anhängen", preview: "Vorschau", publish: "Schreiben", contentWarningPlaceholder: "Betreff zum Beitrag hinzufügen (optional)", - privateWarningPlaceholder: "Bewohner hinzufügen, um einen privaten Beitrag zu senden (optional)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "Schreibe den Text deines Beitrags.", + publishTooLong: "Dein Beitrag ist zu lang. Bitte kürze ihn.", + keepTextRetry: "Erneut senden", + dateInPastError: "Das Datum darf nicht in der Vergangenheit liegen", publishCustomDescription: [ "ACHTUNG: Aufgrund der Blockchain-Technologie kann ein veröffentlichter Beitrag nicht bearbeitet oder gelöscht werden.", ], commentWarning: [ "ACHTUNG: Aufgrund der Blockchain-Technologie kann ein veröffentlichter Beitrag nicht bearbeitet oder gelöscht werden.", ], - commentPublic: "öffentlich", - commentPrivate: "privat", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -454,14 +469,12 @@ module.exports = { ".", ], publishCustom: "Erweiterten Beitrag schreiben", - subtopicLabel: "Ein Unterthema dieses Beitrags erstellen", messagePreview: "Beitragsvorschau", mentionsMatching: "Passende Erwähnungen", mentionsName: "Name", mentionsRelationship: "Beziehung", - updateit: "UPDATES HOLEN!", updateBannerText: "Eine neue Version von Oasis ist verfügbar.", - updateBannerAction: "Jetzt aktualisieren →", + updateBannerAction: "Jetzt aktualisieren!", info: "Info", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -469,44 +482,142 @@ module.exports = { timeAgo: "her", sendTime: "etwa ", theme: "Design", - legacy: "Schlüssel", - legacyTitle: "Schlüssel", - legacyDescription: "Verwalte deinen geheimen Schlüssel (privater Schlüssel) schnell und sicher.", - legacyExportButton: "Exportieren", - legacyImportButton: "Importieren", + backupKeysExportButton: "Exportieren", + backupKeysImportButton: "Importieren", ssbLogStream: "Blockchain", ssbLogStreamDescription: "Konfiguriere das Nachrichtenlimit für Blockchain-Streams.", + logWindowChip: "Letzte {n} Blockchain-Streams", saveSettings: "Einstellungen speichern", - exportTitle: "Daten exportieren", exportDescription: "Passwort festlegen (mind. 32 Zeichen), um deinen Schlüssel zu verschlüsseln", - exportDataTitle: "Sicherung", + exportDataTitle: "Sofort-Sicherung", exportDataDescription: "Lade deine Daten herunter (geheimer Schlüssel ausgeschlossen!)", exportDataButton: "Datenbank herunterladen", - pubWallet: "PUB-Wallet", - pubWalletDescription: "Setze die PUB-Wallet-URL. Diese wird für PUB-Transaktionen (einschließlich UBI) verwendet.", - pubWalletConfiguration: "Konfiguration speichern", - importTitle: "Daten importieren", importDescription: "Importiere deinen verschlüsselten geheimen Schlüssel, um deinen Avatar zu aktivieren", - importAttach: "Verschlüsselte Datei anhängen (.enc)", - passwordLengthInfo: "Das Passwort muss mindestens 32 Zeichen lang sein.", passwordImport: "Gib dein Passwort ein, um die Daten zu entschlüsseln", exportPasswordPlaceholder: "Klein-, Großbuchstaben, Zahlen und Symbole verwenden", - fileInfo: "Dein verschlüsselter Schlüssel wird im Systemverzeichnis gespeichert (Name: oasis.enc)", + fileInfo: "Dein verschlüsselter Schlüssel wird auf dein Gerät heruntergeladen (Name: oasis.enc)", + developer: "Entwicklung", + devTitle: "Entwicklung", + welcomeBannerText: "Dein Avatar ist noch nicht eingerichtet.", + aiSuggestionBanner: "Vorschlag:", + aiSuggestionsEnable: "KI-Vorschläge", + welcomeBannerAction: "Loslegen", + welcomeTitle: "Willkommen", + welcomeStepIdentity: "Identität", + welcomeStepType: "Typ", + welcomeStepUse: "Nutzung", + welcomeStepLook: "Aussehen", + welcomeStepCommunity: "Gemeinschaft", + welcomeStepIntro: "Willkommen", + welcomeHintIdentity: "Dein Name und dein Bild.", + welcomeHintType: "Wie sich Oasis für dich öffnet.", + welcomeHintUse: "Welche Module aktiv bleiben.", + welcomeHintLook: "Das Design, das Oasis trägt.", + welcomeHintCommunity: "Tritt einem PUB bei, um andere Bewohner kennenzulernen.", + welcomeHintIntro: "Sende einen ersten Gruß an die Gemeinschaft.", + welcomeStepThemeAction: "Dieses Design verwenden", + welcomeJoinPubs: "PUBs beitreten", + welcomeNoFederation: "Ohne Föderation", + welcomeStepGreetingAction: "Senden", + welcomeDescription: "Ein paar Dinge, die sich vor dem Start lohnen.", + welcomeSkip: "Jetzt nicht", + welcomeStepProfileAction: "Profil speichern", + welcomeBootstrapRunning: "Snapshot des Pubs wird geladen. Danach wird dem Pub automatisch beigetreten; du kannst Oasis währenddessen weiter nutzen.", + welcomeCompleteTitle: "Alles bereit.", + welcomeCompleteText: "Dein Knoten ist bereit. Von hier an wächst das Netzwerk mit den Bewohnern, die du unterstützt.", + welcomeFindPeople: "Bewohner finden", + devFilterFiles: "DATEIEN", + devFilterSearch: "SUCHEN", + devFilterModules: "MODULE", + devFilterTranslations: "ÜBERSETZUNGEN", + devFilterStyles: "STILE", + devFilterTests: "TESTS", + devVersion: "Version", + devNodeVersion: "Node", + devStatsTests: "Tests", + devParentFolder: "Übergeordneter Ordner", + devOpenFolder: "öffnen", + devDescription: "Erkunde den Quellcode, der auf diesem Knoten läuft.", + devRoot: "Wurzel", + devSearchPlaceholder: "Im Code zu suchender Text", + devAnyExtension: "Beliebige Datei", + devStatsFiles: "Dateien", + devStatsLines: "Zeilen", + devStatsModules: "Module", + devStatsLanguages: "Sprachen", + devNotViewable: "nicht anzeigbar", + devEmptyFolder: "Dieser Ordner ist leer.", + devSize: "Größe", + devShowing: "Anzeige", + devLine: "Zeile", + devReportBug: "Fehler Melden", + devCreateTask: "Aufgabe Erstellen", + devRaw: "RAW", + devReportContext: "Beim Lesen des Oasis-Quellcodes gefunden", + devTaskPrefix: "Prüfen", + devPrevPage: "Vorherige Zeilen", + devNextPage: "Nächste Zeilen", + devMatches: "Treffer", + devFilesScanned: "Dateien geprüft", + devNoResults: "Noch keine Treffer.", + devColModule: "Modul", + devColState: "Status", + devColModel: "Modell", + devColView: "Ansicht", + devColRoutes: "Routen", + devColTests: "Tests", + devOn: "an", + devOff: "aus", + modulesDevLabel: "Entwicklung", + modulesDevDescription: "Modul zur Verwaltung des Oasis-Quellcodes.", themeIntro: "Wähle ein Design.", setTheme: "Design festlegen", language: "Sprache", languageDescription: "Wenn du eine andere Sprache verwenden möchtest, wähle sie hier aus.", setLanguage: "Sprache festlegen", - peerConnections: "Verbindungen", peerConnectionsIntro: "Verwalte alle Verbindungen mit anderen Peers.", + peersConnectFailed: "Verbindung fehlgeschlagen: {reason}.", + peerStateLabel: "Status", + peerAddressLabel: "Adresse", + peerLastChangeLabel: "Letzte Änderung", + peerStateConnected: "Verbunden", + peerStateConnecting: "Verbindet", + peerStateStaged: "Verfügbar", + peerErrSilent: "Keine Aktivität", + peerErrTimeout: "Antwortet nicht", + peerErrRefused: "Lehnt die Verbindung ab", + peerErrNotFound: "Unbekannte Adresse", + peerErrUnreachable: "Netz nicht erreichbar", + peerErrKeys: "Verwendet andere Schlüssel", + peerErrIdentity: "Akzeptiert deine Verbindung nicht", + peerErrTor: "Nur TOR", + peerErrDropped: "Verbindung abgebrochen", + peerErrPaused: "Netzwerk pausiert", + peerErrOther: "Verbindungsfehler", + peerOutdated: "Veraltet", + peerVersionUnannounced: "Gibt seine Version nicht bekannt: ein altes Oasis", + peersTorMissing: "Dieser PUB erlaubt nur Verbindungen über TOR.", peerConnectionsTitle: "Verbindungen", + peersOnlineHint: "Knoten, die gerade mit dir verbunden sind.", + peersDiscoveredHint: "Knoten mit bekannter Adresse, die gerade nicht verbunden sind.", + peersUnknownHint: "Knoten in deinem sozialen Graphen ohne bekannte Adresse.", + peersConnectionsHint: "Adressen, die dieser Knoten anwählen kann, und der Zustand jeder Verbindung.", + peersTabLan: "LAN", + peersTabHealth: "Zustand", + peersHealthHint: "Wie es jedem bekannten Pub oder Peer geht, im lokalen Netz und darüber hinaus.", + peersHealthReachable: "Erreichbar", + peersHealthFailing: "Fehlschlagend", + peersHealthFailures: "Fehlschläge", + peersFailingSince: "Fehlschlagend seit", + peersHealthForget: "Unerreichbare vergessen", + peersLanHint: "Knoten, die in deinem lokalen Netz gefunden wurden.", + peersLanLastSeen: "Zuletzt gesehen", + peersLanEmpty: "Noch kein Knoten im lokalen Netz gefunden.", online: "Online", offline: "Offline", discovered: 'Entdeckt', unknown: 'Unbekannt', - lanBroadcastLabel: "LAN-Broadcast", lanBroadcastActive: "Aktiv (UDP-Multicast im LAN)", - lanBroadcastDisabled: "Deaktiviert (Pub-Modus)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -519,126 +630,101 @@ module.exports = { noConnections: "Keine Peers verbunden.", noDiscovered: "Keine Peers entdeckt.", noUnknownPeers: "Keine unbekannten Peers im Freundes-Graphen.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "Noch keine Peers registriert.", + peerConnect: "Verbinden", + peerDisconnect: "Trennen", peerRefresh: "Aktualisieren", peerPruneIdle: "Inaktive entfernen", + peersPause: "Pausieren", + peersResume: "Fortsetzen", + peersPausedNote: "Pausiert: Oasis verbindet sich mit niemandem und niemand kann sich damit verbinden.", + peersPauseHint: "Stoppt alle Verbindungen: Oasis verbindet sich mit keinem anderen Knoten und nimmt keine an, bis du fortsetzt.", peerExport: "Exportieren", peerImport: "Importieren", peerImportTitle: "Peer-Liste importieren", peerImportPlaceholder: "Eine Multiserver-Adresse pro Zeile einfügen oder eine .txt-Datei hochladen…", - noSupportedConnections: "Keine Peers unterstützt.", - noBlockedConnections: "Keine Peers blockiert.", - noRecommendedConnections: "Keine Peers empfohlen.", connectionActionIntro: "", - startNetworking: "Netzwerk starten", - stopNetworking: "Netzwerk stoppen", - restartNetworking: "Netzwerk neu starten", - sync: "Netzwerk synchronisieren", indexes: "Indizes", + indexesRebuilt: "Indizes neu aufgebaut", + indexesRebuildFailed: "Indizes konnten nicht neu aufgebaut werden", + indexesFiles: "Indexdateien", indexesDescription: "Das Neuerstellen deiner Indizes ist sicher und kann einige Fehlerarten beheben.", homePageTitle: "Startseite", homePageDescription: "Wähle, welches Modul deine Startseite sein soll.", saveHomePage: "Startseite festlegen", invites: "Einladungen", - invitesTitle: "Einladungen", - invitesInvites: "Einladungen", invitesDescription: "Verwalte und verwende Einladungscodes in deinem Netzwerk.", + invitesPubsHint: "Füge einen Pub-Einladungscode ein, um dich mit ihm zu föderieren und seine Bewohner zu replizieren.", + invitesFederationsHint: "Pubs, mit denen du föderiert bist: aktualisieren, unerreichbare entfernen oder die Liste exportieren.", invitesTribesTitle: "Stämme", - invitesTribeInviteCodePlaceholder: "Stamm-Einladungscode eingeben", - invitesTribeJoinButton: "Stamm beitreten", invitesChatsTitle: "Chats", - invitesChatInviteCodePlaceholder: "Chat-Einladungscode eingeben", - invitesChatJoinButton: "Chat beitreten", invitesPadsTitle: "Pads", - invitesPadInviteCodePlaceholder: "Pad-Einladungscode eingeben", - invitesPadJoinButton: "Pad beitreten", invitesCalendarsTitle: "Kalender", - invitesCalendarInviteCodePlaceholder: "Kalender-Einladungscode eingeben", - invitesCalendarJoinButton: "Kalender beitreten", invitesEventsTitle: "Termine", - invitesEventInviteCodePlaceholder: "Termin-Einladungscode eingeben", - invitesEventJoinButton: "Termin beitreten", invitesForumsTitle: "Foren", - invitesForumInviteCodePlaceholder: "Forum-Einladungscode eingeben", - invitesForumJoinButton: "Forum beitreten", invitesMapsTitle: "Karten", - invitesMapInviteCodePlaceholder: "Karten-Einladungscode eingeben", - invitesMapJoinButton: "Karte beitreten", invitesShopsTitle: "Shops", - invitesShopInviteCodePlaceholder: "Shop-Einladungscode eingeben", - invitesShopJoinButton: "Shop beitreten", invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "Empfohlene PUBs", + invitesFilterOthers: "Andere", + invitesJoinTitle: "Einladungen", + invitesJoinHint: "Füge einen beliebigen Einladungscode ein, wofür er auch ist, oder die Oasis-ID eines Bewohners.", + invitesJoinPlaceholder: "Einladungscode oder Oasis-ID", + invitesJoinButton: "Beitreten", + invitesUsedTitle: "Letzte Einladungen", + invitesUsedEmpty: "Du hast noch keine Einladung eingelöst.", + invitesUsedType: "Typ", + invitesUsedName: "Name", + invitesUsedDate: "Datum", invitesPubInviteCodePlaceholder: "PUB-Einladungscode eingeben", invitesAcceptInvite: "PUB beitreten", + invitesPubUses: "Verwendete Einladungen", + invitesPubExhausted: "Dieser PUB hat keine Einladungen mehr.", invitesAlreadyFederated: "Du bist bereits mit diesem Pub föderiert.", + invitesJoinPubMode: "Dieses Oasis läuft als PUB im Nur-Lese-Modus. Um es mit einem anderen PUB zu verbinden, im Terminal: ./oasis.sh join ", + invitesJoinLocalOnly: "Einladungen können nur auf dem Gerät angenommen werden, auf dem Oasis läuft, oder über den Admin-Link, der beim Start angezeigt wird.", invitesFederationsTitle: "Föderationen", invitesAcceptedInvites: "Föderiert", - invitesNoInvites: "Noch keine Einladungen angenommen.", invitesUnfollow: "Entfolgen", invitesFollow: "Folgen", invitesUnfollowedInvites: "Nicht föderiert", invitesNoFederatedPubs: "Keine föderierten Pubs.", invitesNoUnfollowed: "Keine nicht föderierten Pubs.", - invitesUnreachablePubs: "Nicht erreichbar", - invitesNoUnreachablePubs: "Keine nicht erreichbaren Pubs.", invitesPubsRefresh: "Aktualisieren", - invitesPubsClearUnreachable: "Nicht erreichbare entfernen", + invitesPubsClearUnreachable: "Bereinigen", invitesPubsExport: "Exportieren", invitesPubsImport: "Importieren", invitesPubsImportTitle: "Pubs importieren", invitesPubsImportPlaceholder: "Eine Multiserver-Adresse oder einen Einladungscode pro Zeile einfügen oder eine .txt-Datei hochladen…", - currentlyUnreachable: "FEHLER!", - errorDetails: "Fehlerdetails", genericError: "Ein Fehler ist aufgetreten.", panicMode: "Panikmodus!", encryptData: "Passwort festlegen (mind. 32 Zeichen), um deine Blockchain zu verschlüsseln", decryptData: "Passwort eingeben, um deine Blockchain zu entschlüsseln", - panicModeDescription: "Verschlüssele/Entschlüssele oder LÖSCHE deine Blockchain", removeDataDescription: "WARNUNG: Dieser Vorgang kann nicht rückgängig gemacht werden.", - encryptPanicButton: "Blockchain verschlüsseln", - decryptPanicButton: "Blockchain entschlüsseln", removePanicButton: "ALLE DEINE DATEN LÖSCHEN!", searchTitle: "Suche", searchDescriptionLabel: "Suche nach Inhalten in deinem Netzwerk.", - searchLanguagesLabel: "Sprachen", - searchSkillsLabel: "Fähigkeiten", searchPlaceholder:"Nach Inhalten suchen...", - searchDateLabel:"Datum", searchLocationLabel:"Ort", searchPriceLabel:"Preis", - searchUrlLabel: "URL", searchCategoryLabel:"Kategorie", - searchStartLabel: "Startzeit", - searchEndLabel: "Endzeit", searchPriorityLabel:"Priorität", searchStatusLabel: "Status", statusLabel: "Status", - totalVotesLabel: "Stimmen gesamt", noResultsFound: "Keine Ergebnisse gefunden.", votesOption: "Abstimmungsoptionen", voteYesLabel:"Ja", voteNoLabel:"Nein", allTypesLabel: "UNBEGRENZT", - ABSTENTIONLabel: "ENTHALTUNG", YESLabel: "JA", NOLabel: "NEIN", - FOLLOW_MAJORITYLabel: "MEHRHEIT FOLGEN", - CONFUSEDLabel: "VERWIRRT", - NOT_INTERESTEDLabel: "NICHT INTERESSIERT", - StatusLabel: "Status", votesOptionYesLabel:"Ja", votesOptionNoLabel:"Nein", votesOptionAbstentionLabel: "Enthaltung", votesQuestionLabel: "Frage", - votesCreatedByLabel: "Erstellt von", voteStatusOpen: "OFFEN", voteStatusClosed: "GESCHLOSSEN", - imageSearchLabel: "Gib Wörter ein, um nach Bildern zu suchen.", commentDescription: ({ parentUrl }) => [ " hat kommentiert in ", a({ href: parentUrl }, " Thread"), @@ -649,61 +735,27 @@ module.exports = { a({ href: parentUrl }, " einem Beitrag"), ], subtopicTitle: ({ authorName }) => [`Unterthema zu @${authorName}s Beitrag`], - mysteryDescription: "hat einen geheimnisvollen Beitrag veröffentlicht", oasisDescription: "OASIS Projekt-Netzwerk", searchSubmit: "Suchen...", - postLabel: "BEITRÄGE", - aboutLabel: "BEWOHNER", - feedLabel: "FEEDS", votesLabel: "ABSTIMMUNGEN", - reportLabel: "BERICHTE", - imageLabel: "BILDER", - videoLabel: "VIDEOS", - audioLabel: "AUDIOS", - documentLabel: "DOKUMENTE", - torrentLabel: "TORRENTS", pdfFallbackLabel: "PDF-Dokument", - eventLabel: "VERANSTALTUNGEN", - taskLabel: "AUFGABEN", - transferLabel: "ÜBERWEISUNGEN", - curriculumLabel: "LEBENSLAUF", - bookmarkLabel: "LESEZEICHEN", - tribeLabel: "STÄMME", - marketLabel: "MARKT", shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "GELDBÖRSEN", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + voteLabel: "STIMMEN", submit: "Absenden", - subjectLabel: "Betreff", editProfile: "Avatar bearbeiten", - profileQrAlt: "Profil-QR-Code", - profileQrHint: "Scannen, um diese Oasis-ID zu kopieren.", oasisIdLabel: "OasisID", - spreadLabel: "Verbreiten", - spreadHint: "Verbreite dies an deine Unterstützer (über deinen Feed).", + spreadContent: "Replizieren", welcomePmSubject: "Hello World!", - welcomePmBody: "Willkommen bei Oasis — ein freies, peer-to-peer, föderiertes und verschlüsseltes soziales Netzwerk mit einer verteilten Architektur nur mit Einladung.\n\nEinige interessante Orte zum Anfangen:\n\n- /profile — Bearbeite deinen Avatar, Namen, deine Beschreibung und welche Module auf deiner öffentlichen Seite erscheinen.\n- /invites — Füge einen Pub hinzu, um dich mit dem weiteren Netzwerk zu föderieren.\n- /peers — Sieh, wer verbunden ist, scanne dein LAN erneut, exportiere oder importiere Peer-Listen.\n- /inhabitants — Entdecke und besuche die Avatare anderer Personen.\n- /tribes — Erstelle private Gruppen mit Ende-zu-Ende-Verschlüsselung oder tritt ihnen bei.\n- /inbox — Lies und sende private Nachrichten.\n- /activity — Sieh die jüngste Aktivität, deine und die deines Netzwerks.\n- /publish — Schreibe einen Beitrag oder teile ein Bild, Audio, Video oder Dokument.\n\nEinige interessante Orte zum Verbinden:\n\n- /fediverse — Verwalte deine anderen Fediverse-Konten, einschließlich Senden und Empfangen von Inhalten.\n\nDiese Nachricht wurde privat von dir an dich selbst gesendet, daher war keine Verbindung nötig, um sie zu empfangen.", - profileVisibilityTitle: "Sichtbarkeit des öffentlichen Profils", - profileVisibilityHint: "Wähle, welche Felder andere Bewohner in deinem Profil sehen. Standardmäßig wird nur Karma angezeigt.", + welcomePmBody: "Willkommen bei Oasis — ein freies, peer-to-peer, föderiertes und verschlüsseltes soziales Netzwerk mit einer verteilten Architektur nur mit Einladung.\n\nEinige interessante Orte zum Anfangen:\n\n- [/avatar](/profile) — Bearbeite deinen Avatar, Namen, deine Beschreibung und welche Module auf deiner öffentlichen Seite erscheinen.\n- /settings — Passe deine lokale Instanz an: Sprache, Design, Module, Privatsphäre und Synchronisierung.\n- /invites — Füge einen Pub hinzu, um dich mit dem weiteren Netzwerk zu föderieren.\n- /peers — Sieh, wer verbunden ist, scanne dein LAN erneut, exportiere oder importiere Peer-Listen.\n- /inhabitants — Entdecke und besuche die Avatare anderer Bewohner.\n- /tribes — Erstelle private Gruppen mit Ende-zu-Ende-Verschlüsselung oder tritt ihnen bei.\n- /search — Durchsuche die Inhalte des Netzwerks nach Typ.\n\nEinige interessante Orte zum Verbinden:\n\n- /Multiversum — Verwalte deine anderen Multiversum-Konten, einschließlich Senden und Empfangen von Inhalten.\n\nDiese Nachricht wurde privat von dir an dich selbst gesendet, daher war keine Verbindung nötig, um sie zu empfangen.", profileSensorsSectionTitle: "Sensoren", profileVisibilityActivity: "Aktivitätsstufe", - profileVisibilityDevice: "Gerät", profileDeviceLockedHint: "Dieser Sensor ist immer aktiv: Er zeigt anderen, von welchem Gerät du dich verbindest, und kann nicht deaktiviert werden.", profileVisibilityKarma: "KARMA-Punkte", profileVisibilityUbi: "UBI", - profileVisibilityWallet: "ECOIN-Wallet", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityWallet: "ECOin-Wallet", + profileVisibilityEcoTax: "ECO-Steuer", + profileVisibilityLarpSign: "L.A.R.P.-Siegel", profileVisibilityGpg: "GPG-Schlüssel", - profileVisibilityClearnet: "Mein Profil veröffentlichen", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Shops", profileClearnetJobsLabel: "Stellen", @@ -715,6 +767,10 @@ module.exports = { profileClearnetImagesLabel: "Bilder", profileClearnetDocumentsLabel: "Dokumente", profileClearnetTorrentsLabel: "Torrents", + profileClearnetPodcastsLabel: "Podcasts", + profileClearnetMarketLabel: "Markt", + profileClearnetFeedLabel: "Feed", + profileClearnetWikiLabel: "Wikis", editProfileDescription: "", profileName: "Name", @@ -740,7 +796,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "Best.", walletConfirm: "Bestätigen", - walletDescription: "Verwalte deine digitalen Vermögenswerte.", + walletDescription: "Verwalte dein ECOin-Wallet.", walletDate: "Datum", walletFee: "Gebühr (Je höher, desto schneller)", walletFeeLine: ({ fee }) => `Gebühr: ECO ${fee}`, @@ -748,6 +804,86 @@ module.exports = { walletReceive: "Empfangen", walletReset: "Zurücksetzen", walletSend: "Senden", + walletPayingTransfer: "Überweisung wird bezahlt", + walletCreateTransferLabel: "Smart Contract erzeugen (Transfer)", + walletTransferNotifiedNote: "Die Gegenseite wurde über diese Zahlung informiert", + walletTransferCreatedNote: "Für diese Zahlung wurde ein Transfer (Beleg) erstellt", + walletTransferNoPayeeNote: "Niemand hat diese Adresse veröffentlicht, daher wurde kein Transfer erstellt", + walletTransferReceiptConcept: "Zahlung aus dem Wallet", + walletPaymentPmText: "hat dir eine Zahlung gesendet", + walletPayeeLabel: "Empfänger", + walletConceptLabel: "Verwendungszweck", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Du hast dein UBI erhalten.", + bankingBotPaymentTitle: "Du hast eine Zahlung erhalten.", + bankingBotUbiPaidText: "Dein bedingungsloses Grundeinkommen wurde ausgezahlt", + bankUbiPubThisNode: "Dieser Knoten zahlt das UBI", + bankUbiPubConnected: "UBI-PUB", + bankUbiPubNone: "Noch kein PUB kündigt UBI in deinem Netz an", + bankUbiAddressPublished: "ECOin-Adresse veröffentlicht", + bankUbiAddressMissing: "Veröffentliche deine ECOin-Adresse, um UBI zu erhalten", + bankUbiClaimedThisMonth: "Diesen Monat beansprucht", + bankUbiPubBalance: "PUB-Wallet verfügbar für UBI", + bankNoPubAddress: "Das UBI-PUB hat noch keine ECOin-Adresse veröffentlicht.", + bankUbiTab: "UBI", + bankDonateEco: "SPENDEN!", + donateTooltip: "ECOin spenden", + bankNoUserAddress: "Dieser Bewohner teilt keine ECOin-Adresse.", + bankUbiPubDefault: "Standard", + bankUbiPool: "Fonds", + bankUbiLastSeen: "Zuletzt gesehen", + bankUbiLastPayout: "Letzte Aktion", + bankUbiPaidOut: "Ausgezahlt", + bankRangeToday: "Heute", + bankRangeWeek: "Woche", + bankRangeMonth: "Monat", + bankRangeYear: "Jahr", + bankRangeFiveYears: "5 Jahre", + bankRangeAll: "Alles", + bankChartValueTitle: "Entwicklung des ECOin-Werts", + bankChartValueHours: "Zeitäquivalent (Std.)", + bankChartWealthTitle: "Umverteilung des ECOin-Vermögens", + bankChartDistributed: "Verteiltes UBI (ECO)", + bankChartRebalanceTitle: "Umverteilung des Reichtums zwischen PUBs", + bankChartRebalanced: "Zwischen PUBs verschobene ECO", + bankChartTaxes: "Einbehaltene Steuern (ECO)", + bankChartNoDataRange: "Keine Daten in diesem Zeitraum.", + bankChartNoDataYet: "Noch keine Daten", + bankChartPerHour: "pro Stunde", + bankChartPerDay: "pro Tag", + bankChartPerMonth: "pro Monat", + bankTotalUbiDistributed: "Insgesamt umverteiltes UBI (Netz)", + bankTotalTaxesWithheld: "Insgesamt einbehaltene Steuern (Netz)", + bankInflationIssuance: "Emissionsrate (jährlich, aus Blockbelohnung)", + bankRulesEligibilityTitle: "Wer das UBI erhalten kann", + bankRulesEligibility1: "Eine im Profil veröffentlichte ECOin-Adresse (Profil → Sensoren → ECOin-Wallet).", + bankRulesEligibility2: "Ein Feed, der älter als 30 Tage ist und Aktivität im Netz hat: Karma über null.", + bankRulesEligibility3: "Ein Anspruch pro Monat (Epoche), in Banking mit der Schaltfläche UBI beanspruchen. Der laufende Monat kann mit UBI ablehnen abgelehnt werden.", + bankRulesEligibility4: "Der Sockel von 1 ECO wird immer gezahlt; Steuern mindern nur den Teil darüber und werden nie getrennt erhoben.", + bankRulesEligibility5: "Das PUB zahlt im nächsten Zyklus und veröffentlicht einen UBI-Transfer, den du bestätigen kannst.", + bankRulesEligibility6: "Fehlt die Adresse, ist der Feed zu jung, wurde der Monat abgelehnt oder hat das PUB keine Mittel, wird der Anspruch übersprungen und auf dem PUB protokolliert.", + copyLinkTitle: "Link kopieren", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "UBI ablehnen", + bankUbiRefusedThisMonth: "Du hast das UBI dieses Monats abgelehnt.", + bankingBotUbiAvailableTitle: "Du kannst dein UBI beanspruchen.", + bankingBotUbiAvailableText: "Dein UBI für diesen Monat ist verfügbar", + bankingBotConfirmTitle: "Du hast einen Smart Contract, der auf Bestätigung wartet", + bankingBotConfirmText: "Smart Contracts, die auf deine Bestätigung warten", + bankChartZoomHint: "Zum Vergrößern klicken", + bankChartPoolTitle: "UBI-Fonds (im Zeitverlauf)", + bankChartNetworkUbiTitle: "Insgesamt umverteiltes UBI (Netz)", + bankChartSupplyTitle: "Umlaufender ECOin-Bestand (im Zeitverlauf)", + bankChartInflationTitle: "ECOin-Inflation (im Zeitverlauf)", + bankChartInflationSeries: "Annualisierte Rate (%)", + bankChartPool: "Gesamtfonds (ECO)", + bankChartKarmaTitle: "KARMA Scoring (im Zeitverlauf)", + bankChartFundsTitle: "Dein ECOin-Guthaben (im Zeitverlauf)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin verbunden", + bankWalletNotConfigured: "ECOin getrennt", + transfersPayWithWallet: "BEZAHLEN!", walletStatus: "Status", walletDisconnected: [ "ECOin ", @@ -757,9 +893,11 @@ module.exports = { " oder den Verbindungsstatus.", ], walletSentToLine: ({ destination, amount }) => `ECO ${amount} gesendet an ${destination}`, - walletSettingsTitle: "Geldbörse", + walletNotConfigured: "Deine ECOin-Wallet ist noch nicht eingerichtet.", walletSettingsDescription: "Integriere Oasis mit deiner ECOin-Wallet.", - walletSettingsDocLink: "ECOin-Installationsanleitung", + walletSettingsMobileDescription: "Verbinde Oasis mit der ECOin-Wallet-App auf diesem Gerät.", + walletDownloadEcoin: "ECOin herunterladen!", + walletRpcUrl: "RPC-URL", walletStatusMessages: { invalid_amount: "Ungültiger Betrag", invalid_dest: "Ungültige Zieladresse", @@ -775,40 +913,23 @@ module.exports = { walletUser: "Benutzername", walletPass: "Passwort", walletConfiguration: "Wallet konfigurieren", + walletDisconnectButton: "Wallet trennen", cipher: "Verschlüsselung", cipherTitle: "Verschlüsselung", cipherDescription: "Verschlüssele und entschlüssele Text symmetrisch.", randomPassword: "Zufälliges Passwort", - cipherEncryptTitle: "Text verschlüsseln", - cipherEncryptDescription: "Text zum Verschlüsseln eingeben", - cipherTextLabel: "Zu verschlüsselnder Text", cipherTextPlaceholder: "Text eingeben...", cipherPasswordLabel: "Passwort (mind. 32 Zeichen)", cipherPasswordPlaceholder: "Passwort eingeben...", cipherEncryptButton: "Verschlüsseln", - cipherDecryptTitle: "Text entschlüsseln", - cipherDecryptDescription: "Text zum Entschlüsseln eingeben", cipherEncryptedMessageLabel: "Verschlüsselter Text", cipherDecryptedMessageLabel: "Entschlüsselter Text", - cipherPasswordUsedLabel: "Verwendetes Passwort (aufbewahren!)", cipherEncryptedTextPlaceholder: "Verschlüsselten Text eingeben...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "Initialisierungsvektor eingeben...", cipherDecryptButton: "Entschlüsseln", password: "Passwort", text: "Text", encryptedText: "Verschlüsselter Text", iv: "Initialisierungsvektor (IV)", - encryptTitle: "Text verschlüsseln", - encryptDescription: "Text und Passwort eingeben.", - encryptButton: "Verschlüsseln", - decryptTitle: "Text entschlüsseln", - decryptDescription: "Verschlüsselten Text und Passwort eingeben.", - decryptButton: "Entschlüsseln", - passwordLengthError: "Passwort muss mind. 32 Zeichen lang sein.", - missingFieldsError: "Text, Passwort oder IV fehlt.", - encryptionError: "Fehler beim Verschlüsseln.", - decryptionError: "Fehler beim Entschlüsseln.", bookmarkTitle: "Lesezeichen", bookmarkDescription: "Lesezeichen in deinem Netzwerk entdecken und verwalten.", bookmarkAllSectionTitle: "Lesezeichen", @@ -825,19 +946,16 @@ module.exports = { bookmarkFilterRecent: "NEUESTE", bookmarkCreateButton: "Lesezeichen erstellen", bookmarkUpdateButton: "Aktualisieren", - bookmarkDeleteButton: "Löschen", bookmarkAddFavoriteButton: "Favorit hinzufügen", bookmarkRemoveFavoriteButton: "Favorit entfernen", bookmarkUrlLabel: "Link", - bookmarkUrlPlaceholder: "https://example.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Beschreibung", bookmarkDescriptionPlaceholder: "Optional", bookmarkTagsLabel: "Tags", bookmarkTagsPlaceholder: "Tags durch Kommas getrennt eingeben", - bookmarkCategoryLabel: "Kategorie", - bookmarkCategoryPlaceholder: "Optional", bookmarkLastVisitLabel: "Letzter Besuch", - bookmarkSearchPlaceholder: "URL, Tags, Kategorie, Autor suchen...", + bookmarkSearchPlaceholder: "In Lesezeichen suchen...", bookmarkSortRecent: "Neueste zuerst", bookmarkSortOldest: "Älteste zuerst", bookmarkSortTop: "Meistgewählt", @@ -850,8 +968,6 @@ module.exports = { noLastVisit: "Kein letzter Besuch", videoTitle: "Videos", videoDescription: "Videoinhalte in deinem Netzwerk entdecken und verwalten.", - videoPluginTitle: "Titel", - videoPluginDescription: "Beschreibung", videoMineSectionTitle: "Deine Videos", videoCreateSectionTitle: "Video hochladen", videoUpdateSectionTitle: "Video aktualisieren", @@ -878,12 +994,11 @@ module.exports = { videoDescriptionPlaceholder: "Optional", videoNoFile: "Keine Videodatei angegeben", noVideos: "Keine Videos vorhanden.", - videoSearchPlaceholder: "Titel, Tags, Autor suchen...", + videoSearchPlaceholder: "In Videos suchen...", videoSortRecent: "Neueste zuerst", videoSortOldest: "Älteste zuerst", videoSortTop: "Meistgewählt", videoSearchButton: "Suche", - videoMessageAuthorButton: "PN", videoUpdatedAt: "Aktualisiert", videoNoMatch: "Keine Videos gefunden.", documentTitle: "Dokumente", @@ -902,7 +1017,6 @@ module.exports = { documentFilterFavorites: "FAVORITEN", documentCreateButton: "Dokument hochladen", documentUpdateButton: "Aktualisieren", - documentDeleteButton: "Löschen", documentAddFavoriteButton: "Favorit hinzufügen", documentRemoveFavoriteButton: "Aus Favoriten entfernen", documentMessageAuthorButton: "PN", @@ -915,7 +1029,7 @@ module.exports = { documentDescriptionPlaceholder: "Optional", documentNoFile: "Keine Datei.", noDocuments: "Keine Dokumente vorhanden.", - documentSearchPlaceholder: "Titel, Tags, Beschreibung, Autor suchen...", + documentSearchPlaceholder: "In Dokumente suchen...", documentSortRecent: "Neueste zuerst", documentSortOldest: "Älteste zuerst", documentSortTop: "Meistgewählt", @@ -924,8 +1038,6 @@ module.exports = { documentUpdatedAt: "Aktualisiert", audioTitle: "Audios", audioDescription: "Audioinhalte in deinem Netzwerk entdecken und verwalten.", - audioPluginTitle: "Titel", - audioPluginDescription: "Beschreibung", audioMineSectionTitle: "Deine Audios", audioCreateSectionTitle: "Audio hochladen", audioUpdateSectionTitle: "Audio aktualisieren", @@ -936,22 +1048,21 @@ module.exports = { audioFilterMine: "MEINE", audioFilterRecent: "NEUESTE", audioFilterTop: "TOP", - audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Audio hochladen", audioUpdateButton: "Aktualisieren", - audioDeleteButton: "Löschen", audioAddFavoriteButton: "Favorit hinzufügen", audioRemoveFavoriteButton: "Favorit entfernen", audioFileLabel: "Audiodatei auswählen (.mp3, .wav, .ogg)", audioTagsLabel: "Tags", audioTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + tagsPlaceholder: "Tags durch Kommas getrennt eingeben", audioTitleLabel: "Titel", audioTitlePlaceholder: "Optional", audioDescriptionLabel: "Beschreibung", audioDescriptionPlaceholder: "Optional", audioNoFile: "Keine Audiodatei angegeben", noAudios: "Keine Audios vorhanden.", - audioSearchPlaceholder: "Titel, Tags, Autor suchen...", + audioSearchPlaceholder: "In Audios suchen...", audioSortRecent: "Neueste zuerst", audioSortOldest: "Älteste zuerst", audioSortTop: "Meistgewählt", @@ -963,6 +1074,7 @@ module.exports = { audioFilterFavorites: "FAVORITEN", favoritesTitle: "Favoriten", favoritesDescription: "Alle deine favorisierten Inhalte an einem Ort.", + favoritesSearchPlaceholder: "In Favoriten suchen...", favoritesFilterAll: "ALLE", favoritesFilterRecent: "NEUESTE", favoritesFilterAudios: "AUDIOS", @@ -974,22 +1086,16 @@ module.exports = { favoritesFilterChats: "CHATS", favoritesFilterCalendars: "KALENDER", favoritesFilterVideos: "VIDEOS", - favoritesRemoveButton: "Aus Favoriten entfernen", favoritesNoItems: "Noch keine Favoriten.", yourContacts: "Deine Kontakte", allInhabitants: "Bewohner", - allCVs: "Alle Lebensläufe", + allCVs: "Lebensläufe", discoverPeople: "Entdecke Bewohner in deinem Netzwerk.", - allInhabitantsButton: "ALLE", - contactsButton: "UNTERSTÜTZT", - CVsButton: "CVs", - matchSkills: "Fähigkeiten abgleichen", - matchSkillsButton: "FÄHIGKEITEN ABGLEICHEN", - suggestedButton: "VORGESCHLAGEN", - searchInhabitantsPlaceholder: "BEWOHNER NACH NAME FILTERN …", + searchInhabitantsPlaceholder: "In Bewohner suchen...", filterLocation: "BEWOHNER NACH ORT FILTERN …", filterLanguage: "BEWOHNER NACH SPRACHE FILTERN …", filterSkills: "BEWOHNER NACH FÄHIGKEITEN FILTERN …", + cvSkillsCount: "Fähigkeiten", applyFilters: "Filter anwenden", locationLabel: "Ort", languagesLabel: "Sprachen", @@ -998,15 +1104,15 @@ module.exports = { mutualFollowers: "Gemeinsame Follower", suggestedFollowsYou: "Folgt dir", latestInteractions: "Letzte Interaktionen", - viewAvatar: "Avatar anzeigen", - viewCV: "Lebenslauf anzeigen", suggestedSectionTitle: "Vorgeschlagen", topkarmaSectionTitle: "Top Karma", topecoSectionTitle: "Top Öko", topactivitySectionTitle: "Top Aktivität", + "TOP INACTIVITYButton": "AM INAKTIVSTEN", + topinactivitySectionTitle: "Am wenigsten aktive Bewohner", blockedSectionTitle: "Blockiert", gallerySectionTitle: "GALERIE", - blockedButton: "BLOCKIERT", + topSectionTitle: "TOP", blockedLabel: "Blockierter Bewohner", inhabitantviewDetails: "Details anzeigen", keepReading: "Weiterlesen...", @@ -1021,10 +1127,23 @@ module.exports = { parliamentFilterCandidatures: "KANDIDATUREN", parliamentFilterProposals: "VORSCHLÄGE", parliamentFilterLaws: "GESETZE", - parliamentFilterHistorical: "HISTORISCH", - parliamentFilterLeaders: "ANFÜHRER", + parliamentFilterRevocations: "ABBERUFUNGEN", + parliamentFilterHistorical: "VERLAUF", + parliamentFilterLeaders: "FÜHRUNG", parliamentFilterRules: "REGELN", parliamentGovernmentCard: "Aktuelle Regierung", + parliamentSeatsGovernment: "Regierung", + parliamentSeatsOpposition: "Opposition", + parliamentSeatsAbstention: "Enthaltung", + parliamentSeatsQuorum: "Wahlquorum", + parliamentSeatsPass: "Zustimmungsschwelle", + parliamentSeatsByElection: "Nach Wahl", + parliamentSeatsByHouses: "Nach LARP-Haus", + parliamentSeatsSeats: "Sitze", + parliamentSeatsSeat: "Sitz", + parliamentSeatsLeader: "Anführer", + parliamentSeatsVotesFor: "Stimmen für", + parliamentSeatsNoHouse: "Ohne Haus", parliamentActorInPowerInhabitant: 'REGIERENDER BEWOHNER', parliamentActorInPowerTribe: 'REGIERENDER STAMM', parliamentHistoricalGovernmentsTitle: 'REGIERUNGEN', @@ -1046,10 +1165,8 @@ module.exports = { parliamentPoliciesDeclined: "GESETZE ABGELEHNT", parliamentPoliciesDiscarded: "GESETZE VERWORFEN", parliamentEfficiency: "% EFFIZIENZ", - parliamentFilterRevocations: 'WIDERRUFE', parliamentRevocationFormTitle: 'Gesetz widerrufen', parliamentRevocationLaw: 'Gesetz', - parliamentRevocationTitle: 'Titel', parliamentRevocationReasons: 'Begründung', parliamentRevocationPublish: 'Widerruf veröffentlichen', parliamentCurrentRevocationsTitle: 'Aktuelle Widerrufe', @@ -1062,23 +1179,12 @@ module.exports = { parliamentNoGovernments: "Noch keine Regierungen vorhanden.", parliamentCandidatureFormTitle: "Kandidatur vorschlagen", parliamentCandidatureIdPh: "Oasis-ID (@...) oder Stammname", - parliamentCandidatureSlogan: "Slogan (max. 140 Zeichen)", - parliamentCandidatureSloganPh: "Ein kurzes Motto", parliamentCandidatureMethod: "Methode", parliamentCandidatureProposeBtn: "Kandidatur veröffentlichen", - parliamentThDate: "Vorschlagsdatum", - parliamentThSlogan: "Slogan", - parliamentThSince: "Profil seit", - parliamentThVotes: "Erhaltene Stimmen", - parliamentThVoteAction: "Abstimmen", - parliamentTypeUser: "Bewohner", - parliamentTypeTribe: "Stamm", parliamentVoteBtn: "Abstimmen", parliamentProposalFormTitle: "Gesetz vorschlagen", parliamentProposalDescription: "Beschreibung (≤1000)", parliamentProposalPublish: "Vorschlag veröffentlichen", - parliamentFinalize: "Abschließen", - parliamentDeadline: "Frist", parliamentNoProposals: "Noch keine Gesetzesvorschläge vorhanden.", parliamentNoLaws: "Noch keine Gesetze genehmigt.", parliamentMethodDEMOCRACY: "Demokratie", @@ -1096,29 +1202,22 @@ module.exports = { parliamentVotesSlashTotal: "Stimmen/Gesamt", parliamentVotesNeeded: "Benötigte Stimmen", parliamentFutureLawsTitle: "Zukünftige Gesetze", - parliamentNoFutureLaws: "Noch keine zukünftigen Gesetze.", parliamentVoteAction: "Abstimmen", - parliamentLeadersTitle: "Anführer", parliamentThLeader: "AVATAR", parliamentPopulation: "Bevölkerung", - parliamentThType: "Typ", - parliamentThInPower: "An der Macht", parliamentThPresented: "KANDIDATUREN", parliamentNoLeaders: "Noch keine Anführer.", parliamentCandidaturesListTitle: "Liste der Kandidaturen", parliamentTimeRemaining: "Verbleibende Zeit", - parliamentNoLeader: "Noch keine führende Kandidatur.", parliamentElectionsStatusTitle: "Nächste Regierung", parliamentProposalDeadlineLabel: "Frist", + parliamentViewReport: "Bericht ansehen", parliamentProposalTimeLeft: "Verbleibende Zeit", - parliamentProposalOnTrack: "Auf Kurs zur Annahme", - parliamentProposalOffTrack: "Noch nicht genügend Unterstützung", parliamentRulesTitle: "Wie das Parlament funktioniert", parliamentRulesIntro: "Wahlen werden alle 2 Monate aufgelöst; Kandidaturen laufen durchgehend und werden zurückgesetzt, wenn eine Regierung gewählt wird.", parliamentRulesCandidates: "Jeder Bewohner kann sich selbst, einen anderen Bewohner oder einen Stamm vorschlagen. Jeder Bewohner kann bis zu 3 Kandidaturen pro Zyklus einreichen; Duplikate im selben Zyklus werden abgelehnt.", parliamentRulesElection: "Der Gewinner ist die Kandidatur mit den meisten Stimmen zum Zeitpunkt der Auflösung.", parliamentRulesTies: "Stichwahl-Reihenfolge: höchstes Bewohner-Karma; bei Gleichstand ältestes Profil; bei weiterem Gleichstand frühester Vorschlag; dann lexikographisch nach ID.", - parliamentRulesFallback: "Wenn niemand abstimmt, gewinnt die zuletzt vorgeschlagene Kandidatur. Wenn keine Kandidaturen vorliegen, wird ein zufälliger Stamm ausgewählt.", parliamentRulesTerm: "Der Regierungs-Tab zeigt die aktuelle Regierung und Statistiken.", parliamentRulesMethods: "Formen: Anarchie (einfache Mehrheit), Demokratie (50%+1), Mehrheit (80%), Minderheit (20%), Karmatokratie (Vorschläge mit höchstem Karma), Diktatur (sofortige Genehmigung).", parliamentRulesAnarchy: "Anarchie ist der Standardmodus: Wenn bei der Auflösung keine Kandidatur gewählt wird, wird Anarchie ausgerufen. Unter Anarchie kann jeder Bewohner Gesetze vorschlagen.", @@ -1127,6 +1226,7 @@ module.exports = { parliamentRulesLaws: "Wenn ein Vorschlag seine Schwelle erreicht, wird er zum Gesetz und erscheint im Gesetze-Tab mit seinem Inkrafttretungsdatum.", parliamentRulesHistorical: "Im Historisch-Tab kannst du jeden Regierungszyklus sehen, der stattgefunden hat, und Daten über dessen Verwaltung.", parliamentRulesLeaders: "Im Anführer-Tab kannst du ein Ranking der Bewohner/Stämme sehen, die regiert (oder kandidiert) haben, sortiert nach Effizienz.", + parliamentRulesHemicycle: "Das Halbrund vergibt einen Sitz pro Bewohner: den Regierungsblock (Wähler der siegreichen Kandidatur und der Anführer), je einen Oppositionsblock pro weiterer Kandidatur des Zyklus und die Enthaltung. Die gestrichelten Marken zeigen das Wahlquorum (25 % des Zensus) und die Zustimmungsschwelle der aktuellen Methode. „Nach LARP-Haus“ färbt dieselben Sitze nach Haus.", parliamentProposalVoteStatusLabel: "Abstimmungsstatus", parliamentProposalOnTrackYes: "Schwelle erreicht", parliamentProposalOnTrackNo: "Unter der Schwelle", @@ -1140,11 +1240,8 @@ module.exports = { courtsCaseFormTitle: "Fall eröffnen", courtsCaseRespondent: "Angeklagter / Beklagter", courtsCaseRespondentPh: "Oasis-ID (@...) oder Stammname", - courtsCaseMediatorsAccuser: "Mediatoren (Ankläger)", + courtsCaseSubjectPlaceholder: "Betreff oder kurze Beschreibung", courtsCaseMethod: "Lösungsmethode", - courtsCaseDescription: "Beschreibung (max. 1000 Zeichen)", - courtsCaseEvidenceTitle: "Fallbeweise", - courtsCaseEvidenceHelp: "Bilder, Audios, Dokumente (PDF) oder Videos anhängen, die deinen Fall unterstützen.", courtsCaseSubmit: "Fall einreichen", courtsNominateJudge: "Richter nominieren", courtsJudgeId: "Richter", @@ -1177,34 +1274,33 @@ module.exports = { courtsMethodDICTATOR: "Diktator", courtsMethodPOPULAR: "Volksabstimmung", courtsMethodKARMATOCRACY: "Karmatokratie", + courtsCaseType: "Art des Falls", + courtsPresetMinorConflict: "Leichter Konflikt", + courtsPresetModerateConflict: "Mittlerer Konflikt", + courtsPresetSevereConflict: "Schwerer Konflikt", + courtsPresetHarassment: "Belästigung oder Missbrauch", + courtsPresetContentModeration: "Inhaltsmoderation", + courtsPresetBanRestriction: "Sperre oder Einschränkung", + courtsPresetGovernanceDispute: "Governance-Streit", + courtsStatusOPEN: "Offen", + courtsStatusIN_PROGRESS: "In Bearbeitung", + courtsStatusDECIDED: "Entschieden", + courtsStatusCLOSED: "Geschlossen", + courtsStatusSOLVED: "Gelöst", + courtsStatusUNSOLVED: "Ungelöst", + courtsStatusDISCARDED: "Verworfen", courtsRulesTitle: "Wie Gerichte funktionieren", courtsRulesIntro: "Gerichte sind ein gemeinschaftlich geführter Prozess zur Konfliktlösung und Förderung restaurativer Gerechtigkeit. Dialog, klare Beweise und verhältnismäßige Maßnahmen werden priorisiert.", courtsRulesLifecycle: "Ablauf: 1) Fall eröffnen 2) Methode wählen 3) Beweise einreichen 4) Anhörung und Beratung 5) Urteil und Maßnahme 6) Einhaltung und Abschluss 7) Berufung (falls zutreffend).", - courtsRulesRoles: "Ankläger: eröffnet den Fall. Verteidigung: angeklagte Person oder Stamm. Methode: von der Gemeinschaft gewählter Mechanismus zur Erleichterung, Beweiswürdigung und Urteilsfindung. Zeuge: liefert Aussagen oder Beweise. Mediatoren: neutrale Personen, eingeladen vom Ankläger und/oder der Verteidigung, mit Zugang zu allen Details, die helfen, den Konflikt zu deeskalieren und Vereinbarungen zu schaffen.", + courtsRulesRoles: "Ankläger: eröffnet den Fall. Verteidigung: angeklagter Bewohner oder Stamm. Methode: von der Gemeinschaft gewählter Mechanismus zur Erleichterung, Beweiswürdigung und Urteilsfindung. Zeuge: liefert Aussagen oder Beweise. Mediatoren: neutrale Bewohner, eingeladen vom Ankläger und/oder der Verteidigung, mit Zugang zu allen Details, die helfen, den Konflikt zu deeskalieren und Vereinbarungen zu schaffen.", courtsRulesEvidence: "Beschreibung bis zu 1000 Zeichen. Relevante und rechtmäßige Bilder, Audios, Videos und PDF-Dokumente anhängen. Keine sensiblen privaten Daten ohne Einwilligung teilen.", courtsRulesDeliberation: "Anhörungen können öffentlich oder privat sein. Richter sorgen für Respekt, fordern Klarstellungen an und können irrelevantes oder rechtswidriges Material verwerfen.", courtsRulesVerdict: "Wiederherstellung wird priorisiert: Entschuldigungen, Mediationsvereinbarungen, Inhaltsmoderation, vorübergehende Einschränkungen oder andere verhältnismäßige Maßnahmen. Die Begründung muss protokolliert werden.", courtsRulesAppeals: "Berufung: zulässig bei neuen Beweisen oder einem klaren Verfahrensfehler. Muss innerhalb von 7 Tagen eingereicht werden, sofern nicht anders angegeben.", - courtsRulesPrivacy: "Privatsphäre und Sicherheit respektieren. Doxxing, Hass oder Drohungen werden entfernt. Richter können Teile der Akte bearbeiten oder versiegeln, um gefährdete Personen zu schützen.", + courtsRulesPrivacy: "Privatsphäre und Sicherheit respektieren. Doxxing, Hass oder Drohungen werden entfernt. Richter können Teile der Akte bearbeiten oder versiegeln, um gefährdete Bewohner zu schützen.", courtsRulesMisconduct: "Belästigung, Manipulation oder gefälschte Beweise können zu einer sofortigen negativen Lösung führen.", courtsRulesGlossary: "Fall: Aufzeichnung eines Konflikts. Beweis: Materialien, die Ansprüche unterstützen. Urteil: Entscheidung mit Maßnahme. Berufung: Antrag auf Überprüfung des Urteils.", courtsFilterActions: "AKTIONEN", - courtsNoActions: "Keine ausstehenden Aktionen für deine Rolle.", - courtsCaseTitlePlaceholder: "Kurze Beschreibung des Konflikts", - courtsCaseSeverity: "Schweregrad", - courtsCaseSeverityNone: "Kein Schweregrad", - courtsCaseSeverityLOW: "Niedrig", - courtsCaseSeverityMEDIUM: "Mittel", - courtsCaseSeverityHIGH: "Hoch", - courtsCaseSeverityCRITICAL: "Kritisch", - courtsCaseSubject: "Thema", - courtsCaseSubjectNone: "Kein Thema-Tag", - courtsCaseSubjectBEHAVIOUR: "Verhalten", - courtsCaseSubjectCONTENT: "Inhalt", - courtsCaseSubjectGOVERNANCE: "Regierung / Regeln", - courtsCaseSubjectFINANCIAL: "Finanzen / Ressourcen", - courtsCaseSubjectOTHER: "Sonstiges", - courtsHiddenRespondent: "Verborgen (nur für beteiligte Rollen sichtbar).", courtsThRole: "Rolle", courtsRoleAccuser: "Ankläger", courtsRoleDefence: "Verteidigung", @@ -1215,51 +1311,23 @@ module.exports = { courtsAssignJudgeBtn: "Richter auswählen", trendingTitle: "Trends", exploreTrending: "Entdecke die beliebtesten Inhalte in deinem Netzwerk.", - bookmarkButton: "LESEZEICHEN", - transferButton: "ÜBERWEISUNGEN", - eventButton: "TERMINE", - taskButton: "AUFGABEN", votesButton: "ABSTIMMUNGEN", - reportButton: "BERICHTE", - feedButton: "FEED", - marketButton: "MARKT", - imageButton: "BILDER", - audioButton: "AUDIOS", - videoButton: "VIDEOS", - documentButton: "DOKUMENTE", - torrentButton: "TORRENTS", - noTrendingFound: "Keine Trendinhalte gefunden.", - noContentMessage: "Noch keine Trendinhalte vorhanden.", - trendingDescription: "Beschreibung", - trendingDate: "Datum", - trendingLocation: "Ort", - trendingPrice: "Preis", - trendingUrl: "URL", - trendingCategory: "Kategorie", - trendingStart: "Start", - trendingEnd: "Ende", - trendingPriority: "Priorität", - trendingStatus: "Status", - trendingFrom: "Von", - trendingTo: "An", - trendingConcept: "Konzept", - trendingAmount: "Betrag", - trendingDeadline: "Frist", - trendingItemStatus: "Status", - trendingTotalVotes: "Stimmen gesamt", + shopProductButton: "SHOP", + podcastButton: "PODCAST", + podcastEpisodeButton: "EPISODE", + campaignButton: "KAMPAGNE", + logisticsRouteButton: "ROUTE", trendingTotalOpinions: "Meinungen gesamt", - trendingNoContentMessage: "Noch keine Trendinhalte vorhanden.", - trendingAuthor: "Von", - trendingCreatedAtLabel: "Erstellt am", + trendingNoContentMessage: "Noch keine Trends vorhanden.", trendingTotalCount: "Gesamtzahl", tasksTitle: "Aufgaben", tasksDescription: "Aufgaben in deinem Netzwerk entdecken und verwalten.", + taskSearchPlaceholder: "In Aufgaben suchen...", taskTitleLabel: "Titel", taskDescriptionLabel: "Beschreibung", taskStartTimeLabel: "Startzeit", taskEndTimeLabel: "Endzeit", taskPriorityLabel: "Priorität", - taskPrioritySelect: "Priorität auswählen", taskPriorityUrgent: "Dringend", taskPriorityHigh: "Hoch", taskPriorityMedium: "Mittel", @@ -1269,13 +1337,13 @@ module.exports = { taskVisibilityLabel: "Sichtbarkeit", taskPublic: "öffentlich", taskPrivate: "privat", - taskCreatedAt: "Erstellt am", - taskBy: "Von", taskStatus: "Status", taskStatusOpen: "Öffnen", taskStatusInProgress: "In Bearbeitung", taskStatusClosed: "Geschlossen", - taskAssignedTo: "Zugewiesen an", + taskAssignedTo: "Zugewiesen", + taskAssignedChip: "Zugewiesen", + taskUnassignedChip: "Nicht zugewiesen", taskAssignees: "Zugewiesene", taskAssignButton: "Mir zuweisen", taskUnassignButton: "Zuweisung aufheben", @@ -1283,12 +1351,12 @@ module.exports = { taskUpdateButton: "Aktualisieren", taskDeleteButton: "Löschen", taskFilterAll: "ALLE", + taskFilterRecent: "NEUESTE", taskFilterMine: "MEINE", taskFilterOpen: "OFFEN", taskFilterInProgress: "IN BEARBEITUNG", taskFilterClosed: "GESCHLOSSEN", taskFilterAssigned: "ZUGEWIESEN", - taskFilterArchived: "ARCHIVIERT", taskFilterUrgent: "DRINGEND", taskFilterHigh: "HOCH", taskFilterMedium: "MITTEL", @@ -1301,35 +1369,47 @@ module.exports = { taskInProgressTitle: "Aufgaben in Bearbeitung", taskClosedTitle: "Geschlossene Aufgaben", taskAssignedTitle: "Zugewiesene Aufgaben", - taskArchivedTitle: "Archivierte Aufgaben", - taskPublicTitle: "Öffentliche Aufgaben", - taskPrivateTitle: "Private Aufgaben", notasks: "Keine Aufgaben vorhanden.", noLocation: "Kein Ort angegeben", taskSetStatus: "Status setzen", - eventTitle: "Termine", eventDateLabel: "Datum", + eventRecurrenceLabel: "Wiederholung", + eventRecurrenceUntil: "Wiederholen bis", + eventRecurrenceHint: "Lass das Enddatum leer für ein einmaliges Ereignis.", + eventUpcomingDates: "Nächste Termine", eventsTitle: "Termine", eventsDescription: "Termine in deinem Netzwerk entdecken und verwalten.", - eventDescription: "Beschreibung", + eventSearchPlaceholder: "In Termine suchen...", eventPrice: "Preis", eventStatus: "Status", - eventOrganizer: "Veranstalter", eventAllSectionTitle: "Termine", eventMineSectionTitle: "Deine Termine", - eventArchivedTitle: "Archivierte Termine", eventCreateSectionTitle: "Termin erstellen", eventUpdateSectionTitle: "Termin aktualisieren", - eventDeleteButton: "Löschen", eventCreateButton: "Termin erstellen", eventAddToCalendar: "Zum Kalender hinzufügen", eventVisitCalendar: "Kalender ansehen", viewTask: "Aufgabe ansehen", + viewEvent: "Event ansehen", + viewPad: "Pad ansehen", + viewMap: "Karte ansehen", + viewCalendar: "Kalender ansehen", viewReport: "Bericht ansehen", viewTransfer: "Transfer ansehen", viewItem: "Artikel ansehen", viewShop: "Shop ansehen", viewProject: "Projekt ansehen", + viewJob: "Job Ansehen", + viewPlace: "Ort Ansehen", + generatePdf: "PDF Erzeugen", + sharePm: "Per PN Teilen", + galleryImages: "Bilder (max. 75 MB pro Datei)", + galleryPhotos: "Bilder", + galleryAddPhoto: "Bild Hinzufügen", + galleryRemovePhoto: "Entfernen", + galleryVideo: "Video (max. 75 MB)", + galleryAddVideo: "Video Hinzufügen", + galleryRemoveVideo: "Video entfernen", viewVotation: "Abstimmung ansehen", voteCastTitle: "Stimme abgeben", voteResults: "Ergebnisse", @@ -1337,48 +1417,26 @@ module.exports = { eventDescriptionLabel: "Beschreibung", eventDescriptionPlaceholder: "Terminbeschreibung eingeben...", eventUpdateButton: "Aktualisieren", - eventAttendeesLabel: "Teilnehmer", - eventAttendeesPlaceholder: "Teilnehmer eingeben, durch Kommas getrennt...", eventTagsLabel: "Tags", - eventTagsPlaceholder: "Termin-Tags eingeben, durch Kommas getrennt...", - eventTags: "Tags", eventPriceLabel: "Preis", eventUrlLabel: "URL", eventAttendees: "Teilnehmer", - noAttendees: "Noch keine Teilnehmer", - eventCreatedAt: "Erstellt am", eventLocation: "Ort", eventLocationLabel: "Ort", - eventNoLocation: "Kein Ort angegeben", - eventNoURL: "Keine URL angegeben", - eventBy: "Von", noevents: "Keine Termine vorhanden.", eventDate: "Datum", - eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Teilnehmen", - eventUnattendButton: "Nicht mehr teilnehmen", - eventCreatedBy: "Erstellt von", - eventAttendeesCount: "Teilnehmerzahl", - eventCreatedByYou: "Du hast diesen Termin erstellt", eventFilterAll: "ALLE", + eventFilterRecent: "NEUESTE", eventFilterMine: "MEINE", eventFilterToday: "HEUTE", eventFilterWeek: "DIESE WOCHE", eventFilterMonth: "DIESER MONAT", eventFilterYear: "DIESES JAHR", eventFilterArchived: "ARCHIVIERT", - eventTodayTitle: "Heutige Termine", - eventThisWeekTitle: "Termine dieser Woche", - eventThisMonthTitle: "Termine dieses Monats", - eventThisYearTitle: "Termine dieses Jahres", eventPrivacyLabel: "Sichtbarkeit", eventPublic: "Öffentlich", eventPrivate: "Privat", - eventPublicTitle: "Öffentliche Termine", - eventNoPrice: "Kostenloser Termin", - eventNoImage: "Kein Bild hochgeladen", - eventAttendConfirmation: "Du nimmst jetzt an diesem Termin teil", - eventUnattendConfirmation: "Du nimmst nicht mehr an diesem Termin teil", eventAttended: "Teilgenommen", eventUnattended: "Nicht teilgenommen", eventStatusOpen: "Öffnen", @@ -1389,9 +1447,14 @@ module.exports = { tagsTopSectionTitle: "Top-Tags", tagsCloudSectionTitle: "Tag-Wolke", tagsFilterAll: "ALLE", + tagsFilterMine: "MEINE", + tagsFilterRecent: "NEU", + tagsMineSectionTitle: "Meine Tags", + tagsRecentSectionTitle: "Neue Tags", tagsFilterTop: "TOP", tagsFilterCloud: "WOLKE", tagsNoItems: "Keine Tags vorhanden.", + tagsSearchPlaceholder: "In Tags suchen...", tagsTableHeaderTag: "TAG/LINK:", tagsTableHeaderCount: "ANZAHL:", transfersTitle: "Überweisungen", @@ -1399,7 +1462,10 @@ module.exports = { transfersFrom: "Von", transfersTo: "An", transfersFilterAll: "ALLE", + transfersFilterRecent: "NEUESTE", transfersFilterMine: "MEINE", + transfersFilterMinePending: "AUSSTEHEND", + transfersFilterMineConfirmed: "SIGNIERT", transfersFilterUBI: "UBI", transfersFilterMarket: "MARKT", transfersFilterTop: "TOP", @@ -1414,7 +1480,6 @@ module.exports = { transfersCategoryEconomic: "Wirtschaftlich", transfersCategoryTime: "Zeit", transfersCategoryTrust: "Vertrauen", - transfersCategoryApply: "Anwenden", transfersUnitEco: "ECO", transfersUnitHours: "Std", transfersUnitTrust: "Vertrauen", @@ -1431,18 +1496,12 @@ module.exports = { transfersDeadline: "Frist", transfersTags: "Tags", transfersStatus: "Status", - transfersStatusUnconfirmed: "UNBESTÄTIGT", - transfersStatusClosed: "GESCHLOSSEN", - transfersStatusDiscarded: "VERWORFEN", - transfersCreatedAt: "Erstellt am", transfersConfirmations: "BESTÄTIGUNGEN", - transfersContainingBlock: "Enthaltender Block", - transfersExportContract: "Vertrag erstellen", + transfersExportContract: "Rechnung Erzeugen", transfersConfirmButton: "Überweisung bestätigen", transfersNoItems: "Keine Überweisungen gefunden.", transfersMineSectionTitle: "Deine Überweisungen", transfersUBISectionTitle: "UBI Überweisungen", - transfersMarketSectionTitle: "Markt-Überweisungen", transfersTopSectionTitle: "Top-Überweisungen", transfersPendingSectionTitle: "Ausstehende Überweisungen", transfersUnconfirmedSectionTitle: "Unbestätigte Überweisungen", @@ -1450,21 +1509,13 @@ module.exports = { transfersDiscardedSectionTitle: "Verworfene Überweisungen", transfersCreateSectionTitle: "Transfer erstellen", transfersAllSectionTitle: "Überweisungen", - transfersFilterFavs: "Favoriten", - transfersFavsSectionTitle: "Favorisierte Überweisungen", - transfersSearchLabel: "Suche", - transfersSearchPlaceholder: "Konzept, Tags, Bewohner suchen...", + transfersSearchPlaceholder: "In Überweisungen suchen...", transfersMinAmountLabel: "Mindestbetrag", transfersMaxAmountLabel: "Höchstbetrag", - transfersSortLabel: "Sortieren nach", transfersSortRecent: "Neueste zuerst", transfersSortAmount: "Höchster Betrag", transfersSortDeadline: "Nächste Frist", transfersSearchButton: "Suche", - transfersFavoriteButton: "Favorit", - transfersUnfavoriteButton: "Kein Favorit", - transfersMessageUserButton: "Nachricht", - transfersExpiringSoonBadge: "LÄUFT AB", transfersExpiredBadge: "ABGELAUFEN", transfersUpdatedAt: "Aktualisiert", transfersNoMatch: "Keine Überweisungen gefunden.", @@ -1477,6 +1528,10 @@ module.exports = { voteClosedTitle: "Geschlossene Abstimmungen", voteAllSectionTitle: "Abstimmungen", voteCreateButton: "Abstimmung erstellen", + voteErrorTitle: "Formular prüfen", + voteErrorDeadlineMin: "Die Frist muss mindestens {days} Tage in der Zukunft liegen.", + voteErrorGeneric: "Die Abstimmung konnte nicht gespeichert werden.", + voteLocked: "Diese Abstimmung hat bereits Stimmen oder Meinungen erhalten oder gehört zum Parlament oder zu den Gerichten: Sie kann nicht mehr bearbeitet oder gelöscht werden.", voteUpdateButton: "Aktualisieren", voteDeleteButton: "Löschen", voteOptionYes: "JA", @@ -1486,6 +1541,7 @@ module.exports = { voteFollowMajority: "MEHRHEIT FOLGEN", voteNotInterested: "NICHT INTERESSIERT", voteFilterAll: "ALLE", + voteFilterRecent: "NEUESTE", voteFilterMine: "MEINE", voteFilterOpen: "OFFEN", voteFilterClosed: "GESCHLOSSEN", @@ -1501,37 +1557,28 @@ module.exports = { voteTags: "Tags", voteOpinions: "Meinungen", novotes: "Keine Abstimmungsvorschläge vorhanden.", - voteBy: "Von", voteCreatedAt: "Erstellt am", voteNoQuestion: "Keine Frage angegeben", voteUnknownCreator: "Unbekannter Ersteller", voteUnknownDate: "Unbekanntes Datum", - errorVoteNotFound: "Abstimmung nicht gefunden", - errorAlreadyVoted: "Du hast bereits abgestimmt.", - errorVoteClosedCannotEdit: "Eine geschlossene Abstimmung kann nicht bearbeitet werden", - errorVoteDeadlinePassed: "Die Frist für diese Abstimmung ist abgelaufen", - errorRetrievingVote: "Fehler beim Abrufen der Abstimmung", - errorCreatingVote: "Fehler beim Erstellen der Abstimmung", - errorVoteAlreadyVoted: "Nach Abgabe einer Meinung nicht mehr bearbeitbar", - errorDeletingOldVote: "Fehler beim Löschen der alten Meinung", - errorCreatingUpdatedVote: "Fehler beim Erstellen der aktualisierten Meinung", - errorCreatingTombstone: "Fehler beim Erstellen des Tombstone", voteDetailSectionTitle: 'Abstimmungsdetails', voteCommentsLabel: 'Kommentare', voteCommentsForumButton: 'Diskussion eröffnen', voteCommentsSectionTitle: 'Offene Diskussion', voteNoCommentsYet: 'Noch keine Kommentare. Sei der Erste, der antwortet.', + commentsGoToLast: "Zum letzten Kommentar", voteNewCommentPlaceholder: 'Schreibe deinen Kommentar hier…', voteNewCommentButton: 'Kommentar veröffentlichen', voteNewCommentLabel: 'Einen Kommentar hinzufügen', cvTitle: "Lebenslauf", - cvLabel: "LEBENSLÄUFE", cvEditSectionTitle: "Lebenslauf bearbeiten", cvCreateSectionTitle: "Lebenslauf erstellen", cvDescription: "Verwalte und teile deine beruflichen Fähigkeiten und Informationen.", cvNameLabel: "Vollständiger Name", cvDescriptionLabel: "Zusammenfassung", cvPhotoLabel: "Foto", + cvPdfLabel: "Lebenslauf (PDF)", + cvVisitButton: "Lebenslauf ansehen", cvPersonalExperiencesLabel: "Persönliche Erfahrungen", cvPersonalSkillsLabel: "Persönliche Fähigkeiten (durch Komma getrennt)", cvOasisExperiencesLabel: "Oasis-Beitragserfahrungen", @@ -1544,29 +1591,130 @@ module.exports = { cvLocationLabel: "Ort", cvStatusLabel: "Status", cvPreferencesLabel: "Präferenzen", - cvOasisContributorLabel: "Oasis-Mitwirkender", cvPersonal: "Persönlich", cvOasis: "Oasis-Mitwirkender (optional)", - cvOasisContributorView: "Oasis-Beitrag", + cvOasisContributorView: "Beitrag", cvEducational: "Bildung (optional)", cvEducationalView: "Bildung", cvProfessional: "Beruf (optional)", cvProfessionalView: "Beruf", cvAvailability: "Verfügbarkeit (optional)", - cvAvailabilityView: "Verfügbarkeit", cvUpdateButton: "Aktualisieren", cvCreateButton: "Lebenslauf erstellen", - cvContactLabel: "Kontakt", cvCreatedAt: "Erstellt am", cvUpdatedAt: "Aktualisiert am", cvEditButton: "Aktualisieren", cvDeleteButton: "Löschen", cvNoCV: "Kein Lebenslauf gefunden.", blogSubject: "Betreff", - blogMessage: "Text auf 8096 Zeichen begrenzt.", - blogImage: "Medien hochladen (max: 50MB)", - blogPublish: "Vorschau", - noPopularMessages: "Noch keine beliebten Nachrichten veröffentlicht", + blogMessage: "Nachricht", + blogImage: "Medien hochladen (max: 75MB)", + blogMedia: "Anhänge (bis zu 8 Bilder und ein Video)", + blogPublish: "Veröffentlichen", + pollsTitle: "Umfragen", + dataTitle: "Übereinstimmungen", + dataDescription: "Erkunde und visualisiere die Übereinstimmungen in deinem Netzwerk.", + dataFilterAll: "ALLE", + dataFilterMine: "MEINE", + dataFilterRecent: "NEUESTE", + dataFilterTop: "TOP", + dataKindInhabitants: "BEWOHNER", + dataKindJobs: "JOBS", + dataKindProjects: "PROJEKTE", + dataKindEvents: "EREIGNISSE", + dataKindTribes: "STÄMME", + dataKindMarket: "MARKT", + dataKindHousing: "WOHNEN", + dataKindIndustry: "INDUSTRIE", + dataKindTasks: "AUFGABEN", + dataKindReports: "BERICHTE", + dataKindVotes: "ABSTIMMUNGEN", + dataSearchPlaceholder: "In Übereinstimmungen suchen...", + dataCohesionTitle: "Kohäsionskoeffizient (CC)", + dataCcShort: "CC", + dataCohesionHint: "Durchschnittliche Affinität zwischen allem, was das Netzwerk veröffentlicht hat.", + dataAffinity: "Affinität", + dataCommonTerms: "Schlüsselwort", + dataReasonFollowing: "Du unterstützt diesen Bewohner", + dataReasonTribe: "Teilt einen Stamm mit dir", + dataReasonPinned: "Passt zu etwas, das du angeheftet hast", + dataReasonCv: "Passt zu den Fähigkeiten in deinem Lebenslauf", + dataReasonContent: "Verwandt mit dem, was du veröffentlichst", + dataReasonRelated: "Verwandt mit deinen Interessen", + dataReasonAlike: "Veröffentlicht von einem Bewohner wie dir", + dataReasonSupportsYou: "Unterstützt dich", + dataReasonMutual: "Gegenseitige Unterstützung", + dataReasonRated: "Vom Netzwerk gut bewertet", + dataReasonNear: "In deiner Nähe", + dataStatPeople: "Lebensläufe", + dataStatConnected: "Verbunden", + dataStatSkills: "Fähigkeiten", + dataBestMatch: "Bestes Match", + dataStatIsolated: "Isoliert", + dataStatEntities: "Verglichene Einheiten", + dataStatTerms: "Verschiedene Begriffe", + dataStatPairs: "Verbundene Paare", + dataMatchesTitle: "Übereinstimmungen", + dataMatchesHint: "Personalisierte Vorschläge des Algorithmus.", + dataTermsTitle: "Schlagwörter", + dataTermsHint: "Die Begriffe, die in den meisten Inhalten vorkommen.", + dataConnections: "Verknüpfte Treffer", + dataNoMatches: "Keine Querdaten vorhanden.", + dataNoProfile: "Veröffentliche Inhalte oder schreib deinen Lebenslauf, damit der Algorithmus lernt, was dich interessiert.", + pollsDescription: "Modul, um das Netzwerk zu fragen und Antworten zu zählen.", + pollFilterAll: "ALLE", + pollFilterMine: "MEINE", + pollFilterRecent: "NEUESTE", + pollFilterTop: "TOP", + pollFilterVoted: "ABGESTIMMT", + pollFilterOpen: "OFFEN", + pollFilterClosed: "GESCHLOSSEN", + pollCreateButton: "Umfrage Erstellen", + pollCreateTitle: "Umfrage Erstellen", + pollEditTitle: "Umfrage Bearbeiten", + pollQuestion: "Frage", + pollQuestionPlaceholder: "Was möchtest du fragen?", + pollOptions: "Optionen (eine pro Zeile)", + pollOutcome: "Ergebnis", + pollNoVotesYet: "Noch keine Stimmen", + pollTie: "Gleichstand", + pollOptionsPlaceholder: "Platz\nPark\nBar", + pollAnonymous: "Anonym", + pollAnonymousLabel: "Anonyme Umfrage", + pollMultiple: "Mehrfachauswahl", + pollMultipleLabel: "Mehrere Antworten erlauben", + pollDeadline: "Frist", + pollTags: "Schlagwörter", + pollPublishButton: "Umfrage Veröffentlichen", + pollUpdateButton: "Aktualisieren", + pollCloseButton: "Schließen", + pollDeleteButton: "Löschen", + pollVoteButton: "Abstimmen", + pollChangeVote: "Stimme Ändern", + pollVoters: "Abstimmende", + pollComments: "Kommentare", + pollStatusLabel: "Status", + pollStatusOpen: "OFFEN", + pollStatusClosed: "GESCHLOSSEN", + pollSearchPlaceholder: "In Umfragen suchen...", + pollsNoItems: "Keine Umfragen gefunden.", + viewPoll: "Umfrage Ansehen", + pollChatCreate: "Umfrage Erstellen", + pollInChat: "Umfrage", + blogTitle: "Blogs", + blogDescription: "Modul zum Entdecken und Verwalten von Blogs.", + blogFilterAll: "ALLE", + blogFilterMine: "MEINE", + blogFilterRecent: "NEUESTE", + blogFilterFavorites: "FAVORITEN", + blogFilterTop: "TOP", + blogCreateButton: "Blog Erstellen", + blogSearchPlaceholder: "In Blogs suchen...", + blogComments: "Kommentare", + blogAllowComments: "Kommentare erlauben", + blogCommentsClosed: "Der Autor hat die Kommentare für diesen Blog geschlossen.", + blogNoItems: "Keine Blogs gefunden.", + viewBlog: "Blog Ansehen", forumTitle: "Foren", forumCategoryLabel: "Kategorie", forumTitleLabel: "Titel", @@ -1574,43 +1722,21 @@ module.exports = { forumCreateButton: "Forum erstellen", forumCreateSectionTitle: "Forum erstellen", forumDescription: "Diskutiere offen mit anderen Bewohnern in deinem Netzwerk.", + forumSearchPlaceholder: "In Foren suchen...", forumFilterAll: "ALLE", forumFilterMine: "MEINE", forumFilterRecent: "NEUESTE", forumFilterTop: "TOP", - forumMineSectionTitle: "Deine Foren", - forumRecentSectionTitle: "Neueste Foren", - forumAllSectionTitle: "Foren", - forumDeleteButton: "Löschen", forumParticipants: "Teilnehmer", forumMessages: "Nachrichten", - forumLastMessage: "Letzte Nachricht", forumMessageLabel: "Nachricht", forumMessagePlaceholder: "Schreibe deine Nachricht...", forumSendButton: "Senden", - forumVisitForum: "Forum besuchen", noForums: "Keine Foren gefunden.", - forumVisitButton: "Forum besuchen", - forumCatGENERAL: "Allgemein", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "Politik", - forumCatTECH: "Technik", - forumCatSCIENCE: "Wissenschaft", - forumCatMUSIC: "Musik", - forumCatART: "Kunst", - forumCatGAMING: "Gaming", - forumCatBOOKS: "Bücher", - forumCatFILMS: "Filme", - forumCatPHILOSOPHY: "Philosophie", - forumCatSOCIETY: "Gesellschaft", - forumCatPRIVACY: "Privatsphäre", - forumCatCYBERWARFARE: "Cyberkrieg", - forumCatSURVIVALISM: "Survivalismus", + forumVisitButton: "Forum Ansehen", + forumTopicLabel: "Thema", imageTitle: "Bilder", imageDescription: "Bildinhalte in deinem Netzwerk entdecken und verwalten.", - imagePluginTitle: "Titel", - imagePluginDescription: "Beschreibung", imageMineSectionTitle: "Deine Bilder", imageCreateSectionTitle: "Bild hochladen", imageUpdateSectionTitle: "Bild aktualisieren", @@ -1619,17 +1745,14 @@ module.exports = { imageTopSectionTitle: "Top-Bilder", imageFavoritesSectionTitle: "Favoriten", imageGallerySectionTitle: "Galerie", - imageMemeSectionTitle: "Memes", imageFilterAll: "ALLE", imageFilterMine: "MEINE", imageFilterRecent: "NEUESTE", imageFilterTop: "TOP", imageFilterFavorites: "FAVORITEN", imageFilterGallery: "GALERIE", - imageFilterMeme: "MEMES", imageCreateButton: "Bild hochladen", imageUpdateButton: "Aktualisieren", - imageDeleteButton: "Löschen", imageAddFavoriteButton: "Favorit hinzufügen", imageRemoveFavoriteButton: "Favorit entfernen", imageFileLabel: "Bilddatei auswählen (.jpeg, .jpg, .png, .gif)", @@ -1639,33 +1762,31 @@ module.exports = { imageTitlePlaceholder: "Optional", imageDescriptionLabel: "Beschreibung", imageDescriptionPlaceholder: "Optional", - imageMemeLabel: "MEME?", imageNoFile: "Keine Bilddatei angegeben", noImages: "Keine Bilder vorhanden.", - imageSearchPlaceholder: "Titel, Tags, Beschreibung, Autor suchen...", + imageSearchPlaceholder: "In Bilder suchen...", imageSortRecent: "Neueste zuerst", imageSortOldest: "Älteste zuerst", imageSortTop: "Meistgewählt", imageSearchButton: "Suche", - imageMessageAuthorButton: "Nachricht", imageUpdatedAt: "Aktualisiert", imageNoMatch: "Keine Bilder gefunden.", + feedSearchPlaceholder: "In Feed suchen...", feedTitle: "Feed", createFeedTitle: "Feed erstellen", createFeedButton: "Feed senden!", feedPlaceholder: "Was passiert? (max. 280 Zeichen)", TODAYButton: "HEUTE", - CREATEButton: "Feed erstellen", totalOpinions: "Meinungen gesamt", moreVoted: "Meistgewählt", noFeedsFound: "Keine Feeds gefunden.", createdAtLabel: "Erstellt am", FeedshareYourOpinions: "Kurztexte in deinem Netzwerk entdecken und teilen.", - refeedButton: "Refeed", + refeedButton: "Weiterleiten", alreadyRefeeded: "Du hast dies bereits refeeded.", activityTitle: "Aktivität", yourActivity: "Deine Aktivität", - globalActivity: "Globale Aktivität", + globalActivity: "Aktivität", activityList: "Aktivität", activityDesc: "Sieh dir die neueste Aktivität in deinem Netzwerk an.", allButton: "ALLE", @@ -1678,20 +1799,13 @@ module.exports = { concept: "Konzept", description: "Beschreibung", meme: "Meme", - activityContact: "Kontakt", - activityBy: "Name", - activityPixelia: "Neues Pixel hinzugefügt", - viewImage: "Bild anzeigen", - playAudio: "Audio abspielen", - playVideo: "Video abspielen", typeRecent: "NEUESTE", - errorActivity: "Fehler beim Abrufen der Aktivität", - typePost: "BLOGS", - recentButton: "NEUESTE", + typeTop: "TOP", + typePost: "BLOG", + activityUpdateLabel: "AKTUALISIERUNG", typeTribe: "STÄMME", typeLarp: "L.A.R.P.", typeInhabitants: "BEWOHNER", - activityProfileUpdated: "hat sein Profil aktualisiert", typeAbout: "BEWOHNER", typeCurriculum: "LEBENSLÄUFE", typeImage: "BILDER", @@ -1713,7 +1827,7 @@ module.exports = { typeFeed: "FEEDS", typeContact: "KONTAKT", typePub: "PUB", - typeTombstone: "TOMBSTONE", + typeTombstone: "GELÖSCHT", typeBanking: "BANKWESEN", typeBankWallet: "BANKWESEN/WALLET", typeBankClaim: "BANKWESEN/UBI", @@ -1738,8 +1852,6 @@ module.exports = { typeCourtsSettlementAccepted: "Gerichte · Vergleich angenommen", typeCourtsNomination: "Gerichte · Nominierung", typeCourtsNominationVote: "Gerichte · Nominierungsabstimmung", - activitySupport: "Neue Allianz geschmiedet", - activityJoin: "Neuem PUB beigetreten", question: "Frage", deadline: "Frist", status: "Status", @@ -1753,12 +1865,8 @@ module.exports = { date: "Datum", category: "Kategorie", attendees: "Teilnehmer", - activitySpread: "->", - visitLink: "Link besuchen", - viewDocument: "Dokument anzeigen", location: "Ort", contentWarning: "Betreff", - personName: "Bewohnername", bankWalletConnected: "ECOin Wallet", bankUbiReceived: "UBI erhalten", bankTx: "Tx", @@ -1766,9 +1874,8 @@ module.exports = { bankingUserEngagementScore: "KARMA-Bewertung", viewDetails: "Details anzeigen", link: "Link", - activityProjectFollow: "%OASIS% unterstützt jetzt dieses Projekt %PROJECT%", - activityProjectUnfollow: "%OASIS% unterstützt dieses Projekt nicht mehr %PROJECT%", - activityProjectPledged: "%OASIS% hat %AMOUNT% für das Projekt %PROJECT% zugesagt", + activityProjectFollow: "%OASIS% %ACTION% jetzt dieses Projekt %PROJECT%", + activityProjectUnfollow: "%OASIS% %ACTION% dieses Projekt %PROJECT%", following: "UNTERSTÜTZT", unfollowing: "NICHT MEHR UNTERSTÜTZT", pledged: "ZUGESAGT", @@ -1814,26 +1921,17 @@ module.exports = { courtsVotesSlashTotal: "JA / GESAMT", courtsOpenVote: "Abstimmung eröffnen", courtsAnswerTitle: "Auf die Klage antworten", - courtsStanceADMIT: "Zugeben", - courtsStanceDENY: "Bestreiten", - courtsStancePARTIAL: "Teilweise", - courtsStanceCOUNTERCLAIM: "Gegenklage", - courtsStanceNEUTRAL: "Neutral", courtsVerdictResult: "Ergebnis", courtsVerdictOrders: "Anordnungen", courtsSettlementText: "Bedingungen", courtsSettlementAccepted: "Angenommen", - courtsSettlementPending: "Ausstehend", courtsJudge: "Richter", courtsThSupports: "Unterstützungen", courtsFilterOpenCase: 'FALL ERÖFFNEN', - courtsEvidenceFileLabel: 'Beweisdatei (Bild, Audio, Video oder PDF)', - courtsCaseMediators: 'Mediatoren', courtsCaseMediatorsPh: 'Oasis-IDs der Mediatoren, durch Komma getrennt', courtsMediatorsLabel: 'Mediatoren', courtsThCase: 'Fall', courtsThCreatedAt: 'Startdatum', - courtsThActions: 'Aktionen', courtsPublicPrefLabel: 'Sichtbarkeit nach Klärung', courtsPublicPrefYes: 'Ich stimme zu, dass dieser Fall vollständig öffentlich sein kann', courtsPublicPrefNo: 'Ich bevorzuge, die Details privat zu halten', @@ -1841,8 +1939,11 @@ module.exports = { courtsMethodMEDIATION: 'Mediation', courtsNoCases: 'Keine Fälle.', reportsTitle: "Berichte", - reportsDescription: "Berichte zu Problemen, Fehlern, Missbrauch und Inhaltswarnungen in deinem Netzwerk verwalten und verfolgen.", + reportsDescription: "Verwalte und verfolge die Meldungen in deinem Netzwerk.", + reportsSearchPlaceholder: "In Berichte suchen...", reportsFilterAll: "ALLE", + reportsFilterRecent: "NEU", + reportsFilterTop: "TOP", reportsFilterMine: "MEINE", reportsFilterFeatures: "FUNKTIONEN", reportsFilterBugs: "FEHLER", @@ -1855,18 +1956,13 @@ module.exports = { reportsFilterInvalid: "UNGÜLTIG", reportsCreateButton: "Bericht erstellen", reportsTitleLabel: "Titel", - reportsDescriptionLabel: "Beschreibung", - reportsDescriptionPlaceholder: "Bitte eine ausführliche Beschreibung angeben.", reportsCategory: "Kategorie", - reportsCategoryLabel: "Kategorie", reportsCategoryFeatures: "Funktionen", reportsCategoryBugs: "Fehler", reportsCategoryAbuse: "Missbrauch", - reportsCategoryContent: "Inhaltsprobleme", + reportsCategoryContent: "Inhalt", reportsUpdateButton: "Aktualisieren", reportsDeleteButton: "Löschen", - reportsDateLabel: "Datum", - reportsUploadFile: "Medien hochladen (max: 50MB)", reportsMineSectionTitle: "Deine Berichte", reportsFeaturesSectionTitle: "Funktionsanfragen", reportsBugsSectionTitle: "Fehler", @@ -1874,11 +1970,6 @@ module.exports = { reportsContentSectionTitle: "Inhaltsprobleme", reportsAllSectionTitle: "Berichte", reportsNoItems: "Keine Berichte vorhanden.", - reportsValidationTitle: "Bitte einen gültigen Titel eingeben.", - reportsValidationDescription: "Beschreibung darf nicht leer sein.", - reportsValidationCategory: "Bitte eine Kategorie auswählen.", - reportsCreatedAt: "Erstellt am", - reportsCreatedBy: "Von", reportsSeverity: "Schweregrad", reportsSeverityLow: "Niedrig", reportsSeverityMedium: "Mittel", @@ -1889,18 +1980,14 @@ module.exports = { reportsStatusUnderReview: "In Prüfung", reportsStatusResolved: "Gelöst", reportsStatusInvalid: "Ungültig", - reportsUpdateStatusButton: "Status aktualisieren", - reportsAnonymityOption: "Anonym einreichen", - reportsAnonymousAuthor: "Anonym", reportsConfirmButton: "BERICHT BESTÄTIGEN!", reportsConfirmations: "Bestätigungen", reportsConfirmedSectionTitle: "Bestätigte Berichte", - reportsCreateTaskButton: "AUFGABE ERSTELLEN", + reportsCreateTaskButton: "Aufgabe erstellen", reportsOpenSectionTitle: "Offene Berichte", reportsUnderReviewSectionTitle: "Berichte in Prüfung", reportsResolvedSectionTitle: "Gelöste Berichte", reportsInvalidSectionTitle: "Ungültige Berichte", - reportsTemplateSectionTitle: 'Berichtsvorlage', reportsBugTemplateTitle: 'Reproduktionsdetails (Fehler)', reportsFeatureTemplateTitle: 'Details (Funktionen)', reportsAbuseTemplateTitle: 'Details (Missbrauch)', @@ -1939,6 +2026,20 @@ module.exports = { reportsRequestedActionLabel: 'Gewünschte Maßnahme', reportsRequestedActionPlaceholder: 'Entfernen, verbergen, markieren, warnen, etc.', tribesTitle: "Stämme", + tribeFilterAll: "ALLE", + tribeFilterRecent: "NEUESTE", + tribeFilterMine: "MEINE", + tribeFilterMembership: "MITGLIEDSCHAFT", + tribeFilterSubtribes: "UNTERSTÄMME", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALERIE", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "MEINE", + tribeFeedFilterALL: "ALLE", + tribeFeedFilterRECENT: "NEUESTE", + courtsStanceDENY: "Bestreiten", + courtsStanceADMIT: "Einräumen", + courtsStancePARTIAL: "Teilweise einräumen", tribeAllSectionTitle: "Stämme", tribeMineSectionTitle: "Deine Stämme", tribeCreateSectionTitle: "Stamm erstellen", @@ -1946,24 +2047,15 @@ module.exports = { tribeGallerySectionTitle: "Stämme-Galerie", tribeRecentSectionTitle: "Neueste Stämme", tribeTopSectionTitle: "Beliebte Stämme", - tribeviewTribeButton: "Stamm besuchen", - tribeviewSubTribeButton: "Unter-Stamm besuchen", tribeRootLabel: "WURZEL", tribeDescription: "Stämme in deinem Netzwerk erkunden oder erstellen.", - tribeFilterAll: "ALLE", - tribeFilterMine: "MEINE", - tribeFilterMembership: "MITGLIEDSCHAFT", - tribeFilterRecent: "NEUESTE", - tribeFilterTop: "TOP", - tribeFilterSubtribes: "UNTER-STÄMME", - tribeFilterGallery: "GALERIE", tribeMainTribeLabel: "HAUPTSTAMM", tribeCreateButton: "Stamm erstellen", tribeUpdateButton: "Aktualisieren", tribeDeleteButton: "Löschen", - tribeImageLabel: "Medien hochladen (max: 50MB)", + tribeImageLabel: "Medien hochladen (max: 75MB)", tribeTitleLabel: "Titel", - searchTribesPlaceholder: "STÄMME NACH NAME FILTERN …", + searchTribesPlaceholder: "In Stämme suchen...", tribeTitlePlaceholder: "Name des Stammes", tribeDescriptionLabel: "Beschreibung", tribeDescriptionPlaceholder: "Diesen Stamm beschreiben", @@ -1975,13 +2067,17 @@ module.exports = { tribeModeLabel: "Einladungsmodus", tribeIsAnonymousLabel: "STATUS", tribeMembersCount: "Mitgliederzahl", - tribeInviteCodePlaceholder: "Einladungscode eingeben", - tribeJoinByCodeButton: "Mit Code beitreten", - tribeJoinButton: "BEITRETEN", tribeEnterInvite: "CODE EINGEBEN", + subscriptionTitle: "Abonnements", + subscriptionOn: "Abonniert", + subscriptionOff: "Nicht abonniert", + subscriptionSubscribe: "Abonnieren", + subscriptionUnsubscribe: "Abbestellen", + subscriptionSubscribers: "Abonnenten", + pmListLabel: "Senden an", + pmListNone: "— Keine Liste —", + pmListNoSubscribers: "Keine Abonnenten", tribeLeaveButton: "VERLASSEN", - tribeYes: "JA", - tribeNo: "NEIN", tribePublic: "ÖFFENTLICH", tribePrivate: "PRIVAT", tribeGenerateInvite: "EINZELNE EINLADUNG", @@ -1990,13 +2086,8 @@ module.exports = { tribeRemoveInvitation: "EINLADUNG ENTFERNEN", tribeCreatedAt: "Erstellt am", tribeAuthor: "Von", - tribeAuthorLabel: "AUTOR", tribeStrict: "Streng", tribeOpen: "Öffnen", - tribeFeedFilterRECENT: "NEUESTE", - tribeFeedFilterMINE: "MEINE", - tribeFeedFilterALL: "ALLE", - tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Weiterleitungen", tribeFeedRefeed: "Weiterleiten", tribeFeedMessagePlaceholder: "Einen Feed schreiben…", @@ -2006,33 +2097,28 @@ module.exports = { tribeNotFound: "Stamm nicht gefunden!", createTribeTitle: "Stamm erstellen", updateTribeTitle: "Stamm aktualisieren", - tribeSectionOverview: "Übersicht", tribeSectionInhabitants: "Bewohner", tribeSectionVotations: "ABSTIMMUNGEN", tribeSectionEvents: "TERMINE", - tribeSectionReports: "Berichte", tribeSectionTasks: "AUFGABEN", tribeSectionFeed: "FEED", tribeSectionForum: "FORUM", - tribeSectionMarket: "Markt", - tribeSectionJobs: "Stellen", - tribeSectionProjects: "Projekte", tribeSectionMedia: "Medien", tribeSectionImages: "BILDER", tribeSectionAudios: "AUDIOS", tribeSectionVideos: "VIDEOS", tribeSectionDocuments: "DOKUMENTE", tribeSectionBookmarks: "LESEZEICHEN", - tribeSectionMaps: "MAPS", + tribeSectionMaps: "KARTEN", tribeSectionPads: "PADS", tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionCalendars: "KALENDER", + tribePadCreate: "Pad erstellen", + tribeChatCreate: "Chat erstellen", + tribeCalendarCreate: "Kalender erstellen", + tribePadsEmpty: "Noch keine Pads.", + tribeChatsEmpty: "Noch keine Chats.", + tribeCalendarsEmpty: "Noch keine Kalender.", tribeInhabitantsEmpty: "Noch keine Bewohner in diesem Stamm.", tribeEventCreate: "Termin erstellen", tribeEventsEmpty: "Noch keine Termine.", @@ -2054,11 +2140,6 @@ module.exports = { tribeTaskStatusClosed: "SCHLIESSEN", tribeTaskAssign: "ZUWEISEN", tribeTaskUnassign: "ENTZIEHEN", - tribeReportCreate: "Bericht erstellen", - tribeReportsEmpty: "Noch keine Berichte.", - tribeReportTitle: "Titel", - tribeReportDescription: "Beschreibung", - tribeReportCategory: "Kategorie", tribeVotationCreate: "Abstimmung erstellen", tribeVotationsEmpty: "Noch keine Abstimmungen.", tribeVotationTitle: "Titel", @@ -2076,27 +2157,6 @@ module.exports = { tribeForumCategory: "Kategorie", tribeForumReply: "Antworten", tribeForumReplies: "Antworten", - tribeMarketCreate: "Angebot erstellen", - tribeMarketEmpty: "Noch keine Angebote.", - tribeMarketTitle: "Titel", - tribeMarketDescription: "Beschreibung", - tribeMarketPrice: "Preis", - tribeMarketImage: "Bild", - tribeMarketCategory: "Kategorie", - tribeJobCreate: "Stelle erstellen", - tribeJobsEmpty: "Noch keine Stellen.", - tribeJobTitle: "Titel", - tribeJobDescription: "Beschreibung", - tribeJobLocation: "Ort", - tribeJobSalary: "Gehalt", - tribeJobDeadline: "Frist", - tribeProjectCreate: "Projekt erstellen", - tribeProjectsEmpty: "Noch keine Projekte.", - tribeProjectTitle: "Titel", - tribeProjectDescription: "Beschreibung", - tribeProjectGoal: "Ziel", - tribeProjectFunded: "Finanziert", - tribeProjectDeadline: "Frist", tribeMediaUpload: "Medien hochladen", readDocument: "Dokument lesen", tribeCreateImage: "Bild erstellen", @@ -2107,21 +2167,20 @@ module.exports = { tribeMediaEmpty: "Noch keine Medien.", tribeMediaTitle: "Titel", tribeMediaDescription: "Beschreibung", - tribeMediaType: "Typ", tribeMediaTypeImage: "Bild", tribeMediaTypeVideo: "Video", tribeMediaTypeAudio: "Audio", tribeMediaTypeDocument: "Dokument", tribeMediaTypeBookmark: "Lesezeichen", tribeContentDelete: "LÖSCHEN", + tribeReachTribe: "STAMM", + tribeReachNotice: "OASIS zeigt es allen Bewohnern und CLEARNET zusätzlich im offenen Web. Das Öffnen hinterlässt eine lesbare Kopie außerhalb des Stammes: Zurück zu STAMM blendet es aus, aber bereits geteilte Kopien lassen sich nicht löschen.", + tribeReachPrivate: "Inhalte eines privaten Stammes können nur im Stamm bleiben.", + tribeReachOpenedBy: "Geöffnet von", + tribeReachChange: "Wo es veröffentlicht wird", tribeInviteCodeText: "Einladungscode: ", oasisVersionLabel: "Oasis-Version", - tribeInviteCodeHint: "Teile diesen Code mit jemandem, den du einladen möchtest. Er kann über /invites → Tribes beitreten.", - tribeGroupTribe: "Stamm", - tribeGroupOffice: "Büro", - tribeGroupNetwork: "Netzwerk", - tribeGroupEconomy: "Wirtschaft", - tribeGroupMedia: "Medien", + tribeInviteCodeHint: "Teile diesen Code mit jemandem, den du einladen möchtest. Er kann über /invites beitreten.", tribeStatusOpen: "OFFEN", tribeStatusClosed: "GESCHLOSSEN", tribeStatusInProgress: "IN BEARBEITUNG", @@ -2131,33 +2190,17 @@ module.exports = { tribePriorityCritical: "KRITISCH", tribeTaskFilterAll: "ALLE", tribeMediaFilterAll: "ALLE", - tribeReportCatBug: "BUG", - tribeReportCatAbuse: "MISSBRAUCH", - tribeReportCatContent: "INHALT", - tribeReportCatOther: "SONSTIGES", tribeForumCatGeneral: "ALLGEMEIN", tribeForumCatProposal: "VORSCHLAG", tribeForumCatQuestion: "FRAGE", tribeForumCatAnnouncement: "ANKÜNDIGUNG", - tribeMarketCatGoods: "WAREN", - tribeMarketCatServices: "DIENSTE", - tribeMarketCatFood: "LEBENSMITTEL", - tribeMarketCatOther: "SONSTIGES", tribeStatusLabel: "Status", tribeSubTribes: "UNTER-STÄMME", tribeSubTribesCreate: "Unter-Stamm Erstellen", tribeSubTribesStrictDenied: "Der strikte Modus des Stammes erlaubt Ihnen nicht, neue Unter-Stämme zu erstellen. Bitte wenden Sie sich an den Administrator.", - tribeSubTribesEmpty: "Noch keine Unter-Stämme erstellt.", - tribeActivityJoined: "BEIGETRETEN", - tribeActivityLeft: "VERLASSEN", - tribeActivityFeed: "FEED", tribeActivityRefeed: "WEITERLEITUNG", - tribeGroupAnalytics: "Analysen", - tribeGroupCreative: "Kreativ", tribeSectionActivity: "AKTIVITÄT", tribeSectionTrending: "TRENDS", - tribeSectionOpinions: "MEINUNGEN", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAGS", tribeSectionSearch: "SUCHE", tribeActivityEmpty: "Noch keine Aktivität.", @@ -2169,25 +2212,13 @@ module.exports = { tribeTrendingPeriodWeek: "Diese Woche", tribeTrendingPeriodAll: "Gesamte Zeit", tribeTrendingEngagement: "Beteiligung", - tribeOpinionsEmpty: "Noch keine Meinungen.", - tribeOpinionsCast: "Abstimmen", - tribeOpinionsRankings: "Ranglisten", - tribeOpinionsAlreadyVoted: "Bereits abgestimmt", tribeTopCategory: "Meistgewählt", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "Gemeinschaftliche Pixel-Art-Leinwand.", - tribePixeliaPaint: "Malen", - tribePixeliaContributors: "Mitwirkende", - tribePixeliaTotalPixels: "gemalte Pixel", tribeTagsEmpty: "Keine Tags gefunden.", - tribeTagsCloud: "Tag-Wolke", - tribeTagsContentWith: "Inhalt mit Tag", - tribeSearchPlaceholder: "Stamminhalt durchsuchen...", tribeSearchEmpty: "Keine Ergebnisse gefunden.", - tribeSearchResults: "Ergebnisse", tribeSearchMinChars: "Mindestens 2 Zeichen eingeben, um zu suchen.", agendaTitle: "Agenda", agendaDescription: "Hier findest du alle dir zugewiesenen Einträge.", + agendaSearchPlaceholder: "In Agenda suchen...", agendaFilterAll: "ALLE", agendaFilterToday: "HEUTE", agendaFilterUpcoming: "BEVORSTEHEND", @@ -2201,23 +2232,15 @@ module.exports = { agendaFilterReports: "BERICHTE", agendaFilterTransfers: "ÜBERWEISUNGEN", agendaFilterJobs: "STELLEN", + agendaFilterIndustry: "INDUSTRIE", + agendaFilterHousing: "WOHNEN", agendaFilterProjects: "PROJEKTE", agendaFilterCalendars: "KALENDER", agendaNoItems: "Keine Zuweisungen gefunden.", - agendaAuthor: "Von", agendaDiscardButton: "Verwerfen", agendaRestoreButton: "Wiederherstellen", - agendaCreatedAt: "Erstellt am", - agendaTitleLabel: "Titel", + agendaRemoveButton: "Entfernen", agendaMembersCount: "Mitglieder", - agendaDescriptionLabel: "Beschreibung", - agendaStatus: "Status", - agendaVisibility: "Sichtbarkeit", - agendaEventDate: "Termindatum", - agendaEventLocation: "Ort", - agendaEventPrice: "Preis", - agendaEventUrl: "URL", - agendaTaskStart: "Startzeit", agendaLocationLabel: "Ort", agendaYes: "JA", agendaNo: "NEIN", @@ -2227,11 +2250,6 @@ module.exports = { agendareportCategory: "Kategorie", agendareportSeverity: "Schweregrad", agendareportStatus: "Status", - agendareportDescription: "Beschreibung", - agendaTaskEnd: "Endzeit", - agendaTaskPriority: "Priorität", - agendaTransferFrom: "Von", - agendaTransferTo: "An", agendaTransferConcept: "Konzept", agendaTransferAmount: "Betrag", agendaTransferDeadline: "Frist", @@ -2241,47 +2259,7 @@ module.exports = { voteNow: "Jetzt abstimmen", alreadyVoted: "Du hast bereits abgestimmt.", noOpinionsFound: "Keine Meinungen gefunden.", - RECENTButton: "NEUESTE", TOPButton: "TOP", - interestingButton: "INTERESSANT", - necessaryButton: "NOTWENDIG", - funnyButton: "LUSTIG", - disgustingButton: "WIDERLICH", - sensibleButton: "SENSIBEL", - propagandaButton: "PROPAGANDA", - adultOnlyButton: "NUR FÜR ERWACHSENE", - boringButton: "LANGWEILIG", - confusingButton: "VERWIRREND", - usefulButton: "NÜTZLICH", - informativeButton: "INFORMATIV", - wellResearchedButton: "GUT RECHERCHIERT", - accurateButton: "ZUTREFFEND", - needsSourcesButton: "QUELLEN NÖTIG", - wrongButton: "FALSCH", - lowQualityButton: "NIEDRIGE QUALITÄT", - creativeButton: "KREATIV", - insightfulButton: "AUFSCHLUSSREICH", - actionableButton: "UMSETZBAR", - inspiringButton: "INSPIRIEREND", - loveButton: "LIEBE", - clearButton: "KLAR", - upliftingButton: "ERHEBEND", - unnecessaryButton: "UNNÖTIG", - rejectedButton: "ABGELEHNT", - misleadingButton: "IRREFÜHREND", - offTopicButton: "THEMENFREMD", - duplicateButton: "DUPLIKAT", - clickbaitButton: "CLICKBAIT", - spamButton: "SPAM", - trollButton: "TROLL", - nsfwButton: "NSFW", - violentButton: "GEWALT", - toxicButton: "TOXISCH", - harassmentButton: "BELÄSTIGUNG", - hateButton: "HASS", - scamButton: "BETRUG", - triggeringButton: "TRIGGERND", - opinionsCreatedAt: "Erstellt am", opinionsTotalCount: "Meinungen gesamt", voteInteresting: "Interessant", voteNecessary: "Notwendig", @@ -2318,29 +2296,41 @@ module.exports = { voteCreative: "Kreativ", voteSpam: "Spam", voteAdultOnly: "Nur für Erwachsene", - publishBlog: "Blog veröffentlichen", privateMessage: "PN", pmSendTitle: "Private Nachrichten", pmSend: "Senden!", - pmDescription: "Verwende dieses Formular, um eine verschlüsselte Nachricht an andere Bewohner zu senden.", + pmCancel: "Abbrechen", + pmReset: "Zurücksetzen", + pmSharedKeyHint: "Teile diesen Schlüssel mit dem Empfänger über einen anderen Kanal. Er wird zum Entschlüsseln benötigt.", + pmDescription: "Verwende dieses Formular, um eine verschlüsselte Nachricht/Datei an andere Bewohner zu senden.", + pmComposeTitle: "Nachricht senden", pmRecipients: "Empfänger", pmRecipientsHint: "Oasis-IDs durch Kommas getrennt eingeben", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "Bis zu 7 Bewohner pro Nachricht.", + pmTextPlaceholder: "Schreibe deine Nachricht.", pmSubject: "Betreff", pmSubjectHint: "Nachrichtenbetreff eingeben", pmText: "Nachricht", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Entschlüsseln", + fileShareTitle: "Datei teilen", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "Datei", + fileShareDownload: "Herunterladen", + fileShareMutualError: "Du kannst Dateien nur mit Bewohnern mit gegenseitiger Unterstützung teilen.", + fileShareNoFile: "Keine Datei ausgewählt.", + fileShareTooLarge: "Die Datei überschreitet die zulässige Größe.", + fileShareFailed: "Die Datei konnte nicht vorbereitet werden.", + fileShareSendError: "Die Datei konnte nicht gesendet werden.", + fileShareUnavailable: "Diese Datei ist gerade nicht verfügbar. Versuche es später erneut.", pmCrypterBadKey: "Der geteilte Schlüssel ist falsch!", pmCrypterTooLong: "Die Nachricht ist zu lang, um mit dem Crypter verschlüsselt zu werden. Bitte kürze sie und versuche es erneut.", pmCrypterCipherLabel: "Erneut verschlüsselte Nachricht", pmCrypterCharsLabel: "Zeichen", pmInvalidRecipients: "Ungültige Oasis-ID (sollte wie @…=.ed25519 aussehen).", pmEncryptionLabel: "Verschlüsselung", - pmFile: "Anhang", - private: "Privat", - privateDescription: "Deine verschlüsselten Nachrichten.", + private: "Posteingang", + privateDescription: "Verwalte deine privaten Nachrichten.", privateInbox: "POSTEINGANG", privateSent: "GESENDET", privateDelete: "Löschen", @@ -2348,27 +2338,34 @@ module.exports = { noPrivateMessages: "Keine privaten Nachrichten.", pmFromLabel: "Von:", pmToLabel: "An:", - pmInvalidMessage: "Ungültige Nachricht", pmNoSubject: "(kein Betreff)", pmSubjectLabel: "Betreff:", - pmBodyLabel: "Inhalt", - pmBotJobs: "42-StellenBOT", - pmBotProjects: "42-ProjekteBOT", - pmBotMarket: "42-MarktBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "Das regierende Haus des L.A.R.P hat gewechselt.", + politicalBotParliamentTitle: "Im Parlament hat ein neuer Regierungszyklus begonnen.", + politicalBotTribeTitle: "In einem deiner Stämme hat ein neuer Regierungszyklus begonnen.", + politicalBotLarpIntro: "Jetzt regiert {house} in diesem Zyklus: {cycle}", + politicalBotParliamentIntro: "Die aktuelle Regierung ist {gov}", + politicalBotParliamentIntroLeader: "Die aktuelle Regierung ist {gov}, ausgeübt von {leader}", + politicalBotTribeIntro: "Die aktuelle Regierung im Stamm {tribe} ist {gov}", + politicalBotTribeIntroLeader: "Die aktuelle Regierung im Stamm {tribe} ist {gov}, ausgeübt von {leader}", + politicalBotLeaderLabel: "Anführer", inboxJobSubscribedTitle: "Neues Abonnement deines Stellenangebots", pmInhabitantWithId: "Bewohner mit OASIS-ID:", pmHasSubscribedToYourJobOffer: "hat dein Stellenangebot abonniert", - inboxProjectCreatedTitle: "Neues Projekt erstellt", - pmHasCreatedAProject: "hat ein Projekt erstellt", inboxMarketItemSoldTitle: "Artikel verkauft", + inboxShopSoldTitle: "Produkt verkauft", pmYourItem: "Dein Artikel", pmHasBeenSoldTo: "wurde verkauft an", pmFor: "für", inboxProjectPledgedTitle: "Neue Zusage für dein Projekt", pmHasPledged: "hat zugesagt", pmToYourProject: "für dein Projekt", - blockchain: 'BlockExplorer', - blockchainTitle: 'BlockExplorer', + blockchain: "Block-Explorer", blockchainDescription: 'Blöcke in der Blockchain erkunden und visualisieren.', blockchainNoBlocks: 'Keine Blöcke in der Blockchain gefunden.', blockchainStatsTitle: 'Statistiken', @@ -2380,42 +2377,36 @@ module.exports = { blockchainBlockCarbon: 'CO₂-Fußabdruck', blockchainBlockType: 'Typ', blockchainBlockTimestamp: 'Zeitstempel', - blockchainBlockContent: 'Block', - blockchainBlockURL: 'URL:', - blockchainContent: 'Block', - blockchainContentPreview: 'Vorschau des Blockinhalts', blockchainLatestDatagram: 'Letztes Datagramm', - blockchainDatagram: 'Datagramm', - blockchainDetails: 'Blockdetails anzeigen', + blockchainDatagram: "Datagramm", + blockchainDetails: "Blockdetails ansehen", blockchainViewBlockexplorer: "Im Blockexplorer ansehen", - blockchainBlockInfo: 'Blockinformationen', - blockchainBlockDetails: 'Details des ausgewählten Blocks', blockchainBack: 'Zurück zum BlockExplorer', blockchainContentDeleted: "Dieser Inhalt wurde gelöscht (Tombstone)", visitContent: "Inhalt besuchen", + favoriteAdd: "Zu Favoriten anheften", + pmContentTooltip: "Private Nachricht senden", + phoneCallTooltip: "Anruf tätigen", + favoriteRemove: "Von Favoriten lösen", + reportContent: "Inhalt melden", banking: 'Bankwesen', bankingTitle: 'Bankwesen', - bankingDescription: 'Den aktuellen Wert von ECOin und die entsprechende UBI-Zuweisung erkunden, die pro Epoche basierend auf Teilnahme und Vertrauen verteilt wird.', + bankingDescription: "Die kollektive ECOin-Wirtschaft.", bankOverview: 'Übersicht', bankEpochs: 'Epochen', bankRules: 'Regeln', pending: 'Ausstehend', closed: 'Geschlossen', - bankBack: 'Zurück zum Bankwesen', - bankViewTx: 'Tx anzeigen', bankClaimNow: 'Jetzt beanspruchen', - bankClaimUBI: 'UBI beanspruchen!', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'Keine ausstehenden UBI-Zuweisungen für diese Epoche.', + bankClaimUBI: 'UBI beanspruchen', + bankClaimAndPay: "Beanspruchen & zahlen", + bankClaimedPending: "Anspruch ausstehend...", + bankStatusClaimed: "Beansprucht", + bankStatusExpired: "Abgelaufen", + bankPubOnly: "Nur für PUBs", bankEpoch: 'Epoche', bankPool: 'Pool (diese Epoche)', bankWeightsSum: 'Gewichtssumme', - bankAllocations: 'Zuweisungen', bankNoAllocations: 'Keine Zuweisungen gefunden.', bankNoEpochs: 'Keine Epochen gefunden.', bankEpochAllocations: 'Epochen-Zuweisungen', @@ -2429,16 +2420,21 @@ module.exports = { bankEpochId: 'Epochen-ID', bankRuleHash: 'Regelversions-Hash', bankViewEpoch: 'Epoche anzeigen', + bankYourIndustryBalance: "Dein Industrieanteil", + bankYourUbiMonth: "Dein BGE (diesen Monat)", + bankYourFundsMonth: "Mittel gesamt", + bankIndustryBalance: "Industrieproduktion", bankUserBalance: 'Dein Kontostand', - ecoWalletNotConfigured: 'ECOin Wallet nicht konfiguriert', - editWallet: 'Geldbörse bearbeiten', - addWallet: 'Geldbörse hinzufügen', bankAddresses: 'Adressen', bankNoAddresses: 'Keine Adressen gefunden.', bankUser: 'Oasis-ID', bankAddress: 'Adresse', bankAddAddressTitle: 'ECOIN-Adresse hinzufügen', bankAddAddressUser: 'Oasis-ID', + bankAddAddressLabel: "Bezeichnung (optional)", + bankAddAddressLabelPlaceholder: "Bobs Wallet, Alices Laden…", + bankAddAddressUserOptional: "Oasis-ID (optional)", + bankAddressUnnamed: "Adresse ohne Namen", bankAddAddressAddress: 'ECOIN-Adresse', bankAddAddressSave: 'Speichern', bankAddressAdded: 'Adresse hinzugefügt', @@ -2456,19 +2452,11 @@ module.exports = { bankAddressDeleteConfirm: 'Diese Adresse löschen?', search: 'Suchen!', bankLocal: 'Lokal', - bankFromOasis: 'Oasis', - bankMyAddress: 'Deine Adresse', - bankRemoveMyAddress: 'Meine Adresse entfernen', - bankNotRemovableOasis: 'Adressen können nicht lokal entfernt werden', - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "Netz", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "KEIN GUTHABEN!", bankUbiAvailableOk: "VERFÜGBAR!", - bankUbiAvailability: "UBI-Verfügbarkeit", + bankUbiAvailability: "UBI", bankAlreadyClaimedThisMonth: "Diesen Monat bereits beansprucht", bankUbiThisMonth: "UBI (diesen Monat)", bankUbiLastClaimed: "UBI (zuletzt beansprucht)", @@ -2477,18 +2465,11 @@ module.exports = { bankUbiPub: "PUB", bankUbiInhabitant: "BEWOHNER", bankUbiClaimedAmount: "BEANSPRUCHT (ECO)", - typeBankUbiResult: "BANKING - UBI", + typeBankUbiResult: "BANKWESEN - UBI", bankNoPubConfigured: "Kein PUB konfiguriert. Lege deine PUB-ID in den Einstellungen fest.", shopsTitle: "Shops", shopDescription: "Shops im Netzwerk entdecken und verwalten.", shopTitle: "Shop", - shopFilterAll: "ALLE", - shopFilterMine: "MEINE", - shopFilterRecent: "NEUESTE", - shopFilterTop: "TOP", - shopFilterProducts: "PRODUKTE", - shopFilterPrices: "PREISE", - shopFilterFavorites: "FAVORITEN", shopUpload: "Shop erstellen", shopAllSectionTitle: "Alle Shops", shopMineSectionTitle: "Meine Shops", @@ -2505,19 +2486,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Im Clearnet veröffentlichen", - shopClearnetUnpublish: "Aus Clearnet entfernen", - shopClearnetUrlLabel: "Clearnet-URL", - shopClearnetWarning: "Die Veröffentlichung im Clearnet macht diesen Shop für externe Suchmaschinen und Crawler indexierbar.", + transportChipLabel: "Verschlüsselt übertragen", shopAddFavorite: "Favorit hinzufügen", shopRemoveFavorite: "Favorit entfernen", shopOpen: "OFFEN", shopClosed: "GESCHLOSSEN", - shopOpenShop: "Shop öffnen", - shopCloseShop: "Shop schließen", shopMakePrivate: "PRIVAT MACHEN (E2E)", shopMakePublic: "ÖFFENTLICH MACHEN", shopProducts: "Produkte", + shopFilterAll: "ALLE", + shopFilterRecent: "NEU", + shopFilterMine: "MEINE", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUKTE", + shopFilterPrices: "PREISE", + shopFilterFavorites: "FAVORITEN", shopProductAdd: "Produkt hinzufügen", shopProductTitle: "Produkt", shopProductUpdate: "Produkt aktualisieren", @@ -2542,85 +2525,63 @@ module.exports = { shopOrderPrice: "Preis", shopOrderBuyer: "Käufer", shopBackToShop: "Zurück zum Shop", - shopShareUrl: "URL teilen", - shopVisitShop: "SHOP BESUCHEN", marketVisitItem: "ARTIKEL ANZEIGEN", shopStatus: "STATUS", shopCreatedAt: "ERSTELLT", - shopSearchPlaceholder: "Shops suchen...", + shopSearchPlaceholder: "In Shops suchen...", shopUrl: "URL", shopLocation: "Standort", shopTags: "Tags", shopVisibility: "Sichtbarkeit", - shopImage: "Bild", shopShortDescription: "Kurzbeschreibung", shopShortDescriptionPlaceholder: "Kurzbeschreibung fuer Shopkarten (max. 160 Zeichen)", shopTitlePlaceholder: "Name Ihres Shops", shopDescriptionPlaceholder: "Detaillierte Beschreibung Ihres Shops", - shopUrlPlaceholder: "https://ihre-shop-url.com", shopLocationPlaceholder: "Stadt, Land", - shopTagsPlaceholder: "tag1, tag2, tag3", + shopTagsPlaceholder: "Tags durch Kommas getrennt eingeben", mapTitlePlaceholder: "Kartentitel", shopProductFeatured: "Hervorgehobenes Produkt", - shopSendToMarket: "Send to Market", + shopSendToMarket: "An den Markt senden", typeShop: "SHOPS", typeShopProduct: "SHOP-PRODUKT", bankExchange: 'Umtausch', bankExchangeCurrentValue: 'ECOin-Wert (1h)', - bankTotalSupply: 'ECOin Gesamtmenge', - bankEcoinHours: "ECOin-Äquivalent in Zeit", + bankTotalSupply: "ECOin-Gesamtbestand", + bankPubsSupply: "ECOin-Bestand im PUB-Pool", + bankHoldingSupply: "ECOin-Bestand außerhalb des BGE", + bankEcoinHours: "ECOin-Äquivalent in Zeit (1 ECO)", bankHoursOfWork: 'Stunden', bankUnitMs: "ms", bankUnitSeconds: "Sekunden", bankUnitMinutes: "Minuten", bankUnitDays: "Tage", bankExchangeNoData: 'Keine Daten verfügbar', - bankExchangeIndex: 'ECOin-Wert (1h)', - bankInflation: 'ECOin-Inflation (anual)', - bankInflationMonthly: 'ECOin-Inflation (mensual)', + bankInflation: "ECOin-Inflation (jährlich)", + bankInflationMonthly: "ECOin-Inflation (monatlich)", bankExchangeChartTitle: 'Progression des ECOin-Werts im Zeitverlauf', bankExchangeChartValue: 'Wert (ECO/h)', bankExchangeChartSupply: 'Umlauf', bankExchangeChartInflation: 'Inflation %', bankExchangeChartEmpty: 'Noch nicht genug Datenpunkte — komm später zurück', - bankCurrentSupply: 'ECOin aktuelle Menge', + bankCurrentSupply: "Aktueller ECOin-Bestand", bankingSyncStatus: 'ECOin-Status', bankingSyncStatusSynced: 'Synchronisiert', - bankingSyncStatusOutdated: 'Veraltet', + bankingSyncStatusOutdated: "Nicht synchron", statsTitle: 'Statistiken', statistics: "Statistiken", - statsInhabitant: "Bewohner-Statistiken", statsDescription: "Entdecke Statistiken über dein Netzwerk.", ALLButton: "ALLE", + RECENTButton: "NEUESTE", MINEButton: "MEINE", - TOMBSTONEButton: "TOMBSTONES", - statsYou: "Du", - statsUserId: "Oasis-ID", + TOMBSTONEButton: "GELÖSCHT", statsCreatedAt: "Erstellt am", statsYourContent: "Inhalt", statsYourOpinions: "Meinungen", - statsYourTombstone: "Tombstones", - statsNetwork: "Netzwerk", - statsTotalInhabitants: "Bewohner", - statsDiscoveredTribes: "Stämme (Öffentlich)", - statsPrivateDiscoveredTribes: "Stämme (Privat)", statsNetworkContent: "Inhalt", - statsYourMarket: "Markt", - statsYourJob: "Stellen", - statsYourProject: "Projekte", - statsYourTransfer: "Überweisungen", - statsYourForum: "Foren", statsNetworkOpinions: "Meinungen", - statsDiscoveredMarket: "Markt", - statsDiscoveredJob: "Stellen", - statsDiscoveredProject: "Projekte", - statsBankingTitle: "Bankwesen", - statsEcoWalletLabel: "ECOIN Wallet", + statsEcoWalletLabel: "ECOin Wallet", + walletAddressCopyHint: "Klicken zum Markieren, dann kopieren", statsEcoWalletNotConfigured: "Nicht konfiguriert!", - statsTotalEcoAddresses: "Adressen gesamt", - statsDiscoveredTransfer: "Überweisungen", - statsDiscoveredForum: "Foren", - statsNetworkTombstone: "Tombstones", statsBookmark: "Lesezeichen", statsEvent: "Termine", statsTask: "Aufgaben", @@ -2641,17 +2602,15 @@ module.exports = { statsTransfer: "Überweisungen", statsAiExchange: "KI", statsPUBs: 'PUBs', + statsSyncedPeers: "Verbindungen", + statsTotalLabel: "Gesamt", statsPost: "Beiträge", - statsOasisID: "Oasis-ID", statsSize: "Gesamt (Größe)", statsBlockchainSize: "Blockchain (Größe)", statsBlobsSize: "Blobs (Größe)", statsActivity7d: "Aktivität (letzte 7 Tage)", statsActivity7dTotal: "7-Tage-Gesamt", statsActivity30dTotal: "30-Tage-Gesamt", - statsKarmaScore: "KARMA-Punktzahl", - statsPublic: "Öffentlich", - statsPrivate: "Privat", day: "TAG", messages: "Nachrichten", statsProject: "Projekte", @@ -2663,30 +2622,14 @@ module.exports = { statsProjectsCancelled: "Abgebrochen", statsProjectsGoalTotal: "Ziel gesamt", statsProjectsPledgedTotal: "Zugesagt gesamt", - statsProjectsSuccessRate: "Erfolgsquote", - statsProjectsAvgProgress: "Durchschnittlicher Fortschritt", - statsProjectsMedianProgress: "Medianer Fortschritt", - statsProjectsActiveFundingAvg: "Durchschn. aktive Finanzierung", - statsJobsTitle: "Stellen", - statsJobsTotal: "Stellen gesamt", - statsJobsOpen: "Öffnen", - statsJobsClosed: "Geschlossen", - statsJobsOpenVacants: "Offene Stellen", - statsJobsSubscribersTotal: "Abonnenten gesamt", - statsJobsAvgSalary: "Durchschnittsgehalt", - statsJobsMedianSalary: "Mediangehalt", statsMarketTitle: "Markt", statsMarketTotal: "Artikel gesamt", statsMarketForSale: "Zum Verkauf", statsMarketReserved: "Reserviert", statsMarketClosed: "Geschlossen", statsMarketSold: "Verkauft", - statsMarketRevenue: "Umsatz", - statsMarketAvgSoldPrice: "Durchschn. Verkaufspreis", statsUsersTitle: "Bewohner", user: "Bewohner", - statsTombstoneTitle: "Tombstones", - statsNetworkTombstones: "Netzwerk-Tombstones", statsTombstoneRatio: "Tombstone-Anteil (%)", statsParliamentCandidature: "Parlamentskandidaturen", statsParliamentTerm: "Parlamentsperioden", @@ -2712,31 +2655,28 @@ module.exports = { aiPrompt: "Gib eine informative und präzise Antwort.", aiConfiguration: "Prompt setzen", aiPromptUsed: "Prompt", - aiClearHistory: "Chatverlauf löschen", - aiSharePrompt: "Diese Antwort zum kollektiven Training hinzufügen?", - aiShareYes: "Ja", - aiShareNo: "Nein", - aiSharedLabel: "Zum Training hinzugefügt", - aiRejectedLabel: "Nicht zum Training hinzugefügt", - aiServerError: "Die KI konnte nicht antworten. Bitte versuche es erneut.", + aiClearHistory: "Verlauf löschen", + aiServerError: "Ich kann nicht richtig antworten. Bitte versuch es noch einmal.", + aiStatusLoading: "42 lädt das Modell. Frag gleich noch einmal.", + aiThinking: "Denkt nach", + aiWaiting: "Wartet", + aiStatusMissingModel: "Das KI-Modell ist nicht installiert: src/AI/oasis-42-1-chat.Q4_K_M.gguf fehlt.", + aiExportFineTuning: "Fine-Tuning exportieren", + aiFactsTitle: "Daten aus deinem Netzwerk", aiInputPlaceholder: "Was gibt es Neues?", typeAiExchange: "KI", aiApproveTrain: "Zum kollektiven Training hinzufügen", aiRejectTrain: "Nicht trainieren", - aiTrainPending: "Genehmigung ausstehend", aiTrainApproved: "Für Training genehmigt", aiTrainRejected: "Für Training abgelehnt", aiSnippetsUsed: "Verwendete Fragmente", aiSnippetsLearned: "Erlernte Fragmente", statsAITraining: "KI-Training", - aiApproveCustomTrain: "Mit dieser benutzerdefinierten Antwort trainieren", aiCustomAnswerPlaceholder: "Schreibe deine benutzerdefinierte Antwort…", - statsAIExchanges: "Austausche", marketMineSectionTitle: "Deine Artikel", marketCreateSectionTitle: "Artikel erstellen", marketUpdateSectionTitle: "Aktualisieren", marketAllSectionTitle: "Markt", - marketRecentSectionTitle: "Neueste Marktartikel", marketTitle: "Markt", marketDescription: "Ein Marktplatz zum Austausch von Waren oder Dienstleistungen in deinem Netzwerk.", marketFilterAll: "ALLE", @@ -2751,7 +2691,7 @@ module.exports = { marketFilterDiscarded: "VERWORFEN", marketFilterRecent: "NEUESTE", marketFilterMyBids: "GEBOTE", - marketCreateButton: "Artikel erstellen", + marketCreateButton: "Etwas verkaufen", marketItemType: "Typ", marketItemTitle: "Titel", marketItemAvailable: "Frist", @@ -2766,14 +2706,11 @@ module.exports = { marketItemDeadline: "Frist", marketItemIncludesShipping: "Versand inklusive?", marketItemHighestBid: "Höchstgebot", - marketItemHighestBidder: "Höchstbietender", marketItemStock: "Bestand", marketOutOfStock: "Nicht vorrätig", - marketItemBidTime: "Gebotszeit", marketActionsUpdate: "Aktualisieren", marketUpdateButton: "Aktualisieren", marketActionsDelete: "Löschen", - marketActionsSold: "Als verkauft markieren", marketActionsChangeStatus: "STATUS ÄNDERN", marketActionsBuy: "KAUFEN!", marketAuctionBids: "Aktuelle Gebote", @@ -2781,20 +2718,82 @@ module.exports = { marketItemSeller: "Verkäufer", marketNoItems: "Noch keine Artikel vorhanden.", marketYourBid: "Dein Gebot", - marketCreateFormImageLabel: "Medien hochladen (max: 50MB)", - marketSearchLabel: "Suche", - marketSearchPlaceholder: "Titel oder Tags suchen", + marketCreateFormImageLabel: "Medien hochladen (max: 75MB)", + marketSearchPlaceholder: "In Markt suchen...", marketMinPriceLabel: "Mindestpreis", marketMaxPriceLabel: "Höchstpreis", - marketSortLabel: "Sortieren nach", marketSortRecent: "Neueste zuerst", marketSortPrice: "Preis", marketSortDeadline: "Frist", marketSearchButton: "Suche", marketAuctionEndsIn: "Endet", marketAuctionEnded: "Beendet", - marketMyBidBadge: "Du hast geboten", marketNoItemsMatch: "Keine Artikel gefunden.", + housingTitle: "Wohnen", + housingDescriptionText: "Entdecke und verwalte Orte in deinem Netzwerk.", + modulesHousingLabel: "Wohnen", + modulesHousingDescription: "Modul zum Entdecken und Verwalten von Orten.", + typeHousing: "WOHNEN", + housingFilterAll: "ALLE", + housingFilterMine: "MEINE", + housingFilterRequested: "ANGEFRAGT", + housingFilterSale: "VERKAUF", + housingFilterRent: "MIETE", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "VERFÜGBAR", + housingFilterClosed: "NICHT VERFÜGBAR", + housingFilterRecent: "NEU", + housingFilterTop: "TOP", + housingType: "Angebot", + housingTypeSALE: "Zu verkaufen", + housingTypeRENT: "Zu vermieten", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Objekt", + housingPropertyAPARTMENT: "Wohnung", + housingPropertyHOUSE: "Haus", + housingPropertyROOM: "Zimmer", + housingPropertyLAND: "Grundstück", + housingPropertyOTHER: "Andere", + housingTitleLabel: "Titel", + housingTitlePlaceholder: "Name des Ortes", + housingDescription: "Beschreibung", + housingDescriptionPlaceholder: "Beschreibe den Ort, die Gegend, was enthalten ist: Bäder, Garten, Betten…", + housingRules: "Regeln", + housingRulesPlaceholder: "Haustiere, Rauchen, Ruhezeiten, Mindestaufenthalt…", + housingPlace: "Ort", + housingPlacePlaceholder: "Land, Stadt, Adresse", + housingPrice: "Preis", + housingPriceLabel: "Preis (ECO)", + housingSizeLabel: "Fläche (m²)", + housingFree: "GRATIS", + housingPerMonth: "/Monat", + housingRooms: "Zimmer", + housingSize: "Fläche", + housingCapacity: "Gäste", + housingAvailableFrom: "Verfügbar ab", + housingAvailableTo: "Verfügbar bis", + housingTags: "Schlagwörter", + spreadEditWarning: "Dieser Inhalt verliert seine Spreads beim Bearbeiten", + housingRequests: "Anfragen", + housingRequestButton: "Anfragen", + housingCancelButton: "Anfrage zurückziehen", + housingRequestedBadge: "ANGEFRAGT", + housingStatus: "Status", + housingStatusOPEN: "VERFÜGBAR", + housingStatusCLOSED: "NICHT VERFÜGBAR", + housingSetOpen: "Verfügbar machen", + housingSetClosed: "Nicht verfügbar machen", + housingCreateButton: "Ort erstellen", + housingUpdateButton: "Aktualisieren", + housingSearchPlaceholder: "In Wohnen suchen...", + housingSearchButton: "Suchen", + housingMinPrice: "Min. Preis", + housingMaxPrice: "Max. Preis", + housingSortRecent: "Neueste", + housingSortPrice: "Niedrigster Preis", + housingSortRequests: "Meist angefragt", + housingSortRating: "Bestbewertet", + housingNoItems: "Keine Orte gefunden.", jobsTitle: "Stellen", jobsCandidatesTitle: "Vorgeschlagene Kandidaten", jobsCandidatesDescription: "Einwohner, deren Fähigkeiten zu dieser Stelle passen. Sende ihnen eine private Nachricht.", @@ -2842,8 +2841,7 @@ module.exports = { jobLocationPresencial: "Vor Ort", jobLocationRemote: "Remote", jobVacantsPlaceholder: "Anzahl der Positionen", - jobSalaryPlaceholder: "Gehalt in ECO für 1 Arbeitsstunde", - jobImage: "Medien hochladen (max: 50MB)", + jobImage: "Medien hochladen (max: 75MB)", jobTasks: "Aufgaben", jobType: "Stellenart", jobTime: "Arbeitszeit", @@ -2852,7 +2850,6 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Top-Gehalt-Stellen", createJobButton: "Stelle veröffentlichen", - viewDetailsButton: "Details anzeigen", noJobsFound: "Keine Stellenangebote gefunden.", jobAuthor: "Von", jobTypeFreelance: "Freiberufler", @@ -2864,42 +2861,208 @@ module.exports = { jobsHoursOffered: "Angebotene Stunden", jobsHoursRequested: "Im Tausch gefragte Stunden", jobsExchangeSkill: "Im Tausch gefragte Fähigkeit", - jobsHoursOfferedPlaceholder: "z. B. 4", - jobsHoursRequestedPlaceholder: "z. B. 4", - jobsExchangeSkillPlaceholder: "z. B. Tischlerei, Design", - jobExchangeHint: "Für Stundentausch-Angebote füllen Sie statt des Gehalts die folgenden Felder aus.", jobTimePartial: "Teilzeit", jobTimeComplete: "Vollzeit", - jobsDeleteButton: "LÖSCHEN", jobsUpdateButton: "AKTUALISIEREN", jobsFilterApplied: "BEWORBEN", jobsAppliedTitle: "Meine Bewerbungen", jobsAppliedBadge: "Beworben", - jobsFilterFavs: "Favoriten", - jobsFavsTitle: "Favoriten", - jobsFilterNeeds: "Braucht Hilfe", - jobsNeedsTitle: "Braucht Hilfe", - jobsSearchLabel: "Suche", - jobsSearchPlaceholder: "Titel, Tags, Beschreibung suchen...", + jobsSearchPlaceholder: "In Stellen suchen...", jobsMinSalaryLabel: "Mindestgehalt", jobsMaxSalaryLabel: "Höchstgehalt", - jobsSortLabel: "Sortieren nach", jobsSortRecent: "Neueste zuerst", jobsSortSalary: "Höchstes Gehalt", jobsSortSubscribers: "Meiste Bewerber", jobsSearchButton: "Suche", - jobsFavoriteButton: "Favorit", - jobsUnfavoriteButton: "Kein Favorit", - jobsMessageAuthorButton: "PN", jobsApplicants: "Bewerber", jobsUpdatedAt: "Aktualisiert", jobSetOpen: "Als OFFEN setzen", - jobNewBadge: "NEU", jobsTagsLabel: "Tags", - jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "Keine Stellen gefunden.", + industrySearchPlaceholder: "In Industrie suchen...", + industryAllSectors: "Alle Sektoren", + industryVisitButton: "Industrie besuchen", + industryAdvanceTo: "Setzen auf", + industryAmount: "Betrag", + industryApproveBuild: "Genehmigen", + industryBlueprint: "Bauplan", + industryBlueprints: "Baupläne", + industryBuild: "Produktion", + industryBuildNotes: "Notizen", + industryBuildStart: "Startdatum", + industryVoteUpdateButton: "Über Aktualisierung abstimmen", + industryVoteDeleteButton: "Löschung abstimmen", + industryRevokeVote: "Widerrufen", + industryTimeLeft: "Verbleibende Zeit", + industryBuildEnd: "Enddatum", + industryBuilds: "Produktionen", + industryBuildTitle: "Titel", + industryContribute: "Beitragen", + industryContributeButton: "Beitragen", + industryContributions: "Beiträge", + industryCreateBlueprintButton: "Blueprint erstellen", + industryDistribute: "Verteilen", + industryDistributeButton: "Nach Anteilen verteilen", + industryDistribution: "Verteilung", + industryEcoAmount: "Betrag (ECO)", + industryEditBlueprint: "Blueprint bearbeiten", + industryFacility: "Fabrik", + industryKindLabel: "Art", + industryKind_labor: "Arbeit", + industryKind_material: "Material", + industryKind_eco: "Mittel", + industryLaborHours: "Arbeit (Stunden)", + industryHours: "Stunden", + industryMaterialItem: "Material", + industryMaterials: "Materialien", + industryEstMaterials: "Gesamtmaterialien", + industryEstLabor: "Gesamtarbeit", + industryEstTotal: "Geschätzter Preis", + industryBuildingPrice: "Baupreis", + industryFinalPrice: "Endpreis", + industryBlueprintDescPlaceholder: "Spezifikationen, Abmessungen, Gewicht, Doku…", + industryMaterialValue: "Wert (ECO)", + industryMember: "Mitglied", + industryNewBlueprint: "Neuer Blueprint", + industryNewBuild: "Build vorschlagen", + industryEditBuild: "Produktion bearbeiten", + industryNeedBlueprint: "Erstelle zuerst einen Bauplan: jede Produktion fertigt einen.", + industryNoBlueprints: "Noch keine Blueprints.", + industryNoBuilds: "Noch keine Builds.", + industryNote: "Notiz", + industryOutput: "Ausgabe", + industryOutputKind: "Produkttyp", + industryOutputValue: "Ausgabewert (ECO, z. B. aus Verkauf)", + industryPostJob: "Stelle erstellen", + industryPot: "Topf", + industryProposeBuildButton: "Build vorschlagen", + industryProposer: "Vorschlagender", + industryAuthor: "Autor", + industrySellOnMarket: "An den Markt senden", + industrySendToProjects: "Projekt erstellen", + industrySkills: "Fähigkeiten", + industryTreasury: "Kasse", + industryKind_physical: "Physisch", + industryKind_digital: "Digital", + industryBuildStatus_PROPOSED: "VORGESCHLAGEN", + industryBuildStatus_REJECTED: "ABGELEHNT", + industryBuildStatus_APPROVED: "GENEHMIGT", + industryBuildStatus_STOCKING: "BESTÜCKUNG", + industryBuildStatus_IN_PRODUCTION: "IN PRODUKTION", + industryBuildStatus_COMPLETED: "ABGESCHLOSSEN", + industryBuildStatus_FAILED: "FEHLGESCHLAGEN", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Ein Job passt zu deinem Lebenslauf", + jobsBotMatchIntro: "Ein Job passt zu deinem Lebenslauf.", + jobsBotMatchJob: "Job", + jobsBotMatchHint: "Du erhältst dies, weil dein Lebenslauf KI-verwaltet ist. Du kannst es im Lebenslauf abschalten.", + cvAiManaged: "KI-verwaltet", + switchOn: "EIN", + switchOff: "AUS", + cvAiManagedHint: "Wenn aktiv, meldet JobsBot per Privatnachricht, wenn ein Job zu deinem Lebenslauf passt.", + cvMatchThreshold: "Ab dieser Übereinstimmung benachrichtigen", + housingBotRequestedTitle: "Jemand hat einen deiner Orte angefragt.", + housingBotCancelledTitle: "Eine Anfrage zu einem deiner Orte wurde zurückgezogen.", + housingBotYouRequestedTitle: "Du hast einen Ort angefragt.", + housingBotYouCancelledTitle: "Du hast eine Anfrage zurückgezogen.", + housingBotUnavailableTitle: "Ein von dir angefragter Ort ist nicht mehr verfügbar.", + housingBotUnavailableSentTitle: "Du hast die Anfragenden benachrichtigt.", + industryBotAdmittedTitle: "Du wurdest in eine Fabrik aufgenommen.", + industryBotApplicationTitle: "Neuer Mitgliedsantrag in deiner Fabrik.", + industryBotInvitedTitle: "Du wurdest in eine Fabrik eingeladen.", + industryBotDissolvedTitle: "Eine Fabrik, der du angehörst, wurde aufgelöst.", + industryBotBuildApprovedTitle: "Ein Build wurde genehmigt.", + industryBotDistributedTitle: "Ein Build-Output wurde verteilt.", + industryFilterRules: "REGELN", + industryRulesTitle: "Regeln", + industryRulesIntro: "Industry lässt die Bewohner die Produktionsmittel gemeinschaftlich besitzen: Eine Fabrik ist eine netzwerkeigene Werkstatt, die niemand privat besitzt.", + industryRulesSteward: "Der Gründer ist der Verwalter — ein Treuhänder, kein Eigentümer: Er kann die Fabrik nicht allein verkaufen, privatisieren oder auflösen.", + industryRulesMembership: "Die Mitgliedschaftsrichtlinie legt fest, wie man beitritt: Offen (sofort), Per Abstimmung (durch die Stimme der Mitglieder) oder Nur mit Einladung (ein Mitglied muss zuerst einladen).", + industryRulesQuorum: "Das Quorum ist die Mindestzahl an Mitgliedern, die abstimmen müssen, damit eine Entscheidung zählt, damit eine kleine Fabrik nicht von einem einzigen Bewohner entschieden wird.", + industryRulesMajority: "Die Mehrheit ist der Stimmenanteil zum Bestehen (0,5 = Hälfte, 1 = Einstimmigkeit); eine Entscheidung besteht, wenn die JA-Stimmen mindestens max(Quorum, Mitglieder × Mehrheit) erreichen.", + industryRulesDecisions: "Mitglieder stimmen über Aufnahme, Genehmigung von Builds und Auflösung der Fabrik ab; der Verwalter kann diese kollektiven Entscheidungen nicht übergehen.", + industryRulesBlueprints: "Blueprints sind freie (Copyleft-)Rezepte — die Eingaben (Materialien, Arbeitsstunden, Fähigkeiten) und die Ausgabe (ein physisches oder digitales Gut) — und jeder kann sie forken.", + industryRulesBuilds: "Ein Build ist ein Produktionsauftrag: VORGESCHLAGEN → GENEHMIGT (per Abstimmung) → BESTÜCKUNG → IN PRODUKTION → ABGESCHLOSSEN, und er nimmt Beiträge erst nach Genehmigung an.", + industryRulesContributions: "Mitglieder tragen Arbeit (Stunden), Material (deklarierter ECO-Wert) oder ECO zu einem Build bei; jeder Beitrag bringt Karma.", + industryRulesLaborRate: "Der Arbeitssatz ist der ECO-Wert einer Arbeitsstunde in dieser Fabrik; er wandelt Stunden in Karma um, damit Aufwand und Kapital fair vergleichbar sind: Karma = Stunden × Satz + Materialwert + ECO.", + industryRulesShares: "Dein Anteil an einem Build ist dein Karma ÷ Gesamt-Karma und bestimmt, wie viel des Ausgabewerts du erhältst.", + industryRulesDistribution: "Wenn eine Produktion abgeschlossen ist, verteilt der Steward den Topf (die Mittel der Produktion plus Verkaufswert) anteilig an die Beitragenden.", + industryRulesTreasury: "ECO-Beiträge hält der Verwalter als Treuhänder der Build-Kasse bis zur Verteilung; alle Bewegungen werden im Netzwerk aufgezeichnet und Streitfälle können an die Courts gehen.", + industryJobs: "Stellen", + industryReportButton: "Melden", + industryCreateTask: "Aufgabe erstellen", + industryOpenDispute: "Streit eröffnen", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "Einladung generieren", + industryPauseVotes: "Status-Stimmen", + industryTitle: "Industrie", + industryDescription: "Modul zur gemeinschaftlichen Verwaltung der Produktionsmittel.", + typeIndustryBuild: "PRODUKTION", + typeIndustryBlueprint: "BAUPLAN", + typeIndustryAllocation: "VERTEILUNG", + typeIndustry: "INDUSTRIE", + modulesIndustryLabel: "Industrie", + modulesIndustryDescription: "Modul zur gemeinschaftlichen Verwaltung der Produktionsmittel.", + industryFilterAll: "ALLE", + industryFilterRecent: "NEUESTE", + industryFilterMember: "MITGLIED", + industryFilterBlueprints: "BAUPLÄNE", + industryFilterBuilds: "PRODUKTIONEN", + industryFilterMine: "MEINE", + industryFilterActive: "IN BETRIEB", + industryFilterPaused: "PAUSIERT", + industryFilterDissolved: "AUFGELÖST", + industryAllTitle: "Industrie", + industryRecentTitle: "Neueste Fabriken", + industryMemberTitle: "Fabriken, in denen ich bin", + industryMineTitle: "Meine Fabriken", + industryActiveTitle: "In Betrieb", + industryPausedTitle: "Pausierte Fabriken", + industryDissolvedTitle: "Aufgelöste Fabriken", + industryNoFacilitiesFound: "Keine Fabriken gefunden.", + industryCreateFacility: "Fabrik erstellen", + industryMembers: "Mitglieder", + industryMemberBadge: "MITGLIED", + industryName: "Name", + industryNamePlaceholder: "Name der Fabrik", + industryDescriptionLabel: "Beschreibung", + industryDescriptionPlaceholder: "Was produziert diese Fabrik?", + industrySector: "Sektor", + industryMembershipPolicy: "Mitgliedschaftsrichtlinie", + industryQuorum: "Quorum", + industryMajority: "Mehrheit", + industryLaborRate: "Arbeitssatz", + industryCreateButton: "Fabrik erstellen", + industryUpdateButton: "Aktualisieren", + industrySteward: "Verwalter", + industryStatusActive: "IN BETRIEB", + industryStatusPaused: "PAUSIERT", + industryStatusDissolved: "AUFGELÖST", + industryStatusLabel: "Status", + industryJoinButton: "Beitreten", + industryApplyButton: "Beitritt beantragen", + industryLeaveButton: "Verlassen", + industryInviteButton: "Einladen", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Du brauchst eine Einladung, um beizutreten.", + industryPauseButton: "Für Pause stimmen", + industryResumeButton: "Für Fortsetzung stimmen", + industryDeleteButton: "Löschen", + industryPendingApplicants: "Ausstehende Bewerber", + industryVotesYes: "ja", + industryVotesNo: "nein", + industryAlreadyVoted: "abgestimmt", + industryAdmit: "Zulassen", + industryReject: "Ablehnen", + industryDissolveTitle: "Fabrik auflösen", + industryDissolveVote: "Für Auflösung stimmen", + industryPolicy_open: "Offen", + industryPolicy_vote: "Per Abstimmung", + industryPolicy_invite: "Nur mit Einladung", projectsTitle: "Projekte", projectsDescription: "Gemeinschaftsprojekte in deinem Netzwerk erstellen, finanzieren und verfolgen.", + projectSearchPlaceholder: "In Projekte suchen...", projectCreateProject: "Projekt erstellen", projectCreateButton: "Projekt erstellen", projectUpdateButton: "AKTUALISIEREN", @@ -2922,7 +3085,7 @@ module.exports = { projectRecentTitle: "Neueste Projekte", projectTopTitle: "Meistfinanziert", projectTitlePlaceholder: "Projektname", - projectImage: "Medien hochladen (max: 50MB)", + projectImage: "Medien hochladen (max: 75MB)", projectDescription: "Beschreibung", projectDescriptionPlaceholder: "Erzähle die Geschichte und Ziele…", projectGoal: "Ziel (ECO)", @@ -2943,14 +3106,8 @@ module.exports = { projectPledgeTitle: "Dieses Projekt unterstützen", projectPledgePlaceholder: "Betrag in ECO", projectBounties: "Prämien", - projectBountiesInputLabel: "Prämien (eine pro Zeile: Titel|Betrag [ECO]|Beschreibung)", - projectBountiesPlaceholder: "UI-Fehler beheben|100|Link zum Problem\\nDoku schreiben|250|Anwendungsbeispiele skizzieren", projectNoBounties: "Keine Prämien gefunden.", projectTitle: "Titel", - projectAddBountyTitle: "Neue Prämie", - projectBountyTitle: "Prämientitel", - projectBountyAmount: "Betrag (ECO)", - projectBountyDescription: "Beschreibung", projectMilestoneSelect: "Meilenstein auswählen", projectBountyCreateButton: "Prämie erstellen", projectBountyStatus: "Prämienstatus", @@ -2965,19 +3122,15 @@ module.exports = { projectMilestoneTitle: "Meilensteintitel", projectMilestoneTargetPercent: "Prozent (%)", projectMilestoneDueDate: "Datum", - projectMilestoneCreateButton: "Meilenstein erstellen", projectMilestoneStatus: "Meilensteinstatus", projectMilestoneOpen: "offen", projectMilestoneDone: "abgeschlossen", projectMilestoneDue: "fällig", - projectNoMilestones: "Keine Meilensteine gefunden.", projectMilestoneMarkDone: "Als abgeschlossen markieren", projectMilestoneTitlePlaceholder: "Meilensteintitel eingeben", projectMilestoneDescriptionPlaceholder: "Beschreibung für diesen Meilenstein eingeben", projectMilestoneDescription: "Meilensteinbeschreibung", - projectBudgetGoal: "Budget (Ziel)", projectBudgetAssigned: "Prämien zugewiesen", - projectBudgetRemaining: "Verbleibend", projectBudgetOver: "Budget überschritten: Zuweisungen übersteigen das Ziel", projectFollowers: "Follower", projectFollowersTitle: "Follower", @@ -2990,7 +3143,6 @@ module.exports = { projectBackersTotalPledged: "Zugesagt gesamt", projectBackersYourPledge: "Deine Zusage", projectBackersNone: "Noch keine Zusagen.", - projectNoRemainingBudget: "Kein verbleibendes Budget.", projectFilterBackers: "UNTERSTÜTZER", projectFilterApplied: "BEWORBEN", projectAppliedTitle: "BEWORBEN", @@ -2999,36 +3151,19 @@ module.exports = { projectBackerAmount: "Gesamtbeitrag", projectBackerPledges: "Zusagen", projectBackerProjects: "Projekte", - projectPledgeAmount: "Betrag", projectSelectMilestoneOrBounty: "Meilenstein oder Prämie auswählen", projectPledgeButton: "Zusagen", footerLicense: "GPLv3", - footerPackage: "Paket", - footerVersion: "Version", modulesModuleName: "Name", modulesModuleDescription: "Beschreibung", modulesModuleStatus: "Status", - modulesTotalModulesLabel: "Geladene Module", + modulesTotalModulesLabel: "Gesamt", modulesEnabledModulesLabel: "Aktiviert", modulesDisabledModulesLabel: "Deaktiviert", - modulesPopularLabel: "Beliebt", - modulesPopularDescription: "Modul zum Empfang von Beiträgen, die im Trend liegen, am meisten angesehen oder kommentiert werden.", - modulesTopicsLabel: "Themen", - modulesTopicsDescription: "Modul zum Empfang von Diskussionskategorien basierend auf gemeinsamen Interessen.", - modulesSummariesLabel: "Zusammenfassungen", - modulesSummariesDescription: "Modul zum Empfang von Zusammenfassungen langer Diskussionen oder Beiträge.", - modulesLatestLabel: "Neueste", - modulesLatestDescription: "Modul zum Empfang der neuesten Beiträge und Diskussionen.", - modulesThreadsLabel: "Diskussionen", - modulesThreadsDescription: "Modul zum Empfang von Unterhaltungen, gruppiert nach Thema oder Frage.", - modulesMultiverseLabel: "Multiversum", - modulesMultiverseDescription: "Modul zum Empfang von Inhalten anderer föderierter Peers.", modulesInvitesLabel: "Einladungen", modulesInvitesDescription: "Modul zum Verwalten und Einlösen von Einladungscodes.", modulesWalletLabel: "Geldbörse", modulesWalletDescription: "Modul zum Verwalten deiner digitalen Vermögenswerte (ECOin).", - modulesLegacyLabel: "Schlüssel", - modulesLegacyDescription: "Modul zum schnellen und sicheren Verwalten deines geheimen Schlüssels (privater Schlüssel).", modulesCipherLabel: "Verschlüsselung", modulesCipherDescription: "Modul zum symmetrischen Ver- und Entschlüsseln von Text (mit einem gemeinsamen Passwort).", modulesBookmarksLabel: "Lesezeichen", @@ -3063,8 +3198,12 @@ module.exports = { modulesOpinionsDescription: "Modul zum Entdecken und Bewerten von Meinungen.", modulesTransfersLabel: "Überweisungen", modulesTransfersDescription: "Modul zum Entdecken und Verwalten von Smart Contracts (Überweisungen).", - modulesFediverseLabel: "Fediverse", - modulesFediverseDescription: "Verwalte deine anderen Fediverse-Konten, einschließlich Senden und Empfangen von Inhalten.", + modulesFediverseLabel: "Multiversum", + modulesBlogsLabel: "Blogs", + modulesBlogsDescription: "Modul zum Entdecken und Verwalten von Blogs.", + modulesPollsLabel: "Umfragen", + modulesPollsDescription: "Modul, um das Netzwerk zu fragen und Antworten zu zählen.", + modulesFediverseDescription: "Verwalte deine anderen Multiversum-Konten, einschließlich Senden und Empfangen von Inhalten.", modulesFeedLabel: "Feed", modulesFeedDescription: "Modul zum Entdecken und Teilen von Kurztexten (Feeds).", modulesParliamentLabel: "Parlament", @@ -3086,52 +3225,51 @@ module.exports = { modulesProjectsLabel: "Projekte", modulesProjectsDescription: "Modul zum Erkunden, Crowdfunding und Verwalten von Projekten.", modulesBankingLabel: "Bankwesen", - modulesBankingDescription: "Modul zur Bestimmung des realen Werts von ECOIN und Verteilung eines UBI über die gemeinsame Kasse.", + modulesBankingDescription: "Modul zur Verwaltung der gemeinsamen Kasse: ECOIN-Wert, Steuern und BGE.", modulesFavoritesLabel: "Favoriten", modulesFavoritesDescription: "Modul zum Verwalten deiner favorisierten Inhalte.", fileTooLargeTitle: "Datei zu groß", - fileTooLargeMessage: "Die Datei überschreitet die maximal erlaubte Größe (50 MB). Bitte wähle eine kleinere Datei.", + fileTooLargeMessage: "Die Datei überschreitet die maximal erlaubte Größe (75 MB). Bitte wähle eine kleinere Datei.", goBack: "Zurück", + errorDismiss: "OK", errorPageTitle: "Etwas ist schiefgelaufen", + errorNoAllocation: "Keine Zuteilung für diesen Anspruch gefunden.", + errorInvalidClaim: "Diese Zuteilung kann nicht beansprucht werden.", visibilityLabel: "Sichtbarkeit", + clearnetPrivateConflict: "Private, versteckte, geschlossene, nur auf Einladung zugängliche oder kostenpflichtige Inhalte können nicht im CLEARNET veröffentlicht werden. Wähle OASIS oder mache sie öffentlich.", visibilityPublic: "Öffentlich", visibilityHidden: "Versteckt", visibilityMakePublic: "Öffentlich machen", visibilityMakeHidden: "Verstecken", invitesInhabitantsTitle: "Bewohner", - invitesInhabitantsFollow: "Folgen", - aiNavPlaceholder: "Wohin möchtest du gehen?", + aiNavPlaceholder: "Beschreib, was du brauchst...", aiNavDisabled: "KI-Navigation ist deaktiviert.", - aiNavResultsTitle: "KI-Navigationsvorschläge", + aiNavResultsTitle: "AINav-Vorschläge", aiNavQueryLabel: "Anfrage", - aiNavResultsDescription: "Wähle die Route, die am besten zu deiner Suche passt.", - aiNavResultPath: "Route", - aiNavResultDescription: "Was du dort tun kannst", aiNavResultMatch: "Übereinstimmung", aiNavResultsEmpty: "Keine passenden Routen. Versuche stattdessen /search.", - aiNavSearchInstead: "Stattdessen suchen", modulesAINavLabel: "AINav", modulesAINavDescription: "Modul für natürlichsprachliche Anfragen zum Inhalt des Netzwerks.", - directConnect: "Direkte Verbindung", directConnectDescription: "Verbinde dich direkt mit einem Peer, indem du IP-Adresse, Port und öffentlichen Schlüssel eingibst.", peerHost: "IP", - peerPort: "Port (Standard: 8008)", + peerPort: "Port", peerPublicKey: "Öffentlicher Schlüssel (@...ed25519)", connectAndFollow: "Verbinden", - deviceSourceLabel: "Gerätetyp", modulesPresetTitle: "Gängige Konfigurationen", - modulesPreset_minimal: "Minimal", - modulesPreset_basic: "Basis", - modulesPreset_social: "Sozial", - modulesPreset_economy: "Wirtschaft", - modulesPreset_full: "Vollständig", + workflowsTitle: "Workflows", + workflowsDescription: "Ein Workflow legt das Thema und die aktiven Module fest.", + workflowsSet: "Workflow anwenden", + workflow_default: "Standard", + workflow_jobs: "Arbeit", + workflow_social: "Sozial", + workflow_business: "Geschäft", + workflow_night: "Nacht", + workflow_kids: "Kinder", + workflow_activists: "Aktivismus", + workflow_mobile: "Mobil", statsCarbonFootprintTitle: "CO2-Fußabdruck", - statsCarbonFootprintNetwork: "Netzwerk-CO2-Fußabdruck", - statsCarbonFootprintYours: "Dein CO2-Fußabdruck", statsCarbonTombstone: "Tombstoning-Fußabdruck", - feedSuccessMsg: "Feed erfolgreich veröffentlicht!", - dominantOpinionLabel: "Vorherrschende Meinung", - uploadMedia: "Medien hochladen (max: 50MB)", + uploadMedia: "Medien hochladen (max: 75MB)", cipherPasswordDecryptLabel: "Passwort (zum Entschlüsseln)", courtsRespondentInvalid: "Ungültiger Beklagter", feedDetailTitle: "Feed-Details", @@ -3140,7 +3278,7 @@ module.exports = { noComments: "Noch keine Kommentare.", mapsLabel: "Karten", mapTitle: "Karten", - mapDescription: "Erkunden und verwalten Sie Offline-Karten in Ihrem Netzwerk.", + mapDescription: "Verwalten und teilen Sie Offline-Karten in Ihrem Netzwerk.", mapMineSectionTitle: "Ihre Karten", mapCreateSectionTitle: "Karte erstellen", mapUpdateSectionTitle: "Karte aktualisieren", @@ -3164,14 +3302,13 @@ module.exports = { mapDescriptionLabel: "Beschreibung", mapDescriptionPlaceholder: "Beschreiben Sie die Karte oder den Standort...", mapTypeLabel: "Kartentyp", - mapTypeSingle: "EINZELN (nur initialer Standort)", - mapTypeOpen: "OFFEN (jeder kann Markierungen hinzufügen)", - mapTypeClosed: "GESCHLOSSEN (nur Ersteller fügt Markierungen hinzu)", + mapInviteMode: "Einladung", + mapTypeSingle: "EINZELN", + mapTypeOpen: "OFFEN", + mapTypeClosed: "GESCHLOSSEN", mapTagsLabel: "Tags", mapTagsPlaceholder: "Tags durch Kommas getrennt eingeben", mapUrlLabel: "Karten-URL", - mapPickCoordLabel: "Oder wählen Sie einen Standort auf dem Raster:", - mapMarkersLabel: "Markierungen", mapMarkersTitle: "Markierungen", mapMarkerDefault: "Markierung", mapMarkerLatLabel: "Markierung Breitengrad", @@ -3181,9 +3318,22 @@ module.exports = { markerImageLabel: "Marker-Bild", mapAddMarkerTitle: "Markierung hinzufügen", mapAddMarkerButton: "Markierung hinzufügen", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "Markierung entfernen", mapApplyZoom: "Zoom anwenden", - mapSearchPlaceholder: "Beschreibung, Tags, Autor suchen...", + mapPlaceSearchPlaceholder: "Ort oder Land suchen...", + mapNoPlaces: "Keine Orte gefunden.", + mapClusterTitle: "Markierungen hier, klicken zum Vergrößern", + mapDeleteMarker: "Markierung löschen", + mapZoomIn: "Vergrößern", + mapZoomOut: "Verkleinern", + mapFitMarkers: "Alle Markierungen einpassen", + mapPanLabel: "Karte verschieben", + mapCenterHere: "Hier zentrieren", + mapNear: "nahe", + mapPickedSpot: "Gewählter Punkt", + mapPickHint: "Klicke auf die Karte, um einen Punkt zu wählen.", + mapClearPick: "Leeren", + mapSearchPlaceholder: "In Karten suchen...", mapSearchButton: "Suchen", mapUpdatedAt: "Aktualisiert", mapNoMatch: "Keine Karten entsprechen Ihrer Suche.", @@ -3194,61 +3344,628 @@ module.exports = { typeMapMarker: "KARTENMARKIERUNG", modulesMapLabel: "Karten", modulesMapDescription: "Modul zum Verwalten und Teilen von Offline-Karten.", + emergenciesTitle: "Notfälle", + modulesEmergenciesLabel: "Notfälle", + modulesEmergenciesDescription: "Modul zum Auslösen, Bestätigen und Verfolgen von Notfällen im Netzwerk.", + emergenciesDescription: "Modul zum Auslösen, Bestätigen und Verfolgen von Notfällen im Netzwerk.", + emergencyCreate: "Notfall erstellen", + emergencyUpdate: "Aktualisieren", + emergencyDelete: "Löschen", + emergencyTitleLabel: "Titel", + emergencyTitlePlaceholder: "Was passiert?", + emergencyTextLabel: "Details", + emergencyTextPlaceholder: "Wo, seit wann, was tun...", + emergencyCategoryLabel: "Kategorie", + emergencyCategoryWeather: "WETTER", + emergencyCategoryInfrastructure: "INFRASTRUKTUR", + emergencyCategoryHealth: "GESUNDHEIT", + emergencyCategorySecurity: "SICHERHEIT", + emergencyCategoryLost: "FUNDSACHEN", + emergencyCategoryNeighborhood: "NACHBARSCHAFT", + emergencyExpiresLabel: "Läuft ab in", + emergencyExpires1d: "1 Tag", + emergencyExpires3d: "3 Tage", + emergencyExpires7d: "1 Woche", + emergencyExpires30d: "1 Monat", + emergencyExpiresNone: "Unbestimmt", + emergencyExpiresKeep: "Aktuelles Datum behalten", + emergencyTagsLabel: "Tags", + emergencyTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + emergencyFilterAll: "ALLE", + emergencyFilterMine: "MEINE", + emergencyFilterRecent: "NEUESTE", + emergencySeverityUnverified: "UNBESTÄTIGT", + emergencySeverityLow: "NIEDRIG", + emergencySeverityMedium: "MITTEL", + emergencySeverityHigh: "HOCH", + emergencyStatusActive: "AKTIV", + emergencyStatusResolved: "GELÖST", + emergencyStatusExpired: "ABGELAUFEN", + emergencyConfirmations: "Bestätigungen", + emergencyConfirmButton: "Bestätigen", + emergencyConfirmed: "Du hast diesen Notfall bestätigt", + emergencyResolveButton: "Lösen", + emergencyReopenButton: "Wieder öffnen", + emergencyGenerateReport: "Bericht erzeugen", + emergencyUpdateEdited: "Bearbeitet", + emergencyLastUpdate: "LETZTE AKTUALISIERUNG", + emergencyUpdatesTitle: "Letzte Aktualisierungen:", + emergencyNoUpdates: "Noch keine Aktualisierungen.", + emergencyAddUpdate: "Aktualisierung veröffentlichen", + emergencyUpdatePlaceholder: "Was hat sich geändert?", + emergencyNoItems: "Keine Notfälle gefunden.", + emergencyExpiresAt: "Läuft ab", + emergencyBannerLabel: "Aktiver Notfall", + emergencyCreateSectionTitle: "Notfall erstellen", + emergencyUpdateSectionTitle: "Notfall aktualisieren", + emergencySearchPlaceholder: "In Notfällen suchen...", + typeEmergency: "NOTFÄLLE", + typeEmergencyConfirm: "NOTFALLBESTÄTIGUNG", + typeEmergencyUpdate: "NOTFALLAKTUALISIERUNG", + statsEmergency: "Notfälle", + statsEmergencyConfirm: "Notfallbestätigungen", + statsEmergencyUpdate: "Notfallaktualisierungen", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "Ein Notfall, dem du folgst, hat eine Aktualisierung.", + emergencyBotResolvedTitle: "Ein Notfall, dem du folgst, wurde gelöst.", + dataKindEmergencies: "NOTFÄLLE", + mailingTitle: "Mailinglisten", + modulesMailingLabel: "Mailinglisten", + modulesMailingDescription: "Entdecke und verwalte Mailinglisten in deinem Netzwerk.", + modulesPhoneLabel: "Telefon", + modulesPhoneDescription: "Modul für Ende-zu-Ende-verschlüsselte Videoanrufe mit anderen Bewohnern.", + mailingDescription: "Entdecke und verwalte Mailinglisten in deinem Netzwerk.", + mailingCreate: "Mailingliste erstellen", + mailingUpdate: "Aktualisieren", + mailingArchive: "Archivieren", + mailingActivate: "Aktivieren", + mailingCreateSectionTitle: "Mailingliste erstellen", + mailingUpdateSectionTitle: "Mailingliste aktualisieren", + mailingTitleLabel: "Titel", + mailingTitlePlaceholder: "Name der Mailingliste", + mailingDescriptionLabel: "Beschreibung", + mailingDescriptionPlaceholder: "Worum geht es in dieser Liste?", + mailingTypeLabel: "Typ der Mailingliste", + mailingTypeOpen: "OFFEN", + mailingTypeClosed: "GESCHLOSSEN", + mailingStatusLabel: "Status", + mailingStatusActive: "AKTIV", + mailingStatusArchived: "ARCHIVIERT", + mailingMembersLabel: "Bewohner einladen", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Abonnenten", + mailingTagsLabel: "Tags", + mailingTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + mailingFilterAll: "ALLE", + mailingFilterMine: "MEINE", + mailingFilterSubscribed: "ABONNIERT", + mailingFilterRecent: "NEU", + mailingHistoryDate: "DATUM", + mailingHistoryThreads: "THREADS", + mailingHistoryInhabitants: "BEWOHNER", + mailingHistorySearchPlaceholder: "Im Verlauf suchen...", + mailingNoMessages: "Noch keine Nachrichten.", + mailingNoItems: "Keine Mailinglisten gefunden.", + mailingSearchPlaceholder: "In Mailinglisten suchen...", + mailingWriteButton: "Nachricht schreiben", + mailingWriteTitle: "An die Liste schreiben", + mailingReplyTitle: "Der Liste antworten", + mailingSend: "Senden", + mailingTextPlaceholder: "Deine Nachricht erreicht alle Teilnehmer der Liste.", + mailingParticipants: "Teilnehmer", + inboxFilterMailing: "Mailinglisten", + mailingReplyPlaceholder: "Der Liste antworten...", + mailingReplyToList: "Der Liste antworten", + typeMailingList: "MAILINGLISTE", + statsMailingList: "Mailinglisten", + mailingListLabel: "MAILINGLISTE", + logisticsTitle: "Logistik", + modulesLogisticsLabel: "Logistik", + modulesLogisticsDescription: "Entdecke und verwalte Logistikpläne in deinem Netzwerk.", + logisticsDescription: "Entdecke und verwalte Logistikpläne in deinem Netzwerk.", + logisticsCreate: "Route erstellen", + logisticsUpdate: "Aktualisieren", + logisticsClose: "Schließen", + logisticsReopen: "Wieder öffnen", + logisticsCreateSectionTitle: "Route erstellen", + logisticsUpdateSectionTitle: "Route aktualisieren", + logisticsTitleLabel: "Titel", + logisticsTitlePlaceholder: "Fahrt zum Markt, Transporter verfügbar...", + logisticsKindLabel: "Logistiktyp", + logisticsKindTrip: "FAHRT", + logisticsKindShipment: "SENDUNG", + logisticsKindTrips: "FAHRTEN", + logisticsKindShipments: "SENDUNGEN", + logisticsModeLabel: "Modus", + logisticsModeOffer: "BIETE", + logisticsModeRequest: "SUCHE", + logisticsModeOffers: "ANGEBOTE", + logisticsModeRequests: "GESUCHE", + logisticsDescriptionLabel: "Beschreibung", + logisticsDescriptionPlaceholder: "Treffpunkt, was du transportierst, Bedingungen...", + logisticsOriginLabel: "Start", + logisticsOriginPlaceholder: "Wo es losgeht", + logisticsDestinationLabel: "Ziel", + logisticsDestinationPlaceholder: "Wo es endet", + logisticsDateLabel: "Datum und Uhrzeit", + logisticsRecurrenceLabel: "Wiederholung", + logisticsFrequencyLabel: "Häufigkeit", + logisticsRecurrenceNone: "EINMALIG", + logisticsRecurrenceDaily: "TÄGLICH", + logisticsRecurrenceWeekly: "WÖCHENTLICH", + logisticsRecurrenceMonthly: "MONATLICH", + logisticsSeatsLabel: "Plätze", + logisticsSizeLabel: "Größe", + logisticsSizePlaceholder: "z. B. 2m x 1m", + logisticsWeightLabel: "Gewicht", + logisticsWeightPlaceholder: "z. B. 40kg", + logisticsPriceFree: "GRATIS", + logisticsPriceEco: "ECO", + logisticsPriceTime: "ZEIT", + logisticsPriceLabel: "Preis", + logisticsHours: "h", + logisticsOrderRefLabel: "Bestell-Link", + logisticsOrderRefPlaceholder: "/shops/... oder /market/... der zu liefernden Bestellung", + logisticsOrderLink: "Verknüpfte Bestellung", + logisticsTagsLabel: "Tags", + logisticsTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + logisticsFilterAll: "ALLE", + logisticsFilterMine: "MEINE", + logisticsFilterBooked: "GEBUCHT", + logisticsFilterRecent: "NEU", + logisticsFilterHistory: "VERLAUF", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "OFFEN", + logisticsStatusUpcoming: "BEVORSTEHEND", + logisticsStatusPast: "VERGANGEN", + logisticsStatusClosed: "GESCHLOSSEN", + logisticsZoneLabel: "Zone", + logisticsSearchPlaceholder: "In Logistik suchen...", + logisticsNoItems: "Keine Routen gefunden.", + logisticsBookingPending: "AUSSTEHEND", + logisticsBookingConfirmed: "BESTÄTIGT", + logisticsBookingRejected: "ABGELEHNT", + logisticsBookingCancelled: "STORNIERT", + logisticsBookingDelivered: "GELIEFERT", + logisticsConfirm: "Bestätigen", + logisticsReject: "Ablehnen", + logisticsDeliver: "Liefern", + logisticsReceiptLabel: "Beleg", + logisticsReceiptPlaceholder: "Lieferhinweis (optional)", + logisticsCancelBooking: "Buchung stornieren", + logisticsBookTitle: "Diese Route buchen", + logisticsNotesLabel: "Notizen", + logisticsNotesPlaceholder: "Was der Anbieter wissen sollte", + logisticsBook: "Buchen", + logisticsBookingsTitle: "Buchungen", + logisticsMyBookingTitle: "Deine Buchung", + logisticsRatingsTitle: "Bewertungen", + logisticsNoRatings: "Noch keine Bewertungen.", + logisticsRatingsWhenClosed: "Bewertungen sind möglich, sobald die Route geschlossen ist.", + logisticsScoreLabel: "Punkte", + logisticsRatingPlaceholder: "Wie lief es?", + logisticsRate: "Bewerten", + typeLogisticsRoute: "ROUTE", + typeLogisticsRating: "ROUTENBEWERTUNG", + statsLogisticsRoute: "Routen", + statsLogisticsRating: "Routenbewertungen", + logisticsRouteLabel: "ROUTE", + podcastsTitle: "Podcasts", + modulesPodcastsLabel: "Podcasts", + modulesPodcastsDescription: "Modul zum Veröffentlichen von Podcast-Kanälen.", + podcastsDescription: "Modul zum Veröffentlichen von Podcast-Kanälen.", + podcastCreate: "Podcast erstellen", + podcastUpdate: "Aktualisieren", + podcastDelete: "Löschen", + podcastCreateSectionTitle: "Podcast erstellen", + podcastUpdateSectionTitle: "Podcast aktualisieren", + podcastTitleLabel: "Titel", + podcastTitlePlaceholder: "Name des Kanals", + podcastDescriptionLabel: "Beschreibung", + podcastDescriptionPlaceholder: "Worum geht es in diesem Podcast?", + podcastCategoryLabel: "Kategorie", + podcastCategoryNews: "NACHRICHTEN", + podcastCategoryMusic: "MUSIK", + podcastCategoryTalk: "GESPRÄCH", + podcastCategoryEducation: "BILDUNG", + podcastCategoryTech: "TECHNIK", + podcastCategoryCulture: "KULTUR", + podcastCategoryCommunity: "GEMEINSCHAFT", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "SONSTIGES", + podcastTagsLabel: "Tags", + podcastCoverLabel: "Cover (Bild oder Video)", + podcastEpisodeAudioLabel: "Audio- oder Videodatei (max: 75MB)", + podcastTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + podcastFilterAll: "ALLE", + podcastFilterMine: "MEINE", + podcastFilterRecent: "NEU", + podcastFilterTop: "TOP", + podcastFilterViewers: "HÖRER", + podcastSearchPlaceholder: "In Podcasts suchen...", + podcastNoItems: "Keine Podcasts gefunden.", + podcastEpisodesLabel: "Episoden", + podcastPlaysLabel: "Aufrufe", + podcastLastEpisode: "Letzte Episode", + podcastEpisodesTitle: "Episoden", + podcastNoEpisodes: "Noch keine Episoden.", + podcastEpisodeAdd: "Episode hinzufügen", + podcastEpisodeCreateTitle: "Episode veröffentlichen", + podcastEpisodeUpdateTitle: "Episode aktualisieren", + podcastEpisodeTitleLabel: "Episodentitel", + podcastEpisodeTitlePlaceholder: "Titel der Episode", + podcastEpisodeDescriptionLabel: "Shownotes", + podcastEpisodeDescriptionPlaceholder: "Was in dieser Episode passiert, Links, Credits...", + podcastEpisodePublish: "Veröffentlichen", + podcastPlayed: "Gehört", + podcastMarkPlayed: "Als gehört markieren", + typePodcast: "PODCAST", + typePodcastEpisode: "EPISODE", + typePodcastOpinion: "EPISODENMEINUNG", + typePodcastPlay: "WIEDERGABE", + statsPodcast: "Podcasts", + statsPodcastEpisode: "Episoden", + statsPodcastPlay: "Wiedergaben", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "Ein Blog, dem du folgst, hat einen neuen Eintrag.", + podcastBotEpisodeTitle: "Ein Podcast, dem du folgst, hat eine neue Episode.", + podcastLabel: "PODCAST", + podcastEpisodeLabel: "EPISODE", + campaignsTitle: "Kampagnen", + modulesCampaignsLabel: "Kampagnen", + modulesCampaignsDescription: "Entdecke und verwalte Kampagnen in deinem Netzwerk.", + campaignsDescription: "Entdecke und verwalte Kampagnen in deinem Netzwerk.", + campaignCreate: "Kampagne erstellen", + campaignUpdate: "Aktualisieren", + campaignDelete: "Löschen", + campaignClose: "Schließen", + campaignReopen: "Wieder öffnen", + campaignCreateSectionTitle: "Kampagne erstellen", + campaignUpdateSectionTitle: "Kampagne aktualisieren", + campaignTitleLabel: "Titel", + campaignTitlePlaceholder: "Was willst du erreichen?", + campaignTextLabel: "Text", + campaignTextPlaceholder: "Erkläre das Anliegen, wer entscheidet und was du forderst...", + campaignCategoryLabel: "Kategorie", + campaignCategoryEnvironment: "UMWELT", + campaignCategoryRights: "RECHTE", + campaignCategoryHealth: "GESUNDHEIT", + campaignCategoryEducation: "BILDUNG", + campaignCategoryInfrastructure: "INFRASTRUKTUR", + campaignCategoryCulture: "KULTUR", + campaignCategoryEconomy: "WIRTSCHAFT", + campaignCategoryOther: "SONSTIGES", + campaignGoalLabel: "Unterschriftenziel", + campaignDeadlineLabel: "Frist", + campaignTagsLabel: "Tags", + campaignTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + campaignFilterAll: "ALLE", + campaignFilterMine: "MEINE", + campaignFilterSigned: "UNTERSCHRIEBEN", + campaignFilterRecent: "NEU", + campaignFilterTop: "TOP", + campaignStatusOpen: "OFFEN", + campaignStatusAchieved: "ERREICHT", + campaignStatusUnachieved: "NICHT ERREICHT", + campaignStatusClosed: "GESCHLOSSEN", + campaignSearchPlaceholder: "In Kampagnen suchen...", + campaignNoItems: "Keine Kampagnen gefunden.", + campaignSignaturesLabel: "Unterschriften", + campaignSignaturesTitle: "Unterstützer", + campaignNoSignatures: "Noch keine Unterschriften.", + campaignSignTitle: "Diese Kampagne unterschreiben", + campaignSignPlaceholder: "Warum unterstützt du sie? (optional)", + campaignSign: "Unterschreiben", + campaignSigned: "Du hast bereits unterschrieben!", + campaignNotSigned: "Deine Unterschrift fehlt noch", + campaignElevate: "Ins Parlament bringen", + campaignElevated: "Ins Parlament gebracht", + campaignUpdatesTitle: "Aktualisierungen", + campaignNoUpdates: "Noch keine Aktualisierungen.", + campaignUpdatePlaceholder: "Was ist passiert?", + campaignAddUpdate: "Aktualisierung veröffentlichen", + campaignQrAlt: "QR zum Unterschreiben dieser Kampagne", + campaignQrCaption: "Scannen zum Unterschreiben", + typeCampaign: "KAMPAGNE", + typeCampaignSignature: "UNTERSCHRIFT", + typeCampaignUpdate: "KAMPAGNEN-AKTUALISIERUNG", + statsCampaign: "Kampagnen", + statsCampaignSignature: "Kampagnenunterschriften", + statsCampaignUpdate: "Kampagnenaktualisierungen", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "Eine Kampagne, der du folgst, hat eine Aktualisierung.", + campaignBotAchievedTitle: "Eine Kampagne, der du folgst, hat ihr Ziel erreicht.", + campaignBotRaisedTitle: "Eine von dir unterschriebene Kampagne wurde ins Parlament gebracht.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Eine von dir gebuchte Route hat sich geändert.", + logisticsBotClosedTitle: "Eine von dir gebuchte Route wurde geschlossen.", + logisticsBotBookedTitle: "Jemand hat deine Route gebucht.", + logisticsBotCancelledTitle: "Eine Buchung deiner Route wurde storniert.", + logisticsBotConfirmedTitle: "Deine Buchung wurde bestätigt: das Paket ist unterwegs.", + logisticsBotRejectedTitle: "Deine Buchung wurde abgelehnt.", + logisticsBotDeliveredTitle: "Dein Paket ist angekommen.", + campaignLabel: "KAMPAGNE", + campaignGenerateReport: "Bericht erzeugen", + logisticsGenerateReport: "Bericht erzeugen", + mailingGenerateReport: "Bericht erzeugen", + campaignUpdateEdited: "Bearbeitet", + mailingMessagesLabel: "Nachrichten", + logisticsBookingsLabel: "Buchungen", + backupTitle: "Sicherung", + modulesBackupLabel: "Sicherung", + modulesBackupDescription: "Modul zum Exportieren und Importieren deiner Schlüssel und für verschlüsselte Sicherungen.", + backupDescription: "Modul zum Exportieren und Importieren deiner Schlüssel und für verschlüsselte Sicherungen.", + backupTypeRecovery: "WIEDERHERSTELLUNG", + backupTypeKeys: "SCHLÜSSEL EXPORTIEREN", + backupTypeFull: "VOLLSICHERUNG", + backupTypeRestore: "SICHERUNG WIEDERHERSTELLEN", + backupTypeInstant: "SOFORT", + backupInstantDescription: "Ein einfaches .zip deines gesamten Logs und deiner Dateien, ohne geheimen Schlüssel und ohne Passwort. Schnell, aber ", + backupInstantRisk: "wer die Datei hat, liest alles.", + backupKitPageDescription: "Das Wiederherstellungskit kann nur von diesem Gerät aus geöffnet werden.", + backupKitId: "Oasis-ID", + backupKitDate: "Erstellt", + backupKitQrAlt: "QR-Code deines geheimen Schlüssels", + backupKitStep1: "Drucke oder schreibe ihn ab und bewahre ihn sicher und offline auf.", + backupKitStep2: "Zur Wiederherstellung installiere Oasis auf einem neuen Gerät und importiere dieses Geheimnis unter Sicherung › Schlüssel.", + backupKitStep3: "Vernichte diese Seite, falls sie jemals offengelegt wird.", + backupKitPdfButton: "PDF erzeugen", + backupScopeLabel: "Umfang", + backupScopeAll: "ALLES", + backupScopeMine: "NUR MEINE INHALTE", + backupSinceLabel: "Seit", + backupEstimateTitle: "Schätzung", + backupFileInfo: "Deine verschlüsselte Kopie wird auf dein Gerät heruntergeladen (Name: oasis-backup-DATUM.oasisbk)", + backupExportButton: "Sicherung herunterladen", + backupRestorePassword: "Gib das Passwort ein, das bei der Sicherung verwendet wurde", + backupRestoreButton: "Sicherung wiederherstellen", + backupRestoredLine: "Sicherung wiederhergestellt.", + backupRestoredMessages: "Nachrichten hinzugefügt", + backupRestoredSkipped: "Bereits vorhanden", + backupRestoredBlobs: "Dateien hinzugefügt", + backupRestoredState: "Einstellungen wiederhergestellt", + backupRestoredFailed: "Fehlgeschlagen", + backupRestoredForked: "Abweichend", + backupRestoreRunning: "Die Sicherung wird wiederhergestellt. Diese Seite aktualisiert sich automatisch; du kannst Oasis währenddessen weiter benutzen.", + backupRestoreElapsed: "Vergangen", + backupRestoreFailedLine: "Die Sicherung konnte nicht wiederhergestellt werden.", + backupRestoreForkMine: "Dieses Gerät hat bereits Nachrichten mit deiner Identität veröffentlicht, die nicht in der Sicherung sind, daher konnte die Kopie nicht darüber angewendet werden. Stelle auf einer sauberen Installation direkt nach dem Import deiner Schlüssel wieder her, bevor du etwas anderes tust.", + verificationTitle: "Prüfung", + verificationDescription: "Prüfe die Kette deines Feeds, erkenne Abzweigungen und sieh dir verwaiste oder fehlende Dateien an.", + verificationRun: "Prüfung starten", + verificationTotalMessages: "Nachrichten im Log", + verificationFeed: "Dein Feed", + verificationMessages: "Nachrichten", + verificationLastSequence: "letzte Sequenz", + verificationGaps: "Lücken", + verificationBrokenLinks: "Gebrochene Verweise", + verificationBadHashes: "Falsche Hashes", + verificationBadSignatures: "Falsche Signaturen", + verificationDuplicates: "Doppelte Sequenzen", + verificationForks: "Abzweigungen", + verificationBlobs: "Dateien", + verificationBlobsPresent: "vorhanden", + verificationBlobsReferenced: "referenziert", + verificationBlobsOrphan: "verwaist", + verificationBlobsMissing: "fehlend", + verificationBlobsMissingOwn: "Eigene Dateien, die auf diesem Gerät fehlen", + verificationBlobsMissingOthers: "Dateien anderer Bewohner noch nicht heruntergeladen", + verificationBlobsUnavailable: "Dateien konnten auf diesem Gerät nicht aufgelistet werden.", + wikiTitle: "Wikis", + modulesWikiLabel: "Wikis", + modulesWikiDescription: "Modul zum Entdecken und Verwalten von Wikis.", + wikiDescription: "Modul zum Entdecken und Verwalten von Wikis.", + wikiCreate: "Wiki erstellen", + wikiUpdate: "Aktualisieren", + wikiDelete: "Löschen", + wikiEdit: "Bearbeiten", + wikiTitleLabel: "Titel", + wikiTitlePlaceholder: "Wiki-Titel...", + wikiBodyLabel: "Inhalt", + wikiBodyPlaceholder: "Verlinke andere Wikis mit [[Wiki-Titel]] oder [[Wiki-Titel|eigener Text]].\n\nErwähne Bewohner mit @id.\n\nTagge mit #oasis.\n\nFüge Links wie https://solarnethub.com ein\n\nEine Leerzeile beginnt einen neuen Absatz; einfache Zeilenumbrüche bleiben erhalten.\n\nHänge beliebige unterstützte Medien an – sie werden hier eingebettet.", + formatBarLabel: "Format", + wikiStatusLabel: "Wiki-Typ", + wikiStatusOpen: "OFFEN", + wikiStatusClosed: "GESCHLOSSEN", + wikiTagsLabel: "Tags", + wikiTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + wikiSummaryLabel: "Zusammenfassung der Änderung", + wikiSummaryPlaceholder: "Was hat sich geändert?", + wikiFilterAll: "ALLE", + wikiFilterMine: "MEINE", + wikiFilterRecent: "NEUESTE", + wikiFilterLinked: "VERKNÜPFT", + wikiChanges: "Letzte Änderungen", + wikiNoPages: "Keine Wikis gefunden.", + wikiNoChanges: "Noch keine Änderungen.", + wikiHistory: "Verlauf", + wikiVersion: "Version", + wikiVersions: "Versionen", + wikiRestore: "Wiederherstellen", + wikiDiff: "Änderungen", + wikiViewVersion: "Ansehen", + wikiLinkedPages: "Verknüpfte Seiten", + wikiNoLinkedPages: "Diese Seite ist noch mit keiner anderen verknüpft.", + wikiMissingLink: "Seite existiert noch nicht", + wikiDuplicateTitle: "Es gibt bereits eine Seite mit diesem Titel. Ändere den Titel, um eine andere Seite anzulegen.", + wikiCreateSectionTitle: "Wiki erstellen", + wikiUpdateSectionTitle: "Wiki aktualisieren", + wikiSearchPlaceholder: "Im Wiki suchen...", + wikiCurrentVersion: "Aktuelle Version", + wikiViewingVersion: "Du siehst eine alte Version", + wikiBackToPage: "Zurück zur Seite", + wikiLinkedChip: "Verknüpft", + wikiRestoredSummary: "Wiederhergestellte Version", + wikiExportPdf: "Seite erzeugen", + wikiPreview: "Vorschau", + tribeSectionWiki: "WIKI", + tribeWikiCreate: "Wiki erstellen", + tribeWikiEmpty: "Noch keine Wikis.", + typeWiki: "WIKI", padsTitle: "Pads", padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Modul zur Verwaltung kollaborativer Texteditoren.", - padFilterAll: "ALLE", - padFilterMine: "MEINE", - padFilterRecent: "AKTUELL", - padFilterOpen: "OFFEN", - padFilterClosed: "GESCHLOSSEN", padCreate: "Pad Erstellen", - padUpdate: "Pad Aktualisieren", - padDelete: "Pad Löschen", + padUpdate: "Aktualisieren", + padDelete: "Löschen", padTitleLabel: "Titel", padTitlePlaceholder: "Pad-Titel eingeben...", padStatusLabel: "Status", + padTypeLabel: "Pad-Typ", padStatusOpen: "OFFEN", padStatusInviteOnly: "NUR AUF EINLADUNG", padStatusClosed: "GESCHLOSSEN", padDeadlineLabel: "Frist", padTagsLabel: "Tags", - padTagsPlaceholder: "tag1, tag2, ...", + padTagsPlaceholder: "Tags durch Kommas getrennt eingeben", padMembersLabel: "Mitglieder", - padVisitPad: "Pad Besuchen", - padShareUrl: "URL Teilen", padCreated: "Erstellt", - padAuthor: "Autor", - padGenerateCode: "Code Generieren", - padInviteCodeLabel: "Einladungscode", - padInviteCodePlaceholder: "Einladungscode eingeben...", - padValidateInvite: "Validieren", - padStartEditing: "BEARBEITUNG STARTEN!", + padStartEditing: "Bearbeiten", padEditorPlaceholder: "Schreiben beginnen...", padSubmitEntry: "Senden", padNoEntries: "Noch keine Einträge.", padAllSectionTitle: "Alle Pads", padMineSectionTitle: "Meine Pads", padsDescription: "Verwalte kollaborative verschlüsselte Texteditoren in deinem Netzwerk.", + roomsTitle: "Räume", + roomsDescription: "Entdecke verschlüsselte Videokonferenzräume in deinem Netzwerk und tritt ihnen bei.", + modulesRoomsLabel: "Räume", + modulesRoomsDescription: "Modul zum Entdecken und Betreten verschlüsselter Videokonferenzräume.", + roomFilterRecent: "NEUESTE", + roomFilterMine: "MEINE", + roomFilterAll: "ALLE", + roomFilterLive: "LIVE", + roomFilterOpen: "OFFEN", + roomFilterInvite: "NUR AUF EINLADUNG", + roomFilterClosed: "GESCHLOSSEN", + roomCreate: "Raum erstellen", + roomUpdate: "Aktualisieren", + roomDelete: "Löschen", + roomClose: "Schließen", + roomReopen: "Wieder öffnen", + roomCreateSectionTitle: "Raum erstellen", + roomUpdateSectionTitle: "Raum aktualisieren", + roomTitlePlaceholder: "Titel des Raums", + roomDescriptionLabel: "Beschreibung", + roomDescriptionPlaceholder: "Worüber wollt ihr sprechen?", + roomTypeLabel: "Raumtyp", + roomTagsLabel: "Tags", + roomStatusOpen: "OFFEN", + roomStatusInviteOnly: "NUR AUF EINLADUNG", + roomStatusClosed: "GESCHLOSSEN", + roomParticipants: "Teilnehmer", + roomJoin: "Beitreten", + roomLeave: "Verlassen", + roomFull: "Raum voll", + roomLiveChip: "LIVE", + roomMuted: "stumm", + roomRec: "Audio aufnehmen", + roomRecStop: "Aufnahme beenden", + roomStreamFile: "Streamen", + roomStreamStop: "Stoppen", + roomStreaming: "Wird gestreamt", + liveFullscreen: "Vollbild", + roomStreamPickTitle: "Video auswählen", + roomStreamPickHint: "Videos auf diesem Gerät. Das gewählte wird von seinem Speicherort aus gestreamt, ohne es zu kopieren oder zu veröffentlichen.", + roomStreamGo: "Im Raum abspielen", + roomStreamNotFound: "Dieses Video ist nicht in deinen Ordnern.", + roomPublicWarning: "Dieser Raum wird im öffentlichen Web übertragen: Jeder kann sehen und hören, was darin passiert.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} hat den Raum betreten", + roomEventLeft: "{name} hat den Raum verlassen", + roomEventRecStart: "{name} hat die Aufnahme gestartet", + roomEventRecStop: "{name} hat die Aufnahme beendet", + roomEventMuted: "{name} hat sich stummgeschaltet", + roomEventUnmuted: "{name} hat die Stummschaltung aufgehoben", + roomEventHand: "{name} hat die Hand gehoben", + roomRaiseHand: "Hand heben", + roomLowerHand: "Hand senken", + confirmActionTitle: "Diese Aktion bestätigen", + confirmActionButton: "Bestätigen", + confirmJoinText: "Du bist dabei, mit dieser Einladung beizutreten.", + confirmFollowText: "Du bist dabei, diese Bewohnerin bzw. diesen Bewohner zu unterstützen.", + confirmSignText: "Du bist dabei, diese Kampagne zu unterzeichnen.", + confirmDownloadText: "Du bist dabei, diese Datei auf dein Gerät herunterzuladen.", + roomRecordings: "Aufnahmen", + roomClearNotices: "Leeren", + roomLineSwitchboard: "Zentrale", + roomLineDndError: "Die Nummer dieses Raums nimmt keine Anrufe an.", + roomNumberHint: "Wähle diese Nummer im Telefon, um den Raum zu betreten.", + liveRefresh: "Aktualisieren", + roomNobody: "Gerade ist niemand im Raum.", + roomSomeone: "{n} gerade im Raum.", + roomYou: "du", + roomYouAreIn: "Du bist drin", + roomNobodyInside: "Gerade ist niemand in diesem Raum.", + roomErrorFull: "Der Raum ist voll. Du kannst beitreten, sobald jemand geht.", + roomErrorUnreachable: "Der Raum ist gerade nicht erreichbar. Versuche es später erneut.", + roomErrorBusy: "Du bist bereits in einem Anruf oder in einem anderen Raum.", + roomErrorClosed: "Dieser Raum ist geschlossen.", + roomAccessDenied: "Dieser Raum ist nur auf Einladung. Du brauchst eine Einladung, um beizutreten.", + roomsNoItems: "Keine Räume gefunden.", + roomSearchPlaceholder: "Räume suchen...", + roomInviteMode: "Einladung", + tribeSectionRooms: "RÄUME", + tribeRoomCreate: "Raum erstellen", + tribeRoomsEmpty: "Noch keine Räume.", + typeRoom: "RÄUME", + typeRoomMember: "RAUMTEILNEHMER", + statsRoom: "Räume", + statsRoomMember: "Raumteilnehmer", + favoritesFilterRooms: "RÄUME", + invitesRoomsTitle: "Räume", + inviteCodeInvalid: "Dieser Einladungscode ist ungültig oder wurde schon benutzt.", + inviteAlreadyMember: "Du bist schon dabei. Du kannst es über sein Modul öffnen.", + actionFailed: "Das hat nicht geklappt. Bitte versuche es erneut.", + publishTooShort: "Dein Beitrag ist zu kurz.", + walletSendFailed: "Die Zahlung konnte nicht gesendet werden: {reason}", + pixeliaCoordsError: "Diese Koordinaten liegen außerhalb des Rasters.", + courtsErrTitle: "Ein Titel ist erforderlich.", + courtsErrRespondent: "Gib an, gegen wen sich der Fall richtet.", + courtsErrRespondentId: "Das ist keine gültige Oasis-ID.", + courtsErrMethod: "Wähle, wie der Fall gelöst wird.", + courtsErrCaseNotFound: "Dieser Fall wurde nicht gefunden.", + courtsErrBrief: "Schreibe deine Antwort.", + courtsErrStance: "Wähle deine Haltung.", + courtsErrResult: "Schreibe das Ergebnis.", + courtsErrTerms: "Schreibe die Bedingungen der Einigung.", + courtsErrMediator: "Füge mindestens eine vermittelnde Person hinzu.", + courtsErrSelfMediator: "Du kannst in deinem eigenen Fall nicht vermitteln.", + courtsErrJudge: "Wähle eine richtende Person.", + courtsErrSelfJudge: "Du kannst nicht über deinen eigenen Fall richten.", + courtsErrVisibility: "Wähle, wer den Fall sehen kann.", + courtsErrNomination: "Diese Nominierung wurde nicht gefunden.", + opinionAlreadyGiven: "Du hast deine Meinung schon abgegeben.", + cipherPasswordShort: "Das Passwort muss mindestens 32 Zeichen lang sein.", + errorForbidden: "Darauf hast du keinen Zugriff.", + errorNotFound: "Das wurde nicht gefunden.", + errorClosed: "Das ist geschlossen.", + errorInviteOnly: "Das ist nur auf Einladung. Du brauchst eine Einladung.", + errorTooLong: "Dieser Text ist zu lang.", + errorRequired: "Es fehlen Pflichtangaben.", + errorInvalid: "Einige Angaben sind ungültig.", + errorNotAuthor: "Nur wer es erstellt hat, kann das tun.", + panicRemoved: "Deine Blockchain wurde gelöscht.", padRecentSectionTitle: "Aktuelle Pads", padOpenSectionTitle: "Offene Pads", padClosedSectionTitle: "Geschlossene Pads", padCreateSectionTitle: "Neues Pad Erstellen", padUpdateSectionTitle: "Pad Aktualisieren", - padInviteGenerated: "Einladungscode Generiert", typePad: "PADS", padNew: "NEU", padAddFavorite: "Zu Favoriten Hinzufügen", padRemoveFavorite: "Aus Favoriten Entfernen", - padClose: "Pad schließen", + padClose: "Schließen", padBackToEditor: "Zurück zum Editor", - padSearchPlaceholder: "Pads suchen...", + padSearchPlaceholder: "In Pads suchen...", gamesTitle: "Spiele", gamesDescription: "Entdecke und spiele einige Spiele.", gamesFilterAll: "ALLE", gamesPlayButton: "SPIELEN!", - gamesBackToGames: "Zurück zu Spielen", modulesGamesLabel: "Spiele", modulesGamesDescription: "Modul zum Entdecken und Spielen von Spielen.", modulesGraphosLabel: "Graphos", @@ -3265,8 +3982,6 @@ module.exports = { gamesArkanoidDesc: "Brecht alle Steine mit eurem Schläger und Ball. Ein klassisches Arcade-Spiel.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Klassisches Ping-Pong gegen eine KI. Wer zuerst 5 Punkte hat, gewinnt.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "Rennen gegen die Zeit! Weiche dem Verkehr aus und erreiche das Ziel rechtzeitig.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Steuere dein Raumschiff durch ein tödliches Asteroidenfeld. Schieße sie ab, bevor sie dich treffen.", gamesRockPaperScissorsTitle: "Schere Stein Papier", @@ -3276,27 +3991,28 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "Werfe eine Münze und wette auf Kopf oder Zahl. Wie viel Glück hast du?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", + games8BallDesc: "Billard von oben. Klicken zum Zielen, gedrückt halten zum Aufladen. Versenke alle Kugeln mit möglichst wenigen Stößen.", + gamesArtilleryTitle: "Artillerie", + gamesArtilleryDesc: "Richte deine Kanone aus, berücksichtige den Wind und triff das Ziel. 5 Runden, die wenigsten Schüsse gewinnen.", gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + gamesLabyrinthDesc: "Entkomme dem Labyrinth, bevor dir die Züge ausgehen. Jedes Level wird größer und schwerer.", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "Friss alle Punkte, weiche den Geistern aus. Die Geister bewegen sich in Echtzeit — bleib in Bewegung!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", + gamesAudioPendulumTitle: "Klangpendel", + gamesAudioPendulumDesc: "Chaotischer Physiksimulator mit Echtzeit-Audiosynthese. Winkelgeschwindigkeiten werden zu Frequenzen, Spitzen zu Trommelschlägen. Keine zwei Simulationen klingen gleich.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Mische Visuals live auf zwei Decks: wähle je ein Muster, stelle Tempo, Farbe und Zoom ein, blende zwischen beiden über und lass es im Takt blitzen. Tippe das Tempo oder lass das Mikrofon den Puls vorgeben.", + gamesTetrisDesc: "Klassische fallende Blöcke. Räume Reihen ab, um zu punkten. Wie lange hältst du durch?", + gamesMinesweeperTitle: "Minesweeper", + gamesMinesweeperDesc: "Klassische Minensuche. Räume das Feld, um aufzusteigen — größere Felder, mehr Minen, höhere Punktzahl.", + gamesFilterScoring: "PUNKTESTAND", + gamesHallOfFame: "Ruhmeshalle", + gamesHallPlayer: "Spieler", + gamesHallScore: "Punktzahl", gamesNoScores: "Noch keine Ergebnisse.", modulesChatsLabel: "Chats", - modulesChatsDescription: "Modul zum Entdecken und Verwalten verschlüsselter Chats.", - chatMessageLabel: "CHATNACHRICHTEN", + modulesChatsDescription: "Modul zum Entdecken und Verwalten von Chats.", chatsTitle: "Chats", chatMineSectionTitle: "Meine Chats", chatRecentTitle: "Aktuelle Chats", @@ -3306,91 +4022,215 @@ module.exports = { chatDescription: "Beschreibung", chatCategory: "Kategorie", chatStatus: "STATUS", - chatFilterAll: "ALLE", - chatFilterMine: "MEINE", - chatFilterRecent: "AKTUELL", - chatFilterFavorites: "FAVORITEN", - chatFilterOpen: "OFFEN", - chatFilterClosed: "GESCHLOSSEN", chatCreate: "Chat Erstellen", - chatUpdate: "Chat Aktualisieren", - chatDelete: "Chat Löschen", + chatUpdate: "Aktualisieren", + chatDelete: "Löschen", chatClose: "Chat Schließen", chatVisitChat: "CHAT BESUCHEN", chatUntitled: "Unbenannter Chat", chatNoItems: "Keine Chats gefunden.", chatParticipants: "Teilnehmer", - chatStartChatting: "CHAT STARTEN!", - chatGenerateCode: "Code Generieren", - chatShareUrl: "URL Teilen", + chatMessagesLabel: "Nachrichten", + chatThreadsLabel: "Threads", chatCreatedAt: "ERSTELLT", - chatSearchPlaceholder: "Chats suchen...", + chatSearchPlaceholder: "In Chats suchen...", chatStatusOpen: "OFFEN", chatStatusInviteOnly: "NUR AUF EINLADUNG", chatStatusClosed: "GESCHLOSSEN", chatSendMessage: "Senden", + chatJumpLatest: "Neueste Nachricht", + chatReply: "Antworten", + chatReplyCancel: "Antwort abbrechen", + chatPin: "Anheften", + chatUnpin: "Lösen", + chatPinned: "Angeheftet", + chatPickChat: "Wähle einen Chat, um ihn zu öffnen", + chatNoneYet: "Noch kein Chat verfügbar.", + activitySearchPlaceholder: "In Aktivität suchen...", + trendingSearchPlaceholder: "In Trends suchen...", + opinionsSearchPlaceholder: "In Meinungen suchen...", + votesSearchPlaceholder: "In Abstimmungen suchen...", + gamesSearchPlaceholder: "In Spiele suchen...", + schoolTitle: "Schule", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "Eine Wiki-Seite, der du folgst, wurde bearbeitet.", + wikiBotRestoredTitle: "Eine frühere Version einer Wiki-Seite, der du folgst, wurde wiederhergestellt.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Ein neuer Student hat sich in deinen Kurs eingeschrieben.", + schoolBotInvitedTitle: "Du wurdest zu einem Kurs eingeladen.", + schoolBotAdmittedTitle: "Du wurdest in einen Kurs aufgenommen.", + schoolBotCertificateTitle: "Du hast ein Zertifikat erhalten.", + schoolBotPassedTitle: "Ein Student hat deinen Kurs bestanden.", + schoolBotLessonTitle: "Eine neue Lektion wurde veröffentlicht.", + invitesSchoolTitle: "Schule", + schoolEnrollLabel: "Kurs beitreten", + schoolExamRetryLater: "Du kannst es nach 24 Stunden erneut versuchen.", + schoolApproved: "BESTANDEN", + schoolExamPassMark: "Bestehensgrenze (0-10)", + schoolExamPassed: "BESTANDEN", + schoolExamPending: "Ausstehend", + schoolExamFailed: "NICHT BESTANDEN", + schoolExamAddQuestion: "Frage hinzufügen", + schoolExamCreateQuestion: "Frage erstellen", + schoolExamOption: "Option", + schoolExamCorrect: "Richtige Option", + schoolExamPoints: "Punkte", + schoolMaterialTextPlaceholder: "Notizen zu dieser Lektion...", + schoolMaterials: "Materialien", + schoolAddMaterial: "Material hinzufügen", + schoolNoMaterials: "Noch keine Materialien.", + bankYourSchoolBalance: "Deine Schul-Einnahmen", + schoolProgress: "Fortschritt", + schoolProgressMark: "Als abgeschlossen markieren", + schoolProgressDone: "Abgeschlossen", + schoolExams: "Prüfungen", + schoolExamCreate: "Prüfung erstellen", + schoolExamTitlePlaceholder: "Titel der Prüfung", + schoolExamHelp: "Eine Frage pro Zeile: Frage? ; Option ; richtige Option* ; Option", + schoolExamQuestionsPlaceholder: "Was ist SSB? ; Eine Datenbank ; Ein P2P-Protokoll* ; Ein Browser", + schoolExamSubmit: "Antworten absenden", + schoolExamScore: "Deine Punktzahl", + schoolNoScoresYet: "Noch keine Ergebnisse.", + schoolLessonUnit: "Einheit / Phase", + schoolLessonUnitPlaceholder: "z. B. Phase 1", + schoolLessonOrder: "Reihenfolge", + profileClearnetSchoolLabel: "Schule", + schoolStartDate: "Startdatum", + schoolSessionDate: "Sitzungsdatum", + schoolCourseChat: "Kurs-Chat", + agendaFilterSchool: "SCHULE", + agendaFilterCampaigns: "KAMPAGNEN", + agendaFilterLogistics: "LOGISTIK", + schoolCourseType: "Kursart", + schoolTypeOpen: "OFFEN", + schoolTypePaid: "KOSTENPFLICHTIG", + schoolTypeInvite: "NUR MIT EINLADUNG", + schoolLessonLocked: "Verschlüsselte Lektion — bitte den Lehrer um den Kursschlüssel.", + schoolGrantAccess: "Kursschlüssel senden", + schoolKeySent: "Schlüssel gesendet", + schoolOngoing: "Laufend", + schoolFilterApplied: "ANGEMELDET", + schoolFilterOpen: "OFFEN", + schoolFilterFavorites: "FAVORITEN", + schoolPendingPayment: "Zahlung ausstehend", + schoolPendingStudents: "Ausstehende Einschreibungen", + schoolViewBill: "Rechnung ansehen", + schoolPaymentNote: "Die Einschreibung erzeugt eine ECO-Rechnung (einen Transfer). Deine Einschreibung ist bestätigt, sobald der Transfer signiert ist.", + schoolCloseCourse: "Kurs schließen", + schoolReopenCourse: "Kurs wieder öffnen", + schoolDescription: "Lehre und lerne Kurse in deinem Netzwerk.", + schoolFilterAll: "ALLE", + schoolFilterMine: "MEINE", + schoolFilterTop: "TOP", + schoolFilterRecent: "NEUESTE", + schoolCreateButton: "Kurs Erstellen", + schoolCreateSectionTitle: "Kurs erstellen", + schoolUpdateSectionTitle: "Kurs aktualisieren", + schoolCourseTitlePlaceholder: "Name deines Kurses", + schoolCourseDescriptionPlaceholder: "Was wirst du in diesem Kurs unterrichten?", + schoolCourseImage: "Kursbild (max. Größe: 75MB)", + schoolTags: "Tags", + schoolTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + schoolPrice: "Preis (ECO)", + schoolVisibility: "Sichtbarkeit", + schoolVisibilityPublic: "Öffentlich", + schoolVisibilityInvite: "Auf Einladung", + schoolStatus: "Status", + schoolClosed: "Geschlossen", + schoolFree: "Kostenlos", + schoolCreate: "Kurs erstellen", + schoolUpdate: "Kurs aktualisieren", + schoolSearchPlaceholder: "In Schule suchen...", + schoolSortTitle: "Titel", + schoolNoCourses: "Keine Kurse gefunden.", + schoolStudents: "Studenten", + schoolTeacher: "Lehrer", + schoolCreatedAt: "ERSTELLT", + schoolLessons: "Lektionen", + schoolNoLessons: "Noch keine Lektionen.", + schoolEnrollToSee: "Schreibe dich ein, um die Lektionen dieses Kurses zu sehen.", + schoolEnrollButton: "EINSCHREIBEN", + schoolUnenrollButton: "Kurs verlassen", + schoolInviteRequired: "Dieser Kurs ist nur auf Einladung.", + schoolTeacherPanel: "Lehrerbereich", + schoolStudentPanel: "Schülerbereich", + schoolAddLesson: "Lektion erstellen", + schoolLessonTitlePlaceholder: "Titel der Lektion", + schoolLessonTextPlaceholder: "Beschreibe die Inhalte dieser Lektion...", + schoolInviteStudents: "Studenten einladen", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "EINLADEN", + schoolInvited: "Eingeladen", + schoolIssueCertificate: "Zertifikat erstellen", + schoolCertified: "Zertifiziert", + schoolCertificateTextPlaceholder: "Zertifikatsnotiz (optional)", + schoolLessonOpen: "Lektion öffnen", + schoolCertificatePdf: "PDF", + schoolCertificates: "Zertifikate", + schoolNoCertificates: "Noch keine Zertifikate ausgestellt.", + schoolEditButton: "Bearbeiten", + schoolDeleteButton: "Löschen", + typeSchool: "SCHULE", + schoolCourseLabel: "KURS", + emergencyLabel: "NOTFALL", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Kurse", + statsSchoolLesson: "Lektionen", + statsSchoolEnroll: "Einschreibungen", + statsSchoolCertificate: "Schulzertifikate", + modulesSchoolLabel: "Schule", + modulesSchoolDescription: "Modul zum Wissensaustausch.", + welcomeStepUxAction: "Ansicht speichern", + welcomeStepWorkflowTitle: "Welche Nutzung von OASIS hast du im Sinn?", + welcomeStepWorkflowText: "Oasis hat viele Module. Wähle die Konfiguration, die zu dir passt, und nur die nützlichen bleiben aktiv. Du kannst sie jederzeit unter Module anpassen.", + welcomeStepWorkflowAction: "Konfiguration speichern", + welcomeWorkflow_full: "Standard", + chatRateLimitTitle: "Zu viele Nachrichten", + chatRateLimitMessage: "Du hast das Nachrichtenlimit dieses Raums pro Stunde erreicht. Versuche es später erneut.", chatMessagePlaceholder: "Nachricht eingeben...", chatNoMessages: "Noch keine Nachrichten.", - chatLeave: "Chat Verlassen", - chatInviteCodeLabel: "Einladungscode eingeben", - chatJoinByInvite: "Beitreten", chatTitlePlaceholder: "Chat-Titel", chatDescriptionPlaceholder: "Chat-Beschreibung", - chatTagsPlaceholder: "tag1, tag2, tag3", + chatTagsPlaceholder: "Tags durch Kommas getrennt eingeben", chatImageLabel: "Bilddatei auswählen (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "Einladungscode", chatAuthor: "Autor", - chatCreated: "Erstellt", chatAddFavorite: "Zu Favoriten Hinzufügen", chatRemoveFavorite: "Aus Favoriten Entfernen", chatPM: "PM", chatStatusLabel: "Status", + chatTypeLabel: "Chat-Typ", chatCategoryLabel: "Kategorie", - chatParticipantsLabel: "Teilnehmer", calendarsTitle: "Kalender", calendarTitle: "Kalender", modulesCalendarsLabel: "Kalender", modulesCalendarsDescription: "Modul zum Entdecken und Verwalten von Kalendern.", typeCalendar: "KALENDER", - calendarFilterAll: "ALLE", - calendarFilterMine: "MEINE", - calendarFilterOpen: "OFFEN", - calendarFilterClosed: "GESCHLOSSEN", - calendarFilterRecent: "KÜRZLICH", - calendarFilterFavorites: "FAVORITEN", calendarCreate: "Kalender erstellen", calendarUpdate: "Aktualisieren", - calendarDelete: "Löschen", calendarTitleLabel: "Titel", calendarTitlePlaceholder: "Kalendertitel...", calendarStatusLabel: "Status", + calendarTypeLabel: "Kalender-Typ", calendarStatusOpen: "OFFEN", calendarStatusClosed: "GESCHLOSSEN", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "Frist", calendarTagsLabel: "Tags", - calendarTagsPlaceholder: "tag1, tag2...", + calendarTagsPlaceholder: "Tags durch Kommas getrennt eingeben", calendarParticipantsLabel: "Teilnehmer", - calendarParticipantsCount: "Teilnehmer", - calendarVisitCalendar: "Kalender besuchen", calendarCreated: "Erstellt", - calendarAuthor: "Autor", calendarJoin: "Beitreten", calendarGenerateInvite: "Einladung erstellen", - calendarInviteCodePlaceholder: "Einladungscode eingeben...", - calendarValidateInvite: "Code prüfen", - calendarJoined: "Beigetreten", - calendarAddDate: "Datum hinzufügen", - calendarAddNote: "Notiz hinzufügen", calendarDateLabel: "Datum", calendarDatePlaceholder: "Dieses Datum beschreiben...", - calendarNoteLabel: "Notiz", + calendarNoteLabel: "Notizen", calendarNotePlaceholder: "Notiz hinzufügen...", calendarFirstDateLabel: "Datum", calendarFirstNoteLabel: "Notizen", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "Intervall", + calendarIntervalNone: "Keine Wiederholung", + calendarIntervalWeekly: "Wöchentlich", + calendarIntervalMonthly: "Monatlich", + calendarIntervalYearly: "Jährlich", calendarFormDescription: "Beschreibung", calendarNoDates: "Noch keine Daten hinzugefügt.", calendarNoNotes: "Keine Notizen.", @@ -3398,8 +4238,8 @@ module.exports = { calendarsDescription: "Entdecke und verwalte Kalender in deinem Netzwerk.", calendarMonthPrev: "← Zurück", calendarMonthNext: "Weiter →", - calendarMonthLabel: "Daten", - calendarsShareUrl: "Teilen-URL", + calendarMonthNames: "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember", + calendarDayInitials: "Mo,Di,Mi,Do,Fr,Sa,So", calendarAllSectionTitle: "Kalender", calendarRecentSectionTitle: "Kürzliche Kalender", calendarFavoritesSectionTitle: "Favoriten", @@ -3409,11 +4249,10 @@ module.exports = { calendarCreateSectionTitle: "Neuen Kalender erstellen", calendarUpdateSectionTitle: "Kalender aktualisieren", calendarAddFavorite: "Zu Favoriten hinzufügen", - calendarDeleteNote: "Delete", + calendarDeleteNote: "Löschen", calendarRemoveFavorite: "Aus Favoriten entfernen", - calendarSearchPlaceholder: "Kalender suchen...", + calendarSearchPlaceholder: "In Kalender suchen...", calendarAddEntry: "Eintrag hinzufügen", - calendarLeave: "Kalender verlassen", statsCalendar: "Kalender", statsCalendarDate: "Kalenderdaten", statsCalendarNote: "Kalendernotizen", @@ -3424,6 +4263,146 @@ module.exports = { tribeContentAccessDenied: "Zugang Verweigert", tribeContentAccessDeniedMsg: "Dieser Inhalt gehört zu einem Stamm. Sie müssen Mitglied sein, um darauf zuzugreifen.", tribeViewTribes: "Stämme Anzeigen", + tribeFilesEmpty: "Noch keine Dateien.", + filesLabel: "Dateien", + profileClearnetFilesLabel: "Dateien", + filesTitle: "Dateien", + filesDescription: "Dateien in Ihrem Netzwerk erkunden und verwalten.", + fileAllSectionTitle: "Dateien", + fileMineSectionTitle: "Ihre Dateien", + fileRecentSectionTitle: "Neueste Dateien", + fileTopSectionTitle: "Top Dateien", + fileFavoritesSectionTitle: "Favoriten", + fileFilterAll: "ALLE", + fileFilterMine: "MEINE", + fileFilterRecent: "NEUESTE", + fileFilterTop: "TOP", + fileFilterFavorites: "FAVORITEN", + fileCreateSectionTitle: "Datei Hochladen", + fileUpdateSectionTitle: "Datei Aktualisieren", + fileCreateButton: "Datei Hochladen", + fileUpdateButton: "Aktualisieren", + fileAddFavoriteButton: "Zu Favoriten Hinzufügen", + fileRemoveFavoriteButton: "Aus Favoriten Entfernen", + fileFileLabel: "Datei auswählen (max: 75MB)", + calendarFilterRecent: "NEUESTE", + calendarFilterAll: "ALLE", + calendarFilterMine: "MEINE", + calendarFilterFavorites: "FAVORITEN", + calendarFilterOpen: "OFFEN", + calendarFilterClosed: "GESCHLOSSEN", + padFilterRecent: "NEUESTE", + padFilterAll: "ALLE", + padFilterMine: "MEINE", + padFilterOpen: "OFFEN", + padFilterClosed: "GESCHLOSSEN", + chatFilterRecent: "NEUESTE", + chatFilterAll: "ALLE", + chatFilterMine: "MEINE", + chatFilterFavorites: "FAVORITEN", + chatFilterOpen: "OFFEN", + chatFilterClosed: "GESCHLOSSEN", + licenseLabel: "Lizenz", + phoneTitle: "Telefon", + phoneDescription: "Verwalte deine Ende-zu-Ende-verschlüsselten Videoanrufe.", + phoneUnavailable: "Anrufe sind auf diesem Gerät nicht verfügbar.", + phoneCallButton: "Anrufen", + profileVisibilityPhone: "Telefonnummer", + phoneNumberUnknown: "Niemand in deinem Netzwerk hat die Nummer {number}.", + phoneNumberChoose: "Prüfe, wen du unter {number} anrufen möchtest:", + phoneAnswer: "Annehmen", + phoneSpeaker: "Lautsprecher", + phoneEarpiece: "Hörer", + phoneReject: "Ablehnen", + phoneHangup: "Auflegen", + phoneMute: "Stummschalten", + phoneUnmute: "Mikrofon an", + phoneCameraOn: "Kamera an", + phoneCameraOff: "Kamera aus", + settingsPhoneVideo: "Video", + settingsPhoneVideoOn: "In Anrufen und Räumen erlaubt", + settingsPhoneVideoOff: "Nur Audio", + phoneSilence: "Nicht hören", + phoneUnsilence: "Wieder hören", + phoneCalling: "Anruf an", + phoneIsCalling: "ruft dich an", + notifyNewPmFrom: "Neue PN von", + notifyNewPamFrom: "Neue PaM von", + notifyUpdateLabel: "Update", + updateInstalledLabel: "Installierte Version", + updateAvailableLabel: "Verfügbare Version", + updateChangesLabel: "Änderungen", + updateFilesLabel: "Dateien", + updateOriginLabel: "Quelle", + phoneRecording: "PaM wird aufgenommen für", + phoneLeaveMessage: "Niemand nimmt ab: Hinterlasse deine Nachricht nach dem Signalton.", + phoneLeaveMessageBusy: "Gerade in einem anderen Gespräch: Hinterlasse deine Nachricht nach dem Signalton.", + phoneLeaveMessageUnreachable: "Nicht erreichbar: Hinterlasse deine Nachricht nach dem Signalton.", + phoneConnecting: "Verbindung zu", + phoneSendPam: "PaM senden", + phoneCancel: "Abbrechen", + phoneSearchPlaceholder: "In Telefon suchen...", + phonePamTitle: "Audioaufnahmen", + phoneNew: "Neu", + phoneDelete: "Löschen", + phoneNoPams: "Du hast keine Audioaufnahmen.", + phoneHistoryTitle: "Verlauf", + phoneLatestTitle: "Letzte Anrufe", + phoneCreateButton: "Neuer Anruf", + phoneLimitsHint: "Bis zu {n} Bewohner pro Anruf; mit mehr als einem ist es ein gemeinsamer Anruf.", + phoneErrorPub: "Pubs können nicht angerufen werden.", + phoneJointCall: "Gemeinsamer Anruf", + phoneWith: "Mit", + phoneResult: "Ergebnis", + phoneWhen: "Wann", + phoneDuration: "Dauer", + phoneOutgoing: "Ausgehend", + phoneIncoming: "Eingehend", + phoneNoHistory: "Noch keine Protokolle.", + phoneOutcomeEnded: "Angenommen", + phoneOutcomeMissed: "Verpasst", + phoneOutcomeRejected: "Abgelehnt", + phoneOutcomeNoanswer: "Keine Antwort", + phoneOutcomeCancelled: "Abgebrochen", + phoneOutcomePam: "PaM gesendet", + phoneOutcomeFailed: "Fehlgeschlagen", + phoneDialAdd: "Hinzufügen", + phoneDialBack: "Löschen", + phoneDialClear: "Leeren", + phoneDialPlaceholder: "Nummer oder Oasis-ID", + phoneDialInhabitants: "Bewohner", + phoneDialRemove: "Entfernen", + phoneOwnNumber: "Das ist deine eigene Nummer.", + phoneSettingsDesc: "Wähle, wer dich anrufen kann.", + phoneDnd: "Nicht stören", + phoneOpen: "Offen", + phoneRecipientRefuses: "Der Bewohner, den du anrufen möchtest, nimmt keine Anrufe an.", + phoneRefusedNotice: "Diese Bewohner nehmen keine Anrufe an und wurden nicht angerufen:", + fileEmptyError: "Die Datei ist leer. Wähle eine andere Datei.", + fileTitleLabel: "Titel", + fileTitlePlaceholder: "Titel", + fileDescriptionLabel: "Beschreibung", + fileDescriptionPlaceholder: "Beschreibung", + fileTagsLabel: "Tags", + fileTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + fileSizeLabel: "Größe", + fileDownloadButton: "HERUNTERLADEN", + fileDetailsButton: "DETAILS", + fileNoFile: "Keine Datei-Datei", + fileSearchPlaceholder: "In Dateien suchen...", + fileSearchButton: "Suchen", + fileSortRecent: "Neueste", + fileSortOldest: "Älteste", + fileSortTop: "Meistgewählt", + fileNoMatch: "Keine passenden Dateien.", + fileUpdatedAt: "Aktualisiert", + typeFile: "DATEIEN", + modulesFilesLabel: "Dateien", + modulesFilesDescription: "Modul zum Entdecken und Verwalten von Dateien.", + tribeSectionFiles: "DATEIEN", + fileDescription: "Beschreibung", + fileDownload: "Herunterladen", + fileTypeLabel: "Typ", torrentsTitle: "Torrents", torrentsDescription: "Torrents in Ihrem Netzwerk erkunden und verwalten.", torrentAllSectionTitle: "Torrents", @@ -3451,47 +4430,91 @@ module.exports = { torrentTagsLabel: "Tags", torrentTagsPlaceholder: "Tags durch Kommas getrennt eingeben", torrentSizeLabel: "Größe", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "Torrent erzeugen", + torrentFromContentMissing: "Die Datei ist auf diesem Knoten noch nicht verfügbar. Versuche es später erneut.", + torrentDownloads: "Downloads", + torrentFetchOasis: "In Oasis herunterladen", + torrentSaveFile: "Datei speichern", + torrentSeeds: "Seeds", + seedsLabel: "Seeds", + seedAction: "Unterstützen", + torrentSourceFile: "Datei", + torrentStateQueued: "In Warteschlange", + torrentStateProbing: "Suche Seeds", + torrentStateDownloading: "Lädt herunter", + torrentStateDone: "Abgeschlossen", + torrentStateFailed: "Fehlgeschlagen", + torrentStateNoSource: "Keine Seeds verbunden, warte", + torrentStateCancelled: "Abgebrochen", + torrentNoDownloads: "Noch keine Downloads.", + torrentOriginalSection: "Original", + torrentCancel: "Abbrechen", + torrentDownloadButton: "HERUNTERLADEN", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "DETAILS", torrentNoFile: "Keine Torrent-Datei", noTorrents: "Keine Torrents gefunden.", - torrentSearchPlaceholder: "Titel, Tags, Autor suchen...", + noFiles: "Keine Dateien gefunden.", + torrentSearchPlaceholder: "In Torrents suchen...", torrentSearchButton: "Suchen", torrentSortRecent: "Neueste", torrentSortOldest: "Älteste", torrentSortTop: "Meistgewählt", torrentNoMatch: "Keine passenden Torrents.", - torrentMessageAuthorButton: "Nachricht an Autor", torrentUpdatedAt: "Aktualisiert", statsTorrent: "Torrents", + statsFile: "Dateien", typeTorrent: "TORRENTS", modulesTorrentsLabel: "Torrents", modulesTorrentsDescription: "Modul zum Entdecken und Verwalten von Torrents.", favoritesFilterTorrents: "TORRENTS", + favoritesFilterFiles: "DATEIEN", favoritesFilterMarket: "MARKT", + favoritesFilterEvents: "EREIGNISSE", + favoritesFilterForum: "FORUM", + favoritesFilterHousing: "WOHNEN", + favoritesFilterJobs: "JOBS", + favoritesFilterProjects: "PROJEKTE", + favoritesFilterReports: "BERICHTE", + favoritesFilterTasks: "AUFGABEN", + favoritesFilterTransfers: "ÜBERWEISUNGEN", + favoritesFilterVotes: "ABSTIMMUNGEN", favoritesFilterShopProducts: "SHOP-ARTIKEL", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Torrent Hochladen", - tribeMediaTypeTorrent: "Torrent", + tribeCreateFile: "Datei hochladen", settingsWishTitle: "Wunsch", - settingsWishDesc: "Konfiguriere den Wunsch deines Avatars.", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "Legt fest, wie du auf alle Inhalte zugreifst und wie du das Oasis-Netzwerk erlebst.", + settingsWishWhole: "Multiversum", + settingsWishMutuals: "Nur gegenseitige Unterstützung", + settingsWishOnlyLan: "Nur LAN", + settingsWishLocal: "Lokal", settingsPmVisibilityTitle: "Private Nachrichten", settingsPmVisibilityDesc: "Konfiguriere dein Niveau der Offenlegung für private Nachrichten.", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "Multiversum", + settingsPmVisibilityMutuals: "Nur gegenseitige Unterstützung", + mute: "Stummschalten", + unmute: "Stummschaltung aufheben", + relationshipMuted: "Von dir stummgeschaltet", + hideContent: "Für mich ausblenden", + unhideContent: "Wieder anzeigen", + hiddenByYou: "Von dir ausgeblendet", + settingsTabContent: "Inhalt", + settingsHiddenTitle: "Ausgeblendete Inhalte", + settingsHiddenDesc: "Stummgeschaltete Bewohner und ausgeblendete Inhalte.", + settingsMutedLabel: "Stummgeschaltete Bewohner", + settingsHiddenLabel: "Ausgeblendete Inhalte", + settingsMutedEmpty: "Niemand stummgeschaltet.", + settingsHiddenEmpty: "Nichts ausgeblendet.", + settingsPmVisibilityFollowing: "Nur Leute, denen ich folge", + settingsMentionsVisibilityTitle: "Erwähnungen", + settingsMentionsVisibilityDesc: "Wähle, wessen Erwähnungen dich erreichen.", pmMutualNotice: "Ausgang ist ein UX-Schutz. Eingang ist ein Aufmerksamkeitsfilter.", - pmBlockedNonMutual: "Du kannst PNs nur an Bewohner mit gegenseitiger Unterstützung senden.", inhabitantsPendingFollowsTitle: "Ausstehende Unterstützungsanfragen", inhabitantsPendingAccept: "Akzeptieren", inhabitantsPendingReject: "Ablehnen", - bxEncrypted: "VERSCHLÜSSELT", - bxEncryptedHexLabel: "Chiffretext (Vorschau)", tribeSectionGovernance: "GOVERNANCE", - tribeSubStatusPublic: "ÖFFENTLICH", - tribeSubStatusPrivate: "PRIVAT", - tribeSubInheritedPrivate: "PRIVAT (vom Haupt-Stamm geerbt)", tribePadInviteRequired: "Kein Zugriff auf das Pad. Bitte um Einladung.", tribeChatInviteRequired: "Kein Zugriff auf den Chat. Bitte um Einladung.", tribeGovernanceDesc: "Interne Governance dieses Stammes.", @@ -3530,129 +4553,117 @@ module.exports = { tribeGovNoRevocations: "Noch kein Widerruf", tribeGovNoHistorical: "Noch kein Eintrag", logsTitle: "Logbuch", - logsDescription: "Erfasse deine Erfahrung im Netzwerk.", - logsReadMore: "Mehr lesen", + logsDescription: "Führe ein privates Tagebuch deiner Erfahrungen.", logsViewTitle: "Logbuch", logsColumnType: "Typ", logsView: "Ansehen", - logsManualPrompt: "Schreibe dein Logbuch", logsUpdateButton: "Aktualisieren", - logsEditTitle: "Eintrag bearbeiten", - logsCreateDescription: "Erfasse deine Erfahrung...", + logsEditTitle: "Log aktualisieren", logsCreateTitle: "Eintrag erstellen", logsWriteButton: "Schreiben", logsTextPlaceholder: "Beschreibe deine Erfahrungen...", - logsTextField: "Text", - logsLabelPlaceholder: "Bezeichnung...", - logsLabelField: "Schreibe dein Logbuch (Bezeichnung)", - logsAiDisabledWarn: "Aktiviere das KI-Modul in /modules, um KI-Einträge zu verwenden.", - logsAiContextValue: "Blockchain-Tag", - logsAiContext: "Kontext", - logsAiModStatus: "AImod", - logsModeApply: "Anwenden!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "KI-Assistent", logsModeManual: "Manuell", logsModeAI: "KI", - logsColumnDelete: "Löschen", - logsColumnEdit: "Bearbeiten", logsColumnLog: "Logbuch", logsColumnDate: "Datum", logsDelete: "Löschen", - logsEdit: "Bearbeiten", + logsEdit: "Aktualisieren", logsFilterAlways: "IMMER", logsFilterYear: "LETZTES JAHR", logsFilterMonth: "LETZTER MONAT", logsFilterWeek: "LETZTE WOCHE", logsFilterToday: "HEUTE", - logsFilterRecent: "AKTUELL", - logsFilterAll: "ALLE", + logsComments: "Kommentare", + logsAuthorEntries: "Einträge", logsCreate: "Eintrag erstellen", logsExport: "Logbuch exportieren", - logsExportOne: "Exportieren", + logsExportOne: "Log exportieren", logsViewDetails: "Details ansehen", logsGenerateButton: "Text generieren", - logsSearchText: "In Logbüchern suchen...", + logsSearchText: "In Logbuch suchen...", logsSearchAnyType: "Beliebiger Typ", logsSearchButton: "Suchen", logsEmpty: "Noch keine Einträge.", + logsNothingNew: "Es gibt keine neuen Aktionen zum Aufzeichnen.", modulesLogsLabel: "Logbuch", - modulesLogsDescription: "Modul, um (über einen KI-Assistenten) deine Erfahrungen aufzuzeichnen.", + modulesLogsDescription: "Modul für ein privates Tagebuch deiner Erfahrungen.", statsLogsTitle: "Logbuch", - statsLogsEntries: "Einträge", typeLog: "LOGBUCH", - blockchainCycle: "Zyklus", larpTitle: "L.A.R.P.", larpDescription: "Eine Live-Rollenspiel-Schicht für kollaboratives Experimentieren.", + larpNoHousesMatch: "Keine Häuser entsprechen deiner Suche.", + larpSearchPlaceholder: "In L.A.R.P. suchen...", larpAllHouses: "Häuser:", larpFilterRuling: "REGIERT", larpFilterHouses: "HÄUSER", larpFilterRules: "FAQ", larpRulesTitle: "L.A.R.P.-FAQ", - larpRulesEntryTitle: "Starthaus", larpRulesEntryText: "Jede Bewohnerin beginnt standardmäßig in ACADEMIA. Von ACADEMIA aus wählst du ein Haus im Panel \"Haus beitreten\": dadurch öffnet sich seine Eintrittsprüfung. Beantworte die 10 Fragen und sende sie ab. Wenn du die Schwelle (7/10) erreichst, wirst du automatisch in das Haus aufgenommen; andernfalls wirst du abgelehnt und bleibst in ACADEMIA. In beiden Fällen speichert das System deine OasisID und den Zyklus des Versuchs und verhindert weitere Versuche, bis die Abklingzeit von 30 Zyklen abgelaufen ist.", - larpRulesLeaveText: "Mitglieder eines beliebigen Hauses können jederzeit von ihrer Hausseite zu ACADEMIA zurückkehren; dies setzt ihre Mitgliedschaft zurück, hebt aber die Test-Abklingzeit nicht auf.", - larpRulesGovernanceTitle: "Regierungswand", larpRulesGovernanceText: "In ihrem Zyklus trägt das regierende Haus das \"Regiert\"-Abzeichen und ist das einzige, dessen Wand öffentlich unter dem REGIERT-Tab erscheint.", - larpRulesSignTitle: "L.A.R.P.-Emblem", + larpRulesWallText: "Jedes Haus hat eine Wand, auf der seine Mitglieder schreiben. Die Wand eines Hauses ist privat, außer der des regierenden Hauses und der der ACADEMIA, die alle lesen können.", larpRulesSignText: "Bewohner können dies aktivieren (unter /profile/edit > Sensoren), um das Bild ihres aktuellen Hauses als Siegel auf ihrem Profil, in der Autoren- und Bewohnerkarte anzuzeigen. Das Siegel verlinkt zur Hausseite.", larpPostsTitle: "Wand", larpPostsEmpty: "Noch keine Beiträge.", - larpPostLabel: "Neuer Beitrag", larpPostPlaceholder: "Was hat dieses Haus zu sagen?", - larpPostPublic: "Öffentlich (für alle sichtbar, sonst nur für Mitglieder)", larpPostSubmit: "Veröffentlichen", - larpPostMembersOnly: "Nur Mitglieder", larpCycleLabel: "Zyklus:", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", + bankRulesArchTaxNote: "Die ARCH-Steuer spiegelt die langfristigen Kosten für Wachstum und Pflege des Netzwerkarchivs wider: Je älter deine Daten im Netzwerk sind, desto größer dein Anteil an den Wartungskosten.", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", + bankRulesChipNote: "Der Chip jedes Elements wird im Verhältnis zur größten jemals im Netzwerk beobachteten Nachricht eingefärbt und durch die Zahl der aktiven Bewohner abgeschwächt — ein größeres Netzwerk verteilt die Last und schiebt mehr Elemente in den grünen Bereich.", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", + bankRulesEcoTaxNote: "Die ECO-Steuer wird vom Brutto-UBI abgezogen, wenn die Zuteilung der Epoche berechnet wird. Die abgezogenen ECO werden in dieser Epoche nicht umverteilt — sie bleiben im Pub-Guthaben und speisen den Pool der nächsten Epoche über die Obergrenzen reserveMin / alpha.", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", + bankRulesKarmaNote: "Karma ergibt sich aus deinen Aktionen (Beiträge, Stimmen, Beteiligung an Parlament/Gerichten usw.) mit einem zeitlichen Abklingfaktor, abzüglich der von deinem Feed erzeugten Gramm CO2. Was deine Inhalte erhalten, zählt ebenfalls: positive Meinungen, Stimmen, Reaktionen und Wiedergaben geben Karma, negative Meinungen oder Stimmen ziehen es ab, sobald mehrere Bewohner übereinstimmen.", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", - gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", + bankTaxesArchTaxFootprintNote: "Die ARCH-Steuer spiegelt die Kosten für Wachstum und Pflege des Netzwerkarchivs wider. Sie wird aus der Zeitspanne zwischen deinem ersten veröffentlichten Block und dem neuesten Block im Netzwerk berechnet — je länger deine Geschichte im Archiv lebt, desto größer dein Anteil an den Wartungskosten.", + bankTaxesArchTaxNoteBold: "der direkten Senkung der ARCH-Steuer", + bankTaxesArchTaxNoteIntro: "Deine ARCH-Steuer wird von dem Überschuss abgezogen, den das Netzwerk über dein UBI hinaus erzeugt; der Basiswert des UBI gilt als unverrückbares Minimum. Die Steuer wird nie von dem festen Betrag abgezogen, der jedem Bewohner als UBI zusteht. Die abgezogenen Mittel speisen den Algorithmus zur Umverteilung des Wohlstands und fließen über deren UBI-Ansprüche an andere Bewohner zurück. Neuere Bewohner und jene, die sich speziell Archivierungs-, Replikations- und Wartungsaufgaben widmen, haben wahrscheinlich die Aufgabe, das Netzwerkarchiv in deinem Namen gesund zu halten. Oder sie widmen sich ", + bankTaxesArchTaxNoteOutro: " durch Pruning-Vereinbarungen, Deduplizierung redundanter Daten und gemeinsame Wartungsinfrastruktur, und ihr fortlaufender Beitrag kann durch Netzwerkkonsens verlangt werden.", + bankTaxesUserNoteChips: "Der ECO-Steuer-Chip neben jedem Element wechselt die Farbe (grün / gelb / rot) je nach Größe des Elements im Verhältnis zum größten jemals im Netzwerk gesehenen Block, abgeschwächt durch die Zahl der aktiven Bewohner — ein größeres Solarpunk-Netzwerk verteilt die Last und schiebt mehr Elemente in den grünen Bereich.", + bankTaxesUserNoteOtherParams: "Die ECO-Steuer wird derzeit aus dem CO2-Fußabdruck jedes Blocks abgeleitet, kann aber mit der Zeit weitere Parameter einbeziehen — Energie für die Replikation, redundanter Speicher zwischen Peers, Blob-Bandbreite, Rechenaufwand für die Entschlüsselung, Mining-Fußabdruck zugehöriger Transaktionen oder jede andere messbare Last, deren Bewertung das Netzwerk vereinbart.", + gamesHallDate: "Datum", + gamesNewRecord: "Neuer Rekord!", + indexingRefreshNote: "Diese Seite wird alle 10 Sekunden neu geladen.", + indexingMessage: "Oasis synchronisiert das verteilte Netzwerk aus Knoten. Bitte warten.", + indexingTitle: "Synchronisierung", + padsNoItems: "Keine Pads gefunden.", + padVersionHistory: "Versionsverlauf", + padAttributionView: "Nach Autorin", + padVersionView: "Ansehen", statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", + statsChatMessage: "Chat-Nachrichten", + statsGameScore: "Spiele", + statsWiki: "Wiki-Seiten", statsPad: "Pads", - statsPadEntry: "Pad entries", + statsPadEntry: "Pad-Einträge", bankTaxesLookupNote: "Gib eine Block-ID ein, um sie in deinem lokalen Feed nachzuschlagen.", bankTaxesUserNoteChipsClick: "Klicke auf einen Chip, um seinen Block im Blockexplorer zu inspizieren.", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "Einladungscode", larpRulesInviteEntryText: "Hausmitglieder können Einladungscodes erzeugen, die direkten Zugang zum Haus gewähren.", larpRulesOneHouseText: "Du kannst zu jedem Zeitpunkt höchstens einem Haus angehören. Gehörst du keinem an, bist du in ACADEMIA. Jede Nicht-ACADEMIA-Hausseite zeigt seinen Mitgliedern eine Schaltfläche „Haus verlassen\", die die Mitgliedschaft zurück nach ACADEMIA setzt. Das Verlassen hebt die Test-Abklingzeit NICHT auf.", - larpRulesOneHouseTitle: "Ein Haus auf einmal", - larpRulesTestEntry: "WILL-Test", - larpRulesTestEntryText: "Jede Option gewichtet ein oder mehrere Häuser; beim Absenden zählt das System die Punkte und nimmt dich automatisch in das Haus mit der höchsten Punktzahl auf.", + larpRulesTestEntryText: "Im WILL-Test gewichtet jede Option ein oder mehrere Häuser; beim Absenden zählt das System die Punkte und nimmt dich automatisch im Haus mit der höchsten Punktzahl auf.", larpWillTestHint: "Nach Abschluss wirst du dem Haus zugewiesen, das am besten zu deinen Antworten passt. Deine einzelnen Antworten werden lokal verarbeitet und nie gespeichert — nur die resultierende Hauszuweisung wird in deinem Feed veröffentlicht.", larpWillTestTitle: "WILL-Test", - settingsLanDesc: "Diesen Peer periodisch an andere Oasis-Instanzen im selben lokalen Netzwerk ankündigen. Deaktivieren, um UDP-Broadcasts zu stoppen.", + settingsLanDesc: "Diesen Peer periodisch an andere Oasis-Instanzen im selben lokalen Netzwerk ankündigen.", settingsLanEnable: "LAN-Broadcasting aktivieren", settingsLanTitle: "LAN-Broadcasting", settingsReplicationTitle: "Replikation", settingsReplicationDesc: "Konfiguriere die Anzahl der Hops, die dein Peer von deinem eigenen Feed aus beim Replizieren von Inhalten folgt.", settingsReplicationHopsLabel: "Hops", + settingsBlobCacheTitle: "Medien-Cache", + settingsBlobCacheDesc: "Inhalte anderer Bewohner werden auf diesem Gerät gespeichert. Lege fest, wie viel Platz sie belegen dürfen.", + settingsBlobCacheUsage: "Aktuelle Belegung", + settingsBlobCacheFiles: "Dateien", + settingsBlobCacheLimit: "Limit", + settingsBlobCacheUnlimited: "Unbegrenzt", + settingsBlobCacheCleanNow: "Jetzt aufräumen", + settingsBlobCacheCleaned: "{n} Dateien entfernt, {size} freigegeben.", aiApproveTagsLabel: "Tags (kommagetrennt)", aiApproveTagsPlaceholder: "z. B. oasis, governance, Ökologie", aiApproveRatingLabel: "Bewertung", @@ -3663,40 +4674,32 @@ module.exports = { aiExchangeMarkHelpful: "+1 hilfreich", larpCyclesUntilRuling: "Nächster Regierungszyklus", larpVisitTribe: "Stamm besuchen", + larpStartJourney: "Reise beginnen", larpGoverning: "Regiert:", + larpStartHere: "Fang hier an:", larpMyHouse: "Mein Haus:", larpMembersCount: "Mitglieder", - larpMembersTitle: "Mitglieder", - larpNoMembers: "Noch keine Mitglieder.", larpRolesLabel: "Rollen", larpFunctionLabel: "Funktion", larpMonthLabel: "Regierungszyklus", larpLeaveToAcademia: "Zurück zu ACADEMIA", - larpAcademiaJoinTitle: "Haus beitreten", - larpAcademiaJoinHint: "Lege den psychologischen Profiltest ab, um dem Haus zugewiesen zu werden, das am besten zu dir passt, oder löse einen Einladungscode ein, den dir ein Mitglied eines Hauses gegeben hat.", - larpTakeTestButton: "Profiltest ablegen", + larpAcademiaJoinTitle: "L.A.R.P.-Häuser", larpInviteRedeem: "Haus beitreten", larpInviteCreate: "Einladungscode erzeugen", larpInvitePlaceholder: "Einladungscode", larpInviteBannerTitle: "Neuer Einladungscode", larpInviteBannerHint: "Teile diesen Code mit jemandem in ACADEMIA. Er läuft in 30 Zyklen oder nach einmaliger Nutzung ab.", - larpTestAssignedHouse: "Zugewiesenes Haus", larpTestRankingTitle: "Punktzahl nach Haus", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "Haus-Einladungscode", - invitesHouseJoinButton: "Haus beitreten", - ecoTaxLabel: "ECO Tax", - ecoinTaxLabel: "ECOin Tax", + ecoTaxLabel: "ECO-Steuer", bankTaxes: "Steuern", - bankOverviewYourTaxes: "Deine Steuern", - bankTaxesNetworkTitle: "Netzwerksteuern", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesUserTitle: "Deine Steuern", + bankOverviewYourTaxes: "Steuern gesamt", + bankTaxesEcoTaxTitle: "ECO-Steuer", + bankTaxesArchTaxTitle: "ARCH-Steuer", bankTaxesUserAmount: "Deine ECO Tax (zu zahlender Beitrag)", bankTaxesArchTaxUserAmount: "Deine ARCH Tax (zu zahlender Beitrag)", bankTaxesArchTaxFirstBlock: "Alter deines ersten Blocks", - bankTaxesArchTaxRate: "Rate", + bankTaxesArchTaxRate: "Satz", bankTaxesTotalBlocks: "Blöcke gesamt", bankTaxesTotalBytes: "Bytes gesamt", bankTaxesTotalCarbon: "Kohlenstoff gesamt", @@ -3710,7 +4713,7 @@ module.exports = { bankTaxesTotalLifetime: "Total (Lebenszeit)", bankTaxesTotalAnnual: "Total (jährlich)", bankTaxesTotalMonthly: "Total (monatlich)", - bankTaxesRate: "Rate", + bankTaxesRate: "Satz", bankTaxesSpan: "Beobachtungszeitraum", bankTaxesLookupTitle: "Block inspizieren", bankTaxesLookupPlaceholder: "Block-ID (z. B. %abc...=.sha256)", @@ -3720,7 +4723,7 @@ module.exports = { bankTaxesLookupType: "Typ", bankTaxesLookupSize: "Größe", bankTaxesLookupCarbon: "CO₂-Fußabdruck", - bankTaxesLookupEcoin: "ECO Tax", + bankTaxesLookupEcoin: "ECO-Steuer", bankExchangeEcoTaxAnnual: "ECOin-Steuern (jährlich)", bankExchangeEcoTaxMonthly: "ECOin-Steuern (monatlich)", bankTaxesTypesLabel: "Wähle, welche Steuern du zahlen willst", @@ -3728,8 +4731,8 @@ module.exports = { bankRulesPoolTitle: "Monatlicher Pool", bankRulesShareTitle: "Anteil pro Nutzer", bankRulesKarmaTitle: "Karma (Engagement-Score)", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesArchTaxTitle: "ARCH Tax", + bankRulesEcoTaxTitle: "ECO-Steuer", + bankRulesArchTaxTitle: "ARCH-Steuer", bankRulesChipTitle: "ECO-Tax-Chip-Farbband", bankRulesEpochKind: "Epoch-Granularität", bankRulesAlpha: "Alpha (max. Anteil am Pub-Balance pro Epoch)", @@ -3740,32 +4743,25 @@ module.exports = { bankRulesWMax: "Höchstgewicht (w_max)", bankRulesFloor: "Sockel pro Nutzer (brutto)", bankRulesCapUser: "Obergrenze pro Nutzer pro Epoch", - bankRulesEcoTaxRate: "Rate", - bankRulesArchTaxRate: "Rate", + bankRulesEcoTaxRate: "Satz", + bankRulesArchTaxRate: "Satz", bankRulesCarbonFactor: "Kohlenstofffaktor", - statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Noch keine Tombstones im Netzwerk.", blockchainBlockNotAvailable: "Dieser Block ist in deinem lokalen Feed nicht verfügbar. Er gehört möglicherweise einem Peer, von dem du noch nicht replizierst.", blockchainBackToBlockexplorer: "Zurück zum Blockexplorer", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeTitle: "BCS-Transcode", - audioTranscodeDetailTitle: "Transcode", + audioTranscodeButton: "TRANSKODIEREN", + audioTranscodeDetailTitle: "Transkodieren", audioTranscodeDetailDescription: "Entschlüssle die eingebettete Nutzlast und die ursprüngliche Blockchain-Kompositionskarte.", - audioTranscodeStegoTitle: "In die Wellenform eingebettet", - audioTranscodeStegoOasisId: "Von", audioTranscodeStegoTimestamp: "Erzeugt am", audioTranscodeStegoMessage: "TEXT", audioTranscodeStegoEmpty: "(keine)", audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "Aus diesem Audio konnte keine steganographische Nutzlast entschlüsselt werden.", audioTranscodeCompositionEmpty: "Dieses Audio enthält keine gespeicherte Blockchain-Komposition.", - audioBackToAudio: "Zurück zum Audio", audioBackToBcs: "Zurück zu BCS", - audioAuthorLabel: "Autor", melodyCompositionTitle: "Blockchain-Kompositionskarte", melodyFilterMine: "MEINS", - melodyByLabel: "Von", melodyAllEmpty: "Noch keine BCS-Kompositionen anderer Bewohner.", melodyNoteUnavailable: "Block ist nicht in deinem lokalen Feed", melodyUploadBcsTitle: "Veröffentlichen", @@ -3776,7 +4772,6 @@ module.exports = { melodyUploadBcsNameNote: "Das Audio wird unter dem automatisch generierten Namen veröffentlicht", larpLastAttemptTitle: "Dein letzter Versuch", larpLastAttemptHouse: "Haus", - larpLastAttemptResult: "Ergebnis", larpLastAttemptWhen: "Wann", larpLastAttemptCooldown: "Nächster Versuch in", larpTestTitle: "Eintrittsprüfung", @@ -3785,37 +4780,21 @@ module.exports = { larpTestCooldownActive: "Nächster Test verfügbar in", larpTestCooldownDays: "Zyklen", larpTestResultTitle: "Testergebnis", - larpTestPassed: "Test bestanden — willkommen!", - larpTestFailed: "Test nicht bestanden", larpTestScore: "Punktzahl", larpTestNextAttempt: "Du kannst in 30 Zyklen einen weiteren Test absolvieren.", larpGoToHouse: "Zu deinem Haus", larpBackToAcademia: "Zurück zu ACADEMIA", - larpBackToHouses: "◀ Häuser", larpBadgeYou: "Du", larpBadgeRuling: "Regiert", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Modul für die Live-Rollenspielschicht von Oasis (die 9 Häuser).", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - chatLabel: "CHATS", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "Modul für die Live-Rollenspiel-Ebene.", larpProfileQ1: "Wenn du vor einem komplexen Problem stehst, was tust du zuerst?", larpProfileQ1O1: "Mit anderen reden, um Konsens zu finden", larpProfileQ1O2: "Einen Prototyp zum Testen bauen", larpProfileQ1O3: "Die Literatur erforschen", larpProfileQ1O4: "Das System sabotieren, das es verursacht", larpProfileQ2: "Was gibt deiner täglichen Arbeit Sinn?", - larpProfileQ2O1: "Die Menschen verteidigen, die ich liebe", + larpProfileQ2O1: "Die Bewohner verteidigen, die ich liebe", larpProfileQ2O2: "Etwas Greifbares schaffen", larpProfileQ2O3: "Leben heilen oder nähren", larpProfileQ2O4: "Worte und Ideen formen", @@ -3882,7 +4861,52 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "Wähle den UX-Navigationsmodus für deine Oberfläche.", uxModeMenus: "Blöcke", - uxModeAINav: "AI", + uxModeChats: "Unterhaltungen", + uxModeFeed: "Microblogging", + feedTrendingTitle: "Trend-Tags", + feedActiveTitle: "Aktive Bewohner", + cnBrandSub: "Frei · P2P · Föderiert", + cnSyncedPeers: "Synchronisierte Peers", + cnLicense: "Lizenz", + cnNotAccessible: "Der Inhalt ist im Moment nicht erreichbar.", + cnUntitled: "Ohne Titel", + cnSearchPlaceholder: "Suchen…", + cnHubTitle: "Clearnet-HUB", + cnHubDescription: "Öffentliche Inhalte, geteilt von den Bewohnern dieses Oasis-Netzwerks.", + cnHubEmpty: "Es wurden noch keine öffentlichen Inhalte im Clearnet geteilt.", + cnPublicContent: "Inhalte", + cnTribesContent: "Stämme", + cnNetworkContent: "Netzwerk", + cnCategoryEmpty: "Keine Inhalte in dieser Kategorie.", + cnInhabitantEmpty: "Dieser Bewohner hat noch keine Inhalte im Clearnet veröffentlicht.", + cnPrevPage: "← Zurück", + cnNextPage: "Weiter →", + cnPageOf: "Seite {page} von {pages}", + cnMoreInfo: "Mehr Infos →", + cnOpenCourse: "Offener Kurs", + cnAuction: "AUKTION", + cnExchange: "TAUSCH", + cnRssDescription: "veröffentlicht von den Bewohnern dieses HUBs", + cnKindAudio: "Audio", + cnKindBlog: "Blog", + cnKindBookmark: "Lesezeichen", + cnKindDocument: "Dokument", + cnKindEvent: "Termin", + cnKindFeed: "Feed", + cnKindImage: "Bild", + cnKindJob: "Stelle", + cnKindMarket: "Markt", + cnKindPodcast: "Podcast", + cnKindProject: "Projekt", + cnKindCourse: "Kurs", + cnKindShop: "Shop", + cnKindTorrent: "Torrent", + cnKindFile: "Datei", + cnKindVideo: "Video", + cnKindWiki: "Wiki", + cnKindRoom: "Raum", + cnKindMap: "Karte", + uxModeAINav: "KI", } } \ No newline at end of file diff --git a/src/client/assets/translations/oasis_en.js b/src/client/assets/translations/oasis_en.js index e14c720..6361b35 100644 --- a/src/client/assets/translations/oasis_en.js +++ b/src/client/assets/translations/oasis_en.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { en: { - bbAdd: "Add", - bbQuickActions: "Quick actions", - bbPickTitle: "Pin a module", - bbPickDesc: "Choose a module for the + slot in the bottom bar.", - bbPickNone: "None", - bbManage: "Edit", - bbYourBar: "Your bar", - bbRemove: "Remove", - bbFull: "Maximum reached", - bbDone: "Done", - activityChangeFilter: "Change filter", - emptyConnectHint: "Connect to a pub to sync with the network.", - emptyConnectCta: "Connect to a pub", - menuCommunity: "Community", - activityGroupCommunity: "Community & Governance", - activityGroupOrg: "Organization", - activityGroupComm: "Communication", - activityGroupEconomy: "Economy", - activityGroupMedia: "Content & Media", - activityGroupOther: "Others", - languageName: "English", shopOrderStatusPaid: "Payment received", shopOrderStatusReceived: "Received", shopOrderMarkPaid: "Payment received", @@ -32,20 +11,16 @@ module.exports = { shopMyOrdersEmpty: "You have no purchases yet.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Seller", - shopOrderPaymentConcept: "Payment", shopOrderInvoice: "Invoice", shopOrderInvoiceConcept: "Invoice", shopOrderStatusPending: "Pending", shopOrderStatusAccepted: "Accepted", shopOrderStatusRejected: "Rejected", shopOrderStatusShipped: "Shipped", - shopOrderStatusDelivered: "Delivered", shopOrderAccept: "Accept", shopOrderReject: "Reject", shopOrderMarkShipped: "Mark shipped", - shopOrderMarkDelivered: "Mark delivered", shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "Smart-contract", shopOrderContractConcept: "Shop order", shopOrdersPending: "Pending orders", all: "All", @@ -53,7 +28,7 @@ module.exports = { recent: "Recent", anonymous: "Anonymous", unnamed: "Anonymous", - authorLabel: "AUTHOR", + authorLabel: "Author", delete: "Delete", save: "Save", vote: "Vote", @@ -75,18 +50,17 @@ module.exports = { viewJson: "View JSON", matchScore: "Match score", preferencesLabel: "Preferences", - inhabitantProfileTitle: "Inhabitant profile", + inhabitantProfileTitle: "Inhabitant", contentWarningLabel: "Content warning", invalidPost: "Invalid content", invalidPostHint: "This message has invalid/empty text.", - spreadBy: "By", spreadChron: "Spread", spreaded: "Spread", - spreadMore: "more", spreadContentUnavailable: "Content not yet available (pending replication)", filteredByTag: "Filtered by tag", filteredByTagTitle: "Filtered by tag", feedPublishedSuccess: "Feed published successfully!", + feedMastodonFailed: "Published in Oasis, but it could not be sent to Mastodon.", tribeFeedSent: "Message sent successfully!", tribeContentEncrypted: "Encrypted content", tribeTorrentsEmpty: "No torrents, yet.", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "No severity", calendarDeleteDate: "Delete Date", calendarIntervalUntil: "Until", - bookmarkCategory: "Category", bookmarkLastVisit: "Last visit", profileClearnetBookmarksLabel: "Bookmarks", - forumFilterHot: "Hot", invitesPort: "Port", - currentlyUnrecheable: "ERROR!", - peerHostValidation: "Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.example.com)", + peerHostValidation: "Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.solarnethub.com)", peerPortValidation: "Port 1-65535", peerKeyValidation: "SSB ed25519 public key (@<44 chars base64>=.ed25519)", inboxJobUnsubscribedTitle: "Unsubscription from your job offer", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "Task description", exportPasswordLabel: "Export password", importPasswordPlaceholder: "Enter password", - searchCasesPlaceholder: "Search cases…", + searchCasesPlaceholder: "Search in Courts...", shopBuyDeliveryAddressPlaceholder: "Delivery address", shopBuyNotesPlaceholder: "Notes", bankTaxesUserNoteBold: "reducing the ECO tax directly", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "Support case", courtsSettlementAcceptBtn: "Accept settlement", courtsSupportCount: "Supporters", - courtsVerdictVoteTitle: "Vote on verdict", - courtsVerdictVoteLabel: "Verdict vote", - courtsVerdictVoteAccept: "Accept", - courtsVerdictVoteReject: "Reject", - courtsVerdictVoteSubmit: "Submit vote", reportsSetStatus: "Set status", reportsStatusClosed: "CLOSED", statsAvgPerInhabitant: "Avg per inhabitant", statsCarbonMaxAnnual: "Annual max estimate", statsCarbonNetwork: "Network total", statsCarbonOfEstMax: "of estimated max capacity", + statsCarbonYearProgress: "of the year elapsed", + statsNetworkTitle: "Network", + statsStorageTitle: "Storage", + statsEcoTaxTitle: "ECO Tax", + statsAccountTitle: "Account", + statsAveragesTitle: "Averages", statsCarbonOfNetwork: "of network total", statsCarbonUser: "Your footprint", statsContentCountColumn: "Count", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "Top Tags", statsTopTypesTitle: "Top Content Types", statsTotalMsgs: "Total messages", - feedViewDetails: "View details", - eventFileLabel: "Attach image", - taskFileLabel: "Attach image", - courtsRespondentRequired: "Respondent ID is required", extended: "Multiverse", extendedDescription: [ "When you support someone you may download posts from the inhabitants they support, and those posts show up here, sorted by recency.", @@ -215,47 +183,76 @@ module.exports = { ], profile: "Avatar", inhabitants: "Inhabitants", - peersReplicatedFeeds: "Replicated feeds", + peersReplicatedFeeds: "Replicated", + peersTorColumn: "TOR", + peersTorYes: "Reachable over Tor", + peersTorNo: "Not reachable over Tor", + peersOasisId: "ID", + peersOasisVersion: "Version", graphos: "Graphos", graphosDescription: "Interactive map of the network around you.", + graphosFederation: "Federation", + graphosFedPubs: "PUBs", + graphosFedUnreachable: "Unreachable", + graphosFedLinks: "Mutual links", + graphosFedIsolated: "Isolated", + graphosFedNoData: "No data", + graphosFedMutual: "Mutual", + graphosFedOneWay: "One way", + graphosFedTransport: "Network", + graphosFedWith: "Federated with", + graphosZoom: "Enlarge", + graphosFedInhabitants: "Inhabitants", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "Your node does not know any PUB yet.", graphosViewingGraphOf: "Viewing", graphosBackToMine: "← My network", graphosShowing: "Showing", graphosYou: "You", graphosTotalNodes: "Total nodes", - manualMode: "Manual Mode", mentions: "Mentions", mentionsDescription: [ - strong("Posts that @mention you"), - ", sorted by recency.", + strong("Every message that @mention you"), + ".", ], - nextPage: "Next", - previousPage: "Previous", noMentions: "You haven't received @mentions, yet.", + mentionsSearchPlaceholder: "Search in Mentions...", privateReminders: "REMINDERS", + inboxFilterPms: "PMs", + inboxFilterNotifications: "Notifications", + inboxMarkRead: "Mark as Read", + inboxMarkUnread: "Mark as Unread", + inboxUnreadChip: "Unread", + inboxMarkAllRead: "Mark all as Read", + inboxDeleteShown: "Delete all", + inboxArchive: "Archive", + inboxUnarchive: "Unarchive", + inboxFilterArchived: "Archived", + inboxEarlierMessages: "earlier messages", + inboxSortThread: "By thread", + inboxSortRecent: "Newest first", + inboxAllBots: "Bots", + settingsInboxBotsTitle: "Inbox Notifications", + settingsInboxBotsDesc: "Choose which bots can notify you in your Inbox. Unchecked bots are hidden and their notices are not counted.", inboxFiltersLabel: "Filters:", inboxToggleToMutuals: "Switch to mutuals", inboxToggleToWhole: "Switch to whole", - privateFrom: "From", - privateTo: "To", privateDate: "Date", pmReply: "Reply", pmReplies: "replies", - pmNew: "new", - pmMarkRead: "Mark as read", inReplyTo: "IN REPLY TO", + forumInThread: "IN THREAD", pmPreview: "Preview", pmPreviewTitle: "Message preview", peers: "Peers", searchDescription: "Description", - imageSearch: "Image Search", searchFromLabel: "From", searchToLabel: "To", - searchMinOpinionsLabel: "Min opinions", - searchInhabitantLabel: "Inhabitant (Oasis ID)", searchQueryLabel: "Query", - searchPerPageLabel: "Results per page", + searchPerPageLabel: "Results", settings: "Settings", + settingsDescription: "Set your network environment.", continueReading: "Continue reading", moreComments: "more comment", readThread: "read the rest of the thread", @@ -265,26 +262,12 @@ module.exports = { melodyDescription: 'Play the melody of your blockchain — each block becomes a note.', melodyEmpty: 'No notes yet — your blockchain has not produced any block.', melodyCompositionTitle: 'Blockchain Composition Map', - melodyCompositionHelp: 'Each block of your blockchain becomes a musical note based on its type and size.', - melodyStatsTitle: 'Type breakdown', - melodyInhabitantLabel: 'Inhabitant', melodyTotalBlocks: 'Notes', - melodyType: 'Type', - melodyCount: 'Blocks', melodyFilterAll: 'ALL', - melodyFilterShare: 'Upload Melody', - melodyShareTitle: 'Share your melody', - melodyShareHelp: 'Publish a snapshot of your current melody so others can listen and remix it.', - melodyShareTitleLabel: 'Title (optional)', - melodyShareTitlePlaceholder: 'A blockchain ghost', - melodyShareSubmit: 'Publish Melody', - melodyNoShared: 'No blockchain melodies yet.', melodyRegenerate: 'Regenerate', melodyDownload: "Download BCS", melodyUploadBcsTitle: "Publish", melodyUploadBcsButton: "Publish", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", melodyUploadStegoLabel: "Hidden message (steganography)", melodyUploadStegoPlaceholder: "Optional.", melodyUploadStegoMaxLabel: "max 280 characters", @@ -293,53 +276,43 @@ module.exports = { blockchainBackToBlockexplorer: "Back to blockexplorer", melodyUploadBcsNameNote: "The audio will be published with the auto-generated name", melodyFilterMine: "MINE", - melodyByLabel: "By", melodyAllEmpty: "No BCS compositions from other inhabitants yet.", - profileActivityTitle: 'Activity', - profileActivityMore: 'View all activity', profileContentSectionTitle: 'Avatar Content', profileContentHelp: 'Choose which of your modules will be displayed on your avatar.', profileSensorsHelp: 'Optional metrics shown on your avatar.', profileClearnetHelp: 'Modules that can be accessed from outside Oasis.', profileHubAll: 'All', - profileHubTitle: 'Public Content', - footerPulseTitle: 'Pulse', - footerPulsePeers: 'Peers', - footerPulseInbox: 'Inbox', - footerPulseSync: 'Last Sync', - footerPulseEcoValue: 'ECO/h', - footerPulseLastActivity: 'Last Activity', footerLicenseLabel: 'License', - profileSwitchToOasis: 'Return to Oasis', - profileSwitchToClearnet: 'Dive into Clearnet', - profileVisibilityFediverse: "Fediverse", - profileSwitchToFediverse: "Dive into Fediverse", - coordLabel: 'Coordinate (e.g., A3)', - coordPlaceholder: 'Enter coordinate', + profileVisibilityFediverse: "Multiverse", contributorsTitle: "Contributors", - pixeliaBy: "by", colorLabel: 'Pick a color', paintButton: 'Paint it!', - invalidCoordinate: 'Incorrect coordinate', - goToMuralButton: "View Mural", totalPixels: 'Total Pixels', modules: "Modules", modulesViewTitle: "Modules", modulesViewDescription: "Set your environment by enabling or disabling modules.", inbox: "Inbox", + inboxSearchPlaceholder: "Search in Inbox...", multiverse: "Multiverse", - fediverse: "Fediverse", - fediverseDescription: "Manage your other fediverse accounts, including sending and receiving content.", + fediverse: "Multiverse", + fediverseDescription: "Manage your other multiverse accounts, including sending and receiving content.", fediverseStatus: "Status", - fediverseDisconnected: "Fediverse disconnected. Check %LINK% or connection status.", - fediverseSettingsTitle: "Fediverse", + fediverseDisconnected: "Multiverse disconnected. Check %LINK% or connection status.", + fediverseSettingsTitle: "Multiverse", fediverseInstanceLabel: "Address", fediverseTokenLabel: "Access token", - fediverseTokenHelp: "Set your access token. This will be used to manage your Mastodon account from inside Oasis.", fediverseConnect: "Connect it", fediverseConnectedAs: "Connected as", + nextcloudServerLabel: "Server", + nextcloudUserLabel: "User", + nextcloudFolderLabel: "Folder", + nextcloudUploadButton: "Upload", + nextcloudFileLabel: "Select a file", + fileExternalLink: "External link", + nextcloudErrConnect: "Nextcloud could not be reached with these details.", + nextcloudErrUpload: "The upload to Nextcloud failed.", + nextcloudErrShare: "Nextcloud did not give a public link for the file.", fediverseDisconnect: "Disconnect", - fediverseEmpty: "When you connect other fediverse accounts from %LINK%, you can manage them from here.", fediverseEmptyLink: "your settings", fediverseComposePlaceholder: "What's on your mind?", fediverseReplyPlaceholder: "Write your reply…", @@ -348,21 +321,15 @@ module.exports = { fediverseReply: "Reply", fediverseBoosted: "boosted", fediverseNoPosts: "Your timeline is empty.", - fediverseThread: "Thread", fediverseTimeline: "Timelines", - fediverseManageTimeline: "Manage timeline", fediverseFollowers: "Followers", fediverseFollowing: "Following", fediversePosts: "Posts", fediverseJoined: "Joined", - fediverseOverview: "Overview", fediverseRefresh: "Refresh", fediverseWrite: "Write", fediversePreview: "Preview", - fediverseEdit: "Edit", - fediverseOpenFeed: "Visit feed", fediverseManage: "Manage", - fediverseStatusNotConnected: "Not connected", fediverseError: "Could not contact Mastodon.", fediverseErrInstance: "Invalid instance URL.", fediverseErrAuth: "Invalid or expired token.", @@ -373,17 +340,76 @@ module.exports = { fediverseErrPost: "Could not publish.", fediverseErrMedia: "Could not upload the file.", fediverseErrEmpty: "Write something or attach a file.", - popularLabel: "Highlights", - topicsLabel: "Topics", - latestLabel: "Latest", - summariesLabel: "Summaries", - threadsLabel: "Threads", - multiverseLabel: "Multiverse", - inboxLabel: "Inbox", - invitesLabel: "Invites", - walletLabel: "Wallet", - legacyLabel: "Keys", - cipherLabel: "Crypter", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Phone number", + telegramSendCode: "Send code", + telegramCodeHelp: "Enter the login code Telegram just sent you (app or SMS).", + telegramCodeLabel: "Login code", + telegramPasswordHelp: "This account has two-step verification. Enter your cloud password.", + telegramPasswordLabel: "Password", + telegramVerify: "Verify", + telegramCancel: "Cancel", + telegramChats: "Chats", + telegramUnread: "Unread", + telegramOpen: "Open", + telegramSend: "Send", + telegramYou: "You", + telegramNoChats: "No chats yet.", + telegramNoMessages: "No messages yet.", + telegramComposePlaceholder: "Write a message…", + telegramChannel: "Channel", + telegramGroup: "Group", + telegramPrivate: "Private", + telegramErrConnect: "Could not contact Telegram.", + telegramErrTimeout: "Telegram took too long to respond.", + telegramErrAuth: "Telegram session invalid or expired. Connect again.", + telegramErrFetch: "Could not load chats.", + telegramErrSend: "Could not send the message.", + telegramErrMissing: "API ID, API hash and phone number are required.", + telegramErrCode: "Invalid code. Try again.", + telegramErrCodeExpired: "The code expired. Start again.", + telegramErrPassword: "Wrong password.", + telegramErrPhone: "Invalid phone number.", + telegramErrApi: "Invalid API ID or API hash.", + telegramErrFlood: "Too many attempts. Wait a while and try again.", + telegramErrNoLogin: "No login in progress. Start again.", + peertubeUsernameLabel: "Username", + peertubePasswordLabel: "Password", + peertubeSubscriptions: "Subscriptions", + peertubeMyVideos: "My videos", + peertubeVideos: "Videos", + peertubeFollowers: "Followers", + peertubeViews: "views", + peertubeLikes: "likes", + peertubeQuota: "Quota", + peertubeUpload: "Upload video", + peertubeUploadHelp: "Pick a video file. It is uploaded to your channel and PeerTube processes it afterwards.", + peertubeTitleLabel: "Title", + peertubeDescriptionLabel: "Description", + peertubePrivacyLabel: "Privacy", + peertubePrivacyPublic: "Public", + peertubePrivacyUnlisted: "Unlisted", + peertubePrivacyPrivate: "Private", + peertubeNoVideos: "No videos yet.", + peertubeComments: "Comments", + peertubeComment: "Comment", + peertubeCommentPlaceholder: "Write a comment…", + peertubeNoComments: "No comments yet.", + peertubeOpen: "Open on PeerTube", + peertubeWatch: "Watch", + peertubeChannel: "Channel", + peertubeNoStream: "This video cannot be played here yet. Open it on PeerTube.", + peertubeUploaded: "Video uploaded. PeerTube is processing it.", + peertubeErrConnect: "Could not contact PeerTube.", + peertubeErrAuth: "Wrong username or password, or the session expired. Connect again.", + peertubeErrInstance: "Invalid instance URL.", + peertubeErrMissing: "Instance, username and password are required.", + peertubeErrFetch: "Could not load videos.", + peertubeErrUpload: "Could not upload the video.", + peertubeErrTitle: "Title required.", + peertubeErrComment: "Could not publish the comment.", + peertubeErrNoFile: "Choose a video file.", bookmarksLabel: "Bookmarks", videosLabel: "Videos", torrentsLabel: "Torrents", @@ -394,34 +420,22 @@ module.exports = { inhabitantsLabel: "Inhabitants", trendingLabel: "Trending", eventsLabel: "Events", - tasksLabel: "Tasks", apply: "Apply", menuPersonal: "Personal", - menuContent: "Content", - menuBlogs: "Blogs", menuGovernance: "Governance", menuOffice: "Office", - menuMultiverse: "Multiverse", - menuNetwork: "Network", + menuNetwork: "Community", menuCreative: "Creative", menuEconomy: "Economy", - menuMedia: "Media", + menuMedia: "Library", menuTools: "Tools", comment: "Comment", subtopic: "Subtopic", json: "JSON", - createdBy: "by", unfollow: "Unsupport", follow: "Support", block: "Block", unblock: "Unblock", - newerPosts: "Newer posts", - olderPosts: "Older posts", - feedRangeEmpty: "The given range is empty for this feed. Try viewing the ", - seeFullFeed: "full feed", - feedEmpty: "The Oasis network has never seen posts from this account.", - beginningOfFeed: "This is the beginning of the feed", - noNewerPosts: "No newer posts have been received yet.", relationshipNotFollowing: "You are not supported", relationshipTheyFollow: "Supports you", relationshipMutuals: "Mutual support", @@ -431,25 +445,23 @@ module.exports = { relationshipBlockedBy: "You are blocked", relationshipMutualBlock: "Mutual block", relationshipNone: "You are not supporting", - relationshipConflict: "Conflict", relationshipBlockingPost: "Blocked post", viewLikes: "View spreads", + viewSupporters: "View supporters", spreadedDescription: "List of posts spread by the inhabitant.", - totalspreads: "Total spreads", - attachFiles: "Attach files", preview: "Preview", publish: "Write", contentWarningPlaceholder: "Add a subject to the post (optional)", - privateWarningPlaceholder: "Add inhabitants to send a private post (optional)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "Write the body of your entry.", + publishTooLong: "Your post is too long. Please shorten it.", + keepTextRetry: "Send again", + dateInPastError: "The date cannot be in the past", publishCustomDescription: [ "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.", ], commentWarning: [ "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.", ], - commentPublic: "public", - commentPrivate: "private", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -469,14 +481,12 @@ module.exports = { ".", ], publishCustom: "Write advanced post", - subtopicLabel: "Create a subtopic of this post", messagePreview: "Post Preview", mentionsMatching: "Matching Mentions", mentionsName: "Name", mentionsRelationship: "Relationship", - updateit: "GET UPDATES!", updateBannerText: "A new version of Oasis is available.", - updateBannerAction: "Update now →", + updateBannerAction: "Update now!", info: "Info", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -484,52 +494,158 @@ module.exports = { timeAgo: "ago", sendTime: "about ", theme: "Theme", - legacy: "Keys", - legacyTitle: "Keys", - legacyDescription: "Manage your secret (private key) quickly and safely.", - legacyExportButton: "Export", - legacyImportButton: "Import", - ssbLogStream: "Blokchain", + backupKeysExportButton: "Export", + backupKeysImportButton: "Import", + ssbLogStream: "Blockchain", ssbLogStreamDescription: "Configure the message limit for Blockchain streams.", + logWindowChip: "Last {n} blockchain streams", settingsLanTitle: "LAN Broadcasting", - settingsLanDesc: "Periodically announce this peer to other Oasis instances on the same local network. Disable to stop UDP broadcasts.", + settingsLanDesc: "Periodically announce this peer to other Oasis instances on the same local network.", settingsReplicationTitle: "Replication", settingsReplicationDesc: "Configure the number of hops your peer follows out from your own feed when replicating content.", settingsReplicationHopsLabel: "Hops", + settingsBlobCacheTitle: "Media cache", + settingsBlobCacheDesc: "Content from other inhabitants is kept on this device. Choose how much space it may take.", + settingsBlobCacheUsage: "Current usage", + settingsBlobCacheFiles: "files", + settingsBlobCacheLimit: "Limit", + settingsBlobCacheUnlimited: "Unlimited", + settingsBlobCacheCleanNow: "Clean now", + settingsBlobCacheCleaned: "{n} files removed, {size} freed.", settingsLanEnable: "Enable LAN broadcasting", saveSettings: "Save settings", - exportTitle: "Export data", exportDescription: "Set password (min 32 characters long) to encrypt your key", - exportDataTitle: "Backup", + exportDataTitle: "Instant Backup", exportDataDescription: "Download your data (secret key excluded!)", exportDataButton: "Download database", - pubWallet: "PUB Wallet", - pubWalletDescription: "Set the PUB wallet URL. This will be used for PUB transactions (including the UBI).", - pubWalletConfiguration: "Save configuration", - importTitle: "Import data", importDescription: "Import your encrypted secret (private key) to enable your avatar", - importAttach: "Attach encrypted file (.enc)", - passwordLengthInfo: "Password must be at least 32 characters long.", passwordImport: "Write your password to decrypt data that will be saved at your system home (name: secret)", exportPasswordPlaceholder: "Use lowercase, uppercase, numbers & symbols", - fileInfo: "Your encrypted secret key will be saved at your system home (name: oasis.enc)", + fileInfo: "Your encrypted secret key will be downloaded to your device (name: oasis.enc)", + developer: "Developer", + devTitle: "Developer", + welcomeBannerText: "Your avatar is not set up yet.", + aiSuggestionBanner: "Suggestion:", + aiSuggestionsEnable: "AI Suggestions", + welcomeBannerAction: "Start", + welcomeTitle: "Welcome", + welcomeStepIdentity: "Identity", + welcomeStepType: "Type", + welcomeStepUse: "Use", + welcomeStepLook: "Appearance", + welcomeStepCommunity: "Community", + welcomeStepIntro: "Welcome", + welcomeHintIdentity: "Your name and picture.", + welcomeHintType: "How Oasis opens for you.", + welcomeHintUse: "Which modules stay on.", + welcomeHintLook: "The theme Oasis wears.", + welcomeHintCommunity: "Join a PUB to meet other inhabitants.", + welcomeHintIntro: "Send a first greeting to the community.", + welcomeStepThemeAction: "Use this theme", + welcomeJoinPubs: "Join PUBs", + welcomeNoFederation: "No federation", + welcomeStepGreetingAction: "Send Feed", + welcomeDescription: "A few things worth doing before you start.", + welcomeSkip: "Skip for now", + welcomeStepProfileAction: "Save Profile", + welcomeBootstrapRunning: "Loading the pub's snapshot. The pub is joined as soon as it finishes; you can keep using Oasis meanwhile.", + welcomeCompleteTitle: "All set.", + welcomeCompleteText: "Your node is ready. From here the network grows with the inhabitants you support.", + welcomeFindPeople: "Find inhabitants", + devFilterFiles: "FILES", + devFilterSearch: "SEARCH", + devFilterModules: "MODULES", + devFilterTranslations: "TRANSLATIONS", + devFilterStyles: "STYLES", + devFilterTests: "TESTS", + devVersion: "Version", + devNodeVersion: "Node", + devStatsTests: "Tests", + devParentFolder: "Parent folder", + devOpenFolder: "open", + devDescription: "Explore the source code running on this node.", + devRoot: "root", + devSearchPlaceholder: "Text to find in the code", + devAnyExtension: "Any file", + devStatsFiles: "Files", + devStatsLines: "Lines", + devStatsModules: "Modules", + devStatsLanguages: "Languages", + devNotViewable: "not viewable", + devEmptyFolder: "This folder is empty.", + devSize: "Size", + devShowing: "Showing", + devLine: "line", + devReportBug: "Report Bug", + devCreateTask: "Create Task", + devRaw: "RAW", + devReportContext: "Found while reading the Oasis source code", + devTaskPrefix: "Review", + devPrevPage: "Previous lines", + devNextPage: "Next lines", + devMatches: "matches", + devFilesScanned: "files scanned", + devNoResults: "No matches, yet.", + devColModule: "Module", + devColState: "State", + devColModel: "Model", + devColView: "View", + devColRoutes: "Routes", + devColTests: "Tests", + devOn: "on", + devOff: "off", + modulesDevLabel: "Developer", + modulesDevDescription: "Module to manage the Oasis source code.", themeIntro: "Choose a theme.", setTheme: "Set theme", language: "Language", languageDescription: "If you'd like to use another language, select it here.", - setLanguage: "Set language", - peerConnections: "Peers", + setLanguage: "Set Language", peerConnectionsIntro: "Manage all your connections with other peers.", + peersConnectFailed: "Could not connect: {reason}.", + peerStateLabel: "State", + peerAddressLabel: "Address", + peerLastChangeLabel: "Last change", + peerStateConnected: "Connected", + peerStateConnecting: "Connecting", + peerStateStaged: "Available", + peerErrSilent: "No activity", + peerErrTimeout: "Not responding", + peerErrRefused: "Refuses the connection", + peerErrNotFound: "Unknown address", + peerErrUnreachable: "Network unreachable", + peerErrKeys: "Using different keys", + peerErrIdentity: "Does not accept your connection", + peerErrTor: "TOR only", + peerErrDropped: "Connection dropped", + peerErrPaused: "Network paused", + peerErrOther: "Connection error", + peerOutdated: "Outdated", + peerVersionUnannounced: "It does not announce its version: an old Oasis", + peersTorMissing: "This PUB only allows connections through TOR.", peerConnectionsTitle: "Connections", + peersOnlineHint: "Nodes connected to you right now.", + peersDiscoveredHint: "Nodes with a known address that are not connected right now.", + peersUnknownHint: "Nodes in your social graph with no known address.", + peersConnectionsHint: "Addresses this node can dial and the state of each connection.", + peersTabLan: "LAN", + peersTabHealth: "Health", + peersHealthHint: "How each known pub or peer is doing, on the local network and beyond.", + peersHealthReachable: "Reachable", + peersHealthFailing: "Failing", + peersHealthFailures: "Failures", + peersFailingSince: "Failing since", + peersHealthForget: "Forget unreachable", + peersLanHint: "Nodes met on your local network.", + peersLanLastSeen: "Last seen", + peersLanEmpty: "No nodes met on the local network yet.", online: "Online", offline: "Offline", discovered: 'Discovered', unknown: 'Unknown', - lanBroadcastLabel: "LAN broadcast", lanBroadcastActive: "Active (UDP multicast on LAN)", - lanBroadcastDisabled: "Disabled (pub mode)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -542,127 +658,102 @@ module.exports = { noConnections: "No peers connected.", noDiscovered: "No peers discovered.", noUnknownPeers: "No unknown peers in the friend graph.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", peerRefresh: "Refresh", peerPruneIdle: "Remove idle", + peersPause: "Pause", + peersResume: "Resume", + peersPausedNote: "Paused: Oasis does not connect to anyone and nobody can connect to it.", + peersPauseHint: "Stops every connection: Oasis neither connects to other nodes nor accepts them until you resume.", peerExport: "Export", peerImport: "Import", peerImportTitle: "Import peer list", peerImportPlaceholder: "Paste one multiserver address per line, or upload a .txt file…", - noSupportedConnections: "No peers supported.", - noBlockedConnections: "No peers blocked.", - noRecommendedConnections: "No peers recommended.", connectionActionIntro: "", - startNetworking: "Start networking", - stopNetworking: "Stop networking", - restartNetworking: "Restart networking", - sync: "Sync network", indexes: "Indexes", + indexesRebuilt: "Indexes rebuilt", + indexesRebuildFailed: "Could not rebuild the indexes", + indexesFiles: "Index files", indexesDescription: "Rebuilding your indexes is safe, and may fix some types of bugs.", homePageTitle: "Home", homePageDescription: "Select which module you want as your home page.", saveHomePage: "Set home", invites: "Invites", - invitesTitle: "Invites", - invitesInvites: "Invitations", invitesDescription: "Manage and apply invite codes in your network.", + invitesPubsHint: "Paste a pub invite code to federate with it and start replicating its inhabitants.", + invitesFederationsHint: "Pubs you are federated with: refresh them, drop the unreachable ones or export the list.", invitesTribesTitle: "Tribes", - invitesTribeInviteCodePlaceholder: "Enter tribe invite code", - invitesTribeJoinButton: "Join Tribe", invitesChatsTitle: "Chats", - invitesChatInviteCodePlaceholder: "Enter chat invite code", - invitesChatJoinButton: "Join Chat", invitesPadsTitle: "Pads", - invitesPadInviteCodePlaceholder: "Enter pad invite code", - invitesPadJoinButton: "Join Pad", invitesCalendarsTitle: "Calendars", - invitesCalendarInviteCodePlaceholder: "Enter calendar invite code", - invitesCalendarJoinButton: "Join Calendar", invitesEventsTitle: "Events", - invitesEventInviteCodePlaceholder: "Enter event invite code", - invitesEventJoinButton: "Join Event", invitesForumsTitle: "Forums", - invitesForumInviteCodePlaceholder: "Enter forum invite code", - invitesForumJoinButton: "Join Forum", invitesMapsTitle: "Maps", - invitesMapInviteCodePlaceholder: "Enter map invite code", - invitesMapJoinButton: "Join Map", invitesShopsTitle: "Shops", - invitesShopInviteCodePlaceholder: "Enter shop invite code", - invitesShopJoinButton: "Join Shop", invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "Suggested PUBs", + invitesFilterOthers: "Others", + invitesJoinTitle: "Invitations", + invitesJoinHint: "Paste any invitation code, whatever it is for, or an inhabitant's Oasis ID.", + invitesJoinPlaceholder: "Invitation code or Oasis ID", + invitesJoinButton: "Join", + invitesUsedTitle: "Latest invitations", + invitesUsedEmpty: "You have not used any invitation yet.", + invitesUsedType: "Type", + invitesUsedName: "Name", + invitesUsedDate: "Date", invitesPubInviteCodePlaceholder: "Enter PUB invite code", invitesAcceptInvite: "Join PUB", + invitesPubUses: "Invitations used", + invitesPubExhausted: "This PUB has no invitations left.", invitesAlreadyFederated: "You are already federated with this pub.", + invitesJoinPubMode: "This Oasis runs as a PUB, in read-only mode. To join it to another PUB, use the terminal: ./oasis.sh join ", + invitesJoinLocalOnly: "Invites can only be accepted from the device where Oasis runs, or through the admin link it shows when it starts.", invitesFederationsTitle: "Federations", invitesAcceptedInvites: "Federated", - invitesNoInvites: "No invitations accepted, yet.", invitesUnfollow: "Unfollow", invitesFollow: "Follow", invitesUnfollowedInvites: "Unfederated", invitesNoFederatedPubs: "No federated pubs.", invitesNoUnfollowed: "No unfederated pubs.", - invitesUnreachablePubs: "Unreachable", - invitesNoUnreachablePubs: "No unreachable networks.", invitesPubsRefresh: "Refresh", - invitesPubsClearUnreachable: "Remove unreachable", + invitesPubsClearUnreachable: "Clean", invitesPubsExport: "Export", invitesPubsImport: "Import", invitesPubsImportTitle: "Import pubs", invitesPubsImportPlaceholder: "Paste one multiserver address or invite code per line, or upload a .txt file…", - currentlyUnreachable: "ERROR!", - errorDetails: "Error Details", genericError: "An error occurred.", panicMode: "Panic Mode!", encryptData: "Set password (min 32 characters long) to encrypt your blockchain", decryptData: "Enter password to decrypt your blockchain", - panicModeDescription: "Encrypt/Decrypt or DELETE your blockchain", removeDataDescription: "WARNING: This process cannot be undone.", - encryptPanicButton: "Encrypt blockchain", - decryptPanicButton: "Decrypt blockchain", removePanicButton: "DELETE ALL YOUR DATA!", searchTitle: "Search", searchDescriptionLabel: "Search for content in your network.", - searchLanguagesLabel: "Languages", - searchSkillsLabel: "Skills", searchPlaceholder:"Search for content...", - searchDateLabel:"Date", searchLocationLabel:"Location", searchPriceLabel:"Price", - searchUrlLabel:"URL", searchCategoryLabel:"Category", - searchStartLabel:"Start Time", - searchEndLabel:"End Time", searchPriorityLabel:"Priority", searchStatusLabel:"Status", statusLabel:"Status", - totalVotesLabel:"Total Votes", noResultsFound:"No results found.", votesOption:"Vote Options", voteYesLabel:"Yes", voteNoLabel:"No", allTypesLabel: "UNLIMITED", - ABSTENTIONLabel:"ABSTENTION", YESLabel:"YES", NOLabel:"NO", - FOLLOW_MAJORITYLabel: "FOLLOW MAJORITY", - CONFUSEDLabel: "CONFUSED", - NOT_INTERESTEDLabel: "NOT INTERESTED", - StatusLabel:"Status", votesOptionYesLabel:"Yes", votesOptionNoLabel:"No", votesOptionAbstentionLabel:"Abstention", votesQuestionLabel:"Question", - votesCreatedByLabel:"Created by", voteStatusOpen:"OPEN", voteStatusClosed:"CLOSED", - imageSearchLabel: "Enter words to search for images labelled with them.", commentDescription: ({ parentUrl }) => [ " commented on ", a({ href: parentUrl }, " thread"), @@ -673,65 +764,30 @@ module.exports = { a({ href: parentUrl }, " a post"), ], subtopicTitle: ({ authorName }) => [`Subtopic on @${authorName}'s post`], - mysteryDescription: "posted a mysterious post", oasisDescription: "OASIS Project Network", searchSubmit: "Let's Search...", - postLabel: "POSTS", - aboutLabel: "INHABITANTS", - feedLabel: "FEEDS", votesLabel: "VOTATIONS", - reportLabel: "REPORTS", - imageLabel: "IMAGES", - videoLabel: "VIDEOS", - audioLabel: "AUDIOS", - documentLabel: "DOCUMENTS", - torrentLabel: "TORRENTS", pdfFallbackLabel: "PDF Document", - eventLabel: "EVENTS", - taskLabel: "TASKS", - transferLabel: "TRANSFERS", - curriculumLabel: "CURRICULUM", - bookmarkLabel: "BOOKMARKS", - tribeLabel: "TRIBES", - marketLabel: "MARKET", shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "WALLETS", - bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", submit: "Submit", - subjectLabel: "Subject", editProfile: "Edit Avatar", - profileQrAlt: "Profile QR code", - profileQrHint: "Scan to copy this Oasis ID.", oasisIdLabel: "OasisID", - spreadLabel: "Spread", - spreadHint: "Spread this to your supporters (replicates via your feed).", + spreadContent: "Replicate", welcomePmSubject: "Hello World!", - welcomePmBody: "Welcome to Oasis — a libre, peer-to-peer, federated and encrypted social network with a distributed invite-only architecture.\n\nSome interesting places to start:\n\n- /profile — Edit your avatar, name, description and which modules appear on your public side.\n- /settings — Tune your local instance: language, theme, modules, privacy and sync.\n- /invites — Add a pub to federate with the wider network.\n- /peers — See who is connected, rescan your LAN, export or import peer lists.\n- /inhabitants — Discover and visit other people's avatars.\n- /tribes — Create or join private groups with end-to-end encryption.\n- /inbox — Read and send private messages.\n- /banking — See your ECO balance, UBI, ECO tax and the wealth-redistribution rules.\n- /activity — See recent activity, yours and your network's.\n- /publish — Write a post or share an image, audio, video or document.\n\nSome interesting places to connect:\n\n- /fediverse — Manage your other fediverse accounts, including sending and receiving content.\n\nThis message was sent privately from you to yourself, so no connection was required to receive it.", + welcomePmBody: "Welcome to Oasis — a libre, peer-to-peer, federated and encrypted social network with a distributed invite-only architecture.\n\nSome interesting places to start:\n\n- [/avatar](/profile) — Edit your avatar, name, description and which modules appear on your public side.\n- /settings — Tune your local instance: language, theme, modules, privacy and sync.\n- /invites — Add a pub to federate with the wider network.\n- /peers — See who is connected, rescan your LAN, export or import peer lists.\n- /inhabitants — Discover and visit other inhabitants' avatars.\n- /tribes — Create or join private groups with end-to-end encryption.\n- /search — Search the network's content by type.\n\nSome interesting places to connect:\n\n- /multiverse — Manage your other multiverse accounts, including sending and receiving content.\n\nThis message was sent privately from you to yourself, so no connection was required to receive it.", indexingTitle: "Synchronizing", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingEditProfileLink: "Set up my avatar", - profileVisibilityTitle: "Public profile visibility", - profileVisibilityHint: "Toggle which fields other inhabitants see on your profile. By default only Karma is shown.", + indexingMessage: "Oasis is synchronizing the distributed network of nodes. Please wait.", profileSensorsSectionTitle: "Sensors", profileVisibilityActivity: "Activity Level", - profileVisibilityDevice: "Device", profileDeviceLockedHint: "This sensor is always on: it lets other users see which device you connect from and cannot be disabled.", profileVisibilityKarma: "KARMA Scoring", profileVisibilityUbi: "UBI", - profileVisibilityWallet: "ECOIN Wallet", + profileVisibilityWallet: "ECOin Wallet", profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "GPG Key", - profileVisibilityClearnet: "Publish my profile", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Shops", profileClearnetJobsLabel: "Jobs", @@ -743,6 +799,10 @@ module.exports = { profileClearnetImagesLabel: "Images", profileClearnetDocumentsLabel: "Documents", profileClearnetTorrentsLabel: "Torrents", + profileClearnetPodcastsLabel: "Podcasts", + profileClearnetMarketLabel: "Market", + profileClearnetFeedLabel: "Feed", + profileClearnetWikiLabel: "Wikis", editProfileDescription: "", profileName: "Name", @@ -768,7 +828,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "Cnfrs", walletConfirm: "Confirm", - walletDescription: "Manage your digital assets, including sending and receiving ECOin, viewing your balance, and accessing your transaction history.", + walletDescription: "Manage your ECOin wallet.", walletDate: "Date", walletFee: "Fee (The higher the fee, the faster your transaction will be processed)", walletFeeLine: ({ fee }) => `Fee: ECO ${fee}`, @@ -776,6 +836,86 @@ module.exports = { walletReceive: "Receive", walletReset: "Reset", walletSend: "Send", + walletPayingTransfer: "Paying transfer", + walletCreateTransferLabel: "Generate smart-contract (transfer)", + walletTransferNotifiedNote: "The other party has been notified of this payment", + walletTransferCreatedNote: "A transfer (receipt) has been created for this payment", + walletTransferNoPayeeNote: "No inhabitant has published this address, so no transfer was created", + walletTransferReceiptConcept: "Wallet payment", + walletPaymentPmText: "sent you a payment", + walletPayeeLabel: "Recipient", + walletConceptLabel: "Concept", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "You have received your UBI.", + bankingBotPaymentTitle: "You have received a payment.", + bankingBotUbiPaidText: "Your Universal Basic Income has been paid", + bankUbiPubThisNode: "This node pays the UBI", + bankUbiPubConnected: "UBI PUB", + bankUbiPubNone: "No PUB is announcing UBI in your network yet", + bankUbiAddressPublished: "ECOin address published", + bankUbiAddressMissing: "Publish your ECOin address to receive UBI", + bankUbiClaimedThisMonth: "Claimed this month", + bankUbiPubBalance: "PUB wallet available for UBI", + bankNoPubAddress: "The UBI PUB has not published an ECOin address yet.", + bankUbiTab: "UBI", + bankDonateEco: "DONATE!", + donateTooltip: "Donate ECOin", + bankNoUserAddress: "This inhabitant has not shared an ECOin address.", + bankUbiPubDefault: "Default", + bankUbiPool: "Pool", + bankUbiLastSeen: "Last seen", + bankUbiLastPayout: "Last action", + bankUbiPaidOut: "Paid out", + bankRangeToday: "Today", + bankRangeWeek: "Week", + bankRangeMonth: "Month", + bankRangeYear: "Year", + bankRangeFiveYears: "5 years", + bankRangeAll: "All", + bankChartValueTitle: "ECOin Value Evolution", + bankChartValueHours: "Equivalence in time (h)", + bankChartWealthTitle: "ECOin Wealth Redistribution", + bankChartDistributed: "UBI distributed (ECO)", + bankChartRebalanceTitle: "Redistribution of wealth between PUBs", + bankChartRebalanced: "ECO moved between PUBs", + bankChartTaxes: "Taxes withheld (ECO)", + bankChartNoDataRange: "No data in this range.", + bankChartNoDataYet: "No data available, yet", + bankChartPerHour: "per hour", + bankChartPerDay: "per day", + bankChartPerMonth: "per month", + bankTotalUbiDistributed: "Total UBI redistributed (network)", + bankTotalTaxesWithheld: "Total taxes withheld (network)", + bankInflationIssuance: "Issuance rate (annual, from block reward)", + bankRulesEligibilityTitle: "Who can receive the UBI", + bankRulesEligibility1: "An ECOin address published in your profile (Profile → Sensors → ECOin Wallet).", + bankRulesEligibility2: "A feed older than 30 days with activity in the network: karma above zero.", + bankRulesEligibility3: "One claim per month (epoch), made from Banking with the Claim UBI button. It can be refused for the current month with Refuse UBI.", + bankRulesEligibility4: "The floor of 1 ECO is always paid; taxes only reduce the part above it and are never charged apart.", + bankRulesEligibility5: "The PUB pays on its next cycle and publishes a UBI transfer that you can confirm.", + bankRulesEligibility6: "If your address is missing, the feed is too young, you refused the month or the PUB has no funds, the claim is skipped and logged on the PUB.", + copyLinkTitle: "Copy link", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "Refuse UBI", + bankUbiRefusedThisMonth: "You refused this month's UBI.", + bankingBotUbiAvailableTitle: "You can claim your UBI.", + bankingBotUbiAvailableText: "Your UBI for this month is available", + bankingBotConfirmTitle: "You have a smart-contract awaiting confirmation", + bankingBotConfirmText: "Smart-contracts awaiting your confirmation", + bankChartZoomHint: "Click to enlarge", + bankChartPoolTitle: "UBI Pool (over time)", + bankChartNetworkUbiTitle: "Total UBI redistributed (network)", + bankChartSupplyTitle: "ECOin Current Supply (over time)", + bankChartInflationTitle: "ECOin Inflation (over time)", + bankChartInflationSeries: "Annualised rate (%)", + bankChartPool: "Total Pool (ECO)", + bankChartKarmaTitle: "KARMA Scoring (over time)", + bankChartFundsTitle: "Your ECOin funds (over time)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin connected", + bankWalletNotConfigured: "ECOin disconnected", + transfersPayWithWallet: "PAY!", walletStatus: "Status", walletDisconnected: [ "ECOin ", @@ -785,9 +925,11 @@ module.exports = { " or connection status.", ], walletSentToLine: ({ destination, amount }) => `Sent ECO ${amount} to ${destination}`, - walletSettingsTitle: "Wallet", + walletNotConfigured: "Your ECOin wallet is not configured yet.", walletSettingsDescription: "Integrate Oasis with your ECOin wallet.", - walletSettingsDocLink: "ECOin installation guide", + walletSettingsMobileDescription: "Link Oasis with the ECOin Wallet app on this device.", + walletDownloadEcoin: "Download ECOin!", + walletRpcUrl: "RPC URL", walletStatusMessages: { invalid_amount: "Invalid amount", invalid_dest: "Invalid destination address", @@ -803,41 +945,24 @@ module.exports = { walletUser: "Username", walletPass: "Password", walletConfiguration: "Set wallet", + walletDisconnectButton: "Disconnect wallet", cipher: "Crypter", cipherTitle: "Crypter", cipherDescription: "Encrypt and decrypt your text symmetrically (using a shared password).", randomPassword: "Random Password", - cipherEncryptTitle: "Encrypt Text", - cipherEncryptDescription: "Enter text to encrypt", - cipherTextLabel: "Text to Encrypt", cipherTextPlaceholder: "Enter text to encrypt...", cipherPasswordLabel: "Set password (min 32 characters long) to encrypt your text", cipherPasswordDecryptLabel: "Set password (min 32 characters long) to decrypt your text", cipherPasswordPlaceholder: "Enter a password...", cipherEncryptButton: "Encrypt", - cipherDecryptTitle: "Decrypt Text", - cipherDecryptDescription: "Enter text to decrypt", cipherEncryptedMessageLabel: "Encrypted Text", cipherDecryptedMessageLabel: "Decrypted Text", - cipherPasswordUsedLabel: "Password used to encrypt (keep it!)", cipherEncryptedTextPlaceholder: "Enter the encrypted text...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "Enter the initialization vector...", cipherDecryptButton: "Decrypt", password: "Password", text: "Text", encryptedText: "Encrypted Text", iv: "Initialization Vector (IV)", - encryptTitle: "Encrypt your text", - encryptDescription: "Enter the text you want to encrypt and provide a password.", - encryptButton: "Encrypt", - decryptTitle: "Decrypt your text", - decryptDescription: "Enter the encrypted text and provide the same password used for encryption.", - decryptButton: "Decrypt", - passwordLengthError: "Password must be at least 32 characters long.", - missingFieldsError: "Text, password or IV not provided.", - encryptionError: "Error encrypting text.", - decryptionError: "Error decrypting text.", bookmarkTitle: "Bookmarks", bookmarkDescription: "Discover and manage bookmarks in your network.", bookmarkAllSectionTitle: "Bookmarks", @@ -854,19 +979,16 @@ module.exports = { bookmarkFilterRecent: "RECENT", bookmarkCreateButton: "Create Bookmark", bookmarkUpdateButton: "Update", - bookmarkDeleteButton: "Delete", bookmarkAddFavoriteButton: "Add favorite", bookmarkRemoveFavoriteButton: "Remove favorite", bookmarkUrlLabel: "Link", - bookmarkUrlPlaceholder: "https://example.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Description", bookmarkDescriptionPlaceholder: "Optional", bookmarkTagsLabel: "Tags", bookmarkTagsPlaceholder: "Enter tags separated by commas", - bookmarkCategoryLabel: "Category", - bookmarkCategoryPlaceholder: "Optional", bookmarkLastVisitLabel: "Last Visit", - bookmarkSearchPlaceholder: "Search URL, tags, category, author...", + bookmarkSearchPlaceholder: "Search in Bookmarks...", bookmarkSortRecent: "Most recent", bookmarkSortOldest: "Oldest", bookmarkSortTop: "Most voted", @@ -879,8 +1001,6 @@ module.exports = { noLastVisit: "No last visit", videoTitle: "Videos", videoDescription: "Explore and manage video content in your network.", - videoPluginTitle: "Title", - videoPluginDescription: "Description", videoMineSectionTitle: "Your Videos", videoCreateSectionTitle: "Upload Video", videoUpdateSectionTitle: "Update Video", @@ -907,12 +1027,11 @@ module.exports = { videoDescriptionPlaceholder: "Optional", videoNoFile: "No video file provided", noVideos: "No videos available.", - videoSearchPlaceholder: "Search title, tags, author...", + videoSearchPlaceholder: "Search in Videos...", videoSortRecent: "Most recent", videoSortOldest: "Oldest", videoSortTop: "Most voted", videoSearchButton: "Search", - videoMessageAuthorButton: "PM", videoUpdatedAt: "Updated", videoNoMatch: "No videos match your search.", documentTitle: "Documents", @@ -931,7 +1050,6 @@ module.exports = { documentFilterFavorites: "FAVORITES", documentCreateButton: "Upload Document", documentUpdateButton: "Update", - documentDeleteButton: "Delete", documentAddFavoriteButton: "Add favorite", documentRemoveFavoriteButton: "Remove from favorites", documentMessageAuthorButton: "PM", @@ -944,7 +1062,7 @@ module.exports = { documentDescriptionPlaceholder: "Optional", documentNoFile: "No file.", noDocuments: "No documents available.", - documentSearchPlaceholder: "Search title, tags, description, author...", + documentSearchPlaceholder: "Search in Documents...", documentSortRecent: "Most recent", documentSortOldest: "Oldest", documentSortTop: "Most voted", @@ -953,8 +1071,6 @@ module.exports = { documentUpdatedAt: "Updated", audioTitle: "Audios", audioDescription: "Explore and manage audio content in your network.", - audioPluginTitle: "Title", - audioPluginDescription: "Description", audioMineSectionTitle: "Your Audios", audioCreateSectionTitle: "Upload Audio", audioUpdateSectionTitle: "Update Audio", @@ -965,17 +1081,12 @@ module.exports = { audioFilterMine: "MINE", audioFilterRecent: "RECENT", audioFilterTop: "TOP", - audioFilterBlockchain: "BLOCKCHAIN", audioFilterBcs: "BCS", audioTranscodeButton: "TRANSCODE", audioTranscodeDetailTitle: "Transcode", audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", ecoTaxLabel: "ECO Tax", - ecoinTaxLabel: "ECOin Tax", bankTaxes: "Taxes", - bankTaxesNetworkTitle: "Network Taxes", - bankTaxesUserTitle: "Your taxes", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", bankTaxesUserNoteChipsClick: "Click any chip to inspect its block in the blockexplorer.", bankTaxesEcoTaxTitle: "ECO Tax", @@ -988,7 +1099,6 @@ module.exports = { bankTaxesArchTaxRate: "Rate", bankTaxesArchTaxFirstBlock: "Your first block age", bankTaxesArchTaxUserAmount: "Your ARCH tax (price to return)", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", @@ -997,10 +1107,9 @@ module.exports = { bankRulesArchTaxRate: "Rate", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", - bankTaxesExample: "Example", bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", bankTaxesUserAmount: "Your ECO tax (price to return)", - bankOverviewYourTaxes: "Your taxes", + bankOverviewYourTaxes: "Total Taxes", bankTaxesTotalBlocks: "Total blocks", bankTaxesTotalBytes: "Total bytes", bankTaxesTotalCarbon: "Total carbon", @@ -1011,7 +1120,6 @@ module.exports = { bankTaxesTotalLifetime: "Total (lifetime)", bankTaxesTotalAnnual: "Total (annual)", bankTaxesTotalMonthly: "Total (monthly)", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", bankTaxesRate: "Rate", bankTaxesSpan: "Sampling span", bankTaxesLookupTitle: "Inspect a block", @@ -1041,7 +1149,7 @@ module.exports = { bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", bankRulesKarmaTitle: "Karma (user engagement score)", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", + bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed. What your content receives counts too: positive opinions, votes, reactions and plays add karma, and negative opinions or votes subtract it once several inhabitants agree.", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", bankRulesEcoTaxTitle: "ECO Tax", bankRulesEcoTaxRate: "Rate", @@ -1052,36 +1160,29 @@ module.exports = { bankRulesChipTitle: "ECO Tax chip color band", bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "No tombstones in the network yet.", - audioTranscodeTitle: "BCS Transcode", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - audioTranscodeStegoTitle: "Embedded in the waveform", - audioTranscodeStegoOasisId: "By", audioTranscodeStegoTimestamp: "Generated at", audioTranscodeStegoMessage: "TEXT", audioTranscodeStegoEmpty: "(none)", audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "No steganographic payload could be decoded from this audio.", audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", - audioBackToAudio: "Back to audio", audioBackToBcs: "Back to BCS", - audioAuthorLabel: "Author", audioCreateButton: "Upload Audio", audioUpdateButton: "Update", - audioDeleteButton: "Delete", audioAddFavoriteButton: "Add favorite", audioRemoveFavoriteButton: "Remove favorite", audioFileLabel: "Select an audio file (.mp3, .wav, .ogg)", audioTagsLabel: "Tags", audioTagsPlaceholder: "Enter tags separated by commas", + tagsPlaceholder: "Enter tags separated by commas", audioTitleLabel: "Title", audioTitlePlaceholder: "Optional", audioDescriptionLabel: "Description", audioDescriptionPlaceholder: "Optional", audioNoFile: "No audio file provided", noAudios: "No audios available.", - audioSearchPlaceholder: "Search title, tags, author...", + audioSearchPlaceholder: "Search in Audios...", audioSortRecent: "Most recent", audioSortOldest: "Oldest", audioSortTop: "Most voted", @@ -1093,6 +1194,7 @@ module.exports = { audioFilterFavorites: "FAVORITES", favoritesTitle: "Favorites", favoritesDescription: "All your favorited content in one place.", + favoritesSearchPlaceholder: "Search in Favorites...", favoritesFilterAll: "ALL", favoritesFilterRecent: "RECENT", favoritesFilterAudios: "AUDIOS", @@ -1104,22 +1206,16 @@ module.exports = { favoritesFilterChats: "CHATS", favoritesFilterCalendars: "CALENDARS", favoritesFilterVideos: "VIDEOS", - favoritesRemoveButton: "Remove from favorites", favoritesNoItems: "No favorites yet.", yourContacts: "Your Contacts", allInhabitants: "Inhabitants", - allCVs: "All CVs", + allCVs: "CVs", discoverPeople: "Discover inhabitants in your network.", - allInhabitantsButton: "ALL", - contactsButton: "SUPPORTS", - CVsButton: "CVs", - matchSkills: "Match Skills", - matchSkillsButton: "MATCH SKILLS", - suggestedButton: "SUGGESTED", - searchInhabitantsPlaceholder: "FILTER inhabitants BY NAME …", + searchInhabitantsPlaceholder: "Search in Inhabitants...", filterLocation: "FILTER inhabitants BY LOCATION …", filterLanguage: "FILTER inhabitants BY LANGUAGE …", filterSkills: "FILTER inhabitants BY SKILLS …", + cvSkillsCount: "Skills", applyFilters: "Apply Filters", locationLabel: "Location", languagesLabel: "Languages", @@ -1128,15 +1224,15 @@ module.exports = { mutualFollowers: "Mutual Followers", suggestedFollowsYou: "Follows you", latestInteractions: "Latest Interactions", - viewAvatar: "View Avatar", - viewCV: "View CV", suggestedSectionTitle: "Suggested", topkarmaSectionTitle: "Top Karma", topecoSectionTitle: "Top Eco", topactivitySectionTitle: "Top Activity", + "TOP INACTIVITYButton": "TOP INACTIVITY", + topinactivitySectionTitle: "Least active inhabitants", blockedSectionTitle: "Blocked", gallerySectionTitle: "GALLERY", - blockedButton: "BLOCKED", + topSectionTitle: "TOP", blockedLabel: "Blocked User", inhabitantviewDetails: "View Details", keepReading: "Keep reading...", @@ -1147,14 +1243,27 @@ module.exports = { deviceLabel: "Device", parliamentTitle: "Parliament", parliamentDescription: "Explore forms of government and collective management laws.", - parliamentFilterGovernment: "GOVERMENT", + parliamentFilterGovernment: "GOVERNMENT", parliamentFilterCandidatures: "CANDIDATURES", parliamentFilterProposals: "PROPOSALS", parliamentFilterLaws: "LAWS", + parliamentFilterRevocations: "REVOCATIONS", parliamentFilterHistorical: "HISTORICAL", parliamentFilterLeaders: "LEADERS", parliamentFilterRules: "RULES", parliamentGovernmentCard: "Current Government", + parliamentSeatsGovernment: "Government", + parliamentSeatsOpposition: "Opposition", + parliamentSeatsAbstention: "Abstention", + parliamentSeatsQuorum: "Election quorum", + parliamentSeatsPass: "Approval threshold", + parliamentSeatsByElection: "By election", + parliamentSeatsByHouses: "By LARP house", + parliamentSeatsSeats: "seats", + parliamentSeatsSeat: "seat", + parliamentSeatsLeader: "Leader", + parliamentSeatsVotesFor: "votes for", + parliamentSeatsNoHouse: "No house", parliamentActorInPowerInhabitant: 'INHABITANT RULING', parliamentActorInPowerTribe: 'TRIBE RULING', parliamentHistoricalGovernmentsTitle: 'GOVERMENTS', @@ -1176,10 +1285,8 @@ module.exports = { parliamentPoliciesDeclined: "LAWS DECLINED", parliamentPoliciesDiscarded: "LAWS DISCARDED", parliamentEfficiency: "% EFFICIENCY", - parliamentFilterRevocations: 'REVOCATIONS', parliamentRevocationFormTitle: 'Revocate Law', parliamentRevocationLaw: 'Law', - parliamentRevocationTitle: 'Title', parliamentRevocationReasons: 'Reasons', parliamentRevocationPublish: 'Publish Revocation', parliamentCurrentRevocationsTitle: 'Current Revocations', @@ -1192,23 +1299,12 @@ module.exports = { parliamentNoGovernments: "There are not governments, yet.", parliamentCandidatureFormTitle: "Propose Candidature", parliamentCandidatureIdPh: "Oasis ID (@...) or Tribe name", - parliamentCandidatureSlogan: "Slogan (max 140 chars)", - parliamentCandidatureSloganPh: "A short motto", parliamentCandidatureMethod: "Method", parliamentCandidatureProposeBtn: "Publish Candidature", - parliamentThDate: "Proposal date", - parliamentThSlogan: "Slogan", - parliamentThSince: "Profile since", - parliamentThVotes: "Votes recieved", - parliamentThVoteAction: "Vote", - parliamentTypeUser: "Inhabitant", - parliamentTypeTribe: "Tribe", parliamentVoteBtn: "Vote", parliamentProposalFormTitle: "Propose Law", parliamentProposalDescription: "Description (≤1000)", parliamentProposalPublish: "Publish Proposal", - parliamentFinalize: "Finalize", - parliamentDeadline: "Deadline", parliamentNoProposals: "There are not law proposals, yet.", parliamentNoLaws: "There are not laws approved, yet.", parliamentMethodDEMOCRACY: "Democracy", @@ -1226,29 +1322,22 @@ module.exports = { parliamentVotesSlashTotal: "Votes/Total", parliamentVotesNeeded: "Votes Needed", parliamentFutureLawsTitle: "Future Laws", - parliamentNoFutureLaws: "No future laws, yet.", parliamentVoteAction: "Vote", - parliamentLeadersTitle: "Leaders", parliamentThLeader: "AVATAR", parliamentPopulation: "Population", - parliamentThType: "Type", - parliamentThInPower: "In power", parliamentThPresented: "CANDIDATURES", parliamentNoLeaders: "No leaders, yet.", parliamentCandidaturesListTitle: "List of Candidatures", parliamentTimeRemaining: "Time remaining", - parliamentNoLeader: "No winning candidature, yet.", parliamentElectionsStatusTitle: "Next Government", parliamentProposalDeadlineLabel: "Deadline", + parliamentViewReport: "View Report", parliamentProposalTimeLeft: "Time left", - parliamentProposalOnTrack: "On track to pass", - parliamentProposalOffTrack: "Not enough support yet", parliamentRulesTitle: "How Parliament works", parliamentRulesIntro: "Election resolve every 2 months; candidatures are continuous and reset when a government is chosen.", parliamentRulesCandidates: "Any inhabitant may propose themself, another inhabitant, or any tribe. Each inhabitant can propose up to 3 candidatures per cycle; duplicates in the same cycle are rejected.", parliamentRulesElection: "The winner is the candidature with the most votes at resolution time.", parliamentRulesTies: "Tie-break order: highest inhabitant karma; if tied, oldest profile; if still tied, earliest proposal; then lexicographic by ID.", - parliamentRulesFallback: "If no one votes, the latest proposed candidature wins. If there are no candidatures, a random tribe is selected.", parliamentRulesTerm: "The Government tab shows the current government and stats.", parliamentRulesMethods: "Forms: Anarchy (simple majority), Democracy (50%+1), Majority (80%), Minority (20%), Karmatocracy (highest-karma proposals), Dictatorship (instant approval).", parliamentRulesAnarchy: "Anarchy is the default mode: if no candidature is elected at resolution, Anarchy is proclaimed. Under Anarchy, any inhabitant can propose laws.", @@ -1257,6 +1346,7 @@ module.exports = { parliamentRulesLaws: "When a proposal meets its threshold, it becomes a Law and appears in the Laws tab with its enactment date.", parliamentRulesHistorical: "In the Historical tab you can see every government cycle that has occurred and data about its management.", parliamentRulesLeaders: "In the Leaders tab you can see a ranking of inhabitants/tribes that have governed (or stood as candidates), ordered by efficiency.", + parliamentRulesHemicycle: "The hemicycle gives one seat per inhabitant: the government bloc (the winning candidature's voters and the leader), one opposition bloc per other candidature of the cycle, and abstention. The dashed marks show the election quorum (25% of the census) and the approval threshold of the current method. \"By LARP house\" colours the same seats by house.", parliamentProposalVoteStatusLabel: "Vote status", parliamentProposalOnTrackYes: "Threshold reached", parliamentProposalOnTrackNo: "Below threshold", @@ -1270,11 +1360,8 @@ module.exports = { courtsCaseFormTitle: "Open Case", courtsCaseRespondent: "Accused / Respondent", courtsCaseRespondentPh: "Oasis ID (@...) or Tribe name", - courtsCaseMediatorsAccuser: "Mediators (accuser)", + courtsCaseSubjectPlaceholder: "Subject or short description", courtsCaseMethod: "Resolution method", - courtsCaseDescription: "Description (max 1000 chars.)", - courtsCaseEvidenceTitle: "Case evidence", - courtsCaseEvidenceHelp: "Attach images, audios, documents (PDF) or videos that support your case.", courtsCaseSubmit: "File Case", courtsNominateJudge: "Nominate Judge", courtsJudgeId: "Judge", @@ -1308,34 +1395,33 @@ module.exports = { courtsMethodDICTATOR: "Dictator", courtsMethodPOPULAR: "Popular", courtsMethodKARMATOCRACY: "Karmatocracy", + courtsCaseType: "Case type", + courtsPresetMinorConflict: "Minor conflict", + courtsPresetModerateConflict: "Moderate conflict", + courtsPresetSevereConflict: "Severe conflict", + courtsPresetHarassment: "Harassment or abuse", + courtsPresetContentModeration: "Content moderation", + courtsPresetBanRestriction: "Ban or restriction", + courtsPresetGovernanceDispute: "Governance dispute", + courtsStatusOPEN: "Open", + courtsStatusIN_PROGRESS: "In progress", + courtsStatusDECIDED: "Decided", + courtsStatusCLOSED: "Closed", + courtsStatusSOLVED: "Solved", + courtsStatusUNSOLVED: "Unsolved", + courtsStatusDISCARDED: "Discarded", courtsRulesTitle: "How Courts work", courtsRulesIntro: "Courts are a community-run process to resolve conflicts and promote restorative justice. Dialogue, clear evidence, and proportional remedies are prioritized.", courtsRulesLifecycle: "Process: 1) Open case 2) Select method 3) Submit evidence 4) Hearing and deliberation 5) Verdict and remedy 6) Compliance and closure 7) Appeal (if applicable).", - courtsRulesRoles: "Accuser: opens the case. Defence: accused person or tribe. Method: mechanism chosen by the community to facilitate, assess evidence, and issue a verdict. Witness: provides testimony or evidence. Mediators: neutral people invited by the accuser and/or the defence, with access to all details, who help de-escalate the conflict and co-create agreements.", + courtsRulesRoles: "Accuser: opens the case. Defence: accused inhabitant or tribe. Method: mechanism chosen by the community to facilitate, assess evidence, and issue a verdict. Witness: provides testimony or evidence. Mediators: neutral inhabitants invited by the accuser and/or the defence, with access to all details, who help de-escalate the conflict and co-create agreements.", courtsRulesEvidence: "Description up to 1000 characters. Attach relevant and lawful images, audio, video, and PDF documents. Do not share sensitive private data without consent.", courtsRulesDeliberation: "Hearings may be public or private. Judges ensure respect, request clarifications, and may discard irrelevant or unlawful material.", courtsRulesVerdict: "Restoration is prioritized: apologies, mediation agreements, content moderation, temporary restrictions, or other proportional measures. The reasoning must be recorded.", courtsRulesAppeals: "Appeal: allowed when there is new evidence or a clear procedural error. Must be filed within 7 days unless otherwise stated.", - courtsRulesPrivacy: "Respect privacy and safety. Doxing, hate, or threats are removed. Judges may edit or seal parts of the record to protect people at risk.", + courtsRulesPrivacy: "Respect privacy and safety. Doxing, hate, or threats are removed. Judges may edit or seal parts of the record to protect inhabitants at risk.", courtsRulesMisconduct: "Harassment, manipulation, or fabricated evidence may lead to immediate negative resolution.", courtsRulesGlossary: "Case: record of a conflict. Evidence: materials that support claims. Verdict: decision with remedy. Appeal: request to review the verdict.", courtsFilterActions: "ACTIONS", - courtsNoActions: "No pending actions for your role.", - courtsCaseTitlePlaceholder: "Short description of the conflict", - courtsCaseSeverity: "Severity", - courtsCaseSeverityNone: "No severity tag", - courtsCaseSeverityLOW: "Low", - courtsCaseSeverityMEDIUM: "Medium", - courtsCaseSeverityHIGH: "High", - courtsCaseSeverityCRITICAL: "Critical", - courtsCaseSubject: "Topic", - courtsCaseSubjectNone: "No topic tag", - courtsCaseSubjectBEHAVIOUR: "Behaviour", - courtsCaseSubjectCONTENT: "Content", - courtsCaseSubjectGOVERNANCE: "Governance / rules", - courtsCaseSubjectFINANCIAL: "Financial / resources", - courtsCaseSubjectOTHER: "Other", - courtsHiddenRespondent: "Hidden (only visible to involved roles).", courtsThRole: "Role", courtsRoleAccuser: "Accuser", courtsRoleDefence: "Defence", @@ -1346,51 +1432,23 @@ module.exports = { courtsAssignJudgeBtn: "Choose judge", trendingTitle: "Trending", exploreTrending: "Explore the most popular content in your network.", - bookmarkButton: "BOOKMARKS", - transferButton: "TRANSFERS", - eventButton: "EVENTS", - taskButton: "TASKS", votesButton: "VOTATIONS", - reportButton: "REPORTS", - feedButton: "FEED", - marketButton: "MARKET", - imageButton: "IMAGES", - audioButton: "AUDIOS", - videoButton: "VIDEOS", - documentButton: "DOCUMENTS", - torrentButton: "TORRENTS", - noTrendingFound: "No trending content found.", - noContentMessage: "No trending content available, yet.", - trendingDescription: "Description", - trendingDate: "Date", - trendingLocation: "Location", - trendingPrice: "Price", - trendingUrl: "URL", - trendingCategory: "Category", - trendingStart: "Start", - trendingEnd: "End", - trendingPriority: "Priority", - trendingStatus: "Status", - trendingFrom: "From", - trendingTo: "To", - trendingConcept: "Concept", - trendingAmount: "Amount", - trendingDeadline: "Deadline", - trendingItemStatus: "Item Status", - trendingTotalVotes: "Total Votes", + shopProductButton: "SHOP", + podcastButton: "PODCAST", + podcastEpisodeButton: "EPISODE", + campaignButton: "CAMPAIGN", + logisticsRouteButton: "ROUTE", trendingTotalOpinions: "Total Opinions", - trendingNoContentMessage: "No trending content available, yet.", - trendingAuthor: "By", - trendingCreatedAtLabel: "Created At", + trendingNoContentMessage: "No trending available, yet.", trendingTotalCount: "Total Count", tasksTitle: "Tasks", tasksDescription: "Discover and manage tasks in your network.", + taskSearchPlaceholder: "Search in Tasks...", taskTitleLabel: "Title", taskDescriptionLabel: "Description", taskStartTimeLabel: "Start Time", taskEndTimeLabel: "End Time", taskPriorityLabel: "Priority", - taskPrioritySelect: "Select priority", taskPriorityUrgent: "Urgent", taskPriorityHigh: "High", taskPriorityMedium: "Medium", @@ -1400,13 +1458,13 @@ module.exports = { taskVisibilityLabel: "Visibility", taskPublic: "public", taskPrivate: "private", - taskCreatedAt: "Created At", - taskBy: "By", taskStatus: "Status", taskStatusOpen: "Open", taskStatusInProgress: "In Progress", taskStatusClosed: "Closed", - taskAssignedTo: "Assigned to", + taskAssignedTo: "Assigned", + taskAssignedChip: "Assigned", + taskUnassignedChip: "Unassigned", taskAssignees: "Assignees", taskAssignButton: "Assign to Me", taskUnassignButton: "Unassign", @@ -1414,12 +1472,12 @@ module.exports = { taskUpdateButton: "Update", taskDeleteButton: "Delete", taskFilterAll: "ALL", + taskFilterRecent: "RECENT", taskFilterMine: "MINE", taskFilterOpen: "OPEN", taskFilterInProgress: "IN-PROGRESS", taskFilterClosed: "CLOSED", taskFilterAssigned: "ASSIGNED", - taskFilterArchived: "ARCHIVED", taskFilterUrgent: "URGENT", taskFilterHigh: "HIGH", taskFilterMedium: "MEDIUM", @@ -1432,35 +1490,47 @@ module.exports = { taskInProgressTitle: "In Progress Tasks", taskClosedTitle: "Closed Tasks", taskAssignedTitle: "Assigned Tasks", - taskArchivedTitle: "Archived Tasks", - taskPublicTitle: "Public Tasks", - taskPrivateTitle: "Private Tasks", notasks: "No tasks available.", noLocation: "No location specified", taskSetStatus: "Set Status", - eventTitle: "Events", eventDateLabel: "Date", + eventRecurrenceLabel: "Recurrence", + eventRecurrenceUntil: "Repeat until", + eventRecurrenceHint: "Leave the repeat-until date empty for a one-off event.", + eventUpcomingDates: "Upcoming dates", eventsTitle: "Events", eventsDescription: "Discover and manage events in your network.", - eventDescription: "Description", + eventSearchPlaceholder: "Search in Events...", eventPrice: "Price", eventStatus: "Status", - eventOrganizer: "Organizer", eventAllSectionTitle: "Events", eventMineSectionTitle: "Your Events", - eventArchivedTitle: "Archived Events", eventCreateSectionTitle: "Create Event", eventUpdateSectionTitle: "Update Event", - eventDeleteButton: "Delete", eventCreateButton: "Create Event", eventAddToCalendar: "Add to Calendar", eventVisitCalendar: "Visit Calendar", viewTask: "View Task", + viewEvent: "View Event", + viewPad: "View Pad", + viewMap: "View Map", + viewCalendar: "View Calendar", viewReport: "View Report", viewTransfer: "View Transfer", viewItem: "View Item", viewShop: "View Shop", viewProject: "View Project", + viewJob: "View Job", + viewPlace: "View Place", + generatePdf: "Generate PDF", + sharePm: "Share via PM", + galleryImages: "Images (max-size: 75MB each)", + galleryPhotos: "Images", + galleryAddPhoto: "Add Image", + galleryRemovePhoto: "Remove", + galleryVideo: "Video (max-size: 75MB)", + galleryAddVideo: "Add Video", + galleryRemoveVideo: "Remove video", viewVotation: "View Votation", voteCastTitle: "Cast Vote", voteResults: "Results", @@ -1468,48 +1538,26 @@ module.exports = { eventDescriptionLabel: "Description", eventDescriptionPlaceholder: "Enter event description...", eventUpdateButton: "Update", - eventAttendeesLabel: "Attendees", - eventAttendeesPlaceholder: "Enter attendees, separated by commas...", eventTagsLabel: "Tags", - eventTagsPlaceholder: "Enter event tags, separated by commas...", - eventTags: "Tags", eventPriceLabel: "Price", eventUrlLabel: "URL", eventAttendees: "Attendees", - noAttendees: "No attendees yet", - eventCreatedAt: "Created At", eventLocation: "Location", eventLocationLabel: "Location", - eventNoLocation: "No location specified", - eventNoURL: "No URL specified", - eventBy: "By", noevents: "No events available.", eventDate: "Date", - eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Attend Event", - eventUnattendButton: "Unattend Event", - eventCreatedBy: "Created By", - eventAttendeesCount: "Attendees Count", - eventCreatedByYou: "You created this event", eventFilterAll: "ALL", + eventFilterRecent: "RECENT", eventFilterMine: "MINE", eventFilterToday: "TODAY", eventFilterWeek: "THIS WEEK", eventFilterMonth: "THIS MONTH", eventFilterYear: "THIS YEAR", eventFilterArchived: "ARCHIVED", - eventTodayTitle: "Today's Events", - eventThisWeekTitle: "This Week's Events", - eventThisMonthTitle: "This Month's Events", - eventThisYearTitle: "This Year's Events", eventPrivacyLabel: "Visibility", eventPublic: "Public", eventPrivate: "Private", - eventPublicTitle: "Public Events", - eventNoPrice: "Free Event", - eventNoImage: "No image uploaded", - eventAttendConfirmation: "You are now attending this event", - eventUnattendConfirmation: "You are no longer attending this event", eventAttended: "Attended", eventUnattended: "Unattended", eventStatusOpen: "Open", @@ -1520,9 +1568,14 @@ module.exports = { tagsTopSectionTitle: "Top Tags", tagsCloudSectionTitle: "Tags Cloud", tagsFilterAll: "ALL", + tagsFilterMine: "MINE", + tagsFilterRecent: "RECENT", + tagsMineSectionTitle: "My Tags", + tagsRecentSectionTitle: "Recent Tags", tagsFilterTop: "TOP", tagsFilterCloud: "CLOUD", tagsNoItems: "No tags available.", + tagsSearchPlaceholder: "Search in Tags...", tagsTableHeaderTag: "TAG/LINK:", tagsTableHeaderCount: "COUNTER:", transfersTitle: "Transfers", @@ -1530,7 +1583,10 @@ module.exports = { transfersFrom: "From", transfersTo: "To", transfersFilterAll: "ALL", + transfersFilterRecent: "RECENT", transfersFilterMine: "MINE", + transfersFilterMinePending: "PENDING", + transfersFilterMineConfirmed: "SIGNED", transfersFilterUBI: "UBI", transfersFilterMarket: "MARKET", transfersFilterTop: "TOP", @@ -1545,7 +1601,6 @@ module.exports = { transfersCategoryEconomic: "Economic", transfersCategoryTime: "Time", transfersCategoryTrust: "Trust", - transfersCategoryApply: "Apply", transfersUnitEco: "ECO", transfersUnitHours: "h", transfersUnitTrust: "trust", @@ -1562,18 +1617,12 @@ module.exports = { transfersDeadline: "Deadline", transfersTags: "Tags", transfersStatus: "Status", - transfersStatusUnconfirmed: "UNCONFIRMED", - transfersStatusClosed: "CLOSED", - transfersStatusDiscarded: "DISCARDED", - transfersCreatedAt: "Created At", transfersConfirmations: "CONFIRMATIONS", - transfersContainingBlock: "Containing block", - transfersExportContract: "Create Contract", + transfersExportContract: "Generate Bill", transfersConfirmButton: "Confirm Transfer", transfersNoItems: "No transfers found.", transfersMineSectionTitle: "Your Transfers", transfersUBISectionTitle: "UBI Transfers", - transfersMarketSectionTitle: "Market Transfers", transfersTopSectionTitle: "Top Transfers", transfersPendingSectionTitle: "Pending Transfers", transfersUnconfirmedSectionTitle: "Unconfirmed Transfers", @@ -1581,21 +1630,13 @@ module.exports = { transfersDiscardedSectionTitle: "Discarded Transfers", transfersCreateSectionTitle: "Create transfer", transfersAllSectionTitle: "Transfers", - transfersFilterFavs: "Favorites", - transfersFavsSectionTitle: "Favorite transfers", - transfersSearchLabel: "Search", - transfersSearchPlaceholder: "Search concept, tags, users...", + transfersSearchPlaceholder: "Search in Transfers...", transfersMinAmountLabel: "Min amount", transfersMaxAmountLabel: "Max amount", - transfersSortLabel: "Sort by", transfersSortRecent: "Most recent", transfersSortAmount: "Highest amount", transfersSortDeadline: "Closest deadline", transfersSearchButton: "Search", - transfersFavoriteButton: "Favorite", - transfersUnfavoriteButton: "Unfavorite", - transfersMessageUserButton: "Message", - transfersExpiringSoonBadge: "EXPIRING", transfersExpiredBadge: "EXPIRED", transfersUpdatedAt: "Updated", transfersNoMatch: "No transfers match your search.", @@ -1608,6 +1649,10 @@ module.exports = { voteClosedTitle: "Closed Votations", voteAllSectionTitle: "Votations", voteCreateButton: "Create Votation", + voteErrorTitle: "Check the form", + voteErrorDeadlineMin: "The deadline must be at least {days} days from now.", + voteErrorGeneric: "The vote could not be saved.", + voteLocked: "This vote has already received votes or opinions, or belongs to Parliament or Courts: it can no longer be edited or deleted.", voteUpdateButton: "Update", voteDeleteButton: "Delete", voteOptionYes: "YES", @@ -1617,6 +1662,7 @@ module.exports = { voteFollowMajority: "FOLLOW MAJORITY", voteNotInterested: "NOT INTERESTED", voteFilterAll: "ALL", + voteFilterRecent: "RECENT", voteFilterMine: "MINE", voteFilterOpen: "OPEN", voteFilterClosed: "CLOSED", @@ -1632,37 +1678,28 @@ module.exports = { voteTags: "Tags", voteOpinions: "Opinions", novotes: "No voting proposals available.", - voteBy: "By", voteCreatedAt: "Created At", voteNoQuestion: "No question provided", voteUnknownCreator: "Unknown Creator", voteUnknownDate: "Unknown Date", - errorVoteNotFound: "Vote not found", - errorAlreadyVoted: "You have already opined.", - errorVoteClosedCannotEdit: "You cannot edit a closed votation", - errorVoteDeadlinePassed: "The deadline for this votation has passed", - errorRetrievingVote: "Error retrieving vote", - errorCreatingVote: "Error creating vote", - errorVoteAlreadyVoted: "Cannot edit after opinion has been cast", - errorDeletingOldVote: "Error deleting old opinion", - errorCreatingUpdatedVote: "Error creating updated opinion", - errorCreatingTombstone: "Error creating tombstone", voteDetailSectionTitle: 'Vote details', voteCommentsLabel: 'Comments', voteCommentsForumButton: 'Open discussion', voteCommentsSectionTitle: 'Open discussion', voteNoCommentsYet: 'There are no comments yet. Be the first to reply.', + commentsGoToLast: "Go to the last comment", voteNewCommentPlaceholder: 'Write your comment here…', voteNewCommentButton: 'Post comment', voteNewCommentLabel: 'Add a comment', cvTitle: "CV", - cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Edit CV", cvCreateSectionTitle: "Create CV", cvDescription: "Manage and share your professional skills and information.", cvNameLabel: "Full Name", cvDescriptionLabel: "Summary", cvPhotoLabel: "Photo", + cvPdfLabel: "CV (PDF)", + cvVisitButton: "Visit CV", cvPersonalExperiencesLabel: "Personal Experiences", cvPersonalSkillsLabel: "Personal Skills (comma-separated)", cvOasisExperiencesLabel: "Oasis Contribution Experiences", @@ -1675,29 +1712,130 @@ module.exports = { cvLocationLabel: "Location", cvStatusLabel: "Status", cvPreferencesLabel: "Preferences", - cvOasisContributorLabel: "Oasis Contributor", cvPersonal: "Personal", cvOasis: "Oasis Contributor (optional)", - cvOasisContributorView: "Oasis Contribution", + cvOasisContributorView: "Contribution", cvEducational: "Educational (optional)", cvEducationalView: "Educational", cvProfessional: "Professional (optional)", cvProfessionalView: "Professional", cvAvailability: "Availability (optional)", - cvAvailabilityView: "Availability", cvUpdateButton: "Update", cvCreateButton: "Create CV", - cvContactLabel: "Contact", cvCreatedAt: "Created At", cvUpdatedAt: "Updated At", cvEditButton: "Update", cvDeleteButton: "Delete", cvNoCV: "No CV found.", blogSubject: "Subject", - blogMessage: "Body capped at 8096 characters.", - blogImage: "Upload media (max-size: 50MB)", - blogPublish: "Preview", - noPopularMessages: "No popular messages published, yet", + blogMessage: "Message", + blogImage: "Upload media (max-size: 75MB)", + blogMedia: "Attachments (up to 8 images and a video)", + blogPublish: "Publish", + pollsTitle: "Polls", + dataTitle: "Matches", + dataDescription: "Explore and visualize the matches in your network.", + dataFilterAll: "ALL", + dataFilterMine: "MINE", + dataFilterRecent: "RECENT", + dataFilterTop: "TOP", + dataKindInhabitants: "INHABITANTS", + dataKindJobs: "JOBS", + dataKindProjects: "PROJECTS", + dataKindEvents: "EVENTS", + dataKindTribes: "TRIBES", + dataKindMarket: "MARKET", + dataKindHousing: "HOUSING", + dataKindIndustry: "INDUSTRY", + dataKindTasks: "TASKS", + dataKindReports: "REPORTS", + dataKindVotes: "VOTATIONS", + dataSearchPlaceholder: "Search in Matches...", + dataCohesionTitle: "Cohesion Coefficient (CC)", + dataCcShort: "CC", + dataCohesionHint: "Average affinity between everything the network has published.", + dataAffinity: "Affinity", + dataCommonTerms: "Keyword", + dataReasonFollowing: "You support this inhabitant", + dataReasonTribe: "Shares a tribe with you", + dataReasonPinned: "Matches something you pinned", + dataReasonCv: "Matches your CV skills", + dataReasonContent: "Related to what you publish", + dataReasonRelated: "Related to your interests", + dataReasonAlike: "Published by an inhabitant like you", + dataReasonSupportsYou: "Supports you", + dataReasonMutual: "Mutual support", + dataReasonRated: "Well rated by the network", + dataReasonNear: "Near you", + dataStatPeople: "CVs", + dataStatConnected: "Connected", + dataStatSkills: "Skills", + dataBestMatch: "Best Match", + dataStatIsolated: "Isolated", + dataStatEntities: "Entities compared", + dataStatTerms: "Distinct terms", + dataStatPairs: "Connected pairs", + dataMatchesTitle: "Matches", + dataMatchesHint: "Personalized algorithm suggestions.", + dataTermsTitle: "Keywords", + dataTermsHint: "The terms that appear across the most content.", + dataConnections: "Linked matches", + dataNoMatches: "No cross-data to show.", + dataNoProfile: "Publish content or write your CV so the algorithm can learn what interests you.", + pollsDescription: "Module to ask the network and count the answers.", + pollFilterAll: "ALL", + pollFilterMine: "MINE", + pollFilterRecent: "RECENT", + pollFilterTop: "TOP", + pollFilterVoted: "VOTED", + pollFilterOpen: "OPEN", + pollFilterClosed: "CLOSED", + pollCreateButton: "Create Poll", + pollCreateTitle: "Create Poll", + pollEditTitle: "Edit Poll", + pollQuestion: "Question", + pollQuestionPlaceholder: "What do you want to ask?", + pollOptions: "Options (one per line)", + pollOutcome: "Result", + pollNoVotesYet: "No votes yet", + pollTie: "Tie", + pollOptionsPlaceholder: "Plaza\nPark\nBar", + pollAnonymous: "Anonymous", + pollAnonymousLabel: "Anonymous poll", + pollMultiple: "Multiple choice", + pollMultipleLabel: "Allow multiple answers", + pollDeadline: "Deadline", + pollTags: "Tags", + pollPublishButton: "Publish Poll", + pollUpdateButton: "Update", + pollCloseButton: "Close", + pollDeleteButton: "Delete", + pollVoteButton: "Vote", + pollChangeVote: "Change Vote", + pollVoters: "Voters", + pollComments: "Comments", + pollStatusLabel: "Status", + pollStatusOpen: "OPEN", + pollStatusClosed: "CLOSED", + pollSearchPlaceholder: "Search in Polls...", + pollsNoItems: "No polls found.", + viewPoll: "View Poll", + pollChatCreate: "Create Poll", + pollInChat: "Poll", + blogTitle: "Blogs", + blogDescription: "Module to discover and manage blogs.", + blogFilterAll: "ALL", + blogFilterMine: "MINE", + blogFilterRecent: "RECENT", + blogFilterFavorites: "FAVORITES", + blogFilterTop: "TOP", + blogCreateButton: "Create Blog", + blogSearchPlaceholder: "Search in Blogs...", + blogComments: "Comments", + blogAllowComments: "Allow comments", + blogCommentsClosed: "The author has closed comments on this blog.", + blogNoItems: "No blogs found.", + viewBlog: "View Blog", forumTitle: "Forums", forumCategoryLabel: "Category", forumTitleLabel: "Title", @@ -1705,83 +1843,63 @@ module.exports = { forumCreateButton: "Create forum", forumCreateSectionTitle: "Create forum", forumDescription: "Talk openly with other inhabitants in your network.", + forumSearchPlaceholder: "Search in Forums...", forumFilterAll: "ALL", forumFilterMine: "MINE", forumFilterRecent: "RECENT", forumFilterTop: "TOP", - forumMineSectionTitle: "Your Forums", - forumRecentSectionTitle: "Recent Forums", - forumAllSectionTitle: "Forums", - forumDeleteButton: "Delete", forumParticipants: "participants", forumMessages: "messages", - forumLastMessage: "Last message", forumMessageLabel: "Message", forumMessagePlaceholder: "Write your message...", forumSendButton: "Send", - forumVisitForum: "Visit Forum", noForums: "No forums found.", - forumVisitButton: "Visit forum", - forumCatGENERAL: "General", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", + forumVisitButton: "Visit Forum", + forumTopicLabel: "Topic", larpTitle: "L.A.R.P.", larpDescription: "A live action role-playing layer for collaborative experimentation.", + larpNoHousesMatch: "No houses match your search.", + larpSearchPlaceholder: "Search in L.A.R.P....", larpAllHouses: "Houses:", larpFilterRuling: "RULING", larpFilterHouses: "HOUSES", larpFilterRules: "FAQ", larpRulesTitle: "F.A.Q.", - larpRulesEntryTitle: "Starting house", larpRulesEntryText: "Every inhabitant begins in ACADEMIA by default.", - larpRulesTestEntry: "WILL test", - larpRulesTestEntryText: "Each option weights one or more houses; when you submit, the system tallies the scores and auto-admits you into the highest-scoring house.", - larpRulesInviteEntry: "Invitation code", + larpRulesTestEntryText: "In the WILL test each option weights one or more houses; when you submit, the system tallies the scores and auto-admits you into the highest-scoring house.", larpRulesInviteEntryText: "House members can generate invitation codes that grant direct access to the house.", - larpRulesOneHouseTitle: "One house at a time", larpRulesOneHouseText: "You can belong to at most one house at any given moment. If you belong to none, you are in ACADEMIA. Every non-ACADEMIA house page shows a \"Leave House\" button to its members that resets membership back to ACADEMIA. Leaving does NOT waive the test cooldown.", - larpRulesLeaveText: "Members of any house can return to ACADEMIA at any time from their house page; doing so resets their membership but does not waive the test cooldown.", - larpRulesGovernanceTitle: "Governance Wall", larpRulesGovernanceText: "During its cycle, the governing house wears the \"Ruling\" badge and is the only one whose Wall is surfaced publicly under the RULING tab.", - larpRulesSignTitle: "L.A.R.P. Emblem", + larpRulesWallText: "Each house has a Wall where its members write. A house Wall is private to the house, except the one of the governing house and the one of ACADEMIA, which anybody can read.", larpRulesSignText: "Inhabitants can choose whether to add a sensor on their avatar with the emblem of their current house.", larpPostsTitle: "Wall", larpPostsEmpty: "No posts yet.", - larpPostLabel: "New post", larpPostPlaceholder: "What does this house need to say?", - larpPostPublic: "Public (visible to anyone, otherwise only members)", larpPostSubmit: "Publish", - larpPostMembersOnly: "Members only", larpCycleLabel: "Cycle:", larpCyclesUntilRuling: "Next governance cycle", larpVisitTribe: "Visit Tribe", - larpGoverning: "Governing:", + larpStartJourney: "Start Journey", + larpGoverning: "Ruling:", + larpStartHere: "Start here:", larpMyHouse: "My House:", larpMembersCount: "Members", - larpMembersTitle: "Members", - larpNoMembers: "No members yet.", larpRolesLabel: "Roles", larpFunctionLabel: "Function", larpMonthLabel: "Governance cycle", larpLeaveToAcademia: "Leave House", - larpAcademiaJoinTitle: "Houses Current Status", + larpAcademiaJoinTitle: "L.A.R.P. Houses", larpWillTestTitle: "WILL test", larpWillTestHint: "Once completed, you will be assigned the house that best matches your answers. Your individual answers are processed locally and never stored — only the resulting house assignment is published to your feed.", - larpAcademiaJoinHint: "Take the psychological profile test to be assigned the house that fits you best, or redeem an invitation code shared by a member of a house.", - larpTakeTestButton: "Take the profile test", larpInviteRedeem: "Join House", larpInviteCreate: "Generate invitation code", larpInvitePlaceholder: "Invitation code", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "House invitation code", - invitesHouseJoinButton: "Join House", larpInviteBannerTitle: "New invitation code", larpInviteBannerHint: "Share this code with someone in ACADEMIA. It expires in 30 cycles or after one use.", - larpTestAssignedHouse: "Assigned house", larpTestRankingTitle: "Score by house", larpLastAttemptTitle: "Your last attempt", larpLastAttemptHouse: "House", - larpLastAttemptResult: "Result", larpLastAttemptWhen: "When", larpLastAttemptCooldown: "Next attempt in", larpTestTitle: "Profile test", @@ -1792,7 +1910,7 @@ module.exports = { larpProfileQ1O3: "Research the literature", larpProfileQ1O4: "Disrupt the system that creates it", larpProfileQ2: "What gives meaning to your daily work?", - larpProfileQ2O1: "Defending the people I love", + larpProfileQ2O1: "Defending the inhabitants I love", larpProfileQ2O2: "Creating something tangible", larpProfileQ2O3: "Healing or nurturing life", larpProfileQ2O4: "Crafting words and ideas", @@ -1860,34 +1978,16 @@ module.exports = { larpTestCooldownActive: "Next test available in", larpTestCooldownDays: "cycles", larpTestResultTitle: "Test result", - larpTestPassed: "Test passed — welcome!", - larpTestFailed: "Test not passed", larpTestScore: "Score", larpTestNextAttempt: "You can attempt another test in 30 cycles.", larpGoToHouse: "Go to your house", larpBackToAcademia: "Back to ACADEMIA", - larpBackToHouses: "◀ Houses", larpBadgeYou: "You", larpBadgeRuling: "Ruling", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Module for the live-action role-playing layer of Oasis (the 9 Houses).", - forumCatPOLITICS: "Politics", - forumCatTECH: "Tech", - forumCatSCIENCE: "Science", - forumCatMUSIC: "Music", - forumCatART: "Art", - forumCatGAMING: "Gaming", - forumCatBOOKS: "Books", - forumCatFILMS: "Films", - forumCatPHILOSOPHY: "Philosophy", - forumCatSOCIETY: "Society", - forumCatPRIVACY: "Privacy", - forumCatCYBERWARFARE: "Cyberwarfare", - forumCatSURVIVALISM: "Survivalism", + modulesLarpDescription: "Module for the live-action role-playing layer.", imageTitle: "Images", imageDescription: "Explore and manage image content in your network.", - imagePluginTitle: "Title", - imagePluginDescription: "Description", imageMineSectionTitle: "Your Images", imageCreateSectionTitle: "Upload Image", imageUpdateSectionTitle: "Update Image", @@ -1896,17 +1996,14 @@ module.exports = { imageTopSectionTitle: "Top Images", imageFavoritesSectionTitle: "Favorites", imageGallerySectionTitle: "Gallery", - imageMemeSectionTitle: "Memes", imageFilterAll: "ALL", imageFilterMine: "MINE", imageFilterRecent: "RECENT", imageFilterTop: "TOP", imageFilterFavorites: "FAVORITES", imageFilterGallery: "GALLERY", - imageFilterMeme: "MEMES", imageCreateButton: "Upload Image", imageUpdateButton: "Update", - imageDeleteButton: "Delete", imageAddFavoriteButton: "Add favorite", imageRemoveFavoriteButton: "Remove favorite", imageFileLabel: "Select an image file (.jpeg, .jpg, .png, .gif)", @@ -1916,17 +2013,16 @@ module.exports = { imageTitlePlaceholder: "Optional", imageDescriptionLabel: "Description", imageDescriptionPlaceholder: "Optional", - imageMemeLabel: "¿MEME?", imageNoFile: "No image file provided", noImages: "No images available.", - imageSearchPlaceholder: "Search title, tags, description, author...", + imageSearchPlaceholder: "Search in Images...", imageSortRecent: "Most recent", imageSortOldest: "Oldest", imageSortTop: "Most voted", imageSearchButton: "Search", - imageMessageAuthorButton: "Message", imageUpdatedAt: "Updated", imageNoMatch: "No images match your search.", + feedSearchPlaceholder: "Search in Feed...", feedTitle: "Feed", feedDetailTitle: "Feed", feedOpenDiscussion: "Open discussion", @@ -1936,7 +2032,6 @@ module.exports = { createFeedButton: "Send Feed!", feedPlaceholder: "What's happening? (max 280 characters)", TODAYButton: "TODAY", - CREATEButton: "Create Feed", totalOpinions: "Total Opinions", moreVoted: "More Voted", noFeedsFound: "No feeds found.", @@ -1946,7 +2041,7 @@ module.exports = { alreadyRefeeded: "You already refeeded this.", activityTitle: "Activity", yourActivity: "Your Activity", - globalActivity: "Global Activity", + globalActivity: "Activity", activityList: "Activity", activityDesc: "See the latest activity in your network.", allButton: "ALL", @@ -1959,20 +2054,13 @@ module.exports = { concept: "Concept", description: "Description", meme: "Meme", - activityContact: "Contact", - activityBy: "Name", - activityPixelia: "New pixel added", - viewImage: "View image", - playAudio: "Play audio", - playVideo: "Play video", typeRecent: "RECENT", - errorActivity: "Error retrieving activity", - typePost: "BLOGS", - recentButton: "RECENT", + typeTop: "TOP", + typePost: "BLOG", + activityUpdateLabel: "UPDATE", typeTribe: "TRIBES", typeLarp: "L.A.R.P.", typeInhabitants: "INHABITANTS", - activityProfileUpdated: "updated their profile", typeAbout: "INHABITANTS", typeCurriculum: "CVS", typeImage: "IMAGES", @@ -2016,8 +2104,6 @@ module.exports = { typeCourtsSettlementAccepted: "Courts · Settlement Accepted", typeCourtsNomination: "Courts · Nomination", typeCourtsNominationVote: "Courts · Nomination Vote", - activitySupport: "New alliance forged", - activityJoin: "New PUB joined", question: "Question", deadline: "Deadline", status: "Status", @@ -2031,12 +2117,8 @@ module.exports = { date: "Date", category: "Category", attendees: "Attendees", - activitySpread: "->", - visitLink: "Visit Link", - viewDocument: "View Document", location: "Location", contentWarning: "Subject", - personName: "Inhabitant Name", bankWalletConnected: "ECOin Wallet", bankUbiReceived: "UBI Received", bankTx: "Tx", @@ -2054,7 +2136,6 @@ module.exports = { aiExchangeMarkHelpful:"+1 helpful", activityProjectFollow: "%OASIS% is now %ACTION% this project %PROJECT%", activityProjectUnfollow: "%OASIS% is now %ACTION% this project %PROJECT%", - activityProjectPledged: "%OASIS% has %ACTION% %AMOUNT% to project %PROJECT%", following: "FOLLOWING", unfollowing: "UNFOLLOWING", pledged: "PLEDGED", @@ -2100,26 +2181,17 @@ module.exports = { courtsVotesSlashTotal: "YES/TOTAL", courtsOpenVote: "Open vote", courtsAnswerTitle: "Answer", - courtsStanceADMIT: "Admit", - courtsStanceDENY: "Deny", - courtsStancePARTIAL: "Partial", - courtsStanceCOUNTERCLAIM: "Counterclaim", - courtsStanceNEUTRAL: "Neutral", courtsVerdictResult: "Result", courtsVerdictOrders: "Orders", courtsSettlementText: "Settlement", courtsSettlementAccepted: "Accepted", - courtsSettlementPending: "Pending", courtsJudge: "Judge", courtsThSupports: "Supports", courtsFilterOpenCase: 'Open Case', - courtsEvidenceFileLabel: 'Evidence file (image, audio, video or PDF)', - courtsCaseMediators: 'Mediators', courtsCaseMediatorsPh: 'Mediator Oasis IDs, separated by comma', courtsMediatorsLabel: 'Mediators', courtsThCase: 'Case', courtsThCreatedAt: 'Start date', - courtsThActions: 'Actions', courtsPublicPrefLabel: 'Visibility after resolution', courtsPublicPrefYes: 'I agree this case can be fully public', courtsPublicPrefNo: 'I prefer to keep the details private', @@ -2127,8 +2199,11 @@ module.exports = { courtsMethodMEDIATION: 'Mediation', courtsNoCases: 'No cases.', reportsTitle: "Reports", - reportsDescription: "Manage and track reports related to issues, bugs, abuses and content warnings in your network.", + reportsDescription: "Manage and track reports in your network.", + reportsSearchPlaceholder: "Search in Reports...", reportsFilterAll: "ALL", + reportsFilterRecent: "RECENT", + reportsFilterTop: "TOP", reportsFilterMine: "MINE", reportsFilterFeatures: "FEATURES", reportsFilterBugs: "BUGS", @@ -2141,18 +2216,13 @@ module.exports = { reportsFilterInvalid: "INVALID", reportsCreateButton: "Create Report", reportsTitleLabel: "Title", - reportsDescriptionLabel: "Description", - reportsDescriptionPlaceholder: "Please provide a detailed description.", reportsCategory: "Category", - reportsCategoryLabel: "Category", reportsCategoryFeatures: "Features", reportsCategoryBugs: "Bugs", reportsCategoryAbuse: "Abuse", - reportsCategoryContent: "Content Issues", + reportsCategoryContent: "Content", reportsUpdateButton: "Update", reportsDeleteButton: "Delete", - reportsDateLabel: "Date", - reportsUploadFile: "Upload media (max-size: 50MB)", reportsMineSectionTitle: "Your Reports", reportsFeaturesSectionTitle: "Feature Requests", reportsBugsSectionTitle: "Bugs", @@ -2160,11 +2230,6 @@ module.exports = { reportsContentSectionTitle: "Content Issues", reportsAllSectionTitle: "Reports", reportsNoItems: "No reports available.", - reportsValidationTitle: "Please enter a valid title.", - reportsValidationDescription: "Description cannot be empty.", - reportsValidationCategory: "Please select a category.", - reportsCreatedAt: "Created At", - reportsCreatedBy: "By", reportsSeverity: "Severity", reportsSeverityLow: "Low", reportsSeverityMedium: "Medium", @@ -2175,18 +2240,14 @@ module.exports = { reportsStatusUnderReview: "Under Review", reportsStatusResolved: "Resolved", reportsStatusInvalid: "Invalid", - reportsUpdateStatusButton: "Update Status", - reportsAnonymityOption: "Submit Anonymously", - reportsAnonymousAuthor: "Anonymous", reportsConfirmButton: "CONFIRM REPORT!", reportsConfirmations: "Confirmations", reportsConfirmedSectionTitle: "Confirmed Reports", - reportsCreateTaskButton: "CREATE TASK", + reportsCreateTaskButton: "Create Task", reportsOpenSectionTitle: "Open Reports", reportsUnderReviewSectionTitle: "Under Review Reports", reportsResolvedSectionTitle: "Resolved Reports", reportsInvalidSectionTitle: "Invalid Reports", - reportsTemplateSectionTitle: 'Report template', reportsBugTemplateTitle: 'Reproduction details (Bugs)', reportsFeatureTemplateTitle: 'Details (Features)', reportsAbuseTemplateTitle: 'Details (Abuse)', @@ -2225,6 +2286,20 @@ module.exports = { reportsRequestedActionLabel: 'Requested action', reportsRequestedActionPlaceholder: 'Remove, hide, tag, warn, etc.', tribesTitle: "Tribes", + tribeFilterAll: "ALL", + tribeFilterRecent: "RECENT", + tribeFilterMine: "MINE", + tribeFilterMembership: "MEMBERSHIP", + tribeFilterSubtribes: "SUBTRIBES", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALLERY", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "MINE", + tribeFeedFilterALL: "ALL", + tribeFeedFilterRECENT: "RECENT", + courtsStanceDENY: "Deny", + courtsStanceADMIT: "Admit", + courtsStancePARTIAL: "Partially admit", tribeAllSectionTitle: "Tribes", tribeMineSectionTitle: "Your Tribes", tribeCreateSectionTitle: "Create Tribe", @@ -2232,24 +2307,15 @@ module.exports = { tribeGallerySectionTitle: "Tribes Gallery", tribeRecentSectionTitle: "Recent Tribes", tribeTopSectionTitle: "Popular Tribes", - tribeviewTribeButton: "Visit Tribe", - tribeviewSubTribeButton: "Visit Sub-Tribe", tribeRootLabel: "ROOT", tribeDescription: "Explore or create tribes on your network.", - tribeFilterAll: "ALL", - tribeFilterMine: "MINE", - tribeFilterMembership: "MEMBERSHIP", - tribeFilterRecent: "RECENT", - tribeFilterTop: "TOP", - tribeFilterSubtribes: "SUB-TRIBES", - tribeFilterGallery: "GALLERY", tribeMainTribeLabel: "MAIN TRIBE", tribeCreateButton: "Create Tribe", tribeUpdateButton: "Update", tribeDeleteButton: "Delete", - tribeImageLabel: "Upload media (max-size: 50MB)", + tribeImageLabel: "Upload media (max-size: 75MB)", tribeTitleLabel: "Title", - searchTribesPlaceholder: "FILTER tribes BY NAME …", + searchTribesPlaceholder: "Search in Tribes...", tribeTitlePlaceholder: "Name of the tribe", tribeDescriptionLabel: "Description", tribeDescriptionPlaceholder: "Describe this tribe", @@ -2261,13 +2327,17 @@ module.exports = { tribeModeLabel: "MODE", tribeIsAnonymousLabel: "STATUS", tribeMembersCount: "Members", - tribeInviteCodePlaceholder: "Enter invite code", - tribeJoinByCodeButton: "Join with code", - tribeJoinButton: "JOIN", tribeEnterInvite: "ENTER INVITE CODE", + subscriptionTitle: "Subscriptions", + subscriptionOn: "Subscribed", + subscriptionOff: "Not subscribed", + subscriptionSubscribe: "Subscribe", + subscriptionUnsubscribe: "Unsubscribe", + subscriptionSubscribers: "Subscribers", + pmListLabel: "Send to", + pmListNone: "— No list —", + pmListNoSubscribers: "No subscribers", tribeLeaveButton: "LEAVE", - tribeYes: "YES", - tribeNo: "NO", tribePublic: "PUBLIC", tribePrivate: "PRIVATE", tribeGenerateInvite: "SINGLE INVITATION", @@ -2276,13 +2346,8 @@ module.exports = { tribeRemoveInvitation: "REMOVE INVITATION", tribeCreatedAt: "Created at", tribeAuthor: "By", - tribeAuthorLabel: "AUTHOR", tribeStrict: "Strict", tribeOpen: "Open", - tribeFeedFilterRECENT: "RECENT", - tribeFeedFilterMINE: "MINE", - tribeFeedFilterALL: "ALL", - tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Refeeds", tribeFeedRefeed: "Refeed", tribeFeedMessagePlaceholder: "Write a feed…", @@ -2292,17 +2357,12 @@ module.exports = { tribeNotFound: "Tribe not found!", createTribeTitle: "Create Tribe", updateTribeTitle: "Update Tribe", - tribeSectionOverview: "Overview", tribeSectionInhabitants: "Inhabitants", tribeSectionVotations: "VOTATIONS", tribeSectionEvents: "EVENTS", - tribeSectionReports: "Reports", tribeSectionTasks: "TASKS", tribeSectionFeed: "FEED", tribeSectionForum: "FORUM", - tribeSectionMarket: "Market", - tribeSectionJobs: "Jobs", - tribeSectionProjects: "Projects", tribeSectionMedia: "Media", tribeSectionImages: "IMAGES", tribeSectionAudios: "AUDIOS", @@ -2340,11 +2400,6 @@ module.exports = { tribeTaskStatusClosed: "CLOSE", tribeTaskAssign: "ASSIGN", tribeTaskUnassign: "UNASSIGN", - tribeReportCreate: "Create Report", - tribeReportsEmpty: "No reports, yet.", - tribeReportTitle: "Title", - tribeReportDescription: "Description", - tribeReportCategory: "Category", tribeVotationCreate: "Create Votation", tribeVotationsEmpty: "No votations, yet.", tribeVotationTitle: "Title", @@ -2362,27 +2417,6 @@ module.exports = { tribeForumCategory: "Category", tribeForumReply: "Reply", tribeForumReplies: "Replies", - tribeMarketCreate: "Create Listing", - tribeMarketEmpty: "No listings, yet.", - tribeMarketTitle: "Title", - tribeMarketDescription: "Description", - tribeMarketPrice: "Price", - tribeMarketImage: "Image", - tribeMarketCategory: "Category", - tribeJobCreate: "Create Job", - tribeJobsEmpty: "No jobs, yet.", - tribeJobTitle: "Title", - tribeJobDescription: "Description", - tribeJobLocation: "Location", - tribeJobSalary: "Salary", - tribeJobDeadline: "Deadline", - tribeProjectCreate: "Create Project", - tribeProjectsEmpty: "No projects, yet.", - tribeProjectTitle: "Title", - tribeProjectDescription: "Description", - tribeProjectGoal: "Goal", - tribeProjectFunded: "Funded", - tribeProjectDeadline: "Deadline", tribeMediaUpload: "Upload Media", readDocument: "Read Document", tribeCreateImage: "Create Image", @@ -2393,21 +2427,20 @@ module.exports = { tribeMediaEmpty: "No media, yet.", tribeMediaTitle: "Title", tribeMediaDescription: "Description", - tribeMediaType: "Type", tribeMediaTypeImage: "Image", tribeMediaTypeVideo: "Video", tribeMediaTypeAudio: "Audio", tribeMediaTypeDocument: "Document", tribeMediaTypeBookmark: "Bookmark", tribeContentDelete: "DELETE", + tribeReachTribe: "TRIBE", + tribeReachNotice: "OASIS shows it to every inhabitant and CLEARNET also on the open web. Opening it leaves a readable copy outside the tribe: moving it back to TRIBE hides it, but copies already shared cannot be erased.", + tribeReachPrivate: "Content of a private tribe can only stay inside the tribe.", + tribeReachOpenedBy: "Opened by", + tribeReachChange: "Where it is published", tribeInviteCodeText: "Invite code: ", oasisVersionLabel: "Oasis Version", - tribeInviteCodeHint: "Share this code with someone you want to invite. They can join via /invites → Tribes.", - tribeGroupTribe: "Tribe", - tribeGroupOffice: "Office", - tribeGroupNetwork: "Network", - tribeGroupEconomy: "Economy", - tribeGroupMedia: "Media", + tribeInviteCodeHint: "Share this code with someone you want to invite. They can join via /invites.", tribeStatusOpen: "OPEN", tribeStatusClosed: "CLOSED", tribeStatusInProgress: "IN PROGRESS", @@ -2417,33 +2450,17 @@ module.exports = { tribePriorityCritical: "CRITICAL", tribeTaskFilterAll: "ALL", tribeMediaFilterAll: "ALL", - tribeReportCatBug: "BUG", - tribeReportCatAbuse: "ABUSE", - tribeReportCatContent: "CONTENT", - tribeReportCatOther: "OTHER", tribeForumCatGeneral: "GENERAL", tribeForumCatProposal: "PROPOSAL", tribeForumCatQuestion: "QUESTION", tribeForumCatAnnouncement: "ANNOUNCEMENT", - tribeMarketCatGoods: "GOODS", - tribeMarketCatServices: "SERVICES", - tribeMarketCatFood: "FOOD", - tribeMarketCatOther: "OTHER", tribeStatusLabel: "Status", tribeSubTribes: "SUB-TRIBES", tribeSubTribesCreate: "Create Sub-Tribe", tribeSubTribesStrictDenied: "Tribe strict mode does not allow you to create new sub-tribes. Please contact the administrator.", - tribeSubTribesEmpty: "No sub-tribes created, yet.", - tribeActivityJoined: "JOINED", - tribeActivityLeft: "LEFT", - tribeActivityFeed: "FEED", tribeActivityRefeed: "REFEED", - tribeGroupAnalytics: "Analytics", - tribeGroupCreative: "Creative", tribeSectionActivity: "ACTIVITY", tribeSectionTrending: "TRENDING", - tribeSectionOpinions: "OPINIONS", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAGS", tribeSectionSearch: "SEARCH", tribeActivityEmpty: "No activity yet.", @@ -2455,25 +2472,13 @@ module.exports = { tribeTrendingPeriodWeek: "This Week", tribeTrendingPeriodAll: "All Time", tribeTrendingEngagement: "engagement", - tribeOpinionsEmpty: "No opinions yet.", - tribeOpinionsCast: "Vote", - tribeOpinionsRankings: "Rankings", - tribeOpinionsAlreadyVoted: "Already voted", tribeTopCategory: "More Voted", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "Collaborative pixel art canvas.", - tribePixeliaPaint: "Paint", - tribePixeliaContributors: "contributors", - tribePixeliaTotalPixels: "pixels painted", tribeTagsEmpty: "No tags found.", - tribeTagsCloud: "Tag Cloud", - tribeTagsContentWith: "Content with tag", - tribeSearchPlaceholder: "Search tribe content...", tribeSearchEmpty: "No results found.", - tribeSearchResults: "Results", tribeSearchMinChars: "Enter at least 2 characters to search.", agendaTitle: "Agenda", agendaDescription: "Here you can find all your assigned items.", + agendaSearchPlaceholder: "Search in Agenda...", agendaFilterAll: "ALL", agendaFilterToday: "TODAY", agendaFilterUpcoming: "UPCOMING", @@ -2487,23 +2492,15 @@ module.exports = { agendaFilterReports: "REPORTS", agendaFilterTransfers: "TRANSFERS", agendaFilterJobs: "JOBS", + agendaFilterIndustry: "INDUSTRY", + agendaFilterHousing: "HOUSING", agendaFilterProjects: "PROJECTS", agendaFilterCalendars: "CALENDARS", agendaNoItems: "No assignments found.", - agendaAuthor: "By", agendaDiscardButton: "Discard", agendaRestoreButton: "Restore", - agendaCreatedAt: "Created At", - agendaTitleLabel: "Title", + agendaRemoveButton: "Remove", agendaMembersCount: "Members", - agendaDescriptionLabel: "Description", - agendaStatus: "Status", - agendaVisibility: "Visibility", - agendaEventDate: "Event Date", - agendaEventLocation: "Location", - agendaEventPrice: "Price", - agendaEventUrl: "URL", - agendaTaskStart: "Start Time", agendaLocationLabel: "Location", agendaYes: "YES", agendaNo: "NO", @@ -2513,11 +2510,6 @@ module.exports = { agendareportCategory: "Category", agendareportSeverity: "Severity", agendareportStatus: "Status", - agendareportDescription: "Description", - agendaTaskEnd: "End Time", - agendaTaskPriority: "Priority", - agendaTransferFrom: "From", - agendaTransferTo: "To", agendaTransferConcept: "Concept", agendaTransferAmount: "Amount", agendaTransferDeadline: "Deadline", @@ -2527,47 +2519,7 @@ module.exports = { voteNow: "Vote now", alreadyVoted: "You have already opined.", noOpinionsFound: "No opinions found.", - RECENTButton: "RECENT", TOPButton: "TOP", - interestingButton: "INTERESTING", - necessaryButton: "NECESSARY", - funnyButton: "FUNNY", - disgustingButton: "DISGUSTING", - sensibleButton: "SENSIBLE", - propagandaButton: "PROPAGANDA", - adultOnlyButton: "ADULT ONLY", - boringButton: "BORING", - confusingButton: "CONFUSING", - usefulButton: "USEFUL", - informativeButton: "INFORMATIVE", - wellResearchedButton: "WELL RESEARCHED", - accurateButton: "ACCURATE", - needsSourcesButton: "NEEDS SOURCES", - wrongButton: "WRONG", - lowQualityButton: "LOW QUALITY", - creativeButton: "CREATIVE", - insightfulButton: "INSIGHTFUL", - actionableButton: "ACTIONABLE", - inspiringButton: "INSPIRING", - loveButton: "LOVE", - clearButton: "CLEAR", - upliftingButton: "UPLIFTING", - unnecessaryButton: "UNNECESSARY", - rejectedButton: "REJECTED", - misleadingButton: "MISLEADING", - offTopicButton: "OFF TOPIC", - duplicateButton: "DUPLICATE", - clickbaitButton: "CLICKBAIT", - spamButton: "SPAM", - trollButton: "TROLL", - nsfwButton: "NSFW", - violentButton: "VIOLENT", - toxicButton: "TOXIC", - harassmentButton: "HARASSMENT", - hateButton: "HATE", - scamButton: "SCAM", - triggeringButton: "TRIGGERING", - opinionsCreatedAt: "Created At", opinionsTotalCount: "Total Opinions", voteInteresting: "Interesting", voteNecessary: "Necessary", @@ -2604,29 +2556,41 @@ module.exports = { voteCreative: "Creative", voteSpam: "Spam", voteAdultOnly: "Adult Only", - publishBlog: "Publish Blog", privateMessage: "PM", pmSendTitle: "Private Messages", pmSend: "Send!", - pmDescription: "Use this form to send an encrypted message to other inhabitants.", + pmCancel: "Cancel", + pmReset: "Reset", + pmSharedKeyHint: "Share this key with the recipient out of band. It is required to decrypt.", + pmDescription: "Use this form to send an encrypted message/file to other inhabitants.", + pmComposeTitle: "Send a message", pmRecipients: "Recipients", pmRecipientsHint: "Enter Oasis IDs separated by commas", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "Up to 7 inhabitants per message.", + pmTextPlaceholder: "Write your message.", pmSubject: "Subject", pmSubjectHint: "Enter the message subject", pmText: "Message", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Decrypt", + fileShareTitle: "Share a file", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "File", + fileShareDownload: "Download", + fileShareMutualError: "You can only share files with inhabitants with mutual support.", + fileShareNoFile: "No file selected.", + fileShareTooLarge: "The file exceeds the allowed size.", + fileShareFailed: "Could not prepare the file.", + fileShareSendError: "Could not send the file.", + fileShareUnavailable: "This file is not available right now. Try again later.", pmCrypterBadKey: "Your shared key is incorrect!", pmCrypterTooLong: "The message is too long to encrypt with the Crypter. Please shorten it and try again.", pmCrypterCipherLabel: "Re-encrypted message", pmCrypterCharsLabel: "characters", pmInvalidRecipients: "Invalid Oasis ID (should be like @…=.ed25519).", pmEncryptionLabel: "Encryption", - pmFile: "Attachment", - private: "Private", - privateDescription: "Your encrypted messages.", + private: "Inbox", + privateDescription: "Manage your private messages.", privateInbox: "INBOX", privateSent: "SENT", privateDelete: "Delete", @@ -2634,19 +2598,27 @@ module.exports = { noPrivateMessages: "No private messages.", pmFromLabel: "From:", pmToLabel: "To:", - pmInvalidMessage: "Invalid message", pmNoSubject: "(no subject)", pmSubjectLabel: "Subject:", - pmBodyLabel: "Body", - pmBotJobs: "42-JobsBOT", - pmBotProjects: "42-ProjectsBOT", - pmBotMarket: "42-MarketBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "The ruling house of the L.A.R.P has changed.", + politicalBotParliamentTitle: "A new government cycle has begun in the Parliament.", + politicalBotTribeTitle: "A new government cycle has begun in one of your Tribes.", + politicalBotLarpIntro: "Now {house} rules during this cycle: {cycle}", + politicalBotParliamentIntro: "The current government is {gov}", + politicalBotParliamentIntroLeader: "The current government is {gov}, led by {leader}", + politicalBotTribeIntro: "The current government in the Tribe {tribe} is {gov}", + politicalBotTribeIntroLeader: "The current government in the Tribe {tribe} is {gov}, led by {leader}", + politicalBotLeaderLabel: "Leader", inboxJobSubscribedTitle: "New subscription to your job offer", pmInhabitantWithId: "Inhabitant with OASIS ID:", pmHasSubscribedToYourJobOffer: "has subscribed to your job offer", - inboxProjectCreatedTitle: "New project created", - pmHasCreatedAProject: "has created a project", inboxMarketItemSoldTitle: "Item Sold", + inboxShopSoldTitle: "Product Sold", pmYourItem: "Your item", pmHasBeenSoldTo: "has been sold to", pmFor: "for", @@ -2654,7 +2626,6 @@ module.exports = { pmHasPledged: "has pledged", pmToYourProject: "to your project", blockchain: 'BlockExplorer', - blockchainTitle: 'BlockExplorer', blockchainDescription: 'Explore and visualize the blocks in the blockchain.', blockchainNoBlocks: 'No blocks found in the blockchain.', blockchainStatsTitle: 'Stats', @@ -2666,42 +2637,36 @@ module.exports = { blockchainBlockCarbon: 'Carbon footprint', blockchainBlockType: 'Type', blockchainBlockTimestamp: 'Timestamp', - blockchainBlockContent: 'Block', - blockchainBlockURL: 'URL:', - blockchainContent: 'Block', - blockchainContentPreview: 'Preview of the block content', blockchainLatestDatagram: 'Latest Datagram', - blockchainDatagram: 'Datagram', - blockchainDetails: 'View block details', - blockchainViewBlockexplorer: "View blockexplorer", - blockchainBlockInfo: 'Block Information', - blockchainBlockDetails: 'Details of the selected block', + blockchainDatagram: "Datagram", + blockchainDetails: "View Block Details", + blockchainViewBlockexplorer: "View in Blockexplorer", blockchainBack: 'Back to Blockexplorer', blockchainContentDeleted: "This content has been tombstoned", visitContent: "Visit Content", + favoriteAdd: "Pin to Favorites", + pmContentTooltip: "Send a Private Message", + phoneCallTooltip: "Make a Phone Call", + favoriteRemove: "Unpin from Favorites", + reportContent: "Report Content", banking: 'Banking', bankingTitle: 'Banking', - bankingDescription: 'Explore the current value of ECOin and the corresponding UBI allocation, distributed per epoch based on participation and trust.', + bankingDescription: "The ECOin collective economy.", bankOverview: 'Overview', bankEpochs: 'Epochs', bankRules: 'Rules', pending: 'Pending', closed: 'Closed', - bankBack: 'Back to Banking', - bankViewTx: 'View Tx', bankClaimNow: 'Claim now', - bankClaimUBI: 'Claim UBI!', + bankClaimUBI: 'Claim UBI', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'No pending UBI allocations for this epoch.', bankEpoch: 'Epoch', bankPool: 'Pool (this epoch)', bankWeightsSum: 'Sum of weights', - bankAllocations: 'Allocations', bankNoAllocations: 'No allocations found.', bankNoEpochs: 'No epochs found.', bankEpochAllocations: 'Epoch allocations', @@ -2715,16 +2680,21 @@ module.exports = { bankEpochId: 'Epoch ID', bankRuleHash: 'Rules Snapshot Hash', bankViewEpoch: 'View Epoch', + bankYourIndustryBalance: "Your Industry Share", + bankYourUbiMonth: "Your UBI (this month)", + bankYourFundsMonth: "Total Funds", + bankIndustryBalance: "Industry Production", bankUserBalance: 'Your Balance', - ecoWalletNotConfigured: 'ECOin Wallet not configured', - editWallet: 'Edit wallet', - addWallet: 'Add wallet', bankAddresses: 'Addresses', bankNoAddresses: 'No addresses found.', bankUser: 'Oasis ID', bankAddress: 'Address', bankAddAddressTitle: 'Add ECOIN address', bankAddAddressUser: 'Oasis ID', + bankAddAddressLabel: "Label (optional)", + bankAddAddressLabelPlaceholder: "Bob's wallet, Alice's shop…", + bankAddAddressUserOptional: "Oasis ID (optional)", + bankAddressUnnamed: "Unnamed address", bankAddAddressAddress: 'ECOIN Address', bankAddAddressSave: 'Save', bankAddressAdded: 'Address added', @@ -2742,19 +2712,11 @@ module.exports = { bankAddressDeleteConfirm: 'Delete this address?', search: 'Search!', bankLocal: 'Local', - bankFromOasis: 'Oasis', - bankMyAddress: 'Your address', - bankRemoveMyAddress: 'Remove my address', - bankNotRemovableOasis: 'Addresses cannot be removed locally', - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "Network", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "NO FUNDS!", bankUbiAvailableOk: "AVAILABLE!", - bankUbiAvailability: "UBI availability", + bankUbiAvailability: "UBI", bankAlreadyClaimedThisMonth: "Already claimed this month", bankUbiThisMonth: "UBI (this month)", bankUbiLastClaimed: "UBI (last claimed)", @@ -2768,13 +2730,6 @@ module.exports = { shopsTitle: "Shops", shopDescription: "Discover and manage shops in the network.", shopTitle: "Shop", - shopFilterAll: "ALL", - shopFilterMine: "MINE", - shopFilterRecent: "RECENT", - shopFilterTop: "TOP", - shopFilterProducts: "PRODUCTS", - shopFilterPrices: "PRICES", - shopFilterFavorites: "FAVORITES", shopUpload: "Create Shop", shopAllSectionTitle: "Shops", shopMineSectionTitle: "Your Shops", @@ -2791,19 +2746,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Publish to Clearnet", - shopClearnetUnpublish: "Unpublish from Clearnet", - shopClearnetUrlLabel: "Clearnet URL", - shopClearnetWarning: "Publishing on Clearnet makes this shop indexable by external search engines and crawlers.", + transportChipLabel: "Encrypted in transit", shopAddFavorite: "Add Favorite", shopRemoveFavorite: "Remove Favorite", shopOpen: "OPEN", shopClosed: "CLOSED", - shopOpenShop: "Open Shop", - shopCloseShop: "Close Shop", shopMakePrivate: "MAKE PRIVATE (E2E)", shopMakePublic: "MAKE PUBLIC", shopProducts: "Products", + shopFilterAll: "ALL", + shopFilterRecent: "RECENT", + shopFilterMine: "MINE", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUCTS", + shopFilterPrices: "PRICES", + shopFilterFavorites: "FAVORITES", shopProductAdd: "Add Product", shopProductTitle: "Product", shopProductUpdate: "Update Product", @@ -2828,24 +2785,20 @@ module.exports = { shopOrderPrice: "Price", shopOrderBuyer: "Buyer", shopBackToShop: "Back to Shop", - shopShareUrl: "Share URL", - shopVisitShop: "VISIT SHOP", marketVisitItem: "VISIT ITEM", shopStatus: "STATUS", shopCreatedAt: "CREATED", - shopSearchPlaceholder: "Search shops...", + shopSearchPlaceholder: "Search in Shops...", shopUrl: "URL", shopLocation: "Location", shopTags: "Tags", shopVisibility: "Visibility", - shopImage: "Image", shopShortDescription: "Short Description", shopShortDescriptionPlaceholder: "Brief description of your shop", shopTitlePlaceholder: "Name of your shop", shopDescriptionPlaceholder: "Detailed description of your shop", - shopUrlPlaceholder: "https://your-shop-url.com", shopLocationPlaceholder: "City, Country", - shopTagsPlaceholder: "tag1, tag2, tag3", + shopTagsPlaceholder: "Enter tags separated by commas", mapTitlePlaceholder: "Map title", shopProductFeatured: "Featured Product", shopSendToMarket: "Send to Market", @@ -2853,60 +2806,42 @@ module.exports = { typeShopProduct: "SHOP PRODUCT", bankExchange: 'Exchange', bankExchangeCurrentValue: 'ECOin Value (1h)', - bankTotalSupply: 'ECOin Total Supply', - bankEcoinHours: "ECOin Equivalence in Time", + bankTotalSupply: "ECOin Total Supply", + bankPubsSupply: "ECOin PUBs Pool Supply", + bankHoldingSupply: "ECOin Holding Supply", + bankEcoinHours: "ECOin Equivalence in Time (1 ECO)", bankHoursOfWork: 'hours', bankUnitMs: "ms", bankUnitSeconds: "seconds", bankUnitMinutes: "minutes", bankUnitDays: "days", bankExchangeNoData: 'No data available', - bankExchangeIndex: 'ECOin Value (1h)', - bankInflation: 'ECOin Inflation (anual)', - bankInflationMonthly: 'ECOin Inflation (mensual)', + bankInflation: "ECOin Inflation (annual)", + bankInflationMonthly: "ECOin Inflation (monthly)", bankExchangeChartTitle: 'ECOin value progression over time', bankExchangeChartValue: 'Value (ECO/h)', bankExchangeChartSupply: 'Supply', bankExchangeChartInflation: 'Inflation %', bankExchangeChartEmpty: 'Not enough samples yet — revisit later', - bankCurrentSupply: 'ECOin Current Supply', + bankCurrentSupply: "ECOin Current Supply", bankingSyncStatus: 'ECOin Status', bankingSyncStatusSynced: 'Synced', - bankingSyncStatusOutdated: 'Outdated', + bankingSyncStatusOutdated: "Unsync", statsTitle: 'Statistics', statistics: "Statistics", - statsInhabitant: "Inhabitant Stats", statsDescription: "Discover statistics about your network.", ALLButton: "ALL", + RECENTButton: "RECENT", MINEButton: "MINE", TOMBSTONEButton: "TOMBSTONES", - statsYou: "You", - statsUserId: "Oasis ID", statsCreatedAt: "Created At", statsYourContent: "Content", statsYourOpinions: "Opinions", - statsYourTombstone: "Tombstones", - statsNetwork: "Network", - statsTotalInhabitants: "Inhabitants", - statsDiscoveredTribes: "Tribes (Public)", - statsPrivateDiscoveredTribes: "Tribes (Private)", statsNetworkContent: "Content", - statsYourMarket: "Market", - statsYourJob: "Jobs", - statsYourProject: "Projects", - statsYourTransfer: "Transfers", - statsYourForum: "Forums", statsNetworkOpinions: "Opinions", - statsDiscoveredMarket: "Market", - statsDiscoveredJob: "Jobs", - statsDiscoveredProject: "Projects", - statsBankingTitle: "Banking", - statsEcoWalletLabel: "ECOIN Wallet", + statsEcoWalletLabel: "ECOin Wallet", + walletAddressCopyHint: "Click to select, then copy", statsEcoWalletNotConfigured: "Not configured!", - statsTotalEcoAddresses: "Total addresses", - statsDiscoveredTransfer: "Transfers", - statsDiscoveredForum: "Forums", - statsNetworkTombstone: "Tombstones", statsBookmark: "Bookmarks", statsEvent: "Events", statsTask: "Tasks", @@ -2927,17 +2862,15 @@ module.exports = { statsTransfer: "Transfers", statsAiExchange: "AI", statsPUBs: 'PUBs', + statsSyncedPeers: "Peers", + statsTotalLabel: "Total", statsPost: "Posts", - statsOasisID: "Oasis ID", statsSize: "Total (size)", statsBlockchainSize: "Blockchain (size)", statsBlobsSize: "Blobs (size)", statsActivity7d: "Activity (last 7 days)", statsActivity7dTotal: "7-day total", statsActivity30dTotal: "30-day total", - statsKarmaScore: "KARMA Score", - statsPublic: "Public", - statsPrivate: "Private", day: "Day", messages: "Messages", statsProject: "Projects", @@ -2949,30 +2882,14 @@ module.exports = { statsProjectsCancelled: "Cancelled", statsProjectsGoalTotal: "Total goal", statsProjectsPledgedTotal: "Total pledged", - statsProjectsSuccessRate: "Success rate", - statsProjectsAvgProgress: "Average progress", - statsProjectsMedianProgress: "Median progress", - statsProjectsActiveFundingAvg: "Avg. active funding", - statsJobsTitle: "Jobs", - statsJobsTotal: "Total jobs", - statsJobsOpen: "Open", - statsJobsClosed: "Closed", - statsJobsOpenVacants: "Open vacants", - statsJobsSubscribersTotal: "Total subscribers", - statsJobsAvgSalary: "Average salary", - statsJobsMedianSalary: "Median salary", statsMarketTitle: "Market", statsMarketTotal: "Total items", statsMarketForSale: "For sale", statsMarketReserved: "Reserved", statsMarketClosed: "Closed", statsMarketSold: "Sold", - statsMarketRevenue: "Revenue", - statsMarketAvgSoldPrice: "Avg. sold price", statsUsersTitle: "Inhabitants", user: "Inhabitant", - statsTombstoneTitle: "Tombstones", - statsNetworkTombstones: "Network tombstones", statsTombstoneRatio: "Tombstone ratio (%)", statsParliamentCandidature: "Parliament (candidatures)", statsParliamentTerm: "Parliament terms", @@ -2998,31 +2915,28 @@ module.exports = { aiPrompt: "Provide an informative and precise response.", aiConfiguration: "Set prompt", aiPromptUsed: "Prompt", - aiClearHistory: "Clear chat history", - aiSharePrompt: "Add this answer to collective training?", - aiShareYes: "Yes", - aiShareNo: "No", - aiSharedLabel: "Added to training", - aiRejectedLabel: "Not added to training", - aiServerError: "The AI could not answer. Please try again.", + aiClearHistory: "Clear history", + aiServerError: "I cannot answer properly. Please try again.", + aiStatusLoading: "42 is loading the model. Ask again in a moment.", + aiThinking: "Thinking", + aiWaiting: "Waiting", + aiStatusMissingModel: "The AI model is not installed: src/AI/oasis-42-1-chat.Q4_K_M.gguf is missing.", + aiExportFineTuning: "Export Fine-Tuning", + aiFactsTitle: "Data from your network", aiInputPlaceholder: "What is Oasis?", typeAiExchange: "AI", aiApproveTrain: "Add to collective training", aiRejectTrain: "Do not train", - aiTrainPending: "Pending approval", aiTrainApproved: "Approved for training", aiTrainRejected: "Rejected for training", aiSnippetsUsed: "Snippets used", aiSnippetsLearned: "Snippets learned", statsAITraining: "AI training", - aiApproveCustomTrain: "Train using this custom answer", aiCustomAnswerPlaceholder: "Write your custom answer…", - statsAIExchanges: "Model Exchanges", marketMineSectionTitle: "Your Items", marketCreateSectionTitle: "Create Item", marketUpdateSectionTitle: "Update", marketAllSectionTitle: "Market", - marketRecentSectionTitle: "Recent Market", marketTitle: "Market", marketDescription: "A marketplace for exchanging goods or services in your network.", marketFilterAll: "ALL", @@ -3037,7 +2951,7 @@ module.exports = { marketFilterDiscarded: "DISCARDED", marketFilterRecent: "RECENT", marketFilterMyBids: "BIDS", - marketCreateButton: "Create Item", + marketCreateButton: "Sell Something", marketItemType: "Type", marketItemTitle: "Title", marketItemAvailable: "Deadline", @@ -3052,14 +2966,11 @@ module.exports = { marketItemDeadline: "Deadline", marketItemIncludesShipping: "Includes Shipping?", marketItemHighestBid: "Highest Bid", - marketItemHighestBidder: "Highest Bidder", marketItemStock: "Stock", marketOutOfStock: "Out of stock", - marketItemBidTime: "Bid Time", marketActionsUpdate: "Update", marketUpdateButton: "Update", marketActionsDelete: "Delete", - marketActionsSold: "Mark as Sold", marketActionsChangeStatus: "CHANGE STATUS", marketActionsBuy: "BUY!", marketAuctionBids: "Current Bids", @@ -3067,20 +2978,82 @@ module.exports = { marketItemSeller: "Seller", marketNoItems: "No items available, yet.", marketYourBid: "Your Bid", - marketCreateFormImageLabel: "Upload media (max-size: 50MB)", - marketSearchLabel: "Search", - marketSearchPlaceholder: "Search title or tags", + marketCreateFormImageLabel: "Upload media (max-size: 75MB)", + marketSearchPlaceholder: "Search in Market...", marketMinPriceLabel: "Min price", marketMaxPriceLabel: "Max price", - marketSortLabel: "Sort by", marketSortRecent: "Most recent", marketSortPrice: "Price", marketSortDeadline: "Deadline", marketSearchButton: "Search", marketAuctionEndsIn: "Ends", marketAuctionEnded: "Ended", - marketMyBidBadge: "You bid", marketNoItemsMatch: "No items match your search.", + housingTitle: "Housing", + housingDescriptionText: "Discover and manage places in your network.", + modulesHousingLabel: "Housing", + modulesHousingDescription: "Module to discover and manage places.", + typeHousing: "HOUSING", + housingFilterAll: "ALL", + housingFilterMine: "MINE", + housingFilterRequested: "REQUESTED", + housingFilterSale: "SALE", + housingFilterRent: "RENT", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "AVAILABLE", + housingFilterClosed: "UNAVAILABLE", + housingFilterRecent: "RECENT", + housingFilterTop: "TOP", + housingType: "Offer", + housingTypeSALE: "For sale", + housingTypeRENT: "For rent", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Property", + housingPropertyAPARTMENT: "Apartment", + housingPropertyHOUSE: "House", + housingPropertyROOM: "Room", + housingPropertyLAND: "Land", + housingPropertyOTHER: "Other", + housingTitleLabel: "Title", + housingTitlePlaceholder: "Name of the place", + housingDescription: "Description", + housingDescriptionPlaceholder: "Describe the place, the neighbourhood, what is included: bathrooms, garden, beds…", + housingRules: "Rules", + housingRulesPlaceholder: "Pets, smoking, quiet hours, minimum stay…", + housingPlace: "Place", + housingPlacePlaceholder: "Country, city, address", + housingPrice: "Price", + housingPriceLabel: "Price (ECO)", + housingSizeLabel: "Size (m²)", + housingFree: "FREE", + housingPerMonth: "/month", + housingRooms: "Rooms", + housingSize: "Size", + housingCapacity: "Guests", + housingAvailableFrom: "Available from", + housingAvailableTo: "Available until", + housingTags: "Tags", + spreadEditWarning: "This content will lose spreads after editing it", + housingRequests: "Requests", + housingRequestButton: "Request", + housingCancelButton: "Cancel request", + housingRequestedBadge: "REQUESTED", + housingStatus: "Status", + housingStatusOPEN: "AVAILABLE", + housingStatusCLOSED: "UNAVAILABLE", + housingSetOpen: "Make available", + housingSetClosed: "Make unavailable", + housingCreateButton: "Create Place", + housingUpdateButton: "Update", + housingSearchPlaceholder: "Search in Housing...", + housingSearchButton: "Search", + housingMinPrice: "Min price", + housingMaxPrice: "Max price", + housingSortRecent: "Most recent", + housingSortPrice: "Lowest price", + housingSortRequests: "Most requested", + housingSortRating: "Best rated", + housingNoItems: "No places found.", jobsTitle: "Jobs", jobsCandidatesTitle: "Suggested candidates", jobsCandidatesDescription: "Inhabitants whose skills match this job. Send a private message to invite them.", @@ -3128,8 +3101,7 @@ module.exports = { jobLocationPresencial: "On-place", jobLocationRemote: "Remote", jobVacantsPlaceholder: "Number of positions", - jobSalaryPlaceholder: "Salary in ECO for 1 dedicated hour", - jobImage: "Upload media (max-size: 50MB)", + jobImage: "Upload media (max-size: 75MB)", jobTasks: "Tasks", jobType: "Job Type", jobTime: "Job Time", @@ -3138,7 +3110,6 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Top Salary Jobs", createJobButton: "Publish Job", - viewDetailsButton: "View Details", noJobsFound: "No job offers found.", jobAuthor: "By", jobTypeFreelance: "Freelancer", @@ -3150,42 +3121,208 @@ module.exports = { jobsHoursOffered: "Hours offered", jobsHoursRequested: "Hours requested in return", jobsExchangeSkill: "Skill wanted in exchange", - jobsHoursOfferedPlaceholder: "e.g. 4", - jobsHoursRequestedPlaceholder: "e.g. 4", - jobsExchangeSkillPlaceholder: "e.g. carpentry, design", - jobExchangeHint: "For hour-exchange jobs, fill the fields below instead of salary.", jobTimePartial: "Part-time", jobTimeComplete: "Full-time", - jobsDeleteButton: "DELETE", jobsUpdateButton: "UPDATE", jobsFilterApplied: "APPLIED", jobsAppliedTitle: "My applications", jobsAppliedBadge: "Applied", - jobsFilterFavs: "Favorites", - jobsFavsTitle: "Favorites", - jobsFilterNeeds: "Needs help", - jobsNeedsTitle: "Needs help", - jobsSearchLabel: "Search", - jobsSearchPlaceholder: "Search title, tags, description...", + jobsSearchPlaceholder: "Search in Jobs...", jobsMinSalaryLabel: "Min salary", jobsMaxSalaryLabel: "Max salary", - jobsSortLabel: "Sort by", jobsSortRecent: "Most recent", jobsSortSalary: "Highest salary", jobsSortSubscribers: "Most applicants", jobsSearchButton: "Search", - jobsFavoriteButton: "Favorite", - jobsUnfavoriteButton: "Unfavorite", - jobsMessageAuthorButton: "PM", jobsApplicants: "Applicants", jobsUpdatedAt: "Updated", jobSetOpen: "Set open", - jobNewBadge: "NEW", jobsTagsLabel: "Tags", - jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "No jobs match your search.", + industrySearchPlaceholder: "Search in Industry...", + industryAllSectors: "All sectors", + industryVisitButton: "Visit Industry", + industryAdvanceTo: "Set to", + industryAmount: "Amount", + industryApproveBuild: "Approve", + industryBlueprint: "Blueprint", + industryBlueprints: "Blueprints", + industryBuild: "Build", + industryBuildNotes: "Notes", + industryBuildStart: "Start date", + industryVoteUpdateButton: "Vote update", + industryVoteDeleteButton: "Vote delete", + industryRevokeVote: "Revoke", + industryTimeLeft: "Time left", + industryBuildEnd: "End date", + industryBuilds: "Builds", + industryBuildTitle: "Title", + industryContribute: "Contribute", + industryContributeButton: "Contribute", + industryContributions: "Contributions", + industryCreateBlueprintButton: "Create Blueprint", + industryDistribute: "Distribute", + industryDistributeButton: "Distribute by Shares", + industryDistribution: "Distribution", + industryEcoAmount: "Amount (ECO)", + industryEditBlueprint: "Edit Blueprint", + industryFacility: "Facility", + industryKindLabel: "Kind", + industryKind_labor: "Labor", + industryKind_material: "Material", + industryKind_eco: "Funds", + industryLaborHours: "Labor (hours)", + industryHours: "Hours", + industryMaterialItem: "Material", + industryMaterials: "Materials", + industryEstMaterials: "Total Materials", + industryEstLabor: "Total Labor", + industryEstTotal: "Estimated price", + industryBuildingPrice: "Building price", + industryFinalPrice: "Final price", + industryBlueprintDescPlaceholder: "Specs, dimensions, weight, docs…", + industryMaterialValue: "Value (ECO)", + industryMember: "Member", + industryNewBlueprint: "New Blueprint", + industryNewBuild: "Propose a Build", + industryEditBuild: "Edit build", + industryNeedBlueprint: "Create a blueprint first: every build produces one.", + industryNoBlueprints: "No blueprints yet.", + industryNoBuilds: "No builds yet.", + industryNote: "Note", + industryOutput: "Output", + industryOutputKind: "Product type", + industryOutputValue: "Output value (ECO, e.g. from sale)", + industryPostJob: "Create Job", + industryPot: "Pot", + industryProposeBuildButton: "Propose Build", + industryProposer: "Proposer", + industryAuthor: "Author", + industrySellOnMarket: "Send to Market", + industrySendToProjects: "Create Project", + industrySkills: "Skills", + industryTreasury: "Treasury", + industryKind_physical: "Physical", + industryKind_digital: "Digital", + industryBuildStatus_PROPOSED: "PROPOSED", + industryBuildStatus_REJECTED: "REJECTED", + industryBuildStatus_APPROVED: "APPROVED", + industryBuildStatus_STOCKING: "STOCKING", + industryBuildStatus_IN_PRODUCTION: "IN PRODUCTION", + industryBuildStatus_COMPLETED: "COMPLETED", + industryBuildStatus_FAILED: "FAILED", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "A job matches your curriculum", + jobsBotMatchIntro: "A job matches your curriculum.", + jobsBotMatchJob: "Job", + jobsBotMatchHint: "You receive this because your curriculum is AI managed. You can turn it off in your CV.", + cvAiManaged: "AI Managed", + switchOn: "ON", + switchOff: "OFF", + cvAiManagedHint: "When on, JobsBot warns you by private message when a job matches your curriculum.", + cvMatchThreshold: "Notify me from this match level", + housingBotRequestedTitle: "Somebody has requested one of your places.", + housingBotCancelledTitle: "A request on one of your places was cancelled.", + housingBotYouRequestedTitle: "You have requested a place.", + housingBotYouCancelledTitle: "You have cancelled a request.", + housingBotUnavailableTitle: "A place you requested is no longer available.", + housingBotUnavailableSentTitle: "You have told the inhabitants who requested this place.", + industryBotAdmittedTitle: "You have been admitted to a facility.", + industryBotApplicationTitle: "New membership application in your facility.", + industryBotInvitedTitle: "You have been invited to a facility.", + industryBotDissolvedTitle: "A facility you belong to has been dissolved.", + industryBotBuildApprovedTitle: "A build has been approved.", + industryBotDistributedTitle: "A build output has been distributed.", + industryFilterRules: "RULES", + industryRulesTitle: "Rules", + industryRulesIntro: "Industry lets inhabitants collectively own the means of production: a Facility is a network-owned workshop that nobody owns privately.", + industryRulesSteward: "The founder is the steward — a caretaker, not an owner: they cannot sell, privatize or dissolve the Facility on their own.", + industryRulesMembership: "Membership policy sets how inhabitants join: Open (join instantly), By vote (admitted by the members' vote) or Invite only (a member must invite them first).", + industryRulesQuorum: "Quorum is the minimum number of members that must vote for a decision to count, so a small Facility isn't decided by a single inhabitant.", + industryRulesMajority: "Majority is the fraction of votes needed to pass (0.5 = half, 1 = unanimity); a decision passes when YES votes reach at least max(quorum, members × majority).", + industryRulesDecisions: "Members vote to admit newcomers, approve builds and dissolve the Facility; the steward cannot override these collective decisions.", + industryRulesBlueprints: "Blueprints are libre (COPYLEFT) recipes — the inputs (materials, labor hours, skills) and the output (a physical or digital good) — and anyone can fork them.", + industryRulesBuilds: "A Build is a production order: PROPOSED → APPROVED (by vote) → STOCKING → IN PRODUCTION → COMPLETED, and it only accepts contributions once approved.", + industryRulesContributions: "Members contribute labor (hours), materials (declared ECO value) or ECO to a build; each contribution earns karma.", + industryRulesLaborRate: "Labor rate is the ECO value of one work hour in this Facility; it turns hours into karma so effort and capital compare fairly: karma = hours × labor rate + material value + ECO.", + industryRulesShares: "Your share of a build is your karma ÷ total karma, and it sets how much of the output value you receive.", + industryRulesDistribution: "When a build is completed the steward distributes the pot (the build's funds plus any sale value) among contributors, proportional to their shares.", + industryRulesTreasury: "ECO contributions are held by the steward as the build's treasury custodian until distribution; all movements are recorded on the network and disputes can go to Courts.", + industryJobs: "Jobs", + industryReportButton: "Report", + industryCreateTask: "Create Task", + industryOpenDispute: "Open dispute", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "Generate invite", + industryPauseVotes: "Status votes", + industryTitle: "Industry", + industryDescription: "Module to manage the means of production collectively.", + typeIndustryBuild: "BUILD", + typeIndustryBlueprint: "BLUEPRINT", + typeIndustryAllocation: "DISTRIBUTION", + typeIndustry: "INDUSTRY", + modulesIndustryLabel: "Industry", + modulesIndustryDescription: "Module to manage the means of production collectively.", + industryFilterAll: "ALL", + industryFilterRecent: "RECENT", + industryFilterMember: "MEMBER", + industryFilterBlueprints: "BLUEPRINTS", + industryFilterBuilds: "BUILDS", + industryFilterMine: "MINE", + industryFilterActive: "WORKING", + industryFilterPaused: "PAUSED", + industryFilterDissolved: "DISSOLVED", + industryAllTitle: "Industry", + industryRecentTitle: "Recent facilities", + industryMemberTitle: "Facilities I'm in", + industryMineTitle: "My facilities", + industryActiveTitle: "Working", + industryPausedTitle: "Paused facilities", + industryDissolvedTitle: "Dissolved facilities", + industryNoFacilitiesFound: "No facilities found.", + industryCreateFacility: "Create Facility", + industryMembers: "Members", + industryMemberBadge: "MEMBER", + industryName: "Name", + industryNamePlaceholder: "Facility name", + industryDescriptionLabel: "Description", + industryDescriptionPlaceholder: "What does this facility produce?", + industrySector: "Sector", + industryMembershipPolicy: "Membership policy", + industryQuorum: "Quorum", + industryMajority: "Majority", + industryLaborRate: "Labor rate", + industryCreateButton: "Create Facility", + industryUpdateButton: "Update", + industrySteward: "Steward", + industryStatusActive: "WORKING", + industryStatusPaused: "PAUSED", + industryStatusDissolved: "DISSOLVED", + industryStatusLabel: "Status", + industryJoinButton: "Join", + industryApplyButton: "Request to join", + industryLeaveButton: "Leave", + industryInviteButton: "Invite", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "You need an invitation to join.", + industryPauseButton: "Vote to pause", + industryResumeButton: "Vote to resume", + industryDeleteButton: "Delete", + industryPendingApplicants: "Pending applicants", + industryVotesYes: "yes", + industryVotesNo: "no", + industryAlreadyVoted: "voted", + industryAdmit: "Admit", + industryReject: "Reject", + industryDissolveTitle: "Dissolve facility", + industryDissolveVote: "Vote to dissolve", + industryPolicy_open: "Open", + industryPolicy_vote: "By vote", + industryPolicy_invite: "Invite only", projectsTitle: "Projects", projectsDescription: "Create, fund, and follow community-driven projects in your network.", + projectSearchPlaceholder: "Search in Projects...", projectCreateProject: "Create Project", projectCreateButton: "Create Project", projectUpdateButton: "UPDATE", @@ -3208,7 +3345,7 @@ module.exports = { projectRecentTitle: "Recent Projects", projectTopTitle: "Top Funded", projectTitlePlaceholder: "Project name", - projectImage: "Upload media (max-size: 50MB)", + projectImage: "Upload media (max-size: 75MB)", projectDescription: "Description", projectDescriptionPlaceholder: "Tell the story and goals…", projectGoal: "Goal (ECO)", @@ -3229,14 +3366,8 @@ module.exports = { projectPledgeTitle: "Back this project", projectPledgePlaceholder: "Amount in ECO", projectBounties: "Bounties", - projectBountiesInputLabel: "Bounties (one per line: Title|Amount [ECO]|Description)", - projectBountiesPlaceholder: "Fix UI bug|100|Link to issue\nWrite docs|250|Outline usage examples", projectNoBounties: "No bounties found.", projectTitle: "Title", - projectAddBountyTitle: "New Bounty", - projectBountyTitle: "Bounty Title", - projectBountyAmount: "Amount (ECO)", - projectBountyDescription: "Description", projectMilestoneSelect: "Select Milestone", projectBountyCreateButton: "Create Bounty", projectBountyStatus: "Bounty Status", @@ -3251,19 +3382,15 @@ module.exports = { projectMilestoneTitle: "Milestone Title", projectMilestoneTargetPercent: "Percent (%)", projectMilestoneDueDate: "Date", - projectMilestoneCreateButton: "Create Milestone", projectMilestoneStatus: "Milestone Status", projectMilestoneOpen: "open", projectMilestoneDone: "completed", projectMilestoneDue: "due", - projectNoMilestones: "No milestones found.", projectMilestoneMarkDone: "Mark as Done", projectMilestoneTitlePlaceholder: "Enter milestone title", projectMilestoneDescriptionPlaceholder: "Enter description for this milestone", projectMilestoneDescription: "Milestone Description", - projectBudgetGoal: "Budget (Goal)", projectBudgetAssigned: "Assigned to bounties", - projectBudgetRemaining: "Remaining", projectBudgetOver: "⚠ Over budget: assigned exceeds goal", projectFollowers: "Followers", projectFollowersTitle: "Followers", @@ -3276,7 +3403,6 @@ module.exports = { projectBackersTotalPledged: "Total pledged", projectBackersYourPledge: "Your pledge", projectBackersNone: "No pledges yet.", - projectNoRemainingBudget: "No remaining budget.", projectFilterBackers: "BACKERS", projectFilterApplied: "APPLIED", projectAppliedTitle: "APPLIED", @@ -3285,36 +3411,19 @@ module.exports = { projectBackerAmount: "Total contributed", projectBackerPledges: "Pledges", projectBackerProjects: "Projects", - projectPledgeAmount: "Amount", projectSelectMilestoneOrBounty: "Select Milestone or Bounty", projectPledgeButton: "Pledge", footerLicense: "GPLv3", - footerPackage: "Package", - footerVersion: "Version", modulesModuleName: "Name", modulesModuleDescription: "Description", modulesModuleStatus: "Status", - modulesTotalModulesLabel: "Loaded Modules", + modulesTotalModulesLabel: "Total", modulesEnabledModulesLabel: "Enabled", modulesDisabledModulesLabel: "Disabled", - modulesPopularLabel: "Popular", - modulesPopularDescription: "Module to receive posts that are trending, most viewed, or most commented on.", - modulesTopicsLabel: "Topics", - modulesTopicsDescription: "Module to receive discussion categories based on shared interests.", - modulesSummariesLabel: "Summaries", - modulesSummariesDescription: "Module to receive summaries of long discussions or posts.", - modulesLatestLabel: "Latest", - modulesLatestDescription: "Module to receive the most recent posts and discussions.", - modulesThreadsLabel: "Threads", - modulesThreadsDescription: "Module to receive conversations grouped by topic or question.", - modulesMultiverseLabel: "Multiverse", - modulesMultiverseDescription: "Module to receive content from other federated peers.", modulesInvitesLabel: "Invites", modulesInvitesDescription: "Module to manage and apply invite codes.", modulesWalletLabel: "Wallet", modulesWalletDescription: "Module to manage your digital assets (ECOin).", - modulesLegacyLabel: "Legacy", - modulesLegacyDescription: "Module to manage your secret (private key) quickly and securely.", modulesCipherLabel: "Cipher", modulesCipherDescription: "Module to encrypt and decrypt your text symmetrically (using a shared password).", modulesBookmarksLabel: "Bookmarks", @@ -3349,8 +3458,12 @@ module.exports = { modulesOpinionsDescription: "Module to discover and vote on opinions.", modulesTransfersLabel: "Transfers", modulesTransfersDescription: "Module to discover and manage smart-contracts (transfers).", - modulesFediverseLabel: "Fediverse", - modulesFediverseDescription: "Manage your other fediverse accounts, including sending and receiving content.", + modulesFediverseLabel: "Multiverse", + modulesBlogsLabel: "Blogs", + modulesBlogsDescription: "Module to discover and manage blogs.", + modulesPollsLabel: "Polls", + modulesPollsDescription: "Module to ask the network and count the answers.", + modulesFediverseDescription: "Manage your other multiverse accounts, including sending and receiving content.", modulesFeedLabel: "Feed", modulesFeedDescription: "Module to discover and share short-texts (feeds).", modulesParliamentLabel: "Parliament", @@ -3372,55 +3485,54 @@ module.exports = { modulesProjectsLabel: "Projects", modulesProjectsDescription: "Module to explore, crowd-funding and manage projects.", modulesBankingLabel: "Banking", - modulesBankingDescription: "Module to determine the real value of ECOIN and distribute a UBI using the common treasury.", + modulesBankingDescription: "Module to manage the common treasury: ECOIN value, taxes and UBI.", modulesFavoritesLabel: "Favorites", modulesFavoritesDescription: "Module to manage your favorite content.", fileTooLargeTitle: "File too large", - fileTooLargeMessage: "The file exceeds the maximum allowed size (50 MB). Please select a smaller file.", + fileTooLargeMessage: "The file exceeds the maximum allowed size (75 MB). Please select a smaller file.", goBack: "Go back", + errorDismiss: "OK", errorPageTitle: "Something went wrong", + errorNoAllocation: "No allocation found for this claim.", + errorInvalidClaim: "This allocation cannot be claimed.", visibilityLabel: "Visibility", + clearnetPrivateConflict: "Private, hidden, closed, invite-only or paid content cannot be published on the CLEARNET. Choose OASIS or make it public.", visibilityPublic: "Public", visibilityHidden: "Hidden", visibilityMakePublic: "Make public", visibilityMakeHidden: "Make hidden", invitesInhabitantsTitle: "Inhabitants", - invitesInhabitantsFollow: "Give Support", - aiNavPlaceholder: "Where do you want to go?", + aiNavPlaceholder: "Describe what you need...", aiNavDisabled: "AI navigation is disabled.", - aiNavResultsTitle: "AI navigation suggestions", + aiNavResultsTitle: "AINav Suggestions", aiNavQueryLabel: "Query", - aiNavResultsDescription: "Pick the route that best matches what you are looking for.", - aiNavResultPath: "Route", - aiNavResultDescription: "What you can do", aiNavResultMatch: "Match", aiNavResultsEmpty: "No matching routes. Try /search instead.", - aiNavSearchInstead: "Search instead", modulesAINavLabel: "AINav", modulesAINavDescription: "Module for natural-language queries about the network's content.", - directConnect: "Direct Connect", directConnectDescription: "Connect directly to a peer by entering their IP address, port and public key. The peer will be added as a followed connection.", peerHost: "IP", - peerPort: "Port (default: 8008)", + peerPort: "Port", peerPublicKey: "Public Key (@...ed25519)", connectAndFollow: "Connect", - deviceSourceLabel: "Device source", modulesPresetTitle: "Common Configurations", - modulesPreset_minimal: "Minimal", - modulesPreset_basic: "Basic", - modulesPreset_social: "Social", - modulesPreset_economy: "Economy", - modulesPreset_full: "Full", + workflowsTitle: "Workflows", + workflowsDescription: "A workflow sets the theme and which modules are active.", + workflowsSet: "Set workflow", + workflow_default: "Default", + workflow_jobs: "Jobs", + workflow_social: "Social", + workflow_business: "Business", + workflow_night: "Night", + workflow_kids: "Kids", + workflow_activists: "Activists", + workflow_mobile: "Mobile", statsCarbonFootprintTitle: "Carbon Footprint", - statsCarbonFootprintNetwork: "Network carbon footprint", - statsCarbonFootprintYours: "Your carbon footprint", statsCarbonTombstone: "Tombstoning footprint", - feedSuccessMsg: "Feed published successfully!", - dominantOpinionLabel: "Dominant opinion", - uploadMedia: "Upload media (max-size: 50MB)", + uploadMedia: "Upload media (max-size: 75MB)", mapsLabel: "Maps", mapTitle: "Maps", - mapDescription: "Explore and manage offline maps in your network.", + mapDescription: "Manage and share offline maps in your network.", mapMineSectionTitle: "Your Maps", mapCreateSectionTitle: "Create Map", mapUpdateSectionTitle: "Update Map", @@ -3444,14 +3556,13 @@ module.exports = { mapDescriptionLabel: "Description", mapDescriptionPlaceholder: "Describe the map or location...", mapTypeLabel: "Map type", - mapTypeSingle: "SINGLE (only initial location)", - mapTypeOpen: "OPEN (anyone can add markers)", - mapTypeClosed: "CLOSED (only creator adds markers)", + mapInviteMode: "Invite", + mapTypeSingle: "SINGLE", + mapTypeOpen: "OPEN", + mapTypeClosed: "CLOSED", mapTagsLabel: "Tags", mapTagsPlaceholder: "Enter tags separated by commas", mapUrlLabel: "Map URL", - mapPickCoordLabel: "Or select a location on the grid:", - mapMarkersLabel: "markers", mapMarkersTitle: "Markers", mapMarkerDefault: "Marker", mapMarkerLatLabel: "Marker latitude", @@ -3463,12 +3574,21 @@ module.exports = { mapAddMarkerButton: "Add Marker", mapCleanMarkerButton: "Clean Marker", mapApplyZoom: "Apply Zoom", - mapSearchPlaceholder: "Search description, tags, author...", + mapPlaceSearchPlaceholder: "Find a place or country...", + mapNoPlaces: "No places found.", + mapClusterTitle: "markers here, click to zoom in", + mapDeleteMarker: "Delete marker", + mapZoomIn: "Zoom in", + mapZoomOut: "Zoom out", + mapFitMarkers: "Fit all markers", + mapPanLabel: "Move the map", + mapCenterHere: "Center here", + mapNear: "near", + mapPickedSpot: "Picked spot", + mapPickHint: "Click on the map to pick a spot.", + mapClearPick: "Clear", + mapSearchPlaceholder: "Search in Maps...", mapSearchButton: "Search", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", mapUpdatedAt: "Updated", mapNoMatch: "No maps match your search.", noMaps: "No maps available.", @@ -3479,37 +3599,485 @@ module.exports = { typeMapMarker: "MAP MARKER", modulesMapLabel: "Maps", modulesMapDescription: "Module to manage and share offline maps.", + emergenciesTitle: "Emergencies", + modulesEmergenciesLabel: "Emergencies", + modulesEmergenciesDescription: "Module to raise, confirm and follow emergencies in the network.", + emergenciesDescription: "Module to raise, confirm and follow emergencies in the network.", + emergencyCreate: "Create Emergency", + emergencyUpdate: "Update", + emergencyDelete: "Delete", + emergencyTitleLabel: "Title", + emergencyTitlePlaceholder: "What is happening?", + emergencyTextLabel: "Details", + emergencyTextPlaceholder: "Where, since when, what to do...", + emergencyCategoryLabel: "Category", + emergencyCategoryWeather: "WEATHER", + emergencyCategoryInfrastructure: "INFRASTRUCTURE", + emergencyCategoryHealth: "HEALTH", + emergencyCategorySecurity: "SECURITY", + emergencyCategoryLost: "LOST & FOUND", + emergencyCategoryNeighborhood: "NEIGHBORHOOD", + emergencyExpiresLabel: "Expires in", + emergencyExpires1d: "1 day", + emergencyExpires3d: "3 days", + emergencyExpires7d: "1 week", + emergencyExpires30d: "1 month", + emergencyExpiresNone: "Undefined", + emergencyExpiresKeep: "Keep current date", + emergencyTagsLabel: "Tags", + emergencyTagsPlaceholder: "Enter tags separated by commas", + emergencyFilterAll: "ALL", + emergencyFilterMine: "MINE", + emergencyFilterRecent: "RECENT", + emergencySeverityUnverified: "UNVERIFIED", + emergencySeverityLow: "LOW", + emergencySeverityMedium: "MEDIUM", + emergencySeverityHigh: "HIGH", + emergencyStatusActive: "ACTIVE", + emergencyStatusResolved: "RESOLVED", + emergencyStatusExpired: "EXPIRED", + emergencyConfirmations: "Confirmations", + emergencyConfirmButton: "Confirm", + emergencyConfirmed: "You confirmed this emergency", + emergencyResolveButton: "Resolve", + emergencyReopenButton: "Reopen", + emergencyGenerateReport: "Generate Report", + emergencyUpdateEdited: "Edited", + emergencyLastUpdate: "LAST UPDATE", + emergencyUpdatesTitle: "Last Updates:", + emergencyNoUpdates: "No updates yet.", + emergencyAddUpdate: "Post update", + emergencyUpdatePlaceholder: "What changed?", + emergencyNoItems: "No emergencies found.", + emergencyExpiresAt: "Expires", + emergencyBannerLabel: "Active emergency", + emergencyCreateSectionTitle: "Create Emergency", + emergencyUpdateSectionTitle: "Update Emergency", + emergencySearchPlaceholder: "Search in Emergencies...", + typeEmergency: "EMERGENCIES", + typeEmergencyConfirm: "EMERGENCY CONFIRM", + typeEmergencyUpdate: "EMERGENCY UPDATE", + statsEmergency: "Emergencies", + statsEmergencyConfirm: "Emergency confirmations", + statsEmergencyUpdate: "Emergency updates", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "An emergency you follow has an update.", + emergencyBotResolvedTitle: "An emergency you follow has been resolved.", + dataKindEmergencies: "EMERGENCIES", + mailingTitle: "Mailing Lists", + modulesMailingLabel: "Mailing Lists", + modulesMailingDescription: "Discover and manage mailing lists in your network.", + modulesPhoneLabel: "Phone", + modulesPhoneDescription: "Module to call other inhabitants with end-to-end encrypted videocalls.", + mailingDescription: "Discover and manage mailing lists in your network.", + mailingCreate: "Create Mailing List", + mailingUpdate: "Update", + mailingArchive: "Archive", + mailingActivate: "Activate", + mailingCreateSectionTitle: "Create Mailing List", + mailingUpdateSectionTitle: "Update Mailing List", + mailingTitleLabel: "Title", + mailingTitlePlaceholder: "Name of the mailing list", + mailingDescriptionLabel: "Description", + mailingDescriptionPlaceholder: "What is this list about?", + mailingTypeLabel: "Mailing list type", + mailingTypeOpen: "OPEN", + mailingTypeClosed: "CLOSED", + mailingStatusLabel: "Status", + mailingStatusActive: "ACTIVE", + mailingStatusArchived: "ARCHIVED", + mailingMembersLabel: "Invite inhabitants", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Subscribers", + mailingTagsLabel: "Tags", + mailingTagsPlaceholder: "Enter tags separated by commas", + mailingFilterAll: "ALL", + mailingFilterMine: "MINE", + mailingFilterSubscribed: "SUBSCRIBED", + mailingFilterRecent: "RECENT", + mailingHistoryDate: "DATE", + mailingHistoryThreads: "THREADS", + mailingHistoryInhabitants: "INHABITANTS", + mailingHistorySearchPlaceholder: "Search in the history...", + mailingNoMessages: "No messages yet.", + mailingNoItems: "No mailing lists found.", + mailingSearchPlaceholder: "Search in Mailing Lists...", + mailingWriteButton: "Write message", + mailingWriteTitle: "Write to the list", + mailingReplyTitle: "Reply to the list", + mailingSend: "Send", + mailingTextPlaceholder: "Your message reaches every participant of the list.", + mailingParticipants: "Participants", + inboxFilterMailing: "Mailing lists", + mailingReplyPlaceholder: "Reply to the list...", + mailingReplyToList: "Reply to list", + typeMailingList: "MAILING LIST", + statsMailingList: "Mailing lists", + mailingListLabel: "MAILING LIST", + logisticsTitle: "Logistics", + modulesLogisticsLabel: "Logistics", + modulesLogisticsDescription: "Discover and manage logistic plans in your network.", + logisticsDescription: "Discover and manage logistic plans in your network.", + logisticsCreate: "Create Route", + logisticsUpdate: "Update", + logisticsClose: "Close", + logisticsReopen: "Reopen", + logisticsCreateSectionTitle: "Create Route", + logisticsUpdateSectionTitle: "Update Route", + logisticsTitleLabel: "Title", + logisticsTitlePlaceholder: "Ride to the market, van available...", + logisticsKindLabel: "Logistics type", + logisticsKindTrip: "TRIP", + logisticsKindShipment: "SHIPMENT", + logisticsKindTrips: "TRIPS", + logisticsKindShipments: "SHIPMENTS", + logisticsModeLabel: "Mode", + logisticsModeOffer: "OFFER", + logisticsModeRequest: "REQUEST", + logisticsModeOffers: "OFFERS", + logisticsModeRequests: "REQUESTS", + logisticsDescriptionLabel: "Description", + logisticsDescriptionPlaceholder: "Meeting point, what you carry, conditions...", + logisticsOriginLabel: "Origin", + logisticsOriginPlaceholder: "Where it starts", + logisticsDestinationLabel: "Destination", + logisticsDestinationPlaceholder: "Where it ends", + logisticsDateLabel: "Date and time", + logisticsRecurrenceLabel: "Recurrence", + logisticsFrequencyLabel: "Frequency", + logisticsRecurrenceNone: "ONE-OFF", + logisticsRecurrenceDaily: "DAILY", + logisticsRecurrenceWeekly: "WEEKLY", + logisticsRecurrenceMonthly: "MONTHLY", + logisticsSeatsLabel: "Seats", + logisticsSizeLabel: "Size", + logisticsSizePlaceholder: "e.g. 2m x 1m", + logisticsWeightLabel: "Weight", + logisticsWeightPlaceholder: "e.g. 40kg", + logisticsPriceFree: "FREE", + logisticsPriceEco: "ECO", + logisticsPriceTime: "TIME", + logisticsPriceLabel: "Price", + logisticsHours: "h", + logisticsOrderRefLabel: "Order link", + logisticsOrderRefPlaceholder: "/shops/... or /market/... of an order to deliver", + logisticsOrderLink: "Linked order", + logisticsTagsLabel: "Tags", + logisticsTagsPlaceholder: "Enter tags separated by commas", + logisticsFilterAll: "ALL", + logisticsFilterMine: "MINE", + logisticsFilterBooked: "BOOKED", + logisticsFilterRecent: "RECENT", + logisticsFilterHistory: "HISTORY", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "OPEN", + logisticsStatusUpcoming: "UPCOMING", + logisticsStatusPast: "PAST", + logisticsStatusClosed: "CLOSED", + logisticsZoneLabel: "Zone", + logisticsSearchPlaceholder: "Search in Logistics...", + logisticsNoItems: "No routes found.", + logisticsBookingPending: "PENDING", + logisticsBookingConfirmed: "CONFIRMED", + logisticsBookingRejected: "REJECTED", + logisticsBookingCancelled: "CANCELLED", + logisticsBookingDelivered: "DELIVERED", + logisticsConfirm: "Confirm", + logisticsReject: "Reject", + logisticsDeliver: "Deliver", + logisticsReceiptLabel: "Receipt", + logisticsReceiptPlaceholder: "Delivery note (optional)", + logisticsCancelBooking: "Cancel booking", + logisticsBookTitle: "Book this route", + logisticsNotesLabel: "Notes", + logisticsNotesPlaceholder: "Anything the owner should know", + logisticsBook: "Book", + logisticsBookingsTitle: "Bookings", + logisticsMyBookingTitle: "Your booking", + logisticsRatingsTitle: "Ratings", + logisticsNoRatings: "No ratings yet.", + logisticsRatingsWhenClosed: "Ratings open once the route is closed.", + logisticsScoreLabel: "Score", + logisticsRatingPlaceholder: "How did it go?", + logisticsRate: "Rate", + typeLogisticsRoute: "ROUTE", + typeLogisticsRating: "ROUTE RATING", + statsLogisticsRoute: "Routes", + statsLogisticsRating: "Route ratings", + logisticsRouteLabel: "ROUTE", + podcastsTitle: "Podcasts", + modulesPodcastsLabel: "Podcasts", + modulesPodcastsDescription: "Module to publish podcast channels.", + podcastsDescription: "Module to publish podcast channels.", + podcastCreate: "Create Podcast", + podcastUpdate: "Update", + podcastDelete: "Delete", + podcastCreateSectionTitle: "Create Podcast", + podcastUpdateSectionTitle: "Update Podcast", + podcastTitleLabel: "Title", + podcastTitlePlaceholder: "Name of the channel", + podcastDescriptionLabel: "Description", + podcastDescriptionPlaceholder: "What is this podcast about?", + podcastCategoryLabel: "Category", + podcastCategoryNews: "NEWS", + podcastCategoryMusic: "MUSIC", + podcastCategoryTalk: "TALK", + podcastCategoryEducation: "EDUCATION", + podcastCategoryTech: "TECH", + podcastCategoryCulture: "CULTURE", + podcastCategoryCommunity: "COMMUNITY", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "OTHER", + podcastTagsLabel: "Tags", + podcastCoverLabel: "Cover (image or video)", + podcastEpisodeAudioLabel: "Audio or video file (max-size: 75MB)", + podcastTagsPlaceholder: "Enter tags separated by commas", + podcastFilterAll: "ALL", + podcastFilterMine: "MINE", + podcastFilterRecent: "RECENT", + podcastFilterTop: "TOP", + podcastFilterViewers: "VIEWERS", + podcastSearchPlaceholder: "Search in Podcasts...", + podcastNoItems: "No podcasts found.", + podcastEpisodesLabel: "Episodes", + podcastPlaysLabel: "Views", + podcastLastEpisode: "Last episode", + podcastEpisodesTitle: "Episodes", + podcastNoEpisodes: "No episodes yet.", + podcastEpisodeAdd: "Add episode", + podcastEpisodeCreateTitle: "Publish episode", + podcastEpisodeUpdateTitle: "Update episode", + podcastEpisodeTitleLabel: "Episode title", + podcastEpisodeTitlePlaceholder: "Title of the episode", + podcastEpisodeDescriptionLabel: "Show notes", + podcastEpisodeDescriptionPlaceholder: "What happens in this episode, links, credits...", + podcastEpisodePublish: "Publish", + podcastPlayed: "Played", + podcastMarkPlayed: "Mark as played", + typePodcast: "PODCAST", + typePodcastEpisode: "EPISODE", + typePodcastOpinion: "EPISODE OPINION", + typePodcastPlay: "EPISODE PLAY", + statsPodcast: "Podcasts", + statsPodcastEpisode: "Episodes", + statsPodcastPlay: "Episode plays", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "A blog you follow has a new entry.", + podcastBotEpisodeTitle: "A podcast you follow has a new episode.", + podcastLabel: "PODCAST", + podcastEpisodeLabel: "EPISODE", + campaignsTitle: "Campaigns", + modulesCampaignsLabel: "Campaigns", + modulesCampaignsDescription: "Discover and manage campaigns in your network.", + campaignsDescription: "Discover and manage campaigns in your network.", + campaignCreate: "Create Campaign", + campaignUpdate: "Update", + campaignDelete: "Delete", + campaignClose: "Close", + campaignReopen: "Reopen", + campaignCreateSectionTitle: "Create Campaign", + campaignUpdateSectionTitle: "Update Campaign", + campaignTitleLabel: "Title", + campaignTitlePlaceholder: "What do you want to achieve?", + campaignTextLabel: "Text", + campaignTextPlaceholder: "Explain the cause, who decides and what you ask for...", + campaignCategoryLabel: "Category", + campaignCategoryEnvironment: "ENVIRONMENT", + campaignCategoryRights: "RIGHTS", + campaignCategoryHealth: "HEALTH", + campaignCategoryEducation: "EDUCATION", + campaignCategoryInfrastructure: "INFRASTRUCTURE", + campaignCategoryCulture: "CULTURE", + campaignCategoryEconomy: "ECONOMY", + campaignCategoryOther: "OTHER", + campaignGoalLabel: "Signature goal", + campaignDeadlineLabel: "Deadline", + campaignTagsLabel: "Tags", + campaignTagsPlaceholder: "Enter tags separated by commas", + campaignFilterAll: "ALL", + campaignFilterMine: "MINE", + campaignFilterSigned: "SIGNED", + campaignFilterRecent: "RECENT", + campaignFilterTop: "TOP", + campaignStatusOpen: "OPEN", + campaignStatusAchieved: "ACHIEVED", + campaignStatusUnachieved: "UNACHIEVED", + campaignStatusClosed: "CLOSED", + campaignSearchPlaceholder: "Search in Campaigns...", + campaignNoItems: "No campaigns found.", + campaignSignaturesLabel: "Signatures", + campaignSignaturesTitle: "Supporters", + campaignNoSignatures: "No signatures yet.", + campaignSignTitle: "Sign this campaign", + campaignSignPlaceholder: "Why do you support it? (optional)", + campaignSign: "Sign", + campaignSigned: "You already signed!", + campaignNotSigned: "Your signature is still missing", + campaignElevate: "Raise to Parliament", + campaignElevated: "Raised to Parliament", + campaignUpdatesTitle: "Updates", + campaignNoUpdates: "No updates yet.", + campaignUpdatePlaceholder: "What happened?", + campaignAddUpdate: "Post update", + campaignQrAlt: "QR to sign this campaign", + campaignQrCaption: "Scan to sign", + typeCampaign: "CAMPAIGN", + typeCampaignSignature: "SIGNATURE", + typeCampaignUpdate: "CAMPAIGN UPDATE", + statsCampaign: "Campaigns", + statsCampaignSignature: "Campaign signatures", + statsCampaignUpdate: "Campaign updates", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "A campaign you follow has an update.", + campaignBotAchievedTitle: "A campaign you follow has reached its goal.", + campaignBotRaisedTitle: "A campaign you signed has been raised to Parliament.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "A route you booked has changed.", + logisticsBotClosedTitle: "A route you booked has been closed.", + logisticsBotBookedTitle: "Someone booked your route.", + logisticsBotCancelledTitle: "A booking on your route was cancelled.", + logisticsBotConfirmedTitle: "Your booking was confirmed: the package is on its way.", + logisticsBotRejectedTitle: "Your booking was rejected.", + logisticsBotDeliveredTitle: "Your package has arrived.", + campaignLabel: "CAMPAIGN", + campaignGenerateReport: "Generate Report", + logisticsGenerateReport: "Generate Report", + mailingGenerateReport: "Generate Report", + campaignUpdateEdited: "Edited", + mailingMessagesLabel: "Messages", + logisticsBookingsLabel: "Bookings", + backupTitle: "Backup", + modulesBackupLabel: "Backup", + modulesBackupDescription: "Module to export and import your keys and make encrypted backups.", + backupDescription: "Module to export and import your keys and make encrypted backups.", + backupTypeRecovery: "RECOVERY", + backupTypeKeys: "EXPORT KEYS", + backupTypeFull: "FULL BACKUP", + backupTypeRestore: "RESTORE BACKUP", + backupTypeInstant: "INSTANT", + backupInstantDescription: "A plain .zip of your whole log and blobs, without your secret key and without a password. Quick, but ", + backupInstantRisk: "anyone who gets the file can read everything in it.", + backupKitPageDescription: "The recovery kit can only be opened from this device.", + backupKitId: "Oasis ID", + backupKitDate: "Generated", + backupKitQrAlt: "QR code of your secret key", + backupKitStep1: "Print it or write it down and store it somewhere safe and offline.", + backupKitStep2: "To recover, install Oasis on a new device and import this secret from Backup › Keys.", + backupKitStep3: "Destroy this page if it is ever exposed.", + backupKitPdfButton: "Generate PDF", + backupScopeLabel: "Scope", + backupScopeAll: "EVERYTHING", + backupScopeMine: "ONLY MY CONTENT", + backupSinceLabel: "Since", + backupEstimateTitle: "Estimate", + backupFileInfo: "Your encrypted copy will be downloaded to your device (name: oasis-backup-DATE.oasisbk)", + backupExportButton: "Download backup", + backupRestorePassword: "Write the password used when the backup was made", + backupRestoreButton: "Restore backup", + backupRestoredLine: "Backup restored.", + backupRestoredMessages: "Messages added", + backupRestoredSkipped: "Already present", + backupRestoredBlobs: "Files added", + backupRestoredState: "Settings restored", + backupRestoredFailed: "Failed", + backupRestoredForked: "Diverged", + backupRestoreRunning: "Restoring the backup. This page refreshes automatically; you can keep using Oasis meanwhile.", + backupRestoreElapsed: "Elapsed", + backupRestoreFailedLine: "The backup could not be restored.", + backupRestoreForkMine: "This device already published messages with your identity that are not in the backup, so the copy could not be applied on top of them. Restore on a clean installation right after importing your keys, before doing anything else.", + verificationTitle: "Verification", + verificationDescription: "Check the chain of your feed, detect forks and review orphan or missing files.", + verificationRun: "Run verification", + verificationTotalMessages: "messages in the log", + verificationFeed: "Your feed", + verificationMessages: "messages", + verificationLastSequence: "last sequence", + verificationGaps: "Gaps", + verificationBrokenLinks: "Broken links", + verificationBadHashes: "Bad hashes", + verificationBadSignatures: "Bad signatures", + verificationDuplicates: "Duplicated sequences", + verificationForks: "Forks", + verificationBlobs: "Files", + verificationBlobsPresent: "present", + verificationBlobsReferenced: "referenced", + verificationBlobsOrphan: "orphan", + verificationBlobsMissing: "missing", + verificationBlobsMissingOwn: "Your own files missing from this device", + verificationBlobsMissingOthers: "Files from other inhabitants not downloaded yet", + verificationBlobsUnavailable: "Files could not be listed on this device.", + wikiTitle: "Wikis", + modulesWikiLabel: "Wikis", + modulesWikiDescription: "Module to discover and manage wikis.", + wikiDescription: "Module to discover and manage wikis.", + wikiCreate: "Create Wiki", + wikiUpdate: "Update", + wikiDelete: "Delete", + wikiEdit: "Edit", + wikiTitleLabel: "Title", + wikiTitlePlaceholder: "Wiki title...", + wikiBodyLabel: "Content", + wikiBodyPlaceholder: "Link other wikis with [[Wiki title]] or [[Wiki title|a custom label]].\n\nMention an inhabitant with @id.\n\nTag with #oasis.\n\nPaste links like https://solarnethub.com\n\nA blank line starts a new paragraph; single line breaks are kept.\n\nAttach any supported media and it will be embedded here.", + formatBarLabel: "Format", + wikiStatusLabel: "Wiki type", + wikiStatusOpen: "OPEN", + wikiStatusClosed: "CLOSED", + wikiTagsLabel: "Tags", + wikiTagsPlaceholder: "Enter tags separated by commas", + wikiSummaryLabel: "Change summary", + wikiSummaryPlaceholder: "What changed?", + wikiFilterAll: "ALL", + wikiFilterMine: "MINE", + wikiFilterRecent: "RECENT", + wikiFilterLinked: "LINKED", + wikiChanges: "Recent changes", + wikiNoPages: "No wikis found.", + wikiNoChanges: "No changes yet.", + wikiHistory: "History", + wikiVersion: "Version", + wikiVersions: "versions", + wikiRestore: "Restore", + wikiDiff: "Changes", + wikiViewVersion: "View", + wikiLinkedPages: "Linked pages", + wikiNoLinkedPages: "This page is not linked to any other yet.", + wikiMissingLink: "Page does not exist yet", + wikiDuplicateTitle: "A page with this title already exists. Change the title to create a different page.", + wikiCreateSectionTitle: "Create Wiki", + wikiUpdateSectionTitle: "Update Wiki", + wikiSearchPlaceholder: "Search in Wiki...", + wikiCurrentVersion: "Current version", + wikiViewingVersion: "You are viewing an old version", + wikiBackToPage: "Back to page", + wikiLinkedChip: "Linked", + wikiRestoredSummary: "Restored version", + wikiExportPdf: "Generate Page", + wikiPreview: "Preview", + tribeSectionWiki: "WIKI", + tribeWikiCreate: "Create Wiki", + tribeWikiEmpty: "No wikis, yet.", + typeWiki: "WIKI", padsTitle: "Pads", padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Module to manage collaborative text editors.", - padFilterAll: "ALL", - padFilterMine: "MINE", - padFilterRecent: "RECENT", - padFilterOpen: "OPEN", - padFilterClosed: "CLOSED", padCreate: "Create Pad", - padUpdate: "Update Pad", - padDelete: "Delete Pad", + padUpdate: "Update", + padDelete: "Delete", padTitleLabel: "Title", padTitlePlaceholder: "Enter pad title...", padStatusLabel: "Status", + padTypeLabel: "Pad type", padStatusOpen: "OPEN", padStatusInviteOnly: "INVITE-ONLY", padStatusClosed: "CLOSED", padDeadlineLabel: "Deadline", padTagsLabel: "Tags", - padTagsPlaceholder: "tag1, tag2, ...", + padTagsPlaceholder: "Enter tags separated by commas", padMembersLabel: "Members", - padVisitPad: "Visit Pad", - padShareUrl: "Share URL", padCreated: "Created", - padAuthor: "Author", - padGenerateCode: "Generate Code", - padInviteCodeLabel: "Invite Code", - padInviteCodePlaceholder: "Enter invite code...", - padValidateInvite: "Validate", - padStartEditing: "START EDITING!", + padStartEditing: "Edit", padEditorPlaceholder: "Start writing...", padSubmitEntry: "Submit", padNoEntries: "No entries yet.", @@ -3517,63 +4085,171 @@ module.exports = { padMineSectionTitle: "Your Pads", padsNoItems: "No pads found.", padsDescription: "Manage collaborative encrypted text editors in your network.", + roomsTitle: "Rooms", + roomsDescription: "Discover and join encrypted videoconference rooms in your network.", + modulesRoomsLabel: "Rooms", + modulesRoomsDescription: "Module to discover and join encrypted videoconference rooms.", + roomFilterRecent: "RECENT", + roomFilterMine: "MINE", + roomFilterAll: "ALL", + roomFilterLive: "LIVE", + roomFilterOpen: "OPEN", + roomFilterInvite: "INVITE-ONLY", + roomFilterClosed: "CLOSED", + roomCreate: "Create Room", + roomUpdate: "Update", + roomDelete: "Delete", + roomClose: "Close", + roomReopen: "Reopen", + roomCreateSectionTitle: "Create Room", + roomUpdateSectionTitle: "Update Room", + roomTitlePlaceholder: "Room title", + roomDescriptionLabel: "Description", + roomDescriptionPlaceholder: "What will you talk about?", + roomTypeLabel: "Room type", + roomTagsLabel: "Tags", + roomStatusOpen: "OPEN", + roomStatusInviteOnly: "INVITE-ONLY", + roomStatusClosed: "CLOSED", + roomParticipants: "Participants", + roomJoin: "Join", + roomLeave: "Leave", + roomFull: "Room full", + roomLiveChip: "LIVE", + roomMuted: "muted", + roomRec: "Record audio", + roomRecStop: "Stop recording", + roomStreamFile: "Stream", + roomStreamStop: "Stop", + roomStreaming: "Streaming", + liveFullscreen: "Full screen", + roomStreamPickTitle: "Choose a video", + roomStreamPickHint: "Videos on this device. The one you choose is streamed from where it is, without copying or publishing it.", + roomStreamGo: "Play in the room", + roomStreamNotFound: "That video is not in your folders.", + roomPublicWarning: "This room is broadcast on the public web: anyone can see and hear what happens in it.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} entered the room", + roomEventLeft: "{name} left the room", + roomEventRecStart: "{name} started recording", + roomEventRecStop: "{name} stopped recording", + roomEventMuted: "{name} muted", + roomEventUnmuted: "{name} unmuted", + roomEventHand: "{name} raised a hand", + roomRaiseHand: "Raise hand", + roomLowerHand: "Lower hand", + confirmActionTitle: "Confirm this action", + confirmActionButton: "Confirm", + confirmJoinText: "You are about to join using this invitation.", + confirmFollowText: "You are about to support this inhabitant.", + confirmSignText: "You are about to sign this campaign.", + confirmDownloadText: "You are about to download this file to your device.", + roomRecordings: "Recordings", + roomClearNotices: "Clear", + roomLineSwitchboard: "Switchboard", + roomLineDndError: "This room's number is not taking calls.", + roomNumberHint: "Dial this number from Phone to join the room.", + liveRefresh: "Refresh", + roomNobody: "Nobody is in the room right now.", + roomSomeone: "{n} in the room right now.", + roomYou: "you", + roomYouAreIn: "You are in", + roomNobodyInside: "Nobody is in this room right now.", + roomErrorFull: "The room is full. You can join when someone leaves.", + roomErrorUnreachable: "The room can't be reached right now. Try again later.", + roomErrorBusy: "You are already in a call or in another room.", + roomErrorClosed: "This room is closed.", + roomAccessDenied: "This room is invite-only. You need an invitation to join.", + roomsNoItems: "No rooms found.", + roomSearchPlaceholder: "Search rooms...", + roomInviteMode: "Invite", + tribeSectionRooms: "ROOMS", + tribeRoomCreate: "Create Room", + tribeRoomsEmpty: "No rooms, yet.", + typeRoom: "ROOMS", + typeRoomMember: "ROOM PARTICIPANT", + statsRoom: "Rooms", + statsRoomMember: "Room participants", + favoritesFilterRooms: "ROOMS", + invitesRoomsTitle: "Rooms", + inviteCodeInvalid: "This invitation code is not valid or has already been used.", + inviteAlreadyMember: "You are already in. You can open it from its module.", + actionFailed: "That could not be done. Please try again.", + publishTooShort: "Your post is too short.", + walletSendFailed: "The payment could not be sent: {reason}", + pixeliaCoordsError: "Those coordinates are outside the grid.", + courtsErrTitle: "A title is required.", + courtsErrRespondent: "Say who the case is against.", + courtsErrRespondentId: "That is not a valid Oasis ID.", + courtsErrMethod: "Choose how the case will be resolved.", + courtsErrCaseNotFound: "That case could not be found.", + courtsErrBrief: "Write your response.", + courtsErrStance: "Choose your stance.", + courtsErrResult: "Write the result.", + courtsErrTerms: "Write the terms of the agreement.", + courtsErrMediator: "Add at least one mediator.", + courtsErrSelfMediator: "You cannot be a mediator in your own case.", + courtsErrJudge: "Choose a judge.", + courtsErrSelfJudge: "You cannot be the judge of your own case.", + courtsErrVisibility: "Choose who can see the case.", + courtsErrNomination: "That nomination could not be found.", + opinionAlreadyGiven: "You have already given your opinion.", + cipherPasswordShort: "The password must be at least 32 characters long.", + errorForbidden: "You don't have access to this.", + errorNotFound: "That could not be found.", + errorClosed: "This is closed.", + errorInviteOnly: "This is invite-only. You need an invitation.", + errorTooLong: "That text is too long.", + errorRequired: "Some required information is missing.", + errorInvalid: "Some of the information is not valid.", + errorNotAuthor: "Only the author can do this.", + panicRemoved: "Your blockchain has been deleted.", padVersionHistory: "Version History", + padAttributionView: "By author", padVersionView: "View", padRecentSectionTitle: "Recent Pads", padOpenSectionTitle: "Open Pads", padClosedSectionTitle: "Closed Pads", padCreateSectionTitle: "Create New Pad", padUpdateSectionTitle: "Update Pad", - padInviteGenerated: "Invite Code Generated", typePad: "PADS", padNew: "NEW", padAddFavorite: "Add to Favorites", padRemoveFavorite: "Remove from Favorites", - padClose: "Close Pad", + padClose: "Close", padBackToEditor: "Back to editor", - padSearchPlaceholder: "Search pads...", + padSearchPlaceholder: "Search in Pads...", calendarsTitle: "Calendars", calendarTitle: "Calendar", modulesCalendarsLabel: "Calendars", modulesCalendarsDescription: "Module to discover and manage calendars.", typeCalendar: "CALENDARS", - calendarFilterAll: "ALL", - calendarFilterMine: "MINE", - calendarFilterOpen: "OPEN", - calendarFilterClosed: "CLOSED", - calendarFilterRecent: "RECENT", - calendarFilterFavorites: "FAVORITES", calendarCreate: "Create Calendar", calendarUpdate: "Update", - calendarDelete: "Delete", calendarTitleLabel: "Title", calendarTitlePlaceholder: "Calendar title...", calendarStatusLabel: "Status", + calendarTypeLabel: "Calendar type", calendarStatusOpen: "OPEN", calendarStatusClosed: "CLOSED", calendarDeadlineLabel: "Deadline", calendarTagsLabel: "Tags", - calendarTagsPlaceholder: "tag1, tag2...", + calendarTagsPlaceholder: "Enter tags separated by commas", calendarParticipantsLabel: "Participants", - calendarParticipantsCount: "Participants", - calendarVisitCalendar: "Visit Calendar", calendarCreated: "Created", - calendarAuthor: "Author", calendarJoin: "Join Calendar", calendarGenerateInvite: "Generate invite", - calendarInviteCodePlaceholder: "Enter invite code...", - calendarValidateInvite: "Validate code", - calendarJoined: "Joined", - calendarAddDate: "Add Date", - calendarAddNote: "Add Note", calendarDateLabel: "Date", calendarDatePlaceholder: "Describe this date...", - calendarNoteLabel: "Note", + calendarNoteLabel: "Notes", calendarNotePlaceholder: "Add a note...", calendarFirstDateLabel: "Date", calendarFirstNoteLabel: "Notes", calendarIntervalLabel: "Interval", + calendarIntervalNone: "No repetition", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3584,8 +4260,8 @@ module.exports = { calendarsDescription: "Discover and manage calendars in your network.", calendarMonthPrev: "\u2190 Prev", calendarMonthNext: "Next \u2192", - calendarMonthLabel: "Dates", - calendarsShareUrl: "Share URL", + calendarMonthNames: "January,February,March,April,May,June,July,August,September,October,November,December", + calendarDayInitials: "M,T,W,T,F,S,S", calendarAllSectionTitle: "Calendars", calendarRecentSectionTitle: "Recent Calendars", calendarFavoritesSectionTitle: "Favorites", @@ -3597,19 +4273,16 @@ module.exports = { calendarAddFavorite: "Add to Favorites", calendarDeleteNote: "Delete", calendarRemoveFavorite: "Remove from Favorites", - calendarSearchPlaceholder: "Search calendars...", + calendarSearchPlaceholder: "Search in Calendar...", calendarAddEntry: "Add Entry", - calendarLeave: "Leave Calendar", statsCalendar: "Calendars", statsCalendarDate: "Calendar Dates", statsCalendarNote: "Calendar Notes", modulesChatsLabel: "Chats", - modulesChatsDescription: "Module to discover and manage encrypted chats.", + modulesChatsDescription: "Module to discover and manage chats.", typeChat: "CHATS", typeChatMessage: "CHAT MESSAGE", - chatLabel: "CHATS", - chatMessageLabel: "CHAT MESSAGES", chatsTitle: "Chats", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3619,52 +4292,188 @@ module.exports = { chatDescription: "Description", chatCategory: "Category", chatStatus: "STATUS", - chatFilterAll: "ALL", - chatFilterMine: "MINE", - chatFilterRecent: "RECENT", - chatFilterFavorites: "FAVORITES", - chatFilterOpen: "OPEN", - chatFilterClosed: "CLOSED", chatCreate: "Create Chat", - chatUpdate: "Update Chat", - chatDelete: "Delete Chat", + chatUpdate: "Update", + chatDelete: "Delete", chatClose: "Close Chat", chatVisitChat: "VISIT CHAT", chatUntitled: "Untitled Chat", chatNoItems: "No chats found.", chatParticipants: "Participants", - chatStartChatting: "START CHATTING!", - chatGenerateCode: "Generate Code", - chatShareUrl: "Share URL", + chatMessagesLabel: "Messages", + chatThreadsLabel: "Threads", chatCreatedAt: "CREATED", - chatSearchPlaceholder: "Search chats...", + chatSearchPlaceholder: "Search in Chats...", chatStatusOpen: "OPEN", chatStatusInviteOnly: "INVITE-ONLY", chatStatusClosed: "CLOSED", chatSendMessage: "Send", + chatJumpLatest: "Latest Message", + chatReply: "Reply", + chatReplyCancel: "Cancel reply", + chatPin: "Pin", + chatUnpin: "Unpin", + chatPinned: "Pinned", + chatPickChat: "Pick a chat to open it", + chatNoneYet: "No chat available yet.", + activitySearchPlaceholder: "Search in Activity...", + trendingSearchPlaceholder: "Search in Trending...", + opinionsSearchPlaceholder: "Search in Opinions...", + votesSearchPlaceholder: "Search in Votations...", + gamesSearchPlaceholder: "Search in Games...", + schoolTitle: "School", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "A wiki page you follow has been edited.", + wikiBotRestoredTitle: "An earlier version of a wiki page you follow has been restored.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "A new student has enrolled in your course.", + schoolBotInvitedTitle: "You have been invited to a course.", + schoolBotAdmittedTitle: "You have been admitted to a course.", + schoolBotCertificateTitle: "You have received a certificate.", + schoolBotPassedTitle: "A student has passed your course.", + schoolBotLessonTitle: "A new lesson has been published.", + invitesSchoolTitle: "School", + schoolEnrollLabel: "Join Course", + schoolExamRetryLater: "You can retry after 24 hours.", + schoolApproved: "APPROVED", + schoolExamPassMark: "Pass mark (0-10)", + schoolExamPassed: "PASSED", + schoolExamPending: "Pending", + schoolExamFailed: "FAILED", + schoolExamAddQuestion: "Add question", + schoolExamCreateQuestion: "Create question", + schoolExamOption: "Option", + schoolExamCorrect: "Correct option", + schoolExamPoints: "Points", + schoolMaterialTextPlaceholder: "Notes for this lesson...", + schoolMaterials: "Materials", + schoolAddMaterial: "Add material", + schoolNoMaterials: "No materials yet.", + bankYourSchoolBalance: "Your School Earnings", + schoolProgress: "Progress", + schoolProgressMark: "Mark as completed", + schoolProgressDone: "Completed", + schoolExams: "Exams", + schoolExamCreate: "Create exam", + schoolExamTitlePlaceholder: "Exam title", + schoolExamHelp: "One question per line: Question? ; option ; correct option* ; option", + schoolExamQuestionsPlaceholder: "What is SSB? ; A database ; A P2P protocol* ; A browser", + schoolExamSubmit: "Submit answers", + schoolExamScore: "Your score", + schoolNoScoresYet: "No results yet.", + schoolLessonUnit: "Unit / phase", + schoolLessonUnitPlaceholder: "e.g. Phase 1", + schoolLessonOrder: "Order", + profileClearnetSchoolLabel: "School", + schoolStartDate: "Start date", + schoolSessionDate: "Session date", + schoolCourseChat: "Course Chat", + agendaFilterSchool: "SCHOOL", + agendaFilterCampaigns: "CAMPAIGNS", + agendaFilterLogistics: "LOGISTICS", + schoolCourseType: "Course type", + schoolTypeOpen: "OPEN", + schoolTypePaid: "PAID", + schoolTypeInvite: "INVITE-ONLY", + schoolLessonLocked: "Encrypted lesson — ask the teacher for the course key.", + schoolGrantAccess: "Send course key", + schoolKeySent: "Key sent", + schoolOngoing: "Ongoing", + schoolFilterApplied: "APPLIED", + schoolFilterOpen: "OPEN", + schoolFilterFavorites: "FAVORITES", + schoolPendingPayment: "Payment pending", + schoolPendingStudents: "Pending enrollments", + schoolViewBill: "View bill", + schoolPaymentNote: "Enrolling generates an ECO bill (a transfer). Your enrollment is confirmed once the transfer is signed.", + schoolCloseCourse: "Close course", + schoolReopenCourse: "Reopen course", + schoolDescription: "Teach and learn courses in your network.", + schoolFilterAll: "ALL", + schoolFilterMine: "MINE", + schoolFilterTop: "TOP", + schoolFilterRecent: "RECENT", + schoolCreateButton: "Create Course", + schoolCreateSectionTitle: "Create a course", + schoolUpdateSectionTitle: "Update course", + schoolCourseTitlePlaceholder: "Name of your course", + schoolCourseDescriptionPlaceholder: "What will you teach in this course?", + schoolCourseImage: "Course image (max-size: 75MB)", + schoolTags: "Tags", + schoolTagsPlaceholder: "Enter tags separated by commas", + schoolPrice: "Price (ECO)", + schoolVisibility: "Visibility", + schoolVisibilityPublic: "Public", + schoolVisibilityInvite: "By invitation", + schoolStatus: "Status", + schoolClosed: "Closed", + schoolFree: "Free", + schoolCreate: "Create course", + schoolUpdate: "Update course", + schoolSearchPlaceholder: "Search in School...", + schoolSortTitle: "Title", + schoolNoCourses: "No courses found.", + schoolStudents: "Students", + schoolTeacher: "Teacher", + schoolCreatedAt: "CREATED", + schoolLessons: "Lessons", + schoolNoLessons: "No lessons yet.", + schoolEnrollToSee: "Enroll to see the lessons of this course.", + schoolEnrollButton: "ENROLL", + schoolUnenrollButton: "Leave course", + schoolInviteRequired: "This course is by invitation.", + schoolTeacherPanel: "Teacher Panel", + schoolStudentPanel: "Student Panel", + schoolAddLesson: "Create lesson", + schoolLessonTitlePlaceholder: "Lesson title", + schoolLessonTextPlaceholder: "Describe the notions this lesson covers...", + schoolInviteStudents: "Invite students", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "INVITE", + schoolInvited: "Invited", + schoolIssueCertificate: "Create certificate", + schoolCertified: "Certified", + schoolCertificateTextPlaceholder: "Certificate note (optional)", + schoolLessonOpen: "Open lesson", + schoolCertificatePdf: "PDF", + schoolCertificates: "Certificates", + schoolNoCertificates: "No certificates issued yet.", + schoolEditButton: "Edit", + schoolDeleteButton: "Delete", + typeSchool: "SCHOOL", + schoolCourseLabel: "COURSE", + emergencyLabel: "EMERGENCY", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Courses", + statsSchoolLesson: "Lessons", + statsSchoolEnroll: "Enrollments", + statsSchoolCertificate: "School certificates", + modulesSchoolLabel: "School", + modulesSchoolDescription: "Module to exchange knowledge.", + welcomeStepUxAction: "Save View", + welcomeStepWorkflowTitle: "What's the use for OASIS you have in mind?", + welcomeStepWorkflowText: "Oasis has many modules. Pick the configuration that fits you and only the useful ones stay on. You can fine-tune it anytime in Modules.", + welcomeStepWorkflowAction: "Save Configuration", + welcomeWorkflow_full: "Default", + chatRateLimitTitle: "Too many messages", + chatRateLimitMessage: "You have reached the limit of messages this room accepts in an hour. Try again later.", chatMessagePlaceholder: "Type your message...", chatNoMessages: "No messages yet.", - chatLeave: "Leave Chat", - chatInviteCodeLabel: "Enter invite code", - chatJoinByInvite: "Join", chatTitlePlaceholder: "Chat title", chatDescriptionPlaceholder: "Chat description", - chatTagsPlaceholder: "tag1, tag2, tag3", + chatTagsPlaceholder: "Enter tags separated by commas", chatImageLabel: "Select an image file (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "Invite Code", chatAuthor: "Author", - chatCreated: "Created", chatAddFavorite: "Add to Favorites", chatRemoveFavorite: "Remove from Favorites", chatPM: "PM", chatStatusLabel: "Status", + chatTypeLabel: "Chat type", chatCategoryLabel: "Category", - chatParticipantsLabel: "Participants", gamesTitle: "Games", gamesDescription: "Discover and play some mini-games in your network.", gamesFilterAll: "ALL", gamesPlayButton: "PLAY!", - gamesBackToGames: "Back to Games", modulesGamesLabel: "Games", modulesGamesDescription: "Module to discover and play some games.", modulesGraphosLabel: "Graphos", @@ -3681,8 +4490,6 @@ module.exports = { gamesArkanoidDesc: "Break all the bricks with your paddle and ball. A classic arcade challenge.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Classic ping-pong against an AI opponent. First to 5 points wins.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "Race against time! Dodge traffic and reach the finish line before the clock runs out.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilot your ship through a deadly asteroid field. Shoot them down before they hit you.", gamesRockPaperScissorsTitle: "Rock Paper Scissors", @@ -3702,9 +4509,11 @@ module.exports = { gamesTetrisTitle: "Tetris", gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Mix live visuals on two decks: pick a pattern for each, tune speed, colour and zoom, crossfade between them and flash to the beat. Tap the tempo or let the microphone drive the pulse.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", + gamesMinesweeperTitle: "Minesweeper", + gamesMinesweeperDesc: "Classic mine hunting. Clear the board to level up — bigger grids, more mines, higher score.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3713,13 +4522,58 @@ module.exports = { gamesHallDate: "Date", typeGameScore: "GAME SCORES", gamesNewRecord: "New Record", - gameScoreLabel: "GAME SCORES", uxModeTitle: "UX", uxModeDescription: "Select which UX navigation mode you want for your GUI.", uxModeMenus: "Blocks", + uxModeChats: "Conversations", + uxModeFeed: "Microblogging", + feedTrendingTitle: "Trending Tags", + feedActiveTitle: "Active Inhabitants", + cnBrandSub: "Libre · P2P · Federated", + cnSyncedPeers: "Synced-peers", + cnLicense: "License", + cnNotAccessible: "The content is not accessible at this moment.", + cnUntitled: "Untitled", + cnSearchPlaceholder: "Search…", + cnHubTitle: "Clearnet HUB", + cnHubDescription: "Public content shared by the inhabitants of this Oasis network.", + cnHubEmpty: "No public content has been shared to Clearnet yet.", + cnPublicContent: "Content", + cnTribesContent: "Tribes", + cnNetworkContent: "Network", + cnCategoryEmpty: "No content in this category.", + cnInhabitantEmpty: "This inhabitant has not published content to Clearnet yet.", + cnPrevPage: "← Previous", + cnNextPage: "Next →", + cnPageOf: "Page {page} of {pages}", + cnMoreInfo: "More info →", + cnOpenCourse: "Open course", + cnAuction: "AUCTION", + cnExchange: "EXCHANGE", + cnRssDescription: "published by the inhabitants of this HUB", + cnKindAudio: "Audio", + cnKindBlog: "Blog", + cnKindBookmark: "Bookmark", + cnKindDocument: "Document", + cnKindEvent: "Event", + cnKindFeed: "Feed", + cnKindImage: "Image", + cnKindJob: "Job", + cnKindMarket: "Market", + cnKindPodcast: "Podcast", + cnKindProject: "Project", + cnKindCourse: "Course", + cnKindShop: "Shop", + cnKindTorrent: "Torrent", + cnKindFile: "File", + cnKindVideo: "Video", + cnKindWiki: "Wiki", + cnKindRoom: "Room", + cnKindMap: "Map", uxModeAINav: "AI", statsChat: "Chats", statsChatMessage: "Chat messages", + statsWiki: "Wiki pages", statsPad: "Pads", statsPadEntry: "Pad entries", statsGameScore: "Gaming", @@ -3730,6 +4584,146 @@ module.exports = { tribeContentAccessDenied: "Access Denied", tribeContentAccessDeniedMsg: "This content belongs to a tribe. You must be a member to access it.", tribeViewTribes: "View Tribes", + tribeFilesEmpty: "No files, yet.", + filesLabel: "Files", + profileClearnetFilesLabel: "Files", + filesTitle: "Files", + filesDescription: "Explore and manage files in your network.", + fileAllSectionTitle: "Files", + fileMineSectionTitle: "Your Files", + fileRecentSectionTitle: "Recent Files", + fileTopSectionTitle: "Top Files", + fileFavoritesSectionTitle: "Favorites", + fileFilterAll: "ALL", + fileFilterMine: "MINE", + fileFilterRecent: "RECENT", + fileFilterTop: "TOP", + fileFilterFavorites: "FAVORITES", + fileCreateSectionTitle: "Upload File", + fileUpdateSectionTitle: "Update File", + fileCreateButton: "Upload File", + fileUpdateButton: "Update", + fileAddFavoriteButton: "Add Favorite", + fileRemoveFavoriteButton: "Remove Favorite", + fileFileLabel: "Select a file (max-size: 75MB)", + calendarFilterRecent: "RECENT", + calendarFilterAll: "ALL", + calendarFilterMine: "MINE", + calendarFilterFavorites: "FAVORITES", + calendarFilterOpen: "OPEN", + calendarFilterClosed: "CLOSED", + padFilterRecent: "RECENT", + padFilterAll: "ALL", + padFilterMine: "MINE", + padFilterOpen: "OPEN", + padFilterClosed: "CLOSED", + chatFilterRecent: "RECENT", + chatFilterAll: "ALL", + chatFilterMine: "MINE", + chatFilterFavorites: "FAVORITES", + chatFilterOpen: "OPEN", + chatFilterClosed: "CLOSED", + licenseLabel: "License", + phoneTitle: "Phone", + phoneDescription: "Manage your end-to-end encrypted videocalls.", + phoneUnavailable: "Calls are not available on this device.", + phoneCallButton: "Call", + profileVisibilityPhone: "Phone number", + phoneNumberUnknown: "No one in your network has the number {number}.", + phoneNumberChoose: "Check who you want to call at {number}:", + phoneAnswer: "Answer", + phoneSpeaker: "Speaker", + phoneEarpiece: "Earpiece", + phoneReject: "Reject", + phoneHangup: "Hang up", + phoneMute: "Mute", + phoneUnmute: "Unmute", + phoneCameraOn: "Camera on", + phoneCameraOff: "Camera off", + settingsPhoneVideo: "Video", + settingsPhoneVideoOn: "Allowed in calls and rooms", + settingsPhoneVideoOff: "Audio only", + phoneSilence: "Silence", + phoneUnsilence: "Hear again", + phoneCalling: "Calling", + phoneIsCalling: "is calling you", + notifyNewPmFrom: "New PM from", + notifyNewPamFrom: "New PaM from", + notifyUpdateLabel: "Update", + updateInstalledLabel: "Installed version", + updateAvailableLabel: "Available version", + updateChangesLabel: "Changes", + updateFilesLabel: "Files", + updateOriginLabel: "Source", + phoneRecording: "Recording PaM for", + phoneLeaveMessage: "Nobody answered: leave your message after the beep.", + phoneLeaveMessageBusy: "They are on another call: leave your message after the beep.", + phoneLeaveMessageUnreachable: "They could not be reached: leave your message after the beep.", + phoneConnecting: "Connecting to", + phoneSendPam: "Send PaM", + phoneCancel: "Cancel", + phoneSearchPlaceholder: "Search in Phone...", + phonePamTitle: "Audio Records", + phoneNew: "New", + phoneDelete: "Delete", + phoneNoPams: "You have no audio records.", + phoneHistoryTitle: "History", + phoneLatestTitle: "Latest Calls", + phoneCreateButton: "New call", + phoneLimitsHint: "Up to {n} inhabitants per call; more than one makes a joint call.", + phoneErrorPub: "Pubs cannot be called.", + phoneJointCall: "Joint call", + phoneWith: "With", + phoneResult: "Result", + phoneWhen: "When", + phoneDuration: "Duration", + phoneOutgoing: "Outgoing", + phoneIncoming: "Incoming", + phoneNoHistory: "No logs yet.", + phoneOutcomeEnded: "Answered", + phoneOutcomeMissed: "Missed", + phoneOutcomeRejected: "Rejected", + phoneOutcomeNoanswer: "No answer", + phoneOutcomeCancelled: "Cancelled", + phoneOutcomePam: "PaM sent", + phoneOutcomeFailed: "Failed", + phoneDialAdd: "Add", + phoneDialBack: "Delete", + phoneDialClear: "Clear", + phoneDialPlaceholder: "Number or Oasis ID", + phoneDialInhabitants: "Inhabitants", + phoneDialRemove: "Remove", + phoneOwnNumber: "That's your own number.", + phoneSettingsDesc: "Choose who can call you.", + phoneDnd: "Do not disturb", + phoneOpen: "Open", + phoneRecipientRefuses: "The recipient you are trying to call doesn't allow receiving phone calls.", + phoneRefusedNotice: "These inhabitants don't allow receiving phone calls and were not called:", + fileEmptyError: "The file is empty. Choose another file.", + fileTitleLabel: "Title", + fileTitlePlaceholder: "Title", + fileDescriptionLabel: "Description", + fileDescriptionPlaceholder: "Description", + fileTagsLabel: "Tags", + fileTagsPlaceholder: "Enter tags separated by commas", + fileSizeLabel: "Size", + fileDownloadButton: "DOWNLOAD", + fileDetailsButton: "DETAILS", + fileNoFile: "No file file", + fileSearchPlaceholder: "Search in Files...", + fileSearchButton: "Search", + fileSortRecent: "Most recent", + fileSortOldest: "Oldest", + fileSortTop: "Most voted", + fileNoMatch: "No matching files.", + fileUpdatedAt: "Updated", + typeFile: "FILES", + modulesFilesLabel: "Files", + modulesFilesDescription: "Module to discover and manage files.", + tribeSectionFiles: "FILES", + fileDescription: "Description", + fileDownload: "Download", + fileTypeLabel: "Type", torrentsTitle: "Torrents", torrentsDescription: "Explore and manage torrents in your network.", torrentAllSectionTitle: "Torrents", @@ -3757,47 +4751,91 @@ module.exports = { torrentTagsLabel: "Tags", torrentTagsPlaceholder: "Enter tags separated by commas", torrentSizeLabel: "Size", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "Generate torrent", + torrentFromContentMissing: "The file is not available on this node yet. Try again in a while.", + torrentDownloads: "Downloads", + torrentFetchOasis: "Download in Oasis", + torrentSaveFile: "Save file", + torrentSeeds: "seeds", + seedsLabel: "Seeds", + seedAction: "Support", + torrentSourceFile: "File", + torrentStateQueued: "Queued", + torrentStateProbing: "Looking for seeds", + torrentStateDownloading: "Downloading", + torrentStateDone: "Completed", + torrentStateFailed: "Failed", + torrentStateNoSource: "No seeds online yet, waiting", + torrentStateCancelled: "Cancelled", + torrentNoDownloads: "No downloads yet.", + torrentOriginalSection: "Original", + torrentCancel: "Cancel", + torrentDownloadButton: "DOWNLOAD", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "DETAILS", torrentNoFile: "No torrent file", noTorrents: "No torrents found.", - torrentSearchPlaceholder: "Search title, tags, author...", + noFiles: "No files found.", + torrentSearchPlaceholder: "Search in Torrents...", torrentSearchButton: "Search", torrentSortRecent: "Most recent", torrentSortOldest: "Oldest", torrentSortTop: "Most voted", torrentNoMatch: "No matching torrents.", - torrentMessageAuthorButton: "Message Author", torrentUpdatedAt: "Updated", statsTorrent: "Torrents", + statsFile: "Files", typeTorrent: "TORRENTS", modulesTorrentsLabel: "Torrents", modulesTorrentsDescription: "Module to discover and manage torrents.", favoritesFilterTorrents: "TORRENTS", + favoritesFilterFiles: "FILES", favoritesFilterMarket: "MARKET", + favoritesFilterEvents: "EVENTS", + favoritesFilterForum: "FORUM", + favoritesFilterHousing: "HOUSING", + favoritesFilterJobs: "JOBS", + favoritesFilterProjects: "PROJECTS", + favoritesFilterReports: "REPORTS", + favoritesFilterTasks: "TASKS", + favoritesFilterTransfers: "TRANSFERS", + favoritesFilterVotes: "VOTATIONS", favoritesFilterShopProducts: "SHOP ITEMS", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Upload Torrent", - tribeMediaTypeTorrent: "Torrent", + tribeCreateFile: "Upload File", settingsWishTitle: "Wish", - settingsWishDesc: "Configure the wish of your Avatar. This determines how to access the whole content and your level of experiences into the network.", + settingsWishDesc: "Determines how you access all the content and your experience in the Oasis network.", settingsWishWhole: "Multiverse", settingsWishMutuals: "Only mutual-support", settingsWishOnlyLan: "Only LAN", + settingsWishLocal: "Local", settingsPmVisibilityTitle: "Private Messages", settingsPmVisibilityDesc: "Configure the level of exposition to private messages that you want to have in the network.", settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", + mute: "Mute", + unmute: "Unmute", + relationshipMuted: "Muted by you", + hideContent: "Hide for me", + unhideContent: "Show again", + hiddenByYou: "Hidden by you", + settingsTabContent: "Content", + settingsHiddenTitle: "Hidden Content", + settingsHiddenDesc: "Inhabitants you muted and content you hid.", + settingsMutedLabel: "Muted inhabitants", + settingsHiddenLabel: "Hidden content", + settingsMutedEmpty: "Nobody muted.", + settingsHiddenEmpty: "Nothing hidden.", + settingsPmVisibilityFollowing: "Only people I follow", + settingsMentionsVisibilityTitle: "Mentions", + settingsMentionsVisibilityDesc: "Choose whose mentions reach you.", pmMutualNotice: "Outbound is a UX guardrail. Inbound is an attention filter (ciphertext is still replicated).", - pmBlockedNonMutual: "You can only send private messages to habitants with mutual support.", inhabitantsPendingFollowsTitle: "Pending follow requests", inhabitantsPendingAccept: "Accept", inhabitantsPendingReject: "Reject", - bxEncrypted: "ENCRYPTED", - bxEncryptedHexLabel: "Ciphertext (preview)", tribeSectionGovernance: "GOVERNANCE", - tribeSubStatusPublic: "PUBLIC", - tribeSubStatusPrivate: "PRIVATE", - tribeSubInheritedPrivate: "PRIVATE (inherited from main tribe)", tribePadInviteRequired: "You do not have access to the pad. Ask for an invitation to access the content.", tribeChatInviteRequired: "You do not have access to the chat. Ask for an invitation to access the content.", tribeGovernanceDesc: "Internal governance for this tribe. Propose candidatures, debate rules, elect leaders.", @@ -3836,57 +4874,43 @@ module.exports = { tribeGovNoRevocations: "Not any revocation, yet", tribeGovNoHistorical: "Not any record, yet", logsTitle: "Logs", - logsDescription: "Record your experience in the network.", - logsReadMore: "Read more", + logsDescription: "Keep a private diary of your experiences.", logsViewTitle: "Log", logsColumnType: "Type", logsView: "View", - logsManualPrompt: "Write your log", logsUpdateButton: "Update", - logsEditTitle: "Edit Log", - logsCreateDescription: "Record your experience...", + logsEditTitle: "Update Log", logsCreateTitle: "Create Log", logsWriteButton: "Write", logsTextPlaceholder: "Describe your experiences...", - logsTextField: "Text", - logsLabelPlaceholder: "Label...", - logsLabelField: "Write your log (label)", - logsAiDisabledWarn: "Enable the AI module in /modules to use AI-written logs.", - logsAiContextValue: "Blockchain day", - logsAiContext: "Context", - logsAiModStatus: "AImod", - logsModeApply: "Apply!", logsModeAIWritten: "AI-Assistant", logsModeManual: "Manual", logsModeAI: "AI", - logsColumnDelete: "Delete", - logsColumnEdit: "Edit", logsColumnLog: "Log", logsColumnDate: "Date", logsDelete: "Delete", - logsEdit: "Edit", + logsEdit: "Update", logsFilterAlways: "ALWAYS", logsFilterYear: "LAST YEAR", logsFilterMonth: "LAST MONTH", logsFilterWeek: "LAST WEEK", logsFilterToday: "TODAY", - logsFilterRecent: "RECENT", - logsFilterAll: "ALL", + logsComments: "Comments", + logsAuthorEntries: "Entries", logsCreate: "Create Log", logsExport: "Export Logs", - logsExportOne: "Export", + logsExportOne: "Export Log", logsViewDetails: "View Details", logsGenerateButton: "Generate Text", - logsSearchText: "Search in logs...", + logsSearchText: "Search in Logs...", logsSearchAnyType: "Any type", logsSearchButton: "Search", logsEmpty: "No logs yet.", + logsNothingNew: "There are no new actions to record.", modulesLogsLabel: "Logs", - modulesLogsDescription: "Module to record (via AI assistant) your experiences.", + modulesLogsDescription: "Module to keep a private diary of your experiences.", statsLogsTitle: "Logs", - statsLogsEntries: "Entries", typeLog: "LOGS", - blockchainCycle: "Cycle", } diff --git a/src/client/assets/translations/oasis_es.js b/src/client/assets/translations/oasis_es.js index 0faa594..cb106b8 100644 --- a/src/client/assets/translations/oasis_es.js +++ b/src/client/assets/translations/oasis_es.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { es: { - bbAdd: "Añadir", - bbQuickActions: "Acciones rápidas", - bbPickTitle: "Fijar un módulo", - bbPickDesc: "Elige un módulo para el hueco + de la barra inferior.", - bbPickNone: "Ninguno", - bbManage: "Editar", - bbYourBar: "Tu barra", - bbRemove: "Quitar", - bbFull: "Máximo alcanzado", - bbDone: "Hecho", - activityChangeFilter: "Cambiar filtro", - emptyConnectHint: "Conéctate a un pub para sincronizar con la red.", - emptyConnectCta: "Conéctate a un pub", - menuCommunity: "Comunidad", - activityGroupCommunity: "Comunidad y Gobernanza", - activityGroupOrg: "Organización", - activityGroupComm: "Comunicación", - activityGroupEconomy: "Economía", - activityGroupMedia: "Contenido y Media", - activityGroupOther: "Otros", - languageName: "Castellano", shopOrderStatusPaid: "Pago recibido", shopOrderStatusReceived: "Recibido", shopOrderMarkPaid: "Marcar pago recibido", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "Compras", shopMyOrdersDescription: "Tus pedidos de compra.", shopMyOrdersEmpty: "Aún no tienes compras.", - shopOrderPmSeller: "PM Seller", - shopOrderSeller: "Vendedor", - shopOrderPaymentConcept: "Pago", + shopOrderPmSeller: "PM Vendedora", + shopOrderSeller: "Vendedora", shopOrderInvoice: "Factura", shopOrderInvoiceConcept: "Factura", shopOrderStatusPending: "Pendiente", shopOrderStatusAccepted: "Aceptado", shopOrderStatusRejected: "Rechazado", shopOrderStatusShipped: "Enviado", - shopOrderStatusDelivered: "Entregado", shopOrderAccept: "Aceptar", shopOrderReject: "Rechazar", shopOrderMarkShipped: "Marcar enviado", - shopOrderMarkDelivered: "Marcar entregado", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "Contrato", + shopOrderPmBuyer: "PM Compradora", shopOrderContractConcept: "Pedido de tienda", shopOrdersPending: "Pedidos pendientes", all: "Todos", @@ -53,7 +28,7 @@ module.exports = { recent: "Recientes", anonymous: "Anónimo", unnamed: "Anónimo", - authorLabel: "AUTOR", + authorLabel: "Autoría", delete: "Eliminar", save: "Guardar", vote: "Votar", @@ -61,7 +36,7 @@ module.exports = { price: "Precio", priority: "Prioridad", severity: "Gravedad", - organizer: "Organizador", + organizer: "Organizadora", edited: "editado", isPublic: "Público", searchIsPublicLabel: "Público", @@ -75,18 +50,17 @@ module.exports = { viewJson: "Ver JSON", matchScore: "Puntuación de coincidencia", preferencesLabel: "Preferencias", - inhabitantProfileTitle: "Perfil del habitante", + inhabitantProfileTitle: "Habitante", contentWarningLabel: "Aviso de contenido", invalidPost: "Contenido no válido", invalidPostHint: "Este mensaje tiene texto vacío o no válido.", - spreadBy: "Por", - spreadChron: "Difusión", - spreaded: "Difundido", - spreadMore: "más", + spreadChron: "Réplicas", + spreaded: "Replicado", spreadContentUnavailable: "Contenido aún no disponible (replicación pendiente)", filteredByTag: "Filtrado por etiqueta", filteredByTagTitle: "Filtrado por etiqueta", feedPublishedSuccess: "¡Publicación creada con éxito!", + feedMastodonFailed: "Publicado en Oasis, pero no se ha podido enviar a Mastodon.", tribeFeedSent: "¡Mensaje enviado con éxito!", tribeContentEncrypted: "Contenido cifrado", tribeTorrentsEmpty: "Aún no hay torrents.", @@ -100,23 +74,20 @@ module.exports = { noSeverity: "Sin gravedad", calendarDeleteDate: "Eliminar fecha", calendarIntervalUntil: "Hasta", - bookmarkCategory: "Categoría", bookmarkLastVisit: "Última visita", profileClearnetBookmarksLabel: "Marcadores", - forumFilterHot: "Populares", invitesPort: "Puerto", - currentlyUnrecheable: "¡ERROR!", - peerHostValidation: "IPv4 válida (p. ej. 192.168.1.100) o nombre de host (p. ej. pub.example.com)", + peerHostValidation: "IPv4 válida (p. ej. 192.168.1.100) o nombre de host (p. ej. pub.solarnethub.com)", peerPortValidation: "Puerto 1-65535", peerKeyValidation: "Clave pública SSB ed25519 (@<44 caracteres base64>=.ed25519)", inboxJobUnsubscribedTitle: "Baja de tu oferta de empleo", - inboxProjectFollowedTitle: "Nuevo seguidor de tu proyecto", + inboxProjectFollowedTitle: "Nueva seguidora de tu proyecto", inboxProjectUnfollowedTitle: "Dejó de seguir tu proyecto", pmHasFollowedYourProject: "ha seguido tu proyecto", pmHasUnfollowedYourProject: "ha dejado de seguir tu proyecto", pmHasUnsubscribedFromYourJobOffer: "se ha dado de baja de tu oferta de empleo", marketDeleteButton: "Eliminar", - marketNoBids: "No se han realizado pujas, aún.", + marketNoBids: "Aún no se han realizado pujas.", marketAuctionBidAmount: "Importe de la puja", marketAuctionBidTime: "Hora de la puja", marketAuctionUser: "Pujador", @@ -133,11 +104,11 @@ module.exports = { taskDescriptionPlaceholder: "Descripción de la tarea", exportPasswordLabel: "Contraseña de exportación", importPasswordPlaceholder: "Introduce la contraseña", - searchCasesPlaceholder: "Buscar casos…", + searchCasesPlaceholder: "Buscar en Tribunales...", shopBuyDeliveryAddressPlaceholder: "Dirección de entrega", shopBuyNotesPlaceholder: "Notas", bankTaxesUserNoteBold: "reducir directamente el impuesto ECO", - bankTaxesUserNoteIntro: "Tu impuesto ECO se deduce del excedente que la red genera por encima de tu RBU; el valor base de la RBU se fija como mínimo inamovible. El impuesto nunca se deduce de la cantidad fija que corresponde a cada habitante como RBU. Los fondos deducidos alimentan el algoritmo de redistribución de riqueza y se canalizan de vuelta a otros habitantes mediante sus reclamos de RBU. Esos otros habitantes que reciben más RBU para sus proyectos probablemente tendrán alguna tarea dedicada a ayudarte a reducir tu impuesto ECO. O pueden dedicarse a ", + bankTaxesUserNoteIntro: "Tu impuesto ECO se deduce del excedente que la red genera por encima de tu RBU; el valor base de la RBU se fija como mínimo inamovible. El impuesto nunca se deduce de la cantidad fija que corresponde a cada habitante como RBU. Los fondos deducidos alimentan el algoritmo de redistribución de riqueza y se canalizan de vuelta a otras habitantes mediante sus reclamos de RBU. Esas otras habitantes que reciben más RBU para sus proyectos probablemente tendrán alguna tarea dedicada a ayudarte a reducir tu impuesto ECO. O pueden dedicarse a ", bankTaxesUserNoteOutro: ", y el consenso de la red puede requerir su contribución continua.", courtsViewDetails: "Ver", courtsViewDetailsShort: "Detalles", @@ -149,22 +120,23 @@ module.exports = { courtsDetailsNotPublic: "No público", courtsEvidenceSideAccuser: "Pruebas del acusador", courtsEvidenceSideRespondent: "Pruebas de la defensa", - courtsEvidenceSideUnknown: "Desconocido", + courtsEvidenceSideUnknown: "Desconocida", courtsJudgeNotAssigned: "-", courtsSupportCase: "Apoyar el caso", courtsSettlementAcceptBtn: "Aceptar acuerdo", courtsSupportCount: "Apoyos", - courtsVerdictVoteTitle: "Votar el veredicto", - courtsVerdictVoteLabel: "Voto del veredicto", - courtsVerdictVoteAccept: "Aceptar", - courtsVerdictVoteReject: "Rechazar", - courtsVerdictVoteSubmit: "Enviar voto", reportsSetStatus: "Cambiar estado", reportsStatusClosed: "CERRADO", statsAvgPerInhabitant: "Media por habitante", statsCarbonMaxAnnual: "Estimación máx. anual", statsCarbonNetwork: "Total de la red", statsCarbonOfEstMax: "de la capacidad máx. estimada", + statsCarbonYearProgress: "del año transcurrido", + statsNetworkTitle: "Red", + statsStorageTitle: "Almacenamiento", + statsEcoTaxTitle: "Tasa ECO", + statsAccountTitle: "Cuenta", + statsAveragesTitle: "Promedios", statsCarbonOfNetwork: "del total de la red", statsCarbonUser: "Tu huella", statsContentCountColumn: "Cantidad", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "Etiquetas principales", statsTopTypesTitle: "Tipos de contenido principales", statsTotalMsgs: "Mensajes totales", - feedViewDetails: "Ver detalles", - eventFileLabel: "Adjuntar imagen", - taskFileLabel: "Adjuntar imagen", - courtsRespondentRequired: "El ID del demandado es obligatorio", extended: "Multiverso", extendedDescription: [ "Cuando apoyas a alguien, puedes descargar las publicaciones de habitantes que apoya, y esas publicaciones aparecerán aquí, ordenadas por la más reciente.", @@ -215,40 +183,70 @@ module.exports = { ], profile: "Avatar", inhabitants: "Habitantes", - peersReplicatedFeeds: "Feeds replicados", + peersReplicatedFeeds: "Replicados", + peersTorColumn: "TOR", + peersTorYes: "Accesible por Tor", + peersTorNo: "No accesible por Tor", + peersOasisId: "ID", + peersOasisVersion: "Versión", graphos: "Graphos", graphosDescription: "Mapa interactivo de la red alrededor de ti.", + graphosFederation: "Federación", + graphosFedPubs: "PUBs", + graphosFedUnreachable: "Inalcanzables", + graphosFedLinks: "Enlaces mutuos", + graphosFedIsolated: "Aislados", + graphosFedNoData: "Sin datos", + graphosFedMutual: "Mutuo", + graphosFedOneWay: "En un sentido", + graphosFedTransport: "Red", + graphosFedWith: "Federado con", + graphosZoom: "Ampliar", + graphosFedInhabitants: "Habitantes", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "Tu nodo aún no conoce ningún PUB.", graphosViewingGraphOf: "Viendo", graphosBackToMine: "← Mi red", graphosShowing: "Mostrando", graphosYou: "Tú", graphosTotalNodes: "Nodos totales", - manualMode: "Modo Manual", mentions: "Menciones", mentionsDescription: [ - strong("Publicaciones que te @mencionan"), - ", ordenadas por las más recientes.", + strong("Todos los mensajes que te @mencionan"), + ".", ], - nextPage: "Siguiente", - previousPage: "Anterior", - noMentions: "No has recibido @menciones, aún.", + noMentions: "Aún no has recibido @menciones.", + mentionsSearchPlaceholder: "Buscar en Menciones...", privateReminders: "RECORDATORIOS", + inboxFilterPms: "MPs", + inboxFilterNotifications: "Notificaciones", + inboxMarkRead: "Marcar como leído", + inboxMarkUnread: "Marcar como no leído", + inboxUnreadChip: "No leído", + inboxMarkAllRead: "Marcar todo como leído", + inboxDeleteShown: "Borrar todo", + inboxArchive: "Archivar", + inboxUnarchive: "Desarchivar", + inboxFilterArchived: "Archivados", + inboxEarlierMessages: "mensajes anteriores", + inboxSortThread: "Por hilo", + inboxSortRecent: "Más recientes primero", + inboxAllBots: "Bots", + settingsInboxBotsTitle: "Notificaciones del Inbox", + settingsInboxBotsDesc: "Elige qué bots pueden avisarte en tu Inbox. Los bots desmarcados se ocultan y sus avisos no se cuentan.", inboxFiltersLabel: "Filtros:", - inboxToggleToMutuals: "Cambiar a mutuos", + inboxToggleToMutuals: "Cambiar a mutuas", inboxToggleToWhole: "Cambiar a todos", - privateFrom: "De", - privateTo: "Para", privateDate: "Fecha", peers: "Nodos", - searchDescription: "Descripición", - imageSearch: "Buscar Imágenes", + searchDescription: "Descripción", searchFromLabel: "Desde", searchToLabel: "Hasta", - searchMinOpinionsLabel: "Mín. opiniones", - searchInhabitantLabel: "Habitante (Oasis ID)", searchQueryLabel: "Búsqueda", - searchPerPageLabel: "Resultados por página", + searchPerPageLabel: "Resultados", settings: "Configuración", + settingsDescription: "Configura el entorno de tu red.", continueReading: "Continuar leyendo", moreComments: "más comentarios", readThread: "leer el resto del hilo", @@ -257,67 +255,46 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Reproduce la melodía de tu blockchain — cada bloque se convierte en una nota.', melodyEmpty: 'Aún no hay notas — tu blockchain todavía no ha generado ningún bloque.', - melodyCompositionHelp: 'Cada bloque de tu blockchain se convierte en una nota musical según su tipo y tamaño.', - melodyStatsTitle: 'Desglose por tipo', - melodyInhabitantLabel: 'Habitante', melodyTotalBlocks: 'Notas', - melodyType: 'Tipo', - melodyCount: 'Bloques', melodyFilterAll: 'TODAS', - melodyFilterShare: 'Subir melodía', - melodyShareTitle: 'Comparte tu melodía', - melodyShareHelp: 'Publica una instantánea de tu melodía actual para que otros la escuchen y remezclen.', - melodyShareTitleLabel: 'Título (opcional)', - melodyShareTitlePlaceholder: 'Un fantasma de blockchain', - melodyShareSubmit: 'Publicar Melodía', - melodyNoShared: 'Aún no hay melodías de blockchain.', melodyRegenerate: 'Regenerar', melodyDownload: "Descargar BCS", - profileActivityTitle: 'Actividad', - profileActivityMore: 'Ver toda la actividad', profileContentSectionTitle: 'Contenido del Avatar', profileContentHelp: 'Elige qué módulos se mostrarán en tu avatar.', profileSensorsHelp: 'Métricas opcionales que se muestran en tu avatar.', profileClearnetHelp: 'Módulos que pueden ser consultados desde fuera de Oasis.', profileHubAll: 'Todo', - profileHubTitle: 'Contenido Público', - footerPulseTitle: 'Pulso', - footerPulsePeers: 'Pares', - footerPulseInbox: 'Bandeja', - footerPulseSync: 'Última sinc.', - footerPulseEcoValue: 'ECO/h', - footerPulseLastActivity: 'Última actividad', footerLicenseLabel: 'Licencia', - profileSwitchToOasis: 'Volver a Oasis', - profileSwitchToClearnet: 'Sumérgete en Clearnet', - profileVisibilityFediverse: "Fediverse", - profileSwitchToFediverse: "Sumérgete en el Fediverso", - coordLabel: 'Coordenadas (e.g., A3)', - coordPlaceholder: 'Introduce coordenada', + profileVisibilityFediverse: "Multiverso", contributorsTitle: "Contribuciones", - pixeliaBy: "por", colorLabel: 'Elige un color', - paintButton: 'Dibujar!', - invalidCoordinate: 'Coordenada incorrecta', - goToMuralButton: "Ver Mural", + paintButton: "¡Dibujar!", totalPixels: 'Total Pixeles', modules: "Módulos", modulesViewTitle: "Módulos", modulesViewDescription: "Define tu entorno ideal activando y desactivando módulos.", inbox: "Buzón", + inboxSearchPlaceholder: "Buscar en Buzón...", multiverse: "Multiverso", - fediverse: "Fediverse", - fediverseDescription: "Gestiona tus otras cuentas del fediverso, incluyendo enviar y recibir contenido.", + fediverse: "Multiverso", + fediverseDescription: "Gestiona tus otras cuentas del multiverso, incluyendo enviar y recibir contenido.", fediverseStatus: "Estado", - fediverseDisconnected: "Fediverse desconectado. Comprueba %LINK% o el estado de la conexión.", - fediverseSettingsTitle: "Fediverse", + fediverseDisconnected: "Multiverso desconectado. Comprueba %LINK% o el estado de la conexión.", + fediverseSettingsTitle: "Multiverso", fediverseInstanceLabel: "Dirección", fediverseTokenLabel: "Token de acceso", - fediverseTokenHelp: "Configura tu token de acceso. Se usará para gestionar tu cuenta de Mastodon desde Oasis.", fediverseConnect: "Conectar", fediverseConnectedAs: "Conectado como", + nextcloudServerLabel: "Servidor", + nextcloudUserLabel: "Usuario", + nextcloudFolderLabel: "Carpeta", + nextcloudUploadButton: "Subir", + nextcloudFileLabel: "Elige un fichero", + fileExternalLink: "Enlace externo", + nextcloudErrConnect: "No se ha podido llegar a Nextcloud con estos datos.", + nextcloudErrUpload: "La subida a Nextcloud ha fallado.", + nextcloudErrShare: "Nextcloud no ha dado un enlace público para el fichero.", fediverseDisconnect: "Desconectar", - fediverseEmpty: "Cuando conectes otras cuentas del fediverso desde %LINK%, podrás gestionarlas desde aquí.", fediverseEmptyLink: "tus ajustes", fediverseComposePlaceholder: "¿Qué estás pensando?", fediverseReplyPlaceholder: "Escribe tu respuesta…", @@ -326,21 +303,15 @@ module.exports = { fediverseReply: "Responder", fediverseBoosted: "ha impulsado", fediverseNoPosts: "Tu timeline está vacío.", - fediverseThread: "Hilo", fediverseTimeline: "Cronologías", - fediverseManageTimeline: "Gestionar cronología", - fediverseFollowers: "Seguidores", + fediverseFollowers: "Seguidoras", fediverseFollowing: "Siguiendo", fediversePosts: "Publicaciones", fediverseJoined: "Se unió", - fediverseOverview: "Resumen", fediverseRefresh: "Actualizar", fediverseWrite: "Escribir", fediversePreview: "Vista previa", - fediverseEdit: "Editar", - fediverseOpenFeed: "Visitar feed", fediverseManage: "Gestionar", - fediverseStatusNotConnected: "No conectado", fediverseError: "No se pudo contactar con Mastodon.", fediverseErrInstance: "URL de instancia no válida.", fediverseErrAuth: "Token no válido o caducado.", @@ -351,17 +322,76 @@ module.exports = { fediverseErrPost: "No se pudo publicar.", fediverseErrMedia: "No se pudo subir el archivo.", fediverseErrEmpty: "Escribe algo o adjunta un archivo.", - popularLabel: "Destacadas", - topicsLabel: "Tópicos", - latestLabel: "Últimas", - summariesLabel: "Resúmenes", - threadsLabel: "Hilos", - multiverseLabel: "Multiverso", - inboxLabel: "Buzón", - invitesLabel: "Invitaciones", - walletLabel: "Cartera", - legacyLabel: "Llaves", - cipherLabel: "Cifrador", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Número de teléfono", + telegramSendCode: "Enviar código", + telegramCodeHelp: "Introduce el código que Telegram te acaba de enviar (app o SMS).", + telegramCodeLabel: "Código", + telegramPasswordHelp: "Esta cuenta tiene verificación en dos pasos. Introduce tu contraseña.", + telegramPasswordLabel: "Contraseña", + telegramVerify: "Verificar", + telegramCancel: "Cancelar", + telegramChats: "Chats", + telegramUnread: "Sin leer", + telegramOpen: "Abrir", + telegramSend: "Enviar", + telegramYou: "Tú", + telegramNoChats: "Todavía no hay chats.", + telegramNoMessages: "Todavía no hay mensajes.", + telegramComposePlaceholder: "Escribe un mensaje…", + telegramChannel: "Canal", + telegramGroup: "Grupo", + telegramPrivate: "Privado", + telegramErrConnect: "No se pudo contactar con Telegram.", + telegramErrTimeout: "Telegram tardó demasiado en responder.", + telegramErrAuth: "Sesión de Telegram inválida o caducada. Conéctala de nuevo.", + telegramErrFetch: "No se pudieron cargar los chats.", + telegramErrSend: "No se pudo enviar el mensaje.", + telegramErrMissing: "Hacen falta API ID, API hash y número de teléfono.", + telegramErrCode: "Código incorrecto. Inténtalo de nuevo.", + telegramErrCodeExpired: "El código ha caducado. Empieza de nuevo.", + telegramErrPassword: "Contraseña incorrecta.", + telegramErrPhone: "Número de teléfono inválido.", + telegramErrApi: "API ID o API hash inválidos.", + telegramErrFlood: "Demasiados intentos. Espera un rato e inténtalo de nuevo.", + telegramErrNoLogin: "No hay ningún inicio de sesión en curso. Empieza de nuevo.", + peertubeUsernameLabel: "Usuaria", + peertubePasswordLabel: "Contraseña", + peertubeSubscriptions: "Suscripciones", + peertubeMyVideos: "Mis vídeos", + peertubeVideos: "Vídeos", + peertubeFollowers: "Seguidoras", + peertubeViews: "visualizaciones", + peertubeLikes: "me gusta", + peertubeQuota: "Cuota", + peertubeUpload: "Subir vídeo", + peertubeUploadHelp: "Elige un fichero de vídeo. Se sube a tu canal y PeerTube lo procesa después.", + peertubeTitleLabel: "Título", + peertubeDescriptionLabel: "Descripción", + peertubePrivacyLabel: "Privacidad", + peertubePrivacyPublic: "Público", + peertubePrivacyUnlisted: "No listado", + peertubePrivacyPrivate: "Privado", + peertubeNoVideos: "Todavía no hay vídeos.", + peertubeComments: "Comentarios", + peertubeComment: "Comentar", + peertubeCommentPlaceholder: "Escribe un comentario…", + peertubeNoComments: "Todavía no hay comentarios.", + peertubeOpen: "Abrir en PeerTube", + peertubeWatch: "Ver", + peertubeChannel: "Canal", + peertubeNoStream: "Este vídeo aún no se puede reproducir aquí. Ábrelo en PeerTube.", + peertubeUploaded: "Vídeo subido. PeerTube lo está procesando.", + peertubeErrConnect: "No se pudo contactar con PeerTube.", + peertubeErrAuth: "Usuaria o contraseña incorrectas, o la sesión caducó. Conéctala de nuevo.", + peertubeErrInstance: "URL de instancia no válida.", + peertubeErrMissing: "Hacen falta instancia, usuaria y contraseña.", + peertubeErrFetch: "No se pudieron cargar los vídeos.", + peertubeErrUpload: "No se pudo subir el vídeo.", + peertubeErrTitle: "Hace falta un título.", + peertubeErrComment: "No se pudo publicar el comentario.", + peertubeErrNoFile: "Elige un fichero de vídeo.", bookmarksLabel: "Marcadores", videosLabel: "Vídeos", torrentsLabel: "Torrents", @@ -372,69 +402,55 @@ module.exports = { inhabitantsLabel: "Habitantes", trendingLabel: "Tendencias", eventsLabel: "Eventos", - tasksLabel: "Tareas", apply: "Aplicar", menuPersonal: "Personal", - menuContent: "Contenido", - menuBlogs: "Blogs", menuGovernance: "Gobernanza", menuOffice: "Oficina", - menuMultiverse: "Multiverso", - menuNetwork: "Red", + menuNetwork: "Comunidad", menuCreative: "Creativo", menuEconomy: "Economía", - menuMedia: "Multimedia", + menuMedia: "Biblioteca", menuTools: "Herramientas", comment: "Comentar", subtopic: "Subtema", json: "JSON", - createdBy: "por", unfollow: "Dejar de soportar", follow: "Soportar", block: "Bloquear", unblock: "Desbloquear", - newerPosts: "Nuevos posts", - olderPosts: "Viejos posts", - feedRangeEmpty: "El rango aplicado está vación para éste feed. Trata de ver el ", - seeFullFeed: "feed completo", - feedEmpty: "La red Oasis no ha visto nunca posts desde ésta cuenta.", - beginningOfFeed: "Éste es el comienzo del feed", - noNewerPosts: "No se han recibido posts nuevos, aún.", relationshipNotFollowing: "No te da soporte", relationshipTheyFollow: "Te da soporte", - relationshipMutuals: "Soporte mútuo", + relationshipMutuals: "Soporte mutuo", relationshipFollowing: "Estás dando soporte", relationshipYou: "Tú", relationshipBlocking: "Estás bloqueando", relationshipBlockedBy: "Te bloquea", relationshipMutualBlock: "Bloqueo mutuo", relationshipNone: "No estás dando soporte", - relationshipConflict: "Conflicto", relationshipBlockingPost: "Post bloqueado", - viewLikes: "Ver replicas", - spreadedDescription: "Lista de posts replicados por habitante.", - totalspreads: "Replicas totales", - attachFiles: "Adjuntar ficheros", + viewLikes: "Ver réplicas", + viewSupporters: "Ver soportes", + spreadedDescription: "Lista de contenidos replicados por habitante.", preview: "Previsualizar", publish: "Publicar", contentWarningPlaceholder: "Añade un título a tu post (opcional)", - privateWarningPlaceholder: "Añade habitantes para enviar un post privado (opcional)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "Escribe el cuerpo de tu entrada.", + publishTooLong: "Tu publicación es demasiado larga. Acórtala, por favor.", + keepTextRetry: "Enviar de nuevo", + dateInPastError: "La fecha no puede ser anterior a hoy", publishCustomDescription: [ - "RECUERDA: Debido a la tecnología blockchain, una vez has publicado algo, no puede ser editado ni borrado. Piénsalo bien!.", + "RECUERDA: debido a la tecnología blockchain, una vez publicas algo, no se puede editar ni borrar. ¡Piénsalo bien!", ], commentWarning: [ - "RECUERDA: Debido a la tecnología blockchain, una vez has publicado algo, no puede ser editado ni borrado. Piénsalo bien!.", + "RECUERDA: debido a la tecnología blockchain, una vez publicas algo, no se puede editar ni borrar. ¡Piénsalo bien!", ], - commentPublic: "público", - commentPrivate: "privado", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ - "RECUERDA: Debido a la tecnología blockchain, una vez has publicado algo, no puede ser editado ni borrado. Piénsalo bien!.", + "RECUERDA: debido a la tecnología blockchain, una vez publicas algo, no se puede editar ni borrar. ¡Piénsalo bien!", ], replyLabel: ({ markdownUrl }) => [ - "RECUERDA: Debido a la tecnología blockchain, una vez has publicado algo, no puede ser editado ni borrado. Piénsalo bien!.", + "RECUERDA: debido a la tecnología blockchain, una vez publicas algo, no se puede editar ni borrar. ¡Piénsalo bien!", ], publishCustomInfo: ({ href }) => [ "Si tienes experiencia, también puedes ", @@ -447,14 +463,12 @@ module.exports = { ".", ], publishCustom: "Escribir un post avanzado", - subtopicLabel: "Crea un subtopic para éste post", messagePreview: "Previsualización", mentionsMatching: "Menciones", mentionsName: "Nombre", mentionsRelationship: "Relación", - updateit: "OBTENER ACTUALIZACIONES!", updateBannerText: "Hay una nueva versión de Oasis disponible.", - updateBannerAction: "Actualizar ahora →", + updateBannerAction: "¡Actualizar ahora!", info: "Info", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -462,46 +476,144 @@ module.exports = { timeAgo: "hace", sendTime: "escrito ", theme: "Tema", - legacy: "Llaves", - legacyTitle: "Llaves", - legacyDescription: "Maneja tu secreto (llave privada) de forma rápida y segura.", - legacyExportButton: "Exportar", - legacyImportButton: "Importar", + backupKeysExportButton: "Exportar", + backupKeysImportButton: "Importar", ssbLogStream: "Blockchain", ssbLogStreamDescription: "Configura el límite de mensajes para los flujos de la blockchain.", + logWindowChip: "Últimos {n} streams de la blockchain", saveSettings: "Guardar configuración", - exportTitle: "Exportar datos", - exportDescription: "Establece una contraseña (min 32 caracteres de longitud) para cifrar tu secreto", - exportDataTitle: "Copia de Seguridad", - exportDataDescription: "Descargar tu blockchain (llave privada excluida!)", + exportDescription: "Establece una contraseña (mín. 32 caracteres) para cifrar tu secreto", + exportDataTitle: "Copia instantánea", + exportDataDescription: "Descargar tu blockchain (¡llave privada excluida!)", exportDataButton: "Descargar blockchain", - pubWallet: "Cartera del PUB", - pubWalletDescription: "Establece la URL del wallet del PUB. Esta se utilizará para las transacciones del PUB (incluída la RBU).", - pubWalletConfiguration: "Guardar Configuración", - importTitle: "Importar datos", importDescription: "Importa tu secreto cifrado (llave privada) para activar tu avatar", - importAttach: "Adjuntar fichero cifrado (.enc)", - passwordLengthInfo: "La contraseña debe tener mínimo 32 caracteres de longitud.", passwordImport: "Escribe tu contraseña para descifrar los datos que serán guardados en tu sistema (nombre: secret)", exportPasswordPlaceholder: "Utiliza minúsculas, mayúsculas, números y símbolos", - fileInfo: "Tu secreto cifrado será guardado en tu sistema (nombre: oasis.enc)", + fileInfo: "Tu secreto cifrado se descargará en tu dispositivo (nombre: oasis.enc)", + developer: "Desarrollo", + devTitle: "Desarrollo", + welcomeBannerText: "Tu avatar aún no está configurado.", + aiSuggestionBanner: "Sugerencia:", + aiSuggestionsEnable: "Sugerencias de la IA", + welcomeBannerAction: "Empezar", + welcomeTitle: "Bienvenida", + welcomeStepIdentity: "Identidad", + welcomeStepType: "Tipo", + welcomeStepUse: "Uso", + welcomeStepLook: "Apariencia", + welcomeStepCommunity: "Comunidad", + welcomeStepIntro: "Bienvenida", + welcomeHintIdentity: "Tu nombre y tu imagen.", + welcomeHintType: "Cómo se abre Oasis para ti.", + welcomeHintUse: "Qué módulos quedan activos.", + welcomeHintLook: "El tema que viste Oasis.", + welcomeHintCommunity: "Entra en un PUB para conocer a otros habitantes.", + welcomeHintIntro: "Envía un primer saludo a la comunidad.", + welcomeStepThemeAction: "Usar este tema", + welcomeJoinPubs: "Entrar en los PUBs", + welcomeNoFederation: "Sin federarse", + welcomeStepGreetingAction: "Enviar feed", + welcomeDescription: "Unas pocas cosas que conviene hacer antes de empezar.", + welcomeSkip: "Ahora no", + welcomeStepProfileAction: "Guardar perfil", + welcomeBootstrapRunning: "Cargando la instantánea del pub. Al terminar se une al pub automáticamente; mientras tanto puedes seguir usando Oasis.", + welcomeCompleteTitle: "Todo listo.", + welcomeCompleteText: "Tu nodo está preparado. A partir de aquí la red crece con los habitantes a los que apoyas.", + welcomeFindPeople: "Buscar habitantes", + devFilterFiles: "FICHEROS", + devFilterSearch: "BUSCAR", + devFilterModules: "MÓDULOS", + devFilterTranslations: "TRADUCCIONES", + devFilterStyles: "ESTILOS", + devFilterTests: "TESTS", + devVersion: "Versión", + devNodeVersion: "Node", + devStatsTests: "Tests", + devParentFolder: "Carpeta superior", + devOpenFolder: "abrir", + devDescription: "Explora el código que se está ejecutando en este nodo.", + devRoot: "raíz", + devSearchPlaceholder: "Texto a buscar en el código", + devAnyExtension: "Cualquier fichero", + devStatsFiles: "Ficheros", + devStatsLines: "Líneas", + devStatsModules: "Módulos", + devStatsLanguages: "Idiomas", + devNotViewable: "no visualizable", + devEmptyFolder: "Esta carpeta está vacía.", + devSize: "Tamaño", + devShowing: "Mostrando", + devLine: "línea", + devReportBug: "Informar de un error", + devCreateTask: "Crear Tarea", + devRaw: "RAW", + devReportContext: "Encontrado leyendo el código de Oasis", + devTaskPrefix: "Revisar", + devPrevPage: "Líneas anteriores", + devNextPage: "Líneas siguientes", + devMatches: "coincidencias", + devFilesScanned: "ficheros revisados", + devNoResults: "Todavía no hay coincidencias.", + devColModule: "Módulo", + devColState: "Estado", + devColModel: "Modelo", + devColView: "Vista", + devColRoutes: "Rutas", + devColTests: "Tests", + devOn: "activo", + devOff: "inactivo", + modulesDevLabel: "Desarrollo", + modulesDevDescription: "Módulo para gestionar el código de Oasis.", themeIntro: "Elige un tema.", setTheme: "Establecer tema", - language: "Lenguaje", + language: "Idioma", languageDescription: - "Si quieres utilizar otro idioma, seleccionalo aquí.", - setLanguage: "Establecer lenguaje", - peerConnections: "Nodos", - peerConnectionsIntro: "Maneja todas tus conexiones con otros nodos.", + "Si quieres utilizar otro idioma, selecciónalo aquí.", + setLanguage: "Establecer idioma", + peerConnectionsIntro: "Gestiona todas tus conexiones con otros nodos.", + peersConnectFailed: "No se pudo conectar: {reason}.", + peerStateLabel: "Estado", + peerAddressLabel: "Dirección", + peerLastChangeLabel: "Último cambio", + peerStateConnected: "Conectado", + peerStateConnecting: "Conectando", + peerStateStaged: "Disponible", + peerErrSilent: "Sin actividad", + peerErrTimeout: "No responde", + peerErrRefused: "Rechaza la conexión", + peerErrNotFound: "Dirección desconocida", + peerErrUnreachable: "Red inaccesible", + peerErrKeys: "Usando llaves diferentes", + peerErrIdentity: "No acepta tu conexión", + peerErrTor: "Solo TOR", + peerErrDropped: "Conexión cortada", + peerErrPaused: "Red en pausa", + peerErrOther: "Error de conexión", + peerOutdated: "Desactualizado", + peerVersionUnannounced: "No anuncia su versión: un Oasis antiguo", + peersTorMissing: "Este PUB solamente permite conexiones desde TOR.", peerConnectionsTitle: "Conexiones", + peersOnlineHint: "Nodos conectados contigo ahora mismo.", + peersDiscoveredHint: "Nodos con dirección conocida que no están conectados ahora.", + peersUnknownHint: "Nodos de tu grafo social sin dirección conocida.", + peersConnectionsHint: "Direcciones a las que puede conectar este nodo y el estado de cada conexión.", + peersTabLan: "LAN", + peersTabHealth: "Salud", + peersHealthHint: "Cómo está cada pub o nodo conocido, en la red local y fuera de ella.", + peersHealthReachable: "Alcanzable", + peersHealthFailing: "Fallando", + peersHealthFailures: "Fallos", + peersFailingSince: "Falla desde", + peersHealthForget: "Olvidar inalcanzables", + peersLanHint: "Nodos encontrados en tu red local.", + peersLanLastSeen: "Visto por última vez", + peersLanEmpty: "Aún no se ha encontrado ningún nodo en la red local.", online: "Conectado", offline: "Desconectado", discovered: 'Descubiertos', unknown: 'Desconocidos', - lanBroadcastLabel: "Difusión LAN", lanBroadcastActive: "Activa (multicast UDP en la LAN)", - lanBroadcastDisabled: "Desactivada (modo pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -514,188 +626,129 @@ module.exports = { noConnections: "No hay nodos conectados.", noDiscovered: "No has descubierto nodos.", noUnknownPeers: "Sin pares desconocidos en el grafo de amistades.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "Aún no hay nodos registrados.", + peerConnect: "Conectar", + peerDisconnect: "Desconectar", peerRefresh: "Refrescar", peerPruneIdle: "Quitar inactivos", + peersPause: "Pausar", + peersResume: "Reanudar", + peersPausedNote: "En pausa: Oasis no se conecta con nadie y nadie puede conectarse a él.", + peersPauseHint: "Detiene todas las conexiones: Oasis no se conecta a otros nodos ni los acepta hasta que reanudes.", peerExport: "Exportar", peerImport: "Importar", peerImportTitle: "Importar lista de peers", peerImportPlaceholder: "Pega una dirección multiserver por línea, o sube un archivo .txt…", - noSupportedConnections: "No soportas nodos.", - noBlockedConnections: "No bloqueas nodos.", - noRecommendedConnections: "No recomiendas nodos.", connectionActionIntro: "", - startNetworking: "Iniciar red", - stopNetworking: "Parar red", - restartNetworking: "Reiniciar red", - sync: "Sincronizar red", indexes: "Índices", + indexesRebuilt: "Índices reconstruidos", + indexesRebuildFailed: "No se han podido reconstruir los índices", + indexesFiles: "Ficheros de índice", indexesDescription: "Reconstruir índices es seguro, y puede arreglar algunos errores en tu blockchain.", homePageTitle: "Página de inicio", homePageDescription: "Selecciona qué módulo quieres como tu página de inicio.", saveHomePage: "Establecer página de inicio", invites: "Invitaciones", - invitesTitle: "Invitaciones", - invitesInvites: "Invitaciones", - invitesDescription: "Maneja y aplica códigos de invitación en tu red.", + invitesDescription: "Gestiona y aplica códigos de invitación en tu red.", + invitesPubsHint: "Pega un código de invitación de un pub para federarte con él y empezar a replicar a sus habitantes.", + invitesFederationsHint: "Pubs con los que estás federado: actualízalos, quita los inalcanzables o exporta la lista.", invitesTribesTitle: "Tribus", - invitesTribeInviteCodePlaceholder: "Introduce el código de la tribu", - invitesTribeJoinButton: "Entrar en la tribu", invitesChatsTitle: "Chats", - invitesChatInviteCodePlaceholder: "Introduce el código del chat", - invitesChatJoinButton: "Entrar en el chat", invitesPadsTitle: "Pads", - invitesPadInviteCodePlaceholder: "Introduce el código del pad", - invitesPadJoinButton: "Entrar en el pad", invitesCalendarsTitle: "Calendarios", - invitesCalendarInviteCodePlaceholder: "Introduce el código del calendario", - invitesCalendarJoinButton: "Entrar en el calendario", invitesEventsTitle: "Eventos", - invitesEventInviteCodePlaceholder: "Introduce el código del evento", - invitesEventJoinButton: "Entrar en el evento", invitesForumsTitle: "Foros", - invitesForumInviteCodePlaceholder: "Introduce el código del foro", - invitesForumJoinButton: "Entrar en el foro", invitesMapsTitle: "Mapas", - invitesMapInviteCodePlaceholder: "Introduce el código del mapa", - invitesMapJoinButton: "Entrar en el mapa", invitesShopsTitle: "Tiendas", - invitesShopInviteCodePlaceholder: "Introduce el código de invitación de la tienda", - invitesShopJoinButton: "Unirse a la tienda", invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "PUBs Sugeridos", + invitesFilterOthers: "Otras", + invitesJoinTitle: "Invitaciones", + invitesJoinHint: "Pega cualquier código de invitación, sea de lo que sea, o el Oasis ID de un habitante.", + invitesJoinPlaceholder: "Código de invitación u Oasis ID", + invitesJoinButton: "Entrar", + invitesUsedTitle: "Últimas invitaciones", + invitesUsedEmpty: "Aún no has utilizado ninguna invitación.", + invitesUsedType: "Tipo", + invitesUsedName: "Nombre", + invitesUsedDate: "Fecha", invitesPubInviteCodePlaceholder: "Introduce el código de invitación del PUB", invitesAcceptInvite: "Entrar en el PUB", + invitesPubUses: "Invitaciones usadas", + invitesPubExhausted: "Este PUB no tiene invitaciones disponibles.", invitesAlreadyFederated: "Ya estás federado con este pub.", + invitesJoinPubMode: "Este Oasis funciona como PUB, en modo de solo lectura. Para unirlo a otro PUB, usa en la terminal: ./oasis.sh join ", + invitesJoinLocalOnly: "Las invitaciones solo se aceptan desde el equipo donde funciona Oasis, o con el enlace de administración que muestra al arrancar.", invitesFederationsTitle: "Federaciones", invitesAcceptedInvites: "Federadas", - invitesNoInvites: "No hay invitaciones aceptadas, aún.", invitesNoFederatedPubs: "No hay pubs federados.", - invitesUnreachablePubs: "Inalcanzables", - invitesNoUnreachablePubs: "No hay pubs inalcanzables.", invitesPubsRefresh: "Refrescar", - invitesPubsClearUnreachable: "Quitar inalcanzables", + invitesPubsClearUnreachable: "Limpiar", invitesPubsExport: "Exportar", invitesPubsImport: "Importar", invitesPubsImportTitle: "Importar pubs", invitesPubsImportPlaceholder: "Pega una dirección multiserver o código de invitación por línea, o sube un archivo .txt…", - currentlyUnreachable: "¡ERROR!", - errorDetails: "Detalles del error", - genericError: "A ocurrido un error.", - panicMode: "Modo Pánico!", - encryptData: "Establece una contraseña (min 32 caracteres de longitud) para cifrar tu blockchain", + genericError: "Ha ocurrido un error.", + panicMode: "¡Modo pánico!", + encryptData: "Establece una contraseña (mín. 32 caracteres) para cifrar tu blockchain", decryptData: "Introduce contraseña para descifrar tu blockchain", - panicModeDescription: "Cifrar/Descifrar o BORRAR tu blockchain", - removeDataDescription: "CUIDADO: Éste proceso no puede ser revocado.", - encryptPanicButton: "Cifrar blockchain", - decryptPanicButton: "Descifrar blockchain", - removePanicButton: "BORRAR TODOS TUS DATOS!", + removeDataDescription: "CUIDADO: Este proceso no se puede deshacer.", + removePanicButton: "¡BORRAR TODOS TUS DATOS!", searchTitle: "Buscar", searchDescriptionLabel: "Buscar contenido en tu red.", - searchLanguagesLabel: "Lenguajes", - searchSkillsLabel: "Habilidades", searchPlaceholder:"Buscar contenido...", - searchDateLabel:"Fecha", searchLocationLabel:"Localización", searchPriceLabel:"Precio", - searchUrlLabel:"URL", searchCategoryLabel:"Categoría", - searchStartLabel:"Fecha de comienzo", - searchEndLabel:"Fecha de fin", searchPriorityLabel:"Prioridad", searchStatusLabel:"Estado", statusLabel:"Estado", - totalVotesLabel:"Votos Totales", noResultsFound:"No se han encontrado resultados.", votesOption:"Opiniones Votadas", - voteYesLabel:"Si", + voteYesLabel:"Sí", voteNoLabel:"No", allTypesLabel: "SIN LÍMITES", - ABSTENTIONLabel:"ABSTENCIÓN", - YESLabel:"SI", + YESLabel:"SÍ", NOLabel:"NO", - FOLLOW_MAJORITYLabel: "SEGUIR MAYORIA", - CONFUSEDLabel: "CONFUSO", - NOT_INTERESTEDLabel: "SIN INTERÉS", - StatusLabel:"Estado", - votesOptionYesLabel:"Si", + votesOptionYesLabel:"Sí", votesOptionNoLabel:"No", votesOptionAbstentionLabel:"Abstención", votesQuestionLabel:"Pregunta", - votesCreatedByLabel:"Creado el", voteStatusOpen:"ABIERTO", voteStatusClosed:"CERRADO", - imageSearchLabel: "Introduce palabras para buscar las imagenes que hayan sido etiquetadas con ellas.", commentDescription: ({ parentUrl }) => [ " ha comentado en ", a({ href: parentUrl }, " hilo"), ], commentTitle: ({ authorName }) => [`Ha comentado en el post de @${authorName}`], subtopicDescription: ({ parentUrl }) => [ - " creado un subtopic desde ", + " ha creado un subtema desde ", a({ href: parentUrl }, " un post"), ], - subtopicTitle: ({ authorName }) => [`Ha creado un subtopic en el post de @${authorName}`], - mysteryDescription: "publicado un post misterioso", + subtopicTitle: ({ authorName }) => [`Ha creado un subtema en el post de @${authorName}`], oasisDescription: "OASIS Red de Proyectos", searchSubmit: "Buscar...", - postLabel: "POSTS", - aboutLabel: "HABITANTES", - feedLabel: "FEEDS", votesLabel: "GOBIERNO", - reportLabel: "REPORTES", - imageLabel: "IMAGENES", - videoLabel: "VIDEOS", - audioLabel: "AUDIOS", - documentLabel: "DOCUMENTOS", - torrentLabel: "TORRENTS", pdfFallbackLabel: "Documento PDF", - eventLabel: "EVENTOS", - taskLabel: "TAREAS", - transferLabel: "TRANSFERENCIAS", - curriculumLabel: "CURRÍCULUM", - bookmarkLabel: "ENLACES", - tribeLabel: "TRIBUS", - marketLabel: "MERCADO", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "CARTERAS", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + shopLabel: "TIENDAS", + voteLabel: "VOTOS", submit: "Aplicar", - subjectLabel: "Asunto", editProfile: "Editar Avatar", - profileQrAlt: "Código QR del perfil", - profileQrHint: "Escanea para copiar este Oasis ID.", oasisIdLabel: "OasisID", - spreadLabel: "Difundir", - spreadHint: "Difunde esto a quienes te apoyan (se replica vía tu feed).", + spreadContent: "Replicar", welcomePmSubject: "Hello World!", - welcomePmBody: "Bienvenido a Oasis — una red social libre, peer-to-peer, federada y cifrada, con una arquitectura distribuida y solo por invitación.\n\nAlgunos lugares interesantes para empezar:\n\n- /profile — Edita tu avatar, nombre, descripción y qué módulos se muestran en tu cara pública.\n- /invites — Añade un pub para federarte con el resto de la red.\n- /peers — Mira quién está conectado, reescanea tu LAN, exporta o importa listas de peers.\n- /inhabitants — Descubre y visita los avatares de otras personas.\n- /tribes — Crea o únete a grupos privados con cifrado de extremo a extremo.\n- /inbox — Lee y envía mensajes privados.\n- /activity — Mira la actividad reciente, tuya y de tu red.\n- /publish — Escribe un post o comparte una imagen, audio, vídeo o documento.\n\nAlgunos lugares interesantes para conectar:\n\n- /fediverse — Gestiona tus otras cuentas del fediverso, incluyendo enviar y recibir contenido.\n\nEste mensaje se envió de forma privada desde ti hacia ti, por eso no hizo falta conexión para recibirlo.", - profileVisibilityTitle: "Visibilidad del perfil público", - profileVisibilityHint: "Marca qué campos ven otros habitantes en tu perfil. Por defecto solo se muestra Karma.", + welcomePmBody: "Te damos la bienvenida a Oasis — una red social libre, peer-to-peer, federada y cifrada, con una arquitectura distribuida y solo por invitación.\n\nAlgunos lugares interesantes para empezar:\n\n- [/avatar](/profile) — Edita tu avatar, nombre, descripción y qué módulos se muestran en tu avatar público.\n- /settings — Ajusta tu instancia local: idioma, tema, módulos, privacidad y sincronización.\n- /invites — Añade un pub para federarte con el resto de la red.\n- /peers — Mira quién está conectado, reescanea tu LAN, exporta o importa listas de peers.\n- /inhabitants — Descubre y visita los avatares de otros habitantes.\n- /tribes — Crea o únete a grupos privados con cifrado de extremo a extremo.\n- /search — Busca el contenido de la red por tipo.\n\nAlgunos lugares interesantes para conectar:\n\n- /multiverse — Gestiona tus otras cuentas del multiverso, incluyendo enviar y recibir contenido.\n\nEste mensaje se envió de forma privada desde ti hacia ti, por eso no hizo falta conexión para recibirlo.", profileSensorsSectionTitle: "Sensores", profileVisibilityActivity: "Nivel de actividad", - profileVisibilityDevice: "Dispositivo", - profileDeviceLockedHint: "Este sensor está siempre activo: permite a otros ver desde qué dispositivo te conectas y no se puede desactivar.", + profileDeviceLockedHint: "Este sensor está siempre activo: permite a otros habitantes ver desde qué dispositivo te conectas y no se puede desactivar.", profileVisibilityKarma: "Puntuación KARMA", - profileVisibilityUbi: "UBI", - profileVisibilityWallet: "Cartera ECOIN", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityUbi: "RBU", + profileVisibilityWallet: "Cartera ECOin", + profileVisibilityEcoTax: "Tasa ECO", + profileVisibilityLarpSign: "Sello L.A.R.P.", profileVisibilityGpg: "Llave GPG", - profileVisibilityClearnet: "Publicar mi perfil", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Tiendas", profileClearnetJobsLabel: "Empleo", @@ -707,17 +760,21 @@ module.exports = { profileClearnetImagesLabel: "Imágenes", profileClearnetDocumentsLabel: "Documentos", profileClearnetTorrentsLabel: "Torrents", + profileClearnetPodcastsLabel: "Podcasts", + profileClearnetMarketLabel: "Mercado", + profileClearnetFeedLabel: "Feed", + profileClearnetWikiLabel: "Wikis", editProfileDescription: "", profileName: "Nombre", - profileImage: "Imágen Avatar", + profileImage: "Imagen del avatar", profileGpgKey: "Clave Pública GPG (.asc)", profileGpgChip: "GPG", profileGpgDownload: "Descargar", profileGpgRemove: "Eliminar", profileDescription: "Descripción", hashtagDescription: - "Posts de habitantes de tu red que referencian éste #hashtag, ordenados por el más reciente.", + "Posts de habitantes de tu red que mencionan este #hashtag, ordenados por el más reciente.", rebuildName: "Reconstruir blockchain", wallet: "Cartera", walletAddress: "Dirección", @@ -732,7 +789,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "Conf.", walletConfirm: "Confirmar", - walletDescription: "Administra tus activos digitales, incluido el envío y la recepción de ECOin, la visualización del saldo y el acceso al historial de transacciones.", + walletDescription: "Gestiona tu cartera ECOin.", walletDate: "Fecha", walletFee: "Tasa (Cuanto mayor sea la tasa, más rápido se procesará tu transacción)", walletFeeLine: ({ fee }) => `Tasa: ECO ${fee}`, @@ -740,6 +797,86 @@ module.exports = { walletReceive: "Recibir", walletReset: "Resetear", walletSend: "Enviar", + walletPayingTransfer: "Pagando transfer", + walletCreateTransferLabel: "Generar contrato (transfer)", + walletTransferNotifiedNote: "Se ha avisado del pago a la otra parte", + walletTransferCreatedNote: "Se ha creado un transfer (recibo) de este pago", + walletTransferNoPayeeNote: "Ningún habitante ha publicado esta dirección, así que no se creó transfer", + walletTransferReceiptConcept: "Pago desde la cartera", + walletPaymentPmText: "te ha enviado un pago", + walletPayeeLabel: "Destinataria", + walletConceptLabel: "Concepto", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Has recibido tu RBU.", + bankingBotPaymentTitle: "Has recibido un pago.", + bankingBotUbiPaidText: "Tu Renta Básica Universal ha sido pagada", + bankUbiPubThisNode: "Este nodo paga la RBU", + bankUbiPubConnected: "PUB de la RBU", + bankUbiPubNone: "No hay PUBs anunciando una RBU, de momento.", + bankUbiAddressPublished: "Dirección ECOin publicada", + bankUbiAddressMissing: "Publica tu dirección ECOin para recibir la RBU", + bankUbiClaimedThisMonth: "Reclamada este mes", + bankUbiPubBalance: "Cartera del PUB disponible para la RBU", + bankNoPubAddress: "El PUB de la RBU aún no ha publicado una dirección ECOin.", + bankUbiTab: "RBU", + bankDonateEco: "¡DONAR!", + donateTooltip: "Donar ECOin", + bankNoUserAddress: "Este habitante no comparte una dirección ECOin.", + bankUbiPubDefault: "Por defecto", + bankUbiPool: "Asignación", + bankUbiLastSeen: "Visto por última vez", + bankUbiLastPayout: "Última acción", + bankUbiPaidOut: "Repartido", + bankRangeToday: "Hoy", + bankRangeWeek: "Semana", + bankRangeMonth: "Mes", + bankRangeYear: "Año", + bankRangeFiveYears: "5 años", + bankRangeAll: "Todo", + bankChartValueTitle: "Evolución del valor de ECOin", + bankChartValueHours: "Equivalencia en tiempo (h)", + bankChartWealthTitle: "Redistribución de riqueza ECOin", + bankChartDistributed: "RBU repartida (ECO)", + bankChartRebalanceTitle: "Redistribución de riqueza entre PUBs", + bankChartRebalanced: "ECO movidos entre PUBs", + bankChartTaxes: "Impuestos retenidos (ECO)", + bankChartNoDataRange: "Sin datos en este rango.", + bankChartNoDataYet: "Todavía no hay datos", + bankChartPerHour: "por hora", + bankChartPerDay: "por día", + bankChartPerMonth: "por mes", + bankTotalUbiDistributed: "RBU total redistribuida (red)", + bankTotalTaxesWithheld: "Impuestos totales retenidos (red)", + bankInflationIssuance: "Tasa de emisión (anual, por recompensa de bloque)", + bankRulesEligibilityTitle: "Quién puede recibir la RBU", + bankRulesEligibility1: "Una dirección ECOin publicada en tu perfil (Perfil → Sensores → Cartera ECOin).", + bankRulesEligibility2: "Un feed con más de 30 días y actividad en la red: karma por encima de cero.", + bankRulesEligibility3: "Un reclamo por mes (epoch), desde Banking con el botón Reclamar RBU. Se puede rechazar la del mes en curso con Rechazar RBU.", + bankRulesEligibility4: "El suelo de 1 ECO se paga siempre; los impuestos solo reducen la parte que lo supera y nunca se cobran aparte.", + bankRulesEligibility5: "El PUB paga en su siguiente ciclo y publica un transfer RBU que puedes confirmar.", + bankRulesEligibility6: "Si falta tu dirección, el feed es demasiado joven, rechazaste el mes o el PUB no tiene fondos, el reclamo se salta y queda registrado en el PUB.", + copyLinkTitle: "Copiar enlace", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "Rechazar RBU", + bankUbiRefusedThisMonth: "Has rechazado la RBU de este mes.", + bankingBotUbiAvailableTitle: "Puedes reclamar tu RBU.", + bankingBotUbiAvailableText: "Tu RBU de este mes está disponible", + bankingBotConfirmTitle: "Tienes un contrato pendiente de confirmación", + bankingBotConfirmText: "Contratos pendientes de confirmación", + bankChartZoomHint: "Clic para ampliar", + bankChartPoolTitle: "Asignación de la RBU (en el tiempo)", + bankChartNetworkUbiTitle: "RBU total redistribuida (red)", + bankChartSupplyTitle: "Oferta circulante de ECOin (en el tiempo)", + bankChartInflationTitle: "Inflación de ECOin (en el tiempo)", + bankChartInflationSeries: "Tasa anualizada (%)", + bankChartPool: "Asignación total (ECO)", + bankChartKarmaTitle: "KARMA Scoring (en el tiempo)", + bankChartFundsTitle: "Tus fondos ECOin (en el tiempo)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin conectada", + bankWalletNotConfigured: "ECOin desconectada", + transfersPayWithWallet: "¡PAGAR!", walletStatus: "Estado", walletDisconnected: [ "Tienes la ", @@ -749,9 +886,11 @@ module.exports = { " o el estado de tu conexión.", ], walletSentToLine: ({ destination, amount }) => `Enviar ECO ${amount} a ${destination}`, - walletSettingsTitle: "Cartera", + walletNotConfigured: "Tu cartera de ECOin aún no está configurada.", walletSettingsDescription: "Integrar Oasis con tu cartera de ECOin.", - walletSettingsDocLink: "Guía de instalación de ECOin", + walletSettingsMobileDescription: "Vincula Oasis con la app ECOin Wallet de este dispositivo.", + walletDownloadEcoin: "¡Descargar ECOin!", + walletRpcUrl: "URL del RPC", walletStatusMessages: { invalid_amount: "Cantidad inválida", invalid_dest: "Dirección de destino inválida", @@ -767,41 +906,24 @@ module.exports = { walletUser: "Usuario", walletPass: "Contraseña", walletConfiguration: "Configurar cartera", + walletDisconnectButton: "Desconectar cartera", cipher: "Cifrador", cipherTitle: "Cifrador", - cipherDescription: "Encripta y desencripta tu texto de forma simétrica (usando una contraseña compartida).", + cipherDescription: "Cifra y descifra tu texto de forma simétrica (usando una contraseña compartida).", randomPassword: "Contraseña Aleatoria", - cipherEncryptTitle: "Encriptar Texto", - cipherEncryptDescription: "Introduce el texto para encriptar", - cipherTextLabel: "Texto para Encriptar", - cipherTextPlaceholder: "Introduce el texto para encriptar...", - cipherPasswordLabel: "Establece una contraseña (mínimo 32 caracteres) para encriptar tu texto", - cipherPasswordDecryptLabel: "Establece una contraseña (mínimo 32 caracteres) para desencriptar tu texto", + cipherTextPlaceholder: "Introduce el texto para cifrar...", + cipherPasswordLabel: "Establece una contraseña (mínimo 32 caracteres) para cifrar tu texto", + cipherPasswordDecryptLabel: "Establece una contraseña (mínimo 32 caracteres) para descifrar tu texto", cipherPasswordPlaceholder: "Introduce una contraseña...", - cipherEncryptButton: "Encriptar", - cipherDecryptTitle: "Desencriptar Texto", - cipherDecryptDescription: "Introduce el texto para desencriptar", - cipherEncryptedMessageLabel: "Texto Encriptado", - cipherDecryptedMessageLabel: "Texto Desencriptado", - cipherPasswordUsedLabel: "Contraseña usada para encriptar (guárdala!)", - cipherEncryptedTextPlaceholder: "Introduce el texto encriptado...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "Introduce el vector de inicialización...", - cipherDecryptButton: "Desencriptar", + cipherEncryptButton: "Cifrar", + cipherEncryptedMessageLabel: "Texto cifrado", + cipherDecryptedMessageLabel: "Texto descifrado", + cipherEncryptedTextPlaceholder: "Introduce el texto cifrado...", + cipherDecryptButton: "Descifrar", password: "Contraseña", text: "Texto", - encryptedText: "Texto Encriptado", + encryptedText: "Texto cifrado", iv: "Vector de Inicialización (IV)", - encryptTitle: "Encripta tu texto", - encryptDescription: "Introduce el texto que deseas encriptar y proporciona una contraseña.", - encryptButton: "Encriptar", - decryptTitle: "Desencripta tu texto", - decryptDescription: "Introduce el texto encriptado y proporciona la misma contraseña utilizada para la encriptación.", - decryptButton: "Desencriptar", - passwordLengthError: "La contraseña debe tener al menos 32 caracteres.", - missingFieldsError: "Texto, contraseña o IV no proporcionados.", - encryptionError: "Error al encriptar el texto.", - decryptionError: "Error al desencriptar el texto.", bookmarkTitle: "Marcadores", bookmarkDescription: "Descubre y gestiona marcadores en tu red.", bookmarkAllSectionTitle: "Marcadores", @@ -818,19 +940,16 @@ module.exports = { bookmarkFilterRecent: "RECIENTES", bookmarkCreateButton: "Crear marcador", bookmarkUpdateButton: "Actualizar", - bookmarkDeleteButton: "Eliminar", bookmarkAddFavoriteButton: "Añadir favorito", bookmarkRemoveFavoriteButton: "Quitar favorito", bookmarkUrlLabel: "Enlace", - bookmarkUrlPlaceholder: "https://ejemplo.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Descripción", bookmarkDescriptionPlaceholder: "Opcional", bookmarkTagsLabel: "Etiquetas", - bookmarkTagsPlaceholder: "Introduce tags separadas por comas", - bookmarkCategoryLabel: "Categoría", - bookmarkCategoryPlaceholder: "Opcional", + bookmarkTagsPlaceholder: "Introduce etiquetas separadas por comas", bookmarkLastVisitLabel: "Última visita", - bookmarkSearchPlaceholder: "Buscar URL, tags, categoría, autor...", + bookmarkSearchPlaceholder: "Buscar en Marcadores...", bookmarkSortRecent: "Más recientes", bookmarkSortOldest: "Más antiguos", bookmarkSortTop: "Más votados", @@ -843,8 +962,6 @@ module.exports = { noLastVisit: "Sin última visita", videoTitle: "Vídeos", videoDescription: "Explora y gestiona contenido de vídeo en tu red.", - videoPluginTitle: "Título", - videoPluginDescription: "Descripción", videoMineSectionTitle: "Tus vídeos", videoCreateSectionTitle: "Subir vídeo", videoUpdateSectionTitle: "Actualizar vídeo", @@ -871,12 +988,11 @@ module.exports = { videoDescriptionPlaceholder: "Opcional", videoNoFile: "No se ha proporcionado ningún archivo de vídeo", noVideos: "No hay vídeos disponibles.", - videoSearchPlaceholder: "Buscar por título, etiquetas, autor...", + videoSearchPlaceholder: "Buscar en Vídeos...", videoSortRecent: "Más recientes", videoSortOldest: "Más antiguos", videoSortTop: "Más votados", videoSearchButton: "Buscar", - videoMessageAuthorButton: "MP", videoUpdatedAt: "Actualizado", videoNoMatch: "Ningún vídeo coincide con tu búsqueda.", documentTitle: "Documentos", @@ -895,7 +1011,6 @@ module.exports = { documentFilterFavorites: "FAVORITOS", documentCreateButton: "Subir documento", documentUpdateButton: "Actualizar", - documentDeleteButton: "Eliminar", documentAddFavoriteButton: "Añadir a favoritos", documentRemoveFavoriteButton: "Quitar de favoritos", documentMessageAuthorButton: "MP", @@ -908,7 +1023,7 @@ module.exports = { documentDescriptionPlaceholder: "Opcional", documentNoFile: "Sin archivo.", noDocuments: "No hay documentos disponibles.", - documentSearchPlaceholder: "Buscar título, etiquetas, descripción, autor...", + documentSearchPlaceholder: "Buscar en Documentos...", documentSortRecent: "Más recientes", documentSortOldest: "Más antiguos", documentSortTop: "Más votados", @@ -917,8 +1032,6 @@ module.exports = { documentUpdatedAt: "Actualizado", audioTitle: "Audios", audioDescription: "Explora y gestiona contenido de audio en tu red.", - audioPluginTitle: "Título", - audioPluginDescription: "Descripción", audioMineSectionTitle: "Tus audios", audioCreateSectionTitle: "Subir audio", audioUpdateSectionTitle: "Actualizar audio", @@ -929,22 +1042,21 @@ module.exports = { audioFilterMine: "MÍOS", audioFilterRecent: "RECIENTES", audioFilterTop: "TOP", - audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Subir audio", audioUpdateButton: "Actualizar", - audioDeleteButton: "Eliminar", audioAddFavoriteButton: "Añadir a favoritos", audioRemoveFavoriteButton: "Quitar de favoritos", audioFileLabel: "Selecciona un archivo de audio (.mp3, .wav, .ogg)", audioTagsLabel: "Etiquetas", audioTagsPlaceholder: "Introduce etiquetas separadas por comas", + tagsPlaceholder: "Introduce etiquetas separadas por comas", audioTitleLabel: "Título", audioTitlePlaceholder: "Opcional", audioDescriptionLabel: "Descripción", audioDescriptionPlaceholder: "Opcional", audioNoFile: "No se ha proporcionado ningún archivo de audio", noAudios: "No hay audios disponibles.", - audioSearchPlaceholder: "Buscar por título, etiquetas, autor...", + audioSearchPlaceholder: "Buscar en Audios...", audioSortRecent: "Más recientes", audioSortOldest: "Más antiguos", audioSortTop: "Más votados", @@ -956,6 +1068,7 @@ module.exports = { audioFilterFavorites: "FAVORITOS", favoritesTitle: "Favoritos", favoritesDescription: "Todos tus elementos marcados como favoritos en un solo lugar.", + favoritesSearchPlaceholder: "Buscar en Favoritos...", favoritesFilterAll: "TODOS", favoritesFilterRecent: "RECIENTES", favoritesFilterAudios: "AUDIOS", @@ -967,44 +1080,38 @@ module.exports = { favoritesFilterChats: "CHATS", favoritesFilterCalendars: "CALENDARIOS", favoritesFilterVideos: "VÍDEOS", - favoritesRemoveButton: "Quitar de favoritos", favoritesNoItems: "Todavía no hay favoritos.", yourContacts: "Tus Contactos", allInhabitants: "Habitantes", - allCVs: "Todos los CVs", + allCVs: "CVs", discoverPeople: "Descubre habitantes en tu red.", - allInhabitantsButton: "TODOS", - contactsButton: "SOPORTES", - CVsButton: "CVs", - matchSkills: "Coincidir Habilidades", - matchSkillsButton: "COINCIDIR HABILIDADES", - suggestedButton: "SUGERIDOS", - searchInhabitantsPlaceholder: "FILTRAR habitantes POR NOMBRE …", + searchInhabitantsPlaceholder: "Buscar en Habitantes...", filterLocation: "FILTRAR habitantes POR UBICACIÓN …", filterLanguage: "FILTRAR habitantes POR IDIOMA …", filterSkills: "FILTRAR habitantes POR HABILIDADES …", + cvSkillsCount: "Habilidades", applyFilters: "Aplicar Filtros", locationLabel: "Ubicación", languagesLabel: "Idiomas", skillsLabel: "Habilidades", commonSkills: "Habilidades Comunes", - mutualFollowers: "Seguidores Mutuos", + mutualFollowers: "Seguidoras Mutuas", suggestedFollowsYou: "Te sigue", latestInteractions: "Últimas Interacciones", - viewAvatar: "Ver Avatar", - viewCV: "Ver CV", suggestedSectionTitle: "Sugeridos", topkarmaSectionTitle: "Mejor Karma", topecoSectionTitle: "Mejor Eco", topactivitySectionTitle: "Top Actividad", - blockedSectionTitle: "Bloqueados", + "TOP INACTIVITYButton": "MÁS INACTIVOS", + topinactivitySectionTitle: "Habitantes menos activos", + blockedSectionTitle: "Bloqueadas", gallerySectionTitle: "GALERÍA", - blockedButton: "BLOQUEADOS", - blockedLabel: "Usuario Bloqueado", + topSectionTitle: "TOP", + blockedLabel: "Habitante bloqueada", inhabitantviewDetails: "Ver Detalles", keepReading: "Seguir leyendo...", oasisId: "ID", - noInhabitantsFound: "No se encontraron habitantes, aún.", + noInhabitantsFound: "Aún no se han encontrado habitantes.", inhabitantActivityLevel: "Nivel Actividad", lifespanLabel: "Vigencia", deviceLabel: "Dispositivo", @@ -1014,10 +1121,23 @@ module.exports = { parliamentFilterCandidatures: "CANDIDATURAS", parliamentFilterProposals: "PROPUESTAS", parliamentFilterLaws: "LEYES", + parliamentFilterRevocations: "REVOCACIONES", parliamentFilterHistorical: "HISTÓRICO", parliamentFilterLeaders: "LÍDERES", parliamentFilterRules: "REGLAS", parliamentGovernmentCard: "Gobierno actual", + parliamentSeatsGovernment: "Gobierno", + parliamentSeatsOpposition: "Oposición", + parliamentSeatsAbstention: "Abstención", + parliamentSeatsQuorum: "Cuórum de elección", + parliamentSeatsPass: "Umbral de aprobación", + parliamentSeatsByElection: "Por elección", + parliamentSeatsByHouses: "Por casa del LARP", + parliamentSeatsSeats: "escaños", + parliamentSeatsSeat: "escaño", + parliamentSeatsLeader: "Líder", + parliamentSeatsVotesFor: "votos a", + parliamentSeatsNoHouse: "Sin casa", parliamentActorInPowerInhabitant: 'HABITANTE EN EL PODER', parliamentActorInPowerTribe: 'TRIBU EN EL PODER', parliamentHistoricalGovernmentsTitle: 'GOBIERNOS', @@ -1039,10 +1159,8 @@ module.exports = { parliamentPoliciesDeclined: "LEYES RECHAZADAS", parliamentPoliciesDiscarded: "LEYES DESCARTADAS", parliamentEfficiency: "% EFICIENCIA", - parliamentFilterRevocations: 'REVOCACIONES', parliamentRevocationFormTitle: 'Revocar Ley', parliamentRevocationLaw: 'Ley', - parliamentRevocationTitle: 'Título', parliamentRevocationReasons: 'Motivos', parliamentRevocationPublish: 'Publicar Revocación', parliamentCurrentRevocationsTitle: 'Revocaciones Actuales', @@ -1055,23 +1173,12 @@ module.exports = { parliamentNoGovernments: "Todavía no hay gobiernos en el histórico.", parliamentCandidatureFormTitle: "Proponer Candidatura", parliamentCandidatureIdPh: "Oasis ID (@...) o nombre de la tribu", - parliamentCandidatureSlogan: "Lema (máx. 140 car.)", - parliamentCandidatureSloganPh: "Un lema corto", parliamentCandidatureMethod: "Método", parliamentCandidatureProposeBtn: "Publicar Candidatura", - parliamentThDate: "Fecha de propuesta", - parliamentThSlogan: "Lema", - parliamentThSince: "Perfil desde", - parliamentThVotes: "Votos recibidos", - parliamentThVoteAction: "Votar", - parliamentTypeUser: "Habitante", - parliamentTypeTribe: "Tribu", parliamentVoteBtn: "Votar", parliamentProposalFormTitle: "Proponer Ley", parliamentProposalDescription: "Descripción (≤1000)", parliamentProposalPublish: "Publicar Propuesta", - parliamentFinalize: "Finalizar", - parliamentDeadline: "Fecha límite", parliamentNoProposals: "Aún no hay propuestas de ley.", parliamentNoLaws: "Aún no hay leyes aprobadas.", parliamentMethodDEMOCRACY: "Democracia", @@ -1089,37 +1196,31 @@ module.exports = { parliamentVotesSlashTotal: "Votos/Total", parliamentVotesNeeded: "Votos Necesarios", parliamentFutureLawsTitle: "Futuras leyes", - parliamentNoFutureLaws: "Aún no hay futuras leyes.", parliamentVoteAction: "Votar", - parliamentLeadersTitle: "Líderes", parliamentThLeader: "AVATAR", parliamentPopulation: "Población", - parliamentThType: "Tipo", - parliamentThInPower: "Gobiernos", parliamentThPresented: "CANDIDATURAS", parliamentNoLeaders: "Aún no hay líderes.", parliamentCandidaturesListTitle: "Lista de Candidaturas", parliamentTimeRemaining: "Tiempo restante", - parliamentNoLeader: "Aún no hay candidatura ganadora.", parliamentElectionsStatusTitle: "Próximo Gobierno", parliamentProposalDeadlineLabel: "Fecha límite", + parliamentViewReport: "Ver informe", parliamentProposalTimeLeft: "Tiempo restante", - parliamentProposalOnTrack: "Con apoyo suficiente", - parliamentProposalOffTrack: "Apoyo insuficiente", parliamentRulesTitle: "Cómo funciona el Parlamento", parliamentRulesIntro: "Las elecciones se resuelven cada 2 meses; las candidaturas son continuas y se reinician cuando se elige un gobierno.", - parliamentRulesCandidates: "Cualquier habitante puede proponerse a sí mismo, a otro habitante o a cualquier tribu. Cada habitante puede proponer hasta 3 candidaturas por ciclo; se rechazan duplicados en el mismo ciclo.", + parliamentRulesCandidates: "Cualquier habitante puede proponerse a sí misma, a otra habitante o a cualquier tribu. Cada habitante puede proponer hasta 3 candidaturas por ciclo; se rechazan duplicados en el mismo ciclo.", parliamentRulesElection: "Gana la candidatura con más votos en el momento de la resolución.", - parliamentRulesTies: "Desempate: mayor karma del habitante; si persiste, perfil más antiguo; si persiste, propuesta más temprana; después, orden lexicográfico por ID.", - parliamentRulesFallback: "Si nadie vota, gana la última candidatura propuesta. Si no hay candidaturas, se selecciona una tribu al azar.", + parliamentRulesTies: "Desempate: mayor karma de la habitante; si persiste, perfil más antiguo; si persiste, propuesta más temprana; después, orden lexicográfico por ID.", parliamentRulesTerm: "La pestaña Gobierno muestra el gobierno actual y sus estadísticas.", parliamentRulesMethods: "Formas: Anarquía (mayoría simple), Democracia (50%+1), Mayoría (80%), Minoría (20%), Karmatocracia (propuestas con mayor karma), Dictadura (aprobación instantánea).", parliamentRulesAnarchy: "Anarquía es el modo por defecto: si al resolver no se elige ninguna candidatura, se proclama la Anarquía. En Anarquía, cualquier habitante puede proponer leyes.", - parliamentRulesProposals: "Si eres el habitante gobernante o miembro de la tribu gobernante, puedes publicar propuestas de ley. Los métodos no dictatoriales crean una votación pública.", + parliamentRulesProposals: "Si eres la habitante gobernante o miembro de la tribu gobernante, puedes publicar propuestas de ley. Los métodos no dictatoriales crean una votación pública.", parliamentRulesLimit: "Cada habitante puede publicar como máximo 3 propuestas de ley por ciclo.", parliamentRulesLaws: "Cuando una propuesta alcanza su umbral, se convierte en Ley y aparece en la pestaña Leyes con su fecha de entrada en vigor.", parliamentRulesHistorical: "En Histórico se puede ver cada ciclo de gobierno que ha habido y datos sobre su gestión.", parliamentRulesLeaders: "En Líderes se puede ver un ranking de habitantes/tribus que han gobernado (o se han presentado), ordenados por eficacia.", + parliamentRulesHemicycle: "El hemiciclo da un escaño por habitante: el bloque de gobierno (votantes de la candidatura ganadora y quien lidera), un bloque de oposición por cada otra candidatura del ciclo, y la abstención. Las marcas discontinuas señalan el cuórum de elección (25 % del censo) y el umbral de aprobación del método vigente. \"Por casa del LARP\" colorea los mismos escaños por casa.", parliamentProposalVoteStatusLabel: "Estado de la votación", parliamentProposalOnTrackYes: "Umbral alcanzado", parliamentProposalOnTrackNo: "Por debajo del umbral", @@ -1127,17 +1228,14 @@ module.exports = { courtsDescription: "Explora formas de resolución de conflictos y de gestión colectiva de la justicia.", courtsFilterCases: "CASOS", courtsFilterMyCases: "MÍOS", - courtsFilterJudges: "JUECES", + courtsFilterJudges: "JUEZAS", courtsFilterHistory: "HISTORIAL", courtsFilterRules: "REGLAS", courtsCaseFormTitle: "Abrir caso", - courtsCaseRespondent: "Acusado / Parte demandada", + courtsCaseRespondent: "Acusada / Parte demandada", courtsCaseRespondentPh: "ID de Oasis (@...) o nombre de Tribu", - courtsCaseMediatorsAccuser: "Mediadores (acusación)", + courtsCaseSubjectPlaceholder: "Asunto o descripción breve", courtsCaseMethod: "Método de resolución", - courtsCaseDescription: "Descripción (máx. 1000 caracteres)", - courtsCaseEvidenceTitle: "Pruebas del caso", - courtsCaseEvidenceHelp: "Adjunta imágenes, audios, documentos (PDF) o vídeos que apoyen tu caso.", courtsCaseSubmit: "Presentar caso", courtsNominateJudge: "Nominar juez", courtsJudgeId: "Juez", @@ -1168,92 +1266,63 @@ module.exports = { courtsMediatorsSubmit: "Guardar mediadores", courtsNoMyCases: "Aún no tienes conflictos.", courtsNoHistory: "Todavía no hay juicios registrados.", - courtsMethodDICTATOR: "Dictador", + courtsMethodDICTATOR: "Dictadora", courtsMethodPOPULAR: "Popular", courtsMethodKARMATOCRACY: "Karmatocracia", + courtsCaseType: "Tipo de caso", + courtsPresetMinorConflict: "Conflicto leve", + courtsPresetModerateConflict: "Conflicto moderado", + courtsPresetSevereConflict: "Conflicto grave", + courtsPresetHarassment: "Acoso o abuso", + courtsPresetContentModeration: "Moderación de contenido", + courtsPresetBanRestriction: "Expulsión o restricción", + courtsPresetGovernanceDispute: "Disputa de gobernanza", + courtsStatusOPEN: "Abierto", + courtsStatusIN_PROGRESS: "En curso", + courtsStatusDECIDED: "Decidido", + courtsStatusCLOSED: "Cerrado", + courtsStatusSOLVED: "Resuelto", + courtsStatusUNSOLVED: "Sin resolver", + courtsStatusDISCARDED: "Descartado", courtsRulesTitle: "Cómo funcionan los Tribunales", courtsRulesIntro: "Los Tribunales son un proceso gestionado por la comunidad para resolver conflictos y promover la justicia restaurativa. Se priorizan el diálogo, las pruebas claras y los remedios proporcionales.", courtsRulesLifecycle: "Proceso: 1) Abrir caso 2) Seleccionar método 3) Presentar pruebas 4) Audiencia y deliberación 5) Veredicto y remedio 6) Cumplimiento y cierre 7) Apelación (si procede).", - courtsRulesRoles: "Acusación: abre el caso. Defensa: persona o tribu acusada. Método: mecanismo elegido por la comunidad para facilitar, evaluar las pruebas y emitir veredicto. Testigo: aporta testimonio o pruebas. Mediadores: personas neutrales invitadas por la acusación y/o la defensa, con acceso a todos los detalles, que ayudan a desescalar el conflicto y co-crear acuerdos.", + courtsRulesRoles: "Acusación: abre el caso. Defensa: habitante o tribu acusada. Método: mecanismo elegido por la comunidad para facilitar, evaluar las pruebas y emitir veredicto. Testigo: aporta testimonio o pruebas. Mediadores: habitantes neutrales invitadas por la acusación y/o la defensa, con acceso a todos los detalles, que ayudan a desescalar el conflicto y co-crear acuerdos.", courtsRulesEvidence: "Descripción de hasta 1000 caracteres. Adjunta imágenes, audio, vídeo y documentos PDF relevantes y legales. No compartas datos privados sensibles sin consentimiento.", - courtsRulesDeliberation: "Las audiencias pueden ser públicas o privadas. Los jueces garantizan el respeto, piden aclaraciones y pueden descartar material irrelevante o ilegal.", + courtsRulesDeliberation: "Las audiencias pueden ser públicas o privadas. Las juezas garantizan el respeto, piden aclaraciones y pueden descartar material irrelevante o ilegal.", courtsRulesVerdict: "Se prioriza la restauración: disculpas, acuerdos de mediación, moderación de contenido, restricciones temporales u otras medidas proporcionales. El razonamiento debe quedar registrado.", courtsRulesAppeals: "Apelación: permitida cuando hay nuevas pruebas o un error procesal claro. Debe presentarse en un plazo de 7 días salvo que se indique lo contrario.", - courtsRulesPrivacy: "Respeta la privacidad y la seguridad. El doxing, el odio o las amenazas se eliminan. Los jueces pueden editar o sellar partes del expediente para proteger a personas en riesgo.", + courtsRulesPrivacy: "Respeta la privacidad y la seguridad. El doxing, el odio o las amenazas se eliminan. Las juezas pueden editar o sellar partes del expediente para proteger a habitantes en riesgo.", courtsRulesMisconduct: "El acoso, la manipulación o las pruebas fabricadas pueden llevar a una resolución negativa inmediata.", courtsRulesGlossary: "Caso: registro de un conflicto. Pruebas: materiales que respaldan las afirmaciones. Veredicto: decisión con remedio. Apelación: solicitud para revisar el veredicto.", courtsFilterActions: "ACCIONES", - courtsNoActions: "No hay acciones pendientes para tu rol.", - courtsCaseTitlePlaceholder: "Breve descripción del conflicto", - courtsCaseSeverity: "Severidad", - courtsCaseSeverityNone: "Sin etiqueta de severidad", - courtsCaseSeverityLOW: "Baja", - courtsCaseSeverityMEDIUM: "Media", - courtsCaseSeverityHIGH: "Alta", - courtsCaseSeverityCRITICAL: "Crítica", - courtsCaseSubject: "Tema", - courtsCaseSubjectNone: "Sin etiqueta de tema", - courtsCaseSubjectBEHAVIOUR: "Comportamiento", - courtsCaseSubjectCONTENT: "Contenido", - courtsCaseSubjectGOVERNANCE: "Gobernanza / normas", - courtsCaseSubjectFINANCIAL: "Finanzas / recursos", - courtsCaseSubjectOTHER: "Otro", - courtsHiddenRespondent: "Oculto (solo visible para los roles implicados).", courtsThRole: "Rol", courtsRoleAccuser: "Acusación", courtsRoleDefence: "Defensa", courtsRoleMediator: "Mediador", courtsRoleJudge: "Juez", - courtsRoleDictator: "Dictador", + courtsRoleDictator: "Dictadora", courtsAssignJudgeTitle: "Elegir juez", courtsAssignJudgeBtn: "Elegir juez", trendingTitle: "Tendencias", exploreTrending: "Explora el contenido más popular en tu red.", - bookmarkButton: "MARCADORES", - transferButton: "TRANSFERENCIAS", - eventButton: "EVENTOS", - taskButton: "TAREAS", votesButton: "GOBIERNO", - reportButton: "INFORMES", - feedButton: "FEED", - marketButton: "MERCADO", - imageButton: "IMÁGENES", - audioButton: "AUDIOS", - videoButton: "VIDEOS", - documentButton: "DOCUMENTOS", - torrentButton: "TORRENTS", - noTrendingFound: "No se encontraron contenidos populares.", - noContentMessage: "No hay contenido popular disponible, aún.", - trendingDescription: "Descripción", - trendingDate: "Fecha", - trendingLocation: "Ubicación", - trendingPrice: "Precio", - trendingUrl: "URL", - trendingCategory: "Categoría", - trendingStart: "Inicio", - trendingEnd: "Fin", - trendingPriority: "Prioridad", - trendingStatus: "Estado", - trendingFrom: "Desde", - trendingTo: "Hasta", - trendingConcept: "Concepto", - trendingAmount: "Cantidad", - trendingDeadline: "Plazo", - trendingItemStatus: "Estado del Artículo", - trendingTotalVotes: "Total de Votos", + shopProductButton: "TIENDA", + podcastButton: "PODCAST", + podcastEpisodeButton: "EPISODIO", + campaignButton: "CAMPAÑA", + logisticsRouteButton: "RUTA", trendingTotalOpinions: "Total de Opiniones", - trendingNoContentMessage: "No hay tendencias disponibles, aún.", - trendingAuthor: "Por", - trendingCreatedAtLabel: "Creado el", + trendingNoContentMessage: "Todavía no hay tendencias.", trendingTotalCount: "Total de Contadores", tasksTitle: "Tareas", tasksDescription: "Descubre y gestiona tareas en tu red.", + taskSearchPlaceholder: "Buscar en Tareas...", taskTitleLabel: "Título", taskDescriptionLabel: "Descripción", taskStartTimeLabel: "Fecha de Inicio", taskEndTimeLabel: "Fecha de Fin", taskPriorityLabel: "Prioridad", - taskPrioritySelect: "Seleccionar prioridad", taskPriorityUrgent: "Urgente", taskPriorityHigh: "Alta", taskPriorityMedium: "Media", @@ -1263,13 +1332,13 @@ module.exports = { taskVisibilityLabel: "Visibilidad", taskPublic: "público", taskPrivate: "privado", - taskCreatedAt: "Creado el", - taskBy: "Por", taskStatus: "Estado", taskStatusOpen: "Abierta", taskStatusInProgress: "En Progreso", taskStatusClosed: "Cerrada", - taskAssignedTo: "Asignada a", + taskAssignedTo: "Asignada", + taskAssignedChip: "Asignada", + taskUnassignedChip: "Sin asignar", taskAssignees: "Asignados", taskAssignButton: "Asignar a Mí", taskUnassignButton: "Desasignar", @@ -1277,12 +1346,12 @@ module.exports = { taskUpdateButton: "Actualizar", taskDeleteButton: "Eliminar", taskFilterAll: "TODOS", + taskFilterRecent: "RECIENTES", taskFilterMine: "MÍAS", taskFilterOpen: "ABIERTAS", taskFilterInProgress: "EN-PROCESO", taskFilterClosed: "CERRADAS", taskFilterAssigned: "ASIGNADAS", - taskFilterArchived: "ARCHIVADAS", taskFilterUrgent: "URGENTES", taskFilterHigh: "ALTAS", taskFilterMedium: "MEDIAS", @@ -1295,35 +1364,47 @@ module.exports = { taskInProgressTitle: "Tareas en Progreso", taskClosedTitle: "Tareas Cerradas", taskAssignedTitle: "Tareas Asignadas", - taskArchivedTitle: "Tareas Archivadas", - taskPublicTitle: "Tareas Públicas", - taskPrivateTitle: "Tareas Privadas", notasks: "No hay tareas disponibles.", noLocation: "No se especificó ubicación", taskSetStatus: "Establecer estado", - eventTitle: "Eventos", eventDateLabel: "Fecha", + eventRecurrenceLabel: "Recurrencia", + eventRecurrenceUntil: "Repetir hasta", + eventRecurrenceHint: "Deja vacía la fecha de repetición para un evento único.", + eventUpcomingDates: "Próximas fechas", eventsTitle: "Eventos", eventsDescription: "Descubre y gestiona eventos en tu red.", - eventDescription: "Descripción", + eventSearchPlaceholder: "Buscar en Eventos...", eventPrice: "Precio", eventStatus: "Estado", - eventOrganizer: "Organizador", eventAllSectionTitle: "Eventos", eventMineSectionTitle: "Tus Eventos", - eventArchivedTitle: "Eventos Archivados", eventCreateSectionTitle: "Crear Evento", eventUpdateSectionTitle: "Actualizar Evento", - eventDeleteButton: "Eliminar", eventCreateButton: "Crear Evento", eventAddToCalendar: "Añadir al Calendario", eventVisitCalendar: "Visitar Calendario", viewTask: "Ver Tarea", - viewReport: "Ver Reporte", + viewEvent: "Ver evento", + viewPad: "Ver pad", + viewMap: "Ver mapa", + viewCalendar: "Ver calendario", + viewReport: "Ver informe", viewTransfer: "Ver Transferencia", viewItem: "Ver Item", viewShop: "Ver Tienda", viewProject: "Ver Proyecto", + viewJob: "Ver Empleo", + viewPlace: "Ver Lugar", + generatePdf: "Generar PDF", + sharePm: "Compartir por PM", + galleryImages: "Imágenes (tamaño máx: 75MB cada una)", + galleryPhotos: "Imágenes", + galleryAddPhoto: "Añadir Imagen", + galleryRemovePhoto: "Quitar", + galleryVideo: "Vídeo (tamaño máx: 75MB)", + galleryAddVideo: "Añadir Vídeo", + galleryRemoveVideo: "Quitar vídeo", viewVotation: "Ver Votación", voteCastTitle: "Emitir Voto", voteResults: "Resultados", @@ -1331,48 +1412,26 @@ module.exports = { eventDescriptionLabel: "Descripción", eventDescriptionPlaceholder: "Introduce la descripción del evento...", eventUpdateButton: "Actualizar", - eventAttendeesLabel: "Asistentes", - eventAttendeesPlaceholder: "Introduce los asistentes, separados por comas...", eventTagsLabel: "Etiquetas", - eventTagsPlaceholder: "Introduce las etiquetas del evento, separadas por comas...", - eventTags: "Etiquetas", eventPriceLabel: "Precio", eventUrlLabel: "URL", eventAttendees: "Asistentes", - noAttendees: "Aún no hay asistentes", - eventCreatedAt: "Creado el", eventLocation: "Ubicación", eventLocationLabel: "Ubicación", - eventNoLocation: "No se especificó ubicación", - eventNoURL: "No se especificó URL", - eventBy: "Por", noevents: "No hay eventos disponibles.", eventDate: "Fecha", - eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Asistir al Evento", - eventUnattendButton: "Dejar de asistir al Evento", - eventCreatedBy: "Creado por", - eventAttendeesCount: "Número de Asistentes", - eventCreatedByYou: "Tú creaste este evento", eventFilterAll: "TODOS", - eventFilterMine: "MIOS", + eventFilterRecent: "RECIENTES", + eventFilterMine: "MÍOS", eventFilterToday: "HOY", eventFilterWeek: "ESTA SEMANA", eventFilterMonth: "ESTE MES", eventFilterYear: "ESTE AÑO", eventFilterArchived: "ARCHIVADOS", - eventTodayTitle: "Eventos de Hoy", - eventThisWeekTitle: "Eventos de Esta Semana", - eventThisMonthTitle: "Eventos de Este Mes", - eventThisYearTitle: "Eventos de Este Año", eventPrivacyLabel: "Visibilidad", eventPublic: "Público", eventPrivate: "Privado", - eventPublicTitle: "Eventos Públicos", - eventNoPrice: "Evento Gratuito", - eventNoImage: "No se ha subido ninguna imagen", - eventAttendConfirmation: "Ahora estás asistiendo a este evento", - eventUnattendConfirmation: "Ya no estás asistiendo a este evento", eventAttended: "Asisto", eventUnattended: "No asisto", eventStatusOpen: "Abierto", @@ -1383,9 +1442,14 @@ module.exports = { tagsTopSectionTitle: "Etiquetas Principales", tagsCloudSectionTitle: "Nube de Etiquetas", tagsFilterAll: "TODOS", + tagsFilterMine: "MÍAS", + tagsFilterRecent: "RECIENTES", + tagsMineSectionTitle: "Mis etiquetas", + tagsRecentSectionTitle: "Etiquetas recientes", tagsFilterTop: "TOP", tagsFilterCloud: "NUBE", tagsNoItems: "No hay etiquetas disponibles.", + tagsSearchPlaceholder: "Buscar en Etiquetas...", tagsTableHeaderTag: "ETIQUETA/ENLACE:", tagsTableHeaderCount: "CONTADOR:", transfersTitle: "Transferencias", @@ -1393,7 +1457,10 @@ module.exports = { transfersFrom: "De", transfersTo: "A", transfersFilterAll: "TODOS", + transfersFilterRecent: "RECIENTES", transfersFilterMine: "MÍAS", + transfersFilterMinePending: "PENDIENTES", + transfersFilterMineConfirmed: "FIRMADOS", transfersFilterUBI: "RBU", transfersFilterMarket: "MERCADO", transfersFilterTop: "TOP", @@ -1408,7 +1475,6 @@ module.exports = { transfersCategoryEconomic: "Económica", transfersCategoryTime: "Tiempo", transfersCategoryTrust: "Confianza", - transfersCategoryApply: "Aplicar", transfersUnitEco: "ECO", transfersUnitHours: "h", transfersUnitTrust: "confianza", @@ -1425,18 +1491,12 @@ module.exports = { transfersDeadline: "Plazo", transfersTags: "Etiquetas", transfersStatus: "Estado", - transfersStatusUnconfirmed: "NO CONFIRMADA", - transfersStatusClosed: "CERRADA", - transfersStatusDiscarded: "DESCARTADA", - transfersCreatedAt: "Creado el", transfersConfirmations: "CONFIRMACIONES", - transfersContainingBlock: "Bloque contenedor", - transfersExportContract: "Crear contrato", + transfersExportContract: "Generar Factura", transfersConfirmButton: "Confirmar Transferencia", transfersNoItems: "No se encontraron transferencias.", transfersMineSectionTitle: "Tus Transferencias", transfersUBISectionTitle: "Transferencias RBU", - transfersMarketSectionTitle: "Transferencias del Mercado", transfersTopSectionTitle: "Transferencias Principales", transfersPendingSectionTitle: "Transferencias Pendientes", transfersUnconfirmedSectionTitle: "Transferencias No Confirmadas", @@ -1444,21 +1504,13 @@ module.exports = { transfersDiscardedSectionTitle: "Transferencias Descartadas", transfersCreateSectionTitle: "Crear transferencia", transfersAllSectionTitle: "Transferencias", - transfersFilterFavs: "Favoritos", - transfersFavsSectionTitle: "Transferencias favoritas", - transfersSearchLabel: "Buscar", - transfersSearchPlaceholder: "Buscar por concepto, tags, usuarios...", + transfersSearchPlaceholder: "Buscar en Transferencias...", transfersMinAmountLabel: "Importe mín.", transfersMaxAmountLabel: "Importe máx.", - transfersSortLabel: "Ordenar por", transfersSortRecent: "Más recientes", transfersSortAmount: "Mayor importe", - transfersSortDeadline: "Deadline más próximo", + transfersSortDeadline: "Plazo más próximo", transfersSearchButton: "Buscar", - transfersFavoriteButton: "Favorito", - transfersUnfavoriteButton: "Quitar favorito", - transfersMessageUserButton: "Mensaje", - transfersExpiringSoonBadge: "CADUCA PRONTO", transfersExpiredBadge: "CADUCADA", transfersUpdatedAt: "Actualizado", transfersNoMatch: "No hay transferencias que coincidan con tu búsqueda.", @@ -1471,6 +1523,10 @@ module.exports = { voteClosedTitle: "Votaciones Cerradas", voteAllSectionTitle: "Gobierno", voteCreateButton: "Crear Votación", + voteErrorTitle: "Revisa el formulario", + voteErrorDeadlineMin: "La fecha límite debe ser de al menos {days} días desde ahora.", + voteErrorGeneric: "No se ha podido guardar la votación.", + voteLocked: "Esta votación ya ha recibido votos u opiniones, o pertenece al Parlamento o a los Tribunales: ya no se puede editar ni borrar.", voteUpdateButton: "Actualizar", voteDeleteButton: "Eliminar", voteOptionYes: "SÍ", @@ -1480,6 +1536,7 @@ module.exports = { voteFollowMajority: "SEGUIR MAYORÍA", voteNotInterested: "SIN INTERÉS", voteFilterAll: "TODOS", + voteFilterRecent: "RECIENTES", voteFilterMine: "MÍAS", voteFilterOpen: "ABIERTAS", voteFilterClosed: "CERRADAS", @@ -1495,37 +1552,28 @@ module.exports = { voteTags: "Etiquetas", voteOpinions: "Opiniones", novotes: "No hay propuestas de votación disponibles.", - voteBy: "Por", voteCreatedAt: "Creado el", voteNoQuestion: "No se proporcionó pregunta", - voteUnknownCreator: "Creador Desconocido", + voteUnknownCreator: "Creadora desconocida", voteUnknownDate: "Fecha Desconocida", - errorVoteNotFound: "Votación no encontrada", - errorAlreadyVoted: "Ya has opnado.", - errorVoteClosedCannotEdit: "No puedes editar una votación cerrada", - errorVoteDeadlinePassed: "El plazo para esta votación ha pasado", - errorRetrievingVote: "Error al recuperar votación", - errorCreatingVote: "Error al crear votación", - errorVoteAlreadyVoted: "No se puede editar después de haber opinado", - errorDeletingOldVote: "Error al eliminar opinión anterior", - errorCreatingUpdatedVote: "Error al crear opinión actualizada", - errorCreatingTombstone: "Error al crear lápida", voteDetailSectionTitle: 'Detalles de la votación', voteCommentsLabel: 'Comentarios', voteCommentsForumButton: 'Abrir conversación', voteCommentsSectionTitle: 'Conversación abierta', - voteNoCommentsYet: 'Todavía no hay comentarios. Sé la primera persona en opinar.', + voteNoCommentsYet: 'Todavía no hay comentarios. Sé el primer habitante en opinar.', + commentsGoToLast: "Ir al último comentario", voteNewCommentPlaceholder: 'Escribe aquí tu comentario…', voteNewCommentButton: 'Publicar comentario', voteNewCommentLabel: 'Añade un comentario', cvTitle: "CV", - cvLabel: "Currículum Vitae (CV)", cvEditSectionTitle: "Editar CV", cvCreateSectionTitle: "Crear CV", cvDescription: "Gestiona y comparte tus habilidades profesionales e información.", cvNameLabel: "Nombre Completo", cvDescriptionLabel: "Resumen", cvPhotoLabel: "Foto", + cvPdfLabel: "CV (PDF)", + cvVisitButton: "Visitar CV", cvPersonalExperiencesLabel: "Experiencias Personales", cvPersonalSkillsLabel: "Habilidades Personales (separadas por comas)", cvOasisExperiencesLabel: "Experiencias de Contribución en Oasis", @@ -1538,29 +1586,130 @@ module.exports = { cvLocationLabel: "Ubicación", cvStatusLabel: "Estado", cvPreferencesLabel: "Preferencias", - cvOasisContributorLabel: "Contribuyente de Oasis", cvPersonal: "Personal", cvOasis: "Contribuyente de Oasis (opcional)", - cvOasisContributorView: "Contribución en Oasis", + cvOasisContributorView: "Contribución", cvEducational: "Educativo (opcional)", cvEducationalView: "Educativo", cvProfessional: "Profesional (opcional)", cvProfessionalView: "Profesional", cvAvailability: "Disponibilidad (opcional)", - cvAvailabilityView: "Disponibilidad", cvUpdateButton: "Actualizar", cvCreateButton: "Crear CV", - cvContactLabel: "Contacto", cvCreatedAt: "Creado el", cvUpdatedAt: "Actualizado el", cvEditButton: "Actualizar", cvDeleteButton: "Eliminar", cvNoCV: "No se encontró ningún CV.", blogSubject: "Asunto", - blogMessage: "Cuerpo limitado a 8096 caracteres.", - blogImage: "Subir contenido multimedia (max: 50MB)", - blogPublish: "Vista previa", - noPopularMessages: "No se han publicado mensajes populares, aún", + blogMessage: "Mensaje", + blogImage: "Subir contenido multimedia (máx.: 75MB)", + blogMedia: "Adjuntos (hasta 8 imágenes y un vídeo)", + blogPublish: "Publicar", + pollsTitle: "Encuestas", + dataTitle: "Coincidencias", + dataDescription: "Explora y visualiza las coincidencias de tu red.", + dataFilterAll: "TODO", + dataFilterMine: "MÍOS", + dataFilterRecent: "RECIENTES", + dataFilterTop: "TOP", + dataKindInhabitants: "HABITANTES", + dataKindJobs: "EMPLEOS", + dataKindProjects: "PROYECTOS", + dataKindEvents: "EVENTOS", + dataKindTribes: "TRIBUS", + dataKindMarket: "MERCADO", + dataKindHousing: "VIVIENDA", + dataKindIndustry: "INDUSTRIA", + dataKindTasks: "TAREAS", + dataKindReports: "INFORMES", + dataKindVotes: "VOTACIONES", + dataSearchPlaceholder: "Buscar en Coincidencias...", + dataCohesionTitle: "Coeficiente de Cohesión (CC)", + dataCcShort: "CC", + dataCohesionHint: "Afinidad media entre todo lo que la red ha publicado.", + dataAffinity: "Afinidad", + dataCommonTerms: "Palabra clave", + dataReasonFollowing: "Das soporte a este habitante", + dataReasonTribe: "Comparte tribu contigo", + dataReasonPinned: "Coincide con algo que has fijado", + dataReasonCv: "Coincide con las habilidades de tu CV", + dataReasonContent: "Relacionado con lo que publicas", + dataReasonRelated: "Relacionado con tus intereses", + dataReasonAlike: "Publicado por una habitante afín a ti", + dataReasonSupportsYou: "Te da soporte", + dataReasonMutual: "Soporte mutuo", + dataReasonRated: "Bien valorado por la red", + dataReasonNear: "Cerca de ti", + dataStatPeople: "CVs", + dataStatConnected: "Conectados", + dataStatSkills: "Habilidades", + dataBestMatch: "Mejor match", + dataStatIsolated: "Aislados", + dataStatEntities: "Entidades comparadas", + dataStatTerms: "Términos distintos", + dataStatPairs: "Pares conectados", + dataMatchesTitle: "Coincidencias", + dataMatchesHint: "Sugerencias personalizadas del algoritmo.", + dataTermsTitle: "Palabras clave", + dataTermsHint: "Los términos que aparecen en más contenidos.", + dataConnections: "Coincidencias enlazadas", + dataNoMatches: "No hay datos cruzados que mostrar.", + dataNoProfile: "Publica contenido o escribe tu CV para que el algoritmo aprenda qué te interesa.", + pollsDescription: "Módulo para preguntar a la red y contar las respuestas.", + pollFilterAll: "TODAS", + pollFilterMine: "MÍAS", + pollFilterRecent: "RECIENTES", + pollFilterTop: "TOP", + pollFilterVoted: "VOTADAS", + pollFilterOpen: "ABIERTAS", + pollFilterClosed: "CERRADAS", + pollCreateButton: "Crear Encuesta", + pollCreateTitle: "Crear Encuesta", + pollEditTitle: "Editar Encuesta", + pollQuestion: "Pregunta", + pollQuestionPlaceholder: "¿Qué quieres preguntar?", + pollOptions: "Opciones (una por línea)", + pollOutcome: "Resultado", + pollNoVotesYet: "Todavía sin votos", + pollTie: "Empate", + pollOptionsPlaceholder: "Plaza\nParque\nBar", + pollAnonymous: "Anónima", + pollAnonymousLabel: "Encuesta anónima", + pollMultiple: "Respuesta múltiple", + pollMultipleLabel: "Permitir varias respuestas", + pollDeadline: "Fecha límite", + pollTags: "Etiquetas", + pollPublishButton: "Publicar Encuesta", + pollUpdateButton: "Actualizar", + pollCloseButton: "Cerrar", + pollDeleteButton: "Eliminar", + pollVoteButton: "Votar", + pollChangeVote: "Cambiar Voto", + pollVoters: "Votantes", + pollComments: "Comentarios", + pollStatusLabel: "Estado", + pollStatusOpen: "ABIERTA", + pollStatusClosed: "CERRADA", + pollSearchPlaceholder: "Buscar en Encuestas...", + pollsNoItems: "No se han encontrado encuestas.", + viewPoll: "Ver Encuesta", + pollChatCreate: "Crear Encuesta", + pollInChat: "Encuesta", + blogTitle: "Blogs", + blogDescription: "Módulo para descubrir y gestionar blogs.", + blogFilterAll: "TODOS", + blogFilterMine: "MÍOS", + blogFilterRecent: "RECIENTES", + blogFilterFavorites: "FAVORITOS", + blogFilterTop: "TOP", + blogCreateButton: "Crear Blog", + blogSearchPlaceholder: "Buscar en Blogs...", + blogComments: "Comentarios", + blogAllowComments: "Permitir comentarios", + blogCommentsClosed: "La autora ha cerrado los comentarios de este blog.", + blogNoItems: "No se han encontrado blogs.", + viewBlog: "Ver Blog", forumTitle: "Foros", forumCategoryLabel: "Categoría", forumTitleLabel: "Título", @@ -1568,43 +1717,21 @@ module.exports = { forumCreateButton: "Crear foro", forumCreateSectionTitle: "Crear foro", forumDescription: "Habla abiertamente con habitantes de tu red.", + forumSearchPlaceholder: "Buscar en Foros...", forumFilterAll: "TODOS", forumFilterMine: "MÍAS", forumFilterRecent: "RECIENTES", forumFilterTop: "TOP", - forumMineSectionTitle: "Tus Foros", - forumRecentSectionTitle: "Foros Recientes", - forumAllSectionTitle: "Foros", - forumDeleteButton: "Borrar", forumParticipants: "participantes", forumMessages: "mensajes", - forumLastMessage: "Último mensaje", forumMessageLabel: "Mensaje", forumMessagePlaceholder: "Escribe tu mensaje...", forumSendButton: "Enviar", - forumVisitForum: "Visitar Foro", - noForums: "No hay foros disponibles, aún.", - forumVisitButton: "Visitar foro", - forumCatGENERAL: "General", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "Política", - forumCatTECH: "Tecnología", - forumCatSCIENCE: "Ciencia", - forumCatMUSIC: "Música", - forumCatART: "Arte", - forumCatGAMING: "Videojuegos", - forumCatBOOKS: "Libros", - forumCatFILMS: "Cine", - forumCatPHILOSOPHY: "Filosofía", - forumCatSOCIETY: "Sociedad", - forumCatPRIVACY: "Privacidad", - forumCatCYBERWARFARE: "Ciberguerra", - forumCatSURVIVALISM: "Supervivencia", + noForums: "Aún no hay foros disponibles.", + forumVisitButton: "Visitar Foro", + forumTopicLabel: "Tema", imageTitle: "Imágenes", imageDescription: "Explora y gestiona contenido de imágenes en tu red.", - imagePluginTitle: "Título", - imagePluginDescription: "Descripción", imageMineSectionTitle: "Tus imágenes", imageCreateSectionTitle: "Subir imagen", imageUpdateSectionTitle: "Actualizar imagen", @@ -1613,17 +1740,14 @@ module.exports = { imageTopSectionTitle: "Top imágenes", imageFavoritesSectionTitle: "Favoritos", imageGallerySectionTitle: "Galería", - imageMemeSectionTitle: "Memes", imageFilterAll: "TODAS", imageFilterMine: "MÍAS", imageFilterRecent: "RECIENTES", imageFilterTop: "TOP", imageFilterFavorites: "FAVORITOS", imageFilterGallery: "GALERÍA", - imageFilterMeme: "MEMES", imageCreateButton: "Subir imagen", imageUpdateButton: "Actualizar", - imageDeleteButton: "Eliminar", imageAddFavoriteButton: "Añadir favorito", imageRemoveFavoriteButton: "Quitar favorito", imageFileLabel: "Selecciona un archivo de imagen (.jpeg, .jpg, .png, .gif)", @@ -1633,37 +1757,35 @@ module.exports = { imageTitlePlaceholder: "Opcional", imageDescriptionLabel: "Descripción", imageDescriptionPlaceholder: "Opcional", - imageMemeLabel: "¿MEME?", imageNoFile: "No se ha proporcionado ningún archivo de imagen", noImages: "No hay imágenes disponibles.", - imageSearchPlaceholder: "Buscar por título, etiquetas, descripción, autor...", + imageSearchPlaceholder: "Buscar en Imágenes...", imageSortRecent: "Más recientes", imageSortOldest: "Más antiguas", imageSortTop: "Más votadas", imageSearchButton: "Buscar", - imageMessageAuthorButton: "Mensaje", imageUpdatedAt: "Actualizado", imageNoMatch: "Ninguna imagen coincide con tu búsqueda.", + feedSearchPlaceholder: "Buscar en Feed...", feedTitle: "Feed", feedDetailTitle: "Feed", feedOpenDiscussion: "Abrir discusión", feedPostComment: "Publicar comentario", noComments: "Aún no hay comentarios", createFeedTitle: "Crear Feed", - createFeedButton: "Enviar Feed!", + createFeedButton: "¡Enviar feed!", feedPlaceholder: "¿Qué está pasando? (máximo 280 caracteres)", TODAYButton: "HOY", - CREATEButton: "Crear Feed", totalOpinions: "Total de Opiniones", moreVoted: "Más Votado", noFeedsFound: "No se encontraron feeds.", createdAtLabel: "Creado el", FeedshareYourOpinions: "Descubre y comparte textos breves en tu red.", - refeedButton: "Realimentar", + refeedButton: "Reenviar", alreadyRefeeded: "Ya has realimentado esto.", activityTitle: "Actividad", yourActivity: "Tu actividad", - globalActivity: "Actividad global", + globalActivity: "Actividad", activityList: "Actividad", activityDesc: "Consulta la actividad más reciente de tu red.", allButton: "TODO", @@ -1675,20 +1797,13 @@ module.exports = { concept: "Concepto", description: "Descripción", meme: "Meme", - activityContact: "Contacto", - activityBy: "Nombre", - activityPixelia: "Nuevo píxel añadido", - viewImage: "Ver imagen", - playAudio: "Reproducir audio", - playVideo: "Reproducir vídeo", - typeRecent: "RECIENTE", - errorActivity: "Error al recuperar la actividad", - typePost: "BLOGS", - recentButton: "RECIENTE", + typeRecent: "RECIENTES", + typeTop: "TOP", + typePost: "BLOG", + activityUpdateLabel: "ACTUALIZACIÓN", typeTribe: "TRIBUS", typeLarp: "L.A.R.P.", typeInhabitants: "HABITANTES", - activityProfileUpdated: "actualizó su perfil", typeAbout: "HABITANTES", typeCurriculum: "CVS", typeImage: "IMÁGENES", @@ -1706,14 +1821,14 @@ module.exports = { typeTask: "TAREAS", typePixelia: "PIXELIA", typeForum: "FOROS", - typeReport: "REPORTES", + typeReport: "INFORMES", typeFeed: "FEEDS", typeContact: "CONTACTO", typePub: "PUB", typeTombstone: "ELIMINADO", typeBanking: "BANCA", - typeBankWallet: "BANCA/MONEDERO", - typeBankClaim: "BANCA/UBI", + typeBankWallet: "BANCA/CARTERA", + typeBankClaim: "BANCA/RBU", typeKarmaScore: "KARMA", typeParliament: "PARLAMENTO", typeSpread: "RÉPLICAS", @@ -1732,8 +1847,6 @@ module.exports = { typeCourtsSettlementAccepted: "Tribunales · Acuerdo aceptado", typeCourtsNomination: "Tribunales · Nominación", typeCourtsNominationVote: "Tribunales · Votación de nominación", - activitySupport: "Nueva alianza forjada", - activityJoin: "Nuevo PUB unido", question: "Pregunta", deadline: "Fecha límite", status: "Estado", @@ -1747,21 +1860,16 @@ module.exports = { date: "Fecha", category: "Categoría", attendees: "Asistentes", - activitySpread: "->", - visitLink: "Visitar enlace", - viewDocument: "Ver documento", location: "Ubicación", contentWarning: "Asunto", - personName: "Nombre del habitante", - bankWalletConnected: "Monedero ECOin", - bankUbiReceived: "UBI recibido", + bankWalletConnected: "Cartera ECOin", + bankUbiReceived: "RBU recibida", bankTx: "Tx", bankEpochShort: "Época", viewDetails: "Ver detalles", link: "Enlace", activityProjectFollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%", activityProjectUnfollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%", - activityProjectPledged: "%OASIS% ha %ACTION% %AMOUNT% al proyecto %PROJECT%", following: "SIGUIENDO", unfollowing: "DEJANDO DE SEGUIR", pledged: "APORTADO", @@ -1790,7 +1898,7 @@ module.exports = { courtsCaseTitle: "Caso", courtsMethod: "Método", courtsMethodJUDGE: "Juez", - courtsMethodJUDGES: "Panel de jueces", + courtsMethodJUDGES: "Panel de juezas", courtsMethodSINGLE_JUDGE: "Juez único", courtsMethodJURY: "Jurado", courtsMethodCOUNCIL: "Consejo", @@ -1807,26 +1915,17 @@ module.exports = { courtsVotesSlashTotal: "SÍ/TOTAL", courtsOpenVote: "Votación abierta", courtsAnswerTitle: "Respuesta", - courtsStanceADMIT: "Admite", - courtsStanceDENY: "Niega", - courtsStancePARTIAL: "Parcial", - courtsStanceCOUNTERCLAIM: "Reconvención", - courtsStanceNEUTRAL: "Neutral", courtsVerdictResult: "Resultado", courtsVerdictOrders: "Órdenes", courtsSettlementText: "Acuerdo", courtsSettlementAccepted: "Aceptado", - courtsSettlementPending: "Pendiente", courtsJudge: "Juez", courtsThSupports: "Apoyos", courtsFilterOpenCase: 'Abrir caso', - courtsEvidenceFileLabel: 'Archivo de prueba (imagen, audio, vídeo o PDF)', - courtsCaseMediators: 'Mediadores', courtsCaseMediatorsPh: 'IDs de Oasis de mediadores, separados por comas', courtsMediatorsLabel: 'Mediadores', courtsThCase: 'Caso', courtsThCreatedAt: 'Fecha de inicio', - courtsThActions: 'Acciones', courtsPublicPrefLabel: 'Visibilidad tras la resolución', courtsPublicPrefYes: 'Acepto que este caso sea totalmente público', courtsPublicPrefNo: 'Prefiero mantener privados los detalles', @@ -1834,9 +1933,12 @@ module.exports = { courtsMethodMEDIATION: 'Mediación', courtsNoCases: 'No hay casos.', reportsTitle: "Informes", - reportsDescription: "Gestiona y realiza un seguimiento de los informes relacionados con problemas, errores, abusos y advertencias de contenido en tu red.", + reportsDescription: "Gestiona y sigue los reportes de tu red.", + reportsSearchPlaceholder: "Buscar en Informes...", reportsFilterAll: "TODOS", - reportsFilterMine: "MIOS", + reportsFilterRecent: "RECIENTES", + reportsFilterTop: "TOP", + reportsFilterMine: "MÍOS", reportsFilterFeatures: "FUNCIONES", reportsFilterBugs: "ERRORES", reportsFilterAbuse: "ABUSOS", @@ -1845,21 +1947,16 @@ module.exports = { reportsFilterResolved: "RESUELTOS", reportsFilterOpen: "ABIERTOS", reportsFilterUnderReview: "EN REVISIÓN", - reportsFilterInvalid: "INVALIDOS", + reportsFilterInvalid: "INVÁLIDOS", reportsCreateButton: "Crear Informe", reportsTitleLabel: "Título", - reportsDescriptionLabel: "Descripción", - reportsDescriptionPlaceholder: "Por favor, proporciona una descripción detallada.", reportsCategory: "Categoría", - reportsCategoryLabel: "Categoría", reportsCategoryFeatures: "Funciones", reportsCategoryBugs: "Errores", reportsCategoryAbuse: "Abuso", - reportsCategoryContent: "Problemas de Contenido", + reportsCategoryContent: "Contenido", reportsUpdateButton: "Actualizar", reportsDeleteButton: "Eliminar", - reportsDateLabel: "Fecha", - reportsUploadFile: "Subir contenido multimedia (max: 50MB)", reportsMineSectionTitle: "Tus Informes", reportsFeaturesSectionTitle: "Solicitudes de Funciones", reportsBugsSectionTitle: "Errores", @@ -1867,11 +1964,6 @@ module.exports = { reportsContentSectionTitle: "Problemas de Contenido", reportsAllSectionTitle: "Informes", reportsNoItems: "No hay informes disponibles.", - reportsValidationTitle: "Por favor, ingresa un título válido.", - reportsValidationDescription: "La descripción no puede estar vacía.", - reportsValidationCategory: "Por favor, selecciona una categoría.", - reportsCreatedAt: "Creado el", - reportsCreatedBy: "Por", reportsSeverity: "Gravedad", reportsSeverityLow: "Baja", reportsSeverityMedium: "Media", @@ -1882,18 +1974,14 @@ module.exports = { reportsStatusUnderReview: "En Revisión", reportsStatusResolved: "Resuelto", reportsStatusInvalid: "Inválido", - reportsUpdateStatusButton: "Actualizar Estado", - reportsAnonymityOption: "Enviar de forma anónima", - reportsAnonymousAuthor: "Anónimo", - reportsConfirmButton: "CONFIRMAR INFORME!", + reportsConfirmButton: "¡CONFIRMAR INFORME!", reportsConfirmations: "Confirmaciones", reportsConfirmedSectionTitle: "Informes Confirmados", - reportsCreateTaskButton: "CREAR TAREA", + reportsCreateTaskButton: "Crear Tarea", reportsOpenSectionTitle: "Informes Abiertos", reportsUnderReviewSectionTitle: "Informes en Revisión", reportsResolvedSectionTitle: "Informes Resueltos", reportsInvalidSectionTitle: "Informes Inválidos", - reportsTemplateSectionTitle: 'Plantilla de reporte', reportsBugTemplateTitle: 'Datos para reproducir (Bugs)', reportsFeatureTemplateTitle: 'Detalles (Features)', reportsAbuseTemplateTitle: 'Detalles (Abuso)', @@ -1926,11 +2014,25 @@ module.exports = { reportsEvidenceLinksLabel: 'Evidencia / enlaces', reportsEvidenceLinksPlaceholder: 'Pega enlaces, IDs de mensajes, capturas (si aplica), etc.', reportsContentLocationLabel: 'Dónde está el contenido', - reportsContentLocationPlaceholder: 'Enlace, ID, canal, hilo, autor, etc.', + reportsContentLocationPlaceholder: 'Enlace, ID, canal, hilo, autoría, etc.', reportsWhyInappropriatePlaceholder: 'Explica el motivo y el impacto.', reportsRequestedActionLabel: 'Acción solicitada', reportsRequestedActionPlaceholder: 'Eliminar, ocultar, marcar, advertir, etc.', tribesTitle: "Tribus", + tribeFilterAll: "TODAS", + tribeFilterRecent: "RECIENTES", + tribeFilterMine: "MÍAS", + tribeFilterMembership: "MEMBRESÍA", + tribeFilterSubtribes: "SUBTRIBUS", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALERÍA", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "MÍOS", + tribeFeedFilterALL: "TODOS", + tribeFeedFilterRECENT: "RECIENTES", + courtsStanceDENY: "Negar", + courtsStanceADMIT: "Admitir", + courtsStancePARTIAL: "Admitir en parte", tribeAllSectionTitle: "Tribus", tribeMineSectionTitle: "Tus Tribus", tribeCreateSectionTitle: "Crear Tribu", @@ -1938,24 +2040,15 @@ module.exports = { tribeGallerySectionTitle: "Galería de Tribus", tribeRecentSectionTitle: "Tribus Recientes", tribeTopSectionTitle: "Tribus Populares", - tribeviewTribeButton: "Visitar Tribu", - tribeviewSubTribeButton: "Visitar Sub-Tribu", tribeRootLabel: "RAÍZ", tribeDescription: "Explora o crea tribus en tu red.", - tribeFilterAll: "TODOS", - tribeFilterMine: "MÍAS", - tribeFilterMembership: "MIEMBROS", - tribeFilterRecent: "RECIENTES", - tribeFilterTop: "TOP", - tribeFilterSubtribes: "SUB-TRIBUS", - tribeFilterGallery: "GALERÍA", tribeMainTribeLabel: "TRIBU PRINCIPAL", tribeCreateButton: "Crear Tribu", tribeUpdateButton: "Actualizar", tribeDeleteButton: "Eliminar", - tribeImageLabel: "Subir contenido multimedia (max: 50MB)", + tribeImageLabel: "Subir contenido multimedia (máx.: 75MB)", tribeTitleLabel: "Título", - searchTribesPlaceholder: "FILTRAR tribus POR NOMBRE …", + searchTribesPlaceholder: "Buscar en Tribus...", tribeTitlePlaceholder: "Nombre de la tribu", tribeDescriptionLabel: "Descripción", tribeDescriptionPlaceholder: "Describe esta tribu", @@ -1967,13 +2060,17 @@ module.exports = { tribeModeLabel: "MODO", tribeIsAnonymousLabel: "ESTADO", tribeMembersCount: "Miembros", - tribeInviteCodePlaceholder: "Introduce el código de invitación", - tribeJoinByCodeButton: "Unirse con código", - tribeJoinButton: "UNIRSE", tribeEnterInvite: "INTRODUCIR INVITACIÓN", + subscriptionTitle: "Suscripciones", + subscriptionOn: "Suscrita", + subscriptionOff: "No suscrita", + subscriptionSubscribe: "Suscribirse", + subscriptionUnsubscribe: "Cancelar suscripción", + subscriptionSubscribers: "Suscriptoras", + pmListLabel: "Enviar a", + pmListNone: "— Sin lista —", + pmListNoSubscribers: "Sin suscriptoras", tribeLeaveButton: "DEJAR", - tribeYes: "SÍ", - tribeNo: "NO", tribePublic: "PÚBLICA", tribePrivate: "PRIVADA", tribeGenerateInvite: "INVITACIÓN ÚNICA", @@ -1982,52 +2079,42 @@ module.exports = { tribeRemoveInvitation: "ELIMINAR INVITACIÓN", tribeCreatedAt: "Creado el", tribeAuthor: "Por", - tribeAuthorLabel: "AUTOR", tribeStrict: "Estricto", tribeOpen: "Abierta", - tribeFeedFilterRECENT: "RECIENTES", - tribeFeedFilterMINE: "MIOS", - tribeFeedFilterALL: "TODOS", - tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Redifusiones", tribeFeedRefeed: "Redifundir", tribeFeedMessagePlaceholder: "Escribe un feed…", tribeFeedSend: "Enviar", - tribeFeedEmpty: "No hay mensajes de feed disponibles, aún.", - noTribes: "No se encontraron tribus, aún.", - tribeNotFound: "Tribu no encontrada!", + tribeFeedEmpty: "Aún no hay mensajes de feed disponibles.", + noTribes: "Aún no se han encontrado tribus.", + tribeNotFound: "¡Tribu no encontrada!", createTribeTitle: "Crear Tribu", updateTribeTitle: "Actualizar Tribu", - tribeSectionOverview: "Resumen", tribeSectionInhabitants: "Habitantes", tribeSectionVotations: "VOTACIONES", tribeSectionEvents: "EVENTOS", - tribeSectionReports: "Reportes", tribeSectionTasks: "TAREAS", tribeSectionFeed: "FEED", tribeSectionForum: "FORO", - tribeSectionMarket: "Mercado", - tribeSectionJobs: "Empleos", - tribeSectionProjects: "Proyectos", tribeSectionMedia: "Multimedia", tribeSectionImages: "IMÁGENES", tribeSectionAudios: "AUDIOS", tribeSectionVideos: "VÍDEOS", tribeSectionDocuments: "DOCUMENTOS", tribeSectionBookmarks: "MARCADORES", - tribeSectionMaps: "MAPS", + tribeSectionMaps: "MAPAS", tribeSectionPads: "PADS", tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", - tribeInhabitantsEmpty: "No hay habitantes en esta tribu, aún.", + tribeSectionCalendars: "CALENDARIOS", + tribePadCreate: "Crear pad", + tribeChatCreate: "Crear chat", + tribeCalendarCreate: "Crear calendario", + tribePadsEmpty: "Aún no hay pads.", + tribeChatsEmpty: "Aún no hay chats.", + tribeCalendarsEmpty: "Aún no hay calendarios.", + tribeInhabitantsEmpty: "Aún no hay habitantes en esta tribu.", tribeEventCreate: "Crear Evento", - tribeEventsEmpty: "No hay eventos, aún.", + tribeEventsEmpty: "Aún no hay eventos.", tribeEventTitle: "Título", tribeEventDescription: "Descripción", tribeEventDate: "Fecha", @@ -2036,7 +2123,7 @@ module.exports = { tribeEventUnattend: "NO ASISTIR", tribeEventAttendees: "Asistentes", tribeTaskCreate: "Crear Tarea", - tribeTasksEmpty: "No hay tareas, aún.", + tribeTasksEmpty: "Aún no hay tareas.", tribeTaskTitle: "Título", tribeTaskDescription: "Descripción", tribeTaskPriority: "Prioridad", @@ -2046,13 +2133,8 @@ module.exports = { tribeTaskStatusClosed: "CERRAR", tribeTaskAssign: "ASIGNAR", tribeTaskUnassign: "DESASIGNAR", - tribeReportCreate: "Crear Reporte", - tribeReportsEmpty: "No hay reportes, aún.", - tribeReportTitle: "Título", - tribeReportDescription: "Descripción", - tribeReportCategory: "Categoría", tribeVotationCreate: "Crear Votación", - tribeVotationsEmpty: "No hay votaciones, aún.", + tribeVotationsEmpty: "Aún no hay votaciones.", tribeVotationTitle: "Título", tribeVotationDescription: "Descripción", tribeVotationOptions: "Opciones", @@ -2061,59 +2143,37 @@ module.exports = { tribeVotationResults: "Votos", tribeVotationClose: "CERRAR VOTACIÓN", tribeVotationOptionPlaceholder: "Opción", - tribeForumCreate: "Crear Forum", - tribeForumEmpty: "No hay hilos, aún.", + tribeForumCreate: "Crear foro", + tribeForumEmpty: "Aún no hay hilos.", tribeForumTitle: "Título", tribeForumText: "Mensaje", tribeForumCategory: "Categoría", tribeForumReply: "Responder", tribeForumReplies: "Respuestas", - tribeMarketCreate: "Crear Anuncio", - tribeMarketEmpty: "No hay anuncios, aún.", - tribeMarketTitle: "Título", - tribeMarketDescription: "Descripción", - tribeMarketPrice: "Precio", - tribeMarketImage: "Imagen", - tribeMarketCategory: "Categoría", - tribeJobCreate: "Crear Empleo", - tribeJobsEmpty: "No hay empleos, aún.", - tribeJobTitle: "Título", - tribeJobDescription: "Descripción", - tribeJobLocation: "Ubicación", - tribeJobSalary: "Salario", - tribeJobDeadline: "Fecha límite", - tribeProjectCreate: "Crear Proyecto", - tribeProjectsEmpty: "No hay proyectos, aún.", - tribeProjectTitle: "Título", - tribeProjectDescription: "Descripción", - tribeProjectGoal: "Objetivo", - tribeProjectFunded: "Financiado", - tribeProjectDeadline: "Fecha límite", - tribeMediaUpload: "Subir Media", + tribeMediaUpload: "Subir multimedia", readDocument: "Leer Documento", tribeCreateImage: "Crear Imagen", tribeCreateAudio: "Crear Audio", - tribeCreateVideo: "Crear Video", + tribeCreateVideo: "Crear vídeo", tribeCreateDocument: "Crear Documento", tribeCreateBookmark: "Crear Marcador", - tribeMediaEmpty: "No hay media, aún.", + tribeMediaEmpty: "Aún no hay contenido multimedia.", tribeMediaTitle: "Título", tribeMediaDescription: "Descripción", - tribeMediaType: "Tipo", tribeMediaTypeImage: "Imagen", tribeMediaTypeVideo: "Vídeo", tribeMediaTypeAudio: "Audio", tribeMediaTypeDocument: "Documento", tribeMediaTypeBookmark: "Marcador", tribeContentDelete: "ELIMINAR", + tribeReachTribe: "TRIBU", + tribeReachNotice: "OASIS lo muestra a todos los habitantes y CLEARNET también en la web abierta. Abrirlo deja una copia legible fuera de la tribu: volver a TRIBU lo oculta, pero las copias ya compartidas no se pueden borrar.", + tribeReachPrivate: "El contenido de una tribu privada solo puede quedarse dentro de la tribu.", + tribeReachOpenedBy: "Abierto por", + tribeReachChange: "Dónde se publica", tribeInviteCodeText: "Código de invitación: ", oasisVersionLabel: "Versión de Oasis", - tribeInviteCodeHint: "Comparte este código con quien quieras invitar. Puede unirse desde /invites → Tribes.", - tribeGroupTribe: "Tribu", - tribeGroupOffice: "Oficina", - tribeGroupNetwork: "Red", - tribeGroupEconomy: "Economía", - tribeGroupMedia: "Multimedia", + tribeInviteCodeHint: "Comparte este código con quien quieras invitar. Puede unirse desde /invites.", tribeStatusOpen: "ABIERTO", tribeStatusClosed: "CERRADO", tribeStatusInProgress: "EN PROGRESO", @@ -2123,33 +2183,17 @@ module.exports = { tribePriorityCritical: "CRÍTICA", tribeTaskFilterAll: "TODOS", tribeMediaFilterAll: "TODOS", - tribeReportCatBug: "ERROR", - tribeReportCatAbuse: "ABUSO", - tribeReportCatContent: "CONTENIDO", - tribeReportCatOther: "OTRO", tribeForumCatGeneral: "GENERAL", tribeForumCatProposal: "PROPUESTA", tribeForumCatQuestion: "PREGUNTA", tribeForumCatAnnouncement: "ANUNCIO", - tribeMarketCatGoods: "BIENES", - tribeMarketCatServices: "SERVICIOS", - tribeMarketCatFood: "ALIMENTACIÓN", - tribeMarketCatOther: "OTRO", tribeStatusLabel: "Estado", tribeSubTribes: "SUB-TRIBUS", tribeSubTribesCreate: "Crear Sub-Tribu", - tribeSubTribesStrictDenied: "El modo estricto de la tribu no te permite generar nuevas sub-tribus. Ponte en contacto con el administrador.", - tribeSubTribesEmpty: "No se han creado sub-tribus, aún.", - tribeActivityJoined: "UNIDO", - tribeActivityLeft: "SALIDO", - tribeActivityFeed: "FEED", - tribeActivityRefeed: "REFEED", - tribeGroupAnalytics: "Analíticas", - tribeGroupCreative: "Creativo", + tribeSubTribesStrictDenied: "El modo estricto de la tribu no te permite generar nuevas sub-tribus. Ponte en contacto con la administradora.", + tribeActivityRefeed: "REDIFUSIÓN", tribeSectionActivity: "ACTIVIDAD", tribeSectionTrending: "TENDENCIAS", - tribeSectionOpinions: "OPINIONES", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "ETIQUETAS", tribeSectionSearch: "BUSCAR", tribeActivityEmpty: "Sin actividad aún.", @@ -2161,25 +2205,13 @@ module.exports = { tribeTrendingPeriodWeek: "Esta Semana", tribeTrendingPeriodAll: "Todo", tribeTrendingEngagement: "interacción", - tribeOpinionsEmpty: "Sin opiniones aún.", - tribeOpinionsCast: "Votar", - tribeOpinionsRankings: "Clasificaciones", - tribeOpinionsAlreadyVoted: "Ya has votado", tribeTopCategory: "Más votado", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "Lienzo colaborativo de pixel art.", - tribePixeliaPaint: "Pintar", - tribePixeliaContributors: "contribuidores", - tribePixeliaTotalPixels: "píxeles pintados", tribeTagsEmpty: "No se encontraron etiquetas.", - tribeTagsCloud: "Nube de Etiquetas", - tribeTagsContentWith: "Contenido con etiqueta", - tribeSearchPlaceholder: "Buscar contenido de la tribu...", tribeSearchEmpty: "Sin resultados.", - tribeSearchResults: "Resultados", tribeSearchMinChars: "Introduce al menos 2 caracteres para buscar.", agendaTitle: "Agenda", agendaDescription: "Aquí puedes encontrar todos tus elementos asignados.", + agendaSearchPlaceholder: "Buscar en Agenda...", agendaFilterAll: "TODOS", agendaFilterToday: "HOY", agendaFilterUpcoming: "PRÓXIMO", @@ -2193,23 +2225,15 @@ module.exports = { agendaFilterReports: "INFORMES", agendaFilterTransfers: "TRANSFERENCIAS", agendaFilterJobs: "TRABAJOS", + agendaFilterIndustry: "INDUSTRIA", + agendaFilterHousing: "VIVIENDA", agendaFilterProjects: "PROYECTOS", agendaFilterCalendars: "CALENDARIOS", agendaNoItems: "No se encontraron asignaciones.", agendaDiscardButton: "Descartar", agendaRestoreButton: "Restaurar", - agendaAuthor: "Por", - agendaCreatedAt: "Creado el", - agendaTitleLabel: "Título", + agendaRemoveButton: "Quitar", agendaMembersCount: "Miembros", - agendaDescriptionLabel: "Descripción", - agendaStatus: "Estado", - agendaVisibility: "Visibilidad", - agendaEventDate: "Fecha del Evento", - agendaEventLocation: "Ubicación", - agendaEventPrice: "Precio", - agendaEventUrl: "URL", - agendaTaskStart: "Hora de Inicio", agendaLocationLabel: "Ubicación", agendaYes: "SÍ", agendaNo: "NO", @@ -2219,11 +2243,6 @@ module.exports = { agendareportCategory: "Categoría", agendareportSeverity: "Gravedad", agendareportStatus: "Estado", - agendareportDescription: "Descripción", - agendaTaskEnd: "Hora de Fin", - agendaTaskPriority: "Prioridad", - agendaTransferFrom: "De", - agendaTransferTo: "A", agendaTransferConcept: "Concepto", agendaTransferAmount: "Cantidad", agendaTransferDeadline: "Plazo", @@ -2233,47 +2252,7 @@ module.exports = { voteNow: "Vota ahora", alreadyVoted: "Ya has opinado.", noOpinionsFound: "No se encontraron opiniones.", - RECENTButton: "RECIENTES", TOPButton: "TOP", - interestingButton: "INTERESANTE", - necessaryButton: "NECESARIO", - funnyButton: "DIVERTIDO", - disgustingButton: "ASQUEROSO", - sensibleButton: "SENSIBLE", - propagandaButton: "PROPAGANDA", - adultOnlyButton: "SOLO ADULTOS", - boringButton: "ABURRIDO", - confusingButton: "CONFUSO", - usefulButton: "UTIL", - informativeButton: "INFORMATIVO", - wellResearchedButton: "BIEN INVESTIGADO", - accurateButton: "CONCISO", - needsSourcesButton: "NECESITA FUENTES", - wrongButton: "INCORRECTO", - lowQualityButton: "BAJA CALIDAD", - creativeButton: "CREATIVO", - insightfulButton: "PERSPECTIVO", - actionableButton: "ACCIONABLE", - inspiringButton: "INSPIRADOR", - loveButton: "AMOR", - clearButton: "CLARO", - upliftingButton: "EMPODERANTE", - unnecessaryButton: "INNECESARIO", - rejectedButton: "RECHAZADO", - misleadingButton: "ENGAÑOSO", - offTopicButton: "FUERA DE TEMA", - duplicateButton: "DUPLICADO", - clickbaitButton: "CEBO DE CLICS", - spamButton: "SPAM", - trollButton: "TROLL", - nsfwButton: "NSFW", - violentButton: "VIOLENTO", - toxicButton: "TOXICO", - harassmentButton: "ACOSO", - hateButton: "ODIO", - scamButton: "ESTAFA", - triggeringButton: "PROVOCADOR", - opinionsCreatedAt: "Creado en", opinionsTotalCount: "Total de opiniones", voteInteresting: "Interesante", voteNecessary: "Necesario", @@ -2305,34 +2284,46 @@ module.exports = { voteScam: "Estafa", voteTriggering: "Provocador", voteInsightful: "Perspicaz", - voteAccurate: "Conciso", + voteAccurate: "Preciso", voteActionable: "Accionable", voteCreative: "Creativo", voteSpam: "Spam", voteAdultOnly: "Solo adultos", - publishBlog: "Publicar Blog", privateMessage: "MP", pmSendTitle: "Mensajes Privados", - pmSend: "Enviar!", - pmDescription: "Usa este formulario para enviar un mensaje cifrado a otros habitantes.", - pmRecipients: "Destinatarios", + pmSend: "¡Enviar!", + pmCancel: "Cancelar", + pmReset: "Restablecer", + pmSharedKeyHint: "Comparte esta clave con la destinataria por otro canal. Es necesaria para descifrar.", + pmDescription: "Usa este formulario para enviar un mensaje/fichero cifrado a otras habitantes.", + pmComposeTitle: "Enviar un mensaje", + pmRecipients: "Destinatarias", pmRecipientsHint: "Introduce los IDs de Oasis separados por comas", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "Hasta 7 habitantes por mensaje.", + pmTextPlaceholder: "Escribe tu mensaje.", pmSubject: "Asunto", pmSubjectHint: "Introduce el asunto del mensaje", pmText: "Mensaje", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Descifrar", + fileShareTitle: "Compartir un fichero", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "Fichero", + fileShareDownload: "Descargar", + fileShareMutualError: "Solo puedes compartir ficheros con habitantes que te apoyen mutuamente.", + fileShareNoFile: "No se ha seleccionado ningún fichero.", + fileShareTooLarge: "El fichero supera el tamaño permitido.", + fileShareFailed: "No se pudo preparar el fichero.", + fileShareSendError: "No se pudo enviar el fichero.", + fileShareUnavailable: "Este fichero no está disponible ahora mismo. Inténtalo más tarde.", pmCrypterBadKey: "¡La clave compartida es incorrecta!", pmCrypterTooLong: "El mensaje es demasiado largo para cifrarlo con el Crypter. Acórtalo e inténtalo de nuevo.", pmCrypterCipherLabel: "Mensaje recifrado", pmCrypterCharsLabel: "caracteres", pmInvalidRecipients: "Oasis ID no válido (debería ser como @…=.ed25519).", pmEncryptionLabel: "Cifrado", - pmFile: "Archivo adjunto", - private: "Privados", - privateDescription: "Tus mensajes cifrados.", + private: "Buzón", + privateDescription: "Gestiona tus mensajes privados.", privateInbox: "BUZÓN", privateSent: "ENVIADOS", privateDelete: "Borrar", @@ -2340,27 +2331,34 @@ module.exports = { noPrivateMessages: "No hay mensajes privados.", pmReply: "Responder", pmReplies: "respuestas", - pmNew: "nuevas", - pmMarkRead: "Marcar como leido", inReplyTo: "EN RESPUESTA A", + forumInThread: "EN EL HILO", pmPreview: "Previsualizar", pmPreviewTitle: "Vista previa", performed: "→", pmFromLabel: "De:", pmToLabel: "Para:", - pmInvalidMessage: "Mensaje no válido", pmNoSubject: "(sin asunto)", pmSubjectLabel: "Asunto:", - pmBodyLabel: "Cuerpo", - pmBotJobs: "42-EmpleosBOT", - pmBotProjects: "42-ProyectosBOT", - pmBotMarket: "42-MercadoBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "La casa gobernante del L.A.R.P ha cambiado.", + politicalBotParliamentTitle: "Ha comenzado un nuevo ciclo de gobierno en el Parlamento.", + politicalBotTribeTitle: "Ha comenzado un nuevo ciclo de gobierno en una de tus Tribus.", + politicalBotLarpIntro: "Ahora gobierna {house} durante este ciclo: {cycle}", + politicalBotParliamentIntro: "El gobierno actual es {gov}", + politicalBotParliamentIntroLeader: "El gobierno actual es {gov} ejercida por {leader}", + politicalBotTribeIntro: "El gobierno actual en la Tribu {tribe} es {gov}", + politicalBotTribeIntroLeader: "El gobierno actual en la Tribu {tribe} es {gov} ejercida por {leader}", + politicalBotLeaderLabel: "Líder", inboxJobSubscribedTitle: "Nueva suscripción a tu oferta de trabajo", pmInhabitantWithId: "Habitante con ID OASIS:", pmHasSubscribedToYourJobOffer: "se ha suscrito a tu oferta de trabajo", - inboxProjectCreatedTitle: "Nuevo proyecto creado", - pmHasCreatedAProject: "ha creado un proyecto", inboxMarketItemSoldTitle: "Artículo vendido", + inboxShopSoldTitle: "Producto vendido", pmYourItem: "Tu artículo", pmHasBeenSoldTo: "se ha vendido a", pmFor: "por", @@ -2368,52 +2366,40 @@ module.exports = { pmHasPledged: "ha aportado", pmToYourProject: "a tu proyecto", blockchain: 'Explorador', - blockchainTitle: 'Explorador', blockchainDescription: 'Explora y visualiza los bloques de la cadena.', blockchainNoBlocks: 'No se encontraron bloques en la cadena.', blockchainStatsTitle: 'Estadísticas', blockchainStatsTypeBreakdown: 'Desglose por tipo', - blockchainStatsTopAuthors: 'Autores principales', + blockchainStatsTopAuthors: 'Autoras principales', blockchainStatsCount: 'Bloques', blockchainBlockID: 'ID del Bloque', blockchainBlockAuthor: 'Autoría', blockchainBlockCarbon: 'Huella de carbono', blockchainBlockType: 'Tipo', blockchainBlockTimestamp: 'Marca de tiempo', - blockchainBlockContent: 'Bloque', - blockchainBlockURL: 'URL:', - blockchainContent: 'Bloque', - blockchainContentPreview: 'Vista previa del contenido del bloque', blockchainLatestDatagram: 'Último Datagrama', - blockchainDatagram: 'Datagrama', - blockchainDetails: 'Ver detalles del bloque', - blockchainViewBlockexplorer: "Ver en el blockexplorer", - blockchainBlockInfo: 'Información del Bloque', - blockchainBlockDetails: 'Detalles del bloque seleccionado', + blockchainDatagram: "Datagrama", + blockchainDetails: "Ver Detalles del Bloque", + blockchainViewBlockexplorer: "Vista en Blockexplorer", blockchainBack: 'Volver al Blockexplorer', - banking: 'Banking', - bankingTitle: 'Banking', - bankingDescription: 'Explora el valor actual de ECOin y la asignación de RBU correspondiente, distribuida semanalmente en función de la participación y la confianza.', + banking: "Banca", + bankingTitle: "Banca", + bankingDescription: "La economía colectiva de ECOin.", bankOverview: 'Resumen', bankEpochs: 'Épocas', bankRules: 'Reglas', pending: 'Pendientes', closed: 'Cerradas', - bankBack: 'Volver a Banking', - bankViewTx: 'Ver Tx', bankClaimNow: 'Reclamar', - bankClaimUBI: '¡Reclamar RBU!', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'No hay asignaciones RBU pendientes para esta época.', + bankClaimUBI: 'Reclamar RBU', + bankClaimAndPay: "Reclamar y pagar", + bankClaimedPending: "Reclamación pendiente...", + bankStatusClaimed: "Reclamado", + bankStatusExpired: "Caducado", + bankPubOnly: "Operación solo para PUBs", bankEpoch: 'Época', bankPool: 'Fondo (esta época)', bankWeightsSum: 'Suma de pesos', - bankAllocations: 'Asignaciones', bankNoAllocations: 'No se encontraron asignaciones.', bankNoEpochs: 'No se encontraron épocas.', bankEpochAllocations: 'Asignaciones de la época', @@ -2427,16 +2413,21 @@ module.exports = { bankEpochId: 'ID de época', bankRuleHash: 'Hash del snapshot de reglas', bankViewEpoch: 'Ver época', + bankYourIndustryBalance: "Tu parte de la industria", + bankYourUbiMonth: "Tu RBU (este mes)", + bankYourFundsMonth: "Fondos totales", + bankIndustryBalance: "Producción industrial", bankUserBalance: 'Tu saldo', - ecoWalletNotConfigured: 'Cartera de ECOin no configurada', - editWallet: 'Editar cartera', - addWallet: 'Añadir cartera', bankAddresses: 'Direcciones', bankNoAddresses: 'No se encontraron direcciones.', bankUser: 'ID de Oasis', bankAddress: 'Dirección', bankAddAddressTitle: 'Añadir dirección de ECOIN', bankAddAddressUser: 'ID de Oasis', + bankAddAddressLabel: "Etiqueta (opcional)", + bankAddAddressLabelPlaceholder: "Cartera de Bob, tienda de Alice…", + bankAddAddressUserOptional: "Oasis ID (opcional)", + bankAddressUnnamed: "Dirección sin nombre", bankAddAddressAddress: 'Dirección de ECOIN', bankAddAddressSave: 'Guardar', bankAddressAdded: 'Dirección añadida', @@ -2452,41 +2443,26 @@ module.exports = { bankAddressDelete: 'Eliminar', bankAddressSource: 'Fuente', bankAddressDeleteConfirm: 'Eliminar esta dirección?', - search: 'Buscar!', + search: "¡Buscar!", bankLocal: 'Local', - bankFromOasis: 'Oasis', - bankMyAddress: 'Tu dirección', - bankRemoveMyAddress: 'Eliminar mi dirección', - bankNotRemovableOasis: 'Las direcciones no se pueden eliminar localmente', - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "Red", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", - bankUbiAvailableNo: "SIN FONDOS!", + bankUbiAvailableNo: "¡SIN FONDOS!", bankUbiAvailableOk: "¡DISPONIBLE!", - bankUbiAvailability: "Disponibilidad de UBI", + bankUbiAvailability: "RBU", bankAlreadyClaimedThisMonth: "Ya reclamado este mes", - bankUbiThisMonth: "UBI (este mes)", - bankUbiLastClaimed: "UBI (última reclamada)", + bankUbiThisMonth: "RBU (este mes)", + bankUbiLastClaimed: "RBU (última reclamada)", bankUbiNeverClaimed: "Nunca reclamado", - bankUbiTotalClaimed: "UBI (total reclamado)", + bankUbiTotalClaimed: "RBU (total reclamado)", bankUbiPub: "PUB", bankUbiInhabitant: "HABITANTE", bankUbiClaimedAmount: "RECLAMADO (ECO)", - typeBankUbiResult: "BANCARIO - UBI", + typeBankUbiResult: "BANCA - RBU", bankNoPubConfigured: "Sin PUB configurado. Establece tu ID de PUB en Ajustes.", shopsTitle: "Tiendas", shopDescription: "Descubre y gestiona tiendas en la red.", shopTitle: "Tienda", - shopFilterAll: "TODAS", - shopFilterMine: "MÍAS", - shopFilterRecent: "RECIENTES", - shopFilterTop: "TOP", - shopFilterProducts: "PRODUCTOS", - shopFilterPrices: "PRECIOS", - shopFilterFavorites: "FAVORITAS", shopUpload: "Crear Tienda", shopAllSectionTitle: "Tiendas", shopMineSectionTitle: "Mis Tiendas", @@ -2503,19 +2479,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Publicar en Clearnet", - shopClearnetUnpublish: "Quitar de Clearnet", - shopClearnetUrlLabel: "URL en Clearnet", - shopClearnetWarning: "Publicar en Clearnet hace que esta tienda sea indexable por buscadores y rastreadores externos.", + transportChipLabel: "Cifrado en tránsito", shopAddFavorite: "Añadir Favorito", shopRemoveFavorite: "Quitar Favorito", shopOpen: "ABIERTA", shopClosed: "CERRADA", - shopOpenShop: "Abrir Tienda", - shopCloseShop: "Cerrar Tienda", shopMakePrivate: "HACER PRIVADA (E2E)", shopMakePublic: "HACER PÚBLICA", shopProducts: "Productos", + shopFilterAll: "TODAS", + shopFilterRecent: "RECIENTES", + shopFilterMine: "MÍAS", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUCTOS", + shopFilterPrices: "PRECIOS", + shopFilterFavorites: "FAVORITOS", shopProductAdd: "Añadir Producto", shopProductTitle: "Producto", shopProductUpdate: "Actualizar Producto", @@ -2527,7 +2505,7 @@ module.exports = { shopNoProducts: "Aún no hay productos.", shopOutOfStock: "Agotado", shopBuy: "Comprar", - shopBuyEncryptedNote: "Tus datos de entrega se envían cifrados; solo el dueño de la tienda puede verlos.", + shopBuyEncryptedNote: "Tus datos de entrega se envían cifrados; solo la dueña de la tienda puede verlos.", shopBuyDeliveryAddress: "Dirección de entrega", shopBuyContact: "Contacto", shopBuyContactPlaceholder: "correo, teléfono, etc.", @@ -2538,41 +2516,38 @@ module.exports = { shopOrdersEmpty: "Aún no hay pedidos.", shopOrderProduct: "Producto", shopOrderPrice: "Precio", - shopOrderBuyer: "Comprador", + shopOrderBuyer: "Compradora", shopBackToShop: "Volver a la Tienda", - shopShareUrl: "URL para compartir", - shopVisitShop: "VISITAR TIENDA", marketVisitItem: "VER ARTÍCULO", shopStatus: "ESTADO", shopCreatedAt: "CREADO", - shopSearchPlaceholder: "Buscar tiendas...", + shopSearchPlaceholder: "Buscar en Tiendas...", shopUrl: "URL", shopLocation: "Ubicación", shopTags: "Etiquetas", shopVisibility: "Visibilidad", - shopImage: "Imagen", - shopShortDescription: "Descripcion Corta", - shopShortDescriptionPlaceholder: "Descripcion breve para las tarjetas (max 160 caracteres)", + shopShortDescription: "Descripción corta", + shopShortDescriptionPlaceholder: "Descripción breve para las tarjetas (máx. 160 caracteres)", shopTitlePlaceholder: "Nombre de tu tienda", - shopDescriptionPlaceholder: "Descripcion detallada de tu tienda", - shopUrlPlaceholder: "https://url-de-tu-tienda.com", - shopLocationPlaceholder: "Ciudad, Pais", - shopTagsPlaceholder: "etiqueta1, etiqueta2, etiqueta3", - mapTitlePlaceholder: "Titulo del mapa", + shopDescriptionPlaceholder: "Descripción detallada de tu tienda", + shopLocationPlaceholder: "Ciudad, País", + shopTagsPlaceholder: "Introduce etiquetas separadas por comas", + mapTitlePlaceholder: "Título del mapa", shopProductFeatured: "Producto Destacado", - shopSendToMarket: "Send to Market", + shopSendToMarket: "Enviar al mercado", typeShop: "TIENDAS", typeShopProduct: "PRODUCTO DE TIENDA", bankExchange: 'Intercambio', bankExchangeCurrentValue: 'Valor de ECOin (1h)', - bankTotalSupply: 'Suministro Total de ECOin', + bankTotalSupply: "Suministro ECOin total", + bankPubsSupply: "Suministro ECOin del pozo de los PUBs", + bankHoldingSupply: "Suministro ECOin fuera de la RBU", bankHoursOfWork: 'horas', bankUnitMs: "ms", bankUnitSeconds: "segundos", bankUnitMinutes: "minutos", bankUnitDays: "días", bankExchangeNoData: 'No hay datos disponibles', - bankExchangeIndex: 'Valor de ECOin (1h)', bankInflation: 'Inflación de ECOin (anual)', bankInflationMonthly: 'Inflación de ECOin (mensual)', bankExchangeChartTitle: 'Progresión del valor de ECOin a lo largo del tiempo', @@ -2580,44 +2555,25 @@ module.exports = { bankExchangeChartSupply: 'Suministro', bankExchangeChartInflation: 'Inflación %', bankExchangeChartEmpty: 'Aún no hay muestras suficientes — vuelve más tarde', - bankCurrentSupply: 'Suministro Actual de ECOin', + bankCurrentSupply: "Suministro ECOin actual", bankingSyncStatus: 'Estado de ECOin', bankingSyncStatusSynced: 'Sincronizado', - bankingSyncStatusOutdated: 'Desactualizado', + bankingSyncStatusOutdated: "Sin sincronizar", statsTitle: 'Estadísticas', statistics: "Estadísticas", - statsInhabitant: "Estadísticas del Habitante", statsDescription: "Descubre estadísticas sobre tu red.", ALLButton: "TODO", + RECENTButton: "RECIENTES", MINEButton: "MÍAS", TOMBSTONEButton: "ELIMINADOS", - statsYou: "Tú", - statsUserId: "ID de Oasis", statsCreatedAt: "Creado el", statsYourContent: "Contenido", statsYourOpinions: "Opiniones", - statsYourTombstone: "Eliminados", - statsNetwork: "Red", - statsTotalInhabitants: "Habitantes", - statsDiscoveredTribes: "Tribus (Públicas)", - statsPrivateDiscoveredTribes: "Tribus (Privadas)", statsNetworkContent: "Contenido", - statsYourMarket: "Mercado", - statsYourJob: "Empleos", - statsYourProject: "Proyectos", - statsYourTransfer: "Transferencias", - statsYourForum: "Foros", statsNetworkOpinions: "Opiniones", - statsDiscoveredMarket: "Mercado", - statsDiscoveredJob: "Empleos", - statsDiscoveredProject: "Proyectos", - statsBankingTitle: "Banca", - statsEcoWalletLabel: "Billetera ECOIN", + statsEcoWalletLabel: "Cartera ECOin", + walletAddressCopyHint: "Pulsa para seleccionar y copiar", statsEcoWalletNotConfigured: "¡No configurada!", - statsTotalEcoAddresses: "Direcciones totales", - statsDiscoveredTransfer: "Transferencias", - statsDiscoveredForum: "Foros", - statsNetworkTombstone: "Eliminados", statsBookmark: "Marcadores", statsEvent: "Eventos", statsTask: "Tareas", @@ -2625,8 +2581,8 @@ module.exports = { statsMarket: "Mercado", statsForum: "Foros", statsJob: "Empleos", - statsReport: "Reportes", - statsFeed: "Publicaciones", + statsReport: "Informes", + statsFeed: "Feed", statsTribe: "Tribus", statsImage: "Imágenes", statsAudio: "Audios", @@ -2638,17 +2594,15 @@ module.exports = { statsTransfer: "Transferencias", statsAiExchange: "IA", statsPUBs: 'PUBs', + statsSyncedPeers: "Nodos", + statsTotalLabel: "Total", statsPost: "Publicaciones", - statsOasisID: "ID de Oasis", statsSize: "Total (tamaño)", statsBlockchainSize: "Blockchain (tamaño)", statsBlobsSize: "Blobs (tamaño)", statsActivity7d: "Actividad (últimos 7 días)", statsActivity7dTotal: "Total 7 días", statsActivity30dTotal: "Total 30 días", - statsKarmaScore: "Puntuación KARMA", - statsPublic: "Público", - statsPrivate: "Privado", day: "Día", messages: "Mensajes", statsProject: "Proyectos", @@ -2660,30 +2614,14 @@ module.exports = { statsProjectsCancelled: "Cancelados", statsProjectsGoalTotal: "Meta total", statsProjectsPledgedTotal: "Aportación total", - statsProjectsSuccessRate: "Tasa de éxito", - statsProjectsAvgProgress: "Progreso medio", - statsProjectsMedianProgress: "Progreso mediano", - statsProjectsActiveFundingAvg: "Financiación activa media", - statsJobsTitle: "Empleos", - statsJobsTotal: "Total de empleos", - statsJobsOpen: "Abiertos", - statsJobsClosed: "Cerrados", - statsJobsOpenVacants: "Vacantes abiertas", - statsJobsSubscribersTotal: "Suscriptores totales", - statsJobsAvgSalary: "Salario medio", - statsJobsMedianSalary: "Salario mediano", statsMarketTitle: "Mercado", statsMarketTotal: "Artículos totales", statsMarketForSale: "En venta", statsMarketReserved: "Reservados", statsMarketClosed: "Cerrados", statsMarketSold: "Vendidos", - statsMarketRevenue: "Ingresos", - statsMarketAvgSoldPrice: "Precio medio vendido", statsUsersTitle: "Habitantes", user: "Habitante", - statsTombstoneTitle: "Eliminados", - statsNetworkTombstones: "Eliminados en la red", statsTombstoneRatio: "Ratio de eliminados (%)", bankingUserEngagementScore: "Puntuación de KARMA", statsParliamentCandidature: "Candidaturas de parlamento", @@ -2698,43 +2636,40 @@ module.exports = { statsCourtsSettlement: "Acuerdos judiciales", statsCourtsSettlementProposal: "Propuestas de acuerdo", statsCourtsSettlementAccepted: "Acuerdos aceptados", - statsCourtsNomination: "Nominaciones de jueces", + statsCourtsNomination: "Nominaciones de juezas", statsCourtsNominationVote: "Votos de nominación", ai: "IA", aiTitle: "IA", aiDescription: "Una Inteligencia Artificial Colectiva (IAC) llamada '42' que aprende de tu red.", aiUserQuestion: "Pregunta", aiResponseTitle: "Respuesta", - aiSubmitButton: "Enviar!", + aiSubmitButton: "¡Enviar!", aiSettingsDescription: "Configura tu prompt (máx 128 caracteres) para el modelo de IA.", aiPrompt: "Proporciona una respuesta informativa y precisa.", aiConfiguration: "Configurar prompt", aiPromptUsed: "Indicación", - aiClearHistory: "Borrar historial de chat", - aiSharePrompt: "Añadir esta respuesta al entrenamiento colectivo?", - aiShareYes: "Sí", - aiShareNo: "No", - aiSharedLabel: "Añadida al entrenamiento", - aiRejectedLabel: "No añadida al entrenamiento", - aiServerError: "La IA no ha podido responder. Inténtalo de nuevo.", + aiClearHistory: "Borrar historial", + aiServerError: "No puedo responder bien. Inténtalo de nuevo.", + aiStatusLoading: "42 está cargando el modelo. Pregunta de nuevo en un momento.", + aiThinking: "Pensando", + aiWaiting: "Esperando", + aiStatusMissingModel: "El modelo de IA no está instalado: falta src/AI/oasis-42-1-chat.Q4_K_M.gguf.", + aiExportFineTuning: "Exportar Fine-Tuning", + aiFactsTitle: "Datos de tu red", aiInputPlaceholder: "¿Qué es Oasis?", typeAiExchange: "IA", aiApproveTrain: "Añadir al entrenamiento colectivo", aiRejectTrain: "No entrenar", - aiTrainPending: "Pendiente de aprobación", aiTrainApproved: "Aprobado para entrenamiento", aiTrainRejected: "Rechazado para entrenamiento", aiSnippetsUsed: "Fragmentos usados", aiSnippetsLearned: "Fragmentos aprendidos", statsAITraining: "Entrenamiento de IA", - aiApproveCustomTrain: "Entrenar con esta respuesta personalizada", aiCustomAnswerPlaceholder: "Escribe tu respuesta personalizada…", - statsAIExchanges: "Intercambio de Modelos", marketMineSectionTitle: "Tus artículos", marketCreateSectionTitle: "Crear artículo", marketUpdateSectionTitle: "Actualizar", marketAllSectionTitle: "Mercado", - marketRecentSectionTitle: "Mercado reciente", marketTitle: "Mercado", marketDescription: "Un mercado para intercambiar bienes o servicios en tu red.", marketFilterAll: "TODOS", @@ -2747,9 +2682,9 @@ module.exports = { marketFilterForSale: "EN VENTA", marketFilterSold: "VENDIDO", marketFilterDiscarded: "DESCARTADO", - marketFilterRecent: "RECIENTE", + marketFilterRecent: "RECIENTES", marketFilterMyBids: "PUJAS", - marketCreateButton: "Crear artículo", + marketCreateButton: "Vender algo", marketItemType: "Tipo", marketItemTitle: "Título", marketItemAvailable: "Fecha límite", @@ -2764,42 +2699,101 @@ module.exports = { marketItemDeadline: "Fecha límite", marketItemIncludesShipping: "¿Incluye envío?", marketItemHighestBid: "Puja más alta", - marketItemHighestBidder: "Mejor postor", marketItemStock: "Existencias", marketOutOfStock: "Sin stock", - marketItemBidTime: "Fecha de la puja", marketActionsUpdate: "Actualizar", marketUpdateButton: "Actualizar", marketActionsDelete: "Eliminar", - marketActionsSold: "Marcar como vendido", marketActionsChangeStatus: "CAMBIAR ESTADO", marketActionsBuy: "¡COMPRAR!", marketAuctionBids: "Pujas actuales", marketPlaceBidButton: "Pujar", - marketItemSeller: "Vendedor", + marketItemSeller: "Vendedora", marketNoItems: "Aún no hay artículos disponibles.", marketYourBid: "Tu puja", - marketCreateFormImageLabel: "Subir contenido multimedia (max: 50MB)", - marketSearchLabel: "Buscar", - marketSearchPlaceholder: "Buscar por título o etiquetas", + marketCreateFormImageLabel: "Subir contenido multimedia (máx.: 75MB)", + marketSearchPlaceholder: "Buscar en Mercado...", marketMinPriceLabel: "Precio mínimo", marketMaxPriceLabel: "Precio máximo", - marketSortLabel: "Ordenar por", marketSortRecent: "Más recientes", marketSortPrice: "Precio", marketSortDeadline: "Fecha límite", marketSearchButton: "Buscar", marketAuctionEndsIn: "Termina", marketAuctionEnded: "Terminó", - marketMyBidBadge: "Has pujado", marketNoItemsMatch: "No hay artículos que coincidan con tu búsqueda.", + housingTitle: "Vivienda", + housingDescriptionText: "Descubre y gestiona lugares en tu red.", + modulesHousingLabel: "Vivienda", + modulesHousingDescription: "Módulo para descubrir y gestionar lugares.", + typeHousing: "VIVIENDA", + housingFilterAll: "TODO", + housingFilterMine: "MÍOS", + housingFilterRequested: "SOLICITADOS", + housingFilterSale: "VENTA", + housingFilterRent: "ALQUILER", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "DISPONIBLES", + housingFilterClosed: "NO DISPONIBLES", + housingFilterRecent: "RECIENTES", + housingFilterTop: "TOP", + housingType: "Oferta", + housingTypeSALE: "En venta", + housingTypeRENT: "En alquiler", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Inmueble", + housingPropertyAPARTMENT: "Piso", + housingPropertyHOUSE: "Casa", + housingPropertyROOM: "Habitación", + housingPropertyLAND: "Terreno", + housingPropertyOTHER: "Otro", + housingTitleLabel: "Título", + housingTitlePlaceholder: "Nombre del lugar", + housingDescription: "Descripción", + housingDescriptionPlaceholder: "Describe el lugar, el barrio, qué incluye: baños, jardín, camas…", + housingRules: "Normas", + housingRulesPlaceholder: "Mascotas, fumar, horas de silencio, estancia mínima…", + housingPlace: "Lugar", + housingPlacePlaceholder: "País, ciudad, dirección", + housingPrice: "Precio", + housingPriceLabel: "Precio (ECO)", + housingSizeLabel: "Superficie (m²)", + housingFree: "GRATIS", + housingPerMonth: "/mes", + housingRooms: "Habitaciones", + housingSize: "Superficie", + housingCapacity: "Huéspedes", + housingAvailableFrom: "Disponible desde", + housingAvailableTo: "Disponible hasta", + housingTags: "Etiquetas", + spreadEditWarning: "Este contenido perderá sus réplicas al editarlo", + housingRequests: "Solicitudes", + housingRequestButton: "Solicitar", + housingCancelButton: "Cancelar solicitud", + housingRequestedBadge: "SOLICITADO", + housingStatus: "Estado", + housingStatusOPEN: "DISPONIBLE", + housingStatusCLOSED: "NO DISPONIBLE", + housingSetOpen: "Marcar disponible", + housingSetClosed: "Marcar no disponible", + housingCreateButton: "Crear lugar", + housingUpdateButton: "Actualizar", + housingSearchPlaceholder: "Buscar en Vivienda...", + housingSearchButton: "Buscar", + housingMinPrice: "Precio mín.", + housingMaxPrice: "Precio máx.", + housingSortRecent: "Más recientes", + housingSortPrice: "Precio más bajo", + housingSortRequests: "Más solicitados", + housingSortRating: "Mejor valorados", + housingNoItems: "No se han encontrado lugares.", jobsTitle: "Laboral", - jobsCandidatesTitle: "Candidatos sugeridos", + jobsCandidatesTitle: "Candidatas sugeridas", jobsCandidatesDescription: "Habitantes cuyas competencias encajan con este empleo. Envíales un mensaje privado para invitarles.", jobsCandidatesPmBody: "Hola, creo que tu perfil encaja con mi oferta laboral", jobsDescription: "Descubre y gestiona ofertas de trabajo en tu red.", jobsFilterRecent: "RECIENTES", - jobsFilterMine: "MIOS", + jobsFilterMine: "MÍOS", jobsFilterAll: "TODOS", jobsFilterRemote: "REMOTOS", jobsFilterOpen: "ABIERTOS", @@ -2815,10 +2809,10 @@ module.exports = { jobsCVTitle: "CVs", jobsFilterPresencial: "PRESENCIAL", jobsFilterFreelancer: "FREELANCE", - jobsFilterEmployee: "EMPLEADO", + jobsFilterEmployee: "EMPLEADA", jobsPresencialTitle: "Trabajos Presenciales", jobsFreelancerTitle: "Trabajos Freelance", - jobsEmployeeTitle: "Trabajos de Empleado", + jobsEmployeeTitle: "Trabajos por cuenta ajena", jobTitle: "Título", jobLocation: "Ubicación", jobSalary: "Salario (ECO/1h)", @@ -2830,8 +2824,8 @@ module.exports = { jobStatusOPEN: "ABIERTA", jobStatusCLOSED: "CERRADA", jobSetClosed: "Marcar como CERRADA", - jobSubscribeButton: "Unirse a esta oferta!", - jobUnsubscribeButton: "Salir de esta oferta!", + jobSubscribeButton: "¡Unirse a esta oferta!", + jobUnsubscribeButton: "¡Salir de esta oferta!", jobTitlePlaceholder: "Introduce el título del trabajo", jobDescriptionPlaceholder: "Describe el puesto", jobRequirementsPlaceholder: "Introduce los requisitos", @@ -2840,21 +2834,19 @@ module.exports = { jobLocationPresencial: "Presencial", jobLocationRemote: "Remoto", jobVacantsPlaceholder: "Número de vacantes", - jobSalaryPlaceholder: "Salario en ECO por 1 hora dedicada", - jobImage: "Subir contenido multimedia (max: 50MB)", + jobImage: "Subir contenido multimedia (máx.: 75MB)", jobTasks: "Tareas", jobType: "Tipo de Trabajo", jobTime: "Tiempo de Trabajo", - jobSubscribers: "Suscriptores", - noSubscribers: "Sin suscriptores", + jobSubscribers: "Suscriptoras", + noSubscribers: "Sin suscriptoras", jobsFilterTop: "TOP", jobsTopTitle: "Trabajos Mejor Pagados", createJobButton: "Publicar Trabajo", - viewDetailsButton: "Ver Detalles", noJobsFound: "No se encontraron ofertas de trabajo.", jobAuthor: "Por", jobTypeFreelance: "Autónomo", - jobTypeSalary: "Empleado", + jobTypeSalary: "Empleada", jobTypeExchange: "Intercambio de horas", jobTypeEXCHANGE: "INTERCAMBIO DE HORAS", jobsFilterExchange: "INTERCAMBIO", @@ -2862,42 +2854,208 @@ module.exports = { jobsHoursOffered: "Horas ofrecidas", jobsHoursRequested: "Horas pedidas a cambio", jobsExchangeSkill: "Habilidad solicitada a cambio", - jobsHoursOfferedPlaceholder: "p. ej. 4", - jobsHoursRequestedPlaceholder: "p. ej. 4", - jobsExchangeSkillPlaceholder: "p. ej. carpintería, diseño", - jobExchangeHint: "Para trabajos de intercambio de horas, rellena los campos siguientes en vez del salario.", jobTimePartial: "Parcial", jobTimeComplete: "Completo", - jobsDeleteButton: "ELIMINAR", jobsUpdateButton: "ACTUALIZAR", jobsFilterApplied: "CANDIDATURA", jobsAppliedTitle: "Mis candidaturas", - jobsAppliedBadge: "Inscrito", - jobsFilterFavs: "Favoritos", - jobsFavsTitle: "Favoritos", - jobsFilterNeeds: "Necesita ayuda", - jobsNeedsTitle: "Necesita ayuda", - jobsSearchLabel: "Buscar", - jobsSearchPlaceholder: "Buscar por título, tags, descripción...", + jobsAppliedBadge: "Inscrita", + jobsSearchPlaceholder: "Buscar en Laboral...", jobsMinSalaryLabel: "Salario mín.", jobsMaxSalaryLabel: "Salario máx.", - jobsSortLabel: "Ordenar por", jobsSortRecent: "Más recientes", jobsSortSalary: "Mayor salario", - jobsSortSubscribers: "Más candidatos", + jobsSortSubscribers: "Más candidatas", jobsSearchButton: "Buscar", - jobsFavoriteButton: "Favorito", - jobsUnfavoriteButton: "Quitar favorito", - jobsMessageAuthorButton: "MP", - jobsApplicants: "Candidatos", + jobsApplicants: "Candidatas", jobsUpdatedAt: "Actualizado", jobSetOpen: "Marcar abierto", - jobNewBadge: "NUEVO", jobsTagsLabel: "Etiquetas", - jobsTagsPlaceholder: "etiqueta1, etiqueta2, etiqueta3", noJobsMatch: "No hay ofertas que coincidan con tu búsqueda.", + industrySearchPlaceholder: "Buscar en Industria...", + industryAllSectors: "Todos los sectores", + industryVisitButton: "Visitar Industria", + industryAdvanceTo: "Cambiar a", + industryAmount: "Cantidad", + industryApproveBuild: "Aprobar", + industryBlueprint: "Plano", + industryBlueprints: "Planos", + industryBuild: "Producción", + industryBuildNotes: "Notas", + industryBuildStart: "Fecha de inicio", + industryVoteUpdateButton: "Votar actualización", + industryVoteDeleteButton: "Votar borrado", + industryRevokeVote: "Revocar", + industryTimeLeft: "Tiempo restante", + industryBuildEnd: "Fecha de fin", + industryBuilds: "Producciones", + industryBuildTitle: "Título", + industryContribute: "Contribuir", + industryContributeButton: "Contribuir", + industryContributions: "Contribuciones", + industryCreateBlueprintButton: "Crear blueprint", + industryDistribute: "Distribuir", + industryDistributeButton: "Distribuir por cuotas", + industryDistribution: "Distribución", + industryEcoAmount: "Cantidad (ECO)", + industryEditBlueprint: "Editar blueprint", + industryFacility: "Fábrica", + industryKindLabel: "Tipo", + industryKind_labor: "Mano de obra", + industryKind_material: "Material", + industryKind_eco: "Fondos", + industryLaborHours: "Mano de obra (horas)", + industryHours: "Horas", + industryMaterialItem: "Material", + industryMaterials: "Materiales", + industryEstMaterials: "Total materiales", + industryEstLabor: "Total mano de obra", + industryEstTotal: "Precio estimado", + industryBuildingPrice: "Precio de construcción", + industryFinalPrice: "Precio final", + industryBlueprintDescPlaceholder: "Especificaciones, dimensiones, peso, documentación…", + industryMaterialValue: "Valor (ECO)", + industryMember: "Miembro", + industryNewBlueprint: "Nuevo blueprint", + industryNewBuild: "Proponer un build", + industryEditBuild: "Editar producción", + industryNeedBlueprint: "Crea primero un plano: cada producción fabrica uno.", + industryNoBlueprints: "Aún no hay blueprints.", + industryNoBuilds: "Aún no hay builds.", + industryNote: "Nota", + industryOutput: "Salida", + industryOutputKind: "Tipo de producto", + industryOutputValue: "Valor de salida (ECO, p. ej. de la venta)", + industryPostJob: "Crear Trabajo", + industryPot: "Bote", + industryProposeBuildButton: "Proponer build", + industryProposer: "Proponente", + industryAuthor: "Autora", + industrySellOnMarket: "Enviar al Mercado", + industrySendToProjects: "Crear Proyecto", + industrySkills: "Habilidades", + industryTreasury: "Tesorería", + industryKind_physical: "Físico", + industryKind_digital: "Digital", + industryBuildStatus_PROPOSED: "PROPUESTO", + industryBuildStatus_REJECTED: "RECHAZADO", + industryBuildStatus_APPROVED: "APROBADO", + industryBuildStatus_STOCKING: "ABASTECIENDO", + industryBuildStatus_IN_PRODUCTION: "EN PRODUCCIÓN", + industryBuildStatus_COMPLETED: "COMPLETADO", + industryBuildStatus_FAILED: "FALLIDO", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Un empleo encaja con tu currículum", + jobsBotMatchIntro: "Un empleo encaja con tu currículum.", + jobsBotMatchJob: "Empleo", + jobsBotMatchHint: "Recibes esto porque tu currículum está gestionado por IA. Puedes desactivarlo en tu CV.", + cvAiManaged: "Gestionado por IA", + switchOn: "ACTIVADO", + switchOff: "DESACTIVADO", + cvAiManagedHint: "Si está activo, JobsBot te avisa por mensaje privado cuando un empleo encaja con tu currículum.", + cvMatchThreshold: "Avisarme a partir de este nivel de coincidencia", + housingBotRequestedTitle: "Alguien ha solicitado uno de tus lugares.", + housingBotCancelledTitle: "Se ha cancelado una solicitud sobre uno de tus lugares.", + housingBotYouRequestedTitle: "Has solicitado un lugar.", + housingBotYouCancelledTitle: "Has cancelado una solicitud.", + housingBotUnavailableTitle: "Un lugar que solicitaste ya no está disponible.", + housingBotUnavailableSentTitle: "Has avisado a quienes solicitaron este lugar.", + industryBotAdmittedTitle: "Has sido admitido en una fábrica.", + industryBotApplicationTitle: "Nueva solicitud de ingreso en tu fábrica.", + industryBotInvitedTitle: "Has sido invitado a una fábrica.", + industryBotDissolvedTitle: "Una fábrica a la que perteneces ha sido disuelta.", + industryBotBuildApprovedTitle: "Un build ha sido aprobado.", + industryBotDistributedTitle: "Se ha distribuido la salida de un build.", + industryFilterRules: "REGLAS", + industryRulesTitle: "Reglas", + industryRulesIntro: "Industry permite a las habitantes poseer colectivamente los medios de producción: una Fábrica es un taller de la red que nadie posee de forma privada.", + industryRulesSteward: "La fundadora es la mayordoma — una custodia, no una dueña: no puede vender, privatizar ni disolver la Fábrica en solitario.", + industryRulesMembership: "La política de membresía define cómo se unen los habitantes: Abierta (entras al instante), Por voto (te admite el voto de los miembros) o Solo invitación (un miembro debe invitarte antes).", + industryRulesQuorum: "El quórum es el número mínimo de miembros que deben votar para que una decisión cuente, para que una Fábrica pequeña no la decida un solo habitante.", + industryRulesMajority: "La mayoría es la fracción de votos necesaria para aprobar (0,5 = mitad, 1 = unanimidad); una decisión pasa cuando los SÍ alcanzan al menos max(quórum, miembros × mayoría).", + industryRulesDecisions: "Los miembros votan para admitir nuevas integrantes, aprobar builds y disolver la Fábrica; la mayordoma no puede saltarse estas decisiones colectivas.", + industryRulesBlueprints: "Los Blueprints son recetas libres (COPYLEFT) — las entradas (materiales, horas de trabajo, skills) y la salida (un bien físico o digital) — y cualquiera puede forkearlas.", + industryRulesBuilds: "Un Build es una orden de producción: PROPUESTO → APROBADO (por voto) → ABASTECIENDO → EN PRODUCCIÓN → COMPLETADO, y solo acepta contribuciones una vez aprobado.", + industryRulesContributions: "Los miembros aportan trabajo (horas), materiales (valor declarado en ECO) o ECO a un build; cada aporte gana karma.", + industryRulesLaborRate: "La tarifa de trabajo es el valor en ECO de una hora de trabajo en esta Fábrica; convierte horas en karma para comparar esfuerzo y capital de forma justa: karma = horas × tarifa + valor material + ECO.", + industryRulesShares: "Tu cuota en un build es tu karma ÷ karma total, y determina cuánto del valor de salida recibes.", + industryRulesDistribution: "Cuando una producción se completa, el steward reparte el fondo (los fondos de la producción más el valor de venta) entre quienes contribuyeron, en proporción a su participación.", + industryRulesTreasury: "Las aportaciones en ECO las custodia la mayordoma como tesorería del build hasta el reparto; todos los movimientos quedan registrados en la red y las disputas pueden ir a Courts.", + industryJobs: "Empleos", + industryReportButton: "Reportar", + industryCreateTask: "Crear Tarea", + industryOpenDispute: "Abrir disputa", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "Generar invitación", + industryPauseVotes: "Votos de estado", + industryTitle: "Industria", + industryDescription: "Módulo para gestionar los medios de producción de forma colectiva.", + typeIndustryBuild: "PRODUCCIÓN", + typeIndustryBlueprint: "PLANO", + typeIndustryAllocation: "DISTRIBUCIÓN", + typeIndustry: "INDUSTRIA", + modulesIndustryLabel: "Industria", + modulesIndustryDescription: "Módulo para gestionar los medios de producción de forma colectiva.", + industryFilterAll: "TODAS", + industryFilterRecent: "RECIENTES", + industryFilterMember: "MIEMBRO", + industryFilterBlueprints: "PLANOS", + industryFilterBuilds: "PRODUCCIONES", + industryFilterMine: "MÍAS", + industryFilterActive: "EN MARCHA", + industryFilterPaused: "PAUSADAS", + industryFilterDissolved: "DISUELTAS", + industryAllTitle: "Industria", + industryRecentTitle: "Fábricas recientes", + industryMemberTitle: "Fábricas en las que estoy", + industryMineTitle: "Mis fábricas", + industryActiveTitle: "En marcha", + industryPausedTitle: "Fábricas pausadas", + industryDissolvedTitle: "Fábricas disueltas", + industryNoFacilitiesFound: "No se encontraron fábricas.", + industryCreateFacility: "Crear fábrica", + industryMembers: "Miembros", + industryMemberBadge: "MIEMBRO", + industryName: "Nombre", + industryNamePlaceholder: "Nombre de la fábrica", + industryDescriptionLabel: "Descripción", + industryDescriptionPlaceholder: "¿Qué produce esta fábrica?", + industrySector: "Sector", + industryMembershipPolicy: "Política de membresía", + industryQuorum: "Quórum", + industryMajority: "Mayoría", + industryLaborRate: "Tarifa de trabajo", + industryCreateButton: "Crear fábrica", + industryUpdateButton: "Actualizar", + industrySteward: "Mayordoma", + industryStatusActive: "EN MARCHA", + industryStatusPaused: "PAUSADA", + industryStatusDissolved: "DISUELTA", + industryStatusLabel: "Estado", + industryJoinButton: "Unirse", + industryApplyButton: "Solicitar ingreso", + industryLeaveButton: "Salir", + industryInviteButton: "Invitar", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Necesitas una invitación para unirte.", + industryPauseButton: "Votar pausar", + industryResumeButton: "Votar reanudar", + industryDeleteButton: "Eliminar", + industryPendingApplicants: "Solicitudes pendientes", + industryVotesYes: "sí", + industryVotesNo: "no", + industryAlreadyVoted: "votado", + industryAdmit: "Admitir", + industryReject: "Rechazar", + industryDissolveTitle: "Disolver fábrica", + industryDissolveVote: "Votar disolución", + industryPolicy_open: "Abierta", + industryPolicy_vote: "Por voto", + industryPolicy_invite: "Solo invitación", projectsTitle: "Proyectos", projectsDescription: "Crea, financia y sigue proyectos impulsados por la comunidad en tu red.", + projectSearchPlaceholder: "Buscar en Proyectos...", projectCreateProject: "Crear Proyecto", projectCreateButton: "Crear Proyecto", projectUpdateButton: "ACTUALIZAR", @@ -2906,7 +3064,7 @@ module.exports = { projectFilterAll: "TODOS", projectFilterMine: "MIS PROYECTOS", projectFilterActive: "ACTIVOS", - projectFilterPaused: "PONDERADOS", + projectFilterPaused: "PAUSADOS", projectFilterCompleted: "COMPLETADOS", projectFilterFollowing: "SEGUIDOS", projectFilterRecent: "RECIENTES", @@ -2920,7 +3078,7 @@ module.exports = { projectRecentTitle: "Proyectos Recientes", projectTopTitle: "Mejor Financiados", projectTitlePlaceholder: "Nombre del proyecto", - projectImage: "Subir contenido multimedia (max: 50MB)", + projectImage: "Subir contenido multimedia (máx.: 75MB)", projectDescription: "Descripción", projectDescriptionPlaceholder: "Cuenta la historia y los objetivos…", projectGoal: "Meta (ECO)", @@ -2941,14 +3099,8 @@ module.exports = { projectPledgeTitle: "Apoya este proyecto", projectPledgePlaceholder: "Cantidad en ECO", projectBounties: "Recompensas", - projectBountiesInputLabel: "Recompensas (una por línea: Título|Cantidad [ECO]|Descripción)", - projectBountiesPlaceholder: "Arreglar error en UI|100|Enlace al problema\nEscribir documentación|250|Ejemplos de uso", projectNoBounties: "No se encontraron recompensas.", projectTitle: "Título", - projectAddBountyTitle: "Añadir nueva recompensa", - projectBountyTitle: "Título de la recompensa", - projectBountyAmount: "Cantidad (ECO)", - projectBountyDescription: "Descripción", projectMilestoneSelect: "Seleccionar hito", projectBountyCreateButton: "Crear recompensa", projectBountyStatus: "Estado de la recompensa", @@ -2963,23 +3115,19 @@ module.exports = { projectMilestoneTitle: "Título del hito", projectMilestoneTargetPercent: "Porcentaje (%)", projectMilestoneDueDate: "Fecha", - projectMilestoneCreateButton: "Crear hito", projectMilestoneStatus: "Estado del hito", projectMilestoneOpen: "abierto", projectMilestoneDone: "completado", projectMilestoneDue: "debido", - projectNoMilestones: "No se encontraron hitos.", projectMilestoneMarkDone: "Marcar como hecho", projectMilestoneTitlePlaceholder: "Ingresa el título del hito", projectMilestoneDescriptionPlaceholder: "Ingresa la descripción de este hito", projectMilestoneDescription: "Descripción del hito", - projectBudgetGoal: "Presupuesto (Objetivo)", projectBudgetAssigned: "Asignado a recompensas", - projectBudgetRemaining: "Restante", projectBudgetOver: "⚠ Presupuesto excedido: lo asignado supera el objetivo", - projectFollowers: "Seguidores", - projectFollowersTitle: "Seguidores", - projectFollowersNone: "Aún no hay seguidores.", + projectFollowers: "Seguidoras", + projectFollowersTitle: "Seguidoras", + projectFollowersNone: "Aún no hay seguidoras.", projectMore: "más", projectYouFollowHint: "Sigues este proyecto", projectBackers: "Patrocinadores", @@ -2988,7 +3136,6 @@ module.exports = { projectBackersTotalPledged: "Total aportado", projectBackersYourPledge: "Tu aportación", projectBackersNone: "Aún no hay aportaciones.", - projectNoRemainingBudget: "No queda presupuesto.", projectFilterBackers: "MECENAS", projectFilterApplied: "APORTANDO", projectAppliedTitle: "APORTANDO", @@ -2997,41 +3144,24 @@ module.exports = { projectBackerAmount: "Total aportado", projectBackerPledges: "Aportaciones", projectBackerProjects: "Proyectos", - projectPledgeAmount: "Cantidad", projectSelectMilestoneOrBounty: "Seleccionar Hito o Recompensa", projectPledgeButton: "Aportar", footerLicense: "GPLv3", - footerPackage: "Paquete", - footerVersion: "Versión", modulesModuleName: "Nombre", modulesModuleDescription: "Descripción", modulesModuleStatus: "Estado", - modulesTotalModulesLabel: "Módulos Cargados", + modulesTotalModulesLabel: "Total", modulesEnabledModulesLabel: "Habilitados", modulesDisabledModulesLabel: "Deshabilitados", - modulesPopularLabel: "Popular", - modulesPopularDescription: "Módulo para recibir publicaciones que son tendencia, más vistas o más comentadas.", - modulesTopicsLabel: "Temas", - modulesTopicsDescription: "Módulo para recibir categorías de discusión basadas en intereses compartidos.", - modulesSummariesLabel: "Resúmenes", - modulesSummariesDescription: "Módulo para recibir resúmenes de discusiones o publicaciones largas.", - modulesLatestLabel: "Últimos", - modulesLatestDescription: "Módulo para recibir las publicaciones y discusiones más recientes.", - modulesThreadsLabel: "Hilos", - modulesThreadsDescription: "Módulo para recibir conversaciones agrupadas por tema o pregunta.", - modulesMultiverseLabel: "Multiverso", - modulesMultiverseDescription: "Módulo para recibir contenido de otros pares federados.", modulesInvitesLabel: "Invitaciones", modulesInvitesDescription: "Módulo para gestionar y aplicar códigos de invitación.", modulesWalletLabel: "Cartera", modulesWalletDescription: "Módulo para gestionar tus activos digitales (ECOin).", - modulesLegacyLabel: "Legado", - modulesLegacyDescription: "Módulo para gestionar tu secreto (clave privada) de forma rápida y segura.", modulesCipherLabel: "Cifrado", - modulesCipherDescription: "Módulo para encriptar y desencriptar tu texto de forma simétrica (usando una contraseña compartida).", + modulesCipherDescription: "Módulo para cifrar y descifrar tu texto de forma simétrica (usando una contraseña compartida).", modulesBookmarksLabel: "Marcadores", modulesBookmarksDescription: "Módulo para descubrir y gestionar marcadores.", - modulesVideosLabel: "Videos", + modulesVideosLabel: "Vídeos", modulesVideosDescription: "Módulo para descubrir y gestionar videos.", modulesDocsLabel: "Documentos", modulesDocsDescription: "Módulo para descubrir y gestionar documentos.", @@ -3057,7 +3187,7 @@ module.exports = { modulesVotationsDescription: "Módulo para descubrir y gestionar votaciones.", modulesParliamentLabel: "Parlamento", modulesParliamentDescription: "Módulo para elegir gobiernos y votar leyes.", - modulesCourtsLabel: "Juzgados", + modulesCourtsLabel: "Tribunales", modulesCourtsDescription: "Módulo para resolver conflictos y emitir veredictos.", modulesReportsLabel: "Informes", modulesReportsDescription: "Módulo para gestionar y hacer un seguimiento de informes relacionados con problemas, errores, abusos y advertencias de contenido.", @@ -3065,8 +3195,12 @@ module.exports = { modulesOpinionsDescription: "Módulo para descubrir y votar opiniones.", modulesTransfersLabel: "Transferencias", modulesTransfersDescription: "Módulo para descubrir y gestionar contratos inteligentes (transferencias).", - modulesFediverseLabel: "Fediverse", - modulesFediverseDescription: "Gestiona tus otras cuentas del fediverso, incluyendo enviar y recibir contenido.", + modulesFediverseLabel: "Multiverso", + modulesBlogsLabel: "Blogs", + modulesBlogsDescription: "Módulo para descubrir y gestionar blogs.", + modulesPollsLabel: "Encuestas", + modulesPollsDescription: "Módulo para preguntar a la red y contar las respuestas.", + modulesFediverseDescription: "Gestiona tus otras cuentas del multiverso, incluyendo enviar y recibir contenido.", modulesFeedLabel: "Feed", modulesFeedDescription: "Módulo para descubrir y compartir textos breves (feeds).", modulesPixeliaLabel: "Pixelia", @@ -3075,72 +3209,76 @@ module.exports = { modulesMelodyDescription: "Módulo para generar y compartir el \"sonido\" de tu blockchain.", modulesAgendaLabel: "Agenda", modulesAgendaDescription: "Módulo para gestionar todos tus elementos asignados.", - modulesAILabel: "AI", + modulesAILabel: "IA", modulesAIDescription: "Módulo para hablar con un LLM llamado '42'.", modulesForumLabel: "Foros", modulesForumDescription: "Módulo para descubrir y gestionar foros.", modulesJobsLabel: "Trabajos", - modulesJobsDescription: "Modulo para descubrir y gestionar ofertas de trabajo.", + modulesJobsDescription: "Módulo para descubrir y gestionar ofertas de trabajo.", modulesProjectsLabel: "Proyectos", modulesProjectsDescription: "Módulo para explorar, financiar y gestionar proyectos.", modulesBankingLabel: "Banca", - modulesBankingDescription: "Módulo para conocer el valor real de ECOIN y distribuir una RBU utilizando la tesorería común.", + modulesBankingDescription: "Módulo para gestionar el tesoro común: valor de ECOIN, impuestos y RBU.", modulesFavoritesLabel: "Favoritos", modulesFavoritesDescription: "Módulo para gestionar tu contenido favorito.", fileTooLargeTitle: "Archivo demasiado grande", - fileTooLargeMessage: "El archivo supera el tamaño máximo permitido (50 MB). Por favor, selecciona un archivo más pequeño.", + fileTooLargeMessage: "El archivo supera el tamaño máximo permitido (75 MB). Por favor, selecciona un archivo más pequeño.", goBack: "Volver", + errorDismiss: "OK", errorPageTitle: "Ha ocurrido un error", + errorNoAllocation: "No se ha encontrado ninguna asignación para esta reclamación.", + errorInvalidClaim: "Esta asignación no se puede reclamar.", visibilityLabel: "Visibilidad", + clearnetPrivateConflict: "Un contenido privado, oculto, cerrado, solo por invitación o de pago no se puede publicar en la CLEARNET. Elige OASIS o hazlo público.", visibilityPublic: "Pública", visibilityHidden: "Oculta", visibilityMakePublic: "Hacer pública", visibilityMakeHidden: "Hacer oculta", invitesInhabitantsTitle: "Habitantes", - invitesInhabitantsFollow: "Seguir", - aiNavPlaceholder: "¿A dónde quieres ir?", + aiNavPlaceholder: "Describe qué necesitas...", aiNavDisabled: "La navegación con IA está desactivada.", - aiNavResultsTitle: "Sugerencias de navegación", + aiNavResultsTitle: "Sugerencias AINav", aiNavQueryLabel: "Consulta", - aiNavResultsDescription: "Elige la ruta que mejor se ajuste a lo que buscas.", - aiNavResultPath: "Ruta", - aiNavResultDescription: "Qué puedes hacer", aiNavResultMatch: "Coincidencia", aiNavResultsEmpty: "No hay rutas coincidentes. Prueba con /search.", - aiNavSearchInstead: "Buscar en su lugar", modulesAINavLabel: "AINav", modulesAINavDescription: "Módulo para realizar peticiones en lenguaje natural sobre el contenido de la red.", - directConnect: "Conexión Directa", directConnectDescription: "Conéctate directamente a un nodo introduciendo su dirección IP, puerto y clave pública. El nodo se añadirá como conexión seguida.", peerHost: "IP", - peerPort: "Puerto (por defecto: 8008)", + peerPort: "Puerto", peerPublicKey: "Clave Pública (@...ed25519)", connectAndFollow: "Conectar", - deviceSourceLabel: "Dispositivo", modulesPresetTitle: "Configuraciones Comunes", - modulesPreset_minimal: "Mínimo", - modulesPreset_basic: "Básico", - modulesPreset_social: "Social", - modulesPreset_economy: "Economía", - modulesPreset_full: "Completo", + workflowsTitle: "Flujos de trabajo", + workflowsDescription: "Un workflow fija el tema y qué módulos están activos.", + workflowsSet: "Aplicar workflow", + workflow_default: "Por defecto", + workflow_jobs: "Empleo", + workflow_social: "Social", + workflow_business: "Negocios", + workflow_night: "Noche", + workflow_kids: "Niños", + workflow_activists: "Activismo", + workflow_mobile: "Móvil", statsCarbonFootprintTitle: "Huella de Carbono", - statsCarbonFootprintNetwork: "Huella de carbono de la red", - statsCarbonFootprintYours: "Tu huella de carbono", - statsCarbonTombstone: "Huella del tombstoning", - feedSuccessMsg: "¡Feed publicado correctamente!", - dominantOpinionLabel: "Opinión predominante", - uploadMedia: "Subir contenido multimedia (max: 50MB)", + statsCarbonTombstone: "Huella de los elementos eliminados (tombstoning)", + uploadMedia: "Subir contenido multimedia (máx.: 75MB)", invitesUnfollow: "Dejar de seguir", invitesFollow: "Seguir", invitesUnfollowedInvites: "No federadas", invitesNoUnfollowed: "No hay pubs no federados.", reportsWhyInappropriateLabel: "¿Por qué es inapropiado?", blockchainContentDeleted: "Este contenido ha sido eliminado", - visitContent: "Visitar contenido", - bankEcoinHours: "Equivalencia ECOin en tiempo", + visitContent: "Visitar Contenido", + favoriteAdd: "Fijar en Favoritos", + pmContentTooltip: "Enviar un Mensaje Privado", + phoneCallTooltip: "Hacer una Llamada", + favoriteRemove: "Quitar de Favoritos", + reportContent: "Reportar Contenido", + bankEcoinHours: "Equivalencia ECOin en tiempo (1 ECO)", mapsLabel: "Mapas", mapTitle: "Mapas", - mapDescription: "Explora y gestiona mapas offline en tu red.", + mapDescription: "Gestiona y comparte mapas offline en tu red.", mapMineSectionTitle: "Tus Mapas", mapCreateSectionTitle: "Crear Mapa", mapUpdateSectionTitle: "Actualizar Mapa", @@ -3164,29 +3302,39 @@ module.exports = { mapDescriptionLabel: "Descripción", mapDescriptionPlaceholder: "Describe el mapa o ubicación...", mapTypeLabel: "Tipo de mapa", - mapTypeSingle: "ÚNICO (solo ubicación inicial)", - mapTypeOpen: "ABIERTO (cualquiera añade marcadores)", - mapTypeClosed: "CERRADO (solo el creador añade marcadores)", + mapInviteMode: "Invitación", + mapTypeSingle: "ÚNICO", + mapTypeOpen: "ABIERTO", + mapTypeClosed: "CERRADO", mapTagsLabel: "Etiquetas", mapTagsPlaceholder: "Introduce etiquetas separadas por comas", mapUrlLabel: "URL del mapa", - mapPickCoordLabel: "O selecciona una ubicación en la cuadrícula:", - mapMarkersLabel: "marcadores", mapMarkersTitle: "Marcadores", mapMarkerDefault: "Marcador", mapMarkerLatLabel: "Latitud del marcador", mapMarkerLngLabel: "Longitud del marcador", mapMarkerLabelField: "Etiqueta del marcador", mapMarkerLabelPlaceholder: "Describe este marcador...", - markerImageLabel: "Imagen del Marker", + markerImageLabel: "Imagen del marcador", mapAddMarkerTitle: "Añadir Marcador", mapAddMarkerButton: "Añadir Marcador", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "Limpiar marcador", mapApplyZoom: "Aplicar Zoom", - mapSearchPlaceholder: "Buscar descripción, etiquetas, autor...", + mapPlaceSearchPlaceholder: "Buscar un lugar o país...", + mapNoPlaces: "No se encontraron lugares.", + mapClusterTitle: "marcadores aquí, pulsa para acercar", + mapDeleteMarker: "Eliminar marcador", + mapZoomIn: "Acercar", + mapZoomOut: "Alejar", + mapFitMarkers: "Ajustar a todos los marcadores", + mapPanLabel: "Mover el mapa", + mapCenterHere: "Centrar aquí", + mapNear: "cerca de", + mapPickedSpot: "Punto elegido", + mapPickHint: "Pulsa en el mapa para elegir un punto.", + mapClearPick: "Limpiar", + mapSearchPlaceholder: "Buscar en Mapas...", mapSearchButton: "Buscar", - mapClickToCreate: "Haz click en el mapa para seleccionar ubicación", - mapClickToAddMarker: "Haz click en el mapa para añadir un marcador", mapUpdatedAt: "Actualizado", mapNoMatch: "Ningún mapa coincide con tu búsqueda.", noMaps: "No hay mapas disponibles.", @@ -3196,101 +3344,656 @@ module.exports = { typeMapMarker: "MARCADOR DE MAPA", modulesMapLabel: "Mapas", modulesMapDescription: "Módulo para gestionar y compartir mapas offline.", + emergenciesTitle: "Emergencias", + modulesEmergenciesLabel: "Emergencias", + modulesEmergenciesDescription: "Módulo para lanzar, confirmar y seguir emergencias en la red.", + emergenciesDescription: "Módulo para lanzar, confirmar y seguir emergencias en la red.", + emergencyCreate: "Crear emergencia", + emergencyUpdate: "Actualizar", + emergencyDelete: "Borrar", + emergencyTitleLabel: "Título", + emergencyTitlePlaceholder: "¿Qué está pasando?", + emergencyTextLabel: "Detalles", + emergencyTextPlaceholder: "Dónde, desde cuándo, qué hacer...", + emergencyCategoryLabel: "Categoría", + emergencyCategoryWeather: "METEOROLOGÍA", + emergencyCategoryInfrastructure: "INFRAESTRUCTURA", + emergencyCategoryHealth: "SALUD", + emergencyCategorySecurity: "SEGURIDAD", + emergencyCategoryLost: "PERDIDOS", + emergencyCategoryNeighborhood: "VECINDARIO", + emergencyExpiresLabel: "Caduca en", + emergencyExpires1d: "1 día", + emergencyExpires3d: "3 días", + emergencyExpires7d: "1 semana", + emergencyExpires30d: "1 mes", + emergencyExpiresNone: "Indefinido", + emergencyExpiresKeep: "Mantener la fecha actual", + emergencyTagsLabel: "Etiquetas", + emergencyTagsPlaceholder: "Introduce etiquetas separadas por comas", + emergencyFilterAll: "TODAS", + emergencyFilterMine: "MÍAS", + emergencyFilterRecent: "RECIENTES", + emergencySeverityUnverified: "SIN VERIFICAR", + emergencySeverityLow: "BAJA", + emergencySeverityMedium: "MEDIA", + emergencySeverityHigh: "ALTA", + emergencyStatusActive: "ACTIVA", + emergencyStatusResolved: "RESUELTA", + emergencyStatusExpired: "CADUCADA", + emergencyConfirmations: "Confirmaciones", + emergencyConfirmButton: "Confirmar", + emergencyConfirmed: "Has confirmado esta emergencia", + emergencyResolveButton: "Resolver", + emergencyReopenButton: "Reabrir", + emergencyGenerateReport: "Generar informe", + emergencyUpdateEdited: "Editada", + emergencyLastUpdate: "ÚLTIMA ACTUALIZACIÓN", + emergencyUpdatesTitle: "Últimas actualizaciones:", + emergencyNoUpdates: "Aún no hay actualizaciones.", + emergencyAddUpdate: "Publicar actualización", + emergencyUpdatePlaceholder: "¿Qué ha cambiado?", + emergencyNoItems: "No se han encontrado emergencias.", + emergencyExpiresAt: "Caduca", + emergencyBannerLabel: "Emergencia activa", + emergencyCreateSectionTitle: "Crear emergencia", + emergencyUpdateSectionTitle: "Actualizar emergencia", + emergencySearchPlaceholder: "Buscar en emergencias...", + typeEmergency: "EMERGENCIAS", + typeEmergencyConfirm: "CONFIRMACIÓN DE EMERGENCIA", + typeEmergencyUpdate: "ACTUALIZACIÓN DE EMERGENCIA", + statsEmergency: "Emergencias", + statsEmergencyConfirm: "Confirmaciones de emergencias", + statsEmergencyUpdate: "Actualizaciones de emergencias", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "Una emergencia que sigues tiene una actualización.", + emergencyBotResolvedTitle: "Una emergencia que sigues se ha resuelto.", + dataKindEmergencies: "EMERGENCIAS", + mailingTitle: "Listas de correo", + modulesMailingLabel: "Listas de correo", + modulesMailingDescription: "Descubre y gestiona listas de correo en tu red.", + modulesPhoneLabel: "Teléfono", + modulesPhoneDescription: "Módulo para llamar a otros habitantes con videollamadas cifradas de extremo a extremo.", + mailingDescription: "Descubre y gestiona listas de correo en tu red.", + mailingCreate: "Crear lista de correo", + mailingUpdate: "Actualizar", + mailingArchive: "Archivar", + mailingActivate: "Activar", + mailingCreateSectionTitle: "Crear lista de correo", + mailingUpdateSectionTitle: "Actualizar lista de correo", + mailingTitleLabel: "Título", + mailingTitlePlaceholder: "Nombre de la lista de correo", + mailingDescriptionLabel: "Descripción", + mailingDescriptionPlaceholder: "¿De qué trata esta lista?", + mailingTypeLabel: "Tipo de lista de correo", + mailingTypeOpen: "ABIERTA", + mailingTypeClosed: "CERRADA", + mailingStatusLabel: "Estado", + mailingStatusActive: "ACTIVA", + mailingStatusArchived: "ARCHIVADA", + mailingMembersLabel: "Invitar habitantes", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Suscriptoras", + mailingTagsLabel: "Etiquetas", + mailingTagsPlaceholder: "Introduce etiquetas separadas por comas", + mailingFilterAll: "TODAS", + mailingFilterMine: "MÍAS", + mailingFilterSubscribed: "SUSCRITAS", + mailingFilterRecent: "RECIENTES", + mailingHistoryDate: "FECHA", + mailingHistoryThreads: "HILOS", + mailingHistoryInhabitants: "HABITANTES", + mailingHistorySearchPlaceholder: "Buscar en el historial...", + mailingNoMessages: "Aún no hay mensajes.", + mailingNoItems: "No se han encontrado listas de correo.", + mailingSearchPlaceholder: "Buscar en listas de correo...", + mailingWriteButton: "Escribir mensaje", + mailingWriteTitle: "Escribir a la lista", + mailingReplyTitle: "Responder a la lista", + mailingSend: "Enviar", + mailingTextPlaceholder: "Tu mensaje llega a todas las participantes de la lista.", + mailingParticipants: "Participantes", + inboxFilterMailing: "Listas de correo", + mailingReplyPlaceholder: "Responder a la lista...", + mailingReplyToList: "Responder a la lista", + typeMailingList: "LISTA DE CORREO", + statsMailingList: "Listas de correo", + mailingListLabel: "LISTA DE CORREO", + logisticsTitle: "Logística", + modulesLogisticsLabel: "Logística", + modulesLogisticsDescription: "Descubre y gestiona planes logísticos en tu red.", + logisticsDescription: "Descubre y gestiona planes logísticos en tu red.", + logisticsCreate: "Crear ruta", + logisticsUpdate: "Actualizar", + logisticsClose: "Cerrar", + logisticsReopen: "Reabrir", + logisticsCreateSectionTitle: "Crear ruta", + logisticsUpdateSectionTitle: "Actualizar ruta", + logisticsTitleLabel: "Título", + logisticsTitlePlaceholder: "Viaje al mercado, furgoneta disponible...", + logisticsKindLabel: "Tipo de logística", + logisticsKindTrip: "VIAJE", + logisticsKindShipment: "ENVÍO", + logisticsKindTrips: "VIAJES", + logisticsKindShipments: "ENVÍOS", + logisticsModeLabel: "Modo", + logisticsModeOffer: "OFREZCO", + logisticsModeRequest: "BUSCO", + logisticsModeOffers: "OFERTAS", + logisticsModeRequests: "PETICIONES", + logisticsDescriptionLabel: "Descripción", + logisticsDescriptionPlaceholder: "Punto de encuentro, qué llevas, condiciones...", + logisticsOriginLabel: "Origen", + logisticsOriginPlaceholder: "Dónde empieza", + logisticsDestinationLabel: "Destino", + logisticsDestinationPlaceholder: "Dónde termina", + logisticsDateLabel: "Fecha y hora", + logisticsRecurrenceLabel: "Recurrencia", + logisticsFrequencyLabel: "Frecuencia", + logisticsRecurrenceNone: "ÚNICO", + logisticsRecurrenceDaily: "DIARIO", + logisticsRecurrenceWeekly: "SEMANAL", + logisticsRecurrenceMonthly: "MENSUAL", + logisticsSeatsLabel: "Plazas", + logisticsSizeLabel: "Tamaño", + logisticsSizePlaceholder: "p. ej. 2m x 1m", + logisticsWeightLabel: "Peso", + logisticsWeightPlaceholder: "p. ej. 40kg", + logisticsPriceFree: "GRATIS", + logisticsPriceEco: "ECO", + logisticsPriceTime: "TIEMPO", + logisticsPriceLabel: "Precio", + logisticsHours: "h", + logisticsOrderRefLabel: "Enlace al pedido", + logisticsOrderRefPlaceholder: "/shops/... o /market/... del pedido a entregar", + logisticsOrderLink: "Pedido enlazado", + logisticsTagsLabel: "Etiquetas", + logisticsTagsPlaceholder: "Introduce etiquetas separadas por comas", + logisticsFilterAll: "TODAS", + logisticsFilterMine: "MÍAS", + logisticsFilterBooked: "RESERVADAS", + logisticsFilterRecent: "RECIENTES", + logisticsFilterHistory: "HISTORIAL", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "ABIERTA", + logisticsStatusUpcoming: "PRÓXIMAS", + logisticsStatusPast: "PASADAS", + logisticsStatusClosed: "CERRADAS", + logisticsZoneLabel: "Zona", + logisticsSearchPlaceholder: "Buscar en logística...", + logisticsNoItems: "No se han encontrado rutas.", + logisticsBookingPending: "PENDIENTE", + logisticsBookingConfirmed: "CONFIRMADA", + logisticsBookingRejected: "RECHAZADA", + logisticsBookingCancelled: "CANCELADA", + logisticsBookingDelivered: "ENTREGADA", + logisticsConfirm: "Confirmar", + logisticsReject: "Rechazar", + logisticsDeliver: "Entregar", + logisticsReceiptLabel: "Recibo", + logisticsReceiptPlaceholder: "Nota de entrega (opcional)", + logisticsCancelBooking: "Cancelar reserva", + logisticsBookTitle: "Reservar esta ruta", + logisticsNotesLabel: "Notas", + logisticsNotesPlaceholder: "Lo que el responsable deba saber", + logisticsBook: "Reservar", + logisticsBookingsTitle: "Reservas", + logisticsMyBookingTitle: "Tu reserva", + logisticsRatingsTitle: "Valoraciones", + logisticsNoRatings: "Aún no hay valoraciones.", + logisticsRatingsWhenClosed: "Las valoraciones se abren al cerrar la ruta.", + logisticsScoreLabel: "Puntuación", + logisticsRatingPlaceholder: "¿Qué tal fue?", + logisticsRate: "Valorar", + typeLogisticsRoute: "RUTA", + typeLogisticsRating: "VALORACIÓN DE RUTA", + statsLogisticsRoute: "Rutas", + statsLogisticsRating: "Valoraciones de rutas", + logisticsRouteLabel: "RUTA", + podcastsTitle: "Podcasts", + modulesPodcastsLabel: "Podcasts", + modulesPodcastsDescription: "Módulo para publicar canales de podcast.", + podcastsDescription: "Módulo para publicar canales de podcast.", + podcastCreate: "Crear podcast", + podcastUpdate: "Actualizar", + podcastDelete: "Borrar", + podcastCreateSectionTitle: "Crear podcast", + podcastUpdateSectionTitle: "Actualizar podcast", + podcastTitleLabel: "Título", + podcastTitlePlaceholder: "Nombre del canal", + podcastDescriptionLabel: "Descripción", + podcastDescriptionPlaceholder: "¿De qué trata este podcast?", + podcastCategoryLabel: "Categoría", + podcastCategoryNews: "NOTICIAS", + podcastCategoryMusic: "MÚSICA", + podcastCategoryTalk: "CHARLA", + podcastCategoryEducation: "EDUCACIÓN", + podcastCategoryTech: "TECNOLOGÍA", + podcastCategoryCulture: "CULTURA", + podcastCategoryCommunity: "COMUNIDAD", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "OTRAS", + podcastTagsLabel: "Etiquetas", + podcastCoverLabel: "Portada (imagen o vídeo)", + podcastEpisodeAudioLabel: "Archivo de audio o vídeo (máx: 75MB)", + podcastTagsPlaceholder: "Introduce etiquetas separadas por comas", + podcastFilterAll: "TODOS", + podcastFilterMine: "MÍOS", + podcastFilterRecent: "RECIENTES", + podcastFilterTop: "TOP", + podcastFilterViewers: "OYENTES", + podcastSearchPlaceholder: "Buscar en podcasts...", + podcastNoItems: "No se han encontrado podcasts.", + podcastEpisodesLabel: "Episodios", + podcastPlaysLabel: "Visualizaciones", + podcastLastEpisode: "Último episodio", + podcastEpisodesTitle: "Episodios", + podcastNoEpisodes: "Aún no hay episodios.", + podcastEpisodeAdd: "Añadir episodio", + podcastEpisodeCreateTitle: "Publicar episodio", + podcastEpisodeUpdateTitle: "Actualizar episodio", + podcastEpisodeTitleLabel: "Título del episodio", + podcastEpisodeTitlePlaceholder: "Título del episodio", + podcastEpisodeDescriptionLabel: "Notas", + podcastEpisodeDescriptionPlaceholder: "Qué pasa en este episodio, enlaces, créditos...", + podcastEpisodePublish: "Publicar", + podcastPlayed: "Reproducido", + podcastMarkPlayed: "Marcar como reproducido", + typePodcast: "PODCAST", + typePodcastEpisode: "EPISODIO", + typePodcastOpinion: "OPINIÓN DE EPISODIO", + typePodcastPlay: "REPRODUCCIÓN", + statsPodcast: "Podcasts", + statsPodcastEpisode: "Episodios", + statsPodcastPlay: "Reproducciones", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "Un blog que sigues tiene una nueva entrada.", + podcastBotEpisodeTitle: "Un podcast que sigues tiene un episodio nuevo.", + podcastLabel: "PODCAST", + podcastEpisodeLabel: "EPISODIO", + campaignsTitle: "Campañas", + modulesCampaignsLabel: "Campañas", + modulesCampaignsDescription: "Descubre y gestiona campañas en tu red.", + campaignsDescription: "Descubre y gestiona campañas en tu red.", + campaignCreate: "Crear campaña", + campaignUpdate: "Actualizar", + campaignDelete: "Borrar", + campaignClose: "Cerrar", + campaignReopen: "Reabrir", + campaignCreateSectionTitle: "Crear campaña", + campaignUpdateSectionTitle: "Actualizar campaña", + campaignTitleLabel: "Título", + campaignTitlePlaceholder: "¿Qué quieres conseguir?", + campaignTextLabel: "Texto", + campaignTextPlaceholder: "Explica la causa, quién decide y qué pides...", + campaignCategoryLabel: "Categoría", + campaignCategoryEnvironment: "MEDIO AMBIENTE", + campaignCategoryRights: "DERECHOS", + campaignCategoryHealth: "SALUD", + campaignCategoryEducation: "EDUCACIÓN", + campaignCategoryInfrastructure: "INFRAESTRUCTURA", + campaignCategoryCulture: "CULTURA", + campaignCategoryEconomy: "ECONOMÍA", + campaignCategoryOther: "OTRAS", + campaignGoalLabel: "Objetivo de firmas", + campaignDeadlineLabel: "Plazo", + campaignTagsLabel: "Etiquetas", + campaignTagsPlaceholder: "Introduce etiquetas separadas por comas", + campaignFilterAll: "TODAS", + campaignFilterMine: "MÍAS", + campaignFilterSigned: "FIRMADAS", + campaignFilterRecent: "RECIENTES", + campaignFilterTop: "TOP", + campaignStatusOpen: "ABIERTA", + campaignStatusAchieved: "LOGRADA", + campaignStatusUnachieved: "NO LOGRADA", + campaignStatusClosed: "CERRADA", + campaignSearchPlaceholder: "Buscar en campañas...", + campaignNoItems: "No se han encontrado campañas.", + campaignSignaturesLabel: "Firmas", + campaignSignaturesTitle: "Apoyos", + campaignNoSignatures: "Aún no hay firmas.", + campaignSignTitle: "Firmar esta campaña", + campaignSignPlaceholder: "¿Por qué la apoyas? (opcional)", + campaignSign: "Firmar", + campaignSigned: "¡Ya has firmado!", + campaignNotSigned: "Aún falta tu firma", + campaignElevate: "Elevar al Parlamento", + campaignElevated: "Elevada al Parlamento", + campaignUpdatesTitle: "Actualizaciones", + campaignNoUpdates: "Aún no hay actualizaciones.", + campaignUpdatePlaceholder: "¿Qué ha pasado?", + campaignAddUpdate: "Publicar actualización", + campaignQrAlt: "QR para firmar esta campaña", + campaignQrCaption: "Escanea para firmar", + typeCampaign: "CAMPAÑA", + typeCampaignSignature: "FIRMA", + typeCampaignUpdate: "ACTUALIZACIÓN DE CAMPAÑA", + statsCampaign: "Campañas", + statsCampaignSignature: "Firmas de campañas", + statsCampaignUpdate: "Actualizaciones de campañas", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "Una campaña que sigues tiene una actualización.", + campaignBotAchievedTitle: "Una campaña que sigues ha alcanzado su objetivo.", + campaignBotRaisedTitle: "Una campaña que has firmado se ha elevado al Parlamento.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Una ruta que reservaste ha cambiado.", + logisticsBotClosedTitle: "Una ruta que reservaste se ha cerrado.", + logisticsBotBookedTitle: "Alguien ha reservado tu ruta.", + logisticsBotCancelledTitle: "Se ha cancelado una reserva de tu ruta.", + logisticsBotConfirmedTitle: "Tu reserva se ha confirmado: el paquete está en camino.", + logisticsBotRejectedTitle: "Tu reserva se ha rechazado.", + logisticsBotDeliveredTitle: "Tu paquete ha llegado.", + campaignLabel: "CAMPAÑA", + campaignGenerateReport: "Generar informe", + logisticsGenerateReport: "Generar informe", + mailingGenerateReport: "Generar informe", + campaignUpdateEdited: "Editada", + mailingMessagesLabel: "Mensajes", + logisticsBookingsLabel: "Reservas", + backupTitle: "Copia de seguridad", + modulesBackupLabel: "Copia de seguridad", + modulesBackupDescription: "Módulo para exportar e importar tus llaves y hacer copias de seguridad cifradas.", + backupDescription: "Módulo para exportar e importar tus llaves y hacer copias de seguridad cifradas.", + backupTypeRecovery: "RECUPERACIÓN", + backupTypeKeys: "EXPORTAR CLAVES", + backupTypeFull: "COPIA COMPLETA", + backupTypeRestore: "RESTAURAR COPIA", + backupTypeInstant: "INSTANTÁNEA", + backupInstantDescription: "Un .zip llano con todo tu registro y tus ficheros, sin tu clave secreta y sin contraseña. Rápido, pero ", + backupInstantRisk: "quien tenga el fichero lo lee todo.", + backupKitPageDescription: "El kit de recuperación solo puede abrirse desde este dispositivo.", + backupKitId: "ID de Oasis", + backupKitDate: "Generado", + backupKitQrAlt: "Código QR de tu llave secreta", + backupKitStep1: "Imprímelo o cópialo a mano y guárdalo en un lugar seguro y sin conexión.", + backupKitStep2: "Para recuperar, instala Oasis en un dispositivo nuevo e importa este secreto desde Copia de seguridad › Llaves.", + backupKitStep3: "Destruye esta página si alguna vez queda expuesta.", + backupKitPdfButton: "Generar PDF", + backupScopeLabel: "Alcance", + backupScopeAll: "TODO", + backupScopeMine: "SOLO MI CONTENIDO", + backupSinceLabel: "Desde", + backupEstimateTitle: "Estimación", + backupFileInfo: "Tu copia cifrada se descargará en tu dispositivo (nombre: oasis-backup-FECHA.oasisbk)", + backupExportButton: "Descargar copia", + backupRestorePassword: "Escribe la contraseña usada al hacer la copia", + backupRestoreButton: "Restaurar copia", + backupRestoredLine: "Copia restaurada.", + backupRestoredMessages: "Mensajes añadidos", + backupRestoredSkipped: "Ya existentes", + backupRestoredBlobs: "Archivos añadidos", + backupRestoredState: "Configuración restaurada", + backupRestoredFailed: "Fallidos", + backupRestoredForked: "Divergentes", + backupRestoreRunning: "Restaurando la copia. Esta página se actualiza sola; mientras tanto puedes seguir usando Oasis.", + backupRestoreElapsed: "Transcurrido", + backupRestoreFailedLine: "No se pudo restaurar la copia.", + backupRestoreForkMine: "Este dispositivo ya publicó mensajes con tu identidad que no están en la copia, así que no se pudo aplicar encima de ellos. Restaura en una instalación limpia justo después de importar tus llaves, antes de hacer nada más.", + verificationTitle: "Verificación", + verificationDescription: "Comprueba la cadena de tu feed, detecta bifurcaciones y revisa archivos huérfanos o ausentes.", + verificationRun: "Ejecutar verificación", + verificationTotalMessages: "mensajes en el registro", + verificationFeed: "Tu feed", + verificationMessages: "mensajes", + verificationLastSequence: "última secuencia", + verificationGaps: "Huecos", + verificationBrokenLinks: "Enlaces rotos", + verificationBadHashes: "Hashes incorrectos", + verificationBadSignatures: "Firmas incorrectas", + verificationDuplicates: "Secuencias duplicadas", + verificationForks: "Bifurcaciones", + verificationBlobs: "Archivos", + verificationBlobsPresent: "presentes", + verificationBlobsReferenced: "referenciados", + verificationBlobsOrphan: "huérfanos", + verificationBlobsMissing: "ausentes", + verificationBlobsMissingOwn: "Ficheros tuyos que faltan en este dispositivo", + verificationBlobsMissingOthers: "Ficheros de otras habitantes aún sin descargar", + verificationBlobsUnavailable: "No se han podido listar los archivos en este dispositivo.", + wikiTitle: "Wikis", + modulesWikiLabel: "Wikis", + modulesWikiDescription: "Módulo para descubrir y gestionar wikis.", + wikiDescription: "Módulo para descubrir y gestionar wikis.", + wikiCreate: "Crear wiki", + wikiUpdate: "Actualizar", + wikiDelete: "Borrar", + wikiEdit: "Editar", + wikiTitleLabel: "Título", + wikiTitlePlaceholder: "Título de la wiki...", + wikiBodyLabel: "Contenido", + wikiBodyPlaceholder: "Enlaza otras wikis con [[Título de la wiki]] o [[Título de la wiki|un texto propio]].\n\nMenciona a una habitante con @id.\n\nEtiqueta con #oasis.\n\nPega enlaces como https://solarnethub.com\n\nUna línea en blanco abre un párrafo nuevo; los saltos de línea simples se conservan.\n\nAdjunta cualquier media soportado y se incrustará aquí.", + formatBarLabel: "Formato", + wikiStatusLabel: "Tipo de wiki", + wikiStatusOpen: "ABIERTA", + wikiStatusClosed: "CERRADA", + wikiTagsLabel: "Etiquetas", + wikiTagsPlaceholder: "Introduce etiquetas separadas por comas", + wikiSummaryLabel: "Resumen del cambio", + wikiSummaryPlaceholder: "¿Qué ha cambiado?", + wikiFilterAll: "TODAS", + wikiFilterMine: "MÍAS", + wikiFilterRecent: "RECIENTES", + wikiFilterLinked: "ENLAZADAS", + wikiChanges: "Cambios recientes", + wikiNoPages: "No se han encontrado wikis.", + wikiNoChanges: "Aún no hay cambios.", + wikiHistory: "Historial", + wikiVersion: "Versión", + wikiVersions: "versiones", + wikiRestore: "Restaurar", + wikiDiff: "Cambios", + wikiViewVersion: "Ver", + wikiLinkedPages: "Páginas enlazadas", + wikiNoLinkedPages: "Esta página aún no se enlaza con ninguna otra.", + wikiMissingLink: "La página aún no existe", + wikiDuplicateTitle: "Ya existe una página con este título. Cambia el título para crear una página distinta.", + wikiCreateSectionTitle: "Crear wiki", + wikiUpdateSectionTitle: "Actualizar wiki", + wikiSearchPlaceholder: "Buscar en la Wiki...", + wikiCurrentVersion: "Versión actual", + wikiViewingVersion: "Estás viendo una versión antigua", + wikiBackToPage: "Volver a la página", + wikiLinkedChip: "Enlazada", + wikiRestoredSummary: "Versión restaurada", + wikiExportPdf: "Generar página", + wikiPreview: "Vista previa", + tribeSectionWiki: "WIKI", + tribeWikiCreate: "Crear wiki", + tribeWikiEmpty: "Aún no hay wikis.", + typeWiki: "WIKI", padsTitle: "Pads", padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Módulo para gestionar editores de texto colaborativos.", - padFilterAll: "TODOS", - padFilterMine: "MÍO", - padFilterRecent: "RECIENTE", - padFilterOpen: "ABIERTO", - padFilterClosed: "CERRADO", padCreate: "Crear Pad", - padUpdate: "Actualizar Pad", - padDelete: "Eliminar Pad", + padUpdate: "Actualizar", + padDelete: "Eliminar", padTitleLabel: "Título", padTitlePlaceholder: "Escribe el título del pad...", padStatusLabel: "Estado", + padTypeLabel: "Tipo de pad", padStatusOpen: "ABIERTO", - padStatusInviteOnly: "SOLO INVITADOS", + padStatusInviteOnly: "SOLO CON INVITACIÓN", padStatusClosed: "CERRADO", padDeadlineLabel: "Fecha límite", padTagsLabel: "Etiquetas", - padTagsPlaceholder: "etiqueta1, etiqueta2, ...", + padTagsPlaceholder: "Introduce etiquetas separadas por comas", padMembersLabel: "Miembros", - padVisitPad: "Visitar Pad", - padShareUrl: "Compartir URL", padCreated: "Creado", - padAuthor: "Autor", - padGenerateCode: "Generar Código", - padInviteCodeLabel: "Código de Invitación", - padInviteCodePlaceholder: "Introduce el código de invitación...", - padValidateInvite: "Validar", - padStartEditing: "¡EMPEZAR A EDITAR!", + padStartEditing: "Editar", padEditorPlaceholder: "Empieza a escribir...", padSubmitEntry: "Enviar", padNoEntries: "Sin entradas aún.", padAllSectionTitle: "Todos los Pads", padMineSectionTitle: "Mis Pads", padsDescription: "Gestiona editores de texto cifrados colaborativos en tu red.", + roomsTitle: "Salas", + roomsDescription: "Descubre y únete a salas de videoconferencia cifradas en tu red.", + modulesRoomsLabel: "Salas", + modulesRoomsDescription: "Módulo para descubrir y unirse a salas de videoconferencia cifradas.", + roomFilterRecent: "RECIENTES", + roomFilterMine: "MÍAS", + roomFilterAll: "TODAS", + roomFilterLive: "EN DIRECTO", + roomFilterOpen: "ABIERTAS", + roomFilterInvite: "SOLO CON INVITACIÓN", + roomFilterClosed: "CERRADAS", + roomCreate: "Crear sala", + roomUpdate: "Actualizar", + roomDelete: "Eliminar", + roomClose: "Cerrar", + roomReopen: "Reabrir", + roomCreateSectionTitle: "Crear sala", + roomUpdateSectionTitle: "Actualizar sala", + roomTitlePlaceholder: "Título de la sala", + roomDescriptionLabel: "Descripción", + roomDescriptionPlaceholder: "¿De qué vais a hablar?", + roomTypeLabel: "Tipo de sala", + roomTagsLabel: "Etiquetas", + roomStatusOpen: "ABIERTA", + roomStatusInviteOnly: "SOLO CON INVITACIÓN", + roomStatusClosed: "CERRADA", + roomParticipants: "Participantes", + roomJoin: "Entrar", + roomLeave: "Salir", + roomFull: "Sala llena", + roomLiveChip: "EN DIRECTO", + roomMuted: "silenciado", + roomRec: "Grabar audio", + roomRecStop: "Parar grabación", + roomStreamFile: "Emitir", + roomStreamStop: "Parar", + roomStreaming: "Emitiendo", + liveFullscreen: "Pantalla completa", + roomStreamPickTitle: "Elige un vídeo", + roomStreamPickHint: "Vídeos de este dispositivo. El que elijas se emite desde donde está, sin copiarlo ni publicarlo.", + roomStreamGo: "Emitir en la sala", + roomStreamNotFound: "Ese vídeo no está en tus carpetas.", + roomPublicWarning: "Esta sala se retransmite en la web pública: cualquiera puede ver y oír lo que pasa en ella.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} ha entrado en la sala", + roomEventLeft: "{name} ha salido de la sala", + roomEventRecStart: "{name} ha empezado a grabar", + roomEventRecStop: "{name} ha dejado de grabar", + roomEventMuted: "{name} se ha silenciado", + roomEventUnmuted: "{name} ha quitado el silencio", + roomEventHand: "{name} ha levantado la mano", + roomRaiseHand: "Levantar mano", + roomLowerHand: "Bajar mano", + confirmActionTitle: "Confirma esta acción", + confirmActionButton: "Confirmar", + confirmJoinText: "Vas a unirte usando esta invitación.", + confirmFollowText: "Vas a apoyar a este habitante.", + confirmSignText: "Vas a firmar esta campaña.", + confirmDownloadText: "Vas a descargar este archivo a tu dispositivo.", + roomRecordings: "Grabaciones", + roomClearNotices: "Limpiar", + roomLineSwitchboard: "Centralita", + roomLineDndError: "El número de esta sala no acepta llamadas.", + roomNumberHint: "Marca este número desde Teléfono para entrar en la sala.", + liveRefresh: "Refrescar", + roomNobody: "Ahora mismo no hay nadie en la sala.", + roomSomeone: "{n} en la sala ahora mismo.", + roomYou: "tú", + roomYouAreIn: "Estás dentro", + roomNobodyInside: "Ahora mismo no hay nadie en esta sala.", + roomErrorFull: "La sala está llena. Podrás entrar cuando alguien salga.", + roomErrorUnreachable: "Ahora mismo no se puede llegar a la sala. Inténtalo más tarde.", + roomErrorBusy: "Ya estás en una llamada o en otra sala.", + roomErrorClosed: "Esta sala está cerrada.", + roomAccessDenied: "Esta sala es solo con invitación. Necesitas una invitación para entrar.", + roomsNoItems: "No se han encontrado salas.", + roomSearchPlaceholder: "Buscar salas...", + roomInviteMode: "Invitación", + tribeSectionRooms: "SALAS", + tribeRoomCreate: "Crear sala", + tribeRoomsEmpty: "Aún no hay salas.", + typeRoom: "SALAS", + typeRoomMember: "PARTICIPANTE DE SALA", + statsRoom: "Salas", + statsRoomMember: "Participantes de salas", + favoritesFilterRooms: "SALAS", + invitesRoomsTitle: "Salas", + inviteCodeInvalid: "Este código de invitación no es válido o ya se ha usado.", + inviteAlreadyMember: "Ya formas parte. Puedes abrirlo desde su módulo.", + actionFailed: "No se ha podido hacer. Inténtalo de nuevo.", + publishTooShort: "Tu publicación es demasiado corta.", + walletSendFailed: "No se ha podido enviar el pago: {reason}", + pixeliaCoordsError: "Esas coordenadas están fuera de la cuadrícula.", + courtsErrTitle: "Hace falta un título.", + courtsErrRespondent: "Indica contra quién es el caso.", + courtsErrRespondentId: "Ese no es un Oasis ID válido.", + courtsErrMethod: "Elige cómo se resolverá el caso.", + courtsErrCaseNotFound: "No se ha encontrado ese caso.", + courtsErrBrief: "Escribe tu respuesta.", + courtsErrStance: "Elige tu postura.", + courtsErrResult: "Escribe el resultado.", + courtsErrTerms: "Escribe los términos del acuerdo.", + courtsErrMediator: "Añade al menos un mediador.", + courtsErrSelfMediator: "No puedes ser mediador en tu propio caso.", + courtsErrJudge: "Elige un juez.", + courtsErrSelfJudge: "No puedes ser juez de tu propio caso.", + courtsErrVisibility: "Elige quién puede ver el caso.", + courtsErrNomination: "No se ha encontrado esa nominación.", + opinionAlreadyGiven: "Ya has dado tu opinión.", + cipherPasswordShort: "La contraseña debe tener al menos 32 caracteres.", + errorForbidden: "No tienes acceso a esto.", + errorNotFound: "No se ha encontrado.", + errorClosed: "Está cerrado.", + errorInviteOnly: "Esto es solo con invitación. Necesitas una invitación.", + errorTooLong: "Ese texto es demasiado largo.", + errorRequired: "Falta información obligatoria.", + errorInvalid: "Parte de la información no es válida.", + errorNotAuthor: "Solo quien lo creó puede hacer esto.", + panicRemoved: "Tu blockchain se ha eliminado.", padRecentSectionTitle: "Pads Recientes", padOpenSectionTitle: "Pads Abiertos", padClosedSectionTitle: "Pads Cerrados", padCreateSectionTitle: "Crear Nuevo Pad", padUpdateSectionTitle: "Actualizar Pad", - padInviteGenerated: "Código de Invitación Generado", typePad: "PADS", padNew: "NUEVO", padAddFavorite: "Añadir a Favoritos", padRemoveFavorite: "Quitar de Favoritos", - padClose: "Cerrar Pad", + padClose: "Cerrar", padBackToEditor: "Volver al editor", - padSearchPlaceholder: "Buscar pads...", + padSearchPlaceholder: "Buscar en Pads...", calendarsTitle: "Calendarios", calendarTitle: "Calendario", modulesCalendarsLabel: "Calendarios", modulesCalendarsDescription: "Módulo para descubrir y gestionar calendarios.", typeCalendar: "CALENDARIOS", - calendarFilterAll: "TODOS", - calendarFilterMine: "MÍOs", - calendarFilterOpen: "ABIERTOS", - calendarFilterClosed: "CERRADOS", - calendarFilterRecent: "RECIENTES", - calendarFilterFavorites: "FAVORITOS", calendarCreate: "Crear Calendario", calendarUpdate: "Actualizar", - calendarDelete: "Eliminar", calendarTitleLabel: "Título", calendarTitlePlaceholder: "Título del calendario...", calendarStatusLabel: "Estado", + calendarTypeLabel: "Tipo de calendario", calendarStatusOpen: "ABIERTO", calendarStatusClosed: "CERRADO", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "Fecha límite", calendarTagsLabel: "Etiquetas", - calendarTagsPlaceholder: "etiqueta1, etiqueta2...", + calendarTagsPlaceholder: "Introduce etiquetas separadas por comas", calendarParticipantsLabel: "Participantes", - calendarParticipantsCount: "Participantes", - calendarVisitCalendar: "Ver Calendario", calendarCreated: "Creado", - calendarAuthor: "Autor", calendarJoin: "Unirse al Calendario", calendarGenerateInvite: "Generar invitación", - calendarInviteCodePlaceholder: "Introduce código...", - calendarValidateInvite: "Validar código", - calendarJoined: "Unido", - calendarAddDate: "Añadir Fecha", - calendarAddNote: "Añadir Nota", calendarDateLabel: "Fecha", calendarDatePlaceholder: "Describe esta fecha...", - calendarNoteLabel: "Nota", + calendarNoteLabel: "Notas", calendarNotePlaceholder: "Añadir una nota...", calendarFirstDateLabel: "Fecha", calendarFirstNoteLabel: "Notas", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "Intervalo", + calendarIntervalNone: "Sin repetición", + calendarIntervalWeekly: "Semanal", + calendarIntervalMonthly: "Mensual", + calendarIntervalYearly: "Anual", calendarFormDescription: "Descripción", calendarNoDates: "No hay fechas añadidas.", calendarNoNotes: "Sin notas.", @@ -3298,8 +4001,8 @@ module.exports = { calendarsDescription: "Descubre y gestiona calendarios en tu red.", calendarMonthPrev: "\u2190 Anterior", calendarMonthNext: "Siguiente \u2192", - calendarMonthLabel: "Fechas", - calendarsShareUrl: "URL para compartir", + calendarMonthNames: "Enero,Febrero,Marzo,Abril,Mayo,Junio,Julio,Agosto,Septiembre,Octubre,Noviembre,Diciembre", + calendarDayInitials: "L,M,X,J,V,S,D", calendarAllSectionTitle: "Calendarios", calendarRecentSectionTitle: "Calendarios Recientes", calendarFavoritesSectionTitle: "Favoritos", @@ -3309,76 +4012,209 @@ module.exports = { calendarCreateSectionTitle: "Crear Nuevo Calendario", calendarUpdateSectionTitle: "Actualizar Calendario", calendarAddFavorite: "Añadir a Favoritos", - calendarDeleteNote: "Delete", + calendarDeleteNote: "Eliminar", calendarRemoveFavorite: "Quitar de Favoritos", - calendarSearchPlaceholder: "Buscar calendarios...", + calendarSearchPlaceholder: "Buscar en Calendario...", calendarAddEntry: "Añadir Entrada", - calendarLeave: "Salir del Calendario", statsCalendar: "Calendarios", statsCalendarDate: "Fechas de Calendario", statsCalendarNote: "Notas de Calendario", modulesChatsLabel: "Chats", - modulesChatsDescription: "Módulo para descubrir y gestionar chats cifrados.", + modulesChatsDescription: "Módulo para descubrir y gestionar chats.", typeChat: "CHATS", typeChatMessage: "MENSAJE DE CHAT", - chatLabel: "CHATS", - chatMessageLabel: "MENSAJES DE CHAT", chatsTitle: "Chats", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "Tus chats", + chatRecentTitle: "Chats recientes", chatFavoritesTitle: "Favoritos", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "Chats abiertos", + chatClosedTitle: "Chats cerrados", chatDescription: "Descripción", chatCategory: "Categoría", chatStatus: "ESTADO", - chatFilterAll: "TODOS", - chatFilterMine: "MÍO", - chatFilterRecent: "RECIENTE", - chatFilterFavorites: "FAVORITOS", - chatFilterOpen: "ABIERTO", - chatFilterClosed: "CERRADO", chatCreate: "Crear Chat", - chatUpdate: "Actualizar Chat", - chatDelete: "Eliminar Chat", + chatUpdate: "Actualizar", + chatDelete: "Borrar", chatClose: "Cerrar Chat", chatVisitChat: "VER CHAT", chatUntitled: "Chat sin título", chatNoItems: "No se encontraron chats.", chatParticipants: "Participantes", - chatStartChatting: "¡EMPEZAR A CHATEAR!", - chatGenerateCode: "Generar Código", - chatShareUrl: "Compartir URL", + chatMessagesLabel: "Mensajes", + chatThreadsLabel: "Hilos", chatCreatedAt: "CREADO", - chatSearchPlaceholder: "Buscar chats...", + chatSearchPlaceholder: "Buscar en Chats...", chatStatusOpen: "ABIERTO", - chatStatusInviteOnly: "SOLO INVITADOS", + chatStatusInviteOnly: "SOLO CON INVITACIÓN", chatStatusClosed: "CERRADO", chatSendMessage: "Enviar", + chatJumpLatest: "Último Mensaje", + chatReply: "Responder", + chatReplyCancel: "Cancelar respuesta", + chatPin: "Fijar", + chatUnpin: "Desfijar", + chatPinned: "Fijados", + chatPickChat: "Elige un chat para abrirlo", + chatNoneYet: "No existe ningún chat disponible, todavía.", + activitySearchPlaceholder: "Buscar en Actividad...", + trendingSearchPlaceholder: "Buscar en Tendencias...", + opinionsSearchPlaceholder: "Buscar en Opiniones...", + votesSearchPlaceholder: "Buscar en Votaciones...", + gamesSearchPlaceholder: "Buscar en Juegos...", + schoolTitle: "Escuela", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "Se ha editado una página wiki que sigues.", + wikiBotRestoredTitle: "Se ha restaurado una versión anterior de una página wiki que sigues.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Un nuevo estudiante se ha apuntado a tu curso.", + schoolBotInvitedTitle: "Te han invitado a un curso.", + schoolBotAdmittedTitle: "Te han admitido en un curso.", + schoolBotCertificateTitle: "Has recibido un certificado.", + schoolBotPassedTitle: "Un estudiante ha aprobado tu curso.", + schoolBotLessonTitle: "Se ha publicado una nueva lección.", + invitesSchoolTitle: "Escuela", + schoolEnrollLabel: "Unirse al curso", + schoolExamRetryLater: "Podrás reintentarlo pasadas 24 horas.", + schoolApproved: "APROBADO", + schoolExamPassMark: "Nota de aprobado (0-10)", + schoolExamPassed: "APROBADO", + schoolExamPending: "Pendiente", + schoolExamFailed: "SUSPENDIDO", + schoolExamAddQuestion: "Añadir pregunta", + schoolExamCreateQuestion: "Crear pregunta", + schoolExamOption: "Opción", + schoolExamCorrect: "Opción correcta", + schoolExamPoints: "Puntos", + schoolMaterialTextPlaceholder: "Notas para esta lección...", + schoolMaterials: "Materiales", + schoolAddMaterial: "Añadir material", + schoolNoMaterials: "Aún no hay materiales.", + bankYourSchoolBalance: "Tus ingresos de la escuela", + schoolProgress: "Progreso", + schoolProgressMark: "Marcar como completada", + schoolProgressDone: "Completada", + schoolExams: "Exámenes", + schoolExamCreate: "Crear examen", + schoolExamTitlePlaceholder: "Título del examen", + schoolExamHelp: "Una pregunta por línea: ¿Pregunta? ; opción ; opción correcta* ; opción", + schoolExamQuestionsPlaceholder: "¿Qué es SSB? ; Una base de datos ; Un protocolo P2P* ; Un navegador", + schoolExamSubmit: "Enviar respuestas", + schoolExamScore: "Tu nota", + schoolNoScoresYet: "Aún no hay resultados.", + schoolLessonUnit: "Unidad / fase", + schoolLessonUnitPlaceholder: "p. ej. Fase 1", + schoolLessonOrder: "Orden", + profileClearnetSchoolLabel: "Escuela", + schoolStartDate: "Fecha de inicio", + schoolSessionDate: "Fecha de la sesión", + schoolCourseChat: "Chat del curso", + agendaFilterSchool: "ESCUELA", + agendaFilterCampaigns: "CAMPAÑAS", + agendaFilterLogistics: "LOGÍSTICA", + schoolCourseType: "Tipo de curso", + schoolTypeOpen: "ABIERTO", + schoolTypePaid: "DE PAGO", + schoolTypeInvite: "SOLO INVITACIÓN", + schoolLessonLocked: "Lección cifrada — pide la clave a quien imparte del curso.", + schoolGrantAccess: "Enviar clave del curso", + schoolKeySent: "Clave enviada", + schoolOngoing: "En curso", + schoolFilterApplied: "APUNTADO", + schoolFilterOpen: "ABIERTOS", + schoolFilterFavorites: "FAVORITOS", + schoolPendingPayment: "Pago pendiente", + schoolPendingStudents: "Inscripciones pendientes", + schoolViewBill: "Ver factura", + schoolPaymentNote: "Al inscribirte se genera una factura en ECO (una transferencia). Tu inscripción se confirma cuando la transferencia queda firmada.", + schoolCloseCourse: "Cerrar curso", + schoolReopenCourse: "Reabrir curso", + schoolDescription: "Enseña y aprende cursos en tu red.", + schoolFilterAll: "TODOS", + schoolFilterMine: "MÍOS", + schoolFilterTop: "TOP", + schoolFilterRecent: "RECIENTES", + schoolCreateButton: "Crear Curso", + schoolCreateSectionTitle: "Crear un curso", + schoolUpdateSectionTitle: "Actualizar curso", + schoolCourseTitlePlaceholder: "Nombre de tu curso", + schoolCourseDescriptionPlaceholder: "¿Qué vas a enseñar en este curso?", + schoolCourseImage: "Imagen del curso (tamaño máx: 75MB)", + schoolTags: "Etiquetas", + schoolTagsPlaceholder: "Introduce etiquetas separadas por comas", + schoolPrice: "Precio (ECO)", + schoolVisibility: "Visibilidad", + schoolVisibilityPublic: "Público", + schoolVisibilityInvite: "Por invitación", + schoolStatus: "Estado", + schoolClosed: "Cerrado", + schoolFree: "Gratis", + schoolCreate: "Crear curso", + schoolUpdate: "Actualizar curso", + schoolSearchPlaceholder: "Buscar en Escuela...", + schoolSortTitle: "Título", + schoolNoCourses: "No se han encontrado cursos.", + schoolStudents: "Estudiantes", + schoolTeacher: "Docente", + schoolCreatedAt: "CREADO", + schoolLessons: "Lecciones", + schoolNoLessons: "Aún no hay lecciones.", + schoolEnrollToSee: "Inscríbete para ver las lecciones de este curso.", + schoolEnrollButton: "INSCRIBIRSE", + schoolUnenrollButton: "Abandonar curso", + schoolInviteRequired: "Este curso es por invitación.", + schoolTeacherPanel: "Panel docente", + schoolStudentPanel: "Panel del estudiante", + schoolAddLesson: "Crear lección", + schoolLessonTitlePlaceholder: "Título de la lección", + schoolLessonTextPlaceholder: "Describe las nociones que cubre esta lección...", + schoolInviteStudents: "Invitar estudiantes", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "INVITAR", + schoolInvited: "Invitadas", + schoolIssueCertificate: "Crear certificado", + schoolCertified: "Certificado", + schoolCertificateTextPlaceholder: "Nota del certificado (opcional)", + schoolLessonOpen: "Abrir lección", + schoolCertificatePdf: "PDF", + schoolCertificates: "Certificados", + schoolNoCertificates: "Aún no se han emitido certificados.", + schoolEditButton: "Editar", + schoolDeleteButton: "Borrar", + typeSchool: "ESCUELA", + schoolCourseLabel: "CURSO", + emergencyLabel: "EMERGENCIA", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Cursos", + statsSchoolLesson: "Lecciones", + statsSchoolEnroll: "Inscripciones", + statsSchoolCertificate: "Certificados de la escuela", + modulesSchoolLabel: "Escuela", + modulesSchoolDescription: "Módulo para intercambiar conocimiento.", + welcomeStepUxAction: "Guardar vista", + welcomeStepWorkflowTitle: "¿Qué uso de OASIS tienes en mente?", + welcomeStepWorkflowText: "Oasis tiene muchos módulos. Elige la configuración que encaje contigo y solo quedarán activados los útiles. Puedes ajustarlo cuando quieras en Módulos.", + welcomeStepWorkflowAction: "Guardar configuración", + welcomeWorkflow_full: "Por defecto", + chatRateLimitTitle: "Demasiados mensajes", + chatRateLimitMessage: "Has alcanzado el límite de mensajes que esta sala acepta en una hora. Inténtalo más tarde.", chatMessagePlaceholder: "Escribe tu mensaje...", chatNoMessages: "Sin mensajes aún.", - chatLeave: "Salir del Chat", - chatInviteCodeLabel: "Introduce el código de invitación", - chatJoinByInvite: "Unirse", chatTitlePlaceholder: "Título del chat", chatDescriptionPlaceholder: "Descripción del chat", - chatTagsPlaceholder: "etiqueta1, etiqueta2", + chatTagsPlaceholder: "Introduce etiquetas separadas por comas", chatImageLabel: "Selecciona un archivo de imagen (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "Código de Invitación", - chatAuthor: "Autor", - chatCreated: "Creado", + chatAuthor: "Autora", chatAddFavorite: "Añadir a Favoritos", chatRemoveFavorite: "Quitar de Favoritos", chatPM: "MP", chatStatusLabel: "Estado", + chatTypeLabel: "Tipo de chat", chatCategoryLabel: "Categoría", - chatParticipantsLabel: "Participantes", gamesTitle: "Juegos", gamesDescription: "Descubre y juega algunos juegos.", gamesFilterAll: "TODOS", gamesPlayButton: "¡JUGAR!", - gamesBackToGames: "Volver a Juegos", modulesGamesLabel: "Juegos", modulesGamesDescription: "Módulo para descubrir y jugar algunos juegos.", modulesGraphosLabel: "Graphos", @@ -3388,15 +4224,13 @@ module.exports = { gamesTheFlowTitle: "ECOinflow", gamesTheFlowDesc: "Conecta PUBs a habitantes mediante validadores, tiendas y acumuladores. ¡Sobrevive a la amenaza CBDC!", gamesNeonInfiltratorTitle: "Neon Infiltrator", - gamesNeonInfiltratorDesc: "Infiltra la cuadricula, recolecta datos confidenciales, evade los drones de seguridad y escapa. ¿Cuantos niveles puedes superar?", + gamesNeonInfiltratorDesc: "Infíltrate en la cuadrícula, recolecta datos confidenciales, evade los drones de seguridad y escapa. ¿Cuántos niveles puedes superar?", gamesSpaceInvadersTitle: "Space Invaders", gamesSpaceInvadersDesc: "¡Detén la invasión alienígena! Destruye oleadas de invasores antes de que lleguen al suelo.", gamesArkanoidTitle: "Arkanoid", gamesArkanoidDesc: "Rompe todos los ladrillos con tu paleta y pelota. Un clásico desafío arcade.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong clásico contra una IA. El primero en llegar a 5 puntos gana.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "¡Corre contra el tiempo! Esquiva el tráfico y llega a la meta antes de que se acabe el tiempo.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilota tu nave por un campo de asteroides mortal. Dispárales antes de que te alcancen.", gamesRockPaperScissorsTitle: "Piedra Papel Tijeras", @@ -3406,24 +4240,26 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "Lanza una moneda y apuesta cara o cruz. ¿Cuánta suerte tienes?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "Billar visto desde arriba. Haz clic para apuntar y mantén pulsado para cargar la potencia. Mete todas las bolas con el menor número de tiros.", + gamesArtilleryTitle: "Artillería", + gamesArtilleryDesc: "Apunta tu cañón, ten en cuenta el viento y da en el blanco. 5 rondas: gana quien use menos disparos.", + gamesLabyrinthTitle: "Laberinto", + gamesLabyrinthDesc: "Escapa del laberinto antes de que se te acaben los movimientos. Cada nivel es más grande y más difícil.", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "Cómete todos los puntos y esquiva a los fantasmas. Los fantasmas se mueven en tiempo real: ¡no te detengas!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesAudioPendulumTitle: "Péndulo sonoro", + gamesAudioPendulumDesc: "Simulador de física caótica con síntesis de audio en tiempo real. Las velocidades angulares se convierten en frecuencias y los picos en golpes de percusión. No hay dos simulaciones que suenen igual.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Pincha visuales en directo con dos platos: elige un patrón para cada uno, ajusta velocidad, color y zoom, mezcla entre ambos y lanza destellos al ritmo. Marca el tempo o deja que el micrófono lleve el pulso.", + gamesTetrisDesc: "Los clásicos bloques que caen. Completa líneas para puntuar. ¿Cuánto aguantarás?", + gamesMinesweeperTitle: "Buscaminas", + gamesMinesweeperDesc: "La clásica caza de minas. Despeja el tablero para subir de nivel: cuadrículas más grandes, más minas, más puntuación.", + gamesFilterScoring: "PUNTUACIONES", + gamesHallOfFame: "Salón de la fama", + gamesHallPlayer: "Jugadora", + gamesHallScore: "Puntuación", + gamesNoScores: "Aún no hay puntuaciones.", chatAccessDenied: "No tienes acceso a este chat. Solicita una invitación para acceder al contenido.", padAccessDenied: "No tienes acceso a este pad. Solicita una invitación para acceder al contenido.", contentAccessDenied: "No tienes acceso a este contenido.", @@ -3431,6 +4267,146 @@ module.exports = { tribeContentAccessDenied: "Acceso Denegado", tribeContentAccessDeniedMsg: "Este contenido pertenece a una tribu. Debes ser miembro para acceder.", tribeViewTribes: "Ver Tribus", + tribeFilesEmpty: "Aún no hay ficheros.", + filesLabel: "Ficheros", + profileClearnetFilesLabel: "Ficheros", + filesTitle: "Ficheros", + filesDescription: "Explora y gestiona ficheros en tu red.", + fileAllSectionTitle: "Ficheros", + fileMineSectionTitle: "Tus Ficheros", + fileRecentSectionTitle: "Ficheros Recientes", + fileTopSectionTitle: "Ficheros Destacados", + fileFavoritesSectionTitle: "Favoritos", + fileFilterAll: "TODOS", + fileFilterMine: "MÍOS", + fileFilterRecent: "RECIENTES", + fileFilterTop: "DESTACADOS", + fileFilterFavorites: "FAVORITOS", + fileCreateSectionTitle: "Subir Fichero", + fileUpdateSectionTitle: "Actualizar Fichero", + fileCreateButton: "Subir Fichero", + fileUpdateButton: "Actualizar", + fileAddFavoriteButton: "Añadir Favorito", + fileRemoveFavoriteButton: "Quitar Favorito", + fileFileLabel: "Selecciona un archivo (máx.: 75MB)", + calendarFilterRecent: "RECIENTES", + calendarFilterAll: "TODOS", + calendarFilterMine: "MÍOS", + calendarFilterFavorites: "FAVORITOS", + calendarFilterOpen: "ABIERTOS", + calendarFilterClosed: "CERRADOS", + padFilterRecent: "RECIENTES", + padFilterAll: "TODOS", + padFilterMine: "MÍOS", + padFilterOpen: "ABIERTOS", + padFilterClosed: "CERRADOS", + chatFilterRecent: "RECIENTES", + chatFilterAll: "TODOS", + chatFilterMine: "MÍOS", + chatFilterFavorites: "FAVORITOS", + chatFilterOpen: "ABIERTOS", + chatFilterClosed: "CERRADOS", + licenseLabel: "Licencia", + phoneTitle: "Teléfono", + phoneDescription: "Gestiona tus videollamadas cifradas de extremo a extremo.", + phoneUnavailable: "Las llamadas no están disponibles en este dispositivo.", + phoneCallButton: "Llamar", + profileVisibilityPhone: "Número de teléfono", + phoneNumberUnknown: "Nadie en tu red tiene el número {number}.", + phoneNumberChoose: "Comprueba a quién quieres llamar al {number}:", + phoneAnswer: "Contestar", + phoneSpeaker: "Altavoz", + phoneEarpiece: "Auricular", + phoneReject: "Rechazar", + phoneHangup: "Colgar", + phoneMute: "Silenciar", + phoneUnmute: "Activar micrófono", + phoneCameraOn: "Cámara on", + phoneCameraOff: "Cámara off", + settingsPhoneVideo: "Vídeo", + settingsPhoneVideoOn: "Permitido en llamadas y salas", + settingsPhoneVideoOff: "Solo audio", + phoneSilence: "No oír", + phoneUnsilence: "Volver a oír", + phoneCalling: "Llamando a", + phoneIsCalling: "te está llamando", + notifyNewPmFrom: "Nuevo MP de", + notifyNewPamFrom: "Nuevo PaM de", + notifyUpdateLabel: "Actualización", + updateInstalledLabel: "Versión instalada", + updateAvailableLabel: "Versión disponible", + updateChangesLabel: "Cambios", + updateFilesLabel: "Archivos", + updateOriginLabel: "Origen", + phoneRecording: "Grabando PaM para", + phoneLeaveMessage: "Nadie contesta: deja tu mensaje después de la señal.", + phoneLeaveMessageBusy: "Está en otra llamada: deja tu mensaje después de la señal.", + phoneLeaveMessageUnreachable: "No se ha podido contactar: deja tu mensaje después de la señal.", + phoneConnecting: "Conectando con", + phoneSendPam: "Enviar PaM", + phoneCancel: "Cancelar", + phoneSearchPlaceholder: "Buscar en Teléfono...", + phonePamTitle: "Grabaciones de audio", + phoneNew: "Nuevo", + phoneDelete: "Borrar", + phoneNoPams: "No tienes grabaciones de audio.", + phoneHistoryTitle: "Historial", + phoneLatestTitle: "Últimas llamadas", + phoneCreateButton: "Nueva llamada", + phoneLimitsHint: "Hasta {n} habitantes por llamada; con más de uno es una llamada conjunta.", + phoneErrorPub: "No se puede llamar a un pub.", + phoneJointCall: "Llamada conjunta", + phoneWith: "Con", + phoneResult: "Resultado", + phoneWhen: "Cuándo", + phoneDuration: "Duración", + phoneOutgoing: "Saliente", + phoneIncoming: "Entrante", + phoneNoHistory: "Aún no hay registros.", + phoneOutcomeEnded: "Atendida", + phoneOutcomeMissed: "Perdida", + phoneOutcomeRejected: "Rechazada", + phoneOutcomeNoanswer: "Sin respuesta", + phoneOutcomeCancelled: "Cancelada", + phoneOutcomePam: "PaM enviado", + phoneOutcomeFailed: "Fallida", + phoneDialAdd: "Añadir", + phoneDialBack: "Borrar", + phoneDialClear: "Limpiar", + phoneDialPlaceholder: "Número u Oasis ID", + phoneDialInhabitants: "Habitantes", + phoneDialRemove: "Quitar", + phoneOwnNumber: "Ese es tu propio número.", + phoneSettingsDesc: "Elige quién puede llamarte.", + phoneDnd: "No molestar", + phoneOpen: "Abierto", + phoneRecipientRefuses: "El habitante al que intentas llamar no permite recibir llamadas.", + phoneRefusedNotice: "Estos habitantes no permiten recibir llamadas y no se les ha llamado:", + fileEmptyError: "El archivo está vacío. Elige otro archivo.", + fileTitleLabel: "Título", + fileTitlePlaceholder: "Título", + fileDescriptionLabel: "Descripción", + fileDescriptionPlaceholder: "Descripción", + fileTagsLabel: "Etiquetas", + fileTagsPlaceholder: "Introduce etiquetas separadas por comas", + fileSizeLabel: "Tamaño", + fileDownloadButton: "DESCARGA", + fileDetailsButton: "DETALLES", + fileNoFile: "Sin archivo fichero", + fileSearchPlaceholder: "Buscar en Ficheros...", + fileSearchButton: "Buscar", + fileSortRecent: "Más recientes", + fileSortOldest: "Más antiguos", + fileSortTop: "Más votados", + fileNoMatch: "No se encontraron ficheros coincidentes.", + fileUpdatedAt: "Actualizado", + typeFile: "FICHEROS", + modulesFilesLabel: "Ficheros", + modulesFilesDescription: "Módulo para descubrir y gestionar ficheros.", + tribeSectionFiles: "FICHEROS", + fileDescription: "Descripción", + fileDownload: "Descargar", + fileTypeLabel: "Tipo", torrentsTitle: "Torrents", torrentsDescription: "Explora y gestiona torrents en tu red.", torrentAllSectionTitle: "Torrents", @@ -3458,47 +4434,91 @@ module.exports = { torrentTagsLabel: "Etiquetas", torrentTagsPlaceholder: "Introduce etiquetas separadas por comas", torrentSizeLabel: "Tamaño", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "Generar torrent", + torrentFromContentMissing: "El fichero aún no está disponible en este nodo. Inténtalo en un rato.", + torrentDownloads: "Descargas", + torrentFetchOasis: "Descargar en Oasis", + torrentSaveFile: "Guardar fichero", + torrentSeeds: "semillas", + seedsLabel: "Semillas", + seedAction: "Soportar", + torrentSourceFile: "Fichero", + torrentStateQueued: "En cola", + torrentStateProbing: "Buscando semillas", + torrentStateDownloading: "Descargando", + torrentStateDone: "Completada", + torrentStateFailed: "Fallida", + torrentStateNoSource: "Sin semillas conectadas, esperando", + torrentStateCancelled: "Cancelada", + torrentNoDownloads: "Todavía no hay descargas.", + torrentOriginalSection: "Original", + torrentCancel: "Cancelar", + torrentDownloadButton: "DESCARGA", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "DETALLES", torrentNoFile: "Sin archivo torrent", noTorrents: "No se encontraron torrents.", - torrentSearchPlaceholder: "Buscar título, etiquetas, autor...", + noFiles: "No se encontraron ficheros.", + torrentSearchPlaceholder: "Buscar en Torrents...", torrentSearchButton: "Buscar", torrentSortRecent: "Más recientes", torrentSortOldest: "Más antiguos", torrentSortTop: "Más votados", torrentNoMatch: "No se encontraron torrents coincidentes.", - torrentMessageAuthorButton: "Enviar Mensaje al Autor", torrentUpdatedAt: "Actualizado", statsTorrent: "Torrents", + statsFile: "Ficheros", typeTorrent: "TORRENTS", modulesTorrentsLabel: "Torrents", modulesTorrentsDescription: "Módulo para descubrir y gestionar torrents.", favoritesFilterTorrents: "TORRENTS", + favoritesFilterFiles: "FICHEROS", favoritesFilterMarket: "MERCADO", + favoritesFilterEvents: "EVENTOS", + favoritesFilterForum: "FORO", + favoritesFilterHousing: "VIVIENDA", + favoritesFilterJobs: "EMPLEOS", + favoritesFilterProjects: "PROYECTOS", + favoritesFilterReports: "INFORMES", + favoritesFilterTasks: "TAREAS", + favoritesFilterTransfers: "TRANSFERENCIAS", + favoritesFilterVotes: "VOTACIONES", favoritesFilterShopProducts: "ARTÍCULOS DE TIENDA", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Subir Torrent", - tribeMediaTypeTorrent: "Torrent", + tribeCreateFile: "Subir fichero", settingsWishTitle: "Deseo", - settingsWishDesc: "Configura el deseo de tu Avatar. Determina cómo accedes a todo el contenido y tu experiencia en la red Oasis.", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "Determina cómo accedes a todo el contenido y tu experiencia en la red Oasis.", + settingsWishWhole: "Multiverso", + settingsWishMutuals: "Solo soporte mutuo", + settingsWishOnlyLan: "Solo LAN", + settingsWishLocal: "Local", settingsPmVisibilityTitle: "Mensajes Privados", settingsPmVisibilityDesc: "Configura el nivel de exposición a mensajes privados que deseas tener en la red.", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "Multiverso", + settingsPmVisibilityMutuals: "Solo soporte mutuo", + mute: "Silenciar", + unmute: "Dejar de silenciar", + relationshipMuted: "Silenciado por ti", + hideContent: "Ocultar para mí", + unhideContent: "Mostrar de nuevo", + hiddenByYou: "Ocultado por ti", + settingsTabContent: "Contenido", + settingsHiddenTitle: "Contenido oculto", + settingsHiddenDesc: "Habitantes que has silenciado y contenido que has ocultado.", + settingsMutedLabel: "Habitantes silenciados", + settingsHiddenLabel: "Contenido oculto", + settingsMutedEmpty: "Nadie silenciado.", + settingsHiddenEmpty: "Nada oculto.", + settingsPmVisibilityFollowing: "Solo gente a la que sigo", + settingsMentionsVisibilityTitle: "Menciones", + settingsMentionsVisibilityDesc: "Elige de quién te llegan las menciones.", pmMutualNotice: "El envío es una barrera de UX. La recepción es un filtro de atención (el cifrado ya está replicado).", - pmBlockedNonMutual: "Solo puedes enviar mensajes privados a habitantes con apoyo mutuo.", inhabitantsPendingFollowsTitle: "Solicitudes de apoyo pendientes", inhabitantsPendingAccept: "Aceptar", inhabitantsPendingReject: "Rechazar", - bxEncrypted: "CIFRADO", - bxEncryptedHexLabel: "Texto cifrado (vista previa)", tribeSectionGovernance: "GOBIERNO", - tribeSubStatusPublic: "PÚBLICA", - tribeSubStatusPrivate: "PRIVADA", - tribeSubInheritedPrivate: "PRIVADA (heredada de la tribu principal)", tribePadInviteRequired: "No tienes acceso al pad. Pide una invitación para acceder al contenido.", tribeChatInviteRequired: "No tienes acceso al chat. Pide una invitación para acceder al contenido.", tribeGovernanceDesc: "Gobierno interno de esta tribu.", @@ -3530,137 +4550,125 @@ module.exports = { tribeGovernanceNoCandidatures: "Sin candidaturas abiertas.", tribeGovernanceAddRule: "Añadir regla", tribeGovernanceNoRules: "Sin reglas todavía.", - tribeGovernanceNoLeaders: "Sin líderes elegidos todavía.", + tribeGovernanceNoLeaders: "Sin liderazgo elegido todavía.", tribeGovernanceComingSoon: "Próximamente en el módulo de gobierno de esta tribu.", tribeGovNoProposals: "Aún no hay ninguna propuesta", tribeGovNoLaws: "Aún no hay ninguna ley", tribeGovNoRevocations: "Aún no hay ninguna revocación", tribeGovNoHistorical: "Aún no hay ningún registro", logsTitle: "Bitácora", - logsDescription: "Registra tu experiencia en la red.", - logsReadMore: "Leer más", + logsDescription: "Lleva un diario privado de tus experiencias.", logsViewTitle: "Bitácora", logsColumnType: "Tipo", logsView: "Ver", - logsManualPrompt: "Escribe tu bitácora", logsUpdateButton: "Actualizar", - logsEditTitle: "Editar entrada", - logsCreateDescription: "Registra tu experiencia...", + logsEditTitle: "Actualizar registro", logsCreateTitle: "Crear entrada", logsWriteButton: "Escribir", logsTextPlaceholder: "Describe tus experiencias...", - logsTextField: "Texto", - logsLabelPlaceholder: "Etiqueta...", - logsLabelField: "Escribe tu bitácora (etiqueta)", - logsAiDisabledWarn: "Activa el módulo de IA en /modules para usar entradas escritas por IA.", - logsAiContextValue: "Día de blockchain", - logsAiContext: "Contexto", - logsAiModStatus: "AImod", - logsModeApply: "¡Aplicar!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "Asistente IA", logsModeManual: "Manual", logsModeAI: "IA", - logsColumnDelete: "Eliminar", - logsColumnEdit: "Editar", logsColumnLog: "Bitácora", logsColumnDate: "Fecha", logsDelete: "Eliminar", - logsEdit: "Editar", + logsEdit: "Actualizar", logsFilterAlways: "SIEMPRE", logsFilterYear: "ÚLTIMO AÑO", logsFilterMonth: "ÚLTIMO MES", logsFilterWeek: "ÚLTIMA SEMANA", logsFilterToday: "HOY", - logsFilterRecent: "RECIENTES", - logsFilterAll: "TODOS", + logsComments: "Comentarios", + logsAuthorEntries: "Entradas", logsCreate: "Crear entrada", logsExport: "Exportar bitácora", - logsExportOne: "Exportar", + logsExportOne: "Exportar registro", logsViewDetails: "Ver detalles", logsGenerateButton: "Generar texto", - logsSearchText: "Buscar en bitácoras...", + logsSearchText: "Buscar en Bitácora...", logsSearchAnyType: "Cualquier tipo", logsSearchButton: "Buscar", logsEmpty: "Aún no hay entradas.", + logsNothingNew: "No hay acciones nuevas que registrar.", modulesLogsLabel: "Bitácora", - modulesLogsDescription: "Módulo para registrar (vía asistente de IA) tus experiencias.", + modulesLogsDescription: "Módulo para llevar un diario privado de tus experiencias.", statsLogsTitle: "Bitácora", - statsLogsEntries: "Entradas", typeLog: "BITÁCORA", - blockchainCycle: "Ciclo", larpTitle: "L.A.R.P.", larpDescription: "Una capa de juego de rol en vivo para la experimentación colaborativa.", + larpNoHousesMatch: "Ninguna casa coincide con tu búsqueda.", + larpSearchPlaceholder: "Buscar en L.A.R.P....", larpAllHouses: "Casas:", larpFilterRuling: "RIGE", larpFilterHouses: "CASAS", larpFilterRules: "FAQ", larpRulesTitle: "FAQ del L.A.R.P.", - larpRulesEntryTitle: "Casa de inicio", larpRulesEntryText: "Todo habitante comienza en ACADEMIA por defecto. Desde ACADEMIA, elige una casa en el panel \"Unirse a una casa\": esto abre su test de entrada. Responde las 10 preguntas y envía. Si superas el umbral (7/10), serás admitido automáticamente en esa casa; si no, serás rechazado y permanecerás en ACADEMIA. En cualquier caso el sistema registra tu OasisID y el ciclo del intento e impide otro intento hasta que pase el enfriamiento de 30 ciclos.", - larpRulesLeaveText: "Los miembros de cualquier casa pueden volver a ACADEMIA en cualquier momento desde la página de su casa; al hacerlo se restablece su pertenencia, pero no se anula el enfriamiento del test.", - larpRulesGovernanceTitle: "Muro de gobierno", larpRulesGovernanceText: "Durante su ciclo, la casa gobernante luce la insignia \"Rige\" y es la única cuyo Muro se muestra públicamente bajo la pestaña RIGE.", - larpRulesSignTitle: "Emblema L.A.R.P.", - larpRulesSignText: "Los habitantes pueden activarlo (en /profile/edit > Sensores) para mostrar la imagen de su casa actual como un sello en su perfil, en su tarjeta de autor y de habitante. El sello enlaza a la página de la casa.", + larpRulesWallText: "Cada casa tiene un Muro donde escriben sus miembros. El Muro de una casa es privado para la casa, salvo el de la casa gobernante y el de la ACADEMIA, que puede leer cualquiera.", + larpRulesSignText: "Las habitantes pueden activarlo (en /profile/edit > Sensores) para mostrar la imagen de su casa actual como un sello en su perfil, en su tarjeta de autora y de habitante. El sello enlaza a la página de la casa.", larpPostsTitle: "Muro", larpPostsEmpty: "Aún no hay publicaciones.", - larpPostLabel: "Nueva publicación", larpPostPlaceholder: "¿Qué tiene que decir esta casa?", - larpPostPublic: "Pública (visible para cualquiera, si no solo miembros)", larpPostSubmit: "Publicar", - larpPostMembersOnly: "Solo miembros", larpCycleLabel: "Ciclo:", - bankRulesArchTaxFormula: "ARCH Tax(usuario) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", + bankRulesArchTaxFormula: "ARCH Tax(habitante) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", bankRulesArchTaxNote: "El impuesto ARCH refleja el coste a largo plazo de hacer crecer y mantener el archivo de la red: cuanto más antiguos sean tus datos en la red, mayor será tu parte del coste de mantenimiento.", bankRulesChipFormula: "band = ratio / reducer, donde ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). alto ≥ 0.66, medio ≥ 0.33, en otro caso bajo.", bankRulesChipNote: "El chip de cada elemento se colorea respecto al mensaje más grande visto en la red y se suaviza por el número de habitantes activos — una red más grande distribuye la carga y empuja más elementos a la banda verde.", - bankRulesEcoTaxFormula: "ECO Tax(usuario) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "El impuesto ECO se deduce del UBI bruto cuando se calcula la asignación del epoch. Los ECO deducidos no se redistribuyen en este epoch — permanecen en el saldo del pub y alimentan el pool del próximo epoch a través de los topes reserveMin / alpha.", + bankRulesEcoTaxFormula: "ECO Tax(habitante) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", + bankRulesEcoTaxNote: "El impuesto ECO se deduce de la RBU bruta cuando se calcula la asignación de la época. Los ECO deducidos no se redistribuyen en esta época — permanecen en el saldo del pub y alimentan el fondo de la próxima época a través de los topes reserveMin / alpha.", bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation", - bankRulesKarmaNote: "El karma pondera tu participación en la red de cara a la asignación de UBI.", + bankRulesKarmaNote: "El karma se deriva de tus acciones (publicaciones, votos, participación en el parlamento/tribunales, etc.) con un factor de decaimiento temporal, menos los gramos de carbono que genera tu feed. Lo que recibe tu contenido también cuenta: las opiniones, votos, reacciones y reproducciones positivas suman karma, y las opiniones o votos negativos lo restan cuando varios habitantes coinciden.", bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax", bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)", bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", bankTaxesArchTaxFootprintNote: "El impuesto ARCH refleja el coste de hacer crecer y mantener el archivo de la red. Se calcula a partir del tiempo transcurrido entre tu primer bloque publicado y el bloque más nuevo de la red — cuanto más viva tu historia en el archivo, mayor será tu parte del coste de mantenimiento.", bankTaxesArchTaxNoteBold: "reducir el impuesto ARCH directamente", - bankTaxesArchTaxNoteIntro: "Tu impuesto ARCH se deduce del excedente que la red genera sobre tu UBI; el valor base del UBI es un mínimo inamovible. El impuesto nunca se deduce del importe fijo que corresponde a cada habitante como UBI. Los fondos deducidos alimentan el algoritmo de redistribución de la riqueza y se canalizan a otros habitantes a través de sus reclamaciones de UBI. Los habitantes más nuevos y los que se dediquen específicamente a tareas de archivo, replicación y mantenimiento probablemente tengan alguna tarea dedicada a mantener sano el archivo de la red en tu nombre. O pueden dedicarse a ", + bankTaxesArchTaxNoteIntro: "Tu impuesto ARCH se deduce del excedente que la red genera sobre tu RBU; el valor base de la RBU es un mínimo inamovible. El impuesto nunca se deduce del importe fijo que corresponde a cada habitante como RBU. Los fondos deducidos alimentan el algoritmo de redistribución de la riqueza y se canalizan a otras habitantes a través de sus reclamaciones de RBU. Los habitantes más nuevos y los que se dediquen específicamente a tareas de archivo, replicación y mantenimiento probablemente tengan alguna tarea dedicada a mantener sano el archivo de la red en tu nombre. O pueden dedicarse a ", bankTaxesArchTaxNoteOutro: " mediante acuerdos de poda, deduplicación de datos redundantes e infraestructura de mantenimiento compartida, y su contribución continua puede ser requerida por consenso de la red.", bankTaxesUserNoteChips: "El chip de impuesto ECO mostrado junto a cada elemento cambia de color (verde / amarillo / rojo) según el tamaño del elemento en relación con el bloque más grande visto en la red, suavizado por el número de habitantes activos — una red solar-punk más grande distribuye la carga y empuja más elementos a la banda verde.", bankTaxesUserNoteOtherParams: "El impuesto ECO se deriva actualmente de la huella de carbono de cada bloque, pero puede incorporar otros parámetros con el tiempo — energía gastada en replicación, almacenamiento redundante entre peers, ancho de banda de blobs, coste computacional de descifrado, huella de minería de transacciones asociadas, o cualquier otra carga medible que la red acuerde valorar.", gamesHallDate: "Fecha", gamesNewRecord: "¡Nuevo récord!", - indexingEditProfileLink: "Editar perfil", - indexingMessage: "El servidor SSB está indexando. Vuelve en un momento.", - indexingRefreshNote: "Esta página se recargará automáticamente.", - indexingTitle: "Indexando", + indexingRefreshNote: "Esta página se recarga cada 10 segundos.", + indexingMessage: "Oasis está sincronizando la red de nodos distribuida. Por favor, espera.", + indexingTitle: "Sincronizando", padsNoItems: "Aún no hay pads.", padVersionHistory: "Historial", + padAttributionView: "Por autora", padVersionView: "Ver versión", statsChat: "Chats", statsChatMessage: "Mensajes de chat", - statsGameScore: "Gaming", + statsGameScore: "Juegos", + statsWiki: "Páginas wiki", statsPad: "Pads", statsPadEntry: "Entradas de pad", typeGameScore: "PUNTUACIONES", bankTaxesLookupNote: "Introduce un ID de bloque para buscarlo en tu feed local.", bankTaxesUserNoteChipsClick: "Haz clic en cualquier chip para inspeccionar su bloque en el blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "Código de invitación", larpRulesInviteEntryText: "Los miembros de las casas pueden generar códigos de invitación que dan acceso directo a la casa.", larpRulesOneHouseText: "Solo puedes pertenecer a una casa a la vez. Si no perteneces a ninguna, estás en ACADEMIA. Cada página de casa (no ACADEMIA) muestra un botón \"Salir de la Casa\" a sus miembros que restablece la pertenencia a ACADEMIA. Salir NO anula el enfriamiento del test.", - larpRulesOneHouseTitle: "Una casa cada vez", - larpRulesTestEntry: "Test WILL", - larpRulesTestEntryText: "Cada opción pondera una o varias casas; al enviar, el sistema suma los puntos y te admite automáticamente en la casa con mayor puntuación.", + larpRulesTestEntryText: "En el test WILL cada opción pondera una o más casas; al enviarlo, el sistema suma las puntuaciones y te admite automáticamente en la casa con más puntos.", larpWillTestHint: "Una vez completado, se te asignará la casa que más se ajuste a tus respuestas. Tus respuestas individuales se procesan localmente y nunca se almacenan — solo se publica en tu feed la asignación de casa resultante.", larpWillTestTitle: "Test WILL", - settingsLanDesc: "Anuncia periódicamente este peer a otras instancias de Oasis en la misma red local. Desactivar para detener las difusiones UDP.", + settingsLanDesc: "Anuncia periódicamente este peer a otras instancias de Oasis en la misma red local.", settingsLanEnable: "Activar difusión LAN", settingsLanTitle: "Difusión LAN", settingsReplicationTitle: "Replicación", settingsReplicationDesc: "Configura el número de saltos que tu nodo sigue desde tu propio feed al replicar contenido.", settingsReplicationHopsLabel: "Saltos", + settingsBlobCacheTitle: "Caché de medios", + settingsBlobCacheDesc: "El contenido de otros habitantes se guarda en este dispositivo. Elige cuánto espacio puede ocupar.", + settingsBlobCacheUsage: "Uso actual", + settingsBlobCacheFiles: "ficheros", + settingsBlobCacheLimit: "Límite", + settingsBlobCacheUnlimited: "Sin límite", + settingsBlobCacheCleanNow: "Limpiar ahora", + settingsBlobCacheCleaned: "{n} ficheros eliminados, {size} liberados.", aiApproveTagsLabel: "Etiquetas (separadas por comas)", aiApproveTagsPlaceholder: "p.ej. oasis, gobernanza, ecología", aiApproveRatingLabel: "Valoración", @@ -3671,36 +4679,28 @@ module.exports = { aiExchangeMarkHelpful: "+1 útil", larpCyclesUntilRuling: "Próximo ciclo de gobierno", larpVisitTribe: "Visitar tribu", - larpGoverning: "Gobernando:", + larpStartJourney: "Comenzar el viaje", + larpGoverning: "Gobierna:", + larpStartHere: "Empieza aquí:", larpMyHouse: "Mi Casa:", larpMembersCount: "Miembros", - larpMembersTitle: "Miembros", - larpNoMembers: "Aún no hay miembros.", larpRolesLabel: "Roles", larpFunctionLabel: "Función", larpMonthLabel: "Ciclo de gobierno", larpLeaveToAcademia: "Volver a ACADEMIA", - larpAcademiaJoinTitle: "Unirse a una casa", - larpAcademiaJoinHint: "Realiza el test psicológico de perfil para que se te asigne la casa que mejor encaja contigo, o canjea un código de invitación compartido por algún miembro de una casa.", - larpTakeTestButton: "Hacer el test de perfil", + larpAcademiaJoinTitle: "Casas L.A.R.P.", larpInviteRedeem: "Unirse a la Casa", larpInviteCreate: "Generar código de invitación", larpInvitePlaceholder: "Código de invitación", larpInviteBannerTitle: "Nuevo código de invitación", larpInviteBannerHint: "Comparte este código con alguien que esté en ACADEMIA. Expira en 30 ciclos o tras un uso.", - larpTestAssignedHouse: "Casa asignada", larpTestRankingTitle: "Puntuación por casa", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "Código de invitación de casa", - invitesHouseJoinButton: "Unirse a la Casa", - ecoTaxLabel: "ECO Tax", - ecoinTaxLabel: "ECOin Tax", + ecoTaxLabel: "Tasa ECO", bankTaxes: "Impuestos", - bankOverviewYourTaxes: "Tus impuestos", - bankTaxesNetworkTitle: "Impuestos de la red", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesUserTitle: "Tus impuestos", + bankOverviewYourTaxes: "Impuestos totales", + bankTaxesEcoTaxTitle: "Tasa ECO", + bankTaxesArchTaxTitle: "Tasa ARCH", bankTaxesUserAmount: "Tu ECO tax (precio a devolver)", bankTaxesArchTaxUserAmount: "Tu ARCH tax (precio a devolver)", bankTaxesArchTaxFirstBlock: "Edad de tu primer bloque", @@ -3724,57 +4724,50 @@ module.exports = { bankTaxesLookupPlaceholder: "ID del bloque (p. ej. %abc...=.sha256)", bankTaxesLookupButton: "Inspeccionar", bankTaxesLookupNotFound: "No se ha encontrado en tu feed local ningún bloque con ese ID.", - bankTaxesLookupAuthor: "Autor", + bankTaxesLookupAuthor: "Autora", bankTaxesLookupType: "Tipo", bankTaxesLookupSize: "Tamaño", bankTaxesLookupCarbon: "Huella de carbono", - bankTaxesLookupEcoin: "ECO Tax", + bankTaxesLookupEcoin: "Tasa ECO", bankExchangeEcoTaxAnnual: "ECOin Taxes (anual)", bankExchangeEcoTaxMonthly: "ECOin Taxes (mensual)", bankTaxesTypesLabel: "Selecciona qué impuestos quieres pagar", bankTaxesTypesApply: "Establecer mis impuestos", bankRulesPoolTitle: "Bolsa mensual", - bankRulesShareTitle: "Reparto por usuario", + bankRulesShareTitle: "Reparto por habitante", bankRulesKarmaTitle: "Karma (puntuación de implicación)", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesArchTaxTitle: "ARCH Tax", + bankRulesEcoTaxTitle: "Tasa ECO", + bankRulesArchTaxTitle: "Tasa ARCH", bankRulesChipTitle: "Banda de color del chip ECO Tax", bankRulesEpochKind: "Granularidad de epoch", bankRulesAlpha: "Alpha (cuota máx. del balance del pub por epoch)", bankRulesReserveMin: "Reserva mínima (se mantiene en el balance del pub)", bankRulesCapPerEpoch: "Tope por epoch (absoluto)", - bankRulesGraceDays: "Periodo de gracia del claim", + bankRulesGraceDays: "Periodo de gracia de la reclamación", bankRulesWMin: "Peso mínimo (w_min)", bankRulesWMax: "Peso máximo (w_max)", - bankRulesFloor: "Suelo por usuario (bruto)", - bankRulesCapUser: "Tope por usuario por epoch", + bankRulesFloor: "Suelo por habitante (bruto)", + bankRulesCapUser: "Tope por habitante por epoch", bankRulesEcoTaxRate: "Tasa", bankRulesArchTaxRate: "Tasa", bankRulesCarbonFactor: "Factor de carbono", - statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Aún no hay tombstones en la red.", blockchainBlockNotAvailable: "Este bloque no está disponible en tu feed local. Puede pertenecer a un peer del que aún no replicas.", blockchainBackToBlockexplorer: "Volver al blockexplorer", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeTitle: "Transcode BCS", + audioTranscodeButton: "TRANSCODIFICAR", audioTranscodeDetailTitle: "Transcodificar", audioTranscodeDetailDescription: "Decodifica la carga útil incrustada y el mapa de composición original de la blockchain.", - audioTranscodeStegoTitle: "Incrustado en la forma de onda", - audioTranscodeStegoOasisId: "By", audioTranscodeStegoTimestamp: "Generado el", - audioTranscodeStegoMessage: "TEXT", + audioTranscodeStegoMessage: "TEXTO", audioTranscodeStegoEmpty: "(ninguno)", audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "No se ha podido decodificar ninguna carga esteganográfica en este audio.", audioTranscodeCompositionEmpty: "Este audio no incluye una composición de blockchain guardada.", - audioBackToAudio: "Volver al audio", audioBackToBcs: "Volver a BCS", - audioAuthorLabel: "Autor", melodyCompositionTitle: "Mapa de composición de la blockchain", - melodyFilterMine: "MINE", - melodyByLabel: "Por", - melodyAllEmpty: "Todavía no hay composiciones BCS de otros habitantes.", + melodyFilterMine: "MÍAS", + melodyAllEmpty: "Todavía no hay composiciones BCS de otras habitantes.", melodyNoteUnavailable: "el bloque no está en tu feed local", melodyUploadBcsTitle: "Publicar", melodyUploadBcsButton: "Publicar", @@ -3784,7 +4777,6 @@ module.exports = { melodyUploadBcsNameNote: "El audio se publicará con el nombre auto-generado", larpLastAttemptTitle: "Tu último intento", larpLastAttemptHouse: "Casa", - larpLastAttemptResult: "Resultado", larpLastAttemptWhen: "Cuándo", larpLastAttemptCooldown: "Próximo intento en", larpTestTitle: "Test de entrada", @@ -3793,38 +4785,25 @@ module.exports = { larpTestCooldownActive: "Próximo test disponible en", larpTestCooldownDays: "ciclos", larpTestResultTitle: "Resultado del test", - larpTestPassed: "Test superado — ¡bienvenida!", - larpTestFailed: "Test no superado", larpTestScore: "Puntuación", larpTestNextAttempt: "Puedes intentar otro test en 30 ciclos.", larpGoToHouse: "Ir a tu casa", larpBackToAcademia: "Volver a ACADEMIA", - larpBackToHouses: "◀ Casas", larpBadgeYou: "Tú", larpBadgeRuling: "Rige", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Módulo para la capa de juego de rol en vivo de Oasis (las 9 Casas).", - melodyUploadTitlePlaceholder: "Título de la composición", - melodyUploadDescPlaceholder: "Descripción opcional", - bankTaxesUserNote: "Tu impuesto ECO se deduce del excedente que la red genera sobre tu UBI; el valor base del UBI es un mínimo inamovible. El impuesto nunca se deduce del importe fijo que corresponde a cada habitante como UBI. Los fondos deducidos alimentan el algoritmo de redistribución de la riqueza y se canalizan a otros habitantes a través de sus reclamaciones de UBI. Aquellos otros habitantes que reciban más UBI para sus proyectos probablemente tengan alguna tarea dedicada a ayudarte a reducir tu impuesto ECO. O pueden dedicarse a reducir el impuesto ECO directamente, y su contribución continua puede ser requerida por consenso de la red.", - bankTaxesArchTaxNote: "El impuesto ARCH refleja el coste de hacer crecer y mantener el archivo de la red. Se calcula a partir del tiempo transcurrido entre tu primer bloque publicado y el bloque más nuevo de la red — cuanto más viva tu historia en el archivo, mayor será tu parte del coste de mantenimiento. Al igual que el impuesto ECO, el impuesto ARCH se deduce del excedente que la red añade sobre tu UBI; el UBI base nunca se reduce por él.", - bankTaxesExample: "Ejemplo", - bankTaxesSummaryNote: "Total agregado de todos los tipos de impuestos que aplica la red. Actualmente solo el impuesto ECO (huella de carbono) está activado; futuros tipos de impuestos aparecerán como filas adicionales y contribuirán al total.", - audioTranscodeDescription: "Interpretación del audio: la composición original de la blockchain que lo produjo, además del OasisID, marca temporal y mensaje oculto incrustado en la forma de onda mediante esteganografía.", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - gameScoreLabel: "PUNTUACIONES", + modulesLarpDescription: "Módulo para la capa de juego de rol en vivo.", larpProfileQ1: "Cuando te enfrentas a un problema complejo, ¿qué haces primero?", - larpProfileQ1O1: "Hablar con otras personas para buscar consenso", + larpProfileQ1O1: "Hablar con otros habitantes para buscar consenso", larpProfileQ1O2: "Construir un prototipo para probarlo", larpProfileQ1O3: "Investigar la literatura", larpProfileQ1O4: "Sabotear el sistema que lo provoca", larpProfileQ2: "¿Qué da sentido a tu trabajo diario?", - larpProfileQ2O1: "Defender a las personas que quiero", + larpProfileQ2O1: "Defender a los habitantes que quiero", larpProfileQ2O2: "Crear algo tangible", larpProfileQ2O3: "Sanar o cuidar la vida", larpProfileQ2O4: "Trabajar con palabras e ideas", - larpProfileQ2O5: "Hacer reír a los demás", + larpProfileQ2O5: "Hacer reír a las demás", larpProfileQ3: "Cuando surge un conflicto en tu grupo, tú…", larpProfileQ3O1: "Lideras el diálogo", larpProfileQ3O2: "Tomas partido y te mantienes firme", @@ -3887,7 +4866,52 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "Selecciona qué modo de navegación UX quieres para tu interfaz.", uxModeMenus: "Bloques", - uxModeAINav: "AI", + uxModeChats: "Conversaciones", + uxModeFeed: "Microblogging", + feedTrendingTitle: "Etiquetas en tendencia", + feedActiveTitle: "Habitantes Activos", + cnBrandSub: "Libre · P2P · Federada", + cnSyncedPeers: "Nodos sincronizados", + cnLicense: "Licencia", + cnNotAccessible: "El contenido no está accesible en este momento.", + cnUntitled: "Sin título", + cnSearchPlaceholder: "Buscar…", + cnHubTitle: "HUB Clearnet", + cnHubDescription: "Contenido público compartido por las habitantes de esta red Oasis.", + cnHubEmpty: "Aún no se ha compartido contenido público en la Clearnet.", + cnPublicContent: "Contenido", + cnTribesContent: "Tribus", + cnNetworkContent: "Red", + cnCategoryEmpty: "No hay contenido en esta categoría.", + cnInhabitantEmpty: "Este habitante aún no ha publicado contenido en la Clearnet.", + cnPrevPage: "← Anterior", + cnNextPage: "Siguiente →", + cnPageOf: "Página {page} de {pages}", + cnMoreInfo: "Más información →", + cnOpenCourse: "Curso abierto", + cnAuction: "SUBASTA", + cnExchange: "INTERCAMBIO", + cnRssDescription: "publicados por las habitantes de este HUB", + cnKindAudio: "Audio", + cnKindBlog: "Blog", + cnKindBookmark: "Marcador", + cnKindDocument: "Documento", + cnKindEvent: "Evento", + cnKindFeed: "Feed", + cnKindImage: "Imagen", + cnKindJob: "Empleo", + cnKindMarket: "Mercado", + cnKindPodcast: "Podcast", + cnKindProject: "Proyecto", + cnKindCourse: "Curso", + cnKindShop: "Tienda", + cnKindTorrent: "Torrent", + cnKindFile: "Fichero", + cnKindVideo: "Vídeo", + cnKindWiki: "Wiki", + cnKindRoom: "Sala", + cnKindMap: "Mapa", + uxModeAINav: "IA", } }; diff --git a/src/client/assets/translations/oasis_eu.js b/src/client/assets/translations/oasis_eu.js index f67b698..c5fc2a2 100644 --- a/src/client/assets/translations/oasis_eu.js +++ b/src/client/assets/translations/oasis_eu.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { eu: { - bbAdd: "Gehitu", - bbQuickActions: "Ekintza azkarrak", - bbPickTitle: "Ainguratu modulu bat", - bbPickDesc: "Aukeratu modulu bat beheko barrako + hutsunerako.", - bbPickNone: "Bat ere ez", - bbManage: "Editatu", - bbYourBar: "Zure barra", - bbRemove: "Kendu", - bbFull: "Gehienekora iritsi da", - bbDone: "Eginda", - activityChangeFilter: "Aldatu iragazkia", - emptyConnectHint: "Konektatu pub batera sarearekin sinkronizatzeko.", - emptyConnectCta: "Konektatu pub batera", - menuCommunity: "Komunitatea", - activityGroupCommunity: "Komunitatea eta gobernantza", - activityGroupOrg: "Antolakuntza", - activityGroupComm: "Komunikazioa", - activityGroupEconomy: "Ekonomia", - activityGroupMedia: "Edukia eta multimedia", - activityGroupOther: "Bestelakoak", - languageName: "Basque", shopOrderStatusPaid: "Ordainketa jasota", shopOrderStatusReceived: "Jasota", shopOrderMarkPaid: "Markatu ordainketa jasota", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "Erosketak", shopMyOrdersDescription: "Zure erosketa-eskaerak.", shopMyOrdersEmpty: "Oraindik ez duzu erosketarik.", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PM Saltzailea", shopOrderSeller: "Saltzailea", - shopOrderPaymentConcept: "Ordainketa", shopOrderInvoice: "Faktura", shopOrderInvoiceConcept: "Faktura", shopOrderStatusPending: "Zain", shopOrderStatusAccepted: "Onartua", shopOrderStatusRejected: "Baztertua", shopOrderStatusShipped: "Bidalia", - shopOrderStatusDelivered: "Entregatua", shopOrderAccept: "Onartu", shopOrderReject: "Baztertu", shopOrderMarkShipped: "Markatu bidalia", - shopOrderMarkDelivered: "Markatu entregatua", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "Kontratua", + shopOrderPmBuyer: "PM Eroslea", shopOrderContractConcept: "Dendako eskaera", shopOrdersPending: "Eskaera zain", all: "Guztiak", @@ -53,7 +28,7 @@ module.exports = { recent: "Azkenak", anonymous: "Anonimoa", unnamed: "Anonimoa", - authorLabel: "EGILEA", + authorLabel: "Egilea", delete: "Ezabatu", save: "Gorde", vote: "Bozkatu", @@ -75,18 +50,17 @@ module.exports = { viewJson: "Ikusi JSON", matchScore: "Bat-egite puntuazioa", preferencesLabel: "Hobespenak", - inhabitantProfileTitle: "Biztanlearen profila", + inhabitantProfileTitle: "Biztanlea", contentWarningLabel: "Eduki abisua", invalidPost: "Eduki baliogabea", invalidPostHint: "Mezu honek testu baliogabea/hutsa du.", - spreadBy: "Honek", spreadChron: "Hedapena", spreaded: "Hedatua", - spreadMore: "gehiago", spreadContentUnavailable: "Edukia oraindik ez dago eskuragarri (erreplikazioa zain)", filteredByTag: "Etiketaz iragazia", filteredByTagTitle: "Etiketaz iragazia", feedPublishedSuccess: "Jarioa ondo argitaratu da!", + feedMastodonFailed: "Oasisen argitaratu da, baina ezin izan da Mastodonera bidali.", tribeFeedSent: "Mezua ondo bidali da!", tribeContentEncrypted: "Eduki zifratua", tribeTorrentsEmpty: "Oraindik ez dago torrentik.", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "Larritasunik ez", calendarDeleteDate: "Ezabatu data", calendarIntervalUntil: "Arte", - bookmarkCategory: "Kategoria", bookmarkLastVisit: "Azken bisita", profileClearnetBookmarksLabel: "Laster-markak", - forumFilterHot: "Beroa", invitesPort: "Ataka", - currentlyUnrecheable: "ERROREA!", - peerHostValidation: "IPv4 baliozkoa (adib. 192.168.1.100) edo ostalari-izena (adib. pub.example.com)", + peerHostValidation: "IPv4 baliozkoa (adib. 192.168.1.100) edo ostalari-izena (adib. pub.solarnethub.com)", peerPortValidation: "Ataka 1-65535", peerKeyValidation: "SSB ed25519 gako publikoa (@<44 karaktere base64>=.ed25519)", inboxJobUnsubscribedTitle: "Zure lan-eskaintzatik baja", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "Zereginaren deskribapena", exportPasswordLabel: "Esportazio pasahitza", importPasswordPlaceholder: "Sartu pasahitza", - searchCasesPlaceholder: "Bilatu kasuak…", + searchCasesPlaceholder: "Bilatu hemen: Auzitegiak...", shopBuyDeliveryAddressPlaceholder: "Bidalketa-helbidea", shopBuyNotesPlaceholder: "Oharrak", bankTaxesUserNoteBold: "ECO zerga zuzenean murriztea", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "Lagundu kasua", courtsSettlementAcceptBtn: "Onartu akordioa", courtsSupportCount: "Babesak", - courtsVerdictVoteTitle: "Bozkatu epaia", - courtsVerdictVoteLabel: "Epaiaren botoa", - courtsVerdictVoteAccept: "Onartu", - courtsVerdictVoteReject: "Baztertu", - courtsVerdictVoteSubmit: "Bidali botoa", reportsSetStatus: "Ezarri egoera", reportsStatusClosed: "ITXITA", statsAvgPerInhabitant: "Batez bestekoa biztanleko", statsCarbonMaxAnnual: "Urteko gehienezko estimazioa", statsCarbonNetwork: "Sarearen guztira", statsCarbonOfEstMax: "estimatutako gehienezko ahalmenaren", + statsCarbonYearProgress: "urtea igarota", + statsNetworkTitle: "Sarea", + statsStorageTitle: "Biltegiratzea", + statsEcoTaxTitle: "ECO tasa", + statsAccountTitle: "Kontua", + statsAveragesTitle: "Batez bestekoak", statsCarbonOfNetwork: "sarearen guztizkoaren", statsCarbonUser: "Zure aztarna", statsContentCountColumn: "Kopurua", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "Etiketa nagusiak", statsTopTypesTitle: "Eduki mota nagusiak", statsTotalMsgs: "Mezuak guztira", - feedViewDetails: "Ikusi xehetasunak", - eventFileLabel: "Erantsi irudia", - taskFileLabel: "Erantsi irudia", - courtsRespondentRequired: "Demandatuaren IDa beharrezkoa da", extended: "Multibertsoa", extendedDescription: [ "Norbait laguntzen duzunean, laguntzen dituzun erabiltzaileen bidalketak deskargatu ditzakezu. Mezu horiek hemen azalduko dira, dataren arabera antolatuta.", @@ -215,40 +183,70 @@ module.exports = { ], profile: "Abatarra", inhabitants: "Bizilagunak", - peersReplicatedFeeds: "Errepikatutako jarioak", + peersReplicatedFeeds: "Erreplikatuak", + peersTorColumn: "TOR", + peersTorYes: "Tor bidez irisgarria", + peersTorNo: "Ez da Tor bidez irisgarria", + peersOasisId: "ID", + peersOasisVersion: "Bertsioa", graphos: "Graphos", graphosDescription: "Sarearen mapa interaktiboa zure inguruan.", + graphosFederation: "Federazioa", + graphosFedPubs: "PUBak", + graphosFedUnreachable: "Iritsiezinak", + graphosFedLinks: "Elkarrekiko loturak", + graphosFedIsolated: "Isolatuak", + graphosFedNoData: "Daturik gabe", + graphosFedMutual: "Elkarrekikoa", + graphosFedOneWay: "Norabide bakarrean", + graphosFedTransport: "Sarea", + graphosFedWith: "Honekin federatua", + graphosZoom: "Handitu", + graphosFedInhabitants: "Biztanleak", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "Zure nodoak oraindik ez du PUBik ezagutzen.", graphosViewingGraphOf: "Ikusten", graphosBackToMine: "← Nire sarea", graphosShowing: "Erakusten", graphosYou: "Zu", graphosTotalNodes: "Nodo guztiak", - manualMode: "Eskuzko modua", mentions: "Aipamenak", mentionsDescription: [ - strong("@Aipatzen zaituzten bidalketak."), - ", gaurkotasunaren arabera antolatuta.", + strong("Zu @aipatzen zaituzten mezu guztiak"), + ".", ], - nextPage: "Hurrengoa", - previousPage: "Aurrekoa", noMentions: "Ez duzu aipamenik jaso, oraindik.", + mentionsSearchPlaceholder: "Bilatu hemen: Aipamenak...", privateReminders: "OROIGARRIAK", + inboxFilterPms: "MPak", + inboxFilterNotifications: "Jakinarazpenak", + inboxMarkRead: "Irakurritako gisa markatu", + inboxMarkUnread: "Irakurri gabeko gisa markatu", + inboxUnreadChip: "Irakurri gabe", + inboxMarkAllRead: "Dena irakurritzat markatu", + inboxDeleteShown: "Dena ezabatu", + inboxArchive: "Artxibatu", + inboxUnarchive: "Desartxibatu", + inboxFilterArchived: "Artxibatuak", + inboxEarlierMessages: "aurreko mezu", + inboxSortThread: "Harien arabera", + inboxSortRecent: "Berrienak lehenik", + inboxAllBots: "Botak", + settingsInboxBotsTitle: "Inbox jakinarazpenak", + settingsInboxBotsDesc: "Aukeratu zein botek jakinaraz diezazuketen Inboxean. Markatu gabeko botak ezkutatu egiten dira eta haien abisuak ez dira zenbatzen.", inboxFiltersLabel: "Iragazkiak:", inboxToggleToMutuals: "Mutuora aldatu", inboxToggleToWhole: "Guztiora aldatu", - privateFrom: "Nork", - privateTo: "Nori", privateDate: "Data", peers: "Parekoak", searchDescription: "Deskribapena", - imageSearch: "Irudiak bilatu", searchFromLabel: "Hasiera", searchToLabel: "Bukaera", - searchMinOpinionsLabel: "Gutxi. iritziak", - searchInhabitantLabel: "Biztanle (Oasis ID)", searchQueryLabel: "Bilaketa", - searchPerPageLabel: "Emaitzak orriko", + searchPerPageLabel: "Emaitzak", settings: "Ezarpenak", + settingsDescription: "Konfiguratu zure sarearen ingurunea.", continueReading: "Irakurtzen jarraitu", moreComments: "iruzkin gehiago", readThread: "irakurri hari osoa", @@ -258,68 +256,47 @@ module.exports = { melodyDescription: 'Erreproduzitu zure blockchain-aren melodia — bloke bakoitza nota bat bihurtzen da.', melodyEmpty: 'Oraindik notarik ez — zure blockchain-ak ez du oraindik blokerik sortu.', melodyCompositionTitle: 'Konposizioa', - melodyCompositionHelp: 'Zure blockchain-eko bloke bakoitza musika-nota bat bihurtzen da, motaren eta tamainaren arabera.', - melodyStatsTitle: 'Motaren araberako banaketa', - melodyInhabitantLabel: 'Biztanlea', melodyTotalBlocks: 'Notak', - melodyType: 'Mota', - melodyCount: 'Blokeak', melodyFilterMine: 'NIREA', melodyFilterAll: 'GUZTIAK', - melodyFilterShare: 'Igo melodia', - melodyShareTitle: 'Partekatu zure melodia', - melodyShareHelp: 'Argitaratu zure uneko melodiaren irudia, besteek entzun eta birmoldatu ahal izateko.', - melodyShareTitleLabel: 'Izenburua (aukerakoa)', - melodyShareTitlePlaceholder: 'Blockchain mamu bat', - melodyShareSubmit: 'Argitaratu melodia', - melodyNoShared: 'Oraindik ez dago blockchain melodiarik.', melodyRegenerate: 'Birsortu', melodyDownload: "Deskargatu BCS", - profileActivityTitle: 'Jarduera', - profileActivityMore: 'Ikusi jarduera guztia', profileContentSectionTitle: 'Avatarraren Edukia', profileContentHelp: 'Aukeratu zein modulu agertuko diren zure abatarrean.', profileSensorsHelp: 'Zure abatarrean agertzen diren aukerako metrikak.', profileClearnetHelp: 'Oasis kanpotik atzitu daitezkeen moduluak.', profileHubAll: 'Guztiak', - profileHubTitle: 'Eduki Publikoa', - footerPulseTitle: 'Pultsua', - footerPulsePeers: 'Kideak', - footerPulseInbox: 'Sarrera-ontzia', - footerPulseSync: 'Azken sinkr.', - footerPulseEcoValue: 'ECO/h', - footerPulseLastActivity: 'Azken jarduera', footerLicenseLabel: 'Lizentzia', - profileSwitchToOasis: 'Itzuli Oasisera', - profileSwitchToClearnet: 'Murgildu Clearneten', - profileVisibilityFediverse: "Fediverse", - profileSwitchToFediverse: "Murgildu fediversoan", - coordLabel: 'Koordenatuak (Adb. A3)', - coordPlaceholder: 'Sartu koordenatuak', + profileVisibilityFediverse: "Multibertsoa", contributorsTitle: "Ekarpenak", colorLabel: 'Hautatu kolorea', paintButton: 'Koloreztatu!', - invalidCoordinate: 'Koordenatu okerrak', - goToMuralButton: "Ikusi Murala", totalPixels: 'Pixelak guztira', - pixeliaBy: "egilea", modules: "Moduluak", modulesViewTitle: "Moduluak", modulesViewDescription: "Konfiguratu zure ingurunea moduluak gaitu edo desgaituz.", inbox: "Sarrera-ontzia", + inboxSearchPlaceholder: "Bilatu hemen: Sarrera-ontzia...", multiverse: "Multibertsoa", - fediverse: "Fediverse", - fediverseDescription: "Kudeatu zure beste fediverso kontuak, edukia bidaltzea eta jasotzea barne.", + fediverse: "Multibertsoa", + fediverseDescription: "Kudeatu zure beste multibertso kontuak, edukia bidaltzea eta jasotzea barne.", fediverseStatus: "Egoera", - fediverseDisconnected: "Fediversoa deskonektatuta. Egiaztatu %LINK% edo konexioaren egoera.", - fediverseSettingsTitle: "Fediverse", + fediverseDisconnected: "Multibertsoaa deskonektatuta. Egiaztatu %LINK% edo konexioaren egoera.", + fediverseSettingsTitle: "Multibertsoa", fediverseInstanceLabel: "Helbidea", fediverseTokenLabel: "Sarbide token-a", - fediverseTokenHelp: "Ezarri zure sarbide token-a. Zure Mastodon kontua Oasis barrutik kudeatzeko erabiliko da.", fediverseConnect: "Konektatu", fediverseConnectedAs: "Honela konektatuta", + nextcloudServerLabel: "Zerbitzaria", + nextcloudUserLabel: "Erabiltzailea", + nextcloudFolderLabel: "Karpeta", + nextcloudUploadButton: "Igo", + nextcloudFileLabel: "Aukeratu fitxategi bat", + fileExternalLink: "Kanpoko esteka", + nextcloudErrConnect: "Ezin izan da Nextcloud-era iritsi datu hauekin.", + nextcloudErrUpload: "Nextcloud-era igotzeak huts egin du.", + nextcloudErrShare: "Nextcloud-ek ez du esteka publikorik eman fitxategiarentzat.", fediverseDisconnect: "Deskonektatu", - fediverseEmpty: "Zure %LINK% beste fediverso kontu batzuk konektatzen dituzunean, hemendik kudeatu ahal izango dituzu.", fediverseEmptyLink: "ezarpenetatik", fediverseComposePlaceholder: "Zer duzu buruan?", fediverseReplyPlaceholder: "Idatzi zure erantzuna…", @@ -328,21 +305,15 @@ module.exports = { fediverseReply: "Erantzun", fediverseBoosted: "bultzatu du", fediverseNoPosts: "Zure denbora-lerroa hutsik dago.", - fediverseThread: "Haria", fediverseTimeline: "Denbora-lerroak", - fediverseManageTimeline: "Kudeatu denbora-lerroa", fediverseFollowers: "Jarraitzaileak", fediverseFollowing: "Jarraitzen", fediversePosts: "Argitalpenak", fediverseJoined: "Elkartua", - fediverseOverview: "Ikuspegi orokorra", fediverseRefresh: "Freskatu", fediverseWrite: "Idatzi", fediversePreview: "Aurrebista", - fediverseEdit: "Editatu", - fediverseOpenFeed: "Ikusi jarioa", fediverseManage: "Kudeatu", - fediverseStatusNotConnected: "Konektatu gabe", fediverseError: "Ezin izan da Mastodon-ekin konektatu.", fediverseErrInstance: "Instantzia URL baliogabea.", fediverseErrAuth: "Token baliogabea edo iraungia.", @@ -353,17 +324,76 @@ module.exports = { fediverseErrPost: "Ezin izan da argitaratu.", fediverseErrMedia: "Ezin izan da fitxategia igo.", fediverseErrEmpty: "Idatzi zerbait edo erantsi fitxategi bat.", - popularLabel: "Nabarmenak", - topicsLabel: "Gaiak", - latestLabel: "Azkena", - summariesLabel: "Laburpenak", - threadsLabel: "Hariak", - multiverseLabel: "Multibertsoa", - inboxLabel: "Sarrera-ontzia", - invitesLabel: "Gonbidapenak", - walletLabel: "Zorroa", - legacyLabel: "Gakoak", - cipherLabel: "Zifratzailea", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Telefono zenbakia", + telegramSendCode: "Bidali kodea", + telegramCodeHelp: "Sartu Telegramek bidali dizun kodea (aplikazioa edo SMS).", + telegramCodeLabel: "Kodea", + telegramPasswordHelp: "Kontu honek bi urratseko egiaztapena du. Sartu pasahitza.", + telegramPasswordLabel: "Pasahitza", + telegramVerify: "Egiaztatu", + telegramCancel: "Utzi", + telegramChats: "Txatak", + telegramUnread: "Irakurri gabe", + telegramOpen: "Ireki", + telegramSend: "Bidali", + telegramYou: "Zu", + telegramNoChats: "Oraindik ez dago txatik.", + telegramNoMessages: "Oraindik ez dago mezurik.", + telegramComposePlaceholder: "Idatzi mezu bat…", + telegramChannel: "Kanala", + telegramGroup: "Taldea", + telegramPrivate: "Pribatua", + telegramErrConnect: "Ezin izan da Telegramekin konektatu.", + telegramErrTimeout: "Telegramek gehiegi tardatu du erantzuten.", + telegramErrAuth: "Telegram saioa baliogabea edo iraungita. Konektatu berriro.", + telegramErrFetch: "Ezin izan dira txatak kargatu.", + telegramErrSend: "Ezin izan da mezua bidali.", + telegramErrMissing: "API ID, API hash eta telefono zenbakia behar dira.", + telegramErrCode: "Kode okerra. Saiatu berriro.", + telegramErrCodeExpired: "Kodea iraungi da. Hasi berriro.", + telegramErrPassword: "Pasahitz okerra.", + telegramErrPhone: "Telefono zenbaki baliogabea.", + telegramErrApi: "API ID edo API hash baliogabea.", + telegramErrFlood: "Saiakera gehiegi. Itxaron pixka bat eta saiatu berriro.", + telegramErrNoLogin: "Ez dago saio-hasierarik abian. Hasi berriro.", + peertubeUsernameLabel: "Erabiltzailea", + peertubePasswordLabel: "Pasahitza", + peertubeSubscriptions: "Harpidetzak", + peertubeMyVideos: "Nire bideoak", + peertubeVideos: "Bideoak", + peertubeFollowers: "Jarraitzaileak", + peertubeViews: "ikustaldi", + peertubeLikes: "atsegin", + peertubeQuota: "Kuota", + peertubeUpload: "Bideoa igo", + peertubeUploadHelp: "Aukeratu bideo fitxategi bat. Zure kanalera igotzen da eta PeerTubek gero prozesatzen du.", + peertubeTitleLabel: "Izenburua", + peertubeDescriptionLabel: "Deskribapena", + peertubePrivacyLabel: "Pribatutasuna", + peertubePrivacyPublic: "Publikoa", + peertubePrivacyUnlisted: "Zerrendatu gabea", + peertubePrivacyPrivate: "Pribatua", + peertubeNoVideos: "Oraindik ez dago bideorik.", + peertubeComments: "Iruzkinak", + peertubeComment: "Iruzkindu", + peertubeCommentPlaceholder: "Idatzi iruzkin bat…", + peertubeNoComments: "Oraindik ez dago iruzkinik.", + peertubeOpen: "PeerTuben ireki", + peertubeWatch: "Ikusi", + peertubeChannel: "Kanala", + peertubeNoStream: "Bideo hau oraindik ezin da hemen erreproduzitu. Ireki PeerTuben.", + peertubeUploaded: "Bideoa igo da. PeerTube prozesatzen ari da.", + peertubeErrConnect: "Ezin izan da PeerTuberekin kontaktatu.", + peertubeErrAuth: "Erabiltzaile edo pasahitz okerra, edo saioa iraungi da. Konektatu berriro.", + peertubeErrInstance: "Instantziaren URL baliogabea.", + peertubeErrMissing: "Instantzia, erabiltzailea eta pasahitza behar dira.", + peertubeErrFetch: "Ezin izan dira bideoak kargatu.", + peertubeErrUpload: "Ezin izan da bideoa igo.", + peertubeErrTitle: "Izenburua behar da.", + peertubeErrComment: "Ezin izan da iruzkina argitaratu.", + peertubeErrNoFile: "Aukeratu bideo fitxategi bat.", bookmarksLabel: "Markagailuak", videosLabel: "Bideoak", torrentsLabel: "Torrentak", @@ -374,34 +404,22 @@ module.exports = { inhabitantsLabel: "Bizilagunak", trendingLabel: "Pil-pilean", eventsLabel: "Gertakariak", - tasksLabel: "Atazak", apply: "Aplikatu", menuPersonal: "Pertsonala", - menuContent: "Edukia", - menuBlogs: "Blogak", menuGovernance: "Gobernantza", menuOffice: "Bulegoa", - menuMultiverse: "Multibertsoa", - menuNetwork: "Sarea", + menuNetwork: "Komunitatea", menuCreative: "Sortzailea", menuEconomy: "Ekonomia", - menuMedia: "Multimedia", + menuMedia: "Liburutegia", menuTools: "Tresnak", comment: "Iruzkindu", subtopic: "Azpi-gaia", json: "JSON", - createdBy: "Sortzailea", unfollow: "Laguntza eten", follow: "Lagundu", block: "Blokeatu", unblock: "Desblokeatu", - newerPosts: "Bidalketa berrienak", - olderPosts: "Bidalketa zaharrenak", - feedRangeEmpty: "Hautatuako tartea hutsik dago jario honentzat. Saiatu ", - seeFullFeed: "jario osoa ikusiz", - feedEmpty: "Oasis sareak ez du kontu honen bidalketarik ikusi inoiz.", - beginningOfFeed: "Jarioaren hasiera da hau", - noNewerPosts: "Ez da bidalketa berriagorik jaso oraindik.", relationshipNotFollowing: "Ez zaitu laguntzen", relationshipTheyFollow: "Laguntzen zaitu", relationshipMutuals: "Elkar laguntzen", @@ -411,25 +429,23 @@ module.exports = { relationshipBlockedBy: "Blokeatzen zaitu", relationshipMutualBlock: "Elkar blokeatzen", relationshipNone: "Ez duzu laguntzen", - relationshipConflict: "Gatazka", relationshipBlockingPost: "Blokeatutako bidalketa", viewLikes: "Ikusi zabalpenak", + viewSupporters: "Ikusi laguntzaileak", spreadedDescription: "Bizilagunak zabaldutako bidalketen zerrenda.", - totalspreads: "Hedapenak guztira", - attachFiles: "Erantsi fitxategiak", preview: "Aurrebista", publish: "Idatzi", contentWarningPlaceholder: "Gehitu gaia bidalketari (hautazkoa)", - privateWarningPlaceholder: "Gehitu bizilagunak mezu pribatua bidaltzeko (hautazkoa)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "Idatzi zure sarreraren gorputza.", + publishTooLong: "Zure argitalpena luzeegia da. Laburtu ezazu, mesedez.", + keepTextRetry: "Berriro bidali", + dateInPastError: "Data ezin da gaur baino lehenagokoa izan", publishCustomDescription: [ "GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.", ], commentWarning: [ "GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.", ], - commentPublic: "publikoa", - commentPrivate: "pribatua", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -449,14 +465,12 @@ module.exports = { "beharko zenuke.", ], publishCustom: "Idatzi bidalketa aurreratua", - subtopicLabel: "Bidalketa honen azpi-gaia sortu", messagePreview: "Aurreikusi bidalketa", mentionsMatching: "Bat datozen aipamenak", mentionsName: "Izena", mentionsRelationship: "Erlazioa", - updateit: "LORTU EGUNERAKETAK!", updateBannerText: "Oasis-en bertsio berri bat eskuragarri dago.", - updateBannerAction: "Eguneratu orain →", + updateBannerAction: "Eguneratu orain!", info: "Infoa", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -464,29 +478,94 @@ module.exports = { timeAgo: "dela", sendTime: "honi buruz ", theme: "Gaia", - legacy: "Gakoak", - legacyTitle: "Gakoak", - legacyDescription: "Kudeatu zure sekretua (gako pribatua) bizkor eta segurtasunez.", - legacyExportButton: "Esportatu", - legacyImportButton: "Inportatu", + backupKeysExportButton: "Esportatu", + backupKeysImportButton: "Inportatu", ssbLogStream: "Blockchain", ssbLogStreamDescription: "Konfiguratu blockchain fluxuetarako mezu-muga.", + logWindowChip: "Azken {n} blockchain stream", saveSettings: "Ezarpenak gorde", - exportTitle: "Esportatu datuak", exportDescription: "Ezarri pasahitza (gutxienez 32 karaktere) zure gakoa zifratzeko", - exportDataTitle: "Babeskopia", + exportDataTitle: "Bat-bateko babeskopia", exportDataDescription: "Deskargatu zure datuak (gako sekretua izan ezik!)", exportDataButton: "Deskargatu datu-basea", - pubWallet: "PUB Wallet", - pubWalletDescription: "Ezarri PUB wallet-aren URLa. Hau PUB transakzioetarako erabiliko da (RBU barne).", - pubWalletConfiguration: "Ezarpenak gorde", - importTitle: "Inportatu datuak", importDescription: "Inportatu zifratutako sekretua (gako pribatua) zure abatarra gaitzeko", - importAttach: "Gehitu zifratutako fitxategia (.enc)", - passwordLengthInfo: "Pasahitzak 32 karaketereko luzera izan behar du.", passwordImport: "Idatzi pasahitza zure karpeta nagusian gordeko diren datuak deszifratzeko (izena: secret)", exportPasswordPlaceholder: "Erabili minuskulak, maiuskulak, zenbakiak eta sinboloak.", - fileInfo: "Zifratutako gako sekretua zure etxeko karpetan gordeko da. (izena: oasis.enc)", + fileInfo: "Zure gako sekretu zifratua zure gailura deskargatuko da (izena: oasis.enc)", + developer: "Garapena", + devTitle: "Garapena", + welcomeBannerText: "Zure avatarra oraindik ez dago konfiguratuta.", + aiSuggestionBanner: "Iradokizuna:", + aiSuggestionsEnable: "AAren iradokizunak", + welcomeBannerAction: "Hasi", + welcomeTitle: "Ongi etorri", + welcomeStepIdentity: "Nortasuna", + welcomeStepType: "Mota", + welcomeStepUse: "Erabilera", + welcomeStepLook: "Itxura", + welcomeStepCommunity: "Komunitatea", + welcomeStepIntro: "Ongi etorria", + welcomeHintIdentity: "Zure izena eta irudia.", + welcomeHintType: "Nola irekitzen den Oasis zuretzat.", + welcomeHintUse: "Zein modulu geratzen diren aktibo.", + welcomeHintLook: "Oasisek janzten duen gaia.", + welcomeHintCommunity: "Sartu PUB batean beste biztanle batzuk ezagutzeko.", + welcomeHintIntro: "Bidali lehen agur bat komunitateari.", + welcomeStepThemeAction: "Erabili gai hau", + welcomeJoinPubs: "Sartu PUBetan", + welcomeNoFederation: "Federatu gabe", + welcomeStepGreetingAction: "Bidali", + welcomeDescription: "Hasi aurretik komeni diren gauza batzuk.", + welcomeSkip: "Orain ez", + welcomeStepProfileAction: "Gorde profila", + welcomeBootstrapRunning: "Pub-aren argazkia kargatzen. Amaitzean pub-era batuko da automatikoki; bitartean Oasis erabiltzen jarrai dezakezu.", + welcomeCompleteTitle: "Dena prest.", + welcomeCompleteText: "Zure nodoa prest dago. Hemendik aurrera sarea babesten dituzun bizilagunekin hazten da.", + welcomeFindPeople: "Bilatu biztanleak", + devFilterFiles: "FITXATEGIAK", + devFilterSearch: "BILATU", + devFilterModules: "MODULUAK", + devFilterTranslations: "ITZULPENAK", + devFilterStyles: "ESTILOAK", + devFilterTests: "TESTAK", + devVersion: "Bertsioa", + devNodeVersion: "Node", + devStatsTests: "Testak", + devParentFolder: "Goiko karpeta", + devOpenFolder: "ireki", + devDescription: "Arakatu nodo honetan exekutatzen ari den kodea.", + devRoot: "erroa", + devSearchPlaceholder: "Kodean bilatzeko testua", + devAnyExtension: "Edozein fitxategi", + devStatsFiles: "Fitxategiak", + devStatsLines: "Lerroak", + devStatsModules: "Moduluak", + devStatsLanguages: "Hizkuntzak", + devNotViewable: "ez ikusgai", + devEmptyFolder: "Karpeta hau hutsik dago.", + devSize: "Tamaina", + devShowing: "Erakusten", + devLine: "lerroa", + devReportBug: "Akatsa Jakinarazi", + devCreateTask: "Ataza Sortu", + devRaw: "RAW", + devReportContext: "Oasis kodea irakurtzean aurkitua", + devTaskPrefix: "Berrikusi", + devPrevPage: "Aurreko lerroak", + devNextPage: "Hurrengo lerroak", + devMatches: "bat etortze", + devFilesScanned: "fitxategi aztertuta", + devNoResults: "Oraindik ez dago bat etortzerik.", + devColModule: "Modulua", + devColState: "Egoera", + devColModel: "Eredua", + devColView: "Ikuspegia", + devColRoutes: "Bideak", + devColTests: "Testak", + devOn: "aktibo", + devOff: "ez-aktibo", + modulesDevLabel: "Garapena", + modulesDevDescription: "Oasis kodea kudeatzeko modulua.", themeIntro: "Hautatu gaia.", setTheme: "Ezarri gaia", @@ -494,16 +573,49 @@ module.exports = { languageDescription: "Beste hizkuntza bat erabili nahi baduzu, hautatu hemen.", setLanguage: "Ezarri hizkuntza", - peerConnections: "Parekoak", peerConnectionsIntro: "Kudeatu beste parekoekin dituzun konexio guztiak.", + peersConnectFailed: "Ezin izan da konektatu: {reason}.", + peerStateLabel: "Egoera", + peerAddressLabel: "Helbidea", + peerLastChangeLabel: "Azken aldaketa", + peerStateConnected: "Konektatuta", + peerStateConnecting: "Konektatzen", + peerStateStaged: "Erabilgarri", + peerErrSilent: "Jarduerarik gabe", + peerErrTimeout: "Ez du erantzuten", + peerErrRefused: "Konexioa baztertzen du", + peerErrNotFound: "Helbide ezezaguna", + peerErrUnreachable: "Sarea eskuraezin", + peerErrKeys: "Gako desberdinak erabiltzen", + peerErrIdentity: "Ez du zure konexioa onartzen", + peerErrTor: "TOR soilik", + peerErrDropped: "Konexioa etenda", + peerErrPaused: "Sarea pausan", + peerErrOther: "Konexio-errorea", + peerOutdated: "Zaharkituta", + peerVersionUnannounced: "Ez du bere bertsioa iragartzen: Oasis zahar bat", + peersTorMissing: "PUB honek TOR bidezko konexioak soilik onartzen ditu.", peerConnectionsTitle: "Konexioak", + peersOnlineHint: "Orain zurekin konektatuta dauden nodoak.", + peersDiscoveredHint: "Helbide ezaguna duten baina orain konektatuta ez dauden nodoak.", + peersUnknownHint: "Zure grafo sozialeko nodoak, helbide ezagunik gabe.", + peersConnectionsHint: "Nodo honek dei ditzakeen helbideak eta konexio bakoitzaren egoera.", + peersTabLan: "LAN", + peersTabHealth: "Osasuna", + peersHealthHint: "Nola dagoen pub edo nodo ezagun bakoitza, sare lokalean eta kanpoan.", + peersHealthReachable: "Eskuragarri", + peersHealthFailing: "Huts egiten", + peersHealthFailures: "Hutsak", + peersFailingSince: "Huts egiten noiztik", + peersHealthForget: "Ahaztu eskuraezinak", + peersLanHint: "Zure sare lokalean aurkitutako nodoak.", + peersLanLastSeen: "Azkenekoz ikusia", + peersLanEmpty: "Oraindik ez da nodorik aurkitu sare lokalean.", online: "Linean", offline: "Lineaz kanpo", discovered: 'Aurkituak', unknown: 'Ezezagunak', - lanBroadcastLabel: "LAN igorpena", lanBroadcastActive: "Aktibo (UDP multicast LANean)", - lanBroadcastDisabled: "Desgaituta (pub modua)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -516,127 +628,102 @@ module.exports = { noConnections: "Konektatutako parekorik ez.", noDiscovered: "Ez duzu nodorik aurkitu.", noUnknownPeers: "Ez dago kide ezezagunik adiskidetza-grafoan.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "Oraindik ez dago nodorik erregistratuta.", + peerConnect: "Konektatu", + peerDisconnect: "Deskonektatu", peerRefresh: "Berritu", peerPruneIdle: "Inaktiboak kendu", + peersPause: "Pausatu", + peersResume: "Jarraitu", + peersPausedNote: "Pausan: Oasis ez da inorekin konektatzen eta inor ezin da harekin konektatu.", + peersPauseHint: "Konexio guztiak gelditzen ditu: Oasis ez da beste nodoetara konektatzen eta ez ditu onartzen berriro hasi arte.", peerExport: "Esportatu", peerImport: "Inportatu", peerImportTitle: "Peer-zerrenda inportatu", peerImportPlaceholder: "Itsatsi multiserver-helbide bat lerro bakoitzeko, edo igo .txt fitxategi bat…", - noSupportedConnections: "Lagundutako parekorik ez.", - noBlockedConnections: "Blokeatutako parekorik ez.", - noRecommendedConnections: "Gomendatutako parekorik ez.", connectionActionIntro: "", - startNetworking: "Abiarazi sarea", - stopNetworking: "Gelditu sarea", - restartNetworking: "Berrabiarazi sarea", - sync: "Sinkronizatu sarea", indexes: "Indizeak", + indexesRebuilt: "Indizeak berreraiki dira", + indexesRebuildFailed: "Ezin izan dira indizeak berreraiki", + indexesFiles: "Indize fitxategiak", indexesDescription: "Indizeak birsortzea segurua da eta hutsegite mota batzuk konpon ditzake.", homePageTitle: "Hasierako orria", homePageDescription: "Hautatu nahi duzun moduluaren orria hasiera gisa.", saveHomePage: "Gorde hasierako orria", invites: "Gonbidapenak", - invitesTitle: "Gonbidapenak", - invitesInvites: "Gonbidapenak", invitesDescription: "Kudeatu eta erabili gonbidapen kodeak zure sarean.", + invitesPubsHint: "Itsatsi pub baten gonbidapen-kodea harekin federatzeko eta bere biztanleak errepikatzen hasteko.", + invitesFederationsHint: "Federatuta zauden pubak: eguneratu, kendu iritsezinak edo esportatu zerrenda.", invitesTribesTitle: "Tribuak", - invitesTribeInviteCodePlaceholder: "Sartu tribuaren gonbidapen kodea", - invitesTribeJoinButton: "Sartu Tribuan", invitesChatsTitle: "Txatak", - invitesChatInviteCodePlaceholder: "Sartu txataren gonbidapen kodea", - invitesChatJoinButton: "Sartu txatean", invitesPadsTitle: "Padak", - invitesPadInviteCodePlaceholder: "Sartu padaren gonbidapen kodea", - invitesPadJoinButton: "Sartu padean", invitesCalendarsTitle: "Egutegia", - invitesCalendarInviteCodePlaceholder: "Sartu egutegiaren gonbidapen kodea", - invitesCalendarJoinButton: "Sartu egutegian", invitesEventsTitle: "Ekitaldiak", - invitesEventInviteCodePlaceholder: "Sartu ekitaldiaren gonbidapen kodea", - invitesEventJoinButton: "Sartu ekitaldian", invitesForumsTitle: "Foroak", - invitesForumInviteCodePlaceholder: "Sartu foroaren gonbidapen kodea", - invitesForumJoinButton: "Sartu foroan", invitesMapsTitle: "Mapak", - invitesMapInviteCodePlaceholder: "Sartu maparen gonbidapen kodea", - invitesMapJoinButton: "Sartu mapan", invitesShopsTitle: "Dendak", - invitesShopInviteCodePlaceholder: "Sartu dendaren gonbidapen-kodea", - invitesShopJoinButton: "Bat egin dendarekin", invitesPubsTitle: "PUBak", + invitesSuggestedPubs: "Iradokitako PUBak", + invitesFilterOthers: "Besteak", + invitesJoinTitle: "Gonbidapenak", + invitesJoinHint: "Itsatsi edozein gonbidapen-kode, edozertarako dela ere, edo biztanle baten Oasis IDa.", + invitesJoinPlaceholder: "Gonbidapen-kodea edo Oasis IDa", + invitesJoinButton: "Sartu", + invitesUsedTitle: "Azken gonbidapenak", + invitesUsedEmpty: "Oraindik ez duzu gonbidapenik erabili.", + invitesUsedType: "Mota", + invitesUsedName: "Izena", + invitesUsedDate: "Data", invitesPubInviteCodePlaceholder: "Sartu PUB-aren gonbidapen kodea", invitesAcceptInvite: "Sartu PUBean", + invitesPubUses: "Erabilitako gonbidapenak", + invitesPubExhausted: "PUB honek ez du gonbidapen erabilgarririk.", invitesAlreadyFederated: "Dagoeneko pub honekin federatuta zaude.", + invitesJoinPubMode: "Oasis hau PUB gisa dabil, irakurtzeko moduan soilik. Beste PUB batekin batzeko, erabili terminalean: ./oasis.sh join ", + invitesJoinLocalOnly: "Gonbidapenak Oasis dabilen gailutik bakarrik onar daitezke, edo abiaraztean erakusten duen administrazio-estekarekin.", invitesFederationsTitle: "Federazioak", invitesAcceptedInvites: "Federatuak", - invitesNoInvites: "Ez da gonbidapenik onartu, oraindik.", invitesUnfollow: "Utzi jarraitzea", invitesFollow: "Jarraitu", invitesUnfollowedInvites: "Ez federatuak", invitesNoUnfollowed: "Ez dago pub ez-federaturik.", invitesNoFederatedPubs: "Ez dago pub federaturik.", - invitesUnreachablePubs: "Iristezinak", - invitesNoUnreachablePubs: "Ez dago pub iristezinik.", invitesPubsRefresh: "Berritu", - invitesPubsClearUnreachable: "Kendu iristezinak", + invitesPubsClearUnreachable: "Garbitu", invitesPubsExport: "Esportatu", invitesPubsImport: "Inportatu", invitesPubsImportTitle: "Inportatu pubak", invitesPubsImportPlaceholder: "Itsatsi multiserver-helbide bat edo gonbidapen-kode bat lerro bakoitzeko, edo igo .txt fitxategi bat…", - currentlyUnreachable: "ERROR!", - errorDetails: "Errore Xehetasunak", genericError: "Errore bat gertatu da.", panicMode: "Izu Modua!", encryptData: "Idatzi pasahitza (gutxienez 32 karaketere) zure blockchain-a zifratzeko.", decryptData: "Sartu pasahitza zure blockchain-a deszifratzeko.", - panicModeDescription: "Zifratu/deszifratu edo EZABATU zure blockchain-a", removeDataDescription: "KONTUZ: Prozesu hau ezin da desegin.", - encryptPanicButton: "Zifratu blockchain-a", - decryptPanicButton: "Deszifratu blockchain-a", removePanicButton: "EZABATU ZURE DATU GUZTIAK!", searchTitle: "Bilatu", searchDescriptionLabel: "Bilatu edukia zure sarean.", - searchLanguagesLabel: "Hizkuntzak", - searchSkillsLabel: "Trebetasunak", searchPlaceholder:"Bilatu edukia...", - searchDateLabel:"Data", searchLocationLabel:"Kokapena", searchPriceLabel:"Prezioa", - searchUrlLabel:"URL", searchCategoryLabel:"Kategoria", - searchStartLabel:"Hasiera ordua", - searchEndLabel:"Amaiera ordua", searchPriorityLabel:"Lehentasuna", searchStatusLabel:"Egoera", statusLabel:"Egoera", - totalVotesLabel:"Bozkak Guztira", noResultsFound:"Emaitzik ez.", votesOption:"Bozkatzeko Aukerak", voteYesLabel:"Bai", voteNoLabel:"Ez", allTypesLabel: "MUGAGABE", - ABSTENTIONLabel:"ABSTENTZIOA", YESLabel:"BAI", NOLabel:"EZ", - FOLLOW_MAJORITYLabel: "JARRAITU GEHIENGOA", - CONFUSEDLabel: "NAHASTUTA", - NOT_INTERESTEDLabel: "INTERESIK EZ", - StatusLabel:"Egoera", votesOptionYesLabel:"Bai", votesOptionNoLabel:"Ez", votesOptionAbstentionLabel:"Abstentzioa", votesQuestionLabel:"Galdera", - votesCreatedByLabel:"Noiz", voteStatusOpen:"IREKITA", voteStatusClosed:"ITXITA", - imageSearchLabel: "Hitz hauek dituzten irudiak bilatu.", commentDescription: ({ parentUrl }) => [ " noiz komentatuta ", a({ href: parentUrl }, " haria"), @@ -647,61 +734,27 @@ module.exports = { a({ href: parentUrl }, " bidalketa bat"), ], subtopicTitle: ({ authorName }) => [`Subtopic on @${authorName}'s post`], - mysteryDescription: " bidalketa misteriotsua argitaratu du", oasisDescription: "OASIS Proiektuko Sarea", searchSubmit: "Bilatu...", - postLabel: "BIDALKETAK", - aboutLabel: "BIZILAGUNAK", - feedLabel: "JARIOAK", votesLabel: "GOBERNUA", - reportLabel: "TXOSTENAK", - imageLabel: "IRUDIAK", - videoLabel: "BIDEOAK", - audioLabel: "AUDIOAK", - documentLabel: "DOKUMENTUAK", - torrentLabel: "TORRENTAK", pdfFallbackLabel: "PDF Dokumentua", - eventLabel: "EKITALDIAK", - taskLabel: "ATAZAK", - transferLabel: "TRANSFERENTZIAK", - curriculumLabel: "KURRIKULUMA", - bookmarkLabel: "MARKAGAILUAK", - tribeLabel: "TRIBUAK", - marketLabel: "MERKATUA", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "ZORROTAK", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + shopLabel: "DENDAK", + voteLabel: "BOTOAK", submit: "Bidali", - subjectLabel: "Gaia", editProfile: "Editatu Abatarra", - profileQrAlt: "Profilaren QR kodea", - profileQrHint: "Eskaneatu Oasis ID hau kopiatzeko.", oasisIdLabel: "OasisID", - spreadLabel: "Hedatu", - spreadHint: "Hedatu zure babesleei (zure feed bidez errepikatzen da).", + spreadContent: "Errepikatu", welcomePmSubject: "Hello World!", - welcomePmBody: "Ongi etorri Oasis-era — sare sozial libre, parekideen arteko, federatu eta zifratua, gonbidapen bidezko arkitektura banatu batekin.\n\nHasteko leku interesgarri batzuk:\n\n- /profile — Editatu zure avatarra, izena, deskribapena eta zein modulu agertuko diren zure alde publikoan.\n- /invites — Gehitu pub bat sarearen gainerakoarekin federatzeko.\n- /peers — Ikusi nor dagoen konektatuta, eskaneatu LAN-a berriz, esportatu edo inportatu peer-zerrendak.\n- /inhabitants — Aurkitu eta bisitatu beste pertsonen avatarrak.\n- /tribes — Sortu edo elkartu muturretik muturrera zifratutako talde pribatuetan.\n- /inbox — Irakurri eta bidali mezu pribatuak.\n- /activity — Ikusi azken jarduera, zurea eta zure sarearena.\n- /publish — Idatzi post bat edo partekatu irudia, audioa, bideoa edo dokumentua.\n\nKonektatzeko leku interesgarri batzuk:\n\n- /fediverse — Kudeatu zure beste fediverso kontuak, edukia bidaltzea eta jasotzea barne.\n\nMezu hau zugandik zuregana modu pribatuan bidali da; horregatik ez zen konexiorik behar jasotzeko.", - profileVisibilityTitle: "Profil publikoaren ikusgaitasuna", - profileVisibilityHint: "Aukeratu zein eremu ikus ditzaketen beste biztanleek zure profilean. Lehenetsia: Karma soilik.", + welcomePmBody: "Ongi etorri Oasis-era — sare sozial libre, parekideen arteko, federatu eta zifratua, gonbidapen bidezko arkitektura banatu batekin.\n\nHasteko leku interesgarri batzuk:\n\n- [/avatar](/profile) — Editatu zure avatarra, izena, deskribapena eta zein modulu agertuko diren zure alde publikoan.\n- /settings — Doitu zure instantzia lokala: hizkuntza, gaia, moduluak, pribatutasuna eta sinkronizazioa.\n- /invites — Gehitu pub bat sarearen gainerakoarekin federatzeko.\n- /peers — Ikusi nor dagoen konektatuta, eskaneatu LAN-a berriz, esportatu edo inportatu peer-zerrendak.\n- /inhabitants — Aurkitu eta bisitatu beste bizilagunen avatarrak.\n- /tribes — Sortu edo elkartu muturretik muturrera zifratutako talde pribatuetan.\n- /search — Bilatu sarearen edukia motaren arabera.\n\nKonektatzeko leku interesgarri batzuk:\n\n- /multiverse — Kudeatu zure beste multibertso kontuak, edukia bidaltzea eta jasotzea barne.\n\nMezu hau zugandik zuregana modu pribatuan bidali da; horregatik ez zen konexiorik behar jasotzeko.", profileSensorsSectionTitle: "Sentsoreak", profileVisibilityActivity: "Jarduera-maila", - profileVisibilityDevice: "Gailua", profileDeviceLockedHint: "Sentsore hau beti dago aktibo: besteei zein gailutatik konektatzen zaren erakusten die eta ezin da desgaitu.", profileVisibilityKarma: "KARMA puntuazioa", - profileVisibilityUbi: "UBI", - profileVisibilityWallet: "ECOIN zorroa", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityUbi: "OBU", + profileVisibilityWallet: "ECOin zorroa", + profileVisibilityEcoTax: "ECO tasa", + profileVisibilityLarpSign: "L.A.R.P. zigilua", profileVisibilityGpg: "GPG Gakoa", - profileVisibilityClearnet: "Nire profila argitaratu", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Dendak", profileClearnetJobsLabel: "Lanak", @@ -713,6 +766,10 @@ module.exports = { profileClearnetImagesLabel: "Irudiak", profileClearnetDocumentsLabel: "Dokumentuak", profileClearnetTorrentsLabel: "Torrentak", + profileClearnetPodcastsLabel: "Podcastak", + profileClearnetMarketLabel: "Merkatua", + profileClearnetFeedLabel: "Jarioa", + profileClearnetWikiLabel: "Wikiak", editProfileDescription: "", profileName: "Izena", @@ -738,7 +795,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "Berr.", walletConfirm: "Berretsi", - walletDescription: "Kudeatu zure ondasun digitalak. Hala nola, ECOin-ak bidali eta jaso, zure balantzea ikusi eta zure transakzio historia kontsultatu.", + walletDescription: "Kudeatu zure ECOin zorroa.", walletDate: "Data", walletFee: "Tasa (Zenbat eta tasa altuagoa, orduan eta bizkorrago prozesatuko da trantsakzioa.)", walletFeeLine: ({ fee }) => `Tasa: ECO ${fee}`, @@ -746,6 +803,86 @@ module.exports = { walletReceive: "Jaso", walletReset: "Berrezarri", walletSend: "Bidali", + walletPayingTransfer: "Transferra ordaintzen", + walletCreateTransferLabel: "Kontratua sortu (transferentzia)", + walletTransferNotifiedNote: "Beste aldeari ordainketa honen berri eman zaio", + walletTransferCreatedNote: "Ordainketa honen transfer bat (ordainagiria) sortu da", + walletTransferNoPayeeNote: "Biztanle batek ere ez du helbide hau argitaratu, beraz ez da transferrik sortu", + walletTransferReceiptConcept: "Zorrotik egindako ordainketa", + walletPaymentPmText: "ordainketa bat bidali dizu", + walletPayeeLabel: "Hartzailea", + walletConceptLabel: "Kontzeptua", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Zure OBUa jaso duzu.", + bankingBotPaymentTitle: "Ordainketa bat jaso duzu.", + bankingBotUbiPaidText: "Zure Oinarrizko Errenta Unibertsala ordaindu da", + bankUbiPubThisNode: "Nodo honek OBUa ordaintzen du", + bankUbiPubConnected: "OBUko PUBa", + bankUbiPubNone: "Oraindik ez dago OBUa iragartzen duen PUBik zure sarean", + bankUbiAddressPublished: "ECOin helbidea argitaratuta", + bankUbiAddressMissing: "Argitaratu zure ECOin helbidea OBUa jasotzeko", + bankUbiClaimedThisMonth: "Hilabete honetan eskatuta", + bankUbiPubBalance: "PUBaren zorroa OBUrako erabilgarri", + bankNoPubAddress: "OBUko PUBak ez du oraindik ECOin helbiderik argitaratu.", + bankUbiTab: "OBU", + bankDonateEco: "EMAN!", + donateTooltip: "Eman ECOin", + bankNoUserAddress: "Biztanle honek ez du ECOin helbiderik partekatzen.", + bankUbiPubDefault: "Lehenetsia", + bankUbiPool: "Funtsa", + bankUbiLastSeen: "Azkenekoz ikusia", + bankUbiLastPayout: "Azken ekintza", + bankUbiPaidOut: "Banatua", + bankRangeToday: "Gaur", + bankRangeWeek: "Astea", + bankRangeMonth: "Hilabetea", + bankRangeYear: "Urtea", + bankRangeFiveYears: "5 urte", + bankRangeAll: "Guztia", + bankChartValueTitle: "ECOin balioaren bilakaera", + bankChartValueHours: "Denbora baliokidetza (h)", + bankChartWealthTitle: "ECOin aberastasunaren birbanaketa", + bankChartDistributed: "Banatutako OBUa (ECO)", + bankChartRebalanceTitle: "Aberastasunaren birbanaketa PUBen artean", + bankChartRebalanced: "PUBen artean mugitutako ECO", + bankChartTaxes: "Atxikitako zergak (ECO)", + bankChartNoDataRange: "Ez dago daturik tarte honetan.", + bankChartNoDataYet: "Oraindik ez dago daturik", + bankChartPerHour: "orduko", + bankChartPerDay: "eguneko", + bankChartPerMonth: "hileko", + bankTotalUbiDistributed: "Birbanatutako OBU guztira (sarea)", + bankTotalTaxesWithheld: "Atxikitako zerga guztira (sarea)", + bankInflationIssuance: "Jaulkipen-tasa (urtekoa, bloke-sariaren arabera)", + bankRulesEligibilityTitle: "Nork jaso dezake OBUa", + bankRulesEligibility1: "Profilean argitaratutako ECOin helbide bat (Profila → Sentsoreak → ECOin zorroa).", + bankRulesEligibility2: "30 egun baino gehiagoko feed bat sarean jarduerarekin: karma zerotik gora.", + bankRulesEligibility3: "Eskaera bat hilean (epoch), Banking-etik OBUa eskatu botoiarekin. Uneko hilabetea uka daiteke OBUari uko egin botoiarekin.", + bankRulesEligibility4: "1 ECO-ko oinarria beti ordaintzen da; zergek gainetik dagoen zatia bakarrik murrizten dute eta ez dira inoiz aparte kobratzen.", + bankRulesEligibility5: "PUBak hurrengo zikloan ordaintzen du eta berretsi dezakezun OBU transferentzia bat argitaratzen du.", + bankRulesEligibility6: "Helbidea falta bada, feeda gazteegia bada, hilabeteari uko egin badiozu edo PUBak fondorik ez badu, eskaera saltatu eta PUBean erregistratzen da.", + copyLinkTitle: "Kopiatu esteka", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "OBUari uko egin", + bankUbiRefusedThisMonth: "Hilabete honetako OBUari uko egin diozu.", + bankingBotUbiAvailableTitle: "Zure OBUa eska dezakezu.", + bankingBotUbiAvailableText: "Hilabete honetako zure OBUa eskuragarri dago", + bankingBotConfirmTitle: "Baieztatzeko zain dagoen smart-contract bat duzu", + bankingBotConfirmText: "Zure berrespenaren zain dauden smart-contract-ak", + bankChartZoomHint: "Klikatu handitzeko", + bankChartPoolTitle: "OBUaren funtsa (denboran)", + bankChartNetworkUbiTitle: "Birbanatutako OBU guztira (sarea)", + bankChartSupplyTitle: "ECOin zirkulazioko eskaintza (denboran)", + bankChartInflationTitle: "ECOin inflazioa (denboran)", + bankChartInflationSeries: "Urteko tasa (%)", + bankChartPool: "Funts osoa (ECO)", + bankChartKarmaTitle: "KARMA Scoring (denboran)", + bankChartFundsTitle: "Zure ECOin funtsak (denboran)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin konektatuta", + bankWalletNotConfigured: "ECOin deskonektatuta", + transfersPayWithWallet: "ORDAINDU!", walletStatus: "Egoera", walletDisconnected: [ "ECOin ", @@ -755,9 +892,11 @@ module.exports = { " edo konexioaren egoera.", ], walletSentToLine: ({ destination, amount }) => `${amount} ECO bidali dira ${destination} helbidera`, - walletSettingsTitle: "Zorroa", + walletNotConfigured: "Zure ECOin zorroa ez dago oraindik konfiguratuta.", walletSettingsDescription: "Integratu Oasis zure ECOin zorroarekin.", - walletSettingsDocLink: "ECOin instalazio gida", + walletSettingsMobileDescription: "Lotu Oasis gailu honetako ECOin Wallet aplikazioarekin.", + walletDownloadEcoin: "Deskargatu ECOin!", + walletRpcUrl: "RPC URLa", walletStatusMessages: { invalid_amount: "Zenbatekoa oker", invalid_dest: "Hartzailearen helbidea oker", @@ -773,41 +912,24 @@ module.exports = { walletUser: "Erabiltzaile izena", walletPass: "Pasahitza", walletConfiguration: "Ezarri zorroa", + walletDisconnectButton: "Zorroa deskonektatu", cipher: "Zifratzailea", cipherTitle: "Zifratzailea", cipherDescription: "Zifratu eta deszifratu testua simetrikoki (Pasahitza erabiliz).", randomPassword: "Ausazko pasahitza", - cipherEncryptTitle: "Zifratu testua", - cipherEncryptDescription: "Ezarri pasahitza (gutxienez 32 karaktereko luzera) zure testua zifratzeko", - cipherTextLabel: "Zifratuko den testua", cipherTextPlaceholder: "Idatzi zifratuko den testua...", cipherPasswordLabel: "Pasahitza", cipherPasswordDecryptLabel: "Pasahitza ezarri (gutxienez 32 karaktere) testua deszifratzeko", cipherPasswordPlaceholder: "Idatzi pasahitza...", cipherEncryptButton: "Zifratu", - cipherDecryptTitle: "Deszifratu testua", - cipherDecryptDescription: "Sartu zifratutako testua eta pasahitza, deszifratzeko.", cipherEncryptedMessageLabel: "Zifratutako testua", cipherDecryptedMessageLabel: "Deszifratutako Testua", - cipherPasswordUsedLabel: "Zifratzeko pasahitza (gorde!)", cipherEncryptedTextPlaceholder: "Sartu zifratutako testua...", - cipherIvLabel: "HB", - cipherIvPlaceholder: "Sartu hasieratzeko bektorea...", cipherDecryptButton: "Deszifratu", password: "Pasahitza", text: "Testua", encryptedText: "Zifratutako testua", iv: "Hasieratze Bektorea (HB)", - encryptTitle: "Zifratu zure testua", - encryptDescription: "Sartu zifratu nahi duzun testua eta eman pasahitz bat.", - encryptButton: "Zifratu", - decryptTitle: "Deszifratu zure testua", - decryptDescription: "Sartu zifratutako testua eta eman enkripatzeko erabili duzun pasahitz berbera.", - decryptButton: "Deszifratu", - passwordLengthError: "Pasahitzak gutxienez 32 karaketere izan behar ditu.", - missingFieldsError: "Ez da testurik, pasahitzik edo HB-rik sartu.", - encryptionError: "Errorea testua zifratzean.", - decryptionError: "Errorea testua deszifratzean.", bookmarkTitle: "Laster-markak", bookmarkDescription: "Zure sareko laster-markak aurkitu eta kudeatu.", bookmarkAllSectionTitle: "Laster-markak", @@ -824,19 +946,16 @@ module.exports = { bookmarkFilterRecent: "AZKENAK", bookmarkCreateButton: "Laster-marka sortu", bookmarkUpdateButton: "Eguneratu", - bookmarkDeleteButton: "Ezabatu", bookmarkAddFavoriteButton: "Gogokoetara gehitu", bookmarkRemoveFavoriteButton: "Gogokoetatik kendu", bookmarkUrlLabel: "Esteka", - bookmarkUrlPlaceholder: "https://adibidea.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Deskribapena", bookmarkDescriptionPlaceholder: "Aukerakoa", bookmarkTagsLabel: "Tagak", - bookmarkTagsPlaceholder: "Sartu tagak komaz banatuta", - bookmarkCategoryLabel: "Kategoria", - bookmarkCategoryPlaceholder: "Aukerakoa", + bookmarkTagsPlaceholder: "Idatzi etiketak komaz bereizita", bookmarkLastVisitLabel: "Azken bisita", - bookmarkSearchPlaceholder: "Bilatu URLa, tagak, kategoria, egilea...", + bookmarkSearchPlaceholder: "Bilatu hemen: Laster-markak...", bookmarkSortRecent: "Berrienak", bookmarkSortOldest: "Zaharrenak", bookmarkSortTop: "Bozkatuenak", @@ -849,8 +968,6 @@ module.exports = { noLastVisit: "Azken bisitarik ez", videoTitle: "Bideoak", videoDescription: "Arakatu eta kudeatu zure sareko bideo-edukia.", - videoPluginTitle: "Izenburua", - videoPluginDescription: "Deskribapena", videoMineSectionTitle: "Zure bideoak", videoCreateSectionTitle: "Bideoa igo", videoUpdateSectionTitle: "Bideoa eguneratu", @@ -877,12 +994,11 @@ module.exports = { videoDescriptionPlaceholder: "Aukerakoa", videoNoFile: "Ez da bideo-fitxategirik eman", noVideos: "Ez dago bideorik erabilgarri.", - videoSearchPlaceholder: "Bilatu izenburua, etiketak, egilea...", + videoSearchPlaceholder: "Bilatu hemen: Bideoak...", videoSortRecent: "Berrienak", videoSortOldest: "Zaharrenak", videoSortTop: "Bozkatuenak", videoSearchButton: "Bilatu", - videoMessageAuthorButton: "MP", videoUpdatedAt: "Eguneratua", videoNoMatch: "Ez dago zure bilaketarekin bat datorren bideorik.", documentTitle: "Dokumentuak", @@ -901,20 +1017,19 @@ module.exports = { documentFilterFavorites: "GOGOKOAK", documentCreateButton: "Dokumentua igo", documentUpdateButton: "Eguneratu", - documentDeleteButton: "Ezabatu", documentAddFavoriteButton: "Gehitu gogokoetara", documentRemoveFavoriteButton: "Gogokoetatik kendu", documentMessageAuthorButton: "MP", documentFileLabel: "Dokumentua igo (.pdf)", documentTagsLabel: "Etiketak", - documentTagsPlaceholder: "Sartu komaz bereizitako etiketak", + documentTagsPlaceholder: "Idatzi etiketak komaz bereizita", documentTitleLabel: "Izenburua", documentTitlePlaceholder: "Aukerakoa", documentDescriptionLabel: "Deskribapena", documentDescriptionPlaceholder: "Aukerakoa", documentNoFile: "Fitxategirik ez.", noDocuments: "Ez dago dokumenturik.", - documentSearchPlaceholder: "Bilatu izenburua, etiketak, deskribapena, egilea...", + documentSearchPlaceholder: "Bilatu hemen: Dokumentuak...", documentSortRecent: "Berrienak", documentSortOldest: "Zaharrenak", documentSortTop: "Bozkatuenak", @@ -923,8 +1038,6 @@ module.exports = { documentUpdatedAt: "Eguneratua", audioTitle: "Audioak", audioDescription: "Arakatu eta kudeatu zure sareko audio-edukia.", - audioPluginTitle: "Izenburua", - audioPluginDescription: "Deskribapena", audioMineSectionTitle: "Zure audioak", audioCreateSectionTitle: "Audioa igo", audioUpdateSectionTitle: "Audioa eguneratu", @@ -935,22 +1048,21 @@ module.exports = { audioFilterMine: "NIREAK", audioFilterRecent: "AZKENAK", audioFilterTop: "TOP", - audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Audioa igo", audioUpdateButton: "Eguneratu", - audioDeleteButton: "Ezabatu", audioAddFavoriteButton: "Gehitu gogokoetara", audioRemoveFavoriteButton: "Kendu gogokoetatik", audioFileLabel: "Aukeratu audio-fitxategi bat (.mp3, .wav, .ogg)", audioTagsLabel: "Etiketak", audioTagsPlaceholder: "Idatzi etiketak komaz bereizita", + tagsPlaceholder: "Idatzi etiketak komaz bereizita", audioTitleLabel: "Izenburua", audioTitlePlaceholder: "Aukerakoa", audioDescriptionLabel: "Deskribapena", audioDescriptionPlaceholder: "Aukerakoa", audioNoFile: "Ez da audio-fitxategirik eman", noAudios: "Ez dago audiorik erabilgarri.", - audioSearchPlaceholder: "Bilatu izenburua, etiketak, egilea...", + audioSearchPlaceholder: "Bilatu hemen: Audioak...", audioSortRecent: "Berrienak", audioSortOldest: "Zaharrenak", audioSortTop: "Bozkatuenak", @@ -962,6 +1074,7 @@ module.exports = { audioFilterFavorites: "GOGOKOAK", favoritesTitle: "Gogokoak", favoritesDescription: "Zure gogoko guztiak leku berean.", + favoritesSearchPlaceholder: "Bilatu hemen: Gogokoak...", favoritesFilterAll: "GUZTIAK", favoritesFilterRecent: "AZKENAK", favoritesFilterAudios: "AUDIOAK", @@ -973,22 +1086,16 @@ module.exports = { favoritesFilterChats: "TXATAK", favoritesFilterCalendars: "EGUTEGIAK", favoritesFilterVideos: "BIDEOAK", - favoritesRemoveButton: "Gogokoetatik kendu", favoritesNoItems: "Oraindik ez dago gogokorik.", yourContacts: "Zure Kontaktuak", allInhabitants: "Bizilagunak", - allCVs: "CV guztiak", + allCVs: "CVak", discoverPeople: "Aurkitu bizilagunak zure sarean.", - allInhabitantsButton: "GUZTIAK", - contactsButton: "LAGUNTZEN", - CVsButton: "CV-ak", - matchSkills: "Aurkitu trebetasunak", - matchSkillsButton: "AURKITU TREBETASUNAK", - suggestedButton: "GOMENDATUTAKOAK", - searchInhabitantsPlaceholder: "IRAGAZI bizilagunak IZENAREN ARABERA …", + searchInhabitantsPlaceholder: "Bilatu hemen: Bizilagunak...", filterLocation: "IRAGAZI bizilagunak KOKAPENAREN ARABERA …", filterLanguage: "IRAGAZI bizilagunak BY HIZKUNTZAREN ARABERA …", filterSkills: "IRAGAZI bizilagunak BY TREBETASUNEN ARABERA …", + cvSkillsCount: "Trebetasunak", applyFilters: "Ezarri Iragazkiak", locationLabel: "Kokapena", languagesLabel: "Hizkuntzak", @@ -997,15 +1104,15 @@ module.exports = { mutualFollowers: "Jarraitzaile amankomunean", suggestedFollowsYou: "Zu jarraitzen zaitu", latestInteractions: "Interakzioak Azkenak", - viewAvatar: "Ikusi Abatarra", - viewCV: "Ikusi CV-a", suggestedSectionTitle: "Gomendatuta", topkarmaSectionTitle: "Karma Onena", topecoSectionTitle: "Eko Onena", topactivitySectionTitle: "Top Jarduera", + "TOP INACTIVITYButton": "INAKTIBOENAK", + topinactivitySectionTitle: "Biztanle gutxien aktiboak", blockedSectionTitle: "Blokeatuta", gallerySectionTitle: "GALERIA", - blockedButton: "BLOKETATUTA", + topSectionTitle: "TOP", blockedLabel: "Blokeatutako Erabiltzailea", inhabitantviewDetails: "Ikusi Xehetasunak", keepReading: "Irakurtzen jarraitu...", @@ -1020,10 +1127,23 @@ module.exports = { parliamentFilterCandidatures: "HAUTAGAITZAK", parliamentFilterProposals: "PROPOSAMENAK", parliamentFilterLaws: "LEGEAK", + parliamentFilterRevocations: "EZEZTAPENAK", parliamentFilterHistorical: "HISTORIKOA", - parliamentFilterLeaders: "LIDERAK", + parliamentFilterLeaders: "BURUAK", parliamentFilterRules: "ARAUAK", parliamentGovernmentCard: "Uneko gobernua", + parliamentSeatsGovernment: "Gobernua", + parliamentSeatsOpposition: "Oposizioa", + parliamentSeatsAbstention: "Abstentzioa", + parliamentSeatsQuorum: "Hauteskunde-quoruma", + parliamentSeatsPass: "Onarpen-atalasea", + parliamentSeatsByElection: "Hauteskundeen arabera", + parliamentSeatsByHouses: "LARP etxearen arabera", + parliamentSeatsSeats: "eserleku", + parliamentSeatsSeat: "eserleku", + parliamentSeatsLeader: "Buruzagia", + parliamentSeatsVotesFor: "botoak honi:", + parliamentSeatsNoHouse: "Etxerik gabe", parliamentActorInPowerInhabitant: 'BIZTANLEA AGINTEAN', parliamentActorInPowerTribe: 'TRIBUA AGINTEAN', parliamentHistoricalGovernmentsTitle: 'GOBERNUAK', @@ -1045,38 +1165,25 @@ module.exports = { parliamentPoliciesDeclined: "BAZTERTUTAKO LEGEAK", parliamentPoliciesDiscarded: "POLITIKA BAZTERTUAK", parliamentEfficiency: "% ERAGINKORTASUNA", - parliamentFilterRevocations: 'INDARGABETZEAK', parliamentRevocationFormTitle: 'INDARGABETU', parliamentRevocationLaw: 'Legea', - parliamentRevocationTitle: 'Izenburua', parliamentRevocationReasons: 'Arrazoiak', parliamentCurrentRevocationsTitle: 'Uneko Indargabetzeak', parliamentFutureRevocationsTitle: 'Etorkizuneko Indargabetzeak', parliamentPoliciesRevocated: 'HISTORIKOA', - parliamentPoliciesTitle: 'HISTORICAL', + parliamentPoliciesTitle: "HISTORIKOA", parliamentLawsTitle: 'ONARTUTAKO LEGEAK', parliamentRulesRevocations: 'Onartutako edozein lege indargabetu daiteke indarrean dagoen gobernu metodoa erabiliz.', parliamentNoStableGov: "Oraindik ez da gobernurik hautatu.", parliamentNoGovernments: "Oraindik ez dago gobernurik.", parliamentCandidatureFormTitle: "Kandidatura Proposatu", parliamentCandidatureIdPh: "Oasis ID (@...) edo tribuaren izena", - parliamentCandidatureSlogan: "Leloa (geh. 140 karaktere)", - parliamentCandidatureSloganPh: "Lelo labur bat", parliamentCandidatureMethod: "Metodoa", parliamentCandidatureProposeBtn: "Hautagaitza Proposatu", - parliamentThDate: "Proposamen data", - parliamentThSlogan: "Leloa", - parliamentThSince: "Profila noiztik", - parliamentThVotes: "Jasotako botoak", - parliamentThVoteAction: "Bozkatu", - parliamentTypeUser: "Biztanlea", - parliamentTypeTribe: "Tribu", parliamentVoteBtn: "Bozkatu", parliamentProposalFormTitle: "Legea Proposatu", parliamentProposalDescription: "Deskribapena (≤1000)", parliamentProposalPublish: "Proposamena Argitaratu", - parliamentFinalize: "Amaitu", - parliamentDeadline: "Epea", parliamentNoProposals: "Oraindik ez dago lege-proposamenik.", parliamentNoLaws: "Oraindik ez dago onartutako legerik.", parliamentMethodDEMOCRACY: "Demokrazia", @@ -1094,29 +1201,22 @@ module.exports = { parliamentVotesSlashTotal: "Botoak/Guztira", parliamentVotesNeeded: "Beharrezko Botoak", parliamentFutureLawsTitle: "Etorkizuneko legeak", - parliamentNoFutureLaws: "Oraindik ez dago etorkizuneko legerik", parliamentVoteAction: "Bozkatu", - parliamentLeadersTitle: "Liderak", parliamentThLeader: "AVATAR", parliamentPopulation: "Biztanleria", - parliamentThType: "Mota", - parliamentThInPower: "Agintean", parliamentThPresented: "HAUTAGAITZAK", parliamentNoLeaders: "Oraindik ez dago liderrik.", parliamentCandidaturesListTitle: "Kandidaturen Zerrenda", parliamentTimeRemaining: "Geratzen den denbora", - parliamentNoLeader: "Oraindik ez dago irabazten ari den kandidaturarik.", parliamentElectionsStatusTitle: "Hurrengo Gobernua", parliamentProposalDeadlineLabel: "Epemuga", + parliamentViewReport: "Txostena ikusi", parliamentProposalTimeLeft: "Geratzen den denbora", - parliamentProposalOnTrack: "Onartzeko bidean", - parliamentProposalOffTrack: "Euskarri nahikorik ez", parliamentRulesTitle: "Legebiltzarrak nola funtzionatzen duen", parliamentRulesIntro: "Hauteskundeak 2 hilean behin ebazten dira; hautagaitzak etengabeak dira eta gobernua aukeratzean berrabiarazten dira.", parliamentRulesCandidates: "Edozein biztanlek bere burua, beste biztanle bat edo edozein tribu proposa dezake. Biztanle bakoitzak gehienez 3 hautagaitza proposa ditzake ziklo bakoitzean; ziklo bereko bikoiztuak baztertzen dira.", parliamentRulesElection: "Irabazlea ebazpen unean boto gehien dituen hautagaitza da.", parliamentRulesTies: "Berdinketa haustea: biztanlearen karma handiena; berdin jarraituz gero, profilaren antzinatasun handiena; oraindik berdin, proposamen goiztiarrena; ondoren, IDaren hurrenkera lexikografikoa.", - parliamentRulesFallback: "Inork bozkatzen ez badu, azken proposatutako hautagaitzak irabazten du. Hautagaitzarik ez badago, ausazko tribu bat hautatzen da.", parliamentRulesTerm: "Gobernua fitxak uneko gobernua eta estatistikak erakusten ditu.", parliamentRulesMethods: "Ereduak: Anarkia (gehiengo sinplea), Demokrazia (%50+1), Gehiengoa (%80), Gutxiengoa (%20), Karmatokrazia (karma handiena duten proposamenak), Diktadura (berehalako onarpena).", parliamentRulesAnarchy: "Anarkia modu lehenetsia da: ebazpenean ez bada kandidaturarik hautatzen, Anarkia ezartzen da. Anarkian, edozein biztanlek legeak proposa ditzake.", @@ -1125,6 +1225,7 @@ module.exports = { parliamentRulesLaws: "Proposamenak bere atalasea gainditzen duenean, Lege bihurtzen da eta Legeak fitxan agertzen da indarrean sartzeko datarekin.", parliamentRulesHistorical: "Historia fitxan egondako gobernu ziklo bakoitza eta haren kudeaketari buruzko datuak ikus daitezke.", parliamentRulesLeaders: "Liderak fitxan, gobernatu duten (edo aurkeztu diren) biztanle/tribuen sailkapena ikus daiteke, eraginkortasunaren arabera ordenatuta.", + parliamentRulesHemicycle: "Hemizikloak eserleku bat ematen dio biztanle bakoitzari: gobernu-blokea (hautagaitza irabazlearen hautesleak eta buruzagia), oposizio-bloke bat zikloko beste hautagaitza bakoitzeko, eta abstentzioa. Marka etenek hauteskunde-quoruma (erroldaren % 25) eta indarrean dagoen metodoaren onarpen-atalasea erakusten dituzte. \"LARP etxearen arabera\" eserleku berak etxeka koloreztatzen ditu.", parliamentProposalVoteStatusLabel: "Bozketa egoera", parliamentProposalOnTrackYes: "Atalasea lortuta", parliamentProposalOnTrackNo: "Atalasearen azpitik", @@ -1140,12 +1241,9 @@ module.exports = { courtsCaseTitle: "Izenburua", courtsCaseRespondent: "Akusatua / Erantzulea", courtsCaseRespondentPh: "Oasis ID (@...) edo Tribu izena", - courtsCaseMediatorsAccuser: "Bitartekariak (akusazioa)", + courtsCaseSubjectPlaceholder: "Gaia edo deskribapen laburra", courtsCaseMediatorsPh: "Oasis IDak, komaz bereizita", courtsCaseMethod: "Ebazpen metodoa", - courtsCaseDescription: "Azalpena (gehienez 1000 karaktere)", - courtsCaseEvidenceTitle: "Kasuko frogak", - courtsCaseEvidenceHelp: "Erantsi zure kasua babesten duten irudiak, audioak, dokumentuak (PDF) edo bideoak.", courtsCaseSubmit: "Kasu bidali", courtsNominateJudge: "Epailea proposatu", courtsJudgeId: "Epailea", @@ -1160,11 +1258,6 @@ module.exports = { courtsAnswerTitle: "Erreklamazioari erantzun", courtsAnswerText: "Erantzun laburra", courtsAnswerSubmit: "Erantzuna bidali", - courtsStanceDENY: "Ukatu", - courtsStanceADMIT: "Onartu", - courtsStancePARTIAL: "Partziala", - courtsStanceCOUNTERCLAIM: "Kontraerreklamazioa", - courtsStanceNEUTRAL: "Neutrala", courtsVerdictTitle: "Ebazpena eman", courtsVerdictResult: "Emaitza", courtsVerdictOrders: "Aginduak", @@ -1173,7 +1266,6 @@ module.exports = { courtsMediationPropose: "Akordioa proposatu", courtsSettlementText: "Baldintzak", courtsSettlementAccepted: "Onartua", - courtsSettlementPending: "Zain", courtsSettlementProposeBtn: "Proposatu", courtsNominationsTitle: "Epailetzako izendapenak", courtsThJudge: "Epailea", @@ -1190,7 +1282,6 @@ module.exports = { courtsThDecisionBy: "Ebazteko epea", courtsThCase: "Kasua", courtsThCreatedAt: "Hasiera-data", - courtsThActions: "Ekintzak", courtsCaseMediatorsRespondentTitle: "Defentsako bitartekariak gehitu", courtsCaseMediatorsRespondent: "Bitartekariak (defentsa)", courtsMediatorsAccuserLabel: "Bitartekariak (akusazioa)", @@ -1211,6 +1302,21 @@ module.exports = { courtsMethodPOPULAR: "Herritarren bozketa", courtsMethodMEDIATION: "Bitartekaritza", courtsMethodKARMATOCRACY: "Karmatokrazia", + courtsCaseType: "Kasu mota", + courtsPresetMinorConflict: "Gatazka arina", + courtsPresetModerateConflict: "Gatazka ertaina", + courtsPresetSevereConflict: "Gatazka larria", + courtsPresetHarassment: "Jazarpena edo abusua", + courtsPresetContentModeration: "Edukien moderazioa", + courtsPresetBanRestriction: "Kanporaketa edo murrizketa", + courtsPresetGovernanceDispute: "Gobernantza-gatazka", + courtsStatusOPEN: "Irekita", + courtsStatusIN_PROGRESS: "Abian", + courtsStatusDECIDED: "Erabakita", + courtsStatusCLOSED: "Itxita", + courtsStatusSOLVED: "Konponduta", + courtsStatusUNSOLVED: "Konpondu gabe", + courtsStatusDISCARDED: "Baztertuta", courtsMethodJUDGES: "Epaile-mahaia", courtsMethodSINGLE_JUDGE: "Epaile bakarra", courtsMethodJURY: "Epaimahaia", @@ -1222,31 +1328,15 @@ module.exports = { courtsRulesTitle: "Nola funtzionatzen duten Auzitegiek", courtsRulesIntro: "Auzitegiak komunitateak kudeatutako prozesuak dira, gatazkak konpontzeko eta justizia zuzentzailea sustatzeko. Elkarrizketa, froga argiak eta neurri proportzionalak lehenesten dira.", courtsRulesLifecycle: "Prozesua: 1) Kasua ireki 2) Metodoa aukeratu 3) Frogak aurkeztu 4) Entzunaldia eta eztabaida 5) Epai eta neurriak 6) Betetze eta itxiera 7) Helegitea (badagokio).", - courtsRulesRoles: "Akusatzailea: kasua irekitzen du. Defentsa: akusatutako pertsona edo tribua. Metodoa: komunitateak aukeratutako mekanismoa, froga baloratu eta ebazpena emateko. Lekukoa: testigantza edo frogak ematen dituen pertsona. Bitartekariak: akusatzaileak eta/edo defentsak gonbidatutako pertsona neutralak, xehetasun guztietara sarbidea dutenak, gatazka deseskalatzen eta akordioak lankidetzan eraikitzen laguntzen dutenak.", + courtsRulesRoles: "Akusatzailea: kasua irekitzen du. Defentsa: akusatutako biztanlea edo tribua. Metodoa: komunitateak aukeratutako mekanismoa, froga baloratu eta ebazpena emateko. Lekukoa: testigantza edo frogak ematen dituen biztanlea. Bitartekariak: akusatzaileak eta/edo defentsak gonbidatutako biztanle neutralak, xehetasun guztietara sarbidea dutenak, gatazka deseskalatzen eta akordioak lankidetzan eraikitzen laguntzen dutenak.", courtsRulesEvidence: "Gehienez 1000 karaktereko azalpena. Erantsi irudi, audio, bideo eta PDF dokumentu garrantzitsu eta legezkoak. Ez partekatu datu pribatu sentikorrak baimenik gabe.", courtsRulesDeliberation: "Entzunaldiak publikoak edo pribatuak izan daitezke. Epaileek errespetua bermatzen dute, argipenak eskatzen dituzte eta material ez-legala edo esanguragabea baztertu dezakete.", courtsRulesVerdict: "Lehentasuna da konponketa: barkamenak, bitartekaritza-akordioak, edukiaren moderazioa, behin-behineko murrizketak edo beste neurri proportzional batzuk. Arrazoibidea erregistratuta geratu behar da.", courtsRulesAppeals: "Helegitea: onartzen da frogak berriak direnean edo akats prozesal argia dagoenean. Gehienez 7 eguneko epean aurkeztu behar da, bestela adierazi ezean.", - courtsRulesPrivacy: "Errespetatu pribatutasuna eta segurtasuna. Doxing-a, gorrotoa edo mehatxuak ezabatzen dira. Epaileek erregistroaren zatiak editatu edo ezkutatu ditzakete arriskuan dauden pertsonak babesteko.", + courtsRulesPrivacy: "Errespetatu pribatutasuna eta segurtasuna. Doxing-a, gorrotoa edo mehatxuak ezabatzen dira. Epaileek erregistroaren zatiak editatu edo ezkutatu ditzakete arriskuan dauden bizilagunak babesteko.", courtsRulesMisconduct: "Jazarpenak, manipulazioak edo faltsututako frogek berehalako ebazpen negatiboa ekar dezakete.", courtsRulesGlossary: "Kasua: gatazkaren erregistroa. Froga: aldarrikapenak babesten dituzten materialak. Epai/ebazpena: neurri edo konponketarekin datorren erabakia. Helegitea: ebazpena berrikusteko eskaera.", courtsFilterActions: "EKINTZAK", - courtsNoActions: "Ez duzu zain dauden ekintzarik zure rolarentzat.", - courtsCaseTitlePlaceholder: "Gatazkaren deskribapen laburra", - courtsCaseSeverity: "Larritasuna", - courtsCaseSeverityNone: "Larritasun etiketarik ez", - courtsCaseSeverityLOW: "Baxua", - courtsCaseSeverityMEDIUM: "Ertaina", - courtsCaseSeverityHIGH: "Handia", - courtsCaseSeverityCRITICAL: "Larria", - courtsCaseSubject: "Gaia", - courtsCaseSubjectNone: "Gairik gabeko etiketa", - courtsCaseSubjectBEHAVIOUR: "Jokabidea", - courtsCaseSubjectCONTENT: "Edukia", - courtsCaseSubjectGOVERNANCE: "Gobernantza / arauak", - courtsCaseSubjectFINANCIAL: "Finantzak / baliabideak", - courtsCaseSubjectOTHER: "Bestelakoa", - courtsHiddenRespondent: "Ezkutatua (rol inplikatuek bakarrik ikus dezakete).", courtsThRole: "Rola", courtsRoleAccuser: "Akusatzailea", courtsRoleDefence: "Defentsa", @@ -1257,51 +1347,23 @@ module.exports = { courtsAssignJudgeBtn: "Epailea aukeratu", trendingTitle: "Pil-pilean", exploreTrending: "Aurkitu pil-pileko edukia zure sarean.", - bookmarkButton: "MARKAGAILUAK", - transferButton: "TRANSFERENTZIAK", - eventButton: "EKITALDIAK", - taskButton: "ATAZAK", votesButton: "GOBERNUA", - reportButton: "TXOSTENAK", - feedButton: "JARIOA", - marketButton: "MERKATUA", - imageButton: "IRUDIAK", - audioButton: "AUDIOAK", - videoButton: "BIDEOAK", - documentButton: "DOKUMENTUAK", - torrentButton: "TORRENTAK", - noTrendingFound: "Pil-pileko edukirik ez.", - noContentMessage: "Pil-pileko edukirik ez, oraindik.", - trendingDescription: "Deskribapena", - trendingDate: "Data", - trendingLocation: "Kokapena", - trendingPrice: "Prezioa", - trendingUrl: "URL", - trendingCategory: "Kategoria", - trendingStart: "Hasi", - trendingEnd: "Amaitu", - trendingPriority: "Lehentasuna", - trendingStatus: "Egoera", - trendingFrom: "Nork", - trendingTo: "Nori", - trendingConcept: "Kontzeptua", - trendingAmount: "Kopurua", - trendingDeadline: "Epemuga", - trendingItemStatus: "Elementuaren Egoera", - trendingTotalVotes: "Bozkak Guztira", + shopProductButton: "DENDA", + podcastButton: "PODCASTA", + podcastEpisodeButton: "ATALA", + campaignButton: "KANPAINA", + logisticsRouteButton: "IBILBIDEA", trendingTotalOpinions: "Iritzial Guztira", - trendingNoContentMessage: "Edukirik ez.", - trendingAuthor: "Nork", - trendingCreatedAtLabel: "Noiz", + trendingNoContentMessage: "Oraindik ez dago joerarik.", trendingTotalCount: "Kopurua Guztira", tasksTitle: "Atazak", tasksDescription: "Aurkitu eta kudeatu atazak", + taskSearchPlaceholder: "Bilatu hemen: Atazak...", taskTitleLabel: "Izenburua", taskDescriptionLabel: "Deskribapena", taskStartTimeLabel: "Hasiera Data", taskEndTimeLabel: "Amaiera Data", taskPriorityLabel: "Lehentasuna", - taskPrioritySelect: "Aukeratu lehentasuna", taskPriorityUrgent: "Larria", taskPriorityHigh: "Altua", taskPriorityMedium: "Ertaina", @@ -1311,13 +1373,13 @@ module.exports = { taskVisibilityLabel: "Ikusgaitasuna", taskPublic: "publikoa", taskPrivate: "pribatua", - taskCreatedAt: "Noiz", - taskBy: "Nork", taskStatus: "Egoera", taskStatusOpen: "Irekita", taskStatusInProgress: "Ekinean", taskStatusClosed: "Itxita", - taskAssignedTo: "Honi esleituta", + taskAssignedTo: "Esleituta", + taskAssignedChip: "Esleituta", + taskUnassignedChip: "Esleitu gabe", taskAssignees: "Hauei esleituta", taskAssignButton: "Esleitu neuri", taskUnassignButton: "Kendu esleipena", @@ -1325,12 +1387,12 @@ module.exports = { taskUpdateButton: "Eguneratu", taskDeleteButton: "Ezabatu", taskFilterAll: "GUZTIAK", + taskFilterRecent: "AZKENAK", taskFilterMine: "NEUREAK", taskFilterOpen: "IREKITA", taskFilterInProgress: "EKINEAN", taskFilterClosed: "ITXITA", taskFilterAssigned: "ESLEITUTA", - taskFilterArchived: "ARTXIBATUTA", taskFilterUrgent: "LARRIA", taskFilterHigh: "ALTUA", taskFilterMedium: "ERTAINA", @@ -1343,35 +1405,47 @@ module.exports = { taskInProgressTitle: "Atazak Ekinean", taskClosedTitle: "Atazak itxita", taskAssignedTitle: "Atazak esleituta", - taskArchivedTitle: "Atazak artxibatuta", - taskPublicTitle: "Ataza publikoak", - taskPrivateTitle: "Ataza pribatuak", notasks: "Atazarik ez.", noLocation: "Ez da kokapenik zehaztu", taskSetStatus: "Egoera ezarri", - eventTitle: "Ekitaldiak", eventDateLabel: "Data", + eventRecurrenceLabel: "Errepikapena", + eventRecurrenceUntil: "Errepikatu arte", + eventRecurrenceHint: "Utzi hutsik amaiera data ekitaldi bakarrerako.", + eventUpcomingDates: "Hurrengo datak", eventsTitle: "Ekitaldiak", eventsDescription: "Aurkitu eta kudeatu ekitaldiak zure sarean.", - eventDescription: "Deskribapena", + eventSearchPlaceholder: "Bilatu hemen: Ekitaldiak...", eventPrice: "Prezioa", eventStatus: "Egoera", - eventOrganizer: "Antolatzailea", eventAllSectionTitle: "Ekitaldiak", eventMineSectionTitle: "Zeure Ekitaldiak", - eventArchivedTitle: "Artxibatutako Ekitaldiak", eventCreateSectionTitle: "Sortu Ekitaldia", eventUpdateSectionTitle: "Eguneratu Ekitaldia", - eventDeleteButton: "Ezabatu", eventCreateButton: "Sortu Ekitaldia", eventAddToCalendar: "Egutegira gehitu", eventVisitCalendar: "Egutegia ikusi", viewTask: "Ikusi ataza", + viewEvent: "Ikusi ekitaldia", + viewPad: "Ikusi pad-a", + viewMap: "Ikusi mapa", + viewCalendar: "Ikusi egutegia", viewReport: "Ikusi txostena", viewTransfer: "Ikusi transferentzia", viewItem: "Ikusi elementua", viewShop: "Ikusi denda", viewProject: "Ikusi proiektua", + viewJob: "Ikusi Lanpostua", + viewPlace: "Ikusi Lekua", + generatePdf: "Sortu PDFa", + sharePm: "Partekatu PMz", + galleryImages: "Irudiak (geh. 75MB bakoitza)", + galleryPhotos: "Irudiak", + galleryAddPhoto: "Gehitu Irudia", + galleryRemovePhoto: "Kendu", + galleryVideo: "Bideoa (geh. 75MB)", + galleryAddVideo: "Gehitu Bideoa", + galleryRemoveVideo: "Kendu bideoa", viewVotation: "Ikusi bozketa", voteCastTitle: "Eman botoa", voteResults: "Emaitzak", @@ -1379,48 +1453,26 @@ module.exports = { eventDescriptionLabel: "Deskribapena", eventDescriptionPlaceholder: "Sartu ekitaldiaren deskribapena...", eventUpdateButton: "Eguneratu", - eventAttendeesLabel: "Partaideak", - eventAttendeesPlaceholder: "Sartu partaideak, erabili komak bereizteko.", eventTagsLabel: "Etiketak", - eventTagsPlaceholder: "Sartu ekitaldiaren etiketak, erabili komak bereizteko.", - eventTags: "Etiketak", eventPriceLabel: "Prezioa", eventUrlLabel: "URL", eventAttendees: "Partaideak", - noAttendees: "Partaiderik ez oraindik", - eventCreatedAt: "Noiz sortua", eventLocation: "Kokapena", eventLocationLabel: "Kokapena", - eventNoLocation: "Ez da zehaztu kokapenik", - eventNoURL: "Ez da zehaztu URL-rik", - eventBy: "Nork", noevents: "Ekitaldirik ez.", eventDate: "Data", - eventDateFormat: "YYYY/MM/DD HH:mm", eventAttendButton: "Joan Ekitaldira", - eventUnattendButton: "Joateari utzi", - eventCreatedBy: "Nork", - eventAttendeesCount: "Partaide kopurua", - eventCreatedByYou: "Zeuk sortu duzu ekitaldia", eventFilterAll: "GUZTIAK", + eventFilterRecent: "AZKENAK", eventFilterMine: "NEUREAK", eventFilterToday: "GAUR", eventFilterWeek: "ASTEON", eventFilterMonth: "HILEON", eventFilterYear: "AURTEN", eventFilterArchived: "ARTXIBATUTA", - eventTodayTitle: "Ekitaldiak gaur", - eventThisWeekTitle: "Ekitaldiak asteon", - eventThisMonthTitle: "Ekitaldiak hileon", - eventThisYearTitle: "Ekitaldiak aurten", eventPrivacyLabel: "Ikusgaitasuna", eventPublic: "Publikoa", eventPrivate: "Pribatua", - eventPublicTitle: "Ekitaldi Publikoak", - eventNoPrice: "Ekitaldia doan", - eventNoImage: "Ez da irudirik igo", - eventAttendConfirmation: "Ekitaldi honetara zoaz", - eventUnattendConfirmation: "Ez zara ekitaldi honetara joango", eventAttended: "Bertaratzen naiz", eventUnattended: "Ez naiz bertaratzen", eventStatusOpen: "Irekia", @@ -1431,9 +1483,14 @@ module.exports = { tagsTopSectionTitle: "Etiketa Gorenak", tagsCloudSectionTitle: "Etiketen hodeia", tagsFilterAll: "GUZTIAK", + tagsFilterMine: "NIREAK", + tagsFilterRecent: "BERRIAK", + tagsMineSectionTitle: "Nire etiketak", + tagsRecentSectionTitle: "Etiketa berriak", tagsFilterTop: "GORENAK", tagsFilterCloud: "HODEIA", tagsNoItems: "Etiketarik ez.", + tagsSearchPlaceholder: "Bilatu hemen: Etiketak...", tagsTableHeaderTag: "ETIKETA/LOTURA:", tagsTableHeaderCount: "KONTAGAILUA:", transfersTitle: "Transferentziak", @@ -1441,8 +1498,11 @@ module.exports = { transfersFrom: "Nork", transfersTo: "Nori", transfersFilterAll: "GUZTIAK", + transfersFilterRecent: "AZKENAK", transfersFilterMine: "NEUREAK", - transfersFilterUBI: "RBU", + transfersFilterMinePending: "ZAIN", + transfersFilterMineConfirmed: "SINATUAK", + transfersFilterUBI: "OBU", transfersFilterMarket: "MERKATUA", transfersFilterTop: "GORENAK", transfersFilterPending: "ZAIN", @@ -1456,7 +1516,6 @@ module.exports = { transfersCategoryEconomic: "Ekonomikoa", transfersCategoryTime: "Denbora", transfersCategoryTrust: "Konfiantza", - transfersCategoryApply: "Aplikatu", transfersUnitEco: "ECO", transfersUnitHours: "h", transfersUnitTrust: "konfiantza", @@ -1473,18 +1532,12 @@ module.exports = { transfersDeadline: "Epemuga", transfersTags: "Etiketak", transfersStatus: "Egoera", - transfersStatusUnconfirmed: "BERRETSI GABE", - transfersStatusClosed: "ITXITA", - transfersStatusDiscarded: "BAZTERTUTA", - transfersCreatedAt: "Noiz", transfersConfirmations: "BERRESPENAK", - transfersContainingBlock: "Bloke edukitzailea", - transfersExportContract: "Sortu kontratua", + transfersExportContract: "Sortu Faktura", transfersConfirmButton: "Berretsi Transferentzia", transfersNoItems: "Transferentziarik ez.", transfersMineSectionTitle: "Zeure Transferentziak", - transfersUBISectionTitle: "RBU Transferentziak", - transfersMarketSectionTitle: "Merkatuko Transferentziak", + transfersUBISectionTitle: "OBU transferentziak", transfersTopSectionTitle: "Transferentzia Gorenak", transfersPendingSectionTitle: "Transferentziak Zain", transfersUnconfirmedSectionTitle: "Transferentziak Berretsi Gabe", @@ -1492,21 +1545,13 @@ module.exports = { transfersDiscardedSectionTitle: "Transferentziak Baztertuta", transfersCreateSectionTitle: "Sortu transferentzia", transfersAllSectionTitle: "Transferentziak", - transfersFilterFavs: "Gustukoak", - transfersFavsSectionTitle: "Gustuko transferentziak", - transfersSearchLabel: "Bilatu", - transfersSearchPlaceholder: "Bilatu kontzeptua, etiketak, erabiltzaileak...", + transfersSearchPlaceholder: "Bilatu hemen: Transferentziak...", transfersMinAmountLabel: "Gutx. zenbatekoa", transfersMaxAmountLabel: "Geh. zenbatekoa", - transfersSortLabel: "Ordenatu", transfersSortRecent: "Berrienak", transfersSortAmount: "Zenbateko handiena", transfersSortDeadline: "Epe hurbilena", transfersSearchButton: "Bilatu", - transfersFavoriteButton: "Gustukoa", - transfersUnfavoriteButton: "Kendu gustukoa", - transfersMessageUserButton: "Mezua", - transfersExpiringSoonBadge: "LASTER", transfersExpiredBadge: "IRAUNGITA", transfersUpdatedAt: "Eguneratua", transfersNoMatch: "Ez dago bilaketarekin bat datorren transferentziarik.", @@ -1519,6 +1564,10 @@ module.exports = { voteClosedTitle: "Bozketak Itxita", voteAllSectionTitle: "Bozketak", voteCreateButton: "Sortu Bozketa", + voteErrorTitle: "Egiaztatu inprimakia", + voteErrorDeadlineMin: "Epemuga gutxienez {days} egunetara egon behar da.", + voteErrorGeneric: "Ezin izan da bozketa gorde.", + voteLocked: "Bozketa honek botoak edo iritziak jaso ditu dagoeneko, edo Parlamentuarena edo Auzitegiena da: ezin da editatu ez ezabatu.", voteUpdateButton: "Eguneratu", voteDeleteButton: "Ezabatu", voteOptionYes: "BAI", @@ -1528,6 +1577,7 @@ module.exports = { voteFollowMajority: "GEHIENGOAREN HAUTUA", voteNotInterested: "INTERESIK EZ", voteFilterAll: "GUZTIAK", + voteFilterRecent: "AZKENAK", voteFilterMine: "NEUREAK", voteFilterOpen: "IREKITA", voteFilterClosed: "ITXITA", @@ -1543,37 +1593,28 @@ module.exports = { voteTags: "Etiketak", voteOpinions: "Iritziak", novotes: "Bozkatzeko proposamenik ez.", - voteBy: "Nork", voteCreatedAt: "Noiz", voteNoQuestion: "Galderarik ez", voteUnknownCreator: "Sortzaile Ezezaguna", voteUnknownDate: "Data Ezezaguna", - errorVoteNotFound: "Bozka ez da aurkitu", - errorAlreadyVoted: "Jadanik bozkatu duzu", - errorVoteClosedCannotEdit: "Ezin duzu itxitako bozketa bat editatu", - errorVoteDeadlinePassed: "Bozketa honen epemuga igaro da", - errorRetrievingVote: "Errorea bozka eskuratzerakoan", - errorCreatingVote: "Errorea bozka sortzerakoan", - errorVoteAlreadyVoted: "Ezin duzu bozka editatu jadanik bozkatu baduzu", - errorDeletingOldVote: "Errorea bozka zaharra ezabatzerakoan", - errorCreatingUpdatedVote: "Errorea eguneratutako bozka sortzean", - errorCreatingTombstone: "Errorea hilarria sortzerakoan", voteDetailSectionTitle: 'Bozketa xehetasunak', voteCommentsLabel: 'Iruzkinak', voteCommentsForumButton: 'Eztabaida ireki', voteCommentsSectionTitle: 'Eztabaida irekia', voteNoCommentsYet: 'Oraindik ez dago iruzkinik. Izan zaitez lehen erantzuten.', + commentsGoToLast: "Joan azken iruzkinera", voteNewCommentPlaceholder: 'Idatzi hemen zure iruzkina…', voteNewCommentButton: 'Bidali iruzkina', voteNewCommentLabel: 'Gehitu iruzkina', cvTitle: "CV", - cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Editatu CV-a", cvCreateSectionTitle: "Sortu CV-a", cvDescription: "Kudeatu eta partekatu zure trebetasun profesionalak eta informazio gehiago.", cvNameLabel: "Izen Osoa", cvDescriptionLabel: "Laburpena", cvPhotoLabel: "Argazkia", + cvPdfLabel: "CV (PDF)", + cvVisitButton: "Bisitatu CVa", cvPersonalExperiencesLabel: "Esperientzia Pertsonalak", cvPersonalSkillsLabel: "Trebetasun Pertsonalak (komaz bereizita)", cvOasisExperiencesLabel: "Esperientzia Oasis-i Ekarpenak egiten", @@ -1586,29 +1627,130 @@ module.exports = { cvLocationLabel: "Kokapena", cvStatusLabel: "Egoera", cvPreferencesLabel: "Lehentasunak", - cvOasisContributorLabel: "Oasis-eko Parte-Hartzailea", cvPersonal: "Pertsonala", cvOasis: "Oasis-eko Parte-Hartzailea (optional)", - cvOasisContributorView: "Oasis-i Ekarpena", + cvOasisContributorView: "Ekarpena", cvEducational: "Hezkuntza (hautazkoa)", cvEducationalView: "Hezkuntza", cvProfessional: "Profesionala (hautazkoa)", cvProfessionalView: "Profesionala", cvAvailability: "Eskuragarritasuna (hautazkoa)", - cvAvailabilityView: "Eskuragarritasuna", cvUpdateButton: "Eguneratu", cvCreateButton: "Sortu CV-a", - cvContactLabel: "Kontaktua", cvCreatedAt: "Noiz sortua", cvUpdatedAt: "Noiz eguneratua", cvEditButton: "Eguneratu", cvDeleteButton: "Ezabatu", cvNoCV: "CV-rik ez.", blogSubject: "Gaia", - blogMessage: "Edukia 8096 karakteretara mugatuta.", - blogImage: "Multimedia igo (max: 50MB)", - blogPublish: "Aurrebista", - noPopularMessages: "Pil-pileko mezurike ez, oraindik", + blogMessage: "Mezua", + blogImage: "Multimedia igo (max: 75MB)", + blogMedia: "Eranskinak (8 irudi eta bideo bat gehienez)", + blogPublish: "Argitaratu", + pollsTitle: "Inkestak", + dataTitle: "Bat etortzeak", + dataDescription: "Arakatu eta ikusi zure sareko bat etortzeak.", + dataFilterAll: "DENAK", + dataFilterMine: "NIREAK", + dataFilterRecent: "BERRIENAK", + dataFilterTop: "TOP", + dataKindInhabitants: "BIZTANLEAK", + dataKindJobs: "LANPOSTUAK", + dataKindProjects: "PROIEKTUAK", + dataKindEvents: "EKITALDIAK", + dataKindTribes: "TRIBUAK", + dataKindMarket: "MERKATUA", + dataKindHousing: "ETXEBIZITZA", + dataKindIndustry: "INDUSTRIA", + dataKindTasks: "ZEREGINAK", + dataKindReports: "TXOSTENAK", + dataKindVotes: "BOZKETAK", + dataSearchPlaceholder: "Bilatu hemen: Bat etortzeak...", + dataCohesionTitle: "Kohesio Koefizientea (CC)", + dataCcShort: "CC", + dataCohesionHint: "Sareak argitaratutako guztiaren arteko batez besteko afinitatea.", + dataAffinity: "Afinitatea", + dataCommonTerms: "Gako-hitza", + dataReasonFollowing: "Bizilagun honi laguntzen diozu", + dataReasonTribe: "Tribu bat partekatzen du zurekin", + dataReasonPinned: "Finkatu duzun zerbaitekin bat dator", + dataReasonCv: "Zure CVko gaitasunekin bat dator", + dataReasonContent: "Argitaratzen duzunarekin lotuta", + dataReasonRelated: "Zure interesekin lotuta", + dataReasonAlike: "Zu bezalako bizilagun batek argitaratua", + dataReasonSupportsYou: "Laguntzen dizu", + dataReasonMutual: "Elkarren laguntza", + dataReasonRated: "Sareak ondo baloratua", + dataReasonNear: "Zure ondoan", + dataStatPeople: "CVak", + dataStatConnected: "Konektatuta", + dataStatSkills: "Trebetasunak", + dataBestMatch: "Match onena", + dataStatIsolated: "Isolatuta", + dataStatEntities: "Konparatutako entitateak", + dataStatTerms: "Termino desberdinak", + dataStatPairs: "Konektatutako bikoteak", + dataMatchesTitle: "Bat etortzeak", + dataMatchesHint: "Algoritmoaren iradokizun pertsonalizatuak.", + dataTermsTitle: "Gako-hitzak", + dataTermsHint: "Eduki gehienetan agertzen diren terminoak.", + dataConnections: "Lotutako bat-etortzeak", + dataNoMatches: "Ez dago datu gurutzaturik erakusteko.", + dataNoProfile: "Argitaratu edukia edo idatzi zure CVa, algoritmoak zure interesak ikas ditzan.", + pollsDescription: "Sareari galdetzeko eta erantzunak zenbatzeko modulua.", + pollFilterAll: "GUZTIAK", + pollFilterMine: "NIREAK", + pollFilterRecent: "BERRIENAK", + pollFilterTop: "TOP", + pollFilterVoted: "BOZKATUAK", + pollFilterOpen: "IREKIAK", + pollFilterClosed: "ITXIAK", + pollCreateButton: "Sortu Inkesta", + pollCreateTitle: "Sortu Inkesta", + pollEditTitle: "Editatu Inkesta", + pollQuestion: "Galdera", + pollQuestionPlaceholder: "Zer galdetu nahi duzu?", + pollOptions: "Aukerak (bat lerroko)", + pollOutcome: "Emaitza", + pollNoVotesYet: "Oraindik botorik ez", + pollTie: "Berdinketa", + pollOptionsPlaceholder: "Plaza\nParkea\nTaberna", + pollAnonymous: "Anonimoa", + pollAnonymousLabel: "Inkesta anonimoa", + pollMultiple: "Aukera anitza", + pollMultipleLabel: "Baimendu erantzun bat baino gehiago", + pollDeadline: "Epemuga", + pollTags: "Etiketak", + pollPublishButton: "Argitaratu Inkesta", + pollUpdateButton: "Eguneratu", + pollCloseButton: "Itxi", + pollDeleteButton: "Ezabatu", + pollVoteButton: "Bozkatu", + pollChangeVote: "Aldatu Botoa", + pollVoters: "Bozkatzaileak", + pollComments: "Iruzkinak", + pollStatusLabel: "Egoera", + pollStatusOpen: "IREKIA", + pollStatusClosed: "ITXIA", + pollSearchPlaceholder: "Bilatu hemen: Inkestak...", + pollsNoItems: "Ez da inkestarik aurkitu.", + viewPoll: "Ikusi Inkesta", + pollChatCreate: "Sortu Inkesta", + pollInChat: "Inkesta", + blogTitle: "Blogak", + blogDescription: "Blogak aurkitzeko eta kudeatzeko modulua.", + blogFilterAll: "GUZTIAK", + blogFilterMine: "NIREAK", + blogFilterRecent: "BERRIENAK", + blogFilterFavorites: "GOGOKOAK", + blogFilterTop: "TOP", + blogCreateButton: "Sortu Bloga", + blogSearchPlaceholder: "Bilatu hemen: Blogak...", + blogComments: "Iruzkinak", + blogAllowComments: "Baimendu iruzkinak", + blogCommentsClosed: "Egileak blog honen iruzkinak itxi ditu.", + blogNoItems: "Ez da blogik aurkitu.", + viewBlog: "Ikusi Bloga", forumTitle: "Foroak", forumCategoryLabel: "Kategoria", forumTitleLabel: "Izenburua", @@ -1616,43 +1758,21 @@ module.exports = { forumCreateButton: "Sortu foroa", forumCreateSectionTitle: "Sortu foroa", forumDescription: "Hitz egin sareko beste erabiltzaileekin libreki.", + forumSearchPlaceholder: "Bilatu hemen: Foroak...", forumFilterAll: "GUZTIAK", forumFilterMine: "NIREAK", forumFilterRecent: "BERRIENAK", forumFilterTop: "GORENAK", - forumMineSectionTitle: "Zure Foroak", - forumRecentSectionTitle: "Azken Foroak", - forumAllSectionTitle: "Foroak", - forumDeleteButton: "Ezabatu", forumParticipants: "parte-hartzaileak", forumMessages: "mezuak", - forumLastMessage: "Azken mezua", forumMessageLabel: "Mezua", forumMessagePlaceholder: "Idatzi zure mezua...", forumSendButton: "Bidali", - forumVisitForum: "Bisitatu Foroaren", noForums: "Ez da fororik aurkitu.", - forumVisitButton: "Foroa bisitatu", - forumCatGENERAL: "Orokorra", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "Politika", - forumCatTECH: "Teknologia", - forumCatSCIENCE: "Zientzia", - forumCatMUSIC: "Musika", - forumCatART: "Artea", - forumCatGAMING: "Bideojokoak", - forumCatBOOKS: "Liburuak", - forumCatFILMS: "Filmak", - forumCatPHILOSOPHY: "Filosofia", - forumCatSOCIETY: "Gizartea", - forumCatPRIVACY: "Pribatutasuna", - forumCatCYBERWARFARE: "Zibergerra", - forumCatSURVIVALISM: "Biziraupena", + forumVisitButton: "Ikusi Foroa", + forumTopicLabel: "Gaia", imageTitle: "Irudiak", imageDescription: "Arakatu eta kudeatu zure sareko irudi-edukia.", - imagePluginTitle: "Izenburua", - imagePluginDescription: "Deskribapena", imageMineSectionTitle: "Zure irudiak", imageCreateSectionTitle: "Irudia igo", imageUpdateSectionTitle: "Irudia eguneratu", @@ -1661,17 +1781,14 @@ module.exports = { imageTopSectionTitle: "Bozkatuenak", imageFavoritesSectionTitle: "Gogokoak", imageGallerySectionTitle: "Galeria", - imageMemeSectionTitle: "Memak", imageFilterAll: "GUZTIAK", imageFilterMine: "NIREAK", imageFilterRecent: "AZKENAK", imageFilterTop: "TOP", imageFilterFavorites: "GOGOKOAK", imageFilterGallery: "GALERIA", - imageFilterMeme: "MEMAK", imageCreateButton: "Irudia igo", imageUpdateButton: "Eguneratu", - imageDeleteButton: "Ezabatu", imageAddFavoriteButton: "Gehitu gogokoetara", imageRemoveFavoriteButton: "Kendu gogokoetatik", imageFileLabel: "Aukeratu irudi-fitxategi bat (.jpeg, .jpg, .png, .gif)", @@ -1681,17 +1798,16 @@ module.exports = { imageTitlePlaceholder: "Aukerakoa", imageDescriptionLabel: "Deskribapena", imageDescriptionPlaceholder: "Aukerakoa", - imageMemeLabel: "¿MEME?", imageNoFile: "Ez da irudi-fitxategirik eman", noImages: "Ez dago irudirik erabilgarri.", - imageSearchPlaceholder: "Bilatu izenburua, etiketak, deskribapena, egilea...", + imageSearchPlaceholder: "Bilatu hemen: Irudiak...", imageSortRecent: "Berrienak", imageSortOldest: "Zaharrenak", imageSortTop: "Bozkatuenak", imageSearchButton: "Bilatu", - imageMessageAuthorButton: "Mezua", imageUpdatedAt: "Eguneratua", imageNoMatch: "Ez dago zure bilaketarekin bat datorren irudirik.", + feedSearchPlaceholder: "Bilatu hemen: Jarioa...", feedTitle: "Jarioa", feedDetailTitle: "Feed", feedOpenDiscussion: "Eztabaida ireki", @@ -1701,17 +1817,16 @@ module.exports = { createFeedButton: "Bidali Jarioa!", feedPlaceholder: "Zer berri? (max 280 characters)", TODAYButton: "GAUR", - CREATEButton: "Sortu Jarioa", totalOpinions: "Iritziak Guztira", moreVoted: "Gehien bozkatu", noFeedsFound: "Ez da jariorik aurkitu.", createdAtLabel: "Noiz", FeedshareYourOpinions: "Aurkitu eta partekatu testu laburrak zure sarean.", - refeedButton: "Realimentatu", + refeedButton: "Birbidali", alreadyRefeeded: "Berjaiotu duzu jada.", activityTitle: "Jarduera", yourActivity: "Zure jarduera", - globalActivity: "Jarduera globala", + globalActivity: "Jarduera", activityList: "Jarduera", activityDesc: "Ikusi zure sareko azken jarduera.", allButton: "GUZTIA", @@ -1723,20 +1838,13 @@ module.exports = { concept: "Kontzeptua", description: "Deskribapena", meme: "Meme", - activityContact: "Kontaktua", - activityBy: "Izena", - activityPixelia: "Pixel berria gehituta", - viewImage: "Ikusi irudia", - playAudio: "Audioa erreproduzitu", - playVideo: "Bideoa erreproduzitu", - typeRecent: "AZKENA", - errorActivity: "Errorea jarduera eskuratzean", - typePost: "BLOGAK", - recentButton: "AZKENAK", + typeRecent: "AZKENAK", + typeTop: "TOP", + typePost: "BLOGA", + activityUpdateLabel: "EGUNERAKETA", typeTribe: "TRIBUAK", typeLarp: "L.A.R.P.", typeInhabitants: "BIZTANLEAK", - activityProfileUpdated: "bere profila eguneratu du", typeAbout: "BIZTANLEAK", typeCurriculum: "CVAK", typeImage: "IRUDIAK", @@ -1758,10 +1866,10 @@ module.exports = { typeFeed: "FEEDAK", typeContact: "KONTAKTUA", typePub: "PUB", - typeTombstone: "TOMBSTONE", + typeTombstone: "EZABATUA", typeBanking: "BANKUA", typeBankWallet: "BANKUA/ZORROA", - typeBankClaim: "BANKUA/UBI", + typeBankClaim: "BANKUA/OBU", typeKarmaScore: "KARMA", typeParliament: "PARLAMENTUA", typeSpread: "ERREPLIKAK", @@ -1780,8 +1888,6 @@ module.exports = { typeCourtsSettlementAccepted: "Epaitegiak · Akordio onartua", typeCourtsNomination: "Epaitegiak · Izendapena", typeCourtsNominationVote: "Epaitegiak · Izendapen bozketak", - activitySupport: "Aliantza berria sortua", - activityJoin: "PUB berria batu da", question: "Galdera", deadline: "Epemuga", status: "Egoera", @@ -1795,21 +1901,16 @@ module.exports = { date: "Data", category: "Kategoria", attendees: "Parte-hartzaileak", - activitySpread: "->", - visitLink: "Esteka bisitatu", - viewDocument: "Dokumentua ikusi", location: "Kokalekua", contentWarning: "Gaia", - personName: "Biztanlearen izena", bankWalletConnected: "ECOin zorroa", - bankUbiReceived: "Jasotako UBI", + bankUbiReceived: "Jasotako OBU", bankTx: "Tx", bankEpochShort: "Epoka", viewDetails: "Xehetasunak ikusi", link: "Esteka", activityProjectFollow: "%OASIS% orain %ACTION% proiektu hau %PROJECT%", activityProjectUnfollow: "%OASIS% orain %ACTION% proiektu hau %PROJECT%", - activityProjectPledged: "%OASIS%-(e)k %ACTION% %AMOUNT% egin du %PROJECT% proiektuan", following: "JARRAITZEN", unfollowing: "EZ JARRAITZEN", pledged: "KONPROMISOA", @@ -1836,8 +1937,11 @@ module.exports = { parliamentLawVotes: "Botoak", createdAt: "Sortze-data", reportsTitle: "Txostenak", - reportsDescription: "Kudeatu eta jarraitu arazo, akats, gehiegikeri eta eduki-abisuei buruzko txostena zure sarean.", + reportsDescription: "Kudeatu eta jarraitu zure sareko txostenak.", + reportsSearchPlaceholder: "Bilatu hemen: Txostenak...", reportsFilterAll: "GUZTIAK", + reportsFilterRecent: "BERRIAK", + reportsFilterTop: "TOP", reportsFilterMine: "NEUREAK", reportsFilterFeatures: "GAITASUNAK", reportsFilterBugs: "BUGAK", @@ -1850,18 +1954,13 @@ module.exports = { reportsFilterInvalid: "BALIOGABEKOA", reportsCreateButton: "Sortu Txostena", reportsTitleLabel: "Izenburua", - reportsDescriptionLabel: "Deskribapena", - reportsDescriptionPlaceholder: "Idatzi deskribapen zehatza, mesedez.", reportsCategory: "Kategoria", - reportsCategoryLabel: "Kategoria", reportsCategoryFeatures: "Gaitasunak", reportsCategoryBugs: "Bugak", reportsCategoryAbuse: "Gehiegikeria", - reportsCategoryContent: "Arazoak Edukiarekin", + reportsCategoryContent: "Edukia", reportsUpdateButton: "Eguneratu", reportsDeleteButton: "Ezabatu", - reportsDateLabel: "Data", - reportsUploadFile: "Multimedia igo (max: 50MB)", reportsMineSectionTitle: "Zeure Txostenak", reportsFeaturesSectionTitle: "Gaitasuna Eskatu", reportsBugsSectionTitle: "Bugak", @@ -1869,11 +1968,6 @@ module.exports = { reportsContentSectionTitle: "Arazoak Edukiarekin", reportsAllSectionTitle: "Txostenak", reportsNoItems: "Txostenik ez.", - reportsValidationTitle: "Sartu izenburu zuzena, mesedez.", - reportsValidationDescription: "Deskribapena ezin da hutsi egon.", - reportsValidationCategory: "Aukeratu kategoria, mesedez.", - reportsCreatedAt: "Noiz", - reportsCreatedBy: "Nork", reportsSeverity: "Larritasuna", reportsSeverityLow: "Baxua", reportsSeverityMedium: "Ertaina", @@ -1884,18 +1978,14 @@ module.exports = { reportsStatusUnderReview: "Berrikusten", reportsStatusResolved: "Zuzenduta", reportsStatusInvalid: "Balio gabekoa", - reportsUpdateStatusButton: "Eguneratu Egoera", - reportsAnonymityOption: "Bidali Anonimoki", - reportsAnonymousAuthor: "Anonimoa", reportsConfirmButton: "BERRETSI TXOSTENA!", reportsConfirmations: "Berrespenak", reportsConfirmedSectionTitle: "Berretsitako Txostenak", - reportsCreateTaskButton: "SORTU ATAZA", + reportsCreateTaskButton: "Sortu Ataza", reportsOpenSectionTitle: "Ireki Txostenak", reportsUnderReviewSectionTitle: "Txostenak Berrikuspenean", reportsResolvedSectionTitle: "Zuzendutako Txostenak", reportsInvalidSectionTitle: "Txosten Okerrak", - reportsTemplateSectionTitle: 'Txosten-eredua', reportsBugTemplateTitle: 'Erreprodukzio xehetasunak (Bug-ak)', reportsFeatureTemplateTitle: 'Xehetasunak (Ezaugarriak)', reportsAbuseTemplateTitle: 'Xehetasunak (Abusua)', @@ -1933,6 +2023,20 @@ module.exports = { reportsRequestedActionLabel: 'Eskatutako ekintza', reportsRequestedActionPlaceholder: 'Kendu, ezkutatu, etiketatu, ohartarazi, etab.', tribesTitle: "Tribuak", + tribeFilterAll: "GUZTIAK", + tribeFilterRecent: "BERRIAK", + tribeFilterMine: "NIREAK", + tribeFilterMembership: "KIDETZA", + tribeFilterSubtribes: "AZPITRIBUAK", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALERIA", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "NIREAK", + tribeFeedFilterALL: "GUZTIAK", + tribeFeedFilterRECENT: "BERRIAK", + courtsStanceDENY: "Ukatu", + courtsStanceADMIT: "Onartu", + courtsStancePARTIAL: "Neurri batean onartu", tribeAllSectionTitle: "Tribuak", tribeMineSectionTitle: "Zeure Tribuak", tribeCreateSectionTitle: "Sortu Tribua", @@ -1940,42 +2044,37 @@ module.exports = { tribeGallerySectionTitle: "Tribuen Galeria", tribeRecentSectionTitle: "Tribu Berriak", tribeTopSectionTitle: "Tribu Gorenak", - tribeviewTribeButton: "Tribua Bisitatu", - tribeviewSubTribeButton: "Azpi-Tribua Bisitatu", tribeRootLabel: "ERROA", tribeDescription: "Aurkitu edo sortu tribuak zure sarean.", - tribeFilterAll: "GUZTIAK", - tribeFilterMine: "NEUREAK", - tribeFilterMembership: "BAZKIDETZA", - tribeFilterRecent: "BERRIAK", - tribeFilterTop: "TOP", - tribeFilterSubtribes: "AZPI-TRIBUAK", - tribeFilterGallery: "GALERIA", tribeMainTribeLabel: "TRIBU NAGUSIA", tribeCreateButton: "Sortu Tribua", tribeUpdateButton: "Egutneratu", tribeDeleteButton: "Ezabatu", - tribeImageLabel: "Multimedia igo (max: 50MB)", + tribeImageLabel: "Multimedia igo (max: 75MB)", tribeTitleLabel: "Izenburua", - searchTribesPlaceholder: "IRAGAZI tribuak IZENAREN ARABERA …", + searchTribesPlaceholder: "Bilatu hemen: Tribuak...", tribeTitlePlaceholder: "Tribuaren izena", tribeDescriptionLabel: "Deskribapena", tribeDescriptionPlaceholder: "Deskribatu tribua", tribeLocationLabel: "Kokapena", tribeLocationPlaceholder: "Non kokatzen da tribua?", tribeTagsLabel: "Etiketak", - tribeTagsPlaceholder: "Sartu etiketak, erabili komak bereizteko.", + tribeTagsPlaceholder: "Idatzi etiketak komaz bereizita", tribeInviteMode: "Gonbidapen modua", tribeModeLabel: "Gonbidapen MODUA", tribeIsAnonymousLabel: "Publikoa?", tribeMembersCount: "Partaideak", - tribeInviteCodePlaceholder: "Sartu gonbidapen kodea", - tribeJoinByCodeButton: "Sartu kodearekin", - tribeJoinButton: "SARTU", tribeEnterInvite: "SARTU GONBIDAPENA", + subscriptionTitle: "Harpidetzak", + subscriptionOn: "Harpidetuta", + subscriptionOff: "Harpidetu gabe", + subscriptionSubscribe: "Harpidetu", + subscriptionUnsubscribe: "Harpidetza utzi", + subscriptionSubscribers: "Harpidedunak", + pmListLabel: "Nori bidali", + pmListNone: "— Zerrendarik ez —", + pmListNoSubscribers: "Harpidedunik ez", tribeLeaveButton: "ATERA", - tribeYes: "BAI", - tribeNo: "EZ", tribePublic: "PULIKOA", tribePrivate: "PRIBATUA", tribeGenerateInvite: "GONBIDAPEN BAKARRA", @@ -1984,13 +2083,8 @@ module.exports = { tribeRemoveInvitation: "KENDU GONBIDAPENA", tribeCreatedAt: "Noiz", tribeAuthor: "Nork", - tribeAuthorLabel: "EGILEA", tribeStrict: "Zorrotza", tribeOpen: "Irekita", - tribeFeedFilterRECENT: "BERRIAK", - tribeFeedFilterMINE: "NEUREAK", - tribeFeedFilterALL: "GUZTIAK", - tribeFeedFilterTOP: "GORENAK", tribeFeedRefeeds: "BERJARIOAK", tribeFeedRefeed: "Berjariotu", tribeFeedMessagePlaceholder: "Idatzi jarioa…", @@ -2000,33 +2094,28 @@ module.exports = { tribeNotFound: "Tribua ez da aurkitu!", createTribeTitle: "Sortu Tribua", updateTribeTitle: "Eguneratu Tribua", - tribeSectionOverview: "Ikuspegi orokorra", tribeSectionInhabitants: "Biztanleak", tribeSectionVotations: "BOZKATZEAK", tribeSectionEvents: "EKITALDIAK", - tribeSectionReports: "Txostenak", tribeSectionTasks: "ATAZAK", tribeSectionFeed: "JARIOA", tribeSectionForum: "FOROA", - tribeSectionMarket: "Merkatua", - tribeSectionJobs: "Lanak", - tribeSectionProjects: "Proiektuak", - tribeSectionMedia: "Media", + tribeSectionMedia: "Multimedia", tribeSectionImages: "IRUDIAK", tribeSectionAudios: "AUDIOAK", tribeSectionVideos: "BIDEOAK", tribeSectionDocuments: "DOKUMENTUAK", tribeSectionBookmarks: "LASTER-MARKAK", - tribeSectionMaps: "MAPS", - tribeSectionPads: "PADS", - tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionMaps: "MAPAK", + tribeSectionPads: "PADAK", + tribeSectionChats: "TXATAK", + tribeSectionCalendars: "EGUTEGIAK", + tribePadCreate: "Sortu pad bat", + tribeChatCreate: "Sortu txat bat", + tribeCalendarCreate: "Sortu egutegi bat", + tribePadsEmpty: "Oraindik ez dago padik.", + tribeChatsEmpty: "Oraindik ez dago txatik.", + tribeCalendarsEmpty: "Oraindik ez dago egutegirik.", tribeInhabitantsEmpty: "Tribu honetan biztanlerik ez, oraindik.", tribeEventCreate: "Sortu Ekitaldia", tribeEventsEmpty: "Ekitaldirik ez, oraindik.", @@ -2048,11 +2137,6 @@ module.exports = { tribeTaskStatusClosed: "ITXI", tribeTaskAssign: "ESLEITU", tribeTaskUnassign: "KENDU", - tribeReportCreate: "Sortu Txostena", - tribeReportsEmpty: "Txostenik ez, oraindik.", - tribeReportTitle: "Izenburua", - tribeReportDescription: "Deskribapena", - tribeReportCategory: "Kategoria", tribeVotationCreate: "Sortu Bozketa", tribeVotationsEmpty: "Bozketarik ez, oraindik.", tribeVotationTitle: "Izenburua", @@ -2070,27 +2154,6 @@ module.exports = { tribeForumCategory: "Kategoria", tribeForumReply: "Erantzun", tribeForumReplies: "Erantzunak", - tribeMarketCreate: "Sortu Iragarkia", - tribeMarketEmpty: "Iragarkirik ez, oraindik.", - tribeMarketTitle: "Izenburua", - tribeMarketDescription: "Deskribapena", - tribeMarketPrice: "Prezioa", - tribeMarketImage: "Irudia", - tribeMarketCategory: "Kategoria", - tribeJobCreate: "Sortu Lana", - tribeJobsEmpty: "Lanik ez, oraindik.", - tribeJobTitle: "Izenburua", - tribeJobDescription: "Deskribapena", - tribeJobLocation: "Kokalekua", - tribeJobSalary: "Soldata", - tribeJobDeadline: "Epemuga", - tribeProjectCreate: "Sortu Proiektua", - tribeProjectsEmpty: "Proiekturik ez, oraindik.", - tribeProjectTitle: "Izenburua", - tribeProjectDescription: "Deskribapena", - tribeProjectGoal: "Helburua", - tribeProjectFunded: "Finantzatua", - tribeProjectDeadline: "Epemuga", tribeMediaUpload: "Igo Media", readDocument: "Dokumentua Irakurri", tribeCreateImage: "Irudia Sortu", @@ -2101,21 +2164,20 @@ module.exports = { tribeMediaEmpty: "Mediarik ez, oraindik.", tribeMediaTitle: "Izenburua", tribeMediaDescription: "Deskribapena", - tribeMediaType: "Mota", tribeMediaTypeImage: "Irudia", tribeMediaTypeVideo: "Bideoa", tribeMediaTypeAudio: "Audioa", tribeMediaTypeDocument: "Dokumentua", tribeMediaTypeBookmark: "Laster-marka", tribeContentDelete: "EZABATU", + tribeReachTribe: "TRIBUA", + tribeReachNotice: "OASISek biztanle guztiei erakusten die eta CLEARNETek web irekian ere bai. Irekitzeak kopia irakurgarri bat uzten du tributik kanpo: TRIBUAra itzultzeak ezkutatu egiten du, baina dagoeneko partekatutako kopiak ezin dira ezabatu.", + tribeReachPrivate: "Tribu pribatu baten edukia tribuaren barruan bakarrik gera daiteke.", + tribeReachOpenedBy: "Ireki duena", + tribeReachChange: "Non argitaratzen den", tribeInviteCodeText: "Gonbidapen kodea: ", oasisVersionLabel: "Oasis Bertsioa", - tribeInviteCodeHint: "Partekatu kode hau gonbidatu nahi duzun pertsonarekin. /invites → Tribes bidez sar daiteke.", - tribeGroupTribe: "Tribua", - tribeGroupOffice: "Bulegoa", - tribeGroupNetwork: "Sarea", - tribeGroupEconomy: "Ekonomia", - tribeGroupMedia: "Media", + tribeInviteCodeHint: "Partekatu kode hau gonbidatu nahi duzun pertsonarekin. /invites bidez sar daiteke.", tribeStatusOpen: "IREKITA", tribeStatusClosed: "ITXITA", tribeStatusInProgress: "ABIAN", @@ -2125,33 +2187,17 @@ module.exports = { tribePriorityCritical: "KRITIKOA", tribeTaskFilterAll: "GUZTIAK", tribeMediaFilterAll: "GUZTIAK", - tribeReportCatBug: "AKATSA", - tribeReportCatAbuse: "ABUSUA", - tribeReportCatContent: "EDUKIA", - tribeReportCatOther: "BESTEA", tribeForumCatGeneral: "OROKORRA", tribeForumCatProposal: "PROPOSAMENA", tribeForumCatQuestion: "GALDERA", tribeForumCatAnnouncement: "IRAGARPENA", - tribeMarketCatGoods: "ONDASUNAK", - tribeMarketCatServices: "ZERBITZUAK", - tribeMarketCatFood: "ELIKADURA", - tribeMarketCatOther: "BESTEA", tribeStatusLabel: "Egoera", tribeSubTribes: "AZPI-TRIBUAK", tribeSubTribesCreate: "Azpi-Tribua Sortu", tribeSubTribesStrictDenied: "Tribuaren modu zorrotzak ez dizu azpi-tribu berriak sortzen uzten. Jarri harremanetan administratzailearekin.", - tribeSubTribesEmpty: "Ez da azpi-triburik sortu, oraindik.", - tribeActivityJoined: "BATUTA", - tribeActivityLeft: "IRTEN", - tribeActivityFeed: "FEED", - tribeActivityRefeed: "REFEED", - tribeGroupAnalytics: "Analitikak", - tribeGroupCreative: "Sormenez", + tribeActivityRefeed: "BERJARIOA", tribeSectionActivity: "JARDUERA", tribeSectionTrending: "JOERAK", - tribeSectionOpinions: "IRITZIAK", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "ETIKETAK", tribeSectionSearch: "BILATU", tribeActivityEmpty: "Ez dago jarduerarik oraindik.", @@ -2163,25 +2209,13 @@ module.exports = { tribeTrendingPeriodWeek: "Aste Honetan", tribeTrendingPeriodAll: "Dena", tribeTrendingEngagement: "parte-hartzea", - tribeOpinionsEmpty: "Ez dago iritzirik oraindik.", - tribeOpinionsCast: "Bozkatu", - tribeOpinionsRankings: "Sailkapenak", - tribeOpinionsAlreadyVoted: "Dagoeneko bozkatu duzu", tribeTopCategory: "Gehien bozkatua", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "Pixel art oihal kolaboratiboa.", - tribePixeliaPaint: "Pintatu", - tribePixeliaContributors: "laguntzaileak", - tribePixeliaTotalPixels: "pixel margotuta", tribeTagsEmpty: "Ez da etiketarik aurkitu.", - tribeTagsCloud: "Etiketa Hodeia", - tribeTagsContentWith: "Etiketadun edukia", - tribeSearchPlaceholder: "Bilatu tribuaren edukian...", tribeSearchEmpty: "Emaitzarik ez.", - tribeSearchResults: "Emaitzak", tribeSearchMinChars: "Idatzi gutxienez 2 karaktere bilatzeko.", agendaTitle: "Agenda", agendaDescription: "Eskeituta dauzkazun elementu guztiak aurki ditzakezu hemen.", + agendaSearchPlaceholder: "Bilatu hemen: Agenda...", agendaFilterAll: "GUZTIAK", agendaFilterToday: "GAUR", agendaFilterUpcoming: "DATOZENAK", @@ -2195,24 +2229,16 @@ module.exports = { agendaFilterReports: "TXOSTENAK", agendaFilterTransfers: "TRANSFERENTZIAK", agendaFilterJobs: "LANPOSTUAK", + agendaFilterIndustry: "INDUSTRIA", + agendaFilterHousing: "ETXEBIZITZA", agendaFilterProjects: "PROIEKTUAK", agendaFilterCalendars: "EGUTEGIAK", agendaInviteModeLabel: "Egoera", agendaNoItems: "Esleipenik ez.", agendaDiscardButton: "Baztertu", agendaRestoreButton: "Berrezarri", - agendaAuthor: "Nork", - agendaCreatedAt: "Noiz", - agendaTitleLabel: "Izenburua", + agendaRemoveButton: "Kendu", agendaMembersCount: "Partaideak", - agendaDescriptionLabel: "Deskribapena", - agendaStatus: "Egoera", - agendaVisibility: "Ikusgaitasuna", - agendaEventDate: "Ekitaldiaren Data", - agendaEventLocation: "Kokapena", - agendaEventPrice: "Prezioa", - agendaEventUrl: "URL-a", - agendaTaskStart: "Hasiera Data", agendaLocationLabel: "Kokapena", agendaYes: "BAI", agendaNo: "EZ", @@ -2221,11 +2247,6 @@ module.exports = { agendareportCategory: "Kategoria", agendareportSeverity: "Larritasuna", agendareportStatus: "Egoera", - agendareportDescription: "Deskribapena", - agendaTaskEnd: "Amaiera Data", - agendaTaskPriority: "Lehentasuna", - agendaTransferFrom: "Nork", - agendaTransferTo: "Nori", agendaTransferConcept: "Kontzeptua", agendaTransferAmount: "Kopurua", agendaTransferDeadline: "Epemuga", @@ -2235,47 +2256,7 @@ module.exports = { voteNow: "Bozkatu orain", alreadyVoted: "Jada zure iritzia eman duzu.", noOpinionsFound: "Ez da iritzirik aurkitu.", - RECENTButton: "AZKENAK", TOPButton: "GORDEAK", - interestingButton: "INTERESGARRIA", - necessaryButton: "BEHARREZKOA", - funnyButton: "BARREGARRIA", - disgustingButton: "LOREZTUGARRIA", - sensibleButton: "SENTIKORRA", - propagandaButton: "PROPAGANDA", - adultOnlyButton: "ADULTO BAKARRIK", - boringButton: "NEKAGARRIA", - confusingButton: "ZAILA", - usefulButton: "ERABILI", - informativeButton: "INFORMATIBOA", - wellResearchedButton: "ONDO IKERTUA", - accurateButton: "EGOKITUA", - needsSourcesButton: "ITURRIK BEHAR DITU", - wrongButton: "OKERREKOA", - lowQualityButton: "BEHERA KALITATEA", - creativeButton: "KREABO", - insightfulButton: "ARRETAZKOA", - actionableButton: "ERABILI DAITEKE", - inspiringButton: "INSPIRATZAILEA", - loveButton: "MAITASUN", - clearButton: "ARGI", - upliftingButton: "EGOITZAILEA", - unnecessaryButton: "EZINBESTEKOA", - rejectedButton: "UKATUA", - misleadingButton: "ENGAINEZKOA", - offTopicButton: "GAITIK AT", - duplicateButton: "DUPLIKATU", - clickbaitButton: "KLIK-APURKA", - spamButton: "SPAM", - trollButton: "TROLL", - nsfwButton: "NSFW", - violentButton: "BORTITZA", - toxicButton: "TOXIKOA", - harassmentButton: "BAZTERKETA", - hateButton: "ARRETA", - scamButton: "ENGAINU", - triggeringButton: "HAUSKORRA", - opinionsCreatedAt: "Sortu zen", opinionsTotalCount: "Iritzi guztiak", voteInteresting: "Interesgarria", voteNecessary: "Beharrezkoa", @@ -2312,29 +2293,41 @@ module.exports = { voteCreative: "Sortzailea", voteSpam: "Spam", voteAdultOnly: "Ados bakarrik", - publishBlog: "Bloga argitaratu", privateMessage: "MP", pmSendTitle: "Mezu Pribatuak", pmSend: "Bidali!", - pmDescription: "Erabili formulario hau beste biztanleei mezu zifratua bidaltzeko.", + pmCancel: "Utzi", + pmReset: "Berrezarri", + pmSharedKeyHint: "Partekatu gako hau hartzailearekin beste bide batetik. Deszifratzeko beharrezkoa da.", + pmDescription: "Erabili formulario hau beste biztanleei mezu/fitxategi zifratu bat bidaltzeko.", + pmComposeTitle: "Bidali mezu bat", pmRecipients: "Hartzaileak", pmRecipientsHint: "Idatzi Oasis IDak, komaz bereizita", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "Mezu bakoitzeko 7 bizilagun gehienez.", + pmTextPlaceholder: "Idatzi zure mezua.", pmSubject: "Gaia", pmSubjectHint: "Idatzi mezuaren gaia", pmText: "Mezua", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Deszifratu", + fileShareTitle: "Partekatu fitxategi bat", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "Fitxategia", + fileShareDownload: "Deskargatu", + fileShareMutualError: "Elkarrekiko laguntza duten biztanleekin soilik parteka ditzakezu fitxategiak.", + fileShareNoFile: "Ez da fitxategirik hautatu.", + fileShareTooLarge: "Fitxategiak baimendutako tamaina gainditzen du.", + fileShareFailed: "Ezin izan da fitxategia prestatu.", + fileShareSendError: "Ezin izan da fitxategia bidali.", + fileShareUnavailable: "Fitxategi hau ez dago erabilgarri une honetan. Saiatu geroago.", pmCrypterBadKey: "Partekatutako gakoa ez da zuzena!", pmCrypterTooLong: "Mezua luzeegia da Crypter-arekin zifratzeko. Laburtu eta saiatu berriro.", pmCrypterCipherLabel: "Berriz zifratutako mezua", pmCrypterCharsLabel: "karaktere", pmInvalidRecipients: "Oasis ID baliogabea (@…=.ed25519 itxurakoa izan behar du).", pmEncryptionLabel: "Zifratzea", - pmFile: "Eranskina", - private: "Pribatuak", - privateDescription: "Zure mezu zifratuak.", + private: "Sarrera-ontzia", + privateDescription: "Kudeatu zure mezu pribatuak.", privateInbox: "SARRERA-ONTZIA", privateSent: "BIDALITAKOAK", privateDelete: "Ezabatu", @@ -2342,34 +2335,40 @@ module.exports = { noPrivateMessages: "Ez dago mezu pribaturik.", pmReply: "Erantzun", pmReplies: "erantzunak", - pmNew: "berriak", - pmMarkRead: "Irakurritako gisa markatu", inReplyTo: "HONI ERANTZUNEZ", + forumInThread: "HARIAN", pmPreview: "Aurrebista", pmPreviewTitle: "Mezuaren aurrebista", performed: "→", pmFromLabel: "Nork:", pmToLabel: "Nori:", - pmInvalidMessage: "Mezu baliogabea", pmNoSubject: "(gaia gabe)", pmSubjectLabel: "Gaia:", - pmBodyLabel: "Gorputza", - pmBotJobs: "42-JobsBOT", - pmBotProjects: "42-ProjectsBOT", - pmBotMarket: "42-MarketBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "L.A.R.P-eko etxe gobernatzailea aldatu da.", + politicalBotParliamentTitle: "Gobernu-ziklo berri bat hasi da Parlamentuan.", + politicalBotTribeTitle: "Gobernu-ziklo berri bat hasi da zure Tribuetako batean.", + politicalBotLarpIntro: "Orain {house} ari da gobernatzen ziklo honetan: {cycle}", + politicalBotParliamentIntro: "Egungo gobernua {gov} da", + politicalBotParliamentIntroLeader: "Egungo gobernua {gov} da, {leader}-(e)k gauzatua", + politicalBotTribeIntro: "{tribe} Tribuko egungo gobernua {gov} da", + politicalBotTribeIntroLeader: "{tribe} Tribuko egungo gobernua {gov} da, {leader}-(e)k gauzatua", + politicalBotLeaderLabel: "Liderra", inboxJobSubscribedTitle: "Lan-eskaintzara harpidetza berria", pmInhabitantWithId: "OASIS ID duen biztanlea:", pmHasSubscribedToYourJobOffer: "zure lan-eskaintzara harpidetu da", - inboxProjectCreatedTitle: "Proiektu berria sortu da", - pmHasCreatedAProject: "proiektu bat sortu du", inboxMarketItemSoldTitle: "Artikulua salduta", + inboxShopSoldTitle: "Produktua salduta", pmYourItem: "Zure artikulua", pmHasBeenSoldTo: "honi saldu zaio", pmFor: "truke", inboxProjectPledgedTitle: "Ekarpen berria zure proiektuan", pmHasPledged: "ekarpena egin du", pmToYourProject: "zure proiektura", - blockchainTitle: 'BlockExplorer', blockchainDescription: 'Blokeak aztertu eta ikusgaitu blockchain-ean.', blockchainNoBlocks: 'Ez da blokeik aurkitu blockchain-ean.', blockchainStatsTitle: 'Estatistikak', @@ -2381,41 +2380,30 @@ module.exports = { blockchainBlockCarbon: 'Karbono-aztarna', blockchainBlockType: 'Mota', blockchainBlockTimestamp: 'Tzeitza', - blockchainBlockContent: 'Bloke', - blockchainBlockURL: 'URL:', - blockchainContent: 'Bloke', - blockchainContentPreview: 'Bloke edukia aurrebista', blockchainLatestDatagram: 'Azken Datagrama', - blockchainDatagram: 'Datagrama', - blockchainDetails: 'Ikusi blokearen xehetasunak', - blockchainViewBlockexplorer: "Ikusi blockexplorer-ean", - blockchainBlockInfo: 'Blokearen informazioa', - blockchainBlockDetails: 'Hautatutako blokearen xehetasunak', + blockchainDatagram: "Datagrama", + blockchainDetails: "Ikusi Blokearen Xehetasunak", + blockchainViewBlockexplorer: "Ikusi Blockexplorer-en", blockchainBack: 'Itzuli blokearen azterkira', blockchainContentDeleted: "Edukia ezabatu egin da", - banking: 'Banking', - bankingTitle: 'Banking', - bankingDescription: 'Aztertu ECOin-en egungo balioa eta dagokion RBU esleipena, astero banatzen dena parte-hartzearen eta konfiantzaren arabera.', + banking: "Bankua", + bankingTitle: "Bankua", + bankingDescription: "ECOin ekonomia kolektiboa.", bankOverview: 'Laburpena', bankEpochs: 'Epeak', bankRules: 'Arauak', pending: 'Zain', closed: 'Itxita', - bankBack: 'Itzuli Banking-era', - bankViewTx: 'Tx ikusi', bankClaimNow: 'Esleitu', - bankClaimUBI: 'RBU eskatu!', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'Ez dago RBU esleipen zain garai honetan.', + bankClaimUBI: "OBU eskatu", + bankClaimAndPay: "Eskatu eta ordaindu", + bankClaimedPending: "Eskaera zain...", + bankStatusClaimed: "Eskatuta", + bankStatusExpired: "Iraungita", + bankPubOnly: "PUBentzako soilik den eragiketa", bankEpoch: 'Epea', bankPool: 'Funtsa (epe honetan)', bankWeightsSum: 'Pisuen batura', - bankAllocations: 'Esleipenak', bankNoAllocations: 'Ez da esleipenik aurkitu.', bankNoEpochs: 'Ez da eperik aurkitu.', bankEpochAllocations: 'Epearen esleipenak', @@ -2429,16 +2417,21 @@ module.exports = { bankEpochId: 'Epearen IDa', bankRuleHash: 'Arauen snapshot hash-a', bankViewEpoch: 'Epea ikusi', + bankYourIndustryBalance: "Zure industria zatia", + bankYourUbiMonth: "Zure OBU (hilabete honetan)", + bankYourFundsMonth: "Funtsak guztira", + bankIndustryBalance: "Industria ekoizpena", bankUserBalance: 'Zure saldoa', - ecoWalletNotConfigured: 'ECOin poltsa ez dago konfiguratuta', - editWallet: 'Poltsa editatu', - addWallet: 'Poltsa gehitu', bankAddresses: 'Helbideak', bankNoAddresses: 'Ez da helbiderik aurkitu.', bankUser: 'Oasis ID', bankAddress: 'Helbidea', bankAddAddressTitle: 'ECOIN helbidea gehitu', bankAddAddressUser: 'Oasis ID', + bankAddAddressLabel: "Etiketa (aukerakoa)", + bankAddAddressLabelPlaceholder: "Boben zorroa, Aliceren denda…", + bankAddAddressUserOptional: "Oasis ID (aukerakoa)", + bankAddressUnnamed: "Izenik gabeko helbidea", bankAddAddressAddress: 'ECOIN helbidea', bankAddAddressSave: 'Gorde', bankAddressAdded: 'Helbidea gehituta', @@ -2456,40 +2449,25 @@ module.exports = { bankAddressDeleteConfirm: 'Helbide hau ezabatu?', search: 'Bilatu!', bankLocal: 'Lokala', - bankFromOasis: 'Oasis', - bankMyAddress: 'Zure helbidea', - bankRemoveMyAddress: 'Nire helbidea kendu', - bankNotRemovableOasis: 'Oasis helbideak ezin dira lokalki kendu', + bankFromOasis: "Sarea", bankingUserEngagementScore: "KARMA puntuazioa", - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "FUNTSIK EZ!", bankUbiAvailableOk: "ESKURAGARRI!", - bankUbiAvailability: "UBI eskuragarritasuna", + bankUbiAvailability: "OBU", bankAlreadyClaimedThisMonth: "Hilabete honetan jada aldarrikatu da", - bankUbiThisMonth: "UBI (hilabete honetan)", - bankUbiLastClaimed: "UBI (azken aldarrikapena)", + bankUbiThisMonth: "OBU (hilabete honetan)", + bankUbiLastClaimed: "OBU (azken aldarrikapena)", bankUbiNeverClaimed: "Inoiz aldarrikatu ez", - bankUbiTotalClaimed: "UBI (guztira aldarrikatua)", + bankUbiTotalClaimed: "OBU (guztira aldarrikatua)", bankUbiPub: "PUB", bankUbiInhabitant: "BIZTANLEA", bankUbiClaimedAmount: "ALDARRIKATUA (ECO)", - typeBankUbiResult: "BANKUA - UBI", + typeBankUbiResult: "BANKUA - OBU", bankNoPubConfigured: "PUBik ez dago konfiguratuta. Ezarri zure PUB IDa Ezarpenetan.", shopsTitle: "Dendak", shopDescription: "Sareko dendak aurkitu eta kudeatu.", shopTitle: "Denda", - shopFilterAll: "GUZTIAK", - shopFilterMine: "NIREAK", - shopFilterRecent: "BERRIAK", - shopFilterTop: "TOP", - shopFilterProducts: "PRODUKTUAK", - shopFilterPrices: "PREZIOAK", - shopFilterFavorites: "GOGOKOAK", shopUpload: "Denda sortu", shopAllSectionTitle: "Denda guztiak", shopMineSectionTitle: "Nire Dendak", @@ -2506,24 +2484,26 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Clearnet-en argitaratu", - shopClearnetUnpublish: "Clearnet-etik kendu", - shopClearnetUrlLabel: "Clearnet URLa", - shopClearnetWarning: "Clearnet-en argitaratzeak denda hau kanpoko bilatzaileek eta crawlerrek indexagarri egiten du.", + transportChipLabel: "Garraioan zifratua", shopAddFavorite: "Gogoko gehitu", shopRemoveFavorite: "Gogoko kendu", shopOpen: "IREKIA", shopClosed: "ITXIA", - shopOpenShop: "Denda ireki", - shopCloseShop: "Denda itxi", shopMakePrivate: "PRIBATU BIHURTU (E2E)", shopMakePublic: "PUBLIKO BIHURTU", shopProducts: "Produktuak", + shopFilterAll: "GUZTIAK", + shopFilterRecent: "AZKENAK", + shopFilterMine: "NIREAK", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUKTUAK", + shopFilterPrices: "PREZIOAK", + shopFilterFavorites: "GOGOKOAK", shopProductAdd: "Produktua gehitu", shopProductTitle: "Produktua", shopProductUpdate: "Produktua eguneratu", shopProductPrice: "Prezioa (ECO)", - shopProductStock: "Stock", + shopProductStock: "Izakinak", shopProductUntitled: "Izengabeko produktua", shopUntitled: "Izengabeko denda", shopNoItems: "Ez da dendarik aurkitu.", @@ -2543,84 +2523,62 @@ module.exports = { shopOrderPrice: "Prezioa", shopOrderBuyer: "Erosle", shopBackToShop: "Dendara itzuli", - shopShareUrl: "Partekatzeko URLa", - shopVisitShop: "DENDA BISITATU", marketVisitItem: "ITEMA BISITATU", shopStatus: "EGOERA", shopCreatedAt: "SORTUA", - shopSearchPlaceholder: "Dendak bilatu...", + shopSearchPlaceholder: "Bilatu hemen: Dendak...", shopUrl: "URL", shopLocation: "Kokapena", shopTags: "Etiketak", shopVisibility: "Ikusgarritasuna", - shopImage: "Irudia", shopShortDescription: "Deskribapen Laburra", shopShortDescriptionPlaceholder: "Deskribapen laburra txarteletarako (gehienez 160 karaktere)", shopTitlePlaceholder: "Zure dendaren izena", shopDescriptionPlaceholder: "Zure dendaren deskribapen zehatza", - shopUrlPlaceholder: "https://zure-denda-url.com", shopLocationPlaceholder: "Hiria, Herrialdea", - shopTagsPlaceholder: "tag1, tag2, tag3", + shopTagsPlaceholder: "Idatzi etiketak komaz bereizita", mapTitlePlaceholder: "Maparen titulua", shopProductFeatured: "Produktu Nabarmendua", - shopSendToMarket: "Send to Market", + shopSendToMarket: "Bidali merkatura", typeShop: "DENDAK", typeShopProduct: "DENDA PRODUKTUA", bankExchange: 'Trukea', bankExchangeCurrentValue: 'ECOin Balioa (1h)', - bankTotalSupply: 'ECOin Hornidura Guztizkoa', + bankTotalSupply: "ECOin hornidura guztizkoa", + bankPubsSupply: "PUBen poltsako ECOin hornidura", + bankHoldingSupply: "UBItik kanpoko ECOin hornidura", bankHoursOfWork: 'lan orduak', bankUnitMs: "ms", bankUnitSeconds: "segundo", bankUnitMinutes: "minutu", bankUnitDays: "egun", bankExchangeNoData: 'Ez dago daturik eskuragarri', - bankExchangeIndex: 'ECOin Balioa (1h)', - bankInflation: 'ECOin Inflazioa (anual)', - bankInflationMonthly: 'ECOin Inflazioa (mensual)', + bankInflation: "ECOin Inflazioa (urtekoa)", + bankInflationMonthly: "ECOin Inflazioa (hilekoa)", bankExchangeChartTitle: 'ECOin balioaren bilakaera denboran zehar', bankExchangeChartValue: 'Balioa (ECO/h)', bankExchangeChartSupply: 'Eskaintza', bankExchangeChartInflation: 'Inflazioa %', bankExchangeChartEmpty: 'Ez dago lagin nahikorik oraindik — itzuli geroago', - bankCurrentSupply: 'ECOin Oraingo Hornidura', + bankCurrentSupply: "Uneko ECOin hornidura", bankingSyncStatus: 'ECOin Egoera', bankingSyncStatusSynced: 'Sinkronizatuta', - bankingSyncStatusOutdated: 'Desegonetik', + bankingSyncStatusOutdated: "Sinkronizatu gabe", statsTitle: 'Estatistikak', statistics: "Estatistikak", - statsInhabitant: "Bizilagunaren estatistikak", statsDescription: "Ezagutu zure sareari buruzko estatistikak.", ALLButton: "GUZTIAK", + RECENTButton: "AZKENAK", MINEButton: "NIREAK", TOMBSTONEButton: "EZABAKETAK", - statsYou: "Zu", - statsUserId: "Oasis ID", statsCreatedAt: "Sortze data", statsYourContent: "Edukia", statsYourOpinions: "Iritziak", - statsYourTombstone: "Ezabaketak", - statsNetwork: "Sarea", - statsTotalInhabitants: "Bizilagunak", - statsDiscoveredTribes: "Tribuak (Publikoak)", - statsPrivateDiscoveredTribes: "Tribuak (Pribatuak)", statsNetworkContent: "Edukia", - statsYourMarket: "Merkatua", - statsYourJob: "Lanak", - statsYourProject: "Proiektuak", - statsYourTransfer: "Transferentziak", - statsYourForum: "Foroak", statsNetworkOpinions: "Iritziak", - statsDiscoveredMarket: "Merkatua", - statsDiscoveredJob: "Lanak", - statsDiscoveredProject: "Proiektuak", - statsBankingTitle: "Bankua", - statsEcoWalletLabel: "ECOIN zorroa", + statsEcoWalletLabel: "ECOin zorroa", + walletAddressCopyHint: "Sakatu hautatzeko, gero kopiatu", statsEcoWalletNotConfigured: "Konfiguratu gabe!", - statsTotalEcoAddresses: "Helbide kopurua", - statsDiscoveredTransfer: "Transferentziak", - statsDiscoveredForum: "Foroak", - statsNetworkTombstone: "Ezabaketak", statsBookmark: "Laster-markak", statsEvent: "Ekitaldiak", statsTask: "Zereginak", @@ -2641,17 +2599,15 @@ module.exports = { statsTransfer: "Transferentziak", statsAiExchange: "IA", statsPUBs: 'PUBack', + statsSyncedPeers: "Parekoak", + statsTotalLabel: "Guztira", statsPost: "Mezuak", - statsOasisID: "Oasis ID", statsSize: "Guztira (tamaina)", statsBlockchainSize: "Blockchain (tamaina)", statsBlobsSize: "Blobak (tamaina)", statsActivity7d: "Jarduera (azken 7 egun)", statsActivity7dTotal: "7 eguneko guztira", statsActivity30dTotal: "30 eguneko guztira", - statsKarmaScore: "KARMA puntuazioa", - statsPublic: "Publikoa", - statsPrivate: "Pribatua", day: "Eguna", messages: "Mezuak", statsProject: "Proiektuak", @@ -2663,30 +2619,14 @@ module.exports = { statsProjectsCancelled: "Bertan behera", statsProjectsGoalTotal: "Helburu osoa", statsProjectsPledgedTotal: "Konprometitutako guztira", - statsProjectsSuccessRate: "Arrakasta tasa", - statsProjectsAvgProgress: "Batez besteko aurrerapena", - statsProjectsMedianProgress: "Aurrerapen mediana", - statsProjectsActiveFundingAvg: "Finantzaketa aktiboaren batez bestekoa", - statsJobsTitle: "Lanak", - statsJobsTotal: "Lan kopurua", - statsJobsOpen: "Irekita", - statsJobsClosed: "Itxita", - statsJobsOpenVacants: "Plaza irekiak", - statsJobsSubscribersTotal: "Harpidedun kopurua", - statsJobsAvgSalary: "Soldata ertaina", - statsJobsMedianSalary: "Soldata mediana", statsMarketTitle: "Merkatua", statsMarketTotal: "Gai kopurua", statsMarketForSale: "Salgai", statsMarketReserved: "Erreserbatuta", statsMarketClosed: "Itxita", statsMarketSold: "Salduta", - statsMarketRevenue: "Diru-sarrerak", - statsMarketAvgSoldPrice: "Salmenta prezio ertaina", statsUsersTitle: "Bizilagunak", user: "Bizilaguna", - statsTombstoneTitle: "Ezabaketak", - statsNetworkTombstones: "Sareko ezabaketak", statsTombstoneRatio: "Ezabaketa ratioa (%)", statsParliamentCandidature: "Parlamenturako hautagaitzak", statsParliamentTerm: "Parlamentuko agintaldiak", @@ -2712,31 +2652,28 @@ module.exports = { aiPrompt: "Eman erantzun informatibo eta zehatza.", aiConfiguration: "Konfiguratu prompt-a", aiPromptUsed: "Prompt-a", - aiClearHistory: "Txataren historia garbitu", - aiSharePrompt: "Erantzun hau entrenamendu kolektibora gehitu?", - aiShareYes: "Bai", - aiShareNo: "Ez", - aiSharedLabel: "Entrenamendura gehituta", - aiRejectedLabel: "Ez da gehitu entrenamendura", - aiServerError: "Ezin izan da IAren erantzuna lortu. Saiatu berriro.", - aiInputPlaceholder: "What is Oasis?", + aiClearHistory: "Garbitu historia", + aiServerError: "Ezin dut ondo erantzun. Saiatu berriro.", + aiStatusLoading: "42 eredua kargatzen ari da. Galdetu berriro une batean.", + aiThinking: "Pentsatzen", + aiWaiting: "Zain", + aiStatusMissingModel: "IA eredua ez dago instalatuta: src/AI/oasis-42-1-chat.Q4_K_M.gguf falta da.", + aiExportFineTuning: "Fine-Tuning esportatu", + aiFactsTitle: "Zure sareko datuak", + aiInputPlaceholder: "Zer da Oasis?", typeAiExchange: "IA", aiApproveTrain: "Prestakuntza kolektibora gehitu", aiRejectTrain: "Ez entrenatu", - aiTrainPending: "Onartzearen zain", aiTrainApproved: "Entrenamendurako onartua", aiTrainRejected: "Entrenamendurako baztertua", aiSnippetsUsed: "Erabilitako zatiak", aiSnippetsLearned: "Ikasitako fragementuak", statsAITraining: "IA prestakuntza", - aiApproveCustomTrain: "Erantzun pertsonalizatu hau erabiliz trebatu", aiCustomAnswerPlaceholder: "Idatzi zure erantzun pertsonalizatua…", - statsAIExchanges: "Ereduen trukea", marketMineSectionTitle: "Zure artikuluak", marketCreateSectionTitle: "Artikulua sortu", marketUpdateSectionTitle: "Eguneratu", marketAllSectionTitle: "Merkatua", - marketRecentSectionTitle: "Azken merkatua", marketTitle: "Merkatua", marketDescription: "Zure sarean ondasunak edo zerbitzuak trukatzeko merkatua.", marketFilterAll: "GUZTIAK", @@ -2749,9 +2686,9 @@ module.exports = { marketFilterForSale: "SALGAI", marketFilterSold: "SALDUTA", marketFilterDiscarded: "BAZTERTUTA", - marketFilterRecent: "AZKENA", + marketFilterRecent: "AZKENAK", marketFilterMyBids: "PUJAK", - marketCreateButton: "Artikulua sortu", + marketCreateButton: "Saldu zerbait", marketItemType: "Mota", marketItemTitle: "Izenburua", marketItemAvailable: "Epemuga", @@ -2762,18 +2699,15 @@ module.exports = { marketItemCondition: "Egoera (kalitatea)", marketItemPrice: "Prezioa", marketItemTags: "Etiketak", - marketItemTagsPlaceholder: "Sartu etiketak komaz bereizita", + marketItemTagsPlaceholder: "Idatzi etiketak komaz bereizita", marketItemDeadline: "Epemuga", marketItemIncludesShipping: "Bidalketa barne?", marketItemHighestBid: "Puja altuena", - marketItemHighestBidder: "Pujatzaile onena", marketItemStock: "Stocka", marketOutOfStock: "Stockik gabe", - marketItemBidTime: "Pujaren data", marketActionsUpdate: "Eguneratu", marketUpdateButton: "Eguneratu", marketActionsDelete: "Ezabatu", - marketActionsSold: "Salduta gisa markatu", marketActionsChangeStatus: "EGOERA ALDATU", marketActionsBuy: "EROSI!", marketAuctionBids: "Oraingo pujаk", @@ -2781,20 +2715,82 @@ module.exports = { marketItemSeller: "Saltzailea", marketNoItems: "Oraindik ez dago artikulurik eskuragarri.", marketYourBid: "Zure puja", - marketCreateFormImageLabel: "Multimedia igo (max: 50MB)", - marketSearchLabel: "Bilatu", - marketSearchPlaceholder: "Izenburuan edo etiketetan bilatu", + marketCreateFormImageLabel: "Multimedia igo (max: 75MB)", + marketSearchPlaceholder: "Bilatu hemen: Merkatua...", marketMinPriceLabel: "Gutxieneko prezioa", marketMaxPriceLabel: "Gehieneko prezioa", - marketSortLabel: "Ordenatu", marketSortRecent: "Berrienak", marketSortPrice: "Prezioa", marketSortDeadline: "Epemuga", marketSearchButton: "Bilatu", marketAuctionEndsIn: "Amaitzen da", marketAuctionEnded: "Amaituta", - marketMyBidBadge: "Puja egin duzu", marketNoItemsMatch: "Ez dago zure bilaketarekin bat datorren artikulurik.", + housingTitle: "Etxebizitza", + housingDescriptionText: "Aurkitu eta kudeatu lekuak zure sarean.", + modulesHousingLabel: "Etxebizitza", + modulesHousingDescription: "Lekuak aurkitzeko eta kudeatzeko modulua.", + typeHousing: "ETXEBIZITZA", + housingFilterAll: "DENAK", + housingFilterMine: "NIREAK", + housingFilterRequested: "ESKATUTAKOAK", + housingFilterSale: "SALMENTA", + housingFilterRent: "ALOKAIRUA", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "ESKURAGARRI", + housingFilterClosed: "EZ ESKURAGARRI", + housingFilterRecent: "BERRIAK", + housingFilterTop: "TOP", + housingType: "Eskaintza", + housingTypeSALE: "Salgai", + housingTypeRENT: "Alokagai", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Higiezina", + housingPropertyAPARTMENT: "Pisua", + housingPropertyHOUSE: "Etxea", + housingPropertyROOM: "Gela", + housingPropertyLAND: "Lursaila", + housingPropertyOTHER: "Bestelakoa", + housingTitleLabel: "Izenburua", + housingTitlePlaceholder: "Lekuaren izena", + housingDescription: "Deskribapena", + housingDescriptionPlaceholder: "Deskribatu lekua, auzoa, zer dagoen barne: komunak, lorategia, oheak…", + housingRules: "Arauak", + housingRulesPlaceholder: "Animaliak, erretzea, isiltasun orduak, gutxieneko egonaldia…", + housingPlace: "Lekua", + housingPlacePlaceholder: "Herrialdea, hiria, helbidea", + housingPrice: "Prezioa", + housingPriceLabel: "Prezioa (ECO)", + housingSizeLabel: "Azalera (m²)", + housingFree: "DOAN", + housingPerMonth: "/hilabete", + housingRooms: "Gelak", + housingSize: "Azalera", + housingCapacity: "Gonbidatuak", + housingAvailableFrom: "Noiztik erabilgarri", + housingAvailableTo: "Noiz arte erabilgarri", + housingTags: "Etiketak", + spreadEditWarning: "Eduki honek spread-ak galduko ditu editatzean", + housingRequests: "Eskaerak", + housingRequestButton: "Eskatu", + housingCancelButton: "Ezeztatu eskaera", + housingRequestedBadge: "ESKATUTA", + housingStatus: "Egoera", + housingStatusOPEN: "ESKURAGARRI", + housingStatusCLOSED: "EZ ESKURAGARRI", + housingSetOpen: "Eskuragarri jarri", + housingSetClosed: "Ez eskuragarri jarri", + housingCreateButton: "Sortu lekua", + housingUpdateButton: "Eguneratu", + housingSearchPlaceholder: "Bilatu hemen: Etxebizitza...", + housingSearchButton: "Bilatu", + housingMinPrice: "Gutxieneko prezioa", + housingMaxPrice: "Gehieneko prezioa", + housingSortRecent: "Berrienak", + housingSortPrice: "Prezio baxuena", + housingSortRequests: "Gehien eskatuak", + housingSortRating: "Ondoen baloratuak", + housingNoItems: "Ez da lekurik aurkitu.", jobsTitle: "Lanak", jobsCandidatesTitle: "Hautagai iradokitakoak", jobsCandidatesDescription: "Lan honetarako trebetasunak dituzten biztanleak. Bidali mezu pribatua gonbidatzeko.", @@ -2817,10 +2813,6 @@ module.exports = { jobsHoursOffered: "Eskainitako orduak", jobsHoursRequested: "Trukean eskatutako orduak", jobsExchangeSkill: "Trukean eskatutako gaitasuna", - jobsHoursOfferedPlaceholder: "ad. 4", - jobsHoursRequestedPlaceholder: "ad. 4", - jobsExchangeSkillPlaceholder: "ad. zurgintza, diseinua", - jobExchangeHint: "Ordu-truke lanetarako, bete beheko eremuak soldataren ordez.", jobsCV: "CV-ak", jobsCreateJob: "Argitaratu Lana", jobsRecentTitle: "Lana Azkenak", @@ -2857,49 +2849,217 @@ module.exports = { jobLocationPresencial: "Aurrez aurrekoa", jobLocationRemote: "Urrunekoa", jobVacantsPlaceholder: "Postu kopurua", - jobSalaryPlaceholder: "Ordubeteko soldata ECO", - jobImage: "Multimedia igo (max: 50MB)", + jobImage: "Multimedia igo (max: 75MB)", jobTasks: "Eginkizunak", jobType: "Lanen Motak", jobTime: "Lanen Denbora", jobSubscribers: "Harpidetzaileak", noSubscribers: "Ez da harpidetzailerik", createJobButton: "Argitaratu Lana", - viewDetailsButton: "Ikusi Xehetasunak", noJobsFound: "Ez daude lan eskaintzak.", jobAuthor: "Egilea", jobTimePartial: "Partziala", jobTimeComplete: "Osotua", - jobsDeleteButton: "EZABATU", jobsUpdateButton: "EGUNERATU", jobsFilterApplied: "ESKATUTA", jobsAppliedTitle: "Nire eskaerak", jobsAppliedBadge: "Izena emanda", - jobsFilterFavs: "Gustukoak", - jobsFavsTitle: "Gustukoak", - jobsFilterNeeds: "Laguntza behar du", - jobsNeedsTitle: "Laguntza behar du", - jobsSearchLabel: "Bilatu", - jobsSearchPlaceholder: "Bilatu izenburua, etiketak, deskribapena...", + jobsSearchPlaceholder: "Bilatu hemen: Lanak...", jobsMinSalaryLabel: "Gutx. soldata", jobsMaxSalaryLabel: "Geh. soldata", - jobsSortLabel: "Ordenatu", jobsSortRecent: "Berrienak", jobsSortSalary: "Soldata altuena", jobsSortSubscribers: "Hautagai gehien", jobsSearchButton: "Bilatu", - jobsFavoriteButton: "Gustukoa", - jobsUnfavoriteButton: "Kendu gustukoa", - jobsMessageAuthorButton: "MP", jobsApplicants: "Hautagaiak", jobsUpdatedAt: "Eguneratua", jobSetOpen: "Ireki gisa markatu", - jobNewBadge: "BERRIA", jobsTagsLabel: "Etiketak", - jobsTagsPlaceholder: "etiketa1, etiketa2, etiketa3", noJobsMatch: "Ez dago bilaketarekin bat datorren lan-eskaintzarik.", + industrySearchPlaceholder: "Bilatu hemen: Industria...", + industryAllSectors: "Sektore guztiak", + industryVisitButton: "Bisitatu industria", + industryAdvanceTo: "Ezarri", + industryAmount: "Kopurua", + industryApproveBuild: "Onartu", + industryBlueprint: "Planoa", + industryBlueprints: "Blueprint-ak", + industryBuild: "Ekoizpena", + industryBuildNotes: "Oharrak", + industryBuildStart: "Hasiera data", + industryVoteUpdateButton: "Bozkatu eguneraketa", + industryVoteDeleteButton: "Bozkatu ezabaketa", + industryRevokeVote: "Errebokatu", + industryTimeLeft: "Geratzen den denbora", + industryBuildEnd: "Amaiera data", + industryBuilds: "Build-ak", + industryBuildTitle: "Izenburua", + industryContribute: "Lagundu", + industryContributeButton: "Ekarpena egin", + industryContributions: "Ekarpenak", + industryCreateBlueprintButton: "Sortu blueprint", + industryDistribute: "Banatu", + industryDistributeButton: "Banatu kuoten arabera", + industryDistribution: "Banaketa", + industryEcoAmount: "Kopurua (ECO)", + industryEditBlueprint: "Editatu blueprint", + industryFacility: "Fabrika", + industryKindLabel: "Mota", + industryKind_labor: "Lana", + industryKind_material: "Materiala", + industryKind_eco: "Funtsak", + industryLaborHours: "Lana (orduak)", + industryHours: "Orduak", + industryMaterialItem: "Materiala", + industryMaterials: "Materialak", + industryEstMaterials: "Materialak guztira", + industryEstLabor: "Lana guztira", + industryEstTotal: "Prezio estimatua", + industryBuildingPrice: "Eraikuntza prezioa", + industryFinalPrice: "Azken prezioa", + industryBlueprintDescPlaceholder: "Zehaztapenak, neurriak, pisua, dokumentazioa…", + industryMaterialValue: "Balioa (ECO)", + industryMember: "Kidea", + industryNewBlueprint: "Blueprint berria", + industryNewBuild: "Proposatu build bat", + industryEditBuild: "Editatu ekoizpena", + industryNeedBlueprint: "Sortu lehenik planoa: ekoizpen bakoitzak bat egiten du.", + industryNoBlueprints: "Oraindik ez dago blueprint-ik.", + industryNoBuilds: "Oraindik ez dago build-ik.", + industryNote: "Oharra", + industryOutput: "Irteera", + industryOutputKind: "Produktu mota", + industryOutputValue: "Irteera balioa (ECO, adib. salmentatik)", + industryPostJob: "Sortu lana", + industryPot: "Poltsa", + industryProposeBuildButton: "Proposatu build", + industryProposer: "Proposatzailea", + industryAuthor: "Egilea", + industrySellOnMarket: "Bidali merkatura", + industrySendToProjects: "Sortu proiektua", + industrySkills: "Trebetasunak", + industryTreasury: "Diruzaintza", + industryKind_physical: "Fisikoa", + industryKind_digital: "Digitala", + industryBuildStatus_PROPOSED: "PROPOSATUA", + industryBuildStatus_REJECTED: "BAZTERTUA", + industryBuildStatus_APPROVED: "ONARTUA", + industryBuildStatus_STOCKING: "HORNITZEN", + industryBuildStatus_IN_PRODUCTION: "EKOIZPENEAN", + industryBuildStatus_COMPLETED: "OSATUA", + industryBuildStatus_FAILED: "HUTS EGINDA", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Lanpostu bat zure curriculumarekin bat dator", + jobsBotMatchIntro: "Lanpostu bat zure curriculumarekin bat dator.", + jobsBotMatchJob: "Lanpostua", + jobsBotMatchHint: "Hau jasotzen duzu zure curriculuma IAk kudeatzen duelako. Zure CVn desaktiba dezakezu.", + cvAiManaged: "IAk kudeatua", + switchOn: "PIZTUTA", + switchOff: "ITZALITA", + cvAiManagedHint: "Aktibo badago, JobsBotek mezu pribatuz abisatuko dizu lanpostu bat zure curriculumarekin bat datorrenean.", + cvMatchThreshold: "Abisatu bat etortze maila honetatik aurrera", + housingBotRequestedTitle: "Norbaitek zure lekuetako bat eskatu du.", + housingBotCancelledTitle: "Zure lekuetako baten eskaera ezeztatu da.", + housingBotYouRequestedTitle: "Leku bat eskatu duzu.", + housingBotYouCancelledTitle: "Eskaera bat ezeztatu duzu.", + housingBotUnavailableTitle: "Eskatu zenuen leku bat ez dago eskuragarri.", + housingBotUnavailableSentTitle: "Leku hau eskatu zutenei jakinarazi diezu.", + industryBotAdmittedTitle: "Fabrika batean onartu zaituzte.", + industryBotApplicationTitle: "Kidetza eskaera berria zure fabrikan.", + industryBotInvitedTitle: "Fabrika batera gonbidatu zaituzte.", + industryBotDissolvedTitle: "Zure fabrika bat desegin da.", + industryBotBuildApprovedTitle: "Build bat onartu da.", + industryBotDistributedTitle: "Build baten irteera banatu da.", + industryFilterRules: "ARAUAK", + industryRulesTitle: "Arauak", + industryRulesIntro: "Industry-k biztanleei ekoizpen bideak modu kolektiboan izatea ahalbidetzen die: Fabrika bat sareko lantegia da, inork pribatuki ez duena.", + industryRulesSteward: "Sortzailea arduraduna da — zaindaria, ez jabea: ezin du Fabrika bakarrik saldu, pribatizatu edo desegin.", + industryRulesMembership: "Kidetza politikak nola sartu erabakitzen du: Irekia (berehala), Botoz (kideen botoak onartuta) edo Gonbidapenez soilik (kide batek lehenik gonbidatu behar zaitu).", + industryRulesQuorum: "Quoruma erabaki batek balio izateko bozkatu behar duten gutxieneko kide kopurua da, Fabrika txiki bat biztanle bakar batek erabaki ez dezan.", + industryRulesMajority: "Gehiengoa onartzeko behar den boto frakzioa da (0,5 = erdia, 1 = aho batez); erabaki bat onartzen da BAI botoek gutxienez max(quoruma, kideak × gehiengoa) lortzen dutenean.", + industryRulesDecisions: "Kideek bozkatzen dute berriak onartzeko, build-ak onartzeko eta Fabrika desegiteko; arduradunak ezin ditu erabaki kolektibo hauek baztertu.", + industryRulesBlueprints: "Blueprint-ak errezeta libreak dira (COPYLEFT) — sarrerak (materialak, lan orduak, trebetasunak) eta irteera (ondasun fisiko edo digital bat) — eta edonork forka ditzake.", + industryRulesBuilds: "Build bat ekoizpen agindua da: PROPOSATUA → ONARTUA (botoz) → HORNITZEN → EKOIZPENEAN → OSATUA, eta onartu ondoren soilik onartzen ditu ekarpenak.", + industryRulesContributions: "Kideek lana (orduak), materialak (adierazitako ECO balioa) edo ECO ematen diote build bati; ekarpen bakoitzak karma irabazten du.", + industryRulesLaborRate: "Lan tarifa Fabrika honetan lan ordu baten ECO balioa da; orduak karma bihurtzen ditu ahalegina eta kapitala modu justuan konparatzeko: karma = orduak × tarifa + material balioa + ECO.", + industryRulesShares: "Build batean zure kuota zure karma ÷ karma guztia da, eta irteera balioaren zenbat jasotzen duzun zehazten du.", + industryRulesDistribution: "Ekoizpen bat amaitzean, stewardak pota (ekoizpenaren funtsak gehi salmenta balioa) banatzen du ekarpena egin dutenen artean, beren partaidetzaren arabera.", + industryRulesTreasury: "ECO ekarpenak arduradunak gordetzen ditu build-aren diruzaintza gisa banaketa arte; mugimendu guztiak sarean erregistratzen dira eta gatazkak Courts-era joan daitezke.", + industryJobs: "Lanpostuak", + industryReportButton: "Salatu", + industryCreateTask: "Sortu zeregina", + industryOpenDispute: "Ireki auzia", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "Sortu gonbidapena", + industryPauseVotes: "Egoera botoak", + industryTitle: "Industria", + industryDescription: "Ekoizpen bideak modu kolektiboan kudeatzeko modulua.", + typeIndustryBuild: "EKOIZPENA", + typeIndustryBlueprint: "PLANOA", + typeIndustryAllocation: "BANAKETA", + typeIndustry: "INDUSTRIA", + modulesIndustryLabel: "Industria", + modulesIndustryDescription: "Ekoizpen bideak modu kolektiboan kudeatzeko modulua.", + industryFilterAll: "GUZTIAK", + industryFilterRecent: "AZKENAK", + industryFilterMember: "KIDEA", + industryFilterBlueprints: "PLANOAK", + industryFilterBuilds: "EKOIZPENAK", + industryFilterMine: "NIREAK", + industryFilterActive: "MARTXAN", + industryFilterPaused: "ETENDA", + industryFilterDissolved: "DESEGINDA", + industryAllTitle: "Industria", + industryRecentTitle: "Azken fabrikak", + industryMemberTitle: "Kide naizen fabrikak", + industryMineTitle: "Nire fabrikak", + industryActiveTitle: "Martxan", + industryPausedTitle: "Etendako fabrikak", + industryDissolvedTitle: "Desegindako fabrikak", + industryNoFacilitiesFound: "Ez da fabrikarik aurkitu.", + industryCreateFacility: "Sortu fabrika", + industryMembers: "Kideak", + industryMemberBadge: "KIDEA", + industryName: "Izena", + industryNamePlaceholder: "Fabrikaren izena", + industryDescriptionLabel: "Deskribapena", + industryDescriptionPlaceholder: "Zer ekoizten du fabrika honek?", + industrySector: "Sektorea", + industryMembershipPolicy: "Kidetza politika", + industryQuorum: "Quoruma", + industryMajority: "Gehiengoa", + industryLaborRate: "Lan tarifa", + industryCreateButton: "Sortu fabrika", + industryUpdateButton: "Eguneratu", + industrySteward: "Arduraduna", + industryStatusActive: "MARTXAN", + industryStatusPaused: "ETENDA", + industryStatusDissolved: "DESEGINDA", + industryStatusLabel: "Egoera", + industryJoinButton: "Elkartu", + industryApplyButton: "Eskatu sartzea", + industryLeaveButton: "Irten", + industryInviteButton: "Gonbidatu", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Gonbidapena behar duzu elkartzeko.", + industryPauseButton: "Bozkatu gelditzea", + industryResumeButton: "Bozkatu berrekitea", + industryDeleteButton: "Ezabatu", + industryPendingApplicants: "Zain dauden eskaerak", + industryVotesYes: "bai", + industryVotesNo: "ez", + industryAlreadyVoted: "bozkatuta", + industryAdmit: "Onartu", + industryReject: "Ukatu", + industryDissolveTitle: "Desegin fabrika", + industryDissolveVote: "Bozkatu desegitea", + industryPolicy_open: "Irekia", + industryPolicy_vote: "Botoz", + industryPolicy_invite: "Gonbidapenez soilik", projectsTitle: "Proiektuak", projectsDescription: "Sortu, finantzatu eta jarraitu komunitateak gidatutako proiektuak zure sarean.", + projectSearchPlaceholder: "Bilatu hemen: Proiektuak...", projectCreateProject: "Proiektu Bat Sortu", projectCreateButton: "Proiektu Bat Sortu", projectUpdateButton: "EGUNERATU", @@ -2922,7 +3082,7 @@ module.exports = { projectRecentTitle: "Proiektu Azkenak", projectTopTitle: "Finantzatuenak", projectTitlePlaceholder: "Proiektuaren izena", - projectImage: "Multimedia igo (max: 50MB)", + projectImage: "Multimedia igo (max: 75MB)", projectDescription: "Deskribapena", projectDescriptionPlaceholder: "Kontatu istorioa eta helburuak...", projectGoal: "Helburu (ECO)", @@ -2943,14 +3103,8 @@ module.exports = { projectPledgeTitle: "Proiektua babestu", projectPledgePlaceholder: "Kantitatea ECOtan", projectBounties: "Saria", - projectBountiesInputLabel: "Sariak (lerro bakoitzean: Izenburua|Kantitatea [ECO]|Deskribapena)", - projectBountiesPlaceholder: "UI akatsa konpondu|100|Arazoaren esteka\nDokumentuak idatzi|250|Erabilera adibideak", projectNoBounties: "Ez da sariarik aurkitu.", projectTitle: "Izenburua", - projectAddBountyTitle: "Saria berri bat gehitu", - projectBountyTitle: "Sariaren izenburua", - projectBountyAmount: "Kantitatea (ECO)", - projectBountyDescription: "Deskribapena", projectMilestoneSelect: "Hitoa aukeratu", projectBountyCreateButton: "Saria sortu", projectBountyStatus: "Sariaren egoera", @@ -2965,19 +3119,15 @@ module.exports = { projectMilestoneTitle: "Hitoaren izenburua", projectMilestoneTargetPercent: "Portzentajea (%)", projectMilestoneDueDate: "Data", - projectMilestoneCreateButton: "Hitoa sortu", projectMilestoneStatus: "Hitoaren egoera", projectMilestoneOpen: "irekia", projectMilestoneDone: "osatua", projectMilestoneDue: "egonkor", - projectNoMilestones: "Ez da hitorik aurkitu.", projectMilestoneMarkDone: "Markatu amaitutako bezala", projectMilestoneTitlePlaceholder: "Hitoaren izenburua sartu", projectMilestoneDescriptionPlaceholder: "Sartu deskribapena", projectMilestoneDescription: "Hitoaren deskribapena", - projectBudgetGoal: "Aurrekontua (Helburua)", projectBudgetAssigned: "Sarietara esleituta", - projectBudgetRemaining: "Geratzen dena", projectBudgetOver: "⚠ Aurrekontua gaindituta: esleitutako zenbatekoak helburua gainditzen du", projectFollowers: "Jarraitzaileak", projectFollowersTitle: "Jarraitzaileak", @@ -2990,7 +3140,6 @@ module.exports = { projectBackersTotalPledged: "Ekarpenen guztizkoa", projectBackersYourPledge: "Zure ekarpena", projectBackersNone: "Oraindik ez dago ekarpenik.", - projectNoRemainingBudget: "Ez da aurrekonturik geratzen.", projectFilterApplied: "ESKATUTA", projectAppliedTitle: "ESKATUTA", projectFilterBackers: "BABESLEAK", @@ -2999,36 +3148,19 @@ module.exports = { projectBackerAmount: "Ekarpen osoa", projectBackerPledges: "Ekarpenak", projectBackerProjects: "Proiektuak", - projectPledgeAmount: "Kantitatea", projectSelectMilestoneOrBounty: "Hegoa edo Saria hautatu", projectPledgeButton: "Ekimen egin", footerLicense: "GPLv3", - footerPackage: "Paketea", - footerVersion: "Bertsioa", modulesModuleName: "Izena", modulesModuleDescription: "Deskribapena", modulesModuleStatus: "Egoera", - modulesTotalModulesLabel: "Kargatutako Moduluak", + modulesTotalModulesLabel: "Guztira", modulesEnabledModulesLabel: "Gaituta", modulesDisabledModulesLabel: "Desgaituta", - modulesPopularLabel: "Nabarmenak", - modulesPopularDescription: "Gehien ikusi edo komentatu diren mezu ezagunak jasotzeko modulua.", - modulesTopicsLabel: "Gaiak", - modulesTopicsDescription: "Interes partekatutako kategorien araberako eztabaidak jasotzeko modulua.", - modulesSummariesLabel: "Laburpenak", - modulesSummariesDescription: "Eztabaida edo mezu luzeen laburpenak jasotzeko modulua.", - modulesLatestLabel: "Azkenak", - modulesLatestDescription: "Mezu eta eztabaida berrienak jasotzeko modulua.", - modulesThreadsLabel: "Hariak", - modulesThreadsDescription: "Gai edo galdera baten inguruko elkarrizketak jasotzeko modulua.", - modulesMultiverseLabel: "Multibertsoa", - modulesMultiverseDescription: "Beste federatutako kideetatik edukia jasotzeko modulua.", modulesInvitesLabel: "Gonbidapenak", modulesInvitesDescription: "Gonbidapen-kodeak kudeatu eta aplikatzeko modulua.", modulesWalletLabel: "Zorroa", modulesWalletDescription: "Zure aktibo digitalak (ECOin) kudeatzeko modulua.", - modulesLegacyLabel: "Gakoak", - modulesLegacyDescription: "Zure sekretua (gako pribatua) modu azkar eta seguruan kudeatzeko modulua.", modulesCipherLabel: "Zifratzailea", modulesCipherDescription: "Zure testua simetrikoki zifratu eta deszifratzeko modulua (pasahitz partekatu baten bidez).", modulesBookmarksLabel: "Markagailuak", @@ -3059,7 +3191,7 @@ module.exports = { modulesVotationsDescription: "Bozketak aurkitu eta kudeatzeko modulua.", modulesParliamentLabel: "Legebiltzarra", modulesParliamentDescription: "Gobernuak hautatu eta legeak bozkatzeko modulua.", - modulesCourtsLabel: "Epaitegiak", + modulesCourtsLabel: "Auzitegiak", modulesCourtsDescription: "Gatazkak konpontzeko eta epaia emateko modulua.", modulesReportsLabel: "Txostenak", modulesReportsDescription: "Arazo, akats, abusu eta eduki-abisuetan erlazionatutako txostenak kudeatzeko modulua.", @@ -3067,8 +3199,12 @@ module.exports = { modulesOpinionsDescription: "Iritziak aurkitu eta bozkatzeko modulua.", modulesTransfersLabel: "Transferentziak", modulesTransfersDescription: "Kontratu adimendunak (transferentziak) aurkitu eta kudeatzeko modulua.", - modulesFediverseLabel: "Fediverse", - modulesFediverseDescription: "Kudeatu zure beste fediverso kontuak, edukia bidaltzea eta jasotzea barne.", + modulesFediverseLabel: "Multibertsoa", + modulesBlogsLabel: "Blogak", + modulesBlogsDescription: "Blogak aurkitzeko eta kudeatzeko modulua.", + modulesPollsLabel: "Inkestak", + modulesPollsDescription: "Sareari galdetzeko eta erantzunak zenbatzeko modulua.", + modulesFediverseDescription: "Kudeatu zure beste multibertso kontuak, edukia bidaltzea eta jasotzea barne.", modulesFeedLabel: "Jarioa", modulesFeedDescription: "Testu laburrak (jarioak) aurkitu eta partekatzeko modulua.", modulesPixeliaLabel: "Pixelia", @@ -3077,7 +3213,7 @@ module.exports = { modulesMelodyDescription: "Zure blockchain-aren \"soinua\" sortzeko eta partekatzeko modulua.", modulesAgendaLabel: "Agenda", modulesAgendaDescription: "Esleitu zaizkizun elementu guztiak kudeatzeko modulua.", - modulesAILabel: "AI", + modulesAILabel: "IA", modulesAIDescription: "'42' izeneko LLM batekin hitz egiteko modulua.", modulesForumLabel: "Foroak", modulesForumDescription: "Foroak deskubritu eta kudeatzeko modulua.", @@ -3086,59 +3222,63 @@ module.exports = { modulesProjectsLabel: "Proiektuak", modulesProjectsDescription: "Proiektuak esploratzeko, finantzatzeko eta kudeatzeko modulu.", modulesBankingLabel: "Bankua", - modulesBankingDescription: "ECOINen benetako balioa zehazteko eta UBI bat altxortegi komuna erabiliz banatzeko modulua.", + modulesBankingDescription: "Altxor komuna kudeatzeko modulua: ECOINen balioa, zergak eta oinarrizko errenta.", modulesFavoritesLabel: "Gogokoak", modulesFavoritesDescription: "Zure gogoko edukia kudeatzeko modulua.", fileTooLargeTitle: "Fitxategia handiegia", - fileTooLargeMessage: "Fitxategiak onartutako gehienezko tamaina gainditzen du (50 MB). Mesedez, hautatu fitxategi txikiago bat.", + fileTooLargeMessage: "Fitxategiak onartutako gehienezko tamaina gainditzen du (75 MB). Mesedez, hautatu fitxategi txikiago bat.", goBack: "Itzuli", + errorDismiss: "Ados", errorPageTitle: "Zerbait gaizki joan da", + errorNoAllocation: "Ez da esleipenik aurkitu erreklamazio honetarako.", + errorInvalidClaim: "Esleipen hau ezin da erreklamatu.", visibilityLabel: "Ikusgaitasuna", + clearnetPrivateConflict: "Eduki pribatua, ezkutua, itxia, gonbidapen bidezkoa edo ordainpekoa ezin da CLEARNETen argitaratu. Aukeratu OASIS edo egin ezazu publiko.", visibilityPublic: "Publikoa", visibilityHidden: "Ezkutua", visibilityMakePublic: "Publiko egin", visibilityMakeHidden: "Ezkutatu", invitesInhabitantsTitle: "Biztanleak", - invitesInhabitantsFollow: "Jarraitu", - aiNavPlaceholder: "Nora joan nahi duzu?", + aiNavPlaceholder: "Deskribatu zer behar duzun...", aiNavDisabled: "AI nabigazioa desgaituta dago.", - aiNavResultsTitle: "AI nabigazio iradokizunak", + aiNavResultsTitle: "AINav iradokizunak", aiNavQueryLabel: "Galdera", - aiNavResultsDescription: "Aukeratu zure bilaketarekin bat datorren ibilbidea.", - aiNavResultPath: "Ibilbidea", - aiNavResultDescription: "Zer egin dezakezu", aiNavResultMatch: "Bat-egitea", aiNavResultsEmpty: "Ez dago bat datorren ibilbiderik. Saiatu /search.", - aiNavSearchInstead: "Bilatu beste modu batez", modulesAINavLabel: "AINav", modulesAINavDescription: "Sareko edukiari buruzko hizkuntza naturalezko galderak egiteko modulua.", - directConnect: "Zuzeneko Konexioa", directConnectDescription: "Konektatu zuzenean parekide batera bere IP helbidea, portua eta gako publikoa sartuz. Parekidea jarraipen-konexio gisa gehituko da.", peerHost: "IP", - peerPort: "Portua (lehenetsia: 8008)", + peerPort: "Ataka", peerPublicKey: "Gako Publikoa (@...ed25519)", connectAndFollow: "Konektatu", - deviceSourceLabel: "Gailua", modulesPresetTitle: "Konfigurazio Arruntak", - modulesPreset_minimal: "Gutxienekoa", - modulesPreset_basic: "Oinarrizkoa", - modulesPreset_social: "Soziala", - modulesPreset_economy: "Ekonomia", - modulesPreset_full: "Osoa", + workflowsTitle: "Workflow-ak", + workflowsDescription: "Workflow batek gaia eta zein modulu dauden aktibo finkatzen ditu.", + workflowsSet: "Workflow-a ezarri", + workflow_default: "Lehenetsia", + workflow_jobs: "Lana", + workflow_social: "Soziala", + workflow_business: "Negozioak", + workflow_night: "Gaua", + workflow_kids: "Haurrak", + workflow_activists: "Aktibismoa", + workflow_mobile: "Mugikorra", statsCarbonFootprintTitle: "Karbono Aztarna", - statsCarbonFootprintNetwork: "Sarearen karbono aztarna", - statsCarbonFootprintYours: "Zure karbono aztarna", statsCarbonTombstone: "Tombstoning-aren aztarna", - feedSuccessMsg: "Feeda arrakastaz argitaratu da!", - dominantOpinionLabel: "Iritzi nagusia", - uploadMedia: "Multimedia igo (max: 50MB)", + uploadMedia: "Multimedia igo (max: 75MB)", reportsWhyInappropriateLabel: "Zergatik da desegokia?", - visitContent: "Edukia bisitatu", - bankEcoinHours: "ECOin Orduak", + visitContent: "Bisitatu Edukia", + favoriteAdd: "Ainguratu Gogokoetan", + pmContentTooltip: "Bidali Mezu Pribatua", + phoneCallTooltip: "Egin Dei bat", + favoriteRemove: "Kendu Gogokoetatik", + reportContent: "Salatu Edukia", + bankEcoinHours: "ECOin Orduak (1 ECO)", mapsLabel: "Mapak", mapTitle: "Mapak", - mapDescription: "Esploratu eta kudeatu lineaz kanpoko mapak zure sarean.", + mapDescription: "Kudeatu eta partekatu lineaz kanpoko mapak zure sarean.", mapMineSectionTitle: "Zure Mapak", mapCreateSectionTitle: "Mapa Sortu", mapUpdateSectionTitle: "Mapa Eguneratu", @@ -3162,14 +3302,13 @@ module.exports = { mapDescriptionLabel: "Deskribapena", mapDescriptionPlaceholder: "Deskribatu mapa edo kokapena...", mapTypeLabel: "Mapa mota", - mapTypeSingle: "BAKARRA (hasierako kokapena soilik)", - mapTypeOpen: "IREKIA (edonork gehitu ditzake markatzaileak)", - mapTypeClosed: "ITXIA (sortzaileak soilik gehitzen ditu markatzaileak)", + mapInviteMode: "Gonbidapena", + mapTypeSingle: "BAKARRA", + mapTypeOpen: "IREKIA", + mapTypeClosed: "ITXIA", mapTagsLabel: "Etiketak", - mapTagsPlaceholder: "Sartu etiketak komaz bereizita", + mapTagsPlaceholder: "Idatzi etiketak komaz bereizita", mapUrlLabel: "Maparen URLa", - mapPickCoordLabel: "Edo hautatu kokapen bat sarean:", - mapMarkersLabel: "markatzaileak", mapMarkersTitle: "Markatzaileak", mapMarkerDefault: "Markatzailea", mapMarkerLatLabel: "Markatzailearen latitudea", @@ -3179,9 +3318,22 @@ module.exports = { markerImageLabel: "Markatzailearen Irudia", mapAddMarkerTitle: "Markatzailea Gehitu", mapAddMarkerButton: "Markatzailea Gehitu", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "Garbitu markatzailea", mapApplyZoom: "Zoom aplikatu", - mapSearchPlaceholder: "Bilatu deskribapena, etiketak, egilea...", + mapPlaceSearchPlaceholder: "Bilatu leku edo herrialde bat...", + mapNoPlaces: "Ez da lekurik aurkitu.", + mapClusterTitle: "markatzaile hemen, sakatu hurbiltzeko", + mapDeleteMarker: "Ezabatu markatzailea", + mapZoomIn: "Hurbildu", + mapZoomOut: "Urrundu", + mapFitMarkers: "Doitu markatzaile guztietara", + mapPanLabel: "Mugitu mapa", + mapCenterHere: "Zentratu hemen", + mapNear: "hemendik gertu:", + mapPickedSpot: "Aukeratutako puntua", + mapPickHint: "Sakatu mapan puntu bat aukeratzeko.", + mapClearPick: "Garbitu", + mapSearchPlaceholder: "Bilatu hemen: Mapak...", mapSearchButton: "Bilatu", mapUpdatedAt: "Eguneratua", mapNoMatch: "Ez da maparik aurkitu zure bilaketarekin.", @@ -3192,118 +3344,820 @@ module.exports = { typeMapMarker: "MAPA MARKATZAILEA", modulesMapLabel: "Mapak", modulesMapDescription: "Lineaz kanpoko mapak kudeatzeko eta partekatzeko modulua.", + emergenciesTitle: "Larrialdiak", + modulesEmergenciesLabel: "Larrialdiak", + modulesEmergenciesDescription: "Sarean larrialdiak jaurti, berretsi eta jarraitzeko modulua.", + emergenciesDescription: "Sarean larrialdiak jaurti, berretsi eta jarraitzeko modulua.", + emergencyCreate: "Larrialdia sortu", + emergencyUpdate: "Eguneratu", + emergencyDelete: "Ezabatu", + emergencyTitleLabel: "Izenburua", + emergencyTitlePlaceholder: "Zer gertatzen ari da?", + emergencyTextLabel: "Xehetasunak", + emergencyTextPlaceholder: "Non, noiztik, zer egin...", + emergencyCategoryLabel: "Kategoria", + emergencyCategoryWeather: "EGURALDIA", + emergencyCategoryInfrastructure: "AZPIEGITURA", + emergencyCategoryHealth: "OSASUNA", + emergencyCategorySecurity: "SEGURTASUNA", + emergencyCategoryLost: "GALDUTAKOAK", + emergencyCategoryNeighborhood: "AUZOA", + emergencyExpiresLabel: "Iraungitzen da", + emergencyExpires1d: "Egun 1", + emergencyExpires3d: "3 egun", + emergencyExpires7d: "Astebete", + emergencyExpires30d: "Hilabete 1", + emergencyExpiresNone: "Zehaztugabea", + emergencyExpiresKeep: "Mantendu uneko data", + emergencyTagsLabel: "Etiketak", + emergencyTagsPlaceholder: "Idatzi etiketak komaz bereizita", + emergencyFilterAll: "GUZTIAK", + emergencyFilterMine: "NIREAK", + emergencyFilterRecent: "AZKENAK", + emergencySeverityUnverified: "EGIAZTATU GABE", + emergencySeverityLow: "BAXUA", + emergencySeverityMedium: "ERTAINA", + emergencySeverityHigh: "ALTUA", + emergencyStatusActive: "AKTIBOA", + emergencyStatusResolved: "KONPONDUA", + emergencyStatusExpired: "IRAUNGITUA", + emergencyConfirmations: "Berrespenak", + emergencyConfirmButton: "Berretsi", + emergencyConfirmed: "Larrialdi hau berretsi duzu", + emergencyResolveButton: "Konpondu", + emergencyReopenButton: "Berriro ireki", + emergencyGenerateReport: "Txostena sortu", + emergencyUpdateEdited: "Editatua", + emergencyLastUpdate: "AZKEN EGUNERAKETA", + emergencyUpdatesTitle: "Azken eguneraketak:", + emergencyNoUpdates: "Oraindik ez dago eguneraketarik.", + emergencyAddUpdate: "Eguneraketa argitaratu", + emergencyUpdatePlaceholder: "Zer aldatu da?", + emergencyNoItems: "Ez da larrialdirik aurkitu.", + emergencyExpiresAt: "Iraungitzen da", + emergencyBannerLabel: "Larrialdi aktiboa", + emergencyCreateSectionTitle: "Larrialdia sortu", + emergencyUpdateSectionTitle: "Larrialdia eguneratu", + emergencySearchPlaceholder: "Bilatu larrialdietan...", + typeEmergency: "LARRIALDIAK", + typeEmergencyConfirm: "LARRIALDI BERRESPENA", + typeEmergencyUpdate: "LARRIALDI EGUNERAKETA", + statsEmergency: "Larrialdiak", + statsEmergencyConfirm: "Larrialdi berrespenak", + statsEmergencyUpdate: "Larrialdi eguneraketak", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "Jarraitzen duzun larrialdi batek eguneraketa bat du.", + emergencyBotResolvedTitle: "Jarraitzen duzun larrialdi bat konpondu da.", + dataKindEmergencies: "LARRIALDIAK", + mailingTitle: "Posta-zerrendak", + modulesMailingLabel: "Posta-zerrendak", + modulesMailingDescription: "Aurkitu eta kudeatu posta-zerrendak zure sarean.", + modulesPhoneLabel: "Telefonoa", + modulesPhoneDescription: "Beste biztanleei muturretik muturrera zifratutako bideodeiak egiteko modulua.", + mailingDescription: "Aurkitu eta kudeatu posta-zerrendak zure sarean.", + mailingCreate: "Posta-zerrenda sortu", + mailingUpdate: "Eguneratu", + mailingArchive: "Artxibatu", + mailingActivate: "Aktibatu", + mailingCreateSectionTitle: "Posta-zerrenda sortu", + mailingUpdateSectionTitle: "Posta-zerrenda eguneratu", + mailingTitleLabel: "Izenburua", + mailingTitlePlaceholder: "Posta-zerrendaren izena", + mailingDescriptionLabel: "Deskribapena", + mailingDescriptionPlaceholder: "Zeri buruz da zerrenda hau?", + mailingTypeLabel: "Posta-zerrenda mota", + mailingTypeOpen: "IREKIA", + mailingTypeClosed: "ITXIA", + mailingStatusLabel: "Egoera", + mailingStatusActive: "AKTIBOA", + mailingStatusArchived: "ARTXIBATUA", + mailingMembersLabel: "Biztanleak gonbidatu", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Harpidedunak", + mailingTagsLabel: "Etiketak", + mailingTagsPlaceholder: "Idatzi etiketak komaz bereizita", + mailingFilterAll: "GUZTIAK", + mailingFilterMine: "NIREAK", + mailingFilterSubscribed: "HARPIDETUAK", + mailingFilterRecent: "AZKENAK", + mailingHistoryDate: "DATA", + mailingHistoryThreads: "HARIAK", + mailingHistoryInhabitants: "BIZTANLEAK", + mailingHistorySearchPlaceholder: "Bilatu historian...", + mailingNoMessages: "Oraindik ez dago mezurik.", + mailingNoItems: "Ez da posta-zerrendarik aurkitu.", + mailingSearchPlaceholder: "Bilatu posta-zerrendetan...", + mailingWriteButton: "Mezua idatzi", + mailingWriteTitle: "Zerrendari idatzi", + mailingReplyTitle: "Zerrendari erantzun", + mailingSend: "Bidali", + mailingTextPlaceholder: "Zure mezua zerrendako partaide guztiei iristen zaie.", + mailingParticipants: "Partaideak", + inboxFilterMailing: "Posta-zerrendak", + mailingReplyPlaceholder: "Zerrendari erantzun...", + mailingReplyToList: "Zerrendari erantzun", + typeMailingList: "POSTA-ZERRENDA", + statsMailingList: "Posta-zerrendak", + mailingListLabel: "POSTA-ZERRENDA", + logisticsTitle: "Logistika", + modulesLogisticsLabel: "Logistika", + modulesLogisticsDescription: "Aurkitu eta kudeatu plan logistikoak zure sarean.", + logisticsDescription: "Aurkitu eta kudeatu plan logistikoak zure sarean.", + logisticsCreate: "Ibilbidea sortu", + logisticsUpdate: "Eguneratu", + logisticsClose: "Itxi", + logisticsReopen: "Berriro ireki", + logisticsCreateSectionTitle: "Ibilbidea sortu", + logisticsUpdateSectionTitle: "Ibilbidea eguneratu", + logisticsTitleLabel: "Izenburua", + logisticsTitlePlaceholder: "Bidaia merkatura, furgoneta eskuragarri...", + logisticsKindLabel: "Logistika mota", + logisticsKindTrip: "BIDAIA", + logisticsKindShipment: "BIDALKETA", + logisticsKindTrips: "BIDAIAK", + logisticsKindShipments: "BIDALKETAK", + logisticsModeLabel: "Modua", + logisticsModeOffer: "ESKAINTZEN DUT", + logisticsModeRequest: "BILA NABIL", + logisticsModeOffers: "ESKAINTZAK", + logisticsModeRequests: "ESKAERAK", + logisticsDescriptionLabel: "Deskribapena", + logisticsDescriptionPlaceholder: "Elkargunea, zer daramazun, baldintzak...", + logisticsOriginLabel: "Jatorria", + logisticsOriginPlaceholder: "Non hasten den", + logisticsDestinationLabel: "Helmuga", + logisticsDestinationPlaceholder: "Non amaitzen den", + logisticsDateLabel: "Data eta ordua", + logisticsRecurrenceLabel: "Errepikapena", + logisticsFrequencyLabel: "Maiztasuna", + logisticsRecurrenceNone: "BAKARRA", + logisticsRecurrenceDaily: "EGUNEROKOA", + logisticsRecurrenceWeekly: "ASTEKOA", + logisticsRecurrenceMonthly: "HILEKOA", + logisticsSeatsLabel: "Plazak", + logisticsSizeLabel: "Tamaina", + logisticsSizePlaceholder: "adib. 2m x 1m", + logisticsWeightLabel: "Pisua", + logisticsWeightPlaceholder: "adib. 40kg", + logisticsPriceFree: "DOAN", + logisticsPriceEco: "ECO", + logisticsPriceTime: "DENBORA", + logisticsPriceLabel: "Prezioa", + logisticsHours: "h", + logisticsOrderRefLabel: "Eskaeraren esteka", + logisticsOrderRefPlaceholder: "entregatu beharreko eskaeraren /shops/... edo /market/...", + logisticsOrderLink: "Lotutako eskaera", + logisticsTagsLabel: "Etiketak", + logisticsTagsPlaceholder: "Idatzi etiketak komaz bereizita", + logisticsFilterAll: "GUZTIAK", + logisticsFilterMine: "NIREAK", + logisticsFilterBooked: "ERRESERBATUAK", + logisticsFilterRecent: "AZKENAK", + logisticsFilterHistory: "HISTORIA", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "IREKIA", + logisticsStatusUpcoming: "HURRENGOAK", + logisticsStatusPast: "IGAROTAKOAK", + logisticsStatusClosed: "ITXIAK", + logisticsZoneLabel: "Eremua", + logisticsSearchPlaceholder: "Bilatu logistikan...", + logisticsNoItems: "Ez da ibilbiderik aurkitu.", + logisticsBookingPending: "ZAIN", + logisticsBookingConfirmed: "BAIEZTATUA", + logisticsBookingRejected: "BAZTERTUA", + logisticsBookingCancelled: "BERTAN BEHERA", + logisticsBookingDelivered: "ENTREGATUA", + logisticsConfirm: "Baieztatu", + logisticsReject: "Baztertu", + logisticsDeliver: "Entregatu", + logisticsReceiptLabel: "Ordainagiria", + logisticsReceiptPlaceholder: "Entrega-oharra (aukerakoa)", + logisticsCancelBooking: "Erreserba bertan behera utzi", + logisticsBookTitle: "Ibilbide hau erreserbatu", + logisticsNotesLabel: "Oharrak", + logisticsNotesPlaceholder: "Arduradunak jakin beharrekoa", + logisticsBook: "Erreserbatu", + logisticsBookingsTitle: "Erreserbak", + logisticsMyBookingTitle: "Zure erreserba", + logisticsRatingsTitle: "Balorazioak", + logisticsNoRatings: "Oraindik ez dago baloraziorik.", + logisticsRatingsWhenClosed: "Balorazioak ibilbidea ixtean irekitzen dira.", + logisticsScoreLabel: "Puntuazioa", + logisticsRatingPlaceholder: "Nola joan da?", + logisticsRate: "Baloratu", + typeLogisticsRoute: "IBILBIDEA", + typeLogisticsRating: "IBILBIDEAREN BALORAZIOA", + statsLogisticsRoute: "Ibilbideak", + statsLogisticsRating: "Ibilbideen balorazioak", + logisticsRouteLabel: "IBILBIDEA", + podcastsTitle: "Podcastak", + modulesPodcastsLabel: "Podcastak", + modulesPodcastsDescription: "Podcast kanalak argitaratzeko modulua.", + podcastsDescription: "Podcast kanalak argitaratzeko modulua.", + podcastCreate: "Podcasta sortu", + podcastUpdate: "Eguneratu", + podcastDelete: "Ezabatu", + podcastCreateSectionTitle: "Podcasta sortu", + podcastUpdateSectionTitle: "Podcasta eguneratu", + podcastTitleLabel: "Izenburua", + podcastTitlePlaceholder: "Kanalaren izena", + podcastDescriptionLabel: "Deskribapena", + podcastDescriptionPlaceholder: "Zeri buruz da podcast hau?", + podcastCategoryLabel: "Kategoria", + podcastCategoryNews: "ALBISTEAK", + podcastCategoryMusic: "MUSIKA", + podcastCategoryTalk: "SOLASALDIA", + podcastCategoryEducation: "HEZKUNTZA", + podcastCategoryTech: "TEKNOLOGIA", + podcastCategoryCulture: "KULTURA", + podcastCategoryCommunity: "KOMUNITATEA", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "BESTELAKOAK", + podcastTagsLabel: "Etiketak", + podcastCoverLabel: "Azala (irudia edo bideoa)", + podcastEpisodeAudioLabel: "Audio edo bideo fitxategia (gehienez: 75MB)", + podcastTagsPlaceholder: "Idatzi etiketak komaz bereizita", + podcastFilterAll: "GUZTIAK", + podcastFilterMine: "NIREAK", + podcastFilterRecent: "AZKENAK", + podcastFilterTop: "TOP", + podcastFilterViewers: "ENTZULEAK", + podcastSearchPlaceholder: "Bilatu podcastetan...", + podcastNoItems: "Ez da podcastik aurkitu.", + podcastEpisodesLabel: "Atalak", + podcastPlaysLabel: "Ikustaldiak", + podcastLastEpisode: "Azken atala", + podcastEpisodesTitle: "Atalak", + podcastNoEpisodes: "Oraindik ez dago atalik.", + podcastEpisodeAdd: "Atala gehitu", + podcastEpisodeCreateTitle: "Atala argitaratu", + podcastEpisodeUpdateTitle: "Atala eguneratu", + podcastEpisodeTitleLabel: "Atalaren izenburua", + podcastEpisodeTitlePlaceholder: "Atalaren izenburua", + podcastEpisodeDescriptionLabel: "Oharrak", + podcastEpisodeDescriptionPlaceholder: "Zer gertatzen den atal honetan, estekak, kredituak...", + podcastEpisodePublish: "Argitaratu", + podcastPlayed: "Entzunda", + podcastMarkPlayed: "Entzundako gisa markatu", + typePodcast: "PODCASTA", + typePodcastEpisode: "ATALA", + typePodcastOpinion: "ATALAREN IRITZIA", + typePodcastPlay: "ERREPRODUKZIOA", + statsPodcast: "Podcastak", + statsPodcastEpisode: "Atalak", + statsPodcastPlay: "Erreprodukzioak", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "Jarraitzen duzun blog batek sarrera berria du.", + podcastBotEpisodeTitle: "Jarraitzen duzun podcast batek atal berria du.", + podcastLabel: "PODCASTA", + podcastEpisodeLabel: "ATALA", + campaignsTitle: "Kanpainak", + modulesCampaignsLabel: "Kanpainak", + modulesCampaignsDescription: "Aurkitu eta kudeatu kanpainak zure sarean.", + campaignsDescription: "Aurkitu eta kudeatu kanpainak zure sarean.", + campaignCreate: "Kanpaina sortu", + campaignUpdate: "Eguneratu", + campaignDelete: "Ezabatu", + campaignClose: "Itxi", + campaignReopen: "Berriro ireki", + campaignCreateSectionTitle: "Kanpaina sortu", + campaignUpdateSectionTitle: "Kanpaina eguneratu", + campaignTitleLabel: "Izenburua", + campaignTitlePlaceholder: "Zer lortu nahi duzu?", + campaignTextLabel: "Testua", + campaignTextPlaceholder: "Azaldu kausa, nork erabakitzen duen eta zer eskatzen duzun...", + campaignCategoryLabel: "Kategoria", + campaignCategoryEnvironment: "INGURUMENA", + campaignCategoryRights: "ESKUBIDEAK", + campaignCategoryHealth: "OSASUNA", + campaignCategoryEducation: "HEZKUNTZA", + campaignCategoryInfrastructure: "AZPIEGITURA", + campaignCategoryCulture: "KULTURA", + campaignCategoryEconomy: "EKONOMIA", + campaignCategoryOther: "BESTELAKOAK", + campaignGoalLabel: "Sinadura helburua", + campaignDeadlineLabel: "Epea", + campaignTagsLabel: "Etiketak", + campaignTagsPlaceholder: "Idatzi etiketak komaz bereizita", + campaignFilterAll: "GUZTIAK", + campaignFilterMine: "NIREAK", + campaignFilterSigned: "SINATUAK", + campaignFilterRecent: "AZKENAK", + campaignFilterTop: "TOP", + campaignStatusOpen: "IREKIA", + campaignStatusAchieved: "LORTUA", + campaignStatusUnachieved: "LORTU GABE", + campaignStatusClosed: "ITXIA", + campaignSearchPlaceholder: "Bilatu kanpainetan...", + campaignNoItems: "Ez da kanpainarik aurkitu.", + campaignSignaturesLabel: "Sinadurak", + campaignSignaturesTitle: "Babesleak", + campaignNoSignatures: "Oraindik ez dago sinadurarik.", + campaignSignTitle: "Kanpaina hau sinatu", + campaignSignPlaceholder: "Zergatik babesten duzu? (aukerakoa)", + campaignSign: "Sinatu", + campaignSigned: "Dagoeneko sinatu duzu!", + campaignNotSigned: "Zure sinadura falta da oraindik", + campaignElevate: "Parlamentura eraman", + campaignElevated: "Parlamentura eramana", + campaignUpdatesTitle: "Eguneraketak", + campaignNoUpdates: "Oraindik ez dago eguneraketarik.", + campaignUpdatePlaceholder: "Zer gertatu da?", + campaignAddUpdate: "Eguneraketa argitaratu", + campaignQrAlt: "Kanpaina hau sinatzeko QRa", + campaignQrCaption: "Eskaneatu sinatzeko", + typeCampaign: "KANPAINA", + typeCampaignSignature: "SINADURA", + typeCampaignUpdate: "KANPAINAREN EGUNERAKETA", + statsCampaign: "Kanpainak", + statsCampaignSignature: "Kanpainen sinadurak", + statsCampaignUpdate: "Kanpainen eguneraketak", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "Jarraitzen duzun kanpaina batek eguneraketa bat du.", + campaignBotAchievedTitle: "Jarraitzen duzun kanpaina batek helburua lortu du.", + campaignBotRaisedTitle: "Sinatu duzun kanpaina bat Parlamentura eraman da.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Erreserbatu duzun ibilbide bat aldatu da.", + logisticsBotClosedTitle: "Erreserbatu duzun ibilbide bat itxi da.", + logisticsBotBookedTitle: "Norbaitek zure ibilbidea erreserbatu du.", + logisticsBotCancelledTitle: "Zure ibilbideko erreserba bat bertan behera utzi da.", + logisticsBotConfirmedTitle: "Zure erreserba berretsi da: paketea bidean da.", + logisticsBotRejectedTitle: "Zure erreserba baztertu da.", + logisticsBotDeliveredTitle: "Zure paketea iritsi da.", + campaignLabel: "KANPAINA", + campaignGenerateReport: "Txostena sortu", + logisticsGenerateReport: "Txostena sortu", + mailingGenerateReport: "Txostena sortu", + campaignUpdateEdited: "Editatua", + mailingMessagesLabel: "Mezuak", + logisticsBookingsLabel: "Erreserbak", + backupTitle: "Segurtasun-kopia", + modulesBackupLabel: "Segurtasun-kopia", + modulesBackupDescription: "Zure gakoak esportatu eta inportatzeko eta segurtasun-kopia zifratuak egiteko modulua.", + backupDescription: "Zure gakoak esportatu eta inportatzeko eta segurtasun-kopia zifratuak egiteko modulua.", + backupTypeRecovery: "BERRESKURATZEA", + backupTypeKeys: "GAKOAK ESPORTATU", + backupTypeFull: "KOPIA OSOA", + backupTypeRestore: "KOPIA LEHENERATU", + backupTypeInstant: "BAT-BATEAN", + backupInstantDescription: "Zure erregistro eta fitxategi guztien .zip soil bat, gako sekreturik gabe eta pasahitzik gabe. Azkarra, baina ", + backupInstantRisk: "fitxategia duenak dena irakurtzen du.", + backupKitPageDescription: "Berreskuratze-kita gailu honetatik bakarrik ireki daiteke.", + backupKitId: "Oasis IDa", + backupKitDate: "Sortua", + backupKitQrAlt: "Zure gako sekretuaren QR kodea", + backupKitStep1: "Inprimatu edo eskuz kopiatu eta gorde leku seguru batean, konexiorik gabe.", + backupKitStep2: "Berreskuratzeko, instalatu Oasis gailu berri batean eta inportatu sekretu hau Segurtasun-kopia › Gakoak ataletik.", + backupKitStep3: "Suntsitu orri hau inoiz agerian geratzen bada.", + backupKitPdfButton: "PDFa sortu", + backupScopeLabel: "Irismena", + backupScopeAll: "DENA", + backupScopeMine: "NIRE EDUKIA BAKARRIK", + backupSinceLabel: "Noiztik", + backupEstimateTitle: "Estimazioa", + backupFileInfo: "Zure kopia zifratua gailuan deskargatuko da (izena: oasis-backup-DATA.oasisbk)", + backupExportButton: "Kopia deskargatu", + backupRestorePassword: "Idatzi kopia egitean erabilitako pasahitza", + backupRestoreButton: "Kopia berreskuratu", + backupRestoredLine: "Kopia berreskuratuta.", + backupRestoredMessages: "Gehitutako mezuak", + backupRestoredSkipped: "Lehendik zeudenak", + backupRestoredBlobs: "Gehitutako fitxategiak", + backupRestoredState: "Ezarpenak berrezarrita", + backupRestoredFailed: "Huts egindakoak", + backupRestoredForked: "Dibergenteak", + backupRestoreRunning: "Kopia leheneratzen. Orri hau bere kabuz eguneratzen da; bitartean Oasis erabiltzen jarrai dezakezu.", + backupRestoreElapsed: "Igarotako denbora", + backupRestoreFailedLine: "Ezin izan da kopia leheneratu.", + backupRestoreForkMine: "Gailu honek dagoeneko zure identitatearekin kopian ez dauden mezuak argitaratu ditu, beraz ezin izan da haien gainean aplikatu. Leheneratu instalazio garbi batean, giltzak inportatu eta berehala, beste ezer egin aurretik.", + verificationTitle: "Egiaztapena", + verificationDescription: "Egiaztatu zure jarioaren katea, hauteman adarkatzeak eta berrikusi fitxategi umezurtzak edo falta direnak.", + verificationRun: "Egiaztapena exekutatu", + verificationTotalMessages: "mezu erregistroan", + verificationFeed: "Zure jarioa", + verificationMessages: "mezu", + verificationLastSequence: "azken sekuentzia", + verificationGaps: "Hutsuneak", + verificationBrokenLinks: "Lotura hautsiak", + verificationBadHashes: "Hash okerrak", + verificationBadSignatures: "Sinadura okerrak", + verificationDuplicates: "Sekuentzia bikoiztuak", + verificationForks: "Adarkatzeak", + verificationBlobs: "Fitxategiak", + verificationBlobsPresent: "daude", + verificationBlobsReferenced: "erreferentziatuak", + verificationBlobsOrphan: "umezurtzak", + verificationBlobsMissing: "falta dira", + verificationBlobsMissingOwn: "Gailu honetan falta diren zure fitxategiak", + verificationBlobsMissingOthers: "Beste biztanleen fitxategiak oraindik deskargatu gabe", + verificationBlobsUnavailable: "Ezin izan dira fitxategiak zerrendatu gailu honetan.", + wikiTitle: "Wikiak", + modulesWikiLabel: "Wikiak", + modulesWikiDescription: "Wikiak aurkitu eta kudeatzeko modulua.", + wikiDescription: "Wikiak aurkitu eta kudeatzeko modulua.", + wikiCreate: "Wikia sortu", + wikiUpdate: "Eguneratu", + wikiDelete: "Ezabatu", + wikiEdit: "Editatu", + wikiTitleLabel: "Izenburua", + wikiTitlePlaceholder: "Wikiaren izenburua...", + wikiBodyLabel: "Edukia", + wikiBodyPlaceholder: "Lotu beste wiki batzuk [[Wikiaren izenburua]] edo [[Wikiaren izenburua|testu propioa]] erabiliz.\n\nAipatu bizilagun bat @id bidez.\n\nEtiketatu #oasis erabiliz.\n\nItsatsi estekak, adibidez https://solarnethub.com\n\nLerro huts batek paragrafo berria hasten du; lerro-jauzi bakunak mantentzen dira.\n\nErantsi onartutako edozein media eta hemen txertatuko da.", + formatBarLabel: "Formatua", + wikiStatusLabel: "Wiki mota", + wikiStatusOpen: "IREKIA", + wikiStatusClosed: "ITXIA", + wikiTagsLabel: "Etiketak", + wikiTagsPlaceholder: "Idatzi etiketak komaz bereizita", + wikiSummaryLabel: "Aldaketaren laburpena", + wikiSummaryPlaceholder: "Zer aldatu da?", + wikiFilterAll: "GUZTIAK", + wikiFilterMine: "NIREAK", + wikiFilterRecent: "AZKENAK", + wikiFilterLinked: "LOTUTAK", + wikiChanges: "Azken aldaketak", + wikiNoPages: "Ez da wikirik aurkitu.", + wikiNoChanges: "Oraindik ez dago aldaketarik.", + wikiHistory: "Historia", + wikiVersion: "Bertsioa", + wikiVersions: "bertsio", + wikiRestore: "Berreskuratu", + wikiDiff: "Aldaketak", + wikiViewVersion: "Ikusi", + wikiLinkedPages: "Lotutako orriak", + wikiNoLinkedPages: "Orri hau ez dago oraindik beste inorekin lotuta.", + wikiMissingLink: "Orria ez da existitzen oraindik", + wikiDuplicateTitle: "Izenburu hori duen orri bat badago dagoeneko. Aldatu izenburua beste orri bat sortzeko.", + wikiCreateSectionTitle: "Wikia sortu", + wikiUpdateSectionTitle: "Wikia eguneratu", + wikiSearchPlaceholder: "Bilatu Wikian...", + wikiCurrentVersion: "Uneko bertsioa", + wikiViewingVersion: "Bertsio zahar bat ikusten ari zara", + wikiBackToPage: "Itzuli orrira", + wikiLinkedChip: "Lotuta", + wikiRestoredSummary: "Berreskuratutako bertsioa", + wikiExportPdf: "Orria sortu", + wikiPreview: "Aurrebista", + tribeSectionWiki: "WIKIA", + tribeWikiCreate: "Wikia sortu", + tribeWikiEmpty: "Oraindik ez dago wikirik.", + typeWiki: "WIKIA", padsTitle: "Padak", padTitle: "Pad", modulesPadsLabel: "Padak", - modulesPadsDescription: "Testu editoreak lankidetzan kudeatzeko modulua.", - padFilterAll: "GUZTIAK", - padFilterMine: "NIREA", - padFilterRecent: "BERRIA", - padFilterOpen: "IREKIA", - padFilterClosed: "ITXIA", + modulesPadsDescription: "Testu-editore kolaboratiboak kudeatzeko modulua.", padCreate: "Pad Sortu", - padUpdate: "Pad Eguneratu", - padDelete: "Pad Ezabatu", + padUpdate: "Eguneratu", + padDelete: "Ezabatu", padTitleLabel: "Izenburua", padTitlePlaceholder: "Idatzi pad-aren izenburua...", padStatusLabel: "Egoera", + padTypeLabel: "Pad mota", padStatusOpen: "IREKIA", padStatusInviteOnly: "GONBIDATUAK SOILIK", padStatusClosed: "ITXIA", padDeadlineLabel: "Epemuga", padTagsLabel: "Etiketak", - padTagsPlaceholder: "etiketa1, etiketa2, ...", + padTagsPlaceholder: "Idatzi etiketak komaz bereizita", padMembersLabel: "Kideak", - padVisitPad: "Pad Bisitatu", - padShareUrl: "URL Partekatu", padCreated: "Sortua", - padAuthor: "Egilea", - padGenerateCode: "Kodea Sortu", - padInviteCodeLabel: "Gonbidapen Kodea", - padInviteCodePlaceholder: "Sartu gonbidapen kodea...", - padValidateInvite: "Balioztatu", - padStartEditing: "EDITZEN HASI!", + padStartEditing: "Editatu", padEditorPlaceholder: "Idazten hasi...", padSubmitEntry: "Bidali", padNoEntries: "Oraindik sarrerarik ez.", padAllSectionTitle: "Pad Guztiak", padMineSectionTitle: "Nire Padak", padsDescription: "Kudeatu zure sareko enkriptatutako testu-editore kolaboratiboak.", + roomsTitle: "Gelak", + roomsDescription: "Aurkitu zure sareko bideokonferentzia-gela zifratuak eta sartu haietan.", + modulesRoomsLabel: "Gelak", + modulesRoomsDescription: "Bideokonferentzia-gela zifratuak aurkitu eta haietan sartzeko modulua.", + roomFilterRecent: "AZKENAK", + roomFilterMine: "NIREAK", + roomFilterAll: "GUZTIAK", + roomFilterLive: "ZUZENEAN", + roomFilterOpen: "IREKIAK", + roomFilterInvite: "GONBIDATUAK SOILIK", + roomFilterClosed: "ITXIAK", + roomCreate: "Sortu gela", + roomUpdate: "Eguneratu", + roomDelete: "Ezabatu", + roomClose: "Itxi", + roomReopen: "Berriro ireki", + roomCreateSectionTitle: "Sortu gela", + roomUpdateSectionTitle: "Eguneratu gela", + roomTitlePlaceholder: "Gelaren izenburua", + roomDescriptionLabel: "Deskribapena", + roomDescriptionPlaceholder: "Zertaz hitz egingo duzue?", + roomTypeLabel: "Gela mota", + roomTagsLabel: "Etiketak", + roomStatusOpen: "IREKIA", + roomStatusInviteOnly: "GONBIDATUAK SOILIK", + roomStatusClosed: "ITXIA", + roomParticipants: "Parte-hartzaileak", + roomJoin: "Sartu", + roomLeave: "Irten", + roomFull: "Gela beteta", + roomLiveChip: "ZUZENEAN", + roomMuted: "isilduta", + roomRec: "Audioa grabatu", + roomRecStop: "Grabazioa gelditu", + roomStreamFile: "Igorri", + roomStreamStop: "Gelditu", + roomStreaming: "Igortzen", + liveFullscreen: "Pantaila osoa", + roomStreamPickTitle: "Aukeratu bideo bat", + roomStreamPickHint: "Gailu honetako bideoak. Aukeratzen duzuna dagoen tokitik igortzen da, kopiatu edo argitaratu gabe.", + roomStreamGo: "Gelan igorri", + roomStreamNotFound: "Bideo hori ez dago zure karpetetan.", + roomPublicWarning: "Gela hau web publikoan igortzen da: edonork ikus eta entzun dezake bertan gertatzen dena.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} gelan sartu da", + roomEventLeft: "{name} gelatik irten da", + roomEventRecStart: "{name} grabatzen hasi da", + roomEventRecStop: "{name} grabatzeari utzi dio", + roomEventMuted: "{name} isilarazi da", + roomEventUnmuted: "{name}(e)k isilunea kendu du", + roomEventHand: "{name}(e)k eskua altxatu du", + roomRaiseHand: "Eskua altxatu", + roomLowerHand: "Eskua jaitsi", + confirmActionTitle: "Berretsi ekintza hau", + confirmActionButton: "Berretsi", + confirmJoinText: "Gonbidapen honekin batzera zoaz.", + confirmFollowText: "Bizilagun hau babestera zoaz.", + confirmSignText: "Kanpaina hau sinatzera zoaz.", + confirmDownloadText: "Fitxategi hau zure gailura deskargatzera zoaz.", + roomRecordings: "Grabazioak", + roomClearNotices: "Garbitu", + roomLineSwitchboard: "Zentralita", + roomLineDndError: "Gela honen zenbakiak ez du deirik onartzen.", + roomNumberHint: "Markatu zenbaki hau Telefonotik gelan sartzeko.", + liveRefresh: "Berritu", + roomNobody: "Une honetan ez dago inor gelan.", + roomSomeone: "{n} gelan une honetan.", + roomYou: "zu", + roomYouAreIn: "Barruan zaude", + roomNobodyInside: "Une honetan ez dago inor gela honetan.", + roomErrorFull: "Gela beteta dago. Norbait irteten denean sartu ahal izango zara.", + roomErrorUnreachable: "Une honetan ezin da gelara iritsi. Saiatu berriro geroago.", + roomErrorBusy: "Dagoeneko dei batean edo beste gela batean zaude.", + roomErrorClosed: "Gela hau itxita dago.", + roomAccessDenied: "Gela hau gonbidatuentzat soilik da. Gonbidapen bat behar duzu sartzeko.", + roomsNoItems: "Ez da gelarik aurkitu.", + roomSearchPlaceholder: "Bilatu gelak...", + roomInviteMode: "Gonbidapena", + tribeSectionRooms: "GELAK", + tribeRoomCreate: "Sortu gela", + tribeRoomsEmpty: "Oraindik ez dago gelarik.", + typeRoom: "GELAK", + typeRoomMember: "GELAKO PARTE-HARTZAILEA", + statsRoom: "Gelak", + statsRoomMember: "Geletako parte-hartzaileak", + favoritesFilterRooms: "GELAK", + invitesRoomsTitle: "Gelak", + inviteCodeInvalid: "Gonbidapen-kode hau ez da baliozkoa edo dagoeneko erabili da.", + inviteAlreadyMember: "Dagoeneko bertan zaude. Bere modulutik ireki dezakezu.", + actionFailed: "Ezin izan da egin. Saiatu berriro.", + publishTooShort: "Zure argitalpena laburregia da.", + walletSendFailed: "Ezin izan da ordainketa bidali: {reason}", + pixeliaCoordsError: "Koordenatu horiek saretik kanpo daude.", + courtsErrTitle: "Izenburu bat behar da.", + courtsErrRespondent: "Adierazi kasua noren aurka den.", + courtsErrRespondentId: "Hori ez da baliozko Oasis ID bat.", + courtsErrMethod: "Aukeratu kasua nola ebatziko den.", + courtsErrCaseNotFound: "Ez da kasu hori aurkitu.", + courtsErrBrief: "Idatzi zure erantzuna.", + courtsErrStance: "Aukeratu zure jarrera.", + courtsErrResult: "Idatzi emaitza.", + courtsErrTerms: "Idatzi akordioaren baldintzak.", + courtsErrMediator: "Gehitu bitartekari bat gutxienez.", + courtsErrSelfMediator: "Ezin zara zure kasuko bitartekari izan.", + courtsErrJudge: "Aukeratu epaile bat.", + courtsErrSelfJudge: "Ezin zara zure kasuko epaile izan.", + courtsErrVisibility: "Aukeratu nork ikus dezakeen kasua.", + courtsErrNomination: "Ez da izendapen hori aurkitu.", + opinionAlreadyGiven: "Dagoeneko eman duzu zure iritzia.", + cipherPasswordShort: "Pasahitzak gutxienez 32 karaktere izan behar ditu.", + errorForbidden: "Ez duzu honetarako sarbiderik.", + errorNotFound: "Ez da aurkitu.", + errorClosed: "Itxita dago.", + errorInviteOnly: "Hau gonbidatuentzat soilik da. Gonbidapen bat behar duzu.", + errorTooLong: "Testu hori luzeegia da.", + errorRequired: "Derrigorrezko informazioa falta da.", + errorInvalid: "Informazioaren zati bat ez da baliozkoa.", + errorNotAuthor: "Sortzaileak bakarrik egin dezake hau.", + panicRemoved: "Zure blockchaina ezabatu da.", padRecentSectionTitle: "Azken Padak", padOpenSectionTitle: "Pad Irekiak", padClosedSectionTitle: "Pad Itxiak", padCreateSectionTitle: "Pad Berria Sortu", padUpdateSectionTitle: "Pad Eguneratu", - padInviteGenerated: "Gonbidapen Kodea Sortua", typePad: "PADAK", padNew: "BERRIA", padAddFavorite: "Gogokoen Gehitu", padRemoveFavorite: "Gogokoetatik Kendu", - padClose: "Itxi Pad", + padClose: "Itxi", padBackToEditor: "Editorera itzuli", - padSearchPlaceholder: "Bilatu padak...", + padSearchPlaceholder: "Bilatu hemen: Padak...", modulesChatsLabel: "Txatak", - modulesChatsDescription: "Txat zifratu publikoak aurkitu eta kudeatzeko modulua.", + modulesChatsDescription: "Txatak aurkitu eta kudeatzeko modulua.", typeChat: "TXATAK", typeChatMessage: "TXAT MEZUA", - chatLabel: "TXATAK", - chatMessageLabel: "TXAT MEZUAK", chatsTitle: "Txatak", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "Zure txatak", + chatRecentTitle: "Azken txatak", chatFavoritesTitle: "Gogokoak", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "Txat irekiak", + chatClosedTitle: "Txat itxiak", chatDescription: "Deskribapena", chatCategory: "Kategoria", chatStatus: "EGOERA", - chatFilterAll: "GUZTIAK", - chatFilterMine: "NIREA", - chatFilterRecent: "BERRIA", - chatFilterFavorites: "GOGOKOAK", - chatFilterOpen: "IREKIA", - chatFilterClosed: "ITXIA", chatCreate: "Txata Sortu", - chatUpdate: "Txata Eguneratu", - chatDelete: "Txata Ezabatu", + chatUpdate: "Eguneratu", + chatDelete: "Ezabatu", chatClose: "Txata Itxi", chatVisitChat: "TXATA IKUSI", chatUntitled: "Izenburugabeko Txata", chatNoItems: "Ez da txatarik aurkitu.", chatParticipants: "Parte-hartzaileak", - chatStartChatting: "TXATEATZEN HASI!", - chatGenerateCode: "Kodea Sortu", - chatShareUrl: "URLa Partekatu", + chatMessagesLabel: "Mezuak", + chatThreadsLabel: "Hariak", chatCreatedAt: "SORTUA", - chatSearchPlaceholder: "Txatak bilatu...", + chatSearchPlaceholder: "Bilatu hemen: Txatak...", chatStatusOpen: "IREKIA", chatStatusInviteOnly: "GONBIDAPENEZ BAKARRIK", chatStatusClosed: "ITXIA", chatSendMessage: "Bidali", + chatJumpLatest: "Azken Mezua", + chatReply: "Erantzun", + chatReplyCancel: "Erantzuna utzi", + chatPin: "Finkatu", + chatUnpin: "Askatu", + chatPinned: "Finkatuak", + chatPickChat: "Aukeratu txat bat irekitzeko", + chatNoneYet: "Oraindik ez dago txatik eskuragarri.", + activitySearchPlaceholder: "Bilatu hemen: Jarduera...", + trendingSearchPlaceholder: "Bilatu hemen: Pil-pilean...", + opinionsSearchPlaceholder: "Bilatu hemen: Iritziak...", + votesSearchPlaceholder: "Bilatu hemen: Bozkatzeak...", + gamesSearchPlaceholder: "Bilatu hemen: Jokoak...", + schoolTitle: "Eskola", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "Jarraitzen duzun wiki orri bat editatu da.", + wikiBotRestoredTitle: "Jarraitzen duzun wiki orri baten aurreko bertsio bat berreskuratu da.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Ikasle berri batek zure ikastaroan izena eman du.", + schoolBotInvitedTitle: "Ikastaro batera gonbidatu zaituzte.", + schoolBotAdmittedTitle: "Ikastaro batean onartu zaituzte.", + schoolBotCertificateTitle: "Ziurtagiri bat jaso duzu.", + schoolBotPassedTitle: "Ikasle batek zure ikastaroa gainditu du.", + schoolBotLessonTitle: "Ikasgai berri bat argitaratu da.", + invitesSchoolTitle: "Eskola", + schoolEnrollLabel: "Batu ikastarora", + schoolExamRetryLater: "24 ordu igarota berriro saia zaitezke.", + schoolApproved: "GAINDITUTA", + schoolExamPassMark: "Gainditzeko nota (0-10)", + schoolExamPassed: "GAINDITUTA", + schoolExamPending: "Zain", + schoolExamFailed: "EZ GAINDITUA", + schoolExamAddQuestion: "Gehitu galdera", + schoolExamCreateQuestion: "Galdera sortu", + schoolExamOption: "Aukera", + schoolExamCorrect: "Aukera zuzena", + schoolExamPoints: "Puntuak", + schoolMaterialTextPlaceholder: "Ikasgai honetarako oharrak...", + schoolMaterials: "Materialak", + schoolAddMaterial: "Gehitu materiala", + schoolNoMaterials: "Oraindik ez dago materialik.", + bankYourSchoolBalance: "Zure eskolako irabaziak", + schoolProgress: "Aurrerapena", + schoolProgressMark: "Markatu osatutzat", + schoolProgressDone: "Osatuta", + schoolExams: "Azterketak", + schoolExamCreate: "Sortu azterketa", + schoolExamTitlePlaceholder: "Azterketaren izenburua", + schoolExamHelp: "Galdera bat lerroko: Galdera? ; aukera ; aukera zuzena* ; aukera", + schoolExamQuestionsPlaceholder: "Zer da SSB? ; Datu-base bat ; P2P protokolo bat* ; Nabigatzaile bat", + schoolExamSubmit: "Bidali erantzunak", + schoolExamScore: "Zure nota", + schoolNoScoresYet: "Oraindik ez dago emaitzarik.", + schoolLessonUnit: "Unitatea / fasea", + schoolLessonUnitPlaceholder: "adib. 1. fasea", + schoolLessonOrder: "Ordena", + profileClearnetSchoolLabel: "Eskola", + schoolStartDate: "Hasiera data", + schoolSessionDate: "Saioaren data", + schoolCourseChat: "Ikastaroaren txata", + agendaFilterSchool: "ESKOLA", + agendaFilterCampaigns: "KANPAINAK", + agendaFilterLogistics: "LOGISTIKA", + schoolCourseType: "Ikastaro mota", + schoolTypeOpen: "IREKIA", + schoolTypePaid: "ORDAINPEKOA", + schoolTypeInvite: "GONBIDAPENEZ SOILIK", + schoolLessonLocked: "Ikasgai zifratua — eskatu irakasleari ikastaroaren gakoa.", + schoolGrantAccess: "Bidali ikastaroaren gakoa", + schoolKeySent: "Gakoa bidalita", + schoolOngoing: "Abian", + schoolFilterApplied: "IZENA EMANDA", + schoolFilterOpen: "IREKIAK", + schoolFilterFavorites: "GOGOKOAK", + schoolPendingPayment: "Ordainketa zain", + schoolPendingStudents: "Izen-emateak zain", + schoolViewBill: "Ikusi faktura", + schoolPaymentNote: "Izena emateak ECO faktura bat (transferentzia bat) sortzen du. Zure izen-ematea transferentzia sinatzean berresten da.", + schoolCloseCourse: "Itxi ikastaroa", + schoolReopenCourse: "Ireki berriro ikastaroa", + schoolDescription: "Irakatsi eta ikasi ikastaroak zure sarean.", + schoolFilterAll: "GUZTIAK", + schoolFilterMine: "NIREAK", + schoolFilterTop: "TOP", + schoolFilterRecent: "AZKENAK", + schoolCreateButton: "Sortu Ikastaroa", + schoolCreateSectionTitle: "Sortu ikastaro bat", + schoolUpdateSectionTitle: "Eguneratu ikastaroa", + schoolCourseTitlePlaceholder: "Zure ikastaroaren izena", + schoolCourseDescriptionPlaceholder: "Zer irakatsiko duzu ikastaro honetan?", + schoolCourseImage: "Ikastaroaren irudia (gehienezko tamaina: 75MB)", + schoolTags: "Etiketak", + schoolTagsPlaceholder: "Idatzi etiketak komaz bereizita", + schoolPrice: "Prezioa (ECO)", + schoolVisibility: "Ikusgaitasuna", + schoolVisibilityPublic: "Publikoa", + schoolVisibilityInvite: "Gonbidapenez", + schoolStatus: "Egoera", + schoolClosed: "Itxita", + schoolFree: "Doan", + schoolCreate: "Sortu ikastaroa", + schoolUpdate: "Eguneratu ikastaroa", + schoolSearchPlaceholder: "Bilatu hemen: Eskola...", + schoolSortTitle: "Izenburua", + schoolNoCourses: "Ez da ikastarorik aurkitu.", + schoolStudents: "Ikasleak", + schoolTeacher: "Irakaslea", + schoolCreatedAt: "SORTUA", + schoolLessons: "Ikasgaiak", + schoolNoLessons: "Oraindik ez dago ikasgairik.", + schoolEnrollToSee: "Eman izena ikastaro honetako ikasgaiak ikusteko.", + schoolEnrollButton: "IZENA EMAN", + schoolUnenrollButton: "Utzi ikastaroa", + schoolInviteRequired: "Ikastaro hau gonbidapenez da.", + schoolTeacherPanel: "Irakaslearen panela", + schoolStudentPanel: "Ikaslearen panela", + schoolAddLesson: "Ikasgaia sortu", + schoolLessonTitlePlaceholder: "Ikasgaiaren izenburua", + schoolLessonTextPlaceholder: "Deskribatu ikasgai honek lantzen dituen nozioak...", + schoolInviteStudents: "Gonbidatu ikasleak", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "GONBIDATU", + schoolInvited: "Gonbidatuak", + schoolIssueCertificate: "Ziurtagiria sortu", + schoolCertified: "Ziurtatua", + schoolCertificateTextPlaceholder: "Ziurtagiriaren oharra (aukerakoa)", + schoolLessonOpen: "Ikasgaia ireki", + schoolCertificatePdf: "PDF", + schoolCertificates: "Ziurtagiriak", + schoolNoCertificates: "Oraindik ez da ziurtagiririk eman.", + schoolEditButton: "Editatu", + schoolDeleteButton: "Ezabatu", + typeSchool: "ESKOLA", + schoolCourseLabel: "IKASTAROA", + emergencyLabel: "LARRIALDIA", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Ikastaroak", + statsSchoolLesson: "Ikasgaiak", + statsSchoolEnroll: "Izen-emateak", + statsSchoolCertificate: "Eskolako ziurtagiriak", + modulesSchoolLabel: "Eskola", + modulesSchoolDescription: "Ezagutza trukatzeko modulua.", + welcomeStepUxAction: "Gorde ikuspegia", + welcomeStepWorkflowTitle: "Zer erabilera duzu buruan OASISerako?", + welcomeStepWorkflowText: "Oasisek modulu asko ditu. Aukeratu zuri egokitzen zaizun konfigurazioa eta erabilgarriak bakarrik geratuko dira aktibatuta. Nahi duzunean doi dezakezu Moduluak atalean.", + welcomeStepWorkflowAction: "Gorde konfigurazioa", + welcomeWorkflow_full: "Lehenetsia", + chatRateLimitTitle: "Mezu gehiegi", + chatRateLimitMessage: "Gela honek ordu batean onartzen duen mezu mugara iritsi zara. Saiatu geroago.", chatMessagePlaceholder: "Idatzi zure mezua...", chatNoMessages: "Oraindik mezurik ez.", - chatLeave: "Txata Utzi", - chatInviteCodeLabel: "Gonbidapen kodea sartu", - chatJoinByInvite: "Sartu", chatTitlePlaceholder: "Txataren izenburua", chatDescriptionPlaceholder: "Txataren deskribapena", - chatTagsPlaceholder: "etiketa1, etiketa2", + chatTagsPlaceholder: "Idatzi etiketak komaz bereizita", chatImageLabel: "Aukeratu irudi-fitxategi bat (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "Gonbidapen Kodea", chatAuthor: "Egilea", - chatCreated: "Sortua", chatAddFavorite: "Gogokoen artean gehitu", chatRemoveFavorite: "Gogokoenetatik kendu", chatPM: "MP", chatStatusLabel: "Egoera", + chatTypeLabel: "Txat mota", chatCategoryLabel: "Kategoria", - chatParticipantsLabel: "Parte-hartzaileak", gamesTitle: "Jokoak", gamesDescription: "Aurkitu eta jokatu joko batzuk.", gamesFilterAll: "GUZTIAK", gamesPlayButton: "JOLASTU!", - gamesBackToGames: "Jokoetara itzuli", modulesGamesLabel: "Jokoak", modulesGamesDescription: "Jokoak aurkitzeko eta jolasteko modulua.", modulesGraphosLabel: "Graphos", @@ -3320,8 +4174,6 @@ module.exports = { gamesArkanoidDesc: "Hautsi adreilu guztiak zure pala eta pilotarekin. Arcade klasiko bat.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong klasikoa IA baten aurka. Lehenak 5 puntu lortzen duena irabazten du.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "Denboraren aurka lasterketa! Saihestu trafikoa eta iritsi helmugara garaiz.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Gidatu zure ontzia asteroide eremuaren bidez. Tiro egin haiek jo aurretik.", gamesRockPaperScissorsTitle: "Harria Paper Artaziak", @@ -3331,68 +4183,57 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "Bota txanpon bat eta apostatu aurpegian edo buztanean. Zenbat zorte duzu?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "Goitik ikusitako billarra. Egin klik zuzentzeko eta eutsi sakatuta indarra kargatzeko. Sartu bola guztiak ahalik eta jaurtiketa gutxienekin.", + gamesArtilleryTitle: "Artilleria", + gamesArtilleryDesc: "Zuzendu zure kanoia, kontuan hartu haizea eta jo jomuga. 5 txanda: jaurtiketa gutxien egiten duenak irabazten du.", + gamesLabyrinthTitle: "Labirintoa", + gamesLabyrinthDesc: "Ihes egin labirintotik mugimenduak agortu baino lehen. Maila bakoitza handiagoa eta zailagoa da.", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "Jan puntu guztiak eta saihestu mamuak. Mamuak denbora errealean mugitzen dira — ez gelditu!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesAudioPendulumTitle: "Pendulu soinuduna", + gamesAudioPendulumDesc: "Fisika kaotikoaren simulagailua, denbora errealeko audio-sintesiarekin. Abiadura angeluarrak maiztasun bihurtzen dira, eta gailurrak perkusio-kolpe. Ez dago berdin soinatzen duten bi simulaziorik.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Nahastu bisualak zuzenean bi platerekin: aukeratu eredu bat bakoitzarentzat, doitu abiadura, kolorea eta zooma, nahastu biak eta jaurti distirak erritmora. Markatu tempoa edo utzi mikrofonoak pultsua eramaten.", + gamesTetrisDesc: "Erortzen diren bloke klasikoak. Osatu lerroak puntuak lortzeko. Zenbat iraungo duzu?", + gamesMinesweeperTitle: "Minak", + gamesMinesweeperDesc: "Minen ehiza klasikoa. Garbitu taula mailaz igotzeko — sareta handiagoak, mina gehiago, puntuazio handiagoa.", + gamesFilterScoring: "PUNTUAZIOAK", + gamesHallOfFame: "Ospearen aretoa", + gamesHallPlayer: "Jokalaria", + gamesHallScore: "Puntuazioa", + gamesNoScores: "Oraindik ez dago puntuaziorik.", calendarsTitle: "Egutegia", calendarTitle: "Egutegia", modulesCalendarsLabel: "Egutegiak", modulesCalendarsDescription: "Egutegiak aurkitu eta kudeatzeko modulua.", typeCalendar: "EGUTEGIAK", - calendarFilterAll: "GUZTIAK", - calendarFilterMine: "NIREAK", - calendarFilterOpen: "IREKIA", - calendarFilterClosed: "ITXIA", - calendarFilterRecent: "BERRIAK", - calendarFilterFavorites: "GOGOKOAK", calendarCreate: "Egutegia sortu", calendarUpdate: "Eguneratu", - calendarDelete: "Ezabatu", calendarTitleLabel: "Izenburua", calendarTitlePlaceholder: "Egutegiko izenburua...", calendarStatusLabel: "Egoera", + calendarTypeLabel: "Egutegi mota", calendarStatusOpen: "IREKIA", calendarStatusClosed: "ITXIA", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "Epemuga", calendarTagsLabel: "Etiketak", - calendarTagsPlaceholder: "etiketa1, etiketa2...", + calendarTagsPlaceholder: "Idatzi etiketak komaz bereizita", calendarParticipantsLabel: "Parte-hartzaileak", - calendarParticipantsCount: "Parte-hartzaileak", - calendarVisitCalendar: "Egutegia bisitatu", calendarCreated: "Sortua", - calendarAuthor: "Egilea", calendarJoin: "Batu", calendarGenerateInvite: "Sortu gonbidapena", - calendarInviteCodePlaceholder: "Sartu gonbidapen-kodea...", - calendarValidateInvite: "Egiaztatu kodea", - calendarJoined: "Batuta", - calendarAddDate: "Data gehitu", - calendarAddNote: "Oharra gehitu", calendarDateLabel: "Data", calendarDatePlaceholder: "Data hau deskribatu...", - calendarNoteLabel: "Oharra", + calendarNoteLabel: "Oharrak", calendarNotePlaceholder: "Oharra gehitu...", calendarFirstDateLabel: "Data", calendarFirstNoteLabel: "Oharrak", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "Tartea", + calendarIntervalNone: "Errepikapenik gabe", + calendarIntervalWeekly: "Astero", + calendarIntervalMonthly: "Hilero", + calendarIntervalYearly: "Urtero", calendarFormDescription: "Deskribapena", calendarNoDates: "Ez dago datarik.", calendarNoNotes: "Ez dago oharrik.", @@ -3400,8 +4241,8 @@ module.exports = { calendarsDescription: "Aurkitu eta kudeatu zure sareko egutegiak.", calendarMonthPrev: "← Aurrekoa", calendarMonthNext: "Hurrengoa →", - calendarMonthLabel: "Datak", - calendarsShareUrl: "Partekatzeko URLa", + calendarMonthNames: "Urtarrila,Otsaila,Martxoa,Apirila,Maiatza,Ekaina,Uztaila,Abuztua,Iraila,Urria,Azaroa,Abendua", + calendarDayInitials: "Al,Ar,Az,Og,Or,La,Ig", calendarAllSectionTitle: "Egutegiak", calendarRecentSectionTitle: "Egutegi Berriak", calendarFavoritesSectionTitle: "Gogokoak", @@ -3411,11 +4252,10 @@ module.exports = { calendarCreateSectionTitle: "Egutegi berria sortu", calendarUpdateSectionTitle: "Egutegia eguneratu", calendarAddFavorite: "Gogokoen gehitu", - calendarDeleteNote: "Delete", + calendarDeleteNote: "Ezabatu", calendarRemoveFavorite: "Gogokoetatik kendu", - calendarSearchPlaceholder: "Egutegiak bilatu...", + calendarSearchPlaceholder: "Bilatu hemen: Egutegia...", calendarAddEntry: "Gehitu Sarrera", - calendarLeave: "Irten Egutegitik", statsCalendar: "Egutegiak", statsCalendarDate: "Egutegi datak", statsCalendarNote: "Egutegi oharrak", @@ -3426,6 +4266,146 @@ module.exports = { tribeContentAccessDenied: "Sarbidea Ukatua", tribeContentAccessDeniedMsg: "Eduki hau tribu batena da. Kide izan behar zara sartzeko.", tribeViewTribes: "Tribuak Ikusi", + tribeFilesEmpty: "Oraindik ez dago fitxategiaik.", + filesLabel: "Fitxategiaak", + profileClearnetFilesLabel: "Fitxategiaak", + filesTitle: "Fitxategiaak", + filesDescription: "Esploratu eta kudeatu fitxategiaak zure sarean.", + fileAllSectionTitle: "Fitxategiaak", + fileMineSectionTitle: "Zure Fitxategiaak", + fileRecentSectionTitle: "Azken Fitxategiaak", + fileTopSectionTitle: "Fitxategia Onenak", + fileFavoritesSectionTitle: "Gogokoak", + fileFilterAll: "DENAK", + fileFilterMine: "NIREAK", + fileFilterRecent: "AZKENAK", + fileFilterTop: "ONENAK", + fileFilterFavorites: "GOGOKOAK", + fileCreateSectionTitle: "Fitxategiaa Igo", + fileUpdateSectionTitle: "Fitxategiaa Eguneratu", + fileCreateButton: "Fitxategiaa Igo", + fileUpdateButton: "Eguneratu", + fileAddFavoriteButton: "Gogokoetara Gehitu", + fileRemoveFavoriteButton: "Gogokoetatik Kendu", + fileFileLabel: "Aukeratu fitxategi bat (max: 75MB)", + calendarFilterRecent: "AZKENAK", + calendarFilterAll: "GUZTIAK", + calendarFilterMine: "NIREAK", + calendarFilterFavorites: "GOGOKOAK", + calendarFilterOpen: "IREKITA", + calendarFilterClosed: "ITXITA", + padFilterRecent: "AZKENAK", + padFilterAll: "GUZTIAK", + padFilterMine: "NIREAK", + padFilterOpen: "IREKITA", + padFilterClosed: "ITXITA", + chatFilterRecent: "AZKENAK", + chatFilterAll: "GUZTIAK", + chatFilterMine: "NIREAK", + chatFilterFavorites: "GOGOKOAK", + chatFilterOpen: "IREKITA", + chatFilterClosed: "ITXITA", + licenseLabel: "Lizentzia", + phoneTitle: "Telefonoa", + phoneDescription: "Kudeatu muturretik muturrera zifratutako zure bideodeiak.", + phoneUnavailable: "Deiak ez daude erabilgarri gailu honetan.", + phoneCallButton: "Deitu", + profileVisibilityPhone: "Telefono-zenbakia", + phoneNumberUnknown: "Zure sareko inork ez du {number} zenbakia.", + phoneNumberChoose: "Egiaztatu nori deitu nahi diozun {number} zenbakira:", + phoneAnswer: "Erantzun", + phoneSpeaker: "Bozgorailua", + phoneEarpiece: "Entzungailua", + phoneReject: "Baztertu", + phoneHangup: "Eseki", + phoneMute: "Isildu", + phoneUnmute: "Mikrofonoa aktibatu", + phoneCameraOn: "Kamera on", + phoneCameraOff: "Kamera off", + settingsPhoneVideo: "Bideoa", + settingsPhoneVideoOn: "Deietan eta geletan baimenduta", + settingsPhoneVideoOff: "Audioa bakarrik", + phoneSilence: "Ez entzun", + phoneUnsilence: "Berriro entzun", + phoneCalling: "Deitzen:", + phoneIsCalling: "deitzen ari zaizu", + notifyNewPmFrom: "MP berria:", + notifyNewPamFrom: "PaM berria:", + notifyUpdateLabel: "Eguneraketa", + updateInstalledLabel: "Instalatutako bertsioa", + updateAvailableLabel: "Bertsio erabilgarria", + updateChangesLabel: "Aldaketak", + updateFilesLabel: "Fitxategiak", + updateOriginLabel: "Jatorria", + phoneRecording: "PaM grabatzen honentzat:", + phoneLeaveMessage: "Inork ez du erantzun: utzi zure mezua seinalearen ondoren.", + phoneLeaveMessageBusy: "Beste dei batean dago: utzi zure mezua seinalearen ondoren.", + phoneLeaveMessageUnreachable: "Ezin izan da harremanetan jarri: utzi zure mezua seinalearen ondoren.", + phoneConnecting: "Konektatzen:", + phoneSendPam: "PaM bidali", + phoneCancel: "Utzi", + phoneSearchPlaceholder: "Bilatu Telefonoan...", + phonePamTitle: "Audio-grabazioak", + phoneNew: "Berria", + phoneDelete: "Ezabatu", + phoneNoPams: "Ez duzu audio-grabaziorik.", + phoneHistoryTitle: "Historia", + phoneLatestTitle: "Azken deiak", + phoneCreateButton: "Dei berria", + phoneLimitsHint: "Dei bakoitzeko {n} bizilagun gehienez; bat baino gehiagorekin dei bateratua da.", + phoneErrorPub: "Ezin zaio pub bati deitu.", + phoneJointCall: "Dei bateratua", + phoneWith: "Norekin", + phoneResult: "Emaitza", + phoneWhen: "Noiz", + phoneDuration: "Iraupena", + phoneOutgoing: "Irteerakoa", + phoneIncoming: "Sarrerakoa", + phoneNoHistory: "Oraindik ez dago erregistrorik.", + phoneOutcomeEnded: "Erantzunda", + phoneOutcomeMissed: "Galdua", + phoneOutcomeRejected: "Baztertua", + phoneOutcomeNoanswer: "Erantzunik gabe", + phoneOutcomeCancelled: "Bertan behera utzia", + phoneOutcomePam: "PaM bidalia", + phoneOutcomeFailed: "Huts egin du", + phoneDialAdd: "Gehitu", + phoneDialBack: "Ezabatu", + phoneDialClear: "Garbitu", + phoneDialPlaceholder: "Zenbakia edo Oasis IDa", + phoneDialInhabitants: "Biztanleak", + phoneDialRemove: "Kendu", + phoneOwnNumber: "Hori zure zenbakia da.", + phoneSettingsDesc: "Aukeratu nork deitu diezazukeen.", + phoneDnd: "Ez molestatu", + phoneOpen: "Irekia", + phoneRecipientRefuses: "Deitu nahi diozun biztanleak ez du deirik onartzen.", + phoneRefusedNotice: "Biztanle hauek ez dute deirik onartzen eta ez zaie deitu:", + fileEmptyError: "Fitxategia hutsik dago. Aukeratu beste fitxategi bat.", + fileTitleLabel: "Izenburua", + fileTitlePlaceholder: "Izenburua", + fileDescriptionLabel: "Deskribapena", + fileDescriptionPlaceholder: "Deskribapena", + fileTagsLabel: "Etiketak", + fileTagsPlaceholder: "Idatzi etiketak komaz bereizita", + fileSizeLabel: "Tamaina", + fileDownloadButton: "DESKARGATU", + fileDetailsButton: "XEHETASUNAK", + fileNoFile: "Fitxategia fitxategirik ez", + fileSearchPlaceholder: "Bilatu hemen: Fitxategiaak...", + fileSearchButton: "Bilatu", + fileSortRecent: "Berrienak", + fileSortOldest: "Zaharrenak", + fileSortTop: "Bozkatu enak", + fileNoMatch: "Ez dago fitxategia bat datorrenik.", + fileUpdatedAt: "Eguneratua", + typeFile: "FITXATEGIAAK", + modulesFilesLabel: "Fitxategiak", + modulesFilesDescription: "Fitxategiak aurkitu eta kudeatzeko modulua.", + tribeSectionFiles: "FITXATEGIAAK", + fileDescription: "Deskribapena", + fileDownload: "Deskargatu", + fileTypeLabel: "Mota", torrentsTitle: "Torrentak", torrentsDescription: "Esploratu eta kudeatu torrentak zure sarean.", torrentAllSectionTitle: "Torrentak", @@ -3451,49 +4431,93 @@ module.exports = { torrentDescriptionLabel: "Deskribapena", torrentDescriptionPlaceholder: "Deskribapena", torrentTagsLabel: "Etiketak", - torrentTagsPlaceholder: "Sartu etiketak komaz bereizita", + torrentTagsPlaceholder: "Idatzi etiketak komaz bereizita", torrentSizeLabel: "Tamaina", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "Torrenta sortu", + torrentFromContentMissing: "Fitxategia oraindik ez dago nodo honetan eskuragarri. Saiatu geroago.", + torrentDownloads: "Deskargak", + torrentFetchOasis: "Oasisen deskargatu", + torrentSaveFile: "Fitxategia gorde", + torrentSeeds: "hazi", + seedsLabel: "Haziak", + seedAction: "Lagundu", + torrentSourceFile: "Fitxategia", + torrentStateQueued: "Ilaran", + torrentStateProbing: "Haziak bilatzen", + torrentStateDownloading: "Deskargatzen", + torrentStateDone: "Osatuta", + torrentStateFailed: "Huts egin du", + torrentStateNoSource: "Ez dago hazirik konektatuta, zain", + torrentStateCancelled: "Bertan behera utzita", + torrentNoDownloads: "Oraindik ez dago deskargarik.", + torrentOriginalSection: "Jatorrizkoa", + torrentCancel: "Utzi", + torrentDownloadButton: "DESKARGATU", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "XEHETASUNAK", torrentNoFile: "Torrent fitxategirik ez", noTorrents: "Ez da torrentik aurkitu.", - torrentSearchPlaceholder: "Bilatu izenburua, etiketak, egilea...", + noFiles: "Ez da fitxategiaik aurkitu.", + torrentSearchPlaceholder: "Bilatu hemen: Torrentak...", torrentSearchButton: "Bilatu", torrentSortRecent: "Berrienak", torrentSortOldest: "Zaharrenak", torrentSortTop: "Bozkatu enak", torrentNoMatch: "Ez dago torrent bat datorrenik.", - torrentMessageAuthorButton: "Mezua Bidali Egileari", torrentUpdatedAt: "Eguneratua", statsTorrent: "Torrentak", + statsFile: "Fitxategiak", typeTorrent: "TORRENTAK", modulesTorrentsLabel: "Torrentak", modulesTorrentsDescription: "Torrentak aurkitu eta kudeatzeko modulua.", favoritesFilterTorrents: "TORRENTAK", + favoritesFilterFiles: "FITXATEGIAK", favoritesFilterMarket: "MERKATUA", + favoritesFilterEvents: "EKITALDIAK", + favoritesFilterForum: "FOROA", + favoritesFilterHousing: "ETXEBIZITZA", + favoritesFilterJobs: "LANPOSTUAK", + favoritesFilterProjects: "PROIEKTUAK", + favoritesFilterReports: "TXOSTENAK", + favoritesFilterTasks: "ZEREGINAK", + favoritesFilterTransfers: "TRANSFERENTZIAK", + favoritesFilterVotes: "BOZKETAK", favoritesFilterShopProducts: "DENDAKO ARTIKULUAK", tribeSectionTorrents: "TORRENTAK", tribeCreateTorrent: "Torrenta Igo", - tribeMediaTypeTorrent: "Torrenta", + tribeCreateFile: "Fitxategia igo", settingsWishTitle: "Nahia", - settingsWishDesc: "Konfiguratu zure Avatarraren nahia.", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "Eduki guztietara nola sartzen zaren eta Oasis sarean duzun esperientzia zehazten du.", + settingsWishWhole: "Multibertsoa", + settingsWishMutuals: "Elkar laguntza soilik", + settingsWishOnlyLan: "LAN soilik", + settingsWishLocal: "Lokala", settingsPmVisibilityTitle: "Mezu Pribatuak", settingsPmVisibilityDesc: "Konfiguratu sarean izan nahi duzun mezu pribatuen esposizio maila.", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "Multibertsoa", + settingsPmVisibilityMutuals: "Elkar laguntza soilik", + mute: "Isilarazi", + unmute: "Isilarazteari utzi", + relationshipMuted: "Zuk isilarazia", + hideContent: "Niretzat ezkutatu", + unhideContent: "Berriro erakutsi", + hiddenByYou: "Zuk ezkutatua", + settingsTabContent: "Edukia", + settingsHiddenTitle: "Ezkutatutako edukia", + settingsHiddenDesc: "Isilarazi dituzun biztanleak eta ezkutatu duzun edukia.", + settingsMutedLabel: "Isilarazitako biztanleak", + settingsHiddenLabel: "Ezkutatutako edukia", + settingsMutedEmpty: "Inor ez isilarazita.", + settingsHiddenEmpty: "Ezer ez ezkutatuta.", + settingsPmVisibilityFollowing: "Jarraitzen dudan jendea bakarrik", + settingsMentionsVisibilityTitle: "Aipamenak", + settingsMentionsVisibilityDesc: "Aukeratu noren aipamenak iristen zaizkizun.", pmMutualNotice: "Bidalketa UX babes bat da. Jasotzea arreta-iragazkia.", - pmBlockedNonMutual: "Elkarrekiko laguntza duten biztanleei bakarrik bidal diezaiekezu.", inhabitantsPendingFollowsTitle: "Onespen zain dauden eskaerak", inhabitantsPendingAccept: "Onartu", inhabitantsPendingReject: "Ukatu", - bxEncrypted: "ZIFRATUA", - bxEncryptedHexLabel: "Testu zifratua (aurrebista)", tribeSectionGovernance: "GOBERNANTZA", - tribeSubStatusPublic: "PUBLIKOA", - tribeSubStatusPrivate: "PRIBATUA", - tribeSubInheritedPrivate: "PRIBATUA (tribu nagusitik heredatua)", tribePadInviteRequired: "Ez duzu pada atzipena. Eskatu gonbidapena.", tribeChatInviteRequired: "Ez duzu txata atzipena. Eskatu gonbidapena.", tribeGovernanceDesc: "Tribu honen barne gobernantza.", @@ -3532,212 +4556,198 @@ module.exports = { tribeGovNoRevocations: "Oraindik ez dago errebokatzerik", tribeGovNoHistorical: "Oraindik ez dago erregistrorik", logsTitle: "Egunkaria", - logsDescription: "Erregistratu zure esperientzia sarean.", - logsReadMore: "Gehiago irakurri", + logsDescription: "Eraman zure esperientzien egunkari pribatua.", logsViewTitle: "Egunkaria", logsColumnType: "Mota", logsView: "Ikusi", - logsManualPrompt: "Idatzi zure egunkaria", logsUpdateButton: "Eguneratu", - logsEditTitle: "Sarrera editatu", - logsCreateDescription: "Erregistratu zure esperientzia...", + logsEditTitle: "Eguneratu erregistroa", logsCreateTitle: "Sarrera sortu", logsWriteButton: "Idatzi", logsTextPlaceholder: "Deskribatu zure esperientziak...", - logsTextField: "Testua", - logsLabelPlaceholder: "Etiketa...", - logsLabelField: "Idatzi zure egunkaria (etiketa)", - logsAiDisabledWarn: "Gaitu AA modulua /modules-en AA-idatzitako sarrerak erabiltzeko.", - logsAiContextValue: "Blockchain-eguna", - logsAiContext: "Testuingurua", - logsAiModStatus: "AImod", - logsModeApply: "Aplikatu!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "IA laguntzailea", logsModeManual: "Eskuz", logsModeAI: "AA", - logsColumnDelete: "Ezabatu", - logsColumnEdit: "Editatu", logsColumnLog: "Egunkaria", logsColumnDate: "Data", logsDelete: "Ezabatu", - logsEdit: "Editatu", + logsEdit: "Eguneratu", logsFilterAlways: "BETI", logsFilterYear: "AZKEN URTEA", logsFilterMonth: "AZKEN HILABETEA", logsFilterWeek: "AZKEN ASTEA", logsFilterToday: "GAUR", - logsFilterRecent: "BERRIAK", - logsFilterAll: "DENAK", + logsComments: "Iruzkinak", + logsAuthorEntries: "Sarrerak", logsCreate: "Sarrera sortu", logsExport: "Egunkaria esportatu", - logsExportOne: "Esportatu", + logsExportOne: "Esportatu erregistroa", logsViewDetails: "Ikusi xehetasunak", logsGenerateButton: "Sortu testua", - logsSearchText: "Bilatu egunkarietan...", + logsSearchText: "Bilatu hemen: Egunkaria...", logsSearchAnyType: "Edozein mota", logsSearchButton: "Bilatu", logsEmpty: "Oraindik sarrerarik ez.", + logsNothingNew: "Ez dago erregistratzeko ekintza berririk.", modulesLogsLabel: "Egunkaria", - modulesLogsDescription: "Modulua zure esperientziak (AA laguntzailearen bidez) erregistratzeko.", + modulesLogsDescription: "Zure esperientzien egunkari pribatua eramateko modulua.", statsLogsTitle: "Egunkaria", - statsLogsEntries: "Sarrerak", typeLog: "EGUNKARIA", - blockchainCycle: "Zikloa", larpTitle: "L.A.R.P.", larpDescription: "Esperimentazio kolaboratiborako zuzeneko rol-joko geruza bat.", + larpNoHousesMatch: "Ez dago bilaketarekin bat datorren etxerik.", + larpSearchPlaceholder: "Bilatu hemen: L.A.R.P....", larpAllHouses: "Etxeak:", larpFilterRuling: "AGINTZEN", larpFilterHouses: "ETXEAK", larpFilterRules: "FAQ", larpRulesTitle: "L.A.R.P. FAQ", - larpRulesEntryTitle: "Hasierako etxea", larpRulesEntryText: "Bizilagun bakoitza ACADEMIA-n hasten da lehenespenez. ACADEMIA-tik, aukeratu etxe bat \"Etxe batera sartu\" panelean: horrek sarrera-proba irekitzen du. Erantzun 10 galderak eta bidali. Atalasea (7/10) gainditzen baduzu, automatikoki onartzen zaitu etxe horretan; bestela, baztertuta gelditzen zara eta ACADEMIA-n geratzen zara. Bietan, sistemak zure OasisID eta saiakera-zikloa erregistratzen ditu eta beste saiakera bat eragozten du 30 zikloko itxaronaldia igaro arte.", - larpRulesLeaveText: "Edozein etxetako kideek ACADEMIA-ra itzul daitezke noiznahi beren etxe-orritik; horrek kidetza berrabiarazten du, baina ez du saiakera-itxaronaldia indargabetzen.", - larpRulesGovernanceTitle: "Gobernu-horma", larpRulesGovernanceText: "Bere zikloan, etxe agintariak \"Agintzen\" intsignia darama eta bere horma da soilik AGINTZEN fitxapean publikoki erakusten dena.", - larpRulesSignTitle: "L.A.R.P. Ikurra", + larpRulesWallText: "Etxe bakoitzak Horma bat du, non bere kideek idazten duten. Etxe baten Horma pribatua da, agintean dagoen etxearena eta ACADEMIArena izan ezik, edonork irakur baititzake.", larpRulesSignText: "Bizilagunek aktiba dezakete (/profile/edit > Sentsoreak) beren uneko etxearen irudia zigilu gisa erakusteko profilean, egile- eta bizilagun-txartelean. Zigiluak etxearen orrira eramaten du.", larpPostsTitle: "Horma", larpPostsEmpty: "Oraindik ez dago argitalpenik.", - larpPostLabel: "Argitalpen berria", larpPostPlaceholder: "Zer du etxe honek esateko?", - larpPostPublic: "Publikoa (edonork ikusgai, bestela kideek soilik)", larpPostSubmit: "Argitaratu", - larpPostMembersOnly: "Kideek soilik", larpCycleLabel: "Zikloa:", - audioBackToBcs: "Back to BCS", + audioBackToBcs: "Itzuli BCSra", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", - audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", - audioTranscodeDetailTitle: "Transcode", - audioTranscodeStegoEmpty: "(none)", - audioTranscodeStegoMessage: "TEXT", - audioTranscodeStegoNotFound: "No steganographic payload could be decoded from this audio.", - audioTranscodeStegoOasisId: "By", - audioTranscodeStegoTimestamp: "Generated at", + audioTranscodeButton: "TRANSKODETU", + audioTranscodeCompositionEmpty: "Audio honek ez du blockchain-konposizio gordetakorik.", + audioTranscodeDetailDescription: "Deskodetu txertatutako karga eta blockchain-aren jatorrizko konposizio-mapa.", + audioTranscodeDetailTitle: "Transkodetu", + audioTranscodeStegoEmpty: "(bat ere ez)", + audioTranscodeStegoMessage: "TESTUA", + audioTranscodeStegoNotFound: "Ezin izan da audio honetatik karga esteganografikorik deskodetu.", + audioTranscodeStegoTimestamp: "Noiz sortua", audioTranscodeStegoUnknown: "—", - bankExchangeEcoTaxAnnual: "ECOin Taxes (annual)", - bankExchangeEcoTaxMonthly: "ECOin Taxes (monthly)", - bankOverviewYourTaxes: "Your taxes", - bankRulesAlpha: "Alpha (max share of pub balance per epoch)", + bankExchangeEcoTaxAnnual: "ECOin zergak (urtekoak)", + bankExchangeEcoTaxMonthly: "ECOin zergak (hilekoak)", + bankOverviewYourTaxes: "Zergak guztira", + bankRulesAlpha: "Alpha (pubaren saldoaren gehieneko zatia aro bakoitzeko)", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", - bankRulesArchTaxRate: "Rate", - bankRulesArchTaxTitle: "ARCH Tax", - bankRulesCapPerEpoch: "Cap per epoch (absolute)", - bankRulesCapUser: "Cap per user per epoch", - bankRulesCarbonFactor: "Carbon factor", + bankRulesArchTaxNote: "ARCH tasak sarearen artxiboa hazteko eta mantentzeko epe luzeko kostua islatzen du: zenbat eta zaharragoak izan zure datuak sarean, orduan eta handiagoa izango da mantentze-kostuan duzun zatia.", + bankRulesArchTaxRate: "Tarifa", + bankRulesArchTaxTitle: "ARCH tasa", + bankRulesCapPerEpoch: "Aro bakoitzeko muga (absolutua)", + bankRulesCapUser: "Erabiltzaile bakoitzeko muga aro bakoitzeko", + bankRulesCarbonFactor: "Karbono-faktorea", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", - bankRulesChipTitle: "ECO Tax chip color band", + bankRulesChipNote: "Elementu bakoitzaren txipa sarean inoiz ikusitako mezurik handienarekiko koloreztatzen da eta bizilagun aktiboen kopuruak leuntzen du — sare handiago batek karga banatzen du eta elementu gehiago eramaten ditu banda berdera.", + bankRulesChipTitle: "ECO tasaren txiparen kolore-banda", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", - bankRulesEcoTaxRate: "Rate", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesEpochKind: "Epoch granularity", - bankRulesFloor: "Floor per user (gross)", - bankRulesGraceDays: "Claim grace period", + bankRulesEcoTaxNote: "ECO tasa OBU gordinetik kentzen da aroaren esleipena kalkulatzen denean. Kendutako ECOak ez dira aro honetan birbanatzen — pubaren saldoan geratzen dira eta hurrengo aroaren funtsa elikatzen dute reserveMin / alpha mugen bidez.", + bankRulesEcoTaxRate: "Tarifa", + bankRulesEcoTaxTitle: "ECO tasa", + bankRulesEpochKind: "Aroaren granulartasuna", + bankRulesFloor: "Erabiltzaile bakoitzeko gutxienekoa (gordina)", + bankRulesGraceDays: "Eskaeraren grazia-epea", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", - bankRulesKarmaTitle: "Karma (user engagement score)", + bankRulesKarmaNote: "Karma zure ekintzetatik ateratzen da (argitalpenak, botoak, parlamentuan/epaitegietan parte hartzea, etab.), denborarekin ahultzen den faktore batekin, zure jarioak sortutako karbono-gramoak kenduta. Zure edukiak jasotzen duenak ere balio du: iritzi, boto, erreakzio eta erreprodukzio positiboek karma gehitzen dute, eta iritzi edo boto negatiboek kentzen dute hainbat biztanle ados daudenean.", + bankRulesKarmaTitle: "Karma (erabiltzailearen konpromiso-puntuazioa)", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", - bankRulesPoolTitle: "Monthly pool", - bankRulesReserveMin: "Reserve minimum (kept in pub balance)", + bankRulesPoolTitle: "Hileko funtsa", + bankRulesReserveMin: "Gutxieneko erreserba (pubaren saldoan gordea)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", - bankRulesShareTitle: "Per-user share", + bankRulesShareTitle: "Erabiltzaile bakoitzeko zatia", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankRulesWMax: "Maximum weight (w_max)", - bankRulesWMin: "Minimum weight (w_min)", - bankTaxes: "Taxes", - bankTaxesAnnualEcoin: "ECO Tax (annual)", - bankTaxesArchTaxAnnual: "ARCH Tax (annual)", - bankTaxesArchTaxFirstBlock: "Your first block age", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxLifetime: "ARCH Tax (lifetime)", - bankTaxesArchTaxMonthly: "ARCH Tax (monthly)", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxRate: "Rate", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesArchTaxUserAmount: "Your ARCH tax (price to return)", - bankTaxesEcoTaxLifetime: "ECO Tax (lifetime)", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesLookupAuthor: "Author", - bankTaxesLookupButton: "Inspect", - bankTaxesLookupCarbon: "Carbon footprint", - bankTaxesLookupEcoin: "ECO Tax", - bankTaxesLookupNotFound: "No block found in your local feed with that ID.", - bankTaxesLookupPlaceholder: "Block ID (e.g. %abc...=.sha256)", - bankTaxesLookupSize: "Size", - bankTaxesLookupTitle: "Inspect a block", - bankTaxesLookupType: "Type", - bankTaxesMonthlyEcoin: "ECO Tax (monthly)", - bankTaxesRate: "Rate", - bankTaxesSpan: "Sampling span", - bankTaxesTotalAnnual: "Total (annual)", - bankTaxesTotalBlocks: "Total blocks", - bankTaxesTotalBytes: "Total bytes", - bankTaxesTotalCarbon: "Total carbon", - bankTaxesTotalEcoin: "Total ECO tax (lifetime)", - bankTaxesTotalLifetime: "Total (lifetime)", - bankTaxesTotalMonthly: "Total (monthly)", - bankTaxesTypesApply: "Set my taxes", - bankTaxesTypesLabel: "Select which taxes you want to pay", - bankTaxesUserAmount: "Your ECO tax (price to return)", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", - blockchain: "BlockExplorer", - blockchainBackToBlockexplorer: "Back to blockexplorer", - blockchainBlockNotAvailable: "This block is not available in your local feed. It may belong to a peer you are not yet replicating from.", - courtsMediatorsLabel: "Mediators", - ecoTaxLabel: "ECO Tax", - gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - melodyAllEmpty: "No BCS compositions from other inhabitants yet.", - melodyByLabel: "By", - melodyNoteUnavailable: "block not in your local feed", - melodyUploadBcsButton: "Publish", - melodyUploadBcsNameNote: "The audio will be published with the auto-generated name", - melodyUploadBcsTitle: "Publish", - melodyUploadStegoLabel: "Hidden message (steganography)", - melodyUploadStegoMaxLabel: "max 280 characters", - melodyUploadStegoPlaceholder: "Optional.", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", - parliamentRevocationPublish: "Publish Revocation", - statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", - statsPad: "Pads", - statsPadEntry: "Pad entries", - statsTombstoneEmpty: "No tombstones in the network yet.", - typeGameScore: "GAME SCORES", + bankRulesWMax: "Gehieneko pisua (w_max)", + bankRulesWMin: "Gutxieneko pisua (w_min)", + bankTaxes: "Zergak", + bankTaxesAnnualEcoin: "ECO tasa (urtekoa)", + bankTaxesArchTaxAnnual: "ARCH tasa (urtekoa)", + bankTaxesArchTaxFirstBlock: "Zure lehen blokearen adina", + bankTaxesArchTaxFootprintNote: "ARCH tasak sarearen artxiboa hazteko eta mantentzeko kostua islatzen du. Argitaratu zenuen lehen blokearen eta sareko bloke berrienaren arteko denbora-tartetik kalkulatzen da — zenbat eta denbora gehiago egon zure historia artxiboan, orduan eta handiagoa izango da mantentze-kostuan duzun zatia.", + bankTaxesArchTaxLifetime: "ARCH tasa (bizitza osoa)", + bankTaxesArchTaxMonthly: "ARCH tasa (hilekoa)", + bankTaxesArchTaxNoteBold: "ARCH tasa zuzenean murrizteari", + bankTaxesArchTaxNoteIntro: "Zure ARCH tasa sareak zure OBUaren gainetik sortzen duen soberakinetik kentzen da; OBUaren oinarrizko balioa minimo aldaezin gisa finkatzen da. Tasa ez da inoiz kentzen bizilagun bakoitzari OBU gisa dagokion zenbateko finkotik. Kendutako funtsek aberastasuna birbanatzeko algoritmoa elikatzen dute, eta beste bizilagun batzuengana itzultzen dira haien OBU eskaeren bidez. Bizilagun berrienek eta artxibatze-, erreplikazio- eta mantentze-lanetan berariaz aritzen direnek ziurrenik izango dute zure izenean sarearen artxiboa osasuntsu mantentzeko zereginen bat. Edo ", + bankTaxesArchTaxNoteOutro: " ekin diezaiokete, inausketa-akordioen, datu erredundanteen desbikoizketaren eta mantentze-azpiegitura partekatuaren bidez, eta sarearen adostasunak haien ekarpen jarraitua eska dezake.", + bankTaxesArchTaxRate: "Tarifa", + bankTaxesArchTaxTitle: "ARCH tasa", + bankTaxesArchTaxUserAmount: "Zure ARCH tasa (itzuli beharreko prezioa)", + bankTaxesEcoTaxLifetime: "ECO tasa (bizitza osoa)", + bankTaxesEcoTaxTitle: "ECO tasa", + bankTaxesLookupAuthor: "Egilea", + bankTaxesLookupButton: "Ikuskatu", + bankTaxesLookupCarbon: "Karbono-aztarna", + bankTaxesLookupEcoin: "ECO tasa", + bankTaxesLookupNotFound: "Ez da ID hori duen blokerik aurkitu zure jario lokalean.", + bankTaxesLookupPlaceholder: "Blokearen IDa (adib. %abc...=.sha256)", + bankTaxesLookupSize: "Tamaina", + bankTaxesLookupTitle: "Ikuskatu bloke bat", + bankTaxesLookupType: "Mota", + bankTaxesMonthlyEcoin: "ECO tasa (hilekoa)", + bankTaxesRate: "Tarifa", + bankTaxesSpan: "Lagintze-tartea", + bankTaxesTotalAnnual: "Guztira (urtekoa)", + bankTaxesTotalBlocks: "Blokeak guztira", + bankTaxesTotalBytes: "Byteak guztira", + bankTaxesTotalCarbon: "Karbonoa guztira", + bankTaxesTotalEcoin: "ECO tasa guztira (bizitza osoa)", + bankTaxesTotalLifetime: "Guztira (bizitza osoa)", + bankTaxesTotalMonthly: "Guztira (hilekoa)", + bankTaxesTypesApply: "Ezarri nire zergak", + bankTaxesTypesLabel: "Hautatu zein zerga ordaindu nahi dituzun", + bankTaxesUserAmount: "Zure ECO tasa (itzuli beharreko prezioa)", + bankTaxesUserNoteChips: "Elementu bakoitzaren ondoan agertzen den ECO tasaren txipak kolorez aldatzen du (berdea / horia / gorria), elementuaren tamainaren arabera sarean inoiz ikusitako blokerik handienarekin alderatuta, bizilagun aktiboen kopuruak leunduta — sare solar-punk handiago batek karga banatzen du eta elementu gehiago eramaten ditu banda berdera.", + bankTaxesUserNoteOtherParams: "ECO tasa gaur egun bloke bakoitzaren karbono-aztarnatik ateratzen da, baina denborarekin beste parametro batzuk gehi ditzake — erreplikazioan gastatutako energia, nodoen arteko biltegiratze erredundantea, blob-en banda-zabalera, deszifratzearen kostu konputazionala, lotutako transakzioen meatzaritza-aztarna edo sareak baloratzea adosten duen beste edozein karga neurgarri.", + blockchain: "Bloke-esploratzailea", + blockchainBackToBlockexplorer: "Itzuli bloke-esploratzailera", + blockchainBlockNotAvailable: "Bloke hau ez dago eskuragarri zure jario lokalean. Oraindik erreplikatzen ez duzun nodo batena izan daiteke.", + courtsMediatorsLabel: "Bitartekariak", + ecoTaxLabel: "ECO tasa", + gamesHallDate: "Data", + gamesNewRecord: "Errekor berria!", + indexingRefreshNote: "Orri hau 10 segundoro birkargatzen da.", + indexingMessage: "Oasis nodoen sare banatua sinkronizatzen ari da. Itxaron, mesedez.", + indexingTitle: "Sinkronizatzen", + melodyAllEmpty: "Oraindik ez dago beste bizilagunen BCS konposiziorik.", + melodyNoteUnavailable: "blokea ez dago zure jario lokalean", + melodyUploadBcsButton: "Argitaratu", + melodyUploadBcsNameNote: "Audioa automatikoki sortutako izenarekin argitaratuko da", + melodyUploadBcsTitle: "Argitaratu", + melodyUploadStegoLabel: "Mezu ezkutua (esteganografia)", + melodyUploadStegoMaxLabel: "gehienez 280 karaktere", + melodyUploadStegoPlaceholder: "Aukerakoa.", + padsNoItems: "Ez da padik aurkitu.", + padVersionHistory: "Bertsioen historia", + padAttributionView: "Egilearen arabera", + padVersionView: "Ikusi", + parliamentRevocationPublish: "Argitaratu ezeztapena", + statsChat: "Txatak", + statsChatMessage: "Txat-mezuak", + statsGameScore: "Jokoak", + statsWiki: "Wiki orriak", + statsPad: "Padak", + statsPadEntry: "Pad-sarrerak", + statsTombstoneEmpty: "Oraindik ez dago ezabaketarik (tombstone) sarean.", + typeGameScore: "JOKO-PUNTUAZIOAK", bankTaxesLookupNote: "Sartu bloke-ID bat zure jario lokalean bilatzeko.", bankTaxesUserNoteChipsClick: "Egin klik edozein chiptan bere blokea blockexplorer-en ikuskatzeko.", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "Gonbidapen kodea", larpRulesInviteEntryText: "Etxeko kideek gonbidapen kodeak sor ditzakete, etxerako sarrera zuzena ematen dutenak.", larpRulesOneHouseText: "Une bakoitzean etxe bakar batekoa izan zaitezke gehienez. Inori ez baduzu, ACADEMIA-n zaude. ACADEMIA ez den etxeko orri bakoitzak \"Etxetik atera\" botoia erakusten die kideei, kidetza ACADEMIA-ra berrezartzen duena. Ateratzea EZ du proba-itxaronaldia indargabetzen.", - larpRulesOneHouseTitle: "Etxe bana aldi bakoitzean", - larpRulesTestEntry: "WILL proba", - larpRulesTestEntryText: "Aukera bakoitzak etxe bati edo gehiagori pisua ematen die; bidaltzean, sistemak puntuak batzen ditu eta puntuazio handiena duen etxean automatikoki onartzen zaitu.", + larpRulesTestEntryText: "WILL testean aukera bakoitzak etxe bat edo gehiago haztatzen ditu; bidaltzean, sistemak puntuazioak batzen ditu eta puntu gehien dituen etxean onartzen zaitu automatikoki.", larpWillTestHint: "Behin osatutakoan, zure erantzunekin ondoen bat datorren etxera esleituko zaitu. Zure erantzun indibidualak lokalki prozesatzen dira eta inoiz ez dira gordetzen — soilik etxe-esleipenaren emaitza argitaratzen da zure jarioan.", larpWillTestTitle: "WILL proba", - settingsLanDesc: "Aldian behin iragarri pareko hau sare lokal bereko beste Oasis instantziei. Desgaitu UDP igorpenak gelditzeko.", + settingsLanDesc: "Aldian behin iragarri pareko hau sare lokal bereko beste Oasis instantziei.", settingsLanEnable: "Gaitu LAN igorpena", settingsLanTitle: "LAN igorpena", settingsReplicationTitle: "Erreplikazioa", settingsReplicationDesc: "Konfiguratu zenbat jauzi jarraitu behar dituen zure kideak zure jariotik abiatuz edukia erreplikatzean.", settingsReplicationHopsLabel: "Jauziak", + settingsBlobCacheTitle: "Multimedia cachea", + settingsBlobCacheDesc: "Beste biztanleen edukia gailu honetan gordetzen da. Aukeratu zenbat leku har dezakeen.", + settingsBlobCacheUsage: "Uneko erabilera", + settingsBlobCacheFiles: "fitxategi", + settingsBlobCacheLimit: "Muga", + settingsBlobCacheUnlimited: "Mugarik gabe", + settingsBlobCacheCleanNow: "Garbitu orain", + settingsBlobCacheCleaned: "{n} fitxategi ezabatuta, {size} askatuta.", aiApproveTagsLabel: "Etiketak (komaz bereizita)", aiApproveTagsPlaceholder: "adib. oasis, gobernantza, ekologia", aiApproveRatingLabel: "Balorazioa", @@ -3748,31 +4758,25 @@ module.exports = { aiExchangeMarkHelpful: "+1 lagungarri", larpCyclesUntilRuling: "Hurrengo gobernu-zikloa", larpVisitTribe: "Tribua bisitatu", - larpGoverning: "Agintzen:", + larpStartJourney: "Hasi bidaia", + larpGoverning: "Agintean:", + larpStartHere: "Hasi hemen:", larpMyHouse: "Nire Etxea:", larpMembersCount: "Kideak", - larpMembersTitle: "Kideak", - larpNoMembers: "Oraindik ez dago kiderik.", larpRolesLabel: "Rolak", larpFunctionLabel: "Funtzioa", larpMonthLabel: "Gobernu-zikloa", larpLeaveToAcademia: "Itzuli ACADEMIA-ra", - larpAcademiaJoinTitle: "Etxe batera sartu", - larpAcademiaJoinHint: "Egin profil psikologikoaren proba zuri ondoen egokitzen zaizun etxera esleitua izateko, edo erabili etxeko kide batek partekatutako gonbidapen-kodea.", - larpTakeTestButton: "Profil-proba egin", + larpAcademiaJoinTitle: "L.A.R.P. Etxeak", larpInviteRedeem: "Etxera sartu", larpInviteCreate: "Gonbidapen-kodea sortu", larpInvitePlaceholder: "Gonbidapen-kodea", larpInviteBannerTitle: "Gonbidapen-kode berria", larpInviteBannerHint: "Partekatu kode hau ACADEMIA-n dagoen norbaitekin. 30 zikloko edo erabilera bakar baten ostean iraungitzen da.", - larpTestAssignedHouse: "Esleitutako etxea", larpTestRankingTitle: "Etxe bakoitzeko puntuazioa", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "Etxearen gonbidapen-kodea", - invitesHouseJoinButton: "Etxera sartu", larpLastAttemptTitle: "Zure azken saiakera", larpLastAttemptHouse: "Etxea", - larpLastAttemptResult: "Emaitza", larpLastAttemptWhen: "Noiz", larpLastAttemptCooldown: "Hurrengo saiakera", larpTestTitle: "Sarrera-proba", @@ -3781,46 +4785,21 @@ module.exports = { larpTestCooldownActive: "Hurrengo proba erabilgarri", larpTestCooldownDays: "ziklo", larpTestResultTitle: "Probaren emaitza", - larpTestPassed: "Proba gainditua — ongi etorri!", - larpTestFailed: "Proba ez da gainditu", larpTestScore: "Puntuazioa", larpTestNextAttempt: "Beste proba bat egin dezakezu 30 ziklo barru.", larpGoToHouse: "Joan zure etxera", larpBackToAcademia: "Itzuli ACADEMIA-ra", - larpBackToHouses: "◀ Etxeak", larpBadgeYou: "Zu", larpBadgeRuling: "Agintzen", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Oasis-en zuzeneko rol-joko geruzaren modulua (9 Etxeak).", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - ecoinTaxLabel: "ECOin Tax", - bankTaxesNetworkTitle: "Network Taxes", - bankTaxesUserTitle: "Your taxes", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - statsEcoTaxTitle: "ECO Tax", - audioTranscodeTitle: "BCS Transcode", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - audioTranscodeStegoTitle: "Embedded in the waveform", - audioBackToAudio: "Back to audio", - audioAuthorLabel: "Author", - courtsEvidenceFileLabel: "Evidence file (image, audio, video or PDF)", - courtsCaseMediators: "Mediators", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "Zuzeneko rol-jokoaren geruzarako modulua.", larpProfileQ1: "Arazo konplexu baten aurrean, lehenik zer egiten duzu?", larpProfileQ1O1: "Besteekin hitz egin adostasuna lortzeko", larpProfileQ1O2: "Prototipo bat egin probatzeko", larpProfileQ1O3: "Literatura ikertu", larpProfileQ1O4: "Sortzen duen sistema sabotatu", larpProfileQ2: "Zer ematen dio zentzua zure eguneroko lanari?", - larpProfileQ2O1: "Maite ditudan pertsonak defendatu", + larpProfileQ2O1: "Maite ditudan biztanleak defendatu", larpProfileQ2O2: "Zerbait ukigarria sortu", larpProfileQ2O3: "Bizitza sendatu edo zaindu", larpProfileQ2O4: "Hitzak eta ideiak landu", @@ -3887,7 +4866,52 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "Aukeratu nahi duzun UX nabigazio modua zure interfazerako.", uxModeMenus: "Blokeak", - uxModeAINav: "AI", + uxModeChats: "Elkarrizketak", + uxModeFeed: "Mikroblogintza", + feedTrendingTitle: "Joerako Etiketak", + feedActiveTitle: "Biztanle Aktiboak", + cnBrandSub: "Librea · P2P · Federatua", + cnSyncedPeers: "Sinkronizatutako nodoak", + cnLicense: "Lizentzia", + cnNotAccessible: "Edukia ez dago eskuragarri une honetan.", + cnUntitled: "Izenbururik gabe", + cnSearchPlaceholder: "Bilatu…", + cnHubTitle: "Clearnet HUBa", + cnHubDescription: "Oasis sare honetako bizilagunek partekatutako eduki publikoa.", + cnHubEmpty: "Oraindik ez da eduki publikorik partekatu Clearnet-en.", + cnPublicContent: "Edukia", + cnTribesContent: "Tribuak", + cnNetworkContent: "Sarea", + cnCategoryEmpty: "Ez dago edukirik kategoria honetan.", + cnInhabitantEmpty: "Bizilagun honek oraindik ez du edukirik argitaratu Clearnet-en.", + cnPrevPage: "← Aurrekoa", + cnNextPage: "Hurrengoa →", + cnPageOf: "{page}. orria {pages}-tik", + cnMoreInfo: "Informazio gehiago →", + cnOpenCourse: "Ikastaro irekia", + cnAuction: "ENKANTEA", + cnExchange: "TRUKEA", + cnRssDescription: "HUB honetako bizilagunek argitaratuak", + cnKindAudio: "Audioa", + cnKindBlog: "Bloga", + cnKindBookmark: "Laster-marka", + cnKindDocument: "Dokumentua", + cnKindEvent: "Ekitaldia", + cnKindFeed: "Jarioa", + cnKindImage: "Irudia", + cnKindJob: "Lana", + cnKindMarket: "Merkatua", + cnKindPodcast: "Podcasta", + cnKindProject: "Proiektua", + cnKindCourse: "Ikastaroa", + cnKindShop: "Denda", + cnKindTorrent: "Torrenta", + cnKindFile: "Fitxategia", + cnKindVideo: "Bideoa", + cnKindWiki: "Wikia", + cnKindRoom: "Gela", + cnKindMap: "Mapa", + uxModeAINav: "IA", } }; diff --git a/src/client/assets/translations/oasis_fr.js b/src/client/assets/translations/oasis_fr.js index af3da9d..00cfd95 100644 --- a/src/client/assets/translations/oasis_fr.js +++ b/src/client/assets/translations/oasis_fr.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { fr: { - bbAdd: "Ajouter", - bbQuickActions: "Actions rapides", - bbPickTitle: "Épingler un module", - bbPickDesc: "Choisis un module pour l'emplacement + de la barre inférieure.", - bbPickNone: "Aucun", - bbManage: "Modifier", - bbYourBar: "Ta barre", - bbRemove: "Retirer", - bbFull: "Maximum atteint", - bbDone: "Terminé", - activityChangeFilter: "Changer de filtre", - emptyConnectHint: "Connecte-toi à un pub pour synchroniser avec le réseau.", - emptyConnectCta: "Se connecter à un pub", - menuCommunity: "Communauté", - activityGroupCommunity: "Communauté et gouvernance", - activityGroupOrg: "Organisation", - activityGroupComm: "Communication", - activityGroupEconomy: "Économie", - activityGroupMedia: "Contenu et médias", - activityGroupOther: "Autres", - languageName: "Français", shopOrderStatusPaid: "Paiement reçu", shopOrderStatusReceived: "Reçu", shopOrderMarkPaid: "Marquer paiement reçu", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "Achats", shopMyOrdersDescription: "Vos commandes d’achat.", shopMyOrdersEmpty: "Vous n’avez pas encore d’achats.", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PM Vendeur", shopOrderSeller: "Vendeur", - shopOrderPaymentConcept: "Paiement", shopOrderInvoice: "Facture", shopOrderInvoiceConcept: "Facture", shopOrderStatusPending: "En attente", shopOrderStatusAccepted: "Accepté", shopOrderStatusRejected: "Rejeté", shopOrderStatusShipped: "Expédié", - shopOrderStatusDelivered: "Livré", shopOrderAccept: "Accepter", shopOrderReject: "Rejeter", shopOrderMarkShipped: "Marquer expédié", - shopOrderMarkDelivered: "Marquer livré", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "Contrat", + shopOrderPmBuyer: "PM Acheteur", shopOrderContractConcept: "Commande boutique", shopOrdersPending: "Commandes en attente", all: "Tous", @@ -53,7 +28,7 @@ module.exports = { recent: "Récents", anonymous: "Anonyme", unnamed: "Anonyme", - authorLabel: "AUTEUR", + authorLabel: "Auteur", delete: "Supprimer", save: "Enregistrer", vote: "Voter", @@ -75,18 +50,17 @@ module.exports = { viewJson: "Voir le JSON", matchScore: "Score de correspondance", preferencesLabel: "Préférences", - inhabitantProfileTitle: "Profil de l’habitant", + inhabitantProfileTitle: "Habitant", contentWarningLabel: "Avertissement de contenu", invalidPost: "Contenu invalide", invalidPostHint: "Ce message a un texte vide/invalide.", - spreadBy: "Par", spreadChron: "Diffusion", spreaded: "Diffusé", - spreadMore: "plus", spreadContentUnavailable: "Contenu pas encore disponible (réplication en attente)", filteredByTag: "Filtré par étiquette", filteredByTagTitle: "Filtré par étiquette", feedPublishedSuccess: "Flux publié avec succès !", + feedMastodonFailed: "Publié dans Oasis, mais l'envoi vers Mastodon a échoué.", tribeFeedSent: "Message envoyé avec succès !", tribeContentEncrypted: "Contenu chiffré", tribeTorrentsEmpty: "Pas encore de torrents.", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "Aucune gravité", calendarDeleteDate: "Supprimer la date", calendarIntervalUntil: "Jusqu’au", - bookmarkCategory: "Catégorie", bookmarkLastVisit: "Dernière visite", profileClearnetBookmarksLabel: "Favoris", - forumFilterHot: "Populaires", invitesPort: "Port", - currentlyUnrecheable: "ERREUR !", - peerHostValidation: "IPv4 valide (ex. 192.168.1.100) ou nom d’hôte (ex. pub.example.com)", + peerHostValidation: "IPv4 valide (ex. 192.168.1.100) ou nom d’hôte (ex. pub.solarnethub.com)", peerPortValidation: "Port 1-65535", peerKeyValidation: "Clé publique SSB ed25519 (@<44 caractères base64>=.ed25519)", inboxJobUnsubscribedTitle: "Désinscription de votre offre d’emploi", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "Description de la tâche", exportPasswordLabel: "Mot de passe d’export", importPasswordPlaceholder: "Saisissez le mot de passe", - searchCasesPlaceholder: "Rechercher des affaires…", + searchCasesPlaceholder: "Rechercher dans Tribunaux...", shopBuyDeliveryAddressPlaceholder: "Adresse de livraison", shopBuyNotesPlaceholder: "Notes", bankTaxesUserNoteBold: "réduire directement la taxe ECO", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "Soutenir l’affaire", courtsSettlementAcceptBtn: "Accepter l’accord", courtsSupportCount: "Soutiens", - courtsVerdictVoteTitle: "Voter sur le verdict", - courtsVerdictVoteLabel: "Vote du verdict", - courtsVerdictVoteAccept: "Accepter", - courtsVerdictVoteReject: "Rejeter", - courtsVerdictVoteSubmit: "Soumettre le vote", reportsSetStatus: "Définir le statut", reportsStatusClosed: "CLÔTURÉ", statsAvgPerInhabitant: "Moy. par habitant", statsCarbonMaxAnnual: "Estimation max annuelle", statsCarbonNetwork: "Total du réseau", statsCarbonOfEstMax: "de la capacité max estimée", + statsCarbonYearProgress: "de l'année écoulée", + statsNetworkTitle: "Réseau", + statsStorageTitle: "Stockage", + statsEcoTaxTitle: "Taxe ECO", + statsAccountTitle: "Compte", + statsAveragesTitle: "Moyennes", statsCarbonOfNetwork: "du total du réseau", statsCarbonUser: "Votre empreinte", statsContentCountColumn: "Nombre", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "Principales étiquettes", statsTopTypesTitle: "Principaux types de contenu", statsTotalMsgs: "Total des messages", - feedViewDetails: "Voir les détails", - eventFileLabel: "Joindre une image", - taskFileLabel: "Joindre une image", - courtsRespondentRequired: "L'identifiant du défendeur est requis", extended: "Multivers", extendedDescription: [ "Lorsque vous soutenez quelqu’un, vous pouvez télécharger les publications des habitants qu’il soutient, et ces publications apparaîtront ici, triées de la plus récente.", @@ -215,40 +183,70 @@ module.exports = { ], profile: "Avatar", inhabitants: "Habitants", - peersReplicatedFeeds: "Flux répliqués", + peersReplicatedFeeds: "Répliqués", + peersTorColumn: "TOR", + peersTorYes: "Joignable via Tor", + peersTorNo: "Non joignable via Tor", + peersOasisId: "ID", + peersOasisVersion: "Version", graphos: "Graphos", graphosDescription: "Carte interactive du réseau autour de toi.", + graphosFederation: "Fédération", + graphosFedPubs: "PUBs", + graphosFedUnreachable: "Injoignables", + graphosFedLinks: "Liens mutuels", + graphosFedIsolated: "Isolés", + graphosFedNoData: "Pas de données", + graphosFedMutual: "Mutuel", + graphosFedOneWay: "Sens unique", + graphosFedTransport: "Réseau", + graphosFedWith: "Fédéré avec", + graphosZoom: "Agrandir", + graphosFedInhabitants: "Habitants", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "Votre nœud ne connaît encore aucun PUB.", graphosViewingGraphOf: "Affichage", graphosBackToMine: "← Mon réseau", graphosShowing: "Affichage", graphosYou: "Toi", graphosTotalNodes: "Nœuds totaux", - manualMode: "Mode Manuel", mentions: "Mentions", mentionsDescription: [ - strong("Publications qui vous @mentionnent"), - ", triées des plus récentes.", + strong("Chaque message qui vous @mentionne"), + ".", ], - nextPage: "Suivant", - previousPage: "Précédent", noMentions: "Vous n’avez pas encore reçu de @mentions.", + mentionsSearchPlaceholder: "Rechercher dans Mentions...", privateReminders: "RAPPELS", + inboxFilterPms: "MPs", + inboxFilterNotifications: "Notifications", + inboxMarkRead: "Marquer comme lu", + inboxMarkUnread: "Marquer comme non lu", + inboxUnreadChip: "Non lu", + inboxMarkAllRead: "Tout marquer comme lu", + inboxDeleteShown: "Tout supprimer", + inboxArchive: "Archiver", + inboxUnarchive: "Désarchiver", + inboxFilterArchived: "Archivés", + inboxEarlierMessages: "messages précédents", + inboxSortThread: "Par fil", + inboxSortRecent: "Plus récents d'abord", + inboxAllBots: "Bots", + settingsInboxBotsTitle: "Notifications de l'Inbox", + settingsInboxBotsDesc: "Choisissez quels bots peuvent vous notifier dans votre Inbox. Les bots décochés sont masqués et leurs avis ne sont pas comptés.", inboxFiltersLabel: "Filtres :", inboxToggleToMutuals: "Passer à mutuels", inboxToggleToWhole: "Passer à tous", - privateFrom: "De", - privateTo: "Pour", privateDate: "Date", peers: "Nœuds", searchDescription: "Description", - imageSearch: "Rechercher des images", searchFromLabel: "De", searchToLabel: "À", - searchMinOpinionsLabel: "Min opinions", - searchInhabitantLabel: "Habitant (Oasis ID)", searchQueryLabel: "Recherche", - searchPerPageLabel: "Résultats par page", + searchPerPageLabel: "Résultats", settings: "Configuration", + settingsDescription: "Configurez l'environnement de votre réseau.", continueReading: "Continuer la lecture", moreComments: "plus de commentaires", readThread: "lire le reste du fil", @@ -257,67 +255,46 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Joue la mélodie de ta blockchain — chaque bloc devient une note.', melodyEmpty: 'Pas encore de notes — ta blockchain n’a pas encore produit de bloc.', - melodyCompositionHelp: 'Chaque bloc de ta blockchain devient une note musicale selon son type et sa taille.', - melodyStatsTitle: 'Répartition par type', - melodyInhabitantLabel: 'Habitant', melodyTotalBlocks: 'Notes', - melodyType: 'Type', - melodyCount: 'Blocs', melodyFilterAll: 'TOUTES', - melodyFilterShare: 'Téléverser une mélodie', - melodyShareTitle: 'Partage ta mélodie', - melodyShareHelp: 'Publie un instantané de ta mélodie actuelle pour que d’autres puissent l’écouter et la remixer.', - melodyShareTitleLabel: 'Titre (optionnel)', - melodyShareTitlePlaceholder: 'Un fantôme de blockchain', - melodyShareSubmit: 'Publier la mélodie', - melodyNoShared: 'Aucune mélodie blockchain pour l’instant.', melodyRegenerate: 'Régénérer', melodyDownload: "Télécharger BCS", - profileActivityTitle: 'Activité', - profileActivityMore: 'Voir toute l’activité', profileContentSectionTitle: 'Contenu de l’avatar', profileContentHelp: 'Choisis les modules à afficher sur ton avatar.', profileSensorsHelp: 'Métriques optionnelles affichées sur ton avatar.', profileClearnetHelp: 'Modules accessibles depuis l’extérieur d’Oasis.', profileHubAll: 'Tout', - profileHubTitle: 'Contenu Public', - footerPulseTitle: 'Pouls', - footerPulsePeers: 'Pairs', - footerPulseInbox: 'Boîte', - footerPulseSync: 'Dernière sync', - footerPulseEcoValue: 'ECO/h', - footerPulseLastActivity: 'Dernière activité', footerLicenseLabel: 'Licence', - profileSwitchToOasis: 'Retour à Oasis', - profileSwitchToClearnet: 'Plonger dans le Clearnet', - profileVisibilityFediverse: "Fediverse", - profileSwitchToFediverse: "Plonger dans le fédiverse", - coordLabel: 'Coordonnées (ex., A3)', - coordPlaceholder: 'Saisissez une coordonnée', + profileVisibilityFediverse: "Multivers", contributorsTitle: "Contributions", - pixeliaBy: "par", colorLabel: 'Choisir une couleur', paintButton: 'Dessiner !', - invalidCoordinate: 'Coordonnée incorrecte', - goToMuralButton: "Voir la fresque", totalPixels: 'Total de pixels', modules: "Modules", modulesViewTitle: "Modules", modulesViewDescription: "Définissez votre environnement idéal en activant et désactivant des modules.", inbox: "Boîte", + inboxSearchPlaceholder: "Rechercher dans Boîte...", multiverse: "Multivers", - fediverse: "Fediverse", - fediverseDescription: "Gérez vos autres comptes du fédiverse, y compris l'envoi et la réception de contenu.", + fediverse: "Multivers", + fediverseDescription: "Gérez vos autres comptes du multiverse, y compris l'envoi et la réception de contenu.", fediverseStatus: "Statut", - fediverseDisconnected: "Fédiverse déconnecté. Vérifiez %LINK% ou l'état de la connexion.", - fediverseSettingsTitle: "Fediverse", + fediverseDisconnected: "Multiverse déconnecté. Vérifiez %LINK% ou l'état de la connexion.", + fediverseSettingsTitle: "Multivers", fediverseInstanceLabel: "Adresse", fediverseTokenLabel: "Jeton d'accès", - fediverseTokenHelp: "Définissez votre jeton d'accès. Il servira à gérer votre compte Mastodon depuis Oasis.", fediverseConnect: "Connecter", fediverseConnectedAs: "Connecté en tant que", + nextcloudServerLabel: "Serveur", + nextcloudUserLabel: "Utilisateur", + nextcloudFolderLabel: "Dossier", + nextcloudUploadButton: "Envoyer", + nextcloudFileLabel: "Choisissez un fichier", + fileExternalLink: "Lien externe", + nextcloudErrConnect: "Impossible de joindre Nextcloud avec ces informations.", + nextcloudErrUpload: "L'envoi vers Nextcloud a échoué.", + nextcloudErrShare: "Nextcloud n'a pas donné de lien public pour le fichier.", fediverseDisconnect: "Déconnecter", - fediverseEmpty: "Lorsque vous connectez d'autres comptes du fédiverse depuis %LINK%, vous pouvez les gérer ici.", fediverseEmptyLink: "vos réglages", fediverseComposePlaceholder: "À quoi pensez-vous ?", fediverseReplyPlaceholder: "Écrivez votre réponse…", @@ -326,21 +303,15 @@ module.exports = { fediverseReply: "Répondre", fediverseBoosted: "a partagé", fediverseNoPosts: "Votre fil est vide.", - fediverseThread: "Fil", fediverseTimeline: "Fils", - fediverseManageTimeline: "Gérer le fil", fediverseFollowers: "Abonnés", fediverseFollowing: "Abonnements", fediversePosts: "Publications", fediverseJoined: "Inscrit", - fediverseOverview: "Vue d'ensemble", fediverseRefresh: "Actualiser", fediverseWrite: "Écrire", fediversePreview: "Aperçu", - fediverseEdit: "Modifier", - fediverseOpenFeed: "Voir le flux", fediverseManage: "Gérer", - fediverseStatusNotConnected: "Non connecté", fediverseError: "Impossible de contacter Mastodon.", fediverseErrInstance: "URL d'instance invalide.", fediverseErrAuth: "Jeton invalide ou expiré.", @@ -351,17 +322,76 @@ module.exports = { fediverseErrPost: "Impossible de publier.", fediverseErrMedia: "Impossible d'envoyer le fichier.", fediverseErrEmpty: "Écrivez quelque chose ou joignez un fichier.", - popularLabel: "À la une", - topicsLabel: "Sujets", - latestLabel: "Derniers", - summariesLabel: "Résumés", - threadsLabel: "Fils", - multiverseLabel: "Multivers", - inboxLabel: "Boîte", - invitesLabel: "Invitations", - walletLabel: "Portefeuille", - legacyLabel: "Clés", - cipherLabel: "Chiffreur", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Numéro de téléphone", + telegramSendCode: "Envoyer le code", + telegramCodeHelp: "Saisissez le code que Telegram vient de vous envoyer (application ou SMS).", + telegramCodeLabel: "Code", + telegramPasswordHelp: "Ce compte utilise la validation en deux étapes. Saisissez votre mot de passe.", + telegramPasswordLabel: "Mot de passe", + telegramVerify: "Vérifier", + telegramCancel: "Annuler", + telegramChats: "Discussions", + telegramUnread: "Non lus", + telegramOpen: "Ouvrir", + telegramSend: "Envoyer", + telegramYou: "Vous", + telegramNoChats: "Aucune discussion pour l'instant.", + telegramNoMessages: "Aucun message pour l'instant.", + telegramComposePlaceholder: "Écrivez un message…", + telegramChannel: "Chaîne", + telegramGroup: "Groupe", + telegramPrivate: "Privé", + telegramErrConnect: "Impossible de contacter Telegram.", + telegramErrTimeout: "Telegram a mis trop de temps à répondre.", + telegramErrAuth: "Session Telegram invalide ou expirée. Reconnectez-vous.", + telegramErrFetch: "Impossible de charger les discussions.", + telegramErrSend: "Impossible d'envoyer le message.", + telegramErrMissing: "API ID, API hash et numéro de téléphone sont requis.", + telegramErrCode: "Code invalide. Réessayez.", + telegramErrCodeExpired: "Le code a expiré. Recommencez.", + telegramErrPassword: "Mot de passe incorrect.", + telegramErrPhone: "Numéro de téléphone invalide.", + telegramErrApi: "API ID ou API hash invalide.", + telegramErrFlood: "Trop de tentatives. Patientez puis réessayez.", + telegramErrNoLogin: "Aucune connexion en cours. Recommencez.", + peertubeUsernameLabel: "Nom d'utilisatrice", + peertubePasswordLabel: "Mot de passe", + peertubeSubscriptions: "Abonnements", + peertubeMyVideos: "Mes vidéos", + peertubeVideos: "Vidéos", + peertubeFollowers: "Abonné·es", + peertubeViews: "vues", + peertubeLikes: "j'aime", + peertubeQuota: "Quota", + peertubeUpload: "Téléverser une vidéo", + peertubeUploadHelp: "Choisissez un fichier vidéo. Il est envoyé sur votre chaîne et PeerTube le traite ensuite.", + peertubeTitleLabel: "Titre", + peertubeDescriptionLabel: "Description", + peertubePrivacyLabel: "Confidentialité", + peertubePrivacyPublic: "Publique", + peertubePrivacyUnlisted: "Non listée", + peertubePrivacyPrivate: "Privée", + peertubeNoVideos: "Aucune vidéo pour l'instant.", + peertubeComments: "Commentaires", + peertubeComment: "Commenter", + peertubeCommentPlaceholder: "Écrire un commentaire…", + peertubeNoComments: "Aucun commentaire pour l'instant.", + peertubeOpen: "Ouvrir sur PeerTube", + peertubeWatch: "Regarder", + peertubeChannel: "Chaîne", + peertubeNoStream: "Cette vidéo ne peut pas encore être lue ici. Ouvrez-la sur PeerTube.", + peertubeUploaded: "Vidéo envoyée. PeerTube la traite.", + peertubeErrConnect: "Impossible de contacter PeerTube.", + peertubeErrAuth: "Identifiant ou mot de passe incorrect, ou session expirée. Reconnectez-vous.", + peertubeErrInstance: "URL d'instance invalide.", + peertubeErrMissing: "Instance, identifiant et mot de passe sont requis.", + peertubeErrFetch: "Impossible de charger les vidéos.", + peertubeErrUpload: "Impossible d'envoyer la vidéo.", + peertubeErrTitle: "Titre requis.", + peertubeErrComment: "Impossible de publier le commentaire.", + peertubeErrNoFile: "Choisissez un fichier vidéo.", bookmarksLabel: "Marque-pages", videosLabel: "Vidéos", torrentsLabel: "Torrents", @@ -372,34 +402,22 @@ module.exports = { inhabitantsLabel: "Habitants", trendingLabel: "Tendances", eventsLabel: "Événements", - tasksLabel: "Tâches", apply: "Appliquer", menuPersonal: "Personnel", - menuContent: "Contenu", - menuBlogs: "Blogs", menuGovernance: "Gouvernance", menuOffice: "Bureau", - menuMultiverse: "Multivers", - menuNetwork: "Réseau", + menuNetwork: "Communauté", menuCreative: "Créatif", menuEconomy: "Économie", - menuMedia: "Médias", + menuMedia: "Bibliothèque", menuTools: "Outils", comment: "Commenter", subtopic: "Sous-sujet", json: "JSON", - createdBy: "par", unfollow: "Ne plus soutenir", follow: "Soutenir", block: "Bloquer", unblock: "Débloquer", - newerPosts: "Nouveaux posts", - olderPosts: "Anciens posts", - feedRangeEmpty: "La plage appliquée est vide pour ce fil. Essayez de voir le ", - seeFullFeed: "fil complet", - feedEmpty: "Le réseau Oasis n’a jamais vu de posts depuis ce compte.", - beginningOfFeed: "Ceci est le début du fil", - noNewerPosts: "Aucun nouveau post reçu pour l’instant.", relationshipNotFollowing: "Ne vous soutient pas", relationshipTheyFollow: "Vous soutient", relationshipMutuals: "Soutien mutuel", @@ -409,25 +427,23 @@ module.exports = { relationshipBlockedBy: "Vous bloque", relationshipMutualBlock: "Blocage mutuel", relationshipNone: "Vous ne soutenez pas", - relationshipConflict: "Conflit", relationshipBlockingPost: "Post bloqué", - viewLikes: "Voir les soutiens", + viewLikes: "Voir les diffusions", + viewSupporters: "Voir les soutiens", spreadedDescription: "Liste des posts soutenus par habitant.", - totalspreads: "Soutiens totales", - attachFiles: "Joindre des fichiers", preview: "Prévisualiser", publish: "Publier", contentWarningPlaceholder: "Ajoutez un titre à votre post (optionnel)", - privateWarningPlaceholder: "Ajoutez des habitants pour envoyer un post privé (optionnel)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "Écris le corps de ton billet.", + publishTooLong: "Votre publication est trop longue. Veuillez la raccourcir.", + keepTextRetry: "Envoyer à nouveau", + dateInPastError: "La date ne peut pas être dans le passé", publishCustomDescription: [ "RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publié quelque chose, cela ne peut pas être modifié ni supprimé. Réfléchissez bien !", ], commentWarning: [ "RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publié quelque chose, cela ne peut pas être modifié ni supprimé. Réfléchissez bien !", ], - commentPublic: "public", - commentPrivate: "privé", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -447,14 +463,12 @@ module.exports = { ".", ], publishCustom: "Écrire un post avancé", - subtopicLabel: "Créez un sous-sujet pour ce post", messagePreview: "Prévisualisation", mentionsMatching: "Mentions", mentionsName: "Nom", mentionsRelationship: "Relation", - updateit: "OBTENIR DES MISES À JOUR !", updateBannerText: "Une nouvelle version d'Oasis est disponible.", - updateBannerAction: "Mettre à jour →", + updateBannerAction: "Mettre à jour !", info: "Info", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -462,29 +476,94 @@ module.exports = { timeAgo: "il y a", sendTime: "écrit ", theme: "Thème", - legacy: "Clés", - legacyTitle: "Clés", - legacyDescription: "Gérez votre secret (clé privée) rapidement et en toute sécurité.", - legacyExportButton: "Exporter", - legacyImportButton: "Importer", + backupKeysExportButton: "Exporter", + backupKeysImportButton: "Importer", ssbLogStream: "Blockchain", ssbLogStreamDescription: "Configurez la limite de messages pour les flux de la blockchain.", + logWindowChip: "Derniers {n} flux de la blockchain", saveSettings: "Enregistrer la configuration", - exportTitle: "Exporter des données", exportDescription: "Définissez un mot de passe (min 32 caractères) pour chiffrer votre secret", - exportDataTitle: "Sauvegarde", + exportDataTitle: "Sauvegarde instantanée", exportDataDescription: "Télécharger votre blockchain (clé privée exclue !)", exportDataButton: "Télécharger la blockchain", - pubWallet: "Portefeuille du PUB", - pubWalletDescription: "Définissez l’URL du portefeuille du PUB. Elle sera utilisée pour les transactions du PUB (RBU incluse).", - pubWalletConfiguration: "Enregistrer la configuration", - importTitle: "Importer des données", importDescription: "Importez votre secret chiffré (clé privée) pour activer votre avatar", - importAttach: "Joindre le fichier chiffré (.enc)", - passwordLengthInfo: "Le mot de passe doit contenir au moins 32 caractères.", passwordImport: "Saisissez votre mot de passe pour déchiffrer les données qui seront enregistrées sur votre système (nom : secret)", exportPasswordPlaceholder: "Utilisez des minuscules, majuscules, chiffres et symboles", - fileInfo: "Votre secret chiffré sera enregistré sur votre système (nom : oasis.enc)", + fileInfo: "Votre secret chiffré sera téléchargé sur votre appareil (nom : oasis.enc)", + developer: "Développement", + devTitle: "Développement", + welcomeBannerText: "Ton avatar n'est pas encore configuré.", + aiSuggestionBanner: "Suggestion :", + aiSuggestionsEnable: "Suggestions de l'IA", + welcomeBannerAction: "Commencer", + welcomeTitle: "Bienvenue", + welcomeStepIdentity: "Identité", + welcomeStepType: "Type", + welcomeStepUse: "Usage", + welcomeStepLook: "Apparence", + welcomeStepCommunity: "Communauté", + welcomeStepIntro: "Bienvenue", + welcomeHintIdentity: "Votre nom et votre image.", + welcomeHintType: "Comment Oasis s'ouvre pour vous.", + welcomeHintUse: "Quels modules restent actifs.", + welcomeHintLook: "Le thème que porte Oasis.", + welcomeHintCommunity: "Rejoignez un PUB pour rencontrer d'autres habitants.", + welcomeHintIntro: "Envoyez un premier salut à la communauté.", + welcomeStepThemeAction: "Utiliser ce thème", + welcomeJoinPubs: "Rejoindre les PUBs", + welcomeNoFederation: "Sans fédération", + welcomeStepGreetingAction: "Envoyer", + welcomeDescription: "Quelques choses à faire avant de commencer.", + welcomeSkip: "Pas maintenant", + welcomeStepProfileAction: "Enregistrer le profil", + welcomeBootstrapRunning: "Chargement de l'instantané du pub. Le pub est rejoint dès la fin ; vous pouvez continuer à utiliser Oasis entre-temps.", + welcomeCompleteTitle: "Tout est prêt.", + welcomeCompleteText: "Votre nœud est prêt. À partir d'ici le réseau grandit avec les habitants que vous soutenez.", + welcomeFindPeople: "Trouver des habitants", + devFilterFiles: "FICHIERS", + devFilterSearch: "CHERCHER", + devFilterModules: "MODULES", + devFilterTranslations: "TRADUCTIONS", + devFilterStyles: "STYLES", + devFilterTests: "TESTS", + devVersion: "Version", + devNodeVersion: "Node", + devStatsTests: "Tests", + devParentFolder: "Dossier parent", + devOpenFolder: "ouvrir", + devDescription: "Explorez le code source exécuté sur ce nœud.", + devRoot: "racine", + devSearchPlaceholder: "Texte à chercher dans le code", + devAnyExtension: "Tout fichier", + devStatsFiles: "Fichiers", + devStatsLines: "Lignes", + devStatsModules: "Modules", + devStatsLanguages: "Langues", + devNotViewable: "non affichable", + devEmptyFolder: "Ce dossier est vide.", + devSize: "Taille", + devShowing: "Affichage", + devLine: "ligne", + devReportBug: "Signaler un Bug", + devCreateTask: "Créer une Tâche", + devRaw: "RAW", + devReportContext: "Trouvé en lisant le code source d'Oasis", + devTaskPrefix: "Réviser", + devPrevPage: "Lignes précédentes", + devNextPage: "Lignes suivantes", + devMatches: "correspondances", + devFilesScanned: "fichiers analysés", + devNoResults: "Aucune correspondance, pour l'instant.", + devColModule: "Module", + devColState: "État", + devColModel: "Modèle", + devColView: "Vue", + devColRoutes: "Routes", + devColTests: "Tests", + devOn: "actif", + devOff: "inactif", + modulesDevLabel: "Développement", + modulesDevDescription: "Module pour gérer le code source d'Oasis.", themeIntro: "Choisissez un thème.", setTheme: "Définir le thème", @@ -492,16 +571,49 @@ module.exports = { languageDescription: "Si vous souhaitez utiliser une autre langue, sélectionnez-la ici.", setLanguage: "Définir la langue", - peerConnections: "Nœuds", peerConnectionsIntro: "Gérez toutes vos connexions avec d’autres nœuds.", + peersConnectFailed: "Connexion impossible : {reason}.", + peerStateLabel: "État", + peerAddressLabel: "Adresse", + peerLastChangeLabel: "Dernier changement", + peerStateConnected: "Connecté", + peerStateConnecting: "Connexion en cours", + peerStateStaged: "Disponible", + peerErrSilent: "Aucune activité", + peerErrTimeout: "Ne répond pas", + peerErrRefused: "Refuse la connexion", + peerErrNotFound: "Adresse inconnue", + peerErrUnreachable: "Réseau injoignable", + peerErrKeys: "Utilise des clés différentes", + peerErrIdentity: "N'accepte pas votre connexion", + peerErrTor: "TOR uniquement", + peerErrDropped: "Connexion coupée", + peerErrPaused: "Réseau en pause", + peerErrOther: "Erreur de connexion", + peerOutdated: "Obsolète", + peerVersionUnannounced: "N'annonce pas sa version : un ancien Oasis", + peersTorMissing: "Ce PUB n'accepte que des connexions via TOR.", peerConnectionsTitle: "Connexions", + peersOnlineHint: "Nœuds connectés à vous en ce moment.", + peersDiscoveredHint: "Nœuds avec une adresse connue, non connectés pour l'instant.", + peersUnknownHint: "Nœuds de votre graphe social sans adresse connue.", + peersConnectionsHint: "Adresses que ce nœud peut joindre et l'état de chaque connexion.", + peersTabLan: "LAN", + peersTabHealth: "Santé", + peersHealthHint: "L'état de chaque pub ou pair connu, sur le réseau local et au-delà.", + peersHealthReachable: "Joignable", + peersHealthFailing: "En échec", + peersHealthFailures: "Échecs", + peersFailingSince: "En échec depuis", + peersHealthForget: "Oublier les injoignables", + peersLanHint: "Nœuds rencontrés sur votre réseau local.", + peersLanLastSeen: "Vu pour la dernière fois", + peersLanEmpty: "Aucun nœud rencontré sur le réseau local pour l'instant.", online: "En ligne", offline: "Hors ligne", discovered: 'Découverts', unknown: 'Inconnus', - lanBroadcastLabel: "Diffusion LAN", lanBroadcastActive: "Active (multicast UDP sur le LAN)", - lanBroadcastDisabled: "Désactivée (mode pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -514,123 +626,98 @@ module.exports = { noConnections: "Aucun nœud connecté.", noDiscovered: "Vous n’avez découvert aucun nœud.", noUnknownPeers: "Aucun pair inconnu dans le graphe d’amis.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "Aucun pair enregistré pour l'instant.", + peerConnect: "Connecter", + peerDisconnect: "Déconnecter", peerRefresh: "Rafraîchir", peerPruneIdle: "Retirer les inactifs", + peersPause: "Pause", + peersResume: "Reprendre", + peersPausedNote: "En pause : Oasis ne se connecte à personne et personne ne peut s'y connecter.", + peersPauseHint: "Arrête toutes les connexions : Oasis ne se connecte à aucun autre nœud et n'en accepte aucun jusqu'à la reprise.", peerExport: "Exporter", peerImport: "Importer", peerImportTitle: "Importer la liste de pairs", peerImportPlaceholder: "Colle une adresse multiserver par ligne, ou téléverse un fichier .txt…", - noSupportedConnections: "Aucun nœud pris en charge.", - noBlockedConnections: "Aucun nœud bloqué.", - noRecommendedConnections: "Aucun nœud recommandé.", connectionActionIntro: "", - startNetworking: "Démarrer le réseau", - stopNetworking: "Arrêter le réseau", - restartNetworking: "Redémarrer le réseau", - sync: "Synchroniser le réseau", indexes: "Index", + indexesRebuilt: "Index reconstruits", + indexesRebuildFailed: "Impossible de reconstruire les index", + indexesFiles: "Fichiers d'index", indexesDescription: "Reconstruire les index est sans risque et peut corriger certains problèmes dans votre blockchain.", homePageTitle: "Page d'accueil", homePageDescription: "Sélectionnez le module que vous souhaitez comme page d'accueil.", saveHomePage: "Enregistrer la page d'accueil", invites: "Invitations", - invitesTitle: "Invitations", - invitesInvites: "Invitations", invitesDescription: "Gérer et appliquer des codes d’invitation dans votre réseau.", + invitesPubsHint: "Collez un code d'invitation de pub pour vous fédérer avec lui et répliquer ses habitants.", + invitesFederationsHint: "Pubs avec lesquels vous êtes fédéré : actualisez-les, retirez les injoignables ou exportez la liste.", invitesTribesTitle: "Tribus", - invitesTribeInviteCodePlaceholder: "Saisissez le code de la tribu", - invitesTribeJoinButton: "Entrer dans la tribu", invitesChatsTitle: "Chats", - invitesChatInviteCodePlaceholder: "Saisissez le code du chat", - invitesChatJoinButton: "Rejoindre le chat", invitesPadsTitle: "Pads", - invitesPadInviteCodePlaceholder: "Saisissez le code du pad", - invitesPadJoinButton: "Rejoindre le pad", invitesCalendarsTitle: "Calendriers", - invitesCalendarInviteCodePlaceholder: "Saisissez le code du calendrier", - invitesCalendarJoinButton: "Rejoindre le calendrier", invitesEventsTitle: "Événements", - invitesEventInviteCodePlaceholder: "Saisissez le code de l'événement", - invitesEventJoinButton: "Rejoindre l'événement", invitesForumsTitle: "Forums", - invitesForumInviteCodePlaceholder: "Saisissez le code du forum", - invitesForumJoinButton: "Rejoindre le forum", invitesMapsTitle: "Cartes", - invitesMapInviteCodePlaceholder: "Saisissez le code de la carte", - invitesMapJoinButton: "Rejoindre la carte", invitesShopsTitle: "Boutiques", - invitesShopInviteCodePlaceholder: "Saisir le code d'invitation de la boutique", - invitesShopJoinButton: "Rejoindre la boutique", invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "PUBs Suggérés", + invitesFilterOthers: "Autres", + invitesJoinTitle: "Invitations", + invitesJoinHint: "Collez n'importe quel code d'invitation, quel que soit son objet, ou l'Oasis ID d'un habitant.", + invitesJoinPlaceholder: "Code d'invitation ou Oasis ID", + invitesJoinButton: "Rejoindre", + invitesUsedTitle: "Dernières invitations", + invitesUsedEmpty: "Vous n'avez encore employé aucune invitation.", + invitesUsedType: "Type", + invitesUsedName: "Nom", + invitesUsedDate: "Date", invitesPubInviteCodePlaceholder: "Saisissez le code d’invitation du PUB", invitesAcceptInvite: "Entrer dans le PUB", + invitesPubUses: "Invitations utilisées", + invitesPubExhausted: "Ce PUB n'a plus d'invitations disponibles.", invitesAlreadyFederated: "Vous êtes déjà fédéré avec ce pub.", + invitesJoinPubMode: "Cet Oasis fonctionne comme PUB, en lecture seule. Pour le relier à un autre PUB, utilisez le terminal : ./oasis.sh join ", + invitesJoinLocalOnly: "Les invitations ne peuvent être acceptées que depuis l'appareil où tourne Oasis, ou via le lien d'administration affiché au démarrage.", invitesFederationsTitle: "Fédérations", invitesAcceptedInvites: "Fédérés", - invitesNoInvites: "Aucune invitation acceptée pour le moment.", invitesNoFederatedPubs: "Aucun pub fédéré.", - invitesUnreachablePubs: "Inaccessibles", - invitesNoUnreachablePubs: "Aucun pub inaccessible.", invitesPubsRefresh: "Rafraîchir", - invitesPubsClearUnreachable: "Retirer les injoignables", + invitesPubsClearUnreachable: "Nettoyer", invitesPubsExport: "Exporter", invitesPubsImport: "Importer", invitesPubsImportTitle: "Importer des pubs", invitesPubsImportPlaceholder: "Colle une adresse multiserver ou un code d'invitation par ligne, ou téléverse un fichier .txt…", - currentlyUnreachable: "ERREUR !", - errorDetails: "Détails de l’erreur", genericError: "Une erreur s’est produite.", panicMode: "Mode Panique !", encryptData: "Définissez un mot de passe (min 32 caractères) pour chiffrer votre blockchain", decryptData: "Saisissez le mot de passe pour déchiffrer votre blockchain", - panicModeDescription: "Chiffrer/Déchiffrer ou SUPPRIMER votre blockchain", removeDataDescription: "ATTENTION : Ce processus ne peut pas être annulé.", - encryptPanicButton: "Chiffrer la blockchain", - decryptPanicButton: "Déchiffrer la blockchain", removePanicButton: "SUPPRIMER TOUTES VOS DONNÉES !", searchTitle: "Rechercher", searchDescriptionLabel: "Rechercher du contenu dans votre réseau.", - searchLanguagesLabel: "Langues", - searchSkillsLabel: "Compétences", searchPlaceholder:"Rechercher du contenu...", - searchDateLabel:"Date", searchLocationLabel:"Localisation", searchPriceLabel:"Prix", - searchUrlLabel:"URL", searchCategoryLabel:"Catégorie", - searchStartLabel:"Date de début", - searchEndLabel:"Date de fin", searchPriorityLabel:"Priorité", searchStatusLabel:"État", statusLabel:"État", - totalVotesLabel:"Total des votes", noResultsFound:"Aucun résultat trouvé.", votesOption:"Opinions votées", voteYesLabel:"Oui", voteNoLabel:"Non", allTypesLabel: "SANS LIMITES", - ABSTENTIONLabel:"ABSTENTION", YESLabel:"OUI", NOLabel:"NON", - FOLLOW_MAJORITYLabel: "SUIVRE LA MAJORITÉ", - CONFUSEDLabel: "CONFUS", - NOT_INTERESTEDLabel: "SANS INTÉRÊT", - StatusLabel:"État", votesOptionYesLabel:"Oui", votesOptionNoLabel:"Non", votesOptionAbstentionLabel:"Abstention", votesQuestionLabel:"Question", - votesCreatedByLabel:"Créé le", voteStatusOpen:"OUVERT", voteStatusClosed:"FERMÉ", - imageSearchLabel: "Saisissez des mots pour rechercher les images étiquetées avec ces mots.", commentDescription: ({ parentUrl }) => [ " a commenté dans ", a({ href: parentUrl }, " fil"), @@ -641,61 +728,27 @@ module.exports = { a({ href: parentUrl }, " un post"), ], subtopicTitle: ({ authorName }) => [`A créé un sous-sujet dans le post de @${authorName}`], - mysteryDescription: "a publié un post mystérieux", oasisDescription: "OASIS Réseau de Projets", searchSubmit: "Rechercher...", - postLabel: "PUBLICATIONS", - aboutLabel: "HABITANTS", - feedLabel: "FLUX", votesLabel: "GOUVERNANCE", - reportLabel: "RAPPORTS", - imageLabel: "IMAGES", - videoLabel: "VIDÉOS", - audioLabel: "AUDIOS", - documentLabel: "DOCUMENTS", - torrentLabel: "TORRENTS", pdfFallbackLabel: "Document PDF", - eventLabel: "ÉVÉNEMENTS", - taskLabel: "TÂCHES", - transferLabel: "TRANSFERTS", - curriculumLabel: "CURRICULUM VITAE", - bookmarkLabel: "LIENS", - tribeLabel: "TRIBUS", - marketLabel: "MARCHÉ", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "PORTEFEUILLES", - bankClaimLabel: "UBI CLAIMS", + shopLabel: "BOUTIQUES", voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", submit: "Appliquer", - subjectLabel: "Sujet", editProfile: "Éditer l’Avatar", - profileQrAlt: "Code QR du profil", - profileQrHint: "Scannez pour copier cet identifiant Oasis.", oasisIdLabel: "OasisID", - spreadLabel: "Diffuser", - spreadHint: "Diffusez ceci à ceux qui vous soutiennent (réplique via votre feed).", + spreadContent: "Répliquer", welcomePmSubject: "Hello World!", - welcomePmBody: "Bienvenue sur Oasis — un réseau social libre, pair-à-pair, fédéré et chiffré, avec une architecture distribuée sur invitation uniquement.\n\nQuelques endroits intéressants par où commencer :\n\n- /profile — Édite ton avatar, ton nom, ta description et les modules visibles sur ta partie publique.\n- /invites — Ajoute un pub pour fédérer avec le reste du réseau.\n- /peers — Vois qui est connecté, rescanne ton LAN, exporte ou importe des listes de pairs.\n- /inhabitants — Découvre et visite les avatars d'autres personnes.\n- /tribes — Crée ou rejoins des groupes privés chiffrés de bout en bout.\n- /inbox — Lis et envoie des messages privés.\n- /activity — Vois l'activité récente, la tienne et celle de ton réseau.\n- /publish — Écris un post ou partage une image, un audio, une vidéo ou un document.\n\nQuelques endroits intéressants pour se connecter :\n\n- /fediverse — Gérez vos autres comptes du fédiverse, y compris l'envoi et la réception de contenu.\n\nCe message a été envoyé en privé de toi à toi-même : aucune connexion n'était nécessaire pour le recevoir.", - profileVisibilityTitle: "Visibilité du profil public", - profileVisibilityHint: "Choisissez les champs visibles par les autres habitants sur votre profil. Par défaut, seul Karma est affiché.", + welcomePmBody: "Bienvenue sur Oasis — un réseau social libre, pair-à-pair, fédéré et chiffré, avec une architecture distribuée sur invitation uniquement.\n\nQuelques endroits intéressants par où commencer :\n\n- [/avatar](/profile) — Édite ton avatar, ton nom, ta description et les modules visibles sur ta partie publique.\n- /settings — Réglez votre instance locale : langue, thème, modules, confidentialité et synchronisation.\n- /invites — Ajoute un pub pour fédérer avec le reste du réseau.\n- /peers — Vois qui est connecté, rescanne ton LAN, exporte ou importe des listes de pairs.\n- /inhabitants — Découvre et visite les avatars d'autres habitants.\n- /tribes — Crée ou rejoins des groupes privés chiffrés de bout en bout.\n- /search — Recherchez le contenu du réseau par type.\n\nQuelques endroits intéressants pour se connecter :\n\n- /multiverse — Gérez vos autres comptes du multiverse, y compris l'envoi et la réception de contenu.\n\nCe message a été envoyé en privé de toi à toi-même : aucune connexion n'était nécessaire pour le recevoir.", profileSensorsSectionTitle: "Capteurs", profileVisibilityActivity: "Niveau d’activité", - profileVisibilityDevice: "Appareil", profileDeviceLockedHint: "Ce capteur est toujours actif : il permet aux autres de voir depuis quel appareil vous vous connectez et ne peut pas être désactivé.", profileVisibilityKarma: "Score KARMA", - profileVisibilityUbi: "UBI", - profileVisibilityWallet: "Portefeuille ECOIN", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityUbi: "RBU", + profileVisibilityWallet: "Portefeuille ECOin", + profileVisibilityEcoTax: "Taxe ECO", + profileVisibilityLarpSign: "Sceau L.A.R.P.", profileVisibilityGpg: "Clé GPG", - profileVisibilityClearnet: "Publier mon profil", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Boutiques", profileClearnetJobsLabel: "Offres d’emploi", @@ -707,6 +760,10 @@ module.exports = { profileClearnetImagesLabel: "Images", profileClearnetDocumentsLabel: "Documents", profileClearnetTorrentsLabel: "Torrents", + profileClearnetPodcastsLabel: "Podcasts", + profileClearnetMarketLabel: "Marché", + profileClearnetFeedLabel: "Feed", + profileClearnetWikiLabel: "Wikis", editProfileDescription: "", profileName: "Nom", @@ -730,9 +787,9 @@ module.exports = { walletReceiveTitle: "Recevoir", walletHistoryTitle: "Historique", walletBalanceLine: ({ balance }) => `${balance} ECO`, - walletCnfrs: "Cnfrs", + walletCnfrs: "Conf.", walletConfirm: "Confirmer", - walletDescription: "Gérez vos actifs numériques, y compris l’envoi et la réception d’ECOin, l’affichage du solde et l’accès à l’historique des transactions.", + walletDescription: "Gérez votre portefeuille ECOin.", walletDate: "Date", walletFee: "Frais (Plus les frais sont élevés, plus votre transaction sera traitée rapidement)", walletFeeLine: ({ fee }) => `Frais : ECO ${fee}`, @@ -740,6 +797,86 @@ module.exports = { walletReceive: "Recevoir", walletReset: "Réinitialiser", walletSend: "Envoyer", + walletPayingTransfer: "Paiement du transfert", + walletCreateTransferLabel: "Générer un contrat (transfert)", + walletTransferNotifiedNote: "L'autre partie a été informée de ce paiement", + walletTransferCreatedNote: "Un transfert (reçu) a été créé pour ce paiement", + walletTransferNoPayeeNote: "Aucun habitant n'a publié cette adresse, aucun transfert créé", + walletTransferReceiptConcept: "Paiement depuis le portefeuille", + walletPaymentPmText: "vous a envoyé un paiement", + walletPayeeLabel: "Destinataire", + walletConceptLabel: "Concept", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Vous avez reçu votre RBU.", + bankingBotPaymentTitle: "Vous avez reçu un paiement.", + bankingBotUbiPaidText: "Votre Revenu de Base Universel a été versé", + bankUbiPubThisNode: "Ce nœud paie le RBU", + bankUbiPubConnected: "PUB du RBU", + bankUbiPubNone: "Aucun PUB n'annonce encore le RBU dans votre réseau", + bankUbiAddressPublished: "Adresse ECOin publiée", + bankUbiAddressMissing: "Publiez votre adresse ECOin pour recevoir le RBU", + bankUbiClaimedThisMonth: "Réclamé ce mois-ci", + bankUbiPubBalance: "Portefeuille du PUB disponible pour le RBU", + bankNoPubAddress: "Le PUB du RBU n'a pas encore publié d'adresse ECOin.", + bankUbiTab: "RBU", + bankDonateEco: "DONNER !", + donateTooltip: "Faire un don d'ECOin", + bankNoUserAddress: "Cet habitant ne partage pas d'adresse ECOin.", + bankUbiPubDefault: "Par défaut", + bankUbiPool: "Dotation", + bankUbiLastSeen: "Vu pour la dernière fois", + bankUbiLastPayout: "Dernière action", + bankUbiPaidOut: "Distribué", + bankRangeToday: "Aujourd'hui", + bankRangeWeek: "Semaine", + bankRangeMonth: "Mois", + bankRangeYear: "Année", + bankRangeFiveYears: "5 ans", + bankRangeAll: "Tout", + bankChartValueTitle: "Évolution de la valeur d'ECOin", + bankChartValueHours: "Équivalence en temps (h)", + bankChartWealthTitle: "Redistribution de la richesse ECOin", + bankChartDistributed: "RBU distribué (ECO)", + bankChartRebalanceTitle: "Redistribution des richesses entre PUBs", + bankChartRebalanced: "ECO déplacés entre PUBs", + bankChartTaxes: "Impôts retenus (ECO)", + bankChartNoDataRange: "Aucune donnée sur cette période.", + bankChartNoDataYet: "Pas encore de données", + bankChartPerHour: "par heure", + bankChartPerDay: "par jour", + bankChartPerMonth: "par mois", + bankTotalUbiDistributed: "RBU total redistribué (réseau)", + bankTotalTaxesWithheld: "Impôts totaux retenus (réseau)", + bankInflationIssuance: "Taux d'émission (annuel, récompense de bloc)", + bankRulesEligibilityTitle: "Qui peut recevoir le RBU", + bankRulesEligibility1: "Une adresse ECOin publiée dans votre profil (Profil → Capteurs → Portefeuille ECOin).", + bankRulesEligibility2: "Un feed de plus de 30 jours avec de l'activité : karma supérieur à zéro.", + bankRulesEligibility3: "Une réclamation par mois (epoch), depuis Banking avec le bouton Réclamer. Le mois en cours peut être refusé avec Refuser le RBU.", + bankRulesEligibility4: "Le plancher de 1 ECO est toujours payé ; les impôts ne réduisent que la part au-dessus et ne sont jamais facturés à part.", + bankRulesEligibility5: "Le PUB paie à son prochain cycle et publie un transfert RBU que vous pouvez confirmer.", + bankRulesEligibility6: "Sans adresse, avec un feed trop jeune, un mois refusé ou un PUB sans fonds, la réclamation est ignorée et journalisée sur le PUB.", + copyLinkTitle: "Copier le lien", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "Refuser le RBU", + bankUbiRefusedThisMonth: "Vous avez refusé le RBU de ce mois.", + bankingBotUbiAvailableTitle: "Vous pouvez réclamer votre RBU.", + bankingBotUbiAvailableText: "Votre RBU de ce mois est disponible", + bankingBotConfirmTitle: "Vous avez un smart-contract en attente de confirmation", + bankingBotConfirmText: "Smart-contracts en attente de votre confirmation", + bankChartZoomHint: "Cliquer pour agrandir", + bankChartPoolTitle: "Dotation du RBU (dans le temps)", + bankChartNetworkUbiTitle: "RBU total redistribué (réseau)", + bankChartSupplyTitle: "Offre en circulation d'ECOin (dans le temps)", + bankChartInflationTitle: "Inflation d'ECOin (dans le temps)", + bankChartInflationSeries: "Taux annualisé (%)", + bankChartPool: "Dotation totale (ECO)", + bankChartKarmaTitle: "KARMA Scoring (dans le temps)", + bankChartFundsTitle: "Vos fonds ECOin (dans le temps)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin connecté", + bankWalletNotConfigured: "ECOin déconnecté", + transfersPayWithWallet: "PAYER !", walletStatus: "État", walletDisconnected: [ "Vous avez le ", @@ -749,9 +886,11 @@ module.exports = { " ou l’état de votre connexion.", ], walletSentToLine: ({ destination, amount }) => `Envoyer ECO ${amount} à ${destination}`, - walletSettingsTitle: "Portefeuille", + walletNotConfigured: "Votre portefeuille ECOin n'est pas encore configuré.", walletSettingsDescription: "Intégrer Oasis avec votre portefeuille ECOin.", - walletSettingsDocLink: "Guide d’installation d’ECOin", + walletSettingsMobileDescription: "Associez Oasis à l’application ECOin Wallet de cet appareil.", + walletDownloadEcoin: "Télécharger ECOin !", + walletRpcUrl: "URL du RPC", walletStatusMessages: { invalid_amount: "Montant invalide", invalid_dest: "Adresse de destination invalide", @@ -767,41 +906,24 @@ module.exports = { walletUser: "Utilisateur", walletPass: "Mot de passe", walletConfiguration: "Configurer le portefeuille", + walletDisconnectButton: "Déconnecter le portefeuille", cipher: "Chiffreur", cipherTitle: "Chiffreur", cipherDescription: "Chiffrez et déchiffrez votre texte de manière symétrique (à l’aide d’un mot de passe partagé).", randomPassword: "Mot de passe aléatoire", - cipherEncryptTitle: "Chiffrer le texte", - cipherEncryptDescription: "Saisissez le texte à chiffrer", - cipherTextLabel: "Texte à chiffrer", cipherTextPlaceholder: "Saisissez le texte à chiffrer...", cipherPasswordLabel: "Définissez un mot de passe (minimum 32 caractères) pour chiffrer votre texte", cipherPasswordDecryptLabel: "Définissez un mot de passe (32 caractères minimum) pour déchiffrer votre texte", cipherPasswordPlaceholder: "Saisissez un mot de passe...", cipherEncryptButton: "Chiffrer", - cipherDecryptTitle: "Déchiffrer le texte", - cipherDecryptDescription: "Saisissez le texte à déchiffrer", cipherEncryptedMessageLabel: "Texte chiffré", cipherDecryptedMessageLabel: "Texte déchiffré", - cipherPasswordUsedLabel: "Mot de passe utilisé pour chiffrer (gardez-le !)", cipherEncryptedTextPlaceholder: "Saisissez le texte chiffré...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "Saisissez le vecteur d’initialisation...", cipherDecryptButton: "Déchiffrer", password: "Mot de passe", text: "Texte", encryptedText: "Texte chiffré", iv: "Vecteur d’initialisation (IV)", - encryptTitle: "Chiffrez votre texte", - encryptDescription: "Saisissez le texte que vous souhaitez chiffrer et fournissez un mot de passe.", - encryptButton: "Chiffrer", - decryptTitle: "Déchiffrez votre texte", - decryptDescription: "Saisissez le texte chiffré et fournissez le même mot de passe que celui utilisé pour le chiffrement.", - decryptButton: "Déchiffrer", - passwordLengthError: "Le mot de passe doit comporter au moins 32 caractères.", - missingFieldsError: "Texte, mot de passe ou IV non fournis.", - encryptionError: "Erreur lors du chiffrement du texte.", - decryptionError: "Erreur lors du déchiffrement du texte.", bookmarkTitle: "Marque-pages", bookmarkDescription: "Découvrez et gérez des marque-pages dans votre réseau.", bookmarkAllSectionTitle: "Marque-pages", @@ -818,19 +940,16 @@ module.exports = { bookmarkFilterRecent: "RÉCENTS", bookmarkCreateButton: "Créer un marque-page", bookmarkUpdateButton: "Mettre à jour", - bookmarkDeleteButton: "Supprimer", bookmarkAddFavoriteButton: "Ajouter aux favoris", bookmarkRemoveFavoriteButton: "Retirer des favoris", bookmarkUrlLabel: "Lien", - bookmarkUrlPlaceholder: "https://exemple.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Description", bookmarkDescriptionPlaceholder: "Optionnel", bookmarkTagsLabel: "Étiquettes", bookmarkTagsPlaceholder: "Saisissez des tags séparés par des virgules", - bookmarkCategoryLabel: "Catégorie", - bookmarkCategoryPlaceholder: "Optionnel", bookmarkLastVisitLabel: "Dernière visite", - bookmarkSearchPlaceholder: "Rechercher URL, tags, catégorie, auteur...", + bookmarkSearchPlaceholder: "Rechercher dans Marque-pages...", bookmarkSortRecent: "Plus récents", bookmarkSortOldest: "Plus anciens", bookmarkSortTop: "Les plus votés", @@ -843,8 +962,6 @@ module.exports = { noLastVisit: "Aucune dernière visite", videoTitle: "Vidéos", videoDescription: "Explorez et gérez du contenu vidéo dans votre réseau.", - videoPluginTitle: "Titre", - videoPluginDescription: "Description", videoMineSectionTitle: "Vos vidéos", videoCreateSectionTitle: "Téléverser une vidéo", videoUpdateSectionTitle: "Mettre à jour la vidéo", @@ -871,12 +988,11 @@ module.exports = { videoDescriptionPlaceholder: "Facultatif", videoNoFile: "Aucun fichier vidéo fourni", noVideos: "Aucune vidéo disponible.", - videoSearchPlaceholder: "Rechercher par titre, tags, auteur…", + videoSearchPlaceholder: "Rechercher dans Vidéos...", videoSortRecent: "Les plus récentes", videoSortOldest: "Les plus anciennes", videoSortTop: "Les plus votées", videoSearchButton: "Rechercher", - videoMessageAuthorButton: "MP", videoUpdatedAt: "Mis à jour", videoNoMatch: "Aucune vidéo ne correspond à votre recherche.", documentTitle: "Documents", @@ -895,10 +1011,9 @@ module.exports = { documentFilterFavorites: "FAVORIS", documentCreateButton: "Téléverser", documentUpdateButton: "Mettre à jour", - documentDeleteButton: "Supprimer", documentAddFavoriteButton: "Ajouter aux favoris", documentRemoveFavoriteButton: "Retirer des favoris", - documentMessageAuthorButton: "PM", + documentMessageAuthorButton: "MP", documentFileLabel: "Téléverser un document (.pdf)", documentTagsLabel: "Étiquettes", documentTagsPlaceholder: "Saisissez des tags séparés par des virgules", @@ -908,7 +1023,7 @@ module.exports = { documentDescriptionPlaceholder: "Optionnel", documentNoFile: "Aucun fichier.", noDocuments: "Aucun document disponible.", - documentSearchPlaceholder: "Rechercher titre, tags, description, auteur...", + documentSearchPlaceholder: "Rechercher dans Documents...", documentSortRecent: "Les plus récents", documentSortOldest: "Les plus anciens", documentSortTop: "Les plus votés", @@ -917,8 +1032,6 @@ module.exports = { documentUpdatedAt: "Mis à jour", audioTitle: "Audios", audioDescription: "Explorez et gérez du contenu audio dans votre réseau.", - audioPluginTitle: "Titre", - audioPluginDescription: "Description", audioMineSectionTitle: "Vos audios", audioCreateSectionTitle: "Téléverser un audio", audioUpdateSectionTitle: "Mettre à jour l’audio", @@ -929,33 +1042,33 @@ module.exports = { audioFilterMine: "À MOI", audioFilterRecent: "RÉCENTS", audioFilterTop: "TOP", - audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Téléverser un audio", audioUpdateButton: "Mettre à jour", - audioDeleteButton: "Supprimer", audioAddFavoriteButton: "Ajouter aux favoris", audioRemoveFavoriteButton: "Retirer des favoris", audioFileLabel: "Sélectionnez un fichier audio (.mp3, .wav, .ogg)", audioTagsLabel: "Étiquettes", audioTagsPlaceholder: "Saisissez des tags séparés par des virgules", + tagsPlaceholder: "Saisissez des tags séparés par des virgules", audioTitleLabel: "Titre", audioTitlePlaceholder: "Facultatif", audioDescriptionLabel: "Description", audioDescriptionPlaceholder: "Facultatif", audioNoFile: "Aucun fichier audio fourni", noAudios: "Aucun audio disponible.", - audioSearchPlaceholder: "Rechercher par titre, tags, auteur…", + audioSearchPlaceholder: "Rechercher dans Audios...", audioSortRecent: "Les plus récents", audioSortOldest: "Les plus anciens", audioSortTop: "Les plus votés", audioSearchButton: "Rechercher", - audioMessageAuthorButton: "PM", + audioMessageAuthorButton: "MP", audioUpdatedAt: "Mis à jour", audioNoMatch: "Aucun audio ne correspond à votre recherche.", audioFavoritesSectionTitle: "Favoris", audioFilterFavorites: "FAVORIS", favoritesTitle: "Favoris", favoritesDescription: "Tous vos favoris au même endroit.", + favoritesSearchPlaceholder: "Rechercher dans Favoris...", favoritesFilterAll: "TOUS", favoritesFilterRecent: "RÉCENTS", favoritesFilterAudios: "AUDIOS", @@ -967,22 +1080,16 @@ module.exports = { favoritesFilterChats: "CHATS", favoritesFilterCalendars: "CALENDRIERS", favoritesFilterVideos: "VIDÉOS", - favoritesRemoveButton: "Retirer des favoris", favoritesNoItems: "Aucun favori pour le moment.", yourContacts: "Vos contacts", allInhabitants: "Habitants", - allCVs: "Tous les CV", + allCVs: "CV", discoverPeople: "Découvrez des habitants dans votre réseau.", - allInhabitantsButton: "TOUS", - contactsButton: "SOUTIENS", - CVsButton: "CVs", - matchSkills: "Faire correspondre des compétences", - matchSkillsButton: "FAIRE CORRESPONDRE LES COMPÉTENCES", - suggestedButton: "SUGGÉRÉS", - searchInhabitantsPlaceholder: "FILTRER les habitants PAR NOM …", + searchInhabitantsPlaceholder: "Rechercher dans Habitants...", filterLocation: "FILTRER les habitants PAR LOCALISATION …", filterLanguage: "FILTRER les habitants PAR LANGUE …", filterSkills: "FILTRER les habitants PAR COMPÉTENCES …", + cvSkillsCount: "Compétences", applyFilters: "Appliquer les filtres", locationLabel: "Localisation", languagesLabel: "Langues", @@ -991,15 +1098,15 @@ module.exports = { mutualFollowers: "Soutiens mutuels", suggestedFollowsYou: "Te suit", latestInteractions: "Dernières interactions", - viewAvatar: "Voir l’Avatar", - viewCV: "Voir le CV", suggestedSectionTitle: "Suggérés", topkarmaSectionTitle: "Meilleur Karma", topecoSectionTitle: "Meilleur Éco", topactivitySectionTitle: "Top activité", + "TOP INACTIVITYButton": "PLUS INACTIFS", + topinactivitySectionTitle: "Habitants les moins actifs", blockedSectionTitle: "Bloqués", gallerySectionTitle: "GALERIE", - blockedButton: "BLOQUÉS", + topSectionTitle: "TOP", blockedLabel: "Utilisateur bloqué", inhabitantviewDetails: "Voir les détails", keepReading: "Lire la suite...", @@ -1014,10 +1121,23 @@ module.exports = { parliamentFilterCandidatures: "CANDIDATURES", parliamentFilterProposals: "PROPOSITIONS", parliamentFilterLaws: "LOIS", + parliamentFilterRevocations: "RÉVOCATIONS", parliamentFilterHistorical: "HISTORIQUE", parliamentFilterLeaders: "DIRIGEANTS", parliamentFilterRules: "RÈGLES", parliamentGovernmentCard: "Gouvernement actuel", + parliamentSeatsGovernment: "Gouvernement", + parliamentSeatsOpposition: "Opposition", + parliamentSeatsAbstention: "Abstention", + parliamentSeatsQuorum: "Quorum d'élection", + parliamentSeatsPass: "Seuil d'approbation", + parliamentSeatsByElection: "Par élection", + parliamentSeatsByHouses: "Par maison du LARP", + parliamentSeatsSeats: "sièges", + parliamentSeatsSeat: "siège", + parliamentSeatsLeader: "Chef", + parliamentSeatsVotesFor: "votes pour", + parliamentSeatsNoHouse: "Sans maison", parliamentActorInPowerInhabitant: 'HABITANT AU POUVOIR', parliamentActorInPowerTribe: 'TRIBU AU POUVOIR', parliamentHistoricalGovernmentsTitle: 'GOUVERNEMENTS', @@ -1039,10 +1159,8 @@ module.exports = { parliamentPoliciesDeclined: "LOIS REJETÉES", parliamentPoliciesDiscarded: "LOIS REJETÉES", parliamentEfficiency: "% EFFICACITÉ", - parliamentFilterRevocations: 'RÉVOCATIONS', parliamentRevocationFormTitle: 'Révoquer la Loi', parliamentRevocationLaw: 'Loi', - parliamentRevocationTitle: 'Titre', parliamentRevocationReasons: 'Motifs', parliamentCurrentRevocationsTitle: 'Révocations en Cours', parliamentFutureRevocationsTitle: 'Révocations à Venir', @@ -1054,23 +1172,12 @@ module.exports = { parliamentNoGovernments: "Il n’y a pas encore de gouvernements.", parliamentCandidatureFormTitle: "Proposer une Candidature", parliamentCandidatureIdPh: "ID Oasis (@...) ou nom de la tribu", - parliamentCandidatureSlogan: "Slogan (max 140 car.)", - parliamentCandidatureSloganPh: "Un court slogan", parliamentCandidatureMethod: "Méthode", parliamentCandidatureProposeBtn: "Proposer une Candidature", - parliamentThDate: "Date de proposition", - parliamentThSlogan: "Slogan", - parliamentThSince: "Profil depuis", - parliamentThVotes: "Votes reçus", - parliamentThVoteAction: "Voter", - parliamentTypeUser: "Habitant", - parliamentTypeTribe: "Tribu", parliamentVoteBtn: "Voter", parliamentProposalFormTitle: "Proposer une Loi", parliamentProposalDescription: "Description (≤1000)", parliamentProposalPublish: "Publier la Proposition", - parliamentFinalize: "Finaliser", - parliamentDeadline: "Date limite", parliamentNoProposals: "Il n’y a pas encore de propositions de loi.", parliamentNoLaws: "Il n’y a pas encore de lois approuvées.", parliamentMethodDEMOCRACY: "Démocratie", @@ -1088,29 +1195,22 @@ module.exports = { parliamentVotesSlashTotal: "Votes/Total", parliamentVotesNeeded: "Votes Requis", parliamentFutureLawsTitle: "Lois futures", - parliamentNoFutureLaws: "Pas encore de lois futures", parliamentVoteAction: "Voter", - parliamentLeadersTitle: "Dirigeants", parliamentThLeader: "AVATAR", parliamentPopulation: "Population", - parliamentThType: "Type", - parliamentThInPower: "Au pouvoir", parliamentThPresented: "CANDIDATURES", parliamentNoLeaders: "Aucun dirigeant", parliamentCandidaturesListTitle: "Liste des Candidatures", parliamentTimeRemaining: "Temps restant", - parliamentNoLeader: "Pas encore de dirigeants.", parliamentElectionsStatusTitle: "Prochain Gouvernement", parliamentProposalDeadlineLabel: "Date limite", + parliamentViewReport: "Voir le rapport", parliamentProposalTimeLeft: "Temps restant", - parliamentProposalOnTrack: "En bonne voie pour être adoptée", - parliamentProposalOffTrack: "Soutien insuffisant", parliamentRulesTitle: "Fonctionnement du Parlement", parliamentRulesIntro: "Les élections se résolvent tous les 2 mois ; les candidatures sont continues et sont réinitialisées lorsqu’un gouvernement est choisi.", parliamentRulesCandidates: "Tout habitant peut se proposer, proposer un autre habitant ou n’importe quelle tribu. Chaque habitant peut proposer jusqu’à 3 candidatures par cycle ; les doublons dans le même cycle sont rejetés.", parliamentRulesElection: "Gagne la candidature ayant le plus de votes au moment de la résolution.", parliamentRulesTies: "En cas d’égalité : karma d’habitant le plus élevé ; sinon, profil le plus ancien ; sinon, proposition la plus précoce ; puis ordre lexicographique par ID.", - parliamentRulesFallback: "Si personne ne vote, la dernière candidature proposée gagne. S’il n’y a aucune candidature, une tribu aléatoire est sélectionnée.", parliamentRulesTerm: "L’onglet Gouvernement affiche le gouvernement actuel et ses statistiques.", parliamentRulesMethods: "Formes : Anarchie (majorité simple), Démocratie (50 % + 1), Majorité (80 %), Minorité (20 %), Karmatocratie (propositions au karma le plus élevé), Dictature (approbation instantanée).", parliamentRulesAnarchy: "L’Anarchie est le mode par défaut : si aucune candidature n’est élue à la résolution, l’Anarchie est proclamée. En Anarchie, tout habitant peut proposer des lois.", @@ -1119,6 +1219,7 @@ module.exports = { parliamentRulesLaws: "Lorsqu’une proposition atteint son seuil, elle devient une Loi et apparaît dans l’onglet Lois avec sa date d’entrée en vigueur.", parliamentRulesHistorical: "Dans l’onglet Historique, vous pouvez voir chaque cycle de gouvernement qui a eu lieu ainsi que des données sur sa gestion.", parliamentRulesLeaders: "Dans l’onglet Dirigeants, vous pouvez voir un classement des habitants/tribus qui ont gouverné (ou se sont présentés), classés par efficacité.", + parliamentRulesHemicycle: "L'hémicycle attribue un siège par habitant : le bloc du gouvernement (les électeurs de la candidature gagnante et le chef), un bloc d'opposition par autre candidature du cycle, et l'abstention. Les marques en pointillé indiquent le quorum d'élection (25 % du recensement) et le seuil d'approbation de la méthode en vigueur. « Par maison du LARP » colore les mêmes sièges par maison.", parliamentProposalVoteStatusLabel: "Statut du vote", parliamentProposalOnTrackYes: "Seuil atteint", parliamentProposalOnTrackNo: "En dessous du seuil", @@ -1132,11 +1233,8 @@ module.exports = { courtsCaseFormTitle: "Ouvrir une affaire", courtsCaseRespondent: "Accusé / Défendeur", courtsCaseRespondentPh: "ID Oasis (@...) ou nom de Tribu", - courtsCaseMediatorsAccuser: "Médiateurs (accusation)", + courtsCaseSubjectPlaceholder: "Objet ou courte description", courtsCaseMethod: "Méthode de résolution", - courtsCaseDescription: "Description (1000 caractères max)", - courtsCaseEvidenceTitle: "Preuves de l'affaire", - courtsCaseEvidenceHelp: "Joignez des images, audios, documents (PDF) ou vidéos qui appuient votre affaire.", courtsCaseSubmit: "Déposer l'affaire", courtsNominateJudge: "Nommer un juge", courtsJudgeId: "Juge", @@ -1170,34 +1268,33 @@ module.exports = { courtsMethodDICTATOR: "Dictateur", courtsMethodPOPULAR: "Populaire", courtsMethodKARMATOCRACY: "Karmatocratie", + courtsCaseType: "Type de dossier", + courtsPresetMinorConflict: "Conflit mineur", + courtsPresetModerateConflict: "Conflit modéré", + courtsPresetSevereConflict: "Conflit grave", + courtsPresetHarassment: "Harcèlement ou abus", + courtsPresetContentModeration: "Modération de contenu", + courtsPresetBanRestriction: "Bannissement ou restriction", + courtsPresetGovernanceDispute: "Litige de gouvernance", + courtsStatusOPEN: "Ouvert", + courtsStatusIN_PROGRESS: "En cours", + courtsStatusDECIDED: "Décidé", + courtsStatusCLOSED: "Clos", + courtsStatusSOLVED: "Résolu", + courtsStatusUNSOLVED: "Non résolu", + courtsStatusDISCARDED: "Rejeté", courtsRulesTitle: "Fonctionnement des Tribunaux", courtsRulesIntro: "Les Tribunaux sont un processus géré par la communauté pour résoudre les conflits et promouvoir la justice réparatrice. Le dialogue, des preuves claires et des remèdes proportionnels sont privilégiés.", courtsRulesLifecycle: "Processus : 1) Ouvrir une affaire 2) Choisir une méthode 3) Soumettre des preuves 4) Audience et délibération 5) Verdict et remède 6) Exécution et clôture 7) Appel (le cas échéant).", - courtsRulesRoles: "Accusation : ouvre l’affaire. Défense : personne ou tribu mise en cause. Méthode : mécanisme choisi par la communauté pour faciliter, évaluer les preuves et rendre un verdict. Témoin : fournit des témoignages ou des preuves. Médiateurs : personnes neutres invitées par l’accusation et/ou la défense, avec accès à tous les détails, qui aident à désamorcer le conflit et à co-créer des accords.", + courtsRulesRoles: "Accusation : ouvre l’affaire. Défense : habitant ou tribu mis en cause. Méthode : mécanisme choisi par la communauté pour faciliter, évaluer les preuves et rendre un verdict. Témoin : fournit des témoignages ou des preuves. Médiateurs : habitants neutres invités par l’accusation et/ou la défense, avec accès à tous les détails, qui aident à désamorcer le conflit et à co-créer des accords.", courtsRulesEvidence: "Description jusqu’à 1000 caractères. Joignez des images, audios, vidéos et documents PDF pertinents et légaux. Ne partagez pas de données privées sensibles sans consentement.", courtsRulesDeliberation: "Les audiences peuvent être publiques ou privées. Les juges garantissent le respect, demandent des clarifications et peuvent écarter le matériel non pertinent ou illégal.", courtsRulesVerdict: "La restauration est prioritaire : excuses, accords de médiation, modération de contenu, restrictions temporaires ou autres mesures proportionnées. Le raisonnement doit être consigné.", courtsRulesAppeals: "Appel : autorisé en présence de nouvelles preuves ou d’une erreur procédurale manifeste. Doit être déposé dans les 7 jours sauf indication contraire.", - courtsRulesPrivacy: "Respectez la vie privée et la sécurité. Le doxing, la haine ou les menaces sont supprimés. Les juges peuvent modifier ou sceller certaines parties du dossier pour protéger les personnes à risque.", + courtsRulesPrivacy: "Respectez la vie privée et la sécurité. Le doxing, la haine ou les menaces sont supprimés. Les juges peuvent modifier ou sceller certaines parties du dossier pour protéger les habitants à risque.", courtsRulesMisconduct: "Le harcèlement, la manipulation ou les preuves fabriquées peuvent entraîner une résolution négative immédiate.", courtsRulesGlossary: "Affaire : enregistrement d’un conflit. Preuves : éléments à l’appui des affirmations. Verdict : décision avec remède. Appel : demande de réexamen du verdict.", courtsFilterActions: "ACTIONS", - courtsNoActions: "Aucune action en attente pour votre rôle.", - courtsCaseTitlePlaceholder: "Brève description du conflit", - courtsCaseSeverity: "Gravité", - courtsCaseSeverityNone: "Sans étiquette de gravité", - courtsCaseSeverityLOW: "Faible", - courtsCaseSeverityMEDIUM: "Moyenne", - courtsCaseSeverityHIGH: "Élevée", - courtsCaseSeverityCRITICAL: "Critique", - courtsCaseSubject: "Sujet", - courtsCaseSubjectNone: "Sans étiquette de sujet", - courtsCaseSubjectBEHAVIOUR: "Comportement", - courtsCaseSubjectCONTENT: "Contenu", - courtsCaseSubjectGOVERNANCE: "Gouvernance / règles", - courtsCaseSubjectFINANCIAL: "Financier / ressources", - courtsCaseSubjectOTHER: "Autre", - courtsHiddenRespondent: "Caché (visible uniquement pour les rôles impliqués).", courtsThRole: "Rôle", courtsRoleAccuser: "Accusation", courtsRoleDefence: "Défense", @@ -1208,51 +1305,23 @@ module.exports = { courtsAssignJudgeBtn: "Choisir un juge", trendingTitle: "Tendances", exploreTrending: "Explorez le contenu le plus populaire dans votre réseau.", - bookmarkButton: "MARQUE-PAGES", - transferButton: "TRANSFERTS", - eventButton: "ÉVÉNEMENTS", - taskButton: "TÂCHES", votesButton: "GOUVERNANCE", - reportButton: "RAPPORTS", - feedButton: "FLUX", - marketButton: "MARCHÉ", - imageButton: "IMAGES", - audioButton: "AUDIOS", - videoButton: "VIDÉOS", - documentButton: "DOCUMENTS", - torrentButton: "TORRENTS", - noTrendingFound: "Aucun contenu populaire trouvé.", - noContentMessage: "Aucun contenu populaire disponible pour le moment.", - trendingDescription: "Description", - trendingDate: "Date", - trendingLocation: "Localisation", - trendingPrice: "Prix", - trendingUrl: "URL", - trendingCategory: "Catégorie", - trendingStart: "Début", - trendingEnd: "Fin", - trendingPriority: "Priorité", - trendingStatus: "État", - trendingFrom: "De", - trendingTo: "À", - trendingConcept: "Concept", - trendingAmount: "Montant", - trendingDeadline: "Échéance", - trendingItemStatus: "Statut de l’article", - trendingTotalVotes: "Total des votes", + shopProductButton: "BOUTIQUE", + podcastButton: "PODCAST", + podcastEpisodeButton: "ÉPISODE", + campaignButton: "CAMPAGNE", + logisticsRouteButton: "TRAJET", trendingTotalOpinions: "Total des avis", - trendingNoContentMessage: "Aucune tendance disponible pour le moment.", - trendingAuthor: "Par", - trendingCreatedAtLabel: "Créé le", + trendingNoContentMessage: "Pas encore de tendances.", trendingTotalCount: "Total des compteurs", tasksTitle: "Tâches", tasksDescription: "Découvrez et gérez des tâches dans votre réseau.", + taskSearchPlaceholder: "Rechercher dans Tâches...", taskTitleLabel: "Titre", taskDescriptionLabel: "Description", taskStartTimeLabel: "Date de début", taskEndTimeLabel: "Date de fin", taskPriorityLabel: "Priorité", - taskPrioritySelect: "Sélectionner la priorité", taskPriorityUrgent: "Urgente", taskPriorityHigh: "Élevée", taskPriorityMedium: "Moyenne", @@ -1262,13 +1331,13 @@ module.exports = { taskVisibilityLabel: "Visibilité", taskPublic: "public", taskPrivate: "privé", - taskCreatedAt: "Créé le", - taskBy: "Par", taskStatus: "État", taskStatusOpen: "Ouverte", taskStatusInProgress: "En cours", taskStatusClosed: "Fermée", - taskAssignedTo: "Assignée à", + taskAssignedTo: "Assignée", + taskAssignedChip: "Assignée", + taskUnassignedChip: "Non assignée", taskAssignees: "Assignés", taskAssignButton: "M’assigner", taskUnassignButton: "Désassigner", @@ -1276,12 +1345,12 @@ module.exports = { taskUpdateButton: "Mettre à jour", taskDeleteButton: "Supprimer", taskFilterAll: "TOUS", + taskFilterRecent: "RÉCENTS", taskFilterMine: "MIENS", taskFilterOpen: "OUVERTES", taskFilterInProgress: "EN COURS", taskFilterClosed: "FERMÉES", taskFilterAssigned: "ASSIGNÉES", - taskFilterArchived: "ARCHIVÉES", taskFilterUrgent: "URGENTES", taskFilterHigh: "HAUTES", taskFilterMedium: "MOYENNES", @@ -1294,35 +1363,47 @@ module.exports = { taskInProgressTitle: "Tâches en cours", taskClosedTitle: "Tâches fermées", taskAssignedTitle: "Tâches assignées", - taskArchivedTitle: "Tâches archivées", - taskPublicTitle: "Tâches publiques", - taskPrivateTitle: "Tâches privées", notasks: "Aucune tâche disponible.", noLocation: "Aucune localisation spécifiée", taskSetStatus: "Définir l'état", - eventTitle: "Événements", eventDateLabel: "Date", + eventRecurrenceLabel: "Récurrence", + eventRecurrenceUntil: "Répéter jusqu'au", + eventRecurrenceHint: "Laissez la date de fin vide pour un événement unique.", + eventUpcomingDates: "Prochaines dates", eventsTitle: "Événements", eventsDescription: "Découvrez et gérez des événements dans votre réseau.", - eventDescription: "Description", + eventSearchPlaceholder: "Rechercher dans Événements...", eventPrice: "Prix", eventStatus: "État", - eventOrganizer: "Organisateur", eventAllSectionTitle: "Événements", eventMineSectionTitle: "Vos événements", - eventArchivedTitle: "Événements archivés", eventCreateSectionTitle: "Créer un événement", eventUpdateSectionTitle: "Mettre à jour l'événement", - eventDeleteButton: "Supprimer", eventCreateButton: "Créer un événement", eventAddToCalendar: "Ajouter au calendrier", eventVisitCalendar: "Voir le calendrier", viewTask: "Voir la tâche", + viewEvent: "Voir l’événement", + viewPad: "Voir le pad", + viewMap: "Voir la carte", + viewCalendar: "Voir le calendrier", viewReport: "Voir le rapport", viewTransfer: "Voir le transfert", viewItem: "Voir l'élément", viewShop: "Voir la boutique", viewProject: "Voir le projet", + viewJob: "Voir l'Emploi", + viewPlace: "Voir le Lieu", + generatePdf: "Générer un PDF", + sharePm: "Partager par MP", + galleryImages: "Images (taille max : 50 Mo chacune)", + galleryPhotos: "Images", + galleryAddPhoto: "Ajouter une Image", + galleryRemovePhoto: "Retirer", + galleryVideo: "Vidéo (taille max : 50 Mo)", + galleryAddVideo: "Ajouter une Vidéo", + galleryRemoveVideo: "Retirer la vidéo", viewVotation: "Voir le vote", voteCastTitle: "Voter", voteResults: "Résultats", @@ -1330,48 +1411,26 @@ module.exports = { eventDescriptionLabel: "Description", eventDescriptionPlaceholder: "Saisissez la description de l'événement...", eventUpdateButton: "Mettre à jour", - eventAttendeesLabel: "Participants", - eventAttendeesPlaceholder: "Saisissez les participants, séparés par des virgules...", eventTagsLabel: "Étiquettes", - eventTagsPlaceholder: "Saisissez les étiquettes de l'événement, séparées par des virgules...", - eventTags: "Étiquettes", eventPriceLabel: "Prix", eventUrlLabel: "URL", eventAttendees: "Participants", - noAttendees: "Aucun participant pour l'instant", - eventCreatedAt: "Créé le", eventLocation: "Localisation", eventLocationLabel: "Localisation", - eventNoLocation: "Aucune localisation spécifiée", - eventNoURL: "Aucune URL spécifiée", - eventBy: "Par", noevents: "Aucun événement disponible.", eventDate: "Date", - eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Assister à l'événement", - eventUnattendButton: "Ne plus assister à l'événement", - eventCreatedBy: "Créé par", - eventAttendeesCount: "Nombre de participants", - eventCreatedByYou: "Vous avez créé cet événement", eventFilterAll: "TOUS", + eventFilterRecent: "RÉCENTS", eventFilterMine: "MIENS", eventFilterToday: "AUJOURD'HUI", eventFilterWeek: "CETTE SEMAINE", eventFilterMonth: "CE MOIS", eventFilterYear: "CETTE ANNÉE", eventFilterArchived: "ARCHIVÉS", - eventTodayTitle: "Événements d'aujourd'hui", - eventThisWeekTitle: "Événements cette semaine", - eventThisMonthTitle: "Événements ce mois", - eventThisYearTitle: "Événements cette année", eventPrivacyLabel: "Visibilité", eventPublic: "Public", eventPrivate: "Privé", - eventPublicTitle: "Événements publics", - eventNoPrice: "Événement gratuit", - eventNoImage: "Aucune image téléchargée", - eventAttendConfirmation: "Vous assistez maintenant à cet événement", - eventUnattendConfirmation: "Vous ne participez plus à cet événement", eventAttended: "Présent", eventUnattended: "Absent", eventStatusOpen: "Ouvert", @@ -1382,9 +1441,14 @@ module.exports = { tagsTopSectionTitle: "Étiquettes principales", tagsCloudSectionTitle: "Nuage d'étiquettes", tagsFilterAll: "TOUS", + tagsFilterMine: "À MOI", + tagsFilterRecent: "RÉCENTES", + tagsMineSectionTitle: "Mes étiquettes", + tagsRecentSectionTitle: "Étiquettes récentes", tagsFilterTop: "TOP", tagsFilterCloud: "NUAGE", tagsNoItems: "Aucune étiquette disponible.", + tagsSearchPlaceholder: "Rechercher dans Étiquettes...", tagsTableHeaderTag: "ÉTIQUETTE/LINK :", tagsTableHeaderCount: "COMPTEUR :", transfersTitle: "Transferts", @@ -1392,7 +1456,10 @@ module.exports = { transfersFrom: "De", transfersTo: "À", transfersFilterAll: "TOUS", + transfersFilterRecent: "RÉCENTS", transfersFilterMine: "MIENS", + transfersFilterMinePending: "EN ATTENTE", + transfersFilterMineConfirmed: "SIGNÉS", transfersFilterUBI: "RBU", transfersFilterMarket: "MARCHÉ", transfersFilterTop: "TOP", @@ -1407,7 +1474,6 @@ module.exports = { transfersCategoryEconomic: "Économique", transfersCategoryTime: "Temps", transfersCategoryTrust: "Confiance", - transfersCategoryApply: "Appliquer", transfersUnitEco: "ECO", transfersUnitHours: "h", transfersUnitTrust: "confiance", @@ -1424,18 +1490,12 @@ module.exports = { transfersDeadline: "Date limite", transfersTags: "Étiquettes", transfersStatus: "État", - transfersStatusUnconfirmed: "NON CONFIRMÉE", - transfersStatusClosed: "FERMÉE", - transfersStatusDiscarded: "REJETÉE", - transfersCreatedAt: "Créé le", transfersConfirmations: "CONFIRMATIONS", - transfersContainingBlock: "Bloc contenant", - transfersExportContract: "Créer le contrat", + transfersExportContract: "Générer une Facture", transfersConfirmButton: "Confirmer le transfert", transfersNoItems: "Aucun transfert trouvé.", transfersMineSectionTitle: "Vos transferts", transfersUBISectionTitle: "Transferts RBU", - transfersMarketSectionTitle: "Transferts du marché", transfersTopSectionTitle: "Transferts principaux", transfersPendingSectionTitle: "Transferts en attente", transfersUnconfirmedSectionTitle: "Transferts non confirmés", @@ -1443,21 +1503,13 @@ module.exports = { transfersDiscardedSectionTitle: "Transferts rejetés", transfersCreateSectionTitle: "Créer un transfert", transfersAllSectionTitle: "Transferts", - transfersFilterFavs: "Favoris", - transfersFavsSectionTitle: "Transferts favoris", - transfersSearchLabel: "Recherche", - transfersSearchPlaceholder: "Rechercher concept, tags, utilisateurs...", + transfersSearchPlaceholder: "Rechercher dans Transferts...", transfersMinAmountLabel: "Montant min.", transfersMaxAmountLabel: "Montant max.", - transfersSortLabel: "Trier par", transfersSortRecent: "Plus récents", transfersSortAmount: "Montant le plus élevé", transfersSortDeadline: "Échéance la plus proche", transfersSearchButton: "Rechercher", - transfersFavoriteButton: "Favori", - transfersUnfavoriteButton: "Retirer favori", - transfersMessageUserButton: "Message", - transfersExpiringSoonBadge: "BIENTÔT", transfersExpiredBadge: "EXPIRÉ", transfersUpdatedAt: "Mis à jour", transfersNoMatch: "Aucun transfert ne correspond à votre recherche.", @@ -1470,6 +1522,10 @@ module.exports = { voteClosedTitle: "Votes fermés", voteAllSectionTitle: "Gouvernance", voteCreateButton: "Créer un vote", + voteErrorTitle: "Vérifiez le formulaire", + voteErrorDeadlineMin: "La date limite doit être d’au moins {days} jours à partir de maintenant.", + voteErrorGeneric: "Le vote n’a pas pu être enregistré.", + voteLocked: "Ce vote a déjà reçu des votes ou des avis, ou appartient au Parlement ou aux Tribunaux : il ne peut plus être modifié ni supprimé.", voteUpdateButton: "Mettre à jour", voteDeleteButton: "Supprimer", voteOptionYes: "OUI", @@ -1479,6 +1535,7 @@ module.exports = { voteFollowMajority: "SUIVRE LA MAJORITÉ", voteNotInterested: "SANS INTÉRÊT", voteFilterAll: "TOUS", + voteFilterRecent: "RÉCENTS", voteFilterMine: "MIENS", voteFilterOpen: "OUVERTS", voteFilterClosed: "FERMÉS", @@ -1494,37 +1551,28 @@ module.exports = { voteTags: "Étiquettes", voteOpinions: "Opinions", novotes: "Aucune proposition de vote disponible.", - voteBy: "Par", voteCreatedAt: "Créé le", voteNoQuestion: "Aucune question fournie", voteUnknownCreator: "Créateur inconnu", voteUnknownDate: "Date inconnue", - errorVoteNotFound: "Vote introuvable", - errorAlreadyVoted: "Vous avez déjà voté.", - errorVoteClosedCannotEdit: "Impossible de modifier un vote fermé", - errorVoteDeadlinePassed: "Le délai de ce vote est passé", - errorRetrievingVote: "Erreur lors de la récupération du vote", - errorCreatingVote: "Erreur lors de la création du vote", - errorVoteAlreadyVoted: "Impossible de modifier après avoir voté", - errorDeletingOldVote: "Erreur lors de la suppression du vote précédent", - errorCreatingUpdatedVote: "Erreur lors de la création du vote mis à jour", - errorCreatingTombstone: "Erreur lors de la création de la pierre tombale", voteDetailSectionTitle: 'Détails du vote', voteCommentsLabel: 'Commentaires', voteCommentsForumButton: 'Ouvrir la discussion', voteCommentsSectionTitle: 'Discussion ouverte', voteNoCommentsYet: 'Il n’y a pas encore de commentaires. Soyez le premier à répondre.', + commentsGoToLast: "Aller au dernier commentaire", voteNewCommentPlaceholder: 'Écrivez votre commentaire ici…', voteNewCommentButton: 'Publier le commentaire', voteNewCommentLabel: 'Ajouter un commentaire', cvTitle: "CV", - cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Modifier le CV", cvCreateSectionTitle: "Créer un CV", cvDescription: "Gérez et partagez vos compétences professionnelles et informations.", cvNameLabel: "Nom complet", cvDescriptionLabel: "Résumé", cvPhotoLabel: "Photo", + cvPdfLabel: "CV (PDF)", + cvVisitButton: "Voir le CV", cvPersonalExperiencesLabel: "Expériences personnelles", cvPersonalSkillsLabel: "Compétences personnelles (séparées par des virgules)", cvOasisExperiencesLabel: "Expériences de contribution dans Oasis", @@ -1537,29 +1585,130 @@ module.exports = { cvLocationLabel: "Localisation", cvStatusLabel: "État", cvPreferencesLabel: "Préférences", - cvOasisContributorLabel: "Contributeur d’Oasis", cvPersonal: "Personnel", cvOasis: "Contributeur d’Oasis (optionnel)", - cvOasisContributorView: "Contribution dans Oasis", + cvOasisContributorView: "Contribution", cvEducational: "Éducatif (optionnel)", cvEducationalView: "Éducatif", cvProfessional: "Professionnel (optionnel)", cvProfessionalView: "Professionnel", cvAvailability: "Disponibilité (optionnel)", - cvAvailabilityView: "Disponibilité", cvUpdateButton: "Mettre à jour", cvCreateButton: "Créer un CV", - cvContactLabel: "Contact", cvCreatedAt: "Créé le", cvUpdatedAt: "Mis à jour le", cvEditButton: "Mettre à jour", cvDeleteButton: "Supprimer", cvNoCV: "Aucun CV trouvé.", blogSubject: "Sujet", - blogMessage: "Corps limité à 8096 caractères.", + blogMessage: "Message", blogImage: "Télécharger un média (max: 50 Mo)", - blogPublish: "Aperçu", - noPopularMessages: "Aucun message populaire publié pour l’instant", + blogMedia: "Pièces jointes (jusqu'à 8 images et une vidéo)", + blogPublish: "Publier", + pollsTitle: "Sondages", + dataTitle: "Correspondances", + dataDescription: "Explorez et visualisez les correspondances de votre réseau.", + dataFilterAll: "TOUT", + dataFilterMine: "LES MIENS", + dataFilterRecent: "RÉCENTS", + dataFilterTop: "TOP", + dataKindInhabitants: "HABITANTS", + dataKindJobs: "EMPLOIS", + dataKindProjects: "PROJETS", + dataKindEvents: "ÉVÉNEMENTS", + dataKindTribes: "TRIBUS", + dataKindMarket: "MARCHÉ", + dataKindHousing: "LOGEMENT", + dataKindIndustry: "INDUSTRIE", + dataKindTasks: "TÂCHES", + dataKindReports: "RAPPORTS", + dataKindVotes: "VOTES", + dataSearchPlaceholder: "Rechercher dans Correspondances...", + dataCohesionTitle: "Coefficient de cohésion (CC)", + dataCcShort: "CC", + dataCohesionHint: "Affinité moyenne entre tout ce que le réseau a publié.", + dataAffinity: "Affinité", + dataCommonTerms: "Mot-clé", + dataReasonFollowing: "Vous soutenez cet habitant", + dataReasonTribe: "Partage une tribu avec vous", + dataReasonPinned: "Correspond à un contenu que vous avez épinglé", + dataReasonCv: "Correspond aux compétences de votre CV", + dataReasonContent: "Lié à ce que vous publiez", + dataReasonRelated: "Lié à vos centres d’intérêt", + dataReasonAlike: "Publié par un habitant qui vous ressemble", + dataReasonSupportsYou: "Vous soutient", + dataReasonMutual: "Soutien mutuel", + dataReasonRated: "Bien noté par le réseau", + dataReasonNear: "Près de chez vous", + dataStatPeople: "CV", + dataStatConnected: "Connectés", + dataStatSkills: "Compétences", + dataBestMatch: "Meilleur match", + dataStatIsolated: "Isolés", + dataStatEntities: "Entités comparées", + dataStatTerms: "Termes distincts", + dataStatPairs: "Paires connectées", + dataMatchesTitle: "Correspondances", + dataMatchesHint: "Suggestions personnalisées de l'algorithme.", + dataTermsTitle: "Mots-clés", + dataTermsHint: "Les termes présents dans le plus de contenus.", + dataConnections: "Correspondances liées", + dataNoMatches: "Aucune donnée croisée à afficher.", + dataNoProfile: "Publiez du contenu ou rédigez votre CV pour que l'algorithme apprenne ce qui vous intéresse.", + pollsDescription: "Module pour interroger le réseau et compter les réponses.", + pollFilterAll: "TOUS", + pollFilterMine: "LES MIENS", + pollFilterRecent: "RÉCENTS", + pollFilterTop: "TOP", + pollFilterVoted: "VOTÉS", + pollFilterOpen: "OUVERTS", + pollFilterClosed: "FERMÉS", + pollCreateButton: "Créer un Sondage", + pollCreateTitle: "Créer un Sondage", + pollEditTitle: "Modifier le Sondage", + pollQuestion: "Question", + pollQuestionPlaceholder: "Que voulez-vous demander ?", + pollOptions: "Options (une par ligne)", + pollOutcome: "Résultat", + pollNoVotesYet: "Pas encore de votes", + pollTie: "Égalité", + pollOptionsPlaceholder: "Place\nParc\nBar", + pollAnonymous: "Anonyme", + pollAnonymousLabel: "Sondage anonyme", + pollMultiple: "Choix multiple", + pollMultipleLabel: "Autoriser plusieurs réponses", + pollDeadline: "Date limite", + pollTags: "Étiquettes", + pollPublishButton: "Publier le Sondage", + pollUpdateButton: "Modifier", + pollCloseButton: "Fermer", + pollDeleteButton: "Supprimer", + pollVoteButton: "Voter", + pollChangeVote: "Changer de Vote", + pollVoters: "Votants", + pollComments: "Commentaires", + pollStatusLabel: "Statut", + pollStatusOpen: "OUVERT", + pollStatusClosed: "FERMÉ", + pollSearchPlaceholder: "Rechercher dans Sondages...", + pollsNoItems: "Aucun sondage trouvé.", + viewPoll: "Voir le Sondage", + pollChatCreate: "Créer un Sondage", + pollInChat: "Sondage", + blogTitle: "Blogs", + blogDescription: "Module pour découvrir et gérer les blogs.", + blogFilterAll: "TOUS", + blogFilterMine: "LES MIENS", + blogFilterRecent: "RÉCENTS", + blogFilterFavorites: "FAVORIS", + blogFilterTop: "TOP", + blogCreateButton: "Créer un Blog", + blogSearchPlaceholder: "Rechercher dans Blogs...", + blogComments: "Commentaires", + blogAllowComments: "Autoriser les commentaires", + blogCommentsClosed: "L'auteur a fermé les commentaires de ce blog.", + blogNoItems: "Aucun blog trouvé.", + viewBlog: "Voir le Blog", forumTitle: "Forums", forumCategoryLabel: "Catégorie", forumTitleLabel: "Titre", @@ -1567,43 +1716,21 @@ module.exports = { forumCreateButton: "Créer un forum", forumCreateSectionTitle: "Créer un forum", forumDescription: "Discutez ouvertement avec d’autres habitants de votre réseau.", + forumSearchPlaceholder: "Rechercher dans Forums...", forumFilterAll: "TOUS", forumFilterMine: "MIENS", forumFilterRecent: "RÉCENTS", forumFilterTop: "TOP", - forumMineSectionTitle: "Vos forums", - forumRecentSectionTitle: "Forums récents", - forumAllSectionTitle: "Forums", - forumDeleteButton: "Supprimer", forumParticipants: "participants", forumMessages: "messages", - forumLastMessage: "Dernier message", forumMessageLabel: "Message", forumMessagePlaceholder: "Écrivez votre message...", forumSendButton: "Envoyer", - forumVisitForum: "Visiter le forum", noForums: "Aucun forum disponible pour l’instant.", - forumVisitButton: "Visiter le forum", - forumCatGENERAL: "Général", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "Politique", - forumCatTECH: "Technologie", - forumCatSCIENCE: "Science", - forumCatMUSIC: "Musique", - forumCatART: "Art", - forumCatGAMING: "Jeux", - forumCatBOOKS: "Livres", - forumCatFILMS: "Films", - forumCatPHILOSOPHY: "Philosophie", - forumCatSOCIETY: "Société", - forumCatPRIVACY: "Vie privée", - forumCatCYBERWARFARE: "Cyberguerre", - forumCatSURVIVALISM: "Survivalisme", + forumVisitButton: "Visiter le Forum", + forumTopicLabel: "Sujet", imageTitle: "Images", imageDescription: "Explorez et gérez du contenu image dans votre réseau.", - imagePluginTitle: "Titre", - imagePluginDescription: "Description", imageMineSectionTitle: "Vos images", imageCreateSectionTitle: "Téléverser une image", imageUpdateSectionTitle: "Mettre à jour l'image", @@ -1612,17 +1739,14 @@ module.exports = { imageTopSectionTitle: "Top images", imageFavoritesSectionTitle: "Favoris", imageGallerySectionTitle: "Galerie", - imageMemeSectionTitle: "Mèmes", imageFilterAll: "TOUS", imageFilterMine: "À MOI", imageFilterRecent: "RÉCENTES", imageFilterTop: "TOP", imageFilterFavorites: "FAVORIS", imageFilterGallery: "GALERIE", - imageFilterMeme: "MÈMES", imageCreateButton: "Téléverser une image", imageUpdateButton: "Mettre à jour", - imageDeleteButton: "Supprimer", imageAddFavoriteButton: "Ajouter aux favoris", imageRemoveFavoriteButton: "Retirer des favoris", imageFileLabel: "Sélectionnez un fichier image (.jpeg, .jpg, .png, .gif)", @@ -1632,17 +1756,16 @@ module.exports = { imageTitlePlaceholder: "Facultatif", imageDescriptionLabel: "Description", imageDescriptionPlaceholder: "Facultatif", - imageMemeLabel: "MÈME ?", imageNoFile: "Aucun fichier image fourni", noImages: "Aucune image disponible.", - imageSearchPlaceholder: "Rechercher par titre, tags, description, auteur…", + imageSearchPlaceholder: "Rechercher dans Images...", imageSortRecent: "Les plus récentes", imageSortOldest: "Les plus anciennes", imageSortTop: "Les plus votées", imageSearchButton: "Rechercher", - imageMessageAuthorButton: "Message", imageUpdatedAt: "Mis à jour", imageNoMatch: "Aucune image ne correspond à votre recherche.", + feedSearchPlaceholder: "Rechercher dans Fil...", feedTitle: "Fil", feedDetailTitle: "Fil", feedOpenDiscussion: "Ouvrir la discussion", @@ -1652,7 +1775,6 @@ module.exports = { createFeedButton: "Envoyer le fil !", feedPlaceholder: "Que se passe-t-il ? (maximum 280 caractères)", TODAYButton: "AUJOURD’HUI", - CREATEButton: "Créer un fil", totalOpinions: "Total des opinions", moreVoted: "Le plus voté", noFeedsFound: "Aucun fil trouvé.", @@ -1662,7 +1784,7 @@ module.exports = { alreadyRefeeded: "Vous avez déjà repartagé ceci.", activityTitle: "Activité", yourActivity: "Votre activité", - globalActivity: "Activité globale", + globalActivity: "Activité", activityList: "Activité", activityDesc: "Découvrez l’activité récente de votre réseau.", allButton: "TOUT", @@ -1674,20 +1796,13 @@ module.exports = { concept: "Concept", description: "Description", meme: "Mème", - activityContact: "Contact", - activityBy: "Nom", - activityPixelia: "Nouveau pixel ajouté", - viewImage: "Voir l’image", - playAudio: "Lire l’audio", - playVideo: "Lire la vidéo", typeRecent: "RÉCENT", - errorActivity: "Erreur lors de la récupération de l’activité", - typePost: "BLOGS", - recentButton: "RÉCENT", + typeTop: "TOP", + typePost: "BLOG", + activityUpdateLabel: "MISE À JOUR", typeTribe: "TRIBUS", typeLarp: "L.A.R.P.", typeInhabitants: "HABITANTS", - activityProfileUpdated: "a mis à jour son profil", typeAbout: "HABITANTS", typeCurriculum: "CVS", typeImage: "IMAGES", @@ -1712,7 +1827,7 @@ module.exports = { typeTombstone: "SUPPRIMÉ", typeBanking: "BANQUE", typeBankWallet: "BANQUE/PORTEFEUILLE", - typeBankClaim: "BANQUE/UBI", + typeBankClaim: "BANQUE/RBU", typeKarmaScore: "KARMA", typeParliament: "PARLEMENT", typeSpread: "SOUTIENS", @@ -1731,8 +1846,6 @@ module.exports = { typeCourtsSettlementAccepted: "Tribunaux · Règlement accepté", typeCourtsNomination: "Tribunaux · Nomination", typeCourtsNominationVote: "Tribunaux · Vote de nomination", - activitySupport: "Nouvelle alliance forgée", - activityJoin: "Nouveau PUB rejoint", question: "Question", deadline: "Échéance", status: "Statut", @@ -1746,21 +1859,16 @@ module.exports = { date: "Date", category: "Catégorie", attendees: "Participants", - activitySpread: "->", - visitLink: "Visiter le lien", - viewDocument: "Voir le document", location: "Lieu", contentWarning: "Sujet", - personName: "Nom de l’habitant", bankWalletConnected: "Portefeuille ECOin", - bankUbiReceived: "UBI reçu", + bankUbiReceived: "RBU reçue", bankTx: "Tx", bankEpochShort: "Époque", viewDetails: "Voir les détails", link: "Lien", activityProjectFollow: "%OASIS% est maintenant %ACTION% ce projet %PROJECT%", activityProjectUnfollow: "%OASIS% est maintenant %ACTION% ce projet %PROJECT%", - activityProjectPledged: "%OASIS% a %ACTION% %AMOUNT% au projet %PROJECT%", following: "SUIVRE", unfollowing: "NE PLUS SUIVRE", pledged: "ENGAGÉ", @@ -1806,26 +1914,17 @@ module.exports = { courtsVotesSlashTotal: "OUI/TOTAL", courtsOpenVote: "Vote ouvert", courtsAnswerTitle: "Réponse", - courtsStanceADMIT: "Admettre", - courtsStanceDENY: "Refuser", - courtsStancePARTIAL: "Partiel", - courtsStanceCOUNTERCLAIM: "Demande reconventionnelle", - courtsStanceNEUTRAL: "Neutre", courtsVerdictResult: "Résultat", courtsVerdictOrders: "Ordonnances", courtsSettlementText: "Accord", courtsSettlementAccepted: "Accepté", - courtsSettlementPending: "En attente", courtsJudge: "Juge", courtsThSupports: "Soutiens", courtsFilterOpenCase: 'Ouvrir un dossier', - courtsEvidenceFileLabel: 'Fichier de preuve (image, audio, vidéo ou PDF)', - courtsCaseMediators: 'Médiateurs', courtsCaseMediatorsPh: 'IDs Oasis des médiateurs, séparés par des virgules', courtsMediatorsLabel: 'Médiateurs', courtsThCase: 'Dossier', courtsThCreatedAt: 'Date de début', - courtsThActions: 'Actions', courtsPublicPrefLabel: 'Visibilité après la résolution', courtsPublicPrefYes: "J'accepte que ce dossier soit entièrement public", courtsPublicPrefNo: 'Je préfère garder les détails privés', @@ -1833,8 +1932,11 @@ module.exports = { courtsMethodMEDIATION: 'Médiation', courtsNoCases: 'Aucun dossier.', reportsTitle: "Rapports", - reportsDescription: "Gérez et suivez les rapports liés aux problèmes, aux erreurs, aux abus et aux avertissements de contenu dans votre réseau.", + reportsDescription: "Gérez et suivez les signalements de votre réseau.", + reportsSearchPlaceholder: "Rechercher dans Rapports...", reportsFilterAll: "TOUS", + reportsFilterRecent: "RÉCENTS", + reportsFilterTop: "TOP", reportsFilterMine: "MIENS", reportsFilterFeatures: "FONCTIONS", reportsFilterBugs: "ERREURS", @@ -1847,18 +1949,13 @@ module.exports = { reportsFilterInvalid: "INVALIDES", reportsCreateButton: "Créer un rapport", reportsTitleLabel: "Titre", - reportsDescriptionLabel: "Description", - reportsDescriptionPlaceholder: "Veuillez fournir une description détaillée.", reportsCategory: "Catégorie", - reportsCategoryLabel: "Catégorie", reportsCategoryFeatures: "Fonctions", reportsCategoryBugs: "Erreurs", reportsCategoryAbuse: "Abus", - reportsCategoryContent: "Problèmes de Contenu", + reportsCategoryContent: "Contenu", reportsUpdateButton: "Mettre à jour", reportsDeleteButton: "Supprimer", - reportsDateLabel: "Date", - reportsUploadFile: "Télécharger un média (max: 50 Mo)", reportsMineSectionTitle: "Vos rapports", reportsFeaturesSectionTitle: "Demandes de fonctions", reportsBugsSectionTitle: "Erreurs", @@ -1866,11 +1963,6 @@ module.exports = { reportsContentSectionTitle: "Problèmes de contenu", reportsAllSectionTitle: "Rapports", reportsNoItems: "Aucun rapport disponible.", - reportsValidationTitle: "Veuillez saisir un titre valide.", - reportsValidationDescription: "La description ne peut pas être vide.", - reportsValidationCategory: "Veuillez sélectionner une catégorie.", - reportsCreatedAt: "Créé le", - reportsCreatedBy: "Par", reportsSeverity: "Gravité", reportsSeverityLow: "Faible", reportsSeverityMedium: "Moyenne", @@ -1881,18 +1973,14 @@ module.exports = { reportsStatusUnderReview: "En révision", reportsStatusResolved: "Résolu", reportsStatusInvalid: "Invalide", - reportsUpdateStatusButton: "Mettre à jour l’état", - reportsAnonymityOption: "Envoyer de façon anonyme", - reportsAnonymousAuthor: "Anonyme", reportsConfirmButton: "CONFIRMER LE RAPPORT !", reportsConfirmations: "Confirmations", reportsConfirmedSectionTitle: "Rapports confirmés", - reportsCreateTaskButton: "CRÉER UNE TÂCHE", + reportsCreateTaskButton: "Créer une tâche", reportsOpenSectionTitle: "Rapports ouverts", reportsUnderReviewSectionTitle: "Rapports en révision", reportsResolvedSectionTitle: "Rapports résolus", reportsInvalidSectionTitle: "Rapports invalides", - reportsTemplateSectionTitle: 'Modèle de signalement', reportsBugTemplateTitle: 'Détails de reproduction (Bugs)', reportsFeatureTemplateTitle: 'Détails (Fonctionnalités)', reportsAbuseTemplateTitle: 'Détails (Abus)', @@ -1924,6 +2012,20 @@ module.exports = { reportsRequestedActionLabel: 'Action demandée', reportsRequestedActionPlaceholder: 'Supprimer, masquer, étiqueter, avertir, etc.', tribesTitle: "Tribus", + tribeFilterAll: "TOUTES", + tribeFilterRecent: "RÉCENTES", + tribeFilterMine: "MIENNES", + tribeFilterMembership: "ADHÉSION", + tribeFilterSubtribes: "SOUS-TRIBUS", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALERIE", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "MIENS", + tribeFeedFilterALL: "TOUS", + tribeFeedFilterRECENT: "RÉCENTS", + courtsStanceDENY: "Nier", + courtsStanceADMIT: "Admettre", + courtsStancePARTIAL: "Admettre en partie", tribeAllSectionTitle: "Tribus", tribeMineSectionTitle: "Vos tribus", tribeCreateSectionTitle: "Créer une tribu", @@ -1931,42 +2033,37 @@ module.exports = { tribeGallerySectionTitle: "Galerie de tribus", tribeRecentSectionTitle: "Tribus récentes", tribeTopSectionTitle: "Tribus populaires", - tribeviewTribeButton: "Visiter la tribu", - tribeviewSubTribeButton: "Visiter la sous-tribu", tribeRootLabel: "RACINE", tribeDescription: "Explorez ou créez des tribus dans votre réseau.", - tribeFilterAll: "TOUS", - tribeFilterMine: "MIENS", - tribeFilterMembership: "MEMBRES", - tribeFilterRecent: "RÉCENTS", - tribeFilterTop: "TOP", - tribeFilterSubtribes: "SOUS-TRIBUS", - tribeFilterGallery: "GALERIE", tribeMainTribeLabel: "TRIBU PRINCIPALE", tribeCreateButton: "Créer une tribu", tribeUpdateButton: "Mettre à jour", tribeDeleteButton: "Supprimer", tribeImageLabel: "Télécharger un média (max: 50 Mo)", tribeTitleLabel: "Titre", - searchTribesPlaceholder: "FILTRER les tribus PAR NOM …", + searchTribesPlaceholder: "Rechercher dans Tribus...", tribeTitlePlaceholder: "Nom de la tribu", tribeDescriptionLabel: "Description", tribeDescriptionPlaceholder: "Décrivez cette tribu", tribeLocationLabel: "Localisation", tribeLocationPlaceholder: "Où se situe cette tribu ?", tribeTagsLabel: "Étiquettes", - tribeTagsPlaceholder: "Saisissez des étiquettes séparées par des virgules", + tribeTagsPlaceholder: "Saisissez des tags séparés par des virgules", tribeInviteMode: "Mode d’invitation", tribeModeLabel: "MODE", tribeIsAnonymousLabel: "STATUT", tribeMembersCount: "Membres", - tribeInviteCodePlaceholder: "Saisissez le code d’invitation", - tribeJoinByCodeButton: "Rejoindre avec un code", - tribeJoinButton: "REJOINDRE", tribeEnterInvite: "SAISIR LE CODE", + subscriptionTitle: "Abonnements", + subscriptionOn: "Abonné", + subscriptionOff: "Non abonné", + subscriptionSubscribe: "S'abonner", + subscriptionUnsubscribe: "Se désabonner", + subscriptionSubscribers: "Abonnés", + pmListLabel: "Envoyer à", + pmListNone: "— Aucune liste —", + pmListNoSubscribers: "Aucun abonné", tribeLeaveButton: "QUITTER", - tribeYes: "OUI", - tribeNo: "NON", tribePublic: "PUBLIQUE", tribePrivate: "PRIVÉE", tribeGenerateInvite: "INVITATION UNIQUE", @@ -1975,13 +2072,8 @@ module.exports = { tribeRemoveInvitation: "SUPPRIMER L’INVITATION", tribeCreatedAt: "Créé le", tribeAuthor: "Par", - tribeAuthorLabel: "AUTEUR", tribeStrict: "Strict", tribeOpen: "Ouverte", - tribeFeedFilterRECENT: "RÉCENTS", - tribeFeedFilterMINE: "MIENS", - tribeFeedFilterALL: "TOUS", - tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Repartages", tribeFeedRefeed: "Repartager", tribeFeedMessagePlaceholder: "Écrivez un feed…", @@ -1991,33 +2083,28 @@ module.exports = { tribeNotFound: "Tribu introuvable!", createTribeTitle: "Créer une tribu", updateTribeTitle: "Mettre à jour la tribu", - tribeSectionOverview: "Aperçu", tribeSectionInhabitants: "Habitants", tribeSectionVotations: "VOTATIONS", tribeSectionEvents: "ÉVÉNEMENTS", - tribeSectionReports: "Rapports", tribeSectionTasks: "TÂCHES", tribeSectionFeed: "FIL", tribeSectionForum: "FORUM", - tribeSectionMarket: "Marché", - tribeSectionJobs: "Emplois", - tribeSectionProjects: "Projets", tribeSectionMedia: "Média", tribeSectionImages: "IMAGES", tribeSectionAudios: "AUDIOS", tribeSectionVideos: "VIDÉOS", tribeSectionDocuments: "DOCUMENTS", tribeSectionBookmarks: "SIGNETS", - tribeSectionMaps: "MAPS", + tribeSectionMaps: "CARTES", tribeSectionPads: "PADS", tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionCalendars: "CALENDRIERS", + tribePadCreate: "Créer un pad", + tribeChatCreate: "Créer un chat", + tribeCalendarCreate: "Créer un calendrier", + tribePadsEmpty: "Aucun pad pour l'instant.", + tribeChatsEmpty: "Aucun chat pour l'instant.", + tribeCalendarsEmpty: "Aucun calendrier pour l'instant.", tribeInhabitantsEmpty: "Aucun habitant dans cette tribu pour l'instant.", tribeEventCreate: "Créer un événement", tribeEventsEmpty: "Aucun événement pour l'instant.", @@ -2039,11 +2126,6 @@ module.exports = { tribeTaskStatusClosed: "FERMER", tribeTaskAssign: "ASSIGNER", tribeTaskUnassign: "DÉSASSIGNER", - tribeReportCreate: "Créer un rapport", - tribeReportsEmpty: "Aucun rapport pour l'instant.", - tribeReportTitle: "Titre", - tribeReportDescription: "Description", - tribeReportCategory: "Catégorie", tribeVotationCreate: "Créer une votation", tribeVotationsEmpty: "Aucune votation pour l'instant.", tribeVotationTitle: "Titre", @@ -2061,27 +2143,6 @@ module.exports = { tribeForumCategory: "Catégorie", tribeForumReply: "Répondre", tribeForumReplies: "Réponses", - tribeMarketCreate: "Créer une annonce", - tribeMarketEmpty: "Aucune annonce pour l'instant.", - tribeMarketTitle: "Titre", - tribeMarketDescription: "Description", - tribeMarketPrice: "Prix", - tribeMarketImage: "Image", - tribeMarketCategory: "Catégorie", - tribeJobCreate: "Créer un emploi", - tribeJobsEmpty: "Aucun emploi pour l'instant.", - tribeJobTitle: "Titre", - tribeJobDescription: "Description", - tribeJobLocation: "Lieu", - tribeJobSalary: "Salaire", - tribeJobDeadline: "Date limite", - tribeProjectCreate: "Créer un projet", - tribeProjectsEmpty: "Aucun projet pour l'instant.", - tribeProjectTitle: "Titre", - tribeProjectDescription: "Description", - tribeProjectGoal: "Objectif", - tribeProjectFunded: "Financé", - tribeProjectDeadline: "Date limite", tribeMediaUpload: "Télécharger un média", readDocument: "Lire le Document", tribeCreateImage: "Créer Image", @@ -2092,21 +2153,20 @@ module.exports = { tribeMediaEmpty: "Aucun média pour l'instant.", tribeMediaTitle: "Titre", tribeMediaDescription: "Description", - tribeMediaType: "Type", tribeMediaTypeImage: "Image", tribeMediaTypeVideo: "Vidéo", tribeMediaTypeAudio: "Audio", tribeMediaTypeDocument: "Document", tribeMediaTypeBookmark: "Signet", tribeContentDelete: "SUPPRIMER", + tribeReachTribe: "TRIBU", + tribeReachNotice: "OASIS le montre à tous les habitants et CLEARNET aussi sur le web ouvert. L'ouvrir laisse une copie lisible hors de la tribu : revenir à TRIBU le masque, mais les copies déjà partagées ne peuvent pas être effacées.", + tribeReachPrivate: "Le contenu d'une tribu privée ne peut rester que dans la tribu.", + tribeReachOpenedBy: "Ouvert par", + tribeReachChange: "Où c'est publié", tribeInviteCodeText: "Code d'invitation : ", oasisVersionLabel: "Version d’Oasis", - tribeInviteCodeHint: "Partagez ce code avec la personne que vous souhaitez inviter. Elle peut rejoindre via /invites → Tribes.", - tribeGroupTribe: "Tribu", - tribeGroupOffice: "Bureau", - tribeGroupNetwork: "Réseau", - tribeGroupEconomy: "Économie", - tribeGroupMedia: "Média", + tribeInviteCodeHint: "Partagez ce code avec la personne que vous souhaitez inviter. Elle peut rejoindre via /invites.", tribeStatusOpen: "OUVERT", tribeStatusClosed: "FERMÉ", tribeStatusInProgress: "EN COURS", @@ -2116,33 +2176,17 @@ module.exports = { tribePriorityCritical: "CRITIQUE", tribeTaskFilterAll: "TOUS", tribeMediaFilterAll: "TOUS", - tribeReportCatBug: "BUG", - tribeReportCatAbuse: "ABUS", - tribeReportCatContent: "CONTENU", - tribeReportCatOther: "AUTRE", tribeForumCatGeneral: "GÉNÉRAL", tribeForumCatProposal: "PROPOSITION", tribeForumCatQuestion: "QUESTION", tribeForumCatAnnouncement: "ANNONCE", - tribeMarketCatGoods: "BIENS", - tribeMarketCatServices: "SERVICES", - tribeMarketCatFood: "ALIMENTATION", - tribeMarketCatOther: "AUTRE", tribeStatusLabel: "Statut", tribeSubTribes: "SOUS-TRIBUS", tribeSubTribesCreate: "Créer Sous-Tribu", tribeSubTribesStrictDenied: "Le mode strict de la tribu ne vous permet pas de créer de nouvelles sous-tribus. Veuillez contacter l'administrateur.", - tribeSubTribesEmpty: "Aucune sous-tribu créée, pour l'instant.", - tribeActivityJoined: "REJOINT", - tribeActivityLeft: "QUITTÉ", - tribeActivityFeed: "FIL", tribeActivityRefeed: "REPARTAGE", - tribeGroupAnalytics: "Analytiques", - tribeGroupCreative: "Créatif", tribeSectionActivity: "ACTIVITÉ", tribeSectionTrending: "TENDANCES", - tribeSectionOpinions: "OPINIONS", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAGS", tribeSectionSearch: "RECHERCHE", tribeActivityEmpty: "Aucune activité pour le moment.", @@ -2154,25 +2198,13 @@ module.exports = { tribeTrendingPeriodWeek: "Cette Semaine", tribeTrendingPeriodAll: "Tout", tribeTrendingEngagement: "engagement", - tribeOpinionsEmpty: "Aucune opinion pour le moment.", - tribeOpinionsCast: "Voter", - tribeOpinionsRankings: "Classements", - tribeOpinionsAlreadyVoted: "Déjà voté", tribeTopCategory: "Plus voté", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "Toile collaborative de pixel art.", - tribePixeliaPaint: "Peindre", - tribePixeliaContributors: "contributeurs", - tribePixeliaTotalPixels: "pixels peints", tribeTagsEmpty: "Aucun tag trouvé.", - tribeTagsCloud: "Nuage de Tags", - tribeTagsContentWith: "Contenu avec le tag", - tribeSearchPlaceholder: "Rechercher dans la tribu...", tribeSearchEmpty: "Aucun résultat.", - tribeSearchResults: "Résultats", tribeSearchMinChars: "Entrez au moins 2 caractères pour rechercher.", agendaTitle: "Agenda", agendaDescription: "Ici vous pouvez trouver tous vos éléments assignés.", + agendaSearchPlaceholder: "Rechercher dans Agenda...", agendaFilterAll: "TOUS", agendaFilterToday: "AUJOURD’HUI", agendaFilterUpcoming: "À VENIR", @@ -2186,23 +2218,15 @@ module.exports = { agendaFilterReports: "RAPPORTS", agendaFilterTransfers: "TRANSFERTS", agendaFilterJobs: "EMPLOIS", + agendaFilterIndustry: "INDUSTRIE", + agendaFilterHousing: "LOGEMENT", agendaFilterProjects: "PROJETS", agendaFilterCalendars: "CALENDRIERS", agendaNoItems: "Aucune affectation trouvée.", agendaDiscardButton: "Écarter", agendaRestoreButton: "Restaurer", - agendaAuthor: "Par", - agendaCreatedAt: "Créé le", - agendaTitleLabel: "Titre", + agendaRemoveButton: "Retirer", agendaMembersCount: "Membres", - agendaDescriptionLabel: "Description", - agendaStatus: "État", - agendaVisibility: "Visibilité", - agendaEventDate: "Date de l’événement", - agendaEventLocation: "Localisation", - agendaEventPrice: "Prix", - agendaEventUrl: "URL", - agendaTaskStart: "Heure de début", agendaLocationLabel: "Localisation", agendaYes: "OUI", agendaNo: "NON", @@ -2212,11 +2236,6 @@ module.exports = { agendareportCategory: "Catégorie", agendareportSeverity: "Gravité", agendareportStatus: "État", - agendareportDescription: "Description", - agendaTaskEnd: "Heure de fin", - agendaTaskPriority: "Priorité", - agendaTransferFrom: "De", - agendaTransferTo: "À", agendaTransferConcept: "Concept", agendaTransferAmount: "Montant", agendaTransferDeadline: "Date limite", @@ -2226,47 +2245,7 @@ module.exports = { voteNow: "Votez maintenant", alreadyVoted: "Vous avez déjà donné votre avis.", noOpinionsFound: "Aucune opinion trouvée.", - RECENTButton: "RÉCENT", TOPButton: "TOP", - interestingButton: "INTÉRESSANT", - necessaryButton: "NÉCESSAIRE", - funnyButton: "AMUSANT", - disgustingButton: "DÉGOUTANT", - sensibleButton: "SENSIBLE", - propagandaButton: "PROPAGANDE", - adultOnlyButton: "ADULTE SEULEMENT", - boringButton: "ENNUYEUX", - confusingButton: "CONFUS", - usefulButton: "UTILE", - informativeButton: "INFORMATIF", - wellResearchedButton: "BIEN RECHERCHÉ", - accurateButton: "PRÉCIS", - needsSourcesButton: "BESOIN DE SOURCES", - wrongButton: "FAUX", - lowQualityButton: "FAIBLE QUALITÉ", - creativeButton: "CRÉATIF", - insightfulButton: "PERSPECTIF", - actionableButton: "RÉALISABLE", - inspiringButton: "INSPIRANT", - loveButton: "AMOUR", - clearButton: "CLAIR", - upliftingButton: "ÉLÉVANT", - unnecessaryButton: "INUTILE", - rejectedButton: "REJETÉ", - misleadingButton: "TROMPEUR", - offTopicButton: "HORS SUJET", - duplicateButton: "DUPE", - clickbaitButton: "APPÂT À CLIC", - spamButton: "SPAM", - trollButton: "TROLL", - nsfwButton: "NSFW", - violentButton: "VIOLENT", - toxicButton: "TOXIQUE", - harassmentButton: "HARCÈLEMENT", - hateButton: "HAINE", - scamButton: "ESCROQUERIE", - triggeringButton: "PROVOCANT", - opinionsCreatedAt: "Créé à", opinionsTotalCount: "Total des opinions", voteInteresting: "Intéressant", voteNecessary: "Nécessaire", @@ -2303,29 +2282,41 @@ module.exports = { voteCreative: "Créatif", voteSpam: "Spam", voteAdultOnly: "Adultes uniquement", - publishBlog: "Publier un blog", privateMessage: "MP", pmSendTitle: "Messages privés", pmSend: "Envoyer !", - pmDescription: "Utilisez ce formulaire pour envoyer un message chiffré à d’autres habitants.", + pmCancel: "Annuler", + pmReset: "Réinitialiser", + pmSharedKeyHint: "Partagez cette clé avec le destinataire par un autre canal. Elle est nécessaire pour déchiffrer.", + pmDescription: "Utilisez ce formulaire pour envoyer un message/fichier chiffré à d'autres habitants.", + pmComposeTitle: "Envoyer un message", pmRecipients: "Destinataires", pmRecipientsHint: "Saisissez les IDs Oasis séparés par des virgules", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "Jusqu'à 7 habitants par message.", + pmTextPlaceholder: "Écris ton message.", pmSubject: "Sujet", pmSubjectHint: "Saisissez l’objet du message", pmText: "Message", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Déchiffrer", + fileShareTitle: "Partager un fichier", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "Fichier", + fileShareDownload: "Télécharger", + fileShareMutualError: "Vous ne pouvez partager des fichiers qu'avec des habitants en soutien mutuel.", + fileShareNoFile: "Aucun fichier sélectionné.", + fileShareTooLarge: "Le fichier dépasse la taille autorisée.", + fileShareFailed: "Impossible de préparer le fichier.", + fileShareSendError: "Impossible d'envoyer le fichier.", + fileShareUnavailable: "Ce fichier n'est pas disponible pour le moment. Réessayez plus tard.", pmCrypterBadKey: "La clé partagée est incorrecte !", pmCrypterTooLong: "Le message est trop long pour être chiffré avec le Crypter. Raccourcissez-le et réessayez.", pmCrypterCipherLabel: "Message rechiffré", pmCrypterCharsLabel: "caractères", pmInvalidRecipients: "Identifiant Oasis invalide (devrait ressembler à @…=.ed25519).", pmEncryptionLabel: "Chiffrement", - pmFile: "Fichier joint", - private: "Privés", - privateDescription: "Vos messages chiffrés.", + private: "Boîte", + privateDescription: "Gérez vos messages privés.", privateInbox: "BOÎTE DE RÉCEPTION", privateSent: "ENVOYÉS", privateDelete: "Supprimer", @@ -2333,27 +2324,34 @@ module.exports = { noPrivateMessages: "Aucun message privé.", pmReply: "Répondre", pmReplies: "réponses", - pmNew: "nouvelles", - pmMarkRead: "Marquer comme lu", inReplyTo: "EN RÉPONSE À", + forumInThread: "DANS LE FIL", pmPreview: "Aperçu", pmPreviewTitle: "Aperçu", performed: "→", pmFromLabel: "De :", pmToLabel: "À :", - pmInvalidMessage: "Message non valide", pmNoSubject: "(sans objet)", pmSubjectLabel: "Objet :", - pmBodyLabel: "Corps", - pmBotJobs: "42-EmploisBOT", - pmBotProjects: "42-ProjetsBOT", - pmBotMarket: "42-MarchéBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "La maison dirigeante du L.A.R.P a changé.", + politicalBotParliamentTitle: "Un nouveau cycle de gouvernement a commencé au Parlement.", + politicalBotTribeTitle: "Un nouveau cycle de gouvernement a commencé dans l'une de tes Tribus.", + politicalBotLarpIntro: "Maintenant {house} gouverne durant ce cycle : {cycle}", + politicalBotParliamentIntro: "Le gouvernement actuel est {gov}", + politicalBotParliamentIntroLeader: "Le gouvernement actuel est {gov}, exercé par {leader}", + politicalBotTribeIntro: "Le gouvernement actuel dans la Tribu {tribe} est {gov}", + politicalBotTribeIntroLeader: "Le gouvernement actuel dans la Tribu {tribe} est {gov}, exercé par {leader}", + politicalBotLeaderLabel: "Chef", inboxJobSubscribedTitle: "Nouvelle inscription à votre offre d’emploi", pmInhabitantWithId: "Habitat avec ID OASIS :", pmHasSubscribedToYourJobOffer: "s’est inscrit à votre offre d’emploi", - inboxProjectCreatedTitle: "Nouveau projet créé", - pmHasCreatedAProject: "a créé un projet", inboxMarketItemSoldTitle: "Article vendu", + inboxShopSoldTitle: "Produit vendu", pmYourItem: "Votre article", pmHasBeenSoldTo: "a été vendu à", pmFor: "pour", @@ -2361,7 +2359,6 @@ module.exports = { pmHasPledged: "a contribué", pmToYourProject: "à votre projet", blockchain: 'Explorateur de blocs', - blockchainTitle: 'Explorateur de blocs', blockchainDescription: 'Explorez et visualisez les blocs de la chaîne.', blockchainNoBlocks: 'Aucun bloc trouvé sur la chaîne.', blockchainStatsTitle: 'Statistiques', @@ -2373,40 +2370,29 @@ module.exports = { blockchainBlockCarbon: 'Empreinte carbone', blockchainBlockType: 'Type', blockchainBlockTimestamp: 'Horodatage', - blockchainBlockContent: 'Bloc', - blockchainBlockURL: 'URL :', - blockchainContent: 'Bloc', - blockchainContentPreview: 'Aperçu du contenu du bloc', blockchainLatestDatagram: 'Dernier Datagramme', - blockchainDatagram: 'Datagramme', - blockchainDetails: 'Voir les détails du bloc', - blockchainViewBlockexplorer: "Voir dans le blockexplorer", - blockchainBlockInfo: 'Informations du bloc', - blockchainBlockDetails: 'Détails du bloc sélectionné', + blockchainDatagram: "Datagramme", + blockchainDetails: "Voir les Détails du Bloc", + blockchainViewBlockexplorer: "Voir dans le Blockexplorer", blockchainBack: 'Retour à l’explorateur', banking: 'Banque', bankingTitle: 'Banque', - bankingDescription: 'Explorez la valeur actuelle d’ECOin et l’allocation RBU correspondante, distribuée chaque semaine en fonction de la participation et de la confiance.', + bankingDescription: "L'économie collective d'ECOin.", bankOverview: 'Aperçu', bankEpochs: 'Époques', bankRules: 'Règles', pending: 'En attente', closed: 'Fermées', - bankBack: 'Retour à la Banque', - bankViewTx: 'Voir la Tx', bankClaimNow: 'Réclamer', - bankClaimUBI: 'Réclamer RBU !', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'Aucune allocation RBU en attente pour cette époque.', + bankClaimUBI: 'Réclamer RBU ', + bankClaimAndPay: "Réclamer et payer", + bankClaimedPending: "Réclamation en attente...", + bankStatusClaimed: "Réclamé", + bankStatusExpired: "Expiré", + bankPubOnly: "Opération réservée aux PUBs", bankEpoch: 'Époque', bankPool: 'Fonds (cette époque)', bankWeightsSum: 'Somme des poids', - bankAllocations: 'Attributions', bankNoAllocations: 'Aucune attribution trouvée.', bankNoEpochs: 'Aucune époque trouvée.', bankEpochAllocations: 'Attributions de l’époque', @@ -2420,16 +2406,21 @@ module.exports = { bankEpochId: 'ID de l’époque', bankRuleHash: 'Hash de l’instantané des règles', bankViewEpoch: 'Voir l’époque', + bankYourIndustryBalance: "Votre part de l'industrie", + bankYourUbiMonth: "Votre RBU (ce mois-ci)", + bankYourFundsMonth: "Total des fonds", + bankIndustryBalance: "Production industrielle", bankUserBalance: 'Votre solde', - ecoWalletNotConfigured: 'Portefeuille ECOin non configuré', - editWallet: 'Modifier le portefeuille', - addWallet: 'Ajouter un portefeuille', bankAddresses: 'Adresses', bankNoAddresses: 'Aucune adresse trouvée.', bankUser: 'ID d’Oasis', bankAddress: 'Adresse', bankAddAddressTitle: 'Ajouter une adresse ECOIN', bankAddAddressUser: 'ID d’Oasis', + bankAddAddressLabel: "Étiquette (facultatif)", + bankAddAddressLabelPlaceholder: "Portefeuille de Bob, boutique d'Alice…", + bankAddAddressUserOptional: "Oasis ID (facultatif)", + bankAddressUnnamed: "Adresse sans nom", bankAddAddressAddress: 'Adresse ECOIN', bankAddAddressSave: 'Enregistrer', bankAddressAdded: 'Adresse ajoutée', @@ -2447,20 +2438,12 @@ module.exports = { bankAddressDeleteConfirm: 'Supprimer cette adresse ?', search: 'Rechercher !', bankLocal: 'Local', - bankFromOasis: 'Oasis', - bankMyAddress: 'Votre adresse', - bankRemoveMyAddress: 'Supprimer mon adresse', - bankNotRemovableOasis: 'Les adresses ne peuvent pas être supprimées localement', + bankFromOasis: "Réseau", bankingUserEngagementScore: "Score KARMA", - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "PAS DE FONDS!", bankUbiAvailableOk: "DISPONIBLE!", - bankUbiAvailability: "Disponibilité UBI", + bankUbiAvailability: "RBU", bankAlreadyClaimedThisMonth: "Déjà réclamé ce mois-ci", bankUbiThisMonth: "RBU (ce mois)", bankUbiLastClaimed: "RBU (dernière réclamation)", @@ -2474,13 +2457,6 @@ module.exports = { shopsTitle: "Boutiques", shopDescription: "Découvrez et gérez les boutiques du réseau.", shopTitle: "Boutique", - shopFilterAll: "TOUTES", - shopFilterMine: "MIENNES", - shopFilterRecent: "RÉCENTES", - shopFilterTop: "TOP", - shopFilterProducts: "PRODUITS", - shopFilterPrices: "PRIX", - shopFilterFavorites: "FAVORIS", shopUpload: "Créer Boutique", shopAllSectionTitle: "Toutes les Boutiques", shopMineSectionTitle: "Mes Boutiques", @@ -2497,19 +2473,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Publier sur le Clearnet", - shopClearnetUnpublish: "Retirer du Clearnet", - shopClearnetUrlLabel: "URL Clearnet", - shopClearnetWarning: "Publier sur le Clearnet rend cette boutique indexable par les moteurs de recherche et robots externes.", + transportChipLabel: "Chiffré en transit", shopAddFavorite: "Ajouter Favori", shopRemoveFavorite: "Retirer Favori", shopOpen: "OUVERTE", shopClosed: "FERMÉE", - shopOpenShop: "Ouvrir Boutique", - shopCloseShop: "Fermer Boutique", shopMakePrivate: "RENDRE PRIVÉE (E2E)", shopMakePublic: "RENDRE PUBLIQUE", shopProducts: "Produits", + shopFilterAll: "TOUTES", + shopFilterRecent: "RÉCENTES", + shopFilterMine: "MIENNES", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUITS", + shopFilterPrices: "PRIX", + shopFilterFavorites: "FAVORIS", shopProductAdd: "Ajouter Produit", shopProductTitle: "Produit", shopProductUpdate: "Modifier Produit", @@ -2534,39 +2512,36 @@ module.exports = { shopOrderPrice: "Prix", shopOrderBuyer: "Acheteur", shopBackToShop: "Retour à la Boutique", - shopShareUrl: "URL de partage", - shopVisitShop: "VISITER BOUTIQUE", marketVisitItem: "VOIR L’ARTICLE", shopStatus: "STATUT", shopCreatedAt: "CRÉÉ", - shopSearchPlaceholder: "Rechercher des boutiques...", + shopSearchPlaceholder: "Rechercher dans Boutiques...", shopUrl: "URL", shopLocation: "Localisation", shopTags: "Étiquettes", shopVisibility: "Visibilité", - shopImage: "Image", shopShortDescription: "Description Courte", shopShortDescriptionPlaceholder: "Description breve pour les cartes (max 160 caracteres)", shopTitlePlaceholder: "Nom de votre boutique", shopDescriptionPlaceholder: "Description détaillée de votre boutique", - shopUrlPlaceholder: "https://url-de-votre-boutique.com", shopLocationPlaceholder: "Ville, Pays", - shopTagsPlaceholder: "tag1, tag2, tag3", + shopTagsPlaceholder: "Saisissez des tags séparés par des virgules", mapTitlePlaceholder: "Titre de la carte", shopProductFeatured: "Produit Vedette", - shopSendToMarket: "Send to Market", + shopSendToMarket: "Envoyer au marché", typeShop: "BOUTIQUES", typeShopProduct: "PRODUIT BOUTIQUE", bankExchange: 'Échange', bankExchangeCurrentValue: 'Valeur d’ECOin (1h)', - bankTotalSupply: 'Offre totale d’ECOin', + bankTotalSupply: "Offre ECOin totale", + bankPubsSupply: "Offre ECOin de la réserve des PUBs", + bankHoldingSupply: "Offre ECOin hors RBU", bankHoursOfWork: 'heures', bankUnitMs: "ms", bankUnitSeconds: "secondes", bankUnitMinutes: "minutes", bankUnitDays: "jours", bankExchangeNoData: 'Aucune donnée disponible', - bankExchangeIndex: 'Valeur d’ECOin (1h)', bankInflation: 'Inflation d’ECOin (anual)', bankInflationMonthly: 'Inflation d’ECOin (mensual)', bankExchangeChartTitle: 'Progression de la valeur d’ECOin au fil du temps', @@ -2574,44 +2549,25 @@ module.exports = { bankExchangeChartSupply: 'Offre', bankExchangeChartInflation: 'Inflation %', bankExchangeChartEmpty: 'Pas encore assez d’échantillons — reviens plus tard', - bankCurrentSupply: 'Offre actuelle d’ECOin', + bankCurrentSupply: "Offre ECOin actuelle", bankingSyncStatus: 'État d’ECOin', bankingSyncStatusSynced: 'Synchronisé', - bankingSyncStatusOutdated: 'Obsolète', + bankingSyncStatusOutdated: "Non synchronisé", statsTitle: 'Statistiques', statistics: "Statistiques", - statsInhabitant: "Statistiques de l'habitant", statsDescription: "Découvrez des statistiques sur votre réseau.", ALLButton: "TOUT", + RECENTButton: "RÉCENTS", MINEButton: "MIENS", TOMBSTONEButton: "SUPPRESSIONS", - statsYou: "Vous", - statsUserId: "ID Oasis", statsCreatedAt: "Créé le", statsYourContent: "Contenu", statsYourOpinions: "Avis", - statsYourTombstone: "Suppressions", - statsNetwork: "Réseau", - statsTotalInhabitants: "Habitants", - statsDiscoveredTribes: "Tribus (publiques)", - statsPrivateDiscoveredTribes: "Tribus (privées)", statsNetworkContent: "Contenu", - statsYourMarket: "Marché", - statsYourJob: "Emplois", - statsYourProject: "Projets", - statsYourTransfer: "Transferts", - statsYourForum: "Forums", statsNetworkOpinions: "Avis", - statsDiscoveredMarket: "Marché", - statsDiscoveredJob: "Emplois", - statsDiscoveredProject: "Projets", - statsBankingTitle: "Banque", - statsEcoWalletLabel: "Portefeuille ECOIN", + statsEcoWalletLabel: "Portefeuille ECOin", + walletAddressCopyHint: "Cliquez pour sélectionner, puis copiez", statsEcoWalletNotConfigured: "Non configuré !", - statsTotalEcoAddresses: "Adresses totales", - statsDiscoveredTransfer: "Transferts", - statsDiscoveredForum: "Forums", - statsNetworkTombstone: "Suppressions", statsBookmark: "Signets", statsEvent: "Événements", statsTask: "Tâches", @@ -2632,17 +2588,15 @@ module.exports = { statsTransfer: "Transferts", statsAiExchange: "IA", statsPUBs: 'PUBs', + statsSyncedPeers: "Nœuds", + statsTotalLabel: "Total", statsPost: "Publications", - statsOasisID: "ID Oasis", statsSize: "Total (taille)", statsBlockchainSize: "Blockchain (taille)", statsBlobsSize: "Blobs (taille)", statsActivity7d: "Activité (7 derniers jours)", statsActivity7dTotal: "Total 7 jours", statsActivity30dTotal: "Total 30 jours", - statsKarmaScore: "Score KARMA", - statsPublic: "Public", - statsPrivate: "Privé", day: "Jour", messages: "Messages", statsProject: "Projets", @@ -2654,30 +2608,14 @@ module.exports = { statsProjectsCancelled: "Annulés", statsProjectsGoalTotal: "Objectif total", statsProjectsPledgedTotal: "Montant promis total", - statsProjectsSuccessRate: "Taux de réussite", - statsProjectsAvgProgress: "Progression moyenne", - statsProjectsMedianProgress: "Progression médiane", - statsProjectsActiveFundingAvg: "Financement actif moyen", - statsJobsTitle: "Emplois", - statsJobsTotal: "Emplois au total", - statsJobsOpen: "Ouverts", - statsJobsClosed: "Fermés", - statsJobsOpenVacants: "Postes ouverts", - statsJobsSubscribersTotal: "Abonnés au total", - statsJobsAvgSalary: "Salaire moyen", - statsJobsMedianSalary: "Salaire médian", statsMarketTitle: "Marché", statsMarketTotal: "Articles au total", statsMarketForSale: "À vendre", statsMarketReserved: "Réservés", statsMarketClosed: "Fermés", statsMarketSold: "Vendus", - statsMarketRevenue: "Chiffre d'affaires", - statsMarketAvgSoldPrice: "Prix de vente moyen", statsUsersTitle: "Habitants", user: "Habitant", - statsTombstoneTitle: "Suppressions", - statsNetworkTombstones: "Suppressions du réseau", statsTombstoneRatio: "Taux de suppressions (%)", statsParliamentCandidature: "Candidatures au parlement", statsParliamentTerm: "Mandats parlementaires", @@ -2703,31 +2641,28 @@ module.exports = { aiPrompt: "Fournissez une réponse informative et précise.", aiConfiguration: "Configurer le prompt", aiPromptUsed: "Invite", - aiClearHistory: "Effacer l’historique du chat", - aiSharePrompt: "Ajouter cette réponse à l’entraînement collectif ?", - aiShareYes: "Oui", - aiShareNo: "Non", - aiSharedLabel: "Ajoutée à l’entraînement", - aiRejectedLabel: "Non ajoutée à l’entraînement", - aiServerError: "L’IA n’a pas pu répondre. Réessayez.", + aiClearHistory: "Effacer l'historique", + aiServerError: "Je ne peux pas répondre correctement. Réessayez.", + aiStatusLoading: "42 charge le modèle. Réessayez dans un instant.", + aiThinking: "Réflexion", + aiWaiting: "En attente", + aiStatusMissingModel: "Le modèle d'IA n'est pas installé : src/AI/oasis-42-1-chat.Q4_K_M.gguf est absent.", + aiExportFineTuning: "Exporter le Fine-Tuning", + aiFactsTitle: "Données de votre réseau", aiInputPlaceholder: "Qu'est-ce qu'Oasis ?", typeAiExchange: "IA", aiApproveTrain: "Ajouter à l’entraînement collectif", aiRejectTrain: "Ne pas entraîner", - aiTrainPending: "En attente d’approbation", aiTrainApproved: "Approuvé pour l’entraînement", aiTrainRejected: "Rejeté pour l’entraînement", aiSnippetsUsed: "Fragments utilisés", aiSnippetsLearned: "Fragments appris", statsAITraining: "Entraînement de l’IA", - aiApproveCustomTrain: "Entraîner avec cette réponse personnalisée", aiCustomAnswerPlaceholder: "Écrivez votre réponse personnalisée…", - statsAIExchanges: "Échanges de modèles", marketMineSectionTitle: "Vos articles", marketCreateSectionTitle: "Créer un article", marketUpdateSectionTitle: "Mettre à jour", marketAllSectionTitle: "Marché", - marketRecentSectionTitle: "Marché récent", marketTitle: "Marché", marketDescription: "Un marché pour échanger des biens ou des services dans votre réseau.", marketFilterAll: "TOUS", @@ -2742,7 +2677,7 @@ module.exports = { marketFilterDiscarded: "JETÉ", marketFilterRecent: "RÉCENT", marketFilterMyBids: "ENCHÈRES", - marketCreateButton: "Créer un article", + marketCreateButton: "Vendre quelque chose", marketItemType: "Type", marketItemTitle: "Titre", marketItemAvailable: "Date limite", @@ -2753,18 +2688,15 @@ module.exports = { marketItemCondition: "État", marketItemPrice: "Prix", marketItemTags: "Étiquettes", - marketItemTagsPlaceholder: "Entrez des tags séparés par des virgules", + marketItemTagsPlaceholder: "Saisissez des tags séparés par des virgules", marketItemDeadline: "Date limite", marketItemIncludesShipping: "Livraison incluse ?", marketItemHighestBid: "Meilleure offre", - marketItemHighestBidder: "Meilleur enchérisseur", marketItemStock: "Réserve", marketOutOfStock: "Rupture de stock", - marketItemBidTime: "Date de l'enchère", marketActionsUpdate: "Mettre à jour", marketUpdateButton: "Mettre à jour", marketActionsDelete: "Supprimer", - marketActionsSold: "Marquer comme vendu", marketActionsChangeStatus: "CHANGER LE STATUT", marketActionsBuy: "ACHETER !", marketAuctionBids: "Offres en cours", @@ -2773,19 +2705,81 @@ module.exports = { marketNoItems: "Aucun article disponible pour le moment.", marketYourBid: "Votre offre", marketCreateFormImageLabel: "Télécharger un média (max: 50 Mo)", - marketSearchLabel: "Rechercher", - marketSearchPlaceholder: "Rechercher par titre ou tags", + marketSearchPlaceholder: "Rechercher dans Marché...", marketMinPriceLabel: "Prix minimum", marketMaxPriceLabel: "Prix maximum", - marketSortLabel: "Trier par", marketSortRecent: "Les plus récents", marketSortPrice: "Prix", marketSortDeadline: "Date limite", marketSearchButton: "Rechercher", marketAuctionEndsIn: "Se termine", marketAuctionEnded: "Terminé", - marketMyBidBadge: "Vous avez enchéri", marketNoItemsMatch: "Aucun article ne correspond à votre recherche.", + housingTitle: "Logement", + housingDescriptionText: "Découvrez et gérez des lieux dans votre réseau.", + modulesHousingLabel: "Logement", + modulesHousingDescription: "Module pour découvrir et gérer des lieux.", + typeHousing: "LOGEMENT", + housingFilterAll: "TOUT", + housingFilterMine: "À MOI", + housingFilterRequested: "DEMANDÉS", + housingFilterSale: "VENTE", + housingFilterRent: "LOCATION", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "DISPONIBLES", + housingFilterClosed: "INDISPONIBLES", + housingFilterRecent: "RÉCENTS", + housingFilterTop: "TOP", + housingType: "Offre", + housingTypeSALE: "À vendre", + housingTypeRENT: "À louer", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Bien", + housingPropertyAPARTMENT: "Appartement", + housingPropertyHOUSE: "Maison", + housingPropertyROOM: "Chambre", + housingPropertyLAND: "Terrain", + housingPropertyOTHER: "Autre", + housingTitleLabel: "Titre", + housingTitlePlaceholder: "Nom du lieu", + housingDescription: "Description", + housingDescriptionPlaceholder: "Décrivez le lieu, le quartier, ce qui est inclus : salles de bain, jardin, lits…", + housingRules: "Règles", + housingRulesPlaceholder: "Animaux, tabac, heures calmes, séjour minimum…", + housingPlace: "Lieu", + housingPlacePlaceholder: "Pays, ville, adresse", + housingPrice: "Prix", + housingPriceLabel: "Prix (ECO)", + housingSizeLabel: "Surface (m²)", + housingFree: "GRATUIT", + housingPerMonth: "/mois", + housingRooms: "Pièces", + housingSize: "Surface", + housingCapacity: "Voyageurs", + housingAvailableFrom: "Disponible à partir du", + housingAvailableTo: "Disponible jusqu'au", + housingTags: "Étiquettes", + spreadEditWarning: "Ce contenu perdra ses partages après modification", + housingRequests: "Demandes", + housingRequestButton: "Demander", + housingCancelButton: "Annuler la demande", + housingRequestedBadge: "DEMANDÉ", + housingStatus: "Statut", + housingStatusOPEN: "DISPONIBLE", + housingStatusCLOSED: "INDISPONIBLE", + housingSetOpen: "Rendre disponible", + housingSetClosed: "Rendre indisponible", + housingCreateButton: "Créer un lieu", + housingUpdateButton: "Mettre à jour", + housingSearchPlaceholder: "Rechercher dans Logement...", + housingSearchButton: "Rechercher", + housingMinPrice: "Prix min", + housingMaxPrice: "Prix max", + housingSortRecent: "Plus récents", + housingSortPrice: "Prix le plus bas", + housingSortRequests: "Plus demandés", + housingSortRating: "Mieux notés", + housingNoItems: "Aucun lieu trouvé.", jobsTitle: "Offres d’emploi", jobsCandidatesTitle: "Candidats suggérés", jobsCandidatesDescription: "Habitants dont les compétences correspondent à cet emploi. Envoyez-leur un message privé.", @@ -2833,7 +2827,6 @@ module.exports = { jobLocationPresencial: "Présentiel", jobLocationRemote: "À distance", jobVacantsPlaceholder: "Nombre de postes vacants", - jobSalaryPlaceholder: "Salaire en ECO pour 1 heure", jobImage: "Télécharger un média (max: 50 Mo)", jobTasks: "Tâches", jobType: "Type d’emploi", @@ -2843,7 +2836,6 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Emplois les mieux payés", createJobButton: "Publier un emploi", - viewDetailsButton: "Voir les détails", noJobsFound: "Aucune offre d’emploi trouvée.", jobAuthor: "Par", jobTypeFreelance: "Autonome", @@ -2855,42 +2847,208 @@ module.exports = { jobsHoursOffered: "Heures offertes", jobsHoursRequested: "Heures demandées en échange", jobsExchangeSkill: "Compétence demandée en échange", - jobsHoursOfferedPlaceholder: "ex. 4", - jobsHoursRequestedPlaceholder: "ex. 4", - jobsExchangeSkillPlaceholder: "ex. menuiserie, design", - jobExchangeHint: "Pour les emplois en échange d’heures, remplissez les champs ci-dessous au lieu du salaire.", jobTimePartial: "Partiel", jobTimeComplete: "Complet", - jobsDeleteButton: "SUPPRIMER", jobsUpdateButton: "METTRE À JOUR", jobsFilterApplied: "POSTULÉ", jobsAppliedTitle: "Mes candidatures", jobsAppliedBadge: "Candidaté", - jobsFilterFavs: "Favoris", - jobsFavsTitle: "Favoris", - jobsFilterNeeds: "Besoin d'aide", - jobsNeedsTitle: "Besoin d'aide", - jobsSearchLabel: "Recherche", - jobsSearchPlaceholder: "Rechercher titre, tags, description...", + jobsSearchPlaceholder: "Rechercher dans Offres d’emploi...", jobsMinSalaryLabel: "Salaire min.", jobsMaxSalaryLabel: "Salaire max.", - jobsSortLabel: "Trier par", jobsSortRecent: "Plus récents", jobsSortSalary: "Salaire le plus élevé", jobsSortSubscribers: "Plus de candidats", jobsSearchButton: "Rechercher", - jobsFavoriteButton: "Favori", - jobsUnfavoriteButton: "Retirer favori", - jobsMessageAuthorButton: "MP", jobsApplicants: "Candidats", jobsUpdatedAt: "Mis à jour", jobSetOpen: "Marquer ouvert", - jobNewBadge: "NOUVEAU", jobsTagsLabel: "Étiquettes", - jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "Aucune offre ne correspond à votre recherche.", + industrySearchPlaceholder: "Rechercher dans Industrie...", + industryAllSectors: "Tous les secteurs", + industryVisitButton: "Visiter l’industrie", + industryAdvanceTo: "Passer à", + industryAmount: "Montant", + industryApproveBuild: "Approuver", + industryBlueprint: "Plan", + industryBlueprints: "Plans", + industryBuild: "Production", + industryBuildNotes: "Notes", + industryBuildStart: "Date de début", + industryVoteUpdateButton: "Voter la mise à jour", + industryVoteDeleteButton: "Voter la suppression", + industryRevokeVote: "Révoquer", + industryTimeLeft: "Temps restant", + industryBuildEnd: "Date de fin", + industryBuilds: "Productions", + industryBuildTitle: "Titre", + industryContribute: "Contribuer", + industryContributeButton: "Contribuer", + industryContributions: "Contributions", + industryCreateBlueprintButton: "Créer un blueprint", + industryDistribute: "Distribuer", + industryDistributeButton: "Distribuer par parts", + industryDistribution: "Distribution", + industryEcoAmount: "Montant (ECO)", + industryEditBlueprint: "Modifier le blueprint", + industryFacility: "Usine", + industryKindLabel: "Type", + industryKind_labor: "Main-d'œuvre", + industryKind_material: "Matériau", + industryKind_eco: "Fonds", + industryLaborHours: "Main-d'œuvre (heures)", + industryHours: "Heures", + industryMaterialItem: "Matériel", + industryMaterials: "Matériaux", + industryEstMaterials: "Total matériaux", + industryEstLabor: "Total main-d'œuvre", + industryEstTotal: "Prix estimé", + industryBuildingPrice: "Prix de fabrication", + industryFinalPrice: "Prix final", + industryBlueprintDescPlaceholder: "Spécifications, dimensions, poids, documentation…", + industryMaterialValue: "Valeur (ECO)", + industryMember: "Membre", + industryNewBlueprint: "Nouveau blueprint", + industryNewBuild: "Proposer un build", + industryEditBuild: "Modifier la production", + industryNeedBlueprint: "Créez d'abord un plan : chaque production en fabrique un.", + industryNoBlueprints: "Aucun blueprint pour l'instant.", + industryNoBuilds: "Aucun build pour l'instant.", + industryNote: "Note", + industryOutput: "Sortie", + industryOutputKind: "Type de produit", + industryOutputValue: "Valeur de sortie (ECO, p. ex. de la vente)", + industryPostJob: "Créer un emploi", + industryPot: "Cagnotte", + industryProposeBuildButton: "Proposer le build", + industryProposer: "Proposant", + industryAuthor: "Auteur", + industrySellOnMarket: "Envoyer au marché", + industrySendToProjects: "Créer un projet", + industrySkills: "Compétences", + industryTreasury: "Trésorerie", + industryKind_physical: "Physique", + industryKind_digital: "Numérique", + industryBuildStatus_PROPOSED: "PROPOSÉ", + industryBuildStatus_REJECTED: "REJETÉ", + industryBuildStatus_APPROVED: "APPROUVÉ", + industryBuildStatus_STOCKING: "APPROVISIONNEMENT", + industryBuildStatus_IN_PRODUCTION: "EN PRODUCTION", + industryBuildStatus_COMPLETED: "TERMINÉ", + industryBuildStatus_FAILED: "ÉCHOUÉ", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Une offre correspond à votre CV", + jobsBotMatchIntro: "Une offre correspond à votre CV.", + jobsBotMatchJob: "Emploi", + jobsBotMatchHint: "Vous recevez ceci parce que votre CV est géré par l'IA. Vous pouvez le désactiver dans votre CV.", + cvAiManaged: "Géré par l'IA", + switchOn: "ACTIVÉ", + switchOff: "DÉSACTIVÉ", + cvAiManagedHint: "Si activé, JobsBot vous prévient par message privé quand une offre correspond à votre CV.", + cvMatchThreshold: "Me prévenir à partir de ce niveau de correspondance", + housingBotRequestedTitle: "Quelqu’un a demandé l’un de vos lieux.", + housingBotCancelledTitle: "Une demande sur l’un de vos lieux a été annulée.", + housingBotYouRequestedTitle: "Vous avez demandé un lieu.", + housingBotYouCancelledTitle: "Vous avez annulé une demande.", + housingBotUnavailableTitle: "Un lieu que vous aviez demandé n’est plus disponible.", + housingBotUnavailableSentTitle: "Vous avez prévenu les habitants qui avaient demandé ce lieu.", + industryBotAdmittedTitle: "Vous avez été admis dans une usine.", + industryBotApplicationTitle: "Nouvelle demande d'adhésion dans votre usine.", + industryBotInvitedTitle: "Vous avez été invité dans une usine.", + industryBotDissolvedTitle: "Une usine dont vous faites partie a été dissoute.", + industryBotBuildApprovedTitle: "Un build a été approuvé.", + industryBotDistributedTitle: "La sortie d'un build a été distribuée.", + industryFilterRules: "RÈGLES", + industryRulesTitle: "Règles", + industryRulesIntro: "Industry permet aux habitants de posséder collectivement les moyens de production : une Usine est un atelier du réseau que personne ne possède à titre privé.", + industryRulesSteward: "Le fondateur est l'intendant — un gardien, pas un propriétaire : il ne peut ni vendre, ni privatiser, ni dissoudre l'Usine seul.", + industryRulesMembership: "La politique d'adhésion définit comment on rejoint : Ouverte (adhésion immédiate), Par vote (admis par le vote des membres) ou Sur invitation (un membre doit inviter d'abord).", + industryRulesQuorum: "Le quorum est le nombre minimum de membres qui doivent voter pour qu'une décision compte, afin qu'une petite Usine ne soit pas décidée par un seul habitant.", + industryRulesMajority: "La majorité est la fraction de votes nécessaire (0,5 = moitié, 1 = unanimité) ; une décision passe quand les OUI atteignent au moins max(quorum, membres × majorité).", + industryRulesDecisions: "Les membres votent pour admettre de nouveaux venus, approuver les builds et dissoudre l'Usine ; l'intendant ne peut pas passer outre ces décisions collectives.", + industryRulesBlueprints: "Les Blueprints sont des recettes libres (COPYLEFT) — les intrants (matériaux, heures de travail, compétences) et la sortie (un bien physique ou numérique) — et chacun peut les forker.", + industryRulesBuilds: "Un Build est un ordre de production : PROPOSÉ → APPROUVÉ (par vote) → APPROVISIONNEMENT → EN PRODUCTION → TERMINÉ, et il n'accepte des contributions qu'une fois approuvé.", + industryRulesContributions: "Les membres contribuent en travail (heures), matériaux (valeur ECO déclarée) ou ECO à un build ; chaque contribution rapporte du karma.", + industryRulesLaborRate: "Le taux de travail est la valeur ECO d'une heure de travail dans cette Usine ; il convertit les heures en karma pour comparer équitablement effort et capital : karma = heures × taux + valeur matériaux + ECO.", + industryRulesShares: "Votre part d'un build est votre karma ÷ karma total, et fixe la part de la valeur de sortie que vous recevez.", + industryRulesDistribution: "Lorsqu'une production est terminée, le steward répartit le pot (les fonds de la production plus la valeur de vente) entre les contributeurs, au prorata de leurs parts.", + industryRulesTreasury: "Les contributions en ECO sont détenues par l'intendant comme dépositaire de la trésorerie du build jusqu'à la distribution ; tous les mouvements sont enregistrés sur le réseau et les litiges peuvent aller aux Courts.", + industryJobs: "Emplois", + industryReportButton: "Signaler", + industryCreateTask: "Créer une tâche", + industryOpenDispute: "Ouvrir un litige", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "Générer une invitation", + industryPauseVotes: "Votes de statut", + industryTitle: "Industrie", + industryDescription: "Module pour gérer collectivement les moyens de production.", + typeIndustryBuild: "PRODUCTION", + typeIndustryBlueprint: "PLAN", + typeIndustryAllocation: "DISTRIBUTION", + typeIndustry: "INDUSTRIE", + modulesIndustryLabel: "Industrie", + modulesIndustryDescription: "Module pour gérer collectivement les moyens de production.", + industryFilterAll: "TOUTES", + industryFilterRecent: "RÉCENTS", + industryFilterMember: "MEMBRE", + industryFilterBlueprints: "PLANS", + industryFilterBuilds: "PRODUCTIONS", + industryFilterMine: "MIENNES", + industryFilterActive: "EN MARCHE", + industryFilterPaused: "EN PAUSE", + industryFilterDissolved: "DISSOUTES", + industryAllTitle: "Industrie", + industryRecentTitle: "Usines récentes", + industryMemberTitle: "Usines dont je fais partie", + industryMineTitle: "Mes usines", + industryActiveTitle: "En marche", + industryPausedTitle: "Usines en pause", + industryDissolvedTitle: "Usines dissoutes", + industryNoFacilitiesFound: "Aucune usine trouvée.", + industryCreateFacility: "Créer une usine", + industryMembers: "Membres", + industryMemberBadge: "MEMBRE", + industryName: "Nom", + industryNamePlaceholder: "Nom de l'usine", + industryDescriptionLabel: "Description", + industryDescriptionPlaceholder: "Que produit cette usine ?", + industrySector: "Secteur", + industryMembershipPolicy: "Politique d'adhésion", + industryQuorum: "Quorum", + industryMajority: "Majorité", + industryLaborRate: "Taux de travail", + industryCreateButton: "Créer une usine", + industryUpdateButton: "Mettre à jour", + industrySteward: "Intendant", + industryStatusActive: "EN MARCHE", + industryStatusPaused: "EN PAUSE", + industryStatusDissolved: "DISSOUTE", + industryStatusLabel: "État", + industryJoinButton: "Rejoindre", + industryApplyButton: "Demander à rejoindre", + industryLeaveButton: "Quitter", + industryInviteButton: "Inviter", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Vous avez besoin d'une invitation pour rejoindre.", + industryPauseButton: "Voter la pause", + industryResumeButton: "Voter la reprise", + industryDeleteButton: "Supprimer", + industryPendingApplicants: "Candidatures en attente", + industryVotesYes: "oui", + industryVotesNo: "non", + industryAlreadyVoted: "voté", + industryAdmit: "Admettre", + industryReject: "Rejeter", + industryDissolveTitle: "Dissoudre l'usine", + industryDissolveVote: "Voter la dissolution", + industryPolicy_open: "Ouverte", + industryPolicy_vote: "Par vote", + industryPolicy_invite: "Sur invitation", projectsTitle: "Projets", projectsDescription: "Créez, financez et suivez des projets communautaires dans votre réseau.", + projectSearchPlaceholder: "Rechercher dans Projets...", projectCreateProject: "Créer un projet", projectCreateButton: "Créer un projet", projectUpdateButton: "METTRE À JOUR", @@ -2934,14 +3092,8 @@ module.exports = { projectPledgeTitle: "Soutenez ce projet", projectPledgePlaceholder: "Montant en ECO", projectBounties: "Récompenses", - projectBountiesInputLabel: "Récompenses (une par ligne : Titre|Montant [ECO]|Description)", - projectBountiesPlaceholder: "Réparer un bug UI|100|Lien vers le problème\nRédiger de la documentation|250|Exemples d'utilisation", projectNoBounties: "Aucune récompense trouvée.", projectTitle: "Titre", - projectAddBountyTitle: "Ajouter une nouvelle récompense", - projectBountyTitle: "Titre de la récompense", - projectBountyAmount: "Montant (ECO)", - projectBountyDescription: "Description", projectMilestoneSelect: "Sélectionner un jalon", projectBountyCreateButton: "Créer une récompense", projectBountyStatus: "Statut de la récompense", @@ -2956,19 +3108,15 @@ module.exports = { projectMilestoneTitle: "Titre du jalon", projectMilestoneTargetPercent: "Pourcentage (%)", projectMilestoneDueDate: "Date", - projectMilestoneCreateButton: "Créer un jalon", projectMilestoneStatus: "Statut du jalon", projectMilestoneOpen: "ouvert", projectMilestoneDone: "complété", projectMilestoneDue: "échéance", - projectNoMilestones: "Aucun jalon trouvé.", projectMilestoneMarkDone: "Marquer comme fait", projectMilestoneTitlePlaceholder: "Entrez le titre du jalon", projectMilestoneDescriptionPlaceholder: "Entrez la description de ce jalon", projectMilestoneDescription: "Description du jalon", - projectBudgetGoal: "Budget (Objectif)", projectBudgetAssigned: "Assigné aux récompenses", - projectBudgetRemaining: "Restant", projectBudgetOver: "⚠ Budget dépassé : ce qui est assigné dépasse l’objectif", projectFollowers: "Suiveurs", projectFollowersTitle: "Suiveurs", @@ -2981,7 +3129,6 @@ module.exports = { projectBackersTotalPledged: "Total engagé", projectBackersYourPledge: "Votre engagement", projectBackersNone: "Pas encore d'engagement.", - projectNoRemainingBudget: "Aucun budget restant.", projectFilterApplied: "POSTULÉ", projectAppliedTitle: "POSTULÉ", projectFilterBackers: "MÉCÈNES", @@ -2990,36 +3137,19 @@ module.exports = { projectBackerAmount: "Total engagé", projectBackerPledges: "Engagements", projectBackerProjects: "Projets", - projectPledgeAmount: "Montant", projectSelectMilestoneOrBounty: "Sélectionner un jalon ou une récompense", projectPledgeButton: "Soutenir", footerLicense: "GPLv3", - footerPackage: "Paquet", - footerVersion: "Version", modulesModuleName: "Nom", modulesModuleDescription: "Description", modulesModuleStatus: "Statut", - modulesTotalModulesLabel: "Modules chargés", + modulesTotalModulesLabel: "Total", modulesEnabledModulesLabel: "Activés", modulesDisabledModulesLabel: "Désactivés", - modulesPopularLabel: "Populaire", - modulesPopularDescription: "Module pour recevoir les publications tendance, les plus vues ou les plus commentées.", - modulesTopicsLabel: "Sujets", - modulesTopicsDescription: "Module pour recevoir des catégories de discussion basées sur des intérêts partagés.", - modulesSummariesLabel: "Résumé", - modulesSummariesDescription: "Module pour recevoir des résumés de discussions ou publications longues.", - modulesLatestLabel: "Derniers", - modulesLatestDescription: "Module pour recevoir les publications et discussions les plus récentes.", - modulesThreadsLabel: "Fils", - modulesThreadsDescription: "Module pour recevoir des conversations groupées par sujet ou question.", - modulesMultiverseLabel: "Multivers", - modulesMultiverseDescription: "Module pour recevoir du contenu d’autres pairs fédérés.", modulesInvitesLabel: "Invitations", modulesInvitesDescription: "Module pour gérer et appliquer des codes d'invitation.", modulesWalletLabel: "Portefeuille", modulesWalletDescription: "Module pour gérer vos actifs numériques (ECOin).", - modulesLegacyLabel: "Héritage", - modulesLegacyDescription: "Module pour gérer rapidement et en toute sécurité votre secret (clé privée).", modulesCipherLabel: "Chiffreur", modulesCipherDescription: "Module pour crypter et décrypter votre texte de manière symétrique (en utilisant un mot de passe partagé).", modulesBookmarksLabel: "Signets", @@ -3058,8 +3188,12 @@ module.exports = { modulesOpinionsDescription: "Module pour découvrir et voter des opinions.", modulesTransfersLabel: "Transferts", modulesTransfersDescription: "Module pour découvrir et gérer les contrats intelligents (transferts).", - modulesFediverseLabel: "Fediverse", - modulesFediverseDescription: "Gérez vos autres comptes du fédiverse, y compris l'envoi et la réception de contenu.", + modulesFediverseLabel: "Multivers", + modulesBlogsLabel: "Blogs", + modulesBlogsDescription: "Module pour découvrir et gérer les blogs.", + modulesPollsLabel: "Sondages", + modulesPollsDescription: "Module pour interroger le réseau et compter les réponses.", + modulesFediverseDescription: "Gérez vos autres comptes du multiverse, y compris l'envoi et la réception de contenu.", modulesFeedLabel: "Fil", modulesFeedDescription: "Module pour découvrir et partager des textes courts (flux).", modulesPixeliaLabel: "Pixelia", @@ -3077,62 +3211,66 @@ module.exports = { modulesProjectsLabel: "Projets", modulesProjectsDescription: "Module pour explorer, financer et gérer des projets.", modulesBankingLabel: "Banque", - modulesBankingDescription: "Module pour connaître la valeur réelle de ECOIN et distribuer une RBU en utilisant la trésorerie commune.", + modulesBankingDescription: "Module pour gérer le trésor commun : valeur de l’ECOIN, taxes et RBU.", modulesFavoritesLabel: "Favoris", modulesFavoritesDescription: "Module pour gérer votre contenu favori.", fileTooLargeTitle: "Fichier trop volumineux", fileTooLargeMessage: "Le fichier dépasse la taille maximale autorisée (50 Mo). Veuillez sélectionner un fichier plus petit.", goBack: "Retour", + errorDismiss: "OK", errorPageTitle: "Une erreur est survenue", + errorNoAllocation: "Aucune allocation trouvée pour cette réclamation.", + errorInvalidClaim: "Cette allocation ne peut pas être réclamée.", visibilityLabel: "Visibilité", + clearnetPrivateConflict: "Un contenu privé, masqué, fermé, sur invitation ou payant ne peut pas être publié sur la CLEARNET. Choisis OASIS ou rends-le public.", visibilityPublic: "Publique", visibilityHidden: "Cachée", visibilityMakePublic: "Rendre publique", visibilityMakeHidden: "Cacher", invitesInhabitantsTitle: "Habitants", - invitesInhabitantsFollow: "Suivre", - aiNavPlaceholder: "Où veux-tu aller ?", + aiNavPlaceholder: "Décrivez ce qu'il vous faut...", aiNavDisabled: "La navigation par IA est désactivée.", - aiNavResultsTitle: "Suggestions de navigation IA", + aiNavResultsTitle: "Suggestions AINav", aiNavQueryLabel: "Requête", - aiNavResultsDescription: "Choisissez l'itinéraire qui correspond le mieux à ce que vous cherchez.", - aiNavResultPath: "Route", - aiNavResultDescription: "Ce que vous pouvez y faire", aiNavResultMatch: "Correspondance", aiNavResultsEmpty: "Aucune route correspondante. Essayez /search.", - aiNavSearchInstead: "Rechercher plutôt", modulesAINavLabel: "AINav", modulesAINavDescription: "Module pour des requêtes en langage naturel sur le contenu du réseau.", - directConnect: "Connexion Directe", directConnectDescription: "Connectez-vous directement à un pair en saisissant son adresse IP, port et clé publique. Le pair sera ajouté comme connexion suivie.", peerHost: "IP", - peerPort: "Port (par défaut : 8008)", + peerPort: "Port", peerPublicKey: "Clé publique (@...ed25519)", connectAndFollow: "Connecter", - deviceSourceLabel: "Appareil", modulesPresetTitle: "Configurations Communes", - modulesPreset_minimal: "Minimal", - modulesPreset_basic: "Basique", - modulesPreset_social: "Social", - modulesPreset_economy: "Économie", - modulesPreset_full: "Complet", + workflowsTitle: "Flux de travail", + workflowsDescription: "Un workflow définit le thème et les modules actifs.", + workflowsSet: "Appliquer le workflow", + workflow_default: "Par défaut", + workflow_jobs: "Emploi", + workflow_social: "Social", + workflow_business: "Affaires", + workflow_night: "Nuit", + workflow_kids: "Enfants", + workflow_activists: "Activisme", + workflow_mobile: "Mobile", statsCarbonFootprintTitle: "Empreinte Carbone", - statsCarbonFootprintNetwork: "Empreinte carbone du réseau", - statsCarbonFootprintYours: "Votre empreinte carbone", statsCarbonTombstone: "Empreinte du tombstoning", - feedSuccessMsg: "Feed publié avec succès !", - dominantOpinionLabel: "Opinion dominante", uploadMedia: "Télécharger un média (max: 50 Mo)", invitesUnfollow: "Ne plus suivre", invitesFollow: "Suivre", invitesUnfollowedInvites: "Non fédérés", invitesNoUnfollowed: "Aucun pub non fédéré.", blockchainContentDeleted: "Ce contenu a été supprimé", - visitContent: "Voir le contenu", - bankEcoinHours: "Équivalence ECOin en temps", + visitContent: "Visiter le Contenu", + favoriteAdd: "Épingler aux Favoris", + pmContentTooltip: "Envoyer un Message Privé", + phoneCallTooltip: "Passer un Appel", + favoriteRemove: "Retirer des Favoris", + reportContent: "Signaler le Contenu", + bankEcoinHours: "Équivalence ECOin en temps (1 ECO)", mapsLabel: "Cartes", mapTitle: "Cartes", - mapDescription: "Explorez et gérez des cartes hors ligne dans votre réseau.", + mapDescription: "Gérez et partagez des cartes hors ligne dans votre réseau.", mapMineSectionTitle: "Vos Cartes", mapCreateSectionTitle: "Créer une Carte", mapUpdateSectionTitle: "Modifier la Carte", @@ -3156,14 +3294,13 @@ module.exports = { mapDescriptionLabel: "Description", mapDescriptionPlaceholder: "Décrivez la carte ou l'emplacement...", mapTypeLabel: "Type de carte", - mapTypeSingle: "UNIQUE (emplacement initial uniquement)", - mapTypeOpen: "OUVERT (tout le monde peut ajouter des marqueurs)", - mapTypeClosed: "FERMÉ (seul le créateur ajoute des marqueurs)", + mapInviteMode: "Invitation", + mapTypeSingle: "UNIQUE", + mapTypeOpen: "OUVERT", + mapTypeClosed: "FERMÉ", mapTagsLabel: "Étiquettes", - mapTagsPlaceholder: "Entrez des étiquettes séparées par des virgules", + mapTagsPlaceholder: "Saisissez des tags séparés par des virgules", mapUrlLabel: "URL de la carte", - mapPickCoordLabel: "Ou sélectionnez un emplacement sur la grille :", - mapMarkersLabel: "marqueurs", mapMarkersTitle: "Marqueurs", mapMarkerDefault: "Marqueur", mapMarkerLatLabel: "Latitude du marqueur", @@ -3173,9 +3310,22 @@ module.exports = { markerImageLabel: "Image du Marqueur", mapAddMarkerTitle: "Ajouter un Marqueur", mapAddMarkerButton: "Ajouter un Marqueur", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "Effacer le marqueur", mapApplyZoom: "Appliquer Zoom", - mapSearchPlaceholder: "Chercher description, étiquettes, auteur...", + mapPlaceSearchPlaceholder: "Trouver un lieu ou un pays...", + mapNoPlaces: "Aucun lieu trouvé.", + mapClusterTitle: "marqueurs ici, cliquez pour zoomer", + mapDeleteMarker: "Supprimer le marqueur", + mapZoomIn: "Zoom avant", + mapZoomOut: "Zoom arrière", + mapFitMarkers: "Ajuster à tous les marqueurs", + mapPanLabel: "Déplacer la carte", + mapCenterHere: "Centrer ici", + mapNear: "près de", + mapPickedSpot: "Point choisi", + mapPickHint: "Cliquez sur la carte pour choisir un point.", + mapClearPick: "Effacer", + mapSearchPlaceholder: "Rechercher dans Cartes...", mapSearchButton: "Chercher", mapUpdatedAt: "Mis à jour", mapNoMatch: "Aucune carte ne correspond à votre recherche.", @@ -3186,118 +3336,820 @@ module.exports = { typeMapMarker: "MARQUEUR DE CARTE", modulesMapLabel: "Cartes", modulesMapDescription: "Module pour gérer et partager des cartes hors ligne.", + emergenciesTitle: "Urgences", + modulesEmergenciesLabel: "Urgences", + modulesEmergenciesDescription: "Module pour lancer, confirmer et suivre des urgences dans le réseau.", + emergenciesDescription: "Module pour lancer, confirmer et suivre des urgences dans le réseau.", + emergencyCreate: "Créer une urgence", + emergencyUpdate: "Mettre à jour", + emergencyDelete: "Supprimer", + emergencyTitleLabel: "Titre", + emergencyTitlePlaceholder: "Que se passe-t-il ?", + emergencyTextLabel: "Détails", + emergencyTextPlaceholder: "Où, depuis quand, que faire...", + emergencyCategoryLabel: "Catégorie", + emergencyCategoryWeather: "MÉTÉO", + emergencyCategoryInfrastructure: "INFRASTRUCTURE", + emergencyCategoryHealth: "SANTÉ", + emergencyCategorySecurity: "SÉCURITÉ", + emergencyCategoryLost: "OBJETS TROUVÉS", + emergencyCategoryNeighborhood: "VOISINAGE", + emergencyExpiresLabel: "Expire dans", + emergencyExpires1d: "1 jour", + emergencyExpires3d: "3 jours", + emergencyExpires7d: "1 semaine", + emergencyExpires30d: "1 mois", + emergencyExpiresNone: "Indéfini", + emergencyExpiresKeep: "Conserver la date actuelle", + emergencyTagsLabel: "Étiquettes", + emergencyTagsPlaceholder: "Saisissez des tags séparés par des virgules", + emergencyFilterAll: "TOUTES", + emergencyFilterMine: "MIENNES", + emergencyFilterRecent: "RÉCENTES", + emergencySeverityUnverified: "NON VÉRIFIÉE", + emergencySeverityLow: "FAIBLE", + emergencySeverityMedium: "MOYENNE", + emergencySeverityHigh: "ÉLEVÉE", + emergencyStatusActive: "ACTIVE", + emergencyStatusResolved: "RÉSOLUE", + emergencyStatusExpired: "EXPIRÉE", + emergencyConfirmations: "Confirmations", + emergencyConfirmButton: "Confirmer", + emergencyConfirmed: "Vous avez confirmé cette urgence", + emergencyResolveButton: "Résoudre", + emergencyReopenButton: "Rouvrir", + emergencyGenerateReport: "Générer le rapport", + emergencyUpdateEdited: "Modifiée", + emergencyLastUpdate: "DERNIÈRE MISE À JOUR", + emergencyUpdatesTitle: "Dernières mises à jour :", + emergencyNoUpdates: "Pas encore de mises à jour.", + emergencyAddUpdate: "Publier une mise à jour", + emergencyUpdatePlaceholder: "Qu’est-ce qui a changé ?", + emergencyNoItems: "Aucune urgence trouvée.", + emergencyExpiresAt: "Expire", + emergencyBannerLabel: "Urgence active", + emergencyCreateSectionTitle: "Créer une urgence", + emergencyUpdateSectionTitle: "Mettre à jour l’urgence", + emergencySearchPlaceholder: "Rechercher dans les urgences...", + typeEmergency: "URGENCES", + typeEmergencyConfirm: "CONFIRMATION D’URGENCE", + typeEmergencyUpdate: "MISE À JOUR D’URGENCE", + statsEmergency: "Urgences", + statsEmergencyConfirm: "Confirmations d’urgences", + statsEmergencyUpdate: "Mises à jour d’urgences", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "Une urgence que vous suivez a une mise à jour.", + emergencyBotResolvedTitle: "Une urgence que vous suivez a été résolue.", + dataKindEmergencies: "URGENCES", + mailingTitle: "Listes de diffusion", + modulesMailingLabel: "Listes de diffusion", + modulesMailingDescription: "Découvrez et gérez des listes de diffusion dans votre réseau.", + modulesPhoneLabel: "Téléphone", + modulesPhoneDescription: "Module pour appeler d'autres habitants avec des appels vidéo chiffrés de bout en bout.", + mailingDescription: "Découvrez et gérez des listes de diffusion dans votre réseau.", + mailingCreate: "Créer une liste de diffusion", + mailingUpdate: "Mettre à jour", + mailingArchive: "Archiver", + mailingActivate: "Activer", + mailingCreateSectionTitle: "Créer une liste de diffusion", + mailingUpdateSectionTitle: "Mettre à jour la liste de diffusion", + mailingTitleLabel: "Titre", + mailingTitlePlaceholder: "Nom de la liste de diffusion", + mailingDescriptionLabel: "Description", + mailingDescriptionPlaceholder: "De quoi parle cette liste ?", + mailingTypeLabel: "Type de liste de diffusion", + mailingTypeOpen: "OUVERTE", + mailingTypeClosed: "FERMÉE", + mailingStatusLabel: "Statut", + mailingStatusActive: "ACTIVE", + mailingStatusArchived: "ARCHIVÉE", + mailingMembersLabel: "Inviter des habitants", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Abonnés", + mailingTagsLabel: "Étiquettes", + mailingTagsPlaceholder: "Saisissez des tags séparés par des virgules", + mailingFilterAll: "TOUTES", + mailingFilterMine: "MIENNES", + mailingFilterSubscribed: "ABONNÉES", + mailingFilterRecent: "RÉCENTES", + mailingHistoryDate: "DATE", + mailingHistoryThreads: "FILS", + mailingHistoryInhabitants: "HABITANTS", + mailingHistorySearchPlaceholder: "Rechercher dans l’historique...", + mailingNoMessages: "Pas encore de messages.", + mailingNoItems: "Aucune liste de diffusion trouvée.", + mailingSearchPlaceholder: "Rechercher dans les listes de diffusion...", + mailingWriteButton: "Écrire un message", + mailingWriteTitle: "Écrire à la liste", + mailingReplyTitle: "Répondre à la liste", + mailingSend: "Envoyer", + mailingTextPlaceholder: "Votre message atteint tous les participants de la liste.", + mailingParticipants: "Participants", + inboxFilterMailing: "Listes de diffusion", + mailingReplyPlaceholder: "Répondre à la liste...", + mailingReplyToList: "Répondre à la liste", + typeMailingList: "LISTE DE DIFFUSION", + statsMailingList: "Listes de diffusion", + mailingListLabel: "LISTE DE DIFFUSION", + logisticsTitle: "Logistique", + modulesLogisticsLabel: "Logistique", + modulesLogisticsDescription: "Découvrez et gérez des plans logistiques dans votre réseau.", + logisticsDescription: "Découvrez et gérez des plans logistiques dans votre réseau.", + logisticsCreate: "Créer une route", + logisticsUpdate: "Mettre à jour", + logisticsClose: "Clôturer", + logisticsReopen: "Rouvrir", + logisticsCreateSectionTitle: "Créer une route", + logisticsUpdateSectionTitle: "Mettre à jour la route", + logisticsTitleLabel: "Titre", + logisticsTitlePlaceholder: "Trajet vers le marché, camionnette disponible...", + logisticsKindLabel: "Type de logistique", + logisticsKindTrip: "TRAJET", + logisticsKindShipment: "ENVOI", + logisticsKindTrips: "TRAJETS", + logisticsKindShipments: "ENVOIS", + logisticsModeLabel: "Mode", + logisticsModeOffer: "J’OFFRE", + logisticsModeRequest: "JE CHERCHE", + logisticsModeOffers: "OFFRES", + logisticsModeRequests: "DEMANDES", + logisticsDescriptionLabel: "Description", + logisticsDescriptionPlaceholder: "Point de rendez-vous, ce que vous transportez, conditions...", + logisticsOriginLabel: "Origine", + logisticsOriginPlaceholder: "Point de départ", + logisticsDestinationLabel: "Destination", + logisticsDestinationPlaceholder: "Point d’arrivée", + logisticsDateLabel: "Date et heure", + logisticsRecurrenceLabel: "Récurrence", + logisticsFrequencyLabel: "Fréquence", + logisticsRecurrenceNone: "UNIQUE", + logisticsRecurrenceDaily: "QUOTIDIEN", + logisticsRecurrenceWeekly: "HEBDOMADAIRE", + logisticsRecurrenceMonthly: "MENSUEL", + logisticsSeatsLabel: "Places", + logisticsSizeLabel: "Taille", + logisticsSizePlaceholder: "ex. 2m x 1m", + logisticsWeightLabel: "Poids", + logisticsWeightPlaceholder: "ex. 40kg", + logisticsPriceFree: "GRATUIT", + logisticsPriceEco: "ECO", + logisticsPriceTime: "TEMPS", + logisticsPriceLabel: "Prix", + logisticsHours: "h", + logisticsOrderRefLabel: "Lien de commande", + logisticsOrderRefPlaceholder: "/shops/... ou /market/... de la commande à livrer", + logisticsOrderLink: "Commande liée", + logisticsTagsLabel: "Étiquettes", + logisticsTagsPlaceholder: "Saisissez des tags séparés par des virgules", + logisticsFilterAll: "TOUTES", + logisticsFilterMine: "MIENNES", + logisticsFilterBooked: "RÉSERVÉES", + logisticsFilterRecent: "RÉCENTES", + logisticsFilterHistory: "HISTORIQUE", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "OUVERTE", + logisticsStatusUpcoming: "À VENIR", + logisticsStatusPast: "PASSÉES", + logisticsStatusClosed: "CLÔTURÉES", + logisticsZoneLabel: "Zone", + logisticsSearchPlaceholder: "Rechercher dans la logistique...", + logisticsNoItems: "Aucune route trouvée.", + logisticsBookingPending: "EN ATTENTE", + logisticsBookingConfirmed: "CONFIRMÉE", + logisticsBookingRejected: "REFUSÉE", + logisticsBookingCancelled: "ANNULÉE", + logisticsBookingDelivered: "LIVRÉE", + logisticsConfirm: "Confirmer", + logisticsReject: "Refuser", + logisticsDeliver: "Livrer", + logisticsReceiptLabel: "Reçu", + logisticsReceiptPlaceholder: "Note de livraison (facultatif)", + logisticsCancelBooking: "Annuler la réservation", + logisticsBookTitle: "Réserver cette route", + logisticsNotesLabel: "Notes", + logisticsNotesPlaceholder: "Ce que le responsable doit savoir", + logisticsBook: "Réserver", + logisticsBookingsTitle: "Réservations", + logisticsMyBookingTitle: "Votre réservation", + logisticsRatingsTitle: "Évaluations", + logisticsNoRatings: "Pas encore d’évaluations.", + logisticsRatingsWhenClosed: "Les évaluations s’ouvrent une fois la route clôturée.", + logisticsScoreLabel: "Note", + logisticsRatingPlaceholder: "Comment ça s’est passé ?", + logisticsRate: "Évaluer", + typeLogisticsRoute: "ROUTE", + typeLogisticsRating: "ÉVALUATION DE ROUTE", + statsLogisticsRoute: "Routes", + statsLogisticsRating: "Évaluations de routes", + logisticsRouteLabel: "ROUTE", + podcastsTitle: "Podcasts", + modulesPodcastsLabel: "Podcasts", + modulesPodcastsDescription: "Module pour publier des chaînes de podcast.", + podcastsDescription: "Module pour publier des chaînes de podcast.", + podcastCreate: "Créer un podcast", + podcastUpdate: "Mettre à jour", + podcastDelete: "Supprimer", + podcastCreateSectionTitle: "Créer un podcast", + podcastUpdateSectionTitle: "Mettre à jour le podcast", + podcastTitleLabel: "Titre", + podcastTitlePlaceholder: "Nom de la chaîne", + podcastDescriptionLabel: "Description", + podcastDescriptionPlaceholder: "De quoi parle ce podcast ?", + podcastCategoryLabel: "Catégorie", + podcastCategoryNews: "ACTUALITÉS", + podcastCategoryMusic: "MUSIQUE", + podcastCategoryTalk: "DISCUSSION", + podcastCategoryEducation: "ÉDUCATION", + podcastCategoryTech: "TECH", + podcastCategoryCulture: "CULTURE", + podcastCategoryCommunity: "COMMUNAUTÉ", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "AUTRE", + podcastTagsLabel: "Étiquettes", + podcastCoverLabel: "Couverture (image ou vidéo)", + podcastEpisodeAudioLabel: "Fichier audio ou vidéo (max : 75MB)", + podcastTagsPlaceholder: "Saisissez des tags séparés par des virgules", + podcastFilterAll: "TOUS", + podcastFilterMine: "MIENS", + podcastFilterRecent: "RÉCENTS", + podcastFilterTop: "TOP", + podcastFilterViewers: "AUDITEURS", + podcastSearchPlaceholder: "Rechercher dans les podcasts...", + podcastNoItems: "Aucun podcast trouvé.", + podcastEpisodesLabel: "Épisodes", + podcastPlaysLabel: "Vues", + podcastLastEpisode: "Dernier épisode", + podcastEpisodesTitle: "Épisodes", + podcastNoEpisodes: "Pas encore d’épisodes.", + podcastEpisodeAdd: "Ajouter un épisode", + podcastEpisodeCreateTitle: "Publier un épisode", + podcastEpisodeUpdateTitle: "Mettre à jour l’épisode", + podcastEpisodeTitleLabel: "Titre de l’épisode", + podcastEpisodeTitlePlaceholder: "Titre de l’épisode", + podcastEpisodeDescriptionLabel: "Notes", + podcastEpisodeDescriptionPlaceholder: "Ce qui se passe dans cet épisode, liens, crédits...", + podcastEpisodePublish: "Publier", + podcastPlayed: "Écouté", + podcastMarkPlayed: "Marquer comme écouté", + typePodcast: "PODCAST", + typePodcastEpisode: "ÉPISODE", + typePodcastOpinion: "AVIS SUR L’ÉPISODE", + typePodcastPlay: "ÉCOUTE", + statsPodcast: "Podcasts", + statsPodcastEpisode: "Épisodes", + statsPodcastPlay: "Écoutes", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "Un blog que vous suivez a une nouvelle entrée.", + podcastBotEpisodeTitle: "Un podcast que vous suivez a un nouvel épisode.", + podcastLabel: "PODCAST", + podcastEpisodeLabel: "ÉPISODE", + campaignsTitle: "Campagnes", + modulesCampaignsLabel: "Campagnes", + modulesCampaignsDescription: "Découvrez et gérez des campagnes dans votre réseau.", + campaignsDescription: "Découvrez et gérez des campagnes dans votre réseau.", + campaignCreate: "Créer une campagne", + campaignUpdate: "Mettre à jour", + campaignDelete: "Supprimer", + campaignClose: "Clôturer", + campaignReopen: "Rouvrir", + campaignCreateSectionTitle: "Créer une campagne", + campaignUpdateSectionTitle: "Mettre à jour la campagne", + campaignTitleLabel: "Titre", + campaignTitlePlaceholder: "Que voulez-vous obtenir ?", + campaignTextLabel: "Texte", + campaignTextPlaceholder: "Expliquez la cause, qui décide et ce que vous demandez...", + campaignCategoryLabel: "Catégorie", + campaignCategoryEnvironment: "ENVIRONNEMENT", + campaignCategoryRights: "DROITS", + campaignCategoryHealth: "SANTÉ", + campaignCategoryEducation: "ÉDUCATION", + campaignCategoryInfrastructure: "INFRASTRUCTURE", + campaignCategoryCulture: "CULTURE", + campaignCategoryEconomy: "ÉCONOMIE", + campaignCategoryOther: "AUTRE", + campaignGoalLabel: "Objectif de signatures", + campaignDeadlineLabel: "Échéance", + campaignTagsLabel: "Étiquettes", + campaignTagsPlaceholder: "Saisissez des tags séparés par des virgules", + campaignFilterAll: "TOUTES", + campaignFilterMine: "MIENNES", + campaignFilterSigned: "SIGNÉES", + campaignFilterRecent: "RÉCENTES", + campaignFilterTop: "TOP", + campaignStatusOpen: "OUVERTE", + campaignStatusAchieved: "ATTEINTE", + campaignStatusUnachieved: "NON ATTEINTE", + campaignStatusClosed: "CLÔTURÉE", + campaignSearchPlaceholder: "Rechercher dans les campagnes...", + campaignNoItems: "Aucune campagne trouvée.", + campaignSignaturesLabel: "Signatures", + campaignSignaturesTitle: "Soutiens", + campaignNoSignatures: "Pas encore de signatures.", + campaignSignTitle: "Signer cette campagne", + campaignSignPlaceholder: "Pourquoi la soutenez-vous ? (facultatif)", + campaignSign: "Signer", + campaignSigned: "Vous avez déjà signé !", + campaignNotSigned: "Il manque encore votre signature", + campaignElevate: "Porter au Parlement", + campaignElevated: "Portée au Parlement", + campaignUpdatesTitle: "Mises à jour", + campaignNoUpdates: "Pas encore de mises à jour.", + campaignUpdatePlaceholder: "Que s’est-il passé ?", + campaignAddUpdate: "Publier la mise à jour", + campaignQrAlt: "QR pour signer cette campagne", + campaignQrCaption: "Scannez pour signer", + typeCampaign: "CAMPAGNE", + typeCampaignSignature: "SIGNATURE", + typeCampaignUpdate: "MISE À JOUR DE CAMPAGNE", + statsCampaign: "Campagnes", + statsCampaignSignature: "Signatures de campagnes", + statsCampaignUpdate: "Mises à jour de campagnes", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "Une campagne que vous suivez a une mise à jour.", + campaignBotAchievedTitle: "Une campagne que vous suivez a atteint son objectif.", + campaignBotRaisedTitle: "Une campagne que vous avez signée a été portée au Parlement.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Un trajet que vous avez réservé a changé.", + logisticsBotClosedTitle: "Un trajet que vous avez réservé a été fermé.", + logisticsBotBookedTitle: "Quelqu'un a réservé votre trajet.", + logisticsBotCancelledTitle: "Une réservation sur votre trajet a été annulée.", + logisticsBotConfirmedTitle: "Votre réservation est confirmée : le colis est en route.", + logisticsBotRejectedTitle: "Votre réservation a été refusée.", + logisticsBotDeliveredTitle: "Votre colis est arrivé.", + campaignLabel: "CAMPAGNE", + campaignGenerateReport: "Générer le rapport", + logisticsGenerateReport: "Générer le rapport", + mailingGenerateReport: "Générer le rapport", + campaignUpdateEdited: "Modifiée", + mailingMessagesLabel: "Messages", + logisticsBookingsLabel: "Réservations", + backupTitle: "Sauvegarde", + modulesBackupLabel: "Sauvegarde", + modulesBackupDescription: "Module pour exporter et importer vos clés et faire des sauvegardes chiffrées.", + backupDescription: "Module pour exporter et importer vos clés et faire des sauvegardes chiffrées.", + backupTypeRecovery: "RÉCUPÉRATION", + backupTypeKeys: "EXPORTER LES CLÉS", + backupTypeFull: "SAUVEGARDE COMPLÈTE", + backupTypeRestore: "RESTAURER LA SAUVEGARDE", + backupTypeInstant: "INSTANTANÉE", + backupInstantDescription: "Un .zip brut de tout votre registre et de vos fichiers, sans votre clé secrète et sans mot de passe. Rapide, mais ", + backupInstantRisk: "quiconque a le fichier lit tout.", + backupKitPageDescription: "Le kit de récupération ne peut être ouvert que depuis cet appareil.", + backupKitId: "ID Oasis", + backupKitDate: "Généré", + backupKitQrAlt: "Code QR de votre clé secrète", + backupKitStep1: "Imprimez-le ou recopiez-le et rangez-le en lieu sûr, hors ligne.", + backupKitStep2: "Pour récupérer, installez Oasis sur un nouvel appareil et importez ce secret depuis Sauvegarde › Clés.", + backupKitStep3: "Détruisez cette page si elle est un jour exposée.", + backupKitPdfButton: "Générer le PDF", + backupScopeLabel: "Portée", + backupScopeAll: "TOUT", + backupScopeMine: "SEULEMENT MON CONTENU", + backupSinceLabel: "Depuis", + backupEstimateTitle: "Estimation", + backupFileInfo: "Votre copie chiffrée sera téléchargée sur votre appareil (nom : oasis-backup-DATE.oasisbk)", + backupExportButton: "Télécharger la sauvegarde", + backupRestorePassword: "Saisissez le mot de passe utilisé lors de la sauvegarde", + backupRestoreButton: "Restaurer la sauvegarde", + backupRestoredLine: "Sauvegarde restaurée.", + backupRestoredMessages: "Messages ajoutés", + backupRestoredSkipped: "Déjà présents", + backupRestoredBlobs: "Fichiers ajoutés", + backupRestoredState: "Réglages restaurés", + backupRestoredFailed: "Échecs", + backupRestoredForked: "Divergents", + backupRestoreRunning: "Restauration de la copie en cours. Cette page se rafraîchit automatiquement ; vous pouvez continuer à utiliser Oasis.", + backupRestoreElapsed: "Écoulé", + backupRestoreFailedLine: "La copie n’a pas pu être restaurée.", + backupRestoreForkMine: "Cet appareil a déjà publié des messages avec votre identité qui ne figurent pas dans la copie, elle n’a donc pas pu être appliquée par-dessus. Restaurez sur une installation propre juste après avoir importé vos clés, avant toute autre action.", + verificationTitle: "Vérification", + verificationDescription: "Vérifiez la chaîne de votre fil, détectez les bifurcations et passez en revue les fichiers orphelins ou manquants.", + verificationRun: "Lancer la vérification", + verificationTotalMessages: "messages dans le journal", + verificationFeed: "Votre fil", + verificationMessages: "messages", + verificationLastSequence: "dernière séquence", + verificationGaps: "Trous", + verificationBrokenLinks: "Liens rompus", + verificationBadHashes: "Hashs incorrects", + verificationBadSignatures: "Signatures incorrectes", + verificationDuplicates: "Séquences dupliquées", + verificationForks: "Bifurcations", + verificationBlobs: "Fichiers", + verificationBlobsPresent: "présents", + verificationBlobsReferenced: "référencés", + verificationBlobsOrphan: "orphelins", + verificationBlobsMissing: "manquants", + verificationBlobsMissingOwn: "Vos propres fichiers absents de cet appareil", + verificationBlobsMissingOthers: "Fichiers d'autres habitants pas encore téléchargés", + verificationBlobsUnavailable: "Impossible de lister les fichiers sur cet appareil.", + wikiTitle: "Wikis", + modulesWikiLabel: "Wikis", + modulesWikiDescription: "Module pour découvrir et gérer des wikis.", + wikiDescription: "Module pour découvrir et gérer des wikis.", + wikiCreate: "Créer un wiki", + wikiUpdate: "Mettre à jour", + wikiDelete: "Supprimer", + wikiEdit: "Modifier", + wikiTitleLabel: "Titre", + wikiTitlePlaceholder: "Titre du wiki...", + wikiBodyLabel: "Contenu", + wikiBodyPlaceholder: "Reliez d’autres wikis avec [[Titre du wiki]] ou [[Titre du wiki|un libellé personnalisé]].\n\nMentionnez un habitant avec @id.\n\nÉtiquetez avec #oasis.\n\nCollez des liens comme https://solarnethub.com\n\nUne ligne vide ouvre un nouveau paragraphe ; les sauts de ligne simples sont conservés.\n\nJoignez n’importe quel média pris en charge : il sera intégré ici.", + formatBarLabel: "Format", + wikiStatusLabel: "Type de wiki", + wikiStatusOpen: "OUVERT", + wikiStatusClosed: "FERMÉ", + wikiTagsLabel: "Étiquettes", + wikiTagsPlaceholder: "Saisissez des tags séparés par des virgules", + wikiSummaryLabel: "Résumé de la modification", + wikiSummaryPlaceholder: "Qu’est-ce qui a changé ?", + wikiFilterAll: "TOUTES", + wikiFilterMine: "MIENNES", + wikiFilterRecent: "RÉCENTES", + wikiFilterLinked: "LIÉES", + wikiChanges: "Modifications récentes", + wikiNoPages: "Aucun wiki trouvé.", + wikiNoChanges: "Aucune modification pour l’instant.", + wikiHistory: "Historique", + wikiVersion: "Version", + wikiVersions: "versions", + wikiRestore: "Restaurer", + wikiDiff: "Modifications", + wikiViewVersion: "Voir", + wikiLinkedPages: "Pages liées", + wikiNoLinkedPages: "Cette page n'est encore liée à aucune autre.", + wikiMissingLink: "La page n’existe pas encore", + wikiDuplicateTitle: "Une page portant ce titre existe déjà. Changez le titre pour créer une autre page.", + wikiCreateSectionTitle: "Créer un wiki", + wikiUpdateSectionTitle: "Mettre à jour le wiki", + wikiSearchPlaceholder: "Rechercher dans le Wiki...", + wikiCurrentVersion: "Version actuelle", + wikiViewingVersion: "Vous consultez une ancienne version", + wikiBackToPage: "Retour à la page", + wikiLinkedChip: "Liée", + wikiRestoredSummary: "Version restaurée", + wikiExportPdf: "Générer la page", + wikiPreview: "Aperçu", + tribeSectionWiki: "WIKI", + tribeWikiCreate: "Créer un wiki", + tribeWikiEmpty: "Pas encore de wikis.", + typeWiki: "WIKI", padsTitle: "Pads", padTitle: "Pad", modulesPadsLabel: "Pads", - modulesPadsDescription: "Module pour gérer les éditeurs de texte collaboratifs.", - padFilterAll: "TOUS", - padFilterMine: "MES PADS", - padFilterRecent: "RÉCENT", - padFilterOpen: "OUVERT", - padFilterClosed: "FERMÉ", + modulesPadsDescription: "Module pour gérer des éditeurs de texte collaboratifs.", padCreate: "Créer Pad", - padUpdate: "Modifier Pad", - padDelete: "Supprimer Pad", + padUpdate: "Modifier", + padDelete: "Supprimer", padTitleLabel: "Titre", padTitlePlaceholder: "Entrez le titre du pad...", padStatusLabel: "Statut", + padTypeLabel: "Type de pad", padStatusOpen: "OUVERT", padStatusInviteOnly: "SUR INVITATION", padStatusClosed: "FERMÉ", padDeadlineLabel: "Date limite", padTagsLabel: "Tags", - padTagsPlaceholder: "tag1, tag2, ...", + padTagsPlaceholder: "Saisissez des tags séparés par des virgules", padMembersLabel: "Membres", - padVisitPad: "Visiter le Pad", - padShareUrl: "Partager URL", padCreated: "Créé", - padAuthor: "Auteur", - padGenerateCode: "Générer un Code", - padInviteCodeLabel: "Code d'Invitation", - padInviteCodePlaceholder: "Entrez le code d'invitation...", - padValidateInvite: "Valider", - padStartEditing: "COMMENCER À ÉDITER !", + padStartEditing: "Modifier", padEditorPlaceholder: "Commencez à écrire...", padSubmitEntry: "Envoyer", padNoEntries: "Aucune entrée pour l'instant.", padAllSectionTitle: "Tous les Pads", padMineSectionTitle: "Mes Pads", padsDescription: "Gérez des éditeurs de texte chiffrés collaboratifs dans votre réseau.", + roomsTitle: "Salons", + roomsDescription: "Découvrez et rejoignez des salons de visioconférence chiffrés dans votre réseau.", + modulesRoomsLabel: "Salons", + modulesRoomsDescription: "Module pour découvrir et rejoindre des salons de visioconférence chiffrés.", + roomFilterRecent: "RÉCENTS", + roomFilterMine: "LES MIENS", + roomFilterAll: "TOUS", + roomFilterLive: "EN DIRECT", + roomFilterOpen: "OUVERTS", + roomFilterInvite: "SUR INVITATION", + roomFilterClosed: "FERMÉS", + roomCreate: "Créer un salon", + roomUpdate: "Modifier", + roomDelete: "Supprimer", + roomClose: "Fermer", + roomReopen: "Rouvrir", + roomCreateSectionTitle: "Créer un salon", + roomUpdateSectionTitle: "Modifier le salon", + roomTitlePlaceholder: "Titre du salon", + roomDescriptionLabel: "Description", + roomDescriptionPlaceholder: "De quoi allez-vous parler ?", + roomTypeLabel: "Type de salon", + roomTagsLabel: "Tags", + roomStatusOpen: "OUVERT", + roomStatusInviteOnly: "SUR INVITATION", + roomStatusClosed: "FERMÉ", + roomParticipants: "Participants", + roomJoin: "Rejoindre", + roomLeave: "Quitter", + roomFull: "Salon complet", + roomLiveChip: "EN DIRECT", + roomMuted: "micro coupé", + roomRec: "Enregistrer l'audio", + roomRecStop: "Arrêter l'enregistrement", + roomStreamFile: "Diffuser", + roomStreamStop: "Arrêter", + roomStreaming: "En diffusion", + liveFullscreen: "Plein écran", + roomStreamPickTitle: "Choisir une vidéo", + roomStreamPickHint: "Vidéos de cet appareil. Celle que vous choisissez est diffusée depuis son emplacement, sans être copiée ni publiée.", + roomStreamGo: "Diffuser dans le salon", + roomStreamNotFound: "Cette vidéo n'est pas dans vos dossiers.", + roomPublicWarning: "Ce salon est diffusé sur le web public : tout le monde peut voir et entendre ce qui s'y passe.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} est entré dans la salle", + roomEventLeft: "{name} a quitté la salle", + roomEventRecStart: "{name} a commencé à enregistrer", + roomEventRecStop: "{name} a arrêté d'enregistrer", + roomEventMuted: "{name} a coupé son micro", + roomEventUnmuted: "{name} a réactivé son micro", + roomEventHand: "{name} a levé la main", + roomRaiseHand: "Lever la main", + roomLowerHand: "Baisser la main", + confirmActionTitle: "Confirmer cette action", + confirmActionButton: "Confirmer", + confirmJoinText: "Vous allez rejoindre avec cette invitation.", + confirmFollowText: "Vous allez soutenir cet habitant.", + confirmSignText: "Vous allez signer cette campagne.", + confirmDownloadText: "Vous allez télécharger ce fichier sur votre appareil.", + roomRecordings: "Enregistrements", + roomClearNotices: "Effacer", + roomLineSwitchboard: "Standard", + roomLineDndError: "Le numéro de ce salon n'accepte pas d'appels.", + roomNumberHint: "Composez ce numéro depuis Téléphone pour rejoindre le salon.", + liveRefresh: "Rafraîchir", + roomNobody: "Personne n'est dans le salon pour le moment.", + roomSomeone: "{n} dans le salon en ce moment.", + roomYou: "vous", + roomYouAreIn: "Vous y êtes", + roomNobodyInside: "Il n'y a personne dans cette salle pour le moment.", + roomErrorFull: "Le salon est complet. Vous pourrez entrer quand quelqu'un partira.", + roomErrorUnreachable: "Le salon est injoignable pour le moment. Réessayez plus tard.", + roomErrorBusy: "Vous êtes déjà dans un appel ou dans un autre salon.", + roomErrorClosed: "Ce salon est fermé.", + roomAccessDenied: "Ce salon est sur invitation. Il vous faut une invitation pour entrer.", + roomsNoItems: "Aucun salon trouvé.", + roomSearchPlaceholder: "Rechercher des salons...", + roomInviteMode: "Invitation", + tribeSectionRooms: "SALONS", + tribeRoomCreate: "Créer un salon", + tribeRoomsEmpty: "Aucun salon pour l'instant.", + typeRoom: "SALONS", + typeRoomMember: "PARTICIPANT DE SALON", + statsRoom: "Salons", + statsRoomMember: "Participants aux salons", + favoritesFilterRooms: "SALONS", + invitesRoomsTitle: "Salons", + inviteCodeInvalid: "Ce code d'invitation n'est pas valide ou a déjà été utilisé.", + inviteAlreadyMember: "Vous en faites déjà partie. Vous pouvez l'ouvrir depuis son module.", + actionFailed: "Cela n'a pas pu être fait. Réessayez.", + publishTooShort: "Votre publication est trop courte.", + walletSendFailed: "Le paiement n'a pas pu être envoyé : {reason}", + pixeliaCoordsError: "Ces coordonnées sont en dehors de la grille.", + courtsErrTitle: "Un titre est requis.", + courtsErrRespondent: "Indiquez contre qui porte l'affaire.", + courtsErrRespondentId: "Ce n'est pas un Oasis ID valide.", + courtsErrMethod: "Choisissez comment l'affaire sera résolue.", + courtsErrCaseNotFound: "Cette affaire est introuvable.", + courtsErrBrief: "Rédigez votre réponse.", + courtsErrStance: "Choisissez votre position.", + courtsErrResult: "Indiquez le résultat.", + courtsErrTerms: "Rédigez les termes de l'accord.", + courtsErrMediator: "Ajoutez au moins un médiateur.", + courtsErrSelfMediator: "Vous ne pouvez pas être médiateur dans votre propre affaire.", + courtsErrJudge: "Choisissez un juge.", + courtsErrSelfJudge: "Vous ne pouvez pas juger votre propre affaire.", + courtsErrVisibility: "Choisissez qui peut voir l'affaire.", + courtsErrNomination: "Cette nomination est introuvable.", + opinionAlreadyGiven: "Vous avez déjà donné votre avis.", + cipherPasswordShort: "Le mot de passe doit contenir au moins 32 caractères.", + errorForbidden: "Vous n'avez pas accès à ceci.", + errorNotFound: "Introuvable.", + errorClosed: "C'est fermé.", + errorInviteOnly: "C'est sur invitation. Il vous faut une invitation.", + errorTooLong: "Ce texte est trop long.", + errorRequired: "Des informations obligatoires manquent.", + errorInvalid: "Certaines informations ne sont pas valides.", + errorNotAuthor: "Seul l'auteur peut faire cela.", + panicRemoved: "Votre blockchain a été supprimée.", padRecentSectionTitle: "Pads Récents", padOpenSectionTitle: "Pads Ouverts", padClosedSectionTitle: "Pads Fermés", padCreateSectionTitle: "Créer un Nouveau Pad", padUpdateSectionTitle: "Modifier le Pad", - padInviteGenerated: "Code d'Invitation Généré", typePad: "PADS", padNew: "NOUVEAU", padAddFavorite: "Ajouter aux Favoris", padRemoveFavorite: "Retirer des Favoris", - padClose: "Fermer le pad", + padClose: "Fermer", padBackToEditor: "Retour à l'éditeur", - padSearchPlaceholder: "Rechercher des pads...", + padSearchPlaceholder: "Rechercher dans Pads...", modulesChatsLabel: "Chats", - modulesChatsDescription: "Module pour découvrir et gérer les chats chiffrés.", + modulesChatsDescription: "Module pour découvrir et gérer des chats.", typeChat: "CHATS", typeChatMessage: "MESSAGE CHAT", - chatLabel: "CHATS", - chatMessageLabel: "MESSAGES CHAT", chatsTitle: "Chats", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "Vos chats", + chatRecentTitle: "Chats récents", chatFavoritesTitle: "Favoris", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "Chats ouverts", + chatClosedTitle: "Chats fermés", chatDescription: "Description", chatCategory: "Catégorie", chatStatus: "STATUT", - chatFilterAll: "TOUS", - chatFilterMine: "MIEN", - chatFilterRecent: "RÉCENT", - chatFilterFavorites: "FAVORIS", - chatFilterOpen: "OUVERT", - chatFilterClosed: "FERMÉ", chatCreate: "Créer Chat", - chatUpdate: "Modifier Chat", - chatDelete: "Supprimer Chat", + chatUpdate: "Mettre à jour", + chatDelete: "Supprimer", chatClose: "Fermer Chat", chatVisitChat: "VOIR CHAT", chatUntitled: "Chat sans titre", chatNoItems: "Aucun chat trouvé.", chatParticipants: "Participants", - chatStartChatting: "COMMENCER À CHATTER!", - chatGenerateCode: "Générer Code", - chatShareUrl: "Partager URL", + chatMessagesLabel: "Messages", + chatThreadsLabel: "Fils", chatCreatedAt: "CRÉÉ", - chatSearchPlaceholder: "Rechercher chats...", + chatSearchPlaceholder: "Rechercher dans Chats...", chatStatusOpen: "OUVERT", chatStatusInviteOnly: "SUR INVITATION", chatStatusClosed: "FERMÉ", chatSendMessage: "Envoyer", + chatJumpLatest: "Dernier message", + chatReply: "Répondre", + chatReplyCancel: "Annuler la réponse", + chatPin: "Épingler", + chatUnpin: "Désépingler", + chatPinned: "Épinglés", + chatPickChat: "Choisissez un chat pour l'ouvrir", + chatNoneYet: "Aucun chat disponible pour l'instant.", + activitySearchPlaceholder: "Rechercher dans Activité...'activité...", + trendingSearchPlaceholder: "Rechercher dans Tendances...", + opinionsSearchPlaceholder: "Rechercher dans Opinions...", + votesSearchPlaceholder: "Rechercher dans Votations...", + gamesSearchPlaceholder: "Rechercher dans Jeux...", + schoolTitle: "École", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "Une page wiki que vous suivez a été modifiée.", + wikiBotRestoredTitle: "Une version antérieure d’une page wiki que vous suivez a été restaurée.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Un nouvel étudiant s'est inscrit à votre cours.", + schoolBotInvitedTitle: "Vous avez été invité à un cours.", + schoolBotAdmittedTitle: "Vous avez été admis à un cours.", + schoolBotCertificateTitle: "Vous avez reçu un certificat.", + schoolBotPassedTitle: "Un étudiant a réussi votre cours.", + schoolBotLessonTitle: "Une nouvelle leçon a été publiée.", + invitesSchoolTitle: "École", + schoolEnrollLabel: "Rejoindre le cours", + schoolExamRetryLater: "Vous pourrez réessayer après 24 heures.", + schoolApproved: "RÉUSSI", + schoolExamPassMark: "Note de passage (0-10)", + schoolExamPassed: "RÉUSSI", + schoolExamPending: "En attente", + schoolExamFailed: "ÉCHOUÉ", + schoolExamAddQuestion: "Ajouter une question", + schoolExamCreateQuestion: "Créer une question", + schoolExamOption: "Option", + schoolExamCorrect: "Option correcte", + schoolExamPoints: "Points", + schoolMaterialTextPlaceholder: "Notes pour cette leçon...", + schoolMaterials: "Matériels", + schoolAddMaterial: "Ajouter du matériel", + schoolNoMaterials: "Pas encore de matériel.", + bankYourSchoolBalance: "Vos revenus de l'école", + schoolProgress: "Progression", + schoolProgressMark: "Marquer comme terminée", + schoolProgressDone: "Terminée", + schoolExams: "Examens", + schoolExamCreate: "Créer un examen", + schoolExamTitlePlaceholder: "Titre de l'examen", + schoolExamHelp: "Une question par ligne : Question ? ; option ; option correcte* ; option", + schoolExamQuestionsPlaceholder: "Qu'est-ce que SSB ? ; Une base de données ; Un protocole P2P* ; Un navigateur", + schoolExamSubmit: "Envoyer les réponses", + schoolExamScore: "Votre note", + schoolNoScoresYet: "Pas encore de résultats.", + schoolLessonUnit: "Unité / phase", + schoolLessonUnitPlaceholder: "p. ex. Phase 1", + schoolLessonOrder: "Ordre", + profileClearnetSchoolLabel: "École", + schoolStartDate: "Date de début", + schoolSessionDate: "Date de la session", + schoolCourseChat: "Chat du cours", + agendaFilterSchool: "ÉCOLE", + agendaFilterCampaigns: "CAMPAGNES", + agendaFilterLogistics: "LOGISTIQUE", + schoolCourseType: "Type de cours", + schoolTypeOpen: "OUVERT", + schoolTypePaid: "PAYANT", + schoolTypeInvite: "SUR INVITATION", + schoolLessonLocked: "Leçon chiffrée — demandez la clé du cours à l'enseignant.", + schoolGrantAccess: "Envoyer la clé du cours", + schoolKeySent: "Clé envoyée", + schoolOngoing: "En cours", + schoolFilterApplied: "INSCRIT", + schoolFilterOpen: "OUVERTS", + schoolFilterFavorites: "FAVORIS", + schoolPendingPayment: "Paiement en attente", + schoolPendingStudents: "Inscriptions en attente", + schoolViewBill: "Voir la facture", + schoolPaymentNote: "L'inscription génère une facture en ECO (un transfert). Votre inscription est confirmée une fois le transfert signé.", + schoolCloseCourse: "Fermer le cours", + schoolReopenCourse: "Rouvrir le cours", + schoolDescription: "Enseignez et apprenez des cours dans votre réseau.", + schoolFilterAll: "TOUS", + schoolFilterMine: "LES MIENS", + schoolFilterTop: "TOP", + schoolFilterRecent: "RÉCENTS", + schoolCreateButton: "Créer un Cours", + schoolCreateSectionTitle: "Créer un cours", + schoolUpdateSectionTitle: "Mettre à jour le cours", + schoolCourseTitlePlaceholder: "Nom de votre cours", + schoolCourseDescriptionPlaceholder: "Qu'allez-vous enseigner dans ce cours ?", + schoolCourseImage: "Image du cours (taille max : 75MB)", + schoolTags: "Étiquettes", + schoolTagsPlaceholder: "Saisissez des tags séparés par des virgules", + schoolPrice: "Prix (ECO)", + schoolVisibility: "Visibilité", + schoolVisibilityPublic: "Public", + schoolVisibilityInvite: "Sur invitation", + schoolStatus: "Statut", + schoolClosed: "Fermé", + schoolFree: "Gratuit", + schoolCreate: "Créer le cours", + schoolUpdate: "Mettre à jour le cours", + schoolSearchPlaceholder: "Rechercher dans École...", + schoolSortTitle: "Titre", + schoolNoCourses: "Aucun cours trouvé.", + schoolStudents: "Étudiants", + schoolTeacher: "Enseignant", + schoolCreatedAt: "CRÉÉ", + schoolLessons: "Leçons", + schoolNoLessons: "Pas encore de leçons.", + schoolEnrollToSee: "Inscrivez-vous pour voir les leçons de ce cours.", + schoolEnrollButton: "S'INSCRIRE", + schoolUnenrollButton: "Quitter le cours", + schoolInviteRequired: "Ce cours est sur invitation.", + schoolTeacherPanel: "Panneau de l'enseignant", + schoolStudentPanel: "Panneau de l'étudiant", + schoolAddLesson: "Créer une leçon", + schoolLessonTitlePlaceholder: "Titre de la leçon", + schoolLessonTextPlaceholder: "Décrivez les notions abordées dans cette leçon...", + schoolInviteStudents: "Inviter des étudiants", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "INVITER", + schoolInvited: "Invités", + schoolIssueCertificate: "Créer un certificat", + schoolCertified: "Certifié", + schoolCertificateTextPlaceholder: "Note du certificat (optionnel)", + schoolLessonOpen: "Ouvrir la leçon", + schoolCertificatePdf: "PDF", + schoolCertificates: "Certificats", + schoolNoCertificates: "Aucun certificat émis pour le moment.", + schoolEditButton: "Modifier", + schoolDeleteButton: "Supprimer", + typeSchool: "ÉCOLE", + schoolCourseLabel: "COURS", + emergencyLabel: "URGENCE", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Cours", + statsSchoolLesson: "Leçons", + statsSchoolEnroll: "Inscriptions", + statsSchoolCertificate: "Certificats de l'école", + modulesSchoolLabel: "École", + modulesSchoolDescription: "Module pour échanger des connaissances.", + welcomeStepUxAction: "Enregistrer la vue", + welcomeStepWorkflowTitle: "Quel usage d'OASIS avez-vous en tête ?", + welcomeStepWorkflowText: "Oasis a beaucoup de modules. Choisissez la configuration qui vous correspond et seuls les modules utiles resteront activés. Vous pourrez l'ajuster à tout moment dans Modules.", + welcomeStepWorkflowAction: "Enregistrer la configuration", + welcomeWorkflow_full: "Par défaut", + chatRateLimitTitle: "Trop de messages", + chatRateLimitMessage: "Vous avez atteint la limite de messages que ce salon accepte en une heure. Réessayez plus tard.", chatMessagePlaceholder: "Tapez votre message...", chatNoMessages: "Pas encore de messages.", - chatLeave: "Quitter Chat", - chatInviteCodeLabel: "Entrez le code d'invitation", - chatJoinByInvite: "Rejoindre", chatTitlePlaceholder: "Titre du chat", chatDescriptionPlaceholder: "Description du chat", - chatTagsPlaceholder: "tag1, tag2, tag3", + chatTagsPlaceholder: "Saisissez des tags séparés par des virgules", chatImageLabel: "Sélectionnez un fichier image (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "Code d'Invitation", chatAuthor: "Auteur", - chatCreated: "Créé", chatAddFavorite: "Ajouter aux Favoris", chatRemoveFavorite: "Retirer des Favoris", chatPM: "MP", chatStatusLabel: "Statut", + chatTypeLabel: "Type de chat", chatCategoryLabel: "Catégorie", - chatParticipantsLabel: "Participants", gamesTitle: "Jeux", gamesDescription: "Découvrez et jouez à des jeux.", gamesFilterAll: "TOUS", gamesPlayButton: "JOUER!", - gamesBackToGames: "Retour aux Jeux", modulesGamesLabel: "Jeux", modulesGamesDescription: "Module pour découvrir et jouer à des jeux.", modulesGraphosLabel: "Graphos", @@ -3314,8 +4166,6 @@ module.exports = { gamesArkanoidDesc: "Cassez toutes les briques avec votre raquette et votre balle. Un défi arcade classique.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong classique contre une IA. Le premier à 5 points gagne.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "Course contre la montre! Évitez les obstacles et atteignez l'arrivée à temps.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilotez votre vaisseau dans un champ d'astéroïdes. Détruisez-les avant qu'ils ne vous touchent.", gamesRockPaperScissorsTitle: "Pierre Feuille Ciseaux", @@ -3325,68 +4175,57 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "Lancez une pièce et pariez sur pile ou face. Quelle est votre chance?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "Billard vu de dessus. Cliquez pour viser, maintenez pour charger la puissance. Empochez toutes les boules en un minimum de coups.", + gamesArtilleryTitle: "Artillerie", + gamesArtilleryDesc: "Visez avec votre canon, tenez compte du vent et touchez la cible. 5 manches, le moins de tirs l'emporte.", + gamesLabyrinthTitle: "Labyrinthe", + gamesLabyrinthDesc: "Échappez au labyrinthe avant d'épuiser vos mouvements. Chaque niveau est plus grand et plus difficile.", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "Mangez tous les points, évitez les fantômes. Les fantômes se déplacent en temps réel — ne vous arrêtez pas !", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", + gamesAudioPendulumTitle: "Pendule sonore", + gamesAudioPendulumDesc: "Simulateur de physique chaotique avec synthèse audio en temps réel. Les vitesses angulaires deviennent des fréquences, les pics des coups de percussion. Aucune simulation ne sonne pareil.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Mixez des visuels en direct sur deux platines : choisissez un motif pour chacune, réglez vitesse, couleur et zoom, enchaînez de l'une à l'autre et lancez des flashs en rythme. Tapez le tempo ou laissez le micro mener la pulsation.", + gamesTetrisDesc: "Les blocs qui tombent, un classique. Complétez des lignes pour marquer des points. Combien de temps tiendrez-vous ?", + gamesMinesweeperTitle: "Démineur", + gamesMinesweeperDesc: "La chasse aux mines classique. Déminez le plateau pour monter de niveau — grilles plus grandes, plus de mines, score plus élevé.", + gamesFilterScoring: "SCORES", + gamesHallOfFame: "Panthéon", + gamesHallPlayer: "Joueur", gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesNoScores: "Aucun score pour l'instant.", calendarsTitle: "Calendriers", calendarTitle: "Calendrier", modulesCalendarsLabel: "Calendriers", modulesCalendarsDescription: "Module pour découvrir et gérer les calendriers.", typeCalendar: "CALENDRIERS", - calendarFilterAll: "TOUS", - calendarFilterMine: "LES MIENS", - calendarFilterOpen: "OUVERT", - calendarFilterClosed: "FERMÉ", - calendarFilterRecent: "RÉCENTS", - calendarFilterFavorites: "FAVORIS", calendarCreate: "Créer un calendrier", calendarUpdate: "Mettre à jour", - calendarDelete: "Supprimer", calendarTitleLabel: "Titre", calendarTitlePlaceholder: "Titre du calendrier...", calendarStatusLabel: "Statut", + calendarTypeLabel: "Type de calendrier", calendarStatusOpen: "OUVERT", calendarStatusClosed: "FERMÉ", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "Échéance", calendarTagsLabel: "Étiquettes", - calendarTagsPlaceholder: "tag1, tag2...", + calendarTagsPlaceholder: "Saisissez des tags séparés par des virgules", calendarParticipantsLabel: "Participants", - calendarParticipantsCount: "Participants", - calendarVisitCalendar: "Visiter le calendrier", calendarCreated: "Créé", - calendarAuthor: "Auteur", calendarJoin: "Rejoindre", calendarGenerateInvite: "Générer une invitation", - calendarInviteCodePlaceholder: "Entrez le code d'invitation...", - calendarValidateInvite: "Valider le code", - calendarJoined: "Rejoint", - calendarAddDate: "Ajouter une date", - calendarAddNote: "Ajouter une note", calendarDateLabel: "Date", calendarDatePlaceholder: "Décrivez cette date...", - calendarNoteLabel: "Note", + calendarNoteLabel: "Notes", calendarNotePlaceholder: "Ajouter une note...", calendarFirstDateLabel: "Date", calendarFirstNoteLabel: "Notes", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "Intervalle", + calendarIntervalNone: "Sans répétition", + calendarIntervalWeekly: "Hebdomadaire", + calendarIntervalMonthly: "Mensuel", + calendarIntervalYearly: "Annuel", calendarFormDescription: "Description", calendarNoDates: "Aucune date ajoutée.", calendarNoNotes: "Aucune note.", @@ -3394,8 +4233,8 @@ module.exports = { calendarsDescription: "Découvrez et gérez les calendriers de votre réseau.", calendarMonthPrev: "← Précédent", calendarMonthNext: "Suivant →", - calendarMonthLabel: "Dates", - calendarsShareUrl: "URL de partage", + calendarMonthNames: "Janvier,Février,Mars,Avril,Mai,Juin,Juillet,Août,Septembre,Octobre,Novembre,Décembre", + calendarDayInitials: "L,M,M,J,V,S,D", calendarAllSectionTitle: "Calendriers", calendarRecentSectionTitle: "Calendriers Récents", calendarFavoritesSectionTitle: "Favoris", @@ -3405,11 +4244,10 @@ module.exports = { calendarCreateSectionTitle: "Créer un calendrier", calendarUpdateSectionTitle: "Mettre à jour le calendrier", calendarAddFavorite: "Ajouter aux favoris", - calendarDeleteNote: "Delete", + calendarDeleteNote: "Supprimer", calendarRemoveFavorite: "Retirer des favoris", - calendarSearchPlaceholder: "Rechercher des calendriers...", + calendarSearchPlaceholder: "Rechercher dans Calendrier...", calendarAddEntry: "Ajouter une entrée", - calendarLeave: "Quitter le calendrier", statsCalendar: "Calendriers", statsCalendarDate: "Dates de calendrier", statsCalendarNote: "Notes de calendrier", @@ -3420,6 +4258,146 @@ module.exports = { tribeContentAccessDenied: "Accès Refusé", tribeContentAccessDeniedMsg: "Ce contenu appartient à une tribu. Vous devez en être membre pour y accéder.", tribeViewTribes: "Voir les Tribus", + tribeFilesEmpty: "Pas encore de fichiers.", + filesLabel: "Fichiers", + profileClearnetFilesLabel: "Fichiers", + filesTitle: "Fichiers", + filesDescription: "Explorez et gérez les fichiers de votre réseau.", + fileAllSectionTitle: "Fichiers", + fileMineSectionTitle: "Vos Fichiers", + fileRecentSectionTitle: "Fichiers Récents", + fileTopSectionTitle: "Meilleurs Fichiers", + fileFavoritesSectionTitle: "Favoris", + fileFilterAll: "TOUS", + fileFilterMine: "MES", + fileFilterRecent: "RÉCENTS", + fileFilterTop: "MEILLEURS", + fileFilterFavorites: "FAVORIS", + fileCreateSectionTitle: "Téléverser un Fichier", + fileUpdateSectionTitle: "Mettre à jour le Fichier", + fileCreateButton: "Téléverser un Fichier", + fileUpdateButton: "Mettre à jour", + fileAddFavoriteButton: "Ajouter aux Favoris", + fileRemoveFavoriteButton: "Retirer des Favoris", + fileFileLabel: "Sélectionnez un fichier (max: 50 Mo)", + calendarFilterRecent: "RÉCENTS", + calendarFilterAll: "TOUS", + calendarFilterMine: "À MOI", + calendarFilterFavorites: "FAVORIS", + calendarFilterOpen: "OUVERTS", + calendarFilterClosed: "FERMÉS", + padFilterRecent: "RÉCENTS", + padFilterAll: "TOUS", + padFilterMine: "À MOI", + padFilterOpen: "OUVERTS", + padFilterClosed: "FERMÉS", + chatFilterRecent: "RÉCENTS", + chatFilterAll: "TOUS", + chatFilterMine: "À MOI", + chatFilterFavorites: "FAVORIS", + chatFilterOpen: "OUVERTS", + chatFilterClosed: "FERMÉS", + licenseLabel: "Licence", + phoneTitle: "Téléphone", + phoneDescription: "Gérez vos appels vidéo chiffrés de bout en bout.", + phoneUnavailable: "Les appels ne sont pas disponibles sur cet appareil.", + phoneCallButton: "Appeler", + profileVisibilityPhone: "Numéro de téléphone", + phoneNumberUnknown: "Personne dans votre réseau n'a le numéro {number}.", + phoneNumberChoose: "Vérifiez qui vous voulez appeler au {number} :", + phoneAnswer: "Répondre", + phoneSpeaker: "Haut-parleur", + phoneEarpiece: "Écouteur", + phoneReject: "Refuser", + phoneHangup: "Raccrocher", + phoneMute: "Couper le micro", + phoneUnmute: "Réactiver le micro", + phoneCameraOn: "Caméra on", + phoneCameraOff: "Caméra off", + settingsPhoneVideo: "Vidéo", + settingsPhoneVideoOn: "Autorisée dans les appels et les salles", + settingsPhoneVideoOff: "Audio seulement", + phoneSilence: "Ne plus entendre", + phoneUnsilence: "Entendre à nouveau", + phoneCalling: "Appel vers", + phoneIsCalling: "vous appelle", + notifyNewPmFrom: "Nouveau MP de", + notifyNewPamFrom: "Nouveau PaM de", + notifyUpdateLabel: "Mise à jour", + updateInstalledLabel: "Version installée", + updateAvailableLabel: "Version disponible", + updateChangesLabel: "Modifications", + updateFilesLabel: "Fichiers", + updateOriginLabel: "Source", + phoneRecording: "Enregistrement du PaM pour", + phoneLeaveMessage: "Personne ne répond : laissez votre message après le bip.", + phoneLeaveMessageBusy: "Cette personne est déjà en ligne : laissez votre message après le bip.", + phoneLeaveMessageUnreachable: "Impossible de joindre cette personne : laissez votre message après le bip.", + phoneConnecting: "Connexion avec", + phoneSendPam: "Envoyer le PaM", + phoneCancel: "Annuler", + phoneSearchPlaceholder: "Rechercher dans Téléphone...", + phonePamTitle: "Enregistrements audio", + phoneNew: "Nouveau", + phoneDelete: "Supprimer", + phoneNoPams: "Vous n'avez aucun enregistrement audio.", + phoneHistoryTitle: "Historique", + phoneLatestTitle: "Derniers appels", + phoneCreateButton: "Nouvel appel", + phoneLimitsHint: "Jusqu'à {n} habitants par appel ; à plus d'un, c'est un appel conjoint.", + phoneErrorPub: "Impossible d'appeler un pub.", + phoneJointCall: "Appel conjoint", + phoneWith: "Avec", + phoneResult: "Résultat", + phoneWhen: "Quand", + phoneDuration: "Durée", + phoneOutgoing: "Sortant", + phoneIncoming: "Entrant", + phoneNoHistory: "Aucun journal pour l'instant.", + phoneOutcomeEnded: "Répondu", + phoneOutcomeMissed: "Manqué", + phoneOutcomeRejected: "Refusé", + phoneOutcomeNoanswer: "Pas de réponse", + phoneOutcomeCancelled: "Annulé", + phoneOutcomePam: "PaM envoyé", + phoneOutcomeFailed: "Échoué", + phoneDialAdd: "Ajouter", + phoneDialBack: "Effacer", + phoneDialClear: "Vider", + phoneDialPlaceholder: "Numéro ou Oasis ID", + phoneDialInhabitants: "Habitants", + phoneDialRemove: "Retirer", + phoneOwnNumber: "C'est votre propre numéro.", + phoneSettingsDesc: "Choisissez qui peut vous appeler.", + phoneDnd: "Ne pas déranger", + phoneOpen: "Ouvert", + phoneRecipientRefuses: "L'habitant que vous essayez d'appeler n'accepte pas les appels.", + phoneRefusedNotice: "Ces habitants n'acceptent pas les appels et n'ont pas été appelés :", + fileEmptyError: "Le fichier est vide. Choisissez un autre fichier.", + fileTitleLabel: "Titre", + fileTitlePlaceholder: "Titre", + fileDescriptionLabel: "Description", + fileDescriptionPlaceholder: "Description", + fileTagsLabel: "Étiquettes", + fileTagsPlaceholder: "Saisissez des tags séparés par des virgules", + fileSizeLabel: "Taille", + fileDownloadButton: "TÉLÉCHARGER", + fileDetailsButton: "DÉTAILS", + fileNoFile: "Aucun fichier fichier", + fileSearchPlaceholder: "Rechercher dans Fichiers...", + fileSearchButton: "Rechercher", + fileSortRecent: "Plus récents", + fileSortOldest: "Plus anciens", + fileSortTop: "Plus votés", + fileNoMatch: "Aucun fichier correspondant.", + fileUpdatedAt: "Mis à jour", + typeFile: "FICHIERS", + modulesFilesLabel: "Fichiers", + modulesFilesDescription: "Module pour découvrir et gérer les fichiers.", + tribeSectionFiles: "FICHIERS", + fileDescription: "Description", + fileDownload: "Télécharger", + fileTypeLabel: "Type", torrentsTitle: "Torrents", torrentsDescription: "Explorez et gérez les torrents de votre réseau.", torrentAllSectionTitle: "Torrents", @@ -3445,49 +4423,93 @@ module.exports = { torrentDescriptionLabel: "Description", torrentDescriptionPlaceholder: "Description", torrentTagsLabel: "Étiquettes", - torrentTagsPlaceholder: "Entrez des étiquettes séparées par des virgules", + torrentTagsPlaceholder: "Saisissez des tags séparés par des virgules", torrentSizeLabel: "Taille", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "Générer un torrent", + torrentFromContentMissing: "Le fichier n'est pas encore disponible sur ce nœud. Réessayez plus tard.", + torrentDownloads: "Téléchargements", + torrentFetchOasis: "Télécharger dans Oasis", + torrentSaveFile: "Enregistrer le fichier", + torrentSeeds: "sources", + seedsLabel: "Sources", + seedAction: "Soutenir", + torrentSourceFile: "Fichier", + torrentStateQueued: "En attente", + torrentStateProbing: "Recherche de sources", + torrentStateDownloading: "Téléchargement", + torrentStateDone: "Terminé", + torrentStateFailed: "Échec", + torrentStateNoSource: "Aucune source connectée, en attente", + torrentStateCancelled: "Annulé", + torrentNoDownloads: "Aucun téléchargement pour l'instant.", + torrentOriginalSection: "Original", + torrentCancel: "Annuler", + torrentDownloadButton: "TÉLÉCHARGER", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "DÉTAILS", torrentNoFile: "Aucun fichier torrent", noTorrents: "Aucun torrent trouvé.", - torrentSearchPlaceholder: "Rechercher titre, étiquettes, auteur...", + noFiles: "Aucun fichier trouvé.", + torrentSearchPlaceholder: "Rechercher dans Torrents...", torrentSearchButton: "Rechercher", torrentSortRecent: "Plus récents", torrentSortOldest: "Plus anciens", torrentSortTop: "Plus votés", torrentNoMatch: "Aucun torrent correspondant.", - torrentMessageAuthorButton: "Envoyer un Message à l'Auteur", torrentUpdatedAt: "Mis à jour", statsTorrent: "Torrents", + statsFile: "Fichiers", typeTorrent: "TORRENTS", modulesTorrentsLabel: "Torrents", modulesTorrentsDescription: "Module pour découvrir et gérer les torrents.", favoritesFilterTorrents: "TORRENTS", + favoritesFilterFiles: "FICHIERS", favoritesFilterMarket: "MARCHÉ", + favoritesFilterEvents: "ÉVÉNEMENTS", + favoritesFilterForum: "FORUM", + favoritesFilterHousing: "LOGEMENT", + favoritesFilterJobs: "EMPLOIS", + favoritesFilterProjects: "PROJETS", + favoritesFilterReports: "RAPPORTS", + favoritesFilterTasks: "TÂCHES", + favoritesFilterTransfers: "TRANSFERTS", + favoritesFilterVotes: "VOTES", favoritesFilterShopProducts: "ARTICLES BOUTIQUE", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Téléverser un Torrent", - tribeMediaTypeTorrent: "Torrent", + tribeCreateFile: "Envoyer un fichier", settingsWishTitle: "Souhait", - settingsWishDesc: "Configurez le souhait de votre Avatar.", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "Détermine comment vous accédez à tout le contenu et votre expérience dans le réseau Oasis.", + settingsWishWhole: "Multivers", + settingsWishMutuals: "Soutien mutuel uniquement", + settingsWishOnlyLan: "LAN uniquement", + settingsWishLocal: "Local", settingsPmVisibilityTitle: "Messages Privés", settingsPmVisibilityDesc: "Configurez votre niveau d'exposition aux messages privés.", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "Multivers", + settingsPmVisibilityMutuals: "Soutien mutuel uniquement", + mute: "Mettre en sourdine", + unmute: "Réactiver", + relationshipMuted: "En sourdine pour vous", + hideContent: "Masquer pour moi", + unhideContent: "Afficher à nouveau", + hiddenByYou: "Masqué par vous", + settingsTabContent: "Contenu", + settingsHiddenTitle: "Contenu masqué", + settingsHiddenDesc: "Habitants mis en sourdine et contenus masqués.", + settingsMutedLabel: "Habitants en sourdine", + settingsHiddenLabel: "Contenu masqué", + settingsMutedEmpty: "Personne en sourdine.", + settingsHiddenEmpty: "Rien de masqué.", + settingsPmVisibilityFollowing: "Seulement les personnes que je suis", + settingsMentionsVisibilityTitle: "Mentions", + settingsMentionsVisibilityDesc: "Choisissez de qui vous recevez des mentions.", pmMutualNotice: "L'envoi est un garde-fou UX. La réception est un filtre d'attention.", - pmBlockedNonMutual: "Vous ne pouvez envoyer des MP qu'aux habitants en soutien mutuel.", inhabitantsPendingFollowsTitle: "Demandes de soutien en attente", inhabitantsPendingAccept: "Accepter", inhabitantsPendingReject: "Refuser", - bxEncrypted: "CHIFFRÉ", - bxEncryptedHexLabel: "Texte chiffré (aperçu)", tribeSectionGovernance: "GOUVERNANCE", - tribeSubStatusPublic: "PUBLIQUE", - tribeSubStatusPrivate: "PRIVÉE", - tribeSubInheritedPrivate: "PRIVÉE (héritée de la tribu principale)", tribePadInviteRequired: "Vous n'avez pas accès au pad. Demandez une invitation.", tribeChatInviteRequired: "Vous n'avez pas accès au chat. Demandez une invitation.", tribeGovernanceDesc: "Gouvernance interne de cette tribu.", @@ -3526,138 +4548,126 @@ module.exports = { tribeGovNoRevocations: "Aucune révocation pour l’instant", tribeGovNoHistorical: "Aucun enregistrement pour l’instant", logsTitle: "Journal", - logsDescription: "Enregistrez votre expérience sur le réseau.", - logsReadMore: "Lire la suite", + logsDescription: "Tenez un journal privé de vos expériences.", logsViewTitle: "Journal", logsColumnType: "Type", logsView: "Voir", - logsManualPrompt: "Écrivez votre journal", logsUpdateButton: "Mettre à jour", - logsEditTitle: "Éditer une entrée", - logsCreateDescription: "Enregistrez votre expérience...", + logsEditTitle: "Mettre à jour le journal", logsCreateTitle: "Créer une entrée", logsWriteButton: "Écrire", logsTextPlaceholder: "Décrivez vos expériences...", - logsTextField: "Texte", - logsLabelPlaceholder: "Libellé...", - logsLabelField: "Écrivez votre journal (libellé)", - logsAiDisabledWarn: "Activez le module IA dans /modules pour utiliser les entrées écrites par l'IA.", - logsAiContextValue: "Jour de blockchain", - logsAiContext: "Contexte", - logsAiModStatus: "AImod", - logsModeApply: "Appliquer !", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "Assistant IA", logsModeManual: "Manuel", logsModeAI: "IA", - logsColumnDelete: "Supprimer", - logsColumnEdit: "Éditer", logsColumnLog: "Journal", logsColumnDate: "Date", logsDelete: "Supprimer", - logsEdit: "Éditer", + logsEdit: "Mettre à jour", logsFilterAlways: "TOUJOURS", logsFilterYear: "ANNÉE", logsFilterMonth: "MOIS", logsFilterWeek: "SEMAINE", logsFilterToday: "AUJOURD'HUI", - logsFilterRecent: "RÉCENTS", - logsFilterAll: "TOUS", + logsComments: "Commentaires", + logsAuthorEntries: "Entrées", logsCreate: "Créer une entrée", logsExport: "Exporter le journal", - logsExportOne: "Exporter", + logsExportOne: "Exporter le journal", logsViewDetails: "Voir les détails", logsGenerateButton: "Générer le texte", - logsSearchText: "Chercher dans les journaux...", + logsSearchText: "Rechercher dans Journal...", logsSearchAnyType: "Tout type", logsSearchButton: "Chercher", logsEmpty: "Aucune entrée pour l'instant.", + logsNothingNew: "Il n'y a aucune nouvelle action à consigner.", modulesLogsLabel: "Journal", - modulesLogsDescription: "Module pour enregistrer (via un assistant IA) vos expériences.", + modulesLogsDescription: "Module pour tenir un journal privé de vos expériences.", statsLogsTitle: "Journal", - statsLogsEntries: "Entrées", typeLog: "JOURNAL", - blockchainCycle: "Cycle", larpTitle: "L.A.R.P.", larpDescription: "Une couche de jeu de rôle grandeur nature pour l'expérimentation collaborative.", + larpNoHousesMatch: "Aucune maison ne correspond à votre recherche.", + larpSearchPlaceholder: "Rechercher dans L.A.R.P....", larpAllHouses: "Maisons :", larpFilterRuling: "RÈGNE", larpFilterHouses: "MAISONS", larpFilterRules: "FAQ", larpRulesTitle: "FAQ du L.A.R.P.", - larpRulesEntryTitle: "Maison de départ", larpRulesEntryText: "Chaque habitant commence dans ACADEMIA par défaut. Depuis ACADEMIA, choisis une maison dans le panneau \"Rejoindre une maison\" : cela ouvre son test d'entrée. Réponds aux 10 questions et soumets. Si tu dépasses le seuil (7/10), tu es admis automatiquement dans cette maison ; sinon, tu es rejeté et tu restes dans ACADEMIA. Dans tous les cas, le système enregistre ton OasisID et le cycle de la tentative et empêche un nouvel essai jusqu'à la fin du délai de 30 cycles.", - larpRulesLeaveText: "Les membres de toute maison peuvent retourner à ACADEMIA à tout moment depuis leur page de maison ; cela réinitialise leur appartenance mais ne lève pas le délai du test.", - larpRulesGovernanceTitle: "Mur de gouvernance", larpRulesGovernanceText: "Pendant son cycle, la maison régnante arbore l'insigne \"Règne\" et est la seule dont le Mur est exposé publiquement sous l'onglet RÈGNE.", - larpRulesSignTitle: "Emblème L.A.R.P.", + larpRulesWallText: "Chaque maison a un Mur où écrivent ses membres. Le Mur d'une maison est privé, sauf celui de la maison au pouvoir et celui de l'ACADEMIA, que tout le monde peut lire.", larpRulesSignText: "Les habitants peuvent l'activer (dans /profile/edit > Capteurs) pour afficher l'image de leur maison actuelle comme un sceau sur leur profil, leur fiche d'auteur et d'habitant. Le sceau renvoie à la page de la maison.", larpPostsTitle: "Mur", larpPostsEmpty: "Aucune publication pour le moment.", - larpPostLabel: "Nouvelle publication", larpPostPlaceholder: "Qu'a cette maison à dire ?", - larpPostPublic: "Public (visible par tous, sinon réservé aux membres)", larpPostSubmit: "Publier", - larpPostMembersOnly: "Membres seulement", larpCycleLabel: "Cycle :", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", + bankRulesArchTaxNote: "La taxe ARCH reflète le coût à long terme de la croissance et de la maintenance de l'archive du réseau : plus vos données vivent depuis longtemps dans le réseau, plus votre part du coût de maintenance est importante.", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", + bankRulesChipNote: "La pastille de chaque élément est colorée par rapport au plus grand message jamais observé sur le réseau et atténuée par le nombre d'habitants actifs — un réseau plus grand répartit la charge et fait passer davantage d'éléments dans la bande verte.", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", + bankRulesEcoTaxNote: "La taxe ECO est déduite de la RBU brute lors du calcul de l'allocation de l'époque. Les ECO déduits ne sont pas redistribués pendant cette époque — ils restent dans le solde du pub et alimentent le fonds de l'époque suivante via les plafonds reserveMin / alpha.", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", + bankRulesKarmaNote: "Le karma est calculé à partir de vos actions (publications, votes, participation au parlement/aux tribunaux, etc.) avec un facteur de décroissance temporelle, moins les grammes de carbone générés par votre fil. Ce que reçoit votre contenu compte aussi : les avis, votes, réactions et écoutes positifs ajoutent du karma, et les avis ou votes négatifs en retirent dès que plusieurs habitants sont d'accord.", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", + bankTaxesArchTaxFootprintNote: "La taxe ARCH reflète le coût de la croissance et de la maintenance de l'archive du réseau. Elle est calculée à partir de l'écart de temps entre votre premier bloc publié et le bloc le plus récent du réseau — plus votre historique a vécu longtemps dans l'archive, plus votre part du coût de maintenance est importante.", + bankTaxesArchTaxNoteBold: "réduire directement la taxe ARCH", + bankTaxesArchTaxNoteIntro: "Votre taxe ARCH est déduite de l'excédent que le réseau génère au-delà de votre RBU ; la valeur de base de la RBU est fixée comme un minimum intangible. La taxe n'est jamais déduite du montant fixe qui revient à chaque habitant en tant que RBU. Les fonds déduits alimentent l'algorithme de redistribution des richesses et sont reversés à d'autres habitants via leurs demandes de RBU. Les habitants les plus récents et ceux qui se consacrent spécifiquement aux tâches d'archivage, de réplication et de maintenance auront probablement une tâche dédiée à maintenir l'archive du réseau en bonne santé en votre nom. Ou ils peuvent se consacrer à ", + bankTaxesArchTaxNoteOutro: " grâce à des accords d'élagage, à la déduplication des données redondantes et à une infrastructure de maintenance partagée, et leur contribution continue peut être exigée par le consensus du réseau.", + bankTaxesUserNoteChips: "La pastille Taxe ECO affichée à côté de chaque élément change de couleur (vert / jaune / rouge) selon la taille de l'élément par rapport au plus grand bloc jamais vu sur le réseau, atténuée par le nombre d'habitants actifs — un réseau solarpunk plus grand répartit la charge et fait passer davantage d'éléments dans la bande verte.", + bankTaxesUserNoteOtherParams: "La taxe ECO est actuellement calculée à partir de l'empreinte carbone de chaque bloc, mais elle pourra intégrer d'autres paramètres avec le temps — énergie dépensée pour la réplication, stockage redondant entre pairs, bande passante des blobs, coût de calcul du déchiffrement, empreinte de minage des transactions associées, ou toute autre charge mesurable que le réseau décide de valoriser.", gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", - parliamentRevocationPublish: "Publish Revocation", - reportsAcceptanceCriteriaLabel: "Acceptance criteria", - reportsActualBehaviorPlaceholder: "What actually happens?", - reportsEvidenceLinksPlaceholder: "Paste links, message IDs, screenshots (if applicable), etc.", - reportsUserStoryLabel: "Inhabitant story", - reportsUserStoryPlaceholder: "As a , I want , so that .", - reportsWhatHappenedLabel: "What happened?", - reportsWhatHappenedPlaceholder: "Describe the incident with context and approximate dates.", + gamesNewRecord: "Nouveau record !", + indexingRefreshNote: "Cette page se recharge toutes les 10 secondes.", + indexingMessage: "Oasis synchronise le réseau de nœuds distribué. Veuillez patienter.", + indexingTitle: "Synchronisation", + padsNoItems: "Aucun pad trouvé.", + padVersionHistory: "Historique des versions", + padAttributionView: "Par autrice", + padVersionView: "Voir", + parliamentRevocationPublish: "Publier la révocation", + reportsAcceptanceCriteriaLabel: "Critères d'acceptation", + reportsActualBehaviorPlaceholder: "Que se passe-t-il réellement ?", + reportsEvidenceLinksPlaceholder: "Collez des liens, des identifiants de messages, des captures d'écran (le cas échéant), etc.", + reportsUserStoryLabel: "Récit d'habitant", + reportsUserStoryPlaceholder: "En tant que , je veux , afin de .", + reportsWhatHappenedLabel: "Que s'est-il passé ?", + reportsWhatHappenedPlaceholder: "Décrivez l'incident avec le contexte et des dates approximatives.", statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", + statsChatMessage: "Messages de chat", + statsGameScore: "Jeux", + statsWiki: "Pages wiki", statsPad: "Pads", - statsPadEntry: "Pad entries", - typeGameScore: "GAME SCORES", + statsPadEntry: "Entrées de pad", + typeGameScore: "SCORES DE JEU", bankTaxesLookupNote: "Entre un ID de bloc pour le rechercher dans ton flux local.", bankTaxesUserNoteChipsClick: "Clique sur n'importe quel chip pour inspecter son bloc dans le blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "Code d'invitation", larpRulesInviteEntryText: "Les membres des maisons peuvent générer des codes d'invitation qui donnent un accès direct à la maison.", larpRulesOneHouseText: "Tu ne peux appartenir qu'à une seule maison à la fois. Si tu n'appartiens à aucune, tu es dans ACADEMIA. Chaque page de maison (sauf ACADEMIA) affiche un bouton « Quitter la Maison » à ses membres qui remet l'appartenance à ACADEMIA. Quitter n'annule PAS le délai du test.", - larpRulesOneHouseTitle: "Une maison à la fois", - larpRulesTestEntry: "Test WILL", - larpRulesTestEntryText: "Chaque option pondère une ou plusieurs maisons ; lors de l'envoi, le système additionne les scores et t'admet automatiquement dans la maison ayant le score le plus élevé.", + larpRulesTestEntryText: "Dans le test WILL, chaque option pondère une ou plusieurs maisons ; à l'envoi, le système additionne les scores et vous admet automatiquement dans la maison la mieux notée.", larpWillTestHint: "Une fois terminé, on t'assignera la maison qui correspond le mieux à tes réponses. Tes réponses individuelles sont traitées localement et ne sont jamais stockées — seule l'attribution de maison résultante est publiée dans ton flux.", larpWillTestTitle: "Test WILL", - settingsLanDesc: "Annoncer périodiquement ce pair aux autres instances d'Oasis sur le même réseau local. Désactiver pour arrêter les diffusions UDP.", + settingsLanDesc: "Annoncer périodiquement ce pair aux autres instances d'Oasis sur le même réseau local.", settingsLanEnable: "Activer la diffusion LAN", settingsLanTitle: "Diffusion LAN", settingsReplicationTitle: "Réplication", settingsReplicationDesc: "Configure le nombre de sauts que ton pair suit à partir de ton propre flux lors de la réplication du contenu.", settingsReplicationHopsLabel: "Sauts", + settingsBlobCacheTitle: "Cache des médias", + settingsBlobCacheDesc: "Le contenu des autres habitants est conservé sur cet appareil. Choisissez l'espace qu'il peut occuper.", + settingsBlobCacheUsage: "Utilisation actuelle", + settingsBlobCacheFiles: "fichiers", + settingsBlobCacheLimit: "Limite", + settingsBlobCacheUnlimited: "Illimité", + settingsBlobCacheCleanNow: "Nettoyer maintenant", + settingsBlobCacheCleaned: "{n} fichiers supprimés, {size} libérés.", aiApproveTagsLabel: "Étiquettes (séparées par virgules)", aiApproveTagsPlaceholder: "ex. oasis, gouvernance, écologie", aiApproveRatingLabel: "Note", @@ -3668,36 +4678,28 @@ module.exports = { aiExchangeMarkHelpful: "+1 utile", larpCyclesUntilRuling: "Prochain cycle de gouvernance", larpVisitTribe: "Visiter la tribu", - larpGoverning: "Règne :", + larpStartJourney: "Commencer le voyage", + larpGoverning: "Gouverne :", + larpStartHere: "Commencez ici :", larpMyHouse: "Ma Maison :", larpMembersCount: "Membres", - larpMembersTitle: "Membres", - larpNoMembers: "Aucun membre pour le moment.", larpRolesLabel: "Rôles", larpFunctionLabel: "Fonction", larpMonthLabel: "Cycle de gouvernance", larpLeaveToAcademia: "Retour à ACADEMIA", - larpAcademiaJoinTitle: "Rejoindre une maison", - larpAcademiaJoinHint: "Passe le test psychologique de profil pour être affecté à la maison qui te correspond le mieux, ou utilise un code d'invitation partagé par un membre d'une maison.", - larpTakeTestButton: "Passer le test de profil", + larpAcademiaJoinTitle: "Maisons L.A.R.P.", larpInviteRedeem: "Rejoindre la Maison", larpInviteCreate: "Générer un code d'invitation", larpInvitePlaceholder: "Code d'invitation", larpInviteBannerTitle: "Nouveau code d'invitation", larpInviteBannerHint: "Partage ce code avec une personne en ACADEMIA. Il expire dans 30 cycles ou après une utilisation.", - larpTestAssignedHouse: "Maison attribuée", larpTestRankingTitle: "Score par maison", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "Code d'invitation de maison", - invitesHouseJoinButton: "Rejoindre la Maison", - ecoTaxLabel: "ECO Tax", - ecoinTaxLabel: "ECOin Tax", + ecoTaxLabel: "Taxe ECO", bankTaxes: "Impôts", - bankOverviewYourTaxes: "Tes impôts", - bankTaxesNetworkTitle: "Impôts du réseau", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesUserTitle: "Tes impôts", + bankOverviewYourTaxes: "Total des impôts", + bankTaxesEcoTaxTitle: "Taxe ECO", + bankTaxesArchTaxTitle: "Taxe ARCH", bankTaxesUserAmount: "Ton ECO Tax (prix à rendre)", bankTaxesArchTaxUserAmount: "Ton ARCH Tax (prix à rendre)", bankTaxesArchTaxFirstBlock: "Âge de ton premier bloc", @@ -3725,7 +4727,7 @@ module.exports = { bankTaxesLookupType: "Type", bankTaxesLookupSize: "Taille", bankTaxesLookupCarbon: "Empreinte carbone", - bankTaxesLookupEcoin: "ECO Tax", + bankTaxesLookupEcoin: "Taxe ECO", bankExchangeEcoTaxAnnual: "ECOin Taxes (annuel)", bankExchangeEcoTaxMonthly: "ECOin Taxes (mensuel)", bankTaxesTypesLabel: "Sélectionne les impôts que tu veux payer", @@ -3733,14 +4735,14 @@ module.exports = { bankRulesPoolTitle: "Pool mensuel", bankRulesShareTitle: "Part par utilisateur", bankRulesKarmaTitle: "Karma (score d'engagement)", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesArchTaxTitle: "ARCH Tax", + bankRulesEcoTaxTitle: "Taxe ECO", + bankRulesArchTaxTitle: "Taxe ARCH", bankRulesChipTitle: "Bande de couleur du chip ECO Tax", bankRulesEpochKind: "Granularité d'epoch", bankRulesAlpha: "Alpha (part max. du solde du pub par epoch)", bankRulesReserveMin: "Réserve minimale (conservée dans le solde du pub)", bankRulesCapPerEpoch: "Plafond par epoch (absolu)", - bankRulesGraceDays: "Délai de claim", + bankRulesGraceDays: "Délai de grâce de la réclamation", bankRulesWMin: "Poids minimum (w_min)", bankRulesWMax: "Poids maximum (w_max)", bankRulesFloor: "Plancher par utilisateur (brut)", @@ -3748,29 +4750,22 @@ module.exports = { bankRulesEcoTaxRate: "Taux", bankRulesArchTaxRate: "Taux", bankRulesCarbonFactor: "Facteur carbone", - statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Aucun tombstone dans le réseau pour le moment.", blockchainBlockNotAvailable: "Ce bloc n'est pas disponible dans ton feed local. Il appartient peut-être à un peer dont tu ne réplique pas encore les données.", blockchainBackToBlockexplorer: "Retour au blockexplorer", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeTitle: "BCS Transcode", - audioTranscodeDetailTitle: "Transcode", + audioTranscodeButton: "TRANSCODER", + audioTranscodeDetailTitle: "Transcoder", audioTranscodeDetailDescription: "Décode la charge utile incrustée et la carte de composition d'origine de la blockchain.", - audioTranscodeStegoTitle: "Incrusté dans la forme d'onde", - audioTranscodeStegoOasisId: "Par", audioTranscodeStegoTimestamp: "Généré le", - audioTranscodeStegoMessage: "TEXT", + audioTranscodeStegoMessage: "TEXTE", audioTranscodeStegoEmpty: "(aucun)", audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "Aucune charge stéganographique n'a pu être décodée depuis cet audio.", audioTranscodeCompositionEmpty: "Cet audio ne contient pas de composition blockchain enregistrée.", - audioBackToAudio: "Retour à l'audio", audioBackToBcs: "Retour à BCS", - audioAuthorLabel: "Auteur", melodyCompositionTitle: "Carte de composition blockchain", melodyFilterMine: "MIEN", - melodyByLabel: "Par", melodyAllEmpty: "Pas encore de compositions BCS d'autres habitants.", melodyNoteUnavailable: "le bloc n'est pas dans ton feed local", melodyUploadBcsTitle: "Publier", @@ -3781,7 +4776,6 @@ module.exports = { melodyUploadBcsNameNote: "L'audio sera publié sous le nom auto-généré", larpLastAttemptTitle: "Ta dernière tentative", larpLastAttemptHouse: "Maison", - larpLastAttemptResult: "Résultat", larpLastAttemptWhen: "Quand", larpLastAttemptCooldown: "Prochaine tentative dans", larpTestTitle: "Test d'entrée", @@ -3790,29 +4784,14 @@ module.exports = { larpTestCooldownActive: "Prochain test disponible dans", larpTestCooldownDays: "cycles", larpTestResultTitle: "Résultat du test", - larpTestPassed: "Test réussi — bienvenue !", - larpTestFailed: "Test non réussi", larpTestScore: "Score", larpTestNextAttempt: "Tu pourras tenter un autre test dans 30 cycles.", larpGoToHouse: "Aller à ta maison", larpBackToAcademia: "Retour à ACADEMIA", - larpBackToHouses: "◀ Maisons", larpBadgeYou: "Toi", larpBadgeRuling: "Règne", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Module pour la couche de jeu de rôle grandeur nature d'Oasis (les 9 Maisons).", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "Module pour la couche de jeu de rôle grandeur nature.", larpProfileQ1: "Face à un problème complexe, que fais-tu en premier ?", larpProfileQ1O1: "Parler aux autres pour trouver un consensus", larpProfileQ1O2: "Construire un prototype à tester", @@ -3886,7 +4865,52 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "Choisis le mode de navigation UX que tu veux pour ton interface.", uxModeMenus: "Blocs", - uxModeAINav: "AI", + uxModeChats: "Conversations", + uxModeFeed: "Microblogging", + feedTrendingTitle: "Tags Tendance", + feedActiveTitle: "Habitants Actifs", + cnBrandSub: "Libre · P2P · Fédéré", + cnSyncedPeers: "Pairs synchronisés", + cnLicense: "Licence", + cnNotAccessible: "Le contenu n'est pas accessible pour le moment.", + cnUntitled: "Sans titre", + cnSearchPlaceholder: "Rechercher…", + cnHubTitle: "HUB Clearnet", + cnHubDescription: "Contenu public partagé par les habitants de ce réseau Oasis.", + cnHubEmpty: "Aucun contenu public n'a encore été partagé sur la Clearnet.", + cnPublicContent: "Contenu", + cnTribesContent: "Tribus", + cnNetworkContent: "Réseau", + cnCategoryEmpty: "Aucun contenu dans cette catégorie.", + cnInhabitantEmpty: "Cet habitant n'a pas encore publié de contenu sur la Clearnet.", + cnPrevPage: "← Précédent", + cnNextPage: "Suivant →", + cnPageOf: "Page {page} sur {pages}", + cnMoreInfo: "Plus d'infos →", + cnOpenCourse: "Cours ouvert", + cnAuction: "ENCHÈRE", + cnExchange: "ÉCHANGE", + cnRssDescription: "publiés par les habitants de ce HUB", + cnKindAudio: "Audio", + cnKindBlog: "Blog", + cnKindBookmark: "Marque-page", + cnKindDocument: "Document", + cnKindEvent: "Événement", + cnKindFeed: "Fil", + cnKindImage: "Image", + cnKindJob: "Emploi", + cnKindMarket: "Marché", + cnKindPodcast: "Podcast", + cnKindProject: "Projet", + cnKindCourse: "Cours", + cnKindShop: "Boutique", + cnKindTorrent: "Torrent", + cnKindFile: "Fichier", + cnKindVideo: "Vidéo", + cnKindWiki: "Wiki", + cnKindRoom: "Salon", + cnKindMap: "Carte", + uxModeAINav: "IA", } }; diff --git a/src/client/assets/translations/oasis_gl.js b/src/client/assets/translations/oasis_gl.js new file mode 100644 index 0000000..1b55db8 --- /dev/null +++ b/src/client/assets/translations/oasis_gl.js @@ -0,0 +1,4917 @@ +const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); +module.exports = { + gl: { + shopOrderStatusPaid: "Pagamento recibido", + shopOrderStatusReceived: "Recibido", + shopOrderMarkPaid: "Marcar pagamento recibido", + shopOrderConfirmReceived: "Confirmar recibido", + shopMyOrdersTitle: "Os meus pedidos", + shopPurchasesButton: "Compras", + shopMyOrdersDescription: "Os teus pedidos de compra.", + shopMyOrdersEmpty: "Aínda non tes compras.", + shopOrderPmSeller: "PM Vendedor", + shopOrderSeller: "Vendedor", + shopOrderInvoice: "Factura", + shopOrderInvoiceConcept: "Factura", + shopOrderStatusPending: "Pendente", + shopOrderStatusAccepted: "Aceptado", + shopOrderStatusRejected: "Rexeitado", + shopOrderStatusShipped: "Enviado", + shopOrderAccept: "Aceptar", + shopOrderReject: "Rexeitar", + shopOrderMarkShipped: "Marcar enviado", + shopOrderPmBuyer: "PM Comprador", + shopOrderContractConcept: "Pedido de tenda", + shopOrdersPending: "Pedidos pendentes", + all: "Todos", + mine: "Meus", + recent: "Recentes", + anonymous: "Anónimo", + unnamed: "Anónimo", + authorLabel: "Autoría", + delete: "Eliminar", + save: "Gardar", + vote: "Votar", + url: "URL", + price: "Prezo", + priority: "Prioridade", + severity: "Gravidade", + organizer: "Organizador", + edited: "editado", + isPublic: "Público", + searchIsPublicLabel: "Público", + privacyPrivate: "PRIVADO", + privacyPublic: "PÚBLICO", + notFound: "Non atopado", + no_results: "Sen datos", + searchButton: "Buscar", + mapZoomLabel: "Zoom", + modulesTitle: "Módulos", + viewJson: "Ver JSON", + matchScore: "Puntuación de coincidencia", + preferencesLabel: "Preferencias", + inhabitantProfileTitle: "Habitante", + contentWarningLabel: "Aviso de contido", + invalidPost: "Contido non válido", + invalidPostHint: "Esta mensaxe ten o texto baleiro ou non válido.", + spreadChron: "Réplicas", + spreaded: "Replicado", + spreadContentUnavailable: "Contido aínda non dispoñible (replicación pendente)", + filteredByTag: "Filtrado por etiqueta", + filteredByTagTitle: "Filtrado por etiqueta", + feedPublishedSuccess: "Publicación creada correctamente!", + feedMastodonFailed: "Publicado en Oasis, pero non se puido enviar a Mastodon.", + tribeFeedSent: "Mensaxe enviada correctamente!", + tribeContentEncrypted: "Contido cifrado", + tribeTorrentsEmpty: "Aínda non hai torrents.", + torrentDescription: "Descrición", + torrentDownload: "Descargar", + chatInviteMode: "Convidar", + padInviteMode: "Convidar", + noInviteMode: "Sen modo de convite", + noDeadline: "Sen data límite", + noStatus: "Sen estado", + noSeverity: "Sen gravidade", + calendarDeleteDate: "Eliminar data", + calendarIntervalUntil: "Ata", + bookmarkLastVisit: "Última visita", + profileClearnetBookmarksLabel: "Marcadores", + invitesPort: "Porto", + peerHostValidation: "IPv4 válida (p. ex. 192.168.1.100) ou nome de host (p. ex. pub.solarnethub.com)", + peerPortValidation: "Porto 1-65535", + peerKeyValidation: "Chave pública SSB ed25519 (@<44 caracteres base64>=.ed25519)", + inboxJobUnsubscribedTitle: "Baixa da túa oferta de emprego", + inboxProjectFollowedTitle: "Novo seguidor do teu proxecto", + inboxProjectUnfollowedTitle: "Deixou de seguir o teu proxecto", + pmHasFollowedYourProject: "seguiu o teu proxecto", + pmHasUnfollowedYourProject: "deixou de seguir o teu proxecto", + pmHasUnsubscribedFromYourJobOffer: "deuse de baixa da túa oferta de emprego", + marketDeleteButton: "Eliminar", + marketNoBids: "Aínda non se fixeron poxas.", + marketAuctionBidAmount: "Importe da poxa", + marketAuctionBidTime: "Hora da poxa", + marketAuctionUser: "Poxador", + parliamentActorInPower: "TRIBO GOBERNANTE", + parliamentWinningCandidature: "Candidatura gañadora", + projectBounty: "Recompensa", + projectFollowing: "SEGUINDO", + projectBackerAuthor: "Patrocinador", + projectBackerDate: "Data", + projectConfirmTransferButton: "Confirmar transferencia", + projectPendingTransfersTitle: "Transferencias pendentes", + transfersUpdateSectionTitle: "Actualizar transferencia", + taskUnassignedFrom: "Desasignado de", + taskDescriptionPlaceholder: "Descrición da tarefa", + exportPasswordLabel: "Contrasinal de exportación", + importPasswordPlaceholder: "Introduce o contrasinal", + searchCasesPlaceholder: "Buscar en Tribunais...", + shopBuyDeliveryAddressPlaceholder: "Enderezo de entrega", + shopBuyNotesPlaceholder: "Notas", + bankTaxesUserNoteBold: "reducir directamente o imposto ECO", + bankTaxesUserNoteIntro: "O teu imposto ECO dedúcese do excedente que a rede xera por riba da túa RBU; o valor base da RBU fíxase como mínimo inamovible. O imposto nunca se deduce da cantidade fixa que lle corresponde a cada habitante como RBU. Os fondos deducidos alimentan o algoritmo de redistribución da riqueza e canalízanse de volta cara a outros habitantes mediante as súas reclamacións de RBU. Eses outros habitantes que reciben máis RBU para os seus proxectos probablemente terán algunha tarefa dedicada a axudarte a reducir o teu imposto ECO. Ou poden dedicarse a ", + bankTaxesUserNoteOutro: ", e o consenso da rede pode requirir a súa contribución continua.", + courtsViewDetails: "Ver", + courtsViewDetailsShort: "Detalles", + courtsThDetails: "Detalles", + courtsDetailsAnswersTitle: "Respostas", + courtsDetailsEvidenceTitle: "Probas", + courtsDetailsSettlementsTitle: "Acordos", + courtsDetailsVerdictTitle: "Veredicto", + courtsDetailsNotPublic: "Non público", + courtsEvidenceSideAccuser: "Probas da acusación", + courtsEvidenceSideRespondent: "Probas da defensa", + courtsEvidenceSideUnknown: "Descoñecido", + courtsJudgeNotAssigned: "-", + courtsSupportCase: "Apoiar o caso", + courtsSettlementAcceptBtn: "Aceptar acordo", + courtsSupportCount: "Apoios", + reportsSetStatus: "Cambiar estado", + reportsStatusClosed: "PECHADO", + statsAvgPerInhabitant: "Media por habitante", + statsCarbonMaxAnnual: "Estimación máx. anual", + statsCarbonNetwork: "Total da rede", + statsCarbonOfEstMax: "da capacidade máx. estimada", + statsCarbonYearProgress: "do ano transcorrido", + statsNetworkTitle: "Rede", + statsStorageTitle: "Almacenamento", + statsEcoTaxTitle: "Taxa ECO", + statsAccountTitle: "Conta", + statsAveragesTitle: "Medias", + statsCarbonOfNetwork: "do total da rede", + statsCarbonUser: "A túa pegada", + statsContentCountColumn: "Cantidade", + statsContentTypeColumn: "Tipo", + statsMsgsPerDay: "Mensaxes/día (histórico)", + statsMyShare: "A túa parte da rede", + statsNetworkSpan: "Antigüidade da rede", + statsTombstoneRatioLabel: "Proporción de borrados", + statsTopTagsTitle: "Etiquetas principais", + statsTopTypesTitle: "Tipos de contido principais", + statsTotalMsgs: "Mensaxes totais", + extended: "Multiverso", + extendedDescription: [ + "Cando apoias a alguén, podes descargar as publicacións dos habitantes que apoia, e esas publicacións aparecerán aquí, ordenadas pola máis recente.", + ], + popular: "Destacadas", + popularDescription: [ + "Publicacións de habitantes da túa rede, ", + strong("ordenadas por apoios"), + ". Selecciona o período de tempo para obter unha lista.", + ], + week: "Semana", + month: "Mes", + year: "Ano", + latest: "Últimos", + latestDescription: [ + strong("Publicacións"), + " túas e de habitantes que apoias, ordenadas polas máis recentes.", + ], + topics: "Temas", + topicsDescription: [ + strong("Temas"), + " teus e de habitantes que apoias, ordenados polos máis recentes.", + ], + summaries: "Resumos", + summariesDescription: [ + strong("Temas e algúns comentarios"), + " teus e de habitantes que apoias, ordenados polos máis recentes.", + ], + threads: "Fíos", + threadsDescription: [ + strong("Publicacións que teñen comentarios"), + " de habitantes que apoias e do teu multiverso, ordenadas polas máis recentes.", + ], + profile: "Avatar", + inhabitants: "Habitantes", + peersReplicatedFeeds: "Replicados", + peersTorColumn: "TOR", + peersTorYes: "Accesible por Tor", + peersTorNo: "Non accesible por Tor", + peersOasisId: "ID", + peersOasisVersion: "Versión", + graphos: "Graphos", + graphosDescription: "Mapa interactivo da rede ao teu redor.", + graphosFederation: "Federación", + graphosFedPubs: "PUBs", + graphosFedUnreachable: "Inalcanzables", + graphosFedLinks: "Ligazóns mutuas", + graphosFedIsolated: "Illados", + graphosFedNoData: "Sen datos", + graphosFedMutual: "Mutuo", + graphosFedOneWay: "Nun sentido", + graphosFedTransport: "Rede", + graphosFedWith: "Federado con", + graphosZoom: "Ampliar", + graphosFedInhabitants: "Habitantes", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "O teu nodo aínda non coñece ningún PUB.", + graphosViewingGraphOf: "Vendo", + graphosBackToMine: "← A miña rede", + graphosShowing: "Amosando", + graphosYou: "Ti", + graphosTotalNodes: "Nodos totais", + mentions: "Mencións", + mentionsDescription: [ + strong("Todas as mensaxes que te @mencionan"), + ".", + ], + noMentions: "Aínda non recibiches @mencións.", + mentionsSearchPlaceholder: "Buscar en Mencións...", + privateReminders: "RECORDATORIOS", + inboxFilterPms: "PMs", + inboxFilterNotifications: "Notificacións", + inboxMarkRead: "Marcar como lido", + inboxMarkUnread: "Marcar como non lido", + inboxUnreadChip: "Non lido", + inboxMarkAllRead: "Marcar todo como lido", + inboxDeleteShown: "Borrar todo", + inboxArchive: "Arquivar", + inboxUnarchive: "Desarquivar", + inboxFilterArchived: "Arquivados", + inboxEarlierMessages: "mensaxes anteriores", + inboxSortThread: "Por fío", + inboxSortRecent: "Máis recentes primeiro", + inboxAllBots: "Bots", + settingsInboxBotsTitle: "Notificacións do Inbox", + settingsInboxBotsDesc: "Escolle que bots poden avisarte no teu Inbox. Os bots desmarcados ocúltanse e os seus avisos non se contan.", + inboxFiltersLabel: "Filtros:", + inboxToggleToMutuals: "Cambiar a mutuos", + inboxToggleToWhole: "Cambiar a todos", + privateDate: "Data", + peers: "Nodos", + searchDescription: "Descrición", + searchFromLabel: "Desde", + searchToLabel: "Ata", + searchQueryLabel: "Busca", + searchPerPageLabel: "Resultados", + settings: "Configuración", + settingsDescription: "Configura o contorno da túa rede.", + continueReading: "Continuar lendo", + moreComments: "máis comentarios", + readThread: "ler o resto do fío", + pixeliaTitle: "Pixelia", + pixeliaDescription: "Debuxa píxeles nunha grella e colabor-ART con habitantes da túa rede.", + melodyTitle: "Melody", + melodyDescription: "Reproduce a melodía da túa blockchain — cada bloque convértese nunha nota.", + melodyEmpty: "Aínda non hai notas — a túa blockchain aínda non xerou ningún bloque.", + melodyTotalBlocks: "Notas", + melodyFilterAll: "TODAS", + melodyRegenerate: "Rexenerar", + melodyDownload: "Descargar BCS", + profileContentSectionTitle: "Contido do Avatar", + profileContentHelp: "Escolle que módulos se amosarán no teu avatar.", + profileSensorsHelp: "Métricas opcionais que se amosan no teu avatar.", + profileClearnetHelp: "Módulos que se poden consultar desde fóra de Oasis.", + profileHubAll: "Todo", + footerLicenseLabel: "Licenza", + profileVisibilityFediverse: "Multiverso", + contributorsTitle: "Contribucións", + colorLabel: "Escolle unha cor", + paintButton: "Debuxar!", + totalPixels: "Total de píxeles", + modules: "Módulos", + modulesViewTitle: "Módulos", + modulesViewDescription: "Define o teu contorno ideal activando e desactivando módulos.", + inbox: "Buzón", + inboxSearchPlaceholder: "Buscar no Buzón...", + multiverse: "Multiverso", + fediverse: "Multiverso", + fediverseDescription: "Xestiona as túas outras contas do multiverso, incluído enviar e recibir contido.", + fediverseStatus: "Estado", + fediverseDisconnected: "Multiverso desconectado. Comproba %LINK% ou o estado da conexión.", + fediverseSettingsTitle: "Multiverso", + fediverseInstanceLabel: "Enderezo", + fediverseTokenLabel: "Token de acceso", + fediverseConnect: "Conectar", + fediverseConnectedAs: "Conectado como", + nextcloudServerLabel: "Servidor", + nextcloudUserLabel: "Usuario", + nextcloudFolderLabel: "Cartafol", + nextcloudUploadButton: "Subir", + nextcloudFileLabel: "Escolle un ficheiro", + fileExternalLink: "Ligazón externa", + nextcloudErrConnect: "Non se puido chegar a Nextcloud con estes datos.", + nextcloudErrUpload: "A subida a Nextcloud fallou.", + nextcloudErrShare: "Nextcloud non deu unha ligazón pública para o ficheiro.", + fediverseDisconnect: "Desconectar", + fediverseEmptyLink: "a túa configuración", + fediverseComposePlaceholder: "Que estás a pensar?", + fediverseReplyPlaceholder: "Escribe a túa resposta…", + fediverseAttach: "Anexar multimedia", + fediversePublish: "Publicar", + fediverseReply: "Responder", + fediverseBoosted: "impulsou", + fediverseNoPosts: "A túa cronoloxía está baleira.", + fediverseTimeline: "Cronoloxías", + fediverseFollowers: "Seguidores", + fediverseFollowing: "Seguindo", + fediversePosts: "Publicacións", + fediverseJoined: "Uniuse", + fediverseRefresh: "Actualizar", + fediverseWrite: "Escribir", + fediversePreview: "Vista previa", + fediverseManage: "Xestionar", + fediverseError: "Non se puido contactar con Mastodon.", + fediverseErrInstance: "URL de instancia non válido.", + fediverseErrAuth: "Token non válido ou caducado.", + fediverseErrConnect: "Non se puido conectar coa instancia.", + fediverseErrToken: "O token é obrigatorio.", + fediverseErrPublic: "Non dispoñible en modo público.", + fediverseErrFetch: "Non se puido cargar a cronoloxía.", + fediverseErrPost: "Non se puido publicar.", + fediverseErrMedia: "Non se puido subir o ficheiro.", + fediverseErrEmpty: "Escribe algo ou anexa un ficheiro.", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Número de teléfono", + telegramSendCode: "Enviar código", + telegramCodeHelp: "Introduce o código que Telegram che acaba de enviar (app ou SMS).", + telegramCodeLabel: "Código", + telegramPasswordHelp: "Esta conta ten verificación en dous pasos. Introduce o teu contrasinal.", + telegramPasswordLabel: "Contrasinal", + telegramVerify: "Verificar", + telegramCancel: "Cancelar", + telegramChats: "Chats", + telegramUnread: "Sen ler", + telegramOpen: "Abrir", + telegramSend: "Enviar", + telegramYou: "Ti", + telegramNoChats: "Aínda non hai chats.", + telegramNoMessages: "Aínda non hai mensaxes.", + telegramComposePlaceholder: "Escribe unha mensaxe…", + telegramChannel: "Canle", + telegramGroup: "Grupo", + telegramPrivate: "Privado", + telegramErrConnect: "Non se puido contactar con Telegram.", + telegramErrTimeout: "Telegram tardou demasiado en responder.", + telegramErrAuth: "Sesión de Telegram non válida ou caducada. Conéctaa de novo.", + telegramErrFetch: "Non se puideron cargar os chats.", + telegramErrSend: "Non se puido enviar a mensaxe.", + telegramErrMissing: "Fan falta o API ID, o API hash e o número de teléfono.", + telegramErrCode: "Código incorrecto. Téntao de novo.", + telegramErrCodeExpired: "O código caducou. Comeza de novo.", + telegramErrPassword: "Contrasinal incorrecto.", + telegramErrPhone: "Número de teléfono non válido.", + telegramErrApi: "API ID ou API hash non válidos.", + telegramErrFlood: "Demasiados intentos. Agarda un anaco e téntao de novo.", + telegramErrNoLogin: "Non hai ningún inicio de sesión en curso. Comeza de novo.", + peertubeUsernameLabel: "Usuaria", + peertubePasswordLabel: "Contrasinal", + peertubeSubscriptions: "Subscricións", + peertubeMyVideos: "Os meus vídeos", + peertubeVideos: "Vídeos", + peertubeFollowers: "Seguidoras", + peertubeViews: "visualizacións", + peertubeLikes: "gústame", + peertubeQuota: "Cota", + peertubeUpload: "Subir vídeo", + peertubeUploadHelp: "Escolle un ficheiro de vídeo. Súbese á túa canle e PeerTube procésao despois.", + peertubeTitleLabel: "Título", + peertubeDescriptionLabel: "Descrición", + peertubePrivacyLabel: "Privacidade", + peertubePrivacyPublic: "Público", + peertubePrivacyUnlisted: "Non listado", + peertubePrivacyPrivate: "Privado", + peertubeNoVideos: "Aínda non hai vídeos.", + peertubeComments: "Comentarios", + peertubeComment: "Comentar", + peertubeCommentPlaceholder: "Escribe un comentario…", + peertubeNoComments: "Aínda non hai comentarios.", + peertubeOpen: "Abrir en PeerTube", + peertubeWatch: "Ver", + peertubeChannel: "Canle", + peertubeNoStream: "Este vídeo aínda non se pode reproducir aquí. Ábreo en PeerTube.", + peertubeUploaded: "Vídeo subido. PeerTube estao a procesar.", + peertubeErrConnect: "Non se puido contactar con PeerTube.", + peertubeErrAuth: "Usuaria ou contrasinal incorrectos, ou a sesión caducou. Conéctaa de novo.", + peertubeErrInstance: "URL de instancia non válida.", + peertubeErrMissing: "Fan falta instancia, usuaria e contrasinal.", + peertubeErrFetch: "Non se puideron cargar os vídeos.", + peertubeErrUpload: "Non se puido subir o vídeo.", + peertubeErrTitle: "Fai falta un título.", + peertubeErrComment: "Non se puido publicar o comentario.", + peertubeErrNoFile: "Escolle un ficheiro de vídeo.", + bookmarksLabel: "Marcadores", + videosLabel: "Vídeos", + torrentsLabel: "Torrents", + docsLabel: "Documentos", + audiosLabel: "Audios", + tagsLabel: "Etiquetas", + imagesLabel: "Imaxes", + inhabitantsLabel: "Habitantes", + trendingLabel: "Tendencias", + eventsLabel: "Eventos", + apply: "Aplicar", + menuPersonal: "Persoal", + menuGovernance: "Gobernanza", + menuOffice: "Oficina", + menuNetwork: "Comunidade", + menuCreative: "Creativo", + menuEconomy: "Economía", + menuMedia: "Biblioteca", + menuTools: "Ferramentas", + comment: "Comentar", + subtopic: "Subtema", + json: "JSON", + unfollow: "Deixar de apoiar", + follow: "Apoiar", + block: "Bloquear", + unblock: "Desbloquear", + relationshipNotFollowing: "Non te apoia", + relationshipTheyFollow: "Apóiate", + relationshipMutuals: "Apoio mutuo", + relationshipFollowing: "Estás apoiando", + relationshipYou: "Ti", + relationshipBlocking: "Estás bloqueando", + relationshipBlockedBy: "Bloquéate", + relationshipMutualBlock: "Bloqueo mutuo", + relationshipNone: "Non estás apoiando", + relationshipBlockingPost: "Post bloqueado", + viewLikes: "Ver réplicas", + viewSupporters: "Ver apoios", + spreadedDescription: "Lista de contidos replicados por habitante.", + preview: "Previsualizar", + publish: "Publicar", + contentWarningPlaceholder: "Engade un título ao teu post (opcional)", + publishWarningPlaceholder: "Escribe o corpo da túa entrada.", + publishTooLong: "A túa publicación é demasiado longa. Acúrtaa, por favor.", + keepTextRetry: "Enviar de novo", + dateInPastError: "A data non pode ser anterior a hoxe", + publishCustomDescription: [ + "LEMBRA: Por mor da tecnoloxía blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pénsao ben!", + ], + commentWarning: [ + "LEMBRA: Por mor da tecnoloxía blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pénsao ben!", + ], + commentLabel: ({ publicOrPrivate, markdownUrl }) => [ + ], + publishLabel: ({ markdownUrl, linkTarget }) => [ + "LEMBRA: Por mor da tecnoloxía blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pénsao ben!", + ], + replyLabel: ({ markdownUrl }) => [ + "LEMBRA: Por mor da tecnoloxía blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pénsao ben!", + ], + publishCustomInfo: ({ href }) => [ + "Se tes experiencia, tamén podes ", + a({ href }, "escribir un post avanzado"), + ".", + ], + publishBasicInfo: ({ href }) => [ + "Se non tes experiencia, deberías ", + a({ href }, "escribir un post normal"), + ".", + ], + publishCustom: "Escribir un post avanzado", + messagePreview: "Vista previa", + mentionsMatching: "Mencións", + mentionsName: "Nome", + mentionsRelationship: "Relación", + updateBannerText: "Hai unha nova versión de Oasis dispoñible.", + updateBannerAction: "Actualizar agora!", + info: "Info", + settingsIntro: ({ version }) => [ + `[SNH] ꖒ OASIS [ v.${version} ]`, + ], + timeAgo: "hai", + sendTime: "escrito ", + theme: "Tema", + backupKeysExportButton: "Exportar", + backupKeysImportButton: "Importar", + ssbLogStream: "Blockchain", + ssbLogStreamDescription: "Configura o límite de mensaxes para os fluxos da blockchain.", + logWindowChip: "Últimos {n} streams da blockchain", + saveSettings: "Gardar a configuración", + exportDescription: "Establece un contrasinal (mínimo 32 caracteres de lonxitude) para cifrar o teu segredo", + exportDataTitle: "Copia instantánea", + exportDataDescription: "Descargar a túa blockchain (chave privada excluída!)", + exportDataButton: "Descargar a blockchain", + importDescription: "Importa o teu segredo cifrado (chave privada) para activar o teu avatar", + passwordImport: "Escribe o teu contrasinal para descifrar os datos que se gardarán no teu sistema (nome: secret)", + exportPasswordPlaceholder: "Usa minúsculas, maiúsculas, números e símbolos", + fileInfo: "O teu segredo cifrado descargarase no teu dispositivo (nome: oasis.enc)", + developer: "Desenvolvemento", + devTitle: "Desenvolvemento", + welcomeBannerText: "O teu avatar aínda non está configurado.", + aiSuggestionBanner: "Suxestión:", + aiSuggestionsEnable: "Suxestións da IA", + welcomeBannerAction: "Comezar", + welcomeTitle: "Benvida", + welcomeStepIdentity: "Identidade", + welcomeStepType: "Tipo", + welcomeStepUse: "Uso", + welcomeStepLook: "Aparencia", + welcomeStepCommunity: "Comunidade", + welcomeStepIntro: "Benvida", + welcomeHintIdentity: "O teu nome e a túa imaxe.", + welcomeHintType: "Como se abre Oasis para ti.", + welcomeHintUse: "Que módulos quedan activos.", + welcomeHintLook: "O tema que viste Oasis.", + welcomeHintCommunity: "Entra nun PUB para coñecer outros habitantes.", + welcomeHintIntro: "Envía un primeiro saúdo á comunidade.", + welcomeStepThemeAction: "Usar este tema", + welcomeJoinPubs: "Entrar nos PUBs", + welcomeNoFederation: "Sen federarse", + welcomeStepGreetingAction: "Enviar feed", + welcomeDescription: "Unhas poucas cousas que convén facer antes de comezar.", + welcomeSkip: "Agora non", + welcomeStepProfileAction: "Gardar o perfil", + welcomeBootstrapRunning: "Cargando a instantánea do pub. Ao rematar únese ao pub automaticamente; mentres tanto podes seguir usando Oasis.", + welcomeCompleteTitle: "Todo listo.", + welcomeCompleteText: "O teu nodo está preparado. A partir de aquí a rede medra cos habitantes aos que apoias.", + welcomeFindPeople: "Buscar habitantes", + devFilterFiles: "FICHEIROS", + devFilterSearch: "BUSCAR", + devFilterModules: "MÓDULOS", + devFilterTranslations: "TRADUCIÓNS", + devFilterStyles: "ESTILOS", + devFilterTests: "TESTS", + devVersion: "Versión", + devNodeVersion: "Node", + devStatsTests: "Tests", + devParentFolder: "Cartafol superior", + devOpenFolder: "abrir", + devDescription: "Explora o código que se está a executar neste nodo.", + devRoot: "raíz", + devSearchPlaceholder: "Texto que buscar no código", + devAnyExtension: "Calquera ficheiro", + devStatsFiles: "Ficheiros", + devStatsLines: "Liñas", + devStatsModules: "Módulos", + devStatsLanguages: "Idiomas", + devNotViewable: "non visualizable", + devEmptyFolder: "Este cartafol está baleiro.", + devSize: "Tamaño", + devShowing: "Amosando", + devLine: "liña", + devReportBug: "Informar dun erro", + devCreateTask: "Crear tarefa", + devRaw: "RAW", + devReportContext: "Atopado lendo o código de Oasis", + devTaskPrefix: "Revisar", + devPrevPage: "Liñas anteriores", + devNextPage: "Liñas seguintes", + devMatches: "coincidencias", + devFilesScanned: "ficheiros revisados", + devNoResults: "Aínda non hai coincidencias.", + devColModule: "Módulo", + devColState: "Estado", + devColModel: "Modelo", + devColView: "Vista", + devColRoutes: "Rutas", + devColTests: "Tests", + devOn: "activo", + devOff: "inactivo", + modulesDevLabel: "Desenvolvemento", + modulesDevDescription: "Módulo para xestionar o código de Oasis.", + themeIntro: + "Escolle un tema.", + setTheme: "Establecer o tema", + language: "Idioma", + languageDescription: + "Se queres usar outro idioma, selecciónao aquí.", + setLanguage: "Establecer o idioma", + peerConnectionsIntro: "Xestiona todas as túas conexións con outros nodos.", + peersConnectFailed: "Non se puido conectar: {reason}.", + peerStateLabel: "Estado", + peerAddressLabel: "Enderezo", + peerLastChangeLabel: "Último cambio", + peerStateConnected: "Conectado", + peerStateConnecting: "Conectando", + peerStateStaged: "Dispoñible", + peerErrSilent: "Sen actividade", + peerErrTimeout: "Non responde", + peerErrRefused: "Rexeita a conexión", + peerErrNotFound: "Enderezo descoñecido", + peerErrUnreachable: "Rede inaccesible", + peerErrKeys: "Usando chaves diferentes", + peerErrIdentity: "Non acepta a túa conexión", + peerErrTor: "Só TOR", + peerErrDropped: "Conexión cortada", + peerErrPaused: "Rede en pausa", + peerErrOther: "Erro de conexión", + peerOutdated: "Desactualizado", + peerVersionUnannounced: "Non anuncia a súa versión: un Oasis antigo", + peersTorMissing: "Este PUB só permite conexións dende TOR.", + peerConnectionsTitle: "Conexións", + peersOnlineHint: "Nodos conectados contigo agora mesmo.", + peersDiscoveredHint: "Nodos con enderezo coñecido que non están conectados agora.", + peersUnknownHint: "Nodos do teu grafo social sen enderezo coñecido.", + peersConnectionsHint: "Enderezos aos que pode conectar este nodo e o estado de cada conexión.", + peersTabLan: "LAN", + peersTabHealth: "Saúde", + peersHealthHint: "Como está cada pub ou nodo coñecido, na rede local e fóra dela.", + peersHealthReachable: "Alcanzable", + peersHealthFailing: "Fallando", + peersHealthFailures: "Fallos", + peersFailingSince: "Falla desde", + peersHealthForget: "Esquecer inalcanzables", + peersLanHint: "Nodos atopados na túa rede local.", + peersLanLastSeen: "Visto por última vez", + peersLanEmpty: "Aínda non se atopou ningún nodo na rede local.", + online: "Conectado", + offline: "Desconectado", + discovered: "Descubertos", + unknown: "Descoñecidos", + lanBroadcastActive: "Activa (multicast UDP na LAN)", + peerSourceRpc: "RPC", + peerSourceGossip: "Gossip", + peerSourceEbt: "EBT", + peerSourceRecent: "Recente", + peerSourceLan: "LAN", + pub: "PUB", + supported: "Apoiado", + recommended: "Recomendado", + blocked: "Bloqueado", + noConnections: "Non hai nodos conectados.", + noDiscovered: "Non descubriches ningún nodo.", + noUnknownPeers: "Non hai nodos descoñecidos no grafo de amizades.", + peersTechnicalEmpty: "Aínda non hai nodos rexistrados.", + peerConnect: "Conectar", + peerDisconnect: "Desconectar", + peerRefresh: "Refrescar", + peerPruneIdle: "Quitar inactivos", + peersPause: "Pausar", + peersResume: "Retomar", + peersPausedNote: "En pausa: Oasis non se conecta con ninguén e ninguén se pode conectar a el.", + peersPauseHint: "Detén todas as conexións: Oasis non se conecta a outros nodos nin os acepta ata que retomes.", + peerExport: "Exportar", + peerImport: "Importar", + peerImportTitle: "Importar lista de nodos", + peerImportPlaceholder: "Pega un enderezo multiserver por liña, ou sube un ficheiro .txt…", + connectionActionIntro: + "", + indexes: "Índices", + indexesRebuilt: "Índices reconstruídos", + indexesRebuildFailed: "Non se puideron reconstruír os índices", + indexesFiles: "Ficheiros de índice", + indexesDescription: + "Reconstruír os índices é seguro e pode arranxar algúns erros da túa blockchain.", + homePageTitle: "Páxina de inicio", + homePageDescription: "Selecciona que módulo queres como páxina de inicio.", + saveHomePage: "Establecer a páxina de inicio", + invites: "Convites", + invitesDescription: "Xestiona e aplica códigos de convite na túa rede.", + invitesPubsHint: "Pega un código de convite dun pub para federarte con el e comezar a replicar os seus habitantes.", + invitesFederationsHint: "Pubs cos que estás federado: actualízaos, quita os inalcanzables ou exporta a lista.", + invitesTribesTitle: "Tribos", + invitesChatsTitle: "Chats", + invitesPadsTitle: "Pads", + invitesCalendarsTitle: "Calendarios", + invitesEventsTitle: "Eventos", + invitesForumsTitle: "Foros", + invitesMapsTitle: "Mapas", + invitesShopsTitle: "Tendas", + invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "PUBs Suxeridos", + invitesFilterOthers: "Outras", + invitesJoinTitle: "Convites", + invitesJoinHint: "Pega calquera código de convite, sexa do que sexa, ou o Oasis ID dun habitante.", + invitesJoinPlaceholder: "Código de convite ou Oasis ID", + invitesJoinButton: "Entrar", + invitesUsedTitle: "Últimos convites", + invitesUsedEmpty: "Aínda non utilizaches ningún convite.", + invitesUsedType: "Tipo", + invitesUsedName: "Nome", + invitesUsedDate: "Data", + invitesPubInviteCodePlaceholder: "Introduce o código de convite do PUB", + invitesAcceptInvite: "Entrar no PUB", + invitesPubUses: "Invitacións usadas", + invitesPubExhausted: "Este PUB non ten invitacións dispoñibles.", + invitesAlreadyFederated: "Xa estás federado con este pub.", + invitesJoinPubMode: "Este Oasis funciona como PUB, en modo de só lectura. Para unilo a outro PUB, usa no terminal: ./oasis.sh join ", + invitesJoinLocalOnly: "As invitacións só se aceptan desde o equipo onde funciona Oasis, ou coa ligazón de administración que mostra ao arrincar.", + invitesFederationsTitle: "Federacións", + invitesAcceptedInvites: "Federadas", + invitesNoFederatedPubs: "Non hai pubs federados.", + invitesPubsRefresh: "Refrescar", + invitesPubsClearUnreachable: "Limpar", + invitesPubsExport: "Exportar", + invitesPubsImport: "Importar", + invitesPubsImportTitle: "Importar pubs", + invitesPubsImportPlaceholder: "Pega un enderezo multiserver ou un código de convite por liña, ou sube un ficheiro .txt…", + genericError: "Produciuse un erro.", + panicMode: "Modo pánico!", + encryptData: "Establece un contrasinal (mínimo 32 caracteres de lonxitude) para cifrar a túa blockchain", + decryptData: "Introduce o contrasinal para descifrar a túa blockchain", + removeDataDescription: "COIDADO: Este proceso non se pode desfacer.", + removePanicButton: "BORRAR TODOS OS TEUS DATOS!", + searchTitle: "Buscar", + searchDescriptionLabel: "Busca contido na túa rede.", + searchPlaceholder:"Buscar contido...", + searchLocationLabel:"Localización", + searchPriceLabel:"Prezo", + searchCategoryLabel:"Categoría", + searchPriorityLabel:"Prioridade", + searchStatusLabel:"Estado", + statusLabel:"Estado", + noResultsFound:"Non se atoparon resultados.", + votesOption:"Opinións votadas", + voteYesLabel:"Si", + voteNoLabel:"Non", + allTypesLabel: "SEN LÍMITES", + YESLabel:"SI", + NOLabel:"NON", + votesOptionYesLabel:"Si", + votesOptionNoLabel:"Non", + votesOptionAbstentionLabel:"Abstención", + votesQuestionLabel:"Pregunta", + voteStatusOpen:"ABERTO", + voteStatusClosed:"PECHADO", + commentDescription: ({ parentUrl }) => [ + " comentou en ", + a({ href: parentUrl }, " fío"), + ], + commentTitle: ({ authorName }) => [`Comentou no post de @${authorName}`], + subtopicDescription: ({ parentUrl }) => [ + " creou un subtema desde ", + a({ href: parentUrl }, " un post"), + ], + subtopicTitle: ({ authorName }) => [`Creou un subtema no post de @${authorName}`], + oasisDescription: "OASIS Rede de Proxectos", + searchSubmit: "Buscar...", + votesLabel: "GOBERNO", + pdfFallbackLabel: "Documento PDF", + shopLabel: "TENDAS", + voteLabel: "VOTOS", + submit: "Aplicar", + editProfile: "Editar o Avatar", + oasisIdLabel: "OasisID", + spreadContent: "Replicar", + welcomePmSubject: "Hello World!", + welcomePmBody: "Dámosche a benvida a Oasis — unha rede social libre, peer-to-peer, federada e cifrada, cunha arquitectura distribuída e só por convite.\n\nAlgúns lugares interesantes para comezar:\n\n- [/avatar](/profile) — Edita o teu avatar, nome, descrición e que módulos se amosan no teu avatar público.\n- /settings — Axusta a túa instancia local: idioma, tema, módulos, privacidade e sincronización.\n- /invites — Engade un pub para federarte co resto da rede.\n- /peers — Mira quen está conectado, volve escanear a túa LAN, exporta ou importa listas de nodos.\n- /inhabitants — Descubre e visita os avatares doutros habitantes.\n- /tribes — Crea grupos privados con cifrado de extremo a extremo ou únete a eles.\n- /search — Busca o contido da rede por tipo.\n\nAlgúns lugares interesantes para conectar:\n\n- /multiverse — Xestiona as túas outras contas do multiverso, incluído enviar e recibir contido.\n\nEsta mensaxe enviouse de forma privada desde ti cara a ti, por iso non fixo falta conexión para recibila.", + profileSensorsSectionTitle: "Sensores", + profileVisibilityActivity: "Nivel de actividade", + profileDeviceLockedHint: "Este sensor está sempre activo: permite que outros vexan desde que dispositivo te conectas e non se pode desactivar.", + profileVisibilityKarma: "Puntuación KARMA", + profileVisibilityUbi: "RBU", + profileVisibilityWallet: "Carteira ECOin", + profileVisibilityEcoTax: "Taxa ECO", + profileVisibilityLarpSign: "Selo L.A.R.P.", + profileVisibilityGpg: "Chave GPG", + clearnetSectionTitle: "Clearnet", + profileClearnetShopsLabel: "Tendas", + profileClearnetJobsLabel: "Emprego", + profileClearnetEventsLabel: "Eventos", + profileClearnetProjectsLabel: "Proxectos", + profileClearnetPostsLabel: "Blogs", + profileClearnetAudiosLabel: "Audios", + profileClearnetVideosLabel: "Vídeos", + profileClearnetImagesLabel: "Imaxes", + profileClearnetDocumentsLabel: "Documentos", + profileClearnetTorrentsLabel: "Torrents", + profileClearnetPodcastsLabel: "Podcasts", + profileClearnetMarketLabel: "Mercado", + profileClearnetFeedLabel: "Feed", + profileClearnetWikiLabel: "Wikis", + editProfileDescription: + "", + profileName: "Nome", + profileImage: "Imaxe do Avatar", + profileGpgKey: "Chave pública GPG (.asc)", + profileGpgChip: "GPG", + profileGpgDownload: "Descargar", + profileGpgRemove: "Eliminar", + profileDescription: "Descrición", + hashtagDescription: + "Posts de habitantes da túa rede que fan referencia a este #hashtag, ordenados polo máis recente.", + rebuildName: "Reconstruír a blockchain", + wallet: "Carteira", + walletAddress: "Enderezo", + walletAmount: "Cantidade", + walletAddressLine: ({ address }) => `Enderezo: ${address}`, + walletAmountLine: ({ amount }) => `Cantidade: ${amount} ECO`, + walletBack: "Volver", + walletBalanceTitle: "Saldo", + walletWalletSendTitle: "Enviar", + walletReceiveTitle: "Recibir", + walletHistoryTitle: "Historial", + walletBalanceLine: ({ balance }) => `${balance} ECO`, + walletCnfrs: "Conf.", + walletConfirm: "Confirmar", + walletDescription: "Xestiona a túa carteira ECOin.", + walletDate: "Data", + walletFee: "Taxa (canto maior sexa a taxa, máis rápido se procesará a túa transacción)", + walletFeeLine: ({ fee }) => `Taxa: ECO ${fee}`, + walletHistory: "Historial", + walletReceive: "Recibir", + walletReset: "Restablecer", + walletSend: "Enviar", + walletPayingTransfer: "Pagando o transfer", + walletCreateTransferLabel: "Xerar contrato (transfer)", + walletTransferNotifiedNote: "Avisouse do pagamento á outra parte", + walletTransferCreatedNote: "Creouse un transfer (recibo) deste pagamento", + walletTransferNoPayeeNote: "Ningún habitante publicou este enderezo, así que non se creou ningún transfer", + walletTransferReceiptConcept: "Pagamento desde a carteira", + walletPaymentPmText: "envioute un pagamento", + walletPayeeLabel: "Destinatario", + walletConceptLabel: "Concepto", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Recibiches a túa RBU.", + bankingBotPaymentTitle: "Recibiches un pagamento.", + bankingBotUbiPaidText: "A túa Renda Básica Universal foi pagada", + bankUbiPubThisNode: "Este nodo paga a RBU", + bankUbiPubConnected: "PUB da RBU", + bankUbiPubNone: "De momento non hai PUBs que anuncien unha RBU.", + bankUbiAddressPublished: "Enderezo ECOin publicado", + bankUbiAddressMissing: "Publica o teu enderezo ECOin para recibir a RBU", + bankUbiClaimedThisMonth: "Reclamada este mes", + bankUbiPubBalance: "Carteira do PUB dispoñible para a RBU", + bankNoPubAddress: "O PUB da RBU aínda non publicou ningún enderezo ECOin.", + bankUbiTab: "RBU", + bankDonateEco: "DOAR!", + donateTooltip: "Doar ECOin", + bankNoUserAddress: "Este habitante non comparte ningún enderezo ECOin.", + bankUbiPubDefault: "Por defecto", + bankUbiPool: "Asignación", + bankUbiLastSeen: "Visto por última vez", + bankUbiLastPayout: "Última acción", + bankUbiPaidOut: "Repartido", + bankRangeToday: "Hoxe", + bankRangeWeek: "Semana", + bankRangeMonth: "Mes", + bankRangeYear: "Ano", + bankRangeFiveYears: "5 anos", + bankRangeAll: "Todo", + bankChartValueTitle: "Evolución do valor de ECOin", + bankChartValueHours: "Equivalencia en tempo (h)", + bankChartWealthTitle: "Redistribución da riqueza ECOin", + bankChartDistributed: "RBU repartida (ECO)", + bankChartRebalanceTitle: "Redistribución da riqueza entre PUBs", + bankChartRebalanced: "ECO movidos entre PUBs", + bankChartTaxes: "Impostos retidos (ECO)", + bankChartNoDataRange: "Non hai datos neste intervalo.", + bankChartNoDataYet: "Aínda non hai datos", + bankChartPerHour: "por hora", + bankChartPerDay: "por día", + bankChartPerMonth: "por mes", + bankTotalUbiDistributed: "RBU total redistribuída (rede)", + bankTotalTaxesWithheld: "Impostos totais retidos (rede)", + bankInflationIssuance: "Taxa de emisión (anual, por recompensa de bloque)", + bankRulesEligibilityTitle: "Quen pode recibir a RBU", + bankRulesEligibility1: "Un enderezo ECOin publicado no teu perfil (Perfil → Sensores → Carteira ECOin).", + bankRulesEligibility2: "Un feed con máis de 30 días e actividade na rede: karma por riba de cero.", + bankRulesEligibility3: "Unha reclamación por mes (epoch), desde Banca co botón Reclamar RBU. Pódese rexeitar a do mes en curso con Rexeitar RBU.", + bankRulesEligibility4: "O mínimo de 1 ECO págase sempre; os impostos só reducen a parte que o supera e nunca se cobran á parte.", + bankRulesEligibility5: "O PUB paga no seu seguinte ciclo e publica un transfer RBU que podes confirmar.", + bankRulesEligibility6: "Se falta o teu enderezo, o feed é demasiado novo, rexeitaches o mes ou o PUB non ten fondos, a reclamación sáltase e queda rexistrada no PUB.", + copyLinkTitle: "Copiar a ligazón", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "Rexeitar RBU", + bankUbiRefusedThisMonth: "Rexeitaches a RBU deste mes.", + bankingBotUbiAvailableTitle: "Podes reclamar a túa RBU.", + bankingBotUbiAvailableText: "A túa RBU deste mes está dispoñible", + bankingBotConfirmTitle: "Tes un contrato pendente de confirmación", + bankingBotConfirmText: "Contratos pendentes de confirmación", + bankChartZoomHint: "Preme para ampliar", + bankChartPoolTitle: "Asignación da RBU (no tempo)", + bankChartNetworkUbiTitle: "RBU total redistribuída (rede)", + bankChartSupplyTitle: "Oferta circulante de ECOin (no tempo)", + bankChartInflationTitle: "Inflación de ECOin (no tempo)", + bankChartInflationSeries: "Taxa anualizada (%)", + bankChartPool: "Asignación total (ECO)", + bankChartKarmaTitle: "Puntuación KARMA (no tempo)", + bankChartFundsTitle: "Os teus fondos ECOin (no tempo)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin conectada", + bankWalletNotConfigured: "ECOin desconectada", + transfersPayWithWallet: "PAGAR!", + walletStatus: "Estado", + walletDisconnected: [ + "Tes a ", + strong("carteira de ECOin desconectada"), + ". Revisa ", + a({ href: '/settings#wallet' }, "a túa configuración"), + " ou o estado da túa conexión.", + ], + walletSentToLine: ({ destination, amount }) => `Enviar ECO ${amount} a ${destination}`, + walletNotConfigured: "A túa carteira de ECOin aínda non está configurada.", + walletSettingsDescription: "Integrar Oasis coa túa carteira de ECOin.", + walletSettingsMobileDescription: "Vincula Oasis coa app ECOin Wallet deste dispositivo.", + walletDownloadEcoin: "Descargar ECOin!", + walletRpcUrl: "URL do RPC", + walletStatusMessages: { + invalid_amount: "Cantidade non válida", + invalid_dest: "Enderezo de destino non válido", + invalid_fee: "Taxa non válida", + validation_errors: "Erros na validación", + send_tx_success: "Transacción realizada", + }, + walletTitle: "Carteira", + walletTotalCostLine: ({ totalCost }) => `Custo total: ECO ${totalCost}`, + walletTransactionId: "ID da transacción", + walletTxId: "ID Tx", + walletType: "Tipo", + walletUser: "Usuario", + walletPass: "Contrasinal", + walletConfiguration: "Configurar a carteira", + walletDisconnectButton: "Desconectar a carteira", + cipher: "Cifrador", + cipherTitle: "Cifrador", + cipherDescription: "Cifra e descifra o teu texto de forma simétrica (cun contrasinal compartido).", + randomPassword: "Contrasinal aleatorio", + cipherTextPlaceholder: "Introduce o texto que queres cifrar...", + cipherPasswordLabel: "Establece un contrasinal (mínimo 32 caracteres) para cifrar o teu texto", + cipherPasswordDecryptLabel: "Establece un contrasinal (mínimo 32 caracteres) para descifrar o teu texto", + cipherPasswordPlaceholder: "Introduce un contrasinal...", + cipherEncryptButton: "Cifrar", + cipherEncryptedMessageLabel: "Texto cifrado", + cipherDecryptedMessageLabel: "Texto descifrado", + cipherEncryptedTextPlaceholder: "Introduce o texto cifrado...", + cipherDecryptButton: "Descifrar", + password: "Contrasinal", + text: "Texto", + encryptedText: "Texto cifrado", + iv: "Vector de inicialización (IV)", + bookmarkTitle: "Marcadores", + bookmarkDescription: "Descobre e xestiona marcadores na túa rede.", + bookmarkAllSectionTitle: "Marcadores", + bookmarkMineSectionTitle: "Os teus marcadores", + bookmarkRecentSectionTitle: "Marcadores recentes", + bookmarkTopSectionTitle: "Marcadores top", + bookmarkFavoritesSectionTitle: "Favoritos", + bookmarkCreateSectionTitle: "Crear un marcador", + bookmarkUpdateSectionTitle: "Actualizar o marcador", + bookmarkFilterAll: "TODOS", + bookmarkFilterMine: "MEUS", + bookmarkFilterTop: "TOP", + bookmarkFilterFavorites: "FAVORITOS", + bookmarkFilterRecent: "RECENTES", + bookmarkCreateButton: "Crear un marcador", + bookmarkUpdateButton: "Actualizar", + bookmarkAddFavoriteButton: "Engadir a favoritos", + bookmarkRemoveFavoriteButton: "Quitar de favoritos", + bookmarkUrlLabel: "Ligazón", + bookmarkUrlPlaceholder: "https://solarnethub.com", + bookmarkDescriptionLabel: "Descrición", + bookmarkDescriptionPlaceholder: "Opcional", + bookmarkTagsLabel: "Etiquetas", + bookmarkTagsPlaceholder: "Introduce etiquetas separadas por comas", + bookmarkLastVisitLabel: "Última visita", + bookmarkSearchPlaceholder: "Buscar en Marcadores...", + bookmarkSortRecent: "Máis recentes", + bookmarkSortOldest: "Máis antigos", + bookmarkSortTop: "Máis votados", + bookmarkSearchButton: "Buscar", + bookmarkUpdatedAt: "Actualizado", + bookmarkNoMatch: "Non hai marcadores que coincidan coa túa busca.", + noBookmarks: "Non hai marcadores dispoñibles.", + noUrl: "Sen ligazón", + noCategory: "Sen categoría", + noLastVisit: "Sen última visita", + videoTitle: "Vídeos", + videoDescription: "Explora e xestiona contido de vídeo na túa rede.", + videoMineSectionTitle: "Os teus vídeos", + videoCreateSectionTitle: "Subir un vídeo", + videoUpdateSectionTitle: "Actualizar o vídeo", + videoAllSectionTitle: "Vídeos", + videoRecentSectionTitle: "Vídeos recentes", + videoTopSectionTitle: "Vídeos máis votados", + videoFavoritesSectionTitle: "Favoritos", + videoFilterAll: "TODOS", + videoFilterMine: "MEUS", + videoFilterRecent: "RECENTES", + videoFilterTop: "TOP", + videoFilterFavorites: "FAVORITOS", + videoCreateButton: "Subir un vídeo", + videoUpdateButton: "Actualizar", + videoDeleteButton: "Eliminar", + videoAddFavoriteButton: "Engadir a favoritos", + videoRemoveFavoriteButton: "Quitar de favoritos", + videoFileLabel: "Selecciona un ficheiro de vídeo (.mp4, .webm, .ogv, .mov)", + videoTagsLabel: "Etiquetas", + videoTagsPlaceholder: "Introduce etiquetas separadas por comas", + videoTitleLabel: "Título", + videoTitlePlaceholder: "Opcional", + videoDescriptionLabel: "Descrición", + videoDescriptionPlaceholder: "Opcional", + videoNoFile: "Non se proporcionou ningún ficheiro de vídeo", + noVideos: "Non hai vídeos dispoñibles.", + videoSearchPlaceholder: "Buscar en Vídeos...", + videoSortRecent: "Máis recentes", + videoSortOldest: "Máis antigos", + videoSortTop: "Máis votados", + videoSearchButton: "Buscar", + videoUpdatedAt: "Actualizado", + videoNoMatch: "Ningún vídeo coincide coa túa busca.", + documentTitle: "Documentos", + documentDescription: "Descobre e xestiona documentos na túa rede.", + documentAllSectionTitle: "Documentos", + documentMineSectionTitle: "Os teus documentos", + documentRecentSectionTitle: "Documentos recentes", + documentTopSectionTitle: "Documentos top", + documentFavoritesSectionTitle: "Favoritos", + documentCreateSectionTitle: "Subir un documento", + documentUpdateSectionTitle: "Editar o documento", + documentFilterAll: "TODO", + documentFilterMine: "MEUS", + documentFilterRecent: "RECENTES", + documentFilterTop: "TOP", + documentFilterFavorites: "FAVORITOS", + documentCreateButton: "Subir un documento", + documentUpdateButton: "Actualizar", + documentAddFavoriteButton: "Engadir a favoritos", + documentRemoveFavoriteButton: "Quitar de favoritos", + documentMessageAuthorButton: "MP", + documentFileLabel: "Subir un documento (.pdf)", + documentTagsLabel: "Etiquetas", + documentTagsPlaceholder: "Introduce etiquetas separadas por comas", + documentTitleLabel: "Título", + documentTitlePlaceholder: "Opcional", + documentDescriptionLabel: "Descrición", + documentDescriptionPlaceholder: "Opcional", + documentNoFile: "Sen ficheiro.", + noDocuments: "Non hai documentos dispoñibles.", + documentSearchPlaceholder: "Buscar en Documentos...", + documentSortRecent: "Máis recentes", + documentSortOldest: "Máis antigos", + documentSortTop: "Máis votados", + documentSearchButton: "Buscar", + documentNoMatch: "Non hai documentos que coincidan coa túa busca.", + documentUpdatedAt: "Actualizado", + audioTitle: "Audios", + audioDescription: "Explora e xestiona contido de audio na túa rede.", + audioMineSectionTitle: "Os teus audios", + audioCreateSectionTitle: "Subir un audio", + audioUpdateSectionTitle: "Actualizar o audio", + audioAllSectionTitle: "Audios", + audioRecentSectionTitle: "Audios recentes", + audioTopSectionTitle: "Audios máis votados", + audioFilterAll: "TODOS", + audioFilterMine: "MEUS", + audioFilterRecent: "RECENTES", + audioFilterTop: "TOP", + audioCreateButton: "Subir un audio", + audioUpdateButton: "Actualizar", + audioAddFavoriteButton: "Engadir a favoritos", + audioRemoveFavoriteButton: "Quitar de favoritos", + audioFileLabel: "Selecciona un ficheiro de audio (.mp3, .wav, .ogg)", + audioTagsLabel: "Etiquetas", + audioTagsPlaceholder: "Introduce etiquetas separadas por comas", + tagsPlaceholder: "Introduce etiquetas separadas por comas", + audioTitleLabel: "Título", + audioTitlePlaceholder: "Opcional", + audioDescriptionLabel: "Descrición", + audioDescriptionPlaceholder: "Opcional", + audioNoFile: "Non se proporcionou ningún ficheiro de audio", + noAudios: "Non hai audios dispoñibles.", + audioSearchPlaceholder: "Buscar en Audios...", + audioSortRecent: "Máis recentes", + audioSortOldest: "Máis antigos", + audioSortTop: "Máis votados", + audioSearchButton: "Buscar", + audioMessageAuthorButton: "MP", + audioUpdatedAt: "Actualizado", + audioNoMatch: "Ningún audio coincide coa túa busca.", + audioFavoritesSectionTitle: "Favoritos", + audioFilterFavorites: "FAVORITOS", + favoritesTitle: "Favoritos", + favoritesDescription: "Todos os teus elementos marcados como favoritos nun só lugar.", + favoritesSearchPlaceholder: "Buscar en Favoritos...", + favoritesFilterAll: "TODOS", + favoritesFilterRecent: "RECENTES", + favoritesFilterAudios: "AUDIOS", + favoritesFilterBookmarks: "MARCADORES", + favoritesFilterDocuments: "DOCUMENTOS", + favoritesFilterImages: "IMAXES", + favoritesFilterMaps: "MAPAS", + favoritesFilterPads: "PADS", + favoritesFilterChats: "CHATS", + favoritesFilterCalendars: "CALENDARIOS", + favoritesFilterVideos: "VÍDEOS", + favoritesNoItems: "Aínda non hai favoritos.", + yourContacts: "Os teus contactos", + allInhabitants: "Habitantes", + allCVs: "CV", + discoverPeople: "Descobre habitantes na túa rede.", + searchInhabitantsPlaceholder: "Buscar en Habitantes...", + filterLocation: "FILTRAR habitantes POR LOCALIZACIÓN …", + filterLanguage: "FILTRAR habitantes POR IDIOMA …", + filterSkills: "FILTRAR habitantes POR HABILIDADES …", + cvSkillsCount: "Habilidades", + applyFilters: "Aplicar os filtros", + locationLabel: "Localización", + languagesLabel: "Idiomas", + skillsLabel: "Habilidades", + commonSkills: "Habilidades comúns", + mutualFollowers: "Seguidores mutuos", + suggestedFollowsYou: "Ségueche", + latestInteractions: "Últimas interaccións", + suggestedSectionTitle: "Suxeridos", + topkarmaSectionTitle: "Mellor karma", + topecoSectionTitle: "Mellor Eco", + topactivitySectionTitle: "Máis actividade", + "TOP INACTIVITYButton": "MÁIS INACTIVOS", + topinactivitySectionTitle: "Habitantes menos activos", + blockedSectionTitle: "Bloqueados", + gallerySectionTitle: "GALERÍA", + topSectionTitle: "TOP", + blockedLabel: "Usuario bloqueado", + inhabitantviewDetails: "Ver os detalles", + keepReading: "Seguir lendo...", + oasisId: "ID", + noInhabitantsFound: "Aínda non se atoparon habitantes.", + inhabitantActivityLevel: "Nivel de actividade", + lifespanLabel: "Vixencia", + deviceLabel: "Dispositivo", + parliamentTitle: "Parlamento", + parliamentDescription: "Explora formas de goberno e políticas de xestión colectiva.", + parliamentFilterGovernment: "GOBERNO", + parliamentFilterCandidatures: "CANDIDATURAS", + parliamentFilterProposals: "PROPOSTAS", + parliamentFilterLaws: "LEIS", + parliamentFilterRevocations: "REVOGACIÓNS", + parliamentFilterHistorical: "HISTÓRICO", + parliamentFilterLeaders: "LÍDERES", + parliamentFilterRules: "REGRAS", + parliamentGovernmentCard: "Goberno actual", + parliamentSeatsGovernment: "Goberno", + parliamentSeatsOpposition: "Oposición", + parliamentSeatsAbstention: "Abstención", + parliamentSeatsQuorum: "Quórum de elección", + parliamentSeatsPass: "Limiar de aprobación", + parliamentSeatsByElection: "Por elección", + parliamentSeatsByHouses: "Por casa do LARP", + parliamentSeatsSeats: "escanos", + parliamentSeatsSeat: "escano", + parliamentSeatsLeader: "Líder", + parliamentSeatsVotesFor: "votos a", + parliamentSeatsNoHouse: "Sen casa", + parliamentActorInPowerInhabitant: "HABITANTE NO PODER", + parliamentActorInPowerTribe: "TRIBO NO PODER", + parliamentHistoricalGovernmentsTitle: "GOBERNOS", + parliamentHistoricalElectionsTitle: "CICLOS ELECTORAIS", + parliamentHistoricalLawsTitle: "HISTÓRICO", + parliamentHistoricalLeadersTitle: "LÍDERES", + parliamentThCycles: "CICLOS", + parliamentThTimesInPower: "MANDATOS", + parliamentThTotalCandidatures: "CANDIDATURAS", + parliamentThProposed: "LEIS PROPOSTAS", + parliamentThApproved: "LEIS APROBADAS", + parliamentThDeclined: "LEIS REXEITADAS", + parliamentThDiscarded: "LEIS DESCARTADAS", + parliamentMembers: "MEMBROS", + parliamentLegSince: "INICIO DO CICLO", + parliamentLegEnd: "FIN DO CICLO", + parliamentPoliciesProposal: "PROPOSTAS DE LEI", + parliamentPoliciesApproved: "LEIS APROBADAS", + parliamentPoliciesDeclined: "LEIS REXEITADAS", + parliamentPoliciesDiscarded: "LEIS DESCARTADAS", + parliamentEfficiency: "% EFICIENCIA", + parliamentRevocationFormTitle: "Revogar unha lei", + parliamentRevocationLaw: "Lei", + parliamentRevocationReasons: "Motivos", + parliamentRevocationPublish: "Publicar a revogación", + parliamentCurrentRevocationsTitle: "Revogacións actuais", + parliamentFutureRevocationsTitle: "Revogacións futuras", + parliamentPoliciesRevocated: "LEIS REVOGADAS", + parliamentPoliciesTitle: "HISTÓRICO", + parliamentLawsTitle: "LEIS APROBADAS", + parliamentRulesRevocations: "Calquera lei aprobada pode ser revogada co método de goberno vixente.", + parliamentNoStableGov: "Aínda non hai un goberno elixido.", + parliamentNoGovernments: "Aínda non hai gobernos no histórico.", + parliamentCandidatureFormTitle: "Propoñer unha candidatura", + parliamentCandidatureIdPh: "Oasis ID (@...) ou nome da tribo", + parliamentCandidatureMethod: "Método", + parliamentCandidatureProposeBtn: "Publicar a candidatura", + parliamentVoteBtn: "Votar", + parliamentProposalFormTitle: "Propoñer unha lei", + parliamentProposalDescription: "Descrición (≤1000)", + parliamentProposalPublish: "Publicar a proposta", + parliamentNoProposals: "Aínda non hai propostas de lei.", + parliamentNoLaws: "Aínda non hai leis aprobadas.", + parliamentMethodDEMOCRACY: "Democracia", + parliamentMethodMAJORITY: "Maioría (80%)", + parliamentMethodMINORITY: "Minoría (20%)", + parliamentMethodDICTATORSHIP: "Ditadura", + parliamentMethodKARMATOCRACY: "Karmatocracia", + parliamentThId: "ID", + parliamentThProposalDate: "Data da proposta", + parliamentThMethod: "Método", + parliamentThKarma: "Karma", + parliamentThSupports: "Apoios", + parliamentThVote: "Votar", + parliamentCurrentProposalsTitle: "Propostas actuais", + parliamentVotesSlashTotal: "Votos/Total", + parliamentVotesNeeded: "Votos necesarios", + parliamentFutureLawsTitle: "Futuras leis", + parliamentVoteAction: "Votar", + parliamentThLeader: "AVATAR", + parliamentPopulation: "Poboación", + parliamentThPresented: "CANDIDATURAS", + parliamentNoLeaders: "Aínda non hai líderes.", + parliamentCandidaturesListTitle: "Lista de candidaturas", + parliamentTimeRemaining: "Tempo restante", + parliamentElectionsStatusTitle: "Próximo goberno", + parliamentProposalDeadlineLabel: "Data límite", + parliamentViewReport: "Ver o informe", + parliamentProposalTimeLeft: "Tempo restante", + parliamentRulesTitle: "Como funciona o Parlamento", + parliamentRulesIntro: "As eleccións resólvense cada 2 meses; as candidaturas son continuas e reinícianse cando se elixe un goberno.", + parliamentRulesCandidates: "Calquera habitante pode propoñerse a si mesmo, a outro habitante ou a calquera tribo. Cada habitante pode propoñer ata 3 candidaturas por ciclo; rexéitanse os duplicados no mesmo ciclo.", + parliamentRulesElection: "Gaña a candidatura con máis votos no momento da resolución.", + parliamentRulesTies: "Desempate: maior karma do habitante; se persiste, perfil máis antigo; se persiste, proposta máis temperá; despois, orde lexicográfica por ID.", + parliamentRulesTerm: "A lapela Goberno amosa o goberno actual e as súas estatísticas.", + parliamentRulesMethods: "Formas: Anarquía (maioría simple), Democracia (50%+1), Maioría (80%), Minoría (20%), Karmatocracia (propostas con maior karma), Ditadura (aprobación instantánea).", + parliamentRulesAnarchy: "A Anarquía é o modo por defecto: se ao resolver non se elixe ningunha candidatura, proclámase a Anarquía. En Anarquía, calquera habitante pode propoñer leis.", + parliamentRulesProposals: "Se es o habitante gobernante ou membro da tribo gobernante, podes publicar propostas de lei. Os métodos non ditatoriais crean unha votación pública.", + parliamentRulesLimit: "Cada habitante pode publicar como máximo 3 propostas de lei por ciclo.", + parliamentRulesLaws: "Cando unha proposta alcanza o seu limiar, convértese en Lei e aparece na lapela Leis coa súa data de entrada en vigor.", + parliamentRulesHistorical: "En Histórico pódese ver cada ciclo de goberno que houbo e datos sobre a súa xestión.", + parliamentRulesLeaders: "En Líderes pódese ver unha clasificación de habitantes/tribos que gobernaron (ou se presentaron), ordenados por eficacia.", + parliamentRulesHemicycle: "O hemiciclo dá un escano por habitante: o bloque de goberno (votantes da candidatura gañadora e o líder), un bloque de oposición por cada outra candidatura do ciclo, e a abstención. As marcas descontinuas sinalan o quórum de elección (25 % do censo) e o limiar de aprobación do método vixente. \"Por casa do LARP\" colorea os mesmos escanos por casa.", + parliamentProposalVoteStatusLabel: "Estado da votación", + parliamentProposalOnTrackYes: "Limiar alcanzado", + parliamentProposalOnTrackNo: "Por debaixo do limiar", + courtsTitle: "Tribunais", + courtsDescription: "Explora formas de resolución de conflitos e de xestión colectiva da xustiza.", + courtsFilterCases: "CASOS", + courtsFilterMyCases: "MEUS", + courtsFilterJudges: "XUÍCES", + courtsFilterHistory: "HISTORIAL", + courtsFilterRules: "REGRAS", + courtsCaseFormTitle: "Abrir un caso", + courtsCaseRespondent: "Acusado / Parte demandada", + courtsCaseRespondentPh: "ID de Oasis (@...) ou nome da tribo", + courtsCaseSubjectPlaceholder: "Asunto ou descrición breve", + courtsCaseMethod: "Método de resolución", + courtsCaseSubmit: "Presentar o caso", + courtsNominateJudge: "Nomear un xuíz", + courtsJudgeId: "Xuíz", + courtsJudgeIdPh: "ID de Oasis (@...) ou nome do habitante", + courtsNominateBtn: "Nomear", + courtsAddEvidence: "Engadir probas", + courtsEvidenceText: "Texto", + courtsEvidenceLink: "Ligazón", + courtsEvidenceLinkPh: "https://…", + courtsEvidenceSubmit: "Anexar", + courtsAnswerText: "Resumo da resposta", + courtsAnswerSubmit: "Enviar a resposta", + courtsVerdictTitle: "Emitir veredicto", + courtsVerdictOrdersPh: "Accións, prazos, pasos restaurativos...", + courtsIssueVerdict: "Emitir veredicto", + courtsMediationPropose: "Propoñer un acordo", + courtsSettlementProposeBtn: "Propoñer", + courtsNominationsTitle: "Nomeamentos á xudicatura", + courtsThJudge: "Xuíz", + courtsThDate: "Data", + courtsThVote: "Votar", + courtsNoNominations: "Aínda non hai nomeamentos.", + courtsRespondentInvalid: "Debe ser un ID SSB válido (@...ed25519)", + courtsCaseMediatorsRespondentTitle: "Engadir mediadores da defensa", + courtsCaseMediatorsRespondent: "Mediadores (defensa)", + courtsMediatorsAccuserLabel: "Mediadores (acusación)", + courtsMediatorsRespondentLabel: "Mediadores (defensa)", + courtsMediatorsSubmit: "Gardar os mediadores", + courtsNoMyCases: "Aínda non tes conflitos.", + courtsNoHistory: "Aínda non hai xuízos rexistrados.", + courtsMethodDICTATOR: "Ditador", + courtsMethodPOPULAR: "Popular", + courtsMethodKARMATOCRACY: "Karmatocracia", + courtsCaseType: "Tipo de caso", + courtsPresetMinorConflict: "Conflito leve", + courtsPresetModerateConflict: "Conflito moderado", + courtsPresetSevereConflict: "Conflito grave", + courtsPresetHarassment: "Acoso ou abuso", + courtsPresetContentModeration: "Moderación de contido", + courtsPresetBanRestriction: "Expulsión ou restrición", + courtsPresetGovernanceDispute: "Disputa de goberno", + courtsStatusOPEN: "Aberto", + courtsStatusIN_PROGRESS: "En curso", + courtsStatusDECIDED: "Decidido", + courtsStatusCLOSED: "Pechado", + courtsStatusSOLVED: "Resolto", + courtsStatusUNSOLVED: "Sen resolver", + courtsStatusDISCARDED: "Descartado", + courtsRulesTitle: "Como funcionan os Tribunais", + courtsRulesIntro: "Os Tribunais son un proceso xestionado pola comunidade para resolver conflitos e promover a xustiza restaurativa. Priorízanse o diálogo, as probas claras e os remedios proporcionais.", + courtsRulesLifecycle: "Proceso: 1) Abrir o caso 2) Seleccionar o método 3) Presentar probas 4) Audiencia e deliberación 5) Veredicto e remedio 6) Cumprimento e peche 7) Apelación (se procede).", + courtsRulesRoles: "Acusación: abre o caso. Defensa: habitante ou tribo acusada. Método: mecanismo elixido pola comunidade para facilitar, avaliar as probas e emitir veredicto. Testemuña: achega testemuño ou probas. Mediadores: habitantes neutrais convidados pola acusación e/ou a defensa, con acceso a todos os detalles, que axudan a desescalar o conflito e a cocrear acordos.", + courtsRulesEvidence: "Descrición de ata 1000 caracteres. Anexa imaxes, audio, vídeo e documentos PDF relevantes e legais. Non compartas datos privados sensibles sen consentimento.", + courtsRulesDeliberation: "As audiencias poden ser públicas ou privadas. Os xuíces garanten o respecto, piden aclaracións e poden descartar material irrelevante ou ilegal.", + courtsRulesVerdict: "Priorízase a restauración: desculpas, acordos de mediación, moderación de contido, restricións temporais ou outras medidas proporcionais. O razoamento debe quedar rexistrado.", + courtsRulesAppeals: "Apelación: permitida cando hai probas novas ou un erro procesual claro. Debe presentarse nun prazo de 7 días salvo que se indique o contrario.", + courtsRulesPrivacy: "Respecta a privacidade e a seguridade. O doxing, o odio ou as ameazas elimínanse. Os xuíces poden editar ou selar partes do expediente para protexer habitantes en risco.", + courtsRulesMisconduct: "O acoso, a manipulación ou as probas fabricadas poden levar a unha resolución negativa inmediata.", + courtsRulesGlossary: "Caso: rexistro dun conflito. Probas: materiais que respaldan as afirmacións. Veredicto: decisión con remedio. Apelación: solicitude para revisar o veredicto.", + courtsFilterActions: "ACCIÓNS", + courtsThRole: "Rol", + courtsRoleAccuser: "Acusación", + courtsRoleDefence: "Defensa", + courtsRoleMediator: "Mediador", + courtsRoleJudge: "Xuíz", + courtsRoleDictator: "Ditador", + courtsAssignJudgeTitle: "Elixir un xuíz", + courtsAssignJudgeBtn: "Elixir un xuíz", + trendingTitle: "Tendencias", + exploreTrending: "Explora o contido máis popular da túa rede.", + votesButton: "GOBERNO", + shopProductButton: "TENDA", + podcastButton: "PODCAST", + podcastEpisodeButton: "EPISODIO", + campaignButton: "CAMPAÑA", + logisticsRouteButton: "RUTA", + trendingTotalOpinions: "Total de opinións", + trendingNoContentMessage: "Aínda non hai tendencias.", + trendingTotalCount: "Total de contadores", + tasksTitle: "Tarefas", + tasksDescription: "Descobre e xestiona tarefas na túa rede.", + taskSearchPlaceholder: "Buscar en Tarefas...", + taskTitleLabel: "Título", + taskDescriptionLabel: "Descrición", + taskStartTimeLabel: "Data de inicio", + taskEndTimeLabel: "Data de fin", + taskPriorityLabel: "Prioridade", + taskPriorityUrgent: "Urxente", + taskPriorityHigh: "Alta", + taskPriorityMedium: "Media", + taskPriorityLow: "Baixa", + taskLocationLabel: "Localización", + taskTagsLabel: "Etiquetas", + taskVisibilityLabel: "Visibilidade", + taskPublic: "público", + taskPrivate: "privado", + taskStatus: "Estado", + taskStatusOpen: "Aberta", + taskStatusInProgress: "En curso", + taskStatusClosed: "Pechada", + taskAssignedTo: "Asignada", + taskAssignedChip: "Asignada", + taskUnassignedChip: "Sen asignar", + taskAssignees: "Asignados", + taskAssignButton: "Asignarma", + taskUnassignButton: "Desasignar", + taskCreateButton: "Crear unha tarefa", + taskUpdateButton: "Actualizar", + taskDeleteButton: "Eliminar", + taskFilterAll: "TODAS", + taskFilterRecent: "RECENTES", + taskFilterMine: "MIÑAS", + taskFilterOpen: "ABERTAS", + taskFilterInProgress: "EN CURSO", + taskFilterClosed: "PECHADAS", + taskFilterAssigned: "ASIGNADAS", + taskFilterUrgent: "URXENTES", + taskFilterHigh: "ALTAS", + taskFilterMedium: "MEDIAS", + taskFilterLow: "BAIXAS", + taskAllSectionTitle: "Tarefas", + taskMineSectionTitle: "As túas tarefas", + taskCreateSectionTitle: "Crear unha tarefa", + taskUpdateSectionTitle: "Actualizar", + taskOpenTitle: "Tarefas abertas", + taskInProgressTitle: "Tarefas en curso", + taskClosedTitle: "Tarefas pechadas", + taskAssignedTitle: "Tarefas asignadas", + notasks: "Non hai tarefas dispoñibles.", + noLocation: "Non se especificou ningunha localización", + taskSetStatus: "Establecer o estado", + eventDateLabel: "Data", + eventRecurrenceLabel: "Recorrencia", + eventRecurrenceUntil: "Repetir ata", + eventRecurrenceHint: "Deixa baleira a data de repetición para un evento único.", + eventUpcomingDates: "Próximas datas", + eventsTitle: "Eventos", + eventsDescription: "Descobre e xestiona eventos na túa rede.", + eventSearchPlaceholder: "Buscar en Eventos...", + eventPrice: "Prezo", + eventStatus: "Estado", + eventAllSectionTitle: "Eventos", + eventMineSectionTitle: "Os teus eventos", + eventCreateSectionTitle: "Crear un evento", + eventUpdateSectionTitle: "Actualizar o evento", + eventCreateButton: "Crear un evento", + eventAddToCalendar: "Engadir ao calendario", + eventVisitCalendar: "Visitar o calendario", + viewTask: "Ver a tarefa", + viewEvent: "Ver o evento", + viewPad: "Ver o pad", + viewMap: "Ver o mapa", + viewCalendar: "Ver o calendario", + viewReport: "Ver o informe", + viewTransfer: "Ver a transferencia", + viewItem: "Ver o artigo", + viewShop: "Ver a tenda", + viewProject: "Ver o proxecto", + viewJob: "Ver o emprego", + viewPlace: "Ver o lugar", + generatePdf: "Xerar PDF", + sharePm: "Compartir por PM", + galleryImages: "Imaxes (tamaño máx.: 75MB cada unha)", + galleryPhotos: "Imaxes", + galleryAddPhoto: "Engadir unha imaxe", + galleryRemovePhoto: "Quitar", + galleryVideo: "Vídeo (tamaño máx.: 75MB)", + galleryAddVideo: "Engadir un vídeo", + galleryRemoveVideo: "Quitar o vídeo", + viewVotation: "Ver a votación", + voteCastTitle: "Emitir o voto", + voteResults: "Resultados", + eventTitleLabel: "Título", + eventDescriptionLabel: "Descrición", + eventDescriptionPlaceholder: "Introduce a descrición do evento...", + eventUpdateButton: "Actualizar", + eventTagsLabel: "Etiquetas", + eventPriceLabel: "Prezo", + eventUrlLabel: "URL", + eventAttendees: "Asistentes", + eventLocation: "Localización", + eventLocationLabel: "Localización", + noevents: "Non hai eventos dispoñibles.", + eventDate: "Data", + eventAttendButton: "Asistir ao evento", + eventFilterAll: "TODOS", + eventFilterRecent: "RECENTES", + eventFilterMine: "MEUS", + eventFilterToday: "HOXE", + eventFilterWeek: "ESTA SEMANA", + eventFilterMonth: "ESTE MES", + eventFilterYear: "ESTE ANO", + eventFilterArchived: "ARQUIVADOS", + eventPrivacyLabel: "Visibilidade", + eventPublic: "Público", + eventPrivate: "Privado", + eventAttended: "Asisto", + eventUnattended: "Non asisto", + eventStatusOpen: "Aberto", + eventStatusClosed: "Pechado", + tagsTitle: "Etiquetas", + tagsDescription: "Descobre e explora patróns de taxonomía na túa rede.", + tagsAllSectionTitle: "Etiquetas", + tagsTopSectionTitle: "Etiquetas principais", + tagsCloudSectionTitle: "Nube de etiquetas", + tagsFilterAll: "TODAS", + tagsFilterMine: "MIÑAS", + tagsFilterRecent: "RECENTES", + tagsMineSectionTitle: "As miñas etiquetas", + tagsRecentSectionTitle: "Etiquetas recentes", + tagsFilterTop: "TOP", + tagsFilterCloud: "NUBE", + tagsNoItems: "Non hai etiquetas dispoñibles.", + tagsSearchPlaceholder: "Buscar en Etiquetas...", + tagsTableHeaderTag: "ETIQUETA/LIGAZÓN:", + tagsTableHeaderCount: "CONTADOR:", + transfersTitle: "Transferencias", + transfersDescription: "Descobre e xestiona transferencias na túa rede.", + transfersFrom: "De", + transfersTo: "A", + transfersFilterAll: "TODAS", + transfersFilterRecent: "RECENTES", + transfersFilterMine: "MIÑAS", + transfersFilterMinePending: "PENDENTES", + transfersFilterMineConfirmed: "ASINADAS", + transfersFilterUBI: "RBU", + transfersFilterMarket: "MERCADO", + transfersFilterTop: "TOP", + transfersFilterPending: "PENDENTES", + transfersFilterUnconfirmed: "NON CONFIRMADAS", + transfersFilterClosed: "PECHADAS", + transfersFilterDiscarded: "DESCARTADAS", + transfersFilterEconomic: "ECONÓMICA", + transfersFilterTime: "TEMPO", + transfersFilterTrust: "CONFIANZA", + transfersCategory: "Categoría", + transfersCategoryEconomic: "Económica", + transfersCategoryTime: "Tempo", + transfersCategoryTrust: "Confianza", + transfersUnitEco: "ECO", + transfersUnitHours: "h", + transfersUnitTrust: "confianza", + transfersEconomicSectionTitle: "Transferencias económicas", + transfersTimeSectionTitle: "Transferencias de tempo", + transfersTrustSectionTitle: "Transferencias de confianza", + transfersCreateButton: "Crear unha transferencia", + transfersUpdateButton: "Actualizar", + transfersDeleteButton: "Eliminar", + transfersToUser: "ID de Oasis", + transfersToUserValidation: "ID de Oasis válido, p. ex. @…=.ed25519", + transfersConcept: "Concepto", + transfersAmount: "Cantidade", + transfersDeadline: "Prazo", + transfersTags: "Etiquetas", + transfersStatus: "Estado", + transfersConfirmations: "CONFIRMACIÓNS", + transfersExportContract: "Xerar a factura", + transfersConfirmButton: "Confirmar a transferencia", + transfersNoItems: "Non se atoparon transferencias.", + transfersMineSectionTitle: "As túas transferencias", + transfersUBISectionTitle: "Transferencias RBU", + transfersTopSectionTitle: "Transferencias principais", + transfersPendingSectionTitle: "Transferencias pendentes", + transfersUnconfirmedSectionTitle: "Transferencias non confirmadas", + transfersClosedSectionTitle: "Transferencias pechadas", + transfersDiscardedSectionTitle: "Transferencias descartadas", + transfersCreateSectionTitle: "Crear unha transferencia", + transfersAllSectionTitle: "Transferencias", + transfersSearchPlaceholder: "Buscar en Transferencias...", + transfersMinAmountLabel: "Importe mín.", + transfersMaxAmountLabel: "Importe máx.", + transfersSortRecent: "Máis recentes", + transfersSortAmount: "Maior importe", + transfersSortDeadline: "Prazo máis próximo", + transfersSearchButton: "Buscar", + transfersExpiredBadge: "CADUCADA", + transfersUpdatedAt: "Actualizado", + transfersNoMatch: "Non hai transferencias que coincidan coa túa busca.", + votationsTitle: "Votacións", + votationsDescription: "Descobre e xestiona votacións na túa rede.", + voteMineSectionTitle: "As túas votacións", + voteCreateSectionTitle: "Crear unha votación", + voteUpdateSectionTitle: "Actualizar", + voteOpenTitle: "Votacións abertas", + voteClosedTitle: "Votacións pechadas", + voteAllSectionTitle: "Goberno", + voteCreateButton: "Crear unha votación", + voteErrorTitle: "Revisa o formulario", + voteErrorDeadlineMin: "A data límite debe ser de polo menos {days} días desde agora.", + voteErrorGeneric: "Non se puido gardar a votación.", + voteLocked: "Esta votación xa recibiu votos ou opinións, ou pertence ao Parlamento ou aos Tribunais: xa non se pode editar nin borrar.", + voteUpdateButton: "Actualizar", + voteDeleteButton: "Eliminar", + voteOptionYes: "SI", + voteOptionNo: "NON", + voteOptionAbstention: "ABSTENCIÓN", + voteConfused: "CONFUNDIDO", + voteFollowMajority: "SEGUIR A MAIORÍA", + voteNotInterested: "SEN INTERESE", + voteFilterAll: "TODAS", + voteFilterRecent: "RECENTES", + voteFilterMine: "MIÑAS", + voteFilterOpen: "ABERTAS", + voteFilterClosed: "PECHADAS", + voteQuestionLabel: "Pregunta", + voteDeadlineLabel: "Prazo", + voteOptionsLabel: "O teu voto", + voteBreakdown: "Desagregación", + voteFinalResult: "RESULTADO", + voteNoQuorum: "SEN QUÓRUM", + voteTagsLabel: "Etiquetas", + voteDeadline: "Prazo", + voteStatus: "Estado", + voteTags: "Etiquetas", + voteOpinions: "Opinións", + novotes: "Non hai propostas de votación dispoñibles.", + voteCreatedAt: "Creado o", + voteNoQuestion: "Non se proporcionou ningunha pregunta", + voteUnknownCreator: "Creador descoñecido", + voteUnknownDate: "Data descoñecida", + voteDetailSectionTitle: "Detalles da votación", + voteCommentsLabel: "Comentarios", + voteCommentsForumButton: "Abrir a conversa", + voteCommentsSectionTitle: "Conversa aberta", + voteNoCommentsYet: "Aínda non hai comentarios. Sé a primeira persoa en opinar.", + commentsGoToLast: "Ir ao último comentario", + voteNewCommentPlaceholder: "Escribe aquí o teu comentario…", + voteNewCommentButton: "Publicar o comentario", + voteNewCommentLabel: "Engade un comentario", + cvTitle: "CV", + cvEditSectionTitle: "Editar o CV", + cvCreateSectionTitle: "Crear un CV", + cvDescription: "Xestiona e comparte as túas habilidades profesionais e a túa información.", + cvNameLabel: "Nome completo", + cvDescriptionLabel: "Resumo", + cvPhotoLabel: "Foto", + cvPdfLabel: "CV (PDF)", + cvVisitButton: "Visitar o CV", + cvPersonalExperiencesLabel: "Experiencias persoais", + cvPersonalSkillsLabel: "Habilidades persoais (separadas por comas)", + cvOasisExperiencesLabel: "Experiencias de contribución en Oasis", + cvOasisSkillsLabel: "Habilidades de contribución en Oasis (separadas por comas)", + cvEducationExperiencesLabel: "Experiencias educativas", + cvEducationalSkillsLabel: "Habilidades educativas (separadas por comas)", + cvProfessionalExperiencesLabel: "Experiencias profesionais", + cvProfessionalSkillsLabel: "Habilidades profesionais (separadas por comas)", + cvLanguagesLabel: "Idiomas", + cvLocationLabel: "Localización", + cvStatusLabel: "Estado", + cvPreferencesLabel: "Preferencias", + cvPersonal: "Persoal", + cvOasis: "Contribuínte de Oasis (opcional)", + cvOasisContributorView: "Contribución", + cvEducational: "Educativo (opcional)", + cvEducationalView: "Educativo", + cvProfessional: "Profesional (opcional)", + cvProfessionalView: "Profesional", + cvAvailability: "Dispoñibilidade (opcional)", + cvUpdateButton: "Actualizar", + cvCreateButton: "Crear un CV", + cvCreatedAt: "Creado o", + cvUpdatedAt: "Actualizado o", + cvEditButton: "Actualizar", + cvDeleteButton: "Eliminar", + cvNoCV: "Non se atopou ningún CV.", + blogSubject: "Asunto", + blogMessage: "Mensaxe", + blogImage: "Subir contido multimedia (máx.: 75MB)", + blogMedia: "Anexos (ata 8 imaxes e un vídeo)", + blogPublish: "Publicar", + pollsTitle: "Enquisas", + dataTitle: "Coincidencias", + dataDescription: "Explora e visualiza as coincidencias da túa rede.", + dataFilterAll: "TODO", + dataFilterMine: "MEUS", + dataFilterRecent: "RECENTES", + dataFilterTop: "TOP", + dataKindInhabitants: "HABITANTES", + dataKindJobs: "EMPREGOS", + dataKindProjects: "PROXECTOS", + dataKindEvents: "EVENTOS", + dataKindTribes: "TRIBOS", + dataKindMarket: "MERCADO", + dataKindHousing: "VIVENDA", + dataKindIndustry: "INDUSTRIA", + dataKindTasks: "TAREFAS", + dataKindReports: "INFORMES", + dataKindVotes: "VOTACIÓNS", + dataSearchPlaceholder: "Buscar en Coincidencias...", + dataCohesionTitle: "Coeficiente de cohesión (CC)", + dataCcShort: "CC", + dataCohesionHint: "Afinidade media entre todo o que publicou a rede.", + dataAffinity: "Afinidade", + dataCommonTerms: "Palabra clave", + dataReasonFollowing: "Apoias a este habitante", + dataReasonTribe: "Comparte tribo contigo", + dataReasonPinned: "Coincide con algo que fixaches", + dataReasonCv: "Coincide coas habilidades do teu CV", + dataReasonContent: "Relacionado co que publicas", + dataReasonRelated: "Relacionado cos teus intereses", + dataReasonAlike: "Publicado por un habitante afín a ti", + dataReasonSupportsYou: "Apóiate", + dataReasonMutual: "Apoio mutuo", + dataReasonRated: "Ben valorado pola rede", + dataReasonNear: "Preto de ti", + dataStatPeople: "CV", + dataStatConnected: "Conectados", + dataStatSkills: "Habilidades", + dataBestMatch: "Mellor coincidencia", + dataStatIsolated: "Illados", + dataStatEntities: "Entidades comparadas", + dataStatTerms: "Termos distintos", + dataStatPairs: "Pares conectados", + dataMatchesTitle: "Coincidencias", + dataMatchesHint: "Suxestións personalizadas do algoritmo.", + dataTermsTitle: "Palabras clave", + dataTermsHint: "Os termos que aparecen en máis contidos.", + dataConnections: "Coincidencias ligadas", + dataNoMatches: "Non hai datos cruzados que amosar.", + dataNoProfile: "Publica contido ou escribe o teu CV para que o algoritmo aprenda que che interesa.", + pollsDescription: "Módulo para preguntarlle á rede e contar as respostas.", + pollFilterAll: "TODAS", + pollFilterMine: "MIÑAS", + pollFilterRecent: "RECENTES", + pollFilterTop: "TOP", + pollFilterVoted: "VOTADAS", + pollFilterOpen: "ABERTAS", + pollFilterClosed: "PECHADAS", + pollCreateButton: "Crear unha enquisa", + pollCreateTitle: "Crear unha enquisa", + pollEditTitle: "Editar a enquisa", + pollQuestion: "Pregunta", + pollQuestionPlaceholder: "Que queres preguntar?", + pollOptions: "Opcións (unha por liña)", + pollOutcome: "Resultado", + pollNoVotesYet: "Aínda sen votos", + pollTie: "Empate", + pollOptionsPlaceholder: "Praza\nParque\nBar", + pollAnonymous: "Anónima", + pollAnonymousLabel: "Enquisa anónima", + pollMultiple: "Resposta múltiple", + pollMultipleLabel: "Permitir varias respostas", + pollDeadline: "Data límite", + pollTags: "Etiquetas", + pollPublishButton: "Publicar a enquisa", + pollUpdateButton: "Actualizar", + pollCloseButton: "Pechar", + pollDeleteButton: "Eliminar", + pollVoteButton: "Votar", + pollChangeVote: "Cambiar o voto", + pollVoters: "Votantes", + pollComments: "Comentarios", + pollStatusLabel: "Estado", + pollStatusOpen: "ABERTA", + pollStatusClosed: "PECHADA", + pollSearchPlaceholder: "Buscar en Enquisas...", + pollsNoItems: "Non se atoparon enquisas.", + viewPoll: "Ver a enquisa", + pollChatCreate: "Crear unha enquisa", + pollInChat: "Enquisa", + blogTitle: "Blogs", + blogDescription: "Módulo para descubrir e xestionar blogs.", + blogFilterAll: "TODOS", + blogFilterMine: "MEUS", + blogFilterRecent: "RECENTES", + blogFilterFavorites: "FAVORITOS", + blogFilterTop: "TOP", + blogCreateButton: "Crear un blog", + blogSearchPlaceholder: "Buscar en Blogs...", + blogComments: "Comentarios", + blogAllowComments: "Permitir comentarios", + blogCommentsClosed: "O autor pechou os comentarios deste blog.", + blogNoItems: "Non se atoparon blogs.", + viewBlog: "Ver o blog", + forumTitle: "Foros", + forumCategoryLabel: "Categoría", + forumTitleLabel: "Título", + forumTitlePlaceholder: "Título do foro...", + forumCreateButton: "Crear un foro", + forumCreateSectionTitle: "Crear un foro", + forumDescription: "Fala abertamente con habitantes da túa rede.", + forumSearchPlaceholder: "Buscar en Foros...", + forumFilterAll: "TODOS", + forumFilterMine: "MEUS", + forumFilterRecent: "RECENTES", + forumFilterTop: "TOP", + forumParticipants: "participantes", + forumMessages: "mensaxes", + forumMessageLabel: "Mensaxe", + forumMessagePlaceholder: "Escribe a túa mensaxe...", + forumSendButton: "Enviar", + noForums: "Aínda non hai foros dispoñibles.", + forumVisitButton: "Visitar o foro", + forumTopicLabel: "Tema", + imageTitle: "Imaxes", + imageDescription: "Explora e xestiona contido de imaxes na túa rede.", + imageMineSectionTitle: "As túas imaxes", + imageCreateSectionTitle: "Subir unha imaxe", + imageUpdateSectionTitle: "Actualizar a imaxe", + imageAllSectionTitle: "Imaxes", + imageRecentSectionTitle: "Imaxes recentes", + imageTopSectionTitle: "Imaxes top", + imageFavoritesSectionTitle: "Favoritos", + imageGallerySectionTitle: "Galería", + imageFilterAll: "TODAS", + imageFilterMine: "MIÑAS", + imageFilterRecent: "RECENTES", + imageFilterTop: "TOP", + imageFilterFavorites: "FAVORITOS", + imageFilterGallery: "GALERÍA", + imageCreateButton: "Subir unha imaxe", + imageUpdateButton: "Actualizar", + imageAddFavoriteButton: "Engadir a favoritos", + imageRemoveFavoriteButton: "Quitar de favoritos", + imageFileLabel: "Selecciona un ficheiro de imaxe (.jpeg, .jpg, .png, .gif)", + imageTagsLabel: "Etiquetas", + imageTagsPlaceholder: "Introduce etiquetas separadas por comas", + imageTitleLabel: "Título", + imageTitlePlaceholder: "Opcional", + imageDescriptionLabel: "Descrición", + imageDescriptionPlaceholder: "Opcional", + imageNoFile: "Non se proporcionou ningún ficheiro de imaxe", + noImages: "Non hai imaxes dispoñibles.", + imageSearchPlaceholder: "Buscar en Imaxes...", + imageSortRecent: "Máis recentes", + imageSortOldest: "Máis antigas", + imageSortTop: "Máis votadas", + imageSearchButton: "Buscar", + imageUpdatedAt: "Actualizado", + imageNoMatch: "Ningunha imaxe coincide coa túa busca.", + feedSearchPlaceholder: "Buscar en Feed...", + feedTitle: "Feed", + feedDetailTitle: "Feed", + feedOpenDiscussion: "Abrir o debate", + feedPostComment: "Publicar o comentario", + noComments: "Aínda non hai comentarios", + createFeedTitle: "Crear un feed", + createFeedButton: "Enviar o feed!", + feedPlaceholder: "Que está a pasar? (máximo 280 caracteres)", + TODAYButton: "HOXE", + totalOpinions: "Total de opinións", + moreVoted: "Máis votado", + noFeedsFound: "Non se atoparon feeds.", + createdAtLabel: "Creado o", + FeedshareYourOpinions: "Descobre e comparte textos breves na túa rede.", + refeedButton: "Reenviar", + alreadyRefeeded: "Xa realimentaches isto.", + activityTitle: "Actividade", + yourActivity: "A túa actividade", + globalActivity: "Actividade", + activityList: "Actividade", + activityDesc: "Consulta a actividade máis recente da túa rede.", + allButton: "TODO", + mineButton: "MIÑAS", + noActions: "Non hai actividade dispoñible.", + from: "De", + to: "A", + amount: "Cantidade", + concept: "Concepto", + description: "Descrición", + meme: "Meme", + typeRecent: "RECENTES", + typeTop: "TOP", + typePost: "BLOG", + activityUpdateLabel: "ACTUALIZACIÓN", + typeTribe: "TRIBOS", + typeLarp: "L.A.R.P.", + typeInhabitants: "HABITANTES", + typeAbout: "HABITANTES", + typeCurriculum: "CV", + typeImage: "IMAXES", + typeBookmark: "MARCADORES", + typeDocument: "DOCUMENTOS", + typeVotes: "VOTACIÓNS", + typeAudio: "AUDIOS", + typeMarket: "MERCADO", + typeJob: "EMPREGOS", + typeProject: "PROXECTOS", + typeVideo: "VÍDEOS", + typeVote: "DIFUSIÓN", + typeEvent: "EVENTOS", + typeTransfer: "TRANSFERENCIAS", + typeTask: "TAREFAS", + typePixelia: "PIXELIA", + typeForum: "FOROS", + typeReport: "INFORMES", + typeFeed: "FEEDS", + typeContact: "CONTACTO", + typePub: "PUB", + typeTombstone: "ELIMINADO", + typeBanking: "BANCA", + typeBankWallet: "BANCA/CARTEIRA", + typeBankClaim: "BANCA/RBU", + typeKarmaScore: "KARMA", + typeParliament: "PARLAMENTO", + typeSpread: "RÉPLICAS", + typeParliamentCandidature: "Parlamento · Candidatura", + typeParliamentTerm: "Parlamento · Mandato", + typeParliamentProposal:"Parlamento · Proposta", + typeParliamentRevocation:"Parlamento · Revogación", + typeParliamentLaw: "Parlamento · Nova lei", + typeCourts: "TRIBUNAIS", + typeCourtsCase: "Tribunais · Caso", + typeCourtsEvidence: "Tribunais · Proba", + typeCourtsAnswer: "Tribunais · Resposta", + typeCourtsVerdict: "Tribunais · Veredicto", + typeCourtsSettlement: "Tribunais · Acordo", + typeCourtsSettlementProposal: "Tribunais · Proposta de acordo", + typeCourtsSettlementAccepted: "Tribunais · Acordo aceptado", + typeCourtsNomination: "Tribunais · Nomeamento", + typeCourtsNominationVote: "Tribunais · Votación de nomeamento", + question: "Pregunta", + deadline: "Data límite", + status: "Estado", + votes: "Votos", + totalVotes: "Votos totais", + voteTotalVotes: "Votos totais", + name: "Nome", + skills: "Habilidades", + tags: "Etiquetas", + title: "Título", + date: "Data", + category: "Categoría", + attendees: "Asistentes", + location: "Localización", + contentWarning: "Asunto", + bankWalletConnected: "Carteira ECOin", + bankUbiReceived: "RBU recibida", + bankTx: "Tx", + bankEpochShort: "Época", + viewDetails: "Ver os detalles", + link: "Ligazón", + activityProjectFollow: "%OASIS% agora está %ACTION% este proxecto %PROJECT%", + activityProjectUnfollow: "%OASIS% agora está %ACTION% este proxecto %PROJECT%", + following: "SEGUINDO", + unfollowing: "DEIXANDO DE SEGUIR", + pledged: "ACHEGADO", + parliamentCandidatureId: "Candidatura", + parliamentGovMethod: "Método", + parliamentVotesReceived: "Votos recibidos", + parliamentMethodANARCHY: "Anarquía", + parliamentMethodVOTE: "Votación comunitaria", + parliamentMethodRANKED: "Voto preferencial", + parliamentMethodPLURALITY: "Pluralidade", + parliamentMethodCOUNCIL: "Consello", + parliamentMethodJURY: "Xurado", + parliamentAnarchy: "ANARQUÍA", + parliamentElectionsStart: "Inicio das eleccións", + parliamentElectionsEnd: "Fin das eleccións", + parliamentCurrentLeader: "Candidatura gañadora", + parliamentProposalTitle: "Título", + parliamentOpenVote: "Votación aberta", + parliamentStatus: "Estado", + parliamentLawQuestion: "Pregunta", + parliamentLawMethod: "Método", + parliamentLawProposer: "Proponente", + parliamentLawEnacted: "Promulgada", + parliamentLawVotes: "Votos", + createdAt: "Creado o", + courtsCaseTitle: "Caso", + courtsMethod: "Método", + courtsMethodJUDGE: "Xuíz", + courtsMethodJUDGES: "Panel de xuíces", + courtsMethodSINGLE_JUDGE: "Xuíz único", + courtsMethodJURY: "Xurado", + courtsMethodCOUNCIL: "Consello", + courtsMethodCOMMUNITY: "Comunidade", + courtsMethodARBITRATION: "Arbitraxe", + courtsMethodVOTE: "Votación comunitaria", + courtsAccuser: "Acusación", + courtsRespondent: "Defensa", + courtsThStatus: "Estado", + courtsThAnswerBy: "Responder antes de", + courtsThEvidenceBy: "Probas ata", + courtsThDecisionBy: "Decisión antes de", + courtsVotesNeeded: "Votos necesarios", + courtsVotesSlashTotal: "SI/TOTAL", + courtsOpenVote: "Votación aberta", + courtsAnswerTitle: "Resposta", + courtsVerdictResult: "Resultado", + courtsVerdictOrders: "Ordes", + courtsSettlementText: "Acordo", + courtsSettlementAccepted: "Aceptado", + courtsJudge: "Xuíz", + courtsThSupports: "Apoios", + courtsFilterOpenCase: "Abrir un caso", + courtsCaseMediatorsPh: "IDs de Oasis dos mediadores, separados por comas", + courtsMediatorsLabel: "Mediadores", + courtsThCase: "Caso", + courtsThCreatedAt: "Data de inicio", + courtsPublicPrefLabel: "Visibilidade tras a resolución", + courtsPublicPrefYes: "Acepto que este caso sexa totalmente público", + courtsPublicPrefNo: "Prefiro manter privados os detalles", + courtsPublicPrefSubmit: "Gardar a preferencia de visibilidade", + courtsMethodMEDIATION: "Mediación", + courtsNoCases: "Non hai casos.", + reportsTitle: "Informes", + reportsDescription: "Xestiona e segue os informes da túa rede.", + reportsSearchPlaceholder: "Buscar en Informes...", + reportsFilterAll: "TODOS", + reportsFilterRecent: "RECENTES", + reportsFilterTop: "TOP", + reportsFilterMine: "MEUS", + reportsFilterFeatures: "FUNCIÓNS", + reportsFilterBugs: "ERROS", + reportsFilterAbuse: "ABUSOS", + reportsFilterContent: "CONTIDO", + reportsFilterConfirmed: "CONFIRMADOS", + reportsFilterResolved: "RESOLTOS", + reportsFilterOpen: "ABERTOS", + reportsFilterUnderReview: "EN REVISIÓN", + reportsFilterInvalid: "NON VÁLIDOS", + reportsCreateButton: "Crear un informe", + reportsTitleLabel: "Título", + reportsCategory: "Categoría", + reportsCategoryFeatures: "Funcións", + reportsCategoryBugs: "Erros", + reportsCategoryAbuse: "Abuso", + reportsCategoryContent: "Contido", + reportsUpdateButton: "Actualizar", + reportsDeleteButton: "Eliminar", + reportsMineSectionTitle: "Os teus informes", + reportsFeaturesSectionTitle: "Solicitudes de funcións", + reportsBugsSectionTitle: "Erros", + reportsAbuseSectionTitle: "Informes de abuso", + reportsContentSectionTitle: "Problemas de contido", + reportsAllSectionTitle: "Informes", + reportsNoItems: "Non hai informes dispoñibles.", + reportsSeverity: "Gravidade", + reportsSeverityLow: "Baixa", + reportsSeverityMedium: "Media", + reportsSeverityHigh: "Alta", + reportsSeverityCritical: "Crítica", + reportsStatus: "Estado", + reportsStatusOpen: "Aberto", + reportsStatusUnderReview: "En revisión", + reportsStatusResolved: "Resolto", + reportsStatusInvalid: "Non válido", + reportsConfirmButton: "CONFIRMAR O INFORME!", + reportsConfirmations: "Confirmacións", + reportsConfirmedSectionTitle: "Informes confirmados", + reportsCreateTaskButton: "Crear unha tarefa", + reportsOpenSectionTitle: "Informes abertos", + reportsUnderReviewSectionTitle: "Informes en revisión", + reportsResolvedSectionTitle: "Informes resoltos", + reportsInvalidSectionTitle: "Informes non válidos", + reportsBugTemplateTitle: "Datos para reproducir (erros)", + reportsFeatureTemplateTitle: "Detalles (funcións)", + reportsAbuseTemplateTitle: "Detalles (abuso)", + reportsContentTemplateTitle: "Detalles (problemas de contido)", + reportsStepsToReproduceLabel: "Pasos para reproducilo", + reportsStepsToReproducePlaceholder: "1) ...\n2) ...\n3) ...", + reportsExpectedBehaviorLabel: "Resultado esperado", + reportsExpectedBehaviorPlaceholder: "Que debería pasar?", + reportsActualBehaviorLabel: "Resultado real", + reportsActualBehaviorPlaceholder: "Que pasa realmente?", + reportsEnvironmentLabel: "Contorno", + reportsEnvironmentPlaceholder: "Versión, dispositivo, SO, navegador, configuración, rexistros, etc.", + reportsReproduceRateLabel: "Frecuencia", + reportsReproduceRateAlways: "Sempre", + reportsReproduceRateOften: "A miúdo", + reportsReproduceRateSometimes: "Ás veces", + reportsReproduceRateRarely: "Raramente", + reportsReproduceRateUnable: "Non se pode reproducir", + reportsReproduceRateUnknown: "Non especificado", + reportsProblemStatementLabel: "Problema / necesidade", + reportsProblemStatementPlaceholder: "Que problema resolve esta función?", + reportsUserStoryLabel: "Historia de habitante", + reportsUserStoryPlaceholder: "Como , quero , para .", + reportsAcceptanceCriteriaLabel: "Criterios de aceptación", + reportsAcceptanceCriteriaPlaceholder: "- Dado...\n- Cando...\n- Entón...", + reportsWhatHappenedLabel: "Que ocorreu?", + reportsWhatHappenedPlaceholder: "Describe o incidente con contexto e datas aproximadas.", + reportsReportedUserLabel: "Habitante / entidade denunciada", + reportsReportedUserPlaceholder: "@habitante, feed, ID, ligazón, etc.", + reportsEvidenceLinksLabel: "Probas / ligazóns", + reportsEvidenceLinksPlaceholder: "Pega ligazóns, IDs de mensaxes, capturas (se procede), etc.", + reportsContentLocationLabel: "Onde está o contido", + reportsContentLocationPlaceholder: "Ligazón, ID, canle, fío, autor, etc.", + reportsWhyInappropriatePlaceholder: "Explica o motivo e o impacto.", + reportsRequestedActionLabel: "Acción solicitada", + reportsRequestedActionPlaceholder: "Eliminar, agochar, marcar, advertir, etc.", + tribesTitle: "Tribos", + tribeFilterAll: "TODAS", + tribeFilterRecent: "RECENTES", + tribeFilterMine: "MIÑAS", + tribeFilterMembership: "PERTENZA", + tribeFilterSubtribes: "SUBTRIBOS", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALERÍA", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "MEUS", + tribeFeedFilterALL: "TODOS", + tribeFeedFilterRECENT: "RECENTES", + courtsStanceDENY: "Negar", + courtsStanceADMIT: "Admitir", + courtsStancePARTIAL: "Admitir en parte", + tribeAllSectionTitle: "Tribos", + tribeMineSectionTitle: "As túas tribos", + tribeCreateSectionTitle: "Crear unha tribo", + tribeUpdateSectionTitle: "Actualizar a tribo", + tribeGallerySectionTitle: "Galería de tribos", + tribeRecentSectionTitle: "Tribos recentes", + tribeTopSectionTitle: "Tribos populares", + tribeRootLabel: "RAÍZ", + tribeDescription: "Explora ou crea tribos na túa rede.", + tribeMainTribeLabel: "TRIBO PRINCIPAL", + tribeCreateButton: "Crear unha tribo", + tribeUpdateButton: "Actualizar", + tribeDeleteButton: "Eliminar", + tribeImageLabel: "Subir contido multimedia (máx.: 75MB)", + tribeTitleLabel: "Título", + searchTribesPlaceholder: "Buscar en Tribos...", + tribeTitlePlaceholder: "Nome da tribo", + tribeDescriptionLabel: "Descrición", + tribeDescriptionPlaceholder: "Describe esta tribo", + tribeLocationLabel: "Localización", + tribeLocationPlaceholder: "Onde está situada esta tribo?", + tribeTagsLabel: "Etiquetas", + tribeTagsPlaceholder: "Introduce etiquetas separadas por comas", + tribeInviteMode: "Modo de convite", + tribeModeLabel: "MODO", + tribeIsAnonymousLabel: "ESTADO", + tribeMembersCount: "Membros", + tribeEnterInvite: "INTRODUCIR O CONVITE", + subscriptionTitle: "Subscricións", + subscriptionOn: "Subscrito", + subscriptionOff: "Non subscrito", + subscriptionSubscribe: "Subscribirse", + subscriptionUnsubscribe: "Cancelar a subscrición", + subscriptionSubscribers: "Subscritores", + pmListLabel: "Enviar a", + pmListNone: "— Sen lista —", + pmListNoSubscribers: "Sen subscritores", + tribeLeaveButton: "SAÍR", + tribePublic: "PÚBLICA", + tribePrivate: "PRIVADA", + tribeGenerateInvite: "CONVITE ÚNICO", + tribeOpenInvitation: "CONVITE ABERTO", + tribeJoinOpen: "UNIRSE Á TRIBO", + tribeRemoveInvitation: "ELIMINAR O CONVITE", + tribeCreatedAt: "Creado o", + tribeAuthor: "Por", + tribeStrict: "Estrito", + tribeOpen: "Aberta", + tribeFeedRefeeds: "Redifusións", + tribeFeedRefeed: "Redifundir", + tribeFeedMessagePlaceholder: "Escribe un feed…", + tribeFeedSend: "Enviar", + tribeFeedEmpty: "Aínda non hai mensaxes de feed dispoñibles.", + noTribes: "Aínda non se atoparon tribos.", + tribeNotFound: "Non se atopou a tribo!", + createTribeTitle: "Crear unha tribo", + updateTribeTitle: "Actualizar a tribo", + tribeSectionInhabitants: "Habitantes", + tribeSectionVotations: "VOTACIÓNS", + tribeSectionEvents: "EVENTOS", + tribeSectionTasks: "TAREFAS", + tribeSectionFeed: "FEED", + tribeSectionForum: "FORO", + tribeSectionMedia: "Multimedia", + tribeSectionImages: "IMAXES", + tribeSectionAudios: "AUDIOS", + tribeSectionVideos: "VÍDEOS", + tribeSectionDocuments: "DOCUMENTOS", + tribeSectionBookmarks: "MARCADORES", + tribeSectionMaps: "MAPAS", + tribeSectionPads: "PADS", + tribeSectionChats: "CHATS", + tribeSectionCalendars: "CALENDARIOS", + tribePadCreate: "Crear un pad", + tribeChatCreate: "Crear un chat", + tribeCalendarCreate: "Crear un calendario", + tribePadsEmpty: "Aínda non hai pads.", + tribeChatsEmpty: "Aínda non hai chats.", + tribeCalendarsEmpty: "Aínda non hai calendarios.", + tribeInhabitantsEmpty: "Aínda non hai habitantes nesta tribo.", + tribeEventCreate: "Crear un evento", + tribeEventsEmpty: "Aínda non hai eventos.", + tribeEventTitle: "Título", + tribeEventDescription: "Descrición", + tribeEventDate: "Data", + tribeEventLocation: "Localización", + tribeEventAttend: "ASISTIR", + tribeEventUnattend: "NON ASISTIR", + tribeEventAttendees: "Asistentes", + tribeTaskCreate: "Crear unha tarefa", + tribeTasksEmpty: "Aínda non hai tarefas.", + tribeTaskTitle: "Título", + tribeTaskDescription: "Descrición", + tribeTaskPriority: "Prioridade", + tribeTaskDeadline: "Data límite", + tribeTaskAssignees: "Asignados", + tribeTaskStatusInProgress: "EN CURSO", + tribeTaskStatusClosed: "PECHAR", + tribeTaskAssign: "ASIGNAR", + tribeTaskUnassign: "DESASIGNAR", + tribeVotationCreate: "Crear unha votación", + tribeVotationsEmpty: "Aínda non hai votacións.", + tribeVotationTitle: "Título", + tribeVotationDescription: "Descrición", + tribeVotationOptions: "Opcións", + tribeVotationDeadline: "Data límite", + tribeVotationVote: "VOTAR", + tribeVotationResults: "Votos", + tribeVotationClose: "PECHAR A VOTACIÓN", + tribeVotationOptionPlaceholder: "Opción", + tribeForumCreate: "Crear un foro", + tribeForumEmpty: "Aínda non hai fíos.", + tribeForumTitle: "Título", + tribeForumText: "Mensaxe", + tribeForumCategory: "Categoría", + tribeForumReply: "Responder", + tribeForumReplies: "Respostas", + tribeMediaUpload: "Subir multimedia", + readDocument: "Ler o documento", + tribeCreateImage: "Crear unha imaxe", + tribeCreateAudio: "Crear un audio", + tribeCreateVideo: "Crear un vídeo", + tribeCreateDocument: "Crear un documento", + tribeCreateBookmark: "Crear un marcador", + tribeMediaEmpty: "Aínda non hai multimedia.", + tribeMediaTitle: "Título", + tribeMediaDescription: "Descrición", + tribeMediaTypeImage: "Imaxe", + tribeMediaTypeVideo: "Vídeo", + tribeMediaTypeAudio: "Audio", + tribeMediaTypeDocument: "Documento", + tribeMediaTypeBookmark: "Marcador", + tribeContentDelete: "ELIMINAR", + tribeReachTribe: "TRIBO", + tribeReachNotice: "OASIS amósao a todos os habitantes e CLEARNET tamén na web aberta. Abrilo deixa unha copia lexible fóra da tribo: volver a TRIBO agóchao, pero as copias xa compartidas non se poden borrar.", + tribeReachPrivate: "O contido dunha tribo privada só pode quedar dentro da tribo.", + tribeReachOpenedBy: "Aberto por", + tribeReachChange: "Onde se publica", + tribeInviteCodeText: "Código de convite: ", + oasisVersionLabel: "Versión de Oasis", + tribeInviteCodeHint: "Comparte este código con quen queiras convidar. Pode unirse desde /invites.", + tribeStatusOpen: "ABERTO", + tribeStatusClosed: "PECHADO", + tribeStatusInProgress: "EN CURSO", + tribePriorityLow: "BAIXA", + tribePriorityMedium: "MEDIA", + tribePriorityHigh: "ALTA", + tribePriorityCritical: "CRÍTICA", + tribeTaskFilterAll: "TODAS", + tribeMediaFilterAll: "TODOS", + tribeForumCatGeneral: "XERAL", + tribeForumCatProposal: "PROPOSTA", + tribeForumCatQuestion: "PREGUNTA", + tribeForumCatAnnouncement: "ANUNCIO", + tribeStatusLabel: "Estado", + tribeSubTribes: "SUBTRIBOS", + tribeSubTribesCreate: "Crear unha subtribo", + tribeSubTribesStrictDenied: "O modo estrito da tribo non che permite crear novas subtribos. Ponte en contacto co administrador.", + tribeActivityRefeed: "REFEED", + tribeSectionActivity: "ACTIVIDADE", + tribeSectionTrending: "TENDENCIAS", + tribeSectionTags: "ETIQUETAS", + tribeSectionSearch: "BUSCAR", + tribeActivityEmpty: "Aínda non hai actividade.", + tribeActivityCreated: "creou", + tribeActivityPosted: "publicou", + tribeActivityReplied: "respondeu", + tribeTrendingEmpty: "Aínda non hai contido en tendencia.", + tribeTrendingPeriodDay: "Hoxe", + tribeTrendingPeriodWeek: "Esta semana", + tribeTrendingPeriodAll: "Todo", + tribeTrendingEngagement: "interacción", + tribeTopCategory: "Máis votado", + tribeTagsEmpty: "Non se atoparon etiquetas.", + tribeSearchEmpty: "Sen resultados.", + tribeSearchMinChars: "Introduce polo menos 2 caracteres para buscar.", + agendaTitle: "Axenda", + agendaDescription: "Aquí podes atopar todos os teus elementos asignados.", + agendaSearchPlaceholder: "Buscar na Axenda...", + agendaFilterAll: "TODOS", + agendaFilterToday: "HOXE", + agendaFilterUpcoming: "PRÓXIMOS", + agendaFilterOverdue: "VENCIDOS", + agendaFilterOpen: "ABERTOS", + agendaFilterClosed: "PECHADOS", + agendaFilterTasks: "TAREFAS", + agendaFilterMarket: "MERCADO", + agendaFilterTribes: "TRIBOS", + agendaFilterEvents: "EVENTOS", + agendaFilterReports: "INFORMES", + agendaFilterTransfers: "TRANSFERENCIAS", + agendaFilterJobs: "EMPREGOS", + agendaFilterIndustry: "INDUSTRIA", + agendaFilterHousing: "VIVENDA", + agendaFilterProjects: "PROXECTOS", + agendaFilterCalendars: "CALENDARIOS", + agendaNoItems: "Non se atoparon asignacións.", + agendaDiscardButton: "Descartar", + agendaRestoreButton: "Restaurar", + agendaRemoveButton: "Quitar", + agendaMembersCount: "Membros", + agendaLocationLabel: "Localización", + agendaYes: "SI", + agendaNo: "NON", + agendaInviteModeLabel: "Modo de convite", + agendaAnonymousLabel: "Modo anónimo", + agendaMembersLabel: "Membros", + agendareportCategory: "Categoría", + agendareportSeverity: "Gravidade", + agendareportStatus: "Estado", + agendaTransferConcept: "Concepto", + agendaTransferAmount: "Cantidade", + agendaTransferDeadline: "Prazo", + opinionsTitle: "Opinións", + shareYourOpinions: "Descobre e vota opinións na túa rede.", + author: "Por", + voteNow: "Vota agora", + alreadyVoted: "Xa opinaches.", + noOpinionsFound: "Non se atoparon opinións.", + TOPButton: "TOP", + opinionsTotalCount: "Total de opinións", + voteInteresting: "Interesante", + voteNecessary: "Necesario", + voteUseful: "Útil", + voteInformative: "Informativo", + voteWellResearched: "Ben investigado", + voteNeedsSources: "Necesita fontes", + voteWrong: "Incorrecto", + voteLowQuality: "Baixa calidade", + voteLove: "Amor", + voteClear: "Claro", + voteMisleading: "Enganoso", + voteOffTopic: "Fóra de tema", + voteDuplicate: "Duplicado", + voteClickbait: "Cebo de clics", + votePropaganda: "Propaganda", + voteFunny: "Divertido", + voteInspiring: "Inspirador", + voteUplifting: "Alentador", + voteUnnecessary: "Innecesario", + voteRejected: "Rexeitado", + voteConfusing: "Confuso", + voteTroll: "Troll", + voteNsfw: "NSFW", + voteViolent: "Violento", + voteToxic: "Tóxico", + voteHarassment: "Acoso", + voteHate: "Odio", + voteScam: "Estafa", + voteTriggering: "Provocador", + voteInsightful: "Perspicaz", + voteAccurate: "Preciso", + voteActionable: "Aplicable", + voteCreative: "Creativo", + voteSpam: "Spam", + voteAdultOnly: "Só adultos", + privateMessage: "MP", + pmSendTitle: "Mensaxes privadas", + pmSend: "Enviar!", + pmCancel: "Cancelar", + pmReset: "Restablecer", + pmSharedKeyHint: "Comparte esta chave co destinatario por outra canle. É necesaria para descifrar.", + pmDescription: "Usa este formulario para enviar unha mensaxe/ficheiro cifrado a outros habitantes.", + pmComposeTitle: "Enviar unha mensaxe", + pmRecipients: "Destinatarios", + pmRecipientsHint: "Introduce os IDs de Oasis separados por comas", + pmLimitsHint: "Ata 7 habitantes por mensaxe.", + pmTextPlaceholder: "Escribe a túa mensaxe.", + pmSubject: "Asunto", + pmSubjectHint: "Introduce o asunto da mensaxe", + pmText: "Mensaxe", + pmCrypterChip: "2xE2E", + pmCrypterDecryptButton: "Descifrar", + fileShareTitle: "Compartir un ficheiro", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "Ficheiro", + fileShareDownload: "Descargar", + fileShareMutualError: "Só podes compartir ficheiros con habitantes que se apoien mutuamente contigo.", + fileShareNoFile: "Non se seleccionou ningún ficheiro.", + fileShareTooLarge: "O ficheiro supera o tamaño permitido.", + fileShareFailed: "Non se puido preparar o ficheiro.", + fileShareSendError: "Non se puido enviar o ficheiro.", + fileShareUnavailable: "Este ficheiro non está dispoñible agora mesmo. Téntao máis tarde.", + pmCrypterBadKey: "A chave compartida é incorrecta!", + pmCrypterTooLong: "A mensaxe é demasiado longa para cifrala co Crypter. Acúrtaa e téntao de novo.", + pmCrypterCipherLabel: "Mensaxe recifrada", + pmCrypterCharsLabel: "caracteres", + pmInvalidRecipients: "Oasis ID non válido (debería ser como @…=.ed25519).", + pmEncryptionLabel: "Cifrado", + private: "Buzón", + privateDescription: "Xestiona as túas mensaxes privadas.", + privateInbox: "BUZÓN", + privateSent: "ENVIADAS", + privateDelete: "Borrar", + pmCreateButton: "Escribir unha MP", + noPrivateMessages: "Non hai mensaxes privadas.", + pmReply: "Responder", + pmReplies: "respostas", + inReplyTo: "EN RESPOSTA A", + forumInThread: "NO FÍO", + pmPreview: "Previsualizar", + pmPreviewTitle: "Vista previa", + performed: "→", + pmFromLabel: "De:", + pmToLabel: "Para:", + pmNoSubject: "(sen asunto)", + pmSubjectLabel: "Asunto:", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "A casa gobernante do L.A.R.P cambiou.", + politicalBotParliamentTitle: "Comezou un novo ciclo de goberno no Parlamento.", + politicalBotTribeTitle: "Comezou un novo ciclo de goberno nunha das túas tribos.", + politicalBotLarpIntro: "Agora goberna {house} durante este ciclo: {cycle}", + politicalBotParliamentIntro: "O goberno actual é {gov}", + politicalBotParliamentIntroLeader: "O goberno actual é {gov}, exercido por {leader}", + politicalBotTribeIntro: "O goberno actual na tribo {tribe} é {gov}", + politicalBotTribeIntroLeader: "O goberno actual na tribo {tribe} é {gov}, exercido por {leader}", + politicalBotLeaderLabel: "Líder", + inboxJobSubscribedTitle: "Nova subscrición á túa oferta de emprego", + pmInhabitantWithId: "Habitante con ID OASIS:", + pmHasSubscribedToYourJobOffer: "subscribiuse á túa oferta de emprego", + inboxMarketItemSoldTitle: "Artigo vendido", + inboxShopSoldTitle: "Produto vendido", + pmYourItem: "O teu artigo", + pmHasBeenSoldTo: "vendeuse a", + pmFor: "por", + inboxProjectPledgedTitle: "Nova achega ao teu proxecto", + pmHasPledged: "achegou", + pmToYourProject: "ao teu proxecto", + blockchain: "Explorador", + blockchainDescription: "Explora e visualiza os bloques da cadea.", + blockchainNoBlocks: "Non se atoparon bloques na cadea.", + blockchainStatsTitle: "Estatísticas", + blockchainStatsTypeBreakdown: "Desagregación por tipo", + blockchainStatsTopAuthors: "Autores principais", + blockchainStatsCount: "Bloques", + blockchainBlockID: "ID do bloque", + blockchainBlockAuthor: "Autoría", + blockchainBlockCarbon: "Pegada de carbono", + blockchainBlockType: "Tipo", + blockchainBlockTimestamp: "Marca de tempo", + blockchainLatestDatagram: "Último datagrama", + blockchainDatagram: "Datagrama", + blockchainDetails: "Ver os detalles do bloque", + blockchainViewBlockexplorer: "Ver en Blockexplorer", + blockchainBack: "Volver a Blockexplorer", + banking: "Banca", + bankingTitle: "Banca", + bankingDescription: "A economía colectiva de ECOin.", + bankOverview: "Resumo", + bankEpochs: "Épocas", + bankRules: "Regras", + pending: "Pendentes", + closed: "Pechadas", + bankClaimNow: "Reclamar", + bankClaimUBI: "Reclamar RBU", + bankClaimAndPay: "Reclamar e pagar", + bankClaimedPending: "Reclamación pendente...", + bankStatusClaimed: "Reclamado", + bankStatusExpired: "Caducado", + bankPubOnly: "Operación só para PUBs", + bankEpoch: "Época", + bankPool: "Fondo (esta época)", + bankWeightsSum: "Suma de pesos", + bankNoAllocations: "Non se atoparon asignacións.", + bankNoEpochs: "Non se atoparon épocas.", + bankEpochAllocations: "Asignacións da época", + bankAllocId: "ID de asignación", + bankAllocDate: "Data", + bankAllocConcept: "Concepto", + bankAllocFrom: "De", + bankAllocTo: "Para", + bankAllocAmount: "Cantidade", + bankAllocStatus: "Estado", + bankEpochId: "ID de época", + bankRuleHash: "Hash da instantánea de regras", + bankViewEpoch: "Ver a época", + bankYourIndustryBalance: "A túa parte da industria", + bankYourUbiMonth: "A túa RBU (este mes)", + bankYourFundsMonth: "Fondos totais", + bankIndustryBalance: "Produción industrial", + bankUserBalance: "O teu saldo", + bankAddresses: "Enderezos", + bankNoAddresses: "Non se atoparon enderezos.", + bankUser: "ID de Oasis", + bankAddress: "Enderezo", + bankAddAddressTitle: "Engadir un enderezo de ECOin", + bankAddAddressUser: "ID de Oasis", + bankAddAddressLabel: "Etiqueta (opcional)", + bankAddAddressLabelPlaceholder: "Carteira de Bob, tenda de Alice…", + bankAddAddressUserOptional: "Oasis ID (opcional)", + bankAddressUnnamed: "Enderezo sen nome", + bankAddAddressAddress: "Enderezo de ECOin", + bankAddAddressSave: "Gardar", + bankAddressAdded: "Enderezo engadido", + bankAddressUpdated: "Enderezo actualizado", + bankAddressExists: "O enderezo xa existe", + bankAddressInvalid: "Enderezo non válido", + bankAddressDeleted: "Enderezo eliminado", + bankAddressNotFound: "Non se atopou o enderezo", + bankAddressForbidden: "Só podes configurar o teu propio enderezo de cobro", + bankAddressTotal: "Total de enderezos", + bankAddressSearch: "Buscar @habitante ou enderezo", + bankAddressActions: "Accións", + bankAddressDelete: "Eliminar", + bankAddressSource: "Fonte", + bankAddressDeleteConfirm: "Queres eliminar este enderezo?", + search: "Buscar!", + bankLocal: "Local", + bankFromOasis: "Rede", + pubIdLabel: "PUB ID", + bankUbiAvailableNo: "SEN FONDOS!", + bankUbiAvailableOk: "DISPOÑIBLE!", + bankUbiAvailability: "RBU", + bankAlreadyClaimedThisMonth: "Xa reclamada este mes", + bankUbiThisMonth: "RBU (este mes)", + bankUbiLastClaimed: "RBU (última reclamada)", + bankUbiNeverClaimed: "Nunca reclamada", + bankUbiTotalClaimed: "RBU (total reclamado)", + bankUbiPub: "PUB", + bankUbiInhabitant: "HABITANTE", + bankUbiClaimedAmount: "RECLAMADO (ECO)", + typeBankUbiResult: "BANCA - RBU", + bankNoPubConfigured: "Non hai ningún PUB configurado. Establece o ID do teu PUB en Configuración.", + shopsTitle: "Tendas", + shopDescription: "Descobre e xestiona tendas na rede.", + shopTitle: "Tenda", + shopUpload: "Crear unha tenda", + shopAllSectionTitle: "Tendas", + shopMineSectionTitle: "As miñas tendas", + shopRecentSectionTitle: "Tendas recentes", + shopTopSectionTitle: "Tendas destacadas", + shopProductsSectionTitle: "Produtos destacados", + shopPricesSectionTitle: "Produtos por prezo", + shopFavoritesSectionTitle: "Favoritas", + shopCreateSectionTitle: "Crear unha tenda", + shopUpdateSectionTitle: "Actualizar a tenda", + shopCreate: "Crear", + shopUpdate: "Actualizar", + shopDelete: "Eliminar", + shopReachClearnet: "Clearnet", + shopReachOasis: "Oasis", + encryptedChipLabel: "E2E", + transportChipLabel: "Cifrado en tránsito", + shopAddFavorite: "Engadir a favoritas", + shopRemoveFavorite: "Quitar de favoritas", + shopOpen: "ABERTA", + shopClosed: "PECHADA", + shopMakePrivate: "FACELA PRIVADA (E2E)", + shopMakePublic: "FACELA PÚBLICA", + shopProducts: "Produtos", + shopFilterAll: "TODAS", + shopFilterRecent: "RECENTES", + shopFilterMine: "MIÑAS", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUTOS", + shopFilterPrices: "PREZOS", + shopFilterFavorites: "FAVORITOS", + shopProductAdd: "Engadir un produto", + shopProductTitle: "Produto", + shopProductUpdate: "Actualizar o produto", + shopProductPrice: "Prezo (ECO)", + shopProductStock: "Stock", + shopProductUntitled: "Produto sen título", + shopUntitled: "Tenda sen título", + shopNoItems: "Non se atoparon tendas.", + shopNoProducts: "Aínda non hai produtos.", + shopOutOfStock: "Esgotado", + shopBuy: "Comprar", + shopBuyEncryptedNote: "Os teus datos de entrega envíanse cifrados; só o dono da tenda pode velos.", + shopBuyDeliveryAddress: "Enderezo de entrega", + shopBuyContact: "Contacto", + shopBuyContactPlaceholder: "correo, teléfono, etc.", + shopBuyNotes: "Notas", + shopBuyDeliveryRequired: "O enderezo de entrega é obrigatorio.", + shopOrdersTitle: "Pedidos", + shopOrdersDescription: "Pedidos recibidos por esta tenda.", + shopOrdersEmpty: "Aínda non hai pedidos.", + shopOrderProduct: "Produto", + shopOrderPrice: "Prezo", + shopOrderBuyer: "Comprador", + shopBackToShop: "Volver á tenda", + marketVisitItem: "VER O ARTIGO", + shopStatus: "ESTADO", + shopCreatedAt: "CREADA", + shopSearchPlaceholder: "Buscar en Tendas...", + shopUrl: "URL", + shopLocation: "Localización", + shopTags: "Etiquetas", + shopVisibility: "Visibilidade", + shopShortDescription: "Descrición curta", + shopShortDescriptionPlaceholder: "Descrición breve para as tarxetas (máx. 160 caracteres)", + shopTitlePlaceholder: "Nome da túa tenda", + shopDescriptionPlaceholder: "Descrición detallada da túa tenda", + shopLocationPlaceholder: "Cidade, País", + shopTagsPlaceholder: "Introduce etiquetas separadas por comas", + mapTitlePlaceholder: "Título do mapa", + shopProductFeatured: "Produto destacado", + shopSendToMarket: "Enviar ao mercado", + typeShop: "TENDAS", + typeShopProduct: "PRODUTO DE TENDA", + bankExchange: "Intercambio", + bankExchangeCurrentValue: "Valor de ECOin (1h)", + bankTotalSupply: "Oferta total de ECOin", + bankPubsSupply: "Oferta de ECOin do fondo dos PUBs", + bankHoldingSupply: "Oferta de ECOin fóra da RBU", + bankHoursOfWork: "horas", + bankUnitMs: "ms", + bankUnitSeconds: "segundos", + bankUnitMinutes: "minutos", + bankUnitDays: "días", + bankExchangeNoData: "Non hai datos dispoñibles", + bankInflation: "Inflación de ECOin (anual)", + bankInflationMonthly: "Inflación de ECOin (mensual)", + bankExchangeChartTitle: "Evolución do valor de ECOin ao longo do tempo", + bankExchangeChartValue: "Valor (ECO/h)", + bankExchangeChartSupply: "Oferta", + bankExchangeChartInflation: "Inflación %", + bankExchangeChartEmpty: "Aínda non hai mostras suficientes — volve máis tarde", + bankCurrentSupply: "Oferta actual de ECOin", + bankingSyncStatus: "Estado de ECOin", + bankingSyncStatusSynced: "Sincronizado", + bankingSyncStatusOutdated: "Sen sincronizar", + statsTitle: "Estatísticas", + statistics: "Estatísticas", + statsDescription: "Descobre estatísticas sobre a túa rede.", + ALLButton: "TODO", + RECENTButton: "RECENTES", + MINEButton: "MIÑAS", + TOMBSTONEButton: "ELIMINADOS", + statsCreatedAt: "Creado o", + statsYourContent: "Contido", + statsYourOpinions: "Opinións", + statsNetworkContent: "Contido", + statsNetworkOpinions: "Opinións", + statsEcoWalletLabel: "Carteira ECOin", + walletAddressCopyHint: "Preme para seleccionar e copiar", + statsEcoWalletNotConfigured: "Non configurada!", + statsBookmark: "Marcadores", + statsEvent: "Eventos", + statsTask: "Tarefas", + statsVotes: "Votos", + statsMarket: "Mercado", + statsForum: "Foros", + statsJob: "Empregos", + statsReport: "Informes", + statsFeed: "Feed", + statsTribe: "Tribos", + statsImage: "Imaxes", + statsAudio: "Audios", + statsVideo: "Vídeos", + statsDocument: "Documentos", + statsMap: "Mapas", + statsShop: "Tendas", + statsShopProduct: "Produtos de tenda", + statsTransfer: "Transferencias", + statsAiExchange: "IA", + statsPUBs: "PUBs", + statsSyncedPeers: "Nodos", + statsTotalLabel: "Total", + statsPost: "Publicacións", + statsSize: "Total (tamaño)", + statsBlockchainSize: "Blockchain (tamaño)", + statsBlobsSize: "Blobs (tamaño)", + statsActivity7d: "Actividade (últimos 7 días)", + statsActivity7dTotal: "Total 7 días", + statsActivity30dTotal: "Total 30 días", + day: "Día", + messages: "Mensaxes", + statsProject: "Proxectos", + statsProjectsTitle: "Proxectos", + statsProjectsTotal: "Total de proxectos", + statsProjectsActive: "Activos", + statsProjectsCompleted: "Completados", + statsProjectsPaused: "En pausa", + statsProjectsCancelled: "Cancelados", + statsProjectsGoalTotal: "Meta total", + statsProjectsPledgedTotal: "Achega total", + statsMarketTitle: "Mercado", + statsMarketTotal: "Artigos totais", + statsMarketForSale: "En venda", + statsMarketReserved: "Reservados", + statsMarketClosed: "Pechados", + statsMarketSold: "Vendidos", + statsUsersTitle: "Habitantes", + user: "Habitante", + statsTombstoneRatio: "Ratio de eliminados (%)", + bankingUserEngagementScore: "Puntuación de KARMA", + statsParliamentCandidature: "Candidaturas do parlamento", + statsParliamentTerm: "Mandatos do parlamento", + statsParliamentProposal: "Propostas do parlamento", + statsParliamentRevocation: "Revogacións do parlamento", + statsParliamentLaw: "Leis do parlamento", + statsCourtsCase: "Casos xudiciais", + statsCourtsEvidence: "Probas xudiciais", + statsCourtsAnswer: "Respostas xudiciais", + statsCourtsVerdict: "Veredictos xudiciais", + statsCourtsSettlement: "Acordos xudiciais", + statsCourtsSettlementProposal: "Propostas de acordo", + statsCourtsSettlementAccepted: "Acordos aceptados", + statsCourtsNomination: "Nomeamentos de xuíces", + statsCourtsNominationVote: "Votos de nomeamento", + ai: "IA", + aiTitle: "IA", + aiDescription: "Unha Intelixencia Artificial Colectiva (IAC) chamada '42' que aprende da túa rede.", + aiUserQuestion: "Pregunta", + aiResponseTitle: "Resposta", + aiSubmitButton: "Enviar!", + aiSettingsDescription: "Configura o teu prompt (máx. 128 caracteres) para o modelo de IA.", + aiPrompt: "Proporciona unha resposta informativa e precisa.", + aiConfiguration: "Configurar o prompt", + aiPromptUsed: "Indicación", + aiClearHistory: "Borrar o historial", + aiServerError: "Non podo responder ben. Téntao de novo.", + aiStatusLoading: "42 está a cargar o modelo. Pregunta de novo nun momento.", + aiThinking: "Pensando", + aiWaiting: "Agardando", + aiStatusMissingModel: "O modelo de IA non está instalado: falta src/AI/oasis-42-1-chat.Q4_K_M.gguf.", + aiExportFineTuning: "Exportar Fine-Tuning", + aiFactsTitle: "Datos da túa rede", + aiInputPlaceholder: "Que é Oasis?", + typeAiExchange: "IA", + aiApproveTrain: "Engadir ao adestramento colectivo", + aiRejectTrain: "Non adestrar", + aiTrainApproved: "Aprobado para o adestramento", + aiTrainRejected: "Rexeitado para o adestramento", + aiSnippetsUsed: "Fragmentos usados", + aiSnippetsLearned: "Fragmentos aprendidos", + statsAITraining: "Adestramento de IA", + aiCustomAnswerPlaceholder: "Escribe a túa resposta personalizada…", + marketMineSectionTitle: "Os teus artigos", + marketCreateSectionTitle: "Crear un artigo", + marketUpdateSectionTitle: "Actualizar", + marketAllSectionTitle: "Mercado", + marketTitle: "Mercado", + marketDescription: "Un mercado para intercambiar bens ou servizos na túa rede.", + marketFilterAll: "TODOS", + marketFilterMine: "MEUS", + marketFilterAuctions: "POXAS", + marketFilterItems: "INTERCAMBIO", + marketFilterNew: "NOVO", + marketFilterUsed: "USADO", + marketFilterBroken: "ROTO", + marketFilterForSale: "EN VENDA", + marketFilterSold: "VENDIDO", + marketFilterDiscarded: "DESCARTADO", + marketFilterRecent: "RECENTES", + marketFilterMyBids: "POXAS FEITAS", + marketCreateButton: "Vender algo", + marketItemType: "Tipo", + marketItemTitle: "Título", + marketItemAvailable: "Data límite", + marketItemDescription: "Descrición", + marketItemDescriptionPlaceholder: "Describe o artigo que vendes", + marketItemStatus: "Estado", + marketShopLabel: "Tenda", + marketItemCondition: "Condición", + marketItemPrice: "Prezo", + marketItemTags: "Etiquetas", + marketItemTagsPlaceholder: "Introduce etiquetas separadas por comas", + marketItemDeadline: "Data límite", + marketItemIncludesShipping: "Inclúe o envío?", + marketItemHighestBid: "Poxa máis alta", + marketItemStock: "Existencias", + marketOutOfStock: "Sen stock", + marketActionsUpdate: "Actualizar", + marketUpdateButton: "Actualizar", + marketActionsDelete: "Eliminar", + marketActionsChangeStatus: "CAMBIAR O ESTADO", + marketActionsBuy: "COMPRAR!", + marketAuctionBids: "Poxas actuais", + marketPlaceBidButton: "Poxar", + marketItemSeller: "Vendedor", + marketNoItems: "Aínda non hai artigos dispoñibles.", + marketYourBid: "A túa poxa", + marketCreateFormImageLabel: "Subir contido multimedia (máx.: 75MB)", + marketSearchPlaceholder: "Buscar no Mercado...", + marketMinPriceLabel: "Prezo mínimo", + marketMaxPriceLabel: "Prezo máximo", + marketSortRecent: "Máis recentes", + marketSortPrice: "Prezo", + marketSortDeadline: "Data límite", + marketSearchButton: "Buscar", + marketAuctionEndsIn: "Remata", + marketAuctionEnded: "Rematou", + marketNoItemsMatch: "Non hai artigos que coincidan coa túa busca.", + housingTitle: "Vivenda", + housingDescriptionText: "Descobre e xestiona lugares na túa rede.", + modulesHousingLabel: "Vivenda", + modulesHousingDescription: "Módulo para descubrir e xestionar lugares.", + typeHousing: "VIVENDA", + housingFilterAll: "TODO", + housingFilterMine: "MEUS", + housingFilterRequested: "SOLICITADOS", + housingFilterSale: "VENDA", + housingFilterRent: "ALUGUER", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "DISPOÑIBLES", + housingFilterClosed: "NON DISPOÑIBLES", + housingFilterRecent: "RECENTES", + housingFilterTop: "TOP", + housingType: "Oferta", + housingTypeSALE: "En venda", + housingTypeRENT: "En aluguer", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Inmoble", + housingPropertyAPARTMENT: "Piso", + housingPropertyHOUSE: "Casa", + housingPropertyROOM: "Habitación", + housingPropertyLAND: "Terreo", + housingPropertyOTHER: "Outro", + housingTitleLabel: "Título", + housingTitlePlaceholder: "Nome do lugar", + housingDescription: "Descrición", + housingDescriptionPlaceholder: "Describe o lugar, o barrio, que inclúe: baños, xardín, camas…", + housingRules: "Normas", + housingRulesPlaceholder: "Mascotas, fumar, horas de silencio, estadía mínima…", + housingPlace: "Lugar", + housingPlacePlaceholder: "País, cidade, enderezo", + housingPrice: "Prezo", + housingPriceLabel: "Prezo (ECO)", + housingSizeLabel: "Superficie (m²)", + housingFree: "GRATIS", + housingPerMonth: "/mes", + housingRooms: "Habitacións", + housingSize: "Superficie", + housingCapacity: "Hóspedes", + housingAvailableFrom: "Dispoñible desde", + housingAvailableTo: "Dispoñible ata", + housingTags: "Etiquetas", + spreadEditWarning: "Este contido perderá as súas réplicas ao editalo", + housingRequests: "Solicitudes", + housingRequestButton: "Solicitar", + housingCancelButton: "Cancelar a solicitude", + housingRequestedBadge: "SOLICITADO", + housingStatus: "Estado", + housingStatusOPEN: "DISPOÑIBLE", + housingStatusCLOSED: "NON DISPOÑIBLE", + housingSetOpen: "Marcar como dispoñible", + housingSetClosed: "Marcar como non dispoñible", + housingCreateButton: "Crear un lugar", + housingUpdateButton: "Actualizar", + housingSearchPlaceholder: "Buscar en Vivenda...", + housingSearchButton: "Buscar", + housingMinPrice: "Prezo mín.", + housingMaxPrice: "Prezo máx.", + housingSortRecent: "Máis recentes", + housingSortPrice: "Prezo máis baixo", + housingSortRequests: "Máis solicitados", + housingSortRating: "Mellor valorados", + housingNoItems: "Non se atoparon lugares.", + jobsTitle: "Emprego", + jobsCandidatesTitle: "Candidatos suxeridos", + jobsCandidatesDescription: "Habitantes con competencias que encaixan con este emprego. Envíalles unha mensaxe privada para convidalos.", + jobsCandidatesPmBody: "Ola, creo que o teu perfil encaixa coa miña oferta de emprego", + jobsDescription: "Descobre e xestiona ofertas de emprego na túa rede.", + jobsFilterRecent: "RECENTES", + jobsFilterMine: "MEUS", + jobsFilterAll: "TODOS", + jobsFilterRemote: "REMOTOS", + jobsFilterOpen: "ABERTOS", + jobsFilterClosed: "PECHADOS", + jobsCV: "CV", + jobsCreateJob: "Publicar un emprego", + jobsRecentTitle: "Empregos recentes", + jobsMineTitle: "Os teus empregos", + jobsAllTitle: "Ofertas de emprego", + jobsRemoteTitle: "Empregos remotos", + jobsOpenTitle: "Empregos abertos", + jobsClosedTitle: "Empregos pechados", + jobsCVTitle: "CV", + jobsFilterPresencial: "PRESENCIAL", + jobsFilterFreelancer: "FREELANCE", + jobsFilterEmployee: "ASALARIADO", + jobsPresencialTitle: "Empregos presenciais", + jobsFreelancerTitle: "Empregos freelance", + jobsEmployeeTitle: "Empregos asalariados", + jobTitle: "Título", + jobLocation: "Localización", + jobSalary: "Salario (ECO/1h)", + jobVacants: "Vacantes", + jobDescription: "Descrición", + jobRequirements: "Requisitos", + jobLanguages: "Idiomas", + jobStatus: "Estado", + jobStatusOPEN: "ABERTA", + jobStatusCLOSED: "PECHADA", + jobSetClosed: "Marcar como PECHADA", + jobSubscribeButton: "Únete a esta oferta!", + jobUnsubscribeButton: "Sae desta oferta!", + jobTitlePlaceholder: "Introduce o título do emprego", + jobDescriptionPlaceholder: "Describe o posto", + jobRequirementsPlaceholder: "Introduce os requisitos", + jobLanguagesPlaceholder: "Inglés, francés, castelán, galego...", + jobTasksPlaceholder: "Lista de tarefas", + jobLocationPresencial: "Presencial", + jobLocationRemote: "Remoto", + jobVacantsPlaceholder: "Número de vacantes", + jobImage: "Subir contido multimedia (máx.: 75MB)", + jobTasks: "Tarefas", + jobType: "Tipo de emprego", + jobTime: "Tempo de traballo", + jobSubscribers: "Subscritores", + noSubscribers: "Sen subscritores", + jobsFilterTop: "TOP", + jobsTopTitle: "Empregos mellor pagados", + createJobButton: "Publicar un emprego", + noJobsFound: "Non se atoparon ofertas de emprego.", + jobAuthor: "Por", + jobTypeFreelance: "Autónomo", + jobTypeSalary: "Asalariado", + jobTypeExchange: "Intercambio de horas", + jobTypeEXCHANGE: "INTERCAMBIO DE HORAS", + jobsFilterExchange: "INTERCAMBIO", + jobsExchangeTitle: "Empregos por intercambio de horas", + jobsHoursOffered: "Horas ofrecidas", + jobsHoursRequested: "Horas pedidas a cambio", + jobsExchangeSkill: "Habilidade solicitada a cambio", + jobTimePartial: "Parcial", + jobTimeComplete: "Completa", + jobsUpdateButton: "ACTUALIZAR", + jobsFilterApplied: "CANDIDATURA", + jobsAppliedTitle: "As miñas candidaturas", + jobsAppliedBadge: "Inscrito", + jobsSearchPlaceholder: "Buscar en Emprego...", + jobsMinSalaryLabel: "Salario mín.", + jobsMaxSalaryLabel: "Salario máx.", + jobsSortRecent: "Máis recentes", + jobsSortSalary: "Maior salario", + jobsSortSubscribers: "Máis candidatos", + jobsSearchButton: "Buscar", + jobsApplicants: "Candidatos", + jobsUpdatedAt: "Actualizado", + jobSetOpen: "Marcar como aberta", + jobsTagsLabel: "Etiquetas", + noJobsMatch: "Non hai ofertas que coincidan coa túa busca.", + industrySearchPlaceholder: "Buscar en Industria...", + industryAllSectors: "Todos os sectores", + industryVisitButton: "Visitar Industria", + industryAdvanceTo: "Cambiar a", + industryAmount: "Cantidade", + industryApproveBuild: "Aprobar", + industryBlueprint: "Plano", + industryBlueprints: "Planos", + industryBuild: "Produción", + industryBuildNotes: "Notas", + industryBuildStart: "Data de inicio", + industryVoteUpdateButton: "Votar a actualización", + industryVoteDeleteButton: "Votar o borrado", + industryRevokeVote: "Revogar", + industryTimeLeft: "Tempo restante", + industryBuildEnd: "Data de fin", + industryBuilds: "Producións", + industryBuildTitle: "Título", + industryContribute: "Contribuír", + industryContributeButton: "Contribuír", + industryContributions: "Contribucións", + industryCreateBlueprintButton: "Crear un plano", + industryDistribute: "Distribuír", + industryDistributeButton: "Distribuír por cotas", + industryDistribution: "Distribución", + industryEcoAmount: "Cantidade (ECO)", + industryEditBlueprint: "Editar o plano", + industryFacility: "Fábrica", + industryKindLabel: "Tipo", + industryKind_labor: "Man de obra", + industryKind_material: "Material", + industryKind_eco: "Fondos", + industryLaborHours: "Man de obra (horas)", + industryHours: "Horas", + industryMaterialItem: "Material", + industryMaterials: "Materiais", + industryEstMaterials: "Total de materiais", + industryEstLabor: "Total de man de obra", + industryEstTotal: "Prezo estimado", + industryBuildingPrice: "Prezo de construción", + industryFinalPrice: "Prezo final", + industryBlueprintDescPlaceholder: "Especificacións, dimensións, peso, documentación…", + industryMaterialValue: "Valor (ECO)", + industryMember: "Membro", + industryNewBlueprint: "Novo plano", + industryNewBuild: "Propoñer unha produción", + industryEditBuild: "Editar a produción", + industryNeedBlueprint: "Crea primeiro un plano: cada produción fabrica un.", + industryNoBlueprints: "Aínda non hai planos.", + industryNoBuilds: "Aínda non hai producións.", + industryNote: "Nota", + industryOutput: "Saída", + industryOutputKind: "Tipo de produto", + industryOutputValue: "Valor de saída (ECO, p. ex. da venda)", + industryPostJob: "Crear un emprego", + industryPot: "Bote", + industryProposeBuildButton: "Propoñer a produción", + industryProposer: "Proponente", + industryAuthor: "Autor", + industrySellOnMarket: "Enviar ao mercado", + industrySendToProjects: "Crear un proxecto", + industrySkills: "Habilidades", + industryTreasury: "Tesouraría", + industryKind_physical: "Físico", + industryKind_digital: "Dixital", + industryBuildStatus_PROPOSED: "PROPOSTA", + industryBuildStatus_REJECTED: "REXEITADA", + industryBuildStatus_APPROVED: "APROBADA", + industryBuildStatus_STOCKING: "ABASTECÉNDOSE", + industryBuildStatus_IN_PRODUCTION: "EN PRODUCIÓN", + industryBuildStatus_COMPLETED: "COMPLETADA", + industryBuildStatus_FAILED: "FALLIDA", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Un emprego encaixa co teu currículo", + jobsBotMatchIntro: "Un emprego encaixa co teu currículo.", + jobsBotMatchJob: "Emprego", + jobsBotMatchHint: "Recibes isto porque o teu currículo está xestionado por IA. Podes desactivalo no teu CV.", + cvAiManaged: "Xestionado por IA", + switchOn: "ACTIVADO", + switchOff: "DESACTIVADO", + cvAiManagedHint: "Se está activo, JobsBot avísate por mensaxe privada cando un emprego encaixa co teu currículo.", + cvMatchThreshold: "Avisarme a partir deste nivel de coincidencia", + housingBotRequestedTitle: "Alguén solicitou un dos teus lugares.", + housingBotCancelledTitle: "Cancelouse unha solicitude sobre un dos teus lugares.", + housingBotYouRequestedTitle: "Solicitaches un lugar.", + housingBotYouCancelledTitle: "Cancelaches unha solicitude.", + housingBotUnavailableTitle: "Un lugar que solicitaches xa non está dispoñible.", + housingBotUnavailableSentTitle: "Avisaches a quen solicitou este lugar.", + industryBotAdmittedTitle: "Admitíronte nunha fábrica.", + industryBotApplicationTitle: "Nova solicitude de ingreso na túa fábrica.", + industryBotInvitedTitle: "Convidáronte a unha fábrica.", + industryBotDissolvedTitle: "Disolveuse unha fábrica á que pertences.", + industryBotBuildApprovedTitle: "Aprobouse unha produción.", + industryBotDistributedTitle: "Distribuíuse a saída dunha produción.", + industryFilterRules: "REGRAS", + industryRulesTitle: "Regras", + industryRulesIntro: "Industria permite aos habitantes posuír colectivamente os medios de produción: unha Fábrica é un obradoiro da rede que ninguén posúe de forma privada.", + industryRulesSteward: "O fundador é o mordomo — un custodio, non un dono: non pode vender, privatizar nin disolver a Fábrica pola súa conta.", + industryRulesMembership: "A política de pertenza define como se unen os habitantes: Aberta (entras ao instante), Por voto (admíteche o voto dos membros) ou Só por convite (un membro debe convidarte antes).", + industryRulesQuorum: "O quórum é o número mínimo de membros que deben votar para que unha decisión conte, para que unha Fábrica pequena non a decida un só habitante.", + industryRulesMajority: "A maioría é a fracción de votos necesaria para aprobar (0,5 = metade, 1 = unanimidade); unha decisión apróbase cando os SI chegan polo menos a max(quórum, membros × maioría).", + industryRulesDecisions: "Os membros votan para admitir novos, aprobar producións e disolver a Fábrica; o mordomo non pode saltarse estas decisións colectivas.", + industryRulesBlueprints: "Os planos son receitas libres (COPYLEFT) — as entradas (materiais, horas de traballo, habilidades) e a saída (un ben físico ou dixital) — e calquera pode bifurcalas.", + industryRulesBuilds: "Unha produción é unha orde de fabricación: PROPOSTA → APROBADA (por voto) → ABASTECÉNDOSE → EN PRODUCIÓN → COMPLETADA, e só acepta contribucións unha vez aprobada.", + industryRulesContributions: "Os membros achegan traballo (horas), materiais (valor declarado en ECO) ou ECO a unha produción; cada achega gaña karma.", + industryRulesLaborRate: "A tarifa de traballo é o valor en ECO dunha hora de traballo nesta Fábrica; converte horas en karma para comparar esforzo e capital de forma xusta: karma = horas × tarifa + valor material + ECO.", + industryRulesShares: "A túa cota nunha produción é o teu karma ÷ karma total, e determina canto do valor de saída recibes.", + industryRulesDistribution: "Cando unha produción se completa, o mordomo reparte o bote (os fondos da produción máis o valor de venda) entre quen contribuíu, en proporción á súa participación.", + industryRulesTreasury: "O mordomo custodia as achegas en ECO como tesouraría da produción ata o reparto; todos os movementos quedan rexistrados na rede e as disputas poden ir aos Tribunais.", + industryJobs: "Empregos", + industryReportButton: "Informar", + industryCreateTask: "Crear unha tarefa", + industryOpenDispute: "Abrir unha disputa", + industryMaterialsPlaceholder: "placa-solar:2:120\nbatería:1:80\nkit:1:15", + industryCreateInvite: "Xerar un convite", + industryPauseVotes: "Votos de estado", + industryTitle: "Industria", + industryDescription: "Módulo para xestionar colectivamente os medios de produción.", + typeIndustryBuild: "PRODUCIÓN", + typeIndustryBlueprint: "PLANO", + typeIndustryAllocation: "DISTRIBUCIÓN", + typeIndustry: "INDUSTRIA", + modulesIndustryLabel: "Industria", + modulesIndustryDescription: "Módulo para xestionar colectivamente os medios de produción.", + industryFilterAll: "TODAS", + industryFilterRecent: "RECENTES", + industryFilterMember: "MEMBRO", + industryFilterBlueprints: "PLANOS", + industryFilterBuilds: "PRODUCIÓNS", + industryFilterMine: "MIÑAS", + industryFilterActive: "EN MARCHA", + industryFilterPaused: "EN PAUSA", + industryFilterDissolved: "DISOLTAS", + industryAllTitle: "Industria", + industryRecentTitle: "Fábricas recentes", + industryMemberTitle: "Fábricas nas que estou", + industryMineTitle: "As miñas fábricas", + industryActiveTitle: "En marcha", + industryPausedTitle: "Fábricas en pausa", + industryDissolvedTitle: "Fábricas disoltas", + industryNoFacilitiesFound: "Non se atoparon fábricas.", + industryCreateFacility: "Crear unha fábrica", + industryMembers: "Membros", + industryMemberBadge: "MEMBRO", + industryName: "Nome", + industryNamePlaceholder: "Nome da fábrica", + industryDescriptionLabel: "Descrición", + industryDescriptionPlaceholder: "Que produce esta fábrica?", + industrySector: "Sector", + industryMembershipPolicy: "Política de pertenza", + industryQuorum: "Quórum", + industryMajority: "Maioría", + industryLaborRate: "Tarifa de traballo", + industryCreateButton: "Crear a fábrica", + industryUpdateButton: "Actualizar", + industrySteward: "Mordomo", + industryStatusActive: "EN MARCHA", + industryStatusPaused: "EN PAUSA", + industryStatusDissolved: "DISOLTA", + industryStatusLabel: "Estado", + industryJoinButton: "Unirse", + industryApplyButton: "Solicitar o ingreso", + industryLeaveButton: "Saír", + industryInviteButton: "Convidar", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Necesitas un convite para unirte.", + industryPauseButton: "Votar a pausa", + industryResumeButton: "Votar a reanudación", + industryDeleteButton: "Eliminar", + industryPendingApplicants: "Solicitudes pendentes", + industryVotesYes: "si", + industryVotesNo: "non", + industryAlreadyVoted: "votado", + industryAdmit: "Admitir", + industryReject: "Rexeitar", + industryDissolveTitle: "Disolver a fábrica", + industryDissolveVote: "Votar a disolución", + industryPolicy_open: "Aberta", + industryPolicy_vote: "Por voto", + industryPolicy_invite: "Só por convite", + projectsTitle: "Proxectos", + projectsDescription: "Crea, financia e segue proxectos impulsados pola comunidade na túa rede.", + projectSearchPlaceholder: "Buscar en Proxectos...", + projectCreateProject: "Crear un proxecto", + projectCreateButton: "Crear un proxecto", + projectUpdateButton: "ACTUALIZAR", + projectDeleteButton: "ELIMINAR", + projectNoProjectsFound: "Non se atoparon proxectos.", + projectFilterAll: "TODOS", + projectFilterMine: "OS MEUS PROXECTOS", + projectFilterActive: "ACTIVOS", + projectFilterPaused: "EN PAUSA", + projectFilterCompleted: "COMPLETADOS", + projectFilterFollowing: "SEGUIDOS", + projectFilterRecent: "RECENTES", + projectFilterTop: "TOP", + projectAllTitle: "Proxectos", + projectMineTitle: "Os teus proxectos", + projectActiveTitle: "Proxectos activos", + projectPausedTitle: "Proxectos en pausa", + projectCompletedTitle: "Proxectos completados", + projectFollowingTitle: "Proxectos seguidos", + projectRecentTitle: "Proxectos recentes", + projectTopTitle: "Mellor financiados", + projectTitlePlaceholder: "Nome do proxecto", + projectImage: "Subir contido multimedia (máx.: 75MB)", + projectDescription: "Descrición", + projectDescriptionPlaceholder: "Conta a historia e os obxectivos…", + projectGoal: "Meta (ECO)", + projectGoalPlaceholder: "50000", + projectDeadline: "Data límite", + projectProgress: "Progreso inicial (%)", + projectStatus: "Estado", + projectFunding: "Financiamento", + projectPledged: "Comprometido", + projectSetStatus: "Establecer o estado", + projectSetProgress: "Actualizar o progreso", + projectFollowButton: "SEGUIR", + projectUnfollowButton: "DEIXAR DE SEGUIR", + projectStatusACTIVE: "ACTIVO", + projectStatusPAUSED: "EN PAUSA", + projectStatusCOMPLETED: "COMPLETADO", + projectStatusCANCELLED: "CANCELADO", + projectPledgeTitle: "Apoia este proxecto", + projectPledgePlaceholder: "Cantidade en ECO", + projectBounties: "Recompensas", + projectNoBounties: "Non se atoparon recompensas.", + projectTitle: "Título", + projectMilestoneSelect: "Seleccionar unha fita", + projectBountyCreateButton: "Crear unha recompensa", + projectBountyStatus: "Estado da recompensa", + projectBountyOpen: "aberta", + projectBountyClaimed: "reclamada", + projectBountyDone: "completada", + projectBountyClaimedBy: "reclamada por", + projectBountyClaimButton: "reclamar", + projectBountyCompleteButton: "Marcar como completada", + projectMilestones: "Fitos", + projectAddMilestoneTitle: "Novo fito", + projectMilestoneTitle: "Título do fito", + projectMilestoneTargetPercent: "Porcentaxe (%)", + projectMilestoneDueDate: "Data", + projectMilestoneStatus: "Estado do fito", + projectMilestoneOpen: "aberto", + projectMilestoneDone: "completado", + projectMilestoneDue: "vence", + projectMilestoneMarkDone: "Marcar como feito", + projectMilestoneTitlePlaceholder: "Introduce o título do fito", + projectMilestoneDescriptionPlaceholder: "Introduce a descrición deste fito", + projectMilestoneDescription: "Descrición do fito", + projectBudgetAssigned: "Asignado a recompensas", + projectBudgetOver: "⚠ Orzamento excedido: o asignado supera o obxectivo", + projectFollowers: "Seguidores", + projectFollowersTitle: "Seguidores", + projectFollowersNone: "Aínda non hai seguidores.", + projectMore: "máis", + projectYouFollowHint: "Segues este proxecto", + projectBackers: "Patrocinadores", + projectBackersTitle: "Patrocinadores", + projectBackersTotal: "Total de patrocinadores", + projectBackersTotalPledged: "Total achegado", + projectBackersYourPledge: "A túa achega", + projectBackersNone: "Aínda non hai achegas.", + projectFilterBackers: "MECENAS", + projectFilterApplied: "ACHEGANDO", + projectAppliedTitle: "ACHEGANDO", + projectBackersLeaderboardTitle: "Mecenas destacados", + projectNoBackersFound: "Non hai mecenas.", + projectBackerAmount: "Total achegado", + projectBackerPledges: "Achegas", + projectBackerProjects: "Proxectos", + projectSelectMilestoneOrBounty: "Seleccionar un fito ou unha recompensa", + projectPledgeButton: "Achegar", + footerLicense: "GPLv3", + modulesModuleName: "Nome", + modulesModuleDescription: "Descrición", + modulesModuleStatus: "Estado", + modulesTotalModulesLabel: "Total", + modulesEnabledModulesLabel: "Activados", + modulesDisabledModulesLabel: "Desactivados", + modulesInvitesLabel: "Convites", + modulesInvitesDescription: "Módulo para xestionar e aplicar códigos de convite.", + modulesWalletLabel: "Carteira", + modulesWalletDescription: "Módulo para xestionar os teus activos dixitais (ECOin).", + modulesCipherLabel: "Cifrado", + modulesCipherDescription: "Módulo para cifrar e descifrar o teu texto de forma simétrica (cun contrasinal compartido).", + modulesBookmarksLabel: "Marcadores", + modulesBookmarksDescription: "Módulo para descubrir e xestionar marcadores.", + modulesVideosLabel: "Vídeos", + modulesVideosDescription: "Módulo para descubrir e xestionar vídeos.", + modulesDocsLabel: "Documentos", + modulesDocsDescription: "Módulo para descubrir e xestionar documentos.", + modulesAudiosLabel: "Audios", + modulesAudiosDescription: "Módulo para descubrir e xestionar audios.", + modulesTagsLabel: "Etiquetas", + modulesTagsDescription: "Módulo para descubrir e explorar patróns de taxonomía (etiquetas).", + modulesImagesLabel: "Imaxes", + modulesImagesDescription: "Módulo para descubrir e xestionar imaxes.", + modulesTrendingLabel: "Tendencias", + modulesTrendingDescription: "Módulo para explorar o contido máis popular.", + modulesEventsLabel: "Eventos", + modulesEventsDescription: "Módulo para descubrir e xestionar eventos.", + modulesTasksLabel: "Tarefas", + modulesTasksDescription: "Módulo para descubrir e xestionar tarefas.", + modulesMarketLabel: "Mercado", + modulesMarketDescription: "Módulo para intercambiar bens ou servizos.", + modulesShopsLabel: "Tendas", + modulesShopsDescription: "Módulo para xestionar e descubrir tendas.", + modulesTribesLabel: "Tribos", + modulesTribesDescription: "Módulo para explorar ou crear tribos (grupos).", + modulesVotationsLabel: "Votacións", + modulesVotationsDescription: "Módulo para descubrir e xestionar votacións.", + modulesParliamentLabel: "Parlamento", + modulesParliamentDescription: "Módulo para elixir gobernos e votar leis.", + modulesCourtsLabel: "Tribunais", + modulesCourtsDescription: "Módulo para resolver conflitos e emitir veredictos.", + modulesReportsLabel: "Informes", + modulesReportsDescription: "Módulo para xestionar e facer o seguimento de informes sobre problemas, erros, abusos e avisos de contido.", + modulesOpinionsLabel: "Opinións", + modulesOpinionsDescription: "Módulo para descubrir e votar opinións.", + modulesTransfersLabel: "Transferencias", + modulesTransfersDescription: "Módulo para descubrir e xestionar contratos intelixentes (transferencias).", + modulesFediverseLabel: "Multiverso", + modulesBlogsLabel: "Blogs", + modulesBlogsDescription: "Módulo para descubrir e xestionar blogs.", + modulesPollsLabel: "Enquisas", + modulesPollsDescription: "Módulo para preguntarlle á rede e contar as respostas.", + modulesFediverseDescription: "Xestiona as túas outras contas do multiverso, incluído enviar e recibir contido.", + modulesFeedLabel: "Feed", + modulesFeedDescription: "Módulo para descubrir e compartir textos breves (feeds).", + modulesPixeliaLabel: "Pixelia", + modulesPixeliaDescription: "Módulo para debuxar nunha grella colaborativa.", + modulesMelodyLabel: "Melody", + modulesMelodyDescription: "Módulo para xerar e compartir o \"son\" da túa blockchain.", + modulesAgendaLabel: "Axenda", + modulesAgendaDescription: "Módulo para xestionar todos os teus elementos asignados.", + modulesAILabel: "IA", + modulesAIDescription: "Módulo para falar cun LLM chamado '42'.", + modulesForumLabel: "Foros", + modulesForumDescription: "Módulo para descubrir e xestionar foros.", + modulesJobsLabel: "Emprego", + modulesJobsDescription: "Módulo para descubrir e xestionar ofertas de emprego.", + modulesProjectsLabel: "Proxectos", + modulesProjectsDescription: "Módulo para explorar, financiar e xestionar proxectos.", + modulesBankingLabel: "Banca", + modulesBankingDescription: "Módulo para xestionar o tesouro común: valor de ECOin, impostos e RBU.", + modulesFavoritesLabel: "Favoritos", + modulesFavoritesDescription: "Módulo para xestionar o teu contido favorito.", + fileTooLargeTitle: "Ficheiro demasiado grande", + fileTooLargeMessage: "O ficheiro supera o tamaño máximo permitido (75 MB). Por favor, selecciona un ficheiro máis pequeno.", + goBack: "Volver", + errorDismiss: "De acordo", + errorPageTitle: "Produciuse un erro", + errorNoAllocation: "Non se atopou ningunha asignación para esta reclamación.", + errorInvalidClaim: "Esta asignación non se pode reclamar.", + visibilityLabel: "Visibilidade", + clearnetPrivateConflict: "Un contido privado, oculto, pechado, só por invitación ou de pago non se pode publicar na CLEARNET. Escolle OASIS ou faino público.", + visibilityPublic: "Pública", + visibilityHidden: "Oculta", + visibilityMakePublic: "Facela pública", + visibilityMakeHidden: "Facela oculta", + invitesInhabitantsTitle: "Habitantes", + aiNavPlaceholder: "Describe o que necesitas...", + aiNavDisabled: "A navegación con IA está desactivada.", + aiNavResultsTitle: "Suxestións de AINav", + aiNavQueryLabel: "Consulta", + aiNavResultMatch: "Coincidencia", + aiNavResultsEmpty: "Non hai rutas coincidentes. Proba con /search.", + modulesAINavLabel: "AINav", + modulesAINavDescription: "Módulo para facer peticións en linguaxe natural sobre o contido da rede.", + directConnectDescription: "Conéctate directamente a un nodo introducindo o seu enderezo IP, porto e chave pública. O nodo engadirase como conexión seguida.", + peerHost: "IP", + peerPort: "Porto", + peerPublicKey: "Chave pública (@...ed25519)", + connectAndFollow: "Conectar", + modulesPresetTitle: "Configuracións comúns", + workflowsTitle: "Fluxos de traballo", + workflowsDescription: "Un fluxo de traballo fixa o tema e que módulos están activos.", + workflowsSet: "Aplicar o fluxo de traballo", + workflow_default: "Por defecto", + workflow_jobs: "Emprego", + workflow_social: "Social", + workflow_business: "Negocios", + workflow_night: "Noite", + workflow_kids: "Nenos", + workflow_activists: "Activismo", + workflow_mobile: "Móbil", + statsCarbonFootprintTitle: "Pegada de carbono", + statsCarbonTombstone: "Pegada dos borrados (tombstoning)", + uploadMedia: "Subir contido multimedia (máx.: 75MB)", + invitesUnfollow: "Deixar de seguir", + invitesFollow: "Seguir", + invitesUnfollowedInvites: "Non federadas", + invitesNoUnfollowed: "Non hai pubs non federados.", + reportsWhyInappropriateLabel: "Por que é inadecuado?", + blockchainContentDeleted: "Este contido foi eliminado", + visitContent: "Visitar o contido", + favoriteAdd: "Fixar en favoritos", + pmContentTooltip: "Enviar unha mensaxe privada", + phoneCallTooltip: "Facer unha chamada", + favoriteRemove: "Quitar de favoritos", + reportContent: "Denunciar o contido", + bankEcoinHours: "Equivalencia de ECOin en tempo (1 ECO)", + mapsLabel: "Mapas", + mapTitle: "Mapas", + mapDescription: "Xestiona e comparte mapas sen conexión na túa rede.", + mapMineSectionTitle: "Os teus mapas", + mapCreateSectionTitle: "Crear un mapa", + mapUpdateSectionTitle: "Actualizar o mapa", + mapAllSectionTitle: "Mapas", + mapRecentSectionTitle: "Mapas recentes", + mapFavoritesSectionTitle: "Favoritos", + mapFilterAll: "TODOS", + mapFilterMine: "MEUS", + mapFilterRecent: "RECENTES", + mapFilterFavorites: "FAVORITOS", + mapUploadButton: "Crear un mapa", + mapCreateButton: "Crear un mapa", + mapUpdateButton: "Actualizar", + mapDeleteButton: "Eliminar", + mapAddFavoriteButton: "Engadir a favoritos", + mapRemoveFavoriteButton: "Quitar de favoritos", + mapLatLabel: "Latitude", + mapLatPlaceholder: "p. ex. 42.8782", + mapLngLabel: "Lonxitude", + mapLngPlaceholder: "p. ex. -8.5448", + mapDescriptionLabel: "Descrición", + mapDescriptionPlaceholder: "Describe o mapa ou a localización...", + mapTypeLabel: "Tipo de mapa", + mapInviteMode: "Convite", + mapTypeSingle: "ÚNICO", + mapTypeOpen: "ABERTO", + mapTypeClosed: "PECHADO", + mapTagsLabel: "Etiquetas", + mapTagsPlaceholder: "Introduce etiquetas separadas por comas", + mapUrlLabel: "URL do mapa", + mapMarkersTitle: "Marcadores", + mapMarkerDefault: "Marcador", + mapMarkerLatLabel: "Latitude do marcador", + mapMarkerLngLabel: "Lonxitude do marcador", + mapMarkerLabelField: "Etiqueta do marcador", + mapMarkerLabelPlaceholder: "Describe este marcador...", + markerImageLabel: "Imaxe do marcador", + mapAddMarkerTitle: "Engadir un marcador", + mapAddMarkerButton: "Engadir un marcador", + mapCleanMarkerButton: "Limpar o marcador", + mapApplyZoom: "Aplicar o zoom", + mapPlaceSearchPlaceholder: "Buscar un lugar ou país...", + mapNoPlaces: "Non se atoparon lugares.", + mapClusterTitle: "marcadores aquí, preme para achegar", + mapDeleteMarker: "Eliminar marcador", + mapZoomIn: "Achegar", + mapZoomOut: "Afastar", + mapFitMarkers: "Axustar a todos os marcadores", + mapPanLabel: "Mover o mapa", + mapCenterHere: "Centrar aquí", + mapNear: "preto de", + mapPickedSpot: "Punto escollido", + mapPickHint: "Preme no mapa para escoller un punto.", + mapClearPick: "Limpar", + mapSearchPlaceholder: "Buscar en Mapas...", + mapSearchButton: "Buscar", + mapUpdatedAt: "Actualizado", + mapNoMatch: "Ningún mapa coincide coa túa busca.", + noMaps: "Non hai mapas dispoñibles.", + mapLocationTitle: "Localización", + mapVisitLabel: "Visitar o mapa", + typeMap: "MAPAS", + typeMapMarker: "MARCADOR DE MAPA", + modulesMapLabel: "Mapas", + modulesMapDescription: "Módulo para xestionar e compartir mapas sen conexión.", + emergenciesTitle: "Emerxencias", + modulesEmergenciesLabel: "Emerxencias", + modulesEmergenciesDescription: "Módulo para lanzar, confirmar e seguir emerxencias na rede.", + emergenciesDescription: "Módulo para lanzar, confirmar e seguir emerxencias na rede.", + emergencyCreate: "Crear unha emerxencia", + emergencyUpdate: "Actualizar", + emergencyDelete: "Borrar", + emergencyTitleLabel: "Título", + emergencyTitlePlaceholder: "Que está a pasar?", + emergencyTextLabel: "Detalles", + emergencyTextPlaceholder: "Onde, desde cando, que facer...", + emergencyCategoryLabel: "Categoría", + emergencyCategoryWeather: "METEOROLOXÍA", + emergencyCategoryInfrastructure: "INFRAESTRUTURA", + emergencyCategoryHealth: "SAÚDE", + emergencyCategorySecurity: "SEGURIDADE", + emergencyCategoryLost: "PERDIDOS", + emergencyCategoryNeighborhood: "VECIÑANZA", + emergencyExpiresLabel: "Caduca en", + emergencyExpires1d: "1 día", + emergencyExpires3d: "3 días", + emergencyExpires7d: "1 semana", + emergencyExpires30d: "1 mes", + emergencyExpiresNone: "Indefinido", + emergencyExpiresKeep: "Manter a data actual", + emergencyTagsLabel: "Etiquetas", + emergencyTagsPlaceholder: "Introduce etiquetas separadas por comas", + emergencyFilterAll: "TODAS", + emergencyFilterMine: "MIÑAS", + emergencyFilterRecent: "RECENTES", + emergencySeverityUnverified: "SEN VERIFICAR", + emergencySeverityLow: "BAIXA", + emergencySeverityMedium: "MEDIA", + emergencySeverityHigh: "ALTA", + emergencyStatusActive: "ACTIVA", + emergencyStatusResolved: "RESOLTA", + emergencyStatusExpired: "CADUCADA", + emergencyConfirmations: "Confirmacións", + emergencyConfirmButton: "Confirmar", + emergencyConfirmed: "Confirmaches esta emerxencia", + emergencyResolveButton: "Resolver", + emergencyReopenButton: "Reabrir", + emergencyGenerateReport: "Xerar un informe", + emergencyUpdateEdited: "Editada", + emergencyLastUpdate: "ÚLTIMA ACTUALIZACIÓN", + emergencyUpdatesTitle: "Últimas actualizacións:", + emergencyNoUpdates: "Aínda non hai actualizacións.", + emergencyAddUpdate: "Publicar unha actualización", + emergencyUpdatePlaceholder: "Que cambiou?", + emergencyNoItems: "Non se atoparon emerxencias.", + emergencyExpiresAt: "Caduca", + emergencyBannerLabel: "Emerxencia activa", + emergencyCreateSectionTitle: "Crear unha emerxencia", + emergencyUpdateSectionTitle: "Actualizar a emerxencia", + emergencySearchPlaceholder: "Buscar en emerxencias...", + typeEmergency: "EMERXENCIAS", + typeEmergencyConfirm: "CONFIRMACIÓN DE EMERXENCIA", + typeEmergencyUpdate: "ACTUALIZACIÓN DE EMERXENCIA", + statsEmergency: "Emerxencias", + statsEmergencyConfirm: "Confirmacións de emerxencias", + statsEmergencyUpdate: "Actualizacións de emerxencias", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "Unha emerxencia que segues ten unha actualización.", + emergencyBotResolvedTitle: "Unha emerxencia que segues resolveuse.", + dataKindEmergencies: "EMERXENCIAS", + mailingTitle: "Listas de correo", + modulesMailingLabel: "Listas de correo", + modulesMailingDescription: "Descobre e xestiona listas de correo na túa rede.", + modulesPhoneLabel: "Teléfono", + modulesPhoneDescription: "Módulo para chamar a outros habitantes con videochamadas cifradas de extremo a extremo.", + mailingDescription: "Descobre e xestiona listas de correo na túa rede.", + mailingCreate: "Crear unha lista de correo", + mailingUpdate: "Actualizar", + mailingArchive: "Arquivar", + mailingActivate: "Activar", + mailingCreateSectionTitle: "Crear unha lista de correo", + mailingUpdateSectionTitle: "Actualizar a lista de correo", + mailingTitleLabel: "Título", + mailingTitlePlaceholder: "Nome da lista de correo", + mailingDescriptionLabel: "Descrición", + mailingDescriptionPlaceholder: "De que trata esta lista?", + mailingTypeLabel: "Tipo de lista de correo", + mailingTypeOpen: "ABERTA", + mailingTypeClosed: "PECHADA", + mailingStatusLabel: "Estado", + mailingStatusActive: "ACTIVA", + mailingStatusArchived: "ARQUIVADA", + mailingMembersLabel: "Convidar habitantes", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Subscritores", + mailingTagsLabel: "Etiquetas", + mailingTagsPlaceholder: "Introduce etiquetas separadas por comas", + mailingFilterAll: "TODAS", + mailingFilterMine: "MIÑAS", + mailingFilterSubscribed: "SUBSCRITAS", + mailingFilterRecent: "RECENTES", + mailingHistoryDate: "DATA", + mailingHistoryThreads: "FÍOS", + mailingHistoryInhabitants: "HABITANTES", + mailingHistorySearchPlaceholder: "Buscar no historial...", + mailingNoMessages: "Aínda non hai mensaxes.", + mailingNoItems: "Non se atoparon listas de correo.", + mailingSearchPlaceholder: "Buscar en listas de correo...", + mailingWriteButton: "Escribir unha mensaxe", + mailingWriteTitle: "Escribir á lista", + mailingReplyTitle: "Responder á lista", + mailingSend: "Enviar", + mailingTextPlaceholder: "A túa mensaxe chega a todos os participantes da lista.", + mailingParticipants: "Participantes", + inboxFilterMailing: "Listas de correo", + mailingReplyPlaceholder: "Responder á lista...", + mailingReplyToList: "Responder á lista", + typeMailingList: "LISTA DE CORREO", + statsMailingList: "Listas de correo", + mailingListLabel: "LISTA DE CORREO", + logisticsTitle: "Loxística", + modulesLogisticsLabel: "Loxística", + modulesLogisticsDescription: "Descobre e xestiona plans loxísticos na túa rede.", + logisticsDescription: "Descobre e xestiona plans loxísticos na túa rede.", + logisticsCreate: "Crear unha ruta", + logisticsUpdate: "Actualizar", + logisticsClose: "Pechar", + logisticsReopen: "Reabrir", + logisticsCreateSectionTitle: "Crear unha ruta", + logisticsUpdateSectionTitle: "Actualizar a ruta", + logisticsTitleLabel: "Título", + logisticsTitlePlaceholder: "Viaxe ao mercado, furgoneta dispoñible...", + logisticsKindLabel: "Tipo de loxística", + logisticsKindTrip: "VIAXE", + logisticsKindShipment: "ENVÍO", + logisticsKindTrips: "VIAXES", + logisticsKindShipments: "ENVÍOS", + logisticsModeLabel: "Modo", + logisticsModeOffer: "OFREZO", + logisticsModeRequest: "BUSCO", + logisticsModeOffers: "OFERTAS", + logisticsModeRequests: "PETICIÓNS", + logisticsDescriptionLabel: "Descrición", + logisticsDescriptionPlaceholder: "Punto de encontro, que levas, condicións...", + logisticsOriginLabel: "Orixe", + logisticsOriginPlaceholder: "Onde comeza", + logisticsDestinationLabel: "Destino", + logisticsDestinationPlaceholder: "Onde remata", + logisticsDateLabel: "Data e hora", + logisticsRecurrenceLabel: "Recorrencia", + logisticsFrequencyLabel: "Frecuencia", + logisticsRecurrenceNone: "ÚNICO", + logisticsRecurrenceDaily: "DIARIO", + logisticsRecurrenceWeekly: "SEMANAL", + logisticsRecurrenceMonthly: "MENSUAL", + logisticsSeatsLabel: "Prazas", + logisticsSizeLabel: "Tamaño", + logisticsSizePlaceholder: "p. ex. 2m x 1m", + logisticsWeightLabel: "Peso", + logisticsWeightPlaceholder: "p. ex. 40kg", + logisticsPriceFree: "GRATIS", + logisticsPriceEco: "ECO", + logisticsPriceTime: "TEMPO", + logisticsPriceLabel: "Prezo", + logisticsHours: "h", + logisticsOrderRefLabel: "Ligazón ao pedido", + logisticsOrderRefPlaceholder: "/shops/... ou /market/... do pedido que se vai entregar", + logisticsOrderLink: "Pedido ligado", + logisticsTagsLabel: "Etiquetas", + logisticsTagsPlaceholder: "Introduce etiquetas separadas por comas", + logisticsFilterAll: "TODAS", + logisticsFilterMine: "MIÑAS", + logisticsFilterBooked: "RESERVADAS", + logisticsFilterRecent: "RECENTES", + logisticsFilterHistory: "HISTORIAL", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "ABERTA", + logisticsStatusUpcoming: "PRÓXIMAS", + logisticsStatusPast: "PASADAS", + logisticsStatusClosed: "PECHADAS", + logisticsZoneLabel: "Zona", + logisticsSearchPlaceholder: "Buscar en loxística...", + logisticsNoItems: "Non se atoparon rutas.", + logisticsBookingPending: "PENDENTE", + logisticsBookingConfirmed: "CONFIRMADA", + logisticsBookingRejected: "REXEITADA", + logisticsBookingCancelled: "CANCELADA", + logisticsBookingDelivered: "ENTREGADA", + logisticsConfirm: "Confirmar", + logisticsReject: "Rexeitar", + logisticsDeliver: "Entregar", + logisticsReceiptLabel: "Recibo", + logisticsReceiptPlaceholder: "Nota de entrega (opcional)", + logisticsCancelBooking: "Cancelar a reserva", + logisticsBookTitle: "Reservar esta ruta", + logisticsNotesLabel: "Notas", + logisticsNotesPlaceholder: "O que deba saber a persoa responsable", + logisticsBook: "Reservar", + logisticsBookingsTitle: "Reservas", + logisticsMyBookingTitle: "A túa reserva", + logisticsRatingsTitle: "Valoracións", + logisticsNoRatings: "Aínda non hai valoracións.", + logisticsRatingsWhenClosed: "As valoracións ábrense ao pechar a ruta.", + logisticsScoreLabel: "Puntuación", + logisticsRatingPlaceholder: "Que tal foi?", + logisticsRate: "Valorar", + typeLogisticsRoute: "RUTA", + typeLogisticsRating: "VALORACIÓN DE RUTA", + statsLogisticsRoute: "Rutas", + statsLogisticsRating: "Valoracións de rutas", + logisticsRouteLabel: "RUTA", + podcastsTitle: "Podcasts", + modulesPodcastsLabel: "Podcasts", + modulesPodcastsDescription: "Módulo para publicar canles de podcast.", + podcastsDescription: "Módulo para publicar canles de podcast.", + podcastCreate: "Crear un podcast", + podcastUpdate: "Actualizar", + podcastDelete: "Borrar", + podcastCreateSectionTitle: "Crear un podcast", + podcastUpdateSectionTitle: "Actualizar o podcast", + podcastTitleLabel: "Título", + podcastTitlePlaceholder: "Nome da canle", + podcastDescriptionLabel: "Descrición", + podcastDescriptionPlaceholder: "De que trata este podcast?", + podcastCategoryLabel: "Categoría", + podcastCategoryNews: "NOVAS", + podcastCategoryMusic: "MÚSICA", + podcastCategoryTalk: "CHARLA", + podcastCategoryEducation: "EDUCACIÓN", + podcastCategoryTech: "TECNOLOXÍA", + podcastCategoryCulture: "CULTURA", + podcastCategoryCommunity: "COMUNIDADE", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "OUTROS", + podcastTagsLabel: "Etiquetas", + podcastCoverLabel: "Portada (imaxe ou vídeo)", + podcastEpisodeAudioLabel: "Ficheiro de audio ou vídeo (máx.: 75MB)", + podcastTagsPlaceholder: "Introduce etiquetas separadas por comas", + podcastFilterAll: "TODOS", + podcastFilterMine: "MEUS", + podcastFilterRecent: "RECENTES", + podcastFilterTop: "TOP", + podcastFilterViewers: "OÍNTES", + podcastSearchPlaceholder: "Buscar en podcasts...", + podcastNoItems: "Non se atoparon podcasts.", + podcastEpisodesLabel: "Episodios", + podcastPlaysLabel: "Reproducións", + podcastLastEpisode: "Último episodio", + podcastEpisodesTitle: "Episodios", + podcastNoEpisodes: "Aínda non hai episodios.", + podcastEpisodeAdd: "Engadir un episodio", + podcastEpisodeCreateTitle: "Publicar un episodio", + podcastEpisodeUpdateTitle: "Actualizar o episodio", + podcastEpisodeTitleLabel: "Título do episodio", + podcastEpisodeTitlePlaceholder: "Título do episodio", + podcastEpisodeDescriptionLabel: "Notas", + podcastEpisodeDescriptionPlaceholder: "Que pasa neste episodio, ligazóns, créditos...", + podcastEpisodePublish: "Publicar", + podcastPlayed: "Reproducido", + podcastMarkPlayed: "Marcar como reproducido", + typePodcast: "PODCAST", + typePodcastEpisode: "EPISODIO", + typePodcastOpinion: "OPINIÓN DE EPISODIO", + typePodcastPlay: "REPRODUCIÓN", + statsPodcast: "Podcasts", + statsPodcastEpisode: "Episodios", + statsPodcastPlay: "Reproducións", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "Un blog que segues ten unha entrada nova.", + podcastBotEpisodeTitle: "Un podcast que segues ten un episodio novo.", + podcastLabel: "PODCAST", + podcastEpisodeLabel: "EPISODIO", + campaignsTitle: "Campañas", + modulesCampaignsLabel: "Campañas", + modulesCampaignsDescription: "Descobre e xestiona campañas na túa rede.", + campaignsDescription: "Descobre e xestiona campañas na túa rede.", + campaignCreate: "Crear unha campaña", + campaignUpdate: "Actualizar", + campaignDelete: "Borrar", + campaignClose: "Pechar", + campaignReopen: "Reabrir", + campaignCreateSectionTitle: "Crear unha campaña", + campaignUpdateSectionTitle: "Actualizar a campaña", + campaignTitleLabel: "Título", + campaignTitlePlaceholder: "Que queres conseguir?", + campaignTextLabel: "Texto", + campaignTextPlaceholder: "Explica a causa, quen decide e que pides...", + campaignCategoryLabel: "Categoría", + campaignCategoryEnvironment: "MEDIO AMBIENTE", + campaignCategoryRights: "DEREITOS", + campaignCategoryHealth: "SAÚDE", + campaignCategoryEducation: "EDUCACIÓN", + campaignCategoryInfrastructure: "INFRAESTRUTURA", + campaignCategoryCulture: "CULTURA", + campaignCategoryEconomy: "ECONOMÍA", + campaignCategoryOther: "OUTROS", + campaignGoalLabel: "Obxectivo de sinaturas", + campaignDeadlineLabel: "Prazo", + campaignTagsLabel: "Etiquetas", + campaignTagsPlaceholder: "Introduce etiquetas separadas por comas", + campaignFilterAll: "TODAS", + campaignFilterMine: "MIÑAS", + campaignFilterSigned: "ASINADAS", + campaignFilterRecent: "RECENTES", + campaignFilterTop: "TOP", + campaignStatusOpen: "ABERTA", + campaignStatusAchieved: "LOGRADA", + campaignStatusUnachieved: "NON LOGRADA", + campaignStatusClosed: "PECHADA", + campaignSearchPlaceholder: "Buscar en campañas...", + campaignNoItems: "Non se atoparon campañas.", + campaignSignaturesLabel: "Sinaturas", + campaignSignaturesTitle: "Apoios", + campaignNoSignatures: "Aínda non hai sinaturas.", + campaignSignTitle: "Asinar esta campaña", + campaignSignPlaceholder: "Por que a apoias? (opcional)", + campaignSign: "Asinar", + campaignSigned: "Xa a asinaches!", + campaignNotSigned: "Aínda falta a túa sinatura", + campaignElevate: "Elevar ao Parlamento", + campaignElevated: "Elevada ao Parlamento", + campaignUpdatesTitle: "Actualizacións", + campaignNoUpdates: "Aínda non hai actualizacións.", + campaignUpdatePlaceholder: "Que pasou?", + campaignAddUpdate: "Publicar unha actualización", + campaignQrAlt: "QR para asinar esta campaña", + campaignQrCaption: "Escanea para asinar", + typeCampaign: "CAMPAÑA", + typeCampaignSignature: "SINATURA", + typeCampaignUpdate: "ACTUALIZACIÓN DE CAMPAÑA", + statsCampaign: "Campañas", + statsCampaignSignature: "Sinaturas de campañas", + statsCampaignUpdate: "Actualizacións de campañas", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "Unha campaña que segues ten unha actualización.", + campaignBotAchievedTitle: "Unha campaña que segues alcanzou o seu obxectivo.", + campaignBotRaisedTitle: "Unha campaña que asinaches elevouse ao Parlamento.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Unha ruta que reservaches cambiou.", + logisticsBotClosedTitle: "Unha ruta que reservaches pechouse.", + logisticsBotBookedTitle: "Alguén reservou a túa ruta.", + logisticsBotCancelledTitle: "Cancelouse unha reserva da túa ruta.", + logisticsBotConfirmedTitle: "A túa reserva confirmouse: o paquete está en camiño.", + logisticsBotRejectedTitle: "A túa reserva foi rexeitada.", + logisticsBotDeliveredTitle: "O teu paquete chegou.", + campaignLabel: "CAMPAÑA", + campaignGenerateReport: "Xerar un informe", + logisticsGenerateReport: "Xerar un informe", + mailingGenerateReport: "Xerar un informe", + campaignUpdateEdited: "Editada", + mailingMessagesLabel: "Mensaxes", + logisticsBookingsLabel: "Reservas", + backupTitle: "Copia de seguranza", + modulesBackupLabel: "Copia de seguranza", + modulesBackupDescription: "Módulo para exportar e importar as túas chaves e facer copias de seguranza cifradas.", + backupDescription: "Módulo para exportar e importar as túas chaves e facer copias de seguranza cifradas.", + backupTypeRecovery: "RECUPERACIÓN", + backupTypeKeys: "EXPORTAR CHAVES", + backupTypeFull: "COPIA COMPLETA", + backupTypeRestore: "RESTAURAR UNHA COPIA", + backupTypeInstant: "INSTANTÁNEA", + backupInstantDescription: "Un .zip simple con todo o teu rexistro e os teus ficheiros, sen a túa chave secreta e sen contrasinal. Rápida, pero ", + backupInstantRisk: "quen teña o ficheiro pode lelo todo.", + backupKitPageDescription: "O kit de recuperación só se pode abrir desde este dispositivo.", + backupKitId: "ID de Oasis", + backupKitDate: "Xerado", + backupKitQrAlt: "Código QR da túa chave secreta", + backupKitStep1: "Imprímeo ou cópiao a man e gárdao nun lugar seguro e sen conexión.", + backupKitStep2: "Para recuperalo, instala Oasis nun dispositivo novo e importa este segredo desde Copia de seguranza › Chaves.", + backupKitStep3: "Destrúe esta páxina se algunha vez queda exposta.", + backupKitPdfButton: "Xerar PDF", + backupScopeLabel: "Alcance", + backupScopeAll: "TODO", + backupScopeMine: "SÓ O MEU CONTIDO", + backupSinceLabel: "Desde", + backupEstimateTitle: "Estimación", + backupFileInfo: "A túa copia cifrada descargarase no teu dispositivo (nome: oasis-backup-DATA.oasisbk)", + backupExportButton: "Descargar a copia", + backupRestorePassword: "Escribe o contrasinal que usaches ao facer a copia", + backupRestoreButton: "Restaurar a copia", + backupRestoredLine: "Copia restaurada.", + backupRestoredMessages: "Mensaxes engadidas", + backupRestoredSkipped: "Xa existentes", + backupRestoredBlobs: "Ficheiros engadidos", + backupRestoredState: "Configuración restaurada", + backupRestoredFailed: "Fallidos", + backupRestoredForked: "Diverxentes", + backupRestoreRunning: "Estase restaurando a copia. Esta páxina actualízase soa; mentres tanto podes seguir usando Oasis.", + backupRestoreElapsed: "Transcorrido", + backupRestoreFailedLine: "Non se puido restaurar a copia.", + backupRestoreForkMine: "Este dispositivo xa publicou mensaxes coa túa identidade que non están na copia, así que non se puido aplicar enriba delas. Restáuraa nunha instalación limpa xusto despois de importar as túas chaves, antes de facer nada máis.", + verificationTitle: "Verificación", + verificationDescription: "Comproba a cadea do teu feed, detecta bifurcacións e revisa ficheiros orfos ou ausentes.", + verificationRun: "Executar a verificación", + verificationTotalMessages: "mensaxes no rexistro", + verificationFeed: "O teu feed", + verificationMessages: "mensaxes", + verificationLastSequence: "última secuencia", + verificationGaps: "Ocos", + verificationBrokenLinks: "Ligazóns rotas", + verificationBadHashes: "Hashes incorrectos", + verificationBadSignatures: "Sinaturas incorrectas", + verificationDuplicates: "Secuencias duplicadas", + verificationForks: "Bifurcacións", + verificationBlobs: "Ficheiros", + verificationBlobsPresent: "presentes", + verificationBlobsReferenced: "referenciados", + verificationBlobsOrphan: "orfos", + verificationBlobsMissing: "ausentes", + verificationBlobsMissingOwn: "Ficheiros teus que faltan neste dispositivo", + verificationBlobsMissingOthers: "Ficheiros doutros habitantes aínda sen descargar", + verificationBlobsUnavailable: "Non se puideron listar os ficheiros deste dispositivo.", + wikiTitle: "Wikis", + modulesWikiLabel: "Wikis", + modulesWikiDescription: "Módulo para descubrir e xestionar wikis.", + wikiDescription: "Módulo para descubrir e xestionar wikis.", + wikiCreate: "Crear unha wiki", + wikiUpdate: "Actualizar", + wikiDelete: "Borrar", + wikiEdit: "Editar", + wikiTitleLabel: "Título", + wikiTitlePlaceholder: "Título da wiki...", + wikiBodyLabel: "Contido", + wikiBodyPlaceholder: "Liga outras wikis con [[Título da wiki]] ou [[Título da wiki|un texto propio]].\n\nMenciona un habitante con @id.\n\nEtiqueta con #oasis.\n\nPega ligazóns como https://solarnethub.com\n\nUnha liña en branco abre un parágrafo novo; os saltos de liña simples consérvanse.\n\nAnexa calquera medio compatible e incrustarase aquí.", + formatBarLabel: "Formato", + wikiStatusLabel: "Tipo de wiki", + wikiStatusOpen: "ABERTA", + wikiStatusClosed: "PECHADA", + wikiTagsLabel: "Etiquetas", + wikiTagsPlaceholder: "Introduce etiquetas separadas por comas", + wikiSummaryLabel: "Resumo do cambio", + wikiSummaryPlaceholder: "Que cambiou?", + wikiFilterAll: "TODAS", + wikiFilterMine: "MIÑAS", + wikiFilterRecent: "RECENTES", + wikiFilterLinked: "LIGADAS", + wikiChanges: "Cambios recentes", + wikiNoPages: "Non se atoparon wikis.", + wikiNoChanges: "Aínda non hai cambios.", + wikiHistory: "Historial", + wikiVersion: "Versión", + wikiVersions: "versións", + wikiRestore: "Restaurar", + wikiDiff: "Cambios", + wikiViewVersion: "Ver", + wikiLinkedPages: "Páxinas ligadas", + wikiNoLinkedPages: "Esta páxina aínda non se liga con ningunha outra.", + wikiMissingLink: "A páxina aínda non existe", + wikiDuplicateTitle: "Xa existe unha páxina con este título. Cambia o título para crear unha páxina distinta.", + wikiCreateSectionTitle: "Crear unha wiki", + wikiUpdateSectionTitle: "Actualizar a wiki", + wikiSearchPlaceholder: "Buscar na Wiki...", + wikiCurrentVersion: "Versión actual", + wikiViewingVersion: "Estás vendo unha versión antiga", + wikiBackToPage: "Volver á páxina", + wikiLinkedChip: "Ligada", + wikiRestoredSummary: "Versión restaurada", + wikiExportPdf: "Xerar a páxina", + wikiPreview: "Vista previa", + tribeSectionWiki: "WIKI", + tribeWikiCreate: "Crear unha wiki", + tribeWikiEmpty: "Aínda non hai wikis.", + typeWiki: "WIKI", + padsTitle: "Pads", + padTitle: "Pad", + modulesPadsLabel: "Pads", + modulesPadsDescription: "Módulo para xestionar editores de texto colaborativos.", + padCreate: "Crear un pad", + padUpdate: "Actualizar", + padDelete: "Eliminar", + padTitleLabel: "Título", + padTitlePlaceholder: "Escribe o título do pad...", + padStatusLabel: "Estado", + padTypeLabel: "Tipo de pad", + padStatusOpen: "ABERTO", + padStatusInviteOnly: "SÓ CONVIDADOS", + padStatusClosed: "PECHADO", + padDeadlineLabel: "Data límite", + padTagsLabel: "Etiquetas", + padTagsPlaceholder: "Introduce etiquetas separadas por comas", + padMembersLabel: "Membros", + padCreated: "Creado", + padStartEditing: "Editar", + padEditorPlaceholder: "Comeza a escribir...", + padSubmitEntry: "Enviar", + padNoEntries: "Aínda non hai entradas.", + padAllSectionTitle: "Todos os pads", + padMineSectionTitle: "Os meus pads", + padsDescription: "Xestiona editores de texto cifrados e colaborativos na túa rede.", + roomsTitle: "Salas", + roomsDescription: "Descobre e únete a salas de videoconferencia cifradas na túa rede.", + modulesRoomsLabel: "Salas", + modulesRoomsDescription: "Módulo para descubrir e unirse a salas de videoconferencia cifradas.", + roomFilterRecent: "RECENTES", + roomFilterMine: "MIÑAS", + roomFilterAll: "TODAS", + roomFilterLive: "EN DIRECTO", + roomFilterOpen: "ABERTAS", + roomFilterInvite: "SÓ CONVIDADOS", + roomFilterClosed: "PECHADAS", + roomCreate: "Crear sala", + roomUpdate: "Actualizar", + roomDelete: "Eliminar", + roomClose: "Pechar", + roomReopen: "Reabrir", + roomCreateSectionTitle: "Crear sala", + roomUpdateSectionTitle: "Actualizar sala", + roomTitlePlaceholder: "Título da sala", + roomDescriptionLabel: "Descrición", + roomDescriptionPlaceholder: "De que ides falar?", + roomTypeLabel: "Tipo de sala", + roomTagsLabel: "Etiquetas", + roomStatusOpen: "ABERTA", + roomStatusInviteOnly: "SÓ CONVIDADOS", + roomStatusClosed: "PECHADA", + roomParticipants: "Participantes", + roomJoin: "Entrar", + roomLeave: "Saír", + roomFull: "Sala chea", + roomLiveChip: "EN DIRECTO", + roomMuted: "silenciado", + roomRec: "Gravar audio", + roomRecStop: "Parar a gravación", + roomStreamFile: "Emitir", + roomStreamStop: "Parar", + roomStreaming: "Emitindo", + liveFullscreen: "Pantalla completa", + roomStreamPickTitle: "Escolle un vídeo", + roomStreamPickHint: "Vídeos deste dispositivo. O que escollas emítese desde onde está, sen copialo nin publicalo.", + roomStreamGo: "Emitir na sala", + roomStreamNotFound: "Ese vídeo non está nos teus cartafoles.", + roomPublicWarning: "Esta sala retransmítese na web pública: calquera pode ver e oír o que pasa nela.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} entrou na sala", + roomEventLeft: "{name} saíu da sala", + roomEventRecStart: "{name} comezou a gravar", + roomEventRecStop: "{name} deixou de gravar", + roomEventMuted: "{name} silenciouse", + roomEventUnmuted: "{name} quitou o silencio", + roomEventHand: "{name} ergueu a man", + roomRaiseHand: "Erguer a man", + roomLowerHand: "Baixar a man", + confirmActionTitle: "Confirma esta acción", + confirmActionButton: "Confirmar", + confirmJoinText: "Vas unirte usando esta invitación.", + confirmFollowText: "Vas apoiar a este habitante.", + confirmSignText: "Vas asinar esta campaña.", + confirmDownloadText: "Vas descargar este ficheiro ao teu dispositivo.", + roomRecordings: "Gravacións", + roomClearNotices: "Limpar", + roomLineSwitchboard: "Centraliña", + roomLineDndError: "O número desta sala non acepta chamadas.", + roomNumberHint: "Marca este número desde Teléfono para entrar na sala.", + liveRefresh: "Refrescar", + roomNobody: "Agora mesmo non hai ninguén na sala.", + roomSomeone: "{n} na sala agora mesmo.", + roomYou: "ti", + roomYouAreIn: "Estás dentro", + roomNobodyInside: "Agora mesmo non hai ninguén nesta sala.", + roomErrorFull: "A sala está chea. Poderás entrar cando alguén saia.", + roomErrorUnreachable: "Agora mesmo non se pode chegar á sala. Téntao máis tarde.", + roomErrorBusy: "Xa estás nunha chamada ou noutra sala.", + roomErrorClosed: "Esta sala está pechada.", + roomAccessDenied: "Esta sala é só con convite. Precisas un convite para entrar.", + roomsNoItems: "Non se atoparon salas.", + roomSearchPlaceholder: "Buscar salas...", + roomInviteMode: "Convite", + tribeSectionRooms: "SALAS", + tribeRoomCreate: "Crear sala", + tribeRoomsEmpty: "Aínda non hai salas.", + typeRoom: "SALAS", + typeRoomMember: "PARTICIPANTE DE SALA", + statsRoom: "Salas", + statsRoomMember: "Participantes de salas", + favoritesFilterRooms: "SALAS", + invitesRoomsTitle: "Salas", + inviteCodeInvalid: "Este código de convite non é válido ou xa se usou.", + inviteAlreadyMember: "Xa formas parte. Podes abrilo desde o seu módulo.", + actionFailed: "Non se puido facer. Téntao de novo.", + publishTooShort: "A túa publicación é demasiado curta.", + walletSendFailed: "Non se puido enviar o pagamento: {reason}", + pixeliaCoordsError: "Esas coordenadas están fóra da grella.", + courtsErrTitle: "Cómpre un título.", + courtsErrRespondent: "Indica contra quen é o caso.", + courtsErrRespondentId: "Ese non é un Oasis ID válido.", + courtsErrMethod: "Escolle como se resolverá o caso.", + courtsErrCaseNotFound: "Non se atopou ese caso.", + courtsErrBrief: "Escribe a túa resposta.", + courtsErrStance: "Escolle a túa postura.", + courtsErrResult: "Escribe o resultado.", + courtsErrTerms: "Escribe os termos do acordo.", + courtsErrMediator: "Engade polo menos un mediador.", + courtsErrSelfMediator: "Non podes ser mediador no teu propio caso.", + courtsErrJudge: "Escolle un xuíz.", + courtsErrSelfJudge: "Non podes ser xuíz do teu propio caso.", + courtsErrVisibility: "Escolle quen pode ver o caso.", + courtsErrNomination: "Non se atopou esa nominación.", + opinionAlreadyGiven: "Xa deches a túa opinión.", + cipherPasswordShort: "O contrasinal debe ter polo menos 32 caracteres.", + errorForbidden: "Non tes acceso a isto.", + errorNotFound: "Non se atopou.", + errorClosed: "Está pechado.", + errorInviteOnly: "Isto é só con convite. Precisas un convite.", + errorTooLong: "Ese texto é demasiado longo.", + errorRequired: "Falta información obrigatoria.", + errorInvalid: "Parte da información non é válida.", + errorNotAuthor: "Só quen o creou pode facer isto.", + panicRemoved: "A túa blockchain eliminouse.", + padRecentSectionTitle: "Pads recentes", + padOpenSectionTitle: "Pads abertos", + padClosedSectionTitle: "Pads pechados", + padCreateSectionTitle: "Crear un pad novo", + padUpdateSectionTitle: "Actualizar o pad", + typePad: "PADS", + padNew: "NOVO", + padAddFavorite: "Engadir a favoritos", + padRemoveFavorite: "Quitar de favoritos", + padClose: "Pechar", + padBackToEditor: "Volver ao editor", + padSearchPlaceholder: "Buscar en Pads...", + + calendarsTitle: "Calendarios", + calendarTitle: "Calendario", + modulesCalendarsLabel: "Calendarios", + modulesCalendarsDescription: "Módulo para descubrir e xestionar calendarios.", + typeCalendar: "CALENDARIOS", + calendarCreate: "Crear un calendario", + calendarUpdate: "Actualizar", + calendarTitleLabel: "Título", + calendarTitlePlaceholder: "Título do calendario...", + calendarStatusLabel: "Estado", + calendarTypeLabel: "Tipo de calendario", + calendarStatusOpen: "ABERTO", + calendarStatusClosed: "PECHADO", + calendarDeadlineLabel: "Data límite", + calendarTagsLabel: "Etiquetas", + calendarTagsPlaceholder: "Introduce etiquetas separadas por comas", + calendarParticipantsLabel: "Participantes", + calendarCreated: "Creado", + calendarJoin: "Unirse ao calendario", + calendarGenerateInvite: "Xerar un convite", + calendarDateLabel: "Data", + calendarDatePlaceholder: "Describe esta data...", + calendarNoteLabel: "Notas", + calendarNotePlaceholder: "Engadir unha nota...", + calendarFirstDateLabel: "Data", + calendarFirstNoteLabel: "Notas", + calendarIntervalLabel: "Intervalo", + calendarIntervalNone: "Sen repetición", + calendarIntervalWeekly: "Semanal", + calendarIntervalMonthly: "Mensual", + calendarIntervalYearly: "Anual", + calendarFormDescription: "Descrición", + calendarNoDates: "Non hai datas engadidas.", + calendarNoNotes: "Sen notas.", + calendarsNoItems: "Non se atoparon calendarios.", + calendarsDescription: "Descobre e xestiona calendarios na túa rede.", + calendarMonthPrev: "← Anterior", + calendarMonthNext: "Seguinte →", + calendarMonthNames: "Xaneiro,Febreiro,Marzo,Abril,Maio,Xuño,Xullo,Agosto,Setembro,Outubro,Novembro,Decembro", + calendarDayInitials: "L,M,M,X,V,S,D", + calendarAllSectionTitle: "Calendarios", + calendarRecentSectionTitle: "Calendarios recentes", + calendarFavoritesSectionTitle: "Favoritos", + calendarMineSectionTitle: "Os teus calendarios", + calendarOpenSectionTitle: "Calendarios abertos", + calendarClosedSectionTitle: "Calendarios pechados", + calendarCreateSectionTitle: "Crear un calendario novo", + calendarUpdateSectionTitle: "Actualizar o calendario", + calendarAddFavorite: "Engadir a favoritos", + calendarDeleteNote: "Eliminar", + calendarRemoveFavorite: "Quitar de favoritos", + calendarSearchPlaceholder: "Buscar no calendario...", + calendarAddEntry: "Engadir unha entrada", + statsCalendar: "Calendarios", + statsCalendarDate: "Datas de calendario", + statsCalendarNote: "Notas de calendario", + + modulesChatsLabel: "Chats", + modulesChatsDescription: "Módulo para descubrir e xestionar chats.", + typeChat: "CHATS", + typeChatMessage: "MENSAXE DE CHAT", + chatsTitle: "Chats", + chatMineSectionTitle: "Os teus chats", + chatRecentTitle: "Chats recentes", + chatFavoritesTitle: "Favoritos", + chatOpenTitle: "Chats abertos", + chatClosedTitle: "Chats pechados", + chatDescription: "Descrición", + chatCategory: "Categoría", + chatStatus: "ESTADO", + chatCreate: "Crear un chat", + chatUpdate: "Actualizar", + chatDelete: "Borrar", + chatClose: "Pechar o chat", + chatVisitChat: "VER O CHAT", + chatUntitled: "Chat sen título", + chatNoItems: "Non se atoparon chats.", + chatParticipants: "Participantes", + chatMessagesLabel: "Mensaxes", + chatThreadsLabel: "Fíos", + chatCreatedAt: "CREADO", + chatSearchPlaceholder: "Buscar en Chats...", + chatStatusOpen: "ABERTO", + chatStatusInviteOnly: "SÓ CONVIDADOS", + chatStatusClosed: "PECHADO", + chatSendMessage: "Enviar", + chatJumpLatest: "Última mensaxe", + chatReply: "Responder", + chatReplyCancel: "Cancelar a resposta", + chatPin: "Fixar", + chatUnpin: "Desfixar", + chatPinned: "Fixadas", + chatPickChat: "Escolle un chat para abrilo", + chatNoneYet: "Aínda non hai ningún chat dispoñible.", + activitySearchPlaceholder: "Buscar en Actividade...", + trendingSearchPlaceholder: "Buscar en Tendencias...", + opinionsSearchPlaceholder: "Buscar en Opinións...", + votesSearchPlaceholder: "Buscar en Votacións...", + gamesSearchPlaceholder: "Buscar en Xogos...", + schoolTitle: "Escola", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "Editouse unha páxina wiki que segues.", + wikiBotRestoredTitle: "Restaurouse unha versión anterior dunha páxina wiki que segues.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Un estudante novo apuntouse ao teu curso.", + schoolBotInvitedTitle: "Convidáronte a un curso.", + schoolBotAdmittedTitle: "Admitíronte nun curso.", + schoolBotCertificateTitle: "Recibiches un certificado.", + schoolBotPassedTitle: "Un estudante aprobou o teu curso.", + schoolBotLessonTitle: "Publicouse unha lección nova.", + invitesSchoolTitle: "Escola", + schoolEnrollLabel: "Unirse ao curso", + schoolExamRetryLater: "Poderás volver intentalo pasadas 24 horas.", + schoolApproved: "APROBADO", + schoolExamPassMark: "Nota de aprobado (0-10)", + schoolExamPassed: "APROBADO", + schoolExamPending: "Pendente", + schoolExamFailed: "SUSPENSO", + schoolExamAddQuestion: "Engadir unha pregunta", + schoolExamCreateQuestion: "Crear unha pregunta", + schoolExamOption: "Opción", + schoolExamCorrect: "Opción correcta", + schoolExamPoints: "Puntos", + schoolMaterialTextPlaceholder: "Notas para esta lección...", + schoolMaterials: "Materiais", + schoolAddMaterial: "Engadir material", + schoolNoMaterials: "Aínda non hai materiais.", + bankYourSchoolBalance: "Os teus ingresos da escola", + schoolProgress: "Progreso", + schoolProgressMark: "Marcar como completada", + schoolProgressDone: "Completada", + schoolExams: "Exames", + schoolExamCreate: "Crear un exame", + schoolExamTitlePlaceholder: "Título do exame", + schoolExamHelp: "Unha pregunta por liña: Pregunta? ; opción ; opción correcta* ; opción", + schoolExamQuestionsPlaceholder: "Que é SSB? ; Unha base de datos ; Un protocolo P2P* ; Un navegador", + schoolExamSubmit: "Enviar as respostas", + schoolExamScore: "A túa nota", + schoolNoScoresYet: "Aínda non hai resultados.", + schoolLessonUnit: "Unidade / fase", + schoolLessonUnitPlaceholder: "p. ex. Fase 1", + schoolLessonOrder: "Orde", + profileClearnetSchoolLabel: "Escola", + schoolStartDate: "Data de inicio", + schoolSessionDate: "Data da sesión", + schoolCourseChat: "Chat do curso", + agendaFilterSchool: "ESCOLA", + agendaFilterCampaigns: "CAMPAÑAS", + agendaFilterLogistics: "LOXÍSTICA", + schoolCourseType: "Tipo de curso", + schoolTypeOpen: "ABERTO", + schoolTypePaid: "DE PAGAMENTO", + schoolTypeInvite: "SÓ POR CONVITE", + schoolLessonLocked: "Lección cifrada — pídelle a chave do curso ao profesor.", + schoolGrantAccess: "Enviar a chave do curso", + schoolKeySent: "Chave enviada", + schoolOngoing: "En curso", + schoolFilterApplied: "APUNTADO", + schoolFilterOpen: "ABERTOS", + schoolFilterFavorites: "FAVORITOS", + schoolPendingPayment: "Pagamento pendente", + schoolPendingStudents: "Inscricións pendentes", + schoolViewBill: "Ver a factura", + schoolPaymentNote: "Ao inscribirte xérase unha factura en ECO (unha transferencia). A túa inscrición confírmase cando a transferencia queda asinada.", + schoolCloseCourse: "Pechar o curso", + schoolReopenCourse: "Reabrir o curso", + schoolDescription: "Ensina e aprende cursos na túa rede.", + schoolFilterAll: "TODOS", + schoolFilterMine: "MEUS", + schoolFilterTop: "TOP", + schoolFilterRecent: "RECENTES", + schoolCreateButton: "Crear un curso", + schoolCreateSectionTitle: "Crear un curso", + schoolUpdateSectionTitle: "Actualizar o curso", + schoolCourseTitlePlaceholder: "Nome do teu curso", + schoolCourseDescriptionPlaceholder: "Que vas ensinar neste curso?", + schoolCourseImage: "Imaxe do curso (tamaño máx.: 75MB)", + schoolTags: "Etiquetas", + schoolTagsPlaceholder: "Introduce etiquetas separadas por comas", + schoolPrice: "Prezo (ECO)", + schoolVisibility: "Visibilidade", + schoolVisibilityPublic: "Público", + schoolVisibilityInvite: "Por convite", + schoolStatus: "Estado", + schoolClosed: "Pechado", + schoolFree: "Gratis", + schoolCreate: "Crear o curso", + schoolUpdate: "Actualizar o curso", + schoolSearchPlaceholder: "Buscar na Escola...", + schoolSortTitle: "Título", + schoolNoCourses: "Non se atoparon cursos.", + schoolStudents: "Estudantes", + schoolTeacher: "Profesor", + schoolCreatedAt: "CREADO", + schoolLessons: "Leccións", + schoolNoLessons: "Aínda non hai leccións.", + schoolEnrollToSee: "Inscríbete para ver as leccións deste curso.", + schoolEnrollButton: "INSCRIBIRSE", + schoolUnenrollButton: "Abandonar o curso", + schoolInviteRequired: "Este curso é por convite.", + schoolTeacherPanel: "Panel do profesor", + schoolStudentPanel: "Panel do estudante", + schoolAddLesson: "Crear unha lección", + schoolLessonTitlePlaceholder: "Título da lección", + schoolLessonTextPlaceholder: "Describe as nocións que cobre esta lección...", + schoolInviteStudents: "Convidar estudantes", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "CONVIDAR", + schoolInvited: "Convidados", + schoolIssueCertificate: "Crear un certificado", + schoolCertified: "Certificado", + schoolCertificateTextPlaceholder: "Nota do certificado (opcional)", + schoolLessonOpen: "Abrir a lección", + schoolCertificatePdf: "PDF", + schoolCertificates: "Certificados", + schoolNoCertificates: "Aínda non se emitiron certificados.", + schoolEditButton: "Editar", + schoolDeleteButton: "Borrar", + typeSchool: "ESCOLA", + schoolCourseLabel: "CURSO", + emergencyLabel: "EMERXENCIA", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Cursos", + statsSchoolLesson: "Leccións", + statsSchoolEnroll: "Inscricións", + statsSchoolCertificate: "Certificados da escola", + modulesSchoolLabel: "Escola", + modulesSchoolDescription: "Módulo para intercambiar coñecemento.", + welcomeStepUxAction: "Gardar vista", + welcomeStepWorkflowTitle: "Que uso de OASIS tes en mente?", + welcomeStepWorkflowText: "Oasis ten moitos módulos. Escolle a configuración que encaixe contigo e só quedarán activados os útiles. Podes axustalo cando queiras en Módulos.", + welcomeStepWorkflowAction: "Gardar configuración", + welcomeWorkflow_full: "Por defecto", + chatRateLimitTitle: "Demasiadas mensaxes", + chatRateLimitMessage: "Alcanzaches o límite de mensaxes que esta sala acepta nunha hora. Téntao máis tarde.", + chatMessagePlaceholder: "Escribe a túa mensaxe...", + chatNoMessages: "Aínda non hai mensaxes.", + chatTitlePlaceholder: "Título do chat", + chatDescriptionPlaceholder: "Descrición do chat", + chatTagsPlaceholder: "Introduce etiquetas separadas por comas", + chatImageLabel: "Selecciona un ficheiro de imaxe (.jpeg, .jpg, .png, .gif)", + chatAuthor: "Autor", + chatAddFavorite: "Engadir a favoritos", + chatRemoveFavorite: "Quitar de favoritos", + chatPM: "MP", + chatStatusLabel: "Estado", + chatTypeLabel: "Tipo de chat", + chatCategoryLabel: "Categoría", + gamesTitle: "Xogos", + gamesDescription: "Descobre e xoga algúns xogos.", + gamesFilterAll: "TODOS", + gamesPlayButton: "XOGAR!", + modulesGamesLabel: "Xogos", + modulesGamesDescription: "Módulo para descubrir e xogar algúns xogos.", + modulesGraphosLabel: "Graphos", + modulesGraphosDescription: "Módulo para explorar a rede como un mapa interactivo de nodos.", + gamesCocolandTitle: "Cocoland", + gamesCocolandDesc: "Un coco con ollos que salta palmeiras e colecciona ECOins. Ata onde podes chegar?", + gamesTheFlowTitle: "ECOinflow", + gamesTheFlowDesc: "Conecta PUBs con habitantes mediante validadores, tendas e acumuladores. Sobrevive á ameaza CBDC!", + gamesNeonInfiltratorTitle: "Neon Infiltrator", + gamesNeonInfiltratorDesc: "Infíltrate na grella, recolle datos confidenciais, esquiva os drons de seguridade e escapa. Cantos niveis podes superar?", + gamesSpaceInvadersTitle: "Space Invaders", + gamesSpaceInvadersDesc: "Detén a invasión alieníxena! Destrúe vagas de invasores antes de que cheguen ao chan.", + gamesArkanoidTitle: "Arkanoid", + gamesArkanoidDesc: "Rompe todos os ladrillos coa túa pa e a pelota. Un clásico desafío arcade.", + gamesPingPongTitle: "PingPong", + gamesPingPongDesc: "Ping-pong clásico contra unha IA. Gaña quen chegue primeiro a 5 puntos.", + gamesAsteroidsTitle: "Asteroids", + gamesAsteroidsDesc: "Pilota a túa nave por un campo de asteroides mortal. Dispáralles antes de que te alcancen.", + gamesRockPaperScissorsTitle: "Pedra, papel, tesoiras", + gamesRockPaperScissorsDesc: "Pedra, papel ou tesoiras contra unha IA. Gaña quen leve dúas de tres roldas.", + gamesTikTakToeTitle: "TikTakToe", + gamesTikTakToeDesc: "Tres en raia clásico contra a IA. Consegue tres en liña para gañar.", + gamesFlipFlopTitle: "FlipFlop", + gamesFlipFlopDesc: "Lanza unha moeda e aposta cara ou cruz. Canta sorte tes?", + games8BallTitle: "8Ball Pool", + games8BallDesc: "Billar visto desde arriba. Preme para apuntar e mantén premido para cargar a potencia. Mete todas as bólas co menor número de tiros.", + gamesArtilleryTitle: "Artillaría", + gamesArtilleryDesc: "Apunta o canón, ten en conta o vento e acerta no branco. 5 roldas: gaña quen faga menos tiros.", + gamesLabyrinthTitle: "Labirinto", + gamesLabyrinthDesc: "Sae do labirinto antes de que se esgoten os movementos. Cada nivel é máis grande e máis difícil.", + gamesCocomanTitle: "Cocoman", + gamesCocomanDesc: "Come todos os puntos e esquiva as pantasmas. As pantasmas móvense en tempo real: non te deteñas!", + gamesTetrisTitle: "Tetris", + gamesAudioPendulumTitle: "Péndulo sonoro", + gamesAudioPendulumDesc: "Simulador de física caótica con síntese de audio en tempo real. As velocidades angulares convértense en frecuencias e os picos, en golpes de percusión. Non hai dúas simulacións que soen igual.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Pincha visuais en directo con dous pratos: escolle un patrón para cada un, axusta velocidade, cor e zoom, mestura entre ambos e lanza escintileos ao ritmo. Marca o tempo ou deixa que o micrófono leve o pulso.", + gamesTetrisDesc: "Os clásicos bloques que caen. Completa liñas para puntuar. Canto tempo aguantarás?", + gamesMinesweeperTitle: "Buscaminas", + gamesMinesweeperDesc: "A clásica caza de minas. Limpa o taboleiro para subir de nivel — grellas máis grandes, máis minas, máis puntuación.", + gamesFilterScoring: "PUNTUACIÓNS", + gamesHallOfFame: "Salón da fama", + gamesHallPlayer: "Xogador", + gamesHallScore: "Puntuación", + gamesNoScores: "Aínda non hai puntuacións.", + chatAccessDenied: "Non tes acceso a este chat. Solicita un convite para acceder ao contido.", + padAccessDenied: "Non tes acceso a este pad. Solicita un convite para acceder ao contido.", + contentAccessDenied: "Non tes acceso a este contido.", + blockAccessRestricted: "Acceso restrinxido", + tribeContentAccessDenied: "Acceso denegado", + tribeContentAccessDeniedMsg: "Este contido pertence a unha tribo. Debes ser membro para acceder.", + tribeViewTribes: "Ver as tribos", + tribeFilesEmpty: "Aínda non hai ficheiros.", + filesLabel: "Ficheiros", + profileClearnetFilesLabel: "Ficheiros", + filesTitle: "Ficheiros", + filesDescription: "Explora e xestiona ficheiros na túa rede.", + fileAllSectionTitle: "Ficheiros", + fileMineSectionTitle: "Os teus ficheiros", + fileRecentSectionTitle: "Ficheiros recentes", + fileTopSectionTitle: "Ficheiros destacados", + fileFavoritesSectionTitle: "Favoritos", + fileFilterAll: "TODOS", + fileFilterMine: "MEUS", + fileFilterRecent: "RECENTES", + fileFilterTop: "DESTACADOS", + fileFilterFavorites: "FAVORITOS", + fileCreateSectionTitle: "Subir un ficheiro", + fileUpdateSectionTitle: "Actualizar o ficheiro", + fileCreateButton: "Subir un ficheiro", + fileUpdateButton: "Actualizar", + fileAddFavoriteButton: "Engadir a favoritos", + fileRemoveFavoriteButton: "Quitar de favoritos", + fileFileLabel: "Selecciona un ficheiro (máx.: 75MB)", + calendarFilterRecent: "RECENTES", + calendarFilterAll: "TODOS", + calendarFilterMine: "MEUS", + calendarFilterFavorites: "FAVORITOS", + calendarFilterOpen: "ABERTOS", + calendarFilterClosed: "PECHADOS", + padFilterRecent: "RECENTES", + padFilterAll: "TODOS", + padFilterMine: "MEUS", + padFilterOpen: "ABERTOS", + padFilterClosed: "PECHADOS", + chatFilterRecent: "RECENTES", + chatFilterAll: "TODOS", + chatFilterMine: "MEUS", + chatFilterFavorites: "FAVORITOS", + chatFilterOpen: "ABERTOS", + chatFilterClosed: "PECHADOS", + licenseLabel: "Licenza", + phoneTitle: "Teléfono", + phoneDescription: "Xestiona as túas videochamadas cifradas de extremo a extremo.", + phoneUnavailable: "As chamadas non están dispoñibles neste dispositivo.", + phoneCallButton: "Chamar", + profileVisibilityPhone: "Número de teléfono", + phoneNumberUnknown: "Ninguén na túa rede ten o número {number}.", + phoneNumberChoose: "Comproba a quen queres chamar ao {number}:", + phoneAnswer: "Contestar", + phoneSpeaker: "Altofalante", + phoneEarpiece: "Auricular", + phoneReject: "Rexeitar", + phoneHangup: "Colgar", + phoneMute: "Silenciar", + phoneUnmute: "Activar micrófono", + phoneCameraOn: "Cámara on", + phoneCameraOff: "Cámara off", + settingsPhoneVideo: "Vídeo", + settingsPhoneVideoOn: "Permitido en chamadas e salas", + settingsPhoneVideoOff: "Só audio", + phoneSilence: "Non oír", + phoneUnsilence: "Volver oír", + phoneCalling: "Chamando a", + phoneIsCalling: "está a chamarte", + notifyNewPmFrom: "Nova MP de", + notifyNewPamFrom: "Novo PaM de", + notifyUpdateLabel: "Actualización", + updateInstalledLabel: "Versión instalada", + updateAvailableLabel: "Versión dispoñible", + updateChangesLabel: "Cambios", + updateFilesLabel: "Ficheiros", + updateOriginLabel: "Orixe", + phoneRecording: "Gravando PaM para", + phoneLeaveMessage: "Ninguén contesta: deixa a túa mensaxe despois do sinal.", + phoneLeaveMessageBusy: "Está noutra chamada: deixa a túa mensaxe despois do sinal.", + phoneLeaveMessageUnreachable: "Non se puido contactar: deixa a túa mensaxe despois do sinal.", + phoneConnecting: "Conectando con", + phoneSendPam: "Enviar PaM", + phoneCancel: "Cancelar", + phoneSearchPlaceholder: "Buscar en Teléfono...", + phonePamTitle: "Gravacións de audio", + phoneNew: "Novo", + phoneDelete: "Borrar", + phoneNoPams: "Non tes gravacións de audio.", + phoneHistoryTitle: "Historial", + phoneLatestTitle: "Últimas chamadas", + phoneCreateButton: "Nova chamada", + phoneLimitsHint: "Ata {n} habitantes por chamada; con máis dun é unha chamada conxunta.", + phoneErrorPub: "Non se pode chamar a un pub.", + phoneJointCall: "Chamada conxunta", + phoneWith: "Con", + phoneResult: "Resultado", + phoneWhen: "Cando", + phoneDuration: "Duración", + phoneOutgoing: "Saínte", + phoneIncoming: "Entrante", + phoneNoHistory: "Aínda non hai rexistros.", + phoneOutcomeEnded: "Atendida", + phoneOutcomeMissed: "Perdida", + phoneOutcomeRejected: "Rexeitada", + phoneOutcomeNoanswer: "Sen resposta", + phoneOutcomeCancelled: "Cancelada", + phoneOutcomePam: "PaM enviado", + phoneOutcomeFailed: "Fallida", + phoneDialAdd: "Engadir", + phoneDialBack: "Borrar", + phoneDialClear: "Limpar", + phoneDialPlaceholder: "Número ou Oasis ID", + phoneDialInhabitants: "Habitantes", + phoneDialRemove: "Quitar", + phoneOwnNumber: "Ese é o teu propio número.", + phoneSettingsDesc: "Escolle quen pode chamarte.", + phoneDnd: "Non molestar", + phoneOpen: "Aberto", + phoneRecipientRefuses: "O habitante ao que intentas chamar non permite recibir chamadas.", + phoneRefusedNotice: "Estes habitantes non permiten recibir chamadas e non se lles chamou:", + fileEmptyError: "O ficheiro está baleiro. Escolle outro ficheiro.", + fileTitleLabel: "Título", + fileTitlePlaceholder: "Título", + fileDescriptionLabel: "Descrición", + fileDescriptionPlaceholder: "Descrición", + fileTagsLabel: "Etiquetas", + fileTagsPlaceholder: "Introduce etiquetas separadas por comas", + fileSizeLabel: "Tamaño", + fileDownloadButton: "DESCARGA", + fileDetailsButton: "DETALLES", + fileNoFile: "Sen ficheiro ficheiro", + fileSearchPlaceholder: "Buscar en Ficheiros...", + fileSearchButton: "Buscar", + fileSortRecent: "Máis recentes", + fileSortOldest: "Máis antigos", + fileSortTop: "Máis votados", + fileNoMatch: "Non se atoparon ficheiros coincidentes.", + fileUpdatedAt: "Actualizado", + typeFile: "FICHEIROS", + modulesFilesLabel: "Ficheiros", + modulesFilesDescription: "Módulo para descubrir e xestionar ficheiros.", + tribeSectionFiles: "FICHEIROS", + fileDescription: "Descrición", + fileDownload: "Descargar", + fileTypeLabel: "Tipo", + torrentsTitle: "Torrents", + torrentsDescription: "Explora e xestiona torrents na túa rede.", + torrentAllSectionTitle: "Torrents", + torrentMineSectionTitle: "Os teus torrents", + torrentRecentSectionTitle: "Torrents recentes", + torrentTopSectionTitle: "Torrents destacados", + torrentFavoritesSectionTitle: "Favoritos", + torrentFilterAll: "TODOS", + torrentFilterMine: "MEUS", + torrentFilterRecent: "RECENTES", + torrentFilterTop: "DESTACADOS", + torrentFilterFavorites: "FAVORITOS", + torrentCreateSectionTitle: "Subir un torrent", + torrentUpdateSectionTitle: "Actualizar o torrent", + torrentCreateButton: "Subir un torrent", + torrentUpdateButton: "Actualizar", + torrentDeleteButton: "Eliminar", + torrentAddFavoriteButton: "Engadir a favoritos", + torrentRemoveFavoriteButton: "Quitar de favoritos", + torrentFileLabel: "Seleccionar un ficheiro torrent (.torrent)", + torrentTitleLabel: "Título", + torrentTitlePlaceholder: "Título", + torrentDescriptionLabel: "Descrición", + torrentDescriptionPlaceholder: "Descrición", + torrentTagsLabel: "Etiquetas", + torrentTagsPlaceholder: "Introduce etiquetas separadas por comas", + torrentSizeLabel: "Tamaño", + torrentFromContent: "Xerar torrent", + torrentFromContentMissing: "O ficheiro aínda non está dispoñible neste nodo. Téntao máis tarde.", + torrentDownloads: "Descargas", + torrentFetchOasis: "Descargar en Oasis", + torrentSaveFile: "Gardar ficheiro", + torrentSeeds: "sementes", + seedsLabel: "Sementes", + seedAction: "Apoiar", + torrentSourceFile: "Ficheiro", + torrentStateQueued: "En cola", + torrentStateProbing: "Buscando sementes", + torrentStateDownloading: "Descargando", + torrentStateDone: "Completada", + torrentStateFailed: "Fallida", + torrentStateNoSource: "Sen sementes conectadas, agardando", + torrentStateCancelled: "Cancelada", + torrentNoDownloads: "Aínda non hai descargas.", + torrentOriginalSection: "Orixinal", + torrentCancel: "Cancelar", + torrentDownloadButton: "DESCARGA", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "DETALLES", + torrentNoFile: "Sen ficheiro torrent", + noTorrents: "Non se atoparon torrents.", + noFiles: "Non se atoparon ficheiros.", + torrentSearchPlaceholder: "Buscar en Torrents...", + torrentSearchButton: "Buscar", + torrentSortRecent: "Máis recentes", + torrentSortOldest: "Máis antigos", + torrentSortTop: "Máis votados", + torrentNoMatch: "Non se atoparon torrents coincidentes.", + torrentUpdatedAt: "Actualizado", + statsTorrent: "Torrents", + statsFile: "Ficheiros", + typeTorrent: "TORRENTS", + modulesTorrentsLabel: "Torrents", + modulesTorrentsDescription: "Módulo para descubrir e xestionar torrents.", + favoritesFilterTorrents: "TORRENTS", + favoritesFilterFiles: "FICHEIROS", + favoritesFilterMarket: "MERCADO", + favoritesFilterEvents: "EVENTOS", + favoritesFilterForum: "FORO", + favoritesFilterHousing: "VIVENDA", + favoritesFilterJobs: "EMPREGOS", + favoritesFilterProjects: "PROXECTOS", + favoritesFilterReports: "INFORMES", + favoritesFilterTasks: "TAREFAS", + favoritesFilterTransfers: "TRANSFERENCIAS", + favoritesFilterVotes: "VOTACIÓNS", + favoritesFilterShopProducts: "ARTIGOS DE TENDA", + tribeSectionTorrents: "TORRENTS", + tribeCreateTorrent: "Subir un torrent", + tribeCreateFile: "Subir ficheiro", + settingsWishTitle: "Desexo", + settingsWishDesc: "Determina como accedes a todo o contido e a túa experiencia na rede Oasis.", + settingsWishWhole: "Multiverso", + settingsWishMutuals: "Só apoio mutuo", + settingsWishOnlyLan: "Só LAN", + settingsWishLocal: "Local", + settingsPmVisibilityTitle: "Mensaxes privadas", + settingsPmVisibilityDesc: "Configura o nivel de exposición a mensaxes privadas que queres ter na rede.", + settingsPmVisibilityWhole: "Multiverso", + settingsPmVisibilityMutuals: "Só apoio mutuo", + mute: "Silenciar", + unmute: "Deixar de silenciar", + relationshipMuted: "Silenciado por ti", + hideContent: "Agochar para min", + unhideContent: "Mostrar de novo", + hiddenByYou: "Agochado por ti", + settingsTabContent: "Contido", + settingsHiddenTitle: "Contido agochado", + settingsHiddenDesc: "Habitantes que silenciaches e contido que agochaches.", + settingsMutedLabel: "Habitantes silenciados", + settingsHiddenLabel: "Contido agochado", + settingsMutedEmpty: "Ninguén silenciado.", + settingsHiddenEmpty: "Nada agochado.", + settingsPmVisibilityFollowing: "Só xente á que sigo", + settingsMentionsVisibilityTitle: "Mencións", + settingsMentionsVisibilityDesc: "Escolle de quen che chegan as mencións.", + pmMutualNotice: "O envío é unha barreira de UX. A recepción é un filtro de atención (o cifrado xa está replicado).", + inhabitantsPendingFollowsTitle: "Solicitudes de apoio pendentes", + inhabitantsPendingAccept: "Aceptar", + inhabitantsPendingReject: "Rexeitar", + tribeSectionGovernance: "GOBERNO", + tribePadInviteRequired: "Non tes acceso ao pad. Pide un convite para acceder ao contido.", + tribeChatInviteRequired: "Non tes acceso ao chat. Pide un convite para acceder ao contido.", + tribeGovernanceDesc: "Goberno interno desta tribo.", + tribeGovernanceNoGov: "Sen goberno activo", + tribeGovernanceNoGovDesc: "Esta tribo aínda non ten goberno. Propón candidaturas para iniciar o proceso.", + tribeGovCardTitle: "Goberno actual", + tribeGovCycleSince: "INICIO DO CICLO", + tribeGovCycleEnd: "FIN DO CICLO", + tribeGovTimeRemaining: "TEMPO RESTANTE", + tribeGovPopulation: "POBOACIÓN", + tribeGovMethod: "MÉTODO", + tribeGovVotesReceived: "VOTOS RECIBIDOS", + tribeGovLeader: "LÍDER", + tribeGovFilterGovernment: "GOBERNO", + tribeGovFilterCandidatures: "CANDIDATURAS", + tribeGovFilterLaws: "LEIS", + tribeGovCandidatureId: "Candidatura", + tribeGovCandidatureMethod: "Método", + tribeGovCandidatureProposeBtn: "Publicar a candidatura", + tribeGovRuleTitle: "Título da regra", + tribeGovRuleBody: "Corpo da regra", + tribeGovProposals: "PROPOSTAS", + tribeGovRevocations: "REVOGACIÓNS", + tribeGovHistorical: "HISTÓRICO", + tribeGovRules: "REGRAS", + tribeGovernanceAlreadyPublished: "Esta tribo xa ten unha candidatura aberta no ciclo actual do parlamento global.", + tribeGovernanceProposeInternal: "Propoñer unha candidatura interna", + tribeGovernanceInternalCandidatures: "Candidaturas internas", + tribeGovernanceNoCandidatures: "Non hai candidaturas abertas.", + tribeGovernanceAddRule: "Engadir unha regra", + tribeGovernanceNoRules: "Aínda non hai regras.", + tribeGovernanceNoLeaders: "Aínda non hai líderes elixidos.", + tribeGovernanceComingSoon: "Proximamente no módulo de goberno desta tribo.", + tribeGovNoProposals: "Aínda non hai ningunha proposta", + tribeGovNoLaws: "Aínda non hai ningunha lei", + tribeGovNoRevocations: "Aínda non hai ningunha revogación", + tribeGovNoHistorical: "Aínda non hai ningún rexistro", + logsTitle: "Bitácora", + logsDescription: "Leva un diario privado das túas experiencias.", + logsViewTitle: "Bitácora", + logsColumnType: "Tipo", + logsView: "Ver", + logsUpdateButton: "Actualizar", + logsEditTitle: "Actualizar o rexistro", + logsCreateTitle: "Crear unha entrada", + logsWriteButton: "Escribir", + logsTextPlaceholder: "Describe as túas experiencias...", + logsModeAIWritten: "Asistente de IA", + logsModeManual: "Manual", + logsModeAI: "IA", + logsColumnLog: "Bitácora", + logsColumnDate: "Data", + logsDelete: "Eliminar", + logsEdit: "Actualizar", + logsFilterAlways: "SEMPRE", + logsFilterYear: "ÚLTIMO ANO", + logsFilterMonth: "ÚLTIMO MES", + logsFilterWeek: "ÚLTIMA SEMANA", + logsFilterToday: "HOXE", + logsComments: "Comentarios", + logsAuthorEntries: "Entradas", + logsCreate: "Crear unha entrada", + logsExport: "Exportar a bitácora", + logsExportOne: "Exportar o rexistro", + logsViewDetails: "Ver os detalles", + logsGenerateButton: "Xerar o texto", + logsSearchText: "Buscar na Bitácora...", + logsSearchAnyType: "Calquera tipo", + logsSearchButton: "Buscar", + logsEmpty: "Aínda non hai entradas.", + logsNothingNew: "Non hai accións novas que rexistrar.", + modulesLogsLabel: "Bitácora", + modulesLogsDescription: "Módulo para levar un diario privado das túas experiencias.", + statsLogsTitle: "Bitácora", + typeLog: "BITÁCORA", + + larpTitle: "L.A.R.P.", + larpDescription: "Unha capa de xogo de rol en vivo para a experimentación colaborativa.", + larpNoHousesMatch: "Ningunha casa coincide coa túa busca.", + larpSearchPlaceholder: "Buscar en L.A.R.P....", + larpAllHouses: "Casas:", + larpFilterRuling: "GOBERNA", + larpFilterHouses: "CASAS", + larpFilterRules: "FAQ", + larpRulesTitle: "FAQ do L.A.R.P.", + larpRulesEntryText: "Todo habitante comeza por defecto en ACADEMIA. Desde ACADEMIA, escolle unha casa no panel \"Unirse a unha casa\": isto abre o seu test de entrada. Responde as 10 preguntas e envía. Se superas o limiar (7/10), serás admitido automaticamente nesa casa; se non, serás rexeitado e permanecerás en ACADEMIA. En calquera caso o sistema rexistra o teu OasisID e o ciclo do intento, e impide outro intento ata que pase o período de espera de 30 ciclos.", + larpRulesGovernanceText: "Durante o seu ciclo, a casa gobernante luce a insignia \"Goberna\" e é a única cuxo Muro se amosa publicamente na lapela GOBERNA.", + larpRulesWallText: "Cada casa ten un Muro onde escriben os seus membros. O Muro dunha casa é privado para a casa, agás o da casa gobernante e o da ACADEMIA, que pode ler calquera.", + larpRulesSignText: "Os habitantes poden activalo (en /profile/edit > Sensores) para amosar a imaxe da súa casa actual como un selo no seu perfil, na súa tarxeta de autor e de habitante. O selo liga á páxina da casa.", + larpPostsTitle: "Muro", + larpPostsEmpty: "Aínda non hai publicacións.", + larpPostPlaceholder: "Que ten que dicir esta casa?", + larpPostSubmit: "Publicar", + larpCycleLabel: "Ciclo:", + bankRulesArchTaxFormula: "Taxa ARCH(usuario) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", + bankRulesArchTaxNote: "O imposto ARCH reflicte o custo a longo prazo de facer medrar e manter o arquivo da rede: canto máis antigos sexan os teus datos na rede, maior será a túa parte do custo de mantemento.", + bankRulesChipFormula: "band = ratio / reducer, onde ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). alto ≥ 0.66, medio ≥ 0.33, noutro caso baixo.", + bankRulesChipNote: "O chip de cada elemento colórese respecto á mensaxe máis grande vista na rede e suavízase segundo o número de habitantes activos — unha rede máis grande reparte a carga e empuxa máis elementos cara á banda verde.", + bankRulesEcoTaxFormula: "Taxa ECO(usuario) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", + bankRulesEcoTaxNote: "O imposto ECO dedúcese da RBU bruta cando se calcula a asignación da época. Os ECO deducidos non se redistribúen nesta época — quedan no saldo do pub e alimentan o fondo da próxima época a través dos límites reserveMin / alpha.", + bankRulesKarmaFormula: "karma = log10(1 + actions) + reputation", + bankRulesKarmaNote: "O karma derívase das túas accións (publicacións, votos, participación no parlamento/tribunais, etc.) cun factor de decaemento temporal, menos os gramos de carbono que xera o teu feed. O que recibe o teu contido tamén conta: as opinións, votos, reaccións e reproducións positivas suman karma, e as opinións ou votos negativos réstano cando varios habitantes coinciden.", + bankRulesNetFormula: "net_UBI = floor + max(0, gross_UBI − floor) − ECO Tax − ARCH Tax", + bankRulesPoolFormula: "pool = min(pubBal − reserveMin, capPerEpoch, alpha · pubBal)", + bankRulesShareFormula: "share = clamp(1 + karma/100, w_min, w_max) / sum(weights)", + bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", + bankTaxesArchTaxFootprintNote: "O imposto ARCH reflicte o custo de facer medrar e manter o arquivo da rede. Calcúlase a partir do tempo transcorrido entre o primeiro bloque que publicaches e o bloque máis novo da rede — canto máis tempo viva a túa historia no arquivo, maior será a túa parte do custo de mantemento.", + bankTaxesArchTaxNoteBold: "reducir directamente o imposto ARCH", + bankTaxesArchTaxNoteIntro: "O teu imposto ARCH dedúcese do excedente que a rede xera por riba da túa RBU; o valor base da RBU é un mínimo inamovible. O imposto nunca se deduce do importe fixo que lle corresponde a cada habitante como RBU. Os fondos deducidos alimentan o algoritmo de redistribución da riqueza e canalízanse cara a outros habitantes a través das súas reclamacións de RBU. Os habitantes máis novos e os que se dediquen especificamente a tarefas de arquivo, replicación e mantemento probablemente teñan algunha tarefa dedicada a manter san o arquivo da rede no teu nome. Ou poden dedicarse a ", + bankTaxesArchTaxNoteOutro: " mediante acordos de poda, deduplicación de datos redundantes e infraestrutura de mantemento compartida, e o consenso da rede pode requirir a súa contribución continua.", + bankTaxesUserNoteChips: "O chip de imposto ECO que se amosa xunto a cada elemento cambia de cor (verde / amarelo / vermello) segundo o tamaño do elemento en relación co bloque máis grande visto na rede, suavizado polo número de habitantes activos — unha rede solar-punk máis grande reparte a carga e empuxa máis elementos cara á banda verde.", + bankTaxesUserNoteOtherParams: "Polo de agora o imposto ECO derívase da pegada de carbono de cada bloque, pero co tempo pode incorporar outros parámetros — enerxía gastada na replicación, almacenamento redundante entre nodos, largo de banda dos blobs, custo computacional do descifrado, pegada de minaría das transaccións asociadas ou calquera outra carga medible que a rede acorde valorar.", + gamesHallDate: "Data", + gamesNewRecord: "Novo récord!", + indexingRefreshNote: "Esta páxina recárgase cada 10 segundos.", + indexingMessage: "Oasis está sincronizando a rede de nodos distribuída. Por favor, agarda.", + indexingTitle: "Sincronizando", + padsNoItems: "Aínda non hai pads.", + padVersionHistory: "Historial", + padAttributionView: "Por autora", + padVersionView: "Ver a versión", + statsChat: "Chats", + statsChatMessage: "Mensaxes de chat", + statsGameScore: "Xogos", + statsWiki: "Páxinas wiki", + statsPad: "Pads", + statsPadEntry: "Entradas de pad", + typeGameScore: "PUNTUACIÓNS", + bankTaxesLookupNote: "Introduce un ID de bloque para buscalo no teu feed local.", + bankTaxesUserNoteChipsClick: "Preme en calquera chip para inspeccionar o seu bloque no blockexplorer.", + mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntryText: "Os membros das casas poden xerar códigos de convite que dan acceso directo á casa.", + larpRulesOneHouseText: "Só podes pertencer a unha casa á vez. Se non pertences a ningunha, estás en ACADEMIA. Cada páxina de casa (agás ACADEMIA) amosa aos seus membros un botón \"Saír da casa\" que devolve a pertenza a ACADEMIA. Saír NON anula o período de espera do test.", + larpRulesTestEntryText: "No test WILL cada opción pondera unha ou máis casas; ao envialo, o sistema suma as puntuacións e admíteche automaticamente na casa con máis puntos.", + larpWillTestHint: "Unha vez completado, asignaráseche a casa que máis se axuste ás túas respostas. As túas respostas individuais procésanse localmente e nunca se almacenan — só se publica no teu feed a asignación de casa resultante.", + larpWillTestTitle: "Test WILL", + settingsLanDesc: "Anuncia periodicamente este nodo a outras instancias de Oasis na mesma rede local.", + settingsLanEnable: "Activar a difusión LAN", + settingsLanTitle: "Difusión LAN", + settingsReplicationTitle: "Replicación", + settingsReplicationDesc: "Configura o número de saltos que o teu nodo segue desde o teu propio feed ao replicar contido.", + settingsReplicationHopsLabel: "Saltos", + settingsBlobCacheTitle: "Caché de medios", + settingsBlobCacheDesc: "O contido doutros habitantes gárdase neste dispositivo. Escolle canto espazo pode ocupar.", + settingsBlobCacheUsage: "Uso actual", + settingsBlobCacheFiles: "ficheiros", + settingsBlobCacheLimit: "Límite", + settingsBlobCacheUnlimited: "Sen límite", + settingsBlobCacheCleanNow: "Limpar agora", + settingsBlobCacheCleaned: "{n} ficheiros eliminados, {size} liberados.", + aiApproveTagsLabel: "Etiquetas (separadas por comas)", + aiApproveTagsPlaceholder: "p. ex. oasis, gobernanza, ecoloxía", + aiApproveRatingLabel: "Valoración", + aiExchangeLang: "Idioma", + aiExchangeTags: "Etiquetas", + aiExchangeRating: "Valoración", + aiExchangeHelpful: "Útil", + aiExchangeMarkHelpful: "+1 útil", + larpCyclesUntilRuling: "Próximo ciclo de goberno", + larpVisitTribe: "Visitar a tribo", + larpStartJourney: "Comezar a viaxe", + larpGoverning: "Goberna:", + larpStartHere: "Comeza aquí:", + larpMyHouse: "A miña casa:", + larpMembersCount: "Membros", + larpRolesLabel: "Roles", + larpFunctionLabel: "Función", + larpMonthLabel: "Ciclo de goberno", + larpLeaveToAcademia: "Volver á ACADEMIA", + larpAcademiaJoinTitle: "Casas L.A.R.P.", + larpInviteRedeem: "Unirse á casa", + larpInviteCreate: "Xerar un código de convite", + larpInvitePlaceholder: "Código de convite", + larpInviteBannerTitle: "Novo código de convite", + larpInviteBannerHint: "Comparte este código con alguén que estea en ACADEMIA. Caduca en 30 ciclos ou tras un uso.", + larpTestRankingTitle: "Puntuación por casa", + invitesHousesTitle: "L.A.R.P.", + ecoTaxLabel: "Taxa ECO", + bankTaxes: "Impostos", + bankOverviewYourTaxes: "Impostos totais", + bankTaxesEcoTaxTitle: "Taxa ECO", + bankTaxesArchTaxTitle: "Taxa ARCH", + bankTaxesUserAmount: "A túa taxa ECO (importe que devolver)", + bankTaxesArchTaxUserAmount: "A túa taxa ARCH (importe que devolver)", + bankTaxesArchTaxFirstBlock: "Idade do teu primeiro bloque", + bankTaxesArchTaxRate: "Tipo", + bankTaxesTotalBlocks: "Bloques totais", + bankTaxesTotalBytes: "Bytes totais", + bankTaxesTotalCarbon: "Carbono total", + bankTaxesTotalEcoin: "Taxa ECO total (toda a vida)", + bankTaxesEcoTaxLifetime: "Taxa ECO (toda a vida)", + bankTaxesAnnualEcoin: "Taxa ECO (anual)", + bankTaxesMonthlyEcoin: "Taxa ECO (mensual)", + bankTaxesArchTaxLifetime: "Taxa ARCH (toda a vida)", + bankTaxesArchTaxAnnual: "Taxa ARCH (anual)", + bankTaxesArchTaxMonthly: "Taxa ARCH (mensual)", + bankTaxesTotalLifetime: "Total (toda a vida)", + bankTaxesTotalAnnual: "Total (anual)", + bankTaxesTotalMonthly: "Total (mensual)", + bankTaxesRate: "Tipo", + bankTaxesSpan: "Período de mostraxe", + bankTaxesLookupTitle: "Inspeccionar un bloque", + bankTaxesLookupPlaceholder: "ID do bloque (p. ex. %abc...=.sha256)", + bankTaxesLookupButton: "Inspeccionar", + bankTaxesLookupNotFound: "Non se atopou ningún bloque con ese ID no teu feed local.", + bankTaxesLookupAuthor: "Autor", + bankTaxesLookupType: "Tipo", + bankTaxesLookupSize: "Tamaño", + bankTaxesLookupCarbon: "Pegada de carbono", + bankTaxesLookupEcoin: "Taxa ECO", + bankExchangeEcoTaxAnnual: "Impostos de ECOin (anual)", + bankExchangeEcoTaxMonthly: "Impostos de ECOin (mensual)", + bankTaxesTypesLabel: "Selecciona que impostos queres pagar", + bankTaxesTypesApply: "Establecer os meus impostos", + bankRulesPoolTitle: "Fondo mensual", + bankRulesShareTitle: "Reparto por usuario", + bankRulesKarmaTitle: "Karma (puntuación de implicación)", + bankRulesEcoTaxTitle: "Taxa ECO", + bankRulesArchTaxTitle: "Taxa ARCH", + bankRulesChipTitle: "Banda de cor do chip da taxa ECO", + bankRulesEpochKind: "Granularidade da época", + bankRulesAlpha: "Alpha (cota máx. do saldo do pub por época)", + bankRulesReserveMin: "Reserva mínima (mantense no saldo do pub)", + bankRulesCapPerEpoch: "Límite por época (absoluto)", + bankRulesGraceDays: "Período de graza da reclamación", + bankRulesWMin: "Peso mínimo (w_min)", + bankRulesWMax: "Peso máximo (w_max)", + bankRulesFloor: "Mínimo por usuario (bruto)", + bankRulesCapUser: "Límite por usuario e época", + bankRulesEcoTaxRate: "Tipo", + bankRulesArchTaxRate: "Tipo", + bankRulesCarbonFactor: "Factor de carbono", + statsTombstoneEmpty: "Aínda non hai borrados (tombstones) na rede.", + blockchainBlockNotAvailable: "Este bloque non está no teu feed local. Pode pertencer a un nodo que aínda non replicas.", + blockchainBackToBlockexplorer: "Volver a Blockexplorer", + audioFilterBcs: "BCS", + audioTranscodeButton: "TRANSCODIFICAR", + audioTranscodeDetailTitle: "Transcodificar", + audioTranscodeDetailDescription: "Descodifica a carga útil incrustada e o mapa de composición orixinal da blockchain.", + audioTranscodeStegoTimestamp: "Xerado o", + audioTranscodeStegoMessage: "TEXTO", + audioTranscodeStegoEmpty: "(ningún)", + audioTranscodeStegoUnknown: "—", + audioTranscodeStegoNotFound: "Non se puido descodificar ningunha carga esteganográfica neste audio.", + audioTranscodeCompositionEmpty: "Este audio non inclúe ningunha composición de blockchain gardada.", + audioBackToBcs: "Volver a BCS", + melodyCompositionTitle: "Mapa de composición da blockchain", + melodyFilterMine: "MIÑAS", + melodyAllEmpty: "Aínda non hai composicións BCS doutros habitantes.", + melodyNoteUnavailable: "o bloque non está no teu feed local", + melodyUploadBcsTitle: "Publicar", + melodyUploadBcsButton: "Publicar", + melodyUploadStegoLabel: "Mensaxe oculta (esteganografía)", + melodyUploadStegoMaxLabel: "máx. 280 caracteres", + melodyUploadStegoPlaceholder: "Opcional.", + melodyUploadBcsNameNote: "O audio publicarase co nome xerado automaticamente", + larpLastAttemptTitle: "O teu último intento", + larpLastAttemptHouse: "Casa", + larpLastAttemptWhen: "Cando", + larpLastAttemptCooldown: "Próximo intento en", + larpTestTitle: "Test de entrada", + larpTestIntro: "Responde as 10 preguntas. Necesitas polo menos 7 respostas correctas para seres admitido. Podes intentar un test cada 30 ciclos sexa cal sexa o resultado.", + larpTestSubmit: "Unirse á casa", + larpTestCooldownActive: "Próximo test dispoñible en", + larpTestCooldownDays: "ciclos", + larpTestResultTitle: "Resultado do test", + larpTestScore: "Puntuación", + larpTestNextAttempt: "Podes intentar outro test en 30 ciclos.", + larpGoToHouse: "Ir á túa casa", + larpBackToAcademia: "Volver á ACADEMIA", + larpBadgeYou: "Ti", + larpBadgeRuling: "Goberna", + modulesLarpLabel: "L.A.R.P.", + modulesLarpDescription: "Módulo para a capa de xogo de rol en vivo.", + larpProfileQ1: "Cando te enfrontas a un problema complexo, que fas primeiro?", + larpProfileQ1O1: "Falar con outras persoas para buscar consenso", + larpProfileQ1O2: "Construír un prototipo para probalo", + larpProfileQ1O3: "Investigar a bibliografía", + larpProfileQ1O4: "Sabotear o sistema que o provoca", + larpProfileQ2: "Que lle dá sentido ao teu traballo diario?", + larpProfileQ2O1: "Defender os habitantes que quero", + larpProfileQ2O2: "Crear algo tanxible", + larpProfileQ2O3: "Sandar ou coidar a vida", + larpProfileQ2O4: "Traballar con palabras e ideas", + larpProfileQ2O5: "Facer rir os demais", + larpProfileQ3: "Cando xorde un conflito no teu grupo, ti…", + larpProfileQ3O1: "Lideras o diálogo", + larpProfileQ3O2: "Tomas partido e mantéñeste firme", + larpProfileQ3O3: "Soltas unha broma para rebaixar a tensión", + larpProfileQ3O4: "Cuestionas se o conflito é real", + larpProfileQ3O5: "Buscas as causas ecolóxicas de raíz", + larpProfileQ4: "O teu proxecto favorito a longo prazo sería…", + larpProfileQ4O1: "Construír unha cidade", + larpProfileQ4O2: "Restaurar un bosque", + larpProfileQ4O3: "Redactar unha constitución", + larpProfileQ4O4: "Organizar un festival", + larpProfileQ4O5: "Montar unha rede de defensa", + larpProfileQ4O6: "Deseñar unha máquina nova", + larpProfileQ4O7: "Coidar un arquivo", + larpProfileQ4O8: "Desmontar unha orde inxusta", + larpProfileQ5: "Que fai unha boa líder?", + larpProfileQ5O1: "Alguén que escoita e media", + larpProfileQ5O2: "Alguén capaz de loitar e protexer", + larpProfileQ5O3: "Alguén que coñece a historia", + larpProfileQ5O4: "Alguén que te fai sorrir", + larpProfileQ6: "A túa relación coas regras:", + larpProfileQ6O1: "As regras xorden do diálogo e da lei", + larpProfileQ6O2: "As regras débense cumprir estritamente", + larpProfileQ6O3: "As regras hai que rompelas a miúdo", + larpProfileQ6O4: "As regras deben servir á vida", + larpProfileQ6O5: "As regras erguen infraestruturas sólidas", + larpProfileQ7: "Como xestionas a información?", + larpProfileQ7O1: "Coido dela e presérvoa", + larpProfileQ7O2: "Compártoa a través de historias", + larpProfileQ7O3: "Cuestiono as súas orixes", + larpProfileQ7O4: "Extraio o útil", + larpProfileQ7O5: "Úsoa para sandar", + larpProfileQ8: "A túa idea de éxito é…", + larpProfileQ8O1: "Unha máquina que funciona", + larpProfileQ8O2: "Unha comunidade en paz", + larpProfileQ8O3: "Un festival animado", + larpProfileQ8O4: "Unha familia segura", + larpProfileQ8O5: "Un arquivo intacto", + larpProfileQ8O6: "Un dogma resgado", + larpProfileQ8O7: "Unha colleita próspera", + larpProfileQ8O8: "Un edificio rematado", + larpProfileQ9: "Ao erguerte, o que queres é…", + larpProfileQ9O1: "Adestrar o corpo", + larpProfileQ9O2: "Ler ou escribir", + larpProfileQ9O3: "Cultivar ou cociñar", + larpProfileQ9O4: "Argallar con algo", + larpProfileQ9O5: "Cuestionar a autoridade", + larpProfileQ9O6: "Planificar un proxecto", + larpProfileQ9O7: "Falar coas túas amigas", + larpProfileQ9O8: "Facer arte", + larpProfileQ10: "A túa debilidade podería ser:", + larpProfileQ10O1: "Falar demasiado", + larpProfileQ10O2: "Ser demasiado pragmática", + larpProfileQ10O3: "Ser demasiado idealista", + larpProfileQ10O4: "Ser demasiado disruptiva", + larpProfileQ10O5: "Ser demasiado ríxida", + larpProfileQ10O6: "Ser demasiado despreocupada", + larpProfileQ10O7: "Ser demasiado cautelosa", + larpProfileQ10O8: "Ser demasiado ambiciosa", + uxModeTitle: "UX", + uxModeDescription: "Selecciona que modo de navegación UX queres para a túa interface.", + uxModeMenus: "Bloques", + uxModeChats: "Conversas", + uxModeFeed: "Microblogging", + feedTrendingTitle: "Etiquetas en tendencia", + feedActiveTitle: "Habitantes activos", + cnBrandSub: "Libre · P2P · Federada", + cnSyncedPeers: "Nodos sincronizados", + cnLicense: "Licenza", + cnNotAccessible: "O contido non está accesible neste momento.", + cnUntitled: "Sen título", + cnSearchPlaceholder: "Buscar…", + cnHubTitle: "HUB Clearnet", + cnHubDescription: "Contido público compartido polos habitantes desta rede Oasis.", + cnHubEmpty: "Aínda non se compartiu contido público na Clearnet.", + cnPublicContent: "Contido", + cnTribesContent: "Tribos", + cnNetworkContent: "Rede", + cnCategoryEmpty: "Non hai contido nesta categoría.", + cnInhabitantEmpty: "Este habitante aínda non publicou contido na Clearnet.", + cnPrevPage: "← Anterior", + cnNextPage: "Seguinte →", + cnPageOf: "Páxina {page} de {pages}", + cnMoreInfo: "Máis información →", + cnOpenCourse: "Curso aberto", + cnAuction: "POXA", + cnExchange: "INTERCAMBIO", + cnRssDescription: "publicados polos habitantes deste HUB", + cnKindAudio: "Audio", + cnKindBlog: "Blog", + cnKindBookmark: "Marcador", + cnKindDocument: "Documento", + cnKindEvent: "Evento", + cnKindFeed: "Feed", + cnKindImage: "Imaxe", + cnKindJob: "Emprego", + cnKindMarket: "Mercado", + cnKindPodcast: "Podcast", + cnKindProject: "Proxecto", + cnKindCourse: "Curso", + cnKindShop: "Tenda", + cnKindTorrent: "Torrent", + cnKindFile: "Ficheiro", + cnKindVideo: "Vídeo", + cnKindWiki: "Wiki", + cnKindRoom: "Sala", + cnKindMap: "Mapa", + uxModeAINav: "IA", + + } +}; diff --git a/src/client/assets/translations/oasis_hi.js b/src/client/assets/translations/oasis_hi.js index 4b66a12..67dbe0e 100644 --- a/src/client/assets/translations/oasis_hi.js +++ b/src/client/assets/translations/oasis_hi.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { hi: { - bbAdd: "जोड़ें", - bbQuickActions: "त्वरित क्रियाएँ", - bbPickTitle: "एक मॉड्यूल पिन करें", - bbPickDesc: "नीचे की बार में + स्लॉट के लिए एक मॉड्यूल चुनें।", - bbPickNone: "कोई नहीं", - bbManage: "संपादित करें", - bbYourBar: "आपकी बार", - bbRemove: "हटाएँ", - bbFull: "अधिकतम पहुँच गया", - bbDone: "हो गया", - activityChangeFilter: "फ़िल्टर बदलें", - emptyConnectHint: "नेटवर्क से सिंक करने के लिए किसी pub से जुड़ें।", - emptyConnectCta: "pub से जुड़ें", - menuCommunity: "समुदाय", - activityGroupCommunity: "समुदाय और शासन", - activityGroupOrg: "संगठन", - activityGroupComm: "संचार", - activityGroupEconomy: "अर्थव्यवस्था", - activityGroupMedia: "सामग्री और मीडिया", - activityGroupOther: "अन्य", - languageName: "हिन्दी", shopOrderStatusPaid: "भुगतान प्राप्त", shopOrderStatusReceived: "प्राप्त", shopOrderMarkPaid: "भुगतान प्राप्त चिह्नित करें", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "खरीद", shopMyOrdersDescription: "आपके खरीद ऑर्डर।", shopMyOrdersEmpty: "आपके पास अभी कोई खरीद नहीं है।", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PM विक्रेता", shopOrderSeller: "विक्रेता", - shopOrderPaymentConcept: "भुगतान", shopOrderInvoice: "चालान", shopOrderInvoiceConcept: "चालान", shopOrderStatusPending: "लंबित", shopOrderStatusAccepted: "स्वीकृत", shopOrderStatusRejected: "अस्वीकृत", shopOrderStatusShipped: "भेजा गया", - shopOrderStatusDelivered: "वितरित", shopOrderAccept: "स्वीकार करें", shopOrderReject: "अस्वीकार करें", shopOrderMarkShipped: "भेजा गया चिह्नित करें", - shopOrderMarkDelivered: "वितरित चिह्नित करें", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "स्मार्ट अनुबंध", + shopOrderPmBuyer: "PM खरीदार", shopOrderContractConcept: "दुकान ऑर्डर", shopOrdersPending: "लंबित ऑर्डर", all: "सभी", @@ -75,18 +50,17 @@ module.exports = { viewJson: "JSON देखें", matchScore: "मिलान स्कोर", preferencesLabel: "प्राथमिकताएं", - inhabitantProfileTitle: "निवासी प्रोफ़ाइल", + inhabitantProfileTitle: "निवासी", contentWarningLabel: "सामग्री चेतावनी", invalidPost: "अमान्य सामग्री", invalidPostHint: "इस संदेश में अमान्य/खाली पाठ है।", - spreadBy: "द्वारा", spreadChron: "प्रसार", spreaded: "प्रसारित", - spreadMore: "और", spreadContentUnavailable: "सामग्री अभी उपलब्ध नहीं (प्रतिकृति लंबित)", filteredByTag: "टैग द्वारा फ़िल्टर", filteredByTagTitle: "टैग द्वारा फ़िल्टर", feedPublishedSuccess: "फ़ीड सफलतापूर्वक प्रकाशित!", + feedMastodonFailed: "Oasis में प्रकाशित हुआ, लेकिन Mastodon पर नहीं भेजा जा सका।", tribeFeedSent: "संदेश सफलतापूर्वक भेजा गया!", tribeContentEncrypted: "एन्क्रिप्टेड सामग्री", tribeTorrentsEmpty: "अभी तक कोई टोरेंट नहीं।", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "कोई गंभीरता नहीं", calendarDeleteDate: "तारीख हटाएं", calendarIntervalUntil: "तक", - bookmarkCategory: "श्रेणी", bookmarkLastVisit: "अंतिम विज़िट", profileClearnetBookmarksLabel: "बुकमार्क", - forumFilterHot: "लोकप्रिय", invitesPort: "पोर्ट", - currentlyUnrecheable: "त्रुटि!", - peerHostValidation: "मान्य IPv4 (जैसे 192.168.1.100) या होस्टनाम (जैसे pub.example.com)", + peerHostValidation: "मान्य IPv4 (जैसे 192.168.1.100) या होस्टनाम (जैसे pub.solarnethub.com)", peerPortValidation: "पोर्ट 1-65535", peerKeyValidation: "SSB ed25519 सार्वजनिक कुंजी (@<44 वर्ण base64>=.ed25519)", inboxJobUnsubscribedTitle: "आपकी नौकरी से सदस्यता समाप्त", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "कार्य विवरण", exportPasswordLabel: "निर्यात पासवर्ड", importPasswordPlaceholder: "पासवर्ड दर्ज करें", - searchCasesPlaceholder: "मामले खोजें…", + searchCasesPlaceholder: "न्यायालय में खोजें...", shopBuyDeliveryAddressPlaceholder: "डिलीवरी पता", shopBuyNotesPlaceholder: "नोट्स", bankTaxesUserNoteBold: "ECO कर को सीधे कम करना", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "मामले का समर्थन करें", courtsSettlementAcceptBtn: "समझौता स्वीकारें", courtsSupportCount: "समर्थक", - courtsVerdictVoteTitle: "फैसले पर मतदान", - courtsVerdictVoteLabel: "फैसला मतदान", - courtsVerdictVoteAccept: "स्वीकार करें", - courtsVerdictVoteReject: "अस्वीकार करें", - courtsVerdictVoteSubmit: "मत भेजें", reportsSetStatus: "स्थिति निर्धारित करें", reportsStatusClosed: "बंद", statsAvgPerInhabitant: "प्रति निवासी औसत", statsCarbonMaxAnnual: "वार्षिक अधिकतम अनुमान", statsCarbonNetwork: "नेटवर्क कुल", statsCarbonOfEstMax: "अनुमानित अधिकतम क्षमता का", + statsCarbonYearProgress: "वर्ष बीत चुका", + statsNetworkTitle: "नेटवर्क", + statsStorageTitle: "भंडारण", + statsEcoTaxTitle: "ECO कर", + statsAccountTitle: "खाता", + statsAveragesTitle: "औसत", statsCarbonOfNetwork: "नेटवर्क कुल का", statsCarbonUser: "आपका फुटप्रिंट", statsContentCountColumn: "गिनती", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "शीर्ष टैग", statsTopTypesTitle: "शीर्ष सामग्री प्रकार", statsTotalMsgs: "कुल संदेश", - feedViewDetails: "विवरण देखें", - eventFileLabel: "छवि संलग्न करें", - taskFileLabel: "छवि संलग्न करें", - courtsRespondentRequired: "प्रतिवादी आईडी आवश्यक है", extended: "मल्टीवर्स", extendedDescription: [ "जब आप किसी का समर्थन करते हैं तो आप उनके द्वारा समर्थित निवासियों की पोस्ट डाउनलोड कर सकते हैं, और वे पोस्ट यहाँ नवीनता के अनुसार क्रमबद्ध दिखाई देती हैं।", @@ -215,47 +183,76 @@ module.exports = { ], profile: "अवतार", inhabitants: "निवासी", - peersReplicatedFeeds: "प्रतिकृत फ़ीड", + peersReplicatedFeeds: "प्रतिकृत", + peersTorColumn: "TOR", + peersTorYes: "Tor से पहुंचा जा सकता है", + peersTorNo: "Tor से नहीं पहुंचा जा सकता", + peersOasisId: "ID", + peersOasisVersion: "संस्करण", graphos: "ग्राफोस", graphosDescription: "आपके आसपास के नेटवर्क का इंटरैक्टिव मानचित्र।", + graphosFederation: "फ़ेडरेशन", + graphosFedPubs: "PUB", + graphosFedUnreachable: "पहुँच से बाहर", + graphosFedLinks: "पारस्परिक कड़ियाँ", + graphosFedIsolated: "अलग-थलग", + graphosFedNoData: "कोई डेटा नहीं", + graphosFedMutual: "पारस्परिक", + graphosFedOneWay: "एकतरफ़ा", + graphosFedTransport: "नेटवर्क", + graphosFedWith: "इनके साथ फ़ेडरेटेड", + graphosZoom: "बड़ा करें", + graphosFedInhabitants: "निवासी", + graphosFedClearnet: "इंटरनेट", + graphosFedBoth: "इंटरनेट + TOR", + graphosFedEmpty: "आपका नोड अभी किसी PUB को नहीं जानता।", graphosViewingGraphOf: "देख रहे", graphosBackToMine: "← मेरा नेटवर्क", graphosShowing: "दिखा रहे", graphosYou: "आप", graphosTotalNodes: "कुल नोड्स", - manualMode: "मैनुअल मोड", mentions: "उल्लेख", mentionsDescription: [ - strong("वे पोस्ट जो आपको @उल्लेख करती हैं"), - ", नवीनता के अनुसार क्रमबद्ध।", + strong("हर संदेश जो आपको @उल्लेख करता है"), + ".", ], - nextPage: "अगला", - previousPage: "पिछला", noMentions: "आपको अभी तक कोई @उल्लेख नहीं मिला है।", + mentionsSearchPlaceholder: "उल्लेख में खोजें...", privateReminders: "रिमाइंडर", + inboxFilterPms: "निजी संदेश", + inboxFilterNotifications: "सूचनाएँ", + inboxMarkRead: "पढ़ा हुआ चिह्नित करें", + inboxMarkUnread: "अपठित चिह्नित करें", + inboxUnreadChip: "अपठित", + inboxMarkAllRead: "सभी को पठित चिह्नित करें", + inboxDeleteShown: "सभी हटाएँ", + inboxArchive: "संग्रहित करें", + inboxUnarchive: "संग्रह से निकालें", + inboxFilterArchived: "संग्रहित", + inboxEarlierMessages: "पिछले संदेश", + inboxSortThread: "थ्रेड के अनुसार", + inboxSortRecent: "नवीनतम पहले", + inboxAllBots: "बॉट", + settingsInboxBotsTitle: "इनबॉक्स सूचनाएँ", + settingsInboxBotsDesc: "चुनें कि कौन से बॉट आपके इनबॉक्स में सूचित कर सकते हैं। अचयनित बॉट छिपा दिए जाते हैं और उनकी सूचनाएँ गिनी नहीं जातीं।", inboxFiltersLabel: "फ़िल्टर:", inboxToggleToMutuals: "पारस्परिक पर स्विच करें", inboxToggleToWhole: "सभी पर स्विच करें", - privateFrom: "से", - privateTo: "को", privateDate: "दिनांक", pmReply: "उत्तर दें", pmReplies: "उत्तर", - pmNew: "नया", - pmMarkRead: "पढ़ा हुआ चिह्नित करें", inReplyTo: "के उत्तर में", + forumInThread: "थ्रेड में", pmPreview: "पूर्वावलोकन", pmPreviewTitle: "संदेश पूर्वावलोकन", peers: "पीयर", searchDescription: "विवरण", - imageSearch: "छवि खोज", searchFromLabel: "से", searchToLabel: "तक", - searchMinOpinionsLabel: "न्यून. राय", - searchInhabitantLabel: "निवासी (Oasis ID)", searchQueryLabel: "क्वेरी", - searchPerPageLabel: "प्रति पृष्ठ परिणाम", + searchPerPageLabel: "परिणाम", settings: "सेटिंग्स", + settingsDescription: "अपने नेटवर्क का परिवेश सेट करें।", continueReading: "पढ़ना जारी रखें", moreComments: "और टिप्पणी", readThread: "बाकी थ्रेड पढ़ें", @@ -265,68 +262,47 @@ module.exports = { melodyDescription: 'अपनी ब्लॉकचेन की धुन बजाएँ — हर ब्लॉक एक स्वर बन जाता है।', melodyEmpty: 'अभी कोई स्वर नहीं — आपकी ब्लॉकचेन ने अभी कोई ब्लॉक नहीं बनाया है।', melodyCompositionTitle: 'रचना', - melodyCompositionHelp: 'आपकी ब्लॉकचेन का हर ब्लॉक उसके प्रकार और आकार के अनुसार एक संगीत स्वर बन जाता है।', - melodyStatsTitle: 'प्रकार विश्लेषण', - melodyInhabitantLabel: 'निवासी', melodyTotalBlocks: 'स्वर', - melodyType: 'प्रकार', - melodyCount: 'ब्लॉक', melodyFilterMine: 'मेरी', melodyFilterAll: 'सभी', - melodyFilterShare: 'धुन अपलोड करें', - melodyShareTitle: 'अपनी धुन साझा करें', - melodyShareHelp: 'अपनी मौजूदा धुन का स्नैपशॉट प्रकाशित करें ताकि अन्य सुन और रीमिक्स कर सकें।', - melodyShareTitleLabel: 'शीर्षक (वैकल्पिक)', - melodyShareTitlePlaceholder: 'एक ब्लॉकचेन भूत', - melodyShareSubmit: 'धुन प्रकाशित करें', - melodyNoShared: 'अभी तक कोई ब्लॉकचेन धुन नहीं।', melodyRegenerate: 'पुनः उत्पन्न करें', melodyDownload: "BCS डाउनलोड करें", - profileActivityTitle: 'गतिविधि', - profileActivityMore: 'सारी गतिविधि देखें', profileContentSectionTitle: 'अवतार सामग्री', profileContentHelp: 'चुनें कि आपके अवतार पर कौन से मॉड्यूल दिखाए जाएँगे।', profileSensorsHelp: 'आपके अवतार पर दिखाई जाने वाली वैकल्पिक मेट्रिक्स।', profileClearnetHelp: 'Oasis के बाहर से एक्सेस किए जा सकने वाले मॉड्यूल।', profileHubAll: 'सभी', - profileHubTitle: 'सार्वजनिक सामग्री', - footerPulseTitle: 'पल्स', - footerPulsePeers: 'पीयर', - footerPulseInbox: 'इनबॉक्स', - footerPulseSync: 'अंतिम सिंक', - footerPulseEcoValue: 'ECO/घंटा', - footerPulseLastActivity: 'अंतिम गतिविधि', footerLicenseLabel: 'लाइसेंस', - profileSwitchToOasis: 'Oasis पर वापस जाएँ', - profileSwitchToClearnet: 'Clearnet में गोता लगाएँ', - profileVisibilityFediverse: "फ़ेडिवर्स", - profileSwitchToFediverse: "फ़ेडिवर्स में गोता लगाएँ", - coordLabel: 'निर्देशांक (जैसे, A3)', - coordPlaceholder: 'निर्देशांक दर्ज करें', + profileVisibilityFediverse: "मल्टीवर्स", contributorsTitle: "योगदानकर्ता", - pixeliaBy: "द्वारा", colorLabel: 'रंग चुनें', paintButton: 'रंग भरें!', - invalidCoordinate: 'गलत निर्देशांक', - goToMuralButton: "भित्तिचित्र देखें", totalPixels: 'कुल पिक्सेल', modules: "मॉड्यूल", modulesViewTitle: "मॉड्यूल", modulesViewDescription: "मॉड्यूल सक्षम या अक्षम करके अपना वातावरण सेट करें।", inbox: "इनबॉक्स", + inboxSearchPlaceholder: "इनबॉक्स में खोजें...", multiverse: "मल्टीवर्स", - fediverse: "फ़ेडिवर्स", - fediverseDescription: "अपने अन्य फ़ेडिवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।", + fediverse: "मल्टीवर्स", + fediverseDescription: "अपने अन्य मल्टीवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।", fediverseStatus: "स्थिति", - fediverseDisconnected: "फ़ेडिवर्स डिस्कनेक्ट है। %LINK% या कनेक्शन स्थिति जाँचें।", - fediverseSettingsTitle: "फ़ेडिवर्स", + fediverseDisconnected: "मल्टीवर्स डिस्कनेक्ट है। %LINK% या कनेक्शन स्थिति जाँचें।", + fediverseSettingsTitle: "मल्टीवर्स", fediverseInstanceLabel: "पता", fediverseTokenLabel: "एक्सेस टोकन", - fediverseTokenHelp: "अपना एक्सेस टोकन सेट करें। इसका उपयोग Oasis के भीतर से आपके Mastodon खाते को प्रबंधित करने के लिए किया जाएगा।", fediverseConnect: "कनेक्ट करें", fediverseConnectedAs: "इस रूप में कनेक्टेड", + nextcloudServerLabel: "सर्वर", + nextcloudUserLabel: "उपयोगकर्ता", + nextcloudFolderLabel: "फ़ोल्डर", + nextcloudUploadButton: "अपलोड", + nextcloudFileLabel: "एक फ़ाइल चुनें", + fileExternalLink: "बाहरी लिंक", + nextcloudErrConnect: "इन विवरणों से Nextcloud तक नहीं पहुँचा जा सका।", + nextcloudErrUpload: "Nextcloud पर अपलोड विफल रहा।", + nextcloudErrShare: "Nextcloud ने फ़ाइल के लिए सार्वजनिक लिंक नहीं दिया।", fediverseDisconnect: "डिस्कनेक्ट करें", - fediverseEmpty: "जब आप अपनी %LINK% से अन्य फ़ेडिवर्स खाते कनेक्ट करेंगे, तो आप उन्हें यहाँ से प्रबंधित कर सकते हैं।", fediverseEmptyLink: "सेटिंग्स", fediverseComposePlaceholder: "आप क्या सोच रहे हैं?", fediverseReplyPlaceholder: "अपना जवाब लिखें…", @@ -335,21 +311,15 @@ module.exports = { fediverseReply: "जवाब दें", fediverseBoosted: "ने बूस्ट किया", fediverseNoPosts: "आपकी टाइमलाइन खाली है।", - fediverseThread: "थ्रेड", fediverseTimeline: "टाइमलाइन्स", - fediverseManageTimeline: "टाइमलाइन प्रबंधित करें", fediverseFollowers: "फ़ॉलोअर्स", fediverseFollowing: "फ़ॉलोइंग", fediversePosts: "पोस्ट", fediverseJoined: "शामिल हुए", - fediverseOverview: "अवलोकन", fediverseRefresh: "रिफ़्रेश", fediverseWrite: "लिखें", fediversePreview: "पूर्वावलोकन", - fediverseEdit: "संपादित करें", - fediverseOpenFeed: "फ़ीड देखें", fediverseManage: "प्रबंधित करें", - fediverseStatusNotConnected: "कनेक्ट नहीं है", fediverseError: "Mastodon से संपर्क नहीं हो सका।", fediverseErrInstance: "अमान्य इंस्टेंस URL।", fediverseErrAuth: "अमान्य या समाप्त टोकन।", @@ -360,17 +330,76 @@ module.exports = { fediverseErrPost: "प्रकाशित नहीं हो सका।", fediverseErrMedia: "फ़ाइल अपलोड नहीं हो सकी।", fediverseErrEmpty: "कुछ लिखें या फ़ाइल जोड़ें।", - popularLabel: "हाइलाइट्स", - topicsLabel: "विषय", - latestLabel: "नवीनतम", - summariesLabel: "सारांश", - threadsLabel: "थ्रेड्स", - multiverseLabel: "मल्टीवर्स", - inboxLabel: "इनबॉक्स", - invitesLabel: "आमंत्रण", - walletLabel: "वॉलेट", - legacyLabel: "कुंजियाँ", - cipherLabel: "क्रिप्टर", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "फ़ोन नंबर", + telegramSendCode: "कोड भेजें", + telegramCodeHelp: "Telegram द्वारा अभी भेजा गया कोड दर्ज करें (ऐप या SMS)।", + telegramCodeLabel: "कोड", + telegramPasswordHelp: "इस खाते में दो-चरणीय सत्यापन है। अपना पासवर्ड दर्ज करें।", + telegramPasswordLabel: "पासवर्ड", + telegramVerify: "सत्यापित करें", + telegramCancel: "रद्द करें", + telegramChats: "चैट", + telegramUnread: "अपठित", + telegramOpen: "खोलें", + telegramSend: "भेजें", + telegramYou: "आप", + telegramNoChats: "अभी कोई चैट नहीं।", + telegramNoMessages: "अभी कोई संदेश नहीं।", + telegramComposePlaceholder: "संदेश लिखें…", + telegramChannel: "चैनल", + telegramGroup: "समूह", + telegramPrivate: "निजी", + telegramErrConnect: "Telegram से संपर्क नहीं हो सका।", + telegramErrTimeout: "Telegram ने जवाब देने में बहुत समय लिया।", + telegramErrAuth: "Telegram सत्र अमान्य या समाप्त। फिर से जोड़ें।", + telegramErrFetch: "चैट लोड नहीं हो सकीं।", + telegramErrSend: "संदेश नहीं भेजा जा सका।", + telegramErrMissing: "API ID, API hash और फ़ोन नंबर आवश्यक हैं।", + telegramErrCode: "अमान्य कोड। पुनः प्रयास करें।", + telegramErrCodeExpired: "कोड समाप्त हो गया। फिर से शुरू करें।", + telegramErrPassword: "गलत पासवर्ड।", + telegramErrPhone: "अमान्य फ़ोन नंबर।", + telegramErrApi: "अमान्य API ID या API hash।", + telegramErrFlood: "बहुत अधिक प्रयास। थोड़ी देर बाद पुनः प्रयास करें।", + telegramErrNoLogin: "कोई लॉगिन प्रगति में नहीं। फिर से शुरू करें।", + peertubeUsernameLabel: "उपयोगकर्ता नाम", + peertubePasswordLabel: "पासवर्ड", + peertubeSubscriptions: "सदस्यताएँ", + peertubeMyVideos: "मेरे वीडियो", + peertubeVideos: "वीडियो", + peertubeFollowers: "फ़ॉलोअर", + peertubeViews: "बार देखा गया", + peertubeLikes: "पसंद", + peertubeQuota: "कोटा", + peertubeUpload: "वीडियो अपलोड करें", + peertubeUploadHelp: "एक वीडियो फ़ाइल चुनें। यह आपके चैनल पर अपलोड होती है और बाद में PeerTube उसे प्रोसेस करता है।", + peertubeTitleLabel: "शीर्षक", + peertubeDescriptionLabel: "विवरण", + peertubePrivacyLabel: "गोपनीयता", + peertubePrivacyPublic: "सार्वजनिक", + peertubePrivacyUnlisted: "असूचीबद्ध", + peertubePrivacyPrivate: "निजी", + peertubeNoVideos: "अभी कोई वीडियो नहीं।", + peertubeComments: "टिप्पणियाँ", + peertubeComment: "टिप्पणी करें", + peertubeCommentPlaceholder: "टिप्पणी लिखें…", + peertubeNoComments: "अभी कोई टिप्पणी नहीं।", + peertubeOpen: "PeerTube पर खोलें", + peertubeWatch: "देखें", + peertubeChannel: "चैनल", + peertubeNoStream: "यह वीडियो अभी यहाँ नहीं चल सकता। इसे PeerTube पर खोलें।", + peertubeUploaded: "वीडियो अपलोड हो गया। PeerTube इसे प्रोसेस कर रहा है।", + peertubeErrConnect: "PeerTube से संपर्क नहीं हो सका।", + peertubeErrAuth: "उपयोगकर्ता नाम या पासवर्ड गलत है, या सत्र समाप्त हो गया। फिर से जोड़ें।", + peertubeErrInstance: "अमान्य इंस्टेंस URL।", + peertubeErrMissing: "इंस्टेंस, उपयोगकर्ता नाम और पासवर्ड आवश्यक हैं।", + peertubeErrFetch: "वीडियो लोड नहीं हो सके।", + peertubeErrUpload: "वीडियो अपलोड नहीं हो सका।", + peertubeErrTitle: "शीर्षक आवश्यक है।", + peertubeErrComment: "टिप्पणी प्रकाशित नहीं हो सकी।", + peertubeErrNoFile: "एक वीडियो फ़ाइल चुनें।", bookmarksLabel: "बुकमार्क", videosLabel: "वीडियो", torrentsLabel: "टोरेंट", @@ -381,34 +410,22 @@ module.exports = { inhabitantsLabel: "निवासी", trendingLabel: "ट्रेंडिंग", eventsLabel: "कार्यक्रम", - tasksLabel: "कार्य", apply: "लागू करें", menuPersonal: "व्यक्तिगत", - menuContent: "सामग्री", - menuBlogs: "ब्लॉग", menuGovernance: "शासन", menuOffice: "कार्यालय", - menuMultiverse: "मल्टीवर्स", - menuNetwork: "नेटवर्क", + menuNetwork: "समुदाय", menuCreative: "रचनात्मक", menuEconomy: "अर्थव्यवस्था", - menuMedia: "मीडिया", + menuMedia: "पुस्तकालय", menuTools: "उपकरण", comment: "टिप्पणी", subtopic: "उपविषय", json: "JSON", - createdBy: "द्वारा", unfollow: "समर्थन वापस लें", follow: "समर्थन करें", block: "ब्लॉक करें", unblock: "अनब्लॉक करें", - newerPosts: "नई पोस्ट", - olderPosts: "पुरानी पोस्ट", - feedRangeEmpty: "इस फ़ीड के लिए दी गई सीमा खाली है। देखने का प्रयास करें ", - seeFullFeed: "पूरी फ़ीड", - feedEmpty: "Oasis नेटवर्क ने इस खाते से कभी कोई पोस्ट नहीं देखी है।", - beginningOfFeed: "यह फ़ीड की शुरुआत है", - noNewerPosts: "अभी तक कोई नई पोस्ट प्राप्त नहीं हुई है।", relationshipNotFollowing: "आपका समर्थन नहीं किया गया है", relationshipTheyFollow: "आपका समर्थन करते हैं", relationshipMutuals: "पारस्परिक समर्थन", @@ -418,25 +435,23 @@ module.exports = { relationshipBlockedBy: "आप ब्लॉक हैं", relationshipMutualBlock: "पारस्परिक ब्लॉक", relationshipNone: "आप समर्थन नहीं कर रहे हैं", - relationshipConflict: "विवाद", relationshipBlockingPost: "ब्लॉक की गई पोस्ट", viewLikes: "प्रसार देखें", + viewSupporters: "समर्थक देखें", spreadedDescription: "निवासी द्वारा प्रसारित पोस्ट की सूची।", - totalspreads: "कुल प्रसार", - attachFiles: "फ़ाइलें संलग्न करें", preview: "पूर्वावलोकन", publish: "लिखें", contentWarningPlaceholder: "पोस्ट में विषय जोड़ें (वैकल्पिक)", - privateWarningPlaceholder: "निजी पोस्ट भेजने के लिए निवासी जोड़ें (वैकल्पिक)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "अपनी प्रविष्टि का मुख्य पाठ लिखें।", + publishTooLong: "आपकी पोस्ट बहुत लंबी है। कृपया इसे छोटा करें।", + keepTextRetry: "फिर से भेजें", + dateInPastError: "तारीख़ बीते समय की नहीं हो सकती", publishCustomDescription: [ "याद रखें: ब्लॉकचेन तकनीक के कारण, एक बार पोस्ट प्रकाशित होने के बाद इसे संपादित या हटाया नहीं जा सकता।", ], commentWarning: [ "याद रखें: ब्लॉकचेन तकनीक के कारण, एक बार पोस्ट प्रकाशित होने के बाद इसे संपादित या हटाया नहीं जा सकता।", ], - commentPublic: "सार्वजनिक", - commentPrivate: "निजी", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -456,14 +471,12 @@ module.exports = { "।", ], publishCustom: "उन्नत पोस्ट लिखें", - subtopicLabel: "इस पोस्ट का उपविषय बनाएँ", messagePreview: "पोस्ट पूर्वावलोकन", mentionsMatching: "मिलते-जुलते उल्लेख", mentionsName: "नाम", mentionsRelationship: "संबंध", - updateit: "अपडेट प्राप्त करें!", updateBannerText: "Oasis का एक नया संस्करण उपलब्ध है।", - updateBannerAction: "अभी अपडेट करें →", + updateBannerAction: "अभी अपडेट करें!", info: "जानकारी", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -471,29 +484,94 @@ module.exports = { timeAgo: "पहले", sendTime: "लगभग ", theme: "थीम", - legacy: "कुंजियाँ", - legacyTitle: "कुंजियाँ", - legacyDescription: "अपनी गुप्त कुंजी (निजी कुंजी) को जल्दी और सुरक्षित रूप से प्रबंधित करें।", - legacyExportButton: "निर्यात", - legacyImportButton: "आयात", + backupKeysExportButton: "निर्यात", + backupKeysImportButton: "आयात", ssbLogStream: "ब्लॉकचेन", ssbLogStreamDescription: "ब्लॉकचेन स्ट्रीम के लिए संदेश सीमा कॉन्फ़िगर करें।", + logWindowChip: "अंतिम {n} ब्लॉकचेन स्ट्रीम", saveSettings: "सेटिंग्स सहेजें", - exportTitle: "डेटा निर्यात करें", exportDescription: "अपनी कुंजी एन्क्रिप्ट करने के लिए पासवर्ड सेट करें (न्यूनतम 32 अक्षर)", - exportDataTitle: "बैकअप", + exportDataTitle: "तत्काल बैकअप", exportDataDescription: "अपना डेटा डाउनलोड करें (गुप्त कुंजी शामिल नहीं!)", exportDataButton: "डेटाबेस डाउनलोड करें", - pubWallet: "PUB वॉलेट", - pubWalletDescription: "PUB वॉलेट URL सेट करें। इसका उपयोग PUB लेनदेन (UBI सहित) के लिए किया जाएगा।", - pubWalletConfiguration: "कॉन्फ़िगरेशन सहेजें", - importTitle: "डेटा आयात करें", importDescription: "अपना अवतार सक्रिय करने के लिए अपनी एन्क्रिप्टेड गुप्त कुंजी (निजी कुंजी) आयात करें", - importAttach: "एन्क्रिप्टेड फ़ाइल संलग्न करें (.enc)", - passwordLengthInfo: "पासवर्ड कम से कम 32 अक्षर लंबा होना चाहिए।", passwordImport: "डेटा को डिक्रिप्ट करने के लिए अपना पासवर्ड लिखें जो आपके सिस्टम होम पर सहेजा जाएगा (नाम: secret)", exportPasswordPlaceholder: "लोअरकेस, अपरकेस, संख्याएँ और प्रतीक उपयोग करें", - fileInfo: "आपकी एन्क्रिप्टेड गुप्त कुंजी आपके सिस्टम होम पर सहेजी जाएगी (नाम: oasis.enc)", + fileInfo: "आपकी एन्क्रिप्टेड गुप्त कुंजी आपके डिवाइस पर डाउनलोड की जाएगी (नाम: oasis.enc)", + developer: "विकास", + devTitle: "विकास", + welcomeBannerText: "आपका अवतार अभी सेट नहीं हुआ है।", + aiSuggestionBanner: "सुझाव:", + aiSuggestionsEnable: "AI सुझाव", + welcomeBannerAction: "शुरू करें", + welcomeTitle: "स्वागत है", + welcomeStepIdentity: "पहचान", + welcomeStepType: "प्रकार", + welcomeStepUse: "उपयोग", + welcomeStepLook: "रूप", + welcomeStepCommunity: "समुदाय", + welcomeStepIntro: "स्वागत", + welcomeHintIdentity: "आपका नाम और चित्र।", + welcomeHintType: "Oasis आपके लिए कैसे खुलता है।", + welcomeHintUse: "कौन से मॉड्यूल चालू रहते हैं।", + welcomeHintLook: "Oasis की थीम।", + welcomeHintCommunity: "दूसरे निवासियों से मिलने के लिए किसी PUB से जुड़ें।", + welcomeHintIntro: "समुदाय को पहला अभिवादन भेजें।", + welcomeStepThemeAction: "यह थीम इस्तेमाल करें", + welcomeJoinPubs: "PUB से जुड़ें", + welcomeNoFederation: "बिना संघ के", + welcomeStepGreetingAction: "भेजें", + welcomeDescription: "शुरू करने से पहले कुछ ज़रूरी बातें।", + welcomeSkip: "अभी नहीं", + welcomeStepProfileAction: "प्रोफ़ाइल सहेजें", + welcomeBootstrapRunning: "पब का स्नैपशॉट लोड हो रहा है। पूरा होते ही पब से अपने आप जुड़ जाएगा; इस बीच आप Oasis इस्तेमाल करते रह सकते हैं।", + welcomeCompleteTitle: "सब तैयार है।", + welcomeCompleteText: "आपका नोड तैयार है। यहाँ से नेटवर्क उन निवासियों के साथ बढ़ता है जिनका आप समर्थन करते हैं।", + welcomeFindPeople: "निवासी खोजें", + devFilterFiles: "फ़ाइलें", + devFilterSearch: "खोज", + devFilterModules: "मॉड्यूल", + devFilterTranslations: "अनुवाद", + devFilterStyles: "शैलियाँ", + devFilterTests: "टेस्ट", + devVersion: "संस्करण", + devNodeVersion: "Node", + devStatsTests: "टेस्ट", + devParentFolder: "मूल फ़ोल्डर", + devOpenFolder: "खोलें", + devDescription: "इस नोड पर चल रहे स्रोत कोड का अन्वेषण करें।", + devRoot: "मूल", + devSearchPlaceholder: "कोड में खोजने के लिए पाठ", + devAnyExtension: "कोई भी फ़ाइल", + devStatsFiles: "फ़ाइलें", + devStatsLines: "पंक्तियाँ", + devStatsModules: "मॉड्यूल", + devStatsLanguages: "भाषाएँ", + devNotViewable: "देखने योग्य नहीं", + devEmptyFolder: "यह फ़ोल्डर खाली है।", + devSize: "आकार", + devShowing: "दिखा रहे हैं", + devLine: "पंक्ति", + devReportBug: "बग रिपोर्ट करें", + devCreateTask: "कार्य बनाएँ", + devRaw: "RAW", + devReportContext: "Oasis स्रोत कोड पढ़ते समय मिला", + devTaskPrefix: "समीक्षा", + devPrevPage: "पिछली पंक्तियाँ", + devNextPage: "अगली पंक्तियाँ", + devMatches: "मिलान", + devFilesScanned: "फ़ाइलें जाँची गईं", + devNoResults: "अभी तक कोई मिलान नहीं।", + devColModule: "मॉड्यूल", + devColState: "स्थिति", + devColModel: "मॉडल", + devColView: "दृश्य", + devColRoutes: "रूट", + devColTests: "टेस्ट", + devOn: "चालू", + devOff: "बंद", + modulesDevLabel: "विकास", + modulesDevDescription: "Oasis स्रोत कोड को प्रबंधित करने का मॉड्यूल।", themeIntro: "एक थीम चुनें।", setTheme: "थीम सेट करें", @@ -501,16 +579,49 @@ module.exports = { languageDescription: "यदि आप दूसरी भाषा उपयोग करना चाहते हैं, तो यहाँ चुनें।", setLanguage: "भाषा सेट करें", - peerConnections: "पीयर", peerConnectionsIntro: "अन्य पीयर के साथ अपने सभी कनेक्शन प्रबंधित करें।", + peersConnectFailed: "कनेक्ट नहीं हो सका: {reason}।", + peerStateLabel: "स्थिति", + peerAddressLabel: "पता", + peerLastChangeLabel: "अंतिम बदलाव", + peerStateConnected: "जुड़ा हुआ", + peerStateConnecting: "जुड़ रहा है", + peerStateStaged: "उपलब्ध", + peerErrSilent: "कोई गतिविधि नहीं", + peerErrTimeout: "जवाब नहीं दे रहा", + peerErrRefused: "कनेक्शन अस्वीकार करता है", + peerErrNotFound: "अज्ञात पता", + peerErrUnreachable: "नेटवर्क पहुँच से बाहर", + peerErrKeys: "अलग कुंजियाँ इस्तेमाल कर रहा है", + peerErrIdentity: "आपका कनेक्शन स्वीकार नहीं करता", + peerErrTor: "केवल TOR", + peerErrDropped: "कनेक्शन टूट गया", + peerErrPaused: "नेटवर्क रुका हुआ है", + peerErrOther: "कनेक्शन त्रुटि", + peerOutdated: "पुराना संस्करण", + peerVersionUnannounced: "अपना संस्करण घोषित नहीं करता: पुराना Oasis", + peersTorMissing: "यह PUB केवल TOR से कनेक्शन की अनुमति देता है।", peerConnectionsTitle: "कनेक्शन", + peersOnlineHint: "इस समय आपसे जुड़े नोड।", + peersDiscoveredHint: "ज्ञात पते वाले नोड जो अभी जुड़े नहीं हैं।", + peersUnknownHint: "आपके सामाजिक ग्राफ़ के नोड जिनका पता ज्ञात नहीं है।", + peersConnectionsHint: "वे पते जिनसे यह नोड जुड़ सकता है और हर कनेक्शन की स्थिति।", + peersTabLan: "LAN", + peersTabHealth: "स्वास्थ्य", + peersHealthHint: "हर ज्ञात pub या नोड की स्थिति, स्थानीय नेटवर्क में और उसके बाहर।", + peersHealthReachable: "पहुँच योग्य", + peersHealthFailing: "विफल हो रहा", + peersHealthFailures: "विफलताएँ", + peersFailingSince: "कब से विफल", + peersHealthForget: "अप्राप्य को भूलें", + peersLanHint: "आपके स्थानीय नेटवर्क में मिले नोड।", + peersLanLastSeen: "अंतिम बार देखा गया", + peersLanEmpty: "स्थानीय नेटवर्क में अभी कोई नोड नहीं मिला।", online: "ऑनलाइन", offline: "ऑफ़लाइन", discovered: 'खोजे गए', unknown: 'अज्ञात', - lanBroadcastLabel: "LAN प्रसारण", lanBroadcastActive: "सक्रिय (LAN पर UDP मल्टीकास्ट)", - lanBroadcastDisabled: "अक्षम (pub मोड)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -523,127 +634,102 @@ module.exports = { noConnections: "कोई पीयर कनेक्ट नहीं है।", noDiscovered: "कोई पीयर नहीं मिला।", noUnknownPeers: "मित्र-ग्राफ में कोई अज्ञात पीयर नहीं।", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "अभी तक कोई पीयर पंजीकृत नहीं है।", + peerConnect: "कनेक्ट करें", + peerDisconnect: "डिस्कनेक्ट करें", peerRefresh: "ताज़ा करें", peerPruneIdle: "निष्क्रिय हटाएँ", + peersPause: "रोकें", + peersResume: "फिर शुरू करें", + peersPausedNote: "रुका हुआ: Oasis किसी से नहीं जुड़ता और कोई उससे नहीं जुड़ सकता।", + peersPauseHint: "सभी कनेक्शन रोक देता है: जब तक आप फिर से शुरू नहीं करते, Oasis न अन्य नोड्स से जुड़ता है न उन्हें स्वीकार करता है।", peerExport: "निर्यात", peerImport: "आयात", peerImportTitle: "पीयर सूची आयात करें", peerImportPlaceholder: "प्रति पंक्ति एक multiserver पता पेस्ट करें, या एक .txt फ़ाइल अपलोड करें…", - noSupportedConnections: "कोई समर्थित पीयर नहीं।", - noBlockedConnections: "कोई ब्लॉक किया गया पीयर नहीं।", - noRecommendedConnections: "कोई अनुशंसित पीयर नहीं।", connectionActionIntro: "", - startNetworking: "नेटवर्किंग शुरू करें", - stopNetworking: "नेटवर्किंग बंद करें", - restartNetworking: "नेटवर्किंग पुनः आरंभ करें", - sync: "नेटवर्क सिंक करें", indexes: "इंडेक्स", + indexesRebuilt: "सूचकांक पुनर्निर्मित", + indexesRebuildFailed: "सूचकांक पुनर्निर्मित नहीं हो सके", + indexesFiles: "सूचकांक फ़ाइलें", indexesDescription: "अपने इंडेक्स पुनर्निर्माण करना सुरक्षित है, और कुछ प्रकार की त्रुटियों को ठीक कर सकता है।", homePageTitle: "होम", homePageDescription: "चुनें कि कौन सा मॉड्यूल आपका होम पेज हो।", saveHomePage: "होम सेट करें", invites: "आमंत्रण", - invitesTitle: "आमंत्रण", - invitesInvites: "आमंत्रण", invitesDescription: "अपने नेटवर्क में आमंत्रण कोड प्रबंधित करें और लागू करें।", + invitesPubsHint: "किसी पब का आमंत्रण कोड चिपकाएँ ताकि उससे संघ बने और उसके निवासियों की प्रतिकृति शुरू हो।", + invitesFederationsHint: "जिन पब से आप जुड़े हैं: उन्हें ताज़ा करें, अनुपलब्ध हटाएँ या सूची निर्यात करें।", invitesTribesTitle: "जनजातियाँ", - invitesTribeInviteCodePlaceholder: "जनजाति आमंत्रण कोड दर्ज करें", - invitesTribeJoinButton: "जनजाति में शामिल हों", invitesChatsTitle: "चैट्स", - invitesChatInviteCodePlaceholder: "चैट आमंत्रण कोड दर्ज करें", - invitesChatJoinButton: "चैट में शामिल हों", invitesPadsTitle: "पैड्स", - invitesPadInviteCodePlaceholder: "पैड आमंत्रण कोड दर्ज करें", - invitesPadJoinButton: "पैड में शामिल हों", invitesCalendarsTitle: "कैलेंडर", - invitesCalendarInviteCodePlaceholder: "कैलेंडर आमंत्रण कोड दर्ज करें", - invitesCalendarJoinButton: "कैलेंडर में शामिल हों", invitesEventsTitle: "कार्यक्रम", - invitesEventInviteCodePlaceholder: "कार्यक्रम आमंत्रण कोड दर्ज करें", - invitesEventJoinButton: "कार्यक्रम में शामिल हों", invitesForumsTitle: "मंच", - invitesForumInviteCodePlaceholder: "मंच आमंत्रण कोड दर्ज करें", - invitesForumJoinButton: "मंच में शामिल हों", invitesMapsTitle: "मानचित्र", - invitesMapInviteCodePlaceholder: "मानचित्र आमंत्रण कोड दर्ज करें", - invitesMapJoinButton: "मानचित्र में शामिल हों", invitesShopsTitle: "दुकानें", - invitesShopInviteCodePlaceholder: "दुकान आमंत्रण कोड दर्ज करें", - invitesShopJoinButton: "दुकान में शामिल हों", invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "सुझाए गए PUB", + invitesFilterOthers: "अन्य", + invitesJoinTitle: "आमंत्रण", + invitesJoinHint: "कोई भी आमंत्रण कोड चिपकाएँ, चाहे वह किसी के लिए भी हो, या किसी निवासी का Oasis ID।", + invitesJoinPlaceholder: "आमंत्रण कोड या Oasis ID", + invitesJoinButton: "जुड़ें", + invitesUsedTitle: "हाल के आमंत्रण", + invitesUsedEmpty: "आपने अभी तक कोई आमंत्रण उपयोग नहीं किया है।", + invitesUsedType: "प्रकार", + invitesUsedName: "नाम", + invitesUsedDate: "तारीख", invitesPubInviteCodePlaceholder: "PUB आमंत्रण कोड दर्ज करें", invitesAcceptInvite: "PUB में शामिल हों", + invitesPubUses: "उपयोग किए गए निमंत्रण", + invitesPubExhausted: "इस PUB के पास कोई निमंत्रण नहीं बचा है।", invitesAlreadyFederated: "आप पहले से ही इस पब के साथ फेडरेटेड हैं।", + invitesJoinPubMode: "यह Oasis एक PUB के रूप में, केवल-पढ़ने वाले मोड में चल रहा है। इसे किसी दूसरे PUB से जोड़ने के लिए टर्मिनल में चलाएँ: ./oasis.sh join <कोड>", + invitesJoinLocalOnly: "निमंत्रण केवल उसी डिवाइस से स्वीकार किए जा सकते हैं जिस पर Oasis चल रहा है, या शुरू होने पर दिखने वाले व्यवस्थापक लिंक से।", invitesFederationsTitle: "संघ", invitesAcceptedInvites: "संघीय", - invitesNoInvites: "अभी तक कोई आमंत्रण स्वीकार नहीं किया गया।", invitesUnfollow: "अनफ़ॉलो करें", invitesFollow: "फ़ॉलो करें", invitesUnfollowedInvites: "असंघीय", invitesNoFederatedPubs: "कोई संघीय pub नहीं।", invitesNoUnfollowed: "कोई असंघीय pub नहीं।", - invitesUnreachablePubs: "अगम्य", - invitesNoUnreachablePubs: "कोई अगम्य pub नहीं।", invitesPubsRefresh: "ताज़ा करें", - invitesPubsClearUnreachable: "अगम्य हटाएँ", + invitesPubsClearUnreachable: "साफ़ करें", invitesPubsExport: "निर्यात", invitesPubsImport: "आयात", invitesPubsImportTitle: "Pubs आयात करें", invitesPubsImportPlaceholder: "प्रति पंक्ति एक multiserver पता या निमंत्रण कोड पेस्ट करें, या एक .txt फ़ाइल अपलोड करें…", - currentlyUnreachable: "त्रुटि!", - errorDetails: "त्रुटि विवरण", genericError: "एक त्रुटि हुई।", panicMode: "आपातकालीन मोड!", encryptData: "अपनी ब्लॉकचेन एन्क्रिप्ट करने के लिए पासवर्ड सेट करें (न्यूनतम 32 अक्षर)", decryptData: "अपनी ब्लॉकचेन डिक्रिप्ट करने के लिए पासवर्ड दर्ज करें", - panicModeDescription: "अपनी ब्लॉकचेन एन्क्रिप्ट/डिक्रिप्ट या हटाएँ", removeDataDescription: "चेतावनी: यह प्रक्रिया पूर्ववत नहीं की जा सकती।", - encryptPanicButton: "ब्लॉकचेन एन्क्रिप्ट करें", - decryptPanicButton: "ब्लॉकचेन डिक्रिप्ट करें", removePanicButton: "अपना सारा डेटा हटाएँ!", searchTitle: "खोज", searchDescriptionLabel: "अपने नेटवर्क में सामग्री खोजें।", - searchLanguagesLabel: "भाषाएँ", - searchSkillsLabel: "कौशल", searchPlaceholder:"सामग्री खोजें...", - searchDateLabel:"दिनांक", searchLocationLabel:"स्थान", searchPriceLabel:"मूल्य", - searchUrlLabel:"URL", searchCategoryLabel:"श्रेणी", - searchStartLabel:"प्रारंभ समय", - searchEndLabel:"समाप्ति समय", searchPriorityLabel:"प्राथमिकता", searchStatusLabel:"स्थिति", statusLabel:"स्थिति", - totalVotesLabel:"कुल मत", noResultsFound:"कोई परिणाम नहीं मिला।", votesOption:"मत विकल्प", voteYesLabel:"हाँ", voteNoLabel:"नहीं", allTypesLabel: "असीमित", - ABSTENTIONLabel:"तटस्थता", YESLabel:"हाँ", NOLabel:"नहीं", - FOLLOW_MAJORITYLabel: "बहुमत का पालन करें", - CONFUSEDLabel: "भ्रमित", - NOT_INTERESTEDLabel: "रुचि नहीं", - StatusLabel:"स्थिति", votesOptionYesLabel:"हाँ", votesOptionNoLabel:"नहीं", votesOptionAbstentionLabel:"तटस्थता", votesQuestionLabel:"प्रश्न", - votesCreatedByLabel:"द्वारा बनाया गया", voteStatusOpen:"खुला", voteStatusClosed:"बंद", - imageSearchLabel: "छवियों को खोजने के लिए शब्द दर्ज करें।", commentDescription: ({ parentUrl }) => [ " ने टिप्पणी की ", a({ href: parentUrl }, " थ्रेड पर"), @@ -654,61 +740,27 @@ module.exports = { a({ href: parentUrl }, " एक पोस्ट से"), ], subtopicTitle: ({ authorName }) => [`@${authorName} की पोस्ट पर उपविषय`], - mysteryDescription: "एक रहस्यमय पोस्ट प्रकाशित की", oasisDescription: "OASIS परियोजना नेटवर्क", searchSubmit: "खोजें...", - postLabel: "पोस्ट", - aboutLabel: "निवासी", - feedLabel: "फ़ीड", votesLabel: "मतदान", - reportLabel: "रिपोर्ट", - imageLabel: "छवियाँ", - videoLabel: "वीडियो", - audioLabel: "ऑडियो", - documentLabel: "दस्तावेज़", - torrentLabel: "टॉरेंट", pdfFallbackLabel: "PDF दस्तावेज़", - eventLabel: "कार्यक्रम", - taskLabel: "कार्य", - transferLabel: "स्थानांतरण", - curriculumLabel: "पाठ्यक्रम", - bookmarkLabel: "बुकमार्क", - tribeLabel: "जनजातियाँ", - marketLabel: "बाज़ार", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "वॉलेट", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + shopLabel: "दुकानें", + voteLabel: "वोट", submit: "जमा करें", - subjectLabel: "विषय", editProfile: "अवतार संपादित करें", - profileQrAlt: "प्रोफ़ाइल QR कोड", - profileQrHint: "इस Oasis ID को कॉपी करने के लिए स्कैन करें।", oasisIdLabel: "OasisID", - spreadLabel: "फैलाएँ", - spreadHint: "अपने समर्थकों को फैलाएँ (फ़ीड से प्रतिकृति)।", + spreadContent: "प्रतिकृति बनाएँ", welcomePmSubject: "Hello World!", - welcomePmBody: "Oasis में आपका स्वागत है — एक स्वतंत्र, पीयर-टू-पीयर, फ़ेडरेटेड और एन्क्रिप्टेड सोशल नेटवर्क, जिसकी वास्तुकला वितरित और केवल-आमंत्रण आधारित है।\n\nशुरुआत के लिए कुछ रोचक स्थान:\n\n- /profile — अपना अवतार, नाम, विवरण और कौन से मॉड्यूल आपके सार्वजनिक पक्ष पर दिखेंगे, संपादित करें।\n- /invites — व्यापक नेटवर्क के साथ फेडरेट करने के लिए एक pub जोड़ें।\n- /peers — देखें कौन जुड़ा है, अपनी LAN फिर से स्कैन करें, पीयर सूचियाँ निर्यात या आयात करें।\n- /inhabitants — दूसरों के अवतार खोजें और देखें।\n- /tribes — एंड-टू-एंड एन्क्रिप्शन वाले निजी समूह बनाएँ या उनमें शामिल हों।\n- /inbox — निजी संदेश पढ़ें और भेजें।\n- /activity — हाल की गतिविधि देखें — आपकी और आपके नेटवर्क की।\n- /publish — पोस्ट लिखें, या चित्र, ऑडियो, वीडियो, दस्तावेज़ साझा करें।\n\nकनेक्ट करने के लिए कुछ दिलचस्प जगहें:\n\n- /fediverse — अपने अन्य फ़ेडिवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।\n\nयह संदेश आपके द्वारा निजी रूप से स्वयं को भेजा गया था, इसलिए इसे प्राप्त करने के लिए किसी कनेक्शन की आवश्यकता नहीं थी।", - profileVisibilityTitle: "सार्वजनिक प्रोफ़ाइल दृश्यता", - profileVisibilityHint: "तय करें कि आपकी प्रोफ़ाइल में अन्य निवासी कौन से क्षेत्र देखें। डिफ़ॉल्ट रूप से केवल Karma दिखाया जाता है।", + welcomePmBody: "Oasis में आपका स्वागत है — एक स्वतंत्र, पीयर-टू-पीयर, फ़ेडरेटेड और एन्क्रिप्टेड सोशल नेटवर्क, जिसकी वास्तुकला वितरित और केवल-आमंत्रण आधारित है।\n\nशुरुआत के लिए कुछ रोचक स्थान:\n\n- [/avatar](/profile) — अपना अवतार, नाम, विवरण और कौन से मॉड्यूल आपके सार्वजनिक पक्ष पर दिखेंगे, संपादित करें।\n- /settings — अपना स्थानीय इंस्टेंस समायोजित करें: भाषा, थीम, मॉड्यूल, गोपनीयता और सिंक।\n- /invites — व्यापक नेटवर्क के साथ फेडरेट करने के लिए एक pub जोड़ें।\n- /peers — देखें कौन जुड़ा है, अपनी LAN फिर से स्कैन करें, पीयर सूचियाँ निर्यात या आयात करें।\n- /inhabitants — दूसरे निवासियों के अवतार खोजें और देखें।\n- /tribes — एंड-टू-एंड एन्क्रिप्शन वाले निजी समूह बनाएँ या उनमें शामिल हों।\n- /search — नेटवर्क की सामग्री को प्रकार के अनुसार खोजें।\n\nकनेक्ट करने के लिए कुछ दिलचस्प जगहें:\n\n- /multiverse — अपने अन्य मल्टीवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।\n\nयह संदेश आपके द्वारा निजी रूप से स्वयं को भेजा गया था, इसलिए इसे प्राप्त करने के लिए किसी कनेक्शन की आवश्यकता नहीं थी।", profileSensorsSectionTitle: "सेंसर", profileVisibilityActivity: "गतिविधि स्तर", - profileVisibilityDevice: "डिवाइस", profileDeviceLockedHint: "यह सेंसर हमेशा चालू रहता है: यह दूसरों को दिखाता है कि आप किस डिवाइस से कनेक्ट होते हैं और इसे अक्षम नहीं किया जा सकता।", profileVisibilityKarma: "KARMA स्कोर", profileVisibilityUbi: "UBI", - profileVisibilityWallet: "ECOIN वॉलेट", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityWallet: "ECOin वॉलेट", + profileVisibilityEcoTax: "ECO कर", + profileVisibilityLarpSign: "L.A.R.P. मुहर", profileVisibilityGpg: "GPG कुंजी", - profileVisibilityClearnet: "मेरी प्रोफ़ाइल प्रकाशित करें", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "दुकानें", profileClearnetJobsLabel: "नौकरियाँ", @@ -720,6 +772,10 @@ module.exports = { profileClearnetImagesLabel: "चित्र", profileClearnetDocumentsLabel: "दस्तावेज़", profileClearnetTorrentsLabel: "टॉरेंट", + profileClearnetPodcastsLabel: "पॉडकास्ट", + profileClearnetMarketLabel: "बाज़ार", + profileClearnetFeedLabel: "फ़ीड", + profileClearnetWikiLabel: "विकी", editProfileDescription: "", profileName: "नाम", @@ -745,7 +801,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "पुष्टि", walletConfirm: "पुष्टि करें", - walletDescription: "अपनी डिजिटल संपत्तियाँ प्रबंधित करें, जिसमें ECOin भेजना और प्राप्त करना, अपना शेष देखना और अपना लेनदेन इतिहास शामिल है।", + walletDescription: "अपना ECOin वॉलेट प्रबंधित करें।", walletDate: "दिनांक", walletFee: "शुल्क (शुल्क जितना अधिक होगा, आपका लेनदेन उतनी तेज़ी से संसाधित होगा)", walletFeeLine: ({ fee }) => `शुल्क: ECO ${fee}`, @@ -753,6 +809,86 @@ module.exports = { walletReceive: "प्राप्त करें", walletReset: "रीसेट", walletSend: "भेजें", + walletPayingTransfer: "ट्रांसफ़र का भुगतान", + walletCreateTransferLabel: "स्मार्ट कॉन्ट्रैक्ट बनाएँ (ट्रांसफर)", + walletTransferNotifiedNote: "दूसरे पक्ष को इस भुगतान की सूचना दी गई", + walletTransferCreatedNote: "इस भुगतान के लिए एक ट्रांसफ़र (रसीद) बनाया गया", + walletTransferNoPayeeNote: "किसी निवासी ने यह पता प्रकाशित नहीं किया, इसलिए ट्रांसफ़र नहीं बना", + walletTransferReceiptConcept: "वॉलेट से भुगतान", + walletPaymentPmText: "ने आपको भुगतान भेजा", + walletPayeeLabel: "प्राप्तकर्ता", + walletConceptLabel: "विवरण", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "आपको अपना UBI मिल गया है।", + bankingBotPaymentTitle: "आपको एक भुगतान मिला है।", + bankingBotUbiPaidText: "आपकी सार्वभौमिक बुनियादी आय का भुगतान कर दिया गया है", + bankUbiPubThisNode: "यह नोड UBI का भुगतान करता है", + bankUbiPubConnected: "UBI PUB", + bankUbiPubNone: "आपके नेटवर्क में अभी कोई PUB UBI की घोषणा नहीं कर रहा", + bankUbiAddressPublished: "ECOin पता प्रकाशित", + bankUbiAddressMissing: "UBI पाने के लिए अपना ECOin पता प्रकाशित करें", + bankUbiClaimedThisMonth: "इस महीने दावा किया गया", + bankUbiPubBalance: "UBI के लिए उपलब्ध PUB वॉलेट", + bankNoPubAddress: "UBI PUB ने अभी तक ECOin पता प्रकाशित नहीं किया है।", + bankUbiTab: "UBI", + bankDonateEco: "दान करें!", + donateTooltip: "ECOin दान करें", + bankNoUserAddress: "इस निवासी ने ECOin पता साझा नहीं किया है।", + bankUbiPubDefault: "डिफ़ॉल्ट", + bankUbiPool: "निधि", + bankUbiLastSeen: "अंतिम बार देखा गया", + bankUbiLastPayout: "अंतिम कार्रवाई", + bankUbiPaidOut: "वितरित", + bankRangeToday: "आज", + bankRangeWeek: "सप्ताह", + bankRangeMonth: "महीना", + bankRangeYear: "वर्ष", + bankRangeFiveYears: "5 वर्ष", + bankRangeAll: "सभी", + bankChartValueTitle: "ECOin मूल्य का विकास", + bankChartValueHours: "समय में समतुल्य (घं)", + bankChartWealthTitle: "ECOin धन का पुनर्वितरण", + bankChartDistributed: "वितरित UBI (ECO)", + bankChartRebalanceTitle: "PUBs के बीच धन का पुनर्वितरण", + bankChartRebalanced: "PUBs के बीच भेजे गए ECO", + bankChartTaxes: "रोके गए कर (ECO)", + bankChartNoDataRange: "इस अवधि में कोई डेटा नहीं।", + bankChartNoDataYet: "अभी कोई डेटा नहीं", + bankChartPerHour: "प्रति घंटा", + bankChartPerDay: "प्रति दिन", + bankChartPerMonth: "प्रति माह", + bankTotalUbiDistributed: "कुल पुनर्वितरित UBI (नेटवर्क)", + bankTotalTaxesWithheld: "कुल रोके गए कर (नेटवर्क)", + bankInflationIssuance: "जारी करने की दर (वार्षिक, ब्लॉक इनाम से)", + bankRulesEligibilityTitle: "UBI कौन प्राप्त कर सकता है", + bankRulesEligibility1: "प्रोफ़ाइल में प्रकाशित ECOin पता (प्रोफ़ाइल → सेंसर → ECOin वॉलेट)।", + bankRulesEligibility2: "30 दिन से पुरानी फ़ीड जिसमें नेटवर्क में गतिविधि हो: कर्म शून्य से ऊपर।", + bankRulesEligibility3: "प्रति माह (एपॉक) एक दावा, Banking से UBI दावा करें बटन द्वारा। चालू माह को UBI अस्वीकार करें से मना किया जा सकता है।", + bankRulesEligibility4: "1 ECO की न्यूनतम राशि हमेशा दी जाती है; कर केवल उससे ऊपर के हिस्से को घटाते हैं और कभी अलग से नहीं लिए जाते।", + bankRulesEligibility5: "PUB अगले चक्र में भुगतान करता है और एक UBI ट्रांसफर प्रकाशित करता है जिसे आप पुष्टि कर सकते हैं।", + bankRulesEligibility6: "यदि पता नहीं है, फ़ीड बहुत नई है, आपने माह अस्वीकार किया या PUB के पास धन नहीं है, तो दावा छोड़ दिया जाता है और PUB पर लॉग किया जाता है।", + copyLinkTitle: "लिंक कॉपी करें", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "UBI अस्वीकार करें", + bankUbiRefusedThisMonth: "आपने इस महीने का UBI अस्वीकार किया।", + bankingBotUbiAvailableTitle: "आप अपना UBI दावा कर सकते हैं।", + bankingBotUbiAvailableText: "इस महीने का आपका UBI उपलब्ध है", + bankingBotConfirmTitle: "आपके पास पुष्टि की प्रतीक्षा में एक स्मार्ट-कॉन्ट्रैक्ट है", + bankingBotConfirmText: "आपकी पुष्टि की प्रतीक्षा में स्मार्ट-कॉन्ट्रैक्ट", + bankChartZoomHint: "बड़ा करने के लिए क्लिक करें", + bankChartPoolTitle: "UBI निधि (समय के साथ)", + bankChartNetworkUbiTitle: "कुल पुनर्वितरित UBI (नेटवर्क)", + bankChartSupplyTitle: "ECOin परिचालित आपूर्ति (समय के साथ)", + bankChartInflationTitle: "ECOin मुद्रास्फीति (समय के साथ)", + bankChartInflationSeries: "वार्षिक दर (%)", + bankChartPool: "कुल निधि (ECO)", + bankChartKarmaTitle: "KARMA Scoring (समय के साथ)", + bankChartFundsTitle: "आपकी ECOin निधि (समय के साथ)", + bankChartKarma: "कर्म", + bankWalletReady: "ECOin कनेक्टेड", + bankWalletNotConfigured: "ECOin डिस्कनेक्ट", + transfersPayWithWallet: "भुगतान करें!", walletStatus: "स्थिति", walletDisconnected: [ "ECOin ", @@ -762,9 +898,11 @@ module.exports = { " या कनेक्शन स्थिति।", ], walletSentToLine: ({ destination, amount }) => `ECO ${amount} भेजा गया ${destination} को`, - walletSettingsTitle: "वॉलेट", + walletNotConfigured: "आपका ECOin वॉलेट अभी कॉन्फ़िगर नहीं है।", walletSettingsDescription: "Oasis को अपने ECOin वॉलेट के साथ एकीकृत करें।", - walletSettingsDocLink: "ECOin स्थापना मार्गदर्शिका", + walletSettingsMobileDescription: "Oasis को इस डिवाइस के ECOin Wallet ऐप से जोड़ें।", + walletDownloadEcoin: "ECOin डाउनलोड करें!", + walletRpcUrl: "RPC URL", walletStatusMessages: { invalid_amount: "अमान्य राशि", invalid_dest: "अमान्य गंतव्य पता", @@ -780,41 +918,24 @@ module.exports = { walletUser: "उपयोगकर्ता नाम", walletPass: "पासवर्ड", walletConfiguration: "वॉलेट सेट करें", + walletDisconnectButton: "वॉलेट डिस्कनेक्ट करें", cipher: "क्रिप्टर", cipherTitle: "क्रिप्टर", cipherDescription: "अपने टेक्स्ट को सममित रूप से एन्क्रिप्ट और डिक्रिप्ट करें (साझा पासवर्ड का उपयोग करके)।", randomPassword: "यादृच्छिक पासवर्ड", - cipherEncryptTitle: "टेक्स्ट एन्क्रिप्ट करें", - cipherEncryptDescription: "एन्क्रिप्ट करने के लिए टेक्स्ट दर्ज करें", - cipherTextLabel: "एन्क्रिप्ट करने के लिए टेक्स्ट", cipherTextPlaceholder: "एन्क्रिप्ट करने के लिए टेक्स्ट दर्ज करें...", cipherPasswordLabel: "अपना टेक्स्ट एन्क्रिप्ट करने के लिए पासवर्ड सेट करें (न्यूनतम 32 अक्षर)", cipherPasswordDecryptLabel: "अपना टेक्स्ट डिक्रिप्ट करने के लिए पासवर्ड सेट करें (न्यूनतम 32 अक्षर)", cipherPasswordPlaceholder: "पासवर्ड दर्ज करें...", cipherEncryptButton: "एन्क्रिप्ट करें", - cipherDecryptTitle: "टेक्स्ट डिक्रिप्ट करें", - cipherDecryptDescription: "डिक्रिप्ट करने के लिए टेक्स्ट दर्ज करें", cipherEncryptedMessageLabel: "एन्क्रिप्टेड टेक्स्ट", cipherDecryptedMessageLabel: "डिक्रिप्टेड टेक्स्ट", - cipherPasswordUsedLabel: "एन्क्रिप्ट करने के लिए उपयोग किया गया पासवर्ड (इसे रखें!)", cipherEncryptedTextPlaceholder: "एन्क्रिप्टेड टेक्स्ट दर्ज करें...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "इनिशियलाइज़ेशन वेक्टर दर्ज करें...", cipherDecryptButton: "डिक्रिप्ट करें", password: "पासवर्ड", text: "टेक्स्ट", encryptedText: "एन्क्रिप्टेड टेक्स्ट", iv: "इनिशियलाइज़ेशन वेक्टर (IV)", - encryptTitle: "अपना टेक्स्ट एन्क्रिप्ट करें", - encryptDescription: "वह टेक्स्ट दर्ज करें जिसे आप एन्क्रिप्ट करना चाहते हैं और पासवर्ड प्रदान करें।", - encryptButton: "एन्क्रिप्ट करें", - decryptTitle: "अपना टेक्स्ट डिक्रिप्ट करें", - decryptDescription: "एन्क्रिप्टेड टेक्स्ट दर्ज करें और एन्क्रिप्शन के लिए उपयोग किया गया वही पासवर्ड प्रदान करें।", - decryptButton: "डिक्रिप्ट करें", - passwordLengthError: "पासवर्ड कम से कम 32 अक्षर लंबा होना चाहिए।", - missingFieldsError: "टेक्स्ट, पासवर्ड या IV प्रदान नहीं किया गया।", - encryptionError: "टेक्स्ट एन्क्रिप्ट करने में त्रुटि।", - decryptionError: "टेक्स्ट डिक्रिप्ट करने में त्रुटि।", bookmarkTitle: "बुकमार्क", bookmarkDescription: "अपने नेटवर्क में बुकमार्क खोजें और प्रबंधित करें।", bookmarkAllSectionTitle: "बुकमार्क", @@ -831,19 +952,16 @@ module.exports = { bookmarkFilterRecent: "हाल के", bookmarkCreateButton: "बुकमार्क बनाएँ", bookmarkUpdateButton: "अपडेट", - bookmarkDeleteButton: "हटाएँ", bookmarkAddFavoriteButton: "पसंदीदा जोड़ें", bookmarkRemoveFavoriteButton: "पसंदीदा हटाएँ", bookmarkUrlLabel: "लिंक", - bookmarkUrlPlaceholder: "https://example.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "विवरण", bookmarkDescriptionPlaceholder: "वैकल्पिक", bookmarkTagsLabel: "टैग", bookmarkTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", - bookmarkCategoryLabel: "श्रेणी", - bookmarkCategoryPlaceholder: "वैकल्पिक", bookmarkLastVisitLabel: "अंतिम विज़िट", - bookmarkSearchPlaceholder: "URL, टैग, श्रेणी, लेखक खोजें...", + bookmarkSearchPlaceholder: "बुकमार्क में खोजें...", bookmarkSortRecent: "सबसे हाल का", bookmarkSortOldest: "सबसे पुराना", bookmarkSortTop: "सर्वाधिक मतदान", @@ -856,8 +974,6 @@ module.exports = { noLastVisit: "कोई अंतिम विज़िट नहीं", videoTitle: "वीडियो", videoDescription: "अपने नेटवर्क में वीडियो सामग्री खोजें और प्रबंधित करें।", - videoPluginTitle: "शीर्षक", - videoPluginDescription: "विवरण", videoMineSectionTitle: "आपके वीडियो", videoCreateSectionTitle: "वीडियो अपलोड करें", videoUpdateSectionTitle: "वीडियो अपडेट करें", @@ -884,12 +1000,11 @@ module.exports = { videoDescriptionPlaceholder: "वैकल्पिक", videoNoFile: "कोई वीडियो फ़ाइल प्रदान नहीं की गई", noVideos: "कोई वीडियो उपलब्ध नहीं।", - videoSearchPlaceholder: "शीर्षक, टैग, लेखक खोजें...", + videoSearchPlaceholder: "वीडियो में खोजें...", videoSortRecent: "सबसे हाल का", videoSortOldest: "सबसे पुराना", videoSortTop: "सर्वाधिक मतदान", videoSearchButton: "खोजें", - videoMessageAuthorButton: "PM", videoUpdatedAt: "अपडेट किया गया", videoNoMatch: "आपकी खोज से कोई वीडियो मेल नहीं खाता।", documentTitle: "दस्तावेज़", @@ -908,7 +1023,6 @@ module.exports = { documentFilterFavorites: "पसंदीदा", documentCreateButton: "दस्तावेज़ अपलोड करें", documentUpdateButton: "अपडेट", - documentDeleteButton: "हटाएँ", documentAddFavoriteButton: "पसंदीदा जोड़ें", documentRemoveFavoriteButton: "पसंदीदा से हटाएँ", documentMessageAuthorButton: "PM", @@ -921,7 +1035,7 @@ module.exports = { documentDescriptionPlaceholder: "वैकल्पिक", documentNoFile: "कोई फ़ाइल नहीं।", noDocuments: "कोई दस्तावेज़ उपलब्ध नहीं।", - documentSearchPlaceholder: "शीर्षक, टैग, विवरण, लेखक खोजें...", + documentSearchPlaceholder: "दस्तावेज़ में खोजें...", documentSortRecent: "सबसे हाल का", documentSortOldest: "सबसे पुराना", documentSortTop: "सर्वाधिक मतदान", @@ -930,8 +1044,6 @@ module.exports = { documentUpdatedAt: "अपडेट किया गया", audioTitle: "ऑडियो", audioDescription: "अपने नेटवर्क में ऑडियो सामग्री खोजें और प्रबंधित करें।", - audioPluginTitle: "शीर्षक", - audioPluginDescription: "विवरण", audioMineSectionTitle: "आपके ऑडियो", audioCreateSectionTitle: "ऑडियो अपलोड करें", audioUpdateSectionTitle: "ऑडियो अपडेट करें", @@ -942,22 +1054,21 @@ module.exports = { audioFilterMine: "मेरे", audioFilterRecent: "हाल के", audioFilterTop: "शीर्ष", - audioFilterBlockchain: "ब्लॉकचेन", audioCreateButton: "ऑडियो अपलोड करें", audioUpdateButton: "अपडेट", - audioDeleteButton: "हटाएँ", audioAddFavoriteButton: "पसंदीदा जोड़ें", audioRemoveFavoriteButton: "पसंदीदा हटाएँ", audioFileLabel: "ऑडियो फ़ाइल चुनें (.mp3, .wav, .ogg)", audioTagsLabel: "टैग", audioTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + tagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", audioTitleLabel: "शीर्षक", audioTitlePlaceholder: "वैकल्पिक", audioDescriptionLabel: "विवरण", audioDescriptionPlaceholder: "वैकल्पिक", audioNoFile: "कोई ऑडियो फ़ाइल प्रदान नहीं की गई", noAudios: "कोई ऑडियो उपलब्ध नहीं।", - audioSearchPlaceholder: "शीर्षक, टैग, लेखक खोजें...", + audioSearchPlaceholder: "ऑडियो में खोजें...", audioSortRecent: "सबसे हाल का", audioSortOldest: "सबसे पुराना", audioSortTop: "सर्वाधिक मतदान", @@ -969,6 +1080,7 @@ module.exports = { audioFilterFavorites: "पसंदीदा", favoritesTitle: "पसंदीदा", favoritesDescription: "आपकी सभी पसंदीदा सामग्री एक ही स्थान पर।", + favoritesSearchPlaceholder: "पसंदीदा में खोजें...", favoritesFilterAll: "सभी", favoritesFilterRecent: "हाल के", favoritesFilterAudios: "ऑडियो", @@ -980,22 +1092,16 @@ module.exports = { favoritesFilterChats: "चैट्स", favoritesFilterCalendars: "कैलेंडर", favoritesFilterVideos: "वीडियो", - favoritesRemoveButton: "पसंदीदा से हटाएँ", favoritesNoItems: "अभी तक कोई पसंदीदा नहीं।", yourContacts: "आपके संपर्क", allInhabitants: "निवासी", - allCVs: "सभी CVs", + allCVs: "CVs", discoverPeople: "अपने नेटवर्क में निवासियों को खोजें।", - allInhabitantsButton: "सभी", - contactsButton: "समर्थन", - CVsButton: "बायोडाटा", - matchSkills: "कौशल मिलान", - matchSkillsButton: "कौशल मिलान", - suggestedButton: "सुझाए गए", - searchInhabitantsPlaceholder: "नाम से निवासी फ़िल्टर करें …", + searchInhabitantsPlaceholder: "निवासी में खोजें...", filterLocation: "स्थान से निवासी फ़िल्टर करें …", filterLanguage: "भाषा से निवासी फ़िल्टर करें …", filterSkills: "कौशल से निवासी फ़िल्टर करें …", + cvSkillsCount: "कौशल", applyFilters: "फ़िल्टर लागू करें", locationLabel: "स्थान", languagesLabel: "भाषाएँ", @@ -1004,15 +1110,15 @@ module.exports = { mutualFollowers: "पारस्परिक अनुयायी", suggestedFollowsYou: "आपको फ़ॉलो करता है", latestInteractions: "नवीनतम बातचीत", - viewAvatar: "अवतार देखें", - viewCV: "CV देखें", suggestedSectionTitle: "सुझाए गए", topkarmaSectionTitle: "शीर्ष कर्मा", topecoSectionTitle: "शीर्ष इको", topactivitySectionTitle: "शीर्ष गतिविधि", + "TOP INACTIVITYButton": "सबसे निष्क्रिय", + topinactivitySectionTitle: "सबसे कम सक्रिय निवासी", blockedSectionTitle: "ब्लॉक किए गए", gallerySectionTitle: "गैलरी", - blockedButton: "ब्लॉक किए गए", + topSectionTitle: "शीर्ष", blockedLabel: "ब्लॉक किया गया उपयोगकर्ता", inhabitantviewDetails: "विवरण देखें", keepReading: "पढ़ते रहें...", @@ -1024,13 +1130,26 @@ module.exports = { parliamentTitle: "संसद", parliamentDescription: "शासन के रूपों और सामूहिक प्रबंधन कानूनों का अन्वेषण करें।", parliamentFilterGovernment: "सरकार", - parliamentFilterCandidatures: "उम्मीदवारी", + parliamentFilterCandidatures: "उम्मीदवारियाँ", parliamentFilterProposals: "प्रस्ताव", parliamentFilterLaws: "कानून", - parliamentFilterHistorical: "ऐतिहासिक", + parliamentFilterRevocations: "निरसन", + parliamentFilterHistorical: "इतिहास", parliamentFilterLeaders: "नेता", parliamentFilterRules: "नियम", parliamentGovernmentCard: "वर्तमान सरकार", + parliamentSeatsGovernment: "सरकार", + parliamentSeatsOpposition: "विपक्ष", + parliamentSeatsAbstention: "अनुपस्थित", + parliamentSeatsQuorum: "चुनाव कोरम", + parliamentSeatsPass: "अनुमोदन सीमा", + parliamentSeatsByElection: "चुनाव के अनुसार", + parliamentSeatsByHouses: "LARP हाउस के अनुसार", + parliamentSeatsSeats: "सीटें", + parliamentSeatsSeat: "सीट", + parliamentSeatsLeader: "नेता", + parliamentSeatsVotesFor: "के लिए वोट", + parliamentSeatsNoHouse: "कोई हाउस नहीं", parliamentActorInPowerInhabitant: 'सत्तारूढ़ निवासी', parliamentActorInPowerTribe: 'सत्तारूढ़ जनजाति', parliamentHistoricalGovernmentsTitle: 'सरकारें', @@ -1052,10 +1171,8 @@ module.exports = { parliamentPoliciesDeclined: "अस्वीकृत कानून", parliamentPoliciesDiscarded: "निरस्त कानून", parliamentEfficiency: "% दक्षता", - parliamentFilterRevocations: 'निरसन', parliamentRevocationFormTitle: 'कानून निरसन', parliamentRevocationLaw: 'कानून', - parliamentRevocationTitle: 'शीर्षक', parliamentRevocationReasons: 'कारण', parliamentRevocationPublish: 'निरसन प्रकाशित करें', parliamentCurrentRevocationsTitle: 'वर्तमान निरसन', @@ -1068,23 +1185,12 @@ module.exports = { parliamentNoGovernments: "अभी तक कोई सरकार नहीं है।", parliamentCandidatureFormTitle: "उम्मीदवारी प्रस्तावित करें", parliamentCandidatureIdPh: "Oasis ID (@...) या जनजाति का नाम", - parliamentCandidatureSlogan: "नारा (अधिकतम 140 अक्षर)", - parliamentCandidatureSloganPh: "एक छोटा आदर्श वाक्य", parliamentCandidatureMethod: "पद्धति", parliamentCandidatureProposeBtn: "उम्मीदवारी प्रकाशित करें", - parliamentThDate: "प्रस्ताव तिथि", - parliamentThSlogan: "नारा", - parliamentThSince: "प्रोफ़ाइल से", - parliamentThVotes: "प्राप्त मत", - parliamentThVoteAction: "मत दें", - parliamentTypeUser: "निवासी", - parliamentTypeTribe: "जनजाति", parliamentVoteBtn: "मत दें", parliamentProposalFormTitle: "कानून प्रस्तावित करें", parliamentProposalDescription: "विवरण (≤1000)", parliamentProposalPublish: "प्रस्ताव प्रकाशित करें", - parliamentFinalize: "अंतिम रूप दें", - parliamentDeadline: "समय सीमा", parliamentNoProposals: "अभी तक कोई कानून प्रस्ताव नहीं है।", parliamentNoLaws: "अभी तक कोई स्वीकृत कानून नहीं है।", parliamentMethodDEMOCRACY: "लोकतंत्र", @@ -1102,29 +1208,22 @@ module.exports = { parliamentVotesSlashTotal: "मत/कुल", parliamentVotesNeeded: "आवश्यक मत", parliamentFutureLawsTitle: "भविष्य के कानून", - parliamentNoFutureLaws: "अभी तक कोई भविष्य के कानून नहीं।", parliamentVoteAction: "मत दें", - parliamentLeadersTitle: "नेता", parliamentThLeader: "अवतार", parliamentPopulation: "जनसंख्या", - parliamentThType: "प्रकार", - parliamentThInPower: "सत्ता में", parliamentThPresented: "उम्मीदवारी", parliamentNoLeaders: "अभी तक कोई नेता नहीं।", parliamentCandidaturesListTitle: "उम्मीदवारी सूची", parliamentTimeRemaining: "शेष समय", - parliamentNoLeader: "अभी तक कोई विजयी उम्मीदवारी नहीं।", parliamentElectionsStatusTitle: "अगली सरकार", parliamentProposalDeadlineLabel: "समय सीमा", + parliamentViewReport: "रिपोर्ट देखें", parliamentProposalTimeLeft: "शेष समय", - parliamentProposalOnTrack: "पारित होने की राह पर", - parliamentProposalOffTrack: "अभी तक पर्याप्त समर्थन नहीं", parliamentRulesTitle: "संसद कैसे काम करती है", parliamentRulesIntro: "चुनाव हर 2 महीने में हल होते हैं; उम्मीदवारी निरंतर होती है और सरकार चुने जाने पर रीसेट हो जाती है।", parliamentRulesCandidates: "कोई भी निवासी अपने आप को, किसी अन्य निवासी को, या किसी जनजाति को प्रस्तावित कर सकता है। प्रत्येक निवासी प्रति चक्र अधिकतम 3 उम्मीदवारी प्रस्तावित कर सकता है; एक ही चक्र में दोहराव अस्वीकार किए जाते हैं।", parliamentRulesElection: "विजेता वह उम्मीदवारी है जिसे समाधान के समय सबसे अधिक मत प्राप्त हों।", parliamentRulesTies: "टाई-ब्रेक क्रम: सर्वोच्च निवासी कर्म; यदि बराबर, सबसे पुरानी प्रोफ़ाइल; यदि अभी भी बराबर, सबसे पहला प्रस्ताव; फिर शब्दकोषीय ID द्वारा।", - parliamentRulesFallback: "यदि कोई मत नहीं देता, तो नवीनतम प्रस्तावित उम्मीदवारी जीतती है। यदि कोई उम्मीदवारी नहीं है, तो एक यादृच्छिक जनजाति चुनी जाती है।", parliamentRulesTerm: "सरकार टैब वर्तमान सरकार और आँकड़े दिखाता है।", parliamentRulesMethods: "रूप: अराजकता (साधारण बहुमत), लोकतंत्र (50%+1), बहुमत (80%), अल्पमत (20%), कर्मतंत्र (सर्वोच्च-कर्म प्रस्ताव), तानाशाही (तत्काल अनुमोदन)।", parliamentRulesAnarchy: "अराजकता डिफ़ॉल्ट मोड है: यदि समाधान पर कोई उम्मीदवारी नहीं चुनी जाती, तो अराजकता घोषित होती है। अराजकता के तहत, कोई भी निवासी कानून प्रस्तावित कर सकता है।", @@ -1133,6 +1232,7 @@ module.exports = { parliamentRulesLaws: "जब कोई प्रस्ताव अपनी सीमा पूरी करता है, तो वह कानून बन जाता है और कानून टैब में लागू होने की तिथि के साथ दिखाई देता है।", parliamentRulesHistorical: "ऐतिहासिक टैब में आप हर सरकारी चक्र और उसके प्रबंधन का डेटा देख सकते हैं।", parliamentRulesLeaders: "नेता टैब में आप उन निवासियों/जनजातियों की रैंकिंग देख सकते हैं जिन्होंने शासन किया है (या उम्मीदवार रहे हैं), दक्षता के अनुसार क्रमबद्ध।", + parliamentRulesHemicycle: "अर्धवृत्त प्रत्येक निवासी को एक सीट देता है: सरकार का खंड (विजयी उम्मीदवारी के मतदाता और नेता), चक्र की हर अन्य उम्मीदवारी के लिए एक विपक्षी खंड, और अनुपस्थिति। बिंदीदार निशान चुनाव कोरम (जनगणना का 25%) और वर्तमान पद्धति की अनुमोदन सीमा दिखाते हैं। \"LARP हाउस के अनुसार\" उन्हीं सीटों को हाउस के अनुसार रंगता है।", parliamentProposalVoteStatusLabel: "मत स्थिति", parliamentProposalOnTrackYes: "सीमा पूरी हुई", parliamentProposalOnTrackNo: "सीमा से नीचे", @@ -1146,11 +1246,8 @@ module.exports = { courtsCaseFormTitle: "मामला खोलें", courtsCaseRespondent: "आरोपी / प्रतिवादी", courtsCaseRespondentPh: "Oasis ID (@...) या जनजाति का नाम", - courtsCaseMediatorsAccuser: "मध्यस्थ (वादी)", + courtsCaseSubjectPlaceholder: "विषय या संक्षिप्त विवरण", courtsCaseMethod: "समाधान पद्धति", - courtsCaseDescription: "विवरण (अधिकतम 1000 अक्षर)", - courtsCaseEvidenceTitle: "मामले के साक्ष्य", - courtsCaseEvidenceHelp: "अपने मामले का समर्थन करने वाली छवियाँ, ऑडियो, दस्तावेज़ (PDF) या वीडियो संलग्न करें।", courtsCaseSubmit: "मामला दायर करें", courtsNominateJudge: "न्यायाधीश नामांकित करें", courtsJudgeId: "न्यायाधीश", @@ -1183,34 +1280,33 @@ module.exports = { courtsMethodDICTATOR: "तानाशाह", courtsMethodPOPULAR: "लोकप्रिय", courtsMethodKARMATOCRACY: "कर्मतंत्र", + courtsCaseType: "मामले का प्रकार", + courtsPresetMinorConflict: "मामूली विवाद", + courtsPresetModerateConflict: "मध्यम विवाद", + courtsPresetSevereConflict: "गंभीर विवाद", + courtsPresetHarassment: "उत्पीड़न या दुर्व्यवहार", + courtsPresetContentModeration: "सामग्री मॉडरेशन", + courtsPresetBanRestriction: "प्रतिबंध या सीमा", + courtsPresetGovernanceDispute: "शासन विवाद", + courtsStatusOPEN: "खुला", + courtsStatusIN_PROGRESS: "प्रगति में", + courtsStatusDECIDED: "निर्णीत", + courtsStatusCLOSED: "बंद", + courtsStatusSOLVED: "सुलझा", + courtsStatusUNSOLVED: "अनसुलझा", + courtsStatusDISCARDED: "खारिज", courtsRulesTitle: "न्यायालय कैसे काम करते हैं", courtsRulesIntro: "न्यायालय विवादों को हल करने और पुनर्स्थापनात्मक न्याय को बढ़ावा देने की एक सामुदायिक प्रक्रिया है। संवाद, स्पष्ट साक्ष्य और आनुपातिक उपायों को प्राथमिकता दी जाती है।", courtsRulesLifecycle: "प्रक्रिया: 1) मामला खोलें 2) पद्धति चुनें 3) साक्ष्य प्रस्तुत करें 4) सुनवाई और विचार-विमर्श 5) फैसला और उपाय 6) अनुपालन और समापन 7) अपील (यदि लागू हो)।", - courtsRulesRoles: "वादी: मामला खोलता है। बचाव: आरोपित व्यक्ति या जनजाति। पद्धति: समुदाय द्वारा चुनी गई व्यवस्था जो सुविधा प्रदान करती है, साक्ष्य का मूल्यांकन करती है, और फैसला जारी करती है। गवाह: गवाही या साक्ष्य प्रदान करता है। मध्यस्थ: वादी और/या बचाव द्वारा आमंत्रित तटस्थ लोग, जिनकी सभी विवरणों तक पहुँच है, जो विवाद को शांत करने और समझौते सह-निर्माण करने में मदद करते हैं।", + courtsRulesRoles: "वादी: मामला खोलता है। बचाव: आरोपित निवासी या जनजाति। पद्धति: समुदाय द्वारा चुनी गई व्यवस्था जो सुविधा प्रदान करती है, साक्ष्य का मूल्यांकन करती है, और फैसला जारी करती है। गवाह: गवाही या साक्ष्य प्रदान करता है। मध्यस्थ: वादी और/या बचाव द्वारा आमंत्रित तटस्थ निवासी, जिनकी सभी विवरणों तक पहुँच है, जो विवाद को शांत करने और समझौते सह-निर्माण करने में मदद करते हैं।", courtsRulesEvidence: "विवरण अधिकतम 1000 अक्षर। प्रासंगिक और वैध छवियाँ, ऑडियो, वीडियो और PDF दस्तावेज़ संलग्न करें। सहमति के बिना संवेदनशील निजी डेटा साझा न करें।", courtsRulesDeliberation: "सुनवाई सार्वजनिक या निजी हो सकती है। न्यायाधीश सम्मान सुनिश्चित करते हैं, स्पष्टीकरण का अनुरोध करते हैं, और अप्रासंगिक या गैरकानूनी सामग्री को हटा सकते हैं।", courtsRulesVerdict: "पुनर्स्थापना को प्राथमिकता दी जाती है: क्षमा याचना, मध्यस्थता समझौते, सामग्री मॉडरेशन, अस्थायी प्रतिबंध, या अन्य आनुपातिक उपाय। तर्क दर्ज किया जाना चाहिए।", courtsRulesAppeals: "अपील: नए साक्ष्य या स्पष्ट प्रक्रियात्मक त्रुटि होने पर अनुमति है। अन्यथा कहा न जाए तो 7 दिनों के भीतर दायर की जानी चाहिए।", - courtsRulesPrivacy: "गोपनीयता और सुरक्षा का सम्मान करें। डॉक्सिंग, घृणा, या धमकियाँ हटा दी जाती हैं। न्यायाधीश जोखिम वाले लोगों की सुरक्षा के लिए रिकॉर्ड के भागों को संपादित या सील कर सकते हैं।", + courtsRulesPrivacy: "गोपनीयता और सुरक्षा का सम्मान करें। डॉक्सिंग, घृणा, या धमकियाँ हटा दी जाती हैं। न्यायाधीश जोखिम वाले निवासियों की सुरक्षा के लिए रिकॉर्ड के भागों को संपादित या सील कर सकते हैं।", courtsRulesMisconduct: "उत्पीड़न, हेरफेर, या मनगढ़ंत साक्ष्य तत्काल नकारात्मक समाधान का कारण बन सकते हैं।", courtsRulesGlossary: "मामला: विवाद का रिकॉर्ड। साक्ष्य: दावों का समर्थन करने वाली सामग्री। फैसला: उपाय के साथ निर्णय। अपील: फैसले की समीक्षा का अनुरोध।", courtsFilterActions: "कार्रवाई", - courtsNoActions: "आपकी भूमिका के लिए कोई लंबित कार्रवाई नहीं।", - courtsCaseTitlePlaceholder: "विवाद का संक्षिप्त विवरण", - courtsCaseSeverity: "गंभीरता", - courtsCaseSeverityNone: "कोई गंभीरता टैग नहीं", - courtsCaseSeverityLOW: "कम", - courtsCaseSeverityMEDIUM: "मध्यम", - courtsCaseSeverityHIGH: "उच्च", - courtsCaseSeverityCRITICAL: "गंभीर", - courtsCaseSubject: "विषय", - courtsCaseSubjectNone: "कोई विषय टैग नहीं", - courtsCaseSubjectBEHAVIOUR: "व्यवहार", - courtsCaseSubjectCONTENT: "सामग्री", - courtsCaseSubjectGOVERNANCE: "शासन / नियम", - courtsCaseSubjectFINANCIAL: "वित्तीय / संसाधन", - courtsCaseSubjectOTHER: "अन्य", - courtsHiddenRespondent: "छुपा हुआ (केवल संबंधित भूमिकाओं को दिखाई देता है)।", courtsThRole: "भूमिका", courtsRoleAccuser: "वादी", courtsRoleDefence: "बचाव", @@ -1221,51 +1317,23 @@ module.exports = { courtsAssignJudgeBtn: "न्यायाधीश चुनें", trendingTitle: "ट्रेंडिंग", exploreTrending: "अपने नेटवर्क में सबसे लोकप्रिय सामग्री खोजें।", - bookmarkButton: "बुकमार्क", - transferButton: "स्थानांतरण", - eventButton: "कार्यक्रम", - taskButton: "कार्य", votesButton: "मतदान", - reportButton: "रिपोर्ट", - feedButton: "फ़ीड", - marketButton: "बाज़ार", - imageButton: "छवियाँ", - audioButton: "ऑडियो", - videoButton: "वीडियो", - documentButton: "दस्तावेज़", - torrentButton: "टॉरेंट", - noTrendingFound: "कोई ट्रेंडिंग सामग्री नहीं मिली।", - noContentMessage: "अभी तक कोई ट्रेंडिंग सामग्री उपलब्ध नहीं।", - trendingDescription: "विवरण", - trendingDate: "दिनांक", - trendingLocation: "स्थान", - trendingPrice: "मूल्य", - trendingUrl: "URL", - trendingCategory: "श्रेणी", - trendingStart: "प्रारंभ", - trendingEnd: "समाप्ति", - trendingPriority: "प्राथमिकता", - trendingStatus: "स्थिति", - trendingFrom: "से", - trendingTo: "को", - trendingConcept: "अवधारणा", - trendingAmount: "राशि", - trendingDeadline: "समय सीमा", - trendingItemStatus: "आइटम स्थिति", - trendingTotalVotes: "कुल मत", + shopProductButton: "दुकान", + podcastButton: "पॉडकास्ट", + podcastEpisodeButton: "एपिसोड", + campaignButton: "अभियान", + logisticsRouteButton: "मार्ग", trendingTotalOpinions: "कुल राय", - trendingNoContentMessage: "अभी तक कोई ट्रेंडिंग सामग्री उपलब्ध नहीं।", - trendingAuthor: "द्वारा", - trendingCreatedAtLabel: "बनाया गया", + trendingNoContentMessage: "अभी तक कोई ट्रेंडिंग नहीं है।", trendingTotalCount: "कुल संख्या", tasksTitle: "कार्य", tasksDescription: "अपने नेटवर्क में कार्य खोजें और प्रबंधित करें।", + taskSearchPlaceholder: "कार्य में खोजें...", taskTitleLabel: "शीर्षक", taskDescriptionLabel: "विवरण", taskStartTimeLabel: "प्रारंभ समय", taskEndTimeLabel: "समाप्ति समय", taskPriorityLabel: "प्राथमिकता", - taskPrioritySelect: "प्राथमिकता चुनें", taskPriorityUrgent: "अत्यावश्यक", taskPriorityHigh: "उच्च", taskPriorityMedium: "मध्यम", @@ -1275,13 +1343,13 @@ module.exports = { taskVisibilityLabel: "दृश्यता", taskPublic: "सार्वजनिक", taskPrivate: "निजी", - taskCreatedAt: "बनाया गया", - taskBy: "द्वारा", taskStatus: "स्थिति", taskStatusOpen: "खुला", taskStatusInProgress: "प्रगति में", taskStatusClosed: "बंद", - taskAssignedTo: "को सौंपा गया", + taskAssignedTo: "सौंपा गया", + taskAssignedChip: "सौंपा गया", + taskUnassignedChip: "असौंपा", taskAssignees: "सौंपे गए", taskAssignButton: "मुझे सौंपें", taskUnassignButton: "सौंपना रद्द करें", @@ -1289,12 +1357,12 @@ module.exports = { taskUpdateButton: "अपडेट", taskDeleteButton: "हटाएँ", taskFilterAll: "सभी", + taskFilterRecent: "हाल के", taskFilterMine: "मेरे", taskFilterOpen: "खुले", taskFilterInProgress: "प्रगति में", taskFilterClosed: "बंद", taskFilterAssigned: "सौंपे गए", - taskFilterArchived: "संग्रहीत", taskFilterUrgent: "अत्यावश्यक", taskFilterHigh: "उच्च", taskFilterMedium: "मध्यम", @@ -1307,35 +1375,47 @@ module.exports = { taskInProgressTitle: "प्रगति में कार्य", taskClosedTitle: "बंद कार्य", taskAssignedTitle: "सौंपे गए कार्य", - taskArchivedTitle: "संग्रहीत कार्य", - taskPublicTitle: "सार्वजनिक कार्य", - taskPrivateTitle: "निजी कार्य", notasks: "कोई कार्य उपलब्ध नहीं।", noLocation: "कोई स्थान निर्दिष्ट नहीं", taskSetStatus: "स्थिति सेट करें", - eventTitle: "कार्यक्रम", eventDateLabel: "दिनांक", + eventRecurrenceLabel: "पुनरावृत्ति", + eventRecurrenceUntil: "इस तिथि तक दोहराएँ", + eventRecurrenceHint: "एकल इवेंट के लिए अंतिम तिथि खाली छोड़ें।", + eventUpcomingDates: "आगामी तिथियाँ", eventsTitle: "कार्यक्रम", eventsDescription: "अपने नेटवर्क में कार्यक्रम खोजें और प्रबंधित करें।", - eventDescription: "विवरण", + eventSearchPlaceholder: "कार्यक्रम में खोजें...", eventPrice: "मूल्य", eventStatus: "स्थिति", - eventOrganizer: "आयोजक", eventAllSectionTitle: "कार्यक्रम", eventMineSectionTitle: "आपके कार्यक्रम", - eventArchivedTitle: "संग्रहीत कार्यक्रम", eventCreateSectionTitle: "कार्यक्रम बनाएँ", eventUpdateSectionTitle: "कार्यक्रम अपडेट करें", - eventDeleteButton: "हटाएँ", eventCreateButton: "कार्यक्रम बनाएँ", eventAddToCalendar: "कैलेंडर में जोड़ें", eventVisitCalendar: "कैलेंडर देखें", viewTask: "कार्य देखें", + viewEvent: "इवेंट देखें", + viewPad: "पैड देखें", + viewMap: "मानचित्र देखें", + viewCalendar: "कैलेंडर देखें", viewReport: "रिपोर्ट देखें", viewTransfer: "स्थानांतरण देखें", viewItem: "आइटम देखें", viewShop: "दुकान देखें", viewProject: "परियोजना देखें", + viewJob: "नौकरी देखें", + viewPlace: "स्थान देखें", + generatePdf: "PDF बनाएँ", + sharePm: "PM से साझा करें", + galleryImages: "छवियाँ (अधिकतम आकार: 75MB प्रत्येक)", + galleryPhotos: "छवियाँ", + galleryAddPhoto: "छवि जोड़ें", + galleryRemovePhoto: "हटाएँ", + galleryVideo: "वीडियो (अधिकतम आकार: 75MB)", + galleryAddVideo: "वीडियो जोड़ें", + galleryRemoveVideo: "वीडियो हटाएँ", viewVotation: "मतदान देखें", voteCastTitle: "मतदान करें", voteResults: "परिणाम", @@ -1343,48 +1423,26 @@ module.exports = { eventDescriptionLabel: "विवरण", eventDescriptionPlaceholder: "कार्यक्रम विवरण दर्ज करें...", eventUpdateButton: "अपडेट", - eventAttendeesLabel: "उपस्थित", - eventAttendeesPlaceholder: "अल्पविराम से अलग करके उपस्थित दर्ज करें...", eventTagsLabel: "टैग", - eventTagsPlaceholder: "अल्पविराम से अलग करके कार्यक्रम टैग दर्ज करें...", - eventTags: "टैग", eventPriceLabel: "मूल्य", eventUrlLabel: "URL", eventAttendees: "उपस्थित", - noAttendees: "अभी तक कोई उपस्थित नहीं", - eventCreatedAt: "बनाया गया", eventLocation: "स्थान", eventLocationLabel: "स्थान", - eventNoLocation: "कोई स्थान निर्दिष्ट नहीं", - eventNoURL: "कोई URL निर्दिष्ट नहीं", - eventBy: "द्वारा", noevents: "कोई कार्यक्रम उपलब्ध नहीं।", eventDate: "दिनांक", - eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "कार्यक्रम में शामिल हों", - eventUnattendButton: "कार्यक्रम छोड़ें", - eventCreatedBy: "द्वारा बनाया गया", - eventAttendeesCount: "उपस्थित संख्या", - eventCreatedByYou: "आपने यह कार्यक्रम बनाया", eventFilterAll: "सभी", + eventFilterRecent: "हाल के", eventFilterMine: "मेरे", eventFilterToday: "आज", eventFilterWeek: "इस सप्ताह", eventFilterMonth: "इस महीने", eventFilterYear: "इस वर्ष", eventFilterArchived: "संग्रहीत", - eventTodayTitle: "आज के कार्यक्रम", - eventThisWeekTitle: "इस सप्ताह के कार्यक्रम", - eventThisMonthTitle: "इस महीने के कार्यक्रम", - eventThisYearTitle: "इस वर्ष के कार्यक्रम", eventPrivacyLabel: "दृश्यता", eventPublic: "सार्वजनिक", eventPrivate: "निजी", - eventPublicTitle: "सार्वजनिक कार्यक्रम", - eventNoPrice: "निःशुल्क कार्यक्रम", - eventNoImage: "कोई छवि अपलोड नहीं की गई", - eventAttendConfirmation: "अब आप इस कार्यक्रम में शामिल हैं", - eventUnattendConfirmation: "अब आप इस कार्यक्रम में शामिल नहीं हैं", eventAttended: "शामिल हुए", eventUnattended: "शामिल नहीं", eventStatusOpen: "खुला", @@ -1395,9 +1453,14 @@ module.exports = { tagsTopSectionTitle: "शीर्ष टैग", tagsCloudSectionTitle: "टैग क्लाउड", tagsFilterAll: "सभी", + tagsFilterMine: "मेरे", + tagsFilterRecent: "हाल के", + tagsMineSectionTitle: "मेरे टैग", + tagsRecentSectionTitle: "हाल के टैग", tagsFilterTop: "शीर्ष", tagsFilterCloud: "क्लाउड", tagsNoItems: "कोई टैग उपलब्ध नहीं।", + tagsSearchPlaceholder: "टैग में खोजें...", tagsTableHeaderTag: "टैग/लिंक:", tagsTableHeaderCount: "गणना:", transfersTitle: "स्थानांतरण", @@ -1405,7 +1468,10 @@ module.exports = { transfersFrom: "से", transfersTo: "को", transfersFilterAll: "सभी", + transfersFilterRecent: "हाल के", transfersFilterMine: "मेरे", + transfersFilterMinePending: "लंबित", + transfersFilterMineConfirmed: "हस्ताक्षरित", transfersFilterUBI: "UBI", transfersFilterMarket: "बाज़ार", transfersFilterTop: "शीर्ष", @@ -1420,7 +1486,6 @@ module.exports = { transfersCategoryEconomic: "आर्थिक", transfersCategoryTime: "समय", transfersCategoryTrust: "भरोसा", - transfersCategoryApply: "लागू करें", transfersUnitEco: "ECO", transfersUnitHours: "घं", transfersUnitTrust: "भरोसा", @@ -1437,18 +1502,12 @@ module.exports = { transfersDeadline: "समय सीमा", transfersTags: "टैग", transfersStatus: "स्थिति", - transfersStatusUnconfirmed: "अपुष्ट", - transfersStatusClosed: "बंद", - transfersStatusDiscarded: "निरस्त", - transfersCreatedAt: "बनाया गया", transfersConfirmations: "पुष्टि", - transfersContainingBlock: "धारक ब्लॉक", - transfersExportContract: "अनुबंध बनाएँ", + transfersExportContract: "बिल बनाएँ", transfersConfirmButton: "स्थानांतरण पुष्टि करें", transfersNoItems: "कोई स्थानांतरण नहीं मिला।", transfersMineSectionTitle: "आपके स्थानांतरण", transfersUBISectionTitle: "UBI स्थानांतरण", - transfersMarketSectionTitle: "बाज़ार स्थानांतरण", transfersTopSectionTitle: "शीर्ष स्थानांतरण", transfersPendingSectionTitle: "लंबित स्थानांतरण", transfersUnconfirmedSectionTitle: "अपुष्ट स्थानांतरण", @@ -1456,21 +1515,13 @@ module.exports = { transfersDiscardedSectionTitle: "निरस्त स्थानांतरण", transfersCreateSectionTitle: "स्थानांतरण बनाएँ", transfersAllSectionTitle: "स्थानांतरण", - transfersFilterFavs: "पसंदीदा", - transfersFavsSectionTitle: "पसंदीदा स्थानांतरण", - transfersSearchLabel: "खोज", - transfersSearchPlaceholder: "अवधारणा, टैग, उपयोगकर्ता खोजें...", + transfersSearchPlaceholder: "स्थानांतरण में खोजें...", transfersMinAmountLabel: "न्यूनतम राशि", transfersMaxAmountLabel: "अधिकतम राशि", - transfersSortLabel: "क्रमबद्ध करें", transfersSortRecent: "सबसे हाल का", transfersSortAmount: "सबसे अधिक राशि", transfersSortDeadline: "निकटतम समय सीमा", transfersSearchButton: "खोजें", - transfersFavoriteButton: "पसंदीदा", - transfersUnfavoriteButton: "पसंदीदा हटाएँ", - transfersMessageUserButton: "संदेश", - transfersExpiringSoonBadge: "समाप्त हो रहा है", transfersExpiredBadge: "समाप्त", transfersUpdatedAt: "अपडेट किया गया", transfersNoMatch: "आपकी खोज से कोई स्थानांतरण मेल नहीं खाता।", @@ -1483,6 +1534,10 @@ module.exports = { voteClosedTitle: "बंद मतदान", voteAllSectionTitle: "मतदान", voteCreateButton: "मतदान बनाएँ", + voteErrorTitle: "फ़ॉर्म जाँचें", + voteErrorDeadlineMin: "समय-सीमा अभी से कम से कम {days} दिन आगे होनी चाहिए।", + voteErrorGeneric: "वोट सहेजा नहीं जा सका।", + voteLocked: "इस मतदान पर पहले ही वोट या राय आ चुकी है, या यह संसद या न्यायालयों का है: अब इसे संपादित या हटाया नहीं जा सकता।", voteUpdateButton: "अपडेट", voteDeleteButton: "हटाएँ", voteOptionYes: "हाँ", @@ -1492,6 +1547,7 @@ module.exports = { voteFollowMajority: "बहुमत का पालन करें", voteNotInterested: "रुचि नहीं", voteFilterAll: "सभी", + voteFilterRecent: "हाल के", voteFilterMine: "मेरे", voteFilterOpen: "खुले", voteFilterClosed: "बंद", @@ -1507,37 +1563,28 @@ module.exports = { voteTags: "टैग", voteOpinions: "राय", novotes: "कोई मतदान प्रस्ताव उपलब्ध नहीं।", - voteBy: "द्वारा", voteCreatedAt: "बनाया गया", voteNoQuestion: "कोई प्रश्न प्रदान नहीं किया गया", voteUnknownCreator: "अज्ञात निर्माता", voteUnknownDate: "अज्ञात दिनांक", - errorVoteNotFound: "मत नहीं मिला", - errorAlreadyVoted: "आपने पहले ही राय दे दी है।", - errorVoteClosedCannotEdit: "आप बंद मतदान को संपादित नहीं कर सकते", - errorVoteDeadlinePassed: "इस मतदान की समय सीमा बीत चुकी है", - errorRetrievingVote: "मत प्राप्त करने में त्रुटि", - errorCreatingVote: "मत बनाने में त्रुटि", - errorVoteAlreadyVoted: "राय दिए जाने के बाद संपादित नहीं किया जा सकता", - errorDeletingOldVote: "पुरानी राय हटाने में त्रुटि", - errorCreatingUpdatedVote: "अपडेटेड राय बनाने में त्रुटि", - errorCreatingTombstone: "टॉम्बस्टोन बनाने में त्रुटि", voteDetailSectionTitle: 'मत विवरण', voteCommentsLabel: 'टिप्पणियाँ', voteCommentsForumButton: 'चर्चा खोलें', voteCommentsSectionTitle: 'चर्चा खोलें', voteNoCommentsYet: 'अभी तक कोई टिप्पणी नहीं है। पहले उत्तर देने वाले बनें।', + commentsGoToLast: "अंतिम टिप्पणी पर जाएँ", voteNewCommentPlaceholder: 'अपनी टिप्पणी यहाँ लिखें…', voteNewCommentButton: 'टिप्पणी पोस्ट करें', voteNewCommentLabel: 'टिप्पणी जोड़ें', cvTitle: "CV", - cvLabel: "बायोडाटा (CV)", cvEditSectionTitle: "CV संपादित करें", cvCreateSectionTitle: "CV बनाएँ", cvDescription: "अपने पेशेवर कौशल और जानकारी प्रबंधित करें और साझा करें।", cvNameLabel: "पूरा नाम", cvDescriptionLabel: "सारांश", cvPhotoLabel: "फ़ोटो", + cvPdfLabel: "सीवी (PDF)", + cvVisitButton: "CV देखें", cvPersonalExperiencesLabel: "व्यक्तिगत अनुभव", cvPersonalSkillsLabel: "व्यक्तिगत कौशल (अल्पविराम से अलग)", cvOasisExperiencesLabel: "Oasis योगदान अनुभव", @@ -1550,29 +1597,130 @@ module.exports = { cvLocationLabel: "स्थान", cvStatusLabel: "स्थिति", cvPreferencesLabel: "प्राथमिकताएँ", - cvOasisContributorLabel: "Oasis योगदानकर्ता", cvPersonal: "व्यक्तिगत", cvOasis: "Oasis योगदानकर्ता (वैकल्पिक)", - cvOasisContributorView: "Oasis योगदान", + cvOasisContributorView: "योगदान", cvEducational: "शैक्षिक (वैकल्पिक)", cvEducationalView: "शैक्षिक", cvProfessional: "पेशेवर (वैकल्पिक)", cvProfessionalView: "पेशेवर", cvAvailability: "उपलब्धता (वैकल्पिक)", - cvAvailabilityView: "उपलब्धता", cvUpdateButton: "अपडेट", cvCreateButton: "CV बनाएँ", - cvContactLabel: "संपर्क", cvCreatedAt: "बनाया गया", cvUpdatedAt: "अपडेट किया गया", cvEditButton: "अपडेट", cvDeleteButton: "हटाएँ", cvNoCV: "कोई CV नहीं मिला।", blogSubject: "विषय", - blogMessage: "मुख्य भाग 8096 वर्णों तक सीमित।", - blogImage: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", - blogPublish: "पूर्वावलोकन", - noPopularMessages: "अभी तक कोई लोकप्रिय संदेश प्रकाशित नहीं हुआ", + blogMessage: "संदेश", + blogImage: "मीडिया अपलोड करें (अधिकतम-आकार: 75MB)", + blogMedia: "अनुलग्नक (8 चित्र और एक वीडियो तक)", + blogPublish: "प्रकाशित करें", + pollsTitle: "सर्वेक्षण", + dataTitle: "मिलान", + dataDescription: "अपने नेटवर्क के मेल खोजें और देखें।", + dataFilterAll: "सभी", + dataFilterMine: "मेरे", + dataFilterRecent: "हाल के", + dataFilterTop: "शीर्ष", + dataKindInhabitants: "निवासी", + dataKindJobs: "नौकरियाँ", + dataKindProjects: "परियोजनाएँ", + dataKindEvents: "इवेंट्स", + dataKindTribes: "जनजातियाँ", + dataKindMarket: "मार्केट", + dataKindHousing: "आवास", + dataKindIndustry: "उद्योग", + dataKindTasks: "कार्य", + dataKindReports: "रिपोर्ट्स", + dataKindVotes: "मतदान", + dataSearchPlaceholder: "मिलान में खोजें...", + dataCohesionTitle: "संसक्ति गुणांक (CC)", + dataCcShort: "CC", + dataCohesionHint: "नेटवर्क ने जो कुछ प्रकाशित किया है, उसके बीच औसत समानता।", + dataAffinity: "समानता", + dataCommonTerms: "कीवर्ड", + dataReasonFollowing: "आप इस निवासी का समर्थन करते हैं", + dataReasonTribe: "आपके साथ एक जनजाति साझा करता है", + dataReasonPinned: "आपके पिन किए गए कंटेंट से मेल खाता है", + dataReasonCv: "आपके CV के कौशल से मेल खाता है", + dataReasonContent: "आपके प्रकाशित कंटेंट से संबंधित", + dataReasonRelated: "आपकी रुचियों से संबंधित", + dataReasonAlike: "आपके जैसे निवासी द्वारा प्रकाशित", + dataReasonSupportsYou: "आपका समर्थन करता है", + dataReasonMutual: "पारस्परिक समर्थन", + dataReasonRated: "नेटवर्क द्वारा अच्छी रेटिंग", + dataReasonNear: "आपके पास", + dataStatPeople: "CVs", + dataStatConnected: "जुड़े हुए", + dataStatSkills: "कौशल", + dataBestMatch: "सर्वश्रेष्ठ मैच", + dataStatIsolated: "अलग-थलग", + dataStatEntities: "तुलना की गई इकाइयाँ", + dataStatTerms: "विशिष्ट शब्द", + dataStatPairs: "जुड़े हुए जोड़े", + dataMatchesTitle: "मेल", + dataMatchesHint: "एल्गोरिद्म के व्यक्तिगत सुझाव।", + dataTermsTitle: "मुख्य शब्द", + dataTermsHint: "वे शब्द जो सबसे अधिक सामग्री में आते हैं।", + dataConnections: "जुड़े मिलान", + dataNoMatches: "दिखाने के लिए क्रॉस-डेटा नहीं।", + dataNoProfile: "सामग्री प्रकाशित करें या अपना CV लिखें ताकि एल्गोरिद्म जान सके कि आपकी रुचि किसमें है।", + pollsDescription: "नेटवर्क से पूछने और उत्तर गिनने का मॉड्यूल।", + pollFilterAll: "सभी", + pollFilterMine: "मेरे", + pollFilterRecent: "हाल के", + pollFilterTop: "शीर्ष", + pollFilterVoted: "मतदान किया", + pollFilterOpen: "खुले", + pollFilterClosed: "बंद", + pollCreateButton: "सर्वेक्षण बनाएँ", + pollCreateTitle: "सर्वेक्षण बनाएँ", + pollEditTitle: "सर्वेक्षण संपादित करें", + pollQuestion: "प्रश्न", + pollQuestionPlaceholder: "आप क्या पूछना चाहते हैं?", + pollOptions: "विकल्प (प्रति पंक्ति एक)", + pollOutcome: "परिणाम", + pollNoVotesYet: "अभी कोई वोट नहीं", + pollTie: "बराबरी", + pollOptionsPlaceholder: "चौक\nपार्क\nबार", + pollAnonymous: "गुमनाम", + pollAnonymousLabel: "गुमनाम सर्वेक्षण", + pollMultiple: "बहुविकल्प", + pollMultipleLabel: "कई उत्तरों की अनुमति दें", + pollDeadline: "समय सीमा", + pollTags: "टैग", + pollPublishButton: "सर्वेक्षण प्रकाशित करें", + pollUpdateButton: "अपडेट करें", + pollCloseButton: "बंद करें", + pollDeleteButton: "हटाएं", + pollVoteButton: "मत दें", + pollChangeVote: "वोट बदलें", + pollVoters: "मतदाता", + pollComments: "टिप्पणियाँ", + pollStatusLabel: "स्थिति", + pollStatusOpen: "खुला", + pollStatusClosed: "बंद", + pollSearchPlaceholder: "सर्वेक्षण में खोजें...", + pollsNoItems: "कोई सर्वेक्षण नहीं मिला।", + viewPoll: "सर्वेक्षण देखें", + pollChatCreate: "सर्वेक्षण बनाएँ", + pollInChat: "सर्वेक्षण", + blogTitle: "ब्लॉग", + blogDescription: "ब्लॉग खोजने और प्रबंधित करने का मॉड्यूल।", + blogFilterAll: "सभी", + blogFilterMine: "मेरे", + blogFilterRecent: "हाल के", + blogFilterFavorites: "पसंदीदा", + blogFilterTop: "शीर्ष", + blogCreateButton: "ब्लॉग बनाएँ", + blogSearchPlaceholder: "ब्लॉग में खोजें...", + blogComments: "टिप्पणियाँ", + blogAllowComments: "टिप्पणियाँ अनुमति दें", + blogCommentsClosed: "लेखक ने इस ब्लॉग की टिप्पणियाँ बंद कर दी हैं।", + blogNoItems: "कोई ब्लॉग नहीं मिला।", + viewBlog: "ब्लॉग देखें", forumTitle: "मंच", forumCategoryLabel: "श्रेणी", forumTitleLabel: "शीर्षक", @@ -1580,43 +1728,21 @@ module.exports = { forumCreateButton: "मंच बनाएँ", forumCreateSectionTitle: "मंच बनाएँ", forumDescription: "अपने नेटवर्क के अन्य निवासियों से खुलकर बात करें।", + forumSearchPlaceholder: "मंच में खोजें...", forumFilterAll: "सभी", forumFilterMine: "मेरे", forumFilterRecent: "हाल के", forumFilterTop: "शीर्ष", - forumMineSectionTitle: "आपके मंच", - forumRecentSectionTitle: "हाल के मंच", - forumAllSectionTitle: "मंच", - forumDeleteButton: "हटाएँ", forumParticipants: "प्रतिभागी", forumMessages: "संदेश", - forumLastMessage: "अंतिम संदेश", forumMessageLabel: "संदेश", forumMessagePlaceholder: "अपना संदेश लिखें...", forumSendButton: "भेजें", - forumVisitForum: "मंच देखें", noForums: "कोई मंच नहीं मिला।", - forumVisitButton: "मंच देखें", - forumCatGENERAL: "सामान्य", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "राजनीति", - forumCatTECH: "प्रौद्योगिकी", - forumCatSCIENCE: "विज्ञान", - forumCatMUSIC: "संगीत", - forumCatART: "कला", - forumCatGAMING: "गेमिंग", - forumCatBOOKS: "पुस्तकें", - forumCatFILMS: "फ़िल्में", - forumCatPHILOSOPHY: "दर्शन", - forumCatSOCIETY: "समाज", - forumCatPRIVACY: "गोपनीयता", - forumCatCYBERWARFARE: "साइबरयुद्ध", - forumCatSURVIVALISM: "जीवनवाद", + forumVisitButton: "फ़ोरम देखें", + forumTopicLabel: "विषय", imageTitle: "छवियाँ", imageDescription: "अपने नेटवर्क में छवि सामग्री खोजें और प्रबंधित करें।", - imagePluginTitle: "शीर्षक", - imagePluginDescription: "विवरण", imageMineSectionTitle: "आपकी छवियाँ", imageCreateSectionTitle: "छवि अपलोड करें", imageUpdateSectionTitle: "छवि अपडेट करें", @@ -1625,17 +1751,14 @@ module.exports = { imageTopSectionTitle: "शीर्ष छवियाँ", imageFavoritesSectionTitle: "पसंदीदा", imageGallerySectionTitle: "गैलरी", - imageMemeSectionTitle: "मीम्स", imageFilterAll: "सभी", imageFilterMine: "मेरे", imageFilterRecent: "हाल के", imageFilterTop: "शीर्ष", imageFilterFavorites: "पसंदीदा", imageFilterGallery: "गैलरी", - imageFilterMeme: "मीम्स", imageCreateButton: "छवि अपलोड करें", imageUpdateButton: "अपडेट", - imageDeleteButton: "हटाएँ", imageAddFavoriteButton: "पसंदीदा जोड़ें", imageRemoveFavoriteButton: "पसंदीदा हटाएँ", imageFileLabel: "छवि फ़ाइल चुनें (.jpeg, .jpg, .png, .gif)", @@ -1645,23 +1768,21 @@ module.exports = { imageTitlePlaceholder: "वैकल्पिक", imageDescriptionLabel: "विवरण", imageDescriptionPlaceholder: "वैकल्पिक", - imageMemeLabel: "मीम के रूप में चिह्नित करें", imageNoFile: "कोई छवि फ़ाइल प्रदान नहीं की गई", noImages: "कोई छवि उपलब्ध नहीं।", - imageSearchPlaceholder: "शीर्षक, टैग, विवरण, लेखक खोजें...", + imageSearchPlaceholder: "छवियाँ में खोजें...", imageSortRecent: "सबसे हाल का", imageSortOldest: "सबसे पुराना", imageSortTop: "सर्वाधिक मतदान", imageSearchButton: "खोजें", - imageMessageAuthorButton: "संदेश", imageUpdatedAt: "अपडेट किया गया", imageNoMatch: "आपकी खोज से कोई छवि मेल नहीं खाती।", + feedSearchPlaceholder: "फ़ीड में खोजें...", feedTitle: "फ़ीड", createFeedTitle: "फ़ीड बनाएँ", createFeedButton: "फ़ीड भेजें!", feedPlaceholder: "क्या हो रहा है? (अधिकतम 280 अक्षर)", TODAYButton: "आज", - CREATEButton: "फ़ीड बनाएँ", totalOpinions: "कुल राय", moreVoted: "अधिक मतदान", noFeedsFound: "कोई फ़ीड नहीं मिली।", @@ -1671,7 +1792,7 @@ module.exports = { alreadyRefeeded: "आपने इसे पहले ही रीफ़ीड कर दिया है।", activityTitle: "गतिविधि", yourActivity: "आपकी गतिविधि", - globalActivity: "वैश्विक गतिविधि", + globalActivity: "गतिविधि", activityList: "गतिविधि", activityDesc: "अपने नेटवर्क में नवीनतम गतिविधि देखें।", allButton: "सभी", @@ -1684,20 +1805,13 @@ module.exports = { concept: "अवधारणा", description: "विवरण", meme: "मीम", - activityContact: "संपर्क", - activityBy: "नाम", - activityPixelia: "नया पिक्सेल जोड़ा गया", - viewImage: "छवि देखें", - playAudio: "ऑडियो चलाएँ", - playVideo: "वीडियो चलाएँ", typeRecent: "हाल के", - errorActivity: "गतिविधि प्राप्त करने में त्रुटि", - typePost: "ब्लॉग्स", - recentButton: "हाल का", + typeTop: "शीर्ष", + typePost: "ब्लॉग", + activityUpdateLabel: "अपडेट", typeTribe: "जनजातियाँ", typeLarp: "L.A.R.P.", typeInhabitants: "निवासी", - activityProfileUpdated: "ने अपनी प्रोफ़ाइल अपडेट की", typeAbout: "निवासी", typeCurriculum: "CVS", typeImage: "छवियाँ", @@ -1741,8 +1855,6 @@ module.exports = { typeCourtsSettlementAccepted: "न्यायालय · समझौता स्वीकृत", typeCourtsNomination: "न्यायालय · नामांकन", typeCourtsNominationVote: "न्यायालय · नामांकन मत", - activitySupport: "नया गठबंधन बना", - activityJoin: "नए PUB में शामिल हुए", question: "प्रश्न", deadline: "समय सीमा", status: "स्थिति", @@ -1756,12 +1868,8 @@ module.exports = { date: "दिनांक", category: "श्रेणी", attendees: "उपस्थित", - activitySpread: "->", - visitLink: "लिंक देखें", - viewDocument: "दस्तावेज़ देखें", location: "स्थान", contentWarning: "विषय", - personName: "निवासी का नाम", bankWalletConnected: "ECOin वॉलेट", bankUbiReceived: "UBI प्राप्त", bankTx: "Tx", @@ -1771,7 +1879,6 @@ module.exports = { link: "लिंक", activityProjectFollow: "%OASIS% अब इस परियोजना %PROJECT% को %ACTION% कर रहा है", activityProjectUnfollow: "%OASIS% अब इस परियोजना %PROJECT% को %ACTION% कर रहा है", - activityProjectPledged: "%OASIS% ने परियोजना %PROJECT% को %AMOUNT% %ACTION% किया है", following: "फ़ॉलो कर रहे हैं", unfollowing: "अनफ़ॉलो कर रहे हैं", pledged: "प्रतिज्ञा", @@ -1817,26 +1924,17 @@ module.exports = { courtsVotesSlashTotal: "हाँ/कुल", courtsOpenVote: "मतदान खोलें", courtsAnswerTitle: "उत्तर", - courtsStanceADMIT: "स्वीकार", - courtsStanceDENY: "अस्वीकार", - courtsStancePARTIAL: "आंशिक", - courtsStanceCOUNTERCLAIM: "प्रतिदावा", - courtsStanceNEUTRAL: "तटस्थ", courtsVerdictResult: "परिणाम", courtsVerdictOrders: "आदेश", courtsSettlementText: "समझौता", courtsSettlementAccepted: "स्वीकृत", - courtsSettlementPending: "लंबित", courtsJudge: "न्यायाधीश", courtsThSupports: "समर्थन", courtsFilterOpenCase: 'मामला खोलें', - courtsEvidenceFileLabel: 'साक्ष्य फ़ाइल (छवि, ऑडियो, वीडियो या PDF)', - courtsCaseMediators: 'मध्यस्थ', courtsCaseMediatorsPh: 'मध्यस्थ Oasis IDs, अल्पविराम से अलग', courtsMediatorsLabel: 'मध्यस्थ', courtsThCase: 'मामला', courtsThCreatedAt: 'प्रारंभ तिथि', - courtsThActions: 'कार्रवाई', courtsPublicPrefLabel: 'समाधान के बाद दृश्यता', courtsPublicPrefYes: 'मैं सहमत हूँ कि यह मामला पूर्ण रूप से सार्वजनिक हो सकता है', courtsPublicPrefNo: 'मैं विवरण निजी रखना पसंद करता हूँ', @@ -1844,8 +1942,11 @@ module.exports = { courtsMethodMEDIATION: 'मध्यस्थता', courtsNoCases: 'कोई मामला नहीं।', reportsTitle: "रिपोर्ट", - reportsDescription: "अपने नेटवर्क में मुद्दों, बग, दुरुपयोग और सामग्री चेतावनियों से संबंधित रिपोर्ट प्रबंधित करें और ट्रैक करें।", + reportsDescription: "अपने नेटवर्क की रिपोर्टों को प्रबंधित करें और उन पर नज़र रखें।", + reportsSearchPlaceholder: "रिपोर्ट में खोजें...", reportsFilterAll: "सभी", + reportsFilterRecent: "हाल के", + reportsFilterTop: "शीर्ष", reportsFilterMine: "मेरे", reportsFilterFeatures: "सुविधाएँ", reportsFilterBugs: "बग", @@ -1858,18 +1959,13 @@ module.exports = { reportsFilterInvalid: "अमान्य", reportsCreateButton: "रिपोर्ट बनाएँ", reportsTitleLabel: "शीर्षक", - reportsDescriptionLabel: "विवरण", - reportsDescriptionPlaceholder: "कृपया विस्तृत विवरण प्रदान करें।", reportsCategory: "श्रेणी", - reportsCategoryLabel: "श्रेणी", reportsCategoryFeatures: "सुविधाएँ", reportsCategoryBugs: "बग", reportsCategoryAbuse: "दुरुपयोग", - reportsCategoryContent: "सामग्री समस्याएँ", + reportsCategoryContent: "सामग्री", reportsUpdateButton: "अपडेट", reportsDeleteButton: "हटाएँ", - reportsDateLabel: "दिनांक", - reportsUploadFile: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", reportsMineSectionTitle: "आपकी रिपोर्ट", reportsFeaturesSectionTitle: "सुविधा अनुरोध", reportsBugsSectionTitle: "बग", @@ -1877,11 +1973,6 @@ module.exports = { reportsContentSectionTitle: "सामग्री समस्याएँ", reportsAllSectionTitle: "रिपोर्ट", reportsNoItems: "कोई रिपोर्ट उपलब्ध नहीं।", - reportsValidationTitle: "कृपया एक वैध शीर्षक दर्ज करें।", - reportsValidationDescription: "विवरण खाली नहीं हो सकता।", - reportsValidationCategory: "कृपया एक श्रेणी चुनें।", - reportsCreatedAt: "बनाया गया", - reportsCreatedBy: "द्वारा", reportsSeverity: "गंभीरता", reportsSeverityLow: "कम", reportsSeverityMedium: "मध्यम", @@ -1892,9 +1983,6 @@ module.exports = { reportsStatusUnderReview: "समीक्षाधीन", reportsStatusResolved: "हल किया गया", reportsStatusInvalid: "अमान्य", - reportsUpdateStatusButton: "स्थिति अपडेट करें", - reportsAnonymityOption: "गुमनाम रूप से जमा करें", - reportsAnonymousAuthor: "गुमनाम", reportsConfirmButton: "रिपोर्ट पुष्टि करें!", reportsConfirmations: "पुष्टि", reportsConfirmedSectionTitle: "पुष्ट रिपोर्ट", @@ -1903,7 +1991,6 @@ module.exports = { reportsUnderReviewSectionTitle: "समीक्षाधीन रिपोर्ट", reportsResolvedSectionTitle: "हल की गई रिपोर्ट", reportsInvalidSectionTitle: "अमान्य रिपोर्ट", - reportsTemplateSectionTitle: 'रिपोर्ट टेम्पलेट', reportsBugTemplateTitle: 'पुनरुत्पादन विवरण (बग)', reportsFeatureTemplateTitle: 'विवरण (सुविधाएँ)', reportsAbuseTemplateTitle: 'विवरण (दुरुपयोग)', @@ -1942,6 +2029,20 @@ module.exports = { reportsRequestedActionLabel: 'अनुरोधित कार्रवाई', reportsRequestedActionPlaceholder: 'हटाएँ, छुपाएँ, टैग करें, चेतावनी दें, आदि।', tribesTitle: "जनजातियाँ", + tribeFilterAll: "सभी", + tribeFilterRecent: "हाल के", + tribeFilterMine: "मेरे", + tribeFilterMembership: "सदस्यता", + tribeFilterSubtribes: "उप-जनजातियाँ", + tribeFilterTop: "शीर्ष", + tribeFilterGallery: "गैलरी", + tribeFeedFilterTOP: "शीर्ष", + tribeFeedFilterMINE: "मेरे", + tribeFeedFilterALL: "सभी", + tribeFeedFilterRECENT: "हाल के", + courtsStanceDENY: "इनकार", + courtsStanceADMIT: "स्वीकार", + courtsStancePARTIAL: "आंशिक स्वीकार", tribeAllSectionTitle: "जनजातियाँ", tribeMineSectionTitle: "आपकी जनजातियाँ", tribeCreateSectionTitle: "जनजाति बनाएँ", @@ -1949,24 +2050,15 @@ module.exports = { tribeGallerySectionTitle: "जनजाति गैलरी", tribeRecentSectionTitle: "हाल की जनजातियाँ", tribeTopSectionTitle: "लोकप्रिय जनजातियाँ", - tribeviewTribeButton: "जनजाति देखें", - tribeviewSubTribeButton: "उप-जनजाति देखें", tribeRootLabel: "मूल", tribeDescription: "अपने नेटवर्क में जनजातियाँ खोजें या बनाएँ।", - tribeFilterAll: "सभी", - tribeFilterMine: "मेरे", - tribeFilterMembership: "सदस्यता", - tribeFilterRecent: "हाल के", - tribeFilterTop: "शीर्ष", - tribeFilterSubtribes: "उप-जनजातियाँ", - tribeFilterGallery: "गैलरी", tribeMainTribeLabel: "मुख्य जनजाति", tribeCreateButton: "जनजाति बनाएँ", tribeUpdateButton: "अपडेट", tribeDeleteButton: "हटाएँ", - tribeImageLabel: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", + tribeImageLabel: "मीडिया अपलोड करें (अधिकतम-आकार: 75MB)", tribeTitleLabel: "शीर्षक", - searchTribesPlaceholder: "नाम से जनजातियाँ फ़िल्टर करें …", + searchTribesPlaceholder: "जनजातियाँ में खोजें...", tribeTitlePlaceholder: "जनजाति का नाम", tribeDescriptionLabel: "विवरण", tribeDescriptionPlaceholder: "इस जनजाति का वर्णन करें", @@ -1978,13 +2070,17 @@ module.exports = { tribeModeLabel: "मोड", tribeIsAnonymousLabel: "स्थिति", tribeMembersCount: "सदस्य", - tribeInviteCodePlaceholder: "आमंत्रण कोड दर्ज करें", - tribeJoinByCodeButton: "कोड से जुड़ें", - tribeJoinButton: "शामिल हों", tribeEnterInvite: "कोड दर्ज करें", + subscriptionTitle: "सदस्यताएँ", + subscriptionOn: "सदस्यता ली गई", + subscriptionOff: "सदस्यता नहीं", + subscriptionSubscribe: "सदस्यता लें", + subscriptionUnsubscribe: "सदस्यता रद्द करें", + subscriptionSubscribers: "सदस्य", + pmListLabel: "भेजें", + pmListNone: "— कोई सूची नहीं —", + pmListNoSubscribers: "कोई सदस्य नहीं", tribeLeaveButton: "छोड़ें", - tribeYes: "हाँ", - tribeNo: "नहीं", tribePublic: "सार्वजनिक", tribePrivate: "निजी", tribeGenerateInvite: "एकल निमंत्रण", @@ -1993,13 +2089,8 @@ module.exports = { tribeRemoveInvitation: "निमंत्रण हटाएँ", tribeCreatedAt: "बनाया गया", tribeAuthor: "द्वारा", - tribeAuthorLabel: "लेखक", tribeStrict: "सख़्त", tribeOpen: "खुला", - tribeFeedFilterRECENT: "हाल के", - tribeFeedFilterMINE: "मेरे", - tribeFeedFilterALL: "सभी", - tribeFeedFilterTOP: "शीर्ष", tribeFeedRefeeds: "रीफ़ीड", tribeFeedRefeed: "रीफ़ीड", tribeFeedMessagePlaceholder: "एक फ़ीड लिखें…", @@ -2009,33 +2100,28 @@ module.exports = { tribeNotFound: "जनजाति नहीं मिली!", createTribeTitle: "जनजाति बनाएँ", updateTribeTitle: "जनजाति अपडेट करें", - tribeSectionOverview: "अवलोकन", tribeSectionInhabitants: "निवासी", tribeSectionVotations: "मतदान", tribeSectionEvents: "कार्यक्रम", - tribeSectionReports: "रिपोर्ट", tribeSectionTasks: "कार्य", tribeSectionFeed: "फ़ीड", tribeSectionForum: "मंच", - tribeSectionMarket: "बाज़ार", - tribeSectionJobs: "नौकरियाँ", - tribeSectionProjects: "परियोजनाएँ", tribeSectionMedia: "मीडिया", tribeSectionImages: "छवियाँ", tribeSectionAudios: "ऑडियो", tribeSectionVideos: "वीडियो", tribeSectionDocuments: "दस्तावेज़", tribeSectionBookmarks: "बुकमार्क", - tribeSectionMaps: "MAPS", - tribeSectionPads: "PADS", - tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionMaps: "मानचित्र", + tribeSectionPads: "पैड्स", + tribeSectionChats: "चैट्स", + tribeSectionCalendars: "कैलेंडर", + tribePadCreate: "पैड बनाएँ", + tribeChatCreate: "चैट बनाएँ", + tribeCalendarCreate: "कैलेंडर बनाएँ", + tribePadsEmpty: "अभी तक कोई पैड नहीं।", + tribeChatsEmpty: "अभी तक कोई चैट नहीं।", + tribeCalendarsEmpty: "अभी तक कोई कैलेंडर नहीं।", tribeInhabitantsEmpty: "इस जनजाति में अभी तक कोई निवासी नहीं।", tribeEventCreate: "कार्यक्रम बनाएँ", tribeEventsEmpty: "अभी तक कोई कार्यक्रम नहीं।", @@ -2057,11 +2143,6 @@ module.exports = { tribeTaskStatusClosed: "बंद करें", tribeTaskAssign: "सौंपें", tribeTaskUnassign: "सौंपना रद्द करें", - tribeReportCreate: "रिपोर्ट बनाएँ", - tribeReportsEmpty: "अभी तक कोई रिपोर्ट नहीं।", - tribeReportTitle: "शीर्षक", - tribeReportDescription: "विवरण", - tribeReportCategory: "श्रेणी", tribeVotationCreate: "मतदान बनाएँ", tribeVotationsEmpty: "अभी तक कोई मतदान नहीं।", tribeVotationTitle: "शीर्षक", @@ -2079,27 +2160,6 @@ module.exports = { tribeForumCategory: "श्रेणी", tribeForumReply: "उत्तर दें", tribeForumReplies: "उत्तर", - tribeMarketCreate: "लिस्टिंग बनाएँ", - tribeMarketEmpty: "अभी तक कोई लिस्टिंग नहीं।", - tribeMarketTitle: "शीर्षक", - tribeMarketDescription: "विवरण", - tribeMarketPrice: "मूल्य", - tribeMarketImage: "छवि", - tribeMarketCategory: "श्रेणी", - tribeJobCreate: "नौकरी बनाएँ", - tribeJobsEmpty: "अभी तक कोई नौकरी नहीं।", - tribeJobTitle: "शीर्षक", - tribeJobDescription: "विवरण", - tribeJobLocation: "स्थान", - tribeJobSalary: "वेतन", - tribeJobDeadline: "समय सीमा", - tribeProjectCreate: "परियोजना बनाएँ", - tribeProjectsEmpty: "अभी तक कोई परियोजना नहीं।", - tribeProjectTitle: "शीर्षक", - tribeProjectDescription: "विवरण", - tribeProjectGoal: "लक्ष्य", - tribeProjectFunded: "वित्तपोषित", - tribeProjectDeadline: "समय सीमा", tribeMediaUpload: "मीडिया अपलोड करें", readDocument: "दस्तावेज़ पढ़ें", tribeCreateImage: "छवि बनाएँ", @@ -2110,21 +2170,20 @@ module.exports = { tribeMediaEmpty: "अभी तक कोई मीडिया नहीं।", tribeMediaTitle: "शीर्षक", tribeMediaDescription: "विवरण", - tribeMediaType: "प्रकार", tribeMediaTypeImage: "छवि", tribeMediaTypeVideo: "वीडियो", tribeMediaTypeAudio: "ऑडियो", tribeMediaTypeDocument: "दस्तावेज़", tribeMediaTypeBookmark: "बुकमार्क", tribeContentDelete: "हटाएँ", + tribeReachTribe: "जनजाति", + tribeReachNotice: "OASIS इसे सभी निवासियों को दिखाता है और CLEARNET खुले वेब पर भी। इसे खोलने से जनजाति के बाहर एक पढ़ने योग्य प्रति रह जाती है: जनजाति में वापस करने से यह छिप जाता है, पर पहले साझा की गई प्रतियाँ मिटाई नहीं जा सकतीं।", + tribeReachPrivate: "निजी जनजाति की सामग्री केवल जनजाति के भीतर ही रह सकती है।", + tribeReachOpenedBy: "खोलने वाले", + tribeReachChange: "कहाँ प्रकाशित है", tribeInviteCodeText: "आमंत्रण कोड: ", oasisVersionLabel: "Oasis संस्करण", - tribeInviteCodeHint: "इस कोड को साझा करें जिसे आप आमंत्रित करना चाहते हैं। वे /invites → Tribes से शामिल हो सकते हैं।", - tribeGroupTribe: "जनजाति", - tribeGroupOffice: "कार्यालय", - tribeGroupNetwork: "नेटवर्क", - tribeGroupEconomy: "अर्थव्यवस्था", - tribeGroupMedia: "मीडिया", + tribeInviteCodeHint: "इस कोड को साझा करें जिसे आप आमंत्रित करना चाहते हैं। वे /invites से शामिल हो सकते हैं।", tribeStatusOpen: "खुला", tribeStatusClosed: "बंद", tribeStatusInProgress: "प्रगति में", @@ -2134,33 +2193,17 @@ module.exports = { tribePriorityCritical: "गंभीर", tribeTaskFilterAll: "सभी", tribeMediaFilterAll: "सभी", - tribeReportCatBug: "बग", - tribeReportCatAbuse: "दुरुपयोग", - tribeReportCatContent: "सामग्री", - tribeReportCatOther: "अन्य", tribeForumCatGeneral: "सामान्य", tribeForumCatProposal: "प्रस्ताव", tribeForumCatQuestion: "प्रश्न", tribeForumCatAnnouncement: "घोषणा", - tribeMarketCatGoods: "सामान", - tribeMarketCatServices: "सेवाएँ", - tribeMarketCatFood: "भोजन", - tribeMarketCatOther: "अन्य", tribeStatusLabel: "स्थिति", tribeSubTribes: "उप-जनजातियाँ", tribeSubTribesCreate: "उप-जनजाति बनाएँ", tribeSubTribesStrictDenied: "जनजाति का सख्त मोड आपको नई उप-जनजातियाँ बनाने की अनुमति नहीं देता। कृपया प्रशासक से संपर्क करें।", - tribeSubTribesEmpty: "अभी तक कोई उप-जनजाति नहीं बनाई गई।", - tribeActivityJoined: "शामिल हुए", - tribeActivityLeft: "छोड़ दिया", - tribeActivityFeed: "फ़ीड", tribeActivityRefeed: "रीफ़ीड", - tribeGroupAnalytics: "विश्लेषिकी", - tribeGroupCreative: "रचनात्मक", tribeSectionActivity: "गतिविधि", tribeSectionTrending: "ट्रेंडिंग", - tribeSectionOpinions: "राय", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "टैग", tribeSectionSearch: "खोज", tribeActivityEmpty: "अभी तक कोई गतिविधि नहीं।", @@ -2172,25 +2215,13 @@ module.exports = { tribeTrendingPeriodWeek: "इस सप्ताह", tribeTrendingPeriodAll: "सभी समय", tribeTrendingEngagement: "सहभागिता", - tribeOpinionsEmpty: "अभी तक कोई राय नहीं।", - tribeOpinionsCast: "मत दें", - tribeOpinionsRankings: "रैंकिंग", - tribeOpinionsAlreadyVoted: "पहले ही मतदान किया", tribeTopCategory: "अधिक मतदान", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "सहयोगी पिक्सेल कला कैनवास।", - tribePixeliaPaint: "रंग भरें", - tribePixeliaContributors: "योगदानकर्ता", - tribePixeliaTotalPixels: "रंगे गए पिक्सेल", tribeTagsEmpty: "कोई टैग नहीं मिला।", - tribeTagsCloud: "टैग क्लाउड", - tribeTagsContentWith: "टैग वाली सामग्री", - tribeSearchPlaceholder: "जनजाति सामग्री खोजें...", tribeSearchEmpty: "कोई परिणाम नहीं मिला।", - tribeSearchResults: "परिणाम", tribeSearchMinChars: "खोजने के लिए कम से कम 2 अक्षर दर्ज करें।", agendaTitle: "एजेंडा", agendaDescription: "यहाँ आप अपने सभी सौंपे गए आइटम पा सकते हैं।", + agendaSearchPlaceholder: "एजेंडा में खोजें...", agendaFilterAll: "सभी", agendaFilterToday: "आज", agendaFilterUpcoming: "आगामी", @@ -2204,23 +2235,15 @@ module.exports = { agendaFilterReports: "रिपोर्ट", agendaFilterTransfers: "स्थानांतरण", agendaFilterJobs: "नौकरियाँ", + agendaFilterIndustry: "उद्योग", + agendaFilterHousing: "आवास", agendaFilterProjects: "परियोजनाएँ", agendaFilterCalendars: "कैलेंडर", agendaNoItems: "कोई सौंपे गए आइटम नहीं मिले।", - agendaAuthor: "द्वारा", agendaDiscardButton: "निरस्त करें", agendaRestoreButton: "पुनर्स्थापित करें", - agendaCreatedAt: "बनाया गया", - agendaTitleLabel: "शीर्षक", + agendaRemoveButton: "हटाएँ", agendaMembersCount: "सदस्य", - agendaDescriptionLabel: "विवरण", - agendaStatus: "स्थिति", - agendaVisibility: "दृश्यता", - agendaEventDate: "कार्यक्रम तिथि", - agendaEventLocation: "स्थान", - agendaEventPrice: "मूल्य", - agendaEventUrl: "URL", - agendaTaskStart: "प्रारंभ समय", agendaLocationLabel: "स्थान", agendaYes: "हाँ", agendaNo: "नहीं", @@ -2230,11 +2253,6 @@ module.exports = { agendareportCategory: "श्रेणी", agendareportSeverity: "गंभीरता", agendareportStatus: "स्थिति", - agendareportDescription: "विवरण", - agendaTaskEnd: "समाप्ति समय", - agendaTaskPriority: "प्राथमिकता", - agendaTransferFrom: "से", - agendaTransferTo: "को", agendaTransferConcept: "अवधारणा", agendaTransferAmount: "राशि", agendaTransferDeadline: "समय सीमा", @@ -2244,47 +2262,7 @@ module.exports = { voteNow: "अभी मतदान करें", alreadyVoted: "आपने पहले ही राय दे दी है।", noOpinionsFound: "कोई राय नहीं मिली।", - RECENTButton: "हाल के", TOPButton: "शीर्ष", - interestingButton: "दिलचस्प", - necessaryButton: "आवश्यक", - funnyButton: "मज़ेदार", - disgustingButton: "घृणित", - sensibleButton: "समझदार", - propagandaButton: "प्रचार", - adultOnlyButton: "केवल वयस्क", - boringButton: "उबाऊ", - confusingButton: "भ्रमित करने वाला", - usefulButton: "उपयोगी", - informativeButton: "सूचनाप्रद", - wellResearchedButton: "अच्छी तरह शोधित", - accurateButton: "सटीक", - needsSourcesButton: "स्रोत चाहिए", - wrongButton: "गलत", - lowQualityButton: "निम्न गुणवत्ता", - creativeButton: "रचनात्मक", - insightfulButton: "अंतर्दृष्टिपूर्ण", - actionableButton: "कार्रवाई योग्य", - inspiringButton: "प्रेरणादायक", - loveButton: "प्यार", - clearButton: "स्पष्ट", - upliftingButton: "उत्थानकारी", - unnecessaryButton: "अनावश्यक", - rejectedButton: "अस्वीकृत", - misleadingButton: "भ्रामक", - offTopicButton: "विषय से हटकर", - duplicateButton: "डुप्लिकेट", - clickbaitButton: "क्लिकबेट", - spamButton: "स्पैम", - trollButton: "ट्रोल", - nsfwButton: "NSFW", - violentButton: "हिंसक", - toxicButton: "विषाक्त", - harassmentButton: "उत्पीड़न", - hateButton: "घृणा", - scamButton: "धोखाधड़ी", - triggeringButton: "उत्तेजक", - opinionsCreatedAt: "बनाया गया", opinionsTotalCount: "कुल राय", voteInteresting: "दिलचस्प", voteNecessary: "आवश्यक", @@ -2321,29 +2299,41 @@ module.exports = { voteCreative: "रचनात्मक", voteSpam: "स्पैम", voteAdultOnly: "केवल वयस्क", - publishBlog: "ब्लॉग प्रकाशित करें", privateMessage: "PM", pmSendTitle: "निजी संदेश", pmSend: "भेजें!", - pmDescription: "अन्य निवासियों को एन्क्रिप्टेड संदेश भेजने के लिए इस फ़ॉर्म का उपयोग करें।", + pmCancel: "रद्द करें", + pmReset: "रीसेट", + pmSharedKeyHint: "इस कुंजी को प्राप्तकर्ता के साथ किसी अन्य माध्यम से साझा करें। डिक्रिप्ट करने के लिए यह आवश्यक है।", + pmDescription: "अन्य निवासियों को एन्क्रिप्टेड संदेश/फ़ाइल भेजने के लिए इस फ़ॉर्म का उपयोग करें।", + pmComposeTitle: "संदेश भेजें", pmRecipients: "प्राप्तकर्ता", pmRecipientsHint: "अल्पविराम से अलग Oasis IDs दर्ज करें", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "प्रति संदेश अधिकतम 7 निवासी।", + pmTextPlaceholder: "अपना संदेश लिखें।", pmSubject: "विषय", pmSubjectHint: "संदेश विषय दर्ज करें", pmText: "संदेश", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "डिक्रिप्ट करें", + fileShareTitle: "फ़ाइल साझा करें", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "फ़ाइल", + fileShareDownload: "डाउनलोड करें", + fileShareMutualError: "आप केवल पारस्परिक समर्थन वाले निवासियों के साथ फ़ाइलें साझा कर सकते हैं।", + fileShareNoFile: "कोई फ़ाइल चयनित नहीं।", + fileShareTooLarge: "फ़ाइल अनुमत आकार से बड़ी है।", + fileShareFailed: "फ़ाइल तैयार नहीं की जा सकी।", + fileShareSendError: "फ़ाइल भेजी नहीं जा सकी।", + fileShareUnavailable: "यह फ़ाइल अभी उपलब्ध नहीं है। बाद में पुनः प्रयास करें।", pmCrypterBadKey: "साझा कुंजी गलत है!", pmCrypterTooLong: "यह संदेश Crypter से एन्क्रिप्ट करने के लिए बहुत लंबा है। कृपया इसे छोटा करके पुनः प्रयास करें।", pmCrypterCipherLabel: "पुनः एन्क्रिप्टेड संदेश", pmCrypterCharsLabel: "अक्षर", pmInvalidRecipients: "अमान्य Oasis ID (यह @…=.ed25519 जैसा होना चाहिए)।", pmEncryptionLabel: "एन्क्रिप्शन", - pmFile: "संलग्नक", - private: "निजी", - privateDescription: "आपके एन्क्रिप्टेड संदेश।", + private: "इनबॉक्स", + privateDescription: "अपने निजी संदेश प्रबंधित करें।", privateInbox: "इनबॉक्स", privateSent: "भेजे गए", privateDelete: "हटाएँ", @@ -2351,27 +2341,34 @@ module.exports = { noPrivateMessages: "कोई निजी संदेश नहीं।", pmFromLabel: "से:", pmToLabel: "को:", - pmInvalidMessage: "अमान्य संदेश", pmNoSubject: "(कोई विषय नहीं)", pmSubjectLabel: "विषय:", - pmBodyLabel: "मुख्य भाग", - pmBotJobs: "42-JobsBOT", - pmBotProjects: "42-ProjectsBOT", - pmBotMarket: "42-MarketBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "L.A.R.P का शासक घराना बदल गया है।", + politicalBotParliamentTitle: "संसद में एक नया शासन चक्र शुरू हो गया है।", + politicalBotTribeTitle: "आपकी किसी जनजाति में एक नया शासन चक्र शुरू हो गया है।", + politicalBotLarpIntro: "अब इस चक्र में {house} शासन करता है: {cycle}", + politicalBotParliamentIntro: "वर्तमान सरकार {gov} है", + politicalBotParliamentIntroLeader: "वर्तमान सरकार {gov} है, {leader} द्वारा संचालित", + politicalBotTribeIntro: "जनजाति {tribe} में वर्तमान सरकार {gov} है", + politicalBotTribeIntroLeader: "जनजाति {tribe} में वर्तमान सरकार {gov} है, {leader} द्वारा संचालित", + politicalBotLeaderLabel: "नेता", inboxJobSubscribedTitle: "आपके नौकरी प्रस्ताव के लिए नई सदस्यता", pmInhabitantWithId: "OASIS ID वाला निवासी:", pmHasSubscribedToYourJobOffer: "ने आपके नौकरी प्रस्ताव की सदस्यता ली है", - inboxProjectCreatedTitle: "नई परियोजना बनाई गई", - pmHasCreatedAProject: "ने एक परियोजना बनाई है", inboxMarketItemSoldTitle: "आइटम बिक गया", + inboxShopSoldTitle: "उत्पाद बिका", pmYourItem: "आपका आइटम", pmHasBeenSoldTo: "बेचा गया", pmFor: "के लिए", inboxProjectPledgedTitle: "आपकी परियोजना के लिए नई प्रतिज्ञा", pmHasPledged: "ने प्रतिज्ञा की है", pmToYourProject: "आपकी परियोजना को", - blockchain: 'BlockExplorer', - blockchainTitle: 'BlockExplorer', + blockchain: "ब्लॉक एक्सप्लोरर", blockchainDescription: 'ब्लॉकचेन में ब्लॉक खोजें और विज़ुअलाइज़ करें।', blockchainNoBlocks: 'ब्लॉकचेन में कोई ब्लॉक नहीं मिला।', blockchainStatsTitle: 'आँकड़े', @@ -2383,42 +2380,36 @@ module.exports = { blockchainBlockCarbon: 'कार्बन फुटप्रिंट', blockchainBlockType: 'प्रकार', blockchainBlockTimestamp: 'टाइमस्टैम्प', - blockchainBlockContent: 'ब्लॉक', - blockchainBlockURL: 'URL:', - blockchainContent: 'ब्लॉक', - blockchainContentPreview: 'ब्लॉक सामग्री का पूर्वावलोकन', blockchainLatestDatagram: 'नवीनतम डेटाग्राम', - blockchainDatagram: 'डेटाग्राम', - blockchainDetails: 'ब्लॉक विवरण देखें', - blockchainViewBlockexplorer: "ब्लॉकएक्सप्लोरर में देखें", - blockchainBlockInfo: 'ब्लॉक जानकारी', - blockchainBlockDetails: 'चयनित ब्लॉक का विवरण', + blockchainDatagram: "डेटाग्राम", + blockchainDetails: "ब्लॉक विवरण देखें", + blockchainViewBlockexplorer: "Blockexplorer में देखें", blockchainBack: 'BlockExplorer पर वापस जाएँ', blockchainContentDeleted: "यह सामग्री टॉम्बस्टोन कर दी गई है", visitContent: "सामग्री देखें", + favoriteAdd: "पसंदीदा में पिन करें", + pmContentTooltip: "निजी संदेश भेजें", + phoneCallTooltip: "फ़ोन कॉल करें", + favoriteRemove: "पसंदीदा से हटाएँ", + reportContent: "सामग्री की रिपोर्ट करें", banking: 'बैंकिंग', bankingTitle: 'बैंकिंग', - bankingDescription: 'ECOin का वर्तमान मूल्य और संबंधित UBI आवंटन खोजें, जो भागीदारी और विश्वास के आधार पर प्रति युग वितरित होता है।', + bankingDescription: "ECOin की सामूहिक अर्थव्यवस्था।", bankOverview: 'अवलोकन', bankEpochs: 'युग', bankRules: 'नियम', pending: 'लंबित', closed: 'बंद', - bankBack: 'बैंकिंग पर वापस जाएँ', - bankViewTx: 'Tx देखें', bankClaimNow: 'अभी दावा करें', - bankClaimUBI: 'UBI का दावा करें!', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'इस अवधि के लिए कोई लंबित UBI आवंटन नहीं।', + bankClaimUBI: 'UBI का दावा करें', + bankClaimAndPay: "दावा करें और भुगतान करें", + bankClaimedPending: "दावा लंबित...", + bankStatusClaimed: "दावा किया गया", + bankStatusExpired: "समाप्त", + bankPubOnly: "केवल PUB के लिए कार्य", bankEpoch: 'युग', bankPool: 'पूल (यह युग)', bankWeightsSum: 'भारों का योग', - bankAllocations: 'आवंटन', bankNoAllocations: 'कोई आवंटन नहीं मिला।', bankNoEpochs: 'कोई युग नहीं मिला।', bankEpochAllocations: 'युग आवंटन', @@ -2432,16 +2423,21 @@ module.exports = { bankEpochId: 'युग ID', bankRuleHash: 'नियम स्नैपशॉट हैश', bankViewEpoch: 'युग देखें', + bankYourIndustryBalance: "आपका औद्योगिक हिस्सा", + bankYourUbiMonth: "आपकी UBI (इस माह)", + bankYourFundsMonth: "कुल निधि", + bankIndustryBalance: "औद्योगिक उत्पादन", bankUserBalance: 'आपका शेष', - ecoWalletNotConfigured: 'ECOin वॉलेट कॉन्फ़िगर नहीं है', - editWallet: 'वॉलेट संपादित करें', - addWallet: 'वॉलेट जोड़ें', bankAddresses: 'पते', bankNoAddresses: 'कोई पता नहीं मिला।', bankUser: 'Oasis ID', bankAddress: 'पता', bankAddAddressTitle: 'ECOIN पता जोड़ें', bankAddAddressUser: 'Oasis ID', + bankAddAddressLabel: "लेबल (वैकल्पिक)", + bankAddAddressLabelPlaceholder: "बॉब का वॉलेट, ऐलिस की दुकान…", + bankAddAddressUserOptional: "Oasis ID (वैकल्पिक)", + bankAddressUnnamed: "बिना नाम का पता", bankAddAddressAddress: 'ECOIN पता', bankAddAddressSave: 'सहेजें', bankAddressAdded: 'पता जोड़ा गया', @@ -2459,19 +2455,11 @@ module.exports = { bankAddressDeleteConfirm: 'यह पता हटाएँ?', search: 'खोजें!', bankLocal: 'स्थानीय', - bankFromOasis: 'Oasis', - bankMyAddress: 'आपका पता', - bankRemoveMyAddress: 'मेरा पता हटाएँ', - bankNotRemovableOasis: 'पते स्थानीय रूप से नहीं हटाए जा सकते', - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "नेटवर्क", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "कोई धन नहीं!", bankUbiAvailableOk: "उपलब्ध!", - bankUbiAvailability: "UBI उपलब्धता", + bankUbiAvailability: "UBI", bankAlreadyClaimedThisMonth: "इस महीने पहले ही दावा किया गया", bankUbiThisMonth: "UBI (इस महीने)", bankUbiLastClaimed: "UBI (अंतिम दावा)", @@ -2485,13 +2473,6 @@ module.exports = { shopsTitle: "दुकानें", shopDescription: "नेटवर्क में दुकानें खोजें और प्रबंधित करें।", shopTitle: "दुकान", - shopFilterAll: "सभी", - shopFilterMine: "मेरी", - shopFilterRecent: "हालिया", - shopFilterTop: "शीर्ष", - shopFilterProducts: "उत्पाद", - shopFilterPrices: "मूल्य", - shopFilterFavorites: "पसंदीदा", shopUpload: "दुकान बनाएं", shopAllSectionTitle: "सभी दुकानें", shopMineSectionTitle: "मेरी दुकानें", @@ -2508,19 +2489,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Clearnet पर प्रकाशित करें", - shopClearnetUnpublish: "Clearnet से हटाएं", - shopClearnetUrlLabel: "Clearnet URL", - shopClearnetWarning: "Clearnet पर प्रकाशित करने से यह दुकान बाहरी सर्च इंजन और क्रॉलर द्वारा अनुक्रमणीय हो जाती है।", + transportChipLabel: "ट्रांज़िट में एन्क्रिप्टेड", shopAddFavorite: "पसंदीदा जोड़ें", shopRemoveFavorite: "पसंदीदा हटाएं", shopOpen: "खुली", shopClosed: "बंद", - shopOpenShop: "दुकान खोलें", - shopCloseShop: "दुकान बंद करें", shopMakePrivate: "निजी बनाएं (E2E)", shopMakePublic: "सार्वजनिक बनाएं", shopProducts: "उत्पाद", + shopFilterAll: "सभी", + shopFilterRecent: "हाल के", + shopFilterMine: "मेरे", + shopFilterTop: "टॉप", + shopFilterProducts: "उत्पाद", + shopFilterPrices: "मूल्य", + shopFilterFavorites: "पसंदीदा", shopProductAdd: "उत्पाद जोड़ें", shopProductTitle: "उत्पाद", shopProductUpdate: "उत्पाद अपडेट", @@ -2545,85 +2528,63 @@ module.exports = { shopOrderPrice: "मूल्य", shopOrderBuyer: "खरीदार", shopBackToShop: "दुकान पर वापस", - shopShareUrl: "शेयर URL", - shopVisitShop: "दुकान देखें", marketVisitItem: "आइटम देखें", shopStatus: "स्थिति", shopCreatedAt: "बनाया गया", - shopSearchPlaceholder: "दुकानें खोजें...", + shopSearchPlaceholder: "दुकानें में खोजें...", shopUrl: "URL", shopLocation: "स्थान", shopTags: "टैग", shopVisibility: "दृश्यता", - shopImage: "छवि", shopShortDescription: "संक्षिप्त विवरण", shopShortDescriptionPlaceholder: "दुकान कार्ड के लिए संक्षिप्त विवरण (अधिकतम 160 अक्षर)", shopTitlePlaceholder: "आपकी दुकान का नाम", shopDescriptionPlaceholder: "आपकी दुकान का विस्तृत विवरण", - shopUrlPlaceholder: "https://aapki-dukan-url.com", shopLocationPlaceholder: "शहर, देश", - shopTagsPlaceholder: "टैग1, टैग2, टैग3", + shopTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", mapTitlePlaceholder: "मानचित्र शीर्षक", shopProductFeatured: "विशेष उत्पाद", - shopSendToMarket: "Send to Market", + shopSendToMarket: "बाज़ार में भेजें", typeShop: "दुकानें", typeShopProduct: "दुकान उत्पाद", bankExchange: 'विनिमय', bankExchangeCurrentValue: 'ECOin मूल्य (1h)', - bankTotalSupply: 'ECOin कुल आपूर्ति', - bankEcoinHours: "ECOin समय समतुल्यता", + bankTotalSupply: "ECOin कुल आपूर्ति", + bankPubsSupply: "PUBs पूल की ECOin आपूर्ति", + bankHoldingSupply: "UBI से बाहर ECOin आपूर्ति", + bankEcoinHours: "ECOin समय समतुल्यता (1 ECO)", bankHoursOfWork: 'घंटे', bankUnitMs: "मि.से.", bankUnitSeconds: "सेकंड", bankUnitMinutes: "मिनट", bankUnitDays: "दिन", bankExchangeNoData: 'कोई डेटा उपलब्ध नहीं', - bankExchangeIndex: 'ECOin मूल्य (1h)', - bankInflation: 'ECOin मुद्रास्फीति (anual)', - bankInflationMonthly: 'ECOin मुद्रास्फीति (mensual)', + bankInflation: "ECOin मुद्रास्फीति (वार्षिक)", + bankInflationMonthly: "ECOin मुद्रास्फीति (मासिक)", bankExchangeChartTitle: 'समय के साथ ECOin मूल्य की प्रगति', bankExchangeChartValue: 'मूल्य (ECO/घंटा)', bankExchangeChartSupply: 'आपूर्ति', bankExchangeChartInflation: 'मुद्रास्फीति %', bankExchangeChartEmpty: 'अभी पर्याप्त नमूने नहीं — बाद में लौटें', - bankCurrentSupply: 'ECOin वर्तमान आपूर्ति', + bankCurrentSupply: "ECOin वर्तमान आपूर्ति", bankingSyncStatus: 'ECOin स्थिति', bankingSyncStatusSynced: 'सिंक किया गया', - bankingSyncStatusOutdated: 'पुराना', + bankingSyncStatusOutdated: "असिंक", statsTitle: 'सांख्यिकी', statistics: "सांख्यिकी", - statsInhabitant: "निवासी सांख्यिकी", statsDescription: "अपने नेटवर्क के बारे में सांख्यिकी खोजें।", ALLButton: "सभी", + RECENTButton: "हाल के", MINEButton: "मेरे", TOMBSTONEButton: "टॉम्बस्टोन", - statsYou: "आप", - statsUserId: "Oasis ID", statsCreatedAt: "बनाया गया", statsYourContent: "सामग्री", statsYourOpinions: "राय", - statsYourTombstone: "टॉम्बस्टोन", - statsNetwork: "नेटवर्क", - statsTotalInhabitants: "निवासी", - statsDiscoveredTribes: "जनजातियाँ (सार्वजनिक)", - statsPrivateDiscoveredTribes: "जनजातियाँ (निजी)", statsNetworkContent: "सामग्री", - statsYourMarket: "बाज़ार", - statsYourJob: "नौकरियाँ", - statsYourProject: "परियोजनाएँ", - statsYourTransfer: "स्थानांतरण", - statsYourForum: "मंच", statsNetworkOpinions: "राय", - statsDiscoveredMarket: "बाज़ार", - statsDiscoveredJob: "नौकरियाँ", - statsDiscoveredProject: "परियोजनाएँ", - statsBankingTitle: "बैंकिंग", - statsEcoWalletLabel: "ECOIN वॉलेट", + statsEcoWalletLabel: "ECOin वॉलेट", + walletAddressCopyHint: "चुनने के लिए क्लिक करें, फिर कॉपी करें", statsEcoWalletNotConfigured: "कॉन्फ़िगर नहीं!", - statsTotalEcoAddresses: "कुल पते", - statsDiscoveredTransfer: "स्थानांतरण", - statsDiscoveredForum: "मंच", - statsNetworkTombstone: "टॉम्बस्टोन", statsBookmark: "बुकमार्क", statsEvent: "कार्यक्रम", statsTask: "कार्य", @@ -2644,17 +2605,15 @@ module.exports = { statsTransfer: "स्थानांतरण", statsAiExchange: "AI", statsPUBs: 'PUBs', + statsSyncedPeers: "पीयर", + statsTotalLabel: "कुल", statsPost: "पोस्ट", - statsOasisID: "Oasis ID", statsSize: "कुल (आकार)", statsBlockchainSize: "ब्लॉकचेन (आकार)", statsBlobsSize: "ब्लॉब (आकार)", statsActivity7d: "गतिविधि (पिछले 7 दिन)", statsActivity7dTotal: "7-दिन कुल", statsActivity30dTotal: "30-दिन कुल", - statsKarmaScore: "कर्म स्कोर", - statsPublic: "सार्वजनिक", - statsPrivate: "निजी", day: "दिन", messages: "संदेश", statsProject: "परियोजनाएँ", @@ -2666,30 +2625,14 @@ module.exports = { statsProjectsCancelled: "रद्द", statsProjectsGoalTotal: "कुल लक्ष्य", statsProjectsPledgedTotal: "कुल प्रतिज्ञा", - statsProjectsSuccessRate: "सफलता दर", - statsProjectsAvgProgress: "औसत प्रगति", - statsProjectsMedianProgress: "मध्यांश प्रगति", - statsProjectsActiveFundingAvg: "औसत सक्रिय वित्तपोषण", - statsJobsTitle: "नौकरियाँ", - statsJobsTotal: "कुल नौकरियाँ", - statsJobsOpen: "खुली", - statsJobsClosed: "बंद", - statsJobsOpenVacants: "खुली रिक्तियाँ", - statsJobsSubscribersTotal: "कुल सदस्य", - statsJobsAvgSalary: "औसत वेतन", - statsJobsMedianSalary: "मध्यांश वेतन", statsMarketTitle: "बाज़ार", statsMarketTotal: "कुल आइटम", statsMarketForSale: "बिक्री के लिए", statsMarketReserved: "आरक्षित", statsMarketClosed: "बंद", statsMarketSold: "बेचा गया", - statsMarketRevenue: "राजस्व", - statsMarketAvgSoldPrice: "औसत बिक्री मूल्य", statsUsersTitle: "निवासी", user: "निवासी", - statsTombstoneTitle: "टॉम्बस्टोन", - statsNetworkTombstones: "नेटवर्क टॉम्बस्टोन", statsTombstoneRatio: "टॉम्बस्टोन अनुपात (%)", statsParliamentCandidature: "संसद उम्मीदवारी", statsParliamentTerm: "संसद कार्यकाल", @@ -2715,31 +2658,28 @@ module.exports = { aiPrompt: "एक सूचनाप्रद और सटीक प्रतिक्रिया प्रदान करें।", aiConfiguration: "प्रॉम्प्ट सेट करें", aiPromptUsed: "प्रॉम्प्ट", - aiClearHistory: "चैट इतिहास साफ़ करें", - aiSharePrompt: "यह उत्तर सामूहिक प्रशिक्षण में जोड़ें?", - aiShareYes: "हाँ", - aiShareNo: "नहीं", - aiSharedLabel: "प्रशिक्षण में जोड़ा गया", - aiRejectedLabel: "प्रशिक्षण में नहीं जोड़ा गया", - aiServerError: "AI उत्तर नहीं दे सका। कृपया पुनः प्रयास करें।", + aiClearHistory: "इतिहास साफ़ करें", + aiServerError: "मैं ठीक से उत्तर नहीं दे पा रहा हूँ। कृपया फिर से प्रयास करें।", + aiStatusLoading: "42 मॉडल लोड कर रहा है। थोड़ी देर में फिर पूछें।", + aiThinking: "सोच रहा है", + aiWaiting: "प्रतीक्षा में", + aiStatusMissingModel: "AI मॉडल स्थापित नहीं है: src/AI/oasis-42-1-chat.Q4_K_M.gguf नहीं मिला।", + aiExportFineTuning: "Fine-Tuning निर्यात करें", + aiFactsTitle: "आपके नेटवर्क का डेटा", aiInputPlaceholder: "Oasis क्या है?", typeAiExchange: "AI", aiApproveTrain: "सामूहिक प्रशिक्षण में जोड़ें", aiRejectTrain: "प्रशिक्षण न करें", - aiTrainPending: "अनुमोदन लंबित", aiTrainApproved: "प्रशिक्षण के लिए अनुमोदित", aiTrainRejected: "प्रशिक्षण के लिए अस्वीकृत", aiSnippetsUsed: "उपयोग किए गए अंश", aiSnippetsLearned: "सीखे गए अंश", statsAITraining: "AI प्रशिक्षण", - aiApproveCustomTrain: "इस कस्टम उत्तर का उपयोग करके प्रशिक्षित करें", aiCustomAnswerPlaceholder: "अपना कस्टम उत्तर लिखें…", - statsAIExchanges: "मॉडल आदान-प्रदान", marketMineSectionTitle: "आपके आइटम", marketCreateSectionTitle: "आइटम बनाएँ", marketUpdateSectionTitle: "अपडेट", marketAllSectionTitle: "बाज़ार", - marketRecentSectionTitle: "हाल का बाज़ार", marketTitle: "बाज़ार", marketDescription: "अपने नेटवर्क में सामान या सेवाओं के आदान-प्रदान के लिए एक बाज़ार।", marketFilterAll: "सभी", @@ -2754,7 +2694,7 @@ module.exports = { marketFilterDiscarded: "निरस्त", marketFilterRecent: "हाल के", marketFilterMyBids: "बोलियाँ", - marketCreateButton: "आइटम बनाएँ", + marketCreateButton: "कुछ बेचें", marketItemType: "प्रकार", marketItemTitle: "शीर्षक", marketItemAvailable: "समय सीमा", @@ -2769,14 +2709,11 @@ module.exports = { marketItemDeadline: "समय सीमा", marketItemIncludesShipping: "शिपिंग शामिल है?", marketItemHighestBid: "सबसे ऊँची बोली", - marketItemHighestBidder: "सबसे ऊँची बोली लगाने वाला", marketItemStock: "स्टॉक", marketOutOfStock: "स्टॉक में नहीं", - marketItemBidTime: "बोली समय", marketActionsUpdate: "अपडेट", marketUpdateButton: "अपडेट", marketActionsDelete: "हटाएँ", - marketActionsSold: "बिका हुआ चिह्नित करें", marketActionsChangeStatus: "स्थिति बदलें", marketActionsBuy: "खरीदें!", marketAuctionBids: "वर्तमान बोलियाँ", @@ -2784,20 +2721,82 @@ module.exports = { marketItemSeller: "विक्रेता", marketNoItems: "अभी तक कोई आइटम उपलब्ध नहीं।", marketYourBid: "आपकी बोली", - marketCreateFormImageLabel: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", - marketSearchLabel: "खोज", - marketSearchPlaceholder: "शीर्षक या टैग खोजें", + marketCreateFormImageLabel: "मीडिया अपलोड करें (अधिकतम-आकार: 75MB)", + marketSearchPlaceholder: "बाज़ार में खोजें...", marketMinPriceLabel: "न्यूनतम मूल्य", marketMaxPriceLabel: "अधिकतम मूल्य", - marketSortLabel: "क्रमबद्ध करें", marketSortRecent: "सबसे हाल का", marketSortPrice: "मूल्य", marketSortDeadline: "समय सीमा", marketSearchButton: "खोजें", marketAuctionEndsIn: "समाप्त होता है", marketAuctionEnded: "समाप्त हो गया", - marketMyBidBadge: "आपकी बोली", marketNoItemsMatch: "आपकी खोज से कोई आइटम मेल नहीं खाता।", + housingTitle: "आवास", + housingDescriptionText: "अपने नेटवर्क में जगहें खोजें और प्रबंधित करें।", + modulesHousingLabel: "आवास", + modulesHousingDescription: "जगहें खोजने और प्रबंधित करने का मॉड्यूल।", + typeHousing: "आवास", + housingFilterAll: "सभी", + housingFilterMine: "मेरे", + housingFilterRequested: "अनुरोधित", + housingFilterSale: "बिक्री", + housingFilterRent: "किराया", + housingFilterCouchsurfing: "काउचसर्फिंग", + housingFilterOpen: "उपलब्ध", + housingFilterClosed: "अनुपलब्ध", + housingFilterRecent: "हाल के", + housingFilterTop: "शीर्ष", + housingType: "प्रस्ताव", + housingTypeSALE: "बिक्री के लिए", + housingTypeRENT: "किराये के लिए", + housingTypeCOUCHSURFING: "काउचसर्फिंग", + housingProperty: "संपत्ति", + housingPropertyAPARTMENT: "अपार्टमेंट", + housingPropertyHOUSE: "घर", + housingPropertyROOM: "कमरा", + housingPropertyLAND: "ज़मीन", + housingPropertyOTHER: "अन्य", + housingTitleLabel: "शीर्षक", + housingTitlePlaceholder: "जगह का नाम", + housingDescription: "विवरण", + housingDescriptionPlaceholder: "जगह, आस-पास और क्या शामिल है बताएं: बाथरूम, बगीचा, बिस्तर…", + housingRules: "नियम", + housingRulesPlaceholder: "पालतू जानवर, धूम्रपान, शांति के घंटे, न्यूनतम ठहराव…", + housingPlace: "जगह", + housingPlacePlaceholder: "देश, शहर, पता", + housingPrice: "कीमत", + housingPriceLabel: "कीमत (ECO)", + housingSizeLabel: "क्षेत्रफल (m²)", + housingFree: "मुफ़्त", + housingPerMonth: "/माह", + housingRooms: "कमरे", + housingSize: "क्षेत्रफल", + housingCapacity: "मेहमान", + housingAvailableFrom: "से उपलब्ध", + housingAvailableTo: "तक उपलब्ध", + housingTags: "टैग", + spreadEditWarning: "संपादित करने पर यह सामग्री अपने स्प्रेड खो देगी", + housingRequests: "अनुरोध", + housingRequestButton: "अनुरोध करें", + housingCancelButton: "अनुरोध रद्द करें", + housingRequestedBadge: "अनुरोधित", + housingStatus: "स्थिति", + housingStatusOPEN: "उपलब्ध", + housingStatusCLOSED: "अनुपलब्ध", + housingSetOpen: "उपलब्ध करें", + housingSetClosed: "अनुपलब्ध करें", + housingCreateButton: "जगह बनाएं", + housingUpdateButton: "अपडेट", + housingSearchPlaceholder: "आवास में खोजें...", + housingSearchButton: "खोजें", + housingMinPrice: "न्यूनतम कीमत", + housingMaxPrice: "अधिकतम कीमत", + housingSortRecent: "सबसे नए", + housingSortPrice: "सबसे कम कीमत", + housingSortRequests: "सबसे ज़्यादा अनुरोध", + housingSortRating: "सर्वश्रेष्ठ रेटेड", + housingNoItems: "कोई जगह नहीं मिली।", jobsTitle: "नौकरियाँ", jobsCandidatesTitle: "सुझाए गए उम्मीदवार", jobsCandidatesDescription: "ऐसे निवासी जिनके कौशल इस नौकरी से मेल खाते हैं। उन्हें आमंत्रित करने के लिए निजी संदेश भेजें।", @@ -2845,8 +2844,7 @@ module.exports = { jobLocationPresencial: "स्थानीय", jobLocationRemote: "दूरस्थ", jobVacantsPlaceholder: "पदों की संख्या", - jobSalaryPlaceholder: "1 समर्पित घंटे के लिए ECO में वेतन", - jobImage: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", + jobImage: "मीडिया अपलोड करें (अधिकतम-आकार: 75MB)", jobTasks: "कार्य", jobType: "नौकरी प्रकार", jobTime: "नौकरी समय", @@ -2855,7 +2853,6 @@ module.exports = { jobsFilterTop: "शीर्ष", jobsTopTitle: "शीर्ष वेतन नौकरियाँ", createJobButton: "नौकरी प्रकाशित करें", - viewDetailsButton: "विवरण देखें", noJobsFound: "कोई नौकरी प्रस्ताव नहीं मिला।", jobAuthor: "द्वारा", jobTypeFreelance: "फ्रीलांसर", @@ -2867,42 +2864,208 @@ module.exports = { jobsHoursOffered: "दिए गए घंटे", jobsHoursRequested: "बदले में माँगे गए घंटे", jobsExchangeSkill: "बदले में चाहिए कौशल", - jobsHoursOfferedPlaceholder: "जैसे 4", - jobsHoursRequestedPlaceholder: "जैसे 4", - jobsExchangeSkillPlaceholder: "जैसे बढ़ईगिरी, डिज़ाइन", - jobExchangeHint: "घंटों के आदान-प्रदान वाली नौकरियों के लिए, वेतन के बजाय नीचे के फ़ील्ड भरें।", jobTimePartial: "अंशकालिक", jobTimeComplete: "पूर्णकालिक", - jobsDeleteButton: "हटाएँ", jobsUpdateButton: "अपडेट", jobsFilterApplied: "आवेदित", jobsAppliedTitle: "मेरे आवेदन", jobsAppliedBadge: "आवेदित", - jobsFilterFavs: "पसंदीदा", - jobsFavsTitle: "पसंदीदा", - jobsFilterNeeds: "मदद चाहिए", - jobsNeedsTitle: "मदद चाहिए", - jobsSearchLabel: "खोज", - jobsSearchPlaceholder: "शीर्षक, टैग, विवरण खोजें...", + jobsSearchPlaceholder: "नौकरियाँ में खोजें...", jobsMinSalaryLabel: "न्यूनतम वेतन", jobsMaxSalaryLabel: "अधिकतम वेतन", - jobsSortLabel: "क्रमबद्ध करें", jobsSortRecent: "सबसे हाल का", jobsSortSalary: "सबसे अधिक वेतन", jobsSortSubscribers: "सबसे अधिक आवेदक", jobsSearchButton: "खोजें", - jobsFavoriteButton: "पसंदीदा", - jobsUnfavoriteButton: "पसंदीदा हटाएँ", - jobsMessageAuthorButton: "PM", jobsApplicants: "आवेदक", jobsUpdatedAt: "अपडेट किया गया", jobSetOpen: "खुली सेट करें", - jobNewBadge: "नया", jobsTagsLabel: "टैग", - jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "आपकी खोज से कोई नौकरी मेल नहीं खाती।", + industrySearchPlaceholder: "उद्योग में खोजें...", + industryAllSectors: "सभी क्षेत्र", + industryVisitButton: "उद्योग देखें", + industryAdvanceTo: "पर सेट करें", + industryAmount: "राशि", + industryApproveBuild: "मंज़ूर करें", + industryBlueprint: "ब्लूप्रिंट", + industryBlueprints: "ब्लूप्रिंट", + industryBuild: "बिल्ड", + industryBuildNotes: "नोट्स", + industryBuildStart: "आरंभ तिथि", + industryVoteUpdateButton: "अपडेट के लिए वोट", + industryVoteDeleteButton: "हटाने के लिए वोट", + industryRevokeVote: "वापस लें", + industryTimeLeft: "शेष समय", + industryBuildEnd: "समाप्ति तिथि", + industryBuilds: "बिल्ड्स", + industryBuildTitle: "शीर्षक", + industryContribute: "योगदान करें", + industryContributeButton: "योगदान करें", + industryContributions: "योगदान", + industryCreateBlueprintButton: "ब्लूप्रिंट बनाएं", + industryDistribute: "वितरण करें", + industryDistributeButton: "हिस्सों के अनुसार वितरण", + industryDistribution: "वितरण", + industryEcoAmount: "राशि (ECO)", + industryEditBlueprint: "ब्लूप्रिंट संपादित करें", + industryFacility: "फैक्टरी", + industryKindLabel: "प्रकार", + industryKind_labor: "श्रम", + industryKind_material: "सामग्री", + industryKind_eco: "निधि", + industryLaborHours: "श्रम (घंटे)", + industryHours: "घंटे", + industryMaterialItem: "सामग्री", + industryMaterials: "सामग्री", + industryEstMaterials: "कुल सामग्री", + industryEstLabor: "कुल श्रम", + industryEstTotal: "अनुमानित मूल्य", + industryBuildingPrice: "निर्माण मूल्य", + industryFinalPrice: "अंतिम मूल्य", + industryBlueprintDescPlaceholder: "विनिर्देश, आयाम, वज़न, दस्तावेज़…", + industryMaterialValue: "मूल्य (ECO)", + industryMember: "सदस्य", + industryNewBlueprint: "नया ब्लूप्रिंट", + industryNewBuild: "बिल्ड प्रस्तावित करें", + industryEditBuild: "उत्पादन संपादित करें", + industryNeedBlueprint: "पहले ब्लूप्रिंट बनाएँ: हर उत्पादन उसी को बनाता है।", + industryNoBlueprints: "अभी तक कोई ब्लूप्रिंट नहीं।", + industryNoBuilds: "अभी तक कोई बिल्ड नहीं।", + industryNote: "नोट", + industryOutput: "आउटपुट", + industryOutputKind: "उत्पाद का प्रकार", + industryOutputValue: "आउटपुट मूल्य (ECO, जैसे बिक्री से)", + industryPostJob: "नौकरी बनाएँ", + industryPot: "कोष", + industryProposeBuildButton: "बिल्ड प्रस्तावित करें", + industryProposer: "प्रस्तावक", + industryAuthor: "लेखक", + industrySellOnMarket: "बाज़ार में भेजें", + industrySendToProjects: "प्रोजेक्ट बनाएँ", + industrySkills: "कौशल", + industryTreasury: "कोष", + industryKind_physical: "भौतिक", + industryKind_digital: "डिजिटल", + industryBuildStatus_PROPOSED: "प्रस्तावित", + industryBuildStatus_REJECTED: "अस्वीकृत", + industryBuildStatus_APPROVED: "स्वीकृत", + industryBuildStatus_STOCKING: "भंडारण", + industryBuildStatus_IN_PRODUCTION: "उत्पादन में", + industryBuildStatus_COMPLETED: "पूर्ण", + industryBuildStatus_FAILED: "विफल", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "एक नौकरी आपके CV से मेल खाती है", + jobsBotMatchIntro: "एक नौकरी आपके CV से मेल खाती है।", + jobsBotMatchJob: "नौकरी", + jobsBotMatchHint: "यह इसलिए मिला क्योंकि आपका CV AI द्वारा प्रबंधित है। आप इसे CV में बंद कर सकते हैं।", + cvAiManaged: "AI प्रबंधित", + switchOn: "चालू", + switchOff: "बंद", + cvAiManagedHint: "चालू होने पर, JobsBot निजी संदेश से बताता है जब कोई नौकरी आपके CV से मेल खाती है।", + cvMatchThreshold: "इस मेल स्तर से सूचित करें", + housingBotRequestedTitle: "किसी ने आपकी एक जगह का अनुरोध किया है।", + housingBotCancelledTitle: "आपकी एक जगह का अनुरोध रद्द कर दिया गया है।", + housingBotYouRequestedTitle: "आपने एक जगह का अनुरोध किया।", + housingBotYouCancelledTitle: "आपने एक अनुरोध रद्द किया।", + housingBotUnavailableTitle: "आपके द्वारा अनुरोधित जगह अब उपलब्ध नहीं है।", + housingBotUnavailableSentTitle: "आपने अनुरोध करने वालों को सूचित कर दिया।", + industryBotAdmittedTitle: "आपको एक फैक्टरी में स्वीकार किया गया है।", + industryBotApplicationTitle: "आपकी फैक्टरी में नया सदस्यता अनुरोध।", + industryBotInvitedTitle: "आपको एक फैक्टरी में आमंत्रित किया गया है।", + industryBotDissolvedTitle: "आपकी एक फैक्टरी विघटित कर दी गई है।", + industryBotBuildApprovedTitle: "एक बिल्ड स्वीकृत हो गया है।", + industryBotDistributedTitle: "एक बिल्ड का आउटपुट वितरित किया गया है।", + industryFilterRules: "नियम", + industryRulesTitle: "नियम", + industryRulesIntro: "Industry निवासियों को उत्पादन के साधनों का सामूहिक स्वामित्व देता है: एक फैक्टरी नेटवर्क-स्वामित्व वाली कार्यशाला है जिसे कोई निजी रूप से नहीं रखता।", + industryRulesSteward: "संस्थापक प्रबंधक होता है — संरक्षक, मालिक नहीं: वह फैक्टरी को अकेले बेच, निजीकृत या विघटित नहीं कर सकता।", + industryRulesMembership: "सदस्यता नीति तय करती है कि कैसे जुड़ें: खुला (तुरंत), मतदान द्वारा (सदस्यों के वोट से स्वीकृत) या केवल आमंत्रण (पहले किसी सदस्य को आमंत्रित करना होगा)।", + industryRulesQuorum: "कोरम वह न्यूनतम सदस्य संख्या है जिन्हें किसी निर्णय के गिनने के लिए मतदान करना चाहिए, ताकि छोटी फैक्टरी एक निवासी द्वारा तय न हो।", + industryRulesMajority: "बहुमत पारित होने के लिए आवश्यक मतों का अंश है (0.5 = आधा, 1 = सर्वसम्मति); निर्णय तब पारित होता है जब हाँ वोट कम से कम max(कोरम, सदस्य × बहुमत) तक पहुँचें।", + industryRulesDecisions: "सदस्य नए लोगों को स्वीकारने, बिल्ड स्वीकृत करने और फैक्टरी विघटित करने के लिए मतदान करते हैं; प्रबंधक इन सामूहिक निर्णयों को नहीं टाल सकता।", + industryRulesBlueprints: "ब्लूप्रिंट स्वतंत्र (COPYLEFT) व्यंजन हैं — इनपुट (सामग्री, श्रम घंटे, कौशल) और आउटपुट (एक भौतिक या डिजिटल वस्तु) — और कोई भी इन्हें फोर्क कर सकता है।", + industryRulesBuilds: "बिल्ड एक उत्पादन आदेश है: प्रस्तावित → स्वीकृत (मतदान से) → भंडारण → उत्पादन में → पूर्ण, और स्वीकृत होने के बाद ही योगदान स्वीकार करता है।", + industryRulesContributions: "सदस्य किसी बिल्ड में श्रम (घंटे), सामग्री (घोषित ECO मूल्य) या ECO का योगदान करते हैं; हर योगदान Karma अर्जित करता है।", + industryRulesLaborRate: "श्रम दर इस फैक्टरी में एक कार्य-घंटे का ECO मूल्य है; यह घंटों को Karma में बदलती है ताकि श्रम और पूँजी की निष्पक्ष तुलना हो: Karma = घंटे × दर + सामग्री मूल्य + ECO।", + industryRulesShares: "किसी बिल्ड में आपका हिस्सा आपका Karma ÷ कुल Karma है, और यह तय करता है कि आपको आउटपुट मूल्य का कितना भाग मिलेगा।", + industryRulesDistribution: "उत्पादन पूरा होने पर स्टीवर्ड पॉट (उत्पादन की निधि और बिक्री मूल्य) को योगदानकर्ताओं में उनके हिस्से के अनुपात में बाँटता है।", + industryRulesTreasury: "ECO योगदान वितरण तक प्रबंधक द्वारा बिल्ड कोष के संरक्षक के रूप में रखे जाते हैं; सभी लेन-देन नेटवर्क पर दर्ज होते हैं और विवाद Courts में जा सकते हैं।", + industryJobs: "नौकरियाँ", + industryReportButton: "रिपोर्ट", + industryCreateTask: "कार्य बनाएँ", + industryOpenDispute: "विवाद खोलें", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "आमंत्रण उत्पन्न करें", + industryPauseVotes: "स्थिति मत", + industryTitle: "उद्योग", + industryDescription: "उत्पादन के साधनों को सामूहिक रूप से प्रबंधित करने का मॉड्यूल।", + typeIndustryBuild: "बिल्ड", + typeIndustryBlueprint: "ब्लूप्रिंट", + typeIndustryAllocation: "वितरण", + typeIndustry: "उद्योग", + modulesIndustryLabel: "उद्योग", + modulesIndustryDescription: "उत्पादन के साधनों को सामूहिक रूप से प्रबंधित करने का मॉड्यूल।", + industryFilterAll: "सभी", + industryFilterRecent: "हाल के", + industryFilterMember: "सदस्य", + industryFilterBlueprints: "ब्लूप्रिंट", + industryFilterBuilds: "उत्पादन", + industryFilterMine: "मेरी", + industryFilterActive: "चालू", + industryFilterPaused: "रोकी गई", + industryFilterDissolved: "विघटित", + industryAllTitle: "उद्योग", + industryRecentTitle: "हाल की फैक्टरियाँ", + industryMemberTitle: "जिन फैक्टरियों में मैं हूँ", + industryMineTitle: "मेरी फैक्टरियाँ", + industryActiveTitle: "चालू", + industryPausedTitle: "रोकी गई फैक्टरियाँ", + industryDissolvedTitle: "विघटित फैक्टरियाँ", + industryNoFacilitiesFound: "कोई फैक्टरी नहीं मिली।", + industryCreateFacility: "फैक्टरी बनाएं", + industryMembers: "सदस्य", + industryMemberBadge: "सदस्य", + industryName: "नाम", + industryNamePlaceholder: "फैक्टरी का नाम", + industryDescriptionLabel: "विवरण", + industryDescriptionPlaceholder: "यह फैक्टरी क्या उत्पादित करती है?", + industrySector: "क्षेत्र", + industryMembershipPolicy: "सदस्यता नीति", + industryQuorum: "कोरम", + industryMajority: "बहुमत", + industryLaborRate: "श्रम दर", + industryCreateButton: "फैक्टरी बनाएं", + industryUpdateButton: "अपडेट", + industrySteward: "प्रबंधक", + industryStatusActive: "चालू", + industryStatusPaused: "रोका गया", + industryStatusDissolved: "विघटित", + industryStatusLabel: "स्थिति", + industryJoinButton: "शामिल हों", + industryApplyButton: "शामिल होने का अनुरोध", + industryLeaveButton: "छोड़ें", + industryInviteButton: "आमंत्रित करें", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "शामिल होने के लिए आपको आमंत्रण चाहिए।", + industryPauseButton: "रोकने के लिए मतदान", + industryResumeButton: "फिर से शुरू करने के लिए मतदान", + industryDeleteButton: "हटाएं", + industryPendingApplicants: "लंबित आवेदक", + industryVotesYes: "हाँ", + industryVotesNo: "नहीं", + industryAlreadyVoted: "मतदान किया", + industryAdmit: "स्वीकार करें", + industryReject: "अस्वीकार करें", + industryDissolveTitle: "फैक्टरी विघटित करें", + industryDissolveVote: "विघटन के लिए मतदान", + industryPolicy_open: "खुला", + industryPolicy_vote: "मतदान से", + industryPolicy_invite: "केवल आमंत्रण", projectsTitle: "परियोजनाएँ", projectsDescription: "अपने नेटवर्क में समुदाय-संचालित परियोजनाएँ बनाएँ, वित्तपोषित करें और अनुसरण करें।", + projectSearchPlaceholder: "परियोजनाएँ में खोजें...", projectCreateProject: "परियोजना बनाएँ", projectCreateButton: "परियोजना बनाएँ", projectUpdateButton: "अपडेट", @@ -2925,7 +3088,7 @@ module.exports = { projectRecentTitle: "हाल की परियोजनाएँ", projectTopTitle: "शीर्ष वित्तपोषित", projectTitlePlaceholder: "परियोजना का नाम", - projectImage: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", + projectImage: "मीडिया अपलोड करें (अधिकतम-आकार: 75MB)", projectDescription: "विवरण", projectDescriptionPlaceholder: "कहानी और लक्ष्य बताएँ…", projectGoal: "लक्ष्य (ECO)", @@ -2946,14 +3109,8 @@ module.exports = { projectPledgeTitle: "इस परियोजना का समर्थन करें", projectPledgePlaceholder: "ECO में राशि", projectBounties: "इनाम", - projectBountiesInputLabel: "इनाम (प्रति पंक्ति एक: शीर्षक|राशि [ECO]|विवरण)", - projectBountiesPlaceholder: "UI बग ठीक करें|100|समस्या का लिंक\nडॉक्स लिखें|250|उपयोग उदाहरण रूपरेखा", projectNoBounties: "कोई इनाम नहीं मिला।", projectTitle: "शीर्षक", - projectAddBountyTitle: "नया इनाम", - projectBountyTitle: "इनाम शीर्षक", - projectBountyAmount: "राशि (ECO)", - projectBountyDescription: "विवरण", projectMilestoneSelect: "मील का पत्थर चुनें", projectBountyCreateButton: "इनाम बनाएँ", projectBountyStatus: "इनाम स्थिति", @@ -2968,19 +3125,15 @@ module.exports = { projectMilestoneTitle: "मील का पत्थर शीर्षक", projectMilestoneTargetPercent: "प्रतिशत (%)", projectMilestoneDueDate: "दिनांक", - projectMilestoneCreateButton: "मील का पत्थर बनाएँ", projectMilestoneStatus: "मील का पत्थर स्थिति", projectMilestoneOpen: "खुला", projectMilestoneDone: "पूर्ण", projectMilestoneDue: "देय", - projectNoMilestones: "कोई मील का पत्थर नहीं मिला।", projectMilestoneMarkDone: "पूर्ण चिह्नित करें", projectMilestoneTitlePlaceholder: "मील का पत्थर शीर्षक दर्ज करें", projectMilestoneDescriptionPlaceholder: "इस मील के पत्थर का विवरण दर्ज करें", projectMilestoneDescription: "मील का पत्थर विवरण", - projectBudgetGoal: "बजट (लक्ष्य)", projectBudgetAssigned: "इनामों को सौंपा गया", - projectBudgetRemaining: "शेष", projectBudgetOver: "बजट से अधिक: सौंपा गया लक्ष्य से अधिक है", projectFollowers: "अनुसरणकर्ता", projectFollowersTitle: "अनुसरणकर्ता", @@ -2993,7 +3146,6 @@ module.exports = { projectBackersTotalPledged: "कुल प्रतिज्ञा", projectBackersYourPledge: "आपकी प्रतिज्ञा", projectBackersNone: "अभी तक कोई प्रतिज्ञा नहीं।", - projectNoRemainingBudget: "कोई शेष बजट नहीं।", projectFilterBackers: "समर्थक", projectFilterApplied: "आवेदित", projectAppliedTitle: "आवेदित", @@ -3002,36 +3154,19 @@ module.exports = { projectBackerAmount: "कुल योगदान", projectBackerPledges: "प्रतिज्ञाएँ", projectBackerProjects: "परियोजनाएँ", - projectPledgeAmount: "राशि", projectSelectMilestoneOrBounty: "मील का पत्थर या इनाम चुनें", projectPledgeButton: "प्रतिज्ञा करें", footerLicense: "GPLv3", - footerPackage: "पैकेज", - footerVersion: "संस्करण", modulesModuleName: "नाम", modulesModuleDescription: "विवरण", modulesModuleStatus: "स्थिति", - modulesTotalModulesLabel: "लोड किए गए मॉड्यूल", + modulesTotalModulesLabel: "कुल", modulesEnabledModulesLabel: "सक्षम", modulesDisabledModulesLabel: "अक्षम", - modulesPopularLabel: "लोकप्रिय", - modulesPopularDescription: "ट्रेंडिंग, सबसे अधिक देखी गई या सबसे अधिक टिप्पणी की गई पोस्ट प्राप्त करने का मॉड्यूल।", - modulesTopicsLabel: "विषय", - modulesTopicsDescription: "साझा रुचियों पर आधारित चर्चा श्रेणियाँ प्राप्त करने का मॉड्यूल।", - modulesSummariesLabel: "सारांश", - modulesSummariesDescription: "लंबी चर्चाओं या पोस्ट के सारांश प्राप्त करने का मॉड्यूल।", - modulesLatestLabel: "नवीनतम", - modulesLatestDescription: "सबसे हाल की पोस्ट और चर्चाएँ प्राप्त करने का मॉड्यूल।", - modulesThreadsLabel: "थ्रेड्स", - modulesThreadsDescription: "विषय या प्रश्न के अनुसार समूहीकृत बातचीत प्राप्त करने का मॉड्यूल।", - modulesMultiverseLabel: "मल्टीवर्स", - modulesMultiverseDescription: "अन्य संघीय पीयर से सामग्री प्राप्त करने का मॉड्यूल।", modulesInvitesLabel: "आमंत्रण", modulesInvitesDescription: "आमंत्रण कोड प्रबंधित और लागू करने का मॉड्यूल।", modulesWalletLabel: "वॉलेट", modulesWalletDescription: "अपनी डिजिटल संपत्तियाँ (ECOin) प्रबंधित करने का मॉड्यूल।", - modulesLegacyLabel: "विरासत", - modulesLegacyDescription: "अपनी गुप्त कुंजी (निजी कुंजी) को जल्दी और सुरक्षित रूप से प्रबंधित करने का मॉड्यूल।", modulesCipherLabel: "सिफर", modulesCipherDescription: "अपने टेक्स्ट को सममित रूप से एन्क्रिप्ट और डिक्रिप्ट करने का मॉड्यूल (साझा पासवर्ड का उपयोग करके)।", modulesBookmarksLabel: "बुकमार्क", @@ -3066,8 +3201,12 @@ module.exports = { modulesOpinionsDescription: "राय खोजने और मतदान करने का मॉड्यूल।", modulesTransfersLabel: "स्थानांतरण", modulesTransfersDescription: "स्मार्ट-कॉन्ट्रैक्ट (स्थानांतरण) खोजने और प्रबंधित करने का मॉड्यूल।", - modulesFediverseLabel: "फ़ेडिवर्स", - modulesFediverseDescription: "अपने अन्य फ़ेडिवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।", + modulesFediverseLabel: "मल्टीवर्स", + modulesBlogsLabel: "ब्लॉग", + modulesBlogsDescription: "ब्लॉग खोजने और प्रबंधित करने का मॉड्यूल।", + modulesPollsLabel: "सर्वेक्षण", + modulesPollsDescription: "नेटवर्क से पूछने और उत्तर गिनने का मॉड्यूल।", + modulesFediverseDescription: "अपने अन्य मल्टीवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।", modulesFeedLabel: "फ़ीड", modulesFeedDescription: "लघु-पाठ (फ़ीड) खोजने और साझा करने का मॉड्यूल।", modulesParliamentLabel: "संसद", @@ -3089,52 +3228,51 @@ module.exports = { modulesProjectsLabel: "परियोजनाएँ", modulesProjectsDescription: "परियोजनाएँ खोजने, क्राउड-फंडिंग करने और प्रबंधित करने का मॉड्यूल।", modulesBankingLabel: "बैंकिंग", - modulesBankingDescription: "ECOIN का वास्तविक मूल्य निर्धारित करने और सामान्य कोषागार का उपयोग करके UBI वितरित करने का मॉड्यूल।", + modulesBankingDescription: "साझा कोष प्रबंधन मॉड्यूल: ECOIN मूल्य, कर और सार्वभौमिक बुनियादी आय।", modulesFavoritesLabel: "पसंदीदा", modulesFavoritesDescription: "अपनी पसंदीदा सामग्री प्रबंधित करने का मॉड्यूल।", fileTooLargeTitle: "फ़ाइल बहुत बड़ी", - fileTooLargeMessage: "फ़ाइल अधिकतम अनुमत आकार (50 MB) से अधिक है। कृपया एक छोटी फ़ाइल चुनें।", + fileTooLargeMessage: "फ़ाइल अधिकतम अनुमत आकार (75 MB) से अधिक है। कृपया एक छोटी फ़ाइल चुनें।", goBack: "वापस जाएँ", + errorDismiss: "ठीक", errorPageTitle: "कुछ गलत हो गया", + errorNoAllocation: "इस दावे के लिए कोई आवंटन नहीं मिला।", + errorInvalidClaim: "इस आवंटन का दावा नहीं किया जा सकता।", visibilityLabel: "दृश्यता", + clearnetPrivateConflict: "निजी, छिपी, बंद, केवल आमंत्रण वाली या सशुल्क सामग्री CLEARNET पर प्रकाशित नहीं की जा सकती। OASIS चुनें या इसे सार्वजनिक करें।", visibilityPublic: "सार्वजनिक", visibilityHidden: "छिपा हुआ", visibilityMakePublic: "सार्वजनिक करें", visibilityMakeHidden: "छिपाएँ", invitesInhabitantsTitle: "निवासी", - invitesInhabitantsFollow: "फॉलो करें", - aiNavPlaceholder: "आप कहाँ जाना चाहते हैं?", + aiNavPlaceholder: "बताएँ आपको क्या चाहिए...", aiNavDisabled: "AI नेविगेशन अक्षम है।", - aiNavResultsTitle: "AI नेविगेशन सुझाव", + aiNavResultsTitle: "AINav सुझाव", aiNavQueryLabel: "प्रश्न", - aiNavResultsDescription: "अपने अनुरूप मार्ग चुनें।", - aiNavResultPath: "मार्ग", - aiNavResultDescription: "क्या किया जा सकता है", aiNavResultMatch: "मेल", aiNavResultsEmpty: "कोई मेल खाता मार्ग नहीं। /search आज़माएँ।", - aiNavSearchInstead: "इसके बजाय खोजें", modulesAINavLabel: "AINav", modulesAINavDescription: "नेटवर्क की सामग्री पर प्राकृतिक भाषा में प्रश्न करने का मॉड्यूल।", - directConnect: "सीधा कनेक्शन", directConnectDescription: "किसी पीयर से सीधे कनेक्ट करने के लिए उनका IP पता, पोर्ट और सार्वजनिक कुंजी दर्ज करें। पीयर को एक अनुसरित कनेक्शन के रूप में जोड़ा जाएगा।", peerHost: "IP", - peerPort: "पोर्ट (डिफ़ॉल्ट: 8008)", + peerPort: "पोर्ट", peerPublicKey: "सार्वजनिक कुंजी (@...ed25519)", connectAndFollow: "कनेक्ट करें", - deviceSourceLabel: "डिवाइस स्रोत", modulesPresetTitle: "सामान्य कॉन्फ़िगरेशन", - modulesPreset_minimal: "न्यूनतम", - modulesPreset_basic: "बुनियादी", - modulesPreset_social: "सामाजिक", - modulesPreset_economy: "अर्थव्यवस्था", - modulesPreset_full: "पूर्ण", + workflowsTitle: "वर्कफ़्लो", + workflowsDescription: "वर्कफ़्लो थीम और सक्रिय मॉड्यूल तय करता है।", + workflowsSet: "वर्कफ़्लो लागू करें", + workflow_default: "डिफ़ॉल्ट", + workflow_jobs: "रोज़गार", + workflow_social: "सामाजिक", + workflow_business: "व्यापार", + workflow_night: "रात", + workflow_kids: "बच्चे", + workflow_activists: "कार्यकर्ता", + workflow_mobile: "मोबाइल", statsCarbonFootprintTitle: "कार्बन फुटप्रिंट", - statsCarbonFootprintNetwork: "नेटवर्क कार्बन फुटप्रिंट", - statsCarbonFootprintYours: "आपका कार्बन फुटप्रिंट", statsCarbonTombstone: "टॉम्बस्टोनिंग फुटप्रिंट", - feedSuccessMsg: "फ़ीड सफलतापूर्वक प्रकाशित!", - dominantOpinionLabel: "प्रमुख राय", - uploadMedia: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", + uploadMedia: "मीडिया अपलोड करें (अधिकतम-आकार: 75MB)", feedOpenDiscussion: "चर्चा खोलें", feedDetailTitle: "फ़ीड", feedPostComment: "टिप्पणी पोस्ट करें", @@ -3142,7 +3280,7 @@ module.exports = { noComments: "अभी तक कोई टिप्पणी नहीं", mapsLabel: "मानचित्र", mapTitle: "मानचित्र", - mapDescription: "अपने नेटवर्क में ऑफ़लाइन मानचित्र खोजें और प्रबंधित करें।", + mapDescription: "अपने नेटवर्क में ऑफ़लाइन मानचित्र प्रबंधित और साझा करें।", mapMineSectionTitle: "आपके मानचित्र", mapCreateSectionTitle: "मानचित्र बनाएं", mapUpdateSectionTitle: "मानचित्र अपडेट करें", @@ -3166,14 +3304,13 @@ module.exports = { mapDescriptionLabel: "विवरण", mapDescriptionPlaceholder: "मानचित्र या स्थान का वर्णन करें...", mapTypeLabel: "मानचित्र प्रकार", - mapTypeSingle: "एकल (केवल प्रारंभिक स्थान)", - mapTypeOpen: "खुला (कोई भी मार्कर जोड़ सकता है)", - mapTypeClosed: "बंद (केवल निर्माता मार्कर जोड़ता है)", + mapInviteMode: "आमंत्रण", + mapTypeSingle: "एकल", + mapTypeOpen: "खुला", + mapTypeClosed: "बंद", mapTagsLabel: "टैग", - mapTagsPlaceholder: "अल्पविराम से अलग टैग दर्ज करें", + mapTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", mapUrlLabel: "मानचित्र URL", - mapPickCoordLabel: "या ग्रिड पर स्थान चुनें:", - mapMarkersLabel: "मार्कर", mapMarkersTitle: "मार्कर", mapMarkerDefault: "मार्कर", mapMarkerLatLabel: "मार्कर अक्षांश", @@ -3183,9 +3320,22 @@ module.exports = { markerImageLabel: "मार्कर छवि", mapAddMarkerTitle: "मार्कर जोड़ें", mapAddMarkerButton: "मार्कर जोड़ें", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "मार्कर साफ़ करें", mapApplyZoom: "ज़ूम लागू करें", - mapSearchPlaceholder: "विवरण, टैग, लेखक खोजें...", + mapPlaceSearchPlaceholder: "कोई स्थान या देश खोजें...", + mapNoPlaces: "कोई स्थान नहीं मिला।", + mapClusterTitle: "मार्कर यहाँ, ज़ूम करने के लिए क्लिक करें", + mapDeleteMarker: "मार्कर हटाएँ", + mapZoomIn: "ज़ूम इन", + mapZoomOut: "ज़ूम आउट", + mapFitMarkers: "सभी मार्कर दिखाएँ", + mapPanLabel: "मानचित्र हिलाएँ", + mapCenterHere: "यहाँ केंद्रित करें", + mapNear: "के पास", + mapPickedSpot: "चुना गया स्थान", + mapPickHint: "स्थान चुनने के लिए मानचित्र पर क्लिक करें।", + mapClearPick: "साफ़ करें", + mapSearchPlaceholder: "मानचित्र में खोजें...", mapSearchButton: "खोजें", mapUpdatedAt: "अपडेट किया गया", mapNoMatch: "आपकी खोज से मेल खाने वाला कोई मानचित्र नहीं।", @@ -3196,118 +3346,820 @@ module.exports = { typeMapMarker: "मानचित्र मार्कर", modulesMapLabel: "मानचित्र", modulesMapDescription: "ऑफ़लाइन मानचित्रों को प्रबंधित और साझा करने का मॉड्यूल।", + emergenciesTitle: "आपातकाल", + modulesEmergenciesLabel: "आपातकाल", + modulesEmergenciesDescription: "नेटवर्क में आपातकाल घोषित करने, पुष्टि करने और फ़ॉलो करने का मॉड्यूल।", + emergenciesDescription: "नेटवर्क में आपातकाल घोषित करने, पुष्टि करने और फ़ॉलो करने का मॉड्यूल।", + emergencyCreate: "आपातकाल बनाएँ", + emergencyUpdate: "अपडेट", + emergencyDelete: "हटाएँ", + emergencyTitleLabel: "शीर्षक", + emergencyTitlePlaceholder: "क्या हो रहा है?", + emergencyTextLabel: "विवरण", + emergencyTextPlaceholder: "कहाँ, कब से, क्या करें...", + emergencyCategoryLabel: "श्रेणी", + emergencyCategoryWeather: "मौसम", + emergencyCategoryInfrastructure: "बुनियादी ढाँचा", + emergencyCategoryHealth: "स्वास्थ्य", + emergencyCategorySecurity: "सुरक्षा", + emergencyCategoryLost: "खोया-पाया", + emergencyCategoryNeighborhood: "पड़ोस", + emergencyExpiresLabel: "समाप्ति", + emergencyExpires1d: "1 दिन", + emergencyExpires3d: "3 दिन", + emergencyExpires7d: "1 सप्ताह", + emergencyExpires30d: "1 महीना", + emergencyExpiresNone: "अनिश्चित", + emergencyExpiresKeep: "वर्तमान तारीख रखें", + emergencyTagsLabel: "टैग", + emergencyTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + emergencyFilterAll: "सभी", + emergencyFilterMine: "मेरे", + emergencyFilterRecent: "हाल के", + emergencySeverityUnverified: "असत्यापित", + emergencySeverityLow: "कम", + emergencySeverityMedium: "मध्यम", + emergencySeverityHigh: "उच्च", + emergencyStatusActive: "सक्रिय", + emergencyStatusResolved: "हल हो गया", + emergencyStatusExpired: "समाप्त", + emergencyConfirmations: "पुष्टियाँ", + emergencyConfirmButton: "पुष्टि करें", + emergencyConfirmed: "आपने इस आपातकाल की पुष्टि की", + emergencyResolveButton: "हल करें", + emergencyReopenButton: "फिर से खोलें", + emergencyGenerateReport: "रिपोर्ट बनाएँ", + emergencyUpdateEdited: "संपादित", + emergencyLastUpdate: "अंतिम अपडेट", + emergencyUpdatesTitle: "अंतिम अपडेट:", + emergencyNoUpdates: "अभी कोई अपडेट नहीं।", + emergencyAddUpdate: "अपडेट पोस्ट करें", + emergencyUpdatePlaceholder: "क्या बदला?", + emergencyNoItems: "कोई आपातकाल नहीं मिला।", + emergencyExpiresAt: "समाप्ति", + emergencyBannerLabel: "सक्रिय आपातकाल", + emergencyCreateSectionTitle: "आपातकाल बनाएँ", + emergencyUpdateSectionTitle: "आपातकाल अपडेट करें", + emergencySearchPlaceholder: "आपातकाल में खोजें...", + typeEmergency: "आपातकाल", + typeEmergencyConfirm: "आपातकाल पुष्टि", + typeEmergencyUpdate: "आपातकाल अपडेट", + statsEmergency: "आपातकाल", + statsEmergencyConfirm: "आपातकाल पुष्टियाँ", + statsEmergencyUpdate: "आपातकाल अपडेट", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "आपके द्वारा फ़ॉलो किए गए आपातकाल में अपडेट है।", + emergencyBotResolvedTitle: "आपके द्वारा फ़ॉलो किया गया आपातकाल हल हो गया है।", + dataKindEmergencies: "आपातकाल", + mailingTitle: "मेलिंग सूचियाँ", + modulesMailingLabel: "मेलिंग सूचियाँ", + modulesMailingDescription: "अपने नेटवर्क में मेलिंग सूचियाँ खोजें और प्रबंधित करें।", + modulesPhoneLabel: "फ़ोन", + modulesPhoneDescription: "अन्य निवासियों को एंड-टू-एंड एन्क्रिप्टेड वीडियो कॉल करने का मॉड्यूल।", + mailingDescription: "अपने नेटवर्क में मेलिंग सूचियाँ खोजें और प्रबंधित करें।", + mailingCreate: "मेलिंग सूची बनाएँ", + mailingUpdate: "अपडेट करें", + mailingArchive: "संग्रहित करें", + mailingActivate: "सक्रिय करें", + mailingCreateSectionTitle: "मेलिंग सूची बनाएँ", + mailingUpdateSectionTitle: "मेलिंग सूची अपडेट करें", + mailingTitleLabel: "शीर्षक", + mailingTitlePlaceholder: "मेलिंग सूची का नाम", + mailingDescriptionLabel: "विवरण", + mailingDescriptionPlaceholder: "यह सूची किस बारे में है?", + mailingTypeLabel: "मेलिंग सूची का प्रकार", + mailingTypeOpen: "खुली", + mailingTypeClosed: "बंद", + mailingStatusLabel: "स्थिति", + mailingStatusActive: "सक्रिय", + mailingStatusArchived: "संग्रहित", + mailingMembersLabel: "निवासियों को आमंत्रित करें", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "सदस्य", + mailingTagsLabel: "टैग", + mailingTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + mailingFilterAll: "सभी", + mailingFilterMine: "मेरी", + mailingFilterSubscribed: "सदस्यता ली गई", + mailingFilterRecent: "हाल की", + mailingHistoryDate: "तारीख", + mailingHistoryThreads: "थ्रेड", + mailingHistoryInhabitants: "निवासी", + mailingHistorySearchPlaceholder: "इतिहास में खोजें...", + mailingNoMessages: "अभी कोई संदेश नहीं।", + mailingNoItems: "कोई मेलिंग सूची नहीं मिली।", + mailingSearchPlaceholder: "मेलिंग सूचियों में खोजें...", + mailingWriteButton: "संदेश लिखें", + mailingWriteTitle: "सूची को लिखें", + mailingReplyTitle: "सूची को उत्तर दें", + mailingSend: "भेजें", + mailingTextPlaceholder: "आपका संदेश सूची के हर प्रतिभागी तक पहुँचता है।", + mailingParticipants: "प्रतिभागी", + inboxFilterMailing: "मेलिंग सूचियाँ", + mailingReplyPlaceholder: "सूची को उत्तर दें...", + mailingReplyToList: "सूची को उत्तर दें", + typeMailingList: "मेलिंग सूची", + statsMailingList: "मेलिंग सूचियाँ", + mailingListLabel: "मेलिंग सूची", + logisticsTitle: "लॉजिस्टिक्स", + modulesLogisticsLabel: "लॉजिस्टिक्स", + modulesLogisticsDescription: "अपने नेटवर्क में लॉजिस्टिक योजनाएँ खोजें और प्रबंधित करें।", + logisticsDescription: "अपने नेटवर्क में लॉजिस्टिक योजनाएँ खोजें और प्रबंधित करें।", + logisticsCreate: "मार्ग बनाएँ", + logisticsUpdate: "अपडेट करें", + logisticsClose: "बंद करें", + logisticsReopen: "फिर से खोलें", + logisticsCreateSectionTitle: "मार्ग बनाएँ", + logisticsUpdateSectionTitle: "मार्ग अपडेट करें", + logisticsTitleLabel: "शीर्षक", + logisticsTitlePlaceholder: "बाज़ार तक सवारी, वैन उपलब्ध...", + logisticsKindLabel: "लॉजिस्टिक्स प्रकार", + logisticsKindTrip: "यात्रा", + logisticsKindShipment: "शिपमेंट", + logisticsKindTrips: "यात्राएँ", + logisticsKindShipments: "शिपमेंट", + logisticsModeLabel: "मोड", + logisticsModeOffer: "पेशकश", + logisticsModeRequest: "अनुरोध", + logisticsModeOffers: "पेशकशें", + logisticsModeRequests: "अनुरोध", + logisticsDescriptionLabel: "विवरण", + logisticsDescriptionPlaceholder: "मिलने की जगह, आप क्या ले जाते हैं, शर्तें...", + logisticsOriginLabel: "प्रारंभ", + logisticsOriginPlaceholder: "कहाँ से शुरू", + logisticsDestinationLabel: "गंतव्य", + logisticsDestinationPlaceholder: "कहाँ समाप्त", + logisticsDateLabel: "तारीख और समय", + logisticsRecurrenceLabel: "पुनरावृत्ति", + logisticsFrequencyLabel: "आवृत्ति", + logisticsRecurrenceNone: "एक बार", + logisticsRecurrenceDaily: "रोज़", + logisticsRecurrenceWeekly: "साप्ताहिक", + logisticsRecurrenceMonthly: "मासिक", + logisticsSeatsLabel: "सीटें", + logisticsSizeLabel: "आकार", + logisticsSizePlaceholder: "जैसे 2m x 1m", + logisticsWeightLabel: "वज़न", + logisticsWeightPlaceholder: "जैसे 40kg", + logisticsPriceFree: "मुफ़्त", + logisticsPriceEco: "ECO", + logisticsPriceTime: "समय", + logisticsPriceLabel: "मूल्य", + logisticsHours: "घं", + logisticsOrderRefLabel: "ऑर्डर लिंक", + logisticsOrderRefPlaceholder: "डिलीवर किए जाने वाले ऑर्डर का /shops/... या /market/...", + logisticsOrderLink: "जुड़ा ऑर्डर", + logisticsTagsLabel: "टैग", + logisticsTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + logisticsFilterAll: "सभी", + logisticsFilterMine: "मेरे", + logisticsFilterBooked: "बुक किए गए", + logisticsFilterRecent: "हाल के", + logisticsFilterHistory: "इतिहास", + logisticsFilterTop: "टॉप", + logisticsStatusOpen: "खुला", + logisticsStatusUpcoming: "आगामी", + logisticsStatusPast: "बीते", + logisticsStatusClosed: "बंद", + logisticsZoneLabel: "क्षेत्र", + logisticsSearchPlaceholder: "लॉजिस्टिक्स में खोजें...", + logisticsNoItems: "कोई मार्ग नहीं मिला।", + logisticsBookingPending: "लंबित", + logisticsBookingConfirmed: "पुष्टि", + logisticsBookingRejected: "अस्वीकृत", + logisticsBookingCancelled: "रद्द", + logisticsBookingDelivered: "डिलीवर", + logisticsConfirm: "पुष्टि करें", + logisticsReject: "अस्वीकार करें", + logisticsDeliver: "डिलीवर करें", + logisticsReceiptLabel: "रसीद", + logisticsReceiptPlaceholder: "डिलीवरी नोट (वैकल्पिक)", + logisticsCancelBooking: "बुकिंग रद्द करें", + logisticsBookTitle: "यह मार्ग बुक करें", + logisticsNotesLabel: "नोट्स", + logisticsNotesPlaceholder: "जो मालिक को पता होना चाहिए", + logisticsBook: "बुक करें", + logisticsBookingsTitle: "बुकिंग", + logisticsMyBookingTitle: "आपकी बुकिंग", + logisticsRatingsTitle: "रेटिंग", + logisticsNoRatings: "अभी कोई रेटिंग नहीं।", + logisticsRatingsWhenClosed: "मार्ग बंद होने पर रेटिंग खुलती है।", + logisticsScoreLabel: "स्कोर", + logisticsRatingPlaceholder: "कैसा रहा?", + logisticsRate: "रेट करें", + typeLogisticsRoute: "मार्ग", + typeLogisticsRating: "मार्ग रेटिंग", + statsLogisticsRoute: "मार्ग", + statsLogisticsRating: "मार्ग रेटिंग", + logisticsRouteLabel: "मार्ग", + podcastsTitle: "पॉडकास्ट", + modulesPodcastsLabel: "पॉडकास्ट", + modulesPodcastsDescription: "पॉडकास्ट चैनल प्रकाशित करने का मॉड्यूल।", + podcastsDescription: "पॉडकास्ट चैनल प्रकाशित करने का मॉड्यूल।", + podcastCreate: "पॉडकास्ट बनाएँ", + podcastUpdate: "अपडेट करें", + podcastDelete: "हटाएँ", + podcastCreateSectionTitle: "पॉडकास्ट बनाएँ", + podcastUpdateSectionTitle: "पॉडकास्ट अपडेट करें", + podcastTitleLabel: "शीर्षक", + podcastTitlePlaceholder: "चैनल का नाम", + podcastDescriptionLabel: "विवरण", + podcastDescriptionPlaceholder: "यह पॉडकास्ट किस बारे में है?", + podcastCategoryLabel: "श्रेणी", + podcastCategoryNews: "समाचार", + podcastCategoryMusic: "संगीत", + podcastCategoryTalk: "बातचीत", + podcastCategoryEducation: "शिक्षा", + podcastCategoryTech: "तकनीक", + podcastCategoryCulture: "संस्कृति", + podcastCategoryCommunity: "समुदाय", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "अन्य", + podcastTagsLabel: "टैग", + podcastCoverLabel: "कवर (छवि या वीडियो)", + podcastEpisodeAudioLabel: "ऑडियो या वीडियो फ़ाइल (अधिकतम: 75MB)", + podcastTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + podcastFilterAll: "सभी", + podcastFilterMine: "मेरे", + podcastFilterRecent: "हाल के", + podcastFilterTop: "टॉप", + podcastFilterViewers: "श्रोता", + podcastSearchPlaceholder: "पॉडकास्ट में खोजें...", + podcastNoItems: "कोई पॉडकास्ट नहीं मिला।", + podcastEpisodesLabel: "एपिसोड", + podcastPlaysLabel: "व्यूज़", + podcastLastEpisode: "अंतिम एपिसोड", + podcastEpisodesTitle: "एपिसोड", + podcastNoEpisodes: "अभी कोई एपिसोड नहीं।", + podcastEpisodeAdd: "एपिसोड जोड़ें", + podcastEpisodeCreateTitle: "एपिसोड प्रकाशित करें", + podcastEpisodeUpdateTitle: "एपिसोड अपडेट करें", + podcastEpisodeTitleLabel: "एपिसोड शीर्षक", + podcastEpisodeTitlePlaceholder: "एपिसोड का शीर्षक", + podcastEpisodeDescriptionLabel: "शो नोट्स", + podcastEpisodeDescriptionPlaceholder: "इस एपिसोड में क्या होता है, लिंक, श्रेय...", + podcastEpisodePublish: "प्रकाशित करें", + podcastPlayed: "सुना गया", + podcastMarkPlayed: "सुना हुआ चिह्नित करें", + typePodcast: "पॉडकास्ट", + typePodcastEpisode: "एपिसोड", + typePodcastOpinion: "एपिसोड राय", + typePodcastPlay: "एपिसोड प्ले", + statsPodcast: "पॉडकास्ट", + statsPodcastEpisode: "एपिसोड", + statsPodcastPlay: "एपिसोड प्ले", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "आपके द्वारा फ़ॉलो किए गए ब्लॉग में नई प्रविष्टि है।", + podcastBotEpisodeTitle: "आपके द्वारा फ़ॉलो किए गए पॉडकास्ट में नया एपिसोड है।", + podcastLabel: "पॉडकास्ट", + podcastEpisodeLabel: "एपिसोड", + campaignsTitle: "अभियान", + modulesCampaignsLabel: "अभियान", + modulesCampaignsDescription: "अपने नेटवर्क में अभियान खोजें और प्रबंधित करें।", + campaignsDescription: "अपने नेटवर्क में अभियान खोजें और प्रबंधित करें।", + campaignCreate: "अभियान बनाएँ", + campaignUpdate: "अपडेट करें", + campaignDelete: "हटाएँ", + campaignClose: "बंद करें", + campaignReopen: "फिर से खोलें", + campaignCreateSectionTitle: "अभियान बनाएँ", + campaignUpdateSectionTitle: "अभियान अपडेट करें", + campaignTitleLabel: "शीर्षक", + campaignTitlePlaceholder: "आप क्या हासिल करना चाहते हैं?", + campaignTextLabel: "पाठ", + campaignTextPlaceholder: "कारण समझाएँ, कौन तय करता है और आप क्या माँगते हैं...", + campaignCategoryLabel: "श्रेणी", + campaignCategoryEnvironment: "पर्यावरण", + campaignCategoryRights: "अधिकार", + campaignCategoryHealth: "स्वास्थ्य", + campaignCategoryEducation: "शिक्षा", + campaignCategoryInfrastructure: "बुनियादी ढाँचा", + campaignCategoryCulture: "संस्कृति", + campaignCategoryEconomy: "अर्थव्यवस्था", + campaignCategoryOther: "अन्य", + campaignGoalLabel: "हस्ताक्षर लक्ष्य", + campaignDeadlineLabel: "समय सीमा", + campaignTagsLabel: "टैग", + campaignTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + campaignFilterAll: "सभी", + campaignFilterMine: "मेरे", + campaignFilterSigned: "हस्ताक्षरित", + campaignFilterRecent: "हाल के", + campaignFilterTop: "टॉप", + campaignStatusOpen: "खुला", + campaignStatusAchieved: "प्राप्त", + campaignStatusUnachieved: "अप्राप्त", + campaignStatusClosed: "बंद", + campaignSearchPlaceholder: "अभियानों में खोजें...", + campaignNoItems: "कोई अभियान नहीं मिला।", + campaignSignaturesLabel: "हस्ताक्षर", + campaignSignaturesTitle: "समर्थक", + campaignNoSignatures: "अभी कोई हस्ताक्षर नहीं।", + campaignSignTitle: "इस अभियान पर हस्ताक्षर करें", + campaignSignPlaceholder: "आप इसका समर्थन क्यों करते हैं? (वैकल्पिक)", + campaignSign: "हस्ताक्षर करें", + campaignSigned: "आप पहले ही हस्ताक्षर कर चुके हैं!", + campaignNotSigned: "आपके हस्ताक्षर अभी बाकी हैं", + campaignElevate: "संसद तक ले जाएँ", + campaignElevated: "संसद तक ले जाया गया", + campaignUpdatesTitle: "अपडेट", + campaignNoUpdates: "अभी कोई अपडेट नहीं।", + campaignUpdatePlaceholder: "क्या हुआ?", + campaignAddUpdate: "अपडेट पोस्ट करें", + campaignQrAlt: "इस अभियान पर हस्ताक्षर के लिए QR", + campaignQrCaption: "हस्ताक्षर के लिए स्कैन करें", + typeCampaign: "अभियान", + typeCampaignSignature: "हस्ताक्षर", + typeCampaignUpdate: "अभियान अपडेट", + statsCampaign: "अभियान", + statsCampaignSignature: "अभियान हस्ताक्षर", + statsCampaignUpdate: "अभियान अपडेट", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "आपके द्वारा फ़ॉलो किए गए अभियान में अपडेट है।", + campaignBotAchievedTitle: "आपके द्वारा फ़ॉलो किया गया अभियान अपने लक्ष्य तक पहुँच गया।", + campaignBotRaisedTitle: "आपके द्वारा हस्ताक्षरित एक अभियान संसद में उठाया गया है।", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "आपके द्वारा बुक किया गया मार्ग बदल गया है।", + logisticsBotClosedTitle: "आपके द्वारा बुक किया गया मार्ग बंद हो गया है।", + logisticsBotBookedTitle: "किसी ने आपका मार्ग बुक किया है।", + logisticsBotCancelledTitle: "आपके मार्ग की एक बुकिंग रद्द कर दी गई।", + logisticsBotConfirmedTitle: "आपकी बुकिंग पुष्ट हुई: पैकेज रास्ते में है।", + logisticsBotRejectedTitle: "आपकी बुकिंग अस्वीकृत हुई।", + logisticsBotDeliveredTitle: "आपका पैकेज पहुँच गया है।", + campaignLabel: "अभियान", + campaignGenerateReport: "रिपोर्ट बनाएँ", + logisticsGenerateReport: "रिपोर्ट बनाएँ", + mailingGenerateReport: "रिपोर्ट बनाएँ", + campaignUpdateEdited: "संपादित", + mailingMessagesLabel: "संदेश", + logisticsBookingsLabel: "बुकिंग", + backupTitle: "बैकअप", + modulesBackupLabel: "बैकअप", + modulesBackupDescription: "अपनी कुंजियाँ निर्यात/आयात करने और एन्क्रिप्टेड बैकअप बनाने का मॉड्यूल।", + backupDescription: "अपनी कुंजियाँ निर्यात/आयात करने और एन्क्रिप्टेड बैकअप बनाने का मॉड्यूल।", + backupTypeRecovery: "रिकवरी", + backupTypeKeys: "कुंजियाँ निर्यात करें", + backupTypeFull: "पूर्ण बैकअप", + backupTypeRestore: "बैकअप पुनर्स्थापित करें", + backupTypeInstant: "तत्काल", + backupInstantDescription: "आपके पूरे लॉग और फ़ाइलों का सादा .zip, बिना आपकी गुप्त कुंजी और बिना पासवर्ड के। तेज़, पर ", + backupInstantRisk: "जिसे फ़ाइल मिले वह सब पढ़ सकता है।", + backupKitPageDescription: "रिकवरी किट केवल इसी डिवाइस से खोली जा सकती है।", + backupKitId: "Oasis ID", + backupKitDate: "बनाया गया", + backupKitQrAlt: "आपकी गुप्त कुंजी का QR कोड", + backupKitStep1: "इसे प्रिंट करें या हाथ से लिखें और सुरक्षित, ऑफ़लाइन जगह रखें।", + backupKitStep2: "पुनर्प्राप्ति के लिए नए डिवाइस पर Oasis इंस्टॉल करें और बैकअप › कुंजियाँ से यह सीक्रेट आयात करें।", + backupKitStep3: "यदि यह पृष्ठ कभी उजागर हो जाए तो इसे नष्ट कर दें।", + backupKitPdfButton: "PDF बनाएँ", + backupScopeLabel: "दायरा", + backupScopeAll: "सब कुछ", + backupScopeMine: "केवल मेरी सामग्री", + backupSinceLabel: "इस तारीख से", + backupEstimateTitle: "अनुमान", + backupFileInfo: "आपकी एन्क्रिप्टेड प्रति आपके डिवाइस पर डाउनलोड होगी (नाम: oasis-backup-तारीख.oasisbk)", + backupExportButton: "बैकअप डाउनलोड करें", + backupRestorePassword: "बैकअप बनाते समय उपयोग किया गया पासवर्ड लिखें", + backupRestoreButton: "बैकअप पुनर्स्थापित करें", + backupRestoredLine: "बैकअप पुनर्स्थापित हुआ।", + backupRestoredMessages: "संदेश जोड़े गए", + backupRestoredSkipped: "पहले से मौजूद", + backupRestoredBlobs: "फ़ाइलें जोड़ी गईं", + backupRestoredState: "सेटिंग्स बहाल", + backupRestoredFailed: "विफल", + backupRestoredForked: "भिन्न", + backupRestoreRunning: "बैकअप पुनर्स्थापित हो रहा है। यह पेज अपने आप ताज़ा होता है; इस बीच आप Oasis का उपयोग जारी रख सकते हैं।", + backupRestoreElapsed: "बीता समय", + backupRestoreFailedLine: "बैकअप पुनर्स्थापित नहीं हो सका।", + backupRestoreForkMine: "इस डिवाइस ने पहले ही आपकी पहचान से ऐसे संदेश प्रकाशित किए हैं जो बैकअप में नहीं हैं, इसलिए उन पर बैकअप लागू नहीं हो सका। कुंजियाँ आयात करने के तुरंत बाद, कुछ और करने से पहले, एक साफ़ इंस्टॉलेशन पर पुनर्स्थापित करें।", + verificationTitle: "सत्यापन", + verificationDescription: "अपनी फ़ीड की श्रृंखला जाँचें, फोर्क पहचानें और अनाथ या गायब फ़ाइलों की समीक्षा करें।", + verificationRun: "सत्यापन चलाएँ", + verificationTotalMessages: "लॉग में संदेश", + verificationFeed: "आपकी फ़ीड", + verificationMessages: "संदेश", + verificationLastSequence: "अंतिम क्रम", + verificationGaps: "अंतराल", + verificationBrokenLinks: "टूटी कड़ियाँ", + verificationBadHashes: "गलत हैश", + verificationBadSignatures: "गलत हस्ताक्षर", + verificationDuplicates: "दोहराए गए क्रम", + verificationForks: "फोर्क", + verificationBlobs: "फ़ाइलें", + verificationBlobsPresent: "मौजूद", + verificationBlobsReferenced: "संदर्भित", + verificationBlobsOrphan: "अनाथ", + verificationBlobsMissing: "गायब", + verificationBlobsMissingOwn: "इस उपकरण पर आपकी गायब फ़ाइलें", + verificationBlobsMissingOthers: "अन्य निवासियों की फ़ाइलें अभी डाउनलोड नहीं हुईं", + verificationBlobsUnavailable: "इस डिवाइस पर फ़ाइलें सूचीबद्ध नहीं की जा सकीं।", + wikiTitle: "विकी", + modulesWikiLabel: "विकी", + modulesWikiDescription: "विकी खोजने और प्रबंधित करने का मॉड्यूल।", + wikiDescription: "विकी खोजने और प्रबंधित करने का मॉड्यूल।", + wikiCreate: "विकी बनाएँ", + wikiUpdate: "अपडेट", + wikiDelete: "हटाएँ", + wikiEdit: "संपादित करें", + wikiTitleLabel: "शीर्षक", + wikiTitlePlaceholder: "विकी का शीर्षक...", + wikiBodyLabel: "सामग्री", + wikiBodyPlaceholder: "अन्य विकी को [[विकी का शीर्षक]] या [[विकी का शीर्षक|अपना लेबल]] से जोड़ें।\n\n@id से किसी निवासी का उल्लेख करें।\n\n#oasis से टैग करें।\n\nhttps://solarnethub.com जैसे लिंक चिपकाएँ\n\nखाली पंक्ति नया अनुच्छेद शुरू करती है; एकल लाइन ब्रेक बने रहते हैं।\n\nकोई भी समर्थित मीडिया जोड़ें और वह यहाँ एम्बेड हो जाएगा।", + formatBarLabel: "फ़ॉर्मैट", + wikiStatusLabel: "विकी प्रकार", + wikiStatusOpen: "खुली", + wikiStatusClosed: "बंद", + wikiTagsLabel: "टैग", + wikiTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + wikiSummaryLabel: "बदलाव का सारांश", + wikiSummaryPlaceholder: "क्या बदला?", + wikiFilterAll: "सभी", + wikiFilterMine: "मेरे", + wikiFilterRecent: "हाल के", + wikiFilterLinked: "जुड़े हुए", + wikiChanges: "हाल के बदलाव", + wikiNoPages: "कोई विकी नहीं मिली।", + wikiNoChanges: "अभी कोई बदलाव नहीं।", + wikiHistory: "इतिहास", + wikiVersion: "संस्करण", + wikiVersions: "संस्करण", + wikiRestore: "पुनर्स्थापित करें", + wikiDiff: "बदलाव", + wikiViewVersion: "देखें", + wikiLinkedPages: "जुड़े हुए पृष्ठ", + wikiNoLinkedPages: "यह पृष्ठ अभी किसी अन्य से नहीं जुड़ा है।", + wikiMissingLink: "पृष्ठ अभी मौजूद नहीं है", + wikiDuplicateTitle: "इस शीर्षक वाला पृष्ठ पहले से मौजूद है। दूसरा पृष्ठ बनाने के लिए शीर्षक बदलें।", + wikiCreateSectionTitle: "विकी बनाएँ", + wikiUpdateSectionTitle: "विकी अपडेट करें", + wikiSearchPlaceholder: "विकी में खोजें...", + wikiCurrentVersion: "वर्तमान संस्करण", + wikiViewingVersion: "आप एक पुराना संस्करण देख रहे हैं", + wikiBackToPage: "पृष्ठ पर वापस", + wikiLinkedChip: "जुड़ा", + wikiRestoredSummary: "पुनर्स्थापित संस्करण", + wikiExportPdf: "पृष्ठ बनाएँ", + wikiPreview: "पूर्वावलोकन", + tribeSectionWiki: "विकी", + tribeWikiCreate: "विकी बनाएँ", + tribeWikiEmpty: "अभी कोई विकी नहीं।", + typeWiki: "विकी", padsTitle: "पैड्स", padTitle: "पैड", modulesPadsLabel: "पैड्स", - modulesPadsDescription: "सहयोगी पाठ संपादकों को प्रबंधित करने का मॉड्यूल।", - padFilterAll: "सभी", - padFilterMine: "मेरे", - padFilterRecent: "हालिया", - padFilterOpen: "खुला", - padFilterClosed: "बंद", + modulesPadsDescription: "सहयोगी टेक्स्ट एडिटर प्रबंधित करने का मॉड्यूल।", padCreate: "पैड बनाएं", - padUpdate: "पैड अपडेट करें", - padDelete: "पैड हटाएं", + padUpdate: "अपडेट करें", + padDelete: "हटाएं", padTitleLabel: "शीर्षक", padTitlePlaceholder: "पैड का शीर्षक दर्ज करें...", padStatusLabel: "स्थिति", + padTypeLabel: "पैड प्रकार", padStatusOpen: "खुला", padStatusInviteOnly: "केवल आमंत्रण", padStatusClosed: "बंद", padDeadlineLabel: "समय सीमा", padTagsLabel: "टैग", - padTagsPlaceholder: "टैग1, टैग2, ...", + padTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", padMembersLabel: "सदस्य", - padVisitPad: "पैड देखें", - padShareUrl: "URL साझा करें", padCreated: "बनाया गया", - padAuthor: "लेखक", - padGenerateCode: "कोड बनाएं", - padInviteCodeLabel: "आमंत्रण कोड", - padInviteCodePlaceholder: "आमंत्रण कोड दर्ज करें...", - padValidateInvite: "सत्यापित करें", - padStartEditing: "संपादन शुरू करें!", + padStartEditing: "संपादित करें", padEditorPlaceholder: "लिखना शुरू करें...", padSubmitEntry: "सबमिट करें", padNoEntries: "अभी कोई प्रविष्टि नहीं।", padAllSectionTitle: "सभी पैड्स", padMineSectionTitle: "मेरे पैड्स", padsDescription: "अपने नेटवर्क में सहयोगी एन्क्रिप्टेड टेक्स्ट संपादक प्रबंधित करें।", + roomsTitle: "रूम्स", + roomsDescription: "अपने नेटवर्क में एन्क्रिप्टेड वीडियो कॉन्फ्रेंस रूम खोजें और उनमें शामिल हों।", + modulesRoomsLabel: "रूम्स", + modulesRoomsDescription: "एन्क्रिप्टेड वीडियो कॉन्फ्रेंस रूम खोजने और उनमें शामिल होने का मॉड्यूल।", + roomFilterRecent: "हाल के", + roomFilterMine: "मेरे", + roomFilterAll: "सभी", + roomFilterLive: "लाइव", + roomFilterOpen: "खुले", + roomFilterInvite: "केवल आमंत्रण", + roomFilterClosed: "बंद", + roomCreate: "रूम बनाएं", + roomUpdate: "अपडेट करें", + roomDelete: "हटाएं", + roomClose: "बंद करें", + roomReopen: "फिर से खोलें", + roomCreateSectionTitle: "रूम बनाएं", + roomUpdateSectionTitle: "रूम अपडेट करें", + roomTitlePlaceholder: "रूम का शीर्षक", + roomDescriptionLabel: "विवरण", + roomDescriptionPlaceholder: "आप किस बारे में बात करेंगे?", + roomTypeLabel: "रूम का प्रकार", + roomTagsLabel: "टैग", + roomStatusOpen: "खुला", + roomStatusInviteOnly: "केवल आमंत्रण", + roomStatusClosed: "बंद", + roomParticipants: "प्रतिभागी", + roomJoin: "शामिल हों", + roomLeave: "छोड़ें", + roomFull: "रूम भरा हुआ है", + roomLiveChip: "लाइव", + roomMuted: "म्यूट", + roomRec: "ऑडियो रिकॉर्ड करें", + roomRecStop: "रिकॉर्डिंग रोकें", + roomStreamFile: "स्ट्रीम करें", + roomStreamStop: "रोकें", + roomStreaming: "स्ट्रीम हो रहा है", + liveFullscreen: "पूर्ण स्क्रीन", + roomStreamPickTitle: "वीडियो चुनें", + roomStreamPickHint: "इस डिवाइस के वीडियो। आप जो चुनते हैं वह अपनी जगह से ही स्ट्रीम होता है, बिना कॉपी या प्रकाशित किए।", + roomStreamGo: "रूम में चलाएँ", + roomStreamNotFound: "यह वीडियो आपके फ़ोल्डरों में नहीं है।", + roomPublicWarning: "यह रूम सार्वजनिक वेब पर प्रसारित होता है: इसमें जो होता है उसे कोई भी देख और सुन सकता है।", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} कमरे में आए", + roomEventLeft: "{name} कमरे से गए", + roomEventRecStart: "{name} ने रिकॉर्डिंग शुरू की", + roomEventRecStop: "{name} ने रिकॉर्डिंग रोकी", + roomEventMuted: "{name} ने खुद को म्यूट किया", + roomEventUnmuted: "{name} ने म्यूट हटाया", + roomEventHand: "{name} ने हाथ उठाया", + roomRaiseHand: "हाथ उठाएँ", + roomLowerHand: "हाथ नीचे करें", + confirmActionTitle: "इस कार्रवाई की पुष्टि करें", + confirmActionButton: "पुष्टि करें", + confirmJoinText: "आप इस निमंत्रण से शामिल होने वाले हैं।", + confirmFollowText: "आप इस निवासी का समर्थन करने वाले हैं।", + confirmSignText: "आप इस अभियान पर हस्ताक्षर करने वाले हैं।", + confirmDownloadText: "आप यह फ़ाइल अपने डिवाइस पर डाउनलोड करने वाले हैं।", + roomRecordings: "रिकॉर्डिंग", + roomClearNotices: "साफ़ करें", + roomLineSwitchboard: "स्विचबोर्ड", + roomLineDndError: "इस रूम का नंबर कॉल स्वीकार नहीं कर रहा है।", + roomNumberHint: "रूम में शामिल होने के लिए फ़ोन से यह नंबर डायल करें।", + liveRefresh: "ताज़ा करें", + roomNobody: "अभी रूम में कोई नहीं है।", + roomSomeone: "अभी रूम में {n}।", + roomYou: "आप", + roomYouAreIn: "आप अंदर हैं", + roomNobodyInside: "इस समय इस कक्ष में कोई नहीं है।", + roomErrorFull: "रूम भरा हुआ है। किसी के जाने पर आप शामिल हो सकते हैं।", + roomErrorUnreachable: "अभी रूम तक पहुंचा नहीं जा सकता। बाद में फिर से प्रयास करें।", + roomErrorBusy: "आप पहले से किसी कॉल या दूसरे रूम में हैं।", + roomErrorClosed: "यह रूम बंद है।", + roomAccessDenied: "यह रूम केवल आमंत्रण पर है। शामिल होने के लिए आपको आमंत्रण चाहिए।", + roomsNoItems: "कोई रूम नहीं मिला।", + roomSearchPlaceholder: "रूम खोजें...", + roomInviteMode: "आमंत्रण", + tribeSectionRooms: "रूम्स", + tribeRoomCreate: "रूम बनाएं", + tribeRoomsEmpty: "अभी तक कोई रूम नहीं।", + typeRoom: "रूम्स", + typeRoomMember: "रूम प्रतिभागी", + statsRoom: "रूम्स", + statsRoomMember: "रूम प्रतिभागी", + favoritesFilterRooms: "रूम्स", + invitesRoomsTitle: "रूम्स", + inviteCodeInvalid: "यह आमंत्रण कोड मान्य नहीं है या पहले ही इस्तेमाल हो चुका है।", + inviteAlreadyMember: "आप पहले से शामिल हैं। आप इसे इसके मॉड्यूल से खोल सकते हैं।", + actionFailed: "यह नहीं हो सका। कृपया फिर से प्रयास करें।", + publishTooShort: "आपकी पोस्ट बहुत छोटी है।", + walletSendFailed: "भुगतान नहीं भेजा जा सका: {reason}", + pixeliaCoordsError: "ये निर्देशांक ग्रिड से बाहर हैं।", + courtsErrTitle: "शीर्षक आवश्यक है।", + courtsErrRespondent: "बताएं कि मामला किसके विरुद्ध है।", + courtsErrRespondentId: "यह मान्य Oasis ID नहीं है।", + courtsErrMethod: "चुनें कि मामला कैसे सुलझाया जाएगा।", + courtsErrCaseNotFound: "वह मामला नहीं मिला।", + courtsErrBrief: "अपना जवाब लिखें।", + courtsErrStance: "अपना पक्ष चुनें।", + courtsErrResult: "परिणाम लिखें।", + courtsErrTerms: "समझौते की शर्तें लिखें।", + courtsErrMediator: "कम से कम एक मध्यस्थ जोड़ें।", + courtsErrSelfMediator: "आप अपने ही मामले में मध्यस्थ नहीं हो सकते।", + courtsErrJudge: "एक न्यायाधीश चुनें।", + courtsErrSelfJudge: "आप अपने ही मामले के न्यायाधीश नहीं हो सकते।", + courtsErrVisibility: "चुनें कि मामला कौन देख सकता है।", + courtsErrNomination: "वह नामांकन नहीं मिला।", + opinionAlreadyGiven: "आप पहले ही अपनी राय दे चुके हैं।", + cipherPasswordShort: "पासवर्ड कम से कम 32 अक्षरों का होना चाहिए।", + errorForbidden: "आपको इसकी पहुंच नहीं है।", + errorNotFound: "यह नहीं मिला।", + errorClosed: "यह बंद है।", + errorInviteOnly: "यह केवल आमंत्रण पर है। आपको आमंत्रण चाहिए।", + errorTooLong: "यह पाठ बहुत लंबा है।", + errorRequired: "कुछ आवश्यक जानकारी नहीं है।", + errorInvalid: "कुछ जानकारी मान्य नहीं है।", + errorNotAuthor: "केवल लेखक ही ऐसा कर सकता है।", + panicRemoved: "आपकी ब्लॉकचेन हटा दी गई है।", padRecentSectionTitle: "हालिया पैड्स", padOpenSectionTitle: "खुले पैड्स", padClosedSectionTitle: "बंद पैड्स", padCreateSectionTitle: "नया पैड बनाएं", padUpdateSectionTitle: "पैड अपडेट करें", - padInviteGenerated: "आमंत्रण कोड बनाया गया", typePad: "पैड्स", padNew: "नया", padAddFavorite: "पसंदीदा में जोड़ें", padRemoveFavorite: "पसंदीदा से हटाएं", - padClose: "पैड बंद करें", + padClose: "बंद करें", padBackToEditor: "संपादक पर वापस", - padSearchPlaceholder: "पैड खोजें...", + padSearchPlaceholder: "पैड्स में खोजें...", modulesChatsLabel: "चैट्स", - modulesChatsDescription: "एन्क्रिप्टेड चैट खोजने और प्रबंधित करने का मॉड्यूल।", + modulesChatsDescription: "चैट खोजने और प्रबंधित करने का मॉड्यूल।", typeChat: "चैट्स", typeChatMessage: "चैट संदेश", - chatLabel: "चैट्स", - chatMessageLabel: "चैट संदेश", chatsTitle: "चैट्स", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "आपकी चैट्स", + chatRecentTitle: "हाल की चैट्स", chatFavoritesTitle: "पसंदीदा", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "खुली चैट्स", + chatClosedTitle: "बंद चैट्स", chatDescription: "विवरण", chatCategory: "श्रेणी", chatStatus: "स्थिति", - chatFilterAll: "सभी", - chatFilterMine: "मेरे", - chatFilterRecent: "हाल के", - chatFilterFavorites: "पसंदीदा", - chatFilterOpen: "खुले", - chatFilterClosed: "बंद", chatCreate: "चैट बनाएं", - chatUpdate: "चैट अपडेट करें", - chatDelete: "चैट हटाएं", + chatUpdate: "अपडेट", + chatDelete: "हटाएं", chatClose: "चैट बंद करें", chatVisitChat: "चैट देखें", chatUntitled: "बिना शीर्षक चैट", chatNoItems: "कोई चैट नहीं मिली।", chatParticipants: "प्रतिभागी", - chatStartChatting: "चैट शुरू करें!", - chatGenerateCode: "कोड बनाएं", - chatShareUrl: "URL शेयर करें", + chatMessagesLabel: "संदेश", + chatThreadsLabel: "थ्रेड", chatCreatedAt: "बनाया गया", - chatSearchPlaceholder: "चैट खोजें...", + chatSearchPlaceholder: "चैट्स में खोजें...", chatStatusOpen: "खुला", chatStatusInviteOnly: "केवल आमंत्रण", chatStatusClosed: "बंद", chatSendMessage: "भेजें", + chatJumpLatest: "नवीनतम संदेश", + chatReply: "उत्तर दें", + chatReplyCancel: "उत्तर रद्द करें", + chatPin: "पिन करें", + chatUnpin: "अनपिन करें", + chatPinned: "पिन किए गए", + chatPickChat: "खोलने के लिए एक चैट चुनें", + chatNoneYet: "अभी तक कोई चैट उपलब्ध नहीं है।", + activitySearchPlaceholder: "गतिविधि में खोजें...", + trendingSearchPlaceholder: "ट्रेंडिंग में खोजें...", + opinionsSearchPlaceholder: "राय में खोजें...", + votesSearchPlaceholder: "मतदान में खोजें...", + gamesSearchPlaceholder: "खेल में खोजें...", + schoolTitle: "विद्यालय", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "आपके द्वारा फ़ॉलो किया गया विकी पृष्ठ संपादित किया गया है।", + wikiBotRestoredTitle: "आपके द्वारा फ़ॉलो किए गए विकी पृष्ठ का पुराना संस्करण पुनर्स्थापित किया गया है।", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "एक नए छात्र ने आपके पाठ्यक्रम में नामांकन किया है।", + schoolBotInvitedTitle: "आपको एक पाठ्यक्रम में आमंत्रित किया गया है।", + schoolBotAdmittedTitle: "आपको एक पाठ्यक्रम में प्रवेश मिला है।", + schoolBotCertificateTitle: "आपको एक प्रमाणपत्र मिला है।", + schoolBotPassedTitle: "एक छात्र ने आपका पाठ्यक्रम उत्तीर्ण किया है।", + schoolBotLessonTitle: "एक नया पाठ प्रकाशित हुआ है।", + invitesSchoolTitle: "विद्यालय", + schoolEnrollLabel: "पाठ्यक्रम से जुड़ें", + schoolExamRetryLater: "आप 24 घंटे बाद फिर से प्रयास कर सकते हैं।", + schoolApproved: "उत्तीर्ण", + schoolExamPassMark: "उत्तीर्ण अंक (0-10)", + schoolExamPassed: "उत्तीर्ण", + schoolExamPending: "लंबित", + schoolExamFailed: "अनुत्तीर्ण", + schoolExamAddQuestion: "प्रश्न जोड़ें", + schoolExamCreateQuestion: "प्रश्न बनाएँ", + schoolExamOption: "विकल्प", + schoolExamCorrect: "सही विकल्प", + schoolExamPoints: "अंक", + schoolMaterialTextPlaceholder: "इस पाठ के लिए नोट्स...", + schoolMaterials: "सामग्री", + schoolAddMaterial: "सामग्री जोड़ें", + schoolNoMaterials: "अभी तक कोई सामग्री नहीं।", + bankYourSchoolBalance: "आपकी विद्यालय आय", + schoolProgress: "प्रगति", + schoolProgressMark: "पूर्ण के रूप में चिह्नित करें", + schoolProgressDone: "पूर्ण", + schoolExams: "परीक्षाएं", + schoolExamCreate: "परीक्षा बनाएं", + schoolExamTitlePlaceholder: "परीक्षा शीर्षक", + schoolExamHelp: "प्रति पंक्ति एक प्रश्न: प्रश्न? ; विकल्प ; सही विकल्प* ; विकल्प", + schoolExamQuestionsPlaceholder: "SSB क्या है? ; एक डेटाबेस ; एक P2P प्रोटोकॉल* ; एक ब्राउज़र", + schoolExamSubmit: "उत्तर भेजें", + schoolExamScore: "आपका स्कोर", + schoolNoScoresYet: "अभी तक कोई परिणाम नहीं।", + schoolLessonUnit: "इकाई / चरण", + schoolLessonUnitPlaceholder: "जैसे चरण 1", + schoolLessonOrder: "क्रम", + profileClearnetSchoolLabel: "विद्यालय", + schoolStartDate: "आरंभ तिथि", + schoolSessionDate: "सत्र तिथि", + schoolCourseChat: "पाठ्यक्रम चैट", + agendaFilterSchool: "विद्यालय", + agendaFilterCampaigns: "अभियान", + agendaFilterLogistics: "लॉजिस्टिक्स", + schoolCourseType: "पाठ्यक्रम प्रकार", + schoolTypeOpen: "खुला", + schoolTypePaid: "सशुल्क", + schoolTypeInvite: "केवल आमंत्रण", + schoolLessonLocked: "पाठ एन्क्रिप्टेड है — शिक्षक से पाठ्यक्रम की कुंजी मांगें।", + schoolGrantAccess: "पाठ्यक्रम कुंजी भेजें", + schoolKeySent: "कुंजी भेजी गई", + schoolOngoing: "जारी", + schoolFilterApplied: "नामांकित", + schoolFilterOpen: "खुले", + schoolFilterFavorites: "पसंदीदा", + schoolPendingPayment: "भुगतान लंबित", + schoolPendingStudents: "लंबित नामांकन", + schoolViewBill: "बिल देखें", + schoolPaymentNote: "नामांकन एक ECO बिल (एक ट्रांसफर) बनाता है। ट्रांसफर पर हस्ताक्षर होने पर आपका नामांकन पक्का होता है।", + schoolCloseCourse: "पाठ्यक्रम बंद करें", + schoolReopenCourse: "पाठ्यक्रम फिर खोलें", + schoolDescription: "अपने नेटवर्क में पाठ्यक्रम पढ़ाएं और सीखें।", + schoolFilterAll: "सभी", + schoolFilterMine: "मेरे", + schoolFilterTop: "शीर्ष", + schoolFilterRecent: "हाल के", + schoolCreateButton: "पाठ्यक्रम बनाएं", + schoolCreateSectionTitle: "पाठ्यक्रम बनाएं", + schoolUpdateSectionTitle: "पाठ्यक्रम अपडेट करें", + schoolCourseTitlePlaceholder: "आपके पाठ्यक्रम का नाम", + schoolCourseDescriptionPlaceholder: "इस पाठ्यक्रम में आप क्या पढ़ाएंगे?", + schoolCourseImage: "पाठ्यक्रम छवि (अधिकतम आकार: 75MB)", + schoolTags: "टैग", + schoolTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + schoolPrice: "मूल्य (ECO)", + schoolVisibility: "दृश्यता", + schoolVisibilityPublic: "सार्वजनिक", + schoolVisibilityInvite: "आमंत्रण द्वारा", + schoolStatus: "स्थिति", + schoolClosed: "बंद", + schoolFree: "मुफ़्त", + schoolCreate: "पाठ्यक्रम बनाएं", + schoolUpdate: "पाठ्यक्रम अपडेट करें", + schoolSearchPlaceholder: "विद्यालय में खोजें...", + schoolSortTitle: "शीर्षक", + schoolNoCourses: "कोई पाठ्यक्रम नहीं मिला।", + schoolStudents: "छात्र", + schoolTeacher: "शिक्षक", + schoolCreatedAt: "निर्मित", + schoolLessons: "पाठ", + schoolNoLessons: "अभी तक कोई पाठ नहीं।", + schoolEnrollToSee: "इस पाठ्यक्रम के पाठ देखने के लिए नामांकन करें।", + schoolEnrollButton: "नामांकन करें", + schoolUnenrollButton: "पाठ्यक्रम छोड़ें", + schoolInviteRequired: "यह पाठ्यक्रम आमंत्रण द्वारा है।", + schoolTeacherPanel: "शिक्षक पैनल", + schoolStudentPanel: "छात्र पैनल", + schoolAddLesson: "पाठ बनाएँ", + schoolLessonTitlePlaceholder: "पाठ का शीर्षक", + schoolLessonTextPlaceholder: "इस पाठ में शामिल अवधारणाओं का वर्णन करें...", + schoolInviteStudents: "छात्रों को आमंत्रित करें", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "आमंत्रित करें", + schoolInvited: "आमंत्रित", + schoolIssueCertificate: "प्रमाणपत्र बनाएँ", + schoolCertified: "प्रमाणित", + schoolCertificateTextPlaceholder: "प्रमाणपत्र नोट (वैकल्पिक)", + schoolLessonOpen: "पाठ खोलें", + schoolCertificatePdf: "PDF", + schoolCertificates: "प्रमाणपत्र", + schoolNoCertificates: "अभी तक कोई प्रमाणपत्र जारी नहीं हुआ।", + schoolEditButton: "संपादित करें", + schoolDeleteButton: "हटाएं", + typeSchool: "विद्यालय", + schoolCourseLabel: "पाठ्यक्रम", + emergencyLabel: "आपातकाल", + wikiPageLabel: "विकी", + statsSchoolCourse: "पाठ्यक्रम", + statsSchoolLesson: "पाठ", + statsSchoolEnroll: "नामांकन", + statsSchoolCertificate: "विद्यालय प्रमाणपत्र", + modulesSchoolLabel: "विद्यालय", + modulesSchoolDescription: "ज्ञान के आदान-प्रदान के लिए मॉड्यूल।", + welcomeStepUxAction: "दृश्य सहेजें", + welcomeStepWorkflowTitle: "आप OASIS का किस तरह उपयोग करना चाहते हैं?", + welcomeStepWorkflowText: "Oasis में कई मॉड्यूल हैं। अपने लिए सही कॉन्फ़िगरेशन चुनें और केवल उपयोगी मॉड्यूल चालू रहेंगे। आप इसे कभी भी मॉड्यूल में बदल सकते हैं।", + welcomeStepWorkflowAction: "कॉन्फ़िगरेशन सहेजें", + welcomeWorkflow_full: "डिफ़ॉल्ट", + chatRateLimitTitle: "बहुत अधिक संदेश", + chatRateLimitMessage: "आप इस कक्ष की प्रति घंटा संदेश सीमा तक पहुँच गए हैं। बाद में पुनः प्रयास करें।", chatMessagePlaceholder: "संदेश लिखें...", chatNoMessages: "अभी तक कोई संदेश नहीं।", - chatLeave: "चैट छोड़ें", - chatInviteCodeLabel: "आमंत्रण कोड दर्ज करें", - chatJoinByInvite: "शामिल हों", chatTitlePlaceholder: "चैट का शीर्षक", chatDescriptionPlaceholder: "चैट का विवरण", - chatTagsPlaceholder: "टैग1, टैग2", + chatTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", chatImageLabel: "छवि फ़ाइल चुनें (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "आमंत्रण कोड", chatAuthor: "लेखक", - chatCreated: "बनाया", chatAddFavorite: "पसंदीदा में जोड़ें", chatRemoveFavorite: "पसंदीदा से हटाएं", chatPM: "PM", chatStatusLabel: "स्थिति", + chatTypeLabel: "चैट प्रकार", chatCategoryLabel: "श्रेणी", - chatParticipantsLabel: "प्रतिभागी", gamesTitle: "खेल", gamesDescription: "कुछ खेल खोजें और खेलें।", gamesFilterAll: "सभी", gamesPlayButton: "खेलें!", - gamesBackToGames: "खेलों पर वापस जाएं", modulesGamesLabel: "खेल", modulesGamesDescription: "कुछ खेल खोजने और खेलने का मॉड्यूल।", modulesGraphosLabel: "ग्राफोस", @@ -3324,8 +4176,6 @@ module.exports = { gamesArkanoidDesc: "अपने पैडल और गेंद से सभी ईंटें तोड़ें। एक क्लासिक आर्केड चुनौती।", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "AI के खिलाफ क्लासिक पिंग-पोंग। 5 पॉइंट पहले पाने वाला जीतता है।", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "समय के खिलाफ दौड़! यातायात से बचें और समय से पहले मंजिल पर पहुंचें।", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "क्षुद्रग्रह क्षेत्र में अपना यान उड़ाएं। उन्हें आने से पहले नष्ट करें।", gamesRockPaperScissorsTitle: "पत्थर कागज कैंची", @@ -3335,68 +4185,57 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "एक सिक्का उछालें और हेड या टेल पर दांव लगाएं। आप कितने भाग्यशाली हैं?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "ऊपर से दिखने वाला पूल। निशाना लगाने के लिए क्लिक करें, ताकत भरने के लिए दबाए रखें। कम से कम शॉट में सभी गेंदें पॉकेट करें।", + gamesArtilleryTitle: "तोपखाना", + gamesArtilleryDesc: "अपनी तोप से निशाना लगाएँ, हवा का ध्यान रखें और लक्ष्य पर वार करें। 5 राउंड, सबसे कम शॉट वाला जीतता है।", + gamesLabyrinthTitle: "भूलभुलैया", + gamesLabyrinthDesc: "चालें ख़त्म होने से पहले भूलभुलैया से निकलें। हर स्तर बड़ा और कठिन होता जाता है।", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "सारे बिंदु खाएँ और भूतों से बचें। भूत रियल टाइम में चलते हैं — चलते रहें!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesAudioPendulumTitle: "ध्वनि पेंडुलम", + gamesAudioPendulumDesc: "रियल-टाइम ऑडियो संश्लेषण वाला अराजक भौतिकी सिम्युलेटर। कोणीय वेग आवृत्तियाँ बन जाते हैं और शिखर ढोल की थाप। कोई भी दो सिमुलेशन एक जैसे नहीं सुनाई देते।", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "दो डेक पर लाइव विज़ुअल मिक्स करें: हर एक के लिए पैटर्न चुनें, गति, रंग और ज़ूम सेट करें, दोनों के बीच क्रॉसफ़ेड करें और ताल पर फ़्लैश चलाएँ। टेम्पो टैप करें या माइक्रोफ़ोन को धड़कन चलाने दें।", + gamesTetrisDesc: "क्लासिक गिरते ब्लॉक। अंक पाने के लिए पंक्तियाँ साफ़ करें। आप कितनी देर टिक पाएँगे?", + gamesMinesweeperTitle: "माइनस्वीपर", + gamesMinesweeperDesc: "क्लासिक माइन खोज। स्तर बढ़ाने के लिए बोर्ड साफ़ करें — बड़े ग्रिड, ज़्यादा माइन, ऊँचा स्कोर।", + gamesFilterScoring: "अंक", + gamesHallOfFame: "हॉल ऑफ़ फ़ेम", + gamesHallPlayer: "खिलाड़ी", + gamesHallScore: "स्कोर", + gamesNoScores: "अभी तक कोई स्कोर नहीं।", calendarsTitle: "कैलेंडर", calendarTitle: "कैलेंडर", modulesCalendarsLabel: "कैलेंडर", modulesCalendarsDescription: "कैलेंडर खोजने और प्रबंधित करने का मॉड्यूल।", typeCalendar: "कैलेंडर", - calendarFilterAll: "सभी", - calendarFilterMine: "मेरे", - calendarFilterOpen: "खुला", - calendarFilterClosed: "बंद", - calendarFilterRecent: "हाल के", - calendarFilterFavorites: "पसंदीदा", calendarCreate: "कैलेंडर बनाएं", calendarUpdate: "अपडेट करें", - calendarDelete: "हटाएं", calendarTitleLabel: "शीर्षक", calendarTitlePlaceholder: "कैलेंडर शीर्षक...", calendarStatusLabel: "स्थिति", + calendarTypeLabel: "कैलेंडर प्रकार", calendarStatusOpen: "खुला", calendarStatusClosed: "बंद", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "अंतिम तिथि", calendarTagsLabel: "टैग", - calendarTagsPlaceholder: "टैग1, टैग2...", + calendarTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", calendarParticipantsLabel: "प्रतिभागी", - calendarParticipantsCount: "प्रतिभागी", - calendarVisitCalendar: "कैलेंडर देखें", calendarCreated: "बनाया गया", - calendarAuthor: "लेखक", calendarJoin: "जुड़ें", calendarGenerateInvite: "आमंत्रण बनाएं", - calendarInviteCodePlaceholder: "आमंत्रण कोड दर्ज करें...", - calendarValidateInvite: "कोड सत्यापित करें", - calendarJoined: "जुड़े हुए", - calendarAddDate: "तारीख जोड़ें", - calendarAddNote: "नोट जोड़ें", calendarDateLabel: "तारीख", calendarDatePlaceholder: "इस तारीख का वर्णन करें...", - calendarNoteLabel: "नोट", + calendarNoteLabel: "नोट्स", calendarNotePlaceholder: "नोट जोड़ें...", calendarFirstDateLabel: "तारीख", calendarFirstNoteLabel: "नोट्स", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "अंतराल", + calendarIntervalNone: "कोई पुनरावृत्ति नहीं", + calendarIntervalWeekly: "साप्ताहिक", + calendarIntervalMonthly: "मासिक", + calendarIntervalYearly: "वार्षिक", calendarFormDescription: "विवरण", calendarNoDates: "कोई तारीख नहीं जोड़ी गई।", calendarNoNotes: "कोई नोट नहीं।", @@ -3404,8 +4243,8 @@ module.exports = { calendarsDescription: "अपने नेटवर्क में कैलेंडर खोजें और प्रबंधित करें।", calendarMonthPrev: "← पिछला", calendarMonthNext: "अगला →", - calendarMonthLabel: "तारीखें", - calendarsShareUrl: "शेयर URL", + calendarMonthNames: "जनवरी,फ़रवरी,मार्च,अप्रैल,मई,जून,जुलाई,अगस्त,सितंबर,अक्टूबर,नवंबर,दिसंबर", + calendarDayInitials: "सो,मं,बु,गु,शु,श,र", calendarAllSectionTitle: "कैलेंडर", calendarRecentSectionTitle: "हाल के कैलेंडर", calendarFavoritesSectionTitle: "पसंदीदा", @@ -3415,11 +4254,10 @@ module.exports = { calendarCreateSectionTitle: "नया कैलेंडर बनाएं", calendarUpdateSectionTitle: "कैलेंडर अपडेट करें", calendarAddFavorite: "पसंदीदा में जोड़ें", - calendarDeleteNote: "Delete", + calendarDeleteNote: "हटाएँ", calendarRemoveFavorite: "पसंदीदा से हटाएं", - calendarSearchPlaceholder: "कैलेंडर खोजें...", + calendarSearchPlaceholder: "कैलेंडर में खोजें...", calendarAddEntry: "प्रविष्टि जोड़ें", - calendarLeave: "कैलेंडर छोड़ें", statsCalendar: "कैलेंडर", statsCalendarDate: "कैलेंडर तारीखें", statsCalendarNote: "कैलेंडर नोट", @@ -3430,6 +4268,146 @@ module.exports = { tribeContentAccessDenied: "पहुंच अस्वीकृत", tribeContentAccessDeniedMsg: "यह सामग्री एक जनजाति की है। इसे देखने के लिए आपको सदस्य होना चाहिए।", tribeViewTribes: "जनजातियाँ देखें", + tribeFilesEmpty: "अभी तक कोई टोरेंट नहीं।", + filesLabel: "टोरेंट", + profileClearnetFilesLabel: "टॉरेंट", + filesTitle: "टॉरेंट", + filesDescription: "अपने नेटवर्क में टॉरेंट खोजें और प्रबंधित करें।", + fileAllSectionTitle: "टॉरेंट", + fileMineSectionTitle: "आपके टॉरेंट", + fileRecentSectionTitle: "हालिया टॉरेंट", + fileTopSectionTitle: "शीर्ष टॉरेंट", + fileFavoritesSectionTitle: "पसंदीदा", + fileFilterAll: "सभी", + fileFilterMine: "मेरे", + fileFilterRecent: "हालिया", + fileFilterTop: "शीर्ष", + fileFilterFavorites: "पसंदीदा", + fileCreateSectionTitle: "टॉरेंट अपलोड करें", + fileUpdateSectionTitle: "टॉरेंट अपडेट करें", + fileCreateButton: "टॉरेंट अपलोड करें", + fileUpdateButton: "अपडेट करें", + fileAddFavoriteButton: "पसंदीदा में जोड़ें", + fileRemoveFavoriteButton: "पसंदीदा से हटाएं", + fileFileLabel: "एक फ़ाइल चुनें (अधिकतम-आकार: 75MB)", + calendarFilterRecent: "हाल के", + calendarFilterAll: "सभी", + calendarFilterMine: "मेरे", + calendarFilterFavorites: "पसंदीदा", + calendarFilterOpen: "खुले", + calendarFilterClosed: "बंद", + padFilterRecent: "हाल के", + padFilterAll: "सभी", + padFilterMine: "मेरे", + padFilterOpen: "खुले", + padFilterClosed: "बंद", + chatFilterRecent: "हाल के", + chatFilterAll: "सभी", + chatFilterMine: "मेरे", + chatFilterFavorites: "पसंदीदा", + chatFilterOpen: "खुले", + chatFilterClosed: "बंद", + licenseLabel: "लाइसेंस", + phoneTitle: "फ़ोन", + phoneDescription: "अपनी एंड-टू-एंड एन्क्रिप्टेड वीडियो कॉल प्रबंधित करें।", + phoneUnavailable: "इस डिवाइस पर कॉल उपलब्ध नहीं हैं।", + phoneCallButton: "कॉल करें", + profileVisibilityPhone: "फ़ोन नंबर", + phoneNumberUnknown: "आपके नेटवर्क में किसी के पास {number} नंबर नहीं है।", + phoneNumberChoose: "जाँचें कि आप {number} पर किसे कॉल करना चाहते हैं:", + phoneAnswer: "उत्तर दें", + phoneSpeaker: "स्पीकर", + phoneEarpiece: "ईयरपीस", + phoneReject: "अस्वीकार करें", + phoneHangup: "कॉल समाप्त करें", + phoneMute: "म्यूट करें", + phoneUnmute: "अनम्यूट करें", + phoneCameraOn: "कैमरा चालू", + phoneCameraOff: "कैमरा बंद", + settingsPhoneVideo: "वीडियो", + settingsPhoneVideoOn: "कॉल और कमरों में अनुमति", + settingsPhoneVideoOff: "केवल ऑडियो", + phoneSilence: "न सुनें", + phoneUnsilence: "फिर से सुनें", + phoneCalling: "कॉल कर रहे हैं:", + phoneIsCalling: "आपको कॉल कर रहे हैं", + notifyNewPmFrom: "नया PM:", + notifyNewPamFrom: "नया PaM:", + notifyUpdateLabel: "अपडेट", + updateInstalledLabel: "इंस्टॉल किया गया संस्करण", + updateAvailableLabel: "उपलब्ध संस्करण", + updateChangesLabel: "बदलाव", + updateFilesLabel: "फ़ाइलें", + updateOriginLabel: "स्रोत", + phoneRecording: "इनके लिए PaM रिकॉर्ड हो रहा है:", + phoneLeaveMessage: "किसी ने जवाब नहीं दिया: बीप के बाद अपना संदेश छोड़ें।", + phoneLeaveMessageBusy: "दूसरी कॉल पर हैं: बीप के बाद अपना संदेश छोड़ें।", + phoneLeaveMessageUnreachable: "संपर्क नहीं हो सका: बीप के बाद अपना संदेश छोड़ें।", + phoneConnecting: "कनेक्ट हो रहा है:", + phoneSendPam: "PaM भेजें", + phoneCancel: "रद्द करें", + phoneSearchPlaceholder: "फ़ोन में खोजें...", + phonePamTitle: "ऑडियो रिकॉर्ड", + phoneNew: "नया", + phoneDelete: "हटाएँ", + phoneNoPams: "आपके पास कोई ऑडियो रिकॉर्ड नहीं है।", + phoneHistoryTitle: "इतिहास", + phoneLatestTitle: "हाल की कॉल", + phoneCreateButton: "नई कॉल", + phoneLimitsHint: "प्रति कॉल अधिकतम {n} निवासी; एक से अधिक होने पर यह संयुक्त कॉल है।", + phoneErrorPub: "पब को कॉल नहीं किया जा सकता।", + phoneJointCall: "संयुक्त कॉल", + phoneWith: "किसके साथ", + phoneResult: "परिणाम", + phoneWhen: "कब", + phoneDuration: "अवधि", + phoneOutgoing: "आउटगोइंग", + phoneIncoming: "इनकमिंग", + phoneNoHistory: "अभी तक कोई लॉग नहीं।", + phoneOutcomeEnded: "उत्तर दिया", + phoneOutcomeMissed: "छूटी", + phoneOutcomeRejected: "अस्वीकृत", + phoneOutcomeNoanswer: "कोई जवाब नहीं", + phoneOutcomeCancelled: "रद्द", + phoneOutcomePam: "PaM भेजा गया", + phoneOutcomeFailed: "विफल", + phoneDialAdd: "जोड़ें", + phoneDialBack: "मिटाएं", + phoneDialClear: "साफ़ करें", + phoneDialPlaceholder: "नंबर या Oasis ID", + phoneDialInhabitants: "निवासी", + phoneDialRemove: "हटाएं", + phoneOwnNumber: "यह आपका अपना नंबर है।", + phoneSettingsDesc: "चुनें कि कौन आपको कॉल कर सकता है।", + phoneDnd: "परेशान न करें", + phoneOpen: "खुला", + phoneRecipientRefuses: "जिस निवासी को आप कॉल करना चाहते हैं, वह कॉल स्वीकार नहीं करता।", + phoneRefusedNotice: "ये निवासी कॉल स्वीकार नहीं करते और इन्हें कॉल नहीं किया गया:", + fileEmptyError: "फ़ाइल खाली है। कोई दूसरी फ़ाइल चुनें।", + fileTitleLabel: "शीर्षक", + fileTitlePlaceholder: "शीर्षक", + fileDescriptionLabel: "विवरण", + fileDescriptionPlaceholder: "विवरण", + fileTagsLabel: "टैग", + fileTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + fileSizeLabel: "आकार", + fileDownloadButton: "डाउनलोड", + fileDetailsButton: "विवरण", + fileNoFile: "कोई टॉरेंट फ़ाइल नहीं", + fileSearchPlaceholder: "टॉरेंट में खोजें...", + fileSearchButton: "खोजें", + fileSortRecent: "सबसे हालिया", + fileSortOldest: "सबसे पुराने", + fileSortTop: "सबसे अधिक वोट", + fileNoMatch: "कोई मिलान टॉरेंट नहीं।", + fileUpdatedAt: "अपडेट किया गया", + typeFile: "टॉरेंट", + modulesFilesLabel: "फ़ाइलें", + modulesFilesDescription: "फ़ाइलें खोजने और प्रबंधित करने का मॉड्यूल।", + tribeSectionFiles: "टॉरेंट", + fileDescription: "विवरण", + fileDownload: "डाउनलोड", + fileTypeLabel: "प्रकार", torrentsTitle: "टॉरेंट", torrentsDescription: "अपने नेटवर्क में टॉरेंट खोजें और प्रबंधित करें।", torrentAllSectionTitle: "टॉरेंट", @@ -3457,47 +4435,91 @@ module.exports = { torrentTagsLabel: "टैग", torrentTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", torrentSizeLabel: "आकार", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "टोरेंट बनाएँ", + torrentFromContentMissing: "फ़ाइल अभी इस नोड पर उपलब्ध नहीं है। थोड़ी देर बाद फिर कोशिश करें।", + torrentDownloads: "डाउनलोड", + torrentFetchOasis: "Oasis में डाउनलोड करें", + torrentSaveFile: "फ़ाइल सहेजें", + torrentSeeds: "सीड", + seedsLabel: "सीड", + seedAction: "समर्थन करें", + torrentSourceFile: "फ़ाइल", + torrentStateQueued: "कतार में", + torrentStateProbing: "सीड खोजे जा रहे हैं", + torrentStateDownloading: "डाउनलोड हो रहा है", + torrentStateDone: "पूरा हुआ", + torrentStateFailed: "विफल", + torrentStateNoSource: "अभी कोई सीड ऑनलाइन नहीं, प्रतीक्षा में", + torrentStateCancelled: "रद्द", + torrentNoDownloads: "अभी कोई डाउनलोड नहीं।", + torrentOriginalSection: "मूल", + torrentCancel: "रद्द करें", + torrentDownloadButton: "डाउनलोड", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "विवरण", torrentNoFile: "कोई टॉरेंट फ़ाइल नहीं", noTorrents: "कोई टॉरेंट नहीं मिला।", - torrentSearchPlaceholder: "शीर्षक, टैग, लेखक खोजें...", + noFiles: "कोई टॉरेंट नहीं मिला।", + torrentSearchPlaceholder: "टॉरेंट में खोजें...", torrentSearchButton: "खोजें", torrentSortRecent: "सबसे हालिया", torrentSortOldest: "सबसे पुराने", torrentSortTop: "सबसे अधिक वोट", torrentNoMatch: "कोई मिलान टॉरेंट नहीं।", - torrentMessageAuthorButton: "लेखक को संदेश भेजें", torrentUpdatedAt: "अपडेट किया गया", statsTorrent: "टॉरेंट", + statsFile: "फ़ाइलें", typeTorrent: "टॉरेंट", modulesTorrentsLabel: "टॉरेंट", modulesTorrentsDescription: "टॉरेंट खोजने और प्रबंधित करने का मॉड्यूल।", favoritesFilterTorrents: "टॉरेंट", + favoritesFilterFiles: "फ़ाइलें", favoritesFilterMarket: "मार्केट", + favoritesFilterEvents: "इवेंट्स", + favoritesFilterForum: "फ़ोरम", + favoritesFilterHousing: "आवास", + favoritesFilterJobs: "नौकरियाँ", + favoritesFilterProjects: "परियोजनाएँ", + favoritesFilterReports: "रिपोर्ट्स", + favoritesFilterTasks: "कार्य", + favoritesFilterTransfers: "स्थानांतरण", + favoritesFilterVotes: "मतदान", favoritesFilterShopProducts: "दुकान वस्तुएं", tribeSectionTorrents: "टॉरेंट", tribeCreateTorrent: "टॉरेंट अपलोड करें", - tribeMediaTypeTorrent: "टॉरेंट", + tribeCreateFile: "फ़ाइल अपलोड करें", settingsWishTitle: "इच्छा", - settingsWishDesc: "अपने अवतार की इच्छा कॉन्फ़िगर करें।", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "तय करता है कि आप सारी सामग्री तक कैसे पहुँचते हैं और Oasis नेटवर्क में आपका अनुभव कैसा होता है।", + settingsWishWhole: "मल्टीवर्स", + settingsWishMutuals: "केवल पारस्परिक समर्थन", + settingsWishOnlyLan: "केवल LAN", + settingsWishLocal: "स्थानीय", settingsPmVisibilityTitle: "निजी संदेश", settingsPmVisibilityDesc: "नेटवर्क में निजी संदेश एक्सपोज़र स्तर कॉन्फ़िगर करें।", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "मल्टीवर्स", + settingsPmVisibilityMutuals: "केवल पारस्परिक समर्थन", + mute: "म्यूट करें", + unmute: "म्यूट हटाएँ", + relationshipMuted: "आपके द्वारा म्यूट", + hideContent: "मेरे लिए छिपाएँ", + unhideContent: "फिर से दिखाएँ", + hiddenByYou: "आपके द्वारा छिपाया गया", + settingsTabContent: "सामग्री", + settingsHiddenTitle: "छिपी हुई सामग्री", + settingsHiddenDesc: "जिन निवासियों को आपने म्यूट किया और जो सामग्री आपने छिपाई।", + settingsMutedLabel: "म्यूट किए गए निवासी", + settingsHiddenLabel: "छिपाई गई सामग्री", + settingsMutedEmpty: "कोई म्यूट नहीं।", + settingsHiddenEmpty: "कुछ भी छिपा नहीं।", + settingsPmVisibilityFollowing: "केवल वे लोग जिन्हें मैं फ़ॉलो करता हूँ", + settingsMentionsVisibilityTitle: "उल्लेख", + settingsMentionsVisibilityDesc: "चुनें कि किसके उल्लेख आप तक पहुँचें।", pmMutualNotice: "आउटबाउंड UX गार्डरेल है। इनबाउंड ध्यान फ़िल्टर है।", - pmBlockedNonMutual: "केवल पारस्परिक समर्थन वाले निवासियों को PM भेज सकते हैं।", inhabitantsPendingFollowsTitle: "लंबित समर्थन अनुरोध", inhabitantsPendingAccept: "स्वीकार", inhabitantsPendingReject: "अस्वीकार", - bxEncrypted: "एन्क्रिप्टेड", - bxEncryptedHexLabel: "सिफरटेक्स्ट (पूर्वावलोकन)", tribeSectionGovernance: "शासन", - tribeSubStatusPublic: "सार्वजनिक", - tribeSubStatusPrivate: "निजी", - tribeSubInheritedPrivate: "निजी (मुख्य जनजाति से विरासत)", tribePadInviteRequired: "पैड तक पहुंच नहीं। निमंत्रण मांगें।", tribeChatInviteRequired: "चैट तक पहुंच नहीं। निमंत्रण मांगें।", tribeGovernanceDesc: "इस जनजाति का आंतरिक शासन।", @@ -3536,209 +4558,195 @@ module.exports = { tribeGovNoRevocations: "अभी तक कोई निरसन नहीं", tribeGovNoHistorical: "अभी तक कोई रिकॉर्ड नहीं", logsTitle: "लॉग", - logsDescription: "नेटवर्क पर अपना अनुभव रिकॉर्ड करें।", - logsReadMore: "और पढ़ें", + logsDescription: "अपने अनुभवों की निजी डायरी रखें।", logsViewTitle: "लॉग", logsColumnType: "प्रकार", logsView: "देखें", - logsManualPrompt: "अपनी लॉग लिखें", logsUpdateButton: "अपडेट करें", - logsEditTitle: "प्रविष्टि संपादित करें", - logsCreateDescription: "अपना अनुभव रिकॉर्ड करें...", + logsEditTitle: "लॉग अपडेट करें", logsCreateTitle: "प्रविष्टि बनाएँ", logsWriteButton: "लिखें", logsTextPlaceholder: "अपने अनुभव वर्णित करें...", - logsTextField: "पाठ", - logsLabelPlaceholder: "लेबल...", - logsLabelField: "अपनी लॉग लिखें (लेबल)", - logsAiDisabledWarn: "एआई लिखित लॉग के लिए /modules में एआई मॉड्यूल सक्षम करें।", - logsAiContextValue: "ब्लॉकचेन दिन", - logsAiContext: "संदर्भ", - logsAiModStatus: "AImod", - logsModeApply: "लागू करें!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "AI सहायक", logsModeManual: "मैन्युअल", logsModeAI: "एआई", - logsColumnDelete: "हटाएँ", - logsColumnEdit: "संपादित करें", logsColumnLog: "लॉग", logsColumnDate: "दिनांक", logsDelete: "हटाएँ", - logsEdit: "संपादित करें", + logsEdit: "अपडेट करें", logsFilterAlways: "हमेशा", logsFilterYear: "पिछला वर्ष", logsFilterMonth: "पिछला महीना", logsFilterWeek: "पिछला सप्ताह", logsFilterToday: "आज", - logsFilterRecent: "हाल के", - logsFilterAll: "सभी", + logsComments: "टिप्पणियाँ", + logsAuthorEntries: "प्रविष्टियाँ", logsCreate: "प्रविष्टि बनाएँ", logsExport: "लॉग निर्यात करें", - logsExportOne: "निर्यात", + logsExportOne: "लॉग निर्यात करें", logsViewDetails: "विवरण देखें", logsGenerateButton: "पाठ जनरेट करें", logsSearchText: "लॉग में खोजें...", logsSearchAnyType: "कोई भी प्रकार", logsSearchButton: "खोजें", logsEmpty: "कोई प्रविष्टियाँ नहीं।", + logsNothingNew: "दर्ज करने के लिए कोई नई गतिविधि नहीं है।", modulesLogsLabel: "लॉग", - modulesLogsDescription: "(एआई सहायक के माध्यम से) अपने अनुभव रिकॉर्ड करने के लिए मॉड्यूल।", + modulesLogsDescription: "अपने अनुभवों की निजी डायरी रखने का मॉड्यूल।", statsLogsTitle: "लॉग", - statsLogsEntries: "प्रविष्टियाँ", typeLog: "लॉग", - blockchainCycle: "चक्र", larpTitle: "L.A.R.P.", larpDescription: "सहयोगात्मक प्रयोग के लिए एक लाइव-एक्शन रोल-प्लेइंग परत।", + larpNoHousesMatch: "आपकी खोज से कोई हाउस मेल नहीं खाता।", + larpSearchPlaceholder: "L.A.R.P. में खोजें...", larpAllHouses: "गृह:", larpFilterRuling: "शासन", larpFilterHouses: "गृह", - larpFilterRules: "FAQ", + larpFilterRules: "सामान्य प्रश्न", larpRulesTitle: "L.A.R.P. FAQ", - larpRulesEntryTitle: "प्रारंभिक गृह", larpRulesEntryText: "हर निवासी डिफ़ॉल्ट रूप से ACADEMIA में शुरू होता है। ACADEMIA से, \"गृह में शामिल हों\" पैनल में कोई गृह चुनें: इससे उसका प्रवेश परीक्षण खुलता है। 10 प्रश्नों के उत्तर दें और सबमिट करें। यदि आप सीमा (7/10) पार करते हैं, तो आपको स्वचालित रूप से उस गृह में स्वीकार कर लिया जाता है; नहीं तो अस्वीकार कर ACADEMIA में रखा जाता है। दोनों ही मामलों में सिस्टम आपके OasisID और प्रयास के चक्र को दर्ज करता है और 30 चक्रों के कूलडाउन समाप्त होने तक पुनः प्रयास से रोकता है।", - larpRulesLeaveText: "किसी भी गृह के सदस्य अपने गृह पृष्ठ से कभी भी ACADEMIA में लौट सकते हैं; इससे उनकी सदस्यता रीसेट हो जाती है, परंतु परीक्षण कूलडाउन समाप्त नहीं होता।", - larpRulesGovernanceTitle: "शासन दीवार", larpRulesGovernanceText: "अपने चक्र के दौरान, शासक गृह \"शासन\" बैज पहनता है और एकमात्र है जिसकी दीवार शासन टैब के तहत सार्वजनिक रूप से दिखाई जाती है।", - larpRulesSignTitle: "L.A.R.P. प्रतीक", + larpRulesWallText: "हर घर की एक दीवार होती है जहाँ उसके सदस्य लिखते हैं। किसी घर की दीवार निजी होती है, सिवाय शासक घर और ACADEMIA की, जिन्हें कोई भी पढ़ सकता है।", larpRulesSignText: "निवासी इसे चालू कर सकते हैं (/profile/edit > सेंसर) ताकि उनके वर्तमान गृह की छवि उनके प्रोफ़ाइल, लेखक और निवासी कार्ड पर मुहर के रूप में दिखे। मुहर गृह पृष्ठ से जुड़ी होती है।", larpPostsTitle: "दीवार", larpPostsEmpty: "अभी कोई पोस्ट नहीं।", - larpPostLabel: "नई पोस्ट", larpPostPlaceholder: "यह गृह क्या कहना चाहता है?", - larpPostPublic: "सार्वजनिक (किसी को भी दिखे, अन्यथा केवल सदस्यों को)", larpPostSubmit: "प्रकाशित करें", - larpPostMembersOnly: "केवल सदस्य", larpCycleLabel: "चक्र:", - audioBackToBcs: "Back to BCS", + audioBackToBcs: "BCS पर वापस", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", - audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", - audioTranscodeDetailTitle: "Transcode", - audioTranscodeStegoEmpty: "(none)", - audioTranscodeStegoMessage: "TEXT", - audioTranscodeStegoNotFound: "No steganographic payload could be decoded from this audio.", - audioTranscodeStegoOasisId: "By", - audioTranscodeStegoTimestamp: "Generated at", + audioTranscodeButton: "ट्रांसकोड करें", + audioTranscodeCompositionEmpty: "इस ऑडियो में कोई सहेजी गई ब्लॉकचेन रचना नहीं है।", + audioTranscodeDetailDescription: "एम्बेड किया गया पेलोड और ब्लॉकचेन का मूल रचना-मानचित्र डिकोड करें।", + audioTranscodeDetailTitle: "ट्रांसकोड", + audioTranscodeStegoEmpty: "(कोई नहीं)", + audioTranscodeStegoMessage: "टेक्स्ट", + audioTranscodeStegoNotFound: "इस ऑडियो से कोई स्टेग्नोग्राफ़िक पेलोड डिकोड नहीं हो सका।", + audioTranscodeStegoTimestamp: "बनाया गया", audioTranscodeStegoUnknown: "—", - bankExchangeEcoTaxAnnual: "ECOin Taxes (annual)", - bankExchangeEcoTaxMonthly: "ECOin Taxes (monthly)", - bankOverviewYourTaxes: "Your taxes", - bankRulesAlpha: "Alpha (max share of pub balance per epoch)", + bankExchangeEcoTaxAnnual: "ECOin कर (वार्षिक)", + bankExchangeEcoTaxMonthly: "ECOin कर (मासिक)", + bankOverviewYourTaxes: "कुल कर", + bankRulesAlpha: "Alpha (प्रति युग pub शेष का अधिकतम हिस्सा)", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", - bankRulesArchTaxRate: "Rate", - bankRulesArchTaxTitle: "ARCH Tax", - bankRulesCapPerEpoch: "Cap per epoch (absolute)", - bankRulesCapUser: "Cap per user per epoch", - bankRulesCarbonFactor: "Carbon factor", + bankRulesArchTaxNote: "ARCH कर नेटवर्क संग्रह को बढ़ाने और बनाए रखने की दीर्घकालिक लागत दर्शाता है: नेटवर्क में आपका डेटा जितना पुराना है, रखरखाव लागत में आपका हिस्सा उतना बड़ा है।", + bankRulesArchTaxRate: "दर", + bankRulesArchTaxTitle: "ARCH कर", + bankRulesCapPerEpoch: "प्रति युग सीमा (निरपेक्ष)", + bankRulesCapUser: "प्रति उपयोगकर्ता प्रति युग सीमा", + bankRulesCarbonFactor: "कार्बन गुणक", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", - bankRulesChipTitle: "ECO Tax chip color band", + bankRulesChipNote: "हर आइटम की चिप का रंग नेटवर्क में देखे गए सबसे बड़े संदेश के सापेक्ष तय होता है और सक्रिय निवासियों की संख्या से नरम किया जाता है — बड़ा नेटवर्क भार बाँटता है और ज़्यादा आइटम को हरे दायरे में ले आता है।", + bankRulesChipTitle: "ECO कर चिप का रंग दायरा", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", - bankRulesEcoTaxRate: "Rate", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesEpochKind: "Epoch granularity", - bankRulesFloor: "Floor per user (gross)", - bankRulesGraceDays: "Claim grace period", + bankRulesEcoTaxNote: "युग (epoch) का आवंटन गणना करते समय ECO कर सकल UBI से काटा जाता है। काटे गए ECO इस युग में फिर से नहीं बाँटे जाते — वे pub के शेष में रहते हैं और reserveMin / alpha सीमाओं के ज़रिए अगले युग के कोष को पोषित करते हैं।", + bankRulesEcoTaxRate: "दर", + bankRulesEcoTaxTitle: "ECO कर", + bankRulesEpochKind: "युग की सूक्ष्मता", + bankRulesFloor: "प्रति उपयोगकर्ता न्यूनतम (सकल)", + bankRulesGraceDays: "दावे की अनुग्रह अवधि", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", - bankRulesKarmaTitle: "Karma (user engagement score)", + bankRulesKarmaNote: "कर्म आपकी गतिविधियों (पोस्ट, वोट, संसद/अदालतों में भागीदारी आदि) से समय के साथ घटने वाले गुणक सहित निकाला जाता है, जिसमें से आपकी फ़ीड से पैदा हुए कार्बन ग्राम घटा दिए जाते हैं। आपकी सामग्री को जो मिलता है वह भी गिना जाता है: सकारात्मक राय, वोट, प्रतिक्रियाएँ और प्ले कर्म बढ़ाते हैं, और जब कई निवासी सहमत होते हैं तो नकारात्मक राय या वोट उसे घटाते हैं।", + bankRulesKarmaTitle: "कर्म (उपयोगकर्ता सहभागिता स्कोर)", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", - bankRulesPoolTitle: "Monthly pool", - bankRulesReserveMin: "Reserve minimum (kept in pub balance)", + bankRulesPoolTitle: "मासिक कोष", + bankRulesReserveMin: "न्यूनतम आरक्षित (pub शेष में रखा जाता है)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", - bankRulesShareTitle: "Per-user share", + bankRulesShareTitle: "प्रति उपयोगकर्ता हिस्सा", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankRulesWMax: "Maximum weight (w_max)", - bankRulesWMin: "Minimum weight (w_min)", - bankTaxes: "Taxes", - bankTaxesAnnualEcoin: "ECO Tax (annual)", - bankTaxesArchTaxAnnual: "ARCH Tax (annual)", - bankTaxesArchTaxFirstBlock: "Your first block age", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxLifetime: "ARCH Tax (lifetime)", - bankTaxesArchTaxMonthly: "ARCH Tax (monthly)", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxRate: "Rate", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesArchTaxUserAmount: "Your ARCH tax (price to return)", - bankTaxesEcoTaxLifetime: "ECO Tax (lifetime)", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesLookupAuthor: "Author", - bankTaxesLookupButton: "Inspect", - bankTaxesLookupCarbon: "Carbon footprint", - bankTaxesLookupEcoin: "ECO Tax", - bankTaxesLookupNotFound: "No block found in your local feed with that ID.", - bankTaxesLookupPlaceholder: "Block ID (e.g. %abc...=.sha256)", - bankTaxesLookupSize: "Size", - bankTaxesLookupTitle: "Inspect a block", - bankTaxesLookupType: "Type", - bankTaxesMonthlyEcoin: "ECO Tax (monthly)", - bankTaxesRate: "Rate", - bankTaxesSpan: "Sampling span", - bankTaxesTotalAnnual: "Total (annual)", - bankTaxesTotalBlocks: "Total blocks", - bankTaxesTotalBytes: "Total bytes", - bankTaxesTotalCarbon: "Total carbon", - bankTaxesTotalEcoin: "Total ECO tax (lifetime)", - bankTaxesTotalLifetime: "Total (lifetime)", - bankTaxesTotalMonthly: "Total (monthly)", - bankTaxesTypesApply: "Set my taxes", - bankTaxesTypesLabel: "Select which taxes you want to pay", - bankTaxesUserAmount: "Your ECO tax (price to return)", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", - blockchainBackToBlockexplorer: "Back to blockexplorer", - blockchainBlockNotAvailable: "This block is not available in your local feed. It may belong to a peer you are not yet replicating from.", - ecoTaxLabel: "ECO Tax", - gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - melodyAllEmpty: "No BCS compositions from other inhabitants yet.", - melodyByLabel: "By", - melodyNoteUnavailable: "block not in your local feed", - melodyUploadBcsButton: "Publish", - melodyUploadBcsNameNote: "The audio will be published with the auto-generated name", - melodyUploadBcsTitle: "Publish", - melodyUploadStegoLabel: "Hidden message (steganography)", - melodyUploadStegoMaxLabel: "max 280 characters", - melodyUploadStegoPlaceholder: "Optional.", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", - statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", - statsPad: "Pads", - statsPadEntry: "Pad entries", - statsTombstoneEmpty: "No tombstones in the network yet.", - typeGameScore: "GAME SCORES", + bankRulesWMax: "अधिकतम भार (w_max)", + bankRulesWMin: "न्यूनतम भार (w_min)", + bankTaxes: "कर", + bankTaxesAnnualEcoin: "ECO कर (वार्षिक)", + bankTaxesArchTaxAnnual: "ARCH कर (वार्षिक)", + bankTaxesArchTaxFirstBlock: "आपके पहले ब्लॉक की आयु", + bankTaxesArchTaxFootprintNote: "ARCH कर नेटवर्क संग्रह को बढ़ाने और बनाए रखने की लागत दर्शाता है। इसकी गणना आपके पहले प्रकाशित ब्लॉक और नेटवर्क के सबसे नए ब्लॉक के बीच के समय-अंतराल से होती है — आपका इतिहास संग्रह में जितना लंबा रहा है, रखरखाव लागत में आपका हिस्सा उतना बड़ा है।", + bankTaxesArchTaxLifetime: "ARCH कर (आजीवन)", + bankTaxesArchTaxMonthly: "ARCH कर (मासिक)", + bankTaxesArchTaxNoteBold: "ARCH कर को सीधे घटाने", + bankTaxesArchTaxNoteIntro: "आपका ARCH कर उस अधिशेष से काटा जाता है जो नेटवर्क आपके UBI के ऊपर पैदा करता है; UBI का मूल मान एक अटल न्यूनतम के रूप में तय है। कर कभी भी उस निश्चित राशि से नहीं काटा जाता जो हर निवासी को UBI के रूप में मिलती है। काटी गई राशि धन-पुनर्वितरण एल्गोरिदम को पोषित करती है और दूसरे निवासियों के UBI दावों के ज़रिए उन तक वापस पहुँचती है। नए निवासी और वे जो विशेष रूप से संग्रहण, प्रतिकृति और रखरखाव के कामों में लगे हैं, संभवतः आपकी ओर से नेटवर्क संग्रह को स्वस्थ रखने का कोई काम करेंगे। या वे ख़ुद को ", + bankTaxesArchTaxNoteOutro: " में लगा सकते हैं, छँटाई समझौतों, अनावश्यक डेटा के दोहराव हटाने और साझा रखरखाव ढाँचे के ज़रिए, और नेटवर्क की सहमति उनके निरंतर योगदान की माँग कर सकती है।", + bankTaxesArchTaxRate: "दर", + bankTaxesArchTaxTitle: "ARCH कर", + bankTaxesArchTaxUserAmount: "आपका ARCH कर (लौटाने योग्य राशि)", + bankTaxesEcoTaxLifetime: "ECO कर (आजीवन)", + bankTaxesEcoTaxTitle: "ECO कर", + bankTaxesLookupAuthor: "लेखक", + bankTaxesLookupButton: "निरीक्षण करें", + bankTaxesLookupCarbon: "कार्बन फ़ुटप्रिंट", + bankTaxesLookupEcoin: "ECO कर", + bankTaxesLookupNotFound: "आपकी स्थानीय फ़ीड में उस ID का कोई ब्लॉक नहीं मिला।", + bankTaxesLookupPlaceholder: "ब्लॉक ID (जैसे %abc...=.sha256)", + bankTaxesLookupSize: "आकार", + bankTaxesLookupTitle: "ब्लॉक का निरीक्षण करें", + bankTaxesLookupType: "प्रकार", + bankTaxesMonthlyEcoin: "ECO कर (मासिक)", + bankTaxesRate: "दर", + bankTaxesSpan: "नमूना अवधि", + bankTaxesTotalAnnual: "कुल (वार्षिक)", + bankTaxesTotalBlocks: "कुल ब्लॉक", + bankTaxesTotalBytes: "कुल बाइट्स", + bankTaxesTotalCarbon: "कुल कार्बन", + bankTaxesTotalEcoin: "कुल ECO कर (आजीवन)", + bankTaxesTotalLifetime: "कुल (आजीवन)", + bankTaxesTotalMonthly: "कुल (मासिक)", + bankTaxesTypesApply: "मेरे कर तय करें", + bankTaxesTypesLabel: "चुनें कि आप कौन-से कर चुकाना चाहते हैं", + bankTaxesUserAmount: "आपका ECO कर (लौटाने योग्य राशि)", + bankTaxesUserNoteChips: "हर आइटम के पास दिखने वाली ECO कर चिप का रंग (हरा / पीला / लाल) आइटम के आकार के अनुसार बदलता है, जिसकी तुलना नेटवर्क पर देखे गए सबसे बड़े ब्लॉक से की जाती है और सक्रिय निवासियों की संख्या से नरम किया जाता है — बड़ा सोलर-पंक नेटवर्क भार बाँटता है और ज़्यादा आइटम को हरे दायरे में ले आता है।", + bankTaxesUserNoteOtherParams: "ECO कर अभी हर ब्लॉक के कार्बन फ़ुटप्रिंट से निकाला जाता है, लेकिन समय के साथ इसमें दूसरे मापदंड भी जुड़ सकते हैं — प्रतिकृति पर ख़र्च हुई ऊर्जा, पीयरों में अनावश्यक भंडारण, ब्लॉब बैंडविड्थ, डिक्रिप्शन की गणना लागत, संबंधित लेन-देन का माइनिंग फ़ुटप्रिंट, या कोई भी अन्य मापने योग्य भार जिसे नेटवर्क महत्व देने पर सहमत हो।", + blockchainBackToBlockexplorer: "ब्लॉक एक्सप्लोरर पर वापस", + blockchainBlockNotAvailable: "यह ब्लॉक आपकी स्थानीय फ़ीड में उपलब्ध नहीं है। यह किसी ऐसे पीयर का हो सकता है जिससे आप अभी प्रतिकृति नहीं करते।", + ecoTaxLabel: "ECO कर", + gamesHallDate: "तिथि", + gamesNewRecord: "नया रिकॉर्ड!", + indexingRefreshNote: "यह पृष्ठ हर 10 सेकंड में ताज़ा होता है।", + indexingMessage: "Oasis वितरित नोड्स के नेटवर्क को सिंक कर रहा है। कृपया प्रतीक्षा करें।", + indexingTitle: "समन्वयन जारी", + melodyAllEmpty: "अभी तक दूसरे निवासियों की कोई BCS रचना नहीं।", + melodyNoteUnavailable: "ब्लॉक आपकी स्थानीय फ़ीड में नहीं है", + melodyUploadBcsButton: "प्रकाशित करें", + melodyUploadBcsNameNote: "ऑडियो स्वतः बने नाम के साथ प्रकाशित होगा", + melodyUploadBcsTitle: "प्रकाशित करें", + melodyUploadStegoLabel: "छिपा संदेश (स्टेग्नोग्राफ़ी)", + melodyUploadStegoMaxLabel: "अधिकतम 280 अक्षर", + melodyUploadStegoPlaceholder: "वैकल्पिक।", + padsNoItems: "कोई पैड नहीं मिला।", + padVersionHistory: "संस्करण इतिहास", + padAttributionView: "लेखिका के अनुसार", + padVersionView: "देखें", + statsChat: "चैट्स", + statsChatMessage: "चैट संदेश", + statsGameScore: "खेल", + statsWiki: "विकी पृष्ठ", + statsPad: "पैड्स", + statsPadEntry: "पैड प्रविष्टियाँ", + statsTombstoneEmpty: "नेटवर्क में अभी तक कोई टॉम्बस्टोन नहीं।", + typeGameScore: "गेम स्कोर", bankTaxesLookupNote: "अपने स्थानीय फ़ीड में देखने के लिए ब्लॉक ID दर्ज करें।", bankTaxesUserNoteChipsClick: "ब्लॉकexplorer में इसके ब्लॉक का निरीक्षण करने के लिए किसी भी चिप पर क्लिक करें।", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "आमंत्रण कोड", larpRulesInviteEntryText: "गृह सदस्य आमंत्रण कोड बना सकते हैं जो गृह तक सीधी पहुंच देते हैं।", larpRulesOneHouseText: "आप किसी भी समय अधिकतम एक गृह से संबंध रख सकते हैं। यदि आप किसी से संबंधित नहीं हैं, तो आप ACADEMIA में हैं। हर गैर-ACADEMIA गृह पृष्ठ अपने सदस्यों को \"गृह छोड़ें\" बटन दिखाता है जो सदस्यता को ACADEMIA पर रीसेट कर देता है। छोड़ने से परीक्षण कूलडाउन समाप्त नहीं होता।", - larpRulesOneHouseTitle: "एक समय में एक गृह", - larpRulesTestEntry: "WILL परीक्षण", - larpRulesTestEntryText: "प्रत्येक विकल्प एक या अधिक गृहों को वज़न देता है; जमा करने पर, सिस्टम अंकों को जोड़ता है और आपको सबसे अधिक अंक वाले गृह में स्वत: स्वीकार करता है।", + larpRulesTestEntryText: "WILL परीक्षण में हर विकल्प एक या अधिक घरों को भार देता है; भेजने पर सिस्टम अंक जोड़ता है और सबसे अधिक अंक वाले घर में आपको स्वतः प्रवेश देता है।", larpWillTestHint: "पूरा होने पर, आपको वह गृह सौंपा जाएगा जो आपके उत्तरों के साथ सबसे अच्छा मेल खाता है। आपके व्यक्तिगत उत्तरों को स्थानीय रूप से संसाधित किया जाता है और कभी संग्रहीत नहीं किया जाता — केवल परिणामी गृह असाइनमेंट आपकी फ़ीड पर प्रकाशित होता है।", larpWillTestTitle: "WILL परीक्षण", - settingsLanDesc: "इस पीयर को समय-समय पर उसी स्थानीय नेटवर्क में अन्य Oasis इंस्टेंस को घोषित करें। UDP प्रसारण रोकने के लिए अक्षम करें।", + settingsLanDesc: "इस पीयर को समय-समय पर उसी स्थानीय नेटवर्क में अन्य Oasis इंस्टेंस को घोषित करें।", settingsLanEnable: "LAN प्रसारण सक्षम करें", settingsLanTitle: "LAN प्रसारण", settingsReplicationTitle: "प्रतिकृति", settingsReplicationDesc: "सामग्री की प्रतिकृति बनाते समय आपके पीयर द्वारा आपके अपने फ़ीड से आगे जाने वाले हॉप की संख्या कॉन्फ़िगर करें।", settingsReplicationHopsLabel: "हॉप", + settingsBlobCacheTitle: "मीडिया कैश", + settingsBlobCacheDesc: "दूसरे निवासियों की सामग्री इस डिवाइस पर रखी जाती है। चुनें कि वह कितनी जगह ले सकती है।", + settingsBlobCacheUsage: "वर्तमान उपयोग", + settingsBlobCacheFiles: "फ़ाइलें", + settingsBlobCacheLimit: "सीमा", + settingsBlobCacheUnlimited: "असीमित", + settingsBlobCacheCleanNow: "अभी साफ़ करें", + settingsBlobCacheCleaned: "{n} फ़ाइलें हटाई गईं, {size} खाली हुआ।", aiApproveTagsLabel: "टैग (अल्पविराम से अलग)", aiApproveTagsPlaceholder: "जैसे oasis, शासन, पारिस्थितिकी", aiApproveRatingLabel: "रेटिंग", @@ -3749,31 +4757,25 @@ module.exports = { aiExchangeMarkHelpful: "+1 सहायक", larpCyclesUntilRuling: "अगला शासन चक्र", larpVisitTribe: "जनजाति देखें", + larpStartJourney: "यात्रा शुरू करें", larpGoverning: "शासन:", + larpStartHere: "यहाँ से शुरू करें:", larpMyHouse: "मेरा गृह:", larpMembersCount: "सदस्य", - larpMembersTitle: "सदस्य", - larpNoMembers: "अभी कोई सदस्य नहीं।", larpRolesLabel: "भूमिकाएँ", larpFunctionLabel: "कार्य", larpMonthLabel: "शासन चक्र", larpLeaveToAcademia: "ACADEMIA लौटें", - larpAcademiaJoinTitle: "गृह में शामिल हों", - larpAcademiaJoinHint: "मनोवैज्ञानिक प्रोफ़ाइल परीक्षण दें ताकि आपको आपके लिए सबसे उपयुक्त गृह सौंपा जाए, या किसी गृह सदस्य द्वारा साझा किया गया आमंत्रण कोड भुनाएँ।", - larpTakeTestButton: "प्रोफ़ाइल परीक्षण दें", + larpAcademiaJoinTitle: "L.A.R.P. गृह", larpInviteRedeem: "गृह में शामिल हों", larpInviteCreate: "आमंत्रण कोड बनाएँ", larpInvitePlaceholder: "आमंत्रण कोड", larpInviteBannerTitle: "नया आमंत्रण कोड", larpInviteBannerHint: "इस कोड को ACADEMIA में किसी के साथ साझा करें। यह 30 चक्रों या एक उपयोग के बाद समाप्त हो जाता है।", - larpTestAssignedHouse: "नियत गृह", larpTestRankingTitle: "गृह के अनुसार स्कोर", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "गृह आमंत्रण कोड", - invitesHouseJoinButton: "गृह में शामिल हों", larpLastAttemptTitle: "आपका पिछला प्रयास", larpLastAttemptHouse: "गृह", - larpLastAttemptResult: "परिणाम", larpLastAttemptWhen: "कब", larpLastAttemptCooldown: "अगला प्रयास", larpTestTitle: "प्रवेश परीक्षण", @@ -3782,37 +4784,14 @@ module.exports = { larpTestCooldownActive: "अगला परीक्षण उपलब्ध", larpTestCooldownDays: "चक्र", larpTestResultTitle: "परीक्षण परिणाम", - larpTestPassed: "परीक्षण पास — स्वागत है!", - larpTestFailed: "परीक्षण पास नहीं हुआ", larpTestScore: "स्कोर", larpTestNextAttempt: "आप 30 चक्रों के बाद एक और परीक्षण दे सकते हैं।", larpGoToHouse: "अपने गृह पर जाएँ", larpBackToAcademia: "ACADEMIA लौटें", - larpBackToHouses: "◀ गृह", larpBadgeYou: "आप", larpBadgeRuling: "शासन", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Oasis की लाइव-एक्शन रोल-प्लेइंग परत (9 गृह) के लिए मॉड्यूल।", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - ecoinTaxLabel: "ECOin Tax", - bankTaxesNetworkTitle: "Network Taxes", - bankTaxesUserTitle: "Your taxes", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - statsEcoTaxTitle: "ECO Tax", - audioTranscodeTitle: "BCS Transcode", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - audioTranscodeStegoTitle: "Embedded in the waveform", - audioBackToAudio: "Back to audio", - audioAuthorLabel: "Author", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "लाइव-एक्शन रोल-प्लेइंग परत का मॉड्यूल।", larpProfileQ1: "जब आप किसी जटिल समस्या का सामना करते हैं, तो पहले क्या करते हैं?", larpProfileQ1O1: "सहमति खोजने के लिए दूसरों से बात करना", larpProfileQ1O2: "परीक्षण के लिए प्रोटोटाइप बनाना", @@ -3886,6 +4865,51 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "अपने इंटरफ़ेस के लिए UX नेविगेशन मोड चुनें।", uxModeMenus: "ब्लॉक", + uxModeChats: "वार्तालाप", + uxModeFeed: "माइक्रोब्लॉगिंग", + feedTrendingTitle: "ट्रेंडिंग टैग", + feedActiveTitle: "सक्रिय निवासी", + cnBrandSub: "मुक्त · P2P · संघीय", + cnSyncedPeers: "सिंक किए गए पीयर", + cnLicense: "लाइसेंस", + cnNotAccessible: "यह सामग्री इस समय उपलब्ध नहीं है।", + cnUntitled: "बिना शीर्षक", + cnSearchPlaceholder: "खोजें…", + cnHubTitle: "Clearnet हब", + cnHubDescription: "इस Oasis नेटवर्क के निवासियों द्वारा साझा की गई सार्वजनिक सामग्री।", + cnHubEmpty: "अभी तक Clearnet पर कोई सार्वजनिक सामग्री साझा नहीं की गई है।", + cnPublicContent: "सामग्री", + cnTribesContent: "जनजातियाँ", + cnNetworkContent: "नेटवर्क", + cnCategoryEmpty: "इस श्रेणी में कोई सामग्री नहीं।", + cnInhabitantEmpty: "इस निवासी ने अभी तक Clearnet पर कोई सामग्री प्रकाशित नहीं की है।", + cnPrevPage: "← पिछला", + cnNextPage: "अगला →", + cnPageOf: "पृष्ठ {page} / {pages}", + cnMoreInfo: "अधिक जानकारी →", + cnOpenCourse: "खुला कोर्स", + cnAuction: "नीलामी", + cnExchange: "विनिमय", + cnRssDescription: "इस हब के निवासियों द्वारा प्रकाशित", + cnKindAudio: "ऑडियो", + cnKindBlog: "ब्लॉग", + cnKindBookmark: "बुकमार्क", + cnKindDocument: "दस्तावेज़", + cnKindEvent: "इवेंट", + cnKindFeed: "फ़ीड", + cnKindImage: "छवि", + cnKindJob: "नौकरी", + cnKindMarket: "बाज़ार", + cnKindPodcast: "पॉडकास्ट", + cnKindProject: "प्रोजेक्ट", + cnKindCourse: "कोर्स", + cnKindShop: "दुकान", + cnKindTorrent: "टॉरेंट", + cnKindFile: "फ़ाइल", + cnKindVideo: "वीडियो", + cnKindWiki: "विकी", + cnKindRoom: "रूम", + cnKindMap: "मानचित्र", uxModeAINav: "AI", } diff --git a/src/client/assets/translations/oasis_it.js b/src/client/assets/translations/oasis_it.js index a71a93f..c84d3af 100644 --- a/src/client/assets/translations/oasis_it.js +++ b/src/client/assets/translations/oasis_it.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { it: { - bbAdd: "Aggiungi", - bbQuickActions: "Azioni rapide", - bbPickTitle: "Fissa un modulo", - bbPickDesc: "Scegli un modulo per lo spazio + nella barra inferiore.", - bbPickNone: "Nessuno", - bbManage: "Modifica", - bbYourBar: "La tua barra", - bbRemove: "Rimuovi", - bbFull: "Massimo raggiunto", - bbDone: "Fatto", - activityChangeFilter: "Cambia filtro", - emptyConnectHint: "Connettiti a un pub per sincronizzarti con la rete.", - emptyConnectCta: "Connettiti a un pub", - menuCommunity: "Comunità", - activityGroupCommunity: "Comunità e governance", - activityGroupOrg: "Organizzazione", - activityGroupComm: "Comunicazione", - activityGroupEconomy: "Economia", - activityGroupMedia: "Contenuti e media", - activityGroupOther: "Altri", - languageName: "Italiano", shopOrderStatusPaid: "Pagamento ricevuto", shopOrderStatusReceived: "Ricevuto", shopOrderMarkPaid: "Segna pagamento ricevuto", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "Acquisti", shopMyOrdersDescription: "I tuoi ordini di acquisto.", shopMyOrdersEmpty: "Non hai ancora acquisti.", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PM Venditore", shopOrderSeller: "Venditore", - shopOrderPaymentConcept: "Pagamento", shopOrderInvoice: "Fattura", shopOrderInvoiceConcept: "Fattura", shopOrderStatusPending: "In attesa", shopOrderStatusAccepted: "Accettato", shopOrderStatusRejected: "Rifiutato", shopOrderStatusShipped: "Spedito", - shopOrderStatusDelivered: "Consegnato", shopOrderAccept: "Accetta", shopOrderReject: "Rifiuta", shopOrderMarkShipped: "Segna spedito", - shopOrderMarkDelivered: "Segna consegnato", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "Contratto", + shopOrderPmBuyer: "PM Acquirente", shopOrderContractConcept: "Ordine negozio", shopOrdersPending: "Ordini in sospeso", all: "Tutti", @@ -53,7 +28,7 @@ module.exports = { recent: "Recenti", anonymous: "Anonimo", unnamed: "Anonimo", - authorLabel: "AUTORE", + authorLabel: "Autore", delete: "Elimina", save: "Salva", vote: "Vota", @@ -75,18 +50,17 @@ module.exports = { viewJson: "Vedi JSON", matchScore: "Punteggio di corrispondenza", preferencesLabel: "Preferenze", - inhabitantProfileTitle: "Profilo abitante", + inhabitantProfileTitle: "Abitante", contentWarningLabel: "Avviso sul contenuto", invalidPost: "Contenuto non valido", invalidPostHint: "Questo messaggio ha testo vuoto/non valido.", - spreadBy: "Da", spreadChron: "Diffusione", spreaded: "Diffuso", - spreadMore: "altro", spreadContentUnavailable: "Contenuto non ancora disponibile (replica in sospeso)", filteredByTag: "Filtrato per tag", filteredByTagTitle: "Filtrato per tag", feedPublishedSuccess: "Feed pubblicato con successo!", + feedMastodonFailed: "Pubblicato in Oasis, ma non è stato possibile inviarlo a Mastodon.", tribeFeedSent: "Messaggio inviato con successo!", tribeContentEncrypted: "Contenuto cifrato", tribeTorrentsEmpty: "Ancora nessun torrent.", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "Nessuna gravità", calendarDeleteDate: "Elimina data", calendarIntervalUntil: "Fino a", - bookmarkCategory: "Categoria", bookmarkLastVisit: "Ultima visita", profileClearnetBookmarksLabel: "Segnalibri", - forumFilterHot: "Caldi", invitesPort: "Porta", - currentlyUnrecheable: "ERRORE!", - peerHostValidation: "IPv4 valido (es. 192.168.1.100) o hostname (es. pub.example.com)", + peerHostValidation: "IPv4 valido (es. 192.168.1.100) o hostname (es. pub.solarnethub.com)", peerPortValidation: "Porta 1-65535", peerKeyValidation: "Chiave pubblica SSB ed25519 (@<44 caratteri base64>=.ed25519)", inboxJobUnsubscribedTitle: "Disiscrizione dalla tua offerta di lavoro", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "Descrizione attività", exportPasswordLabel: "Password di esportazione", importPasswordPlaceholder: "Inserisci la password", - searchCasesPlaceholder: "Cerca casi…", + searchCasesPlaceholder: "Cerca in Tribunali...", shopBuyDeliveryAddressPlaceholder: "Indirizzo di consegna", shopBuyNotesPlaceholder: "Note", bankTaxesUserNoteBold: "ridurre direttamente la tassa ECO", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "Sostieni il caso", courtsSettlementAcceptBtn: "Accetta l’accordo", courtsSupportCount: "Sostenitori", - courtsVerdictVoteTitle: "Vota sul verdetto", - courtsVerdictVoteLabel: "Voto sul verdetto", - courtsVerdictVoteAccept: "Accetta", - courtsVerdictVoteReject: "Rifiuta", - courtsVerdictVoteSubmit: "Invia voto", reportsSetStatus: "Imposta stato", reportsStatusClosed: "CHIUSO", statsAvgPerInhabitant: "Media per abitante", statsCarbonMaxAnnual: "Stima max annuale", statsCarbonNetwork: "Totale rete", statsCarbonOfEstMax: "della capacità max stimata", + statsCarbonYearProgress: "dell'anno trascorso", + statsNetworkTitle: "Rete", + statsStorageTitle: "Archiviazione", + statsEcoTaxTitle: "Tassa ECO", + statsAccountTitle: "Account", + statsAveragesTitle: "Medie", statsCarbonOfNetwork: "del totale della rete", statsCarbonUser: "La tua impronta", statsContentCountColumn: "Conteggio", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "Tag principali", statsTopTypesTitle: "Tipi di contenuto principali", statsTotalMsgs: "Messaggi totali", - feedViewDetails: "Vedi dettagli", - eventFileLabel: "Allega immagine", - taskFileLabel: "Allega immagine", - courtsRespondentRequired: "L'ID del convenuto è obbligatorio", extended: "Multiverso", extendedDescription: [ "Quando supporti qualcuno, puoi scaricare i post degli abitanti che supportano, e questi appaiono qui, ordinati per data.", @@ -215,47 +183,76 @@ module.exports = { ], profile: "Avatar", inhabitants: "Abitanti", - peersReplicatedFeeds: "Feed replicati", + peersReplicatedFeeds: "Replicati", + peersTorColumn: "TOR", + peersTorYes: "Raggiungibile tramite Tor", + peersTorNo: "Non raggiungibile tramite Tor", + peersOasisId: "ID", + peersOasisVersion: "Versione", graphos: "Graphos", graphosDescription: "Mappa interattiva della rete attorno a te.", + graphosFederation: "Federazione", + graphosFedPubs: "PUB", + graphosFedUnreachable: "Irraggiungibili", + graphosFedLinks: "Collegamenti reciproci", + graphosFedIsolated: "Isolati", + graphosFedNoData: "Nessun dato", + graphosFedMutual: "Reciproco", + graphosFedOneWay: "A senso unico", + graphosFedTransport: "Rete", + graphosFedWith: "Federato con", + graphosZoom: "Ingrandisci", + graphosFedInhabitants: "Abitanti", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "Il tuo nodo non conosce ancora nessun PUB.", graphosViewingGraphOf: "Visualizzando", graphosBackToMine: "← La mia rete", graphosShowing: "Mostrando", graphosYou: "Tu", graphosTotalNodes: "Nodi totali", - manualMode: "Modalità manuale", mentions: "Menzioni", mentionsDescription: [ - strong("Post che ti @menzionano"), - ", ordinati per data.", + strong("Ogni messaggio che ti @menziona"), + ".", ], - nextPage: "Avanti", - previousPage: "Indietro", noMentions: "Non hai ancora ricevuto @menzioni.", + mentionsSearchPlaceholder: "Cerca in Menzioni...", privateReminders: "PROMEMORIA", + inboxFilterPms: "PM", + inboxFilterNotifications: "Notifiche", + inboxMarkRead: "Segna come letto", + inboxMarkUnread: "Segna come non letto", + inboxUnreadChip: "Non letto", + inboxMarkAllRead: "Segna tutto come letto", + inboxDeleteShown: "Elimina tutto", + inboxArchive: "Archivia", + inboxUnarchive: "Ripristina", + inboxFilterArchived: "Archiviati", + inboxEarlierMessages: "messaggi precedenti", + inboxSortThread: "Per conversazione", + inboxSortRecent: "Più recenti prima", + inboxAllBots: "Bot", + settingsInboxBotsTitle: "Notifiche dell'Inbox", + settingsInboxBotsDesc: "Scegli quali bot possono avvisarti nella tua Inbox. I bot deselezionati vengono nascosti e i loro avvisi non vengono contati.", inboxFiltersLabel: "Filtri:", inboxToggleToMutuals: "Passa a reciproci", inboxToggleToWhole: "Passa a tutti", - privateFrom: "Da", - privateTo: "A", privateDate: "Data", pmReply: "Rispondi", pmReplies: "risposte", - pmNew: "nuove", - pmMarkRead: "Segna come letto", inReplyTo: "IN RISPOSTA A", + forumInThread: "NEL THREAD", pmPreview: "Anteprima", pmPreviewTitle: "Anteprima messaggio", peers: "Peer", searchDescription: "Descrizione", - imageSearch: "Ricerca immagini", searchFromLabel: "Da", searchToLabel: "A", - searchMinOpinionsLabel: "Min opinioni", - searchInhabitantLabel: "Abitante (Oasis ID)", searchQueryLabel: "Ricerca", - searchPerPageLabel: "Risultati per pagina", + searchPerPageLabel: "Risultati", settings: "Impostazioni", + settingsDescription: "Configura l'ambiente della tua rete.", continueReading: "Continua a leggere", moreComments: "altri commenti", readThread: "leggi il resto della discussione", @@ -264,67 +261,46 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Riproduce la melodia della tua blockchain — ogni blocco diventa una nota.', melodyEmpty: 'Nessuna nota ancora — la tua blockchain non ha ancora prodotto blocchi.', - melodyCompositionHelp: 'Ogni blocco della tua blockchain diventa una nota musicale in base al tipo e alla dimensione.', - melodyStatsTitle: 'Suddivisione per tipo', - melodyInhabitantLabel: 'Abitante', melodyTotalBlocks: 'Note', - melodyType: 'Tipo', - melodyCount: 'Blocchi', melodyFilterAll: 'TUTTE', - melodyFilterShare: 'Carica melodia', - melodyShareTitle: 'Condividi la tua melodia', - melodyShareHelp: 'Pubblica un’istantanea della tua melodia attuale per farla ascoltare e remixare ad altri.', - melodyShareTitleLabel: 'Titolo (opzionale)', - melodyShareTitlePlaceholder: 'Un fantasma di blockchain', - melodyShareSubmit: 'Pubblica melodia', - melodyNoShared: 'Nessuna melodia di blockchain ancora.', melodyRegenerate: 'Rigenera', melodyDownload: "Scarica BCS", - profileActivityTitle: 'Attività', - profileActivityMore: 'Vedi tutta l’attività', profileContentSectionTitle: 'Contenuto dell’Avatar', profileContentHelp: 'Scegli quali moduli mostrare nel tuo avatar.', profileSensorsHelp: 'Metriche opzionali mostrate nel tuo avatar.', profileClearnetHelp: 'Moduli accessibili dall’esterno di Oasis.', profileHubAll: 'Tutto', - profileHubTitle: 'Contenuto Pubblico', - footerPulseTitle: 'Pulse', - footerPulsePeers: 'Peer', - footerPulseInbox: 'Inbox', - footerPulseSync: 'Ultima sinc.', - footerPulseEcoValue: 'ECO/h', - footerPulseLastActivity: 'Ultima attività', footerLicenseLabel: 'Licenza', - profileSwitchToOasis: 'Torna a Oasis', - profileSwitchToClearnet: 'Immergiti in Clearnet', - profileVisibilityFediverse: "Fediverse", - profileSwitchToFediverse: "Tuffati nel fediverso", - coordLabel: 'Coordinate (es. A3)', - coordPlaceholder: 'Inserisci coordinata', + profileVisibilityFediverse: "Multiverso", contributorsTitle: "Contributori", - pixeliaBy: "di", colorLabel: 'Scegli un colore', paintButton: 'Dipingi!', - invalidCoordinate: 'Coordinata errata', - goToMuralButton: "Vedi murale", totalPixels: 'Pixel totali', modules: "Moduli", modulesViewTitle: "Moduli", modulesViewDescription: "Configura il tuo ambiente attivando o disattivando i moduli.", inbox: "Posta in arrivo", + inboxSearchPlaceholder: "Cerca in Posta in arrivo...", multiverse: "Multiverso", - fediverse: "Fediverse", - fediverseDescription: "Gestisci i tuoi altri account del fediverso, inclusi l'invio e la ricezione di contenuti.", + fediverse: "Multiverso", + fediverseDescription: "Gestisci i tuoi altri account del multiverso, inclusi l'invio e la ricezione di contenuti.", fediverseStatus: "Stato", - fediverseDisconnected: "Fediverso disconnesso. Controlla %LINK% o lo stato della connessione.", - fediverseSettingsTitle: "Fediverse", + fediverseDisconnected: "Multiverso disconnesso. Controlla %LINK% o lo stato della connessione.", + fediverseSettingsTitle: "Multiverso", fediverseInstanceLabel: "Indirizzo", fediverseTokenLabel: "Token di accesso", - fediverseTokenHelp: "Imposta il tuo token di accesso. Sarà usato per gestire il tuo account Mastodon da Oasis.", fediverseConnect: "Connetti", fediverseConnectedAs: "Connesso come", + nextcloudServerLabel: "Server", + nextcloudUserLabel: "Utente", + nextcloudFolderLabel: "Cartella", + nextcloudUploadButton: "Carica", + nextcloudFileLabel: "Scegli un file", + fileExternalLink: "Link esterno", + nextcloudErrConnect: "Impossibile raggiungere Nextcloud con questi dati.", + nextcloudErrUpload: "Il caricamento su Nextcloud non è riuscito.", + nextcloudErrShare: "Nextcloud non ha dato un link pubblico per il file.", fediverseDisconnect: "Disconnetti", - fediverseEmpty: "Quando colleghi altri account del fediverso dalle %LINK%, puoi gestirli da qui.", fediverseEmptyLink: "impostazioni", fediverseComposePlaceholder: "A cosa stai pensando?", fediverseReplyPlaceholder: "Scrivi la tua risposta…", @@ -333,21 +309,15 @@ module.exports = { fediverseReply: "Rispondi", fediverseBoosted: "ha rilanciato", fediverseNoPosts: "La tua timeline è vuota.", - fediverseThread: "Discussione", fediverseTimeline: "Cronologie", - fediverseManageTimeline: "Gestisci la cronologia", fediverseFollowers: "Follower", fediverseFollowing: "Seguiti", fediversePosts: "Post", fediverseJoined: "Iscritto", - fediverseOverview: "Panoramica", fediverseRefresh: "Aggiorna", fediverseWrite: "Scrivi", fediversePreview: "Anteprima", - fediverseEdit: "Modifica", - fediverseOpenFeed: "Visita il feed", fediverseManage: "Gestisci", - fediverseStatusNotConnected: "Non connesso", fediverseError: "Impossibile contattare Mastodon.", fediverseErrInstance: "URL dell'istanza non valido.", fediverseErrAuth: "Token non valido o scaduto.", @@ -358,17 +328,76 @@ module.exports = { fediverseErrPost: "Impossibile pubblicare.", fediverseErrMedia: "Impossibile caricare il file.", fediverseErrEmpty: "Scrivi qualcosa o allega un file.", - popularLabel: "In evidenza", - topicsLabel: "Argomenti", - latestLabel: "Più recenti", - summariesLabel: "Riassunti", - threadsLabel: "Discussioni", - multiverseLabel: "Multiverso", - inboxLabel: "Posta in arrivo", - invitesLabel: "Inviti", - walletLabel: "Portafoglio", - legacyLabel: "Chiavi", - cipherLabel: "Crittografia", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Numero di telefono", + telegramSendCode: "Invia codice", + telegramCodeHelp: "Inserisci il codice che Telegram ti ha appena inviato (app o SMS).", + telegramCodeLabel: "Codice", + telegramPasswordHelp: "Questo account ha la verifica in due passaggi. Inserisci la password.", + telegramPasswordLabel: "Password", + telegramVerify: "Verifica", + telegramCancel: "Annulla", + telegramChats: "Chat", + telegramUnread: "Non letti", + telegramOpen: "Apri", + telegramSend: "Invia", + telegramYou: "Tu", + telegramNoChats: "Nessuna chat per ora.", + telegramNoMessages: "Nessun messaggio per ora.", + telegramComposePlaceholder: "Scrivi un messaggio…", + telegramChannel: "Canale", + telegramGroup: "Gruppo", + telegramPrivate: "Privato", + telegramErrConnect: "Impossibile contattare Telegram.", + telegramErrTimeout: "Telegram ha impiegato troppo a rispondere.", + telegramErrAuth: "Sessione Telegram non valida o scaduta. Ricollegati.", + telegramErrFetch: "Impossibile caricare le chat.", + telegramErrSend: "Impossibile inviare il messaggio.", + telegramErrMissing: "Servono API ID, API hash e numero di telefono.", + telegramErrCode: "Codice non valido. Riprova.", + telegramErrCodeExpired: "Il codice è scaduto. Ricomincia.", + telegramErrPassword: "Password errata.", + telegramErrPhone: "Numero di telefono non valido.", + telegramErrApi: "API ID o API hash non validi.", + telegramErrFlood: "Troppi tentativi. Attendi e riprova.", + telegramErrNoLogin: "Nessun accesso in corso. Ricomincia.", + peertubeUsernameLabel: "Nome utente", + peertubePasswordLabel: "Password", + peertubeSubscriptions: "Iscrizioni", + peertubeMyVideos: "I miei video", + peertubeVideos: "Video", + peertubeFollowers: "Follower", + peertubeViews: "visualizzazioni", + peertubeLikes: "mi piace", + peertubeQuota: "Quota", + peertubeUpload: "Carica video", + peertubeUploadHelp: "Scegli un file video. Viene caricato sul tuo canale e PeerTube lo elabora in seguito.", + peertubeTitleLabel: "Titolo", + peertubeDescriptionLabel: "Descrizione", + peertubePrivacyLabel: "Privacy", + peertubePrivacyPublic: "Pubblico", + peertubePrivacyUnlisted: "Non in elenco", + peertubePrivacyPrivate: "Privato", + peertubeNoVideos: "Ancora nessun video.", + peertubeComments: "Commenti", + peertubeComment: "Commenta", + peertubeCommentPlaceholder: "Scrivi un commento…", + peertubeNoComments: "Ancora nessun commento.", + peertubeOpen: "Apri su PeerTube", + peertubeWatch: "Guarda", + peertubeChannel: "Canale", + peertubeNoStream: "Questo video non può ancora essere riprodotto qui. Aprilo su PeerTube.", + peertubeUploaded: "Video caricato. PeerTube lo sta elaborando.", + peertubeErrConnect: "Impossibile contattare PeerTube.", + peertubeErrAuth: "Nome utente o password errati, oppure la sessione è scaduta. Riconnettiti.", + peertubeErrInstance: "URL dell'istanza non valido.", + peertubeErrMissing: "Servono istanza, nome utente e password.", + peertubeErrFetch: "Impossibile caricare i video.", + peertubeErrUpload: "Impossibile caricare il video.", + peertubeErrTitle: "Titolo obbligatorio.", + peertubeErrComment: "Impossibile pubblicare il commento.", + peertubeErrNoFile: "Scegli un file video.", bookmarksLabel: "Segnalibri", videosLabel: "Video", torrentsLabel: "Torrents", @@ -379,34 +408,22 @@ module.exports = { inhabitantsLabel: "Abitanti", trendingLabel: "Tendenze", eventsLabel: "Eventi", - tasksLabel: "Compiti", apply: "Applica", menuPersonal: "Personale", - menuContent: "Contenuto", - menuBlogs: "Blog", menuGovernance: "Governance", menuOffice: "Ufficio", - menuMultiverse: "Multiverso", - menuNetwork: "Rete", + menuNetwork: "Comunità", menuCreative: "Creativo", menuEconomy: "Economia", - menuMedia: "Media", + menuMedia: "Biblioteca", menuTools: "Strumenti", comment: "Commento", subtopic: "Sotto-argomento", json: "JSON", - createdBy: "di", unfollow: "Rimuovi supporto", follow: "Supporta", block: "Blocca", unblock: "Sblocca", - newerPosts: "Post più recenti", - olderPosts: "Post più vecchi", - feedRangeEmpty: "L'intervallo indicato è vuoto per questo feed. Prova a visualizzare il ", - seeFullFeed: "feed completo", - feedEmpty: "La rete Oasis non ha mai visto post da questo account.", - beginningOfFeed: "Questo è l'inizio del feed", - noNewerPosts: "Non sono ancora stati ricevuti post più recenti.", relationshipNotFollowing: "Non ti supporta", relationshipTheyFollow: "Ti supporta", relationshipMutuals: "Supporto reciproco", @@ -416,25 +433,23 @@ module.exports = { relationshipBlockedBy: "Ti ha bloccato", relationshipMutualBlock: "Blocco reciproco", relationshipNone: "Non stai supportando", - relationshipConflict: "Conflitto", relationshipBlockingPost: "Post bloccato", viewLikes: "Vedi diffusioni", + viewSupporters: "Vedi sostenitori", spreadedDescription: "Lista dei post diffusi dall'abitante.", - totalspreads: "Diffusioni totali", - attachFiles: "Allega file", preview: "Anteprima", publish: "Pubblica", contentWarningPlaceholder: "Aggiungi un oggetto al post (opzionale)", - privateWarningPlaceholder: "Aggiungi abitanti per inviare un post privato (opzionale)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "Scrivi il corpo del tuo articolo.", + publishTooLong: "Il tuo post è troppo lungo. Accorcialo, per favore.", + keepTextRetry: "Invia di nuovo", + dateInPastError: "La data non può essere nel passato", publishCustomDescription: [ "RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non può essere modificato né eliminato.", ], commentWarning: [ "RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non può essere modificato né eliminato.", ], - commentPublic: "pubblico", - commentPrivate: "privato", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -454,14 +469,12 @@ module.exports = { ".", ], publishCustom: "Scrivi un post avanzato", - subtopicLabel: "Crea un sotto-argomento di questo post", messagePreview: "Anteprima post", mentionsMatching: "Menzioni corrispondenti", mentionsName: "Nome", mentionsRelationship: "Relazione", - updateit: "AGGIORNAMENTI!", updateBannerText: "È disponibile una nuova versione di Oasis.", - updateBannerAction: "Aggiorna ora →", + updateBannerAction: "Aggiorna ora!", info: "Info", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -469,29 +482,94 @@ module.exports = { timeAgo: "fa", sendTime: "circa ", theme: "Tema", - legacy: "Chiavi", - legacyTitle: "Chiavi", - legacyDescription: "Gestisci il tuo segreto (chiave privata) in modo rapido e sicuro.", - legacyExportButton: "Esporta", - legacyImportButton: "Importa", - ssbLogStream: "Blokchain", + backupKeysExportButton: "Esporta", + backupKeysImportButton: "Importa", + ssbLogStream: "Blockchain", ssbLogStreamDescription: "Configura il limite di messaggi per i flussi della Blockchain.", + logWindowChip: "Ultimi {n} stream della blockchain", saveSettings: "Salva impostazioni", - exportTitle: "Esporta dati", exportDescription: "Imposta una password (minimo 32 caratteri) per crittografare la tua chiave", - exportDataTitle: "Backup", + exportDataTitle: "Copia istantanea", exportDataDescription: "Scarica i tuoi dati (chiave segreta esclusa!)", exportDataButton: "Scarica database", - pubWallet: "Portafoglio PUB", - pubWalletDescription: "Imposta l'URL del portafoglio PUB. Verrà utilizzato per le transazioni PUB (inclusa la UBI).", - pubWalletConfiguration: "Salva configurazione", - importTitle: "Importa dati", importDescription: "Importa il tuo segreto crittografato (chiave privata) per attivare il tuo avatar", - importAttach: "Allega file crittografato (.enc)", - passwordLengthInfo: "La password deve avere almeno 32 caratteri.", passwordImport: "Scrivi la tua password per decifrare i dati che verranno salvati nella home del sistema (nome: secret)", exportPasswordPlaceholder: "Usa minuscole, maiuscole, numeri e simboli", - fileInfo: "La tua chiave segreta crittografata verrà salvata nella home del sistema (nome: oasis.enc)", + fileInfo: "La tua chiave segreta crittografata verrà scaricata sul tuo dispositivo (nome: oasis.enc)", + developer: "Sviluppo", + devTitle: "Sviluppo", + welcomeBannerText: "Il tuo avatar non è ancora configurato.", + aiSuggestionBanner: "Suggerimento:", + aiSuggestionsEnable: "Suggerimenti dell'IA", + welcomeBannerAction: "Inizia", + welcomeTitle: "Benvenuta", + welcomeStepIdentity: "Identità", + welcomeStepType: "Tipo", + welcomeStepUse: "Uso", + welcomeStepLook: "Aspetto", + welcomeStepCommunity: "Comunità", + welcomeStepIntro: "Benvenuto", + welcomeHintIdentity: "Il tuo nome e la tua immagine.", + welcomeHintType: "Come si apre Oasis per te.", + welcomeHintUse: "Quali moduli restano attivi.", + welcomeHintLook: "Il tema che indossa Oasis.", + welcomeHintCommunity: "Entra in un PUB per conoscere altri abitanti.", + welcomeHintIntro: "Invia un primo saluto alla comunità.", + welcomeStepThemeAction: "Usa questo tema", + welcomeJoinPubs: "Entra nei PUB", + welcomeNoFederation: "Senza federarsi", + welcomeStepGreetingAction: "Invia", + welcomeDescription: "Alcune cose da fare prima di iniziare.", + welcomeSkip: "Non ora", + welcomeStepProfileAction: "Salva profilo", + welcomeBootstrapRunning: "Caricamento dell'istantanea del pub. Al termine ti unisci al pub automaticamente; nel frattempo puoi continuare a usare Oasis.", + welcomeCompleteTitle: "Tutto pronto.", + welcomeCompleteText: "Il tuo nodo è pronto. Da qui la rete cresce con gli abitanti che sostieni.", + welcomeFindPeople: "Trova abitanti", + devFilterFiles: "FILE", + devFilterSearch: "CERCA", + devFilterModules: "MODULI", + devFilterTranslations: "TRADUZIONI", + devFilterStyles: "STILI", + devFilterTests: "TEST", + devVersion: "Versione", + devNodeVersion: "Node", + devStatsTests: "Test", + devParentFolder: "Cartella superiore", + devOpenFolder: "apri", + devDescription: "Esplora il codice sorgente in esecuzione su questo nodo.", + devRoot: "radice", + devSearchPlaceholder: "Testo da cercare nel codice", + devAnyExtension: "Qualsiasi file", + devStatsFiles: "File", + devStatsLines: "Righe", + devStatsModules: "Moduli", + devStatsLanguages: "Lingue", + devNotViewable: "non visualizzabile", + devEmptyFolder: "Questa cartella è vuota.", + devSize: "Dimensione", + devShowing: "Mostrando", + devLine: "riga", + devReportBug: "Segnala Bug", + devCreateTask: "Crea Attività", + devRaw: "RAW", + devReportContext: "Trovato leggendo il codice sorgente di Oasis", + devTaskPrefix: "Rivedere", + devPrevPage: "Righe precedenti", + devNextPage: "Righe successive", + devMatches: "corrispondenze", + devFilesScanned: "file analizzati", + devNoResults: "Ancora nessuna corrispondenza.", + devColModule: "Modulo", + devColState: "Stato", + devColModel: "Modello", + devColView: "Vista", + devColRoutes: "Rotte", + devColTests: "Test", + devOn: "attivo", + devOff: "inattivo", + modulesDevLabel: "Sviluppo", + modulesDevDescription: "Modulo per gestire il codice sorgente di Oasis.", themeIntro: "Scegli un tema.", setTheme: "Imposta tema", @@ -499,16 +577,49 @@ module.exports = { languageDescription: "Se desideri usare un'altra lingua, selezionala qui.", setLanguage: "Imposta lingua", - peerConnections: "Peer", peerConnectionsIntro: "Gestisci tutte le tue connessioni con altri peer.", + peersConnectFailed: "Impossibile connettersi: {reason}.", + peerStateLabel: "Stato", + peerAddressLabel: "Indirizzo", + peerLastChangeLabel: "Ultima modifica", + peerStateConnected: "Connesso", + peerStateConnecting: "Connessione in corso", + peerStateStaged: "Disponibile", + peerErrSilent: "Nessuna attività", + peerErrTimeout: "Non risponde", + peerErrRefused: "Rifiuta la connessione", + peerErrNotFound: "Indirizzo sconosciuto", + peerErrUnreachable: "Rete irraggiungibile", + peerErrKeys: "Usa chiavi diverse", + peerErrIdentity: "Non accetta la tua connessione", + peerErrTor: "Solo TOR", + peerErrDropped: "Connessione interrotta", + peerErrPaused: "Rete in pausa", + peerErrOther: "Errore di connessione", + peerOutdated: "Non aggiornato", + peerVersionUnannounced: "Non annuncia la sua versione: un Oasis vecchio", + peersTorMissing: "Questo PUB accetta solo connessioni tramite TOR.", peerConnectionsTitle: "Connessioni", + peersOnlineHint: "Nodi connessi con te in questo momento.", + peersDiscoveredHint: "Nodi con indirizzo noto che non sono connessi ora.", + peersUnknownHint: "Nodi del tuo grafo sociale senza indirizzo noto.", + peersConnectionsHint: "Indirizzi che questo nodo può contattare e lo stato di ogni connessione.", + peersTabLan: "LAN", + peersTabHealth: "Salute", + peersHealthHint: "Come sta ogni pub o nodo conosciuto, nella rete locale e oltre.", + peersHealthReachable: "Raggiungibile", + peersHealthFailing: "In errore", + peersHealthFailures: "Errori", + peersFailingSince: "In errore da", + peersHealthForget: "Dimentica irraggiungibili", + peersLanHint: "Nodi incontrati nella tua rete locale.", + peersLanLastSeen: "Visto l'ultima volta", + peersLanEmpty: "Nessun nodo incontrato nella rete locale per ora.", online: "Online", offline: "Offline", discovered: 'Scoperti', unknown: 'Sconosciuto', - lanBroadcastLabel: "Broadcast LAN", lanBroadcastActive: "Attivo (multicast UDP sulla LAN)", - lanBroadcastDisabled: "Disattivato (modalità pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -521,127 +632,102 @@ module.exports = { noConnections: "Nessun peer connesso.", noDiscovered: "Nessun peer scoperto.", noUnknownPeers: "Nessun peer sconosciuto nel grafo delle amicizie.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "Nessun peer registrato per ora.", + peerConnect: "Connetti", + peerDisconnect: "Disconnetti", peerRefresh: "Aggiorna", peerPruneIdle: "Rimuovi inattivi", + peersPause: "Pausa", + peersResume: "Riprendi", + peersPausedNote: "In pausa: Oasis non si connette con nessuno e nessuno può connettersi.", + peersPauseHint: "Ferma tutte le connessioni: Oasis non si connette ad altri nodi né li accetta finché non riprendi.", peerExport: "Esporta", peerImport: "Importa", peerImportTitle: "Importa lista peer", peerImportPlaceholder: "Incolla un indirizzo multiserver per riga, o carica un file .txt…", - noSupportedConnections: "Nessun peer supportato.", - noBlockedConnections: "Nessun peer bloccato.", - noRecommendedConnections: "Nessun peer consigliato.", connectionActionIntro: "", - startNetworking: "Avvia rete", - stopNetworking: "Ferma rete", - restartNetworking: "Riavvia rete", - sync: "Sincronizza rete", indexes: "Indici", + indexesRebuilt: "Indici ricostruiti", + indexesRebuildFailed: "Non è stato possibile ricostruire gli indici", + indexesFiles: "File di indice", indexesDescription: "Ricostruire gli indici è sicuro e può risolvere alcuni tipi di errori.", homePageTitle: "Home", homePageDescription: "Seleziona quale modulo vuoi come pagina iniziale.", saveHomePage: "Imposta home", invites: "Inviti", - invitesTitle: "Inviti", - invitesInvites: "Inviti", invitesDescription: "Gestisci e applica codici invito nella tua rete.", + invitesPubsHint: "Incolla un codice d'invito di un pub per federarti con lui e replicarne gli abitanti.", + invitesFederationsHint: "Pub con cui sei federato: aggiornali, togli quelli irraggiungibili o esporta l'elenco.", invitesTribesTitle: "Tribù", - invitesTribeInviteCodePlaceholder: "Inserisci codice invito tribù", - invitesTribeJoinButton: "Unisciti alla tribù", invitesChatsTitle: "Chat", - invitesChatInviteCodePlaceholder: "Inserisci codice invito chat", - invitesChatJoinButton: "Unisciti alla chat", invitesPadsTitle: "Pad", - invitesPadInviteCodePlaceholder: "Inserisci codice invito pad", - invitesPadJoinButton: "Unisciti al pad", invitesCalendarsTitle: "Calendari", - invitesCalendarInviteCodePlaceholder: "Inserisci codice invito calendario", - invitesCalendarJoinButton: "Unisciti al calendario", invitesEventsTitle: "Eventi", - invitesEventInviteCodePlaceholder: "Inserisci codice invito evento", - invitesEventJoinButton: "Unisciti all'evento", invitesForumsTitle: "Forum", - invitesForumInviteCodePlaceholder: "Inserisci codice invito forum", - invitesForumJoinButton: "Unisciti al forum", invitesMapsTitle: "Mappe", - invitesMapInviteCodePlaceholder: "Inserisci codice invito mappa", - invitesMapJoinButton: "Unisciti alla mappa", invitesShopsTitle: "Negozi", - invitesShopInviteCodePlaceholder: "Inserisci il codice di invito del negozio", - invitesShopJoinButton: "Unisciti al negozio", invitesPubsTitle: "PUB", + invitesSuggestedPubs: "PUB Suggeriti", + invitesFilterOthers: "Altre", + invitesJoinTitle: "Inviti", + invitesJoinHint: "Incolla qualsiasi codice d'invito, per qualunque cosa sia, o l'Oasis ID di un abitante.", + invitesJoinPlaceholder: "Codice d'invito o Oasis ID", + invitesJoinButton: "Entra", + invitesUsedTitle: "Ultimi inviti", + invitesUsedEmpty: "Non hai ancora utilizzato nessun invito.", + invitesUsedType: "Tipo", + invitesUsedName: "Nome", + invitesUsedDate: "Data", invitesPubInviteCodePlaceholder: "Inserisci codice invito PUB", invitesAcceptInvite: "Unisciti al PUB", + invitesPubUses: "Inviti usati", + invitesPubExhausted: "Questo PUB non ha più inviti disponibili.", invitesAlreadyFederated: "Sei già federato con questo pub.", + invitesJoinPubMode: "Questo Oasis funziona come PUB, in sola lettura. Per unirlo a un altro PUB, usa il terminale: ./oasis.sh join ", + invitesJoinLocalOnly: "Gli inviti si accettano solo dal dispositivo su cui gira Oasis, o tramite il link di amministrazione mostrato all'avvio.", invitesFederationsTitle: "Federazioni", invitesAcceptedInvites: "Federate", - invitesNoInvites: "Nessun invito accettato ancora.", invitesUnfollow: "Smetti di seguire", invitesFollow: "Segui", invitesUnfollowedInvites: "Non federate", invitesNoFederatedPubs: "Nessun pub federato.", invitesNoUnfollowed: "Nessun pub non federato.", - invitesUnreachablePubs: "Irraggiungibili", - invitesNoUnreachablePubs: "Nessun pub irraggiungibile.", invitesPubsRefresh: "Aggiorna", - invitesPubsClearUnreachable: "Rimuovi irraggiungibili", + invitesPubsClearUnreachable: "Pulisci", invitesPubsExport: "Esporta", invitesPubsImport: "Importa", invitesPubsImportTitle: "Importa pub", invitesPubsImportPlaceholder: "Incolla un indirizzo multiserver o un codice invito per riga, o carica un file .txt…", - currentlyUnreachable: "ERRORE!", - errorDetails: "Dettagli errore", genericError: "Si è verificato un errore.", panicMode: "Modalità panico!", encryptData: "Imposta una password (minimo 32 caratteri) per crittografare la tua blockchain", decryptData: "Inserisci la password per decifrare la tua blockchain", - panicModeDescription: "Crittografa/Decifra o ELIMINA la tua blockchain", removeDataDescription: "ATTENZIONE: Questo processo non può essere annullato.", - encryptPanicButton: "Crittografa blockchain", - decryptPanicButton: "Decifra blockchain", removePanicButton: "ELIMINA TUTTI I TUOI DATI!", searchTitle: "Cerca", searchDescriptionLabel: "Cerca contenuti nella tua rete.", - searchLanguagesLabel: "Lingue", - searchSkillsLabel: "Competenze", searchPlaceholder:"Cerca contenuti...", - searchDateLabel:"Data", searchLocationLabel:"Posizione", searchPriceLabel:"Prezzo", - searchUrlLabel:"URL", searchCategoryLabel:"Categoria", - searchStartLabel:"Inizio", - searchEndLabel:"Fine", searchPriorityLabel:"Priorità", searchStatusLabel:"Stato", statusLabel:"Stato", - totalVotesLabel:"Voti totali", noResultsFound:"Nessun risultato trovato.", votesOption:"Opzioni di voto", voteYesLabel:"Sì", voteNoLabel:"No", allTypesLabel: "TUTTI", - ABSTENTIONLabel:"ASTENSIONE", YESLabel:"SÌ", NOLabel:"NO", - FOLLOW_MAJORITYLabel: "SEGUI MAGGIORANZA", - CONFUSEDLabel: "CONFUSO", - NOT_INTERESTEDLabel: "NON INTERESSATO", - StatusLabel:"Stato", votesOptionYesLabel:"Sì", votesOptionNoLabel:"No", votesOptionAbstentionLabel:"Astensione", votesQuestionLabel:"Domanda", - votesCreatedByLabel:"Creato da", voteStatusOpen:"APERTO", voteStatusClosed:"CHIUSO", - imageSearchLabel: "Inserisci parole per cercare immagini etichettate con esse.", commentDescription: ({ parentUrl }) => [ " ha commentato in ", a({ href: parentUrl }, " discussione"), @@ -652,61 +738,27 @@ module.exports = { a({ href: parentUrl }, " un post"), ], subtopicTitle: ({ authorName }) => [`Sotto-argomento del post di @${authorName}`], - mysteryDescription: "ha pubblicato un post misterioso", oasisDescription: "Rete del Progetto OASIS", searchSubmit: "Cerca...", - postLabel: "POST", - aboutLabel: "ABITANTI", - feedLabel: "FEED", votesLabel: "VOTAZIONI", - reportLabel: "SEGNALAZIONI", - imageLabel: "IMMAGINI", - videoLabel: "VIDEO", - audioLabel: "AUDIO", - documentLabel: "DOCUMENTI", - torrentLabel: "TORRENT", pdfFallbackLabel: "Documento PDF", - eventLabel: "EVENTI", - taskLabel: "COMPITI", - transferLabel: "TRASFERIMENTI", - curriculumLabel: "CURRICULUM", - bookmarkLabel: "SEGNALIBRI", - tribeLabel: "TRIBÙ", - marketLabel: "MERCATO", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "PORTAFOGLI", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + shopLabel: "NEGOZI", + voteLabel: "VOTI", submit: "Invia", - subjectLabel: "Oggetto", editProfile: "Modifica avatar", - profileQrAlt: "Codice QR del profilo", - profileQrHint: "Scansiona per copiare questo Oasis ID.", oasisIdLabel: "OasisID", - spreadLabel: "Diffondi", - spreadHint: "Diffondi questo a chi ti supporta (replica via il tuo feed).", + spreadContent: "Replica", welcomePmSubject: "Hello World!", - welcomePmBody: "Benvenuto su Oasis — un social network libero, peer-to-peer, federato e cifrato, con un'architettura distribuita solo su invito.\n\nAlcuni posti interessanti da cui partire:\n\n- /profile — Modifica il tuo avatar, nome, descrizione e quali moduli appaiono sul tuo lato pubblico.\n- /invites — Aggiungi un pub per federarti con il resto della rete.\n- /peers — Vedi chi è connesso, riscansiona la LAN, esporta o importa liste di peer.\n- /inhabitants — Scopri e visita gli avatar di altre persone.\n- /tribes — Crea o unisciti a gruppi privati cifrati end-to-end.\n- /inbox — Leggi e invia messaggi privati.\n- /activity — Vedi l'attività recente, tua e della tua rete.\n- /publish — Scrivi un post o condividi un'immagine, un audio, un video o un documento.\n\nAlcuni luoghi interessanti per connettersi:\n\n- /fediverse — Gestisci i tuoi altri account del fediverso, inclusi l'invio e la ricezione di contenuti.\n\nQuesto messaggio è stato inviato privatamente da te a te stesso, perciò non serviva alcuna connessione per riceverlo.", - profileVisibilityTitle: "Visibilità del profilo pubblico", - profileVisibilityHint: "Scegli quali campi mostrano gli altri abitanti nel tuo profilo. Per impostazione predefinita appare solo Karma.", + welcomePmBody: "Benvenuto su Oasis — un social network libero, peer-to-peer, federato e cifrato, con un'architettura distribuita solo su invito.\n\nAlcuni posti interessanti da cui partire:\n\n- [/avatar](/profile) — Modifica il tuo avatar, nome, descrizione e quali moduli appaiono sul tuo lato pubblico.\n- /settings — Regola la tua istanza locale: lingua, tema, moduli, privacy e sincronizzazione.\n- /invites — Aggiungi un pub per federarti con il resto della rete.\n- /peers — Vedi chi è connesso, riscansiona la LAN, esporta o importa liste di peer.\n- /inhabitants — Scopri e visita gli avatar di altri abitanti.\n- /tribes — Crea o unisciti a gruppi privati cifrati end-to-end.\n- /search — Cerca i contenuti della rete per tipo.\n\nAlcuni luoghi interessanti per connettersi:\n\n- /multiverse — Gestisci i tuoi altri account del multiverso, inclusi l'invio e la ricezione di contenuti.\n\nQuesto messaggio è stato inviato privatamente da te a te stesso, perciò non serviva alcuna connessione per riceverlo.", profileSensorsSectionTitle: "Sensori", profileVisibilityActivity: "Livello di attività", - profileVisibilityDevice: "Dispositivo", profileDeviceLockedHint: "Questo sensore è sempre attivo: mostra agli altri da quale dispositivo ti connetti e non può essere disattivato.", profileVisibilityKarma: "Punteggio KARMA", - profileVisibilityUbi: "UBI", - profileVisibilityWallet: "Portafoglio ECOIN", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityUbi: "RBU", + profileVisibilityWallet: "Portafoglio ECOin", + profileVisibilityEcoTax: "Tassa ECO", + profileVisibilityLarpSign: "Sigillo L.A.R.P.", profileVisibilityGpg: "Chiave GPG", - profileVisibilityClearnet: "Pubblica il mio profilo", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Negozi", profileClearnetJobsLabel: "Lavori", @@ -718,6 +770,10 @@ module.exports = { profileClearnetImagesLabel: "Immagini", profileClearnetDocumentsLabel: "Documenti", profileClearnetTorrentsLabel: "Torrent", + profileClearnetPodcastsLabel: "Podcast", + profileClearnetMarketLabel: "Mercato", + profileClearnetFeedLabel: "Feed", + profileClearnetWikiLabel: "Wiki", editProfileDescription: "", profileName: "Nome", @@ -743,7 +799,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "Conf.", walletConfirm: "Conferma", - walletDescription: "Gestisci i tuoi asset digitali, incluso inviare e ricevere ECOin, visualizzare il saldo e accedere alla cronologia delle transazioni.", + walletDescription: "Gestisci il tuo portafoglio ECOin.", walletDate: "Data", walletFee: "Commissione (più alta è la commissione, più veloce sarà l'elaborazione della transazione)", walletFeeLine: ({ fee }) => `Commissione: ECO ${fee}`, @@ -751,6 +807,86 @@ module.exports = { walletReceive: "Ricevi", walletReset: "Ripristina", walletSend: "Invia", + walletPayingTransfer: "Pagamento del transfer", + walletCreateTransferLabel: "Genera contratto (trasferimento)", + walletTransferNotifiedNote: "L'altra parte è stata avvisata di questo pagamento", + walletTransferCreatedNote: "È stato creato un transfer (ricevuta) per questo pagamento", + walletTransferNoPayeeNote: "Nessun abitante ha pubblicato questo indirizzo, nessun transfer creato", + walletTransferReceiptConcept: "Pagamento dal portafoglio", + walletPaymentPmText: "ti ha inviato un pagamento", + walletPayeeLabel: "Destinatario", + walletConceptLabel: "Causale", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Hai ricevuto il tuo RBU.", + bankingBotPaymentTitle: "Hai ricevuto un pagamento.", + bankingBotUbiPaidText: "Il tuo Reddito di Base Universale è stato pagato", + bankUbiPubThisNode: "Questo nodo paga il RBU", + bankUbiPubConnected: "PUB del RBU", + bankUbiPubNone: "Nessun PUB annuncia ancora il RBU nella tua rete", + bankUbiAddressPublished: "Indirizzo ECOin pubblicato", + bankUbiAddressMissing: "Pubblica il tuo indirizzo ECOin per ricevere il RBU", + bankUbiClaimedThisMonth: "Richiesto questo mese", + bankUbiPubBalance: "Portafoglio del PUB disponibile per il RBU", + bankNoPubAddress: "Il PUB del RBU non ha ancora pubblicato un indirizzo ECOin.", + bankUbiTab: "RBU", + bankDonateEco: "DONA!", + donateTooltip: "Dona ECOin", + bankNoUserAddress: "Questo abitante non condivide un indirizzo ECOin.", + bankUbiPubDefault: "Predefinito", + bankUbiPool: "Dotazione", + bankUbiLastSeen: "Visto l'ultima volta", + bankUbiLastPayout: "Ultima azione", + bankUbiPaidOut: "Distribuito", + bankRangeToday: "Oggi", + bankRangeWeek: "Settimana", + bankRangeMonth: "Mese", + bankRangeYear: "Anno", + bankRangeFiveYears: "5 anni", + bankRangeAll: "Tutto", + bankChartValueTitle: "Evoluzione del valore di ECOin", + bankChartValueHours: "Equivalenza in tempo (h)", + bankChartWealthTitle: "Redistribuzione della ricchezza ECOin", + bankChartDistributed: "RBU distribuito (ECO)", + bankChartRebalanceTitle: "Redistribuzione della ricchezza tra PUB", + bankChartRebalanced: "ECO spostati tra PUB", + bankChartTaxes: "Imposte trattenute (ECO)", + bankChartNoDataRange: "Nessun dato in questo intervallo.", + bankChartNoDataYet: "Ancora nessun dato", + bankChartPerHour: "all'ora", + bankChartPerDay: "al giorno", + bankChartPerMonth: "al mese", + bankTotalUbiDistributed: "RBU totale redistribuito (rete)", + bankTotalTaxesWithheld: "Imposte totali trattenute (rete)", + bankInflationIssuance: "Tasso di emissione (annuo, dalla ricompensa di blocco)", + bankRulesEligibilityTitle: "Chi può ricevere il RBU", + bankRulesEligibility1: "Un indirizzo ECOin pubblicato nel profilo (Profilo → Sensori → Portafoglio ECOin).", + bankRulesEligibility2: "Un feed con più di 30 giorni e attività nella rete: karma sopra lo zero.", + bankRulesEligibility3: "Una richiesta al mese (epoch), da Banking con il pulsante Richiedi RBU. Il mese in corso si può rifiutare con Rifiuta il RBU.", + bankRulesEligibility4: "La soglia di 1 ECO si paga sempre; le imposte riducono solo la parte che la supera e non si pagano mai a parte.", + bankRulesEligibility5: "Il PUB paga nel ciclo successivo e pubblica un trasferimento RBU che puoi confermare.", + bankRulesEligibility6: "Se manca l'indirizzo, il feed è troppo giovane, hai rifiutato il mese o il PUB non ha fondi, la richiesta viene saltata e registrata sul PUB.", + copyLinkTitle: "Copia link", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "Rifiuta il RBU", + bankUbiRefusedThisMonth: "Hai rifiutato il RBU di questo mese.", + bankingBotUbiAvailableTitle: "Puoi richiedere il tuo RBU.", + bankingBotUbiAvailableText: "Il tuo RBU di questo mese è disponibile", + bankingBotConfirmTitle: "Hai uno smart-contract in attesa di conferma", + bankingBotConfirmText: "Smart-contract in attesa della tua conferma", + bankChartZoomHint: "Clicca per ingrandire", + bankChartPoolTitle: "Dotazione del RBU (nel tempo)", + bankChartNetworkUbiTitle: "RBU totale redistribuito (rete)", + bankChartSupplyTitle: "Offerta circolante di ECOin (nel tempo)", + bankChartInflationTitle: "Inflazione di ECOin (nel tempo)", + bankChartInflationSeries: "Tasso annualizzato (%)", + bankChartPool: "Dotazione totale (ECO)", + bankChartKarmaTitle: "KARMA Scoring (nel tempo)", + bankChartFundsTitle: "I tuoi fondi ECOin (nel tempo)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin connesso", + bankWalletNotConfigured: "ECOin disconnesso", + transfersPayWithWallet: "PAGA!", walletStatus: "Stato", walletDisconnected: [ "Portafoglio ECOin ", @@ -760,9 +896,11 @@ module.exports = { " o lo stato della connessione.", ], walletSentToLine: ({ destination, amount }) => `Inviati ECO ${amount} a ${destination}`, - walletSettingsTitle: "Portafoglio", + walletNotConfigured: "Il tuo portafoglio ECOin non è ancora configurato.", walletSettingsDescription: "Integra Oasis con il tuo portafoglio ECOin.", - walletSettingsDocLink: "Guida all'installazione di ECOin", + walletSettingsMobileDescription: "Collega Oasis all'app ECOin Wallet di questo dispositivo.", + walletDownloadEcoin: "Scarica ECOin!", + walletRpcUrl: "URL RPC", walletStatusMessages: { invalid_amount: "Importo non valido", invalid_dest: "Indirizzo di destinazione non valido", @@ -778,41 +916,24 @@ module.exports = { walletUser: "Nome utente", walletPass: "Password", walletConfiguration: "Imposta portafoglio", + walletDisconnectButton: "Disconnetti wallet", cipher: "Crittografia", cipherTitle: "Crittografia", cipherDescription: "Crittografa e decifra il tuo testo simmetricamente (usando una password condivisa).", randomPassword: "Password casuale", - cipherEncryptTitle: "Crittografa testo", - cipherEncryptDescription: "Inserisci il testo da crittografare", - cipherTextLabel: "Testo da crittografare", cipherTextPlaceholder: "Inserisci il testo da crittografare...", cipherPasswordLabel: "Imposta una password (minimo 32 caratteri) per crittografare il testo", cipherPasswordDecryptLabel: "Imposta una password (minimo 32 caratteri) per decifrare il testo", cipherPasswordPlaceholder: "Inserisci una password...", cipherEncryptButton: "Crittografa", - cipherDecryptTitle: "Decifra testo", - cipherDecryptDescription: "Inserisci il testo da decifrare", cipherEncryptedMessageLabel: "Testo crittografato", cipherDecryptedMessageLabel: "Testo decifrato", - cipherPasswordUsedLabel: "Password usata per crittografare (conservala!)", cipherEncryptedTextPlaceholder: "Inserisci il testo crittografato...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "Inserisci il vettore di inizializzazione...", cipherDecryptButton: "Decifra", password: "Password", text: "Testo", encryptedText: "Testo crittografato", iv: "Vettore di inizializzazione (IV)", - encryptTitle: "Crittografa il tuo testo", - encryptDescription: "Inserisci il testo che vuoi crittografare e fornisci una password.", - encryptButton: "Crittografa", - decryptTitle: "Decifra il tuo testo", - decryptDescription: "Inserisci il testo crittografato e fornisci la stessa password usata per la crittografia.", - decryptButton: "Decifra", - passwordLengthError: "La password deve avere almeno 32 caratteri.", - missingFieldsError: "Testo, password o IV non forniti.", - encryptionError: "Errore durante la crittografia del testo.", - decryptionError: "Errore durante la decifratura del testo.", bookmarkTitle: "Segnalibri", bookmarkDescription: "Scopri e gestisci i segnalibri nella tua rete.", bookmarkAllSectionTitle: "Segnalibri", @@ -829,19 +950,16 @@ module.exports = { bookmarkFilterRecent: "RECENTI", bookmarkCreateButton: "Crea segnalibro", bookmarkUpdateButton: "Aggiorna", - bookmarkDeleteButton: "Elimina", bookmarkAddFavoriteButton: "Aggiungi preferito", bookmarkRemoveFavoriteButton: "Rimuovi preferito", bookmarkUrlLabel: "Link", - bookmarkUrlPlaceholder: "https://example.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Descrizione", bookmarkDescriptionPlaceholder: "Opzionale", bookmarkTagsLabel: "Tag", bookmarkTagsPlaceholder: "Inserisci tag separati da virgole", - bookmarkCategoryLabel: "Categoria", - bookmarkCategoryPlaceholder: "Opzionale", bookmarkLastVisitLabel: "Ultima visita", - bookmarkSearchPlaceholder: "Cerca URL, tag, categoria, autore...", + bookmarkSearchPlaceholder: "Cerca in Segnalibri...", bookmarkSortRecent: "Più recenti", bookmarkSortOldest: "Più vecchi", bookmarkSortTop: "Più votati", @@ -854,8 +972,6 @@ module.exports = { noLastVisit: "Nessuna ultima visita", videoTitle: "Video", videoDescription: "Esplora e gestisci contenuti video nella tua rete.", - videoPluginTitle: "Titolo", - videoPluginDescription: "Descrizione", videoMineSectionTitle: "I tuoi video", videoCreateSectionTitle: "Carica video", videoUpdateSectionTitle: "Aggiorna video", @@ -882,12 +998,11 @@ module.exports = { videoDescriptionPlaceholder: "Opzionale", videoNoFile: "Nessun file video fornito", noVideos: "Nessun video disponibile.", - videoSearchPlaceholder: "Cerca titolo, tag, autore...", + videoSearchPlaceholder: "Cerca in Video...", videoSortRecent: "Più recenti", videoSortOldest: "Più vecchi", videoSortTop: "Più votati", videoSearchButton: "Cerca", - videoMessageAuthorButton: "PM", videoUpdatedAt: "Aggiornato", videoNoMatch: "Nessun video corrisponde alla ricerca.", documentTitle: "Documenti", @@ -906,7 +1021,6 @@ module.exports = { documentFilterFavorites: "PREFERITI", documentCreateButton: "Carica documento", documentUpdateButton: "Aggiorna", - documentDeleteButton: "Elimina", documentAddFavoriteButton: "Aggiungi preferito", documentRemoveFavoriteButton: "Rimuovi dai preferiti", documentMessageAuthorButton: "PM", @@ -919,7 +1033,7 @@ module.exports = { documentDescriptionPlaceholder: "Opzionale", documentNoFile: "Nessun file.", noDocuments: "Nessun documento disponibile.", - documentSearchPlaceholder: "Cerca titolo, tag, descrizione, autore...", + documentSearchPlaceholder: "Cerca in Documenti...", documentSortRecent: "Più recenti", documentSortOldest: "Più vecchi", documentSortTop: "Più votati", @@ -928,8 +1042,6 @@ module.exports = { documentUpdatedAt: "Aggiornato", audioTitle: "Audio", audioDescription: "Esplora e gestisci contenuti audio nella tua rete.", - audioPluginTitle: "Titolo", - audioPluginDescription: "Descrizione", audioMineSectionTitle: "I tuoi audio", audioCreateSectionTitle: "Carica audio", audioUpdateSectionTitle: "Aggiorna audio", @@ -940,22 +1052,21 @@ module.exports = { audioFilterMine: "MIEI", audioFilterRecent: "RECENTI", audioFilterTop: "TOP", - audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Carica audio", audioUpdateButton: "Aggiorna", - audioDeleteButton: "Elimina", audioAddFavoriteButton: "Aggiungi preferito", audioRemoveFavoriteButton: "Rimuovi preferito", audioFileLabel: "Seleziona un file audio (.mp3, .wav, .ogg)", audioTagsLabel: "Tag", audioTagsPlaceholder: "Inserisci tag separati da virgole", + tagsPlaceholder: "Inserisci tag separati da virgole", audioTitleLabel: "Titolo", audioTitlePlaceholder: "Opzionale", audioDescriptionLabel: "Descrizione", audioDescriptionPlaceholder: "Opzionale", audioNoFile: "Nessun file audio fornito", noAudios: "Nessun audio disponibile.", - audioSearchPlaceholder: "Cerca titolo, tag, autore...", + audioSearchPlaceholder: "Cerca in Audio...", audioSortRecent: "Più recenti", audioSortOldest: "Più vecchi", audioSortTop: "Più votati", @@ -967,6 +1078,7 @@ module.exports = { audioFilterFavorites: "PREFERITI", favoritesTitle: "Preferiti", favoritesDescription: "Tutti i tuoi contenuti preferiti in un unico posto.", + favoritesSearchPlaceholder: "Cerca in Preferiti...", favoritesFilterAll: "TUTTI", favoritesFilterRecent: "RECENTI", favoritesFilterAudios: "AUDIO", @@ -978,22 +1090,16 @@ module.exports = { favoritesFilterChats: "CHAT", favoritesFilterCalendars: "CALENDARI", favoritesFilterVideos: "VIDEO", - favoritesRemoveButton: "Rimuovi dai preferiti", favoritesNoItems: "Nessun preferito ancora.", yourContacts: "I tuoi contatti", allInhabitants: "Abitanti", - allCVs: "Tutti i CV", + allCVs: "CV", discoverPeople: "Scopri abitanti nella tua rete.", - allInhabitantsButton: "TUTTI", - contactsButton: "SUPPORTATI", - CVsButton: "CV", - matchSkills: "Abbina competenze", - matchSkillsButton: "ABBINA COMPETENZE", - suggestedButton: "SUGGERITI", - searchInhabitantsPlaceholder: "FILTRA abitanti PER NOME …", + searchInhabitantsPlaceholder: "Cerca in Abitanti...", filterLocation: "FILTRA abitanti PER POSIZIONE …", filterLanguage: "FILTRA abitanti PER LINGUA …", filterSkills: "FILTRA abitanti PER COMPETENZE …", + cvSkillsCount: "Competenze", applyFilters: "Applica filtri", locationLabel: "Posizione", languagesLabel: "Lingue", @@ -1002,15 +1108,15 @@ module.exports = { mutualFollowers: "Follower reciproci", suggestedFollowsYou: "Ti segue", latestInteractions: "Ultime interazioni", - viewAvatar: "Vedi avatar", - viewCV: "Vedi CV", suggestedSectionTitle: "Suggeriti", topkarmaSectionTitle: "Top Karma", topecoSectionTitle: "Top Eco", topactivitySectionTitle: "Top Attività", + "TOP INACTIVITYButton": "PIÙ INATTIVI", + topinactivitySectionTitle: "Abitanti meno attivi", blockedSectionTitle: "Bloccati", gallerySectionTitle: "GALLERIA", - blockedButton: "BLOCCATI", + topSectionTitle: "TOP", blockedLabel: "Abitante bloccato", inhabitantviewDetails: "Vedi dettagli", keepReading: "Continua a leggere...", @@ -1025,10 +1131,23 @@ module.exports = { parliamentFilterCandidatures: "CANDIDATURE", parliamentFilterProposals: "PROPOSTE", parliamentFilterLaws: "LEGGI", + parliamentFilterRevocations: "REVOCHE", parliamentFilterHistorical: "STORICO", parliamentFilterLeaders: "LEADER", parliamentFilterRules: "REGOLE", parliamentGovernmentCard: "Governo attuale", + parliamentSeatsGovernment: "Governo", + parliamentSeatsOpposition: "Opposizione", + parliamentSeatsAbstention: "Astensione", + parliamentSeatsQuorum: "Quorum elettorale", + parliamentSeatsPass: "Soglia di approvazione", + parliamentSeatsByElection: "Per elezione", + parliamentSeatsByHouses: "Per casa del LARP", + parliamentSeatsSeats: "seggi", + parliamentSeatsSeat: "seggio", + parliamentSeatsLeader: "Leader", + parliamentSeatsVotesFor: "voti per", + parliamentSeatsNoHouse: "Senza casa", parliamentActorInPowerInhabitant: 'ABITANTE AL POTERE', parliamentActorInPowerTribe: 'TRIBÙ AL POTERE', parliamentHistoricalGovernmentsTitle: 'GOVERNI', @@ -1050,10 +1169,8 @@ module.exports = { parliamentPoliciesDeclined: "LEGGI RESPINTE", parliamentPoliciesDiscarded: "LEGGI SCARTATE", parliamentEfficiency: "% EFFICIENZA", - parliamentFilterRevocations: 'REVOCHE', parliamentRevocationFormTitle: 'Revoca legge', parliamentRevocationLaw: 'Legge', - parliamentRevocationTitle: 'Titolo', parliamentRevocationReasons: 'Motivazioni', parliamentRevocationPublish: 'Pubblica revoca', parliamentCurrentRevocationsTitle: 'Revoche attuali', @@ -1066,23 +1183,12 @@ module.exports = { parliamentNoGovernments: "Non ci sono ancora governi.", parliamentCandidatureFormTitle: "Proponi candidatura", parliamentCandidatureIdPh: "Oasis ID (@...) o nome tribù", - parliamentCandidatureSlogan: "Slogan (max 140 caratteri)", - parliamentCandidatureSloganPh: "Un breve motto", parliamentCandidatureMethod: "Metodo", parliamentCandidatureProposeBtn: "Pubblica candidatura", - parliamentThDate: "Data proposta", - parliamentThSlogan: "Slogan", - parliamentThSince: "Profilo dal", - parliamentThVotes: "Voti ricevuti", - parliamentThVoteAction: "Vota", - parliamentTypeUser: "Abitante", - parliamentTypeTribe: "Tribù", parliamentVoteBtn: "Vota", parliamentProposalFormTitle: "Proponi legge", parliamentProposalDescription: "Descrizione (≤1000)", parliamentProposalPublish: "Pubblica proposta", - parliamentFinalize: "Finalizza", - parliamentDeadline: "Scadenza", parliamentNoProposals: "Non ci sono ancora proposte di legge.", parliamentNoLaws: "Non ci sono ancora leggi approvate.", parliamentMethodDEMOCRACY: "Democrazia", @@ -1092,7 +1198,7 @@ module.exports = { parliamentMethodKARMATOCRACY: "Karmatocrazia", parliamentThId: "ID", parliamentThProposalDate: "Data proposta", - parliamentThMethod: "Method", + parliamentThMethod: "Metodo", parliamentThKarma: "Karma", parliamentThSupports: "Supporti", parliamentThVote: "Voto", @@ -1100,29 +1206,22 @@ module.exports = { parliamentVotesSlashTotal: "Voti/Totale", parliamentVotesNeeded: "Voti necessari", parliamentFutureLawsTitle: "Leggi future", - parliamentNoFutureLaws: "Nessuna legge futura, per ora.", parliamentVoteAction: "Vota", - parliamentLeadersTitle: "Leader", parliamentThLeader: "AVATAR", parliamentPopulation: "Popolazione", - parliamentThType: "Tipo", - parliamentThInPower: "Al potere", parliamentThPresented: "CANDIDATURE", parliamentNoLeaders: "Nessun leader, per ora.", parliamentCandidaturesListTitle: "Lista delle candidature", parliamentTimeRemaining: "Tempo rimanente", - parliamentNoLeader: "Nessuna candidatura vincente, per ora.", parliamentElectionsStatusTitle: "Prossimo governo", parliamentProposalDeadlineLabel: "Scadenza", + parliamentViewReport: "Vedi rapporto", parliamentProposalTimeLeft: "Tempo rimanente", - parliamentProposalOnTrack: "In linea per l'approvazione", - parliamentProposalOffTrack: "Supporto insufficiente", parliamentRulesTitle: "Come funziona il Parlamento", parliamentRulesIntro: "Le elezioni si risolvono ogni 2 mesi; le candidature sono continue e si azzerano quando un governo viene scelto.", parliamentRulesCandidates: "Qualsiasi abitante può proporre se stesso, un altro abitante o qualsiasi tribù. Ogni abitante può proporre fino a 3 candidature per ciclo; i duplicati nello stesso ciclo vengono rifiutati.", parliamentRulesElection: "Il vincitore è la candidatura con il maggior numero di voti al momento della risoluzione.", parliamentRulesTies: "Ordine di spareggio: karma più alto dell'abitante; in caso di parità, profilo più vecchio; se ancora in parità, proposta più antica; poi ordine lessicografico per ID.", - parliamentRulesFallback: "Se nessuno vota, vince l'ultima candidatura proposta. Se non ci sono candidature, viene selezionata una tribù casuale.", parliamentRulesTerm: "La scheda Governo mostra il governo attuale e le statistiche.", parliamentRulesMethods: "Forme: Anarchia (maggioranza semplice), Democrazia (50%+1), Maggioranza (80%), Minoranza (20%), Karmatocrazia (proposte con karma più alto), Dittatura (approvazione istantanea).", parliamentRulesAnarchy: "L'Anarchia è la modalità predefinita: se nessuna candidatura viene eletta alla risoluzione, viene proclamata l'Anarchia. In Anarchia, qualsiasi abitante può proporre leggi.", @@ -1131,6 +1230,7 @@ module.exports = { parliamentRulesLaws: "Quando una proposta raggiunge la soglia, diventa una Legge e appare nella scheda Leggi con la data di emanazione.", parliamentRulesHistorical: "Nella scheda Storico puoi vedere ogni ciclo di governo avvenuto e i dati sulla sua gestione.", parliamentRulesLeaders: "Nella scheda Leader puoi vedere una classifica degli abitanti/tribù che hanno governato (o si sono candidati), ordinata per efficienza.", + parliamentRulesHemicycle: "L'emiciclo assegna un seggio per abitante: il blocco di governo (gli elettori della candidatura vincente e il leader), un blocco di opposizione per ogni altra candidatura del ciclo, e l'astensione. I segni tratteggiati indicano il quorum elettorale (25% del censimento) e la soglia di approvazione del metodo in vigore. \"Per casa del LARP\" colora gli stessi seggi per casa.", parliamentProposalVoteStatusLabel: "Stato del voto", parliamentProposalOnTrackYes: "Soglia raggiunta", parliamentProposalOnTrackNo: "Sotto la soglia", @@ -1144,11 +1244,8 @@ module.exports = { courtsCaseFormTitle: "Apri caso", courtsCaseRespondent: "Accusato / Convenuto", courtsCaseRespondentPh: "Oasis ID (@...) o nome tribù", - courtsCaseMediatorsAccuser: "Mediatori (accusatore)", + courtsCaseSubjectPlaceholder: "Oggetto o breve descrizione", courtsCaseMethod: "Metodo di risoluzione", - courtsCaseDescription: "Descrizione (max 1000 caratteri)", - courtsCaseEvidenceTitle: "Prove del caso", - courtsCaseEvidenceHelp: "Allega immagini, audio, documenti (PDF) o video a supporto del tuo caso.", courtsCaseSubmit: "Presenta caso", courtsNominateJudge: "Nomina giudice", courtsJudgeId: "Giudice", @@ -1181,34 +1278,33 @@ module.exports = { courtsMethodDICTATOR: "Dittatore", courtsMethodPOPULAR: "Popolare", courtsMethodKARMATOCRACY: "Karmatocrazia", + courtsCaseType: "Tipo di caso", + courtsPresetMinorConflict: "Conflitto lieve", + courtsPresetModerateConflict: "Conflitto moderato", + courtsPresetSevereConflict: "Conflitto grave", + courtsPresetHarassment: "Molestie o abusi", + courtsPresetContentModeration: "Moderazione dei contenuti", + courtsPresetBanRestriction: "Espulsione o restrizione", + courtsPresetGovernanceDispute: "Controversia di governance", + courtsStatusOPEN: "Aperto", + courtsStatusIN_PROGRESS: "In corso", + courtsStatusDECIDED: "Deciso", + courtsStatusCLOSED: "Chiuso", + courtsStatusSOLVED: "Risolto", + courtsStatusUNSOLVED: "Irrisolto", + courtsStatusDISCARDED: "Scartato", courtsRulesTitle: "Come funzionano i Tribunali", courtsRulesIntro: "I Tribunali sono un processo gestito dalla comunità per risolvere i conflitti e promuovere la giustizia riparativa. Dialogo, prove chiare e rimedi proporzionali hanno la priorità.", courtsRulesLifecycle: "Processo: 1) Apri caso 2) Seleziona metodo 3) Presenta prove 4) Udienza e deliberazione 5) Verdetto e rimedio 6) Adempimento e chiusura 7) Appello (se applicabile).", - courtsRulesRoles: "Accusatore: apre il caso. Difesa: persona o tribù accusata. Metodo: meccanismo scelto dalla comunità per facilitare, valutare le prove ed emettere un verdetto. Testimone: fornisce testimonianza o prove. Mediatori: persone neutrali invitate dall'accusatore e/o dalla difesa, con accesso a tutti i dettagli, che aiutano a de-escalare il conflitto e co-creare accordi.", + courtsRulesRoles: "Accusatore: apre il caso. Difesa: abitante o tribù accusata. Metodo: meccanismo scelto dalla comunità per facilitare, valutare le prove ed emettere un verdetto. Testimone: fornisce testimonianza o prove. Mediatori: abitanti neutrali invitati dall'accusatore e/o dalla difesa, con accesso a tutti i dettagli, che aiutano a de-escalare il conflitto e co-creare accordi.", courtsRulesEvidence: "Descrizione fino a 1000 caratteri. Allega immagini, audio, video e documenti PDF pertinenti e leciti. Non condividere dati privati sensibili senza consenso.", courtsRulesDeliberation: "Le udienze possono essere pubbliche o private. I giudici assicurano il rispetto, richiedono chiarimenti e possono scartare materiale irrilevante o illecito.", courtsRulesVerdict: "La riparazione è prioritaria: scuse, accordi di mediazione, moderazione dei contenuti, restrizioni temporanee o altre misure proporzionali. La motivazione deve essere registrata.", courtsRulesAppeals: "Appello: consentito quando ci sono nuove prove o un chiaro errore procedurale. Deve essere presentato entro 7 giorni salvo diversa indicazione.", - courtsRulesPrivacy: "Rispetta la privacy e la sicurezza. Doxing, odio o minacce vengono rimossi. I giudici possono modificare o sigillare parti del verbale per proteggere le persone a rischio.", + courtsRulesPrivacy: "Rispetta la privacy e la sicurezza. Doxing, odio o minacce vengono rimossi. I giudici possono modificare o sigillare parti del verbale per proteggere gli abitanti a rischio.", courtsRulesMisconduct: "Molestie, manipolazione o prove fabbricate possono portare a una risoluzione negativa immediata.", courtsRulesGlossary: "Caso: registro di un conflitto. Prove: materiali a supporto delle affermazioni. Verdetto: decisione con rimedio. Appello: richiesta di revisione del verdetto.", courtsFilterActions: "AZIONI", - courtsNoActions: "Nessuna azione in sospeso per il tuo ruolo.", - courtsCaseTitlePlaceholder: "Breve descrizione del conflitto", - courtsCaseSeverity: "Gravità", - courtsCaseSeverityNone: "Nessun livello di gravità", - courtsCaseSeverityLOW: "Bassa", - courtsCaseSeverityMEDIUM: "Media", - courtsCaseSeverityHIGH: "Alta", - courtsCaseSeverityCRITICAL: "Critico", - courtsCaseSubject: "Argomento", - courtsCaseSubjectNone: "Nessun argomento", - courtsCaseSubjectBEHAVIOUR: "Comportamento", - courtsCaseSubjectCONTENT: "Contenuto", - courtsCaseSubjectGOVERNANCE: "Governance / regole", - courtsCaseSubjectFINANCIAL: "Finanze / risorse", - courtsCaseSubjectOTHER: "Altro", - courtsHiddenRespondent: "Nascosto (visibile solo ai ruoli coinvolti).", courtsThRole: "Ruolo", courtsRoleAccuser: "Accusatore", courtsRoleDefence: "Difesa", @@ -1219,51 +1315,23 @@ module.exports = { courtsAssignJudgeBtn: "Scegli giudice", trendingTitle: "Tendenze", exploreTrending: "Esplora i contenuti più popolari nella tua rete.", - bookmarkButton: "SEGNALIBRI", - transferButton: "TRASFERIMENTI", - eventButton: "EVENTI", - taskButton: "COMPITI", votesButton: "VOTAZIONI", - reportButton: "SEGNALAZIONI", - feedButton: "FEED", - marketButton: "MERCATO", - imageButton: "IMMAGINI", - audioButton: "AUDIO", - videoButton: "VIDEO", - documentButton: "DOCUMENTI", - torrentButton: "TORRENT", - noTrendingFound: "Nessun contenuto di tendenza trovato.", - noContentMessage: "Nessun contenuto di tendenza disponibile.", - trendingDescription: "Descrizione", - trendingDate: "Data", - trendingLocation: "Posizione", - trendingPrice: "Prezzo", - trendingUrl: "URL", - trendingCategory: "Categoria", - trendingStart: "Inizio", - trendingEnd: "Fine", - trendingPriority: "Priorità", - trendingStatus: "Stato", - trendingFrom: "Da", - trendingTo: "A", - trendingConcept: "Concetto", - trendingAmount: "Importo", - trendingDeadline: "Scadenza", - trendingItemStatus: "Stato articolo", - trendingTotalVotes: "Voti totali", + shopProductButton: "NEGOZIO", + podcastButton: "PODCAST", + podcastEpisodeButton: "EPISODIO", + campaignButton: "CAMPAGNA", + logisticsRouteButton: "PERCORSO", trendingTotalOpinions: "Opinioni totali", - trendingNoContentMessage: "Nessun contenuto di tendenza disponibile.", - trendingAuthor: "Di", - trendingCreatedAtLabel: "Creato il", + trendingNoContentMessage: "Ancora nessuna tendenza.", trendingTotalCount: "Conteggio totale", tasksTitle: "Compiti", tasksDescription: "Scopri e gestisci compiti nella tua rete.", + taskSearchPlaceholder: "Cerca in Compiti...", taskTitleLabel: "Titolo", taskDescriptionLabel: "Descrizione", taskStartTimeLabel: "Ora di inizio", taskEndTimeLabel: "Ora di fine", taskPriorityLabel: "Priorità", - taskPrioritySelect: "Seleziona priorità", taskPriorityUrgent: "Urgente", taskPriorityHigh: "Alta", taskPriorityMedium: "Media", @@ -1273,13 +1341,13 @@ module.exports = { taskVisibilityLabel: "Visibilità", taskPublic: "pubblico", taskPrivate: "privato", - taskCreatedAt: "Creato il", - taskBy: "Di", taskStatus: "Stato", taskStatusOpen: "Apri", taskStatusInProgress: "In corso", taskStatusClosed: "Chiuso", - taskAssignedTo: "Assegnato a", + taskAssignedTo: "Assegnato", + taskAssignedChip: "Assegnata", + taskUnassignedChip: "Non assegnata", taskAssignees: "Assegnatari", taskAssignButton: "Assegna a me", taskUnassignButton: "Rimuovi assegnazione", @@ -1287,12 +1355,12 @@ module.exports = { taskUpdateButton: "Aggiorna", taskDeleteButton: "Elimina", taskFilterAll: "TUTTI", + taskFilterRecent: "RECENTI", taskFilterMine: "MIEI", taskFilterOpen: "APERTI", taskFilterInProgress: "IN CORSO", taskFilterClosed: "CHIUSI", taskFilterAssigned: "ASSEGNATI", - taskFilterArchived: "ARCHIVIATI", taskFilterUrgent: "URGENTE", taskFilterHigh: "ALTA", taskFilterMedium: "MEDIA", @@ -1305,35 +1373,47 @@ module.exports = { taskInProgressTitle: "Compiti in corso", taskClosedTitle: "Compiti chiusi", taskAssignedTitle: "Compiti assegnati", - taskArchivedTitle: "Compiti archiviati", - taskPublicTitle: "Compiti pubblici", - taskPrivateTitle: "Compiti privati", notasks: "Nessun compito disponibile.", noLocation: "Nessuna posizione specificata", taskSetStatus: "Imposta stato", - eventTitle: "Eventi", eventDateLabel: "Data", + eventRecurrenceLabel: "Ricorrenza", + eventRecurrenceUntil: "Ripeti fino al", + eventRecurrenceHint: "Lascia vuota la data finale per un evento singolo.", + eventUpcomingDates: "Prossime date", eventsTitle: "Eventi", eventsDescription: "Scopri e gestisci eventi nella tua rete.", - eventDescription: "Descrizione", + eventSearchPlaceholder: "Cerca in Eventi...", eventPrice: "Prezzo", eventStatus: "Stato", - eventOrganizer: "Organizzatore", eventAllSectionTitle: "Eventi", eventMineSectionTitle: "I tuoi eventi", - eventArchivedTitle: "Eventi archiviati", eventCreateSectionTitle: "Crea evento", eventUpdateSectionTitle: "Aggiorna evento", - eventDeleteButton: "Elimina", eventCreateButton: "Crea evento", eventAddToCalendar: "Aggiungi al calendario", eventVisitCalendar: "Vai al calendario", viewTask: "Visualizza attività", + viewEvent: "Vedi evento", + viewPad: "Vedi pad", + viewMap: "Vedi mappa", + viewCalendar: "Vedi calendario", viewReport: "Visualizza segnalazione", viewTransfer: "Visualizza trasferimento", viewItem: "Visualizza articolo", viewShop: "Visualizza negozio", viewProject: "Visualizza progetto", + viewJob: "Vedi Lavoro", + viewPlace: "Vedi Luogo", + generatePdf: "Genera PDF", + sharePm: "Condividi via MP", + galleryImages: "Immagini (dimensione max: 75MB ciascuna)", + galleryPhotos: "Immagini", + galleryAddPhoto: "Aggiungi Immagine", + galleryRemovePhoto: "Rimuovi", + galleryVideo: "Video (dimensione max: 75MB)", + galleryAddVideo: "Aggiungi Video", + galleryRemoveVideo: "Rimuovi video", viewVotation: "Visualizza votazione", voteCastTitle: "Esprimi il voto", voteResults: "Risultati", @@ -1341,48 +1421,26 @@ module.exports = { eventDescriptionLabel: "Descrizione", eventDescriptionPlaceholder: "Inserisci descrizione evento...", eventUpdateButton: "Aggiorna", - eventAttendeesLabel: "Partecipanti", - eventAttendeesPlaceholder: "Inserisci partecipanti, separati da virgole...", eventTagsLabel: "Tag", - eventTagsPlaceholder: "Inserisci tag evento, separati da virgole...", - eventTags: "Tag", eventPriceLabel: "Prezzo", eventUrlLabel: "URL", eventAttendees: "Partecipanti", - noAttendees: "Nessun partecipante", - eventCreatedAt: "Creato il", eventLocation: "Posizione", eventLocationLabel: "Posizione", - eventNoLocation: "Nessuna posizione specificata", - eventNoURL: "Nessun URL specificato", - eventBy: "Di", noevents: "Nessun evento disponibile.", eventDate: "Data", - eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Partecipa all'evento", - eventUnattendButton: "Lascia l'evento", - eventCreatedBy: "Creato da", - eventAttendeesCount: "Numero partecipanti", - eventCreatedByYou: "Hai creato questo evento", eventFilterAll: "TUTTI", + eventFilterRecent: "RECENTI", eventFilterMine: "MIEI", eventFilterToday: "OGGI", eventFilterWeek: "QUESTA SETTIMANA", eventFilterMonth: "QUESTO MESE", eventFilterYear: "QUEST'ANNO", eventFilterArchived: "ARCHIVIATI", - eventTodayTitle: "Eventi di oggi", - eventThisWeekTitle: "Eventi di questa settimana", - eventThisMonthTitle: "Eventi di questo mese", - eventThisYearTitle: "Eventi di quest'anno", eventPrivacyLabel: "Visibilità", eventPublic: "Pubblico", eventPrivate: "Privato", - eventPublicTitle: "Eventi pubblici", - eventNoPrice: "Evento gratuito", - eventNoImage: "Nessuna immagine caricata", - eventAttendConfirmation: "Stai partecipando a questo evento", - eventUnattendConfirmation: "Non partecipi più a questo evento", eventAttended: "Partecipato", eventUnattended: "Non partecipato", eventStatusOpen: "Aperto", @@ -1393,9 +1451,14 @@ module.exports = { tagsTopSectionTitle: "Tag più usati", tagsCloudSectionTitle: "Nuvola di tag", tagsFilterAll: "TUTTI", + tagsFilterMine: "MIE", + tagsFilterRecent: "RECENTI", + tagsMineSectionTitle: "Le mie etichette", + tagsRecentSectionTitle: "Etichette recenti", tagsFilterTop: "TOP", tagsFilterCloud: "NUVOLA", tagsNoItems: "Nessun tag disponibile.", + tagsSearchPlaceholder: "Cerca in Tag...", tagsTableHeaderTag: "TAG/LINK:", tagsTableHeaderCount: "CONTATORE:", transfersTitle: "Trasferimenti", @@ -1403,7 +1466,10 @@ module.exports = { transfersFrom: "Da", transfersTo: "A", transfersFilterAll: "TUTTI", + transfersFilterRecent: "RECENTI", transfersFilterMine: "MIEI", + transfersFilterMinePending: "IN ATTESA", + transfersFilterMineConfirmed: "FIRMATI", transfersFilterUBI: "RBU", transfersFilterMarket: "MERCATO", transfersFilterTop: "TOP", @@ -1418,7 +1484,6 @@ module.exports = { transfersCategoryEconomic: "Economica", transfersCategoryTime: "Tempo", transfersCategoryTrust: "Fiducia", - transfersCategoryApply: "Applica", transfersUnitEco: "ECO", transfersUnitHours: "h", transfersUnitTrust: "fiducia", @@ -1435,18 +1500,12 @@ module.exports = { transfersDeadline: "Scadenza", transfersTags: "Tag", transfersStatus: "Stato", - transfersStatusUnconfirmed: "NON CONFERMATO", - transfersStatusClosed: "CHIUSO", - transfersStatusDiscarded: "SCARTATO", - transfersCreatedAt: "Creato il", transfersConfirmations: "CONFERME", - transfersContainingBlock: "Blocco contenitore", - transfersExportContract: "Crea contratto", + transfersExportContract: "Genera Fattura", transfersConfirmButton: "Conferma trasferimento", transfersNoItems: "Nessun trasferimento trovato.", transfersMineSectionTitle: "I tuoi trasferimenti", transfersUBISectionTitle: "Trasferimenti RBU", - transfersMarketSectionTitle: "Trasferimenti di mercato", transfersTopSectionTitle: "Trasferimenti principali", transfersPendingSectionTitle: "Trasferimenti in attesa", transfersUnconfirmedSectionTitle: "Trasferimenti non confermati", @@ -1454,21 +1513,13 @@ module.exports = { transfersDiscardedSectionTitle: "Trasferimenti scartati", transfersCreateSectionTitle: "Crea trasferimento", transfersAllSectionTitle: "Trasferimenti", - transfersFilterFavs: "Preferiti", - transfersFavsSectionTitle: "Trasferimenti preferiti", - transfersSearchLabel: "Cerca", - transfersSearchPlaceholder: "Cerca concetto, tag, abitanti...", + transfersSearchPlaceholder: "Cerca in Trasferimenti...", transfersMinAmountLabel: "Importo minimo", transfersMaxAmountLabel: "Importo massimo", - transfersSortLabel: "Ordina per", transfersSortRecent: "Più recenti", transfersSortAmount: "Importo più alto", transfersSortDeadline: "Scadenza più vicina", transfersSearchButton: "Cerca", - transfersFavoriteButton: "Preferito", - transfersUnfavoriteButton: "Rimuovi preferito", - transfersMessageUserButton: "Messaggio", - transfersExpiringSoonBadge: "IN SCADENZA", transfersExpiredBadge: "SCADUTO", transfersUpdatedAt: "Aggiornato", transfersNoMatch: "Nessun trasferimento corrisponde alla ricerca.", @@ -1481,6 +1532,10 @@ module.exports = { voteClosedTitle: "Votazioni chiuse", voteAllSectionTitle: "Votazioni", voteCreateButton: "Crea votazione", + voteErrorTitle: "Controlla il modulo", + voteErrorDeadlineMin: "La scadenza deve essere di almeno {days} giorni da ora.", + voteErrorGeneric: "Non è stato possibile salvare la votazione.", + voteLocked: "Questa votazione ha già ricevuto voti o opinioni, o appartiene al Parlamento o ai Tribunali: non si può più modificare né eliminare.", voteUpdateButton: "Aggiorna", voteDeleteButton: "Elimina", voteOptionYes: "SÌ", @@ -1490,6 +1545,7 @@ module.exports = { voteFollowMajority: "SEGUI MAGGIORANZA", voteNotInterested: "NON INTERESSATO", voteFilterAll: "TUTTI", + voteFilterRecent: "RECENTI", voteFilterMine: "MIEI", voteFilterOpen: "APERTE", voteFilterClosed: "CHIUSE", @@ -1505,37 +1561,28 @@ module.exports = { voteTags: "Tag", voteOpinions: "Opinioni", novotes: "Nessuna proposta di voto disponibile.", - voteBy: "Di", voteCreatedAt: "Creato il", voteNoQuestion: "Nessuna domanda fornita", voteUnknownCreator: "Creatore sconosciuto", voteUnknownDate: "Data sconosciuta", - errorVoteNotFound: "Voto non trovato", - errorAlreadyVoted: "Hai già espresso un'opinione.", - errorVoteClosedCannotEdit: "Non puoi modificare una votazione chiusa", - errorVoteDeadlinePassed: "La scadenza per questa votazione è passata", - errorRetrievingVote: "Errore nel recupero del voto", - errorCreatingVote: "Errore nella creazione del voto", - errorVoteAlreadyVoted: "Non puoi modificare dopo aver espresso un'opinione", - errorDeletingOldVote: "Errore nell'eliminazione della vecchia opinione", - errorCreatingUpdatedVote: "Errore nella creazione dell'opinione aggiornata", - errorCreatingTombstone: "Errore nella creazione del tombstone", voteDetailSectionTitle: 'Dettagli voto', voteCommentsLabel: 'Commenti', voteCommentsForumButton: 'Apri discussione', voteCommentsSectionTitle: 'Apri discussione', voteNoCommentsYet: 'Non ci sono ancora commenti. Sii il primo a rispondere.', + commentsGoToLast: "Vai all'ultimo commento", voteNewCommentPlaceholder: 'Scrivi il tuo commento qui…', voteNewCommentButton: 'Pubblica commento', voteNewCommentLabel: 'Aggiungi un commento', cvTitle: "Curriculum", - cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Modifica CV", cvCreateSectionTitle: "Crea CV", cvDescription: "Gestisci e condividi le tue competenze e informazioni professionali.", cvNameLabel: "Nome completo", cvDescriptionLabel: "Riepilogo", cvPhotoLabel: "Foto", + cvPdfLabel: "CV (PDF)", + cvVisitButton: "Visita il CV", cvPersonalExperiencesLabel: "Esperienze personali", cvPersonalSkillsLabel: "Competenze personali (separate da virgole)", cvOasisExperiencesLabel: "Esperienze di contributo Oasis", @@ -1548,29 +1595,130 @@ module.exports = { cvLocationLabel: "Posizione", cvStatusLabel: "Stato", cvPreferencesLabel: "Preferenze", - cvOasisContributorLabel: "Contributore Oasis", cvPersonal: "Personale", cvOasis: "Contributore Oasis (opzionale)", - cvOasisContributorView: "Contributo Oasis", + cvOasisContributorView: "Contributo", cvEducational: "Formazione (opzionale)", cvEducationalView: "Formazione", cvProfessional: "Professionale (opzionale)", cvProfessionalView: "Professionale", cvAvailability: "Disponibilità (opzionale)", - cvAvailabilityView: "Disponibilità", cvUpdateButton: "Aggiorna", cvCreateButton: "Crea CV", - cvContactLabel: "Contatto", cvCreatedAt: "Creato il", cvUpdatedAt: "Aggiornato il", cvEditButton: "Aggiorna", cvDeleteButton: "Elimina", cvNoCV: "Nessun CV trovato.", blogSubject: "Oggetto", - blogMessage: "Corpo limitato a 8096 caratteri.", - blogImage: "Carica media (max: 50MB)", - blogPublish: "Anteprima", - noPopularMessages: "Nessun messaggio popolare pubblicato ancora", + blogMessage: "Messaggio", + blogImage: "Carica media (max: 75MB)", + blogMedia: "Allegati (fino a 8 immagini e un video)", + blogPublish: "Pubblica", + pollsTitle: "Sondaggi", + dataTitle: "Corrispondenze", + dataDescription: "Esplora e visualizza le corrispondenze della tua rete.", + dataFilterAll: "TUTTI", + dataFilterMine: "I MIEI", + dataFilterRecent: "RECENTI", + dataFilterTop: "TOP", + dataKindInhabitants: "ABITANTI", + dataKindJobs: "LAVORI", + dataKindProjects: "PROGETTI", + dataKindEvents: "EVENTI", + dataKindTribes: "TRIBÙ", + dataKindMarket: "MERCATO", + dataKindHousing: "ALLOGGI", + dataKindIndustry: "INDUSTRIA", + dataKindTasks: "COMPITI", + dataKindReports: "RAPPORTI", + dataKindVotes: "VOTAZIONI", + dataSearchPlaceholder: "Cerca in Corrispondenze...", + dataCohesionTitle: "Coefficiente di coesione (CC)", + dataCcShort: "CC", + dataCohesionHint: "Affinità media fra tutto ciò che la rete ha pubblicato.", + dataAffinity: "Affinità", + dataCommonTerms: "Parola chiave", + dataReasonFollowing: "Supporti questo abitante", + dataReasonTribe: "Condivide una tribù con te", + dataReasonPinned: "Coincide con qualcosa che hai fissato", + dataReasonCv: "Corrisponde alle competenze del tuo CV", + dataReasonContent: "Legato a ciò che pubblichi", + dataReasonRelated: "Legato ai tuoi interessi", + dataReasonAlike: "Pubblicato da un abitante affine a te", + dataReasonSupportsYou: "Ti supporta", + dataReasonMutual: "Supporto reciproco", + dataReasonRated: "Ben valutato dalla rete", + dataReasonNear: "Vicino a te", + dataStatPeople: "CV", + dataStatConnected: "Collegati", + dataStatSkills: "Competenze", + dataBestMatch: "Miglior match", + dataStatIsolated: "Isolati", + dataStatEntities: "Entità confrontate", + dataStatTerms: "Termini distinti", + dataStatPairs: "Coppie collegate", + dataMatchesTitle: "Corrispondenze", + dataMatchesHint: "Suggerimenti personalizzati dell'algoritmo.", + dataTermsTitle: "Parole chiave", + dataTermsHint: "I termini che compaiono nel maggior numero di contenuti.", + dataConnections: "Corrispondenze collegate", + dataNoMatches: "Nessun dato incrociato da mostrare.", + dataNoProfile: "Pubblica contenuti o scrivi il tuo CV così l'algoritmo impara cosa ti interessa.", + pollsDescription: "Modulo per interrogare la rete e contare le risposte.", + pollFilterAll: "TUTTI", + pollFilterMine: "I MIEI", + pollFilterRecent: "RECENTI", + pollFilterTop: "TOP", + pollFilterVoted: "VOTATI", + pollFilterOpen: "APERTI", + pollFilterClosed: "CHIUSI", + pollCreateButton: "Crea Sondaggio", + pollCreateTitle: "Crea Sondaggio", + pollEditTitle: "Modifica Sondaggio", + pollQuestion: "Domanda", + pollQuestionPlaceholder: "Cosa vuoi chiedere?", + pollOptions: "Opzioni (una per riga)", + pollOutcome: "Risultato", + pollNoVotesYet: "Ancora nessun voto", + pollTie: "Pareggio", + pollOptionsPlaceholder: "Piazza\nParco\nBar", + pollAnonymous: "Anonimo", + pollAnonymousLabel: "Sondaggio anonimo", + pollMultiple: "Scelta multipla", + pollMultipleLabel: "Consenti più risposte", + pollDeadline: "Scadenza", + pollTags: "Etichette", + pollPublishButton: "Pubblica Sondaggio", + pollUpdateButton: "Aggiorna", + pollCloseButton: "Chiudi", + pollDeleteButton: "Elimina", + pollVoteButton: "Vota", + pollChangeVote: "Cambia Voto", + pollVoters: "Votanti", + pollComments: "Commenti", + pollStatusLabel: "Stato", + pollStatusOpen: "APERTO", + pollStatusClosed: "CHIUSO", + pollSearchPlaceholder: "Cerca in Sondaggi...", + pollsNoItems: "Nessun sondaggio trovato.", + viewPoll: "Vedi Sondaggio", + pollChatCreate: "Crea Sondaggio", + pollInChat: "Sondaggio", + blogTitle: "Blog", + blogDescription: "Modulo per scoprire e gestire i blog.", + blogFilterAll: "TUTTI", + blogFilterMine: "I MIEI", + blogFilterRecent: "RECENTI", + blogFilterFavorites: "PREFERITI", + blogFilterTop: "TOP", + blogCreateButton: "Crea Blog", + blogSearchPlaceholder: "Cerca in Blog...", + blogComments: "Commenti", + blogAllowComments: "Consenti commenti", + blogCommentsClosed: "L'autore ha chiuso i commenti di questo blog.", + blogNoItems: "Nessun blog trovato.", + viewBlog: "Vedi Blog", forumTitle: "Forum", forumCategoryLabel: "Categoria", forumTitleLabel: "Titolo", @@ -1578,43 +1726,21 @@ module.exports = { forumCreateButton: "Crea forum", forumCreateSectionTitle: "Crea forum", forumDescription: "Parla apertamente con altri abitanti nella tua rete.", + forumSearchPlaceholder: "Cerca in Forum...", forumFilterAll: "TUTTI", forumFilterMine: "MIEI", forumFilterRecent: "RECENTI", forumFilterTop: "TOP", - forumMineSectionTitle: "I tuoi forum", - forumRecentSectionTitle: "Forum recenti", - forumAllSectionTitle: "Forum", - forumDeleteButton: "Elimina", forumParticipants: "partecipanti", forumMessages: "messaggi", - forumLastMessage: "Ultimo messaggio", forumMessageLabel: "Messaggio", forumMessagePlaceholder: "Scrivi il tuo messaggio...", forumSendButton: "Invia", - forumVisitForum: "Visita forum", noForums: "Nessun forum trovato.", - forumVisitButton: "Visita forum", - forumCatGENERAL: "Generale", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "Politica", - forumCatTECH: "Tecnologia", - forumCatSCIENCE: "Scienza", - forumCatMUSIC: "Musica", - forumCatART: "Arte", - forumCatGAMING: "Gaming", - forumCatBOOKS: "Libri", - forumCatFILMS: "Film", - forumCatPHILOSOPHY: "Filosofia", - forumCatSOCIETY: "Società", - forumCatPRIVACY: "Privacy", - forumCatCYBERWARFARE: "Cyberwarfare", - forumCatSURVIVALISM: "Survivalismo", + forumVisitButton: "Vedi Forum", + forumTopicLabel: "Argomento", imageTitle: "Immagini", imageDescription: "Esplora e gestisci immagini nella tua rete.", - imagePluginTitle: "Titolo", - imagePluginDescription: "Descrizione", imageMineSectionTitle: "Le tue immagini", imageCreateSectionTitle: "Carica immagine", imageUpdateSectionTitle: "Aggiorna immagine", @@ -1623,17 +1749,14 @@ module.exports = { imageTopSectionTitle: "Immagini più votate", imageFavoritesSectionTitle: "Preferiti", imageGallerySectionTitle: "Galleria", - imageMemeSectionTitle: "Meme", imageFilterAll: "TUTTE", imageFilterMine: "MIE", imageFilterRecent: "RECENTI", imageFilterTop: "TOP", imageFilterFavorites: "PREFERITE", imageFilterGallery: "GALLERIA", - imageFilterMeme: "MEME", imageCreateButton: "Carica immagine", imageUpdateButton: "Aggiorna", - imageDeleteButton: "Elimina", imageAddFavoriteButton: "Aggiungi preferito", imageRemoveFavoriteButton: "Rimuovi preferito", imageFileLabel: "Seleziona un file immagine (.jpeg, .jpg, .png, .gif)", @@ -1643,23 +1766,21 @@ module.exports = { imageTitlePlaceholder: "Opzionale", imageDescriptionLabel: "Descrizione", imageDescriptionPlaceholder: "Opzionale", - imageMemeLabel: "MEME?", imageNoFile: "Nessun file immagine fornito", noImages: "Nessuna immagine disponibile.", - imageSearchPlaceholder: "Cerca titolo, tag, descrizione, autore...", + imageSearchPlaceholder: "Cerca in Immagini...", imageSortRecent: "Più recenti", imageSortOldest: "Più vecchie", imageSortTop: "Più votate", imageSearchButton: "Cerca", - imageMessageAuthorButton: "Messaggio", imageUpdatedAt: "Aggiornato", imageNoMatch: "Nessuna immagine corrisponde alla ricerca.", + feedSearchPlaceholder: "Cerca in Feed...", feedTitle: "Feed", createFeedTitle: "Crea Feed", createFeedButton: "Invia Feed!", feedPlaceholder: "Cosa succede? (max 280 caratteri)", TODAYButton: "OGGI", - CREATEButton: "Crea Feed", totalOpinions: "Opinioni totali", moreVoted: "Più votato", noFeedsFound: "Nessun feed trovato.", @@ -1669,7 +1790,7 @@ module.exports = { alreadyRefeeded: "Hai già fatto il refeed.", activityTitle: "Attività", yourActivity: "La tua attività", - globalActivity: "Attività globale", + globalActivity: "Attività", activityList: "Attività", activityDesc: "Vedi l'ultima attività nella tua rete.", allButton: "TUTTI", @@ -1682,20 +1803,13 @@ module.exports = { concept: "Concetto", description: "Descrizione", meme: "Meme", - activityContact: "Contatto", - activityBy: "Nome", - activityPixelia: "Nuovo pixel aggiunto", - viewImage: "Vedi immagine", - playAudio: "Riproduci audio", - playVideo: "Riproduci video", typeRecent: "RECENTI", - errorActivity: "Errore nel recupero dell'attività", - typePost: "BLOGS", - recentButton: "RECENTI", + typeTop: "TOP", + typePost: "BLOG", + activityUpdateLabel: "AGGIORNAMENTO", typeTribe: "TRIBÙ", typeLarp: "L.A.R.P.", typeInhabitants: "ABITANTI", - activityProfileUpdated: "ha aggiornato il profilo", typeAbout: "ABITANTI", typeCurriculum: "CVS", typeImage: "IMMAGINI", @@ -1717,10 +1831,10 @@ module.exports = { typeFeed: "FEEDS", typeContact: "CONTATTO", typePub: "PUB", - typeTombstone: "TOMBSTONE", + typeTombstone: "ELIMINATO", typeBanking: "BANCA", typeBankWallet: "BANCA/PORTAFOGLIO", - typeBankClaim: "BANCA/UBI", + typeBankClaim: "BANCA/RBU", typeKarmaScore: "KARMA", typeParliament: "PARLAMENTO", typeSpread: "DIFFUSIONI", @@ -1729,7 +1843,7 @@ module.exports = { typeParliamentProposal:"Parlamento · Proposta", typeParliamentRevocation:"Parlamento · Revoca", typeParliamentLaw: "Parlamento · Nuova legge", - typeCourts: "COURTS", + typeCourts: "TRIBUNALI", typeCourtsCase: "Tribunali · Caso", typeCourtsEvidence: "Tribunali · Prova", typeCourtsAnswer: "Tribunali · Risposta", @@ -1739,8 +1853,6 @@ module.exports = { typeCourtsSettlementAccepted: "Tribunali · Accordo accettato", typeCourtsNomination: "Tribunali · Nomina", typeCourtsNominationVote: "Tribunali · Voto di nomina", - activitySupport: "Nuova alleanza forgiata", - activityJoin: "Nuovo PUB unito", question: "Domanda", deadline: "Scadenza", status: "Stato", @@ -1754,14 +1866,10 @@ module.exports = { date: "Data", category: "Categoria", attendees: "Partecipanti", - activitySpread: "->", - visitLink: "Visita link", - viewDocument: "Vedi documento", location: "Posizione", contentWarning: "Oggetto", - personName: "Nome abitante", bankWalletConnected: "Portafoglio ECOin", - bankUbiReceived: "UBI ricevuto", + bankUbiReceived: "RBU ricevuto", bankTx: "Tx", bankEpochShort: "Epoca", bankingUserEngagementScore: "Punteggio KARMA", @@ -1769,14 +1877,13 @@ module.exports = { link: "Link", activityProjectFollow: "%OASIS% ora sta %ACTION% questo progetto %PROJECT%", activityProjectUnfollow: "%OASIS% ora sta %ACTION% questo progetto %PROJECT%", - activityProjectPledged: "%OASIS% ha %ACTION% %AMOUNT% al progetto %PROJECT%", following: "SEGUENDO", unfollowing: "NON SEGUENDO", pledged: "CONTRIBUITO", parliamentCandidatureId: "Candidature", - parliamentGovMethod: "Method", - parliamentVotesReceived: "Votes received", - parliamentMethodANARCHY: "Anarchy", + parliamentGovMethod: "Metodo", + parliamentVotesReceived: "Voti ricevuti", + parliamentMethodANARCHY: "Anarchia", parliamentMethodVOTE: "Voto comunitario", parliamentMethodRANKED: "Scelta graduata", parliamentMethodPLURALITY: "Pluralità", @@ -1815,26 +1922,17 @@ module.exports = { courtsVotesSlashTotal: "SÌ / TOTALE", courtsOpenVote: "Apri votazione", courtsAnswerTitle: "Rispondi alla denuncia", - courtsStanceADMIT: "Ammetti", - courtsStanceDENY: "Nega", - courtsStancePARTIAL: "Parziale", - courtsStanceCOUNTERCLAIM: "Controdenuncia", - courtsStanceNEUTRAL: "Neutrale", courtsVerdictResult: "Risultato", courtsVerdictOrders: "Ordini", courtsSettlementText: "Condizioni", courtsSettlementAccepted: "Accettato", - courtsSettlementPending: "In attesa", courtsJudge: "Giudice", courtsThSupports: "Supporti", courtsFilterOpenCase: 'Apri caso', - courtsEvidenceFileLabel: 'File di prova (immagine, audio, video o PDF)', - courtsCaseMediators: 'Mediatori', courtsCaseMediatorsPh: 'Oasis ID dei mediatori, separati da virgola', courtsMediatorsLabel: 'Mediatori', courtsThCase: 'Caso', courtsThCreatedAt: 'Data di inizio', - courtsThActions: 'Azioni', courtsPublicPrefLabel: 'Visibilità dopo la risoluzione', courtsPublicPrefYes: 'Acconsento che questo caso sia completamente pubblico', courtsPublicPrefNo: 'Preferisco mantenere i dettagli privati', @@ -1842,8 +1940,11 @@ module.exports = { courtsMethodMEDIATION: 'Mediazione', courtsNoCases: 'Nessun caso.', reportsTitle: "Rapporti", - reportsDescription: "Gestisci e monitora le segnalazioni relative a problemi, bug, abusi e avvisi sui contenuti nella tua rete.", + reportsDescription: "Gestisci e segui le segnalazioni della tua rete.", + reportsSearchPlaceholder: "Cerca in Rapporti...", reportsFilterAll: "TUTTI", + reportsFilterRecent: "RECENTI", + reportsFilterTop: "TOP", reportsFilterMine: "MIEI", reportsFilterFeatures: "FUNZIONALITÀ", reportsFilterBugs: "BUG", @@ -1856,18 +1957,13 @@ module.exports = { reportsFilterInvalid: "NON VALIDI", reportsCreateButton: "Crea segnalazione", reportsTitleLabel: "Titolo", - reportsDescriptionLabel: "Descrizione", - reportsDescriptionPlaceholder: "Fornisci una descrizione dettagliata.", reportsCategory: "Categoria", - reportsCategoryLabel: "Categoria", reportsCategoryFeatures: "Funzionalità", reportsCategoryBugs: "Bug", reportsCategoryAbuse: "Abuso", - reportsCategoryContent: "Problemi di contenuto", + reportsCategoryContent: "Contenuto", reportsUpdateButton: "Aggiorna", reportsDeleteButton: "Elimina", - reportsDateLabel: "Data", - reportsUploadFile: "Carica media (max: 50MB)", reportsMineSectionTitle: "Le tue segnalazioni", reportsFeaturesSectionTitle: "Richieste di funzionalità", reportsBugsSectionTitle: "Bug", @@ -1875,11 +1971,6 @@ module.exports = { reportsContentSectionTitle: "Problemi di contenuto", reportsAllSectionTitle: "Rapporti", reportsNoItems: "Nessuna segnalazione disponibile.", - reportsValidationTitle: "Inserisci un titolo valido.", - reportsValidationDescription: "La descrizione non può essere vuota.", - reportsValidationCategory: "Seleziona una categoria.", - reportsCreatedAt: "Creato il", - reportsCreatedBy: "Di", reportsSeverity: "Gravità", reportsSeverityLow: "Bassa", reportsSeverityMedium: "Media", @@ -1890,18 +1981,14 @@ module.exports = { reportsStatusUnderReview: "In revisione", reportsStatusResolved: "Risolto", reportsStatusInvalid: "Non valido", - reportsUpdateStatusButton: "Aggiorna stato", - reportsAnonymityOption: "Invia in modo anonimo", - reportsAnonymousAuthor: "Anonimo", reportsConfirmButton: "CONFERMA SEGNALAZIONE!", reportsConfirmations: "Conferme", reportsConfirmedSectionTitle: "Segnalazioni confermate", - reportsCreateTaskButton: "CREA COMPITO", + reportsCreateTaskButton: "Crea Compito", reportsOpenSectionTitle: "Segnalazioni aperte", reportsUnderReviewSectionTitle: "Segnalazioni in revisione", reportsResolvedSectionTitle: "Segnalazioni risolte", reportsInvalidSectionTitle: "Segnalazioni non valide", - reportsTemplateSectionTitle: 'Modello di segnalazione', reportsBugTemplateTitle: 'Dettagli di riproduzione (Bug)', reportsFeatureTemplateTitle: 'Dettagli (Funzionalità)', reportsAbuseTemplateTitle: 'Dettagli (Abuso)', @@ -1940,6 +2027,20 @@ module.exports = { reportsRequestedActionLabel: 'Azione richiesta', reportsRequestedActionPlaceholder: 'Rimuovi, nascondi, tagga, avvisa, ecc.', tribesTitle: "Tribù", + tribeFilterAll: "TUTTE", + tribeFilterRecent: "RECENTI", + tribeFilterMine: "MIE", + tribeFilterMembership: "ADESIONE", + tribeFilterSubtribes: "SOTTOTRIBÙ", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALLERIA", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "MIEI", + tribeFeedFilterALL: "TUTTI", + tribeFeedFilterRECENT: "RECENTI", + courtsStanceDENY: "Nega", + courtsStanceADMIT: "Ammetti", + courtsStancePARTIAL: "Ammetti in parte", tribeAllSectionTitle: "Tribù", tribeMineSectionTitle: "Le tue tribù", tribeCreateSectionTitle: "Crea tribù", @@ -1947,24 +2048,15 @@ module.exports = { tribeGallerySectionTitle: "Galleria tribù", tribeRecentSectionTitle: "Tribù recenti", tribeTopSectionTitle: "Tribù popolari", - tribeviewTribeButton: "Visita tribù", - tribeviewSubTribeButton: "Visita Sotto-Tribù", tribeRootLabel: "RADICE", tribeDescription: "Esplora o crea tribù nella tua rete.", - tribeFilterAll: "TUTTE", - tribeFilterMine: "MIE", - tribeFilterMembership: "ISCRIZIONE", - tribeFilterRecent: "RECENTI", - tribeFilterTop: "TOP", - tribeFilterSubtribes: "SOTTO-TRIBÙ", - tribeFilterGallery: "GALLERIA", tribeMainTribeLabel: "TRIBÙ PRINCIPALE", tribeCreateButton: "Crea tribù", tribeUpdateButton: "Aggiorna", tribeDeleteButton: "Elimina", - tribeImageLabel: "Carica media (max: 50MB)", + tribeImageLabel: "Carica media (max: 75MB)", tribeTitleLabel: "Titolo", - searchTribesPlaceholder: "FILTRA tribù PER NOME …", + searchTribesPlaceholder: "Cerca in Tribù...", tribeTitlePlaceholder: "Nome della tribù", tribeDescriptionLabel: "Descrizione", tribeDescriptionPlaceholder: "Descrivi questa tribù", @@ -1976,13 +2068,17 @@ module.exports = { tribeModeLabel: "MODE", tribeIsAnonymousLabel: "STATO", tribeMembersCount: "Membri", - tribeInviteCodePlaceholder: "Inserisci codice invito", - tribeJoinByCodeButton: "Unisciti con codice", - tribeJoinButton: "UNISCITI", tribeEnterInvite: "INSERISCI CODICE", + subscriptionTitle: "Iscrizioni", + subscriptionOn: "Iscritto", + subscriptionOff: "Non iscritto", + subscriptionSubscribe: "Iscriviti", + subscriptionUnsubscribe: "Annulla iscrizione", + subscriptionSubscribers: "Iscritti", + pmListLabel: "Invia a", + pmListNone: "— Nessuna lista —", + pmListNoSubscribers: "Nessun iscritto", tribeLeaveButton: "ESCI", - tribeYes: "SÌ", - tribeNo: "NO", tribePublic: "PUBBLICO", tribePrivate: "PRIVATO", tribeGenerateInvite: "INVITO SINGOLO", @@ -1991,13 +2087,8 @@ module.exports = { tribeRemoveInvitation: "RIMUOVI INVITO", tribeCreatedAt: "Creato il", tribeAuthor: "Di", - tribeAuthorLabel: "AUTORE", tribeStrict: "Rigoroso", tribeOpen: "Aperto", - tribeFeedFilterRECENT: "RECENTI", - tribeFeedFilterMINE: "MIEI", - tribeFeedFilterALL: "TUTTI", - tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Ricondivisioni", tribeFeedRefeed: "Ricondividi", tribeFeedMessagePlaceholder: "Scrivi un feed…", @@ -2007,33 +2098,28 @@ module.exports = { tribeNotFound: "Tribù non trovata!", createTribeTitle: "Crea tribù", updateTribeTitle: "Aggiorna tribù", - tribeSectionOverview: "Panoramica", tribeSectionInhabitants: "Abitanti", tribeSectionVotations: "VOTAZIONI", tribeSectionEvents: "EVENTI", - tribeSectionReports: "Segnalazioni", tribeSectionTasks: "COMPITI", tribeSectionFeed: "FEED", tribeSectionForum: "FORUM", - tribeSectionMarket: "Mercato", - tribeSectionJobs: "Lavori", - tribeSectionProjects: "Progetti", tribeSectionMedia: "Media", tribeSectionImages: "IMMAGINI", tribeSectionAudios: "AUDIO", tribeSectionVideos: "VIDEO", tribeSectionDocuments: "DOCUMENTI", tribeSectionBookmarks: "SEGNALIBRI", - tribeSectionMaps: "MAPS", - tribeSectionPads: "PADS", - tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionMaps: "MAPPE", + tribeSectionPads: "PAD", + tribeSectionChats: "CHAT", + tribeSectionCalendars: "CALENDARI", + tribePadCreate: "Crea pad", + tribeChatCreate: "Crea chat", + tribeCalendarCreate: "Crea calendario", + tribePadsEmpty: "Ancora nessun pad.", + tribeChatsEmpty: "Ancora nessuna chat.", + tribeCalendarsEmpty: "Ancora nessun calendario.", tribeInhabitantsEmpty: "Nessun abitante in questa tribù.", tribeEventCreate: "Crea evento", tribeEventsEmpty: "Nessun evento.", @@ -2055,11 +2141,6 @@ module.exports = { tribeTaskStatusClosed: "CHIUDI", tribeTaskAssign: "ASSEGNA", tribeTaskUnassign: "RIMUOVI", - tribeReportCreate: "Crea segnalazione", - tribeReportsEmpty: "Nessuna segnalazione.", - tribeReportTitle: "Titolo", - tribeReportDescription: "Descrizione", - tribeReportCategory: "Categoria", tribeVotationCreate: "Crea votazione", tribeVotationsEmpty: "Nessuna votazione.", tribeVotationTitle: "Titolo", @@ -2077,27 +2158,6 @@ module.exports = { tribeForumCategory: "Categoria", tribeForumReply: "Rispondi", tribeForumReplies: "Risposte", - tribeMarketCreate: "Crea annuncio", - tribeMarketEmpty: "Nessun annuncio.", - tribeMarketTitle: "Titolo", - tribeMarketDescription: "Descrizione", - tribeMarketPrice: "Prezzo", - tribeMarketImage: "Immagine", - tribeMarketCategory: "Categoria", - tribeJobCreate: "Crea lavoro", - tribeJobsEmpty: "Nessun lavoro.", - tribeJobTitle: "Titolo", - tribeJobDescription: "Descrizione", - tribeJobLocation: "Posizione", - tribeJobSalary: "Stipendio", - tribeJobDeadline: "Scadenza", - tribeProjectCreate: "Crea progetto", - tribeProjectsEmpty: "Nessun progetto.", - tribeProjectTitle: "Titolo", - tribeProjectDescription: "Descrizione", - tribeProjectGoal: "Obiettivo", - tribeProjectFunded: "Finanziato", - tribeProjectDeadline: "Scadenza", tribeMediaUpload: "Carica media", readDocument: "Leggi Documento", tribeCreateImage: "Crea Immagine", @@ -2108,21 +2168,20 @@ module.exports = { tribeMediaEmpty: "Nessun media.", tribeMediaTitle: "Titolo", tribeMediaDescription: "Descrizione", - tribeMediaType: "Tipo", tribeMediaTypeImage: "Immagine", tribeMediaTypeVideo: "Video", tribeMediaTypeAudio: "Audio", tribeMediaTypeDocument: "Documento", tribeMediaTypeBookmark: "Segnalibro", tribeContentDelete: "ELIMINA", + tribeReachTribe: "TRIBÙ", + tribeReachNotice: "OASIS lo mostra a tutti gli abitanti e CLEARNET anche sul web aperto. Aprirlo lascia una copia leggibile fuori dalla tribù: tornare a TRIBÙ lo nasconde, ma le copie già condivise non possono essere cancellate.", + tribeReachPrivate: "Il contenuto di una tribù privata può restare solo dentro la tribù.", + tribeReachOpenedBy: "Aperto da", + tribeReachChange: "Dove viene pubblicato", tribeInviteCodeText: "Codice invito: ", oasisVersionLabel: "Versione di Oasis", - tribeInviteCodeHint: "Condividi questo codice con chi vuoi invitare. Può unirsi tramite /invites → Tribes.", - tribeGroupTribe: "Tribù", - tribeGroupOffice: "Ufficio", - tribeGroupNetwork: "Rete", - tribeGroupEconomy: "Economia", - tribeGroupMedia: "Media", + tribeInviteCodeHint: "Condividi questo codice con chi vuoi invitare. Può unirsi tramite /invites.", tribeStatusOpen: "APERTO", tribeStatusClosed: "CHIUSO", tribeStatusInProgress: "IN CORSO", @@ -2132,33 +2191,17 @@ module.exports = { tribePriorityCritical: "CRITICA", tribeTaskFilterAll: "TUTTI", tribeMediaFilterAll: "TUTTI", - tribeReportCatBug: "BUG", - tribeReportCatAbuse: "ABUSO", - tribeReportCatContent: "CONTENUTO", - tribeReportCatOther: "ALTRO", tribeForumCatGeneral: "GENERALE", tribeForumCatProposal: "PROPOSTA", tribeForumCatQuestion: "DOMANDA", tribeForumCatAnnouncement: "ANNUNCIO", - tribeMarketCatGoods: "BENI", - tribeMarketCatServices: "SERVIZI", - tribeMarketCatFood: "CIBO", - tribeMarketCatOther: "ALTRO", tribeStatusLabel: "Stato", tribeSubTribes: "SOTTO-TRIBÙ", tribeSubTribesCreate: "Crea Sotto-Tribù", tribeSubTribesStrictDenied: "La modalità rigida della tribù non ti consente di creare nuove sotto-tribù. Si prega di contattare l'amministratore.", - tribeSubTribesEmpty: "Nessuna sotto-tribù creata.", - tribeActivityJoined: "ISCRITTO", - tribeActivityLeft: "USCITO", - tribeActivityFeed: "FEED", tribeActivityRefeed: "RICONDIVISIONE", - tribeGroupAnalytics: "Analisi", - tribeGroupCreative: "Creativo", tribeSectionActivity: "ATTIVITÀ", tribeSectionTrending: "TENDENZE", - tribeSectionOpinions: "OPINIONI", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAG", tribeSectionSearch: "RICERCA", tribeActivityEmpty: "Nessuna attività.", @@ -2170,25 +2213,13 @@ module.exports = { tribeTrendingPeriodWeek: "Questa settimana", tribeTrendingPeriodAll: "Sempre", tribeTrendingEngagement: "coinvolgimento", - tribeOpinionsEmpty: "Nessuna opinione.", - tribeOpinionsCast: "Vota", - tribeOpinionsRankings: "Classifiche", - tribeOpinionsAlreadyVoted: "Già votato", tribeTopCategory: "Più votato", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "Tela collaborativa di pixel art.", - tribePixeliaPaint: "Dipingi", - tribePixeliaContributors: "contributori", - tribePixeliaTotalPixels: "pixel dipinti", tribeTagsEmpty: "Nessun tag trovato.", - tribeTagsCloud: "Nuvola di tag", - tribeTagsContentWith: "Contenuto con tag", - tribeSearchPlaceholder: "Cerca nei contenuti della tribù...", tribeSearchEmpty: "Nessun risultato trovato.", - tribeSearchResults: "Risultati", tribeSearchMinChars: "Inserisci almeno 2 caratteri per cercare.", agendaTitle: "Agenda", agendaDescription: "Qui trovi tutti gli elementi assegnati a te.", + agendaSearchPlaceholder: "Cerca in Agenda...'agenda...", agendaFilterAll: "TUTTI", agendaFilterToday: "OGGI", agendaFilterUpcoming: "PROSSIMI", @@ -2202,23 +2233,15 @@ module.exports = { agendaFilterReports: "SEGNALAZIONI", agendaFilterTransfers: "TRASFERIMENTI", agendaFilterJobs: "LAVORI", + agendaFilterIndustry: "INDUSTRIA", + agendaFilterHousing: "ALLOGGI", agendaFilterProjects: "PROGETTI", agendaFilterCalendars: "CALENDARI", agendaNoItems: "Nessun incarico trovato.", - agendaAuthor: "Di", agendaDiscardButton: "Scarta", agendaRestoreButton: "Ripristina", - agendaCreatedAt: "Creato il", - agendaTitleLabel: "Titolo", + agendaRemoveButton: "Rimuovi", agendaMembersCount: "Membri", - agendaDescriptionLabel: "Descrizione", - agendaStatus: "Stato", - agendaVisibility: "Visibilità", - agendaEventDate: "Data evento", - agendaEventLocation: "Posizione", - agendaEventPrice: "Prezzo", - agendaEventUrl: "URL", - agendaTaskStart: "Ora di inizio", agendaLocationLabel: "Posizione", agendaYes: "SÌ", agendaNo: "NO", @@ -2228,61 +2251,16 @@ module.exports = { agendareportCategory: "Categoria", agendareportSeverity: "Gravità", agendareportStatus: "Stato", - agendareportDescription: "Descrizione", - agendaTaskEnd: "Ora di fine", - agendaTaskPriority: "Priorità", - agendaTransferFrom: "Da", - agendaTransferTo: "A", agendaTransferConcept: "Concetto", agendaTransferAmount: "Importo", agendaTransferDeadline: "Scadenza", opinionsTitle: "Opinioni", shareYourOpinions: "Scopri e vota opinioni nella tua rete.", - author: "By", + author: "Di", voteNow: "Vota ora", alreadyVoted: "Hai già espresso un'opinione.", noOpinionsFound: "Nessuna opinione trovata.", - RECENTButton: "RECENT", TOPButton: "TOP", - interestingButton: "INTERESSANTE", - necessaryButton: "NECESSARIO", - funnyButton: "DIVERTENTE", - disgustingButton: "DISGUSTOSO", - sensibleButton: "SENSIBILE", - propagandaButton: "PROPAGANDA", - adultOnlyButton: "SOLO ADULTI", - boringButton: "NOIOSO", - confusingButton: "CONFUSO", - usefulButton: "UTILE", - informativeButton: "INFORMATIVO", - wellResearchedButton: "BEN DOCUMENTATO", - accurateButton: "ACCURATO", - needsSourcesButton: "SERVONO FONTI", - wrongButton: "SBAGLIATO", - lowQualityButton: "BASSA QUALITÀ", - creativeButton: "CREATIVO", - insightfulButton: "PERSPICACE", - actionableButton: "ATTUABILE", - inspiringButton: "ISPIRANTE", - loveButton: "AMORE", - clearButton: "CHIARO", - upliftingButton: "EDIFICANTE", - unnecessaryButton: "INUTILE", - rejectedButton: "RIFIUTATO", - misleadingButton: "FUORVIANTE", - offTopicButton: "FUORI TEMA", - duplicateButton: "DUPLICATO", - clickbaitButton: "CLICKBAIT", - spamButton: "SPAM", - trollButton: "TROLL", - nsfwButton: "NSFW", - violentButton: "VIOLENTO", - toxicButton: "TOSSICO", - harassmentButton: "MOLESTIA", - hateButton: "ODIO", - scamButton: "TRUFFA", - triggeringButton: "DISTURBANTE", - opinionsCreatedAt: "Creato il", opinionsTotalCount: "Opinioni totali", voteInteresting: "Interessante", voteNecessary: "Necessario", @@ -2319,29 +2297,41 @@ module.exports = { voteCreative: "Creativo", voteSpam: "Spam", voteAdultOnly: "Solo adulti", - publishBlog: "Pubblica post", privateMessage: "PM", pmSendTitle: "Messaggi privati", pmSend: "Invia!", - pmDescription: "Usa questo modulo per inviare un messaggio crittografato ad altri abitanti.", + pmCancel: "Annulla", + pmReset: "Reimposta", + pmSharedKeyHint: "Condividi questa chiave con il destinatario tramite un altro canale. È necessaria per decifrare.", + pmDescription: "Usa questo modulo per inviare un messaggio/file cifrato ad altri abitanti.", + pmComposeTitle: "Invia un messaggio", pmRecipients: "Destinatari", pmRecipientsHint: "Inserisci Oasis ID separati da virgole", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "Fino a 7 abitanti per messaggio.", + pmTextPlaceholder: "Scrivi il tuo messaggio.", pmSubject: "Oggetto", pmSubjectHint: "Inserisci l'oggetto del messaggio", pmText: "Messaggio", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Decifra", + fileShareTitle: "Condividi un file", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "File", + fileShareDownload: "Scarica", + fileShareMutualError: "Puoi condividere file solo con abitanti con supporto reciproco.", + fileShareNoFile: "Nessun file selezionato.", + fileShareTooLarge: "Il file supera la dimensione consentita.", + fileShareFailed: "Impossibile preparare il file.", + fileShareSendError: "Impossibile inviare il file.", + fileShareUnavailable: "Questo file non è disponibile al momento. Riprova più tardi.", pmCrypterBadKey: "La chiave condivisa è errata!", pmCrypterTooLong: "Il messaggio è troppo lungo per essere cifrato con il Crypter. Accorcialo e riprova.", pmCrypterCipherLabel: "Messaggio ricifrato", pmCrypterCharsLabel: "caratteri", pmInvalidRecipients: "Oasis ID non valido (dovrebbe essere tipo @…=.ed25519).", pmEncryptionLabel: "Cifratura", - pmFile: "Allegato", - private: "Privato", - privateDescription: "I tuoi messaggi crittografati.", + private: "Posta in arrivo", + privateDescription: "Gestisci i tuoi messaggi privati.", privateInbox: "POSTA IN ARRIVO", privateSent: "INVIATI", privateDelete: "Elimina", @@ -2349,27 +2339,34 @@ module.exports = { noPrivateMessages: "Nessun messaggio privato.", pmFromLabel: "Da:", pmToLabel: "A:", - pmInvalidMessage: "Messaggio non valido", pmNoSubject: "(senza oggetto)", pmSubjectLabel: "Oggetto:", - pmBodyLabel: "Messaggio", - pmBotJobs: "42-LavoriBOT", - pmBotProjects: "42-ProgettiBOT", - pmBotMarket: "42-MercatoBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "La casa al governo del L.A.R.P è cambiata.", + politicalBotParliamentTitle: "È iniziato un nuovo ciclo di governo nel Parlamento.", + politicalBotTribeTitle: "È iniziato un nuovo ciclo di governo in una delle tue Tribù.", + politicalBotLarpIntro: "Ora governa {house} durante questo ciclo: {cycle}", + politicalBotParliamentIntro: "Il governo attuale è {gov}", + politicalBotParliamentIntroLeader: "Il governo attuale è {gov}, esercitato da {leader}", + politicalBotTribeIntro: "Il governo attuale nella Tribù {tribe} è {gov}", + politicalBotTribeIntroLeader: "Il governo attuale nella Tribù {tribe} è {gov}, esercitato da {leader}", + politicalBotLeaderLabel: "Leader", inboxJobSubscribedTitle: "Nuova iscrizione alla tua offerta di lavoro", pmInhabitantWithId: "Abitante con Oasis ID:", pmHasSubscribedToYourJobOffer: "si è iscritto alla tua offerta di lavoro", - inboxProjectCreatedTitle: "Nuovo progetto creato", - pmHasCreatedAProject: "ha creato un progetto", inboxMarketItemSoldTitle: "Articolo venduto", + inboxShopSoldTitle: "Prodotto venduto", pmYourItem: "Il tuo articolo", pmHasBeenSoldTo: "è stato venduto a", pmFor: "per", inboxProjectPledgedTitle: "Nuovo contributo al tuo progetto", pmHasPledged: "ha contribuito", pmToYourProject: "al tuo progetto", - blockchain: 'BlockExplorer', - blockchainTitle: 'BlockExplorer', + blockchain: "Esploratore di blocchi", blockchainDescription: 'Esplora e visualizza i blocchi nella blockchain.', blockchainNoBlocks: 'Nessun blocco trovato nella blockchain.', blockchainStatsTitle: 'Statistiche', @@ -2381,42 +2378,36 @@ module.exports = { blockchainBlockCarbon: 'Impronta di carbonio', blockchainBlockType: 'Tipo', blockchainBlockTimestamp: 'Data e ora', - blockchainBlockContent: 'Blocco', - blockchainBlockURL: 'URL:', - blockchainContent: 'Blocco', - blockchainContentPreview: 'Anteprima del contenuto del blocco', blockchainLatestDatagram: 'Ultimo Datagramma', - blockchainDatagram: 'Datagramma', - blockchainDetails: 'Vedi dettagli blocco', - blockchainViewBlockexplorer: "Vedi nel blockexplorer", - blockchainBlockInfo: 'Informazioni blocco', - blockchainBlockDetails: 'Dettagli del blocco selezionato', + blockchainDatagram: "Datagramma", + blockchainDetails: "Vedi i Dettagli del Blocco", + blockchainViewBlockexplorer: "Vedi nel Blockexplorer", blockchainBack: 'Torna al BlockExplorer', blockchainContentDeleted: "Questo contenuto è stato rimosso (tombstone)", - visitContent: "Visita contenuto", - banking: 'Banking', - bankingTitle: 'Banking', - bankingDescription: 'Esplora il valore attuale di ECOin e la corrispondente allocazione UBI, distribuita per epoca in base a partecipazione e fiducia.', + visitContent: "Visita il Contenuto", + favoriteAdd: "Aggiungi ai Preferiti", + pmContentTooltip: "Invia un Messaggio Privato", + phoneCallTooltip: "Fai una Chiamata", + favoriteRemove: "Togli dai Preferiti", + reportContent: "Segnala il Contenuto", + banking: "Banca", + bankingTitle: "Banca", + bankingDescription: "L'economia collettiva di ECOin.", bankOverview: 'Panoramica', bankEpochs: 'Epoche', bankRules: 'Regole', pending: 'In attesa', closed: 'Chiuso', - bankBack: 'Torna alla Banca', - bankViewTx: 'Vedi Tx', bankClaimNow: 'Riscuoti ora', - bankClaimUBI: 'Richiedi RBU!', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'Nessuna assegnazione RBU in sospeso per questa epoca.', + bankClaimUBI: 'Richiedi RBU', + bankClaimAndPay: "Richiedi e paga", + bankClaimedPending: "Richiesta in sospeso...", + bankStatusClaimed: "Richiesto", + bankStatusExpired: "Scaduto", + bankPubOnly: "Operazione solo per PUB", bankEpoch: 'Epoca', bankPool: 'Pool (questa epoca)', bankWeightsSum: 'Somma dei pesi', - bankAllocations: 'Allocazioni', bankNoAllocations: 'Nessuna allocazione trovata.', bankNoEpochs: 'Nessuna epoca trovata.', bankEpochAllocations: 'Allocazioni epoca', @@ -2430,16 +2421,21 @@ module.exports = { bankEpochId: 'ID epoca', bankRuleHash: 'Hash snapshot regole', bankViewEpoch: 'Vedi epoca', + bankYourIndustryBalance: "La tua quota industriale", + bankYourUbiMonth: "Il tuo RBU (questo mese)", + bankYourFundsMonth: "Fondi totali", + bankIndustryBalance: "Produzione industriale", bankUserBalance: 'Il tuo saldo', - ecoWalletNotConfigured: 'Portafoglio ECOin non configurato', - editWallet: 'Modifica portafoglio', - addWallet: 'Aggiungi portafoglio', bankAddresses: 'Indirizzi', bankNoAddresses: 'Nessun indirizzo trovato.', bankUser: 'Oasis ID', bankAddress: 'Indirizzo', bankAddAddressTitle: 'Aggiungi indirizzo ECOIN', bankAddAddressUser: 'Oasis ID', + bankAddAddressLabel: "Etichetta (facoltativa)", + bankAddAddressLabelPlaceholder: "Portafoglio di Bob, negozio di Alice…", + bankAddAddressUserOptional: "Oasis ID (facoltativo)", + bankAddressUnnamed: "Indirizzo senza nome", bankAddAddressAddress: 'Indirizzo ECOIN', bankAddAddressSave: 'Salva', bankAddressAdded: 'Indirizzo aggiunto', @@ -2457,19 +2453,11 @@ module.exports = { bankAddressDeleteConfirm: 'Eliminare questo indirizzo?', search: 'Cerca!', bankLocal: 'Locale', - bankFromOasis: 'Oasis', - bankMyAddress: 'Il tuo indirizzo', - bankRemoveMyAddress: 'Rimuovi il mio indirizzo', - bankNotRemovableOasis: 'Gli indirizzi non possono essere rimossi localmente', - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "Rete", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "NESSUN FONDO!", bankUbiAvailableOk: "DISPONIBILE!", - bankUbiAvailability: "Disponibilità UBI", + bankUbiAvailability: "RBU", bankAlreadyClaimedThisMonth: "Già richiesto questo mese", bankUbiThisMonth: "RBU (questo mese)", bankUbiLastClaimed: "RBU (ultima richiesta)", @@ -2483,13 +2471,6 @@ module.exports = { shopsTitle: "Negozi", shopDescription: "Scopri e gestisci negozi nella rete.", shopTitle: "Negozio", - shopFilterAll: "TUTTI", - shopFilterMine: "MIEI", - shopFilterRecent: "RECENTI", - shopFilterTop: "TOP", - shopFilterProducts: "PRODOTTI", - shopFilterPrices: "PREZZI", - shopFilterFavorites: "PREFERITI", shopUpload: "Crea Negozio", shopAllSectionTitle: "Tutti i Negozi", shopMineSectionTitle: "I Miei Negozi", @@ -2506,19 +2487,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Pubblica su Clearnet", - shopClearnetUnpublish: "Rimuovi dal Clearnet", - shopClearnetUrlLabel: "URL Clearnet", - shopClearnetWarning: "Pubblicare su Clearnet rende questo negozio indicizzabile da motori di ricerca e crawler esterni.", + transportChipLabel: "Cifrato in transito", shopAddFavorite: "Aggiungi Preferito", shopRemoveFavorite: "Rimuovi Preferito", shopOpen: "APERTO", shopClosed: "CHIUSO", - shopOpenShop: "Apri Negozio", - shopCloseShop: "Chiudi Negozio", shopMakePrivate: "RENDI PRIVATO (E2E)", shopMakePublic: "RENDI PUBBLICO", shopProducts: "Prodotti", + shopFilterAll: "TUTTI", + shopFilterRecent: "RECENTI", + shopFilterMine: "MIEI", + shopFilterTop: "TOP", + shopFilterProducts: "PRODOTTI", + shopFilterPrices: "PREZZI", + shopFilterFavorites: "PREFERITI", shopProductAdd: "Aggiungi Prodotto", shopProductTitle: "Prodotto", shopProductUpdate: "Aggiorna Prodotto", @@ -2543,40 +2526,37 @@ module.exports = { shopOrderPrice: "Prezzo", shopOrderBuyer: "Acquirente", shopBackToShop: "Torna al Negozio", - shopShareUrl: "URL di condivisione", - shopVisitShop: "VISITA NEGOZIO", marketVisitItem: "VEDI OGGETTO", shopStatus: "STATO", shopCreatedAt: "CREATO", - shopSearchPlaceholder: "Cerca negozi...", + shopSearchPlaceholder: "Cerca in Negozi...", shopUrl: "URL", shopLocation: "Posizione", shopTags: "Tag", shopVisibility: "Visibilità", - shopImage: "Immagine", shopShortDescription: "Descrizione Breve", shopShortDescriptionPlaceholder: "Descrizione breve per le schede (max 160 caratteri)", shopTitlePlaceholder: "Nome del tuo negozio", shopDescriptionPlaceholder: "Descrizione dettagliata del tuo negozio", - shopUrlPlaceholder: "https://url-del-tuo-negozio.com", shopLocationPlaceholder: "Città, Paese", - shopTagsPlaceholder: "tag1, tag2, tag3", + shopTagsPlaceholder: "Inserisci tag separati da virgole", mapTitlePlaceholder: "Titolo della mappa", shopProductFeatured: "Prodotto in Evidenza", - shopSendToMarket: "Send to Market", + shopSendToMarket: "Invia al mercato", typeShop: "NEGOZI", typeShopProduct: "PRODOTTO NEGOZIO", bankExchange: 'Cambio', bankExchangeCurrentValue: 'Valore ECOin (1h)', - bankTotalSupply: 'Offerta totale ECOin', - bankEcoinHours: "Equivalenza ECOin in tempo", + bankTotalSupply: "Offerta ECOin totale", + bankPubsSupply: "Offerta ECOin del pool dei PUB", + bankHoldingSupply: "Offerta ECOin fuori dal RBU", + bankEcoinHours: "Equivalenza ECOin in tempo (1 ECO)", bankHoursOfWork: 'ore', bankUnitMs: "ms", bankUnitSeconds: "secondi", bankUnitMinutes: "minuti", bankUnitDays: "giorni", bankExchangeNoData: 'Nessun dato disponibile', - bankExchangeIndex: 'Valore ECOin (1h)', bankInflation: 'Inflazione ECOin (anual)', bankInflationMonthly: 'Inflazione ECOin (mensual)', bankExchangeChartTitle: 'Progressione del valore ECOin nel tempo', @@ -2584,44 +2564,25 @@ module.exports = { bankExchangeChartSupply: 'Offerta', bankExchangeChartInflation: 'Inflazione %', bankExchangeChartEmpty: 'Non ci sono ancora abbastanza campioni — torna più tardi', - bankCurrentSupply: 'Offerta attuale ECOin', + bankCurrentSupply: "Offerta ECOin attuale", bankingSyncStatus: 'Stato ECOin', bankingSyncStatusSynced: 'Sincronizzato', - bankingSyncStatusOutdated: 'Non aggiornato', + bankingSyncStatusOutdated: "Non sincronizzato", statsTitle: 'Statistiche', statistics: "Statistiche", - statsInhabitant: "Statistiche abitante", statsDescription: "Scopri le statistiche della tua rete.", - ALLButton: "ALL", - MINEButton: "MINE", - TOMBSTONEButton: "TOMBSTONES", - statsYou: "Tu", - statsUserId: "Oasis ID", + ALLButton: "TUTTO", + RECENTButton: "RECENTI", + MINEButton: "MIEI", + TOMBSTONEButton: "ELIMINATI", statsCreatedAt: "Creato il", statsYourContent: "Contenuto", statsYourOpinions: "Opinioni", - statsYourTombstone: "Tombstone", - statsNetwork: "Rete", - statsTotalInhabitants: "Abitanti", - statsDiscoveredTribes: "Tribù (Pubbliche)", - statsPrivateDiscoveredTribes: "Tribù (Private)", statsNetworkContent: "Contenuto", - statsYourMarket: "Mercato", - statsYourJob: "Lavori", - statsYourProject: "Progetti", - statsYourTransfer: "Trasferimenti", - statsYourForum: "Forum", statsNetworkOpinions: "Opinioni", - statsDiscoveredMarket: "Mercato", - statsDiscoveredJob: "Lavori", - statsDiscoveredProject: "Progetti", - statsBankingTitle: "Banca", - statsEcoWalletLabel: "Portafoglio ECOIN", + statsEcoWalletLabel: "Portafoglio ECOin", + walletAddressCopyHint: "Clicca per selezionare, poi copia", statsEcoWalletNotConfigured: "Non configurato!", - statsTotalEcoAddresses: "Indirizzi totali", - statsDiscoveredTransfer: "Trasferimenti", - statsDiscoveredForum: "Forum", - statsNetworkTombstone: "Tombstone", statsBookmark: "Segnalibri", statsEvent: "Eventi", statsTask: "Compiti", @@ -2642,17 +2603,15 @@ module.exports = { statsTransfer: "Trasferimenti", statsAiExchange: "AI", statsPUBs: 'PUBs', + statsSyncedPeers: "Peer", + statsTotalLabel: "Totale", statsPost: "Post", - statsOasisID: "Oasis ID", statsSize: "Totale (dimensione)", statsBlockchainSize: "Blockchain (dimensione)", statsBlobsSize: "Blob (dimensione)", statsActivity7d: "Attività (ultimi 7 giorni)", statsActivity7dTotal: "Totale 7 giorni", statsActivity30dTotal: "Totale 30 giorni", - statsKarmaScore: "Punteggio KARMA", - statsPublic: "Pubblico", - statsPrivate: "Privato", day: "Giorno", messages: "Messaggi", statsProject: "Progetti", @@ -2664,30 +2623,14 @@ module.exports = { statsProjectsCancelled: "Annullato", statsProjectsGoalTotal: "Obiettivo totale", statsProjectsPledgedTotal: "Contribuito totale", - statsProjectsSuccessRate: "Tasso di successo", - statsProjectsAvgProgress: "Progresso medio", - statsProjectsMedianProgress: "Progresso mediano", - statsProjectsActiveFundingAvg: "Finanziamento attivo medio", - statsJobsTitle: "Lavori", - statsJobsTotal: "Lavori totali", - statsJobsOpen: "Aperti", - statsJobsClosed: "Chiusi", - statsJobsOpenVacants: "Posti vacanti aperti", - statsJobsSubscribersTotal: "Iscritti totali", - statsJobsAvgSalary: "Stipendio medio", - statsJobsMedianSalary: "Stipendio mediano", statsMarketTitle: "Mercato", statsMarketTotal: "Articoli totali", statsMarketForSale: "In vendita", statsMarketReserved: "Riservato", statsMarketClosed: "Chiuso", statsMarketSold: "Venduto", - statsMarketRevenue: "Entrate", - statsMarketAvgSoldPrice: "Prezzo medio venduto", statsUsersTitle: "Abitanti", user: "Abitante", - statsTombstoneTitle: "Tombstone", - statsNetworkTombstones: "Tombstone della rete", statsTombstoneRatio: "Rapporto tombstone (%)", statsParliamentCandidature: "Candidature parlamento", statsParliamentTerm: "Mandati parlamento", @@ -2713,31 +2656,28 @@ module.exports = { aiPrompt: "Fornisci una risposta informativa e precisa.", aiConfiguration: "Imposta prompt", aiPromptUsed: "Prompt", - aiClearHistory: "Cancella cronologia chat", - aiSharePrompt: "Aggiungere questa risposta all'addestramento collettivo?", - aiShareYes: "Sì", - aiShareNo: "No", - aiSharedLabel: "Aggiunto all'addestramento", - aiRejectedLabel: "Non aggiunto all'addestramento", - aiServerError: "L'AI non ha potuto rispondere. Riprova.", + aiClearHistory: "Cancella cronologia", + aiServerError: "Non riesco a rispondere bene. Riprova.", + aiStatusLoading: "42 sta caricando il modello. Riprova tra un momento.", + aiThinking: "Sta pensando", + aiWaiting: "In attesa", + aiStatusMissingModel: "Il modello di IA non è installato: manca src/AI/oasis-42-1-chat.Q4_K_M.gguf.", + aiExportFineTuning: "Esporta Fine-Tuning", + aiFactsTitle: "Dati dalla tua rete", aiInputPlaceholder: "Cos'è Oasis?", typeAiExchange: "AI", aiApproveTrain: "Aggiungi all'addestramento collettivo", aiRejectTrain: "Non addestrare", - aiTrainPending: "In attesa di approvazione", aiTrainApproved: "Approvato per l'addestramento", aiTrainRejected: "Rifiutato per l'addestramento", aiSnippetsUsed: "Snippet utilizzati", aiSnippetsLearned: "Snippet appresi", statsAITraining: "Addestramento AI", - aiApproveCustomTrain: "Addestra usando questa risposta personalizzata", aiCustomAnswerPlaceholder: "Scrivi la tua risposta personalizzata…", - statsAIExchanges: "Scambi modello", marketMineSectionTitle: "I tuoi articoli", marketCreateSectionTitle: "Crea articolo", marketUpdateSectionTitle: "Aggiorna", marketAllSectionTitle: "Mercato", - marketRecentSectionTitle: "Mercato recente", marketTitle: "Mercato", marketDescription: "Un mercato per scambiare beni o servizi nella tua rete.", marketFilterAll: "TUTTI", @@ -2752,7 +2692,7 @@ module.exports = { marketFilterDiscarded: "SCARTATO", marketFilterRecent: "RECENTI", marketFilterMyBids: "OFFERTE", - marketCreateButton: "Crea articolo", + marketCreateButton: "Vendi qualcosa", marketItemType: "Tipo", marketItemTitle: "Titolo", marketItemAvailable: "Scadenza", @@ -2767,14 +2707,11 @@ module.exports = { marketItemDeadline: "Scadenza", marketItemIncludesShipping: "Spedizione inclusa?", marketItemHighestBid: "Offerta più alta", - marketItemHighestBidder: "Miglior offerente", marketItemStock: "Disponibilità", marketOutOfStock: "Esaurito", - marketItemBidTime: "Ora dell'offerta", marketActionsUpdate: "Aggiorna", marketUpdateButton: "Aggiorna", marketActionsDelete: "Elimina", - marketActionsSold: "Segna come venduto", marketActionsChangeStatus: "CAMBIA STATO", marketActionsBuy: "COMPRA!", marketAuctionBids: "Offerte attuali", @@ -2782,20 +2719,82 @@ module.exports = { marketItemSeller: "Venditore", marketNoItems: "Nessun articolo disponibile ancora.", marketYourBid: "La tua offerta", - marketCreateFormImageLabel: "Carica media (max: 50MB)", - marketSearchLabel: "Cerca", - marketSearchPlaceholder: "Cerca titolo o tag", + marketCreateFormImageLabel: "Carica media (max: 75MB)", + marketSearchPlaceholder: "Cerca in Mercato...", marketMinPriceLabel: "Prezzo minimo", marketMaxPriceLabel: "Prezzo massimo", - marketSortLabel: "Ordina per", marketSortRecent: "Più recenti", marketSortPrice: "Prezzo", marketSortDeadline: "Scadenza", marketSearchButton: "Cerca", marketAuctionEndsIn: "Termina", marketAuctionEnded: "Terminata", - marketMyBidBadge: "Hai offerto", marketNoItemsMatch: "Nessun articolo corrisponde alla ricerca.", + housingTitle: "Alloggi", + housingDescriptionText: "Scopri e gestisci luoghi nella tua rete.", + modulesHousingLabel: "Alloggi", + modulesHousingDescription: "Modulo per scoprire e gestire luoghi.", + typeHousing: "ALLOGGI", + housingFilterAll: "TUTTI", + housingFilterMine: "MIEI", + housingFilterRequested: "RICHIESTI", + housingFilterSale: "VENDITA", + housingFilterRent: "AFFITTO", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "DISPONIBILI", + housingFilterClosed: "NON DISPONIBILI", + housingFilterRecent: "RECENTI", + housingFilterTop: "TOP", + housingType: "Offerta", + housingTypeSALE: "In vendita", + housingTypeRENT: "In affitto", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Immobile", + housingPropertyAPARTMENT: "Appartamento", + housingPropertyHOUSE: "Casa", + housingPropertyROOM: "Stanza", + housingPropertyLAND: "Terreno", + housingPropertyOTHER: "Altro", + housingTitleLabel: "Titolo", + housingTitlePlaceholder: "Nome del luogo", + housingDescription: "Descrizione", + housingDescriptionPlaceholder: "Descrivi il luogo, il quartiere, cosa è incluso: bagni, giardino, letti…", + housingRules: "Regole", + housingRulesPlaceholder: "Animali, fumo, orari di silenzio, soggiorno minimo…", + housingPlace: "Luogo", + housingPlacePlaceholder: "Paese, città, indirizzo", + housingPrice: "Prezzo", + housingPriceLabel: "Prezzo (ECO)", + housingSizeLabel: "Superficie (m²)", + housingFree: "GRATIS", + housingPerMonth: "/mese", + housingRooms: "Stanze", + housingSize: "Superficie", + housingCapacity: "Ospiti", + housingAvailableFrom: "Disponibile dal", + housingAvailableTo: "Disponibile fino al", + housingTags: "Etichette", + spreadEditWarning: "Questo contenuto perderà gli spread dopo la modifica", + housingRequests: "Richieste", + housingRequestButton: "Richiedi", + housingCancelButton: "Annulla richiesta", + housingRequestedBadge: "RICHIESTO", + housingStatus: "Stato", + housingStatusOPEN: "DISPONIBILE", + housingStatusCLOSED: "NON DISPONIBILE", + housingSetOpen: "Rendi disponibile", + housingSetClosed: "Rendi non disponibile", + housingCreateButton: "Crea luogo", + housingUpdateButton: "Aggiorna", + housingSearchPlaceholder: "Cerca in Alloggi...", + housingSearchButton: "Cerca", + housingMinPrice: "Prezzo min", + housingMaxPrice: "Prezzo max", + housingSortRecent: "Più recenti", + housingSortPrice: "Prezzo più basso", + housingSortRequests: "Più richiesti", + housingSortRating: "Più votati", + housingNoItems: "Nessun luogo trovato.", jobsTitle: "Lavori", jobsCandidatesTitle: "Candidati suggeriti", jobsCandidatesDescription: "Abitanti le cui competenze corrispondono a questo lavoro. Inviate un messaggio privato per invitarli.", @@ -2817,7 +2816,7 @@ module.exports = { jobsClosedTitle: "Lavori chiusi", jobsCVTitle: "CV", jobsFilterPresencial: "IN SEDE", - jobsFilterFreelancer: "FREELANCER", + jobsFilterFreelancer: "FREELANCE", jobsFilterEmployee: "DIPENDENTE", jobsPresencialTitle: "Lavori in sede", jobsFreelancerTitle: "Lavori freelance", @@ -2843,8 +2842,7 @@ module.exports = { jobLocationPresencial: "In sede", jobLocationRemote: "Da remoto", jobVacantsPlaceholder: "Numero di posizioni", - jobSalaryPlaceholder: "Stipendio in ECO per 1 ora dedicata", - jobImage: "Carica media (max: 50MB)", + jobImage: "Carica media (max: 75MB)", jobTasks: "Compiti", jobType: "Tipo lavoro", jobTime: "Orario lavoro", @@ -2853,7 +2851,6 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Lavori con stipendio più alto", createJobButton: "Pubblica lavoro", - viewDetailsButton: "Vedi dettagli", noJobsFound: "Nessuna offerta di lavoro trovata.", jobAuthor: "Di", jobTypeFreelance: "Freelance", @@ -2865,42 +2862,208 @@ module.exports = { jobsHoursOffered: "Ore offerte", jobsHoursRequested: "Ore richieste in cambio", jobsExchangeSkill: "Competenza richiesta in cambio", - jobsHoursOfferedPlaceholder: "es. 4", - jobsHoursRequestedPlaceholder: "es. 4", - jobsExchangeSkillPlaceholder: "es. falegnameria, design", - jobExchangeHint: "Per i lavori in scambio di ore, compila i campi seguenti invece dello stipendio.", - jobTimePartial: "Part-time", - jobTimeComplete: "Full-time", - jobsDeleteButton: "ELIMINA", + jobTimePartial: "Tempo parziale", + jobTimeComplete: "Tempo pieno", jobsUpdateButton: "AGGIORNA", jobsFilterApplied: "CANDIDATURE", jobsAppliedTitle: "Le mie candidature", jobsAppliedBadge: "Candidato", - jobsFilterFavs: "Preferiti", - jobsFavsTitle: "Preferiti", - jobsFilterNeeds: "Serve aiuto", - jobsNeedsTitle: "Serve aiuto", - jobsSearchLabel: "Cerca", - jobsSearchPlaceholder: "Cerca titolo, tag, descrizione...", + jobsSearchPlaceholder: "Cerca in Lavori...", jobsMinSalaryLabel: "Stipendio minimo", jobsMaxSalaryLabel: "Stipendio massimo", - jobsSortLabel: "Ordina per", jobsSortRecent: "Più recenti", jobsSortSalary: "Stipendio più alto", jobsSortSubscribers: "Più candidati", jobsSearchButton: "Cerca", - jobsFavoriteButton: "Preferito", - jobsUnfavoriteButton: "Rimuovi preferito", - jobsMessageAuthorButton: "PM", jobsApplicants: "Candidati", jobsUpdatedAt: "Aggiornato", jobSetOpen: "Segna come aperto", - jobNewBadge: "NUOVO", jobsTagsLabel: "Tag", - jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "Nessun lavoro corrisponde alla ricerca.", + industrySearchPlaceholder: "Cerca in Industria...", + industryAllSectors: "Tutti i settori", + industryVisitButton: "Visita industria", + industryAdvanceTo: "Imposta a", + industryAmount: "Importo", + industryApproveBuild: "Approva", + industryBlueprint: "Progetto", + industryBlueprints: "Blueprint", + industryBuild: "Produzione", + industryBuildNotes: "Note", + industryBuildStart: "Data di inizio", + industryVoteUpdateButton: "Vota aggiornamento", + industryVoteDeleteButton: "Vota eliminazione", + industryRevokeVote: "Revoca", + industryTimeLeft: "Tempo rimanente", + industryBuildEnd: "Data di fine", + industryBuilds: "Build", + industryBuildTitle: "Titolo", + industryContribute: "Contribuisci", + industryContributeButton: "Contribuisci", + industryContributions: "Contributi", + industryCreateBlueprintButton: "Crea blueprint", + industryDistribute: "Distribuisci", + industryDistributeButton: "Distribuisci per quote", + industryDistribution: "Distribuzione", + industryEcoAmount: "Importo (ECO)", + industryEditBlueprint: "Modifica blueprint", + industryFacility: "Fabbrica", + industryKindLabel: "Tipo", + industryKind_labor: "Lavoro", + industryKind_material: "Materiale", + industryKind_eco: "Fondi", + industryLaborHours: "Lavoro (ore)", + industryHours: "Ore", + industryMaterialItem: "Materiale", + industryMaterials: "Materiali", + industryEstMaterials: "Totale materiali", + industryEstLabor: "Totale lavoro", + industryEstTotal: "Prezzo stimato", + industryBuildingPrice: "Prezzo di costruzione", + industryFinalPrice: "Prezzo finale", + industryBlueprintDescPlaceholder: "Specifiche, dimensioni, peso, documentazione…", + industryMaterialValue: "Valore (ECO)", + industryMember: "Membro", + industryNewBlueprint: "Nuovo blueprint", + industryNewBuild: "Proponi un build", + industryEditBuild: "Modifica produzione", + industryNeedBlueprint: "Crea prima un progetto: ogni produzione ne fabbrica uno.", + industryNoBlueprints: "Ancora nessun blueprint.", + industryNoBuilds: "Ancora nessun build.", + industryNote: "Nota", + industryOutput: "Output", + industryOutputKind: "Tipo di prodotto", + industryOutputValue: "Valore di output (ECO, es. dalla vendita)", + industryPostJob: "Crea lavoro", + industryPot: "Monte", + industryProposeBuildButton: "Proponi build", + industryProposer: "Proponente", + industryAuthor: "Autore", + industrySellOnMarket: "Invia al mercato", + industrySendToProjects: "Crea progetto", + industrySkills: "Competenze", + industryTreasury: "Tesoreria", + industryKind_physical: "Fisico", + industryKind_digital: "Digitale", + industryBuildStatus_PROPOSED: "PROPOSTO", + industryBuildStatus_REJECTED: "RESPINTO", + industryBuildStatus_APPROVED: "APPROVATO", + industryBuildStatus_STOCKING: "APPROVVIGIONAMENTO", + industryBuildStatus_IN_PRODUCTION: "IN PRODUZIONE", + industryBuildStatus_COMPLETED: "COMPLETATO", + industryBuildStatus_FAILED: "FALLITO", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Un lavoro corrisponde al tuo curriculum", + jobsBotMatchIntro: "Un lavoro corrisponde al tuo curriculum.", + jobsBotMatchJob: "Lavoro", + jobsBotMatchHint: "Ricevi questo perché il tuo curriculum è gestito dall'IA. Puoi disattivarlo nel tuo CV.", + cvAiManaged: "Gestito dall'IA", + switchOn: "ATTIVO", + switchOff: "DISATTIVO", + cvAiManagedHint: "Se attivo, JobsBot ti avvisa con un messaggio privato quando un lavoro corrisponde al tuo curriculum.", + cvMatchThreshold: "Avvisami da questo livello di corrispondenza", + housingBotRequestedTitle: "Qualcuno ha richiesto uno dei tuoi luoghi.", + housingBotCancelledTitle: "Una richiesta su uno dei tuoi luoghi è stata annullata.", + housingBotYouRequestedTitle: "Hai richiesto un luogo.", + housingBotYouCancelledTitle: "Hai annullato una richiesta.", + housingBotUnavailableTitle: "Un luogo che avevi richiesto non è più disponibile.", + housingBotUnavailableSentTitle: "Hai avvisato chi aveva richiesto questo luogo.", + industryBotAdmittedTitle: "Sei stato ammesso in una fabbrica.", + industryBotApplicationTitle: "Nuova richiesta di adesione nella tua fabbrica.", + industryBotInvitedTitle: "Sei stato invitato in una fabbrica.", + industryBotDissolvedTitle: "Una fabbrica a cui appartieni è stata sciolta.", + industryBotBuildApprovedTitle: "Un build è stato approvato.", + industryBotDistributedTitle: "L'output di un build è stato distribuito.", + industryFilterRules: "REGOLE", + industryRulesTitle: "Regole", + industryRulesIntro: "Industry permette agli abitanti di possedere collettivamente i mezzi di produzione: una Fabbrica è un'officina della rete che nessuno possiede privatamente.", + industryRulesSteward: "Il fondatore è l'amministratore — un custode, non un proprietario: non può vendere, privatizzare o sciogliere la Fabbrica da solo.", + industryRulesMembership: "La politica di adesione stabilisce come si entra: Aperta (subito), Per voto (ammessi dal voto dei membri) o Solo su invito (un membro deve invitare prima).", + industryRulesQuorum: "Il quorum è il numero minimo di membri che devono votare perché una decisione conti, così una piccola Fabbrica non è decisa da un solo abitante.", + industryRulesMajority: "La maggioranza è la frazione di voti necessaria (0,5 = metà, 1 = unanimità); una decisione passa quando i SÌ raggiungono almeno max(quorum, membri × maggioranza).", + industryRulesDecisions: "I membri votano per ammettere nuovi arrivati, approvare i build e sciogliere la Fabbrica; l'amministratore non può scavalcare queste decisioni collettive.", + industryRulesBlueprints: "I Blueprint sono ricette libere (COPYLEFT) — gli input (materiali, ore di lavoro, competenze) e l'output (un bene fisico o digitale) — e chiunque può forkarle.", + industryRulesBuilds: "Un Build è un ordine di produzione: PROPOSTO → APPROVATO (per voto) → APPROVVIGIONAMENTO → IN PRODUZIONE → COMPLETATO, e accetta contributi solo una volta approvato.", + industryRulesContributions: "I membri contribuiscono con lavoro (ore), materiali (valore ECO dichiarato) o ECO a un build; ogni contributo guadagna karma.", + industryRulesLaborRate: "La tariffa di lavoro è il valore ECO di un'ora di lavoro in questa Fabbrica; converte le ore in karma per confrontare equamente lavoro e capitale: karma = ore × tariffa + valore materiali + ECO.", + industryRulesShares: "La tua quota di un build è il tuo karma ÷ karma totale, e stabilisce quanto del valore di output ricevi.", + industryRulesDistribution: "Quando una produzione è completata, lo steward distribuisce il monte (i fondi della produzione più il valore di vendita) tra i contributori, in proporzione alle loro quote.", + industryRulesTreasury: "I contributi in ECO sono custoditi dall'amministratore come tesoreria del build fino alla distribuzione; tutti i movimenti sono registrati sulla rete e le dispute possono andare alle Courts.", + industryJobs: "Lavori", + industryReportButton: "Segnala", + industryCreateTask: "Crea attività", + industryOpenDispute: "Apri controversia", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "Genera invito", + industryPauseVotes: "Voti di stato", + industryTitle: "Industria", + industryDescription: "Modulo per gestire collettivamente i mezzi di produzione.", + typeIndustryBuild: "PRODUZIONE", + typeIndustryBlueprint: "PROGETTO", + typeIndustryAllocation: "DISTRIBUZIONE", + typeIndustry: "INDUSTRIA", + modulesIndustryLabel: "Industria", + modulesIndustryDescription: "Modulo per gestire collettivamente i mezzi di produzione.", + industryFilterAll: "TUTTE", + industryFilterRecent: "RECENTI", + industryFilterMember: "MEMBRO", + industryFilterBlueprints: "PROGETTI", + industryFilterBuilds: "PRODUZIONI", + industryFilterMine: "MIE", + industryFilterActive: "IN FUNZIONE", + industryFilterPaused: "IN PAUSA", + industryFilterDissolved: "SCIOLTE", + industryAllTitle: "Industria", + industryRecentTitle: "Fabbriche recenti", + industryMemberTitle: "Fabbriche di cui faccio parte", + industryMineTitle: "Le mie fabbriche", + industryActiveTitle: "In funzione", + industryPausedTitle: "Fabbriche in pausa", + industryDissolvedTitle: "Fabbriche sciolte", + industryNoFacilitiesFound: "Nessuna fabbrica trovata.", + industryCreateFacility: "Crea fabbrica", + industryMembers: "Membri", + industryMemberBadge: "MEMBRO", + industryName: "Nome", + industryNamePlaceholder: "Nome della fabbrica", + industryDescriptionLabel: "Descrizione", + industryDescriptionPlaceholder: "Cosa produce questa fabbrica?", + industrySector: "Settore", + industryMembershipPolicy: "Politica di adesione", + industryQuorum: "Quorum", + industryMajority: "Maggioranza", + industryLaborRate: "Tariffa di lavoro", + industryCreateButton: "Crea fabbrica", + industryUpdateButton: "Aggiorna", + industrySteward: "Amministratore", + industryStatusActive: "IN FUNZIONE", + industryStatusPaused: "IN PAUSA", + industryStatusDissolved: "SCIOLTA", + industryStatusLabel: "Stato", + industryJoinButton: "Unisciti", + industryApplyButton: "Richiedi di unirti", + industryLeaveButton: "Esci", + industryInviteButton: "Invita", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Serve un invito per unirti.", + industryPauseButton: "Vota la pausa", + industryResumeButton: "Vota la ripresa", + industryDeleteButton: "Elimina", + industryPendingApplicants: "Richieste in sospeso", + industryVotesYes: "sì", + industryVotesNo: "no", + industryAlreadyVoted: "votato", + industryAdmit: "Ammetti", + industryReject: "Rifiuta", + industryDissolveTitle: "Sciogli fabbrica", + industryDissolveVote: "Vota lo scioglimento", + industryPolicy_open: "Aperta", + industryPolicy_vote: "Per voto", + industryPolicy_invite: "Solo su invito", projectsTitle: "Progetti", projectsDescription: "Crea, finanzia e segui progetti comunitari nella tua rete.", + projectSearchPlaceholder: "Cerca in Progetti...", projectCreateProject: "Crea progetto", projectCreateButton: "Crea progetto", projectUpdateButton: "AGGIORNA", @@ -2923,7 +3086,7 @@ module.exports = { projectRecentTitle: "Progetti recenti", projectTopTitle: "Più finanziati", projectTitlePlaceholder: "Nome del progetto", - projectImage: "Carica media (max: 50MB)", + projectImage: "Carica media (max: 75MB)", projectDescription: "Descrizione", projectDescriptionPlaceholder: "Racconta la storia e gli obiettivi…", projectGoal: "Obiettivo (ECO)", @@ -2944,14 +3107,8 @@ module.exports = { projectPledgeTitle: "Supporta questo progetto", projectPledgePlaceholder: "Importo in ECO", projectBounties: "Taglie", - projectBountiesInputLabel: "Taglie (una per riga: Titolo|Importo [ECO]|Descrizione)", - projectBountiesPlaceholder: "Correggi bug UI|100|Link al problema\nScrivi documentazione|250|Descrivi esempi d'uso", projectNoBounties: "Nessuna taglia trovata.", projectTitle: "Titolo", - projectAddBountyTitle: "Nuova taglia", - projectBountyTitle: "Titolo taglia", - projectBountyAmount: "Importo (ECO)", - projectBountyDescription: "Descrizione", projectMilestoneSelect: "Seleziona traguardo", projectBountyCreateButton: "Crea taglia", projectBountyStatus: "Stato taglia", @@ -2966,19 +3123,15 @@ module.exports = { projectMilestoneTitle: "Titolo traguardo", projectMilestoneTargetPercent: "Percentuale (%)", projectMilestoneDueDate: "Data", - projectMilestoneCreateButton: "Crea traguardo", projectMilestoneStatus: "Stato traguardo", projectMilestoneOpen: "aperto", projectMilestoneDone: "completato", projectMilestoneDue: "scadenza", - projectNoMilestones: "Nessun traguardo trovato.", projectMilestoneMarkDone: "Segna come completato", projectMilestoneTitlePlaceholder: "Inserisci titolo traguardo", projectMilestoneDescriptionPlaceholder: "Inserisci descrizione per questo traguardo", projectMilestoneDescription: "Descrizione traguardo", - projectBudgetGoal: "Budget (obiettivo)", projectBudgetAssigned: "Assegnato alle taglie", - projectBudgetRemaining: "Rimanente", projectBudgetOver: "⚠ Budget superato: l'assegnato supera l'obiettivo", projectFollowers: "Sostenitori", projectFollowersTitle: "Sostenitori", @@ -2991,7 +3144,6 @@ module.exports = { projectBackersTotalPledged: "Totale contribuito", projectBackersYourPledge: "Il tuo contributo", projectBackersNone: "Nessun contributo ancora.", - projectNoRemainingBudget: "Nessun budget rimanente.", projectFilterBackers: "FINANZIATORI", projectFilterApplied: "CANDIDATURE", projectAppliedTitle: "CANDIDATURE", @@ -3000,36 +3152,19 @@ module.exports = { projectBackerAmount: "Totale contribuito", projectBackerPledges: "Contributi", projectBackerProjects: "Progetti", - projectPledgeAmount: "Importo", projectSelectMilestoneOrBounty: "Seleziona traguardo o taglia", projectPledgeButton: "Contribuisci", footerLicense: "GPLv3", - footerPackage: "Pacchetto", - footerVersion: "Versione", modulesModuleName: "Nome", modulesModuleDescription: "Descrizione", modulesModuleStatus: "Stato", - modulesTotalModulesLabel: "Moduli caricati", + modulesTotalModulesLabel: "Totale", modulesEnabledModulesLabel: "Attivato", modulesDisabledModulesLabel: "Disattivato", - modulesPopularLabel: "In evidenza", - modulesPopularDescription: "Modulo per ricevere i post più popolari, più visti o più commentati.", - modulesTopicsLabel: "Argomenti", - modulesTopicsDescription: "Modulo per ricevere categorie di discussione basate su interessi comuni.", - modulesSummariesLabel: "Riassunti", - modulesSummariesDescription: "Modulo per ricevere riassunti di discussioni o post lunghi.", - modulesLatestLabel: "Più recenti", - modulesLatestDescription: "Modulo per ricevere i post e le discussioni più recenti.", - modulesThreadsLabel: "Discussioni", - modulesThreadsDescription: "Modulo per ricevere conversazioni raggruppate per argomento o domanda.", - modulesMultiverseLabel: "Multiverso", - modulesMultiverseDescription: "Modulo per ricevere contenuti da altri peer federati.", modulesInvitesLabel: "Inviti", modulesInvitesDescription: "Modulo per gestire e applicare codici invito.", modulesWalletLabel: "Portafoglio", modulesWalletDescription: "Modulo per gestire i tuoi asset digitali (ECOin).", - modulesLegacyLabel: "Chiavi", - modulesLegacyDescription: "Modulo per gestire il tuo segreto (chiave privata) in modo rapido e sicuro.", modulesCipherLabel: "Crittografia", modulesCipherDescription: "Modulo per crittografare e decifrare il tuo testo simmetricamente (usando una password condivisa).", modulesBookmarksLabel: "Segnalibri", @@ -3064,8 +3199,12 @@ module.exports = { modulesOpinionsDescription: "Modulo per scoprire e votare opinioni.", modulesTransfersLabel: "Trasferimenti", modulesTransfersDescription: "Modulo per scoprire e gestire i contratti intelligenti (trasferimenti).", - modulesFediverseLabel: "Fediverse", - modulesFediverseDescription: "Gestisci i tuoi altri account del fediverso, inclusi l'invio e la ricezione di contenuti.", + modulesFediverseLabel: "Multiverso", + modulesBlogsLabel: "Blog", + modulesBlogsDescription: "Modulo per scoprire e gestire i blog.", + modulesPollsLabel: "Sondaggi", + modulesPollsDescription: "Modulo per interrogare la rete e contare le risposte.", + modulesFediverseDescription: "Gestisci i tuoi altri account del multiverso, inclusi l'invio e la ricezione di contenuti.", modulesFeedLabel: "Feed", modulesFeedDescription: "Modulo per scoprire e condividere testi brevi (feed).", modulesParliamentLabel: "Parlamento", @@ -3087,52 +3226,51 @@ module.exports = { modulesProjectsLabel: "Progetti", modulesProjectsDescription: "Modulo per esplorare, finanziare collettivamente e gestire i progetti.", modulesBankingLabel: "Banca", - modulesBankingDescription: "Modulo per determinare il valore reale di ECOIN e distribuire una UBI usando la tesoreria comune.", + modulesBankingDescription: "Modulo per gestire il tesoro comune: valore di ECOIN, tasse e RBU.", modulesFavoritesLabel: "Preferiti", modulesFavoritesDescription: "Modulo per gestire i tuoi contenuti preferiti.", fileTooLargeTitle: "File troppo grande", - fileTooLargeMessage: "Il file supera la dimensione massima consentita (50 MB). Seleziona un file più piccolo.", + fileTooLargeMessage: "Il file supera la dimensione massima consentita (75 MB). Seleziona un file più piccolo.", goBack: "Torna indietro", + errorDismiss: "OK", errorPageTitle: "Si è verificato un errore", + errorNoAllocation: "Nessuna allocazione trovata per questa richiesta.", + errorInvalidClaim: "Questa allocazione non può essere riscossa.", visibilityLabel: "Visibilità", + clearnetPrivateConflict: "Un contenuto privato, nascosto, chiuso, solo su invito o a pagamento non può essere pubblicato sulla CLEARNET. Scegli OASIS o rendilo pubblico.", visibilityPublic: "Pubblica", visibilityHidden: "Nascosta", visibilityMakePublic: "Rendi pubblica", visibilityMakeHidden: "Rendi nascosta", invitesInhabitantsTitle: "Abitanti", - invitesInhabitantsFollow: "Segui", - aiNavPlaceholder: "Dove vuoi andare?", + aiNavPlaceholder: "Descrivi cosa ti serve...", aiNavDisabled: "La navigazione con IA è disattivata.", - aiNavResultsTitle: "Suggerimenti di navigazione IA", + aiNavResultsTitle: "Suggerimenti AINav", aiNavQueryLabel: "Richiesta", - aiNavResultsDescription: "Scegli il percorso più adatto a ciò che stai cercando.", - aiNavResultPath: "Percorso", - aiNavResultDescription: "Cosa puoi fare", aiNavResultMatch: "Corrispondenza", aiNavResultsEmpty: "Nessun percorso corrispondente. Prova /search.", - aiNavSearchInstead: "Cerca invece", modulesAINavLabel: "AINav", modulesAINavDescription: "Modulo per query in linguaggio naturale sui contenuti della rete.", - directConnect: "Connessione diretta", directConnectDescription: "Connettiti direttamente a un peer inserendo indirizzo IP, porta e chiave pubblica.", peerHost: "IP", - peerPort: "Porta (predefinita: 8008)", + peerPort: "Porta", peerPublicKey: "Chiave pubblica (@...ed25519)", connectAndFollow: "Connetti", - deviceSourceLabel: "Dispositivo", modulesPresetTitle: "Configurazioni Comuni", - modulesPreset_minimal: "Minimo", - modulesPreset_basic: "Base", - modulesPreset_social: "Sociale", - modulesPreset_economy: "Economia", - modulesPreset_full: "Completo", + workflowsTitle: "Workflow", + workflowsDescription: "Un workflow imposta il tema e quali moduli sono attivi.", + workflowsSet: "Applica workflow", + workflow_default: "Predefinito", + workflow_jobs: "Lavoro", + workflow_social: "Sociale", + workflow_business: "Affari", + workflow_night: "Notte", + workflow_kids: "Bambini", + workflow_activists: "Attivismo", + workflow_mobile: "Mobile", statsCarbonFootprintTitle: "Impronta di carbonio", - statsCarbonFootprintNetwork: "Impronta di carbonio della rete", - statsCarbonFootprintYours: "La tua impronta di carbonio", statsCarbonTombstone: "Impronta del tombstoning", - feedSuccessMsg: "Feed pubblicato con successo!", - dominantOpinionLabel: "Opinione dominante", - uploadMedia: "Carica media (max: 50MB)", + uploadMedia: "Carica media (max: 75MB)", courtsRespondentInvalid: "Deve essere un ID SSB valido (@...ed25519)", feedDetailTitle: "Feed", @@ -3141,7 +3279,7 @@ module.exports = { noComments: "Nessun commento ancora", mapsLabel: "Mappe", mapTitle: "Mappe", - mapDescription: "Esplora e gestisci mappe offline nella tua rete.", + mapDescription: "Gestisci e condividi mappe offline nella tua rete.", mapMineSectionTitle: "Le Tue Mappe", mapCreateSectionTitle: "Crea Mappa", mapUpdateSectionTitle: "Aggiorna Mappa", @@ -3165,14 +3303,13 @@ module.exports = { mapDescriptionLabel: "Descrizione", mapDescriptionPlaceholder: "Descrivi la mappa o la posizione...", mapTypeLabel: "Tipo di mappa", - mapTypeSingle: "SINGOLO (solo posizione iniziale)", - mapTypeOpen: "APERTO (chiunque può aggiungere marcatori)", - mapTypeClosed: "CHIUSO (solo il creatore aggiunge marcatori)", + mapInviteMode: "Invito", + mapTypeSingle: "SINGOLO", + mapTypeOpen: "APERTO", + mapTypeClosed: "CHIUSO", mapTagsLabel: "Tag", mapTagsPlaceholder: "Inserisci tag separati da virgole", mapUrlLabel: "URL della mappa", - mapPickCoordLabel: "Oppure seleziona una posizione sulla griglia:", - mapMarkersLabel: "marcatori", mapMarkersTitle: "Marcatori", mapMarkerDefault: "Marcatore", mapMarkerLatLabel: "Latitudine marcatore", @@ -3182,9 +3319,22 @@ module.exports = { markerImageLabel: "Immagine del Marcatore", mapAddMarkerTitle: "Aggiungi Marcatore", mapAddMarkerButton: "Aggiungi Marcatore", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "Rimuovi segnaposto", mapApplyZoom: "Applica Zoom", - mapSearchPlaceholder: "Cerca descrizione, tag, autore...", + mapPlaceSearchPlaceholder: "Cerca un luogo o un paese...", + mapNoPlaces: "Nessun luogo trovato.", + mapClusterTitle: "marcatori qui, clicca per ingrandire", + mapDeleteMarker: "Elimina marcatore", + mapZoomIn: "Ingrandisci", + mapZoomOut: "Riduci", + mapFitMarkers: "Adatta a tutti i marcatori", + mapPanLabel: "Sposta la mappa", + mapCenterHere: "Centra qui", + mapNear: "vicino a", + mapPickedSpot: "Punto scelto", + mapPickHint: "Clicca sulla mappa per scegliere un punto.", + mapClearPick: "Pulisci", + mapSearchPlaceholder: "Cerca in Mappe...", mapSearchButton: "Cerca", mapUpdatedAt: "Aggiornato", mapNoMatch: "Nessuna mappa corrisponde alla tua ricerca.", @@ -3195,118 +3345,820 @@ module.exports = { typeMapMarker: "MARCATORE MAPPA", modulesMapLabel: "Mappe", modulesMapDescription: "Modulo per gestire e condividere mappe offline.", + emergenciesTitle: "Emergenze", + modulesEmergenciesLabel: "Emergenze", + modulesEmergenciesDescription: "Modulo per lanciare, confermare e seguire emergenze nella rete.", + emergenciesDescription: "Modulo per lanciare, confermare e seguire emergenze nella rete.", + emergencyCreate: "Crea emergenza", + emergencyUpdate: "Aggiorna", + emergencyDelete: "Elimina", + emergencyTitleLabel: "Titolo", + emergencyTitlePlaceholder: "Cosa sta succedendo?", + emergencyTextLabel: "Dettagli", + emergencyTextPlaceholder: "Dove, da quando, cosa fare...", + emergencyCategoryLabel: "Categoria", + emergencyCategoryWeather: "METEO", + emergencyCategoryInfrastructure: "INFRASTRUTTURE", + emergencyCategoryHealth: "SALUTE", + emergencyCategorySecurity: "SICUREZZA", + emergencyCategoryLost: "OGGETTI SMARRITI", + emergencyCategoryNeighborhood: "VICINATO", + emergencyExpiresLabel: "Scade tra", + emergencyExpires1d: "1 giorno", + emergencyExpires3d: "3 giorni", + emergencyExpires7d: "1 settimana", + emergencyExpires30d: "1 mese", + emergencyExpiresNone: "Indefinito", + emergencyExpiresKeep: "Mantieni la data attuale", + emergencyTagsLabel: "Tag", + emergencyTagsPlaceholder: "Inserisci tag separati da virgole", + emergencyFilterAll: "TUTTE", + emergencyFilterMine: "MIE", + emergencyFilterRecent: "RECENTI", + emergencySeverityUnverified: "NON VERIFICATA", + emergencySeverityLow: "BASSA", + emergencySeverityMedium: "MEDIA", + emergencySeverityHigh: "ALTA", + emergencyStatusActive: "ATTIVA", + emergencyStatusResolved: "RISOLTA", + emergencyStatusExpired: "SCADUTA", + emergencyConfirmations: "Conferme", + emergencyConfirmButton: "Conferma", + emergencyConfirmed: "Hai confermato questa emergenza", + emergencyResolveButton: "Risolvi", + emergencyReopenButton: "Riapri", + emergencyGenerateReport: "Genera rapporto", + emergencyUpdateEdited: "Modificato", + emergencyLastUpdate: "ULTIMO AGGIORNAMENTO", + emergencyUpdatesTitle: "Ultimi aggiornamenti:", + emergencyNoUpdates: "Ancora nessun aggiornamento.", + emergencyAddUpdate: "Pubblica aggiornamento", + emergencyUpdatePlaceholder: "Cosa è cambiato?", + emergencyNoItems: "Nessuna emergenza trovata.", + emergencyExpiresAt: "Scade", + emergencyBannerLabel: "Emergenza attiva", + emergencyCreateSectionTitle: "Crea emergenza", + emergencyUpdateSectionTitle: "Aggiorna emergenza", + emergencySearchPlaceholder: "Cerca nelle emergenze...", + typeEmergency: "EMERGENZE", + typeEmergencyConfirm: "CONFERMA EMERGENZA", + typeEmergencyUpdate: "AGGIORNAMENTO EMERGENZA", + statsEmergency: "Emergenze", + statsEmergencyConfirm: "Conferme di emergenze", + statsEmergencyUpdate: "Aggiornamenti di emergenze", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "Un’emergenza che segui ha un aggiornamento.", + emergencyBotResolvedTitle: "Un’emergenza che segui è stata risolta.", + dataKindEmergencies: "EMERGENZE", + mailingTitle: "Mailing list", + modulesMailingLabel: "Mailing list", + modulesMailingDescription: "Scopri e gestisci mailing list nella tua rete.", + modulesPhoneLabel: "Telefono", + modulesPhoneDescription: "Modulo per chiamare altri abitanti con videochiamate cifrate end-to-end.", + mailingDescription: "Scopri e gestisci mailing list nella tua rete.", + mailingCreate: "Crea mailing list", + mailingUpdate: "Aggiorna", + mailingArchive: "Archivia", + mailingActivate: "Attiva", + mailingCreateSectionTitle: "Crea mailing list", + mailingUpdateSectionTitle: "Aggiorna mailing list", + mailingTitleLabel: "Titolo", + mailingTitlePlaceholder: "Nome della mailing list", + mailingDescriptionLabel: "Descrizione", + mailingDescriptionPlaceholder: "Di cosa tratta questa lista?", + mailingTypeLabel: "Tipo di mailing list", + mailingTypeOpen: "APERTA", + mailingTypeClosed: "CHIUSA", + mailingStatusLabel: "Stato", + mailingStatusActive: "ATTIVA", + mailingStatusArchived: "ARCHIVIATA", + mailingMembersLabel: "Invita abitanti", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Iscritti", + mailingTagsLabel: "Tag", + mailingTagsPlaceholder: "Inserisci tag separati da virgole", + mailingFilterAll: "TUTTE", + mailingFilterMine: "MIE", + mailingFilterSubscribed: "ISCRITTE", + mailingFilterRecent: "RECENTI", + mailingHistoryDate: "DATA", + mailingHistoryThreads: "DISCUSSIONI", + mailingHistoryInhabitants: "ABITANTI", + mailingHistorySearchPlaceholder: "Cerca nella cronologia...", + mailingNoMessages: "Ancora nessun messaggio.", + mailingNoItems: "Nessuna mailing list trovata.", + mailingSearchPlaceholder: "Cerca nelle mailing list...", + mailingWriteButton: "Scrivi messaggio", + mailingWriteTitle: "Scrivi alla lista", + mailingReplyTitle: "Rispondi alla lista", + mailingSend: "Invia", + mailingTextPlaceholder: "Il tuo messaggio raggiunge tutti i partecipanti della lista.", + mailingParticipants: "Partecipanti", + inboxFilterMailing: "Mailing list", + mailingReplyPlaceholder: "Rispondi alla lista...", + mailingReplyToList: "Rispondi alla lista", + typeMailingList: "MAILING LIST", + statsMailingList: "Mailing list", + mailingListLabel: "MAILING LIST", + logisticsTitle: "Logistica", + modulesLogisticsLabel: "Logistica", + modulesLogisticsDescription: "Scopri e gestisci piani logistici nella tua rete.", + logisticsDescription: "Scopri e gestisci piani logistici nella tua rete.", + logisticsCreate: "Crea rotta", + logisticsUpdate: "Aggiorna", + logisticsClose: "Chiudi", + logisticsReopen: "Riapri", + logisticsCreateSectionTitle: "Crea rotta", + logisticsUpdateSectionTitle: "Aggiorna rotta", + logisticsTitleLabel: "Titolo", + logisticsTitlePlaceholder: "Viaggio al mercato, furgone disponibile...", + logisticsKindLabel: "Tipo di logistica", + logisticsKindTrip: "VIAGGIO", + logisticsKindShipment: "SPEDIZIONE", + logisticsKindTrips: "VIAGGI", + logisticsKindShipments: "SPEDIZIONI", + logisticsModeLabel: "Modalità", + logisticsModeOffer: "OFFRO", + logisticsModeRequest: "CERCO", + logisticsModeOffers: "OFFERTE", + logisticsModeRequests: "RICHIESTE", + logisticsDescriptionLabel: "Descrizione", + logisticsDescriptionPlaceholder: "Punto d’incontro, cosa trasporti, condizioni...", + logisticsOriginLabel: "Origine", + logisticsOriginPlaceholder: "Da dove parte", + logisticsDestinationLabel: "Destinazione", + logisticsDestinationPlaceholder: "Dove arriva", + logisticsDateLabel: "Data e ora", + logisticsRecurrenceLabel: "Ricorrenza", + logisticsFrequencyLabel: "Frequenza", + logisticsRecurrenceNone: "UNICA", + logisticsRecurrenceDaily: "GIORNALIERA", + logisticsRecurrenceWeekly: "SETTIMANALE", + logisticsRecurrenceMonthly: "MENSILE", + logisticsSeatsLabel: "Posti", + logisticsSizeLabel: "Dimensioni", + logisticsSizePlaceholder: "es. 2m x 1m", + logisticsWeightLabel: "Peso", + logisticsWeightPlaceholder: "es. 40kg", + logisticsPriceFree: "GRATIS", + logisticsPriceEco: "ECO", + logisticsPriceTime: "TEMPO", + logisticsPriceLabel: "Prezzo", + logisticsHours: "h", + logisticsOrderRefLabel: "Link all’ordine", + logisticsOrderRefPlaceholder: "/shops/... o /market/... dell’ordine da consegnare", + logisticsOrderLink: "Ordine collegato", + logisticsTagsLabel: "Tag", + logisticsTagsPlaceholder: "Inserisci tag separati da virgole", + logisticsFilterAll: "TUTTE", + logisticsFilterMine: "MIE", + logisticsFilterBooked: "PRENOTATE", + logisticsFilterRecent: "RECENTI", + logisticsFilterHistory: "STORICO", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "APERTA", + logisticsStatusUpcoming: "PROSSIME", + logisticsStatusPast: "PASSATE", + logisticsStatusClosed: "CHIUSE", + logisticsZoneLabel: "Zona", + logisticsSearchPlaceholder: "Cerca nella logistica...", + logisticsNoItems: "Nessuna rotta trovata.", + logisticsBookingPending: "IN ATTESA", + logisticsBookingConfirmed: "CONFERMATA", + logisticsBookingRejected: "RIFIUTATA", + logisticsBookingCancelled: "ANNULLATA", + logisticsBookingDelivered: "CONSEGNATA", + logisticsConfirm: "Conferma", + logisticsReject: "Rifiuta", + logisticsDeliver: "Consegna", + logisticsReceiptLabel: "Ricevuta", + logisticsReceiptPlaceholder: "Nota di consegna (facoltativa)", + logisticsCancelBooking: "Annulla prenotazione", + logisticsBookTitle: "Prenota questa rotta", + logisticsNotesLabel: "Note", + logisticsNotesPlaceholder: "Ciò che il responsabile deve sapere", + logisticsBook: "Prenota", + logisticsBookingsTitle: "Prenotazioni", + logisticsMyBookingTitle: "La tua prenotazione", + logisticsRatingsTitle: "Valutazioni", + logisticsNoRatings: "Ancora nessuna valutazione.", + logisticsRatingsWhenClosed: "Le valutazioni si aprono alla chiusura della rotta.", + logisticsScoreLabel: "Punteggio", + logisticsRatingPlaceholder: "Com’è andata?", + logisticsRate: "Valuta", + typeLogisticsRoute: "ROTTA", + typeLogisticsRating: "VALUTAZIONE ROTTA", + statsLogisticsRoute: "Rotte", + statsLogisticsRating: "Valutazioni delle rotte", + logisticsRouteLabel: "ROTTA", + podcastsTitle: "Podcast", + modulesPodcastsLabel: "Podcast", + modulesPodcastsDescription: "Modulo per pubblicare canali podcast.", + podcastsDescription: "Modulo per pubblicare canali podcast.", + podcastCreate: "Crea podcast", + podcastUpdate: "Aggiorna", + podcastDelete: "Elimina", + podcastCreateSectionTitle: "Crea podcast", + podcastUpdateSectionTitle: "Aggiorna podcast", + podcastTitleLabel: "Titolo", + podcastTitlePlaceholder: "Nome del canale", + podcastDescriptionLabel: "Descrizione", + podcastDescriptionPlaceholder: "Di cosa parla questo podcast?", + podcastCategoryLabel: "Categoria", + podcastCategoryNews: "NOTIZIE", + podcastCategoryMusic: "MUSICA", + podcastCategoryTalk: "CONVERSAZIONE", + podcastCategoryEducation: "EDUCAZIONE", + podcastCategoryTech: "TECNOLOGIA", + podcastCategoryCulture: "CULTURA", + podcastCategoryCommunity: "COMUNITÀ", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "ALTRO", + podcastTagsLabel: "Tag", + podcastCoverLabel: "Copertina (immagine o video)", + podcastEpisodeAudioLabel: "File audio o video (max: 75MB)", + podcastTagsPlaceholder: "Inserisci tag separati da virgole", + podcastFilterAll: "TUTTI", + podcastFilterMine: "MIEI", + podcastFilterRecent: "RECENTI", + podcastFilterTop: "TOP", + podcastFilterViewers: "ASCOLTATORI", + podcastSearchPlaceholder: "Cerca nei podcast...", + podcastNoItems: "Nessun podcast trovato.", + podcastEpisodesLabel: "Episodi", + podcastPlaysLabel: "Visualizzazioni", + podcastLastEpisode: "Ultimo episodio", + podcastEpisodesTitle: "Episodi", + podcastNoEpisodes: "Ancora nessun episodio.", + podcastEpisodeAdd: "Aggiungi episodio", + podcastEpisodeCreateTitle: "Pubblica episodio", + podcastEpisodeUpdateTitle: "Aggiorna episodio", + podcastEpisodeTitleLabel: "Titolo dell’episodio", + podcastEpisodeTitlePlaceholder: "Titolo dell’episodio", + podcastEpisodeDescriptionLabel: "Note", + podcastEpisodeDescriptionPlaceholder: "Cosa succede in questo episodio, link, crediti...", + podcastEpisodePublish: "Pubblica", + podcastPlayed: "Ascoltato", + podcastMarkPlayed: "Segna come ascoltato", + typePodcast: "PODCAST", + typePodcastEpisode: "EPISODIO", + typePodcastOpinion: "OPINIONE SULL’EPISODIO", + typePodcastPlay: "ASCOLTO", + statsPodcast: "Podcast", + statsPodcastEpisode: "Episodi", + statsPodcastPlay: "Ascolti", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "Un blog che segui ha una nuova voce.", + podcastBotEpisodeTitle: "Un podcast che segui ha un nuovo episodio.", + podcastLabel: "PODCAST", + podcastEpisodeLabel: "EPISODIO", + campaignsTitle: "Campagne", + modulesCampaignsLabel: "Campagne", + modulesCampaignsDescription: "Scopri e gestisci campagne nella tua rete.", + campaignsDescription: "Scopri e gestisci campagne nella tua rete.", + campaignCreate: "Crea campagna", + campaignUpdate: "Aggiorna", + campaignDelete: "Elimina", + campaignClose: "Chiudi", + campaignReopen: "Riapri", + campaignCreateSectionTitle: "Crea campagna", + campaignUpdateSectionTitle: "Aggiorna campagna", + campaignTitleLabel: "Titolo", + campaignTitlePlaceholder: "Cosa vuoi ottenere?", + campaignTextLabel: "Testo", + campaignTextPlaceholder: "Spiega la causa, chi decide e cosa chiedi...", + campaignCategoryLabel: "Categoria", + campaignCategoryEnvironment: "AMBIENTE", + campaignCategoryRights: "DIRITTI", + campaignCategoryHealth: "SALUTE", + campaignCategoryEducation: "EDUCAZIONE", + campaignCategoryInfrastructure: "INFRASTRUTTURE", + campaignCategoryCulture: "CULTURA", + campaignCategoryEconomy: "ECONOMIA", + campaignCategoryOther: "ALTRO", + campaignGoalLabel: "Obiettivo di firme", + campaignDeadlineLabel: "Scadenza", + campaignTagsLabel: "Tag", + campaignTagsPlaceholder: "Inserisci tag separati da virgole", + campaignFilterAll: "TUTTE", + campaignFilterMine: "MIE", + campaignFilterSigned: "FIRMATE", + campaignFilterRecent: "RECENTI", + campaignFilterTop: "TOP", + campaignStatusOpen: "APERTA", + campaignStatusAchieved: "RAGGIUNTA", + campaignStatusUnachieved: "NON RAGGIUNTA", + campaignStatusClosed: "CHIUSA", + campaignSearchPlaceholder: "Cerca nelle campagne...", + campaignNoItems: "Nessuna campagna trovata.", + campaignSignaturesLabel: "Firme", + campaignSignaturesTitle: "Sostenitori", + campaignNoSignatures: "Ancora nessuna firma.", + campaignSignTitle: "Firma questa campagna", + campaignSignPlaceholder: "Perché la sostieni? (facoltativo)", + campaignSign: "Firma", + campaignSigned: "Hai già firmato!", + campaignNotSigned: "Manca ancora la tua firma", + campaignElevate: "Porta in Parlamento", + campaignElevated: "Portata in Parlamento", + campaignUpdatesTitle: "Aggiornamenti", + campaignNoUpdates: "Ancora nessun aggiornamento.", + campaignUpdatePlaceholder: "Cosa è successo?", + campaignAddUpdate: "Pubblica aggiornamento", + campaignQrAlt: "QR per firmare questa campagna", + campaignQrCaption: "Scansiona per firmare", + typeCampaign: "CAMPAGNA", + typeCampaignSignature: "FIRMA", + typeCampaignUpdate: "AGGIORNAMENTO CAMPAGNA", + statsCampaign: "Campagne", + statsCampaignSignature: "Firme delle campagne", + statsCampaignUpdate: "Aggiornamenti delle campagne", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "Una campagna che segui ha un aggiornamento.", + campaignBotAchievedTitle: "Una campagna che segui ha raggiunto l’obiettivo.", + campaignBotRaisedTitle: "Una campagna che hai firmato è stata portata in Parlamento.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Un percorso che hai prenotato è cambiato.", + logisticsBotClosedTitle: "Un percorso che hai prenotato è stato chiuso.", + logisticsBotBookedTitle: "Qualcuno ha prenotato il tuo percorso.", + logisticsBotCancelledTitle: "Una prenotazione sul tuo percorso è stata annullata.", + logisticsBotConfirmedTitle: "La tua prenotazione è confermata: il pacco è in viaggio.", + logisticsBotRejectedTitle: "La tua prenotazione è stata rifiutata.", + logisticsBotDeliveredTitle: "Il tuo pacco è arrivato.", + campaignLabel: "CAMPAGNA", + campaignGenerateReport: "Genera rapporto", + logisticsGenerateReport: "Genera rapporto", + mailingGenerateReport: "Genera rapporto", + campaignUpdateEdited: "Modificato", + mailingMessagesLabel: "Messaggi", + logisticsBookingsLabel: "Prenotazioni", + backupTitle: "Backup", + modulesBackupLabel: "Backup", + modulesBackupDescription: "Modulo per esportare e importare le tue chiavi e fare backup cifrati.", + backupDescription: "Modulo per esportare e importare le tue chiavi e fare backup cifrati.", + backupTypeRecovery: "RECUPERO", + backupTypeKeys: "ESPORTA CHIAVI", + backupTypeFull: "BACKUP COMPLETO", + backupTypeRestore: "RIPRISTINA BACKUP", + backupTypeInstant: "ISTANTANEA", + backupInstantDescription: "Uno .zip semplice di tutto il tuo registro e dei tuoi file, senza la chiave segreta e senza password. Rapido, ma ", + backupInstantRisk: "chi ha il file legge tutto.", + backupKitPageDescription: "Il kit di recupero può essere aperto solo da questo dispositivo.", + backupKitId: "ID Oasis", + backupKitDate: "Generato", + backupKitQrAlt: "Codice QR della tua chiave segreta", + backupKitStep1: "Stampalo o ricopialo a mano e conservalo in un luogo sicuro e offline.", + backupKitStep2: "Per recuperare, installa Oasis su un nuovo dispositivo e importa questo segreto da Backup › Chiavi.", + backupKitStep3: "Distruggi questa pagina se dovesse mai essere esposta.", + backupKitPdfButton: "Genera PDF", + backupScopeLabel: "Ambito", + backupScopeAll: "TUTTO", + backupScopeMine: "SOLO I MIEI CONTENUTI", + backupSinceLabel: "Da", + backupEstimateTitle: "Stima", + backupFileInfo: "La tua copia cifrata verrà scaricata sul tuo dispositivo (nome: oasis-backup-DATA.oasisbk)", + backupExportButton: "Scarica backup", + backupRestorePassword: "Scrivi la password usata quando è stato fatto il backup", + backupRestoreButton: "Ripristina backup", + backupRestoredLine: "Backup ripristinato.", + backupRestoredMessages: "Messaggi aggiunti", + backupRestoredSkipped: "Già presenti", + backupRestoredBlobs: "File aggiunti", + backupRestoredState: "Impostazioni ripristinate", + backupRestoredFailed: "Falliti", + backupRestoredForked: "Divergenti", + backupRestoreRunning: "Ripristino della copia in corso. Questa pagina si aggiorna da sola; nel frattempo puoi continuare a usare Oasis.", + backupRestoreElapsed: "Trascorso", + backupRestoreFailedLine: "Non è stato possibile ripristinare la copia.", + backupRestoreForkMine: "Questo dispositivo ha già pubblicato messaggi con la tua identità che non sono nella copia, quindi non è stato possibile applicarla sopra di essi. Ripristina su un’installazione pulita subito dopo aver importato le chiavi, prima di fare qualsiasi altra cosa.", + verificationTitle: "Verifica", + verificationDescription: "Controlla la catena del tuo feed, rileva biforcazioni e rivedi i file orfani o mancanti.", + verificationRun: "Esegui verifica", + verificationTotalMessages: "messaggi nel log", + verificationFeed: "Il tuo feed", + verificationMessages: "messaggi", + verificationLastSequence: "ultima sequenza", + verificationGaps: "Buchi", + verificationBrokenLinks: "Collegamenti rotti", + verificationBadHashes: "Hash errati", + verificationBadSignatures: "Firme errate", + verificationDuplicates: "Sequenze duplicate", + verificationForks: "Biforcazioni", + verificationBlobs: "File", + verificationBlobsPresent: "presenti", + verificationBlobsReferenced: "referenziati", + verificationBlobsOrphan: "orfani", + verificationBlobsMissing: "mancanti", + verificationBlobsMissingOwn: "File tuoi mancanti su questo dispositivo", + verificationBlobsMissingOthers: "File di altri abitanti non ancora scaricati", + verificationBlobsUnavailable: "Impossibile elencare i file su questo dispositivo.", + wikiTitle: "Wiki", + modulesWikiLabel: "Wiki", + modulesWikiDescription: "Modulo per scoprire e gestire wiki.", + wikiDescription: "Modulo per scoprire e gestire wiki.", + wikiCreate: "Crea wiki", + wikiUpdate: "Aggiorna", + wikiDelete: "Elimina", + wikiEdit: "Modifica", + wikiTitleLabel: "Titolo", + wikiTitlePlaceholder: "Titolo della wiki...", + wikiBodyLabel: "Contenuto", + wikiBodyPlaceholder: "Collega altre wiki con [[Titolo della wiki]] o [[Titolo della wiki|un’etichetta personalizzata]].\n\nMenziona un abitante con @id.\n\nTagga con #oasis.\n\nIncolla link come https://solarnethub.com\n\nUna riga vuota apre un nuovo paragrafo; le interruzioni di riga singole si conservano.\n\nAllega qualsiasi media supportato: verrà incorporato qui.", + formatBarLabel: "Formato", + wikiStatusLabel: "Tipo di wiki", + wikiStatusOpen: "APERTA", + wikiStatusClosed: "CHIUSA", + wikiTagsLabel: "Tag", + wikiTagsPlaceholder: "Inserisci tag separati da virgole", + wikiSummaryLabel: "Riepilogo della modifica", + wikiSummaryPlaceholder: "Cosa è cambiato?", + wikiFilterAll: "TUTTE", + wikiFilterMine: "MIE", + wikiFilterRecent: "RECENTI", + wikiFilterLinked: "COLLEGATE", + wikiChanges: "Modifiche recenti", + wikiNoPages: "Nessuna wiki trovata.", + wikiNoChanges: "Ancora nessuna modifica.", + wikiHistory: "Cronologia", + wikiVersion: "Versione", + wikiVersions: "versioni", + wikiRestore: "Ripristina", + wikiDiff: "Modifiche", + wikiViewVersion: "Vedi", + wikiLinkedPages: "Pagine collegate", + wikiNoLinkedPages: "Questa pagina non è ancora collegata a nessun altra.", + wikiMissingLink: "La pagina non esiste ancora", + wikiDuplicateTitle: "Esiste già una pagina con questo titolo. Cambia il titolo per creare una pagina diversa.", + wikiCreateSectionTitle: "Crea wiki", + wikiUpdateSectionTitle: "Aggiorna wiki", + wikiSearchPlaceholder: "Cerca nella Wiki...", + wikiCurrentVersion: "Versione attuale", + wikiViewingVersion: "Stai vedendo una versione precedente", + wikiBackToPage: "Torna alla pagina", + wikiLinkedChip: "Collegata", + wikiRestoredSummary: "Versione ripristinata", + wikiExportPdf: "Genera pagina", + wikiPreview: "Anteprima", + tribeSectionWiki: "WIKI", + tribeWikiCreate: "Crea wiki", + tribeWikiEmpty: "Ancora nessuna wiki.", + typeWiki: "WIKI", padsTitle: "Pad", padTitle: "Pad", modulesPadsLabel: "Pad", modulesPadsDescription: "Modulo per gestire editor di testo collaborativi.", - padFilterAll: "TUTTI", - padFilterMine: "MIO", - padFilterRecent: "RECENTE", - padFilterOpen: "APERTO", - padFilterClosed: "CHIUSO", padCreate: "Crea Pad", - padUpdate: "Aggiorna Pad", - padDelete: "Elimina Pad", + padUpdate: "Aggiorna", + padDelete: "Elimina", padTitleLabel: "Titolo", padTitlePlaceholder: "Inserisci il titolo del pad...", padStatusLabel: "Stato", + padTypeLabel: "Tipo di pad", padStatusOpen: "APERTO", padStatusInviteOnly: "SOLO SU INVITO", padStatusClosed: "CHIUSO", padDeadlineLabel: "Scadenza", padTagsLabel: "Tag", - padTagsPlaceholder: "tag1, tag2, ...", + padTagsPlaceholder: "Inserisci tag separati da virgole", padMembersLabel: "Membri", - padVisitPad: "Visita Pad", - padShareUrl: "Condividi URL", padCreated: "Creato", - padAuthor: "Autore", - padGenerateCode: "Genera Codice", - padInviteCodeLabel: "Codice Invito", - padInviteCodePlaceholder: "Inserisci il codice invito...", - padValidateInvite: "Valida", - padStartEditing: "INIZIA A MODIFICARE!", + padStartEditing: "Modifica", padEditorPlaceholder: "Inizia a scrivere...", padSubmitEntry: "Invia", padNoEntries: "Nessuna voce ancora.", padAllSectionTitle: "Tutti i Pad", padMineSectionTitle: "I Miei Pad", padsDescription: "Gestisci editor di testo collaborativi cifrati nella tua rete.", + roomsTitle: "Stanze", + roomsDescription: "Scopri ed entra nelle stanze di videoconferenza cifrate della tua rete.", + modulesRoomsLabel: "Stanze", + modulesRoomsDescription: "Modulo per scoprire ed entrare in stanze di videoconferenza cifrate.", + roomFilterRecent: "RECENTI", + roomFilterMine: "MIE", + roomFilterAll: "TUTTE", + roomFilterLive: "IN DIRETTA", + roomFilterOpen: "APERTE", + roomFilterInvite: "SOLO SU INVITO", + roomFilterClosed: "CHIUSE", + roomCreate: "Crea stanza", + roomUpdate: "Aggiorna", + roomDelete: "Elimina", + roomClose: "Chiudi", + roomReopen: "Riapri", + roomCreateSectionTitle: "Crea stanza", + roomUpdateSectionTitle: "Aggiorna stanza", + roomTitlePlaceholder: "Titolo della stanza", + roomDescriptionLabel: "Descrizione", + roomDescriptionPlaceholder: "Di cosa parlerete?", + roomTypeLabel: "Tipo di stanza", + roomTagsLabel: "Tag", + roomStatusOpen: "APERTA", + roomStatusInviteOnly: "SOLO SU INVITO", + roomStatusClosed: "CHIUSA", + roomParticipants: "Partecipanti", + roomJoin: "Entra", + roomLeave: "Esci", + roomFull: "Stanza piena", + roomLiveChip: "IN DIRETTA", + roomMuted: "microfono disattivato", + roomRec: "Registra audio", + roomRecStop: "Ferma la registrazione", + roomStreamFile: "Trasmetti", + roomStreamStop: "Ferma", + roomStreaming: "In trasmissione", + liveFullscreen: "Schermo intero", + roomStreamPickTitle: "Scegli un video", + roomStreamPickHint: "Video di questo dispositivo. Quello che scegli viene trasmesso da dove si trova, senza copiarlo né pubblicarlo.", + roomStreamGo: "Trasmetti nella stanza", + roomStreamNotFound: "Quel video non è nelle tue cartelle.", + roomPublicWarning: "Questa stanza è trasmessa sul web pubblico: chiunque può vedere e sentire ciò che vi accade.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} è entrato nella stanza", + roomEventLeft: "{name} ha lasciato la stanza", + roomEventRecStart: "{name} ha iniziato a registrare", + roomEventRecStop: "{name} ha smesso di registrare", + roomEventMuted: "{name} si è silenziato", + roomEventUnmuted: "{name} ha tolto il silenzio", + roomEventHand: "{name} ha alzato la mano", + roomRaiseHand: "Alza la mano", + roomLowerHand: "Abbassa la mano", + confirmActionTitle: "Conferma questa azione", + confirmActionButton: "Conferma", + confirmJoinText: "Stai per unirti con questo invito.", + confirmFollowText: "Stai per sostenere questo abitante.", + confirmSignText: "Stai per firmare questa campagna.", + confirmDownloadText: "Stai per scaricare questo file sul tuo dispositivo.", + roomRecordings: "Registrazioni", + roomClearNotices: "Pulisci", + roomLineSwitchboard: "Centralino", + roomLineDndError: "Il numero di questa stanza non accetta chiamate.", + roomNumberHint: "Componi questo numero da Telefono per entrare nella stanza.", + liveRefresh: "Aggiorna", + roomNobody: "Al momento non c'è nessuno nella stanza.", + roomSomeone: "{n} nella stanza in questo momento.", + roomYou: "tu", + roomYouAreIn: "Ci sei dentro", + roomNobodyInside: "Al momento non c'è nessuno in questa stanza.", + roomErrorFull: "La stanza è piena. Potrai entrare quando qualcuno esce.", + roomErrorUnreachable: "Al momento la stanza non è raggiungibile. Riprova più tardi.", + roomErrorBusy: "Sei già in una chiamata o in un'altra stanza.", + roomErrorClosed: "Questa stanza è chiusa.", + roomAccessDenied: "Questa stanza è solo su invito. Ti serve un invito per entrare.", + roomsNoItems: "Nessuna stanza trovata.", + roomSearchPlaceholder: "Cerca stanze...", + roomInviteMode: "Invito", + tribeSectionRooms: "STANZE", + tribeRoomCreate: "Crea stanza", + tribeRoomsEmpty: "Ancora nessuna stanza.", + typeRoom: "STANZE", + typeRoomMember: "PARTECIPANTE DI STANZA", + statsRoom: "Stanze", + statsRoomMember: "Partecipanti alle stanze", + favoritesFilterRooms: "STANZE", + invitesRoomsTitle: "Stanze", + inviteCodeInvalid: "Questo codice d'invito non è valido o è già stato usato.", + inviteAlreadyMember: "Ne fai già parte. Puoi aprirlo dal suo modulo.", + actionFailed: "Non è stato possibile farlo. Riprova.", + publishTooShort: "Il tuo post è troppo corto.", + walletSendFailed: "Non è stato possibile inviare il pagamento: {reason}", + pixeliaCoordsError: "Quelle coordinate sono fuori dalla griglia.", + courtsErrTitle: "Serve un titolo.", + courtsErrRespondent: "Indica contro chi è il caso.", + courtsErrRespondentId: "Questo non è un Oasis ID valido.", + courtsErrMethod: "Scegli come verrà risolto il caso.", + courtsErrCaseNotFound: "Quel caso non è stato trovato.", + courtsErrBrief: "Scrivi la tua risposta.", + courtsErrStance: "Scegli la tua posizione.", + courtsErrResult: "Scrivi il risultato.", + courtsErrTerms: "Scrivi i termini dell'accordo.", + courtsErrMediator: "Aggiungi almeno un mediatore.", + courtsErrSelfMediator: "Non puoi essere mediatore nel tuo stesso caso.", + courtsErrJudge: "Scegli un giudice.", + courtsErrSelfJudge: "Non puoi essere giudice del tuo stesso caso.", + courtsErrVisibility: "Scegli chi può vedere il caso.", + courtsErrNomination: "Quella candidatura non è stata trovata.", + opinionAlreadyGiven: "Hai già dato la tua opinione.", + cipherPasswordShort: "La password deve avere almeno 32 caratteri.", + errorForbidden: "Non hai accesso a questo.", + errorNotFound: "Non è stato trovato.", + errorClosed: "È chiuso.", + errorInviteOnly: "È solo su invito. Ti serve un invito.", + errorTooLong: "Quel testo è troppo lungo.", + errorRequired: "Mancano informazioni obbligatorie.", + errorInvalid: "Alcune informazioni non sono valide.", + errorNotAuthor: "Solo l'autore può farlo.", + panicRemoved: "La tua blockchain è stata eliminata.", padRecentSectionTitle: "Pad Recenti", padOpenSectionTitle: "Pad Aperti", padClosedSectionTitle: "Pad Chiusi", padCreateSectionTitle: "Crea Nuovo Pad", padUpdateSectionTitle: "Aggiorna Pad", - padInviteGenerated: "Codice Invito Generato", typePad: "PADS", padNew: "NUOVO", padAddFavorite: "Aggiungi ai Preferiti", padRemoveFavorite: "Rimuovi dai Preferiti", - padClose: "Chiudi Pad", + padClose: "Chiudi", padBackToEditor: "Torna all'editor", - padSearchPlaceholder: "Cerca pad...", + padSearchPlaceholder: "Cerca in Pad...", modulesChatsLabel: "Chat", - modulesChatsDescription: "Modulo per scoprire e gestire chat cifrate.", + modulesChatsDescription: "Modulo per scoprire e gestire chat.", typeChat: "CHATS", typeChatMessage: "MESSAGGIO CHAT", - chatLabel: "CHAT", - chatMessageLabel: "MESSAGGI CHAT", chatsTitle: "Chat", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "Le tue chat", + chatRecentTitle: "Chat recenti", chatFavoritesTitle: "Preferiti", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "Chat aperte", + chatClosedTitle: "Chat chiuse", chatDescription: "Descrizione", chatCategory: "Categoria", chatStatus: "STATO", - chatFilterAll: "TUTTI", - chatFilterMine: "MIO", - chatFilterRecent: "RECENTE", - chatFilterFavorites: "PREFERITI", - chatFilterOpen: "APERTO", - chatFilterClosed: "CHIUSO", chatCreate: "Crea Chat", - chatUpdate: "Aggiorna Chat", - chatDelete: "Elimina Chat", + chatUpdate: "Aggiorna", + chatDelete: "Elimina", chatClose: "Chiudi Chat", chatVisitChat: "VISITA CHAT", chatUntitled: "Chat senza titolo", chatNoItems: "Nessuna chat trovata.", chatParticipants: "Partecipanti", - chatStartChatting: "INIZIA A CHATTARE!", - chatGenerateCode: "Genera Codice", - chatShareUrl: "Condividi URL", + chatMessagesLabel: "Messaggi", + chatThreadsLabel: "Discussioni", chatCreatedAt: "CREATO", - chatSearchPlaceholder: "Cerca chat...", + chatSearchPlaceholder: "Cerca in Chat...", chatStatusOpen: "APERTO", chatStatusInviteOnly: "SOLO SU INVITO", chatStatusClosed: "CHIUSO", chatSendMessage: "Invia", + chatJumpLatest: "Ultimo Messaggio", + chatReply: "Rispondi", + chatReplyCancel: "Annulla risposta", + chatPin: "Fissa", + chatUnpin: "Sblocca", + chatPinned: "Fissati", + chatPickChat: "Scegli una chat per aprirla", + chatNoneYet: "Nessuna chat disponibile, per ora.", + activitySearchPlaceholder: "Cerca in Attività...'attività...", + trendingSearchPlaceholder: "Cerca in Tendenze...", + opinionsSearchPlaceholder: "Cerca in Opinioni...", + votesSearchPlaceholder: "Cerca in Votazioni...", + gamesSearchPlaceholder: "Cerca in Giochi...", + schoolTitle: "Scuola", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "Una pagina wiki che segui è stata modificata.", + wikiBotRestoredTitle: "È stata ripristinata una versione precedente di una pagina wiki che segui.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Un nuovo studente si è iscritto al tuo corso.", + schoolBotInvitedTitle: "Sei stato invitato a un corso.", + schoolBotAdmittedTitle: "Sei stato ammesso a un corso.", + schoolBotCertificateTitle: "Hai ricevuto un certificato.", + schoolBotPassedTitle: "Uno studente ha superato il tuo corso.", + schoolBotLessonTitle: "È stata pubblicata una nuova lezione.", + invitesSchoolTitle: "Scuola", + schoolEnrollLabel: "Unisciti al corso", + schoolExamRetryLater: "Potrai riprovare dopo 24 ore.", + schoolApproved: "SUPERATO", + schoolExamPassMark: "Soglia di superamento (0-10)", + schoolExamPassed: "SUPERATO", + schoolExamPending: "In attesa", + schoolExamFailed: "NON SUPERATO", + schoolExamAddQuestion: "Aggiungi domanda", + schoolExamCreateQuestion: "Crea domanda", + schoolExamOption: "Opzione", + schoolExamCorrect: "Opzione corretta", + schoolExamPoints: "Punti", + schoolMaterialTextPlaceholder: "Note per questa lezione...", + schoolMaterials: "Materiali", + schoolAddMaterial: "Aggiungi materiale", + schoolNoMaterials: "Ancora nessun materiale.", + bankYourSchoolBalance: "I tuoi guadagni della scuola", + schoolProgress: "Progresso", + schoolProgressMark: "Segna come completata", + schoolProgressDone: "Completata", + schoolExams: "Esami", + schoolExamCreate: "Crea esame", + schoolExamTitlePlaceholder: "Titolo dell'esame", + schoolExamHelp: "Una domanda per riga: Domanda? ; opzione ; opzione corretta* ; opzione", + schoolExamQuestionsPlaceholder: "Cos'è SSB? ; Un database ; Un protocollo P2P* ; Un browser", + schoolExamSubmit: "Invia risposte", + schoolExamScore: "Il tuo punteggio", + schoolNoScoresYet: "Ancora nessun risultato.", + schoolLessonUnit: "Unità / fase", + schoolLessonUnitPlaceholder: "es. Fase 1", + schoolLessonOrder: "Ordine", + profileClearnetSchoolLabel: "Scuola", + schoolStartDate: "Data di inizio", + schoolSessionDate: "Data della sessione", + schoolCourseChat: "Chat del corso", + agendaFilterSchool: "SCUOLA", + agendaFilterCampaigns: "CAMPAGNE", + agendaFilterLogistics: "LOGISTICA", + schoolCourseType: "Tipo di corso", + schoolTypeOpen: "APERTO", + schoolTypePaid: "A PAGAMENTO", + schoolTypeInvite: "SOLO SU INVITO", + schoolLessonLocked: "Lezione cifrata — chiedi all'insegnante la chiave del corso.", + schoolGrantAccess: "Invia chiave del corso", + schoolKeySent: "Chiave inviata", + schoolOngoing: "In corso", + schoolFilterApplied: "ISCRITTO", + schoolFilterOpen: "APERTI", + schoolFilterFavorites: "PREFERITI", + schoolPendingPayment: "Pagamento in sospeso", + schoolPendingStudents: "Iscrizioni in sospeso", + schoolViewBill: "Vedi fattura", + schoolPaymentNote: "L'iscrizione genera una fattura in ECO (un trasferimento). La tua iscrizione è confermata quando il trasferimento viene firmato.", + schoolCloseCourse: "Chiudi corso", + schoolReopenCourse: "Riapri corso", + schoolDescription: "Insegna e impara corsi nella tua rete.", + schoolFilterAll: "TUTTI", + schoolFilterMine: "MIEI", + schoolFilterTop: "TOP", + schoolFilterRecent: "RECENTI", + schoolCreateButton: "Crea Corso", + schoolCreateSectionTitle: "Crea un corso", + schoolUpdateSectionTitle: "Aggiorna corso", + schoolCourseTitlePlaceholder: "Nome del tuo corso", + schoolCourseDescriptionPlaceholder: "Cosa insegnerai in questo corso?", + schoolCourseImage: "Immagine del corso (dimensione max: 75MB)", + schoolTags: "Tag", + schoolTagsPlaceholder: "Inserisci tag separati da virgole", + schoolPrice: "Prezzo (ECO)", + schoolVisibility: "Visibilità", + schoolVisibilityPublic: "Pubblico", + schoolVisibilityInvite: "Su invito", + schoolStatus: "Stato", + schoolClosed: "Chiuso", + schoolFree: "Gratis", + schoolCreate: "Crea corso", + schoolUpdate: "Aggiorna corso", + schoolSearchPlaceholder: "Cerca in Scuola...", + schoolSortTitle: "Titolo", + schoolNoCourses: "Nessun corso trovato.", + schoolStudents: "Studenti", + schoolTeacher: "Insegnante", + schoolCreatedAt: "CREATO", + schoolLessons: "Lezioni", + schoolNoLessons: "Ancora nessuna lezione.", + schoolEnrollToSee: "Iscriviti per vedere le lezioni di questo corso.", + schoolEnrollButton: "ISCRIVITI", + schoolUnenrollButton: "Abbandona il corso", + schoolInviteRequired: "Questo corso è su invito.", + schoolTeacherPanel: "Pannello dell'insegnante", + schoolStudentPanel: "Pannello dello studente", + schoolAddLesson: "Crea lezione", + schoolLessonTitlePlaceholder: "Titolo della lezione", + schoolLessonTextPlaceholder: "Descrivi le nozioni trattate in questa lezione...", + schoolInviteStudents: "Invita studenti", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "INVITA", + schoolInvited: "Invitati", + schoolIssueCertificate: "Crea certificato", + schoolCertified: "Certificato", + schoolCertificateTextPlaceholder: "Nota del certificato (opzionale)", + schoolLessonOpen: "Apri lezione", + schoolCertificatePdf: "PDF", + schoolCertificates: "Certificati", + schoolNoCertificates: "Nessun certificato emesso finora.", + schoolEditButton: "Modifica", + schoolDeleteButton: "Elimina", + typeSchool: "SCUOLA", + schoolCourseLabel: "CORSO", + emergencyLabel: "EMERGENZA", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Corsi", + statsSchoolLesson: "Lezioni", + statsSchoolEnroll: "Iscrizioni", + statsSchoolCertificate: "Certificati della scuola", + modulesSchoolLabel: "Scuola", + modulesSchoolDescription: "Modulo per scambiare conoscenza.", + welcomeStepUxAction: "Salva vista", + welcomeStepWorkflowTitle: "Che uso di OASIS hai in mente?", + welcomeStepWorkflowText: "Oasis ha molti moduli. Scegli la configurazione adatta a te e resteranno attivi solo quelli utili. Puoi modificarla quando vuoi in Moduli.", + welcomeStepWorkflowAction: "Salva configurazione", + welcomeWorkflow_full: "Predefinito", + chatRateLimitTitle: "Troppi messaggi", + chatRateLimitMessage: "Hai raggiunto il limite di messaggi che questa stanza accetta in un'ora. Riprova più tardi.", chatMessagePlaceholder: "Scrivi il tuo messaggio...", chatNoMessages: "Ancora nessun messaggio.", - chatLeave: "Abbandona Chat", - chatInviteCodeLabel: "Inserisci codice invito", - chatJoinByInvite: "Unisciti", chatTitlePlaceholder: "Titolo chat", chatDescriptionPlaceholder: "Descrizione chat", - chatTagsPlaceholder: "tag1, tag2, tag3", + chatTagsPlaceholder: "Inserisci tag separati da virgole", chatImageLabel: "Seleziona un file immagine (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "Codice Invito", chatAuthor: "Autore", - chatCreated: "Creato", chatAddFavorite: "Aggiungi ai Preferiti", chatRemoveFavorite: "Rimuovi dai Preferiti", chatPM: "MP", chatStatusLabel: "Stato", + chatTypeLabel: "Tipo di chat", chatCategoryLabel: "Categoria", - chatParticipantsLabel: "Partecipanti", gamesTitle: "Giochi", gamesDescription: "Scopri e gioca ad alcuni giochi.", gamesFilterAll: "TUTTI", gamesPlayButton: "GIOCA!", - gamesBackToGames: "Torna ai Giochi", modulesGamesLabel: "Giochi", modulesGamesDescription: "Modulo per scoprire e giocare ad alcuni giochi.", modulesGraphosLabel: "Graphos", @@ -3323,8 +4175,6 @@ module.exports = { gamesArkanoidDesc: "Rompi tutti i mattoni con la tua racchetta e la pallina. Una sfida arcade classica.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong classico contro un'IA. Il primo a 5 punti vince.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "Corsa contro il tempo! Evita il traffico e raggiungi il traguardo prima che scada.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilota la tua navicella in un campo di asteroidi. Sparali prima che ti colpiscano.", gamesRockPaperScissorsTitle: "Carta Forbici Sasso", @@ -3334,68 +4184,57 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "Lancia una moneta e scommetti su testa o croce. Quanta fortuna hai?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "Biliardo visto dall'alto. Clicca per mirare, tieni premuto per caricare la potenza. Imbuca tutte le palle con il minor numero di tiri.", + gamesArtilleryTitle: "Artiglieria", + gamesArtilleryDesc: "Punta il cannone, tieni conto del vento e colpisci il bersaglio. 5 round, vince chi usa meno colpi.", + gamesLabyrinthTitle: "Labirinto", + gamesLabyrinthDesc: "Fuggi dal labirinto prima di esaurire le mosse. Ogni livello è più grande e più difficile.", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "Mangia tutti i puntini, evita i fantasmi. I fantasmi si muovono in tempo reale — non fermarti!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesAudioPendulumTitle: "Pendolo sonoro", + gamesAudioPendulumDesc: "Simulatore di fisica caotica con sintesi audio in tempo reale. Le velocità angolari diventano frequenze, i picchi colpi di percussione. Non esistono due simulazioni che suonino uguali.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Mixa visual dal vivo su due piatti: scegli un motivo per ciascuno, regola velocità, colore e zoom, sfuma tra i due e lancia flash a tempo. Batti il tempo o lascia che il microfono guidi il battito.", + gamesTetrisDesc: "I classici blocchi che cadono. Completa le linee per fare punti. Quanto resisterai?", + gamesMinesweeperTitle: "Campo minato", + gamesMinesweeperDesc: "La classica caccia alle mine. Ripulisci il campo per salire di livello — griglie più grandi, più mine, punteggio più alto.", + gamesFilterScoring: "PUNTEGGI", + gamesHallOfFame: "Albo d'oro", + gamesHallPlayer: "Giocatore", + gamesHallScore: "Punteggio", + gamesNoScores: "Ancora nessun punteggio.", calendarsTitle: "Calendari", calendarTitle: "Calendario", modulesCalendarsLabel: "Calendari", modulesCalendarsDescription: "Modulo per scoprire e gestire i calendari.", typeCalendar: "CALENDARI", - calendarFilterAll: "TUTTI", - calendarFilterMine: "I MIEI", - calendarFilterOpen: "APERTO", - calendarFilterClosed: "CHIUSO", - calendarFilterRecent: "RECENTI", - calendarFilterFavorites: "PREFERITI", calendarCreate: "Crea calendario", calendarUpdate: "Aggiorna", - calendarDelete: "Elimina", calendarTitleLabel: "Titolo", calendarTitlePlaceholder: "Titolo del calendario...", calendarStatusLabel: "Stato", + calendarTypeLabel: "Tipo di calendario", calendarStatusOpen: "APERTO", calendarStatusClosed: "CHIUSO", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "Scadenza", calendarTagsLabel: "Tag", - calendarTagsPlaceholder: "tag1, tag2...", + calendarTagsPlaceholder: "Inserisci tag separati da virgole", calendarParticipantsLabel: "Partecipanti", - calendarParticipantsCount: "Partecipanti", - calendarVisitCalendar: "Visita il calendario", calendarCreated: "Creato", - calendarAuthor: "Autore", calendarJoin: "Partecipa", calendarGenerateInvite: "Genera invito", - calendarInviteCodePlaceholder: "Inserisci il codice di invito...", - calendarValidateInvite: "Convalida codice", - calendarJoined: "Iscritto", - calendarAddDate: "Aggiungi data", - calendarAddNote: "Aggiungi nota", calendarDateLabel: "Data", calendarDatePlaceholder: "Descrivi questa data...", - calendarNoteLabel: "Nota", + calendarNoteLabel: "Note", calendarNotePlaceholder: "Aggiungi una nota...", calendarFirstDateLabel: "Data", calendarFirstNoteLabel: "Note", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "Intervallo", + calendarIntervalNone: "Senza ripetizione", + calendarIntervalWeekly: "Settimanale", + calendarIntervalMonthly: "Mensile", + calendarIntervalYearly: "Annuale", calendarFormDescription: "Descrizione", calendarNoDates: "Nessuna data aggiunta.", calendarNoNotes: "Nessuna nota.", @@ -3403,8 +4242,8 @@ module.exports = { calendarsDescription: "Scopri e gestisci i calendari nella tua rete.", calendarMonthPrev: "← Precedente", calendarMonthNext: "Successivo →", - calendarMonthLabel: "Date", - calendarsShareUrl: "URL di condivisione", + calendarMonthNames: "Gennaio,Febbraio,Marzo,Aprile,Maggio,Giugno,Luglio,Agosto,Settembre,Ottobre,Novembre,Dicembre", + calendarDayInitials: "L,M,M,G,V,S,D", calendarAllSectionTitle: "Calendari", calendarRecentSectionTitle: "Calendari Recenti", calendarFavoritesSectionTitle: "Preferiti", @@ -3414,11 +4253,10 @@ module.exports = { calendarCreateSectionTitle: "Crea nuovo calendario", calendarUpdateSectionTitle: "Aggiorna calendario", calendarAddFavorite: "Aggiungi ai preferiti", - calendarDeleteNote: "Delete", + calendarDeleteNote: "Elimina", calendarRemoveFavorite: "Rimuovi dai preferiti", - calendarSearchPlaceholder: "Cerca calendari...", + calendarSearchPlaceholder: "Cerca in Calendario...", calendarAddEntry: "Aggiungi Voce", - calendarLeave: "Lascia Calendario", statsCalendar: "Calendari", statsCalendarDate: "Date calendario", statsCalendarNote: "Note calendario", @@ -3429,6 +4267,146 @@ module.exports = { tribeContentAccessDenied: "Accesso Negato", tribeContentAccessDeniedMsg: "Questo contenuto appartiene a una tribù. Devi essere membro per accedervi.", tribeViewTribes: "Visualizza Tribù", + tribeFilesEmpty: "Ancora nessun file.", + filesLabel: "File", + profileClearnetFilesLabel: "File", + filesTitle: "File", + filesDescription: "Esplora e gestisci i file nella tua rete.", + fileAllSectionTitle: "File", + fileMineSectionTitle: "I Tuoi File", + fileRecentSectionTitle: "File Recenti", + fileTopSectionTitle: "Migliori File", + fileFavoritesSectionTitle: "Preferiti", + fileFilterAll: "TUTTI", + fileFilterMine: "MIEI", + fileFilterRecent: "RECENTI", + fileFilterTop: "MIGLIORI", + fileFilterFavorites: "PREFERITI", + fileCreateSectionTitle: "Carica File", + fileUpdateSectionTitle: "Aggiorna File", + fileCreateButton: "Carica File", + fileUpdateButton: "Aggiorna", + fileAddFavoriteButton: "Aggiungi ai Preferiti", + fileRemoveFavoriteButton: "Rimuovi dai Preferiti", + fileFileLabel: "Seleziona un file (max: 75MB)", + calendarFilterRecent: "RECENTI", + calendarFilterAll: "TUTTI", + calendarFilterMine: "MIEI", + calendarFilterFavorites: "PREFERITI", + calendarFilterOpen: "APERTI", + calendarFilterClosed: "CHIUSI", + padFilterRecent: "RECENTI", + padFilterAll: "TUTTI", + padFilterMine: "MIEI", + padFilterOpen: "APERTI", + padFilterClosed: "CHIUSI", + chatFilterRecent: "RECENTI", + chatFilterAll: "TUTTI", + chatFilterMine: "MIEI", + chatFilterFavorites: "PREFERITI", + chatFilterOpen: "APERTI", + chatFilterClosed: "CHIUSI", + licenseLabel: "Licenza", + phoneTitle: "Telefono", + phoneDescription: "Gestisci le tue videochiamate cifrate end-to-end.", + phoneUnavailable: "Le chiamate non sono disponibili su questo dispositivo.", + phoneCallButton: "Chiama", + profileVisibilityPhone: "Numero di telefono", + phoneNumberUnknown: "Nessuno nella tua rete ha il numero {number}.", + phoneNumberChoose: "Controlla chi vuoi chiamare al {number}:", + phoneAnswer: "Rispondi", + phoneSpeaker: "Altoparlante", + phoneEarpiece: "Auricolare", + phoneReject: "Rifiuta", + phoneHangup: "Riaggancia", + phoneMute: "Disattiva microfono", + phoneUnmute: "Attiva microfono", + phoneCameraOn: "Fotocamera on", + phoneCameraOff: "Fotocamera off", + settingsPhoneVideo: "Video", + settingsPhoneVideoOn: "Consentito in chiamate e stanze", + settingsPhoneVideoOff: "Solo audio", + phoneSilence: "Non sentire", + phoneUnsilence: "Sentire di nuovo", + phoneCalling: "Chiamata a", + phoneIsCalling: "ti sta chiamando", + notifyNewPmFrom: "Nuovo MP da", + notifyNewPamFrom: "Nuovo PaM da", + notifyUpdateLabel: "Aggiornamento", + updateInstalledLabel: "Versione installata", + updateAvailableLabel: "Versione disponibile", + updateChangesLabel: "Modifiche", + updateFilesLabel: "File", + updateOriginLabel: "Origine", + phoneRecording: "Registrazione PaM per", + phoneLeaveMessage: "Nessuno risponde: lascia il tuo messaggio dopo il segnale acustico.", + phoneLeaveMessageBusy: "È in un'altra chiamata: lascia il tuo messaggio dopo il segnale.", + phoneLeaveMessageUnreachable: "Non è stato possibile contattarlo: lascia il tuo messaggio dopo il segnale.", + phoneConnecting: "Connessione con", + phoneSendPam: "Invia PaM", + phoneCancel: "Annulla", + phoneSearchPlaceholder: "Cerca in Telefono...", + phonePamTitle: "Registrazioni audio", + phoneNew: "Nuovo", + phoneDelete: "Elimina", + phoneNoPams: "Non hai registrazioni audio.", + phoneHistoryTitle: "Cronologia", + phoneLatestTitle: "Ultime chiamate", + phoneCreateButton: "Nuova chiamata", + phoneLimitsHint: "Fino a {n} abitanti per chiamata; con più di uno è una chiamata congiunta.", + phoneErrorPub: "Non si può chiamare un pub.", + phoneJointCall: "Chiamata congiunta", + phoneWith: "Con", + phoneResult: "Esito", + phoneWhen: "Quando", + phoneDuration: "Durata", + phoneOutgoing: "In uscita", + phoneIncoming: "In arrivo", + phoneNoHistory: "Ancora nessun registro.", + phoneOutcomeEnded: "Risposta", + phoneOutcomeMissed: "Persa", + phoneOutcomeRejected: "Rifiutata", + phoneOutcomeNoanswer: "Nessuna risposta", + phoneOutcomeCancelled: "Annullata", + phoneOutcomePam: "PaM inviato", + phoneOutcomeFailed: "Non riuscita", + phoneDialAdd: "Aggiungi", + phoneDialBack: "Cancella", + phoneDialClear: "Svuota", + phoneDialPlaceholder: "Numero o Oasis ID", + phoneDialInhabitants: "Abitanti", + phoneDialRemove: "Rimuovi", + phoneOwnNumber: "Questo è il tuo numero.", + phoneSettingsDesc: "Scegli chi può chiamarti.", + phoneDnd: "Non disturbare", + phoneOpen: "Aperto", + phoneRecipientRefuses: "L'abitante che stai cercando di chiamare non accetta chiamate.", + phoneRefusedNotice: "Questi abitanti non accettano chiamate e non sono stati chiamati:", + fileEmptyError: "Il file è vuoto. Scegli un altro file.", + fileTitleLabel: "Titolo", + fileTitlePlaceholder: "Titolo", + fileDescriptionLabel: "Descrizione", + fileDescriptionPlaceholder: "Descrizione", + fileTagsLabel: "Tag", + fileTagsPlaceholder: "Inserisci tag separati da virgole", + fileSizeLabel: "Dimensione", + fileDownloadButton: "SCARICA", + fileDetailsButton: "DETTAGLI", + fileNoFile: "Nessun file file", + fileSearchPlaceholder: "Cerca in File...", + fileSearchButton: "Cerca", + fileSortRecent: "Più recenti", + fileSortOldest: "Più vecchi", + fileSortTop: "Più votati", + fileNoMatch: "Nessun file corrispondente.", + fileUpdatedAt: "Aggiornato", + typeFile: "FILE", + modulesFilesLabel: "File", + modulesFilesDescription: "Modulo per scoprire e gestire gli file.", + tribeSectionFiles: "FILE", + fileDescription: "Descrizione", + fileDownload: "Scarica", + fileTypeLabel: "Tipo", torrentsTitle: "Torrent", torrentsDescription: "Esplora e gestisci i torrent nella tua rete.", torrentAllSectionTitle: "Torrent", @@ -3456,47 +4434,91 @@ module.exports = { torrentTagsLabel: "Tag", torrentTagsPlaceholder: "Inserisci tag separati da virgole", torrentSizeLabel: "Dimensione", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "Genera torrent", + torrentFromContentMissing: "Il file non è ancora disponibile su questo nodo. Riprova tra un po'.", + torrentDownloads: "Download", + torrentFetchOasis: "Scarica in Oasis", + torrentSaveFile: "Salva file", + torrentSeeds: "seed", + seedsLabel: "Seed", + seedAction: "Supporta", + torrentSourceFile: "File", + torrentStateQueued: "In coda", + torrentStateProbing: "Ricerca seed", + torrentStateDownloading: "Scaricamento", + torrentStateDone: "Completato", + torrentStateFailed: "Fallito", + torrentStateNoSource: "Nessun seed connesso, in attesa", + torrentStateCancelled: "Annullato", + torrentNoDownloads: "Nessun download per ora.", + torrentOriginalSection: "Originale", + torrentCancel: "Annulla", + torrentDownloadButton: "SCARICA", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "DETTAGLI", torrentNoFile: "Nessun file torrent", noTorrents: "Nessun torrent trovato.", - torrentSearchPlaceholder: "Cerca titolo, tag, autore...", + noFiles: "Nessun file trovato.", + torrentSearchPlaceholder: "Cerca in Torrent...", torrentSearchButton: "Cerca", torrentSortRecent: "Più recenti", torrentSortOldest: "Più vecchi", torrentSortTop: "Più votati", torrentNoMatch: "Nessun torrent corrispondente.", - torrentMessageAuthorButton: "Invia Messaggio all'Autore", torrentUpdatedAt: "Aggiornato", statsTorrent: "Torrent", + statsFile: "File", typeTorrent: "TORRENT", modulesTorrentsLabel: "Torrent", modulesTorrentsDescription: "Modulo per scoprire e gestire i torrent.", favoritesFilterTorrents: "TORRENT", + favoritesFilterFiles: "FILE", favoritesFilterMarket: "MERCATO", + favoritesFilterEvents: "EVENTI", + favoritesFilterForum: "FORUM", + favoritesFilterHousing: "ALLOGGI", + favoritesFilterJobs: "LAVORI", + favoritesFilterProjects: "PROGETTI", + favoritesFilterReports: "RAPPORTI", + favoritesFilterTasks: "COMPITI", + favoritesFilterTransfers: "TRASFERIMENTI", + favoritesFilterVotes: "VOTAZIONI", favoritesFilterShopProducts: "ARTICOLI NEGOZIO", tribeSectionTorrents: "TORRENT", tribeCreateTorrent: "Carica Torrent", - tribeMediaTypeTorrent: "Torrent", + tribeCreateFile: "Carica file", settingsWishTitle: "Desiderio", - settingsWishDesc: "Configura il desiderio del tuo Avatar.", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "Determina come accedi a tutti i contenuti e la tua esperienza nella rete Oasis.", + settingsWishWhole: "Multiverso", + settingsWishMutuals: "Solo supporto reciproco", + settingsWishOnlyLan: "Solo LAN", + settingsWishLocal: "Locale", settingsPmVisibilityTitle: "Messaggi Privati", settingsPmVisibilityDesc: "Configura il livello di esposizione ai messaggi privati.", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "Multiverso", + settingsPmVisibilityMutuals: "Solo supporto reciproco", + mute: "Silenzia", + unmute: "Riattiva", + relationshipMuted: "Silenziato da te", + hideContent: "Nascondi per me", + unhideContent: "Mostra di nuovo", + hiddenByYou: "Nascosto da te", + settingsTabContent: "Contenuti", + settingsHiddenTitle: "Contenuti nascosti", + settingsHiddenDesc: "Abitanti silenziati e contenuti nascosti.", + settingsMutedLabel: "Abitanti silenziati", + settingsHiddenLabel: "Contenuti nascosti", + settingsMutedEmpty: "Nessuno silenziato.", + settingsHiddenEmpty: "Niente di nascosto.", + settingsPmVisibilityFollowing: "Solo persone che seguo", + settingsMentionsVisibilityTitle: "Menzioni", + settingsMentionsVisibilityDesc: "Scegli da chi ricevere le menzioni.", pmMutualNotice: "L'invio è un guardrail UX. La ricezione è un filtro di attenzione.", - pmBlockedNonMutual: "Puoi inviare MP solo ad abitanti in supporto reciproco.", inhabitantsPendingFollowsTitle: "Richieste di supporto in attesa", inhabitantsPendingAccept: "Accetta", inhabitantsPendingReject: "Rifiuta", - bxEncrypted: "CRITTOGRAFATO", - bxEncryptedHexLabel: "Testo cifrato (anteprima)", tribeSectionGovernance: "GOVERNANCE", - tribeSubStatusPublic: "PUBBLICA", - tribeSubStatusPrivate: "PRIVATA", - tribeSubInheritedPrivate: "PRIVATA (ereditata dalla tribù principale)", tribePadInviteRequired: "Non hai accesso al pad. Richiedi un invito.", tribeChatInviteRequired: "Non hai accesso alla chat. Richiedi un invito.", tribeGovernanceDesc: "Governance interna di questa tribù.", @@ -3535,130 +4557,118 @@ module.exports = { tribeGovNoRevocations: "Ancora nessuna revoca", tribeGovNoHistorical: "Ancora nessun record", logsTitle: "Diario", - logsDescription: "Registra la tua esperienza nella rete.", - logsReadMore: "Leggi di più", + logsDescription: "Tieni un diario privato delle tue esperienze.", logsViewTitle: "Diario", logsColumnType: "Tipo", logsView: "Vedi", - logsManualPrompt: "Scrivi il tuo diario", logsUpdateButton: "Aggiorna", - logsEditTitle: "Modifica voce", - logsCreateDescription: "Registra la tua esperienza...", + logsEditTitle: "Aggiorna registro", logsCreateTitle: "Crea voce", logsWriteButton: "Scrivi", logsTextPlaceholder: "Descrivi le tue esperienze...", - logsTextField: "Testo", - logsLabelPlaceholder: "Etichetta...", - logsLabelField: "Scrivi il tuo diario (etichetta)", - logsAiDisabledWarn: "Attiva il modulo IA in /modules per usare le voci scritte dall'IA.", - logsAiContextValue: "Giorno di blockchain", - logsAiContext: "Contesto", - logsAiModStatus: "AImod", - logsModeApply: "Applica!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "Assistente AI", logsModeManual: "Manuale", logsModeAI: "IA", - logsColumnDelete: "Elimina", - logsColumnEdit: "Modifica", logsColumnLog: "Diario", logsColumnDate: "Data", logsDelete: "Elimina", - logsEdit: "Modifica", + logsEdit: "Aggiorna", logsFilterAlways: "SEMPRE", logsFilterYear: "ULTIMO ANNO", logsFilterMonth: "ULTIMO MESE", logsFilterWeek: "ULTIMA SETTIMANA", logsFilterToday: "OGGI", - logsFilterRecent: "RECENTI", - logsFilterAll: "TUTTI", + logsComments: "Commenti", + logsAuthorEntries: "Voci", logsCreate: "Crea voce", logsExport: "Esporta diario", - logsExportOne: "Esporta", + logsExportOne: "Esporta registro", logsViewDetails: "Vedi dettagli", logsGenerateButton: "Genera testo", - logsSearchText: "Cerca nei diari...", + logsSearchText: "Cerca in Diario...", logsSearchAnyType: "Qualsiasi tipo", logsSearchButton: "Cerca", logsEmpty: "Nessuna voce ancora.", + logsNothingNew: "Non ci sono nuove azioni da registrare.", modulesLogsLabel: "Diario", - modulesLogsDescription: "Modulo per registrare (tramite assistente IA) le tue esperienze.", + modulesLogsDescription: "Modulo per tenere un diario privato delle tue esperienze.", statsLogsTitle: "Diario", - statsLogsEntries: "Voci", typeLog: "DIARIO", - blockchainCycle: "Ciclo", larpTitle: "L.A.R.P.", larpDescription: "Uno strato di gioco di ruolo dal vivo per la sperimentazione collaborativa.", + larpNoHousesMatch: "Nessuna casa corrisponde alla tua ricerca.", + larpSearchPlaceholder: "Cerca in L.A.R.P....", larpAllHouses: "Case:", larpFilterRuling: "GOVERNA", larpFilterHouses: "CASE", larpFilterRules: "FAQ", larpRulesTitle: "FAQ del L.A.R.P.", - larpRulesEntryTitle: "Casa iniziale", larpRulesEntryText: "Ogni abitante inizia in ACADEMIA per impostazione predefinita. Da ACADEMIA, scegli una casa dal pannello \"Unisciti a una casa\": questo apre il suo test d'ingresso. Rispondi alle 10 domande e invia. Se superi la soglia (7/10), sei ammesso automaticamente in quella casa; altrimenti vieni rifiutato e rimani in ACADEMIA. In entrambi i casi il sistema registra il tuo OasisID e il ciclo del tentativo e impedisce un nuovo tentativo fino allo scadere del periodo di attesa di 30 cicli.", - larpRulesLeaveText: "I membri di qualsiasi casa possono tornare ad ACADEMIA in qualsiasi momento dalla pagina della propria casa; ciò ripristina la loro appartenenza, ma non annulla il periodo di attesa del test.", - larpRulesGovernanceTitle: "Muro di governo", larpRulesGovernanceText: "Durante il suo ciclo, la casa governante porta l'insegna \"Governa\" ed è l'unica il cui Muro è visibile pubblicamente sotto la scheda GOVERNA.", - larpRulesSignTitle: "Emblema L.A.R.P.", + larpRulesWallText: "Ogni casa ha un Muro dove scrivono i suoi membri. Il Muro di una casa è privato, tranne quello della casa al governo e quello dell'ACADEMIA, che chiunque può leggere.", larpRulesSignText: "Gli abitanti possono attivarlo (in /profile/edit > Sensori) per mostrare l'immagine della loro casa attuale come sigillo sul profilo, sulla scheda autore e abitante. Il sigillo rimanda alla pagina della casa.", larpPostsTitle: "Muro", larpPostsEmpty: "Ancora nessuna pubblicazione.", - larpPostLabel: "Nuova pubblicazione", larpPostPlaceholder: "Cosa ha da dire questa casa?", - larpPostPublic: "Pubblico (visibile a chiunque, altrimenti solo ai membri)", larpPostSubmit: "Pubblica", - larpPostMembersOnly: "Solo membri", larpCycleLabel: "Ciclo:", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", + bankRulesArchTaxNote: "La tassa ARCH riflette il costo a lungo termine di crescita e manutenzione dell'archivio della rete: più i tuoi dati sono vecchi nella rete, maggiore è la tua quota del costo di manutenzione.", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", + bankRulesChipNote: "Il chip di ogni elemento è colorato rispetto al messaggio più grande mai osservato nella rete e attenuato dal numero di abitanti attivi — una rete più grande distribuisce il carico e porta più elementi nella fascia verde.", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", + bankRulesEcoTaxNote: "La tassa ECO viene dedotta dal RBU lordo quando si calcola l'assegnazione dell'epoca. Gli ECO dedotti non vengono ridistribuiti in questa epoca — restano nel saldo del pub e alimentano il fondo dell'epoca successiva tramite i limiti reserveMin / alpha.", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", + bankRulesKarmaNote: "Il karma deriva dalle tue azioni (post, voti, partecipazione a parlamento/tribunali, ecc.) con un fattore di decadimento nel tempo, meno i grammi di carbonio generati dal tuo feed. Conta anche ciò che ricevono i tuoi contenuti: opinioni, voti, reazioni e riproduzioni positive aggiungono karma, mentre opinioni o voti negativi lo tolgono quando più abitanti concordano.", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", - gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", - statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", - statsPad: "Pads", - statsPadEntry: "Pad entries", - typeGameScore: "GAME SCORES", + bankTaxesArchTaxFootprintNote: "La tassa ARCH riflette il costo di crescita e manutenzione dell'archivio della rete. Si calcola dall'intervallo di tempo tra il tuo primo blocco pubblicato e il blocco più recente della rete — più a lungo la tua storia è rimasta nell'archivio, maggiore è la tua quota del costo di manutenzione.", + bankTaxesArchTaxNoteBold: "ridurre direttamente la tassa ARCH", + bankTaxesArchTaxNoteIntro: "La tua tassa ARCH viene dedotta dall'eccedenza che la rete genera oltre il tuo RBU; il valore di base del RBU è fissato come minimo inamovibile. La tassa non viene mai dedotta dall'importo fisso che spetta a ogni abitante come RBU. I fondi dedotti alimentano l'algoritmo di redistribuzione della ricchezza e vengono incanalati verso altri abitanti tramite le loro richieste di RBU. Gli abitanti più recenti e quelli dedicati specificamente ad attività di archiviazione, replica e manutenzione avranno probabilmente un compito dedicato a mantenere in salute l'archivio della rete per tuo conto. Oppure possono dedicarsi a ", + bankTaxesArchTaxNoteOutro: " tramite accordi di potatura, deduplicazione dei dati ridondanti e infrastrutture di manutenzione condivise, e il loro contributo continuo può essere richiesto dal consenso della rete.", + bankTaxesUserNoteChips: "Il chip della Tassa ECO mostrato accanto a ogni elemento cambia colore (verde / giallo / rosso) in base alla dimensione dell'elemento rispetto al blocco più grande mai visto nella rete, attenuato dal numero di abitanti attivi — una rete solarpunk più grande distribuisce il carico e porta più elementi nella fascia verde.", + bankTaxesUserNoteOtherParams: "La tassa ECO deriva attualmente dall'impronta di carbonio di ogni blocco, ma col tempo potrà includere altri parametri — energia spesa per la replica, archiviazione ridondante tra i peer, banda dei blob, costo computazionale della decifratura, impronta di mining delle transazioni associate o qualsiasi altro carico misurabile che la rete decida di valutare.", + gamesHallDate: "Data", + gamesNewRecord: "Nuovo record!", + indexingRefreshNote: "Questa pagina si ricarica ogni 10 secondi.", + indexingMessage: "Oasis sta sincronizzando la rete di nodi distribuita. Attendi, per favore.", + indexingTitle: "Sincronizzazione", + padsNoItems: "Nessun pad trovato.", + padVersionHistory: "Cronologia delle versioni", + padAttributionView: "Per autrice", + padVersionView: "Visualizza", + statsChat: "Chat", + statsChatMessage: "Messaggi di chat", + statsGameScore: "Giochi", + statsWiki: "Pagine wiki", + statsPad: "Pad", + statsPadEntry: "Voci dei pad", + typeGameScore: "PUNTEGGI DI GIOCO", bankTaxesLookupNote: "Inserisci un ID di blocco per cercarlo nel tuo feed locale.", bankTaxesUserNoteChipsClick: "Clicca su qualsiasi chip per ispezionare il suo blocco nel blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "Codice d'invito", larpRulesInviteEntryText: "I membri delle case possono generare codici di invito che danno accesso diretto alla casa.", larpRulesOneHouseText: "Puoi appartenere al massimo a una casa in ogni momento. Se non appartieni a nessuna, sei in ACADEMIA. Ogni pagina di casa (non ACADEMIA) mostra ai suoi membri un pulsante \"Lascia la Casa\" che reimposta l'appartenenza ad ACADEMIA. Uscire NON annulla il periodo di attesa del test.", - larpRulesOneHouseTitle: "Una casa alla volta", - larpRulesTestEntry: "Test WILL", - larpRulesTestEntryText: "Ogni opzione pondera una o più case; all'invio, il sistema somma i punteggi e ti ammette automaticamente nella casa con il punteggio più alto.", + larpRulesTestEntryText: "Nel test WILL ogni opzione pondera una o più case; all'invio il sistema somma i punteggi e ti ammette automaticamente nella casa con il punteggio più alto.", larpWillTestHint: "Una volta completato, ti verrà assegnata la casa che meglio corrisponde alle tue risposte. Le tue risposte individuali vengono elaborate localmente e mai memorizzate — solo l'assegnazione di casa risultante viene pubblicata nel tuo feed.", larpWillTestTitle: "Test WILL", - settingsLanDesc: "Annuncia periodicamente questo peer ad altre istanze di Oasis sulla stessa rete locale. Disabilita per interrompere le trasmissioni UDP.", + settingsLanDesc: "Annuncia periodicamente questo peer ad altre istanze di Oasis sulla stessa rete locale.", settingsLanEnable: "Abilita trasmissione LAN", settingsLanTitle: "Trasmissione LAN", settingsReplicationTitle: "Replicazione", settingsReplicationDesc: "Configura il numero di salti che il tuo peer segue dal tuo feed durante la replica dei contenuti.", settingsReplicationHopsLabel: "Salti", + settingsBlobCacheTitle: "Cache dei media", + settingsBlobCacheDesc: "I contenuti degli altri abitanti vengono conservati su questo dispositivo. Scegli quanto spazio possono occupare.", + settingsBlobCacheUsage: "Uso attuale", + settingsBlobCacheFiles: "file", + settingsBlobCacheLimit: "Limite", + settingsBlobCacheUnlimited: "Illimitato", + settingsBlobCacheCleanNow: "Pulisci ora", + settingsBlobCacheCleaned: "{n} file rimossi, {size} liberati.", aiApproveTagsLabel: "Tag (separati da virgole)", aiApproveTagsPlaceholder: "es. oasis, governance, ecologia", aiApproveRatingLabel: "Valutazione", @@ -3669,36 +4679,28 @@ module.exports = { aiExchangeMarkHelpful: "+1 utile", larpCyclesUntilRuling: "Prossimo ciclo di governo", larpVisitTribe: "Visita la tribù", + larpStartJourney: "Inizia il viaggio", larpGoverning: "Governa:", + larpStartHere: "Inizia qui:", larpMyHouse: "La mia Casa:", larpMembersCount: "Membri", - larpMembersTitle: "Membri", - larpNoMembers: "Ancora nessun membro.", larpRolesLabel: "Ruoli", larpFunctionLabel: "Funzione", larpMonthLabel: "Ciclo di governo", larpLeaveToAcademia: "Torna ad ACADEMIA", - larpAcademiaJoinTitle: "Unisciti a una casa", - larpAcademiaJoinHint: "Sostieni il test psicologico di profilo per essere assegnato alla casa che meglio si adatta a te, oppure usa un codice di invito condiviso da un membro di una casa.", - larpTakeTestButton: "Sostieni il test di profilo", + larpAcademiaJoinTitle: "Case L.A.R.P.", larpInviteRedeem: "Unisciti alla Casa", larpInviteCreate: "Genera codice di invito", larpInvitePlaceholder: "Codice di invito", larpInviteBannerTitle: "Nuovo codice di invito", larpInviteBannerHint: "Condividi questo codice con qualcuno in ACADEMIA. Scade tra 30 cicli o dopo un singolo uso.", - larpTestAssignedHouse: "Casa assegnata", larpTestRankingTitle: "Punteggio per casa", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "Codice di invito di casa", - invitesHouseJoinButton: "Unisciti alla Casa", - ecoTaxLabel: "ECO Tax", - ecoinTaxLabel: "ECOin Tax", + ecoTaxLabel: "Tassa ECO", bankTaxes: "Tasse", - bankOverviewYourTaxes: "Le tue tasse", - bankTaxesNetworkTitle: "Tasse della rete", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesUserTitle: "Le tue tasse", + bankOverviewYourTaxes: "Imposte totali", + bankTaxesEcoTaxTitle: "Tassa ECO", + bankTaxesArchTaxTitle: "Tassa ARCH", bankTaxesUserAmount: "La tua ECO Tax (prezzo da restituire)", bankTaxesArchTaxUserAmount: "La tua ARCH Tax (prezzo da restituire)", bankTaxesArchTaxFirstBlock: "Età del tuo primo blocco", @@ -3726,7 +4728,7 @@ module.exports = { bankTaxesLookupType: "Tipo", bankTaxesLookupSize: "Dimensione", bankTaxesLookupCarbon: "Impronta di carbonio", - bankTaxesLookupEcoin: "ECO Tax", + bankTaxesLookupEcoin: "Tassa ECO", bankExchangeEcoTaxAnnual: "ECOin Taxes (annuale)", bankExchangeEcoTaxMonthly: "ECOin Taxes (mensile)", bankTaxesTypesLabel: "Seleziona quali tasse vuoi pagare", @@ -3734,14 +4736,14 @@ module.exports = { bankRulesPoolTitle: "Pool mensile", bankRulesShareTitle: "Quota per utente", bankRulesKarmaTitle: "Karma (punteggio di partecipazione)", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesArchTaxTitle: "ARCH Tax", + bankRulesEcoTaxTitle: "Tassa ECO", + bankRulesArchTaxTitle: "Tassa ARCH", bankRulesChipTitle: "Banda di colore del chip ECO Tax", bankRulesEpochKind: "Granularità dell'epoch", bankRulesAlpha: "Alpha (quota massima del saldo del pub per epoch)", bankRulesReserveMin: "Riserva minima (mantenuta nel saldo del pub)", bankRulesCapPerEpoch: "Tetto per epoch (assoluto)", - bankRulesGraceDays: "Periodo di grazia del claim", + bankRulesGraceDays: "Periodo di grazia della richiesta", bankRulesWMin: "Peso minimo (w_min)", bankRulesWMax: "Peso massimo (w_max)", bankRulesFloor: "Pavimento per utente (lordo)", @@ -3749,29 +4751,22 @@ module.exports = { bankRulesEcoTaxRate: "Aliquota", bankRulesArchTaxRate: "Aliquota", bankRulesCarbonFactor: "Fattore di carbonio", - statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Ancora nessun tombstone nella rete.", blockchainBlockNotAvailable: "Questo blocco non è disponibile nel tuo feed locale. Potrebbe appartenere a un peer da cui non stai ancora replicando.", blockchainBackToBlockexplorer: "Torna al blockexplorer", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeTitle: "BCS Transcode", - audioTranscodeDetailTitle: "Transcode", + audioTranscodeButton: "TRANSCODIFICA", + audioTranscodeDetailTitle: "Transcodifica", audioTranscodeDetailDescription: "Decodifica il payload incorporato e la mappa di composizione originale della blockchain.", - audioTranscodeStegoTitle: "Incorporato nella forma d'onda", - audioTranscodeStegoOasisId: "Da", audioTranscodeStegoTimestamp: "Generato il", - audioTranscodeStegoMessage: "TEXT", + audioTranscodeStegoMessage: "TESTO", audioTranscodeStegoEmpty: "(nessuno)", audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "Nessun payload steganografico è stato decodificato da questo audio.", audioTranscodeCompositionEmpty: "Questo audio non include una composizione blockchain salvata.", - audioBackToAudio: "Torna all'audio", audioBackToBcs: "Torna a BCS", - audioAuthorLabel: "Autore", melodyCompositionTitle: "Mappa di composizione blockchain", melodyFilterMine: "MIO", - melodyByLabel: "Da", melodyAllEmpty: "Ancora nessuna composizione BCS da altri abitanti.", melodyNoteUnavailable: "il blocco non è nel tuo feed locale", melodyUploadBcsTitle: "Pubblica", @@ -3782,7 +4777,6 @@ module.exports = { melodyUploadBcsNameNote: "L'audio sarà pubblicato col nome auto-generato", larpLastAttemptTitle: "Il tuo ultimo tentativo", larpLastAttemptHouse: "Casa", - larpLastAttemptResult: "Risultato", larpLastAttemptWhen: "Quando", larpLastAttemptCooldown: "Prossimo tentativo in", larpTestTitle: "Test d'ingresso", @@ -3791,36 +4785,21 @@ module.exports = { larpTestCooldownActive: "Prossimo test disponibile tra", larpTestCooldownDays: "cicli", larpTestResultTitle: "Risultato del test", - larpTestPassed: "Test superato — benvenuto!", - larpTestFailed: "Test non superato", larpTestScore: "Punteggio", larpTestNextAttempt: "Puoi tentare un altro test tra 30 cicli.", larpGoToHouse: "Vai alla tua casa", larpBackToAcademia: "Torna ad ACADEMIA", - larpBackToHouses: "◀ Case", larpBadgeYou: "Tu", larpBadgeRuling: "Governa", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Modulo per lo strato di gioco di ruolo dal vivo di Oasis (le 9 Case).", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "Modulo per il livello di gioco di ruolo dal vivo.", larpProfileQ1: "Di fronte a un problema complesso, cosa fai per primo?", larpProfileQ1O1: "Parlare con altri per cercare il consenso", larpProfileQ1O2: "Costruire un prototipo da testare", larpProfileQ1O3: "Studiare la letteratura", larpProfileQ1O4: "Sabotare il sistema che lo provoca", larpProfileQ2: "Cosa dà senso al tuo lavoro quotidiano?", - larpProfileQ2O1: "Difendere le persone che amo", + larpProfileQ2O1: "Difendere gli abitanti che amo", larpProfileQ2O2: "Creare qualcosa di tangibile", larpProfileQ2O3: "Guarire o coltivare la vita", larpProfileQ2O4: "Lavorare con parole e idee", @@ -3887,6 +4866,51 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "Seleziona la modalità di navigazione UX per la tua interfaccia.", uxModeMenus: "Blocchi", + uxModeChats: "Conversazioni", + uxModeFeed: "Microblogging", + feedTrendingTitle: "Tag di Tendenza", + feedActiveTitle: "Abitanti Attivi", + cnBrandSub: "Libero · P2P · Federato", + cnSyncedPeers: "Peer sincronizzati", + cnLicense: "Licenza", + cnNotAccessible: "Il contenuto non è accessibile in questo momento.", + cnUntitled: "Senza titolo", + cnSearchPlaceholder: "Cerca…", + cnHubTitle: "HUB Clearnet", + cnHubDescription: "Contenuti pubblici condivisi dagli abitanti di questa rete Oasis.", + cnHubEmpty: "Nessun contenuto pubblico è ancora stato condiviso sulla Clearnet.", + cnPublicContent: "Contenuti", + cnTribesContent: "Tribù", + cnNetworkContent: "Rete", + cnCategoryEmpty: "Nessun contenuto in questa categoria.", + cnInhabitantEmpty: "Questo abitante non ha ancora pubblicato contenuti sulla Clearnet.", + cnPrevPage: "← Precedente", + cnNextPage: "Successiva →", + cnPageOf: "Pagina {page} di {pages}", + cnMoreInfo: "Maggiori informazioni →", + cnOpenCourse: "Corso aperto", + cnAuction: "ASTA", + cnExchange: "SCAMBIO", + cnRssDescription: "pubblicati dagli abitanti di questo HUB", + cnKindAudio: "Audio", + cnKindBlog: "Blog", + cnKindBookmark: "Segnalibro", + cnKindDocument: "Documento", + cnKindEvent: "Evento", + cnKindFeed: "Feed", + cnKindImage: "Immagine", + cnKindJob: "Lavoro", + cnKindMarket: "Mercato", + cnKindPodcast: "Podcast", + cnKindProject: "Progetto", + cnKindCourse: "Corso", + cnKindShop: "Negozio", + cnKindTorrent: "Torrent", + cnKindFile: "File", + cnKindVideo: "Video", + cnKindWiki: "Wiki", + cnKindRoom: "Stanza", + cnKindMap: "Mappa", uxModeAINav: "AI", } diff --git a/src/client/assets/translations/oasis_pt.js b/src/client/assets/translations/oasis_pt.js index 07a52ad..c3dd6d3 100644 --- a/src/client/assets/translations/oasis_pt.js +++ b/src/client/assets/translations/oasis_pt.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { pt: { - bbAdd: "Adicionar", - bbQuickActions: "Ações rápidas", - bbPickTitle: "Fixar um módulo", - bbPickDesc: "Escolhe um módulo para o espaço + da barra inferior.", - bbPickNone: "Nenhum", - bbManage: "Editar", - bbYourBar: "Sua barra", - bbRemove: "Remover", - bbFull: "Máximo atingido", - bbDone: "Concluído", - activityChangeFilter: "Mudar filtro", - emptyConnectHint: "Liga-te a um pub para sincronizar com a rede.", - emptyConnectCta: "Ligar a um pub", - menuCommunity: "Comunidade", - activityGroupCommunity: "Comunidade e governança", - activityGroupOrg: "Organização", - activityGroupComm: "Comunicação", - activityGroupEconomy: "Economia", - activityGroupMedia: "Conteúdo e média", - activityGroupOther: "Outros", - languageName: "Português", shopOrderStatusPaid: "Pagamento recebido", shopOrderStatusReceived: "Recebido", shopOrderMarkPaid: "Marcar pagamento recebido", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "Compras", shopMyOrdersDescription: "As tuas encomendas de compra.", shopMyOrdersEmpty: "Ainda não tens compras.", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PM Vendedor", shopOrderSeller: "Vendedor", - shopOrderPaymentConcept: "Pagamento", shopOrderInvoice: "Fatura", shopOrderInvoiceConcept: "Fatura", shopOrderStatusPending: "Pendente", shopOrderStatusAccepted: "Aceite", shopOrderStatusRejected: "Rejeitado", shopOrderStatusShipped: "Enviado", - shopOrderStatusDelivered: "Entregue", shopOrderAccept: "Aceitar", shopOrderReject: "Rejeitar", shopOrderMarkShipped: "Marcar enviado", - shopOrderMarkDelivered: "Marcar entregue", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "Contrato", + shopOrderPmBuyer: "PM Comprador", shopOrderContractConcept: "Encomenda de loja", shopOrdersPending: "Encomendas pendentes", all: "Todos", @@ -53,7 +28,7 @@ module.exports = { recent: "Recentes", anonymous: "Anónimo", unnamed: "Anónimo", - authorLabel: "AUTOR", + authorLabel: "Autoria", delete: "Eliminar", save: "Guardar", vote: "Votar", @@ -75,18 +50,17 @@ module.exports = { viewJson: "Ver JSON", matchScore: "Pontuação de correspondência", preferencesLabel: "Preferências", - inhabitantProfileTitle: "Perfil do habitante", + inhabitantProfileTitle: "Habitante", contentWarningLabel: "Aviso de conteúdo", invalidPost: "Conteúdo inválido", invalidPostHint: "Esta mensagem tem texto vazio/inválido.", - spreadBy: "Por", spreadChron: "Difusão", spreaded: "Difundido", - spreadMore: "mais", spreadContentUnavailable: "Conteúdo ainda não disponível (replicação pendente)", filteredByTag: "Filtrado por etiqueta", filteredByTagTitle: "Filtrado por etiqueta", feedPublishedSuccess: "Feed publicado com sucesso!", + feedMastodonFailed: "Publicado no Oasis, mas não foi possível enviá-lo para o Mastodon.", tribeFeedSent: "Mensagem enviada com sucesso!", tribeContentEncrypted: "Conteúdo encriptado", tribeTorrentsEmpty: "Ainda sem torrents.", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "Sem gravidade", calendarDeleteDate: "Eliminar data", calendarIntervalUntil: "Até", - bookmarkCategory: "Categoria", bookmarkLastVisit: "Última visita", profileClearnetBookmarksLabel: "Marcadores", - forumFilterHot: "Populares", invitesPort: "Porta", - currentlyUnrecheable: "ERRO!", - peerHostValidation: "IPv4 válido (ex. 192.168.1.100) ou nome de host (ex. pub.example.com)", + peerHostValidation: "IPv4 válido (ex. 192.168.1.100) ou nome de host (ex. pub.solarnethub.com)", peerPortValidation: "Porta 1-65535", peerKeyValidation: "Chave pública SSB ed25519 (@<44 caracteres base64>=.ed25519)", inboxJobUnsubscribedTitle: "Cancelamento da tua oferta de emprego", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "Descrição da tarefa", exportPasswordLabel: "Senha de exportação", importPasswordPlaceholder: "Introduz a palavra-passe", - searchCasesPlaceholder: "Pesquisar casos…", + searchCasesPlaceholder: "Pesquisar em Tribunais...", shopBuyDeliveryAddressPlaceholder: "Endereço de entrega", shopBuyNotesPlaceholder: "Notas", bankTaxesUserNoteBold: "reduzir diretamente o imposto ECO", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "Apoiar o caso", courtsSettlementAcceptBtn: "Aceitar acordo", courtsSupportCount: "Apoios", - courtsVerdictVoteTitle: "Votar no veredicto", - courtsVerdictVoteLabel: "Voto do veredicto", - courtsVerdictVoteAccept: "Aceitar", - courtsVerdictVoteReject: "Rejeitar", - courtsVerdictVoteSubmit: "Enviar voto", reportsSetStatus: "Definir estado", reportsStatusClosed: "FECHADO", statsAvgPerInhabitant: "Média por habitante", statsCarbonMaxAnnual: "Estimativa máx. anual", statsCarbonNetwork: "Total da rede", statsCarbonOfEstMax: "da capacidade máx. estimada", + statsCarbonYearProgress: "do ano decorrido", + statsNetworkTitle: "Rede", + statsStorageTitle: "Armazenamento", + statsEcoTaxTitle: "Taxa ECO", + statsAccountTitle: "Conta", + statsAveragesTitle: "Médias", statsCarbonOfNetwork: "do total da rede", statsCarbonUser: "A tua pegada", statsContentCountColumn: "Quantidade", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "Principais etiquetas", statsTopTypesTitle: "Principais tipos de conteúdo", statsTotalMsgs: "Total de mensagens", - feedViewDetails: "Ver detalhes", - eventFileLabel: "Anexar imagem", - taskFileLabel: "Anexar imagem", - courtsRespondentRequired: "O ID do réu é obrigatório", extended: "Multiverso", extendedDescription: [ "Quando apoias alguém, podes descarregar publicações dos habitantes que apoiam, e estas aparecem aqui, ordenadas por data.", @@ -215,47 +183,76 @@ module.exports = { ], profile: "Avatar", inhabitants: "Habitantes", - peersReplicatedFeeds: "Feeds replicados", + peersReplicatedFeeds: "Replicados", + peersTorColumn: "TOR", + peersTorYes: "Acessível por Tor", + peersTorNo: "Não acessível por Tor", + peersOasisId: "ID", + peersOasisVersion: "Versão", graphos: "Graphos", graphosDescription: "Mapa interativo da rede ao seu redor.", + graphosFederation: "Federação", + graphosFedPubs: "PUBs", + graphosFedUnreachable: "Inalcançáveis", + graphosFedLinks: "Ligações mútuas", + graphosFedIsolated: "Isolados", + graphosFedNoData: "Sem dados", + graphosFedMutual: "Mútuo", + graphosFedOneWay: "Num sentido", + graphosFedTransport: "Rede", + graphosFedWith: "Federado com", + graphosZoom: "Ampliar", + graphosFedInhabitants: "Habitantes", + graphosFedClearnet: "Internet", + graphosFedBoth: "Internet + TOR", + graphosFedEmpty: "O seu nó ainda não conhece nenhum PUB.", graphosViewingGraphOf: "A ver", graphosBackToMine: "← Minha rede", graphosShowing: "Mostrando", graphosYou: "Você", graphosTotalNodes: "Total de nós", - manualMode: "Modo manual", mentions: "Menções", mentionsDescription: [ - strong("Publicações que te @mencionam"), - ", ordenadas por data.", + strong("Todas as mensagens que te @mencionam"), + ".", ], - nextPage: "Seguinte", - previousPage: "Anterior", noMentions: "Ainda não recebeste @menções.", + mentionsSearchPlaceholder: "Pesquisar em Menções...", privateReminders: "LEMBRETES", + inboxFilterPms: "MPs", + inboxFilterNotifications: "Notificações", + inboxMarkRead: "Marcar como lido", + inboxMarkUnread: "Marcar como não lido", + inboxUnreadChip: "Não lido", + inboxMarkAllRead: "Marcar tudo como lido", + inboxDeleteShown: "Apagar tudo", + inboxArchive: "Arquivar", + inboxUnarchive: "Desarquivar", + inboxFilterArchived: "Arquivados", + inboxEarlierMessages: "mensagens anteriores", + inboxSortThread: "Por conversa", + inboxSortRecent: "Mais recentes primeiro", + inboxAllBots: "Bots", + settingsInboxBotsTitle: "Notificações do Inbox", + settingsInboxBotsDesc: "Escolhe quais bots podem avisar-te no teu Inbox. Os bots desmarcados ficam ocultos e os seus avisos não são contados.", inboxFiltersLabel: "Filtros:", inboxToggleToMutuals: "Mudar para mútuos", inboxToggleToWhole: "Mudar para todos", - privateFrom: "De", - privateTo: "Para", privateDate: "Data", pmReply: "Responder", pmReplies: "respostas", - pmNew: "novas", - pmMarkRead: "Marcar como lido", inReplyTo: "EM RESPOSTA A", + forumInThread: "NO TÓPICO", pmPreview: "Pré-visualizar", pmPreviewTitle: "Pré-visualização da mensagem", peers: "Pares", searchDescription: "Descrição", - imageSearch: "Pesquisa de imagens", searchFromLabel: "De", searchToLabel: "Até", - searchMinOpinionsLabel: "Mín opiniões", - searchInhabitantLabel: "Habitante (Oasis ID)", searchQueryLabel: "Pesquisa", - searchPerPageLabel: "Resultados por página", + searchPerPageLabel: "Resultados", settings: "Definições", + settingsDescription: "Configura o ambiente da tua rede.", continueReading: "Continuar a ler", moreComments: "mais comentários", readThread: "ler o resto da conversa", @@ -264,67 +261,46 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Toca a melodia da tua blockchain — cada bloco torna-se uma nota.', melodyEmpty: 'Ainda sem notas — a tua blockchain ainda não produziu nenhum bloco.', - melodyCompositionHelp: 'Cada bloco da tua blockchain torna-se uma nota musical segundo o seu tipo e tamanho.', - melodyStatsTitle: 'Distribuição por tipo', - melodyInhabitantLabel: 'Habitante', melodyTotalBlocks: 'Notas', - melodyType: 'Tipo', - melodyCount: 'Blocos', melodyFilterAll: 'TODAS', - melodyFilterShare: 'Carregar melodia', - melodyShareTitle: 'Partilha a tua melodia', - melodyShareHelp: 'Publica um instantâneo da tua melodia atual para que outros a oiçam e remixem.', - melodyShareTitleLabel: 'Título (opcional)', - melodyShareTitlePlaceholder: 'Um fantasma de blockchain', - melodyShareSubmit: 'Publicar melodia', - melodyNoShared: 'Ainda sem melodias de blockchain.', melodyRegenerate: 'Regenerar', melodyDownload: "Descarregar BCS", - profileActivityTitle: 'Atividade', - profileActivityMore: 'Ver toda a atividade', profileContentSectionTitle: 'Conteúdo do Avatar', profileContentHelp: 'Escolhe quais módulos serão exibidos no teu avatar.', profileSensorsHelp: 'Métricas opcionais exibidas no teu avatar.', profileClearnetHelp: 'Módulos acessíveis a partir de fora do Oasis.', profileHubAll: 'Tudo', - profileHubTitle: 'Conteúdo Público', - footerPulseTitle: 'Pulso', - footerPulsePeers: 'Pares', - footerPulseInbox: 'Caixa', - footerPulseSync: 'Última sinc.', - footerPulseEcoValue: 'ECO/h', - footerPulseLastActivity: 'Última atividade', footerLicenseLabel: 'Licença', - profileSwitchToOasis: 'Voltar para Oasis', - profileSwitchToClearnet: 'Mergulha na Clearnet', - profileVisibilityFediverse: "Fediverse", - profileSwitchToFediverse: "Mergulhar no fediverso", - coordLabel: 'Coordenada (ex.: A3)', - coordPlaceholder: 'Introduz coordenada', + profileVisibilityFediverse: "Multiverso", contributorsTitle: "Contribuidores", - pixeliaBy: "por", colorLabel: 'Escolhe uma cor', paintButton: 'Pintar!', - invalidCoordinate: 'Coordenada incorreta', - goToMuralButton: "Ver mural", totalPixels: 'Total de pixéis', modules: "Módulos", modulesViewTitle: "Módulos", modulesViewDescription: "Configura o teu ambiente ativando ou desativando módulos.", inbox: "Caixa de entrada", + inboxSearchPlaceholder: "Pesquisar em Caixa de entrada...", multiverse: "Multiverso", - fediverse: "Fediverse", - fediverseDescription: "Faça a gestão das suas outras contas do fediverso, incluindo enviar e receber conteúdo.", + fediverse: "Multiverso", + fediverseDescription: "Faça a gestão das suas outras contas do multiverso, incluindo enviar e receber conteúdo.", fediverseStatus: "Estado", - fediverseDisconnected: "Fediverso desligado. Verifique %LINK% ou o estado da ligação.", - fediverseSettingsTitle: "Fediverse", + fediverseDisconnected: "Multiverso desligado. Verifique %LINK% ou o estado da ligação.", + fediverseSettingsTitle: "Multiverso", fediverseInstanceLabel: "Endereço", fediverseTokenLabel: "Token de acesso", - fediverseTokenHelp: "Defina o seu token de acesso. Será usado para gerir a sua conta de Mastodon a partir do Oasis.", fediverseConnect: "Ligar", fediverseConnectedAs: "Conectado como", + nextcloudServerLabel: "Servidor", + nextcloudUserLabel: "Utilizador", + nextcloudFolderLabel: "Pasta", + nextcloudUploadButton: "Enviar", + nextcloudFileLabel: "Escolhe um ficheiro", + fileExternalLink: "Ligação externa", + nextcloudErrConnect: "Não foi possível chegar ao Nextcloud com estes dados.", + nextcloudErrUpload: "O envio para o Nextcloud falhou.", + nextcloudErrShare: "O Nextcloud não deu uma ligação pública para o ficheiro.", fediverseDisconnect: "Desconectar", - fediverseEmpty: "Quando ligar outras contas do fediverso nas %LINK%, poderá geri-las a partir daqui.", fediverseEmptyLink: "suas definições", fediverseComposePlaceholder: "No que está pensando?", fediverseReplyPlaceholder: "Escreva a sua resposta…", @@ -333,21 +309,15 @@ module.exports = { fediverseReply: "Responder", fediverseBoosted: "impulsionou", fediverseNoPosts: "A sua timeline está vazia.", - fediverseThread: "Tópico", fediverseTimeline: "Cronologias", - fediverseManageTimeline: "Gerir cronologia", fediverseFollowers: "Seguidores", fediverseFollowing: "A seguir", fediversePosts: "Publicações", fediverseJoined: "Aderiu", - fediverseOverview: "Resumo", fediverseRefresh: "Atualizar", fediverseWrite: "Escrever", fediversePreview: "Pré-visualizar", - fediverseEdit: "Editar", - fediverseOpenFeed: "Ver feed", fediverseManage: "Gerir", - fediverseStatusNotConnected: "Não conectado", fediverseError: "Não foi possível contactar o Mastodon.", fediverseErrInstance: "URL de instância inválido.", fediverseErrAuth: "Token inválido ou expirado.", @@ -358,17 +328,76 @@ module.exports = { fediverseErrPost: "Não foi possível publicar.", fediverseErrMedia: "Não foi possível enviar o ficheiro.", fediverseErrEmpty: "Escreva algo ou anexe um ficheiro.", - popularLabel: "Destaques", - topicsLabel: "Tópicos", - latestLabel: "Mais recentes", - summariesLabel: "Resumos", - threadsLabel: "Conversas", - multiverseLabel: "Multiverso", - inboxLabel: "Caixa de entrada", - invitesLabel: "Convites", - walletLabel: "Carteira", - legacyLabel: "Chaves", - cipherLabel: "Encriptação", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Número de telefone", + telegramSendCode: "Enviar código", + telegramCodeHelp: "Introduz o código que o Telegram te acabou de enviar (app ou SMS).", + telegramCodeLabel: "Código", + telegramPasswordHelp: "Esta conta tem verificação em dois passos. Introduz a tua palavra-passe.", + telegramPasswordLabel: "Palavra-passe", + telegramVerify: "Verificar", + telegramCancel: "Cancelar", + telegramChats: "Conversas", + telegramUnread: "Não lidas", + telegramOpen: "Abrir", + telegramSend: "Enviar", + telegramYou: "Tu", + telegramNoChats: "Ainda não há conversas.", + telegramNoMessages: "Ainda não há mensagens.", + telegramComposePlaceholder: "Escreve uma mensagem…", + telegramChannel: "Canal", + telegramGroup: "Grupo", + telegramPrivate: "Privado", + telegramErrConnect: "Não foi possível contactar o Telegram.", + telegramErrTimeout: "O Telegram demorou demasiado a responder.", + telegramErrAuth: "Sessão do Telegram inválida ou expirada. Liga de novo.", + telegramErrFetch: "Não foi possível carregar as conversas.", + telegramErrSend: "Não foi possível enviar a mensagem.", + telegramErrMissing: "São necessários API ID, API hash e número de telefone.", + telegramErrCode: "Código inválido. Tenta de novo.", + telegramErrCodeExpired: "O código expirou. Começa de novo.", + telegramErrPassword: "Palavra-passe errada.", + telegramErrPhone: "Número de telefone inválido.", + telegramErrApi: "API ID ou API hash inválidos.", + telegramErrFlood: "Demasiadas tentativas. Espera um pouco e tenta de novo.", + telegramErrNoLogin: "Nenhum início de sessão em curso. Começa de novo.", + peertubeUsernameLabel: "Utilizadora", + peertubePasswordLabel: "Palavra-passe", + peertubeSubscriptions: "Subscrições", + peertubeMyVideos: "Os meus vídeos", + peertubeVideos: "Vídeos", + peertubeFollowers: "Seguidoras", + peertubeViews: "visualizações", + peertubeLikes: "gostos", + peertubeQuota: "Quota", + peertubeUpload: "Carregar vídeo", + peertubeUploadHelp: "Escolhe um ficheiro de vídeo. É enviado para o teu canal e o PeerTube processa-o depois.", + peertubeTitleLabel: "Título", + peertubeDescriptionLabel: "Descrição", + peertubePrivacyLabel: "Privacidade", + peertubePrivacyPublic: "Público", + peertubePrivacyUnlisted: "Não listado", + peertubePrivacyPrivate: "Privado", + peertubeNoVideos: "Ainda não há vídeos.", + peertubeComments: "Comentários", + peertubeComment: "Comentar", + peertubeCommentPlaceholder: "Escreve um comentário…", + peertubeNoComments: "Ainda não há comentários.", + peertubeOpen: "Abrir no PeerTube", + peertubeWatch: "Ver", + peertubeChannel: "Canal", + peertubeNoStream: "Este vídeo ainda não pode ser reproduzido aqui. Abre-o no PeerTube.", + peertubeUploaded: "Vídeo enviado. O PeerTube está a processá-lo.", + peertubeErrConnect: "Não foi possível contactar o PeerTube.", + peertubeErrAuth: "Utilizadora ou palavra-passe incorretas, ou a sessão expirou. Liga de novo.", + peertubeErrInstance: "URL de instância inválido.", + peertubeErrMissing: "São necessários instância, utilizadora e palavra-passe.", + peertubeErrFetch: "Não foi possível carregar os vídeos.", + peertubeErrUpload: "Não foi possível enviar o vídeo.", + peertubeErrTitle: "Título obrigatório.", + peertubeErrComment: "Não foi possível publicar o comentário.", + peertubeErrNoFile: "Escolhe um ficheiro de vídeo.", bookmarksLabel: "Marcadores", videosLabel: "Vídeos", torrentsLabel: "Torrents", @@ -379,34 +408,22 @@ module.exports = { inhabitantsLabel: "Habitantes", trendingLabel: "Tendências", eventsLabel: "Eventos", - tasksLabel: "Tarefas", apply: "Aplicar", menuPersonal: "Pessoal", - menuContent: "Conteúdo", - menuBlogs: "Blogues", menuGovernance: "Governação", menuOffice: "Escritório", - menuMultiverse: "Multiverso", - menuNetwork: "Rede", + menuNetwork: "Comunidade", menuCreative: "Criativo", menuEconomy: "Economia", - menuMedia: "Multimédia", + menuMedia: "Biblioteca", menuTools: "Ferramentas", comment: "Comentário", subtopic: "Subtópico", json: "JSON", - createdBy: "por", unfollow: "Deixar de apoiar", follow: "Apoiar", block: "Bloquear", unblock: "Desbloquear", - newerPosts: "Publicações mais recentes", - olderPosts: "Publicações mais antigas", - feedRangeEmpty: "O intervalo indicado está vazio para este feed. Tenta ver o ", - seeFullFeed: "feed completo", - feedEmpty: "A rede Oasis nunca viu publicações desta conta.", - beginningOfFeed: "Este é o início do feed", - noNewerPosts: "Ainda não foram recebidas publicações mais recentes.", relationshipNotFollowing: "Não te apoia", relationshipTheyFollow: "Apoia-te", relationshipMutuals: "Apoio mútuo", @@ -416,25 +433,23 @@ module.exports = { relationshipBlockedBy: "Estás bloqueado", relationshipMutualBlock: "Bloqueio mútuo", relationshipNone: "Não estás a apoiar", - relationshipConflict: "Conflito", relationshipBlockingPost: "Publicação bloqueada", viewLikes: "Ver difusões", + viewSupporters: "Ver apoiantes", spreadedDescription: "Lista de publicações difundidas pelo habitante.", - totalspreads: "Total de difusões", - attachFiles: "Anexar ficheiros", preview: "Pré-visualizar", publish: "Publicar", contentWarningPlaceholder: "Adiciona um assunto à publicação (opcional)", - privateWarningPlaceholder: "Adiciona habitantes para enviar uma publicação privada (opcional)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "Escreve o corpo da tua entrada.", + publishTooLong: "A tua publicação é demasiado longa. Encurta-a, por favor.", + keepTextRetry: "Enviar de novo", + dateInPastError: "A data não pode ser anterior a hoje", publishCustomDescription: [ "LEMBRA-TE: Devido à tecnologia blockchain, uma vez publicado, não pode ser editado nem eliminado.", ], commentWarning: [ "LEMBRA-TE: Devido à tecnologia blockchain, uma vez publicado, não pode ser editado nem eliminado.", ], - commentPublic: "público", - commentPrivate: "privado", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -454,14 +469,12 @@ module.exports = { ".", ], publishCustom: "Escrever publicação avançada", - subtopicLabel: "Criar um subtópico desta publicação", messagePreview: "Pré-visualização da publicação", mentionsMatching: "Menções correspondentes", mentionsName: "Nome", mentionsRelationship: "Relacionamento", - updateit: "OBTER ATUALIZAÇÕES!", updateBannerText: "Uma nova versão do Oasis está disponível.", - updateBannerAction: "Atualizar agora →", + updateBannerAction: "Atualizar agora!", info: "Info", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -469,29 +482,94 @@ module.exports = { timeAgo: "atrás", sendTime: "há ", theme: "Tema", - legacy: "Chaves", - legacyTitle: "Chaves", - legacyDescription: "Gere o teu segredo (chave privada) de forma rápida e segura.", - legacyExportButton: "Exportar", - legacyImportButton: "Importar", - ssbLogStream: "Blokchain", + backupKeysExportButton: "Exportar", + backupKeysImportButton: "Importar", + ssbLogStream: "Blockchain", ssbLogStreamDescription: "Configura o limite de mensagens para os fluxos da Blockchain.", + logWindowChip: "Últimos {n} streams da blockchain", saveSettings: "Guardar definições", - exportTitle: "Exportar dados", exportDescription: "Define uma senha (mínimo 32 caracteres) para encriptar a tua chave", - exportDataTitle: "Cópia de segurança", + exportDataTitle: "Cópia instantânea", exportDataDescription: "Descarrega os teus dados (chave secreta excluída!)", exportDataButton: "Descarregar base de dados", - pubWallet: "Carteira PUB", - pubWalletDescription: "Define o URL da carteira PUB. Será usado para transações PUB (incluindo o UBI).", - pubWalletConfiguration: "Guardar configuração", - importTitle: "Importar dados", importDescription: "Importa o teu segredo encriptado (chave privada) para ativar o teu avatar", - importAttach: "Anexar ficheiro encriptado (.enc)", - passwordLengthInfo: "A senha deve ter pelo menos 32 caracteres.", passwordImport: "Escreve a tua senha para decifrar os dados que serão guardados no teu diretório pessoal (nome: secret)", exportPasswordPlaceholder: "Usa minúsculas, maiúsculas, números e símbolos", - fileInfo: "A tua chave secreta encriptada será guardada no teu diretório pessoal (nome: oasis.enc)", + fileInfo: "A tua chave secreta encriptada será descarregada para o teu dispositivo (nome: oasis.enc)", + developer: "Desenvolvimento", + devTitle: "Desenvolvimento", + welcomeBannerText: "O teu avatar ainda não está configurado.", + aiSuggestionBanner: "Sugestão:", + aiSuggestionsEnable: "Sugestões da IA", + welcomeBannerAction: "Começar", + welcomeTitle: "Bem-vinda", + welcomeStepIdentity: "Identidade", + welcomeStepType: "Tipo", + welcomeStepUse: "Uso", + welcomeStepLook: "Aparência", + welcomeStepCommunity: "Comunidade", + welcomeStepIntro: "Boas-vindas", + welcomeHintIdentity: "O teu nome e a tua imagem.", + welcomeHintType: "Como o Oasis se abre para ti.", + welcomeHintUse: "Que módulos ficam ativos.", + welcomeHintLook: "O tema que o Oasis veste.", + welcomeHintCommunity: "Entra num PUB para conheceres outros habitantes.", + welcomeHintIntro: "Envia uma primeira saudação à comunidade.", + welcomeStepThemeAction: "Usar este tema", + welcomeJoinPubs: "Entrar nos PUBs", + welcomeNoFederation: "Sem federação", + welcomeStepGreetingAction: "Enviar", + welcomeDescription: "Algumas coisas que vale a pena fazer antes de começar.", + welcomeSkip: "Agora não", + welcomeStepProfileAction: "Guardar perfil", + welcomeBootstrapRunning: "A carregar o instantâneo do pub. Ao terminar junta-se ao pub automaticamente; entretanto podes continuar a usar o Oasis.", + welcomeCompleteTitle: "Tudo pronto.", + welcomeCompleteText: "O teu nó está pronto. A partir daqui a rede cresce com os habitantes que apoias.", + welcomeFindPeople: "Procurar habitantes", + devFilterFiles: "FICHEIROS", + devFilterSearch: "PROCURAR", + devFilterModules: "MÓDULOS", + devFilterTranslations: "TRADUÇÕES", + devFilterStyles: "ESTILOS", + devFilterTests: "TESTES", + devVersion: "Versão", + devNodeVersion: "Node", + devStatsTests: "Testes", + devParentFolder: "Pasta superior", + devOpenFolder: "abrir", + devDescription: "Explora o código-fonte em execução neste nó.", + devRoot: "raiz", + devSearchPlaceholder: "Texto a procurar no código", + devAnyExtension: "Qualquer ficheiro", + devStatsFiles: "Ficheiros", + devStatsLines: "Linhas", + devStatsModules: "Módulos", + devStatsLanguages: "Idiomas", + devNotViewable: "não visualizável", + devEmptyFolder: "Esta pasta está vazia.", + devSize: "Tamanho", + devShowing: "A mostrar", + devLine: "linha", + devReportBug: "Reportar Bug", + devCreateTask: "Criar Tarefa", + devRaw: "RAW", + devReportContext: "Encontrado ao ler o código-fonte do Oasis", + devTaskPrefix: "Rever", + devPrevPage: "Linhas anteriores", + devNextPage: "Linhas seguintes", + devMatches: "correspondências", + devFilesScanned: "ficheiros analisados", + devNoResults: "Ainda sem correspondências.", + devColModule: "Módulo", + devColState: "Estado", + devColModel: "Modelo", + devColView: "Vista", + devColRoutes: "Rotas", + devColTests: "Testes", + devOn: "ativo", + devOff: "inativo", + modulesDevLabel: "Desenvolvimento", + modulesDevDescription: "Módulo para gerir o código-fonte do Oasis.", themeIntro: "Escolhe um tema.", setTheme: "Definir tema", @@ -499,16 +577,49 @@ module.exports = { languageDescription: "Se preferires usar outro idioma, seleciona-o aqui.", setLanguage: "Definir idioma", - peerConnections: "Pares", peerConnectionsIntro: "Gere todas as tuas ligações com outros pares.", + peersConnectFailed: "Não foi possível ligar: {reason}.", + peerStateLabel: "Estado", + peerAddressLabel: "Endereço", + peerLastChangeLabel: "Última alteração", + peerStateConnected: "Ligado", + peerStateConnecting: "A ligar", + peerStateStaged: "Disponível", + peerErrSilent: "Sem atividade", + peerErrTimeout: "Não responde", + peerErrRefused: "Recusa a ligação", + peerErrNotFound: "Endereço desconhecido", + peerErrUnreachable: "Rede inacessível", + peerErrKeys: "A usar chaves diferentes", + peerErrIdentity: "Não aceita a sua ligação", + peerErrTor: "Só TOR", + peerErrDropped: "Ligação cortada", + peerErrPaused: "Rede em pausa", + peerErrOther: "Erro de ligação", + peerOutdated: "Desatualizado", + peerVersionUnannounced: "Não anuncia a sua versão: um Oasis antigo", + peersTorMissing: "Este PUB só permite ligações através do TOR.", peerConnectionsTitle: "Ligações", + peersOnlineHint: "Nós ligados a ti neste momento.", + peersDiscoveredHint: "Nós com endereço conhecido que não estão ligados agora.", + peersUnknownHint: "Nós do teu grafo social sem endereço conhecido.", + peersConnectionsHint: "Endereços a que este nó pode ligar e o estado de cada ligação.", + peersTabLan: "LAN", + peersTabHealth: "Saúde", + peersHealthHint: "Como está cada pub ou nó conhecido, na rede local e fora dela.", + peersHealthReachable: "Alcançável", + peersHealthFailing: "A falhar", + peersHealthFailures: "Falhas", + peersFailingSince: "A falhar desde", + peersHealthForget: "Esquecer inalcançáveis", + peersLanHint: "Nós encontrados na tua rede local.", + peersLanLastSeen: "Visto pela última vez", + peersLanEmpty: "Ainda não foi encontrado nenhum nó na rede local.", online: "Online", offline: "Offline", discovered: 'Descobertos', unknown: 'Desconhecido', - lanBroadcastLabel: "Difusão LAN", lanBroadcastActive: "Ativa (multicast UDP na LAN)", - lanBroadcastDisabled: "Desativada (modo pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -521,127 +632,102 @@ module.exports = { noConnections: "Nenhum par conectado.", noDiscovered: "Nenhum par descoberto.", noUnknownPeers: "Sem pares desconhecidos no grafo de amizades.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "Ainda não há pares registados.", + peerConnect: "Ligar", + peerDisconnect: "Desligar", peerRefresh: "Atualizar", peerPruneIdle: "Remover inativos", + peersPause: "Pausar", + peersResume: "Retomar", + peersPausedNote: "Em pausa: o Oasis não se liga a ninguém e ninguém se pode ligar a ele.", + peersPauseHint: "Para todas as ligações: o Oasis não se liga a outros nós nem os aceita até retomares.", peerExport: "Exportar", peerImport: "Importar", peerImportTitle: "Importar lista de peers", peerImportPlaceholder: "Cola um endereço multiserver por linha, ou carrega um ficheiro .txt…", - noSupportedConnections: "Nenhum par apoiado.", - noBlockedConnections: "Nenhum par bloqueado.", - noRecommendedConnections: "Nenhum par recomendado.", connectionActionIntro: "", - startNetworking: "Iniciar rede", - stopNetworking: "Parar rede", - restartNetworking: "Reiniciar rede", - sync: "Sincronizar rede", indexes: "Índices", + indexesRebuilt: "Índices reconstruídos", + indexesRebuildFailed: "Não foi possível reconstruir os índices", + indexesFiles: "Ficheiros de índice", indexesDescription: "Reconstruir os teus índices é seguro e pode corrigir alguns tipos de erros.", homePageTitle: "Início", homePageDescription: "Seleciona qual módulo queres como página inicial.", saveHomePage: "Definir início", invites: "Convites", - invitesTitle: "Convites", - invitesInvites: "Convites", invitesDescription: "Gere e aplica códigos de convite na tua rede.", + invitesPubsHint: "Cola um código de convite de um pub para te federares com ele e replicar os seus habitantes.", + invitesFederationsHint: "Pubs com que estás federado: atualiza-os, remove os inacessíveis ou exporta a lista.", invitesTribesTitle: "Tribos", - invitesTribeInviteCodePlaceholder: "Introduz código de convite da tribo", - invitesTribeJoinButton: "Aderir à tribo", invitesChatsTitle: "Chats", - invitesChatInviteCodePlaceholder: "Introduz código de convite do chat", - invitesChatJoinButton: "Entrar no chat", invitesPadsTitle: "Pads", - invitesPadInviteCodePlaceholder: "Introduz código de convite do pad", - invitesPadJoinButton: "Entrar no pad", invitesCalendarsTitle: "Calendários", - invitesCalendarInviteCodePlaceholder: "Introduz código de convite do calendário", - invitesCalendarJoinButton: "Entrar no calendário", invitesEventsTitle: "Eventos", - invitesEventInviteCodePlaceholder: "Introduz código de convite do evento", - invitesEventJoinButton: "Entrar no evento", invitesForumsTitle: "Fóruns", - invitesForumInviteCodePlaceholder: "Introduz código de convite do fórum", - invitesForumJoinButton: "Entrar no fórum", invitesMapsTitle: "Mapas", - invitesMapInviteCodePlaceholder: "Introduz código de convite do mapa", - invitesMapJoinButton: "Entrar no mapa", invitesShopsTitle: "Lojas", - invitesShopInviteCodePlaceholder: "Insira o código de convite da loja", - invitesShopJoinButton: "Entrar na loja", invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "PUBs Sugeridos", + invitesFilterOthers: "Outras", + invitesJoinTitle: "Convites", + invitesJoinHint: "Cola qualquer código de convite, seja do que for, ou o Oasis ID de um habitante.", + invitesJoinPlaceholder: "Código de convite ou Oasis ID", + invitesJoinButton: "Entrar", + invitesUsedTitle: "Últimos convites", + invitesUsedEmpty: "Ainda não utilizaste nenhum convite.", + invitesUsedType: "Tipo", + invitesUsedName: "Nome", + invitesUsedDate: "Data", invitesPubInviteCodePlaceholder: "Introduz código de convite PUB", invitesAcceptInvite: "Aderir ao PUB", + invitesPubUses: "Convites usados", + invitesPubExhausted: "Este PUB não tem convites disponíveis.", invitesAlreadyFederated: "Você já está federado com este pub.", + invitesJoinPubMode: "Este Oasis funciona como PUB, em modo só de leitura. Para o juntar a outro PUB, usa o terminal: ./oasis.sh join ", + invitesJoinLocalOnly: "Os convites só podem ser aceites a partir do dispositivo onde o Oasis corre, ou com a ligação de administração que mostra ao arrancar.", invitesFederationsTitle: "Federações", invitesAcceptedInvites: "Federadas", - invitesNoInvites: "Ainda sem convites aceites.", invitesUnfollow: "Deixar de seguir", invitesFollow: "Seguir", invitesUnfollowedInvites: "Não federadas", invitesNoFederatedPubs: "Sem pubs federados.", invitesNoUnfollowed: "Sem pubs não federados.", - invitesUnreachablePubs: "Inalcançáveis", - invitesNoUnreachablePubs: "Sem pubs inalcançáveis.", invitesPubsRefresh: "Atualizar", - invitesPubsClearUnreachable: "Remover inalcançáveis", + invitesPubsClearUnreachable: "Limpar", invitesPubsExport: "Exportar", invitesPubsImport: "Importar", invitesPubsImportTitle: "Importar pubs", invitesPubsImportPlaceholder: "Cola um endereço multiserver ou código de convite por linha, ou carrega um ficheiro .txt…", - currentlyUnreachable: "ERRO!", - errorDetails: "Detalhes do erro", genericError: "Ocorreu um erro.", panicMode: "Modo pânico!", encryptData: "Define uma senha (mínimo 32 caracteres) para encriptar a tua blockchain", decryptData: "Introduz a senha para decifrar a tua blockchain", - panicModeDescription: "Encriptar/Decifrar ou ELIMINAR a tua blockchain", removeDataDescription: "AVISO: Este processo não pode ser revertido.", - encryptPanicButton: "Encriptar blockchain", - decryptPanicButton: "Decifrar blockchain", removePanicButton: "ELIMINAR TODOS OS TEUS DADOS!", searchTitle: "Pesquisa", searchDescriptionLabel: "Pesquisa conteúdo na tua rede.", - searchLanguagesLabel: "Idiomas", - searchSkillsLabel: "Competências", searchPlaceholder:"Pesquisar conteúdo...", - searchDateLabel:"Data", searchLocationLabel:"Localização", searchPriceLabel:"Preço", - searchUrlLabel:"URL", searchCategoryLabel:"Categoria", - searchStartLabel:"Hora de início", - searchEndLabel:"Hora de fim", searchPriorityLabel:"Prioridade", searchStatusLabel:"Estado", statusLabel:"Estado", - totalVotesLabel:"Total de votos", noResultsFound:"Nenhum resultado encontrado.", votesOption:"Opções de voto", voteYesLabel:"Sim", voteNoLabel:"Não", allTypesLabel: "ILIMITADO", - ABSTENTIONLabel:"ABSTENÇÃO", YESLabel:"SIM", NOLabel:"NÃO", - FOLLOW_MAJORITYLabel: "SEGUIR MAIORIA", - CONFUSEDLabel: "CONFUSO", - NOT_INTERESTEDLabel: "SEM INTERESSE", - StatusLabel:"Estado", votesOptionYesLabel:"Sim", votesOptionNoLabel:"Não", votesOptionAbstentionLabel:"Abstenção", votesQuestionLabel:"Pergunta", - votesCreatedByLabel:"Criado por", voteStatusOpen:"ABERTO", voteStatusClosed:"FECHADO", - imageSearchLabel: "Introduz palavras para pesquisar imagens etiquetadas com elas.", commentDescription: ({ parentUrl }) => [ " comentou em ", a({ href: parentUrl }, " conversa"), @@ -652,61 +738,27 @@ module.exports = { a({ href: parentUrl }, " uma publicação"), ], subtopicTitle: ({ authorName }) => [`Subtópico na publicação de @${authorName}`], - mysteryDescription: "publicou uma publicação misteriosa", oasisDescription: "Rede do Projeto OASIS", searchSubmit: "Pesquisar...", - postLabel: "PUBLICAÇÕES", - aboutLabel: "HABITANTES", - feedLabel: "FEEDS", votesLabel: "VOTAÇÕES", - reportLabel: "DENÚNCIAS", - imageLabel: "IMAGENS", - videoLabel: "VÍDEOS", - audioLabel: "ÁUDIOS", - documentLabel: "DOCUMENTOS", - torrentLabel: "TORRENTS", pdfFallbackLabel: "Documento PDF", - eventLabel: "EVENTOS", - taskLabel: "TAREFAS", - transferLabel: "TRANSFERÊNCIAS", - curriculumLabel: "CURRÍCULO", - bookmarkLabel: "MARCADORES", - tribeLabel: "TRIBOS", - marketLabel: "MERCADO", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "CARTEIRAS", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + shopLabel: "LOJAS", + voteLabel: "VOTOS", submit: "Enviar", - subjectLabel: "Assunto", editProfile: "Editar avatar", - profileQrAlt: "Código QR do perfil", - profileQrHint: "Lê para copiar este Oasis ID.", oasisIdLabel: "OasisID", - spreadLabel: "Difundir", - spreadHint: "Difunde isto aos teus apoiantes (replica pelo teu feed).", + spreadContent: "Replicar", welcomePmSubject: "Hello World!", - welcomePmBody: "Bem-vindo ao Oasis — uma rede social livre, peer-to-peer, federada e cifrada, com uma arquitetura distribuída apenas por convite.\n\nAlguns locais interessantes por onde começar:\n\n- /profile — Edita o teu avatar, nome, descrição e quais módulos aparecem no teu lado público.\n- /invites — Adiciona um pub para te federares com o resto da rede.\n- /peers — Vê quem está ligado, faz nova varredura da LAN, exporta ou importa listas de peers.\n- /inhabitants — Descobre e visita os avatares de outras pessoas.\n- /tribes — Cria ou junta-te a grupos privados com cifragem ponta-a-ponta.\n- /inbox — Lê e envia mensagens privadas.\n- /activity — Vê a atividade recente, tua e da tua rede.\n- /publish — Escreve um post ou partilha uma imagem, áudio, vídeo ou documento.\n\nAlguns lugares interessantes para conectar:\n\n- /fediverse — Faça a gestão das suas outras contas do fediverso, incluindo enviar e receber conteúdo.\n\nEsta mensagem foi enviada de forma privada de ti para ti mesmo, por isso não foi necessária qualquer ligação para a receber.", - profileVisibilityTitle: "Visibilidade do perfil público", - profileVisibilityHint: "Escolhe que campos os outros habitantes veem no teu perfil. Por defeito apenas Karma é mostrado.", + welcomePmBody: "Bem-vindo ao Oasis — uma rede social livre, peer-to-peer, federada e cifrada, com uma arquitetura distribuída apenas por convite.\n\nAlguns locais interessantes por onde começar:\n\n- [/avatar](/profile) — Edita o teu avatar, nome, descrição e quais módulos aparecem no teu lado público.\n- /settings — Ajusta a tua instância local: idioma, tema, módulos, privacidade e sincronização.\n- /invites — Adiciona um pub para te federares com o resto da rede.\n- /peers — Vê quem está ligado, faz nova varredura da LAN, exporta ou importa listas de peers.\n- /inhabitants — Descobre e visita os avatares de outros habitantes.\n- /tribes — Cria ou junta-te a grupos privados com cifragem ponta-a-ponta.\n- /search — Pesquise o conteúdo da rede por tipo.\n\nAlguns lugares interessantes para conectar:\n\n- /multiverse — Faça a gestão das suas outras contas do multiverso, incluindo enviar e receber conteúdo.\n\nEsta mensagem foi enviada de forma privada de ti para ti mesmo, por isso não foi necessária qualquer ligação para a receber.", profileSensorsSectionTitle: "Sensores", profileVisibilityActivity: "Nível de atividade", - profileVisibilityDevice: "Dispositivo", profileDeviceLockedHint: "Este sensor está sempre ativo: permite que outros vejam de que dispositivo te conectas e não pode ser desativado.", profileVisibilityKarma: "Pontuação KARMA", - profileVisibilityUbi: "UBI", - profileVisibilityWallet: "Carteira ECOIN", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityUbi: "RBU", + profileVisibilityWallet: "Carteira ECOin", + profileVisibilityEcoTax: "Taxa ECO", + profileVisibilityLarpSign: "Selo L.A.R.P.", profileVisibilityGpg: "Chave GPG", - profileVisibilityClearnet: "Publicar o meu perfil", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Lojas", profileClearnetJobsLabel: "Empregos", @@ -718,6 +770,10 @@ module.exports = { profileClearnetImagesLabel: "Imagens", profileClearnetDocumentsLabel: "Documentos", profileClearnetTorrentsLabel: "Torrents", + profileClearnetPodcastsLabel: "Podcasts", + profileClearnetMarketLabel: "Mercado", + profileClearnetFeedLabel: "Feed", + profileClearnetWikiLabel: "Wikis", editProfileDescription: "", profileName: "Nome", @@ -743,7 +799,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "Conf.", walletConfirm: "Confirmar", - walletDescription: "Gere os teus ativos digitais, incluindo enviar e receber ECOin, ver o teu saldo e aceder ao teu histórico de transações.", + walletDescription: "Gere a tua carteira ECOin.", walletDate: "Data", walletFee: "Taxa (quanto maior a taxa, mais rápido a tua transação será processada)", walletFeeLine: ({ fee }) => `Taxa: ECO ${fee}`, @@ -751,6 +807,86 @@ module.exports = { walletReceive: "Receber", walletReset: "Repor", walletSend: "Enviar", + walletPayingTransfer: "A pagar transfer", + walletCreateTransferLabel: "Gerar contrato (transferência)", + walletTransferNotifiedNote: "A outra parte foi avisada deste pagamento", + walletTransferCreatedNote: "Foi criado um transfer (recibo) deste pagamento", + walletTransferNoPayeeNote: "Nenhum habitante publicou este endereço, por isso não se criou transfer", + walletTransferReceiptConcept: "Pagamento a partir da carteira", + walletPaymentPmText: "enviou-te um pagamento", + walletPayeeLabel: "Destinatário", + walletConceptLabel: "Conceito", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Recebeste o teu RBU.", + bankingBotPaymentTitle: "Recebeste um pagamento.", + bankingBotUbiPaidText: "O teu Rendimento Básico Universal foi pago", + bankUbiPubThisNode: "Este nó paga o RBU", + bankUbiPubConnected: "PUB do RBU", + bankUbiPubNone: "Nenhum PUB anuncia ainda RBU na tua rede", + bankUbiAddressPublished: "Endereço ECOin publicado", + bankUbiAddressMissing: "Publica o teu endereço ECOin para receber o RBU", + bankUbiClaimedThisMonth: "Reclamado este mês", + bankUbiPubBalance: "Carteira do PUB disponível para o RBU", + bankNoPubAddress: "O PUB do RBU ainda não publicou um endereço ECOin.", + bankUbiTab: "RBU", + bankDonateEco: "DOAR!", + donateTooltip: "Doar ECOin", + bankNoUserAddress: "Este habitante não partilha um endereço ECOin.", + bankUbiPubDefault: "Predefinido", + bankUbiPool: "Dote", + bankUbiLastSeen: "Visto pela última vez", + bankUbiLastPayout: "Última ação", + bankUbiPaidOut: "Distribuído", + bankRangeToday: "Hoje", + bankRangeWeek: "Semana", + bankRangeMonth: "Mês", + bankRangeYear: "Ano", + bankRangeFiveYears: "5 anos", + bankRangeAll: "Tudo", + bankChartValueTitle: "Evolução do valor de ECOin", + bankChartValueHours: "Equivalência em tempo (h)", + bankChartWealthTitle: "Redistribuição da riqueza ECOin", + bankChartDistributed: "RBU distribuído (ECO)", + bankChartRebalanceTitle: "Redistribuição da riqueza entre PUBs", + bankChartRebalanced: "ECO movidos entre PUBs", + bankChartTaxes: "Impostos retidos (ECO)", + bankChartNoDataRange: "Sem dados neste intervalo.", + bankChartNoDataYet: "Ainda sem dados", + bankChartPerHour: "por hora", + bankChartPerDay: "por dia", + bankChartPerMonth: "por mês", + bankTotalUbiDistributed: "RBU total redistribuído (rede)", + bankTotalTaxesWithheld: "Impostos totais retidos (rede)", + bankInflationIssuance: "Taxa de emissão (anual, pela recompensa de bloco)", + bankRulesEligibilityTitle: "Quem pode receber o RBU", + bankRulesEligibility1: "Um endereço ECOin publicado no perfil (Perfil → Sensores → Carteira ECOin).", + bankRulesEligibility2: "Um feed com mais de 30 dias e atividade na rede: karma acima de zero.", + bankRulesEligibility3: "Uma reclamação por mês (epoch), em Banking com o botão Reclamar RBU. O mês em curso pode recusar-se com Recusar RBU.", + bankRulesEligibility4: "O mínimo de 1 ECO paga-se sempre; os impostos só reduzem a parte acima dele e nunca se cobram à parte.", + bankRulesEligibility5: "O PUB paga no ciclo seguinte e publica um transfer RBU que podes confirmar.", + bankRulesEligibility6: "Se falta o endereço, o feed é demasiado novo, recusaste o mês ou o PUB não tem fundos, a reclamação é ignorada e registada no PUB.", + copyLinkTitle: "Copiar ligação", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "Recusar RBU", + bankUbiRefusedThisMonth: "Recusaste o RBU deste mês.", + bankingBotUbiAvailableTitle: "Podes reclamar o teu RBU.", + bankingBotUbiAvailableText: "O teu RBU deste mês está disponível", + bankingBotConfirmTitle: "Tens um smart-contract pendente de confirmação", + bankingBotConfirmText: "Smart-contracts à espera da tua confirmação", + bankChartZoomHint: "Clica para ampliar", + bankChartPoolTitle: "Dote do RBU (ao longo do tempo)", + bankChartNetworkUbiTitle: "RBU total redistribuído (rede)", + bankChartSupplyTitle: "Oferta circulante de ECOin (ao longo do tempo)", + bankChartInflationTitle: "Inflação de ECOin (ao longo do tempo)", + bankChartInflationSeries: "Taxa anualizada (%)", + bankChartPool: "Dote total (ECO)", + bankChartKarmaTitle: "KARMA Scoring (ao longo do tempo)", + bankChartFundsTitle: "Os seus fundos ECOin (ao longo do tempo)", + bankChartKarma: "Karma", + bankWalletReady: "ECOin ligado", + bankWalletNotConfigured: "ECOin desligado", + transfersPayWithWallet: "PAGAR!", walletStatus: "Estado", walletDisconnected: [ "ECOin ", @@ -760,9 +896,11 @@ module.exports = { " ou o estado da ligação.", ], walletSentToLine: ({ destination, amount }) => `Enviado ECO ${amount} para ${destination}`, - walletSettingsTitle: "Carteira", + walletNotConfigured: "A sua carteira ECOin ainda não está configurada.", walletSettingsDescription: "Integra o Oasis com a tua carteira ECOin.", - walletSettingsDocLink: "Guia de instalação ECOin", + walletSettingsMobileDescription: "Ligue o Oasis à app ECOin Wallet deste dispositivo.", + walletDownloadEcoin: "Descarregar ECOin!", + walletRpcUrl: "URL do RPC", walletStatusMessages: { invalid_amount: "Montante inválido", invalid_dest: "Endereço de destino inválido", @@ -778,41 +916,24 @@ module.exports = { walletUser: "Nome de utilizador", walletPass: "Senha", walletConfiguration: "Definir carteira", + walletDisconnectButton: "Desligar carteira", cipher: "Encriptação", cipherTitle: "Encriptação", cipherDescription: "Encripta e decifra o teu texto simetricamente (usando uma senha partilhada).", randomPassword: "Senha aleatória", - cipherEncryptTitle: "Encriptar texto", - cipherEncryptDescription: "Introduz o texto a encriptar", - cipherTextLabel: "Texto a encriptar", cipherTextPlaceholder: "Introduz o texto a encriptar...", cipherPasswordLabel: "Define uma senha (mínimo 32 caracteres) para encriptar o teu texto", cipherPasswordDecryptLabel: "Define uma senha (mínimo 32 caracteres) para decifrar o seu texto", cipherPasswordPlaceholder: "Introduz uma senha...", cipherEncryptButton: "Encriptar", - cipherDecryptTitle: "Decifrar texto", - cipherDecryptDescription: "Introduz o texto a decifrar", cipherEncryptedMessageLabel: "Texto encriptado", cipherDecryptedMessageLabel: "Texto decifrado", - cipherPasswordUsedLabel: "Senha usada para encriptar (guarda-a!)", cipherEncryptedTextPlaceholder: "Introduz o texto encriptado...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "Introduz o vetor de inicialização...", cipherDecryptButton: "Decifrar", password: "Senha", text: "Texto", encryptedText: "Texto encriptado", iv: "Vetor de inicialização (IV)", - encryptTitle: "Encriptar o teu texto", - encryptDescription: "Introduz o texto que queres encriptar e fornece uma senha.", - encryptButton: "Encriptar", - decryptTitle: "Decifrar o teu texto", - decryptDescription: "Introduz o texto encriptado e fornece a mesma senha usada para encriptar.", - decryptButton: "Decifrar", - passwordLengthError: "A senha deve ter pelo menos 32 caracteres.", - missingFieldsError: "Texto, senha ou IV não fornecidos.", - encryptionError: "Erro ao encriptar o texto.", - decryptionError: "Erro ao decifrar o texto.", bookmarkTitle: "Marcadores", bookmarkDescription: "Descobre e gere marcadores na tua rede.", bookmarkAllSectionTitle: "Marcadores", @@ -829,19 +950,16 @@ module.exports = { bookmarkFilterRecent: "RECENTES", bookmarkCreateButton: "Criar marcador", bookmarkUpdateButton: "Atualizar", - bookmarkDeleteButton: "Eliminar", bookmarkAddFavoriteButton: "Adicionar favorito", bookmarkRemoveFavoriteButton: "Remover favorito", bookmarkUrlLabel: "Link", - bookmarkUrlPlaceholder: "https://example.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Descrição", bookmarkDescriptionPlaceholder: "Opcional", bookmarkTagsLabel: "Tags", bookmarkTagsPlaceholder: "Introduz tags separadas por vírgulas", - bookmarkCategoryLabel: "Categoria", - bookmarkCategoryPlaceholder: "Opcional", bookmarkLastVisitLabel: "Última visita", - bookmarkSearchPlaceholder: "Pesquisar URL, tags, categoria, autor...", + bookmarkSearchPlaceholder: "Pesquisar em Marcadores...", bookmarkSortRecent: "Mais recentes", bookmarkSortOldest: "Mais antigos", bookmarkSortTop: "Mais votados", @@ -854,8 +972,6 @@ module.exports = { noLastVisit: "Sem última visita", videoTitle: "Vídeos", videoDescription: "Explora e gere conteúdo de vídeo na tua rede.", - videoPluginTitle: "Título", - videoPluginDescription: "Descrição", videoMineSectionTitle: "Os teus vídeos", videoCreateSectionTitle: "Carregar vídeo", videoUpdateSectionTitle: "Atualizar vídeo", @@ -882,12 +998,11 @@ module.exports = { videoDescriptionPlaceholder: "Opcional", videoNoFile: "Nenhum ficheiro de vídeo fornecido", noVideos: "Sem vídeos disponíveis.", - videoSearchPlaceholder: "Pesquisar título, tags, autor...", + videoSearchPlaceholder: "Pesquisar em Vídeos...", videoSortRecent: "Mais recentes", videoSortOldest: "Mais antigos", videoSortTop: "Mais votados", videoSearchButton: "Pesquisa", - videoMessageAuthorButton: "MP", videoUpdatedAt: "Atualizado", videoNoMatch: "Nenhum vídeo corresponde à tua pesquisa.", documentTitle: "Documentos", @@ -906,7 +1021,6 @@ module.exports = { documentFilterFavorites: "FAVORITOS", documentCreateButton: "Carregar documento", documentUpdateButton: "Atualizar", - documentDeleteButton: "Eliminar", documentAddFavoriteButton: "Adicionar favorito", documentRemoveFavoriteButton: "Remover dos favoritos", documentMessageAuthorButton: "MP", @@ -919,7 +1033,7 @@ module.exports = { documentDescriptionPlaceholder: "Opcional", documentNoFile: "Sem ficheiro.", noDocuments: "Sem documentos disponíveis.", - documentSearchPlaceholder: "Pesquisar título, tags, descrição, autor...", + documentSearchPlaceholder: "Pesquisar em Documentos...", documentSortRecent: "Mais recentes", documentSortOldest: "Mais antigos", documentSortTop: "Mais votados", @@ -928,8 +1042,6 @@ module.exports = { documentUpdatedAt: "Atualizado", audioTitle: "Áudios", audioDescription: "Explora e gere conteúdo de áudio na tua rede.", - audioPluginTitle: "Título", - audioPluginDescription: "Descrição", audioMineSectionTitle: "Os teus áudios", audioCreateSectionTitle: "Carregar áudio", audioUpdateSectionTitle: "Atualizar áudio", @@ -940,22 +1052,21 @@ module.exports = { audioFilterMine: "MEUS", audioFilterRecent: "RECENTES", audioFilterTop: "TOP", - audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Carregar áudio", audioUpdateButton: "Atualizar", - audioDeleteButton: "Eliminar", audioAddFavoriteButton: "Adicionar favorito", audioRemoveFavoriteButton: "Remover favorito", audioFileLabel: "Seleciona um ficheiro de áudio (.mp3, .wav, .ogg)", audioTagsLabel: "Tags", audioTagsPlaceholder: "Introduz tags separadas por vírgulas", + tagsPlaceholder: "Introduz tags separadas por vírgulas", audioTitleLabel: "Título", audioTitlePlaceholder: "Opcional", audioDescriptionLabel: "Descrição", audioDescriptionPlaceholder: "Opcional", audioNoFile: "Nenhum ficheiro de áudio fornecido", noAudios: "Sem áudios disponíveis.", - audioSearchPlaceholder: "Pesquisar título, tags, autor...", + audioSearchPlaceholder: "Pesquisar em Áudios...", audioSortRecent: "Mais recentes", audioSortOldest: "Mais antigos", audioSortTop: "Mais votados", @@ -967,6 +1078,7 @@ module.exports = { audioFilterFavorites: "FAVORITOS", favoritesTitle: "Favoritos", favoritesDescription: "Todos os teus conteúdos favoritos num só lugar.", + favoritesSearchPlaceholder: "Pesquisar em Favoritos...", favoritesFilterAll: "TODOS", favoritesFilterRecent: "RECENTES", favoritesFilterAudios: "ÁUDIOS", @@ -978,22 +1090,16 @@ module.exports = { favoritesFilterChats: "CHATS", favoritesFilterCalendars: "CALENDÁRIOS", favoritesFilterVideos: "VÍDEOS", - favoritesRemoveButton: "Remover dos favoritos", favoritesNoItems: "Ainda sem favoritos.", yourContacts: "Os teus contactos", allInhabitants: "Habitantes", - allCVs: "Todos os CVs", + allCVs: "CVs", discoverPeople: "Descobre habitantes na tua rede.", - allInhabitantsButton: "TODOS", - contactsButton: "APOIOS", - CVsButton: "CVs", - matchSkills: "Combinar competências", - matchSkillsButton: "COMBINAR COMPETÊNCIAS", - suggestedButton: "SUGERIDOS", - searchInhabitantsPlaceholder: "FILTRAR habitantes POR NOME …", + searchInhabitantsPlaceholder: "Pesquisar em Habitantes...", filterLocation: "FILTRAR habitantes POR LOCALIZAÇÃO …", filterLanguage: "FILTRAR habitantes POR IDIOMA …", filterSkills: "FILTRAR habitantes POR COMPETÊNCIAS …", + cvSkillsCount: "Competências", applyFilters: "Aplicar filtros", locationLabel: "Localização", languagesLabel: "Idiomas", @@ -1002,15 +1108,15 @@ module.exports = { mutualFollowers: "Seguidores mútuos", suggestedFollowsYou: "Segue-te", latestInteractions: "Interações recentes", - viewAvatar: "Ver Avatar", - viewCV: "Ver CV", suggestedSectionTitle: "Sugeridos", topkarmaSectionTitle: "Melhor Karma", topecoSectionTitle: "Melhor Eco", topactivitySectionTitle: "Top Atividade", + "TOP INACTIVITYButton": "MAIS INATIVOS", + topinactivitySectionTitle: "Habitantes menos ativos", blockedSectionTitle: "Bloqueados", gallerySectionTitle: "GALERIA", - blockedButton: "BLOQUEADOS", + topSectionTitle: "TOP", blockedLabel: "Habitante bloqueado", inhabitantviewDetails: "Ver detalhes", keepReading: "Continuar lendo...", @@ -1025,10 +1131,23 @@ module.exports = { parliamentFilterCandidatures: "CANDIDATURAS", parliamentFilterProposals: "PROPOSTAS", parliamentFilterLaws: "LEIS", + parliamentFilterRevocations: "REVOGAÇÕES", parliamentFilterHistorical: "HISTÓRICO", parliamentFilterLeaders: "LÍDERES", parliamentFilterRules: "REGRAS", parliamentGovernmentCard: "Governo atual", + parliamentSeatsGovernment: "Governo", + parliamentSeatsOpposition: "Oposição", + parliamentSeatsAbstention: "Abstenção", + parliamentSeatsQuorum: "Quórum de eleição", + parliamentSeatsPass: "Limiar de aprovação", + parliamentSeatsByElection: "Por eleição", + parliamentSeatsByHouses: "Por casa do LARP", + parliamentSeatsSeats: "assentos", + parliamentSeatsSeat: "assento", + parliamentSeatsLeader: "Líder", + parliamentSeatsVotesFor: "votos para", + parliamentSeatsNoHouse: "Sem casa", parliamentActorInPowerInhabitant: 'HABITANTE NO PODER', parliamentActorInPowerTribe: 'TRIBO NO PODER', parliamentHistoricalGovernmentsTitle: 'GOVERNOS', @@ -1050,10 +1169,8 @@ module.exports = { parliamentPoliciesDeclined: "LEIS REJEITADAS", parliamentPoliciesDiscarded: "LEIS DESCARTADAS", parliamentEfficiency: "% EFICIÊNCIA", - parliamentFilterRevocations: 'REVOGAÇÕES', parliamentRevocationFormTitle: 'Revogar lei', parliamentRevocationLaw: 'Lei', - parliamentRevocationTitle: 'Título', parliamentRevocationReasons: 'Motivos', parliamentRevocationPublish: 'Publicar revogação', parliamentCurrentRevocationsTitle: 'Revogações atuais', @@ -1066,23 +1183,12 @@ module.exports = { parliamentNoGovernments: "Ainda sem governos.", parliamentCandidatureFormTitle: "Propor candidatura", parliamentCandidatureIdPh: "Oasis ID (@...) ou nome da tribo", - parliamentCandidatureSlogan: "Slogan (máx 140 caracteres)", - parliamentCandidatureSloganPh: "Um lema curto", parliamentCandidatureMethod: "Método", parliamentCandidatureProposeBtn: "Publicar candidatura", - parliamentThDate: "Data da proposta", - parliamentThSlogan: "Slogan", - parliamentThSince: "Perfil desde", - parliamentThVotes: "Votos recebidos", - parliamentThVoteAction: "Votar", - parliamentTypeUser: "Habitante", - parliamentTypeTribe: "Tribo", parliamentVoteBtn: "Votar", parliamentProposalFormTitle: "Propor lei", parliamentProposalDescription: "Descrição (≤1000)", parliamentProposalPublish: "Publicar proposta", - parliamentFinalize: "Finalizar", - parliamentDeadline: "Prazo", parliamentNoProposals: "Ainda sem propostas de lei.", parliamentNoLaws: "Ainda sem leis aprovadas.", parliamentMethodDEMOCRACY: "Democracia", @@ -1092,7 +1198,7 @@ module.exports = { parliamentMethodKARMATOCRACY: "Karmatocracia", parliamentThId: "ID", parliamentThProposalDate: "Data da proposta", - parliamentThMethod: "Method", + parliamentThMethod: "Método", parliamentThKarma: "Karma", parliamentThSupports: "Apoios", parliamentThVote: "Votar", @@ -1100,29 +1206,22 @@ module.exports = { parliamentVotesSlashTotal: "Votos/Total", parliamentVotesNeeded: "Votos necessários", parliamentFutureLawsTitle: "Leis futuras", - parliamentNoFutureLaws: "Ainda sem leis futuras.", parliamentVoteAction: "Votar", - parliamentLeadersTitle: "Líderes", parliamentThLeader: "AVATAR", parliamentPopulation: "População", - parliamentThType: "Tipo", - parliamentThInPower: "No poder", parliamentThPresented: "CANDIDATURAS", parliamentNoLeaders: "Ainda sem líderes.", parliamentCandidaturesListTitle: "Lista de candidaturas", parliamentTimeRemaining: "Tempo restante", - parliamentNoLeader: "Ainda sem candidatura vencedora.", parliamentElectionsStatusTitle: "Próximo governo", parliamentProposalDeadlineLabel: "Prazo", + parliamentViewReport: "Ver relatório", parliamentProposalTimeLeft: "Tempo restante", - parliamentProposalOnTrack: "A caminho da aprovação", - parliamentProposalOffTrack: "Apoio insuficiente", parliamentRulesTitle: "Como funciona o Parlamento", parliamentRulesIntro: "As eleições resolvem-se a cada 2 meses; as candidaturas são contínuas e reiniciam quando um governo é escolhido.", parliamentRulesCandidates: "Qualquer habitante pode propor-se a si, a outro habitante ou a qualquer tribo. Cada habitante pode propor até 3 candidaturas por ciclo; duplicados no mesmo ciclo são rejeitados.", parliamentRulesElection: "O vencedor é a candidatura com mais votos no momento da resolução.", parliamentRulesTies: "Desempate: maior karma do habitante; se empatado, perfil mais antigo; se ainda empatado, proposta mais antiga; depois ordem lexicográfica por ID.", - parliamentRulesFallback: "Se ninguém votar, a última candidatura proposta vence. Se não houver candidaturas, uma tribo aleatória é selecionada.", parliamentRulesTerm: "O separador Governo mostra o governo atual e estatísticas.", parliamentRulesMethods: "Formas: Anarquia (maioria simples), Democracia (50%+1), Maioria (80%), Minoria (20%), Karmatocracia (propostas com maior karma), Ditadura (aprovação instantânea).", parliamentRulesAnarchy: "A Anarquia é o modo predefinido: se nenhuma candidatura for eleita na resolução, a Anarquia é proclamada. Sob Anarquia, qualquer habitante pode propor leis.", @@ -1131,6 +1230,7 @@ module.exports = { parliamentRulesLaws: "Quando uma proposta atinge o limiar, torna-se Lei e aparece no separador Leis com a data de promulgação.", parliamentRulesHistorical: "No separador Histórico podes ver cada ciclo de governo que ocorreu e dados sobre a sua gestão.", parliamentRulesLeaders: "No separador Líderes podes ver um ranking de habitantes/tribos que governaram (ou se candidataram), ordenados por eficiência.", + parliamentRulesHemicycle: "O hemiciclo atribui um assento por habitante: o bloco do governo (os eleitores da candidatura vencedora e o líder), um bloco de oposição por cada outra candidatura do ciclo, e a abstenção. As marcas tracejadas mostram o quórum de eleição (25 % do censo) e o limiar de aprovação do método em vigor. \"Por casa do LARP\" colore os mesmos assentos por casa.", parliamentProposalVoteStatusLabel: "Estado da votação", parliamentProposalOnTrackYes: "Limiar atingido", parliamentProposalOnTrackNo: "Abaixo do limiar", @@ -1144,11 +1244,8 @@ module.exports = { courtsCaseFormTitle: "Abrir caso", courtsCaseRespondent: "Acusado / Demandado", courtsCaseRespondentPh: "Oasis ID (@...) ou nome da tribo", - courtsCaseMediatorsAccuser: "Mediadores (acusação)", + courtsCaseSubjectPlaceholder: "Assunto ou descrição breve", courtsCaseMethod: "Método de resolução", - courtsCaseDescription: "Descrição (máx 1000 caracteres)", - courtsCaseEvidenceTitle: "Provas do caso", - courtsCaseEvidenceHelp: "Anexa imagens, áudios, documentos (PDF) ou vídeos que sustentem o teu caso.", courtsCaseSubmit: "Apresentar caso", courtsNominateJudge: "Nomear juiz", courtsJudgeId: "Juiz", @@ -1181,34 +1278,33 @@ module.exports = { courtsMethodDICTATOR: "Ditador", courtsMethodPOPULAR: "Popular", courtsMethodKARMATOCRACY: "Karmatocracia", + courtsCaseType: "Tipo de caso", + courtsPresetMinorConflict: "Conflito leve", + courtsPresetModerateConflict: "Conflito moderado", + courtsPresetSevereConflict: "Conflito grave", + courtsPresetHarassment: "Assédio ou abuso", + courtsPresetContentModeration: "Moderação de conteúdo", + courtsPresetBanRestriction: "Banimento ou restrição", + courtsPresetGovernanceDispute: "Disputa de governança", + courtsStatusOPEN: "Aberto", + courtsStatusIN_PROGRESS: "Em curso", + courtsStatusDECIDED: "Decidido", + courtsStatusCLOSED: "Fechado", + courtsStatusSOLVED: "Resolvido", + courtsStatusUNSOLVED: "Não resolvido", + courtsStatusDISCARDED: "Descartado", courtsRulesTitle: "Como funcionam os Tribunais", courtsRulesIntro: "Os Tribunais são um processo comunitário para resolver conflitos e promover justiça restaurativa. Diálogo, provas claras e medidas proporcionais são priorizados.", courtsRulesLifecycle: "Processo: 1) Abrir caso 2) Selecionar método 3) Apresentar provas 4) Audiência e deliberação 5) Veredicto e medida 6) Cumprimento e encerramento 7) Recurso (se aplicável).", - courtsRulesRoles: "Acusador: abre o caso. Defesa: pessoa ou tribo acusada. Método: mecanismo escolhido pela comunidade para facilitar, avaliar provas e emitir veredicto. Testemunha: fornece depoimento ou provas. Mediadores: pessoas neutras convidadas pelo acusador e/ou defesa, com acesso a todos os detalhes, que ajudam a desescalar o conflito e co-criar acordos.", + courtsRulesRoles: "Acusador: abre o caso. Defesa: habitante ou tribo acusada. Método: mecanismo escolhido pela comunidade para facilitar, avaliar provas e emitir veredicto. Testemunha: fornece depoimento ou provas. Mediadores: habitantes neutros convidados pelo acusador e/ou defesa, com acesso a todos os detalhes, que ajudam a desescalar o conflito e co-criar acordos.", courtsRulesEvidence: "Descrição até 1000 caracteres. Anexa imagens, áudio, vídeo e documentos PDF relevantes e legais. Não partilhes dados privados sensíveis sem consentimento.", courtsRulesDeliberation: "As audiências podem ser públicas ou privadas. Os juízes garantem respeito, pedem esclarecimentos e podem descartar material irrelevante ou ilegal.", courtsRulesVerdict: "A restauração é priorizada: pedidos de desculpa, acordos de mediação, moderação de conteúdo, restrições temporárias ou outras medidas proporcionais. A fundamentação deve ser registada.", courtsRulesAppeals: "Recurso: permitido quando há novas provas ou erro processual claro. Deve ser apresentado em 7 dias, salvo indicação contrária.", - courtsRulesPrivacy: "Respeita a privacidade e segurança. Doxing, ódio ou ameaças são removidos. Os juízes podem editar ou selar partes do registo para proteger pessoas em risco.", + courtsRulesPrivacy: "Respeita a privacidade e segurança. Doxing, ódio ou ameaças são removidos. Os juízes podem editar ou selar partes do registo para proteger habitantes em risco.", courtsRulesMisconduct: "Assédio, manipulação ou provas fabricadas podem levar a resolução negativa imediata.", courtsRulesGlossary: "Caso: registo de um conflito. Provas: materiais que sustentam alegações. Veredicto: decisão com medida. Recurso: pedido de revisão do veredicto.", courtsFilterActions: "AÇÕES", - courtsNoActions: "Sem ações pendentes para o teu papel.", - courtsCaseTitlePlaceholder: "Breve descrição do conflito", - courtsCaseSeverity: "Gravidade", - courtsCaseSeverityNone: "Sem etiqueta de gravidade", - courtsCaseSeverityLOW: "Baixa", - courtsCaseSeverityMEDIUM: "Média", - courtsCaseSeverityHIGH: "Alta", - courtsCaseSeverityCRITICAL: "Crítica", - courtsCaseSubject: "Tópico", - courtsCaseSubjectNone: "Sem etiqueta de tópico", - courtsCaseSubjectBEHAVIOUR: "Comportamento", - courtsCaseSubjectCONTENT: "Conteúdo", - courtsCaseSubjectGOVERNANCE: "Governação / regras", - courtsCaseSubjectFINANCIAL: "Financeiro / recursos", - courtsCaseSubjectOTHER: "Outro", - courtsHiddenRespondent: "Oculto (visível apenas para os envolvidos).", courtsThRole: "Papel", courtsRoleAccuser: "Acusador", courtsRoleDefence: "Defesa", @@ -1219,51 +1315,23 @@ module.exports = { courtsAssignJudgeBtn: "Escolher juiz", trendingTitle: "Tendências", exploreTrending: "Explora o conteúdo mais popular na tua rede.", - bookmarkButton: "MARCADORES", - transferButton: "TRANSFERÊNCIAS", - eventButton: "EVENTOS", - taskButton: "TAREFAS", votesButton: "VOTAÇÕES", - reportButton: "RELATÓRIOS", - feedButton: "FEED", - marketButton: "MERCADO", - imageButton: "IMAGENS", - audioButton: "ÁUDIOS", - videoButton: "VÍDEOS", - documentButton: "DOCUMENTOS", - torrentButton: "TORRENTS", - noTrendingFound: "Nenhum conteúdo em tendência encontrado.", - noContentMessage: "Ainda sem conteúdo em tendência disponível.", - trendingDescription: "Descrição", - trendingDate: "Data", - trendingLocation: "Localização", - trendingPrice: "Preço", - trendingUrl: "URL", - trendingCategory: "Categoria", - trendingStart: "Início", - trendingEnd: "Fim", - trendingPriority: "Prioridade", - trendingStatus: "Estado", - trendingFrom: "De", - trendingTo: "Para", - trendingConcept: "Conceito", - trendingAmount: "Montante", - trendingDeadline: "Prazo", - trendingItemStatus: "Estado do item", - trendingTotalVotes: "Total de votos", + shopProductButton: "LOJA", + podcastButton: "PODCAST", + podcastEpisodeButton: "EPISÓDIO", + campaignButton: "CAMPANHA", + logisticsRouteButton: "ROTA", trendingTotalOpinions: "Total de opiniões", - trendingNoContentMessage: "Ainda sem conteúdo em tendência disponível.", - trendingAuthor: "Por", - trendingCreatedAtLabel: "Criado em", + trendingNoContentMessage: "Ainda não há tendências.", trendingTotalCount: "Contagem total", tasksTitle: "Tarefas", tasksDescription: "Descobre e gere tarefas na tua rede.", + taskSearchPlaceholder: "Pesquisar em Tarefas...", taskTitleLabel: "Título", taskDescriptionLabel: "Descrição", taskStartTimeLabel: "Hora de início", taskEndTimeLabel: "Hora de fim", taskPriorityLabel: "Prioridade", - taskPrioritySelect: "Selecionar prioridade", taskPriorityUrgent: "Urgente", taskPriorityHigh: "Alta", taskPriorityMedium: "Média", @@ -1273,13 +1341,13 @@ module.exports = { taskVisibilityLabel: "Visibilidade", taskPublic: "público", taskPrivate: "privado", - taskCreatedAt: "Criado em", - taskBy: "Por", taskStatus: "Estado", taskStatusOpen: "Abrir", taskStatusInProgress: "Em progresso", taskStatusClosed: "Fechado", - taskAssignedTo: "Atribuído a", + taskAssignedTo: "Atribuído", + taskAssignedChip: "Atribuída", + taskUnassignedChip: "Por atribuir", taskAssignees: "Atribuídos", taskAssignButton: "Atribuir a mim", taskUnassignButton: "Remover", @@ -1287,12 +1355,12 @@ module.exports = { taskUpdateButton: "Atualizar", taskDeleteButton: "Eliminar", taskFilterAll: "TODOS", + taskFilterRecent: "RECENTES", taskFilterMine: "MEUS", taskFilterOpen: "ABERTO", taskFilterInProgress: "EM PROGRESSO", taskFilterClosed: "FECHADO", taskFilterAssigned: "ATRIBUÍDO", - taskFilterArchived: "ARQUIVADO", taskFilterUrgent: "URGENTE", taskFilterHigh: "ALTA", taskFilterMedium: "MÉDIA", @@ -1305,35 +1373,47 @@ module.exports = { taskInProgressTitle: "Tarefas em progresso", taskClosedTitle: "Tarefas fechadas", taskAssignedTitle: "Tarefas atribuídas", - taskArchivedTitle: "Tarefas arquivadas", - taskPublicTitle: "Tarefas públicas", - taskPrivateTitle: "Tarefas privadas", notasks: "Sem tarefas disponíveis.", noLocation: "Sem localização especificada", taskSetStatus: "Definir estado", - eventTitle: "Eventos", eventDateLabel: "Data", + eventRecurrenceLabel: "Recorrência", + eventRecurrenceUntil: "Repetir até", + eventRecurrenceHint: "Deixa a data final vazia para um evento único.", + eventUpcomingDates: "Próximas datas", eventsTitle: "Eventos", eventsDescription: "Descobre e gere eventos na tua rede.", - eventDescription: "Descrição", + eventSearchPlaceholder: "Pesquisar em Eventos...", eventPrice: "Preço", eventStatus: "Estado", - eventOrganizer: "Organizador", eventAllSectionTitle: "Eventos", eventMineSectionTitle: "Os teus eventos", - eventArchivedTitle: "Eventos arquivados", eventCreateSectionTitle: "Criar evento", eventUpdateSectionTitle: "Atualizar evento", - eventDeleteButton: "Eliminar", eventCreateButton: "Criar evento", eventAddToCalendar: "Adicionar ao calendário", eventVisitCalendar: "Ver calendário", viewTask: "Ver tarefa", + viewEvent: "Ver evento", + viewPad: "Ver pad", + viewMap: "Ver mapa", + viewCalendar: "Ver calendário", viewReport: "Ver relatório", viewTransfer: "Ver transferência", viewItem: "Ver item", viewShop: "Ver loja", viewProject: "Ver projeto", + viewJob: "Ver Emprego", + viewPlace: "Ver Lugar", + generatePdf: "Gerar PDF", + sharePm: "Partilhar por MP", + galleryImages: "Imagens (tamanho máx: 75MB cada)", + galleryPhotos: "Imagens", + galleryAddPhoto: "Adicionar Imagem", + galleryRemovePhoto: "Remover", + galleryVideo: "Vídeo (tamanho máx: 75MB)", + galleryAddVideo: "Adicionar Vídeo", + galleryRemoveVideo: "Remover vídeo", viewVotation: "Ver votação", voteCastTitle: "Emitir Voto", voteResults: "Resultados", @@ -1341,48 +1421,26 @@ module.exports = { eventDescriptionLabel: "Descrição", eventDescriptionPlaceholder: "Introduz a descrição do evento...", eventUpdateButton: "Atualizar", - eventAttendeesLabel: "Participantes", - eventAttendeesPlaceholder: "Introduz participantes, separados por vírgulas...", eventTagsLabel: "Tags", - eventTagsPlaceholder: "Introduz tags do evento, separadas por vírgulas...", - eventTags: "Tags", eventPriceLabel: "Preço", eventUrlLabel: "URL", eventAttendees: "Participantes", - noAttendees: "Ainda sem participantes", - eventCreatedAt: "Criado em", eventLocation: "Localização", eventLocationLabel: "Localização", - eventNoLocation: "Sem localização especificada", - eventNoURL: "Sem URL especificado", - eventBy: "Por", noevents: "Sem eventos disponíveis.", eventDate: "Data", - eventDateFormat: "DD:MM:AAAA HH:mm", eventAttendButton: "Participar no evento", - eventUnattendButton: "Sair do evento", - eventCreatedBy: "Criado por", - eventAttendeesCount: "Número de participantes", - eventCreatedByYou: "Criaste este evento", eventFilterAll: "TODOS", + eventFilterRecent: "RECENTES", eventFilterMine: "MEUS", eventFilterToday: "HOJE", eventFilterWeek: "ESTA SEMANA", eventFilterMonth: "ESTE MÊS", eventFilterYear: "ESTE ANO", eventFilterArchived: "ARQUIVADO", - eventTodayTitle: "Eventos de hoje", - eventThisWeekTitle: "Eventos desta semana", - eventThisMonthTitle: "Eventos deste mês", - eventThisYearTitle: "Eventos deste ano", eventPrivacyLabel: "Visibilidade", eventPublic: "Público", eventPrivate: "Privado", - eventPublicTitle: "Eventos públicos", - eventNoPrice: "Evento gratuito", - eventNoImage: "Sem imagem carregada", - eventAttendConfirmation: "You are now attending this event", - eventUnattendConfirmation: "You are no longer attending this event", eventAttended: "Presente", eventUnattended: "Ausente", eventStatusOpen: "Abrir", @@ -1393,9 +1451,14 @@ module.exports = { tagsTopSectionTitle: "Tags populares", tagsCloudSectionTitle: "Nuvem de tags", tagsFilterAll: "TODOS", + tagsFilterMine: "MINHAS", + tagsFilterRecent: "RECENTES", + tagsMineSectionTitle: "As minhas etiquetas", + tagsRecentSectionTitle: "Etiquetas recentes", tagsFilterTop: "TOP", tagsFilterCloud: "NUVEM", tagsNoItems: "Sem tags disponíveis.", + tagsSearchPlaceholder: "Pesquisar em Tags...", tagsTableHeaderTag: "TAG/LINK:", tagsTableHeaderCount: "CONTADOR:", transfersTitle: "Transferências", @@ -1403,7 +1466,10 @@ module.exports = { transfersFrom: "De", transfersTo: "Para", transfersFilterAll: "TODOS", + transfersFilterRecent: "RECENTES", transfersFilterMine: "MEUS", + transfersFilterMinePending: "PENDENTES", + transfersFilterMineConfirmed: "ASSINADOS", transfersFilterUBI: "RBU", transfersFilterMarket: "MERCADO", transfersFilterTop: "TOP", @@ -1418,7 +1484,6 @@ module.exports = { transfersCategoryEconomic: "Económica", transfersCategoryTime: "Tempo", transfersCategoryTrust: "Confiança", - transfersCategoryApply: "Aplicar", transfersUnitEco: "ECO", transfersUnitHours: "h", transfersUnitTrust: "confiança", @@ -1435,18 +1500,12 @@ module.exports = { transfersDeadline: "Prazo", transfersTags: "Tags", transfersStatus: "Estado", - transfersStatusUnconfirmed: "NÃO CONFIRMADO", - transfersStatusClosed: "FECHADO", - transfersStatusDiscarded: "DESCARTADO", - transfersCreatedAt: "Criado em", transfersConfirmations: "CONFIRMAÇÕES", - transfersContainingBlock: "Bloco contendor", - transfersExportContract: "Criar contrato", + transfersExportContract: "Gerar Fatura", transfersConfirmButton: "Confirmar transferência", transfersNoItems: "Sem transferências encontradas.", transfersMineSectionTitle: "As tuas transferências", transfersUBISectionTitle: "Transferências RBU", - transfersMarketSectionTitle: "Transferências de mercado", transfersTopSectionTitle: "Transferências mais votadas", transfersPendingSectionTitle: "Transferências pendentes", transfersUnconfirmedSectionTitle: "Transferências não confirmadas", @@ -1454,21 +1513,13 @@ module.exports = { transfersDiscardedSectionTitle: "Transferências descartadas", transfersCreateSectionTitle: "Criar transferência", transfersAllSectionTitle: "Transferências", - transfersFilterFavs: "Favoritos", - transfersFavsSectionTitle: "Transferências favoritas", - transfersSearchLabel: "Pesquisa", - transfersSearchPlaceholder: "Pesquisar conceito, tags, utilizadores...", + transfersSearchPlaceholder: "Pesquisar em Transferências...", transfersMinAmountLabel: "Montante mínimo", transfersMaxAmountLabel: "Montante máximo", - transfersSortLabel: "Ordenar por", transfersSortRecent: "Mais recentes", transfersSortAmount: "Maior montante", transfersSortDeadline: "Prazo mais próximo", transfersSearchButton: "Pesquisa", - transfersFavoriteButton: "Favorito", - transfersUnfavoriteButton: "Remover favorito", - transfersMessageUserButton: "Mensagem", - transfersExpiringSoonBadge: "A EXPIRAR", transfersExpiredBadge: "EXPIRADO", transfersUpdatedAt: "Atualizado", transfersNoMatch: "Nenhuma transferência corresponde à tua pesquisa.", @@ -1481,15 +1532,20 @@ module.exports = { voteClosedTitle: "Votações fechadas", voteAllSectionTitle: "Votações", voteCreateButton: "Criar votação", + voteErrorTitle: "Verifica o formulário", + voteErrorDeadlineMin: "O prazo tem de ser de pelo menos {days} dias a partir de agora.", + voteErrorGeneric: "Não foi possível guardar a votação.", + voteLocked: "Esta votação já recebeu votos ou opiniões, ou pertence ao Parlamento ou aos Tribunais: já não pode ser editada nem apagada.", voteUpdateButton: "Atualizar", voteDeleteButton: "Eliminar", - voteOptionYes: "YES", + voteOptionYes: "SIM", voteOptionNo: "NO", voteOptionAbstention: "ABSTENTION", voteConfused: "CONFUSO", voteFollowMajority: "SEGUIR MAIORIA", voteNotInterested: "SEM INTERESSE", voteFilterAll: "TODOS", + voteFilterRecent: "RECENTES", voteFilterMine: "MEUS", voteFilterOpen: "ABERTO", voteFilterClosed: "FECHADO", @@ -1505,37 +1561,28 @@ module.exports = { voteTags: "Tags", voteOpinions: "Opiniões", novotes: "Sem propostas de votação disponíveis.", - voteBy: "Por", voteCreatedAt: "Criado em", voteNoQuestion: "Sem questão fornecida", voteUnknownCreator: "Criador desconhecido", voteUnknownDate: "Data desconhecida", - errorVoteNotFound: "Votação não encontrada", - errorAlreadyVoted: "Já deste a tua opinião.", - errorVoteClosedCannotEdit: "Não podes editar uma votação fechada", - errorVoteDeadlinePassed: "O prazo desta votação já passou", - errorRetrievingVote: "Erro ao obter votação", - errorCreatingVote: "Erro ao criar votação", - errorVoteAlreadyVoted: "Não é possível editar após a opinião ter sido emitida", - errorDeletingOldVote: "Erro ao eliminar opinião anterior", - errorCreatingUpdatedVote: "Erro ao criar opinião atualizada", - errorCreatingTombstone: "Erro ao criar tombstone", voteDetailSectionTitle: 'Detalhes da votação', voteCommentsLabel: 'Comentários', voteCommentsForumButton: 'Abrir discussão', voteCommentsSectionTitle: 'Abrir discussão', voteNoCommentsYet: 'Ainda sem comentários. Sê o primeiro a responder.', + commentsGoToLast: "Ir para o último comentário", voteNewCommentPlaceholder: 'Escreve o teu comentário aqui…', voteNewCommentButton: 'Publicar comentário', voteNewCommentLabel: 'Adicionar comentário', cvTitle: "Currículo", - cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Editar CV", cvCreateSectionTitle: "Criar CV", cvDescription: "Gere e partilha as tuas competências e informações profissionais.", cvNameLabel: "Nome completo", cvDescriptionLabel: "Resumo", cvPhotoLabel: "Foto", + cvPdfLabel: "CV (PDF)", + cvVisitButton: "Visitar CV", cvPersonalExperiencesLabel: "Experiências pessoais", cvPersonalSkillsLabel: "Competências pessoais (separadas por vírgulas)", cvOasisExperiencesLabel: "Experiências de contribuição Oasis", @@ -1548,29 +1595,130 @@ module.exports = { cvLocationLabel: "Localização", cvStatusLabel: "Estado", cvPreferencesLabel: "Preferências", - cvOasisContributorLabel: "Contribuidor Oasis", cvPersonal: "Pessoal", cvOasis: "Contribuidor Oasis (opcional)", - cvOasisContributorView: "Contribuição Oasis", + cvOasisContributorView: "Contribuição", cvEducational: "Educativo (opcional)", cvEducationalView: "Educativo", cvProfessional: "Profissional (opcional)", cvProfessionalView: "Profissional", cvAvailability: "Disponibilidade (opcional)", - cvAvailabilityView: "Disponibilidade", cvUpdateButton: "Atualizar", cvCreateButton: "Criar CV", - cvContactLabel: "Contacto", cvCreatedAt: "Criado em", cvUpdatedAt: "Atualizado em", cvEditButton: "Atualizar", cvDeleteButton: "Eliminar", cvNoCV: "Sem CV encontrado.", blogSubject: "Assunto", - blogMessage: "Corpo limitado a 8096 caracteres.", - blogImage: "Carregar mídia (máx: 50MB)", - blogPublish: "Pré-visualizar", - noPopularMessages: "Ainda sem mensagens populares publicadas", + blogMessage: "Mensagem", + blogImage: "Carregar mídia (máx: 75MB)", + blogMedia: "Anexos (até 8 imagens e um vídeo)", + blogPublish: "Publicar", + pollsTitle: "Sondagens", + dataTitle: "Correspondências", + dataDescription: "Explora e visualiza as correspondências da tua rede.", + dataFilterAll: "TUDO", + dataFilterMine: "MEUS", + dataFilterRecent: "RECENTES", + dataFilterTop: "TOP", + dataKindInhabitants: "HABITANTES", + dataKindJobs: "EMPREGOS", + dataKindProjects: "PROJETOS", + dataKindEvents: "EVENTOS", + dataKindTribes: "TRIBOS", + dataKindMarket: "MERCADO", + dataKindHousing: "HABITAÇÃO", + dataKindIndustry: "INDÚSTRIA", + dataKindTasks: "TAREFAS", + dataKindReports: "RELATÓRIOS", + dataKindVotes: "VOTAÇÕES", + dataSearchPlaceholder: "Pesquisar em Correspondências...", + dataCohesionTitle: "Coeficiente de coesão (CC)", + dataCcShort: "CC", + dataCohesionHint: "Afinidade média entre tudo o que a rede publicou.", + dataAffinity: "Afinidade", + dataCommonTerms: "Palavra-chave", + dataReasonFollowing: "Apoias este habitante", + dataReasonTribe: "Partilha uma tribo contigo", + dataReasonPinned: "Coincide com algo que fixaste", + dataReasonCv: "Coincide com as competências do teu CV", + dataReasonContent: "Relacionado com o que publicas", + dataReasonRelated: "Relacionado com os teus interesses", + dataReasonAlike: "Publicado por um habitante afim a ti", + dataReasonSupportsYou: "Apoia-te", + dataReasonMutual: "Apoio mútuo", + dataReasonRated: "Bem avaliado pela rede", + dataReasonNear: "Perto de ti", + dataStatPeople: "CVs", + dataStatConnected: "Ligados", + dataStatSkills: "Habilidades", + dataBestMatch: "Melhor match", + dataStatIsolated: "Isolados", + dataStatEntities: "Entidades comparadas", + dataStatTerms: "Termos distintos", + dataStatPairs: "Pares ligados", + dataMatchesTitle: "Correspondências", + dataMatchesHint: "Sugestões personalizadas do algoritmo.", + dataTermsTitle: "Palavras-chave", + dataTermsHint: "Os termos que aparecem em mais conteúdos.", + dataConnections: "Correspondências ligadas", + dataNoMatches: "Sem dados cruzados para mostrar.", + dataNoProfile: "Publica conteúdo ou escreve o teu CV para que o algoritmo aprenda o que te interessa.", + pollsDescription: "Módulo para perguntar à rede e contar as respostas.", + pollFilterAll: "TODAS", + pollFilterMine: "MINHAS", + pollFilterRecent: "RECENTES", + pollFilterTop: "TOP", + pollFilterVoted: "VOTADAS", + pollFilterOpen: "ABERTAS", + pollFilterClosed: "FECHADAS", + pollCreateButton: "Criar Sondagem", + pollCreateTitle: "Criar Sondagem", + pollEditTitle: "Editar Sondagem", + pollQuestion: "Pergunta", + pollQuestionPlaceholder: "O que queres perguntar?", + pollOptions: "Opções (uma por linha)", + pollOutcome: "Resultado", + pollNoVotesYet: "Ainda sem votos", + pollTie: "Empate", + pollOptionsPlaceholder: "Praça\nParque\nBar", + pollAnonymous: "Anónima", + pollAnonymousLabel: "Sondagem anónima", + pollMultiple: "Escolha múltipla", + pollMultipleLabel: "Permitir várias respostas", + pollDeadline: "Prazo", + pollTags: "Etiquetas", + pollPublishButton: "Publicar Sondagem", + pollUpdateButton: "Atualizar", + pollCloseButton: "Fechar", + pollDeleteButton: "Eliminar", + pollVoteButton: "Votar", + pollChangeVote: "Mudar Voto", + pollVoters: "Votantes", + pollComments: "Comentários", + pollStatusLabel: "Estado", + pollStatusOpen: "ABERTA", + pollStatusClosed: "FECHADA", + pollSearchPlaceholder: "Pesquisar em Sondagens...", + pollsNoItems: "Nenhuma sondagem encontrada.", + viewPoll: "Ver Sondagem", + pollChatCreate: "Criar Sondagem", + pollInChat: "Sondagem", + blogTitle: "Blogs", + blogDescription: "Módulo para descobrir e gerir blogs.", + blogFilterAll: "TODOS", + blogFilterMine: "MEUS", + blogFilterRecent: "RECENTES", + blogFilterFavorites: "FAVORITOS", + blogFilterTop: "TOP", + blogCreateButton: "Criar Blog", + blogSearchPlaceholder: "Pesquisar em Blogs...", + blogComments: "Comentários", + blogAllowComments: "Permitir comentários", + blogCommentsClosed: "O autor fechou os comentários deste blog.", + blogNoItems: "Nenhum blog encontrado.", + viewBlog: "Ver Blog", forumTitle: "Fóruns", forumCategoryLabel: "Categoria", forumTitleLabel: "Título", @@ -1578,43 +1726,21 @@ module.exports = { forumCreateButton: "Criar fórum", forumCreateSectionTitle: "Criar fórum", forumDescription: "Conversa abertamente com outros habitantes na tua rede.", + forumSearchPlaceholder: "Pesquisar em Fóruns...", forumFilterAll: "TODOS", forumFilterMine: "MEUS", forumFilterRecent: "RECENTES", forumFilterTop: "TOP", - forumMineSectionTitle: "Os teus fóruns", - forumRecentSectionTitle: "Fóruns recentes", - forumAllSectionTitle: "Fóruns", - forumDeleteButton: "Eliminar", forumParticipants: "participantes", forumMessages: "mensagens", - forumLastMessage: "Última mensagem", forumMessageLabel: "Mensagem", forumMessagePlaceholder: "Escreve a tua mensagem...", forumSendButton: "Enviar", - forumVisitForum: "Visitar fórum", noForums: "Sem fóruns encontrados.", - forumVisitButton: "Visitar fórum", - forumCatGENERAL: "Geral", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "Política", - forumCatTECH: "Tecnologia", - forumCatSCIENCE: "Ciência", - forumCatMUSIC: "Música", - forumCatART: "Arte", - forumCatGAMING: "Jogos", - forumCatBOOKS: "Livros", - forumCatFILMS: "Filmes", - forumCatPHILOSOPHY: "Filosofia", - forumCatSOCIETY: "Sociedade", - forumCatPRIVACY: "Privacidade", - forumCatCYBERWARFARE: "Guerra cibernética", - forumCatSURVIVALISM: "Sobrevivencialismo", + forumVisitButton: "Ver Fórum", + forumTopicLabel: "Tópico", imageTitle: "Imagens", imageDescription: "Explora e gere conteúdo de imagens na tua rede.", - imagePluginTitle: "Título", - imagePluginDescription: "Descrição", imageMineSectionTitle: "As tuas imagens", imageCreateSectionTitle: "Carregar imagem", imageUpdateSectionTitle: "Atualizar imagem", @@ -1623,17 +1749,14 @@ module.exports = { imageTopSectionTitle: "Imagens mais votadas", imageFavoritesSectionTitle: "Favoritos", imageGallerySectionTitle: "Galeria", - imageMemeSectionTitle: "Memes", imageFilterAll: "TODOS", imageFilterMine: "MEUS", imageFilterRecent: "RECENTES", imageFilterTop: "TOP", imageFilterFavorites: "FAVORITOS", imageFilterGallery: "GALERIA", - imageFilterMeme: "MEMES", imageCreateButton: "Carregar imagem", imageUpdateButton: "Atualizar", - imageDeleteButton: "Eliminar", imageAddFavoriteButton: "Adicionar favorito", imageRemoveFavoriteButton: "Remover favorito", imageFileLabel: "Seleciona um ficheiro de imagem (.jpeg, .jpg, .png, .gif)", @@ -1643,23 +1766,21 @@ module.exports = { imageTitlePlaceholder: "Opcional", imageDescriptionLabel: "Descrição", imageDescriptionPlaceholder: "Opcional", - imageMemeLabel: "MEME?", imageNoFile: "Nenhum ficheiro de imagem fornecido", noImages: "Sem imagens disponíveis.", - imageSearchPlaceholder: "Pesquisar título, tags, descrição, autor...", + imageSearchPlaceholder: "Pesquisar em Imagens...", imageSortRecent: "Mais recentes", imageSortOldest: "Mais antigos", imageSortTop: "Mais votados", imageSearchButton: "Pesquisa", - imageMessageAuthorButton: "Mensagem", imageUpdatedAt: "Atualizado", imageNoMatch: "Nenhuma imagem corresponde à tua pesquisa.", + feedSearchPlaceholder: "Pesquisar em Feed...", feedTitle: "Feed", createFeedTitle: "Criar Feed", createFeedButton: "Enviar Feed!", feedPlaceholder: "O que se passa? (máx 280 caracteres)", TODAYButton: "HOJE", - CREATEButton: "Criar Feed", totalOpinions: "Total de opiniões", moreVoted: "Mais votado", noFeedsFound: "Sem feeds encontrados.", @@ -1669,7 +1790,7 @@ module.exports = { alreadyRefeeded: "Já fizeste refeed disto.", activityTitle: "Atividade", yourActivity: "A tua atividade", - globalActivity: "Atividade global", + globalActivity: "Atividade", activityList: "Atividade", activityDesc: "Vê a atividade mais recente na tua rede.", allButton: "TODOS", @@ -1682,20 +1803,13 @@ module.exports = { concept: "Conceito", description: "Descrição", meme: "Meme", - activityContact: "Contacto", - activityBy: "Nome", - activityPixelia: "Novo pixel adicionado", - viewImage: "Ver imagem", - playAudio: "Reproduzir áudio", - playVideo: "Reproduzir vídeo", typeRecent: "RECENTES", - errorActivity: "Erro ao obter atividade", - typePost: "BLOGS", - recentButton: "RECENTES", + typeTop: "TOP", + typePost: "BLOG", + activityUpdateLabel: "ATUALIZAÇÃO", typeTribe: "TRIBOS", typeLarp: "L.A.R.P.", typeInhabitants: "HABITANTES", - activityProfileUpdated: "atualizou seu perfil", typeAbout: "HABITANTES", typeCurriculum: "CVS", typeImage: "IMAGENS", @@ -1717,19 +1831,19 @@ module.exports = { typeFeed: "FEEDS", typeContact: "CONTACTO", typePub: "PUB", - typeTombstone: "TOMBSTONE", + typeTombstone: "ELIMINADO", typeBanking: "BANCA", typeBankWallet: "BANCA/CARTEIRA", - typeBankClaim: "BANCA/UBI", + typeBankClaim: "BANCA/RBU", typeKarmaScore: "KARMA", - typeParliament: "PARLIAMENT", + typeParliament: "PARLAMENTO", typeSpread: "DIFUSÕES", - typeParliamentCandidature: "Parliament · Candidature", - typeParliamentTerm: "Parliament · Term", - typeParliamentProposal:"Parliament · Proposal", + typeParliamentCandidature: "Parlamento · Candidatura", + typeParliamentTerm: "Parlamento · Mandato", + typeParliamentProposal:"Parlamento · Proposta", typeParliamentRevocation:"Parlamento · Revogação", - typeParliamentLaw: "Parliament · New Law", - typeCourts: "COURTS", + typeParliamentLaw: "Parlamento · Nova lei", + typeCourts: "TRIBUNAIS", typeCourtsCase: "Tribunais · Caso", typeCourtsEvidence: "Tribunais · Prova", typeCourtsAnswer: "Tribunais · Resposta", @@ -1739,14 +1853,12 @@ module.exports = { typeCourtsSettlementAccepted: "Tribunais · Acordo aceite", typeCourtsNomination: "Tribunais · Nomeação", typeCourtsNominationVote: "Tribunais · Voto de nomeação", - activitySupport: "Nova aliança formada", - activityJoin: "Novo PUB aderido", question: "Pergunta", deadline: "Prazo", status: "Estado", votes: "Votos", - totalVotes: "Total Votes", - voteTotalVotes: "Total Votes", + totalVotes: "Total de votos", + voteTotalVotes: "Total de votos", name: "Nome", skills: "Competências", tags: "Tags", @@ -1754,14 +1866,10 @@ module.exports = { date: "Data", category: "Categoria", attendees: "Participantes", - activitySpread: "->", - visitLink: "Visitar link", - viewDocument: "Ver documento", location: "Localização", contentWarning: "Assunto", - personName: "Nome do habitante", bankWalletConnected: "Carteira ECOin", - bankUbiReceived: "UBI recebido", + bankUbiReceived: "RBU recebido", bankTx: "Tx", bankEpochShort: "Época", bankingUserEngagementScore: "Pontuação KARMA", @@ -1769,35 +1877,34 @@ module.exports = { link: "Link", activityProjectFollow: "%OASIS% está agora a %ACTION% este projeto %PROJECT%", activityProjectUnfollow: "%OASIS% está agora a %ACTION% este projeto %PROJECT%", - activityProjectPledged: "%OASIS% %ACTION% %AMOUNT% ao projeto %PROJECT%", following: "A SEGUIR", unfollowing: "A DEIXAR DE SEGUIR", pledged: "COMPROMETIDO", parliamentCandidatureId: "Candidature", - parliamentGovMethod: "Method", - parliamentVotesReceived: "Votes received", - parliamentMethodANARCHY: "Anarchy", + parliamentGovMethod: "Método", + parliamentVotesReceived: "Votos recebidos", + parliamentMethodANARCHY: "Anarquia", parliamentMethodVOTE: "Votação comunitária", parliamentMethodRANKED: "Escolha por ranking", parliamentMethodPLURALITY: "Pluralidade", parliamentMethodCOUNCIL: "Conselho", parliamentMethodJURY: "Júri", parliamentAnarchy: "ANARQUIA", - parliamentElectionsStart: "Elections start", - parliamentElectionsEnd: "Elections end", - parliamentCurrentLeader: "Winning candidature", + parliamentElectionsStart: "Início das eleições", + parliamentElectionsEnd: "Fim das eleições", + parliamentCurrentLeader: "Candidatura vencedora", parliamentProposalTitle: "Título", - parliamentOpenVote: "Open vote", + parliamentOpenVote: "Votação aberta", parliamentStatus: "Estado", parliamentLawQuestion: "Question", - parliamentLawMethod: "Method", - parliamentLawProposer: "Proposer", - parliamentLawEnacted: "Enacted at", + parliamentLawMethod: "Método", + parliamentLawProposer: "Proponente", + parliamentLawEnacted: "Promulgada em", parliamentLawVotes: "Votações", - createdAt: "Created at", - courtsCaseTitle: "Case", - courtsMethod: "Method", - courtsMethodJUDGE: "Judge", + createdAt: "Criado em", + courtsCaseTitle: "Caso", + courtsMethod: "Método", + courtsMethodJUDGE: "Juiz", courtsMethodJUDGES: "Painel de juízes", courtsMethodSINGLE_JUDGE: "Juiz único", courtsMethodJURY: "Júri", @@ -1805,48 +1912,42 @@ module.exports = { courtsMethodCOMMUNITY: "Comunidade", courtsMethodARBITRATION: "Arbitragem", courtsMethodVOTE: "Votação comunitária", - courtsAccuser: "Accuser", - courtsRespondent: "Respondent", + courtsAccuser: "Acusação", + courtsRespondent: "Defesa", courtsThStatus: "Estado", - courtsThAnswerBy: "Answer by", - courtsThEvidenceBy: "Evidence by", - courtsThDecisionBy: "Decision by", - courtsVotesNeeded: "Votes needed", - courtsVotesSlashTotal: "YES/TOTAL", - courtsOpenVote: "Open vote", - courtsAnswerTitle: "Answer", - courtsStanceADMIT: "Admit", - courtsStanceDENY: "Deny", - courtsStancePARTIAL: "Partial", - courtsStanceCOUNTERCLAIM: "Reconvenção", - courtsStanceNEUTRAL: "Neutro", - courtsVerdictResult: "Result", - courtsVerdictOrders: "Orders", - courtsSettlementText: "Settlement", + courtsThAnswerBy: "Responder até", + courtsThEvidenceBy: "Provas até", + courtsThDecisionBy: "Decisão até", + courtsVotesNeeded: "Votos necessários", + courtsVotesSlashTotal: "SIM/TOTAL", + courtsOpenVote: "Votação aberta", + courtsAnswerTitle: "Resposta", + courtsVerdictResult: "Resultado", + courtsVerdictOrders: "Ordens", + courtsSettlementText: "Acordo", courtsSettlementAccepted: "Aceite", - courtsSettlementPending: "Pendente", courtsJudge: "Juiz", - courtsThSupports: "Supports", - courtsFilterOpenCase: 'Open Case', - courtsEvidenceFileLabel: 'Ficheiro de prova (imagem, áudio, vídeo ou PDF)', - courtsCaseMediators: 'Mediadores', - courtsCaseMediatorsPh: 'Mediator Oasis IDs, separated by comma', + courtsThSupports: "Apoios", + courtsFilterOpenCase: "Abrir caso", + courtsCaseMediatorsPh: "IDs de Oasis dos mediadores, separados por vírgulas", courtsMediatorsLabel: 'Mediadores', - courtsThCase: 'Case', - courtsThCreatedAt: 'Start date', - courtsThActions: 'Actions', - courtsPublicPrefLabel: 'Visibility after resolution', - courtsPublicPrefYes: 'I agree this case can be fully public', - courtsPublicPrefNo: 'I prefer to keep the details private', - courtsPublicPrefSubmit: 'Save visibility preference', - courtsMethodMEDIATION: 'Mediation', - courtsNoCases: 'No cases.', + courtsThCase: "Caso", + courtsThCreatedAt: "Data de início", + courtsPublicPrefLabel: "Visibilidade após a resolução", + courtsPublicPrefYes: "Aceito que este caso seja totalmente público", + courtsPublicPrefNo: "Prefiro manter os detalhes privados", + courtsPublicPrefSubmit: "Guardar preferência de visibilidade", + courtsMethodMEDIATION: "Mediação", + courtsNoCases: "Não há casos.", reportsTitle: "Relatórios", - reportsDescription: "Gere e acompanha relatórios relacionados com problemas, bugs, abusos e avisos de conteúdo na tua rede.", + reportsDescription: "Gere e acompanha os relatos da tua rede.", + reportsSearchPlaceholder: "Pesquisar em Relatórios...", reportsFilterAll: "TODOS", + reportsFilterRecent: "RECENTES", + reportsFilterTop: "TOP", reportsFilterMine: "MEUS", reportsFilterFeatures: "FUNCIONALIDADES", - reportsFilterBugs: "BUGS", + reportsFilterBugs: "ERROS", reportsFilterAbuse: "ABUSO", reportsFilterContent: "CONTEÚDO", reportsFilterConfirmed: "CONFIRMADO", @@ -1856,30 +1957,20 @@ module.exports = { reportsFilterInvalid: "INVÁLIDO", reportsCreateButton: "Criar relatório", reportsTitleLabel: "Título", - reportsDescriptionLabel: "Descrição", - reportsDescriptionPlaceholder: "Por favor, fornece uma descrição detalhada.", reportsCategory: "Categoria", - reportsCategoryLabel: "Categoria", reportsCategoryFeatures: "Funcionalidades", - reportsCategoryBugs: "Bugs", + reportsCategoryBugs: "Erros", reportsCategoryAbuse: "Abuso", - reportsCategoryContent: "Problemas de conteúdo", + reportsCategoryContent: "Conteúdo", reportsUpdateButton: "Atualizar", reportsDeleteButton: "Eliminar", - reportsDateLabel: "Data", - reportsUploadFile: "Carregar mídia (máx: 50MB)", reportsMineSectionTitle: "Os teus relatórios", reportsFeaturesSectionTitle: "Pedidos de funcionalidades", - reportsBugsSectionTitle: "Bugs", + reportsBugsSectionTitle: "Erros", reportsAbuseSectionTitle: "Relatórios de abuso", reportsContentSectionTitle: "Problemas de conteúdo", reportsAllSectionTitle: "Relatórios", reportsNoItems: "Sem relatórios disponíveis.", - reportsValidationTitle: "Por favor, introduz um título válido.", - reportsValidationDescription: "A descrição não pode estar vazia.", - reportsValidationCategory: "Por favor, seleciona uma categoria.", - reportsCreatedAt: "Criado em", - reportsCreatedBy: "By", reportsSeverity: "Gravidade", reportsSeverityLow: "Baixa", reportsSeverityMedium: "Média", @@ -1890,18 +1981,14 @@ module.exports = { reportsStatusUnderReview: "Em análise", reportsStatusResolved: "Resolvido", reportsStatusInvalid: "Inválido", - reportsUpdateStatusButton: "Atualizar estado", - reportsAnonymityOption: "Enviar anonimamente", - reportsAnonymousAuthor: "Anónimo", reportsConfirmButton: "CONFIRMAR RELATÓRIO!", reportsConfirmations: "Confirmações", reportsConfirmedSectionTitle: "Relatórios confirmados", - reportsCreateTaskButton: "CRIAR TAREFA", + reportsCreateTaskButton: "Criar Tarefa", reportsOpenSectionTitle: "Relatórios abertos", reportsUnderReviewSectionTitle: "Relatórios em análise", reportsResolvedSectionTitle: "Relatórios resolvidos", reportsInvalidSectionTitle: "Relatórios inválidos", - reportsTemplateSectionTitle: 'Modelo de relatório', reportsBugTemplateTitle: 'Detalhes de reprodução (Bugs)', reportsFeatureTemplateTitle: 'Detalhes (Funcionalidades)', reportsAbuseTemplateTitle: 'Detalhes (Abuso)', @@ -1940,6 +2027,20 @@ module.exports = { reportsRequestedActionLabel: 'Ação solicitada', reportsRequestedActionPlaceholder: 'Remover, ocultar, etiquetar, avisar, etc.', tribesTitle: "Tribos", + tribeFilterAll: "TODAS", + tribeFilterRecent: "RECENTES", + tribeFilterMine: "MINHAS", + tribeFilterMembership: "ADESÃO", + tribeFilterSubtribes: "SUBTRIBOS", + tribeFilterTop: "TOP", + tribeFilterGallery: "GALERIA", + tribeFeedFilterTOP: "TOP", + tribeFeedFilterMINE: "MEUS", + tribeFeedFilterALL: "TODOS", + tribeFeedFilterRECENT: "RECENTES", + courtsStanceDENY: "Negar", + courtsStanceADMIT: "Admitir", + courtsStancePARTIAL: "Admitir em parte", tribeAllSectionTitle: "Tribos", tribeMineSectionTitle: "As tuas tribos", tribeCreateSectionTitle: "Criar tribo", @@ -1947,24 +2048,15 @@ module.exports = { tribeGallerySectionTitle: "Galeria de tribos", tribeRecentSectionTitle: "Tribos recentes", tribeTopSectionTitle: "Tribos populares", - tribeviewTribeButton: "Visitar tribo", - tribeviewSubTribeButton: "Visitar sub-tribo", tribeRootLabel: "RAIZ", tribeDescription: "Explora ou cria tribos na tua rede.", - tribeFilterAll: "TODOS", - tribeFilterMine: "MEUS", - tribeFilterMembership: "MEMBROS", - tribeFilterRecent: "RECENTES", - tribeFilterTop: "TOP", - tribeFilterSubtribes: "SUB-TRIBOS", - tribeFilterGallery: "GALERIA", tribeMainTribeLabel: "TRIBO PRINCIPAL", tribeCreateButton: "Criar tribo", tribeUpdateButton: "Atualizar", tribeDeleteButton: "Eliminar", - tribeImageLabel: "Carregar mídia (máx: 50MB)", + tribeImageLabel: "Carregar mídia (máx: 75MB)", tribeTitleLabel: "Título", - searchTribesPlaceholder: "FILTRAR tribos POR NOME …", + searchTribesPlaceholder: "Pesquisar em Tribos...", tribeTitlePlaceholder: "Nome da tribo", tribeDescriptionLabel: "Descrição", tribeDescriptionPlaceholder: "Descreve esta tribo", @@ -1975,14 +2067,18 @@ module.exports = { tribeInviteMode: "Modo de convite", tribeModeLabel: "MODE", tribeIsAnonymousLabel: "ESTADO", - tribeMembersCount: "Members", - tribeInviteCodePlaceholder: "Introduz código de convite", - tribeJoinByCodeButton: "Aderir com código", - tribeJoinButton: "ADERIR", + tribeMembersCount: "Membros", tribeEnterInvite: "INSERIR CONVITE", + subscriptionTitle: "Subscrições", + subscriptionOn: "Subscrito", + subscriptionOff: "Não subscrito", + subscriptionSubscribe: "Subscrever", + subscriptionUnsubscribe: "Cancelar subscrição", + subscriptionSubscribers: "Subscritores", + pmListLabel: "Enviar para", + pmListNone: "— Sem lista —", + pmListNoSubscribers: "Sem subscritores", tribeLeaveButton: "SAIR", - tribeYes: "SIM", - tribeNo: "NÃO", tribePublic: "PÚBLICO", tribePrivate: "PRIVADO", tribeGenerateInvite: "CONVITE ÚNICO", @@ -1991,13 +2087,8 @@ module.exports = { tribeRemoveInvitation: "REMOVER CONVITE", tribeCreatedAt: "Criado em", tribeAuthor: "Por", - tribeAuthorLabel: "AUTOR", tribeStrict: "Restrito", tribeOpen: "Abrir", - tribeFeedFilterRECENT: "RECENTES", - tribeFeedFilterMINE: "MEUS", - tribeFeedFilterALL: "TODOS", - tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Republicações", tribeFeedRefeed: "Republicar", tribeFeedMessagePlaceholder: "Escreve um feed…", @@ -2007,33 +2098,28 @@ module.exports = { tribeNotFound: "Tribo não encontrada!", createTribeTitle: "Criar tribo", updateTribeTitle: "Atualizar tribo", - tribeSectionOverview: "Visão geral", tribeSectionInhabitants: "Habitantes", tribeSectionVotations: "VOTAÇÕES", tribeSectionEvents: "EVENTOS", - tribeSectionReports: "Relatórios", tribeSectionTasks: "TAREFAS", tribeSectionFeed: "FEED", tribeSectionForum: "FÓRUM", - tribeSectionMarket: "Mercado", - tribeSectionJobs: "Empregos", - tribeSectionProjects: "Projetos", tribeSectionMedia: "Média", tribeSectionImages: "IMAGENS", tribeSectionAudios: "ÁUDIOS", tribeSectionVideos: "VÍDEOS", tribeSectionDocuments: "DOCUMENTOS", tribeSectionBookmarks: "MARCADORES", - tribeSectionMaps: "MAPS", + tribeSectionMaps: "MAPAS", tribeSectionPads: "PADS", tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionCalendars: "CALENDÁRIOS", + tribePadCreate: "Criar pad", + tribeChatCreate: "Criar chat", + tribeCalendarCreate: "Criar calendário", + tribePadsEmpty: "Ainda não há pads.", + tribeChatsEmpty: "Ainda não há chats.", + tribeCalendarsEmpty: "Ainda não há calendários.", tribeInhabitantsEmpty: "Ainda sem habitantes nesta tribo.", tribeEventCreate: "Criar evento", tribeEventsEmpty: "Ainda sem eventos.", @@ -2055,11 +2141,6 @@ module.exports = { tribeTaskStatusClosed: "FECHAR", tribeTaskAssign: "ATRIBUIR", tribeTaskUnassign: "REMOVER", - tribeReportCreate: "Criar relatório", - tribeReportsEmpty: "Ainda sem relatórios.", - tribeReportTitle: "Título", - tribeReportDescription: "Descrição", - tribeReportCategory: "Categoria", tribeVotationCreate: "Criar votação", tribeVotationsEmpty: "Ainda sem votações.", tribeVotationTitle: "Título", @@ -2077,27 +2158,6 @@ module.exports = { tribeForumCategory: "Categoria", tribeForumReply: "Responder", tribeForumReplies: "Respostas", - tribeMarketCreate: "Criar anúncio", - tribeMarketEmpty: "Ainda sem anúncios.", - tribeMarketTitle: "Título", - tribeMarketDescription: "Descrição", - tribeMarketPrice: "Preço", - tribeMarketImage: "Imagem", - tribeMarketCategory: "Categoria", - tribeJobCreate: "Criar emprego", - tribeJobsEmpty: "Ainda sem empregos.", - tribeJobTitle: "Título", - tribeJobDescription: "Descrição", - tribeJobLocation: "Localização", - tribeJobSalary: "Salário", - tribeJobDeadline: "Prazo", - tribeProjectCreate: "Criar projeto", - tribeProjectsEmpty: "Ainda sem projetos.", - tribeProjectTitle: "Título", - tribeProjectDescription: "Descrição", - tribeProjectGoal: "Objetivo", - tribeProjectFunded: "Financiado", - tribeProjectDeadline: "Prazo", tribeMediaUpload: "Carregar média", readDocument: "Ler Documento", tribeCreateImage: "Criar Imagem", @@ -2108,21 +2168,20 @@ module.exports = { tribeMediaEmpty: "Ainda sem média.", tribeMediaTitle: "Título", tribeMediaDescription: "Descrição", - tribeMediaType: "Tipo", tribeMediaTypeImage: "Imagem", tribeMediaTypeVideo: "Vídeo", tribeMediaTypeAudio: "Áudio", tribeMediaTypeDocument: "Documento", tribeMediaTypeBookmark: "Favorito", tribeContentDelete: "ELIMINAR", + tribeReachTribe: "TRIBO", + tribeReachNotice: "OASIS mostra-o a todos os habitantes e CLEARNET também na web aberta. Abri-lo deixa uma cópia legível fora da tribo: voltar a TRIBO oculta-o, mas as cópias já partilhadas não podem ser apagadas.", + tribeReachPrivate: "O conteúdo de uma tribo privada só pode ficar dentro da tribo.", + tribeReachOpenedBy: "Aberto por", + tribeReachChange: "Onde é publicado", tribeInviteCodeText: "Código de convite: ", oasisVersionLabel: "Versão do Oasis", - tribeInviteCodeHint: "Partilha este código com quem queres convidar. Pode entrar via /invites → Tribes.", - tribeGroupTribe: "Tribo", - tribeGroupOffice: "Escritório", - tribeGroupNetwork: "Rede", - tribeGroupEconomy: "Economia", - tribeGroupMedia: "Média", + tribeInviteCodeHint: "Partilha este código com quem queres convidar. Pode entrar via /invites.", tribeStatusOpen: "ABERTO", tribeStatusClosed: "FECHADO", tribeStatusInProgress: "EM PROGRESSO", @@ -2132,33 +2191,17 @@ module.exports = { tribePriorityCritical: "CRÍTICA", tribeTaskFilterAll: "TODOS", tribeMediaFilterAll: "TODOS", - tribeReportCatBug: "BUG", - tribeReportCatAbuse: "ABUSO", - tribeReportCatContent: "CONTEÚDO", - tribeReportCatOther: "OUTRO", tribeForumCatGeneral: "GERAL", tribeForumCatProposal: "PROPOSTA", tribeForumCatQuestion: "PERGUNTA", tribeForumCatAnnouncement: "ANÚNCIO", - tribeMarketCatGoods: "BENS", - tribeMarketCatServices: "SERVIÇOS", - tribeMarketCatFood: "ALIMENTAÇÃO", - tribeMarketCatOther: "OUTRO", tribeStatusLabel: "Estado", tribeSubTribes: "SUB-TRIBOS", tribeSubTribesCreate: "Criar Sub-Tribo", tribeSubTribesStrictDenied: "O modo estrito da tribo não permite criar novas sub-tribos. Por favor, entre em contato com o administrador.", - tribeSubTribesEmpty: "Ainda sem sub-tribos criadas.", - tribeActivityJoined: "ADERIU", - tribeActivityLeft: "SAIU", - tribeActivityFeed: "FEED", tribeActivityRefeed: "REPUBLICAÇÃO", - tribeGroupAnalytics: "Análises", - tribeGroupCreative: "Criativo", tribeSectionActivity: "ATIVIDADE", tribeSectionTrending: "TENDÊNCIAS", - tribeSectionOpinions: "OPINIÕES", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAGS", tribeSectionSearch: "PESQUISA", tribeActivityEmpty: "Ainda sem atividade.", @@ -2170,25 +2213,13 @@ module.exports = { tribeTrendingPeriodWeek: "Esta semana", tribeTrendingPeriodAll: "Todo o tempo", tribeTrendingEngagement: "envolvimento", - tribeOpinionsEmpty: "Ainda sem opiniões.", - tribeOpinionsCast: "Votar", - tribeOpinionsRankings: "Classificações", - tribeOpinionsAlreadyVoted: "Já votado", tribeTopCategory: "Mais votado", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "Tela colaborativa de pixel art.", - tribePixeliaPaint: "Pintar", - tribePixeliaContributors: "contribuidores", - tribePixeliaTotalPixels: "pixeis pintados", tribeTagsEmpty: "Nenhum tag encontrado.", - tribeTagsCloud: "Nuvem de tags", - tribeTagsContentWith: "Conteúdo com tag", - tribeSearchPlaceholder: "Pesquisar conteúdo da tribo...", tribeSearchEmpty: "Nenhum resultado encontrado.", - tribeSearchResults: "Resultados", tribeSearchMinChars: "Introduz pelo menos 2 caracteres para pesquisar.", agendaTitle: "Agenda", agendaDescription: "Aqui podes encontrar todos os teus itens atribuídos.", + agendaSearchPlaceholder: "Pesquisar em Agenda...", agendaFilterAll: "TODOS", agendaFilterToday: "HOJE", agendaFilterUpcoming: "PRÓXIMOS", @@ -2202,23 +2233,15 @@ module.exports = { agendaFilterReports: "RELATÓRIOS", agendaFilterTransfers: "TRANSFERÊNCIAS", agendaFilterJobs: "EMPREGOS", + agendaFilterIndustry: "INDÚSTRIA", + agendaFilterHousing: "HABITAÇÃO", agendaFilterProjects: "PROJETOS", agendaFilterCalendars: "CALENDÁRIOS", agendaNoItems: "Sem atribuições encontradas.", - agendaAuthor: "Por", agendaDiscardButton: "Descartar", agendaRestoreButton: "Restaurar", - agendaCreatedAt: "Criado em", - agendaTitleLabel: "Título", + agendaRemoveButton: "Remover", agendaMembersCount: "Membros", - agendaDescriptionLabel: "Descrição", - agendaStatus: "Estado", - agendaVisibility: "Visibilidade", - agendaEventDate: "Data do evento", - agendaEventLocation: "Localização", - agendaEventPrice: "Preço", - agendaEventUrl: "URL", - agendaTaskStart: "Hora de início", agendaLocationLabel: "Localização", agendaYes: "SIM", agendaNo: "NÃO", @@ -2228,61 +2251,16 @@ module.exports = { agendareportCategory: "Categoria", agendareportSeverity: "Gravidade", agendareportStatus: "Estado", - agendareportDescription: "Descrição", - agendaTaskEnd: "Hora de fim", - agendaTaskPriority: "Prioridade", - agendaTransferFrom: "De", - agendaTransferTo: "Para", agendaTransferConcept: "Concept", agendaTransferAmount: "Montante", - agendaTransferDeadline: "Deadline", + agendaTransferDeadline: "Prazo", opinionsTitle: "Opiniões", shareYourOpinions: "Descobre e vota em opiniões na tua rede.", - author: "By", + author: "Por", voteNow: "Votar agora", - alreadyVoted: "You have already opined.", + alreadyVoted: "Já opinaste.", noOpinionsFound: "Sem opiniões encontradas.", - RECENTButton: "RECENT", TOPButton: "TOP", - interestingButton: "INTERESSANTE", - necessaryButton: "NECESSÁRIO", - funnyButton: "ENGRAÇADO", - disgustingButton: "REPUGNANTE", - sensibleButton: "SENSÍVEL", - propagandaButton: "PROPAGANDA", - adultOnlyButton: "PARA ADULTOS", - boringButton: "ABORRECIDO", - confusingButton: "CONFUSO", - usefulButton: "ÚTIL", - informativeButton: "INFORMATIVO", - wellResearchedButton: "BEM PESQUISADO", - accurateButton: "PRECISO", - needsSourcesButton: "PRECISA FONTES", - wrongButton: "ERRADO", - lowQualityButton: "BAIXA QUALIDADE", - creativeButton: "CRIATIVO", - insightfulButton: "PERSPICAZ", - actionableButton: "ACIONÁVEL", - inspiringButton: "INSPIRING", - loveButton: "AMOR", - clearButton: "CLARO", - upliftingButton: "EDIFICANTE", - unnecessaryButton: "DESNECESSÁRIO", - rejectedButton: "REJEITADO", - misleadingButton: "ENGANOSO", - offTopicButton: "FORA DO TÓPICO", - duplicateButton: "DUPLICADO", - clickbaitButton: "CLICKBAIT", - spamButton: "SPAM", - trollButton: "TROLL", - nsfwButton: "NSFW", - violentButton: "VIOLENTO", - toxicButton: "TÓXICO", - harassmentButton: "ASSÉDIO", - hateButton: "ÓDIO", - scamButton: "FRAUDE", - triggeringButton: "PERTURBANTE", - opinionsCreatedAt: "Criado em", opinionsTotalCount: "Total de opiniões", voteInteresting: "Interessante", voteNecessary: "Necessário", @@ -2319,157 +2297,167 @@ module.exports = { voteCreative: "Criativo", voteSpam: "Spam", voteAdultOnly: "Para adultos", - publishBlog: "Publicar blog", privateMessage: "MP", pmSendTitle: "Mensagens privadas", pmSend: "Enviar!", - pmDescription: "Usa este formulário para enviar uma mensagem encriptada a outros habitantes.", + pmCancel: "Cancelar", + pmReset: "Repor", + pmSharedKeyHint: "Partilha esta chave com o destinatário por outro canal. É necessária para decifrar.", + pmDescription: "Use este formulário para enviar uma mensagem/ficheiro cifrado a outros habitantes.", + pmComposeTitle: "Enviar uma mensagem", pmRecipients: "Destinatários", pmRecipientsHint: "Introduz Oasis IDs separados por vírgulas", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "Até 7 habitantes por mensagem.", + pmTextPlaceholder: "Escreve a tua mensagem.", pmSubject: "Assunto", pmSubjectHint: "Introduz o assunto da mensagem", pmText: "Mensagem", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Decifrar", + fileShareTitle: "Compartilhar um ficheiro", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "Ficheiro", + fileShareDownload: "Baixar", + fileShareMutualError: "Só pode compartilhar ficheiros com habitantes com apoio mútuo.", + fileShareNoFile: "Nenhum ficheiro selecionado.", + fileShareTooLarge: "O ficheiro excede o tamanho permitido.", + fileShareFailed: "Não foi possível preparar o ficheiro.", + fileShareSendError: "Não foi possível enviar o ficheiro.", + fileShareUnavailable: "Este ficheiro não está disponível agora. Tenta mais tarde.", pmCrypterBadKey: "A chave partilhada está incorreta!", pmCrypterTooLong: "A mensagem é demasiado longa para ser cifrada com o Crypter. Encurta-a e tenta de novo.", pmCrypterCipherLabel: "Mensagem recifrada", pmCrypterCharsLabel: "caracteres", pmInvalidRecipients: "Oasis ID inválido (deveria ser como @…=.ed25519).", pmEncryptionLabel: "Cifragem", - pmFile: "Anexo", - private: "Privado", - privateDescription: "Your encrypted messages.", + private: "Caixa de entrada", + privateDescription: "Gere as tuas mensagens privadas.", privateInbox: "CAIXA DE ENTRADA", privateSent: "ENVIADAS", privateDelete: "Eliminar", pmCreateButton: "Escrever MP", - noPrivateMessages: "No private messages.", + noPrivateMessages: "Não há mensagens privadas.", pmFromLabel: "De:", pmToLabel: "Para:", - pmInvalidMessage: "Mensagem inválida", pmNoSubject: "(sem assunto)", pmSubjectLabel: "Assunto:", - pmBodyLabel: "Corpo", - pmBotJobs: "42-EmpregoBOT", - pmBotProjects: "42-ProjetosBOT", - pmBotMarket: "42-MercadoBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "A casa governante do L.A.R.P mudou.", + politicalBotParliamentTitle: "Começou um novo ciclo de governo no Parlamento.", + politicalBotTribeTitle: "Começou um novo ciclo de governo numa das tuas Tribos.", + politicalBotLarpIntro: "Agora {house} governa durante este ciclo: {cycle}", + politicalBotParliamentIntro: "O governo atual é {gov}", + politicalBotParliamentIntroLeader: "O governo atual é {gov}, exercido por {leader}", + politicalBotTribeIntro: "O governo atual na Tribo {tribe} é {gov}", + politicalBotTribeIntroLeader: "O governo atual na Tribo {tribe} é {gov}, exercido por {leader}", + politicalBotLeaderLabel: "Líder", inboxJobSubscribedTitle: "Nova subscrição à tua oferta de emprego", pmInhabitantWithId: "Habitante com Oasis ID:", pmHasSubscribedToYourJobOffer: "subscreveu a tua oferta de emprego", - inboxProjectCreatedTitle: "Novo projeto criado", - pmHasCreatedAProject: "criou um projeto", inboxMarketItemSoldTitle: "Item vendido", + inboxShopSoldTitle: "Produto vendido", pmYourItem: "O teu item", pmHasBeenSoldTo: "foi vendido a", pmFor: "por", inboxProjectPledgedTitle: "Novo compromisso com o teu projeto", pmHasPledged: "comprometeu", pmToYourProject: "ao teu projeto", - blockchain: 'BlockExplorer', - blockchainTitle: 'BlockExplorer', - blockchainDescription: 'Explore and visualize the blocks in the blockchain.', - blockchainNoBlocks: 'No blocks found in the blockchain.', + blockchain: "Explorador de blocos", + blockchainDescription: "Explora e visualiza os blocos da blockchain.", + blockchainNoBlocks: "Não foram encontrados blocos na blockchain.", blockchainStatsTitle: 'Estatísticas', blockchainStatsTypeBreakdown: 'Distribuição por tipo', blockchainStatsTopAuthors: 'Autores principais', blockchainStatsCount: 'Blocos', - blockchainBlockID: 'Block ID', - blockchainBlockAuthor: 'Author', + blockchainBlockID: "ID do bloco", + blockchainBlockAuthor: "Autor", blockchainBlockCarbon: 'Pegada de carbono', blockchainBlockType: 'Type', - blockchainBlockTimestamp: 'Timestamp', - blockchainBlockContent: 'Block', - blockchainBlockURL: 'URL:', - blockchainContent: 'Block', - blockchainContentPreview: 'Preview of the block content', + blockchainBlockTimestamp: "Carimbo temporal", blockchainLatestDatagram: 'Último Datagrama', - blockchainDatagram: 'Datagrama', - blockchainDetails: 'View block details', - blockchainViewBlockexplorer: "Ver no blockexplorer", - blockchainBlockInfo: 'Block Information', - blockchainBlockDetails: 'Details of the selected block', - blockchainBack: 'Back to Blockexplorer', + blockchainDatagram: "Datagrama", + blockchainDetails: "Ver Detalhes do Bloco", + blockchainViewBlockexplorer: "Ver no Blockexplorer", + blockchainBack: "Voltar ao explorador de blocos", blockchainContentDeleted: "Este conteúdo foi eliminado (tombstone)", - visitContent: "Visitar conteúdo", - banking: 'Banking', - bankingTitle: 'Banking', - bankingDescription: 'Explore the current value of ECOin and the corresponding UBI allocation, distributed per epoch based on participation and trust.', - bankOverview: 'Overview', - bankEpochs: 'Epochs', - bankRules: 'Rules', - pending: 'Pending', - closed: 'Closed', - bankBack: 'Back to Banking', - bankViewTx: 'View Tx', - bankClaimNow: 'Claim now', - bankClaimUBI: 'Reivindicar RBU!', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: 'Nenhuma alocação RBU pendente para esta época.', - bankEpoch: 'Epoch', - bankPool: 'Pool (this epoch)', - bankWeightsSum: 'Sum of weights', - bankAllocations: 'Allocations', - bankNoAllocations: 'No allocations found.', - bankNoEpochs: 'No epochs found.', - bankEpochAllocations: 'Epoch allocations', - bankAllocId: 'Allocation ID', + visitContent: "Visitar Conteúdo", + favoriteAdd: "Fixar nos Favoritos", + pmContentTooltip: "Enviar Mensagem Privada", + phoneCallTooltip: "Fazer uma Chamada", + favoriteRemove: "Remover dos Favoritos", + reportContent: "Denunciar Conteúdo", + banking: "Banca", + bankingTitle: "Banca", + bankingDescription: "A economia coletiva de ECOin.", + bankOverview: "Resumo", + bankEpochs: "Épocas", + bankRules: "Regras", + pending: "Pendentes", + closed: "Fechadas", + bankClaimNow: "Reivindicar agora", + bankClaimUBI: 'Reivindicar RBU', + bankClaimAndPay: "Reivindicar e pagar", + bankClaimedPending: "Reivindicação pendente...", + bankStatusClaimed: "Reivindicado", + bankStatusExpired: "Expirado", + bankPubOnly: "Operação só para PUBs", + bankEpoch: "Época", + bankPool: "Fundo (esta época)", + bankWeightsSum: "Soma dos pesos", + bankNoAllocations: "Não foram encontradas atribuições.", + bankNoEpochs: "Não foram encontradas épocas.", + bankEpochAllocations: "Atribuições da época", + bankAllocId: "ID da atribuição", bankAllocDate: 'Date', bankAllocConcept: 'Concept', - bankAllocFrom: 'From', - bankAllocTo: 'To', - bankAllocAmount: 'Amount', + bankAllocFrom: "De", + bankAllocTo: "Para", + bankAllocAmount: "Montante", bankAllocStatus: 'Status', - bankEpochId: 'Epoch ID', - bankRuleHash: 'Rules Snapshot Hash', - bankViewEpoch: 'View Epoch', - bankUserBalance: 'Your Balance', - ecoWalletNotConfigured: 'ECOin Wallet not configured', - editWallet: 'Edit wallet', - addWallet: 'Add wallet', - bankAddresses: 'Addresses', - bankNoAddresses: 'No addresses found.', + bankEpochId: "ID da época", + bankRuleHash: "Hash do instantâneo das regras", + bankViewEpoch: "Ver época", + bankYourIndustryBalance: "A tua parte da indústria", + bankYourUbiMonth: "O teu RBU (este mês)", + bankYourFundsMonth: "Fundos totais", + bankIndustryBalance: "Produção industrial", + bankUserBalance: "O teu saldo", + bankAddresses: "Endereços", + bankNoAddresses: "Não foram encontrados endereços.", bankUser: 'Oasis ID', - bankAddress: 'Address', - bankAddAddressTitle: 'Add ECOIN address', + bankAddress: "Endereço", + bankAddAddressTitle: "Adicionar endereço ECOin", bankAddAddressUser: 'Oasis ID', - bankAddAddressAddress: 'ECOIN Address', - bankAddAddressSave: 'Save', - bankAddressAdded: 'Address added', - bankAddressUpdated: 'Address updated', - bankAddressExists: 'Address already exists', - bankAddressInvalid: 'Invalid address', - bankAddressDeleted: 'Address deleted', - bankAddressNotFound: 'Address not found', + bankAddAddressLabel: "Etiqueta (opcional)", + bankAddAddressLabelPlaceholder: "Carteira do Bob, loja da Alice…", + bankAddAddressUserOptional: "Oasis ID (opcional)", + bankAddressUnnamed: "Endereço sem nome", + bankAddAddressAddress: "Endereço ECOin", + bankAddAddressSave: "Guardar", + bankAddressAdded: "Endereço adicionado", + bankAddressUpdated: "Endereço atualizado", + bankAddressExists: "O endereço já existe", + bankAddressInvalid: "Endereço inválido", + bankAddressDeleted: "Endereço eliminado", + bankAddressNotFound: "Endereço não encontrado", bankAddressForbidden: 'Só podes configurar o teu próprio endereço de cobrança', - bankAddressTotal: 'Total Addresses', - bankAddressSearch: 'Search @inhabitant or address', + bankAddressTotal: "Total de endereços", + bankAddressSearch: "Procurar @habitante ou endereço", bankAddressActions: 'Actions', - bankAddressDelete: 'Delete', + bankAddressDelete: "Eliminar", bankAddressSource: 'Source', - bankAddressDeleteConfirm: 'Delete this address?', - search: 'Search!', + bankAddressDeleteConfirm: "Eliminar este endereço?", + search: "Procurar!", bankLocal: 'Local', - bankFromOasis: 'Oasis', - bankMyAddress: 'Your address', - bankRemoveMyAddress: 'Remove my address', - bankNotRemovableOasis: 'Addresses cannot be removed locally', - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "Rede", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "SEM FUNDOS!", bankUbiAvailableOk: "DISPONÍVEL!", - bankUbiAvailability: "Disponibilidade UBI", + bankUbiAvailability: "RBU", bankAlreadyClaimedThisMonth: "Já reclamado este mês", bankUbiThisMonth: "RBU (este mês)", bankUbiLastClaimed: "RBU (última reclamada)", @@ -2483,13 +2471,6 @@ module.exports = { shopsTitle: "Lojas", shopDescription: "Descubra e gerencie lojas na rede.", shopTitle: "Loja", - shopFilterAll: "TODAS", - shopFilterMine: "MINHAS", - shopFilterRecent: "RECENTES", - shopFilterTop: "TOP", - shopFilterProducts: "PRODUTOS", - shopFilterPrices: "PREÇOS", - shopFilterFavorites: "FAVORITAS", shopUpload: "Criar Loja", shopAllSectionTitle: "Todas as Lojas", shopMineSectionTitle: "Minhas Lojas", @@ -2506,19 +2487,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Publicar na Clearnet", - shopClearnetUnpublish: "Remover da Clearnet", - shopClearnetUrlLabel: "URL Clearnet", - shopClearnetWarning: "Publicar na Clearnet torna esta loja indexável por motores de busca e rastreadores externos.", + transportChipLabel: "Cifrado em trânsito", shopAddFavorite: "Adicionar Favorito", shopRemoveFavorite: "Remover Favorito", shopOpen: "ABERTA", shopClosed: "FECHADA", - shopOpenShop: "Abrir Loja", - shopCloseShop: "Fechar Loja", shopMakePrivate: "TORNAR PRIVADA (E2E)", shopMakePublic: "TORNAR PÚBLICA", shopProducts: "Produtos", + shopFilterAll: "TODAS", + shopFilterRecent: "RECENTES", + shopFilterMine: "MINHAS", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUTOS", + shopFilterPrices: "PREÇOS", + shopFilterFavorites: "FAVORITOS", shopProductAdd: "Adicionar Produto", shopProductTitle: "Produto", shopProductUpdate: "Atualizar Produto", @@ -2543,40 +2526,37 @@ module.exports = { shopOrderPrice: "Preço", shopOrderBuyer: "Comprador", shopBackToShop: "Voltar à Loja", - shopShareUrl: "URL de partilha", - shopVisitShop: "VISITAR LOJA", marketVisitItem: "VER ARTIGO", shopStatus: "ESTADO", shopCreatedAt: "CRIADO", - shopSearchPlaceholder: "Pesquisar lojas...", + shopSearchPlaceholder: "Pesquisar em Lojas...", shopUrl: "URL", shopLocation: "Localização", shopTags: "Tags", shopVisibility: "Visibilidade", - shopImage: "Imagem", shopShortDescription: "Descricao Curta", shopShortDescriptionPlaceholder: "Descricao breve para os cartoes (max 160 caracteres)", shopTitlePlaceholder: "Nome da sua loja", shopDescriptionPlaceholder: "Descricao detalhada da sua loja", - shopUrlPlaceholder: "https://url-da-sua-loja.com", shopLocationPlaceholder: "Cidade, Pais", - shopTagsPlaceholder: "tag1, tag2, tag3", + shopTagsPlaceholder: "Introduz tags separadas por vírgulas", mapTitlePlaceholder: "Titulo do mapa", shopProductFeatured: "Produto em Destaque", - shopSendToMarket: "Send to Market", + shopSendToMarket: "Enviar para o mercado", typeShop: "LOJAS", typeShopProduct: "PRODUTO DE LOJA", - bankExchange: 'Exchange', - bankExchangeCurrentValue: 'ECOin Value (1h)', - bankTotalSupply: 'ECOin Total Supply', - bankEcoinHours: "Equivalência ECOin em tempo", - bankHoursOfWork: 'hours', + bankExchange: "Câmbio", + bankExchangeCurrentValue: "Valor de ECOin (1h)", + bankTotalSupply: "Oferta ECOin total", + bankPubsSupply: "Oferta ECOin da reserva dos PUBs", + bankHoldingSupply: "Oferta ECOin fora do RBU", + bankEcoinHours: "Equivalência ECOin em tempo (1 ECO)", + bankHoursOfWork: "horas", bankUnitMs: "ms", bankUnitSeconds: "segundos", bankUnitMinutes: "minutos", bankUnitDays: "dias", - bankExchangeNoData: 'No data available', - bankExchangeIndex: 'ECOin Value (1h)', + bankExchangeNoData: "Não há dados disponíveis", bankInflation: 'ECOin Inflation (anual)', bankInflationMonthly: 'ECOin Inflation (mensual)', bankExchangeChartTitle: 'Progressão do valor de ECOin ao longo do tempo', @@ -2584,44 +2564,25 @@ module.exports = { bankExchangeChartSupply: 'Oferta', bankExchangeChartInflation: 'Inflação %', bankExchangeChartEmpty: 'Ainda não há amostras suficientes — volte mais tarde', - bankCurrentSupply: 'ECOin Current Supply', - bankingSyncStatus: 'ECOin Status', - bankingSyncStatusSynced: 'Synced', - bankingSyncStatusOutdated: 'Outdated', - statsTitle: 'Statistics', + bankCurrentSupply: "Oferta ECOin atual", + bankingSyncStatus: "Estado de ECOin", + bankingSyncStatusSynced: "Sincronizado", + bankingSyncStatusOutdated: "Não sincronizado", + statsTitle: "Estatísticas", statistics: "Estatísticas", - statsInhabitant: "Estatísticas do habitante", statsDescription: "Descobre estatísticas sobre a tua rede.", - ALLButton: "ALL", - MINEButton: "MINE", - TOMBSTONEButton: "TOMBSTONES", - statsYou: "Tu", - statsUserId: "Oasis ID", + ALLButton: "TUDO", + RECENTButton: "RECENTES", + MINEButton: "MEUS", + TOMBSTONEButton: "ELIMINADOS", statsCreatedAt: "Criado em", statsYourContent: "Conteúdo", statsYourOpinions: "Opiniões", - statsYourTombstone: "Tombstones", - statsNetwork: "Rede", - statsTotalInhabitants: "Habitantes", - statsDiscoveredTribes: "Tribos (públicas)", - statsPrivateDiscoveredTribes: "Tribos (privadas)", statsNetworkContent: "Conteúdo", - statsYourMarket: "Mercado", - statsYourJob: "Empregos", - statsYourProject: "Projetos", - statsYourTransfer: "Transferências", - statsYourForum: "Fóruns", statsNetworkOpinions: "Opiniões", - statsDiscoveredMarket: "Mercado", - statsDiscoveredJob: "Empregos", - statsDiscoveredProject: "Projetos", - statsBankingTitle: "Banca", - statsEcoWalletLabel: "Carteira ECOIN", + statsEcoWalletLabel: "Carteira ECOin", + walletAddressCopyHint: "Clica para selecionar e copiar", statsEcoWalletNotConfigured: "Não configurada!", - statsTotalEcoAddresses: "Total de endereços", - statsDiscoveredTransfer: "Transferências", - statsDiscoveredForum: "Fóruns", - statsNetworkTombstone: "Tombstones", statsBookmark: "Marcadores", statsEvent: "Eventos", statsTask: "Tarefas", @@ -2642,18 +2603,16 @@ module.exports = { statsTransfer: "Transferências", statsAiExchange: "IA", statsPUBs: 'PUBs', + statsSyncedPeers: "Pares", + statsTotalLabel: "Total", statsPost: "Publicações", - statsOasisID: "Oasis ID", statsSize: "Total (tamanho)", statsBlockchainSize: "Blockchain (tamanho)", statsBlobsSize: "Blobs (tamanho)", statsActivity7d: "Atividade (últimos 7 dias)", statsActivity7dTotal: "Total 7 dias", statsActivity30dTotal: "Total 30 dias", - statsKarmaScore: "Pontuação KARMA", - statsPublic: "Público", - statsPrivate: "Privado", - day: "Day", + day: "Dia", messages: "Mensagens", statsProject: "Projetos", statsProjectsTitle: "Projetos", @@ -2664,30 +2623,14 @@ module.exports = { statsProjectsCancelled: "Cancelado", statsProjectsGoalTotal: "Objetivo total", statsProjectsPledgedTotal: "Total comprometido", - statsProjectsSuccessRate: "Taxa de sucesso", - statsProjectsAvgProgress: "Progresso médio", - statsProjectsMedianProgress: "Progresso mediano", - statsProjectsActiveFundingAvg: "Financiamento ativo médio", - statsJobsTitle: "Empregos", - statsJobsTotal: "Total de empregos", - statsJobsOpen: "Abrir", - statsJobsClosed: "Fechado", - statsJobsOpenVacants: "Vagas abertas", - statsJobsSubscribersTotal: "Total de subscritores", - statsJobsAvgSalary: "Salário médio", - statsJobsMedianSalary: "Salário mediano", statsMarketTitle: "Mercado", statsMarketTotal: "Total de itens", statsMarketForSale: "À venda", statsMarketReserved: "Reservado", statsMarketClosed: "Fechado", statsMarketSold: "Vendido", - statsMarketRevenue: "Receita", - statsMarketAvgSoldPrice: "Preço médio de venda", statsUsersTitle: "Habitantes", user: "Habitante", - statsTombstoneTitle: "Tombstones", - statsNetworkTombstones: "Tombstones da rede", statsTombstoneRatio: "Rácio de tombstones (%)", statsParliamentCandidature: "Candidaturas parlamentares", statsParliamentTerm: "Mandatos parlamentares", @@ -2713,31 +2656,28 @@ module.exports = { aiPrompt: "Fornece uma resposta informativa e precisa.", aiConfiguration: "Definir prompt", aiPromptUsed: "Prompt", - aiClearHistory: "Limpar histórico de conversa", - aiSharePrompt: "Adicionar esta resposta ao treino coletivo?", - aiShareYes: "Sim", - aiShareNo: "Não", - aiSharedLabel: "Adicionado ao treino", - aiRejectedLabel: "Não adicionado ao treino", - aiServerError: "A IA não conseguiu responder. Por favor, tenta novamente.", - aiInputPlaceholder: "What is Oasis?", + aiClearHistory: "Limpar histórico", + aiServerError: "Não consigo responder bem. Tenta de novo.", + aiStatusLoading: "42 está a carregar o modelo. Pergunta de novo dentro de momentos.", + aiThinking: "A pensar", + aiWaiting: "À espera", + aiStatusMissingModel: "O modelo de IA não está instalado: falta src/AI/oasis-42-1-chat.Q4_K_M.gguf.", + aiExportFineTuning: "Exportar Fine-Tuning", + aiFactsTitle: "Dados da tua rede", + aiInputPlaceholder: "O que é o Oasis?", typeAiExchange: "IA", aiApproveTrain: "Adicionar ao treino coletivo", aiRejectTrain: "Não treinar", - aiTrainPending: "Aprovação pendente", aiTrainApproved: "Aprovado para treino", aiTrainRejected: "Rejeitado para treino", aiSnippetsUsed: "Excertos usados", - aiSnippetsLearned: "Snippets learned", - statsAITraining: "AI training", - aiApproveCustomTrain: "Treinar com esta resposta personalizada", + aiSnippetsLearned: "Fragmentos aprendidos", + statsAITraining: "Treino de IA", aiCustomAnswerPlaceholder: "Escreve a tua resposta personalizada…", - statsAIExchanges: "Model Exchanges", marketMineSectionTitle: "Os teus itens", marketCreateSectionTitle: "Criar item", marketUpdateSectionTitle: "Atualizar", marketAllSectionTitle: "Mercado", - marketRecentSectionTitle: "Mercado recente", marketTitle: "Mercado", marketDescription: "Um mercado para trocar bens ou serviços na tua rede.", marketFilterAll: "TODOS", @@ -2752,7 +2692,7 @@ module.exports = { marketFilterDiscarded: "DESCARTADO", marketFilterRecent: "RECENTES", marketFilterMyBids: "LICITAÇÕES", - marketCreateButton: "Criar item", + marketCreateButton: "Vender algo", marketItemType: "Tipo", marketItemTitle: "Título", marketItemAvailable: "Prazo", @@ -2767,14 +2707,11 @@ module.exports = { marketItemDeadline: "Prazo", marketItemIncludesShipping: "Inclui envio?", marketItemHighestBid: "Maior licitação", - marketItemHighestBidder: "Maior licitante", marketItemStock: "Disponibilidade", marketOutOfStock: "Esgotado", - marketItemBidTime: "Hora da licitação", marketActionsUpdate: "Atualizar", marketUpdateButton: "Atualizar", marketActionsDelete: "Eliminar", - marketActionsSold: "Marcar como vendido", marketActionsChangeStatus: "ALTERAR ESTADO", marketActionsBuy: "COMPRAR!", marketAuctionBids: "Licitações atuais", @@ -2782,20 +2719,82 @@ module.exports = { marketItemSeller: "Vendedor", marketNoItems: "Ainda sem itens disponíveis.", marketYourBid: "A tua licitação", - marketCreateFormImageLabel: "Carregar mídia (máx: 50MB)", - marketSearchLabel: "Pesquisa", - marketSearchPlaceholder: "Pesquisar título ou tags", + marketCreateFormImageLabel: "Carregar mídia (máx: 75MB)", + marketSearchPlaceholder: "Pesquisar em Mercado...", marketMinPriceLabel: "Preço mínimo", marketMaxPriceLabel: "Preço máximo", - marketSortLabel: "Ordenar por", marketSortRecent: "Mais recentes", marketSortPrice: "Preço", marketSortDeadline: "Prazo", marketSearchButton: "Pesquisa", marketAuctionEndsIn: "Termina", marketAuctionEnded: "Terminado", - marketMyBidBadge: "Licitaste", marketNoItemsMatch: "Nenhum item corresponde à tua pesquisa.", + housingTitle: "Habitação", + housingDescriptionText: "Descobre e gere lugares na tua rede.", + modulesHousingLabel: "Habitação", + modulesHousingDescription: "Módulo para descobrir e gerir lugares.", + typeHousing: "HABITAÇÃO", + housingFilterAll: "TUDO", + housingFilterMine: "MEUS", + housingFilterRequested: "PEDIDOS", + housingFilterSale: "VENDA", + housingFilterRent: "ARRENDAMENTO", + housingFilterCouchsurfing: "COUCHSURFING", + housingFilterOpen: "DISPONÍVEIS", + housingFilterClosed: "INDISPONÍVEIS", + housingFilterRecent: "RECENTES", + housingFilterTop: "TOP", + housingType: "Oferta", + housingTypeSALE: "À venda", + housingTypeRENT: "Para arrendar", + housingTypeCOUCHSURFING: "Couchsurfing", + housingProperty: "Imóvel", + housingPropertyAPARTMENT: "Apartamento", + housingPropertyHOUSE: "Casa", + housingPropertyROOM: "Quarto", + housingPropertyLAND: "Terreno", + housingPropertyOTHER: "Outro", + housingTitleLabel: "Título", + housingTitlePlaceholder: "Nome do lugar", + housingDescription: "Descrição", + housingDescriptionPlaceholder: "Descreve o lugar, o bairro, o que está incluído: casas de banho, jardim, camas…", + housingRules: "Regras", + housingRulesPlaceholder: "Animais, fumar, horas de silêncio, estadia mínima…", + housingPlace: "Lugar", + housingPlacePlaceholder: "País, cidade, morada", + housingPrice: "Preço", + housingPriceLabel: "Preço (ECO)", + housingSizeLabel: "Área (m²)", + housingFree: "GRÁTIS", + housingPerMonth: "/mês", + housingRooms: "Quartos", + housingSize: "Área", + housingCapacity: "Hóspedes", + housingAvailableFrom: "Disponível desde", + housingAvailableTo: "Disponível até", + housingTags: "Etiquetas", + spreadEditWarning: "Este conteúdo perderá os spreads ao editá-lo", + housingRequests: "Pedidos", + housingRequestButton: "Pedir", + housingCancelButton: "Cancelar pedido", + housingRequestedBadge: "PEDIDO", + housingStatus: "Estado", + housingStatusOPEN: "DISPONÍVEL", + housingStatusCLOSED: "INDISPONÍVEL", + housingSetOpen: "Tornar disponível", + housingSetClosed: "Tornar indisponível", + housingCreateButton: "Criar lugar", + housingUpdateButton: "Atualizar", + housingSearchPlaceholder: "Pesquisar em Habitação...", + housingSearchButton: "Procurar", + housingMinPrice: "Preço mín.", + housingMaxPrice: "Preço máx.", + housingSortRecent: "Mais recentes", + housingSortPrice: "Preço mais baixo", + housingSortRequests: "Mais pedidos", + housingSortRating: "Melhor avaliados", + housingNoItems: "Não foram encontrados lugares.", jobsTitle: "Empregos", jobsCandidatesTitle: "Candidatos sugeridos", jobsCandidatesDescription: "Habitantes cujas competências combinam com este emprego. Envie uma mensagem privada para convidá-los.", @@ -2843,8 +2842,7 @@ module.exports = { jobLocationPresencial: "Presencial", jobLocationRemote: "Remoto", jobVacantsPlaceholder: "Número de posições", - jobSalaryPlaceholder: "Salário em ECO por 1 hora dedicada", - jobImage: "Carregar mídia (máx: 50MB)", + jobImage: "Carregar mídia (máx: 75MB)", jobTasks: "Tarefas", jobType: "Tipo de emprego", jobTime: "Horário", @@ -2853,10 +2851,9 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Empregos com melhor salário", createJobButton: "Publicar emprego", - viewDetailsButton: "Ver detalhes", noJobsFound: "Sem ofertas de emprego encontradas.", jobAuthor: "Por", - jobTypeFreelance: "Freelancer", + jobTypeFreelance: "Independente", jobTypeSalary: "Empregado", jobTypeExchange: "Troca de horas", jobTypeEXCHANGE: "TROCA DE HORAS", @@ -2865,42 +2862,208 @@ module.exports = { jobsHoursOffered: "Horas oferecidas", jobsHoursRequested: "Horas pedidas em troca", jobsExchangeSkill: "Habilidade pedida em troca", - jobsHoursOfferedPlaceholder: "p. ex. 4", - jobsHoursRequestedPlaceholder: "p. ex. 4", - jobsExchangeSkillPlaceholder: "p. ex. carpintaria, design", - jobExchangeHint: "Para empregos de troca de horas, preencha os campos abaixo em vez do salário.", - jobTimePartial: "Part-time", - jobTimeComplete: "Full-time", - jobsDeleteButton: "ELIMINAR", + jobTimePartial: "Tempo parcial", + jobTimeComplete: "Tempo inteiro", jobsUpdateButton: "ATUALIZAR", jobsFilterApplied: "CANDIDATADO", jobsAppliedTitle: "As minhas candidaturas", jobsAppliedBadge: "Candidatado", - jobsFilterFavs: "Favoritos", - jobsFavsTitle: "Favoritos", - jobsFilterNeeds: "Precisa ajuda", - jobsNeedsTitle: "Precisa ajuda", - jobsSearchLabel: "Pesquisa", - jobsSearchPlaceholder: "Pesquisar título, tags, descrição...", + jobsSearchPlaceholder: "Pesquisar em Empregos...", jobsMinSalaryLabel: "Salário mínimo", jobsMaxSalaryLabel: "Salário máximo", - jobsSortLabel: "Ordenar por", jobsSortRecent: "Mais recentes", jobsSortSalary: "Maior salário", jobsSortSubscribers: "Mais candidatos", jobsSearchButton: "Pesquisa", - jobsFavoriteButton: "Favorito", - jobsUnfavoriteButton: "Remover favorito", - jobsMessageAuthorButton: "MP", jobsApplicants: "Candidatos", jobsUpdatedAt: "Atualizado", - jobSetOpen: "Set open", - jobNewBadge: "NOVO", + jobSetOpen: "Marcar como aberto", jobsTagsLabel: "Tags", - jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "Nenhum emprego corresponde à tua pesquisa.", + industrySearchPlaceholder: "Pesquisar em Indústria...", + industryAllSectors: "Todos os setores", + industryVisitButton: "Visitar indústria", + industryAdvanceTo: "Definir para", + industryAmount: "Quantia", + industryApproveBuild: "Aprovar", + industryBlueprint: "Plano", + industryBlueprints: "Planos", + industryBuild: "Produção", + industryBuildNotes: "Notas", + industryBuildStart: "Data de início", + industryVoteUpdateButton: "Votar atualização", + industryVoteDeleteButton: "Votar exclusão", + industryRevokeVote: "Revogar", + industryTimeLeft: "Tempo restante", + industryBuildEnd: "Data de fim", + industryBuilds: "Produções", + industryBuildTitle: "Título", + industryContribute: "Contribuir", + industryContributeButton: "Contribuir", + industryContributions: "Contribuições", + industryCreateBlueprintButton: "Criar blueprint", + industryDistribute: "Distribuir", + industryDistributeButton: "Distribuir por quotas", + industryDistribution: "Distribuição", + industryEcoAmount: "Quantia (ECO)", + industryEditBlueprint: "Editar blueprint", + industryFacility: "Fábrica", + industryKindLabel: "Tipo", + industryKind_labor: "Mão de obra", + industryKind_material: "Material", + industryKind_eco: "Fundos", + industryLaborHours: "Mão de obra (horas)", + industryHours: "Horas", + industryMaterialItem: "Material", + industryMaterials: "Materiais", + industryEstMaterials: "Total materiais", + industryEstLabor: "Total mão de obra", + industryEstTotal: "Preço estimado", + industryBuildingPrice: "Preço de construção", + industryFinalPrice: "Preço final", + industryBlueprintDescPlaceholder: "Especificações, dimensões, peso, documentação…", + industryMaterialValue: "Valor (ECO)", + industryMember: "Membro", + industryNewBlueprint: "Novo blueprint", + industryNewBuild: "Propor um build", + industryEditBuild: "Editar produção", + industryNeedBlueprint: "Cria primeiro um plano: cada produção fabrica um.", + industryNoBlueprints: "Ainda não há blueprints.", + industryNoBuilds: "Ainda não há builds.", + industryNote: "Nota", + industryOutput: "Saída", + industryOutputKind: "Tipo de produto", + industryOutputValue: "Valor de saída (ECO, ex. da venda)", + industryPostJob: "Criar Emprego", + industryPot: "Bolo", + industryProposeBuildButton: "Propor build", + industryProposer: "Proponente", + industryAuthor: "Autor", + industrySellOnMarket: "Enviar ao Mercado", + industrySendToProjects: "Criar Projeto", + industrySkills: "Competências", + industryTreasury: "Tesouraria", + industryKind_physical: "Físico", + industryKind_digital: "Digital", + industryBuildStatus_PROPOSED: "PROPOSTO", + industryBuildStatus_REJECTED: "REJEITADO", + industryBuildStatus_APPROVED: "APROVADO", + industryBuildStatus_STOCKING: "ABASTECENDO", + industryBuildStatus_IN_PRODUCTION: "EM PRODUÇÃO", + industryBuildStatus_COMPLETED: "CONCLUÍDO", + industryBuildStatus_FAILED: "FALHADO", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Um emprego corresponde ao teu currículo", + jobsBotMatchIntro: "Um emprego corresponde ao teu currículo.", + jobsBotMatchJob: "Emprego", + jobsBotMatchHint: "Recebes isto porque o teu currículo é gerido por IA. Podes desativar no teu CV.", + cvAiManaged: "Gerido por IA", + switchOn: "LIGADO", + switchOff: "DESLIGADO", + cvAiManagedHint: "Se ativo, o JobsBot avisa-te por mensagem privada quando um emprego corresponde ao teu currículo.", + cvMatchThreshold: "Avisar a partir deste nível de correspondência", + housingBotRequestedTitle: "Alguém pediu um dos teus lugares.", + housingBotCancelledTitle: "Um pedido sobre um dos teus lugares foi cancelado.", + housingBotYouRequestedTitle: "Pediste um lugar.", + housingBotYouCancelledTitle: "Cancelaste um pedido.", + housingBotUnavailableTitle: "Um lugar que pediste já não está disponível.", + housingBotUnavailableSentTitle: "Avisaste quem pediu este lugar.", + industryBotAdmittedTitle: "Você foi admitido numa fábrica.", + industryBotApplicationTitle: "Nova solicitação de adesão na sua fábrica.", + industryBotInvitedTitle: "Você foi convidado para uma fábrica.", + industryBotDissolvedTitle: "Uma fábrica a que pertence foi dissolvida.", + industryBotBuildApprovedTitle: "Um build foi aprovado.", + industryBotDistributedTitle: "A saída de um build foi distribuída.", + industryFilterRules: "REGRAS", + industryRulesTitle: "Regras", + industryRulesIntro: "Industry permite aos habitantes possuir coletivamente os meios de produção: uma Fábrica é uma oficina da rede que ninguém possui de forma privada.", + industryRulesSteward: "O fundador é o administrador — um guardião, não um dono: não pode vender, privatizar nem dissolver a Fábrica sozinho.", + industryRulesMembership: "A política de adesão define como se entra: Aberta (na hora), Por voto (admitido pelo voto dos membros) ou Apenas convite (um membro deve convidar primeiro).", + industryRulesQuorum: "O quórum é o número mínimo de membros que devem votar para uma decisão contar, para que uma Fábrica pequena não seja decidida por um só habitante.", + industryRulesMajority: "A maioria é a fração de votos necessária (0,5 = metade, 1 = unanimidade); uma decisão passa quando os SIM atingem pelo menos max(quórum, membros × maioria).", + industryRulesDecisions: "Os membros votam para admitir novos, aprovar builds e dissolver a Fábrica; o administrador não pode ignorar estas decisões coletivas.", + industryRulesBlueprints: "Os Blueprints são receitas livres (COPYLEFT) — as entradas (materiais, horas de trabalho, competências) e a saída (um bem físico ou digital) — e qualquer um pode forká-las.", + industryRulesBuilds: "Um Build é uma ordem de produção: PROPOSTO → APROVADO (por voto) → ABASTECENDO → EM PRODUÇÃO → CONCLUÍDO, e só aceita contribuições depois de aprovado.", + industryRulesContributions: "Os membros contribuem com trabalho (horas), materiais (valor ECO declarado) ou ECO a um build; cada contribuição ganha karma.", + industryRulesLaborRate: "A tarifa de trabalho é o valor ECO de uma hora de trabalho nesta Fábrica; converte horas em karma para comparar esforço e capital de forma justa: karma = horas × tarifa + valor material + ECO.", + industryRulesShares: "A tua quota de um build é o teu karma ÷ karma total, e define quanto do valor de saída recebes.", + industryRulesDistribution: "Quando uma produção é concluída, o steward distribui o fundo (os fundos da produção mais o valor de venda) entre os contribuidores, proporcionalmente às suas quotas.", + industryRulesTreasury: "As contribuições em ECO são guardadas pelo administrador como tesouraria do build até à distribuição; todos os movimentos ficam registados na rede e as disputas podem ir aos Courts.", + industryJobs: "Empregos", + industryReportButton: "Denunciar", + industryCreateTask: "Criar Tarefa", + industryOpenDispute: "Abrir disputa", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "Gerar convite", + industryPauseVotes: "Votos de estado", + industryTitle: "Indústria", + industryDescription: "Módulo para gerir os meios de produção de forma coletiva.", + typeIndustryBuild: "PRODUÇÃO", + typeIndustryBlueprint: "PLANO", + typeIndustryAllocation: "DISTRIBUIÇÃO", + typeIndustry: "INDÚSTRIA", + modulesIndustryLabel: "Indústria", + modulesIndustryDescription: "Módulo para gerir os meios de produção de forma coletiva.", + industryFilterAll: "TODAS", + industryFilterRecent: "RECENTES", + industryFilterMember: "MEMBRO", + industryFilterBlueprints: "PLANOS", + industryFilterBuilds: "PRODUÇÕES", + industryFilterMine: "MINHAS", + industryFilterActive: "EM FUNCIONAMENTO", + industryFilterPaused: "PAUSADAS", + industryFilterDissolved: "DISSOLVIDAS", + industryAllTitle: "Indústria", + industryRecentTitle: "Fábricas recentes", + industryMemberTitle: "Fábricas de que faço parte", + industryMineTitle: "Minhas fábricas", + industryActiveTitle: "Em funcionamento", + industryPausedTitle: "Fábricas pausadas", + industryDissolvedTitle: "Fábricas dissolvidas", + industryNoFacilitiesFound: "Nenhuma fábrica encontrada.", + industryCreateFacility: "Criar fábrica", + industryMembers: "Membros", + industryMemberBadge: "MEMBRO", + industryName: "Nome", + industryNamePlaceholder: "Nome da fábrica", + industryDescriptionLabel: "Descrição", + industryDescriptionPlaceholder: "O que esta fábrica produz?", + industrySector: "Setor", + industryMembershipPolicy: "Política de adesão", + industryQuorum: "Quórum", + industryMajority: "Maioria", + industryLaborRate: "Tarifa de trabalho", + industryCreateButton: "Criar fábrica", + industryUpdateButton: "Atualizar", + industrySteward: "Administrador", + industryStatusActive: "EM FUNCIONAMENTO", + industryStatusPaused: "PAUSADA", + industryStatusDissolved: "DISSOLVIDA", + industryStatusLabel: "Estado", + industryJoinButton: "Juntar-se", + industryApplyButton: "Pedir para entrar", + industryLeaveButton: "Sair", + industryInviteButton: "Convidar", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Precisas de um convite para entrar.", + industryPauseButton: "Votar pausar", + industryResumeButton: "Votar retomar", + industryDeleteButton: "Eliminar", + industryPendingApplicants: "Candidaturas pendentes", + industryVotesYes: "sim", + industryVotesNo: "não", + industryAlreadyVoted: "votado", + industryAdmit: "Admitir", + industryReject: "Rejeitar", + industryDissolveTitle: "Dissolver fábrica", + industryDissolveVote: "Votar dissolução", + industryPolicy_open: "Aberta", + industryPolicy_vote: "Por voto", + industryPolicy_invite: "Apenas convite", projectsTitle: "Projetos", projectsDescription: "Cria, financia e acompanha projetos comunitários na tua rede.", + projectSearchPlaceholder: "Pesquisar em Projetos...", projectCreateProject: "Criar projeto", projectCreateButton: "Criar projeto", projectUpdateButton: "ATUALIZAR", @@ -2923,7 +3086,7 @@ module.exports = { projectRecentTitle: "Projetos recentes", projectTopTitle: "Mais financiados", projectTitlePlaceholder: "Nome do projeto", - projectImage: "Carregar mídia (máx: 50MB)", + projectImage: "Carregar mídia (máx: 75MB)", projectDescription: "Descrição", projectDescriptionPlaceholder: "Conta a história e os objetivos…", projectGoal: "Objetivo (ECO)", @@ -2944,14 +3107,8 @@ module.exports = { projectPledgeTitle: "Apoiar este projeto", projectPledgePlaceholder: "Montante em ECO", projectBounties: "Recompensas", - projectBountiesInputLabel: "Recompensas (uma por linha: Título|Montante [ECO]|Descrição)", - projectBountiesPlaceholder: "Corrigir bug UI|100|Link para o problema\nEscrever docs|250|Exemplos de utilização", projectNoBounties: "Sem recompensas encontradas.", projectTitle: "Título", - projectAddBountyTitle: "Nova recompensa", - projectBountyTitle: "Título da recompensa", - projectBountyAmount: "Montante (ECO)", - projectBountyDescription: "Descrição", projectMilestoneSelect: "Selecionar marco", projectBountyCreateButton: "Criar recompensa", projectBountyStatus: "Estado da recompensa", @@ -2966,19 +3123,15 @@ module.exports = { projectMilestoneTitle: "Título do marco", projectMilestoneTargetPercent: "Percentagem (%)", projectMilestoneDueDate: "Data", - projectMilestoneCreateButton: "Criar marco", projectMilestoneStatus: "Estado do marco", projectMilestoneOpen: "aberto", projectMilestoneDone: "concluído", projectMilestoneDue: "prazo", - projectNoMilestones: "Sem marcos encontrados.", - projectMilestoneMarkDone: "Mark as Done", + projectMilestoneMarkDone: "Marcar como concluído", projectMilestoneTitlePlaceholder: "Introduz o título do marco", projectMilestoneDescriptionPlaceholder: "Introduz a descrição para este marco", projectMilestoneDescription: "Descrição do marco", - projectBudgetGoal: "Orçamento (objetivo)", projectBudgetAssigned: "Atribuído a recompensas", - projectBudgetRemaining: "Restante", projectBudgetOver: "Acima do orçamento: atribuído excede o objetivo", projectFollowers: "Seguidores", projectFollowersTitle: "Seguidores", @@ -2991,7 +3144,6 @@ module.exports = { projectBackersTotalPledged: "Total comprometido", projectBackersYourPledge: "O teu compromisso", projectBackersNone: "Ainda sem compromissos.", - projectNoRemainingBudget: "Sem orçamento restante.", projectFilterBackers: "APOIANTES", projectFilterApplied: "CANDIDATADO", projectAppliedTitle: "CANDIDATADO", @@ -3000,36 +3152,19 @@ module.exports = { projectBackerAmount: "Total contribuído", projectBackerPledges: "Compromissos", projectBackerProjects: "Projetos", - projectPledgeAmount: "Montante", projectSelectMilestoneOrBounty: "Selecionar marco ou recompensa", projectPledgeButton: "Comprometer", footerLicense: "GPLv3", - footerPackage: "Pacote", - footerVersion: "Versão", modulesModuleName: "Nome", modulesModuleDescription: "Descrição", modulesModuleStatus: "Estado", - modulesTotalModulesLabel: "Módulos carregados", + modulesTotalModulesLabel: "Total", modulesEnabledModulesLabel: "Ativado", modulesDisabledModulesLabel: "Desativado", - modulesPopularLabel: "Destaques", - modulesPopularDescription: "Módulo para receber publicações em tendência, mais vistas ou mais comentadas.", - modulesTopicsLabel: "Tópicos", - modulesTopicsDescription: "Módulo para receber categorias de discussão baseadas em interesses comuns.", - modulesSummariesLabel: "Resumos", - modulesSummariesDescription: "Módulo para receber resumos de discussões ou publicações longas.", - modulesLatestLabel: "Mais recentes", - modulesLatestDescription: "Módulo para receber as publicações e discussões mais recentes.", - modulesThreadsLabel: "Conversas", - modulesThreadsDescription: "Módulo para receber conversas agrupadas por tópico ou questão.", - modulesMultiverseLabel: "Multiverso", - modulesMultiverseDescription: "Módulo para receber conteúdo de outros pares federados.", modulesInvitesLabel: "Convites", modulesInvitesDescription: "Módulo para gerir e aplicar códigos de convite.", modulesWalletLabel: "Carteira", modulesWalletDescription: "Módulo para gerir os teus ativos digitais (ECOin).", - modulesLegacyLabel: "Chaves", - modulesLegacyDescription: "Módulo para gerir o teu segredo (chave privada) de forma rápida e segura.", modulesCipherLabel: "Encriptação", modulesCipherDescription: "Módulo para encriptar e decifrar o teu texto simetricamente (com senha partilhada).", modulesBookmarksLabel: "Marcadores", @@ -3064,8 +3199,12 @@ module.exports = { modulesOpinionsDescription: "Módulo para descobrir e votar em opiniões.", modulesTransfersLabel: "Transferências", modulesTransfersDescription: "Módulo para descobrir e gerir contratos inteligentes (transferências).", - modulesFediverseLabel: "Fediverse", - modulesFediverseDescription: "Faça a gestão das suas outras contas do fediverso, incluindo enviar e receber conteúdo.", + modulesFediverseLabel: "Multiverso", + modulesBlogsLabel: "Blogs", + modulesBlogsDescription: "Módulo para descobrir e gerir blogs.", + modulesPollsLabel: "Sondagens", + modulesPollsDescription: "Módulo para perguntar à rede e contar as respostas.", + modulesFediverseDescription: "Faça a gestão das suas outras contas do multiverso, incluindo enviar e receber conteúdo.", modulesFeedLabel: "Feed", modulesFeedDescription: "Módulo para descobrir e partilhar textos curtos (feeds).", modulesParliamentLabel: "Parlamento", @@ -3078,7 +3217,7 @@ module.exports = { modulesMelodyDescription: "Módulo para gerar e partilhar o \"som\" da tua blockchain.", modulesAgendaLabel: "Agenda", modulesAgendaDescription: "Módulo para gerir todos os teus itens atribuídos.", - modulesAILabel: "AI", + modulesAILabel: "IA", modulesAIDescription: "Módulo para conversar com um LLM chamado '42'.", modulesForumLabel: "Fóruns", modulesForumDescription: "Módulo para descobrir e gerir fóruns.", @@ -3087,52 +3226,51 @@ module.exports = { modulesProjectsLabel: "Projetos", modulesProjectsDescription: "Módulo para explorar, financiar coletivamente e gerir projetos.", modulesBankingLabel: "Banca", - modulesBankingDescription: "Módulo para determinar o valor real do ECOIN e distribuir um UBI usando o tesouro comum.", + modulesBankingDescription: "Módulo para gerir o tesouro comum: valor do ECOIN, impostos e RBU.", modulesFavoritesLabel: "Favoritos", modulesFavoritesDescription: "Módulo para gerir o teu conteúdo favorito.", fileTooLargeTitle: "Ficheiro demasiado grande", - fileTooLargeMessage: "O ficheiro excede o tamanho máximo permitido (50 MB). Por favor, seleciona um ficheiro mais pequeno.", + fileTooLargeMessage: "O ficheiro excede o tamanho máximo permitido (75 MB). Por favor, seleciona um ficheiro mais pequeno.", goBack: "Voltar", + errorDismiss: "OK", errorPageTitle: "Algo correu mal", + errorNoAllocation: "Nenhuma alocação encontrada para esta reivindicação.", + errorInvalidClaim: "Esta alocação não pode ser reivindicada.", visibilityLabel: "Visibilidade", + clearnetPrivateConflict: "Um conteúdo privado, oculto, fechado, só por convite ou pago não pode ser publicado na CLEARNET. Escolhe OASIS ou torna-o público.", visibilityPublic: "Pública", visibilityHidden: "Oculta", visibilityMakePublic: "Tornar pública", visibilityMakeHidden: "Ocultar", invitesInhabitantsTitle: "Habitantes", - invitesInhabitantsFollow: "Seguir", - aiNavPlaceholder: "Para onde queres ir?", + aiNavPlaceholder: "Descreve o que precisas...", aiNavDisabled: "A navegação por IA está desativada.", - aiNavResultsTitle: "Sugestões de navegação IA", + aiNavResultsTitle: "Sugestões AINav", aiNavQueryLabel: "Consulta", - aiNavResultsDescription: "Escolhe a rota que melhor corresponde ao que procuras.", - aiNavResultPath: "Rota", - aiNavResultDescription: "O que podes fazer", aiNavResultMatch: "Correspondência", aiNavResultsEmpty: "Sem rotas correspondentes. Tenta /search.", - aiNavSearchInstead: "Pesquisar em vez disso", modulesAINavLabel: "AINav", modulesAINavDescription: "Módulo para consultas em linguagem natural sobre o conteúdo da rede.", - directConnect: "Ligação direta", directConnectDescription: "Conecta-te diretamente a um par inserindo o endereço IP, porta e chave pública.", peerHost: "IP", - peerPort: "Porta (predefinida: 8008)", + peerPort: "Porta", peerPublicKey: "Chave pública (@...ed25519)", connectAndFollow: "Conectar", - deviceSourceLabel: "Dispositivo", modulesPresetTitle: "Configurações Comuns", - modulesPreset_minimal: "Mínimo", - modulesPreset_basic: "Básico", - modulesPreset_social: "Social", - modulesPreset_economy: "Economia", - modulesPreset_full: "Completo", + workflowsTitle: "Fluxos de trabalho", + workflowsDescription: "Um workflow define o tema e quais módulos estão ativos.", + workflowsSet: "Aplicar workflow", + workflow_default: "Predefinido", + workflow_jobs: "Emprego", + workflow_social: "Social", + workflow_business: "Negócios", + workflow_night: "Noite", + workflow_kids: "Crianças", + workflow_activists: "Ativismo", + workflow_mobile: "Móvel", statsCarbonFootprintTitle: "Pegada de carbono", - statsCarbonFootprintNetwork: "Pegada de carbono da rede", - statsCarbonFootprintYours: "A tua pegada de carbono", statsCarbonTombstone: "Pegada do tombstoning", - feedSuccessMsg: "Feed publicado com sucesso!", - dominantOpinionLabel: "Opinião dominante", - uploadMedia: "Carregar mídia (máx: 50MB)", + uploadMedia: "Carregar mídia (máx: 75MB)", courtsRespondentInvalid: "Demandado inválido", feedDetailTitle: "Detalhe do feed", @@ -3141,7 +3279,7 @@ module.exports = { noComments: "Ainda sem comentários.", mapsLabel: "Mapas", mapTitle: "Mapas", - mapDescription: "Explore e gerencie mapas offline na sua rede.", + mapDescription: "Gerencie e compartilhe mapas offline na sua rede.", mapMineSectionTitle: "Seus Mapas", mapCreateSectionTitle: "Criar Mapa", mapUpdateSectionTitle: "Atualizar Mapa", @@ -3165,14 +3303,13 @@ module.exports = { mapDescriptionLabel: "Descrição", mapDescriptionPlaceholder: "Descreva o mapa ou localização...", mapTypeLabel: "Tipo de mapa", - mapTypeSingle: "ÚNICO (apenas localização inicial)", - mapTypeOpen: "ABERTO (qualquer um pode adicionar marcadores)", - mapTypeClosed: "FECHADO (apenas o criador adiciona marcadores)", + mapInviteMode: "Convite", + mapTypeSingle: "ÚNICO", + mapTypeOpen: "ABERTO", + mapTypeClosed: "FECHADO", mapTagsLabel: "Tags", - mapTagsPlaceholder: "Insira tags separadas por vírgulas", + mapTagsPlaceholder: "Introduz tags separadas por vírgulas", mapUrlLabel: "URL do mapa", - mapPickCoordLabel: "Ou selecione uma localização na grade:", - mapMarkersLabel: "marcadores", mapMarkersTitle: "Marcadores", mapMarkerDefault: "Marcador", mapMarkerLatLabel: "Latitude do marcador", @@ -3182,9 +3319,22 @@ module.exports = { markerImageLabel: "Imagem do Marcador", mapAddMarkerTitle: "Adicionar Marcador", mapAddMarkerButton: "Adicionar Marcador", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "Limpar marcador", mapApplyZoom: "Aplicar Zoom", - mapSearchPlaceholder: "Buscar descrição, tags, autor...", + mapPlaceSearchPlaceholder: "Procurar um lugar ou país...", + mapNoPlaces: "Nenhum lugar encontrado.", + mapClusterTitle: "marcadores aqui, clique para aproximar", + mapDeleteMarker: "Eliminar marcador", + mapZoomIn: "Aproximar", + mapZoomOut: "Afastar", + mapFitMarkers: "Ajustar a todos os marcadores", + mapPanLabel: "Mover o mapa", + mapCenterHere: "Centrar aqui", + mapNear: "perto de", + mapPickedSpot: "Ponto escolhido", + mapPickHint: "Clique no mapa para escolher um ponto.", + mapClearPick: "Limpar", + mapSearchPlaceholder: "Pesquisar em Mapas...", mapSearchButton: "Buscar", mapUpdatedAt: "Atualizado", mapNoMatch: "Nenhum mapa corresponde à sua busca.", @@ -3195,118 +3345,820 @@ module.exports = { typeMapMarker: "MARCADOR DE MAPA", modulesMapLabel: "Mapas", modulesMapDescription: "Módulo para gerenciar e compartilhar mapas offline.", + emergenciesTitle: "Emergências", + modulesEmergenciesLabel: "Emergências", + modulesEmergenciesDescription: "Módulo para lançar, confirmar e seguir emergências na rede.", + emergenciesDescription: "Módulo para lançar, confirmar e seguir emergências na rede.", + emergencyCreate: "Criar emergência", + emergencyUpdate: "Atualizar", + emergencyDelete: "Apagar", + emergencyTitleLabel: "Título", + emergencyTitlePlaceholder: "O que está a acontecer?", + emergencyTextLabel: "Detalhes", + emergencyTextPlaceholder: "Onde, desde quando, o que fazer...", + emergencyCategoryLabel: "Categoria", + emergencyCategoryWeather: "METEOROLOGIA", + emergencyCategoryInfrastructure: "INFRAESTRUTURA", + emergencyCategoryHealth: "SAÚDE", + emergencyCategorySecurity: "SEGURANÇA", + emergencyCategoryLost: "PERDIDOS E ACHADOS", + emergencyCategoryNeighborhood: "VIZINHANÇA", + emergencyExpiresLabel: "Expira em", + emergencyExpires1d: "1 dia", + emergencyExpires3d: "3 dias", + emergencyExpires7d: "1 semana", + emergencyExpires30d: "1 mês", + emergencyExpiresNone: "Indefinido", + emergencyExpiresKeep: "Manter a data atual", + emergencyTagsLabel: "Etiquetas", + emergencyTagsPlaceholder: "Introduz tags separadas por vírgulas", + emergencyFilterAll: "TODOS", + emergencyFilterMine: "MEUS", + emergencyFilterRecent: "RECENTES", + emergencySeverityUnverified: "NÃO VERIFICADO", + emergencySeverityLow: "BAIXA", + emergencySeverityMedium: "MÉDIA", + emergencySeverityHigh: "ALTA", + emergencyStatusActive: "ATIVO", + emergencyStatusResolved: "RESOLVIDO", + emergencyStatusExpired: "EXPIRADO", + emergencyConfirmations: "Confirmações", + emergencyConfirmButton: "Confirmar", + emergencyConfirmed: "Confirmaste esta emergência", + emergencyResolveButton: "Resolver", + emergencyReopenButton: "Reabrir", + emergencyGenerateReport: "Gerar relatório", + emergencyUpdateEdited: "Editada", + emergencyLastUpdate: "ÚLTIMA ATUALIZAÇÃO", + emergencyUpdatesTitle: "Últimas atualizações:", + emergencyNoUpdates: "Ainda sem atualizações.", + emergencyAddUpdate: "Publicar atualização", + emergencyUpdatePlaceholder: "O que mudou?", + emergencyNoItems: "Nenhuma emergência encontrada.", + emergencyExpiresAt: "Expira", + emergencyBannerLabel: "Emergência ativa", + emergencyCreateSectionTitle: "Criar emergência", + emergencyUpdateSectionTitle: "Atualizar emergência", + emergencySearchPlaceholder: "Pesquisar em emergências...", + typeEmergency: "EMERGÊNCIAS", + typeEmergencyConfirm: "CONFIRMAÇÃO DE EMERGÊNCIA", + typeEmergencyUpdate: "ATUALIZAÇÃO DE EMERGÊNCIA", + statsEmergency: "Emergências", + statsEmergencyConfirm: "Confirmações de emergências", + statsEmergencyUpdate: "Atualizações de emergências", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "Uma emergência que segues tem uma atualização.", + emergencyBotResolvedTitle: "Uma emergência que segues foi resolvida.", + dataKindEmergencies: "EMERGÊNCIAS", + mailingTitle: "Listas de correio", + modulesMailingLabel: "Listas de correio", + modulesMailingDescription: "Descobre e gere listas de correio na tua rede.", + modulesPhoneLabel: "Telefone", + modulesPhoneDescription: "Módulo para ligar a outros habitantes com videochamadas cifradas de ponta a ponta.", + mailingDescription: "Descobre e gere listas de correio na tua rede.", + mailingCreate: "Criar lista de correio", + mailingUpdate: "Atualizar", + mailingArchive: "Arquivar", + mailingActivate: "Ativar", + mailingCreateSectionTitle: "Criar lista de correio", + mailingUpdateSectionTitle: "Atualizar lista de correio", + mailingTitleLabel: "Título", + mailingTitlePlaceholder: "Nome da lista de correio", + mailingDescriptionLabel: "Descrição", + mailingDescriptionPlaceholder: "Sobre o que é esta lista?", + mailingTypeLabel: "Tipo de lista de correio", + mailingTypeOpen: "ABERTA", + mailingTypeClosed: "FECHADA", + mailingStatusLabel: "Estado", + mailingStatusActive: "ATIVA", + mailingStatusArchived: "ARQUIVADA", + mailingMembersLabel: "Convidar habitantes", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Subscritores", + mailingTagsLabel: "Etiquetas", + mailingTagsPlaceholder: "Introduz tags separadas por vírgulas", + mailingFilterAll: "TODAS", + mailingFilterMine: "MINHAS", + mailingFilterSubscribed: "SUBSCRITAS", + mailingFilterRecent: "RECENTES", + mailingHistoryDate: "DATA", + mailingHistoryThreads: "TÓPICOS", + mailingHistoryInhabitants: "HABITANTES", + mailingHistorySearchPlaceholder: "Pesquisar no histórico...", + mailingNoMessages: "Ainda não há mensagens.", + mailingNoItems: "Nenhuma lista de correio encontrada.", + mailingSearchPlaceholder: "Pesquisar em listas de correio...", + mailingWriteButton: "Escrever mensagem", + mailingWriteTitle: "Escrever à lista", + mailingReplyTitle: "Responder à lista", + mailingSend: "Enviar", + mailingTextPlaceholder: "A tua mensagem chega a todos os participantes da lista.", + mailingParticipants: "Participantes", + inboxFilterMailing: "Listas de correio", + mailingReplyPlaceholder: "Responder à lista...", + mailingReplyToList: "Responder à lista", + typeMailingList: "LISTA DE CORREIO", + statsMailingList: "Listas de correio", + mailingListLabel: "LISTA DE CORREIO", + logisticsTitle: "Logística", + modulesLogisticsLabel: "Logística", + modulesLogisticsDescription: "Descobre e gere planos logísticos na tua rede.", + logisticsDescription: "Descobre e gere planos logísticos na tua rede.", + logisticsCreate: "Criar rota", + logisticsUpdate: "Atualizar", + logisticsClose: "Fechar", + logisticsReopen: "Reabrir", + logisticsCreateSectionTitle: "Criar rota", + logisticsUpdateSectionTitle: "Atualizar rota", + logisticsTitleLabel: "Título", + logisticsTitlePlaceholder: "Viagem ao mercado, carrinha disponível...", + logisticsKindLabel: "Tipo de logística", + logisticsKindTrip: "VIAGEM", + logisticsKindShipment: "ENVIO", + logisticsKindTrips: "VIAGENS", + logisticsKindShipments: "ENVIOS", + logisticsModeLabel: "Modo", + logisticsModeOffer: "OFEREÇO", + logisticsModeRequest: "PROCURO", + logisticsModeOffers: "OFERTAS", + logisticsModeRequests: "PEDIDOS", + logisticsDescriptionLabel: "Descrição", + logisticsDescriptionPlaceholder: "Ponto de encontro, o que levas, condições...", + logisticsOriginLabel: "Origem", + logisticsOriginPlaceholder: "Onde começa", + logisticsDestinationLabel: "Destino", + logisticsDestinationPlaceholder: "Onde termina", + logisticsDateLabel: "Data e hora", + logisticsRecurrenceLabel: "Recorrência", + logisticsFrequencyLabel: "Frequência", + logisticsRecurrenceNone: "ÚNICA", + logisticsRecurrenceDaily: "DIÁRIA", + logisticsRecurrenceWeekly: "SEMANAL", + logisticsRecurrenceMonthly: "MENSAL", + logisticsSeatsLabel: "Lugares", + logisticsSizeLabel: "Tamanho", + logisticsSizePlaceholder: "p. ex. 2m x 1m", + logisticsWeightLabel: "Peso", + logisticsWeightPlaceholder: "p. ex. 40kg", + logisticsPriceFree: "GRÁTIS", + logisticsPriceEco: "ECO", + logisticsPriceTime: "TEMPO", + logisticsPriceLabel: "Preço", + logisticsHours: "h", + logisticsOrderRefLabel: "Ligação ao pedido", + logisticsOrderRefPlaceholder: "/shops/... ou /market/... do pedido a entregar", + logisticsOrderLink: "Pedido ligado", + logisticsTagsLabel: "Etiquetas", + logisticsTagsPlaceholder: "Introduz tags separadas por vírgulas", + logisticsFilterAll: "TODAS", + logisticsFilterMine: "MINHAS", + logisticsFilterBooked: "RESERVADAS", + logisticsFilterRecent: "RECENTES", + logisticsFilterHistory: "HISTÓRICO", + logisticsFilterTop: "TOP", + logisticsStatusOpen: "ABERTA", + logisticsStatusUpcoming: "PRÓXIMAS", + logisticsStatusPast: "PASSADAS", + logisticsStatusClosed: "FECHADAS", + logisticsZoneLabel: "Zona", + logisticsSearchPlaceholder: "Pesquisar em logística...", + logisticsNoItems: "Nenhuma rota encontrada.", + logisticsBookingPending: "PENDENTE", + logisticsBookingConfirmed: "CONFIRMADA", + logisticsBookingRejected: "RECUSADA", + logisticsBookingCancelled: "CANCELADA", + logisticsBookingDelivered: "ENTREGUE", + logisticsConfirm: "Confirmar", + logisticsReject: "Recusar", + logisticsDeliver: "Entregar", + logisticsReceiptLabel: "Recibo", + logisticsReceiptPlaceholder: "Nota de entrega (opcional)", + logisticsCancelBooking: "Cancelar reserva", + logisticsBookTitle: "Reservar esta rota", + logisticsNotesLabel: "Notas", + logisticsNotesPlaceholder: "O que o responsável deve saber", + logisticsBook: "Reservar", + logisticsBookingsTitle: "Reservas", + logisticsMyBookingTitle: "A tua reserva", + logisticsRatingsTitle: "Avaliações", + logisticsNoRatings: "Ainda não há avaliações.", + logisticsRatingsWhenClosed: "As avaliações abrem quando a rota fecha.", + logisticsScoreLabel: "Pontuação", + logisticsRatingPlaceholder: "Como correu?", + logisticsRate: "Avaliar", + typeLogisticsRoute: "ROTA", + typeLogisticsRating: "AVALIAÇÃO DE ROTA", + statsLogisticsRoute: "Rotas", + statsLogisticsRating: "Avaliações de rotas", + logisticsRouteLabel: "ROTA", + podcastsTitle: "Podcasts", + modulesPodcastsLabel: "Podcasts", + modulesPodcastsDescription: "Módulo para publicar canais de podcast.", + podcastsDescription: "Módulo para publicar canais de podcast.", + podcastCreate: "Criar podcast", + podcastUpdate: "Atualizar", + podcastDelete: "Eliminar", + podcastCreateSectionTitle: "Criar podcast", + podcastUpdateSectionTitle: "Atualizar podcast", + podcastTitleLabel: "Título", + podcastTitlePlaceholder: "Nome do canal", + podcastDescriptionLabel: "Descrição", + podcastDescriptionPlaceholder: "Sobre o que é este podcast?", + podcastCategoryLabel: "Categoria", + podcastCategoryNews: "NOTÍCIAS", + podcastCategoryMusic: "MÚSICA", + podcastCategoryTalk: "CONVERSA", + podcastCategoryEducation: "EDUCAÇÃO", + podcastCategoryTech: "TECNOLOGIA", + podcastCategoryCulture: "CULTURA", + podcastCategoryCommunity: "COMUNIDADE", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "OUTROS", + podcastTagsLabel: "Etiquetas", + podcastCoverLabel: "Capa (imagem ou vídeo)", + podcastEpisodeAudioLabel: "Ficheiro de áudio ou vídeo (máx: 75MB)", + podcastTagsPlaceholder: "Introduz tags separadas por vírgulas", + podcastFilterAll: "TODOS", + podcastFilterMine: "MEUS", + podcastFilterRecent: "RECENTES", + podcastFilterTop: "TOP", + podcastFilterViewers: "OUVINTES", + podcastSearchPlaceholder: "Pesquisar em podcasts...", + podcastNoItems: "Nenhum podcast encontrado.", + podcastEpisodesLabel: "Episódios", + podcastPlaysLabel: "Visualizações", + podcastLastEpisode: "Último episódio", + podcastEpisodesTitle: "Episódios", + podcastNoEpisodes: "Ainda não há episódios.", + podcastEpisodeAdd: "Adicionar episódio", + podcastEpisodeCreateTitle: "Publicar episódio", + podcastEpisodeUpdateTitle: "Atualizar episódio", + podcastEpisodeTitleLabel: "Título do episódio", + podcastEpisodeTitlePlaceholder: "Título do episódio", + podcastEpisodeDescriptionLabel: "Notas", + podcastEpisodeDescriptionPlaceholder: "O que acontece neste episódio, ligações, créditos...", + podcastEpisodePublish: "Publicar", + podcastPlayed: "Reproduzido", + podcastMarkPlayed: "Marcar como reproduzido", + typePodcast: "PODCAST", + typePodcastEpisode: "EPISÓDIO", + typePodcastOpinion: "OPINIÃO DO EPISÓDIO", + typePodcastPlay: "REPRODUÇÃO", + statsPodcast: "Podcasts", + statsPodcastEpisode: "Episódios", + statsPodcastPlay: "Reproduções", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "Um blog que segues tem uma nova entrada.", + podcastBotEpisodeTitle: "Um podcast que segues tem um novo episódio.", + podcastLabel: "PODCAST", + podcastEpisodeLabel: "EPISÓDIO", + campaignsTitle: "Campanhas", + modulesCampaignsLabel: "Campanhas", + modulesCampaignsDescription: "Descobre e gere campanhas na tua rede.", + campaignsDescription: "Descobre e gere campanhas na tua rede.", + campaignCreate: "Criar campanha", + campaignUpdate: "Atualizar", + campaignDelete: "Eliminar", + campaignClose: "Fechar", + campaignReopen: "Reabrir", + campaignCreateSectionTitle: "Criar campanha", + campaignUpdateSectionTitle: "Atualizar campanha", + campaignTitleLabel: "Título", + campaignTitlePlaceholder: "O que queres alcançar?", + campaignTextLabel: "Texto", + campaignTextPlaceholder: "Explica a causa, quem decide e o que pedes...", + campaignCategoryLabel: "Categoria", + campaignCategoryEnvironment: "AMBIENTE", + campaignCategoryRights: "DIREITOS", + campaignCategoryHealth: "SAÚDE", + campaignCategoryEducation: "EDUCAÇÃO", + campaignCategoryInfrastructure: "INFRAESTRUTURA", + campaignCategoryCulture: "CULTURA", + campaignCategoryEconomy: "ECONOMIA", + campaignCategoryOther: "OUTROS", + campaignGoalLabel: "Objetivo de assinaturas", + campaignDeadlineLabel: "Prazo", + campaignTagsLabel: "Etiquetas", + campaignTagsPlaceholder: "Introduz tags separadas por vírgulas", + campaignFilterAll: "TODAS", + campaignFilterMine: "MINHAS", + campaignFilterSigned: "ASSINADAS", + campaignFilterRecent: "RECENTES", + campaignFilterTop: "TOP", + campaignStatusOpen: "ABERTA", + campaignStatusAchieved: "ALCANÇADA", + campaignStatusUnachieved: "NÃO ALCANÇADA", + campaignStatusClosed: "FECHADA", + campaignSearchPlaceholder: "Pesquisar em campanhas...", + campaignNoItems: "Nenhuma campanha encontrada.", + campaignSignaturesLabel: "Assinaturas", + campaignSignaturesTitle: "Apoiantes", + campaignNoSignatures: "Ainda não há assinaturas.", + campaignSignTitle: "Assinar esta campanha", + campaignSignPlaceholder: "Porque a apoias? (opcional)", + campaignSign: "Assinar", + campaignSigned: "Já assinaste!", + campaignNotSigned: "Ainda falta a tua assinatura", + campaignElevate: "Levar ao Parlamento", + campaignElevated: "Levada ao Parlamento", + campaignUpdatesTitle: "Atualizações", + campaignNoUpdates: "Ainda não há atualizações.", + campaignUpdatePlaceholder: "O que aconteceu?", + campaignAddUpdate: "Publicar atualização", + campaignQrAlt: "QR para assinar esta campanha", + campaignQrCaption: "Digitaliza para assinar", + typeCampaign: "CAMPANHA", + typeCampaignSignature: "ASSINATURA", + typeCampaignUpdate: "ATUALIZAÇÃO DE CAMPANHA", + statsCampaign: "Campanhas", + statsCampaignSignature: "Assinaturas de campanhas", + statsCampaignUpdate: "Atualizações de campanhas", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "Uma campanha que segues tem uma atualização.", + campaignBotAchievedTitle: "Uma campanha que segues atingiu o objetivo.", + campaignBotRaisedTitle: "Uma campanha que assinaste foi levada ao Parlamento.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Uma rota que reservaste mudou.", + logisticsBotClosedTitle: "Uma rota que reservaste foi fechada.", + logisticsBotBookedTitle: "Alguém reservou a tua rota.", + logisticsBotCancelledTitle: "Uma reserva na tua rota foi cancelada.", + logisticsBotConfirmedTitle: "A tua reserva foi confirmada: o pacote está a caminho.", + logisticsBotRejectedTitle: "A tua reserva foi rejeitada.", + logisticsBotDeliveredTitle: "O teu pacote chegou.", + campaignLabel: "CAMPANHA", + campaignGenerateReport: "Gerar relatório", + logisticsGenerateReport: "Gerar relatório", + mailingGenerateReport: "Gerar relatório", + campaignUpdateEdited: "Editada", + mailingMessagesLabel: "Mensagens", + logisticsBookingsLabel: "Reservas", + backupTitle: "Cópia de segurança", + modulesBackupLabel: "Cópia de segurança", + modulesBackupDescription: "Módulo para exportar e importar as tuas chaves e fazer cópias de segurança cifradas.", + backupDescription: "Módulo para exportar e importar as tuas chaves e fazer cópias de segurança cifradas.", + backupTypeRecovery: "RECUPERAÇÃO", + backupTypeKeys: "EXPORTAR CHAVES", + backupTypeFull: "CÓPIA COMPLETA", + backupTypeRestore: "RESTAURAR CÓPIA", + backupTypeInstant: "INSTANTÂNEA", + backupInstantDescription: "Um .zip simples de todo o teu registo e ficheiros, sem a tua chave secreta e sem palavra-passe. Rápido, mas ", + backupInstantRisk: "quem tiver o ficheiro lê tudo.", + backupKitPageDescription: "O kit de recuperação só pode ser aberto a partir deste dispositivo.", + backupKitId: "ID Oasis", + backupKitDate: "Gerado", + backupKitQrAlt: "Código QR da tua chave secreta", + backupKitStep1: "Imprime-o ou copia-o à mão e guarda-o num lugar seguro e offline.", + backupKitStep2: "Para recuperar, instala o Oasis num dispositivo novo e importa este segredo em Cópia de segurança › Chaves.", + backupKitStep3: "Destrói esta página se alguma vez ficar exposta.", + backupKitPdfButton: "Gerar PDF", + backupScopeLabel: "Âmbito", + backupScopeAll: "TUDO", + backupScopeMine: "SÓ O MEU CONTEÚDO", + backupSinceLabel: "Desde", + backupEstimateTitle: "Estimativa", + backupFileInfo: "A tua cópia cifrada será descarregada para o teu dispositivo (nome: oasis-backup-DATA.oasisbk)", + backupExportButton: "Descarregar cópia", + backupRestorePassword: "Escreve a palavra-passe usada ao fazer a cópia", + backupRestoreButton: "Restaurar cópia", + backupRestoredLine: "Cópia restaurada.", + backupRestoredMessages: "Mensagens adicionadas", + backupRestoredSkipped: "Já existentes", + backupRestoredBlobs: "Ficheiros adicionados", + backupRestoredState: "Configurações restauradas", + backupRestoredFailed: "Falhados", + backupRestoredForked: "Divergentes", + backupRestoreRunning: "Restaurando a cópia. Esta página atualiza-se sozinha; entretanto podes continuar a usar o Oasis.", + backupRestoreElapsed: "Decorrido", + backupRestoreFailedLine: "Não foi possível restaurar a cópia.", + backupRestoreForkMine: "Este dispositivo já publicou mensagens com a tua identidade que não estão na cópia, por isso não foi possível aplicá-la por cima. Restaura numa instalação limpa logo após importar as tuas chaves, antes de fazer qualquer outra coisa.", + verificationTitle: "Verificação", + verificationDescription: "Verifica a cadeia do teu feed, deteta bifurcações e revê ficheiros órfãos ou em falta.", + verificationRun: "Executar verificação", + verificationTotalMessages: "mensagens no registo", + verificationFeed: "O teu feed", + verificationMessages: "mensagens", + verificationLastSequence: "última sequência", + verificationGaps: "Lacunas", + verificationBrokenLinks: "Ligações quebradas", + verificationBadHashes: "Hashes incorretos", + verificationBadSignatures: "Assinaturas incorretas", + verificationDuplicates: "Sequências duplicadas", + verificationForks: "Bifurcações", + verificationBlobs: "Ficheiros", + verificationBlobsPresent: "presentes", + verificationBlobsReferenced: "referenciados", + verificationBlobsOrphan: "órfãos", + verificationBlobsMissing: "em falta", + verificationBlobsMissingOwn: "Ficheiros teus em falta neste dispositivo", + verificationBlobsMissingOthers: "Ficheiros de outros habitantes ainda por descarregar", + verificationBlobsUnavailable: "Não foi possível listar os ficheiros neste dispositivo.", + wikiTitle: "Wikis", + modulesWikiLabel: "Wikis", + modulesWikiDescription: "Módulo para descobrir e gerir wikis.", + wikiDescription: "Módulo para descobrir e gerir wikis.", + wikiCreate: "Criar wiki", + wikiUpdate: "Atualizar", + wikiDelete: "Apagar", + wikiEdit: "Editar", + wikiTitleLabel: "Título", + wikiTitlePlaceholder: "Título da wiki...", + wikiBodyLabel: "Conteúdo", + wikiBodyPlaceholder: "Liga outras wikis com [[Título da wiki]] ou [[Título da wiki|um texto próprio]].\n\nMenciona um habitante com @id.\n\nEtiqueta com #oasis.\n\nCola ligações como https://solarnethub.com\n\nUma linha em branco abre um novo parágrafo; as quebras de linha simples mantêm-se.\n\nAnexa qualquer media suportado e será incorporado aqui.", + formatBarLabel: "Formato", + wikiStatusLabel: "Tipo de wiki", + wikiStatusOpen: "ABERTA", + wikiStatusClosed: "FECHADA", + wikiTagsLabel: "Etiquetas", + wikiTagsPlaceholder: "Introduz tags separadas por vírgulas", + wikiSummaryLabel: "Resumo da alteração", + wikiSummaryPlaceholder: "O que mudou?", + wikiFilterAll: "TODAS", + wikiFilterMine: "MINHAS", + wikiFilterRecent: "RECENTES", + wikiFilterLinked: "LIGADAS", + wikiChanges: "Alterações recentes", + wikiNoPages: "Nenhuma wiki encontrada.", + wikiNoChanges: "Ainda sem alterações.", + wikiHistory: "Histórico", + wikiVersion: "Versão", + wikiVersions: "versões", + wikiRestore: "Restaurar", + wikiDiff: "Alterações", + wikiViewVersion: "Ver", + wikiLinkedPages: "Páginas ligadas", + wikiNoLinkedPages: "Esta página ainda não está ligada a nenhuma outra.", + wikiMissingLink: "A página ainda não existe", + wikiDuplicateTitle: "Já existe uma página com este título. Muda o título para criar uma página diferente.", + wikiCreateSectionTitle: "Criar wiki", + wikiUpdateSectionTitle: "Atualizar wiki", + wikiSearchPlaceholder: "Pesquisar na Wiki...", + wikiCurrentVersion: "Versão atual", + wikiViewingVersion: "Estás a ver uma versão antiga", + wikiBackToPage: "Voltar à página", + wikiLinkedChip: "Ligada", + wikiRestoredSummary: "Versão restaurada", + wikiExportPdf: "Gerar página", + wikiPreview: "Pré-visualizar", + tribeSectionWiki: "WIKI", + tribeWikiCreate: "Criar wiki", + tribeWikiEmpty: "Ainda sem wikis.", + typeWiki: "WIKI", padsTitle: "Pads", padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Módulo para gerir editores de texto colaborativos.", - padFilterAll: "TODOS", - padFilterMine: "MEU", - padFilterRecent: "RECENTE", - padFilterOpen: "ABERTO", - padFilterClosed: "FECHADO", padCreate: "Criar Pad", - padUpdate: "Atualizar Pad", - padDelete: "Eliminar Pad", + padUpdate: "Atualizar", + padDelete: "Eliminar", padTitleLabel: "Título", padTitlePlaceholder: "Escreva o título do pad...", padStatusLabel: "Estado", + padTypeLabel: "Tipo de pad", padStatusOpen: "ABERTO", padStatusInviteOnly: "APENAS POR CONVITE", padStatusClosed: "FECHADO", padDeadlineLabel: "Prazo", padTagsLabel: "Tags", - padTagsPlaceholder: "tag1, tag2, ...", + padTagsPlaceholder: "Introduz tags separadas por vírgulas", padMembersLabel: "Membros", - padVisitPad: "Visitar Pad", - padShareUrl: "Partilhar URL", padCreated: "Criado", - padAuthor: "Autor", - padGenerateCode: "Gerar Código", - padInviteCodeLabel: "Código de Convite", - padInviteCodePlaceholder: "Introduza o código de convite...", - padValidateInvite: "Validar", - padStartEditing: "COMEÇAR A EDITAR!", + padStartEditing: "Editar", padEditorPlaceholder: "Comece a escrever...", padSubmitEntry: "Enviar", padNoEntries: "Sem entradas ainda.", padAllSectionTitle: "Todos os Pads", padMineSectionTitle: "Os Meus Pads", padsDescription: "Gere editores de texto colaborativos cifrados na tua rede.", + roomsTitle: "Salas", + roomsDescription: "Descobre e entra em salas de videoconferência cifradas na tua rede.", + modulesRoomsLabel: "Salas", + modulesRoomsDescription: "Módulo para descobrir e entrar em salas de videoconferência cifradas.", + roomFilterRecent: "RECENTES", + roomFilterMine: "MINHAS", + roomFilterAll: "TODAS", + roomFilterLive: "EM DIRETO", + roomFilterOpen: "ABERTAS", + roomFilterInvite: "APENAS POR CONVITE", + roomFilterClosed: "FECHADAS", + roomCreate: "Criar sala", + roomUpdate: "Atualizar", + roomDelete: "Eliminar", + roomClose: "Fechar", + roomReopen: "Reabrir", + roomCreateSectionTitle: "Criar sala", + roomUpdateSectionTitle: "Atualizar sala", + roomTitlePlaceholder: "Título da sala", + roomDescriptionLabel: "Descrição", + roomDescriptionPlaceholder: "Sobre o que vão falar?", + roomTypeLabel: "Tipo de sala", + roomTagsLabel: "Tags", + roomStatusOpen: "ABERTA", + roomStatusInviteOnly: "APENAS POR CONVITE", + roomStatusClosed: "FECHADA", + roomParticipants: "Participantes", + roomJoin: "Entrar", + roomLeave: "Sair", + roomFull: "Sala cheia", + roomLiveChip: "EM DIRETO", + roomMuted: "silenciado", + roomRec: "Gravar áudio", + roomRecStop: "Parar a gravação", + roomStreamFile: "Transmitir", + roomStreamStop: "Parar", + roomStreaming: "A transmitir", + liveFullscreen: "Ecrã inteiro", + roomStreamPickTitle: "Escolhe um vídeo", + roomStreamPickHint: "Vídeos deste dispositivo. O que escolheres é transmitido a partir de onde está, sem ser copiado nem publicado.", + roomStreamGo: "Transmitir na sala", + roomStreamNotFound: "Esse vídeo não está nas tuas pastas.", + roomPublicWarning: "Esta sala é transmitida na web pública: qualquer pessoa pode ver e ouvir o que nela acontece.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} entrou na sala", + roomEventLeft: "{name} saiu da sala", + roomEventRecStart: "{name} começou a gravar", + roomEventRecStop: "{name} parou de gravar", + roomEventMuted: "{name} silenciou-se", + roomEventUnmuted: "{name} tirou o silêncio", + roomEventHand: "{name} levantou a mão", + roomRaiseHand: "Levantar a mão", + roomLowerHand: "Baixar a mão", + confirmActionTitle: "Confirma esta ação", + confirmActionButton: "Confirmar", + confirmJoinText: "Vais juntar-te usando este convite.", + confirmFollowText: "Vais apoiar este habitante.", + confirmSignText: "Vais assinar esta campanha.", + confirmDownloadText: "Vais transferir este ficheiro para o teu dispositivo.", + roomRecordings: "Gravações", + roomClearNotices: "Limpar", + roomLineSwitchboard: "Central", + roomLineDndError: "O número desta sala não aceita chamadas.", + roomNumberHint: "Marque este número a partir do Telefone para entrar na sala.", + liveRefresh: "Atualizar", + roomNobody: "Neste momento não está ninguém na sala.", + roomSomeone: "{n} na sala neste momento.", + roomYou: "tu", + roomYouAreIn: "Está dentro", + roomNobodyInside: "Neste momento não há ninguém nesta sala.", + roomErrorFull: "A sala está cheia. Podes entrar quando alguém sair.", + roomErrorUnreachable: "Neste momento não é possível chegar à sala. Tenta mais tarde.", + roomErrorBusy: "Já estás numa chamada ou noutra sala.", + roomErrorClosed: "Esta sala está fechada.", + roomAccessDenied: "Esta sala é apenas por convite. Precisas de um convite para entrar.", + roomsNoItems: "Não foram encontradas salas.", + roomSearchPlaceholder: "Procurar salas...", + roomInviteMode: "Convite", + tribeSectionRooms: "SALAS", + tribeRoomCreate: "Criar sala", + tribeRoomsEmpty: "Ainda não há salas.", + typeRoom: "SALAS", + typeRoomMember: "PARTICIPANTE DE SALA", + statsRoom: "Salas", + statsRoomMember: "Participantes de salas", + favoritesFilterRooms: "SALAS", + invitesRoomsTitle: "Salas", + inviteCodeInvalid: "Este código de convite não é válido ou já foi usado.", + inviteAlreadyMember: "Já fazes parte. Podes abri-lo a partir do seu módulo.", + actionFailed: "Não foi possível fazê-lo. Tenta de novo.", + publishTooShort: "A tua publicação é demasiado curta.", + walletSendFailed: "Não foi possível enviar o pagamento: {reason}", + pixeliaCoordsError: "Essas coordenadas estão fora da grelha.", + courtsErrTitle: "É necessário um título.", + courtsErrRespondent: "Indica contra quem é o caso.", + courtsErrRespondentId: "Esse não é um Oasis ID válido.", + courtsErrMethod: "Escolhe como o caso será resolvido.", + courtsErrCaseNotFound: "Esse caso não foi encontrado.", + courtsErrBrief: "Escreve a tua resposta.", + courtsErrStance: "Escolhe a tua posição.", + courtsErrResult: "Escreve o resultado.", + courtsErrTerms: "Escreve os termos do acordo.", + courtsErrMediator: "Adiciona pelo menos um mediador.", + courtsErrSelfMediator: "Não podes ser mediador no teu próprio caso.", + courtsErrJudge: "Escolhe um juiz.", + courtsErrSelfJudge: "Não podes ser juiz do teu próprio caso.", + courtsErrVisibility: "Escolhe quem pode ver o caso.", + courtsErrNomination: "Essa nomeação não foi encontrada.", + opinionAlreadyGiven: "Já deste a tua opinião.", + cipherPasswordShort: "A palavra-passe deve ter pelo menos 32 caracteres.", + errorForbidden: "Não tens acesso a isto.", + errorNotFound: "Não foi encontrado.", + errorClosed: "Está fechado.", + errorInviteOnly: "Isto é apenas por convite. Precisas de um convite.", + errorTooLong: "Esse texto é demasiado longo.", + errorRequired: "Falta informação obrigatória.", + errorInvalid: "Parte da informação não é válida.", + errorNotAuthor: "Só quem o criou pode fazer isto.", + panicRemoved: "A tua blockchain foi eliminada.", padRecentSectionTitle: "Pads Recentes", padOpenSectionTitle: "Pads Abertos", padClosedSectionTitle: "Pads Fechados", padCreateSectionTitle: "Criar Novo Pad", padUpdateSectionTitle: "Atualizar Pad", - padInviteGenerated: "Código de Convite Gerado", typePad: "PADS", padNew: "NOVO", padAddFavorite: "Adicionar aos Favoritos", padRemoveFavorite: "Remover dos Favoritos", - padClose: "Fechar Pad", + padClose: "Fechar", padBackToEditor: "Voltar ao editor", - padSearchPlaceholder: "Pesquisar pads...", + padSearchPlaceholder: "Pesquisar em Pads...", modulesChatsLabel: "Chats", - modulesChatsDescription: "Módulo para descobrir e gerir chats cifrados.", + modulesChatsDescription: "Módulo para descobrir e gerir chats.", typeChat: "CHATS", typeChatMessage: "MENSAGEM CHAT", - chatLabel: "CHATS", - chatMessageLabel: "MENSAGENS CHAT", chatsTitle: "Chats", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "Os teus chats", + chatRecentTitle: "Chats recentes", chatFavoritesTitle: "Favoritos", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "Chats abertos", + chatClosedTitle: "Chats fechados", chatDescription: "Descrição", chatCategory: "Categoria", chatStatus: "ESTADO", - chatFilterAll: "TODOS", - chatFilterMine: "MEU", - chatFilterRecent: "RECENTE", - chatFilterFavorites: "FAVORITOS", - chatFilterOpen: "ABERTO", - chatFilterClosed: "FECHADO", chatCreate: "Criar Chat", - chatUpdate: "Atualizar Chat", - chatDelete: "Eliminar Chat", + chatUpdate: "Atualizar", + chatDelete: "Apagar", chatClose: "Fechar Chat", chatVisitChat: "VER CHAT", chatUntitled: "Chat sem título", chatNoItems: "Nenhum chat encontrado.", chatParticipants: "Participantes", - chatStartChatting: "COMEÇAR A CONVERSAR!", - chatGenerateCode: "Gerar Código", - chatShareUrl: "Partilhar URL", + chatMessagesLabel: "Mensagens", + chatThreadsLabel: "Tópicos", chatCreatedAt: "CRIADO", - chatSearchPlaceholder: "Pesquisar chats...", + chatSearchPlaceholder: "Pesquisar em Chats...", chatStatusOpen: "ABERTO", chatStatusInviteOnly: "SÓ POR CONVITE", chatStatusClosed: "FECHADO", chatSendMessage: "Enviar", + chatJumpLatest: "Última Mensagem", + chatReply: "Responder", + chatReplyCancel: "Cancelar resposta", + chatPin: "Fixar", + chatUnpin: "Desafixar", + chatPinned: "Fixados", + chatPickChat: "Escolhe um chat para o abrir", + chatNoneYet: "Ainda não há nenhum chat disponível.", + activitySearchPlaceholder: "Pesquisar em Atividade...", + trendingSearchPlaceholder: "Pesquisar em Tendências...", + opinionsSearchPlaceholder: "Pesquisar em Opiniões...", + votesSearchPlaceholder: "Pesquisar em Votações...", + gamesSearchPlaceholder: "Pesquisar em Jogos...", + schoolTitle: "Escola", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "Uma página wiki que segues foi editada.", + wikiBotRestoredTitle: "Foi restaurada uma versão anterior de uma página wiki que segues.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Um novo estudante se inscreveu no seu curso.", + schoolBotInvitedTitle: "Você foi convidado para um curso.", + schoolBotAdmittedTitle: "Você foi admitido em um curso.", + schoolBotCertificateTitle: "Você recebeu um certificado.", + schoolBotPassedTitle: "Um estudante foi aprovado no seu curso.", + schoolBotLessonTitle: "Uma nova lição foi publicada.", + invitesSchoolTitle: "Escola", + schoolEnrollLabel: "Juntar-se ao curso", + schoolExamRetryLater: "Você poderá tentar novamente após 24 horas.", + schoolApproved: "APROVADO", + schoolExamPassMark: "Nota de aprovação (0-10)", + schoolExamPassed: "APROVADO", + schoolExamPending: "Pendente", + schoolExamFailed: "REPROVADO", + schoolExamAddQuestion: "Adicionar pergunta", + schoolExamCreateQuestion: "Criar pergunta", + schoolExamOption: "Opção", + schoolExamCorrect: "Opção correta", + schoolExamPoints: "Pontos", + schoolMaterialTextPlaceholder: "Notas para esta lição...", + schoolMaterials: "Materiais", + schoolAddMaterial: "Adicionar material", + schoolNoMaterials: "Ainda não há materiais.", + bankYourSchoolBalance: "Seus ganhos da escola", + schoolProgress: "Progresso", + schoolProgressMark: "Marcar como concluída", + schoolProgressDone: "Concluída", + schoolExams: "Exames", + schoolExamCreate: "Criar exame", + schoolExamTitlePlaceholder: "Título do exame", + schoolExamHelp: "Uma pergunta por linha: Pergunta? ; opção ; opção correta* ; opção", + schoolExamQuestionsPlaceholder: "O que é SSB? ; Um banco de dados ; Um protocolo P2P* ; Um navegador", + schoolExamSubmit: "Enviar respostas", + schoolExamScore: "Sua nota", + schoolNoScoresYet: "Ainda não há resultados.", + schoolLessonUnit: "Unidade / fase", + schoolLessonUnitPlaceholder: "ex. Fase 1", + schoolLessonOrder: "Ordem", + profileClearnetSchoolLabel: "Escola", + schoolStartDate: "Data de início", + schoolSessionDate: "Data da sessão", + schoolCourseChat: "Chat do curso", + agendaFilterSchool: "ESCOLA", + agendaFilterCampaigns: "CAMPANHAS", + agendaFilterLogistics: "LOGÍSTICA", + schoolCourseType: "Tipo de curso", + schoolTypeOpen: "ABERTO", + schoolTypePaid: "PAGO", + schoolTypeInvite: "SÓ POR CONVITE", + schoolLessonLocked: "Lição cifrada — peça a chave do curso ao professor.", + schoolGrantAccess: "Enviar chave do curso", + schoolKeySent: "Chave enviada", + schoolOngoing: "Em curso", + schoolFilterApplied: "INSCRITO", + schoolFilterOpen: "ABERTOS", + schoolFilterFavorites: "FAVORITOS", + schoolPendingPayment: "Pagamento pendente", + schoolPendingStudents: "Inscrições pendentes", + schoolViewBill: "Ver fatura", + schoolPaymentNote: "A inscrição gera uma fatura em ECO (uma transferência). Sua inscrição é confirmada quando a transferência é assinada.", + schoolCloseCourse: "Fechar curso", + schoolReopenCourse: "Reabrir curso", + schoolDescription: "Ensine e aprenda cursos na sua rede.", + schoolFilterAll: "TODOS", + schoolFilterMine: "MEUS", + schoolFilterTop: "TOP", + schoolFilterRecent: "RECENTES", + schoolCreateButton: "Criar Curso", + schoolCreateSectionTitle: "Criar um curso", + schoolUpdateSectionTitle: "Atualizar curso", + schoolCourseTitlePlaceholder: "Nome do seu curso", + schoolCourseDescriptionPlaceholder: "O que você vai ensinar neste curso?", + schoolCourseImage: "Imagem do curso (tamanho máx: 75MB)", + schoolTags: "Tags", + schoolTagsPlaceholder: "Introduz tags separadas por vírgulas", + schoolPrice: "Preço (ECO)", + schoolVisibility: "Visibilidade", + schoolVisibilityPublic: "Público", + schoolVisibilityInvite: "Por convite", + schoolStatus: "Estado", + schoolClosed: "Fechado", + schoolFree: "Grátis", + schoolCreate: "Criar curso", + schoolUpdate: "Atualizar curso", + schoolSearchPlaceholder: "Pesquisar em Escola...", + schoolSortTitle: "Título", + schoolNoCourses: "Nenhum curso encontrado.", + schoolStudents: "Estudantes", + schoolTeacher: "Professor", + schoolCreatedAt: "CRIADO", + schoolLessons: "Lições", + schoolNoLessons: "Ainda não há lições.", + schoolEnrollToSee: "Inscreva-se para ver as lições deste curso.", + schoolEnrollButton: "INSCREVER-SE", + schoolUnenrollButton: "Abandonar curso", + schoolInviteRequired: "Este curso é por convite.", + schoolTeacherPanel: "Painel do professor", + schoolStudentPanel: "Painel do estudante", + schoolAddLesson: "Criar lição", + schoolLessonTitlePlaceholder: "Título da lição", + schoolLessonTextPlaceholder: "Descreva as noções abordadas nesta lição...", + schoolInviteStudents: "Convidar estudantes", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "CONVIDAR", + schoolInvited: "Convidados", + schoolIssueCertificate: "Criar certificado", + schoolCertified: "Certificado", + schoolCertificateTextPlaceholder: "Nota do certificado (opcional)", + schoolLessonOpen: "Abrir lição", + schoolCertificatePdf: "PDF", + schoolCertificates: "Certificados", + schoolNoCertificates: "Nenhum certificado emitido ainda.", + schoolEditButton: "Editar", + schoolDeleteButton: "Apagar", + typeSchool: "ESCOLA", + schoolCourseLabel: "CURSO", + emergencyLabel: "EMERGÊNCIA", + wikiPageLabel: "WIKI", + statsSchoolCourse: "Cursos", + statsSchoolLesson: "Lições", + statsSchoolEnroll: "Inscrições", + statsSchoolCertificate: "Certificados da escola", + modulesSchoolLabel: "Escola", + modulesSchoolDescription: "Módulo para trocar conhecimento.", + welcomeStepUxAction: "Guardar vista", + welcomeStepWorkflowTitle: "Que uso do OASIS tens em mente?", + welcomeStepWorkflowText: "O Oasis tem muitos módulos. Escolhe a configuração que combina contigo e só os úteis ficam ativados. Podes ajustá-la quando quiseres em Módulos.", + welcomeStepWorkflowAction: "Guardar configuração", + welcomeWorkflow_full: "Predefinido", + chatRateLimitTitle: "Demasiadas mensagens", + chatRateLimitMessage: "Atingiste o limite de mensagens que esta sala aceita numa hora. Tenta mais tarde.", chatMessagePlaceholder: "Escreva a sua mensagem...", chatNoMessages: "Sem mensagens ainda.", - chatLeave: "Sair do Chat", - chatInviteCodeLabel: "Introduza o código de convite", - chatJoinByInvite: "Aderir", chatTitlePlaceholder: "Título do chat", chatDescriptionPlaceholder: "Descrição do chat", - chatTagsPlaceholder: "tag1, tag2, tag3", + chatTagsPlaceholder: "Introduz tags separadas por vírgulas", chatImageLabel: "Seleciona um ficheiro de imagem (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "Código de Convite", chatAuthor: "Autor", - chatCreated: "Criado", chatAddFavorite: "Adicionar aos Favoritos", chatRemoveFavorite: "Remover dos Favoritos", chatPM: "MP", chatStatusLabel: "Estado", + chatTypeLabel: "Tipo de chat", chatCategoryLabel: "Categoria", - chatParticipantsLabel: "Participantes", gamesTitle: "Jogos", gamesDescription: "Descubra e jogue alguns jogos.", gamesFilterAll: "TODOS", gamesPlayButton: "JOGAR!", - gamesBackToGames: "Voltar aos Jogos", modulesGamesLabel: "Jogos", modulesGamesDescription: "Módulo para descobrir e jogar alguns jogos.", modulesGraphosLabel: "Graphos", @@ -3323,8 +4175,6 @@ module.exports = { gamesArkanoidDesc: "Quebre todos os tijolos com sua raquete e bola. Um clássico desafio arcade.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong clássico contra uma IA. O primeiro a 5 pontos ganha.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "Corrida contra o tempo! Desvie do tráfego e chegue à meta antes do tempo acabar.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilote sua nave por um campo de asteroides mortais. Destrua-os antes que te atinjam.", gamesRockPaperScissorsTitle: "Pedra Papel Tesoura", @@ -3334,68 +4184,57 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "Lance uma moeda e aposte em cara ou coroa. Qual é a sua sorte?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "Bilhar visto de cima. Clica para apontar e mantém premido para carregar a força. Mete todas as bolas com o menor número de tacadas.", + gamesArtilleryTitle: "Artilharia", + gamesArtilleryDesc: "Aponta o canhão, tem em conta o vento e acerta no alvo. 5 rondas: ganha quem fizer menos disparos.", + gamesLabyrinthTitle: "Labirinto", + gamesLabyrinthDesc: "Foge do labirinto antes de ficares sem movimentos. Cada nível é maior e mais difícil.", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "Come todos os pontos e evita os fantasmas. Os fantasmas movem-se em tempo real — não pares!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesAudioPendulumTitle: "Pêndulo sonoro", + gamesAudioPendulumDesc: "Simulador de física caótica com síntese de áudio em tempo real. As velocidades angulares tornam-se frequências e os picos, batidas de percussão. Não há duas simulações que soem iguais.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Mistura visuais ao vivo em dois pratos: escolhe um padrão para cada um, ajusta velocidade, cor e zoom, faz a transição entre ambos e lança flashes ao ritmo. Marca o tempo ou deixa o microfone comandar a pulsação.", + gamesTetrisDesc: "Os clássicos blocos que caem. Completa linhas para pontuar. Quanto tempo aguentas?", + gamesMinesweeperTitle: "Caça-minas", + gamesMinesweeperDesc: "A clássica caça às minas. Limpa o tabuleiro para subir de nível — grelhas maiores, mais minas, mais pontuação.", + gamesFilterScoring: "PONTUAÇÕES", + gamesHallOfFame: "Hall da fama", + gamesHallPlayer: "Jogador", + gamesHallScore: "Pontuação", + gamesNoScores: "Ainda não há pontuações.", calendarsTitle: "Calendários", calendarTitle: "Calendário", modulesCalendarsLabel: "Calendários", modulesCalendarsDescription: "Módulo para descobrir e gerir calendários.", typeCalendar: "CALENDÁRIOS", - calendarFilterAll: "TODOS", - calendarFilterMine: "OS MEUS", - calendarFilterOpen: "ABERTO", - calendarFilterClosed: "FECHADO", - calendarFilterRecent: "RECENTES", - calendarFilterFavorites: "FAVORITOS", calendarCreate: "Criar calendário", calendarUpdate: "Atualizar", - calendarDelete: "Eliminar", calendarTitleLabel: "Título", calendarTitlePlaceholder: "Título do calendário...", calendarStatusLabel: "Estado", + calendarTypeLabel: "Tipo de calendário", calendarStatusOpen: "ABERTO", calendarStatusClosed: "FECHADO", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "Prazo", calendarTagsLabel: "Etiquetas", - calendarTagsPlaceholder: "etiqueta1, etiqueta2...", + calendarTagsPlaceholder: "Introduz tags separadas por vírgulas", calendarParticipantsLabel: "Participantes", - calendarParticipantsCount: "Participantes", - calendarVisitCalendar: "Visitar calendário", calendarCreated: "Criado", - calendarAuthor: "Autor", calendarJoin: "Participar", calendarGenerateInvite: "Gerar convite", - calendarInviteCodePlaceholder: "Digite o código de convite...", - calendarValidateInvite: "Validar código", - calendarJoined: "Inscrito", - calendarAddDate: "Adicionar data", - calendarAddNote: "Adicionar nota", calendarDateLabel: "Data", calendarDatePlaceholder: "Descrever esta data...", - calendarNoteLabel: "Nota", + calendarNoteLabel: "Notas", calendarNotePlaceholder: "Adicionar uma nota...", calendarFirstDateLabel: "Data", calendarFirstNoteLabel: "Notas", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "Intervalo", + calendarIntervalNone: "Sem repetição", + calendarIntervalWeekly: "Semanal", + calendarIntervalMonthly: "Mensal", + calendarIntervalYearly: "Anual", calendarFormDescription: "Descrição", calendarNoDates: "Nenhuma data adicionada.", calendarNoNotes: "Nenhuma nota.", @@ -3403,8 +4242,8 @@ module.exports = { calendarsDescription: "Descubra e gerencie calendários na sua rede.", calendarMonthPrev: "← Anterior", calendarMonthNext: "Seguinte →", - calendarMonthLabel: "Datas", - calendarsShareUrl: "URL de partilha", + calendarMonthNames: "Janeiro,Fevereiro,Março,Abril,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro", + calendarDayInitials: "S,T,Q,Q,S,S,D", calendarAllSectionTitle: "Calendários", calendarRecentSectionTitle: "Calendários Recentes", calendarFavoritesSectionTitle: "Favoritos", @@ -3414,11 +4253,10 @@ module.exports = { calendarCreateSectionTitle: "Criar novo calendário", calendarUpdateSectionTitle: "Atualizar calendário", calendarAddFavorite: "Adicionar aos favoritos", - calendarDeleteNote: "Delete", + calendarDeleteNote: "Eliminar", calendarRemoveFavorite: "Remover dos favoritos", - calendarSearchPlaceholder: "Pesquisar calendários...", + calendarSearchPlaceholder: "Pesquisar em Calendário...", calendarAddEntry: "Adicionar Entrada", - calendarLeave: "Sair do Calendário", statsCalendar: "Calendários", statsCalendarDate: "Datas de calendário", statsCalendarNote: "Notas de calendário", @@ -3429,6 +4267,146 @@ module.exports = { tribeContentAccessDenied: "Acesso Negado", tribeContentAccessDeniedMsg: "Este conteúdo pertence a uma tribo. Você deve ser membro para acessá-lo.", tribeViewTribes: "Ver Tribos", + tribeFilesEmpty: "Ainda sem ficheiros.", + filesLabel: "Ficheiros", + profileClearnetFilesLabel: "Ficheiros", + filesTitle: "Ficheiros", + filesDescription: "Explore e gerencie ficheiros na sua rede.", + fileAllSectionTitle: "Ficheiros", + fileMineSectionTitle: "Seus Ficheiros", + fileRecentSectionTitle: "Ficheiros Recentes", + fileTopSectionTitle: "Melhores Ficheiros", + fileFavoritesSectionTitle: "Favoritos", + fileFilterAll: "TODOS", + fileFilterMine: "MEUS", + fileFilterRecent: "RECENTES", + fileFilterTop: "MELHORES", + fileFilterFavorites: "FAVORITOS", + fileCreateSectionTitle: "Enviar Ficheiro", + fileUpdateSectionTitle: "Atualizar Ficheiro", + fileCreateButton: "Enviar Ficheiro", + fileUpdateButton: "Atualizar", + fileAddFavoriteButton: "Adicionar aos Favoritos", + fileRemoveFavoriteButton: "Remover dos Favoritos", + fileFileLabel: "Seleciona um ficheiro (máx: 75MB)", + calendarFilterRecent: "RECENTES", + calendarFilterAll: "TODOS", + calendarFilterMine: "MEUS", + calendarFilterFavorites: "FAVORITOS", + calendarFilterOpen: "ABERTO", + calendarFilterClosed: "FECHADO", + padFilterRecent: "RECENTES", + padFilterAll: "TODOS", + padFilterMine: "MEUS", + padFilterOpen: "ABERTO", + padFilterClosed: "FECHADO", + chatFilterRecent: "RECENTES", + chatFilterAll: "TODOS", + chatFilterMine: "MEUS", + chatFilterFavorites: "FAVORITOS", + chatFilterOpen: "ABERTO", + chatFilterClosed: "FECHADO", + licenseLabel: "Licença", + phoneTitle: "Telefone", + phoneDescription: "Gere as tuas videochamadas cifradas de ponta a ponta.", + phoneUnavailable: "As chamadas não estão disponíveis neste dispositivo.", + phoneCallButton: "Ligar", + profileVisibilityPhone: "Número de telefone", + phoneNumberUnknown: "Ninguém na tua rede tem o número {number}.", + phoneNumberChoose: "Confirma a quem queres ligar para o {number}:", + phoneAnswer: "Atender", + phoneSpeaker: "Altifalante", + phoneEarpiece: "Auricular", + phoneReject: "Recusar", + phoneHangup: "Desligar", + phoneMute: "Silenciar", + phoneUnmute: "Ativar microfone", + phoneCameraOn: "Câmara on", + phoneCameraOff: "Câmara off", + settingsPhoneVideo: "Vídeo", + settingsPhoneVideoOn: "Permitido em chamadas e salas", + settingsPhoneVideoOff: "Só áudio", + phoneSilence: "Não ouvir", + phoneUnsilence: "Voltar a ouvir", + phoneCalling: "Ligando para", + phoneIsCalling: "está ligando para você", + notifyNewPmFrom: "Nova MP de", + notifyNewPamFrom: "Novo PaM de", + notifyUpdateLabel: "Atualização", + updateInstalledLabel: "Versão instalada", + updateAvailableLabel: "Versão disponível", + updateChangesLabel: "Alterações", + updateFilesLabel: "Arquivos", + updateOriginLabel: "Origem", + phoneRecording: "Gravando PaM para", + phoneLeaveMessage: "Ninguém atende: deixa a tua mensagem depois do sinal.", + phoneLeaveMessageBusy: "Está noutra chamada: deixe a sua mensagem depois do sinal.", + phoneLeaveMessageUnreachable: "Não foi possível contactar: deixe a sua mensagem depois do sinal.", + phoneConnecting: "A ligar a", + phoneSendPam: "Enviar PaM", + phoneCancel: "Cancelar", + phoneSearchPlaceholder: "Pesquisar em Telefone...", + phonePamTitle: "Gravações de áudio", + phoneNew: "Novo", + phoneDelete: "Apagar", + phoneNoPams: "Não tens gravações de áudio.", + phoneHistoryTitle: "Histórico", + phoneLatestTitle: "Últimas chamadas", + phoneCreateButton: "Nova chamada", + phoneLimitsHint: "Até {n} habitantes por chamada; com mais de um é uma chamada conjunta.", + phoneErrorPub: "Não é possível ligar para um pub.", + phoneJointCall: "Chamada conjunta", + phoneWith: "Com", + phoneResult: "Resultado", + phoneWhen: "Quando", + phoneDuration: "Duração", + phoneOutgoing: "Efetuada", + phoneIncoming: "Recebida", + phoneNoHistory: "Ainda não há registos.", + phoneOutcomeEnded: "Atendida", + phoneOutcomeMissed: "Perdida", + phoneOutcomeRejected: "Recusada", + phoneOutcomeNoanswer: "Sem resposta", + phoneOutcomeCancelled: "Cancelada", + phoneOutcomePam: "PaM enviado", + phoneOutcomeFailed: "Falhou", + phoneDialAdd: "Adicionar", + phoneDialBack: "Apagar", + phoneDialClear: "Limpar", + phoneDialPlaceholder: "Número ou Oasis ID", + phoneDialInhabitants: "Habitantes", + phoneDialRemove: "Remover", + phoneOwnNumber: "Esse é o teu próprio número.", + phoneSettingsDesc: "Escolha quem pode ligar para você.", + phoneDnd: "Não incomodar", + phoneOpen: "Aberto", + phoneRecipientRefuses: "O habitante a quem estás a tentar ligar não aceita chamadas.", + phoneRefusedNotice: "Estes habitantes não aceitam chamadas e não foram chamados:", + fileEmptyError: "O ficheiro está vazio. Escolhe outro ficheiro.", + fileTitleLabel: "Título", + fileTitlePlaceholder: "Título", + fileDescriptionLabel: "Descrição", + fileDescriptionPlaceholder: "Descrição", + fileTagsLabel: "Tags", + fileTagsPlaceholder: "Introduz tags separadas por vírgulas", + fileSizeLabel: "Tamanho", + fileDownloadButton: "DESCARREGAR", + fileDetailsButton: "DETALHES", + fileNoFile: "Nenhum arquivo ficheiro", + fileSearchPlaceholder: "Pesquisar em Ficheiros...", + fileSearchButton: "Pesquisar", + fileSortRecent: "Mais recentes", + fileSortOldest: "Mais antigos", + fileSortTop: "Mais votados", + fileNoMatch: "Nenhum ficheiro correspondente.", + fileUpdatedAt: "Atualizado", + typeFile: "FICHEIROS", + modulesFilesLabel: "Ficheiros", + modulesFilesDescription: "Módulo para descobrir e gerir áudios.", + tribeSectionFiles: "FICHEIROS", + fileDescription: "Descrição", + fileDownload: "Baixar", + fileTypeLabel: "Tipo", torrentsTitle: "Torrents", torrentsDescription: "Explore e gerencie torrents na sua rede.", torrentAllSectionTitle: "Torrents", @@ -3454,49 +4432,93 @@ module.exports = { torrentDescriptionLabel: "Descrição", torrentDescriptionPlaceholder: "Descrição", torrentTagsLabel: "Tags", - torrentTagsPlaceholder: "Insira tags separadas por vírgulas", + torrentTagsPlaceholder: "Introduz tags separadas por vírgulas", torrentSizeLabel: "Tamanho", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "Gerar torrent", + torrentFromContentMissing: "O ficheiro ainda não está disponível neste nó. Tenta de novo mais tarde.", + torrentDownloads: "Transferências", + torrentFetchOasis: "Transferir no Oasis", + torrentSaveFile: "Guardar ficheiro", + torrentSeeds: "seeds", + seedsLabel: "Sementes", + seedAction: "Apoiar", + torrentSourceFile: "Ficheiro", + torrentStateQueued: "Em fila", + torrentStateProbing: "À procura de seeds", + torrentStateDownloading: "A transferir", + torrentStateDone: "Concluída", + torrentStateFailed: "Falhou", + torrentStateNoSource: "Sem seeds ligados, à espera", + torrentStateCancelled: "Cancelada", + torrentNoDownloads: "Ainda não há transferências.", + torrentOriginalSection: "Original", + torrentCancel: "Cancelar", + torrentDownloadButton: "DESCARREGAR", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "DETALHES", torrentNoFile: "Nenhum arquivo torrent", noTorrents: "Nenhum torrent encontrado.", - torrentSearchPlaceholder: "Pesquisar título, tags, autor...", + noFiles: "Nenhum ficheiro encontrado.", + torrentSearchPlaceholder: "Pesquisar em Torrents...", torrentSearchButton: "Pesquisar", torrentSortRecent: "Mais recentes", torrentSortOldest: "Mais antigos", torrentSortTop: "Mais votados", torrentNoMatch: "Nenhum torrent correspondente.", - torrentMessageAuthorButton: "Enviar Mensagem ao Autor", torrentUpdatedAt: "Atualizado", statsTorrent: "Torrents", + statsFile: "Ficheiros", typeTorrent: "TORRENTS", modulesTorrentsLabel: "Torrents", modulesTorrentsDescription: "Módulo para descobrir e gerenciar torrents.", favoritesFilterTorrents: "TORRENTS", + favoritesFilterFiles: "FICHEIROS", favoritesFilterMarket: "MERCADO", + favoritesFilterEvents: "EVENTOS", + favoritesFilterForum: "FÓRUM", + favoritesFilterHousing: "HABITAÇÃO", + favoritesFilterJobs: "EMPREGOS", + favoritesFilterProjects: "PROJETOS", + favoritesFilterReports: "RELATÓRIOS", + favoritesFilterTasks: "TAREFAS", + favoritesFilterTransfers: "TRANSFERÊNCIAS", + favoritesFilterVotes: "VOTAÇÕES", favoritesFilterShopProducts: "ITENS DA LOJA", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Enviar Torrent", - tribeMediaTypeTorrent: "Torrent", + tribeCreateFile: "Carregar ficheiro", settingsWishTitle: "Desejo", - settingsWishDesc: "Configure o desejo do seu Avatar.", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "Determina como acedes a todo o conteúdo e a tua experiência na rede Oasis.", + settingsWishWhole: "Multiverso", + settingsWishMutuals: "Só apoio mútuo", + settingsWishOnlyLan: "Só LAN", + settingsWishLocal: "Local", settingsPmVisibilityTitle: "Mensagens Privadas", settingsPmVisibilityDesc: "Configure seu nível de exposição a mensagens privadas.", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "Multiverso", + settingsPmVisibilityMutuals: "Só apoio mútuo", + mute: "Silenciar", + unmute: "Deixar de silenciar", + relationshipMuted: "Silenciado por ti", + hideContent: "Ocultar para mim", + unhideContent: "Mostrar de novo", + hiddenByYou: "Ocultado por ti", + settingsTabContent: "Conteúdo", + settingsHiddenTitle: "Conteúdo oculto", + settingsHiddenDesc: "Habitantes que silenciaste e conteúdo que ocultaste.", + settingsMutedLabel: "Habitantes silenciados", + settingsHiddenLabel: "Conteúdo oculto", + settingsMutedEmpty: "Ninguém silenciado.", + settingsHiddenEmpty: "Nada oculto.", + settingsPmVisibilityFollowing: "Só pessoas que sigo", + settingsMentionsVisibilityTitle: "Menções", + settingsMentionsVisibilityDesc: "Escolhe de quem te chegam as menções.", pmMutualNotice: "O envio é uma barreira de UX. O recebimento é um filtro de atenção.", - pmBlockedNonMutual: "Só pode enviar MPs a habitantes em apoio mútuo.", inhabitantsPendingFollowsTitle: "Pedidos de apoio pendentes", inhabitantsPendingAccept: "Aceitar", inhabitantsPendingReject: "Rejeitar", - bxEncrypted: "CRIPTOGRAFADO", - bxEncryptedHexLabel: "Texto cifrado (pré-visualização)", tribeSectionGovernance: "GOVERNANÇA", - tribeSubStatusPublic: "PÚBLICA", - tribeSubStatusPrivate: "PRIVADA", - tribeSubInheritedPrivate: "PRIVADA (herdada da tribo principal)", tribePadInviteRequired: "Sem acesso ao pad. Peça um convite.", tribeChatInviteRequired: "Sem acesso ao chat. Peça um convite.", tribeGovernanceDesc: "Governança interna desta tribo.", @@ -3535,145 +4557,133 @@ module.exports = { tribeGovNoRevocations: "Ainda nenhuma revogação", tribeGovNoHistorical: "Ainda nenhum registo", logsTitle: "Diário", - logsDescription: "Regista a sua experiência na rede.", - logsReadMore: "Ler mais", + logsDescription: "Mantém um diário privado das tuas experiências.", logsViewTitle: "Diário", logsColumnType: "Tipo", logsView: "Ver", - logsManualPrompt: "Escreva o seu diário", logsUpdateButton: "Atualizar", - logsEditTitle: "Editar entrada", - logsCreateDescription: "Regista a sua experiência...", + logsEditTitle: "Atualizar registo", logsCreateTitle: "Criar entrada", logsWriteButton: "Escrever", logsTextPlaceholder: "Descreva as suas experiências...", - logsTextField: "Texto", - logsLabelPlaceholder: "Rótulo...", - logsLabelField: "Escreva o seu diário (rótulo)", - logsAiDisabledWarn: "Ative o módulo de IA em /modules para usar entradas escritas pela IA.", - logsAiContextValue: "Dia de blockchain", - logsAiContext: "Contexto", - logsAiModStatus: "AImod", - logsModeApply: "Aplicar!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "Assistente IA", logsModeManual: "Manual", logsModeAI: "IA", - logsColumnDelete: "Eliminar", - logsColumnEdit: "Editar", logsColumnLog: "Diário", logsColumnDate: "Data", logsDelete: "Eliminar", - logsEdit: "Editar", + logsEdit: "Atualizar", logsFilterAlways: "SEMPRE", logsFilterYear: "ÚLTIMO ANO", logsFilterMonth: "ÚLTIMO MÊS", logsFilterWeek: "ÚLTIMA SEMANA", logsFilterToday: "HOJE", - logsFilterRecent: "RECENTES", - logsFilterAll: "TODOS", + logsComments: "Comentários", + logsAuthorEntries: "Entradas", logsCreate: "Criar entrada", logsExport: "Exportar diário", - logsExportOne: "Exportar", + logsExportOne: "Exportar registo", logsViewDetails: "Ver detalhes", logsGenerateButton: "Gerar texto", - logsSearchText: "Procurar nos diários...", + logsSearchText: "Pesquisar em Diário...", logsSearchAnyType: "Qualquer tipo", logsSearchButton: "Procurar", logsEmpty: "Sem entradas.", + logsNothingNew: "Não há ações novas para registar.", modulesLogsLabel: "Diário", - modulesLogsDescription: "Módulo para registar (via assistente de IA) as suas experiências.", + modulesLogsDescription: "Módulo para manter um diário privado das tuas experiências.", statsLogsTitle: "Diário", - statsLogsEntries: "Entradas", typeLog: "DIÁRIO", - blockchainCycle: "Ciclo", larpTitle: "L.A.R.P.", larpDescription: "Uma camada de jogo de interpretação ao vivo para experimentação colaborativa.", + larpNoHousesMatch: "Nenhuma casa corresponde à sua pesquisa.", + larpSearchPlaceholder: "Pesquisar em L.A.R.P....", larpAllHouses: "Casas:", larpFilterRuling: "GOVERNA", larpFilterHouses: "CASAS", larpFilterRules: "FAQ", larpRulesTitle: "FAQ do L.A.R.P.", - larpRulesEntryTitle: "Casa inicial", larpRulesEntryText: "Cada habitante começa em ACADEMIA por defeito. A partir de ACADEMIA, escolhe uma casa no painel \"Entrar numa casa\": isto abre o seu teste de entrada. Responde às 10 perguntas e submete. Se passares o limiar (7/10), és automaticamente admitido nessa casa; caso contrário, és rejeitado e permaneces em ACADEMIA. Em qualquer caso o sistema regista o teu OasisID e o ciclo da tentativa e impede nova tentativa até decorrer o período de espera de 30 ciclos.", - larpRulesLeaveText: "Os membros de qualquer casa podem regressar a ACADEMIA a qualquer momento a partir da página da sua casa; isso repõe a sua participação, mas não anula o período de espera do teste.", - larpRulesGovernanceTitle: "Mural de governança", larpRulesGovernanceText: "Durante o seu ciclo, a casa governante exibe a insígnia \"Governa\" e é a única cujo Mural é exibido publicamente no separador GOVERNA.", - larpRulesSignTitle: "Emblema L.A.R.P.", + larpRulesWallText: "Cada casa tem um Muro onde os seus membros escrevem. O Muro de uma casa é privado, exceto o da casa governante e o da ACADEMIA, que qualquer pessoa pode ler.", larpRulesSignText: "Os habitantes podem ativá-lo (em /profile/edit > Sensores) para mostrar a imagem da sua casa atual como um selo no perfil, no cartão de autor e de habitante. O selo liga à página da casa.", larpPostsTitle: "Mural", larpPostsEmpty: "Ainda sem publicações.", - larpPostLabel: "Nova publicação", larpPostPlaceholder: "O que tem esta casa a dizer?", - larpPostPublic: "Público (visível para qualquer pessoa, senão só para membros)", larpPostSubmit: "Publicar", - larpPostMembersOnly: "Apenas membros", larpCycleLabel: "Ciclo:", - audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", - audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", - audioTranscodeStegoNotFound: "No steganographic payload could be decoded from this audio.", - bankRulesAlpha: "Alpha (max share of pub balance per epoch)", + audioTranscodeCompositionEmpty: "Este áudio não inclui nenhuma composição de blockchain guardada.", + audioTranscodeDetailDescription: "Descodifica a carga incorporada e o mapa de composição original da blockchain.", + audioTranscodeStegoNotFound: "Não foi possível descodificar nenhuma carga esteganográfica deste áudio.", + bankRulesAlpha: "Alpha (quota máx. do saldo do pub por época)", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", - bankRulesArchTaxRate: "Rate", - bankRulesCapPerEpoch: "Cap per epoch (absolute)", - bankRulesCapUser: "Cap per user per epoch", - bankRulesCarbonFactor: "Carbon factor", + bankRulesArchTaxNote: "A taxa ARCH reflete o custo a longo prazo de fazer crescer e manter o arquivo da rede: quanto mais antigos forem os teus dados na rede, maior será a tua parte do custo de manutenção.", + bankRulesArchTaxRate: "Tarifa", + bankRulesCapPerEpoch: "Limite por época (absoluto)", + bankRulesCapUser: "Limite por utilizador por época", + bankRulesCarbonFactor: "Fator de carbono", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", + bankRulesChipNote: "O chip de cada elemento é colorido em relação à maior mensagem alguma vez observada na rede e suavizado pelo número de habitantes ativos — uma rede maior distribui a carga e empurra mais elementos para a faixa verde.", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", - bankRulesEcoTaxRate: "Rate", - bankRulesEpochKind: "Epoch granularity", - bankRulesFloor: "Floor per user (gross)", - bankRulesGraceDays: "Claim grace period", + bankRulesEcoTaxNote: "A taxa ECO é deduzida do RBU bruto quando se calcula a atribuição da época. Os ECO deduzidos não são redistribuídos nesta época — ficam no saldo do pub e alimentam o fundo da época seguinte através dos limites reserveMin / alpha.", + bankRulesEcoTaxRate: "Tarifa", + bankRulesEpochKind: "Granularidade da época", + bankRulesFloor: "Mínimo por utilizador (bruto)", + bankRulesGraceDays: "Período de carência da reivindicação", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", + bankRulesKarmaNote: "O karma deriva das tuas ações (publicações, votos, participação no parlamento/tribunais, etc.) com um fator de decaimento temporal, menos os gramas de carbono gerados pelo teu feed. O que o teu conteúdo recebe também conta: opiniões, votos, reações e reproduções positivas somam karma, e opiniões ou votos negativos subtraem-no quando vários habitantes coincidem.", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", - bankRulesReserveMin: "Reserve minimum (kept in pub balance)", + bankRulesReserveMin: "Reserva mínima (mantida no saldo do pub)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankRulesWMax: "Maximum weight (w_max)", - bankRulesWMin: "Minimum weight (w_min)", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", - gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", + bankRulesWMax: "Peso máximo (w_max)", + bankRulesWMin: "Peso mínimo (w_min)", + bankTaxesArchTaxFootprintNote: "A taxa ARCH reflete o custo de fazer crescer e manter o arquivo da rede. É calculada a partir do intervalo de tempo entre o teu primeiro bloco publicado e o bloco mais recente da rede — quanto mais tempo a tua história viver no arquivo, maior será a tua parte do custo de manutenção.", + bankTaxesArchTaxNoteBold: "reduzir diretamente a taxa ARCH", + bankTaxesArchTaxNoteIntro: "A tua taxa ARCH é deduzida do excedente que a rede gera acima do teu RBU; o valor base do RBU é fixado como um mínimo inamovível. A taxa nunca é deduzida do montante fixo que cabe a cada habitante como RBU. Os fundos deduzidos alimentam o algoritmo de redistribuição da riqueza e são canalizados de volta para outros habitantes através das suas reivindicações de RBU. Os habitantes mais recentes e os que se dedicam especificamente a tarefas de arquivo, replicação e manutenção terão provavelmente alguma tarefa dedicada a manter saudável o arquivo da rede em teu nome. Ou podem dedicar-se a ", + bankTaxesArchTaxNoteOutro: " através de acordos de poda, deduplicação de dados redundantes e infraestrutura de manutenção partilhada, e o seu contributo contínuo pode ser exigido pelo consenso da rede.", + bankTaxesUserNoteChips: "O chip da Taxa ECO mostrado junto a cada elemento muda de cor (verde / amarelo / vermelho) consoante o tamanho do elemento em relação ao maior bloco alguma vez visto na rede, suavizado pelo número de habitantes ativos — uma rede solarpunk maior distribui a carga e empurra mais elementos para a faixa verde.", + bankTaxesUserNoteOtherParams: "A taxa ECO deriva atualmente da pegada de carbono de cada bloco, mas pode incorporar outros parâmetros com o tempo — energia gasta na replicação, armazenamento redundante entre pares, largura de banda dos blobs, custo computacional da decifração, pegada de mineração das transações associadas ou qualquer outra carga mensurável que a rede acorde valorizar.", + gamesHallDate: "Data", + gamesNewRecord: "Novo recorde!", + indexingRefreshNote: "Esta página recarrega a cada 10 segundos.", + indexingMessage: "O Oasis está sincronizando a rede distribuída de nós. Por favor, aguarde.", + indexingTitle: "A sincronizar", + padsNoItems: "Nenhum pad encontrado.", + padVersionHistory: "Histórico de versões", + padAttributionView: "Por autora", + padVersionView: "Ver", statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", + statsChatMessage: "Mensagens de chat", + statsGameScore: "Jogos", + statsWiki: "Páginas wiki", statsPad: "Pads", - statsPadEntry: "Pad entries", - typeGameScore: "GAME SCORES", + statsPadEntry: "Entradas de pad", + typeGameScore: "PONTUAÇÕES DE JOGO", bankTaxesLookupNote: "Introduz um ID de bloco para o procurar no teu feed local.", bankTaxesUserNoteChipsClick: "Clica em qualquer chip para inspecionar o seu bloco no blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "Código de convite", larpRulesInviteEntryText: "Os membros das casas podem gerar códigos de convite que dão acesso direto à casa.", larpRulesOneHouseText: "Só podes pertencer a uma casa de cada vez. Se não pertenceres a nenhuma, estás em ACADEMIA. Cada página de casa (não ACADEMIA) mostra um botão \"Sair da Casa\" aos seus membros que reinicia a pertença a ACADEMIA. Sair NÃO cancela o tempo de espera do teste.", - larpRulesOneHouseTitle: "Uma casa de cada vez", - larpRulesTestEntry: "Teste WILL", - larpRulesTestEntryText: "Cada opção pondera uma ou mais casas; ao submeter, o sistema soma os pontos e admite-te automaticamente na casa com a maior pontuação.", + larpRulesTestEntryText: "No teste WILL cada opção pondera uma ou mais casas; ao enviar, o sistema soma as pontuações e admite-te automaticamente na casa com mais pontos.", larpWillTestHint: "Uma vez concluído, ser-te-á atribuída a casa que melhor corresponde às tuas respostas. As tuas respostas individuais são processadas localmente e nunca armazenadas — apenas a atribuição de casa resultante é publicada no teu feed.", larpWillTestTitle: "Teste WILL", - settingsLanDesc: "Anuncia periodicamente este peer a outras instâncias do Oasis na mesma rede local. Desativar para parar as difusões UDP.", + settingsLanDesc: "Anuncia periodicamente este peer a outras instâncias do Oasis na mesma rede local.", settingsLanEnable: "Ativar difusão LAN", settingsLanTitle: "Difusão LAN", settingsReplicationTitle: "Replicação", settingsReplicationDesc: "Configura o número de saltos que o teu peer segue a partir do teu próprio feed ao replicar conteúdo.", settingsReplicationHopsLabel: "Saltos", + settingsBlobCacheTitle: "Cache de média", + settingsBlobCacheDesc: "O conteúdo de outros habitantes fica guardado neste dispositivo. Escolhe quanto espaço pode ocupar.", + settingsBlobCacheUsage: "Utilização atual", + settingsBlobCacheFiles: "ficheiros", + settingsBlobCacheLimit: "Limite", + settingsBlobCacheUnlimited: "Sem limite", + settingsBlobCacheCleanNow: "Limpar agora", + settingsBlobCacheCleaned: "{n} ficheiros removidos, {size} libertados.", aiApproveTagsLabel: "Etiquetas (separadas por vírgulas)", aiApproveTagsPlaceholder: "p.ex. oasis, governança, ecologia", aiApproveRatingLabel: "Avaliação", @@ -3684,36 +4694,28 @@ module.exports = { aiExchangeMarkHelpful: "+1 útil", larpCyclesUntilRuling: "Próximo ciclo de governança", larpVisitTribe: "Visitar tribo", + larpStartJourney: "Começar a jornada", larpGoverning: "Governa:", + larpStartHere: "Começa aqui:", larpMyHouse: "A minha Casa:", larpMembersCount: "Membros", - larpMembersTitle: "Membros", - larpNoMembers: "Ainda sem membros.", larpRolesLabel: "Funções", larpFunctionLabel: "Função", larpMonthLabel: "Ciclo de governança", larpLeaveToAcademia: "Voltar a ACADEMIA", - larpAcademiaJoinTitle: "Entrar numa casa", - larpAcademiaJoinHint: "Faz o teste psicológico de perfil para que te seja atribuída a casa que melhor encaixa contigo, ou usa um código de convite partilhado por um membro de uma casa.", - larpTakeTestButton: "Fazer o teste de perfil", + larpAcademiaJoinTitle: "Casas L.A.R.P.", larpInviteRedeem: "Entrar na Casa", larpInviteCreate: "Gerar código de convite", larpInvitePlaceholder: "Código de convite", larpInviteBannerTitle: "Novo código de convite", larpInviteBannerHint: "Partilha este código com alguém em ACADEMIA. Expira em 30 ciclos ou após um único uso.", - larpTestAssignedHouse: "Casa atribuída", larpTestRankingTitle: "Pontuação por casa", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "Código de convite de casa", - invitesHouseJoinButton: "Entrar na Casa", - ecoTaxLabel: "ECO Tax", - ecoinTaxLabel: "ECOin Tax", + ecoTaxLabel: "Taxa ECO", bankTaxes: "Impostos", - bankOverviewYourTaxes: "Os teus impostos", - bankTaxesNetworkTitle: "Impostos da rede", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesUserTitle: "Os teus impostos", + bankOverviewYourTaxes: "Impostos totais", + bankTaxesEcoTaxTitle: "Taxa ECO", + bankTaxesArchTaxTitle: "Taxa ARCH", bankTaxesUserAmount: "A tua ECO Tax (preço a devolver)", bankTaxesArchTaxUserAmount: "A tua ARCH Tax (preço a devolver)", bankTaxesArchTaxFirstBlock: "Idade do teu primeiro bloco", @@ -3741,7 +4743,7 @@ module.exports = { bankTaxesLookupType: "Tipo", bankTaxesLookupSize: "Tamanho", bankTaxesLookupCarbon: "Pegada de carbono", - bankTaxesLookupEcoin: "ECO Tax", + bankTaxesLookupEcoin: "Taxa ECO", bankExchangeEcoTaxAnnual: "ECOin Taxes (anual)", bankExchangeEcoTaxMonthly: "ECOin Taxes (mensal)", bankTaxesTypesLabel: "Seleciona que impostos queres pagar", @@ -3749,28 +4751,22 @@ module.exports = { bankRulesPoolTitle: "Pool mensal", bankRulesShareTitle: "Parcela por utilizador", bankRulesKarmaTitle: "Karma (pontuação de envolvimento)", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesArchTaxTitle: "ARCH Tax", + bankRulesEcoTaxTitle: "Taxa ECO", + bankRulesArchTaxTitle: "Taxa ARCH", bankRulesChipTitle: "Faixa de cor do chip ECO Tax", - statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Ainda sem tombstones na rede.", blockchainBlockNotAvailable: "Este bloco não está disponível no teu feed local. Pode pertencer a um peer do qual ainda não replicas.", blockchainBackToBlockexplorer: "Voltar ao blockexplorer", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeTitle: "BCS Transcode", + audioTranscodeButton: "TRANSCODIFICAR", audioTranscodeDetailTitle: "Transcodificar", - audioTranscodeStegoOasisId: "Por", audioTranscodeStegoTimestamp: "Gerado em", - audioTranscodeStegoMessage: "TEXT", + audioTranscodeStegoMessage: "TEXTO", audioTranscodeStegoEmpty: "(nenhum)", audioTranscodeStegoUnknown: "—", - audioBackToAudio: "Voltar ao áudio", audioBackToBcs: "Voltar ao BCS", - audioAuthorLabel: "Autor", melodyCompositionTitle: "Mapa de composição da blockchain", melodyFilterMine: "MEU", - melodyByLabel: "Por", melodyAllEmpty: "Ainda sem composições BCS de outros habitantes.", melodyNoteUnavailable: "o bloco não está no teu feed local", melodyUploadBcsTitle: "Publicar", @@ -3781,7 +4777,6 @@ module.exports = { melodyUploadBcsNameNote: "O áudio será publicado com o nome auto-gerado", larpLastAttemptTitle: "A tua última tentativa", larpLastAttemptHouse: "Casa", - larpLastAttemptResult: "Resultado", larpLastAttemptWhen: "Quando", larpLastAttemptCooldown: "Próxima tentativa em", larpTestTitle: "Teste de entrada", @@ -3790,37 +4785,21 @@ module.exports = { larpTestCooldownActive: "Próximo teste disponível em", larpTestCooldownDays: "ciclos", larpTestResultTitle: "Resultado do teste", - larpTestPassed: "Teste passado — bem-vindo!", - larpTestFailed: "Teste não passado", larpTestScore: "Pontuação", larpTestNextAttempt: "Podes tentar outro teste em 30 ciclos.", larpGoToHouse: "Ir para a tua casa", larpBackToAcademia: "Voltar a ACADEMIA", - larpBackToHouses: "◀ Casas", larpBadgeYou: "Tu", larpBadgeRuling: "Governa", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Módulo para a camada de jogo de interpretação ao vivo do Oasis (as 9 Casas).", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - audioTranscodeStegoTitle: "Embedded in the waveform", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "Módulo para a camada de jogo de papéis ao vivo.", larpProfileQ1: "Diante de um problema complexo, o que fazes primeiro?", larpProfileQ1O1: "Falar com outras pessoas para procurar consenso", larpProfileQ1O2: "Construir um protótipo para testar", larpProfileQ1O3: "Investigar a literatura", larpProfileQ1O4: "Sabotar o sistema que o provoca", larpProfileQ2: "O que dá sentido ao teu trabalho diário?", - larpProfileQ2O1: "Defender as pessoas que amo", + larpProfileQ2O1: "Defender os habitantes que amo", larpProfileQ2O2: "Criar algo tangível", larpProfileQ2O3: "Curar ou cuidar da vida", larpProfileQ2O4: "Trabalhar com palavras e ideias", @@ -3887,7 +4866,52 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "Seleciona o modo de navegação UX para a tua interface.", uxModeMenus: "Blocos", - uxModeAINav: "AI", + uxModeChats: "Conversas", + uxModeFeed: "Microblogging", + feedTrendingTitle: "Tags em Tendência", + feedActiveTitle: "Habitantes Ativos", + cnBrandSub: "Livre · P2P · Federada", + cnSyncedPeers: "Pares sincronizados", + cnLicense: "Licença", + cnNotAccessible: "O conteúdo não está acessível neste momento.", + cnUntitled: "Sem título", + cnSearchPlaceholder: "Procurar…", + cnHubTitle: "HUB Clearnet", + cnHubDescription: "Conteúdo público partilhado pelos habitantes desta rede Oasis.", + cnHubEmpty: "Ainda não foi partilhado conteúdo público na Clearnet.", + cnPublicContent: "Conteúdo", + cnTribesContent: "Tribos", + cnNetworkContent: "Rede", + cnCategoryEmpty: "Não há conteúdo nesta categoria.", + cnInhabitantEmpty: "Este habitante ainda não publicou conteúdo na Clearnet.", + cnPrevPage: "← Anterior", + cnNextPage: "Seguinte →", + cnPageOf: "Página {page} de {pages}", + cnMoreInfo: "Mais informações →", + cnOpenCourse: "Curso aberto", + cnAuction: "LEILÃO", + cnExchange: "TROCA", + cnRssDescription: "publicados pelos habitantes deste HUB", + cnKindAudio: "Áudio", + cnKindBlog: "Blog", + cnKindBookmark: "Marcador", + cnKindDocument: "Documento", + cnKindEvent: "Evento", + cnKindFeed: "Feed", + cnKindImage: "Imagem", + cnKindJob: "Emprego", + cnKindMarket: "Mercado", + cnKindPodcast: "Podcast", + cnKindProject: "Projeto", + cnKindCourse: "Curso", + cnKindShop: "Loja", + cnKindTorrent: "Torrent", + cnKindFile: "Ficheiro", + cnKindVideo: "Vídeo", + cnKindWiki: "Wiki", + cnKindRoom: "Sala", + cnKindMap: "Mapa", + uxModeAINav: "IA", } }; diff --git a/src/client/assets/translations/oasis_ru.js b/src/client/assets/translations/oasis_ru.js index d422c1d..910d3d6 100644 --- a/src/client/assets/translations/oasis_ru.js +++ b/src/client/assets/translations/oasis_ru.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { ru: { - bbAdd: "Добавить", - bbQuickActions: "Быстрые действия", - bbPickTitle: "Закрепить модуль", - bbPickDesc: "Выберите модуль для слота + в нижней панели.", - bbPickNone: "Нет", - bbManage: "Изменить", - bbYourBar: "Ваша панель", - bbRemove: "Убрать", - bbFull: "Достигнут максимум", - bbDone: "Готово", - activityChangeFilter: "Сменить фильтр", - emptyConnectHint: "Подключитесь к пабу для синхронизации с сетью.", - emptyConnectCta: "Подключиться к пабу", - menuCommunity: "Сообщество", - activityGroupCommunity: "Сообщество и управление", - activityGroupOrg: "Организация", - activityGroupComm: "Коммуникация", - activityGroupEconomy: "Экономика", - activityGroupMedia: "Контент и медиа", - activityGroupOther: "Другое", - languageName: "Русский", shopOrderStatusPaid: "Оплата получена", shopOrderStatusReceived: "Получено", shopOrderMarkPaid: "Отметить оплату полученной", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "Покупки", shopMyOrdersDescription: "Ваши заказы.", shopMyOrdersEmpty: "У вас пока нет покупок.", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PM Продавец", shopOrderSeller: "Продавец", - shopOrderPaymentConcept: "Оплата", shopOrderInvoice: "Счёт", shopOrderInvoiceConcept: "Счёт", shopOrderStatusPending: "Ожидает", shopOrderStatusAccepted: "Принят", shopOrderStatusRejected: "Отклонён", shopOrderStatusShipped: "Отправлен", - shopOrderStatusDelivered: "Доставлен", shopOrderAccept: "Принять", shopOrderReject: "Отклонить", shopOrderMarkShipped: "Отметить отправленным", - shopOrderMarkDelivered: "Отметить доставленным", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "Смарт-контракт", + shopOrderPmBuyer: "PM Покупатель", shopOrderContractConcept: "Заказ магазина", shopOrdersPending: "Ожидающие заказы", all: "Все", @@ -53,7 +28,7 @@ module.exports = { recent: "Недавние", anonymous: "Аноним", unnamed: "Аноним", - authorLabel: "АВТОР", + authorLabel: "Автор", delete: "Удалить", save: "Сохранить", vote: "Голосовать", @@ -75,18 +50,17 @@ module.exports = { viewJson: "Просмотр JSON", matchScore: "Совпадение", preferencesLabel: "Предпочтения", - inhabitantProfileTitle: "Профиль жителя", + inhabitantProfileTitle: "Житель", contentWarningLabel: "Предупреждение о контенте", invalidPost: "Недопустимое содержимое", invalidPostHint: "В этом сообщении пустой/недопустимый текст.", - spreadBy: "От", spreadChron: "Распространение", spreaded: "Распространено", - spreadMore: "ещё", spreadContentUnavailable: "Контент пока недоступен (ожидается репликация)", filteredByTag: "Отфильтровано по тегу", filteredByTagTitle: "Отфильтровано по тегу", feedPublishedSuccess: "Лента успешно опубликована!", + feedMastodonFailed: "Опубликовано в Oasis, но отправить в Mastodon не удалось.", tribeFeedSent: "Сообщение успешно отправлено!", tribeContentEncrypted: "Зашифрованное содержимое", tribeTorrentsEmpty: "Торрентов пока нет.", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "Без серьёзности", calendarDeleteDate: "Удалить дату", calendarIntervalUntil: "До", - bookmarkCategory: "Категория", bookmarkLastVisit: "Последний визит", profileClearnetBookmarksLabel: "Закладки", - forumFilterHot: "Популярные", invitesPort: "Порт", - currentlyUnrecheable: "ОШИБКА!", - peerHostValidation: "Корректный IPv4 (напр. 192.168.1.100) или имя хоста (напр. pub.example.com)", + peerHostValidation: "Корректный IPv4 (напр. 192.168.1.100) или имя хоста (напр. pub.solarnethub.com)", peerPortValidation: "Порт 1-65535", peerKeyValidation: "Открытый ключ SSB ed25519 (@<44 символа base64>=.ed25519)", inboxJobUnsubscribedTitle: "Отписка от вашей вакансии", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "Описание задачи", exportPasswordLabel: "Пароль экспорта", importPasswordPlaceholder: "Введите пароль", - searchCasesPlaceholder: "Поиск дел…", + searchCasesPlaceholder: "Поиск в Суды...", shopBuyDeliveryAddressPlaceholder: "Адрес доставки", shopBuyNotesPlaceholder: "Заметки", bankTaxesUserNoteBold: "прямое снижение ECO-налога", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "Поддержать дело", courtsSettlementAcceptBtn: "Принять соглашение", courtsSupportCount: "Поддержавшие", - courtsVerdictVoteTitle: "Голосование по вердикту", - courtsVerdictVoteLabel: "Голос по вердикту", - courtsVerdictVoteAccept: "Принять", - courtsVerdictVoteReject: "Отклонить", - courtsVerdictVoteSubmit: "Отправить голос", reportsSetStatus: "Установить статус", reportsStatusClosed: "ЗАКРЫТО", statsAvgPerInhabitant: "Среднее на жителя", statsCarbonMaxAnnual: "Годовая макс. оценка", statsCarbonNetwork: "Всего по сети", statsCarbonOfEstMax: "от оценочной макс. ёмкости", + statsCarbonYearProgress: "года прошло", + statsNetworkTitle: "Сеть", + statsStorageTitle: "Хранилище", + statsEcoTaxTitle: "ECO-налог", + statsAccountTitle: "Аккаунт", + statsAveragesTitle: "Средние значения", statsCarbonOfNetwork: "от общего по сети", statsCarbonUser: "Ваш след", statsContentCountColumn: "Количество", @@ -212,55 +184,84 @@ module.exports = { ], profile: "Аватар", inhabitants: "Жители", - peersReplicatedFeeds: "Реплицированные ленты", + peersReplicatedFeeds: "Реплицированные", + peersTorColumn: "TOR", + peersTorYes: "Доступен через Tor", + peersTorNo: "Недоступен через Tor", + peersOasisId: "ID", + peersOasisVersion: "Версия", graphos: "Графос", graphosDescription: "Интерактивная карта сети вокруг вас.", + graphosFederation: "Федерация", + graphosFedPubs: "PUB", + graphosFedUnreachable: "Недоступны", + graphosFedLinks: "Взаимные связи", + graphosFedIsolated: "Изолированы", + graphosFedNoData: "Нет данных", + graphosFedMutual: "Взаимная", + graphosFedOneWay: "Односторонняя", + graphosFedTransport: "Сеть", + graphosFedWith: "Федерирован с", + graphosZoom: "Увеличить", + graphosFedInhabitants: "Жители", + graphosFedClearnet: "Интернет", + graphosFedBoth: "Интернет + TOR", + graphosFedEmpty: "Ваш узел пока не знает ни одного PUB.", graphosViewingGraphOf: "Просмотр", graphosBackToMine: "← Моя сеть", graphosShowing: "Показано", graphosYou: "Вы", graphosTotalNodes: "Всего узлов", - manualMode: "Ручной режим", mentions: "Упоминания", mentionsDescription: [ - strong("Записи, в которых @упоминают вас"), - ", в обратном хронологическом порядке.", + strong("Каждое сообщение, где вас @упоминают"), + ".", ], - nextPage: "Далее", - previousPage: "Назад", noMentions: "Вы ещё не получали @упоминаний.", + mentionsSearchPlaceholder: "Поиск в Упоминания...", private: "Входящие", + privateDescription: "Управляйте своими личными сообщениями.", privateInbox: "ВХОДЯЩИЕ", privateReminders: "НАПОМИНАНИЯ", + inboxFilterPms: "ЛС", + inboxFilterNotifications: "Уведомления", + inboxMarkRead: "Отметить прочитанным", + inboxMarkUnread: "Отметить непрочитанным", + inboxUnreadChip: "Не прочитано", + inboxMarkAllRead: "Отметить все прочитанными", + inboxDeleteShown: "Удалить все", + inboxArchive: "В архив", + inboxUnarchive: "Из архива", + inboxFilterArchived: "Архив", + inboxEarlierMessages: "предыдущих сообщений", + inboxSortThread: "По темам", + inboxSortRecent: "Сначала новые", + inboxAllBots: "Боты", + settingsInboxBotsTitle: "Уведомления Inbox", + settingsInboxBotsDesc: "Выберите, какие боты могут уведомлять вас в Inbox. Снятые боты скрываются, и их уведомления не учитываются.", inboxFiltersLabel: "Фильтры:", inboxToggleToMutuals: "Переключить на взаимных", inboxToggleToWhole: "Переключить на всех", privateSent: "ОТПРАВЛЕННЫЕ", - privateFrom: "От", - privateTo: "Кому", privateDate: "Дата", privateDelete: "Удалить", pmCreateButton: "Написать ЛС", pmReply: "Ответить", pmReplies: "ответы", - pmNew: "новое", - pmMarkRead: "Отметить как прочитанное", inReplyTo: "В ОТВЕТ НА", + forumInThread: "В ТЕМЕ", pmPreview: "Предпросмотр", pmPreviewTitle: "Предпросмотр сообщения", noPrivateMessages: "Вы ещё не получали личных сообщений.", peers: "Узлы", - privateDescription: ["Личные сообщения ",strong("зашифрованы вашим открытым ключом")," и могут иметь максимум 7 получателей."], search: "Поиск", searchDescription: "Описание", - imageSearch: "Поиск изображений", searchFromLabel: "С", searchToLabel: "До", - searchMinOpinionsLabel: "Мин. мнений", - searchInhabitantLabel: "Житель (Oasis ID)", searchQueryLabel: "Запрос", - searchPerPageLabel: "Результатов на странице", + searchPerPageLabel: "Результаты", settings: "Настройки", + settingsDescription: "Настройте окружение вашей сети.", continueReading: "Продолжить чтение", moreComments: "ещё комментарий", readThread: "прочитать остальное обсуждение", @@ -270,68 +271,47 @@ module.exports = { melodyDescription: 'Воспроизведите мелодию вашего блокчейна — каждый блок становится нотой.', melodyEmpty: 'Ещё нет нот — ваш блокчейн пока не создал ни одного блока.', melodyCompositionTitle: 'Композиция', - melodyCompositionHelp: 'Каждый блок вашего блокчейна становится музыкальной нотой по типу и размеру.', - melodyStatsTitle: 'Разбивка по типам', - melodyInhabitantLabel: 'Житель', melodyTotalBlocks: 'Ноты', - melodyType: 'Тип', - melodyCount: 'Блоки', melodyFilterMine: 'МОЁ', melodyFilterAll: 'ВСЕ', - melodyFilterShare: 'Загрузить мелодию', - melodyShareTitle: 'Поделитесь своей мелодией', - melodyShareHelp: 'Опубликуйте снимок вашей текущей мелодии, чтобы другие могли её услышать и переработать.', - melodyShareTitleLabel: 'Название (необязательно)', - melodyShareTitlePlaceholder: 'Призрак блокчейна', - melodyShareSubmit: 'Опубликовать мелодию', - melodyNoShared: 'Ещё нет мелодий блокчейна.', melodyRegenerate: 'Перегенерировать', melodyDownload: "Скачать BCS", - profileActivityTitle: 'Активность', - profileActivityMore: 'Вся активность', profileContentSectionTitle: 'Содержимое аватара', profileContentHelp: 'Выбери, какие модули будут отображаться в твоём аватаре.', profileSensorsHelp: 'Дополнительные метрики, отображаемые в аватаре.', profileClearnetHelp: 'Модули, доступные извне Oasis.', profileHubAll: 'Все', - profileHubTitle: 'Публичное содержимое', - footerPulseTitle: 'Пульс', - footerPulsePeers: 'Узлы', - footerPulseInbox: 'Входящие', - footerPulseSync: 'Посл. синх.', - footerPulseEcoValue: 'ECO/ч', - footerPulseLastActivity: 'Последняя активность', footerLicenseLabel: 'Лицензия', - profileSwitchToOasis: 'Вернуться в Oasis', - profileSwitchToClearnet: 'Погрузиться в Clearnet', - profileVisibilityFediverse: "Федиверс", - profileSwitchToFediverse: "Погрузиться в федиверс", - coordLabel: 'Координата (напр., A3)', - coordPlaceholder: 'Введите координату', + profileVisibilityFediverse: "Мультивселенная", contributorsTitle: "Участники", - pixeliaBy: "автор", colorLabel: 'Выберите цвет', paintButton: 'Рисовать!', - invalidCoordinate: 'Неверная координата', - goToMuralButton: "Смотреть фреску", totalPixels: 'Всего пикселей', modules: "Модули", modulesViewTitle: "Модули", modulesViewDescription: "Настройте свою среду, включая или отключая модули.", inbox: "Входящие", + inboxSearchPlaceholder: "Поиск в Входящие...", multiverse: "Мультивселенная", - fediverse: "Федиверс", - fediverseDescription: "Управляйте своими другими аккаунтами федиверса, включая отправку и получение контента.", + fediverse: "Мультивселенная", + fediverseDescription: "Управляйте своими другими аккаунтами мультивселенной, включая отправку и получение контента.", fediverseStatus: "Статус", - fediverseDisconnected: "Федиверс отключён. Проверьте %LINK% или состояние подключения.", - fediverseSettingsTitle: "Федиверс", + fediverseDisconnected: "Мультивселенная отключён. Проверьте %LINK% или состояние подключения.", + fediverseSettingsTitle: "Мультивселенная", fediverseInstanceLabel: "Адрес", fediverseTokenLabel: "Токен доступа", - fediverseTokenHelp: "Укажите ваш токен доступа. Он будет использоваться для управления вашим аккаунтом Mastodon из Oasis.", fediverseConnect: "Подключить", fediverseConnectedAs: "Подключено как", + nextcloudServerLabel: "Сервер", + nextcloudUserLabel: "Пользователь", + nextcloudFolderLabel: "Папка", + nextcloudUploadButton: "Загрузить", + nextcloudFileLabel: "Выберите файл", + fileExternalLink: "Внешняя ссылка", + nextcloudErrConnect: "Не удалось связаться с Nextcloud с этими данными.", + nextcloudErrUpload: "Загрузка в Nextcloud не удалась.", + nextcloudErrShare: "Nextcloud не дал публичную ссылку на файл.", fediverseDisconnect: "Отключить", - fediverseEmpty: "Когда вы подключите другие аккаунты федиверса из %LINK%, вы сможете управлять ими отсюда.", fediverseEmptyLink: "настроек", fediverseComposePlaceholder: "О чём вы думаете?", fediverseReplyPlaceholder: "Напишите ответ…", @@ -340,21 +320,15 @@ module.exports = { fediverseReply: "Ответить", fediverseBoosted: "продвинул(а)", fediverseNoPosts: "Ваша лента пуста.", - fediverseThread: "Обсуждение", fediverseTimeline: "Ленты", - fediverseManageTimeline: "Управлять лентой", fediverseFollowers: "Подписчики", fediverseFollowing: "Подписки", fediversePosts: "Посты", fediverseJoined: "Регистрация", - fediverseOverview: "Обзор", fediverseRefresh: "Обновить", fediverseWrite: "Написать", fediversePreview: "Предпросмотр", - fediverseEdit: "Изменить", - fediverseOpenFeed: "Открыть ленту", fediverseManage: "Управление", - fediverseStatusNotConnected: "Не подключено", fediverseError: "Не удалось связаться с Mastodon.", fediverseErrInstance: "Неверный URL сервера.", fediverseErrAuth: "Неверный или просроченный токен.", @@ -365,17 +339,76 @@ module.exports = { fediverseErrPost: "Не удалось опубликовать.", fediverseErrMedia: "Не удалось загрузить файл.", fediverseErrEmpty: "Напишите что-нибудь или прикрепите файл.", - popularLabel: "Избранное", - topicsLabel: "Темы", - latestLabel: "Последние", - summariesLabel: "Сводки", - threadsLabel: "Обсуждения", - multiverseLabel: "Мультивселенная", - inboxLabel: "Входящие", - invitesLabel: "Приглашения", - walletLabel: "Кошелёк", - legacyLabel: "Ключи", - cipherLabel: "Шифратор", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "Номер телефона", + telegramSendCode: "Отправить код", + telegramCodeHelp: "Введите код, который Telegram только что отправил (приложение или SMS).", + telegramCodeLabel: "Код", + telegramPasswordHelp: "У аккаунта включена двухэтапная проверка. Введите облачный пароль.", + telegramPasswordLabel: "Пароль", + telegramVerify: "Подтвердить", + telegramCancel: "Отмена", + telegramChats: "Чаты", + telegramUnread: "Непрочитанные", + telegramOpen: "Открыть", + telegramSend: "Отправить", + telegramYou: "Вы", + telegramNoChats: "Чатов пока нет.", + telegramNoMessages: "Сообщений пока нет.", + telegramComposePlaceholder: "Напишите сообщение…", + telegramChannel: "Канал", + telegramGroup: "Группа", + telegramPrivate: "Личный", + telegramErrConnect: "Не удалось связаться с Telegram.", + telegramErrTimeout: "Telegram слишком долго не отвечал.", + telegramErrAuth: "Сессия Telegram недействительна или истекла. Подключитесь снова.", + telegramErrFetch: "Не удалось загрузить чаты.", + telegramErrSend: "Не удалось отправить сообщение.", + telegramErrMissing: "Нужны API ID, API hash и номер телефона.", + telegramErrCode: "Неверный код. Попробуйте ещё раз.", + telegramErrCodeExpired: "Код истёк. Начните заново.", + telegramErrPassword: "Неверный пароль.", + telegramErrPhone: "Неверный номер телефона.", + telegramErrApi: "Неверные API ID или API hash.", + telegramErrFlood: "Слишком много попыток. Подождите и попробуйте снова.", + telegramErrNoLogin: "Вход не начат. Начните заново.", + peertubeUsernameLabel: "Имя пользователя", + peertubePasswordLabel: "Пароль", + peertubeSubscriptions: "Подписки", + peertubeMyVideos: "Мои видео", + peertubeVideos: "Видео", + peertubeFollowers: "Подписчики", + peertubeViews: "просмотров", + peertubeLikes: "лайков", + peertubeQuota: "Квота", + peertubeUpload: "Загрузить видео", + peertubeUploadHelp: "Выберите видеофайл. Он загружается в ваш канал, затем PeerTube его обрабатывает.", + peertubeTitleLabel: "Название", + peertubeDescriptionLabel: "Описание", + peertubePrivacyLabel: "Приватность", + peertubePrivacyPublic: "Публичное", + peertubePrivacyUnlisted: "По ссылке", + peertubePrivacyPrivate: "Приватное", + peertubeNoVideos: "Видео пока нет.", + peertubeComments: "Комментарии", + peertubeComment: "Комментировать", + peertubeCommentPlaceholder: "Напишите комментарий…", + peertubeNoComments: "Комментариев пока нет.", + peertubeOpen: "Открыть на PeerTube", + peertubeWatch: "Смотреть", + peertubeChannel: "Канал", + peertubeNoStream: "Это видео пока нельзя воспроизвести здесь. Откройте его на PeerTube.", + peertubeUploaded: "Видео загружено. PeerTube обрабатывает его.", + peertubeErrConnect: "Не удалось связаться с PeerTube.", + peertubeErrAuth: "Неверное имя пользователя или пароль, либо сессия истекла. Подключитесь снова.", + peertubeErrInstance: "Неверный адрес инстанса.", + peertubeErrMissing: "Нужны инстанс, имя пользователя и пароль.", + peertubeErrFetch: "Не удалось загрузить видео.", + peertubeErrUpload: "Не удалось загрузить видео на сервер.", + peertubeErrTitle: "Нужно название.", + peertubeErrComment: "Не удалось опубликовать комментарий.", + peertubeErrNoFile: "Выберите видеофайл.", bookmarksLabel: "Закладки", videosLabel: "Видео", torrentsLabel: "Торренты", @@ -386,34 +419,22 @@ module.exports = { inhabitantsLabel: "Жители", trendingLabel: "В тренде", eventsLabel: "События", - tasksLabel: "Задачи", apply: "Применить", menuPersonal: "Личное", - menuContent: "Контент", - menuBlogs: "Блоги", menuGovernance: "Управление", menuOffice: "Офис", - menuMultiverse: "Мультивселенная", - menuNetwork: "Сеть", + menuNetwork: "Сообщество", menuCreative: "Творчество", menuEconomy: "Экономика", - menuMedia: "Медиа", + menuMedia: "Библиотека", menuTools: "Инструменты", comment: "Комментарий", subtopic: "Подтема", json: "JSON", - createdBy: "автор", unfollow: "Отменить поддержку", follow: "Поддержать", block: "Заблокировать", unblock: "Разблокировать", - newerPosts: "Более новые записи", - olderPosts: "Более старые записи", - feedRangeEmpty: "Данный диапазон пуст для этой ленты. Попробуйте просмотреть ", - seeFullFeed: "полную ленту", - feedEmpty: "Сеть Oasis никогда не видела записей от этого аккаунта.", - beginningOfFeed: "Это начало ленты", - noNewerPosts: "Более новых записей пока не получено.", relationshipNotFollowing: "Вас не поддерживают", relationshipTheyFollow: "Поддерживает вас", relationshipMutuals: "Взаимная поддержка", @@ -423,25 +444,23 @@ module.exports = { relationshipBlockedBy: "Вы заблокированы", relationshipMutualBlock: "Взаимная блокировка", relationshipNone: "Вы не поддерживаете", - relationshipConflict: "Конфликт", relationshipBlockingPost: "Заблокированная запись", viewLikes: "Посмотреть распространения", + viewSupporters: "Посмотреть сторонников", spreadedDescription: "Список записей, распространённых жителем.", - totalspreads: "Всего распространений", - attachFiles: "Прикрепить файлы", preview: "Предпросмотр", publish: "Написать", contentWarningPlaceholder: "Добавьте тему к записи (необязательно)", - privateWarningPlaceholder: "Добавьте жителей для отправки приватной записи (необязательно)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "Напишите текст вашей записи.", + publishTooLong: "Ваш пост слишком длинный. Пожалуйста, сократите его.", + keepTextRetry: "Отправить снова", + dateInPastError: "Дата не может быть в прошлом", publishCustomDescription: [ "ПОМНИТЕ: Благодаря технологии блокчейн, после публикации запись невозможно отредактировать или удалить.", ], commentWarning: [ "ПОМНИТЕ: Благодаря технологии блокчейн, после публикации запись невозможно отредактировать или удалить.", ], - commentPublic: "публичный", - commentPrivate: "приватный", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -461,14 +480,12 @@ module.exports = { ".", ], publishCustom: "Написать расширенную запись", - subtopicLabel: "Создать подтему для этой записи", messagePreview: "Предпросмотр записи", mentionsMatching: "Совпадающие упоминания", mentionsName: "Имя", mentionsRelationship: "Отношение", - updateit: "ОБНОВИТЬ!", updateBannerText: "Доступна новая версия Oasis.", - updateBannerAction: "Обновить сейчас →", + updateBannerAction: "Обновить сейчас!", info: "Информация", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -476,29 +493,94 @@ module.exports = { timeAgo: "назад", sendTime: "примерно ", theme: "Тема", - legacy: "Ключи", - legacyTitle: "Ключи", - legacyDescription: "Быстро и безопасно управляйте своим секретом (приватным ключом).", - legacyExportButton: "Экспорт", - legacyImportButton: "Импорт", + backupKeysExportButton: "Экспорт", + backupKeysImportButton: "Импорт", ssbLogStream: "Блокчейн", ssbLogStreamDescription: "Настройте лимит сообщений для потоков блокчейна.", + logWindowChip: "Последние {n} потоков блокчейна", saveSettings: "Сохранить настройки", - exportTitle: "Экспорт данных", exportDescription: "Установите пароль (минимум 32 символа) для шифрования вашего ключа", - exportDataTitle: "Резервная копия", + exportDataTitle: "Мгновенная копия", exportDataDescription: "Скачайте ваши данные (без секретного ключа!)", exportDataButton: "Скачать базу данных", - pubWallet: "PUB Кошелёк", - pubWalletDescription: "Установите URL кошелька PUB. Он будет использоваться для транзакций PUB (включая UBI).", - pubWalletConfiguration: "Сохранить конфигурацию", - importTitle: "Импорт данных", importDescription: "Импортируйте ваш зашифрованный секрет (приватный ключ) для активации аватара", - importAttach: "Прикрепить зашифрованный файл (.enc)", - passwordLengthInfo: "Пароль должен содержать минимум 32 символа.", passwordImport: "Введите пароль для расшифровки данных, которые будут сохранены в домашней директории системы (имя файла: secret)", exportPasswordPlaceholder: "Используйте строчные, заглавные буквы, цифры и символы", - fileInfo: "Ваш зашифрованный секретный ключ будет сохранён в домашней директории системы (имя файла: oasis.enc)", + fileInfo: "Ваш зашифрованный секретный ключ будет скачан на ваше устройство (имя файла: oasis.enc)", + developer: "Разработка", + devTitle: "Разработка", + welcomeBannerText: "Твой аватар ещё не настроен.", + aiSuggestionBanner: "Совет:", + aiSuggestionsEnable: "Советы ИИ", + welcomeBannerAction: "Начать", + welcomeTitle: "Добро пожаловать", + welcomeStepIdentity: "Личность", + welcomeStepType: "Тип", + welcomeStepUse: "Использование", + welcomeStepLook: "Внешний вид", + welcomeStepCommunity: "Сообщество", + welcomeStepIntro: "Приветствие", + welcomeHintIdentity: "Ваше имя и изображение.", + welcomeHintType: "Как Oasis открывается для вас.", + welcomeHintUse: "Какие модули остаются включёнными.", + welcomeHintLook: "Тема, которую носит Oasis.", + welcomeHintCommunity: "Войдите в PUB, чтобы познакомиться с другими жителями.", + welcomeHintIntro: "Отправьте первое приветствие сообществу.", + welcomeStepThemeAction: "Использовать эту тему", + welcomeJoinPubs: "Войти в PUB", + welcomeNoFederation: "Без федерации", + welcomeStepGreetingAction: "Отправить", + welcomeDescription: "Несколько вещей, которые стоит сделать перед началом.", + welcomeSkip: "Не сейчас", + welcomeStepProfileAction: "Сохранить профиль", + welcomeBootstrapRunning: "Загружается снимок паба. По завершении паб будет подключён автоматически; тем временем можно продолжать пользоваться Oasis.", + welcomeCompleteTitle: "Всё готово.", + welcomeCompleteText: "Ваш узел готов. Дальше сеть растёт вместе с жителями, которых вы поддерживаете.", + welcomeFindPeople: "Найти жителей", + devFilterFiles: "ФАЙЛЫ", + devFilterSearch: "ПОИСК", + devFilterModules: "МОДУЛИ", + devFilterTranslations: "ПЕРЕВОДЫ", + devFilterStyles: "СТИЛИ", + devFilterTests: "ТЕСТЫ", + devVersion: "Версия", + devNodeVersion: "Node", + devStatsTests: "Тесты", + devParentFolder: "Родительская папка", + devOpenFolder: "открыть", + devDescription: "Изучайте исходный код, работающий на этом узле.", + devRoot: "корень", + devSearchPlaceholder: "Текст для поиска в коде", + devAnyExtension: "Любой файл", + devStatsFiles: "Файлы", + devStatsLines: "Строки", + devStatsModules: "Модули", + devStatsLanguages: "Языки", + devNotViewable: "нельзя открыть", + devEmptyFolder: "Эта папка пуста.", + devSize: "Размер", + devShowing: "Показано", + devLine: "строка", + devReportBug: "Сообщить об Ошибке", + devCreateTask: "Создать Задачу", + devRaw: "RAW", + devReportContext: "Найдено при чтении исходного кода Oasis", + devTaskPrefix: "Проверить", + devPrevPage: "Предыдущие строки", + devNextPage: "Следующие строки", + devMatches: "совпадений", + devFilesScanned: "файлов проверено", + devNoResults: "Совпадений пока нет.", + devColModule: "Модуль", + devColState: "Состояние", + devColModel: "Модель", + devColView: "Представление", + devColRoutes: "Маршруты", + devColTests: "Тесты", + devOn: "вкл", + devOff: "выкл", + modulesDevLabel: "Разработка", + modulesDevDescription: "Модуль для управления исходным кодом Oasis.", themeIntro: "Выберите тему.", setTheme: "Установить тему", @@ -506,16 +588,49 @@ module.exports = { languageDescription: "Если вы хотите использовать другой язык, выберите его здесь.", setLanguage: "Установить язык", - peerConnections: "Узлы", peerConnectionsIntro: "Управляйте всеми подключениями к другим узлам.", + peersConnectFailed: "Не удалось подключиться: {reason}.", + peerStateLabel: "Состояние", + peerAddressLabel: "Адрес", + peerLastChangeLabel: "Последнее изменение", + peerStateConnected: "Подключён", + peerStateConnecting: "Подключение", + peerStateStaged: "Доступен", + peerErrSilent: "Нет активности", + peerErrTimeout: "Не отвечает", + peerErrRefused: "Отклоняет подключение", + peerErrNotFound: "Неизвестный адрес", + peerErrUnreachable: "Сеть недоступна", + peerErrKeys: "Использует другие ключи", + peerErrIdentity: "Не принимает ваше подключение", + peerErrTor: "Только TOR", + peerErrDropped: "Соединение прервано", + peerErrPaused: "Сеть приостановлена", + peerErrOther: "Ошибка подключения", + peerOutdated: "Устарел", + peerVersionUnannounced: "Не сообщает свою версию: старый Oasis", + peersTorMissing: "Этот PUB принимает подключения только через TOR.", peerConnectionsTitle: "Подключения", + peersOnlineHint: "Узлы, подключённые к вам прямо сейчас.", + peersDiscoveredHint: "Узлы с известным адресом, не подключённые сейчас.", + peersUnknownHint: "Узлы вашего социального графа без известного адреса.", + peersConnectionsHint: "Адреса, к которым может подключаться этот узел, и состояние каждого соединения.", + peersTabLan: "LAN", + peersTabHealth: "Состояние", + peersHealthHint: "Как дела у каждого известного паба или узла, в локальной сети и за её пределами.", + peersHealthReachable: "Доступен", + peersHealthFailing: "Сбоит", + peersHealthFailures: "Сбои", + peersFailingSince: "Сбоит с", + peersHealthForget: "Забыть недоступные", + peersLanHint: "Узлы, найденные в вашей локальной сети.", + peersLanLastSeen: "Последний раз замечен", + peersLanEmpty: "В локальной сети пока не найдено ни одного узла.", online: "В сети", offline: "Не в сети", discovered: 'Обнаружен', unknown: 'Неизвестно', - lanBroadcastLabel: "LAN-вещание", lanBroadcastActive: "Активно (UDP-multicast в LAN)", - lanBroadcastDisabled: "Отключено (режим pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -528,127 +643,102 @@ module.exports = { noConnections: "Нет подключённых узлов.", noDiscovered: "Нет обнаруженных узлов.", noUnknownPeers: "Нет неизвестных пиров в графе друзей.", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "Пока нет зарегистрированных узлов.", + peerConnect: "Подключить", + peerDisconnect: "Отключить", peerRefresh: "Обновить", peerPruneIdle: "Удалить неактивные", + peersPause: "Пауза", + peersResume: "Продолжить", + peersPausedNote: "На паузе: Oasis ни с кем не соединяется, и к нему никто не может подключиться.", + peersPauseHint: "Останавливает все соединения: Oasis не подключается к другим узлам и не принимает их, пока вы не возобновите.", peerExport: "Экспорт", peerImport: "Импорт", peerImportTitle: "Импорт списка пиров", peerImportPlaceholder: "Вставьте по одному multiserver-адресу на строку или загрузите .txt файл…", - noSupportedConnections: "Нет поддерживаемых узлов.", - noBlockedConnections: "Нет заблокированных узлов.", - noRecommendedConnections: "Нет рекомендованных узлов.", connectionActionIntro: "", - startNetworking: "Запустить сеть", - stopNetworking: "Остановить сеть", - restartNetworking: "Перезапустить сеть", - sync: "Синхронизировать сеть", indexes: "Индексы", + indexesRebuilt: "Индексы перестроены", + indexesRebuildFailed: "Не удалось перестроить индексы", + indexesFiles: "Файлы индексов", indexesDescription: "Перестройка индексов безопасна и может исправить некоторые типы ошибок.", homePageTitle: "Главная", homePageDescription: "Выберите модуль, который будет вашей главной страницей.", saveHomePage: "Установить главную", invites: "Приглашения", - invitesTitle: "Приглашения", - invitesInvites: "Приглашения", invitesDescription: "Управляйте и применяйте коды приглашений в вашей сети.", + invitesPubsHint: "Вставьте пригласительный код паба, чтобы федерироваться с ним и начать реплицировать его жителей.", + invitesFederationsHint: "Пабы, с которыми вы федерированы: обновите их, удалите недоступные или экспортируйте список.", invitesTribesTitle: "Племена", - invitesTribeInviteCodePlaceholder: "Введите код приглашения племени", - invitesTribeJoinButton: "Вступить в племя", invitesChatsTitle: "Чаты", - invitesChatInviteCodePlaceholder: "Введите код приглашения чата", - invitesChatJoinButton: "Войти в чат", invitesPadsTitle: "Пады", - invitesPadInviteCodePlaceholder: "Введите код приглашения пада", - invitesPadJoinButton: "Войти в пад", invitesCalendarsTitle: "Календари", - invitesCalendarInviteCodePlaceholder: "Введите код приглашения календаря", - invitesCalendarJoinButton: "Войти в календарь", invitesEventsTitle: "События", - invitesEventInviteCodePlaceholder: "Введите код приглашения события", - invitesEventJoinButton: "Войти в событие", invitesForumsTitle: "Форумы", - invitesForumInviteCodePlaceholder: "Введите код приглашения форума", - invitesForumJoinButton: "Войти на форум", invitesMapsTitle: "Карты", - invitesMapInviteCodePlaceholder: "Введите код приглашения карты", - invitesMapJoinButton: "Войти на карту", invitesShopsTitle: "Магазины", - invitesShopInviteCodePlaceholder: "Введите код приглашения магазина", - invitesShopJoinButton: "Войти в магазин", invitesPubsTitle: "PUB", + invitesSuggestedPubs: "Рекомендуемые PUB", + invitesFilterOthers: "Другие", + invitesJoinTitle: "Приглашения", + invitesJoinHint: "Вставьте любой пригласительный код, для чего бы он ни был, или Oasis ID жителя.", + invitesJoinPlaceholder: "Пригласительный код или Oasis ID", + invitesJoinButton: "Войти", + invitesUsedTitle: "Последние приглашения", + invitesUsedEmpty: "Вы ещё не использовали ни одного приглашения.", + invitesUsedType: "Тип", + invitesUsedName: "Название", + invitesUsedDate: "Дата", invitesPubInviteCodePlaceholder: "Введите код приглашения PUB", invitesAcceptInvite: "Присоединиться к PUB", + invitesPubUses: "Использовано приглашений", + invitesPubExhausted: "У этого PUB не осталось приглашений.", invitesAlreadyFederated: "Вы уже федерированы с этим пабом.", + invitesJoinPubMode: "Этот Oasis работает как PUB в режиме только для чтения. Чтобы присоединить его к другому PUB, выполните в терминале: ./oasis.sh join <код>", + invitesJoinLocalOnly: "Приглашения можно принять только на устройстве, где работает Oasis, или по ссылке администратора, которую он показывает при запуске.", invitesFederationsTitle: "Федерации", invitesAcceptedInvites: "Федеративные", - invitesNoInvites: "Приглашения ещё не приняты.", invitesUnfollow: "Отписаться", invitesFollow: "Подписаться", invitesUnfollowedInvites: "Нефедеративные", invitesNoFederatedPubs: "Нет федеративных pub-ов.", invitesNoUnfollowed: "Нет нефедеративных pub-ов.", - invitesUnreachablePubs: "Недоступные", - invitesNoUnreachablePubs: "Нет недоступных pub-ов.", invitesPubsRefresh: "Обновить", - invitesPubsClearUnreachable: "Удалить недоступные", + invitesPubsClearUnreachable: "Очистить", invitesPubsExport: "Экспорт", invitesPubsImport: "Импорт", invitesPubsImportTitle: "Импорт pub-ов", invitesPubsImportPlaceholder: "Вставьте один multiserver-адрес или код приглашения на строку, или загрузите .txt файл…", - currentlyUnreachable: "ОШИБКА!", - errorDetails: "Подробности ошибки", genericError: "Произошла ошибка.", panicMode: "Режим паники!", encryptData: "Установите пароль (минимум 32 символа) для шифрования вашего блокчейна", decryptData: "Введите пароль для расшифровки вашего блокчейна", - panicModeDescription: "Зашифровать/Расшифровать или УДАЛИТЬ ваш блокчейн", removeDataDescription: "ВНИМАНИЕ: Этот процесс нельзя отменить.", - encryptPanicButton: "Зашифровать блокчейн", - decryptPanicButton: "Расшифровать блокчейн", removePanicButton: "УДАЛИТЬ ВСЕ ДАННЫЕ!", searchTitle: "Поиск", searchDescriptionLabel: "Поиск контента в вашей сети.", - searchLanguagesLabel: "Языки", - searchSkillsLabel: "Навыки", searchPlaceholder:"Искать контент...", - searchDateLabel:"Дата", searchLocationLabel:"Местоположение", searchPriceLabel:"Цена", - searchUrlLabel:"URL", searchCategoryLabel:"Категория", - searchStartLabel:"Время начала", - searchEndLabel:"Время окончания", searchPriorityLabel:"Приоритет", searchStatusLabel:"Статус", statusLabel:"Статус", - totalVotesLabel:"Всего голосов", noResultsFound:"Результатов не найдено.", votesOption:"Варианты голосования", voteYesLabel:"За", voteNoLabel:"Против", allTypesLabel: "БЕЗ ОГРАНИЧЕНИЙ", - ABSTENTIONLabel:"ВОЗДЕРЖАНИЕ", YESLabel:"ЗА", NOLabel:"ПРОТИВ", - FOLLOW_MAJORITYLabel: "СЛЕДОВАТЬ БОЛЬШИНСТВУ", - CONFUSEDLabel: "ЗАМЕШАТЕЛЬСТВО", - NOT_INTERESTEDLabel: "НЕ ИНТЕРЕСНО", - StatusLabel:"Статус", votesOptionYesLabel:"За", votesOptionNoLabel:"Против", votesOptionAbstentionLabel:"Воздержание", votesQuestionLabel:"Вопрос", - votesCreatedByLabel:"Создано", voteStatusOpen:"ОТКРЫТО", voteStatusClosed:"ЗАКРЫТО", - imageSearchLabel: "Введите слова для поиска изображений с соответствующими метками.", commentDescription: ({ parentUrl }) => [ " прокомментировал ", a({ href: parentUrl }, " обсуждение"), @@ -659,61 +749,27 @@ module.exports = { a({ href: parentUrl }, " записи"), ], subtopicTitle: ({ authorName }) => [`Подтема записи @${authorName}`], - mysteryDescription: "опубликовал загадочную запись", oasisDescription: "Сеть проекта OASIS", searchSubmit: "Искать...", - postLabel: "ЗАПИСИ", - aboutLabel: "ЖИТЕЛИ", - feedLabel: "ЛЕНТЫ", votesLabel: "ГОЛОСОВАНИЯ", - reportLabel: "ОТЧЁТЫ", - imageLabel: "ИЗОБРАЖЕНИЯ", - videoLabel: "ВИДЕО", - audioLabel: "АУДИО", - documentLabel: "ДОКУМЕНТЫ", - torrentLabel: "ТОРРЕНТЫ", pdfFallbackLabel: "PDF документ", - eventLabel: "СОБЫТИЯ", - taskLabel: "ЗАДАЧИ", - transferLabel: "ПЕРЕВОДЫ", - curriculumLabel: "РЕЗЮМЕ", - bookmarkLabel: "ЗАКЛАДКИ", - tribeLabel: "ПЛЕМЕНА", - marketLabel: "РЫНОК", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "КОШЕЛЬКИ", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + shopLabel: "МАГАЗИНЫ", + voteLabel: "ГОЛОСА", submit: "Отправить", - subjectLabel: "Тема", editProfile: "Редактировать аватар", - profileQrAlt: "QR-код профиля", - profileQrHint: "Отсканируйте, чтобы скопировать этот Oasis ID.", oasisIdLabel: "OasisID", - spreadLabel: "Распространить", - spreadHint: "Распространить вашим сторонникам (через ваш feed).", + spreadContent: "Реплицировать", welcomePmSubject: "Hello World!", - welcomePmBody: "Добро пожаловать в Oasis — свободная, одноранговая, федеративная и зашифрованная социальная сеть с распределённой архитектурой только по приглашениям.\n\nНесколько интересных мест, с которых можно начать:\n\n- /profile — Отредактируй аватар, имя, описание и какие модули видны на твоей публичной стороне.\n- /invites — Добавь pub, чтобы федерироваться с остальной сетью.\n- /peers — Посмотри, кто на связи, пересканируй LAN, экспортируй или импортируй списки пиров.\n- /inhabitants — Открывай и заходи к аватарам других людей.\n- /tribes — Создавай или вступай в приватные группы со сквозным шифрованием.\n- /inbox — Читай и отправляй приватные сообщения.\n- /activity — Смотри свежую активность — свою и твоей сети.\n- /publish — Напиши пост или поделись изображением, аудио, видео или документом.\n\nНесколько интересных мест для подключения:\n\n- /fediverse — Управляйте своими другими аккаунтами федиверса, включая отправку и получение контента.\n\nЭто сообщение было отправлено приватно от тебя самому себе, поэтому для его получения не требовалось соединение.", - profileVisibilityTitle: "Видимость публичного профиля", - profileVisibilityHint: "Выберите поля, видимые другим жителям. По умолчанию показана только Карма.", + welcomePmBody: "Добро пожаловать в Oasis — свободная, одноранговая, федеративная и зашифрованная социальная сеть с распределённой архитектурой только по приглашениям.\n\nНесколько интересных мест, с которых можно начать:\n\n- [/avatar](/profile) — Отредактируй аватар, имя, описание и какие модули видны на твоей публичной стороне.\n- /settings — Настройте локальный экземпляр: язык, тема, модули, приватность и синхронизация.\n- /invites — Добавь pub, чтобы федерироваться с остальной сетью.\n- /peers — Посмотри, кто на связи, пересканируй LAN, экспортируй или импортируй списки пиров.\n- /inhabitants — Открывай и заходи к аватарам других жителей.\n- /tribes — Создавай или вступай в приватные группы со сквозным шифрованием.\n- /search — Ищите содержимое сети по типу.\n\nНесколько интересных мест для подключения:\n\n- /мультивселенной — Управляйте своими другими аккаунтами мультивселенной, включая отправку и получение контента.\n\nЭто сообщение было отправлено приватно от тебя самому себе, поэтому для его получения не требовалось соединение.", profileSensorsSectionTitle: "Сенсоры", profileVisibilityActivity: "Уровень активности", - profileVisibilityDevice: "Устройство", profileDeviceLockedHint: "Этот датчик всегда включён: он показывает другим, с какого устройства вы подключаетесь, и не может быть отключён.", profileVisibilityKarma: "Карма", - profileVisibilityUbi: "UBI", - profileVisibilityWallet: "Кошелёк ECOIN", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityUbi: "ББД", + profileVisibilityWallet: "Кошелёк ECOin", + profileVisibilityEcoTax: "ECO-налог", + profileVisibilityLarpSign: "Печать L.A.R.P.", profileVisibilityGpg: "Ключ GPG", - profileVisibilityClearnet: "Опубликовать мой профиль", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Магазины", profileClearnetJobsLabel: "Вакансии", @@ -725,6 +781,10 @@ module.exports = { profileClearnetImagesLabel: "Изображения", profileClearnetDocumentsLabel: "Документы", profileClearnetTorrentsLabel: "Торренты", + profileClearnetPodcastsLabel: "Подкасты", + profileClearnetMarketLabel: "Рынок", + profileClearnetFeedLabel: "Лента", + profileClearnetWikiLabel: "Вики", editProfileDescription: "", profileName: "Имя", @@ -750,7 +810,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "Подтв.", walletConfirm: "Подтвердить", - walletDescription: "Управляйте цифровыми активами: отправка и получение ECOin, просмотр баланса и история транзакций.", + walletDescription: "Управляйте своим кошельком ECOin.", walletDate: "Дата", walletFee: "Комиссия (чем выше комиссия, тем быстрее обработка транзакции)", walletFeeLine: ({ fee }) => `Комиссия: ECO ${fee}`, @@ -758,6 +818,86 @@ module.exports = { walletReceive: "Получить", walletReset: "Сбросить", walletSend: "Отправить", + walletPayingTransfer: "Оплата перевода", + walletCreateTransferLabel: "Создать смарт-контракт (перевод)", + walletTransferNotifiedNote: "Другая сторона уведомлена об этом платеже", + walletTransferCreatedNote: "Для этого платежа создан перевод (квитанция)", + walletTransferNoPayeeNote: "Никто не опубликовал этот адрес, перевод не создан", + walletTransferReceiptConcept: "Платёж из кошелька", + walletPaymentPmText: "отправил(а) вам платёж", + walletPayeeLabel: "Получатель", + walletConceptLabel: "Назначение", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "Вы получили свой ББД.", + bankingBotPaymentTitle: "Вы получили платёж.", + bankingBotUbiPaidText: "Ваш безусловный базовый доход выплачен", + bankUbiPubThisNode: "Этот узел выплачивает ББД", + bankUbiPubConnected: "PUB ББД", + bankUbiPubNone: "Ни один PUB в вашей сети пока не объявляет ББД", + bankUbiAddressPublished: "Адрес ECOin опубликован", + bankUbiAddressMissing: "Опубликуйте адрес ECOin, чтобы получать ББД", + bankUbiClaimedThisMonth: "Запрошено в этом месяце", + bankUbiPubBalance: "Кошелёк PUB, доступный для ББД", + bankNoPubAddress: "PUB ББД ещё не опубликовал адрес ECOin.", + bankUbiTab: "ББД", + bankDonateEco: "ПОЖЕРТВОВАТЬ!", + donateTooltip: "Пожертвовать ECOin", + bankNoUserAddress: "Этот житель не делится адресом ECOin.", + bankUbiPubDefault: "По умолчанию", + bankUbiPool: "Фонд", + bankUbiLastSeen: "Последний раз в сети", + bankUbiLastPayout: "Последнее действие", + bankUbiPaidOut: "Выплачено", + bankRangeToday: "Сегодня", + bankRangeWeek: "Неделя", + bankRangeMonth: "Месяц", + bankRangeYear: "Год", + bankRangeFiveYears: "5 лет", + bankRangeAll: "Всё", + bankChartValueTitle: "Динамика стоимости ECOin", + bankChartValueHours: "Эквивалент во времени (ч)", + bankChartWealthTitle: "Перераспределение богатства ECOin", + bankChartDistributed: "Выплаченный ББД (ECO)", + bankChartRebalanceTitle: "Перераспределение богатства между PUB", + bankChartRebalanced: "ECO, переданные между PUB", + bankChartTaxes: "Удержанные налоги (ECO)", + bankChartNoDataRange: "Нет данных за этот период.", + bankChartNoDataYet: "Данных пока нет", + bankChartPerHour: "в час", + bankChartPerDay: "в день", + bankChartPerMonth: "в месяц", + bankTotalUbiDistributed: "Всего перераспределено ББД (сеть)", + bankTotalTaxesWithheld: "Всего удержано налогов (сеть)", + bankInflationIssuance: "Темп эмиссии (годовой, по награде за блок)", + bankRulesEligibilityTitle: "Кто может получать ББД", + bankRulesEligibility1: "Адрес ECOin, опубликованный в профиле (Профиль → Сенсоры → Кошелёк ECOin).", + bankRulesEligibility2: "Лента старше 30 дней с активностью в сети: карма выше нуля.", + bankRulesEligibility3: "Один запрос в месяц (эпоху), из Banking кнопкой Запросить ББД. От текущего месяца можно отказаться кнопкой Отказаться от ББД.", + bankRulesEligibility4: "Минимум в 1 ECO выплачивается всегда; налоги уменьшают только часть сверх него и никогда не взимаются отдельно.", + bankRulesEligibility5: "PUB платит в следующем цикле и публикует перевод ББД, который вы можете подтвердить.", + bankRulesEligibility6: "Если нет адреса, лента слишком молода, месяц отклонён или у PUB нет средств, запрос пропускается и записывается в журнал PUB.", + copyLinkTitle: "Скопировать ссылку", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "Отказаться от ББД", + bankUbiRefusedThisMonth: "Вы отказались от ББД этого месяца.", + bankingBotUbiAvailableTitle: "Вы можете запросить ББД.", + bankingBotUbiAvailableText: "Ваш ББД за этот месяц доступен", + bankingBotConfirmTitle: "У вас есть смарт-контракт, ожидающий подтверждения", + bankingBotConfirmText: "Смарт-контракты, ожидающие вашего подтверждения", + bankChartZoomHint: "Нажмите, чтобы увеличить", + bankChartPoolTitle: "Фонд ББД (во времени)", + bankChartNetworkUbiTitle: "Всего перераспределено ББД (сеть)", + bankChartSupplyTitle: "Оборотное предложение ECOin (во времени)", + bankChartInflationTitle: "Инфляция ECOin (во времени)", + bankChartInflationSeries: "Годовая ставка (%)", + bankChartPool: "Общий фонд (ECO)", + bankChartKarmaTitle: "KARMA Scoring (во времени)", + bankChartFundsTitle: "Ваши средства ECOin (во времени)", + bankChartKarma: "Карма", + bankWalletReady: "ECOin подключён", + bankWalletNotConfigured: "ECOin отключён", + transfersPayWithWallet: "ОПЛАТИТЬ!", walletStatus: "Статус", walletDisconnected: [ "ECOin ", @@ -767,9 +907,11 @@ module.exports = { " или состояние подключения.", ], walletSentToLine: ({ destination, amount }) => `Отправлено ECO ${amount} на ${destination}`, - walletSettingsTitle: "Кошелёк", + walletNotConfigured: "Ваш кошелёк ECOin ещё не настроен.", walletSettingsDescription: "Интегрируйте Oasis с вашим кошельком ECOin.", - walletSettingsDocLink: "Руководство по установке ECOin", + walletSettingsMobileDescription: "Свяжите Oasis с приложением ECOin Wallet на этом устройстве.", + walletDownloadEcoin: "Скачать ECOin!", + walletRpcUrl: "URL RPC", walletStatusMessages: { invalid_amount: "Неверная сумма", invalid_dest: "Неверный адрес назначения", @@ -785,41 +927,24 @@ module.exports = { walletUser: "Имя пользователя", walletPass: "Пароль", walletConfiguration: "Настроить кошелёк", + walletDisconnectButton: "Отключить кошелёк", cipher: "Шифратор", cipherTitle: "Шифратор", cipherDescription: "Симметричное шифрование и расшифровка текста (с использованием общего пароля).", randomPassword: "Случайный пароль", - cipherEncryptTitle: "Зашифровать текст", - cipherEncryptDescription: "Введите текст для шифрования", - cipherTextLabel: "Текст для шифрования", cipherTextPlaceholder: "Введите текст для шифрования...", cipherPasswordLabel: "Установите пароль (минимум 32 символа) для шифрования текста", cipherPasswordDecryptLabel: "Установите пароль (минимум 32 символа) для расшифровки текста", cipherPasswordPlaceholder: "Введите пароль...", cipherEncryptButton: "Зашифровать", - cipherDecryptTitle: "Расшифровать текст", - cipherDecryptDescription: "Введите текст для расшифровки", cipherEncryptedMessageLabel: "Зашифрованный текст", cipherDecryptedMessageLabel: "Расшифрованный текст", - cipherPasswordUsedLabel: "Пароль, использованный для шифрования (сохраните его!)", cipherEncryptedTextPlaceholder: "Введите зашифрованный текст...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "Введите вектор инициализации...", cipherDecryptButton: "Расшифровать", password: "Пароль", text: "Текст", encryptedText: "Зашифрованный текст", iv: "Вектор инициализации (IV)", - encryptTitle: "Зашифровать текст", - encryptDescription: "Введите текст для шифрования и укажите пароль.", - encryptButton: "Зашифровать", - decryptTitle: "Расшифровать текст", - decryptDescription: "Введите зашифрованный текст и укажите тот же пароль, что использовался при шифровании.", - decryptButton: "Расшифровать", - passwordLengthError: "Пароль должен содержать минимум 32 символа.", - missingFieldsError: "Текст, пароль или IV не указаны.", - encryptionError: "Ошибка шифрования текста.", - decryptionError: "Ошибка расшифровки текста.", bookmarkTitle: "Закладки", bookmarkDescription: "Находите и управляйте закладками в вашей сети.", bookmarkAllSectionTitle: "Закладки", @@ -836,19 +961,16 @@ module.exports = { bookmarkFilterRecent: "НЕДАВНИЕ", bookmarkCreateButton: "Создать закладку", bookmarkUpdateButton: "Обновить", - bookmarkDeleteButton: "Удалить", bookmarkAddFavoriteButton: "В избранное", bookmarkRemoveFavoriteButton: "Из избранного", bookmarkUrlLabel: "Ссылка", - bookmarkUrlPlaceholder: "https://example.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Описание", bookmarkDescriptionPlaceholder: "Необязательно", bookmarkTagsLabel: "Теги", bookmarkTagsPlaceholder: "Введите теги через запятую", - bookmarkCategoryLabel: "Категория", - bookmarkCategoryPlaceholder: "Необязательно", bookmarkLastVisitLabel: "Последний визит", - bookmarkSearchPlaceholder: "Искать URL, теги, категорию, автора...", + bookmarkSearchPlaceholder: "Поиск в Закладки...", bookmarkSortRecent: "Новейшие", bookmarkSortOldest: "Старейшие", bookmarkSortTop: "Больше голосов", @@ -861,8 +983,6 @@ module.exports = { noLastVisit: "Нет записи о посещении", videoTitle: "Видео", videoDescription: "Просматривайте и управляйте видеоконтентом в вашей сети.", - videoPluginTitle: "Название", - videoPluginDescription: "Описание", videoMineSectionTitle: "Ваши видео", videoCreateSectionTitle: "Загрузить видео", videoUpdateSectionTitle: "Обновить видео", @@ -889,12 +1009,11 @@ module.exports = { videoDescriptionPlaceholder: "Необязательно", videoNoFile: "Видеофайл не указан", noVideos: "Нет доступных видео.", - videoSearchPlaceholder: "Искать название, теги, автора...", + videoSearchPlaceholder: "Поиск в Видео...", videoSortRecent: "Новейшие", videoSortOldest: "Старейшие", videoSortTop: "Больше голосов", videoSearchButton: "Искать", - videoMessageAuthorButton: "ЛС", videoUpdatedAt: "Обновлено", videoNoMatch: "Видео не найдены.", documentTitle: "Документы", @@ -913,7 +1032,6 @@ module.exports = { documentFilterFavorites: "ИЗБРАННОЕ", documentCreateButton: "Загрузить документ", documentUpdateButton: "Обновить", - documentDeleteButton: "Удалить", documentAddFavoriteButton: "В избранное", documentRemoveFavoriteButton: "Из избранного", documentMessageAuthorButton: "ЛС", @@ -926,7 +1044,7 @@ module.exports = { documentDescriptionPlaceholder: "Необязательно", documentNoFile: "Нет файла.", noDocuments: "Нет доступных документов.", - documentSearchPlaceholder: "Искать название, теги, описание, автора...", + documentSearchPlaceholder: "Поиск в Документы...", documentSortRecent: "Новейшие", documentSortOldest: "Старейшие", documentSortTop: "Больше голосов", @@ -935,8 +1053,6 @@ module.exports = { documentUpdatedAt: "Обновлено", audioTitle: "Аудио", audioDescription: "Просматривайте и управляйте аудиоконтентом в вашей сети.", - audioPluginTitle: "Название", - audioPluginDescription: "Описание", audioMineSectionTitle: "Ваши аудио", audioCreateSectionTitle: "Загрузить аудио", audioUpdateSectionTitle: "Обновить аудио", @@ -947,22 +1063,21 @@ module.exports = { audioFilterMine: "МОИ", audioFilterRecent: "НЕДАВНИЕ", audioFilterTop: "ЛУЧШИЕ", - audioFilterBlockchain: "БЛОКЧЕЙН", audioCreateButton: "Загрузить аудио", audioUpdateButton: "Обновить", - audioDeleteButton: "Удалить", audioAddFavoriteButton: "В избранное", audioRemoveFavoriteButton: "Из избранного", audioFileLabel: "Выберите аудиофайл (.mp3, .wav, .ogg)", audioTagsLabel: "Теги", audioTagsPlaceholder: "Введите теги через запятую", + tagsPlaceholder: "Введите теги через запятую", audioTitleLabel: "Название", audioTitlePlaceholder: "Необязательно", audioDescriptionLabel: "Описание", audioDescriptionPlaceholder: "Необязательно", audioNoFile: "Аудиофайл не указан", noAudios: "Нет доступных аудио.", - audioSearchPlaceholder: "Искать название, теги, автора...", + audioSearchPlaceholder: "Поиск в Аудио...", audioSortRecent: "Новейшие", audioSortOldest: "Старейшие", audioSortTop: "Больше голосов", @@ -974,6 +1089,7 @@ module.exports = { audioFilterFavorites: "ИЗБРАННОЕ", favoritesTitle: "Избранное", favoritesDescription: "Весь ваш избранный контент в одном месте.", + favoritesSearchPlaceholder: "Поиск в Избранное...", favoritesFilterAll: "ВСЕ", favoritesFilterRecent: "НЕДАВНИЕ", favoritesFilterAudios: "АУДИО", @@ -985,22 +1101,16 @@ module.exports = { favoritesFilterChats: "ЧАТЫ", favoritesFilterCalendars: "КАЛЕНДАРИ", favoritesFilterVideos: "ВИДЕО", - favoritesRemoveButton: "Убрать из избранного", favoritesNoItems: "Пока нет избранного.", yourContacts: "Ваши контакты", allInhabitants: "Жители", - allCVs: "Все резюме", + allCVs: "Резюме", discoverPeople: "Находите жителей в вашей сети.", - allInhabitantsButton: "ВСЕ", - contactsButton: "ПОДДЕРЖКА", - CVsButton: "Резюме", - matchSkills: "Совпадение навыков", - matchSkillsButton: "СОВПАДЕНИЕ НАВЫКОВ", - suggestedButton: "РЕКОМЕНДОВАННЫЕ", - searchInhabitantsPlaceholder: "ФИЛЬТР жителей ПО ИМЕНИ...", + searchInhabitantsPlaceholder: "Поиск в Жители...", filterLocation: "ФИЛЬТР жителей ПО МЕСТОПОЛОЖЕНИЮ...", filterLanguage: "ФИЛЬТР жителей ПО ЯЗЫКУ...", filterSkills: "ФИЛЬТР жителей ПО НАВЫКАМ...", + cvSkillsCount: "Навыки", applyFilters: "Применить фильтры", locationLabel: "Местоположение", languagesLabel: "Языки", @@ -1009,15 +1119,15 @@ module.exports = { mutualFollowers: "Общие подписчики", suggestedFollowsYou: "Подписан на тебя", latestInteractions: "Последние взаимодействия", - viewAvatar: "Смотреть аватар", - viewCV: "Смотреть резюме", suggestedSectionTitle: "Рекомендованные", topkarmaSectionTitle: "Лучшая Карма", topecoSectionTitle: "Лучшие Эко", topactivitySectionTitle: "Наибольшая активность", + "TOP INACTIVITYButton": "НЕАКТИВНЫЕ", + topinactivitySectionTitle: "Наименее активные жители", blockedSectionTitle: "Заблокированные", gallerySectionTitle: "ГАЛЕРЕЯ", - blockedButton: "ЗАБЛОКИРОВАННЫЕ", + topSectionTitle: "ТОП", blockedLabel: "Заблокированный пользователь", inhabitantviewDetails: "Подробнее", keepReading: "Продолжить чтение...", @@ -1029,12 +1139,25 @@ module.exports = { parliamentDescription: "Изучайте формы правления и коллективные законы управления.", parliamentFilterGovernment: "ПРАВИТЕЛЬСТВО", parliamentFilterCandidatures: "КАНДИДАТУРЫ", - parliamentFilterProposals: "ЗАКОНОПРОЕКТЫ", + parliamentFilterProposals: "ПРЕДЛОЖЕНИЯ", parliamentFilterLaws: "ЗАКОНЫ", + parliamentFilterRevocations: "ОТЗЫВЫ", parliamentFilterHistorical: "ИСТОРИЯ", parliamentFilterLeaders: "ЛИДЕРЫ", parliamentFilterRules: "ПРАВИЛА", parliamentGovernmentCard: "Текущее правительство", + parliamentSeatsGovernment: "Правительство", + parliamentSeatsOpposition: "Оппозиция", + parliamentSeatsAbstention: "Воздержались", + parliamentSeatsQuorum: "Кворум выборов", + parliamentSeatsPass: "Порог одобрения", + parliamentSeatsByElection: "По выборам", + parliamentSeatsByHouses: "По домам LARP", + parliamentSeatsSeats: "мест", + parliamentSeatsSeat: "место", + parliamentSeatsLeader: "Лидер", + parliamentSeatsVotesFor: "голоса за", + parliamentSeatsNoHouse: "Без дома", parliamentActorInPowerInhabitant: 'ПРАВЯЩИЙ ЖИТЕЛЬ', parliamentActorInPowerTribe: 'ПРАВЯЩЕЕ ПЛЕМЯ', parliamentHistoricalGovernmentsTitle: 'ПРАВИТЕЛЬСТВА', @@ -1056,10 +1179,8 @@ module.exports = { parliamentPoliciesDeclined: "ОТКЛОНЁННЫЕ ЗАКОНЫ", parliamentPoliciesDiscarded: "ОТМЕНЁННЫЕ ЗАКОНЫ", parliamentEfficiency: "% ЭФФЕКТИВНОСТЬ", - parliamentFilterRevocations: 'ОТЗЫВЫ', parliamentRevocationFormTitle: 'Отозвать закон', parliamentRevocationLaw: 'Закон', - parliamentRevocationTitle: 'Название', parliamentRevocationReasons: 'Причины', parliamentRevocationPublish: 'Опубликовать отзыв', parliamentCurrentRevocationsTitle: 'Текущие отзывы', @@ -1072,27 +1193,15 @@ module.exports = { parliamentNoGovernments: "Правительств пока нет.", parliamentCandidatureFormTitle: "Предложить кандидатуру", parliamentCandidatureIdPh: "Oasis ID (@...) или название племени", - parliamentCandidatureSlogan: "Слоган (макс. 140 символов)", - parliamentCandidatureSloganPh: "Краткий девиз", parliamentCandidatureMethod: "Метод", parliamentCandidatureProposeBtn: "Опубликовать кандидатуру", - parliamentThType: "Тип", parliamentThId: "ID", - parliamentThDate: "Дата предложения", - parliamentThSlogan: "Слоган", parliamentThMethod: "Метод", parliamentThKarma: "Карма", - parliamentThSince: "Профиль с", - parliamentThVotes: "Получено голосов", - parliamentThVoteAction: "Голосовать", - parliamentTypeUser: "Житель", - parliamentTypeTribe: "Племя", parliamentVoteBtn: "Голосовать", parliamentProposalFormTitle: "Предложить закон", parliamentProposalDescription: "Описание (до 1000 символов)", parliamentProposalPublish: "Опубликовать законопроект", - parliamentFinalize: "Завершить", - parliamentDeadline: "Крайний срок", parliamentNoProposals: "Законопроектов пока нет.", parliamentNoLaws: "Принятых законов пока нет.", parliamentMethodDEMOCRACY: "Демократия", @@ -1107,28 +1216,22 @@ module.exports = { parliamentVotesSlashTotal: "Голоса/Всего", parliamentVotesNeeded: "Необходимо голосов", parliamentFutureLawsTitle: "Будущие законы", - parliamentNoFutureLaws: "Будущих законов пока нет.", parliamentVoteAction: "Голосовать", - parliamentLeadersTitle: "Лидеры", parliamentThLeader: "АВАТАР", parliamentPopulation: "Население", - parliamentThInPower: "У власти", parliamentThPresented: "КАНДИДАТУРЫ", parliamentNoLeaders: "Лидеров пока нет.", parliamentCandidaturesListTitle: "Список кандидатур", parliamentTimeRemaining: "Оставшееся время", - parliamentNoLeader: "Лидирующей кандидатуры пока нет.", parliamentElectionsStatusTitle: "Следующее правительство", parliamentProposalDeadlineLabel: "Крайний срок", + parliamentViewReport: "Посмотреть отчёт", parliamentProposalTimeLeft: "Осталось времени", - parliamentProposalOnTrack: "На пути к принятию", - parliamentProposalOffTrack: "Недостаточно поддержки", parliamentRulesTitle: "Как работает Парламент", parliamentRulesIntro: "Выборы проводятся каждые 2 месяца; кандидатуры подаются непрерывно и сбрасываются при выборе правительства.", parliamentRulesCandidates: "Любой житель может выдвинуть себя, другого жителя или любое племя. Каждый житель может предложить до 3 кандидатур за цикл; дубликаты в одном цикле отклоняются.", parliamentRulesElection: "Побеждает кандидатура, набравшая наибольшее количество голосов на момент подведения итогов.", parliamentRulesTies: "Порядок разрешения ничьих: наивысшая karma жителя; при равенстве — самый старый профиль; при дальнейшем равенстве — самое раннее предложение; затем лексикографически по ID.", - parliamentRulesFallback: "Если никто не голосует, побеждает последняя предложенная кандидатура. Если кандидатур нет, выбирается случайное племя.", parliamentRulesTerm: "На вкладке «Правительство» отображается текущее правительство и статистика.", parliamentRulesMethods: "Формы: Анархия (простое большинство), Демократия (50%+1), Большинство (80%), Меньшинство (20%), Карматократия (законопроекты с наивысшей karma), Диктатура (мгновенное утверждение).", parliamentRulesAnarchy: "Анархия — режим по умолчанию: если ни одна кандидатура не избрана на момент подведения итогов, провозглашается Анархия. При Анархии любой житель может предлагать законы.", @@ -1137,6 +1240,7 @@ module.exports = { parliamentRulesLaws: "Когда законопроект набирает необходимый порог, он становится Законом и появляется на вкладке «Законы» с датой принятия.", parliamentRulesHistorical: "На вкладке «История» вы можете увидеть каждый цикл правления и данные о его управлении.", parliamentRulesLeaders: "На вкладке «Лидеры» вы можете увидеть рейтинг жителей/племён, которые правили (или выдвигались), отсортированный по эффективности.", + parliamentRulesHemicycle: "Полукруг даёт одно место каждому жителю: блок правительства (избиратели победившей кандидатуры и лидер), по блоку оппозиции на каждую другую кандидатуру цикла и воздержавшиеся. Пунктирные отметки показывают кворум выборов (25 % от переписи) и порог одобрения текущего метода. «По домам LARP» раскрашивает те же места по домам.", parliamentProposalVoteStatusLabel: "Статус голосования", parliamentProposalOnTrackYes: "Порог достигнут", parliamentProposalOnTrackNo: "Ниже порога", @@ -1150,11 +1254,8 @@ module.exports = { courtsCaseFormTitle: "Открыть дело", courtsCaseRespondent: "Обвиняемый / Ответчик", courtsCaseRespondentPh: "Oasis ID (@...) или название племени", - courtsCaseMediatorsAccuser: "Посредники (обвинитель)", + courtsCaseSubjectPlaceholder: "Тема или краткое описание", courtsCaseMethod: "Метод разрешения", - courtsCaseDescription: "Описание (макс. 1000 символов)", - courtsCaseEvidenceTitle: "Доказательства по делу", - courtsCaseEvidenceHelp: "Прикрепите изображения, аудио, документы (PDF) или видео, подтверждающие вашу позицию.", courtsCaseSubmit: "Подать дело", courtsNominateJudge: "Выдвинуть судью", courtsJudgeId: "Судья", @@ -1187,34 +1288,33 @@ module.exports = { courtsMethodDICTATOR: "Диктатор", courtsMethodPOPULAR: "Народный", courtsMethodKARMATOCRACY: "Карматократия", + courtsCaseType: "Тип дела", + courtsPresetMinorConflict: "Незначительный конфликт", + courtsPresetModerateConflict: "Умеренный конфликт", + courtsPresetSevereConflict: "Серьёзный конфликт", + courtsPresetHarassment: "Домогательства или злоупотребления", + courtsPresetContentModeration: "Модерация контента", + courtsPresetBanRestriction: "Бан или ограничение", + courtsPresetGovernanceDispute: "Спор об управлении", + courtsStatusOPEN: "Открыто", + courtsStatusIN_PROGRESS: "В процессе", + courtsStatusDECIDED: "Решено", + courtsStatusCLOSED: "Закрыто", + courtsStatusSOLVED: "Урегулировано", + courtsStatusUNSOLVED: "Не урегулировано", + courtsStatusDISCARDED: "Отклонено", courtsRulesTitle: "Как работают Суды", courtsRulesIntro: "Суды — это общественный процесс разрешения конфликтов и содействия восстановительному правосудию. Приоритет отдаётся диалогу, чётким доказательствам и соразмерным мерам.", courtsRulesLifecycle: "Процесс: 1) Открытие дела 2) Выбор метода 3) Представление доказательств 4) Слушание и обсуждение 5) Вердикт и средства защиты 6) Исполнение и закрытие 7) Апелляция (при необходимости).", - courtsRulesRoles: "Обвинитель: открывает дело. Защита: обвиняемое лицо или племя. Метод: механизм, выбранный сообществом для содействия, оценки доказательств и вынесения вердикта. Свидетель: предоставляет показания или доказательства. Посредники: нейтральные лица, приглашённые обвинителем и/или защитой, имеющие доступ ко всем деталям, которые помогают деэскалировать конфликт и совместно выработать соглашения.", + courtsRulesRoles: "Обвинитель: открывает дело. Защита: обвиняемый житель или племя. Метод: механизм, выбранный сообществом для содействия, оценки доказательств и вынесения вердикта. Свидетель: предоставляет показания или доказательства. Посредники: нейтральные жители, приглашённые обвинителем и/или защитой, имеющие доступ ко всем деталям, которые помогают деэскалировать конфликт и совместно выработать соглашения.", courtsRulesEvidence: "Описание до 1000 символов. Прикрепляйте релевантные и законные изображения, аудио, видео и PDF-документы. Не передавайте конфиденциальные личные данные без согласия.", courtsRulesDeliberation: "Слушания могут быть публичными или закрытыми. Судьи обеспечивают уважение, запрашивают уточнения и могут отклонить нерелевантные или незаконные материалы.", courtsRulesVerdict: "Приоритет отдаётся восстановлению: извинения, соглашения о медиации, модерация контента, временные ограничения или другие соразмерные меры. Обоснование должно быть зафиксировано.", courtsRulesAppeals: "Апелляция: допускается при наличии новых доказательств или явной процедурной ошибки. Должна быть подана в течение 7 дней, если не указано иное.", - courtsRulesPrivacy: "Уважайте конфиденциальность и безопасность. Доксинг, ненависть или угрозы удаляются. Судьи могут редактировать или засекречивать части записи для защиты людей в зоне риска.", + courtsRulesPrivacy: "Уважайте конфиденциальность и безопасность. Доксинг, ненависть или угрозы удаляются. Судьи могут редактировать или засекречивать части записи для защиты жителей в зоне риска.", courtsRulesMisconduct: "Преследование, манипуляции или фабрикация доказательств могут привести к немедленному негативному решению.", courtsRulesGlossary: "Дело: запись о конфликте. Доказательство: материалы, подтверждающие утверждения. Вердикт: решение с мерой пресечения. Апелляция: запрос на пересмотр вердикта.", courtsFilterActions: "ДЕЙСТВИЯ", - courtsNoActions: "Нет ожидающих действий для вашей роли.", - courtsCaseTitlePlaceholder: "Краткое описание конфликта", - courtsCaseSeverity: "Серьёзность", - courtsCaseSeverityNone: "Без метки серьёзности", - courtsCaseSeverityLOW: "Низкая", - courtsCaseSeverityMEDIUM: "Средняя", - courtsCaseSeverityHIGH: "Высокая", - courtsCaseSeverityCRITICAL: "Критическая", - courtsCaseSubject: "Тема", - courtsCaseSubjectNone: "Без метки темы", - courtsCaseSubjectBEHAVIOUR: "Поведение", - courtsCaseSubjectCONTENT: "Контент", - courtsCaseSubjectGOVERNANCE: "Управление / правила", - courtsCaseSubjectFINANCIAL: "Финансы / ресурсы", - courtsCaseSubjectOTHER: "Другое", - courtsHiddenRespondent: "Скрыто (видно только участникам дела).", courtsThRole: "Роль", courtsRoleAccuser: "Обвинитель", courtsRoleDefence: "Защита", @@ -1225,52 +1325,23 @@ module.exports = { courtsAssignJudgeBtn: "Выбрать судью", trendingTitle: "В тренде", exploreTrending: "Изучайте самый популярный контент в вашей сети.", - RECENTButton: "НЕДАВНИЕ", - bookmarkButton: "ЗАКЛАДКИ", - transferButton: "ПЕРЕВОДЫ", - eventButton: "СОБЫТИЯ", - taskButton: "ЗАДАЧИ", votesButton: "ГОЛОСОВАНИЯ", - reportButton: "ОТЧЁТЫ", - feedButton: "ЛЕНТА", - marketButton: "РЫНОК", - imageButton: "ИЗОБРАЖЕНИЯ", - audioButton: "АУДИО", - videoButton: "ВИДЕО", - documentButton: "ДОКУМЕНТЫ", - torrentButton: "ТОРРЕНТЫ", - noTrendingFound: "Трендовый контент не найден.", - noContentMessage: "Трендового контента пока нет.", - trendingDescription: "Описание", - trendingDate: "Дата", - trendingLocation: "Местоположение", - trendingPrice: "Цена", - trendingUrl: "URL", - trendingCategory: "Категория", - trendingStart: "Начало", - trendingEnd: "Конец", - trendingPriority: "Приоритет", - trendingStatus: "Статус", - trendingFrom: "От", - trendingTo: "Кому", - trendingConcept: "Концепция", - trendingAmount: "Сумма", - trendingDeadline: "Крайний срок", - trendingItemStatus: "Статус элемента", - trendingTotalVotes: "Всего голосов", + shopProductButton: "МАГАЗИН", + podcastButton: "ПОДКАСТ", + podcastEpisodeButton: "ЭПИЗОД", + campaignButton: "КАМПАНИЯ", + logisticsRouteButton: "МАРШРУТ", trendingTotalOpinions: "Всего мнений", - trendingNoContentMessage: "Трендового контента пока нет.", - trendingAuthor: "Автор", - trendingCreatedAtLabel: "Создано", + trendingNoContentMessage: "Трендов пока нет.", trendingTotalCount: "Всего", tasksTitle: "Задачи", tasksDescription: "Находите и управляйте задачами в вашей сети.", + taskSearchPlaceholder: "Поиск в Задачи...", taskTitleLabel: "Название", taskDescriptionLabel: "Описание", taskStartTimeLabel: "Время начала", taskEndTimeLabel: "Время окончания", taskPriorityLabel: "Приоритет", - taskPrioritySelect: "Выберите приоритет", taskPriorityUrgent: "Срочный", taskPriorityHigh: "Высокий", taskPriorityMedium: "Средний", @@ -1280,13 +1351,13 @@ module.exports = { taskVisibilityLabel: "Видимость", taskPublic: "публичная", taskPrivate: "приватная", - taskCreatedAt: "Создано", - taskBy: "Автор", taskStatus: "Статус", taskStatusOpen: "Открыта", taskStatusInProgress: "В работе", taskStatusClosed: "Закрыта", taskAssignedTo: "Назначено", + taskAssignedChip: "Назначено", + taskUnassignedChip: "Не назначено", taskAssignees: "Исполнители", taskAssignButton: "Назначить себе", taskUnassignButton: "Снять назначение", @@ -1294,12 +1365,12 @@ module.exports = { taskUpdateButton: "Обновить", taskDeleteButton: "Удалить", taskFilterAll: "ВСЕ", + taskFilterRecent: "НЕДАВНИЕ", taskFilterMine: "МОИ", taskFilterOpen: "ОТКРЫТЫЕ", taskFilterInProgress: "В РАБОТЕ", taskFilterClosed: "ЗАКРЫТЫЕ", taskFilterAssigned: "НАЗНАЧЕННЫЕ", - taskFilterArchived: "АРХИВ", taskFilterUrgent: "СРОЧНЫЕ", taskFilterHigh: "ВЫСОКИЙ", taskFilterMedium: "СРЕДНИЙ", @@ -1312,35 +1383,47 @@ module.exports = { taskInProgressTitle: "Задачи в работе", taskClosedTitle: "Закрытые задачи", taskAssignedTitle: "Назначенные задачи", - taskArchivedTitle: "Архивные задачи", - taskPublicTitle: "Публичные задачи", - taskPrivateTitle: "Приватные задачи", notasks: "Нет доступных задач.", noLocation: "Местоположение не указано", taskSetStatus: "Установить статус", - eventTitle: "Событие", eventDateLabel: "Дата", + eventRecurrenceLabel: "Повторение", + eventRecurrenceUntil: "Повторять до", + eventRecurrenceHint: "Оставьте дату окончания пустой для разового события.", + eventUpcomingDates: "Ближайшие даты", eventsTitle: "События", eventsDescription: "Находите и управляйте событиями в вашей сети.", - eventDescription: "Описание", + eventSearchPlaceholder: "Поиск в События...", eventPrice: "Цена", eventStatus: "Статус", - eventOrganizer: "Организатор", eventAllSectionTitle: "События", eventMineSectionTitle: "Ваши события", - eventArchivedTitle: "Архивные события", eventCreateSectionTitle: "Создать событие", eventUpdateSectionTitle: "Обновить событие", - eventDeleteButton: "Удалить", eventCreateButton: "Создать событие", eventAddToCalendar: "Добавить в календарь", eventVisitCalendar: "Открыть календарь", viewTask: "Открыть задачу", + viewEvent: "Открыть событие", + viewPad: "Открыть пад", + viewMap: "Открыть карту", + viewCalendar: "Открыть календарь", viewReport: "Открыть отчёт", viewTransfer: "Открыть перевод", viewItem: "Открыть объект", viewShop: "Открыть магазин", viewProject: "Открыть проект", + viewJob: "Смотреть Вакансию", + viewPlace: "Смотреть Место", + generatePdf: "Создать PDF", + sharePm: "Отправить в ЛС", + galleryImages: "Изображения (макс. 50 МБ каждое)", + galleryPhotos: "Изображения", + galleryAddPhoto: "Добавить изображение", + galleryRemovePhoto: "Удалить", + galleryVideo: "Видео (макс. 50 МБ)", + galleryAddVideo: "Добавить видео", + galleryRemoveVideo: "Удалить видео", viewVotation: "Открыть голосование", voteCastTitle: "Проголосовать", voteResults: "Результаты", @@ -1348,48 +1431,26 @@ module.exports = { eventDescriptionLabel: "Описание", eventDescriptionPlaceholder: "Введите описание события...", eventUpdateButton: "Обновить", - eventAttendeesLabel: "Участники", - eventAttendeesPlaceholder: "Введите участников через запятую...", eventTagsLabel: "Теги", - eventTagsPlaceholder: "Введите теги через запятую...", - eventTags: "Теги", eventPriceLabel: "Цена", eventUrlLabel: "URL", eventAttendees: "Участники", - noAttendees: "Участников пока нет", - eventCreatedAt: "Создано", eventLocation: "Местоположение", eventLocationLabel: "Местоположение", - eventNoLocation: "Местоположение не указано", - eventNoURL: "URL не указан", - eventBy: "Автор", noevents: "Нет доступных событий.", eventDate: "Дата", - eventDateFormat: "ДД:ММ:ГГГГ ЧЧ:мм", eventAttendButton: "Принять участие", - eventUnattendButton: "Отменить участие", - eventCreatedBy: "Создано", - eventAttendeesCount: "Количество участников", - eventCreatedByYou: "Вы создали это событие", - eventAttendConfirmation: "Вы теперь участвуете в этом событии", - eventUnattendConfirmation: "Вы больше не участвуете в этом событии", eventFilterAll: "ВСЕ", + eventFilterRecent: "НЕДАВНИЕ", eventFilterMine: "МОИ", eventFilterToday: "СЕГОДНЯ", eventFilterWeek: "ЭТА НЕДЕЛЯ", eventFilterMonth: "ЭТОТ МЕСЯЦ", eventFilterYear: "ЭТОТ ГОД", eventFilterArchived: "АРХИВ", - eventTodayTitle: "Сегодняшние события", - eventThisWeekTitle: "События этой недели", - eventThisMonthTitle: "События этого месяца", - eventThisYearTitle: "События этого года", eventPrivacyLabel: "Видимость", eventPublic: "Публичное", eventPrivate: "Приватное", - eventPublicTitle: "Публичные события", - eventNoPrice: "Бесплатное событие", - eventNoImage: "Изображение не загружено", eventAttended: "Участвует", eventUnattended: "Не участвует", eventStatusOpen: "Открыто", @@ -1400,9 +1461,14 @@ module.exports = { tagsTopSectionTitle: "Популярные теги", tagsCloudSectionTitle: "Облако тегов", tagsFilterAll: "ВСЕ", + tagsFilterMine: "МОИ", + tagsFilterRecent: "НОВЫЕ", + tagsMineSectionTitle: "Мои теги", + tagsRecentSectionTitle: "Новые теги", tagsFilterTop: "ЛУЧШИЕ", tagsFilterCloud: "ОБЛАКО", tagsNoItems: "Нет доступных тегов.", + tagsSearchPlaceholder: "Поиск в Теги...", tagsTableHeaderTag: "ТЕГ/ССЫЛКА:", tagsTableHeaderCount: "СЧЁТЧИК:", transfersTitle: "Переводы", @@ -1410,8 +1476,11 @@ module.exports = { transfersFrom: "От", transfersTo: "Кому", transfersFilterAll: "ВСЕ", + transfersFilterRecent: "НЕДАВНИЕ", transfersFilterMine: "МОИ", - transfersFilterUBI: "UBI", + transfersFilterMinePending: "ОЖИДАЮТ", + transfersFilterMineConfirmed: "ПОДПИСАННЫЕ", + transfersFilterUBI: "ББД", transfersFilterMarket: "РЫНОК", transfersFilterTop: "ЛУЧШИЕ", transfersFilterPending: "ОЖИДАЮЩИЕ", @@ -1425,7 +1494,6 @@ module.exports = { transfersCategoryEconomic: "Экономический", transfersCategoryTime: "Время", transfersCategoryTrust: "Доверие", - transfersCategoryApply: "Применить", transfersUnitEco: "ECO", transfersUnitHours: "ч", transfersUnitTrust: "доверие", @@ -1442,18 +1510,12 @@ module.exports = { transfersDeadline: "Крайний срок", transfersTags: "Теги", transfersStatus: "Статус", - transfersStatusUnconfirmed: "НЕПОДТВЕРЖДЁН", - transfersStatusClosed: "ЗАКРЫТ", - transfersStatusDiscarded: "ОТМЕНЁН", - transfersCreatedAt: "Создано", transfersConfirmations: "ПОДТВЕРЖДЕНИЯ", - transfersContainingBlock: "Содержащий блок", - transfersExportContract: "Создать контракт", + transfersExportContract: "Создать Счёт", transfersConfirmButton: "Подтвердить перевод", transfersNoItems: "Переводы не найдены.", transfersMineSectionTitle: "Ваши переводы", transfersUBISectionTitle: "UBI переводы", - transfersMarketSectionTitle: "Рыночные переводы", transfersTopSectionTitle: "Лучшие переводы", transfersPendingSectionTitle: "Ожидающие переводы", transfersUnconfirmedSectionTitle: "Неподтверждённые переводы", @@ -1461,21 +1523,13 @@ module.exports = { transfersDiscardedSectionTitle: "Отменённые переводы", transfersCreateSectionTitle: "Создать перевод", transfersAllSectionTitle: "Переводы", - transfersFilterFavs: "Избранное", - transfersFavsSectionTitle: "Избранные переводы", - transfersSearchLabel: "Поиск", - transfersSearchPlaceholder: "Искать концепцию, теги, пользователей...", + transfersSearchPlaceholder: "Поиск в Переводы...", transfersMinAmountLabel: "Мин. сумма", transfersMaxAmountLabel: "Макс. сумма", - transfersSortLabel: "Сортировать по", transfersSortRecent: "Новейшие", transfersSortAmount: "Наибольшая сумма", transfersSortDeadline: "Ближайший срок", transfersSearchButton: "Искать", - transfersFavoriteButton: "В избранное", - transfersUnfavoriteButton: "Из избранного", - transfersMessageUserButton: "Написать", - transfersExpiringSoonBadge: "ИСТЕКАЕТ", transfersExpiredBadge: "ИСТЁК", transfersUpdatedAt: "Обновлено", transfersNoMatch: "Переводы не найдены по вашему запросу.", @@ -1488,6 +1542,10 @@ module.exports = { voteClosedTitle: "Закрытые голосования", voteAllSectionTitle: "Голосования", voteCreateButton: "Создать голосование", + voteErrorTitle: "Проверьте форму", + voteErrorDeadlineMin: "Срок должен быть не менее {days} дней от текущего момента.", + voteErrorGeneric: "Не удалось сохранить голосование.", + voteLocked: "За это голосование уже голосовали или оставляли мнения, или оно принадлежит Парламенту или Судам: его больше нельзя изменить или удалить.", voteUpdateButton: "Обновить", voteDeleteButton: "Удалить", voteOptionYes: "ЗА", @@ -1497,6 +1555,7 @@ module.exports = { voteFollowMajority: "СЛЕДОВАТЬ БОЛЬШИНСТВУ", voteNotInterested: "НЕ ИНТЕРЕСНО", voteFilterAll: "ВСЕ", + voteFilterRecent: "НЕДАВНИЕ", voteFilterMine: "МОИ", voteFilterOpen: "ОТКРЫТЫЕ", voteFilterClosed: "ЗАКРЫТЫЕ", @@ -1512,37 +1571,28 @@ module.exports = { voteTags: "Теги", voteOpinions: "Мнения", novotes: "Нет доступных голосований.", - voteBy: "Автор", voteCreatedAt: "Создано", voteNoQuestion: "Вопрос не указан", voteUnknownCreator: "Неизвестный автор", voteUnknownDate: "Неизвестная дата", - errorVoteNotFound: "Голосование не найдено", - errorAlreadyVoted: "Вы уже высказали мнение.", - errorVoteClosedCannotEdit: "Нельзя редактировать закрытое голосование", - errorVoteDeadlinePassed: "Срок этого голосования истёк", - errorRetrievingVote: "Ошибка получения голосования", - errorCreatingVote: "Ошибка создания голосования", - errorVoteAlreadyVoted: "Нельзя редактировать после голосования", - errorDeletingOldVote: "Ошибка удаления старого мнения", - errorCreatingUpdatedVote: "Ошибка создания обновлённого мнения", - errorCreatingTombstone: "Ошибка создания записи об удалении", voteDetailSectionTitle: 'Детали голосования', voteCommentsLabel: 'Комментарии', voteCommentsForumButton: 'Открыть обсуждение', voteCommentsSectionTitle: 'Открыть обсуждение', voteNoCommentsYet: 'Комментариев пока нет. Будьте первым.', + commentsGoToLast: "Перейти к последнему комментарию", voteNewCommentPlaceholder: 'Напишите ваш комментарий...', voteNewCommentButton: 'Отправить комментарий', voteNewCommentLabel: 'Добавить комментарий', cvTitle: "Резюме", - cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Редактировать резюме", cvCreateSectionTitle: "Создать резюме", cvDescription: "Управляйте и делитесь профессиональными навыками и информацией.", cvNameLabel: "Полное имя", cvDescriptionLabel: "Описание", cvPhotoLabel: "Фото", + cvPdfLabel: "Резюме (PDF)", + cvVisitButton: "Открыть резюме", cvPersonalExperiencesLabel: "Личный опыт", cvPersonalSkillsLabel: "Личные навыки (через запятую)", cvOasisExperiencesLabel: "Опыт вклада в Oasis", @@ -1555,29 +1605,130 @@ module.exports = { cvLocationLabel: "Местоположение", cvStatusLabel: "Статус", cvPreferencesLabel: "Предпочтения", - cvOasisContributorLabel: "Участник Oasis", cvPersonal: "Личное", cvOasis: "Участник Oasis (необязательно)", - cvOasisContributorView: "Вклад в Oasis", + cvOasisContributorView: "Вклад", cvEducational: "Образование (необязательно)", cvEducationalView: "Образование", cvProfessional: "Профессиональное (необязательно)", cvProfessionalView: "Профессиональное", cvAvailability: "Доступность (необязательно)", - cvAvailabilityView: "Доступность", cvUpdateButton: "Обновить", cvCreateButton: "Создать резюме", - cvContactLabel: "Контакт", cvCreatedAt: "Создано", cvUpdatedAt: "Обновлено", cvEditButton: "Обновить", cvDeleteButton: "Удалить", cvNoCV: "Резюме не найдено.", blogSubject: "Тема", - blogMessage: "Текст ограничен 8096 символами.", - blogImage: "Загрузить медиа (макс: 50MB)", - blogPublish: "Предварительный просмотр", - noPopularMessages: "Популярных сообщений пока нет", + blogMessage: "Сообщение", + blogImage: "Загрузить медиа (макс: 75MB)", + blogMedia: "Вложения (до 8 изображений и одно видео)", + blogPublish: "Опубликовать", + pollsTitle: "Опросы", + dataTitle: "Совпадения", + dataDescription: "Исследуйте и визуализируйте совпадения в вашей сети.", + dataFilterAll: "ВСЁ", + dataFilterMine: "МОИ", + dataFilterRecent: "НЕДАВНИЕ", + dataFilterTop: "ТОП", + dataKindInhabitants: "ЖИТЕЛИ", + dataKindJobs: "ВАКАНСИИ", + dataKindProjects: "ПРОЕКТЫ", + dataKindEvents: "СОБЫТИЯ", + dataKindTribes: "ПЛЕМЕНА", + dataKindMarket: "РЫНОК", + dataKindHousing: "ЖИЛЬЁ", + dataKindIndustry: "ПРОИЗВОДСТВО", + dataKindTasks: "ЗАДАЧИ", + dataKindReports: "ОТЧЁТЫ", + dataKindVotes: "ГОЛОСОВАНИЯ", + dataSearchPlaceholder: "Поиск в Совпадения...", + dataCohesionTitle: "Коэффициент сплочённости (CC)", + dataCcShort: "CC", + dataCohesionHint: "Средняя близость между всем, что опубликовано в сети.", + dataAffinity: "Сходство", + dataCommonTerms: "Ключевое слово", + dataReasonFollowing: "Вы поддерживаете этого жителя", + dataReasonTribe: "Состоит с вами в одном племени", + dataReasonPinned: "Совпадает с тем, что вы закрепили", + dataReasonCv: "Совпадает с навыками из вашего резюме", + dataReasonContent: "Связано с тем, что вы публикуете", + dataReasonRelated: "Связано с вашими интересами", + dataReasonAlike: "Опубликовано близким вам по духу жителем", + dataReasonSupportsYou: "Поддерживает вас", + dataReasonMutual: "Взаимная поддержка", + dataReasonRated: "Высоко оценено сетью", + dataReasonNear: "Рядом с вами", + dataStatPeople: "Резюме", + dataStatConnected: "Связаны", + dataStatSkills: "Навыки", + dataBestMatch: "Лучшее совпадение", + dataStatIsolated: "Изолированы", + dataStatEntities: "Сравнённые сущности", + dataStatTerms: "Различных терминов", + dataStatPairs: "Связанные пары", + dataMatchesTitle: "Совпадения", + dataMatchesHint: "Персонализированные предложения алгоритма.", + dataTermsTitle: "Ключевые слова", + dataTermsHint: "Термины, встречающиеся в наибольшем числе материалов.", + dataConnections: "Связанные совпадения", + dataNoMatches: "Нет перекрёстных данных.", + dataNoProfile: "Публикуйте контент или заполните CV, чтобы алгоритм узнал, что вам интересно.", + pollsDescription: "Модуль, чтобы спросить сеть и подсчитать ответы.", + pollFilterAll: "ВСЕ", + pollFilterMine: "МОИ", + pollFilterRecent: "НЕДАВНИЕ", + pollFilterTop: "ТОП", + pollFilterVoted: "ПРОГОЛОСОВАЛ", + pollFilterOpen: "ОТКРЫТЫЕ", + pollFilterClosed: "ЗАКРЫТЫЕ", + pollCreateButton: "Создать опрос", + pollCreateTitle: "Создать опрос", + pollEditTitle: "Изменить опрос", + pollQuestion: "Вопрос", + pollQuestionPlaceholder: "Что вы хотите спросить?", + pollOptions: "Варианты (по одному в строке)", + pollOutcome: "Результат", + pollNoVotesYet: "Голосов пока нет", + pollTie: "Ничья", + pollOptionsPlaceholder: "Площадь\nПарк\nБар", + pollAnonymous: "Анонимный", + pollAnonymousLabel: "Анонимный опрос", + pollMultiple: "Множественный выбор", + pollMultipleLabel: "Разрешить несколько ответов", + pollDeadline: "Срок", + pollTags: "Метки", + pollPublishButton: "Опубликовать опрос", + pollUpdateButton: "Обновить", + pollCloseButton: "Закрыть", + pollDeleteButton: "Удалить", + pollVoteButton: "Голосовать", + pollChangeVote: "Изменить голос", + pollVoters: "Проголосовали", + pollComments: "Комментарии", + pollStatusLabel: "Статус", + pollStatusOpen: "ОТКРЫТ", + pollStatusClosed: "ЗАКРЫТ", + pollSearchPlaceholder: "Поиск в Опросы...", + pollsNoItems: "Опросы не найдены.", + viewPoll: "Смотреть опрос", + pollChatCreate: "Создать опрос", + pollInChat: "Опрос", + blogTitle: "Блоги", + blogDescription: "Модуль для поиска и управления блогами.", + blogFilterAll: "ВСЕ", + blogFilterMine: "МОИ", + blogFilterRecent: "НЕДАВНИЕ", + blogFilterFavorites: "ИЗБРАННОЕ", + blogFilterTop: "ТОП", + blogCreateButton: "Создать блог", + blogSearchPlaceholder: "Поиск в Блоги...", + blogComments: "Комментарии", + blogAllowComments: "Разрешить комментарии", + blogCommentsClosed: "Автор закрыл комментарии к этому блогу.", + blogNoItems: "Блоги не найдены.", + viewBlog: "Смотреть блог", forumTitle: "Форумы", forumCategoryLabel: "Категория", forumTitleLabel: "Название", @@ -1585,43 +1736,21 @@ module.exports = { forumCreateButton: "Создать форум", forumCreateSectionTitle: "Создать форум", forumDescription: "Общайтесь открыто с другими жителями вашей сети.", + forumSearchPlaceholder: "Поиск в Форумы...", forumFilterAll: "ВСЕ", forumFilterMine: "МОИ", forumFilterRecent: "НЕДАВНИЕ", forumFilterTop: "ТОП", - forumMineSectionTitle: "Ваши форумы", - forumRecentSectionTitle: "Последние форумы", - forumAllSectionTitle: "Форумы", - forumDeleteButton: "Удалить", forumParticipants: "участники", forumMessages: "сообщения", - forumLastMessage: "Последнее сообщение", forumMessageLabel: "Сообщение", forumMessagePlaceholder: "Напишите сообщение...", forumSendButton: "Отправить", - forumVisitForum: "Посетить форум", noForums: "Форумы не найдены.", - forumVisitButton: "Посетить форум", - forumCatGENERAL: "Общее", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "Политика", - forumCatTECH: "Технологии", - forumCatSCIENCE: "Наука", - forumCatMUSIC: "Музыка", - forumCatART: "Искусство", - forumCatGAMING: "Игры", - forumCatBOOKS: "Книги", - forumCatFILMS: "Фильмы", - forumCatPHILOSOPHY: "Философия", - forumCatSOCIETY: "Общество", - forumCatPRIVACY: "Конфиденциальность", - forumCatCYBERWARFARE: "Кибервойна", - forumCatSURVIVALISM: "Выживание", + forumVisitButton: "Открыть форум", + forumTopicLabel: "Тема", imageTitle: "Изображения", imageDescription: "Исследуйте и управляйте изображениями в вашей сети.", - imagePluginTitle: "Название", - imagePluginDescription: "Описание", imageMineSectionTitle: "Ваши изображения", imageCreateSectionTitle: "Загрузить изображение", imageUpdateSectionTitle: "Обновить изображение", @@ -1630,17 +1759,14 @@ module.exports = { imageTopSectionTitle: "Топ изображений", imageFavoritesSectionTitle: "Избранное", imageGallerySectionTitle: "Галерея", - imageMemeSectionTitle: "Мемы", imageFilterAll: "ВСЕ", imageFilterMine: "МОИ", imageFilterRecent: "НЕДАВНИЕ", imageFilterTop: "ТОП", imageFilterFavorites: "ИЗБРАННОЕ", imageFilterGallery: "ГАЛЕРЕЯ", - imageFilterMeme: "МЕМЫ", imageCreateButton: "Загрузить изображение", imageUpdateButton: "Обновить", - imageDeleteButton: "Удалить", imageAddFavoriteButton: "Добавить в избранное", imageRemoveFavoriteButton: "Удалить из избранного", imageFileLabel: "Выбрать файл изображения (.jpeg, .jpg, .png, .gif)", @@ -1650,26 +1776,25 @@ module.exports = { imageTitlePlaceholder: "Необязательно", imageDescriptionLabel: "Описание", imageDescriptionPlaceholder: "Необязательно", - imageMemeLabel: "Отметить как МЕМ", imageNoFile: "Файл изображения не предоставлен", noImages: "Изображений нет.", - imageSearchPlaceholder: "Поиск по названию, тегам, описанию, автору...", + imageSearchPlaceholder: "Поиск в Изображения...", imageSortRecent: "Самые новые", imageSortOldest: "Самые старые", imageSortTop: "С наибольшим рейтингом", imageSearchButton: "Поиск", - imageMessageAuthorButton: "Сообщение", imageUpdatedAt: "Обновлено", imageNoMatch: "Изображения не найдены.", + feedSearchPlaceholder: "Поиск в Лента...", feedTitle: "Лента", createFeedTitle: "Создать пост", createFeedButton: "Отправить пост!", feedPlaceholder: "Что происходит? (макс 280 символов)", ALLButton: "Посты", + RECENTButton: "НЕДАВНИЕ", MINEButton: "Ваши посты", TODAYButton: "СЕГОДНЯ", TOPButton: "Топ постов", - CREATEButton: "Создать пост", totalOpinions: "Всего мнений", moreVoted: "Больше голосов", alreadyVoted: "Вы уже высказали мнение.", @@ -1681,7 +1806,7 @@ module.exports = { alreadyRefeeded: "Вы уже переопубликовали это.", activityTitle: "Активность", yourActivity: "Ваша активность", - globalActivity: "Глобальная активность", + globalActivity: "Активность", activityList: "Активность", activityDesc: "Смотрите последнюю активность в вашей сети.", allButton: "ВСЕ", @@ -1694,20 +1819,13 @@ module.exports = { concept: "Концепция", description: "Описание", meme: "Мем", - activityContact: "Контакт", - activityBy: "Имя", - activityPixelia: "Добавлен новый пиксель", - viewImage: "Просмотр изображения", - playAudio: "Воспроизвести аудио", - playVideo: "Воспроизвести видео", typeRecent: "НЕДАВНИЕ", - errorActivity: "Ошибка получения активности", - typePost: "БЛОГИ", - recentButton: "НЕДАВНИЕ", + typeTop: "ТОП", + typePost: "БЛОГ", + activityUpdateLabel: "ОБНОВЛЕНИЕ", typeTribe: "ПЛЕМЕНА", typeLarp: "L.A.R.P.", typeInhabitants: "ЖИТЕЛИ", - activityProfileUpdated: "обновил профиль", typeAbout: "ЖИТЕЛИ", typeCurriculum: "РЕЗЮМЕ", typeImage: "ИЗОБРАЖЕНИЯ", @@ -1751,8 +1869,6 @@ module.exports = { typeCourtsSettlementAccepted: "Суды · Мировое соглашение принято", typeCourtsNomination: "Суды · Назначение", typeCourtsNominationVote: "Суды · Голосование за назначение", - activitySupport: "Заключён новый альянс", - activityJoin: "Присоединился к новому PUB", question: "Вопрос", deadline: "Срок", status: "Статус", @@ -1766,12 +1882,8 @@ module.exports = { date: "Дата", category: "Категория", attendees: "Участники", - activitySpread: "->", - visitLink: "Перейти по ссылке", - viewDocument: "Просмотр документа", location: "Местоположение", contentWarning: "Тема", - personName: "Имя жителя", bankWalletConnected: "Кошелёк ECOin", bankUbiReceived: "Получен UBI", bankTx: "Транзакция", @@ -1781,7 +1893,6 @@ module.exports = { link: "Ссылка", activityProjectFollow: "%OASIS% теперь %ACTION% этот проект %PROJECT%", activityProjectUnfollow: "%OASIS% теперь %ACTION% этот проект %PROJECT%", - activityProjectPledged: "%OASIS% внёс %ACTION% %AMOUNT% в проект %PROJECT%", following: "СЛЕЖУ", unfollowing: "ОТПИСАН", pledged: "ПОДДЕРЖАЛ", @@ -1828,34 +1939,28 @@ module.exports = { courtsVotesSlashTotal: "ЗА/ВСЕГО", courtsOpenVote: "Открытое голосование", courtsAnswerTitle: "Ответ", - courtsStanceADMIT: "Признаю", - courtsStanceDENY: "Отрицаю", - courtsStancePARTIAL: "Частично", - courtsStanceCOUNTERCLAIM: "Встречный иск", - courtsStanceNEUTRAL: "Нейтрально", courtsVerdictResult: "Результат", courtsVerdictOrders: "Предписания", courtsSettlementText: "Соглашение", courtsSettlementAccepted: "Принято", - courtsSettlementPending: "В ожидании", courtsJudge: "Судья", courtsThSupports: "Поддержки", courtsFilterOpenCase: "Открытое дело", - courtsEvidenceFileLabel: "Файл доказательства (изображение, аудио, видео или PDF)", - courtsCaseMediators: "Медиаторы", courtsCaseMediatorsPh: "Oasis ID медиаторов через запятую", courtsMediatorsLabel: "Медиаторы", courtsThCase: "Дело", courtsThCreatedAt: "Дата начала", - courtsThActions: "Действия", courtsPublicPrefLabel: "Видимость после решения", courtsPublicPrefYes: "Согласен, что это дело может быть полностью публичным", courtsPublicPrefNo: "Предпочитаю сохранить детали в тайне", courtsPublicPrefSubmit: "Сохранить настройку видимости", courtsNoCases: "Дел нет.", reportsTitle: "Отчёты", - reportsDescription: "Управляйте и отслеживайте отчёты, связанные с проблемами, ошибками, злоупотреблениями и предупреждениями в вашей сети.", + reportsDescription: "Управляйте отчётами вашей сети и следите за ними.", + reportsSearchPlaceholder: "Поиск в Отчёты...", reportsFilterAll: "ВСЕ", + reportsFilterRecent: "НОВЫЕ", + reportsFilterTop: "ТОП", reportsFilterMine: "МОИ", reportsFilterFeatures: "ФУНКЦИИ", reportsFilterBugs: "ОШИБКИ", @@ -1868,19 +1973,13 @@ module.exports = { reportsFilterInvalid: "НЕДЕЙСТВИТЕЛЬНЫЕ", reportsCreateButton: "Создать отчёт", reportsTitleLabel: "Название", - reportsDescriptionLabel: "Описание", - reportsDescriptionPlaceholder: "Пожалуйста, предоставьте подробное описание.", reportsCategory: "Категория", - reportsCategoryLabel: "Категория", reportsCategoryFeatures: "Функции", reportsCategoryBugs: "Ошибки", reportsCategoryAbuse: "Злоупотребления", - reportsCategoryContent: "Проблемы с контентом", + reportsCategoryContent: "Контент", reportsUpdateButton: "Обновить", reportsDeleteButton: "Удалить", - reportsDateLabel: "Дата", - reportsUploadFile: "Загрузить медиа (макс: 50MB)", - reportsCreatedBy: "Автор", reportsMineSectionTitle: "Ваши отчёты", reportsFeaturesSectionTitle: "Запросы функций", reportsBugsSectionTitle: "Ошибки", @@ -1888,10 +1987,6 @@ module.exports = { reportsContentSectionTitle: "Проблемы с контентом", reportsAllSectionTitle: "Отчёты", reportsNoItems: "Отчётов нет.", - reportsValidationTitle: "Введите допустимое название.", - reportsValidationDescription: "Описание не может быть пустым.", - reportsValidationCategory: "Выберите категорию.", - reportsCreatedAt: "Создано", reportsSeverity: "Серьёзность", reportsSeverityLow: "Низкая", reportsSeverityMedium: "Средняя", @@ -1902,18 +1997,14 @@ module.exports = { reportsStatusUnderReview: "На рассмотрении", reportsStatusResolved: "Решён", reportsStatusInvalid: "Недействительный", - reportsUpdateStatusButton: "Обновить статус", - reportsAnonymityOption: "Отправить анонимно", - reportsAnonymousAuthor: "Анонимный", reportsConfirmButton: "ПОДТВЕРДИТЬ ОТЧЁТ!", reportsConfirmations: "Подтверждения", reportsConfirmedSectionTitle: "Подтверждённые отчёты", - reportsCreateTaskButton: "СОЗДАТЬ ЗАДАЧУ", + reportsCreateTaskButton: "Создать задачу", reportsOpenSectionTitle: "Открытые отчёты", reportsUnderReviewSectionTitle: "Отчёты на рассмотрении", reportsResolvedSectionTitle: "Решённые отчёты", reportsInvalidSectionTitle: "Недействительные отчёты", - reportsTemplateSectionTitle: "Шаблон отчёта", reportsBugTemplateTitle: "Детали воспроизведения (Ошибки)", reportsFeatureTemplateTitle: "Детали (Функции)", reportsAbuseTemplateTitle: "Детали (Злоупотребления)", @@ -1952,6 +2043,20 @@ module.exports = { reportsRequestedActionLabel: "Запрашиваемое действие", reportsRequestedActionPlaceholder: "Удалить, скрыть, пометить, предупредить и т.д.", tribesTitle: "Племена", + tribeFilterAll: "ВСЕ", + tribeFilterRecent: "НЕДАВНИЕ", + tribeFilterMine: "МОИ", + tribeFilterMembership: "ЧЛЕНСТВО", + tribeFilterSubtribes: "ПОДПЛЕМЕНА", + tribeFilterTop: "ТОП", + tribeFilterGallery: "ГАЛЕРЕЯ", + tribeFeedFilterTOP: "ТОП", + tribeFeedFilterMINE: "МОИ", + tribeFeedFilterALL: "ВСЕ", + tribeFeedFilterRECENT: "НЕДАВНИЕ", + courtsStanceDENY: "Отрицать", + courtsStanceADMIT: "Признать", + courtsStancePARTIAL: "Признать частично", tribeAllSectionTitle: "Племена", tribeMineSectionTitle: "Ваши племена", tribeCreateSectionTitle: "Создать племя", @@ -1959,24 +2064,15 @@ module.exports = { tribeGallerySectionTitle: "Галерея племён", tribeRecentSectionTitle: "Недавние племена", tribeTopSectionTitle: "Популярные племена", - tribeviewTribeButton: "Посетить племя", - tribeviewSubTribeButton: "Посетить под-племя", tribeRootLabel: "КОРЕНЬ", tribeDescription: "Исследуйте или создавайте племена в вашей сети.", - tribeFilterAll: "ВСЕ", - tribeFilterMine: "МОИ", - tribeFilterMembership: "ЧЛЕНСТВО", - tribeFilterRecent: "НЕДАВНИЕ", - tribeFilterTop: "ТОП", - tribeFilterSubtribes: "ПОДПЛЕМЕНА", - tribeFilterGallery: "ГАЛЕРЕЯ", tribeMainTribeLabel: "ГЛАВНОЕ ПЛЕМЯ", tribeCreateButton: "Создать племя", tribeUpdateButton: "Обновить", tribeDeleteButton: "Удалить", - tribeImageLabel: "Загрузить медиа (макс: 50MB)", + tribeImageLabel: "Загрузить медиа (макс: 75MB)", tribeTitleLabel: "Название", - searchTribesPlaceholder: "ФИЛЬТР племён ПО ИМЕНИ …", + searchTribesPlaceholder: "Поиск в Племена...", tribeTitlePlaceholder: "Название племени", tribeDescriptionLabel: "Описание", tribeDescriptionPlaceholder: "Опишите это племя", @@ -1988,13 +2084,17 @@ module.exports = { tribeModeLabel: "РЕЖИМ", tribeIsAnonymousLabel: "СТАТУС", tribeMembersCount: "Участники", - tribeInviteCodePlaceholder: "Введите код приглашения", - tribeJoinByCodeButton: "Войти с кодом", - tribeJoinButton: "ВСТУПИТЬ", tribeEnterInvite: "ВВЕСТИ КОД", + subscriptionTitle: "Подписки", + subscriptionOn: "Подписан", + subscriptionOff: "Не подписан", + subscriptionSubscribe: "Подписаться", + subscriptionUnsubscribe: "Отписаться", + subscriptionSubscribers: "Подписчики", + pmListLabel: "Отправить", + pmListNone: "— Без списка —", + pmListNoSubscribers: "Нет подписчиков", tribeLeaveButton: "ПОКИНУТЬ", - tribeYes: "ДА", - tribeNo: "НЕТ", tribePublic: "ПУБЛИЧНОЕ", tribePrivate: "ЧАСТНОЕ", tribeGenerateInvite: "РАЗОВОЕ ПРИГЛАШЕНИЕ", @@ -2003,13 +2103,8 @@ module.exports = { tribeRemoveInvitation: "УДАЛИТЬ ПРИГЛАШЕНИЕ", tribeCreatedAt: "Создано", tribeAuthor: "Автор", - tribeAuthorLabel: "АВТОР", tribeStrict: "Строгое", tribeOpen: "Открытое", - tribeFeedFilterRECENT: "НЕДАВНИЕ", - tribeFeedFilterMINE: "МОИ", - tribeFeedFilterALL: "ВСЕ", - tribeFeedFilterTOP: "ТОП", tribeFeedRefeeds: "Переопубликации", tribeFeedRefeed: "Переопубликовать", tribeFeedMessagePlaceholder: "Написать пост…", @@ -2019,33 +2114,28 @@ module.exports = { tribeNotFound: "Племя не найдено!", createTribeTitle: "Создать племя", updateTribeTitle: "Обновить племя", - tribeSectionOverview: "Обзор", tribeSectionInhabitants: "Жители", tribeSectionVotations: "ГОЛОСОВАНИЯ", tribeSectionEvents: "СОБЫТИЯ", - tribeSectionReports: "Отчёты", tribeSectionTasks: "ЗАДАЧИ", tribeSectionFeed: "ЛЕНТА", tribeSectionForum: "ФОРУМ", - tribeSectionMarket: "Рынок", - tribeSectionJobs: "Вакансии", - tribeSectionProjects: "Проекты", tribeSectionMedia: "Медиа", tribeSectionImages: "ИЗОБРАЖЕНИЯ", tribeSectionAudios: "АУДИО", tribeSectionVideos: "ВИДЕО", tribeSectionDocuments: "ДОКУМЕНТЫ", tribeSectionBookmarks: "ЗАКЛАДКИ", - tribeSectionMaps: "MAPS", - tribeSectionPads: "PADS", - tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionMaps: "КАРТЫ", + tribeSectionPads: "ПАДЫ", + tribeSectionChats: "ЧАТЫ", + tribeSectionCalendars: "КАЛЕНДАРИ", + tribePadCreate: "Создать пад", + tribeChatCreate: "Создать чат", + tribeCalendarCreate: "Создать календарь", + tribePadsEmpty: "Пока нет падов.", + tribeChatsEmpty: "Пока нет чатов.", + tribeCalendarsEmpty: "Пока нет календарей.", tribeInhabitantsEmpty: "В этом племени пока нет жителей.", tribeEventCreate: "Создать событие", tribeEventsEmpty: "Событий пока нет.", @@ -2067,11 +2157,6 @@ module.exports = { tribeTaskStatusClosed: "ЗАКРЫТЬ", tribeTaskAssign: "НАЗНАЧИТЬ", tribeTaskUnassign: "СНЯТЬ НАЗНАЧЕНИЕ", - tribeReportCreate: "Создать отчёт", - tribeReportsEmpty: "Отчётов пока нет.", - tribeReportTitle: "Название", - tribeReportDescription: "Описание", - tribeReportCategory: "Категория", tribeVotationCreate: "Создать голосование", tribeVotationsEmpty: "Голосований пока нет.", tribeVotationTitle: "Название", @@ -2089,27 +2174,6 @@ module.exports = { tribeForumCategory: "Категория", tribeForumReply: "Ответить", tribeForumReplies: "Ответы", - tribeMarketCreate: "Создать объявление", - tribeMarketEmpty: "Объявлений пока нет.", - tribeMarketTitle: "Название", - tribeMarketDescription: "Описание", - tribeMarketPrice: "Цена", - tribeMarketImage: "Изображение", - tribeMarketCategory: "Категория", - tribeJobCreate: "Создать вакансию", - tribeJobsEmpty: "Вакансий пока нет.", - tribeJobTitle: "Название", - tribeJobDescription: "Описание", - tribeJobLocation: "Местоположение", - tribeJobSalary: "Зарплата", - tribeJobDeadline: "Срок", - tribeProjectCreate: "Создать проект", - tribeProjectsEmpty: "Проектов пока нет.", - tribeProjectTitle: "Название", - tribeProjectDescription: "Описание", - tribeProjectGoal: "Цель", - tribeProjectFunded: "Финансировано", - tribeProjectDeadline: "Срок", tribeMediaUpload: "Загрузить медиа", readDocument: "Читать документ", tribeCreateImage: "Создать изображение", @@ -2120,21 +2184,20 @@ module.exports = { tribeMediaEmpty: "Медиа пока нет.", tribeMediaTitle: "Название", tribeMediaDescription: "Описание", - tribeMediaType: "Тип", tribeMediaTypeImage: "Изображение", tribeMediaTypeVideo: "Видео", tribeMediaTypeAudio: "Аудио", tribeMediaTypeDocument: "Документ", tribeMediaTypeBookmark: "Закладка", tribeContentDelete: "УДАЛИТЬ", + tribeReachTribe: "ПЛЕМЯ", + tribeReachNotice: "OASIS показывает это всем жителям, а CLEARNET — ещё и в открытом вебе. Открытие оставляет читаемую копию вне племени: возврат к ПЛЕМЯ скрывает её, но уже распространённые копии удалить нельзя.", + tribeReachPrivate: "Содержимое частного племени может оставаться только внутри племени.", + tribeReachOpenedBy: "Открыл(а)", + tribeReachChange: "Где публикуется", tribeInviteCodeText: "Код приглашения: ", oasisVersionLabel: "Версия Oasis", - tribeInviteCodeHint: "Поделитесь этим кодом с тем, кого хотите пригласить. Он может присоединиться через /invites → Tribes.", - tribeGroupTribe: "Племя", - tribeGroupOffice: "Офис", - tribeGroupNetwork: "Сеть", - tribeGroupEconomy: "Экономика", - tribeGroupMedia: "Медиа", + tribeInviteCodeHint: "Поделитесь этим кодом с тем, кого хотите пригласить. Он может присоединиться через /invites.", tribeStatusOpen: "ОТКРЫТОЕ", tribeStatusClosed: "ЗАКРЫТОЕ", tribeStatusInProgress: "В ПРОЦЕССЕ", @@ -2144,33 +2207,17 @@ module.exports = { tribePriorityCritical: "КРИТИЧЕСКИЙ", tribeTaskFilterAll: "ВСЕ", tribeMediaFilterAll: "ВСЕ", - tribeReportCatBug: "ОШИБКА", - tribeReportCatAbuse: "ЗЛОУПОТРЕБЛЕНИЕ", - tribeReportCatContent: "КОНТЕНТ", - tribeReportCatOther: "ДРУГОЕ", tribeForumCatGeneral: "ОБЩЕЕ", tribeForumCatProposal: "ПРЕДЛОЖЕНИЕ", tribeForumCatQuestion: "ВОПРОС", tribeForumCatAnnouncement: "ОБЪЯВЛЕНИЕ", - tribeMarketCatGoods: "ТОВАРЫ", - tribeMarketCatServices: "УСЛУГИ", - tribeMarketCatFood: "ЕДА", - tribeMarketCatOther: "ДРУГОЕ", tribeStatusLabel: "Статус", tribeSubTribes: "ПОДПЛЕМЕНА", tribeSubTribesCreate: "Создать подплемя", tribeSubTribesStrictDenied: "Строгий режим племени не позволяет создавать новые подплемена. Пожалуйста, свяжитесь с администратором.", - tribeSubTribesEmpty: "Подплемён пока не создано.", - tribeActivityJoined: "ВСТУПИЛ", - tribeActivityLeft: "ПОКИНУЛ", - tribeActivityFeed: "ЛЕНТА", tribeActivityRefeed: "ПЕРЕОПУБЛИКОВАЛ", - tribeGroupAnalytics: "Аналитика", - tribeGroupCreative: "Творчество", tribeSectionActivity: "АКТИВНОСТЬ", tribeSectionTrending: "ПОПУЛЯРНОЕ", - tribeSectionOpinions: "МНЕНИЯ", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "ТЕГИ", tribeSectionSearch: "ПОИСК", tribeActivityEmpty: "Активности пока нет.", @@ -2182,25 +2229,13 @@ module.exports = { tribeTrendingPeriodWeek: "Эта неделя", tribeTrendingPeriodAll: "За всё время", tribeTrendingEngagement: "вовлечённость", - tribeOpinionsEmpty: "Мнений пока нет.", - tribeOpinionsCast: "Голосовать", - tribeOpinionsRankings: "Рейтинги", - tribeOpinionsAlreadyVoted: "Уже проголосовали", tribeTopCategory: "Больше голосов", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "Совместный пиксель-арт.", - tribePixeliaPaint: "Рисовать", - tribePixeliaContributors: "участников", - tribePixeliaTotalPixels: "пикселей нарисовано", tribeTagsEmpty: "Тегов не найдено.", - tribeTagsCloud: "Облако тегов", - tribeTagsContentWith: "Контент с тегом", - tribeSearchPlaceholder: "Поиск контента племени...", tribeSearchEmpty: "Результаты не найдены.", - tribeSearchResults: "Результаты", tribeSearchMinChars: "Введите не менее 2 символов для поиска.", agendaTitle: "Повестка дня", agendaDescription: "Здесь вы можете найти все назначенные вам элементы.", + agendaSearchPlaceholder: "Поиск в Повестка дня...", agendaFilterAll: "ВСЕ", agendaFilterToday: "СЕГОДНЯ", agendaFilterUpcoming: "БЛИЖАЙШИЕ", @@ -2214,23 +2249,15 @@ module.exports = { agendaFilterReports: "ОТЧЁТЫ", agendaFilterTransfers: "ПЕРЕВОДЫ", agendaFilterJobs: "ВАКАНСИИ", + agendaFilterIndustry: "ПРОМЫШЛЕННОСТЬ", + agendaFilterHousing: "ЖИЛЬЁ", agendaFilterProjects: "ПРОЕКТЫ", agendaFilterCalendars: "КАЛЕНДАРИ", agendaNoItems: "Назначений не найдено.", - agendaAuthor: "Автор", agendaDiscardButton: "Отклонить", agendaRestoreButton: "Восстановить", - agendaCreatedAt: "Создано", - agendaTitleLabel: "Название", + agendaRemoveButton: "Убрать", agendaMembersCount: "Участники", - agendaDescriptionLabel: "Описание", - agendaStatus: "Статус", - agendaVisibility: "Видимость", - agendaEventDate: "Дата события", - agendaEventLocation: "Местоположение", - agendaEventPrice: "Цена", - agendaEventUrl: "URL", - agendaTaskStart: "Время начала", agendaLocationLabel: "Местоположение", agendaYes: "ДА", agendaNo: "НЕТ", @@ -2240,11 +2267,6 @@ module.exports = { agendareportCategory: "Категория", agendareportSeverity: "Серьёзность", agendareportStatus: "Статус", - agendareportDescription: "Описание", - agendaTaskEnd: "Время окончания", - agendaTaskPriority: "Приоритет", - agendaTransferFrom: "От", - agendaTransferTo: "Кому", agendaTransferConcept: "Концепция", agendaTransferAmount: "Сумма", agendaTransferDeadline: "Срок", @@ -2252,45 +2274,6 @@ module.exports = { shareYourOpinions: "Открывайте и голосуйте за мнения в вашей сети.", voteNow: "Голосовать", noOpinionsFound: "Мнений не найдено.", - interestingButton: "ИНТЕРЕСНОЕ", - necessaryButton: "НЕОБХОДИМОЕ", - funnyButton: "СМЕШНОЕ", - disgustingButton: "ОТВРАТИТЕЛЬНОЕ", - sensibleButton: "РАЗУМНОЕ", - propagandaButton: "ПРОПАГАНДА", - adultOnlyButton: "ТОЛЬКО ДЛЯ ВЗРОСЛЫХ", - boringButton: "СКУЧНОЕ", - confusingButton: "ЗАПУТАННОЕ", - inspiringButton: "ВДОХНОВЛЯЮЩЕЕ", - spamButton: "СПАМ", - usefulButton: "ПОЛЕЗНОЕ", - informativeButton: "ИНФОРМАТИВНОЕ", - wellResearchedButton: "ХОРОШО ИССЛЕДОВАННОЕ", - accurateButton: "ТОЧНОЕ", - needsSourcesButton: "НУЖНЫ ИСТОЧНИКИ", - wrongButton: "НЕПРАВИЛЬНОЕ", - lowQualityButton: "НИЗКОЕ КАЧЕСТВО", - creativeButton: "ТВОРЧЕСКОЕ", - insightfulButton: "ПРОНИЦАТЕЛЬНОЕ", - actionableButton: "ПРИМЕНИМОЕ", - loveButton: "ЛЮБЛЮ", - clearButton: "ЯСНОЕ", - upliftingButton: "ПОДНИМАЮЩЕЕ НАСТРОЙ", - unnecessaryButton: "НЕНУЖНОЕ", - rejectedButton: "ОТКЛОНЁННОЕ", - misleadingButton: "ВВОДЯЩЕЕ В ЗАБЛУЖДЕНИЕ", - offTopicButton: "НЕ ПО ТЕМЕ", - duplicateButton: "ДУБЛИРОВАНИЕ", - clickbaitButton: "КЛИКБЕЙТ", - trollButton: "ТРОЛЛЬ", - nsfwButton: "NSFW", - violentButton: "НАСИЛИЕ", - toxicButton: "ТОКСИЧНОЕ", - harassmentButton: "ПРЕСЛЕДОВАНИЕ", - hateButton: "НЕНАВИСТЬ", - scamButton: "МОШЕННИЧЕСТВО", - triggeringButton: "ПРОВОКАЦИОННОЕ", - opinionsCreatedAt: "Создано", opinionsTotalCount: "Всего мнений", voteInteresting: "Интересное", voteNecessary: "Необходимое", @@ -2327,48 +2310,67 @@ module.exports = { voteCreative: "Творческое", voteSpam: "Спам", voteAdultOnly: "Только для взрослых", - publishBlog: "Опубликовать блог", privateMessage: "ЛС", pmSendTitle: "Личные сообщения", pmSend: "Отправить!", - pmDescription: "Используйте эту форму для отправки зашифрованного сообщения другим жителям.", + pmCancel: "Отмена", + pmReset: "Сбросить", + pmSharedKeyHint: "Передайте этот ключ получателю по другому каналу. Он нужен для расшифровки.", + pmDescription: "Используйте эту форму, чтобы отправить зашифрованное сообщение/файл другим жителям.", + pmComposeTitle: "Отправить сообщение", pmRecipients: "Получатели", pmRecipientsHint: "Введите Oasis ID через запятую", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "До 7 жителей на сообщение.", + pmTextPlaceholder: "Напишите ваше сообщение.", pmSubject: "Тема", pmSubjectHint: "Введите тему сообщения", pmText: "Сообщение", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Расшифровать", + fileShareTitle: "Поделиться файлом", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "Файл", + fileShareDownload: "Скачать", + fileShareMutualError: "Вы можете делиться файлами только с жителями при взаимной поддержке.", + fileShareNoFile: "Файл не выбран.", + fileShareTooLarge: "Файл превышает допустимый размер.", + fileShareFailed: "Не удалось подготовить файл.", + fileShareSendError: "Не удалось отправить файл.", + fileShareUnavailable: "Этот файл сейчас недоступен. Попробуйте позже.", pmCrypterBadKey: "Общий ключ неверный!", pmCrypterTooLong: "Сообщение слишком длинное для шифрования с помощью Crypter. Сократите его и попробуйте снова.", pmCrypterCipherLabel: "Повторно зашифрованное сообщение", pmCrypterCharsLabel: "символов", pmInvalidRecipients: "Недействительный Oasis ID (должен быть вида @…=.ed25519).", pmEncryptionLabel: "Шифрование", - pmFile: "Вложение", - pmInvalidMessage: "Недействительное сообщение", pmNoSubject: "(без темы)", pmSubjectLabel: "Тема:", - pmBodyLabel: "Тело", - pmBotJobs: "42-JobsBOT", - pmBotProjects: "42-ProjectsBOT", - pmBotMarket: "42-MarketBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "Правящий дом L.A.R.P сменился.", + politicalBotParliamentTitle: "В Парламенте начался новый цикл правления.", + politicalBotTribeTitle: "В одном из ваших племён начался новый цикл правления.", + politicalBotLarpIntro: "Теперь в этом цикле правит {house}: {cycle}", + politicalBotParliamentIntro: "Текущее правление — {gov}", + politicalBotParliamentIntroLeader: "Текущее правление — {gov}, осуществляет {leader}", + politicalBotTribeIntro: "Текущее правление в племени {tribe} — {gov}", + politicalBotTribeIntroLeader: "Текущее правление в племени {tribe} — {gov}, осуществляет {leader}", + politicalBotLeaderLabel: "Лидер", inboxJobSubscribedTitle: "Новая подписка на вашу вакансию", pmInhabitantWithId: "Житель с OASIS ID:", pmHasSubscribedToYourJobOffer: "подписался на вашу вакансию", - inboxProjectCreatedTitle: "Создан новый проект", - pmHasCreatedAProject: "создал проект", inboxMarketItemSoldTitle: "Товар продан", + inboxShopSoldTitle: "Товар продан", pmYourItem: "Ваш товар", pmHasBeenSoldTo: "был продан", pmFor: "за", inboxProjectPledgedTitle: "Новый взнос в ваш проект", pmHasPledged: "внёс", pmToYourProject: "в ваш проект", - blockchain: "BlockExplorer", - blockchainTitle: "BlockExplorer", + blockchain: "Обозреватель блоков", blockchainDescription: "Исследуйте и визуализируйте блоки в блокчейне.", blockchainNoBlocks: "Блоков в блокчейне не найдено.", blockchainStatsTitle: "Статистика", @@ -2380,42 +2382,36 @@ module.exports = { blockchainBlockCarbon: "Углеродный след", blockchainBlockType: "Тип", blockchainBlockTimestamp: "Временная метка", - blockchainBlockContent: "Блок", - blockchainBlockURL: "URL:", - blockchainContent: "Блок", - blockchainContentPreview: "Предварительный просмотр содержимого блока", blockchainLatestDatagram: "Последняя датаграмма", blockchainDatagram: "Датаграмма", - blockchainDetails: "Просмотр деталей блока", - blockchainViewBlockexplorer: "Открыть в blockexplorer", - blockchainBlockInfo: "Информация о блоке", - blockchainBlockDetails: "Детали выбранного блока", + blockchainDetails: "Посмотреть детали блока", + blockchainViewBlockexplorer: "Открыть в Blockexplorer", blockchainBack: "Назад в BlockExplorer", blockchainContentDeleted: "Этот контент был удалён", - visitContent: "Посетить контент", + visitContent: "Открыть контент", + favoriteAdd: "Закрепить в избранном", + pmContentTooltip: "Отправить личное сообщение", + phoneCallTooltip: "Позвонить", + favoriteRemove: "Убрать из избранного", + reportContent: "Пожаловаться на контент", banking: "Банкинг", bankingTitle: "Банкинг", - bankingDescription: "Исследуйте текущую стоимость ECOin и соответствующее распределение UBI, распределяемое за эпоху на основе участия и доверия.", + bankingDescription: "Коллективная экономика ECOin.", bankOverview: "Обзор", bankEpochs: "Эпохи", bankRules: "Правила", pending: "В ожидании", closed: "Закрыто", - bankBack: "Назад в Банкинг", - bankViewTx: "Просмотр транзакции", bankClaimNow: "Получить сейчас", - bankClaimUBI: "Получить UBI!", - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: "Нет ожидающих распределений UBI для этой эпохи.", + bankClaimUBI: "Получить UBI", + bankClaimAndPay: "Получить и оплатить", + bankClaimedPending: "Заявка в обработке...", + bankStatusClaimed: "Получено", + bankStatusExpired: "Истекло", + bankPubOnly: "Операция только для PUB", bankEpoch: "Эпоха", bankPool: "Пул (эта эпоха)", bankWeightsSum: "Сумма весов", - bankAllocations: "Распределения", bankNoAllocations: "Распределений не найдено.", bankNoEpochs: "Эпох не найдено.", bankEpochAllocations: "Распределения эпохи", @@ -2428,16 +2424,21 @@ module.exports = { bankEpochId: "ID эпохи", bankRuleHash: "Хэш снимка правил", bankViewEpoch: "Просмотр эпохи", + bankYourIndustryBalance: "Ваша доля в промышленности", + bankYourUbiMonth: "Ваш БОД (в этом месяце)", + bankYourFundsMonth: "Итого средства", + bankIndustryBalance: "Промышленное производство", bankUserBalance: "Ваш баланс", - ecoWalletNotConfigured: "Кошелёк ECOin не настроен", - editWallet: "Редактировать кошелёк", - addWallet: "Добавить кошелёк", bankAddresses: "Адреса", bankNoAddresses: "Адресов не найдено.", bankUser: "Oasis ID", bankAddress: "Адрес", bankAddAddressTitle: "Добавить адрес ECOIN", bankAddAddressUser: "Oasis ID", + bankAddAddressLabel: "Метка (необязательно)", + bankAddAddressLabelPlaceholder: "Кошелёк Боба, магазин Алисы…", + bankAddAddressUserOptional: "Oasis ID (необязательно)", + bankAddressUnnamed: "Адрес без имени", bankAddAddressAddress: "Адрес ECOIN", bankAddAddressSave: "Сохранить", bankAddressAdded: "Адрес добавлен", @@ -2454,19 +2455,11 @@ module.exports = { bankAddressSource: "Источник", bankAddressDeleteConfirm: "Удалить этот адрес?", bankLocal: "Локальный", - bankFromOasis: "Oasis", - bankMyAddress: "Ваш адрес", - bankRemoveMyAddress: "Удалить мой адрес", - bankNotRemovableOasis: "Адреса нельзя удалить локально", - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "Сеть", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "НЕТ СРЕДСТВ!", bankUbiAvailableOk: "ДОСТУПНО!", - bankUbiAvailability: "Доступность UBI", + bankUbiAvailability: "ББД", bankAlreadyClaimedThisMonth: "Уже получено в этом месяце", bankUbiThisMonth: "БОД (этот месяц)", bankUbiLastClaimed: "БОД (последнее получение)", @@ -2480,13 +2473,6 @@ module.exports = { shopsTitle: "Магазины", shopDescription: "Откройте и управляйте магазинами в сети.", shopTitle: "Магазин", - shopFilterAll: "ВСЕ", - shopFilterMine: "МОИ", - shopFilterRecent: "НЕДАВНИЕ", - shopFilterTop: "ТОП", - shopFilterProducts: "ТОВАРЫ", - shopFilterPrices: "ЦЕНЫ", - shopFilterFavorites: "ИЗБРАННЫЕ", shopUpload: "Создать магазин", shopAllSectionTitle: "Все магазины", shopMineSectionTitle: "Мои магазины", @@ -2503,19 +2489,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "Опубликовать в Clearnet", - shopClearnetUnpublish: "Убрать из Clearnet", - shopClearnetUrlLabel: "URL в Clearnet", - shopClearnetWarning: "Публикация в Clearnet делает этот магазин доступным для индексации поисковыми системами и внешними сканерами.", + transportChipLabel: "Шифруется при передаче", shopAddFavorite: "Добавить в избранное", shopRemoveFavorite: "Убрать из избранного", shopOpen: "ОТКРЫТ", shopClosed: "ЗАКРЫТ", - shopOpenShop: "Открыть магазин", - shopCloseShop: "Закрыть магазин", shopMakePrivate: "СДЕЛАТЬ ПРИВАТНОЙ (E2E)", shopMakePublic: "СДЕЛАТЬ ПУБЛИЧНОЙ", shopProducts: "Товары", + shopFilterAll: "ВСЕ", + shopFilterRecent: "НЕДАВНИЕ", + shopFilterMine: "МОИ", + shopFilterTop: "ТОП", + shopFilterProducts: "ТОВАРЫ", + shopFilterPrices: "ЦЕНЫ", + shopFilterFavorites: "ИЗБРАННОЕ", shopProductAdd: "Добавить товар", shopProductTitle: "Товар", shopProductUpdate: "Обновить товар", @@ -2540,83 +2528,60 @@ module.exports = { shopOrderPrice: "Цена", shopOrderBuyer: "Покупатель", shopBackToShop: "Назад в магазин", - shopShareUrl: "Ссылка для обмена", - shopVisitShop: "ПОСЕТИТЬ МАГАЗИН", marketVisitItem: "ОТКРЫТЬ ТОВАР", shopStatus: "СТАТУС", shopCreatedAt: "СОЗДАН", - shopSearchPlaceholder: "Поиск магазинов...", + shopSearchPlaceholder: "Поиск в Магазины...", shopUrl: "URL", shopLocation: "Местоположение", shopTags: "Теги", shopVisibility: "Видимость", - shopImage: "Изображение", shopShortDescription: "Краткое описание", shopShortDescriptionPlaceholder: "Краткое описание для карточек (макс. 160 символов)", shopTitlePlaceholder: "Название вашего магазина", shopDescriptionPlaceholder: "Подробное описание вашего магазина", - shopUrlPlaceholder: "https://url-vashego-magazina.ru", shopLocationPlaceholder: "Город, Страна", - shopTagsPlaceholder: "тег1, тег2, тег3", + shopTagsPlaceholder: "Введите теги через запятую", mapTitlePlaceholder: "Название карты", shopProductFeatured: "Рекомендуемый товар", - shopSendToMarket: "Send to Market", + shopSendToMarket: "Отправить на рынок", typeShop: "МАГАЗИНЫ", typeShopProduct: "ТОВАР МАГАЗИНА", bankExchange: "Обмен", bankExchangeCurrentValue: "Стоимость ECOin (1ч)", - bankTotalSupply: "Общий объём ECOin", - bankEcoinHours: "Эквивалент ECOin во времени", + bankTotalSupply: "Общее предложение ECOin", + bankPubsSupply: "Предложение ECOin в пуле PUB", + bankHoldingSupply: "Предложение ECOin вне ББД", + bankEcoinHours: "Эквивалент ECOin во времени (1 ECO)", bankHoursOfWork: "часов", bankUnitMs: "мс", bankUnitSeconds: "секунды", bankUnitMinutes: "минуты", bankUnitDays: "дни", bankExchangeNoData: "Данные недоступны", - bankExchangeIndex: "Стоимость ECOin (1ч)", - bankInflation: "Инфляция ECOin (anual)", - bankInflationMonthly: "Инфляция ECOin (mensual)", + bankInflation: "Инфляция ECOin (годовая)", + bankInflationMonthly: "Инфляция ECOin (месячная)", bankExchangeChartTitle: "Динамика стоимости ECOin со временем", bankExchangeChartValue: "Стоимость (ECO/ч)", bankExchangeChartSupply: "Предложение", bankExchangeChartInflation: "Инфляция %", bankExchangeChartEmpty: "Пока недостаточно данных — загляните позже", - bankCurrentSupply: "Текущий объём ECOin", + bankCurrentSupply: "Текущее предложение ECOin", bankingSyncStatus: "Статус ECOin", bankingSyncStatusSynced: "Синхронизирован", - bankingSyncStatusOutdated: "Устарел", + bankingSyncStatusOutdated: "Не синхронизирован", statsTitle: "Статистика", statistics: "Статистика", - statsInhabitant: "Статистика жителя", statsDescription: "Откройте статистику вашей сети.", TOMBSTONEButton: "НАДГРОБИЯ", - statsYou: "Вы", - statsUserId: "Oasis ID", statsCreatedAt: "Создано", statsYourContent: "Контент", statsYourOpinions: "Мнения", - statsYourTombstone: "Надгробия", - statsNetwork: "Сеть", - statsTotalInhabitants: "Жители", - statsDiscoveredTribes: "Племена (публичные)", - statsPrivateDiscoveredTribes: "Племена (частные)", statsNetworkContent: "Контент", - statsYourMarket: "Рынок", - statsYourJob: "Вакансии", - statsYourProject: "Проекты", - statsYourTransfer: "Переводы", - statsYourForum: "Форумы", statsNetworkOpinions: "Мнения", - statsDiscoveredMarket: "Рынок", - statsDiscoveredJob: "Вакансии", - statsDiscoveredProject: "Проекты", - statsBankingTitle: "Банкинг", - statsEcoWalletLabel: "Кошелёк ECOIN", + statsEcoWalletLabel: "Кошелёк ECOin", + walletAddressCopyHint: "Нажмите, чтобы выделить, затем скопируйте", statsEcoWalletNotConfigured: "Не настроен!", - statsTotalEcoAddresses: "Всего адресов", - statsDiscoveredTransfer: "Переводы", - statsDiscoveredForum: "Форумы", - statsNetworkTombstone: "Надгробия", statsBookmark: "Закладки", statsEvent: "События", statsTask: "Задачи", @@ -2638,17 +2603,15 @@ module.exports = { statsTransfer: "Переводы", statsAiExchange: "ИИ", statsPUBs: "PUBs", + statsSyncedPeers: "Узлы", + statsTotalLabel: "Всего", statsPost: "Посты", - statsOasisID: "Oasis ID", statsSize: "Итого (размер)", statsBlockchainSize: "Блокчейн (размер)", statsBlobsSize: "Блобы (размер)", statsActivity7d: "Активность (последние 7 дней)", statsActivity7dTotal: "Итого за 7 дней", statsActivity30dTotal: "Итого за 30 дней", - statsKarmaScore: "Очки КАРМЫ", - statsPublic: "Публичное", - statsPrivate: "Частное", messages: "Сообщения", statsProjectsTitle: "Проекты", statsProjectsTotal: "Всего проектов", @@ -2658,33 +2621,16 @@ module.exports = { statsProjectsCancelled: "Отменённые", statsProjectsGoalTotal: "Общая цель", statsProjectsPledgedTotal: "Всего поддержано", - statsProjectsSuccessRate: "Процент успеха", - statsProjectsAvgProgress: "Средний прогресс", - statsProjectsMedianProgress: "Медианный прогресс", - statsProjectsActiveFundingAvg: "Среднее активное финансирование", - statsJobsTitle: "Вакансии", - statsJobsTotal: "Всего вакансий", - statsJobsOpen: "Открытые", - statsJobsClosed: "Закрытые", - statsJobsOpenVacants: "Открытые места", - statsJobsSubscribersTotal: "Всего подписчиков", - statsJobsAvgSalary: "Средняя зарплата", - statsJobsMedianSalary: "Медианная зарплата", statsMarketTitle: "Рынок", statsMarketTotal: "Всего товаров", statsMarketForSale: "На продаже", statsMarketReserved: "Зарезервировано", statsMarketClosed: "Закрыто", statsMarketSold: "Продано", - statsMarketRevenue: "Выручка", - statsMarketAvgSoldPrice: "Средняя цена продажи", statsUsersTitle: "Жители", user: "Житель", - statsTombstoneTitle: "Надгробия", - statsNetworkTombstones: "Надгробия сети", statsTombstoneRatio: "Доля надгробий (%)", statsAITraining: "Обучение ИИ", - statsAIExchanges: "Обмены", statsParliamentCandidature: "Кандидатуры парламента", statsParliamentTerm: "Сроки парламента", statsParliamentProposal: "Предложения парламента", @@ -2709,28 +2655,26 @@ module.exports = { aiPrompt: "Предоставьте информативный и точный ответ.", aiConfiguration: "Установить запрос", aiPromptUsed: "Запрос", - aiClearHistory: "Очистить историю чата", - aiSharePrompt: "Добавить этот ответ в коллективное обучение?", - aiShareYes: "Да", - aiShareNo: "Нет", - aiSharedLabel: "Добавлено в обучение", - aiRejectedLabel: "Не добавлено в обучение", - aiServerError: "ИИ не смог ответить. Пожалуйста, попробуйте снова.", + aiClearHistory: "Очистить историю", + aiServerError: "Я не могу ответить как следует. Попробуйте ещё раз.", + aiStatusLoading: "42 загружает модель. Спросите снова через мгновение.", + aiThinking: "Думает", + aiWaiting: "Ожидание", + aiStatusMissingModel: "Модель ИИ не установлена: отсутствует src/AI/oasis-42-1-chat.Q4_K_M.gguf.", + aiExportFineTuning: "Экспорт Fine-Tuning", + aiFactsTitle: "Данные из вашей сети", typeAiExchange: "ИИ", aiApproveTrain: "Добавить в коллективное обучение", aiRejectTrain: "Не обучать", - aiTrainPending: "Ожидает одобрения", aiTrainApproved: "Одобрено для обучения", aiTrainRejected: "Отклонено для обучения", aiSnippetsUsed: "Использованные фрагменты", aiSnippetsLearned: "Изученные фрагменты", - aiApproveCustomTrain: "Обучить используя этот пользовательский ответ", aiCustomAnswerPlaceholder: "Напишите ваш пользовательский ответ…", marketMineSectionTitle: "Ваши товары", marketCreateSectionTitle: "Создать товар", marketUpdateSectionTitle: "Обновить", marketAllSectionTitle: "Рынок", - marketRecentSectionTitle: "Последние товары", marketTitle: "Рынок", marketDescription: "Торговая площадка для обмена товарами или услугами в вашей сети.", marketFilterAll: "ВСЕ", @@ -2745,7 +2689,7 @@ module.exports = { marketFilterDiscarded: "ОТКЛОНЁННЫЕ", marketFilterRecent: "НЕДАВНИЕ", marketFilterMyBids: "СТАВКИ", - marketCreateButton: "Создать товар", + marketCreateButton: "Продать что-то", marketItemType: "Тип", marketItemTitle: "Название", marketItemAvailable: "Срок", @@ -2760,14 +2704,11 @@ module.exports = { marketItemDeadline: "Срок", marketItemIncludesShipping: "Включена доставка?", marketItemHighestBid: "Наибольшая ставка", - marketItemHighestBidder: "Лидер аукциона", marketItemStock: "Запас", marketOutOfStock: "Нет в наличии", - marketItemBidTime: "Время ставки", marketActionsUpdate: "Обновить", marketUpdateButton: "Обновить", marketActionsDelete: "Удалить", - marketActionsSold: "Отметить как проданное", marketActionsChangeStatus: "ИЗМЕНИТЬ СТАТУС", marketActionsBuy: "КУПИТЬ!", marketAuctionBids: "Текущие ставки", @@ -2775,20 +2716,82 @@ module.exports = { marketItemSeller: "Продавец", marketNoItems: "Товаров пока нет.", marketYourBid: "Ваша ставка", - marketCreateFormImageLabel: "Загрузить медиа (макс: 50MB)", - marketSearchLabel: "Поиск", - marketSearchPlaceholder: "Поиск по названию или тегам", + marketCreateFormImageLabel: "Загрузить медиа (макс: 75MB)", + marketSearchPlaceholder: "Поиск в Рынок...", marketMinPriceLabel: "Мин. цена", marketMaxPriceLabel: "Макс. цена", - marketSortLabel: "Сортировать по", marketSortRecent: "Самые новые", marketSortPrice: "Цена", marketSortDeadline: "Срок", marketSearchButton: "Поиск", marketAuctionEndsIn: "Завершается", marketAuctionEnded: "Завершён", - marketMyBidBadge: "Ваша ставка", marketNoItemsMatch: "Товаров не найдено.", + housingTitle: "Жильё", + housingDescriptionText: "Находите и управляйте местами в вашей сети.", + modulesHousingLabel: "Жильё", + modulesHousingDescription: "Модуль для поиска и управления местами.", + typeHousing: "ЖИЛЬЁ", + housingFilterAll: "ВСЕ", + housingFilterMine: "МОИ", + housingFilterRequested: "ЗАПРОШЕННЫЕ", + housingFilterSale: "ПРОДАЖА", + housingFilterRent: "АРЕНДА", + housingFilterCouchsurfing: "КАУЧСЁРФИНГ", + housingFilterOpen: "ДОСТУПНЫЕ", + housingFilterClosed: "НЕДОСТУПНЫЕ", + housingFilterRecent: "НОВЫЕ", + housingFilterTop: "ТОП", + housingType: "Предложение", + housingTypeSALE: "Продажа", + housingTypeRENT: "Аренда", + housingTypeCOUCHSURFING: "Каучсёрфинг", + housingProperty: "Объект", + housingPropertyAPARTMENT: "Квартира", + housingPropertyHOUSE: "Дом", + housingPropertyROOM: "Комната", + housingPropertyLAND: "Участок", + housingPropertyOTHER: "Другое", + housingTitleLabel: "Заголовок", + housingTitlePlaceholder: "Название места", + housingDescription: "Описание", + housingDescriptionPlaceholder: "Опишите место, район, что включено: санузлы, сад, спальные места…", + housingRules: "Правила", + housingRulesPlaceholder: "Животные, курение, тихие часы, минимальный срок…", + housingPlace: "Место", + housingPlacePlaceholder: "Страна, город, адрес", + housingPrice: "Цена", + housingPriceLabel: "Цена (ECO)", + housingSizeLabel: "Площадь (м²)", + housingFree: "БЕСПЛАТНО", + housingPerMonth: "/месяц", + housingRooms: "Комнаты", + housingSize: "Площадь", + housingCapacity: "Гости", + housingAvailableFrom: "Доступно с", + housingAvailableTo: "Доступно до", + housingTags: "Теги", + spreadEditWarning: "Этот контент потеряет спреды после редактирования", + housingRequests: "Запросы", + housingRequestButton: "Запросить", + housingCancelButton: "Отменить запрос", + housingRequestedBadge: "ЗАПРОШЕНО", + housingStatus: "Статус", + housingStatusOPEN: "ДОСТУПНО", + housingStatusCLOSED: "НЕДОСТУПНО", + housingSetOpen: "Сделать доступным", + housingSetClosed: "Сделать недоступным", + housingCreateButton: "Создать место", + housingUpdateButton: "Обновить", + housingSearchPlaceholder: "Поиск в Жильё...", + housingSearchButton: "Искать", + housingMinPrice: "Мин. цена", + housingMaxPrice: "Макс. цена", + housingSortRecent: "Сначала новые", + housingSortPrice: "Сначала дешевле", + housingSortRequests: "Больше запросов", + housingSortRating: "Лучшие по оценке", + housingNoItems: "Места не найдены.", jobsTitle: "Вакансии", jobsCandidatesTitle: "Рекомендуемые кандидаты", jobsCandidatesDescription: "Жители, чьи навыки соответствуют этой вакансии. Отправьте им личное сообщение.", @@ -2837,8 +2840,7 @@ module.exports = { jobLocationPresencial: "На месте", jobLocationRemote: "Удалённо", jobVacantsPlaceholder: "Количество позиций", - jobSalaryPlaceholder: "Зарплата в ECO за 1 час работы", - jobImage: "Загрузить медиа (макс: 50MB)", + jobImage: "Загрузить медиа (макс: 75MB)", jobTasks: "Задачи", jobType: "Тип занятости", jobTime: "Время занятости", @@ -2847,7 +2849,6 @@ module.exports = { jobsFilterTop: "ТОП", jobsTopTitle: "Топ вакансий по зарплате", createJobButton: "Опубликовать вакансию", - viewDetailsButton: "Просмотр деталей", noJobsFound: "Вакансий не найдено.", jobAuthor: "Автор", jobTypeFreelance: "Фриланс", @@ -2859,41 +2860,207 @@ module.exports = { jobsHoursOffered: "Предлагаемые часы", jobsHoursRequested: "Часы, запрашиваемые взамен", jobsExchangeSkill: "Запрашиваемый навык в обмен", - jobsHoursOfferedPlaceholder: "напр. 4", - jobsHoursRequestedPlaceholder: "напр. 4", - jobsExchangeSkillPlaceholder: "напр. плотничество, дизайн", - jobExchangeHint: "Для работ по обмену часами заполните поля ниже вместо зарплаты.", jobTimePartial: "Неполная занятость", jobTimeComplete: "Полная занятость", - jobsDeleteButton: "УДАЛИТЬ", jobsUpdateButton: "ОБНОВИТЬ", jobsFilterApplied: "ПОДАННЫЕ", jobsAppliedTitle: "Мои заявки", jobsAppliedBadge: "Подано", - jobsFilterFavs: "Избранное", - jobsFavsTitle: "Избранное", - jobsFilterNeeds: "Нужна помощь", - jobsNeedsTitle: "Нужна помощь", - jobsSearchLabel: "Поиск", - jobsSearchPlaceholder: "Поиск по названию, тегам, описанию...", + jobsSearchPlaceholder: "Поиск в Вакансии...", jobsMinSalaryLabel: "Мин. зарплата", jobsMaxSalaryLabel: "Макс. зарплата", - jobsSortLabel: "Сортировать по", jobsSortRecent: "Самые новые", jobsSortSalary: "Самая высокая зарплата", jobsSortSubscribers: "Больше всего заявок", jobsSearchButton: "Поиск", - jobsFavoriteButton: "Добавить в избранное", - jobsUnfavoriteButton: "Удалить из избранного", - jobsMessageAuthorButton: "ЛС", jobsApplicants: "Заявители", jobsUpdatedAt: "Обновлено", - jobNewBadge: "НОВАЯ", jobsTagsLabel: "Теги", - jobsTagsPlaceholder: "тег1, тег2, тег3", noJobsMatch: "Вакансий не найдено.", + industrySearchPlaceholder: "Поиск в Промышленность...", + industryAllSectors: "Все секторы", + industryVisitButton: "Открыть промышленность", + industryAdvanceTo: "Перевести в", + industryAmount: "Сумма", + industryApproveBuild: "Одобрить", + industryBlueprint: "Чертёж", + industryBlueprints: "Чертежи", + industryBuild: "Сборка", + industryBuildNotes: "Заметки", + industryBuildStart: "Дата начала", + industryVoteUpdateButton: "Голос за обновление", + industryVoteDeleteButton: "Голос за удаление", + industryRevokeVote: "Отозвать", + industryTimeLeft: "Осталось времени", + industryBuildEnd: "Дата окончания", + industryBuilds: "Сборки", + industryBuildTitle: "Название", + industryContribute: "Внести вклад", + industryContributeButton: "Внести вклад", + industryContributions: "Вклады", + industryCreateBlueprintButton: "Создать чертёж", + industryDistribute: "Распределить", + industryDistributeButton: "Распределить по долям", + industryDistribution: "Распределение", + industryEcoAmount: "Сумма (ECO)", + industryEditBlueprint: "Редактировать чертёж", + industryFacility: "Фабрика", + industryKindLabel: "Тип", + industryKind_labor: "Труд", + industryKind_material: "Материал", + industryKind_eco: "Средства", + industryLaborHours: "Труд (часы)", + industryHours: "Часы", + industryMaterialItem: "Материал", + industryMaterials: "Материалы", + industryEstMaterials: "Итого материалы", + industryEstLabor: "Итого труд", + industryEstTotal: "Оценочная цена", + industryBuildingPrice: "Цена изготовления", + industryFinalPrice: "Итоговая цена", + industryBlueprintDescPlaceholder: "Характеристики, размеры, вес, документация…", + industryMaterialValue: "Стоимость (ECO)", + industryMember: "Участник", + industryNewBlueprint: "Новый чертёж", + industryNewBuild: "Предложить сборку", + industryEditBuild: "Редактировать производство", + industryNeedBlueprint: "Сначала создайте чертёж: каждое производство изготавливает его.", + industryNoBlueprints: "Пока нет чертежей.", + industryNoBuilds: "Пока нет сборок.", + industryNote: "Заметка", + industryOutput: "Выход", + industryOutputKind: "Тип продукта", + industryOutputValue: "Стоимость выхода (ECO, напр. от продажи)", + industryPostJob: "Создать вакансию", + industryPot: "Банк", + industryProposeBuildButton: "Предложить сборку", + industryProposer: "Инициатор", + industryAuthor: "Автор", + industrySellOnMarket: "Отправить на рынок", + industrySendToProjects: "Создать проект", + industrySkills: "Навыки", + industryTreasury: "Казна", + industryKind_physical: "Физический", + industryKind_digital: "Цифровой", + industryBuildStatus_PROPOSED: "ПРЕДЛОЖЕНО", + industryBuildStatus_REJECTED: "ОТКЛОНЁН", + industryBuildStatus_APPROVED: "ОДОБРЕНО", + industryBuildStatus_STOCKING: "СНАБЖЕНИЕ", + industryBuildStatus_IN_PRODUCTION: "В ПРОИЗВОДСТВЕ", + industryBuildStatus_COMPLETED: "ЗАВЕРШЕНО", + industryBuildStatus_FAILED: "ПРОВАЛЕНО", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "Вакансия совпадает с вашим резюме", + jobsBotMatchIntro: "Вакансия совпадает с вашим резюме.", + jobsBotMatchJob: "Вакансия", + jobsBotMatchHint: "Вы получаете это, потому что вашим резюме управляет ИИ. Отключить можно в резюме.", + cvAiManaged: "Управление ИИ", + switchOn: "ВКЛ", + switchOff: "ВЫКЛ", + cvAiManagedHint: "Если включено, JobsBot сообщит личным сообщением о подходящей вакансии.", + cvMatchThreshold: "Уведомлять с этого уровня совпадения", + housingBotRequestedTitle: "Кто-то запросил одно из ваших мест.", + housingBotCancelledTitle: "Запрос на одно из ваших мест отменён.", + housingBotYouRequestedTitle: "Вы запросили место.", + housingBotYouCancelledTitle: "Вы отменили запрос.", + housingBotUnavailableTitle: "Запрошенное вами место больше недоступно.", + housingBotUnavailableSentTitle: "Вы уведомили тех, кто запрашивал это место.", + industryBotAdmittedTitle: "Вас приняли на фабрику.", + industryBotApplicationTitle: "Новая заявка на вступление в вашу фабрику.", + industryBotInvitedTitle: "Вас пригласили на фабрику.", + industryBotDissolvedTitle: "Фабрика, к которой вы принадлежите, была распущена.", + industryBotBuildApprovedTitle: "Сборка одобрена.", + industryBotDistributedTitle: "Выход сборки распределён.", + industryFilterRules: "ПРАВИЛА", + industryRulesTitle: "Правила", + industryRulesIntro: "Industry позволяет жителям коллективно владеть средствами производства: Фабрика — это принадлежащая сети мастерская, которой никто не владеет частно.", + industryRulesSteward: "Основатель — управляющий, хранитель, а не владелец: он не может в одиночку продать, приватизировать или распустить Фабрику.", + industryRulesMembership: "Политика членства задаёт способ вступления: Открытая (сразу), Голосованием (принимают голосом участников) или Только по приглашению (сначала должен пригласить участник).", + industryRulesQuorum: "Кворум — минимальное число участников, которые должны проголосовать, чтобы решение засчиталось, чтобы маленькую Фабрику не решал один житель.", + industryRulesMajority: "Большинство — доля голосов для принятия (0,5 = половина, 1 = единогласие); решение проходит, когда голоса ЗА достигают хотя бы max(кворум, участники × большинство).", + industryRulesDecisions: "Участники голосуют за приём новых, одобрение сборок и роспуск Фабрики; управляющий не может обойти эти коллективные решения.", + industryRulesBlueprints: "Чертежи — свободные (COPYLEFT) рецепты: входы (материалы, часы труда, навыки) и выход (физический или цифровой продукт), и любой может их форкнуть.", + industryRulesBuilds: "Сборка — производственный заказ: ПРЕДЛОЖЕНО → ОДОБРЕНО (голосованием) → СНАБЖЕНИЕ → В ПРОИЗВОДСТВЕ → ЗАВЕРШЕНО, и принимает вклады только после одобрения.", + industryRulesContributions: "Участники вносят труд (часы), материалы (заявленная стоимость в ECO) или ECO в сборку; каждый вклад даёт карму.", + industryRulesLaborRate: "Ставка труда — стоимость в ECO одного рабочего часа на этой Фабрике; она переводит часы в карму, чтобы справедливо сравнивать труд и капитал: карма = часы × ставка + стоимость материалов + ECO.", + industryRulesShares: "Ваша доля в сборке — это ваша карма ÷ общая карма, и она определяет, сколько стоимости выхода вы получите.", + industryRulesDistribution: "После завершения производства стюард распределяет банк (средства производства плюс стоимость продажи) между участниками пропорционально их долям.", + industryRulesTreasury: "Вклады в ECO хранит управляющий как хранитель казны сборки до распределения; все движения записываются в сети, а споры могут идти в Courts.", + industryJobs: "Вакансии", + industryReportButton: "Пожаловаться", + industryCreateTask: "Создать задачу", + industryOpenDispute: "Открыть спор", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "Создать приглашение", + industryPauseVotes: "Голоса за статус", + industryTitle: "Промышленность", + industryDescription: "Модуль для коллективного управления средствами производства.", + typeIndustryBuild: "СБОРКА", + typeIndustryBlueprint: "ЧЕРТЁЖ", + typeIndustryAllocation: "РАСПРЕДЕЛЕНИЕ", + typeIndustry: "ПРОМЫШЛЕННОСТЬ", + modulesIndustryLabel: "Промышленность", + modulesIndustryDescription: "Модуль для коллективного управления средствами производства.", + industryFilterAll: "ВСЕ", + industryFilterRecent: "НЕДАВНИЕ", + industryFilterMember: "УЧАСТНИК", + industryFilterBlueprints: "ЧЕРТЕЖИ", + industryFilterBuilds: "ПРОИЗВОДСТВА", + industryFilterMine: "МОИ", + industryFilterActive: "РАБОТАЕТ", + industryFilterPaused: "НА ПАУЗЕ", + industryFilterDissolved: "РАСПУЩЕННЫЕ", + industryAllTitle: "Промышленность", + industryRecentTitle: "Недавние фабрики", + industryMemberTitle: "Фабрики, где я состою", + industryMineTitle: "Мои фабрики", + industryActiveTitle: "Работает", + industryPausedTitle: "Фабрики на паузе", + industryDissolvedTitle: "Распущенные фабрики", + industryNoFacilitiesFound: "Фабрики не найдены.", + industryCreateFacility: "Создать фабрику", + industryMembers: "Участники", + industryMemberBadge: "УЧАСТНИК", + industryName: "Название", + industryNamePlaceholder: "Название фабрики", + industryDescriptionLabel: "Описание", + industryDescriptionPlaceholder: "Что производит эта фабрика?", + industrySector: "Сектор", + industryMembershipPolicy: "Политика членства", + industryQuorum: "Кворум", + industryMajority: "Большинство", + industryLaborRate: "Ставка труда", + industryCreateButton: "Создать фабрику", + industryUpdateButton: "Обновить", + industrySteward: "Управляющий", + industryStatusActive: "РАБОТАЕТ", + industryStatusPaused: "НА ПАУЗЕ", + industryStatusDissolved: "РАСПУЩЕНА", + industryStatusLabel: "Статус", + industryJoinButton: "Присоединиться", + industryApplyButton: "Подать заявку", + industryLeaveButton: "Покинуть", + industryInviteButton: "Пригласить", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "Для вступления нужно приглашение.", + industryPauseButton: "Голосовать за паузу", + industryResumeButton: "Голосовать за возобновление", + industryDeleteButton: "Удалить", + industryPendingApplicants: "Ожидающие заявки", + industryVotesYes: "да", + industryVotesNo: "нет", + industryAlreadyVoted: "проголосовал", + industryAdmit: "Принять", + industryReject: "Отклонить", + industryDissolveTitle: "Распустить фабрику", + industryDissolveVote: "Голосовать за роспуск", + industryPolicy_open: "Открытая", + industryPolicy_vote: "Голосованием", + industryPolicy_invite: "Только по приглашению", projectsTitle: "Проекты", projectsDescription: "Создавайте, финансируйте и следите за проектами сообщества в вашей сети.", + projectSearchPlaceholder: "Поиск в Проекты...", projectCreateProject: "Создать проект", projectCreateButton: "Создать проект", projectUpdateButton: "ОБНОВИТЬ", @@ -2916,7 +3083,7 @@ module.exports = { projectRecentTitle: "Последние проекты", projectTopTitle: "Топ по финансированию", projectTitlePlaceholder: "Название проекта", - projectImage: "Загрузить медиа (макс: 50MB)", + projectImage: "Загрузить медиа (макс: 75MB)", projectDescription: "Описание", projectDescriptionPlaceholder: "Расскажите историю и цели…", projectGoal: "Цель (ECO)", @@ -2937,14 +3104,8 @@ module.exports = { projectPledgeTitle: "Поддержать проект", projectPledgePlaceholder: "Сумма в ECO", projectBounties: "Награды", - projectBountiesInputLabel: "Награды (по одной в строке: Название|Сумма [ECO]|Описание)", - projectBountiesPlaceholder: "Исправить баг UI|100|Ссылка на задачу\nНаписать документацию|250|Примеры использования", projectNoBounties: "Наград не найдено.", projectTitle: "Название", - projectAddBountyTitle: "Новая награда", - projectBountyTitle: "Название награды", - projectBountyAmount: "Сумма (ECO)", - projectBountyDescription: "Описание", projectMilestoneSelect: "Выбрать веху", projectBountyCreateButton: "Создать награду", projectBountyStatus: "Статус награды", @@ -2959,19 +3120,15 @@ module.exports = { projectMilestoneTitle: "Название вехи", projectMilestoneTargetPercent: "Процент (%)", projectMilestoneDueDate: "Дата", - projectMilestoneCreateButton: "Создать веху", projectMilestoneStatus: "Статус вехи", projectMilestoneOpen: "открытая", projectMilestoneDone: "завершена", projectMilestoneMarkDone: "Отметить завершённой", projectMilestoneDue: "дата", - projectNoMilestones: "Вех не найдено.", projectMilestoneTitlePlaceholder: "Введите название вехи", projectMilestoneDescriptionPlaceholder: "Введите описание этой вехи", projectMilestoneDescription: "Описание вехи", - projectBudgetGoal: "Бюджет (Цель)", projectBudgetAssigned: "Назначено на награды", - projectBudgetRemaining: "Остаток", projectBudgetOver: "⚠ Превышение бюджета: назначено больше цели", projectFollowers: "Подписчики", projectFollowersTitle: "Подписчики", @@ -2984,7 +3141,6 @@ module.exports = { projectBackersTotalPledged: "Всего поддержано", projectBackersYourPledge: "Ваш взнос", projectBackersNone: "Взносов пока нет.", - projectNoRemainingBudget: "Оставшегося бюджета нет.", projectFilterBackers: "СПОНСОРЫ", projectFilterApplied: "ПОДАННЫЕ", projectAppliedTitle: "ПОДАННЫЕ", @@ -2993,36 +3149,19 @@ module.exports = { projectBackerAmount: "Всего вложено", projectBackerPledges: "Взносы", projectBackerProjects: "Проекты", - projectPledgeAmount: "Сумма", projectSelectMilestoneOrBounty: "Выбрать веху или награду", projectPledgeButton: "Поддержать", footerLicense: "GPLv3", - footerPackage: "Пакет", - footerVersion: "Версия", modulesModuleName: "Имя", modulesModuleDescription: "Описание", modulesModuleStatus: "Статус", - modulesTotalModulesLabel: "Загруженные модули", + modulesTotalModulesLabel: "Всего", modulesEnabledModulesLabel: "Включённые", modulesDisabledModulesLabel: "Отключённые", - modulesPopularLabel: "Популярное", - modulesPopularDescription: "Модуль для получения популярных, наиболее просматриваемых или комментируемых постов.", - modulesTopicsLabel: "Темы", - modulesTopicsDescription: "Модуль для получения категорий обсуждений на основе общих интересов.", - modulesSummariesLabel: "Резюме", - modulesSummariesDescription: "Модуль для получения резюме длинных обсуждений или постов.", - modulesLatestLabel: "Последние", - modulesLatestDescription: "Модуль для получения последних постов и обсуждений.", - modulesThreadsLabel: "Треды", - modulesThreadsDescription: "Модуль для получения разговоров, сгруппированных по теме или вопросу.", - modulesMultiverseLabel: "Мультивселенная", - modulesMultiverseDescription: "Модуль для получения контента от других федеративных узлов.", modulesInvitesLabel: "Приглашения", modulesInvitesDescription: "Модуль для управления и применения кодов приглашений.", modulesWalletLabel: "Кошелёк", modulesWalletDescription: "Модуль для управления вашими цифровыми активами (ECOin).", - modulesLegacyLabel: "Наследие", - modulesLegacyDescription: "Модуль для быстрого и безопасного управления вашим секретом (приватным ключом).", modulesCipherLabel: "Шифр", modulesCipherDescription: "Модуль для симметричного шифрования и расшифровки текста (с использованием общего пароля).", modulesBookmarksLabel: "Закладки", @@ -3057,8 +3196,12 @@ module.exports = { modulesOpinionsDescription: "Модуль для открытия и голосования за мнения.", modulesTransfersLabel: "Переводы", modulesTransfersDescription: "Модуль для открытия и управления смарт-контрактами (переводами).", - modulesFediverseLabel: "Федиверс", - modulesFediverseDescription: "Управляйте своими другими аккаунтами федиверса, включая отправку и получение контента.", + modulesFediverseLabel: "Мультивселенная", + modulesBlogsLabel: "Блоги", + modulesBlogsDescription: "Модуль для поиска и управления блогами.", + modulesPollsLabel: "Опросы", + modulesPollsDescription: "Модуль, чтобы спросить сеть и подсчитать ответы.", + modulesFediverseDescription: "Управляйте своими другими аккаунтами мультивселенной, включая отправку и получение контента.", modulesFeedLabel: "Лента", modulesFeedDescription: "Модуль для открытия и публикации коротких текстов (лента).", modulesParliamentLabel: "Парламент", @@ -3080,59 +3223,54 @@ module.exports = { modulesProjectsLabel: "Проекты", modulesProjectsDescription: "Модуль для исследования, краудфандинга и управления проектами.", modulesBankingLabel: "Банкинг", - modulesBankingDescription: "Модуль для определения реальной стоимости ECOIN и распределения UBI из общей казны.", + modulesBankingDescription: "Модуль управления общей казной: стоимость ECOIN, налоги и БОД.", modulesFavoritesLabel: "Избранное", modulesFavoritesDescription: "Модуль для управления избранным контентом.", fileTooLargeTitle: "Файл слишком большой", fileTooLargeMessage: "Файл превышает максимально допустимый размер (50 МБ). Пожалуйста, выберите файл меньшего размера.", goBack: "Назад", + errorDismiss: "ОК", errorPageTitle: "Что-то пошло не так", + errorNoAllocation: "Распределение для этого запроса не найдено.", + errorInvalidClaim: "Это распределение нельзя получить.", visibilityLabel: "Видимость", + clearnetPrivateConflict: "Приватный, скрытый, закрытый, доступный только по приглашению или платный контент нельзя опубликовать в CLEARNET. Выберите OASIS или сделайте его публичным.", visibilityPublic: "Публичная", visibilityHidden: "Скрытая", visibilityMakePublic: "Сделать публичным", visibilityMakeHidden: "Скрыть", invitesInhabitantsTitle: "Жители", - invitesInhabitantsFollow: "Подписаться", - aiNavPlaceholder: "Куда вы хотите перейти?", + aiNavPlaceholder: "Опишите, что вам нужно...", aiNavDisabled: "Навигация с ИИ отключена.", - aiNavResultsTitle: "Предложения навигации с ИИ", + aiNavResultsTitle: "Подсказки AINav", aiNavQueryLabel: "Запрос", - aiNavResultsDescription: "Выберите маршрут, наиболее подходящий вашему запросу.", - aiNavResultPath: "Маршрут", - aiNavResultDescription: "Что можно сделать", aiNavResultMatch: "Совпадение", aiNavResultsEmpty: "Подходящих маршрутов нет. Попробуйте /search.", - aiNavSearchInstead: "Поиск вместо этого", modulesAINavLabel: "AINav", modulesAINavDescription: "Модуль для запросов на естественном языке к содержимому сети.", - directConnect: "Прямое подключение", directConnectDescription: "Подключитесь напрямую к узлу, введя его IP-адрес, порт и публичный ключ. Узел будет добавлен как отслеживаемое соединение.", peerHost: "IP", - peerPort: "Порт (по умолчанию: 8008)", + peerPort: "Порт", peerPublicKey: "Публичный ключ (@...ed25519)", connectAndFollow: "Подключиться", - deviceSourceLabel: "Источник устройства", modulesPresetTitle: "Общие конфигурации", - modulesPreset_minimal: "Минимальный", - modulesPreset_basic: "Базовая", - modulesPreset_social: "Социальный", - modulesPreset_economy: "Экономическая", - modulesPreset_full: "Полная", + workflowsTitle: "Рабочие режимы", + workflowsDescription: "Режим задаёт тему и набор активных модулей.", + workflowsSet: "Применить режим", + workflow_default: "По умолчанию", + workflow_jobs: "Работа", + workflow_social: "Общение", + workflow_business: "Бизнес", + workflow_night: "Ночь", + workflow_kids: "Дети", + workflow_activists: "Активизм", + workflow_mobile: "Мобильный", statsCarbonFootprintTitle: "Углеродный след", - statsCarbonFootprintNetwork: "Углеродный след сети", - statsCarbonFootprintYours: "Ваш углеродный след", statsCarbonTombstone: "Углеродный след надгробий", - feedSuccessMsg: "Пост успешно опубликован!", - dominantOpinionLabel: "Доминирующее мнение", - uploadMedia: "Загрузить медиа (макс: 50MB)", - feedViewDetails: "Просмотр деталей", + uploadMedia: "Загрузить медиа (макс: 75MB)", feedOpenDiscussion: "Открыть обсуждение", feedDetailTitle: "Лента", feedPostComment: "Опубликовать комментарий", - eventFileLabel: "Прикрепить изображение", - taskFileLabel: "Прикрепить изображение", - courtsRespondentRequired: "ID ответчика обязателен", courtsRespondentInvalid: "Должен быть действительным SSB ID (@...ed25519)", noComments: "Пока нет комментариев", bankAllocId: "ID распределения", @@ -3142,7 +3280,7 @@ module.exports = { deviceLabel: "Устройство", mapsLabel: "Карты", mapTitle: "Карты", - mapDescription: "Исследуйте и управляйте офлайн-картами в вашей сети.", + mapDescription: "Управляйте офлайн-картами и делитесь ими в вашей сети.", mapMineSectionTitle: "Ваши Карты", mapCreateSectionTitle: "Создать Карту", mapUpdateSectionTitle: "Обновить Карту", @@ -3166,14 +3304,13 @@ module.exports = { mapDescriptionLabel: "Описание", mapDescriptionPlaceholder: "Опишите карту или местоположение...", mapTypeLabel: "Тип карты", - mapTypeSingle: "ОДИНОЧНЫЙ (только начальное местоположение)", - mapTypeOpen: "ОТКРЫТЫЙ (любой может добавлять маркеры)", - mapTypeClosed: "ЗАКРЫТЫЙ (только создатель добавляет маркеры)", + mapInviteMode: "Приглашение", + mapTypeSingle: "ОДИНОЧНЫЙ", + mapTypeOpen: "ОТКРЫТЫЙ", + mapTypeClosed: "ЗАКРЫТЫЙ", mapTagsLabel: "Теги", mapTagsPlaceholder: "Введите теги через запятую", mapUrlLabel: "URL карты", - mapPickCoordLabel: "Или выберите местоположение на сетке:", - mapMarkersLabel: "маркеры", mapMarkersTitle: "Маркеры", mapMarkerDefault: "Маркер", mapMarkerLatLabel: "Широта маркера", @@ -3183,9 +3320,22 @@ module.exports = { markerImageLabel: "Изображение Маркера", mapAddMarkerTitle: "Добавить Маркер", mapAddMarkerButton: "Добавить Маркер", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "Очистить метку", mapApplyZoom: "Применить масштаб", - mapSearchPlaceholder: "Поиск по описанию, тегам, автору...", + mapPlaceSearchPlaceholder: "Найти место или страну...", + mapNoPlaces: "Места не найдены.", + mapClusterTitle: "меток здесь, нажмите для приближения", + mapDeleteMarker: "Удалить метку", + mapZoomIn: "Приблизить", + mapZoomOut: "Отдалить", + mapFitMarkers: "Показать все метки", + mapPanLabel: "Переместить карту", + mapCenterHere: "Центрировать здесь", + mapNear: "рядом с", + mapPickedSpot: "Выбранная точка", + mapPickHint: "Нажмите на карту, чтобы выбрать точку.", + mapClearPick: "Очистить", + mapSearchPlaceholder: "Поиск в Карты...", mapSearchButton: "Искать", mapUpdatedAt: "Обновлено", mapNoMatch: "Нет карт, соответствующих вашему запросу.", @@ -3196,118 +3346,820 @@ module.exports = { typeMapMarker: "МАРКЕР КАРТЫ", modulesMapLabel: "Карты", modulesMapDescription: "Модуль для управления и обмена офлайн-картами.", + emergenciesTitle: "Чрезвычайные ситуации", + modulesEmergenciesLabel: "Чрезвычайные ситуации", + modulesEmergenciesDescription: "Модуль для объявления, подтверждения и отслеживания чрезвычайных ситуаций в сети.", + emergenciesDescription: "Модуль для объявления, подтверждения и отслеживания чрезвычайных ситуаций в сети.", + emergencyCreate: "Создать ЧС", + emergencyUpdate: "Обновить", + emergencyDelete: "Удалить", + emergencyTitleLabel: "Заголовок", + emergencyTitlePlaceholder: "Что происходит?", + emergencyTextLabel: "Подробности", + emergencyTextPlaceholder: "Где, с какого момента, что делать...", + emergencyCategoryLabel: "Категория", + emergencyCategoryWeather: "ПОГОДА", + emergencyCategoryInfrastructure: "ИНФРАСТРУКТУРА", + emergencyCategoryHealth: "ЗДОРОВЬЕ", + emergencyCategorySecurity: "БЕЗОПАСНОСТЬ", + emergencyCategoryLost: "ПОТЕРИ И НАХОДКИ", + emergencyCategoryNeighborhood: "СОСЕДСТВО", + emergencyExpiresLabel: "Истекает через", + emergencyExpires1d: "1 день", + emergencyExpires3d: "3 дня", + emergencyExpires7d: "1 неделя", + emergencyExpires30d: "1 месяц", + emergencyExpiresNone: "Не определено", + emergencyExpiresKeep: "Оставить текущую дату", + emergencyTagsLabel: "Теги", + emergencyTagsPlaceholder: "Введите теги через запятую", + emergencyFilterAll: "ВСЕ", + emergencyFilterMine: "МОИ", + emergencyFilterRecent: "НЕДАВНИЕ", + emergencySeverityUnverified: "НЕ ПОДТВЕРЖДЕНО", + emergencySeverityLow: "НИЗКАЯ", + emergencySeverityMedium: "СРЕДНЯЯ", + emergencySeverityHigh: "ВЫСОКАЯ", + emergencyStatusActive: "АКТИВНО", + emergencyStatusResolved: "РЕШЕНО", + emergencyStatusExpired: "ИСТЕКЛО", + emergencyConfirmations: "Подтверждения", + emergencyConfirmButton: "Подтвердить", + emergencyConfirmed: "Вы подтвердили эту чрезвычайную ситуацию", + emergencyResolveButton: "Закрыть", + emergencyReopenButton: "Открыть снова", + emergencyGenerateReport: "Создать отчёт", + emergencyUpdateEdited: "Изменено", + emergencyLastUpdate: "ПОСЛЕДНЕЕ ОБНОВЛЕНИЕ", + emergencyUpdatesTitle: "Последние обновления:", + emergencyNoUpdates: "Обновлений пока нет.", + emergencyAddUpdate: "Опубликовать обновление", + emergencyUpdatePlaceholder: "Что изменилось?", + emergencyNoItems: "Чрезвычайные ситуации не найдены.", + emergencyExpiresAt: "Истекает", + emergencyBannerLabel: "Активная ЧС", + emergencyCreateSectionTitle: "Создать ЧС", + emergencyUpdateSectionTitle: "Обновить ЧС", + emergencySearchPlaceholder: "Поиск по чрезвычайным ситуациям...", + typeEmergency: "ЧС", + typeEmergencyConfirm: "ПОДТВЕРЖДЕНИЕ ЧС", + typeEmergencyUpdate: "ОБНОВЛЕНИЕ ЧС", + statsEmergency: "Чрезвычайные ситуации", + statsEmergencyConfirm: "Подтверждения ЧС", + statsEmergencyUpdate: "Обновления ЧС", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "В отслеживаемой вами ЧС появилось обновление.", + emergencyBotResolvedTitle: "Отслеживаемая вами ЧС разрешена.", + dataKindEmergencies: "ЧС", + mailingTitle: "Списки рассылки", + modulesMailingLabel: "Списки рассылки", + modulesMailingDescription: "Находите и управляйте списками рассылки в своей сети.", + modulesPhoneLabel: "Телефон", + modulesPhoneDescription: "Модуль для видеозвонков другим жителям со сквозным шифрованием.", + mailingDescription: "Находите и управляйте списками рассылки в своей сети.", + mailingCreate: "Создать список рассылки", + mailingUpdate: "Обновить", + mailingArchive: "В архив", + mailingActivate: "Активировать", + mailingCreateSectionTitle: "Создать список рассылки", + mailingUpdateSectionTitle: "Обновить список рассылки", + mailingTitleLabel: "Название", + mailingTitlePlaceholder: "Название списка рассылки", + mailingDescriptionLabel: "Описание", + mailingDescriptionPlaceholder: "О чём этот список?", + mailingTypeLabel: "Тип списка рассылки", + mailingTypeOpen: "ОТКРЫТЫЙ", + mailingTypeClosed: "ЗАКРЫТЫЙ", + mailingStatusLabel: "Статус", + mailingStatusActive: "АКТИВНЫЙ", + mailingStatusArchived: "В АРХИВЕ", + mailingMembersLabel: "Пригласить жителей", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "Подписчики", + mailingTagsLabel: "Теги", + mailingTagsPlaceholder: "Введите теги через запятую", + mailingFilterAll: "ВСЕ", + mailingFilterMine: "МОИ", + mailingFilterSubscribed: "ПОДПИСКИ", + mailingFilterRecent: "НЕДАВНИЕ", + mailingHistoryDate: "ДАТА", + mailingHistoryThreads: "ТЕМЫ", + mailingHistoryInhabitants: "ЖИТЕЛИ", + mailingHistorySearchPlaceholder: "Поиск по истории...", + mailingNoMessages: "Сообщений пока нет.", + mailingNoItems: "Списки рассылки не найдены.", + mailingSearchPlaceholder: "Поиск по спискам рассылки...", + mailingWriteButton: "Написать сообщение", + mailingWriteTitle: "Написать в список", + mailingReplyTitle: "Ответить в список", + mailingSend: "Отправить", + mailingTextPlaceholder: "Ваше сообщение получат все участники списка.", + mailingParticipants: "Участники", + inboxFilterMailing: "Списки рассылки", + mailingReplyPlaceholder: "Ответить в список...", + mailingReplyToList: "Ответить в список", + typeMailingList: "СПИСОК РАССЫЛКИ", + statsMailingList: "Списки рассылки", + mailingListLabel: "СПИСОК РАССЫЛКИ", + logisticsTitle: "Логистика", + modulesLogisticsLabel: "Логистика", + modulesLogisticsDescription: "Находите и управляйте логистическими планами в своей сети.", + logisticsDescription: "Находите и управляйте логистическими планами в своей сети.", + logisticsCreate: "Создать маршрут", + logisticsUpdate: "Обновить", + logisticsClose: "Закрыть", + logisticsReopen: "Открыть снова", + logisticsCreateSectionTitle: "Создать маршрут", + logisticsUpdateSectionTitle: "Обновить маршрут", + logisticsTitleLabel: "Название", + logisticsTitlePlaceholder: "Поездка на рынок, есть фургон...", + logisticsKindLabel: "Тип логистики", + logisticsKindTrip: "ПОЕЗДКА", + logisticsKindShipment: "ПЕРЕВОЗКА", + logisticsKindTrips: "ПОЕЗДКИ", + logisticsKindShipments: "ПЕРЕВОЗКИ", + logisticsModeLabel: "Режим", + logisticsModeOffer: "ПРЕДЛАГАЮ", + logisticsModeRequest: "ИЩУ", + logisticsModeOffers: "ПРЕДЛОЖЕНИЯ", + logisticsModeRequests: "ЗАПРОСЫ", + logisticsDescriptionLabel: "Описание", + logisticsDescriptionPlaceholder: "Место встречи, что везёте, условия...", + logisticsOriginLabel: "Откуда", + logisticsOriginPlaceholder: "Где начинается", + logisticsDestinationLabel: "Куда", + logisticsDestinationPlaceholder: "Где заканчивается", + logisticsDateLabel: "Дата и время", + logisticsRecurrenceLabel: "Повторение", + logisticsFrequencyLabel: "Периодичность", + logisticsRecurrenceNone: "ОДНОКРАТНО", + logisticsRecurrenceDaily: "ЕЖЕДНЕВНО", + logisticsRecurrenceWeekly: "ЕЖЕНЕДЕЛЬНО", + logisticsRecurrenceMonthly: "ЕЖЕМЕСЯЧНО", + logisticsSeatsLabel: "Места", + logisticsSizeLabel: "Размер", + logisticsSizePlaceholder: "напр. 2m x 1m", + logisticsWeightLabel: "Вес", + logisticsWeightPlaceholder: "напр. 40kg", + logisticsPriceFree: "БЕСПЛАТНО", + logisticsPriceEco: "ECO", + logisticsPriceTime: "ВРЕМЯ", + logisticsPriceLabel: "Цена", + logisticsHours: "ч", + logisticsOrderRefLabel: "Ссылка на заказ", + logisticsOrderRefPlaceholder: "/shops/... или /market/... заказа для доставки", + logisticsOrderLink: "Связанный заказ", + logisticsTagsLabel: "Теги", + logisticsTagsPlaceholder: "Введите теги через запятую", + logisticsFilterAll: "ВСЕ", + logisticsFilterMine: "МОИ", + logisticsFilterBooked: "ЗАБРОНИРОВАННЫЕ", + logisticsFilterRecent: "НЕДАВНИЕ", + logisticsFilterHistory: "ИСТОРИЯ", + logisticsFilterTop: "ТОП", + logisticsStatusOpen: "ОТКРЫТ", + logisticsStatusUpcoming: "ПРЕДСТОЯЩИЕ", + logisticsStatusPast: "ПРОШЕДШИЕ", + logisticsStatusClosed: "ЗАКРЫТЫЕ", + logisticsZoneLabel: "Зона", + logisticsSearchPlaceholder: "Поиск по логистике...", + logisticsNoItems: "Маршруты не найдены.", + logisticsBookingPending: "ОЖИДАЕТ", + logisticsBookingConfirmed: "ПОДТВЕРЖДЕНО", + logisticsBookingRejected: "ОТКЛОНЕНО", + logisticsBookingCancelled: "ОТМЕНЕНО", + logisticsBookingDelivered: "ДОСТАВЛЕНО", + logisticsConfirm: "Подтвердить", + logisticsReject: "Отклонить", + logisticsDeliver: "Доставить", + logisticsReceiptLabel: "Квитанция", + logisticsReceiptPlaceholder: "Примечание к доставке (необязательно)", + logisticsCancelBooking: "Отменить бронь", + logisticsBookTitle: "Забронировать маршрут", + logisticsNotesLabel: "Заметки", + logisticsNotesPlaceholder: "Что должен знать владелец", + logisticsBook: "Забронировать", + logisticsBookingsTitle: "Брони", + logisticsMyBookingTitle: "Ваша бронь", + logisticsRatingsTitle: "Оценки", + logisticsNoRatings: "Оценок пока нет.", + logisticsRatingsWhenClosed: "Оценки открываются после закрытия маршрута.", + logisticsScoreLabel: "Балл", + logisticsRatingPlaceholder: "Как всё прошло?", + logisticsRate: "Оценить", + typeLogisticsRoute: "МАРШРУТ", + typeLogisticsRating: "ОЦЕНКА МАРШРУТА", + statsLogisticsRoute: "Маршруты", + statsLogisticsRating: "Оценки маршрутов", + logisticsRouteLabel: "МАРШРУТ", + podcastsTitle: "Подкасты", + modulesPodcastsLabel: "Подкасты", + modulesPodcastsDescription: "Модуль для публикации подкаст-каналов.", + podcastsDescription: "Модуль для публикации подкаст-каналов.", + podcastCreate: "Создать подкаст", + podcastUpdate: "Обновить", + podcastDelete: "Удалить", + podcastCreateSectionTitle: "Создать подкаст", + podcastUpdateSectionTitle: "Обновить подкаст", + podcastTitleLabel: "Название", + podcastTitlePlaceholder: "Название канала", + podcastDescriptionLabel: "Описание", + podcastDescriptionPlaceholder: "О чём этот подкаст?", + podcastCategoryLabel: "Категория", + podcastCategoryNews: "НОВОСТИ", + podcastCategoryMusic: "МУЗЫКА", + podcastCategoryTalk: "БЕСЕДЫ", + podcastCategoryEducation: "ОБРАЗОВАНИЕ", + podcastCategoryTech: "ТЕХНОЛОГИИ", + podcastCategoryCulture: "КУЛЬТУРА", + podcastCategoryCommunity: "СООБЩЕСТВО", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "ДРУГОЕ", + podcastTagsLabel: "Теги", + podcastCoverLabel: "Обложка (изображение или видео)", + podcastEpisodeAudioLabel: "Аудио или видеофайл (макс: 75MB)", + podcastTagsPlaceholder: "Введите теги через запятую", + podcastFilterAll: "ВСЕ", + podcastFilterMine: "МОИ", + podcastFilterRecent: "НЕДАВНИЕ", + podcastFilterTop: "ТОП", + podcastFilterViewers: "СЛУШАТЕЛИ", + podcastSearchPlaceholder: "Поиск по подкастам...", + podcastNoItems: "Подкасты не найдены.", + podcastEpisodesLabel: "Выпуски", + podcastPlaysLabel: "Просмотры", + podcastLastEpisode: "Последний выпуск", + podcastEpisodesTitle: "Выпуски", + podcastNoEpisodes: "Выпусков пока нет.", + podcastEpisodeAdd: "Добавить выпуск", + podcastEpisodeCreateTitle: "Опубликовать выпуск", + podcastEpisodeUpdateTitle: "Обновить выпуск", + podcastEpisodeTitleLabel: "Название выпуска", + podcastEpisodeTitlePlaceholder: "Название выпуска", + podcastEpisodeDescriptionLabel: "Заметки", + podcastEpisodeDescriptionPlaceholder: "Что происходит в выпуске, ссылки, авторы...", + podcastEpisodePublish: "Опубликовать", + podcastPlayed: "Прослушано", + podcastMarkPlayed: "Отметить как прослушанное", + typePodcast: "ПОДКАСТ", + typePodcastEpisode: "ВЫПУСК", + typePodcastOpinion: "МНЕНИЕ О ВЫПУСКЕ", + typePodcastPlay: "ПРОСЛУШИВАНИЕ", + statsPodcast: "Подкасты", + statsPodcastEpisode: "Выпуски", + statsPodcastPlay: "Прослушивания", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "В блоге, на который вы подписаны, новая запись.", + podcastBotEpisodeTitle: "У подкаста, на который вы подписаны, новый выпуск.", + podcastLabel: "ПОДКАСТ", + podcastEpisodeLabel: "ВЫПУСК", + campaignsTitle: "Кампании", + modulesCampaignsLabel: "Кампании", + modulesCampaignsDescription: "Находите и управляйте кампаниями в своей сети.", + campaignsDescription: "Находите и управляйте кампаниями в своей сети.", + campaignCreate: "Создать кампанию", + campaignUpdate: "Обновить", + campaignDelete: "Удалить", + campaignClose: "Закрыть", + campaignReopen: "Открыть снова", + campaignCreateSectionTitle: "Создать кампанию", + campaignUpdateSectionTitle: "Обновить кампанию", + campaignTitleLabel: "Название", + campaignTitlePlaceholder: "Чего вы хотите добиться?", + campaignTextLabel: "Текст", + campaignTextPlaceholder: "Объясните суть, кто решает и чего вы требуете...", + campaignCategoryLabel: "Категория", + campaignCategoryEnvironment: "ЭКОЛОГИЯ", + campaignCategoryRights: "ПРАВА", + campaignCategoryHealth: "ЗДОРОВЬЕ", + campaignCategoryEducation: "ОБРАЗОВАНИЕ", + campaignCategoryInfrastructure: "ИНФРАСТРУКТУРА", + campaignCategoryCulture: "КУЛЬТУРА", + campaignCategoryEconomy: "ЭКОНОМИКА", + campaignCategoryOther: "ДРУГОЕ", + campaignGoalLabel: "Цель по подписям", + campaignDeadlineLabel: "Срок", + campaignTagsLabel: "Теги", + campaignTagsPlaceholder: "Введите теги через запятую", + campaignFilterAll: "ВСЕ", + campaignFilterMine: "МОИ", + campaignFilterSigned: "ПОДПИСАННЫЕ", + campaignFilterRecent: "НЕДАВНИЕ", + campaignFilterTop: "ТОП", + campaignStatusOpen: "ОТКРЫТА", + campaignStatusAchieved: "ДОСТИГНУТА", + campaignStatusUnachieved: "НЕ ДОСТИГНУТА", + campaignStatusClosed: "ЗАКРЫТА", + campaignSearchPlaceholder: "Поиск по кампаниям...", + campaignNoItems: "Кампании не найдены.", + campaignSignaturesLabel: "Подписи", + campaignSignaturesTitle: "Сторонники", + campaignNoSignatures: "Подписей пока нет.", + campaignSignTitle: "Подписать кампанию", + campaignSignPlaceholder: "Почему вы её поддерживаете? (необязательно)", + campaignSign: "Подписать", + campaignSigned: "Вы уже подписали!", + campaignNotSigned: "Вашей подписи ещё не хватает", + campaignElevate: "Передать в Парламент", + campaignElevated: "Передана в Парламент", + campaignUpdatesTitle: "Обновления", + campaignNoUpdates: "Обновлений пока нет.", + campaignUpdatePlaceholder: "Что произошло?", + campaignAddUpdate: "Опубликовать обновление", + campaignQrAlt: "QR для подписи кампании", + campaignQrCaption: "Отсканируйте, чтобы подписать", + typeCampaign: "КАМПАНИЯ", + typeCampaignSignature: "ПОДПИСЬ", + typeCampaignUpdate: "ОБНОВЛЕНИЕ КАМПАНИИ", + statsCampaign: "Кампании", + statsCampaignSignature: "Подписи кампаний", + statsCampaignUpdate: "Обновления кампаний", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "У кампании, за которой вы следите, есть обновление.", + campaignBotAchievedTitle: "Кампания, за которой вы следите, достигла цели.", + campaignBotRaisedTitle: "Кампания, которую вы подписали, вынесена в Парламент.", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "Забронированный вами маршрут изменился.", + logisticsBotClosedTitle: "Забронированный вами маршрут закрыт.", + logisticsBotBookedTitle: "Кто-то забронировал ваш маршрут.", + logisticsBotCancelledTitle: "Бронирование на вашем маршруте отменено.", + logisticsBotConfirmedTitle: "Ваше бронирование подтверждено: посылка в пути.", + logisticsBotRejectedTitle: "Ваше бронирование отклонено.", + logisticsBotDeliveredTitle: "Ваша посылка доставлена.", + campaignLabel: "КАМПАНИЯ", + campaignGenerateReport: "Создать отчёт", + logisticsGenerateReport: "Создать отчёт", + mailingGenerateReport: "Создать отчёт", + campaignUpdateEdited: "Изменено", + mailingMessagesLabel: "Сообщения", + logisticsBookingsLabel: "Брони", + backupTitle: "Резервная копия", + modulesBackupLabel: "Резервная копия", + modulesBackupDescription: "Модуль для экспорта и импорта ключей и создания зашифрованных резервных копий.", + backupDescription: "Модуль для экспорта и импорта ключей и создания зашифрованных резервных копий.", + backupTypeRecovery: "ВОССТАНОВЛЕНИЕ", + backupTypeKeys: "ЭКСПОРТ КЛЮЧЕЙ", + backupTypeFull: "ПОЛНАЯ КОПИЯ", + backupTypeRestore: "ВОССТАНОВИТЬ КОПИЮ", + backupTypeInstant: "МГНОВЕННО", + backupInstantDescription: "Обычный .zip со всем вашим журналом и файлами, без секретного ключа и без пароля. Быстро, но ", + backupInstantRisk: "тот, у кого окажется файл, прочтёт всё.", + backupKitPageDescription: "Набор для восстановления можно открыть только с этого устройства.", + backupKitId: "Oasis ID", + backupKitDate: "Создано", + backupKitQrAlt: "QR-код вашего секретного ключа", + backupKitStep1: "Распечатайте или перепишите и храните в надёжном месте офлайн.", + backupKitStep2: "Для восстановления установите Oasis на новом устройстве и импортируйте этот секрет в разделе Резервная копия › Ключи.", + backupKitStep3: "Уничтожьте эту страницу, если она когда-либо будет раскрыта.", + backupKitPdfButton: "Создать PDF", + backupScopeLabel: "Охват", + backupScopeAll: "ВСЁ", + backupScopeMine: "ТОЛЬКО МОЙ КОНТЕНТ", + backupSinceLabel: "С даты", + backupEstimateTitle: "Оценка", + backupFileInfo: "Зашифрованная копия будет загружена на устройство (имя: oasis-backup-ДАТА.oasisbk)", + backupExportButton: "Скачать копию", + backupRestorePassword: "Введите пароль, использованный при создании копии", + backupRestoreButton: "Восстановить копию", + backupRestoredLine: "Копия восстановлена.", + backupRestoredMessages: "Добавлено сообщений", + backupRestoredSkipped: "Уже были", + backupRestoredBlobs: "Добавлено файлов", + backupRestoredState: "Восстановлено настроек", + backupRestoredFailed: "Ошибок", + backupRestoredForked: "Расходятся", + backupRestoreRunning: "Восстановление копии. Страница обновляется автоматически; тем временем можно продолжать пользоваться Oasis.", + backupRestoreElapsed: "Прошло", + backupRestoreFailedLine: "Не удалось восстановить копию.", + backupRestoreForkMine: "Это устройство уже опубликовало сообщения с вашей идентичностью, которых нет в копии, поэтому применить её поверх них не удалось. Восстанавливайте на чистой установке сразу после импорта ключей, прежде чем делать что-либо ещё.", + verificationTitle: "Проверка", + verificationDescription: "Проверьте цепочку своей ленты, найдите ответвления и просмотрите осиротевшие или отсутствующие файлы.", + verificationRun: "Запустить проверку", + verificationTotalMessages: "сообщений в журнале", + verificationFeed: "Ваша лента", + verificationMessages: "сообщений", + verificationLastSequence: "последняя последовательность", + verificationGaps: "Пропуски", + verificationBrokenLinks: "Разорванные ссылки", + verificationBadHashes: "Неверные хеши", + verificationBadSignatures: "Неверные подписи", + verificationDuplicates: "Дублированные номера", + verificationForks: "Ответвления", + verificationBlobs: "Файлы", + verificationBlobsPresent: "есть", + verificationBlobsReferenced: "упомянуто", + verificationBlobsOrphan: "осиротевших", + verificationBlobsMissing: "отсутствует", + verificationBlobsMissingOwn: "Ваши файлы, отсутствующие на этом устройстве", + verificationBlobsMissingOthers: "Файлы других жителей ещё не загружены", + verificationBlobsUnavailable: "Не удалось получить список файлов на этом устройстве.", + wikiTitle: "Вики", + modulesWikiLabel: "Вики", + modulesWikiDescription: "Модуль для поиска и управления вики.", + wikiDescription: "Модуль для поиска и управления вики.", + wikiCreate: "Создать вики", + wikiUpdate: "Обновить", + wikiDelete: "Удалить", + wikiEdit: "Редактировать", + wikiTitleLabel: "Заголовок", + wikiTitlePlaceholder: "Название вики...", + wikiBodyLabel: "Содержимое", + wikiBodyPlaceholder: "Ссылайтесь на другие вики через [[Название вики]] или [[Название вики|своя подпись]].\n\nУпоминайте жителей через @id.\n\nСтавьте теги #oasis.\n\nВставляйте ссылки вроде https://solarnethub.com\n\nПустая строка начинает новый абзац; одиночные переносы сохраняются.\n\nПрикрепите любой поддерживаемый медиафайл — он встроится сюда.", + formatBarLabel: "Формат", + wikiStatusLabel: "Тип вики", + wikiStatusOpen: "ОТКРЫТА", + wikiStatusClosed: "ЗАКРЫТА", + wikiTagsLabel: "Теги", + wikiTagsPlaceholder: "Введите теги через запятую", + wikiSummaryLabel: "Описание изменения", + wikiSummaryPlaceholder: "Что изменилось?", + wikiFilterAll: "ВСЕ", + wikiFilterMine: "МОИ", + wikiFilterRecent: "НЕДАВНИЕ", + wikiFilterLinked: "СВЯЗАННЫЕ", + wikiChanges: "Последние изменения", + wikiNoPages: "Вики не найдены.", + wikiNoChanges: "Изменений пока нет.", + wikiHistory: "История", + wikiVersion: "Версия", + wikiVersions: "версий", + wikiRestore: "Восстановить", + wikiDiff: "Изменения", + wikiViewVersion: "Смотреть", + wikiLinkedPages: "Связанные страницы", + wikiNoLinkedPages: "Эта страница пока не связана ни с одной другой.", + wikiMissingLink: "Страница ещё не существует", + wikiDuplicateTitle: "Страница с таким названием уже существует. Измените название, чтобы создать другую страницу.", + wikiCreateSectionTitle: "Создать вики", + wikiUpdateSectionTitle: "Обновить вики", + wikiSearchPlaceholder: "Поиск по вики...", + wikiCurrentVersion: "Текущая версия", + wikiViewingVersion: "Вы смотрите старую версию", + wikiBackToPage: "Назад к странице", + wikiLinkedChip: "Связана", + wikiRestoredSummary: "Восстановленная версия", + wikiExportPdf: "Создать страницу", + wikiPreview: "Предпросмотр", + tribeSectionWiki: "ВИКИ", + tribeWikiCreate: "Создать вики", + tribeWikiEmpty: "Вики пока нет.", + typeWiki: "ВИКИ", padsTitle: "Пады", padTitle: "Пад", modulesPadsLabel: "Пады", modulesPadsDescription: "Модуль для управления совместными текстовыми редакторами.", - padFilterAll: "ВСЕ", - padFilterMine: "МОИ", - padFilterRecent: "НЕДАВНИЕ", - padFilterOpen: "ОТКРЫТЫЕ", - padFilterClosed: "ЗАКРЫТЫЕ", padCreate: "Создать Пад", - padUpdate: "Обновить Пад", - padDelete: "Удалить Пад", + padUpdate: "Обновить", + padDelete: "Удалить", padTitleLabel: "Заголовок", padTitlePlaceholder: "Введите заголовок пада...", padStatusLabel: "Статус", + padTypeLabel: "Тип пада", padStatusOpen: "ОТКРЫТ", padStatusInviteOnly: "ТОЛЬКО ПО ПРИГЛАШЕНИЮ", padStatusClosed: "ЗАКРЫТ", padDeadlineLabel: "Срок", padTagsLabel: "Теги", - padTagsPlaceholder: "тег1, тег2, ...", + padTagsPlaceholder: "Введите теги через запятую", padMembersLabel: "Участники", - padVisitPad: "Посетить Пад", - padShareUrl: "Поделиться URL", padCreated: "Создано", - padAuthor: "Автор", - padGenerateCode: "Сгенерировать Код", - padInviteCodeLabel: "Код Приглашения", - padInviteCodePlaceholder: "Введите код приглашения...", - padValidateInvite: "Проверить", - padStartEditing: "НАЧАТЬ РЕДАКТИРОВАНИЕ!", + padStartEditing: "Редактировать", padEditorPlaceholder: "Начните писать...", padSubmitEntry: "Отправить", padNoEntries: "Записей пока нет.", padAllSectionTitle: "Все Пады", padMineSectionTitle: "Мои Пады", padsDescription: "Управляйте совместными зашифрованными текстовыми редакторами в вашей сети.", + roomsTitle: "Комнаты", + roomsDescription: "Находите зашифрованные комнаты видеоконференций в вашей сети и присоединяйтесь к ним.", + modulesRoomsLabel: "Комнаты", + modulesRoomsDescription: "Модуль для поиска зашифрованных комнат видеоконференций и входа в них.", + roomFilterRecent: "НЕДАВНИЕ", + roomFilterMine: "МОИ", + roomFilterAll: "ВСЕ", + roomFilterLive: "В ЭФИРЕ", + roomFilterOpen: "ОТКРЫТЫЕ", + roomFilterInvite: "ТОЛЬКО ПО ПРИГЛАШЕНИЮ", + roomFilterClosed: "ЗАКРЫТЫЕ", + roomCreate: "Создать комнату", + roomUpdate: "Обновить", + roomDelete: "Удалить", + roomClose: "Закрыть", + roomReopen: "Открыть снова", + roomCreateSectionTitle: "Создать комнату", + roomUpdateSectionTitle: "Обновить комнату", + roomTitlePlaceholder: "Название комнаты", + roomDescriptionLabel: "Описание", + roomDescriptionPlaceholder: "О чём вы будете говорить?", + roomTypeLabel: "Тип комнаты", + roomTagsLabel: "Теги", + roomStatusOpen: "ОТКРЫТА", + roomStatusInviteOnly: "ТОЛЬКО ПО ПРИГЛАШЕНИЮ", + roomStatusClosed: "ЗАКРЫТА", + roomParticipants: "Участники", + roomJoin: "Войти", + roomLeave: "Выйти", + roomFull: "Комната заполнена", + roomLiveChip: "В ЭФИРЕ", + roomMuted: "микрофон выключен", + roomRec: "Записать аудио", + roomRecStop: "Остановить запись", + roomStreamFile: "Транслировать", + roomStreamStop: "Остановить", + roomStreaming: "Идёт трансляция", + liveFullscreen: "Во весь экран", + roomStreamPickTitle: "Выберите видео", + roomStreamPickHint: "Видео на этом устройстве. Выбранное транслируется прямо с места хранения, без копирования и публикации.", + roomStreamGo: "Показать в комнате", + roomStreamNotFound: "Этого видео нет в ваших папках.", + roomPublicWarning: "Эта комната транслируется в открытом интернете: любой может видеть и слышать, что в ней происходит.", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} вошёл в комнату", + roomEventLeft: "{name} вышел из комнаты", + roomEventRecStart: "{name} начал запись", + roomEventRecStop: "{name} остановил запись", + roomEventMuted: "{name} выключил микрофон", + roomEventUnmuted: "{name} включил микрофон", + roomEventHand: "{name} поднял руку", + roomRaiseHand: "Поднять руку", + roomLowerHand: "Опустить руку", + confirmActionTitle: "Подтвердите это действие", + confirmActionButton: "Подтвердить", + confirmJoinText: "Вы собираетесь присоединиться по этому приглашению.", + confirmFollowText: "Вы собираетесь поддержать этого жителя.", + confirmSignText: "Вы собираетесь подписать эту кампанию.", + confirmDownloadText: "Вы собираетесь скачать этот файл на своё устройство.", + roomRecordings: "Записи", + roomClearNotices: "Очистить", + roomLineSwitchboard: "Коммутатор", + roomLineDndError: "Номер этой комнаты не принимает звонки.", + roomNumberHint: "Наберите этот номер в Телефоне, чтобы войти в комнату.", + liveRefresh: "Обновить", + roomNobody: "Сейчас в комнате никого нет.", + roomSomeone: "Сейчас в комнате: {n}.", + roomYou: "вы", + roomYouAreIn: "Вы внутри", + roomNobodyInside: "Сейчас в этой комнате никого нет.", + roomErrorFull: "Комната заполнена. Вы сможете войти, когда кто-нибудь выйдет.", + roomErrorUnreachable: "Сейчас до комнаты не достучаться. Попробуйте позже.", + roomErrorBusy: "Вы уже в звонке или в другой комнате.", + roomErrorClosed: "Эта комната закрыта.", + roomAccessDenied: "Эта комната только по приглашению. Чтобы войти, нужно приглашение.", + roomsNoItems: "Комнаты не найдены.", + roomSearchPlaceholder: "Искать комнаты...", + roomInviteMode: "Приглашение", + tribeSectionRooms: "КОМНАТЫ", + tribeRoomCreate: "Создать комнату", + tribeRoomsEmpty: "Пока нет комнат.", + typeRoom: "КОМНАТЫ", + typeRoomMember: "УЧАСТНИК КОМНАТЫ", + statsRoom: "Комнаты", + statsRoomMember: "Участники комнат", + favoritesFilterRooms: "КОМНАТЫ", + invitesRoomsTitle: "Комнаты", + inviteCodeInvalid: "Этот код приглашения недействителен или уже использован.", + inviteAlreadyMember: "Вы уже участвуете. Откройте его в соответствующем модуле.", + actionFailed: "Не удалось это сделать. Попробуйте ещё раз.", + publishTooShort: "Ваша публикация слишком короткая.", + walletSendFailed: "Не удалось отправить платёж: {reason}", + pixeliaCoordsError: "Эти координаты находятся за пределами сетки.", + courtsErrTitle: "Нужен заголовок.", + courtsErrRespondent: "Укажите, против кого дело.", + courtsErrRespondentId: "Это недействительный Oasis ID.", + courtsErrMethod: "Выберите, как будет решено дело.", + courtsErrCaseNotFound: "Это дело не найдено.", + courtsErrBrief: "Напишите свой ответ.", + courtsErrStance: "Выберите свою позицию.", + courtsErrResult: "Укажите результат.", + courtsErrTerms: "Укажите условия соглашения.", + courtsErrMediator: "Добавьте хотя бы одного посредника.", + courtsErrSelfMediator: "Вы не можете быть посредником в своём деле.", + courtsErrJudge: "Выберите судью.", + courtsErrSelfJudge: "Вы не можете быть судьёй в своём деле.", + courtsErrVisibility: "Выберите, кто может видеть дело.", + courtsErrNomination: "Эта номинация не найдена.", + opinionAlreadyGiven: "Вы уже высказали своё мнение.", + cipherPasswordShort: "Пароль должен содержать не менее 32 символов.", + errorForbidden: "У вас нет доступа к этому.", + errorNotFound: "Не найдено.", + errorClosed: "Это закрыто.", + errorInviteOnly: "Только по приглашению. Нужно приглашение.", + errorTooLong: "Этот текст слишком длинный.", + errorRequired: "Не хватает обязательных данных.", + errorInvalid: "Часть данных недействительна.", + errorNotAuthor: "Это может сделать только автор.", + panicRemoved: "Ваш блокчейн удалён.", padRecentSectionTitle: "Недавние Пады", padOpenSectionTitle: "Открытые Пады", padClosedSectionTitle: "Закрытые Пады", padCreateSectionTitle: "Создать Новый Пад", padUpdateSectionTitle: "Обновить Пад", - padInviteGenerated: "Код Приглашения Сгенерирован", typePad: "ПАДЫ", padNew: "НОВЫЙ", padAddFavorite: "Добавить в Избранное", padRemoveFavorite: "Удалить из Избранного", - padClose: "Закрыть Pad", + padClose: "Закрыть", padBackToEditor: "Назад к редактору", - padSearchPlaceholder: "Поиск падов...", + padSearchPlaceholder: "Поиск в Пады...", modulesChatsLabel: "Чаты", - modulesChatsDescription: "Модуль для обнаружения и управления зашифрованными чатами.", + modulesChatsDescription: "Модуль для поиска и управления чатами.", typeChat: "ЧАТЫ", typeChatMessage: "СООБЩЕНИЕ ЧАТА", - chatLabel: "ЧАТЫ", - chatMessageLabel: "СООБЩЕНИЯ ЧАТА", chatsTitle: "Чаты", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "Ваши чаты", + chatRecentTitle: "Недавние чаты", chatFavoritesTitle: "Избранное", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "Открытые чаты", + chatClosedTitle: "Закрытые чаты", chatDescription: "Описание", chatCategory: "Категория", chatStatus: "СТАТУС", - chatFilterAll: "ВСЕ", - chatFilterMine: "МОИ", - chatFilterRecent: "НЕДАВНИЕ", - chatFilterFavorites: "ИЗБРАННОЕ", - chatFilterOpen: "ОТКРЫТЫЕ", - chatFilterClosed: "ЗАКРЫТЫЕ", chatCreate: "Создать Чат", - chatUpdate: "Обновить Чат", - chatDelete: "Удалить Чат", + chatUpdate: "Обновить", + chatDelete: "Удалить", chatClose: "Закрыть Чат", chatVisitChat: "ПЕРЕЙТИ В ЧАТ", chatUntitled: "Чат без названия", chatNoItems: "Чаты не найдены.", chatParticipants: "Участники", - chatStartChatting: "НАЧАТЬ ОБЩЕНИЕ!", - chatGenerateCode: "Создать Код", - chatShareUrl: "Поделиться URL", + chatMessagesLabel: "Сообщения", + chatThreadsLabel: "Темы", chatCreatedAt: "СОЗДАН", - chatSearchPlaceholder: "Поиск чатов...", + chatSearchPlaceholder: "Поиск в Чаты...", chatStatusOpen: "ОТКРЫТЫЙ", chatStatusInviteOnly: "ТОЛЬКО ПО ПРИГЛАШЕНИЮ", chatStatusClosed: "ЗАКРЫТЫЙ", chatSendMessage: "Отправить", + chatJumpLatest: "Последнее сообщение", + chatReply: "Ответить", + chatReplyCancel: "Отменить ответ", + chatPin: "Закрепить", + chatUnpin: "Открепить", + chatPinned: "Закреплённые", + chatPickChat: "Выберите чат, чтобы открыть его", + chatNoneYet: "Пока нет доступных чатов.", + activitySearchPlaceholder: "Поиск в Активность...", + trendingSearchPlaceholder: "Поиск в В тренде...", + opinionsSearchPlaceholder: "Поиск в Мнения...", + votesSearchPlaceholder: "Поиск в Голосования...", + gamesSearchPlaceholder: "Поиск в Игры...", + schoolTitle: "Школа", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "Вики-страница, за которой вы следите, была изменена.", + wikiBotRestoredTitle: "Восстановлена более ранняя версия вики-страницы, за которой вы следите.", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "Новый студент записался на ваш курс.", + schoolBotInvitedTitle: "Вас пригласили на курс.", + schoolBotAdmittedTitle: "Вас приняли на курс.", + schoolBotCertificateTitle: "Вы получили сертификат.", + schoolBotPassedTitle: "Студент успешно прошёл ваш курс.", + schoolBotLessonTitle: "Опубликован новый урок.", + invitesSchoolTitle: "Школа", + schoolEnrollLabel: "Присоединиться к курсу", + schoolExamRetryLater: "Повторить можно через 24 часа.", + schoolApproved: "СДАНО", + schoolExamPassMark: "Проходной балл (0-10)", + schoolExamPassed: "СДАН", + schoolExamPending: "Ожидается", + schoolExamFailed: "НЕ СДАН", + schoolExamAddQuestion: "Добавить вопрос", + schoolExamCreateQuestion: "Создать вопрос", + schoolExamOption: "Вариант", + schoolExamCorrect: "Правильный вариант", + schoolExamPoints: "Баллы", + schoolMaterialTextPlaceholder: "Заметки к этому уроку...", + schoolMaterials: "Материалы", + schoolAddMaterial: "Добавить материал", + schoolNoMaterials: "Материалов пока нет.", + bankYourSchoolBalance: "Ваш доход от школы", + schoolProgress: "Прогресс", + schoolProgressMark: "Отметить как пройденный", + schoolProgressDone: "Пройден", + schoolExams: "Экзамены", + schoolExamCreate: "Создать экзамен", + schoolExamTitlePlaceholder: "Название экзамена", + schoolExamHelp: "Один вопрос в строке: Вопрос? ; вариант ; правильный вариант* ; вариант", + schoolExamQuestionsPlaceholder: "Что такое SSB? ; База данных ; P2P-протокол* ; Браузер", + schoolExamSubmit: "Отправить ответы", + schoolExamScore: "Ваш результат", + schoolNoScoresYet: "Результатов пока нет.", + schoolLessonUnit: "Раздел / фаза", + schoolLessonUnitPlaceholder: "напр. Фаза 1", + schoolLessonOrder: "Порядок", + profileClearnetSchoolLabel: "Школа", + schoolStartDate: "Дата начала", + schoolSessionDate: "Дата занятия", + schoolCourseChat: "Чат курса", + agendaFilterSchool: "ШКОЛА", + agendaFilterCampaigns: "КАМПАНИИ", + agendaFilterLogistics: "ЛОГИСТИКА", + schoolCourseType: "Тип курса", + schoolTypeOpen: "ОТКРЫТЫЙ", + schoolTypePaid: "ПЛАТНЫЙ", + schoolTypeInvite: "ТОЛЬКО ПО ПРИГЛАШЕНИЮ", + schoolLessonLocked: "Урок зашифрован — запросите ключ курса у преподавателя.", + schoolGrantAccess: "Отправить ключ курса", + schoolKeySent: "Ключ отправлен", + schoolOngoing: "Идёт", + schoolFilterApplied: "ЗАПИСАН", + schoolFilterOpen: "ОТКРЫТЫЕ", + schoolFilterFavorites: "ИЗБРАННОЕ", + schoolPendingPayment: "Ожидается оплата", + schoolPendingStudents: "Ожидающие записи", + schoolViewBill: "Посмотреть счёт", + schoolPaymentNote: "Запись создаёт счёт в ECO (перевод). Запись подтверждается после подписания перевода.", + schoolCloseCourse: "Закрыть курс", + schoolReopenCourse: "Открыть курс снова", + schoolDescription: "Преподавайте и изучайте курсы в вашей сети.", + schoolFilterAll: "ВСЕ", + schoolFilterMine: "МОИ", + schoolFilterTop: "ТОП", + schoolFilterRecent: "НЕДАВНИЕ", + schoolCreateButton: "Создать Курс", + schoolCreateSectionTitle: "Создать курс", + schoolUpdateSectionTitle: "Обновить курс", + schoolCourseTitlePlaceholder: "Название вашего курса", + schoolCourseDescriptionPlaceholder: "Чему вы будете учить на этом курсе?", + schoolCourseImage: "Изображение курса (макс. размер: 75MB)", + schoolTags: "Теги", + schoolTagsPlaceholder: "Введите теги через запятую", + schoolPrice: "Цена (ECO)", + schoolVisibility: "Видимость", + schoolVisibilityPublic: "Публичный", + schoolVisibilityInvite: "По приглашению", + schoolStatus: "Статус", + schoolClosed: "Закрыт", + schoolFree: "Бесплатно", + schoolCreate: "Создать курс", + schoolUpdate: "Обновить курс", + schoolSearchPlaceholder: "Поиск в Школа...", + schoolSortTitle: "Название", + schoolNoCourses: "Курсы не найдены.", + schoolStudents: "Студенты", + schoolTeacher: "Преподаватель", + schoolCreatedAt: "СОЗДАН", + schoolLessons: "Уроки", + schoolNoLessons: "Уроков пока нет.", + schoolEnrollToSee: "Запишитесь, чтобы увидеть уроки этого курса.", + schoolEnrollButton: "ЗАПИСАТЬСЯ", + schoolUnenrollButton: "Покинуть курс", + schoolInviteRequired: "Этот курс по приглашению.", + schoolTeacherPanel: "Панель преподавателя", + schoolStudentPanel: "Панель студента", + schoolAddLesson: "Создать урок", + schoolLessonTitlePlaceholder: "Название урока", + schoolLessonTextPlaceholder: "Опишите, что изучается в этом уроке...", + schoolInviteStudents: "Пригласить студентов", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "ПРИГЛАСИТЬ", + schoolInvited: "Приглашённые", + schoolIssueCertificate: "Создать сертификат", + schoolCertified: "Сертифицирован", + schoolCertificateTextPlaceholder: "Примечание к сертификату (необязательно)", + schoolLessonOpen: "Открыть урок", + schoolCertificatePdf: "PDF", + schoolCertificates: "Сертификаты", + schoolNoCertificates: "Сертификаты ещё не выданы.", + schoolEditButton: "Редактировать", + schoolDeleteButton: "Удалить", + typeSchool: "ШКОЛА", + schoolCourseLabel: "КУРС", + emergencyLabel: "ЧС", + wikiPageLabel: "ВИКИ", + statsSchoolCourse: "Курсы", + statsSchoolLesson: "Уроки", + statsSchoolEnroll: "Записи", + statsSchoolCertificate: "Сертификаты школы", + modulesSchoolLabel: "Школа", + modulesSchoolDescription: "Модуль для обмена знаниями.", + welcomeStepUxAction: "Сохранить вид", + welcomeStepWorkflowTitle: "Как вы собираетесь использовать OASIS?", + welcomeStepWorkflowText: "В Oasis много модулей. Выберите подходящую вам настройку, и включёнными останутся только полезные. Её можно изменить в любой момент в разделе «Модули».", + welcomeStepWorkflowAction: "Сохранить настройку", + welcomeWorkflow_full: "По умолчанию", + chatRateLimitTitle: "Слишком много сообщений", + chatRateLimitMessage: "Вы достигли лимита сообщений этой комнаты за час. Попробуйте позже.", chatMessagePlaceholder: "Введите сообщение...", chatNoMessages: "Сообщений пока нет.", - chatLeave: "Покинуть Чат", - chatInviteCodeLabel: "Введите код приглашения", - chatJoinByInvite: "Войти", chatTitlePlaceholder: "Название чата", chatDescriptionPlaceholder: "Описание чата", - chatTagsPlaceholder: "тег1, тег2, тег3", + chatTagsPlaceholder: "Введите теги через запятую", chatImageLabel: "Выбрать файл изображения (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "Код Приглашения", chatAuthor: "Автор", - chatCreated: "Создан", chatAddFavorite: "Добавить в Избранное", chatRemoveFavorite: "Убрать из Избранного", chatPM: "ЛС", chatStatusLabel: "Статус", + chatTypeLabel: "Тип чата", chatCategoryLabel: "Категория", - chatParticipantsLabel: "Участники", gamesTitle: "Игры", gamesDescription: "Откройте и играйте в игры.", gamesFilterAll: "ВСЕ", gamesPlayButton: "ИГРАТЬ!", - gamesBackToGames: "Назад к играм", modulesGamesLabel: "Игры", modulesGamesDescription: "Модуль для открытия и игры в игры.", modulesGraphosLabel: "Графос", @@ -3324,8 +4176,6 @@ module.exports = { gamesArkanoidDesc: "Разбейте все кирпичи своей ракеткой и мячом. Классическая аркада.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Классический пинг-понг против ИИ. Первый до 5 очков побеждает.", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "Гонка со временем! Уворачивайтесь от машин и доберитесь до финиша вовремя.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Пилотируйте корабль через астероидное поле. Уничтожайте их до столкновения.", gamesRockPaperScissorsTitle: "Камень Ножницы Бумага", @@ -3335,68 +4185,57 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "Подбросьте монету и угадайте орёл или решку. Насколько вы удачливы?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "Бильярд с видом сверху. Щёлкните, чтобы прицелиться, удерживайте, чтобы набрать силу. Забейте все шары за наименьшее число ударов.", + gamesArtilleryTitle: "Артиллерия", + gamesArtilleryDesc: "Наведите пушку, учтите ветер и поразите цель. 5 раундов, побеждает тот, кто сделал меньше выстрелов.", + gamesLabyrinthTitle: "Лабиринт", + gamesLabyrinthDesc: "Выберитесь из лабиринта, пока не закончились ходы. Каждый уровень больше и сложнее.", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "Съешьте все точки и избегайте призраков. Призраки двигаются в реальном времени — не останавливайтесь!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesAudioPendulumTitle: "Звуковой маятник", + gamesAudioPendulumDesc: "Симулятор хаотической физики с синтезом звука в реальном времени. Угловые скорости превращаются в частоты, пики — в удары барабана. Ни одна симуляция не звучит одинаково.", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "Сводите визуалы вживую на двух деках: выберите узор для каждой, настройте скорость, цвет и масштаб, переходите между ними и пускайте вспышки в такт. Отбейте темп или доверьте пульс микрофону.", + gamesTetrisDesc: "Классические падающие блоки. Заполняйте линии, чтобы набирать очки. Сколько продержитесь?", + gamesMinesweeperTitle: "Сапёр", + gamesMinesweeperDesc: "Классическая охота на мины. Очистите поле, чтобы перейти на новый уровень — больше поле, больше мин, выше счёт.", + gamesFilterScoring: "ОЧКИ", + gamesHallOfFame: "Зал славы", + gamesHallPlayer: "Игрок", + gamesHallScore: "Счёт", + gamesNoScores: "Пока нет результатов.", calendarsTitle: "Календари", calendarTitle: "Календарь", modulesCalendarsLabel: "Календари", modulesCalendarsDescription: "Модуль для просмотра и управления календарями.", typeCalendar: "КАЛЕНДАРИ", - calendarFilterAll: "ВСЕ", - calendarFilterMine: "МОИ", - calendarFilterOpen: "ОТКРЫТЫЕ", - calendarFilterClosed: "ЗАКРЫТЫЕ", - calendarFilterRecent: "НЕДАВНИЕ", - calendarFilterFavorites: "ИЗБРАННОЕ", calendarCreate: "Создать календарь", calendarUpdate: "Обновить", - calendarDelete: "Удалить", calendarTitleLabel: "Название", calendarTitlePlaceholder: "Название календаря...", calendarStatusLabel: "Статус", + calendarTypeLabel: "Тип календаря", calendarStatusOpen: "ОТКРЫТЫЙ", calendarStatusClosed: "ЗАКРЫТЫЙ", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "Срок", calendarTagsLabel: "Теги", - calendarTagsPlaceholder: "тег1, тег2...", + calendarTagsPlaceholder: "Введите теги через запятую", calendarParticipantsLabel: "Участники", - calendarParticipantsCount: "Участники", - calendarVisitCalendar: "Открыть календарь", calendarCreated: "Создан", - calendarAuthor: "Автор", calendarJoin: "Присоединиться", calendarGenerateInvite: "Создать приглашение", - calendarInviteCodePlaceholder: "Введите код приглашения...", - calendarValidateInvite: "Проверить код", - calendarJoined: "Участник", - calendarAddDate: "Добавить дату", - calendarAddNote: "Добавить заметку", calendarDateLabel: "Дата", calendarDatePlaceholder: "Опишите эту дату...", - calendarNoteLabel: "Заметка", + calendarNoteLabel: "Заметки", calendarNotePlaceholder: "Добавить заметку...", calendarFirstDateLabel: "Дата", calendarFirstNoteLabel: "Заметки", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "Интервал", + calendarIntervalNone: "Без повторения", + calendarIntervalWeekly: "Еженедельно", + calendarIntervalMonthly: "Ежемесячно", + calendarIntervalYearly: "Ежегодно", calendarFormDescription: "Описание", calendarNoDates: "Даты не добавлены.", calendarNoNotes: "Заметок нет.", @@ -3404,8 +4243,8 @@ module.exports = { calendarsDescription: "Находите и управляйте календарями в вашей сети.", calendarMonthPrev: "← Назад", calendarMonthNext: "Вперёд →", - calendarMonthLabel: "Даты", - calendarsShareUrl: "Поделиться URL", + calendarMonthNames: "Январь,Февраль,Март,Апрель,Май,Июнь,Июль,Август,Сентябрь,Октябрь,Ноябрь,Декабрь", + calendarDayInitials: "Пн,Вт,Ср,Чт,Пт,Сб,Вс", calendarAllSectionTitle: "Календари", calendarRecentSectionTitle: "Недавние календари", calendarFavoritesSectionTitle: "Избранное", @@ -3415,11 +4254,10 @@ module.exports = { calendarCreateSectionTitle: "Создать новый календарь", calendarUpdateSectionTitle: "Обновить календарь", calendarAddFavorite: "Добавить в избранное", - calendarDeleteNote: "Delete", + calendarDeleteNote: "Удалить", calendarRemoveFavorite: "Убрать из избранного", - calendarSearchPlaceholder: "Поиск календарей...", + calendarSearchPlaceholder: "Поиск в Календарь...", calendarAddEntry: "Добавить запись", - calendarLeave: "Покинуть календарь", statsCalendar: "Календари", statsCalendarDate: "Даты календаря", statsCalendarNote: "Заметки календаря", @@ -3430,6 +4268,146 @@ module.exports = { tribeContentAccessDenied: "Доступ Запрещён", tribeContentAccessDeniedMsg: "Этот контент принадлежит племени. Вы должны быть участником, чтобы получить к нему доступ.", tribeViewTribes: "Посмотреть Племена", + tribeFilesEmpty: "Торрентов пока нет.", + filesLabel: "Торренты", + profileClearnetFilesLabel: "Торренты", + filesTitle: "Торренты", + filesDescription: "Исследуйте и управляйте торрентами в вашей сети.", + fileAllSectionTitle: "Торренты", + fileMineSectionTitle: "Ваши Торренты", + fileRecentSectionTitle: "Недавние Торренты", + fileTopSectionTitle: "Лучшие Торренты", + fileFavoritesSectionTitle: "Избранное", + fileFilterAll: "ВСЕ", + fileFilterMine: "МОИ", + fileFilterRecent: "НЕДАВНИЕ", + fileFilterTop: "ЛУЧШИЕ", + fileFilterFavorites: "ИЗБРАННОЕ", + fileCreateSectionTitle: "Загрузить Торрент", + fileUpdateSectionTitle: "Обновить Торрент", + fileCreateButton: "Загрузить Торрент", + fileUpdateButton: "Обновить", + fileAddFavoriteButton: "Добавить в Избранное", + fileRemoveFavoriteButton: "Убрать из Избранного", + fileFileLabel: "Выберите файл (макс: 75MB)", + calendarFilterRecent: "НЕДАВНИЕ", + calendarFilterAll: "ВСЕ", + calendarFilterMine: "МОИ", + calendarFilterFavorites: "ИЗБРАННОЕ", + calendarFilterOpen: "ОТКРЫТЫЕ", + calendarFilterClosed: "ЗАКРЫТЫЕ", + padFilterRecent: "НЕДАВНИЕ", + padFilterAll: "ВСЕ", + padFilterMine: "МОИ", + padFilterOpen: "ОТКРЫТЫЕ", + padFilterClosed: "ЗАКРЫТЫЕ", + chatFilterRecent: "НЕДАВНИЕ", + chatFilterAll: "ВСЕ", + chatFilterMine: "МОИ", + chatFilterFavorites: "ИЗБРАННОЕ", + chatFilterOpen: "ОТКРЫТЫЕ", + chatFilterClosed: "ЗАКРЫТЫЕ", + licenseLabel: "Лицензия", + phoneTitle: "Телефон", + phoneDescription: "Управляйте своими видеозвонками со сквозным шифрованием.", + phoneUnavailable: "Звонки недоступны на этом устройстве.", + phoneCallButton: "Позвонить", + profileVisibilityPhone: "Номер телефона", + phoneNumberUnknown: "Ни у кого в вашей сети нет номера {number}.", + phoneNumberChoose: "Проверьте, кому вы хотите позвонить по номеру {number}:", + phoneAnswer: "Ответить", + phoneSpeaker: "Динамик", + phoneEarpiece: "Наушник", + phoneReject: "Отклонить", + phoneHangup: "Завершить", + phoneMute: "Выключить микрофон", + phoneUnmute: "Включить микрофон", + phoneCameraOn: "Камера вкл", + phoneCameraOff: "Камера выкл", + settingsPhoneVideo: "Видео", + settingsPhoneVideoOn: "Разрешено в звонках и комнатах", + settingsPhoneVideoOff: "Только звук", + phoneSilence: "Не слышать", + phoneUnsilence: "Слышать снова", + phoneCalling: "Вызов:", + phoneIsCalling: "звонит вам", + notifyNewPmFrom: "Новое ЛС от", + notifyNewPamFrom: "Новое PaM от", + notifyUpdateLabel: "Обновление", + updateInstalledLabel: "Установленная версия", + updateAvailableLabel: "Доступная версия", + updateChangesLabel: "Изменения", + updateFilesLabel: "Файлы", + updateOriginLabel: "Источник", + phoneRecording: "Запись PaM для", + phoneLeaveMessage: "Никто не ответил: оставьте сообщение после сигнала.", + phoneLeaveMessageBusy: "Абонент разговаривает: оставьте сообщение после сигнала.", + phoneLeaveMessageUnreachable: "Не удалось связаться: оставьте сообщение после сигнала.", + phoneConnecting: "Соединение с", + phoneSendPam: "Отправить PaM", + phoneCancel: "Отмена", + phoneSearchPlaceholder: "Поиск в Телефоне...", + phonePamTitle: "Аудиозаписи", + phoneNew: "Новое", + phoneDelete: "Удалить", + phoneNoPams: "У вас нет аудиозаписей.", + phoneHistoryTitle: "История", + phoneLatestTitle: "Последние звонки", + phoneCreateButton: "Новый звонок", + phoneLimitsHint: "До {n} жителей на звонок; если больше одного — это совместный звонок.", + phoneErrorPub: "Пабам звонить нельзя.", + phoneJointCall: "Совместный звонок", + phoneWith: "С кем", + phoneResult: "Результат", + phoneWhen: "Когда", + phoneDuration: "Длительность", + phoneOutgoing: "Исходящий", + phoneIncoming: "Входящий", + phoneNoHistory: "Записей пока нет.", + phoneOutcomeEnded: "Принят", + phoneOutcomeMissed: "Пропущен", + phoneOutcomeRejected: "Отклонён", + phoneOutcomeNoanswer: "Нет ответа", + phoneOutcomeCancelled: "Отменён", + phoneOutcomePam: "PaM отправлен", + phoneOutcomeFailed: "Не удалось", + phoneDialAdd: "Добавить", + phoneDialBack: "Стереть", + phoneDialClear: "Очистить", + phoneDialPlaceholder: "Номер или Oasis ID", + phoneDialInhabitants: "Жители", + phoneDialRemove: "Убрать", + phoneOwnNumber: "Это ваш собственный номер.", + phoneSettingsDesc: "Выберите, кто может вам звонить.", + phoneDnd: "Не беспокоить", + phoneOpen: "Открыто", + phoneRecipientRefuses: "Житель, которому вы пытаетесь позвонить, не принимает звонки.", + phoneRefusedNotice: "Эти жители не принимают звонки, им не звонили:", + fileEmptyError: "Файл пустой. Выберите другой файл.", + fileTitleLabel: "Название", + fileTitlePlaceholder: "Название", + fileDescriptionLabel: "Описание", + fileDescriptionPlaceholder: "Описание", + fileTagsLabel: "Теги", + fileTagsPlaceholder: "Введите теги через запятую", + fileSizeLabel: "Размер", + fileDownloadButton: "СКАЧАТЬ", + fileDetailsButton: "ПОДРОБНОСТИ", + fileNoFile: "Нет торрент-файла", + fileSearchPlaceholder: "Поиск в Торренты...", + fileSearchButton: "Поиск", + fileSortRecent: "Самые новые", + fileSortOldest: "Самые старые", + fileSortTop: "Самые популярные", + fileNoMatch: "Совпадений не найдено.", + fileUpdatedAt: "Обновлено", + typeFile: "ТОРРЕНТЫ", + modulesFilesLabel: "Файлы", + modulesFilesDescription: "Модуль для открытия и управления аудио.", + tribeSectionFiles: "ТОРРЕНТЫ", + fileDescription: "Описание", + fileDownload: "Скачать", + fileTypeLabel: "Тип", torrentsTitle: "Торренты", torrentsDescription: "Исследуйте и управляйте торрентами в вашей сети.", torrentAllSectionTitle: "Торренты", @@ -3457,47 +4435,91 @@ module.exports = { torrentTagsLabel: "Теги", torrentTagsPlaceholder: "Введите теги через запятую", torrentSizeLabel: "Размер", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "Создать торрент", + torrentFromContentMissing: "Файл ещё недоступен на этом узле. Попробуйте позже.", + torrentDownloads: "Загрузки", + torrentFetchOasis: "Скачать в Oasis", + torrentSaveFile: "Сохранить файл", + torrentSeeds: "сиды", + seedsLabel: "Сиды", + seedAction: "Поддержать", + torrentSourceFile: "Файл", + torrentStateQueued: "В очереди", + torrentStateProbing: "Поиск сидов", + torrentStateDownloading: "Загрузка", + torrentStateDone: "Завершено", + torrentStateFailed: "Ошибка", + torrentStateNoSource: "Нет подключённых сидов, ожидание", + torrentStateCancelled: "Отменено", + torrentNoDownloads: "Загрузок пока нет.", + torrentOriginalSection: "Оригинал", + torrentCancel: "Отмена", + torrentDownloadButton: "СКАЧАТЬ", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "ПОДРОБНОСТИ", torrentNoFile: "Нет торрент-файла", noTorrents: "Торренты не найдены.", - torrentSearchPlaceholder: "Поиск по названию, тегам, автору...", + noFiles: "Торренты не найдены.", + torrentSearchPlaceholder: "Поиск в Торренты...", torrentSearchButton: "Поиск", torrentSortRecent: "Самые новые", torrentSortOldest: "Самые старые", torrentSortTop: "Самые популярные", torrentNoMatch: "Совпадений не найдено.", - torrentMessageAuthorButton: "Написать Автору", torrentUpdatedAt: "Обновлено", statsTorrent: "Торренты", + statsFile: "Файлы", typeTorrent: "ТОРРЕНТЫ", modulesTorrentsLabel: "Торренты", modulesTorrentsDescription: "Модуль для поиска и управления торрентами.", favoritesFilterTorrents: "ТОРРЕНТЫ", + favoritesFilterFiles: "ФАЙЛЫ", favoritesFilterMarket: "РЫНОК", + favoritesFilterEvents: "СОБЫТИЯ", + favoritesFilterForum: "ФОРУМ", + favoritesFilterHousing: "ЖИЛЬЁ", + favoritesFilterJobs: "ВАКАНСИИ", + favoritesFilterProjects: "ПРОЕКТЫ", + favoritesFilterReports: "ОТЧЁТЫ", + favoritesFilterTasks: "ЗАДАЧИ", + favoritesFilterTransfers: "ПЕРЕВОДЫ", + favoritesFilterVotes: "ГОЛОСОВАНИЯ", favoritesFilterShopProducts: "ТОВАРЫ МАГАЗИНА", tribeSectionTorrents: "ТОРРЕНТЫ", tribeCreateTorrent: "Загрузить Торрент", - tribeMediaTypeTorrent: "Торрент", + tribeCreateFile: "Загрузить файл", settingsWishTitle: "Желание", - settingsWishDesc: "Настройте желание вашего Аватара.", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "Определяет, как вы получаете доступ ко всему контенту и ваш опыт в сети Oasis.", + settingsWishWhole: "Мультивселенная", + settingsWishMutuals: "Только взаимная поддержка", + settingsWishOnlyLan: "Только LAN", + settingsWishLocal: "Локально", settingsPmVisibilityTitle: "Личные сообщения", settingsPmVisibilityDesc: "Настройте уровень открытости личных сообщений.", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "Мультивселенная", + settingsPmVisibilityMutuals: "Только взаимная поддержка", + mute: "Заглушить", + unmute: "Снять заглушение", + relationshipMuted: "Заглушён вами", + hideContent: "Скрыть для меня", + unhideContent: "Показать снова", + hiddenByYou: "Скрыто вами", + settingsTabContent: "Контент", + settingsHiddenTitle: "Скрытый контент", + settingsHiddenDesc: "Заглушённые вами жители и скрытый вами контент.", + settingsMutedLabel: "Заглушённые жители", + settingsHiddenLabel: "Скрытый контент", + settingsMutedEmpty: "Никто не заглушён.", + settingsHiddenEmpty: "Ничего не скрыто.", + settingsPmVisibilityFollowing: "Только те, на кого я подписан", + settingsMentionsVisibilityTitle: "Упоминания", + settingsMentionsVisibilityDesc: "Выберите, чьи упоминания доходят до вас.", pmMutualNotice: "Исходящие — защита UX. Входящие — фильтр внимания.", - pmBlockedNonMutual: "Личные сообщения можно отправлять только жителям со взаимной поддержкой.", inhabitantsPendingFollowsTitle: "Ожидающие запросы на поддержку", inhabitantsPendingAccept: "Принять", inhabitantsPendingReject: "Отклонить", - bxEncrypted: "ЗАШИФРОВАНО", - bxEncryptedHexLabel: "Шифротекст (превью)", tribeSectionGovernance: "УПРАВЛЕНИЕ", - tribeSubStatusPublic: "ПУБЛИЧНАЯ", - tribeSubStatusPrivate: "ЧАСТНАЯ", - tribeSubInheritedPrivate: "ЧАСТНАЯ (унаследовано от главного племени)", tribePadInviteRequired: "Нет доступа к паду. Запросите приглашение.", tribeChatInviteRequired: "Нет доступа к чату. Запросите приглашение.", tribeGovernanceDesc: "Внутреннее управление этим племенем.", @@ -3536,209 +4558,195 @@ module.exports = { tribeGovNoRevocations: "Пока нет ни одного отзыва", tribeGovNoHistorical: "Пока нет ни одной записи", logsTitle: "Журнал", - logsDescription: "Записывайте свой опыт в сети.", - logsReadMore: "Читать далее", + logsDescription: "Ведите личный дневник ваших впечатлений.", logsViewTitle: "Журнал", logsColumnType: "Тип", logsView: "Смотреть", - logsManualPrompt: "Напишите ваш журнал", logsUpdateButton: "Обновить", - logsEditTitle: "Редактировать запись", - logsCreateDescription: "Записывайте свой опыт...", + logsEditTitle: "Обновить запись", logsCreateTitle: "Создать запись", logsWriteButton: "Написать", logsTextPlaceholder: "Опишите ваши впечатления...", - logsTextField: "Текст", - logsLabelPlaceholder: "Метка...", - logsLabelField: "Напишите ваш журнал (метка)", - logsAiDisabledWarn: "Включите модуль ИИ в /modules для записей от ИИ.", - logsAiContextValue: "День блокчейна", - logsAiContext: "Контекст", - logsAiModStatus: "AImod", - logsModeApply: "Применить!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "ИИ-ассистент", logsModeManual: "Вручную", logsModeAI: "ИИ", - logsColumnDelete: "Удалить", - logsColumnEdit: "Редактировать", logsColumnLog: "Журнал", logsColumnDate: "Дата", logsDelete: "Удалить", - logsEdit: "Редактировать", + logsEdit: "Обновить", logsFilterAlways: "ВСЕГДА", logsFilterYear: "ГОД", logsFilterMonth: "МЕСЯЦ", logsFilterWeek: "НЕДЕЛЯ", logsFilterToday: "СЕГОДНЯ", - logsFilterRecent: "НЕДАВНИЕ", - logsFilterAll: "ВСЕ", + logsComments: "Комментарии", + logsAuthorEntries: "Записи", logsCreate: "Создать запись", logsExport: "Экспорт журнала", - logsExportOne: "Экспорт", + logsExportOne: "Экспортировать запись", logsViewDetails: "Подробнее", logsGenerateButton: "Сгенерировать текст", - logsSearchText: "Искать в журналах...", + logsSearchText: "Поиск в Журнал...", logsSearchAnyType: "Любой тип", logsSearchButton: "Искать", logsEmpty: "Записей пока нет.", + logsNothingNew: "Нет новых действий для записи.", modulesLogsLabel: "Журнал", - modulesLogsDescription: "Модуль для записи (с помощью ИИ-ассистента) ваших впечатлений.", + modulesLogsDescription: "Модуль для ведения личного дневника ваших впечатлений.", statsLogsTitle: "Журнал", - statsLogsEntries: "Записи", typeLog: "ЖУРНАЛ", - blockchainCycle: "Цикл", larpTitle: "L.A.R.P.", larpDescription: "Слой ролевой игры в реальном времени для совместных экспериментов.", + larpNoHousesMatch: "Нет домов, соответствующих запросу.", + larpSearchPlaceholder: "Поиск в L.A.R.P....", larpAllHouses: "Дома:", larpFilterRuling: "ПРАВИТ", larpFilterHouses: "ДОМА", - larpFilterRules: "FAQ", + larpFilterRules: "ЧАВО", larpRulesTitle: "L.A.R.P. FAQ", - larpRulesEntryTitle: "Стартовый дом", larpRulesEntryText: "Каждый житель по умолчанию начинает в ACADEMIA. Из ACADEMIA выберите дом в панели «Вступить в дом»: это откроет вступительный тест. Ответьте на 10 вопросов и отправьте. Если вы преодолеете порог (7/10), вы будете автоматически приняты в этот дом; иначе вас отклонят, и вы останетесь в ACADEMIA. В любом случае система записывает ваш OasisID и цикл попытки и не даёт повторить попытку до окончания периода ожидания в 30 циклов.", - larpRulesLeaveText: "Участники любого дома могут вернуться в ACADEMIA в любое время со страницы своего дома; это сбрасывает их членство, но не отменяет ожидание теста.", - larpRulesGovernanceTitle: "Стена управления", larpRulesGovernanceText: "В свой цикл правящий дом носит значок «Правит» и является единственным, чья Стена показывается публично под вкладкой ПРАВИТ.", - larpRulesSignTitle: "Эмблема L.A.R.P.", + larpRulesWallText: "У каждого дома есть Стена, где пишут его участники. Стена дома закрыта, кроме стены правящего дома и стены ACADEMIA, которые может читать любой.", larpRulesSignText: "Жители могут включить (в /profile/edit > Сенсоры) показ изображения своего текущего дома как знака в профиле, на карточке автора и жителя. Знак ведёт на страницу дома.", larpPostsTitle: "Стена", larpPostsEmpty: "Пока нет публикаций.", - larpPostLabel: "Новая публикация", larpPostPlaceholder: "Что должен сказать этот дом?", - larpPostPublic: "Публичная (видно всем, иначе только участникам)", larpPostSubmit: "Опубликовать", - larpPostMembersOnly: "Только участники", larpCycleLabel: "Цикл:", - audioBackToBcs: "Back to BCS", + audioBackToBcs: "Назад к BCS", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", - audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", - audioTranscodeDetailTitle: "Transcode", - audioTranscodeStegoEmpty: "(none)", - audioTranscodeStegoMessage: "TEXT", - audioTranscodeStegoNotFound: "No steganographic payload could be decoded from this audio.", - audioTranscodeStegoOasisId: "By", - audioTranscodeStegoTimestamp: "Generated at", + audioTranscodeButton: "ПЕРЕКОДИРОВАТЬ", + audioTranscodeCompositionEmpty: "Это аудио не содержит сохранённой композиции блокчейна.", + audioTranscodeDetailDescription: "Декодировать встроенную полезную нагрузку и исходную карту композиции блокчейна.", + audioTranscodeDetailTitle: "Перекодирование", + audioTranscodeStegoEmpty: "(нет)", + audioTranscodeStegoMessage: "ТЕКСТ", + audioTranscodeStegoNotFound: "Не удалось декодировать стеганографическую нагрузку из этого аудио.", + audioTranscodeStegoTimestamp: "Создано", audioTranscodeStegoUnknown: "—", - bankExchangeEcoTaxAnnual: "ECOin Taxes (annual)", - bankExchangeEcoTaxMonthly: "ECOin Taxes (monthly)", - bankOverviewYourTaxes: "Your taxes", - bankRulesAlpha: "Alpha (max share of pub balance per epoch)", + bankExchangeEcoTaxAnnual: "Налоги ECOin (в год)", + bankExchangeEcoTaxMonthly: "Налоги ECOin (в месяц)", + bankOverviewYourTaxes: "Итого налоги", + bankRulesAlpha: "Alpha (макс. доля баланса паба за эпоху)", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", - bankRulesArchTaxRate: "Rate", - bankRulesArchTaxTitle: "ARCH Tax", - bankRulesCapPerEpoch: "Cap per epoch (absolute)", - bankRulesCapUser: "Cap per user per epoch", - bankRulesCarbonFactor: "Carbon factor", + bankRulesArchTaxNote: "ARCH-налог отражает долгосрочную стоимость роста и поддержки архива сети: чем старше ваши данные в сети, тем больше ваша доля расходов на обслуживание.", + bankRulesArchTaxRate: "Ставка", + bankRulesArchTaxTitle: "ARCH-налог", + bankRulesCapPerEpoch: "Лимит на эпоху (абсолютный)", + bankRulesCapUser: "Лимит на пользователя за эпоху", + bankRulesCarbonFactor: "Углеродный коэффициент", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", - bankRulesChipTitle: "ECO Tax chip color band", + bankRulesChipNote: "Метка каждого элемента окрашивается относительно самого большого сообщения, когда-либо замеченного в сети, с поправкой на число активных жителей — более крупная сеть распределяет нагрузку и переводит больше элементов в зелёную зону.", + bankRulesChipTitle: "Цветовая шкала метки ECO-налога", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", - bankRulesEcoTaxRate: "Rate", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesEpochKind: "Epoch granularity", - bankRulesFloor: "Floor per user (gross)", - bankRulesGraceDays: "Claim grace period", + bankRulesEcoTaxNote: "ECO-налог вычитается из валового ББД при расчёте распределения эпохи. Вычтенные ECO не перераспределяются в этой эпохе — они остаются на балансе паба и пополняют фонд следующей эпохи через ограничения reserveMin / alpha.", + bankRulesEcoTaxRate: "Ставка", + bankRulesEcoTaxTitle: "ECO-налог", + bankRulesEpochKind: "Гранулярность эпохи", + bankRulesFloor: "Минимум на пользователя (брутто)", + bankRulesGraceDays: "Льготный период для заявки", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", - bankRulesKarmaTitle: "Karma (user engagement score)", + bankRulesKarmaNote: "Карма вычисляется по вашим действиям (публикации, голоса, участие в парламенте/судах и т. д.) с коэффициентом затухания во времени, за вычетом граммов углерода, созданных вашей лентой. То, что получает ваш контент, тоже учитывается: положительные мнения, голоса, реакции и прослушивания добавляют карму, а отрицательные мнения или голоса отнимают её, когда несколько жителей согласны.", + bankRulesKarmaTitle: "Карма (оценка вовлечённости)", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", - bankRulesPoolTitle: "Monthly pool", - bankRulesReserveMin: "Reserve minimum (kept in pub balance)", + bankRulesPoolTitle: "Ежемесячный фонд", + bankRulesReserveMin: "Минимальный резерв (остаётся на балансе паба)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", - bankRulesShareTitle: "Per-user share", + bankRulesShareTitle: "Доля на пользователя", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankRulesWMax: "Maximum weight (w_max)", - bankRulesWMin: "Minimum weight (w_min)", - bankTaxes: "Taxes", - bankTaxesAnnualEcoin: "ECO Tax (annual)", - bankTaxesArchTaxAnnual: "ARCH Tax (annual)", - bankTaxesArchTaxFirstBlock: "Your first block age", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxLifetime: "ARCH Tax (lifetime)", - bankTaxesArchTaxMonthly: "ARCH Tax (monthly)", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxRate: "Rate", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesArchTaxUserAmount: "Your ARCH tax (price to return)", - bankTaxesEcoTaxLifetime: "ECO Tax (lifetime)", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesLookupAuthor: "Author", - bankTaxesLookupButton: "Inspect", - bankTaxesLookupCarbon: "Carbon footprint", - bankTaxesLookupEcoin: "ECO Tax", - bankTaxesLookupNotFound: "No block found in your local feed with that ID.", - bankTaxesLookupPlaceholder: "Block ID (e.g. %abc...=.sha256)", - bankTaxesLookupSize: "Size", - bankTaxesLookupTitle: "Inspect a block", - bankTaxesLookupType: "Type", - bankTaxesMonthlyEcoin: "ECO Tax (monthly)", - bankTaxesRate: "Rate", - bankTaxesSpan: "Sampling span", - bankTaxesTotalAnnual: "Total (annual)", - bankTaxesTotalBlocks: "Total blocks", - bankTaxesTotalBytes: "Total bytes", - bankTaxesTotalCarbon: "Total carbon", - bankTaxesTotalEcoin: "Total ECO tax (lifetime)", - bankTaxesTotalLifetime: "Total (lifetime)", - bankTaxesTotalMonthly: "Total (monthly)", - bankTaxesTypesApply: "Set my taxes", - bankTaxesTypesLabel: "Select which taxes you want to pay", - bankTaxesUserAmount: "Your ECO tax (price to return)", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", - blockchainBackToBlockexplorer: "Back to blockexplorer", - blockchainBlockNotAvailable: "This block is not available in your local feed. It may belong to a peer you are not yet replicating from.", - ecoTaxLabel: "ECO Tax", - gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - melodyAllEmpty: "No BCS compositions from other inhabitants yet.", - melodyByLabel: "By", - melodyNoteUnavailable: "block not in your local feed", - melodyUploadBcsButton: "Publish", - melodyUploadBcsNameNote: "The audio will be published with the auto-generated name", - melodyUploadBcsTitle: "Publish", - melodyUploadStegoLabel: "Hidden message (steganography)", - melodyUploadStegoMaxLabel: "max 280 characters", - melodyUploadStegoPlaceholder: "Optional.", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", - statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", - statsPad: "Pads", - statsPadEntry: "Pad entries", - statsTombstoneEmpty: "No tombstones in the network yet.", - typeGameScore: "GAME SCORES", + bankRulesWMax: "Максимальный вес (w_max)", + bankRulesWMin: "Минимальный вес (w_min)", + bankTaxes: "Налоги", + bankTaxesAnnualEcoin: "ECO-налог (в год)", + bankTaxesArchTaxAnnual: "ARCH-налог (в год)", + bankTaxesArchTaxFirstBlock: "Возраст вашего первого блока", + bankTaxesArchTaxFootprintNote: "ARCH-налог отражает стоимость роста и поддержки архива сети. Он рассчитывается по промежутку времени между вашим первым опубликованным блоком и самым новым блоком в сети — чем дольше ваша история живёт в архиве, тем больше ваша доля расходов на обслуживание.", + bankTaxesArchTaxLifetime: "ARCH-налог (за всё время)", + bankTaxesArchTaxMonthly: "ARCH-налог (в месяц)", + bankTaxesArchTaxNoteBold: "прямому снижению ARCH-налога", + bankTaxesArchTaxNoteIntro: "Ваш ARCH-налог вычитается из излишка, который сеть создаёт сверх вашего ББД; базовое значение ББД установлено как незыблемый минимум. Налог никогда не вычитается из фиксированной суммы, положенной каждому жителю в качестве ББД. Вычтенные средства питают алгоритм перераспределения богатства и возвращаются другим жителям через их заявки на ББД. У новых жителей и тех, кто специально занимается архивированием, репликацией и обслуживанием, скорее всего, будет задача поддерживать архив сети в порядке от вашего имени. Или они могут посвятить себя ", + bankTaxesArchTaxNoteOutro: " с помощью соглашений об обрезке, дедупликации избыточных данных и общей инфраструктуры обслуживания, а их постоянный вклад может потребоваться по консенсусу сети.", + bankTaxesArchTaxRate: "Ставка", + bankTaxesArchTaxTitle: "ARCH-налог", + bankTaxesArchTaxUserAmount: "Ваш ARCH-налог (сумма к возврату)", + bankTaxesEcoTaxLifetime: "ECO-налог (за всё время)", + bankTaxesEcoTaxTitle: "ECO-налог", + bankTaxesLookupAuthor: "Автор", + bankTaxesLookupButton: "Проверить", + bankTaxesLookupCarbon: "Углеродный след", + bankTaxesLookupEcoin: "ECO-налог", + bankTaxesLookupNotFound: "В вашей локальной ленте не найден блок с таким ID.", + bankTaxesLookupPlaceholder: "ID блока (напр. %abc...=.sha256)", + bankTaxesLookupSize: "Размер", + bankTaxesLookupTitle: "Проверить блок", + bankTaxesLookupType: "Тип", + bankTaxesMonthlyEcoin: "ECO-налог (в месяц)", + bankTaxesRate: "Ставка", + bankTaxesSpan: "Период выборки", + bankTaxesTotalAnnual: "Итого (в год)", + bankTaxesTotalBlocks: "Всего блоков", + bankTaxesTotalBytes: "Всего байт", + bankTaxesTotalCarbon: "Всего углерода", + bankTaxesTotalEcoin: "Итого ECO-налог (за всё время)", + bankTaxesTotalLifetime: "Итого (за всё время)", + bankTaxesTotalMonthly: "Итого (в месяц)", + bankTaxesTypesApply: "Установить мои налоги", + bankTaxesTypesLabel: "Выберите, какие налоги вы хотите платить", + bankTaxesUserAmount: "Ваш ECO-налог (сумма к возврату)", + bankTaxesUserNoteChips: "Метка ECO-налога рядом с каждым элементом меняет цвет (зелёный / жёлтый / красный) в зависимости от размера элемента относительно самого большого блока, когда-либо виденного в сети, с поправкой на число активных жителей — более крупная солнечно-панковская сеть распределяет нагрузку и переводит больше элементов в зелёную зону.", + bankTaxesUserNoteOtherParams: "Сейчас ECO-налог рассчитывается по углеродному следу каждого блока, но со временем может учитывать и другие параметры — энергию, затраченную на репликацию, избыточное хранение у узлов, пропускную способность для блобов, вычислительную стоимость расшифровки, майнинговый след связанных транзакций или любую другую измеримую нагрузку, которую сеть договорится учитывать.", + blockchainBackToBlockexplorer: "Назад к обозревателю блоков", + blockchainBlockNotAvailable: "Этот блок недоступен в вашей локальной ленте. Возможно, он принадлежит узлу, с которого вы ещё не реплицируете.", + ecoTaxLabel: "ECO-налог", + gamesHallDate: "Дата", + gamesNewRecord: "Новый рекорд!", + indexingRefreshNote: "Страница обновляется каждые 10 секунд.", + indexingMessage: "Oasis синхронизирует распределённую сеть узлов. Пожалуйста, подождите.", + indexingTitle: "Синхронизация", + melodyAllEmpty: "Пока нет BCS-композиций других жителей.", + melodyNoteUnavailable: "блока нет в вашей локальной ленте", + melodyUploadBcsButton: "Опубликовать", + melodyUploadBcsNameNote: "Аудио будет опубликовано с автоматически созданным именем", + melodyUploadBcsTitle: "Опубликовать", + melodyUploadStegoLabel: "Скрытое сообщение (стеганография)", + melodyUploadStegoMaxLabel: "не более 280 символов", + melodyUploadStegoPlaceholder: "Необязательно.", + padsNoItems: "Пады не найдены.", + padVersionHistory: "История версий", + padAttributionView: "По авторам", + padVersionView: "Просмотр", + statsChat: "Чаты", + statsChatMessage: "Сообщения чата", + statsGameScore: "Игры", + statsWiki: "Вики-страницы", + statsPad: "Пады", + statsPadEntry: "Записи падов", + statsTombstoneEmpty: "В сети пока нет удалений (tombstones).", + typeGameScore: "ИГРОВЫЕ ОЧКИ", bankTaxesLookupNote: "Введите ID блока, чтобы найти его в вашем локальном канале.", bankTaxesUserNoteChipsClick: "Нажмите на любой чип, чтобы проверить его блок в blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "Код приглашения", larpRulesInviteEntryText: "Участники домов могут создавать коды приглашения, дающие прямой доступ к дому.", larpRulesOneHouseText: "Вы можете быть участником не более одного дома в любой момент. Если ни в одном — вы в ACADEMIA. Каждая страница дома (кроме ACADEMIA) показывает участникам кнопку «Покинуть Дом», которая сбрасывает членство к ACADEMIA. Выход НЕ отменяет ожидание теста.", - larpRulesOneHouseTitle: "По одному дому за раз", - larpRulesTestEntry: "WILL-тест", - larpRulesTestEntryText: "Каждый вариант взвешивает один или несколько домов; при отправке система суммирует баллы и автоматически принимает вас в дом с наибольшим количеством баллов.", + larpRulesTestEntryText: "В тесте WILL каждый вариант даёт вес одному или нескольким домам; при отправке система суммирует баллы и автоматически принимает вас в дом с наибольшим счётом.", larpWillTestHint: "После завершения вам будет назначен дом, который лучше всего соответствует вашим ответам. Ваши индивидуальные ответы обрабатываются локально и никогда не сохраняются — в ваш канал публикуется только результат назначения дома.", larpWillTestTitle: "WILL-тест", - settingsLanDesc: "Периодически анонсировать этот узел другим экземплярам Oasis в той же локальной сети. Отключите, чтобы остановить UDP-вещание.", + settingsLanDesc: "Периодически анонсировать этот узел другим экземплярам Oasis в той же локальной сети.", settingsLanEnable: "Включить LAN-вещание", settingsLanTitle: "LAN-вещание", settingsReplicationTitle: "Репликация", settingsReplicationDesc: "Настройте количество переходов, которые ваш узел делает от вашего собственного канала при репликации содержимого.", settingsReplicationHopsLabel: "Переходы", + settingsBlobCacheTitle: "Кэш медиа", + settingsBlobCacheDesc: "Контент других жителей хранится на этом устройстве. Выберите, сколько места он может занимать.", + settingsBlobCacheUsage: "Текущее использование", + settingsBlobCacheFiles: "файлов", + settingsBlobCacheLimit: "Лимит", + settingsBlobCacheUnlimited: "Без ограничения", + settingsBlobCacheCleanNow: "Очистить сейчас", + settingsBlobCacheCleaned: "Удалено файлов: {n}, освобождено {size}.", aiApproveTagsLabel: "Теги (через запятую)", aiApproveTagsPlaceholder: "напр. oasis, управление, экология", aiApproveRatingLabel: "Оценка", @@ -3749,31 +4757,25 @@ module.exports = { aiExchangeMarkHelpful: "+1 полезно", larpCyclesUntilRuling: "Следующий цикл управления", larpVisitTribe: "Посетить племя", + larpStartJourney: "Начать путь", larpGoverning: "Правит:", + larpStartHere: "Начните здесь:", larpMyHouse: "Мой дом:", larpMembersCount: "Участники", - larpMembersTitle: "Участники", - larpNoMembers: "Пока нет участников.", larpRolesLabel: "Роли", larpFunctionLabel: "Функция", larpMonthLabel: "Цикл управления", larpLeaveToAcademia: "Вернуться в ACADEMIA", - larpAcademiaJoinTitle: "Вступить в дом", - larpAcademiaJoinHint: "Пройдите психологический тест профиля, чтобы вас отнесли к дому, который больше всего вам подходит, или используйте код приглашения, которым поделился участник одного из домов.", - larpTakeTestButton: "Пройти тест профиля", + larpAcademiaJoinTitle: "Дома L.A.R.P.", larpInviteRedeem: "Вступить в Дом", larpInviteCreate: "Создать код приглашения", larpInvitePlaceholder: "Код приглашения", larpInviteBannerTitle: "Новый код приглашения", larpInviteBannerHint: "Поделитесь этим кодом с кем-то в ACADEMIA. Срок действия — 30 циклов или одно использование.", - larpTestAssignedHouse: "Назначенный дом", larpTestRankingTitle: "Очки по домам", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "Код приглашения дома", - invitesHouseJoinButton: "Вступить в Дом", larpLastAttemptTitle: "Ваша последняя попытка", larpLastAttemptHouse: "Дом", - larpLastAttemptResult: "Результат", larpLastAttemptWhen: "Когда", larpLastAttemptCooldown: "Следующая попытка через", larpTestTitle: "Вступительный тест", @@ -3782,37 +4784,14 @@ module.exports = { larpTestCooldownActive: "Следующий тест доступен через", larpTestCooldownDays: "циклов", larpTestResultTitle: "Результат теста", - larpTestPassed: "Тест пройден — добро пожаловать!", - larpTestFailed: "Тест не пройден", larpTestScore: "Очки", larpTestNextAttempt: "Вы сможете пройти ещё один тест через 30 циклов.", larpGoToHouse: "Перейти в свой дом", larpBackToAcademia: "Назад в ACADEMIA", - larpBackToHouses: "◀ Дома", larpBadgeYou: "Вы", larpBadgeRuling: "Правит", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Модуль слоя ролевой игры в реальном времени Oasis (9 Домов).", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - ecoinTaxLabel: "ECOin Tax", - bankTaxesNetworkTitle: "Network Taxes", - bankTaxesUserTitle: "Your taxes", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - statsEcoTaxTitle: "ECO Tax", - audioTranscodeTitle: "BCS Transcode", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - audioTranscodeStegoTitle: "Embedded in the waveform", - audioBackToAudio: "Back to audio", - audioAuthorLabel: "Author", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "Модуль для слоя живой ролевой игры.", larpProfileQ1: "Столкнувшись со сложной проблемой, что вы делаете в первую очередь?", larpProfileQ1O1: "Говорить с другими, чтобы найти консенсус", larpProfileQ1O2: "Создать прототип для проверки", @@ -3886,7 +4865,52 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "Выберите режим UX-навигации для вашего интерфейса.", uxModeMenus: "Блоки", - uxModeAINav: "AI", + uxModeChats: "Беседы", + uxModeFeed: "Микроблогинг", + feedTrendingTitle: "Популярные теги", + feedActiveTitle: "Активные жители", + cnBrandSub: "Свободная · P2P · Федеративная", + cnSyncedPeers: "Синхронизированные узлы", + cnLicense: "Лицензия", + cnNotAccessible: "Контент сейчас недоступен.", + cnUntitled: "Без названия", + cnSearchPlaceholder: "Поиск…", + cnHubTitle: "Clearnet HUB", + cnHubDescription: "Публичный контент, которым делятся жители этой сети Oasis.", + cnHubEmpty: "В Clearnet пока не опубликовано публичного контента.", + cnPublicContent: "Контент", + cnTribesContent: "Племена", + cnNetworkContent: "Сеть", + cnCategoryEmpty: "В этой категории нет контента.", + cnInhabitantEmpty: "Этот житель пока не публиковал контент в Clearnet.", + cnPrevPage: "← Назад", + cnNextPage: "Далее →", + cnPageOf: "Страница {page} из {pages}", + cnMoreInfo: "Подробнее →", + cnOpenCourse: "Открытый курс", + cnAuction: "АУКЦИОН", + cnExchange: "ОБМЕН", + cnRssDescription: "опубликовано жителями этого HUB", + cnKindAudio: "Аудио", + cnKindBlog: "Блог", + cnKindBookmark: "Закладка", + cnKindDocument: "Документ", + cnKindEvent: "Событие", + cnKindFeed: "Лента", + cnKindImage: "Изображение", + cnKindJob: "Вакансия", + cnKindMarket: "Рынок", + cnKindPodcast: "Подкаст", + cnKindProject: "Проект", + cnKindCourse: "Курс", + cnKindShop: "Магазин", + cnKindTorrent: "Торрент", + cnKindFile: "Файл", + cnKindVideo: "Видео", + cnKindWiki: "Вики", + cnKindRoom: "Комната", + cnKindMap: "Карта", + uxModeAINav: "ИИ", } }; diff --git a/src/client/assets/translations/oasis_zh.js b/src/client/assets/translations/oasis_zh.js index 18367fd..e030108 100644 --- a/src/client/assets/translations/oasis_zh.js +++ b/src/client/assets/translations/oasis_zh.js @@ -1,27 +1,6 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { zh: { - bbAdd: "添加", - bbQuickActions: "快捷操作", - bbPickTitle: "固定一个模块", - bbPickDesc: "为底部栏的 + 位置选择一个模块。", - bbPickNone: "无", - bbManage: "编辑", - bbYourBar: "你的工具栏", - bbRemove: "移除", - bbFull: "已达上限", - bbDone: "完成", - activityChangeFilter: "更改筛选", - emptyConnectHint: "连接到一个 pub 以与网络同步。", - emptyConnectCta: "连接到 pub", - menuCommunity: "社区", - activityGroupCommunity: "社区与治理", - activityGroupOrg: "组织", - activityGroupComm: "沟通", - activityGroupEconomy: "经济", - activityGroupMedia: "内容与媒体", - activityGroupOther: "其他", - languageName: "中文", shopOrderStatusPaid: "已收款", shopOrderStatusReceived: "已收到", shopOrderMarkPaid: "标记已收款", @@ -30,22 +9,18 @@ module.exports = { shopPurchasesButton: "购买记录", shopMyOrdersDescription: "你的购买订单。", shopMyOrdersEmpty: "你还没有购买记录。", - shopOrderPmSeller: "PM Seller", + shopOrderPmSeller: "PM 卖家", shopOrderSeller: "卖家", - shopOrderPaymentConcept: "付款", shopOrderInvoice: "发票", shopOrderInvoiceConcept: "发票", shopOrderStatusPending: "待处理", shopOrderStatusAccepted: "已接受", shopOrderStatusRejected: "已拒绝", shopOrderStatusShipped: "已发货", - shopOrderStatusDelivered: "已送达", shopOrderAccept: "接受", shopOrderReject: "拒绝", shopOrderMarkShipped: "标记为已发货", - shopOrderMarkDelivered: "标记为已送达", - shopOrderPmBuyer: "PM Buyer", - shopOrderGenerateContract: "智能合约", + shopOrderPmBuyer: "PM 买家", shopOrderContractConcept: "商店订单", shopOrdersPending: "待处理订单", all: "全部", @@ -75,18 +50,17 @@ module.exports = { viewJson: "查看 JSON", matchScore: "匹配度", preferencesLabel: "偏好", - inhabitantProfileTitle: "居民资料", + inhabitantProfileTitle: "居民", contentWarningLabel: "内容警告", invalidPost: "无效内容", invalidPostHint: "此消息文本无效或为空。", - spreadBy: "由", spreadChron: "传播", spreaded: "已传播", - spreadMore: "更多", spreadContentUnavailable: "内容尚不可用(等待同步)", filteredByTag: "按标签筛选", filteredByTagTitle: "按标签筛选", feedPublishedSuccess: "动态发布成功!", + feedMastodonFailed: "已在 Oasis 发布,但无法发送到 Mastodon。", tribeFeedSent: "消息发送成功!", tribeContentEncrypted: "加密内容", tribeTorrentsEmpty: "暂无种子。", @@ -100,13 +74,10 @@ module.exports = { noSeverity: "无严重程度", calendarDeleteDate: "删除日期", calendarIntervalUntil: "直到", - bookmarkCategory: "分类", bookmarkLastVisit: "最近访问", profileClearnetBookmarksLabel: "书签", - forumFilterHot: "热门", invitesPort: "端口", - currentlyUnrecheable: "错误!", - peerHostValidation: "有效的 IPv4(如 192.168.1.100)或主机名(如 pub.example.com)", + peerHostValidation: "有效的 IPv4(如 192.168.1.100)或主机名(如 pub.solarnethub.com)", peerPortValidation: "端口 1-65535", peerKeyValidation: "SSB ed25519 公钥 (@<44 字符 base64>=.ed25519)", inboxJobUnsubscribedTitle: "取消订阅你的招聘", @@ -133,7 +104,7 @@ module.exports = { taskDescriptionPlaceholder: "任务描述", exportPasswordLabel: "导出密码", importPasswordPlaceholder: "输入密码", - searchCasesPlaceholder: "搜索案件…", + searchCasesPlaceholder: "在法庭中搜索...", shopBuyDeliveryAddressPlaceholder: "收货地址", shopBuyNotesPlaceholder: "备注", bankTaxesUserNoteBold: "直接降低 ECO 税", @@ -154,17 +125,18 @@ module.exports = { courtsSupportCase: "支持该案", courtsSettlementAcceptBtn: "接受和解", courtsSupportCount: "支持者", - courtsVerdictVoteTitle: "对裁决投票", - courtsVerdictVoteLabel: "裁决投票", - courtsVerdictVoteAccept: "接受", - courtsVerdictVoteReject: "拒绝", - courtsVerdictVoteSubmit: "提交投票", reportsSetStatus: "设置状态", reportsStatusClosed: "已关闭", statsAvgPerInhabitant: "人均", statsCarbonMaxAnnual: "年度最大估计", statsCarbonNetwork: "网络总计", statsCarbonOfEstMax: "占估计最大容量", + statsCarbonYearProgress: "年度已过", + statsNetworkTitle: "网络", + statsStorageTitle: "存储", + statsEcoTaxTitle: "ECO 税", + statsAccountTitle: "账户", + statsAveragesTitle: "平均值", statsCarbonOfNetwork: "占网络总计", statsCarbonUser: "你的足迹", statsContentCountColumn: "数量", @@ -176,10 +148,6 @@ module.exports = { statsTopTagsTitle: "热门标签", statsTopTypesTitle: "热门内容类型", statsTotalMsgs: "消息总数", - feedViewDetails: "查看详情", - eventFileLabel: "附加图片", - taskFileLabel: "附加图片", - courtsRespondentRequired: "需要被告ID", extended: "多元宇宙", extendedDescription: [ "当你支持某人时,你可能会下载他们所支持的居民的帖子,这些帖子会显示在这里,按时间倒序排列。", @@ -215,47 +183,76 @@ module.exports = { ], profile: "头像", inhabitants: "居民", - peersReplicatedFeeds: "复制的 Feed", + peersReplicatedFeeds: "已复制", + peersTorColumn: "TOR", + peersTorYes: "可通过 Tor 访问", + peersTorNo: "无法通过 Tor 访问", + peersOasisId: "ID", + peersOasisVersion: "版本", graphos: "Graphos", graphosDescription: "你周围网络的互动地图。", + graphosFederation: "联邦", + graphosFedPubs: "PUB", + graphosFedUnreachable: "无法访问", + graphosFedLinks: "双向链接", + graphosFedIsolated: "孤立", + graphosFedNoData: "无数据", + graphosFedMutual: "双向", + graphosFedOneWay: "单向", + graphosFedTransport: "网络", + graphosFedWith: "联邦对象", + graphosZoom: "放大", + graphosFedInhabitants: "居民", + graphosFedClearnet: "互联网", + graphosFedBoth: "互联网 + TOR", + graphosFedEmpty: "你的节点还不知道任何 PUB。", graphosViewingGraphOf: "正在查看", graphosBackToMine: "← 我的网络", graphosShowing: "显示", graphosYou: "你", graphosTotalNodes: "节点总数", - manualMode: "手动模式", mentions: "提及", mentionsDescription: [ - strong("@提及你的帖子"), - ",按时间倒序排列。", + strong("每一条 @提到你的消息"), + "。", ], - nextPage: "下一页", - previousPage: "上一页", noMentions: "你还没有收到@提及。", + mentionsSearchPlaceholder: "在提及中搜索...", privateReminders: "提醒", + inboxFilterPms: "私信", + inboxFilterNotifications: "通知", + inboxMarkRead: "标为已读", + inboxMarkUnread: "标为未读", + inboxUnreadChip: "未读", + inboxMarkAllRead: "全部标为已读", + inboxDeleteShown: "全部删除", + inboxArchive: "归档", + inboxUnarchive: "取消归档", + inboxFilterArchived: "已归档", + inboxEarlierMessages: "条较早的消息", + inboxSortThread: "按会话", + inboxSortRecent: "最新优先", + inboxAllBots: "机器人", + settingsInboxBotsTitle: "收件箱通知", + settingsInboxBotsDesc: "选择哪些机器人可以在收件箱中通知你。未勾选的机器人将被隐藏,其通知不计入计数。", inboxFiltersLabel: "筛选:", inboxToggleToMutuals: "切换为相互关注", inboxToggleToWhole: "切换为所有人", - privateFrom: "发件人", - privateTo: "收件人", privateDate: "日期", pmReply: "回复", pmReplies: "回复", - pmNew: "新消息", - pmMarkRead: "标记为已读", inReplyTo: "回复", + forumInThread: "主题内", pmPreview: "预览", pmPreviewTitle: "消息预览", peers: "节点", searchDescription: "描述", - imageSearch: "图片搜索", searchFromLabel: "从", searchToLabel: "到", - searchMinOpinionsLabel: "最少意见", - searchInhabitantLabel: "居民 (Oasis ID)", searchQueryLabel: "查询", - searchPerPageLabel: "每页结果数", + searchPerPageLabel: "结果", settings: "设置", + settingsDescription: "设置你的网络环境。", continueReading: "继续阅读", moreComments: "更多评论", readThread: "阅读剩余讨论串", @@ -265,68 +262,47 @@ module.exports = { melodyDescription: '播放你区块链的旋律 — 每个区块都会变成一个音符。', melodyEmpty: '还没有音符 — 你的区块链尚未产生任何区块。', melodyCompositionTitle: '作曲', - melodyCompositionHelp: '区块链中的每个区块都会根据其类型和大小变成一个音符。', - melodyStatsTitle: '类型分布', - melodyInhabitantLabel: '居民', melodyTotalBlocks: '音符', - melodyType: '类型', - melodyCount: '区块', melodyFilterMine: '我的', melodyFilterAll: '全部', - melodyFilterShare: '上传旋律', - melodyShareTitle: '分享你的旋律', - melodyShareHelp: '发布你当前旋律的快照,让别人收听和重新混音。', - melodyShareTitleLabel: '标题(可选)', - melodyShareTitlePlaceholder: '区块链幽灵', - melodyShareSubmit: '发布旋律', - melodyNoShared: '还没有区块链旋律。', melodyRegenerate: '重新生成', melodyDownload: "下载 BCS", - profileActivityTitle: '活动', - profileActivityMore: '查看所有活动', profileContentSectionTitle: '头像内容', profileContentHelp: '选择在你的头像中显示哪些模块。', profileSensorsHelp: '头像中显示的可选指标。', profileClearnetHelp: '可以从 Oasis 外部访问的模块。', profileHubAll: '全部', - profileHubTitle: '公开内容', - footerPulseTitle: '脉搏', - footerPulsePeers: '节点', - footerPulseInbox: '收件箱', - footerPulseSync: '上次同步', - footerPulseEcoValue: 'ECO/小时', - footerPulseLastActivity: '最近活动', footerLicenseLabel: '许可证', - profileSwitchToOasis: '返回 Oasis', - profileSwitchToClearnet: '进入 Clearnet', - profileVisibilityFediverse: "联邦宇宙", - profileSwitchToFediverse: "潜入联邦宇宙", - coordLabel: '坐标(例如 A3)', - coordPlaceholder: '输入坐标', + profileVisibilityFediverse: "多元宇宙", contributorsTitle: "贡献者", - pixeliaBy: "作者", colorLabel: '选择颜色', paintButton: '画!', - invalidCoordinate: '坐标无效', - goToMuralButton: "查看壁画", totalPixels: '总像素数', modules: "模块", modulesViewTitle: "模块", modulesViewDescription: "通过启用或禁用模块来设置你的环境。", inbox: "收件箱", + inboxSearchPlaceholder: "在收件箱中搜索...", multiverse: "多元宇宙", - fediverse: "联邦宇宙", - fediverseDescription: "管理你的其他联邦宇宙账户,包括发送和接收内容。", + fediverse: "多元宇宙", + fediverseDescription: "管理你的其他多元宇宙账户,包括发送和接收内容。", fediverseStatus: "状态", - fediverseDisconnected: "联邦宇宙已断开。请检查%LINK%或连接状态。", - fediverseSettingsTitle: "联邦宇宙", + fediverseDisconnected: "多元宇宙已断开。请检查%LINK%或连接状态。", + fediverseSettingsTitle: "多元宇宙", fediverseInstanceLabel: "地址", fediverseTokenLabel: "访问令牌", - fediverseTokenHelp: "设置你的访问令牌。它将用于在 Oasis 中管理你的 Mastodon 账户。", fediverseConnect: "连接", fediverseConnectedAs: "已连接为", + nextcloudServerLabel: "服务器", + nextcloudUserLabel: "用户", + nextcloudFolderLabel: "文件夹", + nextcloudUploadButton: "上传", + nextcloudFileLabel: "选择文件", + fileExternalLink: "外部链接", + nextcloudErrConnect: "无法用这些信息连接到 Nextcloud。", + nextcloudErrUpload: "上传到 Nextcloud 失败。", + nextcloudErrShare: "Nextcloud 没有为该文件提供公开链接。", fediverseDisconnect: "断开连接", - fediverseEmpty: "当你从%LINK%连接其他联邦宇宙账户后,便可在此管理它们。", fediverseEmptyLink: "设置", fediverseComposePlaceholder: "在想什么?", fediverseReplyPlaceholder: "写下你的回复…", @@ -335,21 +311,15 @@ module.exports = { fediverseReply: "回复", fediverseBoosted: "转嗷了", fediverseNoPosts: "你的时间线是空的。", - fediverseThread: "讨论串", fediverseTimeline: "时间线", - fediverseManageTimeline: "管理时间线", fediverseFollowers: "关注者", fediverseFollowing: "正在关注", fediversePosts: "嘟文", fediverseJoined: "加入于", - fediverseOverview: "概览", fediverseRefresh: "刷新", fediverseWrite: "撰写", fediversePreview: "预览", - fediverseEdit: "编辑", - fediverseOpenFeed: "查看信息流", fediverseManage: "管理", - fediverseStatusNotConnected: "未连接", fediverseError: "无法连接 Mastodon。", fediverseErrInstance: "实例地址无效。", fediverseErrAuth: "令牌无效或已过期。", @@ -360,17 +330,76 @@ module.exports = { fediverseErrPost: "无法发布。", fediverseErrMedia: "无法上传文件。", fediverseErrEmpty: "写点什么或添加文件。", - popularLabel: "精选", - topicsLabel: "话题", - latestLabel: "最新", - summariesLabel: "摘要", - threadsLabel: "讨论串", - multiverseLabel: "多元宇宙", - inboxLabel: "收件箱", - invitesLabel: "邀请", - walletLabel: "钱包", - legacyLabel: "密钥", - cipherLabel: "加密器", + telegramApiIdLabel: "ID", + telegramApiHashLabel: "API hash", + telegramPhoneLabel: "手机号", + telegramSendCode: "发送验证码", + telegramCodeHelp: "输入 Telegram 刚发送的登录码(应用或短信)。", + telegramCodeLabel: "登录码", + telegramPasswordHelp: "该账号启用了两步验证,请输入云密码。", + telegramPasswordLabel: "密码", + telegramVerify: "验证", + telegramCancel: "取消", + telegramChats: "聊天", + telegramUnread: "未读", + telegramOpen: "打开", + telegramSend: "发送", + telegramYou: "你", + telegramNoChats: "还没有聊天。", + telegramNoMessages: "还没有消息。", + telegramComposePlaceholder: "输入消息…", + telegramChannel: "频道", + telegramGroup: "群组", + telegramPrivate: "私聊", + telegramErrConnect: "无法连接 Telegram。", + telegramErrTimeout: "Telegram 响应超时。", + telegramErrAuth: "Telegram 会话无效或已过期,请重新连接。", + telegramErrFetch: "无法加载聊天。", + telegramErrSend: "无法发送消息。", + telegramErrMissing: "需要 API ID、API hash 和手机号。", + telegramErrCode: "验证码无效,请重试。", + telegramErrCodeExpired: "验证码已过期,请重新开始。", + telegramErrPassword: "密码错误。", + telegramErrPhone: "手机号无效。", + telegramErrApi: "API ID 或 API hash 无效。", + telegramErrFlood: "尝试次数过多,请稍后再试。", + telegramErrNoLogin: "没有进行中的登录,请重新开始。", + peertubeUsernameLabel: "用户名", + peertubePasswordLabel: "密码", + peertubeSubscriptions: "订阅", + peertubeMyVideos: "我的视频", + peertubeVideos: "视频", + peertubeFollowers: "关注者", + peertubeViews: "次观看", + peertubeLikes: "赞", + peertubeQuota: "配额", + peertubeUpload: "上传视频", + peertubeUploadHelp: "选择一个视频文件。它会上传到你的频道,随后由 PeerTube 处理。", + peertubeTitleLabel: "标题", + peertubeDescriptionLabel: "描述", + peertubePrivacyLabel: "隐私", + peertubePrivacyPublic: "公开", + peertubePrivacyUnlisted: "不公开列出", + peertubePrivacyPrivate: "私密", + peertubeNoVideos: "还没有视频。", + peertubeComments: "评论", + peertubeComment: "评论", + peertubeCommentPlaceholder: "写下评论…", + peertubeNoComments: "还没有评论。", + peertubeOpen: "在 PeerTube 打开", + peertubeWatch: "观看", + peertubeChannel: "频道", + peertubeNoStream: "此视频暂时无法在这里播放。请在 PeerTube 打开。", + peertubeUploaded: "视频已上传。PeerTube 正在处理。", + peertubeErrConnect: "无法连接 PeerTube。", + peertubeErrAuth: "用户名或密码错误,或会话已过期。请重新连接。", + peertubeErrInstance: "实例地址无效。", + peertubeErrMissing: "需要实例、用户名和密码。", + peertubeErrFetch: "无法加载视频。", + peertubeErrUpload: "无法上传视频。", + peertubeErrTitle: "需要标题。", + peertubeErrComment: "无法发布评论。", + peertubeErrNoFile: "请选择视频文件。", bookmarksLabel: "书签", videosLabel: "视频", torrentsLabel: "种子", @@ -381,34 +410,22 @@ module.exports = { inhabitantsLabel: "居民", trendingLabel: "热门", eventsLabel: "活动", - tasksLabel: "任务", apply: "应用", menuPersonal: "个人", - menuContent: "内容", - menuBlogs: "博客", menuGovernance: "治理", menuOffice: "办公", - menuMultiverse: "多元宇宙", - menuNetwork: "网络", + menuNetwork: "社区", menuCreative: "创意", menuEconomy: "经济", - menuMedia: "媒体", + menuMedia: "资料库", menuTools: "工具", comment: "评论", subtopic: "子话题", json: "JSON", - createdBy: "作者", unfollow: "取消支持", follow: "支持", block: "屏蔽", unblock: "取消屏蔽", - newerPosts: "更新的帖子", - olderPosts: "更早的帖子", - feedRangeEmpty: "该范围内没有内容。请尝试查看", - seeFullFeed: "完整动态", - feedEmpty: "Oasis 网络从未见过此账户的帖子。", - beginningOfFeed: "这是动态的起始位置", - noNewerPosts: "尚未收到更新的帖子。", relationshipNotFollowing: "你未被支持", relationshipTheyFollow: "支持你", relationshipMutuals: "互相支持", @@ -418,25 +435,23 @@ module.exports = { relationshipBlockedBy: "你被屏蔽了", relationshipMutualBlock: "互相屏蔽", relationshipNone: "你未支持", - relationshipConflict: "冲突", relationshipBlockingPost: "已屏蔽的帖子", viewLikes: "查看传播", + viewSupporters: "查看支持者", spreadedDescription: "该居民传播的帖子列表。", - totalspreads: "总传播数", - attachFiles: "附加文件", preview: "预览", publish: "写作", contentWarningPlaceholder: "为帖子添加主题(可选)", - privateWarningPlaceholder: "添加居民以发送私密帖子(可选)", - publishWarningPlaceholder: "...", + publishWarningPlaceholder: "写下你的文章正文。", + publishTooLong: "你的帖子太长了,请缩短。", + keepTextRetry: "重新发送", + dateInPastError: "日期不能早于今天", publishCustomDescription: [ "提醒:由于区块链技术,帖子一旦发布就无法编辑或删除。", ], commentWarning: [ "提醒:由于区块链技术,帖子一旦发布就无法编辑或删除。", ], - commentPublic: "公开", - commentPrivate: "私密", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -456,14 +471,12 @@ module.exports = { "。", ], publishCustom: "撰写高级帖子", - subtopicLabel: "创建此帖子的子话题", messagePreview: "帖子预览", mentionsMatching: "匹配的提及", mentionsName: "名称", mentionsRelationship: "关系", - updateit: "获取更新!", updateBannerText: "Oasis 有新版本可用。", - updateBannerAction: "立即更新 →", + updateBannerAction: "立即更新!", info: "信息", settingsIntro: ({ version }) => [ `[SNH] ꖒ OASIS [ v.${version} ]`, @@ -471,29 +484,94 @@ module.exports = { timeAgo: "前", sendTime: "大约 ", theme: "主题", - legacy: "密钥", - legacyTitle: "密钥", - legacyDescription: "快速安全地管理你的密钥(私钥)。", - legacyExportButton: "导出", - legacyImportButton: "导入", + backupKeysExportButton: "导出", + backupKeysImportButton: "导入", ssbLogStream: "区块链", ssbLogStreamDescription: "配置区块链流的消息限制。", + logWindowChip: "最近 {n} 条区块链流", saveSettings: "保存设置", - exportTitle: "导出数据", exportDescription: "设置密码(至少32个字符)以加密你的密钥", - exportDataTitle: "备份", + exportDataTitle: "即时备份", exportDataDescription: "下载你的数据(不包含私钥!)", exportDataButton: "下载数据库", - pubWallet: "PUB 钱包", - pubWalletDescription: "设置 PUB 钱包 URL。这将用于 PUB 交易(包括 UBI)。", - pubWalletConfiguration: "保存配置", - importTitle: "导入数据", importDescription: "导入你的加密私钥以启用你的头像", - importAttach: "附加加密文件 (.enc)", - passwordLengthInfo: "密码至少需要32个字符。", passwordImport: "输入密码以解密将保存在系统主目录的数据(文件名:secret)", exportPasswordPlaceholder: "使用小写字母、大写字母、数字和符号", - fileInfo: "你的加密私钥将保存在系统主目录(文件名:oasis.enc)", + fileInfo: "你的加密私钥将下载到你的设备(文件名:oasis.enc)", + developer: "开发", + devTitle: "开发", + welcomeBannerText: "你的头像还没有设置。", + aiSuggestionBanner: "建议:", + aiSuggestionsEnable: "AI 建议", + welcomeBannerAction: "开始", + welcomeTitle: "欢迎", + welcomeStepIdentity: "身份", + welcomeStepType: "类型", + welcomeStepUse: "用途", + welcomeStepLook: "外观", + welcomeStepCommunity: "社区", + welcomeStepIntro: "欢迎", + welcomeHintIdentity: "你的名字和头像。", + welcomeHintType: "Oasis 为你打开的方式。", + welcomeHintUse: "保留哪些模块。", + welcomeHintLook: "Oasis 使用的主题。", + welcomeHintCommunity: "加入一个 PUB 认识其他居民。", + welcomeHintIntro: "向社区发送第一条问候。", + welcomeStepThemeAction: "使用此主题", + welcomeJoinPubs: "加入 PUB", + welcomeNoFederation: "不加入联邦", + welcomeStepGreetingAction: "发送", + welcomeDescription: "开始之前值得做的几件事。", + welcomeSkip: "暂不设置", + welcomeStepProfileAction: "保存资料", + welcomeBootstrapRunning: "正在加载 pub 的快照。完成后会自动加入该 pub;在此期间你可以继续使用 Oasis。", + welcomeCompleteTitle: "全部就绪。", + welcomeCompleteText: "你的节点已经就绪。从这里开始,网络随着你支持的居民一起成长。", + welcomeFindPeople: "寻找居民", + devFilterFiles: "文件", + devFilterSearch: "搜索", + devFilterModules: "模块", + devFilterTranslations: "翻译", + devFilterStyles: "样式", + devFilterTests: "测试", + devVersion: "版本", + devNodeVersion: "Node", + devStatsTests: "测试", + devParentFolder: "上级文件夹", + devOpenFolder: "打开", + devDescription: "浏览本节点上运行的源代码。", + devRoot: "根目录", + devSearchPlaceholder: "要在代码中查找的文本", + devAnyExtension: "任意文件", + devStatsFiles: "文件", + devStatsLines: "行数", + devStatsModules: "模块", + devStatsLanguages: "语言", + devNotViewable: "无法查看", + devEmptyFolder: "此文件夹为空。", + devSize: "大小", + devShowing: "显示", + devLine: "行", + devReportBug: "报告缺陷", + devCreateTask: "创建任务", + devRaw: "RAW", + devReportContext: "在阅读 Oasis 源代码时发现", + devTaskPrefix: "复查", + devPrevPage: "上一段", + devNextPage: "下一段", + devMatches: "个匹配", + devFilesScanned: "个文件已扫描", + devNoResults: "暂无匹配。", + devColModule: "模块", + devColState: "状态", + devColModel: "模型", + devColView: "视图", + devColRoutes: "路由", + devColTests: "测试", + devOn: "开", + devOff: "关", + modulesDevLabel: "开发", + modulesDevDescription: "用于管理 Oasis 源代码的模块。", themeIntro: "选择一个主题。", setTheme: "设置主题", @@ -501,16 +579,49 @@ module.exports = { languageDescription: "如果你想使用其他语言,请在此选择。", setLanguage: "设置语言", - peerConnections: "节点", peerConnectionsIntro: "管理与其他节点的所有连接。", + peersConnectFailed: "无法连接:{reason}。", + peerStateLabel: "状态", + peerAddressLabel: "地址", + peerLastChangeLabel: "最后变更", + peerStateConnected: "已连接", + peerStateConnecting: "正在连接", + peerStateStaged: "可用", + peerErrSilent: "无活动", + peerErrTimeout: "无响应", + peerErrRefused: "拒绝连接", + peerErrNotFound: "未知地址", + peerErrUnreachable: "网络不可达", + peerErrKeys: "使用不同的密钥", + peerErrIdentity: "不接受你的连接", + peerErrTor: "仅限 TOR", + peerErrDropped: "连接中断", + peerErrPaused: "网络已暂停", + peerErrOther: "连接错误", + peerOutdated: "已过时", + peerVersionUnannounced: "未公布其版本:旧版 Oasis", + peersTorMissing: "此 PUB 仅允许通过 TOR 连接。", peerConnectionsTitle: "连接", + peersOnlineHint: "当前与你相连的节点。", + peersDiscoveredHint: "地址已知但当前未连接的节点。", + peersUnknownHint: "你的社交图谱中没有已知地址的节点。", + peersConnectionsHint: "本节点可以拨号的地址及每个连接的状态。", + peersTabLan: "局域网", + peersTabHealth: "健康", + peersHealthHint: "每个已知 pub 或节点的状态,包括局域网内外。", + peersHealthReachable: "可达", + peersHealthFailing: "失败中", + peersHealthFailures: "失败次数", + peersFailingSince: "失败开始于", + peersHealthForget: "忘记不可达节点", + peersLanHint: "在你的局域网中发现的节点。", + peersLanLastSeen: "最后出现", + peersLanEmpty: "局域网中尚未发现任何节点。", online: "在线", offline: "离线", discovered: '已发现', unknown: '未知', - lanBroadcastLabel: "LAN 广播", lanBroadcastActive: "已启用(LAN 上的 UDP 组播)", - lanBroadcastDisabled: "已停用(pub 模式)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -523,127 +634,102 @@ module.exports = { noConnections: "没有已连接的节点。", noDiscovered: "没有已发现的节点。", noUnknownPeers: "好友图中没有未知节点。", - peersTechnicalTitle: "Peers (technical)", - peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", - peersTechnicalEmpty: "No peers registered yet.", - peerConnect: "Connect", - peerDisconnect: "Disconnect", + peersTechnicalEmpty: "尚未登记任何节点。", + peerConnect: "连接", + peerDisconnect: "断开", peerRefresh: "刷新", peerPruneIdle: "移除空闲", + peersPause: "暂停", + peersResume: "继续", + peersPausedNote: "已暂停:Oasis 不会连接任何人,也没有人能连接到它。", + peersPauseHint: "停止所有连接:在你恢复之前,Oasis 不会连接其他节点,也不接受它们的连接。", peerExport: "导出", peerImport: "导入", peerImportTitle: "导入对等节点列表", peerImportPlaceholder: "每行粘贴一个 multiserver 地址,或上传 .txt 文件……", - noSupportedConnections: "没有已支持的节点。", - noBlockedConnections: "没有已屏蔽的节点。", - noRecommendedConnections: "没有推荐的节点。", connectionActionIntro: "", - startNetworking: "启动网络", - stopNetworking: "停止网络", - restartNetworking: "重启网络", - sync: "同步网络", indexes: "索引", + indexesRebuilt: "索引已重建", + indexesRebuildFailed: "无法重建索引", + indexesFiles: "索引文件", indexesDescription: "重建索引是安全的,可能修复某些类型的错误。", homePageTitle: "主页", homePageDescription: "选择你想要作为主页的模块。", saveHomePage: "设置主页", invites: "邀请", - invitesTitle: "邀请", - invitesInvites: "邀请", invitesDescription: "管理和应用你网络中的邀请码。", + invitesPubsHint: "粘贴 pub 邀请码,与它建立联邦并开始复制其居民。", + invitesFederationsHint: "已联邦的 pub:刷新、移除无法连接的,或导出列表。", invitesTribesTitle: "部落", - invitesTribeInviteCodePlaceholder: "输入部落邀请码", - invitesTribeJoinButton: "加入部落", invitesChatsTitle: "聊天", - invitesChatInviteCodePlaceholder: "输入聊天邀请码", - invitesChatJoinButton: "加入聊天", invitesPadsTitle: "协作板", - invitesPadInviteCodePlaceholder: "输入协作板邀请码", - invitesPadJoinButton: "加入协作板", invitesCalendarsTitle: "日历", - invitesCalendarInviteCodePlaceholder: "输入日历邀请码", - invitesCalendarJoinButton: "加入日历", invitesEventsTitle: "活动", - invitesEventInviteCodePlaceholder: "输入活动邀请码", - invitesEventJoinButton: "加入活动", invitesForumsTitle: "论坛", - invitesForumInviteCodePlaceholder: "输入论坛邀请码", - invitesForumJoinButton: "加入论坛", invitesMapsTitle: "地图", - invitesMapInviteCodePlaceholder: "输入地图邀请码", - invitesMapJoinButton: "加入地图", invitesShopsTitle: "商店", - invitesShopInviteCodePlaceholder: "输入商店邀请码", - invitesShopJoinButton: "加入商店", invitesPubsTitle: "PUBs", + invitesSuggestedPubs: "推荐的 PUB", + invitesFilterOthers: "其他", + invitesJoinTitle: "邀请", + invitesJoinHint: "粘贴任何邀请码(无论用于什么),或某位居民的 Oasis ID。", + invitesJoinPlaceholder: "邀请码或 Oasis ID", + invitesJoinButton: "加入", + invitesUsedTitle: "最近的邀请", + invitesUsedEmpty: "你还没有使用过任何邀请。", + invitesUsedType: "类型", + invitesUsedName: "名称", + invitesUsedDate: "日期", invitesPubInviteCodePlaceholder: "输入 PUB 邀请码", invitesAcceptInvite: "加入 PUB", + invitesPubUses: "已用邀请", + invitesPubExhausted: "此 PUB 已没有可用的邀请。", invitesAlreadyFederated: "您已与该 pub 建立联邦。", + invitesJoinPubMode: "此 Oasis 以只读模式作为 PUB 运行。要将其加入另一个 PUB,请在终端中使用:./oasis.sh join <邀请码>", + invitesJoinLocalOnly: "只能在运行 Oasis 的设备上接受邀请,或使用其启动时显示的管理链接。", invitesFederationsTitle: "联邦", invitesAcceptedInvites: "已联邦", - invitesNoInvites: "尚未接受任何邀请。", invitesUnfollow: "取消关注", invitesFollow: "关注", invitesUnfollowedInvites: "未联邦", invitesNoFederatedPubs: "没有已联邦的 pub。", invitesNoUnfollowed: "没有未联邦的 pub。", - invitesUnreachablePubs: "不可达", - invitesNoUnreachablePubs: "没有不可达的 pub。", invitesPubsRefresh: "刷新", - invitesPubsClearUnreachable: "移除不可达", + invitesPubsClearUnreachable: "清理", invitesPubsExport: "导出", invitesPubsImport: "导入", invitesPubsImportTitle: "导入 pubs", invitesPubsImportPlaceholder: "每行粘贴一个 multiserver 地址或邀请码,或上传 .txt 文件……", - currentlyUnreachable: "错误!", - errorDetails: "错误详情", genericError: "发生了一个错误。", panicMode: "紧急模式!", encryptData: "设置密码(至少32个字符)以加密你的区块链", decryptData: "输入密码以解密你的区块链", - panicModeDescription: "加密/解密或删除你的区块链", removeDataDescription: "警告:此操作不可撤销。", - encryptPanicButton: "加密区块链", - decryptPanicButton: "解密区块链", removePanicButton: "删除所有数据!", searchTitle: "搜索", searchDescriptionLabel: "在你的网络中搜索内容。", - searchLanguagesLabel: "语言", - searchSkillsLabel: "技能", searchPlaceholder:"搜索内容...", - searchDateLabel:"日期", searchLocationLabel:"位置", searchPriceLabel:"价格", - searchUrlLabel:"URL", searchCategoryLabel:"类别", - searchStartLabel:"开始时间", - searchEndLabel:"结束时间", searchPriorityLabel:"优先级", searchStatusLabel:"状态", statusLabel:"状态", - totalVotesLabel:"总票数", noResultsFound:"未找到结果。", votesOption:"投票选项", voteYesLabel:"赞成", voteNoLabel:"反对", allTypesLabel: "无限制", - ABSTENTIONLabel:"弃权", YESLabel:"赞成", NOLabel:"反对", - FOLLOW_MAJORITYLabel: "跟随多数", - CONFUSEDLabel: "困惑", - NOT_INTERESTEDLabel: "不感兴趣", - StatusLabel:"状态", votesOptionYesLabel:"赞成", votesOptionNoLabel:"反对", votesOptionAbstentionLabel:"弃权", votesQuestionLabel:"问题", - votesCreatedByLabel:"创建者", voteStatusOpen:"进行中", voteStatusClosed:"已关闭", - imageSearchLabel: "输入关键词以搜索带有相应标签的图片。", commentDescription: ({ parentUrl }) => [ " 评论了 ", a({ href: parentUrl }, " 讨论串"), @@ -655,61 +741,27 @@ module.exports = { " 创建了子话题", ], subtopicTitle: ({ authorName }) => [`@${authorName} 帖子的子话题`], - mysteryDescription: "发布了一篇神秘的帖子", oasisDescription: "OASIS 项目网络", searchSubmit: "搜索...", - postLabel: "帖子", - aboutLabel: "居民", - feedLabel: "动态", votesLabel: "投票", - reportLabel: "报告", - imageLabel: "图片", - videoLabel: "视频", - audioLabel: "音频", - documentLabel: "文档", - torrentLabel: "种子", pdfFallbackLabel: "PDF 文档", - eventLabel: "活动", - taskLabel: "任务", - transferLabel: "转账", - curriculumLabel: "简历", - bookmarkLabel: "书签", - tribeLabel: "部落", - marketLabel: "市场", - shopLabel: "SHOPS", - shopProductLabel: "SHOP PRODUCTS", - mapLabel: "MAPS", - jobLabel: "JOBS", - forumLabel: "FORUMS", - projectLabel: "PROJECTS", - bankWalletLabel: "钱包", - bankClaimLabel: "UBI CLAIMS", - voteLabel: "VOTES", - contactLabel: "CONTACTS", - pubLabel: "PUBS", + shopLabel: "商店", + voteLabel: "投票", submit: "提交", - subjectLabel: "主题", editProfile: "编辑头像", - profileQrAlt: "个人资料二维码", - profileQrHint: "扫码以复制此 Oasis ID。", oasisIdLabel: "OasisID", - spreadLabel: "转发", - spreadHint: "转发给你的支持者(通过你的 feed 复制)。", + spreadContent: "复制", welcomePmSubject: "Hello World!", - welcomePmBody: "欢迎来到 Oasis — 一个自由、点对点、联邦化且加密的社交网络,采用仅限邀请的分布式架构。\n\n一些值得开始探索的地方:\n\n- /profile —— 编辑你的头像、名字、简介以及在公开侧显示哪些模块。\n- /invites —— 添加一个 pub,与更广泛的网络联邦。\n- /peers —— 查看谁已连接,重新扫描局域网,导出或导入对等节点列表。\n- /inhabitants —— 发现并访问其他人的头像。\n- /tribes —— 创建或加入端到端加密的私人小组。\n- /inbox —— 阅读和发送私信。\n- /activity —— 查看最近的活动,你的和你网络中的。\n- /publish —— 写一篇帖子,或分享图片、音频、视频、文档。\n\n一些值得连接的地方:\n\n- /fediverse — 管理你的其他联邦宇宙账户,包括发送和接收内容。\n\n这条消息是私下从你发给你自己的,因此无需任何连接即可接收。", - profileVisibilityTitle: "公开资料可见性", - profileVisibilityHint: "选择其他居民在你的资料中可见的字段。默认仅显示 Karma。", + welcomePmBody: "欢迎来到 Oasis — 一个自由、点对点、联邦化且加密的社交网络,采用仅限邀请的分布式架构。\n\n一些值得开始探索的地方:\n\n- [/avatar](/profile) —— 编辑你的头像、名字、简介以及在公开侧显示哪些模块。\n- /settings — 调整你的本地实例:语言、主题、模块、隐私和同步。\n- /invites —— 添加一个 pub,与更广泛的网络联邦。\n- /peers —— 查看谁已连接,重新扫描局域网,导出或导入对等节点列表。\n- /inhabitants —— 发现并访问其他居民的头像。\n- /tribes —— 创建或加入端到端加密的私人小组。\n- /search — 按类型搜索网络内容。\n\n一些值得连接的地方:\n\n- /multiverse — 管理你的其他多元宇宙账户,包括发送和接收内容。\n\n这条消息是私下从你发给你自己的,因此无需任何连接即可接收。", profileSensorsSectionTitle: "传感器", profileVisibilityActivity: "活跃度", - profileVisibilityDevice: "设备", profileDeviceLockedHint: "此传感器始终开启:它让其他人看到你从哪个设备连接,并且无法关闭。", profileVisibilityKarma: "KARMA 评分", profileVisibilityUbi: "UBI", - profileVisibilityWallet: "ECOIN 钱包", - profileVisibilityEcoTax: "ECO Tax", - profileVisibilityLarpSign: "L.A.R.P. Sign", + profileVisibilityWallet: "ECOin 钱包", + profileVisibilityEcoTax: "ECO 税", + profileVisibilityLarpSign: "L.A.R.P. 徽记", profileVisibilityGpg: "GPG 密钥", - profileVisibilityClearnet: "发布我的资料", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "店铺", profileClearnetJobsLabel: "工作", @@ -721,6 +773,10 @@ module.exports = { profileClearnetImagesLabel: "图片", profileClearnetDocumentsLabel: "文档", profileClearnetTorrentsLabel: "种子", + profileClearnetPodcastsLabel: "播客", + profileClearnetMarketLabel: "市场", + profileClearnetFeedLabel: "动态", + profileClearnetWikiLabel: "维基", editProfileDescription: "", profileName: "名称", @@ -746,7 +802,7 @@ module.exports = { walletBalanceLine: ({ balance }) => `${balance} ECO`, walletCnfrs: "确认数", walletConfirm: "确认", - walletDescription: "管理你的数字资产,包括发送和接收 ECOin、查看余额和访问交易历史。", + walletDescription: "管理您的 ECOin 钱包。", walletDate: "日期", walletFee: "手续费(手续费越高,交易处理越快)", walletFeeLine: ({ fee }) => `手续费:ECO ${fee}`, @@ -754,6 +810,86 @@ module.exports = { walletReceive: "接收", walletReset: "重置", walletSend: "发送", + walletPayingTransfer: "正在支付转账", + walletCreateTransferLabel: "生成智能合约(转账)", + walletTransferNotifiedNote: "已通知对方本次付款", + walletTransferCreatedNote: "已为本次付款创建转账(收据)", + walletTransferNoPayeeNote: "没有居民发布过此地址,因此未创建转账", + walletTransferReceiptConcept: "钱包付款", + walletPaymentPmText: "向你发送了一笔付款", + walletPayeeLabel: "收款人", + walletConceptLabel: "事由", + pmBotBanking: "BankingBot", + bankingBotUbiPaidTitle: "您已收到 UBI。", + bankingBotPaymentTitle: "您已收到一笔付款。", + bankingBotUbiPaidText: "您的全民基本收入已发放", + bankUbiPubThisNode: "本节点负责发放 UBI", + bankUbiPubConnected: "UBI PUB", + bankUbiPubNone: "您的网络中尚无 PUB 宣告 UBI", + bankUbiAddressPublished: "ECOin 地址已发布", + bankUbiAddressMissing: "发布您的 ECOin 地址以领取 UBI", + bankUbiClaimedThisMonth: "本月已领取", + bankUbiPubBalance: "可用于 UBI 的 PUB 钱包", + bankNoPubAddress: "UBI PUB 尚未发布 ECOin 地址。", + bankUbiTab: "UBI", + bankDonateEco: "捐赠!", + donateTooltip: "捐赠 ECOin", + bankNoUserAddress: "该居民未公开 ECOin 地址。", + bankUbiPubDefault: "默认", + bankUbiPool: "资金池", + bankUbiLastSeen: "最后在线", + bankUbiLastPayout: "最近操作", + bankUbiPaidOut: "已发放", + bankRangeToday: "今天", + bankRangeWeek: "本周", + bankRangeMonth: "本月", + bankRangeYear: "本年", + bankRangeFiveYears: "5 年", + bankRangeAll: "全部", + bankChartValueTitle: "ECOin 价值演变", + bankChartValueHours: "时间等值(小时)", + bankChartWealthTitle: "ECOin 财富再分配", + bankChartDistributed: "已发放 UBI(ECO)", + bankChartRebalanceTitle: "PUB 之间的财富再分配", + bankChartRebalanced: "在 PUB 之间转移的 ECO", + bankChartTaxes: "已扣税款(ECO)", + bankChartNoDataRange: "此范围内没有数据。", + bankChartNoDataYet: "暂无数据", + bankChartPerHour: "每小时", + bankChartPerDay: "每天", + bankChartPerMonth: "每月", + bankTotalUbiDistributed: "已重新分配的 UBI 总额(全网)", + bankTotalTaxesWithheld: "已扣税款总额(全网)", + bankInflationIssuance: "发行率(年化,按区块奖励)", + bankRulesEligibilityTitle: "谁可以领取 UBI", + bankRulesEligibility1: "在个人资料中发布的 ECOin 地址(资料 → 传感器 → ECOin 钱包)。", + bankRulesEligibility2: "超过 30 天且在网络中有活动的 feed:业力大于零。", + bankRulesEligibility3: "每月(每个周期)一次领取,在 Banking 中点击领取 UBI。可用拒绝 UBI 放弃当月。", + bankRulesEligibility4: "1 ECO 的底线始终发放;税款只减少超出部分,从不另行收取。", + bankRulesEligibility5: "PUB 在下一周期支付并发布一笔可供确认的 UBI 转账。", + bankRulesEligibility6: "若缺少地址、feed 太新、已拒绝当月或 PUB 无资金,领取将被跳过并记录在 PUB 上。", + copyLinkTitle: "复制链接", + linkBoxOasis: "Oasis", + linkBoxClearnet: "Clearnet", + bankRefuseUBI: "拒绝 UBI", + bankUbiRefusedThisMonth: "您已拒绝本月的 UBI。", + bankingBotUbiAvailableTitle: "您可以领取 UBI 了。", + bankingBotUbiAvailableText: "您本月的 UBI 已可领取", + bankingBotConfirmTitle: "您有一份等待确认的智能合约", + bankingBotConfirmText: "等待您确认的智能合约", + bankChartZoomHint: "点击放大", + bankChartPoolTitle: "UBI 资金池(变化)", + bankChartNetworkUbiTitle: "已重新分配的 UBI 总额(全网)", + bankChartSupplyTitle: "ECOin 流通供应量(变化)", + bankChartInflationTitle: "ECOin 通胀(变化)", + bankChartInflationSeries: "年化率(%)", + bankChartPool: "资金池总额(ECO)", + bankChartKarmaTitle: "KARMA Scoring(变化)", + bankChartFundsTitle: "你的 ECOin 资金(变化)", + bankChartKarma: "业力", + bankWalletReady: "ECOin 已连接", + bankWalletNotConfigured: "ECOin 未连接", + transfersPayWithWallet: "支付!", walletStatus: "状态", walletDisconnected: [ "ECOin ", @@ -763,9 +899,11 @@ module.exports = { " 或连接状态。", ], walletSentToLine: ({ destination, amount }) => `已发送 ECO ${amount} 至 ${destination}`, - walletSettingsTitle: "钱包", + walletNotConfigured: "你的 ECOin 钱包尚未配置。", walletSettingsDescription: "将 Oasis 与你的 ECOin 钱包集成。", - walletSettingsDocLink: "ECOin 安装指南", + walletSettingsMobileDescription: "将 Oasis 与此设备上的 ECOin Wallet 应用关联。", + walletDownloadEcoin: "下载 ECOin!", + walletRpcUrl: "RPC 地址", walletStatusMessages: { invalid_amount: "无效金额", invalid_dest: "无效目标地址", @@ -781,41 +919,24 @@ module.exports = { walletUser: "用户名", walletPass: "密码", walletConfiguration: "设置钱包", + walletDisconnectButton: "断开钱包", cipher: "加密器", cipherTitle: "加密器", cipherDescription: "使用共享密码对文本进行对称加密和解密。", randomPassword: "随机密码", - cipherEncryptTitle: "加密文本", - cipherEncryptDescription: "输入要加密的文本", - cipherTextLabel: "要加密的文本", cipherTextPlaceholder: "输入要加密的文本...", cipherPasswordLabel: "设置密码(至少32个字符)以加密你的文本", cipherPasswordDecryptLabel: "设置密码(至少32个字符)以解密你的文本", cipherPasswordPlaceholder: "输入密码...", cipherEncryptButton: "加密", - cipherDecryptTitle: "解密文本", - cipherDecryptDescription: "输入要解密的文本", cipherEncryptedMessageLabel: "加密文本", cipherDecryptedMessageLabel: "解密文本", - cipherPasswordUsedLabel: "加密所用密码(请妥善保管!)", cipherEncryptedTextPlaceholder: "输入加密文本...", - cipherIvLabel: "IV", - cipherIvPlaceholder: "输入初始化向量...", cipherDecryptButton: "解密", password: "密码", text: "文本", encryptedText: "加密文本", iv: "初始化向量 (IV)", - encryptTitle: "加密你的文本", - encryptDescription: "输入要加密的文本并提供密码。", - encryptButton: "加密", - decryptTitle: "解密你的文本", - decryptDescription: "输入加密文本并提供相同的加密密码。", - decryptButton: "解密", - passwordLengthError: "密码至少需要32个字符。", - missingFieldsError: "未提供文本、密码或 IV。", - encryptionError: "加密文本时出错。", - decryptionError: "解密文本时出错。", bookmarkTitle: "书签", bookmarkDescription: "发现和管理你网络中的书签。", bookmarkAllSectionTitle: "书签", @@ -832,19 +953,16 @@ module.exports = { bookmarkFilterRecent: "最近", bookmarkCreateButton: "创建书签", bookmarkUpdateButton: "更新", - bookmarkDeleteButton: "删除", bookmarkAddFavoriteButton: "添加收藏", bookmarkRemoveFavoriteButton: "取消收藏", bookmarkUrlLabel: "链接", - bookmarkUrlPlaceholder: "https://example.com", + bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "描述", bookmarkDescriptionPlaceholder: "可选", bookmarkTagsLabel: "标签", bookmarkTagsPlaceholder: "输入标签,用逗号分隔", - bookmarkCategoryLabel: "类别", - bookmarkCategoryPlaceholder: "可选", bookmarkLastVisitLabel: "上次访问", - bookmarkSearchPlaceholder: "搜索 URL、标签、类别、作者...", + bookmarkSearchPlaceholder: "在书签中搜索...", bookmarkSortRecent: "最近", bookmarkSortOldest: "最早", bookmarkSortTop: "最多投票", @@ -857,8 +975,6 @@ module.exports = { noLastVisit: "无访问记录", videoTitle: "视频", videoDescription: "探索和管理你网络中的视频内容。", - videoPluginTitle: "标题", - videoPluginDescription: "描述", videoMineSectionTitle: "你的视频", videoCreateSectionTitle: "上传视频", videoUpdateSectionTitle: "更新视频", @@ -885,12 +1001,11 @@ module.exports = { videoDescriptionPlaceholder: "可选", videoNoFile: "未提供视频文件", noVideos: "没有可用的视频。", - videoSearchPlaceholder: "搜索标题、标签、作者...", + videoSearchPlaceholder: "在视频中搜索...", videoSortRecent: "最近", videoSortOldest: "最早", videoSortTop: "最多投票", videoSearchButton: "搜索", - videoMessageAuthorButton: "私信", videoUpdatedAt: "已更新", videoNoMatch: "没有匹配的视频。", documentTitle: "文档", @@ -909,7 +1024,6 @@ module.exports = { documentFilterFavorites: "收藏", documentCreateButton: "上传文档", documentUpdateButton: "更新", - documentDeleteButton: "删除", documentAddFavoriteButton: "添加收藏", documentRemoveFavoriteButton: "取消收藏", documentMessageAuthorButton: "私信", @@ -922,7 +1036,7 @@ module.exports = { documentDescriptionPlaceholder: "可选", documentNoFile: "无文件。", noDocuments: "没有可用的文档。", - documentSearchPlaceholder: "搜索标题、标签、描述、作者...", + documentSearchPlaceholder: "在文档中搜索...", documentSortRecent: "最近", documentSortOldest: "最早", documentSortTop: "最多投票", @@ -931,8 +1045,6 @@ module.exports = { documentUpdatedAt: "已更新", audioTitle: "音频", audioDescription: "探索和管理你网络中的音频内容。", - audioPluginTitle: "标题", - audioPluginDescription: "描述", audioMineSectionTitle: "你的音频", audioCreateSectionTitle: "上传音频", audioUpdateSectionTitle: "更新音频", @@ -943,22 +1055,21 @@ module.exports = { audioFilterMine: "我的", audioFilterRecent: "最近", audioFilterTop: "热门", - audioFilterBlockchain: "区块链", audioCreateButton: "上传音频", audioUpdateButton: "更新", - audioDeleteButton: "删除", audioAddFavoriteButton: "添加收藏", audioRemoveFavoriteButton: "取消收藏", audioFileLabel: "选择音频文件 (.mp3, .wav, .ogg)", audioTagsLabel: "标签", audioTagsPlaceholder: "输入标签,用逗号分隔", + tagsPlaceholder: "输入标签,用逗号分隔", audioTitleLabel: "标题", audioTitlePlaceholder: "可选", audioDescriptionLabel: "描述", audioDescriptionPlaceholder: "可选", audioNoFile: "未提供音频文件", noAudios: "没有可用的音频。", - audioSearchPlaceholder: "搜索标题、标签、作者...", + audioSearchPlaceholder: "在音频中搜索...", audioSortRecent: "最近", audioSortOldest: "最早", audioSortTop: "最多投票", @@ -970,6 +1081,7 @@ module.exports = { audioFilterFavorites: "收藏", favoritesTitle: "收藏", favoritesDescription: "所有你收藏的内容集中在一处。", + favoritesSearchPlaceholder: "在收藏中搜索...", favoritesFilterAll: "全部", favoritesFilterRecent: "最近", favoritesFilterAudios: "音频", @@ -981,22 +1093,16 @@ module.exports = { favoritesFilterChats: "聊天", favoritesFilterCalendars: "日历", favoritesFilterVideos: "视频", - favoritesRemoveButton: "取消收藏", favoritesNoItems: "还没有收藏。", yourContacts: "你的联系人", allInhabitants: "居民", - allCVs: "所有简历", + allCVs: "简历", discoverPeople: "发现你网络中的居民。", - allInhabitantsButton: "全部", - contactsButton: "支持", - CVsButton: "简历", - matchSkills: "匹配技能", - matchSkillsButton: "匹配技能", - suggestedButton: "推荐", - searchInhabitantsPlaceholder: "按名称筛选居民...", + searchInhabitantsPlaceholder: "在居民中搜索...", filterLocation: "按位置筛选居民...", filterLanguage: "按语言筛选居民...", filterSkills: "按技能筛选居民...", + cvSkillsCount: "技能", applyFilters: "应用筛选", locationLabel: "位置", languagesLabel: "语言", @@ -1005,15 +1111,15 @@ module.exports = { mutualFollowers: "共同关注者", suggestedFollowsYou: "关注了你", latestInteractions: "最近互动", - viewAvatar: "查看头像", - viewCV: "查看简历", suggestedSectionTitle: "推荐", topkarmaSectionTitle: "最高因缘值", topecoSectionTitle: "最佳生态", topactivitySectionTitle: "最高活跃度", + "TOP INACTIVITYButton": "最不活跃", + topinactivitySectionTitle: "最不活跃的居民", blockedSectionTitle: "已屏蔽", gallerySectionTitle: "画廊", - blockedButton: "已屏蔽", + topSectionTitle: "排行", blockedLabel: "已屏蔽用户", inhabitantviewDetails: "查看详情", keepReading: "继续阅读...", @@ -1028,10 +1134,23 @@ module.exports = { parliamentFilterCandidatures: "候选", parliamentFilterProposals: "提案", parliamentFilterLaws: "法律", + parliamentFilterRevocations: "罢免", parliamentFilterHistorical: "历史", - parliamentFilterLeaders: "领袖", + parliamentFilterLeaders: "领导", parliamentFilterRules: "规则", parliamentGovernmentCard: "现任政府", + parliamentSeatsGovernment: "政府", + parliamentSeatsOpposition: "反对派", + parliamentSeatsAbstention: "弃权", + parliamentSeatsQuorum: "选举法定人数", + parliamentSeatsPass: "通过门槛", + parliamentSeatsByElection: "按选举", + parliamentSeatsByHouses: "按 LARP 家族", + parliamentSeatsSeats: "席", + parliamentSeatsSeat: "席", + parliamentSeatsLeader: "领袖", + parliamentSeatsVotesFor: "投给", + parliamentSeatsNoHouse: "无家族", parliamentActorInPowerInhabitant: '执政居民', parliamentActorInPowerTribe: '执政部落', parliamentHistoricalGovernmentsTitle: '政府', @@ -1053,10 +1172,8 @@ module.exports = { parliamentPoliciesDeclined: "否决的法律", parliamentPoliciesDiscarded: "废弃的法律", parliamentEfficiency: "% 效率", - parliamentFilterRevocations: '撤销', parliamentRevocationFormTitle: '撤销法律', parliamentRevocationLaw: '法律', - parliamentRevocationTitle: '标题', parliamentRevocationReasons: '理由', parliamentRevocationPublish: '发布撤销', parliamentCurrentRevocationsTitle: '当前撤销', @@ -1069,23 +1186,12 @@ module.exports = { parliamentNoGovernments: "还没有政府。", parliamentCandidatureFormTitle: "提议候选", parliamentCandidatureIdPh: "Oasis ID (@...) 或部落名称", - parliamentCandidatureSlogan: "口号(最多140字)", - parliamentCandidatureSloganPh: "一个简短的口号", parliamentCandidatureMethod: "方式", parliamentCandidatureProposeBtn: "发布候选", - parliamentThDate: "提议日期", - parliamentThSlogan: "口号", - parliamentThSince: "注册时间", - parliamentThVotes: "获得票数", - parliamentThVoteAction: "投票", - parliamentTypeUser: "居民", - parliamentTypeTribe: "部落", parliamentVoteBtn: "投票", parliamentProposalFormTitle: "提议法律", parliamentProposalDescription: "描述(不超过1000字)", parliamentProposalPublish: "发布提案", - parliamentFinalize: "结束", - parliamentDeadline: "截止日期", parliamentNoProposals: "还没有法律提案。", parliamentNoLaws: "还没有已通过的法律。", parliamentMethodDEMOCRACY: "民主", @@ -1096,36 +1202,29 @@ module.exports = { parliamentThId: "ID", parliamentThProposalDate: "提议日期", parliamentThMethod: "方式", - parliamentThKarma: "Karma", + parliamentThKarma: "业力", parliamentThSupports: "支持", parliamentThVote: "投票", parliamentCurrentProposalsTitle: "当前提案", parliamentVotesSlashTotal: "票数/总计", parliamentVotesNeeded: "所需票数", parliamentFutureLawsTitle: "未来法律", - parliamentNoFutureLaws: "还没有未来法律。", parliamentVoteAction: "投票", - parliamentLeadersTitle: "领袖", parliamentThLeader: "头像", parliamentPopulation: "人口", - parliamentThType: "类型", - parliamentThInPower: "执政中", parliamentThPresented: "候选", parliamentNoLeaders: "还没有领袖。", parliamentCandidaturesListTitle: "候选列表", parliamentTimeRemaining: "剩余时间", - parliamentNoLeader: "尚无领先候选。", parliamentElectionsStatusTitle: "下届政府", parliamentProposalDeadlineLabel: "截止日期", + parliamentViewReport: "查看报告", parliamentProposalTimeLeft: "剩余时间", - parliamentProposalOnTrack: "有望通过", - parliamentProposalOffTrack: "支持不足", parliamentRulesTitle: "议会运作方式", parliamentRulesIntro: "选举每2个月进行一次;候选是持续的,在政府选定后重置。", parliamentRulesCandidates: "任何居民都可以提名自己、其他居民或任何部落。每位居民每个周期最多可提出3项候选;同一周期内的重复候选将被拒绝。", parliamentRulesElection: "获胜者是在结算时获得最多选票的候选。", parliamentRulesTies: "平局规则:最高居民 Karma;如果仍然平局,最早注册的;如果仍然平局,最早提出的;然后按 ID 字典序。", - parliamentRulesFallback: "如果没有人投票,最后提出的候选获胜。如果没有候选,将随机选择一个部落。", parliamentRulesTerm: "政府标签显示当前政府和统计数据。", parliamentRulesMethods: "形式:无政府(简单多数)、民主(50%+1)、多数(80%)、少数(20%)、Karma 制(最高 Karma 提案)、独裁(即时通过)。", parliamentRulesAnarchy: "无政府是默认模式:如果在结算时没有候选被选出,宣布无政府状态。在无政府状态下,任何居民都可以提议法律。", @@ -1134,6 +1233,7 @@ module.exports = { parliamentRulesLaws: "当提案达到其门槛时,它成为法律并出现在法律标签中,附有生效日期。", parliamentRulesHistorical: "在历史标签中,你可以看到已发生的每个政府周期及其管理数据。", parliamentRulesLeaders: "在领袖标签中,你可以看到曾经执政(或作为候选人)的居民/部落排名,按效率排序。", + parliamentRulesHemicycle: "半圆议席图为每位居民分配一席:政府席(获胜候选的投票者与领袖)、每个其他候选各一个反对席,以及弃权席。虚线标记显示选举法定人数(人口的 25%)和当前方法的通过门槛。“按 LARP 家族”以家族为同一批席位着色。", parliamentProposalVoteStatusLabel: "投票状态", parliamentProposalOnTrackYes: "已达门槛", parliamentProposalOnTrackNo: "低于门槛", @@ -1147,11 +1247,8 @@ module.exports = { courtsCaseFormTitle: "开立案件", courtsCaseRespondent: "被告/答辩人", courtsCaseRespondentPh: "Oasis ID (@...) 或部落名称", - courtsCaseMediatorsAccuser: "调解员(原告方)", + courtsCaseSubjectPlaceholder: "主题或简短描述", courtsCaseMethod: "解决方式", - courtsCaseDescription: "描述(最多1000字)", - courtsCaseEvidenceTitle: "案件证据", - courtsCaseEvidenceHelp: "附加图片、音频、文档(PDF)或视频以支持你的案件。", courtsCaseSubmit: "提交案件", courtsNominateJudge: "提名法官", courtsJudgeId: "法官", @@ -1184,34 +1281,33 @@ module.exports = { courtsMethodDICTATOR: "独裁者", courtsMethodPOPULAR: "民意", courtsMethodKARMATOCRACY: "Karma 制", + courtsCaseType: "案件类型", + courtsPresetMinorConflict: "轻微冲突", + courtsPresetModerateConflict: "中度冲突", + courtsPresetSevereConflict: "严重冲突", + courtsPresetHarassment: "骚扰或虐待", + courtsPresetContentModeration: "内容审核", + courtsPresetBanRestriction: "封禁或限制", + courtsPresetGovernanceDispute: "治理争议", + courtsStatusOPEN: "开放", + courtsStatusIN_PROGRESS: "进行中", + courtsStatusDECIDED: "已裁决", + courtsStatusCLOSED: "已关闭", + courtsStatusSOLVED: "已解决", + courtsStatusUNSOLVED: "未解决", + courtsStatusDISCARDED: "已驳回", courtsRulesTitle: "法庭运作方式", courtsRulesIntro: "法庭是社区驱动的冲突解决和恢复性司法流程。优先考虑对话、清晰证据和适当补救。", courtsRulesLifecycle: "流程:1) 开立案件 2) 选择方式 3) 提交证据 4) 听证和审议 5) 裁决和补救 6) 执行和结案 7) 上诉(如适用)。", - courtsRulesRoles: "原告:开立案件。辩护方:被告人或部落。方式:社区选择的用于促进、评估证据和发布裁决的机制。证人:提供证词或证据。调解员:由原告和/或辩护方邀请的中立人士,可访问所有细节,帮助缓和冲突并共同达成协议。", + courtsRulesRoles: "原告:开立案件。辩护方:被告居民或部落。方式:社区选择的用于促进、评估证据和发布裁决的机制。证人:提供证词或证据。调解员:由原告和/或辩护方邀请的中立居民,可访问所有细节,帮助缓和冲突并共同达成协议。", courtsRulesEvidence: "描述不超过1000字。附加相关合法的图片、音频、视频和 PDF 文档。未经同意不要分享敏感私人数据。", courtsRulesDeliberation: "听证可以是公开的或私密的。法官确保尊重、要求澄清,并可能排除不相关或不合法的材料。", courtsRulesVerdict: "优先考虑修复:道歉、调解协议、内容审核、临时限制或其他适当措施。必须记录推理过程。", courtsRulesAppeals: "上诉:在有新证据或明显程序错误时允许。除非另有规定,必须在7天内提出。", - courtsRulesPrivacy: "尊重隐私和安全。人肉搜索、仇恨或威胁将被删除。法官可以编辑或封存记录的部分内容以保护有风险的人。", + courtsRulesPrivacy: "尊重隐私和安全。人肉搜索、仇恨或威胁将被删除。法官可以编辑或封存记录的部分内容以保护有风险的居民。", courtsRulesMisconduct: "骚扰、操纵或伪造证据可能导致立即不利裁决。", courtsRulesGlossary: "案件:冲突记录。证据:支持诉求的材料。裁决:附有补救措施的决定。上诉:请求审查裁决。", courtsFilterActions: "操作", - courtsNoActions: "你的角色没有待处理的操作。", - courtsCaseTitlePlaceholder: "冲突的简要描述", - courtsCaseSeverity: "严重程度", - courtsCaseSeverityNone: "无严重程度标签", - courtsCaseSeverityLOW: "低", - courtsCaseSeverityMEDIUM: "中", - courtsCaseSeverityHIGH: "高", - courtsCaseSeverityCRITICAL: "严重", - courtsCaseSubject: "主题", - courtsCaseSubjectNone: "无主题标签", - courtsCaseSubjectBEHAVIOUR: "行为", - courtsCaseSubjectCONTENT: "内容", - courtsCaseSubjectGOVERNANCE: "治理/规则", - courtsCaseSubjectFINANCIAL: "财务/资源", - courtsCaseSubjectOTHER: "其他", - courtsHiddenRespondent: "已隐藏(仅相关角色可见)。", courtsThRole: "角色", courtsRoleAccuser: "原告", courtsRoleDefence: "辩护方", @@ -1222,51 +1318,23 @@ module.exports = { courtsAssignJudgeBtn: "选择法官", trendingTitle: "热门", exploreTrending: "探索你网络中最受欢迎的内容。", - bookmarkButton: "书签", - transferButton: "转账", - eventButton: "活动", - taskButton: "任务", votesButton: "投票", - reportButton: "报告", - feedButton: "动态", - marketButton: "市场", - imageButton: "图片", - audioButton: "音频", - videoButton: "视频", - documentButton: "文档", - torrentButton: "种子", - noTrendingFound: "未找到热门内容。", - noContentMessage: "暂无热门内容。", - trendingDescription: "描述", - trendingDate: "日期", - trendingLocation: "位置", - trendingPrice: "价格", - trendingUrl: "URL", - trendingCategory: "类别", - trendingStart: "开始", - trendingEnd: "结束", - trendingPriority: "优先级", - trendingStatus: "状态", - trendingFrom: "发件人", - trendingTo: "收件人", - trendingConcept: "概念", - trendingAmount: "金额", - trendingDeadline: "截止日期", - trendingItemStatus: "项目状态", - trendingTotalVotes: "总票数", + shopProductButton: "商店", + podcastButton: "播客", + podcastEpisodeButton: "单集", + campaignButton: "活动", + logisticsRouteButton: "路线", trendingTotalOpinions: "总观点数", - trendingNoContentMessage: "暂无热门内容。", - trendingAuthor: "作者", - trendingCreatedAtLabel: "创建于", + trendingNoContentMessage: "暂时还没有热门内容。", trendingTotalCount: "总计", tasksTitle: "任务", tasksDescription: "发现和管理你网络中的任务。", + taskSearchPlaceholder: "在任务中搜索...", taskTitleLabel: "标题", taskDescriptionLabel: "描述", taskStartTimeLabel: "开始时间", taskEndTimeLabel: "结束时间", taskPriorityLabel: "优先级", - taskPrioritySelect: "选择优先级", taskPriorityUrgent: "紧急", taskPriorityHigh: "高", taskPriorityMedium: "中", @@ -1276,13 +1344,13 @@ module.exports = { taskVisibilityLabel: "可见性", taskPublic: "公开", taskPrivate: "私密", - taskCreatedAt: "创建于", - taskBy: "作者", taskStatus: "状态", taskStatusOpen: "开放", taskStatusInProgress: "进行中", taskStatusClosed: "已关闭", - taskAssignedTo: "分配给", + taskAssignedTo: "分配", + taskAssignedChip: "已认领", + taskUnassignedChip: "未认领", taskAssignees: "被分配者", taskAssignButton: "分配给我", taskUnassignButton: "取消分配", @@ -1290,12 +1358,12 @@ module.exports = { taskUpdateButton: "更新", taskDeleteButton: "删除", taskFilterAll: "全部", + taskFilterRecent: "最近", taskFilterMine: "我的", taskFilterOpen: "开放", taskFilterInProgress: "进行中", taskFilterClosed: "已关闭", taskFilterAssigned: "已分配", - taskFilterArchived: "已归档", taskFilterUrgent: "紧急", taskFilterHigh: "高", taskFilterMedium: "中", @@ -1308,35 +1376,47 @@ module.exports = { taskInProgressTitle: "进行中的任务", taskClosedTitle: "已关闭的任务", taskAssignedTitle: "已分配的任务", - taskArchivedTitle: "已归档的任务", - taskPublicTitle: "公开任务", - taskPrivateTitle: "私密任务", notasks: "没有可用的任务。", noLocation: "未指定位置", taskSetStatus: "设置状态", - eventTitle: "活动", eventDateLabel: "日期", + eventRecurrenceLabel: "重复", + eventRecurrenceUntil: "重复至", + eventRecurrenceHint: "一次性活动请留空结束日期。", + eventUpcomingDates: "接下来的日期", eventsTitle: "活动", eventsDescription: "发现和管理你网络中的活动。", - eventDescription: "描述", + eventSearchPlaceholder: "在活动中搜索...", eventPrice: "价格", eventStatus: "状态", - eventOrganizer: "组织者", eventAllSectionTitle: "活动", eventMineSectionTitle: "你的活动", - eventArchivedTitle: "已归档的活动", eventCreateSectionTitle: "创建活动", eventUpdateSectionTitle: "更新活动", - eventDeleteButton: "删除", eventCreateButton: "创建活动", eventAddToCalendar: "添加到日历", eventVisitCalendar: "查看日历", viewTask: "查看任务", + viewEvent: "查看活动", + viewPad: "查看协作文档", + viewMap: "查看地图", + viewCalendar: "查看日历", viewReport: "查看报告", viewTransfer: "查看转账", viewItem: "查看物品", viewShop: "查看商店", viewProject: "查看项目", + viewJob: "查看工作", + viewPlace: "查看地点", + generatePdf: "生成 PDF", + sharePm: "通过私信分享", + galleryImages: "图片(每张最大 75MB)", + galleryPhotos: "图片", + galleryAddPhoto: "添加图片", + galleryRemovePhoto: "移除", + galleryVideo: "视频(最大 75MB)", + galleryAddVideo: "添加视频", + galleryRemoveVideo: "移除视频", viewVotation: "查看投票", voteCastTitle: "投票", voteResults: "结果", @@ -1344,48 +1424,26 @@ module.exports = { eventDescriptionLabel: "描述", eventDescriptionPlaceholder: "输入活动描述...", eventUpdateButton: "更新", - eventAttendeesLabel: "参与者", - eventAttendeesPlaceholder: "输入参与者,用逗号分隔...", eventTagsLabel: "标签", - eventTagsPlaceholder: "输入活动标签,用逗号分隔...", - eventTags: "标签", eventPriceLabel: "价格", eventUrlLabel: "URL", eventAttendees: "参与者", - noAttendees: "暂无参与者", - eventCreatedAt: "创建于", eventLocation: "位置", eventLocationLabel: "位置", - eventNoLocation: "未指定位置", - eventNoURL: "未指定 URL", - eventBy: "作者", noevents: "没有可用的活动。", eventDate: "日期", - eventDateFormat: "YYYY年MM月DD日 HH:mm", eventAttendButton: "参加活动", - eventUnattendButton: "取消参加", - eventCreatedBy: "创建者", - eventAttendeesCount: "参与人数", - eventCreatedByYou: "你创建了此活动", eventFilterAll: "全部", + eventFilterRecent: "最近", eventFilterMine: "我的", eventFilterToday: "今天", eventFilterWeek: "本周", eventFilterMonth: "本月", eventFilterYear: "今年", eventFilterArchived: "已归档", - eventTodayTitle: "今天的活动", - eventThisWeekTitle: "本周的活动", - eventThisMonthTitle: "本月的活动", - eventThisYearTitle: "今年的活动", eventPrivacyLabel: "可见性", eventPublic: "公开", eventPrivate: "私密", - eventPublicTitle: "公开活动", - eventNoPrice: "免费活动", - eventNoImage: "未上传图片", - eventAttendConfirmation: "你现在正在参加此活动", - eventUnattendConfirmation: "你已取消参加此活动", eventAttended: "已参加", eventUnattended: "未参加", eventStatusOpen: "开放", @@ -1396,9 +1454,14 @@ module.exports = { tagsTopSectionTitle: "热门标签", tagsCloudSectionTitle: "标签云", tagsFilterAll: "全部", + tagsFilterMine: "我的", + tagsFilterRecent: "最新", + tagsMineSectionTitle: "我的标签", + tagsRecentSectionTitle: "最新标签", tagsFilterTop: "热门", tagsFilterCloud: "标签云", tagsNoItems: "没有可用的标签。", + tagsSearchPlaceholder: "在标签中搜索...", tagsTableHeaderTag: "标签/链接:", tagsTableHeaderCount: "计数:", transfersTitle: "转账", @@ -1406,7 +1469,10 @@ module.exports = { transfersFrom: "发起人", transfersTo: "接收人", transfersFilterAll: "全部", + transfersFilterRecent: "最近", transfersFilterMine: "我的", + transfersFilterMinePending: "待确认", + transfersFilterMineConfirmed: "已签署", transfersFilterUBI: "UBI", transfersFilterMarket: "市场", transfersFilterTop: "热门", @@ -1421,7 +1487,6 @@ module.exports = { transfersCategoryEconomic: "经济", transfersCategoryTime: "时间", transfersCategoryTrust: "信任", - transfersCategoryApply: "应用", transfersUnitEco: "ECO", transfersUnitHours: "小时", transfersUnitTrust: "信任", @@ -1438,18 +1503,12 @@ module.exports = { transfersDeadline: "截止日期", transfersTags: "标签", transfersStatus: "状态", - transfersStatusUnconfirmed: "未确认", - transfersStatusClosed: "已关闭", - transfersStatusDiscarded: "已废弃", - transfersCreatedAt: "创建于", transfersConfirmations: "确认", - transfersContainingBlock: "所在区块", - transfersExportContract: "创建合约", + transfersExportContract: "生成账单", transfersConfirmButton: "确认转账", transfersNoItems: "未找到转账。", transfersMineSectionTitle: "你的转账", transfersUBISectionTitle: "UBI 转账", - transfersMarketSectionTitle: "市场转账", transfersTopSectionTitle: "热门转账", transfersPendingSectionTitle: "待处理转账", transfersUnconfirmedSectionTitle: "未确认转账", @@ -1457,21 +1516,13 @@ module.exports = { transfersDiscardedSectionTitle: "已废弃转账", transfersCreateSectionTitle: "创建转账", transfersAllSectionTitle: "转账", - transfersFilterFavs: "收藏", - transfersFavsSectionTitle: "收藏的转账", - transfersSearchLabel: "搜索", - transfersSearchPlaceholder: "搜索概念、标签、用户...", + transfersSearchPlaceholder: "在转账中搜索...", transfersMinAmountLabel: "最低金额", transfersMaxAmountLabel: "最高金额", - transfersSortLabel: "排序", transfersSortRecent: "最近", transfersSortAmount: "最高金额", transfersSortDeadline: "最近截止", transfersSearchButton: "搜索", - transfersFavoriteButton: "收藏", - transfersUnfavoriteButton: "取消收藏", - transfersMessageUserButton: "消息", - transfersExpiringSoonBadge: "即将到期", transfersExpiredBadge: "已过期", transfersUpdatedAt: "已更新", transfersNoMatch: "没有匹配的转账。", @@ -1484,6 +1535,10 @@ module.exports = { voteClosedTitle: "已关闭的投票", voteAllSectionTitle: "投票", voteCreateButton: "创建投票", + voteErrorTitle: "请检查表单", + voteErrorDeadlineMin: "截止时间至少要在 {days} 天之后。", + voteErrorGeneric: "无法保存投票。", + voteLocked: "此投票已收到投票或意见,或属于议会或法院:无法再编辑或删除。", voteUpdateButton: "更新", voteDeleteButton: "删除", voteOptionYes: "赞成", @@ -1493,6 +1548,7 @@ module.exports = { voteFollowMajority: "跟随多数", voteNotInterested: "不感兴趣", voteFilterAll: "全部", + voteFilterRecent: "最近", voteFilterMine: "我的", voteFilterOpen: "进行中", voteFilterClosed: "已关闭", @@ -1508,37 +1564,28 @@ module.exports = { voteTags: "标签", voteOpinions: "观点", novotes: "没有可用的投票提案。", - voteBy: "作者", voteCreatedAt: "创建于", voteNoQuestion: "未提供问题", voteUnknownCreator: "未知创建者", voteUnknownDate: "未知日期", - errorVoteNotFound: "未找到投票", - errorAlreadyVoted: "你已经表达了意见。", - errorVoteClosedCannotEdit: "无法编辑已关闭的投票", - errorVoteDeadlinePassed: "此投票的截止日期已过", - errorRetrievingVote: "获取投票时出错", - errorCreatingVote: "创建投票时出错", - errorVoteAlreadyVoted: "意见提交后无法编辑", - errorDeletingOldVote: "删除旧意见时出错", - errorCreatingUpdatedVote: "创建更新的意见时出错", - errorCreatingTombstone: "创建墓碑时出错", voteDetailSectionTitle: '投票详情', voteCommentsLabel: '评论', voteCommentsForumButton: '公开讨论', voteCommentsSectionTitle: '公开讨论', voteNoCommentsYet: '还没有评论。成为第一个回复的人。', + commentsGoToLast: "跳到最后一条评论", voteNewCommentPlaceholder: '在此写下你的评论…', voteNewCommentButton: '发表评论', voteNewCommentLabel: '添加评论', cvTitle: "简历", - cvLabel: "简历 (CV)", cvEditSectionTitle: "编辑简历", cvCreateSectionTitle: "创建简历", cvDescription: "管理和分享你的专业技能和信息。", cvNameLabel: "全名", cvDescriptionLabel: "摘要", cvPhotoLabel: "照片", + cvPdfLabel: "简历 (PDF)", + cvVisitButton: "查看简历", cvPersonalExperiencesLabel: "个人经历", cvPersonalSkillsLabel: "个人技能(逗号分隔)", cvOasisExperiencesLabel: "Oasis 贡献经历", @@ -1551,29 +1598,130 @@ module.exports = { cvLocationLabel: "位置", cvStatusLabel: "状态", cvPreferencesLabel: "偏好", - cvOasisContributorLabel: "Oasis 贡献者", cvPersonal: "个人", cvOasis: "Oasis 贡献者(可选)", - cvOasisContributorView: "Oasis 贡献", + cvOasisContributorView: "贡献", cvEducational: "教育(可选)", cvEducationalView: "教育", cvProfessional: "专业(可选)", cvProfessionalView: "专业", cvAvailability: "可用性(可选)", - cvAvailabilityView: "可用性", cvUpdateButton: "更新", cvCreateButton: "创建简历", - cvContactLabel: "联系方式", cvCreatedAt: "创建于", cvUpdatedAt: "更新于", cvEditButton: "更新", cvDeleteButton: "删除", cvNoCV: "未找到简历。", blogSubject: "主题", - blogMessage: "正文限制为 8096 个字符。", - blogImage: "上传媒体(最大:50MB)", - blogPublish: "预览", - noPopularMessages: "还没有发布热门消息", + blogMessage: "消息", + blogImage: "上传媒体(最大:75MB)", + blogMedia: "附件(最多 8 张图片和 1 个视频)", + blogPublish: "发布", + pollsTitle: "投票", + dataTitle: "匹配", + dataDescription: "探索并可视化你的网络中的匹配。", + dataFilterAll: "全部", + dataFilterMine: "我的", + dataFilterRecent: "最近", + dataFilterTop: "最高", + dataKindInhabitants: "居民", + dataKindJobs: "工作", + dataKindProjects: "项目", + dataKindEvents: "活动", + dataKindTribes: "部落", + dataKindMarket: "市场", + dataKindHousing: "住房", + dataKindIndustry: "产业", + dataKindTasks: "任务", + dataKindReports: "报告", + dataKindVotes: "投票", + dataSearchPlaceholder: "在匹配中搜索...", + dataCohesionTitle: "凝聚系数 (CC)", + dataCcShort: "CC", + dataCohesionHint: "网络已发布内容之间的平均相近度。", + dataAffinity: "匹配度", + dataCommonTerms: "关键词", + dataReasonFollowing: "你正在支持这位居民", + dataReasonTribe: "与你同属一个部落", + dataReasonPinned: "与你置顶的内容相符", + dataReasonCv: "与你简历中的技能相符", + dataReasonContent: "与你发布的内容相关", + dataReasonRelated: "与你的兴趣相关", + dataReasonAlike: "由与你相似的居民发布", + dataReasonSupportsYou: "支持你", + dataReasonMutual: "相互支持", + dataReasonRated: "网络评价很高", + dataReasonNear: "在你附近", + dataStatPeople: "简历", + dataStatConnected: "已连接", + dataStatSkills: "技能", + dataBestMatch: "最佳匹配", + dataStatIsolated: "孤立", + dataStatEntities: "已比较的实体", + dataStatTerms: "不同词条", + dataStatPairs: "已连接的配对", + dataMatchesTitle: "匹配", + dataMatchesHint: "算法的个性化推荐。", + dataTermsTitle: "关键词", + dataTermsHint: "出现在最多内容中的词条。", + dataConnections: "关联匹配", + dataNoMatches: "没有可显示的交叉数据。", + dataNoProfile: "发布内容或填写简历,让算法了解你的兴趣。", + pollsDescription: "用于向网络提问并统计答案的模块。", + pollFilterAll: "全部", + pollFilterMine: "我的", + pollFilterRecent: "最近", + pollFilterTop: "热门", + pollFilterVoted: "已投票", + pollFilterOpen: "进行中", + pollFilterClosed: "已结束", + pollCreateButton: "创建投票", + pollCreateTitle: "创建投票", + pollEditTitle: "编辑投票", + pollQuestion: "问题", + pollQuestionPlaceholder: "你想问什么?", + pollOptions: "选项(每行一个)", + pollOutcome: "结果", + pollNoVotesYet: "尚无投票", + pollTie: "平局", + pollOptionsPlaceholder: "广场\n公园\n酒吧", + pollAnonymous: "匿名", + pollAnonymousLabel: "匿名投票", + pollMultiple: "多选", + pollMultipleLabel: "允许多个答案", + pollDeadline: "截止时间", + pollTags: "标签", + pollPublishButton: "发布投票", + pollUpdateButton: "更新", + pollCloseButton: "关闭", + pollDeleteButton: "删除", + pollVoteButton: "投票", + pollChangeVote: "修改投票", + pollVoters: "投票人", + pollComments: "评论", + pollStatusLabel: "状态", + pollStatusOpen: "进行中", + pollStatusClosed: "已结束", + pollSearchPlaceholder: "在投票中搜索...", + pollsNoItems: "未找到投票。", + viewPoll: "查看投票", + pollChatCreate: "创建投票", + pollInChat: "投票", + blogTitle: "博客", + blogDescription: "用于发现和管理博客的模块。", + blogFilterAll: "全部", + blogFilterMine: "我的", + blogFilterRecent: "最近", + blogFilterFavorites: "收藏", + blogFilterTop: "热门", + blogCreateButton: "创建博客", + blogSearchPlaceholder: "在博客中搜索...", + blogComments: "评论", + blogAllowComments: "允许评论", + blogCommentsClosed: "作者已关闭此博客的评论。", + blogNoItems: "未找到博客。", + viewBlog: "查看博客", forumTitle: "论坛", forumCategoryLabel: "类别", forumTitleLabel: "标题", @@ -1581,43 +1729,21 @@ module.exports = { forumCreateButton: "创建论坛", forumCreateSectionTitle: "创建论坛", forumDescription: "与你网络中的其他居民公开交流。", + forumSearchPlaceholder: "在论坛中搜索...", forumFilterAll: "全部", forumFilterMine: "我的", forumFilterRecent: "最近", forumFilterTop: "热门", - forumMineSectionTitle: "你的论坛", - forumRecentSectionTitle: "最近的论坛", - forumAllSectionTitle: "论坛", - forumDeleteButton: "删除", forumParticipants: "参与者", forumMessages: "消息", - forumLastMessage: "最后消息", forumMessageLabel: "消息", forumMessagePlaceholder: "写下你的消息...", forumSendButton: "发送", - forumVisitForum: "访问论坛", noForums: "未找到论坛。", - forumVisitButton: "访问论坛", - forumCatGENERAL: "综合", - forumCatOASIS: "Oasis", - forumCatLARP: "L.A.R.P.", - forumCatPOLITICS: "政治", - forumCatTECH: "科技", - forumCatSCIENCE: "科学", - forumCatMUSIC: "音乐", - forumCatART: "艺术", - forumCatGAMING: "游戏", - forumCatBOOKS: "书籍", - forumCatFILMS: "电影", - forumCatPHILOSOPHY: "哲学", - forumCatSOCIETY: "社会", - forumCatPRIVACY: "隐私", - forumCatCYBERWARFARE: "网络战争", - forumCatSURVIVALISM: "生存主义", + forumVisitButton: "查看论坛", + forumTopicLabel: "主题", imageTitle: "图片", imageDescription: "探索和管理你网络中的图片内容。", - imagePluginTitle: "标题", - imagePluginDescription: "描述", imageMineSectionTitle: "你的图片", imageCreateSectionTitle: "上传图片", imageUpdateSectionTitle: "更新图片", @@ -1626,17 +1752,14 @@ module.exports = { imageTopSectionTitle: "热门图片", imageFavoritesSectionTitle: "收藏", imageGallerySectionTitle: "画廊", - imageMemeSectionTitle: "梗图", imageFilterAll: "全部", imageFilterMine: "我的", imageFilterRecent: "最近", imageFilterTop: "热门", imageFilterFavorites: "收藏", imageFilterGallery: "画廊", - imageFilterMeme: "梗图", imageCreateButton: "上传图片", imageUpdateButton: "更新", - imageDeleteButton: "删除", imageAddFavoriteButton: "添加收藏", imageRemoveFavoriteButton: "取消收藏", imageFileLabel: "选择图片文件 (.jpeg, .jpg, .png, .gif)", @@ -1646,23 +1769,21 @@ module.exports = { imageTitlePlaceholder: "可选", imageDescriptionLabel: "描述", imageDescriptionPlaceholder: "可选", - imageMemeLabel: "标记为表情包", imageNoFile: "未提供图片文件", noImages: "没有可用的图片。", - imageSearchPlaceholder: "搜索标题、标签、描述、作者...", + imageSearchPlaceholder: "在图片中搜索...", imageSortRecent: "最近", imageSortOldest: "最早", imageSortTop: "最多投票", imageSearchButton: "搜索", - imageMessageAuthorButton: "消息", imageUpdatedAt: "已更新", imageNoMatch: "没有匹配的图片。", + feedSearchPlaceholder: "在动态中搜索...", feedTitle: "动态", createFeedTitle: "创建动态", createFeedButton: "发送动态!", feedPlaceholder: "有什么新鲜事?(最多280字)", TODAYButton: "今天", - CREATEButton: "创建动态", totalOpinions: "总观点数", moreVoted: "最多投票", noFeedsFound: "未找到动态。", @@ -1672,7 +1793,7 @@ module.exports = { alreadyRefeeded: "你已经转发过了。", activityTitle: "活动记录", yourActivity: "你的活动", - globalActivity: "全局活动", + globalActivity: "动态", activityList: "活动记录", activityDesc: "查看你网络中的最新活动。", allButton: "全部", @@ -1685,20 +1806,13 @@ module.exports = { concept: "概念", description: "描述", meme: "表情包", - activityContact: "联系人", - activityBy: "名称", - activityPixelia: "新增像素", - viewImage: "查看图片", - playAudio: "播放音频", - playVideo: "播放视频", typeRecent: "最近", - errorActivity: "获取活动时出错", + typeTop: "排行", typePost: "博客", - recentButton: "最近", + activityUpdateLabel: "更新", typeTribe: "部落", typeLarp: "L.A.R.P.", typeInhabitants: "居民", - activityProfileUpdated: "更新了个人资料", typeAbout: "居民", typeCurriculum: "简历", typeImage: "图片", @@ -1742,8 +1856,6 @@ module.exports = { typeCourtsSettlementAccepted: "法庭 · 和解接受", typeCourtsNomination: "法庭 · 提名", typeCourtsNominationVote: "法庭 · 提名投票", - activitySupport: "新联盟建立", - activityJoin: "加入新 PUB", question: "问题", deadline: "截止日期", status: "状态", @@ -1757,12 +1869,8 @@ module.exports = { date: "日期", category: "类别", attendees: "参与者", - activitySpread: "->", - visitLink: "访问链接", - viewDocument: "查看文档", location: "位置", contentWarning: "主题", - personName: "居民名称", bankWalletConnected: "ECOin 钱包", bankUbiReceived: "已收到 UBI", bankTx: "交易", @@ -1772,7 +1880,6 @@ module.exports = { link: "链接", activityProjectFollow: "%OASIS% 正在 %ACTION% 此项目 %PROJECT%", activityProjectUnfollow: "%OASIS% 正在 %ACTION% 此项目 %PROJECT%", - activityProjectPledged: "%OASIS% 已 %ACTION% %AMOUNT% 至项目 %PROJECT%", following: "关注中", unfollowing: "取消关注中", pledged: "已质押", @@ -1818,26 +1925,17 @@ module.exports = { courtsVotesSlashTotal: "赞成/总计", courtsOpenVote: "公开投票", courtsAnswerTitle: "答辩", - courtsStanceADMIT: "承认", - courtsStanceDENY: "否认", - courtsStancePARTIAL: "部分承认", - courtsStanceCOUNTERCLAIM: "反诉", - courtsStanceNEUTRAL: "中立", courtsVerdictResult: "结果", courtsVerdictOrders: "命令", courtsSettlementText: "和解", courtsSettlementAccepted: "已接受", - courtsSettlementPending: "待处理", courtsJudge: "法官", courtsThSupports: "支持", courtsFilterOpenCase: '开立案件', - courtsEvidenceFileLabel: '证据文件(图片、音频、视频或 PDF)', - courtsCaseMediators: '调解员', courtsCaseMediatorsPh: '调解员 Oasis ID,用逗号分隔', courtsMediatorsLabel: '调解员', courtsThCase: '案件', courtsThCreatedAt: '开始日期', - courtsThActions: '操作', courtsPublicPrefLabel: '解决后的可见性', courtsPublicPrefYes: '我同意此案件可完全公开', courtsPublicPrefNo: '我倾向于保持细节私密', @@ -1845,8 +1943,11 @@ module.exports = { courtsMethodMEDIATION: '调解', courtsNoCases: '没有案件。', reportsTitle: "报告", - reportsDescription: "管理和追踪你网络中与问题、漏洞、滥用和内容警告相关的报告。", + reportsDescription: "管理并跟踪你的网络中的报告。", + reportsSearchPlaceholder: "在报告中搜索...", reportsFilterAll: "全部", + reportsFilterRecent: "最新", + reportsFilterTop: "热门", reportsFilterMine: "我的", reportsFilterFeatures: "功能", reportsFilterBugs: "漏洞", @@ -1859,18 +1960,13 @@ module.exports = { reportsFilterInvalid: "无效", reportsCreateButton: "创建报告", reportsTitleLabel: "标题", - reportsDescriptionLabel: "描述", - reportsDescriptionPlaceholder: "请提供详细描述。", reportsCategory: "类别", - reportsCategoryLabel: "类别", reportsCategoryFeatures: "功能", reportsCategoryBugs: "漏洞", reportsCategoryAbuse: "滥用", - reportsCategoryContent: "内容问题", + reportsCategoryContent: "内容", reportsUpdateButton: "更新", reportsDeleteButton: "删除", - reportsDateLabel: "日期", - reportsUploadFile: "上传媒体(最大:50MB)", reportsMineSectionTitle: "你的报告", reportsFeaturesSectionTitle: "功能请求", reportsBugsSectionTitle: "漏洞", @@ -1878,11 +1974,6 @@ module.exports = { reportsContentSectionTitle: "内容问题", reportsAllSectionTitle: "报告", reportsNoItems: "没有可用的报告。", - reportsValidationTitle: "请输入有效的标题。", - reportsValidationDescription: "描述不能为空。", - reportsValidationCategory: "请选择一个类别。", - reportsCreatedAt: "创建于", - reportsCreatedBy: "作者", reportsSeverity: "严重程度", reportsSeverityLow: "低", reportsSeverityMedium: "中", @@ -1893,9 +1984,6 @@ module.exports = { reportsStatusUnderReview: "审查中", reportsStatusResolved: "已解决", reportsStatusInvalid: "无效", - reportsUpdateStatusButton: "更新状态", - reportsAnonymityOption: "匿名提交", - reportsAnonymousAuthor: "匿名", reportsConfirmButton: "确认报告!", reportsConfirmations: "确认", reportsConfirmedSectionTitle: "已确认的报告", @@ -1904,7 +1992,6 @@ module.exports = { reportsUnderReviewSectionTitle: "审查中的报告", reportsResolvedSectionTitle: "已解决的报告", reportsInvalidSectionTitle: "无效报告", - reportsTemplateSectionTitle: '报告模板', reportsBugTemplateTitle: '复现详情(漏洞)', reportsFeatureTemplateTitle: '详情(功能)', reportsAbuseTemplateTitle: '详情(滥用)', @@ -1943,6 +2030,20 @@ module.exports = { reportsRequestedActionLabel: '请求的操作', reportsRequestedActionPlaceholder: '删除、隐藏、标记、警告等。', tribesTitle: "部落", + tribeFilterAll: "全部", + tribeFilterRecent: "最近", + tribeFilterMine: "我的", + tribeFilterMembership: "成员", + tribeFilterSubtribes: "子部落", + tribeFilterTop: "热门", + tribeFilterGallery: "图库", + tribeFeedFilterTOP: "热门", + tribeFeedFilterMINE: "我的", + tribeFeedFilterALL: "全部", + tribeFeedFilterRECENT: "最近", + courtsStanceDENY: "否认", + courtsStanceADMIT: "承认", + courtsStancePARTIAL: "部分承认", tribeAllSectionTitle: "部落", tribeMineSectionTitle: "你的部落", tribeCreateSectionTitle: "创建部落", @@ -1950,24 +2051,15 @@ module.exports = { tribeGallerySectionTitle: "部落画廊", tribeRecentSectionTitle: "最近的部落", tribeTopSectionTitle: "热门部落", - tribeviewTribeButton: "访问部落", - tribeviewSubTribeButton: "访问子部落", tribeRootLabel: "根", tribeDescription: "探索或创建你网络中的部落。", - tribeFilterAll: "全部", - tribeFilterMine: "我的", - tribeFilterMembership: "成员资格", - tribeFilterRecent: "最近", - tribeFilterTop: "热门", - tribeFilterSubtribes: "子部落", - tribeFilterGallery: "画廊", tribeMainTribeLabel: "主部落", tribeCreateButton: "创建部落", tribeUpdateButton: "更新", tribeDeleteButton: "删除", - tribeImageLabel: "上传媒体(最大:50MB)", + tribeImageLabel: "上传媒体(最大:75MB)", tribeTitleLabel: "标题", - searchTribesPlaceholder: "按名称筛选部落...", + searchTribesPlaceholder: "在部落中搜索...", tribeTitlePlaceholder: "部落名称", tribeDescriptionLabel: "描述", tribeDescriptionPlaceholder: "描述这个部落", @@ -1979,13 +2071,17 @@ module.exports = { tribeModeLabel: "模式", tribeIsAnonymousLabel: "状态", tribeMembersCount: "成员", - tribeInviteCodePlaceholder: "输入邀请码", - tribeJoinByCodeButton: "使用邀请码加入", - tribeJoinButton: "加入", tribeEnterInvite: "输入邀请码", + subscriptionTitle: "订阅", + subscriptionOn: "已订阅", + subscriptionOff: "未订阅", + subscriptionSubscribe: "订阅", + subscriptionUnsubscribe: "取消订阅", + subscriptionSubscribers: "订阅者", + pmListLabel: "发送至", + pmListNone: "— 无列表 —", + pmListNoSubscribers: "没有订阅者", tribeLeaveButton: "离开", - tribeYes: "是", - tribeNo: "否", tribePublic: "公开", tribePrivate: "私密", tribeGenerateInvite: "单次邀请", @@ -1994,13 +2090,8 @@ module.exports = { tribeRemoveInvitation: "移除邀请", tribeCreatedAt: "创建于", tribeAuthor: "作者", - tribeAuthorLabel: "作者", tribeStrict: "严格", tribeOpen: "开放", - tribeFeedFilterRECENT: "最近", - tribeFeedFilterMINE: "我的", - tribeFeedFilterALL: "全部", - tribeFeedFilterTOP: "热门", tribeFeedRefeeds: "转发", tribeFeedRefeed: "转发", tribeFeedMessagePlaceholder: "写一条动态…", @@ -2010,33 +2101,28 @@ module.exports = { tribeNotFound: "未找到部落!", createTribeTitle: "创建部落", updateTribeTitle: "更新部落", - tribeSectionOverview: "概览", tribeSectionInhabitants: "居民", tribeSectionVotations: "投票", tribeSectionEvents: "活动", - tribeSectionReports: "报告", tribeSectionTasks: "任务", tribeSectionFeed: "动态", tribeSectionForum: "论坛", - tribeSectionMarket: "市场", - tribeSectionJobs: "工作", - tribeSectionProjects: "项目", tribeSectionMedia: "媒体", tribeSectionImages: "图片", tribeSectionAudios: "音频", tribeSectionVideos: "视频", tribeSectionDocuments: "文档", tribeSectionBookmarks: "书签", - tribeSectionMaps: "MAPS", - tribeSectionPads: "PADS", - tribeSectionChats: "CHATS", - tribeSectionCalendars: "CALENDARS", - tribePadCreate: "Create Pad", - tribeChatCreate: "Create Chat", - tribeCalendarCreate: "Create Calendar", - tribePadsEmpty: "No pads, yet.", - tribeChatsEmpty: "No chats, yet.", - tribeCalendarsEmpty: "No calendars, yet.", + tribeSectionMaps: "地图", + tribeSectionPads: "协作板", + tribeSectionChats: "聊天", + tribeSectionCalendars: "日历", + tribePadCreate: "创建协作板", + tribeChatCreate: "创建聊天", + tribeCalendarCreate: "创建日历", + tribePadsEmpty: "暂无协作板。", + tribeChatsEmpty: "暂无聊天。", + tribeCalendarsEmpty: "暂无日历。", tribeInhabitantsEmpty: "此部落中还没有居民。", tribeEventCreate: "创建活动", tribeEventsEmpty: "还没有活动。", @@ -2058,11 +2144,6 @@ module.exports = { tribeTaskStatusClosed: "关闭", tribeTaskAssign: "分配", tribeTaskUnassign: "取消分配", - tribeReportCreate: "创建报告", - tribeReportsEmpty: "还没有报告。", - tribeReportTitle: "标题", - tribeReportDescription: "描述", - tribeReportCategory: "类别", tribeVotationCreate: "创建投票", tribeVotationsEmpty: "还没有投票。", tribeVotationTitle: "标题", @@ -2080,27 +2161,6 @@ module.exports = { tribeForumCategory: "类别", tribeForumReply: "回复", tribeForumReplies: "回复", - tribeMarketCreate: "创建商品", - tribeMarketEmpty: "还没有商品。", - tribeMarketTitle: "标题", - tribeMarketDescription: "描述", - tribeMarketPrice: "价格", - tribeMarketImage: "图片", - tribeMarketCategory: "类别", - tribeJobCreate: "创建工作", - tribeJobsEmpty: "还没有工作。", - tribeJobTitle: "标题", - tribeJobDescription: "描述", - tribeJobLocation: "位置", - tribeJobSalary: "薪资", - tribeJobDeadline: "截止日期", - tribeProjectCreate: "创建项目", - tribeProjectsEmpty: "还没有项目。", - tribeProjectTitle: "标题", - tribeProjectDescription: "描述", - tribeProjectGoal: "目标", - tribeProjectFunded: "已筹资", - tribeProjectDeadline: "截止日期", tribeMediaUpload: "上传媒体", readDocument: "阅读文档", tribeCreateImage: "创建图片", @@ -2111,21 +2171,20 @@ module.exports = { tribeMediaEmpty: "还没有媒体。", tribeMediaTitle: "标题", tribeMediaDescription: "描述", - tribeMediaType: "类型", tribeMediaTypeImage: "图片", tribeMediaTypeVideo: "视频", tribeMediaTypeAudio: "音频", tribeMediaTypeDocument: "文档", tribeMediaTypeBookmark: "书签", tribeContentDelete: "删除", + tribeReachTribe: "部落", + tribeReachNotice: "OASIS 会向所有居民显示,CLEARNET 还会在开放网络上显示。公开后会在部落之外留下可读副本:改回部落会将其隐藏,但已经分享出去的副本无法删除。", + tribeReachPrivate: "私密部落的内容只能留在部落内部。", + tribeReachOpenedBy: "公开者", + tribeReachChange: "发布位置", tribeInviteCodeText: "邀请码:", oasisVersionLabel: "Oasis 版本", - tribeInviteCodeHint: "将此邀请码分享给你想邀请的人,他们可在 /invites → Tribes 加入。", - tribeGroupTribe: "部落", - tribeGroupOffice: "办公", - tribeGroupNetwork: "网络", - tribeGroupEconomy: "经济", - tribeGroupMedia: "媒体", + tribeInviteCodeHint: "将此邀请码分享给你想邀请的人,他们可在 /invites 加入。", tribeStatusOpen: "开放", tribeStatusClosed: "已关闭", tribeStatusInProgress: "进行中", @@ -2135,33 +2194,17 @@ module.exports = { tribePriorityCritical: "严重", tribeTaskFilterAll: "全部", tribeMediaFilterAll: "全部", - tribeReportCatBug: "漏洞", - tribeReportCatAbuse: "滥用", - tribeReportCatContent: "内容", - tribeReportCatOther: "其他", tribeForumCatGeneral: "综合", tribeForumCatProposal: "提案", tribeForumCatQuestion: "问题", tribeForumCatAnnouncement: "公告", - tribeMarketCatGoods: "商品", - tribeMarketCatServices: "服务", - tribeMarketCatFood: "食品", - tribeMarketCatOther: "其他", tribeStatusLabel: "状态", tribeSubTribes: "子部落", tribeSubTribesCreate: "创建子部落", tribeSubTribesStrictDenied: "部落的严格模式不允许您创建新的子部落。请联系管理员。", - tribeSubTribesEmpty: "还没有创建子部落。", - tribeActivityJoined: "已加入", - tribeActivityLeft: "已离开", - tribeActivityFeed: "动态", tribeActivityRefeed: "转发", - tribeGroupAnalytics: "分析", - tribeGroupCreative: "创意", tribeSectionActivity: "活动记录", tribeSectionTrending: "热门", - tribeSectionOpinions: "观点", - tribeSectionPixelia: "PIXELIA", tribeSectionTags: "标签", tribeSectionSearch: "搜索", tribeActivityEmpty: "还没有活动。", @@ -2173,25 +2216,13 @@ module.exports = { tribeTrendingPeriodWeek: "本周", tribeTrendingPeriodAll: "全部时间", tribeTrendingEngagement: "参与度", - tribeOpinionsEmpty: "还没有观点。", - tribeOpinionsCast: "投票", - tribeOpinionsRankings: "排名", - tribeOpinionsAlreadyVoted: "已投票", tribeTopCategory: "最多投票", - tribePixeliaTitle: "Pixelia", - tribePixeliaDescription: "协作像素艺术画布。", - tribePixeliaPaint: "画", - tribePixeliaContributors: "贡献者", - tribePixeliaTotalPixels: "已绘制像素", tribeTagsEmpty: "未找到标签。", - tribeTagsCloud: "标签云", - tribeTagsContentWith: "带有标签的内容", - tribeSearchPlaceholder: "搜索部落内容...", tribeSearchEmpty: "未找到结果。", - tribeSearchResults: "结果", tribeSearchMinChars: "请输入至少2个字符进行搜索。", agendaTitle: "日程", agendaDescription: "在这里你可以找到所有分配给你的项目。", + agendaSearchPlaceholder: "在日程中搜索...", agendaFilterAll: "全部", agendaFilterToday: "今天", agendaFilterUpcoming: "即将到来", @@ -2205,23 +2236,15 @@ module.exports = { agendaFilterReports: "报告", agendaFilterTransfers: "转账", agendaFilterJobs: "工作", + agendaFilterIndustry: "工业", + agendaFilterHousing: "住所", agendaFilterProjects: "项目", agendaFilterCalendars: "日历", agendaNoItems: "未找到分配项目。", - agendaAuthor: "作者", agendaDiscardButton: "丢弃", agendaRestoreButton: "恢复", - agendaCreatedAt: "创建于", - agendaTitleLabel: "标题", + agendaRemoveButton: "移除", agendaMembersCount: "成员", - agendaDescriptionLabel: "描述", - agendaStatus: "状态", - agendaVisibility: "可见性", - agendaEventDate: "活动日期", - agendaEventLocation: "位置", - agendaEventPrice: "价格", - agendaEventUrl: "URL", - agendaTaskStart: "开始时间", agendaLocationLabel: "位置", agendaYes: "是", agendaNo: "否", @@ -2231,11 +2254,6 @@ module.exports = { agendareportCategory: "类别", agendareportSeverity: "严重程度", agendareportStatus: "状态", - agendareportDescription: "描述", - agendaTaskEnd: "结束时间", - agendaTaskPriority: "优先级", - agendaTransferFrom: "发起人", - agendaTransferTo: "接收人", agendaTransferConcept: "概念", agendaTransferAmount: "金额", agendaTransferDeadline: "截止日期", @@ -2245,47 +2263,7 @@ module.exports = { voteNow: "立即投票", alreadyVoted: "你已经表达了意见。", noOpinionsFound: "未找到观点。", - RECENTButton: "最近", TOPButton: "热门", - interestingButton: "有趣", - necessaryButton: "必要", - funnyButton: "搞笑", - disgustingButton: "恶心", - sensibleButton: "明智", - propagandaButton: "宣传", - adultOnlyButton: "仅限成人", - boringButton: "无聊", - confusingButton: "令人困惑", - usefulButton: "有用", - informativeButton: "信息丰富", - wellResearchedButton: "研究充分", - accurateButton: "准确", - needsSourcesButton: "需要来源", - wrongButton: "错误", - lowQualityButton: "低质量", - creativeButton: "有创意", - insightfulButton: "有洞察力", - actionableButton: "可操作", - inspiringButton: "鼓舞人心", - loveButton: "喜爱", - clearButton: "清晰", - upliftingButton: "振奋人心", - unnecessaryButton: "不必要", - rejectedButton: "被拒绝", - misleadingButton: "误导", - offTopicButton: "离题", - duplicateButton: "重复", - clickbaitButton: "标题党", - spamButton: "垃圾信息", - trollButton: "恶搞", - nsfwButton: "NSFW", - violentButton: "暴力", - toxicButton: "有毒", - harassmentButton: "骚扰", - hateButton: "仇恨", - scamButton: "诈骗", - triggeringButton: "触发性", - opinionsCreatedAt: "创建于", opinionsTotalCount: "总观点数", voteInteresting: "有趣", voteNecessary: "必要", @@ -2322,29 +2300,41 @@ module.exports = { voteCreative: "有创意", voteSpam: "垃圾信息", voteAdultOnly: "仅限成人", - publishBlog: "发布博客", privateMessage: "私信", pmSendTitle: "私信", pmSend: "发送!", - pmDescription: "使用此表单向其他居民发送加密消息。", + pmCancel: "取消", + pmReset: "重置", + pmSharedKeyHint: "请通过其他渠道将此密钥分享给收件人。解密时需要用到。", + pmDescription: "使用此表单向其他居民发送加密的消息/文件。", + pmComposeTitle: "发送消息", pmRecipients: "收件人", pmRecipientsHint: "输入 Oasis ID,用逗号分隔", - pmLevelOfExposition: "Level of exposition", - pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", + pmLimitsHint: "每条消息最多 7 位居民。", + pmTextPlaceholder: "写下你的消息。", pmSubject: "主题", pmSubjectHint: "输入消息主题", pmText: "消息", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "解密", + fileShareTitle: "分享文件", + fileShareRecipientPlaceholder: "@...=.ed25519", + fileShareFileLabel: "文件", + fileShareDownload: "下载", + fileShareMutualError: "你只能与相互支持的居民分享文件。", + fileShareNoFile: "未选择文件。", + fileShareTooLarge: "文件超过允许的大小。", + fileShareFailed: "无法准备文件。", + fileShareSendError: "无法发送文件。", + fileShareUnavailable: "该文件当前不可用。请稍后再试。", pmCrypterBadKey: "共享密钥不正确!", pmCrypterTooLong: "消息过长,无法用 Crypter 加密。请缩短后重试。", pmCrypterCipherLabel: "重新加密的消息", pmCrypterCharsLabel: "字符", pmInvalidRecipients: "无效的 Oasis ID(应形如 @…=.ed25519)。", pmEncryptionLabel: "加密", - pmFile: "附件", - private: "私密", - privateDescription: "你的加密消息。", + private: "收件箱", + privateDescription: "管理你的私信。", privateInbox: "收件箱", privateSent: "已发送", privateDelete: "删除", @@ -2352,19 +2342,27 @@ module.exports = { noPrivateMessages: "没有私信。", pmFromLabel: "发件人:", pmToLabel: "收件人:", - pmInvalidMessage: "无效消息", pmNoSubject: "(无主题)", pmSubjectLabel: "主题:", - pmBodyLabel: "正文", - pmBotJobs: "42-JobsBOT", - pmBotProjects: "42-ProjectsBOT", - pmBotMarket: "42-MarketBOT", + pmBotJobs: "JobsBot", + pmBotProjects: "ProjectsBot", + pmBotMarket: "MarketBot", + pmBotShops: "ShopsBot", + pmBotPolitical: "PoliticalBot", + politicalBotLarpTitle: "L.A.R.P 的执政家族已更替。", + politicalBotParliamentTitle: "议会已开始新的执政周期。", + politicalBotTribeTitle: "你所在的某个部落已开始新的执政周期。", + politicalBotLarpIntro: "现在由 {house} 在本周期执政:{cycle}", + politicalBotParliamentIntro: "当前政府为 {gov}", + politicalBotParliamentIntroLeader: "当前政府为 {gov},由 {leader} 行使", + politicalBotTribeIntro: "部落 {tribe} 当前政府为 {gov}", + politicalBotTribeIntroLeader: "部落 {tribe} 当前政府为 {gov},由 {leader} 行使", + politicalBotLeaderLabel: "领袖", inboxJobSubscribedTitle: "你的工作机会有新订阅", pmInhabitantWithId: "Oasis ID 为以下的居民:", pmHasSubscribedToYourJobOffer: "已订阅你的工作机会", - inboxProjectCreatedTitle: "新项目已创建", - pmHasCreatedAProject: "已创建一个项目", inboxMarketItemSoldTitle: "商品已售出", + inboxShopSoldTitle: "商品已售出", pmYourItem: "你的商品", pmHasBeenSoldTo: "已售出给", pmFor: "售价", @@ -2372,7 +2370,6 @@ module.exports = { pmHasPledged: "已质押", pmToYourProject: "至你的项目", blockchain: '区块浏览器', - blockchainTitle: '区块浏览器', blockchainDescription: '探索和可视化区块链中的区块。', blockchainNoBlocks: '区块链中未找到区块。', blockchainStatsTitle: '统计', @@ -2384,42 +2381,36 @@ module.exports = { blockchainBlockCarbon: '碳足迹', blockchainBlockType: '类型', blockchainBlockTimestamp: '时间戳', - blockchainBlockContent: '区块', - blockchainBlockURL: 'URL:', - blockchainContent: '区块', - blockchainContentPreview: '区块内容预览', blockchainLatestDatagram: '最新数据报', - blockchainDatagram: '数据报', - blockchainDetails: '查看区块详情', - blockchainViewBlockexplorer: "在区块浏览器中查看", - blockchainBlockInfo: '区块信息', - blockchainBlockDetails: '所选区块的详情', + blockchainDatagram: "数据报", + blockchainDetails: "查看区块详情", + blockchainViewBlockexplorer: "在 Blockexplorer 中查看", blockchainBack: '返回区块浏览器', blockchainContentDeleted: "此内容已被标记为墓碑", - visitContent: "访问内容", + visitContent: "查看内容", + favoriteAdd: "固定到收藏", + pmContentTooltip: "发送私信", + phoneCallTooltip: "拨打电话", + favoriteRemove: "从收藏移除", + reportContent: "举报内容", banking: '银行', bankingTitle: '银行', - bankingDescription: '探索 ECOin 的当前价值和相应的 UBI 分配,根据参与度和信任度按纪元分配。', + bankingDescription: "ECOin 集体经济。", bankOverview: '概览', bankEpochs: '纪元', bankRules: '规则', pending: '待处理', closed: '已关闭', - bankBack: '返回银行', - bankViewTx: '查看交易', bankClaimNow: '立即领取', - bankClaimUBI: '领取 UBI!', - bankClaimAndPay: 'Claim & Pay', - bankClaimedPending: 'Claim pending...', - bankStatusUnclaimed: 'Unclaimed', - bankStatusClaimed: 'Claimed', - bankStatusExpired: 'Expired', - bankPubOnly: 'PUB-only operation', - bankNoPendingUBI: '本期无待领取的 UBI 分配。', + bankClaimUBI: '领取 UBI', + bankClaimAndPay: "领取并支付", + bankClaimedPending: "领取处理中...", + bankStatusClaimed: "已领取", + bankStatusExpired: "已过期", + bankPubOnly: "仅限 PUB 的操作", bankEpoch: '纪元', bankPool: '资金池(本纪元)', bankWeightsSum: '权重总和', - bankAllocations: '分配', bankNoAllocations: '未找到分配。', bankNoEpochs: '未找到纪元。', bankEpochAllocations: '纪元分配', @@ -2433,16 +2424,21 @@ module.exports = { bankEpochId: '纪元 ID', bankRuleHash: '规则快照哈希', bankViewEpoch: '查看纪元', + bankYourIndustryBalance: "你的工业份额", + bankYourUbiMonth: "你本月的全民基本收入", + bankYourFundsMonth: "资金合计", + bankIndustryBalance: "工业产值", bankUserBalance: '你的余额', - ecoWalletNotConfigured: '未配置 ECOin 钱包', - editWallet: '编辑钱包', - addWallet: '添加钱包', bankAddresses: '地址', bankNoAddresses: '未找到地址。', bankUser: 'Oasis ID', bankAddress: '地址', bankAddAddressTitle: '添加 ECOIN 地址', bankAddAddressUser: 'Oasis ID', + bankAddAddressLabel: "标签(可选)", + bankAddAddressLabelPlaceholder: "Bob 的钱包、Alice 的商店…", + bankAddAddressUserOptional: "Oasis ID(可选)", + bankAddressUnnamed: "未命名地址", bankAddAddressAddress: 'ECOIN 地址', bankAddAddressSave: '保存', bankAddressAdded: '地址已添加', @@ -2460,19 +2456,11 @@ module.exports = { bankAddressDeleteConfirm: '删除此地址?', search: '搜索!', bankLocal: '本地', - bankFromOasis: 'Oasis', - bankMyAddress: '你的地址', - bankRemoveMyAddress: '移除我的地址', - bankNotRemovableOasis: '无法在本地移除地址', - bankingFutureUBI: "UBI", - pubIdTitle: "PUB Wallet", - pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", + bankFromOasis: "网络", pubIdLabel: "PUB ID", - pubIdSave: "Save configuration", - pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "资金不足!", bankUbiAvailableOk: "可用!", - bankUbiAvailability: "UBI 可用性", + bankUbiAvailability: "UBI", bankAlreadyClaimedThisMonth: "本月已领取", bankUbiThisMonth: "UBI(本月)", bankUbiLastClaimed: "UBI(上次领取)", @@ -2486,13 +2474,6 @@ module.exports = { shopsTitle: "商店", shopDescription: "发现和管理网络中的商店。", shopTitle: "商店", - shopFilterAll: "全部", - shopFilterMine: "我的", - shopFilterRecent: "最近", - shopFilterTop: "热门", - shopFilterProducts: "产品", - shopFilterPrices: "价格", - shopFilterFavorites: "收藏", shopUpload: "创建商店", shopAllSectionTitle: "所有商店", shopMineSectionTitle: "我的商店", @@ -2509,19 +2490,21 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", - shopClearnetPublish: "发布到 Clearnet", - shopClearnetUnpublish: "从 Clearnet 撤下", - shopClearnetUrlLabel: "Clearnet 网址", - shopClearnetWarning: "发布到 Clearnet 后,本店铺可被外部搜索引擎和爬虫索引。", + transportChipLabel: "传输加密", shopAddFavorite: "添加收藏", shopRemoveFavorite: "取消收藏", shopOpen: "开放", shopClosed: "关闭", - shopOpenShop: "开放商店", - shopCloseShop: "关闭商店", shopMakePrivate: "设为私密 (E2E)", shopMakePublic: "设为公开", shopProducts: "产品", + shopFilterAll: "全部", + shopFilterRecent: "最近", + shopFilterMine: "我的", + shopFilterTop: "热门", + shopFilterProducts: "产品", + shopFilterPrices: "价格", + shopFilterFavorites: "收藏", shopProductAdd: "添加产品", shopProductTitle: "产品", shopProductUpdate: "更新产品", @@ -2546,85 +2529,63 @@ module.exports = { shopOrderPrice: "价格", shopOrderBuyer: "买家", shopBackToShop: "返回商店", - shopShareUrl: "分享链接", - shopVisitShop: "访问商店", marketVisitItem: "查看商品", shopStatus: "状态", shopCreatedAt: "创建时间", - shopSearchPlaceholder: "搜索商店...", + shopSearchPlaceholder: "在商店中搜索...", shopUrl: "网址", shopLocation: "位置", shopTags: "标签", shopVisibility: "可见性", - shopImage: "图片", shopShortDescription: "简短描述", shopShortDescriptionPlaceholder: "店铺卡片简短描述(最多160个字符)", shopTitlePlaceholder: "您的店铺名称", shopDescriptionPlaceholder: "您的店铺详细描述", - shopUrlPlaceholder: "https://您的店铺网址.com", shopLocationPlaceholder: "城市,国家", - shopTagsPlaceholder: "标签1, 标签2, 标签3", + shopTagsPlaceholder: "输入标签,用逗号分隔", mapTitlePlaceholder: "地图标题", shopProductFeatured: "推荐产品", - shopSendToMarket: "Send to Market", + shopSendToMarket: "发送到市场", typeShop: "商店", typeShopProduct: "店铺商品", bankExchange: '交易所', bankExchangeCurrentValue: 'ECOin 价值(1小时)', - bankTotalSupply: 'ECOin 总供应量', - bankEcoinHours: "ECOin 时间等价", + bankTotalSupply: "ECOin 总供应量", + bankPubsSupply: "PUB 资金池的 ECOin 供应量", + bankHoldingSupply: "UBI 之外的 ECOin 供应量", + bankEcoinHours: "ECOin 时间等价 (1 ECO)", bankHoursOfWork: '小时', bankUnitMs: "毫秒", bankUnitSeconds: "秒", bankUnitMinutes: "分钟", bankUnitDays: "天", bankExchangeNoData: '无可用数据', - bankExchangeIndex: 'ECOin 价值(1小时)', - bankInflation: 'ECOin 通胀率 (anual)', - bankInflationMonthly: 'ECOin 通胀率 (mensual)', + bankInflation: "ECOin 通胀率 (年)", + bankInflationMonthly: "ECOin 通胀率 (月)", bankExchangeChartTitle: 'ECOin 价值随时间的演进', bankExchangeChartValue: '价值 (ECO/小时)', bankExchangeChartSupply: '供应量', bankExchangeChartInflation: '通胀率 %', bankExchangeChartEmpty: '采样不足 — 稍后再来', - bankCurrentSupply: 'ECOin 当前供应量', + bankCurrentSupply: "ECOin 当前供应量", bankingSyncStatus: 'ECOin 状态', bankingSyncStatusSynced: '已同步', - bankingSyncStatusOutdated: '已过时', + bankingSyncStatusOutdated: "未同步", statsTitle: '统计', statistics: "统计", - statsInhabitant: "居民统计", statsDescription: "发现你网络的统计数据。", ALLButton: "全部", + RECENTButton: "最近", MINEButton: "我的", TOMBSTONEButton: "墓碑", - statsYou: "你", - statsUserId: "Oasis ID", statsCreatedAt: "创建于", statsYourContent: "内容", statsYourOpinions: "观点", - statsYourTombstone: "墓碑", - statsNetwork: "网络", - statsTotalInhabitants: "居民", - statsDiscoveredTribes: "部落(公开)", - statsPrivateDiscoveredTribes: "部落(私密)", statsNetworkContent: "内容", - statsYourMarket: "市场", - statsYourJob: "工作", - statsYourProject: "项目", - statsYourTransfer: "转账", - statsYourForum: "论坛", statsNetworkOpinions: "观点", - statsDiscoveredMarket: "市场", - statsDiscoveredJob: "工作", - statsDiscoveredProject: "项目", - statsBankingTitle: "银行", - statsEcoWalletLabel: "ECOIN 钱包", + statsEcoWalletLabel: "ECOin 钱包", + walletAddressCopyHint: "点击选中后复制", statsEcoWalletNotConfigured: "未配置!", - statsTotalEcoAddresses: "地址总数", - statsDiscoveredTransfer: "转账", - statsDiscoveredForum: "论坛", - statsNetworkTombstone: "墓碑", statsBookmark: "书签", statsEvent: "活动", statsTask: "任务", @@ -2645,17 +2606,15 @@ module.exports = { statsTransfer: "转账", statsAiExchange: "AI", statsPUBs: 'PUB', + statsSyncedPeers: "节点", + statsTotalLabel: "总数", statsPost: "帖子", - statsOasisID: "Oasis ID", statsSize: "总计(大小)", statsBlockchainSize: "区块链(大小)", statsBlobsSize: "Blob(大小)", statsActivity7d: "活动(最近7天)", statsActivity7dTotal: "7天总计", statsActivity30dTotal: "30天总计", - statsKarmaScore: "KARMA 评分", - statsPublic: "公开", - statsPrivate: "私密", day: "日", messages: "消息", statsProject: "项目", @@ -2667,30 +2626,14 @@ module.exports = { statsProjectsCancelled: "已取消", statsProjectsGoalTotal: "目标总额", statsProjectsPledgedTotal: "质押总额", - statsProjectsSuccessRate: "成功率", - statsProjectsAvgProgress: "平均进度", - statsProjectsMedianProgress: "中位数进度", - statsProjectsActiveFundingAvg: "平均活跃筹资", - statsJobsTitle: "工作", - statsJobsTotal: "工作总数", - statsJobsOpen: "开放", - statsJobsClosed: "已关闭", - statsJobsOpenVacants: "开放职位", - statsJobsSubscribersTotal: "订阅者总数", - statsJobsAvgSalary: "平均薪资", - statsJobsMedianSalary: "中位数薪资", statsMarketTitle: "市场", statsMarketTotal: "商品总数", statsMarketForSale: "在售", statsMarketReserved: "已预订", statsMarketClosed: "已关闭", statsMarketSold: "已售出", - statsMarketRevenue: "收入", - statsMarketAvgSoldPrice: "平均售价", statsUsersTitle: "居民", user: "居民", - statsTombstoneTitle: "墓碑", - statsNetworkTombstones: "网络墓碑", statsTombstoneRatio: "墓碑比率 (%)", statsParliamentCandidature: "议会候选", statsParliamentTerm: "议会任期", @@ -2716,31 +2659,28 @@ module.exports = { aiPrompt: "提供信息丰富且精确的回答。", aiConfiguration: "设置提示语", aiPromptUsed: "提示语", - aiClearHistory: "清除聊天记录", - aiSharePrompt: "将此回答添加到集体训练?", - aiShareYes: "是", - aiShareNo: "否", - aiSharedLabel: "已添加到训练", - aiRejectedLabel: "未添加到训练", - aiServerError: "AI 无法回答。请重试。", + aiClearHistory: "清除历史", + aiServerError: "我无法正常回答,请再试一次。", + aiStatusLoading: "42 正在加载模型,请稍后再问。", + aiThinking: "思考中", + aiWaiting: "等待中", + aiStatusMissingModel: "AI 模型未安装:缺少 src/AI/oasis-42-1-chat.Q4_K_M.gguf。", + aiExportFineTuning: "导出微调数据", + aiFactsTitle: "来自你网络的数据", aiInputPlaceholder: "什么是 Oasis?", typeAiExchange: "AI", aiApproveTrain: "添加到集体训练", aiRejectTrain: "不训练", - aiTrainPending: "待批准", aiTrainApproved: "已批准训练", aiTrainRejected: "已拒绝训练", aiSnippetsUsed: "使用的片段", aiSnippetsLearned: "已学习的片段", statsAITraining: "AI 训练", - aiApproveCustomTrain: "使用此自定义回答训练", aiCustomAnswerPlaceholder: "写下你的自定义回答…", - statsAIExchanges: "模型交流", marketMineSectionTitle: "你的商品", marketCreateSectionTitle: "创建商品", marketUpdateSectionTitle: "更新", marketAllSectionTitle: "市场", - marketRecentSectionTitle: "最近的市场", marketTitle: "市场", marketDescription: "在你的网络中交换商品或服务的市场。", marketFilterAll: "全部", @@ -2755,7 +2695,7 @@ module.exports = { marketFilterDiscarded: "已废弃", marketFilterRecent: "最近", marketFilterMyBids: "出价", - marketCreateButton: "创建商品", + marketCreateButton: "出售物品", marketItemType: "类型", marketItemTitle: "标题", marketItemAvailable: "截止日期", @@ -2770,14 +2710,11 @@ module.exports = { marketItemDeadline: "截止日期", marketItemIncludesShipping: "包含运费?", marketItemHighestBid: "最高出价", - marketItemHighestBidder: "最高出价者", marketItemStock: "库存", marketOutOfStock: "缺货", - marketItemBidTime: "出价时间", marketActionsUpdate: "更新", marketUpdateButton: "更新", marketActionsDelete: "删除", - marketActionsSold: "标记为已售出", marketActionsChangeStatus: "更改状态", marketActionsBuy: "购买!", marketAuctionBids: "当前出价", @@ -2785,20 +2722,82 @@ module.exports = { marketItemSeller: "卖家", marketNoItems: "暂无可用商品。", marketYourBid: "你的出价", - marketCreateFormImageLabel: "上传媒体(最大:50MB)", - marketSearchLabel: "搜索", - marketSearchPlaceholder: "搜索标题或标签", + marketCreateFormImageLabel: "上传媒体(最大:75MB)", + marketSearchPlaceholder: "在市场中搜索...", marketMinPriceLabel: "最低价格", marketMaxPriceLabel: "最高价格", - marketSortLabel: "排序", marketSortRecent: "最近", marketSortPrice: "价格", marketSortDeadline: "截止日期", marketSearchButton: "搜索", marketAuctionEndsIn: "结束于", marketAuctionEnded: "已结束", - marketMyBidBadge: "你已出价", marketNoItemsMatch: "没有匹配的商品。", + housingTitle: "住所", + housingDescriptionText: "在你的网络中发现和管理住所。", + modulesHousingLabel: "住所", + modulesHousingDescription: "发现和管理住所的模块。", + typeHousing: "住所", + housingFilterAll: "全部", + housingFilterMine: "我的", + housingFilterRequested: "已申请", + housingFilterSale: "出售", + housingFilterRent: "出租", + housingFilterCouchsurfing: "沙发客", + housingFilterOpen: "可入住", + housingFilterClosed: "不可入住", + housingFilterRecent: "最新", + housingFilterTop: "热门", + housingType: "类型", + housingTypeSALE: "出售", + housingTypeRENT: "出租", + housingTypeCOUCHSURFING: "沙发客", + housingProperty: "房产", + housingPropertyAPARTMENT: "公寓", + housingPropertyHOUSE: "房屋", + housingPropertyROOM: "房间", + housingPropertyLAND: "土地", + housingPropertyOTHER: "其他", + housingTitleLabel: "标题", + housingTitlePlaceholder: "住所名称", + housingDescription: "描述", + housingDescriptionPlaceholder: "描述住所、社区以及包含的内容:卫生间、花园、床位…", + housingRules: "规则", + housingRulesPlaceholder: "宠物、吸烟、安静时段、最短入住…", + housingPlace: "地点", + housingPlacePlaceholder: "国家、城市、地址", + housingPrice: "价格", + housingPriceLabel: "价格(ECO)", + housingSizeLabel: "面积(m²)", + housingFree: "免费", + housingPerMonth: "/月", + housingRooms: "房间数", + housingSize: "面积", + housingCapacity: "可住人数", + housingAvailableFrom: "可入住起始日", + housingAvailableTo: "可入住截止日", + housingTags: "标签", + spreadEditWarning: "编辑后此内容将失去已有的转发", + housingRequests: "申请", + housingRequestButton: "申请", + housingCancelButton: "取消申请", + housingRequestedBadge: "已申请", + housingStatus: "状态", + housingStatusOPEN: "可入住", + housingStatusCLOSED: "不可入住", + housingSetOpen: "设为可入住", + housingSetClosed: "设为不可入住", + housingCreateButton: "创建住所", + housingUpdateButton: "更新", + housingSearchPlaceholder: "在住所中搜索...", + housingSearchButton: "搜索", + housingMinPrice: "最低价格", + housingMaxPrice: "最高价格", + housingSortRecent: "最新", + housingSortPrice: "价格最低", + housingSortRequests: "申请最多", + housingSortRating: "评价最高", + housingNoItems: "未找到住所。", jobsTitle: "工作", jobsCandidatesTitle: "推荐候选人", jobsCandidatesDescription: "技能与此工作匹配的居民。请发送私信邀请他们。", @@ -2846,8 +2845,7 @@ module.exports = { jobLocationPresencial: "现场", jobLocationRemote: "远程", jobVacantsPlaceholder: "职位数量", - jobSalaryPlaceholder: "每小时 ECO 薪资", - jobImage: "上传媒体(最大:50MB)", + jobImage: "上传媒体(最大:75MB)", jobTasks: "任务", jobType: "工作类型", jobTime: "工作时间", @@ -2856,7 +2854,6 @@ module.exports = { jobsFilterTop: "热门", jobsTopTitle: "最高薪资工作", createJobButton: "发布工作", - viewDetailsButton: "查看详情", noJobsFound: "未找到工作机会。", jobAuthor: "作者", jobTypeFreelance: "自由职业", @@ -2868,42 +2865,208 @@ module.exports = { jobsHoursOffered: "提供的小时数", jobsHoursRequested: "请求的小时数", jobsExchangeSkill: "交换需要的技能", - jobsHoursOfferedPlaceholder: "如 4", - jobsHoursRequestedPlaceholder: "如 4", - jobsExchangeSkillPlaceholder: "如 木工、设计", - jobExchangeHint: "对于时间交换工作,请填写下面的字段,而不是薪水。", jobTimePartial: "兼职", jobTimeComplete: "全职", - jobsDeleteButton: "删除", jobsUpdateButton: "更新", jobsFilterApplied: "已申请", jobsAppliedTitle: "我的申请", jobsAppliedBadge: "已申请", - jobsFilterFavs: "收藏", - jobsFavsTitle: "收藏", - jobsFilterNeeds: "需要帮助", - jobsNeedsTitle: "需要帮助", - jobsSearchLabel: "搜索", - jobsSearchPlaceholder: "搜索标题、标签、描述...", + jobsSearchPlaceholder: "在工作中搜索...", jobsMinSalaryLabel: "最低薪资", jobsMaxSalaryLabel: "最高薪资", - jobsSortLabel: "排序", jobsSortRecent: "最近", jobsSortSalary: "最高薪资", jobsSortSubscribers: "最多申请者", jobsSearchButton: "搜索", - jobsFavoriteButton: "收藏", - jobsUnfavoriteButton: "取消收藏", - jobsMessageAuthorButton: "私信", jobsApplicants: "申请者", jobsUpdatedAt: "已更新", jobSetOpen: "设为开放", - jobNewBadge: "新", jobsTagsLabel: "标签", - jobsTagsPlaceholder: "标签1, 标签2, 标签3", noJobsMatch: "没有匹配的工作。", + industrySearchPlaceholder: "在工业中搜索...", + industryAllSectors: "所有部门", + industryVisitButton: "访问工业", + industryAdvanceTo: "设为", + industryAmount: "金额", + industryApproveBuild: "批准", + industryBlueprint: "蓝图", + industryBlueprints: "蓝图", + industryBuild: "构建", + industryBuildNotes: "备注", + industryBuildStart: "开始日期", + industryVoteUpdateButton: "投票更新", + industryVoteDeleteButton: "投票删除", + industryRevokeVote: "撤销", + industryTimeLeft: "剩余时间", + industryBuildEnd: "结束日期", + industryBuilds: "构建", + industryBuildTitle: "标题", + industryContribute: "贡献", + industryContributeButton: "贡献", + industryContributions: "贡献", + industryCreateBlueprintButton: "创建蓝图", + industryDistribute: "分配", + industryDistributeButton: "按份额分配", + industryDistribution: "分配", + industryEcoAmount: "金额 (ECO)", + industryEditBlueprint: "编辑蓝图", + industryFacility: "工厂", + industryKindLabel: "类型", + industryKind_labor: "人工", + industryKind_material: "材料", + industryKind_eco: "资金", + industryLaborHours: "人工(小时)", + industryHours: "小时", + industryMaterialItem: "材料", + industryMaterials: "材料", + industryEstMaterials: "材料总计", + industryEstLabor: "人工总计", + industryEstTotal: "预计价格", + industryBuildingPrice: "制造价格", + industryFinalPrice: "最终价格", + industryBlueprintDescPlaceholder: "规格、尺寸、重量、文档……", + industryMaterialValue: "价值(ECO)", + industryMember: "成员", + industryNewBlueprint: "新建蓝图", + industryNewBuild: "提议构建", + industryEditBuild: "编辑生产", + industryNeedBlueprint: "请先创建蓝图:每次生产都基于蓝图。", + industryNoBlueprints: "还没有蓝图。", + industryNoBuilds: "还没有构建。", + industryNote: "备注", + industryOutput: "产出", + industryOutputKind: "产品类型", + industryOutputValue: "产出价值(ECO,例如来自销售)", + industryPostJob: "创建工作", + industryPot: "资金池", + industryProposeBuildButton: "提议构建", + industryProposer: "提议者", + industryAuthor: "作者", + industrySellOnMarket: "发送到市场", + industrySendToProjects: "创建项目", + industrySkills: "技能", + industryTreasury: "资金", + industryKind_physical: "实体", + industryKind_digital: "数字", + industryBuildStatus_PROPOSED: "已提议", + industryBuildStatus_REJECTED: "已拒绝", + industryBuildStatus_APPROVED: "已批准", + industryBuildStatus_STOCKING: "备料中", + industryBuildStatus_IN_PRODUCTION: "生产中", + industryBuildStatus_COMPLETED: "已完成", + industryBuildStatus_FAILED: "已失败", + pmBotIndustry: "IndustryBot", + pmBotHousing: "HousingBot", + jobsBotMatchTitle: "有一份工作与你的简历匹配", + jobsBotMatchIntro: "有一份工作与你的简历匹配。", + jobsBotMatchJob: "工作", + jobsBotMatchHint: "你收到这条消息是因为你的简历由 AI 管理。可在简历中关闭。", + cvAiManaged: "AI 管理", + switchOn: "开启", + switchOff: "关闭", + cvAiManagedHint: "开启后,当有工作与你的简历匹配时,JobsBot 会通过私信通知你。", + cvMatchThreshold: "达到此匹配度时通知我", + housingBotRequestedTitle: "有人申请了你的一处住所。", + housingBotCancelledTitle: "一处住所的申请已被取消。", + housingBotYouRequestedTitle: "你申请了一处住所。", + housingBotYouCancelledTitle: "你取消了一次申请。", + housingBotUnavailableTitle: "你申请过的住所已不可入住。", + housingBotUnavailableSentTitle: "你已通知申请过该住所的人。", + industryBotAdmittedTitle: "你已被接纳进入一家工厂。", + industryBotApplicationTitle: "你的工厂有新的加入申请。", + industryBotInvitedTitle: "你被邀请加入一家工厂。", + industryBotDissolvedTitle: "你所属的一家工厂已被解散。", + industryBotBuildApprovedTitle: "一个构建已获批准。", + industryBotDistributedTitle: "一个构建的产出已被分配。", + industryFilterRules: "规则", + industryRulesTitle: "规则", + industryRulesIntro: "Industry 让居民集体拥有生产资料:一家工厂是网络所有的作坊,无人私有。", + industryRulesSteward: "创始人是管理人——托管者,而非所有者:不能独自出售、私有化或解散工厂。", + industryRulesMembership: "成员政策决定如何加入:开放(立即加入)、投票(由成员投票接纳)或仅邀请(需成员先邀请)。", + industryRulesQuorum: "法定人数是一项决定生效所需投票的最少成员数,以免小工厂由一位居民决定。", + industryRulesMajority: "多数是通过所需的投票比例(0.5=一半,1=全体一致);当赞成票达到至少 max(法定人数, 成员×多数) 时决定通过。", + industryRulesDecisions: "成员投票接纳新人、批准构建和解散工厂;管理人不能凌驾于这些集体决定之上。", + industryRulesBlueprints: "蓝图是自由(COPYLEFT)配方——投入(材料、工时、技能)与产出(实体或数字商品)——任何人都可以复刻。", + industryRulesBuilds: "构建是一份生产订单:已提议→已批准(投票)→备料→生产中→已完成,且仅在批准后接受贡献。", + industryRulesContributions: "成员向构建贡献劳动(工时)、材料(申报的 ECO 价值)或 ECO;每次贡献获得 Karma。", + industryRulesLaborRate: "劳动费率是本工厂一工时的 ECO 价值;它把工时转为 Karma,以公平比较劳动与资本:Karma = 工时×费率 + 材料价值 + ECO。", + industryRulesShares: "你在构建中的份额是你的 Karma÷总 Karma,决定你能获得多少产出价值。", + industryRulesDistribution: "生产完成后,管理者将资金池(生产资金加销售价值)按贡献份额分配给贡献者。", + industryRulesTreasury: "ECO 贡献由管理人作为构建资金托管人保管,直到分配;所有流动都记录在网络上,纠纷可提交 Courts。", + industryJobs: "职位", + industryReportButton: "举报", + industryCreateTask: "创建任务", + industryOpenDispute: "发起争议", + industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", + industryCreateInvite: "生成邀请", + industryPauseVotes: "状态票数", + industryTitle: "工业", + industryDescription: "用于集体管理生产资料的模块。", + typeIndustryBuild: "构建", + typeIndustryBlueprint: "蓝图", + typeIndustryAllocation: "分配", + typeIndustry: "工业", + modulesIndustryLabel: "工业", + modulesIndustryDescription: "用于集体管理生产资料的模块。", + industryFilterAll: "全部", + industryFilterRecent: "最近", + industryFilterMember: "成员", + industryFilterBlueprints: "蓝图", + industryFilterBuilds: "生产", + industryFilterMine: "我的", + industryFilterActive: "运行中", + industryFilterPaused: "已暂停", + industryFilterDissolved: "已解散", + industryAllTitle: "工业", + industryRecentTitle: "最近的工厂", + industryMemberTitle: "我参与的工厂", + industryMineTitle: "我的工厂", + industryActiveTitle: "运行中", + industryPausedTitle: "已暂停的工厂", + industryDissolvedTitle: "已解散的工厂", + industryNoFacilitiesFound: "未找到工厂。", + industryCreateFacility: "创建工厂", + industryMembers: "成员", + industryMemberBadge: "成员", + industryName: "名称", + industryNamePlaceholder: "工厂名称", + industryDescriptionLabel: "描述", + industryDescriptionPlaceholder: "这家工厂生产什么?", + industrySector: "部门", + industryMembershipPolicy: "成员政策", + industryQuorum: "法定人数", + industryMajority: "多数", + industryLaborRate: "劳动费率", + industryCreateButton: "创建工厂", + industryUpdateButton: "更新", + industrySteward: "管理人", + industryStatusActive: "运行中", + industryStatusPaused: "已暂停", + industryStatusDissolved: "已解散", + industryStatusLabel: "状态", + industryJoinButton: "加入", + industryApplyButton: "申请加入", + industryLeaveButton: "退出", + industryInviteButton: "邀请", + industryInvitePlaceholder: "@...=.ed25519", + industryInviteNeeded: "你需要邀请才能加入。", + industryPauseButton: "投票暂停", + industryResumeButton: "投票恢复", + industryDeleteButton: "删除", + industryPendingApplicants: "待处理申请", + industryVotesYes: "赞成", + industryVotesNo: "反对", + industryAlreadyVoted: "已投票", + industryAdmit: "接纳", + industryReject: "拒绝", + industryDissolveTitle: "解散工厂", + industryDissolveVote: "投票解散", + industryPolicy_open: "开放", + industryPolicy_vote: "投票", + industryPolicy_invite: "仅邀请", projectsTitle: "项目", projectsDescription: "创建、资助和关注你网络中的社区驱动项目。", + projectSearchPlaceholder: "在项目中搜索...", projectCreateProject: "创建项目", projectCreateButton: "创建项目", projectUpdateButton: "更新", @@ -2926,7 +3089,7 @@ module.exports = { projectRecentTitle: "最近的项目", projectTopTitle: "最高筹资", projectTitlePlaceholder: "项目名称", - projectImage: "上传媒体(最大:50MB)", + projectImage: "上传媒体(最大:75MB)", projectDescription: "描述", projectDescriptionPlaceholder: "讲述故事和目标…", projectGoal: "目标 (ECO)", @@ -2947,14 +3110,8 @@ module.exports = { projectPledgeTitle: "支持此项目", projectPledgePlaceholder: "ECO 金额", projectBounties: "悬赏", - projectBountiesInputLabel: "悬赏(每行一个:标题|金额 [ECO]|描述)", - projectBountiesPlaceholder: "修复 UI 漏洞|100|问题链接\n编写文档|250|列出使用示例", projectNoBounties: "未找到悬赏。", projectTitle: "标题", - projectAddBountyTitle: "新悬赏", - projectBountyTitle: "悬赏标题", - projectBountyAmount: "金额 (ECO)", - projectBountyDescription: "描述", projectMilestoneSelect: "选择里程碑", projectBountyCreateButton: "创建悬赏", projectBountyStatus: "悬赏状态", @@ -2969,19 +3126,15 @@ module.exports = { projectMilestoneTitle: "里程碑标题", projectMilestoneTargetPercent: "百分比 (%)", projectMilestoneDueDate: "日期", - projectMilestoneCreateButton: "创建里程碑", projectMilestoneStatus: "里程碑状态", projectMilestoneOpen: "开放", projectMilestoneDone: "已完成", projectMilestoneDue: "截止", - projectNoMilestones: "未找到里程碑。", projectMilestoneMarkDone: "标记为已完成", projectMilestoneTitlePlaceholder: "输入里程碑标题", projectMilestoneDescriptionPlaceholder: "输入此里程碑的描述", projectMilestoneDescription: "里程碑描述", - projectBudgetGoal: "预算(目标)", projectBudgetAssigned: "已分配至悬赏", - projectBudgetRemaining: "剩余", projectBudgetOver: "⚠ 超出预算:分配金额超过目标", projectFollowers: "关注者", projectFollowersTitle: "关注者", @@ -2994,7 +3147,6 @@ module.exports = { projectBackersTotalPledged: "质押总额", projectBackersYourPledge: "你的质押", projectBackersNone: "还没有质押。", - projectNoRemainingBudget: "没有剩余预算。", projectFilterBackers: "支持者", projectFilterApplied: "已申请", projectAppliedTitle: "已申请", @@ -3003,36 +3155,19 @@ module.exports = { projectBackerAmount: "总贡献", projectBackerPledges: "质押", projectBackerProjects: "项目", - projectPledgeAmount: "金额", projectSelectMilestoneOrBounty: "选择里程碑或悬赏", projectPledgeButton: "质押", footerLicense: "GPLv3", - footerPackage: "软件包", - footerVersion: "版本", modulesModuleName: "名称", modulesModuleDescription: "描述", modulesModuleStatus: "状态", - modulesTotalModulesLabel: "已加载模块", + modulesTotalModulesLabel: "总计", modulesEnabledModulesLabel: "已启用", modulesDisabledModulesLabel: "已禁用", - modulesPopularLabel: "精选", - modulesPopularDescription: "接收热门、最多浏览或最多评论帖子的模块。", - modulesTopicsLabel: "话题", - modulesTopicsDescription: "接收基于共同兴趣的讨论类别的模块。", - modulesSummariesLabel: "摘要", - modulesSummariesDescription: "接收长讨论或帖子摘要的模块。", - modulesLatestLabel: "最新", - modulesLatestDescription: "接收最新帖子和讨论的模块。", - modulesThreadsLabel: "讨论串", - modulesThreadsDescription: "接收按话题或问题分组的对话的模块。", - modulesMultiverseLabel: "多元宇宙", - modulesMultiverseDescription: "接收来自其他联邦节点内容的模块。", modulesInvitesLabel: "邀请", modulesInvitesDescription: "管理和应用邀请码的模块。", modulesWalletLabel: "钱包", modulesWalletDescription: "管理你的数字资产 (ECOin) 的模块。", - modulesLegacyLabel: "密钥", - modulesLegacyDescription: "快速安全地管理你的私钥的模块。", modulesCipherLabel: "加密器", modulesCipherDescription: "使用共享密码对文本进行对称加密和解密的模块。", modulesBookmarksLabel: "书签", @@ -3067,8 +3202,12 @@ module.exports = { modulesOpinionsDescription: "发现和为观点投票的模块。", modulesTransfersLabel: "转账", modulesTransfersDescription: "发现和管理智能合约(转账)的模块。", - modulesFediverseLabel: "联邦宇宙", - modulesFediverseDescription: "管理你的其他联邦宇宙账户,包括发送和接收内容。", + modulesFediverseLabel: "多元宇宙", + modulesBlogsLabel: "博客", + modulesBlogsDescription: "用于发现和管理博客的模块。", + modulesPollsLabel: "投票", + modulesPollsDescription: "用于向网络提问并统计答案的模块。", + modulesFediverseDescription: "管理你的其他多元宇宙账户,包括发送和接收内容。", modulesFeedLabel: "动态", modulesFeedDescription: "发现和分享短文(动态)的模块。", modulesParliamentLabel: "议会", @@ -3090,52 +3229,51 @@ module.exports = { modulesProjectsLabel: "项目", modulesProjectsDescription: "探索、众筹和管理项目的模块。", modulesBankingLabel: "银行", - modulesBankingDescription: "确定 ECOIN 真实价值并使用公共金库分配 UBI 的模块。", + modulesBankingDescription: "用于管理共同金库的模块:ECOIN 价值、税收与全民基本收入。", modulesFavoritesLabel: "收藏", modulesFavoritesDescription: "管理你收藏内容的模块。", fileTooLargeTitle: "文件过大", - fileTooLargeMessage: "文件超过了允许的最大大小(50 MB)。请选择较小的文件。", + fileTooLargeMessage: "文件超过了允许的最大大小(75 MB)。请选择较小的文件。", goBack: "返回", + errorDismiss: "好", errorPageTitle: "出现了问题", + errorNoAllocation: "未找到与此申领对应的分配。", + errorInvalidClaim: "此分配无法申领。", visibilityLabel: "可见性", + clearnetPrivateConflict: "私密、隐藏、关闭、仅限邀请或付费的内容不能发布到 CLEARNET。请选择 OASIS 或将其设为公开。", visibilityPublic: "公开", visibilityHidden: "隐藏", visibilityMakePublic: "设为公开", visibilityMakeHidden: "设为隐藏", invitesInhabitantsTitle: "居民", - invitesInhabitantsFollow: "关注", - aiNavPlaceholder: "你想去哪里?", + aiNavPlaceholder: "描述你需要什么...", aiNavDisabled: "AI 导航已停用。", - aiNavResultsTitle: "AI 导航建议", + aiNavResultsTitle: "AINav 建议", aiNavQueryLabel: "查询", - aiNavResultsDescription: "选择最符合你需求的路径。", - aiNavResultPath: "路径", - aiNavResultDescription: "你可以做什么", aiNavResultMatch: "匹配度", aiNavResultsEmpty: "无匹配路径,请尝试 /search。", - aiNavSearchInstead: "改为搜索", modulesAINavLabel: "AINav", modulesAINavDescription: "用于以自然语言查询网络内容的模块。", - directConnect: "直接连接", directConnectDescription: "通过输入对方的 IP 地址、端口和公钥直接连接到节点。该节点将被添加为已关注的连接。", peerHost: "IP", - peerPort: "端口(默认:8008)", + peerPort: "端口", peerPublicKey: "公钥 (@...ed25519)", connectAndFollow: "连接", - deviceSourceLabel: "设备来源", modulesPresetTitle: "常用配置", - modulesPreset_minimal: "极简", - modulesPreset_basic: "基础", - modulesPreset_social: "社交", - modulesPreset_economy: "经济", - modulesPreset_full: "完整", + workflowsTitle: "工作模式", + workflowsDescription: "工作模式会设定主题与启用的模块。", + workflowsSet: "应用工作模式", + workflow_default: "默认", + workflow_jobs: "工作", + workflow_social: "社交", + workflow_business: "商务", + workflow_night: "夜间", + workflow_kids: "儿童", + workflow_activists: "活动人士", + workflow_mobile: "移动端", statsCarbonFootprintTitle: "碳足迹", - statsCarbonFootprintNetwork: "网络碳足迹", - statsCarbonFootprintYours: "你的碳足迹", statsCarbonTombstone: "墓碑碳足迹", - feedSuccessMsg: "动态发布成功!", - dominantOpinionLabel: "主流观点", - uploadMedia: "上传媒体(最大:50MB)", + uploadMedia: "上传媒体(最大:75MB)", feedOpenDiscussion: "公开讨论", feedDetailTitle: "动态", feedPostComment: "发表评论", @@ -3143,7 +3281,7 @@ module.exports = { noComments: "还没有评论", mapsLabel: "地图", mapTitle: "地图", - mapDescription: "在您的网络中探索和管理离线地图。", + mapDescription: "在您的网络中管理和分享离线地图。", mapMineSectionTitle: "我的地图", mapCreateSectionTitle: "创建地图", mapUpdateSectionTitle: "更新地图", @@ -3167,14 +3305,13 @@ module.exports = { mapDescriptionLabel: "描述", mapDescriptionPlaceholder: "描述地图或位置...", mapTypeLabel: "地图类型", - mapTypeSingle: "单一(仅初始位置)", - mapTypeOpen: "开放(任何人可添加标记)", - mapTypeClosed: "封闭(仅创建者可添加标记)", + mapInviteMode: "邀请", + mapTypeSingle: "单一", + mapTypeOpen: "开放", + mapTypeClosed: "封闭", mapTagsLabel: "标签", - mapTagsPlaceholder: "输入以逗号分隔的标签", + mapTagsPlaceholder: "输入标签,用逗号分隔", mapUrlLabel: "地图链接", - mapPickCoordLabel: "或在网格上选择位置:", - mapMarkersLabel: "标记", mapMarkersTitle: "标记", mapMarkerDefault: "标记", mapMarkerLatLabel: "标记纬度", @@ -3184,9 +3321,22 @@ module.exports = { markerImageLabel: "标记图片", mapAddMarkerTitle: "添加标记", mapAddMarkerButton: "添加标记", - mapCleanMarkerButton: "Clean Marker", + mapCleanMarkerButton: "清除标记", mapApplyZoom: "应用缩放", - mapSearchPlaceholder: "搜索描述、标签、作者...", + mapPlaceSearchPlaceholder: "查找地点或国家...", + mapNoPlaces: "未找到地点。", + mapClusterTitle: "个标记在此,点击放大", + mapDeleteMarker: "删除标记", + mapZoomIn: "放大", + mapZoomOut: "缩小", + mapFitMarkers: "显示所有标记", + mapPanLabel: "移动地图", + mapCenterHere: "在此居中", + mapNear: "靠近", + mapPickedSpot: "所选地点", + mapPickHint: "点击地图选择一个地点。", + mapClearPick: "清除", + mapSearchPlaceholder: "在地图中搜索...", mapSearchButton: "搜索", mapUpdatedAt: "已更新", mapNoMatch: "没有匹配的地图。", @@ -3197,118 +3347,820 @@ module.exports = { typeMapMarker: "地图标记", modulesMapLabel: "地图", modulesMapDescription: "管理和分享离线地图的模块。", + emergenciesTitle: "紧急情况", + modulesEmergenciesLabel: "紧急情况", + modulesEmergenciesDescription: "用于在网络中发布、确认和跟进紧急情况的模块。", + emergenciesDescription: "用于在网络中发布、确认和跟进紧急情况的模块。", + emergencyCreate: "创建紧急情况", + emergencyUpdate: "更新", + emergencyDelete: "删除", + emergencyTitleLabel: "标题", + emergencyTitlePlaceholder: "发生了什么?", + emergencyTextLabel: "详情", + emergencyTextPlaceholder: "地点、开始时间、应对措施...", + emergencyCategoryLabel: "类别", + emergencyCategoryWeather: "天气", + emergencyCategoryInfrastructure: "基础设施", + emergencyCategoryHealth: "健康", + emergencyCategorySecurity: "安全", + emergencyCategoryLost: "失物招领", + emergencyCategoryNeighborhood: "社区", + emergencyExpiresLabel: "有效期", + emergencyExpires1d: "1 天", + emergencyExpires3d: "3 天", + emergencyExpires7d: "1 周", + emergencyExpires30d: "1 个月", + emergencyExpiresNone: "未定", + emergencyExpiresKeep: "保留当前日期", + emergencyTagsLabel: "标签", + emergencyTagsPlaceholder: "输入标签,用逗号分隔", + emergencyFilterAll: "全部", + emergencyFilterMine: "我的", + emergencyFilterRecent: "最近", + emergencySeverityUnverified: "未核实", + emergencySeverityLow: "低", + emergencySeverityMedium: "中", + emergencySeverityHigh: "高", + emergencyStatusActive: "进行中", + emergencyStatusResolved: "已解决", + emergencyStatusExpired: "已过期", + emergencyConfirmations: "确认数", + emergencyConfirmButton: "确认", + emergencyConfirmed: "你已确认此紧急情况", + emergencyResolveButton: "标记为已解决", + emergencyReopenButton: "重新开放", + emergencyGenerateReport: "生成报告", + emergencyUpdateEdited: "已编辑", + emergencyLastUpdate: "最新更新", + emergencyUpdatesTitle: "最新更新:", + emergencyNoUpdates: "暂无更新。", + emergencyAddUpdate: "发布更新", + emergencyUpdatePlaceholder: "有什么变化?", + emergencyNoItems: "未找到紧急情况。", + emergencyExpiresAt: "到期", + emergencyBannerLabel: "进行中的紧急情况", + emergencyCreateSectionTitle: "创建紧急情况", + emergencyUpdateSectionTitle: "更新紧急情况", + emergencySearchPlaceholder: "在紧急情况中搜索...", + typeEmergency: "紧急情况", + typeEmergencyConfirm: "紧急情况确认", + typeEmergencyUpdate: "紧急情况更新", + statsEmergency: "紧急情况", + statsEmergencyConfirm: "紧急情况确认", + statsEmergencyUpdate: "紧急情况更新", + pmBotEmergencies: "EmergencyBot", + emergencyBotUpdatedTitle: "你关注的紧急情况有更新。", + emergencyBotResolvedTitle: "你关注的紧急情况已解决。", + dataKindEmergencies: "紧急情况", + mailingTitle: "邮件列表", + modulesMailingLabel: "邮件列表", + modulesMailingDescription: "在你的网络中发现和管理邮件列表。", + modulesPhoneLabel: "电话", + modulesPhoneDescription: "与其他居民进行端到端加密视频通话的模块。", + mailingDescription: "在你的网络中发现和管理邮件列表。", + mailingCreate: "创建邮件列表", + mailingUpdate: "更新", + mailingArchive: "归档", + mailingActivate: "激活", + mailingCreateSectionTitle: "创建邮件列表", + mailingUpdateSectionTitle: "更新邮件列表", + mailingTitleLabel: "标题", + mailingTitlePlaceholder: "邮件列表名称", + mailingDescriptionLabel: "描述", + mailingDescriptionPlaceholder: "这个列表是关于什么的?", + mailingTypeLabel: "邮件列表类型", + mailingTypeOpen: "开放", + mailingTypeClosed: "封闭", + mailingStatusLabel: "状态", + mailingStatusActive: "活跃", + mailingStatusArchived: "已归档", + mailingMembersLabel: "邀请居民", + mailingMembersPlaceholder: "@id1.ed25519, @id2.ed25519, ...", + mailingSubscribers: "订阅者", + mailingTagsLabel: "标签", + mailingTagsPlaceholder: "输入标签,用逗号分隔", + mailingFilterAll: "全部", + mailingFilterMine: "我的", + mailingFilterSubscribed: "已订阅", + mailingFilterRecent: "最近", + mailingHistoryDate: "日期", + mailingHistoryThreads: "话题", + mailingHistoryInhabitants: "居民", + mailingHistorySearchPlaceholder: "在历史中搜索...", + mailingNoMessages: "还没有消息。", + mailingNoItems: "未找到邮件列表。", + mailingSearchPlaceholder: "在邮件列表中搜索...", + mailingWriteButton: "写消息", + mailingWriteTitle: "写信给列表", + mailingReplyTitle: "回复列表", + mailingSend: "发送", + mailingTextPlaceholder: "你的消息会送达列表中的每个参与者。", + mailingParticipants: "参与者", + inboxFilterMailing: "邮件列表", + mailingReplyPlaceholder: "回复列表...", + mailingReplyToList: "回复列表", + typeMailingList: "邮件列表", + statsMailingList: "邮件列表", + mailingListLabel: "邮件列表", + logisticsTitle: "物流", + modulesLogisticsLabel: "物流", + modulesLogisticsDescription: "在你的网络中发现和管理物流计划。", + logisticsDescription: "在你的网络中发现和管理物流计划。", + logisticsCreate: "创建路线", + logisticsUpdate: "更新", + logisticsClose: "关闭", + logisticsReopen: "重新开放", + logisticsCreateSectionTitle: "创建路线", + logisticsUpdateSectionTitle: "更新路线", + logisticsTitleLabel: "标题", + logisticsTitlePlaceholder: "去市场的顺风车,有货车...", + logisticsKindLabel: "物流类型", + logisticsKindTrip: "出行", + logisticsKindShipment: "运送", + logisticsKindTrips: "出行", + logisticsKindShipments: "运送", + logisticsModeLabel: "模式", + logisticsModeOffer: "提供", + logisticsModeRequest: "寻求", + logisticsModeOffers: "提供", + logisticsModeRequests: "请求", + logisticsDescriptionLabel: "描述", + logisticsDescriptionPlaceholder: "集合地点、携带物品、条件...", + logisticsOriginLabel: "起点", + logisticsOriginPlaceholder: "从哪里出发", + logisticsDestinationLabel: "终点", + logisticsDestinationPlaceholder: "到哪里结束", + logisticsDateLabel: "日期和时间", + logisticsRecurrenceLabel: "重复", + logisticsFrequencyLabel: "频率", + logisticsRecurrenceNone: "单次", + logisticsRecurrenceDaily: "每天", + logisticsRecurrenceWeekly: "每周", + logisticsRecurrenceMonthly: "每月", + logisticsSeatsLabel: "座位", + logisticsSizeLabel: "尺寸", + logisticsSizePlaceholder: "例如 2m x 1m", + logisticsWeightLabel: "重量", + logisticsWeightPlaceholder: "例如 40kg", + logisticsPriceFree: "免费", + logisticsPriceEco: "ECO", + logisticsPriceTime: "时间", + logisticsPriceLabel: "价格", + logisticsHours: "小时", + logisticsOrderRefLabel: "订单链接", + logisticsOrderRefPlaceholder: "待送达订单的 /shops/... 或 /market/...", + logisticsOrderLink: "关联订单", + logisticsTagsLabel: "标签", + logisticsTagsPlaceholder: "输入标签,用逗号分隔", + logisticsFilterAll: "全部", + logisticsFilterMine: "我的", + logisticsFilterBooked: "已预订", + logisticsFilterRecent: "最近", + logisticsFilterHistory: "历史", + logisticsFilterTop: "热门", + logisticsStatusOpen: "开放", + logisticsStatusUpcoming: "即将", + logisticsStatusPast: "已过", + logisticsStatusClosed: "已关闭", + logisticsZoneLabel: "区域", + logisticsSearchPlaceholder: "在物流中搜索...", + logisticsNoItems: "未找到路线。", + logisticsBookingPending: "待处理", + logisticsBookingConfirmed: "已确认", + logisticsBookingRejected: "已拒绝", + logisticsBookingCancelled: "已取消", + logisticsBookingDelivered: "已送达", + logisticsConfirm: "确认", + logisticsReject: "拒绝", + logisticsDeliver: "送达", + logisticsReceiptLabel: "收据", + logisticsReceiptPlaceholder: "送达备注(可选)", + logisticsCancelBooking: "取消预订", + logisticsBookTitle: "预订此路线", + logisticsNotesLabel: "备注", + logisticsNotesPlaceholder: "负责人需要了解的信息", + logisticsBook: "预订", + logisticsBookingsTitle: "预订", + logisticsMyBookingTitle: "你的预订", + logisticsRatingsTitle: "评价", + logisticsNoRatings: "还没有评价。", + logisticsRatingsWhenClosed: "路线关闭后开放评价。", + logisticsScoreLabel: "评分", + logisticsRatingPlaceholder: "进行得如何?", + logisticsRate: "评价", + typeLogisticsRoute: "路线", + typeLogisticsRating: "路线评价", + statsLogisticsRoute: "路线", + statsLogisticsRating: "路线评价", + logisticsRouteLabel: "路线", + podcastsTitle: "播客", + modulesPodcastsLabel: "播客", + modulesPodcastsDescription: "用于发布播客频道的模块。", + podcastsDescription: "用于发布播客频道的模块。", + podcastCreate: "创建播客", + podcastUpdate: "更新", + podcastDelete: "删除", + podcastCreateSectionTitle: "创建播客", + podcastUpdateSectionTitle: "更新播客", + podcastTitleLabel: "标题", + podcastTitlePlaceholder: "频道名称", + podcastDescriptionLabel: "描述", + podcastDescriptionPlaceholder: "这个播客是关于什么的?", + podcastCategoryLabel: "分类", + podcastCategoryNews: "新闻", + podcastCategoryMusic: "音乐", + podcastCategoryTalk: "谈话", + podcastCategoryEducation: "教育", + podcastCategoryTech: "科技", + podcastCategoryCulture: "文化", + podcastCategoryCommunity: "社区", + podcastCategoryOasis: "OASIS", + podcastCategoryOther: "其他", + podcastTagsLabel: "标签", + podcastCoverLabel: "封面(图片或视频)", + podcastEpisodeAudioLabel: "音频或视频文件(最大 75MB)", + podcastTagsPlaceholder: "输入标签,用逗号分隔", + podcastFilterAll: "全部", + podcastFilterMine: "我的", + podcastFilterRecent: "最近", + podcastFilterTop: "热门", + podcastFilterViewers: "听众", + podcastSearchPlaceholder: "在播客中搜索...", + podcastNoItems: "未找到播客。", + podcastEpisodesLabel: "剧集", + podcastPlaysLabel: "观看次数", + podcastLastEpisode: "最新剧集", + podcastEpisodesTitle: "剧集", + podcastNoEpisodes: "还没有剧集。", + podcastEpisodeAdd: "添加剧集", + podcastEpisodeCreateTitle: "发布剧集", + podcastEpisodeUpdateTitle: "更新剧集", + podcastEpisodeTitleLabel: "剧集标题", + podcastEpisodeTitlePlaceholder: "剧集标题", + podcastEpisodeDescriptionLabel: "节目笔记", + podcastEpisodeDescriptionPlaceholder: "本集内容、链接、致谢...", + podcastEpisodePublish: "发布", + podcastPlayed: "已播放", + podcastMarkPlayed: "标记为已播放", + typePodcast: "播客", + typePodcastEpisode: "剧集", + typePodcastOpinion: "剧集评价", + typePodcastPlay: "剧集播放", + statsPodcast: "播客", + statsPodcastEpisode: "剧集", + statsPodcastPlay: "剧集播放", + pmBotPodcasts: "PodcastBot", + pmBotBlogs: "BlogsBot", + blogBotNewTitle: "你关注的博客有新文章。", + podcastBotEpisodeTitle: "你关注的播客有新剧集。", + podcastLabel: "播客", + podcastEpisodeLabel: "剧集", + campaignsTitle: "倡议", + modulesCampaignsLabel: "倡议", + modulesCampaignsDescription: "在你的网络中发现和管理倡议。", + campaignsDescription: "在你的网络中发现和管理倡议。", + campaignCreate: "发起倡议", + campaignUpdate: "更新", + campaignDelete: "删除", + campaignClose: "关闭", + campaignReopen: "重新开放", + campaignCreateSectionTitle: "发起倡议", + campaignUpdateSectionTitle: "更新倡议", + campaignTitleLabel: "标题", + campaignTitlePlaceholder: "你想达成什么?", + campaignTextLabel: "正文", + campaignTextPlaceholder: "说明诉求、由谁决定以及你要求什么...", + campaignCategoryLabel: "分类", + campaignCategoryEnvironment: "环境", + campaignCategoryRights: "权利", + campaignCategoryHealth: "健康", + campaignCategoryEducation: "教育", + campaignCategoryInfrastructure: "基础设施", + campaignCategoryCulture: "文化", + campaignCategoryEconomy: "经济", + campaignCategoryOther: "其他", + campaignGoalLabel: "签名目标", + campaignDeadlineLabel: "截止日期", + campaignTagsLabel: "标签", + campaignTagsPlaceholder: "输入标签,用逗号分隔", + campaignFilterAll: "全部", + campaignFilterMine: "我的", + campaignFilterSigned: "已签名", + campaignFilterRecent: "最近", + campaignFilterTop: "热门", + campaignStatusOpen: "进行中", + campaignStatusAchieved: "已达成", + campaignStatusUnachieved: "未达成", + campaignStatusClosed: "已关闭", + campaignSearchPlaceholder: "在倡议中搜索...", + campaignNoItems: "未找到倡议。", + campaignSignaturesLabel: "签名", + campaignSignaturesTitle: "支持者", + campaignNoSignatures: "还没有签名。", + campaignSignTitle: "签署此倡议", + campaignSignPlaceholder: "你为什么支持?(可选)", + campaignSign: "签名", + campaignSigned: "你已签名!", + campaignNotSigned: "还缺你的签名", + campaignElevate: "提交议会", + campaignElevated: "已提交议会", + campaignUpdatesTitle: "进展", + campaignNoUpdates: "还没有进展。", + campaignUpdatePlaceholder: "发生了什么?", + campaignAddUpdate: "发布进展", + campaignQrAlt: "签署此倡议的二维码", + campaignQrCaption: "扫码签名", + typeCampaign: "倡议", + typeCampaignSignature: "签名", + typeCampaignUpdate: "倡议进展", + statsCampaign: "倡议", + statsCampaignSignature: "倡议签名", + statsCampaignUpdate: "倡议进展", + pmBotCampaigns: "CampaignBot", + campaignBotUpdatedTitle: "你关注的倡议有新进展。", + campaignBotAchievedTitle: "你关注的倡议已达成目标。", + campaignBotRaisedTitle: "你签署的一个活动已提交议会。", + pmBotLogistics: "LogisticsBot", + logisticsBotUpdatedTitle: "你预订的路线已更改。", + logisticsBotClosedTitle: "你预订的路线已关闭。", + logisticsBotBookedTitle: "有人预订了你的路线。", + logisticsBotCancelledTitle: "你路线上的一个预订已取消。", + logisticsBotConfirmedTitle: "你的预订已确认:包裹已在途中。", + logisticsBotRejectedTitle: "你的预订已被拒绝。", + logisticsBotDeliveredTitle: "你的包裹已送达。", + campaignLabel: "倡议", + campaignGenerateReport: "生成报告", + logisticsGenerateReport: "生成报告", + mailingGenerateReport: "生成报告", + campaignUpdateEdited: "已编辑", + mailingMessagesLabel: "消息", + logisticsBookingsLabel: "预订", + backupTitle: "备份", + modulesBackupLabel: "备份", + modulesBackupDescription: "用于导出和导入密钥并创建加密备份的模块。", + backupDescription: "用于导出和导入密钥并创建加密备份的模块。", + backupTypeRecovery: "恢复", + backupTypeKeys: "导出密钥", + backupTypeFull: "完整备份", + backupTypeRestore: "恢复备份", + backupTypeInstant: "即时", + backupInstantDescription: "一个普通的 .zip,包含你全部的日志与文件,不含你的私钥,也没有密码。很快,但", + backupInstantRisk: "拿到文件的人能读到里面的一切。", + backupKitPageDescription: "恢复工具包只能在本设备上打开。", + backupKitId: "Oasis ID", + backupKitDate: "生成时间", + backupKitQrAlt: "你的私钥二维码", + backupKitStep1: "打印或手抄,并离线存放在安全的地方。", + backupKitStep2: "恢复时,在新设备上安装 Oasis,并在 备份 › 密钥 中导入此私钥。", + backupKitStep3: "如果此页面泄露,请立即销毁。", + backupKitPdfButton: "生成 PDF", + backupScopeLabel: "范围", + backupScopeAll: "全部", + backupScopeMine: "仅我的内容", + backupSinceLabel: "起始日期", + backupEstimateTitle: "估算", + backupFileInfo: "加密副本将下载到你的设备(文件名:oasis-backup-日期.oasisbk)", + backupExportButton: "下载备份", + backupRestorePassword: "输入创建备份时使用的密码", + backupRestoreButton: "恢复备份", + backupRestoredLine: "备份已恢复。", + backupRestoredMessages: "新增消息", + backupRestoredSkipped: "已存在", + backupRestoredBlobs: "新增文件", + backupRestoredState: "已恢复设置", + backupRestoredFailed: "失败", + backupRestoredForked: "分叉", + backupRestoreRunning: "正在恢复备份。此页面会自动刷新;期间你可以继续使用 Oasis。", + backupRestoreElapsed: "已用时", + backupRestoreFailedLine: "无法恢复备份。", + backupRestoreForkMine: "此设备已用你的身份发布了备份中没有的消息,因此无法在其之上应用备份。请在全新安装中导入密钥后立即恢复,且不要先做其他操作。", + verificationTitle: "校验", + verificationDescription: "检查你的动态链、检测分叉,并查看孤立或缺失的文件。", + verificationRun: "运行校验", + verificationTotalMessages: "日志中的消息", + verificationFeed: "你的动态", + verificationMessages: "条消息", + verificationLastSequence: "最后序号", + verificationGaps: "缺口", + verificationBrokenLinks: "断链", + verificationBadHashes: "错误哈希", + verificationBadSignatures: "错误签名", + verificationDuplicates: "重复序号", + verificationForks: "分叉", + verificationBlobs: "文件", + verificationBlobsPresent: "存在", + verificationBlobsReferenced: "被引用", + verificationBlobsOrphan: "孤立", + verificationBlobsMissing: "缺失", + verificationBlobsMissingOwn: "此设备上缺失的你自己的文件", + verificationBlobsMissingOthers: "其他居民的文件尚未下载", + verificationBlobsUnavailable: "无法在此设备上列出文件。", + wikiTitle: "维基", + modulesWikiLabel: "维基", + modulesWikiDescription: "用于发现和管理维基的模块。", + wikiDescription: "用于发现和管理维基的模块。", + wikiCreate: "创建维基", + wikiUpdate: "更新", + wikiDelete: "删除", + wikiEdit: "编辑", + wikiTitleLabel: "标题", + wikiTitlePlaceholder: "维基标题...", + wikiBodyLabel: "内容", + wikiBodyPlaceholder: "用 [[维基标题]] 或 [[维基标题|自定义文字]] 链接其他维基。\n\n用 @id 提及居民。\n\n用 #oasis 添加标签。\n\n粘贴链接,如 https://solarnethub.com\n\n空行开始新段落;单个换行会被保留。\n\n附加任何受支持的媒体,它会嵌入到这里。", + formatBarLabel: "格式", + wikiStatusLabel: "维基类型", + wikiStatusOpen: "开放", + wikiStatusClosed: "关闭", + wikiTagsLabel: "标签", + wikiTagsPlaceholder: "输入标签,用逗号分隔", + wikiSummaryLabel: "修改摘要", + wikiSummaryPlaceholder: "改了什么?", + wikiFilterAll: "全部", + wikiFilterMine: "我的", + wikiFilterRecent: "最近", + wikiFilterLinked: "已关联", + wikiChanges: "最近更改", + wikiNoPages: "未找到维基。", + wikiNoChanges: "暂无更改。", + wikiHistory: "历史", + wikiVersion: "版本", + wikiVersions: "个版本", + wikiRestore: "恢复", + wikiDiff: "更改", + wikiViewVersion: "查看", + wikiLinkedPages: "关联页面", + wikiNoLinkedPages: "此页面尚未与其他页面互链。", + wikiMissingLink: "页面尚不存在", + wikiDuplicateTitle: "已存在使用该标题的页面。请修改标题以创建另一个页面。", + wikiCreateSectionTitle: "创建维基", + wikiUpdateSectionTitle: "更新维基", + wikiSearchPlaceholder: "在维基中搜索...", + wikiCurrentVersion: "当前版本", + wikiViewingVersion: "你正在查看旧版本", + wikiBackToPage: "返回页面", + wikiLinkedChip: "已关联", + wikiRestoredSummary: "已恢复的版本", + wikiExportPdf: "生成页面", + wikiPreview: "预览", + tribeSectionWiki: "维基", + tribeWikiCreate: "创建维基", + tribeWikiEmpty: "还没有维基。", + typeWiki: "维基", padsTitle: "协作板", padTitle: "协作板", modulesPadsLabel: "协作板", - modulesPadsDescription: "管理协作文本编辑器的模块。", - padFilterAll: "全部", - padFilterMine: "我的", - padFilterRecent: "最近", - padFilterOpen: "开放", - padFilterClosed: "关闭", + modulesPadsDescription: "用于管理协作文本编辑器的模块。", padCreate: "创建协作板", - padUpdate: "更新协作板", - padDelete: "删除协作板", + padUpdate: "更新", + padDelete: "删除", padTitleLabel: "标题", padTitlePlaceholder: "输入协作板标题...", padStatusLabel: "状态", + padTypeLabel: "Pad 类型", padStatusOpen: "开放", padStatusInviteOnly: "仅限受邀者", padStatusClosed: "关闭", padDeadlineLabel: "截止日期", padTagsLabel: "标签", - padTagsPlaceholder: "标签1, 标签2, ...", + padTagsPlaceholder: "输入标签,用逗号分隔", padMembersLabel: "成员", - padVisitPad: "访问协作板", - padShareUrl: "分享链接", padCreated: "创建时间", - padAuthor: "作者", - padGenerateCode: "生成邀请码", - padInviteCodeLabel: "邀请码", - padInviteCodePlaceholder: "输入邀请码...", - padValidateInvite: "验证", - padStartEditing: "开始编辑!", + padStartEditing: "编辑", padEditorPlaceholder: "开始写作...", padSubmitEntry: "提交", padNoEntries: "暂无内容。", padAllSectionTitle: "全部协作板", padMineSectionTitle: "我的协作板", padsDescription: "在您的网络中管理协作加密的文本编辑器。", + roomsTitle: "房间", + roomsDescription: "发现并加入您网络中的加密视频会议房间。", + modulesRoomsLabel: "房间", + modulesRoomsDescription: "用于发现并加入加密视频会议房间的模块。", + roomFilterRecent: "最近", + roomFilterMine: "我的", + roomFilterAll: "全部", + roomFilterLive: "直播中", + roomFilterOpen: "开放", + roomFilterInvite: "仅限受邀者", + roomFilterClosed: "已关闭", + roomCreate: "创建房间", + roomUpdate: "更新", + roomDelete: "删除", + roomClose: "关闭", + roomReopen: "重新开放", + roomCreateSectionTitle: "创建房间", + roomUpdateSectionTitle: "更新房间", + roomTitlePlaceholder: "房间标题", + roomDescriptionLabel: "描述", + roomDescriptionPlaceholder: "你们要聊什么?", + roomTypeLabel: "房间类型", + roomTagsLabel: "标签", + roomStatusOpen: "开放", + roomStatusInviteOnly: "仅限受邀者", + roomStatusClosed: "已关闭", + roomParticipants: "参与者", + roomJoin: "加入", + roomLeave: "离开", + roomFull: "房间已满", + roomLiveChip: "直播中", + roomMuted: "已静音", + roomRec: "录制音频", + roomRecStop: "停止录音", + roomStreamFile: "播放", + roomStreamStop: "停止", + roomStreaming: "正在播放", + liveFullscreen: "全屏", + roomStreamPickTitle: "选择视频", + roomStreamPickHint: "此设备上的视频。所选视频将从其所在位置直接播放,不会被复制或发布。", + roomStreamGo: "在房间中播放", + roomStreamNotFound: "你的文件夹中没有这个视频。", + roomPublicWarning: "此房间在公共网络上直播:任何人都能看到和听到其中发生的一切。", + roomRecording: "REC", + roomNotificationsOn: "ON", + roomNotificationsOff: "OFF", + roomEventJoined: "{name} 进入了房间", + roomEventLeft: "{name} 离开了房间", + roomEventRecStart: "{name} 开始录音", + roomEventRecStop: "{name} 停止录音", + roomEventMuted: "{name} 已静音", + roomEventUnmuted: "{name} 已取消静音", + roomEventHand: "{name} 举手了", + roomRaiseHand: "举手", + roomLowerHand: "放下手", + confirmActionTitle: "确认此操作", + confirmActionButton: "确认", + confirmJoinText: "你即将使用此邀请加入。", + confirmFollowText: "你即将支持这位居民。", + confirmSignText: "你即将签署此活动。", + confirmDownloadText: "你即将把此文件下载到你的设备。", + roomRecordings: "录音记录", + roomClearNotices: "清除", + roomLineSwitchboard: "总机", + roomLineDndError: "此房间的号码不接受来电。", + roomNumberHint: "在电话中拨打此号码即可进入房间。", + liveRefresh: "刷新", + roomNobody: "目前房间里没有人。", + roomSomeone: "目前房间里有 {n} 人。", + roomYou: "你", + roomYouAreIn: "你已在其中", + roomNobodyInside: "此房间现在没有人。", + roomErrorFull: "房间已满。有人离开后即可加入。", + roomErrorUnreachable: "目前无法连接到该房间,请稍后再试。", + roomErrorBusy: "你已在通话中或在另一个房间里。", + roomErrorClosed: "此房间已关闭。", + roomAccessDenied: "此房间仅限受邀者。你需要邀请才能加入。", + roomsNoItems: "未找到房间。", + roomSearchPlaceholder: "搜索房间……", + roomInviteMode: "邀请", + tribeSectionRooms: "房间", + tribeRoomCreate: "创建房间", + tribeRoomsEmpty: "暂无房间。", + typeRoom: "房间", + typeRoomMember: "房间参与者", + statsRoom: "房间", + statsRoomMember: "房间参与者", + favoritesFilterRooms: "房间", + invitesRoomsTitle: "房间", + inviteCodeInvalid: "此邀请码无效或已被使用。", + inviteAlreadyMember: "你已经加入了,可以在对应模块中打开。", + actionFailed: "无法完成此操作,请重试。", + publishTooShort: "你的帖子太短了。", + walletSendFailed: "无法发送付款:{reason}", + pixeliaCoordsError: "这些坐标超出了网格范围。", + courtsErrTitle: "需要填写标题。", + courtsErrRespondent: "请说明案件针对谁。", + courtsErrRespondentId: "这不是有效的 Oasis ID。", + courtsErrMethod: "请选择案件的解决方式。", + courtsErrCaseNotFound: "找不到该案件。", + courtsErrBrief: "请填写你的答复。", + courtsErrStance: "请选择你的立场。", + courtsErrResult: "请填写结果。", + courtsErrTerms: "请填写协议条款。", + courtsErrMediator: "请至少添加一名调解人。", + courtsErrSelfMediator: "你不能在自己的案件中担任调解人。", + courtsErrJudge: "请选择一名法官。", + courtsErrSelfJudge: "你不能担任自己案件的法官。", + courtsErrVisibility: "请选择谁可以查看该案件。", + courtsErrNomination: "找不到该提名。", + opinionAlreadyGiven: "你已经发表过意见了。", + cipherPasswordShort: "密码长度至少为 32 个字符。", + errorForbidden: "你无权访问此内容。", + errorNotFound: "未找到。", + errorClosed: "已关闭。", + errorInviteOnly: "仅限受邀者,你需要邀请。", + errorTooLong: "文本太长。", + errorRequired: "缺少必填信息。", + errorInvalid: "部分信息无效。", + errorNotAuthor: "只有作者可以执行此操作。", + panicRemoved: "你的区块链已被删除。", padRecentSectionTitle: "最近协作板", padOpenSectionTitle: "开放协作板", padClosedSectionTitle: "关闭协作板", padCreateSectionTitle: "创建新协作板", padUpdateSectionTitle: "更新协作板", - padInviteGenerated: "邀请码已生成", typePad: "协作板", padNew: "新建", padAddFavorite: "添加到收藏", padRemoveFavorite: "从收藏中移除", - padClose: "关闭记事本", + padClose: "关闭", padBackToEditor: "返回编辑器", - padSearchPlaceholder: "搜索共享文本...", + padSearchPlaceholder: "在协作板中搜索...", modulesChatsLabel: "聊天", - modulesChatsDescription: "发现和管理加密聊天室的模块。", + modulesChatsDescription: "用于发现和管理聊天的模块。", typeChat: "聊天", typeChatMessage: "聊天消息", - chatLabel: "聊天", - chatMessageLabel: "聊天消息", chatsTitle: "聊天", - chatMineSectionTitle: "Your Chats", - chatRecentTitle: "Recent Chats", + chatMineSectionTitle: "你的聊天", + chatRecentTitle: "最近的聊天", chatFavoritesTitle: "收藏", - chatOpenTitle: "Open Chats", - chatClosedTitle: "Closed Chats", + chatOpenTitle: "开放的聊天", + chatClosedTitle: "已关闭的聊天", chatDescription: "描述", chatCategory: "类别", chatStatus: "状态", - chatFilterAll: "全部", - chatFilterMine: "我的", - chatFilterRecent: "最近", - chatFilterFavorites: "收藏", - chatFilterOpen: "开放", - chatFilterClosed: "已关闭", chatCreate: "创建聊天", - chatUpdate: "更新聊天", - chatDelete: "删除聊天", + chatUpdate: "更新", + chatDelete: "删除", chatClose: "关闭聊天", chatVisitChat: "访问聊天", chatUntitled: "无标题聊天", chatNoItems: "未找到聊天。", chatParticipants: "参与者", - chatStartChatting: "开始聊天!", - chatGenerateCode: "生成代码", - chatShareUrl: "分享链接", + chatMessagesLabel: "消息", + chatThreadsLabel: "话题", chatCreatedAt: "创建时间", - chatSearchPlaceholder: "搜索聊天...", + chatSearchPlaceholder: "在聊天中搜索...", chatStatusOpen: "开放", chatStatusInviteOnly: "仅限邀请", chatStatusClosed: "已关闭", chatSendMessage: "发送", + chatJumpLatest: "最新消息", + chatReply: "回复", + chatReplyCancel: "取消回复", + chatPin: "置顶", + chatUnpin: "取消置顶", + chatPinned: "已置顶", + chatPickChat: "选择一个聊天以打开", + chatNoneYet: "目前还没有可用的聊天。", + activitySearchPlaceholder: "在活动记录中搜索...", + trendingSearchPlaceholder: "在热门中搜索...", + opinionsSearchPlaceholder: "在观点中搜索...", + votesSearchPlaceholder: "在投票中搜索...", + gamesSearchPlaceholder: "在游戏中搜索...", + schoolTitle: "学校", + pmBotWiki: "WikiBot", + wikiBotEditedTitle: "你关注的维基页面已被编辑。", + wikiBotRestoredTitle: "你关注的维基页面已恢复到较早的版本。", + pmBotSchool: "EducaBot", + schoolBotEnrolledTitle: "一名新学生报名了您的课程。", + schoolBotInvitedTitle: "您已被邀请加入课程。", + schoolBotAdmittedTitle: "您已被课程录取。", + schoolBotCertificateTitle: "您已获得证书。", + schoolBotPassedTitle: "一名学生通过了您的课程。", + schoolBotLessonTitle: "已发布新课时。", + invitesSchoolTitle: "学校", + schoolEnrollLabel: "加入课程", + schoolExamRetryLater: "24 小时后可重试。", + schoolApproved: "已通过", + schoolExamPassMark: "及格分(0-10)", + schoolExamPassed: "通过", + schoolExamPending: "待完成", + schoolExamFailed: "未通过", + schoolExamAddQuestion: "添加问题", + schoolExamCreateQuestion: "创建问题", + schoolExamOption: "选项", + schoolExamCorrect: "正确选项", + schoolExamPoints: "分数", + schoolMaterialTextPlaceholder: "本课时的备注...", + schoolMaterials: "资料", + schoolAddMaterial: "添加资料", + schoolNoMaterials: "暂无资料。", + bankYourSchoolBalance: "您的学校收入", + schoolProgress: "进度", + schoolProgressMark: "标记为已完成", + schoolProgressDone: "已完成", + schoolExams: "考试", + schoolExamCreate: "创建考试", + schoolExamTitlePlaceholder: "考试标题", + schoolExamHelp: "每行一个问题:问题?; 选项 ; 正确选项* ; 选项", + schoolExamQuestionsPlaceholder: "SSB 是什么?; 一个数据库 ; 一个 P2P 协议* ; 一个浏览器", + schoolExamSubmit: "提交答案", + schoolExamScore: "您的得分", + schoolNoScoresYet: "暂无成绩。", + schoolLessonUnit: "单元/阶段", + schoolLessonUnitPlaceholder: "例如:阶段 1", + schoolLessonOrder: "顺序", + profileClearnetSchoolLabel: "学校", + schoolStartDate: "开始日期", + schoolSessionDate: "课次日期", + schoolCourseChat: "课程聊天", + agendaFilterSchool: "学校", + agendaFilterCampaigns: "活动", + agendaFilterLogistics: "物流", + schoolCourseType: "课程类型", + schoolTypeOpen: "开放", + schoolTypePaid: "付费", + schoolTypeInvite: "仅限邀请", + schoolLessonLocked: "课时已加密——请向教师索取课程密钥。", + schoolGrantAccess: "发送课程密钥", + schoolKeySent: "密钥已发送", + schoolOngoing: "进行中", + schoolFilterApplied: "已报名", + schoolFilterOpen: "开放", + schoolFilterFavorites: "收藏", + schoolPendingPayment: "待付款", + schoolPendingStudents: "待确认报名", + schoolViewBill: "查看账单", + schoolPaymentNote: "报名会生成一张 ECO 账单(转账)。转账签署后,您的报名即被确认。", + schoolCloseCourse: "关闭课程", + schoolReopenCourse: "重新开放课程", + schoolDescription: "在您的网络中教授和学习课程。", + schoolFilterAll: "全部", + schoolFilterMine: "我的", + schoolFilterTop: "热门", + schoolFilterRecent: "最新", + schoolCreateButton: "创建课程", + schoolCreateSectionTitle: "创建课程", + schoolUpdateSectionTitle: "更新课程", + schoolCourseTitlePlaceholder: "课程名称", + schoolCourseDescriptionPlaceholder: "你将在这门课程中教什么?", + schoolCourseImage: "课程图片(最大:75MB)", + schoolTags: "标签", + schoolTagsPlaceholder: "输入标签,用逗号分隔", + schoolPrice: "价格(ECO)", + schoolVisibility: "可见性", + schoolVisibilityPublic: "公开", + schoolVisibilityInvite: "邀请制", + schoolStatus: "状态", + schoolClosed: "关闭", + schoolFree: "免费", + schoolCreate: "创建课程", + schoolUpdate: "更新课程", + schoolSearchPlaceholder: "在学校中搜索...", + schoolSortTitle: "标题", + schoolNoCourses: "未找到课程。", + schoolStudents: "学生", + schoolTeacher: "教师", + schoolCreatedAt: "创建时间", + schoolLessons: "课时", + schoolNoLessons: "暂无课时。", + schoolEnrollToSee: "报名后即可查看本课程的课时。", + schoolEnrollButton: "报名", + schoolUnenrollButton: "退出课程", + schoolInviteRequired: "本课程为邀请制。", + schoolTeacherPanel: "教师面板", + schoolStudentPanel: "学生面板", + schoolAddLesson: "创建课时", + schoolLessonTitlePlaceholder: "课时标题", + schoolLessonTextPlaceholder: "描述本课时涵盖的内容...", + schoolInviteStudents: "邀请学生", + schoolInvitePlaceholder: "@id1, @id2...", + schoolInviteButton: "邀请", + schoolInvited: "已邀请", + schoolIssueCertificate: "创建证书", + schoolCertified: "已认证", + schoolCertificateTextPlaceholder: "证书备注(可选)", + schoolLessonOpen: "打开课程", + schoolCertificatePdf: "PDF", + schoolCertificates: "证书", + schoolNoCertificates: "尚未颁发证书。", + schoolEditButton: "编辑", + schoolDeleteButton: "删除", + typeSchool: "学校", + schoolCourseLabel: "课程", + emergencyLabel: "紧急情况", + wikiPageLabel: "维基", + statsSchoolCourse: "课程", + statsSchoolLesson: "课时", + statsSchoolEnroll: "报名", + statsSchoolCertificate: "学校证书", + modulesSchoolLabel: "学校", + modulesSchoolDescription: "知识交流模块。", + welcomeStepUxAction: "保存视图", + welcomeStepWorkflowTitle: "你打算如何使用 OASIS?", + welcomeStepWorkflowText: "Oasis 有很多模块。选择适合你的配置,只保留有用的模块。之后随时可以在“模块”中调整。", + welcomeStepWorkflowAction: "保存配置", + welcomeWorkflow_full: "默认", + chatRateLimitTitle: "消息过多", + chatRateLimitMessage: "你已达到该房间每小时的消息上限,请稍后再试。", chatMessagePlaceholder: "输入您的消息...", chatNoMessages: "暂无消息。", - chatLeave: "离开聊天", - chatInviteCodeLabel: "输入邀请码", - chatJoinByInvite: "加入", chatTitlePlaceholder: "聊天标题", chatDescriptionPlaceholder: "聊天描述", - chatTagsPlaceholder: "标签1, 标签2", + chatTagsPlaceholder: "输入标签,用逗号分隔", chatImageLabel: "选择图片文件 (.jpeg, .jpg, .png, .gif)", - chatInviteCode: "邀请码", chatAuthor: "作者", - chatCreated: "创建", chatAddFavorite: "添加到收藏", chatRemoveFavorite: "从收藏中移除", chatPM: "私信", chatStatusLabel: "状态", + chatTypeLabel: "聊天类型", chatCategoryLabel: "类别", - chatParticipantsLabel: "参与者", gamesTitle: "游戏", gamesDescription: "发现并玩一些游戏。", gamesFilterAll: "全部", gamesPlayButton: "开始游戏!", - gamesBackToGames: "返回游戏", modulesGamesLabel: "游戏", modulesGamesDescription: "用于发现和玩游戏的模块。", modulesGraphosLabel: "Graphos", @@ -3325,8 +4177,6 @@ module.exports = { gamesArkanoidDesc: "用你的球拍和球打破所有砖块。经典街机挑战。", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "与AI对战的经典乒乓球。先得5分者获胜。", - gamesOutrunTitle: "Outrun", - gamesOutrunDesc: "与时间赛跑!躲避交通障碍,在时间用完前到达终点。", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "驾驶飞船穿越小行星带。在它们撞上你之前将其摧毁。", gamesRockPaperScissorsTitle: "石头剪刀布", @@ -3336,68 +4186,57 @@ module.exports = { gamesFlipFlopTitle: "FlipFlop", gamesFlipFlopDesc: "抛硬币,猜正面还是反面。你有多幸运?", games8BallTitle: "8Ball Pool", - games8BallDesc: "Top-down pool. Click to aim, hold to charge power. Pot all balls in the fewest shots.", - gamesArtilleryTitle: "Artillery", - gamesArtilleryDesc: "Aim your cannon, factor in the wind, and hit the target. 5 rounds, fewest shots wins.", - gamesLabyrinthTitle: "Labyrinth", - gamesLabyrinthDesc: "Escape the maze before your moves run out. Each level gets bigger and harder.", + games8BallDesc: "俯视视角台球。点击瞄准,按住蓄力。用最少的击球次数打进所有球。", + gamesArtilleryTitle: "炮兵", + gamesArtilleryDesc: "瞄准你的大炮,考虑风向,击中目标。共 5 回合,用炮最少者获胜。", + gamesLabyrinthTitle: "迷宫", + gamesLabyrinthDesc: "在步数用完之前逃出迷宫。每一关都更大、更难。", gamesCocomanTitle: "Cocoman", - gamesCocomanDesc: "Eat all the dots, avoid the ghosts. Turn-based Pac-Man — every key press counts.", + gamesCocomanDesc: "吃掉所有豆子,避开幽灵。幽灵实时移动——别停下来!", gamesTetrisTitle: "Tetris", - gamesAudioPendulumTitle: "Audio Pendulum", - gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", - gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", - gamesQuakeTitle: "Quake Arena", - gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", - gamesFilterScoring: "SCORING", - gamesHallOfFame: "Hall of Fame", - gamesHallPlayer: "Player", - gamesHallScore: "Score", - gamesNoScores: "No scores yet.", + gamesAudioPendulumTitle: "声音摆", + gamesAudioPendulumDesc: "带有实时音频合成的混沌物理模拟器。角速度变成频率,峰值变成鼓点。没有两次模拟听起来相同。", + gamesVisualDjTitle: "Visual DJ", + gamesVisualDjDesc: "在两个唱盘上现场混合视觉效果:为每个唱盘选择图案,调节速度、色彩和缩放,在两者之间推拉切换,并随节拍闪光。点击打拍子,或让麦克风驱动脉动。", + gamesTetrisDesc: "经典的下落方块。消除整行得分。你能坚持多久?", + gamesMinesweeperTitle: "扫雷", + gamesMinesweeperDesc: "经典扫雷。清空棋盘即可升级——更大的网格、更多的地雷、更高的分数。", + gamesFilterScoring: "得分", + gamesHallOfFame: "名人堂", + gamesHallPlayer: "玩家", + gamesHallScore: "得分", + gamesNoScores: "暂无得分。", calendarsTitle: "日历", calendarTitle: "日历", modulesCalendarsLabel: "日历", modulesCalendarsDescription: "用于发现和管理日历的模块。", typeCalendar: "日历", - calendarFilterAll: "全部", - calendarFilterMine: "我的", - calendarFilterOpen: "开放", - calendarFilterClosed: "关闭", - calendarFilterRecent: "最近", - calendarFilterFavorites: "收藏", calendarCreate: "创建日历", calendarUpdate: "更新", - calendarDelete: "删除", calendarTitleLabel: "标题", calendarTitlePlaceholder: "日历标题...", calendarStatusLabel: "状态", + calendarTypeLabel: "日历类型", calendarStatusOpen: "开放", calendarStatusClosed: "关闭", - calendarDeadlineLabel: "Deadline", + calendarDeadlineLabel: "截止日期", calendarTagsLabel: "标签", - calendarTagsPlaceholder: "标签1, 标签2...", + calendarTagsPlaceholder: "输入标签,用逗号分隔", calendarParticipantsLabel: "参与者", - calendarParticipantsCount: "参与者", - calendarVisitCalendar: "访问日历", calendarCreated: "已创建", - calendarAuthor: "作者", calendarJoin: "加入", calendarGenerateInvite: "生成邀请", - calendarInviteCodePlaceholder: "输入邀请码...", - calendarValidateInvite: "验证邀请码", - calendarJoined: "已加入", - calendarAddDate: "添加日期", - calendarAddNote: "添加笔记", calendarDateLabel: "日期", calendarDatePlaceholder: "描述这个日期...", - calendarNoteLabel: "笔记", + calendarNoteLabel: "备注", calendarNotePlaceholder: "添加笔记...", calendarFirstDateLabel: "日期", calendarFirstNoteLabel: "备注", - calendarIntervalLabel: "Interval", - calendarIntervalWeekly: "Weekly", - calendarIntervalMonthly: "Monthly", - calendarIntervalYearly: "Yearly", + calendarIntervalLabel: "间隔", + calendarIntervalNone: "不重复", + calendarIntervalWeekly: "每周", + calendarIntervalMonthly: "每月", + calendarIntervalYearly: "每年", calendarFormDescription: "描述", calendarNoDates: "尚未添加日期。", calendarNoNotes: "暂无笔记。", @@ -3405,8 +4244,8 @@ module.exports = { calendarsDescription: "在您的网络中发现和管理日历。", calendarMonthPrev: "← 上一月", calendarMonthNext: "下一月 →", - calendarMonthLabel: "日期", - calendarsShareUrl: "分享链接", + calendarMonthNames: "一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月", + calendarDayInitials: "一,二,三,四,五,六,日", calendarAllSectionTitle: "日历", calendarRecentSectionTitle: "最近的日历", calendarFavoritesSectionTitle: "收藏", @@ -3416,11 +4255,10 @@ module.exports = { calendarCreateSectionTitle: "创建新日历", calendarUpdateSectionTitle: "更新日历", calendarAddFavorite: "添加到收藏", - calendarDeleteNote: "Delete", + calendarDeleteNote: "删除", calendarRemoveFavorite: "从收藏移除", - calendarSearchPlaceholder: "搜索日历...", + calendarSearchPlaceholder: "在日历中搜索...", calendarAddEntry: "添加条目", - calendarLeave: "离开日历", statsCalendar: "日历", statsCalendarDate: "日历日期", statsCalendarNote: "日历笔记", @@ -3431,6 +4269,146 @@ module.exports = { tribeContentAccessDenied: "访问被拒绝", tribeContentAccessDeniedMsg: "此内容属于一个部落。您必须是成员才能访问它。", tribeViewTribes: "查看部落", + tribeFilesEmpty: "暂无种子。", + filesLabel: "种子", + profileClearnetFilesLabel: "种子", + filesTitle: "种子", + filesDescription: "浏览和管理网络中的种子。", + fileAllSectionTitle: "种子", + fileMineSectionTitle: "我的种子", + fileRecentSectionTitle: "最近的种子", + fileTopSectionTitle: "热门种子", + fileFavoritesSectionTitle: "收藏", + fileFilterAll: "全部", + fileFilterMine: "我的", + fileFilterRecent: "最近", + fileFilterTop: "热门", + fileFilterFavorites: "收藏", + fileCreateSectionTitle: "上传种子", + fileUpdateSectionTitle: "更新种子", + fileCreateButton: "上传种子", + fileUpdateButton: "更新", + fileAddFavoriteButton: "添加到收藏", + fileRemoveFavoriteButton: "从收藏移除", + fileFileLabel: "选择文件(最大:75MB)", + calendarFilterRecent: "最近", + calendarFilterAll: "全部", + calendarFilterMine: "我的", + calendarFilterFavorites: "收藏", + calendarFilterOpen: "开放", + calendarFilterClosed: "已关闭", + padFilterRecent: "最近", + padFilterAll: "全部", + padFilterMine: "我的", + padFilterOpen: "开放", + padFilterClosed: "已关闭", + chatFilterRecent: "最近", + chatFilterAll: "全部", + chatFilterMine: "我的", + chatFilterFavorites: "收藏", + chatFilterOpen: "开放", + chatFilterClosed: "已关闭", + licenseLabel: "许可协议", + phoneTitle: "电话", + phoneDescription: "管理你的端到端加密视频通话。", + phoneUnavailable: "此设备上无法使用通话。", + phoneCallButton: "呼叫", + profileVisibilityPhone: "电话号码", + phoneNumberUnknown: "你的网络中没有人使用号码 {number}。", + phoneNumberChoose: "请确认你要拨打 {number} 联系的人:", + phoneAnswer: "接听", + phoneSpeaker: "扬声器", + phoneEarpiece: "听筒", + phoneReject: "拒绝", + phoneHangup: "挂断", + phoneMute: "静音", + phoneUnmute: "取消静音", + phoneCameraOn: "摄像头 开", + phoneCameraOff: "摄像头 关", + settingsPhoneVideo: "视频", + settingsPhoneVideoOn: "通话和房间中允许", + settingsPhoneVideoOff: "仅音频", + phoneSilence: "不再收听", + phoneUnsilence: "恢复收听", + phoneCalling: "正在呼叫", + phoneIsCalling: "正在呼叫你", + notifyNewPmFrom: "新私信,来自", + notifyNewPamFrom: "新 PaM,来自", + notifyUpdateLabel: "更新", + updateInstalledLabel: "已安装版本", + updateAvailableLabel: "可用版本", + updateChangesLabel: "更改", + updateFilesLabel: "文件", + updateOriginLabel: "来源", + phoneRecording: "正在录制 PaM,发给", + phoneLeaveMessage: "无人接听:请在提示音后留言。", + phoneLeaveMessageBusy: "对方正在通话中:请在提示音后留言。", + phoneLeaveMessageUnreachable: "无法联系到对方:请在提示音后留言。", + phoneConnecting: "正在连接", + phoneSendPam: "发送 PaM", + phoneCancel: "取消", + phoneSearchPlaceholder: "在电话中搜索...", + phonePamTitle: "音频录音", + phoneNew: "新", + phoneDelete: "删除", + phoneNoPams: "你没有音频录音。", + phoneHistoryTitle: "历史记录", + phoneLatestTitle: "最近通话", + phoneCreateButton: "新通话", + phoneLimitsHint: "每次通话最多 {n} 位居民;多于一位即为联合通话。", + phoneErrorPub: "不能呼叫 pub。", + phoneJointCall: "联合通话", + phoneWith: "对象", + phoneResult: "结果", + phoneWhen: "时间", + phoneDuration: "时长", + phoneOutgoing: "呼出", + phoneIncoming: "呼入", + phoneNoHistory: "暂无记录。", + phoneOutcomeEnded: "已接听", + phoneOutcomeMissed: "未接", + phoneOutcomeRejected: "已拒绝", + phoneOutcomeNoanswer: "无人接听", + phoneOutcomeCancelled: "已取消", + phoneOutcomePam: "已发送 PaM", + phoneOutcomeFailed: "失败", + phoneDialAdd: "添加", + phoneDialBack: "删除", + phoneDialClear: "清空", + phoneDialPlaceholder: "号码或 Oasis ID", + phoneDialInhabitants: "居民", + phoneDialRemove: "移除", + phoneOwnNumber: "这是你自己的号码。", + phoneSettingsDesc: "选择谁可以呼叫你。", + phoneDnd: "免打扰", + phoneOpen: "开放", + phoneRecipientRefuses: "你要呼叫的居民不接受来电。", + phoneRefusedNotice: "这些居民不接受来电,未被呼叫:", + fileEmptyError: "文件为空。请选择其他文件。", + fileTitleLabel: "标题", + fileTitlePlaceholder: "标题", + fileDescriptionLabel: "描述", + fileDescriptionPlaceholder: "描述", + fileTagsLabel: "标签", + fileTagsPlaceholder: "输入标签,用逗号分隔", + fileSizeLabel: "大小", + fileDownloadButton: "下载", + fileDetailsButton: "详情", + fileNoFile: "没有种子文件", + fileSearchPlaceholder: "在种子中搜索...", + fileSearchButton: "搜索", + fileSortRecent: "最新", + fileSortOldest: "最旧", + fileSortTop: "最多投票", + fileNoMatch: "没有匹配的种子。", + fileUpdatedAt: "已更新", + typeFile: "种子", + modulesFilesLabel: "文件", + modulesFilesDescription: "发现和管理文件的模块。", + tribeSectionFiles: "种子", + fileDescription: "描述", + fileDownload: "下载", + fileTypeLabel: "类型", torrentsTitle: "种子", torrentsDescription: "浏览和管理网络中的种子。", torrentAllSectionTitle: "种子", @@ -3456,49 +4434,93 @@ module.exports = { torrentDescriptionLabel: "描述", torrentDescriptionPlaceholder: "描述", torrentTagsLabel: "标签", - torrentTagsPlaceholder: "输入以逗号分隔的标签", + torrentTagsPlaceholder: "输入标签,用逗号分隔", torrentSizeLabel: "大小", - torrentDownloadButton: "DOWNLOAD IT!", + torrentFromContent: "生成种子", + torrentFromContentMissing: "该文件在本节点尚不可用,请稍后再试。", + torrentDownloads: "下载", + torrentFetchOasis: "在 Oasis 中下载", + torrentSaveFile: "保存文件", + torrentSeeds: "做种", + seedsLabel: "做种数", + seedAction: "支持", + torrentSourceFile: "文件", + torrentStateQueued: "排队中", + torrentStateProbing: "正在寻找做种者", + torrentStateDownloading: "下载中", + torrentStateDone: "已完成", + torrentStateFailed: "失败", + torrentStateNoSource: "暂无在线做种者,等待中", + torrentStateCancelled: "已取消", + torrentNoDownloads: "还没有下载。", + torrentOriginalSection: "原始文件", + torrentCancel: "取消", + torrentDownloadButton: "下载", + torrentOasisButton: "OASIS", + torrentExternalButton: "EXTERNAL", + torrentDetailsButton: "详情", torrentNoFile: "没有种子文件", noTorrents: "未找到种子。", - torrentSearchPlaceholder: "搜索标题、标签、作者...", + noFiles: "未找到种子。", + torrentSearchPlaceholder: "在种子中搜索...", torrentSearchButton: "搜索", torrentSortRecent: "最新", torrentSortOldest: "最旧", torrentSortTop: "最多投票", torrentNoMatch: "没有匹配的种子。", - torrentMessageAuthorButton: "给作者发消息", torrentUpdatedAt: "已更新", statsTorrent: "种子", + statsFile: "文件", typeTorrent: "种子", modulesTorrentsLabel: "种子", modulesTorrentsDescription: "发现和管理种子的模块。", favoritesFilterTorrents: "种子", + favoritesFilterFiles: "文件", favoritesFilterMarket: "市场", + favoritesFilterEvents: "活动", + favoritesFilterForum: "论坛", + favoritesFilterHousing: "住房", + favoritesFilterJobs: "工作", + favoritesFilterProjects: "项目", + favoritesFilterReports: "报告", + favoritesFilterTasks: "任务", + favoritesFilterTransfers: "转账", + favoritesFilterVotes: "投票", favoritesFilterShopProducts: "商店商品", tribeSectionTorrents: "种子", tribeCreateTorrent: "上传种子", - tribeMediaTypeTorrent: "种子", + tribeCreateFile: "上传文件", settingsWishTitle: "愿望", - settingsWishDesc: "配置您的头像愿望。", - settingsWishWhole: "Multiverse", - settingsWishMutuals: "Only mutual-support", - settingsWishOnlyLan: "Only LAN", + settingsWishDesc: "决定你如何访问全部内容以及你在 Oasis 网络中的体验。", + settingsWishWhole: "多元宇宙", + settingsWishMutuals: "仅限互相支持", + settingsWishOnlyLan: "仅限局域网", + settingsWishLocal: "本地", settingsPmVisibilityTitle: "私信", settingsPmVisibilityDesc: "配置您希望在网络中的私信曝光级别。", - settingsPmVisibilityWhole: "Multiverse", - settingsPmVisibilityMutuals: "Only mutual-support", + settingsPmVisibilityWhole: "多元宇宙", + settingsPmVisibilityMutuals: "仅限互相支持", + mute: "静音", + unmute: "取消静音", + relationshipMuted: "已被你静音", + hideContent: "对我隐藏", + unhideContent: "重新显示", + hiddenByYou: "已被你隐藏", + settingsTabContent: "内容", + settingsHiddenTitle: "隐藏的内容", + settingsHiddenDesc: "你静音的居民和你隐藏的内容。", + settingsMutedLabel: "已静音的居民", + settingsHiddenLabel: "已隐藏的内容", + settingsMutedEmpty: "没有静音任何人。", + settingsHiddenEmpty: "没有隐藏任何内容。", + settingsPmVisibilityFollowing: "仅限我关注的人", + settingsMentionsVisibilityTitle: "提及", + settingsMentionsVisibilityDesc: "选择谁的提及可以送达你。", pmMutualNotice: "出站是 UX 保护。入站是关注过滤器。", - pmBlockedNonMutual: "只能向相互支持的居民发送私信。", inhabitantsPendingFollowsTitle: "待处理的支持请求", inhabitantsPendingAccept: "接受", inhabitantsPendingReject: "拒绝", - bxEncrypted: "已加密", - bxEncryptedHexLabel: "密文(预览)", tribeSectionGovernance: "治理", - tribeSubStatusPublic: "公开", - tribeSubStatusPrivate: "私密", - tribeSubInheritedPrivate: "私密(继承自主部落)", tribePadInviteRequired: "您无权访问 pad。请求邀请。", tribeChatInviteRequired: "您无权访问聊天。请求邀请。", tribeGovernanceDesc: "此部落的内部治理。", @@ -3537,209 +4559,195 @@ module.exports = { tribeGovNoRevocations: "暂无任何撤销", tribeGovNoHistorical: "暂无任何记录", logsTitle: "日志", - logsDescription: "记录您在网络中的体验。", - logsReadMore: "阅读更多", + logsDescription: "记录你个人经历的私人日记。", logsViewTitle: "日志", logsColumnType: "类型", logsView: "查看", - logsManualPrompt: "撰写您的日志", logsUpdateButton: "更新", - logsEditTitle: "编辑条目", - logsCreateDescription: "记录您的体验...", + logsEditTitle: "更新日志", logsCreateTitle: "创建条目", logsWriteButton: "撰写", logsTextPlaceholder: "描述您的体验...", - logsTextField: "文本", - logsLabelPlaceholder: "标签...", - logsLabelField: "撰写您的日志(标签)", - logsAiDisabledWarn: "在 /modules 中启用 AI 模块以使用 AI 撰写的日志。", - logsAiContextValue: "区块链日", - logsAiContext: "上下文", - logsAiModStatus: "AImod", - logsModeApply: "应用!", - logsModeAIWritten: "AI-Assistant", + logsModeAIWritten: "AI 助手", logsModeManual: "手动", logsModeAI: "AI", - logsColumnDelete: "删除", - logsColumnEdit: "编辑", logsColumnLog: "日志", logsColumnDate: "日期", logsDelete: "删除", - logsEdit: "编辑", + logsEdit: "更新", logsFilterAlways: "始终", logsFilterYear: "去年", logsFilterMonth: "上月", logsFilterWeek: "上周", logsFilterToday: "今天", - logsFilterRecent: "最近", - logsFilterAll: "全部", + logsComments: "评论", + logsAuthorEntries: "条目", logsCreate: "创建条目", logsExport: "导出日志", - logsExportOne: "导出", + logsExportOne: "导出日志", logsViewDetails: "查看详情", logsGenerateButton: "生成文本", logsSearchText: "在日志中搜索...", logsSearchAnyType: "任意类型", logsSearchButton: "搜索", logsEmpty: "暂无条目。", + logsNothingNew: "没有可记录的新操作。", modulesLogsLabel: "日志", - modulesLogsDescription: "用于(通过 AI 助手)记录体验的模块。", + modulesLogsDescription: "用于记录你个人经历的私人日记模块。", statsLogsTitle: "日志", - statsLogsEntries: "条目", typeLog: "日志", - blockchainCycle: "周期", larpTitle: "L.A.R.P.", larpDescription: "用于协作实验的实时角色扮演层。", + larpNoHousesMatch: "没有匹配的家族。", + larpSearchPlaceholder: "在L.A.R.P.中搜索...", larpAllHouses: "家族:", larpFilterRuling: "执政", larpFilterHouses: "家族", - larpFilterRules: "FAQ", + larpFilterRules: "常见问题", larpRulesTitle: "L.A.R.P. FAQ", - larpRulesEntryTitle: "起始家族", larpRulesEntryText: "每位居民默认从 ACADEMIA 开始。在 ACADEMIA 中,从“加入家族”面板选择一个家族:这将打开其入会测试。回答 10 道问题并提交。若达到阈值(7/10),你将被自动接纳进入该家族;否则将被拒绝,留在 ACADEMIA 中。无论如何,系统会记录你的 OasisID 和尝试周期,并在 30 个周期的冷却期结束前阻止再次尝试。", - larpRulesLeaveText: "任何家族的成员都可以随时从其家族页面返回 ACADEMIA;这样会重置其成员身份,但不会取消测试的冷却时间。", - larpRulesGovernanceTitle: "执政墙", larpRulesGovernanceText: "在其执政周期内,执政家族佩戴“执政”徽章,是唯一其墙面在“执政”标签下公开显示的家族。", - larpRulesSignTitle: "L.A.R.P. 徽章", + larpRulesWallText: "每个家族都有一面墙,供其成员书写。家族墙对外不公开,但执政家族的墙和 ACADEMIA 的墙人人可读。", larpRulesSignText: "居民可在 /profile/edit > 传感器 中启用,将当前家族的图像作为徽记显示在个人资料、作者卡和居民卡上。徽记链接到家族页面。", larpPostsTitle: "墙", larpPostsEmpty: "暂无发布。", - larpPostLabel: "新发布", larpPostPlaceholder: "这个家族想说什么?", - larpPostPublic: "公开(所有人可见,否则仅限成员)", larpPostSubmit: "发布", - larpPostMembersOnly: "仅成员", larpCycleLabel: "周期:", - audioBackToBcs: "Back to BCS", + audioBackToBcs: "返回 BCS", audioFilterBcs: "BCS", - audioTranscodeButton: "TRANSCODE", - audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", - audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", - audioTranscodeDetailTitle: "Transcode", - audioTranscodeStegoEmpty: "(none)", - audioTranscodeStegoMessage: "TEXT", - audioTranscodeStegoNotFound: "No steganographic payload could be decoded from this audio.", - audioTranscodeStegoOasisId: "By", - audioTranscodeStegoTimestamp: "Generated at", + audioTranscodeButton: "转码", + audioTranscodeCompositionEmpty: "此音频不包含已保存的区块链作曲。", + audioTranscodeDetailDescription: "解码嵌入的载荷和原始区块链作曲图。", + audioTranscodeDetailTitle: "转码", + audioTranscodeStegoEmpty: "(无)", + audioTranscodeStegoMessage: "文本", + audioTranscodeStegoNotFound: "无法从此音频中解码出隐写载荷。", + audioTranscodeStegoTimestamp: "生成时间", audioTranscodeStegoUnknown: "—", - bankExchangeEcoTaxAnnual: "ECOin Taxes (annual)", - bankExchangeEcoTaxMonthly: "ECOin Taxes (monthly)", - bankOverviewYourTaxes: "Your taxes", - bankRulesAlpha: "Alpha (max share of pub balance per epoch)", + bankExchangeEcoTaxAnnual: "ECOin 税费(年)", + bankExchangeEcoTaxMonthly: "ECOin 税费(月)", + bankOverviewYourTaxes: "税款合计", + bankRulesAlpha: "Alpha(每个周期 pub 余额的最大份额)", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", - bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", - bankRulesArchTaxRate: "Rate", - bankRulesArchTaxTitle: "ARCH Tax", - bankRulesCapPerEpoch: "Cap per epoch (absolute)", - bankRulesCapUser: "Cap per user per epoch", - bankRulesCarbonFactor: "Carbon factor", + bankRulesArchTaxNote: "ARCH 税反映了扩展和维护网络档案的长期成本:你的数据在网络中存在得越久,你分担的维护成本就越大。", + bankRulesArchTaxRate: "税率", + bankRulesArchTaxTitle: "ARCH 税", + bankRulesCapPerEpoch: "每个周期上限(绝对值)", + bankRulesCapUser: "每位用户每个周期的上限", + bankRulesCarbonFactor: "碳系数", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", - bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", - bankRulesChipTitle: "ECO Tax chip color band", + bankRulesChipNote: "每个项目的标签颜色取决于它相对于网络中观察到的最大消息的大小,并按活跃居民数量进行平滑——更大的网络会分摊负载,让更多项目进入绿色区间。", + bankRulesChipTitle: "ECO 税标签颜色区间", bankRulesEcoTaxFormula: "ECO Tax(user) = (userBytes / 1 MiB) × 0.095 × ecoinPerGramCO2", - bankRulesEcoTaxNote: "ECO Tax is deducted from the gross UBI when the epoch allocation is computed. The deducted ECO are not redistributed in this epoch — they remain in the pub balance and feed the next epoch's pool through reserveMin / alpha caps.", - bankRulesEcoTaxRate: "Rate", - bankRulesEcoTaxTitle: "ECO Tax", - bankRulesEpochKind: "Epoch granularity", - bankRulesFloor: "Floor per user (gross)", - bankRulesGraceDays: "Claim grace period", + bankRulesEcoTaxNote: "在计算每个周期(epoch)的分配时,ECO 税从 UBI 总额中扣除。扣除的 ECO 不会在本周期重新分配——它们留在 pub 余额中,并通过 reserveMin / alpha 上限注入下一周期的资金池。", + bankRulesEcoTaxRate: "税率", + bankRulesEcoTaxTitle: "ECO 税", + bankRulesEpochKind: "周期粒度", + bankRulesFloor: "每位用户的保底(总额)", + bankRulesGraceDays: "领取宽限期", bankRulesKarmaFormula: "karma = max(0, round(scoreFromActions − carbonGramsForUser))", - bankRulesKarmaNote: "Karma is derived from your actions (posts, votes, parliament/courts participation, etc.) with a time-decay factor, minus the carbon grams generated by your feed.", - bankRulesKarmaTitle: "Karma (user engagement score)", + bankRulesKarmaNote: "业力来自你的行为(发帖、投票、参与议会/法庭等),带有随时间衰减的系数,并减去你的动态所产生的碳克数。 你的内容收到的反馈同样计入:正面的评价、投票、反应和播放会增加业力,当多位居民一致给出负面评价或投票时则会扣减。", + bankRulesKarmaTitle: "业力(用户参与度评分)", bankRulesNetFormula: "net_UBI(user) = max(0, gross_UBI − ECO Tax)", bankRulesPoolFormula: "Pool = min(pubBal − reserveMin, capPerEpoch, alpha × pubBal)", - bankRulesPoolTitle: "Monthly pool", - bankRulesReserveMin: "Reserve minimum (kept in pub balance)", + bankRulesPoolTitle: "月度资金池", + bankRulesReserveMin: "最低储备(保留在 pub 余额中)", bankRulesShareFormula: "gross_UBI(user) = clamp(Pool × w / Σw, floor_user, cap_user_epoch)", - bankRulesShareTitle: "Per-user share", + bankRulesShareTitle: "每位用户的份额", bankRulesWeightFormula: "w = clamp(1 + karma/100, w_min, w_max)", - bankRulesWMax: "Maximum weight (w_max)", - bankRulesWMin: "Minimum weight (w_min)", - bankTaxes: "Taxes", - bankTaxesAnnualEcoin: "ECO Tax (annual)", - bankTaxesArchTaxAnnual: "ARCH Tax (annual)", - bankTaxesArchTaxFirstBlock: "Your first block age", - bankTaxesArchTaxFootprintNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost.", - bankTaxesArchTaxLifetime: "ARCH Tax (lifetime)", - bankTaxesArchTaxMonthly: "ARCH Tax (monthly)", - bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", - bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", - bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxRate: "Rate", - bankTaxesArchTaxTitle: "ARCH Tax", - bankTaxesArchTaxUserAmount: "Your ARCH tax (price to return)", - bankTaxesEcoTaxLifetime: "ECO Tax (lifetime)", - bankTaxesEcoTaxTitle: "ECO Tax", - bankTaxesLookupAuthor: "Author", - bankTaxesLookupButton: "Inspect", - bankTaxesLookupCarbon: "Carbon footprint", - bankTaxesLookupEcoin: "ECO Tax", - bankTaxesLookupNotFound: "No block found in your local feed with that ID.", - bankTaxesLookupPlaceholder: "Block ID (e.g. %abc...=.sha256)", - bankTaxesLookupSize: "Size", - bankTaxesLookupTitle: "Inspect a block", - bankTaxesLookupType: "Type", - bankTaxesMonthlyEcoin: "ECO Tax (monthly)", - bankTaxesRate: "Rate", - bankTaxesSpan: "Sampling span", - bankTaxesTotalAnnual: "Total (annual)", - bankTaxesTotalBlocks: "Total blocks", - bankTaxesTotalBytes: "Total bytes", - bankTaxesTotalCarbon: "Total carbon", - bankTaxesTotalEcoin: "Total ECO tax (lifetime)", - bankTaxesTotalLifetime: "Total (lifetime)", - bankTaxesTotalMonthly: "Total (monthly)", - bankTaxesTypesApply: "Set my taxes", - bankTaxesTypesLabel: "Select which taxes you want to pay", - bankTaxesUserAmount: "Your ECO tax (price to return)", - bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", - bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", - blockchainBackToBlockexplorer: "Back to blockexplorer", - blockchainBlockNotAvailable: "This block is not available in your local feed. It may belong to a peer you are not yet replicating from.", - ecoTaxLabel: "ECO Tax", - gamesHallDate: "Date", - gamesNewRecord: "New Record", - indexingEditProfileLink: "Set up my avatar", - indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", - indexingRefreshNote: "This page refreshes every 10 seconds.", - indexingTitle: "Synchronizing", - melodyAllEmpty: "No BCS compositions from other inhabitants yet.", - melodyByLabel: "By", - melodyNoteUnavailable: "block not in your local feed", - melodyUploadBcsButton: "Publish", - melodyUploadBcsNameNote: "The audio will be published with the auto-generated name", - melodyUploadBcsTitle: "Publish", - melodyUploadStegoLabel: "Hidden message (steganography)", - melodyUploadStegoMaxLabel: "max 280 characters", - melodyUploadStegoPlaceholder: "Optional.", - padsNoItems: "No pads found.", - padVersionHistory: "Version History", - padVersionView: "View", - statsChat: "Chats", - statsChatMessage: "Chat messages", - statsGameScore: "Gaming", - statsPad: "Pads", - statsPadEntry: "Pad entries", - statsTombstoneEmpty: "No tombstones in the network yet.", + bankRulesWMax: "最大权重 (w_max)", + bankRulesWMin: "最小权重 (w_min)", + bankTaxes: "税费", + bankTaxesAnnualEcoin: "ECO 税(年)", + bankTaxesArchTaxAnnual: "ARCH 税(年)", + bankTaxesArchTaxFirstBlock: "你的首个区块的时长", + bankTaxesArchTaxFootprintNote: "ARCH 税反映了扩展和维护网络档案的成本。它根据你发布的第一个区块与网络中最新区块之间的时间差计算——你的历史在档案中存在得越久,你分担的维护成本就越大。", + bankTaxesArchTaxLifetime: "ARCH 税(累计)", + bankTaxesArchTaxMonthly: "ARCH 税(月)", + bankTaxesArchTaxNoteBold: "直接降低 ARCH 税", + bankTaxesArchTaxNoteIntro: "你的 ARCH 税从网络在你的 UBI 之上产生的盈余中扣除;UBI 的基础值被设定为不可动摇的最低值。该税绝不会从每位居民作为 UBI 应得的固定金额中扣除。扣除的资金会注入财富再分配算法,并通过其他居民的 UBI 领取重新流向他们。较新的居民以及专门从事归档、复制和维护任务的居民,很可能会有专门的任务代你保持网络档案的健康。或者,他们可以致力于", + bankTaxesArchTaxNoteOutro: ",方式包括修剪协议、冗余数据去重以及共享维护基础设施,而他们的持续贡献可能会由网络共识提出要求。", + bankTaxesArchTaxRate: "税率", + bankTaxesArchTaxTitle: "ARCH 税", + bankTaxesArchTaxUserAmount: "你的 ARCH 税(需返还的金额)", + bankTaxesEcoTaxLifetime: "ECO 税(累计)", + bankTaxesEcoTaxTitle: "ECO 税", + bankTaxesLookupAuthor: "作者", + bankTaxesLookupButton: "检查", + bankTaxesLookupCarbon: "碳足迹", + bankTaxesLookupEcoin: "ECO 税", + bankTaxesLookupNotFound: "在你的本地动态中未找到该 ID 的区块。", + bankTaxesLookupPlaceholder: "区块 ID(例如 %abc...=.sha256)", + bankTaxesLookupSize: "大小", + bankTaxesLookupTitle: "检查区块", + bankTaxesLookupType: "类型", + bankTaxesMonthlyEcoin: "ECO 税(月)", + bankTaxesRate: "税率", + bankTaxesSpan: "采样区间", + bankTaxesTotalAnnual: "总计(年)", + bankTaxesTotalBlocks: "区块总数", + bankTaxesTotalBytes: "总字节数", + bankTaxesTotalCarbon: "碳总量", + bankTaxesTotalEcoin: "ECO 税总计(累计)", + bankTaxesTotalLifetime: "总计(累计)", + bankTaxesTotalMonthly: "总计(月)", + bankTaxesTypesApply: "设置我的税", + bankTaxesTypesLabel: "选择你要缴纳的税种", + bankTaxesUserAmount: "你的 ECO 税(需返还的金额)", + bankTaxesUserNoteChips: "每个项目旁显示的 ECO 税标签会根据该项目相对于网络中见过的最大区块的大小改变颜色(绿 / 黄 / 红),并按活跃居民数量进行平滑——更大的太阳朋克网络会分摊负载,让更多项目进入绿色区间。", + bankTaxesUserNoteOtherParams: "ECO 税目前根据每个区块的碳足迹计算,但随着时间推移可能纳入其他参数——复制所消耗的能量、节点间的冗余存储、blob 带宽、解密的计算成本、相关交易的挖矿足迹,或网络同意计价的任何其他可衡量负载。", + blockchainBackToBlockexplorer: "返回区块浏览器", + blockchainBlockNotAvailable: "此区块在你的本地动态中不可用。它可能属于你尚未从其复制的节点。", + ecoTaxLabel: "ECO 税", + gamesHallDate: "日期", + gamesNewRecord: "新纪录!", + indexingRefreshNote: "本页面每 10 秒刷新一次。", + indexingMessage: "Oasis 正在同步分布式节点网络。请稍候。", + indexingTitle: "正在同步", + melodyAllEmpty: "暂无其他居民的 BCS 作品。", + melodyNoteUnavailable: "该区块不在你的本地动态中", + melodyUploadBcsButton: "发布", + melodyUploadBcsNameNote: "音频将以自动生成的名称发布", + melodyUploadBcsTitle: "发布", + melodyUploadStegoLabel: "隐藏消息(隐写术)", + melodyUploadStegoMaxLabel: "最多 280 个字符", + melodyUploadStegoPlaceholder: "可选。", + padsNoItems: "未找到协作板。", + padVersionHistory: "版本历史", + padAttributionView: "按作者", + padVersionView: "查看", + statsChat: "聊天", + statsChatMessage: "聊天消息", + statsGameScore: "游戏", + statsWiki: "维基页面", + statsPad: "协作板", + statsPadEntry: "协作板条目", + statsTombstoneEmpty: "网络中暂无已删除记录(tombstone)。", typeGameScore: "GAME SCORE", bankTaxesLookupNote: "输入区块 ID 在本地数据流中查询。", bankTaxesUserNoteChipsClick: "点击任何标签以在 blockexplorer 中检查其区块。", mapUrlPlaceholder: "/maps/MAP_ID", - larpRulesInviteEntry: "邀请码", larpRulesInviteEntryText: "家族成员可以生成邀请码,提供对家族的直接访问。", larpRulesOneHouseText: "你在任何时刻最多只能属于一个家族。如果你不属于任何家族,你就在 ACADEMIA。每个非 ACADEMIA 的家族页面都向其成员显示一个\"离开家族\"按钮,将成员身份重置为 ACADEMIA。离开并不会取消测试冷却。", - larpRulesOneHouseTitle: "一次一个家族", - larpRulesTestEntry: "WILL 测试", - larpRulesTestEntryText: "每个选项对一个或多个家族加权;提交时,系统统计得分并自动将你纳入得分最高的家族。", + larpRulesTestEntryText: "在 WILL 测试中,每个选项会为一个或多个家族加权;提交后系统统计得分,并自动把你录入得分最高的家族。", larpWillTestHint: "完成后,你将被分配到与你的回答最匹配的家族。你的单个回答在本地处理,从不存储 — 只有最终的家族分配会发布到你的信息流。", larpWillTestTitle: "WILL 测试", - settingsLanDesc: "定期向同一局域网中的其他 Oasis 实例广播此节点。禁用以停止 UDP 广播。", + settingsLanDesc: "定期向同一局域网中的其他 Oasis 实例广播此节点。", settingsLanEnable: "启用局域网广播", settingsLanTitle: "局域网广播", settingsReplicationTitle: "复制", settingsReplicationDesc: "配置在复制内容时,你的节点从自己的信息流出发跟随的跳数。", settingsReplicationHopsLabel: "跳数", + settingsBlobCacheTitle: "媒体缓存", + settingsBlobCacheDesc: "其他居民的内容会保存在本设备上。请选择它最多可占用的空间。", + settingsBlobCacheUsage: "当前占用", + settingsBlobCacheFiles: "个文件", + settingsBlobCacheLimit: "上限", + settingsBlobCacheUnlimited: "不限", + settingsBlobCacheCleanNow: "立即清理", + settingsBlobCacheCleaned: "已删除 {n} 个文件,释放 {size}。", aiApproveTagsLabel: "标签(逗号分隔)", aiApproveTagsPlaceholder: "例如 oasis、治理、生态", aiApproveRatingLabel: "评分", @@ -3750,31 +4758,25 @@ module.exports = { aiExchangeMarkHelpful: "+1 有帮助", larpCyclesUntilRuling: "下一执政周期", larpVisitTribe: "访问部落", + larpStartJourney: "开始旅程", larpGoverning: "执政:", + larpStartHere: "从这里开始:", larpMyHouse: "我的家族:", larpMembersCount: "成员", - larpMembersTitle: "成员", - larpNoMembers: "暂无成员。", larpRolesLabel: "角色", larpFunctionLabel: "功能", larpMonthLabel: "执政周期", larpLeaveToAcademia: "返回 ACADEMIA", - larpAcademiaJoinTitle: "加入家族", - larpAcademiaJoinHint: "进行心理画像测试以被分配到最适合你的家族,或使用家族成员分享的邀请码。", - larpTakeTestButton: "进行画像测试", + larpAcademiaJoinTitle: "L.A.R.P. 家族", larpInviteRedeem: "加入家族", larpInviteCreate: "生成邀请码", larpInvitePlaceholder: "邀请码", larpInviteBannerTitle: "新邀请码", larpInviteBannerHint: "将此代码分享给 ACADEMIA 中的某人。30 个周期后过期,或使用一次后失效。", - larpTestAssignedHouse: "已分配家族", larpTestRankingTitle: "各家族分数", invitesHousesTitle: "L.A.R.P.", - invitesHouseInviteCodePlaceholder: "家族邀请码", - invitesHouseJoinButton: "加入家族", larpLastAttemptTitle: "你最近的尝试", larpLastAttemptHouse: "家族", - larpLastAttemptResult: "结果", larpLastAttemptWhen: "时间", larpLastAttemptCooldown: "下次尝试还需", larpTestTitle: "入会测试", @@ -3783,44 +4785,21 @@ module.exports = { larpTestCooldownActive: "下次测试可用还需", larpTestCooldownDays: "周期", larpTestResultTitle: "测试结果", - larpTestPassed: "测试通过 — 欢迎!", - larpTestFailed: "测试未通过", larpTestScore: "得分", larpTestNextAttempt: "你可以在 30 个周期后再次尝试测试。", larpGoToHouse: "前往你的家族", larpBackToAcademia: "返回 ACADEMIA", - larpBackToHouses: "◀ 家族", larpBadgeYou: "你", larpBadgeRuling: "执政", modulesLarpLabel: "L.A.R.P.", - modulesLarpDescription: "Oasis 实时角色扮演层(9 个家族)的模块。", - melodyUploadTitlePlaceholder: "Composition title", - melodyUploadDescPlaceholder: "Optional description", - ecoinTaxLabel: "ECOin Tax", - bankTaxesNetworkTitle: "Network Taxes", - bankTaxesUserTitle: "Your taxes", - bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", - bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", - bankTaxesExample: "Example", - bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", - statsEcoTaxTitle: "ECO Tax", - audioTranscodeTitle: "BCS Transcode", - audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", - audioTranscodeStegoTitle: "Embedded in the waveform", - audioBackToAudio: "Back to audio", - audioAuthorLabel: "Author", - mapZoomIn: "Zoom +", - mapZoomOut: "Zoom −", - mapClickToCreate: "Click on the map to select a location", - mapClickToAddMarker: "Click on the map to add a marker", - gameScoreLabel: "GAME SCORES", + modulesLarpDescription: "用于实景角色扮演层的模块。", larpProfileQ1: "面对复杂问题时,你首先做什么?", larpProfileQ1O1: "与他人交谈以达成共识", larpProfileQ1O2: "构建原型进行测试", larpProfileQ1O3: "研究相关文献", larpProfileQ1O4: "颠覆产生它的系统", larpProfileQ2: "什么赋予你日常工作意义?", - larpProfileQ2O1: "保护我所爱的人", + larpProfileQ2O1: "保护我所爱的居民", larpProfileQ2O2: "创造有形之物", larpProfileQ2O3: "疗愈或滋养生命", larpProfileQ2O4: "雕琢文字与想法", @@ -3887,6 +4866,51 @@ module.exports = { uxModeTitle: "UX", uxModeDescription: "选择你想要的 UX 导航模式。", uxModeMenus: "块", + uxModeChats: "对话", + uxModeFeed: "微博客", + feedTrendingTitle: "热门标签", + feedActiveTitle: "活跃居民", + cnBrandSub: "自由 · P2P · 联邦", + cnSyncedPeers: "已同步节点", + cnLicense: "许可证", + cnNotAccessible: "该内容目前无法访问。", + cnUntitled: "无标题", + cnSearchPlaceholder: "搜索…", + cnHubTitle: "Clearnet 中心", + cnHubDescription: "由本 Oasis 网络的居民分享的公开内容。", + cnHubEmpty: "尚未有公开内容分享到 Clearnet。", + cnPublicContent: "内容", + cnTribesContent: "部落", + cnNetworkContent: "网络", + cnCategoryEmpty: "此分类下没有内容。", + cnInhabitantEmpty: "该居民尚未在 Clearnet 发布内容。", + cnPrevPage: "← 上一页", + cnNextPage: "下一页 →", + cnPageOf: "第 {page} 页,共 {pages} 页", + cnMoreInfo: "更多信息 →", + cnOpenCourse: "开放课程", + cnAuction: "拍卖", + cnExchange: "交换", + cnRssDescription: "由本中心的居民发布", + cnKindAudio: "音频", + cnKindBlog: "博客", + cnKindBookmark: "书签", + cnKindDocument: "文档", + cnKindEvent: "活动", + cnKindFeed: "动态", + cnKindImage: "图片", + cnKindJob: "职位", + cnKindMarket: "市场", + cnKindPodcast: "播客", + cnKindProject: "项目", + cnKindCourse: "课程", + cnKindShop: "商店", + cnKindTorrent: "种子", + cnKindFile: "文件", + cnKindVideo: "视频", + cnKindWiki: "维基", + cnKindRoom: "房间", + cnKindMap: "地图", uxModeAINav: "AI", } diff --git a/src/client/gui.js b/src/client/gui.js index dfe02ae..bc4374b 100644 --- a/src/client/gui.js +++ b/src/client/gui.js @@ -10,10 +10,13 @@ const { printMetadata } = require('../server/ssb_metadata'); const updateFlagPath = path.join(__dirname, "../server/.update_required"); let internalSSB = null; -try { - const { server } = require('../server/SSB_server'); - internalSSB = server; -} catch {} +let internalOpen = null; +if (!process.env.OASIS_TEST) { + try { + const SSB_server = require('../server/SSB_server'); + internalOpen = () => SSB_server.open().then((srv) => { internalSSB = srv; return srv; }); + } catch {} +} if (process.env.OASIS_TEST) { ssbConfig.path = fs.mkdtempSync(path.join(os.tmpdir(), "oasis-")); @@ -24,10 +27,33 @@ const socketPath = path.join(ssbConfig.path, "socket"); const publicInteger = ssbConfig.keys.public.replace(".ed25519", ""); const remote = `unix:${socketPath}~noauth:${publicInteger}`; +const windowLogStream = (ssb) => { + if (!ssb || ssb._logStreamWindowed || typeof ssb.createLogStream !== "function") return ssb; + const pull = require("../server/node_modules/pull-stream"); + const defer = require("../server/node_modules/pull-defer"); + const orig = ssb.createLogStream; + ssb.createLogStream = function (opts) { + const o = opts || {}; + const plain = o.limit && !o.reverse && !o.live && !o.raw && o.old === undefined && !o.gt && !o.lt; + if (!plain) return orig.call(ssb, opts); + const src = defer.source(); + pull( + orig.call(ssb, { ...o, reverse: true }), + pull.collect((err, msgs) => { + if (err) return src.abort(err); + src.resolve(pull.values(msgs.reverse())); + }) + ); + return src; + }; + ssb._logStreamWindowed = true; + return ssb; +}; + const connect = (options) => new Promise((resolve, reject) => { ssbClient(process.env.OASIS_TEST ? ssbConfig.keys : null, options) - .then(resolve) + .then((ssb) => resolve(windowLogStream(ssb))) .catch(reject); }); @@ -89,8 +115,15 @@ module.exports = ({ offline, port = 3000, host = 'localhost', isPublic = false } return new Promise((resolve, reject) => { if (internalSSB) { const { printMetadata, colors } = require('../server/ssb_metadata'); - printMetadata('OASIS GUI', colors.yellow, port, host, offline, isPublic); - return resolve(internalSSB); + printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic); + return resolve(windowLogStream(internalSSB)); + } + if (internalOpen) { + return internalOpen().then((srv) => { + const { printMetadata, colors } = require('../server/ssb_metadata'); + printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic); + resolve(windowLogStream(srv)); + }).catch(reject); } if (clientHandle && clientHandle.closed === false) { @@ -105,7 +138,7 @@ module.exports = ({ offline, port = 3000, host = 'localhost', isPublic = false } reject(new Error("Closing Oasis")); } else { const { printMetadata, colors } = require('../server/ssb_metadata'); - printMetadata('OASIS GUI', colors.yellow, port, host, offline, isPublic); + printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic); resolve(ssb); } }).catch(reject); diff --git a/src/client/middleware.js b/src/client/middleware.js index 2801b1c..a804895 100644 --- a/src/client/middleware.js +++ b/src/client/middleware.js @@ -8,20 +8,21 @@ const mount = require(path.join(__dirname, "../server/node_modules/koa-mount")); function obfuscateClearnetHtml(html) { if (typeof html !== 'string' || html.length === 0) return html; const preserve = []; + const tag = require('crypto').randomBytes(12).toString('hex'); const stash = (re) => { html = html.replace(re, (m) => { preserve.push(m); - return `${preserve.length - 1}`; + return `\u0001${tag}:${preserve.length - 1}\u0001`; }); }; stash(//gi); stash(//gi); stash(//gi); html = html.replace(//g, ''); - html = html.replace(/>[\s\n\r\t]+<'); + html = html.replace(/>[ \t]*[\r\n][\s]*<'); html = html.replace(/[ \t]{2,}/g, ' '); html = html.replace(/[\r\n]+/g, ''); - html = html.replace(/(\d+)/g, (_, i) => preserve[Number(i)] || ''); + html = html.replace(new RegExp(`\\u0001${tag}:(\\d+)\\u0001`, 'g'), (_, i) => preserve[Number(i)] || ''); return html; } @@ -42,49 +43,26 @@ const collectLocalIPs = () => { module.exports = ({ host, port, middleware, allowHost }) => { const assets = new Koa() + assets.use(async (ctx, next) => { + ctx.set("Cache-Control", ctx.query && ctx.query.v ? "public, max-age=31536000, immutable" : "public, max-age=3600"); + await next(); + }); assets.use(koaStatic(join(__dirname, "..", "client", "assets"))); const app = new Koa(); const validHosts = []; - const isClearnetPath = (request) => { - const url = String(request.url || ''); - return url === '/c' || url.startsWith('/c/') || url.startsWith('/c?'); - }; + const { isClearnetPath, isTrustedRequest, buildCsp } = require(path.join(__dirname, "..", "backend", "request_guards")); + const isValidRequest = (request) => isTrustedRequest(request, validHosts); - const isValidRequest = (request) => { - if (isClearnetPath(request)) return request.method === 'GET'; - if (validHosts.includes(request.hostname) !== true) { - return false; - } - if (request.method !== "GET") { - if (request.header.referer == null) { - return false; - } - - try { - const refererUrl = new URL(request.header.referer); - if (validHosts.includes(refererUrl.hostname) !== true) { - return false; - } - - if (refererUrl.pathname.startsWith("/blob/")) { - return false; - } - } catch (e) { - return false; - } - } - - return true; - }; + const httpDebug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; app.on("error", (err, ctx) => { if (err && (err.code === 'ECONNRESET' || err.code === 'EPIPE')) { return; } if (err && (err.name === 'BadRequestError' || err.status === 400)) { - console.error(`[400] ${err.message}`); + if (httpDebug) console.error(`[400] ${err.message}`); return null; } console.error(err); @@ -96,17 +74,21 @@ module.exports = ({ host, port, middleware, allowHost }) => { }); app.use(mount("/assets", assets)); + app.use(mount("/c/assets", assets)); - const maptiles = new Koa(); - maptiles.use(koaStatic(join(__dirname, "..", "maps", "tiles"))); - app.use(mount("/maptiles", maptiles)); - - const mapcache = new Koa(); - mapcache.use(koaStatic(join(__dirname, "..", "maps", "cache"))); - app.use(mount("/mapcache", mapcache)); - + const mapTiles = new Koa(); + mapTiles.use(koaStatic(join(__dirname, "..", "maps", "tiles"), { maxage: 30 * 24 * 60 * 60 * 1000, immutable: true })); + app.use(mount("/maptiles", mapTiles)); + app.use(mount("/c/maptiles", mapTiles)); const gamesStatic = new Koa(); + gamesStatic.use(async (ctx, next) => { + ctx.set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' data: blob: mediastream:; connect-src 'none'; form-action 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'self'"); + ctx.set("X-Content-Type-Options", "nosniff"); + ctx.set("X-Frame-Options", "SAMEORIGIN"); + ctx.set("Referrer-Policy", "same-origin"); + await next(); + }); gamesStatic.use(koaStatic(join(__dirname, "..", "games"))); app.use(mount("/game-assets", gamesStatic)); @@ -114,38 +96,11 @@ module.exports = ({ host, port, middleware, allowHost }) => { app.use(koaStatic(path.join(__dirname, 'public'))); app.use(async (ctx, next) => { - - //console.log("Requesting:", ctx.path); // uncomment to check for HTTP requests + + if (httpDebug) console.log(`[http] ${ctx.method} ${ctx.path}`); const isClearnet = isClearnetPath(ctx.request); - const csp = isClearnet - ? [ - "default-src 'self'", - "script-src 'none'", - "style-src 'self' 'unsafe-inline'", - "img-src 'self' data:", - "media-src 'self' blob:", - "connect-src 'self'", - "form-action 'self'", - "object-src 'none'", - "base-uri 'none'", - "frame-ancestors 'none'" - ].join("; ") - : [ - "default-src 'self'", - "script-src 'self' http://localhost:3000/js", - "style-src 'self'", - "img-src 'self'", - "media-src 'self' blob:", - "worker-src 'self' blob:", - "frame-src 'self'", - "form-action 'self'", - "object-src 'none'", - "base-uri 'none'", - "frame-ancestors 'none'" - ].join("; "); - - ctx.set("Content-Security-Policy", csp); + ctx.set("Content-Security-Policy", buildCsp(isClearnet, { frames: ctx.method === "GET" && /^\/games\/[a-z0-9]+\/?$/.test(ctx.path) })); ctx.set("X-Frame-Options", "SAMEORIGIN"); ctx.set("X-Content-Type-Options", "nosniff"); @@ -153,12 +108,10 @@ module.exports = ({ host, port, middleware, allowHost }) => { ctx.set("Referrer-Policy", "same-origin"); ctx.set("Permissions-Policy", "speaker=(self)"); - const validHostsString = validHosts.join(" or "); - ctx.assert( isValidRequest(ctx.request), 400, - `Request must be addressed to ${validHostsString} and non-GET requests must contain non-blob referer.` + "Request must be addressed to this node and non-GET requests must come from its own pages." ); await next(); @@ -171,13 +124,10 @@ module.exports = ({ host, port, middleware, allowHost }) => { } }); - // pdf viewer - const pdfjsPath = path.join(__dirname, '../server/node_modules/pdfjs-dist/build/pdf.min.js'); - app.use(koaStatic(pdfjsPath)); - middleware.forEach((m) => app.use(m)); - const server = app.listen({ host, port }); + const server = require("http").createServer({ maxHeaderSize: 256 * 1024 }, app.callback()).listen({ host, port }); + server.on("listening", () => { const address = server.address(); diff --git a/src/client/public/docs/ecoin.md b/src/client/public/docs/ecoin.md deleted file mode 100644 index 6449f16..0000000 --- a/src/client/public/docs/ecoin.md +++ /dev/null @@ -1,115 +0,0 @@ - -ECOin - Copyright (c) - 2014/2025 - GPLv3 - epsylon@riseup.net (https://ecoin.03c8.net) - -=========================================== -# SOURCES for P2P Crypto-Currency (ECOin) # -=========================================== - -Testing machine is: Debian GNU/Linux 12 (bookworm) (x86_64). - -All of the commands should be executed in a shell. - ------------------------------- - -(0.) Clone the github tree to get the source code: - - + Official: - - git clone http://code.03c8.net/epsylon/ecoin - - + Mirror: - - git clone https://github.com/epsylon/ecoin - ------------------------------- - -=================================================== -# SERVER -ecoind- for P2P Crypto-Currency (ECOin) # -=================================================== - -(0.) Version libraries: - - - Libboost -> source code 1.68 provided at: src/boost_1_68_0 - -(1.) Install dependencies: - - sudo apt-get install build-essential libssl-dev libssl3 libdb5.3-dev libdb5.3++-dev libleveldb-dev miniupnpc libminiupnpc-dev - - + Optionally install qrencode (and set USE_QRCODE=1): - - sudo apt-get install libqrencode-dev - -(2.) Now you should be able to build ecoind: - - cd src/ - make -f makefile.linux USE_UPNP=- USE_IPV6=- - strip ecoind - - An executable named 'ecoind' will be built. - - Now you can launch: ./ecoind to run your ECOin server. - ------------------------------- - -============================================ -# WALLET for P2P Crypto-Currency (ECOin) # -============================================ - -(0.) Version libraries: - - - Libboost -> source code 1.68 provided at: src/boost_1_68_0 - -(1.) First, make sure that the required packages for Qt5 development (an the others required for building the daemon) are installed: - - sudo apt-get install qt5-qmake qtbase5-dev build-essential libssl-dev libssl3 libdb5.3-dev libdb5.3++-dev libleveldb-dev miniupnpc libminiupnpc-dev - - + Optionally install qrencode (and set USE_QRCODE=1): - - sudo apt-get install libqrencode-dev - -(2.) Then execute the following: - - qmake USE_UPNP=- USE_IPV6=- - make - - An executable named 'ecoin-qt' will be built. - - Now you can launch: ./ecoin-qt to run your ECOin wallet/GUI. - ------------------------------- -======================================== -+ CPU MINER for ECOin (Unix/GNU-Linux) # -======================================== - -See doc/MINING.txt for detailed instructions on running /ecoin-miner/ on different platforms. - - + GNU/Linux: - - cd miner/ - sh build.sh - - An executable named 'cpuminer' will be built. - - Now you can launch: ./cpuminer to run your ECOin PoW miner. - -====================================== - -For a list of command-line options: - - ./ecoind --help - -To start the ECOin daemon: - - ./ecoind -daemon - -For ECOin-QT Wallet - - ./ecoin-qt - -For debugging: - - tail -f /home/$USER/.ecoin/debug.log - -====================================== - - diff --git a/src/client/public/js/mobile_menu.js b/src/client/public/js/mobile_menu.js new file mode 100644 index 0000000..df270bc --- /dev/null +++ b/src/client/public/js/mobile_menu.js @@ -0,0 +1,31 @@ +// Hoja del menu (pestaña Menu de la barra de abajo). Abre y cierra con una casilla +// y CSS; esto solo hace que el boton atras de Android cierre la hoja en vez de +// cambiar de pagina. Ver docs/ESTUDIO_NAVEGACION.md. +(function () { + var box = document.getElementById('oasis-menu-toggle'); + if (!box || !window.history || !history.pushState) return; + var sheet = document.querySelector('.menu-sheet'); + var pending = null; + if (history.state && history.state.oasisMenu) box.checked = true; + box.addEventListener('change', function () { + if (box.checked) { history.pushState({ oasisMenu: true }, ''); } + else if (history.state && history.state.oasisMenu) { history.back(); } + }); + window.addEventListener('popstate', function () { + if (box.checked) box.checked = false; + // se navega despues de que la vuelta atras termine (en WebView 69, si no, se + // perdia la entrada nueva) + if (pending) setTimeout(follow, 50); + }); + function follow() { if (pending) { var go = pending; pending = null; location.href = go; } } + // al ir a un modulo desde la hoja, primero se quita la entrada de la hoja: si no, + // al volver atras desde el modulo la portada aparecia con la hoja abierta + if (sheet) sheet.addEventListener('click', function (e) { + var a = e.target.closest ? e.target.closest('a[href]') : null; + if (!a || !(history.state && history.state.oasisMenu)) return; + e.preventDefault(); + pending = a.href; + history.back(); + setTimeout(follow, 1000); + }); +})(); diff --git a/src/client/public/js/oasis-mobile-nav.js b/src/client/public/js/oasis-mobile-nav.js deleted file mode 100644 index 2878122..0000000 --- a/src/client/public/js/oasis-mobile-nav.js +++ /dev/null @@ -1,36 +0,0 @@ -(function () { - function mark() { - var p = (location.pathname || "/").replace(/\/+$/, "") || "/"; - var links = document.querySelectorAll(".oasis-bottombar a.bb-item"); - var best = null, bestLen = -1; - links.forEach(function (a) { - var h = (a.getAttribute("href") || "").replace(/\/+$/, "") || "/"; - if (h === "/") return; - if (p === h || p.indexOf(h + "/") === 0) { - if (h.length > bestLen) { best = a; bestLen = h.length; } - } - }); - if (best) best.classList.add("current"); - } - - function onToggle(e) { - var t = e.target; - if (!t || !t.classList || !t.classList.contains("oasis-nav-toggle")) return; - if (!t.checked) return; - var group = t.closest(".oasis-nav-group"); - var scope = group && group.parentElement; - if (!scope) return; - var others = scope.querySelectorAll( - ":scope > .oasis-nav-group > .oasis-nav-toggle" - ); - others.forEach(function (o) { if (o !== t) o.checked = false; }); - } - - function init() { - mark(); - document.addEventListener("change", onToggle); - } - - if (document.readyState !== "loading") init(); - else document.addEventListener("DOMContentLoaded", init); -})(); diff --git a/src/client/public/js/pdf-viewer.js b/src/client/public/js/pdf-viewer.js new file mode 100644 index 0000000..5dd09a8 --- /dev/null +++ b/src/client/public/js/pdf-viewer.js @@ -0,0 +1,131 @@ +const showPdfFallback = () => { + document.querySelectorAll('.pdf-viewer-container').forEach(container => { + if (container.dataset.pdfReady) return; + container.dataset.pdfReady = '1'; + container.textContent = ''; + const url = container.getAttribute('data-pdf-url'); + if (url) { + const link = document.createElement('a'); + link.href = url; + link.textContent = '📄 PDF'; + container.appendChild(link); + } + }); +}; + +const initPdfViewers = async () => { + let pdfjsLib; + try { + pdfjsLib = await import('/js/pdf.min.mjs?v=102'); + pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.min.mjs?v=102'; + } catch (err) { + showPdfFallback(); + return; + } + + document.querySelectorAll('.pdf-viewer-container').forEach(async container => { + const pdfUrl = container.getAttribute('data-pdf-url'); + if (!pdfUrl || container.dataset.pdfReady) return; + container.dataset.pdfReady = '1'; + + let pdf; + try { + pdf = await pdfjsLib.getDocument(pdfUrl).promise; + } catch (err) { + container.textContent = ''; + const fallback = document.createElement('a'); + fallback.href = pdfUrl; + fallback.textContent = '📄 PDF'; + container.appendChild(fallback); + return; + } + + let currentPage = 1; + let scale = 1.5; + let rotation = 0; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const canvasWrap = document.createElement('div'); + canvasWrap.className = 'pdf-canvas-wrap'; + canvasWrap.appendChild(canvas); + container.innerHTML = ''; + container.appendChild(canvasWrap); + + const controls = document.createElement('div'); + controls.className = 'pdf-controls'; + controls.innerHTML = ` + + + + + + + + + `; + + container.appendChild(controls); + + const renderPage = async (num) => { + const page = await pdf.getPage(num); + const viewport = page.getViewport({ scale, rotation }); + canvas.width = viewport.width; + canvas.height = viewport.height; + ctx.clearRect(0, 0, canvas.width, canvas.height); + await page.render({ canvasContext: ctx, viewport }).promise; + }; + + const goToPage = (delta) => { + const newPage = currentPage + delta; + if (newPage >= 1 && newPage <= pdf.numPages) { + currentPage = newPage; + renderPage(currentPage); + } + }; + + try { + const firstPage = await pdf.getPage(1); + const baseViewport = firstPage.getViewport({ scale: 1, rotation: 0 }); + const avail = canvasWrap.clientWidth || container.clientWidth; + if (avail > 0 && baseViewport.width > 0) { + scale = Math.min(1.5, Math.max(0.5, (avail - 40) / baseViewport.width)); + } + } catch (e) {} + + renderPage(currentPage); + + controls.querySelector('.pdf-prev').onclick = () => goToPage(-1); + controls.querySelector('.pdf-next').onclick = () => goToPage(1); + controls.querySelector('.pdf-zoom-in').onclick = () => { scale += 0.2; renderPage(currentPage); }; + controls.querySelector('.pdf-zoom-out').onclick = () => { scale = Math.max(0.5, scale - 0.2); renderPage(currentPage); }; + controls.querySelector('.pdf-rotate').onclick = () => { rotation = (rotation + 90) % 360; renderPage(currentPage); }; + controls.querySelector('.pdf-download').onclick = () => { + const a = document.createElement('a'); + a.href = pdfUrl; + a.download = 'document.pdf'; + a.click(); + }; + controls.querySelector('.pdf-fullscreen').onclick = async () => { + const maximized = container.classList.toggle('pdf-maximized'); + document.body.classList.toggle('pdf-maximized-body', maximized); + try { + const p1 = await pdf.getPage(currentPage); + const vw1 = p1.getViewport({ scale: 1, rotation }); + const avail = Math.max(260, (maximized ? window.innerWidth : (canvasWrap.clientWidth || container.clientWidth || 360)) - 24); + scale = maximized ? avail / vw1.width : Math.min(1.5, avail / vw1.width); + await renderPage(currentPage); + } catch (_) {} + }; + controls.querySelector('.pdf-metadata').onclick = async () => { + const info = await pdf.getMetadata(); + alert(`Title: ${info.info.Title || 'N/A'}\nAuthor: ${info.info.Author || 'N/A'}\nPDF Producer: ${info.info.Producer || 'N/A'}`); + }; + }); +}; + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPdfViewers); +} else { + initPdfViewers(); +} diff --git a/src/client/public/js/pdf.min.mjs b/src/client/public/js/pdf.min.mjs new file mode 100644 index 0000000..3b3c62f --- /dev/null +++ b/src/client/public/js/pdf.min.mjs @@ -0,0 +1,29 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */ +/** + * pdfjsVersion = 5.3.31 + * pdfjsBuild = 47ad820d9 + */ +const t=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),e=[.001,0,0,.001,0,0],i=1.35,s=1,n=2,a=4,r=16,o=32,l=64,h=128,d=256,c={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3},u={DISABLE:-1,NONE:0,FREETEXT:3,HIGHLIGHT:9,STAMP:13,INK:15,SIGNATURE:101},p={RESIZE:1,CREATE:2,FREETEXT_SIZE:11,FREETEXT_COLOR:12,FREETEXT_OPACITY:13,INK_COLOR:21,INK_THICKNESS:22,INK_OPACITY:23,HIGHLIGHT_COLOR:31,HIGHLIGHT_DEFAULT_COLOR:32,HIGHLIGHT_THICKNESS:33,HIGHLIGHT_FREE:34,HIGHLIGHT_SHOW_ALL:35,DRAW_STEP:41},g={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},f=0,m=1,b=2,A=3,w=3,y=4,v={GRAYSCALE_1BPP:1,RGB_24BPP:2,RGBA_32BPP:3},x={TEXT:1,LINK:2,FREETEXT:3,LINE:4,SQUARE:5,CIRCLE:6,POLYGON:7,POLYLINE:8,HIGHLIGHT:9,UNDERLINE:10,SQUIGGLY:11,STRIKEOUT:12,STAMP:13,CARET:14,INK:15,POPUP:16,FILEATTACHMENT:17,SOUND:18,MOVIE:19,WIDGET:20,SCREEN:21,PRINTERMARK:22,TRAPNET:23,WATERMARK:24,THREED:25,REDACT:26},E=1,_=2,S=3,C=4,T=5,M={ERRORS:0,WARNINGS:1,INFOS:5},D={dependency:1,setLineWidth:2,setLineCap:3,setLineJoin:4,setMiterLimit:5,setDash:6,setRenderingIntent:7,setFlatness:8,setGState:9,save:10,restore:11,transform:12,moveTo:13,lineTo:14,curveTo:15,curveTo2:16,curveTo3:17,closePath:18,rectangle:19,stroke:20,closeStroke:21,fill:22,eoFill:23,fillStroke:24,eoFillStroke:25,closeFillStroke:26,closeEOFillStroke:27,endPath:28,clip:29,eoClip:30,beginText:31,endText:32,setCharSpacing:33,setWordSpacing:34,setHScale:35,setLeading:36,setFont:37,setTextRenderingMode:38,setTextRise:39,moveText:40,setLeadingMoveText:41,setTextMatrix:42,nextLine:43,showText:44,showSpacedText:45,nextLineShowText:46,nextLineSetSpacingShowText:47,setCharWidth:48,setCharWidthAndBounds:49,setStrokeColorSpace:50,setFillColorSpace:51,setStrokeColor:52,setStrokeColorN:53,setFillColor:54,setFillColorN:55,setStrokeGray:56,setFillGray:57,setStrokeRGBColor:58,setFillRGBColor:59,setStrokeCMYKColor:60,setFillCMYKColor:61,shadingFill:62,beginInlineImage:63,beginImageData:64,endInlineImage:65,paintXObject:66,markPoint:67,markPointProps:68,beginMarkedContent:69,beginMarkedContentProps:70,endMarkedContent:71,beginCompat:72,endCompat:73,paintFormXObjectBegin:74,paintFormXObjectEnd:75,beginGroup:76,endGroup:77,beginAnnotation:80,endAnnotation:81,paintImageMaskXObject:83,paintImageMaskXObjectGroup:84,paintImageXObject:85,paintInlineImageXObject:86,paintInlineImageXObjectGroup:87,paintImageXObjectRepeat:88,paintImageMaskXObjectRepeat:89,paintSolidColorImageMask:90,constructPath:91,setStrokeTransparent:92,setFillTransparent:93,rawFillPath:94},P=0,k=1,I=2,R=3,F={NEED_PASSWORD:1,INCORRECT_PASSWORD:2};let L=M.WARNINGS;function setVerbosityLevel(t){Number.isInteger(t)&&(L=t)}function getVerbosityLevel(){return L}function info(t){L>=M.INFOS&&console.log(`Info: ${t}`)}function warn(t){L>=M.WARNINGS&&console.log(`Warning: ${t}`)}function unreachable(t){throw new Error(t)}function assert(t,e){t||unreachable(e)}function createValidAbsoluteUrl(t,e=null,i=null){if(!t)return null;if(i&&"string"==typeof t){if(i.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(i.tryConvertEncoding)try{t=function stringToUTF8String(t){return decodeURIComponent(escape(t))}(t)}catch{}}const s=e?URL.parse(t,e):URL.parse(t);return function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(s)?s:null}function updateUrlHash(t,e,i=!1){const s=URL.parse(t);if(s){s.hash=e;return s.href}return i&&createValidAbsoluteUrl(t,"http://example.com")?t.split("#",1)[0]+""+(e?`#${e}`:""):""}function shadow(t,e,i,s=!1){Object.defineProperty(t,e,{value:i,enumerable:!s,configurable:!0,writable:!1});return i}const O=function BaseExceptionClosure(){function BaseException(t,e){this.message=t;this.name=e}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends O{constructor(t,e){super(t,"PasswordException");this.code=e}}class UnknownErrorException extends O{constructor(t,e){super(t,"UnknownErrorException");this.details=e}}class InvalidPDFException extends O{constructor(t){super(t,"InvalidPDFException")}}class ResponseException extends O{constructor(t,e,i){super(t,"ResponseException");this.status=e;this.missing=i}}class FormatError extends O{constructor(t){super(t,"FormatError")}}class AbortException extends O{constructor(t){super(t,"AbortException")}}function bytesToString(t){"object"==typeof t&&void 0!==t?.length||unreachable("Invalid argument for bytesToString");const e=t.length,i=8192;if(et.toString(16).padStart(2,"0")));class Util{static makeHexColor(t,e,i){return`#${N[t]}${N[e]}${N[i]}`}static scaleMinMax(t,e){let i;if(t[0]){if(t[0]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[0];e[2]*=t[0];if(t[3]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[3];e[3]*=t[3]}else{i=e[0];e[0]=e[1];e[1]=i;i=e[2];e[2]=e[3];e[3]=i;if(t[1]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[1];e[3]*=t[1];if(t[2]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[2];e[2]*=t[2]}e[0]+=t[4];e[1]+=t[5];e[2]+=t[4];e[3]+=t[5]}static transform(t,e){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],t[0]*e[4]+t[2]*e[5]+t[4],t[1]*e[4]+t[3]*e[5]+t[5]]}static applyTransform(t,e,i=0){const s=t[i],n=t[i+1];t[i]=s*e[0]+n*e[2]+e[4];t[i+1]=s*e[1]+n*e[3]+e[5]}static applyTransformToBezier(t,e,i=0){const s=e[0],n=e[1],a=e[2],r=e[3],o=e[4],l=e[5];for(let e=0;e<6;e+=2){const h=t[i+e],d=t[i+e+1];t[i+e]=h*s+d*a+o;t[i+e+1]=h*n+d*r+l}}static applyInverseTransform(t,e){const i=t[0],s=t[1],n=e[0]*e[3]-e[1]*e[2];t[0]=(i*e[3]-s*e[2]+e[2]*e[5]-e[4]*e[3])/n;t[1]=(-i*e[1]+s*e[0]+e[4]*e[1]-e[5]*e[0])/n}static axialAlignedBoundingBox(t,e,i){const s=e[0],n=e[1],a=e[2],r=e[3],o=e[4],l=e[5],h=t[0],d=t[1],c=t[2],u=t[3];let p=s*h+o,g=p,f=s*c+o,m=f,b=r*d+l,A=b,w=r*u+l,y=w;if(0!==n||0!==a){const t=n*h,e=n*c,i=a*d,s=a*u;p+=i;m+=i;f+=s;g+=s;b+=t;y+=t;w+=e;A+=e}i[0]=Math.min(i[0],p,f,g,m);i[1]=Math.min(i[1],b,w,A,y);i[2]=Math.max(i[2],p,f,g,m);i[3]=Math.max(i[3],b,w,A,y)}static inverseTransform(t){const e=t[0]*t[3]-t[1]*t[2];return[t[3]/e,-t[1]/e,-t[2]/e,t[0]/e,(t[2]*t[5]-t[4]*t[3])/e,(t[4]*t[1]-t[5]*t[0])/e]}static singularValueDecompose2dScale(t,e){const i=t[0],s=t[1],n=t[2],a=t[3],r=i**2+s**2,o=i*n+s*a,l=n**2+a**2,h=(r+l)/2,d=Math.sqrt(h**2-(r*l-o**2));e[0]=Math.sqrt(h+d||1);e[1]=Math.sqrt(h-d||1)}static normalizeRect(t){const e=t.slice(0);if(t[0]>t[2]){e[0]=t[2];e[2]=t[0]}if(t[1]>t[3]){e[1]=t[3];e[3]=t[1]}return e}static intersect(t,e){const i=Math.max(Math.min(t[0],t[2]),Math.min(e[0],e[2])),s=Math.min(Math.max(t[0],t[2]),Math.max(e[0],e[2]));if(i>s)return null;const n=Math.max(Math.min(t[1],t[3]),Math.min(e[1],e[3])),a=Math.min(Math.max(t[1],t[3]),Math.max(e[1],e[3]));return n>a?null:[i,n,s,a]}static pointBoundingBox(t,e,i){i[0]=Math.min(i[0],t);i[1]=Math.min(i[1],e);i[2]=Math.max(i[2],t);i[3]=Math.max(i[3],e)}static rectBoundingBox(t,e,i,s,n){n[0]=Math.min(n[0],t,i);n[1]=Math.min(n[1],e,s);n[2]=Math.max(n[2],t,i);n[3]=Math.max(n[3],e,s)}static#t(t,e,i,s,n,a,r,o,l,h){if(l<=0||l>=1)return;const d=1-l,c=l*l,u=c*l,p=d*(d*(d*t+3*l*e)+3*c*i)+u*s,g=d*(d*(d*n+3*l*a)+3*c*r)+u*o;h[0]=Math.min(h[0],p);h[1]=Math.min(h[1],g);h[2]=Math.max(h[2],p);h[3]=Math.max(h[3],g)}static#e(t,e,i,s,n,a,r,o,l,h,d,c){if(Math.abs(l)<1e-12){Math.abs(h)>=1e-12&&this.#t(t,e,i,s,n,a,r,o,-d/h,c);return}const u=h**2-4*d*l;if(u<0)return;const p=Math.sqrt(u),g=2*l;this.#t(t,e,i,s,n,a,r,o,(-h+p)/g,c);this.#t(t,e,i,s,n,a,r,o,(-h-p)/g,c)}static bezierBoundingBox(t,e,i,s,n,a,r,o,l){l[0]=Math.min(l[0],t,r);l[1]=Math.min(l[1],e,o);l[2]=Math.max(l[2],t,r);l[3]=Math.max(l[3],e,o);this.#e(t,i,n,r,e,s,a,o,3*(3*(i-n)-t+r),6*(t-2*i+n),3*(i-t),l);this.#e(t,i,n,r,e,s,a,o,3*(3*(s-a)-e+o),6*(e-2*s+a),3*(s-e),l)}}let B=null,H=null;function normalizeUnicode(t){if(!B){B=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;H=new Map([["ſt","ſt"]])}return t.replaceAll(B,((t,e,i)=>e?e.normalize("NFKC"):H.get(i)))}function getUuid(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);crypto.getRandomValues(t);return bytesToString(t)}const U="pdfjs_internal_id_";function MathClamp(t,e,i){return Math.min(Math.max(t,e),i)}function toBase64Util(t){return Uint8Array.prototype.toBase64?t.toBase64():btoa(bytesToString(t))}"function"!=typeof Promise.try&&(Promise.try=function(t,...e){return new Promise((i=>{i(t(...e))}))});"function"!=typeof Math.sumPrecise&&(Math.sumPrecise=function(t){return t.reduce(((t,e)=>t+e),0)});const z="http://www.w3.org/2000/svg";class PixelsPerInch{static CSS=96;static PDF=72;static PDF_TO_CSS_UNITS=this.CSS/this.PDF}async function fetchData(t,e="text"){if(isValidFetchUrl(t,document.baseURI)){const i=await fetch(t);if(!i.ok)throw new Error(i.statusText);switch(e){case"arraybuffer":return i.arrayBuffer();case"blob":return i.blob();case"json":return i.json()}return i.text()}return new Promise(((i,s)=>{const n=new XMLHttpRequest;n.open("GET",t,!0);n.responseType=e;n.onreadystatechange=()=>{if(n.readyState===XMLHttpRequest.DONE)if(200!==n.status&&0!==n.status)s(new Error(n.statusText));else{switch(e){case"arraybuffer":case"blob":case"json":i(n.response);return}i(n.responseText)}};n.send(null)}))}class PageViewport{constructor({viewBox:t,userUnit:e,scale:i,rotation:s,offsetX:n=0,offsetY:a=0,dontFlip:r=!1}){this.viewBox=t;this.userUnit=e;this.scale=i;this.rotation=s;this.offsetX=n;this.offsetY=a;i*=e;const o=(t[2]+t[0])/2,l=(t[3]+t[1])/2;let h,d,c,u,p,g,f,m;(s%=360)<0&&(s+=360);switch(s){case 180:h=-1;d=0;c=0;u=1;break;case 90:h=0;d=1;c=1;u=0;break;case 270:h=0;d=-1;c=-1;u=0;break;case 0:h=1;d=0;c=0;u=-1;break;default:throw new Error("PageViewport: Invalid rotation, must be a multiple of 90 degrees.")}if(r){c=-c;u=-u}if(0===h){p=Math.abs(l-t[1])*i+n;g=Math.abs(o-t[0])*i+a;f=(t[3]-t[1])*i;m=(t[2]-t[0])*i}else{p=Math.abs(o-t[0])*i+n;g=Math.abs(l-t[1])*i+a;f=(t[2]-t[0])*i;m=(t[3]-t[1])*i}this.transform=[h*i,d*i,c*i,u*i,p-h*i*o-c*i*l,g-d*i*o-u*i*l];this.width=f;this.height=m}get rawDims(){const t=this.viewBox;return shadow(this,"rawDims",{pageWidth:t[2]-t[0],pageHeight:t[3]-t[1],pageX:t[0],pageY:t[1]})}clone({scale:t=this.scale,rotation:e=this.rotation,offsetX:i=this.offsetX,offsetY:s=this.offsetY,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.viewBox.slice(),userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}convertToViewportPoint(t,e){const i=[t,e];Util.applyTransform(i,this.transform);return i}convertToViewportRectangle(t){const e=[t[0],t[1]];Util.applyTransform(e,this.transform);const i=[t[2],t[3]];Util.applyTransform(i,this.transform);return[e[0],e[1],i[0],i[1]]}convertToPdfPoint(t,e){const i=[t,e];Util.applyInverseTransform(i,this.transform);return i}}class RenderingCancelledException extends O{constructor(t,e=0){super(t,"RenderingCancelledException");this.extraDelay=e}}function isDataScheme(t){const e=t.length;let i=0;for(;i=1&&s<=12?s-1:0;let n=parseInt(e[3],10);n=n>=1&&n<=31?n:1;let a=parseInt(e[4],10);a=a>=0&&a<=23?a:0;let r=parseInt(e[5],10);r=r>=0&&r<=59?r:0;let o=parseInt(e[6],10);o=o>=0&&o<=59?o:0;const l=e[7]||"Z";let h=parseInt(e[8],10);h=h>=0&&h<=23?h:0;let d=parseInt(e[9],10)||0;d=d>=0&&d<=59?d:0;if("-"===l){a+=h;r+=d}else if("+"===l){a-=h;r-=d}return new Date(Date.UTC(i,s,n,a,r,o))}}function getXfaPageViewport(t,{scale:e=1,rotation:i=0}){const{width:s,height:n}=t.attributes.style,a=[0,0,parseInt(s),parseInt(n)];return new PageViewport({viewBox:a,userUnit:1,scale:e,rotation:i})}function getRGB(t){if(t.startsWith("#")){const e=parseInt(t.slice(1),16);return[(16711680&e)>>16,(65280&e)>>8,255&e]}if(t.startsWith("rgb("))return t.slice(4,-1).split(",").map((t=>parseInt(t)));if(t.startsWith("rgba("))return t.slice(5,-1).split(",").map((t=>parseInt(t))).slice(0,3);warn(`Not a valid color format: "${t}"`);return[0,0,0]}function getCurrentTransform(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform();return[e,i,s,n,a,r]}function getCurrentTransformInverse(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform().invertSelf();return[e,i,s,n,a,r]}function setLayerDimensions(t,e,i=!1,s=!0){if(e instanceof PageViewport){const{pageWidth:s,pageHeight:n}=e.rawDims,{style:a}=t,r=util_FeatureTest.isCSSRoundSupported,o=`var(--total-scale-factor) * ${s}px`,l=`var(--total-scale-factor) * ${n}px`,h=r?`round(down, ${o}, var(--scale-round-x))`:`calc(${o})`,d=r?`round(down, ${l}, var(--scale-round-y))`:`calc(${l})`;if(i&&e.rotation%180!=0){a.width=d;a.height=h}else{a.width=h;a.height=d}}s&&t.setAttribute("data-main-rotation",e.rotation)}class OutputScale{constructor(){const{pixelRatio:t}=OutputScale;this.sx=t;this.sy=t}get scaled(){return 1!==this.sx||1!==this.sy}get symmetric(){return this.sx===this.sy}limitCanvas(t,e,i,s,n=-1){let a=1/0,r=1/0,o=1/0;(i=OutputScale.capPixels(i,n))>0&&(a=Math.sqrt(i/(t*e)));if(-1!==s){r=s/t;o=s/e}const l=Math.min(a,r,o);if(this.sx>l||this.sy>l){this.sx=l;this.sy=l;return!0}return!1}static get pixelRatio(){return globalThis.devicePixelRatio||1}static capPixels(t,e){if(e>=0){const i=Math.ceil(window.screen.availWidth*window.screen.availHeight*this.pixelRatio**2*(1+e/100));return t>0?Math.min(t,i):i}return t}}const G=["image/apng","image/avif","image/bmp","image/gif","image/jpeg","image/png","image/svg+xml","image/webp","image/x-icon"];class EditorToolbar{#s=null;#n=null;#a;#r=null;#o=null;#l=null;static#h=null;constructor(t){this.#a=t;EditorToolbar.#h||=Object.freeze({freetext:"pdfjs-editor-remove-freetext-button",highlight:"pdfjs-editor-remove-highlight-button",ink:"pdfjs-editor-remove-ink-button",stamp:"pdfjs-editor-remove-stamp-button",signature:"pdfjs-editor-remove-signature-button"})}render(){const t=this.#s=document.createElement("div");t.classList.add("editToolbar","hidden");t.setAttribute("role","toolbar");const e=this.#a._uiManager._signal;t.addEventListener("contextmenu",noContextMenu,{signal:e});t.addEventListener("pointerdown",EditorToolbar.#d,{signal:e});const i=this.#r=document.createElement("div");i.className="buttons";t.append(i);const s=this.#a.toolbarPosition;if(s){const{style:e}=t,i="ltr"===this.#a._uiManager.direction?1-s[0]:s[0];e.insetInlineEnd=100*i+"%";e.top=`calc(${100*s[1]}% + var(--editor-toolbar-vert-offset))`}this.#c();return t}get div(){return this.#s}static#d(t){t.stopPropagation()}#u(t){this.#a._focusEventsAllowed=!1;stopEvent(t)}#p(t){this.#a._focusEventsAllowed=!0;stopEvent(t)}#g(t){const e=this.#a._uiManager._signal;t.addEventListener("focusin",this.#u.bind(this),{capture:!0,signal:e});t.addEventListener("focusout",this.#p.bind(this),{capture:!0,signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e})}hide(){this.#s.classList.add("hidden");this.#n?.hideDropdown()}show(){this.#s.classList.remove("hidden");this.#o?.shown()}#c(){const{editorType:t,_uiManager:e}=this.#a,i=document.createElement("button");i.className="delete";i.tabIndex=0;i.setAttribute("data-l10n-id",EditorToolbar.#h[t]);this.#g(i);i.addEventListener("click",(t=>{e.delete()}),{signal:e._signal});this.#r.append(i)}get#f(){const t=document.createElement("div");t.className="divider";return t}async addAltText(t){const e=await t.render();this.#g(e);this.#r.prepend(e,this.#f);this.#o=t}addColorPicker(t){this.#n=t;const e=t.renderButton();this.#g(e);this.#r.prepend(e,this.#f)}async addEditSignatureButton(t){const e=this.#l=await t.renderEditButton(this.#a);this.#g(e);this.#r.prepend(e,this.#f)}updateEditSignatureButton(t){this.#l&&(this.#l.title=t)}remove(){this.#s.remove();this.#n?.destroy();this.#n=null}}class HighlightToolbar{#r=null;#s=null;#m;constructor(t){this.#m=t}#b(){const t=this.#s=document.createElement("div");t.className="editToolbar";t.setAttribute("role","toolbar");t.addEventListener("contextmenu",noContextMenu,{signal:this.#m._signal});const e=this.#r=document.createElement("div");e.className="buttons";t.append(e);this.#A();return t}#w(t,e){let i=0,s=0;for(const n of t){const t=n.y+n.height;if(ti){s=a;i=t}else e?a>s&&(s=a):a{this.#m.highlightSelection("floating_button")}),{signal:i});this.#r.append(t)}}function bindEvents(t,e,i){for(const s of i)e.addEventListener(s,t[s].bind(t))}class IdManager{#y=0;get id(){return"pdfjs_internal_editor_"+this.#y++}}class ImageManager{#v=getUuid();#y=0;#x=null;static get _isSVGFittingCanvas(){const t=new OffscreenCanvas(1,3).getContext("2d",{willReadFrequently:!0}),e=new Image;e.src='data:image/svg+xml;charset=UTF-8,';return shadow(this,"_isSVGFittingCanvas",e.decode().then((()=>{t.drawImage(e,0,0,1,1,0,0,1,3);return 0===new Uint32Array(t.getImageData(0,0,1,1).data.buffer)[0]})))}async#E(t,e){this.#x||=new Map;let i=this.#x.get(t);if(null===i)return null;if(i?.bitmap){i.refCounter+=1;return i}try{i||={bitmap:null,id:`image_${this.#v}_${this.#y++}`,refCounter:0,isSvg:!1};let t;if("string"==typeof e){i.url=e;t=await fetchData(e,"blob")}else e instanceof File?t=i.file=e:e instanceof Blob&&(t=e);if("image/svg+xml"===t.type){const e=ImageManager._isSVGFittingCanvas,s=new FileReader,n=new Image,a=new Promise(((t,a)=>{n.onload=()=>{i.bitmap=n;i.isSvg=!0;t()};s.onload=async()=>{const t=i.svgUrl=s.result;n.src=await e?`${t}#svgView(preserveAspectRatio(none))`:t};n.onerror=s.onerror=a}));s.readAsDataURL(t);await a}else i.bitmap=await createImageBitmap(t);i.refCounter=1}catch(t){warn(t);i=null}this.#x.set(t,i);i&&this.#x.set(i.id,i);return i}async getFromFile(t){const{lastModified:e,name:i,size:s,type:n}=t;return this.#E(`${e}_${i}_${s}_${n}`,t)}async getFromUrl(t){return this.#E(t,t)}async getFromBlob(t,e){const i=await e;return this.#E(t,i)}async getFromId(t){this.#x||=new Map;const e=this.#x.get(t);if(!e)return null;if(e.bitmap){e.refCounter+=1;return e}if(e.file)return this.getFromFile(e.file);if(e.blobPromise){const{blobPromise:t}=e;delete e.blobPromise;return this.getFromBlob(e.id,t)}return this.getFromUrl(e.url)}getFromCanvas(t,e){this.#x||=new Map;let i=this.#x.get(t);if(i?.bitmap){i.refCounter+=1;return i}const s=new OffscreenCanvas(e.width,e.height);s.getContext("2d").drawImage(e,0,0);i={bitmap:s.transferToImageBitmap(),id:`image_${this.#v}_${this.#y++}`,refCounter:1,isSvg:!1};this.#x.set(t,i);this.#x.set(i.id,i);return i}getSvgUrl(t){const e=this.#x.get(t);return e?.isSvg?e.svgUrl:null}deleteId(t){this.#x||=new Map;const e=this.#x.get(t);if(!e)return;e.refCounter-=1;if(0!==e.refCounter)return;const{bitmap:i}=e;if(!e.url&&!e.file){const t=new OffscreenCanvas(i.width,i.height);t.getContext("bitmaprenderer").transferFromImageBitmap(i);e.blobPromise=t.convertToBlob()}i.close?.();e.bitmap=null}isValidId(t){return t.startsWith(`image_${this.#v}_`)}}class CommandManager{#_=[];#S=!1;#C;#T=-1;constructor(t=128){this.#C=t}add({cmd:t,undo:e,post:i,mustExec:s,type:n=NaN,overwriteIfSameType:a=!1,keepUndo:r=!1}){s&&t();if(this.#S)return;const o={cmd:t,undo:e,post:i,type:n};if(-1===this.#T){this.#_.length>0&&(this.#_.length=0);this.#T=0;this.#_.push(o);return}if(a&&this.#_[this.#T].type===n){r&&(o.undo=this.#_[this.#T].undo);this.#_[this.#T]=o;return}const l=this.#T+1;if(l===this.#C)this.#_.splice(0,1);else{this.#T=l;l=0;e--)if(this.#_[e].type!==t){this.#_.splice(e+1,this.#T-e);this.#T=e;return}this.#_.length=0;this.#T=-1}}destroy(){this.#_=null}}class KeyboardManager{constructor(t){this.buffer=[];this.callbacks=new Map;this.allKeys=new Set;const{isMac:e}=util_FeatureTest.platform;for(const[i,s,n={}]of t)for(const t of i){const i=t.startsWith("mac+");if(e&&i){this.callbacks.set(t.slice(4),{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}else if(!e&&!i){this.callbacks.set(t,{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}}}#M(t){t.altKey&&this.buffer.push("alt");t.ctrlKey&&this.buffer.push("ctrl");t.metaKey&&this.buffer.push("meta");t.shiftKey&&this.buffer.push("shift");this.buffer.push(t.key);const e=this.buffer.join("+");this.buffer.length=0;return e}exec(t,e){if(!this.allKeys.has(e.key))return;const i=this.callbacks.get(this.#M(e));if(!i)return;const{callback:s,options:{bubbles:n=!1,args:a=[],checker:r=null}}=i;if(!r||r(t,e)){s.bind(t,...a,e)();n||stopEvent(e)}}}class ColorManager{static _colorsMapping=new Map([["CanvasText",[0,0,0]],["Canvas",[255,255,255]]]);get _colors(){const t=new Map([["CanvasText",null],["Canvas",null]]);!function getColorValues(t){const e=document.createElement("span");e.style.visibility="hidden";e.style.colorScheme="only light";document.body.append(e);for(const i of t.keys()){e.style.color=i;const s=window.getComputedStyle(e).color;t.set(i,getRGB(s))}e.remove()}(t);return shadow(this,"_colors",t)}convert(t){const e=getRGB(t);if(!window.matchMedia("(forced-colors: active)").matches)return e;for(const[t,i]of this._colors)if(i.every(((t,i)=>t===e[i])))return ColorManager._colorsMapping.get(t);return e}getHexCode(t){const e=this._colors.get(t);return e?Util.makeHexColor(...e):t}}class AnnotationEditorUIManager{#D=new AbortController;#P=null;#k=new Map;#I=new Map;#R=null;#F=null;#L=null;#O=new CommandManager;#N=null;#B=null;#H=0;#U=new Set;#z=null;#G=null;#$=new Set;_editorUndoBar=null;#W=!1;#V=!1;#j=!1;#q=null;#X=null;#K=null;#Y=null;#Q=!1;#J=null;#Z=new IdManager;#tt=!1;#et=!1;#it=null;#st=null;#nt=null;#at=null;#rt=null;#ot=u.NONE;#lt=new Set;#ht=null;#dt=null;#ct=null;#ut=null;#pt={isEditing:!1,isEmpty:!0,hasSomethingToUndo:!1,hasSomethingToRedo:!1,hasSelectedEditor:!1,hasSelectedText:!1};#gt=[0,0];#ft=null;#mt=null;#bt=null;#At=null;static TRANSLATE_SMALL=1;static TRANSLATE_BIG=10;static get _keyboardManager(){const t=AnnotationEditorUIManager.prototype,arrowChecker=t=>t.#mt.contains(document.activeElement)&&"BUTTON"!==document.activeElement.tagName&&t.hasSomethingToControl(),textInputChecker=(t,{target:e})=>{if(e instanceof HTMLInputElement){const{type:t}=e;return"text"!==t&&"number"!==t}return!0},e=this.TRANSLATE_SMALL,i=this.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+a","mac+meta+a"],t.selectAll,{checker:textInputChecker}],[["ctrl+z","mac+meta+z"],t.undo,{checker:textInputChecker}],[["ctrl+y","ctrl+shift+z","mac+meta+shift+z","ctrl+shift+Z","mac+meta+shift+Z"],t.redo,{checker:textInputChecker}],[["Backspace","alt+Backspace","ctrl+Backspace","shift+Backspace","mac+Backspace","mac+alt+Backspace","mac+ctrl+Backspace","Delete","ctrl+Delete","shift+Delete","mac+Delete"],t.delete,{checker:textInputChecker}],[["Enter","mac+Enter"],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#mt.contains(e)&&!t.isEnterHandled}],[[" ","mac+ "],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#mt.contains(document.activeElement)}],[["Escape","mac+Escape"],t.unselectAll],[["ArrowLeft","mac+ArrowLeft"],t.translateSelectedEditors,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t.translateSelectedEditors,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t.translateSelectedEditors,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t.translateSelectedEditors,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t.translateSelectedEditors,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t.translateSelectedEditors,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t.translateSelectedEditors,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t.translateSelectedEditors,{args:[0,i],checker:arrowChecker}]]))}constructor(t,e,i,s,n,a,r,o,l,h,d,c,u,p){const g=this._signal=this.#D.signal;this.#mt=t;this.#bt=e;this.#R=i;this.#dt=s;this._eventBus=n;n._on("editingaction",this.onEditingAction.bind(this),{signal:g});n._on("pagechanging",this.onPageChanging.bind(this),{signal:g});n._on("scalechanging",this.onScaleChanging.bind(this),{signal:g});n._on("rotationchanging",this.onRotationChanging.bind(this),{signal:g});n._on("setpreference",this.onSetPreference.bind(this),{signal:g});n._on("switchannotationeditorparams",(t=>this.updateParams(t.type,t.value)),{signal:g});this.#wt();this.#yt();this.#vt();this.#F=a.annotationStorage;this.#q=a.filterFactory;this.#ct=r;this.#Y=o||null;this.#W=l;this.#V=h;this.#j=d;this.#rt=c||null;this.viewParameters={realScale:PixelsPerInch.PDF_TO_CSS_UNITS,rotation:0};this.isShiftKeyDown=!1;this._editorUndoBar=u||null;this._supportsPinchToZoom=!1!==p}destroy(){this.#At?.resolve();this.#At=null;this.#D?.abort();this.#D=null;this._signal=null;for(const t of this.#I.values())t.destroy();this.#I.clear();this.#k.clear();this.#$.clear();this.#at?.clear();this.#P=null;this.#lt.clear();this.#O.destroy();this.#R?.destroy();this.#dt?.destroy();this.#J?.hide();this.#J=null;this.#nt?.destroy();this.#nt=null;if(this.#X){clearTimeout(this.#X);this.#X=null}if(this.#ft){clearTimeout(this.#ft);this.#ft=null}this._editorUndoBar?.destroy()}combinedSignal(t){return AbortSignal.any([this._signal,t.signal])}get mlManager(){return this.#rt}get useNewAltTextFlow(){return this.#V}get useNewAltTextWhenAddingImage(){return this.#j}get hcmFilter(){return shadow(this,"hcmFilter",this.#ct?this.#q.addHCMFilter(this.#ct.foreground,this.#ct.background):"none")}get direction(){return shadow(this,"direction",getComputedStyle(this.#mt).direction)}get highlightColors(){return shadow(this,"highlightColors",this.#Y?new Map(this.#Y.split(",").map((t=>t.split("=").map((t=>t.trim()))))):null)}get highlightColorNames(){return shadow(this,"highlightColorNames",this.highlightColors?new Map(Array.from(this.highlightColors,(t=>t.reverse()))):null)}setCurrentDrawingSession(t){if(t){this.unselectAll();this.disableUserSelect(!0)}else this.disableUserSelect(!1);this.#B=t}setMainHighlightColorPicker(t){this.#nt=t}editAltText(t,e=!1){this.#R?.editAltText(this,t,e)}getSignature(t){this.#dt?.getSignature({uiManager:this,editor:t})}get signatureManager(){return this.#dt}switchToMode(t,e){this._eventBus.on("annotationeditormodechanged",e,{once:!0,signal:this._signal});this._eventBus.dispatch("showannotationeditorui",{source:this,mode:t})}setPreference(t,e){this._eventBus.dispatch("setpreference",{source:this,name:t,value:e})}onSetPreference({name:t,value:e}){if("enableNewAltTextWhenAddingImage"===t)this.#j=e}onPageChanging({pageNumber:t}){this.#H=t-1}focusMainContainer(){this.#mt.focus()}findParent(t,e){for(const i of this.#I.values()){const{x:s,y:n,width:a,height:r}=i.div.getBoundingClientRect();if(t>=s&&t<=s+a&&e>=n&&e<=n+r)return i}return null}disableUserSelect(t=!1){this.#bt.classList.toggle("noUserSelect",t)}addShouldRescale(t){this.#$.add(t)}removeShouldRescale(t){this.#$.delete(t)}onScaleChanging({scale:t}){this.commitOrRemove();this.viewParameters.realScale=t*PixelsPerInch.PDF_TO_CSS_UNITS;for(const t of this.#$)t.onScaleChanging();this.#B?.onScaleChanging()}onRotationChanging({pagesRotation:t}){this.commitOrRemove();this.viewParameters.rotation=t}#xt({anchorNode:t}){return t.nodeType===Node.TEXT_NODE?t.parentElement:t}#Et(t){const{currentLayer:e}=this;if(e.hasTextLayer(t))return e;for(const e of this.#I.values())if(e.hasTextLayer(t))return e;return null}highlightSelection(t=""){const e=document.getSelection();if(!e||e.isCollapsed)return;const{anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a}=e,r=e.toString(),o=this.#xt(e).closest(".textLayer"),l=this.getSelectionBoxes(o);if(!l)return;e.empty();const h=this.#Et(o),d=this.#ot===u.NONE,callback=()=>{h?.createAndAddNewEditor({x:0,y:0},!1,{methodOfCreation:t,boxes:l,anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a,text:r});d&&this.showAllEditors("highlight",!0,!0)};d?this.switchToMode(u.HIGHLIGHT,callback):callback()}#_t(){const t=document.getSelection();if(!t||t.isCollapsed)return;const e=this.#xt(t).closest(".textLayer"),i=this.getSelectionBoxes(e);if(i){this.#J||=new HighlightToolbar(this);this.#J.show(e,i,"ltr"===this.direction)}}addToAnnotationStorage(t){t.isEmpty()||!this.#F||this.#F.has(t.id)||this.#F.setValue(t.id,t)}#St(){const t=document.getSelection();if(!t||t.isCollapsed){if(this.#ht){this.#J?.hide();this.#ht=null;this.#Ct({hasSelectedText:!1})}return}const{anchorNode:e}=t;if(e===this.#ht)return;const i=this.#xt(t).closest(".textLayer");if(i){this.#J?.hide();this.#ht=e;this.#Ct({hasSelectedText:!0});if(this.#ot===u.HIGHLIGHT||this.#ot===u.NONE){this.#ot===u.HIGHLIGHT&&this.showAllEditors("highlight",!0,!0);this.#Q=this.isShiftKeyDown;if(!this.isShiftKeyDown){const t=this.#ot===u.HIGHLIGHT?this.#Et(i):null;t?.toggleDrawing();const e=new AbortController,s=this.combinedSignal(e),pointerup=i=>{if("pointerup"!==i.type||0===i.button){e.abort();t?.toggleDrawing(!0);"pointerup"===i.type&&this.#Tt("main_toolbar")}};window.addEventListener("pointerup",pointerup,{signal:s});window.addEventListener("blur",pointerup,{signal:s})}}}else if(this.#ht){this.#J?.hide();this.#ht=null;this.#Ct({hasSelectedText:!1})}}#Tt(t=""){this.#ot===u.HIGHLIGHT?this.highlightSelection(t):this.#W&&this.#_t()}#wt(){document.addEventListener("selectionchange",this.#St.bind(this),{signal:this._signal})}#Mt(){if(this.#K)return;this.#K=new AbortController;const t=this.combinedSignal(this.#K);window.addEventListener("focus",this.focus.bind(this),{signal:t});window.addEventListener("blur",this.blur.bind(this),{signal:t})}#Dt(){this.#K?.abort();this.#K=null}blur(){this.isShiftKeyDown=!1;if(this.#Q){this.#Q=!1;this.#Tt("main_toolbar")}if(!this.hasSelection)return;const{activeElement:t}=document;for(const e of this.#lt)if(e.div.contains(t)){this.#st=[e,t];e._focusEventsAllowed=!1;break}}focus(){if(!this.#st)return;const[t,e]=this.#st;this.#st=null;e.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this._signal});e.focus()}#vt(){if(this.#it)return;this.#it=new AbortController;const t=this.combinedSignal(this.#it);window.addEventListener("keydown",this.keydown.bind(this),{signal:t});window.addEventListener("keyup",this.keyup.bind(this),{signal:t})}#Pt(){this.#it?.abort();this.#it=null}#kt(){if(this.#N)return;this.#N=new AbortController;const t=this.combinedSignal(this.#N);document.addEventListener("copy",this.copy.bind(this),{signal:t});document.addEventListener("cut",this.cut.bind(this),{signal:t});document.addEventListener("paste",this.paste.bind(this),{signal:t})}#It(){this.#N?.abort();this.#N=null}#yt(){const t=this._signal;document.addEventListener("dragover",this.dragOver.bind(this),{signal:t});document.addEventListener("drop",this.drop.bind(this),{signal:t})}addEditListeners(){this.#vt();this.#kt()}removeEditListeners(){this.#Pt();this.#It()}dragOver(t){for(const{type:e}of t.dataTransfer.items)for(const i of this.#G)if(i.isHandlingMimeForPasting(e)){t.dataTransfer.dropEffect="copy";t.preventDefault();return}}drop(t){for(const e of t.dataTransfer.items)for(const i of this.#G)if(i.isHandlingMimeForPasting(e.type)){i.paste(e,this.currentLayer);t.preventDefault();return}}copy(t){t.preventDefault();this.#P?.commitOrRemove();if(!this.hasSelection)return;const e=[];for(const t of this.#lt){const i=t.serialize(!0);i&&e.push(i)}0!==e.length&&t.clipboardData.setData("application/pdfjs",JSON.stringify(e))}cut(t){this.copy(t);this.delete()}async paste(t){t.preventDefault();const{clipboardData:e}=t;for(const t of e.items)for(const e of this.#G)if(e.isHandlingMimeForPasting(t.type)){e.paste(t,this.currentLayer);return}let i=e.getData("application/pdfjs");if(!i)return;try{i=JSON.parse(i)}catch(t){warn(`paste: "${t.message}".`);return}if(!Array.isArray(i))return;this.unselectAll();const s=this.currentLayer;try{const t=[];for(const e of i){const i=await s.deserialize(e);if(!i)return;t.push(i)}const cmd=()=>{for(const e of t)this.#Rt(e);this.#Ft(t)},undo=()=>{for(const e of t)e.remove()};this.addCommands({cmd,undo,mustExec:!0})}catch(t){warn(`paste: "${t.message}".`)}}keydown(t){this.isShiftKeyDown||"Shift"!==t.key||(this.isShiftKeyDown=!0);this.#ot===u.NONE||this.isEditorHandlingKeyboard||AnnotationEditorUIManager._keyboardManager.exec(this,t)}keyup(t){if(this.isShiftKeyDown&&"Shift"===t.key){this.isShiftKeyDown=!1;if(this.#Q){this.#Q=!1;this.#Tt("main_toolbar")}}}onEditingAction({name:t}){switch(t){case"undo":case"redo":case"delete":case"selectAll":this[t]();break;case"highlightSelection":this.highlightSelection("context_menu")}}#Ct(t){if(Object.entries(t).some((([t,e])=>this.#pt[t]!==e))){this._eventBus.dispatch("annotationeditorstateschanged",{source:this,details:Object.assign(this.#pt,t)});this.#ot===u.HIGHLIGHT&&!1===t.hasSelectedEditor&&this.#Lt([[p.HIGHLIGHT_FREE,!0]])}}#Lt(t){this._eventBus.dispatch("annotationeditorparamschanged",{source:this,details:t})}setEditingState(t){if(t){this.#Mt();this.#kt();this.#Ct({isEditing:this.#ot!==u.NONE,isEmpty:this.#Ot(),hasSomethingToUndo:this.#O.hasSomethingToUndo(),hasSomethingToRedo:this.#O.hasSomethingToRedo(),hasSelectedEditor:!1})}else{this.#Dt();this.#It();this.#Ct({isEditing:!1});this.disableUserSelect(!1)}}registerEditorTypes(t){if(!this.#G){this.#G=t;for(const t of this.#G)this.#Lt(t.defaultPropertiesToUpdate)}}getId(){return this.#Z.id}get currentLayer(){return this.#I.get(this.#H)}getLayer(t){return this.#I.get(t)}get currentPageIndex(){return this.#H}addLayer(t){this.#I.set(t.pageIndex,t);this.#tt?t.enable():t.disable()}removeLayer(t){this.#I.delete(t.pageIndex)}async updateMode(t,e=null,i=!1){if(this.#ot!==t){if(this.#At){await this.#At.promise;if(!this.#At)return}this.#At=Promise.withResolvers();this.#B?.commitOrRemove();this.#ot=t;if(t!==u.NONE){t===u.SIGNATURE&&await(this.#dt?.loadSignatures());this.setEditingState(!0);await this.#Nt();this.unselectAll();for(const e of this.#I.values())e.updateMode(t);if(e){for(const t of this.#k.values())if(t.annotationElementId===e||t.id===e){this.setSelected(t);t.enterInEditMode()}else t.unselect();this.#At.resolve()}else{i&&this.addNewEditorFromKeyboard();this.#At.resolve()}}else{this.setEditingState(!1);this.#Bt();this._editorUndoBar?.hide();this.#At.resolve()}}}addNewEditorFromKeyboard(){this.currentLayer.canCreateNewEmptyEditor()&&this.currentLayer.addNewEditor()}updateToolbar(t){t.mode!==this.#ot&&this._eventBus.dispatch("switchannotationeditormode",{source:this,...t})}updateParams(t,e){if(this.#G){switch(t){case p.CREATE:this.currentLayer.addNewEditor(e);return;case p.HIGHLIGHT_DEFAULT_COLOR:this.#nt?.updateColor(e);break;case p.HIGHLIGHT_SHOW_ALL:this._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:{type:"highlight",action:"toggle_visibility"}}});(this.#ut||=new Map).set(t,e);this.showAllEditors("highlight",e)}for(const i of this.#lt)i.updateParams(t,e);for(const i of this.#G)i.updateDefaultParams(t,e)}}showAllEditors(t,e,i=!1){for(const i of this.#k.values())i.editorType===t&&i.show(e);(this.#ut?.get(p.HIGHLIGHT_SHOW_ALL)??!0)!==e&&this.#Lt([[p.HIGHLIGHT_SHOW_ALL,e]])}enableWaiting(t=!1){if(this.#et!==t){this.#et=t;for(const e of this.#I.values()){t?e.disableClick():e.enableClick();e.div.classList.toggle("waiting",t)}}}async#Nt(){if(!this.#tt){this.#tt=!0;const t=[];for(const e of this.#I.values())t.push(e.enable());await Promise.all(t);for(const t of this.#k.values())t.enable()}}#Bt(){this.unselectAll();if(this.#tt){this.#tt=!1;for(const t of this.#I.values())t.disable();for(const t of this.#k.values())t.disable()}}getEditors(t){const e=[];for(const i of this.#k.values())i.pageIndex===t&&e.push(i);return e}getEditor(t){return this.#k.get(t)}addEditor(t){this.#k.set(t.id,t)}removeEditor(t){if(t.div.contains(document.activeElement)){this.#X&&clearTimeout(this.#X);this.#X=setTimeout((()=>{this.focusMainContainer();this.#X=null}),0)}this.#k.delete(t.id);t.annotationElementId&&this.#at?.delete(t.annotationElementId);this.unselect(t);t.annotationElementId&&this.#U.has(t.annotationElementId)||this.#F?.remove(t.id)}addDeletedAnnotationElement(t){this.#U.add(t.annotationElementId);this.addChangedExistingAnnotation(t);t.deleted=!0}isDeletedAnnotationElement(t){return this.#U.has(t)}removeDeletedAnnotationElement(t){this.#U.delete(t.annotationElementId);this.removeChangedExistingAnnotation(t);t.deleted=!1}#Rt(t){const e=this.#I.get(t.pageIndex);if(e)e.addOrRebuild(t);else{this.addEditor(t);this.addToAnnotationStorage(t)}}setActiveEditor(t){if(this.#P!==t){this.#P=t;t&&this.#Lt(t.propertiesToUpdate)}}get#Ht(){let t=null;for(t of this.#lt);return t}updateUI(t){this.#Ht===t&&this.#Lt(t.propertiesToUpdate)}updateUIForDefaultProperties(t){this.#Lt(t.defaultPropertiesToUpdate)}toggleSelected(t){if(this.#lt.has(t)){this.#lt.delete(t);t.unselect();this.#Ct({hasSelectedEditor:this.hasSelection})}else{this.#lt.add(t);t.select();this.#Lt(t.propertiesToUpdate);this.#Ct({hasSelectedEditor:!0})}}setSelected(t){this.#B?.commitOrRemove();for(const e of this.#lt)e!==t&&e.unselect();this.#lt.clear();this.#lt.add(t);t.select();this.#Lt(t.propertiesToUpdate);this.#Ct({hasSelectedEditor:!0})}isSelected(t){return this.#lt.has(t)}get firstSelectedEditor(){return this.#lt.values().next().value}unselect(t){t.unselect();this.#lt.delete(t);this.#Ct({hasSelectedEditor:this.hasSelection})}get hasSelection(){return 0!==this.#lt.size}get isEnterHandled(){return 1===this.#lt.size&&this.firstSelectedEditor.isEnterHandled}undo(){this.#O.undo();this.#Ct({hasSomethingToUndo:this.#O.hasSomethingToUndo(),hasSomethingToRedo:!0,isEmpty:this.#Ot()});this._editorUndoBar?.hide()}redo(){this.#O.redo();this.#Ct({hasSomethingToUndo:!0,hasSomethingToRedo:this.#O.hasSomethingToRedo(),isEmpty:this.#Ot()})}addCommands(t){this.#O.add(t);this.#Ct({hasSomethingToUndo:!0,hasSomethingToRedo:!1,isEmpty:this.#Ot()})}cleanUndoStack(t){this.#O.cleanType(t)}#Ot(){if(0===this.#k.size)return!0;if(1===this.#k.size)for(const t of this.#k.values())return t.isEmpty();return!1}delete(){this.commitOrRemove();const t=this.currentLayer?.endDrawingSession(!0);if(!this.hasSelection&&!t)return;const e=t?[t]:[...this.#lt],undo=()=>{for(const t of e)this.#Rt(t)};this.addCommands({cmd:()=>{this._editorUndoBar?.show(undo,1===e.length?e[0].editorType:e.length);for(const t of e)t.remove()},undo,mustExec:!0})}commitOrRemove(){this.#P?.commitOrRemove()}hasSomethingToControl(){return this.#P||this.hasSelection}#Ft(t){for(const t of this.#lt)t.unselect();this.#lt.clear();for(const e of t)if(!e.isEmpty()){this.#lt.add(e);e.select()}this.#Ct({hasSelectedEditor:this.hasSelection})}selectAll(){for(const t of this.#lt)t.commit();this.#Ft(this.#k.values())}unselectAll(){if(this.#P){this.#P.commitOrRemove();if(this.#ot!==u.NONE)return}if(!this.#B?.commitOrRemove()&&this.hasSelection){for(const t of this.#lt)t.unselect();this.#lt.clear();this.#Ct({hasSelectedEditor:!1})}}translateSelectedEditors(t,e,i=!1){i||this.commitOrRemove();if(!this.hasSelection)return;this.#gt[0]+=t;this.#gt[1]+=e;const[s,n]=this.#gt,a=[...this.#lt];this.#ft&&clearTimeout(this.#ft);this.#ft=setTimeout((()=>{this.#ft=null;this.#gt[0]=this.#gt[1]=0;this.addCommands({cmd:()=>{for(const t of a)if(this.#k.has(t.id)){t.translateInPage(s,n);t.translationDone()}},undo:()=>{for(const t of a)if(this.#k.has(t.id)){t.translateInPage(-s,-n);t.translationDone()}},mustExec:!1})}),1e3);for(const i of a){i.translateInPage(t,e);i.translationDone()}}setUpDragSession(){if(this.hasSelection){this.disableUserSelect(!0);this.#z=new Map;for(const t of this.#lt)this.#z.set(t,{savedX:t.x,savedY:t.y,savedPageIndex:t.pageIndex,newX:0,newY:0,newPageIndex:-1})}}endDragSession(){if(!this.#z)return!1;this.disableUserSelect(!1);const t=this.#z;this.#z=null;let e=!1;for(const[{x:i,y:s,pageIndex:n},a]of t){a.newX=i;a.newY=s;a.newPageIndex=n;e||=i!==a.savedX||s!==a.savedY||n!==a.savedPageIndex}if(!e)return!1;const move=(t,e,i,s)=>{if(this.#k.has(t.id)){const n=this.#I.get(s);if(n)t._setParentAndPosition(n,e,i);else{t.pageIndex=s;t.x=e;t.y=i}}};this.addCommands({cmd:()=>{for(const[e,{newX:i,newY:s,newPageIndex:n}]of t)move(e,i,s,n)},undo:()=>{for(const[e,{savedX:i,savedY:s,savedPageIndex:n}]of t)move(e,i,s,n)},mustExec:!0});return!0}dragSelectedEditors(t,e){if(this.#z)for(const i of this.#z.keys())i.drag(t,e)}rebuild(t){if(null===t.parent){const e=this.getLayer(t.pageIndex);if(e){e.changeParent(t);e.addOrRebuild(t)}else{this.addEditor(t);this.addToAnnotationStorage(t);t.rebuild()}}else t.parent.addOrRebuild(t)}get isEditorHandlingKeyboard(){return this.getActive()?.shouldGetKeyboardEvents()||1===this.#lt.size&&this.firstSelectedEditor.shouldGetKeyboardEvents()}isActive(t){return this.#P===t}getActive(){return this.#P}getMode(){return this.#ot}get imageManager(){return shadow(this,"imageManager",new ImageManager)}getSelectionBoxes(t){if(!t)return null;const e=document.getSelection();for(let i=0,s=e.rangeCount;i({x:(e-s)/a,y:1-(t+r-i)/n,width:o/a,height:r/n});break;case"180":r=(t,e,r,o)=>({x:1-(t+r-i)/n,y:1-(e+o-s)/a,width:r/n,height:o/a});break;case"270":r=(t,e,r,o)=>({x:1-(e+o-s)/a,y:(t-i)/n,width:o/a,height:r/n});break;default:r=(t,e,r,o)=>({x:(t-i)/n,y:(e-s)/a,width:r/n,height:o/a})}const o=[];for(let t=0,i=e.rangeCount;tt.stopPropagation()),{signal:i});const onClick=t=>{t.preventDefault();this.#a._uiManager.editAltText(this.#a);this.#Kt&&this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_clicked",data:{label:this.#Qt}})};t.addEventListener("click",onClick,{capture:!0,signal:i});t.addEventListener("keydown",(e=>{if(e.target===t&&"Enter"===e.key){this.#Vt=!0;onClick(e)}}),{signal:i});await this.#Jt();return t}get#Qt(){return(this.#o?"added":null===this.#o&&this.guessedText&&"review")||"missing"}finish(){if(this.#zt){this.#zt.focus({focusVisible:this.#Vt});this.#Vt=!1}}isEmpty(){return this.#Kt?null===this.#o:!this.#o&&!this.#Ut}hasData(){return this.#Kt?null!==this.#o||!!this.#qt:this.isEmpty()}get guessedText(){return this.#qt}async setGuessedText(t){if(null===this.#o){this.#qt=t;this.#Xt=await AltText._l10n.get("pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer",{generatedAltText:t});this.#Jt()}}toggleAltTextBadge(t=!1){if(this.#Kt&&!this.#o){if(!this.#jt){const t=this.#jt=document.createElement("div");t.className="noAltTextBadge";this.#a.div.append(t)}this.#jt.classList.toggle("hidden",!t)}else{this.#jt?.remove();this.#jt=null}}serialize(t){let e=this.#o;t||this.#qt!==e||(e=this.#Xt);return{altText:e,decorative:this.#Ut,guessedText:this.#qt,textWithDisclaimer:this.#Xt}}get data(){return{altText:this.#o,decorative:this.#Ut}}set data({altText:t,decorative:e,guessedText:i,textWithDisclaimer:s,cancel:n=!1}){if(i){this.#qt=i;this.#Xt=s}if(this.#o!==t||this.#Ut!==e){if(!n){this.#o=t;this.#Ut=e}this.#Jt()}}toggle(t=!1){if(this.#zt){if(!t&&this.#Wt){clearTimeout(this.#Wt);this.#Wt=null}this.#zt.disabled=!t}}shown(){this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_displayed",data:{label:this.#Qt}})}destroy(){this.#zt?.remove();this.#zt=null;this.#Gt=null;this.#$t=null;this.#jt?.remove();this.#jt=null}async#Jt(){const t=this.#zt;if(!t)return;if(this.#Kt){t.classList.toggle("done",!!this.#o);t.setAttribute("data-l10n-id",AltText.#Yt[this.#Qt]);this.#Gt?.setAttribute("data-l10n-id",AltText.#Yt[`${this.#Qt}-label`]);if(!this.#o){this.#$t?.remove();return}}else{if(!this.#o&&!this.#Ut){t.classList.remove("done");this.#$t?.remove();return}t.classList.add("done");t.setAttribute("data-l10n-id","pdfjs-editor-alt-text-edit-button")}let e=this.#$t;if(!e){this.#$t=e=document.createElement("span");e.className="tooltip";e.setAttribute("role","tooltip");e.id=`alt-text-tooltip-${this.#a.id}`;const i=100,s=this.#a._uiManager._signal;s.addEventListener("abort",(()=>{clearTimeout(this.#Wt);this.#Wt=null}),{once:!0});t.addEventListener("mouseenter",(()=>{this.#Wt=setTimeout((()=>{this.#Wt=null;this.#$t.classList.add("show");this.#a._reportTelemetry({action:"alt_text_tooltip"})}),i)}),{signal:s});t.addEventListener("mouseleave",(()=>{if(this.#Wt){clearTimeout(this.#Wt);this.#Wt=null}this.#$t?.classList.remove("show")}),{signal:s})}if(this.#Ut)e.setAttribute("data-l10n-id","pdfjs-editor-alt-text-decorative-tooltip");else{e.removeAttribute("data-l10n-id");e.textContent=this.#o}e.parentNode||t.append(e);const i=this.#a.getElementForAltText();i?.setAttribute("aria-describedby",e.id)}}class TouchManager{#mt;#Zt=!1;#te=null;#ee;#ie;#se;#ne;#ae=null;#re;#oe=null;#le;#he=null;constructor({container:t,isPinchingDisabled:e=null,isPinchingStopped:i=null,onPinchStart:s=null,onPinching:n=null,onPinchEnd:a=null,signal:r}){this.#mt=t;this.#te=i;this.#ee=e;this.#ie=s;this.#se=n;this.#ne=a;this.#le=new AbortController;this.#re=AbortSignal.any([r,this.#le.signal]);t.addEventListener("touchstart",this.#de.bind(this),{passive:!1,signal:this.#re})}get MIN_TOUCH_DISTANCE_TO_PINCH(){return 35/OutputScale.pixelRatio}#de(t){if(this.#ee?.())return;if(1===t.touches.length){if(this.#ae)return;const t=this.#ae=new AbortController,e=AbortSignal.any([this.#re,t.signal]),i=this.#mt,s={capture:!0,signal:e,passive:!1},cancelPointerDown=t=>{if("touch"===t.pointerType){this.#ae?.abort();this.#ae=null}};i.addEventListener("pointerdown",(t=>{if("touch"===t.pointerType){stopEvent(t);cancelPointerDown(t)}}),s);i.addEventListener("pointerup",cancelPointerDown,s);i.addEventListener("pointercancel",cancelPointerDown,s);return}if(!this.#he){this.#he=new AbortController;const t=AbortSignal.any([this.#re,this.#he.signal]),e=this.#mt,i={signal:t,capture:!1,passive:!1};e.addEventListener("touchmove",this.#ce.bind(this),i);const s=this.#ue.bind(this);e.addEventListener("touchend",s,i);e.addEventListener("touchcancel",s,i);i.capture=!0;e.addEventListener("pointerdown",stopEvent,i);e.addEventListener("pointermove",stopEvent,i);e.addEventListener("pointercancel",stopEvent,i);e.addEventListener("pointerup",stopEvent,i);this.#ie?.()}stopEvent(t);if(2!==t.touches.length||this.#te?.()){this.#oe=null;return}let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);this.#oe={touch0X:e.screenX,touch0Y:e.screenY,touch1X:i.screenX,touch1Y:i.screenY}}#ce(t){if(!this.#oe||2!==t.touches.length)return;stopEvent(t);let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);const{screenX:s,screenY:n}=e,{screenX:a,screenY:r}=i,o=this.#oe,{touch0X:l,touch0Y:h,touch1X:d,touch1Y:c}=o,u=d-l,p=c-h,g=a-s,f=r-n,m=Math.hypot(g,f)||1,b=Math.hypot(u,p)||1;if(!this.#Zt&&Math.abs(b-m)<=TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH)return;o.touch0X=s;o.touch0Y=n;o.touch1X=a;o.touch1Y=r;if(!this.#Zt){this.#Zt=!0;return}const A=[(s+a)/2,(n+r)/2];this.#se?.(A,b,m)}#ue(t){if(!(t.touches.length>=2)){if(this.#he){this.#he.abort();this.#he=null;this.#ne?.()}if(this.#oe){stopEvent(t);this.#oe=null;this.#Zt=!1}}}destroy(){this.#le?.abort();this.#le=null;this.#ae?.abort();this.#ae=null}}class AnnotationEditor{#pe=null;#ge=null;#o=null;#fe=!1;#me=null;#be="";#Ae=!1;#we=null;#ye=null;#ve=null;#xe=null;#Ee="";#_e=!1;#Se=null;#Ce=!1;#Te=!1;#Me=!1;#De=null;#Pe=0;#ke=0;#Ie=null;#Re=null;isSelected=!1;_isCopy=!1;_editToolbar=null;_initialOptions=Object.create(null);_initialData=null;_isVisible=!0;_uiManager=null;_focusEventsAllowed=!0;static _l10n=null;static _l10nResizer=null;#Fe=!1;#Le=AnnotationEditor._zIndex++;static _borderLineWidth=-1;static _colorManager=new ColorManager;static _zIndex=1;static _telemetryTimeout=1e3;static get _resizerKeyboardManager(){const t=AnnotationEditor.prototype._resizeWithKeyboard,e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_resizerKeyboardManager",new KeyboardManager([[["ArrowLeft","mac+ArrowLeft"],t,{args:[-e,0]}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t,{args:[-i,0]}],[["ArrowRight","mac+ArrowRight"],t,{args:[e,0]}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t,{args:[i,0]}],[["ArrowUp","mac+ArrowUp"],t,{args:[0,-e]}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t,{args:[0,-i]}],[["ArrowDown","mac+ArrowDown"],t,{args:[0,e]}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t,{args:[0,i]}],[["Escape","mac+Escape"],AnnotationEditor.prototype._stopResizingWithKeyboard]]))}constructor(t){this.parent=t.parent;this.id=t.id;this.width=this.height=null;this.pageIndex=t.parent.pageIndex;this.name=t.name;this.div=null;this._uiManager=t.uiManager;this.annotationElementId=null;this._willKeepAspectRatio=!1;this._initialOptions.isCentered=t.isCentered;this._structTreeParentId=null;const{rotation:e,rawDims:{pageWidth:i,pageHeight:s,pageX:n,pageY:a}}=this.parent.viewport;this.rotation=e;this.pageRotation=(360+e-this._uiManager.viewParameters.rotation)%360;this.pageDimensions=[i,s];this.pageTranslation=[n,a];const[r,o]=this.parentDimensions;this.x=t.x/r;this.y=t.y/o;this.isAttachedToDOM=!1;this.deleted=!1}get editorType(){return Object.getPrototypeOf(this).constructor._type}static get isDrawer(){return!1}static get _defaultLineColor(){return shadow(this,"_defaultLineColor",this._colorManager.getHexCode("CanvasText"))}static deleteAnnotationElement(t){const e=new FakeEditor({id:t.parent.getNextId(),parent:t.parent,uiManager:t._uiManager});e.annotationElementId=t.annotationElementId;e.deleted=!0;e._uiManager.addToAnnotationStorage(e)}static initialize(t,e){AnnotationEditor._l10n??=t;AnnotationEditor._l10nResizer||=Object.freeze({topLeft:"pdfjs-editor-resizer-top-left",topMiddle:"pdfjs-editor-resizer-top-middle",topRight:"pdfjs-editor-resizer-top-right",middleRight:"pdfjs-editor-resizer-middle-right",bottomRight:"pdfjs-editor-resizer-bottom-right",bottomMiddle:"pdfjs-editor-resizer-bottom-middle",bottomLeft:"pdfjs-editor-resizer-bottom-left",middleLeft:"pdfjs-editor-resizer-middle-left"});if(-1!==AnnotationEditor._borderLineWidth)return;const i=getComputedStyle(document.documentElement);AnnotationEditor._borderLineWidth=parseFloat(i.getPropertyValue("--outline-width"))||0}static updateDefaultParams(t,e){}static get defaultPropertiesToUpdate(){return[]}static isHandlingMimeForPasting(t){return!1}static paste(t,e){unreachable("Not implemented")}get propertiesToUpdate(){return[]}get _isDraggable(){return this.#Fe}set _isDraggable(t){this.#Fe=t;this.div?.classList.toggle("draggable",t)}get isEnterHandled(){return!0}center(){const[t,e]=this.pageDimensions;switch(this.parentRotation){case 90:this.x-=this.height*e/(2*t);this.y+=this.width*t/(2*e);break;case 180:this.x+=this.width/2;this.y+=this.height/2;break;case 270:this.x+=this.height*e/(2*t);this.y-=this.width*t/(2*e);break;default:this.x-=this.width/2;this.y-=this.height/2}this.fixAndSetPosition()}addCommands(t){this._uiManager.addCommands(t)}get currentLayer(){return this._uiManager.currentLayer}setInBackground(){this.div.style.zIndex=0}setInForeground(){this.div.style.zIndex=this.#Le}setParent(t){if(null!==t){this.pageIndex=t.pageIndex;this.pageDimensions=t.pageDimensions}else this.#Oe();this.parent=t}focusin(t){this._focusEventsAllowed&&(this.#_e?this.#_e=!1:this.parent.setSelected(this))}focusout(t){if(!this._focusEventsAllowed)return;if(!this.isAttachedToDOM)return;const e=t.relatedTarget;if(!e?.closest(`#${this.id}`)){t.preventDefault();this.parent?.isMultipleSelection||this.commitOrRemove()}}commitOrRemove(){this.isEmpty()?this.remove():this.commit()}commit(){this.addToAnnotationStorage()}addToAnnotationStorage(){this._uiManager.addToAnnotationStorage(this)}setAt(t,e,i,s){const[n,a]=this.parentDimensions;[i,s]=this.screenToPageTranslation(i,s);this.x=(t+i)/n;this.y=(e+s)/a;this.fixAndSetPosition()}_moveAfterPaste(t,e){const[i,s]=this.parentDimensions;this.setAt(t*i,e*s,this.width*i,this.height*s);this._onTranslated()}#Ne([t,e],i,s){[i,s]=this.screenToPageTranslation(i,s);this.x+=i/t;this.y+=s/e;this._onTranslating(this.x,this.y);this.fixAndSetPosition()}translate(t,e){this.#Ne(this.parentDimensions,t,e)}translateInPage(t,e){this.#Se||=[this.x,this.y,this.width,this.height];this.#Ne(this.pageDimensions,t,e);this.div.scrollIntoView({block:"nearest"})}translationDone(){this._onTranslated(this.x,this.y)}drag(t,e){this.#Se||=[this.x,this.y,this.width,this.height];const{div:i,parentDimensions:[s,n]}=this;this.x+=t/s;this.y+=e/n;if(this.parent&&(this.x<0||this.x>1||this.y<0||this.y>1)){const{x:t,y:e}=this.div.getBoundingClientRect();if(this.parent.findNewParent(this,t,e)){this.x-=Math.floor(this.x);this.y-=Math.floor(this.y)}}let{x:a,y:r}=this;const[o,l]=this.getBaseTranslation();a+=o;r+=l;const{style:h}=i;h.left=`${(100*a).toFixed(2)}%`;h.top=`${(100*r).toFixed(2)}%`;this._onTranslating(a,r);i.scrollIntoView({block:"nearest"})}_onTranslating(t,e){}_onTranslated(t,e){}get _hasBeenMoved(){return!!this.#Se&&(this.#Se[0]!==this.x||this.#Se[1]!==this.y)}get _hasBeenResized(){return!!this.#Se&&(this.#Se[2]!==this.width||this.#Se[3]!==this.height)}getBaseTranslation(){const[t,e]=this.parentDimensions,{_borderLineWidth:i}=AnnotationEditor,s=i/t,n=i/e;switch(this.rotation){case 90:return[-s,n];case 180:return[s,n];case 270:return[s,-n];default:return[-s,-n]}}get _mustFixPosition(){return!0}fixAndSetPosition(t=this.rotation){const{div:{style:e},pageDimensions:[i,s]}=this;let{x:n,y:a,width:r,height:o}=this;r*=i;o*=s;n*=i;a*=s;if(this._mustFixPosition)switch(t){case 0:n=MathClamp(n,0,i-r);a=MathClamp(a,0,s-o);break;case 90:n=MathClamp(n,0,i-o);a=MathClamp(a,r,s);break;case 180:n=MathClamp(n,r,i);a=MathClamp(a,o,s);break;case 270:n=MathClamp(n,o,i);a=MathClamp(a,0,s-r)}this.x=n/=i;this.y=a/=s;const[l,h]=this.getBaseTranslation();n+=l;a+=h;e.left=`${(100*n).toFixed(2)}%`;e.top=`${(100*a).toFixed(2)}%`;this.moveInDOM()}static#Be(t,e,i){switch(i){case 90:return[e,-t];case 180:return[-t,-e];case 270:return[-e,t];default:return[t,e]}}screenToPageTranslation(t,e){return AnnotationEditor.#Be(t,e,this.parentRotation)}pageTranslationToScreen(t,e){return AnnotationEditor.#Be(t,e,360-this.parentRotation)}#He(t){switch(t){case 90:{const[t,e]=this.pageDimensions;return[0,-t/e,e/t,0]}case 180:return[-1,0,0,-1];case 270:{const[t,e]=this.pageDimensions;return[0,t/e,-e/t,0]}default:return[1,0,0,1]}}get parentScale(){return this._uiManager.viewParameters.realScale}get parentRotation(){return(this._uiManager.viewParameters.rotation+this.pageRotation)%360}get parentDimensions(){const{parentScale:t,pageDimensions:[e,i]}=this;return[e*t,i*t]}setDims(t,e){const[i,s]=this.parentDimensions,{style:n}=this.div;n.width=`${(100*t/i).toFixed(2)}%`;this.#Ae||(n.height=`${(100*e/s).toFixed(2)}%`)}fixDims(){const{style:t}=this.div,{height:e,width:i}=t,s=i.endsWith("%"),n=!this.#Ae&&e.endsWith("%");if(s&&n)return;const[a,r]=this.parentDimensions;s||(t.width=`${(100*parseFloat(i)/a).toFixed(2)}%`);this.#Ae||n||(t.height=`${(100*parseFloat(e)/r).toFixed(2)}%`)}getInitialTranslation(){return[0,0]}#Ue(){if(this.#we)return;this.#we=document.createElement("div");this.#we.classList.add("resizers");const t=this._willKeepAspectRatio?["topLeft","topRight","bottomRight","bottomLeft"]:["topLeft","topMiddle","topRight","middleRight","bottomRight","bottomMiddle","bottomLeft","middleLeft"],e=this._uiManager._signal;for(const i of t){const t=document.createElement("div");this.#we.append(t);t.classList.add("resizer",i);t.setAttribute("data-resizer-name",i);t.addEventListener("pointerdown",this.#ze.bind(this,i),{signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e});t.tabIndex=-1}this.div.prepend(this.#we)}#ze(t,e){e.preventDefault();const{isMac:i}=util_FeatureTest.platform;if(0!==e.button||e.ctrlKey&&i)return;this.#o?.toggle(!1);const s=this._isDraggable;this._isDraggable=!1;this.#ye=[e.screenX,e.screenY];const n=new AbortController,a=this._uiManager.combinedSignal(n);this.parent.togglePointerEvents(!1);window.addEventListener("pointermove",this.#Ge.bind(this,t),{passive:!0,capture:!0,signal:a});window.addEventListener("touchmove",stopEvent,{passive:!1,signal:a});window.addEventListener("contextmenu",noContextMenu,{signal:a});this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const r=this.parent.div.style.cursor,o=this.div.style.cursor;this.div.style.cursor=this.parent.div.style.cursor=window.getComputedStyle(e.target).cursor;const pointerUpCallback=()=>{n.abort();this.parent.togglePointerEvents(!0);this.#o?.toggle(!0);this._isDraggable=s;this.parent.div.style.cursor=r;this.div.style.cursor=o;this.#$e()};window.addEventListener("pointerup",pointerUpCallback,{signal:a});window.addEventListener("blur",pointerUpCallback,{signal:a})}#We(t,e,i,s){this.width=i;this.height=s;this.x=t;this.y=e;const[n,a]=this.parentDimensions;this.setDims(n*i,a*s);this.fixAndSetPosition();this._onResized()}_onResized(){}#$e(){if(!this.#ve)return;const{savedX:t,savedY:e,savedWidth:i,savedHeight:s}=this.#ve;this.#ve=null;const n=this.x,a=this.y,r=this.width,o=this.height;n===t&&a===e&&r===i&&o===s||this.addCommands({cmd:this.#We.bind(this,n,a,r,o),undo:this.#We.bind(this,t,e,i,s),mustExec:!0})}static _round(t){return Math.round(1e4*t)/1e4}#Ge(t,e){const[i,s]=this.parentDimensions,n=this.x,a=this.y,r=this.width,o=this.height,l=AnnotationEditor.MIN_SIZE/i,h=AnnotationEditor.MIN_SIZE/s,d=this.#He(this.rotation),transf=(t,e)=>[d[0]*t+d[2]*e,d[1]*t+d[3]*e],c=this.#He(360-this.rotation);let u,p,g=!1,f=!1;switch(t){case"topLeft":g=!0;u=(t,e)=>[0,0];p=(t,e)=>[t,e];break;case"topMiddle":u=(t,e)=>[t/2,0];p=(t,e)=>[t/2,e];break;case"topRight":g=!0;u=(t,e)=>[t,0];p=(t,e)=>[0,e];break;case"middleRight":f=!0;u=(t,e)=>[t,e/2];p=(t,e)=>[0,e/2];break;case"bottomRight":g=!0;u=(t,e)=>[t,e];p=(t,e)=>[0,0];break;case"bottomMiddle":u=(t,e)=>[t/2,e];p=(t,e)=>[t/2,0];break;case"bottomLeft":g=!0;u=(t,e)=>[0,e];p=(t,e)=>[t,0];break;case"middleLeft":f=!0;u=(t,e)=>[0,e/2];p=(t,e)=>[t,e/2]}const m=u(r,o),b=p(r,o);let A=transf(...b);const w=AnnotationEditor._round(n+A[0]),y=AnnotationEditor._round(a+A[1]);let v,x,E=1,_=1;if(e.fromKeyboard)({deltaX:v,deltaY:x}=e);else{const{screenX:t,screenY:i}=e,[s,n]=this.#ye;[v,x]=this.screenToPageTranslation(t-s,i-n);this.#ye[0]=t;this.#ye[1]=i}[v,x]=(S=v/i,C=x/s,[c[0]*S+c[2]*C,c[1]*S+c[3]*C]);var S,C;if(g){const t=Math.hypot(r,o);E=_=Math.max(Math.min(Math.hypot(b[0]-m[0]-v,b[1]-m[1]-x)/t,1/r,1/o),l/r,h/o)}else f?E=MathClamp(Math.abs(b[0]-m[0]-v),l,1)/r:_=MathClamp(Math.abs(b[1]-m[1]-x),h,1)/o;const T=AnnotationEditor._round(r*E),M=AnnotationEditor._round(o*_);A=transf(...p(T,M));const D=w-A[0],P=y-A[1];this.#Se||=[this.x,this.y,this.width,this.height];this.width=T;this.height=M;this.x=D;this.y=P;this.setDims(i*T,s*M);this.fixAndSetPosition();this._onResizing()}_onResizing(){}altTextFinish(){this.#o?.finish()}async addEditToolbar(){if(this._editToolbar||this.#Te)return this._editToolbar;this._editToolbar=new EditorToolbar(this);this.div.append(this._editToolbar.render());this.#o&&await this._editToolbar.addAltText(this.#o);return this._editToolbar}removeEditToolbar(){if(this._editToolbar){this._editToolbar.remove();this._editToolbar=null;this.#o?.destroy()}}addContainer(t){const e=this._editToolbar?.div;e?e.before(t):this.div.append(t)}getClientDimensions(){return this.div.getBoundingClientRect()}async addAltTextButton(){if(!this.#o){AltText.initialize(AnnotationEditor._l10n);this.#o=new AltText(this);if(this.#pe){this.#o.data=this.#pe;this.#pe=null}await this.addEditToolbar()}}get altTextData(){return this.#o?.data}set altTextData(t){this.#o&&(this.#o.data=t)}get guessedAltText(){return this.#o?.guessedText}async setGuessedAltText(t){await(this.#o?.setGuessedText(t))}serializeAltText(t){return this.#o?.serialize(t)}hasAltText(){return!!this.#o&&!this.#o.isEmpty()}hasAltTextData(){return this.#o?.hasData()??!1}render(){const t=this.div=document.createElement("div");t.setAttribute("data-editor-rotation",(360-this.rotation)%360);t.className=this.name;t.setAttribute("id",this.id);t.tabIndex=this.#fe?-1:0;t.setAttribute("role","application");this.defaultL10nId&&t.setAttribute("data-l10n-id",this.defaultL10nId);this._isVisible||t.classList.add("hidden");this.setInForeground();this.#Ve();const[e,i]=this.parentDimensions;if(this.parentRotation%180!=0){t.style.maxWidth=`${(100*i/e).toFixed(2)}%`;t.style.maxHeight=`${(100*e/i).toFixed(2)}%`}const[s,n]=this.getInitialTranslation();this.translate(s,n);bindEvents(this,t,["keydown","pointerdown","dblclick"]);this.isResizable&&this._uiManager._supportsPinchToZoom&&(this.#Re||=new TouchManager({container:t,isPinchingDisabled:()=>!this.isSelected,onPinchStart:this.#je.bind(this),onPinching:this.#qe.bind(this),onPinchEnd:this.#Xe.bind(this),signal:this._uiManager._signal}));this._uiManager._editorUndoBar?.hide();return t}#je(){this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};this.#o?.toggle(!1);this.parent.togglePointerEvents(!1)}#qe(t,e,i){let s=i/e*.7+1-.7;if(1===s)return;const n=this.#He(this.rotation),transf=(t,e)=>[n[0]*t+n[2]*e,n[1]*t+n[3]*e],[a,r]=this.parentDimensions,o=this.x,l=this.y,h=this.width,d=this.height,c=AnnotationEditor.MIN_SIZE/a,u=AnnotationEditor.MIN_SIZE/r;s=Math.max(Math.min(s,1/h,1/d),c/h,u/d);const p=AnnotationEditor._round(h*s),g=AnnotationEditor._round(d*s);if(p===h&&g===d)return;this.#Se||=[o,l,h,d];const f=transf(h/2,d/2),m=AnnotationEditor._round(o+f[0]),b=AnnotationEditor._round(l+f[1]),A=transf(p/2,g/2);this.x=m-A[0];this.y=b-A[1];this.width=p;this.height=g;this.setDims(a*p,r*g);this.fixAndSetPosition();this._onResizing()}#Xe(){this.#o?.toggle(!0);this.parent.togglePointerEvents(!0);this.#$e()}pointerdown(t){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)t.preventDefault();else{this.#_e=!0;this._isDraggable?this.#Ke(t):this.#Ye(t)}}#Ye(t){const{isMac:e}=util_FeatureTest.platform;t.ctrlKey&&!e||t.shiftKey||t.metaKey&&e?this.parent.toggleSelected(this):this.parent.setSelected(this)}#Ke(t){const{isSelected:e}=this;this._uiManager.setUpDragSession();let i=!1;const s=new AbortController,n=this._uiManager.combinedSignal(s),a={capture:!0,passive:!1,signal:n},cancelDrag=t=>{s.abort();this.#me=null;this.#_e=!1;this._uiManager.endDragSession()||this.#Ye(t);i&&this._onStopDragging()};if(e){this.#Pe=t.clientX;this.#ke=t.clientY;this.#me=t.pointerId;this.#be=t.pointerType;window.addEventListener("pointermove",(t=>{if(!i){i=!0;this._onStartDragging()}const{clientX:e,clientY:s,pointerId:n}=t;if(n!==this.#me){stopEvent(t);return}const[a,r]=this.screenToPageTranslation(e-this.#Pe,s-this.#ke);this.#Pe=e;this.#ke=s;this._uiManager.dragSelectedEditors(a,r)}),a);window.addEventListener("touchmove",stopEvent,a);window.addEventListener("pointerdown",(t=>{t.pointerType===this.#be&&(this.#Re||t.isPrimary)&&cancelDrag(t);stopEvent(t)}),a)}const pointerUpCallback=t=>{this.#me&&this.#me!==t.pointerId?stopEvent(t):cancelDrag(t)};window.addEventListener("pointerup",pointerUpCallback,{signal:n});window.addEventListener("blur",pointerUpCallback,{signal:n})}_onStartDragging(){}_onStopDragging(){}moveInDOM(){this.#De&&clearTimeout(this.#De);this.#De=setTimeout((()=>{this.#De=null;this.parent?.moveEditorInDOM(this)}),0)}_setParentAndPosition(t,e,i){t.changeParent(this);this.x=e;this.y=i;this.fixAndSetPosition();this._onTranslated()}getRect(t,e,i=this.rotation){const s=this.parentScale,[n,a]=this.pageDimensions,[r,o]=this.pageTranslation,l=t/s,h=e/s,d=this.x*n,c=this.y*a,u=this.width*n,p=this.height*a;switch(i){case 0:return[d+l+r,a-c-h-p+o,d+l+u+r,a-c-h+o];case 90:return[d+h+r,a-c+l+o,d+h+p+r,a-c+l+u+o];case 180:return[d-l-u+r,a-c+h+o,d-l+r,a-c+h+p+o];case 270:return[d-h-p+r,a-c-l-u+o,d-h+r,a-c-l+o];default:throw new Error("Invalid rotation")}}getRectInCurrentCoords(t,e){const[i,s,n,a]=t,r=n-i,o=a-s;switch(this.rotation){case 0:return[i,e-a,r,o];case 90:return[i,e-s,o,r];case 180:return[n,e-s,r,o];case 270:return[n,e-a,o,r];default:throw new Error("Invalid rotation")}}onceAdded(t){}isEmpty(){return!1}enableEditMode(){if(this.isInEditMode())return!1;this.parent.setEditingState(!1);this.#Te=!0;return!0}disableEditMode(){if(!this.isInEditMode())return!1;this.parent.setEditingState(!0);this.#Te=!1;return!0}isInEditMode(){return this.#Te}shouldGetKeyboardEvents(){return this.#Me}needsToBeRebuilt(){return this.div&&!this.isAttachedToDOM}get isOnScreen(){const{top:t,left:e,bottom:i,right:s}=this.getClientDimensions(),{innerHeight:n,innerWidth:a}=window;return e0&&t0}#Ve(){if(this.#xe||!this.div)return;this.#xe=new AbortController;const t=this._uiManager.combinedSignal(this.#xe);this.div.addEventListener("focusin",this.focusin.bind(this),{signal:t});this.div.addEventListener("focusout",this.focusout.bind(this),{signal:t})}rebuild(){this.#Ve()}rotate(t){}resize(){}serializeDeleted(){return{id:this.annotationElementId,deleted:!0,pageIndex:this.pageIndex,popupRef:this._initialData?.popupRef||""}}serialize(t=!1,e=null){unreachable("An editor must be serializable")}static async deserialize(t,e,i){const s=new this.prototype.constructor({parent:e,id:e.getNextId(),uiManager:i});s.rotation=t.rotation;s.#pe=t.accessibilityData;s._isCopy=t.isCopy||!1;const[n,a]=s.pageDimensions,[r,o,l,h]=s.getRectInCurrentCoords(t.rect,a);s.x=r/n;s.y=o/a;s.width=l/n;s.height=h/a;return s}get hasBeenModified(){return!!this.annotationElementId&&(this.deleted||null!==this.serialize())}remove(){this.#xe?.abort();this.#xe=null;this.isEmpty()||this.commit();this.parent?this.parent.remove(this):this._uiManager.removeEditor(this);if(this.#De){clearTimeout(this.#De);this.#De=null}this.#Oe();this.removeEditToolbar();if(this.#Ie){for(const t of this.#Ie.values())clearTimeout(t);this.#Ie=null}this.parent=null;this.#Re?.destroy();this.#Re=null}get isResizable(){return!1}makeResizable(){if(this.isResizable){this.#Ue();this.#we.classList.remove("hidden")}}get toolbarPosition(){return null}keydown(t){if(!this.isResizable||t.target!==this.div||"Enter"!==t.key)return;this._uiManager.setSelected(this);this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const e=this.#we.children;if(!this.#ge){this.#ge=Array.from(e);const t=this.#Qe.bind(this),i=this.#Je.bind(this),s=this._uiManager._signal;for(const e of this.#ge){const n=e.getAttribute("data-resizer-name");e.setAttribute("role","spinbutton");e.addEventListener("keydown",t,{signal:s});e.addEventListener("blur",i,{signal:s});e.addEventListener("focus",this.#Ze.bind(this,n),{signal:s});e.setAttribute("data-l10n-id",AnnotationEditor._l10nResizer[n])}}const i=this.#ge[0];let s=0;for(const t of e){if(t===i)break;s++}const n=(360-this.rotation+this.parentRotation)%360/90*(this.#ge.length/4);if(n!==s){if(ns)for(let t=0;t{this.div?.classList.contains("selectedEditor")&&this._editToolbar?.show()}))}}unselect(){if(this.isSelected){this.isSelected=!1;this.#we?.classList.add("hidden");this.div?.classList.remove("selectedEditor");this.div?.contains(document.activeElement)&&this._uiManager.currentLayer.div.focus({preventScroll:!0});this._editToolbar?.hide();this.#o?.toggleAltTextBadge(!0)}}updateParams(t,e){}disableEditing(){}enableEditing(){}get canChangeContent(){return!1}enterInEditMode(){if(this.canChangeContent){this.enableEditMode();this.div.focus()}}dblclick(t){this.enterInEditMode();this.parent.updateToolbar({mode:this.constructor._editorType,editId:this.id})}getElementForAltText(){return this.div}get contentDiv(){return this.div}get isEditing(){return this.#Ce}set isEditing(t){this.#Ce=t;if(this.parent)if(t){this.parent.setSelected(this);this.parent.setActiveEditor(this)}else this.parent.setActiveEditor(null)}setAspectRatio(t,e){this.#Ae=!0;const i=t/e,{style:s}=this.div;s.aspectRatio=i;s.height="auto"}static get MIN_SIZE(){return 16}static canCreateNewEmptyEditor(){return!0}get telemetryInitialData(){return{action:"added"}}get telemetryFinalData(){return null}_reportTelemetry(t,e=!1){if(e){this.#Ie||=new Map;const{action:e}=t;let i=this.#Ie.get(e);i&&clearTimeout(i);i=setTimeout((()=>{this._reportTelemetry(t);this.#Ie.delete(e);0===this.#Ie.size&&(this.#Ie=null)}),AnnotationEditor._telemetryTimeout);this.#Ie.set(e,i)}else{t.type||=this.editorType;this._uiManager._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:t}})}}show(t=this._isVisible){this.div.classList.toggle("hidden",!t);this._isVisible=t}enable(){this.div&&(this.div.tabIndex=0);this.#fe=!1}disable(){this.div&&(this.div.tabIndex=-1);this.#fe=!0}renderAnnotationElement(t){let e=t.container.querySelector(".annotationContent");if(e){if("CANVAS"===e.nodeName){const t=e;e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.before(e)}}else{e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.container.prepend(e)}return e}resetAnnotationElement(t){const{firstChild:e}=t.container;"DIV"===e?.nodeName&&e.classList.contains("annotationContent")&&e.remove()}}class FakeEditor extends AnnotationEditor{constructor(t){super(t);this.annotationElementId=t.annotationElementId;this.deleted=!0}serialize(){return this.serializeDeleted()}}const $=3285377520,W=4294901760,V=65535;class MurmurHash3_64{constructor(t){this.h1=t?4294967295&t:$;this.h2=t?4294967295&t:$}update(t){let e,i;if("string"==typeof t){e=new Uint8Array(2*t.length);i=0;for(let s=0,n=t.length;s>>8;e[i++]=255&n}}}else{if(!ArrayBuffer.isView(t))throw new Error("Invalid data format, must be a string or TypedArray.");e=t.slice();i=e.byteLength}const s=i>>2,n=i-4*s,a=new Uint32Array(e.buffer,0,s);let r=0,o=0,l=this.h1,h=this.h2;const d=3432918353,c=461845907,u=11601,p=13715;for(let t=0;t>>17;r=r*c&W|r*p&V;l^=r;l=l<<13|l>>>19;l=5*l+3864292196}else{o=a[t];o=o*d&W|o*u&V;o=o<<15|o>>>17;o=o*c&W|o*p&V;h^=o;h=h<<13|h>>>19;h=5*h+3864292196}r=0;switch(n){case 3:r^=e[4*s+2]<<16;case 2:r^=e[4*s+1]<<8;case 1:r^=e[4*s];r=r*d&W|r*u&V;r=r<<15|r>>>17;r=r*c&W|r*p&V;1&s?l^=r:h^=r}this.h1=l;this.h2=h}hexdigest(){let t=this.h1,e=this.h2;t^=e>>>1;t=3981806797*t&W|36045*t&V;e=4283543511*e&W|(2950163797*(e<<16|t>>>16)&W)>>>16;t^=e>>>1;t=444984403*t&W|60499*t&V;e=3301882366*e&W|(3120437893*(e<<16|t>>>16)&W)>>>16;t^=e>>>1;return(t>>>0).toString(16).padStart(8,"0")+(e>>>0).toString(16).padStart(8,"0")}}const j=Object.freeze({map:null,hash:"",transfer:void 0});class AnnotationStorage{#ei=!1;#ii=null;#si=new Map;constructor(){this.onSetModified=null;this.onResetModified=null;this.onAnnotationEditor=null}getValue(t,e){const i=this.#si.get(t);return void 0===i?e:Object.assign(e,i)}getRawValue(t){return this.#si.get(t)}remove(t){this.#si.delete(t);0===this.#si.size&&this.resetModified();if("function"==typeof this.onAnnotationEditor){for(const t of this.#si.values())if(t instanceof AnnotationEditor)return;this.onAnnotationEditor(null)}}setValue(t,e){const i=this.#si.get(t);let s=!1;if(void 0!==i){for(const[t,n]of Object.entries(e))if(i[t]!==n){s=!0;i[t]=n}}else{s=!0;this.#si.set(t,e)}s&&this.#ni();e instanceof AnnotationEditor&&"function"==typeof this.onAnnotationEditor&&this.onAnnotationEditor(e.constructor._type)}has(t){return this.#si.has(t)}get size(){return this.#si.size}#ni(){if(!this.#ei){this.#ei=!0;"function"==typeof this.onSetModified&&this.onSetModified()}}resetModified(){if(this.#ei){this.#ei=!1;"function"==typeof this.onResetModified&&this.onResetModified()}}get print(){return new PrintAnnotationStorage(this)}get serializable(){if(0===this.#si.size)return j;const t=new Map,e=new MurmurHash3_64,i=[],s=Object.create(null);let n=!1;for(const[i,a]of this.#si){const r=a instanceof AnnotationEditor?a.serialize(!1,s):a;if(r){t.set(i,r);e.update(`${i}:${JSON.stringify(r)}`);n||=!!r.bitmap}}if(n)for(const e of t.values())e.bitmap&&i.push(e.bitmap);return t.size>0?{map:t,hash:e.hexdigest(),transfer:i}:j}get editorStats(){let t=null;const e=new Map;for(const i of this.#si.values()){if(!(i instanceof AnnotationEditor))continue;const s=i.telemetryFinalData;if(!s)continue;const{type:n}=s;e.has(n)||e.set(n,Object.getPrototypeOf(i).constructor);t||=Object.create(null);const a=t[n]||=new Map;for(const[t,e]of Object.entries(s)){if("type"===t)continue;let i=a.get(t);if(!i){i=new Map;a.set(t,i)}const s=i.get(e)??0;i.set(e,s+1)}}for(const[i,s]of e)t[i]=s.computeTelemetryFinalData(t[i]);return t}resetModifiedIds(){this.#ii=null}get modifiedIds(){if(this.#ii)return this.#ii;const t=[];for(const e of this.#si.values())e instanceof AnnotationEditor&&e.annotationElementId&&e.serialize()&&t.push(e.annotationElementId);return this.#ii={ids:new Set(t),hash:t.join(",")}}[Symbol.iterator](){return this.#si.entries()}}class PrintAnnotationStorage extends AnnotationStorage{#ai;constructor(t){super();const{map:e,hash:i,transfer:s}=t.serializable,n=structuredClone(e,s?{transfer:s}:null);this.#ai={map:n,hash:i,transfer:s}}get print(){unreachable("Should not call PrintAnnotationStorage.print")}get serializable(){return this.#ai}get modifiedIds(){return shadow(this,"modifiedIds",{ids:new Set,hash:""})}}class FontLoader{#ri=new Set;constructor({ownerDocument:t=globalThis.document,styleElement:e=null}){this._document=t;this.nativeFontFaces=new Set;this.styleElement=null;this.loadingRequests=[];this.loadTestFontId=0}addNativeFontFace(t){this.nativeFontFaces.add(t);this._document.fonts.add(t)}removeNativeFontFace(t){this.nativeFontFaces.delete(t);this._document.fonts.delete(t)}insertRule(t){if(!this.styleElement){this.styleElement=this._document.createElement("style");this._document.documentElement.getElementsByTagName("head")[0].append(this.styleElement)}const e=this.styleElement.sheet;e.insertRule(t,e.cssRules.length)}clear(){for(const t of this.nativeFontFaces)this._document.fonts.delete(t);this.nativeFontFaces.clear();this.#ri.clear();if(this.styleElement){this.styleElement.remove();this.styleElement=null}}async loadSystemFont({systemFontInfo:t,disableFontFace:e,_inspectFont:i}){if(t&&!this.#ri.has(t.loadedName)){assert(!e,"loadSystemFont shouldn't be called when `disableFontFace` is set.");if(this.isFontLoadingAPISupported){const{loadedName:e,src:s,style:n}=t,a=new FontFace(e,s,n);this.addNativeFontFace(a);try{await a.load();this.#ri.add(e);i?.(t)}catch{warn(`Cannot load system font: ${t.baseFontName}, installing it could help to improve PDF rendering.`);this.removeNativeFontFace(a)}}else unreachable("Not implemented: loadSystemFont without the Font Loading API.")}}async bind(t){if(t.attached||t.missingFile&&!t.systemFontInfo)return;t.attached=!0;if(t.systemFontInfo){await this.loadSystemFont(t);return}if(this.isFontLoadingAPISupported){const e=t.createNativeFontFace();if(e){this.addNativeFontFace(e);try{await e.loaded}catch(i){warn(`Failed to load font '${e.family}': '${i}'.`);t.disableFontFace=!0;throw i}}return}const e=t.createFontFaceRule();if(e){this.insertRule(e);if(this.isSyncFontLoadingSupported)return;await new Promise((e=>{const i=this._queueLoadingCallback(e);this._prepareFontLoadEvent(t,i)}))}}get isFontLoadingAPISupported(){return shadow(this,"isFontLoadingAPISupported",!!this._document?.fonts)}get isSyncFontLoadingSupported(){return shadow(this,"isSyncFontLoadingSupported",t||util_FeatureTest.platform.isFirefox)}_queueLoadingCallback(t){const{loadingRequests:e}=this,i={done:!1,complete:function completeRequest(){assert(!i.done,"completeRequest() cannot be called twice.");i.done=!0;for(;e.length>0&&e[0].done;){const t=e.shift();setTimeout(t.callback,0)}},callback:t};e.push(i);return i}get _loadTestFont(){return shadow(this,"_loadTestFont",atob("T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA=="))}_prepareFontLoadEvent(t,e){function int32(t,e){return t.charCodeAt(e)<<24|t.charCodeAt(e+1)<<16|t.charCodeAt(e+2)<<8|255&t.charCodeAt(e+3)}function spliceString(t,e,i,s){return t.substring(0,e)+s+t.substring(e+i)}let i,s;const n=this._document.createElement("canvas");n.width=1;n.height=1;const a=n.getContext("2d");let r=0;const o=`lt${Date.now()}${this.loadTestFontId++}`;let l=this._loadTestFont;l=spliceString(l,976,o.length,o);const h=1482184792;let d=int32(l,16);for(i=0,s=o.length-3;i>24&255,t>>16&255,t>>8&255,255&t)}(d));const c=`@font-face {font-family:"${o}";src:${`url(data:font/opentype;base64,${btoa(l)});`}}`;this.insertRule(c);const u=this._document.createElement("div");u.style.visibility="hidden";u.style.width=u.style.height="10px";u.style.position="absolute";u.style.top=u.style.left="0px";for(const e of[t.loadedName,o]){const t=this._document.createElement("span");t.textContent="Hi";t.style.fontFamily=e;u.append(t)}this._document.body.append(u);!function isFontReady(t,e){if(++r>30){warn("Load test font never loaded.");e();return}a.font="30px "+t;a.fillText(".",0,20);a.getImageData(0,0,1,1).data[3]>0?e():setTimeout(isFontReady.bind(null,t,e))}(o,(()=>{u.remove();e.complete()}))}}class FontFaceObject{constructor(t,e=null){this.compiledGlyphs=Object.create(null);for(const e in t)this[e]=t[e];this._inspectFont=e}createNativeFontFace(){if(!this.data||this.disableFontFace)return null;let t;if(this.cssFontInfo){const e={weight:this.cssFontInfo.fontWeight};this.cssFontInfo.italicAngle&&(e.style=`oblique ${this.cssFontInfo.italicAngle}deg`);t=new FontFace(this.cssFontInfo.fontFamily,this.data,e)}else t=new FontFace(this.loadedName,this.data,{});this._inspectFont?.(this);return t}createFontFaceRule(){if(!this.data||this.disableFontFace)return null;const t=`url(data:${this.mimetype};base64,${toBase64Util(this.data)});`;let e;if(this.cssFontInfo){let i=`font-weight: ${this.cssFontInfo.fontWeight};`;this.cssFontInfo.italicAngle&&(i+=`font-style: oblique ${this.cssFontInfo.italicAngle}deg;`);e=`@font-face {font-family:"${this.cssFontInfo.fontFamily}";${i}src:${t}}`}else e=`@font-face {font-family:"${this.loadedName}";src:${t}}`;this._inspectFont?.(this,t);return e}getPathGenerator(t,e){if(void 0!==this.compiledGlyphs[e])return this.compiledGlyphs[e];const i=this.loadedName+"_path_"+e;let s;try{s=t.get(i)}catch(t){warn(`getPathGenerator - ignoring character: "${t}".`)}const n=new Path2D(s||"");this.fontExtraProperties||t.delete(i);return this.compiledGlyphs[e]=n}}function getFactoryUrlProp(t){if("string"!=typeof t)return null;if(t.endsWith("/"))return t;throw new Error(`Invalid factory url: "${t}" must include trailing slash.`)}const isRefProxy=t=>"object"==typeof t&&Number.isInteger(t?.num)&&t.num>=0&&Number.isInteger(t?.gen)&&t.gen>=0,q=function _isValidExplicitDest(t,e,i){if(!Array.isArray(i)||i.length<2)return!1;const[s,n,...a]=i;if(!t(s)&&!Number.isInteger(s))return!1;if(!e(n))return!1;const r=a.length;let o=!0;switch(n.name){case"XYZ":if(r<2||r>3)return!1;break;case"Fit":case"FitB":return 0===r;case"FitH":case"FitBH":case"FitV":case"FitBV":if(r>1)return!1;break;case"FitR":if(4!==r)return!1;o=!1;break;default:return!1}for(const t of a)if(!("number"==typeof t||o&&null===t))return!1;return!0}.bind(null,isRefProxy,(t=>"object"==typeof t&&"string"==typeof t?.name));class LoopbackPort{#oi=new Map;#li=Promise.resolve();postMessage(t,e){const i={data:structuredClone(t,e?{transfer:e}:null)};this.#li.then((()=>{for(const[t]of this.#oi)t.call(this,i)}))}addEventListener(t,e,i=null){let s=null;if(i?.signal instanceof AbortSignal){const{signal:n}=i;if(n.aborted){warn("LoopbackPort - cannot use an `aborted` signal.");return}const onAbort=()=>this.removeEventListener(t,e);s=()=>n.removeEventListener("abort",onAbort);n.addEventListener("abort",onAbort)}this.#oi.set(e,s)}removeEventListener(t,e){const i=this.#oi.get(e);i?.();this.#oi.delete(e)}terminate(){for(const[,t]of this.#oi)t?.();this.#oi.clear()}}const X=1,K=2,Y=1,Q=2,J=3,Z=4,tt=5,et=6,it=7,st=8;function onFn(){}function wrapReason(t){if(t instanceof AbortException||t instanceof InvalidPDFException||t instanceof PasswordException||t instanceof ResponseException||t instanceof UnknownErrorException)return t;t instanceof Error||"object"==typeof t&&null!==t||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(t.name){case"AbortException":return new AbortException(t.message);case"InvalidPDFException":return new InvalidPDFException(t.message);case"PasswordException":return new PasswordException(t.message,t.code);case"ResponseException":return new ResponseException(t.message,t.status,t.missing);case"UnknownErrorException":return new UnknownErrorException(t.message,t.details)}return new UnknownErrorException(t.message,t.toString())}class MessageHandler{#hi=new AbortController;constructor(t,e,i){this.sourceName=t;this.targetName=e;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);i.addEventListener("message",this.#di.bind(this),{signal:this.#hi.signal})}#di({data:t}){if(t.targetName!==this.sourceName)return;if(t.stream){this.#ci(t);return}if(t.callback){const e=t.callbackId,i=this.callbackCapabilities[e];if(!i)throw new Error(`Cannot resolve callback ${e}`);delete this.callbackCapabilities[e];if(t.callback===X)i.resolve(t.data);else{if(t.callback!==K)throw new Error("Unexpected callback case");i.reject(wrapReason(t.reason))}return}const e=this.actionHandler[t.action];if(!e)throw new Error(`Unknown action from worker: ${t.action}`);if(t.callbackId){const i=this.sourceName,s=t.sourceName,n=this.comObj;Promise.try(e,t.data).then((function(e){n.postMessage({sourceName:i,targetName:s,callback:X,callbackId:t.callbackId,data:e})}),(function(e){n.postMessage({sourceName:i,targetName:s,callback:K,callbackId:t.callbackId,reason:wrapReason(e)})}))}else t.streamId?this.#ui(t):e(t.data)}on(t,e){const i=this.actionHandler;if(i[t])throw new Error(`There is already an actionName called "${t}"`);i[t]=e}send(t,e,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,data:e},i)}sendWithPromise(t,e,i){const s=this.callbackId++,n=Promise.withResolvers();this.callbackCapabilities[s]=n;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,callbackId:s,data:e},i)}catch(t){n.reject(t)}return n.promise}sendWithStream(t,e,i,s){const n=this.streamId++,a=this.sourceName,r=this.targetName,o=this.comObj;return new ReadableStream({start:i=>{const l=Promise.withResolvers();this.streamControllers[n]={controller:i,startCall:l,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:a,targetName:r,action:t,streamId:n,data:e,desiredSize:i.desiredSize},s);return l.promise},pull:t=>{const e=Promise.withResolvers();this.streamControllers[n].pullCall=e;o.postMessage({sourceName:a,targetName:r,stream:et,streamId:n,desiredSize:t.desiredSize});return e.promise},cancel:t=>{assert(t instanceof Error,"cancel must have a valid reason");const e=Promise.withResolvers();this.streamControllers[n].cancelCall=e;this.streamControllers[n].isClosed=!0;o.postMessage({sourceName:a,targetName:r,stream:Y,streamId:n,reason:wrapReason(t)});return e.promise}},i)}#ui(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this,r=this.actionHandler[t.action],o={enqueue(t,a=1,r){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=a;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}n.postMessage({sourceName:i,targetName:s,stream:Z,streamId:e,chunk:t},r)},close(){if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:J,streamId:e});delete a.streamSinks[e]}},error(t){assert(t instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:tt,streamId:e,reason:wrapReason(t)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:t.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[e]=o;Promise.try(r,t.data,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:st,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:st,streamId:e,reason:wrapReason(t)})}))}#ci(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this.streamControllers[e],r=this.streamSinks[e];switch(t.stream){case st:t.success?a.startCall.resolve():a.startCall.reject(wrapReason(t.reason));break;case it:t.success?a.pullCall.resolve():a.pullCall.reject(wrapReason(t.reason));break;case et:if(!r){n.postMessage({sourceName:i,targetName:s,stream:it,streamId:e,success:!0});break}r.desiredSize<=0&&t.desiredSize>0&&r.sinkCapability.resolve();r.desiredSize=t.desiredSize;Promise.try(r.onPull||onFn).then((function(){n.postMessage({sourceName:i,targetName:s,stream:it,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:it,streamId:e,reason:wrapReason(t)})}));break;case Z:assert(a,"enqueue should have stream controller");if(a.isClosed)break;a.controller.enqueue(t.chunk);break;case J:assert(a,"close should have stream controller");if(a.isClosed)break;a.isClosed=!0;a.controller.close();this.#pi(a,e);break;case tt:assert(a,"error should have stream controller");a.controller.error(wrapReason(t.reason));this.#pi(a,e);break;case Q:t.success?a.cancelCall.resolve():a.cancelCall.reject(wrapReason(t.reason));this.#pi(a,e);break;case Y:if(!r)break;const o=wrapReason(t.reason);Promise.try(r.onCancel||onFn,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:Q,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:Q,streamId:e,reason:wrapReason(t)})}));r.sinkCapability.reject(o);r.isCancelled=!0;delete this.streamSinks[e];break;default:throw new Error("Unexpected stream case")}}async#pi(t,e){await Promise.allSettled([t.startCall?.promise,t.pullCall?.promise,t.cancelCall?.promise]);delete this.streamControllers[e]}destroy(){this.#hi?.abort();this.#hi=null}}class BaseCanvasFactory{#gi=!1;constructor({enableHWA:t=!1}){this.#gi=t}create(t,e){if(t<=0||e<=0)throw new Error("Invalid canvas size");const i=this._createCanvas(t,e);return{canvas:i,context:i.getContext("2d",{willReadFrequently:!this.#gi})}}reset(t,e,i){if(!t.canvas)throw new Error("Canvas is not specified");if(e<=0||i<=0)throw new Error("Invalid canvas size");t.canvas.width=e;t.canvas.height=i}destroy(t){if(!t.canvas)throw new Error("Canvas is not specified");t.canvas.width=0;t.canvas.height=0;t.canvas=null;t.context=null}_createCanvas(t,e){unreachable("Abstract method `_createCanvas` called.")}}class DOMCanvasFactory extends BaseCanvasFactory{constructor({ownerDocument:t=globalThis.document,enableHWA:e=!1}){super({enableHWA:e});this._document=t}_createCanvas(t,e){const i=this._document.createElement("canvas");i.width=t;i.height=e;return i}}class BaseCMapReaderFactory{constructor({baseUrl:t=null,isCompressed:e=!0}){this.baseUrl=t;this.isCompressed=e}async fetch({name:t}){if(!this.baseUrl)throw new Error("Ensure that the `cMapUrl` and `cMapPacked` API parameters are provided.");if(!t)throw new Error("CMap name must be specified.");const e=this.baseUrl+t+(this.isCompressed?".bcmap":"");return this._fetch(e).then((t=>({cMapData:t,isCompressed:this.isCompressed}))).catch((t=>{throw new Error(`Unable to load ${this.isCompressed?"binary ":""}CMap at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){const e=await fetchData(t,this.isCompressed?"arraybuffer":"text");return e instanceof ArrayBuffer?new Uint8Array(e):stringToBytes(e)}}class BaseFilterFactory{addFilter(t){return"none"}addHCMFilter(t,e){return"none"}addAlphaFilter(t){return"none"}addLuminosityFilter(t){return"none"}addHighlightHCMFilter(t,e,i,s,n){return"none"}destroy(t=!1){}}class DOMFilterFactory extends BaseFilterFactory{#fi;#mi;#bi;#Ai;#wi;#yi;#y=0;constructor({docId:t,ownerDocument:e=globalThis.document}){super();this.#Ai=t;this.#wi=e}get#x(){return this.#mi||=new Map}get#vi(){return this.#yi||=new Map}get#xi(){if(!this.#bi){const t=this.#wi.createElement("div"),{style:e}=t;e.visibility="hidden";e.contain="strict";e.width=e.height=0;e.position="absolute";e.top=e.left=0;e.zIndex=-1;const i=this.#wi.createElementNS(z,"svg");i.setAttribute("width",0);i.setAttribute("height",0);this.#bi=this.#wi.createElementNS(z,"defs");t.append(i);i.append(this.#bi);this.#wi.body.append(t)}return this.#bi}#Ei(t){if(1===t.length){const e=t[0],i=new Array(256);for(let t=0;t<256;t++)i[t]=e[t]/255;const s=i.join(",");return[s,s,s]}const[e,i,s]=t,n=new Array(256),a=new Array(256),r=new Array(256);for(let t=0;t<256;t++){n[t]=e[t]/255;a[t]=i[t]/255;r[t]=s[t]/255}return[n.join(","),a.join(","),r.join(",")]}#_i(t){if(void 0===this.#fi){this.#fi="";const t=this.#wi.URL;t!==this.#wi.baseURI&&(isDataScheme(t)?warn('#createUrl: ignore "data:"-URL for performance reasons.'):this.#fi=updateUrlHash(t,""))}return`url(${this.#fi}#${t})`}addFilter(t){if(!t)return"none";let e=this.#x.get(t);if(e)return e;const[i,s,n]=this.#Ei(t),a=1===t.length?i:`${i}${s}${n}`;e=this.#x.get(a);if(e){this.#x.set(t,e);return e}const r=`g_${this.#Ai}_transfer_map_${this.#y++}`,o=this.#_i(r);this.#x.set(t,o);this.#x.set(a,o);const l=this.#Si(r);this.#Ci(i,s,n,l);return o}addHCMFilter(t,e){const i=`${t}-${e}`,s="base";let n=this.#vi.get(s);if(n?.key===i)return n.url;if(n){n.filter?.remove();n.key=i;n.url="none";n.filter=null}else{n={key:i,url:"none",filter:null};this.#vi.set(s,n)}if(!t||!e)return n.url;const a=this.#Ti(t);t=Util.makeHexColor(...a);const r=this.#Ti(e);e=Util.makeHexColor(...r);this.#xi.style.color="";if("#000000"===t&&"#ffffff"===e||t===e)return n.url;const o=new Array(256);for(let t=0;t<=255;t++){const e=t/255;o[t]=e<=.03928?e/12.92:((e+.055)/1.055)**2.4}const l=o.join(","),h=`g_${this.#Ai}_hcm_filter`,d=n.filter=this.#Si(h);this.#Ci(l,l,l,d);this.#Mi(d);const getSteps=(t,e)=>{const i=a[t]/255,s=r[t]/255,n=new Array(e+1);for(let t=0;t<=e;t++)n[t]=i+t/e*(s-i);return n.join(",")};this.#Ci(getSteps(0,5),getSteps(1,5),getSteps(2,5),d);n.url=this.#_i(h);return n.url}addAlphaFilter(t){let e=this.#x.get(t);if(e)return e;const[i]=this.#Ei([t]),s=`alpha_${i}`;e=this.#x.get(s);if(e){this.#x.set(t,e);return e}const n=`g_${this.#Ai}_alpha_map_${this.#y++}`,a=this.#_i(n);this.#x.set(t,a);this.#x.set(s,a);const r=this.#Si(n);this.#Di(i,r);return a}addLuminosityFilter(t){let e,i,s=this.#x.get(t||"luminosity");if(s)return s;if(t){[e]=this.#Ei([t]);i=`luminosity_${e}`}else i="luminosity";s=this.#x.get(i);if(s){this.#x.set(t,s);return s}const n=`g_${this.#Ai}_luminosity_map_${this.#y++}`,a=this.#_i(n);this.#x.set(t,a);this.#x.set(i,a);const r=this.#Si(n);this.#Pi(r);t&&this.#Di(e,r);return a}addHighlightHCMFilter(t,e,i,s,n){const a=`${e}-${i}-${s}-${n}`;let r=this.#vi.get(t);if(r?.key===a)return r.url;if(r){r.filter?.remove();r.key=a;r.url="none";r.filter=null}else{r={key:a,url:"none",filter:null};this.#vi.set(t,r)}if(!e||!i)return r.url;const[o,l]=[e,i].map(this.#Ti.bind(this));let h=Math.round(.2126*o[0]+.7152*o[1]+.0722*o[2]),d=Math.round(.2126*l[0]+.7152*l[1]+.0722*l[2]),[c,u]=[s,n].map(this.#Ti.bind(this));d{const s=new Array(256),n=(d-h)/i,a=t/255,r=(e-t)/(255*i);let o=0;for(let t=0;t<=i;t++){const e=Math.round(h+t*n),i=a+t*r;for(let t=o;t<=e;t++)s[t]=i;o=e+1}for(let t=o;t<256;t++)s[t]=s[o-1];return s.join(",")},p=`g_${this.#Ai}_hcm_${t}_filter`,g=r.filter=this.#Si(p);this.#Mi(g);this.#Ci(getSteps(c[0],u[0],5),getSteps(c[1],u[1],5),getSteps(c[2],u[2],5),g);r.url=this.#_i(p);return r.url}destroy(t=!1){if(!t||!this.#yi?.size){this.#bi?.parentNode.parentNode.remove();this.#bi=null;this.#mi?.clear();this.#mi=null;this.#yi?.clear();this.#yi=null;this.#y=0}}#Pi(t){const e=this.#wi.createElementNS(z,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0");t.append(e)}#Mi(t){const e=this.#wi.createElementNS(z,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");t.append(e)}#Si(t){const e=this.#wi.createElementNS(z,"filter");e.setAttribute("color-interpolation-filters","sRGB");e.setAttribute("id",t);this.#xi.append(e);return e}#ki(t,e,i){const s=this.#wi.createElementNS(z,e);s.setAttribute("type","discrete");s.setAttribute("tableValues",i);t.append(s)}#Ci(t,e,i,s){const n=this.#wi.createElementNS(z,"feComponentTransfer");s.append(n);this.#ki(n,"feFuncR",t);this.#ki(n,"feFuncG",e);this.#ki(n,"feFuncB",i)}#Di(t,e){const i=this.#wi.createElementNS(z,"feComponentTransfer");e.append(i);this.#ki(i,"feFuncA",t)}#Ti(t){this.#xi.style.color=t;return getRGB(getComputedStyle(this.#xi).getPropertyValue("color"))}}class BaseStandardFontDataFactory{constructor({baseUrl:t=null}){this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error("Ensure that the `standardFontDataUrl` API parameter is provided.");if(!t)throw new Error("Font filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetch(e).catch((t=>{throw new Error(`Unable to load font data at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){const e=await fetchData(t,"arraybuffer");return new Uint8Array(e)}}class BaseWasmFactory{constructor({baseUrl:t=null}){this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error("Ensure that the `wasmUrl` API parameter is provided.");if(!t)throw new Error("Wasm filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetch(e).catch((t=>{throw new Error(`Unable to load wasm data at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMWasmFactory extends BaseWasmFactory{async _fetch(t){const e=await fetchData(t,"arraybuffer");return new Uint8Array(e)}}t&&warn("Please use the `legacy` build in Node.js environments.");async function node_utils_fetchData(t){const e=process.getBuiltinModule("fs"),i=await e.promises.readFile(t);return new Uint8Array(i)}class NodeFilterFactory extends BaseFilterFactory{}class NodeCanvasFactory extends BaseCanvasFactory{_createCanvas(t,e){return process.getBuiltinModule("module").createRequire(import.meta.url)("@napi-rs/canvas").createCanvas(t,e)}}class NodeCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){return node_utils_fetchData(t)}}class NodeStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){return node_utils_fetchData(t)}}class NodeWasmFactory extends BaseWasmFactory{async _fetch(t){return node_utils_fetchData(t)}}const nt="Fill",at="Stroke",rt="Shading";function applyBoundingBox(t,e){if(!e)return;const i=e[2]-e[0],s=e[3]-e[1],n=new Path2D;n.rect(e[0],e[1],i,s);t.clip(n)}class BaseShadingPattern{isModifyingCurrentTransform(){return!1}getPattern(){unreachable("Abstract method `getPattern` called.")}}class RadialAxialShadingPattern extends BaseShadingPattern{constructor(t){super();this._type=t[1];this._bbox=t[2];this._colorStops=t[3];this._p0=t[4];this._p1=t[5];this._r0=t[6];this._r1=t[7];this.matrix=null}_createGradient(t){let e;"axial"===this._type?e=t.createLinearGradient(this._p0[0],this._p0[1],this._p1[0],this._p1[1]):"radial"===this._type&&(e=t.createRadialGradient(this._p0[0],this._p0[1],this._r0,this._p1[0],this._p1[1],this._r1));for(const t of this._colorStops)e.addColorStop(t[0],t[1]);return e}getPattern(t,e,i,s){let n;if(s===at||s===nt){const a=e.current.getClippedPathBoundingBox(s,getCurrentTransform(t))||[0,0,0,0],r=Math.ceil(a[2]-a[0])||1,o=Math.ceil(a[3]-a[1])||1,l=e.cachedCanvases.getCanvas("pattern",r,o),h=l.context;h.clearRect(0,0,h.canvas.width,h.canvas.height);h.beginPath();h.rect(0,0,h.canvas.width,h.canvas.height);h.translate(-a[0],-a[1]);i=Util.transform(i,[1,0,0,1,a[0],a[1]]);h.transform(...e.baseTransform);this.matrix&&h.transform(...this.matrix);applyBoundingBox(h,this._bbox);h.fillStyle=this._createGradient(h);h.fill();n=t.createPattern(l.canvas,"no-repeat");const d=new DOMMatrix(i);n.setTransform(d)}else{applyBoundingBox(t,this._bbox);n=this._createGradient(t)}return n}}function drawTriangle(t,e,i,s,n,a,r,o){const l=e.coords,h=e.colors,d=t.data,c=4*t.width;let u;if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}if(l[s+1]>l[n+1]){u=s;s=n;n=u;u=r;r=o;o=u}if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}const p=(l[i]+e.offsetX)*e.scaleX,g=(l[i+1]+e.offsetY)*e.scaleY,f=(l[s]+e.offsetX)*e.scaleX,m=(l[s+1]+e.offsetY)*e.scaleY,b=(l[n]+e.offsetX)*e.scaleX,A=(l[n+1]+e.offsetY)*e.scaleY;if(g>=A)return;const w=h[a],y=h[a+1],v=h[a+2],x=h[r],E=h[r+1],_=h[r+2],S=h[o],C=h[o+1],T=h[o+2],M=Math.round(g),D=Math.round(A);let P,k,I,R,F,L,O,N;for(let t=M;t<=D;t++){if(tA?1:m===A?0:(m-t)/(m-A);P=f-(f-b)*e;k=x-(x-S)*e;I=E-(E-C)*e;R=_-(_-T)*e}let e;e=tA?1:(g-t)/(g-A);F=p-(p-b)*e;L=w-(w-S)*e;O=y-(y-C)*e;N=v-(v-T)*e;const i=Math.round(Math.min(P,F)),s=Math.round(Math.max(P,F));let n=c*t+4*i;for(let t=i;t<=s;t++){e=(P-t)/(P-F);e<0?e=0:e>1&&(e=1);d[n++]=k-(k-L)*e|0;d[n++]=I-(I-O)*e|0;d[n++]=R-(R-N)*e|0;d[n++]=255}}}function drawFigure(t,e,i){const s=e.coords,n=e.colors;let a,r;switch(e.type){case"lattice":const o=e.verticesPerRow,l=Math.floor(s.length/o)-1,h=o-1;for(a=0;a=Math.ceil(p*A)?y=o:x=!0;S>=Math.ceil(g*w)?v=l:E=!0;const C=this.getSizeAndScale(y,this.ctx.canvas.width,A),T=this.getSizeAndScale(v,this.ctx.canvas.height,w),M=t.cachedCanvases.getCanvas("pattern",C.size,T.size),D=M.context,P=r.createCanvasGraphics(D);P.groupLevel=t.groupLevel;this.setFillAndStrokeStyleToContext(P,s,a);D.translate(-C.scale*h,-T.scale*d);P.transform(C.scale,0,0,T.scale,0,0);D.save();this.clipBbox(P,h,d,c,u);P.baseTransform=getCurrentTransform(P.ctx);P.executeOperatorList(i);P.endDrawing();D.restore();if(x||E){const e=M.canvas;x&&(y=o);E&&(v=l);const i=this.getSizeAndScale(y,this.ctx.canvas.width,A),s=this.getSizeAndScale(v,this.ctx.canvas.height,w),n=i.size,a=s.size,r=t.cachedCanvases.getCanvas("pattern-workaround",n,a),c=r.context,u=x?Math.floor(p/o):0,f=E?Math.floor(g/l):0;for(let t=0;t<=u;t++)for(let i=0;i<=f;i++)c.drawImage(e,n*t,a*i,n,a,0,0,n,a);return{canvas:r.canvas,scaleX:i.scale,scaleY:s.scale,offsetX:h,offsetY:d}}return{canvas:M.canvas,scaleX:C.scale,scaleY:T.scale,offsetX:h,offsetY:d}}getSizeAndScale(t,e,i){const s=Math.max(TilingPattern.MAX_PATTERN_SIZE,e);let n=Math.ceil(t*i);n>=s?n=s:i=n/t;return{scale:i,size:n}}clipBbox(t,e,i,s,n){const a=s-e,r=n-i;t.ctx.rect(e,i,a,r);Util.axialAlignedBoundingBox([e,i,s,n],getCurrentTransform(t.ctx),t.current.minMax);t.clip();t.endPath()}setFillAndStrokeStyleToContext(t,e,i){const s=t.ctx,n=t.current;switch(e){case ot:const{fillStyle:t,strokeStyle:a}=this.ctx;s.fillStyle=n.fillColor=t;s.strokeStyle=n.strokeColor=a;break;case lt:s.fillStyle=s.strokeStyle=i;n.fillColor=n.strokeColor=i;break;default:throw new FormatError(`Unsupported paint type: ${e}`)}}isModifyingCurrentTransform(){return!1}getPattern(t,e,i,s){let n=i;if(s!==rt){n=Util.transform(n,e.baseTransform);this.matrix&&(n=Util.transform(n,this.matrix))}const a=this.createPatternCanvas(e);let r=new DOMMatrix(n);r=r.translate(a.offsetX,a.offsetY);r=r.scale(1/a.scaleX,1/a.scaleY);const o=t.createPattern(a.canvas,"repeat");o.setTransform(r);return o}}function convertBlackAndWhiteToRGBA({src:t,srcPos:e=0,dest:i,width:s,height:n,nonBlackColor:a=4294967295,inverseDecode:r=!1}){const o=util_FeatureTest.isLittleEndian?4278190080:255,[l,h]=r?[a,o]:[o,a],d=s>>3,c=7&s,u=t.length;i=new Uint32Array(i.buffer);let p=0;for(let s=0;s>2),f=i.length,m=s+7>>3,b=4294967295,A=util_FeatureTest.isLittleEndian?4278190080:255;for(u=0;um?s:8*t-7,r=-8&a;let o=0,c=0;for(;n>=1}}for(;l=a){g=n;f=s*g}l=0;for(p=f;p--;){c[l++]=d[h++];c[l++]=d[h++];c[l++]=d[h++];c[l++]=255}t.putImageData(o,0,u*ht)}}}function putBinaryImageMask(t,e){if(e.bitmap){t.drawImage(e.bitmap,0,0);return}const i=e.height,s=e.width,n=i%ht,a=(i-n)/ht,r=0===n?a:a+1,o=t.createImageData(s,ht);let l=0;const h=e.data,d=o.data;for(let e=0;e10&&"function"==typeof i,h=l?Date.now()+15:0;let d=0;const c=this.commonObjs,u=this.objs;let p;for(;;){if(void 0!==s&&r===s.nextBreakPoint){s.breakIt(r,i);return r}p=a[r];if(p!==D.dependency)this[p].apply(this,n[r]);else for(const t of n[r]){const e=t.startsWith("g_")?c:u;if(!e.has(t)){e.get(t,i);return r}}r++;if(r===o)return r;if(l&&++d>10){if(Date.now()>h){i();return r}d=0}}}#Ii(){for(;this.stateStack.length||this.inSMaskMode;)this.restore();this.current.activeSMask=null;this.ctx.restore();if(this.transparentCanvas){this.ctx=this.compositeCtx;this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.drawImage(this.transparentCanvas,0,0);this.ctx.restore();this.transparentCanvas=null}}endDrawing(){this.#Ii();this.cachedCanvases.clear();this.cachedPatterns.clear();for(const t of this._cachedBitmapsMap.values()){for(const e of t.values())"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement&&(e.width=e.height=0);t.clear()}this._cachedBitmapsMap.clear();this.#Ri()}#Ri(){if(this.pageColors){const t=this.filterFactory.addHCMFilter(this.pageColors.foreground,this.pageColors.background);if("none"!==t){const e=this.ctx.filter;this.ctx.filter=t;this.ctx.drawImage(this.ctx.canvas,0,0);this.ctx.filter=e}}}_scaleImage(t,e){const i=t.width??t.displayWidth,s=t.height??t.displayHeight;let n,a,r=Math.max(Math.hypot(e[0],e[1]),1),o=Math.max(Math.hypot(e[2],e[3]),1),l=i,h=s,d="prescale1";for(;r>2&&l>1||o>2&&h>1;){let e=l,i=h;if(r>2&&l>1){e=l>=16384?Math.floor(l/2)-1||1:Math.ceil(l/2);r/=l/e}if(o>2&&h>1){i=h>=16384?Math.floor(h/2)-1||1:Math.ceil(h)/2;o/=h/i}n=this.cachedCanvases.getCanvas(d,e,i);a=n.context;a.clearRect(0,0,e,i);a.drawImage(t,0,0,l,h,0,0,e,i);t=n.canvas;l=e;h=i;d="prescale1"===d?"prescale2":"prescale1"}return{img:t,paintWidth:l,paintHeight:h}}_createMaskCanvas(t){const e=this.ctx,{width:i,height:s}=t,n=this.current.fillColor,a=this.current.patternFill,r=getCurrentTransform(e);let o,l,h,d;if((t.bitmap||t.data)&&t.count>1){const e=t.bitmap||t.data.buffer;l=JSON.stringify(a?r:[r.slice(0,4),n]);o=this._cachedBitmapsMap.get(e);if(!o){o=new Map;this._cachedBitmapsMap.set(e,o)}const i=o.get(l);if(i&&!a){return{canvas:i,offsetX:Math.round(Math.min(r[0],r[2])+r[4]),offsetY:Math.round(Math.min(r[1],r[3])+r[5])}}h=i}if(!h){d=this.cachedCanvases.getCanvas("maskCanvas",i,s);putBinaryImageMask(d.context,t)}let c=Util.transform(r,[1/i,0,0,-1/s,0,0]);c=Util.transform(c,[1,0,0,1,0,-s]);const u=ut.slice();Util.axialAlignedBoundingBox([0,0,i,s],c,u);const[p,g,f,m]=u,b=Math.round(f-p)||1,A=Math.round(m-g)||1,w=this.cachedCanvases.getCanvas("fillCanvas",b,A),y=w.context,v=p,x=g;y.translate(-v,-x);y.transform(...c);if(!h){h=this._scaleImage(d.canvas,getCurrentTransformInverse(y));h=h.img;o&&a&&o.set(l,h)}y.imageSmoothingEnabled=getImageSmoothingEnabled(getCurrentTransform(y),t.interpolate);drawImageAtIntegerCoords(y,h,0,0,h.width,h.height,0,0,i,s);y.globalCompositeOperation="source-in";const E=Util.transform(getCurrentTransformInverse(y),[1,0,0,1,-v,-x]);y.fillStyle=a?n.getPattern(e,this,E,nt):n;y.fillRect(0,0,i,s);if(o&&!a){this.cachedCanvases.delete("fillCanvas");o.set(l,w.canvas)}return{canvas:w.canvas,offsetX:Math.round(v),offsetY:Math.round(x)}}setLineWidth(t){t!==this.current.lineWidth&&(this._cachedScaleForStroking[0]=-1);this.current.lineWidth=t;this.ctx.lineWidth=t}setLineCap(t){this.ctx.lineCap=pt[t]}setLineJoin(t){this.ctx.lineJoin=gt[t]}setMiterLimit(t){this.ctx.miterLimit=t}setDash(t,e){const i=this.ctx;if(void 0!==i.setLineDash){i.setLineDash(t);i.lineDashOffset=e}}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,i]of t)switch(e){case"LW":this.setLineWidth(i);break;case"LC":this.setLineCap(i);break;case"LJ":this.setLineJoin(i);break;case"ML":this.setMiterLimit(i);break;case"D":this.setDash(i[0],i[1]);break;case"RI":this.setRenderingIntent(i);break;case"FL":this.setFlatness(i);break;case"Font":this.setFont(i[0],i[1]);break;case"CA":this.current.strokeAlpha=i;break;case"ca":this.ctx.globalAlpha=this.current.fillAlpha=i;break;case"BM":this.ctx.globalCompositeOperation=i;break;case"SMask":this.current.activeSMask=i?this.tempSMask:null;this.tempSMask=null;this.checkSMaskState();break;case"TR":this.ctx.filter=this.current.transferMaps=this.filterFactory.addFilter(i)}}get inSMaskMode(){return!!this.suspendedCtx}checkSMaskState(){const t=this.inSMaskMode;this.current.activeSMask&&!t?this.beginSMaskMode():!this.current.activeSMask&&t&&this.endSMaskMode()}beginSMaskMode(){if(this.inSMaskMode)throw new Error("beginSMaskMode called while already in smask mode");const t=this.ctx.canvas.width,e=this.ctx.canvas.height,i="smaskGroupAt"+this.groupLevel,s=this.cachedCanvases.getCanvas(i,t,e);this.suspendedCtx=this.ctx;const n=this.ctx=s.context;n.setTransform(this.suspendedCtx.getTransform());copyCtxState(this.suspendedCtx,n);!function mirrorContextOperations(t,e){if(t._removeMirroring)throw new Error("Context is already forwarding operations.");t.__originalSave=t.save;t.__originalRestore=t.restore;t.__originalRotate=t.rotate;t.__originalScale=t.scale;t.__originalTranslate=t.translate;t.__originalTransform=t.transform;t.__originalSetTransform=t.setTransform;t.__originalResetTransform=t.resetTransform;t.__originalClip=t.clip;t.__originalMoveTo=t.moveTo;t.__originalLineTo=t.lineTo;t.__originalBezierCurveTo=t.bezierCurveTo;t.__originalRect=t.rect;t.__originalClosePath=t.closePath;t.__originalBeginPath=t.beginPath;t._removeMirroring=()=>{t.save=t.__originalSave;t.restore=t.__originalRestore;t.rotate=t.__originalRotate;t.scale=t.__originalScale;t.translate=t.__originalTranslate;t.transform=t.__originalTransform;t.setTransform=t.__originalSetTransform;t.resetTransform=t.__originalResetTransform;t.clip=t.__originalClip;t.moveTo=t.__originalMoveTo;t.lineTo=t.__originalLineTo;t.bezierCurveTo=t.__originalBezierCurveTo;t.rect=t.__originalRect;t.closePath=t.__originalClosePath;t.beginPath=t.__originalBeginPath;delete t._removeMirroring};t.save=function(){e.save();this.__originalSave()};t.restore=function(){e.restore();this.__originalRestore()};t.translate=function(t,i){e.translate(t,i);this.__originalTranslate(t,i)};t.scale=function(t,i){e.scale(t,i);this.__originalScale(t,i)};t.transform=function(t,i,s,n,a,r){e.transform(t,i,s,n,a,r);this.__originalTransform(t,i,s,n,a,r)};t.setTransform=function(t,i,s,n,a,r){e.setTransform(t,i,s,n,a,r);this.__originalSetTransform(t,i,s,n,a,r)};t.resetTransform=function(){e.resetTransform();this.__originalResetTransform()};t.rotate=function(t){e.rotate(t);this.__originalRotate(t)};t.clip=function(t){e.clip(t);this.__originalClip(t)};t.moveTo=function(t,i){e.moveTo(t,i);this.__originalMoveTo(t,i)};t.lineTo=function(t,i){e.lineTo(t,i);this.__originalLineTo(t,i)};t.bezierCurveTo=function(t,i,s,n,a,r){e.bezierCurveTo(t,i,s,n,a,r);this.__originalBezierCurveTo(t,i,s,n,a,r)};t.rect=function(t,i,s,n){e.rect(t,i,s,n);this.__originalRect(t,i,s,n)};t.closePath=function(){e.closePath();this.__originalClosePath()};t.beginPath=function(){e.beginPath();this.__originalBeginPath()}}(n,this.suspendedCtx);this.setGState([["BM","source-over"]])}endSMaskMode(){if(!this.inSMaskMode)throw new Error("endSMaskMode called while not in smask mode");this.ctx._removeMirroring();copyCtxState(this.ctx,this.suspendedCtx);this.ctx=this.suspendedCtx;this.suspendedCtx=null}compose(t){if(!this.current.activeSMask)return;if(t){t[0]=Math.floor(t[0]);t[1]=Math.floor(t[1]);t[2]=Math.ceil(t[2]);t[3]=Math.ceil(t[3])}else t=[0,0,this.ctx.canvas.width,this.ctx.canvas.height];const e=this.current.activeSMask,i=this.suspendedCtx;this.composeSMask(i,e,this.ctx,t);this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);this.ctx.restore()}composeSMask(t,e,i,s){const n=s[0],a=s[1],r=s[2]-n,o=s[3]-a;if(0!==r&&0!==o){this.genericComposeSMask(e.context,i,r,o,e.subtype,e.backdrop,e.transferMap,n,a,e.offsetX,e.offsetY);t.save();t.globalAlpha=1;t.globalCompositeOperation="source-over";t.setTransform(1,0,0,1,0,0);t.drawImage(i.canvas,0,0);t.restore()}}genericComposeSMask(t,e,i,s,n,a,r,o,l,h,d){let c=t.canvas,u=o-h,p=l-d;if(a)if(u<0||p<0||u+i>c.width||p+s>c.height){const t=this.cachedCanvases.getCanvas("maskExtension",i,s),e=t.context;e.drawImage(c,-u,-p);e.globalCompositeOperation="destination-atop";e.fillStyle=a;e.fillRect(0,0,i,s);e.globalCompositeOperation="source-over";c=t.canvas;u=p=0}else{t.save();t.globalAlpha=1;t.setTransform(1,0,0,1,0,0);const e=new Path2D;e.rect(u,p,i,s);t.clip(e);t.globalCompositeOperation="destination-atop";t.fillStyle=a;t.fillRect(u,p,i,s);t.restore()}e.save();e.globalAlpha=1;e.setTransform(1,0,0,1,0,0);"Alpha"===n&&r?e.filter=this.filterFactory.addAlphaFilter(r):"Luminosity"===n&&(e.filter=this.filterFactory.addLuminosityFilter(r));const g=new Path2D;g.rect(o,l,i,s);e.clip(g);e.globalCompositeOperation="destination-in";e.drawImage(c,u,p,i,s,o,l,i,s);e.restore()}save(){this.inSMaskMode&©CtxState(this.ctx,this.suspendedCtx);this.ctx.save();const t=this.current;this.stateStack.push(t);this.current=t.clone()}restore(){if(0!==this.stateStack.length){this.current=this.stateStack.pop();this.ctx.restore();this.inSMaskMode&©CtxState(this.suspendedCtx,this.ctx);this.checkSMaskState();this.pendingClip=null;this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}else this.inSMaskMode&&this.endSMaskMode()}transform(t,e,i,s,n,a){this.ctx.transform(t,e,i,s,n,a);this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}constructPath(t,e,i){let[s]=e;if(i){if(!(s instanceof Path2D)){const t=e[0]=new Path2D;for(let e=0,i=s.length;e100&&(h=100);this.current.fontSizeScale=i/h;this.ctx.font=`${l} ${o} ${h}px ${r}`}setTextRenderingMode(t){this.current.textRenderingMode=t}setTextRise(t){this.current.textRise=t}moveText(t,e){this.current.x=this.current.lineX+=t;this.current.y=this.current.lineY+=e}setLeadingMoveText(t,e){this.setLeading(-e);this.moveText(t,e)}setTextMatrix(t){const{current:e}=this;e.textMatrix=t;e.textMatrixScale=Math.hypot(t[0],t[1]);e.x=e.lineX=0;e.y=e.lineY=0}nextLine(){this.moveText(0,this.current.leading)}#Fi(t,e,i){const s=new Path2D;s.addPath(t,new DOMMatrix(i).invertSelf().multiplySelf(e));return s}paintChar(t,e,i,s,n){const a=this.ctx,r=this.current,o=r.font,l=r.textRenderingMode,h=r.fontSize/r.fontSizeScale,d=l&w,c=!!(l&y),u=r.patternFill&&!o.missingFile,p=r.patternStroke&&!o.missingFile;let g;(o.disableFontFace||c||u||p)&&(g=o.getPathGenerator(this.commonObjs,t));if(o.disableFontFace||u||p){a.save();a.translate(e,i);a.scale(h,-h);let t;if(d===f||d===b)if(s){t=a.getTransform();a.setTransform(...s);a.fill(this.#Fi(g,t,s))}else a.fill(g);if(d===m||d===b)if(n){t||=a.getTransform();a.setTransform(...n);const{a:e,b:i,c:s,d:r}=t,o=Util.inverseTransform(n),l=Util.transform([e,i,s,r,0,0],o);Util.singularValueDecompose2dScale(l,ct);a.lineWidth*=Math.max(ct[0],ct[1])/h;a.stroke(this.#Fi(g,t,n))}else{a.lineWidth/=h;a.stroke(g)}a.restore()}else{d!==f&&d!==b||a.fillText(t,e,i);d!==m&&d!==b||a.strokeText(t,e,i)}if(c){(this.pendingTextPaths||=[]).push({transform:getCurrentTransform(a),x:e,y:i,fontSize:h,path:g})}}get isFontSubpixelAAEnabled(){const{context:t}=this.cachedCanvases.getCanvas("isFontSubpixelAAEnabled",10,10);t.scale(1.5,1);t.fillText("I",0,10);const e=t.getImageData(0,0,10,10).data;let i=!1;for(let t=3;t0&&e[t]<255){i=!0;break}return shadow(this,"isFontSubpixelAAEnabled",i)}showText(t){const e=this.current,i=e.font;if(i.isType3Font)return this.showType3Text(t);const s=e.fontSize;if(0===s)return;const n=this.ctx,a=e.fontSizeScale,r=e.charSpacing,o=e.wordSpacing,l=e.fontDirection,h=e.textHScale*l,d=t.length,c=i.vertical,u=c?1:-1,p=i.defaultVMetrics,g=s*e.fontMatrix[0],A=e.textRenderingMode===f&&!i.disableFontFace&&!e.patternFill;n.save();e.textMatrix&&n.transform(...e.textMatrix);n.translate(e.x,e.y+e.textRise);l>0?n.scale(h,-1):n.scale(h,1);let y,v;if(e.patternFill){n.save();const t=e.fillColor.getPattern(n,this,getCurrentTransformInverse(n),nt);y=getCurrentTransform(n);n.restore();n.fillStyle=t}if(e.patternStroke){n.save();const t=e.strokeColor.getPattern(n,this,getCurrentTransformInverse(n),at);v=getCurrentTransform(n);n.restore();n.strokeStyle=t}let x=e.lineWidth;const E=e.textMatrixScale;if(0===E||0===x){const t=e.textRenderingMode&w;t!==m&&t!==b||(x=this.getSinglePixelWidth())}else x/=E;if(1!==a){n.scale(a,a);x/=a}n.lineWidth=x;if(i.isInvalidPDFjsFont){const i=[];let s=0;for(const e of t){i.push(e.unicode);s+=e.width}n.fillText(i.join(""),0,0);e.x+=s*g*h;n.restore();this.compose();return}let _,S=0;for(_=0;_0){const t=1e3*n.measureText(f).width/s*a;if(xnew CanvasGraphics(t,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:this.optionalContentConfig,markedContentStack:this.markedContentStack})};e=new TilingPattern(t,this.ctx,s,i)}else e=this._getPattern(t[1],t[2]);return e}setStrokeColorN(){this.current.strokeColor=this.getColorN_Pattern(arguments);this.current.patternStroke=!0}setFillColorN(){this.current.fillColor=this.getColorN_Pattern(arguments);this.current.patternFill=!0}setStrokeRGBColor(t){this.ctx.strokeStyle=this.current.strokeColor=t;this.current.patternStroke=!1}setStrokeTransparent(){this.ctx.strokeStyle=this.current.strokeColor="transparent";this.current.patternStroke=!1}setFillRGBColor(t){this.ctx.fillStyle=this.current.fillColor=t;this.current.patternFill=!1}setFillTransparent(){this.ctx.fillStyle=this.current.fillColor="transparent";this.current.patternFill=!1}_getPattern(t,e=null){let i;if(this.cachedPatterns.has(t))i=this.cachedPatterns.get(t);else{i=function getShadingPattern(t){switch(t[0]){case"RadialAxial":return new RadialAxialShadingPattern(t);case"Mesh":return new MeshShadingPattern(t);case"Dummy":return new DummyShadingPattern}throw new Error(`Unknown IR type: ${t[0]}`)}(this.getObject(t));this.cachedPatterns.set(t,i)}e&&(i.matrix=e);return i}shadingFill(t){if(!this.contentVisible)return;const e=this.ctx;this.save();const i=this._getPattern(t);e.fillStyle=i.getPattern(e,this,getCurrentTransformInverse(e),rt);const s=getCurrentTransformInverse(e);if(s){const{width:t,height:i}=e.canvas,n=ut.slice();Util.axialAlignedBoundingBox([0,0,t,i],s,n);const[a,r,o,l]=n;this.ctx.fillRect(a,r,o-a,l-r)}else this.ctx.fillRect(-1e10,-1e10,2e10,2e10);this.compose(this.current.getClippedPathBoundingBox());this.restore()}beginInlineImage(){unreachable("Should not call beginInlineImage")}beginImageData(){unreachable("Should not call beginImageData")}paintFormXObjectBegin(t,e){if(this.contentVisible){this.save();this.baseTransformStack.push(this.baseTransform);t&&this.transform(...t);this.baseTransform=getCurrentTransform(this.ctx);if(e){Util.axialAlignedBoundingBox(e,this.baseTransform,this.current.minMax);const[t,i,s,n]=e,a=new Path2D;a.rect(t,i,s-t,n-i);this.ctx.clip(a);this.endPath()}}}paintFormXObjectEnd(){if(this.contentVisible){this.restore();this.baseTransform=this.baseTransformStack.pop()}}beginGroup(t){if(!this.contentVisible)return;this.save();if(this.inSMaskMode){this.endSMaskMode();this.current.activeSMask=null}const e=this.ctx;t.isolated||info("TODO: Support non-isolated groups.");t.knockout&&warn("Knockout groups not supported.");const i=getCurrentTransform(e);t.matrix&&e.transform(...t.matrix);if(!t.bbox)throw new Error("Bounding box is required.");let s=ut.slice();Util.axialAlignedBoundingBox(t.bbox,getCurrentTransform(e),s);const n=[0,0,e.canvas.width,e.canvas.height];s=Util.intersect(s,n)||[0,0,0,0];const a=Math.floor(s[0]),r=Math.floor(s[1]),o=Math.max(Math.ceil(s[2])-a,1),l=Math.max(Math.ceil(s[3])-r,1);this.current.startNewPathAndClipBox([0,0,o,l]);let h="groupAt"+this.groupLevel;t.smask&&(h+="_smask_"+this.smaskCounter++%2);const d=this.cachedCanvases.getCanvas(h,o,l),c=d.context;c.translate(-a,-r);c.transform(...i);let u=new Path2D;const[p,g,f,m]=t.bbox;u.rect(p,g,f-p,m-g);if(t.matrix){const e=new Path2D;e.addPath(u,new DOMMatrix(t.matrix));u=e}c.clip(u);if(t.smask)this.smaskStack.push({canvas:d.canvas,context:c,offsetX:a,offsetY:r,subtype:t.smask.subtype,backdrop:t.smask.backdrop,transferMap:t.smask.transferMap||null,startTransformInverse:null});else{e.setTransform(1,0,0,1,0,0);e.translate(a,r);e.save()}copyCtxState(e,c);this.ctx=c;this.setGState([["BM","source-over"],["ca",1],["CA",1]]);this.groupStack.push(e);this.groupLevel++}endGroup(t){if(!this.contentVisible)return;this.groupLevel--;const e=this.ctx,i=this.groupStack.pop();this.ctx=i;this.ctx.imageSmoothingEnabled=!1;if(t.smask){this.tempSMask=this.smaskStack.pop();this.restore()}else{this.ctx.restore();const t=getCurrentTransform(this.ctx);this.restore();this.ctx.save();this.ctx.setTransform(...t);const i=ut.slice();Util.axialAlignedBoundingBox([0,0,e.canvas.width,e.canvas.height],t,i);this.ctx.drawImage(e.canvas,0,0);this.ctx.restore();this.compose(i)}}beginAnnotation(t,e,i,s,n){this.#Ii();resetCtxToDefault(this.ctx);this.ctx.save();this.save();this.baseTransform&&this.ctx.setTransform(...this.baseTransform);if(e){const s=e[2]-e[0],a=e[3]-e[1];if(n&&this.annotationCanvasMap){(i=i.slice())[4]-=e[0];i[5]-=e[1];(e=e.slice())[0]=e[1]=0;e[2]=s;e[3]=a;Util.singularValueDecompose2dScale(getCurrentTransform(this.ctx),ct);const{viewportScale:n}=this,r=Math.ceil(s*this.outputScaleX*n),o=Math.ceil(a*this.outputScaleY*n);this.annotationCanvas=this.canvasFactory.create(r,o);const{canvas:l,context:h}=this.annotationCanvas;this.annotationCanvasMap.set(t,l);this.annotationCanvas.savedCtx=this.ctx;this.ctx=h;this.ctx.save();this.ctx.setTransform(ct[0],0,0,-ct[1],0,a*ct[1]);resetCtxToDefault(this.ctx)}else{resetCtxToDefault(this.ctx);this.endPath();const t=new Path2D;t.rect(e[0],e[1],s,a);this.ctx.clip(t)}}this.current=new CanvasExtraState(this.ctx.canvas.width,this.ctx.canvas.height);this.transform(...i);this.transform(...s)}endAnnotation(){if(this.annotationCanvas){this.ctx.restore();this.#Ri();this.ctx=this.annotationCanvas.savedCtx;delete this.annotationCanvas.savedCtx;delete this.annotationCanvas}}paintImageMaskXObject(t){if(!this.contentVisible)return;const e=t.count;(t=this.getObject(t.data,t)).count=e;const i=this.ctx,s=this._createMaskCanvas(t),n=s.canvas;i.save();i.setTransform(1,0,0,1,0,0);i.drawImage(n,s.offsetX,s.offsetY);i.restore();this.compose()}paintImageMaskXObjectRepeat(t,e,i=0,s=0,n,a){if(!this.contentVisible)return;t=this.getObject(t.data,t);const r=this.ctx;r.save();const o=getCurrentTransform(r);r.transform(e,i,s,n,0,0);const l=this._createMaskCanvas(t);r.setTransform(1,0,0,1,l.offsetX-o[4],l.offsetY-o[5]);for(let t=0,h=a.length;te?h/e:1;r=l>e?l/e:1}}this._cachedScaleForStroking[0]=a;this._cachedScaleForStroking[1]=r}return this._cachedScaleForStroking}rescaleAndStroke(t,e){const{ctx:i,current:{lineWidth:s}}=this,[n,a]=this.getScaleForStroking();if(n===a){i.lineWidth=(s||1)*n;i.stroke(t);return}const r=i.getLineDash();e&&i.save();i.scale(n,a);dt.a=1/n;dt.d=1/a;const o=new Path2D;o.addPath(t,dt);if(r.length>0){const t=Math.max(n,a);i.setLineDash(r.map((e=>e/t)));i.lineDashOffset/=t}i.lineWidth=s||1;i.stroke(o);e&&i.restore()}isContentVisible(){for(let t=this.markedContentStack.length-1;t>=0;t--)if(!this.markedContentStack[t].visible)return!1;return!0}}for(const t in D)void 0!==CanvasGraphics.prototype[t]&&(CanvasGraphics.prototype[D[t]]=CanvasGraphics.prototype[t]);class GlobalWorkerOptions{static#Li=null;static#Oi="";static get workerPort(){return this.#Li}static set workerPort(t){if(!("undefined"!=typeof Worker&&t instanceof Worker)&&null!==t)throw new Error("Invalid `workerPort` type.");this.#Li=t}static get workerSrc(){return this.#Oi}static set workerSrc(t){if("string"!=typeof t)throw new Error("Invalid `workerSrc` type.");this.#Oi=t}}class Metadata{#Ni;#Bi;constructor({parsedData:t,rawData:e}){this.#Ni=t;this.#Bi=e}getRaw(){return this.#Bi}get(t){return this.#Ni.get(t)??null}[Symbol.iterator](){return this.#Ni.entries()}}const bt=Symbol("INTERNAL");class OptionalContentGroup{#Hi=!1;#Ui=!1;#zi=!1;#Gi=!0;constructor(t,{name:e,intent:i,usage:s,rbGroups:r}){this.#Hi=!!(t&n);this.#Ui=!!(t&a);this.name=e;this.intent=i;this.usage=s;this.rbGroups=r}get visible(){if(this.#zi)return this.#Gi;if(!this.#Gi)return!1;const{print:t,view:e}=this.usage;return this.#Hi?"OFF"!==e?.viewState:!this.#Ui||"OFF"!==t?.printState}_setVisible(t,e,i=!1){t!==bt&&unreachable("Internal method `_setVisible` called.");this.#zi=i;this.#Gi=e}}class OptionalContentConfig{#$i=null;#Wi=new Map;#Vi=null;#ji=null;constructor(t,e=n){this.renderingIntent=e;this.name=null;this.creator=null;if(null!==t){this.name=t.name;this.creator=t.creator;this.#ji=t.order;for(const i of t.groups)this.#Wi.set(i.id,new OptionalContentGroup(e,i));if("OFF"===t.baseState)for(const t of this.#Wi.values())t._setVisible(bt,!1);for(const e of t.on)this.#Wi.get(e)._setVisible(bt,!0);for(const e of t.off)this.#Wi.get(e)._setVisible(bt,!1);this.#Vi=this.getHash()}}#qi(t){const e=t.length;if(e<2)return!0;const i=t[0];for(let s=1;s0){const t=n instanceof Uint8Array&&n.byteLength===n.buffer.byteLength?n.buffer:new Uint8Array(n).buffer;this._queuedChunks.push(t)}this._pdfDataRangeTransport=t;this._isStreamingSupported=!i;this._isRangeSupported=!e;this._contentLength=s;this._fullRequestReader=null;this._rangeReaders=[];t.addRangeListener(((t,e)=>{this._onReceiveData({begin:t,chunk:e})}));t.addProgressListener(((t,e)=>{this._onProgress({loaded:t,total:e})}));t.addProgressiveReadListener((t=>{this._onReceiveData({chunk:t})}));t.addProgressiveDoneListener((()=>{this._onProgressiveDone()}));t.transportReady()}_onReceiveData({begin:t,chunk:e}){const i=e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength?e.buffer:new Uint8Array(e).buffer;if(void 0===t)this._fullRequestReader?this._fullRequestReader._enqueue(i):this._queuedChunks.push(i);else{assert(this._rangeReaders.some((function(e){if(e._begin!==t)return!1;e._enqueue(i);return!0})),"_onReceiveData - no `PDFDataTransportStreamRangeReader` instance found.")}}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}_onProgress(t){void 0===t.total?this._rangeReaders[0]?.onProgress?.({loaded:t.loaded}):this._fullRequestReader?.onProgress?.({loaded:t.loaded,total:t.total})}_onProgressiveDone(){this._fullRequestReader?.progressiveDone();this._progressiveDone=!0}_removeRangeReader(t){const e=this._rangeReaders.indexOf(t);e>=0&&this._rangeReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFDataTransportStream.getFullReader can only be called once.");const t=this._queuedChunks;this._queuedChunks=null;return new PDFDataTransportStreamReader(this,t,this._progressiveDone,this._contentDispositionFilename)}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFDataTransportStreamRangeReader(this,t,e);this._pdfDataRangeTransport.requestDataRange(t,e);this._rangeReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeReaders.slice(0))e.cancel(t);this._pdfDataRangeTransport.abort()}}class PDFDataTransportStreamReader{constructor(t,e,i=!1,s=null){this._stream=t;this._done=i||!1;this._filename=isPdfFile(s)?s:null;this._queuedChunks=e||[];this._loaded=0;for(const t of this._queuedChunks)this._loaded+=t.byteLength;this._requests=[];this._headersReady=Promise.resolve();t._fullRequestReader=this;this.onProgress=null}_enqueue(t){if(!this._done){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._queuedChunks.push(t);this._loaded+=t.byteLength}}get headersReady(){return this._headersReady}get filename(){return this._filename}get isRangeSupported(){return this._stream._isRangeSupported}get isStreamingSupported(){return this._stream._isStreamingSupported}get contentLength(){return this._stream._contentLength}async read(){if(this._queuedChunks.length>0){return{value:this._queuedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}progressiveDone(){this._done||(this._done=!0)}}class PDFDataTransportStreamRangeReader{constructor(t,e,i){this._stream=t;this._begin=e;this._end=i;this._queuedChunk=null;this._requests=[];this._done=!1;this.onProgress=null}_enqueue(t){if(!this._done){if(0===this._requests.length)this._queuedChunk=t;else{this._requests.shift().resolve({value:t,done:!1});for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}this._done=!0;this._stream._removeRangeReader(this)}}get isStreamingSupported(){return!1}async read(){if(this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._stream._removeRangeReader(this)}}function createHeaders(t,e){const i=new Headers;if(!t||!e||"object"!=typeof e)return i;for(const t in e){const s=e[t];void 0!==s&&i.append(t,s)}return i}function getResponseOrigin(t){return URL.parse(t)?.origin??null}function validateRangeRequestCapabilities({responseHeaders:t,isHttp:e,rangeChunkSize:i,disableRange:s}){const n={allowRangeRequests:!1,suggestedLength:void 0},a=parseInt(t.get("Content-Length"),10);if(!Number.isInteger(a))return n;n.suggestedLength=a;if(a<=2*i)return n;if(s||!e)return n;if("bytes"!==t.get("Accept-Ranges"))return n;if("identity"!==(t.get("Content-Encoding")||"identity"))return n;n.allowRangeRequests=!0;return n}function extractFilenameFromHeader(t){const e=t.get("Content-Disposition");if(e){let t=function getFilenameFromContentDispositionHeader(t){let e=!0,i=toParamRegExp("filename\\*","i").exec(t);if(i){i=i[1];let t=rfc2616unquote(i);t=unescape(t);t=rfc5987decode(t);t=rfc2047decode(t);return fixupEncoding(t)}i=function rfc2231getparam(t){const e=[];let i;const s=toParamRegExp("filename\\*((?!0\\d)\\d+)(\\*?)","ig");for(;null!==(i=s.exec(t));){let[,t,s,n]=i;t=parseInt(t,10);if(t in e){if(0===t)break}else e[t]=[s,n]}const n=[];for(let t=0;t{t._responseOrigin=getResponseOrigin(e.url);if(!validateResponseStatus(e.status))throw createResponseError(e.status,s);this._reader=e.body.getReader();this._headersCapability.resolve();const i=e.headers,{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:i,isHttp:t.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});this._isRangeSupported=n;this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(i);!this._isStreamingSupported&&this._isRangeSupported&&this.cancel(new AbortException("Streaming is disabled."))})).catch(this._headersCapability.reject);this.onProgress=null}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._headersCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class PDFFetchStreamRangeReader{constructor(t,e,i){this._stream=t;this._reader=null;this._loaded=0;const s=t.source;this._withCredentials=s.withCredentials||!1;this._readCapability=Promise.withResolvers();this._isStreamingSupported=!s.disableStream;this._abortController=new AbortController;const n=new Headers(t.headers);n.append("Range",`bytes=${e}-${i-1}`);const a=s.url;fetch(a,createFetchOptions(n,this._withCredentials,this._abortController)).then((e=>{const i=getResponseOrigin(e.url);if(i!==t._responseOrigin)throw new Error(`Expected range response-origin "${i}" to match "${t._responseOrigin}".`);if(!validateResponseStatus(e.status))throw createResponseError(e.status,a);this._readCapability.resolve();this._reader=e.body.getReader()})).catch(this._readCapability.reject);this.onProgress=null}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class NetworkManager{_responseOrigin=null;constructor({url:t,httpHeaders:e,withCredentials:i}){this.url=t;this.isHttp=/^https?:/i.test(t);this.headers=createHeaders(this.isHttp,e);this.withCredentials=i||!1;this.currXhrId=0;this.pendingRequests=Object.create(null)}request(t){const e=new XMLHttpRequest,i=this.currXhrId++,s=this.pendingRequests[i]={xhr:e};e.open("GET",this.url);e.withCredentials=this.withCredentials;for(const[t,i]of this.headers)e.setRequestHeader(t,i);if(this.isHttp&&"begin"in t&&"end"in t){e.setRequestHeader("Range",`bytes=${t.begin}-${t.end-1}`);s.expectedStatus=206}else s.expectedStatus=200;e.responseType="arraybuffer";assert(t.onError,"Expected `onError` callback to be provided.");e.onerror=()=>{t.onError(e.status)};e.onreadystatechange=this.onStateChange.bind(this,i);e.onprogress=this.onProgress.bind(this,i);s.onHeadersReceived=t.onHeadersReceived;s.onDone=t.onDone;s.onError=t.onError;s.onProgress=t.onProgress;e.send(null);return i}onProgress(t,e){const i=this.pendingRequests[t];i&&i.onProgress?.(e)}onStateChange(t,e){const i=this.pendingRequests[t];if(!i)return;const s=i.xhr;if(s.readyState>=2&&i.onHeadersReceived){i.onHeadersReceived();delete i.onHeadersReceived}if(4!==s.readyState)return;if(!(t in this.pendingRequests))return;delete this.pendingRequests[t];if(0===s.status&&this.isHttp){i.onError(s.status);return}const n=s.status||200;if(!(200===n&&206===i.expectedStatus)&&n!==i.expectedStatus){i.onError(s.status);return}const a=function network_getArrayBuffer(t){const e=t.response;return"string"!=typeof e?e:stringToBytes(e).buffer}(s);if(206===n){const t=s.getResponseHeader("Content-Range"),e=/bytes (\d+)-(\d+)\/(\d+)/.exec(t);if(e)i.onDone({begin:parseInt(e[1],10),chunk:a});else{warn('Missing or invalid "Content-Range" header.');i.onError(0)}}else a?i.onDone({begin:0,chunk:a}):i.onError(s.status)}getRequestXhr(t){return this.pendingRequests[t].xhr}isPendingRequest(t){return t in this.pendingRequests}abortRequest(t){const e=this.pendingRequests[t].xhr;delete this.pendingRequests[t];e.abort()}}class PDFNetworkStream{constructor(t){this._source=t;this._manager=new NetworkManager(t);this._rangeChunkSize=t.rangeChunkSize;this._fullRequestReader=null;this._rangeRequestReaders=[]}_onRangeRequestReaderClosed(t){const e=this._rangeRequestReaders.indexOf(t);e>=0&&this._rangeRequestReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFNetworkStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNetworkStreamFullRequestReader(this._manager,this._source);return this._fullRequestReader}getRangeReader(t,e){const i=new PDFNetworkStreamRangeRequestReader(this._manager,t,e);i.onClosed=this._onRangeRequestReaderClosed.bind(this);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNetworkStreamFullRequestReader{constructor(t,e){this._manager=t;this._url=e.url;this._fullRequestId=t.request({onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._headersCapability=Promise.withResolvers();this._disableRange=e.disableRange||!1;this._contentLength=e.length;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!1;this._isRangeSupported=!1;this._cachedChunks=[];this._requests=[];this._done=!1;this._storedError=void 0;this._filename=null;this.onProgress=null}_onHeadersReceived(){const t=this._fullRequestId,e=this._manager.getRequestXhr(t);this._manager._responseOrigin=getResponseOrigin(e.responseURL);const i=e.getAllResponseHeaders(),s=new Headers(i?i.trimStart().replace(/[^\S ]+$/,"").split(/[\r\n]+/).map((t=>{const[e,...i]=t.split(": ");return[e,i.join(": ")]})):[]),{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:s,isHttp:this._manager.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});n&&(this._isRangeSupported=!0);this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(s);this._isRangeSupported&&this._manager.abortRequest(t);this._headersCapability.resolve()}_onDone(t){if(t)if(this._requests.length>0){this._requests.shift().resolve({value:t.chunk,done:!1})}else this._cachedChunks.push(t.chunk);this._done=!0;if(!(this._cachedChunks.length>0)){for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}}_onError(t){this._storedError=createResponseError(t,this._url);this._headersCapability.reject(this._storedError);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._cachedChunks.length=0}_onProgress(t){this.onProgress?.({loaded:t.loaded,total:t.lengthComputable?t.total:this._contentLength})}get filename(){return this._filename}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}get contentLength(){return this._contentLength}get headersReady(){return this._headersCapability.promise}async read(){await this._headersCapability.promise;if(this._storedError)throw this._storedError;if(this._cachedChunks.length>0){return{value:this._cachedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this._headersCapability.reject(t);for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._fullRequestId)&&this._manager.abortRequest(this._fullRequestId);this._fullRequestReader=null}}class PDFNetworkStreamRangeRequestReader{constructor(t,e,i){this._manager=t;this._url=t.url;this._requestId=t.request({begin:e,end:i,onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._requests=[];this._queuedChunk=null;this._done=!1;this._storedError=void 0;this.onProgress=null;this.onClosed=null}_onHeadersReceived(){const t=getResponseOrigin(this._manager.getRequestXhr(this._requestId)?.responseURL);if(t!==this._manager._responseOrigin){this._storedError=new Error(`Expected range response-origin "${t}" to match "${this._manager._responseOrigin}".`);this._onError(0)}}_close(){this.onClosed?.(this)}_onDone(t){const e=t.chunk;if(this._requests.length>0){this._requests.shift().resolve({value:e,done:!1})}else this._queuedChunk=e;this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._close()}_onError(t){this._storedError??=createResponseError(t,this._url);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._queuedChunk=null}_onProgress(t){this.isStreamingSupported||this.onProgress?.({loaded:t.loaded})}get isStreamingSupported(){return!1}async read(){if(this._storedError)throw this._storedError;if(null!==this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._requestId)&&this._manager.abortRequest(this._requestId);this._close()}}const At=/^[a-z][a-z0-9\-+.]+:/i;class PDFNodeStream{constructor(t){this.source=t;this.url=function parseUrlOrPath(t){if(At.test(t))return new URL(t);const e=process.getBuiltinModule("url");return new URL(e.pathToFileURL(t))}(t.url);assert("file:"===this.url.protocol,"PDFNodeStream only supports file:// URLs.");this._fullRequestReader=null;this._rangeRequestReaders=[]}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}getFullReader(){assert(!this._fullRequestReader,"PDFNodeStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNodeStreamFsFullReader(this);return this._fullRequestReader}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFNodeStreamFsRangeReader(this,t,e);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNodeStreamFsFullReader{constructor(t){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;const e=t.source;this._contentLength=e.length;this._loaded=0;this._filename=null;this._disableRange=e.disableRange||!1;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!e.disableStream;this._isRangeSupported=!e.disableRange;this._readableStream=null;this._readCapability=Promise.withResolvers();this._headersCapability=Promise.withResolvers();const i=process.getBuiltinModule("fs");i.promises.lstat(this._url).then((t=>{this._contentLength=t.size;this._setReadableStream(i.createReadStream(this._url));this._headersCapability.resolve()}),(t=>{"ENOENT"===t.code&&(t=createResponseError(0,this._url.href));this._storedError=t;this._headersCapability.reject(t)}))}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));!this._isStreamingSupported&&this._isRangeSupported&&this._error(new AbortException("streaming is disabled"));this._storedError&&this._readableStream.destroy(this._storedError)}}class PDFNodeStreamFsRangeReader{constructor(t,e,i){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;this._loaded=0;this._readableStream=null;this._readCapability=Promise.withResolvers();const s=t.source;this._isStreamingSupported=!s.disableStream;const n=process.getBuiltinModule("fs");this._setReadableStream(n.createReadStream(this._url,{start:e,end:i-1}))}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));this._storedError&&this._readableStream.destroy(this._storedError)}}const wt=Symbol("INITIAL_DATA");class PDFObjects{#Xi=Object.create(null);#Ki(t){return this.#Xi[t]||={...Promise.withResolvers(),data:wt}}get(t,e=null){if(e){const i=this.#Ki(t);i.promise.then((()=>e(i.data)));return null}const i=this.#Xi[t];if(!i||i.data===wt)throw new Error(`Requesting object that isn't resolved yet ${t}.`);return i.data}has(t){const e=this.#Xi[t];return!!e&&e.data!==wt}delete(t){const e=this.#Xi[t];if(!e||e.data===wt)return!1;delete this.#Xi[t];return!0}resolve(t,e=null){const i=this.#Ki(t);i.data=e;i.resolve()}clear(){for(const t in this.#Xi){const{data:e}=this.#Xi[t];e?.bitmap?.close()}this.#Xi=Object.create(null)}*[Symbol.iterator](){for(const t in this.#Xi){const{data:e}=this.#Xi[t];e!==wt&&(yield[t,e])}}}class TextLayer{#Yi=Promise.withResolvers();#mt=null;#Qi=!1;#Ji=!!globalThis.FontInspector?.enabled;#Zi=null;#ts=null;#es=0;#is=0;#ss=null;#ns=null;#as=0;#rs=0;#os=Object.create(null);#ls=[];#hs=null;#ds=[];#cs=new WeakMap;#us=null;static#ps=new Map;static#gs=new Map;static#fs=new WeakMap;static#ms=null;static#bs=new Set;constructor({textContentSource:t,container:e,viewport:i}){if(t instanceof ReadableStream)this.#hs=t;else{if("object"!=typeof t)throw new Error('No "textContentSource" parameter specified.');this.#hs=new ReadableStream({start(e){e.enqueue(t);e.close()}})}this.#mt=this.#ns=e;this.#rs=i.scale*OutputScale.pixelRatio;this.#as=i.rotation;this.#ts={div:null,properties:null,ctx:null};const{pageWidth:s,pageHeight:n,pageX:a,pageY:r}=i.rawDims;this.#us=[1,0,0,-1,-a,r+n];this.#is=s;this.#es=n;TextLayer.#As();setLayerDimensions(e,i);this.#Yi.promise.finally((()=>{TextLayer.#bs.delete(this);this.#ts=null;this.#os=null})).catch((()=>{}))}static get fontFamilyMap(){const{isWindows:t,isFirefox:e}=util_FeatureTest.platform;return shadow(this,"fontFamilyMap",new Map([["sans-serif",(t&&e?"Calibri, ":"")+"sans-serif"],["monospace",(t&&e?"Lucida Console, ":"")+"monospace"]]))}render(){const pump=()=>{this.#ss.read().then((({value:t,done:e})=>{if(e)this.#Yi.resolve();else{this.#Zi??=t.lang;Object.assign(this.#os,t.styles);this.#ws(t.items);pump()}}),this.#Yi.reject)};this.#ss=this.#hs.getReader();TextLayer.#bs.add(this);pump();return this.#Yi.promise}update({viewport:t,onBefore:e=null}){const i=t.scale*OutputScale.pixelRatio,s=t.rotation;if(s!==this.#as){e?.();this.#as=s;setLayerDimensions(this.#ns,{rotation:s})}if(i!==this.#rs){e?.();this.#rs=i;const t={div:null,properties:null,ctx:TextLayer.#ys(this.#Zi)};for(const e of this.#ds){t.properties=this.#cs.get(e);t.div=e;this.#vs(t)}}}cancel(){const t=new AbortException("TextLayer task cancelled.");this.#ss?.cancel(t).catch((()=>{}));this.#ss=null;this.#Yi.reject(t)}get textDivs(){return this.#ds}get textContentItemsStr(){return this.#ls}#ws(t){if(this.#Qi)return;this.#ts.ctx??=TextLayer.#ys(this.#Zi);const e=this.#ds,i=this.#ls;for(const s of t){if(e.length>1e5){warn("Ignoring additional textDivs for performance reasons.");this.#Qi=!0;return}if(void 0!==s.str){i.push(s.str);this.#xs(s)}else if("beginMarkedContentProps"===s.type||"beginMarkedContent"===s.type){const t=this.#mt;this.#mt=document.createElement("span");this.#mt.classList.add("markedContent");null!==s.id&&this.#mt.setAttribute("id",`${s.id}`);t.append(this.#mt)}else"endMarkedContent"===s.type&&(this.#mt=this.#mt.parentNode)}}#xs(t){const e=document.createElement("span"),i={angle:0,canvasWidth:0,hasText:""!==t.str,hasEOL:t.hasEOL,fontSize:0};this.#ds.push(e);const s=Util.transform(this.#us,t.transform);let n=Math.atan2(s[1],s[0]);const a=this.#os[t.fontName];a.vertical&&(n+=Math.PI/2);let r=this.#Ji&&a.fontSubstitution||a.fontFamily;r=TextLayer.fontFamilyMap.get(r)||r;const o=Math.hypot(s[2],s[3]),l=o*TextLayer.#Es(r,a,this.#Zi);let h,d;if(0===n){h=s[4];d=s[5]-l}else{h=s[4]+l*Math.sin(n);d=s[5]-l*Math.cos(n)}const c="calc(var(--total-scale-factor) *",u=e.style;if(this.#mt===this.#ns){u.left=`${(100*h/this.#is).toFixed(2)}%`;u.top=`${(100*d/this.#es).toFixed(2)}%`}else{u.left=`${c}${h.toFixed(2)}px)`;u.top=`${c}${d.toFixed(2)}px)`}u.fontSize=`${c}${(TextLayer.#ms*o).toFixed(2)}px)`;u.fontFamily=r;i.fontSize=o;e.setAttribute("role","presentation");e.textContent=t.str;e.dir=t.dir;this.#Ji&&(e.dataset.fontName=a.fontSubstitutionLoadedName||t.fontName);0!==n&&(i.angle=n*(180/Math.PI));let p=!1;if(t.str.length>1)p=!0;else if(" "!==t.str&&t.transform[0]!==t.transform[3]){const e=Math.abs(t.transform[0]),i=Math.abs(t.transform[3]);e!==i&&Math.max(e,i)/Math.min(e,i)>1.5&&(p=!0)}p&&(i.canvasWidth=a.vertical?t.height:t.width);this.#cs.set(e,i);this.#ts.div=e;this.#ts.properties=i;this.#vs(this.#ts);i.hasText&&this.#mt.append(e);if(i.hasEOL){const t=document.createElement("br");t.setAttribute("role","presentation");this.#mt.append(t)}}#vs(t){const{div:e,properties:i,ctx:s}=t,{style:n}=e;let a="";TextLayer.#ms>1&&(a=`scale(${1/TextLayer.#ms})`);if(0!==i.canvasWidth&&i.hasText){const{fontFamily:t}=n,{canvasWidth:r,fontSize:o}=i;TextLayer.#_s(s,o*this.#rs,t);const{width:l}=s.measureText(e.textContent);l>0&&(a=`scaleX(${r*this.#rs/l}) ${a}`)}0!==i.angle&&(a=`rotate(${i.angle}deg) ${a}`);a.length>0&&(n.transform=a)}static cleanup(){if(!(this.#bs.size>0)){this.#ps.clear();for(const{canvas:t}of this.#gs.values())t.remove();this.#gs.clear()}}static#ys(t=null){let e=this.#gs.get(t||="");if(!e){const i=document.createElement("canvas");i.className="hiddenCanvasElement";i.lang=t;document.body.append(i);e=i.getContext("2d",{alpha:!1,willReadFrequently:!0});this.#gs.set(t,e);this.#fs.set(e,{size:0,family:""})}return e}static#_s(t,e,i){const s=this.#fs.get(t);if(e!==s.size||i!==s.family){t.font=`${e}px ${i}`;s.size=e;s.family=i}}static#As(){if(null!==this.#ms)return;const t=document.createElement("div");t.style.opacity=0;t.style.lineHeight=1;t.style.fontSize="1px";t.style.position="absolute";t.textContent="X";document.body.append(t);this.#ms=t.getBoundingClientRect().height;t.remove()}static#Es(t,e,i){const s=this.#ps.get(t);if(s)return s;const n=this.#ys(i);n.canvas.width=n.canvas.height=30;this.#_s(n,30,t);const a=n.measureText(""),r=a.fontBoundingBoxAscent,o=Math.abs(a.fontBoundingBoxDescent);n.canvas.width=n.canvas.height=0;let l=.8;if(r)l=r/(r+o);else{util_FeatureTest.platform.isFirefox&&warn("Enable the `dom.textMetrics.fontBoundingBox.enabled` preference in `about:config` to improve TextLayer rendering.");e.ascent?l=e.ascent:e.descent&&(l=1+e.descent)}this.#ps.set(t,l);return l}}class XfaText{static textContent(t){const e=[],i={items:e,styles:Object.create(null)};!function walk(t){if(!t)return;let i=null;const s=t.name;if("#text"===s)i=t.value;else{if(!XfaText.shouldBuildText(s))return;t?.attributes?.textContent?i=t.attributes.textContent:t.value&&(i=t.value)}null!==i&&e.push({str:i});if(t.children)for(const e of t.children)walk(e)}(t);return i}static shouldBuildText(t){return!("textarea"===t||"input"===t||"option"===t||"select"===t)}}function getDocument(e={}){"string"==typeof e||e instanceof URL?e={url:e}:(e instanceof ArrayBuffer||ArrayBuffer.isView(e))&&(e={data:e});const i=new PDFDocumentLoadingTask,{docId:s}=i,n=e.url?function getUrlProp(e){if(e instanceof URL)return e.href;if("string"==typeof e){if(t)return e;const i=URL.parse(e,window.location);if(i)return i.href}throw new Error("Invalid PDF url data: either string or URL-object is expected in the url property.")}(e.url):null,a=e.data?function getDataProp(e){if(t&&"undefined"!=typeof Buffer&&e instanceof Buffer)throw new Error("Please provide binary data as `Uint8Array`, rather than `Buffer`.");if(e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength)return e;if("string"==typeof e)return stringToBytes(e);if(e instanceof ArrayBuffer||ArrayBuffer.isView(e)||"object"==typeof e&&!isNaN(e?.length))return new Uint8Array(e);throw new Error("Invalid PDF binary data: either TypedArray, string, or array-like object is expected in the data property.")}(e.data):null,r=e.httpHeaders||null,o=!0===e.withCredentials,l=e.password??null,h=e.range instanceof PDFDataRangeTransport?e.range:null,d=Number.isInteger(e.rangeChunkSize)&&e.rangeChunkSize>0?e.rangeChunkSize:65536;let c=e.worker instanceof PDFWorker?e.worker:null;const u=e.verbosity,p="string"!=typeof e.docBaseUrl||isDataScheme(e.docBaseUrl)?null:e.docBaseUrl,g=getFactoryUrlProp(e.cMapUrl),f=!1!==e.cMapPacked,m=e.CMapReaderFactory||(t?NodeCMapReaderFactory:DOMCMapReaderFactory),b=getFactoryUrlProp(e.iccUrl),A=getFactoryUrlProp(e.standardFontDataUrl),w=e.StandardFontDataFactory||(t?NodeStandardFontDataFactory:DOMStandardFontDataFactory),y=getFactoryUrlProp(e.wasmUrl),v=e.WasmFactory||(t?NodeWasmFactory:DOMWasmFactory),x=!0!==e.stopAtErrors,E=Number.isInteger(e.maxImageSize)&&e.maxImageSize>-1?e.maxImageSize:-1,_=!1!==e.isEvalSupported,S="boolean"==typeof e.isOffscreenCanvasSupported?e.isOffscreenCanvasSupported:!t,C="boolean"==typeof e.isImageDecoderSupported?e.isImageDecoderSupported:!t&&(util_FeatureTest.platform.isFirefox||!globalThis.chrome),T=Number.isInteger(e.canvasMaxAreaInBytes)?e.canvasMaxAreaInBytes:-1,M="boolean"==typeof e.disableFontFace?e.disableFontFace:t,D=!0===e.fontExtraProperties,P=!0===e.enableXfa,k=e.ownerDocument||globalThis.document,I=!0===e.disableRange,R=!0===e.disableStream,F=!0===e.disableAutoFetch,L=!0===e.pdfBug,O=e.CanvasFactory||(t?NodeCanvasFactory:DOMCanvasFactory),N=e.FilterFactory||(t?NodeFilterFactory:DOMFilterFactory),B=!0===e.enableHWA,H=!1!==e.useWasm,U=h?h.length:e.length??NaN,z="boolean"==typeof e.useSystemFonts?e.useSystemFonts:!t&&!M,G="boolean"==typeof e.useWorkerFetch?e.useWorkerFetch:!!(m===DOMCMapReaderFactory&&w===DOMStandardFontDataFactory&&v===DOMWasmFactory&&g&&A&&y&&isValidFetchUrl(g,document.baseURI)&&isValidFetchUrl(A,document.baseURI)&&isValidFetchUrl(y,document.baseURI));setVerbosityLevel(u);const $={canvasFactory:new O({ownerDocument:k,enableHWA:B}),filterFactory:new N({docId:s,ownerDocument:k}),cMapReaderFactory:G?null:new m({baseUrl:g,isCompressed:f}),standardFontDataFactory:G?null:new w({baseUrl:A}),wasmFactory:G?null:new v({baseUrl:y})};if(!c){c=PDFWorker.create({verbosity:u,port:GlobalWorkerOptions.workerPort});i._worker=c}const W={docId:s,apiVersion:"5.3.31",data:a,password:l,disableAutoFetch:F,rangeChunkSize:d,length:U,docBaseUrl:p,enableXfa:P,evaluatorOptions:{maxImageSize:E,disableFontFace:M,ignoreErrors:x,isEvalSupported:_,isOffscreenCanvasSupported:S,isImageDecoderSupported:C,canvasMaxAreaInBytes:T,fontExtraProperties:D,useSystemFonts:z,useWasm:H,useWorkerFetch:G,cMapUrl:g,iccUrl:b,standardFontDataUrl:A,wasmUrl:y}},V={ownerDocument:k,pdfBug:L,styleElement:null,loadingParams:{disableAutoFetch:F,enableXfa:P}};c.promise.then((function(){if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=c.messageHandler.sendWithPromise("GetDocRequest",W,a?[a.buffer]:null);let l;if(h)l=new PDFDataTransportStream(h,{disableRange:I,disableStream:R});else if(!a){if(!n)throw new Error("getDocument - no `url` parameter provided.");const e=isValidFetchUrl(n)?PDFFetchStream:t?PDFNodeStream:PDFNetworkStream;l=new e({url:n,length:U,httpHeaders:r,withCredentials:o,rangeChunkSize:d,disableRange:I,disableStream:R})}return e.then((t=>{if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=new MessageHandler(s,t,c.port),n=new WorkerTransport(e,i,l,V,$);i._transport=n;e.send("Ready",null)}))})).catch(i._capability.reject);return i}class PDFDocumentLoadingTask{static#Ai=0;_capability=Promise.withResolvers();_transport=null;_worker=null;docId="d"+PDFDocumentLoadingTask.#Ai++;destroyed=!1;onPassword=null;onProgress=null;get promise(){return this._capability.promise}async destroy(){this.destroyed=!0;try{this._worker?.port&&(this._worker._pendingDestroy=!0);await(this._transport?.destroy())}catch(t){this._worker?.port&&delete this._worker._pendingDestroy;throw t}this._transport=null;this._worker?.destroy();this._worker=null}async getData(){return this._transport.getData()}}class PDFDataRangeTransport{#Yi=Promise.withResolvers();#Ss=[];#Cs=[];#Ts=[];#Ms=[];constructor(t,e,i=!1,s=null){this.length=t;this.initialData=e;this.progressiveDone=i;this.contentDispositionFilename=s}addRangeListener(t){this.#Ms.push(t)}addProgressListener(t){this.#Ts.push(t)}addProgressiveReadListener(t){this.#Cs.push(t)}addProgressiveDoneListener(t){this.#Ss.push(t)}onDataRange(t,e){for(const i of this.#Ms)i(t,e)}onDataProgress(t,e){this.#Yi.promise.then((()=>{for(const i of this.#Ts)i(t,e)}))}onDataProgressiveRead(t){this.#Yi.promise.then((()=>{for(const e of this.#Cs)e(t)}))}onDataProgressiveDone(){this.#Yi.promise.then((()=>{for(const t of this.#Ss)t()}))}transportReady(){this.#Yi.resolve()}requestDataRange(t,e){unreachable("Abstract method PDFDataRangeTransport.requestDataRange")}abort(){}}class PDFDocumentProxy{constructor(t,e){this._pdfInfo=t;this._transport=e}get annotationStorage(){return this._transport.annotationStorage}get canvasFactory(){return this._transport.canvasFactory}get filterFactory(){return this._transport.filterFactory}get numPages(){return this._pdfInfo.numPages}get fingerprints(){return this._pdfInfo.fingerprints}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}get allXfaHtml(){return this._transport._htmlForXfa}getPage(t){return this._transport.getPage(t)}getPageIndex(t){return this._transport.getPageIndex(t)}getDestinations(){return this._transport.getDestinations()}getDestination(t){return this._transport.getDestination(t)}getPageLabels(){return this._transport.getPageLabels()}getPageLayout(){return this._transport.getPageLayout()}getPageMode(){return this._transport.getPageMode()}getViewerPreferences(){return this._transport.getViewerPreferences()}getOpenAction(){return this._transport.getOpenAction()}getAttachments(){return this._transport.getAttachments()}getJSActions(){return this._transport.getDocJSActions()}getOutline(){return this._transport.getOutline()}getOptionalContentConfig({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getOptionalContentConfig(e)}getPermissions(){return this._transport.getPermissions()}getMetadata(){return this._transport.getMetadata()}getMarkInfo(){return this._transport.getMarkInfo()}getData(){return this._transport.getData()}saveDocument(){return this._transport.saveDocument()}getDownloadInfo(){return this._transport.downloadInfoCapability.promise}cleanup(t=!1){return this._transport.startCleanup(t||this.isPureXfa)}destroy(){return this.loadingTask.destroy()}cachedPageNumber(t){return this._transport.cachedPageNumber(t)}get loadingParams(){return this._transport.loadingParams}get loadingTask(){return this._transport.loadingTask}getFieldObjects(){return this._transport.getFieldObjects()}hasJSActions(){return this._transport.hasJSActions()}getCalculationOrderIds(){return this._transport.getCalculationOrderIds()}}class PDFPageProxy{#Ds=!1;constructor(t,e,i,s=!1){this._pageIndex=t;this._pageInfo=e;this._transport=i;this._stats=s?new StatTimer:null;this._pdfBug=s;this.commonObjs=i.commonObjs;this.objs=new PDFObjects;this._intentStates=new Map;this.destroyed=!1}get pageNumber(){return this._pageIndex+1}get rotate(){return this._pageInfo.rotate}get ref(){return this._pageInfo.ref}get userUnit(){return this._pageInfo.userUnit}get view(){return this._pageInfo.view}getViewport({scale:t,rotation:e=this.rotate,offsetX:i=0,offsetY:s=0,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.view,userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}getAnnotations({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getAnnotations(this._pageIndex,e)}getJSActions(){return this._transport.getPageJSActions(this._pageIndex)}get filterFactory(){return this._transport.filterFactory}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}async getXfa(){return this._transport._htmlForXfa?.children[this._pageIndex]||null}render({canvasContext:t,viewport:e,intent:i="display",annotationMode:s=c.ENABLE,transform:n=null,background:r=null,optionalContentConfigPromise:o=null,annotationCanvasMap:l=null,pageColors:h=null,printAnnotationStorage:d=null,isEditing:u=!1}){this._stats?.time("Overall");const p=this._transport.getRenderingIntent(i,s,d,u),{renderingIntent:g,cacheKey:f}=p;this.#Ds=!1;o||=this._transport.getOptionalContentConfig(g);let m=this._intentStates.get(f);if(!m){m=Object.create(null);this._intentStates.set(f,m)}if(m.streamReaderCancelTimeout){clearTimeout(m.streamReaderCancelTimeout);m.streamReaderCancelTimeout=null}const b=!!(g&a);if(!m.displayReadyCapability){m.displayReadyCapability=Promise.withResolvers();m.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(p)}const complete=t=>{m.renderTasks.delete(A);b&&(this.#Ds=!0);this.#Ps();if(t){A.capability.reject(t);this._abortOperatorList({intentState:m,reason:t instanceof Error?t:new Error(t)})}else A.capability.resolve();if(this._stats){this._stats.timeEnd("Rendering");this._stats.timeEnd("Overall");globalThis.Stats?.enabled&&globalThis.Stats.add(this.pageNumber,this._stats)}},A=new InternalRenderTask({callback:complete,params:{canvasContext:t,viewport:e,transform:n,background:r},objs:this.objs,commonObjs:this.commonObjs,annotationCanvasMap:l,operatorList:m.operatorList,pageIndex:this._pageIndex,canvasFactory:this._transport.canvasFactory,filterFactory:this._transport.filterFactory,useRequestAnimationFrame:!b,pdfBug:this._pdfBug,pageColors:h});(m.renderTasks||=new Set).add(A);const w=A.task;Promise.all([m.displayReadyCapability.promise,o]).then((([t,e])=>{if(this.destroyed)complete();else{this._stats?.time("Rendering");if(!(e.renderingIntent&g))throw new Error("Must use the same `intent`-argument when calling the `PDFPageProxy.render` and `PDFDocumentProxy.getOptionalContentConfig` methods.");A.initializeGraphics({transparency:t,optionalContentConfig:e});A.operatorListChanged()}})).catch(complete);return w}getOperatorList({intent:t="display",annotationMode:e=c.ENABLE,printAnnotationStorage:i=null,isEditing:s=!1}={}){const n=this._transport.getRenderingIntent(t,e,i,s,!0);let a,r=this._intentStates.get(n.cacheKey);if(!r){r=Object.create(null);this._intentStates.set(n.cacheKey,r)}if(!r.opListReadCapability){a=Object.create(null);a.operatorListChanged=function operatorListChanged(){if(r.operatorList.lastChunk){r.opListReadCapability.resolve(r.operatorList);r.renderTasks.delete(a)}};r.opListReadCapability=Promise.withResolvers();(r.renderTasks||=new Set).add(a);r.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(n)}return r.opListReadCapability.promise}streamTextContent({includeMarkedContent:t=!1,disableNormalization:e=!1}={}){return this._transport.messageHandler.sendWithStream("GetTextContent",{pageIndex:this._pageIndex,includeMarkedContent:!0===t,disableNormalization:!0===e},{highWaterMark:100,size:t=>t.items.length})}getTextContent(t={}){if(this._transport._htmlForXfa)return this.getXfa().then((t=>XfaText.textContent(t)));const e=this.streamTextContent(t);return new Promise((function(t,i){const s=e.getReader(),n={items:[],styles:Object.create(null),lang:null};!function pump(){s.read().then((function({value:e,done:i}){if(i)t(n);else{n.lang??=e.lang;Object.assign(n.styles,e.styles);n.items.push(...e.items);pump()}}),i)}()}))}getStructTree(){return this._transport.getStructTree(this._pageIndex)}_destroy(){this.destroyed=!0;const t=[];for(const e of this._intentStates.values()){this._abortOperatorList({intentState:e,reason:new Error("Page was destroyed."),force:!0});if(!e.opListReadCapability)for(const i of e.renderTasks){t.push(i.completed);i.cancel()}}this.objs.clear();this.#Ds=!1;return Promise.all(t)}cleanup(t=!1){this.#Ds=!0;const e=this.#Ps();t&&e&&(this._stats&&=new StatTimer);return e}#Ps(){if(!this.#Ds||this.destroyed)return!1;for(const{renderTasks:t,operatorList:e}of this._intentStates.values())if(t.size>0||!e.lastChunk)return!1;this._intentStates.clear();this.objs.clear();this.#Ds=!1;return!0}_startRenderPage(t,e){const i=this._intentStates.get(e);if(i){this._stats?.timeEnd("Page Request");i.displayReadyCapability?.resolve(t)}}_renderPageChunk(t,e){for(let i=0,s=t.length;i{r.read().then((({value:t,done:e})=>{if(e)o.streamReader=null;else if(!this._transport.destroyed){this._renderPageChunk(t,o);pump()}}),(t=>{o.streamReader=null;if(!this._transport.destroyed){if(o.operatorList){o.operatorList.lastChunk=!0;for(const t of o.renderTasks)t.operatorListChanged();this.#Ps()}if(o.displayReadyCapability)o.displayReadyCapability.reject(t);else{if(!o.opListReadCapability)throw t;o.opListReadCapability.reject(t)}}}))};pump()}_abortOperatorList({intentState:t,reason:e,force:i=!1}){if(t.streamReader){if(t.streamReaderCancelTimeout){clearTimeout(t.streamReaderCancelTimeout);t.streamReaderCancelTimeout=null}if(!i){if(t.renderTasks.size>0)return;if(e instanceof RenderingCancelledException){let i=100;e.extraDelay>0&&e.extraDelay<1e3&&(i+=e.extraDelay);t.streamReaderCancelTimeout=setTimeout((()=>{t.streamReaderCancelTimeout=null;this._abortOperatorList({intentState:t,reason:e,force:!0})}),i);return}}t.streamReader.cancel(new AbortException(e.message)).catch((()=>{}));t.streamReader=null;if(!this._transport.destroyed){for(const[e,i]of this._intentStates)if(i===t){this._intentStates.delete(e);break}this.cleanup()}}}get stats(){return this._stats}}class PDFWorker{#Yi=Promise.withResolvers();#ks=null;#Li=null;#Is=null;static#Rs=0;static#Fs=!1;static#Ls=new WeakMap;static{if(t){this.#Fs=!0;GlobalWorkerOptions.workerSrc||="./pdf.worker.mjs"}this._isSameOrigin=(t,e)=>{const i=URL.parse(t);if(!i?.origin||"null"===i.origin)return!1;const s=new URL(e,i);return i.origin===s.origin};this._createCDNWrapper=t=>{const e=`await import("${t}");`;return URL.createObjectURL(new Blob([e],{type:"text/javascript"}))};this.fromPort=t=>{!function deprecated(t){console.log("Deprecated API usage: "+t)}("`PDFWorker.fromPort` - please use `PDFWorker.create` instead.");if(!t?.port)throw new Error("PDFWorker.fromPort - invalid method signature.");return this.create(t)}}constructor({name:t=null,port:e=null,verbosity:i=getVerbosityLevel()}={}){this.name=t;this.destroyed=!1;this.verbosity=i;if(e){if(PDFWorker.#Ls.has(e))throw new Error("Cannot use more than one PDFWorker per port.");PDFWorker.#Ls.set(e,this);this.#Os(e)}else this.#Ns()}get promise(){return this.#Yi.promise}#Bs(){this.#Yi.resolve();this.#ks.send("configure",{verbosity:this.verbosity})}get port(){return this.#Li}get messageHandler(){return this.#ks}#Os(t){this.#Li=t;this.#ks=new MessageHandler("main","worker",t);this.#ks.on("ready",(()=>{}));this.#Bs()}#Ns(){if(PDFWorker.#Fs||PDFWorker.#Hs){this.#Us();return}let{workerSrc:t}=PDFWorker;try{PDFWorker._isSameOrigin(window.location,t)||(t=PDFWorker._createCDNWrapper(new URL(t,window.location).href));const e=new Worker(t,{type:"module"}),i=new MessageHandler("main","worker",e),terminateEarly=()=>{s.abort();i.destroy();e.terminate();this.destroyed?this.#Yi.reject(new Error("Worker was destroyed")):this.#Us()},s=new AbortController;e.addEventListener("error",(()=>{this.#Is||terminateEarly()}),{signal:s.signal});i.on("test",(t=>{s.abort();if(!this.destroyed&&t){this.#ks=i;this.#Li=e;this.#Is=e;this.#Bs()}else terminateEarly()}));i.on("ready",(t=>{s.abort();if(this.destroyed)terminateEarly();else try{sendTest()}catch{this.#Us()}}));const sendTest=()=>{const t=new Uint8Array;i.send("test",t,[t.buffer])};sendTest();return}catch{info("The worker has been disabled.")}this.#Us()}#Us(){if(!PDFWorker.#Fs){warn("Setting up fake worker.");PDFWorker.#Fs=!0}PDFWorker._setupFakeWorkerGlobal.then((t=>{if(this.destroyed){this.#Yi.reject(new Error("Worker was destroyed"));return}const e=new LoopbackPort;this.#Li=e;const i="fake"+PDFWorker.#Rs++,s=new MessageHandler(i+"_worker",i,e);t.setup(s,e);this.#ks=new MessageHandler(i,i+"_worker",e);this.#Bs()})).catch((t=>{this.#Yi.reject(new Error(`Setting up fake worker failed: "${t.message}".`))}))}destroy(){this.destroyed=!0;this.#Is?.terminate();this.#Is=null;PDFWorker.#Ls.delete(this.#Li);this.#Li=null;this.#ks?.destroy();this.#ks=null}static create(t){const e=this.#Ls.get(t?.port);if(e){if(e._pendingDestroy)throw new Error("PDFWorker.create - the worker is being destroyed.\nPlease remember to await `PDFDocumentLoadingTask.destroy()`-calls.");return e}return new PDFWorker(t)}static get workerSrc(){if(GlobalWorkerOptions.workerSrc)return GlobalWorkerOptions.workerSrc;throw new Error('No "GlobalWorkerOptions.workerSrc" specified.')}static get#Hs(){try{return globalThis.pdfjsWorker?.WorkerMessageHandler||null}catch{return null}}static get _setupFakeWorkerGlobal(){return shadow(this,"_setupFakeWorkerGlobal",(async()=>{if(this.#Hs)return this.#Hs;return(await import( +/*webpackIgnore: true*/ +/*@vite-ignore*/ +this.workerSrc)).WorkerMessageHandler})())}}class WorkerTransport{#zs=new Map;#Gs=new Map;#$s=new Map;#Ws=new Map;#Vs=null;constructor(t,e,i,s,n){this.messageHandler=t;this.loadingTask=e;this.commonObjs=new PDFObjects;this.fontLoader=new FontLoader({ownerDocument:s.ownerDocument,styleElement:s.styleElement});this.loadingParams=s.loadingParams;this._params=s;this.canvasFactory=n.canvasFactory;this.filterFactory=n.filterFactory;this.cMapReaderFactory=n.cMapReaderFactory;this.standardFontDataFactory=n.standardFontDataFactory;this.wasmFactory=n.wasmFactory;this.destroyed=!1;this.destroyCapability=null;this._networkStream=i;this._fullReader=null;this._lastProgress=null;this.downloadInfoCapability=Promise.withResolvers();this.setupMessageHandler()}#js(t,e=null){const i=this.#zs.get(t);if(i)return i;const s=this.messageHandler.sendWithPromise(t,e);this.#zs.set(t,s);return s}get annotationStorage(){return shadow(this,"annotationStorage",new AnnotationStorage)}getRenderingIntent(t,e=c.ENABLE,i=null,u=!1,p=!1){let g=n,f=j;switch(t){case"any":g=s;break;case"display":break;case"print":g=a;break;default:warn(`getRenderingIntent - invalid intent: ${t}`)}const m=g&a&&i instanceof PrintAnnotationStorage?i:this.annotationStorage;switch(e){case c.DISABLE:g+=l;break;case c.ENABLE:break;case c.ENABLE_FORMS:g+=r;break;case c.ENABLE_STORAGE:g+=o;f=m.serializable;break;default:warn(`getRenderingIntent - invalid annotationMode: ${e}`)}u&&(g+=h);p&&(g+=d);const{ids:b,hash:A}=m.modifiedIds;return{renderingIntent:g,cacheKey:[g,f.hash,A].join("_"),annotationStorageSerializable:f,modifiedIds:b}}destroy(){if(this.destroyCapability)return this.destroyCapability.promise;this.destroyed=!0;this.destroyCapability=Promise.withResolvers();this.#Vs?.reject(new Error("Worker was destroyed during onPassword callback"));const t=[];for(const e of this.#Gs.values())t.push(e._destroy());this.#Gs.clear();this.#$s.clear();this.#Ws.clear();this.hasOwnProperty("annotationStorage")&&this.annotationStorage.resetModified();const e=this.messageHandler.sendWithPromise("Terminate",null);t.push(e);Promise.all(t).then((()=>{this.commonObjs.clear();this.fontLoader.clear();this.#zs.clear();this.filterFactory.destroy();TextLayer.cleanup();this._networkStream?.cancelAllRequests(new AbortException("Worker was terminated."));this.messageHandler?.destroy();this.messageHandler=null;this.destroyCapability.resolve()}),this.destroyCapability.reject);return this.destroyCapability.promise}setupMessageHandler(){const{messageHandler:t,loadingTask:e}=this;t.on("GetReader",((t,e)=>{assert(this._networkStream,"GetReader - no `IPDFStream` instance available.");this._fullReader=this._networkStream.getFullReader();this._fullReader.onProgress=t=>{this._lastProgress={loaded:t.loaded,total:t.total}};e.onPull=()=>{this._fullReader.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{this._fullReader.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}));t.on("ReaderHeadersReady",(async t=>{await this._fullReader.headersReady;const{isStreamingSupported:i,isRangeSupported:s,contentLength:n}=this._fullReader;if(!i||!s){this._lastProgress&&e.onProgress?.(this._lastProgress);this._fullReader.onProgress=t=>{e.onProgress?.({loaded:t.loaded,total:t.total})}}return{isStreamingSupported:i,isRangeSupported:s,contentLength:n}}));t.on("GetRangeReader",((t,e)=>{assert(this._networkStream,"GetRangeReader - no `IPDFStream` instance available.");const i=this._networkStream.getRangeReader(t.begin,t.end);if(i){e.onPull=()=>{i.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetRangeReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{i.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}else e.close()}));t.on("GetDoc",(({pdfInfo:t})=>{this._numPages=t.numPages;this._htmlForXfa=t.htmlForXfa;delete t.htmlForXfa;e._capability.resolve(new PDFDocumentProxy(t,this))}));t.on("DocException",(t=>{e._capability.reject(wrapReason(t))}));t.on("PasswordRequest",(t=>{this.#Vs=Promise.withResolvers();try{if(!e.onPassword)throw wrapReason(t);const updatePassword=t=>{t instanceof Error?this.#Vs.reject(t):this.#Vs.resolve({password:t})};e.onPassword(updatePassword,t.code)}catch(t){this.#Vs.reject(t)}return this.#Vs.promise}));t.on("DataLoaded",(t=>{e.onProgress?.({loaded:t.length,total:t.length});this.downloadInfoCapability.resolve(t)}));t.on("StartRenderPage",(t=>{if(this.destroyed)return;this.#Gs.get(t.pageIndex)._startRenderPage(t.transparency,t.cacheKey)}));t.on("commonobj",(([e,i,s])=>{if(this.destroyed)return null;if(this.commonObjs.has(e))return null;switch(i){case"Font":if("error"in s){const t=s.error;warn(`Error during font loading: ${t}`);this.commonObjs.resolve(e,t);break}const n=this._params.pdfBug&&globalThis.FontInspector?.enabled?(t,e)=>globalThis.FontInspector.fontAdded(t,e):null,a=new FontFaceObject(s,n);this.fontLoader.bind(a).catch((()=>t.sendWithPromise("FontFallback",{id:e}))).finally((()=>{!a.fontExtraProperties&&a.data&&(a.data=null);this.commonObjs.resolve(e,a)}));break;case"CopyLocalImage":const{imageRef:r}=s;assert(r,"The imageRef must be defined.");for(const t of this.#Gs.values())for(const[,i]of t.objs)if(i?.ref===r){if(!i.dataLen)return null;this.commonObjs.resolve(e,structuredClone(i));return i.dataLen}break;case"FontPath":case"Image":case"Pattern":this.commonObjs.resolve(e,s);break;default:throw new Error(`Got unknown common object type ${i}`)}return null}));t.on("obj",(([t,e,i,s])=>{if(this.destroyed)return;const n=this.#Gs.get(e);if(!n.objs.has(t))if(0!==n._intentStates.size)switch(i){case"Image":case"Pattern":n.objs.resolve(t,s);break;default:throw new Error(`Got unknown object type ${i}`)}else s?.bitmap?.close()}));t.on("DocProgress",(t=>{this.destroyed||e.onProgress?.({loaded:t.loaded,total:t.total})}));t.on("FetchBinaryData",(async t=>{if(this.destroyed)throw new Error("Worker was destroyed.");const e=this[t.type];if(!e)throw new Error(`${t.type} not initialized, see the \`useWorkerFetch\` parameter.`);return e.fetch(t)}))}getData(){return this.messageHandler.sendWithPromise("GetData",null)}saveDocument(){this.annotationStorage.size<=0&&warn("saveDocument called while `annotationStorage` is empty, please use the getData-method instead.");const{map:t,transfer:e}=this.annotationStorage.serializable;return this.messageHandler.sendWithPromise("SaveDocument",{isPureXfa:!!this._htmlForXfa,numPages:this._numPages,annotationStorage:t,filename:this._fullReader?.filename??null},e).finally((()=>{this.annotationStorage.resetModified()}))}getPage(t){if(!Number.isInteger(t)||t<=0||t>this._numPages)return Promise.reject(new Error("Invalid page request."));const e=t-1,i=this.#$s.get(e);if(i)return i;const s=this.messageHandler.sendWithPromise("GetPage",{pageIndex:e}).then((i=>{if(this.destroyed)throw new Error("Transport destroyed");i.refStr&&this.#Ws.set(i.refStr,t);const s=new PDFPageProxy(e,i,this,this._params.pdfBug);this.#Gs.set(e,s);return s}));this.#$s.set(e,s);return s}getPageIndex(t){return isRefProxy(t)?this.messageHandler.sendWithPromise("GetPageIndex",{num:t.num,gen:t.gen}):Promise.reject(new Error("Invalid pageIndex request."))}getAnnotations(t,e){return this.messageHandler.sendWithPromise("GetAnnotations",{pageIndex:t,intent:e})}getFieldObjects(){return this.#js("GetFieldObjects")}hasJSActions(){return this.#js("HasJSActions")}getCalculationOrderIds(){return this.messageHandler.sendWithPromise("GetCalculationOrderIds",null)}getDestinations(){return this.messageHandler.sendWithPromise("GetDestinations",null)}getDestination(t){return"string"!=typeof t?Promise.reject(new Error("Invalid destination request.")):this.messageHandler.sendWithPromise("GetDestination",{id:t})}getPageLabels(){return this.messageHandler.sendWithPromise("GetPageLabels",null)}getPageLayout(){return this.messageHandler.sendWithPromise("GetPageLayout",null)}getPageMode(){return this.messageHandler.sendWithPromise("GetPageMode",null)}getViewerPreferences(){return this.messageHandler.sendWithPromise("GetViewerPreferences",null)}getOpenAction(){return this.messageHandler.sendWithPromise("GetOpenAction",null)}getAttachments(){return this.messageHandler.sendWithPromise("GetAttachments",null)}getDocJSActions(){return this.#js("GetDocJSActions")}getPageJSActions(t){return this.messageHandler.sendWithPromise("GetPageJSActions",{pageIndex:t})}getStructTree(t){return this.messageHandler.sendWithPromise("GetStructTree",{pageIndex:t})}getOutline(){return this.messageHandler.sendWithPromise("GetOutline",null)}getOptionalContentConfig(t){return this.#js("GetOptionalContentConfig").then((e=>new OptionalContentConfig(e,t)))}getPermissions(){return this.messageHandler.sendWithPromise("GetPermissions",null)}getMetadata(){const t="GetMetadata",e=this.#zs.get(t);if(e)return e;const i=this.messageHandler.sendWithPromise(t,null).then((t=>({info:t[0],metadata:t[1]?new Metadata(t[1]):null,contentDispositionFilename:this._fullReader?.filename??null,contentLength:this._fullReader?.contentLength??null})));this.#zs.set(t,i);return i}getMarkInfo(){return this.messageHandler.sendWithPromise("GetMarkInfo",null)}async startCleanup(t=!1){if(!this.destroyed){await this.messageHandler.sendWithPromise("Cleanup",null);for(const t of this.#Gs.values()){if(!t.cleanup())throw new Error(`startCleanup: Page ${t.pageNumber} is currently rendering.`)}this.commonObjs.clear();t||this.fontLoader.clear();this.#zs.clear();this.filterFactory.destroy(!0);TextLayer.cleanup()}}cachedPageNumber(t){if(!isRefProxy(t))return null;const e=0===t.gen?`${t.num}R`:`${t.num}R${t.gen}`;return this.#Ws.get(e)??null}}class RenderTask{#qs=null;onContinue=null;onError=null;constructor(t){this.#qs=t}get promise(){return this.#qs.capability.promise}cancel(t=0){this.#qs.cancel(null,t)}get separateAnnots(){const{separateAnnots:t}=this.#qs.operatorList;if(!t)return!1;const{annotationCanvasMap:e}=this.#qs;return t.form||t.canvas&&e?.size>0}}class InternalRenderTask{#Xs=null;static#Ks=new WeakSet;constructor({callback:t,params:e,objs:i,commonObjs:s,annotationCanvasMap:n,operatorList:a,pageIndex:r,canvasFactory:o,filterFactory:l,useRequestAnimationFrame:h=!1,pdfBug:d=!1,pageColors:c=null}){this.callback=t;this.params=e;this.objs=i;this.commonObjs=s;this.annotationCanvasMap=n;this.operatorListIdx=null;this.operatorList=a;this._pageIndex=r;this.canvasFactory=o;this.filterFactory=l;this._pdfBug=d;this.pageColors=c;this.running=!1;this.graphicsReadyCallback=null;this.graphicsReady=!1;this._useRequestAnimationFrame=!0===h&&"undefined"!=typeof window;this.cancelled=!1;this.capability=Promise.withResolvers();this.task=new RenderTask(this);this._cancelBound=this.cancel.bind(this);this._continueBound=this._continue.bind(this);this._scheduleNextBound=this._scheduleNext.bind(this);this._nextBound=this._next.bind(this);this._canvas=e.canvasContext.canvas}get completed(){return this.capability.promise.catch((function(){}))}initializeGraphics({transparency:t=!1,optionalContentConfig:e}){if(this.cancelled)return;if(this._canvas){if(InternalRenderTask.#Ks.has(this._canvas))throw new Error("Cannot use the same canvas during multiple render() operations. Use different canvas or ensure previous operations were cancelled or completed.");InternalRenderTask.#Ks.add(this._canvas)}if(this._pdfBug&&globalThis.StepperManager?.enabled){this.stepper=globalThis.StepperManager.create(this._pageIndex);this.stepper.init(this.operatorList);this.stepper.nextBreakPoint=this.stepper.getNextBreakPoint()}const{canvasContext:i,viewport:s,transform:n,background:a}=this.params;this.gfx=new CanvasGraphics(i,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:e},this.annotationCanvasMap,this.pageColors);this.gfx.beginDrawing({transform:n,viewport:s,transparency:t,background:a});this.operatorListIdx=0;this.graphicsReady=!0;this.graphicsReadyCallback?.()}cancel(t=null,e=0){this.running=!1;this.cancelled=!0;this.gfx?.endDrawing();if(this.#Xs){window.cancelAnimationFrame(this.#Xs);this.#Xs=null}InternalRenderTask.#Ks.delete(this._canvas);t||=new RenderingCancelledException(`Rendering cancelled, page ${this._pageIndex+1}`,e);this.callback(t);this.task.onError?.(t)}operatorListChanged(){if(this.graphicsReady){this.stepper?.updateOperatorList(this.operatorList);this.running||this._continue()}else this.graphicsReadyCallback||=this._continueBound}_continue(){this.running=!0;this.cancelled||(this.task.onContinue?this.task.onContinue(this._scheduleNextBound):this._scheduleNext())}_scheduleNext(){this._useRequestAnimationFrame?this.#Xs=window.requestAnimationFrame((()=>{this.#Xs=null;this._nextBound().catch(this._cancelBound)})):Promise.resolve().then(this._nextBound).catch(this._cancelBound)}async _next(){if(!this.cancelled){this.operatorListIdx=this.gfx.executeOperatorList(this.operatorList,this.operatorListIdx,this._continueBound,this.stepper);if(this.operatorListIdx===this.operatorList.argsArray.length){this.running=!1;if(this.operatorList.lastChunk){this.gfx.endDrawing();InternalRenderTask.#Ks.delete(this._canvas);this.callback()}}}}}const yt="5.3.31",vt="47ad820d9";function makeColorComp(t){return Math.floor(255*Math.max(0,Math.min(1,t))).toString(16).padStart(2,"0")}function scaleAndClamp(t){return Math.max(0,Math.min(255,255*t))}class ColorConverters{static CMYK_G([t,e,i,s]){return["G",1-Math.min(1,.3*t+.59*i+.11*e+s)]}static G_CMYK([t]){return["CMYK",0,0,0,1-t]}static G_RGB([t]){return["RGB",t,t,t]}static G_rgb([t]){return[t=scaleAndClamp(t),t,t]}static G_HTML([t]){const e=makeColorComp(t);return`#${e}${e}${e}`}static RGB_G([t,e,i]){return["G",.3*t+.59*e+.11*i]}static RGB_rgb(t){return t.map(scaleAndClamp)}static RGB_HTML(t){return`#${t.map(makeColorComp).join("")}`}static T_HTML(){return"#00000000"}static T_rgb(){return[null]}static CMYK_RGB([t,e,i,s]){return["RGB",1-Math.min(1,t+s),1-Math.min(1,i+s),1-Math.min(1,e+s)]}static CMYK_rgb([t,e,i,s]){return[scaleAndClamp(1-Math.min(1,t+s)),scaleAndClamp(1-Math.min(1,i+s)),scaleAndClamp(1-Math.min(1,e+s))]}static CMYK_HTML(t){const e=this.CMYK_RGB(t).slice(1);return this.RGB_HTML(e)}static RGB_CMYK([t,e,i]){const s=1-t,n=1-e,a=1-i;return["CMYK",s,n,a,Math.min(s,n,a)]}}class BaseSVGFactory{create(t,e,i=!1){if(t<=0||e<=0)throw new Error("Invalid SVG dimensions");const s=this._createSVG("svg:svg");s.setAttribute("version","1.1");if(!i){s.setAttribute("width",`${t}px`);s.setAttribute("height",`${e}px`)}s.setAttribute("preserveAspectRatio","none");s.setAttribute("viewBox",`0 0 ${t} ${e}`);return s}createElement(t){if("string"!=typeof t)throw new Error("Invalid SVG element type");return this._createSVG(t)}_createSVG(t){unreachable("Abstract method `_createSVG` called.")}}class DOMSVGFactory extends BaseSVGFactory{_createSVG(t){return document.createElementNS(z,t)}}class XfaLayer{static setupStorage(t,e,i,s,n){const a=s.getValue(e,{value:null});switch(i.name){case"textarea":null!==a.value&&(t.textContent=a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}));break;case"input":if("radio"===i.attributes.type||"checkbox"===i.attributes.type){a.value===i.attributes.xfaOn?t.setAttribute("checked",!0):a.value===i.attributes.xfaOff&&t.removeAttribute("checked");if("print"===n)break;t.addEventListener("change",(t=>{s.setValue(e,{value:t.target.checked?t.target.getAttribute("xfaOn"):t.target.getAttribute("xfaOff")})}))}else{null!==a.value&&t.setAttribute("value",a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}))}break;case"select":if(null!==a.value){t.setAttribute("value",a.value);for(const t of i.children)t.attributes.value===a.value?t.attributes.selected=!0:t.attributes.hasOwnProperty("selected")&&delete t.attributes.selected}t.addEventListener("input",(t=>{const i=t.target.options,n=-1===i.selectedIndex?"":i[i.selectedIndex].value;s.setValue(e,{value:n})}))}}static setAttributes({html:t,element:e,storage:i=null,intent:s,linkService:n}){const{attributes:a}=e,r=t instanceof HTMLAnchorElement;"radio"===a.type&&(a.name=`${a.name}-${s}`);for(const[e,i]of Object.entries(a))if(null!=i)switch(e){case"class":i.length&&t.setAttribute(e,i.join(" "));break;case"dataId":break;case"id":t.setAttribute("data-element-id",i);break;case"style":Object.assign(t.style,i);break;case"textContent":t.textContent=i;break;default:(!r||"href"!==e&&"newWindow"!==e)&&t.setAttribute(e,i)}r&&n.addLinkAttributes(t,a.href,a.newWindow);i&&a.dataId&&this.setupStorage(t,a.dataId,e,i)}static render(t){const e=t.annotationStorage,i=t.linkService,s=t.xfaHtml,n=t.intent||"display",a=document.createElement(s.name);s.attributes&&this.setAttributes({html:a,element:s,intent:n,linkService:i});const r="richText"!==n,o=t.div;o.append(a);if(t.viewport){const e=`matrix(${t.viewport.transform.join(",")})`;o.style.transform=e}r&&o.setAttribute("class","xfaLayer xfaFont");const l=[];if(0===s.children.length){if(s.value){const t=document.createTextNode(s.value);a.append(t);r&&XfaText.shouldBuildText(s.name)&&l.push(t)}return{textDivs:l}}const h=[[s,-1,a]];for(;h.length>0;){const[t,s,a]=h.at(-1);if(s+1===t.children.length){h.pop();continue}const o=t.children[++h.at(-1)[1]];if(null===o)continue;const{name:d}=o;if("#text"===d){const t=document.createTextNode(o.value);l.push(t);a.append(t);continue}const c=o?.attributes?.xmlns?document.createElementNS(o.attributes.xmlns,d):document.createElement(d);a.append(c);o.attributes&&this.setAttributes({html:c,element:o,storage:e,intent:n,linkService:i});if(o.children?.length>0)h.push([o,-1,c]);else if(o.value){const t=document.createTextNode(o.value);r&&XfaText.shouldBuildText(d)&&l.push(t);c.append(t)}}for(const t of o.querySelectorAll(".xfaNonInteractive input, .xfaNonInteractive textarea"))t.setAttribute("readOnly",!0);return{textDivs:l}}static update(t){const e=`matrix(${t.viewport.transform.join(",")})`;t.div.style.transform=e;t.div.hidden=!1}}const xt=1e3,Et=new WeakSet;class AnnotationElementFactory{static create(t){switch(t.data.annotationType){case x.LINK:return new LinkAnnotationElement(t);case x.TEXT:return new TextAnnotationElement(t);case x.WIDGET:switch(t.data.fieldType){case"Tx":return new TextWidgetAnnotationElement(t);case"Btn":return t.data.radioButton?new RadioButtonWidgetAnnotationElement(t):t.data.checkBox?new CheckboxWidgetAnnotationElement(t):new PushButtonWidgetAnnotationElement(t);case"Ch":return new ChoiceWidgetAnnotationElement(t);case"Sig":return new SignatureWidgetAnnotationElement(t)}return new WidgetAnnotationElement(t);case x.POPUP:return new PopupAnnotationElement(t);case x.FREETEXT:return new FreeTextAnnotationElement(t);case x.LINE:return new LineAnnotationElement(t);case x.SQUARE:return new SquareAnnotationElement(t);case x.CIRCLE:return new CircleAnnotationElement(t);case x.POLYLINE:return new PolylineAnnotationElement(t);case x.CARET:return new CaretAnnotationElement(t);case x.INK:return new InkAnnotationElement(t);case x.POLYGON:return new PolygonAnnotationElement(t);case x.HIGHLIGHT:return new HighlightAnnotationElement(t);case x.UNDERLINE:return new UnderlineAnnotationElement(t);case x.SQUIGGLY:return new SquigglyAnnotationElement(t);case x.STRIKEOUT:return new StrikeOutAnnotationElement(t);case x.STAMP:return new StampAnnotationElement(t);case x.FILEATTACHMENT:return new FileAttachmentAnnotationElement(t);default:return new AnnotationElement(t)}}}class AnnotationElement{#Ys=null;#Qs=!1;#Js=null;constructor(t,{isRenderable:e=!1,ignoreBorder:i=!1,createQuadrilaterals:s=!1}={}){this.isRenderable=e;this.data=t.data;this.layer=t.layer;this.linkService=t.linkService;this.downloadManager=t.downloadManager;this.imageResourcesPath=t.imageResourcesPath;this.renderForms=t.renderForms;this.svgFactory=t.svgFactory;this.annotationStorage=t.annotationStorage;this.enableScripting=t.enableScripting;this.hasJSActions=t.hasJSActions;this._fieldObjects=t.fieldObjects;this.parent=t.parent;e&&(this.container=this._createContainer(i));s&&this._createQuadrilaterals()}static _hasPopupData({titleObj:t,contentsObj:e,richText:i}){return!!(t?.str||e?.str||i?.str)}get _isEditable(){return this.data.isEditable}get hasPopupData(){return AnnotationElement._hasPopupData(this.data)}updateEdited(t){if(!this.container)return;this.#Ys||={rect:this.data.rect.slice(0)};const{rect:e}=t;e&&this.#Zs(e);this.#Js?.popup.updateEdited(t)}resetEdited(){if(this.#Ys){this.#Zs(this.#Ys.rect);this.#Js?.popup.resetEdited();this.#Ys=null}}#Zs(t){const{container:{style:e},data:{rect:i,rotation:s},parent:{viewport:{rawDims:{pageWidth:n,pageHeight:a,pageX:r,pageY:o}}}}=this;i?.splice(0,4,...t);e.left=100*(t[0]-r)/n+"%";e.top=100*(a-t[3]+o)/a+"%";if(0===s){e.width=100*(t[2]-t[0])/n+"%";e.height=100*(t[3]-t[1])/a+"%"}else this.setRotation(s)}_createContainer(t){const{data:e,parent:{page:i,viewport:s}}=this,n=document.createElement("section");n.setAttribute("data-annotation-id",e.id);this instanceof WidgetAnnotationElement||(n.tabIndex=xt);const{style:a}=n;a.zIndex=this.parent.zIndex++;e.alternativeText&&(n.title=e.alternativeText);e.noRotate&&n.classList.add("norotate");if(!e.rect||this instanceof PopupAnnotationElement){const{rotation:t}=e;e.hasOwnCanvas||0===t||this.setRotation(t,n);return n}const{width:r,height:o}=this;if(!t&&e.borderStyle.width>0){a.borderWidth=`${e.borderStyle.width}px`;const t=e.borderStyle.horizontalCornerRadius,i=e.borderStyle.verticalCornerRadius;if(t>0||i>0){const e=`calc(${t}px * var(--total-scale-factor)) / calc(${i}px * var(--total-scale-factor))`;a.borderRadius=e}else if(this instanceof RadioButtonWidgetAnnotationElement){const t=`calc(${r}px * var(--total-scale-factor)) / calc(${o}px * var(--total-scale-factor))`;a.borderRadius=t}switch(e.borderStyle.style){case E:a.borderStyle="solid";break;case _:a.borderStyle="dashed";break;case S:warn("Unimplemented border style: beveled");break;case C:warn("Unimplemented border style: inset");break;case T:a.borderBottomStyle="solid"}const s=e.borderColor||null;if(s){this.#Qs=!0;a.borderColor=Util.makeHexColor(0|s[0],0|s[1],0|s[2])}else a.borderWidth=0}const l=Util.normalizeRect([e.rect[0],i.view[3]-e.rect[1]+i.view[1],e.rect[2],i.view[3]-e.rect[3]+i.view[1]]),{pageWidth:h,pageHeight:d,pageX:c,pageY:u}=s.rawDims;a.left=100*(l[0]-c)/h+"%";a.top=100*(l[1]-u)/d+"%";const{rotation:p}=e;if(e.hasOwnCanvas||0===p){a.width=100*r/h+"%";a.height=100*o/d+"%"}else this.setRotation(p,n);return n}setRotation(t,e=this.container){if(!this.data.rect)return;const{pageWidth:i,pageHeight:s}=this.parent.viewport.rawDims;let{width:n,height:a}=this;t%180!=0&&([n,a]=[a,n]);e.style.width=100*n/i+"%";e.style.height=100*a/s+"%";e.setAttribute("data-main-rotation",(360-t)%360)}get _commonActions(){const setColor=(t,e,i)=>{const s=i.detail[t],n=s[0],a=s.slice(1);i.target.style[e]=ColorConverters[`${n}_HTML`](a);this.annotationStorage.setValue(this.data.id,{[e]:ColorConverters[`${n}_rgb`](a)})};return shadow(this,"_commonActions",{display:t=>{const{display:e}=t.detail,i=e%2==1;this.container.style.visibility=i?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noView:i,noPrint:1===e||2===e})},print:t=>{this.annotationStorage.setValue(this.data.id,{noPrint:!t.detail.print})},hidden:t=>{const{hidden:e}=t.detail;this.container.style.visibility=e?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noPrint:e,noView:e})},focus:t=>{setTimeout((()=>t.target.focus({preventScroll:!1})),0)},userName:t=>{t.target.title=t.detail.userName},readonly:t=>{t.target.disabled=t.detail.readonly},required:t=>{this._setRequired(t.target,t.detail.required)},bgColor:t=>{setColor("bgColor","backgroundColor",t)},fillColor:t=>{setColor("fillColor","backgroundColor",t)},fgColor:t=>{setColor("fgColor","color",t)},textColor:t=>{setColor("textColor","color",t)},borderColor:t=>{setColor("borderColor","borderColor",t)},strokeColor:t=>{setColor("strokeColor","borderColor",t)},rotation:t=>{const e=t.detail.rotation;this.setRotation(e);this.annotationStorage.setValue(this.data.id,{rotation:e})}})}_dispatchEventFromSandbox(t,e){const i=this._commonActions;for(const s of Object.keys(e.detail)){const n=t[s]||i[s];n?.(e)}}_setDefaultPropertiesFromJS(t){if(!this.enableScripting)return;const e=this.annotationStorage.getRawValue(this.data.id);if(!e)return;const i=this._commonActions;for(const[s,n]of Object.entries(e)){const a=i[s];if(a){a({detail:{[s]:n},target:t});delete e[s]}}}_createQuadrilaterals(){if(!this.container)return;const{quadPoints:t}=this.data;if(!t)return;const[e,i,s,n]=this.data.rect.map((t=>Math.fround(t)));if(8===t.length){const[a,r,o,l]=t.subarray(2,6);if(s===a&&n===r&&e===o&&i===l)return}const{style:a}=this.container;let r;if(this.#Qs){const{borderColor:t,borderWidth:e}=a;a.borderWidth=0;r=["url('data:image/svg+xml;utf8,",'',``];this.container.classList.add("hasBorder")}const o=s-e,l=n-i,{svgFactory:h}=this,d=h.createElement("svg");d.classList.add("quadrilateralsContainer");d.setAttribute("width",0);d.setAttribute("height",0);const c=h.createElement("defs");d.append(c);const u=h.createElement("clipPath"),p=`clippath_${this.data.id}`;u.setAttribute("id",p);u.setAttribute("clipPathUnits","objectBoundingBox");c.append(u);for(let i=2,s=t.length;i`)}if(this.#Qs){r.push("')");a.backgroundImage=r.join("")}this.container.append(d);this.container.style.clipPath=`url(#${p})`}_createPopup(){const{data:t}=this,e=this.#Js=new PopupAnnotationElement({data:{color:t.color,titleObj:t.titleObj,modificationDate:t.modificationDate,contentsObj:t.contentsObj,richText:t.richText,parentRect:t.rect,borderStyle:0,id:`popup_${t.id}`,rotation:t.rotation},parent:this.parent,elements:[this]});this.parent.div.append(e.render())}render(){unreachable("Abstract method `AnnotationElement.render` called")}_getElementsByName(t,e=null){const i=[];if(this._fieldObjects){const s=this._fieldObjects[t];if(s)for(const{page:t,id:n,exportValues:a}of s){if(-1===t)continue;if(n===e)continue;const s="string"==typeof a?a:null,r=document.querySelector(`[data-element-id="${n}"]`);!r||Et.has(r)?i.push({id:n,exportValue:s,domElement:r}):warn(`_getElementsByName - element not allowed: ${n}`)}return i}for(const s of document.getElementsByName(t)){const{exportValue:t}=s,n=s.getAttribute("data-element-id");n!==e&&(Et.has(s)&&i.push({id:n,exportValue:t,domElement:s}))}return i}show(){this.container&&(this.container.hidden=!1);this.popup?.maybeShow()}hide(){this.container&&(this.container.hidden=!0);this.popup?.forceHide()}getElementsToTriggerPopup(){return this.container}addHighlightArea(){const t=this.getElementsToTriggerPopup();if(Array.isArray(t))for(const e of t)e.classList.add("highlightArea");else t.classList.add("highlightArea")}_editOnDoubleClick(){if(!this._isEditable)return;const{annotationEditorType:t,data:{id:e}}=this;this.container.addEventListener("dblclick",(()=>{this.linkService.eventBus?.dispatch("switchannotationeditormode",{source:this,mode:t,editId:e})}))}get width(){return this.data.rect[2]-this.data.rect[0]}get height(){return this.data.rect[3]-this.data.rect[1]}}class LinkAnnotationElement extends AnnotationElement{constructor(t,e=null){super(t,{isRenderable:!0,ignoreBorder:!!e?.ignoreBorder,createQuadrilaterals:!0});this.isTooltipOnly=t.data.isTooltipOnly}render(){const{data:t,linkService:e}=this,i=document.createElement("a");i.setAttribute("data-element-id",t.id);let s=!1;if(t.url){e.addLinkAttributes(i,t.url,t.newWindow);s=!0}else if(t.action){this._bindNamedAction(i,t.action);s=!0}else if(t.attachment){this.#tn(i,t.attachment,t.attachmentDest);s=!0}else if(t.setOCGState){this.#en(i,t.setOCGState);s=!0}else if(t.dest){this._bindLink(i,t.dest);s=!0}else{if(t.actions&&(t.actions.Action||t.actions["Mouse Up"]||t.actions["Mouse Down"])&&this.enableScripting&&this.hasJSActions){this._bindJSAction(i,t);s=!0}if(t.resetForm){this._bindResetFormAction(i,t.resetForm);s=!0}else if(this.isTooltipOnly&&!s){this._bindLink(i,"");s=!0}}this.container.classList.add("linkAnnotation");s&&this.container.append(i);return this.container}#in(){this.container.setAttribute("data-internal-link","")}_bindLink(t,e){t.href=this.linkService.getDestinationHash(e);t.onclick=()=>{e&&this.linkService.goToDestination(e);return!1};(e||""===e)&&this.#in()}_bindNamedAction(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeNamedAction(e);return!1};this.#in()}#tn(t,e,i=null){t.href=this.linkService.getAnchorUrl("");e.description&&(t.title=e.description);t.onclick=()=>{this.downloadManager?.openOrDownloadData(e.content,e.filename,i);return!1};this.#in()}#en(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeSetOCGState(e);return!1};this.#in()}_bindJSAction(t,e){t.href=this.linkService.getAnchorUrl("");const i=new Map([["Action","onclick"],["Mouse Up","onmouseup"],["Mouse Down","onmousedown"]]);for(const s of Object.keys(e.actions)){const n=i.get(s);n&&(t[n]=()=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e.id,name:s}});return!1})}t.onclick||(t.onclick=()=>!1);this.#in()}_bindResetFormAction(t,e){const i=t.onclick;i||(t.href=this.linkService.getAnchorUrl(""));this.#in();if(this._fieldObjects)t.onclick=()=>{i?.();const{fields:t,refs:s,include:n}=e,a=[];if(0!==t.length||0!==s.length){const e=new Set(s);for(const i of t){const t=this._fieldObjects[i]||[];for(const{id:i}of t)e.add(i)}for(const t of Object.values(this._fieldObjects))for(const i of t)e.has(i.id)===n&&a.push(i)}else for(const t of Object.values(this._fieldObjects))a.push(...t);const r=this.annotationStorage,o=[];for(const t of a){const{id:e}=t;o.push(e);switch(t.type){case"text":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}case"checkbox":case"radiobutton":{const i=t.defaultValue===t.exportValues;r.setValue(e,{value:i});break}case"combobox":case"listbox":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}default:continue}const i=document.querySelector(`[data-element-id="${e}"]`);i&&(Et.has(i)?i.dispatchEvent(new Event("resetform")):warn(`_bindResetFormAction - element not allowed: ${e}`))}this.enableScripting&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:"app",ids:o,name:"ResetForm"}});return!1};else{warn('_bindResetFormAction - "resetForm" action not supported, ensure that the `fieldObjects` parameter is provided.');i||(t.onclick=()=>!1)}}}class TextAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!0})}render(){this.container.classList.add("textAnnotation");const t=document.createElement("img");t.src=this.imageResourcesPath+"annotation-"+this.data.name.toLowerCase()+".svg";t.setAttribute("data-l10n-id","pdfjs-text-annotation-type");t.setAttribute("data-l10n-args",JSON.stringify({type:this.data.name}));!this.data.popupRef&&this.hasPopupData&&this._createPopup();this.container.append(t);return this.container}}class WidgetAnnotationElement extends AnnotationElement{render(){return this.container}showElementAndHideCanvas(t){if(this.data.hasOwnCanvas){"CANVAS"===t.previousSibling?.nodeName&&(t.previousSibling.hidden=!0);t.hidden=!1}}_getKeyModifier(t){return util_FeatureTest.platform.isMac?t.metaKey:t.ctrlKey}_setEventListener(t,e,i,s,n){i.includes("mouse")?t.addEventListener(i,(t=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t),shift:t.shiftKey,modifier:this._getKeyModifier(t)}})})):t.addEventListener(i,(t=>{if("blur"===i){if(!e.focused||!t.relatedTarget)return;e.focused=!1}else if("focus"===i){if(e.focused)return;e.focused=!0}n&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t)}})}))}_setEventListeners(t,e,i,s){for(const[n,a]of i)if("Action"===a||this.data.actions?.[a]){"Focus"!==a&&"Blur"!==a||(e||={focused:!1});this._setEventListener(t,e,n,a,s);"Focus"!==a||this.data.actions?.Blur?"Blur"!==a||this.data.actions?.Focus||this._setEventListener(t,e,"focus","Focus",null):this._setEventListener(t,e,"blur","Blur",null)}}_setBackgroundColor(t){const e=this.data.backgroundColor||null;t.style.backgroundColor=null===e?"transparent":Util.makeHexColor(e[0],e[1],e[2])}_setTextStyle(t){const e=["left","center","right"],{fontColor:s}=this.data.defaultAppearanceData,n=this.data.defaultAppearanceData.fontSize||9,a=t.style;let r;const roundToOneDecimal=t=>Math.round(10*t)/10;if(this.data.multiLine){const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2),e=t/(Math.round(t/(i*n))||1);r=Math.min(n,roundToOneDecimal(e/i))}else{const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2);r=Math.min(n,roundToOneDecimal(t/i))}a.fontSize=`calc(${r}px * var(--total-scale-factor))`;a.color=Util.makeHexColor(s[0],s[1],s[2]);null!==this.data.textAlignment&&(a.textAlign=e[this.data.textAlignment])}_setRequired(t,e){e?t.setAttribute("required",!0):t.removeAttribute("required");t.setAttribute("aria-required",e)}}class TextWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms||t.data.hasOwnCanvas||!t.data.hasAppearance&&!!t.data.fieldValue})}setPropertyOnSiblings(t,e,i,s){const n=this.annotationStorage;for(const a of this._getElementsByName(t.name,t.id)){a.domElement&&(a.domElement[e]=i);n.setValue(a.id,{[s]:i})}}render(){const t=this.annotationStorage,e=this.data.id;this.container.classList.add("textWidgetAnnotation");let i=null;if(this.renderForms){const s=t.getValue(e,{value:this.data.fieldValue});let n=s.value||"";const a=t.getValue(e,{charLimit:this.data.maxLen}).charLimit;a&&n.length>a&&(n=n.slice(0,a));let r=s.formattedValue||this.data.textContent?.join("\n")||null;r&&this.data.comb&&(r=r.replaceAll(/\s+/g,""));const o={userValue:n,formattedValue:r,lastCommittedValue:null,commitKey:1,focused:!1};if(this.data.multiLine){i=document.createElement("textarea");i.textContent=r??n;this.data.doNotScroll&&(i.style.overflowY="hidden")}else{i=document.createElement("input");i.type=this.data.password?"password":"text";i.setAttribute("value",r??n);this.data.doNotScroll&&(i.style.overflowX="hidden")}this.data.hasOwnCanvas&&(i.hidden=!0);Et.add(i);i.setAttribute("data-element-id",e);i.disabled=this.data.readOnly;i.name=this.data.fieldName;i.tabIndex=xt;this._setRequired(i,this.data.required);a&&(i.maxLength=a);i.addEventListener("input",(s=>{t.setValue(e,{value:s.target.value});this.setPropertyOnSiblings(i,"value",s.target.value,"value");o.formattedValue=null}));i.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue??"";i.value=o.userValue=e;o.formattedValue=null}));let blurListener=t=>{const{formattedValue:e}=o;null!=e&&(t.target.value=e);t.target.scrollLeft=0};if(this.enableScripting&&this.hasJSActions){i.addEventListener("focus",(t=>{if(o.focused)return;const{target:e}=t;o.userValue&&(e.value=o.userValue);o.lastCommittedValue=e.value;o.commitKey=1;this.data.actions?.Focus||(o.focused=!0)}));i.addEventListener("updatefromsandbox",(i=>{this.showElementAndHideCanvas(i.target);const s={value(i){o.userValue=i.detail.value??"";t.setValue(e,{value:o.userValue.toString()});i.target.value=o.userValue},formattedValue(i){const{formattedValue:s}=i.detail;o.formattedValue=s;null!=s&&i.target!==document.activeElement&&(i.target.value=s);t.setValue(e,{formattedValue:s})},selRange(t){t.target.setSelectionRange(...t.detail.selRange)},charLimit:i=>{const{charLimit:s}=i.detail,{target:n}=i;if(0===s){n.removeAttribute("maxLength");return}n.setAttribute("maxLength",s);let a=o.userValue;if(a&&!(a.length<=s)){a=a.slice(0,s);n.value=o.userValue=a;t.setValue(e,{value:a});this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:a,willCommit:!0,commitKey:1,selStart:n.selectionStart,selEnd:n.selectionEnd}})}}};this._dispatchEventFromSandbox(s,i)}));i.addEventListener("keydown",(t=>{o.commitKey=1;let i=-1;"Escape"===t.key?i=0:"Enter"!==t.key||this.data.multiLine?"Tab"===t.key&&(o.commitKey=3):i=2;if(-1===i)return;const{value:s}=t.target;if(o.lastCommittedValue!==s){o.lastCommittedValue=s;o.userValue=s;this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:s,willCommit:!0,commitKey:i,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}})}}));const s=blurListener;blurListener=null;i.addEventListener("blur",(t=>{if(!o.focused||!t.relatedTarget)return;this.data.actions?.Blur||(o.focused=!1);const{value:i}=t.target;o.userValue=i;o.lastCommittedValue!==i&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:i,willCommit:!0,commitKey:o.commitKey,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}});s(t)}));this.data.actions?.Keystroke&&i.addEventListener("beforeinput",(t=>{o.lastCommittedValue=null;const{data:i,target:s}=t,{value:n,selectionStart:a,selectionEnd:r}=s;let l=a,h=r;switch(t.inputType){case"deleteWordBackward":{const t=n.substring(0,a).match(/\w*[^\w]*$/);t&&(l-=t[0].length);break}case"deleteWordForward":{const t=n.substring(a).match(/^[^\w]*\w*/);t&&(h+=t[0].length);break}case"deleteContentBackward":a===r&&(l-=1);break;case"deleteContentForward":a===r&&(h+=1)}t.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:n,change:i||"",willCommit:!1,selStart:l,selEnd:h}})}));this._setEventListeners(i,o,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.value))}blurListener&&i.addEventListener("blur",blurListener);if(this.data.comb){const t=(this.data.rect[2]-this.data.rect[0])/a;i.classList.add("comb");i.style.letterSpacing=`calc(${t}px * var(--total-scale-factor) - 1ch)`}}else{i=document.createElement("div");i.textContent=this.data.fieldValue;i.style.verticalAlign="middle";i.style.display="table-cell";this.data.hasOwnCanvas&&(i.hidden=!0)}this._setTextStyle(i);this._setBackgroundColor(i);this._setDefaultPropertiesFromJS(i);this.container.append(i);return this.container}}class SignatureWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:!!t.data.hasOwnCanvas})}}class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.exportValue===e.fieldValue}).value;if("string"==typeof s){s="Off"!==s;t.setValue(i,{value:s})}this.container.classList.add("buttonWidgetAnnotation","checkBox");const n=document.createElement("input");Et.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="checkbox";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.setAttribute("exportValue",e.exportValue);n.tabIndex=xt;n.addEventListener("change",(s=>{const{name:n,checked:a}=s.target;for(const s of this._getElementsByName(n,i)){const i=a&&s.exportValue===e.exportValue;s.domElement&&(s.domElement.checked=i);t.setValue(s.id,{value:i})}t.setValue(i,{value:a})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue||"Off";t.target.checked=i===e.exportValue}));if(this.enableScripting&&this.hasJSActions){n.addEventListener("updatefromsandbox",(e=>{const s={value(e){e.target.checked="Off"!==e.detail.value;t.setValue(i,{value:e.target.checked})}};this._dispatchEventFromSandbox(s,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("buttonWidgetAnnotation","radioButton");const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.fieldValue===e.buttonValue}).value;if("string"==typeof s){s=s!==e.buttonValue;t.setValue(i,{value:s})}if(s)for(const s of this._getElementsByName(e.fieldName,i))t.setValue(s.id,{value:!1});const n=document.createElement("input");Et.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="radio";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.tabIndex=xt;n.addEventListener("change",(e=>{const{name:s,checked:n}=e.target;for(const e of this._getElementsByName(s,i))t.setValue(e.id,{value:!1});t.setValue(i,{value:n})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue;t.target.checked=null!=i&&i===e.buttonValue}));if(this.enableScripting&&this.hasJSActions){const s=e.buttonValue;n.addEventListener("updatefromsandbox",(e=>{const n={value:e=>{const n=s===e.detail.value;for(const s of this._getElementsByName(e.target.name)){const e=n&&s.id===i;s.domElement&&(s.domElement.checked=e);t.setValue(s.id,{value:e})}}};this._dispatchEventFromSandbox(n,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class PushButtonWidgetAnnotationElement extends LinkAnnotationElement{constructor(t){super(t,{ignoreBorder:t.data.hasAppearance})}render(){const t=super.render();t.classList.add("buttonWidgetAnnotation","pushButton");const e=t.lastChild;if(this.enableScripting&&this.hasJSActions&&e){this._setDefaultPropertiesFromJS(e);e.addEventListener("updatefromsandbox",(t=>{this._dispatchEventFromSandbox({},t)}))}return t}}class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("choiceWidgetAnnotation");const t=this.annotationStorage,e=this.data.id,i=t.getValue(e,{value:this.data.fieldValue}),s=document.createElement("select");Et.add(s);s.setAttribute("data-element-id",e);s.disabled=this.data.readOnly;this._setRequired(s,this.data.required);s.name=this.data.fieldName;s.tabIndex=xt;let n=this.data.combo&&this.data.options.length>0;if(!this.data.combo){s.size=this.data.options.length;this.data.multiSelect&&(s.multiple=!0)}s.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue;for(const t of s.options)t.selected=t.value===e}));for(const t of this.data.options){const e=document.createElement("option");e.textContent=t.displayValue;e.value=t.exportValue;if(i.value.includes(t.exportValue)){e.setAttribute("selected",!0);n=!1}s.append(e)}let a=null;if(n){const t=document.createElement("option");t.value=" ";t.setAttribute("hidden",!0);t.setAttribute("selected",!0);s.prepend(t);a=()=>{t.remove();s.removeEventListener("input",a);a=null};s.addEventListener("input",a)}const getValue=t=>{const e=t?"value":"textContent",{options:i,multiple:n}=s;return n?Array.prototype.filter.call(i,(t=>t.selected)).map((t=>t[e])):-1===i.selectedIndex?null:i[i.selectedIndex][e]};let r=getValue(!1);const getItems=t=>{const e=t.target.options;return Array.prototype.map.call(e,(t=>({displayValue:t.textContent,exportValue:t.value})))};if(this.enableScripting&&this.hasJSActions){s.addEventListener("updatefromsandbox",(i=>{const n={value(i){a?.();const n=i.detail.value,o=new Set(Array.isArray(n)?n:[n]);for(const t of s.options)t.selected=o.has(t.value);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},multipleSelection(t){s.multiple=!0},remove(i){const n=s.options,a=i.detail.remove;n[a].selected=!1;s.remove(a);if(n.length>0){-1===Array.prototype.findIndex.call(n,(t=>t.selected))&&(n[0].selected=!0)}t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},clear(i){for(;0!==s.length;)s.remove(0);t.setValue(e,{value:null,items:[]});r=getValue(!1)},insert(i){const{index:n,displayValue:a,exportValue:o}=i.detail.insert,l=s.children[n],h=document.createElement("option");h.textContent=a;h.value=o;l?l.before(h):s.append(h);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},items(i){const{items:n}=i.detail;for(;0!==s.length;)s.remove(0);for(const t of n){const{displayValue:e,exportValue:i}=t,n=document.createElement("option");n.textContent=e;n.value=i;s.append(n)}s.options.length>0&&(s.options[0].selected=!0);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},indices(i){const s=new Set(i.detail.indices);for(const t of i.target.options)t.selected=s.has(t.index);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},editable(t){t.target.disabled=!t.detail.editable}};this._dispatchEventFromSandbox(n,i)}));s.addEventListener("input",(i=>{const s=getValue(!0),n=getValue(!1);t.setValue(e,{value:s});i.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:r,change:n,changeEx:s,willCommit:!1,commitKey:1,keyDown:!1}})}));this._setEventListeners(s,null,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"],["input","Action"],["input","Validate"]],(t=>t.target.value))}else s.addEventListener("input",(function(i){t.setValue(e,{value:getValue(!0)})}));this.data.combo&&this._setTextStyle(s);this._setBackgroundColor(s);this._setDefaultPropertiesFromJS(s);this.container.append(s);return this.container}}class PopupAnnotationElement extends AnnotationElement{constructor(t){const{data:e,elements:i}=t;super(t,{isRenderable:AnnotationElement._hasPopupData(e)});this.elements=i;this.popup=null}render(){this.container.classList.add("popupAnnotation");const t=this.popup=new PopupElement({container:this.container,color:this.data.color,titleObj:this.data.titleObj,modificationDate:this.data.modificationDate,contentsObj:this.data.contentsObj,richText:this.data.richText,rect:this.data.rect,parentRect:this.data.parentRect||null,parent:this.parent,elements:this.elements,open:this.data.open}),e=[];for(const i of this.elements){i.popup=t;i.container.ariaHasPopup="dialog";e.push(i.data.id);i.addHighlightArea()}this.container.setAttribute("aria-controls",e.map((t=>`${U}${t}`)).join(","));return this.container}}class PopupElement{#sn=this.#nn.bind(this);#an=this.#rn.bind(this);#on=this.#ln.bind(this);#hn=this.#dn.bind(this);#cn=null;#mt=null;#un=null;#pn=null;#gn=null;#fn=null;#mn=null;#bn=!1;#An=null;#T=null;#wn=null;#yn=null;#vn=null;#Ys=null;#xn=!1;constructor({container:t,color:e,elements:i,titleObj:s,modificationDate:n,contentsObj:a,richText:r,parent:o,rect:l,parentRect:h,open:d}){this.#mt=t;this.#vn=s;this.#un=a;this.#yn=r;this.#fn=o;this.#cn=e;this.#wn=l;this.#mn=h;this.#gn=i;this.#pn=PDFDateString.toDateObject(n);this.trigger=i.flatMap((t=>t.getElementsToTriggerPopup()));for(const t of this.trigger){t.addEventListener("click",this.#hn);t.addEventListener("mouseenter",this.#on);t.addEventListener("mouseleave",this.#an);t.classList.add("popupTriggerArea")}for(const t of i)t.container?.addEventListener("keydown",this.#sn);this.#mt.hidden=!0;d&&this.#dn()}render(){if(this.#An)return;const t=this.#An=document.createElement("div");t.className="popup";if(this.#cn){const e=t.style.outlineColor=Util.makeHexColor(...this.#cn);t.style.backgroundColor=`color-mix(in srgb, ${e} 30%, white)`}const e=document.createElement("span");e.className="header";const i=document.createElement("h1");e.append(i);({dir:i.dir,str:i.textContent}=this.#vn);t.append(e);if(this.#pn){const t=document.createElement("span");t.classList.add("popupDate");t.setAttribute("data-l10n-id","pdfjs-annotation-date-time-string");t.setAttribute("data-l10n-args",JSON.stringify({dateObj:this.#pn.valueOf()}));e.append(t)}const s=this.#En;if(s){XfaLayer.render({xfaHtml:s,intent:"richText",div:t});t.lastChild.classList.add("richText","popupContent")}else{const e=this._formatContents(this.#un);t.append(e)}this.#mt.append(t)}get#En(){const t=this.#yn,e=this.#un;return!t?.str||e?.str&&e.str!==t.str?null:this.#yn.html||null}get#_n(){return this.#En?.attributes?.style?.fontSize||0}get#Sn(){return this.#En?.attributes?.style?.color||null}#Cn(t){const e=[],i={str:t,html:{name:"div",attributes:{dir:"auto"},children:[{name:"p",children:e}]}},s={style:{color:this.#Sn,fontSize:this.#_n?`calc(${this.#_n}px * var(--total-scale-factor))`:""}};for(const i of t.split("\n"))e.push({name:"span",value:i,attributes:s});return i}_formatContents({str:t,dir:e}){const i=document.createElement("p");i.classList.add("popupContent");i.dir=e;const s=t.split(/(?:\r\n?|\n)/);for(let t=0,e=s.length;t=0&&n.setAttribute("stroke-width",e||1);if(i)for(let t=0,e=this.#Rn.length;t{"Enter"===t.key&&(s?t.metaKey:t.ctrlKey)&&this.#On()}));!e.popupRef&&this.hasPopupData?this._createPopup():i.classList.add("popupTriggerArea");t.append(i);return t}getElementsToTriggerPopup(){return this.#Ln}addHighlightArea(){this.container.classList.add("highlightArea")}#On(){this.downloadManager?.openOrDownloadData(this.content,this.filename)}}class AnnotationLayer{#Nn=null;#Bn=null;#Hn=new Map;#Un=null;constructor({div:t,accessibilityManager:e,annotationCanvasMap:i,annotationEditorUIManager:s,page:n,viewport:a,structTreeLayer:r}){this.div=t;this.#Nn=e;this.#Bn=i;this.#Un=r||null;this.page=n;this.viewport=a;this.zIndex=0;this._annotationEditorUIManager=s}hasEditableAnnotations(){return this.#Hn.size>0}async#zn(t,e){const i=t.firstChild||t,s=i.id=`${U}${e}`,n=await(this.#Un?.getAriaAttributes(s));if(n)for(const[t,e]of n)i.setAttribute(t,e);this.div.append(t);this.#Nn?.moveElementInDOM(this.div,t,i,!1)}async render(t){const{annotations:e}=t,i=this.div;setLayerDimensions(i,this.viewport);const s=new Map,n={data:null,layer:i,linkService:t.linkService,downloadManager:t.downloadManager,imageResourcesPath:t.imageResourcesPath||"",renderForms:!1!==t.renderForms,svgFactory:new DOMSVGFactory,annotationStorage:t.annotationStorage||new AnnotationStorage,enableScripting:!0===t.enableScripting,hasJSActions:t.hasJSActions,fieldObjects:t.fieldObjects,parent:this,elements:null};for(const t of e){if(t.noHTML)continue;const e=t.annotationType===x.POPUP;if(e){const e=s.get(t.id);if(!e)continue;n.elements=e}else if(t.rect[2]===t.rect[0]||t.rect[3]===t.rect[1])continue;n.data=t;const i=AnnotationElementFactory.create(n);if(!i.isRenderable)continue;if(!e&&t.popupRef){const e=s.get(t.popupRef);e?e.push(i):s.set(t.popupRef,[i])}const a=i.render();t.hidden&&(a.style.visibility="hidden");await this.#zn(a,t.id);if(i._isEditable){this.#Hn.set(i.data.id,i);this._annotationEditorUIManager?.renderAnnotationElement(i)}}this.#Gn()}async addLinkAnnotations(t,e){const i={data:null,layer:this.div,linkService:e,svgFactory:new DOMSVGFactory,parent:this};for(const e of t){e.borderStyle||=AnnotationLayer._defaultBorderStyle;i.data=e;const t=AnnotationElementFactory.create(i);if(!t.isRenderable)continue;const s=t.render();await this.#zn(s,e.id)}}update({viewport:t}){const e=this.div;this.viewport=t;setLayerDimensions(e,{rotation:t.rotation});this.#Gn();e.hidden=!1}#Gn(){if(!this.#Bn)return;const t=this.div;for(const[e,i]of this.#Bn){const s=t.querySelector(`[data-annotation-id="${e}"]`);if(!s)continue;i.className="annotationContent";const{firstChild:n}=s;n?"CANVAS"===n.nodeName?n.replaceWith(i):n.classList.contains("annotationContent")?n.after(i):n.before(i):s.append(i);const a=this.#Hn.get(e);if(a)if(a._hasNoCanvas){this._annotationEditorUIManager?.setMissingCanvas(e,s.id,i);a._hasNoCanvas=!1}else a.canvas=i}this.#Bn.clear()}getEditableAnnotations(){return Array.from(this.#Hn.values())}getEditableAnnotation(t){return this.#Hn.get(t)}static get _defaultBorderStyle(){return shadow(this,"_defaultBorderStyle",Object.freeze({width:1,rawWidth:1,style:E,dashArray:[3],horizontalCornerRadius:0,verticalCornerRadius:0}))}}const _t=/\r\n?|\n/g;class FreeTextEditor extends AnnotationEditor{#cn;#$n="";#Wn=`${this.id}-editor`;#Vn=null;#_n;static _freeTextDefaultContent="";static _internalPadding=0;static _defaultColor=null;static _defaultFontSize=10;static get _keyboardManager(){const t=FreeTextEditor.prototype,arrowChecker=t=>t.isEmpty(),e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+s","mac+meta+s","ctrl+p","mac+meta+p"],t.commitOrRemove,{bubbles:!0}],[["ctrl+Enter","mac+meta+Enter","Escape","mac+Escape"],t.commitOrRemove],[["ArrowLeft","mac+ArrowLeft"],t._translateEmpty,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t._translateEmpty,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t._translateEmpty,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t._translateEmpty,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t._translateEmpty,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t._translateEmpty,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t._translateEmpty,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t._translateEmpty,{args:[0,i],checker:arrowChecker}]]))}static _type="freetext";static _editorType=u.FREETEXT;constructor(t){super({...t,name:"freeTextEditor"});this.#cn=t.color||FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor;this.#_n=t.fontSize||FreeTextEditor._defaultFontSize}static initialize(t,e){AnnotationEditor.initialize(t,e);const i=getComputedStyle(document.documentElement);this._internalPadding=parseFloat(i.getPropertyValue("--freetext-padding"))}static updateDefaultParams(t,e){switch(t){case p.FREETEXT_SIZE:FreeTextEditor._defaultFontSize=e;break;case p.FREETEXT_COLOR:FreeTextEditor._defaultColor=e}}updateParams(t,e){switch(t){case p.FREETEXT_SIZE:this.#jn(e);break;case p.FREETEXT_COLOR:this.#qn(e)}}static get defaultPropertiesToUpdate(){return[[p.FREETEXT_SIZE,FreeTextEditor._defaultFontSize],[p.FREETEXT_COLOR,FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor]]}get propertiesToUpdate(){return[[p.FREETEXT_SIZE,this.#_n],[p.FREETEXT_COLOR,this.#cn]]}#jn(t){const setFontsize=t=>{this.editorDiv.style.fontSize=`calc(${t}px * var(--total-scale-factor))`;this.translate(0,-(t-this.#_n)*this.parentScale);this.#_n=t;this.#Xn()},e=this.#_n;this.addCommands({cmd:setFontsize.bind(this,t),undo:setFontsize.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.FREETEXT_SIZE,overwriteIfSameType:!0,keepUndo:!0})}#qn(t){const setColor=t=>{this.#cn=this.editorDiv.style.color=t},e=this.#cn;this.addCommands({cmd:setColor.bind(this,t),undo:setColor.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.FREETEXT_COLOR,overwriteIfSameType:!0,keepUndo:!0})}_translateEmpty(t,e){this._uiManager.translateSelectedEditors(t,e,!0)}getInitialTranslation(){const t=this.parentScale;return[-FreeTextEditor._internalPadding*t,-(FreeTextEditor._internalPadding+this.#_n)*t]}rebuild(){if(this.parent){super.rebuild();null!==this.div&&(this.isAttachedToDOM||this.parent.add(this))}}enableEditMode(){if(!super.enableEditMode())return!1;this.overlayDiv.classList.remove("enabled");this.editorDiv.contentEditable=!0;this._isDraggable=!1;this.div.removeAttribute("aria-activedescendant");this.#Vn=new AbortController;const t=this._uiManager.combinedSignal(this.#Vn);this.editorDiv.addEventListener("keydown",this.editorDivKeydown.bind(this),{signal:t});this.editorDiv.addEventListener("focus",this.editorDivFocus.bind(this),{signal:t});this.editorDiv.addEventListener("blur",this.editorDivBlur.bind(this),{signal:t});this.editorDiv.addEventListener("input",this.editorDivInput.bind(this),{signal:t});this.editorDiv.addEventListener("paste",this.editorDivPaste.bind(this),{signal:t});return!0}disableEditMode(){if(!super.disableEditMode())return!1;this.overlayDiv.classList.add("enabled");this.editorDiv.contentEditable=!1;this.div.setAttribute("aria-activedescendant",this.#Wn);this._isDraggable=!0;this.#Vn?.abort();this.#Vn=null;this.div.focus({preventScroll:!0});this.isEditing=!1;this.parent.div.classList.add("freetextEditing");return!0}focusin(t){if(this._focusEventsAllowed){super.focusin(t);t.target!==this.editorDiv&&this.editorDiv.focus()}}onceAdded(t){if(!this.width){this.enableEditMode();t&&this.editorDiv.focus();this._initialOptions?.isCentered&&this.center();this._initialOptions=null}}isEmpty(){return!this.editorDiv||""===this.editorDiv.innerText.trim()}remove(){this.isEditing=!1;if(this.parent){this.parent.setEditingState(!0);this.parent.div.classList.add("freetextEditing")}super.remove()}#Kn(){const t=[];this.editorDiv.normalize();let e=null;for(const i of this.editorDiv.childNodes)if(e?.nodeType!==Node.TEXT_NODE||"BR"!==i.nodeName){t.push(FreeTextEditor.#Yn(i));e=i}return t.join("\n")}#Xn(){const[t,e]=this.parentDimensions;let i;if(this.isAttachedToDOM)i=this.div.getBoundingClientRect();else{const{currentLayer:t,div:e}=this,s=e.style.display,n=e.classList.contains("hidden");e.classList.remove("hidden");e.style.display="hidden";t.div.append(this.div);i=e.getBoundingClientRect();e.remove();e.style.display=s;e.classList.toggle("hidden",n)}if(this.rotation%180==this.parentRotation%180){this.width=i.width/t;this.height=i.height/e}else{this.width=i.height/t;this.height=i.width/e}this.fixAndSetPosition()}commit(){if(!this.isInEditMode())return;super.commit();this.disableEditMode();const t=this.#$n,e=this.#$n=this.#Kn().trimEnd();if(t===e)return;const setText=t=>{this.#$n=t;if(t){this.#Qn();this._uiManager.rebuild(this);this.#Xn()}else this.remove()};this.addCommands({cmd:()=>{setText(e)},undo:()=>{setText(t)},mustExec:!1});this.#Xn()}shouldGetKeyboardEvents(){return this.isInEditMode()}enterInEditMode(){this.enableEditMode();this.editorDiv.focus()}keydown(t){if(t.target===this.div&&"Enter"===t.key){this.enterInEditMode();t.preventDefault()}}editorDivKeydown(t){FreeTextEditor._keyboardManager.exec(this,t)}editorDivFocus(t){this.isEditing=!0}editorDivBlur(t){this.isEditing=!1}editorDivInput(t){this.parent.div.classList.toggle("freetextEditing",this.isEmpty())}disableEditing(){this.editorDiv.setAttribute("role","comment");this.editorDiv.removeAttribute("aria-multiline")}enableEditing(){this.editorDiv.setAttribute("role","textbox");this.editorDiv.setAttribute("aria-multiline",!0)}get canChangeContent(){return!0}render(){if(this.div)return this.div;let t,e;if(this._isCopy||this.annotationElementId){t=this.x;e=this.y}super.render();this.editorDiv=document.createElement("div");this.editorDiv.className="internal";this.editorDiv.setAttribute("id",this.#Wn);this.editorDiv.setAttribute("data-l10n-id","pdfjs-free-text2");this.editorDiv.setAttribute("data-l10n-attrs","default-content");this.enableEditing();this.editorDiv.contentEditable=!0;const{style:i}=this.editorDiv;i.fontSize=`calc(${this.#_n}px * var(--total-scale-factor))`;i.color=this.#cn;this.div.append(this.editorDiv);this.overlayDiv=document.createElement("div");this.overlayDiv.classList.add("overlay","enabled");this.div.append(this.overlayDiv);if(this._isCopy||this.annotationElementId){const[i,s]=this.parentDimensions;if(this.annotationElementId){const{position:n}=this._initialData;let[a,r]=this.getInitialTranslation();[a,r]=this.pageTranslationToScreen(a,r);const[o,l]=this.pageDimensions,[h,d]=this.pageTranslation;let c,u;switch(this.rotation){case 0:c=t+(n[0]-h)/o;u=e+this.height-(n[1]-d)/l;break;case 90:c=t+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[r,-a];break;case 180:c=t-this.width+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[-a,-r];break;case 270:c=t+(n[0]-h-this.height*l)/o;u=e+(n[1]-d-this.width*o)/l;[a,r]=[-r,a]}this.setAt(c*i,u*s,a,r)}else this._moveAfterPaste(t,e);this.#Qn();this._isDraggable=!0;this.editorDiv.contentEditable=!1}else{this._isDraggable=!1;this.editorDiv.contentEditable=!0}return this.div}static#Yn(t){return(t.nodeType===Node.TEXT_NODE?t.nodeValue:t.innerText).replaceAll(_t,"")}editorDivPaste(t){const e=t.clipboardData||window.clipboardData,{types:i}=e;if(1===i.length&&"text/plain"===i[0])return;t.preventDefault();const s=FreeTextEditor.#Jn(e.getData("text")||"").replaceAll(_t,"\n");if(!s)return;const n=window.getSelection();if(!n.rangeCount)return;this.editorDiv.normalize();n.deleteFromDocument();const a=n.getRangeAt(0);if(!s.includes("\n")){a.insertNode(document.createTextNode(s));this.editorDiv.normalize();n.collapseToStart();return}const{startContainer:r,startOffset:o}=a,l=[],h=[];if(r.nodeType===Node.TEXT_NODE){const t=r.parentElement;h.push(r.nodeValue.slice(o).replaceAll(_t,""));if(t!==this.editorDiv){let e=l;for(const i of this.editorDiv.childNodes)i!==t?e.push(FreeTextEditor.#Yn(i)):e=h}l.push(r.nodeValue.slice(0,o).replaceAll(_t,""))}else if(r===this.editorDiv){let t=l,e=0;for(const i of this.editorDiv.childNodes){e++===o&&(t=h);t.push(FreeTextEditor.#Yn(i))}}this.#$n=`${l.join("\n")}${s}${h.join("\n")}`;this.#Qn();const d=new Range;let c=Math.sumPrecise(l.map((t=>t.length)));for(const{firstChild:t}of this.editorDiv.childNodes)if(t.nodeType===Node.TEXT_NODE){const e=t.nodeValue.length;if(c<=e){d.setStart(t,c);d.setEnd(t,c);break}c-=e}n.removeAllRanges();n.addRange(d)}#Qn(){this.editorDiv.replaceChildren();if(this.#$n)for(const t of this.#$n.split("\n")){const e=document.createElement("div");e.append(t?document.createTextNode(t):document.createElement("br"));this.editorDiv.append(e)}}#Zn(){return this.#$n.replaceAll(" "," ")}static#Jn(t){return t.replaceAll(" "," ")}get contentDiv(){return this.editorDiv}static async deserialize(t,e,i){let s=null;if(t instanceof FreeTextAnnotationElement){const{data:{defaultAppearanceData:{fontSize:e,fontColor:i},rect:n,rotation:a,id:r,popupRef:o},textContent:l,textPosition:h,parent:{page:{pageNumber:d}}}=t;if(!l||0===l.length)return null;s=t={annotationType:u.FREETEXT,color:Array.from(i),fontSize:e,value:l.join("\n"),position:h,pageIndex:d-1,rect:n.slice(0),rotation:a,id:r,deleted:!1,popupRef:o}}const n=await super.deserialize(t,e,i);n.#_n=t.fontSize;n.#cn=Util.makeHexColor(...t.color);n.#$n=FreeTextEditor.#Jn(t.value);n.annotationElementId=t.id||null;n._initialData=s;return n}serialize(t=!1){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const e=FreeTextEditor._internalPadding*this.parentScale,i=this.getRect(e,e),s=AnnotationEditor._colorManager.convert(this.isAttachedToDOM?getComputedStyle(this.editorDiv).color:this.#cn),n={annotationType:u.FREETEXT,color:s,fontSize:this.#_n,value:this.#Zn(),pageIndex:this.pageIndex,rect:i,rotation:this.rotation,structTreeParentId:this._structTreeParentId};if(t){n.isCopy=!0;return n}if(this.annotationElementId&&!this.#ta(n))return null;n.id=this.annotationElementId;return n}#ta(t){const{value:e,fontSize:i,color:s,pageIndex:n}=this._initialData;return this._hasBeenMoved||t.value!==e||t.fontSize!==i||t.color.some(((t,e)=>t!==s[e]))||t.pageIndex!==n}renderAnnotationElement(t){const e=super.renderAnnotationElement(t);if(this.deleted)return e;const{style:i}=e;i.fontSize=`calc(${this.#_n}px * var(--total-scale-factor))`;i.color=this.#cn;e.replaceChildren();for(const t of this.#$n.split("\n")){const i=document.createElement("div");i.append(t?document.createTextNode(t):document.createElement("br"));e.append(i)}const s=FreeTextEditor._internalPadding*this.parentScale;t.updateEdited({rect:this.getRect(s,s),popupContent:this.#$n});return e}resetAnnotationElement(t){super.resetAnnotationElement(t);t.resetEdited()}}class Outline{static PRECISION=1e-4;toSVGPath(){unreachable("Abstract method `toSVGPath` must be implemented.")}get box(){unreachable("Abstract getter `box` must be implemented.")}serialize(t,e){unreachable("Abstract method `serialize` must be implemented.")}static _rescale(t,e,i,s,n,a){a||=new Float32Array(t.length);for(let r=0,o=t.length;r=6;t-=6)isNaN(e[t])?i.push(`L${e[t+4]} ${e[t+5]}`):i.push(`C${e[t]} ${e[t+1]} ${e[t+2]} ${e[t+3]} ${e[t+4]} ${e[t+5]}`);this.#ya(i);return i.join(" ")}#Aa(){const[t,e,i,s]=this.#ea,[n,a,r,o]=this.#ba();return`M${(this.#ra[2]-t)/i} ${(this.#ra[3]-e)/s} L${(this.#ra[4]-t)/i} ${(this.#ra[5]-e)/s} L${n} ${a} L${r} ${o} L${(this.#ra[16]-t)/i} ${(this.#ra[17]-e)/s} L${(this.#ra[14]-t)/i} ${(this.#ra[15]-e)/s} Z`}#ya(t){const e=this.#ia;t.push(`L${e[4]} ${e[5]} Z`)}#wa(t){const[e,i,s,n]=this.#ea,a=this.#ra.subarray(4,6),r=this.#ra.subarray(16,18),[o,l,h,d]=this.#ba();t.push(`L${(a[0]-e)/s} ${(a[1]-i)/n} L${o} ${l} L${h} ${d} L${(r[0]-e)/s} ${(r[1]-i)/n}`)}newFreeDrawOutline(t,e,i,s,n,a){return new FreeDrawOutline(t,e,i,s,n,a)}getOutlines(){const t=this.#aa,e=this.#ia,i=this.#ra,[s,n,a,r]=this.#ea,o=new Float32Array((this.#pa?.length??0)+2);for(let t=0,e=o.length-2;t=6;t-=6)for(let i=0;i<6;i+=2)if(isNaN(e[t+i])){l[h]=l[h+1]=NaN;h+=2}else{l[h]=e[t+i];l[h+1]=e[t+i+1];h+=2}this.#Ea(l,h);return this.newFreeDrawOutline(l,o,this.#ea,this.#ca,this.#sa,this.#na)}#va(t){const e=this.#ra,[i,s,n,a]=this.#ea,[r,o,l,h]=this.#ba(),d=new Float32Array(36);d.set([NaN,NaN,NaN,NaN,(e[2]-i)/n,(e[3]-s)/a,NaN,NaN,NaN,NaN,(e[4]-i)/n,(e[5]-s)/a,NaN,NaN,NaN,NaN,r,o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,(e[16]-i)/n,(e[17]-s)/a,NaN,NaN,NaN,NaN,(e[14]-i)/n,(e[15]-s)/a],0);return this.newFreeDrawOutline(d,t,this.#ea,this.#ca,this.#sa,this.#na)}#Ea(t,e){const i=this.#ia;t.set([NaN,NaN,NaN,NaN,i[4],i[5]],e);return e+6}#xa(t,e){const i=this.#ra.subarray(4,6),s=this.#ra.subarray(16,18),[n,a,r,o]=this.#ea,[l,h,d,c]=this.#ba();t.set([NaN,NaN,NaN,NaN,(i[0]-n)/r,(i[1]-a)/o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,d,c,NaN,NaN,NaN,NaN,(s[0]-n)/r,(s[1]-a)/o],e);return e+24}}class FreeDrawOutline extends Outline{#ea;#_a=new Float32Array(4);#sa;#na;#pa;#ca;#Sa;constructor(t,e,i,s,n,a){super();this.#Sa=t;this.#pa=e;this.#ea=i;this.#ca=s;this.#sa=n;this.#na=a;this.lastPoint=[NaN,NaN];this.#Ca(a);const[r,o,l,h]=this.#_a;for(let e=0,i=t.length;et[0]-e[0]||t[1]-e[1]||t[2]-e[2]));const t=[];for(const e of this.#Ma)if(e[3]){t.push(...this.#Pa(e));this.#ka(e)}else{this.#Ia(e);t.push(...this.#Pa(e))}return this.#Ra(t)}#Ra(t){const e=[],i=new Set;for(const i of t){const[t,s,n]=i;e.push([t,s,i],[t,n,i])}e.sort(((t,e)=>t[1]-e[1]||t[0]-e[0]));for(let t=0,s=e.length;t0;){const t=i.values().next().value;let[e,a,r,o,l]=t;i.delete(t);let h=e,d=a;n=[e,r];s.push(n);for(;;){let t;if(i.has(o))t=o;else{if(!i.has(l))break;t=l}i.delete(t);[e,a,r,o,l]=t;if(h!==e){n.push(h,d,e,d===a?a:r);h=e}d=d===a?r:a}n.push(h,d)}return new HighlightOutline(s,this.#ea,this.#Ta)}#Fa(t){const e=this.#Da;let i=0,s=e.length-1;for(;i<=s;){const n=i+s>>1,a=e[n][0];if(a===t)return n;a=0;s--){const[i,n]=this.#Da[s];if(i!==t)break;if(i===t&&n===e){this.#Da.splice(s,1);return}}}#Pa(t){const[e,i,s]=t,n=[[e,i,s]],a=this.#Fa(s);for(let t=0;t=i)if(o>s)n[t][1]=s;else{if(1===a)return[];n.splice(t,1);t--;a--}else{n[t][2]=i;o>s&&n.push([e,s,o])}}}return n}}class HighlightOutline extends Outline{#ea;#La;constructor(t,e,i){super();this.#La=t;this.#ea=e;this.lastPoint=i}toSVGPath(){const t=[];for(const e of this.#La){let[i,s]=e;t.push(`M${i} ${s}`);for(let n=2;n-1){this.#ar=!0;this.#dr(t);this.#cr()}else if(this.#Ja){this.#Ya=t.anchorNode;this.#Qa=t.anchorOffset;this.#er=t.focusNode;this.#ir=t.focusOffset;this.#ur();this.#cr();this.rotate(this.rotation)}}get telemetryInitialData(){return{action:"added",type:this.#ar?"free_highlight":"highlight",color:this._uiManager.highlightColorNames.get(this.color),thickness:this.#ua,methodOfCreation:this.#hr}}get telemetryFinalData(){return{type:"highlight",color:this._uiManager.highlightColorNames.get(this.color)}}static computeTelemetryFinalData(t){return{numberOfColors:t.get("color").size}}#ur(){const t=new HighlightOutliner(this.#Ja,.001);this.#nr=t.getOutlines();[this.x,this.y,this.width,this.height]=this.#nr.box;const e=new HighlightOutliner(this.#Ja,.0025,.001,"ltr"===this._uiManager.direction);this.#tr=e.getOutlines();const{lastPoint:i}=this.#tr;this.#Ta=[(i[0]-this.x)/this.width,(i[1]-this.y)/this.height]}#dr({highlightOutlines:t,highlightId:e,clipPathId:i}){this.#nr=t;this.#tr=t.getNewOutline(this.#ua/2+1.5,.0025);if(e>=0){this.#y=e;this.#Za=i;this.parent.drawLayer.finalizeDraw(e,{bbox:t.box,path:{d:t.toSVGPath()}});this.#or=this.parent.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:!0},bbox:this.#tr.box,path:{d:this.#tr.toSVGPath()}},!0)}else if(this.parent){const e=this.parent.viewport.rotation;this.parent.drawLayer.updateProperties(this.#y,{bbox:HighlightEditor.#pr(this.#nr.box,(e-this.rotation+360)%360),path:{d:t.toSVGPath()}});this.parent.drawLayer.updateProperties(this.#or,{bbox:HighlightEditor.#pr(this.#tr.box,e),path:{d:this.#tr.toSVGPath()}})}const[s,n,a,r]=t.box;switch(this.rotation){case 0:this.x=s;this.y=n;this.width=a;this.height=r;break;case 90:{const[t,e]=this.parentDimensions;this.x=n;this.y=1-s;this.width=a*e/t;this.height=r*t/e;break}case 180:this.x=1-s;this.y=1-n;this.width=a;this.height=r;break;case 270:{const[t,e]=this.parentDimensions;this.x=1-n;this.y=s;this.width=a*e/t;this.height=r*t/e;break}}const{lastPoint:o}=this.#tr;this.#Ta=[(o[0]-s)/a,(o[1]-n)/r]}static initialize(t,e){AnnotationEditor.initialize(t,e);HighlightEditor._defaultColor||=e.highlightColors?.values().next().value||"#fff066"}static updateDefaultParams(t,e){switch(t){case p.HIGHLIGHT_DEFAULT_COLOR:HighlightEditor._defaultColor=e;break;case p.HIGHLIGHT_THICKNESS:HighlightEditor._defaultThickness=e}}translateInPage(t,e){}get toolbarPosition(){return this.#Ta}updateParams(t,e){switch(t){case p.HIGHLIGHT_COLOR:this.#qn(e);break;case p.HIGHLIGHT_THICKNESS:this.#gr(e)}}static get defaultPropertiesToUpdate(){return[[p.HIGHLIGHT_DEFAULT_COLOR,HighlightEditor._defaultColor],[p.HIGHLIGHT_THICKNESS,HighlightEditor._defaultThickness]]}get propertiesToUpdate(){return[[p.HIGHLIGHT_COLOR,this.color||HighlightEditor._defaultColor],[p.HIGHLIGHT_THICKNESS,this.#ua||HighlightEditor._defaultThickness],[p.HIGHLIGHT_FREE,this.#ar]]}#qn(t){const setColorAndOpacity=(t,e)=>{this.color=t;this.#rr=e;this.parent?.drawLayer.updateProperties(this.#y,{root:{fill:t,"fill-opacity":e}});this.#n?.updateColor(t)},e=this.color,i=this.#rr;this.addCommands({cmd:setColorAndOpacity.bind(this,t,HighlightEditor._defaultOpacity),undo:setColorAndOpacity.bind(this,e,i),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.HIGHLIGHT_COLOR,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"color_changed",color:this._uiManager.highlightColorNames.get(t)},!0)}#gr(t){const e=this.#ua,setThickness=t=>{this.#ua=t;this.#fr(t)};this.addCommands({cmd:setThickness.bind(this,t),undo:setThickness.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.INK_THICKNESS,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"thickness_changed",thickness:t},!0)}async addEditToolbar(){const t=await super.addEditToolbar();if(!t)return null;if(this._uiManager.highlightColors){this.#n=new ColorPicker({editor:this});t.addColorPicker(this.#n)}return t}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}fixAndSetPosition(){return super.fixAndSetPosition(this.#mr())}getBaseTranslation(){return[0,0]}getRect(t,e){return super.getRect(t,e,this.#mr())}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);t&&this.div.focus()}remove(){this.#br();this._reportTelemetry({action:"deleted"});super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#cr();this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t)this.#br();else if(t){this.#cr(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);this.show(this._isVisible);e&&this.select()}#fr(t){if(!this.#ar)return;this.#dr({highlightOutlines:this.#nr.getNewOutline(t/2)});this.fixAndSetPosition();const[e,i]=this.parentDimensions;this.setDims(this.width*e,this.height*i)}#br(){if(null!==this.#y&&this.parent){this.parent.drawLayer.remove(this.#y);this.#y=null;this.parent.drawLayer.remove(this.#or);this.#or=null}}#cr(t=this.parent){if(null===this.#y){({id:this.#y,clipPathId:this.#Za}=t.drawLayer.draw({bbox:this.#nr.box,root:{viewBox:"0 0 1 1",fill:this.color,"fill-opacity":this.#rr},rootClass:{highlight:!0,free:this.#ar},path:{d:this.#nr.toSVGPath()}},!1,!0));this.#or=t.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:this.#ar},bbox:this.#tr.box,path:{d:this.#tr.toSVGPath()}},this.#ar);this.#sr&&(this.#sr.style.clipPath=this.#Za)}}static#pr([t,e,i,s],n){switch(n){case 90:return[1-e-s,t,s,i];case 180:return[1-t-i,1-e-s,i,s];case 270:return[e,1-t-i,s,i]}return[t,e,i,s]}rotate(t){const{drawLayer:e}=this.parent;let i;if(this.#ar){t=(t-this.rotation+360)%360;i=HighlightEditor.#pr(this.#nr.box,t)}else i=HighlightEditor.#pr([this.x,this.y,this.width,this.height],t);e.updateProperties(this.#y,{bbox:i,root:{"data-main-rotation":t}});e.updateProperties(this.#or,{bbox:HighlightEditor.#pr(this.#tr.box,t),root:{"data-main-rotation":t}})}render(){if(this.div)return this.div;const t=super.render();if(this.#lr){t.setAttribute("aria-label",this.#lr);t.setAttribute("role","mark")}this.#ar?t.classList.add("free"):this.div.addEventListener("keydown",this.#Ar.bind(this),{signal:this._uiManager._signal});const e=this.#sr=document.createElement("div");t.append(e);e.setAttribute("aria-hidden","true");e.className="internal";e.style.clipPath=this.#Za;const[i,s]=this.parentDimensions;this.setDims(this.width*i,this.height*s);bindEvents(this,this.#sr,["pointerover","pointerleave"]);this.enableEditing();return t}pointerover(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#or,{rootClass:{hovered:!0}})}pointerleave(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#or,{rootClass:{hovered:!1}})}#Ar(t){HighlightEditor._keyboardManager.exec(this,t)}_moveCaret(t){this.parent.unselect(this);switch(t){case 0:case 2:this.#wr(!0);break;case 1:case 3:this.#wr(!1)}}#wr(t){if(!this.#Ya)return;const e=window.getSelection();t?e.setPosition(this.#Ya,this.#Qa):e.setPosition(this.#er,this.#ir)}select(){super.select();this.#or&&this.parent?.drawLayer.updateProperties(this.#or,{rootClass:{hovered:!1,selected:!0}})}unselect(){super.unselect();if(this.#or){this.parent?.drawLayer.updateProperties(this.#or,{rootClass:{selected:!1}});this.#ar||this.#wr(!1)}}get _mustFixPosition(){return!this.#ar}show(t=this._isVisible){super.show(t);if(this.parent){this.parent.drawLayer.updateProperties(this.#y,{rootClass:{hidden:!t}});this.parent.drawLayer.updateProperties(this.#or,{rootClass:{hidden:!t}})}}#mr(){return this.#ar?this.rotation:0}#yr(){if(this.#ar)return null;const[t,e]=this.pageDimensions,[i,s]=this.pageTranslation,n=this.#Ja,a=new Float32Array(8*n.length);let r=0;for(const{x:o,y:l,width:h,height:d}of n){const n=o*t+i,c=(1-l)*e+s;a[r]=a[r+4]=n;a[r+1]=a[r+3]=c;a[r+2]=a[r+6]=n+h*t;a[r+5]=a[r+7]=c-d*e;r+=8}return a}#vr(t){return this.#nr.serialize(t,this.#mr())}static startHighlighting(t,e,{target:i,x:s,y:n}){const{x:a,y:r,width:o,height:l}=i.getBoundingClientRect(),h=new AbortController,d=t.combinedSignal(h),pointerUpCallback=e=>{h.abort();this.#xr(t,e)};window.addEventListener("blur",pointerUpCallback,{signal:d});window.addEventListener("pointerup",pointerUpCallback,{signal:d});window.addEventListener("pointerdown",stopEvent,{capture:!0,passive:!1,signal:d});window.addEventListener("contextmenu",noContextMenu,{signal:d});i.addEventListener("pointermove",this.#Er.bind(this,t),{signal:d});this._freeHighlight=new FreeHighlightOutliner({x:s,y:n},[a,r,o,l],t.scale,this._defaultThickness/2,e,.001);({id:this._freeHighlightId,clipPathId:this._freeHighlightClipId}=t.drawLayer.draw({bbox:[0,0,1,1],root:{viewBox:"0 0 1 1",fill:this._defaultColor,"fill-opacity":this._defaultOpacity},rootClass:{highlight:!0,free:!0},path:{d:this._freeHighlight.toSVGPath()}},!0,!0))}static#Er(t,e){this._freeHighlight.add(e)&&t.drawLayer.updateProperties(this._freeHighlightId,{path:{d:this._freeHighlight.toSVGPath()}})}static#xr(t,e){this._freeHighlight.isEmpty()?t.drawLayer.remove(this._freeHighlightId):t.createAndAddNewEditor(e,!1,{highlightId:this._freeHighlightId,highlightOutlines:this._freeHighlight.getOutlines(),clipPathId:this._freeHighlightClipId,methodOfCreation:"main_toolbar"});this._freeHighlightId=-1;this._freeHighlight=null;this._freeHighlightClipId=""}static async deserialize(t,e,i){let s=null;if(t instanceof HighlightAnnotationElement){const{data:{quadPoints:e,rect:i,rotation:n,id:a,color:r,opacity:o,popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:u.HIGHLIGHT,color:Array.from(r),opacity:o,quadPoints:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}else if(t instanceof InkAnnotationElement){const{data:{inkLists:e,rect:i,rotation:n,id:a,color:r,borderStyle:{rawWidth:o},popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:u.HIGHLIGHT,color:Array.from(r),thickness:o,inkLists:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}const{color:n,quadPoints:a,inkLists:r,opacity:o}=t,l=await super.deserialize(t,e,i);l.color=Util.makeHexColor(...n);l.#rr=o||1;r&&(l.#ua=t.thickness);l.annotationElementId=t.id||null;l._initialData=s;const[h,d]=l.pageDimensions,[c,p]=l.pageTranslation;if(a){const t=l.#Ja=[];for(let e=0;et!==e[i]))}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class DrawingOptions{#_r=Object.create(null);updateProperty(t,e){this[t]=e;this.updateSVGProperty(t,e)}updateProperties(t){if(t)for(const[e,i]of Object.entries(t))e.startsWith("_")||this.updateProperty(e,i)}updateSVGProperty(t,e){this.#_r[t]=e}toSVGProperties(){const t=this.#_r;this.#_r=Object.create(null);return{root:t}}reset(){this.#_r=Object.create(null)}updateAll(t=this){this.updateProperties(t)}clone(){unreachable("Not implemented")}}class DrawingEditor extends AnnotationEditor{#Sr=null;#Cr;_drawId=null;static _currentDrawId=-1;static _currentParent=null;static#Tr=null;static#Mr=null;static#Dr=null;static#Pr=NaN;static#kr=null;static#Ir=null;static#Rr=NaN;static _INNER_MARGIN=3;constructor(t){super(t);this.#Cr=t.mustBeCommitted||!1;this._addOutlines(t)}_addOutlines(t){if(t.drawOutlines){this.#Fr(t);this.#cr()}}#Fr({drawOutlines:t,drawId:e,drawingOptions:i}){this.#Sr=t;this._drawingOptions||=i;if(e>=0){this._drawId=e;this.parent.drawLayer.finalizeDraw(e,t.defaultProperties)}else this._drawId=this.#Lr(t,this.parent);this.#Or(t.box)}#Lr(t,e){const{id:i}=e.drawLayer.draw(DrawingEditor._mergeSVGProperties(this._drawingOptions.toSVGProperties(),t.defaultSVGProperties),!1,!1);return i}static _mergeSVGProperties(t,e){const i=new Set(Object.keys(t));for(const[s,n]of Object.entries(e))i.has(s)?Object.assign(t[s],n):t[s]=n;return t}static getDefaultDrawingOptions(t){unreachable("Not implemented")}static get typesMap(){unreachable("Not implemented")}static get isDrawer(){return!0}static get supportMultipleDrawings(){return!1}static updateDefaultParams(t,e){const i=this.typesMap.get(t);i&&this._defaultDrawingOptions.updateProperty(i,e);if(this._currentParent){DrawingEditor.#Tr.updateProperty(i,e);this._currentParent.drawLayer.updateProperties(this._currentDrawId,this._defaultDrawingOptions.toSVGProperties())}}updateParams(t,e){const i=this.constructor.typesMap.get(t);i&&this._updateProperty(t,i,e)}static get defaultPropertiesToUpdate(){const t=[],e=this._defaultDrawingOptions;for(const[i,s]of this.typesMap)t.push([i,e[s]]);return t}get propertiesToUpdate(){const t=[],{_drawingOptions:e}=this;for(const[i,s]of this.constructor.typesMap)t.push([i,e[s]]);return t}_updateProperty(t,e,i){const s=this._drawingOptions,n=s[e],setter=t=>{s.updateProperty(e,t);const i=this.#Sr.updateProperty(e,t);i&&this.#Or(i);this.parent?.drawLayer.updateProperties(this._drawId,s.toSVGProperties())};this.addCommands({cmd:setter.bind(this,i),undo:setter.bind(this,n),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:t,overwriteIfSameType:!0,keepUndo:!0})}_onResizing(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#Sr.getPathResizingSVGProperties(this.#Nr()),{bbox:this.#Br()}))}_onResized(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#Sr.getPathResizedSVGProperties(this.#Nr()),{bbox:this.#Br()}))}_onTranslating(t,e){this.parent?.drawLayer.updateProperties(this._drawId,{bbox:this.#Br()})}_onTranslated(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#Sr.getPathTranslatedSVGProperties(this.#Nr(),this.parentDimensions),{bbox:this.#Br()}))}_onStartDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!0}})}_onStopDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!1}})}commit(){super.commit();this.disableEditMode();this.disableEditing()}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}getBaseTranslation(){return[0,0]}get isResizable(){return!0}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);this._isDraggable=!0;if(this.#Cr){this.#Cr=!1;this.commit();this.parent.setSelected(this);t&&this.isOnScreen&&this.div.focus()}}remove(){this.#br();super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#cr();this.#Or(this.#Sr.box);this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t){this._uiManager.removeShouldRescale(this);this.#br()}else if(t){this._uiManager.addShouldRescale(this);this.#cr(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);e&&this.select()}#br(){if(null!==this._drawId&&this.parent){this.parent.drawLayer.remove(this._drawId);this._drawId=null;this._drawingOptions.reset()}}#cr(t=this.parent){if(null===this._drawId||this.parent!==t)if(null===this._drawId){this._drawingOptions.updateAll();this._drawId=this.#Lr(this.#Sr,t)}else this.parent.drawLayer.updateParent(this._drawId,t.drawLayer)}#Hr([t,e,i,s]){const{parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[e,1-t,i*(a/n),s*(n/a)];case 180:return[1-t,1-e,i,s];case 270:return[1-e,t,i*(a/n),s*(n/a)];default:return[t,e,i,s]}}#Nr(){const{x:t,y:e,width:i,height:s,parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[1-e,t,i*(n/a),s*(a/n)];case 180:return[1-t,1-e,i,s];case 270:return[e,1-t,i*(n/a),s*(a/n)];default:return[t,e,i,s]}}#Or(t){[this.x,this.y,this.width,this.height]=this.#Hr(t);if(this.div){this.fixAndSetPosition();const[t,e]=this.parentDimensions;this.setDims(this.width*t,this.height*e)}this._onResized()}#Br(){const{x:t,y:e,width:i,height:s,rotation:n,parentRotation:a,parentDimensions:[r,o]}=this;switch((4*n+a)/90){case 1:return[1-e-s,t,s,i];case 2:return[1-t-i,1-e-s,i,s];case 3:return[e,1-t-i,s,i];case 4:return[t,e-i*(r/o),s*(o/r),i*(r/o)];case 5:return[1-e,t,i*(r/o),s*(o/r)];case 6:return[1-t-s*(o/r),1-e,s*(o/r),i*(r/o)];case 7:return[e-i*(r/o),1-t-s*(o/r),i*(r/o),s*(o/r)];case 8:return[t-i,e-s,i,s];case 9:return[1-e,t-i,s,i];case 10:return[1-t,1-e,i,s];case 11:return[e-s,1-t,s,i];case 12:return[t-s*(o/r),e,s*(o/r),i*(r/o)];case 13:return[1-e-i*(r/o),t-s*(o/r),i*(r/o),s*(o/r)];case 14:return[1-t,1-e-i*(r/o),s*(o/r),i*(r/o)];case 15:return[e,1-t,i*(r/o),s*(o/r)];default:return[t,e,i,s]}}rotate(){this.parent&&this.parent.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties({bbox:this.#Br()},this.#Sr.updateRotation((this.parentRotation-this.rotation+360)%360)))}onScaleChanging(){this.parent&&this.#Or(this.#Sr.updateParentDimensions(this.parentDimensions,this.parent.scale))}static onScaleChangingWhenDrawing(){}render(){if(this.div)return this.div;let t,e;if(this._isCopy){t=this.x;e=this.y}const i=super.render();i.classList.add("draw");const s=document.createElement("div");i.append(s);s.setAttribute("aria-hidden","true");s.className="internal";const[n,a]=this.parentDimensions;this.setDims(this.width*n,this.height*a);this._uiManager.addShouldRescale(this);this.disableEditing();this._isCopy&&this._moveAfterPaste(t,e);return i}static createDrawerInstance(t,e,i,s,n){unreachable("Not implemented")}static startDrawing(t,e,i,s){const{target:n,offsetX:a,offsetY:r,pointerId:o,pointerType:l}=s;if(DrawingEditor.#kr&&DrawingEditor.#kr!==l)return;const{viewport:{rotation:h}}=t,{width:d,height:c}=n.getBoundingClientRect(),u=DrawingEditor.#Mr=new AbortController,p=t.combinedSignal(u);DrawingEditor.#Pr||=o;DrawingEditor.#kr??=l;window.addEventListener("pointerup",(t=>{DrawingEditor.#Pr===t.pointerId?this._endDraw(t):DrawingEditor.#Ir?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointercancel",(t=>{DrawingEditor.#Pr===t.pointerId?this._currentParent.endDrawingSession():DrawingEditor.#Ir?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointerdown",(t=>{if(DrawingEditor.#kr===t.pointerType){(DrawingEditor.#Ir||=new Set).add(t.pointerId);if(DrawingEditor.#Tr.isCancellable()){DrawingEditor.#Tr.removeLastElement();DrawingEditor.#Tr.isEmpty()?this._currentParent.endDrawingSession(!0):this._endDraw(null)}}}),{capture:!0,passive:!1,signal:p});window.addEventListener("contextmenu",noContextMenu,{signal:p});n.addEventListener("pointermove",this._drawMove.bind(this),{signal:p});n.addEventListener("touchmove",(t=>{t.timeStamp===DrawingEditor.#Rr&&stopEvent(t)}),{signal:p});t.toggleDrawing();e._editorUndoBar?.hide();if(DrawingEditor.#Tr)t.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Tr.startNew(a,r,d,c,h));else{e.updateUIForDefaultProperties(this);DrawingEditor.#Tr=this.createDrawerInstance(a,r,d,c,h);DrawingEditor.#Dr=this.getDefaultDrawingOptions();this._currentParent=t;({id:this._currentDrawId}=t.drawLayer.draw(this._mergeSVGProperties(DrawingEditor.#Dr.toSVGProperties(),DrawingEditor.#Tr.defaultSVGProperties),!0,!1))}}static _drawMove(t){DrawingEditor.#Rr=-1;if(!DrawingEditor.#Tr)return;const{offsetX:e,offsetY:i,pointerId:s}=t;if(DrawingEditor.#Pr===s)if(DrawingEditor.#Ir?.size>=1)this._endDraw(t);else{this._currentParent.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Tr.add(e,i));DrawingEditor.#Rr=t.timeStamp;stopEvent(t)}}static _cleanup(t){if(t){this._currentDrawId=-1;this._currentParent=null;DrawingEditor.#Tr=null;DrawingEditor.#Dr=null;DrawingEditor.#kr=null;DrawingEditor.#Rr=NaN}if(DrawingEditor.#Mr){DrawingEditor.#Mr.abort();DrawingEditor.#Mr=null;DrawingEditor.#Pr=NaN;DrawingEditor.#Ir=null}}static _endDraw(t){const e=this._currentParent;if(e){e.toggleDrawing(!0);this._cleanup(!1);t?.target===e.div&&e.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Tr.end(t.offsetX,t.offsetY));if(this.supportMultipleDrawings){const t=DrawingEditor.#Tr,i=this._currentDrawId,s=t.getLastElement();e.addCommands({cmd:()=>{e.drawLayer.updateProperties(i,t.setLastElement(s))},undo:()=>{e.drawLayer.updateProperties(i,t.removeLastElement())},mustExec:!1,type:p.DRAW_STEP})}else this.endDrawing(!1)}}static endDrawing(t){const e=this._currentParent;if(!e)return null;e.toggleDrawing(!0);e.cleanUndoStack(p.DRAW_STEP);if(!DrawingEditor.#Tr.isEmpty()){const{pageDimensions:[i,s],scale:n}=e,a=e.createAndAddNewEditor({offsetX:0,offsetY:0},!1,{drawId:this._currentDrawId,drawOutlines:DrawingEditor.#Tr.getOutlines(i*n,s*n,n,this._INNER_MARGIN),drawingOptions:DrawingEditor.#Dr,mustBeCommitted:!t});this._cleanup(!0);return a}e.drawLayer.remove(this._currentDrawId);this._cleanup(!0);return null}createDrawingOptions(t){}static deserializeDraw(t,e,i,s,n,a){unreachable("Not implemented")}static async deserialize(t,e,i){const{rawDims:{pageWidth:s,pageHeight:n,pageX:a,pageY:r}}=e.viewport,o=this.deserializeDraw(a,r,s,n,this._INNER_MARGIN,t),l=await super.deserialize(t,e,i);l.createDrawingOptions(t);l.#Fr({drawOutlines:o});l.#cr();l.onScaleChanging();l.rotate();return l}serializeDraw(t){const[e,i]=this.pageTranslation,[s,n]=this.pageDimensions;return this.#Sr.serialize([e,i,s,n],t)}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class InkDrawOutliner{#ra=new Float64Array(6);#Mn;#Ur;#as;#ua;#pa;#zr="";#Gr=0;#La=new InkDrawOutline;#$r;#Wr;constructor(t,e,i,s,n,a){this.#$r=i;this.#Wr=s;this.#as=n;this.#ua=a;[t,e]=this.#Vr(t,e);const r=this.#Mn=[NaN,NaN,NaN,NaN,t,e];this.#pa=[t,e];this.#Ur=[{line:r,points:this.#pa}];this.#ra.set(r,0)}updateProperty(t,e){"stroke-width"===t&&(this.#ua=e)}#Vr(t,e){return Outline._normalizePoint(t,e,this.#$r,this.#Wr,this.#as)}isEmpty(){return!this.#Ur||0===this.#Ur.length}isCancellable(){return this.#pa.length<=10}add(t,e){[t,e]=this.#Vr(t,e);const[i,s,n,a]=this.#ra.subarray(2,6),r=t-n,o=e-a;if(Math.hypot(this.#$r*r,this.#Wr*o)<=2)return null;this.#pa.push(t,e);if(isNaN(i)){this.#ra.set([n,a,t,e],2);this.#Mn.push(NaN,NaN,NaN,NaN,t,e);return{path:{d:this.toSVGPath()}}}isNaN(this.#ra[0])&&this.#Mn.splice(6,6);this.#ra.set([i,s,n,a,t,e],0);this.#Mn.push(...Outline.createBezierPoints(i,s,n,a,t,e));return{path:{d:this.toSVGPath()}}}end(t,e){const i=this.add(t,e);return i||(2===this.#pa.length?{path:{d:this.toSVGPath()}}:null)}startNew(t,e,i,s,n){this.#$r=i;this.#Wr=s;this.#as=n;[t,e]=this.#Vr(t,e);const a=this.#Mn=[NaN,NaN,NaN,NaN,t,e];this.#pa=[t,e];const r=this.#Ur.at(-1);if(r){r.line=new Float32Array(r.line);r.points=new Float32Array(r.points)}this.#Ur.push({line:a,points:this.#pa});this.#ra.set(a,0);this.#Gr=0;this.toSVGPath();return null}getLastElement(){return this.#Ur.at(-1)}setLastElement(t){if(!this.#Ur)return this.#La.setLastElement(t);this.#Ur.push(t);this.#Mn=t.line;this.#pa=t.points;this.#Gr=0;return{path:{d:this.toSVGPath()}}}removeLastElement(){if(!this.#Ur)return this.#La.removeLastElement();this.#Ur.pop();this.#zr="";for(let t=0,e=this.#Ur.length;tt??NaN)),d,c,u,p),points:g(r[t].map((t=>t??NaN)),d,c,u,p)});const f=new this.prototype.constructor;f.build(h,i,s,1,o,l,n);return f}#Yr(t=this.#ua){const e=this.#sa+t/2*this.#qr;return this.#as%180==0?[e/this.#$r,e/this.#Wr]:[e/this.#Wr,e/this.#$r]}#Kr(){const[t,e,i,s]=this.#_a,[n,a]=this.#Yr(0);return[t+n,e+a,i-2*n,s-2*a]}#Xr(){const t=this.#_a=new Float32Array([1/0,1/0,-1/0,-1/0]);for(const{line:e}of this.#Ur){if(e.length<=12){for(let i=4,s=e.length;it!==e[i]))||t.thickness!==i||t.opacity!==s||t.pageIndex!==n}renderAnnotationElement(t){const{points:e,rect:i}=this.serializeDraw(!1);t.updateEdited({rect:i,thickness:this._drawingOptions["stroke-width"],points:e});return null}}class ContourDrawOutline extends InkDrawOutline{toSVGPath(){let t=super.toSVGPath();t.endsWith("Z")||(t+="Z");return t}}class SignatureExtractor{static#Qr={maxDim:512,sigmaSFactor:.02,sigmaR:25,kernelSize:16};static#Jr(t,e,i,s){s-=e;return 0===(i-=t)?s>0?0:4:1===i?s+6:2-s}static#Zr=new Int32Array([0,1,-1,1,-1,0,-1,-1,0,-1,1,-1,1,0,1,1]);static#to(t,e,i,s,n,a,r){const o=this.#Jr(i,s,n,a);for(let n=0;n<8;n++){const a=(-n+o-r+16)%8;if(0!==t[(i+this.#Zr[2*a])*e+(s+this.#Zr[2*a+1])])return a}return-1}static#eo(t,e,i,s,n,a,r){const o=this.#Jr(i,s,n,a);for(let n=0;n<8;n++){const a=(n+o+r+16)%8;if(0!==t[(i+this.#Zr[2*a])*e+(s+this.#Zr[2*a+1])])return a}return-1}static#io(t,e,i,s){const n=t.length,a=new Int32Array(n);for(let e=0;e=1&&0===a[s+1])){1!==n&&(r=Math.abs(n));continue}o+=1;d+=1;n>1&&(r=n)}const c=[i,t],u=d===i+1,p={isHole:u,points:c,id:o,parent:0};l.push(p);let g;for(const t of l)if(t.id===r){g=t;break}g?g.isHole?p.parent=u?g.parent:r:p.parent=u?r:g.parent:p.parent=u?r:0;const f=this.#to(a,e,t,i,h,d,0);if(-1===f){a[s]=-o;1!==a[s]&&(r=Math.abs(a[s]));continue}let m=this.#Zr[2*f],b=this.#Zr[2*f+1];const A=t+m,w=i+b;h=A;d=w;let y=t,v=i;for(;;){const n=this.#eo(a,e,y,v,h,d,1);m=this.#Zr[2*n];b=this.#Zr[2*n+1];const l=y+m,u=v+b;c.push(u,l);const p=y*e+v;0===a[p+1]?a[p]=-o:1===a[p]&&(a[p]=o);if(l===t&&u===i&&y===A&&v===w){1!==a[s]&&(r=Math.abs(a[s]));break}h=y;d=v;y=l;v=u}}}return l}static#so(t,e,i,s){if(i-e<=4){for(let n=e;ny){v=s;y=e}}if(y>(l*w)**2){this.#so(t,e,v+2,s);this.#so(t,v,i,s)}else s.push(n,a)}static#no(t){const e=[],i=t.length;this.#so(t,0,i,e);e.push(t[i-2],t[i-1]);return e.length<=4?null:e}static#ao(t,e,i,s,n,a){const r=new Float32Array(a**2),o=-2*s**2,l=a>>1;for(let t=0;t=i))for(let i=0;i=e)continue;const p=t[u*e+s],f=r[o*a+i]*h[Math.abs(p-d)];c+=p*f;g+=f}}p[u[o]=Math.round(c/g)]++}return[u,p]}static#ro(t){const e=new Uint32Array(256);for(const i of t)e[i]++;return e}static#oo(t){const e=t.length,i=new Uint8ClampedArray(e>>2);let s=-1/0,n=1/0;for(let e=0,a=i.length;es&&(s=a);a0!==t));let a=n,r=n;for(e=n;e<256;e++){const n=t[e];if(n>i){if(e-a>s){s=e-a;r=e-1}i=n;a=e}}for(e=r-1;e>=0&&!(t[e]>t[e+1]);e--);return e}static#ho(t){const e=t,{width:i,height:s}=t,{maxDim:n}=this.#Qr;let a=i,r=s;if(i>n||s>n){let o=i,l=s,h=Math.log2(Math.max(i,s)/n);const d=Math.floor(h);h=h===d?d-1:d;for(let i=0;i=-128&&o<=127?Int8Array:r>=-32768&&o<=32767?Int16Array:Int32Array;const h=t.length,d=8+3*h,c=new Uint32Array(d);let u=0;c[u++]=d*Uint32Array.BYTES_PER_ELEMENT+(l-2*h)*a.BYTES_PER_ELEMENT;c[u++]=0;c[u++]=s;c[u++]=n;c[u++]=e?0:1;c[u++]=Math.max(0,Math.floor(i??0));c[u++]=h;c[u++]=a.BYTES_PER_ELEMENT;for(const e of t){c[u++]=e.length-2;c[u++]=e[0];c[u++]=e[1]}const p=new CompressionStream("deflate-raw"),g=p.writable.getWriter();await g.ready;g.write(c);const f=a.prototype.constructor;for(const e of t){const t=new f(e.length-2);for(let i=2,s=e.length;i{await n.ready;await n.close()})).catch((()=>{}));let a=null,r=0;for await(const t of i){a||=new Uint8Array(new Uint32Array(t.buffer,0,4)[0]);a.set(t,r);r+=t.length}const o=new Uint32Array(a.buffer,0,a.length>>2),l=o[1];if(0!==l)throw new Error(`Invalid version: ${l}`);const h=o[2],d=o[3],c=0===o[4],u=o[5],p=o[6],g=o[7],f=[],m=(8+3*p)*Uint32Array.BYTES_PER_ELEMENT;let b;switch(g){case Int8Array.BYTES_PER_ELEMENT:b=new Int8Array(a.buffer,m);break;case Int16Array.BYTES_PER_ELEMENT:b=new Int16Array(a.buffer,m);break;case Int32Array.BYTES_PER_ELEMENT:b=new Int32Array(a.buffer,m)}r=0;for(let t=0;t{e?.updateEditSignatureButton(t)}))}getSignaturePreview(){const{newCurves:t,areContours:e,thickness:i,width:s,height:n}=this.#uo,a=Math.max(s,n);return{areContours:e,outline:SignatureExtractor.processDrawnLines({lines:{curves:t.map((t=>({points:t}))),thickness:i,width:s,height:n},pageWidth:a,pageHeight:a,rotation:0,innerMargin:0,mustSmooth:!1,areContours:e}).outline}}async addEditToolbar(){const t=await super.addEditToolbar();if(!t)return null;if(this._uiManager.signatureManager&&null!==this.#co){await t.addEditSignatureButton(this._uiManager.signatureManager,this.#po,this.#co);t.show()}return t}addSignature(t,e,i,s){const{x:n,y:a}=this,{outline:r}=this.#uo=t;this.#do=r instanceof ContourDrawOutline;this.#co=i;this.div.setAttribute("data-l10n-args",JSON.stringify({description:i}));let o;if(this.#do)o=SignatureEditor.getDefaultDrawingOptions();else{o=SignatureEditor._defaultDrawnSignatureOptions.clone();o.updateProperties({"stroke-width":r.thickness})}this._addOutlines({drawOutlines:r,drawingOptions:o});const[l,h]=this.parentDimensions,[,d]=this.pageDimensions;let c=e/d;c=c>=1?.5:c;this.width*=c/this.height;if(this.width>=1){c*=.9/this.width;this.width=.9}this.height=c;this.setDims(l*this.width,h*this.height);this.x=n;this.y=a;this.center();this._onResized();this.onScaleChanging();this.rotate();this._uiManager.addToAnnotationStorage(this);this.setUuid(s);this._reportTelemetry({action:"pdfjs.signature.inserted",data:{hasBeenSaved:!!s,hasDescription:!!i}});this.div.hidden=!1}getFromImage(t){const{rawDims:{pageWidth:e,pageHeight:i},rotation:s}=this.parent.viewport;return SignatureExtractor.process(t,e,i,s,SignatureEditor._INNER_MARGIN)}getFromText(t,e){const{rawDims:{pageWidth:i,pageHeight:s},rotation:n}=this.parent.viewport;return SignatureExtractor.extractContoursFromText(t,e,i,s,n,SignatureEditor._INNER_MARGIN)}getDrawnSignature(t){const{rawDims:{pageWidth:e,pageHeight:i},rotation:s}=this.parent.viewport;return SignatureExtractor.processDrawnLines({lines:t,pageWidth:e,pageHeight:i,rotation:s,innerMargin:SignatureEditor._INNER_MARGIN,mustSmooth:!1,areContours:!1})}createDrawingOptions({areContours:t,thickness:e}){if(t)this._drawingOptions=SignatureEditor.getDefaultDrawingOptions();else{this._drawingOptions=SignatureEditor._defaultDrawnSignatureOptions.clone();this._drawingOptions.updateProperties({"stroke-width":e})}}serialize(t=!1){if(this.isEmpty())return null;const{lines:e,points:i,rect:s}=this.serializeDraw(t),{_drawingOptions:{"stroke-width":n}}=this,a={annotationType:u.SIGNATURE,isSignature:!0,areContours:this.#do,color:[0,0,0],thickness:this.#do?0:n,pageIndex:this.pageIndex,rect:s,rotation:this.rotation,structTreeParentId:this._structTreeParentId};if(t){a.paths={lines:e,points:i};a.uuid=this.#po;a.isCopy=!0}else a.lines=e;this.#co&&(a.accessibilityData={type:"Figure",alt:this.#co});return a}static deserializeDraw(t,e,i,s,n,a){return a.areContours?ContourDrawOutline.deserialize(t,e,i,s,n,a):InkDrawOutline.deserialize(t,e,i,s,n,a)}static async deserialize(t,e,i){const s=await super.deserialize(t,e,i);s.#do=t.areContours;s.#co=t.accessibilityData?.alt||"";s.#po=t.uuid;return s}}class StampEditor extends AnnotationEditor{#go=null;#fo=null;#mo=null;#bo=null;#Ao=null;#wo="";#yo=null;#vo=!1;#xo=null;#Eo=!1;#_o=!1;static _type="stamp";static _editorType=u.STAMP;constructor(t){super({...t,name:"stampEditor"});this.#bo=t.bitmapUrl;this.#Ao=t.bitmapFile;this.defaultL10nId="pdfjs-editor-stamp-editor"}static initialize(t,e){AnnotationEditor.initialize(t,e)}static isHandlingMimeForPasting(t){return G.includes(t)}static paste(t,e){e.pasteEditor({mode:u.STAMP},{bitmapFile:t.getAsFile()})}altTextFinish(){this._uiManager.useNewAltTextFlow&&(this.div.hidden=!1);super.altTextFinish()}get telemetryFinalData(){return{type:"stamp",hasAltText:!!this.altTextData?.altText}}static computeTelemetryFinalData(t){const e=t.get("hasAltText");return{hasAltText:e.get(!0)??0,hasNoAltText:e.get(!1)??0}}#So(t,e=!1){if(t){this.#go=t.bitmap;if(!e){this.#fo=t.id;this.#Eo=t.isSvg}t.file&&(this.#wo=t.file.name);this.#Co()}else this.remove()}#To(){this.#mo=null;this._uiManager.enableWaiting(!1);if(this.#yo)if(this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#go){this._editToolbar.hide();this._uiManager.editAltText(this,!0)}else{if(!this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#go){this._reportTelemetry({action:"pdfjs.image.image_added",data:{alt_text_modal:!1,alt_text_type:"empty"}});try{this.mlGuessAltText()}catch{}}this.div.focus()}}async mlGuessAltText(t=null,e=!0){if(this.hasAltTextData())return null;const{mlManager:i}=this._uiManager;if(!i)throw new Error("No ML.");if(!await i.isEnabledFor("altText"))throw new Error("ML isn't enabled for alt text.");const{data:s,width:n,height:a}=t||this.copyCanvas(null,null,!0).imageData,r=await i.guess({name:"altText",request:{data:s,width:n,height:a,channels:s.length/(n*a)}});if(!r)throw new Error("No response from the AI service.");if(r.error)throw new Error("Error from the AI service.");if(r.cancel)return null;if(!r.output)throw new Error("No valid response from the AI service.");const o=r.output;await this.setGuessedAltText(o);e&&!this.hasAltTextData()&&(this.altTextData={alt:o,decorative:!1});return o}#Mo(){if(this.#fo){this._uiManager.enableWaiting(!0);this._uiManager.imageManager.getFromId(this.#fo).then((t=>this.#So(t,!0))).finally((()=>this.#To()));return}if(this.#bo){const t=this.#bo;this.#bo=null;this._uiManager.enableWaiting(!0);this.#mo=this._uiManager.imageManager.getFromUrl(t).then((t=>this.#So(t))).finally((()=>this.#To()));return}if(this.#Ao){const t=this.#Ao;this.#Ao=null;this._uiManager.enableWaiting(!0);this.#mo=this._uiManager.imageManager.getFromFile(t).then((t=>this.#So(t))).finally((()=>this.#To()));return}const t=document.createElement("input");t.type="file";t.accept=G.join(",");const e=this._uiManager._signal;this.#mo=new Promise((i=>{t.addEventListener("change",(async()=>{if(t.files&&0!==t.files.length){this._uiManager.enableWaiting(!0);const e=await this._uiManager.imageManager.getFromFile(t.files[0]);this._reportTelemetry({action:"pdfjs.image.image_selected",data:{alt_text_modal:this._uiManager.useNewAltTextFlow}});this.#So(e)}else this.remove();i()}),{signal:e});t.addEventListener("cancel",(()=>{this.remove();i()}),{signal:e})})).finally((()=>this.#To()));t.click()}remove(){if(this.#fo){this.#go=null;this._uiManager.imageManager.deleteId(this.#fo);this.#yo?.remove();this.#yo=null;if(this.#xo){clearTimeout(this.#xo);this.#xo=null}}super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#fo&&null===this.#yo&&this.#Mo();this.isAttachedToDOM||this.parent.add(this)}}else this.#fo&&this.#Mo()}onceAdded(t){this._isDraggable=!0;t&&this.div.focus()}isEmpty(){return!(this.#mo||this.#go||this.#bo||this.#Ao||this.#fo||this.#vo)}get isResizable(){return!0}render(){if(this.div)return this.div;let t,e;if(this._isCopy){t=this.x;e=this.y}super.render();this.div.hidden=!0;this.addAltTextButton();this.#vo||(this.#go?this.#Co():this.#Mo());this._isCopy&&this._moveAfterPaste(t,e);this._uiManager.addShouldRescale(this);return this.div}setCanvas(t,e){const{id:i,bitmap:s}=this._uiManager.imageManager.getFromCanvas(t,e);e.remove();if(i&&this._uiManager.imageManager.isValidId(i)){this.#fo=i;s&&(this.#go=s);this.#vo=!1;this.#Co()}}_onResized(){this.onScaleChanging()}onScaleChanging(){if(!this.parent)return;null!==this.#xo&&clearTimeout(this.#xo);this.#xo=setTimeout((()=>{this.#xo=null;this.#Do()}),200)}#Co(){const{div:t}=this;let{width:e,height:i}=this.#go;const[s,n]=this.pageDimensions,a=.75;if(this.width){e=this.width*s;i=this.height*n}else if(e>a*s||i>a*n){const t=Math.min(a*s/e,a*n/i);e*=t;i*=t}const[r,o]=this.parentDimensions;this.setDims(e*r/s,i*o/n);this._uiManager.enableWaiting(!1);const l=this.#yo=document.createElement("canvas");l.setAttribute("role","img");this.addContainer(l);this.width=e/s;this.height=i/n;this._initialOptions?.isCentered?this.center():this.fixAndSetPosition();this._initialOptions=null;this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&!this.annotationElementId||(t.hidden=!1);this.#Do();if(!this.#_o){this.parent.addUndoableEditor(this);this.#_o=!0}this._reportTelemetry({action:"inserted_image"});this.#wo&&this.div.setAttribute("aria-description",this.#wo)}copyCanvas(t,e,i=!1){t||(t=224);const{width:s,height:n}=this.#go,a=new OutputScale;let r=this.#go,o=s,l=n,h=null;if(e){if(s>e||n>e){const t=Math.min(e/s,e/n);o=Math.floor(s*t);l=Math.floor(n*t)}h=document.createElement("canvas");const t=h.width=Math.ceil(o*a.sx),i=h.height=Math.ceil(l*a.sy);this.#Eo||(r=this.#Po(t,i));const d=h.getContext("2d");d.filter=this._uiManager.hcmFilter;let c="white",u="#cfcfd8";if("none"!==this._uiManager.hcmFilter)u="black";else if(window.matchMedia?.("(prefers-color-scheme: dark)").matches){c="#8f8f9d";u="#42414d"}const p=15,g=p*a.sx,f=p*a.sy,m=new OffscreenCanvas(2*g,2*f),b=m.getContext("2d");b.fillStyle=c;b.fillRect(0,0,2*g,2*f);b.fillStyle=u;b.fillRect(0,0,g,f);b.fillRect(g,f,g,f);d.fillStyle=d.createPattern(m,"repeat");d.fillRect(0,0,t,i);d.drawImage(r,0,0,r.width,r.height,0,0,t,i)}let d=null;if(i){let e,i;if(a.symmetric&&r.widtht||n>t){const a=Math.min(t/s,t/n);e=Math.floor(s*a);i=Math.floor(n*a);this.#Eo||(r=this.#Po(e,i))}}const o=new OffscreenCanvas(e,i).getContext("2d",{willReadFrequently:!0});o.drawImage(r,0,0,r.width,r.height,0,0,e,i);d={width:e,height:i,data:o.getImageData(0,0,e,i).data}}return{canvas:h,width:o,height:l,imageData:d}}#Po(t,e){const{width:i,height:s}=this.#go;let n=i,a=s,r=this.#go;for(;n>2*t||a>2*e;){const i=n,s=a;n>2*t&&(n=n>=16384?Math.floor(n/2)-1:Math.ceil(n/2));a>2*e&&(a=a>=16384?Math.floor(a/2)-1:Math.ceil(a/2));const o=new OffscreenCanvas(n,a);o.getContext("2d").drawImage(r,0,0,i,s,0,0,n,a);r=o.transferToImageBitmap()}return r}#Do(){const[t,e]=this.parentDimensions,{width:i,height:s}=this,n=new OutputScale,a=Math.ceil(i*t*n.sx),r=Math.ceil(s*e*n.sy),o=this.#yo;if(!o||o.width===a&&o.height===r)return;o.width=a;o.height=r;const l=this.#Eo?this.#go:this.#Po(a,r),h=o.getContext("2d");h.filter=this._uiManager.hcmFilter;h.drawImage(l,0,0,l.width,l.height,0,0,a,r)}#ko(t){if(t){if(this.#Eo){const t=this._uiManager.imageManager.getSvgUrl(this.#fo);if(t)return t}const t=document.createElement("canvas");({width:t.width,height:t.height}=this.#go);t.getContext("2d").drawImage(this.#go,0,0);return t.toDataURL()}if(this.#Eo){const[t,e]=this.pageDimensions,i=Math.round(this.width*t*PixelsPerInch.PDF_TO_CSS_UNITS),s=Math.round(this.height*e*PixelsPerInch.PDF_TO_CSS_UNITS),n=new OffscreenCanvas(i,s);n.getContext("2d").drawImage(this.#go,0,0,this.#go.width,this.#go.height,0,0,i,s);return n.transferToImageBitmap()}return structuredClone(this.#go)}static async deserialize(t,e,i){let s=null,n=!1;if(t instanceof StampAnnotationElement){const{data:{rect:a,rotation:r,id:o,structParent:l,popupRef:h},container:d,parent:{page:{pageNumber:c}},canvas:p}=t;let g,f;if(p){delete t.canvas;({id:g,bitmap:f}=i.imageManager.getFromCanvas(d.id,p));p.remove()}else{n=!0;t._hasNoCanvas=!0}const m=(await e._structTree.getAriaAttributes(`${U}${o}`))?.get("aria-label")||"";s=t={annotationType:u.STAMP,bitmapId:g,bitmap:f,pageIndex:c-1,rect:a.slice(0),rotation:r,id:o,deleted:!1,accessibilityData:{decorative:!1,altText:m},isSvg:!1,structParent:l,popupRef:h}}const a=await super.deserialize(t,e,i),{rect:r,bitmap:o,bitmapUrl:l,bitmapId:h,isSvg:d,accessibilityData:c}=t;if(n){i.addMissingCanvas(t.id,a);a.#vo=!0}else if(h&&i.imageManager.isValidId(h)){a.#fo=h;o&&(a.#go=o)}else a.#bo=l;a.#Eo=d;const[p,g]=a.pageDimensions;a.width=(r[2]-r[0])/p;a.height=(r[3]-r[1])/g;a.annotationElementId=t.id||null;c&&(a.altTextData=c);a._initialData=s;a.#_o=!!s;return a}serialize(t=!1,e=null){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const i={annotationType:u.STAMP,bitmapId:this.#fo,pageIndex:this.pageIndex,rect:this.getRect(0,0),rotation:this.rotation,isSvg:this.#Eo,structTreeParentId:this._structTreeParentId};if(t){i.bitmapUrl=this.#ko(!0);i.accessibilityData=this.serializeAltText(!0);i.isCopy=!0;return i}const{decorative:s,altText:n}=this.serializeAltText(!1);!s&&n&&(i.accessibilityData={type:"Figure",alt:n});if(this.annotationElementId){const t=this.#ta(i);if(t.isSame)return null;t.isSameAltText?delete i.accessibilityData:i.accessibilityData.structParent=this._initialData.structParent??-1}i.id=this.annotationElementId;if(null===e)return i;e.stamps||=new Map;const a=this.#Eo?(i.rect[2]-i.rect[0])*(i.rect[3]-i.rect[1]):null;if(e.stamps.has(this.#fo)){if(this.#Eo){const t=e.stamps.get(this.#fo);if(a>t.area){t.area=a;t.serialized.bitmap.close();t.serialized.bitmap=this.#ko(!1)}}}else{e.stamps.set(this.#fo,{area:a,serialized:i});i.bitmap=this.#ko(!1)}return i}#ta(t){const{pageIndex:e,accessibilityData:{altText:i}}=this._initialData,s=t.pageIndex===e,n=(t.accessibilityData?.alt||"")===i;return{isSame:!this._hasBeenMoved&&!this._hasBeenResized&&s&&n,isSameAltText:n}}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}}class AnnotationEditorLayer{#Nn;#Io=!1;#Ro=null;#Fo=null;#Lo=null;#Oo=new Map;#No=!1;#Bo=!1;#Ho=!1;#Uo=null;#zo=null;#Go=null;#$o=null;#m;static _initialized=!1;static#G=new Map([FreeTextEditor,InkEditor,StampEditor,HighlightEditor,SignatureEditor].map((t=>[t._editorType,t])));constructor({uiManager:t,pageIndex:e,div:i,structTreeLayer:s,accessibilityManager:n,annotationLayer:a,drawLayer:r,textLayer:o,viewport:l,l10n:h}){const d=[...AnnotationEditorLayer.#G.values()];if(!AnnotationEditorLayer._initialized){AnnotationEditorLayer._initialized=!0;for(const e of d)e.initialize(h,t)}t.registerEditorTypes(d);this.#m=t;this.pageIndex=e;this.div=i;this.#Nn=n;this.#Ro=a;this.viewport=l;this.#Go=o;this.drawLayer=r;this._structTree=s;this.#m.addLayer(this)}get isEmpty(){return 0===this.#Oo.size}get isInvisible(){return this.isEmpty&&this.#m.getMode()===u.NONE}updateToolbar(t){this.#m.updateToolbar(t)}updateMode(t=this.#m.getMode()){this.#Wo();switch(t){case u.NONE:this.disableTextSelection();this.togglePointerEvents(!1);this.toggleAnnotationLayerPointerEvents(!0);this.disableClick();return;case u.INK:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick();break;case u.HIGHLIGHT:this.enableTextSelection();this.togglePointerEvents(!1);this.disableClick();break;default:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick()}this.toggleAnnotationLayerPointerEvents(!1);const{classList:e}=this.div;for(const i of AnnotationEditorLayer.#G.values())e.toggle(`${i._type}Editing`,t===i._editorType);this.div.hidden=!1}hasTextLayer(t){return t===this.#Go?.div}setEditingState(t){this.#m.setEditingState(t)}addCommands(t){this.#m.addCommands(t)}cleanUndoStack(t){this.#m.cleanUndoStack(t)}toggleDrawing(t=!1){this.div.classList.toggle("drawing",!t)}togglePointerEvents(t=!1){this.div.classList.toggle("disabled",!t)}toggleAnnotationLayerPointerEvents(t=!1){this.#Ro?.div.classList.toggle("disabled",!t)}async enable(){this.#Ho=!0;this.div.tabIndex=0;this.togglePointerEvents(!0);const t=new Set;for(const e of this.#Oo.values()){e.enableEditing();e.show(!0);if(e.annotationElementId){this.#m.removeChangedExistingAnnotation(e);t.add(e.annotationElementId)}}if(!this.#Ro){this.#Ho=!1;return}const e=this.#Ro.getEditableAnnotations();for(const i of e){i.hide();if(this.#m.isDeletedAnnotationElement(i.data.id))continue;if(t.has(i.data.id))continue;const e=await this.deserialize(i);if(e){this.addOrRebuild(e);e.enableEditing()}}this.#Ho=!1}disable(){this.#Bo=!0;this.div.tabIndex=-1;this.togglePointerEvents(!1);const t=new Map,e=new Map;for(const i of this.#Oo.values()){i.disableEditing();if(i.annotationElementId)if(null===i.serialize()){e.set(i.annotationElementId,i);this.getEditableAnnotation(i.annotationElementId)?.show();i.remove()}else t.set(i.annotationElementId,i)}if(this.#Ro){const i=this.#Ro.getEditableAnnotations();for(const s of i){const{id:i}=s.data;if(this.#m.isDeletedAnnotationElement(i))continue;let n=e.get(i);if(n){n.resetAnnotationElement(s);n.show(!1);s.show()}else{n=t.get(i);if(n){this.#m.addChangedExistingAnnotation(n);n.renderAnnotationElement(s)&&n.show(!1)}s.show()}}}this.#Wo();this.isEmpty&&(this.div.hidden=!0);const{classList:i}=this.div;for(const t of AnnotationEditorLayer.#G.values())i.remove(`${t._type}Editing`);this.disableTextSelection();this.toggleAnnotationLayerPointerEvents(!0);this.#Bo=!1}getEditableAnnotation(t){return this.#Ro?.getEditableAnnotation(t)||null}setActiveEditor(t){this.#m.getActive()!==t&&this.#m.setActiveEditor(t)}enableTextSelection(){this.div.tabIndex=-1;if(this.#Go?.div&&!this.#$o){this.#$o=new AbortController;const t=this.#m.combinedSignal(this.#$o);this.#Go.div.addEventListener("pointerdown",this.#Vo.bind(this),{signal:t});this.#Go.div.classList.add("highlighting")}}disableTextSelection(){this.div.tabIndex=0;if(this.#Go?.div&&this.#$o){this.#$o.abort();this.#$o=null;this.#Go.div.classList.remove("highlighting")}}#Vo(t){this.#m.unselectAll();const{target:e}=t;if(e===this.#Go.div||("img"===e.getAttribute("role")||e.classList.contains("endOfContent"))&&this.#Go.div.contains(e)){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;this.#m.showAllEditors("highlight",!0,!0);this.#Go.div.classList.add("free");this.toggleDrawing();HighlightEditor.startHighlighting(this,"ltr"===this.#m.direction,{target:this.#Go.div,x:t.x,y:t.y});this.#Go.div.addEventListener("pointerup",(()=>{this.#Go.div.classList.remove("free");this.toggleDrawing(!0)}),{once:!0,signal:this.#m._signal});t.preventDefault()}}enableClick(){if(this.#Fo)return;this.#Fo=new AbortController;const t=this.#m.combinedSignal(this.#Fo);this.div.addEventListener("pointerdown",this.pointerdown.bind(this),{signal:t});const e=this.pointerup.bind(this);this.div.addEventListener("pointerup",e,{signal:t});this.div.addEventListener("pointercancel",e,{signal:t})}disableClick(){this.#Fo?.abort();this.#Fo=null}attach(t){this.#Oo.set(t.id,t);const{annotationElementId:e}=t;e&&this.#m.isDeletedAnnotationElement(e)&&this.#m.removeDeletedAnnotationElement(t)}detach(t){this.#Oo.delete(t.id);this.#Nn?.removePointerInTextLayer(t.contentDiv);!this.#Bo&&t.annotationElementId&&this.#m.addDeletedAnnotationElement(t)}remove(t){this.detach(t);this.#m.removeEditor(t);t.div.remove();t.isAttachedToDOM=!1}changeParent(t){if(t.parent!==this){if(t.parent&&t.annotationElementId){this.#m.addDeletedAnnotationElement(t.annotationElementId);AnnotationEditor.deleteAnnotationElement(t);t.annotationElementId=null}this.attach(t);t.parent?.detach(t);t.setParent(this);if(t.div&&t.isAttachedToDOM){t.div.remove();this.div.append(t.div)}}}add(t){if(t.parent!==this||!t.isAttachedToDOM){this.changeParent(t);this.#m.addEditor(t);this.attach(t);if(!t.isAttachedToDOM){const e=t.render();this.div.append(e);t.isAttachedToDOM=!0}t.fixAndSetPosition();t.onceAdded(!this.#Ho);this.#m.addToAnnotationStorage(t);t._reportTelemetry(t.telemetryInitialData)}}moveEditorInDOM(t){if(!t.isAttachedToDOM)return;const{activeElement:e}=document;if(t.div.contains(e)&&!this.#Lo){t._focusEventsAllowed=!1;this.#Lo=setTimeout((()=>{this.#Lo=null;if(t.div.contains(document.activeElement))t._focusEventsAllowed=!0;else{t.div.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this.#m._signal});e.focus()}}),0)}t._structTreeParentId=this.#Nn?.moveElementInDOM(this.div,t.div,t.contentDiv,!0)}addOrRebuild(t){if(t.needsToBeRebuilt()){t.parent||=this;t.rebuild();t.show()}else this.add(t)}addUndoableEditor(t){this.addCommands({cmd:()=>t._uiManager.rebuild(t),undo:()=>{t.remove()},mustExec:!1})}getNextId(){return this.#m.getId()}get#jo(){return AnnotationEditorLayer.#G.get(this.#m.getMode())}combinedSignal(t){return this.#m.combinedSignal(t)}#qo(t){const e=this.#jo;return e?new e.prototype.constructor(t):null}canCreateNewEmptyEditor(){return this.#jo?.canCreateNewEmptyEditor()}async pasteEditor(t,e){this.updateToolbar(t);await this.#m.updateMode(t.mode);const{offsetX:i,offsetY:s}=this.#Xo(),n=this.getNextId(),a=this.#qo({parent:this,id:n,x:i,y:s,uiManager:this.#m,isCentered:!0,...e});a&&this.add(a)}async deserialize(t){return await(AnnotationEditorLayer.#G.get(t.annotationType??t.annotationEditorType)?.deserialize(t,this,this.#m))||null}createAndAddNewEditor(t,e,i={}){const s=this.getNextId(),n=this.#qo({parent:this,id:s,x:t.offsetX,y:t.offsetY,uiManager:this.#m,isCentered:e,...i});n&&this.add(n);return n}#Xo(){const{x:t,y:e,width:i,height:s}=this.div.getBoundingClientRect(),n=Math.max(0,t),a=Math.max(0,e),r=(n+Math.min(window.innerWidth,t+i))/2-t,o=(a+Math.min(window.innerHeight,e+s))/2-e,[l,h]=this.viewport.rotation%180==0?[r,o]:[o,r];return{offsetX:l,offsetY:h}}addNewEditor(t={}){this.createAndAddNewEditor(this.#Xo(),!0,t)}setSelected(t){this.#m.setSelected(t)}toggleSelected(t){this.#m.toggleSelected(t)}unselect(t){this.#m.unselect(t)}pointerup(t){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;if(!this.#No)return;this.#No=!1;if(this.#jo?.isDrawer&&this.#jo.supportMultipleDrawings)return;if(!this.#Io){this.#Io=!0;return}const i=this.#m.getMode();i!==u.STAMP&&i!==u.SIGNATURE?this.createAndAddNewEditor(t,!1):this.#m.unselectAll()}pointerdown(t){this.#m.getMode()===u.HIGHLIGHT&&this.enableTextSelection();if(this.#No){this.#No=!1;return}const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;this.#No=!0;if(this.#jo?.isDrawer){this.startDrawingSession(t);return}const i=this.#m.getActive();this.#Io=!i||i.isEmpty()}startDrawingSession(t){this.div.focus({preventScroll:!0});if(this.#Uo){this.#jo.startDrawing(this,this.#m,!1,t);return}this.#m.setCurrentDrawingSession(this);this.#Uo=new AbortController;const e=this.#m.combinedSignal(this.#Uo);this.div.addEventListener("blur",(({relatedTarget:t})=>{if(t&&!this.div.contains(t)){this.#zo=null;this.commitOrRemove()}}),{signal:e});this.#jo.startDrawing(this,this.#m,!1,t)}pause(t){if(t){const{activeElement:t}=document;this.div.contains(t)&&(this.#zo=t)}else this.#zo&&setTimeout((()=>{this.#zo?.focus();this.#zo=null}),0)}endDrawingSession(t=!1){if(!this.#Uo)return null;this.#m.setCurrentDrawingSession(null);this.#Uo.abort();this.#Uo=null;this.#zo=null;return this.#jo.endDrawing(t)}findNewParent(t,e,i){const s=this.#m.findParent(e,i);if(null===s||s===this)return!1;s.changeParent(t);return!0}commitOrRemove(){if(this.#Uo){this.endDrawingSession();return!0}return!1}onScaleChanging(){this.#Uo&&this.#jo.onScaleChangingWhenDrawing(this)}destroy(){this.commitOrRemove();if(this.#m.getActive()?.parent===this){this.#m.commitOrRemove();this.#m.setActiveEditor(null)}if(this.#Lo){clearTimeout(this.#Lo);this.#Lo=null}for(const t of this.#Oo.values()){this.#Nn?.removePointerInTextLayer(t.contentDiv);t.setParent(null);t.isAttachedToDOM=!1;t.div.remove()}this.div=null;this.#Oo.clear();this.#m.removeLayer(this)}#Wo(){for(const t of this.#Oo.values())t.isEmpty()&&t.remove()}render({viewport:t}){this.viewport=t;setLayerDimensions(this.div,t);for(const t of this.#m.getEditors(this.pageIndex)){this.add(t);t.rebuild()}this.updateMode()}update({viewport:t}){this.#m.commitOrRemove();this.#Wo();const e=this.viewport.rotation,i=t.rotation;this.viewport=t;setLayerDimensions(this.div,{rotation:i});if(e!==i)for(const t of this.#Oo.values())t.rotate(i)}get pageDimensions(){const{pageWidth:t,pageHeight:e}=this.viewport.rawDims;return[t,e]}get scale(){return this.#m.viewParameters.realScale}}class DrawLayer{#fn=null;#Ko=new Map;#Yo=new Map;static#y=0;constructor({pageIndex:t}){this.pageIndex=t}setParent(t){if(this.#fn){if(this.#fn!==t){if(this.#Ko.size>0)for(const e of this.#Ko.values()){e.remove();t.append(e)}this.#fn=t}}else this.#fn=t}static get _svgFactory(){return shadow(this,"_svgFactory",new DOMSVGFactory)}static#Qo(t,[e,i,s,n]){const{style:a}=t;a.top=100*i+"%";a.left=100*e+"%";a.width=100*s+"%";a.height=100*n+"%"}#Jo(){const t=DrawLayer._svgFactory.create(1,1,!0);this.#fn.append(t);t.setAttribute("aria-hidden",!0);return t}#Zo(t,e){const i=DrawLayer._svgFactory.createElement("clipPath");t.append(i);const s=`clip_${e}`;i.setAttribute("id",s);i.setAttribute("clipPathUnits","objectBoundingBox");const n=DrawLayer._svgFactory.createElement("use");i.append(n);n.setAttribute("href",`#${e}`);n.classList.add("clip");return s}#tl(t,e){for(const[i,s]of Object.entries(e))null===s?t.removeAttribute(i):t.setAttribute(i,s)}draw(t,e=!1,i=!1){const s=DrawLayer.#y++,n=this.#Jo(),a=DrawLayer._svgFactory.createElement("defs");n.append(a);const r=DrawLayer._svgFactory.createElement("path");a.append(r);const o=`path_p${this.pageIndex}_${s}`;r.setAttribute("id",o);r.setAttribute("vector-effect","non-scaling-stroke");e&&this.#Yo.set(s,r);const l=i?this.#Zo(a,o):null,h=DrawLayer._svgFactory.createElement("use");n.append(h);h.setAttribute("href",`#${o}`);this.updateProperties(n,t);this.#Ko.set(s,n);return{id:s,clipPathId:`url(#${l})`}}drawOutline(t,e){const i=DrawLayer.#y++,s=this.#Jo(),n=DrawLayer._svgFactory.createElement("defs");s.append(n);const a=DrawLayer._svgFactory.createElement("path");n.append(a);const r=`path_p${this.pageIndex}_${i}`;a.setAttribute("id",r);a.setAttribute("vector-effect","non-scaling-stroke");let o;if(e){const t=DrawLayer._svgFactory.createElement("mask");n.append(t);o=`mask_p${this.pageIndex}_${i}`;t.setAttribute("id",o);t.setAttribute("maskUnits","objectBoundingBox");const e=DrawLayer._svgFactory.createElement("rect");t.append(e);e.setAttribute("width","1");e.setAttribute("height","1");e.setAttribute("fill","white");const s=DrawLayer._svgFactory.createElement("use");t.append(s);s.setAttribute("href",`#${r}`);s.setAttribute("stroke","none");s.setAttribute("fill","black");s.setAttribute("fill-rule","nonzero");s.classList.add("mask")}const l=DrawLayer._svgFactory.createElement("use");s.append(l);l.setAttribute("href",`#${r}`);o&&l.setAttribute("mask",`url(#${o})`);const h=l.cloneNode();s.append(h);l.classList.add("mainOutline");h.classList.add("secondaryOutline");this.updateProperties(s,t);this.#Ko.set(i,s);return i}finalizeDraw(t,e){this.#Yo.delete(t);this.updateProperties(t,e)}updateProperties(t,e){if(!e)return;const{root:i,bbox:s,rootClass:n,path:a}=e,r="number"==typeof t?this.#Ko.get(t):t;if(r){i&&this.#tl(r,i);s&&DrawLayer.#Qo(r,s);if(n){const{classList:t}=r;for(const[e,i]of Object.entries(n))t.toggle(e,i)}if(a){const t=r.firstChild.firstChild;this.#tl(t,a)}}}updateParent(t,e){if(e===this)return;const i=this.#Ko.get(t);if(i){e.#fn.append(i);this.#Ko.delete(t);e.#Ko.set(t,i)}}remove(t){this.#Yo.delete(t);if(null!==this.#fn){this.#Ko.get(t).remove();this.#Ko.delete(t)}}destroy(){this.#fn=null;for(const t of this.#Ko.values())t.remove();this.#Ko.clear();this.#Yo.clear()}}globalThis._pdfjsTestingUtils={HighlightOutliner};globalThis.pdfjsLib={AbortException,AnnotationEditorLayer,AnnotationEditorParamsType:p,AnnotationEditorType:u,AnnotationEditorUIManager,AnnotationLayer,AnnotationMode:c,AnnotationType:x,build:vt,ColorPicker,createValidAbsoluteUrl,DOMSVGFactory,DrawLayer,FeatureTest:util_FeatureTest,fetchData,getDocument,getFilenameFromUrl,getPdfFilenameFromUrl,getUuid,getXfaPageViewport,GlobalWorkerOptions,ImageKind:v,InvalidPDFException,isDataScheme,isPdfFile,isValidExplicitDest:q,MathClamp,noContextMenu,normalizeUnicode,OPS:D,OutputScale,PasswordResponses:F,PDFDataRangeTransport,PDFDateString,PDFWorker,PermissionFlag:g,PixelsPerInch,RenderingCancelledException,ResponseException,setLayerDimensions,shadow,SignatureExtractor,stopEvent,SupportedImageMimeTypes:G,TextLayer,TouchManager,updateUrlHash,Util,VerbosityLevel:M,version:yt,XfaLayer};export{AbortException,AnnotationEditorLayer,p as AnnotationEditorParamsType,u as AnnotationEditorType,AnnotationEditorUIManager,AnnotationLayer,c as AnnotationMode,x as AnnotationType,ColorPicker,DOMSVGFactory,DrawLayer,util_FeatureTest as FeatureTest,GlobalWorkerOptions,v as ImageKind,InvalidPDFException,MathClamp,D as OPS,OutputScale,PDFDataRangeTransport,PDFDateString,PDFWorker,F as PasswordResponses,g as PermissionFlag,PixelsPerInch,RenderingCancelledException,ResponseException,SignatureExtractor,G as SupportedImageMimeTypes,TextLayer,TouchManager,Util,M as VerbosityLevel,XfaLayer,vt as build,createValidAbsoluteUrl,fetchData,getDocument,getFilenameFromUrl,getPdfFilenameFromUrl,getUuid,getXfaPageViewport,isDataScheme,isPdfFile,q as isValidExplicitDest,noContextMenu,normalizeUnicode,setLayerDimensions,shadow,stopEvent,updateUrlHash,yt as version}; \ No newline at end of file diff --git a/src/client/public/js/pdf.worker.min.mjs b/src/client/public/js/pdf.worker.min.mjs new file mode 100644 index 0000000..de36edc --- /dev/null +++ b/src/client/public/js/pdf.worker.min.mjs @@ -0,0 +1,29 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */ +/** + * pdfjsVersion = 5.3.31 + * pdfjsBuild = 47ad820d9 + */ +const e=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),t=[.001,0,0,.001,0,0],a=1.35,r=.35,i=.25925925925925924,n=1,s=2,o=4,c=8,l=16,h=64,u=128,d=256,f="pdfjs_internal_editor_",g=3,p=9,m=13,b=15,y=101,w={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},S=0,x=4,k=1,C=2,v=3,F=1,O=2,T=3,M=4,D=5,R=6,N=7,E=8,L=9,j=10,_=11,U=12,X=13,q=14,H=15,W=16,z=17,$=20,G="Group",V="R",K=1,J=2,Y=4,Z=16,Q=32,ee=128,te=512,ae=1,re=2,ie=4096,ne=8192,se=32768,oe=65536,ce=131072,le=1048576,he=2097152,ue=8388608,de=16777216,fe=1,ge=2,pe=3,me=4,be=5,ye={E:"Mouse Enter",X:"Mouse Exit",D:"Mouse Down",U:"Mouse Up",Fo:"Focus",Bl:"Blur",PO:"PageOpen",PC:"PageClose",PV:"PageVisible",PI:"PageInvisible",K:"Keystroke",F:"Format",V:"Validate",C:"Calculate"},we={WC:"WillClose",WS:"WillSave",DS:"DidSave",WP:"WillPrint",DP:"DidPrint"},Se={O:"PageOpen",C:"PageClose"},xe=1,Ae=5,ke=1,Ce=2,ve=3,Fe=4,Ie=5,Oe=6,Te=7,Me=8,De=9,Be=10,Re=11,Ne=12,Ee=13,Pe=14,Le=15,je=16,_e=17,Ue=18,Xe=19,qe=20,He=21,We=22,ze=23,$e=24,Ge=25,Ve=26,Ke=27,Je=28,Ye=29,Ze=30,Qe=31,et=32,tt=33,at=34,rt=35,it=36,nt=37,st=38,ot=39,ct=40,lt=41,ht=42,ut=43,dt=44,ft=45,gt=46,pt=47,mt=48,bt=49,yt=50,wt=51,St=52,xt=53,At=54,kt=55,Ct=56,vt=57,Ft=58,It=59,Ot=60,Tt=61,Mt=62,Dt=63,Bt=64,Rt=65,Nt=66,Et=67,Pt=68,Lt=69,jt=70,_t=71,Ut=72,Xt=73,qt=74,Ht=75,Wt=76,zt=77,$t=80,Gt=81,Vt=83,Kt=84,Jt=85,Yt=86,Zt=87,Qt=88,ea=89,ta=90,aa=91,ra=92,ia=93,na=94,sa=0,oa=1,ca=2,la=3,ha=1,ua=2;let da=xe;function getVerbosityLevel(){return da}function info(e){da>=Ae&&console.log(`Info: ${e}`)}function warn(e){da>=xe&&console.log(`Warning: ${e}`)}function unreachable(e){throw new Error(e)}function assert(e,t){e||unreachable(t)}function createValidAbsoluteUrl(e,t=null,a=null){if(!e)return null;if(a&&"string"==typeof e){if(a.addDefaultProtocol&&e.startsWith("www.")){const t=e.match(/\./g);t?.length>=2&&(e=`http://${e}`)}if(a.tryConvertEncoding)try{e=stringToUTF8String(e)}catch{}}const r=t?URL.parse(e,t):URL.parse(e);return function _isValidProtocol(e){switch(e?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(r)?r:null}function shadow(e,t,a,r=!1){Object.defineProperty(e,t,{value:a,enumerable:!r,configurable:!0,writable:!1});return a}const fa=function BaseExceptionClosure(){function BaseException(e,t){this.message=e;this.name=t}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends fa{constructor(e,t){super(e,"PasswordException");this.code=t}}class UnknownErrorException extends fa{constructor(e,t){super(e,"UnknownErrorException");this.details=t}}class InvalidPDFException extends fa{constructor(e){super(e,"InvalidPDFException")}}class ResponseException extends fa{constructor(e,t,a){super(e,"ResponseException");this.status=t;this.missing=a}}class FormatError extends fa{constructor(e){super(e,"FormatError")}}class AbortException extends fa{constructor(e){super(e,"AbortException")}}function bytesToString(e){"object"==typeof e&&void 0!==e?.length||unreachable("Invalid argument for bytesToString");const t=e.length,a=8192;if(t>24&255,e>>16&255,e>>8&255,255&e)}function objectSize(e){return Object.keys(e).length}class FeatureTest{static get isLittleEndian(){return shadow(this,"isLittleEndian",function isLittleEndian(){const e=new Uint8Array(4);e[0]=1;return 1===new Uint32Array(e.buffer,0,1)[0]}())}static get isEvalSupported(){return shadow(this,"isEvalSupported",function isEvalSupported(){try{new Function("");return!0}catch{return!1}}())}static get isOffscreenCanvasSupported(){return shadow(this,"isOffscreenCanvasSupported","undefined"!=typeof OffscreenCanvas)}static get isImageDecoderSupported(){return shadow(this,"isImageDecoderSupported","undefined"!=typeof ImageDecoder)}static get platform(){const{platform:e,userAgent:t}=navigator;return shadow(this,"platform",{isAndroid:t.includes("Android"),isLinux:e.includes("Linux"),isMac:e.includes("Mac"),isWindows:e.includes("Win"),isFirefox:t.includes("Firefox")})}static get isCSSRoundSupported(){return shadow(this,"isCSSRoundSupported",globalThis.CSS?.supports?.("width: round(1.5px, 1px)"))}}const ga=Array.from(Array(256).keys(),(e=>e.toString(16).padStart(2,"0")));class Util{static makeHexColor(e,t,a){return`#${ga[e]}${ga[t]}${ga[a]}`}static scaleMinMax(e,t){let a;if(e[0]){if(e[0]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[0];t[2]*=e[0];if(e[3]<0){a=t[1];t[1]=t[3];t[3]=a}t[1]*=e[3];t[3]*=e[3]}else{a=t[0];t[0]=t[1];t[1]=a;a=t[2];t[2]=t[3];t[3]=a;if(e[1]<0){a=t[1];t[1]=t[3];t[3]=a}t[1]*=e[1];t[3]*=e[1];if(e[2]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[2];t[2]*=e[2]}t[0]+=e[4];t[1]+=e[5];t[2]+=e[4];t[3]+=e[5]}static transform(e,t){return[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],e[0]*t[4]+e[2]*t[5]+e[4],e[1]*t[4]+e[3]*t[5]+e[5]]}static applyTransform(e,t,a=0){const r=e[a],i=e[a+1];e[a]=r*t[0]+i*t[2]+t[4];e[a+1]=r*t[1]+i*t[3]+t[5]}static applyTransformToBezier(e,t,a=0){const r=t[0],i=t[1],n=t[2],s=t[3],o=t[4],c=t[5];for(let t=0;t<6;t+=2){const l=e[a+t],h=e[a+t+1];e[a+t]=l*r+h*n+o;e[a+t+1]=l*i+h*s+c}}static applyInverseTransform(e,t){const a=e[0],r=e[1],i=t[0]*t[3]-t[1]*t[2];e[0]=(a*t[3]-r*t[2]+t[2]*t[5]-t[4]*t[3])/i;e[1]=(-a*t[1]+r*t[0]+t[4]*t[1]-t[5]*t[0])/i}static axialAlignedBoundingBox(e,t,a){const r=t[0],i=t[1],n=t[2],s=t[3],o=t[4],c=t[5],l=e[0],h=e[1],u=e[2],d=e[3];let f=r*l+o,g=f,p=r*u+o,m=p,b=s*h+c,y=b,w=s*d+c,S=w;if(0!==i||0!==n){const e=i*l,t=i*u,a=n*h,r=n*d;f+=a;m+=a;p+=r;g+=r;b+=e;S+=e;w+=t;y+=t}a[0]=Math.min(a[0],f,p,g,m);a[1]=Math.min(a[1],b,w,y,S);a[2]=Math.max(a[2],f,p,g,m);a[3]=Math.max(a[3],b,w,y,S)}static inverseTransform(e){const t=e[0]*e[3]-e[1]*e[2];return[e[3]/t,-e[1]/t,-e[2]/t,e[0]/t,(e[2]*e[5]-e[4]*e[3])/t,(e[4]*e[1]-e[5]*e[0])/t]}static singularValueDecompose2dScale(e,t){const a=e[0],r=e[1],i=e[2],n=e[3],s=a**2+r**2,o=a*i+r*n,c=i**2+n**2,l=(s+c)/2,h=Math.sqrt(l**2-(s*c-o**2));t[0]=Math.sqrt(l+h||1);t[1]=Math.sqrt(l-h||1)}static normalizeRect(e){const t=e.slice(0);if(e[0]>e[2]){t[0]=e[2];t[2]=e[0]}if(e[1]>e[3]){t[1]=e[3];t[3]=e[1]}return t}static intersect(e,t){const a=Math.max(Math.min(e[0],e[2]),Math.min(t[0],t[2])),r=Math.min(Math.max(e[0],e[2]),Math.max(t[0],t[2]));if(a>r)return null;const i=Math.max(Math.min(e[1],e[3]),Math.min(t[1],t[3])),n=Math.min(Math.max(e[1],e[3]),Math.max(t[1],t[3]));return i>n?null:[a,i,r,n]}static pointBoundingBox(e,t,a){a[0]=Math.min(a[0],e);a[1]=Math.min(a[1],t);a[2]=Math.max(a[2],e);a[3]=Math.max(a[3],t)}static rectBoundingBox(e,t,a,r,i){i[0]=Math.min(i[0],e,a);i[1]=Math.min(i[1],t,r);i[2]=Math.max(i[2],e,a);i[3]=Math.max(i[3],t,r)}static#e(e,t,a,r,i,n,s,o,c,l){if(c<=0||c>=1)return;const h=1-c,u=c*c,d=u*c,f=h*(h*(h*e+3*c*t)+3*u*a)+d*r,g=h*(h*(h*i+3*c*n)+3*u*s)+d*o;l[0]=Math.min(l[0],f);l[1]=Math.min(l[1],g);l[2]=Math.max(l[2],f);l[3]=Math.max(l[3],g)}static#t(e,t,a,r,i,n,s,o,c,l,h,u){if(Math.abs(c)<1e-12){Math.abs(l)>=1e-12&&this.#e(e,t,a,r,i,n,s,o,-h/l,u);return}const d=l**2-4*h*c;if(d<0)return;const f=Math.sqrt(d),g=2*c;this.#e(e,t,a,r,i,n,s,o,(-l+f)/g,u);this.#e(e,t,a,r,i,n,s,o,(-l-f)/g,u)}static bezierBoundingBox(e,t,a,r,i,n,s,o,c){c[0]=Math.min(c[0],e,s);c[1]=Math.min(c[1],t,o);c[2]=Math.max(c[2],e,s);c[3]=Math.max(c[3],t,o);this.#t(e,a,i,s,t,r,n,o,3*(3*(a-i)-e+s),6*(e-2*a+i),3*(a-e),c);this.#t(e,a,i,s,t,r,n,o,3*(3*(r-n)-t+o),6*(t-2*r+n),3*(r-t),c)}}const pa=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,728,711,710,729,733,731,730,732,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8226,8224,8225,8230,8212,8211,402,8260,8249,8250,8722,8240,8222,8220,8221,8216,8217,8218,8482,64257,64258,321,338,352,376,381,305,322,339,353,382,0,8364];function stringToPDFString(e,t=!1){if(e[0]>="ï"){let a;if("þ"===e[0]&&"ÿ"===e[1]){a="utf-16be";e.length%2==1&&(e=e.slice(0,-1))}else if("ÿ"===e[0]&&"þ"===e[1]){a="utf-16le";e.length%2==1&&(e=e.slice(0,-1))}else"ï"===e[0]&&"»"===e[1]&&"¿"===e[2]&&(a="utf-8");if(a)try{const r=new TextDecoder(a,{fatal:!0}),i=stringToBytes(e),n=r.decode(i);return t||!n.includes("")?n:n.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g,"")}catch(e){warn(`stringToPDFString: "${e}".`)}}const a=[];for(let r=0,i=e.length;rga[e])).join("")}"function"!=typeof Promise.try&&(Promise.try=function(e,...t){return new Promise((a=>{a(e(...t))}))});"function"!=typeof Math.sumPrecise&&(Math.sumPrecise=function(e){return e.reduce(((e,t)=>e+t),0)});const ya=Symbol("CIRCULAR_REF"),wa=Symbol("EOF");let Sa=Object.create(null),xa=Object.create(null),Aa=Object.create(null);class Name{constructor(e){this.name=e}static get(e){return xa[e]||=new Name(e)}}class Cmd{constructor(e){this.cmd=e}static get(e){return Sa[e]||=new Cmd(e)}}const ka=function nonSerializableClosure(){return ka};class Dict{constructor(e=null){this._map=new Map;this.xref=e;this.objId=null;this.suppressEncryption=!1;this.__nonSerializable__=ka}assignXref(e){this.xref=e}get size(){return this._map.size}get(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}return r instanceof Ref&&this.xref?this.xref.fetch(r,this.suppressEncryption):r}async getAsync(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}return r instanceof Ref&&this.xref?this.xref.fetchAsync(r,this.suppressEncryption):r}getArray(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}r instanceof Ref&&this.xref&&(r=this.xref.fetch(r,this.suppressEncryption));if(Array.isArray(r)){r=r.slice();for(let e=0,t=r.length;e{unreachable("Should not call `set` on the empty dictionary.")};return shadow(this,"empty",e)}static merge({xref:e,dictArray:t,mergeSubDicts:a=!1}){const r=new Dict(e),i=new Map;for(const e of t)if(e instanceof Dict)for(const[t,r]of e._map){let e=i.get(t);if(void 0===e){e=[];i.set(t,e)}else if(!(a&&r instanceof Dict))continue;e.push(r)}for(const[t,a]of i){if(1===a.length||!(a[0]instanceof Dict)){r._map.set(t,a[0]);continue}const i=new Dict(e);for(const e of a)for(const[t,a]of e._map)i._map.has(t)||i._map.set(t,a);i.size>0&&r._map.set(t,i)}i.clear();return r.size>0?r:Dict.empty}clone(){const e=new Dict(this.xref);for(const t of this.getKeys())e.set(t,this.getRaw(t));return e}delete(e){delete this._map[e]}}class Ref{constructor(e,t){this.num=e;this.gen=t}toString(){return 0===this.gen?`${this.num}R`:`${this.num}R${this.gen}`}static fromString(e){const t=Aa[e];if(t)return t;const a=/^(\d+)R(\d*)$/.exec(e);return a&&"0"!==a[1]?Aa[e]=new Ref(parseInt(a[1]),a[2]?parseInt(a[2]):0):null}static get(e,t){const a=0===t?`${e}R`:`${e}R${t}`;return Aa[a]||=new Ref(e,t)}}class RefSet{constructor(e=null){this._set=new Set(e?._set)}has(e){return this._set.has(e.toString())}put(e){this._set.add(e.toString())}remove(e){this._set.delete(e.toString())}[Symbol.iterator](){return this._set.values()}clear(){this._set.clear()}}class RefSetCache{constructor(){this._map=new Map}get size(){return this._map.size}get(e){return this._map.get(e.toString())}has(e){return this._map.has(e.toString())}put(e,t){this._map.set(e.toString(),t)}putAlias(e,t){this._map.set(e.toString(),this.get(t))}[Symbol.iterator](){return this._map.values()}clear(){this._map.clear()}*values(){yield*this._map.values()}*items(){for(const[e,t]of this._map)yield[Ref.fromString(e),t]}}function isName(e,t){return e instanceof Name&&(void 0===t||e.name===t)}function isCmd(e,t){return e instanceof Cmd&&(void 0===t||e.cmd===t)}function isDict(e,t){return e instanceof Dict&&(void 0===t||isName(e.get("Type"),t))}function isRefsEqual(e,t){return e.num===t.num&&e.gen===t.gen}class BaseStream{get length(){unreachable("Abstract getter `length` accessed")}get isEmpty(){unreachable("Abstract getter `isEmpty` accessed")}get isDataLoaded(){return shadow(this,"isDataLoaded",!0)}getByte(){unreachable("Abstract method `getByte` called")}getBytes(e){unreachable("Abstract method `getBytes` called")}async getImageData(e,t){return this.getBytes(e,t)}async asyncGetBytes(){unreachable("Abstract method `asyncGetBytes` called")}get isAsync(){return!1}get isAsyncDecoder(){return!1}get canAsyncDecodeImageFromBuffer(){return!1}async getTransferableImage(){return null}peekByte(){const e=this.getByte();-1!==e&&this.pos--;return e}peekBytes(e){const t=this.getBytes(e);this.pos-=t.length;return t}getUint16(){const e=this.getByte(),t=this.getByte();return-1===e||-1===t?-1:(e<<8)+t}getInt32(){return(this.getByte()<<24)+(this.getByte()<<16)+(this.getByte()<<8)+this.getByte()}getByteRange(e,t){unreachable("Abstract method `getByteRange` called")}getString(e){return bytesToString(this.getBytes(e))}skip(e){this.pos+=e||1}reset(){unreachable("Abstract method `reset` called")}moveStart(){unreachable("Abstract method `moveStart` called")}makeSubStream(e,t,a=null){unreachable("Abstract method `makeSubStream` called")}getBaseStreams(){return null}}const Ca=/^[1-9]\.\d$/,va=2**31-1,Fa=[1,0,0,1,0,0],Ia=["ColorSpace","ExtGState","Font","Pattern","Properties","Shading","XObject"],Oa=["ExtGState","Font","Properties","XObject"];function getLookupTableFactory(e){let t;return function(){if(e){t=Object.create(null);e(t);e=null}return t}}class MissingDataException extends fa{constructor(e,t){super(`Missing data [${e}, ${t})`,"MissingDataException");this.begin=e;this.end=t}}class ParserEOFException extends fa{constructor(e){super(e,"ParserEOFException")}}class XRefEntryException extends fa{constructor(e){super(e,"XRefEntryException")}}class XRefParseException extends fa{constructor(e){super(e,"XRefParseException")}}function arrayBuffersToBytes(e){const t=e.length;if(0===t)return new Uint8Array(0);if(1===t)return new Uint8Array(e[0]);let a=0;for(let r=0;r0,"The number should be a positive integer.");const a="M".repeat(e/1e3|0)+Ta[e%1e3/100|0]+Ta[10+(e%100/10|0)]+Ta[20+e%10];return t?a.toLowerCase():a}function log2(e){return e>0?Math.ceil(Math.log2(e)):0}function readInt8(e,t){return e[t]<<24>>24}function readInt16(e,t){return(e[t]<<24|e[t+1]<<16)>>16}function readUint16(e,t){return e[t]<<8|e[t+1]}function readUint32(e,t){return(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0}function isWhiteSpace(e){return 32===e||9===e||13===e||10===e}function isNumberArray(e,t){return Array.isArray(e)?(null===t||e.length===t)&&e.every((e=>"number"==typeof e)):ArrayBuffer.isView(e)&&!(e instanceof BigInt64Array||e instanceof BigUint64Array)&&(null===t||e.length===t)}function lookupMatrix(e,t){return isNumberArray(e,6)?e:t}function lookupRect(e,t){return isNumberArray(e,4)?e:t}function lookupNormalRect(e,t){return isNumberArray(e,4)?Util.normalizeRect(e):t}function parseXFAPath(e){const t=/(.+)\[(\d+)\]$/;return e.split(".").map((e=>{const a=e.match(t);return a?{name:a[1],pos:parseInt(a[2],10)}:{name:e,pos:0}}))}function escapePDFName(e){const t=[];let a=0;for(let r=0,i=e.length;r126||35===i||40===i||41===i||60===i||62===i||91===i||93===i||123===i||125===i||47===i||37===i){a"\n"===e?"\\n":"\r"===e?"\\r":`\\${e}`))}function _collectJS(e,t,a,r){if(!e)return;let i=null;if(e instanceof Ref){if(r.has(e))return;i=e;r.put(i);e=t.fetch(e)}if(Array.isArray(e))for(const i of e)_collectJS(i,t,a,r);else if(e instanceof Dict){if(isName(e.get("S"),"JavaScript")){const t=e.get("JS");let r;t instanceof BaseStream?r=t.getString():"string"==typeof t&&(r=t);r&&=stringToPDFString(r,!0).replaceAll("\0","");r&&a.push(r)}_collectJS(e.getRaw("Next"),t,a,r)}i&&r.remove(i)}function collectActions(e,t,a){const r=Object.create(null),i=getInheritableProperty({dict:t,key:"AA",stopWhenFound:!1});if(i)for(let t=i.length-1;t>=0;t--){const n=i[t];if(n instanceof Dict)for(const t of n.getKeys()){const i=a[t];if(!i)continue;const s=[];_collectJS(n.getRaw(t),e,s,new RefSet);s.length>0&&(r[i]=s)}}if(t.has("A")){const a=[];_collectJS(t.get("A"),e,a,new RefSet);a.length>0&&(r.Action=a)}return objectSize(r)>0?r:null}const Ma={60:"<",62:">",38:"&",34:""",39:"'"};function*codePointIter(e){for(let t=0,a=e.length;t55295&&(a<57344||a>65533)&&t++;yield a}}function encodeToXmlString(e){const t=[];let a=0;for(let r=0,i=e.length;r55295&&(i<57344||i>65533)&&r++;a=r+1}}if(0===t.length)return e;a: ${e}.`);return!1}return!0}function validateCSSFont(e){const t=new Set(["100","200","300","400","500","600","700","800","900","1000","normal","bold","bolder","lighter"]),{fontFamily:a,fontWeight:r,italicAngle:i}=e;if(!validateFontName(a,!0))return!1;const n=r?r.toString():"";e.fontWeight=t.has(n)?n:"400";const s=parseFloat(i);e.italicAngle=isNaN(s)||s<-90||s>90?"14":i.toString();return!0}function recoverJsURL(e){const t=new RegExp("^\\s*("+["app.launchURL","window.open","xfa.host.gotoURL"].join("|").replaceAll(".","\\.")+")\\((?:'|\")([^'\"]*)(?:'|\")(?:,\\s*(\\w+)\\)|\\))","i").exec(e);return t?.[2]?{url:t[2],newWindow:"app.launchURL"===t[1]&&"true"===t[3]}:null}function numberToString(e){if(Number.isInteger(e))return e.toString();const t=Math.round(100*e);return t%100==0?(t/100).toString():t%10==0?e.toFixed(1):e.toFixed(2)}function getNewAnnotationsMap(e){if(!e)return null;const t=new Map;for(const[a,r]of e){if(!a.startsWith(f))continue;let e=t.get(r.pageIndex);if(!e){e=[];t.set(r.pageIndex,e)}e.push(r)}return t.size>0?t:null}function stringToAsciiOrUTF16BE(e){return function isAscii(e){return/^[\x00-\x7F]*$/.test(e)}(e)?e:stringToUTF16String(e,!0)}function stringToUTF16HexString(e){const t=[];for(let a=0,r=e.length;a>8&255],ga[255&r])}return t.join("")}function stringToUTF16String(e,t=!1){const a=[];t&&a.push("þÿ");for(let t=0,r=e.length;t>8&255),String.fromCharCode(255&r))}return a.join("")}function getRotationMatrix(e,t,a){switch(e){case 90:return[0,1,-1,0,t,0];case 180:return[-1,0,0,-1,t,a];case 270:return[0,-1,1,0,0,a];default:throw new Error("Invalid rotation")}}function getSizeInBytes(e){return Math.ceil(Math.ceil(Math.log2(1+e))/8)}class QCMS{static#a=null;static _memory=null;static _mustAddAlpha=!1;static _destBuffer=null;static _destOffset=0;static _destLength=0;static _cssColor="";static _makeHexColor=null;static get _memoryArray(){const e=this.#a;return e?.byteLength?e:this.#a=new Uint8Array(this._memory.buffer)}}let Da;const Ba="undefined"!=typeof TextDecoder?new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}):{decode:()=>{throw Error("TextDecoder not available")}};"undefined"!=typeof TextDecoder&&Ba.decode();let Ra=null;function getUint8ArrayMemory0(){null!==Ra&&0!==Ra.byteLength||(Ra=new Uint8Array(Da.memory.buffer));return Ra}let Na=0;function passArray8ToWasm0(e,t){const a=t(1*e.length,1)>>>0;getUint8ArrayMemory0().set(e,a/1);Na=e.length;return a}const Ea=Object.freeze({RGB8:0,0:"RGB8",RGBA8:1,1:"RGBA8",BGRA8:2,2:"BGRA8",Gray8:3,3:"Gray8",GrayA8:4,4:"GrayA8",CMYK:5,5:"CMYK"}),Pa=Object.freeze({Perceptual:0,0:"Perceptual",RelativeColorimetric:1,1:"RelativeColorimetric",Saturation:2,2:"Saturation",AbsoluteColorimetric:3,3:"AbsoluteColorimetric"});function __wbg_get_imports(){const e={wbg:{}};e.wbg.__wbg_copyresult_b08ee7d273f295dd=function(e,t){!function copy_result(e,t){const{_mustAddAlpha:a,_destBuffer:r,_destOffset:i,_destLength:n,_memoryArray:s}=QCMS;if(t!==n)if(a)for(let a=e,n=e+t,o=i;a>>0,t>>>0)};e.wbg.__wbg_copyrgb_d60ce17bb05d9b67=function(e){!function copy_rgb(e){const{_destBuffer:t,_destOffset:a,_memoryArray:r}=QCMS;t[a]=r[e];t[a+1]=r[e+1];t[a+2]=r[e+2]}(e>>>0)};e.wbg.__wbg_makecssRGB_893bf0cd9fdb302d=function(e){!function make_cssRGB(e){const{_memoryArray:t}=QCMS;QCMS._cssColor=QCMS._makeHexColor(t[e],t[e+1],t[e+2])}(e>>>0)};e.wbg.__wbindgen_init_externref_table=function(){const e=Da.__wbindgen_export_0,t=e.grow(4);e.set(0,void 0);e.set(t+0,void 0);e.set(t+1,null);e.set(t+2,!0);e.set(t+3,!1)};e.wbg.__wbindgen_throw=function(e,t){throw new Error(function getStringFromWasm0(e,t){e>>>=0;return Ba.decode(getUint8ArrayMemory0().subarray(e,e+t))}(e,t))};return e}function __wbg_finalize_init(e,t){Da=e.exports;__wbg_init.__wbindgen_wasm_module=t;Ra=null;Da.__wbindgen_start();return Da}async function __wbg_init(e){if(void 0!==Da)return Da;void 0!==e&&(Object.getPrototypeOf(e)===Object.prototype?({module_or_path:e}=e):console.warn("using deprecated parameters for the initialization function; pass a single object instead"));const t=__wbg_get_imports();("string"==typeof e||"function"==typeof Request&&e instanceof Request||"function"==typeof URL&&e instanceof URL)&&(e=fetch(e));const{instance:a,module:r}=await async function __wbg_load(e,t){if("function"==typeof Response&&e instanceof Response){if("function"==typeof WebAssembly.instantiateStreaming)try{return await WebAssembly.instantiateStreaming(e,t)}catch(t){if("application/wasm"==e.headers.get("Content-Type"))throw t;console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",t)}const a=await e.arrayBuffer();return await WebAssembly.instantiate(a,t)}{const a=await WebAssembly.instantiate(e,t);return a instanceof WebAssembly.Instance?{instance:a,module:e}:a}}(await e,t);return __wbg_finalize_init(a,r)}class ColorSpace{static#r=new Uint8ClampedArray(3);constructor(e,t){this.name=e;this.numComps=t}getRgb(e,t,a=new Uint8ClampedArray(3)){this.getRgbItem(e,t,a,0);return a}getRgbHex(e,t){const a=this.getRgb(e,t,ColorSpace.#r);return Util.makeHexColor(a[0],a[1],a[2])}getRgbItem(e,t,a,r){unreachable("Should not call ColorSpace.getRgbItem")}getRgbBuffer(e,t,a,r,i,n,s){unreachable("Should not call ColorSpace.getRgbBuffer")}getOutputLength(e,t){unreachable("Should not call ColorSpace.getOutputLength")}isPassthrough(e){return!1}isDefaultDecode(e,t){return ColorSpace.isDefaultDecode(e,this.numComps)}fillRgb(e,t,a,r,i,n,s,o,c){const l=t*a;let h=null;const u=1<u&&"DeviceGray"!==this.name&&"DeviceRGB"!==this.name){const t=s<=8?new Uint8Array(u):new Uint16Array(u);for(let e=0;e=.99554525?1:MathClamp(1.055*e**(1/2.4)-.055,0,1)}#b(e){return e<0?-this.#b(-e):e>8?((e+16)/116)**3:e*CalRGBCS.#d}#y(e,t,a){if(0===e[0]&&0===e[1]&&0===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const r=this.#b(0),i=(1-r)/(1-this.#b(e[0])),n=1-i,s=(1-r)/(1-this.#b(e[1])),o=1-s,c=(1-r)/(1-this.#b(e[2])),l=1-c;a[0]=t[0]*i+n;a[1]=t[1]*s+o;a[2]=t[2]*c+l}#w(e,t,a){if(1===e[0]&&1===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const r=a;this.#f(CalRGBCS.#n,t,r);const i=CalRGBCS.#l;this.#g(e,r,i);this.#f(CalRGBCS.#s,i,a)}#S(e,t,a){const r=a;this.#f(CalRGBCS.#n,t,r);const i=CalRGBCS.#l;this.#p(e,r,i);this.#f(CalRGBCS.#s,i,a)}#i(e,t,a,r,i){const n=MathClamp(e[t]*i,0,1),s=MathClamp(e[t+1]*i,0,1),o=MathClamp(e[t+2]*i,0,1),c=1===n?1:n**this.GR,l=1===s?1:s**this.GG,h=1===o?1:o**this.GB,u=this.MXA*c+this.MXB*l+this.MXC*h,d=this.MYA*c+this.MYB*l+this.MYC*h,f=this.MZA*c+this.MZB*l+this.MZC*h,g=CalRGBCS.#h;g[0]=u;g[1]=d;g[2]=f;const p=CalRGBCS.#u;this.#w(this.whitePoint,g,p);const m=CalRGBCS.#h;this.#y(this.blackPoint,p,m);const b=CalRGBCS.#u;this.#S(CalRGBCS.#c,m,b);const y=CalRGBCS.#h;this.#f(CalRGBCS.#o,b,y);a[r]=255*this.#m(y[0]);a[r+1]=255*this.#m(y[1]);a[r+2]=255*this.#m(y[2])}getRgbItem(e,t,a,r){this.#i(e,t,a,r,1)}getRgbBuffer(e,t,a,r,i,n,s){const o=1/((1<this.amax||this.bmin>this.bmax){info("Invalid Range, falling back to defaults");this.amin=-100;this.amax=100;this.bmin=-100;this.bmax=100}}#x(e){return e>=6/29?e**3:108/841*(e-4/29)}#A(e,t,a,r){return a+e*(r-a)/t}#i(e,t,a,r,i){let n=e[t],s=e[t+1],o=e[t+2];if(!1!==a){n=this.#A(n,a,0,100);s=this.#A(s,a,this.amin,this.amax);o=this.#A(o,a,this.bmin,this.bmax)}s>this.amax?s=this.amax:sthis.bmax?o=this.bmax:o{!function qcms_drop_transformer(e){Da.qcms_drop_transformer(e)}(e)}));constructor(e,t,a){if(!IccColorSpace.isUsable)throw new Error("No ICC color space support");super(t,a);let r;switch(a){case 1:r=Ea.Gray8;this.#C=(e,t,a)=>function qcms_convert_one(e,t,a){Da.qcms_convert_one(e,t,a)}(this.#k,255*e[t],a);break;case 3:r=Ea.RGB8;this.#C=(e,t,a)=>function qcms_convert_three(e,t,a,r,i){Da.qcms_convert_three(e,t,a,r,i)}(this.#k,255*e[t],255*e[t+1],255*e[t+2],a);break;case 4:r=Ea.CMYK;this.#C=(e,t,a)=>function qcms_convert_four(e,t,a,r,i,n){Da.qcms_convert_four(e,t,a,r,i,n)}(this.#k,255*e[t],255*e[t+1],255*e[t+2],255*e[t+3],a);break;default:throw new Error(`Unsupported number of components: ${a}`)}this.#k=function qcms_transformer_from_memory(e,t,a){const r=passArray8ToWasm0(e,Da.__wbindgen_malloc),i=Na;return Da.qcms_transformer_from_memory(r,i,t,a)>>>0}(e,r,Pa.Perceptual);if(!this.#k)throw new Error("Failed to create ICC color space");IccColorSpace.#I.register(this,this.#k)}getRgbHex(e,t){this.#C(e,t,!0);return QCMS._cssColor}getRgbItem(e,t,a,r){QCMS._destBuffer=a;QCMS._destOffset=r;QCMS._destLength=3;this.#C(e,t,!1);QCMS._destBuffer=null}getRgbBuffer(e,t,a,r,i,n,s){e=e.subarray(t,t+a*this.numComps);if(8!==n){const t=255/((1<=this.end?-1:this.bytes[this.pos++]}getBytes(e){const t=this.bytes,a=this.pos,r=this.end;if(!e)return t.subarray(a,r);let i=a+e;i>r&&(i=r);this.pos=i;return t.subarray(a,i)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);return this.bytes.subarray(e,t)}reset(){this.pos=this.start}moveStart(){this.start=this.pos}makeSubStream(e,t,a=null){return new Stream(this.bytes.buffer,e,t,a)}}class StringStream extends Stream{constructor(e){super(stringToBytes(e))}}class NullStream extends Stream{constructor(){super(new Uint8Array(0))}}class ChunkedStream extends Stream{constructor(e,t,a){super(new Uint8Array(e),0,e,null);this.chunkSize=t;this._loadedChunks=new Set;this.numChunks=Math.ceil(e/t);this.manager=a;this.progressiveDataLength=0;this.lastSuccessfulEnsureByteChunk=-1}getMissingChunks(){const e=[];for(let t=0,a=this.numChunks;t=this.end?this.numChunks:Math.floor(t/this.chunkSize);for(let e=a;ethis.numChunks)&&t!==this.lastSuccessfulEnsureByteChunk){if(!this._loadedChunks.has(t))throw new MissingDataException(e,e+1);this.lastSuccessfulEnsureByteChunk=t}}ensureRange(e,t){if(e>=t)return;if(t<=this.progressiveDataLength)return;const a=Math.floor(e/this.chunkSize);if(a>this.numChunks)return;const r=Math.min(Math.floor((t-1)/this.chunkSize)+1,this.numChunks);for(let i=a;i=this.end)return-1;e>=this.progressiveDataLength&&this.ensureByte(e);return this.bytes[this.pos++]}getBytes(e){const t=this.bytes,a=this.pos,r=this.end;if(!e){r>this.progressiveDataLength&&this.ensureRange(a,r);return t.subarray(a,r)}let i=a+e;i>r&&(i=r);i>this.progressiveDataLength&&this.ensureRange(a,i);this.pos=i;return t.subarray(a,i)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);t>this.progressiveDataLength&&this.ensureRange(e,t);return this.bytes.subarray(e,t)}makeSubStream(e,t,a=null){t?e+t>this.progressiveDataLength&&this.ensureRange(e,e+t):e>=this.progressiveDataLength&&this.ensureByte(e);function ChunkedStreamSubstream(){}ChunkedStreamSubstream.prototype=Object.create(this);ChunkedStreamSubstream.prototype.getMissingChunks=function(){const e=this.chunkSize,t=Math.floor(this.start/e),a=Math.floor((this.end-1)/e)+1,r=[];for(let e=t;e{const readChunk=({value:n,done:s})=>{try{if(s){const t=arrayBuffersToBytes(r);r=null;e(t);return}i+=n.byteLength;a.isStreamingSupported&&this.onProgress({loaded:i});r.push(n);a.read().then(readChunk,t)}catch(e){t(e)}};a.read().then(readChunk,t)})).then((t=>{this.aborted||this.onReceiveData({chunk:t,begin:e})}))}requestAllChunks(e=!1){if(!e){const e=this.stream.getMissingChunks();this._requestChunks(e)}return this._loadedStreamCapability.promise}_requestChunks(e){const t=this.currRequestId++,a=new Set;this._chunksNeededByRequest.set(t,a);for(const t of e)this.stream.hasChunk(t)||a.add(t);if(0===a.size)return Promise.resolve();const r=Promise.withResolvers();this._promisesByRequest.set(t,r);const i=[];for(const e of a){let a=this._requestsByChunk.get(e);if(!a){a=[];this._requestsByChunk.set(e,a);i.push(e)}a.push(t)}if(i.length>0){const e=this.groupChunks(i);for(const t of e){const e=t.beginChunk*this.chunkSize,a=Math.min(t.endChunk*this.chunkSize,this.length);this.sendRequest(e,a).catch(r.reject)}}return r.promise.catch((e=>{if(!this.aborted)throw e}))}getStream(){return this.stream}requestRange(e,t){t=Math.min(t,this.length);const a=this.getBeginChunk(e),r=this.getEndChunk(t),i=[];for(let e=a;ee-t));return this._requestChunks(t)}groupChunks(e){const t=[];let a=-1,r=-1;for(let i=0,n=e.length;i=0&&r+1!==n){t.push({beginChunk:a,endChunk:r+1});a=n}i+1===e.length&&t.push({beginChunk:a,endChunk:n+1});r=n}return t}onProgress(e){this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize+e.loaded,total:this.length})}onReceiveData(e){const t=e.chunk,a=void 0===e.begin,r=a?this.progressiveDataLength:e.begin,i=r+t.byteLength,n=Math.floor(r/this.chunkSize),s=i0||o.push(a)}}}if(!this.disableAutoFetch&&0===this._requestsByChunk.size){let e;if(1===this.stream.numChunksLoaded){const t=this.stream.numChunks-1;this.stream.hasChunk(t)||(e=t)}else e=this.stream.nextEmptyChunk(s);Number.isInteger(e)&&this._requestChunks([e])}for(const e of o){const t=this._promisesByRequest.get(e);this._promisesByRequest.delete(e);t.resolve()}this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize,total:this.length})}onError(e){this._loadedStreamCapability.reject(e)}getBeginChunk(e){return Math.floor(e/this.chunkSize)}getEndChunk(e){return Math.floor((e-1)/this.chunkSize)+1}abort(e){this.aborted=!0;this.pdfNetworkStream?.cancelAllRequests(e);for(const t of this._promisesByRequest.values())t.reject(e)}}function convertToRGBA(e){switch(e.kind){case k:return convertBlackAndWhiteToRGBA(e);case C:return function convertRGBToRGBA({src:e,srcPos:t=0,dest:a,destPos:r=0,width:i,height:n}){let s=0;const o=i*n*3,c=o>>2,l=new Uint32Array(e.buffer,t,c);if(FeatureTest.isLittleEndian){for(;s>>24|t<<8|4278190080;a[r+2]=t>>>16|i<<16|4278190080;a[r+3]=i>>>8|4278190080}for(let i=4*s,n=t+o;i>>8|255;a[r+2]=t<<16|i>>>16|255;a[r+3]=i<<8|255}for(let i=4*s,n=t+o;i>3,u=7&r,d=e.length;a=new Uint32Array(a.buffer);let f=0;for(let r=0;ra||t>a)return!0;const r=e*t;if(this._hasMaxArea)return r>this.MAX_AREA;if(r(this.MAX_AREA=this.#T**2)}static getReducePowerForJPX(e,t,a){const r=e*t,i=2**30/(4*a);if(!this.needsToBeResized(e,t))return r>i?Math.ceil(Math.log2(r/i)):0;const{MAX_DIM:n,MAX_AREA:s}=this,o=Math.max(e/n,t/n,Math.sqrt(r/Math.min(i,s)));return Math.ceil(Math.log2(o))}static get MAX_DIM(){return shadow(this,"MAX_DIM",this._guessMax(2048,65537,0,1))}static get MAX_AREA(){this._hasMaxArea=!0;return shadow(this,"MAX_AREA",this._guessMax(this.#T,this.MAX_DIM,128,0)**2)}static set MAX_AREA(e){if(e>=0){this._hasMaxArea=!0;shadow(this,"MAX_AREA",e)}}static setOptions({canvasMaxAreaInBytes:e=-1,isImageDecoderSupported:t=!1}){this._hasMaxArea||(this.MAX_AREA=e>>2);this.#M=t}static _areGoodDims(e,t){try{const a=new OffscreenCanvas(e,t),r=a.getContext("2d");r.fillRect(0,0,1,1);const i=r.getImageData(0,0,1,1).data[3];a.width=a.height=1;return 0!==i}catch{return!1}}static _guessMax(e,t,a,r){for(;e+a+1va){const e=this.#D();if(e)return e}const r=this._encodeBMP();let i,n;if(await ImageResizer.canUseImageDecoder){i=new ImageDecoder({data:r,type:"image/bmp",preferAnimation:!1,transfer:[r.buffer]});n=i.decode().catch((e=>{warn(`BMP image decoding failed: ${e}`);return createImageBitmap(new Blob([this._encodeBMP().buffer],{type:"image/bmp"}))})).finally((()=>{i.close()}))}else n=createImageBitmap(new Blob([r.buffer],{type:"image/bmp"}));const{MAX_AREA:s,MAX_DIM:o}=ImageResizer,c=Math.max(t/o,a/o,Math.sqrt(t*a/s)),l=Math.max(c,2),h=Math.round(10*(c+1.25))/10/l,u=Math.floor(Math.log2(h)),d=new Array(u+2).fill(2);d[0]=l;d.splice(-1,1,h/(1<>s,c=r>>s;let l,h=r;try{l=new Uint8Array(n)}catch{let e=Math.floor(Math.log2(n+1));for(;;)try{l=new Uint8Array(2**e-1);break}catch{e-=1}h=Math.floor((2**e-1)/(4*a));const t=a*h*4;t>s;e>3,s=a+3&-4;if(a!==s){const e=new Uint8Array(s*t);let r=0;for(let n=0,o=t*a;ni&&(r=i)}else{for(;!this.eof;)this.readBlock(t);r=this.bufferLength}this.pos=r;return this.buffer.subarray(a,r)}async getImageData(e,t){if(!this.canAsyncDecodeImageFromBuffer)return this.isAsyncDecoder?this.decodeImage(null,t):this.getBytes(e,t);const a=await this.stream.asyncGetBytes();return this.decodeImage(a,t)}reset(){this.pos=0}makeSubStream(e,t,a=null){if(void 0===t)for(;!this.eof;)this.readBlock();else{const a=e+t;for(;this.bufferLength<=a&&!this.eof;)this.readBlock()}return new Stream(this.buffer,e,t,a)}getBaseStreams(){return this.str?this.str.getBaseStreams():null}}class StreamsSequenceStream extends DecodeStream{constructor(e,t=null){e=e.filter((e=>e instanceof BaseStream));let a=0;for(const t of e)a+=t instanceof DecodeStream?t._rawMinBufferLength:t.length;super(a);this.streams=e;this._onError=t}readBlock(){const e=this.streams;if(0===e.length){this.eof=!0;return}const t=e.shift();let a;try{a=t.getBytes()}catch(e){if(this._onError){this._onError(e,t.dict?.objId);return}throw e}const r=this.bufferLength,i=r+a.length;this.ensureBuffer(i).set(a,r);this.bufferLength=i}getBaseStreams(){const e=[];for(const t of this.streams){const a=t.getBaseStreams();a&&e.push(...a)}return e.length>0?e:null}}class ColorSpaceUtils{static parse({cs:e,xref:t,resources:a=null,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n,asyncIfNotCached:s=!1}){const o={xref:t,resources:a,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n};let c,l,h;if(e instanceof Ref){l=e;const a=i.getByRef(l)||n.getByRef(l);if(a)return a;e=t.fetch(e)}if(e instanceof Name){c=e.name;const t=n.getByName(c);if(t)return t}try{h=this.#B(e,o)}catch(e){if(s&&!(e instanceof MissingDataException))return Promise.reject(e);throw e}if(c||l){n.set(c,l,h);l&&i.set(null,l,h)}return s?Promise.resolve(h):h}static#R(e,t){const{globalColorSpaceCache:a}=t;let r;if(e instanceof Ref){r=e;const t=a.getByRef(r);if(t)return t}const i=this.#B(e,t);r&&a.set(null,r,i);return i}static#B(e,t){const{xref:a,resources:r,pdfFunctionFactory:i,globalColorSpaceCache:n}=t;if((e=a.fetchIfRef(e))instanceof Name)switch(e.name){case"G":case"DeviceGray":return this.gray;case"RGB":case"DeviceRGB":return this.rgb;case"DeviceRGBA":return this.rgba;case"CMYK":case"DeviceCMYK":return this.cmyk;case"Pattern":return new PatternCS(null);default:if(r instanceof Dict){const a=r.get("ColorSpace");if(a instanceof Dict){const r=a.get(e.name);if(r){if(r instanceof Name)return this.#B(r,t);e=r;break}}}warn(`Unrecognized ColorSpace: ${e.name}`);return this.gray}if(Array.isArray(e)){const r=a.fetchIfRef(e[0]).name;let s,o,c,l,h,u;switch(r){case"G":case"DeviceGray":return this.gray;case"RGB":case"DeviceRGB":return this.rgb;case"CMYK":case"DeviceCMYK":return this.cmyk;case"CalGray":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");u=s.get("Gamma");return new CalGrayCS(l,h,u);case"CalRGB":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");u=s.getArray("Gamma");const d=s.getArray("Matrix");return new CalRGBCS(l,h,u,d);case"ICCBased":const f=e[1]instanceof Ref;if(f){const t=n.getByRef(e[1]);if(t)return t}const g=a.fetchIfRef(e[1]),p=g.dict;o=p.get("N");if(IccColorSpace.isUsable)try{const t=new IccColorSpace(g.getBytes(),"ICCBased",o);f&&n.set(null,e[1],t);return t}catch(t){if(t instanceof MissingDataException)throw t;warn(`ICCBased color space (${e[1]}): "${t}".`)}const m=p.getRaw("Alternate");if(m){const e=this.#R(m,t);if(e.numComps===o)return e;warn("ICCBased color space: Ignoring incorrect /Alternate entry.")}if(1===o)return this.gray;if(3===o)return this.rgb;if(4===o)return this.cmyk;break;case"Pattern":c=e[1]||null;c&&(c=this.#R(c,t));return new PatternCS(c);case"I":case"Indexed":c=this.#R(e[1],t);const b=MathClamp(a.fetchIfRef(e[2]),0,255),y=a.fetchIfRef(e[3]);return new IndexedCS(c,b,y);case"Separation":case"DeviceN":const w=a.fetchIfRef(e[1]);o=Array.isArray(w)?w.length:1;c=this.#R(e[2],t);const S=i.create(e[3]);return new AlternateCS(o,c,S);case"Lab":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");const x=s.getArray("Range");return new LabCS(l,h,x);default:warn(`Unimplemented ColorSpace object: ${r}`);return this.gray}}warn(`Unrecognized ColorSpace object: ${e}`);return this.gray}static get gray(){return shadow(this,"gray",new DeviceGrayCS)}static get rgb(){return shadow(this,"rgb",new DeviceRgbCS)}static get rgba(){return shadow(this,"rgba",new DeviceRgbaCS)}static get cmyk(){if(CmykICCBasedCS.isUsable)try{return shadow(this,"cmyk",new CmykICCBasedCS)}catch{warn("CMYK fallback: DeviceCMYK")}return shadow(this,"cmyk",new DeviceCmykCS)}}class JpegError extends fa{constructor(e){super(e,"JpegError")}}class DNLMarkerError extends fa{constructor(e,t){super(e,"DNLMarkerError");this.scanLines=t}}class EOIMarkerError extends fa{constructor(e){super(e,"EOIMarkerError")}}const ja=new Uint8Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]),_a=4017,Ua=799,Xa=3406,qa=2276,Ha=1567,Wa=3784,za=5793,$a=2896;function buildHuffmanTable(e,t){let a,r,i=0,n=16;for(;n>0&&!e[n-1];)n--;const s=[{children:[],index:0}];let o,c=s[0];for(a=0;a0;)c=s.pop();c.index++;s.push(c);for(;s.length<=a;){s.push(o={children:[],index:0});c.children[c.index]=o.children;c=o}i++}if(a+10){g--;return f>>g&1}f=e[t++];if(255===f){const r=e[t++];if(r){if(220===r&&l){const r=readUint16(e,t+=2);t+=2;if(r>0&&r!==a.scanLines)throw new DNLMarkerError("Found DNL marker (0xFFDC) while parsing scan data",r)}else if(217===r){if(l){const e=y*(8===a.precision?8:0);if(e>0&&Math.round(a.scanLines/e)>=5)throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, possibly caused by incorrect `scanLines` parameter",e)}throw new EOIMarkerError("Found EOI marker (0xFFD9) while parsing scan data")}throw new JpegError(`unexpected marker ${(f<<8|r).toString(16)}`)}}g=7;return f>>>7}function decodeHuffman(e){let t=e;for(;;){t=t[readBit()];switch(typeof t){case"number":return t;case"object":continue}throw new JpegError("invalid huffman sequence")}}function receive(e){let t=0;for(;e>0;){t=t<<1|readBit();e--}return t}function receiveAndExtend(e){if(1===e)return 1===readBit()?1:-1;const t=receive(e);return t>=1<0){p--;return}let a=n;const r=s;for(;a<=r;){const r=decodeHuffman(e.huffmanTableAC),i=15&r,n=r>>4;if(0===i){if(n<15){p=receive(n)+(1<>4;if(0===i)if(l<15){p=receive(l)+(1<>4;if(0===r){if(n<15)break;i+=16;continue}i+=n;const s=ja[i];e.blockData[t+s]=receiveAndExtend(r);i++}};let O,T=0;const M=1===w?r[0].blocksPerLine*r[0].blocksPerColumn:h*a.mcusPerColumn;let D,R;for(;T<=M;){const a=i?Math.min(M-T,i):M;if(a>0){for(x=0;x0?"unexpected":"excessive"} MCU data, current marker is: ${O.invalid}`);t=O.offset}if(!(O.marker>=65488&&O.marker<=65495))break;t+=2}return t-d}function quantizeAndInverse(e,t,a){const r=e.quantizationTable,i=e.blockData;let n,s,o,c,l,h,u,d,f,g,p,m,b,y,w,S,x;if(!r)throw new JpegError("missing required Quantization Table.");for(let e=0;e<64;e+=8){f=i[t+e];g=i[t+e+1];p=i[t+e+2];m=i[t+e+3];b=i[t+e+4];y=i[t+e+5];w=i[t+e+6];S=i[t+e+7];f*=r[e];if(g|p|m|b|y|w|S){g*=r[e+1];p*=r[e+2];m*=r[e+3];b*=r[e+4];y*=r[e+5];w*=r[e+6];S*=r[e+7];n=za*f+128>>8;s=za*b+128>>8;o=p;c=w;l=$a*(g-S)+128>>8;d=$a*(g+S)+128>>8;h=m<<4;u=y<<4;n=n+s+1>>1;s=n-s;x=o*Wa+c*Ha+128>>8;o=o*Ha-c*Wa+128>>8;c=x;l=l+u+1>>1;u=l-u;d=d+h+1>>1;h=d-h;n=n+c+1>>1;c=n-c;s=s+o+1>>1;o=s-o;x=l*qa+d*Xa+2048>>12;l=l*Xa-d*qa+2048>>12;d=x;x=h*Ua+u*_a+2048>>12;h=h*_a-u*Ua+2048>>12;u=x;a[e]=n+d;a[e+7]=n-d;a[e+1]=s+u;a[e+6]=s-u;a[e+2]=o+h;a[e+5]=o-h;a[e+3]=c+l;a[e+4]=c-l}else{x=za*f+512>>10;a[e]=x;a[e+1]=x;a[e+2]=x;a[e+3]=x;a[e+4]=x;a[e+5]=x;a[e+6]=x;a[e+7]=x}}for(let e=0;e<8;++e){f=a[e];g=a[e+8];p=a[e+16];m=a[e+24];b=a[e+32];y=a[e+40];w=a[e+48];S=a[e+56];if(g|p|m|b|y|w|S){n=za*f+2048>>12;s=za*b+2048>>12;o=p;c=w;l=$a*(g-S)+2048>>12;d=$a*(g+S)+2048>>12;h=m;u=y;n=4112+(n+s+1>>1);s=n-s;x=o*Wa+c*Ha+2048>>12;o=o*Ha-c*Wa+2048>>12;c=x;l=l+u+1>>1;u=l-u;d=d+h+1>>1;h=d-h;n=n+c+1>>1;c=n-c;s=s+o+1>>1;o=s-o;x=l*qa+d*Xa+2048>>12;l=l*Xa-d*qa+2048>>12;d=x;x=h*Ua+u*_a+2048>>12;h=h*_a-u*Ua+2048>>12;u=x;f=n+d;S=n-d;g=s+u;w=s-u;p=o+h;y=o-h;m=c+l;b=c-l;f<16?f=0:f>=4080?f=255:f>>=4;g<16?g=0:g>=4080?g=255:g>>=4;p<16?p=0:p>=4080?p=255:p>>=4;m<16?m=0:m>=4080?m=255:m>>=4;b<16?b=0:b>=4080?b=255:b>>=4;y<16?y=0:y>=4080?y=255:y>>=4;w<16?w=0:w>=4080?w=255:w>>=4;S<16?S=0:S>=4080?S=255:S>>=4;i[t+e]=f;i[t+e+8]=g;i[t+e+16]=p;i[t+e+24]=m;i[t+e+32]=b;i[t+e+40]=y;i[t+e+48]=w;i[t+e+56]=S}else{x=za*f+8192>>14;x=x<-2040?0:x>=2024?255:x+2056>>4;i[t+e]=x;i[t+e+8]=x;i[t+e+16]=x;i[t+e+24]=x;i[t+e+32]=x;i[t+e+40]=x;i[t+e+48]=x;i[t+e+56]=x}}}function buildComponentData(e,t){const a=t.blocksPerLine,r=t.blocksPerColumn,i=new Int16Array(64);for(let e=0;e=r)return null;const n=readUint16(e,t);if(n>=65472&&n<=65534)return{invalid:null,marker:n,offset:t};let s=readUint16(e,i);for(;!(s>=65472&&s<=65534);){if(++i>=r)return null;s=readUint16(e,i)}return{invalid:n.toString(16),marker:s,offset:i}}function prepareComponents(e){const t=Math.ceil(e.samplesPerLine/8/e.maxH),a=Math.ceil(e.scanLines/8/e.maxV);for(const r of e.components){const i=Math.ceil(Math.ceil(e.samplesPerLine/8)*r.h/e.maxH),n=Math.ceil(Math.ceil(e.scanLines/8)*r.v/e.maxV),s=t*r.h,o=64*(a*r.v)*(s+1);r.blockData=new Int16Array(o);r.blocksPerLine=i;r.blocksPerColumn=n}e.mcusPerLine=t;e.mcusPerColumn=a}function readDataBlock(e,t){const a=readUint16(e,t);let r=(t+=2)+a-2;const i=findNextFileMarker(e,r,t);if(i?.invalid){warn("readDataBlock - incorrect length, current marker is: "+i.invalid);r=i.offset}const n=e.subarray(t,r);return{appData:n,oldOffset:t,newOffset:t+n.length}}function skipData(e,t){const a=readUint16(e,t),r=(t+=2)+a-2,i=findNextFileMarker(e,r,t);return i?.invalid?i.offset:r}class JpegImage{constructor({decodeTransform:e=null,colorTransform:t=-1}={}){this._decodeTransform=e;this._colorTransform=t}static canUseImageDecoder(e,t=-1){let a=null,r=0,i=null,n=readUint16(e,r);r+=2;if(65496!==n)throw new JpegError("SOI not found");n=readUint16(e,r);r+=2;e:for(;65497!==n;){switch(n){case 65505:const{appData:t,oldOffset:s,newOffset:o}=readDataBlock(e,r);r=o;if(69===t[0]&&120===t[1]&&105===t[2]&&102===t[3]&&0===t[4]&&0===t[5]){if(a)throw new JpegError("Duplicate EXIF-blocks found.");a={exifStart:s+6,exifEnd:o}}n=readUint16(e,r);r+=2;continue;case 65472:case 65473:case 65474:i=e[r+7];break e;case 65535:255!==e[r]&&r--}r=skipData(e,r);n=readUint16(e,r);r+=2}return 4===i||3===i&&0===t?null:a||{}}parse(e,{dnlScanLines:t=null}={}){let a,r,i=0,n=null,s=null,o=0;const c=[],l=[],h=[];let u=readUint16(e,i);i+=2;if(65496!==u)throw new JpegError("SOI not found");u=readUint16(e,i);i+=2;e:for(;65497!==u;){let d,f,g;switch(u){case 65504:case 65505:case 65506:case 65507:case 65508:case 65509:case 65510:case 65511:case 65512:case 65513:case 65514:case 65515:case 65516:case 65517:case 65518:case 65519:case 65534:const{appData:p,newOffset:m}=readDataBlock(e,i);i=m;65504===u&&74===p[0]&&70===p[1]&&73===p[2]&&70===p[3]&&0===p[4]&&(n={version:{major:p[5],minor:p[6]},densityUnits:p[7],xDensity:p[8]<<8|p[9],yDensity:p[10]<<8|p[11],thumbWidth:p[12],thumbHeight:p[13],thumbData:p.subarray(14,14+3*p[12]*p[13])});65518===u&&65===p[0]&&100===p[1]&&111===p[2]&&98===p[3]&&101===p[4]&&(s={version:p[5]<<8|p[6],flags0:p[7]<<8|p[8],flags1:p[9]<<8|p[10],transformCode:p[11]});break;case 65499:const b=readUint16(e,i);i+=2;const y=b+i-2;let w;for(;i>4){if(t>>4!=1)throw new JpegError("DQT - invalid table spec");for(f=0;f<64;f++){w=ja[f];a[w]=readUint16(e,i);i+=2}}else for(f=0;f<64;f++){w=ja[f];a[w]=e[i++]}c[15&t]=a}break;case 65472:case 65473:case 65474:if(a)throw new JpegError("Only single frame JPEGs supported");i+=2;a={};a.extended=65473===u;a.progressive=65474===u;a.precision=e[i++];const S=readUint16(e,i);i+=2;a.scanLines=t||S;a.samplesPerLine=readUint16(e,i);i+=2;a.components=[];a.componentIds={};const x=e[i++];let k=0,C=0;for(d=0;d>4,n=15&e[i+1];k>4?l:h)[15&t]=buildHuffmanTable(a,n)}break;case 65501:i+=2;r=readUint16(e,i);i+=2;break;case 65498:const F=1==++o&&!t;i+=2;const O=e[i++],T=[];for(d=0;d>4];n.huffmanTableAC=l[15&s];T.push(n)}const M=e[i++],D=e[i++],R=e[i++];try{i+=decodeScan(e,i,a,T,r,M,D,R>>4,15&R,F)}catch(t){if(t instanceof DNLMarkerError){warn(`${t.message} -- attempting to re-parse the JPEG image.`);return this.parse(e,{dnlScanLines:t.scanLines})}if(t instanceof EOIMarkerError){warn(`${t.message} -- ignoring the rest of the image data.`);break e}throw t}break;case 65500:i+=4;break;case 65535:255!==e[i]&&i--;break;default:const N=findNextFileMarker(e,i-2,i-3);if(N?.invalid){warn("JpegImage.parse - unexpected data, current marker is: "+N.invalid);i=N.offset;break}if(!N||i>=e.length-1){warn("JpegImage.parse - reached the end of the image data without finding an EOI marker (0xFFD9).");break e}throw new JpegError("JpegImage.parse - unknown marker: "+u.toString(16))}u=readUint16(e,i);i+=2}if(!a)throw new JpegError("JpegImage.parse - no frame data found.");this.width=a.samplesPerLine;this.height=a.scanLines;this.jfif=n;this.adobe=s;this.components=[];for(const e of a.components){const t=c[e.quantizationId];t&&(e.quantizationTable=t);this.components.push({index:e.index,output:buildComponentData(0,e),scaleX:e.h/a.maxH,scaleY:e.v/a.maxV,blocksPerLine:e.blocksPerLine,blocksPerColumn:e.blocksPerColumn})}this.numComponents=this.components.length}_getLinearizedBlockData(e,t,a=!1){const r=this.width/e,i=this.height/t;let n,s,o,c,l,h,u,d,f,g,p,m=0;const b=this.components.length,y=e*t*b,w=new Uint8ClampedArray(y),S=new Uint32Array(e),x=4294967288;let k;for(u=0;u>8)+C[f+1];return w}get _isColorConversionNeeded(){return this.adobe?!!this.adobe.transformCode:3===this.numComponents?0!==this._colorTransform&&(82!==this.components[0].index||71!==this.components[1].index||66!==this.components[2].index):1===this._colorTransform}_convertYccToRgb(e){let t,a,r;for(let i=0,n=e.length;i4)throw new JpegError("Unsupported color mode");const n=this._getLinearizedBlockData(e,t,i);if(1===this.numComponents&&(a||r)){const e=n.length*(a?4:3),t=new Uint8ClampedArray(e);let r=0;if(a)!function grayToRGBA(e,t){if(FeatureTest.isLittleEndian)for(let a=0,r=e.length;a0&&(e=e.subarray(t));break}return e}decodeImage(e){if(this.eof)return this.buffer;e=this.#N(e||this.bytes);const t=new JpegImage(this.jpegOptions);t.parse(e);const a=t.getData({width:this.drawWidth,height:this.drawHeight,forceRGBA:this.forceRGBA,forceRGB:this.forceRGB,isSourcePDF:!0});this.buffer=a;this.bufferLength=a.length;this.eof=!0;return this.buffer}get canAsyncDecodeImageFromBuffer(){return this.stream.isAsync}async getTransferableImage(){if(!await JpegStream.canUseImageDecoder)return null;const e=this.jpegOptions;if(e.decodeTransform)return null;let t;try{const a=this.canAsyncDecodeImageFromBuffer&&await this.stream.asyncGetBytes()||this.bytes;if(!a)return null;let r=this.#N(a);const i=JpegImage.canUseImageDecoder(r,e.colorTransform);if(!i)return null;if(i.exifStart){r=r.slice();r.fill(0,i.exifStart,i.exifEnd)}t=new ImageDecoder({data:r,type:"image/jpeg",preferAnimation:!1});return(await t.decode()).image}catch(e){warn(`getTransferableImage - failed: "${e}".`);return null}finally{t?.close()}}}var OpenJPEG=async function(e={}){var t,a,r=e,i=new Promise(((e,r)=>{t=e;a=r})),n="./this.program",quit_=(e,t)=>{throw t},s=import.meta.url;try{new URL(".",s).href}catch{}var o,c,l,h,u,d,f=console.log.bind(console),g=console.error.bind(console),p=!1;function updateMemoryViews(){var e=o.buffer;l=new Int8Array(e);new Int16Array(e);h=new Uint8Array(e);new Uint16Array(e);u=new Int32Array(e);d=new Uint32Array(e);new Float32Array(e);new Float64Array(e);new BigInt64Array(e);new BigUint64Array(e)}var m=0,b=null;class ExitStatus{name="ExitStatus";constructor(e){this.message=`Program terminated with exit(${e})`;this.status=e}}var callRuntimeCallbacks=e=>{for(;e.length>0;)e.shift()(r)},y=[],addOnPostRun=e=>y.push(e),w=[],addOnPreRun=e=>w.push(e),S=!0,x=0,k={},handleException=e=>{if(e instanceof ExitStatus||"unwind"==e)return c;quit_(0,e)},keepRuntimeAlive=()=>S||x>0,_proc_exit=e=>{c=e;if(!keepRuntimeAlive()){r.onExit?.(e);p=!0}quit_(0,new ExitStatus(e))},_exit=(e,t)=>{c=e;_proc_exit(e)},callUserCallback=e=>{if(!p)try{e();(()=>{if(!keepRuntimeAlive())try{_exit(c)}catch(e){handleException(e)}})()}catch(e){handleException(e)}},growMemory=e=>{var t=(e-o.buffer.byteLength+65535)/65536|0;try{o.grow(t);updateMemoryViews();return 1}catch(e){}},C={},getEnvStrings=()=>{if(!getEnvStrings.strings){var e={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:n||"./this.program"};for(var t in C)void 0===C[t]?delete e[t]:e[t]=C[t];var a=[];for(var t in e)a.push(`${t}=${e[t]}`);getEnvStrings.strings=a}return getEnvStrings.strings},lengthBytesUTF8=e=>{for(var t=0,a=0;a=55296&&r<=57343){t+=4;++a}else t+=3}return t},v=[null,[],[]],F="undefined"!=typeof TextDecoder?new TextDecoder:void 0,UTF8ArrayToString=(e,t=0,a=NaN)=>{for(var r=t+a,i=t;e[i]&&!(i>=r);)++i;if(i-t>16&&e.buffer&&F)return F.decode(e.subarray(t,i));for(var n="";t>10,56320|1023&l)}}else n+=String.fromCharCode((31&s)<<6|o)}else n+=String.fromCharCode(s)}return n},printChar=(e,t)=>{var a=v[e];if(0===t||10===t){(1===e?f:g)(UTF8ArrayToString(a));a.length=0}else a.push(t)},UTF8ToString=(e,t)=>e?UTF8ArrayToString(h,e,t):"";r.noExitRuntime&&(S=r.noExitRuntime);r.print&&(f=r.print);r.printErr&&(g=r.printErr);r.wasmBinary&&r.wasmBinary;r.arguments&&r.arguments;r.thisProgram&&(n=r.thisProgram);r.writeArrayToMemory=(e,t)=>{l.set(e,t)};var O={l:()=>function abort(e){r.onAbort?.(e);g(e="Aborted("+e+")");p=!0;e+=". Build with -sASSERTIONS for more info.";var t=new WebAssembly.RuntimeError(e);a(t);throw t}(""),k:()=>{S=!1;x=0},m:(e,t)=>{if(k[e]){clearTimeout(k[e].id);delete k[e]}if(!t)return 0;var a=setTimeout((()=>{delete k[e];callUserCallback((()=>M(e,performance.now())))}),t);k[e]={id:a,timeout_ms:t};return 0},g:function _copy_pixels_1(e,t){e>>=2;const a=r.imageData=new Uint8ClampedArray(t),i=u.subarray(e,e+t);a.set(i)},f:function _copy_pixels_3(e,t,a,i){e>>=2;t>>=2;a>>=2;const n=r.imageData=new Uint8ClampedArray(3*i),s=u.subarray(e,e+i),o=u.subarray(t,t+i),c=u.subarray(a,a+i);for(let e=0;e>=2;t>>=2;a>>=2;i>>=2;const s=r.imageData=new Uint8ClampedArray(4*n),o=u.subarray(e,e+n),c=u.subarray(t,t+n),l=u.subarray(a,a+n),h=u.subarray(i,i+n);for(let e=0;e{var t,a,r=h.length,i=2147483648;if((e>>>=0)>i)return!1;for(var n=1;n<=4;n*=2){var s=r*(1+.2/n);s=Math.min(s,e+100663296);var o=Math.min(i,(t=Math.max(e,s),a=65536,Math.ceil(t/a)*a));if(growMemory(o))return!0}return!1},p:(e,t)=>{var a=0,r=0;for(var i of getEnvStrings()){var n=t+a;d[e+r>>2]=n;a+=((e,t,a,r)=>{if(!(r>0))return 0;for(var i=a,n=a+r-1,s=0;s=55296&&o<=57343&&(o=65536+((1023&o)<<10)|1023&e.charCodeAt(++s));if(o<=127){if(a>=n)break;t[a++]=o}else if(o<=2047){if(a+1>=n)break;t[a++]=192|o>>6;t[a++]=128|63&o}else if(o<=65535){if(a+2>=n)break;t[a++]=224|o>>12;t[a++]=128|o>>6&63;t[a++]=128|63&o}else{if(a+3>=n)break;t[a++]=240|o>>18;t[a++]=128|o>>12&63;t[a++]=128|o>>6&63;t[a++]=128|63&o}}t[a]=0;return a-i})(i,h,n,1/0)+1;r+=4}return 0},q:(e,t)=>{var a=getEnvStrings();d[e>>2]=a.length;var r=0;for(var i of a)r+=lengthBytesUTF8(i)+1;d[t>>2]=r;return 0},b:e=>52,o:function _fd_seek(e,t,a,r){t=(i=t)<-9007199254740992||i>9007199254740992?NaN:Number(i);var i;return 70},c:(e,t,a,r)=>{for(var i=0,n=0;n>2],o=d[t+4>>2];t+=8;for(var c=0;c>2]=i;return 0},r:function _gray_to_rgba(e,t){e>>=2;const a=r.imageData=new Uint8ClampedArray(4*t),i=u.subarray(e,e+t);for(let e=0;e>=2;t>>=2;const i=r.imageData=new Uint8ClampedArray(4*a),n=u.subarray(e,e+a),s=u.subarray(t,t+a);for(let e=0;e>=2;t>>=2;a>>=2;const n=r.imageData=new Uint8ClampedArray(4*i),s=u.subarray(e,e+i),o=u.subarray(t,t+i),c=u.subarray(a,a+i);for(let e=0;e{r.instantiateWasm(e,((e,a)=>{t(receiveInstance(e))}))}))}(),M=(T.t,r._malloc=T.u,r._free=T.v,r._jp2_decode=T.w,T.x);!function preInit(){if(r.preInit){"function"==typeof r.preInit&&(r.preInit=[r.preInit]);for(;r.preInit.length>0;)r.preInit.shift()()}}();!function run(){if(m>0)b=run;else{!function preRun(){if(r.preRun){"function"==typeof r.preRun&&(r.preRun=[r.preRun]);for(;r.preRun.length;)addOnPreRun(r.preRun.shift())}callRuntimeCallbacks(w)}();if(m>0)b=run;else if(r.setStatus){r.setStatus("Running...");setTimeout((()=>{setTimeout((()=>r.setStatus("")),1);doRun()}),1)}else doRun()}function doRun(){r.calledRun=!0;if(!p){!function initRuntime(){T.t()}();t(r);r.onRuntimeInitialized?.();!function postRun(){if(r.postRun){"function"==typeof r.postRun&&(r.postRun=[r.postRun]);for(;r.postRun.length;)addOnPostRun(r.postRun.shift())}callRuntimeCallbacks(y)}()}}}();return i};const Ga=OpenJPEG;class JpxError extends fa{constructor(e){super(e,"JpxError")}}class JpxImage{static#E=null;static#P=null;static#L=null;static#v=!0;static#j=!0;static#F=null;static setOptions({handler:e,useWasm:t,useWorkerFetch:a,wasmUrl:r}){this.#v=t;this.#j=a;this.#F=r;a||(this.#P=e)}static async#_(e){const t=`${this.#F}openjpeg_nowasm_fallback.js`;let a=null;try{a=(await import( +/*webpackIgnore: true*/ +/*@vite-ignore*/ +t)).default()}catch(e){warn(`JpxImage#getJsModule: ${e}`)}e(a)}static async#U(e,t,a){const r="openjpeg.wasm";try{this.#E||(this.#j?this.#E=await fetchBinaryData(`${this.#F}${r}`):this.#E=await this.#P.sendWithPromise("FetchBinaryData",{type:"wasmFactory",filename:r}));return a((await WebAssembly.instantiate(this.#E,t)).instance)}catch(t){warn(`JpxImage#instantiateWasm: ${t}`);this.#_(e);return null}finally{this.#P=null}}static async decode(e,{numComponents:t=4,isIndexedColormap:a=!1,smaskInData:r=!1,reducePower:i=0}={}){if(!this.#L){const{promise:e,resolve:t}=Promise.withResolvers(),a=[e];this.#v?a.push(Ga({warn,instantiateWasm:this.#U.bind(this,t)})):this.#_(t);this.#L=Promise.race(a)}const n=await this.#L;if(!n)throw new JpxError("OpenJPEG failed to initialize");let s;try{const o=e.length;s=n._malloc(o);n.writeArrayToMemory(e,s);if(n._jp2_decode(s,o,t>0?t:0,!!a,!!r,i)){const{errorMessages:e}=n;if(e){delete n.errorMessages;throw new JpxError(e)}throw new JpxError("Unknown error")}const{imageData:c}=n;n.imageData=null;return c}finally{s&&n._free(s)}}static cleanup(){this.#L=null}static parseImageProperties(e){let t=e.getByte();for(;t>=0;){const a=t;t=e.getByte();if(65361===(a<<8|t)){e.skip(4);const t=e.getInt32()>>>0,a=e.getInt32()>>>0,r=e.getInt32()>>>0,i=e.getInt32()>>>0;e.skip(16);return{width:t-r,height:a-i,bitsPerComponent:8,componentsCount:e.getUint16()}}}throw new JpxError("No size marker found in JPX stream")}}function addState(e,t,a,r,i){let n=e;for(let e=0,a=t.length-1;e1e3){l=Math.max(l,d);f+=u+2;d=0;u=0}h.push({transform:t,x:d,y:f,w:a.width,h:a.height});d+=a.width+2;u=Math.max(u,a.height)}const g=Math.max(l,d)+1,p=f+u+1,m=new Uint8Array(g*p*4),b=g<<2;for(let e=0;e=0;){t[n-4]=t[n];t[n-3]=t[n+1];t[n-2]=t[n+2];t[n-1]=t[n+3];t[n+a]=t[n+a-4];t[n+a+1]=t[n+a-3];t[n+a+2]=t[n+a-2];t[n+a+3]=t[n+a-1];n-=b}}const y={width:g,height:p};if(e.isOffscreenCanvasSupported){const e=new OffscreenCanvas(g,p);e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(m.buffer),g,p),0,0);y.bitmap=e.transferToImageBitmap();y.data=null}else{y.kind=v;y.data=m}a.splice(n,4*c,Zt);r.splice(n,4*c,[y,h]);return n+1}));addState(Va,[Be,Ne,Vt,Re],null,(function iterateImageMaskGroup(e,t){const a=e.fnArray,r=(t-(e.iCurr-3))%4;switch(r){case 0:return a[t]===Be;case 1:return a[t]===Ne;case 2:return a[t]===Vt;case 3:return a[t]===Re}throw new Error(`iterateImageMaskGroup - invalid pos: ${r}`)}),(function foundImageMaskGroup(e,t){const a=e.fnArray,r=e.argsArray,i=e.iCurr,n=i-3,s=i-2,o=i-1;let c=Math.floor((t-n)/4);if(c<10)return t-(t-n)%4;let l,h,u=!1;const d=r[o][0],f=r[s][0],g=r[s][1],p=r[s][2],m=r[s][3];if(g===p){u=!0;l=s+4;let e=o+4;for(let t=1;t=4&&a[n-4]===a[s]&&a[n-3]===a[o]&&a[n-2]===a[c]&&a[n-1]===a[l]&&r[n-4][0]===h&&r[n-4][1]===u){d++;f-=5}let g=f+4;for(let e=1;e{const t=e.argsArray,a=t[e.iCurr-1][0];if(a!==qe&&a!==He&&a!==$e&&a!==Ge&&a!==Ve&&a!==Ke)return!0;const r=t[e.iCurr-2];return 1===r[0]&&0===r[1]&&0===r[2]&&1===r[3]}),(()=>!1),((e,t)=>{const{fnArray:a,argsArray:r}=e,i=e.iCurr,n=i-3,s=i-2,o=r[i-1],c=r[s],[,[l],h]=o;if(h){Util.scaleMinMax(c,h);for(let e=0,t=l.length;e=a)break}r=(r||Va)[e[t]];if(r&&!Array.isArray(r)){n.iCurr=t;t++;if(!r.checkFn||(0,r.checkFn)(n)){i=r;r=null}else r=null}else t++}this.state=r;this.match=i;this.lastProcessed=t}flush(){for(;this.match;){const e=this.queue.fnArray.length;this.lastProcessed=(0,this.match.processFn)(this.context,e);this.match=null;this.state=null;this._optimize()}}reset(){this.state=null;this.match=null;this.lastProcessed=0}}class OperatorList{static CHUNK_SIZE=1e3;static CHUNK_SIZE_ABOUT=this.CHUNK_SIZE-5;static isOffscreenCanvasSupported=!1;constructor(e=0,t){this._streamSink=t;this.fnArray=[];this.argsArray=[];this.optimizer=!t||e&d?new NullOptimizer(this):new QueueOptimizer(this);this.dependencies=new Set;this._totalLength=0;this.weight=0;this._resolved=t?null:Promise.resolve()}static setOptions({isOffscreenCanvasSupported:e}){this.isOffscreenCanvasSupported=e}get length(){return this.argsArray.length}get ready(){return this._resolved||this._streamSink.ready}get totalLength(){return this._totalLength+this.length}addOp(e,t){this.optimizer.push(e,t);this.weight++;this._streamSink&&(this.weight>=OperatorList.CHUNK_SIZE||this.weight>=OperatorList.CHUNK_SIZE_ABOUT&&(e===Re||e===et))&&this.flush()}addImageOps(e,t,a,r=!1){if(r){this.addOp(Be);this.addOp(De,[[["SMask",!1]]])}void 0!==a&&this.addOp(jt,["OC",a]);this.addOp(e,t);void 0!==a&&this.addOp(_t,[]);r&&this.addOp(Re)}addDependency(e){if(!this.dependencies.has(e)){this.dependencies.add(e);this.addOp(ke,[e])}}addDependencies(e){for(const t of e)this.addDependency(t)}addOpList(e){if(e instanceof OperatorList){for(const t of e.dependencies)this.dependencies.add(t);for(let t=0,a=e.length;t>>0}function hexToStr(e,t){return 1===t?String.fromCharCode(e[0],e[1]):3===t?String.fromCharCode(e[0],e[1],e[2],e[3]):String.fromCharCode(...e.subarray(0,t+1))}function addHex(e,t,a){let r=0;for(let i=a;i>=0;i--){r+=e[i]+t[i];e[i]=255&r;r>>=8}}function incHex(e,t){let a=1;for(let r=t;r>=0&&a>0;r--){a+=e[r];e[r]=255&a;a>>=8}}const Ka=16;class BinaryCMapStream{constructor(e){this.buffer=e;this.pos=0;this.end=e.length;this.tmpBuf=new Uint8Array(19)}readByte(){return this.pos>=this.end?-1:this.buffer[this.pos++]}readNumber(){let e,t=0;do{const a=this.readByte();if(a<0)throw new FormatError("unexpected EOF in bcmap");e=!(128&a);t=t<<7|127&a}while(!e);return t}readSigned(){const e=this.readNumber();return 1&e?~(e>>>1):e>>>1}readHex(e,t){e.set(this.buffer.subarray(this.pos,this.pos+t+1));this.pos+=t+1}readHexNumber(e,t){let a;const r=this.tmpBuf;let i=0;do{const e=this.readByte();if(e<0)throw new FormatError("unexpected EOF in bcmap");a=!(128&e);r[i++]=127&e}while(!a);let n=t,s=0,o=0;for(;n>=0;){for(;o<8&&r.length>0;){s|=r[--i]<>=8;o-=8}}readHexSigned(e,t){this.readHexNumber(e,t);const a=1&e[t]?255:0;let r=0;for(let i=0;i<=t;i++){r=(1&r)<<8|e[i];e[i]=r>>1^a}}readString(){const e=this.readNumber(),t=new Array(e);for(let a=0;a=0;){const e=d>>5;if(7===e){switch(31&d){case 0:r.readString();break;case 1:n=r.readString()}continue}const a=!!(16&d),i=15&d;if(i+1>Ka)throw new Error("BinaryCMapReader.process: Invalid dataSize.");const f=1,g=r.readNumber();switch(e){case 0:r.readHex(s,i);r.readHexNumber(o,i);addHex(o,s,i);t.addCodespaceRange(i+1,hexToInt(s,i),hexToInt(o,i));for(let e=1;e=0;--i){r[a+i]=255&s;s>>=8}}}}class AsciiHexStream extends DecodeStream{constructor(e,t){t&&(t*=.5);super(t);this.str=e;this.dict=e.dict;this.firstDigit=-1}readBlock(){const e=this.str.getBytes(8e3);if(!e.length){this.eof=!0;return}const t=e.length+1>>1,a=this.ensureBuffer(this.bufferLength+t);let r=this.bufferLength,i=this.firstDigit;for(const t of e){let e;if(t>=48&&t<=57)e=15&t;else{if(!(t>=65&&t<=70||t>=97&&t<=102)){if(62===t){this.eof=!0;break}continue}e=9+(15&t)}if(i<0)i=e;else{a[r++]=i<<4|e;i=-1}}if(i>=0&&this.eof){a[r++]=i<<4;i=-1}this.firstDigit=i;this.bufferLength=r}}const Ja=-1,Ya=[[-1,-1],[-1,-1],[7,8],[7,7],[6,6],[6,6],[6,5],[6,5],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2]],Za=[[-1,-1],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[12,1984],[12,2048],[12,2112],[12,2176],[12,2240],[12,2304],[11,1856],[11,1856],[11,1920],[11,1920],[12,2368],[12,2432],[12,2496],[12,2560]],Qa=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[8,29],[8,29],[8,30],[8,30],[8,45],[8,45],[8,46],[8,46],[7,22],[7,22],[7,22],[7,22],[7,23],[7,23],[7,23],[7,23],[8,47],[8,47],[8,48],[8,48],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[7,20],[7,20],[7,20],[7,20],[8,33],[8,33],[8,34],[8,34],[8,35],[8,35],[8,36],[8,36],[8,37],[8,37],[8,38],[8,38],[7,19],[7,19],[7,19],[7,19],[8,31],[8,31],[8,32],[8,32],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[8,53],[8,53],[8,54],[8,54],[7,26],[7,26],[7,26],[7,26],[8,39],[8,39],[8,40],[8,40],[8,41],[8,41],[8,42],[8,42],[8,43],[8,43],[8,44],[8,44],[7,21],[7,21],[7,21],[7,21],[7,28],[7,28],[7,28],[7,28],[8,61],[8,61],[8,62],[8,62],[8,63],[8,63],[8,0],[8,0],[8,320],[8,320],[8,384],[8,384],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[7,27],[7,27],[7,27],[7,27],[8,59],[8,59],[8,60],[8,60],[9,1472],[9,1536],[9,1600],[9,1728],[7,18],[7,18],[7,18],[7,18],[7,24],[7,24],[7,24],[7,24],[8,49],[8,49],[8,50],[8,50],[8,51],[8,51],[8,52],[8,52],[7,25],[7,25],[7,25],[7,25],[8,55],[8,55],[8,56],[8,56],[8,57],[8,57],[8,58],[8,58],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[8,448],[8,448],[8,512],[8,512],[9,704],[9,768],[8,640],[8,640],[8,576],[8,576],[9,832],[9,896],[9,960],[9,1024],[9,1088],[9,1152],[9,1216],[9,1280],[9,1344],[9,1408],[7,256],[7,256],[7,256],[7,256],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7]],er=[[-1,-1],[-1,-1],[12,-2],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[11,1792],[11,1792],[12,1984],[12,1984],[12,2048],[12,2048],[12,2112],[12,2112],[12,2176],[12,2176],[12,2240],[12,2240],[12,2304],[12,2304],[11,1856],[11,1856],[11,1856],[11,1856],[11,1920],[11,1920],[11,1920],[11,1920],[12,2368],[12,2368],[12,2432],[12,2432],[12,2496],[12,2496],[12,2560],[12,2560],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[12,52],[12,52],[13,640],[13,704],[13,768],[13,832],[12,55],[12,55],[12,56],[12,56],[13,1280],[13,1344],[13,1408],[13,1472],[12,59],[12,59],[12,60],[12,60],[13,1536],[13,1600],[11,24],[11,24],[11,24],[11,24],[11,25],[11,25],[11,25],[11,25],[13,1664],[13,1728],[12,320],[12,320],[12,384],[12,384],[12,448],[12,448],[13,512],[13,576],[12,53],[12,53],[12,54],[12,54],[13,896],[13,960],[13,1024],[13,1088],[13,1152],[13,1216],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64]],tr=[[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[11,23],[11,23],[12,50],[12,51],[12,44],[12,45],[12,46],[12,47],[12,57],[12,58],[12,61],[12,256],[10,16],[10,16],[10,16],[10,16],[10,17],[10,17],[10,17],[10,17],[12,48],[12,49],[12,62],[12,63],[12,30],[12,31],[12,32],[12,33],[12,40],[12,41],[11,22],[11,22],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[12,128],[12,192],[12,26],[12,27],[12,28],[12,29],[11,19],[11,19],[11,20],[11,20],[12,34],[12,35],[12,36],[12,37],[12,38],[12,39],[11,21],[11,21],[12,42],[12,43],[10,0],[10,0],[10,0],[10,0],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12]],ar=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[6,9],[6,8],[5,7],[5,7],[4,6],[4,6],[4,6],[4,6],[4,5],[4,5],[4,5],[4,5],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2]];class CCITTFaxDecoder{constructor(e,t={}){if("function"!=typeof e?.next)throw new Error('CCITTFaxDecoder - invalid "source" parameter.');this.source=e;this.eof=!1;this.encoding=t.K||0;this.eoline=t.EndOfLine||!1;this.byteAlign=t.EncodedByteAlign||!1;this.columns=t.Columns||1728;this.rows=t.Rows||0;this.eoblock=t.EndOfBlock??!0;this.black=t.BlackIs1||!1;this.codingLine=new Uint32Array(this.columns+1);this.refLine=new Uint32Array(this.columns+2);this.codingLine[0]=this.columns;this.codingPos=0;this.row=0;this.nextLine2D=this.encoding<0;this.inputBits=0;this.inputBuf=0;this.outputBits=0;this.rowsDone=!1;let a;for(;0===(a=this._lookBits(12));)this._eatBits(1);1===a&&this._eatBits(12);if(this.encoding>0){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}}readNextChar(){if(this.eof)return-1;const e=this.refLine,t=this.codingLine,a=this.columns;let r,i,n,s,o;if(0===this.outputBits){this.rowsDone&&(this.eof=!0);if(this.eof)return-1;this.err=!1;let n,o,c;if(this.nextLine2D){for(s=0;t[s]=64);do{o+=c=this._getWhiteCode()}while(c>=64)}else{do{n+=c=this._getWhiteCode()}while(c>=64);do{o+=c=this._getBlackCode()}while(c>=64)}this._addPixels(t[this.codingPos]+n,i);t[this.codingPos]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]=64);else do{n+=c=this._getWhiteCode()}while(c>=64);this._addPixels(t[this.codingPos]+n,i);i^=1}}let l=!1;this.byteAlign&&(this.inputBits&=-8);if(this.eoblock||this.row!==this.rows-1){n=this._lookBits(12);if(this.eoline)for(;n!==Ja&&1!==n;){this._eatBits(1);n=this._lookBits(12)}else for(;0===n;){this._eatBits(1);n=this._lookBits(12)}if(1===n){this._eatBits(12);l=!0}else n===Ja&&(this.eof=!0)}else this.rowsDone=!0;if(!this.eof&&this.encoding>0&&!this.rowsDone){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}if(this.eoblock&&l&&this.byteAlign){n=this._lookBits(12);if(1===n){this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}if(this.encoding>=0)for(s=0;s<4;++s){n=this._lookBits(12);1!==n&&info("bad rtc code: "+n);this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}}this.eof=!0}}else if(this.err&&this.eoline){for(;;){n=this._lookBits(13);if(n===Ja){this.eof=!0;return-1}if(n>>1==1)break;this._eatBits(1)}this._eatBits(12);if(this.encoding>0){this._eatBits(1);this.nextLine2D=!(1&n)}}this.outputBits=t[0]>0?t[this.codingPos=0]:t[this.codingPos=1];this.row++}if(this.outputBits>=8){o=1&this.codingPos?0:255;this.outputBits-=8;if(0===this.outputBits&&t[this.codingPos]n){o<<=n;1&this.codingPos||(o|=255>>8-n);this.outputBits-=n;n=0}else{o<<=this.outputBits;1&this.codingPos||(o|=255>>8-this.outputBits);n-=this.outputBits;this.outputBits=0;if(t[this.codingPos]0){o<<=n;n=0}}}while(n)}this.black&&(o^=255);return o}_addPixels(e,t){const a=this.codingLine;let r=this.codingPos;if(e>a[r]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&r^t&&++r;a[r]=e}this.codingPos=r}_addPixelsNeg(e,t){const a=this.codingLine;let r=this.codingPos;if(e>a[r]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&r^t&&++r;a[r]=e}else if(e0&&e=i){const t=a[e-i];if(t[0]===r){this._eatBits(r);return[!0,t[1],!0]}}}return[!1,0,!1]}_getTwoDimCode(){let e,t=0;if(this.eoblock){t=this._lookBits(7);e=Ya[t];if(e?.[0]>0){this._eatBits(e[0]);return e[1]}}else{const e=this._findTableCode(1,7,Ya);if(e[0]&&e[2])return e[1]}info("Bad two dim code");return Ja}_getWhiteCode(){let e,t=0;if(this.eoblock){t=this._lookBits(12);if(t===Ja)return 1;e=t>>5?Qa[t>>3]:Za[t];if(e[0]>0){this._eatBits(e[0]);return e[1]}}else{let e=this._findTableCode(1,9,Qa);if(e[0])return e[1];e=this._findTableCode(11,12,Za);if(e[0])return e[1]}info("bad white code");this._eatBits(1);return 1}_getBlackCode(){let e,t;if(this.eoblock){e=this._lookBits(13);if(e===Ja)return 1;t=e>>7?!(e>>9)&&e>>7?tr[(e>>1)-64]:ar[e>>7]:er[e];if(t[0]>0){this._eatBits(t[0]);return t[1]}}else{let e=this._findTableCode(2,6,ar);if(e[0])return e[1];e=this._findTableCode(7,12,tr,64);if(e[0])return e[1];e=this._findTableCode(10,13,er);if(e[0])return e[1]}info("bad black code");this._eatBits(1);return 1}_lookBits(e){let t;for(;this.inputBits>16-e;this.inputBuf=this.inputBuf<<8|t;this.inputBits+=8}return this.inputBuf>>this.inputBits-e&65535>>16-e}_eatBits(e){(this.inputBits-=e)<0&&(this.inputBits=0)}}class CCITTFaxStream extends DecodeStream{constructor(e,t,a){super(t);this.str=e;this.dict=e.dict;a instanceof Dict||(a=Dict.empty);const r={next:()=>e.getByte()};this.ccittFaxDecoder=new CCITTFaxDecoder(r,{K:a.get("K"),EndOfLine:a.get("EndOfLine"),EncodedByteAlign:a.get("EncodedByteAlign"),Columns:a.get("Columns"),Rows:a.get("Rows"),EndOfBlock:a.get("EndOfBlock"),BlackIs1:a.get("BlackIs1")})}readBlock(){for(;!this.eof;){const e=this.ccittFaxDecoder.readNextChar();if(-1===e){this.eof=!0;return}this.ensureBuffer(this.bufferLength+1);this.buffer[this.bufferLength++]=e}}}const rr=new Int32Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),ir=new Int32Array([3,4,5,6,7,8,9,10,65547,65549,65551,65553,131091,131095,131099,131103,196643,196651,196659,196667,262211,262227,262243,262259,327811,327843,327875,327907,258,258,258]),nr=new Int32Array([1,2,3,4,65541,65543,131081,131085,196625,196633,262177,262193,327745,327777,393345,393409,459009,459137,524801,525057,590849,591361,657409,658433,724993,727041,794625,798721,868353,876545]),sr=[new Int32Array([459008,524368,524304,524568,459024,524400,524336,590016,459016,524384,524320,589984,524288,524416,524352,590048,459012,524376,524312,589968,459028,524408,524344,590032,459020,524392,524328,59e4,524296,524424,524360,590064,459010,524372,524308,524572,459026,524404,524340,590024,459018,524388,524324,589992,524292,524420,524356,590056,459014,524380,524316,589976,459030,524412,524348,590040,459022,524396,524332,590008,524300,524428,524364,590072,459009,524370,524306,524570,459025,524402,524338,590020,459017,524386,524322,589988,524290,524418,524354,590052,459013,524378,524314,589972,459029,524410,524346,590036,459021,524394,524330,590004,524298,524426,524362,590068,459011,524374,524310,524574,459027,524406,524342,590028,459019,524390,524326,589996,524294,524422,524358,590060,459015,524382,524318,589980,459031,524414,524350,590044,459023,524398,524334,590012,524302,524430,524366,590076,459008,524369,524305,524569,459024,524401,524337,590018,459016,524385,524321,589986,524289,524417,524353,590050,459012,524377,524313,589970,459028,524409,524345,590034,459020,524393,524329,590002,524297,524425,524361,590066,459010,524373,524309,524573,459026,524405,524341,590026,459018,524389,524325,589994,524293,524421,524357,590058,459014,524381,524317,589978,459030,524413,524349,590042,459022,524397,524333,590010,524301,524429,524365,590074,459009,524371,524307,524571,459025,524403,524339,590022,459017,524387,524323,589990,524291,524419,524355,590054,459013,524379,524315,589974,459029,524411,524347,590038,459021,524395,524331,590006,524299,524427,524363,590070,459011,524375,524311,524575,459027,524407,524343,590030,459019,524391,524327,589998,524295,524423,524359,590062,459015,524383,524319,589982,459031,524415,524351,590046,459023,524399,524335,590014,524303,524431,524367,590078,459008,524368,524304,524568,459024,524400,524336,590017,459016,524384,524320,589985,524288,524416,524352,590049,459012,524376,524312,589969,459028,524408,524344,590033,459020,524392,524328,590001,524296,524424,524360,590065,459010,524372,524308,524572,459026,524404,524340,590025,459018,524388,524324,589993,524292,524420,524356,590057,459014,524380,524316,589977,459030,524412,524348,590041,459022,524396,524332,590009,524300,524428,524364,590073,459009,524370,524306,524570,459025,524402,524338,590021,459017,524386,524322,589989,524290,524418,524354,590053,459013,524378,524314,589973,459029,524410,524346,590037,459021,524394,524330,590005,524298,524426,524362,590069,459011,524374,524310,524574,459027,524406,524342,590029,459019,524390,524326,589997,524294,524422,524358,590061,459015,524382,524318,589981,459031,524414,524350,590045,459023,524398,524334,590013,524302,524430,524366,590077,459008,524369,524305,524569,459024,524401,524337,590019,459016,524385,524321,589987,524289,524417,524353,590051,459012,524377,524313,589971,459028,524409,524345,590035,459020,524393,524329,590003,524297,524425,524361,590067,459010,524373,524309,524573,459026,524405,524341,590027,459018,524389,524325,589995,524293,524421,524357,590059,459014,524381,524317,589979,459030,524413,524349,590043,459022,524397,524333,590011,524301,524429,524365,590075,459009,524371,524307,524571,459025,524403,524339,590023,459017,524387,524323,589991,524291,524419,524355,590055,459013,524379,524315,589975,459029,524411,524347,590039,459021,524395,524331,590007,524299,524427,524363,590071,459011,524375,524311,524575,459027,524407,524343,590031,459019,524391,524327,589999,524295,524423,524359,590063,459015,524383,524319,589983,459031,524415,524351,590047,459023,524399,524335,590015,524303,524431,524367,590079]),9],or=[new Int32Array([327680,327696,327688,327704,327684,327700,327692,327708,327682,327698,327690,327706,327686,327702,327694,0,327681,327697,327689,327705,327685,327701,327693,327709,327683,327699,327691,327707,327687,327703,327695,0]),5];class FlateStream extends DecodeStream{constructor(e,t){super(t);this.str=e;this.dict=e.dict;const a=e.getByte(),r=e.getByte();if(-1===a||-1===r)throw new FormatError(`Invalid header in flate stream: ${a}, ${r}`);if(8!=(15&a))throw new FormatError(`Unknown compression method in flate stream: ${a}, ${r}`);if(((a<<8)+r)%31!=0)throw new FormatError(`Bad FCHECK in flate stream: ${a}, ${r}`);if(32&r)throw new FormatError(`FDICT bit set in flate stream: ${a}, ${r}`);this.codeSize=0;this.codeBuf=0}async getImageData(e,t){const a=await this.asyncGetBytes();return a?a.length<=e?a:a.subarray(0,e):this.getBytes(e)}async asyncGetBytes(){this.str.reset();const e=this.str.getBytes();try{const{readable:t,writable:a}=new DecompressionStream("deflate"),r=a.getWriter();await r.ready;r.write(e).then((async()=>{await r.ready;await r.close()})).catch((()=>{}));const i=[];let n=0;for await(const e of t){i.push(e);n+=e.byteLength}const s=new Uint8Array(n);let o=0;for(const e of i){s.set(e,o);o+=e.byteLength}return s}catch{this.str=new Stream(e,2,e.length,this.str.dict);this.reset();return null}}get isAsync(){return!0}getBits(e){const t=this.str;let a,r=this.codeSize,i=this.codeBuf;for(;r>e;this.codeSize=r-=e;return a}getCode(e){const t=this.str,a=e[0],r=e[1];let i,n=this.codeSize,s=this.codeBuf;for(;n>16,l=65535&o;if(c<1||n>c;this.codeSize=n-c;return l}generateHuffmanTable(e){const t=e.length;let a,r=0;for(a=0;ar&&(r=e[a]);const i=1<>=1}for(a=e;a>=1;if(0===t){let t;if(-1===(t=r.getByte())){this.#X("Bad block header in flate stream");return}let a=t;if(-1===(t=r.getByte())){this.#X("Bad block header in flate stream");return}a|=t<<8;if(-1===(t=r.getByte())){this.#X("Bad block header in flate stream");return}let i=t;if(-1===(t=r.getByte())){this.#X("Bad block header in flate stream");return}i|=t<<8;if(i!==(65535&~a)&&(0!==a||0!==i))throw new FormatError("Bad uncompressed block length in flate stream");this.codeBuf=0;this.codeSize=0;const n=this.bufferLength,s=n+a;e=this.ensureBuffer(s);this.bufferLength=s;if(0===a)-1===r.peekByte()&&(this.eof=!0);else{const t=r.getBytes(a);e.set(t,n);t.length0;)h[o++]=f}i=this.generateHuffmanTable(h.subarray(0,e));n=this.generateHuffmanTable(h.subarray(e,l))}}e=this.buffer;let s=e?e.length:0,o=this.bufferLength;for(;;){let t=this.getCode(i);if(t<256){if(o+1>=s){e=this.ensureBuffer(o+1);s=e.length}e[o++]=t;continue}if(256===t){this.bufferLength=o;return}t-=257;t=ir[t];let r=t>>16;r>0&&(r=this.getBits(r));a=(65535&t)+r;t=this.getCode(n);t=nr[t];r=t>>16;r>0&&(r=this.getBits(r));const c=(65535&t)+r;if(o+a>=s){e=this.ensureBuffer(o+a);s=e.length}for(let t=0;t>9&127;this.clow=this.clow<<7&65535;this.ct-=7;this.a=32768}byteIn(){const e=this.data;let t=this.bp;if(255===e[t])if(e[t+1]>143){this.clow+=65280;this.ct=8}else{t++;this.clow+=e[t]<<9;this.ct=7;this.bp=t}else{t++;this.clow+=t65535){this.chigh+=this.clow>>16;this.clow&=65535}}readBit(e,t){let a=e[t]>>1,r=1&e[t];const i=cr[a],n=i.qe;let s,o=this.a-n;if(this.chigh>15&1;this.clow=this.clow<<1&65535;this.ct--}while(!(32768&o));this.a=o;e[t]=a<<1|r;return s}}class Jbig2Error extends fa{constructor(e){super(e,"Jbig2Error")}}class ContextCache{getContexts(e){return e in this?this[e]:this[e]=new Int8Array(65536)}}class DecodingContext{constructor(e,t,a){this.data=e;this.start=t;this.end=a}get decoder(){return shadow(this,"decoder",new ArithmeticDecoder(this.data,this.start,this.end))}get contextCache(){return shadow(this,"contextCache",new ContextCache)}}function decodeInteger(e,t,a){const r=e.getContexts(t);let i=1;function readBits(e){let t=0;for(let n=0;n>>0}const n=readBits(1),s=readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(32)+4436:readBits(12)+340:readBits(8)+84:readBits(6)+20:readBits(4)+4:readBits(2);let o;0===n?o=s:s>0&&(o=-s);return o>=-2147483648&&o<=va?o:null}function decodeIAID(e,t,a){const r=e.getContexts("IAID");let i=1;for(let e=0;ee.y-t.y||e.x-t.x));const h=l.length,u=new Int8Array(h),d=new Int8Array(h),f=[];let g,p,m=0,b=0,y=0,w=0;for(p=0;p=v&&E=F){q=q<<1&m;for(p=0;p=0&&j=0){_=D[L][j];_&&(q|=_<=e?l<<=1:l=l<<1|x[o][c]}for(f=0;f=w||c<0||c>=y?l<<=1:l=l<<1|r[o][c]}const g=k.readBit(C,l);t[s]=g}}return x}function decodeTextRegion(e,t,a,r,i,n,s,o,c,l,h,u,d,f,g,p,m,b,y){if(e&&t)throw new Jbig2Error("refinement with Huffman is not supported");const w=[];let S,x;for(S=0;S1&&(i=e?y.readBits(b):decodeInteger(C,"IAIT",k));const n=s*v+i,F=e?f.symbolIDTable.decode(y):decodeIAID(C,k,c),O=t&&(e?y.readBit():decodeInteger(C,"IARI",k));let T=o[F],M=T[0].length,D=T.length;if(O){const e=decodeInteger(C,"IARDW",k),t=decodeInteger(C,"IARDH",k);M+=e;D+=t;T=decodeRefinement(M,D,g,T,(e>>1)+decodeInteger(C,"IARDX",k),(t>>1)+decodeInteger(C,"IARDY",k),!1,p,m)}let R=0;l?1&u?R=D-1:r+=D-1:u>1?r+=M-1:R=M-1;const N=n-(1&u?0:D-1),E=r-(2&u?M-1:0);let L,j,_;if(l)for(L=0;L>5&7;const c=[31&s];let l=t+6;if(7===s){o=536870911&readUint32(e,l-1);l+=3;let t=o+7>>3;c[0]=e[l++];for(;--t>0;)c.push(e[l++])}else if(5===s||6===s)throw new Jbig2Error("invalid referred-to flags");a.retainBits=c;let h=4;a.number<=256?h=1:a.number<=65536&&(h=2);const u=[];let d,f;for(d=0;d>>24&255;n[3]=t.height>>16&255;n[4]=t.height>>8&255;n[5]=255&t.height;for(d=l,f=e.length;d>2&3;e.huffmanDWSelector=t>>4&3;e.bitmapSizeSelector=t>>6&1;e.aggregationInstancesSelector=t>>7&1;e.bitmapCodingContextUsed=!!(256&t);e.bitmapCodingContextRetained=!!(512&t);e.template=t>>10&3;e.refinementTemplate=t>>12&1;l+=2;if(!e.huffman){c=0===e.template?4:1;s=[];for(o=0;o>2&3;h.stripSize=1<>4&3;h.transposed=!!(64&u);h.combinationOperator=u>>7&3;h.defaultPixelValue=u>>9&1;h.dsOffset=u<<17>>27;h.refinementTemplate=u>>15&1;if(h.huffman){const e=readUint16(r,l);l+=2;h.huffmanFS=3&e;h.huffmanDS=e>>2&3;h.huffmanDT=e>>4&3;h.huffmanRefinementDW=e>>6&3;h.huffmanRefinementDH=e>>8&3;h.huffmanRefinementDX=e>>10&3;h.huffmanRefinementDY=e>>12&3;h.huffmanRefinementSizeSelector=!!(16384&e)}if(h.refinement&&!h.refinementTemplate){s=[];for(o=0;o<2;o++){s.push({x:readInt8(r,l),y:readInt8(r,l+1)});l+=2}h.refinementAt=s}h.numberOfSymbolInstances=readUint32(r,l);l+=4;n=[h,a.referredTo,r,l,i];break;case 16:const d={},f=r[l++];d.mmr=!!(1&f);d.template=f>>1&3;d.patternWidth=r[l++];d.patternHeight=r[l++];d.maxPatternIndex=readUint32(r,l);l+=4;n=[d,a.number,r,l,i];break;case 22:case 23:const g={};g.info=readRegionSegmentInformation(r,l);l+=gr;const p=r[l++];g.mmr=!!(1&p);g.template=p>>1&3;g.enableSkip=!!(8&p);g.combinationOperator=p>>4&7;g.defaultPixelValue=p>>7&1;g.gridWidth=readUint32(r,l);l+=4;g.gridHeight=readUint32(r,l);l+=4;g.gridOffsetX=4294967295&readUint32(r,l);l+=4;g.gridOffsetY=4294967295&readUint32(r,l);l+=4;g.gridVectorX=readUint16(r,l);l+=2;g.gridVectorY=readUint16(r,l);l+=2;n=[g,a.referredTo,r,l,i];break;case 38:case 39:const m={};m.info=readRegionSegmentInformation(r,l);l+=gr;const b=r[l++];m.mmr=!!(1&b);m.template=b>>1&3;m.prediction=!!(8&b);if(!m.mmr){c=0===m.template?4:1;s=[];for(o=0;o>2&1;y.combinationOperator=w>>3&3;y.requiresBuffer=!!(32&w);y.combinationOperatorOverride=!!(64&w);n=[y];break;case 49:case 50:case 51:case 62:break;case 53:n=[a.number,r,l,i];break;default:throw new Jbig2Error(`segment type ${a.typeName}(${a.type}) is not implemented`)}const h="on"+a.typeName;h in t&&t[h].apply(t,n)}function processSegments(e,t){for(let a=0,r=e.length;a>3,a=new Uint8ClampedArray(t*e.height);e.defaultPixelValue&&a.fill(255);this.buffer=a}drawBitmap(e,t){const a=this.currentPageInfo,r=e.width,i=e.height,n=a.width+7>>3,s=a.combinationOperatorOverride?e.combinationOperator:a.combinationOperator,o=this.buffer,c=128>>(7&e.x);let l,h,u,d,f=e.y*n+(e.x>>3);switch(s){case 0:for(l=0;l>=1;if(!u){u=128;d++}}f+=n}break;case 2:for(l=0;l>=1;if(!u){u=128;d++}}f+=n}break;default:throw new Jbig2Error(`operator ${s} is not supported`)}}onImmediateGenericRegion(e,t,a,r){const i=e.info,n=new DecodingContext(t,a,r),s=decodeBitmap(e.mmr,i.width,i.height,e.template,e.prediction,null,e.at,n);this.drawBitmap(i,s)}onImmediateLosslessGenericRegion(){this.onImmediateGenericRegion(...arguments)}onSymbolDictionary(e,t,a,r,i,n){let s,o;if(e.huffman){s=function getSymbolDictionaryHuffmanTables(e,t,a){let r,i,n,s,o=0;switch(e.huffmanDHSelector){case 0:case 1:r=getStandardTable(e.huffmanDHSelector+4);break;case 3:r=getCustomHuffmanTable(o,t,a);o++;break;default:throw new Jbig2Error("invalid Huffman DH selector")}switch(e.huffmanDWSelector){case 0:case 1:i=getStandardTable(e.huffmanDWSelector+2);break;case 3:i=getCustomHuffmanTable(o,t,a);o++;break;default:throw new Jbig2Error("invalid Huffman DW selector")}if(e.bitmapSizeSelector){n=getCustomHuffmanTable(o,t,a);o++}else n=getStandardTable(1);s=e.aggregationInstancesSelector?getCustomHuffmanTable(o,t,a):getStandardTable(1);return{tableDeltaHeight:r,tableDeltaWidth:i,tableBitmapSize:n,tableAggregateInstances:s}}(e,a,this.customTables);o=new Reader(r,i,n)}let c=this.symbols;c||(this.symbols=c={});const l=[];for(const e of a){const t=c[e];t&&l.push(...t)}const h=new DecodingContext(r,i,n);c[t]=function decodeSymbolDictionary(e,t,a,r,i,n,s,o,c,l,h,u){if(e&&t)throw new Jbig2Error("symbol refinement with Huffman is not supported");const d=[];let f=0,g=log2(a.length+r);const p=h.decoder,m=h.contextCache;let b,y;if(e){b=getStandardTable(1);y=[];g=Math.max(g,1)}for(;d.length1)w=decodeTextRegion(e,t,r,f,0,i,1,a.concat(d),g,0,0,1,0,n,c,l,h,0,u);else{const e=decodeIAID(m,p,g),t=decodeInteger(m,"IARDX",p),i=decodeInteger(m,"IARDY",p);w=decodeRefinement(r,f,c,e=32){let a,r,s;switch(t){case 32:if(0===e)throw new Jbig2Error("no previous value in symbol ID table");r=i.readBits(2)+3;a=n[e-1].prefixLength;break;case 33:r=i.readBits(3)+3;a=0;break;case 34:r=i.readBits(7)+11;a=0;break;default:throw new Jbig2Error("invalid code length in symbol ID table")}for(s=0;s=0;m--){T=e?decodeMMRBitmap(O,c,l,!0):decodeBitmap(!1,c,l,a,!1,null,v,g);F[m]=T}for(M=0;M=0;b--){R^=F[b][M][D];N|=R<>8;j=u+M*d-D*f>>8;if(L>=0&&L+x<=r&&j>=0&&j+k<=i)for(m=0;m=i)){U=p[t];_=E[m];for(b=0;b=0&&e>1&7),c=1+(r>>4&7),l=[];let h,u,d=i;do{h=s.readBits(o);u=s.readBits(c);l.push(new HuffmanLine([d,h,u,0]));d+=1<>t&1;if(t<=0)this.children[a]=new HuffmanTreeNode(e);else{let r=this.children[a];r||(this.children[a]=r=new HuffmanTreeNode(null));r.buildTree(e,t-1)}}decodeNode(e){if(this.isLeaf){if(this.isOOB)return null;const t=e.readBits(this.rangeLength);return this.rangeLow+(this.isLowerRange?-t:t)}const t=this.children[e.readBit()];if(!t)throw new Jbig2Error("invalid Huffman data");return t.decodeNode(e)}}class HuffmanTable{constructor(e,t){t||this.assignPrefixCodes(e);this.rootNode=new HuffmanTreeNode(null);for(let t=0,a=e.length;t0&&this.rootNode.buildTree(a,a.prefixLength-1)}}decode(e){return this.rootNode.decodeNode(e)}assignPrefixCodes(e){const t=e.length;let a=0;for(let r=0;r=this.end)throw new Jbig2Error("end of data while reading bit");this.currentByte=this.data[this.position++];this.shift=7}const e=this.currentByte>>this.shift&1;this.shift--;return e}readBits(e){let t,a=0;for(t=e-1;t>=0;t--)a|=this.readBit()<=this.end?-1:this.data[this.position++]}}function getCustomHuffmanTable(e,t,a){let r=0;for(let i=0,n=t.length;i>a&1;a--}}if(r&&!o){const e=5;for(let t=0;t>>t&(1<0;if(e<256){d[0]=e;f=1}else{if(!(e>=258)){if(256===e){h=9;s=258;f=0;continue}this.eof=!0;delete this.lzwState;break}if(e=0;t--){d[t]=o[a];a=l[a]}}else d[f++]=d[0]}if(i){l[s]=u;c[s]=c[u]+1;o[s]=d[0];s++;h=s+n&s+n-1?h:0|Math.min(Math.log(s+n)/.6931471805599453+1,12)}u=e;g+=f;if(r15))throw new FormatError(`Unsupported predictor: ${r}`);this.readBlock=2===r?this.readBlockTiff:this.readBlockPng;this.str=e;this.dict=e.dict;const i=this.colors=a.get("Colors")||1,n=this.bits=a.get("BPC","BitsPerComponent")||8,s=this.columns=a.get("Columns")||1;this.pixBytes=i*n+7>>3;this.rowBytes=s*i*n+7>>3;return this}readBlockTiff(){const e=this.rowBytes,t=this.bufferLength,a=this.ensureBuffer(t+e),r=this.bits,i=this.colors,n=this.str.getBytes(e);this.eof=!n.length;if(this.eof)return;let s,o=0,c=0,l=0,h=0,u=t;if(1===r&&1===i)for(s=0;s>1;e^=e>>2;e^=e>>4;o=(1&e)<<7;a[u++]=e}else if(8===r){for(s=0;s>8&255;a[u++]=255&e}}else{const e=new Uint8Array(i+1),u=(1<>l-r)&u;l-=r;c=c<=8){a[f++]=c>>h-8&255;h-=8}}h>0&&(a[f++]=(c<<8-h)+(o&(1<<8-h)-1))}this.bufferLength+=e}readBlockPng(){const e=this.rowBytes,t=this.pixBytes,a=this.str.getByte(),r=this.str.getBytes(e);this.eof=!r.length;if(this.eof)return;const i=this.bufferLength,n=this.ensureBuffer(i+e);let s=n.subarray(i-e,i);0===s.length&&(s=new Uint8Array(e));let o,c,l,h=i;switch(a){case 0:for(o=0;o>1)+r[o];for(;o>1)+r[o]&255;h++}break;case 4:for(o=0;o0){const e=this.str.getBytes(r);t.set(e,a);a+=r}}else{r=257-r;t=this.ensureBuffer(a+r+1);t.fill(e[1],a,a+r);a+=r}this.bufferLength=a}}class Parser{constructor({lexer:e,xref:t,allowStreams:a=!1,recoveryMode:r=!1}){this.lexer=e;this.xref=t;this.allowStreams=a;this.recoveryMode=r;this.imageCache=Object.create(null);this._imageId=0;this.refill()}refill(){this.buf1=this.lexer.getObj();this.buf2=this.lexer.getObj()}shift(){if(this.buf2 instanceof Cmd&&"ID"===this.buf2.cmd){this.buf1=this.buf2;this.buf2=null}else{this.buf1=this.buf2;this.buf2=this.lexer.getObj()}}tryShift(){try{this.shift();return!0}catch(e){if(e instanceof MissingDataException)throw e;return!1}}getObj(e=null){const t=this.buf1;this.shift();if(t instanceof Cmd)switch(t.cmd){case"BI":return this.makeInlineImage(e);case"[":const a=[];for(;!isCmd(this.buf1,"]")&&this.buf1!==wa;)a.push(this.getObj(e));if(this.buf1===wa){if(this.recoveryMode)return a;throw new ParserEOFException("End of file inside array.")}this.shift();return a;case"<<":const r=new Dict(this.xref);for(;!isCmd(this.buf1,">>")&&this.buf1!==wa;){if(!(this.buf1 instanceof Name)){info("Malformed dictionary: key must be a name object");this.shift();continue}const t=this.buf1.name;this.shift();if(this.buf1===wa)break;r.set(t,this.getObj(e))}if(this.buf1===wa){if(this.recoveryMode)return r;throw new ParserEOFException("End of file inside dictionary.")}if(isCmd(this.buf2,"stream"))return this.allowStreams?this.makeStream(r,e):r;this.shift();return r;default:return t}if(Number.isInteger(t)){if(Number.isInteger(this.buf1)&&isCmd(this.buf2,"R")){const e=Ref.get(t,this.buf1);this.shift();this.shift();return e}return t}return"string"==typeof t&&e?e.decryptString(t):t}findDefaultInlineStreamEnd(e){const{knownCommands:t}=this.lexer,a=e.pos;let r,i,n=0;for(;-1!==(r=e.getByte());)if(0===n)n=69===r?1:0;else if(1===n)n=73===r?2:0;else if(32===r||10===r||13===r){i=e.pos;const a=e.peekBytes(15),s=a.length;if(0===s)break;for(let e=0;e127))){n=0;break}}if(2!==n)continue;if(!t){warn("findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined.");continue}const o=new Lexer(new Stream(e.peekBytes(75)),t);o._hexStringWarn=()=>{};let c=0;for(;;){const e=o.getObj();if(e===wa){n=0;break}if(e instanceof Cmd){const a=t[e.cmd];if(!a){n=0;break}if(a.variableArgs?c<=a.numArgs:c===a.numArgs)break;c=0}else c++}if(2===n)break}else n=0;if(-1===r){warn("findDefaultInlineStreamEnd: Reached the end of the stream without finding a valid EI marker");if(i){warn('... trying to recover by using the last "EI" occurrence.');e.skip(-(e.pos-i))}}let s=4;e.skip(-s);r=e.peekByte();e.skip(s);isWhiteSpace(r)||s--;return e.pos-s-a}findDCTDecodeInlineStreamEnd(e){const t=e.pos;let a,r,i=!1;for(;-1!==(a=e.getByte());)if(255===a){switch(e.getByte()){case 0:break;case 255:e.skip(-1);break;case 217:i=!0;break;case 192:case 193:case 194:case 195:case 197:case 198:case 199:case 201:case 202:case 203:case 205:case 206:case 207:case 196:case 204:case 218:case 219:case 220:case 221:case 222:case 223:case 224:case 225:case 226:case 227:case 228:case 229:case 230:case 231:case 232:case 233:case 234:case 235:case 236:case 237:case 238:case 239:case 254:r=e.getUint16();r>2?e.skip(r-2):e.skip(-2)}if(i)break}const n=e.pos-t;if(-1===a){warn("Inline DCTDecode image stream: EOI marker not found, searching for /EI/ instead.");e.skip(-n);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return n}findASCII85DecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte());)if(126===a){const t=e.pos;a=e.peekByte();for(;isWhiteSpace(a);){e.skip();a=e.peekByte()}if(62===a){e.skip();break}if(e.pos>t){const t=e.peekBytes(2);if(69===t[0]&&73===t[1])break}}const r=e.pos-t;if(-1===a){warn("Inline ASCII85Decode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}findASCIIHexDecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte())&&62!==a;);const r=e.pos-t;if(-1===a){warn("Inline ASCIIHexDecode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}inlineStreamSkipEI(e){let t,a=0;for(;-1!==(t=e.getByte());)if(0===a)a=69===t?1:0;else if(1===a)a=73===t?2:0;else if(2===a)break}makeInlineImage(e){const t=this.lexer,a=t.stream,r=Object.create(null);let i;for(;!isCmd(this.buf1,"ID")&&this.buf1!==wa;){if(!(this.buf1 instanceof Name))throw new FormatError("Dictionary key must be a name object");const t=this.buf1.name;this.shift();if(this.buf1===wa)break;r[t]=this.getObj(e)}-1!==t.beginInlineImagePos&&(i=a.pos-t.beginInlineImagePos);const n=this.xref.fetchIfRef(r.F||r.Filter);let s;if(n instanceof Name)s=n.name;else if(Array.isArray(n)){const e=this.xref.fetchIfRef(n[0]);e instanceof Name&&(s=e.name)}const o=a.pos;let c,l;switch(s){case"DCT":case"DCTDecode":c=this.findDCTDecodeInlineStreamEnd(a);break;case"A85":case"ASCII85Decode":c=this.findASCII85DecodeInlineStreamEnd(a);break;case"AHx":case"ASCIIHexDecode":c=this.findASCIIHexDecodeInlineStreamEnd(a);break;default:c=this.findDefaultInlineStreamEnd(a)}if(c<1e3&&i>0){const e=a.pos;a.pos=t.beginInlineImagePos;l=function getInlineImageCacheKey(e){const t=[],a=e.length;let r=0;for(;r=r){let r=!1;for(const e of i){const t=e.length;let i=0;for(;i=n){r=!0;break}if(i>=t){if(isWhiteSpace(s[c+o+i])){info(`Found "${bytesToString([...a,...e])}" when searching for endstream command.`);r=!0}break}}if(r){t.pos+=c;return t.pos-e}}c++}t.pos+=o}return-1}makeStream(e,t){const a=this.lexer;let r=a.stream;a.skipToNextLine();const i=r.pos-1;let n=e.get("Length");if(!Number.isInteger(n)){info(`Bad length "${n&&n.toString()}" in stream.`);n=0}r.pos=i+n;a.nextChar();if(this.tryShift()&&isCmd(this.buf2,"endstream"))this.shift();else{n=this.#q(i);if(n<0)throw new FormatError("Missing endstream command.");a.nextChar();this.shift();this.shift()}this.shift();r=r.makeSubStream(i,n,e);t&&(r=t.createStream(r,n));r=this.filter(r,e,n);r.dict=e;return r}filter(e,t,a){let r=t.get("F","Filter"),i=t.get("DP","DecodeParms");if(r instanceof Name){Array.isArray(i)&&warn("/DecodeParms should not be an Array, when /Filter is a Name.");return this.makeFilter(e,r.name,a,i)}let n=a;if(Array.isArray(r)){const t=r,a=i;for(let s=0,o=t.length;s=48&&e<=57?15&e:e>=65&&e<=70||e>=97&&e<=102?9+(15&e):-1}class Lexer{constructor(e,t=null){this.stream=e;this.nextChar();this.strBuf=[];this.knownCommands=t;this._hexStringNumWarn=0;this.beginInlineImagePos=-1}nextChar(){return this.currentChar=this.stream.getByte()}peekChar(){return this.stream.peekByte()}getNumber(){let e=this.currentChar,t=!1,a=0,r=1;if(45===e){r=-1;e=this.nextChar();45===e&&(e=this.nextChar())}else 43===e&&(e=this.nextChar());if(10===e||13===e)do{e=this.nextChar()}while(10===e||13===e);if(46===e){a=10;e=this.nextChar()}if(e<48||e>57){const t=`Invalid number: ${String.fromCharCode(e)} (charCode ${e})`;if(isWhiteSpace(e)||40===e||60===e||-1===e){info(`Lexer.getNumber - "${t}".`);return 0}throw new FormatError(t)}let i=e-48,n=0,s=1;for(;(e=this.nextChar())>=0;)if(e>=48&&e<=57){const r=e-48;if(t)n=10*n+r;else{0!==a&&(a*=10);i=10*i+r}}else if(46===e){if(0!==a)break;a=1}else if(45===e)warn("Badly formatted number: minus sign in the middle");else{if(69!==e&&101!==e)break;e=this.peekChar();if(43===e||45===e){s=45===e?-1:1;this.nextChar()}else if(e<48||e>57)break;t=!0}0!==a&&(i/=a);t&&(i*=10**(s*n));return r*i}getString(){let e=1,t=!1;const a=this.strBuf;a.length=0;let r=this.nextChar();for(;;){let i=!1;switch(0|r){case-1:warn("Unterminated string");t=!0;break;case 40:++e;a.push("(");break;case 41:if(0==--e){this.nextChar();t=!0}else a.push(")");break;case 92:r=this.nextChar();switch(r){case-1:warn("Unterminated string");t=!0;break;case 110:a.push("\n");break;case 114:a.push("\r");break;case 116:a.push("\t");break;case 98:a.push("\b");break;case 102:a.push("\f");break;case 92:case 40:case 41:a.push(String.fromCharCode(r));break;case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:let e=15&r;r=this.nextChar();i=!0;if(r>=48&&r<=55){e=(e<<3)+(15&r);r=this.nextChar();if(r>=48&&r<=55){i=!1;e=(e<<3)+(15&r)}}a.push(String.fromCharCode(e));break;case 13:10===this.peekChar()&&this.nextChar();break;case 10:break;default:a.push(String.fromCharCode(r))}break;default:a.push(String.fromCharCode(r))}if(t)break;i||(r=this.nextChar())}return a.join("")}getName(){let e,t;const a=this.strBuf;a.length=0;for(;(e=this.nextChar())>=0&&!mr[e];)if(35===e){e=this.nextChar();if(mr[e]){warn("Lexer_getName: NUMBER SIGN (#) should be followed by a hexadecimal number.");a.push("#");break}const r=toHexDigit(e);if(-1!==r){t=e;e=this.nextChar();const i=toHexDigit(e);if(-1===i){warn(`Lexer_getName: Illegal digit (${String.fromCharCode(e)}) in hexadecimal number.`);a.push("#",String.fromCharCode(t));if(mr[e])break;a.push(String.fromCharCode(e));continue}a.push(String.fromCharCode(r<<4|i))}else a.push("#",String.fromCharCode(e))}else a.push(String.fromCharCode(e));a.length>127&&warn(`Name token is longer than allowed by the spec: ${a.length}`);return Name.get(a.join(""))}_hexStringWarn(e){5!=this._hexStringNumWarn++?this._hexStringNumWarn>5||warn(`getHexString - ignoring invalid character: ${e}`):warn("getHexString - ignoring additional invalid characters.")}getHexString(){const e=this.strBuf;e.length=0;let t=this.currentChar,a=-1,r=-1;this._hexStringNumWarn=0;for(;;){if(t<0){warn("Unterminated hex string");break}if(62===t){this.nextChar();break}if(1!==mr[t]){r=toHexDigit(t);if(-1===r)this._hexStringWarn(t);else if(-1===a)a=r;else{e.push(String.fromCharCode(a<<4|r));a=-1}t=this.nextChar()}else t=this.nextChar()}-1!==a&&e.push(String.fromCharCode(a<<4));return e.join("")}getObj(){let e=!1,t=this.currentChar;for(;;){if(t<0)return wa;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(1!==mr[t])break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return this.getNumber();case 40:return this.getString();case 47:return this.getName();case 91:this.nextChar();return Cmd.get("[");case 93:this.nextChar();return Cmd.get("]");case 60:t=this.nextChar();if(60===t){this.nextChar();return Cmd.get("<<")}return this.getHexString();case 62:t=this.nextChar();if(62===t){this.nextChar();return Cmd.get(">>")}return Cmd.get(">");case 123:this.nextChar();return Cmd.get("{");case 125:this.nextChar();return Cmd.get("}");case 41:this.nextChar();throw new FormatError(`Illegal character: ${t}`)}let a=String.fromCharCode(t);if(t<32||t>127){const e=this.peekChar();if(e>=32&&e<=127){this.nextChar();return Cmd.get(a)}}const r=this.knownCommands;let i=void 0!==r?.[a];for(;(t=this.nextChar())>=0&&!mr[t];){const e=a+String.fromCharCode(t);if(i&&void 0===r[e])break;if(128===a.length)throw new FormatError(`Command token too long: ${a.length}`);a=e;i=void 0!==r?.[a]}if("true"===a)return!0;if("false"===a)return!1;if("null"===a)return null;"BI"===a&&(this.beginInlineImagePos=this.stream.pos);return Cmd.get(a)}skipToNextLine(){let e=this.currentChar;for(;e>=0;){if(13===e){e=this.nextChar();10===e&&this.nextChar();break}if(10===e){this.nextChar();break}e=this.nextChar()}}}class Linearization{static create(e){function getInt(e,t,a=!1){const r=e.get(t);if(Number.isInteger(r)&&(a?r>=0:r>0))return r;throw new Error(`The "${t}" parameter in the linearization dictionary is invalid.`)}const t=new Parser({lexer:new Lexer(e),xref:null}),a=t.getObj(),r=t.getObj(),i=t.getObj(),n=t.getObj();let s,o;if(!(Number.isInteger(a)&&Number.isInteger(r)&&isCmd(i,"obj")&&n instanceof Dict&&"number"==typeof(s=n.get("Linearized"))&&s>0))return null;if((o=getInt(n,"L"))!==e.length)throw new Error('The "L" parameter in the linearization dictionary does not equal the stream length.');return{length:o,hints:function getHints(e){const t=e.get("H");let a;if(Array.isArray(t)&&(2===(a=t.length)||4===a)){for(let e=0;e0))throw new Error(`Hint (${e}) in the linearization dictionary is invalid.`)}return t}throw new Error("Hint array in the linearization dictionary is invalid.")}(n),objectNumberFirst:getInt(n,"O"),endFirst:getInt(n,"E"),numPages:getInt(n,"N"),mainXRefEntriesOffset:getInt(n,"T"),pageFirst:n.has("P")?getInt(n,"P",!0):0}}}const br=["Adobe-GB1-UCS2","Adobe-CNS1-UCS2","Adobe-Japan1-UCS2","Adobe-Korea1-UCS2","78-EUC-H","78-EUC-V","78-H","78-RKSJ-H","78-RKSJ-V","78-V","78ms-RKSJ-H","78ms-RKSJ-V","83pv-RKSJ-H","90ms-RKSJ-H","90ms-RKSJ-V","90msp-RKSJ-H","90msp-RKSJ-V","90pv-RKSJ-H","90pv-RKSJ-V","Add-H","Add-RKSJ-H","Add-RKSJ-V","Add-V","Adobe-CNS1-0","Adobe-CNS1-1","Adobe-CNS1-2","Adobe-CNS1-3","Adobe-CNS1-4","Adobe-CNS1-5","Adobe-CNS1-6","Adobe-GB1-0","Adobe-GB1-1","Adobe-GB1-2","Adobe-GB1-3","Adobe-GB1-4","Adobe-GB1-5","Adobe-Japan1-0","Adobe-Japan1-1","Adobe-Japan1-2","Adobe-Japan1-3","Adobe-Japan1-4","Adobe-Japan1-5","Adobe-Japan1-6","Adobe-Korea1-0","Adobe-Korea1-1","Adobe-Korea1-2","B5-H","B5-V","B5pc-H","B5pc-V","CNS-EUC-H","CNS-EUC-V","CNS1-H","CNS1-V","CNS2-H","CNS2-V","ETHK-B5-H","ETHK-B5-V","ETen-B5-H","ETen-B5-V","ETenms-B5-H","ETenms-B5-V","EUC-H","EUC-V","Ext-H","Ext-RKSJ-H","Ext-RKSJ-V","Ext-V","GB-EUC-H","GB-EUC-V","GB-H","GB-V","GBK-EUC-H","GBK-EUC-V","GBK2K-H","GBK2K-V","GBKp-EUC-H","GBKp-EUC-V","GBT-EUC-H","GBT-EUC-V","GBT-H","GBT-V","GBTpc-EUC-H","GBTpc-EUC-V","GBpc-EUC-H","GBpc-EUC-V","H","HKdla-B5-H","HKdla-B5-V","HKdlb-B5-H","HKdlb-B5-V","HKgccs-B5-H","HKgccs-B5-V","HKm314-B5-H","HKm314-B5-V","HKm471-B5-H","HKm471-B5-V","HKscs-B5-H","HKscs-B5-V","Hankaku","Hiragana","KSC-EUC-H","KSC-EUC-V","KSC-H","KSC-Johab-H","KSC-Johab-V","KSC-V","KSCms-UHC-H","KSCms-UHC-HW-H","KSCms-UHC-HW-V","KSCms-UHC-V","KSCpc-EUC-H","KSCpc-EUC-V","Katakana","NWP-H","NWP-V","RKSJ-H","RKSJ-V","Roman","UniCNS-UCS2-H","UniCNS-UCS2-V","UniCNS-UTF16-H","UniCNS-UTF16-V","UniCNS-UTF32-H","UniCNS-UTF32-V","UniCNS-UTF8-H","UniCNS-UTF8-V","UniGB-UCS2-H","UniGB-UCS2-V","UniGB-UTF16-H","UniGB-UTF16-V","UniGB-UTF32-H","UniGB-UTF32-V","UniGB-UTF8-H","UniGB-UTF8-V","UniJIS-UCS2-H","UniJIS-UCS2-HW-H","UniJIS-UCS2-HW-V","UniJIS-UCS2-V","UniJIS-UTF16-H","UniJIS-UTF16-V","UniJIS-UTF32-H","UniJIS-UTF32-V","UniJIS-UTF8-H","UniJIS-UTF8-V","UniJIS2004-UTF16-H","UniJIS2004-UTF16-V","UniJIS2004-UTF32-H","UniJIS2004-UTF32-V","UniJIS2004-UTF8-H","UniJIS2004-UTF8-V","UniJISPro-UCS2-HW-V","UniJISPro-UCS2-V","UniJISPro-UTF8-V","UniJISX0213-UTF32-H","UniJISX0213-UTF32-V","UniJISX02132004-UTF32-H","UniJISX02132004-UTF32-V","UniKS-UCS2-H","UniKS-UCS2-V","UniKS-UTF16-H","UniKS-UTF16-V","UniKS-UTF32-H","UniKS-UTF32-V","UniKS-UTF8-H","UniKS-UTF8-V","V","WP-Symbol"],yr=2**24-1;class CMap{constructor(e=!1){this.codespaceRanges=[[],[],[],[]];this.numCodespaceRanges=0;this._map=[];this.name="";this.vertical=!1;this.useCMap=null;this.builtInCMap=e}addCodespaceRange(e,t,a){this.codespaceRanges[e-1].push(t,a);this.numCodespaceRanges++}mapCidRange(e,t,a){if(t-e>yr)throw new Error("mapCidRange - ignoring data above MAX_MAP_RANGE.");for(;e<=t;)this._map[e++]=a++}mapBfRange(e,t,a){if(t-e>yr)throw new Error("mapBfRange - ignoring data above MAX_MAP_RANGE.");const r=a.length-1;for(;e<=t;){this._map[e++]=a;const t=a.charCodeAt(r)+1;t>255?a=a.substring(0,r-1)+String.fromCharCode(a.charCodeAt(r-1)+1)+"\0":a=a.substring(0,r)+String.fromCharCode(t)}}mapBfRangeToArray(e,t,a){if(t-e>yr)throw new Error("mapBfRangeToArray - ignoring data above MAX_MAP_RANGE.");const r=a.length;let i=0;for(;e<=t&&i>>0;const s=i[n];for(let e=0,t=s.length;e=t&&r<=i){a.charcode=r;a.length=n+1;return}}}a.charcode=0;a.length=1}getCharCodeLength(e){const t=this.codespaceRanges;for(let a=0,r=t.length;a=i&&e<=n)return a+1}}return 1}get length(){return this._map.length}get isIdentityCMap(){if("Identity-H"!==this.name&&"Identity-V"!==this.name)return!1;if(65536!==this._map.length)return!1;for(let e=0;e<65536;e++)if(this._map[e]!==e)return!1;return!0}}class IdentityCMap extends CMap{constructor(e,t){super();this.vertical=e;this.addCodespaceRange(t,0,65535)}mapCidRange(e,t,a){unreachable("should not call mapCidRange")}mapBfRange(e,t,a){unreachable("should not call mapBfRange")}mapBfRangeToArray(e,t,a){unreachable("should not call mapBfRangeToArray")}mapOne(e,t){unreachable("should not call mapCidOne")}lookup(e){return Number.isInteger(e)&&e<=65535?e:void 0}contains(e){return Number.isInteger(e)&&e<=65535}forEach(e){for(let t=0;t<=65535;t++)e(t,t)}charCodeOf(e){return Number.isInteger(e)&&e<=65535?e:-1}getMap(){const e=new Array(65536);for(let t=0;t<=65535;t++)e[t]=t;return e}get length(){return 65536}get isIdentityCMap(){unreachable("should not access .isIdentityCMap")}}function strToInt(e){let t=0;for(let a=0;a>>0}function expectString(e){if("string"!=typeof e)throw new FormatError("Malformed CMap: expected string.")}function expectInt(e){if(!Number.isInteger(e))throw new FormatError("Malformed CMap: expected int.")}function parseBfChar(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endbfchar"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=a;e.mapOne(r,i)}}function parseBfRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endbfrange"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=strToInt(a);a=t.getObj();if(Number.isInteger(a)||"string"==typeof a){const t=Number.isInteger(a)?String.fromCharCode(a):a;e.mapBfRange(r,i,t)}else{if(!isCmd(a,"["))break;{a=t.getObj();const n=[];for(;!isCmd(a,"]")&&a!==wa;){n.push(a);a=t.getObj()}e.mapBfRangeToArray(r,i,n)}}}throw new FormatError("Invalid bf range.")}function parseCidChar(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcidchar"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectInt(a);const i=a;e.mapOne(r,i)}}function parseCidRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcidrange"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=strToInt(a);a=t.getObj();expectInt(a);const n=a;e.mapCidRange(r,i,n)}}function parseCodespaceRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcodespacerange"))return;if("string"!=typeof a)break;const r=strToInt(a);a=t.getObj();if("string"!=typeof a)break;const i=strToInt(a);e.addCodespaceRange(a.length,r,i)}throw new FormatError("Invalid codespace range.")}function parseWMode(e,t){const a=t.getObj();Number.isInteger(a)&&(e.vertical=!!a)}function parseCMapName(e,t){const a=t.getObj();a instanceof Name&&(e.name=a.name)}async function parseCMap(e,t,a,r){let i,n;e:for(;;)try{const a=t.getObj();if(a===wa)break;if(a instanceof Name){"WMode"===a.name?parseWMode(e,t):"CMapName"===a.name&&parseCMapName(e,t);i=a}else if(a instanceof Cmd)switch(a.cmd){case"endcmap":break e;case"usecmap":i instanceof Name&&(n=i.name);break;case"begincodespacerange":parseCodespaceRange(e,t);break;case"beginbfchar":parseBfChar(e,t);break;case"begincidchar":parseCidChar(e,t);break;case"beginbfrange":parseBfRange(e,t);break;case"begincidrange":parseCidRange(e,t)}}catch(e){if(e instanceof MissingDataException)throw e;warn("Invalid cMap data: "+e);continue}!r&&n&&(r=n);return r?extendCMap(e,a,r):e}async function extendCMap(e,t,a){e.useCMap=await createBuiltInCMap(a,t);if(0===e.numCodespaceRanges){const t=e.useCMap.codespaceRanges;for(let a=0;aextendCMap(i,t,e)));const n=new Lexer(new Stream(a));return parseCMap(i,n,t,null)}class CMapFactory{static async create({encoding:e,fetchBuiltInCMap:t,useCMap:a}){if(e instanceof Name)return createBuiltInCMap(e.name,t);if(e instanceof BaseStream){const r=await parseCMap(new CMap,new Lexer(e),t,a);return r.isIdentityCMap?createBuiltInCMap(r.name,t):r}throw new Error("Encoding required.")}}const wr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","","asuperior","bsuperior","centsuperior","dsuperior","esuperior","","","","isuperior","","","lsuperior","msuperior","nsuperior","osuperior","","","rsuperior","ssuperior","tsuperior","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdownsmall","centoldstyle","Lslashsmall","","","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","","Dotaccentsmall","","","Macronsmall","","","figuredash","hypheninferior","","","Ogoneksmall","Ringsmall","Cedillasmall","","","","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],Sr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","centoldstyle","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","","threequartersemdash","","questionsmall","","","","","Ethsmall","","","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","","","","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hypheninferior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","asuperior","centsuperior","","","","","Aacutesmall","Agravesmall","Acircumflexsmall","Adieresissmall","Atildesmall","Aringsmall","Ccedillasmall","Eacutesmall","Egravesmall","Ecircumflexsmall","Edieresissmall","Iacutesmall","Igravesmall","Icircumflexsmall","Idieresissmall","Ntildesmall","Oacutesmall","Ogravesmall","Ocircumflexsmall","Odieresissmall","Otildesmall","Uacutesmall","Ugravesmall","Ucircumflexsmall","Udieresissmall","","eightsuperior","fourinferior","threeinferior","sixinferior","eightinferior","seveninferior","Scaronsmall","","centinferior","twoinferior","","Dieresissmall","","Caronsmall","osuperior","fiveinferior","","commainferior","periodinferior","Yacutesmall","","dollarinferior","","","Thornsmall","","nineinferior","zeroinferior","Zcaronsmall","AEsmall","Oslashsmall","questiondownsmall","oneinferior","Lslashsmall","","","","","","","Cedillasmall","","","","","","OEsmall","figuredash","hyphensuperior","","","","","exclamdownsmall","","Ydieresissmall","","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","ninesuperior","zerosuperior","","esuperior","rsuperior","tsuperior","","","isuperior","ssuperior","dsuperior","","","","","","lsuperior","Ogoneksmall","Brevesmall","Macronsmall","bsuperior","nsuperior","msuperior","commasuperior","periodsuperior","Dotaccentsmall","Ringsmall","","","",""],xr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","space","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron"],Ar=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","","endash","dagger","daggerdbl","periodcentered","","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","","questiondown","","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","","ring","cedilla","","hungarumlaut","ogonek","caron","emdash","","","","","","","","","","","","","","","","","AE","","ordfeminine","","","","","Lslash","Oslash","OE","ordmasculine","","","","","","ae","","","","dotlessi","","","lslash","oslash","oe","germandbls","","","",""],kr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","bullet","Euro","bullet","quotesinglbase","florin","quotedblbase","ellipsis","dagger","daggerdbl","circumflex","perthousand","Scaron","guilsinglleft","OE","bullet","Zcaron","bullet","bullet","quoteleft","quoteright","quotedblleft","quotedblright","bullet","endash","emdash","tilde","trademark","scaron","guilsinglright","oe","bullet","zcaron","Ydieresis","space","exclamdown","cent","sterling","currency","yen","brokenbar","section","dieresis","copyright","ordfeminine","guillemotleft","logicalnot","hyphen","registered","macron","degree","plusminus","twosuperior","threesuperior","acute","mu","paragraph","periodcentered","cedilla","onesuperior","ordmasculine","guillemotright","onequarter","onehalf","threequarters","questiondown","Agrave","Aacute","Acircumflex","Atilde","Adieresis","Aring","AE","Ccedilla","Egrave","Eacute","Ecircumflex","Edieresis","Igrave","Iacute","Icircumflex","Idieresis","Eth","Ntilde","Ograve","Oacute","Ocircumflex","Otilde","Odieresis","multiply","Oslash","Ugrave","Uacute","Ucircumflex","Udieresis","Yacute","Thorn","germandbls","agrave","aacute","acircumflex","atilde","adieresis","aring","ae","ccedilla","egrave","eacute","ecircumflex","edieresis","igrave","iacute","icircumflex","idieresis","eth","ntilde","ograve","oacute","ocircumflex","otilde","odieresis","divide","oslash","ugrave","uacute","ucircumflex","udieresis","yacute","thorn","ydieresis"],Cr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","universal","numbersign","existential","percent","ampersand","suchthat","parenleft","parenright","asteriskmath","plus","comma","minus","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","congruent","Alpha","Beta","Chi","Delta","Epsilon","Phi","Gamma","Eta","Iota","theta1","Kappa","Lambda","Mu","Nu","Omicron","Pi","Theta","Rho","Sigma","Tau","Upsilon","sigma1","Omega","Xi","Psi","Zeta","bracketleft","therefore","bracketright","perpendicular","underscore","radicalex","alpha","beta","chi","delta","epsilon","phi","gamma","eta","iota","phi1","kappa","lambda","mu","nu","omicron","pi","theta","rho","sigma","tau","upsilon","omega1","omega","xi","psi","zeta","braceleft","bar","braceright","similar","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Euro","Upsilon1","minute","lessequal","fraction","infinity","florin","club","diamond","heart","spade","arrowboth","arrowleft","arrowup","arrowright","arrowdown","degree","plusminus","second","greaterequal","multiply","proportional","partialdiff","bullet","divide","notequal","equivalence","approxequal","ellipsis","arrowvertex","arrowhorizex","carriagereturn","aleph","Ifraktur","Rfraktur","weierstrass","circlemultiply","circleplus","emptyset","intersection","union","propersuperset","reflexsuperset","notsubset","propersubset","reflexsubset","element","notelement","angle","gradient","registerserif","copyrightserif","trademarkserif","product","radical","dotmath","logicalnot","logicaland","logicalor","arrowdblboth","arrowdblleft","arrowdblup","arrowdblright","arrowdbldown","lozenge","angleleft","registersans","copyrightsans","trademarksans","summation","parenlefttp","parenleftex","parenleftbt","bracketlefttp","bracketleftex","bracketleftbt","bracelefttp","braceleftmid","braceleftbt","braceex","","angleright","integral","integraltp","integralex","integralbt","parenrighttp","parenrightex","parenrightbt","bracketrighttp","bracketrightex","bracketrightbt","bracerighttp","bracerightmid","bracerightbt",""],vr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","a1","a2","a202","a3","a4","a5","a119","a118","a117","a11","a12","a13","a14","a15","a16","a105","a17","a18","a19","a20","a21","a22","a23","a24","a25","a26","a27","a28","a6","a7","a8","a9","a10","a29","a30","a31","a32","a33","a34","a35","a36","a37","a38","a39","a40","a41","a42","a43","a44","a45","a46","a47","a48","a49","a50","a51","a52","a53","a54","a55","a56","a57","a58","a59","a60","a61","a62","a63","a64","a65","a66","a67","a68","a69","a70","a71","a72","a73","a74","a203","a75","a204","a76","a77","a78","a79","a81","a82","a83","a84","a97","a98","a99","a100","","a89","a90","a93","a94","a91","a92","a205","a85","a206","a86","a87","a88","a95","a96","","","","","","","","","","","","","","","","","","","","a101","a102","a103","a104","a106","a107","a108","a112","a111","a110","a109","a120","a121","a122","a123","a124","a125","a126","a127","a128","a129","a130","a131","a132","a133","a134","a135","a136","a137","a138","a139","a140","a141","a142","a143","a144","a145","a146","a147","a148","a149","a150","a151","a152","a153","a154","a155","a156","a157","a158","a159","a160","a161","a163","a164","a196","a165","a192","a166","a167","a168","a169","a170","a171","a172","a173","a162","a174","a175","a176","a177","a178","a179","a193","a180","a199","a181","a200","a182","","a201","a183","a184","a197","a185","a194","a198","a186","a195","a187","a188","a189","a190","a191",""];function getEncoding(e){switch(e){case"WinAnsiEncoding":return kr;case"StandardEncoding":return Ar;case"MacRomanEncoding":return xr;case"SymbolSetEncoding":return Cr;case"ZapfDingbatsEncoding":return vr;case"ExpertEncoding":return wr;case"MacExpertEncoding":return Sr;default:return null}}const Fr=getLookupTableFactory((function(e){e.A=65;e.AE=198;e.AEacute=508;e.AEmacron=482;e.AEsmall=63462;e.Aacute=193;e.Aacutesmall=63457;e.Abreve=258;e.Abreveacute=7854;e.Abrevecyrillic=1232;e.Abrevedotbelow=7862;e.Abrevegrave=7856;e.Abrevehookabove=7858;e.Abrevetilde=7860;e.Acaron=461;e.Acircle=9398;e.Acircumflex=194;e.Acircumflexacute=7844;e.Acircumflexdotbelow=7852;e.Acircumflexgrave=7846;e.Acircumflexhookabove=7848;e.Acircumflexsmall=63458;e.Acircumflextilde=7850;e.Acute=63177;e.Acutesmall=63412;e.Acyrillic=1040;e.Adblgrave=512;e.Adieresis=196;e.Adieresiscyrillic=1234;e.Adieresismacron=478;e.Adieresissmall=63460;e.Adotbelow=7840;e.Adotmacron=480;e.Agrave=192;e.Agravesmall=63456;e.Ahookabove=7842;e.Aiecyrillic=1236;e.Ainvertedbreve=514;e.Alpha=913;e.Alphatonos=902;e.Amacron=256;e.Amonospace=65313;e.Aogonek=260;e.Aring=197;e.Aringacute=506;e.Aringbelow=7680;e.Aringsmall=63461;e.Asmall=63329;e.Atilde=195;e.Atildesmall=63459;e.Aybarmenian=1329;e.B=66;e.Bcircle=9399;e.Bdotaccent=7682;e.Bdotbelow=7684;e.Becyrillic=1041;e.Benarmenian=1330;e.Beta=914;e.Bhook=385;e.Blinebelow=7686;e.Bmonospace=65314;e.Brevesmall=63220;e.Bsmall=63330;e.Btopbar=386;e.C=67;e.Caarmenian=1342;e.Cacute=262;e.Caron=63178;e.Caronsmall=63221;e.Ccaron=268;e.Ccedilla=199;e.Ccedillaacute=7688;e.Ccedillasmall=63463;e.Ccircle=9400;e.Ccircumflex=264;e.Cdot=266;e.Cdotaccent=266;e.Cedillasmall=63416;e.Chaarmenian=1353;e.Cheabkhasiancyrillic=1212;e.Checyrillic=1063;e.Chedescenderabkhasiancyrillic=1214;e.Chedescendercyrillic=1206;e.Chedieresiscyrillic=1268;e.Cheharmenian=1347;e.Chekhakassiancyrillic=1227;e.Cheverticalstrokecyrillic=1208;e.Chi=935;e.Chook=391;e.Circumflexsmall=63222;e.Cmonospace=65315;e.Coarmenian=1361;e.Csmall=63331;e.D=68;e.DZ=497;e.DZcaron=452;e.Daarmenian=1332;e.Dafrican=393;e.Dcaron=270;e.Dcedilla=7696;e.Dcircle=9401;e.Dcircumflexbelow=7698;e.Dcroat=272;e.Ddotaccent=7690;e.Ddotbelow=7692;e.Decyrillic=1044;e.Deicoptic=1006;e.Delta=8710;e.Deltagreek=916;e.Dhook=394;e.Dieresis=63179;e.DieresisAcute=63180;e.DieresisGrave=63181;e.Dieresissmall=63400;e.Digammagreek=988;e.Djecyrillic=1026;e.Dlinebelow=7694;e.Dmonospace=65316;e.Dotaccentsmall=63223;e.Dslash=272;e.Dsmall=63332;e.Dtopbar=395;e.Dz=498;e.Dzcaron=453;e.Dzeabkhasiancyrillic=1248;e.Dzecyrillic=1029;e.Dzhecyrillic=1039;e.E=69;e.Eacute=201;e.Eacutesmall=63465;e.Ebreve=276;e.Ecaron=282;e.Ecedillabreve=7708;e.Echarmenian=1333;e.Ecircle=9402;e.Ecircumflex=202;e.Ecircumflexacute=7870;e.Ecircumflexbelow=7704;e.Ecircumflexdotbelow=7878;e.Ecircumflexgrave=7872;e.Ecircumflexhookabove=7874;e.Ecircumflexsmall=63466;e.Ecircumflextilde=7876;e.Ecyrillic=1028;e.Edblgrave=516;e.Edieresis=203;e.Edieresissmall=63467;e.Edot=278;e.Edotaccent=278;e.Edotbelow=7864;e.Efcyrillic=1060;e.Egrave=200;e.Egravesmall=63464;e.Eharmenian=1335;e.Ehookabove=7866;e.Eightroman=8551;e.Einvertedbreve=518;e.Eiotifiedcyrillic=1124;e.Elcyrillic=1051;e.Elevenroman=8554;e.Emacron=274;e.Emacronacute=7702;e.Emacrongrave=7700;e.Emcyrillic=1052;e.Emonospace=65317;e.Encyrillic=1053;e.Endescendercyrillic=1186;e.Eng=330;e.Enghecyrillic=1188;e.Enhookcyrillic=1223;e.Eogonek=280;e.Eopen=400;e.Epsilon=917;e.Epsilontonos=904;e.Ercyrillic=1056;e.Ereversed=398;e.Ereversedcyrillic=1069;e.Escyrillic=1057;e.Esdescendercyrillic=1194;e.Esh=425;e.Esmall=63333;e.Eta=919;e.Etarmenian=1336;e.Etatonos=905;e.Eth=208;e.Ethsmall=63472;e.Etilde=7868;e.Etildebelow=7706;e.Euro=8364;e.Ezh=439;e.Ezhcaron=494;e.Ezhreversed=440;e.F=70;e.Fcircle=9403;e.Fdotaccent=7710;e.Feharmenian=1366;e.Feicoptic=996;e.Fhook=401;e.Fitacyrillic=1138;e.Fiveroman=8548;e.Fmonospace=65318;e.Fourroman=8547;e.Fsmall=63334;e.G=71;e.GBsquare=13191;e.Gacute=500;e.Gamma=915;e.Gammaafrican=404;e.Gangiacoptic=1002;e.Gbreve=286;e.Gcaron=486;e.Gcedilla=290;e.Gcircle=9404;e.Gcircumflex=284;e.Gcommaaccent=290;e.Gdot=288;e.Gdotaccent=288;e.Gecyrillic=1043;e.Ghadarmenian=1346;e.Ghemiddlehookcyrillic=1172;e.Ghestrokecyrillic=1170;e.Gheupturncyrillic=1168;e.Ghook=403;e.Gimarmenian=1331;e.Gjecyrillic=1027;e.Gmacron=7712;e.Gmonospace=65319;e.Grave=63182;e.Gravesmall=63328;e.Gsmall=63335;e.Gsmallhook=667;e.Gstroke=484;e.H=72;e.H18533=9679;e.H18543=9642;e.H18551=9643;e.H22073=9633;e.HPsquare=13259;e.Haabkhasiancyrillic=1192;e.Hadescendercyrillic=1202;e.Hardsigncyrillic=1066;e.Hbar=294;e.Hbrevebelow=7722;e.Hcedilla=7720;e.Hcircle=9405;e.Hcircumflex=292;e.Hdieresis=7718;e.Hdotaccent=7714;e.Hdotbelow=7716;e.Hmonospace=65320;e.Hoarmenian=1344;e.Horicoptic=1e3;e.Hsmall=63336;e.Hungarumlaut=63183;e.Hungarumlautsmall=63224;e.Hzsquare=13200;e.I=73;e.IAcyrillic=1071;e.IJ=306;e.IUcyrillic=1070;e.Iacute=205;e.Iacutesmall=63469;e.Ibreve=300;e.Icaron=463;e.Icircle=9406;e.Icircumflex=206;e.Icircumflexsmall=63470;e.Icyrillic=1030;e.Idblgrave=520;e.Idieresis=207;e.Idieresisacute=7726;e.Idieresiscyrillic=1252;e.Idieresissmall=63471;e.Idot=304;e.Idotaccent=304;e.Idotbelow=7882;e.Iebrevecyrillic=1238;e.Iecyrillic=1045;e.Ifraktur=8465;e.Igrave=204;e.Igravesmall=63468;e.Ihookabove=7880;e.Iicyrillic=1048;e.Iinvertedbreve=522;e.Iishortcyrillic=1049;e.Imacron=298;e.Imacroncyrillic=1250;e.Imonospace=65321;e.Iniarmenian=1339;e.Iocyrillic=1025;e.Iogonek=302;e.Iota=921;e.Iotaafrican=406;e.Iotadieresis=938;e.Iotatonos=906;e.Ismall=63337;e.Istroke=407;e.Itilde=296;e.Itildebelow=7724;e.Izhitsacyrillic=1140;e.Izhitsadblgravecyrillic=1142;e.J=74;e.Jaarmenian=1345;e.Jcircle=9407;e.Jcircumflex=308;e.Jecyrillic=1032;e.Jheharmenian=1355;e.Jmonospace=65322;e.Jsmall=63338;e.K=75;e.KBsquare=13189;e.KKsquare=13261;e.Kabashkircyrillic=1184;e.Kacute=7728;e.Kacyrillic=1050;e.Kadescendercyrillic=1178;e.Kahookcyrillic=1219;e.Kappa=922;e.Kastrokecyrillic=1182;e.Kaverticalstrokecyrillic=1180;e.Kcaron=488;e.Kcedilla=310;e.Kcircle=9408;e.Kcommaaccent=310;e.Kdotbelow=7730;e.Keharmenian=1364;e.Kenarmenian=1343;e.Khacyrillic=1061;e.Kheicoptic=998;e.Khook=408;e.Kjecyrillic=1036;e.Klinebelow=7732;e.Kmonospace=65323;e.Koppacyrillic=1152;e.Koppagreek=990;e.Ksicyrillic=1134;e.Ksmall=63339;e.L=76;e.LJ=455;e.LL=63167;e.Lacute=313;e.Lambda=923;e.Lcaron=317;e.Lcedilla=315;e.Lcircle=9409;e.Lcircumflexbelow=7740;e.Lcommaaccent=315;e.Ldot=319;e.Ldotaccent=319;e.Ldotbelow=7734;e.Ldotbelowmacron=7736;e.Liwnarmenian=1340;e.Lj=456;e.Ljecyrillic=1033;e.Llinebelow=7738;e.Lmonospace=65324;e.Lslash=321;e.Lslashsmall=63225;e.Lsmall=63340;e.M=77;e.MBsquare=13190;e.Macron=63184;e.Macronsmall=63407;e.Macute=7742;e.Mcircle=9410;e.Mdotaccent=7744;e.Mdotbelow=7746;e.Menarmenian=1348;e.Mmonospace=65325;e.Msmall=63341;e.Mturned=412;e.Mu=924;e.N=78;e.NJ=458;e.Nacute=323;e.Ncaron=327;e.Ncedilla=325;e.Ncircle=9411;e.Ncircumflexbelow=7754;e.Ncommaaccent=325;e.Ndotaccent=7748;e.Ndotbelow=7750;e.Nhookleft=413;e.Nineroman=8552;e.Nj=459;e.Njecyrillic=1034;e.Nlinebelow=7752;e.Nmonospace=65326;e.Nowarmenian=1350;e.Nsmall=63342;e.Ntilde=209;e.Ntildesmall=63473;e.Nu=925;e.O=79;e.OE=338;e.OEsmall=63226;e.Oacute=211;e.Oacutesmall=63475;e.Obarredcyrillic=1256;e.Obarreddieresiscyrillic=1258;e.Obreve=334;e.Ocaron=465;e.Ocenteredtilde=415;e.Ocircle=9412;e.Ocircumflex=212;e.Ocircumflexacute=7888;e.Ocircumflexdotbelow=7896;e.Ocircumflexgrave=7890;e.Ocircumflexhookabove=7892;e.Ocircumflexsmall=63476;e.Ocircumflextilde=7894;e.Ocyrillic=1054;e.Odblacute=336;e.Odblgrave=524;e.Odieresis=214;e.Odieresiscyrillic=1254;e.Odieresissmall=63478;e.Odotbelow=7884;e.Ogoneksmall=63227;e.Ograve=210;e.Ogravesmall=63474;e.Oharmenian=1365;e.Ohm=8486;e.Ohookabove=7886;e.Ohorn=416;e.Ohornacute=7898;e.Ohorndotbelow=7906;e.Ohorngrave=7900;e.Ohornhookabove=7902;e.Ohorntilde=7904;e.Ohungarumlaut=336;e.Oi=418;e.Oinvertedbreve=526;e.Omacron=332;e.Omacronacute=7762;e.Omacrongrave=7760;e.Omega=8486;e.Omegacyrillic=1120;e.Omegagreek=937;e.Omegaroundcyrillic=1146;e.Omegatitlocyrillic=1148;e.Omegatonos=911;e.Omicron=927;e.Omicrontonos=908;e.Omonospace=65327;e.Oneroman=8544;e.Oogonek=490;e.Oogonekmacron=492;e.Oopen=390;e.Oslash=216;e.Oslashacute=510;e.Oslashsmall=63480;e.Osmall=63343;e.Ostrokeacute=510;e.Otcyrillic=1150;e.Otilde=213;e.Otildeacute=7756;e.Otildedieresis=7758;e.Otildesmall=63477;e.P=80;e.Pacute=7764;e.Pcircle=9413;e.Pdotaccent=7766;e.Pecyrillic=1055;e.Peharmenian=1354;e.Pemiddlehookcyrillic=1190;e.Phi=934;e.Phook=420;e.Pi=928;e.Piwrarmenian=1363;e.Pmonospace=65328;e.Psi=936;e.Psicyrillic=1136;e.Psmall=63344;e.Q=81;e.Qcircle=9414;e.Qmonospace=65329;e.Qsmall=63345;e.R=82;e.Raarmenian=1356;e.Racute=340;e.Rcaron=344;e.Rcedilla=342;e.Rcircle=9415;e.Rcommaaccent=342;e.Rdblgrave=528;e.Rdotaccent=7768;e.Rdotbelow=7770;e.Rdotbelowmacron=7772;e.Reharmenian=1360;e.Rfraktur=8476;e.Rho=929;e.Ringsmall=63228;e.Rinvertedbreve=530;e.Rlinebelow=7774;e.Rmonospace=65330;e.Rsmall=63346;e.Rsmallinverted=641;e.Rsmallinvertedsuperior=694;e.S=83;e.SF010000=9484;e.SF020000=9492;e.SF030000=9488;e.SF040000=9496;e.SF050000=9532;e.SF060000=9516;e.SF070000=9524;e.SF080000=9500;e.SF090000=9508;e.SF100000=9472;e.SF110000=9474;e.SF190000=9569;e.SF200000=9570;e.SF210000=9558;e.SF220000=9557;e.SF230000=9571;e.SF240000=9553;e.SF250000=9559;e.SF260000=9565;e.SF270000=9564;e.SF280000=9563;e.SF360000=9566;e.SF370000=9567;e.SF380000=9562;e.SF390000=9556;e.SF400000=9577;e.SF410000=9574;e.SF420000=9568;e.SF430000=9552;e.SF440000=9580;e.SF450000=9575;e.SF460000=9576;e.SF470000=9572;e.SF480000=9573;e.SF490000=9561;e.SF500000=9560;e.SF510000=9554;e.SF520000=9555;e.SF530000=9579;e.SF540000=9578;e.Sacute=346;e.Sacutedotaccent=7780;e.Sampigreek=992;e.Scaron=352;e.Scarondotaccent=7782;e.Scaronsmall=63229;e.Scedilla=350;e.Schwa=399;e.Schwacyrillic=1240;e.Schwadieresiscyrillic=1242;e.Scircle=9416;e.Scircumflex=348;e.Scommaaccent=536;e.Sdotaccent=7776;e.Sdotbelow=7778;e.Sdotbelowdotaccent=7784;e.Seharmenian=1357;e.Sevenroman=8550;e.Shaarmenian=1351;e.Shacyrillic=1064;e.Shchacyrillic=1065;e.Sheicoptic=994;e.Shhacyrillic=1210;e.Shimacoptic=1004;e.Sigma=931;e.Sixroman=8549;e.Smonospace=65331;e.Softsigncyrillic=1068;e.Ssmall=63347;e.Stigmagreek=986;e.T=84;e.Tau=932;e.Tbar=358;e.Tcaron=356;e.Tcedilla=354;e.Tcircle=9417;e.Tcircumflexbelow=7792;e.Tcommaaccent=354;e.Tdotaccent=7786;e.Tdotbelow=7788;e.Tecyrillic=1058;e.Tedescendercyrillic=1196;e.Tenroman=8553;e.Tetsecyrillic=1204;e.Theta=920;e.Thook=428;e.Thorn=222;e.Thornsmall=63486;e.Threeroman=8546;e.Tildesmall=63230;e.Tiwnarmenian=1359;e.Tlinebelow=7790;e.Tmonospace=65332;e.Toarmenian=1337;e.Tonefive=444;e.Tonesix=388;e.Tonetwo=423;e.Tretroflexhook=430;e.Tsecyrillic=1062;e.Tshecyrillic=1035;e.Tsmall=63348;e.Twelveroman=8555;e.Tworoman=8545;e.U=85;e.Uacute=218;e.Uacutesmall=63482;e.Ubreve=364;e.Ucaron=467;e.Ucircle=9418;e.Ucircumflex=219;e.Ucircumflexbelow=7798;e.Ucircumflexsmall=63483;e.Ucyrillic=1059;e.Udblacute=368;e.Udblgrave=532;e.Udieresis=220;e.Udieresisacute=471;e.Udieresisbelow=7794;e.Udieresiscaron=473;e.Udieresiscyrillic=1264;e.Udieresisgrave=475;e.Udieresismacron=469;e.Udieresissmall=63484;e.Udotbelow=7908;e.Ugrave=217;e.Ugravesmall=63481;e.Uhookabove=7910;e.Uhorn=431;e.Uhornacute=7912;e.Uhorndotbelow=7920;e.Uhorngrave=7914;e.Uhornhookabove=7916;e.Uhorntilde=7918;e.Uhungarumlaut=368;e.Uhungarumlautcyrillic=1266;e.Uinvertedbreve=534;e.Ukcyrillic=1144;e.Umacron=362;e.Umacroncyrillic=1262;e.Umacrondieresis=7802;e.Umonospace=65333;e.Uogonek=370;e.Upsilon=933;e.Upsilon1=978;e.Upsilonacutehooksymbolgreek=979;e.Upsilonafrican=433;e.Upsilondieresis=939;e.Upsilondieresishooksymbolgreek=980;e.Upsilonhooksymbol=978;e.Upsilontonos=910;e.Uring=366;e.Ushortcyrillic=1038;e.Usmall=63349;e.Ustraightcyrillic=1198;e.Ustraightstrokecyrillic=1200;e.Utilde=360;e.Utildeacute=7800;e.Utildebelow=7796;e.V=86;e.Vcircle=9419;e.Vdotbelow=7806;e.Vecyrillic=1042;e.Vewarmenian=1358;e.Vhook=434;e.Vmonospace=65334;e.Voarmenian=1352;e.Vsmall=63350;e.Vtilde=7804;e.W=87;e.Wacute=7810;e.Wcircle=9420;e.Wcircumflex=372;e.Wdieresis=7812;e.Wdotaccent=7814;e.Wdotbelow=7816;e.Wgrave=7808;e.Wmonospace=65335;e.Wsmall=63351;e.X=88;e.Xcircle=9421;e.Xdieresis=7820;e.Xdotaccent=7818;e.Xeharmenian=1341;e.Xi=926;e.Xmonospace=65336;e.Xsmall=63352;e.Y=89;e.Yacute=221;e.Yacutesmall=63485;e.Yatcyrillic=1122;e.Ycircle=9422;e.Ycircumflex=374;e.Ydieresis=376;e.Ydieresissmall=63487;e.Ydotaccent=7822;e.Ydotbelow=7924;e.Yericyrillic=1067;e.Yerudieresiscyrillic=1272;e.Ygrave=7922;e.Yhook=435;e.Yhookabove=7926;e.Yiarmenian=1349;e.Yicyrillic=1031;e.Yiwnarmenian=1362;e.Ymonospace=65337;e.Ysmall=63353;e.Ytilde=7928;e.Yusbigcyrillic=1130;e.Yusbigiotifiedcyrillic=1132;e.Yuslittlecyrillic=1126;e.Yuslittleiotifiedcyrillic=1128;e.Z=90;e.Zaarmenian=1334;e.Zacute=377;e.Zcaron=381;e.Zcaronsmall=63231;e.Zcircle=9423;e.Zcircumflex=7824;e.Zdot=379;e.Zdotaccent=379;e.Zdotbelow=7826;e.Zecyrillic=1047;e.Zedescendercyrillic=1176;e.Zedieresiscyrillic=1246;e.Zeta=918;e.Zhearmenian=1338;e.Zhebrevecyrillic=1217;e.Zhecyrillic=1046;e.Zhedescendercyrillic=1174;e.Zhedieresiscyrillic=1244;e.Zlinebelow=7828;e.Zmonospace=65338;e.Zsmall=63354;e.Zstroke=437;e.a=97;e.aabengali=2438;e.aacute=225;e.aadeva=2310;e.aagujarati=2694;e.aagurmukhi=2566;e.aamatragurmukhi=2622;e.aarusquare=13059;e.aavowelsignbengali=2494;e.aavowelsigndeva=2366;e.aavowelsigngujarati=2750;e.abbreviationmarkarmenian=1375;e.abbreviationsigndeva=2416;e.abengali=2437;e.abopomofo=12570;e.abreve=259;e.abreveacute=7855;e.abrevecyrillic=1233;e.abrevedotbelow=7863;e.abrevegrave=7857;e.abrevehookabove=7859;e.abrevetilde=7861;e.acaron=462;e.acircle=9424;e.acircumflex=226;e.acircumflexacute=7845;e.acircumflexdotbelow=7853;e.acircumflexgrave=7847;e.acircumflexhookabove=7849;e.acircumflextilde=7851;e.acute=180;e.acutebelowcmb=791;e.acutecmb=769;e.acutecomb=769;e.acutedeva=2388;e.acutelowmod=719;e.acutetonecmb=833;e.acyrillic=1072;e.adblgrave=513;e.addakgurmukhi=2673;e.adeva=2309;e.adieresis=228;e.adieresiscyrillic=1235;e.adieresismacron=479;e.adotbelow=7841;e.adotmacron=481;e.ae=230;e.aeacute=509;e.aekorean=12624;e.aemacron=483;e.afii00208=8213;e.afii08941=8356;e.afii10017=1040;e.afii10018=1041;e.afii10019=1042;e.afii10020=1043;e.afii10021=1044;e.afii10022=1045;e.afii10023=1025;e.afii10024=1046;e.afii10025=1047;e.afii10026=1048;e.afii10027=1049;e.afii10028=1050;e.afii10029=1051;e.afii10030=1052;e.afii10031=1053;e.afii10032=1054;e.afii10033=1055;e.afii10034=1056;e.afii10035=1057;e.afii10036=1058;e.afii10037=1059;e.afii10038=1060;e.afii10039=1061;e.afii10040=1062;e.afii10041=1063;e.afii10042=1064;e.afii10043=1065;e.afii10044=1066;e.afii10045=1067;e.afii10046=1068;e.afii10047=1069;e.afii10048=1070;e.afii10049=1071;e.afii10050=1168;e.afii10051=1026;e.afii10052=1027;e.afii10053=1028;e.afii10054=1029;e.afii10055=1030;e.afii10056=1031;e.afii10057=1032;e.afii10058=1033;e.afii10059=1034;e.afii10060=1035;e.afii10061=1036;e.afii10062=1038;e.afii10063=63172;e.afii10064=63173;e.afii10065=1072;e.afii10066=1073;e.afii10067=1074;e.afii10068=1075;e.afii10069=1076;e.afii10070=1077;e.afii10071=1105;e.afii10072=1078;e.afii10073=1079;e.afii10074=1080;e.afii10075=1081;e.afii10076=1082;e.afii10077=1083;e.afii10078=1084;e.afii10079=1085;e.afii10080=1086;e.afii10081=1087;e.afii10082=1088;e.afii10083=1089;e.afii10084=1090;e.afii10085=1091;e.afii10086=1092;e.afii10087=1093;e.afii10088=1094;e.afii10089=1095;e.afii10090=1096;e.afii10091=1097;e.afii10092=1098;e.afii10093=1099;e.afii10094=1100;e.afii10095=1101;e.afii10096=1102;e.afii10097=1103;e.afii10098=1169;e.afii10099=1106;e.afii10100=1107;e.afii10101=1108;e.afii10102=1109;e.afii10103=1110;e.afii10104=1111;e.afii10105=1112;e.afii10106=1113;e.afii10107=1114;e.afii10108=1115;e.afii10109=1116;e.afii10110=1118;e.afii10145=1039;e.afii10146=1122;e.afii10147=1138;e.afii10148=1140;e.afii10192=63174;e.afii10193=1119;e.afii10194=1123;e.afii10195=1139;e.afii10196=1141;e.afii10831=63175;e.afii10832=63176;e.afii10846=1241;e.afii299=8206;e.afii300=8207;e.afii301=8205;e.afii57381=1642;e.afii57388=1548;e.afii57392=1632;e.afii57393=1633;e.afii57394=1634;e.afii57395=1635;e.afii57396=1636;e.afii57397=1637;e.afii57398=1638;e.afii57399=1639;e.afii57400=1640;e.afii57401=1641;e.afii57403=1563;e.afii57407=1567;e.afii57409=1569;e.afii57410=1570;e.afii57411=1571;e.afii57412=1572;e.afii57413=1573;e.afii57414=1574;e.afii57415=1575;e.afii57416=1576;e.afii57417=1577;e.afii57418=1578;e.afii57419=1579;e.afii57420=1580;e.afii57421=1581;e.afii57422=1582;e.afii57423=1583;e.afii57424=1584;e.afii57425=1585;e.afii57426=1586;e.afii57427=1587;e.afii57428=1588;e.afii57429=1589;e.afii57430=1590;e.afii57431=1591;e.afii57432=1592;e.afii57433=1593;e.afii57434=1594;e.afii57440=1600;e.afii57441=1601;e.afii57442=1602;e.afii57443=1603;e.afii57444=1604;e.afii57445=1605;e.afii57446=1606;e.afii57448=1608;e.afii57449=1609;e.afii57450=1610;e.afii57451=1611;e.afii57452=1612;e.afii57453=1613;e.afii57454=1614;e.afii57455=1615;e.afii57456=1616;e.afii57457=1617;e.afii57458=1618;e.afii57470=1607;e.afii57505=1700;e.afii57506=1662;e.afii57507=1670;e.afii57508=1688;e.afii57509=1711;e.afii57511=1657;e.afii57512=1672;e.afii57513=1681;e.afii57514=1722;e.afii57519=1746;e.afii57534=1749;e.afii57636=8362;e.afii57645=1470;e.afii57658=1475;e.afii57664=1488;e.afii57665=1489;e.afii57666=1490;e.afii57667=1491;e.afii57668=1492;e.afii57669=1493;e.afii57670=1494;e.afii57671=1495;e.afii57672=1496;e.afii57673=1497;e.afii57674=1498;e.afii57675=1499;e.afii57676=1500;e.afii57677=1501;e.afii57678=1502;e.afii57679=1503;e.afii57680=1504;e.afii57681=1505;e.afii57682=1506;e.afii57683=1507;e.afii57684=1508;e.afii57685=1509;e.afii57686=1510;e.afii57687=1511;e.afii57688=1512;e.afii57689=1513;e.afii57690=1514;e.afii57694=64298;e.afii57695=64299;e.afii57700=64331;e.afii57705=64287;e.afii57716=1520;e.afii57717=1521;e.afii57718=1522;e.afii57723=64309;e.afii57793=1460;e.afii57794=1461;e.afii57795=1462;e.afii57796=1467;e.afii57797=1464;e.afii57798=1463;e.afii57799=1456;e.afii57800=1458;e.afii57801=1457;e.afii57802=1459;e.afii57803=1474;e.afii57804=1473;e.afii57806=1465;e.afii57807=1468;e.afii57839=1469;e.afii57841=1471;e.afii57842=1472;e.afii57929=700;e.afii61248=8453;e.afii61289=8467;e.afii61352=8470;e.afii61573=8236;e.afii61574=8237;e.afii61575=8238;e.afii61664=8204;e.afii63167=1645;e.afii64937=701;e.agrave=224;e.agujarati=2693;e.agurmukhi=2565;e.ahiragana=12354;e.ahookabove=7843;e.aibengali=2448;e.aibopomofo=12574;e.aideva=2320;e.aiecyrillic=1237;e.aigujarati=2704;e.aigurmukhi=2576;e.aimatragurmukhi=2632;e.ainarabic=1593;e.ainfinalarabic=65226;e.aininitialarabic=65227;e.ainmedialarabic=65228;e.ainvertedbreve=515;e.aivowelsignbengali=2504;e.aivowelsigndeva=2376;e.aivowelsigngujarati=2760;e.akatakana=12450;e.akatakanahalfwidth=65393;e.akorean=12623;e.alef=1488;e.alefarabic=1575;e.alefdageshhebrew=64304;e.aleffinalarabic=65166;e.alefhamzaabovearabic=1571;e.alefhamzaabovefinalarabic=65156;e.alefhamzabelowarabic=1573;e.alefhamzabelowfinalarabic=65160;e.alefhebrew=1488;e.aleflamedhebrew=64335;e.alefmaddaabovearabic=1570;e.alefmaddaabovefinalarabic=65154;e.alefmaksuraarabic=1609;e.alefmaksurafinalarabic=65264;e.alefmaksurainitialarabic=65267;e.alefmaksuramedialarabic=65268;e.alefpatahhebrew=64302;e.alefqamatshebrew=64303;e.aleph=8501;e.allequal=8780;e.alpha=945;e.alphatonos=940;e.amacron=257;e.amonospace=65345;e.ampersand=38;e.ampersandmonospace=65286;e.ampersandsmall=63270;e.amsquare=13250;e.anbopomofo=12578;e.angbopomofo=12580;e.angbracketleft=12296;e.angbracketright=12297;e.angkhankhuthai=3674;e.angle=8736;e.anglebracketleft=12296;e.anglebracketleftvertical=65087;e.anglebracketright=12297;e.anglebracketrightvertical=65088;e.angleleft=9001;e.angleright=9002;e.angstrom=8491;e.anoteleia=903;e.anudattadeva=2386;e.anusvarabengali=2434;e.anusvaradeva=2306;e.anusvaragujarati=2690;e.aogonek=261;e.apaatosquare=13056;e.aparen=9372;e.apostrophearmenian=1370;e.apostrophemod=700;e.apple=63743;e.approaches=8784;e.approxequal=8776;e.approxequalorimage=8786;e.approximatelyequal=8773;e.araeaekorean=12686;e.araeakorean=12685;e.arc=8978;e.arighthalfring=7834;e.aring=229;e.aringacute=507;e.aringbelow=7681;e.arrowboth=8596;e.arrowdashdown=8675;e.arrowdashleft=8672;e.arrowdashright=8674;e.arrowdashup=8673;e.arrowdblboth=8660;e.arrowdbldown=8659;e.arrowdblleft=8656;e.arrowdblright=8658;e.arrowdblup=8657;e.arrowdown=8595;e.arrowdownleft=8601;e.arrowdownright=8600;e.arrowdownwhite=8681;e.arrowheaddownmod=709;e.arrowheadleftmod=706;e.arrowheadrightmod=707;e.arrowheadupmod=708;e.arrowhorizex=63719;e.arrowleft=8592;e.arrowleftdbl=8656;e.arrowleftdblstroke=8653;e.arrowleftoverright=8646;e.arrowleftwhite=8678;e.arrowright=8594;e.arrowrightdblstroke=8655;e.arrowrightheavy=10142;e.arrowrightoverleft=8644;e.arrowrightwhite=8680;e.arrowtableft=8676;e.arrowtabright=8677;e.arrowup=8593;e.arrowupdn=8597;e.arrowupdnbse=8616;e.arrowupdownbase=8616;e.arrowupleft=8598;e.arrowupleftofdown=8645;e.arrowupright=8599;e.arrowupwhite=8679;e.arrowvertex=63718;e.asciicircum=94;e.asciicircummonospace=65342;e.asciitilde=126;e.asciitildemonospace=65374;e.ascript=593;e.ascriptturned=594;e.asmallhiragana=12353;e.asmallkatakana=12449;e.asmallkatakanahalfwidth=65383;e.asterisk=42;e.asteriskaltonearabic=1645;e.asteriskarabic=1645;e.asteriskmath=8727;e.asteriskmonospace=65290;e.asterisksmall=65121;e.asterism=8258;e.asuperior=63209;e.asymptoticallyequal=8771;e.at=64;e.atilde=227;e.atmonospace=65312;e.atsmall=65131;e.aturned=592;e.aubengali=2452;e.aubopomofo=12576;e.audeva=2324;e.augujarati=2708;e.augurmukhi=2580;e.aulengthmarkbengali=2519;e.aumatragurmukhi=2636;e.auvowelsignbengali=2508;e.auvowelsigndeva=2380;e.auvowelsigngujarati=2764;e.avagrahadeva=2365;e.aybarmenian=1377;e.ayin=1506;e.ayinaltonehebrew=64288;e.ayinhebrew=1506;e.b=98;e.babengali=2476;e.backslash=92;e.backslashmonospace=65340;e.badeva=2348;e.bagujarati=2732;e.bagurmukhi=2604;e.bahiragana=12400;e.bahtthai=3647;e.bakatakana=12496;e.bar=124;e.barmonospace=65372;e.bbopomofo=12549;e.bcircle=9425;e.bdotaccent=7683;e.bdotbelow=7685;e.beamedsixteenthnotes=9836;e.because=8757;e.becyrillic=1073;e.beharabic=1576;e.behfinalarabic=65168;e.behinitialarabic=65169;e.behiragana=12409;e.behmedialarabic=65170;e.behmeeminitialarabic=64671;e.behmeemisolatedarabic=64520;e.behnoonfinalarabic=64621;e.bekatakana=12505;e.benarmenian=1378;e.bet=1489;e.beta=946;e.betasymbolgreek=976;e.betdagesh=64305;e.betdageshhebrew=64305;e.bethebrew=1489;e.betrafehebrew=64332;e.bhabengali=2477;e.bhadeva=2349;e.bhagujarati=2733;e.bhagurmukhi=2605;e.bhook=595;e.bihiragana=12403;e.bikatakana=12499;e.bilabialclick=664;e.bindigurmukhi=2562;e.birusquare=13105;e.blackcircle=9679;e.blackdiamond=9670;e.blackdownpointingtriangle=9660;e.blackleftpointingpointer=9668;e.blackleftpointingtriangle=9664;e.blacklenticularbracketleft=12304;e.blacklenticularbracketleftvertical=65083;e.blacklenticularbracketright=12305;e.blacklenticularbracketrightvertical=65084;e.blacklowerlefttriangle=9699;e.blacklowerrighttriangle=9698;e.blackrectangle=9644;e.blackrightpointingpointer=9658;e.blackrightpointingtriangle=9654;e.blacksmallsquare=9642;e.blacksmilingface=9787;e.blacksquare=9632;e.blackstar=9733;e.blackupperlefttriangle=9700;e.blackupperrighttriangle=9701;e.blackuppointingsmalltriangle=9652;e.blackuppointingtriangle=9650;e.blank=9251;e.blinebelow=7687;e.block=9608;e.bmonospace=65346;e.bobaimaithai=3610;e.bohiragana=12412;e.bokatakana=12508;e.bparen=9373;e.bqsquare=13251;e.braceex=63732;e.braceleft=123;e.braceleftbt=63731;e.braceleftmid=63730;e.braceleftmonospace=65371;e.braceleftsmall=65115;e.bracelefttp=63729;e.braceleftvertical=65079;e.braceright=125;e.bracerightbt=63742;e.bracerightmid=63741;e.bracerightmonospace=65373;e.bracerightsmall=65116;e.bracerighttp=63740;e.bracerightvertical=65080;e.bracketleft=91;e.bracketleftbt=63728;e.bracketleftex=63727;e.bracketleftmonospace=65339;e.bracketlefttp=63726;e.bracketright=93;e.bracketrightbt=63739;e.bracketrightex=63738;e.bracketrightmonospace=65341;e.bracketrighttp=63737;e.breve=728;e.brevebelowcmb=814;e.brevecmb=774;e.breveinvertedbelowcmb=815;e.breveinvertedcmb=785;e.breveinverteddoublecmb=865;e.bridgebelowcmb=810;e.bridgeinvertedbelowcmb=826;e.brokenbar=166;e.bstroke=384;e.bsuperior=63210;e.btopbar=387;e.buhiragana=12406;e.bukatakana=12502;e.bullet=8226;e.bulletinverse=9688;e.bulletoperator=8729;e.bullseye=9678;e.c=99;e.caarmenian=1390;e.cabengali=2458;e.cacute=263;e.cadeva=2330;e.cagujarati=2714;e.cagurmukhi=2586;e.calsquare=13192;e.candrabindubengali=2433;e.candrabinducmb=784;e.candrabindudeva=2305;e.candrabindugujarati=2689;e.capslock=8682;e.careof=8453;e.caron=711;e.caronbelowcmb=812;e.caroncmb=780;e.carriagereturn=8629;e.cbopomofo=12568;e.ccaron=269;e.ccedilla=231;e.ccedillaacute=7689;e.ccircle=9426;e.ccircumflex=265;e.ccurl=597;e.cdot=267;e.cdotaccent=267;e.cdsquare=13253;e.cedilla=184;e.cedillacmb=807;e.cent=162;e.centigrade=8451;e.centinferior=63199;e.centmonospace=65504;e.centoldstyle=63394;e.centsuperior=63200;e.chaarmenian=1401;e.chabengali=2459;e.chadeva=2331;e.chagujarati=2715;e.chagurmukhi=2587;e.chbopomofo=12564;e.cheabkhasiancyrillic=1213;e.checkmark=10003;e.checyrillic=1095;e.chedescenderabkhasiancyrillic=1215;e.chedescendercyrillic=1207;e.chedieresiscyrillic=1269;e.cheharmenian=1395;e.chekhakassiancyrillic=1228;e.cheverticalstrokecyrillic=1209;e.chi=967;e.chieuchacirclekorean=12919;e.chieuchaparenkorean=12823;e.chieuchcirclekorean=12905;e.chieuchkorean=12618;e.chieuchparenkorean=12809;e.chochangthai=3594;e.chochanthai=3592;e.chochingthai=3593;e.chochoethai=3596;e.chook=392;e.cieucacirclekorean=12918;e.cieucaparenkorean=12822;e.cieuccirclekorean=12904;e.cieuckorean=12616;e.cieucparenkorean=12808;e.cieucuparenkorean=12828;e.circle=9675;e.circlecopyrt=169;e.circlemultiply=8855;e.circleot=8857;e.circleplus=8853;e.circlepostalmark=12342;e.circlewithlefthalfblack=9680;e.circlewithrighthalfblack=9681;e.circumflex=710;e.circumflexbelowcmb=813;e.circumflexcmb=770;e.clear=8999;e.clickalveolar=450;e.clickdental=448;e.clicklateral=449;e.clickretroflex=451;e.club=9827;e.clubsuitblack=9827;e.clubsuitwhite=9831;e.cmcubedsquare=13220;e.cmonospace=65347;e.cmsquaredsquare=13216;e.coarmenian=1409;e.colon=58;e.colonmonetary=8353;e.colonmonospace=65306;e.colonsign=8353;e.colonsmall=65109;e.colontriangularhalfmod=721;e.colontriangularmod=720;e.comma=44;e.commaabovecmb=787;e.commaaboverightcmb=789;e.commaaccent=63171;e.commaarabic=1548;e.commaarmenian=1373;e.commainferior=63201;e.commamonospace=65292;e.commareversedabovecmb=788;e.commareversedmod=701;e.commasmall=65104;e.commasuperior=63202;e.commaturnedabovecmb=786;e.commaturnedmod=699;e.compass=9788;e.congruent=8773;e.contourintegral=8750;e.control=8963;e.controlACK=6;e.controlBEL=7;e.controlBS=8;e.controlCAN=24;e.controlCR=13;e.controlDC1=17;e.controlDC2=18;e.controlDC3=19;e.controlDC4=20;e.controlDEL=127;e.controlDLE=16;e.controlEM=25;e.controlENQ=5;e.controlEOT=4;e.controlESC=27;e.controlETB=23;e.controlETX=3;e.controlFF=12;e.controlFS=28;e.controlGS=29;e.controlHT=9;e.controlLF=10;e.controlNAK=21;e.controlNULL=0;e.controlRS=30;e.controlSI=15;e.controlSO=14;e.controlSOT=2;e.controlSTX=1;e.controlSUB=26;e.controlSYN=22;e.controlUS=31;e.controlVT=11;e.copyright=169;e.copyrightsans=63721;e.copyrightserif=63193;e.cornerbracketleft=12300;e.cornerbracketlefthalfwidth=65378;e.cornerbracketleftvertical=65089;e.cornerbracketright=12301;e.cornerbracketrighthalfwidth=65379;e.cornerbracketrightvertical=65090;e.corporationsquare=13183;e.cosquare=13255;e.coverkgsquare=13254;e.cparen=9374;e.cruzeiro=8354;e.cstretched=663;e.curlyand=8911;e.curlyor=8910;e.currency=164;e.cyrBreve=63185;e.cyrFlex=63186;e.cyrbreve=63188;e.cyrflex=63189;e.d=100;e.daarmenian=1380;e.dabengali=2470;e.dadarabic=1590;e.dadeva=2342;e.dadfinalarabic=65214;e.dadinitialarabic=65215;e.dadmedialarabic=65216;e.dagesh=1468;e.dageshhebrew=1468;e.dagger=8224;e.daggerdbl=8225;e.dagujarati=2726;e.dagurmukhi=2598;e.dahiragana=12384;e.dakatakana=12480;e.dalarabic=1583;e.dalet=1491;e.daletdagesh=64307;e.daletdageshhebrew=64307;e.dalethebrew=1491;e.dalfinalarabic=65194;e.dammaarabic=1615;e.dammalowarabic=1615;e.dammatanaltonearabic=1612;e.dammatanarabic=1612;e.danda=2404;e.dargahebrew=1447;e.dargalefthebrew=1447;e.dasiapneumatacyrilliccmb=1157;e.dblGrave=63187;e.dblanglebracketleft=12298;e.dblanglebracketleftvertical=65085;e.dblanglebracketright=12299;e.dblanglebracketrightvertical=65086;e.dblarchinvertedbelowcmb=811;e.dblarrowleft=8660;e.dblarrowright=8658;e.dbldanda=2405;e.dblgrave=63190;e.dblgravecmb=783;e.dblintegral=8748;e.dbllowline=8215;e.dbllowlinecmb=819;e.dbloverlinecmb=831;e.dblprimemod=698;e.dblverticalbar=8214;e.dblverticallineabovecmb=782;e.dbopomofo=12553;e.dbsquare=13256;e.dcaron=271;e.dcedilla=7697;e.dcircle=9427;e.dcircumflexbelow=7699;e.dcroat=273;e.ddabengali=2465;e.ddadeva=2337;e.ddagujarati=2721;e.ddagurmukhi=2593;e.ddalarabic=1672;e.ddalfinalarabic=64393;e.dddhadeva=2396;e.ddhabengali=2466;e.ddhadeva=2338;e.ddhagujarati=2722;e.ddhagurmukhi=2594;e.ddotaccent=7691;e.ddotbelow=7693;e.decimalseparatorarabic=1643;e.decimalseparatorpersian=1643;e.decyrillic=1076;e.degree=176;e.dehihebrew=1453;e.dehiragana=12391;e.deicoptic=1007;e.dekatakana=12487;e.deleteleft=9003;e.deleteright=8998;e.delta=948;e.deltaturned=397;e.denominatorminusonenumeratorbengali=2552;e.dezh=676;e.dhabengali=2471;e.dhadeva=2343;e.dhagujarati=2727;e.dhagurmukhi=2599;e.dhook=599;e.dialytikatonos=901;e.dialytikatonoscmb=836;e.diamond=9830;e.diamondsuitwhite=9826;e.dieresis=168;e.dieresisacute=63191;e.dieresisbelowcmb=804;e.dieresiscmb=776;e.dieresisgrave=63192;e.dieresistonos=901;e.dihiragana=12386;e.dikatakana=12482;e.dittomark=12291;e.divide=247;e.divides=8739;e.divisionslash=8725;e.djecyrillic=1106;e.dkshade=9619;e.dlinebelow=7695;e.dlsquare=13207;e.dmacron=273;e.dmonospace=65348;e.dnblock=9604;e.dochadathai=3598;e.dodekthai=3604;e.dohiragana=12393;e.dokatakana=12489;e.dollar=36;e.dollarinferior=63203;e.dollarmonospace=65284;e.dollaroldstyle=63268;e.dollarsmall=65129;e.dollarsuperior=63204;e.dong=8363;e.dorusquare=13094;e.dotaccent=729;e.dotaccentcmb=775;e.dotbelowcmb=803;e.dotbelowcomb=803;e.dotkatakana=12539;e.dotlessi=305;e.dotlessj=63166;e.dotlessjstrokehook=644;e.dotmath=8901;e.dottedcircle=9676;e.doubleyodpatah=64287;e.doubleyodpatahhebrew=64287;e.downtackbelowcmb=798;e.downtackmod=725;e.dparen=9375;e.dsuperior=63211;e.dtail=598;e.dtopbar=396;e.duhiragana=12389;e.dukatakana=12485;e.dz=499;e.dzaltone=675;e.dzcaron=454;e.dzcurl=677;e.dzeabkhasiancyrillic=1249;e.dzecyrillic=1109;e.dzhecyrillic=1119;e.e=101;e.eacute=233;e.earth=9793;e.ebengali=2447;e.ebopomofo=12572;e.ebreve=277;e.ecandradeva=2317;e.ecandragujarati=2701;e.ecandravowelsigndeva=2373;e.ecandravowelsigngujarati=2757;e.ecaron=283;e.ecedillabreve=7709;e.echarmenian=1381;e.echyiwnarmenian=1415;e.ecircle=9428;e.ecircumflex=234;e.ecircumflexacute=7871;e.ecircumflexbelow=7705;e.ecircumflexdotbelow=7879;e.ecircumflexgrave=7873;e.ecircumflexhookabove=7875;e.ecircumflextilde=7877;e.ecyrillic=1108;e.edblgrave=517;e.edeva=2319;e.edieresis=235;e.edot=279;e.edotaccent=279;e.edotbelow=7865;e.eegurmukhi=2575;e.eematragurmukhi=2631;e.efcyrillic=1092;e.egrave=232;e.egujarati=2703;e.eharmenian=1383;e.ehbopomofo=12573;e.ehiragana=12360;e.ehookabove=7867;e.eibopomofo=12575;e.eight=56;e.eightarabic=1640;e.eightbengali=2542;e.eightcircle=9319;e.eightcircleinversesansserif=10129;e.eightdeva=2414;e.eighteencircle=9329;e.eighteenparen=9349;e.eighteenperiod=9369;e.eightgujarati=2798;e.eightgurmukhi=2670;e.eighthackarabic=1640;e.eighthangzhou=12328;e.eighthnotebeamed=9835;e.eightideographicparen=12839;e.eightinferior=8328;e.eightmonospace=65304;e.eightoldstyle=63288;e.eightparen=9339;e.eightperiod=9359;e.eightpersian=1784;e.eightroman=8567;e.eightsuperior=8312;e.eightthai=3672;e.einvertedbreve=519;e.eiotifiedcyrillic=1125;e.ekatakana=12456;e.ekatakanahalfwidth=65396;e.ekonkargurmukhi=2676;e.ekorean=12628;e.elcyrillic=1083;e.element=8712;e.elevencircle=9322;e.elevenparen=9342;e.elevenperiod=9362;e.elevenroman=8570;e.ellipsis=8230;e.ellipsisvertical=8942;e.emacron=275;e.emacronacute=7703;e.emacrongrave=7701;e.emcyrillic=1084;e.emdash=8212;e.emdashvertical=65073;e.emonospace=65349;e.emphasismarkarmenian=1371;e.emptyset=8709;e.enbopomofo=12579;e.encyrillic=1085;e.endash=8211;e.endashvertical=65074;e.endescendercyrillic=1187;e.eng=331;e.engbopomofo=12581;e.enghecyrillic=1189;e.enhookcyrillic=1224;e.enspace=8194;e.eogonek=281;e.eokorean=12627;e.eopen=603;e.eopenclosed=666;e.eopenreversed=604;e.eopenreversedclosed=606;e.eopenreversedhook=605;e.eparen=9376;e.epsilon=949;e.epsilontonos=941;e.equal=61;e.equalmonospace=65309;e.equalsmall=65126;e.equalsuperior=8316;e.equivalence=8801;e.erbopomofo=12582;e.ercyrillic=1088;e.ereversed=600;e.ereversedcyrillic=1101;e.escyrillic=1089;e.esdescendercyrillic=1195;e.esh=643;e.eshcurl=646;e.eshortdeva=2318;e.eshortvowelsigndeva=2374;e.eshreversedloop=426;e.eshsquatreversed=645;e.esmallhiragana=12359;e.esmallkatakana=12455;e.esmallkatakanahalfwidth=65386;e.estimated=8494;e.esuperior=63212;e.eta=951;e.etarmenian=1384;e.etatonos=942;e.eth=240;e.etilde=7869;e.etildebelow=7707;e.etnahtafoukhhebrew=1425;e.etnahtafoukhlefthebrew=1425;e.etnahtahebrew=1425;e.etnahtalefthebrew=1425;e.eturned=477;e.eukorean=12641;e.euro=8364;e.evowelsignbengali=2503;e.evowelsigndeva=2375;e.evowelsigngujarati=2759;e.exclam=33;e.exclamarmenian=1372;e.exclamdbl=8252;e.exclamdown=161;e.exclamdownsmall=63393;e.exclammonospace=65281;e.exclamsmall=63265;e.existential=8707;e.ezh=658;e.ezhcaron=495;e.ezhcurl=659;e.ezhreversed=441;e.ezhtail=442;e.f=102;e.fadeva=2398;e.fagurmukhi=2654;e.fahrenheit=8457;e.fathaarabic=1614;e.fathalowarabic=1614;e.fathatanarabic=1611;e.fbopomofo=12552;e.fcircle=9429;e.fdotaccent=7711;e.feharabic=1601;e.feharmenian=1414;e.fehfinalarabic=65234;e.fehinitialarabic=65235;e.fehmedialarabic=65236;e.feicoptic=997;e.female=9792;e.ff=64256;e.f_f=64256;e.ffi=64259;e.f_f_i=64259;e.ffl=64260;e.f_f_l=64260;e.fi=64257;e.f_i=64257;e.fifteencircle=9326;e.fifteenparen=9346;e.fifteenperiod=9366;e.figuredash=8210;e.filledbox=9632;e.filledrect=9644;e.finalkaf=1498;e.finalkafdagesh=64314;e.finalkafdageshhebrew=64314;e.finalkafhebrew=1498;e.finalmem=1501;e.finalmemhebrew=1501;e.finalnun=1503;e.finalnunhebrew=1503;e.finalpe=1507;e.finalpehebrew=1507;e.finaltsadi=1509;e.finaltsadihebrew=1509;e.firsttonechinese=713;e.fisheye=9673;e.fitacyrillic=1139;e.five=53;e.fivearabic=1637;e.fivebengali=2539;e.fivecircle=9316;e.fivecircleinversesansserif=10126;e.fivedeva=2411;e.fiveeighths=8541;e.fivegujarati=2795;e.fivegurmukhi=2667;e.fivehackarabic=1637;e.fivehangzhou=12325;e.fiveideographicparen=12836;e.fiveinferior=8325;e.fivemonospace=65301;e.fiveoldstyle=63285;e.fiveparen=9336;e.fiveperiod=9356;e.fivepersian=1781;e.fiveroman=8564;e.fivesuperior=8309;e.fivethai=3669;e.fl=64258;e.f_l=64258;e.florin=402;e.fmonospace=65350;e.fmsquare=13209;e.fofanthai=3615;e.fofathai=3613;e.fongmanthai=3663;e.forall=8704;e.four=52;e.fourarabic=1636;e.fourbengali=2538;e.fourcircle=9315;e.fourcircleinversesansserif=10125;e.fourdeva=2410;e.fourgujarati=2794;e.fourgurmukhi=2666;e.fourhackarabic=1636;e.fourhangzhou=12324;e.fourideographicparen=12835;e.fourinferior=8324;e.fourmonospace=65300;e.fournumeratorbengali=2551;e.fouroldstyle=63284;e.fourparen=9335;e.fourperiod=9355;e.fourpersian=1780;e.fourroman=8563;e.foursuperior=8308;e.fourteencircle=9325;e.fourteenparen=9345;e.fourteenperiod=9365;e.fourthai=3668;e.fourthtonechinese=715;e.fparen=9377;e.fraction=8260;e.franc=8355;e.g=103;e.gabengali=2455;e.gacute=501;e.gadeva=2327;e.gafarabic=1711;e.gaffinalarabic=64403;e.gafinitialarabic=64404;e.gafmedialarabic=64405;e.gagujarati=2711;e.gagurmukhi=2583;e.gahiragana=12364;e.gakatakana=12460;e.gamma=947;e.gammalatinsmall=611;e.gammasuperior=736;e.gangiacoptic=1003;e.gbopomofo=12557;e.gbreve=287;e.gcaron=487;e.gcedilla=291;e.gcircle=9430;e.gcircumflex=285;e.gcommaaccent=291;e.gdot=289;e.gdotaccent=289;e.gecyrillic=1075;e.gehiragana=12370;e.gekatakana=12466;e.geometricallyequal=8785;e.gereshaccenthebrew=1436;e.gereshhebrew=1523;e.gereshmuqdamhebrew=1437;e.germandbls=223;e.gershayimaccenthebrew=1438;e.gershayimhebrew=1524;e.getamark=12307;e.ghabengali=2456;e.ghadarmenian=1394;e.ghadeva=2328;e.ghagujarati=2712;e.ghagurmukhi=2584;e.ghainarabic=1594;e.ghainfinalarabic=65230;e.ghaininitialarabic=65231;e.ghainmedialarabic=65232;e.ghemiddlehookcyrillic=1173;e.ghestrokecyrillic=1171;e.gheupturncyrillic=1169;e.ghhadeva=2394;e.ghhagurmukhi=2650;e.ghook=608;e.ghzsquare=13203;e.gihiragana=12366;e.gikatakana=12462;e.gimarmenian=1379;e.gimel=1490;e.gimeldagesh=64306;e.gimeldageshhebrew=64306;e.gimelhebrew=1490;e.gjecyrillic=1107;e.glottalinvertedstroke=446;e.glottalstop=660;e.glottalstopinverted=662;e.glottalstopmod=704;e.glottalstopreversed=661;e.glottalstopreversedmod=705;e.glottalstopreversedsuperior=740;e.glottalstopstroke=673;e.glottalstopstrokereversed=674;e.gmacron=7713;e.gmonospace=65351;e.gohiragana=12372;e.gokatakana=12468;e.gparen=9378;e.gpasquare=13228;e.gradient=8711;e.grave=96;e.gravebelowcmb=790;e.gravecmb=768;e.gravecomb=768;e.gravedeva=2387;e.gravelowmod=718;e.gravemonospace=65344;e.gravetonecmb=832;e.greater=62;e.greaterequal=8805;e.greaterequalorless=8923;e.greatermonospace=65310;e.greaterorequivalent=8819;e.greaterorless=8823;e.greateroverequal=8807;e.greatersmall=65125;e.gscript=609;e.gstroke=485;e.guhiragana=12368;e.guillemotleft=171;e.guillemotright=187;e.guilsinglleft=8249;e.guilsinglright=8250;e.gukatakana=12464;e.guramusquare=13080;e.gysquare=13257;e.h=104;e.haabkhasiancyrillic=1193;e.haaltonearabic=1729;e.habengali=2489;e.hadescendercyrillic=1203;e.hadeva=2361;e.hagujarati=2745;e.hagurmukhi=2617;e.haharabic=1581;e.hahfinalarabic=65186;e.hahinitialarabic=65187;e.hahiragana=12399;e.hahmedialarabic=65188;e.haitusquare=13098;e.hakatakana=12495;e.hakatakanahalfwidth=65418;e.halantgurmukhi=2637;e.hamzaarabic=1569;e.hamzalowarabic=1569;e.hangulfiller=12644;e.hardsigncyrillic=1098;e.harpoonleftbarbup=8636;e.harpoonrightbarbup=8640;e.hasquare=13258;e.hatafpatah=1458;e.hatafpatah16=1458;e.hatafpatah23=1458;e.hatafpatah2f=1458;e.hatafpatahhebrew=1458;e.hatafpatahnarrowhebrew=1458;e.hatafpatahquarterhebrew=1458;e.hatafpatahwidehebrew=1458;e.hatafqamats=1459;e.hatafqamats1b=1459;e.hatafqamats28=1459;e.hatafqamats34=1459;e.hatafqamatshebrew=1459;e.hatafqamatsnarrowhebrew=1459;e.hatafqamatsquarterhebrew=1459;e.hatafqamatswidehebrew=1459;e.hatafsegol=1457;e.hatafsegol17=1457;e.hatafsegol24=1457;e.hatafsegol30=1457;e.hatafsegolhebrew=1457;e.hatafsegolnarrowhebrew=1457;e.hatafsegolquarterhebrew=1457;e.hatafsegolwidehebrew=1457;e.hbar=295;e.hbopomofo=12559;e.hbrevebelow=7723;e.hcedilla=7721;e.hcircle=9431;e.hcircumflex=293;e.hdieresis=7719;e.hdotaccent=7715;e.hdotbelow=7717;e.he=1492;e.heart=9829;e.heartsuitblack=9829;e.heartsuitwhite=9825;e.hedagesh=64308;e.hedageshhebrew=64308;e.hehaltonearabic=1729;e.heharabic=1607;e.hehebrew=1492;e.hehfinalaltonearabic=64423;e.hehfinalalttwoarabic=65258;e.hehfinalarabic=65258;e.hehhamzaabovefinalarabic=64421;e.hehhamzaaboveisolatedarabic=64420;e.hehinitialaltonearabic=64424;e.hehinitialarabic=65259;e.hehiragana=12408;e.hehmedialaltonearabic=64425;e.hehmedialarabic=65260;e.heiseierasquare=13179;e.hekatakana=12504;e.hekatakanahalfwidth=65421;e.hekutaarusquare=13110;e.henghook=615;e.herutusquare=13113;e.het=1495;e.hethebrew=1495;e.hhook=614;e.hhooksuperior=689;e.hieuhacirclekorean=12923;e.hieuhaparenkorean=12827;e.hieuhcirclekorean=12909;e.hieuhkorean=12622;e.hieuhparenkorean=12813;e.hihiragana=12402;e.hikatakana=12498;e.hikatakanahalfwidth=65419;e.hiriq=1460;e.hiriq14=1460;e.hiriq21=1460;e.hiriq2d=1460;e.hiriqhebrew=1460;e.hiriqnarrowhebrew=1460;e.hiriqquarterhebrew=1460;e.hiriqwidehebrew=1460;e.hlinebelow=7830;e.hmonospace=65352;e.hoarmenian=1392;e.hohipthai=3627;e.hohiragana=12411;e.hokatakana=12507;e.hokatakanahalfwidth=65422;e.holam=1465;e.holam19=1465;e.holam26=1465;e.holam32=1465;e.holamhebrew=1465;e.holamnarrowhebrew=1465;e.holamquarterhebrew=1465;e.holamwidehebrew=1465;e.honokhukthai=3630;e.hookabovecomb=777;e.hookcmb=777;e.hookpalatalizedbelowcmb=801;e.hookretroflexbelowcmb=802;e.hoonsquare=13122;e.horicoptic=1001;e.horizontalbar=8213;e.horncmb=795;e.hotsprings=9832;e.house=8962;e.hparen=9379;e.hsuperior=688;e.hturned=613;e.huhiragana=12405;e.huiitosquare=13107;e.hukatakana=12501;e.hukatakanahalfwidth=65420;e.hungarumlaut=733;e.hungarumlautcmb=779;e.hv=405;e.hyphen=45;e.hypheninferior=63205;e.hyphenmonospace=65293;e.hyphensmall=65123;e.hyphensuperior=63206;e.hyphentwo=8208;e.i=105;e.iacute=237;e.iacyrillic=1103;e.ibengali=2439;e.ibopomofo=12583;e.ibreve=301;e.icaron=464;e.icircle=9432;e.icircumflex=238;e.icyrillic=1110;e.idblgrave=521;e.ideographearthcircle=12943;e.ideographfirecircle=12939;e.ideographicallianceparen=12863;e.ideographiccallparen=12858;e.ideographiccentrecircle=12965;e.ideographicclose=12294;e.ideographiccomma=12289;e.ideographiccommaleft=65380;e.ideographiccongratulationparen=12855;e.ideographiccorrectcircle=12963;e.ideographicearthparen=12847;e.ideographicenterpriseparen=12861;e.ideographicexcellentcircle=12957;e.ideographicfestivalparen=12864;e.ideographicfinancialcircle=12950;e.ideographicfinancialparen=12854;e.ideographicfireparen=12843;e.ideographichaveparen=12850;e.ideographichighcircle=12964;e.ideographiciterationmark=12293;e.ideographiclaborcircle=12952;e.ideographiclaborparen=12856;e.ideographicleftcircle=12967;e.ideographiclowcircle=12966;e.ideographicmedicinecircle=12969;e.ideographicmetalparen=12846;e.ideographicmoonparen=12842;e.ideographicnameparen=12852;e.ideographicperiod=12290;e.ideographicprintcircle=12958;e.ideographicreachparen=12867;e.ideographicrepresentparen=12857;e.ideographicresourceparen=12862;e.ideographicrightcircle=12968;e.ideographicsecretcircle=12953;e.ideographicselfparen=12866;e.ideographicsocietyparen=12851;e.ideographicspace=12288;e.ideographicspecialparen=12853;e.ideographicstockparen=12849;e.ideographicstudyparen=12859;e.ideographicsunparen=12848;e.ideographicsuperviseparen=12860;e.ideographicwaterparen=12844;e.ideographicwoodparen=12845;e.ideographiczero=12295;e.ideographmetalcircle=12942;e.ideographmooncircle=12938;e.ideographnamecircle=12948;e.ideographsuncircle=12944;e.ideographwatercircle=12940;e.ideographwoodcircle=12941;e.ideva=2311;e.idieresis=239;e.idieresisacute=7727;e.idieresiscyrillic=1253;e.idotbelow=7883;e.iebrevecyrillic=1239;e.iecyrillic=1077;e.ieungacirclekorean=12917;e.ieungaparenkorean=12821;e.ieungcirclekorean=12903;e.ieungkorean=12615;e.ieungparenkorean=12807;e.igrave=236;e.igujarati=2695;e.igurmukhi=2567;e.ihiragana=12356;e.ihookabove=7881;e.iibengali=2440;e.iicyrillic=1080;e.iideva=2312;e.iigujarati=2696;e.iigurmukhi=2568;e.iimatragurmukhi=2624;e.iinvertedbreve=523;e.iishortcyrillic=1081;e.iivowelsignbengali=2496;e.iivowelsigndeva=2368;e.iivowelsigngujarati=2752;e.ij=307;e.ikatakana=12452;e.ikatakanahalfwidth=65394;e.ikorean=12643;e.ilde=732;e.iluyhebrew=1452;e.imacron=299;e.imacroncyrillic=1251;e.imageorapproximatelyequal=8787;e.imatragurmukhi=2623;e.imonospace=65353;e.increment=8710;e.infinity=8734;e.iniarmenian=1387;e.integral=8747;e.integralbottom=8993;e.integralbt=8993;e.integralex=63733;e.integraltop=8992;e.integraltp=8992;e.intersection=8745;e.intisquare=13061;e.invbullet=9688;e.invcircle=9689;e.invsmileface=9787;e.iocyrillic=1105;e.iogonek=303;e.iota=953;e.iotadieresis=970;e.iotadieresistonos=912;e.iotalatin=617;e.iotatonos=943;e.iparen=9380;e.irigurmukhi=2674;e.ismallhiragana=12355;e.ismallkatakana=12451;e.ismallkatakanahalfwidth=65384;e.issharbengali=2554;e.istroke=616;e.isuperior=63213;e.iterationhiragana=12445;e.iterationkatakana=12541;e.itilde=297;e.itildebelow=7725;e.iubopomofo=12585;e.iucyrillic=1102;e.ivowelsignbengali=2495;e.ivowelsigndeva=2367;e.ivowelsigngujarati=2751;e.izhitsacyrillic=1141;e.izhitsadblgravecyrillic=1143;e.j=106;e.jaarmenian=1393;e.jabengali=2460;e.jadeva=2332;e.jagujarati=2716;e.jagurmukhi=2588;e.jbopomofo=12560;e.jcaron=496;e.jcircle=9433;e.jcircumflex=309;e.jcrossedtail=669;e.jdotlessstroke=607;e.jecyrillic=1112;e.jeemarabic=1580;e.jeemfinalarabic=65182;e.jeeminitialarabic=65183;e.jeemmedialarabic=65184;e.jeharabic=1688;e.jehfinalarabic=64395;e.jhabengali=2461;e.jhadeva=2333;e.jhagujarati=2717;e.jhagurmukhi=2589;e.jheharmenian=1403;e.jis=12292;e.jmonospace=65354;e.jparen=9381;e.jsuperior=690;e.k=107;e.kabashkircyrillic=1185;e.kabengali=2453;e.kacute=7729;e.kacyrillic=1082;e.kadescendercyrillic=1179;e.kadeva=2325;e.kaf=1499;e.kafarabic=1603;e.kafdagesh=64315;e.kafdageshhebrew=64315;e.kaffinalarabic=65242;e.kafhebrew=1499;e.kafinitialarabic=65243;e.kafmedialarabic=65244;e.kafrafehebrew=64333;e.kagujarati=2709;e.kagurmukhi=2581;e.kahiragana=12363;e.kahookcyrillic=1220;e.kakatakana=12459;e.kakatakanahalfwidth=65398;e.kappa=954;e.kappasymbolgreek=1008;e.kapyeounmieumkorean=12657;e.kapyeounphieuphkorean=12676;e.kapyeounpieupkorean=12664;e.kapyeounssangpieupkorean=12665;e.karoriisquare=13069;e.kashidaautoarabic=1600;e.kashidaautonosidebearingarabic=1600;e.kasmallkatakana=12533;e.kasquare=13188;e.kasraarabic=1616;e.kasratanarabic=1613;e.kastrokecyrillic=1183;e.katahiraprolongmarkhalfwidth=65392;e.kaverticalstrokecyrillic=1181;e.kbopomofo=12558;e.kcalsquare=13193;e.kcaron=489;e.kcedilla=311;e.kcircle=9434;e.kcommaaccent=311;e.kdotbelow=7731;e.keharmenian=1412;e.kehiragana=12369;e.kekatakana=12465;e.kekatakanahalfwidth=65401;e.kenarmenian=1391;e.kesmallkatakana=12534;e.kgreenlandic=312;e.khabengali=2454;e.khacyrillic=1093;e.khadeva=2326;e.khagujarati=2710;e.khagurmukhi=2582;e.khaharabic=1582;e.khahfinalarabic=65190;e.khahinitialarabic=65191;e.khahmedialarabic=65192;e.kheicoptic=999;e.khhadeva=2393;e.khhagurmukhi=2649;e.khieukhacirclekorean=12920;e.khieukhaparenkorean=12824;e.khieukhcirclekorean=12906;e.khieukhkorean=12619;e.khieukhparenkorean=12810;e.khokhaithai=3586;e.khokhonthai=3589;e.khokhuatthai=3587;e.khokhwaithai=3588;e.khomutthai=3675;e.khook=409;e.khorakhangthai=3590;e.khzsquare=13201;e.kihiragana=12365;e.kikatakana=12461;e.kikatakanahalfwidth=65399;e.kiroguramusquare=13077;e.kiromeetorusquare=13078;e.kirosquare=13076;e.kiyeokacirclekorean=12910;e.kiyeokaparenkorean=12814;e.kiyeokcirclekorean=12896;e.kiyeokkorean=12593;e.kiyeokparenkorean=12800;e.kiyeoksioskorean=12595;e.kjecyrillic=1116;e.klinebelow=7733;e.klsquare=13208;e.kmcubedsquare=13222;e.kmonospace=65355;e.kmsquaredsquare=13218;e.kohiragana=12371;e.kohmsquare=13248;e.kokaithai=3585;e.kokatakana=12467;e.kokatakanahalfwidth=65402;e.kooposquare=13086;e.koppacyrillic=1153;e.koreanstandardsymbol=12927;e.koroniscmb=835;e.kparen=9382;e.kpasquare=13226;e.ksicyrillic=1135;e.ktsquare=13263;e.kturned=670;e.kuhiragana=12367;e.kukatakana=12463;e.kukatakanahalfwidth=65400;e.kvsquare=13240;e.kwsquare=13246;e.l=108;e.labengali=2482;e.lacute=314;e.ladeva=2354;e.lagujarati=2738;e.lagurmukhi=2610;e.lakkhangyaothai=3653;e.lamaleffinalarabic=65276;e.lamalefhamzaabovefinalarabic=65272;e.lamalefhamzaaboveisolatedarabic=65271;e.lamalefhamzabelowfinalarabic=65274;e.lamalefhamzabelowisolatedarabic=65273;e.lamalefisolatedarabic=65275;e.lamalefmaddaabovefinalarabic=65270;e.lamalefmaddaaboveisolatedarabic=65269;e.lamarabic=1604;e.lambda=955;e.lambdastroke=411;e.lamed=1500;e.lameddagesh=64316;e.lameddageshhebrew=64316;e.lamedhebrew=1500;e.lamfinalarabic=65246;e.lamhahinitialarabic=64714;e.laminitialarabic=65247;e.lamjeeminitialarabic=64713;e.lamkhahinitialarabic=64715;e.lamlamhehisolatedarabic=65010;e.lammedialarabic=65248;e.lammeemhahinitialarabic=64904;e.lammeeminitialarabic=64716;e.largecircle=9711;e.lbar=410;e.lbelt=620;e.lbopomofo=12556;e.lcaron=318;e.lcedilla=316;e.lcircle=9435;e.lcircumflexbelow=7741;e.lcommaaccent=316;e.ldot=320;e.ldotaccent=320;e.ldotbelow=7735;e.ldotbelowmacron=7737;e.leftangleabovecmb=794;e.lefttackbelowcmb=792;e.less=60;e.lessequal=8804;e.lessequalorgreater=8922;e.lessmonospace=65308;e.lessorequivalent=8818;e.lessorgreater=8822;e.lessoverequal=8806;e.lesssmall=65124;e.lezh=622;e.lfblock=9612;e.lhookretroflex=621;e.lira=8356;e.liwnarmenian=1388;e.lj=457;e.ljecyrillic=1113;e.ll=63168;e.lladeva=2355;e.llagujarati=2739;e.llinebelow=7739;e.llladeva=2356;e.llvocalicbengali=2529;e.llvocalicdeva=2401;e.llvocalicvowelsignbengali=2531;e.llvocalicvowelsigndeva=2403;e.lmiddletilde=619;e.lmonospace=65356;e.lmsquare=13264;e.lochulathai=3628;e.logicaland=8743;e.logicalnot=172;e.logicalnotreversed=8976;e.logicalor=8744;e.lolingthai=3621;e.longs=383;e.lowlinecenterline=65102;e.lowlinecmb=818;e.lowlinedashed=65101;e.lozenge=9674;e.lparen=9383;e.lslash=322;e.lsquare=8467;e.lsuperior=63214;e.ltshade=9617;e.luthai=3622;e.lvocalicbengali=2444;e.lvocalicdeva=2316;e.lvocalicvowelsignbengali=2530;e.lvocalicvowelsigndeva=2402;e.lxsquare=13267;e.m=109;e.mabengali=2478;e.macron=175;e.macronbelowcmb=817;e.macroncmb=772;e.macronlowmod=717;e.macronmonospace=65507;e.macute=7743;e.madeva=2350;e.magujarati=2734;e.magurmukhi=2606;e.mahapakhhebrew=1444;e.mahapakhlefthebrew=1444;e.mahiragana=12414;e.maichattawalowleftthai=63637;e.maichattawalowrightthai=63636;e.maichattawathai=3659;e.maichattawaupperleftthai=63635;e.maieklowleftthai=63628;e.maieklowrightthai=63627;e.maiekthai=3656;e.maiekupperleftthai=63626;e.maihanakatleftthai=63620;e.maihanakatthai=3633;e.maitaikhuleftthai=63625;e.maitaikhuthai=3655;e.maitholowleftthai=63631;e.maitholowrightthai=63630;e.maithothai=3657;e.maithoupperleftthai=63629;e.maitrilowleftthai=63634;e.maitrilowrightthai=63633;e.maitrithai=3658;e.maitriupperleftthai=63632;e.maiyamokthai=3654;e.makatakana=12510;e.makatakanahalfwidth=65423;e.male=9794;e.mansyonsquare=13127;e.maqafhebrew=1470;e.mars=9794;e.masoracirclehebrew=1455;e.masquare=13187;e.mbopomofo=12551;e.mbsquare=13268;e.mcircle=9436;e.mcubedsquare=13221;e.mdotaccent=7745;e.mdotbelow=7747;e.meemarabic=1605;e.meemfinalarabic=65250;e.meeminitialarabic=65251;e.meemmedialarabic=65252;e.meemmeeminitialarabic=64721;e.meemmeemisolatedarabic=64584;e.meetorusquare=13133;e.mehiragana=12417;e.meizierasquare=13182;e.mekatakana=12513;e.mekatakanahalfwidth=65426;e.mem=1502;e.memdagesh=64318;e.memdageshhebrew=64318;e.memhebrew=1502;e.menarmenian=1396;e.merkhahebrew=1445;e.merkhakefulahebrew=1446;e.merkhakefulalefthebrew=1446;e.merkhalefthebrew=1445;e.mhook=625;e.mhzsquare=13202;e.middledotkatakanahalfwidth=65381;e.middot=183;e.mieumacirclekorean=12914;e.mieumaparenkorean=12818;e.mieumcirclekorean=12900;e.mieumkorean=12609;e.mieumpansioskorean=12656;e.mieumparenkorean=12804;e.mieumpieupkorean=12654;e.mieumsioskorean=12655;e.mihiragana=12415;e.mikatakana=12511;e.mikatakanahalfwidth=65424;e.minus=8722;e.minusbelowcmb=800;e.minuscircle=8854;e.minusmod=727;e.minusplus=8723;e.minute=8242;e.miribaarusquare=13130;e.mirisquare=13129;e.mlonglegturned=624;e.mlsquare=13206;e.mmcubedsquare=13219;e.mmonospace=65357;e.mmsquaredsquare=13215;e.mohiragana=12418;e.mohmsquare=13249;e.mokatakana=12514;e.mokatakanahalfwidth=65427;e.molsquare=13270;e.momathai=3617;e.moverssquare=13223;e.moverssquaredsquare=13224;e.mparen=9384;e.mpasquare=13227;e.mssquare=13235;e.msuperior=63215;e.mturned=623;e.mu=181;e.mu1=181;e.muasquare=13186;e.muchgreater=8811;e.muchless=8810;e.mufsquare=13196;e.mugreek=956;e.mugsquare=13197;e.muhiragana=12416;e.mukatakana=12512;e.mukatakanahalfwidth=65425;e.mulsquare=13205;e.multiply=215;e.mumsquare=13211;e.munahhebrew=1443;e.munahlefthebrew=1443;e.musicalnote=9834;e.musicalnotedbl=9835;e.musicflatsign=9837;e.musicsharpsign=9839;e.mussquare=13234;e.muvsquare=13238;e.muwsquare=13244;e.mvmegasquare=13241;e.mvsquare=13239;e.mwmegasquare=13247;e.mwsquare=13245;e.n=110;e.nabengali=2472;e.nabla=8711;e.nacute=324;e.nadeva=2344;e.nagujarati=2728;e.nagurmukhi=2600;e.nahiragana=12394;e.nakatakana=12490;e.nakatakanahalfwidth=65413;e.napostrophe=329;e.nasquare=13185;e.nbopomofo=12555;e.nbspace=160;e.ncaron=328;e.ncedilla=326;e.ncircle=9437;e.ncircumflexbelow=7755;e.ncommaaccent=326;e.ndotaccent=7749;e.ndotbelow=7751;e.nehiragana=12397;e.nekatakana=12493;e.nekatakanahalfwidth=65416;e.newsheqelsign=8362;e.nfsquare=13195;e.ngabengali=2457;e.ngadeva=2329;e.ngagujarati=2713;e.ngagurmukhi=2585;e.ngonguthai=3591;e.nhiragana=12435;e.nhookleft=626;e.nhookretroflex=627;e.nieunacirclekorean=12911;e.nieunaparenkorean=12815;e.nieuncieuckorean=12597;e.nieuncirclekorean=12897;e.nieunhieuhkorean=12598;e.nieunkorean=12596;e.nieunpansioskorean=12648;e.nieunparenkorean=12801;e.nieunsioskorean=12647;e.nieuntikeutkorean=12646;e.nihiragana=12395;e.nikatakana=12491;e.nikatakanahalfwidth=65414;e.nikhahitleftthai=63641;e.nikhahitthai=3661;e.nine=57;e.ninearabic=1641;e.ninebengali=2543;e.ninecircle=9320;e.ninecircleinversesansserif=10130;e.ninedeva=2415;e.ninegujarati=2799;e.ninegurmukhi=2671;e.ninehackarabic=1641;e.ninehangzhou=12329;e.nineideographicparen=12840;e.nineinferior=8329;e.ninemonospace=65305;e.nineoldstyle=63289;e.nineparen=9340;e.nineperiod=9360;e.ninepersian=1785;e.nineroman=8568;e.ninesuperior=8313;e.nineteencircle=9330;e.nineteenparen=9350;e.nineteenperiod=9370;e.ninethai=3673;e.nj=460;e.njecyrillic=1114;e.nkatakana=12531;e.nkatakanahalfwidth=65437;e.nlegrightlong=414;e.nlinebelow=7753;e.nmonospace=65358;e.nmsquare=13210;e.nnabengali=2467;e.nnadeva=2339;e.nnagujarati=2723;e.nnagurmukhi=2595;e.nnnadeva=2345;e.nohiragana=12398;e.nokatakana=12494;e.nokatakanahalfwidth=65417;e.nonbreakingspace=160;e.nonenthai=3603;e.nonuthai=3609;e.noonarabic=1606;e.noonfinalarabic=65254;e.noonghunnaarabic=1722;e.noonghunnafinalarabic=64415;e.nooninitialarabic=65255;e.noonjeeminitialarabic=64722;e.noonjeemisolatedarabic=64587;e.noonmedialarabic=65256;e.noonmeeminitialarabic=64725;e.noonmeemisolatedarabic=64590;e.noonnoonfinalarabic=64653;e.notcontains=8716;e.notelement=8713;e.notelementof=8713;e.notequal=8800;e.notgreater=8815;e.notgreaternorequal=8817;e.notgreaternorless=8825;e.notidentical=8802;e.notless=8814;e.notlessnorequal=8816;e.notparallel=8742;e.notprecedes=8832;e.notsubset=8836;e.notsucceeds=8833;e.notsuperset=8837;e.nowarmenian=1398;e.nparen=9385;e.nssquare=13233;e.nsuperior=8319;e.ntilde=241;e.nu=957;e.nuhiragana=12396;e.nukatakana=12492;e.nukatakanahalfwidth=65415;e.nuktabengali=2492;e.nuktadeva=2364;e.nuktagujarati=2748;e.nuktagurmukhi=2620;e.numbersign=35;e.numbersignmonospace=65283;e.numbersignsmall=65119;e.numeralsigngreek=884;e.numeralsignlowergreek=885;e.numero=8470;e.nun=1504;e.nundagesh=64320;e.nundageshhebrew=64320;e.nunhebrew=1504;e.nvsquare=13237;e.nwsquare=13243;e.nyabengali=2462;e.nyadeva=2334;e.nyagujarati=2718;e.nyagurmukhi=2590;e.o=111;e.oacute=243;e.oangthai=3629;e.obarred=629;e.obarredcyrillic=1257;e.obarreddieresiscyrillic=1259;e.obengali=2451;e.obopomofo=12571;e.obreve=335;e.ocandradeva=2321;e.ocandragujarati=2705;e.ocandravowelsigndeva=2377;e.ocandravowelsigngujarati=2761;e.ocaron=466;e.ocircle=9438;e.ocircumflex=244;e.ocircumflexacute=7889;e.ocircumflexdotbelow=7897;e.ocircumflexgrave=7891;e.ocircumflexhookabove=7893;e.ocircumflextilde=7895;e.ocyrillic=1086;e.odblacute=337;e.odblgrave=525;e.odeva=2323;e.odieresis=246;e.odieresiscyrillic=1255;e.odotbelow=7885;e.oe=339;e.oekorean=12634;e.ogonek=731;e.ogonekcmb=808;e.ograve=242;e.ogujarati=2707;e.oharmenian=1413;e.ohiragana=12362;e.ohookabove=7887;e.ohorn=417;e.ohornacute=7899;e.ohorndotbelow=7907;e.ohorngrave=7901;e.ohornhookabove=7903;e.ohorntilde=7905;e.ohungarumlaut=337;e.oi=419;e.oinvertedbreve=527;e.okatakana=12458;e.okatakanahalfwidth=65397;e.okorean=12631;e.olehebrew=1451;e.omacron=333;e.omacronacute=7763;e.omacrongrave=7761;e.omdeva=2384;e.omega=969;e.omega1=982;e.omegacyrillic=1121;e.omegalatinclosed=631;e.omegaroundcyrillic=1147;e.omegatitlocyrillic=1149;e.omegatonos=974;e.omgujarati=2768;e.omicron=959;e.omicrontonos=972;e.omonospace=65359;e.one=49;e.onearabic=1633;e.onebengali=2535;e.onecircle=9312;e.onecircleinversesansserif=10122;e.onedeva=2407;e.onedotenleader=8228;e.oneeighth=8539;e.onefitted=63196;e.onegujarati=2791;e.onegurmukhi=2663;e.onehackarabic=1633;e.onehalf=189;e.onehangzhou=12321;e.oneideographicparen=12832;e.oneinferior=8321;e.onemonospace=65297;e.onenumeratorbengali=2548;e.oneoldstyle=63281;e.oneparen=9332;e.oneperiod=9352;e.onepersian=1777;e.onequarter=188;e.oneroman=8560;e.onesuperior=185;e.onethai=3665;e.onethird=8531;e.oogonek=491;e.oogonekmacron=493;e.oogurmukhi=2579;e.oomatragurmukhi=2635;e.oopen=596;e.oparen=9386;e.openbullet=9702;e.option=8997;e.ordfeminine=170;e.ordmasculine=186;e.orthogonal=8735;e.oshortdeva=2322;e.oshortvowelsigndeva=2378;e.oslash=248;e.oslashacute=511;e.osmallhiragana=12361;e.osmallkatakana=12457;e.osmallkatakanahalfwidth=65387;e.ostrokeacute=511;e.osuperior=63216;e.otcyrillic=1151;e.otilde=245;e.otildeacute=7757;e.otildedieresis=7759;e.oubopomofo=12577;e.overline=8254;e.overlinecenterline=65098;e.overlinecmb=773;e.overlinedashed=65097;e.overlinedblwavy=65100;e.overlinewavy=65099;e.overscore=175;e.ovowelsignbengali=2507;e.ovowelsigndeva=2379;e.ovowelsigngujarati=2763;e.p=112;e.paampssquare=13184;e.paasentosquare=13099;e.pabengali=2474;e.pacute=7765;e.padeva=2346;e.pagedown=8671;e.pageup=8670;e.pagujarati=2730;e.pagurmukhi=2602;e.pahiragana=12401;e.paiyannoithai=3631;e.pakatakana=12497;e.palatalizationcyrilliccmb=1156;e.palochkacyrillic=1216;e.pansioskorean=12671;e.paragraph=182;e.parallel=8741;e.parenleft=40;e.parenleftaltonearabic=64830;e.parenleftbt=63725;e.parenleftex=63724;e.parenleftinferior=8333;e.parenleftmonospace=65288;e.parenleftsmall=65113;e.parenleftsuperior=8317;e.parenlefttp=63723;e.parenleftvertical=65077;e.parenright=41;e.parenrightaltonearabic=64831;e.parenrightbt=63736;e.parenrightex=63735;e.parenrightinferior=8334;e.parenrightmonospace=65289;e.parenrightsmall=65114;e.parenrightsuperior=8318;e.parenrighttp=63734;e.parenrightvertical=65078;e.partialdiff=8706;e.paseqhebrew=1472;e.pashtahebrew=1433;e.pasquare=13225;e.patah=1463;e.patah11=1463;e.patah1d=1463;e.patah2a=1463;e.patahhebrew=1463;e.patahnarrowhebrew=1463;e.patahquarterhebrew=1463;e.patahwidehebrew=1463;e.pazerhebrew=1441;e.pbopomofo=12550;e.pcircle=9439;e.pdotaccent=7767;e.pe=1508;e.pecyrillic=1087;e.pedagesh=64324;e.pedageshhebrew=64324;e.peezisquare=13115;e.pefinaldageshhebrew=64323;e.peharabic=1662;e.peharmenian=1402;e.pehebrew=1508;e.pehfinalarabic=64343;e.pehinitialarabic=64344;e.pehiragana=12410;e.pehmedialarabic=64345;e.pekatakana=12506;e.pemiddlehookcyrillic=1191;e.perafehebrew=64334;e.percent=37;e.percentarabic=1642;e.percentmonospace=65285;e.percentsmall=65130;e.period=46;e.periodarmenian=1417;e.periodcentered=183;e.periodhalfwidth=65377;e.periodinferior=63207;e.periodmonospace=65294;e.periodsmall=65106;e.periodsuperior=63208;e.perispomenigreekcmb=834;e.perpendicular=8869;e.perthousand=8240;e.peseta=8359;e.pfsquare=13194;e.phabengali=2475;e.phadeva=2347;e.phagujarati=2731;e.phagurmukhi=2603;e.phi=966;e.phi1=981;e.phieuphacirclekorean=12922;e.phieuphaparenkorean=12826;e.phieuphcirclekorean=12908;e.phieuphkorean=12621;e.phieuphparenkorean=12812;e.philatin=632;e.phinthuthai=3642;e.phisymbolgreek=981;e.phook=421;e.phophanthai=3614;e.phophungthai=3612;e.phosamphaothai=3616;e.pi=960;e.pieupacirclekorean=12915;e.pieupaparenkorean=12819;e.pieupcieuckorean=12662;e.pieupcirclekorean=12901;e.pieupkiyeokkorean=12658;e.pieupkorean=12610;e.pieupparenkorean=12805;e.pieupsioskiyeokkorean=12660;e.pieupsioskorean=12612;e.pieupsiostikeutkorean=12661;e.pieupthieuthkorean=12663;e.pieuptikeutkorean=12659;e.pihiragana=12404;e.pikatakana=12500;e.pisymbolgreek=982;e.piwrarmenian=1411;e.planckover2pi=8463;e.planckover2pi1=8463;e.plus=43;e.plusbelowcmb=799;e.pluscircle=8853;e.plusminus=177;e.plusmod=726;e.plusmonospace=65291;e.plussmall=65122;e.plussuperior=8314;e.pmonospace=65360;e.pmsquare=13272;e.pohiragana=12413;e.pointingindexdownwhite=9759;e.pointingindexleftwhite=9756;e.pointingindexrightwhite=9758;e.pointingindexupwhite=9757;e.pokatakana=12509;e.poplathai=3611;e.postalmark=12306;e.postalmarkface=12320;e.pparen=9387;e.precedes=8826;e.prescription=8478;e.primemod=697;e.primereversed=8245;e.product=8719;e.projective=8965;e.prolongedkana=12540;e.propellor=8984;e.propersubset=8834;e.propersuperset=8835;e.proportion=8759;e.proportional=8733;e.psi=968;e.psicyrillic=1137;e.psilipneumatacyrilliccmb=1158;e.pssquare=13232;e.puhiragana=12407;e.pukatakana=12503;e.pvsquare=13236;e.pwsquare=13242;e.q=113;e.qadeva=2392;e.qadmahebrew=1448;e.qafarabic=1602;e.qaffinalarabic=65238;e.qafinitialarabic=65239;e.qafmedialarabic=65240;e.qamats=1464;e.qamats10=1464;e.qamats1a=1464;e.qamats1c=1464;e.qamats27=1464;e.qamats29=1464;e.qamats33=1464;e.qamatsde=1464;e.qamatshebrew=1464;e.qamatsnarrowhebrew=1464;e.qamatsqatanhebrew=1464;e.qamatsqatannarrowhebrew=1464;e.qamatsqatanquarterhebrew=1464;e.qamatsqatanwidehebrew=1464;e.qamatsquarterhebrew=1464;e.qamatswidehebrew=1464;e.qarneyparahebrew=1439;e.qbopomofo=12561;e.qcircle=9440;e.qhook=672;e.qmonospace=65361;e.qof=1511;e.qofdagesh=64327;e.qofdageshhebrew=64327;e.qofhebrew=1511;e.qparen=9388;e.quarternote=9833;e.qubuts=1467;e.qubuts18=1467;e.qubuts25=1467;e.qubuts31=1467;e.qubutshebrew=1467;e.qubutsnarrowhebrew=1467;e.qubutsquarterhebrew=1467;e.qubutswidehebrew=1467;e.question=63;e.questionarabic=1567;e.questionarmenian=1374;e.questiondown=191;e.questiondownsmall=63423;e.questiongreek=894;e.questionmonospace=65311;e.questionsmall=63295;e.quotedbl=34;e.quotedblbase=8222;e.quotedblleft=8220;e.quotedblmonospace=65282;e.quotedblprime=12318;e.quotedblprimereversed=12317;e.quotedblright=8221;e.quoteleft=8216;e.quoteleftreversed=8219;e.quotereversed=8219;e.quoteright=8217;e.quoterightn=329;e.quotesinglbase=8218;e.quotesingle=39;e.quotesinglemonospace=65287;e.r=114;e.raarmenian=1404;e.rabengali=2480;e.racute=341;e.radeva=2352;e.radical=8730;e.radicalex=63717;e.radoverssquare=13230;e.radoverssquaredsquare=13231;e.radsquare=13229;e.rafe=1471;e.rafehebrew=1471;e.ragujarati=2736;e.ragurmukhi=2608;e.rahiragana=12425;e.rakatakana=12521;e.rakatakanahalfwidth=65431;e.ralowerdiagonalbengali=2545;e.ramiddlediagonalbengali=2544;e.ramshorn=612;e.ratio=8758;e.rbopomofo=12566;e.rcaron=345;e.rcedilla=343;e.rcircle=9441;e.rcommaaccent=343;e.rdblgrave=529;e.rdotaccent=7769;e.rdotbelow=7771;e.rdotbelowmacron=7773;e.referencemark=8251;e.reflexsubset=8838;e.reflexsuperset=8839;e.registered=174;e.registersans=63720;e.registerserif=63194;e.reharabic=1585;e.reharmenian=1408;e.rehfinalarabic=65198;e.rehiragana=12428;e.rekatakana=12524;e.rekatakanahalfwidth=65434;e.resh=1512;e.reshdageshhebrew=64328;e.reshhebrew=1512;e.reversedtilde=8765;e.reviahebrew=1431;e.reviamugrashhebrew=1431;e.revlogicalnot=8976;e.rfishhook=638;e.rfishhookreversed=639;e.rhabengali=2525;e.rhadeva=2397;e.rho=961;e.rhook=637;e.rhookturned=635;e.rhookturnedsuperior=693;e.rhosymbolgreek=1009;e.rhotichookmod=734;e.rieulacirclekorean=12913;e.rieulaparenkorean=12817;e.rieulcirclekorean=12899;e.rieulhieuhkorean=12608;e.rieulkiyeokkorean=12602;e.rieulkiyeoksioskorean=12649;e.rieulkorean=12601;e.rieulmieumkorean=12603;e.rieulpansioskorean=12652;e.rieulparenkorean=12803;e.rieulphieuphkorean=12607;e.rieulpieupkorean=12604;e.rieulpieupsioskorean=12651;e.rieulsioskorean=12605;e.rieulthieuthkorean=12606;e.rieultikeutkorean=12650;e.rieulyeorinhieuhkorean=12653;e.rightangle=8735;e.righttackbelowcmb=793;e.righttriangle=8895;e.rihiragana=12426;e.rikatakana=12522;e.rikatakanahalfwidth=65432;e.ring=730;e.ringbelowcmb=805;e.ringcmb=778;e.ringhalfleft=703;e.ringhalfleftarmenian=1369;e.ringhalfleftbelowcmb=796;e.ringhalfleftcentered=723;e.ringhalfright=702;e.ringhalfrightbelowcmb=825;e.ringhalfrightcentered=722;e.rinvertedbreve=531;e.rittorusquare=13137;e.rlinebelow=7775;e.rlongleg=636;e.rlonglegturned=634;e.rmonospace=65362;e.rohiragana=12429;e.rokatakana=12525;e.rokatakanahalfwidth=65435;e.roruathai=3619;e.rparen=9389;e.rrabengali=2524;e.rradeva=2353;e.rragurmukhi=2652;e.rreharabic=1681;e.rrehfinalarabic=64397;e.rrvocalicbengali=2528;e.rrvocalicdeva=2400;e.rrvocalicgujarati=2784;e.rrvocalicvowelsignbengali=2500;e.rrvocalicvowelsigndeva=2372;e.rrvocalicvowelsigngujarati=2756;e.rsuperior=63217;e.rtblock=9616;e.rturned=633;e.rturnedsuperior=692;e.ruhiragana=12427;e.rukatakana=12523;e.rukatakanahalfwidth=65433;e.rupeemarkbengali=2546;e.rupeesignbengali=2547;e.rupiah=63197;e.ruthai=3620;e.rvocalicbengali=2443;e.rvocalicdeva=2315;e.rvocalicgujarati=2699;e.rvocalicvowelsignbengali=2499;e.rvocalicvowelsigndeva=2371;e.rvocalicvowelsigngujarati=2755;e.s=115;e.sabengali=2488;e.sacute=347;e.sacutedotaccent=7781;e.sadarabic=1589;e.sadeva=2360;e.sadfinalarabic=65210;e.sadinitialarabic=65211;e.sadmedialarabic=65212;e.sagujarati=2744;e.sagurmukhi=2616;e.sahiragana=12373;e.sakatakana=12469;e.sakatakanahalfwidth=65403;e.sallallahoualayhewasallamarabic=65018;e.samekh=1505;e.samekhdagesh=64321;e.samekhdageshhebrew=64321;e.samekhhebrew=1505;e.saraaathai=3634;e.saraaethai=3649;e.saraaimaimalaithai=3652;e.saraaimaimuanthai=3651;e.saraamthai=3635;e.saraathai=3632;e.saraethai=3648;e.saraiileftthai=63622;e.saraiithai=3637;e.saraileftthai=63621;e.saraithai=3636;e.saraothai=3650;e.saraueeleftthai=63624;e.saraueethai=3639;e.saraueleftthai=63623;e.sarauethai=3638;e.sarauthai=3640;e.sarauuthai=3641;e.sbopomofo=12569;e.scaron=353;e.scarondotaccent=7783;e.scedilla=351;e.schwa=601;e.schwacyrillic=1241;e.schwadieresiscyrillic=1243;e.schwahook=602;e.scircle=9442;e.scircumflex=349;e.scommaaccent=537;e.sdotaccent=7777;e.sdotbelow=7779;e.sdotbelowdotaccent=7785;e.seagullbelowcmb=828;e.second=8243;e.secondtonechinese=714;e.section=167;e.seenarabic=1587;e.seenfinalarabic=65202;e.seeninitialarabic=65203;e.seenmedialarabic=65204;e.segol=1462;e.segol13=1462;e.segol1f=1462;e.segol2c=1462;e.segolhebrew=1462;e.segolnarrowhebrew=1462;e.segolquarterhebrew=1462;e.segoltahebrew=1426;e.segolwidehebrew=1462;e.seharmenian=1405;e.sehiragana=12379;e.sekatakana=12475;e.sekatakanahalfwidth=65406;e.semicolon=59;e.semicolonarabic=1563;e.semicolonmonospace=65307;e.semicolonsmall=65108;e.semivoicedmarkkana=12444;e.semivoicedmarkkanahalfwidth=65439;e.sentisquare=13090;e.sentosquare=13091;e.seven=55;e.sevenarabic=1639;e.sevenbengali=2541;e.sevencircle=9318;e.sevencircleinversesansserif=10128;e.sevendeva=2413;e.seveneighths=8542;e.sevengujarati=2797;e.sevengurmukhi=2669;e.sevenhackarabic=1639;e.sevenhangzhou=12327;e.sevenideographicparen=12838;e.seveninferior=8327;e.sevenmonospace=65303;e.sevenoldstyle=63287;e.sevenparen=9338;e.sevenperiod=9358;e.sevenpersian=1783;e.sevenroman=8566;e.sevensuperior=8311;e.seventeencircle=9328;e.seventeenparen=9348;e.seventeenperiod=9368;e.seventhai=3671;e.sfthyphen=173;e.shaarmenian=1399;e.shabengali=2486;e.shacyrillic=1096;e.shaddaarabic=1617;e.shaddadammaarabic=64609;e.shaddadammatanarabic=64606;e.shaddafathaarabic=64608;e.shaddakasraarabic=64610;e.shaddakasratanarabic=64607;e.shade=9618;e.shadedark=9619;e.shadelight=9617;e.shademedium=9618;e.shadeva=2358;e.shagujarati=2742;e.shagurmukhi=2614;e.shalshelethebrew=1427;e.shbopomofo=12565;e.shchacyrillic=1097;e.sheenarabic=1588;e.sheenfinalarabic=65206;e.sheeninitialarabic=65207;e.sheenmedialarabic=65208;e.sheicoptic=995;e.sheqel=8362;e.sheqelhebrew=8362;e.sheva=1456;e.sheva115=1456;e.sheva15=1456;e.sheva22=1456;e.sheva2e=1456;e.shevahebrew=1456;e.shevanarrowhebrew=1456;e.shevaquarterhebrew=1456;e.shevawidehebrew=1456;e.shhacyrillic=1211;e.shimacoptic=1005;e.shin=1513;e.shindagesh=64329;e.shindageshhebrew=64329;e.shindageshshindot=64300;e.shindageshshindothebrew=64300;e.shindageshsindot=64301;e.shindageshsindothebrew=64301;e.shindothebrew=1473;e.shinhebrew=1513;e.shinshindot=64298;e.shinshindothebrew=64298;e.shinsindot=64299;e.shinsindothebrew=64299;e.shook=642;e.sigma=963;e.sigma1=962;e.sigmafinal=962;e.sigmalunatesymbolgreek=1010;e.sihiragana=12375;e.sikatakana=12471;e.sikatakanahalfwidth=65404;e.siluqhebrew=1469;e.siluqlefthebrew=1469;e.similar=8764;e.sindothebrew=1474;e.siosacirclekorean=12916;e.siosaparenkorean=12820;e.sioscieuckorean=12670;e.sioscirclekorean=12902;e.sioskiyeokkorean=12666;e.sioskorean=12613;e.siosnieunkorean=12667;e.siosparenkorean=12806;e.siospieupkorean=12669;e.siostikeutkorean=12668;e.six=54;e.sixarabic=1638;e.sixbengali=2540;e.sixcircle=9317;e.sixcircleinversesansserif=10127;e.sixdeva=2412;e.sixgujarati=2796;e.sixgurmukhi=2668;e.sixhackarabic=1638;e.sixhangzhou=12326;e.sixideographicparen=12837;e.sixinferior=8326;e.sixmonospace=65302;e.sixoldstyle=63286;e.sixparen=9337;e.sixperiod=9357;e.sixpersian=1782;e.sixroman=8565;e.sixsuperior=8310;e.sixteencircle=9327;e.sixteencurrencydenominatorbengali=2553;e.sixteenparen=9347;e.sixteenperiod=9367;e.sixthai=3670;e.slash=47;e.slashmonospace=65295;e.slong=383;e.slongdotaccent=7835;e.smileface=9786;e.smonospace=65363;e.sofpasuqhebrew=1475;e.softhyphen=173;e.softsigncyrillic=1100;e.sohiragana=12381;e.sokatakana=12477;e.sokatakanahalfwidth=65407;e.soliduslongoverlaycmb=824;e.solidusshortoverlaycmb=823;e.sorusithai=3625;e.sosalathai=3624;e.sosothai=3595;e.sosuathai=3626;e.space=32;e.spacehackarabic=32;e.spade=9824;e.spadesuitblack=9824;e.spadesuitwhite=9828;e.sparen=9390;e.squarebelowcmb=827;e.squarecc=13252;e.squarecm=13213;e.squarediagonalcrosshatchfill=9641;e.squarehorizontalfill=9636;e.squarekg=13199;e.squarekm=13214;e.squarekmcapital=13262;e.squareln=13265;e.squarelog=13266;e.squaremg=13198;e.squaremil=13269;e.squaremm=13212;e.squaremsquared=13217;e.squareorthogonalcrosshatchfill=9638;e.squareupperlefttolowerrightfill=9639;e.squareupperrighttolowerleftfill=9640;e.squareverticalfill=9637;e.squarewhitewithsmallblack=9635;e.srsquare=13275;e.ssabengali=2487;e.ssadeva=2359;e.ssagujarati=2743;e.ssangcieuckorean=12617;e.ssanghieuhkorean=12677;e.ssangieungkorean=12672;e.ssangkiyeokkorean=12594;e.ssangnieunkorean=12645;e.ssangpieupkorean=12611;e.ssangsioskorean=12614;e.ssangtikeutkorean=12600;e.ssuperior=63218;e.sterling=163;e.sterlingmonospace=65505;e.strokelongoverlaycmb=822;e.strokeshortoverlaycmb=821;e.subset=8834;e.subsetnotequal=8842;e.subsetorequal=8838;e.succeeds=8827;e.suchthat=8715;e.suhiragana=12377;e.sukatakana=12473;e.sukatakanahalfwidth=65405;e.sukunarabic=1618;e.summation=8721;e.sun=9788;e.superset=8835;e.supersetnotequal=8843;e.supersetorequal=8839;e.svsquare=13276;e.syouwaerasquare=13180;e.t=116;e.tabengali=2468;e.tackdown=8868;e.tackleft=8867;e.tadeva=2340;e.tagujarati=2724;e.tagurmukhi=2596;e.taharabic=1591;e.tahfinalarabic=65218;e.tahinitialarabic=65219;e.tahiragana=12383;e.tahmedialarabic=65220;e.taisyouerasquare=13181;e.takatakana=12479;e.takatakanahalfwidth=65408;e.tatweelarabic=1600;e.tau=964;e.tav=1514;e.tavdages=64330;e.tavdagesh=64330;e.tavdageshhebrew=64330;e.tavhebrew=1514;e.tbar=359;e.tbopomofo=12554;e.tcaron=357;e.tccurl=680;e.tcedilla=355;e.tcheharabic=1670;e.tchehfinalarabic=64379;e.tchehinitialarabic=64380;e.tchehmedialarabic=64381;e.tcircle=9443;e.tcircumflexbelow=7793;e.tcommaaccent=355;e.tdieresis=7831;e.tdotaccent=7787;e.tdotbelow=7789;e.tecyrillic=1090;e.tedescendercyrillic=1197;e.teharabic=1578;e.tehfinalarabic=65174;e.tehhahinitialarabic=64674;e.tehhahisolatedarabic=64524;e.tehinitialarabic=65175;e.tehiragana=12390;e.tehjeeminitialarabic=64673;e.tehjeemisolatedarabic=64523;e.tehmarbutaarabic=1577;e.tehmarbutafinalarabic=65172;e.tehmedialarabic=65176;e.tehmeeminitialarabic=64676;e.tehmeemisolatedarabic=64526;e.tehnoonfinalarabic=64627;e.tekatakana=12486;e.tekatakanahalfwidth=65411;e.telephone=8481;e.telephoneblack=9742;e.telishagedolahebrew=1440;e.telishaqetanahebrew=1449;e.tencircle=9321;e.tenideographicparen=12841;e.tenparen=9341;e.tenperiod=9361;e.tenroman=8569;e.tesh=679;e.tet=1496;e.tetdagesh=64312;e.tetdageshhebrew=64312;e.tethebrew=1496;e.tetsecyrillic=1205;e.tevirhebrew=1435;e.tevirlefthebrew=1435;e.thabengali=2469;e.thadeva=2341;e.thagujarati=2725;e.thagurmukhi=2597;e.thalarabic=1584;e.thalfinalarabic=65196;e.thanthakhatlowleftthai=63640;e.thanthakhatlowrightthai=63639;e.thanthakhatthai=3660;e.thanthakhatupperleftthai=63638;e.theharabic=1579;e.thehfinalarabic=65178;e.thehinitialarabic=65179;e.thehmedialarabic=65180;e.thereexists=8707;e.therefore=8756;e.theta=952;e.theta1=977;e.thetasymbolgreek=977;e.thieuthacirclekorean=12921;e.thieuthaparenkorean=12825;e.thieuthcirclekorean=12907;e.thieuthkorean=12620;e.thieuthparenkorean=12811;e.thirteencircle=9324;e.thirteenparen=9344;e.thirteenperiod=9364;e.thonangmonthothai=3601;e.thook=429;e.thophuthaothai=3602;e.thorn=254;e.thothahanthai=3607;e.thothanthai=3600;e.thothongthai=3608;e.thothungthai=3606;e.thousandcyrillic=1154;e.thousandsseparatorarabic=1644;e.thousandsseparatorpersian=1644;e.three=51;e.threearabic=1635;e.threebengali=2537;e.threecircle=9314;e.threecircleinversesansserif=10124;e.threedeva=2409;e.threeeighths=8540;e.threegujarati=2793;e.threegurmukhi=2665;e.threehackarabic=1635;e.threehangzhou=12323;e.threeideographicparen=12834;e.threeinferior=8323;e.threemonospace=65299;e.threenumeratorbengali=2550;e.threeoldstyle=63283;e.threeparen=9334;e.threeperiod=9354;e.threepersian=1779;e.threequarters=190;e.threequartersemdash=63198;e.threeroman=8562;e.threesuperior=179;e.threethai=3667;e.thzsquare=13204;e.tihiragana=12385;e.tikatakana=12481;e.tikatakanahalfwidth=65409;e.tikeutacirclekorean=12912;e.tikeutaparenkorean=12816;e.tikeutcirclekorean=12898;e.tikeutkorean=12599;e.tikeutparenkorean=12802;e.tilde=732;e.tildebelowcmb=816;e.tildecmb=771;e.tildecomb=771;e.tildedoublecmb=864;e.tildeoperator=8764;e.tildeoverlaycmb=820;e.tildeverticalcmb=830;e.timescircle=8855;e.tipehahebrew=1430;e.tipehalefthebrew=1430;e.tippigurmukhi=2672;e.titlocyrilliccmb=1155;e.tiwnarmenian=1407;e.tlinebelow=7791;e.tmonospace=65364;e.toarmenian=1385;e.tohiragana=12392;e.tokatakana=12488;e.tokatakanahalfwidth=65412;e.tonebarextrahighmod=741;e.tonebarextralowmod=745;e.tonebarhighmod=742;e.tonebarlowmod=744;e.tonebarmidmod=743;e.tonefive=445;e.tonesix=389;e.tonetwo=424;e.tonos=900;e.tonsquare=13095;e.topatakthai=3599;e.tortoiseshellbracketleft=12308;e.tortoiseshellbracketleftsmall=65117;e.tortoiseshellbracketleftvertical=65081;e.tortoiseshellbracketright=12309;e.tortoiseshellbracketrightsmall=65118;e.tortoiseshellbracketrightvertical=65082;e.totaothai=3605;e.tpalatalhook=427;e.tparen=9391;e.trademark=8482;e.trademarksans=63722;e.trademarkserif=63195;e.tretroflexhook=648;e.triagdn=9660;e.triaglf=9668;e.triagrt=9658;e.triagup=9650;e.ts=678;e.tsadi=1510;e.tsadidagesh=64326;e.tsadidageshhebrew=64326;e.tsadihebrew=1510;e.tsecyrillic=1094;e.tsere=1461;e.tsere12=1461;e.tsere1e=1461;e.tsere2b=1461;e.tserehebrew=1461;e.tserenarrowhebrew=1461;e.tserequarterhebrew=1461;e.tserewidehebrew=1461;e.tshecyrillic=1115;e.tsuperior=63219;e.ttabengali=2463;e.ttadeva=2335;e.ttagujarati=2719;e.ttagurmukhi=2591;e.tteharabic=1657;e.ttehfinalarabic=64359;e.ttehinitialarabic=64360;e.ttehmedialarabic=64361;e.tthabengali=2464;e.tthadeva=2336;e.tthagujarati=2720;e.tthagurmukhi=2592;e.tturned=647;e.tuhiragana=12388;e.tukatakana=12484;e.tukatakanahalfwidth=65410;e.tusmallhiragana=12387;e.tusmallkatakana=12483;e.tusmallkatakanahalfwidth=65391;e.twelvecircle=9323;e.twelveparen=9343;e.twelveperiod=9363;e.twelveroman=8571;e.twentycircle=9331;e.twentyhangzhou=21316;e.twentyparen=9351;e.twentyperiod=9371;e.two=50;e.twoarabic=1634;e.twobengali=2536;e.twocircle=9313;e.twocircleinversesansserif=10123;e.twodeva=2408;e.twodotenleader=8229;e.twodotleader=8229;e.twodotleadervertical=65072;e.twogujarati=2792;e.twogurmukhi=2664;e.twohackarabic=1634;e.twohangzhou=12322;e.twoideographicparen=12833;e.twoinferior=8322;e.twomonospace=65298;e.twonumeratorbengali=2549;e.twooldstyle=63282;e.twoparen=9333;e.twoperiod=9353;e.twopersian=1778;e.tworoman=8561;e.twostroke=443;e.twosuperior=178;e.twothai=3666;e.twothirds=8532;e.u=117;e.uacute=250;e.ubar=649;e.ubengali=2441;e.ubopomofo=12584;e.ubreve=365;e.ucaron=468;e.ucircle=9444;e.ucircumflex=251;e.ucircumflexbelow=7799;e.ucyrillic=1091;e.udattadeva=2385;e.udblacute=369;e.udblgrave=533;e.udeva=2313;e.udieresis=252;e.udieresisacute=472;e.udieresisbelow=7795;e.udieresiscaron=474;e.udieresiscyrillic=1265;e.udieresisgrave=476;e.udieresismacron=470;e.udotbelow=7909;e.ugrave=249;e.ugujarati=2697;e.ugurmukhi=2569;e.uhiragana=12358;e.uhookabove=7911;e.uhorn=432;e.uhornacute=7913;e.uhorndotbelow=7921;e.uhorngrave=7915;e.uhornhookabove=7917;e.uhorntilde=7919;e.uhungarumlaut=369;e.uhungarumlautcyrillic=1267;e.uinvertedbreve=535;e.ukatakana=12454;e.ukatakanahalfwidth=65395;e.ukcyrillic=1145;e.ukorean=12636;e.umacron=363;e.umacroncyrillic=1263;e.umacrondieresis=7803;e.umatragurmukhi=2625;e.umonospace=65365;e.underscore=95;e.underscoredbl=8215;e.underscoremonospace=65343;e.underscorevertical=65075;e.underscorewavy=65103;e.union=8746;e.universal=8704;e.uogonek=371;e.uparen=9392;e.upblock=9600;e.upperdothebrew=1476;e.upsilon=965;e.upsilondieresis=971;e.upsilondieresistonos=944;e.upsilonlatin=650;e.upsilontonos=973;e.uptackbelowcmb=797;e.uptackmod=724;e.uragurmukhi=2675;e.uring=367;e.ushortcyrillic=1118;e.usmallhiragana=12357;e.usmallkatakana=12453;e.usmallkatakanahalfwidth=65385;e.ustraightcyrillic=1199;e.ustraightstrokecyrillic=1201;e.utilde=361;e.utildeacute=7801;e.utildebelow=7797;e.uubengali=2442;e.uudeva=2314;e.uugujarati=2698;e.uugurmukhi=2570;e.uumatragurmukhi=2626;e.uuvowelsignbengali=2498;e.uuvowelsigndeva=2370;e.uuvowelsigngujarati=2754;e.uvowelsignbengali=2497;e.uvowelsigndeva=2369;e.uvowelsigngujarati=2753;e.v=118;e.vadeva=2357;e.vagujarati=2741;e.vagurmukhi=2613;e.vakatakana=12535;e.vav=1493;e.vavdagesh=64309;e.vavdagesh65=64309;e.vavdageshhebrew=64309;e.vavhebrew=1493;e.vavholam=64331;e.vavholamhebrew=64331;e.vavvavhebrew=1520;e.vavyodhebrew=1521;e.vcircle=9445;e.vdotbelow=7807;e.vecyrillic=1074;e.veharabic=1700;e.vehfinalarabic=64363;e.vehinitialarabic=64364;e.vehmedialarabic=64365;e.vekatakana=12537;e.venus=9792;e.verticalbar=124;e.verticallineabovecmb=781;e.verticallinebelowcmb=809;e.verticallinelowmod=716;e.verticallinemod=712;e.vewarmenian=1406;e.vhook=651;e.vikatakana=12536;e.viramabengali=2509;e.viramadeva=2381;e.viramagujarati=2765;e.visargabengali=2435;e.visargadeva=2307;e.visargagujarati=2691;e.vmonospace=65366;e.voarmenian=1400;e.voicediterationhiragana=12446;e.voicediterationkatakana=12542;e.voicedmarkkana=12443;e.voicedmarkkanahalfwidth=65438;e.vokatakana=12538;e.vparen=9393;e.vtilde=7805;e.vturned=652;e.vuhiragana=12436;e.vukatakana=12532;e.w=119;e.wacute=7811;e.waekorean=12633;e.wahiragana=12431;e.wakatakana=12527;e.wakatakanahalfwidth=65436;e.wakorean=12632;e.wasmallhiragana=12430;e.wasmallkatakana=12526;e.wattosquare=13143;e.wavedash=12316;e.wavyunderscorevertical=65076;e.wawarabic=1608;e.wawfinalarabic=65262;e.wawhamzaabovearabic=1572;e.wawhamzaabovefinalarabic=65158;e.wbsquare=13277;e.wcircle=9446;e.wcircumflex=373;e.wdieresis=7813;e.wdotaccent=7815;e.wdotbelow=7817;e.wehiragana=12433;e.weierstrass=8472;e.wekatakana=12529;e.wekorean=12638;e.weokorean=12637;e.wgrave=7809;e.whitebullet=9702;e.whitecircle=9675;e.whitecircleinverse=9689;e.whitecornerbracketleft=12302;e.whitecornerbracketleftvertical=65091;e.whitecornerbracketright=12303;e.whitecornerbracketrightvertical=65092;e.whitediamond=9671;e.whitediamondcontainingblacksmalldiamond=9672;e.whitedownpointingsmalltriangle=9663;e.whitedownpointingtriangle=9661;e.whiteleftpointingsmalltriangle=9667;e.whiteleftpointingtriangle=9665;e.whitelenticularbracketleft=12310;e.whitelenticularbracketright=12311;e.whiterightpointingsmalltriangle=9657;e.whiterightpointingtriangle=9655;e.whitesmallsquare=9643;e.whitesmilingface=9786;e.whitesquare=9633;e.whitestar=9734;e.whitetelephone=9743;e.whitetortoiseshellbracketleft=12312;e.whitetortoiseshellbracketright=12313;e.whiteuppointingsmalltriangle=9653;e.whiteuppointingtriangle=9651;e.wihiragana=12432;e.wikatakana=12528;e.wikorean=12639;e.wmonospace=65367;e.wohiragana=12434;e.wokatakana=12530;e.wokatakanahalfwidth=65382;e.won=8361;e.wonmonospace=65510;e.wowaenthai=3623;e.wparen=9394;e.wring=7832;e.wsuperior=695;e.wturned=653;e.wynn=447;e.x=120;e.xabovecmb=829;e.xbopomofo=12562;e.xcircle=9447;e.xdieresis=7821;e.xdotaccent=7819;e.xeharmenian=1389;e.xi=958;e.xmonospace=65368;e.xparen=9395;e.xsuperior=739;e.y=121;e.yaadosquare=13134;e.yabengali=2479;e.yacute=253;e.yadeva=2351;e.yaekorean=12626;e.yagujarati=2735;e.yagurmukhi=2607;e.yahiragana=12420;e.yakatakana=12516;e.yakatakanahalfwidth=65428;e.yakorean=12625;e.yamakkanthai=3662;e.yasmallhiragana=12419;e.yasmallkatakana=12515;e.yasmallkatakanahalfwidth=65388;e.yatcyrillic=1123;e.ycircle=9448;e.ycircumflex=375;e.ydieresis=255;e.ydotaccent=7823;e.ydotbelow=7925;e.yeharabic=1610;e.yehbarreearabic=1746;e.yehbarreefinalarabic=64431;e.yehfinalarabic=65266;e.yehhamzaabovearabic=1574;e.yehhamzaabovefinalarabic=65162;e.yehhamzaaboveinitialarabic=65163;e.yehhamzaabovemedialarabic=65164;e.yehinitialarabic=65267;e.yehmedialarabic=65268;e.yehmeeminitialarabic=64733;e.yehmeemisolatedarabic=64600;e.yehnoonfinalarabic=64660;e.yehthreedotsbelowarabic=1745;e.yekorean=12630;e.yen=165;e.yenmonospace=65509;e.yeokorean=12629;e.yeorinhieuhkorean=12678;e.yerahbenyomohebrew=1450;e.yerahbenyomolefthebrew=1450;e.yericyrillic=1099;e.yerudieresiscyrillic=1273;e.yesieungkorean=12673;e.yesieungpansioskorean=12675;e.yesieungsioskorean=12674;e.yetivhebrew=1434;e.ygrave=7923;e.yhook=436;e.yhookabove=7927;e.yiarmenian=1397;e.yicyrillic=1111;e.yikorean=12642;e.yinyang=9775;e.yiwnarmenian=1410;e.ymonospace=65369;e.yod=1497;e.yoddagesh=64313;e.yoddageshhebrew=64313;e.yodhebrew=1497;e.yodyodhebrew=1522;e.yodyodpatahhebrew=64287;e.yohiragana=12424;e.yoikorean=12681;e.yokatakana=12520;e.yokatakanahalfwidth=65430;e.yokorean=12635;e.yosmallhiragana=12423;e.yosmallkatakana=12519;e.yosmallkatakanahalfwidth=65390;e.yotgreek=1011;e.yoyaekorean=12680;e.yoyakorean=12679;e.yoyakthai=3618;e.yoyingthai=3597;e.yparen=9396;e.ypogegrammeni=890;e.ypogegrammenigreekcmb=837;e.yr=422;e.yring=7833;e.ysuperior=696;e.ytilde=7929;e.yturned=654;e.yuhiragana=12422;e.yuikorean=12684;e.yukatakana=12518;e.yukatakanahalfwidth=65429;e.yukorean=12640;e.yusbigcyrillic=1131;e.yusbigiotifiedcyrillic=1133;e.yuslittlecyrillic=1127;e.yuslittleiotifiedcyrillic=1129;e.yusmallhiragana=12421;e.yusmallkatakana=12517;e.yusmallkatakanahalfwidth=65389;e.yuyekorean=12683;e.yuyeokorean=12682;e.yyabengali=2527;e.yyadeva=2399;e.z=122;e.zaarmenian=1382;e.zacute=378;e.zadeva=2395;e.zagurmukhi=2651;e.zaharabic=1592;e.zahfinalarabic=65222;e.zahinitialarabic=65223;e.zahiragana=12374;e.zahmedialarabic=65224;e.zainarabic=1586;e.zainfinalarabic=65200;e.zakatakana=12470;e.zaqefgadolhebrew=1429;e.zaqefqatanhebrew=1428;e.zarqahebrew=1432;e.zayin=1494;e.zayindagesh=64310;e.zayindageshhebrew=64310;e.zayinhebrew=1494;e.zbopomofo=12567;e.zcaron=382;e.zcircle=9449;e.zcircumflex=7825;e.zcurl=657;e.zdot=380;e.zdotaccent=380;e.zdotbelow=7827;e.zecyrillic=1079;e.zedescendercyrillic=1177;e.zedieresiscyrillic=1247;e.zehiragana=12380;e.zekatakana=12476;e.zero=48;e.zeroarabic=1632;e.zerobengali=2534;e.zerodeva=2406;e.zerogujarati=2790;e.zerogurmukhi=2662;e.zerohackarabic=1632;e.zeroinferior=8320;e.zeromonospace=65296;e.zerooldstyle=63280;e.zeropersian=1776;e.zerosuperior=8304;e.zerothai=3664;e.zerowidthjoiner=65279;e.zerowidthnonjoiner=8204;e.zerowidthspace=8203;e.zeta=950;e.zhbopomofo=12563;e.zhearmenian=1386;e.zhebrevecyrillic=1218;e.zhecyrillic=1078;e.zhedescendercyrillic=1175;e.zhedieresiscyrillic=1245;e.zihiragana=12376;e.zikatakana=12472;e.zinorhebrew=1454;e.zlinebelow=7829;e.zmonospace=65370;e.zohiragana=12382;e.zokatakana=12478;e.zparen=9397;e.zretroflexhook=656;e.zstroke=438;e.zuhiragana=12378;e.zukatakana=12474;e[".notdef"]=0;e.angbracketleftbig=9001;e.angbracketleftBig=9001;e.angbracketleftbigg=9001;e.angbracketleftBigg=9001;e.angbracketrightBig=9002;e.angbracketrightbig=9002;e.angbracketrightBigg=9002;e.angbracketrightbigg=9002;e.arrowhookleft=8618;e.arrowhookright=8617;e.arrowlefttophalf=8636;e.arrowleftbothalf=8637;e.arrownortheast=8599;e.arrownorthwest=8598;e.arrowrighttophalf=8640;e.arrowrightbothalf=8641;e.arrowsoutheast=8600;e.arrowsouthwest=8601;e.backslashbig=8726;e.backslashBig=8726;e.backslashBigg=8726;e.backslashbigg=8726;e.bardbl=8214;e.bracehtipdownleft=65079;e.bracehtipdownright=65079;e.bracehtipupleft=65080;e.bracehtipupright=65080;e.braceleftBig=123;e.braceleftbig=123;e.braceleftbigg=123;e.braceleftBigg=123;e.bracerightBig=125;e.bracerightbig=125;e.bracerightbigg=125;e.bracerightBigg=125;e.bracketleftbig=91;e.bracketleftBig=91;e.bracketleftbigg=91;e.bracketleftBigg=91;e.bracketrightBig=93;e.bracketrightbig=93;e.bracketrightbigg=93;e.bracketrightBigg=93;e.ceilingleftbig=8968;e.ceilingleftBig=8968;e.ceilingleftBigg=8968;e.ceilingleftbigg=8968;e.ceilingrightbig=8969;e.ceilingrightBig=8969;e.ceilingrightbigg=8969;e.ceilingrightBigg=8969;e.circledotdisplay=8857;e.circledottext=8857;e.circlemultiplydisplay=8855;e.circlemultiplytext=8855;e.circleplusdisplay=8853;e.circleplustext=8853;e.contintegraldisplay=8750;e.contintegraltext=8750;e.coproductdisplay=8720;e.coproducttext=8720;e.floorleftBig=8970;e.floorleftbig=8970;e.floorleftbigg=8970;e.floorleftBigg=8970;e.floorrightbig=8971;e.floorrightBig=8971;e.floorrightBigg=8971;e.floorrightbigg=8971;e.hatwide=770;e.hatwider=770;e.hatwidest=770;e.intercal=7488;e.integraldisplay=8747;e.integraltext=8747;e.intersectiondisplay=8898;e.intersectiontext=8898;e.logicalanddisplay=8743;e.logicalandtext=8743;e.logicalordisplay=8744;e.logicalortext=8744;e.parenleftBig=40;e.parenleftbig=40;e.parenleftBigg=40;e.parenleftbigg=40;e.parenrightBig=41;e.parenrightbig=41;e.parenrightBigg=41;e.parenrightbigg=41;e.prime=8242;e.productdisplay=8719;e.producttext=8719;e.radicalbig=8730;e.radicalBig=8730;e.radicalBigg=8730;e.radicalbigg=8730;e.radicalbt=8730;e.radicaltp=8730;e.radicalvertex=8730;e.slashbig=47;e.slashBig=47;e.slashBigg=47;e.slashbigg=47;e.summationdisplay=8721;e.summationtext=8721;e.tildewide=732;e.tildewider=732;e.tildewidest=732;e.uniondisplay=8899;e.unionmultidisplay=8846;e.unionmultitext=8846;e.unionsqdisplay=8852;e.unionsqtext=8852;e.uniontext=8899;e.vextenddouble=8741;e.vextendsingle=8739})),Ir=getLookupTableFactory((function(e){e.space=32;e.a1=9985;e.a2=9986;e.a202=9987;e.a3=9988;e.a4=9742;e.a5=9990;e.a119=9991;e.a118=9992;e.a117=9993;e.a11=9755;e.a12=9758;e.a13=9996;e.a14=9997;e.a15=9998;e.a16=9999;e.a105=1e4;e.a17=10001;e.a18=10002;e.a19=10003;e.a20=10004;e.a21=10005;e.a22=10006;e.a23=10007;e.a24=10008;e.a25=10009;e.a26=10010;e.a27=10011;e.a28=10012;e.a6=10013;e.a7=10014;e.a8=10015;e.a9=10016;e.a10=10017;e.a29=10018;e.a30=10019;e.a31=10020;e.a32=10021;e.a33=10022;e.a34=10023;e.a35=9733;e.a36=10025;e.a37=10026;e.a38=10027;e.a39=10028;e.a40=10029;e.a41=10030;e.a42=10031;e.a43=10032;e.a44=10033;e.a45=10034;e.a46=10035;e.a47=10036;e.a48=10037;e.a49=10038;e.a50=10039;e.a51=10040;e.a52=10041;e.a53=10042;e.a54=10043;e.a55=10044;e.a56=10045;e.a57=10046;e.a58=10047;e.a59=10048;e.a60=10049;e.a61=10050;e.a62=10051;e.a63=10052;e.a64=10053;e.a65=10054;e.a66=10055;e.a67=10056;e.a68=10057;e.a69=10058;e.a70=10059;e.a71=9679;e.a72=10061;e.a73=9632;e.a74=10063;e.a203=10064;e.a75=10065;e.a204=10066;e.a76=9650;e.a77=9660;e.a78=9670;e.a79=10070;e.a81=9687;e.a82=10072;e.a83=10073;e.a84=10074;e.a97=10075;e.a98=10076;e.a99=10077;e.a100=10078;e.a101=10081;e.a102=10082;e.a103=10083;e.a104=10084;e.a106=10085;e.a107=10086;e.a108=10087;e.a112=9827;e.a111=9830;e.a110=9829;e.a109=9824;e.a120=9312;e.a121=9313;e.a122=9314;e.a123=9315;e.a124=9316;e.a125=9317;e.a126=9318;e.a127=9319;e.a128=9320;e.a129=9321;e.a130=10102;e.a131=10103;e.a132=10104;e.a133=10105;e.a134=10106;e.a135=10107;e.a136=10108;e.a137=10109;e.a138=10110;e.a139=10111;e.a140=10112;e.a141=10113;e.a142=10114;e.a143=10115;e.a144=10116;e.a145=10117;e.a146=10118;e.a147=10119;e.a148=10120;e.a149=10121;e.a150=10122;e.a151=10123;e.a152=10124;e.a153=10125;e.a154=10126;e.a155=10127;e.a156=10128;e.a157=10129;e.a158=10130;e.a159=10131;e.a160=10132;e.a161=8594;e.a163=8596;e.a164=8597;e.a196=10136;e.a165=10137;e.a192=10138;e.a166=10139;e.a167=10140;e.a168=10141;e.a169=10142;e.a170=10143;e.a171=10144;e.a172=10145;e.a173=10146;e.a162=10147;e.a174=10148;e.a175=10149;e.a176=10150;e.a177=10151;e.a178=10152;e.a179=10153;e.a193=10154;e.a180=10155;e.a199=10156;e.a181=10157;e.a200=10158;e.a182=10159;e.a201=10161;e.a183=10162;e.a184=10163;e.a197=10164;e.a185=10165;e.a194=10166;e.a198=10167;e.a186=10168;e.a195=10169;e.a187=10170;e.a188=10171;e.a189=10172;e.a190=10173;e.a191=10174;e.a89=10088;e.a90=10089;e.a93=10090;e.a94=10091;e.a91=10092;e.a92=10093;e.a205=10094;e.a85=10095;e.a206=10096;e.a86=10097;e.a87=10098;e.a88=10099;e.a95=10100;e.a96=10101;e[".notdef"]=0})),Or=getLookupTableFactory((function(e){e[63721]=169;e[63193]=169;e[63720]=174;e[63194]=174;e[63722]=8482;e[63195]=8482;e[63729]=9127;e[63730]=9128;e[63731]=9129;e[63740]=9131;e[63741]=9132;e[63742]=9133;e[63726]=9121;e[63727]=9122;e[63728]=9123;e[63737]=9124;e[63738]=9125;e[63739]=9126;e[63723]=9115;e[63724]=9116;e[63725]=9117;e[63734]=9118;e[63735]=9119;e[63736]=9120}));function getUnicodeForGlyph(e,t){let a=t[e];if(void 0!==a)return a;if(!e)return-1;if("u"===e[0]){const t=e.length;let r;if(7===t&&"n"===e[1]&&"i"===e[2])r=e.substring(3);else{if(!(t>=5&&t<=7))return-1;r=e.substring(1)}if(r===r.toUpperCase()){a=parseInt(r,16);if(a>=0)return a}}return-1}const Tr=[[0,127],[128,255],[256,383],[384,591],[592,687,7424,7551,7552,7615],[688,767,42752,42783],[768,879,7616,7679],[880,1023],[11392,11519],[1024,1279,1280,1327,11744,11775,42560,42655],[1328,1423],[1424,1535],[42240,42559],[1536,1791,1872,1919],[1984,2047],[2304,2431],[2432,2559],[2560,2687],[2688,2815],[2816,2943],[2944,3071],[3072,3199],[3200,3327],[3328,3455],[3584,3711],[3712,3839],[4256,4351,11520,11567],[6912,7039],[4352,4607],[7680,7935,11360,11391,42784,43007],[7936,8191],[8192,8303,11776,11903],[8304,8351],[8352,8399],[8400,8447],[8448,8527],[8528,8591],[8592,8703,10224,10239,10496,10623,11008,11263],[8704,8959,10752,11007,10176,10223,10624,10751],[8960,9215],[9216,9279],[9280,9311],[9312,9471],[9472,9599],[9600,9631],[9632,9727],[9728,9983],[9984,10175],[12288,12351],[12352,12447],[12448,12543,12784,12799],[12544,12591,12704,12735],[12592,12687],[43072,43135],[12800,13055],[13056,13311],[44032,55215],[55296,57343],[67840,67871],[19968,40959,11904,12031,12032,12255,12272,12287,13312,19903,131072,173791,12688,12703],[57344,63743],[12736,12783,63744,64255,194560,195103],[64256,64335],[64336,65023],[65056,65071],[65040,65055],[65104,65135],[65136,65279],[65280,65519],[65520,65535],[3840,4095],[1792,1871],[1920,1983],[3456,3583],[4096,4255],[4608,4991,4992,5023,11648,11743],[5024,5119],[5120,5759],[5760,5791],[5792,5887],[6016,6143],[6144,6319],[10240,10495],[40960,42127],[5888,5919,5920,5951,5952,5983,5984,6015],[66304,66351],[66352,66383],[66560,66639],[118784,119039,119040,119295,119296,119375],[119808,120831],[1044480,1048573],[65024,65039,917760,917999],[917504,917631],[6400,6479],[6480,6527],[6528,6623],[6656,6687],[11264,11359],[11568,11647],[19904,19967],[43008,43055],[65536,65663,65664,65791,65792,65855],[65856,65935],[66432,66463],[66464,66527],[66640,66687],[66688,66735],[67584,67647],[68096,68191],[119552,119647],[73728,74751,74752,74879],[119648,119679],[7040,7103],[7168,7247],[7248,7295],[43136,43231],[43264,43311],[43312,43359],[43520,43615],[65936,65999],[66e3,66047],[66208,66271,66176,66207,67872,67903],[127024,127135,126976,127023]];function getUnicodeRangeFor(e,t=-1){if(-1!==t){const a=Tr[t];for(let r=0,i=a.length;r=a[r]&&e<=a[r+1])return t}for(let t=0,a=Tr.length;t=a[r]&&e<=a[r+1])return t}return-1}const Mr=new RegExp("^(\\s)|(\\p{Mn})|(\\p{Cf})$","u"),Dr=new Map;const Rr=!0,Nr=1,Er=2,Pr=4,Lr=32,jr=[".notdef",".null","nonmarkingreturn","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","nonbreakingspace","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron","Lslash","lslash","Scaron","scaron","Zcaron","zcaron","brokenbar","Eth","eth","Yacute","yacute","Thorn","thorn","minus","multiply","onesuperior","twosuperior","threesuperior","onehalf","onequarter","threequarters","franc","Gbreve","gbreve","Idotaccent","Scedilla","scedilla","Cacute","cacute","Ccaron","ccaron","dcroat"];function recoverGlyphName(e,t){if(void 0!==t[e])return e;const a=getUnicodeForGlyph(e,t);if(-1!==a)for(const e in t)if(t[e]===a)return e;info("Unable to recover a standard glyph name for: "+e);return e}function type1FontGlyphMapping(e,t,a){const r=Object.create(null);let i,n,s;const o=!!(e.flags&Pr);if(e.isInternalFont){s=t;for(n=0;n=0?i:0}}else if(e.baseEncodingName){s=getEncoding(e.baseEncodingName);for(n=0;n=0?i:0}}else if(o)for(n in t)r[n]=t[n];else{s=Ar;for(n=0;n=0?i:0}}const c=e.differences;let l;if(c)for(n in c){const e=c[n];i=a.indexOf(e);if(-1===i){l||(l=Fr());const t=recoverGlyphName(e,l);t!==e&&(i=a.indexOf(t))}r[n]=i>=0?i:0}return r}function normalizeFontName(e){return e.replaceAll(/[,_]/g,"-").replaceAll(/\s/g,"")}const _r=getLookupTableFactory((e=>{e[8211]=65074;e[8212]=65073;e[8229]=65072;e[8230]=65049;e[12289]=65041;e[12290]=65042;e[12296]=65087;e[12297]=65088;e[12298]=65085;e[12299]=65086;e[12300]=65089;e[12301]=65090;e[12302]=65091;e[12303]=65092;e[12304]=65083;e[12305]=65084;e[12308]=65081;e[12309]=65082;e[12310]=65047;e[12311]=65048;e[65103]=65076;e[65281]=65045;e[65288]=65077;e[65289]=65078;e[65292]=65040;e[65306]=65043;e[65307]=65044;e[65311]=65046;e[65339]=65095;e[65341]=65096;e[65343]=65075;e[65371]=65079;e[65373]=65080}));const Ur=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron"],Xr=[".notdef","space","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],qr=[".notdef","space","dollaroldstyle","dollarsuperior","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","hyphensuperior","colonmonetary","onefitted","rupiah","centoldstyle","figuredash","hypheninferior","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior"],Hr=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall","001.000","001.001","001.002","001.003","Black","Bold","Book","Light","Medium","Regular","Roman","Semibold"],Wr=391,zr=[null,{id:"hstem",min:2,stackClearing:!0,stem:!0},null,{id:"vstem",min:2,stackClearing:!0,stem:!0},{id:"vmoveto",min:1,stackClearing:!0},{id:"rlineto",min:2,resetStack:!0},{id:"hlineto",min:1,resetStack:!0},{id:"vlineto",min:1,resetStack:!0},{id:"rrcurveto",min:6,resetStack:!0},null,{id:"callsubr",min:1,undefStack:!0},{id:"return",min:0,undefStack:!0},null,null,{id:"endchar",min:0,stackClearing:!0},null,null,null,{id:"hstemhm",min:2,stackClearing:!0,stem:!0},{id:"hintmask",min:0,stackClearing:!0},{id:"cntrmask",min:0,stackClearing:!0},{id:"rmoveto",min:2,stackClearing:!0},{id:"hmoveto",min:1,stackClearing:!0},{id:"vstemhm",min:2,stackClearing:!0,stem:!0},{id:"rcurveline",min:8,resetStack:!0},{id:"rlinecurve",min:8,resetStack:!0},{id:"vvcurveto",min:4,resetStack:!0},{id:"hhcurveto",min:4,resetStack:!0},null,{id:"callgsubr",min:1,undefStack:!0},{id:"vhcurveto",min:4,resetStack:!0},{id:"hvcurveto",min:4,resetStack:!0}],$r=[null,null,null,{id:"and",min:2,stackDelta:-1},{id:"or",min:2,stackDelta:-1},{id:"not",min:1,stackDelta:0},null,null,null,{id:"abs",min:1,stackDelta:0},{id:"add",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]+e[t-1]}},{id:"sub",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]-e[t-1]}},{id:"div",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]/e[t-1]}},null,{id:"neg",min:1,stackDelta:0,stackFn(e,t){e[t-1]=-e[t-1]}},{id:"eq",min:2,stackDelta:-1},null,null,{id:"drop",min:1,stackDelta:-1},null,{id:"put",min:2,stackDelta:-2},{id:"get",min:1,stackDelta:0},{id:"ifelse",min:4,stackDelta:-3},{id:"random",min:0,stackDelta:1},{id:"mul",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]*e[t-1]}},null,{id:"sqrt",min:1,stackDelta:0},{id:"dup",min:1,stackDelta:1},{id:"exch",min:2,stackDelta:0},{id:"index",min:2,stackDelta:0},{id:"roll",min:3,stackDelta:-2},null,null,null,{id:"hflex",min:7,resetStack:!0},{id:"flex",min:13,resetStack:!0},{id:"hflex1",min:9,resetStack:!0},{id:"flex1",min:11,resetStack:!0}];class CFFParser{constructor(e,t,a){this.bytes=e.getBytes();this.properties=t;this.seacAnalysisEnabled=!!a}parse(){const e=this.properties,t=new CFF;this.cff=t;const a=this.parseHeader(),r=this.parseIndex(a.endPos),i=this.parseIndex(r.endPos),n=this.parseIndex(i.endPos),s=this.parseIndex(n.endPos),o=this.parseDict(i.obj.get(0)),c=this.createDict(CFFTopDict,o,t.strings);t.header=a.obj;t.names=this.parseNameIndex(r.obj);t.strings=this.parseStringIndex(n.obj);t.topDict=c;t.globalSubrIndex=s.obj;this.parsePrivateDict(t.topDict);t.isCIDFont=c.hasName("ROS");const l=c.getByName("CharStrings"),h=this.parseIndex(l).obj,u=c.getByName("FontMatrix");u&&(e.fontMatrix=u);const d=c.getByName("FontBBox");if(d){e.ascent=Math.max(d[3],d[1]);e.descent=Math.min(d[1],d[3]);e.ascentScaled=!0}let f,g;if(t.isCIDFont){const e=this.parseIndex(c.getByName("FDArray")).obj;for(let a=0,r=e.count;a=t)throw new FormatError("Invalid CFF header");if(0!==a){info("cff data is shifted");e=e.subarray(a);this.bytes=e}const r=e[0],i=e[1],n=e[2],s=e[3];return{obj:new CFFHeader(r,i,n,s),endPos:n}}parseDict(e){let t=0;function parseOperand(){let a=e[t++];if(30===a)return function parseFloatOperand(){let a="";const r=15,i=["0","1","2","3","4","5","6","7","8","9",".","E","E-",null,"-"],n=e.length;for(;t>4,o=15&n;if(s===r)break;a+=i[s];if(o===r)break;a+=i[o]}return parseFloat(a)}();if(28===a){a=readInt16(e,t);t+=2;return a}if(29===a){a=e[t++];a=a<<8|e[t++];a=a<<8|e[t++];a=a<<8|e[t++];return a}if(a>=32&&a<=246)return a-139;if(a>=247&&a<=250)return 256*(a-247)+e[t++]+108;if(a>=251&&a<=254)return-256*(a-251)-e[t++]-108;warn('CFFParser_parseDict: "'+a+'" is a reserved command.');return NaN}let a=[];const r=[];t=0;const i=e.length;for(;t10)return!1;let i=e.stackSize;const n=e.stack;let s=t.length;for(let o=0;o=4){i-=4;if(this.seacAnalysisEnabled){e.seac=n.slice(i,i+4);return!1}}l=zr[c]}else if(c>=32&&c<=246){n[i]=c-139;i++}else if(c>=247&&c<=254){n[i]=c<251?(c-247<<8)+t[o]+108:-(c-251<<8)-t[o]-108;o++;i++}else if(255===c){n[i]=(t[o]<<24|t[o+1]<<16|t[o+2]<<8|t[o+3])/65536;o+=4;i++}else if(19===c||20===c){e.hints+=i>>1;if(0===e.hints){t.copyWithin(o-1,o,-1);o-=1;s-=1;continue}o+=e.hints+7>>3;i%=2;l=zr[c]}else{if(10===c||29===c){const t=10===c?a:r;if(!t){l=zr[c];warn("Missing subrsIndex for "+l.id);return!1}let s=32768;t.count<1240?s=107:t.count<33900&&(s=1131);const o=n[--i]+s;if(o<0||o>=t.count||isNaN(o)){l=zr[c];warn("Out of bounds subrIndex for "+l.id);return!1}e.stackSize=i;e.callDepth++;if(!this.parseCharString(e,t.get(o),a,r))return!1;e.callDepth--;i=e.stackSize;continue}if(11===c){e.stackSize=i;return!0}if(0===c&&o===t.length){t[o-1]=14;l=zr[14]}else{if(9===c){t.copyWithin(o-1,o,-1);o-=1;s-=1;continue}l=zr[c]}}if(l){if(l.stem){e.hints+=i>>1;if(3===c||23===c)e.hasVStems=!0;else if(e.hasVStems&&(1===c||18===c)){warn("CFF stem hints are in wrong order");t[o-1]=1===c?3:23}}if("min"in l&&!e.undefStack&&i=2&&l.stem?i%=2:i>1&&warn("Found too many parameters for stack-clearing command");i>0&&(e.width=n[i-1])}if("stackDelta"in l){"stackFn"in l&&l.stackFn(n,i);i+=l.stackDelta}else if(l.stackClearing)i=0;else if(l.resetStack){i=0;e.undefStack=!1}else if(l.undefStack){i=0;e.undefStack=!0;e.firstStackClearing=!1}}}s=i.length){warn("Invalid fd index for glyph index.");u=!1}if(u){f=i[e].privateDict;d=f.subrsIndex}}else t&&(d=t);u&&(u=this.parseCharString(h,c,d,a));if(null!==h.width){const e=f.getByName("nominalWidthX");o[l]=e+h.width}else{const e=f.getByName("defaultWidthX");o[l]=e}null!==h.seac&&(s[l]=h.seac);u||e.set(l,new Uint8Array([14]))}return{charStrings:e,seacs:s,widths:o}}emptyPrivateDictionary(e){const t=this.createDict(CFFPrivateDict,[],e.strings);e.setByKey(18,[0,0]);e.privateDict=t}parsePrivateDict(e){if(!e.hasName("Private")){this.emptyPrivateDictionary(e);return}const t=e.getByName("Private");if(!Array.isArray(t)||2!==t.length){e.removeByName("Private");return}const a=t[0],r=t[1];if(0===a||r>=this.bytes.length){this.emptyPrivateDictionary(e);return}const i=r+a,n=this.bytes.subarray(r,i),s=this.parseDict(n),o=this.createDict(CFFPrivateDict,s,e.strings);e.privateDict=o;0===o.getByName("ExpansionFactor")&&o.setByName("ExpansionFactor",.06);if(!o.getByName("Subrs"))return;const c=o.getByName("Subrs"),l=r+c;if(0===c||l>=this.bytes.length){this.emptyPrivateDictionary(e);return}const h=this.parseIndex(l);o.subrsIndex=h.obj}parseCharsets(e,t,a,r){if(0===e)return new CFFCharset(!0,Kr.ISO_ADOBE,Ur);if(1===e)return new CFFCharset(!0,Kr.EXPERT,Xr);if(2===e)return new CFFCharset(!0,Kr.EXPERT_SUBSET,qr);const i=this.bytes,n=e,s=i[e++],o=[r?0:".notdef"];let c,l,h;t-=1;switch(s){case 0:for(h=0;h=65535){warn("Not enough space in charstrings to duplicate first glyph.");return}const e=this.charStrings.get(0);this.charStrings.add(e);this.isCIDFont&&this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0])}hasGlyphId(e){if(e<0||e>=this.charStrings.count)return!1;return this.charStrings.get(e).length>0}}class CFFHeader{constructor(e,t,a,r){this.major=e;this.minor=t;this.hdrSize=a;this.offSize=r}}class CFFStrings{constructor(){this.strings=[]}get(e){return e>=0&&e<=390?Hr[e]:e-Wr<=this.strings.length?this.strings[e-Wr]:Hr[0]}getSID(e){let t=Hr.indexOf(e);if(-1!==t)return t;t=this.strings.indexOf(e);return-1!==t?t+Wr:-1}add(e){this.strings.push(e)}get count(){return this.strings.length}}class CFFIndex{constructor(){this.objects=[];this.length=0}add(e){this.length+=e.length;this.objects.push(e)}set(e,t){this.length+=t.length-this.objects[e].length;this.objects[e]=t}get(e){return this.objects[e]}get count(){return this.objects.length}}class CFFDict{constructor(e,t){this.keyToNameMap=e.keyToNameMap;this.nameToKeyMap=e.nameToKeyMap;this.defaults=e.defaults;this.types=e.types;this.opcodes=e.opcodes;this.order=e.order;this.strings=t;this.values=Object.create(null)}setByKey(e,t){if(!(e in this.keyToNameMap))return!1;if(0===t.length)return!0;for(const a of t)if(isNaN(a)){warn(`Invalid CFFDict value: "${t}" for key "${e}".`);return!0}const a=this.types[e];"num"!==a&&"sid"!==a&&"offset"!==a||(t=t[0]);this.values[e]=t;return!0}setByName(e,t){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name "${e}"`);this.values[this.nameToKeyMap[e]]=t}hasName(e){return this.nameToKeyMap[e]in this.values}getByName(e){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name ${e}"`);const t=this.nameToKeyMap[e];return t in this.values?this.values[t]:this.defaults[t]}removeByName(e){delete this.values[this.nameToKeyMap[e]]}static createTables(e){const t={keyToNameMap:{},nameToKeyMap:{},defaults:{},types:{},opcodes:{},order:[]};for(const a of e){const e=Array.isArray(a[0])?(a[0][0]<<8)+a[0][1]:a[0];t.keyToNameMap[e]=a[1];t.nameToKeyMap[a[1]]=e;t.types[e]=a[2];t.defaults[e]=a[3];t.opcodes[e]=Array.isArray(a[0])?a[0]:[a[0]];t.order.push(e)}return t}}const Gr=[[[12,30],"ROS",["sid","sid","num"],null],[[12,20],"SyntheticBase","num",null],[0,"version","sid",null],[1,"Notice","sid",null],[[12,0],"Copyright","sid",null],[2,"FullName","sid",null],[3,"FamilyName","sid",null],[4,"Weight","sid",null],[[12,1],"isFixedPitch","num",0],[[12,2],"ItalicAngle","num",0],[[12,3],"UnderlinePosition","num",-100],[[12,4],"UnderlineThickness","num",50],[[12,5],"PaintType","num",0],[[12,6],"CharstringType","num",2],[[12,7],"FontMatrix",["num","num","num","num","num","num"],[.001,0,0,.001,0,0]],[13,"UniqueID","num",null],[5,"FontBBox",["num","num","num","num"],[0,0,0,0]],[[12,8],"StrokeWidth","num",0],[14,"XUID","array",null],[15,"charset","offset",0],[16,"Encoding","offset",0],[17,"CharStrings","offset",0],[18,"Private",["offset","offset"],null],[[12,21],"PostScript","sid",null],[[12,22],"BaseFontName","sid",null],[[12,23],"BaseFontBlend","delta",null],[[12,31],"CIDFontVersion","num",0],[[12,32],"CIDFontRevision","num",0],[[12,33],"CIDFontType","num",0],[[12,34],"CIDCount","num",8720],[[12,35],"UIDBase","num",null],[[12,37],"FDSelect","offset",null],[[12,36],"FDArray","offset",null],[[12,38],"FontName","sid",null]];class CFFTopDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(Gr))}constructor(e){super(CFFTopDict.tables,e);this.privateDict=null}}const Vr=[[6,"BlueValues","delta",null],[7,"OtherBlues","delta",null],[8,"FamilyBlues","delta",null],[9,"FamilyOtherBlues","delta",null],[[12,9],"BlueScale","num",.039625],[[12,10],"BlueShift","num",7],[[12,11],"BlueFuzz","num",1],[10,"StdHW","num",null],[11,"StdVW","num",null],[[12,12],"StemSnapH","delta",null],[[12,13],"StemSnapV","delta",null],[[12,14],"ForceBold","num",0],[[12,17],"LanguageGroup","num",0],[[12,18],"ExpansionFactor","num",.06],[[12,19],"initialRandomSeed","num",0],[20,"defaultWidthX","num",0],[21,"nominalWidthX","num",0],[19,"Subrs","offset",null]];class CFFPrivateDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(Vr))}constructor(e){super(CFFPrivateDict.tables,e);this.subrsIndex=null}}const Kr={ISO_ADOBE:0,EXPERT:1,EXPERT_SUBSET:2};class CFFCharset{constructor(e,t,a,r){this.predefined=e;this.format=t;this.charset=a;this.raw=r}}class CFFEncoding{constructor(e,t,a,r){this.predefined=e;this.format=t;this.encoding=a;this.raw=r}}class CFFFDSelect{constructor(e,t){this.format=e;this.fdSelect=t}getFDIndex(e){return e<0||e>=this.fdSelect.length?-1:this.fdSelect[e]}}class CFFOffsetTracker{constructor(){this.offsets=Object.create(null)}isTracking(e){return e in this.offsets}track(e,t){if(e in this.offsets)throw new FormatError(`Already tracking location of ${e}`);this.offsets[e]=t}offset(e){for(const t in this.offsets)this.offsets[t]+=e}setEntryLocation(e,t,a){if(!(e in this.offsets))throw new FormatError(`Not tracking location of ${e}`);const r=a.data,i=this.offsets[e];for(let e=0,a=t.length;e>24&255;r[s]=l>>16&255;r[o]=l>>8&255;r[c]=255&l}}}class CFFCompiler{constructor(e){this.cff=e}compile(){const e=this.cff,t={data:[],length:0,add(e){try{this.data.push(...e)}catch{this.data=this.data.concat(e)}this.length=this.data.length}},a=this.compileHeader(e.header);t.add(a);const r=this.compileNameIndex(e.names);t.add(r);if(e.isCIDFont&&e.topDict.hasName("FontMatrix")){const t=e.topDict.getByName("FontMatrix");e.topDict.removeByName("FontMatrix");for(const a of e.fdArray){let e=t.slice(0);a.hasName("FontMatrix")&&(e=Util.transform(e,a.getByName("FontMatrix")));a.setByName("FontMatrix",e)}}const i=e.topDict.getByName("XUID");i?.length>16&&e.topDict.removeByName("XUID");e.topDict.setByName("charset",0);let n=this.compileTopDicts([e.topDict],t.length,e.isCIDFont);t.add(n.output);const s=n.trackers[0],o=this.compileStringIndex(e.strings.strings);t.add(o);const c=this.compileIndex(e.globalSubrIndex);t.add(c);if(e.encoding&&e.topDict.hasName("Encoding"))if(e.encoding.predefined)s.setEntryLocation("Encoding",[e.encoding.format],t);else{const a=this.compileEncoding(e.encoding);s.setEntryLocation("Encoding",[t.length],t);t.add(a)}const l=this.compileCharset(e.charset,e.charStrings.count,e.strings,e.isCIDFont);s.setEntryLocation("charset",[t.length],t);t.add(l);const h=this.compileCharStrings(e.charStrings);s.setEntryLocation("CharStrings",[t.length],t);t.add(h);if(e.isCIDFont){s.setEntryLocation("FDSelect",[t.length],t);const a=this.compileFDSelect(e.fdSelect);t.add(a);n=this.compileTopDicts(e.fdArray,t.length,!0);s.setEntryLocation("FDArray",[t.length],t);t.add(n.output);const r=n.trackers;this.compilePrivateDicts(e.fdArray,r,t)}this.compilePrivateDicts([e.topDict],[s],t);t.add([0]);return t.data}encodeNumber(e){return Number.isInteger(e)?this.encodeInteger(e):this.encodeFloat(e)}static get EncodeFloatRegExp(){return shadow(this,"EncodeFloatRegExp",/\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/)}encodeFloat(e){let t=e.toString();const a=CFFCompiler.EncodeFloatRegExp.exec(t);if(a){const r=parseFloat("1e"+((a[2]?+a[2]:0)+a[1].length));t=(Math.round(e*r)/r).toString()}let r,i,n="";for(r=0,i=t.length;r=-107&&e<=107?[e+139]:e>=108&&e<=1131?[247+((e-=108)>>8),255&e]:e>=-1131&&e<=-108?[251+((e=-e-108)>>8),255&e]:e>=-32768&&e<=32767?[28,e>>8&255,255&e]:[29,e>>24&255,e>>16&255,e>>8&255,255&e];return t}compileHeader(e){return[e.major,e.minor,4,e.offSize]}compileNameIndex(e){const t=new CFFIndex;for(const a of e){const e=Math.min(a.length,127);let r=new Array(e);for(let t=0;t"~"||"["===e||"]"===e||"("===e||")"===e||"{"===e||"}"===e||"<"===e||">"===e||"/"===e||"%"===e)&&(e="_");r[t]=e}r=r.join("");""===r&&(r="Bad_Font_Name");t.add(stringToBytes(r))}return this.compileIndex(t)}compileTopDicts(e,t,a){const r=[];let i=new CFFIndex;for(const n of e){if(a){n.removeByName("CIDFontVersion");n.removeByName("CIDFontRevision");n.removeByName("CIDFontType");n.removeByName("CIDCount");n.removeByName("UIDBase")}const e=new CFFOffsetTracker,s=this.compileDict(n,e);r.push(e);i.add(s);e.offset(t)}i=this.compileIndex(i,r);return{trackers:r,output:i}}compilePrivateDicts(e,t,a){for(let r=0,i=e.length;r>8&255,255&e])}else{i=new Uint8Array(1+2*n);i[0]=0;let t=0;const r=e.charset.length;let s=!1;for(let n=1;n>8&255;i[n+1]=255&o}}return this.compileTypedArray(i)}compileEncoding(e){return this.compileTypedArray(e.raw)}compileFDSelect(e){const t=e.format;let a,r;switch(t){case 0:a=new Uint8Array(1+e.fdSelect.length);a[0]=t;for(r=0;r>8&255,255&i,n];for(r=1;r>8&255,255&r,t);n=t}}const o=(s.length-3)/3;s[1]=o>>8&255;s[2]=255&o;s.push(r>>8&255,255&r);a=new Uint8Array(s)}return this.compileTypedArray(a)}compileTypedArray(e){return Array.from(e)}compileIndex(e,t=[]){const a=e.objects,r=a.length;if(0===r)return[0,0];const i=[r>>8&255,255&r];let n,s,o=1;for(n=0;n>8&255,255&c):3===s?i.push(c>>16&255,c>>8&255,255&c):i.push(c>>>24&255,c>>16&255,c>>8&255,255&c);a[n]&&(c+=a[n].length)}for(n=0;n=this.firstChar&&e<=this.lastChar?e:-1}amend(e){unreachable("Should not call amend()")}}class CFFFont{constructor(e,t){this.properties=t;const a=new CFFParser(e,t,Rr);this.cff=a.parse();this.cff.duplicateFirstGlyph();const r=new CFFCompiler(this.cff);this.seacs=this.cff.seacs;try{this.data=r.compile()}catch{warn("Failed to compile font "+t.loadedName);this.data=e}this._createBuiltInEncoding()}get numGlyphs(){return this.cff.charStrings.count}getCharset(){return this.cff.charset.charset}getGlyphMapping(){const e=this.cff,t=this.properties,{cidToGidMap:a,cMap:r}=t,i=e.charset.charset;let n,s;if(t.composite){let t,o;if(a?.length>0){t=Object.create(null);for(let e=0,r=a.length;e=0){const r=a[t];r&&(i[e]=r)}}i.length>0&&(this.properties.builtInEncoding=i)}}function getFloat214(e,t){return readInt16(e,t)/16384}function getSubroutineBias(e){const t=e.length;let a=32768;t<1240?a=107:t<33900&&(a=1131);return a}function parseCmap(e,t,a){const r=1===readUint16(e,t+2)?readUint32(e,t+8):readUint32(e,t+16),i=readUint16(e,t+r);let n,s,o;if(4===i){readUint16(e,t+r+2);const a=readUint16(e,t+r+6)>>1;s=t+r+14;n=[];for(o=0;o>1;a0;)h.push({flags:n})}for(a=0;a>1;y=!0;break;case 4:s+=i.pop();moveTo(n,s);y=!0;break;case 5:for(;i.length>0;){n+=i.shift();s+=i.shift();lineTo(n,s)}break;case 6:for(;i.length>0;){n+=i.shift();lineTo(n,s);if(0===i.length)break;s+=i.shift();lineTo(n,s)}break;case 7:for(;i.length>0;){s+=i.shift();lineTo(n,s);if(0===i.length)break;n+=i.shift();lineTo(n,s)}break;case 8:for(;i.length>0;){l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}break;case 10:m=i.pop();b=null;if(a.isCFFCIDFont){const e=a.fdSelect.getFDIndex(r);if(e>=0&&eMath.abs(s-t)?n+=i.shift():s+=i.shift();bezierCurveTo(l,u,h,d,n,s);break;default:throw new FormatError(`unknown operator: 12 ${w}`)}break;case 14:if(i.length>=4){const e=i.pop(),r=i.pop();s=i.pop();n=i.pop();t.save();t.translate(n,s);let o=lookupCmap(a.cmap,String.fromCharCode(a.glyphNameMap[Ar[e]]));compileCharString(a.glyphs[o.glyphId],t,a,o.glyphId);t.restore();o=lookupCmap(a.cmap,String.fromCharCode(a.glyphNameMap[Ar[r]]));compileCharString(a.glyphs[o.glyphId],t,a,o.glyphId)}return;case 19:case 20:o+=i.length>>1;c+=o+7>>3;y=!0;break;case 21:s+=i.pop();n+=i.pop();moveTo(n,s);y=!0;break;case 22:n+=i.pop();moveTo(n,s);y=!0;break;case 24:for(;i.length>2;){l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}n+=i.shift();s+=i.shift();lineTo(n,s);break;case 25:for(;i.length>6;){n+=i.shift();s+=i.shift();lineTo(n,s)}l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s);break;case 26:i.length%2&&(n+=i.shift());for(;i.length>0;){l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h;s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}break;case 27:i.length%2&&(s+=i.shift());for(;i.length>0;){l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d;bezierCurveTo(l,u,h,d,n,s)}break;case 28:i.push(readInt16(e,c));c+=2;break;case 29:m=i.pop()+a.gsubrsBias;b=a.gsubrs[m];b&&parse(b);break;case 30:for(;i.length>0;){l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s);if(0===i.length)break;l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();s=d+i.shift();n=h+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s)}break;case 31:for(;i.length>0;){l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();s=d+i.shift();n=h+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s);if(0===i.length)break;l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s)}break;default:if(w<32)throw new FormatError(`unknown operator: ${w}`);if(w<247)i.push(w-139);else if(w<251)i.push(256*(w-247)+e[c++]+108);else if(w<255)i.push(256*-(w-251)-e[c++]-108);else{i.push((e[c]<<24|e[c+1]<<16|e[c+2]<<8|e[c+3])/65536);c+=4}}y&&(i.length=0)}}(e)}class Commands{cmds=[];transformStack=[];currentTransform=[1,0,0,1,0,0];add(e,t){if(t){const{currentTransform:a}=this;for(let e=0,r=t.length;e=0&&e2*readUint16(e,t)}const n=[];let s=i(t,0);for(let a=r;ae.getSize()+3&-4)))}write(){const e=this.getSize(),t=new DataView(new ArrayBuffer(e)),a=e>131070,r=a?4:2,i=new DataView(new ArrayBuffer((this.glyphs.length+1)*r));a?i.setUint32(0,0):i.setUint16(0,0);let n=0,s=0;for(const e of this.glyphs){n+=e.write(n,t);n=n+3&-4;s+=r;a?i.setUint32(s,n):i.setUint16(s,n>>1)}return{isLocationLong:a,loca:new Uint8Array(i.buffer),glyf:new Uint8Array(t.buffer)}}scale(e){for(let t=0,a=this.glyphs.length;te.getSize())));return this.header.getSize()+e}write(e,t){if(!this.header)return 0;const a=e;e+=this.header.write(e,t);if(this.simple)e+=this.simple.write(e,t);else for(const a of this.composites)e+=a.write(e,t);return e-a}scale(e){if(!this.header)return;const t=(this.header.xMin+this.header.xMax)/2;this.header.scale(t,e);if(this.simple)this.simple.scale(t,e);else for(const a of this.composites)a.scale(t,e)}}class GlyphHeader{constructor({numberOfContours:e,xMin:t,yMin:a,xMax:r,yMax:i}){this.numberOfContours=e;this.xMin=t;this.yMin=a;this.xMax=r;this.yMax=i}static parse(e,t){return[10,new GlyphHeader({numberOfContours:t.getInt16(e),xMin:t.getInt16(e+2),yMin:t.getInt16(e+4),xMax:t.getInt16(e+6),yMax:t.getInt16(e+8)})]}getSize(){return 10}write(e,t){t.setInt16(e,this.numberOfContours);t.setInt16(e+2,this.xMin);t.setInt16(e+4,this.yMin);t.setInt16(e+6,this.xMax);t.setInt16(e+8,this.yMax);return 10}scale(e,t){this.xMin=Math.round(e+(this.xMin-e)*t);this.xMax=Math.round(e+(this.xMax-e)*t)}}class Contour{constructor({flags:e,xCoordinates:t,yCoordinates:a}){this.xCoordinates=t;this.yCoordinates=a;this.flags=e}}class SimpleGlyph{constructor({contours:e,instructions:t}){this.contours=e;this.instructions=t}static parse(e,t,a){const r=[];for(let i=0;i255?e+=2:o>0&&(e+=1);t=n;o=Math.abs(s-a);o>255?e+=2:o>0&&(e+=1);a=s}}return e}write(e,t){const a=e,r=[],i=[],n=[];let s=0,o=0;for(const a of this.contours){for(let e=0,t=a.xCoordinates.length;e=0?18:2;r.push(e)}else r.push(l)}s=c;const h=a.yCoordinates[e];l=h-o;if(0===l){t|=32;i.push(0)}else{const e=Math.abs(l);if(e<=255){t|=l>=0?36:4;i.push(e)}else i.push(l)}o=h;n.push(t)}t.setUint16(e,r.length-1);e+=2}t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}for(const a of n)t.setUint8(e++,a);for(let a=0,i=r.length;a=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(e+=2):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(e+=2);return e}write(e,t){const a=e;2&this.flags?this.argument1>=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(this.flags|=1):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(this.flags|=1);t.setUint16(e,this.flags);t.setUint16(e+2,this.glyphIndex);e+=4;if(1&this.flags){if(2&this.flags){t.setInt16(e,this.argument1);t.setInt16(e+2,this.argument2)}else{t.setUint16(e,this.argument1);t.setUint16(e+2,this.argument2)}e+=4}else{t.setUint8(e,this.argument1);t.setUint8(e+1,this.argument2);e+=2}if(256&this.flags){t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}}return e-a}scale(e,t){}}function writeInt16(e,t,a){e[t]=a>>8&255;e[t+1]=255&a}function writeInt32(e,t,a){e[t]=a>>24&255;e[t+1]=a>>16&255;e[t+2]=a>>8&255;e[t+3]=255&a}function writeData(e,t,a){if(a instanceof Uint8Array)e.set(a,t);else if("string"==typeof a)for(let r=0,i=a.length;ra;){a<<=1;r++}const i=a*t;return{range:i,entry:r,rangeShift:t*e-i}}toArray(){let e=this.sfnt;const t=this.tables,a=Object.keys(t);a.sort();const r=a.length;let i,n,s,o,c,l=12+16*r;const h=[l];for(i=0;i>>0;h.push(l)}const u=new Uint8Array(l);for(i=0;i>>0}writeInt32(u,l+4,e);writeInt32(u,l+8,h[i]);writeInt32(u,l+12,t[c].length);l+=16}return u}addTable(e,t){if(e in this.tables)throw new Error("Table "+e+" already exists");this.tables[e]=t}}const si=[4],oi=[5],ci=[6],li=[7],hi=[8],ui=[12,35],di=[14],fi=[21],gi=[22],pi=[30],mi=[31];class Type1CharString{constructor(){this.width=0;this.lsb=0;this.flexing=!1;this.output=[];this.stack=[]}convert(e,t,a){const r=e.length;let i,n,s,o=!1;for(let c=0;cr)return!0;const i=r-e;for(let e=i;e>8&255,255&t);else{t=65536*t|0;this.output.push(255,t>>24&255,t>>16&255,t>>8&255,255&t)}}this.output.push(...t);a?this.stack.splice(i,e):this.stack.length=0;return!1}}function isHexDigit(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function decrypt(e,t,a){if(a>=e.length)return new Uint8Array(0);let r,i,n=0|t;for(r=0;r>8;n=52845*(t+n)+22719&65535}return o}function isSpecial(e){return 47===e||91===e||93===e||123===e||125===e||40===e||41===e}class Type1Parser{constructor(e,t,a){if(t){const t=e.getBytes(),a=!((isHexDigit(t[0])||isWhiteSpace(t[0]))&&isHexDigit(t[1])&&isHexDigit(t[2])&&isHexDigit(t[3])&&isHexDigit(t[4])&&isHexDigit(t[5])&&isHexDigit(t[6])&&isHexDigit(t[7]));e=new Stream(a?decrypt(t,55665,4):function decryptAscii(e,t,a){let r=0|t;const i=e.length,n=new Uint8Array(i>>>1);let s,o;for(s=0,o=0;s>8;r=52845*(e+r)+22719&65535}}return n.slice(a,o)}(t,55665,4))}this.seacAnalysisEnabled=!!a;this.stream=e;this.nextChar()}readNumberArray(){this.getToken();const e=[];for(;;){const t=this.getToken();if(null===t||"]"===t||"}"===t)break;e.push(parseFloat(t||0))}return e}readNumber(){const e=this.getToken();return parseFloat(e||0)}readInt(){const e=this.getToken();return 0|parseInt(e||0,10)}readBoolean(){return"true"===this.getToken()?1:0}nextChar(){return this.currentChar=this.stream.getByte()}prevChar(){this.stream.skip(-2);return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(-1===t)return null;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}if(isSpecial(t)){this.nextChar();return String.fromCharCode(t)}let a="";do{a+=String.fromCharCode(t);t=this.nextChar()}while(t>=0&&!isWhiteSpace(t)&&!isSpecial(t));return a}readCharStrings(e,t){return-1===t?e:decrypt(e,4330,t)}extractFontProgram(e){const t=this.stream,a=[],r=[],i=Object.create(null);i.lenIV=4;const n={subrs:[],charstrings:[],properties:{privateData:i}};let s,o,c,l;for(;null!==(s=this.getToken());)if("/"===s){s=this.getToken();switch(s){case"CharStrings":this.getToken();this.getToken();this.getToken();this.getToken();for(;;){s=this.getToken();if(null===s||"end"===s)break;if("/"!==s)continue;const e=this.getToken();o=this.readInt();this.getToken();c=o>0?t.getBytes(o):new Uint8Array(0);l=n.properties.privateData.lenIV;const a=this.readCharStrings(c,l);this.nextChar();s=this.getToken();"noaccess"===s?this.getToken():"/"===s&&this.prevChar();r.push({glyph:e,encoded:a})}break;case"Subrs":this.readInt();this.getToken();for(;"dup"===this.getToken();){const e=this.readInt();o=this.readInt();this.getToken();c=o>0?t.getBytes(o):new Uint8Array(0);l=n.properties.privateData.lenIV;const r=this.readCharStrings(c,l);this.nextChar();s=this.getToken();"noaccess"===s&&this.getToken();a[e]=r}break;case"BlueValues":case"OtherBlues":case"FamilyBlues":case"FamilyOtherBlues":const e=this.readNumberArray();e.length>0&&e.length,0;break;case"StemSnapH":case"StemSnapV":n.properties.privateData[s]=this.readNumberArray();break;case"StdHW":case"StdVW":n.properties.privateData[s]=this.readNumberArray()[0];break;case"BlueShift":case"lenIV":case"BlueFuzz":case"BlueScale":case"LanguageGroup":n.properties.privateData[s]=this.readNumber();break;case"ExpansionFactor":n.properties.privateData[s]=this.readNumber()||.06;break;case"ForceBold":n.properties.privateData[s]=this.readBoolean()}}for(const{encoded:t,glyph:i}of r){const r=new Type1CharString,s=r.convert(t,a,this.seacAnalysisEnabled);let o=r.output;s&&(o=[14]);const c={glyphName:i,charstring:o,width:r.width,lsb:r.lsb,seac:r.seac};".notdef"===i?n.charstrings.unshift(c):n.charstrings.push(c);if(e.builtInEncoding){const t=e.builtInEncoding.indexOf(i);t>-1&&void 0===e.widths[t]&&t>=e.firstChar&&t<=e.lastChar&&(e.widths[t]=r.width)}}return n}extractFontHeader(e){let t;for(;null!==(t=this.getToken());)if("/"===t){t=this.getToken();switch(t){case"FontMatrix":const a=this.readNumberArray();e.fontMatrix=a;break;case"Encoding":const r=this.getToken();let i;if(/^\d+$/.test(r)){i=[];const e=0|parseInt(r,10);this.getToken();for(let a=0;a=i){s+=a;for(;s=0&&(r[e]=i)}}return type1FontGlyphMapping(e,r,a)}hasGlyphId(e){if(e<0||e>=this.numGlyphs)return!1;if(0===e)return!0;return this.charstrings[e-1].charstring.length>0}getSeacs(e){const t=[];for(let a=0,r=e.length;a0;e--)t[e]-=t[e-1];f.setByName(e,t)}n.topDict.privateDict=f;const p=new CFFIndex;for(h=0,u=r.length;h0&&e.toUnicode.amend(t)}class fonts_Glyph{constructor(e,t,a,r,i,n,s,o,c){this.originalCharCode=e;this.fontChar=t;this.unicode=a;this.accent=r;this.width=i;this.vmetric=n;this.operatorListId=s;this.isSpace=o;this.isInFont=c}get category(){return shadow(this,"category",function getCharUnicodeCategory(e){const t=Dr.get(e);if(t)return t;const a=e.match(Mr),r={isWhitespace:!!a?.[1],isZeroWidthDiacritic:!!a?.[2],isInvisibleFormatMark:!!a?.[3]};Dr.set(e,r);return r}(this.unicode),!0)}}function int16(e,t){return(e<<8)+t}function writeSignedInt16(e,t,a){e[t+1]=a;e[t]=a>>>8}function signedInt16(e,t){const a=(e<<8)+t;return 32768&a?a-65536:a}function string16(e){return String.fromCharCode(e>>8&255,255&e)}function safeString16(e){e>32767?e=32767:e<-32768&&(e=-32768);return String.fromCharCode(e>>8&255,255&e)}function isTrueTypeCollectionFile(e){return"ttcf"===bytesToString(e.peekBytes(4))}function getFontFileType(e,{type:t,subtype:a,composite:r}){let i,n;if(function isTrueTypeFile(e){const t=e.peekBytes(4);return 65536===readUint32(t,0)||"true"===bytesToString(t)}(e)||isTrueTypeCollectionFile(e))i=r?"CIDFontType2":"TrueType";else if(function isOpenTypeFile(e){return"OTTO"===bytesToString(e.peekBytes(4))}(e))i=r?"CIDFontType2":"OpenType";else if(function isType1File(e){const t=e.peekBytes(2);return 37===t[0]&&33===t[1]||128===t[0]&&1===t[1]}(e))i=r?"CIDFontType0":"MMType1"===t?"MMType1":"Type1";else if(function isCFFFile(e){const t=e.peekBytes(4);return t[0]>=1&&t[3]>=1&&t[3]<=4}(e))if(r){i="CIDFontType0";n="CIDFontType0C"}else{i="MMType1"===t?"MMType1":"Type1";n="Type1C"}else{warn("getFontFileType: Unable to detect correct font file Type/Subtype.");i=t;n=a}return[i,n]}function applyStandardFontGlyphMap(e,t){for(const a in t)e[+a]=t[a]}function buildToFontChar(e,t,a){const r=[];let i;for(let a=0,n=e.length;ah){c++;if(c>=bi.length){warn("Ran out of space in font private use area.");break}l=bi[c][0];h=bi[c][1]}const p=l++;0===g&&(g=a);let m=r.get(f);if("string"==typeof m)if(1===m.length)m=m.codePointAt(0);else{if(!u){u=new Map;for(let e=64256;e<=64335;e++){const t=String.fromCharCode(e).normalize("NFKD");t.length>1&&u.set(t,e)}}m=u.get(m)||m.codePointAt(0)}if(m&&!(d=m,bi[0][0]<=d&&d<=bi[0][1]||bi[1][0]<=d&&d<=bi[1][1])&&!o.has(g)){n.set(m,g);o.add(g)}i[p]=g;s[f]=p}var d;return{toFontChar:s,charCodeToGlyphId:i,toUnicodeExtraMap:n,nextAvailableFontCharCode:l}}function createCmapTable(e,t,a){const r=function getRanges(e,t,a){const r=[];for(const t in e)e[t]>=a||r.push({fontCharCode:0|t,glyphId:e[t]});if(t)for(const[e,i]of t)i>=a||r.push({fontCharCode:e,glyphId:i});0===r.length&&r.push({fontCharCode:0,glyphId:0});r.sort(((e,t)=>e.fontCharCode-t.fontCharCode));const i=[],n=r.length;for(let e=0;e65535?2:1;let n,s,o,c,l="\0\0"+string16(i)+"\0\0"+string32(4+8*i);for(n=r.length-1;n>=0&&!(r[n][0]<=65535);--n);const h=n+1;r[n][0]<65535&&65535===r[n][1]&&(r[n][1]=65534);const u=r[n][1]<65535?1:0,d=h+u,f=OpenTypeFileBuilder.getSearchParams(d,2);let g,p,m,b,y="",w="",S="",x="",k="",C=0;for(n=0,s=h;n0){w+="ÿÿ";y+="ÿÿ";S+="\0";x+="\0\0"}const v="\0\0"+string16(2*d)+string16(f.range)+string16(f.entry)+string16(f.rangeShift)+w+"\0\0"+y+S+x+k;let F="",O="";if(i>1){l+="\0\0\n"+string32(4+8*i+4+v.length);F="";for(n=0,s=r.length;ne||!o)&&(o=e);c 123 are reserved for internal usage");s|=1<65535&&(c=65535)}else{o=0;c=255}const h=e.bbox||[0,0,0,0],u=a.unitsPerEm||(e.fontMatrix?1/Math.max(...e.fontMatrix.slice(0,4).map(Math.abs)):1e3),d=e.ascentScaled?1:u/yi,f=a.ascent||Math.round(d*(e.ascent||h[3]));let g=a.descent||Math.round(d*(e.descent||h[1]));g>0&&e.descent>0&&h[1]<0&&(g=-g);const p=a.yMax||f,m=-a.yMin||-g;return"\0$ô\0\0\0Š»\0\0\0ŒŠ»\0\0ß\x001\0\0\0\0"+String.fromCharCode(e.fixedPitch?9:0)+"\0\0\0\0\0\0"+string32(r)+string32(i)+string32(n)+string32(s)+"*21*"+string16(e.italicAngle?1:0)+string16(o||e.firstChar)+string16(c||e.lastChar)+string16(f)+string16(g)+"\0d"+string16(p)+string16(m)+"\0\0\0\0\0\0\0\0"+string16(e.xHeight)+string16(e.capHeight)+string16(0)+string16(o||e.firstChar)+"\0"}function createPostTable(e){return"\0\0\0"+string32(Math.floor(65536*e.italicAngle))+"\0\0\0\0"+string32(e.fixedPitch?1:0)+"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}function createPostscriptName(e){return e.replaceAll(/[^\x21-\x7E]|[[\](){}<>/%]/g,"").slice(0,63)}function createNameTable(e,t){t||(t=[[],[]]);const a=[t[0][0]||"Original licence",t[0][1]||e,t[0][2]||"Unknown",t[0][3]||"uniqueID",t[0][4]||e,t[0][5]||"Version 0.11",t[0][6]||createPostscriptName(e),t[0][7]||"Unknown",t[0][8]||"Unknown",t[0][9]||"Unknown"],r=[];let i,n,s,o,c;for(i=0,n=a.length;i0;if((s||o)&&"CIDFontType2"===a&&this.cidEncoding.startsWith("Identity-")){const a=e.cidToGidMap,r=[];applyStandardFontGlyphMap(r,ti());/Arial-?Black/i.test(t)?applyStandardFontGlyphMap(r,ai()):/Calibri/i.test(t)&&applyStandardFontGlyphMap(r,ri());if(a){for(const e in r){const t=r[e];void 0!==a[t]&&(r[+e]=a[t])}a.length!==this.toUnicode.length&&e.hasIncludedToUnicodeMap&&this.toUnicode instanceof IdentityToUnicodeMap&&this.toUnicode.forEach((function(e,t){const i=r[e];void 0===a[i]&&(r[+e]=t)}))}this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(e,t){r[+e]=t}));this.toFontChar=r;this.toUnicode=new ToUnicodeMap(r)}else if(/Symbol/i.test(r))this.toFontChar=buildToFontChar(Cr,Fr(),this.differences);else if(/Dingbats/i.test(r))this.toFontChar=buildToFontChar(vr,Ir(),this.differences);else if(s||o){const e=buildToFontChar(this.defaultEncoding,Fr(),this.differences);"CIDFontType2"!==a||this.cidEncoding.startsWith("Identity-")||this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(t,a){e[+t]=a}));this.toFontChar=e}else{const e=Fr(),a=[];this.toUnicode.forEach(((t,r)=>{if(!this.composite){const a=getUnicodeForGlyph(this.differences[t]||this.defaultEncoding[t],e);-1!==a&&(r=a)}a[+t]=r}));this.composite&&this.toUnicode instanceof IdentityToUnicodeMap&&/Tahoma|Verdana/i.test(t)&&applyStandardFontGlyphMap(a,ti());this.toFontChar=a}amendFallbackToUnicode(e);this.loadedName=r.split("-",1)[0]}checkAndRepair(e,t,a){const r=["OS/2","cmap","head","hhea","hmtx","maxp","name","post","loca","glyf","fpgm","prep","cvt ","CFF "];function readTables(e,t){const a=Object.create(null);a["OS/2"]=null;a.cmap=null;a.head=null;a.hhea=null;a.hmtx=null;a.maxp=null;a.name=null;a.post=null;for(let i=0;i>>0,r=e.getInt32()>>>0,i=e.getInt32()>>>0,n=e.pos;e.pos=e.start||0;e.skip(r);const s=e.getBytes(i);e.pos=n;if("head"===t){s[8]=s[9]=s[10]=s[11]=0;s[17]|=32}return{tag:t,checksum:a,length:i,offset:r,data:s}}function readOpenTypeHeader(e){return{version:e.getString(4),numTables:e.getUint16(),searchRange:e.getUint16(),entrySelector:e.getUint16(),rangeShift:e.getUint16()}}function sanitizeGlyph(e,t,a,r,i,n){const s={length:0,sizeOfInstructions:0};if(t<0||t>=e.length||a>e.length||a-t<=12)return s;const o=e.subarray(t,a),c=signedInt16(o[2],o[3]),l=signedInt16(o[4],o[5]),h=signedInt16(o[6],o[7]),u=signedInt16(o[8],o[9]);if(c>h){writeSignedInt16(o,2,h);writeSignedInt16(o,6,c)}if(l>u){writeSignedInt16(o,4,u);writeSignedInt16(o,8,l)}const d=signedInt16(o[0],o[1]);if(d<0){if(d<-1)return s;r.set(o,i);s.length=o.length;return s}let f,g=10,p=0;for(f=0;fo.length)return s;if(!n&&b>0){r.set(o.subarray(0,m),i);r.set([0,0],i+m);r.set(o.subarray(y,S),i+m+2);S-=b;o.length-S>3&&(S=S+3&-4);s.length=S;return s}if(o.length-S>3){S=S+3&-4;r.set(o.subarray(0,S),i);s.length=S;return s}r.set(o,i);s.length=o.length;return s}function readNameTable(e){const a=(t.start||0)+e.offset;t.pos=a;const r=[[],[]],i=[],n=e.length,s=a+n;if(0!==t.getUint16()||n<6)return[r,i];const o=t.getUint16(),c=t.getUint16();let l,h;for(l=0;ls)continue;t.pos=n;const o=e.name;if(e.encoding){let a="";for(let r=0,i=e.length;r0&&(l+=e-1)}}else{if(m||y){warn("TT: nested FDEFs not allowed");p=!0}m=!0;u=l;s=d.pop();t.functionsDefined[s]={data:c,i:l}}else if(!m&&!y){s=d.at(-1);if(isNaN(s))info("TT: CALL empty stack (or invalid entry).");else{t.functionsUsed[s]=!0;if(s in t.functionsStackDeltas){const e=d.length+t.functionsStackDeltas[s];if(e<0){warn("TT: CALL invalid functions stack delta.");t.hintsValid=!1;return}d.length=e}else if(s in t.functionsDefined&&!g.includes(s)){f.push({data:c,i:l,stackTop:d.length-1});g.push(s);o=t.functionsDefined[s];if(!o){warn("TT: CALL non-existent function");t.hintsValid=!1;return}c=o.data;l=o.i}}}if(!m&&!y){let t=0;e<=142?t=i[e]:e>=192&&e<=223?t=-1:e>=224&&(t=-2);if(e>=113&&e<=117){r=d.pop();isNaN(r)||(t=2*-r)}for(;t<0&&d.length>0;){d.pop();t++}for(;t>0;){d.push(NaN);t--}}}t.tooComplexToFollowFunctions=p;const w=[c];l>c.length&&w.push(new Uint8Array(l-c.length));if(u>h){warn("TT: complementing a missing function tail");w.push(new Uint8Array([34,45]))}!function foldTTTable(e,t){if(t.length>1){let a,r,i=0;for(a=0,r=t.length;a>>0,n=[];for(let t=0;t>>0);const s={ttcTag:t,majorVersion:a,minorVersion:r,numFonts:i,offsetTable:n};switch(a){case 1:return s;case 2:s.dsigTag=e.getInt32()>>>0;s.dsigLength=e.getInt32()>>>0;s.dsigOffset=e.getInt32()>>>0;return s}throw new FormatError(`Invalid TrueType Collection majorVersion: ${a}.`)}(e),i=t.split("+");let n;for(let s=0;s0||!(a.cMap instanceof IdentityCMap));if("OTTO"===n.version&&!t||!s.head||!s.hhea||!s.maxp||!s.post){c=new Stream(s["CFF "].data);o=new CFFFont(c,a);return this.convert(e,o,a)}delete s.glyf;delete s.loca;delete s.fpgm;delete s.prep;delete s["cvt "];this.isOpenType=!0}if(!s.maxp)throw new FormatError('Required "maxp" table is not found');t.pos=(t.start||0)+s.maxp.offset;let h=t.getInt32();const u=t.getUint16();if(65536!==h&&20480!==h){if(6===s.maxp.length)h=20480;else{if(!(s.maxp.length>=32))throw new FormatError('"maxp" table has a wrong version number');h=65536}!function writeUint32(e,t,a){e[t+3]=255&a;e[t+2]=a>>>8;e[t+1]=a>>>16;e[t]=a>>>24}(s.maxp.data,0,h)}if(a.scaleFactors?.length===u&&l){const{scaleFactors:e}=a,t=int16(s.head.data[50],s.head.data[51]),r=new GlyfTable({glyfTable:s.glyf.data,isGlyphLocationsLong:t,locaTable:s.loca.data,numGlyphs:u});r.scale(e);const{glyf:i,loca:n,isLocationLong:o}=r.write();s.glyf.data=i;s.loca.data=n;if(o!==!!t){s.head.data[50]=0;s.head.data[51]=o?1:0}const c=s.hmtx.data;for(let t=0;t>8&255;c[a+1]=255&r;writeSignedInt16(c,a+2,Math.round(e[t]*signedInt16(c[a+2],c[a+3])))}}let d=u+1,f=!0;if(d>65535){f=!1;d=u;warn("Not enough space in glyfs to duplicate first glyph.")}let g=0,p=0;if(h>=65536&&s.maxp.length>=32){t.pos+=8;if(t.getUint16()>2){s.maxp.data[14]=0;s.maxp.data[15]=2}t.pos+=4;g=t.getUint16();t.pos+=4;p=t.getUint16()}s.maxp.data[4]=d>>8;s.maxp.data[5]=255&d;const m=function sanitizeTTPrograms(e,t,a,r){const i={functionsDefined:[],functionsUsed:[],functionsStackDeltas:[],tooComplexToFollowFunctions:!1,hintsValid:!0};e&&sanitizeTTProgram(e,i);t&&sanitizeTTProgram(t,i);e&&function checkInvalidFunctions(e,t){if(!e.tooComplexToFollowFunctions)if(e.functionsDefined.length>t){warn("TT: more functions defined than expected");e.hintsValid=!1}else for(let a=0,r=e.functionsUsed.length;at){warn("TT: invalid function id: "+a);e.hintsValid=!1;return}if(e.functionsUsed[a]&&!e.functionsDefined[a]){warn("TT: undefined function: "+a);e.hintsValid=!1;return}}}(i,r);if(a&&1&a.length){const e=new Uint8Array(a.length+1);e.set(a.data);a.data=e}return i.hintsValid}(s.fpgm,s.prep,s["cvt "],g);if(!m){delete s.fpgm;delete s.prep;delete s["cvt "]}!function sanitizeMetrics(e,t,a,r,i,n){if(!t){a&&(a.data=null);return}e.pos=(e.start||0)+t.offset;e.pos+=4;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;const s=e.getUint16();e.pos+=8;e.pos+=2;let o=e.getUint16();if(0!==s){if(!(2&int16(r.data[44],r.data[45]))){t.data[22]=0;t.data[23]=0}}if(o>i){info(`The numOfMetrics (${o}) should not be greater than the numGlyphs (${i}).`);o=i;t.data[34]=(65280&o)>>8;t.data[35]=255&o}const c=i-o-(a.length-4*o>>1);if(c>0){const e=new Uint8Array(a.length+2*c);e.set(a.data);if(n){e[a.length]=a.data[2];e[a.length+1]=a.data[3]}a.data=e}}(t,s.hhea,s.hmtx,s.head,d,f);if(!s.head)throw new FormatError('Required "head" table is not found');!function sanitizeHead(e,t,a){const r=e.data,i=function int32(e,t,a,r){return(e<<24)+(t<<16)+(a<<8)+r}(r[0],r[1],r[2],r[3]);if(i>>16!=1){info("Attempting to fix invalid version in head table: "+i);r[0]=0;r[1]=1;r[2]=0;r[3]=0}const n=int16(r[50],r[51]);if(n<0||n>1){info("Attempting to fix invalid indexToLocFormat in head table: "+n);const e=t+1;if(a===e<<1){r[50]=0;r[51]=0}else{if(a!==e<<2)throw new FormatError("Could not fix indexToLocFormat: "+n);r[50]=0;r[51]=1}}}(s.head,u,l?s.loca.length:0);let b=Object.create(null);if(l){const e=int16(s.head.data[50],s.head.data[51]),t=function sanitizeGlyphLocations(e,t,a,r,i,n,s){let o,c,l;if(r){o=4;c=function fontItemDecodeLong(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]};l=function fontItemEncodeLong(e,t,a){e[t]=a>>>24&255;e[t+1]=a>>16&255;e[t+2]=a>>8&255;e[t+3]=255&a}}else{o=2;c=function fontItemDecode(e,t){return e[t]<<9|e[t+1]<<1};l=function fontItemEncode(e,t,a){e[t]=a>>9&255;e[t+1]=a>>1&255}}const h=n?a+1:a,u=o*(1+h),d=new Uint8Array(u);d.set(e.data.subarray(0,u));e.data=d;const f=t.data,g=f.length,p=new Uint8Array(g);let m,b;const y=[];for(m=0,b=0;mg&&(e=g);y.push({index:m,offset:e,endOffset:0})}y.sort(((e,t)=>e.offset-t.offset));for(m=0;me.index-t.index));for(m=0;ms&&(s=e.sizeOfInstructions);x+=t;l(d,b,x)}if(0===x){const e=new Uint8Array([0,1,0,0,0,0,0,0,0,0,0,0,0,0,49,0]);for(m=0,b=o;ma+x)t.data=p.subarray(0,a+x);else{t.data=new Uint8Array(a+x);t.data.set(p.subarray(0,x))}t.data.set(p.subarray(0,a),x);l(e.data,d.length-o,x+a)}else t.data=p.subarray(0,x);return{missingGlyphs:S,maxSizeOfInstructions:s}}(s.loca,s.glyf,u,e,m,f,p);b=t.missingGlyphs;if(h>=65536&&s.maxp.length>=32){s.maxp.data[26]=t.maxSizeOfInstructions>>8;s.maxp.data[27]=255&t.maxSizeOfInstructions}}if(!s.hhea)throw new FormatError('Required "hhea" table is not found');if(0===s.hhea.data[10]&&0===s.hhea.data[11]){s.hhea.data[10]=255;s.hhea.data[11]=255}const y={unitsPerEm:int16(s.head.data[18],s.head.data[19]),yMax:signedInt16(s.head.data[42],s.head.data[43]),yMin:signedInt16(s.head.data[38],s.head.data[39]),ascent:signedInt16(s.hhea.data[4],s.hhea.data[5]),descent:signedInt16(s.hhea.data[6],s.hhea.data[7]),lineGap:signedInt16(s.hhea.data[8],s.hhea.data[9])};this.ascent=y.ascent/y.unitsPerEm;this.descent=y.descent/y.unitsPerEm;this.lineGap=y.lineGap/y.unitsPerEm;if(this.cssFontInfo?.lineHeight){this.lineHeight=this.cssFontInfo.metrics.lineHeight;this.lineGap=this.cssFontInfo.metrics.lineGap}else this.lineHeight=this.ascent-this.descent+this.lineGap;s.post&&function readPostScriptTable(e,a,r){const i=(t.start||0)+e.offset;t.pos=i;const n=i+e.length,s=t.getInt32();t.skip(28);let o,c,l=!0;switch(s){case 65536:o=jr;break;case 131072:const e=t.getUint16();if(e!==r){l=!1;break}const i=[];for(c=0;c=32768){l=!1;break}i.push(e)}if(!l)break;const h=[],u=[];for(;t.pos65535)throw new FormatError("Max size of CID is 65,535");let i=-1;t?i=r:void 0!==e[r]&&(i=e[r]);i>=0&&i>>0;let h=!1;if(o?.platformId!==i||o?.encodingId!==n){if(0!==i||0!==n&&1!==n&&3!==n)if(1===i&&0===n)h=!0;else if(3!==i||1!==n||!r&&o){if(a&&3===i&&0===n){h=!0;let a=!0;if(e>3;e.push(r);a=Math.max(r,a)}const r=[];for(let e=0;e<=a;e++)r.push({firstCode:t.getUint16(),entryCount:t.getUint16(),idDelta:signedInt16(t.getByte(),t.getByte()),idRangePos:t.pos+t.getUint16()});for(let a=0;a<256;a++)if(0===e[a]){t.pos=r[0].idRangePos+2*a;f=t.getUint16();u.push({charCode:a,glyphId:f})}else{const i=r[e[a]];for(d=0;d>1;t.skip(6);const a=[];let r;for(r=0;r>1)-(e-r);i.offsetIndex=s;o=Math.max(o,s+i.end-i.start+1)}else i.offsetIndex=-1}const c=[];for(d=0;d>>0;for(d=0;d>>0,a=t.getInt32()>>>0;let r=t.getInt32()>>>0;for(let t=e;t<=a;t++)u.push({charCode:t,glyphId:r++})}}}u.sort(((e,t)=>e.charCode-t.charCode));const g=[],p=new Set;for(const e of u){const{charCode:t}=e;if(!p.has(t)){p.add(t);g.push(e)}}return{platformId:o.platformId,encodingId:o.encodingId,mappings:g,hasShortCmap:h}}(s.cmap,t,this.isSymbolicFont,a.hasEncoding),r=e.platformId,i=e.encodingId,n=e.mappings;let o=[],c=!1;!a.hasEncoding||"MacRomanEncoding"!==a.baseEncodingName&&"WinAnsiEncoding"!==a.baseEncodingName||(o=getEncoding(a.baseEncodingName));if(a.hasEncoding&&!this.isSymbolicFont&&(3===r&&1===i||1===r&&0===i)){const e=Fr();for(let t=0;t<256;t++){let s;s=void 0!==this.differences[t]?this.differences[t]:o.length&&""!==o[t]?o[t]:Ar[t];if(!s)continue;const c=recoverGlyphName(s,e);let l;3===r&&1===i?l=e[c]:1===r&&0===i&&(l=xr.indexOf(c));if(void 0===l){if(!a.glyphNames&&a.hasIncludedToUnicodeMap&&!(this.toUnicode instanceof IdentityToUnicodeMap)){const e=this.toUnicode.get(t);e&&(l=e.codePointAt(0))}if(void 0===l)continue}for(const e of n)if(e.charCode===l){w[t]=e.glyphId;break}}}else if(0===r){for(const e of n)w[e.charCode]=e.glyphId;c=!0}else if(3===r&&0===i)for(const e of n){let t=e.charCode;t>=61440&&t<=61695&&(t&=255);w[t]=e.glyphId}else for(const e of n)w[e.charCode]=e.glyphId;if(a.glyphNames&&(o.length||this.differences.length))for(let e=0;e<256;++e){if(!c&&void 0!==w[e])continue;const t=this.differences[e]||o[e];if(!t)continue;const r=a.glyphNames.indexOf(t);r>0&&hasGlyph(r)&&(w[e]=r)}}0===w.length&&(w[0]=0);let S=d-1;f||(S=0);if(!a.cssFontInfo){const e=adjustMapping(w,hasGlyph,S,this.toUnicode);this.toFontChar=e.toFontChar;s.cmap={tag:"cmap",data:createCmapTable(e.charCodeToGlyphId,e.toUnicodeExtraMap,d)};s["OS/2"]&&function validateOS2Table(e,t){t.pos=(t.start||0)+e.offset;const a=t.getUint16();t.skip(60);const r=t.getUint16();if(a<4&&768&r)return!1;if(t.getUint16()>t.getUint16())return!1;t.skip(6);if(0===t.getUint16())return!1;e.data[8]=e.data[9]=0;return!0}(s["OS/2"],t)||(s["OS/2"]={tag:"OS/2",data:createOS2Table(a,e.charCodeToGlyphId,y)})}if(!l)try{c=new Stream(s["CFF "].data);o=new CFFParser(c,a,Rr).parse();o.duplicateFirstGlyph();const e=new CFFCompiler(o);s["CFF "].data=e.compile()}catch{warn("Failed to compile font "+a.loadedName)}if(s.name){const[t,r]=readNameTable(s.name);s.name.data=createNameTable(e,t);this.psName=t[0][6]||null;a.composite||function adjustTrueTypeToUnicode(e,t,a){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(e.hasEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;if(!t)return;if(0===a.length)return;if(e.defaultEncoding===kr)return;for(const e of a)if(!isWinNameRecord(e))return;const r=kr,i=[],n=Fr();for(const e in r){const t=r[e];if(""===t)continue;const a=n[t];void 0!==a&&(i[e]=String.fromCharCode(a))}i.length>0&&e.toUnicode.amend(i)}(a,this.isSymbolicFont,r)}else s.name={tag:"name",data:createNameTable(this.name)};const x=new OpenTypeFileBuilder(n.version);for(const e in s)x.addTable(e,s[e].data);return x.toArray()}convert(e,a,r){r.fixedPitch=!1;r.builtInEncoding&&function adjustType1ToUnicode(e,t){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(t===e.defaultEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;const a=[],r=Fr();for(const i in t){if(e.hasEncoding&&(e.baseEncodingName||void 0!==e.differences[i]))continue;const n=getUnicodeForGlyph(t[i],r);-1!==n&&(a[i]=String.fromCharCode(n))}a.length>0&&e.toUnicode.amend(a)}(r,r.builtInEncoding);let i=1;a instanceof CFFFont&&(i=a.numGlyphs-1);const n=a.getGlyphMapping(r);let s=null,o=n,c=null;if(!r.cssFontInfo){s=adjustMapping(n,a.hasGlyphId.bind(a),i,this.toUnicode);this.toFontChar=s.toFontChar;o=s.charCodeToGlyphId;c=s.toUnicodeExtraMap}const l=a.numGlyphs;function getCharCodes(e,t){let a=null;for(const r in e)t===e[r]&&(a||=[]).push(0|r);return a}function createCharCode(e,t){for(const a in e)if(t===e[a])return 0|a;s.charCodeToGlyphId[s.nextAvailableFontCharCode]=t;return s.nextAvailableFontCharCode++}const h=a.seacs;if(s&&h?.length){const e=r.fontMatrix||t,i=a.getCharset(),o=Object.create(null);for(let t in h){t|=0;const a=h[t],r=Ar[a[2]],c=Ar[a[3]],l=i.indexOf(r),u=i.indexOf(c);if(l<0||u<0)continue;const d={x:a[0]*e[0]+a[1]*e[2]+e[4],y:a[0]*e[1]+a[1]*e[3]+e[5]},f=getCharCodes(n,t);if(f)for(const e of f){const t=s.charCodeToGlyphId,a=createCharCode(t,l),r=createCharCode(t,u);o[e]={baseFontCharCode:a,accentFontCharCode:r,accentOffset:d}}}r.seacMap=o}const u=r.fontMatrix?1/Math.max(...r.fontMatrix.slice(0,4).map(Math.abs)):1e3,d=new OpenTypeFileBuilder("OTTO");d.addTable("CFF ",a.data);d.addTable("OS/2",createOS2Table(r,o));d.addTable("cmap",createCmapTable(o,c,l));d.addTable("head","\0\0\0\0\0\0\0\0\0\0_<õ\0\0"+safeString16(u)+"\0\0\0\0ž\v~'\0\0\0\0ž\v~'\0\0"+safeString16(r.descent)+"ÿ"+safeString16(r.ascent)+string16(r.italicAngle?2:0)+"\0\0\0\0\0\0\0");d.addTable("hhea","\0\0\0"+safeString16(r.ascent)+safeString16(r.descent)+"\0\0ÿÿ\0\0\0\0\0\0"+safeString16(r.capHeight)+safeString16(Math.tan(r.italicAngle)*r.xHeight)+"\0\0\0\0\0\0\0\0\0\0\0\0"+string16(l));d.addTable("hmtx",function fontFieldsHmtx(){const e=a.charstrings,t=a.cff?a.cff.widths:null;let r="\0\0\0\0";for(let a=1,i=l;a=65520&&e<=65535?0:e>=62976&&e<=63743?Or()[e]||e:173===e?45:e}(a)}this.isType3Font&&(i=a);let h=null;if(this.seacMap?.[e]){l=!0;const t=this.seacMap[e];a=t.baseFontCharCode;h={fontChar:String.fromCodePoint(t.accentFontCharCode),offset:t.accentOffset}}let u="";"number"==typeof a&&(a<=1114111?u=String.fromCodePoint(a):warn(`charToGlyph - invalid fontCharCode: ${a}`));if(this.missingFile&&this.vertical&&1===u.length){const e=_r()[u.charCodeAt(0)];e&&(u=c=String.fromCharCode(e))}n=new fonts_Glyph(e,u,c,h,r,o,i,t,l);return this._glyphCache[e]=n}charsToGlyphs(e){let t=this._charsCache[e];if(t)return t;t=[];if(this.cMap){const a=Object.create(null),r=e.length;let i=0;for(;it.length%2==1,r=this.toUnicode instanceof IdentityToUnicodeMap?e=>this.toUnicode.charCodeOf(e):e=>this.toUnicode.charCodeOf(String.fromCodePoint(e));for(let i=0,n=e.length;i55295&&(n<57344||n>65533)&&i++;if(this.toUnicode){const e=r(n);if(-1!==e){if(hasCurrentBufErrors()){t.push(a.join(""));a.length=0}for(let t=(this.cMap?this.cMap.getCharCodeLength(e):1)-1;t>=0;t--)a.push(String.fromCharCode(e>>8*t&255));continue}}if(!hasCurrentBufErrors()){t.push(a.join(""));a.length=0}a.push(String.fromCodePoint(n))}t.push(a.join(""));return t}}class ErrorFont{constructor(e){this.error=e;this.loadedName="g_font_error";this.missingFile=!0}charsToGlyphs(){return[]}encodeString(e){return[e]}exportData(){return{error:this.error}}}const xi=2,Ai=3,ki=4,Ci=5,vi=6,Fi=7;class Pattern{constructor(){unreachable("Cannot initialize Pattern.")}static parseShading(e,t,a,r,i,n){const s=e instanceof BaseStream?e.dict:e,o=s.get("ShadingType");try{switch(o){case xi:case Ai:return new RadialAxialShading(s,t,a,r,i,n);case ki:case Ci:case vi:case Fi:return new MeshShading(e,t,a,r,i,n);default:throw new FormatError("Unsupported ShadingType: "+o)}}catch(e){if(e instanceof MissingDataException)throw e;warn(e);return new DummyShading}}}class BaseShading{static SMALL_NUMBER=1e-6;getIR(){unreachable("Abstract method `getIR` called.")}}class RadialAxialShading extends BaseShading{constructor(e,t,a,r,i,n){super();this.shadingType=e.get("ShadingType");let s=0;this.shadingType===xi?s=4:this.shadingType===Ai&&(s=6);this.coordsArr=e.getArray("Coords");if(!isNumberArray(this.coordsArr,s))throw new FormatError("RadialAxialShading: Invalid /Coords array.");const o=ColorSpaceUtils.parse({cs:e.getRaw("CS")||e.getRaw("ColorSpace"),xref:t,resources:a,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n});this.bbox=lookupNormalRect(e.getArray("BBox"),null);let c=0,l=1;const h=e.getArray("Domain");isNumberArray(h,2)&&([c,l]=h);let u=!1,d=!1;const f=e.getArray("Extend");(function isBooleanArray(e,t){return Array.isArray(e)&&(null===t||e.length===t)&&e.every((e=>"boolean"==typeof e))})(f,2)&&([u,d]=f);if(!(this.shadingType!==Ai||u&&d)){const[e,t,a,r,i,n]=this.coordsArr,s=Math.hypot(e-r,t-i);a<=n+s&&n<=a+s&&warn("Unsupported radial gradient.")}this.extendStart=u;this.extendEnd=d;const g=e.getRaw("Function"),p=r.create(g,!0),m=(l-c)/840,b=this.colorStops=[];if(c>=l||m<=0){info("Bad shading domain.");return}const y=new Float32Array(o.numComps),w=new Float32Array(1);let S=0;w[0]=c;p(w,0,y,0);const x=new Uint8ClampedArray(3);o.getRgb(y,0,x);let[k,C,v]=x;b.push([0,Util.makeHexColor(k,C,v)]);let F=1;w[0]=c+m;p(w,0,y,0);o.getRgb(y,0,x);let[O,T,M]=x,D=O-k+1,R=T-C+1,N=M-v+1,E=O-k-1,L=T-C-1,j=M-v-1;for(let e=2;e<840;e++){w[0]=c+e*m;p(w,0,y,0);o.getRgb(y,0,x);const[t,a,r]=x,i=e-S;D=Math.min(D,(t-k+1)/i);R=Math.min(R,(a-C+1)/i);N=Math.min(N,(r-v+1)/i);E=Math.max(E,(t-k-1)/i);L=Math.max(L,(a-C-1)/i);j=Math.max(j,(r-v-1)/i);if(!(E<=D&&L<=R&&j<=N)){const e=Util.makeHexColor(O,T,M);b.push([F/840,e]);D=t-O+1;R=a-T+1;N=r-M+1;E=t-O-1;L=a-T-1;j=r-M-1;S=F;k=O;C=T;v=M}F=e;O=t;T=a;M=r}b.push([1,Util.makeHexColor(O,T,M)]);let _="transparent";e.has("Background")&&(_=o.getRgbHex(e.get("Background"),0));if(!u){b.unshift([0,_]);b[1][0]+=BaseShading.SMALL_NUMBER}if(!d){b.at(-1)[0]-=BaseShading.SMALL_NUMBER;b.push([1,_])}this.colorStops=b}getIR(){const{coordsArr:e,shadingType:t}=this;let a,r,i,n,s;if(t===xi){r=[e[0],e[1]];i=[e[2],e[3]];n=null;s=null;a="axial"}else if(t===Ai){r=[e[0],e[1]];i=[e[3],e[4]];n=e[2];s=e[5];a="radial"}else unreachable(`getPattern type unknown: ${t}`);return["RadialAxial",a,this.bbox,this.colorStops,r,i,n,s]}}class MeshStreamReader{constructor(e,t){this.stream=e;this.context=t;this.buffer=0;this.bufferLength=0;const a=t.numComps;this.tmpCompsBuf=new Float32Array(a);const r=t.colorSpace.numComps;this.tmpCsCompsBuf=t.colorFn?new Float32Array(r):this.tmpCompsBuf}get hasData(){if(this.stream.end)return this.stream.pos0)return!0;const e=this.stream.getByte();if(e<0)return!1;this.buffer=e;this.bufferLength=8;return!0}readBits(e){const{stream:t}=this;let{buffer:a,bufferLength:r}=this;if(32===e){if(0===r)return t.getInt32()>>>0;a=a<<24|t.getByte()<<16|t.getByte()<<8|t.getByte();const e=t.getByte();this.buffer=e&(1<>r)>>>0}if(8===e&&0===r)return t.getByte();for(;r>r}align(){this.buffer=0;this.bufferLength=0}readFlag(){return this.readBits(this.context.bitsPerFlag)}readCoordinate(){const{bitsPerCoordinate:e,decode:t}=this.context,a=this.readBits(e),r=this.readBits(e),i=e<32?1/((1<n?n:e;t=t>s?s:t;a=ae*i[t])):a;let s,o=-2;const c=[];for(const[e,t]of r.map(((e,t)=>[e,t])).sort((([e],[t])=>e-t)))if(-1!==e)if(e===o+1){s.push(n[t]);o+=1}else{o=e;s=[n[t]];c.push(e,s)}return c}(e),a=new Dict(null);a.set("BaseFont",Name.get(e));a.set("Type",Name.get("Font"));a.set("Subtype",Name.get("CIDFontType2"));a.set("Encoding",Name.get("Identity-H"));a.set("CIDToGIDMap",Name.get("Identity"));a.set("W",t);a.set("FirstChar",t[0]);a.set("LastChar",t.at(-2)+t.at(-1).length-1);const r=new Dict(null);a.set("FontDescriptor",r);const i=new Dict(null);i.set("Ordering","Identity");i.set("Registry","Adobe");i.set("Supplement",0);a.set("CIDSystemInfo",i);return a}class PostScriptParser{constructor(e){this.lexer=e;this.operators=[];this.token=null;this.prev=null}nextToken(){this.prev=this.token;this.token=this.lexer.getToken()}accept(e){if(this.token.type===e){this.nextToken();return!0}return!1}expect(e){if(this.accept(e))return!0;throw new FormatError(`Unexpected symbol: found ${this.token.type} expected ${e}.`)}parse(){this.nextToken();this.expect(yn.LBRACE);this.parseBlock();this.expect(yn.RBRACE);return this.operators}parseBlock(){for(;;)if(this.accept(yn.NUMBER))this.operators.push(this.prev.value);else if(this.accept(yn.OPERATOR))this.operators.push(this.prev.value);else{if(!this.accept(yn.LBRACE))return;this.parseCondition()}}parseCondition(){const e=this.operators.length;this.operators.push(null,null);this.parseBlock();this.expect(yn.RBRACE);if(this.accept(yn.IF)){this.operators[e]=this.operators.length;this.operators[e+1]="jz"}else{if(!this.accept(yn.LBRACE))throw new FormatError("PS Function: error parsing conditional.");{const t=this.operators.length;this.operators.push(null,null);const a=this.operators.length;this.parseBlock();this.expect(yn.RBRACE);this.expect(yn.IFELSE);this.operators[t]=this.operators.length;this.operators[t+1]="j";this.operators[e]=a;this.operators[e+1]="jz"}}}}const yn={LBRACE:0,RBRACE:1,NUMBER:2,OPERATOR:3,IF:4,IFELSE:5};class PostScriptToken{static get opCache(){return shadow(this,"opCache",Object.create(null))}constructor(e,t){this.type=e;this.value=t}static getOperator(e){return PostScriptToken.opCache[e]||=new PostScriptToken(yn.OPERATOR,e)}static get LBRACE(){return shadow(this,"LBRACE",new PostScriptToken(yn.LBRACE,"{"))}static get RBRACE(){return shadow(this,"RBRACE",new PostScriptToken(yn.RBRACE,"}"))}static get IF(){return shadow(this,"IF",new PostScriptToken(yn.IF,"IF"))}static get IFELSE(){return shadow(this,"IFELSE",new PostScriptToken(yn.IFELSE,"IFELSE"))}}class PostScriptLexer{constructor(e){this.stream=e;this.nextChar();this.strBuf=[]}nextChar(){return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(t<0)return wa;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return new PostScriptToken(yn.NUMBER,this.getNumber());case 123:this.nextChar();return PostScriptToken.LBRACE;case 125:this.nextChar();return PostScriptToken.RBRACE}const a=this.strBuf;a.length=0;a[0]=String.fromCharCode(t);for(;(t=this.nextChar())>=0&&(t>=65&&t<=90||t>=97&&t<=122);)a.push(String.fromCharCode(t));const r=a.join("");switch(r.toLowerCase()){case"if":return PostScriptToken.IF;case"ifelse":return PostScriptToken.IFELSE;default:return PostScriptToken.getOperator(r)}}getNumber(){let e=this.currentChar;const t=this.strBuf;t.length=0;t[0]=String.fromCharCode(e);for(;(e=this.nextChar())>=0&&(e>=48&&e<=57||45===e||46===e);)t.push(String.fromCharCode(e));const a=parseFloat(t.join(""));if(isNaN(a))throw new FormatError(`Invalid floating point number: ${a}`);return a}}class BaseLocalCache{constructor(e){this._onlyRefs=!0===e?.onlyRefs;if(!this._onlyRefs){this._nameRefMap=new Map;this._imageMap=new Map}this._imageCache=new RefSetCache}getByName(e){this._onlyRefs&&unreachable("Should not call `getByName` method.");const t=this._nameRefMap.get(e);return t?this.getByRef(t):this._imageMap.get(e)||null}getByRef(e){return this._imageCache.get(e)||null}set(e,t,a){unreachable("Abstract method `set` called.")}}class LocalImageCache extends BaseLocalCache{set(e,t=null,a){if("string"!=typeof e)throw new Error('LocalImageCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalColorSpaceCache extends BaseLocalCache{set(e=null,t=null,a){if("string"!=typeof e&&!t)throw new Error('LocalColorSpaceCache.set - expected "name" and/or "ref" argument.');if(t){if(this._imageCache.has(t))return;null!==e&&this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalFunctionCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('LocalFunctionCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class LocalGStateCache extends BaseLocalCache{set(e,t=null,a){if("string"!=typeof e)throw new Error('LocalGStateCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalTilingPatternCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('LocalTilingPatternCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class RegionalImageCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('RegionalImageCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class GlobalColorSpaceCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('GlobalColorSpaceCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}clear(){this._imageCache.clear()}}class GlobalImageCache{static NUM_PAGES_THRESHOLD=2;static MIN_IMAGES_TO_CACHE=10;static MAX_BYTE_SIZE=5e7;#H=new RefSet;constructor(){this._refCache=new RefSetCache;this._imageCache=new RefSetCache}get#W(){let e=0;for(const t of this._imageCache)e+=t.byteSize;return e}get#z(){return!(this._imageCache.size+e)):null}class PDFFunction{static getSampleArray(e,t,a,r){let i,n,s=1;for(i=0,n=e.length;i>c)*h;l&=(1<0&&(d=n[u-1]);let f=a[1];u>1,c=r.length>>1,l=new PostScriptEvaluator(s),h=Object.create(null);let u=8192;const d=new Float32Array(c);return function constructPostScriptFn(e,t,a,r){let n,s,f="";const g=d;for(n=0;ne&&(s=e)}m[n]=s}if(u>0){u--;h[f]=m}a.set(m,r)}}}function isPDFFunction(e){let t;if(e instanceof Dict)t=e;else{if(!(e instanceof BaseStream))return!1;t=e.dict}return t.has("FunctionType")}class PostScriptStack{static MAX_STACK_SIZE=100;constructor(e){this.stack=e?Array.from(e):[]}push(e){if(this.stack.length>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");this.stack.push(e)}pop(){if(this.stack.length<=0)throw new Error("PostScript function stack underflow.");return this.stack.pop()}copy(e){if(this.stack.length+e>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");const t=this.stack;for(let a=t.length-e,r=e-1;r>=0;r--,a++)t.push(t[a])}index(e){this.push(this.stack[this.stack.length-e-1])}roll(e,t){const a=this.stack,r=a.length-e,i=a.length-1,n=r+(t-Math.floor(t/e)*e);for(let e=r,t=i;e0?t.push(s<>o);break;case"ceiling":s=t.pop();t.push(Math.ceil(s));break;case"copy":s=t.pop();t.copy(s);break;case"cos":s=t.pop();t.push(Math.cos(s%360/180*Math.PI));break;case"cvi":s=0|t.pop();t.push(s);break;case"cvr":break;case"div":o=t.pop();s=t.pop();t.push(s/o);break;case"dup":t.copy(1);break;case"eq":o=t.pop();s=t.pop();t.push(s===o);break;case"exch":t.roll(2,1);break;case"exp":o=t.pop();s=t.pop();t.push(s**o);break;case"false":t.push(!1);break;case"floor":s=t.pop();t.push(Math.floor(s));break;case"ge":o=t.pop();s=t.pop();t.push(s>=o);break;case"gt":o=t.pop();s=t.pop();t.push(s>o);break;case"idiv":o=t.pop();s=t.pop();t.push(s/o|0);break;case"index":s=t.pop();t.index(s);break;case"le":o=t.pop();s=t.pop();t.push(s<=o);break;case"ln":s=t.pop();t.push(Math.log(s));break;case"log":s=t.pop();t.push(Math.log10(s));break;case"lt":o=t.pop();s=t.pop();t.push(s=t?new AstLiteral(t):e.max<=t?e:new AstMin(e,t)}class PostScriptCompiler{compile(e,t,a){const r=[],i=[],n=t.length>>1,s=a.length>>1;let o,c,l,h,u,d,f,g,p=0;for(let e=0;et.min){o.unshift("Math.max(",n,", ");o.push(")")}if(s4){r=!0;t=0}else{r=!1;t=1}const c=[];for(n=0;n=0&&"ET"===An[e];--e)An[e]="EN";for(let e=n+1;e0&&(t=An[n-1]);let a=u;e+1g&&isOdd(g)&&(m=g)}for(g=p;g>=m;--g){let e=-1;for(n=0,s=c.length;n=0){reverseValues(xn,e,n);e=-1}}else e<0&&(e=n);e>=0&&reverseValues(xn,e,c.length)}for(n=0,s=xn.length;n"!==e||(xn[n]="")}return createBidiText(xn.join(""),r)}const kn={style:"normal",weight:"normal"},Cn={style:"normal",weight:"bold"},vn={style:"italic",weight:"normal"},Fn={style:"italic",weight:"bold"},In=new Map([["Times-Roman",{local:["Times New Roman","Times-Roman","Times","Liberation Serif","Nimbus Roman","Nimbus Roman L","Tinos","Thorndale","TeX Gyre Termes","FreeSerif","Linux Libertine O","Libertinus Serif","DejaVu Serif","Bitstream Vera Serif","Ubuntu"],style:kn,ultimate:"serif"}],["Times-Bold",{alias:"Times-Roman",style:Cn,ultimate:"serif"}],["Times-Italic",{alias:"Times-Roman",style:vn,ultimate:"serif"}],["Times-BoldItalic",{alias:"Times-Roman",style:Fn,ultimate:"serif"}],["Helvetica",{local:["Helvetica","Helvetica Neue","Arial","Arial Nova","Liberation Sans","Arimo","Nimbus Sans","Nimbus Sans L","A030","TeX Gyre Heros","FreeSans","DejaVu Sans","Albany","Bitstream Vera Sans","Arial Unicode MS","Microsoft Sans Serif","Apple Symbols","Cantarell"],path:"LiberationSans-Regular.ttf",style:kn,ultimate:"sans-serif"}],["Helvetica-Bold",{alias:"Helvetica",path:"LiberationSans-Bold.ttf",style:Cn,ultimate:"sans-serif"}],["Helvetica-Oblique",{alias:"Helvetica",path:"LiberationSans-Italic.ttf",style:vn,ultimate:"sans-serif"}],["Helvetica-BoldOblique",{alias:"Helvetica",path:"LiberationSans-BoldItalic.ttf",style:Fn,ultimate:"sans-serif"}],["Courier",{local:["Courier","Courier New","Liberation Mono","Nimbus Mono","Nimbus Mono L","Cousine","Cumberland","TeX Gyre Cursor","FreeMono","Linux Libertine Mono O","Libertinus Mono"],style:kn,ultimate:"monospace"}],["Courier-Bold",{alias:"Courier",style:Cn,ultimate:"monospace"}],["Courier-Oblique",{alias:"Courier",style:vn,ultimate:"monospace"}],["Courier-BoldOblique",{alias:"Courier",style:Fn,ultimate:"monospace"}],["ArialBlack",{local:["Arial Black"],style:{style:"normal",weight:"900"},fallback:"Helvetica-Bold"}],["ArialBlack-Bold",{alias:"ArialBlack"}],["ArialBlack-Italic",{alias:"ArialBlack",style:{style:"italic",weight:"900"},fallback:"Helvetica-BoldOblique"}],["ArialBlack-BoldItalic",{alias:"ArialBlack-Italic"}],["ArialNarrow",{local:["Arial Narrow","Liberation Sans Narrow","Helvetica Condensed","Nimbus Sans Narrow","TeX Gyre Heros Cn"],style:kn,fallback:"Helvetica"}],["ArialNarrow-Bold",{alias:"ArialNarrow",style:Cn,fallback:"Helvetica-Bold"}],["ArialNarrow-Italic",{alias:"ArialNarrow",style:vn,fallback:"Helvetica-Oblique"}],["ArialNarrow-BoldItalic",{alias:"ArialNarrow",style:Fn,fallback:"Helvetica-BoldOblique"}],["Calibri",{local:["Calibri","Carlito"],style:kn,fallback:"Helvetica"}],["Calibri-Bold",{alias:"Calibri",style:Cn,fallback:"Helvetica-Bold"}],["Calibri-Italic",{alias:"Calibri",style:vn,fallback:"Helvetica-Oblique"}],["Calibri-BoldItalic",{alias:"Calibri",style:Fn,fallback:"Helvetica-BoldOblique"}],["Wingdings",{local:["Wingdings","URW Dingbats"],style:kn}],["Wingdings-Regular",{alias:"Wingdings"}],["Wingdings-Bold",{alias:"Wingdings"}]]),On=new Map([["Arial-Black","ArialBlack"]]);function getFamilyName(e){const t=new Set(["thin","extralight","ultralight","demilight","semilight","light","book","regular","normal","medium","demibold","semibold","bold","extrabold","ultrabold","black","heavy","extrablack","ultrablack","roman","italic","oblique","ultracondensed","extracondensed","condensed","semicondensed","normal","semiexpanded","expanded","extraexpanded","ultraexpanded","bolditalic"]);return e.split(/[- ,+]+/g).filter((e=>!t.has(e.toLowerCase()))).join(" ")}function generateFont({alias:e,local:t,path:a,fallback:r,style:i,ultimate:n},s,o,c=!0,l=!0,h=""){const u={style:null,ultimate:null};if(t){const e=h?` ${h}`:"";for(const a of t)s.push(`local(${a}${e})`)}if(e){const t=In.get(e),n=h||function getStyleToAppend(e){switch(e){case Cn:return"Bold";case vn:return"Italic";case Fn:return"Bold Italic";default:if("bold"===e?.weight)return"Bold";if("italic"===e?.style)return"Italic"}return""}(i);Object.assign(u,generateFont(t,s,o,c&&!r,l&&!a,n))}i&&(u.style=i);n&&(u.ultimate=n);if(c&&r){const e=In.get(r),{ultimate:t}=generateFont(e,s,o,c,l&&!a,h);u.ultimate||=t}l&&a&&o&&s.push(`url(${o}${a})`);return u}function getFontSubstitution(e,t,a,r,i,n){if(r.startsWith("InvalidPDFjsFont_"))return null;"TrueType"!==n&&"Type1"!==n||!/^[A-Z]{6}\+/.test(r)||(r=r.slice(7));const s=r=normalizeFontName(r);let o=e.get(s);if(o)return o;let c=In.get(r);if(!c)for(const[e,t]of On)if(r.startsWith(e)){r=`${t}${r.substring(e.length)}`;c=In.get(r);break}let l=!1;if(!c){c=In.get(i);l=!0}const h=`${t.getDocId()}_s${t.createFontId()}`;if(!c){if(!validateFontName(r)){warn(`Cannot substitute the font because of its name: ${r}`);e.set(s,null);return null}const t=/bold/gi.test(r),a=/oblique|italic/gi.test(r),i=t&&a&&Fn||t&&Cn||a&&vn||kn;o={css:`"${getFamilyName(r)}",${h}`,guessFallback:!0,loadedName:h,baseFontName:r,src:`local(${r})`,style:i};e.set(s,o);return o}const u=[];l&&validateFontName(r)&&u.push(`local(${r})`);const{style:d,ultimate:f}=generateFont(c,u,a),g=null===f,p=g?"":`,${f}`;o={css:`"${getFamilyName(r)}",${h}${p}`,guessFallback:g,loadedName:h,baseFontName:r,src:u.join(","),style:d};e.set(s,o);return o}const Tn=3285377520,Mn=4294901760,Dn=65535;class MurmurHash3_64{constructor(e){this.h1=e?4294967295&e:Tn;this.h2=e?4294967295&e:Tn}update(e){let t,a;if("string"==typeof e){t=new Uint8Array(2*e.length);a=0;for(let r=0,i=e.length;r>>8;t[a++]=255&i}}}else{if(!ArrayBuffer.isView(e))throw new Error("Invalid data format, must be a string or TypedArray.");t=e.slice();a=t.byteLength}const r=a>>2,i=a-4*r,n=new Uint32Array(t.buffer,0,r);let s=0,o=0,c=this.h1,l=this.h2;const h=3432918353,u=461845907,d=11601,f=13715;for(let e=0;e>>17;s=s*u&Mn|s*f&Dn;c^=s;c=c<<13|c>>>19;c=5*c+3864292196}else{o=n[e];o=o*h&Mn|o*d&Dn;o=o<<15|o>>>17;o=o*u&Mn|o*f&Dn;l^=o;l=l<<13|l>>>19;l=5*l+3864292196}s=0;switch(i){case 3:s^=t[4*r+2]<<16;case 2:s^=t[4*r+1]<<8;case 1:s^=t[4*r];s=s*h&Mn|s*d&Dn;s=s<<15|s>>>17;s=s*u&Mn|s*f&Dn;1&r?c^=s:l^=s}this.h1=c;this.h2=l}hexdigest(){let e=this.h1,t=this.h2;e^=t>>>1;e=3981806797*e&Mn|36045*e&Dn;t=4283543511*t&Mn|(2950163797*(t<<16|e>>>16)&Mn)>>>16;e^=t>>>1;e=444984403*e&Mn|60499*e&Dn;t=3301882366*t&Mn|(3120437893*(t<<16|e>>>16)&Mn)>>>16;e^=t>>>1;return(e>>>0).toString(16).padStart(8,"0")+(t>>>0).toString(16).padStart(8,"0")}}function resizeImageMask(e,t,a,r,i,n){const s=i*n;let o;o=t<=8?new Uint8Array(s):t<=16?new Uint16Array(s):new Uint32Array(s);const c=a/i,l=r/n;let h,u,d,f,g=0;const p=new Uint16Array(i),m=a;for(h=0;h0&&Number.isInteger(a.height)&&a.height>0&&(a.width!==f||a.height!==g)){warn("PDFImage - using the Width/Height of the image data, rather than the image dictionary.");f=a.width;g=a.height}else{const e="number"==typeof f&&f>0,t="number"==typeof g&&g>0;if(!e||!t){if(!a.fallbackDims)throw new FormatError(`Invalid image width: ${f} or height: ${g}`);warn("PDFImage - using the Width/Height of the parent image, for SMask/Mask data.");e||(f=a.fallbackDims.width);t||(g=a.fallbackDims.height)}}this.width=f;this.height=g;this.interpolate=h.get("I","Interpolate");this.imageMask=h.get("IM","ImageMask")||!1;this.matte=h.get("Matte")||!1;let p=a.bitsPerComponent;if(!p){p=h.get("BPC","BitsPerComponent");if(!p){if(!this.imageMask)throw new FormatError(`Bits per component missing in image: ${this.imageMask}`);p=1}}this.bpc=p;if(!this.imageMask){let i=h.getRaw("CS")||h.getRaw("ColorSpace");const n=!!i;if(n)this.jpxDecoderOptions?.smaskInData&&(i=Name.get("DeviceRGBA"));else if(this.jpxDecoderOptions)i=Name.get("DeviceRGBA");else switch(a.numComps){case 1:i=Name.get("DeviceGray");break;case 3:i=Name.get("DeviceRGB");break;case 4:i=Name.get("DeviceCMYK");break;default:throw new Error(`Images with ${a.numComps} color components not supported.`)}this.colorSpace=ColorSpaceUtils.parse({cs:i,xref:e,resources:r?t:null,pdfFunctionFactory:o,globalColorSpaceCache:c,localColorSpaceCache:l});this.numComps=this.colorSpace.numComps;if(this.jpxDecoderOptions){this.jpxDecoderOptions.numComponents=n?this.numComps:0;this.jpxDecoderOptions.isIndexedColormap="Indexed"===this.colorSpace.name}}this.decode=h.getArray("D","Decode");this.needsDecode=!1;if(this.decode&&(this.colorSpace&&!this.colorSpace.isDefaultDecode(this.decode,p)||s&&!ColorSpace.isDefaultDecode(this.decode,1))){this.needsDecode=!0;const e=(1<0,c=(r+7>>3)*i,l=e.getBytes(c),h=1===r&&1===i&&o===(0===l.length||!!(128&l[0]));if(h)return{isSingleOpaquePixel:h};if(t){if(ImageResizer.needsToBeResized(r,i)){const e=new Uint8ClampedArray(r*i*4);convertBlackAndWhiteToRGBA({src:l,dest:e,width:r,height:i,nonBlackColor:0,inverseDecode:o});return ImageResizer.createImage({kind:v,data:e,width:r,height:i,interpolate:n})}const e=new OffscreenCanvas(r,i),t=e.getContext("2d"),a=t.createImageData(r,i);convertBlackAndWhiteToRGBA({src:l,dest:a.data,width:r,height:i,nonBlackColor:0,inverseDecode:o});t.putImageData(a,0,0);return{data:null,width:r,height:i,interpolate:n,bitmap:e.transferToImageBitmap()}}const u=l.byteLength;let d;if(e instanceof DecodeStream&&(!o||c===u))d=l;else if(o){d=new Uint8Array(c);d.set(l);d.fill(255,u)}else d=new Uint8Array(l);if(o)for(let e=0;e>7&1;s[d+1]=u>>6&1;s[d+2]=u>>5&1;s[d+3]=u>>4&1;s[d+4]=u>>3&1;s[d+5]=u>>2&1;s[d+6]=u>>1&1;s[d+7]=1&u;d+=8}if(d>=1}}}}else{let a=0;u=0;for(d=0,h=n;d>r;i<0?i=0:i>l&&(i=l);s[d]=i;u&=(1<s[r+1]){t=255;break}}o[h]=t}}}if(o)for(h=0,d=3,u=t*r;h>3,h=t&&ImageResizer.needsToBeResized(a,r);if(!this.smask&&!this.mask&&"DeviceRGBA"===this.colorSpace.name){i.kind=v;const e=i.data=await this.getImageBytes(o*s*4,{});return t?h?ImageResizer.createImage(i,!1):this.createBitmap(v,a,r,e):i}if(!e){let e;"DeviceGray"===this.colorSpace.name&&1===c?e=k:"DeviceRGB"!==this.colorSpace.name||8!==c||this.needsDecode||(e=C);if(e&&!this.smask&&!this.mask&&a===s&&r===o){const n=await this.#$(s,o);if(n)return n;const c=await this.getImageBytes(o*l,{});if(t)return h?ImageResizer.createImage({data:c,kind:e,width:a,height:r,interpolate:this.interpolate},this.needsDecode):this.createBitmap(e,s,o,c);i.kind=e;i.data=c;if(this.needsDecode){assert(e===k,"PDFImage.createImageData: The image must be grayscale.");const t=i.data;for(let e=0,a=t.length;e>3,s=await this.getImageBytes(r*n,{internal:!0}),o=this.getComponents(s);let c,l;if(1===i){l=a*r;if(this.needsDecode)for(c=0;c0&&r[0].count++}class TimeSlotManager{static TIME_SLOT_DURATION_MS=20;static CHECK_TIME_EVERY=100;constructor(){this.reset()}check(){if(++this.checkedo){const e="Image exceeded maximum allowed size and was removed.";if(!c)throw new Error(e);warn(e);return}let g;h.has("OC")&&(g=await this.parseMarkedContentProps(h.get("OC"),e));let p,m,b;if(h.get("IM","ImageMask")||!1){p=await PDFImage.createMask({image:t,isOffscreenCanvasSupported:l&&!this.parsingType3Font});if(p.isSingleOpaquePixel){m=ta;b=[];r.addImageOps(m,b,g);if(i){const e={fn:m,args:b,optionalContent:g};n.set(i,u,e);u&&this._regionalImageCache.set(null,u,e)}return}if(this.parsingType3Font){b=function compileType3Glyph({data:e,width:t,height:a}){if(t>1e3||a>1e3)return null;const r=new Uint8Array([0,2,4,0,1,0,5,4,8,10,0,8,0,2,1,0]),i=t+1,n=new Uint8Array(i*(a+1));let s,o,c;const l=t+7&-8,h=new Uint8Array(l*a);let u=0;for(const t of e){let e=128;for(;e>0;){h[u++]=t&e?0:255;e>>=1}}let d=0;u=0;if(0!==h[u]){n[0]=1;++d}for(o=1;o>2)+(h[u+1]?4:0)+(h[u-l+1]?8:0);if(r[e]){n[c+o]=r[e];++d}u++}if(h[u-l]!==h[u]){n[c+o]=h[u]?2:4;++d}if(d>1e3)return null}u=l*(a-1);c=s*i;if(0!==h[u]){n[c]=8;++d}for(o=1;o1e3)return null;const f=new Int32Array([0,i,-1,0,-i,0,0,0,1]),g=[],{a:p,b:m,c:b,d:y,e:w,f:S}=(new DOMMatrix).scaleSelf(1/t,-1/a).translateSelf(0,-a);for(s=0;d&&s<=a;s++){let e=s*i;const a=e+t;for(;e>4;n[e]&=l>>2|l<<2}r=e%i;o=e/i|0;g.push(oa,p*r+b*o+w,m*r+y*o+S);n[e]||--d}while(c!==e);--s}return[na,[new Float32Array(g)],new Float32Array([0,0,t,a])]}(p);if(b){r.addImageOps(aa,b,g);return}warn("Cannot compile Type3 glyph.");r.addImageOps(Vt,[p],g);return}const e=`mask_${this.idFactory.createObjId()}`;r.addDependency(e);p.dataLen=p.bitmap?p.width*p.height*4:p.data.length;this._sendImgData(e,p);m=Vt;b=[{data:e,width:p.width,height:p.height,interpolate:p.interpolate,count:1}];r.addImageOps(m,b,g);if(i){const t={objId:e,fn:m,args:b,optionalContent:g};n.set(i,u,t);u&&this._regionalImageCache.set(null,u,t)}return}const y=h.has("SMask")||h.has("Mask");if(a&&d+f<200&&!y){try{const i=new PDFImage({xref:this.xref,res:e,image:t,isInline:a,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:s});p=await i.createImageData(!0,!1);r.addImageOps(Yt,[p],g)}catch(e){const t=`Unable to decode inline image: "${e}".`;if(!c)throw new Error(t);warn(t)}return}let w=`img_${this.idFactory.createObjId()}`,S=!1,x=null;if(this.parsingType3Font)w=`${this.idFactory.getDocId()}_type3_${w}`;else if(i&&u){S=this.globalImageCache.shouldCache(u,this.pageIndex);if(S){assert(!a,"Cannot cache an inline image globally.");w=`${this.idFactory.getDocId()}_${w}`}}r.addDependency(w);m=Jt;b=[w,d,f];r.addImageOps(m,b,g,y);if(S){x={objId:w,fn:m,args:b,optionalContent:g,hasMask:y,byteSize:0};if(this.globalImageCache.hasDecodeFailed(u)){this.globalImageCache.setData(u,x);this._sendImgData(w,null,S);return}if(d*f>25e4||y){const e=await this.handler.sendWithPromise("commonobj",[w,"CopyLocalImage",{imageRef:u}]);if(e){this.globalImageCache.setData(u,x);this.globalImageCache.addByteSize(u,e);return}}}PDFImage.buildImage({xref:this.xref,res:e,image:t,isInline:a,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:s}).then((async e=>{p=await e.createImageData(!1,l);p.dataLen=p.bitmap?p.width*p.height*4:p.data.length;p.ref=u;S&&this.globalImageCache.addByteSize(u,p.dataLen);return this._sendImgData(w,p,S)})).catch((e=>{warn(`Unable to decode image "${w}": "${e}".`);u&&this.globalImageCache.addDecodeFailed(u);return this._sendImgData(w,null,S)}));if(i){const e={objId:w,fn:m,args:b,optionalContent:g,hasMask:y};n.set(i,u,e);if(u){this._regionalImageCache.set(null,u,e);if(S){assert(x,"The global cache-data must be available.");this.globalImageCache.setData(u,x)}}}}handleSMask(e,t,a,r,i,n,s){const o=e.get("G"),c={subtype:e.get("S").name,backdrop:e.get("BC")},l=e.get("TR");if(isPDFFunction(l)){const e=this._pdfFunctionFactory.create(l),t=new Uint8Array(256),a=new Float32Array(1);for(let r=0;r<256;r++){a[0]=r/255;e(a,0,a,0);t[r]=255*a[0]|0}c.transferMap=t}return this.buildFormXObject(t,o,c,a,r,i.state.clone({newPath:!0}),n,s)}handleTransferFunction(e){let t;if(Array.isArray(e))t=e;else{if(!isPDFFunction(e))return null;t=[e]}const a=[];let r=0,i=0;for(const e of t){const t=this.xref.fetchIfRef(e);r++;if(isName(t,"Identity")){a.push(null);continue}if(!isPDFFunction(t))return null;const n=this._pdfFunctionFactory.create(t),s=new Uint8Array(256),o=new Float32Array(1);for(let e=0;e<256;e++){o[0]=e/255;n(o,0,o,0);s[e]=255*o[0]|0}a.push(s);i++}return 1!==r&&4!==r||0===i?null:a}handleTilingType(e,t,a,r,i,n,s,o){const c=new OperatorList,l=Dict.merge({xref:this.xref,dictArray:[i.get("Resources"),a]});return this.getOperatorList({stream:r,task:s,resources:l,operatorList:c}).then((function(){const a=c.getIR(),r=getTilingPatternIR(a,i,t);n.addDependencies(c.dependencies);n.addOp(e,r);i.objId&&o.set(null,i.objId,{operatorListIR:a,dict:i})})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`handleTilingType - ignoring pattern: "${e}".`)}}))}async handleSetFont(e,t,a,r,i,n,s=null,o=null){const c=t?.[0]instanceof Name?t[0].name:null,l=await this.loadFont(c,a,e,i,s,o);l.font.isType3Font&&r.addDependencies(l.type3Dependencies);n.font=l.font;l.send(this.handler);return l.loadedName}handleText(e,t){const a=t.font,r=a.charsToGlyphs(e);if(a.data){(!!(t.textRenderingMode&x)||"Pattern"===t.fillColorSpace.name||a.disableFontFace)&&PartialEvaluator.buildFontPaths(a,r,this.handler,this.options)}return r}ensureStateFont(e){if(e.font)return;const t=new FormatError("Missing setFont (Tf) operator before text rendering operator.");if(!this.options.ignoreErrors)throw t;warn(`ensureStateFont: "${t}".`)}async setGState({resources:e,gState:t,operatorList:a,cacheKey:r,task:i,stateManager:n,localGStateCache:s,localColorSpaceCache:o,seenRefs:c}){const l=t.objId;let h=!0;const u=[];let d=Promise.resolve();for(const[r,s]of t)switch(r){case"Type":break;case"LW":if("number"!=typeof s){warn(`Invalid LW (line width): ${s}`);break}u.push([r,Math.abs(s)]);break;case"LC":case"LJ":case"ML":case"D":case"RI":case"FL":case"CA":case"ca":u.push([r,s]);break;case"Font":h=!1;d=d.then((()=>this.handleSetFont(e,null,s[0],a,i,n.state).then((function(e){a.addDependency(e);u.push([r,[e,s[1]]])}))));break;case"BM":u.push([r,normalizeBlendMode(s)]);break;case"SMask":if(isName(s,"None")){u.push([r,!1]);break}if(s instanceof Dict){h=!1;d=d.then((()=>this.handleSMask(s,e,a,i,n,o,c)));u.push([r,!0])}else warn("Unsupported SMask type");break;case"TR":const t=this.handleTransferFunction(s);u.push([r,t]);break;case"OP":case"op":case"OPM":case"BG":case"BG2":case"UCR":case"UCR2":case"TR2":case"HT":case"SM":case"SA":case"AIS":case"TK":info("graphic state operator "+r);break;default:info("Unknown graphic state operator "+r)}await d;u.length>0&&a.addOp(De,[u]);h&&s.set(r,l,u)}loadFont(e,t,a,r,i=null,n=null){const errorFont=async()=>new TranslatedFont({loadedName:"g_font_error",font:new ErrorFont(`Font "${e}" is not available.`),dict:t});let s;if(t)t instanceof Ref&&(s=t);else{const t=a.get("Font");t&&(s=t.getRaw(e))}if(s){if(this.type3FontRefs?.has(s))return errorFont();if(this.fontCache.has(s))return this.fontCache.get(s);try{t=this.xref.fetchIfRef(s)}catch(e){warn(`loadFont - lookup failed: "${e}".`)}}if(!(t instanceof Dict)){if(!this.options.ignoreErrors&&!this.parsingType3Font){warn(`Font "${e}" is not available.`);return errorFont()}warn(`Font "${e}" is not available -- attempting to fallback to a default font.`);t=i||PartialEvaluator.fallbackFontDict}if(t.cacheKey&&this.fontCache.has(t.cacheKey))return this.fontCache.get(t.cacheKey);const{promise:o,resolve:c}=Promise.withResolvers();let l;try{l=this.preEvaluateFont(t);l.cssFontInfo=n}catch(e){warn(`loadFont - preEvaluateFont failed: "${e}".`);return errorFont()}const{descriptor:h,hash:u}=l,d=s instanceof Ref;let f;if(u&&h instanceof Dict){const e=h.fontAliases||=Object.create(null);if(e[u]){const t=e[u].aliasRef;if(d&&t&&this.fontCache.has(t)){this.fontCache.putAlias(s,t);return this.fontCache.get(s)}}else e[u]={fontID:this.idFactory.createFontId()};d&&(e[u].aliasRef=s);f=e[u].fontID}else f=this.idFactory.createFontId();assert(f?.startsWith("f"),'The "fontID" must be (correctly) defined.');if(d)this.fontCache.put(s,o);else{t.cacheKey=`cacheKey_${f}`;this.fontCache.put(t.cacheKey,o)}t.loadedName=`${this.idFactory.getDocId()}_${f}`;this.translateFont(l).then((async e=>{const i=new TranslatedFont({loadedName:t.loadedName,font:e,dict:t});if(e.isType3Font)try{await i.loadType3Data(this,a,r)}catch(e){throw new Error(`Type3 font load error: ${e}`)}c(i)})).catch((e=>{warn(`loadFont - translateFont failed: "${e}".`);c(new TranslatedFont({loadedName:t.loadedName,font:new ErrorFont(e?.message),dict:t}))}));return o}buildPath(e,t,a){const{pathMinMax:r,pathBuffer:i}=a;switch(0|e){case Xe:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1],s=t[2],o=t[3],c=e+s,l=n+o;0===s||0===o?i.push(sa,e,n,oa,c,l,la):i.push(sa,e,n,oa,c,n,oa,c,l,oa,e,l,la);Util.rectBoundingBox(e,n,c,l,r);break}case Ee:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1];i.push(sa,e,n);Util.pointBoundingBox(e,n,r);break}case Pe:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1];i.push(oa,e,n);Util.pointBoundingBox(e,n,r);break}case Le:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l,h,u]=t;a.currentPointX=h;a.currentPointY=u;i.push(ca,s,o,c,l,h,u);Util.bezierBoundingBox(e,n,s,o,c,l,h,u,r);break}case je:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l]=t;a.currentPointX=c;a.currentPointY=l;i.push(ca,e,n,s,o,c,l);Util.bezierBoundingBox(e,n,e,n,s,o,c,l,r);break}case _e:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l]=t;a.currentPointX=c;a.currentPointY=l;i.push(ca,s,o,c,l,c,l);Util.bezierBoundingBox(e,n,s,o,c,l,c,l,r);break}case Ue:i.push(la)}}_getColorSpace(e,t,a){return ColorSpaceUtils.parse({cs:e,xref:this.xref,resources:t,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:a,asyncIfNotCached:!0})}async _handleColorSpace(e){try{return await e}catch(e){if(e instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`_handleColorSpace - ignoring ColorSpace: "${e}".`);return null}throw e}}parseShading({shading:e,resources:t,localColorSpaceCache:a,localShadingPatternCache:r}){let i,n=r.get(e);if(n)return n;try{i=Pattern.parseShading(e,this.xref,t,this._pdfFunctionFactory,this.globalColorSpaceCache,a).getIR()}catch(t){if(t instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`parseShading - ignoring shading: "${t}".`);r.set(e,null);return null}throw t}n=`pattern_${this.idFactory.createObjId()}`;this.parsingType3Font&&(n=`${this.idFactory.getDocId()}_type3_${n}`);r.set(e,n);this.parsingType3Font?this.handler.send("commonobj",[n,"Pattern",i]):this.handler.send("obj",[n,this.pageIndex,"Pattern",i]);return n}handleColorN(e,t,a,r,i,n,s,o,c,l){const h=a.pop();if(h instanceof Name){const u=i.getRaw(h.name),d=u instanceof Ref&&c.getByRef(u);if(d)try{const i=r.base?r.base.getRgbHex(a,0):null,n=getTilingPatternIR(d.operatorListIR,d.dict,i);e.addOp(t,n);return}catch{}const f=this.xref.fetchIfRef(u);if(f){const i=f instanceof BaseStream?f.dict:f,h=i.get("PatternType");if(h===Rn){const o=r.base?r.base.getRgbHex(a,0):null;return this.handleTilingType(t,o,n,f,i,e,s,c)}if(h===Nn){const a=i.get("Shading"),r=this.parseShading({shading:a,resources:n,localColorSpaceCache:o,localShadingPatternCache:l});if(r){const a=lookupMatrix(i.getArray("Matrix"),null);e.addOp(t,["Shading",r,a])}return}throw new FormatError(`Unknown PatternType: ${h}`)}}throw new FormatError(`Unknown PatternName: ${h}`)}_parseVisibilityExpression(e,t,a){if(++t>10){warn("Visibility expression is too deeply nested");return}const r=e.length,i=this.xref.fetchIfRef(e[0]);if(!(r<2)&&i instanceof Name){switch(i.name){case"And":case"Or":case"Not":a.push(i.name);break;default:warn(`Invalid operator ${i.name} in visibility expression`);return}for(let i=1;i0)return{type:"OCMD",expression:t}}const t=a.get("OCGs");if(Array.isArray(t)||t instanceof Dict){const e=[];if(Array.isArray(t))for(const a of t)e.push(a.toString());else e.push(t.objId);return{type:r,ids:e,policy:a.get("P")instanceof Name?a.get("P").name:null,expression:null}}if(t instanceof Ref)return{type:r,id:t.toString()}}return null}getOperatorList({stream:e,task:t,resources:a,operatorList:r,initialState:i=null,fallbackFontDict:n=null,prevRefs:s=null}){const o=e.dict?.objId,c=new RefSet(s);if(o){if(s?.has(o))throw new Error(`getOperatorList - ignoring circular reference: ${o}`);c.put(o)}a||=Dict.empty;i||=new EvalState;if(!r)throw new Error('getOperatorList: missing "operatorList" parameter');const l=this,h=this.xref,u=new LocalImageCache,d=new LocalColorSpaceCache,f=new LocalGStateCache,g=new LocalTilingPatternCache,p=new Map,m=a.get("XObject")||Dict.empty,b=a.get("Pattern")||Dict.empty,y=new StateManager(i),w=new EvaluatorPreprocessor(e,h,y),S=new TimeSlotManager;function closePendingRestoreOPS(e){for(let e=0,t=w.savedStatesDepth;e{y.state.fillColorSpace=e||ColorSpaceUtils.gray})));return}case yt:{const t=l._getColorSpace(e[0],a,d);if(t instanceof ColorSpace){y.state.strokeColorSpace=t;continue}next(l._handleColorSpace(t).then((e=>{y.state.strokeColorSpace=e||ColorSpaceUtils.gray})));return}case At:C=y.state.fillColorSpace;e=[C.getRgbHex(e,0)];i=It;break;case St:C=y.state.strokeColorSpace;e=[C.getRgbHex(e,0)];i=Ft;break;case vt:y.state.fillColorSpace=ColorSpaceUtils.gray;e=[ColorSpaceUtils.gray.getRgbHex(e,0)];i=It;break;case Ct:y.state.strokeColorSpace=ColorSpaceUtils.gray;e=[ColorSpaceUtils.gray.getRgbHex(e,0)];i=Ft;break;case Tt:y.state.fillColorSpace=ColorSpaceUtils.cmyk;e=[ColorSpaceUtils.cmyk.getRgbHex(e,0)];i=It;break;case Ot:y.state.strokeColorSpace=ColorSpaceUtils.cmyk;e=[ColorSpaceUtils.cmyk.getRgbHex(e,0)];i=Ft;break;case It:y.state.fillColorSpace=ColorSpaceUtils.rgb;e=[ColorSpaceUtils.rgb.getRgbHex(e,0)];break;case Ft:y.state.strokeColorSpace=ColorSpaceUtils.rgb;e=[ColorSpaceUtils.rgb.getRgbHex(e,0)];break;case kt:C=y.state.patternFillColorSpace;if(!C){if(isNumberArray(e,null)){e=[ColorSpaceUtils.gray.getRgbHex(e,0)];i=It;break}e=[];i=ia;break}if("Pattern"===C.name){next(l.handleColorN(r,kt,e,C,b,a,t,d,g,p));return}e=[C.getRgbHex(e,0)];i=It;break;case xt:C=y.state.patternStrokeColorSpace;if(!C){if(isNumberArray(e,null)){e=[ColorSpaceUtils.gray.getRgbHex(e,0)];i=Ft;break}e=[];i=ra;break}if("Pattern"===C.name){next(l.handleColorN(r,xt,e,C,b,a,t,d,g,p));return}e=[C.getRgbHex(e,0)];i=Ft;break;case Mt:let O;try{const t=a.get("Shading");if(!t)throw new FormatError("No shading resource found");O=t.get(e[0].name);if(!O)throw new FormatError("No shading object found")}catch(e){if(e instanceof AbortException)continue;if(l.options.ignoreErrors){warn(`getOperatorList - ignoring Shading: "${e}".`);continue}throw e}const T=l.parseShading({shading:O,resources:a,localColorSpaceCache:d,localShadingPatternCache:p});if(!T)continue;e=[T];i=Mt;break;case De:F=e[0]instanceof Name;v=e[0].name;if(F){const t=f.getByName(v);if(t){t.length>0&&r.addOp(De,[t]);e=null;continue}}next(new Promise((function(e,i){if(!F)throw new FormatError("GState must be referred to by name.");const n=a.get("ExtGState");if(!(n instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const s=n.get(v);if(!(s instanceof Dict))throw new FormatError("GState should be a dictionary.");l.setGState({resources:a,gState:s,operatorList:r,cacheKey:v,task:t,stateManager:y,localGStateCache:f,localColorSpaceCache:d,seenRefs:c}).then(e,i)})).catch((function(e){if(!(e instanceof AbortException)){if(!l.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring ExtGState: "${e}".`)}})));return;case Ce:{const[t]=e;if("number"!=typeof t){warn(`Invalid setLineWidth: ${t}`);continue}e[0]=Math.abs(t);break}case Ee:case Pe:case Le:case je:case _e:case Ue:case Xe:l.buildPath(i,e,y.state);continue;case qe:case He:case We:case ze:case $e:case Ge:case Ve:case Ke:case Je:{const{state:{pathBuffer:e,pathMinMax:t}}=y;i!==He&&i!==Ve&&i!==Ke||e.push(la);if(0===e.length)r.addOp(aa,[i,[null],null]);else{r.addOp(aa,[i,[new Float32Array(e)],t.slice()]);e.length=0;t.set([1/0,1/0,-1/0,-1/0],0)}continue}case ht:r.addOp(i,[new Float32Array(e)]);continue;case Et:case Pt:case Ut:case Xt:continue;case jt:if(!(e[0]instanceof Name)){warn(`Expected name for beginMarkedContentProps arg0=${e[0]}`);r.addOp(jt,["OC",null]);continue}if("OC"===e[0].name){next(l.parseMarkedContentProps(e[1],a).then((e=>{r.addOp(jt,["OC",e])})).catch((e=>{if(!(e instanceof AbortException)){if(!l.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring beginMarkedContentProps: "${e}".`);r.addOp(jt,["OC",null])}})));return}e=[e[0].name,e[1]instanceof Dict?e[1].get("MCID"):null];break;default:if(null!==e){for(x=0,k=e.length;x{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring errors during "${t.name}" task: "${e}".`);closePendingRestoreOPS()}}))}getTextContent({stream:e,task:a,resources:r,stateManager:i=null,includeMarkedContent:n=!1,sink:s,seenStyles:o=new Set,viewBox:c,lang:l=null,markedContentData:h=null,disableNormalization:u=!1,keepWhiteSpace:d=!1,prevRefs:f=null}){const g=e.dict?.objId,p=new RefSet(f);if(g){if(f?.has(g))throw new Error(`getTextContent - ignoring circular reference: ${g}`);p.put(g)}r||=Dict.empty;i||=new StateManager(new TextState);n&&(h||={level:0});const m={items:[],styles:Object.create(null),lang:l},b={initialized:!1,str:[],totalWidth:0,totalHeight:0,width:0,height:0,vertical:!1,prevTransform:null,textAdvanceScale:0,spaceInFlowMin:0,spaceInFlowMax:0,trackingSpaceMin:1/0,negativeSpaceMax:-1/0,notASpace:-1/0,transform:null,fontName:null,hasEOL:!1},y=[" "," "];let w=0;function saveLastChar(e){const t=(w+1)%2,a=" "!==y[w]&&" "===y[t];y[w]=e;w=t;return!d&&a}function shouldAddWhitepsace(){return!d&&" "!==y[w]&&" "===y[(w+1)%2]}function resetLastChars(){y[0]=y[1]=" ";w=0}const S=this,x=this.xref,k=[];let C=null;const v=new LocalImageCache,F=new LocalGStateCache,O=new EvaluatorPreprocessor(e,x,i);let T;function pushWhitespace({width:e=0,height:t=0,transform:a=b.prevTransform,fontName:r=b.fontName}){m.items.push({str:" ",dir:"ltr",width:e,height:t,transform:a,fontName:r,hasEOL:!1})}function getCurrentTextTransform(){const e=T.font,a=[T.fontSize*T.textHScale,0,0,T.fontSize,0,T.textRise];if(e.isType3Font&&(T.fontSize<=1||e.isCharBBox)&&!isArrayEqual(T.fontMatrix,t)){const t=e.bbox[3]-e.bbox[1];t>0&&(a[3]*=t*T.fontMatrix[3])}return Util.transform(T.ctm,Util.transform(T.textMatrix,a))}function ensureTextContentItem(){if(b.initialized)return b;const{font:e,loadedName:t}=T;if(!o.has(t)){o.add(t);m.styles[t]={fontFamily:e.fallbackName,ascent:e.ascent,descent:e.descent,vertical:e.vertical};if(S.options.fontExtraProperties&&e.systemFontInfo){const a=m.styles[t];a.fontSubstitution=e.systemFontInfo.css;a.fontSubstitutionLoadedName=e.systemFontInfo.loadedName}}b.fontName=t;const a=b.transform=getCurrentTextTransform();if(e.vertical){b.width=b.totalWidth=Math.hypot(a[0],a[1]);b.height=b.totalHeight=0;b.vertical=!0}else{b.width=b.totalWidth=0;b.height=b.totalHeight=Math.hypot(a[2],a[3]);b.vertical=!1}const r=Math.hypot(T.textLineMatrix[0],T.textLineMatrix[1]),i=Math.hypot(T.ctm[0],T.ctm[1]);b.textAdvanceScale=i*r;const{fontSize:n}=T;b.trackingSpaceMin=.102*n;b.notASpace=.03*n;b.negativeSpaceMax=-.2*n;b.spaceInFlowMin=.102*n;b.spaceInFlowMax=.6*n;b.hasEOL=!1;b.initialized=!0;return b}function updateAdvanceScale(){if(!b.initialized)return;const e=Math.hypot(T.textLineMatrix[0],T.textLineMatrix[1]),t=Math.hypot(T.ctm[0],T.ctm[1])*e;if(t!==b.textAdvanceScale){if(b.vertical){b.totalHeight+=b.height*b.textAdvanceScale;b.height=0}else{b.totalWidth+=b.width*b.textAdvanceScale;b.width=0}b.textAdvanceScale=t}}function runBidiTransform(e){let t=e.str.join("");u||(t=function normalizeUnicode(e){if(!ma){ma=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;ba=new Map([["ſt","ſt"]])}return e.replaceAll(ma,((e,t,a)=>t?t.normalize("NFKC"):ba.get(a)))}(t));const a=bidi(t,-1,e.vertical);return{str:a.str,dir:a.dir,width:Math.abs(e.totalWidth),height:Math.abs(e.totalHeight),transform:e.transform,fontName:e.fontName,hasEOL:e.hasEOL}}async function handleSetFont(e,i){const n=await S.loadFont(e,i,r,a);T.loadedName=n.loadedName;T.font=n.font;T.fontMatrix=n.font.fontMatrix||t}function applyInverseRotation(e,t,a){const r=Math.hypot(a[0],a[1]);return[(a[0]*e+a[1]*t)/r,(a[2]*e+a[3]*t)/r]}function compareWithLastPosition(e){const t=getCurrentTextTransform();let a=t[4],r=t[5];if(T.font?.vertical){if(ac[2]||r+ec[3])return!1}else if(a+ec[2]||rc[3])return!1;if(!T.font||!b.prevTransform)return!0;let i=b.prevTransform[4],n=b.prevTransform[5];if(i===a&&n===r)return!0;let s=-1;t[0]&&0===t[1]&&0===t[2]?s=t[0]>0?0:180:t[1]&&0===t[0]&&0===t[3]&&(s=t[1]>0?90:270);switch(s){case 0:break;case 90:[a,r]=[r,a];[i,n]=[n,i];break;case 180:[a,r,i,n]=[-a,-r,-i,-n];break;case 270:[a,r]=[-r,-a];[i,n]=[-n,-i];break;default:[a,r]=applyInverseRotation(a,r,t);[i,n]=applyInverseRotation(i,n,b.prevTransform)}if(T.font.vertical){const e=(n-r)/b.textAdvanceScale,t=a-i,s=Math.sign(b.height);if(e.5*b.width){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(t)>b.width){appendEOL();return!0}e<=s*b.notASpace&&resetLastChars();if(e<=s*b.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({height:Math.abs(e)})}else b.height+=e;else if(!addFakeSpaces(e,b.prevTransform,s))if(0===b.str.length){resetLastChars();pushWhitespace({height:Math.abs(e)})}else b.height+=e;Math.abs(t)>.25*b.width&&flushTextContentItem();return!0}const o=(a-i)/b.textAdvanceScale,l=r-n,h=Math.sign(b.width);if(o.5*b.height){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(l)>b.height){appendEOL();return!0}o<=h*b.notASpace&&resetLastChars();if(o<=h*b.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({width:Math.abs(o)})}else b.width+=o;else if(!addFakeSpaces(o,b.prevTransform,h))if(0===b.str.length){resetLastChars();pushWhitespace({width:Math.abs(o)})}else b.width+=o;Math.abs(l)>.25*b.height&&flushTextContentItem();return!0}function buildTextContentItem({chars:e,extraSpacing:t}){const a=T.font;if(!e){const e=T.charSpacing+t;e&&(a.vertical?T.translateTextMatrix(0,-e):T.translateTextMatrix(e*T.textHScale,0));d&&compareWithLastPosition(0);return}const r=a.charsToGlyphs(e),i=T.fontMatrix[0]*T.fontSize;for(let e=0,n=r.length;e0){const e=k.join("");k.length=0;buildTextContentItem({chars:e,extraSpacing:0})}break;case dt:if(!i.state.font){S.ensureStateFont(i.state);continue}buildTextContentItem({chars:w[0],extraSpacing:0});break;case gt:if(!i.state.font){S.ensureStateFont(i.state);continue}T.carriageReturn();buildTextContentItem({chars:w[0],extraSpacing:0});break;case pt:if(!i.state.font){S.ensureStateFont(i.state);continue}T.wordSpacing=w[0];T.charSpacing=w[1];T.carriageReturn();buildTextContentItem({chars:w[2],extraSpacing:0});break;case Nt:flushTextContentItem();C??=r.get("XObject")||Dict.empty;y=w[0]instanceof Name;b=w[0].name;if(y&&v.getByName(b))break;next(new Promise((function(e,t){if(!y)throw new FormatError("XObject must be referred to by name.");let f=C.getRaw(b);if(f instanceof Ref){if(v.getByRef(f)){e();return}if(S.globalImageCache.getData(f,S.pageIndex)){e();return}f=x.fetch(f)}if(!(f instanceof BaseStream))throw new FormatError("XObject should be a stream");const{dict:g}=f,m=g.get("Subtype");if(!(m instanceof Name))throw new FormatError("XObject should have a Name subtype");if("Form"!==m.name){v.set(b,g.objId,!0);e();return}const w=i.state.clone(),k=new StateManager(w),F=lookupMatrix(g.getArray("Matrix"),null);F&&k.transform(F);const O=g.get("Resources");enqueueChunk();const T={enqueueInvoked:!1,enqueue(e,t){this.enqueueInvoked=!0;s.enqueue(e,t)},get desiredSize(){return s.desiredSize},get ready(){return s.ready}};S.getTextContent({stream:f,task:a,resources:O instanceof Dict?O:r,stateManager:k,includeMarkedContent:n,sink:T,seenStyles:o,viewBox:c,lang:l,markedContentData:h,disableNormalization:u,keepWhiteSpace:d,prevRefs:p}).then((function(){T.enqueueInvoked||v.set(b,g.objId,!0);e()}),t)})).catch((function(e){if(!(e instanceof AbortException)){if(!S.options.ignoreErrors)throw e;warn(`getTextContent - ignoring XObject: "${e}".`)}})));return;case De:y=w[0]instanceof Name;b=w[0].name;if(y&&F.getByName(b))break;next(new Promise((function(e,t){if(!y)throw new FormatError("GState must be referred to by name.");const a=r.get("ExtGState");if(!(a instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const i=a.get(b);if(!(i instanceof Dict))throw new FormatError("GState should be a dictionary.");const n=i.get("Font");if(n){flushTextContentItem();T.fontName=null;T.fontSize=n[1];handleSetFont(null,n[0]).then(e,t)}else{F.set(b,i.objId,!0);e()}})).catch((function(e){if(!(e instanceof AbortException)){if(!S.options.ignoreErrors)throw e;warn(`getTextContent - ignoring ExtGState: "${e}".`)}})));return;case Lt:flushTextContentItem();if(n){h.level++;m.items.push({type:"beginMarkedContent",tag:w[0]instanceof Name?w[0].name:null})}break;case jt:flushTextContentItem();if(n){h.level++;let e=null;w[1]instanceof Dict&&(e=w[1].get("MCID"));m.items.push({type:"beginMarkedContentProps",id:Number.isInteger(e)?`${S.idFactory.getPageObjId()}_mc${e}`:null,tag:w[0]instanceof Name?w[0].name:null})}break;case _t:flushTextContentItem();if(n){if(0===h.level)break;h.level--;m.items.push({type:"endMarkedContent"})}break;case Re:!e||e.font===T.font&&e.fontSize===T.fontSize&&e.fontName===T.fontName||flushTextContentItem()}if(m.items.length>=s.desiredSize){g=!0;break}}if(g)next(En);else{flushTextContentItem();enqueueChunk();e()}})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getTextContent - ignoring errors during "${a.name}" task: "${e}".`);flushTextContentItem();enqueueChunk()}}))}async extractDataStructures(e,t){const a=this.xref;let r;const i=this.readToUnicode(t.toUnicode);if(t.composite){const a=e.get("CIDSystemInfo");a instanceof Dict&&(t.cidSystemInfo={registry:stringToPDFString(a.get("Registry")),ordering:stringToPDFString(a.get("Ordering")),supplement:a.get("Supplement")});try{const t=e.get("CIDToGIDMap");t instanceof BaseStream&&(r=t.getBytes())}catch(e){if(!this.options.ignoreErrors)throw e;warn(`extractDataStructures - ignoring CIDToGIDMap data: "${e}".`)}}const n=[];let s,o=null;if(e.has("Encoding")){s=e.get("Encoding");if(s instanceof Dict){o=s.get("BaseEncoding");o=o instanceof Name?o.name:null;if(s.has("Differences")){const e=s.get("Differences");let t=0;for(const r of e){const e=a.fetchIfRef(r);if("number"==typeof e)t=e;else{if(!(e instanceof Name))throw new FormatError(`Invalid entry in 'Differences' array: ${e}`);n[t++]=e.name}}}}else if(s instanceof Name)o=s.name;else{const e="Encoding is not a Name nor a Dict";if(!this.options.ignoreErrors)throw new FormatError(e);warn(e)}"MacRomanEncoding"!==o&&"MacExpertEncoding"!==o&&"WinAnsiEncoding"!==o&&(o=null)}const c=!t.file||t.isInternalFont,l=ei()[t.name];o&&c&&l&&(o=null);if(o)t.defaultEncoding=getEncoding(o);else{const e=!!(t.flags&Pr),a=!!(t.flags&Lr);s=Ar;"TrueType"!==t.type||a||(s=kr);if(e||l){s=xr;c&&(/Symbol/i.test(t.name)?s=Cr:/Dingbats/i.test(t.name)?s=vr:/Wingdings/i.test(t.name)&&(s=kr))}t.defaultEncoding=s}t.differences=n;t.baseEncodingName=o;t.hasEncoding=!!o||n.length>0;t.dict=e;t.toUnicode=await i;const h=await this.buildToUnicode(t);t.toUnicode=h;r&&(t.cidToGidMap=this.readCidToGidMap(r,h));return t}_simpleFontToUnicode(e,t=!1){assert(!e.composite,"Must be a simple font.");const a=[],r=e.defaultEncoding.slice(),i=e.baseEncodingName,n=e.differences;for(const e in n){const t=n[e];".notdef"!==t&&(r[e]=t)}const s=Fr();for(const n in r){let o=r[n];if(""===o)continue;let c=s[o];if(void 0!==c){a[n]=String.fromCharCode(c);continue}let l=0;switch(o[0]){case"G":3===o.length&&(l=parseInt(o.substring(1),16));break;case"g":5===o.length&&(l=parseInt(o.substring(1),16));break;case"C":case"c":if(o.length>=3&&o.length<=4){const a=o.substring(1);if(t){l=parseInt(a,16);break}l=+a;if(Number.isNaN(l)&&Number.isInteger(parseInt(a,16)))return this._simpleFontToUnicode(e,!0)}break;case"u":c=getUnicodeForGlyph(o,s);-1!==c&&(l=c);break;default:switch(o){case"f_h":case"f_t":case"T_h":a[n]=o.replaceAll("_","");continue}}if(l>0&&l<=1114111&&Number.isInteger(l)){if(i&&l===+n){const e=getEncoding(i);if(e&&(o=e[n])){a[n]=String.fromCharCode(s[o]);continue}}a[n]=String.fromCodePoint(l)}}return a}async buildToUnicode(e){e.hasIncludedToUnicodeMap=e.toUnicode?.length>0;if(e.hasIncludedToUnicodeMap){!e.composite&&e.hasEncoding&&(e.fallbackToUnicode=this._simpleFontToUnicode(e));return e.toUnicode}if(!e.composite)return new ToUnicodeMap(this._simpleFontToUnicode(e));if(e.composite&&(e.cMap.builtInCMap&&!(e.cMap instanceof IdentityCMap)||"Adobe"===e.cidSystemInfo?.registry&&("GB1"===e.cidSystemInfo.ordering||"CNS1"===e.cidSystemInfo.ordering||"Japan1"===e.cidSystemInfo.ordering||"Korea1"===e.cidSystemInfo.ordering))){const{registry:t,ordering:a}=e.cidSystemInfo,r=Name.get(`${t}-${a}-UCS2`),i=await CMapFactory.create({encoding:r,fetchBuiltInCMap:this._fetchBuiltInCMapBound,useCMap:null}),n=[],s=[];e.cMap.forEach((function(e,t){if(t>65535)throw new FormatError("Max size of CID is 65,535");const a=i.lookup(t);if(a){s.length=0;for(let e=0,t=a.length;e>1;(0!==i||t.has(n))&&(a[n]=i)}return a}extractWidths(e,t,a){const r=this.xref;let i=[],n=0;const s=[];let o;if(a.composite){const t=e.get("DW");n="number"==typeof t?Math.ceil(t):1e3;const c=e.get("W");if(Array.isArray(c))for(let e=0,t=c.length;e{const t=c.get(e),r=new OperatorList;return n.getOperatorList({stream:t,task:a,resources:l,operatorList:r}).then((()=>{switch(r.fnArray[0]){case bt:this.#K(r,b);break;case mt:b||this.#J(r)}h[e]=r.getIR();for(const e of r.dependencies)i.add(e)})).catch((function(t){warn(`Type3 font resource "${e}" is not available.`);const a=new OperatorList;h[e]=a.getIR()}))}));this.#V=o.then((()=>{r.charProcOperatorList=h;if(this._bbox){r.isCharBBox=!0;r.bbox=this._bbox}}));return this.#V}#K(e,t=NaN){const a=Util.normalizeRect(e.argsArray[0].slice(2)),r=a[2]-a[0],i=a[3]-a[1],n=Math.hypot(r,i);if(0===r||0===i){e.fnArray.splice(0,1);e.argsArray.splice(0,1)}else if(0===t||Math.round(n/t)>=10){this._bbox??=[1/0,1/0,-1/0,-1/0];Util.rectBoundingBox(...a,this._bbox)}let s=0,o=e.length;for(;s=Ee&&n<=Je;if(i.variableArgs)o>s&&info(`Command ${r}: expected [0, ${s}] args, but received ${o} args.`);else{if(o!==s){const e=this.nonProcessedArgs;for(;o>s;){e.push(t.shift());o--}for(;oEvaluatorPreprocessor.MAX_INVALID_PATH_OPS)throw new FormatError(`Invalid ${e}`);warn(`Skipping ${e}`);null!==t&&(t.length=0);continue}}this.preprocessCommand(n,t);e.fn=n;e.args=t;return!0}if(a===wa)return!1;if(null!==a){null===t&&(t=[]);t.push(a);if(t.length>33)throw new FormatError("Too many arguments")}}}preprocessCommand(e,t){switch(0|e){case Be:this.stateManager.save();break;case Re:this.stateManager.restore();break;case Ne:this.stateManager.transform(t)}}}class DefaultAppearanceEvaluator extends EvaluatorPreprocessor{constructor(e){super(new StringStream(e))}parse(){const e={fn:0,args:[]},t={fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3)};try{for(;;){e.args.length=0;if(!this.read(e))break;if(0!==this.savedStatesDepth)continue;const{fn:a,args:r}=e;switch(0|a){case nt:const[e,a]=r;e instanceof Name&&(t.fontName=e.name);"number"==typeof a&&a>0&&(t.fontSize=a);break;case It:ColorSpaceUtils.rgb.getRgbItem(r,0,t.fontColor,0);break;case vt:ColorSpaceUtils.gray.getRgbItem(r,0,t.fontColor,0);break;case Tt:ColorSpaceUtils.cmyk.getRgbItem(r,0,t.fontColor,0)}}}catch(e){warn(`parseDefaultAppearance - ignoring errors: "${e}".`)}return t}}function parseDefaultAppearance(e){return new DefaultAppearanceEvaluator(e).parse()}class AppearanceStreamEvaluator extends EvaluatorPreprocessor{constructor(e,t,a,r){super(e);this.stream=e;this.evaluatorOptions=t;this.xref=a;this.globalColorSpaceCache=r;this.resources=e.dict?.get("Resources")}parse(){const e={fn:0,args:[]};let t={scaleFactor:1,fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3),fillColorSpace:ColorSpaceUtils.gray},a=!1;const r=[];try{for(;;){e.args.length=0;if(a||!this.read(e))break;const{fn:i,args:n}=e;switch(0|i){case Be:r.push({scaleFactor:t.scaleFactor,fontSize:t.fontSize,fontName:t.fontName,fontColor:t.fontColor.slice(),fillColorSpace:t.fillColorSpace});break;case Re:t=r.pop()||t;break;case ht:t.scaleFactor*=Math.hypot(n[0],n[1]);break;case nt:const[e,i]=n;e instanceof Name&&(t.fontName=e.name);"number"==typeof i&&i>0&&(t.fontSize=i*t.scaleFactor);break;case wt:t.fillColorSpace=ColorSpaceUtils.parse({cs:n[0],xref:this.xref,resources:this.resources,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:this._localColorSpaceCache});break;case At:t.fillColorSpace.getRgbItem(n,0,t.fontColor,0);break;case It:ColorSpaceUtils.rgb.getRgbItem(n,0,t.fontColor,0);break;case vt:ColorSpaceUtils.gray.getRgbItem(n,0,t.fontColor,0);break;case Tt:ColorSpaceUtils.cmyk.getRgbItem(n,0,t.fontColor,0);break;case dt:case ft:case gt:case pt:a=!0}}}catch(e){warn(`parseAppearanceStream - ignoring errors: "${e}".`)}this.stream.reset();delete t.scaleFactor;delete t.fillColorSpace;return t}get _localColorSpaceCache(){return shadow(this,"_localColorSpaceCache",new LocalColorSpaceCache)}get _pdfFunctionFactory(){return shadow(this,"_pdfFunctionFactory",new PDFFunctionFactory({xref:this.xref,isEvalSupported:this.evaluatorOptions.isEvalSupported}))}}function getPdfColor(e,t){if(e[0]===e[1]&&e[1]===e[2]){return`${numberToString(e[0]/255)} ${t?"g":"G"}`}return Array.from(e,(e=>numberToString(e/255))).join(" ")+" "+(t?"rg":"RG")}class FakeUnicodeFont{constructor(e,t){this.xref=e;this.widths=null;this.firstChar=1/0;this.lastChar=-1/0;this.fontFamily=t;const a=new OffscreenCanvas(1,1);this.ctxMeasure=a.getContext("2d",{willReadFrequently:!0});FakeUnicodeFont._fontNameId||(FakeUnicodeFont._fontNameId=1);this.fontName=Name.get(`InvalidPDFjsFont_${t}_${FakeUnicodeFont._fontNameId++}`)}get fontDescriptorRef(){if(!FakeUnicodeFont._fontDescriptorRef){const e=new Dict(this.xref);e.set("Type",Name.get("FontDescriptor"));e.set("FontName",this.fontName);e.set("FontFamily","MyriadPro Regular");e.set("FontBBox",[0,0,0,0]);e.set("FontStretch",Name.get("Normal"));e.set("FontWeight",400);e.set("ItalicAngle",0);FakeUnicodeFont._fontDescriptorRef=this.xref.getNewPersistentRef(e)}return FakeUnicodeFont._fontDescriptorRef}get descendantFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("CIDFontType0"));e.set("CIDToGIDMap",Name.get("Identity"));e.set("FirstChar",this.firstChar);e.set("LastChar",this.lastChar);e.set("FontDescriptor",this.fontDescriptorRef);e.set("DW",1e3);const t=[],a=[...this.widths.entries()].sort();let r=null,i=null;for(const[e,n]of a)if(r)if(e===r+i.length)i.push(n);else{t.push(r,i);r=e;i=[n]}else{r=e;i=[n]}r&&t.push(r,i);e.set("W",t);const n=new Dict(this.xref);n.set("Ordering","Identity");n.set("Registry","Adobe");n.set("Supplement",0);e.set("CIDSystemInfo",n);return this.xref.getNewPersistentRef(e)}get baseFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type0"));e.set("Encoding",Name.get("Identity-H"));e.set("DescendantFonts",[this.descendantFontRef]);e.set("ToUnicode",Name.get("Identity-H"));return this.xref.getNewPersistentRef(e)}get resources(){const e=new Dict(this.xref),t=new Dict(this.xref);t.set(this.fontName.name,this.baseFontRef);e.set("Font",t);return e}_createContext(){this.widths=new Map;this.ctxMeasure.font=`1000px ${this.fontFamily}`;return this.ctxMeasure}createFontResources(e){const t=this._createContext();for(const a of e.split(/\r\n?|\n/))for(const e of a.split("")){const a=e.charCodeAt(0);if(this.widths.has(a))continue;const r=t.measureText(e),i=Math.ceil(r.width);this.widths.set(a,i);this.firstChar=Math.min(a,this.firstChar);this.lastChar=Math.max(a,this.lastChar)}return this.resources}static getFirstPositionInfo(e,t,i){const[n,s,o,c]=e;let l=o-n,h=c-s;t%180!=0&&([l,h]=[h,l]);const u=a*i;return{coords:[0,h+r*i-u],bbox:[0,0,l,h],matrix:0!==t?getRotationMatrix(t,h,u):void 0}}createAppearance(e,t,i,n,s,o){const c=this._createContext(),l=[];let h=-1/0;for(const t of e.split(/\r\n?|\n/)){l.push(t);const e=c.measureText(t).width;h=Math.max(h,e);for(const e of codePointIter(t)){const t=String.fromCodePoint(e);let a=this.widths.get(e);if(void 0===a){const r=c.measureText(t);a=Math.ceil(r.width);this.widths.set(e,a);this.firstChar=Math.min(e,this.firstChar);this.lastChar=Math.max(e,this.lastChar)}}}h*=n/1e3;const[u,d,f,g]=t;let p=f-u,m=g-d;i%180!=0&&([p,m]=[m,p]);let b=1;h>p&&(b=p/h);let y=1;const w=a*n,S=r*n,x=w*l.length;x>m&&(y=m/x);const k=n*Math.min(b,y),C=["q",`0 0 ${numberToString(p)} ${numberToString(m)} re W n`,"BT",`1 0 0 1 0 ${numberToString(m+S)} Tm 0 Tc ${getPdfColor(s,!0)}`,`/${this.fontName.name} ${numberToString(k)} Tf`],{resources:v}=this;if(1!==(o="number"==typeof o&&o>=0&&o<=1?o:1)){C.push("/R0 gs");const e=new Dict(this.xref),t=new Dict(this.xref);t.set("ca",o);t.set("CA",o);t.set("Type",Name.get("ExtGState"));e.set("R0",t);v.set("ExtGState",e)}const F=numberToString(w);for(const e of l)C.push(`0 -${F} Td <${stringToUTF16HexString(e)}> Tj`);C.push("ET","Q");const O=C.join("\n"),T=new Dict(this.xref);T.set("Subtype",Name.get("Form"));T.set("Type",Name.get("XObject"));T.set("BBox",[0,0,p,m]);T.set("Length",O.length);T.set("Resources",v);if(i){const e=getRotationMatrix(i,p,m);T.set("Matrix",e)}const M=new StringStream(O);M.dict=T;return M}}class NameOrNumberTree{constructor(e,t,a){this.root=e;this.xref=t;this._type=a}getAll(){const e=new Map;if(!this.root)return e;const t=this.xref,a=new RefSet;a.put(this.root);const r=[this.root];for(;r.length>0;){const i=t.fetchIfRef(r.shift());if(!(i instanceof Dict))continue;if(i.has("Kids")){const e=i.get("Kids");if(!Array.isArray(e))continue;for(const t of e){if(a.has(t))throw new FormatError(`Duplicate entry in "${this._type}" tree.`);r.push(t);a.put(t)}continue}const n=i.get(this._type);if(Array.isArray(n))for(let a=0,r=n.length;a10){warn(`Search depth limit reached for "${this._type}" tree.`);return null}const i=a.get("Kids");if(!Array.isArray(i))return null;let n=0,s=i.length-1;for(;n<=s;){const r=n+s>>1,o=t.fetchIfRef(i[r]),c=o.get("Limits");if(et.fetchIfRef(c[1]))){a=o;break}n=r+1}}if(n>s)return null}const i=a.get(this._type);if(Array.isArray(i)){let a=0,r=i.length-2;for(;a<=r;){const n=a+r>>1,s=n+(1&n),o=t.fetchIfRef(i[s]);if(eo))return i[s+1];a=s+2}}}return null}get(e){return this.xref.fetchIfRef(this.getRaw(e))}}class NameTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Names")}}class NumberTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Nums")}}function clearGlobalCaches(){!function clearPatternCaches(){Ii=Object.create(null)}();!function clearPrimitiveCaches(){Sa=Object.create(null);xa=Object.create(null);Aa=Object.create(null)}();!function clearUnicodeCaches(){Dr.clear()}();JpxImage.cleanup()}function pickPlatformItem(e){return e instanceof Dict?e.has("UF")?e.get("UF"):e.has("F")?e.get("F"):e.has("Unix")?e.get("Unix"):e.has("Mac")?e.get("Mac"):e.has("DOS")?e.get("DOS"):null:null}class FileSpec{#Y=!1;constructor(e,t,a=!1){if(e instanceof Dict){this.xref=t;this.root=e;e.has("FS")&&(this.fs=e.get("FS"));e.has("RF")&&warn("Related file specifications are not supported");a||(e.has("EF")?this.#Y=!0:warn("Non-embedded file specifications are not supported"))}}get filename(){let e="";const t=pickPlatformItem(this.root);t&&"string"==typeof t&&(e=stringToPDFString(t,!0).replaceAll("\\\\","\\").replaceAll("\\/","/").replaceAll("\\","/"));return shadow(this,"filename",e||"unnamed")}get content(){if(!this.#Y)return null;this._contentRef||=pickPlatformItem(this.root?.get("EF"));let e=null;if(this._contentRef){const t=this.xref.fetchIfRef(this._contentRef);t instanceof BaseStream?e=t.getBytes():warn("Embedded file specification points to non-existing/invalid content")}else warn("Embedded file specification does not have any content");return e}get description(){let e="";const t=this.root?.get("Desc");t&&"string"==typeof t&&(e=stringToPDFString(t));return shadow(this,"description",e)}get serializable(){return{rawFilename:this.filename,filename:(e=this.filename,e.substring(e.lastIndexOf("/")+1)),content:this.content,description:this.description};var e}}const Pn=0,Ln=-2,jn=-3,_n=-4,Un=-5,Xn=-6,qn=-9;function isWhitespace(e,t){const a=e[t];return" "===a||"\n"===a||"\r"===a||"\t"===a}class XMLParserBase{_resolveEntities(e){return e.replaceAll(/&([^;]+);/g,((e,t)=>{if("#x"===t.substring(0,2))return String.fromCodePoint(parseInt(t.substring(2),16));if("#"===t.substring(0,1))return String.fromCodePoint(parseInt(t.substring(1),10));switch(t){case"lt":return"<";case"gt":return">";case"amp":return"&";case"quot":return'"';case"apos":return"'"}return this.onResolveEntity(t)}))}_parseContent(e,t){const a=[];let r=t;function skipWs(){for(;r"!==e[r]&&"/"!==e[r];)++r;const i=e.substring(t,r);skipWs();for(;r"!==e[r]&&"/"!==e[r]&&"?"!==e[r];){skipWs();let t="",i="";for(;r"!==e[a]&&"?"!==e[a]&&"/"!==e[a];)++a;const r=e.substring(t,a);!function skipWs(){for(;a"!==e[a+1]);)++a;return{name:r,value:e.substring(i,a),parsed:a-t}}parseXml(e){let t=0;for(;t",a);if(t<0){this.onError(qn);return}this.onEndElement(e.substring(a,t));a=t+1;break;case"?":++a;const r=this._parseProcessingInstruction(e,a);if("?>"!==e.substring(a+r.parsed,a+r.parsed+2)){this.onError(jn);return}this.onPi(r.name,r.value);a+=r.parsed+2;break;case"!":if("--"===e.substring(a+1,a+3)){t=e.indexOf("--\x3e",a+3);if(t<0){this.onError(Un);return}this.onComment(e.substring(a+3,t));a=t+3}else if("[CDATA["===e.substring(a+1,a+8)){t=e.indexOf("]]>",a+8);if(t<0){this.onError(Ln);return}this.onCdata(e.substring(a+8,t));a=t+3}else{if("DOCTYPE"!==e.substring(a+1,a+8)){this.onError(Xn);return}{const r=e.indexOf("[",a+8);let i=!1;t=e.indexOf(">",a+8);if(t<0){this.onError(_n);return}if(r>0&&t>r){t=e.indexOf("]>",a+8);if(t<0){this.onError(_n);return}i=!0}const n=e.substring(a+8,t+(i?1:0));this.onDoctype(n);a=t+(i?2:1)}}break;default:const i=this._parseContent(e,a);if(null===i){this.onError(Xn);return}let n=!1;if("/>"===e.substring(a+i.parsed,a+i.parsed+2))n=!0;else if(">"!==e.substring(a+i.parsed,a+i.parsed+1)){this.onError(qn);return}this.onBeginElement(i.name,i.attributes,n);a+=i.parsed+(n?2:1)}}else{for(;ae.textContent)).join(""):this.nodeValue||""}get children(){return this.childNodes||[]}hasChildNodes(){return this.childNodes?.length>0}searchNode(e,t){if(t>=e.length)return this;const a=e[t];if(a.name.startsWith("#")&&t0){r.push([i,0]);i=i.childNodes[0]}else{if(0===r.length)return null;for(;0!==r.length;){const[e,t]=r.pop(),a=t+1;if(a");for(const t of this.childNodes)t.dump(e);e.push(``)}else this.nodeValue?e.push(`>${encodeToXmlString(this.nodeValue)}`):e.push("/>")}else e.push(encodeToXmlString(this.nodeValue))}}class SimpleXMLParser extends XMLParserBase{constructor({hasAttributes:e=!1,lowerCaseName:t=!1}){super();this._currentFragment=null;this._stack=null;this._errorCode=Pn;this._hasAttributes=e;this._lowerCaseName=t}parseFromString(e){this._currentFragment=[];this._stack=[];this._errorCode=Pn;this.parseXml(e);if(this._errorCode!==Pn)return;const[t]=this._currentFragment;return t?{documentElement:t}:void 0}onText(e){if(function isWhitespaceString(e){for(let t=0,a=e.length;t\\376\\377([^<]+)/g,(function(e,t){const a=t.replaceAll(/\\([0-3])([0-7])([0-7])/g,(function(e,t,a,r){return String.fromCharCode(64*t+8*a+1*r)})).replaceAll(/&(amp|apos|gt|lt|quot);/g,(function(e,t){switch(t){case"amp":return"&";case"apos":return"'";case"gt":return">";case"lt":return"<";case"quot":return'"'}throw new Error(`_repair: ${t} isn't defined.`)})),r=[">"];for(let e=0,t=a.length;e=32&&t<127&&60!==t&&62!==t&&38!==t?r.push(String.fromCharCode(t)):r.push("&#x"+(65536+t).toString(16).substring(1)+";")}return r.join("")}))}_getSequence(e){const t=e.nodeName;return"rdf:bag"!==t&&"rdf:seq"!==t&&"rdf:alt"!==t?null:e.childNodes.filter((e=>"rdf:li"===e.nodeName))}_parseArray(e){if(!e.hasChildNodes())return;const[t]=e.childNodes,a=this._getSequence(t)||[];this._metadataMap.set(e.nodeName,a.map((e=>e.textContent.trim())))}_parse(e){let t=e.documentElement;if("rdf:rdf"!==t.nodeName){t=t.firstChild;for(;t&&"rdf:rdf"!==t.nodeName;)t=t.nextSibling}if(t&&"rdf:rdf"===t.nodeName&&t.hasChildNodes())for(const e of t.childNodes)if("rdf:description"===e.nodeName)for(const t of e.childNodes){const e=t.nodeName;switch(e){case"#text":continue;case"dc:creator":case"dc:subject":this._parseArray(t);continue}this._metadataMap.set(e,t.textContent.trim())}}get serializable(){return{parsedData:this._metadataMap,rawData:this._data}}}const Hn=1,Wn=2,zn=3,$n=4,Gn=5;class StructTreeRoot{constructor(e,t,a){this.xref=e;this.dict=t;this.ref=a instanceof Ref?a:null;this.roleMap=new Map;this.structParentIds=null}init(){this.readRoleMap()}#Z(e,t,a){if(!(e instanceof Ref)||t<0)return;this.structParentIds||=new RefSetCache;let r=this.structParentIds.get(e);if(!r){r=[];this.structParentIds.put(e,r)}r.push([t,a])}addAnnotationIdToPage(e,t){this.#Z(e,t,$n)}readRoleMap(){const e=this.dict.get("RoleMap");if(e instanceof Dict)for(const[t,a]of e)a instanceof Name&&this.roleMap.set(t,a.name)}static async canCreateStructureTree({catalogRef:e,pdfManager:t,newAnnotationsByPage:a}){if(!(e instanceof Ref)){warn("Cannot save the struct tree: no catalog reference.");return!1}let r=0,i=!0;for(const[e,n]of a){const{ref:a}=await t.getPage(e);if(!(a instanceof Ref)){warn(`Cannot save the struct tree: page ${e} has no ref.`);i=!0;break}for(const e of n)if(e.accessibilityData?.type){e.parentTreeId=r++;i=!1}}if(i){for(const e of a.values())for(const t of e)delete t.parentTreeId;return!1}return!0}static async createStructureTree({newAnnotationsByPage:e,xref:t,catalogRef:a,pdfManager:r,changes:i}){const n=await r.ensureCatalog("cloneDict"),s=new RefSetCache;s.put(a,n);const o=t.getNewTemporaryRef();n.set("StructTreeRoot",o);const c=new Dict(t);c.set("Type",Name.get("StructTreeRoot"));const l=t.getNewTemporaryRef();c.set("ParentTree",l);const h=[];c.set("K",h);s.put(o,c);const u=new Dict(t),d=[];u.set("Nums",d);const f=await this.#Q({newAnnotationsByPage:e,structTreeRootRef:o,structTreeRoot:null,kids:h,nums:d,xref:t,pdfManager:r,changes:i,cache:s});c.set("ParentTreeNextKey",f);s.put(l,u);for(const[e,t]of s.items())i.put(e,{data:t})}async canUpdateStructTree({pdfManager:e,newAnnotationsByPage:t}){if(!this.ref){warn("Cannot update the struct tree: no root reference.");return!1}let a=this.dict.get("ParentTreeNextKey");if(!Number.isInteger(a)||a<0){warn("Cannot update the struct tree: invalid next key.");return!1}const r=this.dict.get("ParentTree");if(!(r instanceof Dict)){warn("Cannot update the struct tree: ParentTree isn't a dict.");return!1}const i=r.get("Nums");if(!Array.isArray(i)){warn("Cannot update the struct tree: nums isn't an array.");return!1}const n=new NumberTree(r,this.xref);for(const a of t.keys()){const{pageDict:t}=await e.getPage(a);if(!t.has("StructParents"))continue;const r=t.get("StructParents");if(!Number.isInteger(r)||!Array.isArray(n.get(r))){warn(`Cannot save the struct tree: page ${a} has a wrong id.`);return!1}}let s=!0;for(const[r,i]of t){const{pageDict:t}=await e.getPage(r);StructTreeRoot.#ee({elements:i,xref:this.xref,pageDict:t,numberTree:n});for(const e of i)if(e.accessibilityData?.type){e.accessibilityData.structParent>=0||(e.parentTreeId=a++);s=!1}}if(s){for(const e of t.values())for(const t of e){delete t.parentTreeId;delete t.structTreeParent}return!1}return!0}async updateStructureTree({newAnnotationsByPage:e,pdfManager:t,changes:a}){const{ref:r,xref:i}=this,n=this.dict.clone(),s=new RefSetCache;s.put(r,n);let o,c=n.getRaw("ParentTree");if(c instanceof Ref)o=i.fetch(c);else{o=c;c=i.getNewTemporaryRef();n.set("ParentTree",c)}o=o.clone();s.put(c,o);let l=o.getRaw("Nums"),h=null;if(l instanceof Ref){h=l;l=i.fetch(h)}l=l.slice();h||o.set("Nums",l);const u=await StructTreeRoot.#Q({newAnnotationsByPage:e,structTreeRootRef:r,structTreeRoot:this,kids:null,nums:l,xref:i,pdfManager:t,changes:a,cache:s});if(-1!==u){n.set("ParentTreeNextKey",u);h&&s.put(h,l);for(const[e,t]of s.items())a.put(e,{data:t})}}static async#Q({newAnnotationsByPage:e,structTreeRootRef:t,structTreeRoot:a,kids:r,nums:i,xref:n,pdfManager:s,changes:o,cache:c}){const l=Name.get("OBJR");let h,u=-1;for(const[d,f]of e){const e=await s.getPage(d),{ref:g}=e,p=g instanceof Ref;for(const{accessibilityData:s,ref:m,parentTreeId:b,structTreeParent:y}of f){if(!s?.type)continue;const{structParent:f}=s;if(a&&Number.isInteger(f)&&f>=0){let t=(h||=new Map).get(d);if(void 0===t){t=new StructTreePage(a,e.pageDict).collectObjects(g);h.set(d,t)}const r=t?.get(f);if(r){const e=n.fetch(r).clone();StructTreeRoot.#te(e,s);o.put(r,{data:e});continue}}u=Math.max(u,b);const w=n.getNewTemporaryRef(),S=new Dict(n);StructTreeRoot.#te(S,s);await this.#ae({structTreeParent:y,tagDict:S,newTagRef:w,structTreeRootRef:t,fallbackKids:r,xref:n,cache:c});const x=new Dict(n);S.set("K",x);x.set("Type",l);p&&x.set("Pg",g);x.set("Obj",m);c.put(w,S);i.push(b,w)}}return u+1}static#te(e,{type:t,title:a,lang:r,alt:i,expanded:n,actualText:s}){e.set("S",Name.get(t));a&&e.set("T",stringToAsciiOrUTF16BE(a));r&&e.set("Lang",stringToAsciiOrUTF16BE(r));i&&e.set("Alt",stringToAsciiOrUTF16BE(i));n&&e.set("E",stringToAsciiOrUTF16BE(n));s&&e.set("ActualText",stringToAsciiOrUTF16BE(s))}static#ee({elements:e,xref:t,pageDict:a,numberTree:r}){const i=new Map;for(const t of e)if(t.structTreeParentId){const e=parseInt(t.structTreeParentId.split("_mc")[1],10);let a=i.get(e);if(!a){a=[];i.set(e,a)}a.push(t)}const n=a.get("StructParents");if(!Number.isInteger(n))return;const s=r.get(n),updateElement=(e,a,r)=>{const n=i.get(e);if(n){const e=a.getRaw("P"),i=t.fetchIfRef(e);if(e instanceof Ref&&i instanceof Dict){const e={ref:r,dict:a};for(const t of n)t.structTreeParent=e}return!0}return!1};for(const e of s){if(!(e instanceof Ref))continue;const a=t.fetch(e),r=a.get("K");if(Number.isInteger(r))updateElement(r,a,e);else if(Array.isArray(r))for(let i of r){i=t.fetchIfRef(i);if(Number.isInteger(i)&&updateElement(i,a,e))break;if(!(i instanceof Dict))continue;if(!isName(i.get("Type"),"MCR"))break;const r=i.get("MCID");if(Number.isInteger(r)&&updateElement(r,a,e))break}}}static async#ae({structTreeParent:e,tagDict:t,newTagRef:a,structTreeRootRef:r,fallbackKids:i,xref:n,cache:s}){let o,c=null;if(e){({ref:c}=e);o=e.dict.getRaw("P")||r}else o=r;t.set("P",o);const l=n.fetchIfRef(o);if(!l){i.push(a);return}let h=s.get(o);if(!h){h=l.clone();s.put(o,h)}const u=h.getRaw("K");let d=u instanceof Ref?s.get(u):null;if(!d){d=n.fetchIfRef(u);d=Array.isArray(d)?d.slice():[u];const e=n.getNewTemporaryRef();h.set("K",e);s.put(e,d)}const f=d.indexOf(c);d.splice(f>=0?f+1:d.length,0,a)}}class StructElementNode{constructor(e,t){this.tree=e;this.xref=e.xref;this.dict=t;this.kids=[];this.parseKids()}get role(){const e=this.dict.get("S"),t=e instanceof Name?e.name:"",{root:a}=this.tree;return a.roleMap.get(t)??t}parseKids(){let e=null;const t=this.dict.getRaw("Pg");t instanceof Ref&&(e=t.toString());const a=this.dict.get("K");if(Array.isArray(a))for(const t of a){const a=this.parseKid(e,this.xref.fetchIfRef(t));a&&this.kids.push(a)}else{const t=this.parseKid(e,a);t&&this.kids.push(t)}}parseKid(e,t){if(Number.isInteger(t))return this.tree.pageDict.objId!==e?null:new StructElement({type:Hn,mcid:t,pageObjId:e});if(!(t instanceof Dict))return null;const a=t.getRaw("Pg");a instanceof Ref&&(e=a.toString());const r=t.get("Type")instanceof Name?t.get("Type").name:null;if("MCR"===r){if(this.tree.pageDict.objId!==e)return null;const a=t.getRaw("Stm");return new StructElement({type:Wn,refObjId:a instanceof Ref?a.toString():null,pageObjId:e,mcid:t.get("MCID")})}if("OBJR"===r){if(this.tree.pageDict.objId!==e)return null;const a=t.getRaw("Obj");return new StructElement({type:zn,refObjId:a instanceof Ref?a.toString():null,pageObjId:e})}return new StructElement({type:Gn,dict:t})}}class StructElement{constructor({type:e,dict:t=null,mcid:a=null,pageObjId:r=null,refObjId:i=null}){this.type=e;this.dict=t;this.mcid=a;this.pageObjId=r;this.refObjId=i;this.parentNode=null}}class StructTreePage{constructor(e,t){this.root=e;this.xref=e?.xref??null;this.rootDict=e?.dict??null;this.pageDict=t;this.nodes=[]}collectObjects(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return null;const t=this.rootDict.get("ParentTree");if(!t)return null;const a=this.root.structParentIds?.get(e);if(!a)return null;const r=new Map,i=new NumberTree(t,this.xref);for(const[e]of a){const t=i.getRaw(e);t instanceof Ref&&r.set(e,t)}return r}parse(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return;const t=this.rootDict.get("ParentTree");if(!t)return;const a=this.pageDict.get("StructParents"),r=this.root.structParentIds?.get(e);if(!Number.isInteger(a)&&!r)return;const i=new Map,n=new NumberTree(t,this.xref);if(Number.isInteger(a)){const e=n.get(a);if(Array.isArray(e))for(const t of e)t instanceof Ref&&this.addNode(this.xref.fetch(t),i)}if(r)for(const[e,t]of r){const a=n.get(e);if(a){const e=this.addNode(this.xref.fetchIfRef(a),i);1===e?.kids?.length&&e.kids[0].type===zn&&(e.kids[0].type=t)}}}addNode(e,t,a=0){if(a>40){warn("StructTree MAX_DEPTH reached.");return null}if(!(e instanceof Dict))return null;if(t.has(e))return t.get(e);const r=new StructElementNode(this,e);t.set(e,r);const i=e.get("P");if(!(i instanceof Dict)||isName(i.get("Type"),"StructTreeRoot")){this.addTopLevelNode(e,r)||t.delete(e);return r}const n=this.addNode(i,t,a+1);if(!n)return r;let s=!1;for(const t of n.kids)if(t.type===Gn&&t.dict===e){t.parentNode=r;s=!0}s||t.delete(e);return r}addTopLevelNode(e,t){const a=this.rootDict.get("K");if(!a)return!1;if(a instanceof Dict){if(a.objId!==e.objId)return!1;this.nodes[0]=t;return!0}if(!Array.isArray(a))return!0;let r=!1;for(let i=0;i40){warn("StructTree too deep to be fully serialized.");return}const r=Object.create(null);r.role=e.role;r.children=[];t.children.push(r);let i=e.dict.get("Alt");"string"!=typeof i&&(i=e.dict.get("ActualText"));"string"==typeof i&&(r.alt=stringToPDFString(i));const n=e.dict.get("A");if(n instanceof Dict){const e=lookupNormalRect(n.getArray("BBox"),null);if(e)r.bbox=e;else{const e=n.get("Width"),t=n.get("Height");"number"==typeof e&&e>0&&"number"==typeof t&&t>0&&(r.bbox=[0,0,e,t])}}const s=e.dict.get("Lang");"string"==typeof s&&(r.lang=stringToPDFString(s));for(const t of e.kids){const e=t.type===Gn?t.parentNode:null;e?nodeToSerializable(e,r,a+1):t.type===Hn||t.type===Wn?r.children.push({type:"content",id:`p${t.pageObjId}_mc${t.mcid}`}):t.type===zn?r.children.push({type:"object",id:t.refObjId}):t.type===$n&&r.children.push({type:"annotation",id:`pdfjs_internal_id_${t.refObjId}`})}}const e=Object.create(null);e.children=[];e.role="Root";for(const t of this.nodes)t&&nodeToSerializable(t,e);return e}}const Vn=function _isValidExplicitDest(e,t,a){if(!Array.isArray(a)||a.length<2)return!1;const[r,i,...n]=a;if(!e(r)&&!Number.isInteger(r))return!1;if(!t(i))return!1;const s=n.length;let o=!0;switch(i.name){case"XYZ":if(s<2||s>3)return!1;break;case"Fit":case"FitB":return 0===s;case"FitH":case"FitBH":case"FitV":case"FitBV":if(s>1)return!1;break;case"FitR":if(4!==s)return!1;o=!1;break;default:return!1}for(const e of n)if(!("number"==typeof e||o&&null===e))return!1;return!0}.bind(null,(e=>e instanceof Ref),isName);function fetchDest(e){e instanceof Dict&&(e=e.get("D"));return Vn(e)?e:null}function fetchRemoteDest(e){let t=e.get("D");if(t){t instanceof Name&&(t=t.name);if("string"==typeof t)return stringToPDFString(t,!0);if(Vn(t))return JSON.stringify(t)}return null}class Catalog{#re=null;#ie=null;builtInCMapCache=new Map;fontCache=new RefSetCache;globalColorSpaceCache=new GlobalColorSpaceCache;globalImageCache=new GlobalImageCache;nonBlendModesSet=new RefSet;pageDictCache=new RefSetCache;pageIndexCache=new RefSetCache;pageKidsCountCache=new RefSetCache;standardFontDataCache=new Map;systemFontCache=new Map;constructor(e,t){this.pdfManager=e;this.xref=t;this.#ie=t.getCatalogObj();if(!(this.#ie instanceof Dict))throw new FormatError("Catalog object is not a dictionary.");this.toplevelPagesDict}cloneDict(){return this.#ie.clone()}get version(){const e=this.#ie.get("Version");if(e instanceof Name){if(Ca.test(e.name))return shadow(this,"version",e.name);warn(`Invalid PDF catalog version: ${e.name}`)}return shadow(this,"version",null)}get lang(){const e=this.#ie.get("Lang");return shadow(this,"lang",e&&"string"==typeof e?stringToPDFString(e):null)}get needsRendering(){const e=this.#ie.get("NeedsRendering");return shadow(this,"needsRendering","boolean"==typeof e&&e)}get collection(){let e=null;try{const t=this.#ie.get("Collection");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch Collection entry; assuming no collection is present.")}return shadow(this,"collection",e)}get acroForm(){let e=null;try{const t=this.#ie.get("AcroForm");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch AcroForm entry; assuming no forms are present.")}return shadow(this,"acroForm",e)}get acroFormRef(){const e=this.#ie.getRaw("AcroForm");return shadow(this,"acroFormRef",e instanceof Ref?e:null)}get metadata(){const e=this.#ie.getRaw("Metadata");if(!(e instanceof Ref))return shadow(this,"metadata",null);let t=null;try{const a=this.xref.fetch(e,!this.xref.encrypt?.encryptMetadata);if(a instanceof BaseStream&&a.dict instanceof Dict){const e=a.dict.get("Type"),r=a.dict.get("Subtype");if(isName(e,"Metadata")&&isName(r,"XML")){const e=stringToUTF8String(a.getString());e&&(t=new MetadataParser(e).serializable)}}}catch(e){if(e instanceof MissingDataException)throw e;info(`Skipping invalid Metadata: "${e}".`)}return shadow(this,"metadata",t)}get markInfo(){let e=null;try{e=this.#ne()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read mark info.")}return shadow(this,"markInfo",e)}#ne(){const e=this.#ie.get("MarkInfo");if(!(e instanceof Dict))return null;const t={Marked:!1,UserProperties:!1,Suspects:!1};for(const a in t){const r=e.get(a);"boolean"==typeof r&&(t[a]=r)}return t}get structTreeRoot(){let e=null;try{e=this.#se()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable read to structTreeRoot info.")}return shadow(this,"structTreeRoot",e)}#se(){const e=this.#ie.getRaw("StructTreeRoot"),t=this.xref.fetchIfRef(e);if(!(t instanceof Dict))return null;const a=new StructTreeRoot(this.xref,t,e);a.init();return a}get toplevelPagesDict(){const e=this.#ie.get("Pages");if(!(e instanceof Dict))throw new FormatError("Invalid top-level pages dictionary.");return shadow(this,"toplevelPagesDict",e)}get documentOutline(){let e=null;try{e=this.#oe()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read document outline.")}return shadow(this,"documentOutline",e)}#oe(){let e=this.#ie.get("Outlines");if(!(e instanceof Dict))return null;e=e.getRaw("First");if(!(e instanceof Ref))return null;const t={items:[]},a=[{obj:e,parent:t}],r=new RefSet;r.put(e);const i=this.xref,n=new Uint8ClampedArray(3);for(;a.length>0;){const t=a.shift(),s=i.fetchIfRef(t.obj);if(null===s)continue;s.has("Title")||warn("Invalid outline item encountered.");const o={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:s,resultObj:o,docBaseUrl:this.baseUrl,docAttachments:this.attachments});const c=s.get("Title"),l=s.get("F")||0,h=s.getArray("C"),u=s.get("Count");let d=n;!isNumberArray(h,3)||0===h[0]&&0===h[1]&&0===h[2]||(d=ColorSpaceUtils.rgb.getRgb(h,0));const f={action:o.action,attachment:o.attachment,dest:o.dest,url:o.url,unsafeUrl:o.unsafeUrl,newWindow:o.newWindow,setOCGState:o.setOCGState,title:"string"==typeof c?stringToPDFString(c):"",color:d,count:Number.isInteger(u)?u:void 0,bold:!!(2&l),italic:!!(1&l),items:[]};t.parent.items.push(f);e=s.getRaw("First");if(e instanceof Ref&&!r.has(e)){a.push({obj:e,parent:f});r.put(e)}e=s.getRaw("Next");if(e instanceof Ref&&!r.has(e)){a.push({obj:e,parent:t.parent});r.put(e)}}return t.items.length>0?t.items:null}get permissions(){let e=null;try{e=this.#ce()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read permissions.")}return shadow(this,"permissions",e)}#ce(){const e=this.xref.trailer.get("Encrypt");if(!(e instanceof Dict))return null;let t=e.get("P");if("number"!=typeof t)return null;t+=2**32;const a=[];for(const e in w){const r=w[e];t&r&&a.push(r)}return a}get optionalContentConfig(){let e=null;try{const t=this.#ie.get("OCProperties");if(!t)return shadow(this,"optionalContentConfig",null);const a=t.get("D");if(!a)return shadow(this,"optionalContentConfig",null);const r=t.get("OCGs");if(!Array.isArray(r))return shadow(this,"optionalContentConfig",null);const i=new RefSetCache;for(const e of r)e instanceof Ref&&!i.has(e)&&i.put(e,this.#le(e));e=this.#he(a,i)}catch(e){if(e instanceof MissingDataException)throw e;warn(`Unable to read optional content config: ${e}`)}return shadow(this,"optionalContentConfig",e)}#le(e){const t=this.xref.fetch(e),a={id:e.toString(),name:null,intent:null,usage:{print:null,view:null},rbGroups:[]},r=t.get("Name");"string"==typeof r&&(a.name=stringToPDFString(r));let i=t.getArray("Intent");Array.isArray(i)||(i=[i]);i.every((e=>e instanceof Name))&&(a.intent=i.map((e=>e.name)));const n=t.get("Usage");if(!(n instanceof Dict))return a;const s=a.usage,o=n.get("Print");if(o instanceof Dict){const e=o.get("PrintState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":s.print={printState:e.name}}}const c=n.get("View");if(c instanceof Dict){const e=c.get("ViewState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":s.view={viewState:e.name}}}return a}#he(e,t){function parseOnOff(e){const a=[];if(Array.isArray(e))for(const r of e)r instanceof Ref&&t.has(r)&&a.push(r.toString());return a}function parseOrder(e,a=0){if(!Array.isArray(e))return null;const i=[];for(const n of e){if(n instanceof Ref&&t.has(n)){r.put(n);i.push(n.toString());continue}const e=parseNestedOrder(n,a);e&&i.push(e)}if(a>0)return i;const n=[];for(const[e]of t.items())r.has(e)||n.push(e.toString());n.length&&i.push({name:null,order:n});return i}function parseNestedOrder(e,t){if(++t>i){warn("parseNestedOrder - reached MAX_NESTED_LEVELS.");return null}const r=a.fetchIfRef(e);if(!Array.isArray(r))return null;const n=a.fetchIfRef(r[0]);if("string"!=typeof n)return null;const s=parseOrder(r.slice(1),t);return s?.length?{name:stringToPDFString(n),order:s}:null}const a=this.xref,r=new RefSet,i=10;!function parseRBGroups(e){if(Array.isArray(e))for(const r of e){const e=a.fetchIfRef(r);if(!Array.isArray(e)||!e.length)continue;const i=new Set;for(const a of e)if(a instanceof Ref&&t.has(a)&&!i.has(a.toString())){i.add(a.toString());t.get(a).rbGroups.push(i)}}}(e.get("RBGroups"));return{name:"string"==typeof e.get("Name")?stringToPDFString(e.get("Name")):null,creator:"string"==typeof e.get("Creator")?stringToPDFString(e.get("Creator")):null,baseState:e.get("BaseState")instanceof Name?e.get("BaseState").name:null,on:parseOnOff(e.get("ON")),off:parseOnOff(e.get("OFF")),order:parseOrder(e.get("Order")),groups:[...t]}}setActualNumPages(e=null){this.#re=e}get hasActualNumPages(){return null!==this.#re}get _pagesCount(){const e=this.toplevelPagesDict.get("Count");if(!Number.isInteger(e))throw new FormatError("Page count in top-level pages dictionary is not an integer.");return shadow(this,"_pagesCount",e)}get numPages(){return this.#re??this._pagesCount}get destinations(){const e=this.#ue(),t=Object.create(null);for(const a of e)if(a instanceof NameTree)for(const[e,r]of a.getAll()){const a=fetchDest(r);a&&(t[stringToPDFString(e,!0)]=a)}else if(a instanceof Dict)for(const[e,r]of a){const a=fetchDest(r);a&&(t[stringToPDFString(e,!0)]||=a)}return shadow(this,"destinations",t)}getDestination(e){if(this.hasOwnProperty("destinations"))return this.destinations[e]??null;const t=this.#ue();for(const a of t)if(a instanceof NameTree||a instanceof Dict){const t=fetchDest(a.get(e));if(t)return t}if(t.length){const t=this.destinations[e];if(t)return t}return null}#ue(){const e=this.#ie.get("Names"),t=[];e?.has("Dests")&&t.push(new NameTree(e.getRaw("Dests"),this.xref));this.#ie.has("Dests")&&t.push(this.#ie.get("Dests"));return t}get pageLabels(){let e=null;try{e=this.#de()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read page labels.")}return shadow(this,"pageLabels",e)}#de(){const e=this.#ie.getRaw("PageLabels");if(!e)return null;const t=new Array(this.numPages);let a=null,r="";const i=new NumberTree(e,this.xref).getAll();let n="",s=1;for(let e=0,o=this.numPages;e=1))throw new FormatError("Invalid start in PageLabel dictionary.");s=e}else s=1}switch(a){case"D":n=s;break;case"R":case"r":n=toRomanNumerals(s,"r"===a);break;case"A":case"a":const e=26,t="a"===a?97:65,r=s-1;n=String.fromCharCode(t+r%e).repeat(Math.floor(r/e)+1);break;default:if(a)throw new FormatError(`Invalid style "${a}" in PageLabel dictionary.`);n=""}t[e]=r+n;s++}return t}get pageLayout(){const e=this.#ie.get("PageLayout");let t="";if(e instanceof Name)switch(e.name){case"SinglePage":case"OneColumn":case"TwoColumnLeft":case"TwoColumnRight":case"TwoPageLeft":case"TwoPageRight":t=e.name}return shadow(this,"pageLayout",t)}get pageMode(){const e=this.#ie.get("PageMode");let t="UseNone";if(e instanceof Name)switch(e.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"FullScreen":case"UseOC":case"UseAttachments":t=e.name}return shadow(this,"pageMode",t)}get viewerPreferences(){const e=this.#ie.get("ViewerPreferences");if(!(e instanceof Dict))return shadow(this,"viewerPreferences",null);let t=null;for(const[a,r]of e){let e;switch(a){case"HideToolbar":case"HideMenubar":case"HideWindowUI":case"FitWindow":case"CenterWindow":case"DisplayDocTitle":case"PickTrayByPDFSize":"boolean"==typeof r&&(e=r);break;case"NonFullScreenPageMode":if(r instanceof Name)switch(r.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"UseOC":e=r.name;break;default:e="UseNone"}break;case"Direction":if(r instanceof Name)switch(r.name){case"L2R":case"R2L":e=r.name;break;default:e="L2R"}break;case"ViewArea":case"ViewClip":case"PrintArea":case"PrintClip":if(r instanceof Name)switch(r.name){case"MediaBox":case"CropBox":case"BleedBox":case"TrimBox":case"ArtBox":e=r.name;break;default:e="CropBox"}break;case"PrintScaling":if(r instanceof Name)switch(r.name){case"None":case"AppDefault":e=r.name;break;default:e="AppDefault"}break;case"Duplex":if(r instanceof Name)switch(r.name){case"Simplex":case"DuplexFlipShortEdge":case"DuplexFlipLongEdge":e=r.name;break;default:e="None"}break;case"PrintPageRange":if(Array.isArray(r)&&r.length%2==0){r.every(((e,t,a)=>Number.isInteger(e)&&e>0&&(0===t||e>=a[t-1])&&e<=this.numPages))&&(e=r)}break;case"NumCopies":Number.isInteger(r)&&r>0&&(e=r);break;default:warn(`Ignoring non-standard key in ViewerPreferences: ${a}.`);continue}if(void 0!==e){t??=Object.create(null);t[a]=e}else warn(`Bad value, for key "${a}", in ViewerPreferences: ${r}.`)}return shadow(this,"viewerPreferences",t)}get openAction(){const e=this.#ie.get("OpenAction"),t=Object.create(null);if(e instanceof Dict){const a=new Dict(this.xref);a.set("A",e);const r={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:a,resultObj:r});Array.isArray(r.dest)?t.dest=r.dest:r.action&&(t.action=r.action)}else Vn(e)&&(t.dest=e);return shadow(this,"openAction",objectSize(t)>0?t:null)}get attachments(){const e=this.#ie.get("Names");let t=null;if(e instanceof Dict&&e.has("EmbeddedFiles")){const a=new NameTree(e.getRaw("EmbeddedFiles"),this.xref);for(const[e,r]of a.getAll()){const a=new FileSpec(r,this.xref);t??=Object.create(null);t[stringToPDFString(e,!0)]=a.serializable}}return shadow(this,"attachments",t)}get xfaImages(){const e=this.#ie.get("Names");let t=null;if(e instanceof Dict&&e.has("XFAImages")){const a=new NameTree(e.getRaw("XFAImages"),this.xref);for(const[e,r]of a.getAll())if(r instanceof BaseStream){t??=new Map;t.set(stringToPDFString(e,!0),r.getBytes())}}return shadow(this,"xfaImages",t)}#fe(){const e=this.#ie.get("Names");let t=null;function appendIfJavaScriptDict(e,a){if(!(a instanceof Dict))return;if(!isName(a.get("S"),"JavaScript"))return;let r=a.get("JS");if(r instanceof BaseStream)r=r.getString();else if("string"!=typeof r)return;r=stringToPDFString(r,!0).replaceAll("\0","");r&&(t||=new Map).set(e,r)}if(e instanceof Dict&&e.has("JavaScript")){const t=new NameTree(e.getRaw("JavaScript"),this.xref);for(const[e,a]of t.getAll())appendIfJavaScriptDict(stringToPDFString(e,!0),a)}const a=this.#ie.get("OpenAction");a&&appendIfJavaScriptDict("OpenAction",a);return t}get jsActions(){const e=this.#fe();let t=collectActions(this.xref,this.#ie,we);if(e){t||=Object.create(null);for(const[a,r]of e)a in t?t[a].push(r):t[a]=[r]}return shadow(this,"jsActions",t)}async cleanup(e=!1){clearGlobalCaches();this.globalColorSpaceCache.clear();this.globalImageCache.clear(e);this.pageKidsCountCache.clear();this.pageIndexCache.clear();this.pageDictCache.clear();this.nonBlendModesSet.clear();for(const{dict:e}of await Promise.all(this.fontCache))delete e.cacheKey;this.fontCache.clear();this.builtInCMapCache.clear();this.standardFontDataCache.clear();this.systemFontCache.clear()}async getPageDict(e){const t=[this.toplevelPagesDict],a=new RefSet,r=this.#ie.getRaw("Pages");r instanceof Ref&&a.put(r);const i=this.xref,n=this.pageKidsCountCache,s=this.pageIndexCache,o=this.pageDictCache;let c=0;for(;t.length;){const r=t.pop();if(r instanceof Ref){const l=n.get(r);if(l>=0&&c+l<=e){c+=l;continue}if(a.has(r))throw new FormatError("Pages tree contains circular reference.");a.put(r);const h=await(o.get(r)||i.fetchAsync(r));if(h instanceof Dict){let t=h.getRaw("Type");t instanceof Ref&&(t=await i.fetchAsync(t));if(isName(t,"Page")||!h.has("Kids")){n.has(r)||n.put(r,1);s.has(r)||s.put(r,c);if(c===e)return[h,r];c++;continue}}t.push(h);continue}if(!(r instanceof Dict))throw new FormatError("Page dictionary kid reference points to wrong type of object.");const{objId:l}=r;let h=r.getRaw("Count");h instanceof Ref&&(h=await i.fetchAsync(h));if(Number.isInteger(h)&&h>=0){l&&!n.has(l)&&n.put(l,h);if(c+h<=e){c+=h;continue}}let u=r.getRaw("Kids");u instanceof Ref&&(u=await i.fetchAsync(u));if(!Array.isArray(u)){let t=r.getRaw("Type");t instanceof Ref&&(t=await i.fetchAsync(t));if(isName(t,"Page")||!r.has("Kids")){if(c===e)return[r,null];c++;continue}throw new FormatError("Page dictionary kids object is not an array.")}for(let e=u.length-1;e>=0;e--){const a=u[e];t.push(a);r===this.toplevelPagesDict&&a instanceof Ref&&!o.has(a)&&o.put(a,i.fetchAsync(a))}}throw new Error(`Page index ${e} not found.`)}async getAllPageDicts(e=!1){const{ignoreErrors:t}=this.pdfManager.evaluatorOptions,a=[{currentNode:this.toplevelPagesDict,posInKids:0}],r=new RefSet,i=this.#ie.getRaw("Pages");i instanceof Ref&&r.put(i);const n=new Map,s=this.xref,o=this.pageIndexCache;let c=0;function addPageDict(e,t){t&&!o.has(t)&&o.put(t,c);n.set(c++,[e,t])}function addPageError(a){if(a instanceof XRefEntryException&&!e)throw a;if(e&&t&&0===c){warn(`getAllPageDicts - Skipping invalid first page: "${a}".`);a=Dict.empty}n.set(c++,[a,null])}for(;a.length>0;){const e=a.at(-1),{currentNode:t,posInKids:i}=e;let n=t.getRaw("Kids");if(n instanceof Ref)try{n=await s.fetchAsync(n)}catch(e){addPageError(e);break}if(!Array.isArray(n)){addPageError(new FormatError("Page dictionary kids object is not an array."));break}if(i>=n.length){a.pop();continue}const o=n[i];let c;if(o instanceof Ref){if(r.has(o)){addPageError(new FormatError("Pages tree contains circular reference."));break}r.put(o);try{c=await s.fetchAsync(o)}catch(e){addPageError(e);break}}else c=o;if(!(c instanceof Dict)){addPageError(new FormatError("Page dictionary kid reference points to wrong type of object."));break}let l=c.getRaw("Type");if(l instanceof Ref)try{l=await s.fetchAsync(l)}catch(e){addPageError(e);break}isName(l,"Page")||!c.has("Kids")?addPageDict(c,o instanceof Ref?o:null):a.push({currentNode:c,posInKids:0});e.posInKids++}return n}getPageIndex(e){const t=this.pageIndexCache.get(e);if(void 0!==t)return Promise.resolve(t);const a=this.xref;let r=0;const next=t=>function pagesBeforeRef(t){let r,i=0;return a.fetchAsync(t).then((function(a){if(isRefsEqual(t,e)&&!isDict(a,"Page")&&!(a instanceof Dict&&!a.has("Type")&&a.has("Contents")))throw new FormatError("The reference does not point to a /Page dictionary.");if(!a)return null;if(!(a instanceof Dict))throw new FormatError("Node must be a dictionary.");r=a.getRaw("Parent");return a.getAsync("Parent")})).then((function(e){if(!e)return null;if(!(e instanceof Dict))throw new FormatError("Parent must be a dictionary.");return e.getAsync("Kids")})).then((function(e){if(!e)return null;const n=[];let s=!1;for(const r of e){if(!(r instanceof Ref))throw new FormatError("Kid must be a reference.");if(isRefsEqual(r,t)){s=!0;break}n.push(a.fetchAsync(r).then((function(e){if(!(e instanceof Dict))throw new FormatError("Kid node must be a dictionary.");e.has("Count")?i+=e.get("Count"):i++})))}if(!s)throw new FormatError("Kid reference not found in parent's kids.");return Promise.all(n).then((()=>[i,r]))}))}(t).then((t=>{if(!t){this.pageIndexCache.put(e,r);return r}const[a,i]=t;r+=a;return next(i)}));return next(e)}get baseUrl(){const e=this.#ie.get("URI");if(e instanceof Dict){const t=e.get("Base");if("string"==typeof t){const e=createValidAbsoluteUrl(t,null,{tryConvertEncoding:!0});if(e)return shadow(this,"baseUrl",e.href)}}return shadow(this,"baseUrl",this.pdfManager.docBaseUrl)}static parseDestDictionary({destDict:e,resultObj:t,docBaseUrl:a=null,docAttachments:r=null}){if(!(e instanceof Dict)){warn("parseDestDictionary: `destDict` must be a dictionary.");return}let i,n,s=e.get("A");if(!(s instanceof Dict))if(e.has("Dest"))s=e.get("Dest");else{s=e.get("AA");s instanceof Dict&&(s.has("D")?s=s.get("D"):s.has("U")&&(s=s.get("U")))}if(s instanceof Dict){const e=s.get("S");if(!(e instanceof Name)){warn("parseDestDictionary: Invalid type in Action dictionary.");return}const a=e.name;switch(a){case"ResetForm":const e=s.get("Flags"),o=!(1&("number"==typeof e?e:0)),c=[],l=[];for(const e of s.get("Fields")||[])e instanceof Ref?l.push(e.toString()):"string"==typeof e&&c.push(stringToPDFString(e));t.resetForm={fields:c,refs:l,include:o};break;case"URI":i=s.get("URI");i instanceof Name&&(i="/"+i.name);break;case"GoTo":n=s.get("D");break;case"Launch":case"GoToR":const h=s.get("F");if(h instanceof Dict){const e=new FileSpec(h,null,!0),{rawFilename:t}=e.serializable;i=t}else"string"==typeof h&&(i=h);const u=fetchRemoteDest(s);u&&"string"==typeof i&&(i=i.split("#",1)[0]+"#"+u);const d=s.get("NewWindow");"boolean"==typeof d&&(t.newWindow=d);break;case"GoToE":const f=s.get("T");let g;if(r&&f instanceof Dict){const e=f.get("R"),t=f.get("N");isName(e,"C")&&"string"==typeof t&&(g=r[stringToPDFString(t,!0)])}if(g){t.attachment=g;const e=fetchRemoteDest(s);e&&(t.attachmentDest=e)}else warn('parseDestDictionary - unimplemented "GoToE" action.');break;case"Named":const p=s.get("N");p instanceof Name&&(t.action=p.name);break;case"SetOCGState":const m=s.get("State"),b=s.get("PreserveRB");if(!Array.isArray(m)||0===m.length)break;const y=[];for(const e of m)if(e instanceof Name)switch(e.name){case"ON":case"OFF":case"Toggle":y.push(e.name)}else e instanceof Ref&&y.push(e.toString());if(y.length!==m.length)break;t.setOCGState={state:y,preserveRB:"boolean"!=typeof b||b};break;case"JavaScript":const w=s.get("JS");let S;w instanceof BaseStream?S=w.getString():"string"==typeof w&&(S=w);const x=S&&recoverJsURL(stringToPDFString(S,!0));if(x){i=x.url;t.newWindow=x.newWindow;break}default:if("JavaScript"===a||"SubmitForm"===a)break;warn(`parseDestDictionary - unsupported action: "${a}".`)}}else e.has("Dest")&&(n=e.get("Dest"));if("string"==typeof i){const e=createValidAbsoluteUrl(i,a,{addDefaultProtocol:!0,tryConvertEncoding:!0});e&&(t.url=e.href);t.unsafeUrl=i}if(n){n instanceof Name&&(n=n.name);"string"==typeof n?t.dest=stringToPDFString(n,!0):Vn(n)&&(t.dest=n)}}}function addChildren(e,t){if(e instanceof Dict)e=e.getRawValues();else if(e instanceof BaseStream)e=e.dict.getRawValues();else if(!Array.isArray(e))return;for(const r of e)((a=r)instanceof Ref||a instanceof Dict||a instanceof BaseStream||Array.isArray(a))&&t.push(r);var a}class ObjectLoader{refSet=new RefSet;constructor(e,t,a){this.dict=e;this.keys=t;this.xref=a}async load(){const{keys:e,dict:t}=this,a=[];for(const r of e){const e=t.getRaw(r);void 0!==e&&a.push(e)}await this.#ge(a);this.refSet=null}async#ge(e){const t=[],a=[];for(;e.length;){let r=e.pop();if(r instanceof Ref){if(this.refSet.has(r))continue;try{this.refSet.put(r);r=this.xref.fetch(r)}catch(e){if(!(e instanceof MissingDataException)){warn(`ObjectLoader.#walk - requesting all data: "${e}".`);await this.xref.stream.manager.requestAllChunks();return}t.push(r);a.push({begin:e.begin,end:e.end})}}if(r instanceof BaseStream){const e=r.getBaseStreams();if(e){let i=!1;for(const t of e)if(!t.isDataLoaded){i=!0;a.push({begin:t.start,end:t.end})}i&&t.push(r)}}addChildren(r,e)}if(a.length){await this.xref.stream.manager.requestRanges(a);for(const e of t)e instanceof Ref&&this.refSet.remove(e);await this.#ge(t)}}static async load(e,t,a){if(a.stream.isDataLoaded)return;const r=new ObjectLoader(e,t,a);await r.load()}}const Kn=Symbol(),Jn=Symbol(),Yn=Symbol(),Zn=Symbol(),Qn=Symbol(),es=Symbol(),ts=Symbol(),as=Symbol(),rs=Symbol(),is=Symbol("content"),ns=Symbol("data"),ss=Symbol(),os=Symbol("extra"),cs=Symbol(),ls=Symbol(),hs=Symbol(),us=Symbol(),ds=Symbol(),fs=Symbol(),gs=Symbol(),ps=Symbol(),ms=Symbol(),bs=Symbol(),ys=Symbol(),ws=Symbol(),Ss=Symbol(),xs=Symbol(),As=Symbol(),ks=Symbol(),Cs=Symbol(),vs=Symbol(),Fs=Symbol(),Is=Symbol(),Os=Symbol(),Ts=Symbol(),Ms=Symbol(),Ds=Symbol(),Bs=Symbol(),Rs=Symbol(),Ns=Symbol(),Es=Symbol(),Ls=Symbol(),js=Symbol(),_s=Symbol(),Us=Symbol(),Xs=Symbol("namespaceId"),qs=Symbol("nodeName"),Hs=Symbol(),Ws=Symbol(),zs=Symbol(),$s=Symbol(),Gs=Symbol(),Vs=Symbol(),Ks=Symbol(),Js=Symbol(),Ys=Symbol("root"),Zs=Symbol(),Qs=Symbol(),eo=Symbol(),to=Symbol(),ao=Symbol(),ro=Symbol(),io=Symbol(),no=Symbol(),so=Symbol(),oo=Symbol(),co=Symbol(),lo=Symbol("uid"),ho=Symbol(),uo={config:{id:0,check:e=>e.startsWith("http://www.xfa.org/schema/xci/")},connectionSet:{id:1,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-connection-set/")},datasets:{id:2,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-data/")},form:{id:3,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-form/")},localeSet:{id:4,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-locale-set/")},pdf:{id:5,check:e=>"http://ns.adobe.com/xdp/pdf/"===e},signature:{id:6,check:e=>"http://www.w3.org/2000/09/xmldsig#"===e},sourceSet:{id:7,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-source-set/")},stylesheet:{id:8,check:e=>"http://www.w3.org/1999/XSL/Transform"===e},template:{id:9,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-template/")},xdc:{id:10,check:e=>e.startsWith("http://www.xfa.org/schema/xdc/")},xdp:{id:11,check:e=>"http://ns.adobe.com/xdp/"===e},xfdf:{id:12,check:e=>"http://ns.adobe.com/xfdf/"===e},xhtml:{id:13,check:e=>"http://www.w3.org/1999/xhtml"===e},xmpmeta:{id:14,check:e=>"http://ns.adobe.com/xmpmeta/"===e}},fo={pt:e=>e,cm:e=>e/2.54*72,mm:e=>e/25.4*72,in:e=>72*e,px:e=>e},go=/([+-]?\d+\.?\d*)(.*)/;function stripQuotes(e){return e.startsWith("'")||e.startsWith('"')?e.slice(1,-1):e}function getInteger({data:e,defaultValue:t,validate:a}){if(!e)return t;e=e.trim();const r=parseInt(e,10);return!isNaN(r)&&a(r)?r:t}function getFloat({data:e,defaultValue:t,validate:a}){if(!e)return t;e=e.trim();const r=parseFloat(e);return!isNaN(r)&&a(r)?r:t}function getKeyword({data:e,defaultValue:t,validate:a}){return e&&a(e=e.trim())?e:t}function getStringOption(e,t){return getKeyword({data:e,defaultValue:t[0],validate:e=>t.includes(e)})}function getMeasurement(e,t="0"){t||="0";if(!e)return getMeasurement(t);const a=e.trim().match(go);if(!a)return getMeasurement(t);const[,r,i]=a,n=parseFloat(r);if(isNaN(n))return getMeasurement(t);if(0===n)return 0;const s=fo[i];return s?s(n):n}function getRatio(e){if(!e)return{num:1,den:1};const t=e.split(":",2).map((e=>parseFloat(e.trim()))).filter((e=>!isNaN(e)));1===t.length&&t.push(1);if(0===t.length)return{num:1,den:1};const[a,r]=t;return{num:a,den:r}}function getRelevant(e){return e?e.trim().split(/\s+/).map((e=>({excluded:"-"===e[0],viewname:e.substring(1)}))):[]}class HTMLResult{static get FAILURE(){return shadow(this,"FAILURE",new HTMLResult(!1,null,null,null))}static get EMPTY(){return shadow(this,"EMPTY",new HTMLResult(!0,null,null,null))}constructor(e,t,a,r){this.success=e;this.html=t;this.bbox=a;this.breakNode=r}isBreak(){return!!this.breakNode}static breakNode(e){return new HTMLResult(!1,null,null,e)}static success(e,t=null){return new HTMLResult(!0,e,t,null)}}class FontFinder{constructor(e){this.fonts=new Map;this.cache=new Map;this.warned=new Set;this.defaultFont=null;this.add(e)}add(e,t=null){for(const t of e)this.addPdfFont(t);for(const e of this.fonts.values())e.regular||(e.regular=e.italic||e.bold||e.bolditalic);if(!t||0===t.size)return;const a=this.fonts.get("PdfJS-Fallback-PdfJS-XFA");for(const e of t)this.fonts.set(e,a)}addPdfFont(e){const t=e.cssFontInfo,a=t.fontFamily;let r=this.fonts.get(a);if(!r){r=Object.create(null);this.fonts.set(a,r);this.defaultFont||(this.defaultFont=r)}let i="";const n=parseFloat(t.fontWeight);0!==parseFloat(t.italicAngle)?i=n>=700?"bolditalic":"italic":n>=700&&(i="bold");if(!i){(e.name.includes("Bold")||e.psName?.includes("Bold"))&&(i="bold");(e.name.includes("Italic")||e.name.endsWith("It")||e.psName?.includes("Italic")||e.psName?.endsWith("It"))&&(i+="italic")}i||(i="regular");r[i]=e}getDefault(){return this.defaultFont}find(e,t=!0){let a=this.fonts.get(e)||this.cache.get(e);if(a)return a;const r=/,|-|_| |bolditalic|bold|italic|regular|it/gi;let i=e.replaceAll(r,"");a=this.fonts.get(i);if(a){this.cache.set(e,a);return a}i=i.toLowerCase();const n=[];for(const[e,t]of this.fonts.entries())e.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(t);if(0===n.length)for(const[,e]of this.fonts.entries())e.regular.name?.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(e);if(0===n.length){i=i.replaceAll(/psmt|mt/gi,"");for(const[e,t]of this.fonts.entries())e.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(t)}if(0===n.length)for(const e of this.fonts.values())e.regular.name?.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(e);if(n.length>=1){1!==n.length&&t&&warn(`XFA - Too many choices to guess the correct font: ${e}`);this.cache.set(e,n[0]);return n[0]}if(t&&!this.warned.has(e)){this.warned.add(e);warn(`XFA - Cannot find the font: ${e}`)}return null}}function selectFont(e,t){return"italic"===e.posture?"bold"===e.weight?t.bolditalic:t.italic:"bold"===e.weight?t.bold:t.regular}class FontInfo{constructor(e,t,a,r){this.lineHeight=a;this.paraMargin=t||{top:0,bottom:0,left:0,right:0};if(!e){[this.pdfFont,this.xfaFont]=this.defaultFont(r);return}this.xfaFont={typeface:e.typeface,posture:e.posture,weight:e.weight,size:e.size,letterSpacing:e.letterSpacing};const i=r.find(e.typeface);if(i){this.pdfFont=selectFont(e,i);this.pdfFont||([this.pdfFont,this.xfaFont]=this.defaultFont(r))}else[this.pdfFont,this.xfaFont]=this.defaultFont(r)}defaultFont(e){const t=e.find("Helvetica",!1)||e.find("Myriad Pro",!1)||e.find("Arial",!1)||e.getDefault();if(t?.regular){const e=t.regular;return[e,{typeface:e.cssFontInfo.fontFamily,posture:"normal",weight:"normal",size:10,letterSpacing:0}]}return[null,{typeface:"Courier",posture:"normal",weight:"normal",size:10,letterSpacing:0}]}}class FontSelector{constructor(e,t,a,r){this.fontFinder=r;this.stack=[new FontInfo(e,t,a,r)]}pushData(e,t,a){const r=this.stack.at(-1);for(const t of["typeface","posture","weight","size","letterSpacing"])e[t]||(e[t]=r.xfaFont[t]);for(const e of["top","bottom","left","right"])isNaN(t[e])&&(t[e]=r.paraMargin[e]);const i=new FontInfo(e,t,a||r.lineHeight,this.fontFinder);i.pdfFont||(i.pdfFont=r.pdfFont);this.stack.push(i)}popFont(){this.stack.pop()}topFont(){return this.stack.at(-1)}}class TextMeasure{constructor(e,t,a,r){this.glyphs=[];this.fontSelector=new FontSelector(e,t,a,r);this.extraHeight=0}pushData(e,t,a){this.fontSelector.pushData(e,t,a)}popFont(e){return this.fontSelector.popFont()}addPara(){const e=this.fontSelector.topFont();this.extraHeight+=e.paraMargin.top+e.paraMargin.bottom}addString(e){if(!e)return;const t=this.fontSelector.topFont(),a=t.xfaFont.size;if(t.pdfFont){const r=t.xfaFont.letterSpacing,i=t.pdfFont,n=i.lineHeight||1.2,s=t.lineHeight||Math.max(1.2,n)*a,o=n-(void 0===i.lineGap?.2:i.lineGap),c=Math.max(1,o)*a,l=a/1e3,h=i.defaultWidth||i.charsToGlyphs(" ")[0].width;for(const t of e.split(/[\u2029\n]/)){const e=i.encodeString(t).join(""),a=i.charsToGlyphs(e);for(const e of a){const t=e.width||h;this.glyphs.push([t*l+r,s,c,e.unicode,!1])}this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}else{for(const t of e.split(/[\u2029\n]/)){for(const e of t.split(""))this.glyphs.push([a,1.2*a,a,e,!1]);this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}}compute(e){let t=-1,a=0,r=0,i=0,n=0,s=0,o=!1,c=!0;for(let l=0,h=this.glyphs.length;le){r=Math.max(r,n);n=0;i+=s;s=m;t=-1;a=0;o=!0;c=!1}else{s=Math.max(m,s);a=n;n+=h;t=l}else if(n+h>e){i+=s;s=m;if(-1!==t){l=t;r=Math.max(r,a);n=0;t=-1;a=0}else{r=Math.max(r,n);n=h}o=!0;c=!1}else{n+=h;s=Math.max(m,s)}}r=Math.max(r,n);i+=s+this.extraHeight;return{width:1.02*r,height:i,isBroken:o}}}const po=/^[^.[]+/,mo=/^[^\]]+/,bo=0,yo=1,wo=2,So=3,xo=4,Ao=new Map([["$data",(e,t)=>e.datasets?e.datasets.data:e],["$record",(e,t)=>(e.datasets?e.datasets.data:e)[ws]()[0]],["$template",(e,t)=>e.template],["$connectionSet",(e,t)=>e.connectionSet],["$form",(e,t)=>e.form],["$layout",(e,t)=>e.layout],["$host",(e,t)=>e.host],["$dataWindow",(e,t)=>e.dataWindow],["$event",(e,t)=>e.event],["!",(e,t)=>e.datasets],["$xfa",(e,t)=>e],["xfa",(e,t)=>e],["$",(e,t)=>t]]),ko=new WeakMap;function parseExpression(e,t,a=!0){let r=e.match(po);if(!r)return null;let[i]=r;const n=[{name:i,cacheName:"."+i,index:0,js:null,formCalc:null,operator:bo}];let s=i.length;for(;s0&&h.push(e)}if(0!==h.length||o||0!==c)e=isFinite(l)?h.filter((e=>le[l])):h.flat();else{const a=t[ks]();if(!(t=a))return null;c=-1;e=[t]}}return 0===e.length?null:e}function createDataNode(e,t,a){const r=parseExpression(a);if(!r)return null;if(r.some((e=>e.operator===yo)))return null;const i=Ao.get(r[0].name);let n=0;if(i){e=i(e,t);n=1}else e=t||e;for(let t=r.length;ne[io]())).join("")}get[Fo](){const e=Object.getPrototypeOf(this);if(!e._attributes){const t=e._attributes=new Set;for(const e of Object.getOwnPropertyNames(this)){if(null===this[e]||this[e]instanceof XFAObject||this[e]instanceof XFAObjectArray)break;t.add(e)}}return shadow(this,Fo,e._attributes)}[Rs](e){let t=this;for(;t;){if(t===e)return!0;t=t[ks]()}return!1}[ks](){return this[jo]}[As](){return this[ks]()}[ws](e=null){return e?this[e]:this[Io]}[ss](){const e=Object.create(null);this[is]&&(e.$content=this[is]);for(const t of Object.getOwnPropertyNames(this)){const a=this[t];null!==a&&(a instanceof XFAObject?e[t]=a[ss]():a instanceof XFAObjectArray?a.isEmpty()||(e[t]=a.dump()):e[t]=a)}return e}[co](){return null}[so](){return HTMLResult.EMPTY}*[Ss](){for(const e of this[ws]())yield e}*[Bo](e,t){for(const a of this[Ss]())if(!e||t===e.has(a[qs])){const e=this[ds](),t=a[so](e);t.success||(this[os].failingNode=a);yield t}}[ls](){return null}[Jn](e,t){this[os].children.push(e)}[ds](){}[Zn]({filter:e=null,include:t=!0}){if(this[os].generator){const e=this[ds](),t=this[os].failingNode[so](e);if(!t.success)return t;t.html&&this[Jn](t.html,t.bbox);delete this[os].failingNode}else this[os].generator=this[Bo](e,t);for(;;){const e=this[os].generator.next();if(e.done)break;const t=e.value;if(!t.success)return t;t.html&&this[Jn](t.html,t.bbox)}this[os].generator=null;return HTMLResult.EMPTY}[to](e){this[Uo]=new Set(Object.keys(e))}[No](e){const t=this[Fo],a=this[Uo];return[...e].filter((e=>t.has(e)&&!a.has(e)))}[Zs](e,t=new Set){for(const a of this[Io])a[_o](e,t)}[_o](e,t){const a=this[Ro](e,t);a?this[Co](a,e,t):this[Zs](e,t)}[Ro](e,t){const{use:a,usehref:r}=this;if(!a&&!r)return null;let i=null,n=null,s=null,o=a;if(r){o=r;r.startsWith("#som(")&&r.endsWith(")")?n=r.slice(5,-1):r.startsWith(".#som(")&&r.endsWith(")")?n=r.slice(6,-1):r.startsWith("#")?s=r.slice(1):r.startsWith(".#")&&(s=r.slice(2))}else a.startsWith("#")?s=a.slice(1):n=a;this.use=this.usehref="";if(s)i=e.get(s);else{i=searchNode(e.get(Ys),this,n,!0,!1);i&&(i=i[0])}if(!i){warn(`XFA - Invalid prototype reference: ${o}.`);return null}if(i[qs]!==this[qs]){warn(`XFA - Incompatible prototype: ${i[qs]} !== ${this[qs]}.`);return null}if(t.has(i)){warn("XFA - Cycle detected in prototypes use.");return null}t.add(i);const c=i[Ro](e,t);c&&i[Co](c,e,t);i[Zs](e,t);t.delete(i);return i}[Co](e,t,a){if(a.has(e)){warn("XFA - Cycle detected in prototypes use.");return}!this[is]&&e[is]&&(this[is]=e[is]);new Set(a).add(e);for(const t of this[No](e[Uo])){this[t]=e[t];this[Uo]&&this[Uo].add(t)}for(const r of Object.getOwnPropertyNames(this)){if(this[Fo].has(r))continue;const i=this[r],n=e[r];if(i instanceof XFAObjectArray){for(const e of i[Io])e[_o](t,a);for(let r=i[Io].length,s=n[Io].length;rXFAObject[Oo](e))):"object"==typeof e&&null!==e?Object.assign({},e):e}[as](){const e=Object.create(Object.getPrototypeOf(this));for(const t of Object.getOwnPropertySymbols(this))try{e[t]=this[t]}catch{shadow(e,t,this[t])}e[lo]=`${e[qs]}${qo++}`;e[Io]=[];for(const t of Object.getOwnPropertyNames(this)){if(this[Fo].has(t)){e[t]=XFAObject[Oo](this[t]);continue}const a=this[t];e[t]=a instanceof XFAObjectArray?new XFAObjectArray(a[Po]):null}for(const t of this[Io]){const a=t[qs],r=t[as]();e[Io].push(r);r[jo]=e;null===e[a]?e[a]=r:e[a][Io].push(r)}return e}[ws](e=null){return e?this[Io].filter((t=>t[qs]===e)):this[Io]}[fs](e){return this[e]}[gs](e,t,a=!0){return Array.from(this[ps](e,t,a))}*[ps](e,t,a=!0){if("parent"!==e){for(const a of this[Io]){a[qs]===e&&(yield a);a.name===e&&(yield a);(t||a[js]())&&(yield*a[ps](e,t,!1))}a&&this[Fo].has(e)&&(yield new XFAAttribute(this,e,this[e]))}else yield this[jo]}}class XFAObjectArray{constructor(e=1/0){this[Po]=e;this[Io]=[]}get isXFAObject(){return!1}get isXFAObjectArray(){return!0}push(e){if(this[Io].length<=this[Po]){this[Io].push(e);return!0}warn(`XFA - node "${e[qs]}" accepts no more than ${this[Po]} children`);return!1}isEmpty(){return 0===this[Io].length}dump(){return 1===this[Io].length?this[Io][0][ss]():this[Io].map((e=>e[ss]()))}[as](){const e=new XFAObjectArray(this[Po]);e[Io]=this[Io].map((e=>e[as]()));return e}get children(){return this[Io]}clear(){this[Io].length=0}}class XFAAttribute{constructor(e,t,a){this[jo]=e;this[qs]=t;this[is]=a;this[rs]=!1;this[lo]="attribute"+qo++}[ks](){return this[jo]}[Bs](){return!0}[ms](){return this[is].trim()}[ao](e){e=e.value||"";this[is]=e.toString()}[io](){return this[is]}[Rs](e){return this[jo]===e||this[jo][Rs](e)}}class XmlObject extends XFAObject{constructor(e,t,a={}){super(e,t);this[is]="";this[Mo]=null;if("#text"!==t){const e=new Map;this[vo]=e;for(const[t,r]of Object.entries(a))e.set(t,new XFAAttribute(this,t,r));if(a.hasOwnProperty(Hs)){const e=a[Hs].xfa.dataNode;void 0!==e&&("dataGroup"===e?this[Mo]=!1:"dataValue"===e&&(this[Mo]=!0))}}this[rs]=!1}[oo](e){const t=this[qs];if("#text"===t){e.push(encodeToXmlString(this[is]));return}const a=utf8StringToString(t),r=this[Xs]===Ho?"xfa:":"";e.push(`<${r}${a}`);for(const[t,a]of this[vo].entries()){const r=utf8StringToString(t);e.push(` ${r}="${encodeToXmlString(a[is])}"`)}null!==this[Mo]&&(this[Mo]?e.push(' xfa:dataNode="dataValue"'):e.push(' xfa:dataNode="dataGroup"'));if(this[is]||0!==this[Io].length){e.push(">");if(this[is])"string"==typeof this[is]?e.push(encodeToXmlString(this[is])):this[is][oo](e);else for(const t of this[Io])t[oo](e);e.push(``)}else e.push("/>")}[Ws](e){if(this[is]){const e=new XmlObject(this[Xs],"#text");this[Yn](e);e[is]=this[is];this[is]=""}this[Yn](e);return!0}[$s](e){this[is]+=e}[cs](){if(this[is]&&this[Io].length>0){const e=new XmlObject(this[Xs],"#text");this[Yn](e);e[is]=this[is];delete this[is]}}[so](){return"#text"===this[qs]?HTMLResult.success({name:"#text",value:this[is]}):HTMLResult.EMPTY}[ws](e=null){return e?this[Io].filter((t=>t[qs]===e)):this[Io]}[us](){return this[vo]}[fs](e){const t=this[vo].get(e);return void 0!==t?t:this[ws](e)}*[ps](e,t){const a=this[vo].get(e);a&&(yield a);for(const a of this[Io]){a[qs]===e&&(yield a);t&&(yield*a[ps](e,t))}}*[hs](e,t){const a=this[vo].get(e);!a||t&&a[rs]||(yield a);for(const a of this[Io])yield*a[hs](e,t)}*[ys](e,t,a){for(const r of this[Io]){r[qs]!==e||a&&r[rs]||(yield r);t&&(yield*r[ys](e,t,a))}}[Bs](){return null===this[Mo]?0===this[Io].length||this[Io][0][Xs]===uo.xhtml.id:this[Mo]}[ms](){return null===this[Mo]?0===this[Io].length?this[is].trim():this[Io][0][Xs]===uo.xhtml.id?this[Io][0][io]().trim():null:this[is].trim()}[ao](e){e=e.value||"";this[is]=e.toString()}[ss](e=!1){const t=Object.create(null);e&&(t.$ns=this[Xs]);this[is]&&(t.$content=this[is]);t.$name=this[qs];t.children=[];for(const a of this[Io])t.children.push(a[ss](e));t.attributes=Object.create(null);for(const[e,a]of this[vo])t.attributes[e]=a[is];return t}}class ContentObject extends XFAObject{constructor(e,t){super(e,t);this[is]=""}[$s](e){this[is]+=e}[cs](){}}class OptionObject extends ContentObject{constructor(e,t,a){super(e,t);this[Lo]=a}[cs](){this[is]=getKeyword({data:this[is],defaultValue:this[Lo][0],validate:e=>this[Lo].includes(e)})}[Qn](e){super[Qn](e);delete this[Lo]}}class StringObject extends ContentObject{[cs](){this[is]=this[is].trim()}}class IntegerObject extends ContentObject{constructor(e,t,a,r){super(e,t);this[Do]=a;this[Xo]=r}[cs](){this[is]=getInteger({data:this[is],defaultValue:this[Do],validate:this[Xo]})}[Qn](e){super[Qn](e);delete this[Do];delete this[Xo]}}class Option01 extends IntegerObject{constructor(e,t){super(e,t,0,(e=>1===e))}}class Option10 extends IntegerObject{constructor(e,t){super(e,t,1,(e=>0===e))}}function measureToString(e){return"string"==typeof e?"0px":Number.isInteger(e)?`${e}px`:`${e.toFixed(2)}px`}const Wo={anchorType(e,t){const a=e[As]();if(a&&(!a.layout||"position"===a.layout)){"transform"in t||(t.transform="");switch(e.anchorType){case"bottomCenter":t.transform+="translate(-50%, -100%)";break;case"bottomLeft":t.transform+="translate(0,-100%)";break;case"bottomRight":t.transform+="translate(-100%,-100%)";break;case"middleCenter":t.transform+="translate(-50%,-50%)";break;case"middleLeft":t.transform+="translate(0,-50%)";break;case"middleRight":t.transform+="translate(-100%,-50%)";break;case"topCenter":t.transform+="translate(-50%,0)";break;case"topRight":t.transform+="translate(-100%,0)"}}},dimensions(e,t){const a=e[As]();let r=e.w;const i=e.h;if(a.layout?.includes("row")){const t=a[os],i=e.colSpan;let n;if(-1===i){n=Math.sumPrecise(t.columnWidths.slice(t.currentColumn));t.currentColumn=0}else{n=Math.sumPrecise(t.columnWidths.slice(t.currentColumn,t.currentColumn+i));t.currentColumn=(t.currentColumn+e.colSpan)%t.columnWidths.length}isNaN(n)||(r=e.w=n)}t.width=""!==r?measureToString(r):"auto";t.height=""!==i?measureToString(i):"auto"},position(e,t){const a=e[As]();if(!a?.layout||"position"===a.layout){t.position="absolute";t.left=measureToString(e.x);t.top=measureToString(e.y)}},rotate(e,t){if(e.rotate){"transform"in t||(t.transform="");t.transform+=`rotate(-${e.rotate}deg)`;t.transformOrigin="top left"}},presence(e,t){switch(e.presence){case"invisible":t.visibility="hidden";break;case"hidden":case"inactive":t.display="none"}},hAlign(e,t){if("para"===e[qs])switch(e.hAlign){case"justifyAll":t.textAlign="justify-all";break;case"radix":t.textAlign="left";break;default:t.textAlign=e.hAlign}else switch(e.hAlign){case"left":t.alignSelf="start";break;case"center":t.alignSelf="center";break;case"right":t.alignSelf="end"}},margin(e,t){e.margin&&(t.margin=e.margin[co]().margin)}};function setMinMaxDimensions(e,t){if("position"===e[As]().layout){e.minW>0&&(t.minWidth=measureToString(e.minW));e.maxW>0&&(t.maxWidth=measureToString(e.maxW));e.minH>0&&(t.minHeight=measureToString(e.minH));e.maxH>0&&(t.maxHeight=measureToString(e.maxH))}}function layoutText(e,t,a,r,i,n){const s=new TextMeasure(t,a,r,i);"string"==typeof e?s.addString(e):e[Gs](s);return s.compute(n)}function layoutNode(e,t){let a=null,r=null,i=!1;if((!e.w||!e.h)&&e.value){let n=0,s=0;if(e.margin){n=e.margin.leftInset+e.margin.rightInset;s=e.margin.topInset+e.margin.bottomInset}let o=null,c=null;if(e.para){c=Object.create(null);o=""===e.para.lineHeight?null:e.para.lineHeight;c.top=""===e.para.spaceAbove?0:e.para.spaceAbove;c.bottom=""===e.para.spaceBelow?0:e.para.spaceBelow;c.left=""===e.para.marginLeft?0:e.para.marginLeft;c.right=""===e.para.marginRight?0:e.para.marginRight}let l=e.font;if(!l){const t=e[Cs]();let a=e[ks]();for(;a&&a!==t;){if(a.font){l=a.font;break}a=a[ks]()}}const h=(e.w||t.width)-n,u=e[vs].fontFinder;if(e.value.exData&&e.value.exData[is]&&"text/html"===e.value.exData.contentType){const t=layoutText(e.value.exData[is],l,c,o,u,h);r=t.width;a=t.height;i=t.isBroken}else{const t=e.value[io]();if(t){const e=layoutText(t,l,c,o,u,h);r=e.width;a=e.height;i=e.isBroken}}null===r||e.w||(r+=n);null===a||e.h||(a+=s)}return{w:r,h:a,isBroken:i}}function computeBbox(e,t,a){let r;if(""!==e.w&&""!==e.h)r=[e.x,e.y,e.w,e.h];else{if(!a)return null;let i=e.w;if(""===i){if(0===e.maxW){const t=e[As]();i="position"===t.layout&&""!==t.w?0:e.minW}else i=Math.min(e.maxW,a.width);t.attributes.style.width=measureToString(i)}let n=e.h;if(""===n){if(0===e.maxH){const t=e[As]();n="position"===t.layout&&""!==t.h?0:e.minH}else n=Math.min(e.maxH,a.height);t.attributes.style.height=measureToString(n)}r=[e.x,e.y,i,n]}return r}function fixDimensions(e){const t=e[As]();if(t.layout?.includes("row")){const a=t[os],r=e.colSpan;let i;i=-1===r?Math.sumPrecise(a.columnWidths.slice(a.currentColumn)):Math.sumPrecise(a.columnWidths.slice(a.currentColumn,a.currentColumn+r));isNaN(i)||(e.w=i)}t.layout&&"position"!==t.layout&&(e.x=e.y=0);"table"===e.layout&&""===e.w&&Array.isArray(e.columnWidths)&&(e.w=Math.sumPrecise(e.columnWidths))}function layoutClass(e){switch(e.layout){case"position":default:return"xfaPosition";case"lr-tb":return"xfaLrTb";case"rl-row":return"xfaRlRow";case"rl-tb":return"xfaRlTb";case"row":return"xfaRow";case"table":return"xfaTable";case"tb":return"xfaTb"}}function toStyle(e,...t){const a=Object.create(null);for(const r of t){const t=e[r];if(null!==t)if(Wo.hasOwnProperty(r))Wo[r](e,a);else if(t instanceof XFAObject){const e=t[co]();e?Object.assign(a,e):warn(`(DEBUG) - XFA - style for ${r} not implemented yet`)}}return a}function createWrapper(e,t){const{attributes:a}=t,{style:r}=a,i={name:"div",attributes:{class:["xfaWrapper"],style:Object.create(null)},children:[]};a.class.push("xfaWrapped");if(e.border){const{widths:a,insets:n}=e.border[os];let s,o,c=n[0],l=n[3];const h=n[0]+n[2],u=n[1]+n[3];switch(e.border.hand){case"even":c-=a[0]/2;l-=a[3]/2;s=`calc(100% + ${(a[1]+a[3])/2-u}px)`;o=`calc(100% + ${(a[0]+a[2])/2-h}px)`;break;case"left":c-=a[0];l-=a[3];s=`calc(100% + ${a[1]+a[3]-u}px)`;o=`calc(100% + ${a[0]+a[2]-h}px)`;break;case"right":s=u?`calc(100% - ${u}px)`:"100%";o=h?`calc(100% - ${h}px)`:"100%"}const d=["xfaBorder"];isPrintOnly(e.border)&&d.push("xfaPrintOnly");const f={name:"div",attributes:{class:d,style:{top:`${c}px`,left:`${l}px`,width:s,height:o}},children:[]};for(const e of["border","borderWidth","borderColor","borderRadius","borderStyle"])if(void 0!==r[e]){f.attributes.style[e]=r[e];delete r[e]}i.children.push(f,t)}else i.children.push(t);for(const e of["background","backgroundClip","top","left","width","height","minWidth","minHeight","maxWidth","maxHeight","transform","transformOrigin","visibility"])if(void 0!==r[e]){i.attributes.style[e]=r[e];delete r[e]}i.attributes.style.position="absolute"===r.position?"absolute":"relative";delete r.position;if(r.alignSelf){i.attributes.style.alignSelf=r.alignSelf;delete r.alignSelf}return i}function fixTextIndent(e){const t=getMeasurement(e.textIndent,"0px");if(t>=0)return;const a="padding"+("left"===("right"===e.textAlign?"right":"left")?"Left":"Right"),r=getMeasurement(e[a],"0px");e[a]=r-t+"px"}function setAccess(e,t){switch(e.access){case"nonInteractive":t.push("xfaNonInteractive");break;case"readOnly":t.push("xfaReadOnly");break;case"protected":t.push("xfaDisabled")}}function isPrintOnly(e){return e.relevant.length>0&&!e.relevant[0].excluded&&"print"===e.relevant[0].viewname}function getCurrentPara(e){const t=e[Cs]()[os].paraStack;return t.length?t.at(-1):null}function setPara(e,t,a){if(a.attributes.class?.includes("xfaRich")){if(t){""===e.h&&(t.height="auto");""===e.w&&(t.width="auto")}const r=getCurrentPara(e);if(r){const e=a.attributes.style;e.display="flex";e.flexDirection="column";switch(r.vAlign){case"top":e.justifyContent="start";break;case"bottom":e.justifyContent="end";break;case"middle":e.justifyContent="center"}const t=r[co]();for(const[a,r]of Object.entries(t))a in e||(e[a]=r)}}}function setFontFamily(e,t,a,r){if(!a){delete r.fontFamily;return}const i=stripQuotes(e.typeface);r.fontFamily=`"${i}"`;const n=a.find(i);if(n){const{fontFamily:a}=n.regular.cssFontInfo;a!==i&&(r.fontFamily=`"${a}"`);const s=getCurrentPara(t);if(s&&""!==s.lineHeight)return;if(r.lineHeight)return;const o=selectFont(e,n);o&&(r.lineHeight=Math.max(1.2,o.lineHeight))}}function fixURL(e){const t=createValidAbsoluteUrl(e,null,{addDefaultProtocol:!0,tryConvertEncoding:!0});return t?t.href:null}function createLine(e,t){return{name:"div",attributes:{class:["lr-tb"===e.layout?"xfaLr":"xfaRl"]},children:t}}function flushHTML(e){if(!e[os])return null;const t={name:"div",attributes:e[os].attributes,children:e[os].children};if(e[os].failingNode){const a=e[os].failingNode[ls]();a&&(e.layout.endsWith("-tb")?t.children.push(createLine(e,[a])):t.children.push(a))}return 0===t.children.length?null:t}function addHTML(e,t,a){const r=e[os],i=r.availableSpace,[n,s,o,c]=a;switch(e.layout){case"position":r.width=Math.max(r.width,n+o);r.height=Math.max(r.height,s+c);r.children.push(t);break;case"lr-tb":case"rl-tb":if(!r.line||1===r.attempt){r.line=createLine(e,[]);r.children.push(r.line);r.numberInLine=0}r.numberInLine+=1;r.line.children.push(t);if(0===r.attempt){r.currentWidth+=o;r.height=Math.max(r.height,r.prevHeight+c)}else{r.currentWidth=o;r.prevHeight=r.height;r.height+=c;r.attempt=0}r.width=Math.max(r.width,r.currentWidth);break;case"rl-row":case"row":{r.children.push(t);r.width+=o;r.height=Math.max(r.height,c);const e=measureToString(r.height);for(const t of r.children)t.attributes.style.height=e;break}case"table":case"tb":r.width=MathClamp(o,r.width,i.width);r.height+=c;r.children.push(t)}}function getAvailableSpace(e){const t=e[os].availableSpace,a=e.margin?e.margin.topInset+e.margin.bottomInset:0,r=e.margin?e.margin.leftInset+e.margin.rightInset:0;switch(e.layout){case"lr-tb":case"rl-tb":return 0===e[os].attempt?{width:t.width-r-e[os].currentWidth,height:t.height-a-e[os].prevHeight}:{width:t.width-r,height:t.height-a-e[os].height};case"rl-row":case"row":return{width:Math.sumPrecise(e[os].columnWidths.slice(e[os].currentColumn)),height:t.height-r};case"table":case"tb":return{width:t.width-r,height:t.height-a-e[os].height};default:return t}}function checkDimensions(e,t){if(null===e[Cs]()[os].firstUnsplittable)return!0;if(0===e.w||0===e.h)return!0;const a=e[As](),r=a[os]?.attempt||0,[,i,n,s]=function getTransformedBBox(e){let t,a,r=""===e.w?NaN:e.w,i=""===e.h?NaN:e.h,[n,s]=[0,0];switch(e.anchorType||""){case"bottomCenter":[n,s]=[r/2,i];break;case"bottomLeft":[n,s]=[0,i];break;case"bottomRight":[n,s]=[r,i];break;case"middleCenter":[n,s]=[r/2,i/2];break;case"middleLeft":[n,s]=[0,i/2];break;case"middleRight":[n,s]=[r,i/2];break;case"topCenter":[n,s]=[r/2,0];break;case"topRight":[n,s]=[r,0]}switch(e.rotate||0){case 0:[t,a]=[-n,-s];break;case 90:[t,a]=[-s,n];[r,i]=[i,-r];break;case 180:[t,a]=[n,s];[r,i]=[-r,-i];break;case 270:[t,a]=[s,-n];[r,i]=[-i,r]}return[e.x+t+Math.min(0,r),e.y+a+Math.min(0,i),Math.abs(r),Math.abs(i)]}(e);switch(a.layout){case"lr-tb":case"rl-tb":return 0===r?e[Cs]()[os].noLayoutFailure?""!==e.w?Math.round(n-t.width)<=2:t.width>2:!(""!==e.h&&Math.round(s-t.height)>2)&&(""!==e.w?Math.round(n-t.width)<=2||0===a[os].numberInLine&&t.height>2:t.width>2):!!e[Cs]()[os].noLayoutFailure||!(""!==e.h&&Math.round(s-t.height)>2)&&((""===e.w||Math.round(n-t.width)<=2||!a[Ls]())&&t.height>2);case"table":case"tb":return!!e[Cs]()[os].noLayoutFailure||(""===e.h||e[Es]()?(""===e.w||Math.round(n-t.width)<=2||!a[Ls]())&&t.height>2:Math.round(s-t.height)<=2);case"position":if(e[Cs]()[os].noLayoutFailure)return!0;if(""===e.h||Math.round(s+i-t.height)<=2)return!0;return s+i>e[Cs]()[os].currentContentArea.h;case"rl-row":case"row":return!!e[Cs]()[os].noLayoutFailure||(""===e.h||Math.round(s-t.height)<=2);default:return!0}}const zo=uo.template.id,$o="http://www.w3.org/2000/svg",Go=/^H(\d+)$/,Vo=new Set(["image/gif","image/jpeg","image/jpg","image/pjpeg","image/png","image/apng","image/x-png","image/bmp","image/x-ms-bmp","image/tiff","image/tif","application/octet-stream"]),Ko=[[[66,77],"image/bmp"],[[255,216,255],"image/jpeg"],[[73,73,42,0],"image/tiff"],[[77,77,0,42],"image/tiff"],[[71,73,70,56,57,97],"image/gif"],[[137,80,78,71,13,10,26,10],"image/png"]];function getBorderDims(e){if(!e||!e.border)return{w:0,h:0};const t=e.border[bs]();return t?{w:t.widths[0]+t.widths[2]+t.insets[0]+t.insets[2],h:t.widths[1]+t.widths[3]+t.insets[1]+t.insets[3]}:{w:0,h:0}}function hasMargin(e){return e.margin&&(e.margin.topInset||e.margin.rightInset||e.margin.bottomInset||e.margin.leftInset)}function _setValue(e,t){if(!e.value){const t=new Value({});e[Yn](t);e.value=t}e.value[ao](t)}function*getContainedChildren(e){for(const t of e[ws]())t instanceof SubformSet?yield*t[Ss]():yield t}function isRequired(e){return"error"===e.validate?.nullTest}function setTabIndex(e){for(;e;){if(!e.traversal){e[ro]=e[ks]()[ro];return}if(e[ro])return;let t=null;for(const a of e.traversal[ws]())if("next"===a.operation){t=a;break}if(!t||!t.ref){e[ro]=e[ks]()[ro];return}const a=e[Cs]();e[ro]=++a[ro];const r=a[Qs](t.ref,e);if(!r)return;e=r[0]}}function applyAssist(e,t){const a=e.assist;if(a){const e=a[so]();e&&(t.title=e);const r=a.role.match(Go);if(r){const e="heading",a=r[1];t.role=e;t["aria-level"]=a}}if("table"===e.layout)t.role="table";else if("row"===e.layout)t.role="row";else{const a=e[ks]();"row"===a.layout&&(t.role="TH"===a.assist?.role?"columnheader":"cell")}}function ariaLabel(e){if(!e.assist)return null;const t=e.assist;return t.speak&&""!==t.speak[is]?t.speak[is]:t.toolTip?t.toolTip[is]:null}function valueToHtml(e){return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:Object.create(null)},children:[{name:"span",attributes:{style:Object.create(null)},value:e}]})}function setFirstUnsplittable(e){const t=e[Cs]();if(null===t[os].firstUnsplittable){t[os].firstUnsplittable=e;t[os].noLayoutFailure=!0}}function unsetFirstUnsplittable(e){const t=e[Cs]();t[os].firstUnsplittable===e&&(t[os].noLayoutFailure=!1)}function handleBreak(e){if(e[os])return!1;e[os]=Object.create(null);if("auto"===e.targetType)return!1;const t=e[Cs]();let a=null;if(e.target){a=t[Qs](e.target,e[ks]());if(!a)return!1;a=a[0]}const{currentPageArea:r,currentContentArea:i}=t[os];if("pageArea"===e.targetType){a instanceof PageArea||(a=null);if(e.startNew){e[os].target=a||r;return!0}if(a&&a!==r){e[os].target=a;return!0}return!1}a instanceof ContentArea||(a=null);const n=a&&a[ks]();let s,o=n;if(e.startNew)if(a){const e=n.contentArea.children,t=e.indexOf(i),r=e.indexOf(a);-1!==t&&te;r[os].noLayoutFailure=!0;const s=t[so](a);e[Jn](s.html,s.bbox);r[os].noLayoutFailure=i;t[As]=n}class AppearanceFilter extends StringObject{constructor(e){super(zo,"appearanceFilter");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Arc extends XFAObject{constructor(e){super(zo,"arc",!0);this.circular=getInteger({data:e.circular,defaultValue:0,validate:e=>1===e});this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.startAngle=getFloat({data:e.startAngle,defaultValue:0,validate:e=>!0});this.sweepAngle=getFloat({data:e.sweepAngle,defaultValue:360,validate:e=>!0});this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null;this.fill=null}[so](){const e=this.edge||new Edge({}),t=e[co](),a=Object.create(null);"visible"===this.fill?.presence?Object.assign(a,this.fill[co]()):a.fill="transparent";a.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);a.stroke=t.color;let r;const i={xmlns:$o,style:{width:"100%",height:"100%",overflow:"visible"}};if(360===this.sweepAngle)r={name:"ellipse",attributes:{xmlns:$o,cx:"50%",cy:"50%",rx:"50%",ry:"50%",style:a}};else{const e=this.startAngle*Math.PI/180,t=this.sweepAngle*Math.PI/180,n=this.sweepAngle>180?1:0,[s,o,c,l]=[50*(1+Math.cos(e)),50*(1-Math.sin(e)),50*(1+Math.cos(e+t)),50*(1-Math.sin(e+t))];r={name:"path",attributes:{xmlns:$o,d:`M ${s} ${o} A 50 50 0 ${n} 0 ${c} ${l}`,vectorEffect:"non-scaling-stroke",style:a}};Object.assign(i,{viewBox:"0 0 100 100",preserveAspectRatio:"none"})}const n={name:"svg",children:[r],attributes:i};if(hasMargin(this[ks]()[ks]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[n]});n.attributes.style.position="absolute";return HTMLResult.success(n)}}class Area extends XFAObject{constructor(e){super(zo,"area",!0);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null;this.area=new XFAObjectArray;this.draw=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[Ss](){yield*getContainedChildren(this)}[js](){return!0}[Ds](){return!0}[Jn](e,t){const[a,r,i,n]=t;this[os].width=Math.max(this[os].width,a+i);this[os].height=Math.max(this[os].height,r+n);this[os].children.push(e)}[ds](){return this[os].availableSpace}[so](e){const t=toStyle(this,"position"),a={style:t,id:this[lo],class:["xfaArea"]};isPrintOnly(this)&&a.class.push("xfaPrintOnly");this.name&&(a.xfaName=this.name);const r=[];this[os]={children:r,width:0,height:0,availableSpace:e};const i=this[Zn]({filter:new Set(["area","draw","field","exclGroup","subform","subformSet"]),include:!0});if(!i.success){if(i.isBreak())return i;delete this[os];return HTMLResult.FAILURE}t.width=measureToString(this[os].width);t.height=measureToString(this[os].height);const n={name:"div",attributes:a,children:r},s=[this.x,this.y,this[os].width,this[os].height];delete this[os];return HTMLResult.success(n,s)}}class Assist extends XFAObject{constructor(e){super(zo,"assist",!0);this.id=e.id||"";this.role=e.role||"";this.use=e.use||"";this.usehref=e.usehref||"";this.speak=null;this.toolTip=null}[so](){return this.toolTip?.[is]||null}}class Barcode extends XFAObject{constructor(e){super(zo,"barcode",!0);this.charEncoding=getKeyword({data:e.charEncoding?e.charEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.checksum=getStringOption(e.checksum,["none","1mod10","1mod10_1mod11","2mod10","auto"]);this.dataColumnCount=getInteger({data:e.dataColumnCount,defaultValue:-1,validate:e=>e>=0});this.dataLength=getInteger({data:e.dataLength,defaultValue:-1,validate:e=>e>=0});this.dataPrep=getStringOption(e.dataPrep,["none","flateCompress"]);this.dataRowCount=getInteger({data:e.dataRowCount,defaultValue:-1,validate:e=>e>=0});this.endChar=e.endChar||"";this.errorCorrectionLevel=getInteger({data:e.errorCorrectionLevel,defaultValue:-1,validate:e=>e>=0&&e<=8});this.id=e.id||"";this.moduleHeight=getMeasurement(e.moduleHeight,"5mm");this.moduleWidth=getMeasurement(e.moduleWidth,"0.25mm");this.printCheckDigit=getInteger({data:e.printCheckDigit,defaultValue:0,validate:e=>1===e});this.rowColumnRatio=getRatio(e.rowColumnRatio);this.startChar=e.startChar||"";this.textLocation=getStringOption(e.textLocation,["below","above","aboveEmbedded","belowEmbedded","none"]);this.truncate=getInteger({data:e.truncate,defaultValue:0,validate:e=>1===e});this.type=getStringOption(e.type?e.type.toLowerCase():"",["aztec","codabar","code2of5industrial","code2of5interleaved","code2of5matrix","code2of5standard","code3of9","code3of9extended","code11","code49","code93","code128","code128a","code128b","code128c","code128sscc","datamatrix","ean8","ean8add2","ean8add5","ean13","ean13add2","ean13add5","ean13pwcd","fim","logmars","maxicode","msi","pdf417","pdf417macro","plessey","postauscust2","postauscust3","postausreplypaid","postausstandard","postukrm4scc","postusdpbc","postusimb","postusstandard","postus5zip","qrcode","rfid","rss14","rss14expanded","rss14limited","rss14stacked","rss14stackedomni","rss14truncated","telepen","ucc128","ucc128random","ucc128sscc","upca","upcaadd2","upcaadd5","upcapwcd","upce","upceadd2","upceadd5","upcean2","upcean5","upsmaxicode"]);this.upsMode=getStringOption(e.upsMode,["usCarrier","internationalCarrier","secureSymbol","standardSymbol"]);this.use=e.use||"";this.usehref=e.usehref||"";this.wideNarrowRatio=getRatio(e.wideNarrowRatio);this.encrypt=null;this.extras=null}}class Bind extends XFAObject{constructor(e){super(zo,"bind",!0);this.match=getStringOption(e.match,["once","dataRef","global","none"]);this.ref=e.ref||"";this.picture=null}}class BindItems extends XFAObject{constructor(e){super(zo,"bindItems");this.connection=e.connection||"";this.labelRef=e.labelRef||"";this.ref=e.ref||"";this.valueRef=e.valueRef||""}}class Bookend extends XFAObject{constructor(e){super(zo,"bookend");this.id=e.id||"";this.leader=e.leader||"";this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||""}}class BooleanElement extends Option01{constructor(e){super(zo,"boolean");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[so](e){return valueToHtml(1===this[is]?"1":"0")}}class Border extends XFAObject{constructor(e){super(zo,"border",!0);this.break=getStringOption(e.break,["close","open"]);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.extras=null;this.fill=null;this.margin=null}[bs](){if(!this[os]){const e=this.edge.children.slice();if(e.length<4){const t=e.at(-1)||new Edge({});for(let a=e.length;a<4;a++)e.push(t)}const t=e.map((e=>e.thickness)),a=[0,0,0,0];if(this.margin){a[0]=this.margin.topInset;a[1]=this.margin.rightInset;a[2]=this.margin.bottomInset;a[3]=this.margin.leftInset}this[os]={widths:t,insets:a,edges:e}}return this[os]}[co](){const{edges:e}=this[bs](),t=e.map((e=>{const t=e[co]();t.color||="#000000";return t})),a=Object.create(null);this.margin&&Object.assign(a,this.margin[co]());"visible"===this.fill?.presence&&Object.assign(a,this.fill[co]());if(this.corner.children.some((e=>0!==e.radius))){const e=this.corner.children.map((e=>e[co]()));if(2===e.length||3===e.length){const t=e.at(-1);for(let a=e.length;a<4;a++)e.push(t)}a.borderRadius=e.map((e=>e.radius)).join(" ")}switch(this.presence){case"invisible":case"hidden":a.borderStyle="";break;case"inactive":a.borderStyle="none";break;default:a.borderStyle=t.map((e=>e.style)).join(" ")}a.borderWidth=t.map((e=>e.width)).join(" ");a.borderColor=t.map((e=>e.color)).join(" ");return a}}class Break extends XFAObject{constructor(e){super(zo,"break",!0);this.after=getStringOption(e.after,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.afterTarget=e.afterTarget||"";this.before=getStringOption(e.before,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.beforeTarget=e.beforeTarget||"";this.bookendLeader=e.bookendLeader||"";this.bookendTrailer=e.bookendTrailer||"";this.id=e.id||"";this.overflowLeader=e.overflowLeader||"";this.overflowTarget=e.overflowTarget||"";this.overflowTrailer=e.overflowTrailer||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class BreakAfter extends XFAObject{constructor(e){super(zo,"breakAfter",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}}class BreakBefore extends XFAObject{constructor(e){super(zo,"breakBefore",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}[so](e){this[os]={};return HTMLResult.FAILURE}}class Button extends XFAObject{constructor(e){super(zo,"button",!0);this.highlight=getStringOption(e.highlight,["inverted","none","outline","push"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[so](e){const t=this[ks]()[ks](),a={name:"button",attributes:{id:this[lo],class:["xfaButton"],style:{}},children:[]};for(const e of t.event.children){if("click"!==e.activity||!e.script)continue;const t=recoverJsURL(e.script[is]);if(!t)continue;const r=fixURL(t.url);r&&a.children.push({name:"a",attributes:{id:"link"+this[lo],href:r,newWindow:t.newWindow,class:["xfaLink"],style:{}},children:[]})}return HTMLResult.success(a)}}class Calculate extends XFAObject{constructor(e){super(zo,"calculate",!0);this.id=e.id||"";this.override=getStringOption(e.override,["disabled","error","ignore","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.script=null}}class Caption extends XFAObject{constructor(e){super(zo,"caption",!0);this.id=e.id||"";this.placement=getStringOption(e.placement,["left","bottom","inline","right","top"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.reserve=Math.ceil(getMeasurement(e.reserve));this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.font=null;this.margin=null;this.para=null;this.value=null}[ao](e){_setValue(this,e)}[bs](e){if(!this[os]){let{width:t,height:a}=e;switch(this.placement){case"left":case"right":case"inline":t=this.reserve<=0?t:this.reserve;break;case"top":case"bottom":a=this.reserve<=0?a:this.reserve}this[os]=layoutNode(this,{width:t,height:a})}return this[os]}[so](e){if(!this.value)return HTMLResult.EMPTY;this[Ks]();const t=this.value[so](e).html;if(!t){this[Vs]();return HTMLResult.EMPTY}const a=this.reserve;if(this.reserve<=0){const{w:t,h:a}=this[bs](e);switch(this.placement){case"left":case"right":case"inline":this.reserve=t;break;case"top":case"bottom":this.reserve=a}}const r=[];"string"==typeof t?r.push({name:"#text",value:t}):r.push(t);const i=toStyle(this,"font","margin","visibility");switch(this.placement){case"left":case"right":this.reserve>0&&(i.width=measureToString(this.reserve));break;case"top":case"bottom":this.reserve>0&&(i.height=measureToString(this.reserve))}setPara(this,null,t);this[Vs]();this.reserve=a;return HTMLResult.success({name:"div",attributes:{style:i,class:["xfaCaption"]},children:r})}}class Certificate extends StringObject{constructor(e){super(zo,"certificate");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Certificates extends XFAObject{constructor(e){super(zo,"certificates",!0);this.credentialServerPolicy=getStringOption(e.credentialServerPolicy,["optional","required"]);this.id=e.id||"";this.url=e.url||"";this.urlPolicy=e.urlPolicy||"";this.use=e.use||"";this.usehref=e.usehref||"";this.encryption=null;this.issuers=null;this.keyUsage=null;this.oids=null;this.signing=null;this.subjectDNs=null}}class CheckButton extends XFAObject{constructor(e){super(zo,"checkButton",!0);this.id=e.id||"";this.mark=getStringOption(e.mark,["default","check","circle","cross","diamond","square","star"]);this.shape=getStringOption(e.shape,["square","round"]);this.size=getMeasurement(e.size,"10pt");this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"margin"),a=measureToString(this.size);t.width=t.height=a;let r,i,n;const s=this[ks]()[ks](),o=s.items.children.length&&s.items.children[0][so]().html||[],c={on:(void 0!==o[0]?o[0]:"on").toString(),off:(void 0!==o[1]?o[1]:"off").toString()},l=(s.value?.[io]()||"off")===c.on||void 0,h=s[As](),u=s[lo];let d;if(h instanceof ExclGroup){n=h[lo];r="radio";i="xfaRadio";d=h[ns]?.[lo]||h[lo]}else{r="checkbox";i="xfaCheckbox";d=s[ns]?.[lo]||s[lo]}const f={name:"input",attributes:{class:[i],style:t,fieldId:u,dataId:d,type:r,checked:l,xfaOn:c.on,xfaOff:c.off,"aria-label":ariaLabel(s),"aria-required":!1}};n&&(f.attributes.name=n);if(isRequired(s)){f.attributes["aria-required"]=!0;f.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[f]})}}class ChoiceList extends XFAObject{constructor(e){super(zo,"choiceList",!0);this.commitOn=getStringOption(e.commitOn,["select","exit"]);this.id=e.id||"";this.open=getStringOption(e.open,["userControl","always","multiSelect","onEntry"]);this.textEntry=getInteger({data:e.textEntry,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"border","margin"),a=this[ks]()[ks](),r={fontSize:`calc(${a.font?.size||10}px * var(--total-scale-factor))`},i=[];if(a.items.children.length>0){const e=a.items;let t=0,n=0;if(2===e.children.length){t=e.children[0].save;n=1-t}const s=e.children[t][so]().html,o=e.children[n][so]().html;let c=!1;const l=a.value?.[io]()||"";for(let e=0,t=s.length;eMathClamp(parseInt(e.trim(),10),0,255))).map((e=>isNaN(e)?0:e));if(n.length<3)return{r:a,g:r,b:i};[a,r,i]=n;return{r:a,g:r,b:i}}(e.value):"";this.extras=null}[Fs](){return!1}[co](){return this.value?Util.makeHexColor(this.value.r,this.value.g,this.value.b):null}}class Comb extends XFAObject{constructor(e){super(zo,"comb");this.id=e.id||"";this.numberOfCells=getInteger({data:e.numberOfCells,defaultValue:0,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||""}}class Connect extends XFAObject{constructor(e){super(zo,"connect",!0);this.connection=e.connection||"";this.id=e.id||"";this.ref=e.ref||"";this.usage=getStringOption(e.usage,["exportAndImport","exportOnly","importOnly"]);this.use=e.use||"";this.usehref=e.usehref||"";this.picture=null}}class ContentArea extends XFAObject{constructor(e){super(zo,"contentArea",!0);this.h=getMeasurement(e.h);this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=getMeasurement(e.w);this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null}[so](e){const t={left:measureToString(this.x),top:measureToString(this.y),width:measureToString(this.w),height:measureToString(this.h)},a=["xfaContentarea"];isPrintOnly(this)&&a.push("xfaPrintOnly");return HTMLResult.success({name:"div",children:[],attributes:{style:t,class:a,id:this[lo]}})}}class Corner extends XFAObject{constructor(e){super(zo,"corner",!0);this.id=e.id||"";this.inverted=getInteger({data:e.inverted,defaultValue:0,validate:e=>1===e});this.join=getStringOption(e.join,["square","round"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.radius=getMeasurement(e.radius);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](){const e=toStyle(this,"visibility");e.radius=measureToString("square"===this.join?0:this.radius);return e}}class DateElement extends ContentObject{constructor(e){super(zo,"date");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=this[is].trim();this[is]=e?new Date(e):null}[so](e){return valueToHtml(this[is]?this[is].toString():"")}}class DateTime extends ContentObject{constructor(e){super(zo,"dateTime");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=this[is].trim();this[is]=e?new Date(e):null}[so](e){return valueToHtml(this[is]?this[is].toString():"")}}class DateTimeEdit extends XFAObject{constructor(e){super(zo,"dateTimeEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.picker=getStringOption(e.picker,["host","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"border","font","margin"),a=this[ks]()[ks](),r={name:"input",attributes:{type:"text",fieldId:a[lo],dataId:a[ns]?.[lo]||a[lo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){r.attributes["aria-required"]=!0;r.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[r]})}}class Decimal extends ContentObject{constructor(e){super(zo,"decimal");this.fracDigits=getInteger({data:e.fracDigits,defaultValue:2,validate:e=>!0});this.id=e.id||"";this.leadDigits=getInteger({data:e.leadDigits,defaultValue:-1,validate:e=>!0});this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=parseFloat(this[is].trim());this[is]=isNaN(e)?null:e}[so](e){return valueToHtml(null!==this[is]?this[is].toString():"")}}class DefaultUi extends XFAObject{constructor(e){super(zo,"defaultUi",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class Desc extends XFAObject{constructor(e){super(zo,"desc",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class DigestMethod extends OptionObject{constructor(e){super(zo,"digestMethod",["","SHA1","SHA256","SHA512","RIPEMD160"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class DigestMethods extends XFAObject{constructor(e){super(zo,"digestMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.digestMethod=new XFAObjectArray}}class Draw extends XFAObject{constructor(e){super(zo,"draw",!0);this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.border=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.value=null;this.setProperty=new XFAObjectArray}[ao](e){_setValue(this,e)}[so](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;fixDimensions(this);this[Ks]();const t=this.w,a=this.h,{w:r,h:i,isBroken:n}=layoutNode(this,e);if(r&&""===this.w){if(n&&this[As]()[Ls]()){this[Vs]();return HTMLResult.FAILURE}this.w=r}i&&""===this.h&&(this.h=i);setFirstUnsplittable(this);if(!checkDimensions(this,e)){this.w=t;this.h=a;this[Vs]();return HTMLResult.FAILURE}unsetFirstUnsplittable(this);const s=toStyle(this,"font","hAlign","dimensions","position","presence","rotate","anchorType","border","margin");setMinMaxDimensions(this,s);if(s.margin){s.padding=s.margin;delete s.margin}const o=["xfaDraw"];this.font&&o.push("xfaFont");isPrintOnly(this)&&o.push("xfaPrintOnly");const c={style:s,id:this[lo],class:o};this.name&&(c.xfaName=this.name);const l={name:"div",attributes:c,children:[]};applyAssist(this,c);const h=computeBbox(this,l,e),u=this.value?this.value[so](e).html:null;if(null===u){this.w=t;this.h=a;this[Vs]();return HTMLResult.success(createWrapper(this,l),h)}l.children.push(u);setPara(this,s,u);this.w=t;this.h=a;this[Vs]();return HTMLResult.success(createWrapper(this,l),h)}}class Edge extends XFAObject{constructor(e){super(zo,"edge",!0);this.cap=getStringOption(e.cap,["square","butt","round"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](){const e=toStyle(this,"visibility");Object.assign(e,{linecap:this.cap,width:measureToString(this.thickness),color:this.color?this.color[co]():"#000000",style:""});if("visible"!==this.presence)e.style="none";else switch(this.stroke){case"solid":e.style="solid";break;case"dashDot":case"dashDotDot":case"dashed":e.style="dashed";break;case"dotted":e.style="dotted";break;case"embossed":e.style="ridge";break;case"etched":e.style="groove";break;case"lowered":e.style="inset";break;case"raised":e.style="outset"}return e}}class Encoding extends OptionObject{constructor(e){super(zo,"encoding",["adbe.x509.rsa_sha1","adbe.pkcs7.detached","adbe.pkcs7.sha1"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Encodings extends XFAObject{constructor(e){super(zo,"encodings",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encoding=new XFAObjectArray}}class Encrypt extends XFAObject{constructor(e){super(zo,"encrypt",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=null}}class EncryptData extends XFAObject{constructor(e){super(zo,"encryptData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["encrypt","decrypt"]);this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Encryption extends XFAObject{constructor(e){super(zo,"encryption",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class EncryptionMethod extends OptionObject{constructor(e){super(zo,"encryptionMethod",["","AES256-CBC","TRIPLEDES-CBC","AES128-CBC","AES192-CBC"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EncryptionMethods extends XFAObject{constructor(e){super(zo,"encryptionMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encryptionMethod=new XFAObjectArray}}class Event extends XFAObject{constructor(e){super(zo,"event",!0);this.activity=getStringOption(e.activity,["click","change","docClose","docReady","enter","exit","full","indexChange","initialize","mouseDown","mouseEnter","mouseExit","mouseUp","postExecute","postOpen","postPrint","postSave","postSign","postSubmit","preExecute","preOpen","prePrint","preSave","preSign","preSubmit","ready","validationState"]);this.id=e.id||"";this.listen=getStringOption(e.listen,["refOnly","refAndDescendents"]);this.name=e.name||"";this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.encryptData=null;this.execute=null;this.script=null;this.signData=null;this.submit=null}}class ExData extends ContentObject{constructor(e){super(zo,"exData");this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.maxLength=getInteger({data:e.maxLength,defaultValue:-1,validate:e=>e>=-1});this.name=e.name||"";this.rid=e.rid||"";this.transferEncoding=getStringOption(e.transferEncoding,["none","base64","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[Ms](){return"text/html"===this.contentType}[Ws](e){if("text/html"===this.contentType&&e[Xs]===uo.xhtml.id){this[is]=e;return!0}if("text/xml"===this.contentType){this[is]=e;return!0}return!1}[so](e){return"text/html"===this.contentType&&this[is]?this[is][so](e):HTMLResult.EMPTY}}class ExObject extends XFAObject{constructor(e){super(zo,"exObject",!0);this.archive=e.archive||"";this.classId=e.classId||"";this.codeBase=e.codeBase||"";this.codeType=e.codeType||"";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class ExclGroup extends XFAObject{constructor(e){super(zo,"exclGroup",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.accessKey=e.accessKey||"";this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.margin=null;this.para=null;this.traversal=null;this.validate=null;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.field=new XFAObjectArray;this.setProperty=new XFAObjectArray}[Ds](){return!0}[Fs](){return!0}[ao](e){for(const t of this.field.children){if(!t.value){const e=new Value({});t[Yn](e);t.value=e}t.value[ao](e)}}[Ls](){return this.layout.endsWith("-tb")&&0===this[os].attempt&&this[os].numberInLine>0||this[ks]()[Ls]()}[Es](){const e=this[As]();if(!e[Es]())return!1;if(void 0!==this[os]._isSplittable)return this[os]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[os]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[os].numberInLine)return!1;this[os]._isSplittable=!0;return!0}[ls](){return flushHTML(this)}[Jn](e,t){addHTML(this,e,t)}[ds](){return getAvailableSpace(this)}[so](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;fixDimensions(this);const t=[],a={id:this[lo],class:[]};setAccess(this,a.class);this[os]||=Object.create(null);Object.assign(this[os],{children:t,attributes:a,attempt:0,line:null,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const r=this[Es]();r||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const i=new Set(["field"]);if(this.layout.includes("row")){const e=this[As]().columnWidths;if(Array.isArray(e)&&e.length>0){this[os].columnWidths=e;this[os].currentColumn=0}}const n=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),s=["xfaExclgroup"],o=layoutClass(this);o&&s.push(o);isPrintOnly(this)&&s.push("xfaPrintOnly");a.style=n;a.class=s;this.name&&(a.xfaName=this.name);this[Ks]();const c="lr-tb"===this.layout||"rl-tb"===this.layout,l=c?2:1;for(;this[os].attempte>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.format=null;this.items=new XFAObjectArray(2);this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.validate=null;this.value=null;this.bindItems=new XFAObjectArray;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.setProperty=new XFAObjectArray}[Ds](){return!0}[ao](e){_setValue(this,e)}[so](e){setTabIndex(this);if(!this.ui){this.ui=new Ui({});this.ui[vs]=this[vs];this[Yn](this.ui);let e;switch(this.items.children.length){case 0:e=new TextEdit({});this.ui.textEdit=e;break;case 1:e=new CheckButton({});this.ui.checkButton=e;break;case 2:e=new ChoiceList({});this.ui.choiceList=e}this.ui[Yn](e)}if(!this.ui||"hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;this.caption&&delete this.caption[os];this[Ks]();const t=this.caption?this.caption[so](e).html:null,a=this.w,r=this.h;let i=0,n=0;if(this.margin){i=this.margin.leftInset+this.margin.rightInset;n=this.margin.topInset+this.margin.bottomInset}let s=null;if(""===this.w||""===this.h){let t=null,a=null,r=0,o=0;if(this.ui.checkButton)r=o=this.ui.checkButton.size;else{const{w:t,h:a}=layoutNode(this,e);if(null!==t){r=t;o=a}else o=function fonts_getMetrics(e,t=!1){let a=null;if(e){const t=stripQuotes(e.typeface),r=e[vs].fontFinder.find(t);a=selectFont(e,r)}if(!a)return{lineHeight:12,lineGap:2,lineNoGap:10};const r=e.size||10,i=a.lineHeight?Math.max(t?0:1.2,a.lineHeight):1.2,n=void 0===a.lineGap?.2:a.lineGap;return{lineHeight:i*r,lineGap:n*r,lineNoGap:Math.max(1,i-n)*r}}(this.font,!0).lineNoGap}s=getBorderDims(this.ui[bs]());r+=s.w;o+=s.h;if(this.caption){const{w:i,h:n,isBroken:s}=this.caption[bs](e);if(s&&this[As]()[Ls]()){this[Vs]();return HTMLResult.FAILURE}t=i;a=n;switch(this.caption.placement){case"left":case"right":case"inline":t+=r;break;case"top":case"bottom":a+=o}}else{t=r;a=o}if(t&&""===this.w){t+=i;this.w=Math.min(this.maxW<=0?1/0:this.maxW,this.minW+1e>=1&&e<=5});this.appearanceFilter=null;this.certificates=null;this.digestMethods=null;this.encodings=null;this.encryptionMethods=null;this.handler=null;this.lockDocument=null;this.mdp=null;this.reasons=null;this.timeStamp=null}}class Float extends ContentObject{constructor(e){super(zo,"float");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=parseFloat(this[is].trim());this[is]=isNaN(e)?null:e}[so](e){return valueToHtml(null!==this[is]?this[is].toString():"")}}class template_Font extends XFAObject{constructor(e){super(zo,"font",!0);this.baselineShift=getMeasurement(e.baselineShift);this.fontHorizontalScale=getFloat({data:e.fontHorizontalScale,defaultValue:100,validate:e=>e>=0});this.fontVerticalScale=getFloat({data:e.fontVerticalScale,defaultValue:100,validate:e=>e>=0});this.id=e.id||"";this.kerningMode=getStringOption(e.kerningMode,["none","pair"]);this.letterSpacing=getMeasurement(e.letterSpacing,"0");this.lineThrough=getInteger({data:e.lineThrough,defaultValue:0,validate:e=>1===e||2===e});this.lineThroughPeriod=getStringOption(e.lineThroughPeriod,["all","word"]);this.overline=getInteger({data:e.overline,defaultValue:0,validate:e=>1===e||2===e});this.overlinePeriod=getStringOption(e.overlinePeriod,["all","word"]);this.posture=getStringOption(e.posture,["normal","italic"]);this.size=getMeasurement(e.size,"10pt");this.typeface=e.typeface||"Courier";this.underline=getInteger({data:e.underline,defaultValue:0,validate:e=>1===e||2===e});this.underlinePeriod=getStringOption(e.underlinePeriod,["all","word"]);this.use=e.use||"";this.usehref=e.usehref||"";this.weight=getStringOption(e.weight,["normal","bold"]);this.extras=null;this.fill=null}[Qn](e){super[Qn](e);this[vs].usedTypefaces.add(this.typeface)}[co](){const e=toStyle(this,"fill"),t=e.color;if(t)if("#000000"===t)delete e.color;else if(!t.startsWith("#")){e.background=t;e.backgroundClip="text";e.color="transparent"}this.baselineShift&&(e.verticalAlign=measureToString(this.baselineShift));e.fontKerning="none"===this.kerningMode?"none":"normal";e.letterSpacing=measureToString(this.letterSpacing);if(0!==this.lineThrough){e.textDecoration="line-through";2===this.lineThrough&&(e.textDecorationStyle="double")}if(0!==this.overline){e.textDecoration="overline";2===this.overline&&(e.textDecorationStyle="double")}e.fontStyle=this.posture;e.fontSize=measureToString(.99*this.size);setFontFamily(this,this,this[vs].fontFinder,e);if(0!==this.underline){e.textDecoration="underline";2===this.underline&&(e.textDecorationStyle="double")}e.fontWeight=this.weight;return e}}class Format extends XFAObject{constructor(e){super(zo,"format",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null}}class Handler extends StringObject{constructor(e){super(zo,"handler");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Hyphenation extends XFAObject{constructor(e){super(zo,"hyphenation");this.excludeAllCaps=getInteger({data:e.excludeAllCaps,defaultValue:0,validate:e=>1===e});this.excludeInitialCap=getInteger({data:e.excludeInitialCap,defaultValue:0,validate:e=>1===e});this.hyphenate=getInteger({data:e.hyphenate,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.pushCharacterCount=getInteger({data:e.pushCharacterCount,defaultValue:3,validate:e=>e>=0});this.remainCharacterCount=getInteger({data:e.remainCharacterCount,defaultValue:3,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||"";this.wordCharacterCount=getInteger({data:e.wordCharacterCount,defaultValue:7,validate:e=>e>=0})}}class Image extends StringObject{constructor(e){super(zo,"image");this.aspect=getStringOption(e.aspect,["fit","actual","height","none","width"]);this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.name=e.name||"";this.transferEncoding=getStringOption(e.transferEncoding,["base64","none","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[so](){if(this.contentType&&!Vo.has(this.contentType.toLowerCase()))return HTMLResult.EMPTY;let e=this[vs].images?.get(this.href);if(!e&&(this.href||!this[is]))return HTMLResult.EMPTY;e||"base64"!==this.transferEncoding||(e=function fromBase64Util(e){return Uint8Array.fromBase64?Uint8Array.fromBase64(e):stringToBytes(atob(e))}(this[is]));if(!e)return HTMLResult.EMPTY;if(!this.contentType){for(const[t,a]of Ko)if(e.length>t.length&&t.every(((t,a)=>t===e[a]))){this.contentType=a;break}if(!this.contentType)return HTMLResult.EMPTY}const t=new Blob([e],{type:this.contentType});let a;switch(this.aspect){case"fit":case"actual":break;case"height":a={height:"100%",objectFit:"fill"};break;case"none":a={width:"100%",height:"100%",objectFit:"fill"};break;case"width":a={width:"100%",objectFit:"fill"}}const r=this[ks]();return HTMLResult.success({name:"img",attributes:{class:["xfaImage"],style:a,src:URL.createObjectURL(t),alt:r?ariaLabel(r[ks]()):null}})}}class ImageEdit extends XFAObject{constructor(e){super(zo,"imageEdit",!0);this.data=getStringOption(e.data,["link","embed"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[so](e){return"embed"===this.data?HTMLResult.success({name:"div",children:[],attributes:{}}):HTMLResult.EMPTY}}class Integer extends ContentObject{constructor(e){super(zo,"integer");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=parseInt(this[is].trim(),10);this[is]=isNaN(e)?null:e}[so](e){return valueToHtml(null!==this[is]?this[is].toString():"")}}class Issuers extends XFAObject{constructor(e){super(zo,"issuers",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Items extends XFAObject{constructor(e){super(zo,"items",!0);this.id=e.id||"";this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.ref=e.ref||"";this.save=getInteger({data:e.save,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[so](){const e=[];for(const t of this[ws]())e.push(t[io]());return HTMLResult.success(e)}}class Keep extends XFAObject{constructor(e){super(zo,"keep",!0);this.id=e.id||"";const t=["none","contentArea","pageArea"];this.intact=getStringOption(e.intact,t);this.next=getStringOption(e.next,t);this.previous=getStringOption(e.previous,t);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class KeyUsage extends XFAObject{constructor(e){super(zo,"keyUsage");const t=["","yes","no"];this.crlSign=getStringOption(e.crlSign,t);this.dataEncipherment=getStringOption(e.dataEncipherment,t);this.decipherOnly=getStringOption(e.decipherOnly,t);this.digitalSignature=getStringOption(e.digitalSignature,t);this.encipherOnly=getStringOption(e.encipherOnly,t);this.id=e.id||"";this.keyAgreement=getStringOption(e.keyAgreement,t);this.keyCertSign=getStringOption(e.keyCertSign,t);this.keyEncipherment=getStringOption(e.keyEncipherment,t);this.nonRepudiation=getStringOption(e.nonRepudiation,t);this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Line extends XFAObject{constructor(e){super(zo,"line",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.slope=getStringOption(e.slope,["\\","/"]);this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null}[so](){const e=this[ks]()[ks](),t=this.edge||new Edge({}),a=t[co](),r=Object.create(null),i="visible"===t.presence?t.thickness:0;r.strokeWidth=measureToString(i);r.stroke=a.color;let n,s,o,c,l="100%",h="100%";if(e.w<=i){[n,s,o,c]=["50%",0,"50%","100%"];l=r.strokeWidth}else if(e.h<=i){[n,s,o,c]=[0,"50%","100%","50%"];h=r.strokeWidth}else"\\"===this.slope?[n,s,o,c]=[0,0,"100%","100%"]:[n,s,o,c]=[0,"100%","100%",0];const u={name:"svg",children:[{name:"line",attributes:{xmlns:$o,x1:n,y1:s,x2:o,y2:c,style:r}}],attributes:{xmlns:$o,width:l,height:h,style:{overflow:"visible"}}};if(hasMargin(e))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[u]});u.attributes.style.position="absolute";return HTMLResult.success(u)}}class Linear extends XFAObject{constructor(e){super(zo,"linear",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toRight","toBottom","toLeft","toTop"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](e){e=e?e[co]():"#FFFFFF";return`linear-gradient(${this.type.replace(/([RBLT])/," $1").toLowerCase()}, ${e}, ${this.color?this.color[co]():"#000000"})`}}class LockDocument extends ContentObject{constructor(e){super(zo,"lockDocument");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}[cs](){this[is]=getStringOption(this[is],["auto","0","1"])}}class Manifest extends XFAObject{constructor(e){super(zo,"manifest",!0);this.action=getStringOption(e.action,["include","all","exclude"]);this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.ref=new XFAObjectArray}}class Margin extends XFAObject{constructor(e){super(zo,"margin",!0);this.bottomInset=getMeasurement(e.bottomInset,"0");this.id=e.id||"";this.leftInset=getMeasurement(e.leftInset,"0");this.rightInset=getMeasurement(e.rightInset,"0");this.topInset=getMeasurement(e.topInset,"0");this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[co](){return{margin:measureToString(this.topInset)+" "+measureToString(this.rightInset)+" "+measureToString(this.bottomInset)+" "+measureToString(this.leftInset)}}}class Mdp extends XFAObject{constructor(e){super(zo,"mdp");this.id=e.id||"";this.permissions=getInteger({data:e.permissions,defaultValue:2,validate:e=>1===e||3===e});this.signatureType=getStringOption(e.signatureType,["filler","author"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Medium extends XFAObject{constructor(e){super(zo,"medium");this.id=e.id||"";this.imagingBBox=function getBBox(e){const t=-1;if(!e)return{x:t,y:t,width:t,height:t};const a=e.split(",",4).map((e=>getMeasurement(e.trim(),"-1")));if(a.length<4||a[2]<0||a[3]<0)return{x:t,y:t,width:t,height:t};const[r,i,n,s]=a;return{x:r,y:i,width:n,height:s}}(e.imagingBBox);this.long=getMeasurement(e.long);this.orientation=getStringOption(e.orientation,["portrait","landscape"]);this.short=getMeasurement(e.short);this.stock=e.stock||"";this.trayIn=getStringOption(e.trayIn,["auto","delegate","pageFront"]);this.trayOut=getStringOption(e.trayOut,["auto","delegate"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Message extends XFAObject{constructor(e){super(zo,"message",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.text=new XFAObjectArray}}class NumericEdit extends XFAObject{constructor(e){super(zo,"numericEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"border","font","margin"),a=this[ks]()[ks](),r={name:"input",attributes:{type:"text",fieldId:a[lo],dataId:a[ns]?.[lo]||a[lo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){r.attributes["aria-required"]=!0;r.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[r]})}}class Occur extends XFAObject{constructor(e){super(zo,"occur",!0);this.id=e.id||"";this.initial=""!==e.initial?getInteger({data:e.initial,defaultValue:"",validate:e=>!0}):"";this.max=""!==e.max?getInteger({data:e.max,defaultValue:1,validate:e=>!0}):"";this.min=""!==e.min?getInteger({data:e.min,defaultValue:1,validate:e=>!0}):"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Qn](){const e=this[ks](),t=this.min;""===this.min&&(this.min=e instanceof PageArea||e instanceof PageSet?0:1);""===this.max&&(this.max=""===t?e instanceof PageArea||e instanceof PageSet?-1:1:this.min);-1!==this.max&&this.max!0});this.name=e.name||"";this.numbered=getInteger({data:e.numbered,defaultValue:1,validate:e=>!0});this.oddOrEven=getStringOption(e.oddOrEven,["any","even","odd"]);this.pagePosition=getStringOption(e.pagePosition,["any","first","last","only","rest"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.desc=null;this.extras=null;this.medium=null;this.occur=null;this.area=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.draw=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray}[_s](){if(!this[os]){this[os]={numberOfUse:0};return!0}return!this.occur||-1===this.occur.max||this[os].numberOfUsee.oddOrEven===t&&e.pagePosition===a));if(r)return r;r=this.pageArea.children.find((e=>"any"===e.oddOrEven&&e.pagePosition===a));if(r)return r;r=this.pageArea.children.find((e=>"any"===e.oddOrEven&&"any"===e.pagePosition));return r||this.pageArea.children[0]}}class Para extends XFAObject{constructor(e){super(zo,"para",!0);this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.lineHeight=e.lineHeight?getMeasurement(e.lineHeight,"0pt"):"";this.marginLeft=e.marginLeft?getMeasurement(e.marginLeft,"0pt"):"";this.marginRight=e.marginRight?getMeasurement(e.marginRight,"0pt"):"";this.orphans=getInteger({data:e.orphans,defaultValue:0,validate:e=>e>=0});this.preserve=e.preserve||"";this.radixOffset=e.radixOffset?getMeasurement(e.radixOffset,"0pt"):"";this.spaceAbove=e.spaceAbove?getMeasurement(e.spaceAbove,"0pt"):"";this.spaceBelow=e.spaceBelow?getMeasurement(e.spaceBelow,"0pt"):"";this.tabDefault=e.tabDefault?getMeasurement(this.tabDefault):"";this.tabStops=(e.tabStops||"").trim().split(/\s+/).map(((e,t)=>t%2==1?getMeasurement(e):e));this.textIndent=e.textIndent?getMeasurement(e.textIndent,"0pt"):"";this.use=e.use||"";this.usehref=e.usehref||"";this.vAlign=getStringOption(e.vAlign,["top","bottom","middle"]);this.widows=getInteger({data:e.widows,defaultValue:0,validate:e=>e>=0});this.hyphenation=null}[co](){const e=toStyle(this,"hAlign");""!==this.marginLeft&&(e.paddingLeft=measureToString(this.marginLeft));""!==this.marginRight&&(e.paddingRight=measureToString(this.marginRight));""!==this.spaceAbove&&(e.paddingTop=measureToString(this.spaceAbove));""!==this.spaceBelow&&(e.paddingBottom=measureToString(this.spaceBelow));if(""!==this.textIndent){e.textIndent=measureToString(this.textIndent);fixTextIndent(e)}this.lineHeight>0&&(e.lineHeight=measureToString(this.lineHeight));""!==this.tabDefault&&(e.tabSize=measureToString(this.tabDefault));this.tabStops.length;this.hyphenatation&&Object.assign(e,this.hyphenatation[co]());return e}}class PasswordEdit extends XFAObject{constructor(e){super(zo,"passwordEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.passwordChar=e.passwordChar||"*";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}}class template_Pattern extends XFAObject{constructor(e){super(zo,"pattern",!0);this.id=e.id||"";this.type=getStringOption(e.type,["crossHatch","crossDiagonal","diagonalLeft","diagonalRight","horizontal","vertical"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](e){e=e?e[co]():"#FFFFFF";const t=this.color?this.color[co]():"#000000",a="repeating-linear-gradient",r=`${e},${e} 5px,${t} 5px,${t} 10px`;switch(this.type){case"crossHatch":return`${a}(to top,${r}) ${a}(to right,${r})`;case"crossDiagonal":return`${a}(45deg,${r}) ${a}(-45deg,${r})`;case"diagonalLeft":return`${a}(45deg,${r})`;case"diagonalRight":return`${a}(-45deg,${r})`;case"horizontal":return`${a}(to top,${r})`;case"vertical":return`${a}(to right,${r})`}return""}}class Picture extends StringObject{constructor(e){super(zo,"picture");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Proto extends XFAObject{constructor(e){super(zo,"proto",!0);this.appearanceFilter=new XFAObjectArray;this.arc=new XFAObjectArray;this.area=new XFAObjectArray;this.assist=new XFAObjectArray;this.barcode=new XFAObjectArray;this.bindItems=new XFAObjectArray;this.bookend=new XFAObjectArray;this.boolean=new XFAObjectArray;this.border=new XFAObjectArray;this.break=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.button=new XFAObjectArray;this.calculate=new XFAObjectArray;this.caption=new XFAObjectArray;this.certificate=new XFAObjectArray;this.certificates=new XFAObjectArray;this.checkButton=new XFAObjectArray;this.choiceList=new XFAObjectArray;this.color=new XFAObjectArray;this.comb=new XFAObjectArray;this.connect=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.corner=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.dateTimeEdit=new XFAObjectArray;this.decimal=new XFAObjectArray;this.defaultUi=new XFAObjectArray;this.desc=new XFAObjectArray;this.digestMethod=new XFAObjectArray;this.digestMethods=new XFAObjectArray;this.draw=new XFAObjectArray;this.edge=new XFAObjectArray;this.encoding=new XFAObjectArray;this.encodings=new XFAObjectArray;this.encrypt=new XFAObjectArray;this.encryptData=new XFAObjectArray;this.encryption=new XFAObjectArray;this.encryptionMethod=new XFAObjectArray;this.encryptionMethods=new XFAObjectArray;this.event=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.execute=new XFAObjectArray;this.extras=new XFAObjectArray;this.field=new XFAObjectArray;this.fill=new XFAObjectArray;this.filter=new XFAObjectArray;this.float=new XFAObjectArray;this.font=new XFAObjectArray;this.format=new XFAObjectArray;this.handler=new XFAObjectArray;this.hyphenation=new XFAObjectArray;this.image=new XFAObjectArray;this.imageEdit=new XFAObjectArray;this.integer=new XFAObjectArray;this.issuers=new XFAObjectArray;this.items=new XFAObjectArray;this.keep=new XFAObjectArray;this.keyUsage=new XFAObjectArray;this.line=new XFAObjectArray;this.linear=new XFAObjectArray;this.lockDocument=new XFAObjectArray;this.manifest=new XFAObjectArray;this.margin=new XFAObjectArray;this.mdp=new XFAObjectArray;this.medium=new XFAObjectArray;this.message=new XFAObjectArray;this.numericEdit=new XFAObjectArray;this.occur=new XFAObjectArray;this.oid=new XFAObjectArray;this.oids=new XFAObjectArray;this.overflow=new XFAObjectArray;this.pageArea=new XFAObjectArray;this.pageSet=new XFAObjectArray;this.para=new XFAObjectArray;this.passwordEdit=new XFAObjectArray;this.pattern=new XFAObjectArray;this.picture=new XFAObjectArray;this.radial=new XFAObjectArray;this.reason=new XFAObjectArray;this.reasons=new XFAObjectArray;this.rectangle=new XFAObjectArray;this.ref=new XFAObjectArray;this.script=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.signData=new XFAObjectArray;this.signature=new XFAObjectArray;this.signing=new XFAObjectArray;this.solid=new XFAObjectArray;this.speak=new XFAObjectArray;this.stipple=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray;this.subjectDN=new XFAObjectArray;this.subjectDNs=new XFAObjectArray;this.submit=new XFAObjectArray;this.text=new XFAObjectArray;this.textEdit=new XFAObjectArray;this.time=new XFAObjectArray;this.timeStamp=new XFAObjectArray;this.toolTip=new XFAObjectArray;this.traversal=new XFAObjectArray;this.traverse=new XFAObjectArray;this.ui=new XFAObjectArray;this.validate=new XFAObjectArray;this.value=new XFAObjectArray;this.variables=new XFAObjectArray}}class Radial extends XFAObject{constructor(e){super(zo,"radial",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toEdge","toCenter"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](e){e=e?e[co]():"#FFFFFF";const t=this.color?this.color[co]():"#000000";return`radial-gradient(circle at center, ${"toEdge"===this.type?`${e},${t}`:`${t},${e}`})`}}class Reason extends StringObject{constructor(e){super(zo,"reason");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Reasons extends XFAObject{constructor(e){super(zo,"reasons",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.reason=new XFAObjectArray}}class Rectangle extends XFAObject{constructor(e){super(zo,"rectangle",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.fill=null}[so](){const e=this.edge.children.length?this.edge.children[0]:new Edge({}),t=e[co](),a=Object.create(null);"visible"===this.fill?.presence?Object.assign(a,this.fill[co]()):a.fill="transparent";a.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);a.stroke=t.color;const r=(this.corner.children.length?this.corner.children[0]:new Corner({}))[co](),i={name:"svg",children:[{name:"rect",attributes:{xmlns:$o,width:"100%",height:"100%",x:0,y:0,rx:r.radius,ry:r.radius,style:a}}],attributes:{xmlns:$o,style:{overflow:"visible"},width:"100%",height:"100%"}};if(hasMargin(this[ks]()[ks]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[i]});i.attributes.style.position="absolute";return HTMLResult.success(i)}}class RefElement extends StringObject{constructor(e){super(zo,"ref");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Script extends StringObject{constructor(e){super(zo,"script");this.binding=e.binding||"";this.contentType=e.contentType||"";this.id=e.id||"";this.name=e.name||"";this.runAt=getStringOption(e.runAt,["client","both","server"]);this.use=e.use||"";this.usehref=e.usehref||""}}class SetProperty extends XFAObject{constructor(e){super(zo,"setProperty");this.connection=e.connection||"";this.ref=e.ref||"";this.target=e.target||""}}class SignData extends XFAObject{constructor(e){super(zo,"signData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["sign","clear","verify"]);this.ref=e.ref||"";this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Signature extends XFAObject{constructor(e){super(zo,"signature",!0);this.id=e.id||"";this.type=getStringOption(e.type,["PDF1.3","PDF1.6"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.filter=null;this.manifest=null;this.margin=null}}class Signing extends XFAObject{constructor(e){super(zo,"signing",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Solid extends XFAObject{constructor(e){super(zo,"solid",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[co](e){return e?e[co]():"#FFFFFF"}}class Speak extends StringObject{constructor(e){super(zo,"speak");this.disable=getInteger({data:e.disable,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.priority=getStringOption(e.priority,["custom","caption","name","toolTip"]);this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Stipple extends XFAObject{constructor(e){super(zo,"stipple",!0);this.id=e.id||"";this.rate=getInteger({data:e.rate,defaultValue:50,validate:e=>e>=0&&e<=100});this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](e){const t=this.rate/100;return Util.makeHexColor(Math.round(e.value.r*(1-t)+this.value.r*t),Math.round(e.value.g*(1-t)+this.value.g*t),Math.round(e.value.b*(1-t)+this.value.b*t))}}class Subform extends XFAObject{constructor(e){super(zo,"subform",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.allowMacro=getInteger({data:e.allowMacro,defaultValue:0,validate:e=>1===e});this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.columnWidths=(e.columnWidths||"").trim().split(/\s+/).map((e=>"-1"===e?-1:getMeasurement(e)));this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.mergeMode=getStringOption(e.mergeMode,["consumeData","matchTemplate"]);this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.restoreState=getStringOption(e.restoreState,["manual","auto"]);this.scope=getStringOption(e.scope,["name","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.bookend=null;this.border=null;this.break=null;this.calculate=null;this.desc=null;this.extras=null;this.keep=null;this.margin=null;this.occur=null;this.overflow=null;this.pageSet=null;this.para=null;this.traversal=null;this.validate=null;this.variables=null;this.area=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.connect=new XFAObjectArray;this.draw=new XFAObjectArray;this.event=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.proto=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}[As](){const e=this[ks]();return e instanceof SubformSet?e[As]():e}[Ds](){return!0}[Ls](){return this.layout.endsWith("-tb")&&0===this[os].attempt&&this[os].numberInLine>0||this[ks]()[Ls]()}*[Ss](){yield*getContainedChildren(this)}[ls](){return flushHTML(this)}[Jn](e,t){addHTML(this,e,t)}[ds](){return getAvailableSpace(this)}[Es](){const e=this[As]();if(!e[Es]())return!1;if(void 0!==this[os]._isSplittable)return this[os]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[os]._isSplittable=!1;return!1}if(this.keep&&"none"!==this.keep.intact){this[os]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[os].numberInLine)return!1;this[os]._isSplittable=!0;return!0}[so](e){setTabIndex(this);if(this.break){if("auto"!==this.break.after||""!==this.break.afterTarget){const e=new BreakAfter({targetType:this.break.after,target:this.break.afterTarget,startNew:this.break.startNew.toString()});e[vs]=this[vs];this[Yn](e);this.breakAfter.push(e)}if("auto"!==this.break.before||""!==this.break.beforeTarget){const e=new BreakBefore({targetType:this.break.before,target:this.break.beforeTarget,startNew:this.break.startNew.toString()});e[vs]=this[vs];this[Yn](e);this.breakBefore.push(e)}if(""!==this.break.overflowTarget){const e=new Overflow({target:this.break.overflowTarget,leader:this.break.overflowLeader,trailer:this.break.overflowTrailer});e[vs]=this[vs];this[Yn](e);this.overflow.push(e)}this[Js](this.break);this.break=null}if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;(this.breakBefore.children.length>1||this.breakAfter.children.length>1)&&warn("XFA - Several breakBefore or breakAfter in subforms: please file a bug.");if(this.breakBefore.children.length>=1){const e=this.breakBefore.children[0];if(handleBreak(e))return HTMLResult.breakNode(e)}if(this[os]?.afterBreakAfter)return HTMLResult.EMPTY;fixDimensions(this);const t=[],a={id:this[lo],class:[]};setAccess(this,a.class);this[os]||=Object.create(null);Object.assign(this[os],{children:t,line:null,attributes:a,attempt:0,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const r=this[Cs](),i=r[os].noLayoutFailure,n=this[Es]();n||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const s=new Set(["area","draw","exclGroup","field","subform","subformSet"]);if(this.layout.includes("row")){const e=this[As]().columnWidths;if(Array.isArray(e)&&e.length>0){this[os].columnWidths=e;this[os].currentColumn=0}}const o=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),c=["xfaSubform"],l=layoutClass(this);l&&c.push(l);a.style=o;a.class=c;this.name&&(a.xfaName=this.name);if(this.overflow){const t=this.overflow[bs]();if(t.addLeader){t.addLeader=!1;handleOverflow(this,t.leader,e)}}this[Ks]();const h="lr-tb"===this.layout||"rl-tb"===this.layout,u=h?2:1;for(;this[os].attempt=1){const e=this.breakAfter.children[0];if(handleBreak(e)){this[os].afterBreakAfter=y;return HTMLResult.breakNode(e)}}delete this[os];return y}}class SubformSet extends XFAObject{constructor(e){super(zo,"subformSet",!0);this.id=e.id||"";this.name=e.name||"";this.relation=getStringOption(e.relation,["ordered","choice","unordered"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.bookend=null;this.break=null;this.desc=null;this.extras=null;this.occur=null;this.overflow=null;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[Ss](){yield*getContainedChildren(this)}[As](){let e=this[ks]();for(;!(e instanceof Subform);)e=e[ks]();return e}[Ds](){return!0}}class SubjectDN extends ContentObject{constructor(e){super(zo,"subjectDN");this.delimiter=e.delimiter||",";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){this[is]=new Map(this[is].split(this.delimiter).map((e=>{(e=e.split("=",2))[0]=e[0].trim();return e})))}}class SubjectDNs extends XFAObject{constructor(e){super(zo,"subjectDNs",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.subjectDN=new XFAObjectArray}}class Submit extends XFAObject{constructor(e){super(zo,"submit",!0);this.embedPDF=getInteger({data:e.embedPDF,defaultValue:0,validate:e=>1===e});this.format=getStringOption(e.format,["xdp","formdata","pdf","urlencoded","xfd","xml"]);this.id=e.id||"";this.target=e.target||"";this.textEncoding=getKeyword({data:e.textEncoding?e.textEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.use=e.use||"";this.usehref=e.usehref||"";this.xdpContent=e.xdpContent||"";this.encrypt=null;this.encryptData=new XFAObjectArray;this.signData=new XFAObjectArray}}class Template extends XFAObject{constructor(e){super(zo,"template",!0);this.baseProfile=getStringOption(e.baseProfile,["full","interactiveForms"]);this.extras=null;this.subform=new XFAObjectArray}[cs](){0===this.subform.children.length&&warn("XFA - No subforms in template node.");this.subform.children.length>=2&&warn("XFA - Several subforms in template node: please file a bug.");this[ro]=5e3}[Es](){return!0}[Qs](e,t){return e.startsWith("#")?[this[Is].get(e.slice(1))]:searchNode(this,t,e,!0,!0)}*[no](){if(!this.subform.children.length)return HTMLResult.success({name:"div",children:[]});this[os]={overflowNode:null,firstUnsplittable:null,currentContentArea:null,currentPageArea:null,noLayoutFailure:!1,pageNumber:1,pagePosition:"first",oddOrEven:"odd",blankOrNotBlank:"nonBlank",paraStack:[]};const e=this.subform.children[0];e.pageSet[es]();const t=e.pageSet.pageArea.children,a={name:"div",children:[]};let r=null,i=null,n=null;if(e.breakBefore.children.length>=1){i=e.breakBefore.children[0];n=i.target}else if(e.subform.children.length>=1&&e.subform.children[0].breakBefore.children.length>=1){i=e.subform.children[0].breakBefore.children[0];n=i.target}else if(e.break?.beforeTarget){i=e.break;n=i.beforeTarget}else if(e.subform.children.length>=1&&e.subform.children[0].break?.beforeTarget){i=e.subform.children[0].break;n=i.beforeTarget}if(i){const e=this[Qs](n,i[ks]());if(e instanceof PageArea){r=e;i[os]={}}}r||=t[0];r[os]={numberOfUse:1};const s=r[ks]();s[os]={numberOfUse:1,pageIndex:s.pageArea.children.indexOf(r),pageSetIndex:0};let o,c=null,l=null,h=!0,u=0,d=0;for(;;){if(h)u=0;else{a.children.pop();if(3==++u){warn("XFA - Something goes wrong: please file a bug.");return a}}o=null;this[os].currentPageArea=r;const t=r[so]().html;a.children.push(t);if(c){this[os].noLayoutFailure=!0;t.children.push(c[so](r[os].space).html);c=null}if(l){this[os].noLayoutFailure=!0;t.children.push(l[so](r[os].space).html);l=null}const i=r.contentArea.children,n=t.children.filter((e=>e.attributes.class.includes("xfaContentarea")));h=!1;this[os].firstUnsplittable=null;this[os].noLayoutFailure=!1;const flush=t=>{const a=e[ls]();if(a){h||=a.children?.length>0;n[t].children.push(a)}};for(let t=d,r=i.length;t0;n[t].children.push(u.html)}else!h&&a.children.length>1&&a.children.pop();return a}if(u.isBreak()){const e=u.breakNode;flush(t);if("auto"===e.targetType)continue;if(e.leader){c=this[Qs](e.leader,e[ks]());c=c?c[0]:null}if(e.trailer){l=this[Qs](e.trailer,e[ks]());l=l?l[0]:null}if("pageArea"===e.targetType){o=e[os].target;t=1/0}else if(e[os].target){o=e[os].target;d=e[os].index+1;t=1/0}else t=e[os].index}else if(this[os].overflowNode){const e=this[os].overflowNode;this[os].overflowNode=null;const a=e[bs](),r=a.target;a.addLeader=null!==a.leader;a.addTrailer=null!==a.trailer;flush(t);const n=t;t=1/0;if(r instanceof PageArea)o=r;else if(r instanceof ContentArea){const e=i.indexOf(r);if(-1!==e)e>n?t=e-1:d=e;else{o=r[ks]();d=o.contentArea.children.indexOf(r)}}}else flush(t)}this[os].pageNumber+=1;o&&(o[_s]()?o[os].numberOfUse+=1:o=null);r=o||r[xs]();yield null}}}class Text extends ContentObject{constructor(e){super(zo,"text");this.id=e.id||"";this.maxChars=getInteger({data:e.maxChars,defaultValue:0,validate:e=>e>=0});this.name=e.name||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}[Kn](){return!0}[Ws](e){if(e[Xs]===uo.xhtml.id){this[is]=e;return!0}warn(`XFA - Invalid content in Text: ${e[qs]}.`);return!1}[$s](e){this[is]instanceof XFAObject||super[$s](e)}[cs](){"string"==typeof this[is]&&(this[is]=this[is].replaceAll("\r\n","\n"))}[bs](){return"string"==typeof this[is]?this[is].split(/[\u2029\u2028\n]/).filter((e=>!!e)).join("\n"):this[is][io]()}[so](e){if("string"==typeof this[is]){const e=valueToHtml(this[is]).html;if(this[is].includes("\u2029")){e.name="div";e.children=[];this[is].split("\u2029").map((e=>e.split(/[\u2028\n]/).flatMap((e=>[{name:"span",value:e},{name:"br"}])))).forEach((t=>{e.children.push({name:"p",children:t})}))}else if(/[\u2028\n]/.test(this[is])){e.name="div";e.children=[];this[is].split(/[\u2028\n]/).forEach((t=>{e.children.push({name:"span",value:t},{name:"br"})}))}return HTMLResult.success(e)}return this[is][so](e)}}class TextEdit extends XFAObject{constructor(e){super(zo,"textEdit",!0);this.allowRichText=getInteger({data:e.allowRichText,defaultValue:0,validate:e=>1===e});this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.multiLine=getInteger({data:e.multiLine,defaultValue:"",validate:e=>0===e||1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.vScrollPolicy=getStringOption(e.vScrollPolicy,["auto","off","on"]);this.border=null;this.comb=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"border","font","margin");let a;const r=this[ks]()[ks]();""===this.multiLine&&(this.multiLine=r instanceof Draw?1:0);a=1===this.multiLine?{name:"textarea",attributes:{dataId:r[ns]?.[lo]||r[lo],fieldId:r[lo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(r),"aria-required":!1}}:{name:"input",attributes:{type:"text",dataId:r[ns]?.[lo]||r[lo],fieldId:r[lo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(r),"aria-required":!1}};if(isRequired(r)){a.attributes["aria-required"]=!0;a.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[a]})}}class Time extends StringObject{constructor(e){super(zo,"time");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=this[is].trim();this[is]=e?new Date(e):null}[so](e){return valueToHtml(this[is]?this[is].toString():"")}}class TimeStamp extends XFAObject{constructor(e){super(zo,"timeStamp");this.id=e.id||"";this.server=e.server||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class ToolTip extends StringObject{constructor(e){super(zo,"toolTip");this.id=e.id||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Traversal extends XFAObject{constructor(e){super(zo,"traversal",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.traverse=new XFAObjectArray}}class Traverse extends XFAObject{constructor(e){super(zo,"traverse",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["next","back","down","first","left","right","up"]);this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.script=null}get name(){return this.operation}[js](){return!1}}class Ui extends XFAObject{constructor(e){super(zo,"ui",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null;this.barcode=null;this.button=null;this.checkButton=null;this.choiceList=null;this.dateTimeEdit=null;this.defaultUi=null;this.imageEdit=null;this.numericEdit=null;this.passwordEdit=null;this.signature=null;this.textEdit=null}[bs](){if(void 0===this[os]){for(const e of Object.getOwnPropertyNames(this)){if("extras"===e||"picture"===e)continue;const t=this[e];if(t instanceof XFAObject){this[os]=t;return t}}this[os]=null}return this[os]}[so](e){const t=this[bs]();return t?t[so](e):HTMLResult.EMPTY}}class Validate extends XFAObject{constructor(e){super(zo,"validate",!0);this.formatTest=getStringOption(e.formatTest,["warning","disabled","error"]);this.id=e.id||"";this.nullTest=getStringOption(e.nullTest,["disabled","error","warning"]);this.scriptTest=getStringOption(e.scriptTest,["error","disabled","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.picture=null;this.script=null}}class Value extends XFAObject{constructor(e){super(zo,"value",!0);this.id=e.id||"";this.override=getInteger({data:e.override,defaultValue:0,validate:e=>1===e});this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.arc=null;this.boolean=null;this.date=null;this.dateTime=null;this.decimal=null;this.exData=null;this.float=null;this.image=null;this.integer=null;this.line=null;this.rectangle=null;this.text=null;this.time=null}[ao](e){const t=this[ks]();if(t instanceof Field&&t.ui?.imageEdit){if(!this.image){this.image=new Image({});this[Yn](this.image)}this.image[is]=e[is];return}const a=e[qs];if(null===this[a]){for(const e of Object.getOwnPropertyNames(this)){const t=this[e];if(t instanceof XFAObject){this[e]=null;this[Js](t)}}this[e[qs]]=e;this[Yn](e)}else this[a][is]=e[is]}[io](){if(this.exData)return"string"==typeof this.exData[is]?this.exData[is].trim():this.exData[is][io]().trim();for(const e of Object.getOwnPropertyNames(this)){if("image"===e)continue;const t=this[e];if(t instanceof XFAObject)return(t[is]||"").toString().trim()}return null}[so](e){for(const t of Object.getOwnPropertyNames(this)){const a=this[t];if(a instanceof XFAObject)return a[so](e)}return HTMLResult.EMPTY}}class Variables extends XFAObject{constructor(e){super(zo,"variables",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.manifest=new XFAObjectArray;this.script=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[js](){return!0}}class TemplateNamespace{static[ho](e,t){if(TemplateNamespace.hasOwnProperty(e)){const a=TemplateNamespace[e](t);a[to](t);return a}}static appearanceFilter(e){return new AppearanceFilter(e)}static arc(e){return new Arc(e)}static area(e){return new Area(e)}static assist(e){return new Assist(e)}static barcode(e){return new Barcode(e)}static bind(e){return new Bind(e)}static bindItems(e){return new BindItems(e)}static bookend(e){return new Bookend(e)}static boolean(e){return new BooleanElement(e)}static border(e){return new Border(e)}static break(e){return new Break(e)}static breakAfter(e){return new BreakAfter(e)}static breakBefore(e){return new BreakBefore(e)}static button(e){return new Button(e)}static calculate(e){return new Calculate(e)}static caption(e){return new Caption(e)}static certificate(e){return new Certificate(e)}static certificates(e){return new Certificates(e)}static checkButton(e){return new CheckButton(e)}static choiceList(e){return new ChoiceList(e)}static color(e){return new Color(e)}static comb(e){return new Comb(e)}static connect(e){return new Connect(e)}static contentArea(e){return new ContentArea(e)}static corner(e){return new Corner(e)}static date(e){return new DateElement(e)}static dateTime(e){return new DateTime(e)}static dateTimeEdit(e){return new DateTimeEdit(e)}static decimal(e){return new Decimal(e)}static defaultUi(e){return new DefaultUi(e)}static desc(e){return new Desc(e)}static digestMethod(e){return new DigestMethod(e)}static digestMethods(e){return new DigestMethods(e)}static draw(e){return new Draw(e)}static edge(e){return new Edge(e)}static encoding(e){return new Encoding(e)}static encodings(e){return new Encodings(e)}static encrypt(e){return new Encrypt(e)}static encryptData(e){return new EncryptData(e)}static encryption(e){return new Encryption(e)}static encryptionMethod(e){return new EncryptionMethod(e)}static encryptionMethods(e){return new EncryptionMethods(e)}static event(e){return new Event(e)}static exData(e){return new ExData(e)}static exObject(e){return new ExObject(e)}static exclGroup(e){return new ExclGroup(e)}static execute(e){return new Execute(e)}static extras(e){return new Extras(e)}static field(e){return new Field(e)}static fill(e){return new Fill(e)}static filter(e){return new Filter(e)}static float(e){return new Float(e)}static font(e){return new template_Font(e)}static format(e){return new Format(e)}static handler(e){return new Handler(e)}static hyphenation(e){return new Hyphenation(e)}static image(e){return new Image(e)}static imageEdit(e){return new ImageEdit(e)}static integer(e){return new Integer(e)}static issuers(e){return new Issuers(e)}static items(e){return new Items(e)}static keep(e){return new Keep(e)}static keyUsage(e){return new KeyUsage(e)}static line(e){return new Line(e)}static linear(e){return new Linear(e)}static lockDocument(e){return new LockDocument(e)}static manifest(e){return new Manifest(e)}static margin(e){return new Margin(e)}static mdp(e){return new Mdp(e)}static medium(e){return new Medium(e)}static message(e){return new Message(e)}static numericEdit(e){return new NumericEdit(e)}static occur(e){return new Occur(e)}static oid(e){return new Oid(e)}static oids(e){return new Oids(e)}static overflow(e){return new Overflow(e)}static pageArea(e){return new PageArea(e)}static pageSet(e){return new PageSet(e)}static para(e){return new Para(e)}static passwordEdit(e){return new PasswordEdit(e)}static pattern(e){return new template_Pattern(e)}static picture(e){return new Picture(e)}static proto(e){return new Proto(e)}static radial(e){return new Radial(e)}static reason(e){return new Reason(e)}static reasons(e){return new Reasons(e)}static rectangle(e){return new Rectangle(e)}static ref(e){return new RefElement(e)}static script(e){return new Script(e)}static setProperty(e){return new SetProperty(e)}static signData(e){return new SignData(e)}static signature(e){return new Signature(e)}static signing(e){return new Signing(e)}static solid(e){return new Solid(e)}static speak(e){return new Speak(e)}static stipple(e){return new Stipple(e)}static subform(e){return new Subform(e)}static subformSet(e){return new SubformSet(e)}static subjectDN(e){return new SubjectDN(e)}static subjectDNs(e){return new SubjectDNs(e)}static submit(e){return new Submit(e)}static template(e){return new Template(e)}static text(e){return new Text(e)}static textEdit(e){return new TextEdit(e)}static time(e){return new Time(e)}static timeStamp(e){return new TimeStamp(e)}static toolTip(e){return new ToolTip(e)}static traversal(e){return new Traversal(e)}static traverse(e){return new Traverse(e)}static ui(e){return new Ui(e)}static validate(e){return new Validate(e)}static value(e){return new Value(e)}static variables(e){return new Variables(e)}}const Jo=uo.datasets.id;function createText(e){const t=new Text({});t[is]=e;return t}class Binder{constructor(e){this.root=e;this.datasets=e.datasets;this.data=e.datasets?.data||new XmlObject(uo.datasets.id,"data");this.emptyMerge=0===this.data[ws]().length;this.root.form=this.form=e.template[as]()}_isConsumeData(){return!this.emptyMerge&&this._mergeMode}_isMatchTemplate(){return!this._isConsumeData()}bind(){this._bindElement(this.form,this.data);return this.form}getData(){return this.data}_bindValue(e,t,a){e[ns]=t;if(e[Fs]())if(t[Bs]()){const a=t[ms]();e[ao](createText(a))}else if(e instanceof Field&&"multiSelect"===e.ui?.choiceList?.open){const a=t[ws]().map((e=>e[is].trim())).join("\n");e[ao](createText(a))}else this._isConsumeData()&&warn("XFA - Nodes haven't the same type.");else!t[Bs]()||this._isMatchTemplate()?this._bindElement(e,t):warn("XFA - Nodes haven't the same type.")}_findDataByNameToConsume(e,t,a,r){if(!e)return null;let i,n;for(let r=0;r<3;r++){i=a[ys](e,!1,!0);for(;;){n=i.next().value;if(!n)break;if(t===n[Bs]())return n}if(a[Xs]===uo.datasets.id&&"data"===a[qs])break;a=a[ks]()}if(!r)return null;i=this.data[ys](e,!0,!1);n=i.next().value;if(n)return n;i=this.data[hs](e,!0);n=i.next().value;return n?.[Bs]()?n:null}_setProperties(e,t){if(e.hasOwnProperty("setProperty"))for(const{ref:a,target:r,connection:i}of e.setProperty.children){if(i)continue;if(!a)continue;const n=searchNode(this.root,t,a,!1,!1);if(!n){warn(`XFA - Invalid reference: ${a}.`);continue}const[s]=n;if(!s[Rs](this.data)){warn("XFA - Invalid node: must be a data node.");continue}const o=searchNode(this.root,e,r,!1,!1);if(!o){warn(`XFA - Invalid target: ${r}.`);continue}const[c]=o;if(!c[Rs](e)){warn("XFA - Invalid target: must be a property or subproperty.");continue}const l=c[ks]();if(c instanceof SetProperty||l instanceof SetProperty){warn("XFA - Invalid target: cannot be a setProperty or one of its properties.");continue}if(c instanceof BindItems||l instanceof BindItems){warn("XFA - Invalid target: cannot be a bindItems or one of its properties.");continue}const h=s[io](),u=c[qs];if(c instanceof XFAAttribute){const e=Object.create(null);e[u]=h;const t=Reflect.construct(Object.getPrototypeOf(l).constructor,[e]);l[u]=t[u]}else if(c.hasOwnProperty(is)){c[ns]=s;c[is]=h;c[cs]()}else warn("XFA - Invalid node to use in setProperty")}}_bindItems(e,t){if(!e.hasOwnProperty("items")||!e.hasOwnProperty("bindItems")||e.bindItems.isEmpty())return;for(const t of e.items.children)e[Js](t);e.items.clear();const a=new Items({}),r=new Items({});e[Yn](a);e.items.push(a);e[Yn](r);e.items.push(r);for(const{ref:i,labelRef:n,valueRef:s,connection:o}of e.bindItems.children){if(o)continue;if(!i)continue;const e=searchNode(this.root,t,i,!1,!1);if(e)for(const t of e){if(!t[Rs](this.datasets)){warn(`XFA - Invalid ref (${i}): must be a datasets child.`);continue}const e=searchNode(this.root,t,n,!0,!1);if(!e){warn(`XFA - Invalid label: ${n}.`);continue}const[o]=e;if(!o[Rs](this.datasets)){warn("XFA - Invalid label: must be a datasets child.");continue}const c=searchNode(this.root,t,s,!0,!1);if(!c){warn(`XFA - Invalid value: ${s}.`);continue}const[l]=c;if(!l[Rs](this.datasets)){warn("XFA - Invalid value: must be a datasets child.");continue}const h=createText(o[io]()),u=createText(l[io]());a[Yn](h);a.text.push(h);r[Yn](u);r.text.push(u)}else warn(`XFA - Invalid reference: ${i}.`)}}_bindOccurrences(e,t,a){let r;if(t.length>1){r=e[as]();r[Js](r.occur);r.occur=null}this._bindValue(e,t[0],a);this._setProperties(e,t[0]);this._bindItems(e,t[0]);if(1===t.length)return;const i=e[ks](),n=e[qs],s=i[Os](e);for(let e=1,o=t.length;et.name===e.name)).length:a[r].children.length;const n=a[Os](e)+1,s=t.initial-i;if(s){const t=e[as]();t[Js](t.occur);t.occur=null;a[r].push(t);a[Ts](n,t);for(let e=1;e0)this._bindOccurrences(r,[e[0]],null);else if(this.emptyMerge){const e=t[Xs]===Jo?-1:t[Xs],a=r[ns]=new XmlObject(e,r.name||"root");t[Yn](a);this._bindElement(r,a)}continue}if(!r[Ds]())continue;let e=!1,i=null,n=null,s=null;if(r.bind){switch(r.bind.match){case"none":this._setAndBind(r,t);continue;case"global":e=!0;break;case"dataRef":if(!r.bind.ref){warn(`XFA - ref is empty in node ${r[qs]}.`);this._setAndBind(r,t);continue}n=r.bind.ref}r.bind.picture&&(i=r.bind.picture[is])}const[o,c]=this._getOccurInfo(r);if(n){s=searchNode(this.root,t,n,!0,!1);if(null===s){s=createDataNode(this.data,t,n);if(!s)continue;this._isConsumeData()&&(s[rs]=!0);this._setAndBind(r,s);continue}this._isConsumeData()&&(s=s.filter((e=>!e[rs])));s.length>c?s=s.slice(0,c):0===s.length&&(s=null);s&&this._isConsumeData()&&s.forEach((e=>{e[rs]=!0}))}else{if(!r.name){this._setAndBind(r,t);continue}if(this._isConsumeData()){const a=[];for(;a.length0?a:null}else{s=t[ys](r.name,!1,this.emptyMerge).next().value;if(!s){if(0===o){a.push(r);continue}const e=t[Xs]===Jo?-1:t[Xs];s=r[ns]=new XmlObject(e,r.name);this.emptyMerge&&(s[rs]=!0);t[Yn](s);this._setAndBind(r,s);continue}this.emptyMerge&&(s[rs]=!0);s=[s]}}s?this._bindOccurrences(r,s,i):o>0?this._setAndBind(r,t):a.push(r)}a.forEach((e=>e[ks]()[Js](e)))}}class DataHandler{constructor(e,t){this.data=t;this.dataset=e.datasets||null}serialize(e){const t=[[-1,this.data[ws]()]];for(;t.length>0;){const a=t.at(-1),[r,i]=a;if(r+1===i.length){t.pop();continue}const n=i[++a[0]],s=e.get(n[lo]);if(s)n[ao](s);else{const t=n[us]();for(const a of t.values()){const t=e.get(a[lo]);if(t){a[ao](t);break}}}const o=n[ws]();o.length>0&&t.push([-1,o])}const a=[''];if(this.dataset)for(const e of this.dataset[ws]())"data"!==e[qs]&&e[oo](a);this.data[oo](a);a.push("");return a.join("")}}const Yo=uo.config.id;class Acrobat extends XFAObject{constructor(e){super(Yo,"acrobat",!0);this.acrobat7=null;this.autoSave=null;this.common=null;this.validate=null;this.validateApprovalSignatures=null;this.submitUrl=new XFAObjectArray}}class Acrobat7 extends XFAObject{constructor(e){super(Yo,"acrobat7",!0);this.dynamicRender=null}}class ADBE_JSConsole extends OptionObject{constructor(e){super(Yo,"ADBE_JSConsole",["delegate","Enable","Disable"])}}class ADBE_JSDebugger extends OptionObject{constructor(e){super(Yo,"ADBE_JSDebugger",["delegate","Enable","Disable"])}}class AddSilentPrint extends Option01{constructor(e){super(Yo,"addSilentPrint")}}class AddViewerPreferences extends Option01{constructor(e){super(Yo,"addViewerPreferences")}}class AdjustData extends Option10{constructor(e){super(Yo,"adjustData")}}class AdobeExtensionLevel extends IntegerObject{constructor(e){super(Yo,"adobeExtensionLevel",0,(e=>e>=1&&e<=8))}}class Agent extends XFAObject{constructor(e){super(Yo,"agent",!0);this.name=e.name?e.name.trim():"";this.common=new XFAObjectArray}}class AlwaysEmbed extends ContentObject{constructor(e){super(Yo,"alwaysEmbed")}}class Amd extends StringObject{constructor(e){super(Yo,"amd")}}class config_Area extends XFAObject{constructor(e){super(Yo,"area");this.level=getInteger({data:e.level,defaultValue:0,validate:e=>e>=1&&e<=3});this.name=getStringOption(e.name,["","barcode","coreinit","deviceDriver","font","general","layout","merge","script","signature","sourceSet","templateCache"])}}class Attributes extends OptionObject{constructor(e){super(Yo,"attributes",["preserve","delegate","ignore"])}}class AutoSave extends OptionObject{constructor(e){super(Yo,"autoSave",["disabled","enabled"])}}class Base extends StringObject{constructor(e){super(Yo,"base")}}class BatchOutput extends XFAObject{constructor(e){super(Yo,"batchOutput");this.format=getStringOption(e.format,["none","concat","zip","zipCompress"])}}class BehaviorOverride extends ContentObject{constructor(e){super(Yo,"behaviorOverride")}[cs](){this[is]=new Map(this[is].trim().split(/\s+/).filter((e=>e.includes(":"))).map((e=>e.split(":",2))))}}class Cache extends XFAObject{constructor(e){super(Yo,"cache",!0);this.templateCache=null}}class Change extends Option01{constructor(e){super(Yo,"change")}}class Common extends XFAObject{constructor(e){super(Yo,"common",!0);this.data=null;this.locale=null;this.localeSet=null;this.messaging=null;this.suppressBanner=null;this.template=null;this.validationMessaging=null;this.versionControl=null;this.log=new XFAObjectArray}}class Compress extends XFAObject{constructor(e){super(Yo,"compress");this.scope=getStringOption(e.scope,["imageOnly","document"])}}class CompressLogicalStructure extends Option01{constructor(e){super(Yo,"compressLogicalStructure")}}class CompressObjectStream extends Option10{constructor(e){super(Yo,"compressObjectStream")}}class Compression extends XFAObject{constructor(e){super(Yo,"compression",!0);this.compressLogicalStructure=null;this.compressObjectStream=null;this.level=null;this.type=null}}class Config extends XFAObject{constructor(e){super(Yo,"config",!0);this.acrobat=null;this.present=null;this.trace=null;this.agent=new XFAObjectArray}}class Conformance extends OptionObject{constructor(e){super(Yo,"conformance",["A","B"])}}class ContentCopy extends Option01{constructor(e){super(Yo,"contentCopy")}}class Copies extends IntegerObject{constructor(e){super(Yo,"copies",1,(e=>e>=1))}}class Creator extends StringObject{constructor(e){super(Yo,"creator")}}class CurrentPage extends IntegerObject{constructor(e){super(Yo,"currentPage",0,(e=>e>=0))}}class Data extends XFAObject{constructor(e){super(Yo,"data",!0);this.adjustData=null;this.attributes=null;this.incrementalLoad=null;this.outputXSL=null;this.range=null;this.record=null;this.startNode=null;this.uri=null;this.window=null;this.xsl=null;this.excludeNS=new XFAObjectArray;this.transform=new XFAObjectArray}}class Debug extends XFAObject{constructor(e){super(Yo,"debug",!0);this.uri=null}}class DefaultTypeface extends ContentObject{constructor(e){super(Yo,"defaultTypeface");this.writingScript=getStringOption(e.writingScript,["*","Arabic","Cyrillic","EastEuropeanRoman","Greek","Hebrew","Japanese","Korean","Roman","SimplifiedChinese","Thai","TraditionalChinese","Vietnamese"])}}class Destination extends OptionObject{constructor(e){super(Yo,"destination",["pdf","pcl","ps","webClient","zpl"])}}class DocumentAssembly extends Option01{constructor(e){super(Yo,"documentAssembly")}}class Driver extends XFAObject{constructor(e){super(Yo,"driver",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class DuplexOption extends OptionObject{constructor(e){super(Yo,"duplexOption",["simplex","duplexFlipLongEdge","duplexFlipShortEdge"])}}class DynamicRender extends OptionObject{constructor(e){super(Yo,"dynamicRender",["forbidden","required"])}}class Embed extends Option01{constructor(e){super(Yo,"embed")}}class config_Encrypt extends Option01{constructor(e){super(Yo,"encrypt")}}class config_Encryption extends XFAObject{constructor(e){super(Yo,"encryption",!0);this.encrypt=null;this.encryptionLevel=null;this.permissions=null}}class EncryptionLevel extends OptionObject{constructor(e){super(Yo,"encryptionLevel",["40bit","128bit"])}}class Enforce extends StringObject{constructor(e){super(Yo,"enforce")}}class Equate extends XFAObject{constructor(e){super(Yo,"equate");this.force=getInteger({data:e.force,defaultValue:1,validate:e=>0===e});this.from=e.from||"";this.to=e.to||""}}class EquateRange extends XFAObject{constructor(e){super(Yo,"equateRange");this.from=e.from||"";this.to=e.to||"";this._unicodeRange=e.unicodeRange||""}get unicodeRange(){const e=[],t=/U\+([0-9a-fA-F]+)/,a=this._unicodeRange;for(let r of a.split(",").map((e=>e.trim())).filter((e=>!!e))){r=r.split("-",2).map((e=>{const a=e.match(t);return a?parseInt(a[1],16):0}));1===r.length&&r.push(r[0]);e.push(r)}return shadow(this,"unicodeRange",e)}}class Exclude extends ContentObject{constructor(e){super(Yo,"exclude")}[cs](){this[is]=this[is].trim().split(/\s+/).filter((e=>e&&["calculate","close","enter","exit","initialize","ready","validate"].includes(e)))}}class ExcludeNS extends StringObject{constructor(e){super(Yo,"excludeNS")}}class FlipLabel extends OptionObject{constructor(e){super(Yo,"flipLabel",["usePrinterSetting","on","off"])}}class config_FontInfo extends XFAObject{constructor(e){super(Yo,"fontInfo",!0);this.embed=null;this.map=null;this.subsetBelow=null;this.alwaysEmbed=new XFAObjectArray;this.defaultTypeface=new XFAObjectArray;this.neverEmbed=new XFAObjectArray}}class FormFieldFilling extends Option01{constructor(e){super(Yo,"formFieldFilling")}}class GroupParent extends StringObject{constructor(e){super(Yo,"groupParent")}}class IfEmpty extends OptionObject{constructor(e){super(Yo,"ifEmpty",["dataValue","dataGroup","ignore","remove"])}}class IncludeXDPContent extends StringObject{constructor(e){super(Yo,"includeXDPContent")}}class IncrementalLoad extends OptionObject{constructor(e){super(Yo,"incrementalLoad",["none","forwardOnly"])}}class IncrementalMerge extends Option01{constructor(e){super(Yo,"incrementalMerge")}}class Interactive extends Option01{constructor(e){super(Yo,"interactive")}}class Jog extends OptionObject{constructor(e){super(Yo,"jog",["usePrinterSetting","none","pageSet"])}}class LabelPrinter extends XFAObject{constructor(e){super(Yo,"labelPrinter",!0);this.name=getStringOption(e.name,["zpl","dpl","ipl","tcpl"]);this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class Layout extends OptionObject{constructor(e){super(Yo,"layout",["paginate","panel"])}}class Level extends IntegerObject{constructor(e){super(Yo,"level",0,(e=>e>0))}}class Linearized extends Option01{constructor(e){super(Yo,"linearized")}}class Locale extends StringObject{constructor(e){super(Yo,"locale")}}class LocaleSet extends StringObject{constructor(e){super(Yo,"localeSet")}}class Log extends XFAObject{constructor(e){super(Yo,"log",!0);this.mode=null;this.threshold=null;this.to=null;this.uri=null}}class MapElement extends XFAObject{constructor(e){super(Yo,"map",!0);this.equate=new XFAObjectArray;this.equateRange=new XFAObjectArray}}class MediumInfo extends XFAObject{constructor(e){super(Yo,"mediumInfo",!0);this.map=null}}class config_Message extends XFAObject{constructor(e){super(Yo,"message",!0);this.msgId=null;this.severity=null}}class Messaging extends XFAObject{constructor(e){super(Yo,"messaging",!0);this.message=new XFAObjectArray}}class Mode extends OptionObject{constructor(e){super(Yo,"mode",["append","overwrite"])}}class ModifyAnnots extends Option01{constructor(e){super(Yo,"modifyAnnots")}}class MsgId extends IntegerObject{constructor(e){super(Yo,"msgId",1,(e=>e>=1))}}class NameAttr extends StringObject{constructor(e){super(Yo,"nameAttr")}}class NeverEmbed extends ContentObject{constructor(e){super(Yo,"neverEmbed")}}class NumberOfCopies extends IntegerObject{constructor(e){super(Yo,"numberOfCopies",null,(e=>e>=2&&e<=5))}}class OpenAction extends XFAObject{constructor(e){super(Yo,"openAction",!0);this.destination=null}}class Output extends XFAObject{constructor(e){super(Yo,"output",!0);this.to=null;this.type=null;this.uri=null}}class OutputBin extends StringObject{constructor(e){super(Yo,"outputBin")}}class OutputXSL extends XFAObject{constructor(e){super(Yo,"outputXSL",!0);this.uri=null}}class Overprint extends OptionObject{constructor(e){super(Yo,"overprint",["none","both","draw","field"])}}class Packets extends StringObject{constructor(e){super(Yo,"packets")}[cs](){"*"!==this[is]&&(this[is]=this[is].trim().split(/\s+/).filter((e=>["config","datasets","template","xfdf","xslt"].includes(e))))}}class PageOffset extends XFAObject{constructor(e){super(Yo,"pageOffset");this.x=getInteger({data:e.x,defaultValue:"useXDCSetting",validate:e=>!0});this.y=getInteger({data:e.y,defaultValue:"useXDCSetting",validate:e=>!0})}}class PageRange extends StringObject{constructor(e){super(Yo,"pageRange")}[cs](){const e=this[is].trim().split(/\s+/).map((e=>parseInt(e,10))),t=[];for(let a=0,r=e.length;a!1))}}class Pcl extends XFAObject{constructor(e){super(Yo,"pcl",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.pageOffset=null;this.staple=null;this.xdc=null}}class Pdf extends XFAObject{constructor(e){super(Yo,"pdf",!0);this.name=e.name||"";this.adobeExtensionLevel=null;this.batchOutput=null;this.compression=null;this.creator=null;this.encryption=null;this.fontInfo=null;this.interactive=null;this.linearized=null;this.openAction=null;this.pdfa=null;this.producer=null;this.renderPolicy=null;this.scriptModel=null;this.silentPrint=null;this.submitFormat=null;this.tagged=null;this.version=null;this.viewerPreferences=null;this.xdc=null}}class Pdfa extends XFAObject{constructor(e){super(Yo,"pdfa",!0);this.amd=null;this.conformance=null;this.includeXDPContent=null;this.part=null}}class Permissions extends XFAObject{constructor(e){super(Yo,"permissions",!0);this.accessibleContent=null;this.change=null;this.contentCopy=null;this.documentAssembly=null;this.formFieldFilling=null;this.modifyAnnots=null;this.plaintextMetadata=null;this.print=null;this.printHighQuality=null}}class PickTrayByPDFSize extends Option01{constructor(e){super(Yo,"pickTrayByPDFSize")}}class config_Picture extends StringObject{constructor(e){super(Yo,"picture")}}class PlaintextMetadata extends Option01{constructor(e){super(Yo,"plaintextMetadata")}}class Presence extends OptionObject{constructor(e){super(Yo,"presence",["preserve","dissolve","dissolveStructure","ignore","remove"])}}class Present extends XFAObject{constructor(e){super(Yo,"present",!0);this.behaviorOverride=null;this.cache=null;this.common=null;this.copies=null;this.destination=null;this.incrementalMerge=null;this.layout=null;this.output=null;this.overprint=null;this.pagination=null;this.paginationOverride=null;this.script=null;this.validate=null;this.xdp=null;this.driver=new XFAObjectArray;this.labelPrinter=new XFAObjectArray;this.pcl=new XFAObjectArray;this.pdf=new XFAObjectArray;this.ps=new XFAObjectArray;this.submitUrl=new XFAObjectArray;this.webClient=new XFAObjectArray;this.zpl=new XFAObjectArray}}class Print extends Option01{constructor(e){super(Yo,"print")}}class PrintHighQuality extends Option01{constructor(e){super(Yo,"printHighQuality")}}class PrintScaling extends OptionObject{constructor(e){super(Yo,"printScaling",["appdefault","noScaling"])}}class PrinterName extends StringObject{constructor(e){super(Yo,"printerName")}}class Producer extends StringObject{constructor(e){super(Yo,"producer")}}class Ps extends XFAObject{constructor(e){super(Yo,"ps",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.staple=null;this.xdc=null}}class Range extends ContentObject{constructor(e){super(Yo,"range")}[cs](){this[is]=this[is].split(",",2).map((e=>e.split("-").map((e=>parseInt(e.trim(),10))))).filter((e=>e.every((e=>!isNaN(e))))).map((e=>{1===e.length&&e.push(e[0]);return e}))}}class Record extends ContentObject{constructor(e){super(Yo,"record")}[cs](){this[is]=this[is].trim();const e=parseInt(this[is],10);!isNaN(e)&&e>=0&&(this[is]=e)}}class Relevant extends ContentObject{constructor(e){super(Yo,"relevant")}[cs](){this[is]=this[is].trim().split(/\s+/)}}class Rename extends ContentObject{constructor(e){super(Yo,"rename")}[cs](){this[is]=this[is].trim();(this[is].toLowerCase().startsWith("xml")||new RegExp("[\\p{L}_][\\p{L}\\d._\\p{M}-]*","u").test(this[is]))&&warn("XFA - Rename: invalid XFA name")}}class RenderPolicy extends OptionObject{constructor(e){super(Yo,"renderPolicy",["server","client"])}}class RunScripts extends OptionObject{constructor(e){super(Yo,"runScripts",["both","client","none","server"])}}class config_Script extends XFAObject{constructor(e){super(Yo,"script",!0);this.currentPage=null;this.exclude=null;this.runScripts=null}}class ScriptModel extends OptionObject{constructor(e){super(Yo,"scriptModel",["XFA","none"])}}class Severity extends OptionObject{constructor(e){super(Yo,"severity",["ignore","error","information","trace","warning"])}}class SilentPrint extends XFAObject{constructor(e){super(Yo,"silentPrint",!0);this.addSilentPrint=null;this.printerName=null}}class Staple extends XFAObject{constructor(e){super(Yo,"staple");this.mode=getStringOption(e.mode,["usePrinterSetting","on","off"])}}class StartNode extends StringObject{constructor(e){super(Yo,"startNode")}}class StartPage extends IntegerObject{constructor(e){super(Yo,"startPage",0,(e=>!0))}}class SubmitFormat extends OptionObject{constructor(e){super(Yo,"submitFormat",["html","delegate","fdf","xml","pdf"])}}class SubmitUrl extends StringObject{constructor(e){super(Yo,"submitUrl")}}class SubsetBelow extends IntegerObject{constructor(e){super(Yo,"subsetBelow",100,(e=>e>=0&&e<=100))}}class SuppressBanner extends Option01{constructor(e){super(Yo,"suppressBanner")}}class Tagged extends Option01{constructor(e){super(Yo,"tagged")}}class config_Template extends XFAObject{constructor(e){super(Yo,"template",!0);this.base=null;this.relevant=null;this.startPage=null;this.uri=null;this.xsl=null}}class Threshold extends OptionObject{constructor(e){super(Yo,"threshold",["trace","error","information","warning"])}}class To extends OptionObject{constructor(e){super(Yo,"to",["null","memory","stderr","stdout","system","uri"])}}class TemplateCache extends XFAObject{constructor(e){super(Yo,"templateCache");this.maxEntries=getInteger({data:e.maxEntries,defaultValue:5,validate:e=>e>=0})}}class Trace extends XFAObject{constructor(e){super(Yo,"trace",!0);this.area=new XFAObjectArray}}class Transform extends XFAObject{constructor(e){super(Yo,"transform",!0);this.groupParent=null;this.ifEmpty=null;this.nameAttr=null;this.picture=null;this.presence=null;this.rename=null;this.whitespace=null}}class Type extends OptionObject{constructor(e){super(Yo,"type",["none","ascii85","asciiHex","ccittfax","flate","lzw","runLength","native","xdp","mergedXDP"])}}class Uri extends StringObject{constructor(e){super(Yo,"uri")}}class config_Validate extends OptionObject{constructor(e){super(Yo,"validate",["preSubmit","prePrint","preExecute","preSave"])}}class ValidateApprovalSignatures extends ContentObject{constructor(e){super(Yo,"validateApprovalSignatures")}[cs](){this[is]=this[is].trim().split(/\s+/).filter((e=>["docReady","postSign"].includes(e)))}}class ValidationMessaging extends OptionObject{constructor(e){super(Yo,"validationMessaging",["allMessagesIndividually","allMessagesTogether","firstMessageOnly","noMessages"])}}class Version extends OptionObject{constructor(e){super(Yo,"version",["1.7","1.6","1.5","1.4","1.3","1.2"])}}class VersionControl extends XFAObject{constructor(e){super(Yo,"VersionControl");this.outputBelow=getStringOption(e.outputBelow,["warn","error","update"]);this.sourceAbove=getStringOption(e.sourceAbove,["warn","error"]);this.sourceBelow=getStringOption(e.sourceBelow,["update","maintain"])}}class ViewerPreferences extends XFAObject{constructor(e){super(Yo,"viewerPreferences",!0);this.ADBE_JSConsole=null;this.ADBE_JSDebugger=null;this.addViewerPreferences=null;this.duplexOption=null;this.enforce=null;this.numberOfCopies=null;this.pageRange=null;this.pickTrayByPDFSize=null;this.printScaling=null}}class WebClient extends XFAObject{constructor(e){super(Yo,"webClient",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class Whitespace extends OptionObject{constructor(e){super(Yo,"whitespace",["preserve","ltrim","normalize","rtrim","trim"])}}class Window extends ContentObject{constructor(e){super(Yo,"window")}[cs](){const e=this[is].split(",",2).map((e=>parseInt(e.trim(),10)));if(e.some((e=>isNaN(e))))this[is]=[0,0];else{1===e.length&&e.push(e[0]);this[is]=e}}}class Xdc extends XFAObject{constructor(e){super(Yo,"xdc",!0);this.uri=new XFAObjectArray;this.xsl=new XFAObjectArray}}class Xdp extends XFAObject{constructor(e){super(Yo,"xdp",!0);this.packets=null}}class Xsl extends XFAObject{constructor(e){super(Yo,"xsl",!0);this.debug=null;this.uri=null}}class Zpl extends XFAObject{constructor(e){super(Yo,"zpl",!0);this.name=e.name?e.name.trim():"";this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class ConfigNamespace{static[ho](e,t){if(ConfigNamespace.hasOwnProperty(e))return ConfigNamespace[e](t)}static acrobat(e){return new Acrobat(e)}static acrobat7(e){return new Acrobat7(e)}static ADBE_JSConsole(e){return new ADBE_JSConsole(e)}static ADBE_JSDebugger(e){return new ADBE_JSDebugger(e)}static addSilentPrint(e){return new AddSilentPrint(e)}static addViewerPreferences(e){return new AddViewerPreferences(e)}static adjustData(e){return new AdjustData(e)}static adobeExtensionLevel(e){return new AdobeExtensionLevel(e)}static agent(e){return new Agent(e)}static alwaysEmbed(e){return new AlwaysEmbed(e)}static amd(e){return new Amd(e)}static area(e){return new config_Area(e)}static attributes(e){return new Attributes(e)}static autoSave(e){return new AutoSave(e)}static base(e){return new Base(e)}static batchOutput(e){return new BatchOutput(e)}static behaviorOverride(e){return new BehaviorOverride(e)}static cache(e){return new Cache(e)}static change(e){return new Change(e)}static common(e){return new Common(e)}static compress(e){return new Compress(e)}static compressLogicalStructure(e){return new CompressLogicalStructure(e)}static compressObjectStream(e){return new CompressObjectStream(e)}static compression(e){return new Compression(e)}static config(e){return new Config(e)}static conformance(e){return new Conformance(e)}static contentCopy(e){return new ContentCopy(e)}static copies(e){return new Copies(e)}static creator(e){return new Creator(e)}static currentPage(e){return new CurrentPage(e)}static data(e){return new Data(e)}static debug(e){return new Debug(e)}static defaultTypeface(e){return new DefaultTypeface(e)}static destination(e){return new Destination(e)}static documentAssembly(e){return new DocumentAssembly(e)}static driver(e){return new Driver(e)}static duplexOption(e){return new DuplexOption(e)}static dynamicRender(e){return new DynamicRender(e)}static embed(e){return new Embed(e)}static encrypt(e){return new config_Encrypt(e)}static encryption(e){return new config_Encryption(e)}static encryptionLevel(e){return new EncryptionLevel(e)}static enforce(e){return new Enforce(e)}static equate(e){return new Equate(e)}static equateRange(e){return new EquateRange(e)}static exclude(e){return new Exclude(e)}static excludeNS(e){return new ExcludeNS(e)}static flipLabel(e){return new FlipLabel(e)}static fontInfo(e){return new config_FontInfo(e)}static formFieldFilling(e){return new FormFieldFilling(e)}static groupParent(e){return new GroupParent(e)}static ifEmpty(e){return new IfEmpty(e)}static includeXDPContent(e){return new IncludeXDPContent(e)}static incrementalLoad(e){return new IncrementalLoad(e)}static incrementalMerge(e){return new IncrementalMerge(e)}static interactive(e){return new Interactive(e)}static jog(e){return new Jog(e)}static labelPrinter(e){return new LabelPrinter(e)}static layout(e){return new Layout(e)}static level(e){return new Level(e)}static linearized(e){return new Linearized(e)}static locale(e){return new Locale(e)}static localeSet(e){return new LocaleSet(e)}static log(e){return new Log(e)}static map(e){return new MapElement(e)}static mediumInfo(e){return new MediumInfo(e)}static message(e){return new config_Message(e)}static messaging(e){return new Messaging(e)}static mode(e){return new Mode(e)}static modifyAnnots(e){return new ModifyAnnots(e)}static msgId(e){return new MsgId(e)}static nameAttr(e){return new NameAttr(e)}static neverEmbed(e){return new NeverEmbed(e)}static numberOfCopies(e){return new NumberOfCopies(e)}static openAction(e){return new OpenAction(e)}static output(e){return new Output(e)}static outputBin(e){return new OutputBin(e)}static outputXSL(e){return new OutputXSL(e)}static overprint(e){return new Overprint(e)}static packets(e){return new Packets(e)}static pageOffset(e){return new PageOffset(e)}static pageRange(e){return new PageRange(e)}static pagination(e){return new Pagination(e)}static paginationOverride(e){return new PaginationOverride(e)}static part(e){return new Part(e)}static pcl(e){return new Pcl(e)}static pdf(e){return new Pdf(e)}static pdfa(e){return new Pdfa(e)}static permissions(e){return new Permissions(e)}static pickTrayByPDFSize(e){return new PickTrayByPDFSize(e)}static picture(e){return new config_Picture(e)}static plaintextMetadata(e){return new PlaintextMetadata(e)}static presence(e){return new Presence(e)}static present(e){return new Present(e)}static print(e){return new Print(e)}static printHighQuality(e){return new PrintHighQuality(e)}static printScaling(e){return new PrintScaling(e)}static printerName(e){return new PrinterName(e)}static producer(e){return new Producer(e)}static ps(e){return new Ps(e)}static range(e){return new Range(e)}static record(e){return new Record(e)}static relevant(e){return new Relevant(e)}static rename(e){return new Rename(e)}static renderPolicy(e){return new RenderPolicy(e)}static runScripts(e){return new RunScripts(e)}static script(e){return new config_Script(e)}static scriptModel(e){return new ScriptModel(e)}static severity(e){return new Severity(e)}static silentPrint(e){return new SilentPrint(e)}static staple(e){return new Staple(e)}static startNode(e){return new StartNode(e)}static startPage(e){return new StartPage(e)}static submitFormat(e){return new SubmitFormat(e)}static submitUrl(e){return new SubmitUrl(e)}static subsetBelow(e){return new SubsetBelow(e)}static suppressBanner(e){return new SuppressBanner(e)}static tagged(e){return new Tagged(e)}static template(e){return new config_Template(e)}static templateCache(e){return new TemplateCache(e)}static threshold(e){return new Threshold(e)}static to(e){return new To(e)}static trace(e){return new Trace(e)}static transform(e){return new Transform(e)}static type(e){return new Type(e)}static uri(e){return new Uri(e)}static validate(e){return new config_Validate(e)}static validateApprovalSignatures(e){return new ValidateApprovalSignatures(e)}static validationMessaging(e){return new ValidationMessaging(e)}static version(e){return new Version(e)}static versionControl(e){return new VersionControl(e)}static viewerPreferences(e){return new ViewerPreferences(e)}static webClient(e){return new WebClient(e)}static whitespace(e){return new Whitespace(e)}static window(e){return new Window(e)}static xdc(e){return new Xdc(e)}static xdp(e){return new Xdp(e)}static xsl(e){return new Xsl(e)}static zpl(e){return new Zpl(e)}}const Zo=uo.connectionSet.id;class ConnectionSet extends XFAObject{constructor(e){super(Zo,"connectionSet",!0);this.wsdlConnection=new XFAObjectArray;this.xmlConnection=new XFAObjectArray;this.xsdConnection=new XFAObjectArray}}class EffectiveInputPolicy extends XFAObject{constructor(e){super(Zo,"effectiveInputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EffectiveOutputPolicy extends XFAObject{constructor(e){super(Zo,"effectiveOutputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Operation extends StringObject{constructor(e){super(Zo,"operation");this.id=e.id||"";this.input=e.input||"";this.name=e.name||"";this.output=e.output||"";this.use=e.use||"";this.usehref=e.usehref||""}}class RootElement extends StringObject{constructor(e){super(Zo,"rootElement");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAction extends StringObject{constructor(e){super(Zo,"soapAction");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAddress extends StringObject{constructor(e){super(Zo,"soapAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class connection_set_Uri extends StringObject{constructor(e){super(Zo,"uri");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlAddress extends StringObject{constructor(e){super(Zo,"wsdlAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlConnection extends XFAObject{constructor(e){super(Zo,"wsdlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.effectiveInputPolicy=null;this.effectiveOutputPolicy=null;this.operation=null;this.soapAction=null;this.soapAddress=null;this.wsdlAddress=null}}class XmlConnection extends XFAObject{constructor(e){super(Zo,"xmlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.uri=null}}class XsdConnection extends XFAObject{constructor(e){super(Zo,"xsdConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.rootElement=null;this.uri=null}}class ConnectionSetNamespace{static[ho](e,t){if(ConnectionSetNamespace.hasOwnProperty(e))return ConnectionSetNamespace[e](t)}static connectionSet(e){return new ConnectionSet(e)}static effectiveInputPolicy(e){return new EffectiveInputPolicy(e)}static effectiveOutputPolicy(e){return new EffectiveOutputPolicy(e)}static operation(e){return new Operation(e)}static rootElement(e){return new RootElement(e)}static soapAction(e){return new SoapAction(e)}static soapAddress(e){return new SoapAddress(e)}static uri(e){return new connection_set_Uri(e)}static wsdlAddress(e){return new WsdlAddress(e)}static wsdlConnection(e){return new WsdlConnection(e)}static xmlConnection(e){return new XmlConnection(e)}static xsdConnection(e){return new XsdConnection(e)}}const Qo=uo.datasets.id;class datasets_Data extends XmlObject{constructor(e){super(Qo,"data",e)}[Ns](){return!0}}class Datasets extends XFAObject{constructor(e){super(Qo,"datasets",!0);this.data=null;this.Signature=null}[Ws](e){const t=e[qs];("data"===t&&e[Xs]===Qo||"Signature"===t&&e[Xs]===uo.signature.id)&&(this[t]=e);this[Yn](e)}}class DatasetsNamespace{static[ho](e,t){if(DatasetsNamespace.hasOwnProperty(e))return DatasetsNamespace[e](t)}static datasets(e){return new Datasets(e)}static data(e){return new datasets_Data(e)}}const ec=uo.localeSet.id;class CalendarSymbols extends XFAObject{constructor(e){super(ec,"calendarSymbols",!0);this.name="gregorian";this.dayNames=new XFAObjectArray(2);this.eraNames=null;this.meridiemNames=null;this.monthNames=new XFAObjectArray(2)}}class CurrencySymbol extends StringObject{constructor(e){super(ec,"currencySymbol");this.name=getStringOption(e.name,["symbol","isoname","decimal"])}}class CurrencySymbols extends XFAObject{constructor(e){super(ec,"currencySymbols",!0);this.currencySymbol=new XFAObjectArray(3)}}class DatePattern extends StringObject{constructor(e){super(ec,"datePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class DatePatterns extends XFAObject{constructor(e){super(ec,"datePatterns",!0);this.datePattern=new XFAObjectArray(4)}}class DateTimeSymbols extends ContentObject{constructor(e){super(ec,"dateTimeSymbols")}}class Day extends StringObject{constructor(e){super(ec,"day")}}class DayNames extends XFAObject{constructor(e){super(ec,"dayNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.day=new XFAObjectArray(7)}}class Era extends StringObject{constructor(e){super(ec,"era")}}class EraNames extends XFAObject{constructor(e){super(ec,"eraNames",!0);this.era=new XFAObjectArray(2)}}class locale_set_Locale extends XFAObject{constructor(e){super(ec,"locale",!0);this.desc=e.desc||"";this.name="isoname";this.calendarSymbols=null;this.currencySymbols=null;this.datePatterns=null;this.dateTimeSymbols=null;this.numberPatterns=null;this.numberSymbols=null;this.timePatterns=null;this.typeFaces=null}}class locale_set_LocaleSet extends XFAObject{constructor(e){super(ec,"localeSet",!0);this.locale=new XFAObjectArray}}class Meridiem extends StringObject{constructor(e){super(ec,"meridiem")}}class MeridiemNames extends XFAObject{constructor(e){super(ec,"meridiemNames",!0);this.meridiem=new XFAObjectArray(2)}}class Month extends StringObject{constructor(e){super(ec,"month")}}class MonthNames extends XFAObject{constructor(e){super(ec,"monthNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.month=new XFAObjectArray(12)}}class NumberPattern extends StringObject{constructor(e){super(ec,"numberPattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class NumberPatterns extends XFAObject{constructor(e){super(ec,"numberPatterns",!0);this.numberPattern=new XFAObjectArray(4)}}class NumberSymbol extends StringObject{constructor(e){super(ec,"numberSymbol");this.name=getStringOption(e.name,["decimal","grouping","percent","minus","zero"])}}class NumberSymbols extends XFAObject{constructor(e){super(ec,"numberSymbols",!0);this.numberSymbol=new XFAObjectArray(5)}}class TimePattern extends StringObject{constructor(e){super(ec,"timePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class TimePatterns extends XFAObject{constructor(e){super(ec,"timePatterns",!0);this.timePattern=new XFAObjectArray(4)}}class TypeFace extends XFAObject{constructor(e){super(ec,"typeFace",!0);this.name=""|e.name}}class TypeFaces extends XFAObject{constructor(e){super(ec,"typeFaces",!0);this.typeFace=new XFAObjectArray}}class LocaleSetNamespace{static[ho](e,t){if(LocaleSetNamespace.hasOwnProperty(e))return LocaleSetNamespace[e](t)}static calendarSymbols(e){return new CalendarSymbols(e)}static currencySymbol(e){return new CurrencySymbol(e)}static currencySymbols(e){return new CurrencySymbols(e)}static datePattern(e){return new DatePattern(e)}static datePatterns(e){return new DatePatterns(e)}static dateTimeSymbols(e){return new DateTimeSymbols(e)}static day(e){return new Day(e)}static dayNames(e){return new DayNames(e)}static era(e){return new Era(e)}static eraNames(e){return new EraNames(e)}static locale(e){return new locale_set_Locale(e)}static localeSet(e){return new locale_set_LocaleSet(e)}static meridiem(e){return new Meridiem(e)}static meridiemNames(e){return new MeridiemNames(e)}static month(e){return new Month(e)}static monthNames(e){return new MonthNames(e)}static numberPattern(e){return new NumberPattern(e)}static numberPatterns(e){return new NumberPatterns(e)}static numberSymbol(e){return new NumberSymbol(e)}static numberSymbols(e){return new NumberSymbols(e)}static timePattern(e){return new TimePattern(e)}static timePatterns(e){return new TimePatterns(e)}static typeFace(e){return new TypeFace(e)}static typeFaces(e){return new TypeFaces(e)}}const tc=uo.signature.id;class signature_Signature extends XFAObject{constructor(e){super(tc,"signature",!0)}}class SignatureNamespace{static[ho](e,t){if(SignatureNamespace.hasOwnProperty(e))return SignatureNamespace[e](t)}static signature(e){return new signature_Signature(e)}}const ac=uo.stylesheet.id;class Stylesheet extends XFAObject{constructor(e){super(ac,"stylesheet",!0)}}class StylesheetNamespace{static[ho](e,t){if(StylesheetNamespace.hasOwnProperty(e))return StylesheetNamespace[e](t)}static stylesheet(e){return new Stylesheet(e)}}const rc=uo.xdp.id;class xdp_Xdp extends XFAObject{constructor(e){super(rc,"xdp",!0);this.uuid=e.uuid||"";this.timeStamp=e.timeStamp||"";this.config=null;this.connectionSet=null;this.datasets=null;this.localeSet=null;this.stylesheet=new XFAObjectArray;this.template=null}[zs](e){const t=uo[e[qs]];return t&&e[Xs]===t.id}}class XdpNamespace{static[ho](e,t){if(XdpNamespace.hasOwnProperty(e))return XdpNamespace[e](t)}static xdp(e){return new xdp_Xdp(e)}}const ic=uo.xhtml.id,nc=Symbol(),sc=new Set(["color","font","font-family","font-size","font-stretch","font-style","font-weight","margin","margin-bottom","margin-left","margin-right","margin-top","letter-spacing","line-height","orphans","page-break-after","page-break-before","page-break-inside","tab-interval","tab-stop","text-align","text-decoration","text-indent","vertical-align","widows","kerning-mode","xfa-font-horizontal-scale","xfa-font-vertical-scale","xfa-spacerun","xfa-tab-stops"]),oc=new Map([["page-break-after","breakAfter"],["page-break-before","breakBefore"],["page-break-inside","breakInside"],["kerning-mode",e=>"none"===e?"none":"normal"],["xfa-font-horizontal-scale",e=>`scaleX(${Math.max(0,parseInt(e)/100).toFixed(2)})`],["xfa-font-vertical-scale",e=>`scaleY(${Math.max(0,parseInt(e)/100).toFixed(2)})`],["xfa-spacerun",""],["xfa-tab-stops",""],["font-size",(e,t)=>measureToString(.99*(e=t.fontSize=Math.abs(getMeasurement(e))))],["letter-spacing",e=>measureToString(getMeasurement(e))],["line-height",e=>measureToString(getMeasurement(e))],["margin",e=>measureToString(getMeasurement(e))],["margin-bottom",e=>measureToString(getMeasurement(e))],["margin-left",e=>measureToString(getMeasurement(e))],["margin-right",e=>measureToString(getMeasurement(e))],["margin-top",e=>measureToString(getMeasurement(e))],["text-indent",e=>measureToString(getMeasurement(e))],["font-family",e=>e],["vertical-align",e=>measureToString(getMeasurement(e))]]),cc=/\s+/g,lc=/[\r\n]+/g,hc=/\r\n?/g;function mapStyle(e,t,a){const r=Object.create(null);if(!e)return r;const i=Object.create(null);for(const[t,a]of e.split(";").map((e=>e.split(":",2)))){const e=oc.get(t);if(""===e)continue;let n=a;e&&(n="string"==typeof e?e:e(a,i));t.endsWith("scale")?r.transform=r.transform?`${r[t]} ${n}`:n:r[t.replaceAll(/-([a-zA-Z])/g,((e,t)=>t.toUpperCase()))]=n}r.fontFamily&&setFontFamily({typeface:r.fontFamily,weight:r.fontWeight||"normal",posture:r.fontStyle||"normal",size:i.fontSize||0},t,t[vs].fontFinder,r);if(a&&r.verticalAlign&&"0px"!==r.verticalAlign&&r.fontSize){const e=.583,t=.333,a=getMeasurement(r.fontSize);r.fontSize=measureToString(a*e);r.verticalAlign=measureToString(Math.sign(getMeasurement(r.verticalAlign))*a*t)}a&&r.fontSize&&(r.fontSize=`calc(${r.fontSize} * var(--total-scale-factor))`);fixTextIndent(r);return r}const uc=new Set(["body","html"]);class XhtmlObject extends XmlObject{constructor(e,t){super(ic,t);this[nc]=!1;this.style=e.style||""}[Qn](e){super[Qn](e);this.style=function checkStyle(e){return e.style?e.style.split(";").filter((e=>!!e.trim())).map((e=>e.split(":",2).map((e=>e.trim())))).filter((([t,a])=>{"font-family"===t&&e[vs].usedTypefaces.add(a);return sc.has(t)})).map((e=>e.join(":"))).join(";"):""}(this)}[Kn](){return!uc.has(this[qs])}[$s](e,t=!1){if(t)this[nc]=!0;else{e=e.replaceAll(lc,"");this.style.includes("xfa-spacerun:yes")||(e=e.replaceAll(cc," "))}e&&(this[is]+=e)}[Gs](e,t=!0){const a=Object.create(null),r={top:NaN,bottom:NaN,left:NaN,right:NaN};let i=null;for(const[e,t]of this.style.split(";").map((e=>e.split(":",2))))switch(e){case"font-family":a.typeface=stripQuotes(t);break;case"font-size":a.size=getMeasurement(t);break;case"font-weight":a.weight=t;break;case"font-style":a.posture=t;break;case"letter-spacing":a.letterSpacing=getMeasurement(t);break;case"margin":const e=t.split(/ \t/).map((e=>getMeasurement(e)));switch(e.length){case 1:r.top=r.bottom=r.left=r.right=e[0];break;case 2:r.top=r.bottom=e[0];r.left=r.right=e[1];break;case 3:r.top=e[0];r.bottom=e[2];r.left=r.right=e[1];break;case 4:r.top=e[0];r.left=e[1];r.bottom=e[2];r.right=e[3]}break;case"margin-top":r.top=getMeasurement(t);break;case"margin-bottom":r.bottom=getMeasurement(t);break;case"margin-left":r.left=getMeasurement(t);break;case"margin-right":r.right=getMeasurement(t);break;case"line-height":i=getMeasurement(t)}e.pushData(a,r,i);if(this[is])e.addString(this[is]);else for(const t of this[ws]())"#text"!==t[qs]?t[Gs](e):e.addString(t[is]);t&&e.popFont()}[so](e){const t=[];this[os]={children:t};this[Zn]({});if(0===t.length&&!this[is])return HTMLResult.EMPTY;let a;a=this[nc]?this[is]?this[is].replaceAll(hc,"\n"):void 0:this[is]||void 0;return HTMLResult.success({name:this[qs],attributes:{href:this.href,style:mapStyle(this.style,this,this[nc])},children:t,value:a})}}class A extends XhtmlObject{constructor(e){super(e,"a");this.href=fixURL(e.href)||""}}class B extends XhtmlObject{constructor(e){super(e,"b")}[Gs](e){e.pushFont({weight:"bold"});super[Gs](e);e.popFont()}}class Body extends XhtmlObject{constructor(e){super(e,"body")}[so](e){const t=super[so](e),{html:a}=t;if(!a)return HTMLResult.EMPTY;a.name="div";a.attributes.class=["xfaRich"];return t}}class Br extends XhtmlObject{constructor(e){super(e,"br")}[io](){return"\n"}[Gs](e){e.addString("\n")}[so](e){return HTMLResult.success({name:"br"})}}class Html extends XhtmlObject{constructor(e){super(e,"html")}[so](e){const t=[];this[os]={children:t};this[Zn]({});if(0===t.length)return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},value:this[is]||""});if(1===t.length){const e=t[0];if(e.attributes?.class.includes("xfaRich"))return HTMLResult.success(e)}return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},children:t})}}class I extends XhtmlObject{constructor(e){super(e,"i")}[Gs](e){e.pushFont({posture:"italic"});super[Gs](e);e.popFont()}}class Li extends XhtmlObject{constructor(e){super(e,"li")}}class Ol extends XhtmlObject{constructor(e){super(e,"ol")}}class P extends XhtmlObject{constructor(e){super(e,"p")}[Gs](e){super[Gs](e,!1);e.addString("\n");e.addPara();e.popFont()}[io](){return this[ks]()[ws]().at(-1)===this?super[io]():super[io]()+"\n"}}class Span extends XhtmlObject{constructor(e){super(e,"span")}}class Sub extends XhtmlObject{constructor(e){super(e,"sub")}}class Sup extends XhtmlObject{constructor(e){super(e,"sup")}}class Ul extends XhtmlObject{constructor(e){super(e,"ul")}}class XhtmlNamespace{static[ho](e,t){if(XhtmlNamespace.hasOwnProperty(e))return XhtmlNamespace[e](t)}static a(e){return new A(e)}static b(e){return new B(e)}static body(e){return new Body(e)}static br(e){return new Br(e)}static html(e){return new Html(e)}static i(e){return new I(e)}static li(e){return new Li(e)}static ol(e){return new Ol(e)}static p(e){return new P(e)}static span(e){return new Span(e)}static sub(e){return new Sub(e)}static sup(e){return new Sup(e)}static ul(e){return new Ul(e)}}const dc={config:ConfigNamespace,connection:ConnectionSetNamespace,datasets:DatasetsNamespace,localeSet:LocaleSetNamespace,signature:SignatureNamespace,stylesheet:StylesheetNamespace,template:TemplateNamespace,xdp:XdpNamespace,xhtml:XhtmlNamespace};class UnknownNamespace{constructor(e){this.namespaceId=e}[ho](e,t){return new XmlObject(this.namespaceId,e,t)}}class Root extends XFAObject{constructor(e){super(-1,"root",Object.create(null));this.element=null;this[Is]=e}[Ws](e){this.element=e;return!0}[cs](){super[cs]();if(this.element.template instanceof Template){this[Is].set(Ys,this.element);this.element.template[Zs](this[Is]);this.element.template[Is]=this[Is]}}}class Empty extends XFAObject{constructor(){super(-1,"",Object.create(null))}[Ws](e){return!1}}class Builder{constructor(e=null){this._namespaceStack=[];this._nsAgnosticLevel=0;this._namespacePrefixes=new Map;this._namespaces=new Map;this._nextNsId=Math.max(...Object.values(uo).map((({id:e})=>e)));this._currentNamespace=e||new UnknownNamespace(++this._nextNsId)}buildRoot(e){return new Root(e)}build({nsPrefix:e,name:t,attributes:a,namespace:r,prefixes:i}){const n=null!==r;if(n){this._namespaceStack.push(this._currentNamespace);this._currentNamespace=this._searchNamespace(r)}i&&this._addNamespacePrefix(i);if(a.hasOwnProperty(Hs)){const e=dc.datasets,t=a[Hs];let r=null;for(const[a,i]of Object.entries(t)){if(this._getNamespaceToUse(a)===e){r={xfa:i};break}}r?a[Hs]=r:delete a[Hs]}const s=this._getNamespaceToUse(e),o=s?.[ho](t,a)||new Empty;o[Ns]()&&this._nsAgnosticLevel++;(n||i||o[Ns]())&&(o[ts]={hasNamespace:n,prefixes:i,nsAgnostic:o[Ns]()});return o}isNsAgnostic(){return this._nsAgnosticLevel>0}_searchNamespace(e){let t=this._namespaces.get(e);if(t)return t;for(const[a,{check:r}]of Object.entries(uo))if(r(e)){t=dc[a];if(t){this._namespaces.set(e,t);return t}break}t=new UnknownNamespace(++this._nextNsId);this._namespaces.set(e,t);return t}_addNamespacePrefix(e){for(const{prefix:t,value:a}of e){const e=this._searchNamespace(a);let r=this._namespacePrefixes.get(t);if(!r){r=[];this._namespacePrefixes.set(t,r)}r.push(e)}}_getNamespaceToUse(e){if(!e)return this._currentNamespace;const t=this._namespacePrefixes.get(e);if(t?.length>0)return t.at(-1);warn(`Unknown namespace prefix: ${e}.`);return null}clean(e){const{hasNamespace:t,prefixes:a,nsAgnostic:r}=e;t&&(this._currentNamespace=this._namespaceStack.pop());a&&a.forEach((({prefix:e})=>{this._namespacePrefixes.get(e).pop()}));r&&this._nsAgnosticLevel--}}class XFAParser extends XMLParserBase{constructor(e=null,t=!1){super();this._builder=new Builder(e);this._stack=[];this._globalData={usedTypefaces:new Set};this._ids=new Map;this._current=this._builder.buildRoot(this._ids);this._errorCode=Pn;this._whiteRegex=/^\s+$/;this._nbsps=/\xa0+/g;this._richText=t}parse(e){this.parseXml(e);if(this._errorCode===Pn){this._current[cs]();return this._current.element}}onText(e){e=e.replace(this._nbsps,(e=>e.slice(1)+" "));this._richText||this._current[Kn]()?this._current[$s](e,this._richText):this._whiteRegex.test(e)||this._current[$s](e.trim())}onCdata(e){this._current[$s](e)}_mkAttributes(e,t){let a=null,r=null;const i=Object.create({});for(const{name:n,value:s}of e)if("xmlns"===n)a?warn(`XFA - multiple namespace definition in <${t}>`):a=s;else if(n.startsWith("xmlns:")){const e=n.substring(6);r??=[];r.push({prefix:e,value:s})}else{const e=n.indexOf(":");if(-1===e)i[n]=s;else{const t=i[Hs]??=Object.create(null),[a,r]=[n.slice(0,e),n.slice(e+1)];(t[a]||=Object.create(null))[r]=s}}return[a,r,i]}_getNameAndPrefix(e,t){const a=e.indexOf(":");return-1===a?[e,null]:[e.substring(a+1),t?"":e.substring(0,a)]}onBeginElement(e,t,a){const[r,i,n]=this._mkAttributes(t,e),[s,o]=this._getNameAndPrefix(e,this._builder.isNsAgnostic()),c=this._builder.build({nsPrefix:o,name:s,attributes:n,namespace:r,prefixes:i});c[vs]=this._globalData;if(a){c[cs]();this._current[Ws](c)&&c[eo](this._ids);c[Qn](this._builder)}else{this._stack.push(this._current);this._current=c}}onEndElement(e){const t=this._current;if(t[Ms]()&&"string"==typeof t[is]){const e=new XFAParser;e._globalData=this._globalData;const a=e.parse(t[is]);t[is]=null;t[Ws](a)}t[cs]();this._current=this._stack.pop();this._current[Ws](t)&&t[eo](this._ids);t[Qn](this._builder)}onError(e){this._errorCode=e}}class XFAFactory{constructor(e){try{this.root=(new XFAParser).parse(XFAFactory._createDocument(e));const t=new Binder(this.root);this.form=t.bind();this.dataHandler=new DataHandler(this.root,t.getData());this.form[vs].template=this.form}catch(e){warn(`XFA - an error occurred during parsing and binding: ${e}`)}}isValid(){return!(!this.root||!this.form)}_createPagesHelper(){const e=this.form[no]();return new Promise(((t,a)=>{const nextIteration=()=>{try{const a=e.next();a.done?t(a.value):setTimeout(nextIteration,0)}catch(e){a(e)}};setTimeout(nextIteration,0)}))}async _createPages(){try{this.pages=await this._createPagesHelper();this.dims=this.pages.children.map((e=>{const{width:t,height:a}=e.attributes.style;return[0,0,parseInt(t),parseInt(a)]}))}catch(e){warn(`XFA - an error occurred during layout: ${e}`)}}getBoundingBox(e){return this.dims[e]}async getNumPages(){this.pages||await this._createPages();return this.dims.length}setImages(e){this.form[vs].images=e}setFonts(e){this.form[vs].fontFinder=new FontFinder(e);const t=[];for(let e of this.form[vs].usedTypefaces){e=stripQuotes(e);this.form[vs].fontFinder.find(e)||t.push(e)}return t.length>0?t:null}appendFonts(e,t){this.form[vs].fontFinder.add(e,t)}async getPages(){this.pages||await this._createPages();const e=this.pages;this.pages=null;return e}serializeData(e){return this.dataHandler.serialize(e)}static _createDocument(e){return e["/xdp:xdp"]?Object.values(e).join(""):e["xdp:xdp"]}static getRichTextAsHtml(e){if(!e||"string"!=typeof e)return null;try{let t=new XFAParser(XhtmlNamespace,!0).parse(e);if(!["body","xhtml"].includes(t[qs])){const e=XhtmlNamespace.body({});e[Yn](t);t=e}const a=t[so]();if(!a.success)return null;const{html:r}=a,{attributes:i}=r;if(i){i.class&&(i.class=i.class.filter((e=>!e.startsWith("xfa"))));i.dir="auto"}return{html:r,str:t[io]()}}catch(e){warn(`XFA - an error occurred during parsing of rich text: ${e}`)}return null}}class AnnotationFactory{static createGlobals(e){return Promise.all([e.ensureCatalog("acroForm"),e.ensureDoc("xfaDatasets"),e.ensureCatalog("structTreeRoot"),e.ensureCatalog("baseUrl"),e.ensureCatalog("attachments"),e.ensureCatalog("globalColorSpaceCache")]).then((([t,a,r,i,n,s])=>({pdfManager:e,acroForm:t instanceof Dict?t:Dict.empty,xfaDatasets:a,structTreeRoot:r,baseUrl:i,attachments:n,globalColorSpaceCache:s})),(e=>{warn(`createGlobals: "${e}".`);return null}))}static async create(e,t,a,r,i,n,s){const o=i?await this._getPageIndex(e,t,a.pdfManager):null;return a.pdfManager.ensure(this,"_create",[e,t,a,r,i,n,o,s])}static _create(e,t,a,r,i=!1,n=null,s=null,o=null){const c=e.fetchIfRef(t);if(!(c instanceof Dict))return;const{acroForm:l,pdfManager:h}=a,u=t instanceof Ref?t.toString():`annot_${r.createObjId()}`;let d=c.get("Subtype");d=d instanceof Name?d.name:null;const f={xref:e,ref:t,dict:c,subtype:d,id:u,annotationGlobals:a,collectFields:i,orphanFields:n,needAppearances:!i&&!0===l.get("NeedAppearances"),pageIndex:s,evaluatorOptions:h.evaluatorOptions,pageRef:o};switch(d){case"Link":return new LinkAnnotation(f);case"Text":return new TextAnnotation(f);case"Widget":let e=getInheritableProperty({dict:c,key:"FT"});e=e instanceof Name?e.name:null;switch(e){case"Tx":return new TextWidgetAnnotation(f);case"Btn":return new ButtonWidgetAnnotation(f);case"Ch":return new ChoiceWidgetAnnotation(f);case"Sig":return new SignatureWidgetAnnotation(f)}warn(`Unimplemented widget field type "${e}", falling back to base field type.`);return new WidgetAnnotation(f);case"Popup":return new PopupAnnotation(f);case"FreeText":return new FreeTextAnnotation(f);case"Line":return new LineAnnotation(f);case"Square":return new SquareAnnotation(f);case"Circle":return new CircleAnnotation(f);case"PolyLine":return new PolylineAnnotation(f);case"Polygon":return new PolygonAnnotation(f);case"Caret":return new CaretAnnotation(f);case"Ink":return new InkAnnotation(f);case"Highlight":return new HighlightAnnotation(f);case"Underline":return new UnderlineAnnotation(f);case"Squiggly":return new SquigglyAnnotation(f);case"StrikeOut":return new StrikeOutAnnotation(f);case"Stamp":return new StampAnnotation(f);case"FileAttachment":return new FileAttachmentAnnotation(f);default:i||warn(d?`Unimplemented annotation type "${d}", falling back to base annotation.`:"Annotation is missing the required /Subtype.");return new Annotation(f)}}static async _getPageIndex(e,t,a){try{const r=await e.fetchIfRefAsync(t);if(!(r instanceof Dict))return-1;const i=r.getRaw("P");if(i instanceof Ref)try{return await a.ensureCatalog("getPageIndex",[i])}catch(e){info(`_getPageIndex -- not a valid page reference: "${e}".`)}if(r.has("Kids"))return-1;const n=await a.ensureDoc("numPages");for(let e=0;ee/255))}function getQuadPoints(e,t){const a=e.getArray("QuadPoints");if(!isNumberArray(a,null)||0===a.length||a.length%8>0)return null;const r=new Float32Array(a.length);for(let e=0,i=a.length;et[2]||gt[3]))return null;r.set([d,p,f,p,d,g,f,g],e)}return r}function getTransformMatrix(e,t,a){const r=new Float32Array([1/0,1/0,-1/0,-1/0]);Util.axialAlignedBoundingBox(t,a,r);const[i,n,s,o]=r;if(i===s||n===o)return[1,0,0,1,e[0],e[1]];const c=(e[2]-e[0])/(s-i),l=(e[3]-e[1])/(o-n);return[c,0,0,l,e[0]-i*c,e[1]-n*l]}class Annotation{constructor(e){const{dict:t,xref:a,annotationGlobals:r,ref:i,orphanFields:n}=e,s=n?.get(i);s&&t.set("Parent",s);this.setTitle(t.get("T"));this.setContents(t.get("Contents"));this.setModificationDate(t.get("M"));this.setFlags(t.get("F"));this.setRectangle(t.getArray("Rect"));this.setColor(t.getArray("C"));this.setBorderStyle(t);this.setAppearance(t);this.setOptionalContent(t);const o=t.get("MK");this.setBorderAndBackgroundColors(o);this.setRotation(o,t);this.ref=e.ref instanceof Ref?e.ref:null;this._streams=[];this.appearance&&this._streams.push(this.appearance);const c=!!(this.flags&ee),l=!!(this.flags&te);this.data={annotationFlags:this.flags,borderStyle:this.borderStyle,color:this.color,backgroundColor:this.backgroundColor,borderColor:this.borderColor,rotation:this.rotation,contentsObj:this._contents,hasAppearance:!!this.appearance,id:e.id,modificationDate:this.modificationDate,rect:this.rectangle,subtype:e.subtype,hasOwnCanvas:!1,noRotate:!!(this.flags&Z),noHTML:c&&l,isEditable:!1,structParent:-1};if(r.structTreeRoot){let a=t.get("StructParent");this.data.structParent=a=Number.isInteger(a)&&a>=0?a:-1;r.structTreeRoot.addAnnotationIdToPage(e.pageRef,a)}if(e.collectFields){const r=t.get("Kids");if(Array.isArray(r)){const e=[];for(const t of r)t instanceof Ref&&e.push(t.toString());0!==e.length&&(this.data.kidIds=e)}this.data.actions=collectActions(a,t,ye);this.data.fieldName=this._constructFieldName(t);this.data.pageIndex=e.pageIndex}const h=t.get("IT");h instanceof Name&&(this.data.it=h.name);this._isOffscreenCanvasSupported=e.evaluatorOptions.isOffscreenCanvasSupported;this._fallbackFontDict=null;this._needAppearances=!1}_hasFlag(e,t){return!!(e&t)}_buildFlags(e,t){let{flags:a}=this;if(void 0===e){if(void 0===t)return;return t?a&~Y:a&~J|Y}if(e){a|=Y;return t?a&~Q|J:a&~J|Q}a&=~(J|Q);return t?a&~Y:a|Y}_isViewable(e){return!this._hasFlag(e,K)&&!this._hasFlag(e,Q)}_isPrintable(e){return this._hasFlag(e,Y)&&!this._hasFlag(e,J)&&!this._hasFlag(e,K)}mustBeViewed(e,t){const a=e?.get(this.data.id)?.noView;return void 0!==a?!a:this.viewable&&!this._hasFlag(this.flags,J)}mustBePrinted(e){const t=e?.get(this.data.id)?.noPrint;return void 0!==t?!t:this.printable}mustBeViewedWhenEditing(e,t=null){return e?!this.data.isEditable:!t?.has(this.data.id)}get viewable(){return null!==this.data.quadPoints&&(0===this.flags||this._isViewable(this.flags))}get printable(){return null!==this.data.quadPoints&&(0!==this.flags&&this._isPrintable(this.flags))}_parseStringHelper(e){const t="string"==typeof e?stringToPDFString(e):"";return{str:t,dir:t&&"rtl"===bidi(t).dir?"rtl":"ltr"}}setDefaultAppearance(e){const{dict:t,annotationGlobals:a}=e,r=getInheritableProperty({dict:t,key:"DA"})||a.acroForm.get("DA");this._defaultAppearance="string"==typeof r?r:"";this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance)}setTitle(e){this._title=this._parseStringHelper(e)}setContents(e){this._contents=this._parseStringHelper(e)}setModificationDate(e){this.modificationDate="string"==typeof e?e:null}setFlags(e){this.flags=Number.isInteger(e)&&e>0?e:0;this.flags&K&&"Annotation"!==this.constructor.name&&(this.flags^=K)}hasFlag(e){return this._hasFlag(this.flags,e)}setRectangle(e){this.rectangle=lookupNormalRect(e,[0,0,0,0])}setColor(e){this.color=getRgbColor(e)}setLineEndings(e){this.lineEndings=["None","None"];if(Array.isArray(e)&&2===e.length)for(let t=0;t<2;t++){const a=e[t];if(a instanceof Name)switch(a.name){case"None":continue;case"Square":case"Circle":case"Diamond":case"OpenArrow":case"ClosedArrow":case"Butt":case"ROpenArrow":case"RClosedArrow":case"Slash":this.lineEndings[t]=a.name;continue}warn(`Ignoring invalid lineEnding: ${a}`)}}setRotation(e,t){this.rotation=0;let a=e instanceof Dict?e.get("R")||0:t.get("Rotate")||0;if(Number.isInteger(a)&&0!==a){a%=360;a<0&&(a+=360);a%90==0&&(this.rotation=a)}}setBorderAndBackgroundColors(e){if(e instanceof Dict){this.borderColor=getRgbColor(e.getArray("BC"),null);this.backgroundColor=getRgbColor(e.getArray("BG"),null)}else this.borderColor=this.backgroundColor=null}setBorderStyle(e){this.borderStyle=new AnnotationBorderStyle;if(e instanceof Dict)if(e.has("BS")){const t=e.get("BS");if(t instanceof Dict){const e=t.get("Type");if(!e||isName(e,"Border")){this.borderStyle.setWidth(t.get("W"),this.rectangle);this.borderStyle.setStyle(t.get("S"));this.borderStyle.setDashArray(t.getArray("D"))}}}else if(e.has("Border")){const t=e.getArray("Border");if(Array.isArray(t)&&t.length>=3){this.borderStyle.setHorizontalCornerRadius(t[0]);this.borderStyle.setVerticalCornerRadius(t[1]);this.borderStyle.setWidth(t[2],this.rectangle);4===t.length&&this.borderStyle.setDashArray(t[3],!0)}}else this.borderStyle.setWidth(0)}setAppearance(e){this.appearance=null;const t=e.get("AP");if(!(t instanceof Dict))return;const a=t.get("N");if(a instanceof BaseStream){this.appearance=a;return}if(!(a instanceof Dict))return;const r=e.get("AS");if(!(r instanceof Name&&a.has(r.name)))return;const i=a.get(r.name);i instanceof BaseStream&&(this.appearance=i)}setOptionalContent(e){this.oc=null;const t=e.get("OC");t instanceof Name?warn("setOptionalContent: Support for /Name-entry is not implemented."):t instanceof Dict&&(this.oc=t)}async loadResources(e,t){const a=await t.dict.getAsync("Resources");a&&await ObjectLoader.load(a,e,a.xref);return a}async getOperatorList(e,t,a,r){const{hasOwnCanvas:i,id:n,rect:o}=this.data;let c=this.appearance;const l=!!(i&&a&s);if(l&&(0===this.width||0===this.height)){this.data.hasOwnCanvas=!1;return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}if(!c){if(!l)return{opList:new OperatorList,separateForm:!1,separateCanvas:!1};c=new StringStream("");c.dict=new Dict}const h=c.dict,u=await this.loadResources(Ia,c),d=lookupRect(h.getArray("BBox"),[0,0,1,1]),f=lookupMatrix(h.getArray("Matrix"),Fa),g=getTransformMatrix(o,d,f),p=new OperatorList;let m;this.oc&&(m=await e.parseMarkedContentProps(this.oc,null));void 0!==m&&p.addOp(jt,["OC",m]);p.addOp($t,[n,o,g,f,l]);await e.getOperatorList({stream:c,task:t,resources:u,operatorList:p,fallbackFontDict:this._fallbackFontDict});p.addOp(Gt,[]);void 0!==m&&p.addOp(_t,[]);this.reset();return{opList:p,separateForm:!1,separateCanvas:l}}async save(e,t,a,r){return null}get hasTextContent(){return!1}async extractTextContent(e,t,a){if(!this.appearance)return;const r=await this.loadResources(Oa,this.appearance),i=[],n=[];let s=null;const o={desiredSize:Math.Infinity,ready:!0,enqueue(e,t){for(const t of e.items)if(void 0!==t.str){s||=t.transform.slice(-2);n.push(t.str);if(t.hasEOL){i.push(n.join("").trimEnd());n.length=0}}}};await e.getTextContent({stream:this.appearance,task:t,resources:r,includeMarkedContent:!0,keepWhiteSpace:!0,sink:o,viewBox:a});this.reset();n.length&&i.push(n.join("").trimEnd());if(i.length>1||i[0]){const e=this.appearance.dict,t=lookupRect(e.getArray("BBox"),null),a=lookupMatrix(e.getArray("Matrix"),null);this.data.textPosition=this._transformPoint(s,t,a);this.data.textContent=i}}_transformPoint(e,t,a){const{rect:r}=this.data;t||=[0,0,1,1];a||=[1,0,0,1,0,0];const i=getTransformMatrix(r,t,a);i[4]-=r[0];i[5]-=r[1];const n=e.slice();Util.applyTransform(n,i);Util.applyTransform(n,a);return n}getFieldObject(){return this.data.kidIds?{id:this.data.id,actions:this.data.actions,name:this.data.fieldName,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,type:"",kidIds:this.data.kidIds,page:this.data.pageIndex,rotation:this.rotation}:null}reset(){for(const e of this._streams)e.reset()}_constructFieldName(e){if(!e.has("T")&&!e.has("Parent")){warn("Unknown field name, falling back to empty field name.");return""}if(!e.has("Parent"))return stringToPDFString(e.get("T"));const t=[];e.has("T")&&t.unshift(stringToPDFString(e.get("T")));let a=e;const r=new RefSet;e.objId&&r.put(e.objId);for(;a.has("Parent");){a=a.get("Parent");if(!(a instanceof Dict)||a.objId&&r.has(a.objId))break;a.objId&&r.put(a.objId);a.has("T")&&t.unshift(stringToPDFString(a.get("T")))}return t.join(".")}get width(){return this.data.rect[2]-this.data.rect[0]}get height(){return this.data.rect[3]-this.data.rect[1]}}class AnnotationBorderStyle{constructor(){this.width=1;this.rawWidth=1;this.style=fe;this.dashArray=[3];this.horizontalCornerRadius=0;this.verticalCornerRadius=0}setWidth(e,t=[0,0,0,0]){if(e instanceof Name)this.width=0;else if("number"==typeof e){if(e>0){this.rawWidth=e;const a=(t[2]-t[0])/2,r=(t[3]-t[1])/2;if(a>0&&r>0&&(e>a||e>r)){warn(`AnnotationBorderStyle.setWidth - ignoring width: ${e}`);e=1}}this.width=e}}setStyle(e){if(e instanceof Name)switch(e.name){case"S":this.style=fe;break;case"D":this.style=ge;break;case"B":this.style=pe;break;case"I":this.style=me;break;case"U":this.style=be}}setDashArray(e,t=!1){if(Array.isArray(e)){let a=!0,r=!0;for(const t of e){if(!(+t>=0)){a=!1;break}t>0&&(r=!1)}if(0===e.length||a&&!r){this.dashArray=e;t&&this.setStyle(Name.get("D"))}else this.width=0}else e&&(this.width=0)}setHorizontalCornerRadius(e){Number.isInteger(e)&&(this.horizontalCornerRadius=e)}setVerticalCornerRadius(e){Number.isInteger(e)&&(this.verticalCornerRadius=e)}}class MarkupAnnotation extends Annotation{constructor(e){super(e);const{dict:t}=e;if(t.has("IRT")){const e=t.getRaw("IRT");this.data.inReplyTo=e instanceof Ref?e.toString():null;const a=t.get("RT");this.data.replyType=a instanceof Name?a.name:V}let a=null;if(this.data.replyType===G){const e=t.get("IRT");this.setTitle(e.get("T"));this.data.titleObj=this._title;this.setContents(e.get("Contents"));this.data.contentsObj=this._contents;if(e.has("CreationDate")){this.setCreationDate(e.get("CreationDate"));this.data.creationDate=this.creationDate}else this.data.creationDate=null;if(e.has("M")){this.setModificationDate(e.get("M"));this.data.modificationDate=this.modificationDate}else this.data.modificationDate=null;a=e.getRaw("Popup");if(e.has("C")){this.setColor(e.getArray("C"));this.data.color=this.color}else this.data.color=null}else{this.data.titleObj=this._title;this.setCreationDate(t.get("CreationDate"));this.data.creationDate=this.creationDate;a=t.getRaw("Popup");t.has("C")||(this.data.color=null)}this.data.popupRef=a instanceof Ref?a.toString():null;t.has("RC")&&(this.data.richText=XFAFactory.getRichTextAsHtml(t.get("RC")))}setCreationDate(e){this.creationDate="string"==typeof e?e:null}_setDefaultAppearance({xref:e,extra:t,strokeColor:a,fillColor:r,blendMode:i,strokeAlpha:n,fillAlpha:s,pointsCallback:o}){const c=this.data.rect=[1/0,1/0,-1/0,-1/0],l=["q"];t&&l.push(t);a&&l.push(`${a[0]} ${a[1]} ${a[2]} RG`);r&&l.push(`${r[0]} ${r[1]} ${r[2]} rg`);const h=this.data.quadPoints||Float32Array.from([this.rectangle[0],this.rectangle[3],this.rectangle[2],this.rectangle[3],this.rectangle[0],this.rectangle[1],this.rectangle[2],this.rectangle[1]]);for(let e=0,t=h.length;e"string"==typeof e)).map((e=>stringToPDFString(e))):e instanceof Name?stringToPDFString(e.name):"string"==typeof e?stringToPDFString(e):null}hasFieldFlag(e){return!!(this.data.fieldFlags&e)}_isViewable(e){return!0}mustBeViewed(e,t){return t?this.viewable:super.mustBeViewed(e,t)&&!this._hasFlag(this.flags,Q)}getRotationMatrix(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);return 0===t?Fa:getRotationMatrix(t,this.width,this.height)}getBorderAndBackgroundAppearances(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);if(!this.backgroundColor&&!this.borderColor)return"";const a=0===t||180===t?`0 0 ${this.width} ${this.height} re`:`0 0 ${this.height} ${this.width} re`;let r="";this.backgroundColor&&(r=`${getPdfColor(this.backgroundColor,!0)} ${a} f `);if(this.borderColor){r+=`${this.borderStyle.width||1} w ${getPdfColor(this.borderColor,!1)} ${a} S `}return r}async getOperatorList(e,t,a,r){if(a&l&&!(this instanceof SignatureWidgetAnnotation)&&!this.data.noHTML&&!this.data.hasOwnCanvas)return{opList:new OperatorList,separateForm:!0,separateCanvas:!1};if(!this._hasText)return super.getOperatorList(e,t,a,r);const i=await this._getAppearance(e,t,a,r);if(this.appearance&&null===i)return super.getOperatorList(e,t,a,r);const n=new OperatorList;if(!this._defaultAppearance||null===i)return{opList:n,separateForm:!1,separateCanvas:!1};const o=!!(this.data.hasOwnCanvas&&a&s),c=[0,0,this.width,this.height],h=getTransformMatrix(this.data.rect,c,[1,0,0,1,0,0]);let u;this.oc&&(u=await e.parseMarkedContentProps(this.oc,null));void 0!==u&&n.addOp(jt,["OC",u]);n.addOp($t,[this.data.id,this.data.rect,h,this.getRotationMatrix(r),o]);const d=new StringStream(i);await e.getOperatorList({stream:d,task:t,resources:this._fieldResources.mergedResources,operatorList:n});n.addOp(Gt,[]);void 0!==u&&n.addOp(_t,[]);return{opList:n,separateForm:!1,separateCanvas:o}}_getMKDict(e){const t=new Dict(null);e&&t.set("R",e);this.borderColor&&t.set("BC",getPdfColorArray(this.borderColor));this.backgroundColor&&t.set("BG",getPdfColorArray(this.backgroundColor));return t.size>0?t:null}amendSavedDict(e,t){}setValue(e,t,a,r){const{dict:i,ref:n}=function getParentToUpdate(e,t,a){const r=new RefSet,i=e,n={dict:null,ref:null};for(;e instanceof Dict&&!r.has(t);){r.put(t);if(e.has("T"))break;if(!((t=e.getRaw("Parent"))instanceof Ref))return n;e=a.fetch(t)}if(e instanceof Dict&&e!==i){n.dict=e;n.ref=t}return n}(e,this.ref,a);if(i){if(!r.has(n)){const e=i.clone();e.set("V",t);r.put(n,{data:e});return e}}else e.set("V",t);return null}async save(e,t,a,r){const i=a?.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.value,o=i?.rotation;if(s===this.data.fieldValue||void 0===s){if(!this._hasValueFromXFA&&void 0===o&&void 0===n)return;s||=this.data.fieldValue}if(void 0===o&&!this._hasValueFromXFA&&Array.isArray(s)&&Array.isArray(this.data.fieldValue)&&isArrayEqual(s,this.data.fieldValue)&&void 0===n)return;void 0===o&&(o=this.rotation);let l=null;if(!this._needAppearances){l=await this._getAppearance(e,t,c,a);if(null===l&&void 0===n)return}let h=!1;if(l?.needAppearances){h=!0;l=null}const{xref:u}=e,d=u.fetchIfRef(this.ref);if(!(d instanceof Dict))return;const f=new Dict(u);for(const e of d.getKeys())"AP"!==e&&f.set(e,d.getRaw(e));if(void 0!==n){f.set("F",n);if(null===l&&!h){const e=d.getRaw("AP");e&&f.set("AP",e)}}const g={path:this.data.fieldName,value:s},p=this.setValue(f,Array.isArray(s)?s.map(stringToAsciiOrUTF16BE):stringToAsciiOrUTF16BE(s),u,r);this.amendSavedDict(a,p||f);const m=this._getMKDict(o);m&&f.set("MK",m);r.put(this.ref,{data:f,xfa:g,needAppearances:h});if(null!==l){const e=u.getNewTemporaryRef(),t=new Dict(u);f.set("AP",t);t.set("N",e);const i=this._getSaveFieldResources(u),n=new StringStream(l),s=n.dict=new Dict(u);s.set("Subtype",Name.get("Form"));s.set("Resources",i);const c=o%180==0?[0,0,this.width,this.height]:[0,0,this.height,this.width];s.set("BBox",c);const h=this.getRotationMatrix(a);h!==Fa&&s.set("Matrix",h);r.put(e,{data:n,xfa:null,needAppearances:!1})}f.set("M",`D:${getModificationDate()}`)}async _getAppearance(e,t,a,r){if(this.data.password)return null;const n=r?.get(this.data.id);let s,o;if(n){s=n.formattedValue||n.value;o=n.rotation}if(void 0===o&&void 0===s&&!this._needAppearances&&(!this._hasValueFromXFA||this.appearance))return null;const l=this.getBorderAndBackgroundAppearances(r);if(void 0===s){s=this.data.fieldValue;if(!s)return`/Tx BMC q ${l}Q EMC`}Array.isArray(s)&&1===s.length&&(s=s[0]);assert("string"==typeof s,"Expected `value` to be a string.");s=s.trimEnd();if(this.data.combo){const e=this.data.options.find((({exportValue:e})=>s===e));s=e?.displayValue||s}if(""===s)return`/Tx BMC q ${l}Q EMC`;void 0===o&&(o=this.rotation);let h,u=-1;if(this.data.multiLine){h=s.split(/\r\n?|\n/).map((e=>e.normalize("NFC")));u=h.length}else h=[s.replace(/\r\n?|\n/,"").normalize("NFC")];let{width:d,height:f}=this;90!==o&&270!==o||([d,f]=[f,d]);this._defaultAppearance||(this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance="/Helvetica 0 Tf 0 g"));let g,p,m,b=await WidgetAnnotation._getFontData(e,t,this.data.defaultAppearanceData,this._fieldResources.mergedResources);const y=[];let w=!1;for(const e of h){const t=b.encodeString(e);t.length>1&&(w=!0);y.push(t.join(""))}if(w&&a&c)return{needAppearances:!0};if(w&&this._isOffscreenCanvasSupported){const a=this.data.comb?"monospace":"sans-serif",r=new FakeUnicodeFont(e.xref,a),i=r.createFontResources(h.join("")),n=i.getRaw("Font");if(this._fieldResources.mergedResources.has("Font")){const e=this._fieldResources.mergedResources.get("Font");for(const t of n.getKeys())e.set(t,n.getRaw(t))}else this._fieldResources.mergedResources.set("Font",n);const o=r.fontName.name;b=await WidgetAnnotation._getFontData(e,t,{fontName:o,fontSize:0},i);for(let e=0,t=y.length;e2)return`/Tx BMC q ${l}BT `+g+` 1 0 0 1 ${numberToString(2)} ${numberToString(C)} Tm (${escapeString(y[0])}) Tj ET Q EMC`;return`/Tx BMC q ${l}BT `+g+` 1 0 0 1 0 0 Tm ${this._renderText(y[0],b,p,d,k,{shift:0},2,C)} ET Q EMC`}static async _getFontData(e,t,a,r){const i=new OperatorList,n={font:null,clone(){return this}},{fontName:s,fontSize:o}=a;await e.handleSetFont(r,[s&&Name.get(s),o],null,i,t,n,null);return n.font}_getTextWidth(e,t){return Math.sumPrecise(t.charsToGlyphs(e).map((e=>e.width)))/1e3}_computeFontSize(e,t,r,i,n){let{fontSize:s}=this.data.defaultAppearanceData,o=(s||12)*a,c=Math.round(e/o);if(!s){const roundWithTwoDigits=e=>Math.floor(100*e)/100;if(-1===n){const n=this._getTextWidth(r,i);s=roundWithTwoDigits(Math.min(e/a,t/n));c=1}else{const l=r.split(/\r\n?|\n/),h=[];for(const e of l){const t=i.encodeString(e).join(""),a=i.charsToGlyphs(t),r=i.getCharPositions(t);h.push({line:t,glyphs:a,positions:r})}const isTooBig=a=>{let r=0;for(const n of h){r+=this._splitLine(null,i,a,t,n).length*a;if(r>e)return!0}return!1};c=Math.max(c,n);for(;;){o=e/c;s=roundWithTwoDigits(o/a);if(!isTooBig(s))break;c++}}const{fontName:l,fontColor:h}=this.data.defaultAppearanceData;this._defaultAppearance=function createDefaultAppearance({fontSize:e,fontName:t,fontColor:a}){return`/${escapePDFName(t)} ${e} Tf ${getPdfColor(a,!0)}`}({fontSize:s,fontName:l,fontColor:h})}return[this._defaultAppearance,s,e/c]}_renderText(e,t,a,r,i,n,s,o){let c;if(1===i){c=(r-this._getTextWidth(e,t)*a)/2}else if(2===i){c=r-this._getTextWidth(e,t)*a-s}else c=s;const l=numberToString(c-n.shift);n.shift=c;return`${l} ${o=numberToString(o)} Td (${escapeString(e)}) Tj`}_getSaveFieldResources(e){const{localResources:t,appearanceResources:a,acroFormResources:r}=this._fieldResources,i=this.data.defaultAppearanceData?.fontName;if(!i)return t||Dict.empty;for(const e of[t,a])if(e instanceof Dict){const t=e.get("Font");if(t instanceof Dict&&t.has(i))return e}if(r instanceof Dict){const a=r.get("Font");if(a instanceof Dict&&a.has(i)){const r=new Dict(e);r.set(i,a.getRaw(i));const n=new Dict(e);n.set("Font",r);return Dict.merge({xref:e,dictArray:[n,t],mergeSubDicts:!0})}}return t||Dict.empty}getFieldObject(){return null}}class TextWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t}=e;if(t.has("PMD")){this.flags|=J;this.data.hidden=!0;warn("Barcodes are not supported")}this.data.hasOwnCanvas=this.data.readOnly&&!this.data.noHTML;this._hasText=!0;"string"!=typeof this.data.fieldValue&&(this.data.fieldValue="");let a=getInheritableProperty({dict:t,key:"Q"});(!Number.isInteger(a)||a<0||a>2)&&(a=null);this.data.textAlignment=a;let r=getInheritableProperty({dict:t,key:"MaxLen"});(!Number.isInteger(r)||r<0)&&(r=0);this.data.maxLen=r;this.data.multiLine=this.hasFieldFlag(ie);this.data.comb=this.hasFieldFlag(de)&&!this.data.multiLine&&!this.data.password&&!this.hasFieldFlag(le)&&0!==this.data.maxLen;this.data.doNotScroll=this.hasFieldFlag(ue)}get hasTextContent(){return!!this.appearance&&!this._needAppearances}_getCombAppearance(e,t,a,r,i,n,s,o,c,l,h){const u=i/this.data.maxLen,d=this.getBorderAndBackgroundAppearances(h),f=[],g=t.getCharPositions(a);for(const[e,t]of g)f.push(`(${escapeString(a.substring(e,t))}) Tj`);const p=f.join(` ${numberToString(u)} 0 Td `);return`/Tx BMC q ${d}BT `+e+` 1 0 0 1 ${numberToString(s)} ${numberToString(o+c)} Tm ${p} ET Q EMC`}_getMultilineAppearance(e,t,a,r,i,n,s,o,c,l,h,u){const d=[],f=i-2*o,g={shift:0};for(let e=0,n=t.length;er){c.push(e.substring(d,a));d=a;f=p;l=-1;u=-1}else{f+=p;l=a;h=i;u=t}else if(f+p>r)if(-1!==l){c.push(e.substring(d,h));d=h;t=u+1;l=-1;f=0}else{c.push(e.substring(d,a));d=a;f=p}else f+=p}dt?`\\${t}`:"\\s+"));new RegExp(`^\\s*${n}\\s*$`).test(this.data.fieldValue)&&(this.data.textContent=this.data.fieldValue.split("\n"))}getFieldObject(){return{id:this.data.id,value:this.data.fieldValue,defaultValue:this.data.defaultFieldValue||"",multiline:this.data.multiLine,password:this.data.password,charLimit:this.data.maxLen,comb:this.data.comb,editable:!this.data.readOnly,hidden:this.data.hidden,name:this.data.fieldName,rect:this.data.rect,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:"text"}}}class ButtonWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);this.checkedAppearance=null;this.uncheckedAppearance=null;const t=this.hasFieldFlag(se),a=this.hasFieldFlag(oe);this.data.checkBox=!t&&!a;this.data.radioButton=t&&!a;this.data.pushButton=a;this.data.isTooltipOnly=!1;if(this.data.checkBox)this._processCheckBox(e);else if(this.data.radioButton)this._processRadioButton(e);else if(this.data.pushButton){this.data.hasOwnCanvas=!0;this.data.noHTML=!1;this._processPushButton(e)}else warn("Invalid field flags for button widget annotation")}async getOperatorList(e,t,a,r){if(this.data.pushButton)return super.getOperatorList(e,t,a,!1,r);let i=null,n=null;if(r){const e=r.get(this.data.id);i=e?e.value:null;n=e?e.rotation:null}if(null===i&&this.appearance)return super.getOperatorList(e,t,a,r);null==i&&(i=this.data.checkBox?this.data.fieldValue===this.data.exportValue:this.data.fieldValue===this.data.buttonValue);const s=i?this.checkedAppearance:this.uncheckedAppearance;if(s){const i=this.appearance,o=lookupMatrix(s.dict.getArray("Matrix"),Fa);n&&s.dict.set("Matrix",this.getRotationMatrix(r));this.appearance=s;const c=super.getOperatorList(e,t,a,r);this.appearance=i;s.dict.set("Matrix",o);return c}return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}async save(e,t,a,r){this.data.checkBox?this._saveCheckbox(e,t,a,r):this.data.radioButton&&this._saveRadioButton(e,t,a,r)}async _saveCheckbox(e,t,a,r){if(!a)return;const i=a.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.rotation,o=i?.value;if(void 0===s&&void 0===n){if(void 0===o)return;if(this.data.fieldValue===this.data.exportValue===o)return}let c=e.xref.fetchIfRef(this.ref);if(!(c instanceof Dict))return;c=c.clone();void 0===s&&(s=this.rotation);void 0===o&&(o=this.data.fieldValue===this.data.exportValue);const l={path:this.data.fieldName,value:o?this.data.exportValue:""},h=Name.get(o?this.data.exportValue:"Off");this.setValue(c,h,e.xref,r);c.set("AS",h);c.set("M",`D:${getModificationDate()}`);void 0!==n&&c.set("F",n);const u=this._getMKDict(s);u&&c.set("MK",u);r.put(this.ref,{data:c,xfa:l,needAppearances:!1})}async _saveRadioButton(e,t,a,r){if(!a)return;const i=a.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.rotation,o=i?.value;if(void 0===s&&void 0===n){if(void 0===o)return;if(this.data.fieldValue===this.data.buttonValue===o)return}let c=e.xref.fetchIfRef(this.ref);if(!(c instanceof Dict))return;c=c.clone();void 0===o&&(o=this.data.fieldValue===this.data.buttonValue);void 0===s&&(s=this.rotation);const l={path:this.data.fieldName,value:o?this.data.buttonValue:""},h=Name.get(o?this.data.buttonValue:"Off");o&&this.setValue(c,h,e.xref,r);c.set("AS",h);c.set("M",`D:${getModificationDate()}`);void 0!==n&&c.set("F",n);const u=this._getMKDict(s);u&&c.set("MK",u);r.put(this.ref,{data:c,xfa:l,needAppearances:!1})}_getDefaultCheckedAppearance(e,t){const{width:a,height:r}=this,i=[0,0,a,r],n=.8*Math.min(a,r);let s,o;if("check"===t){s={width:.755*n,height:.705*n};o="3"}else if("disc"===t){s={width:.791*n,height:.705*n};o="l"}else unreachable(`_getDefaultCheckedAppearance - unsupported type: ${t}`);const c=`q BT /PdfJsZaDb ${n} Tf 0 g ${numberToString((a-s.width)/2)} ${numberToString((r-s.height)/2)} Td (${o}) Tj ET Q`,l=new Dict(e.xref);l.set("FormType",1);l.set("Subtype",Name.get("Form"));l.set("Type",Name.get("XObject"));l.set("BBox",i);l.set("Matrix",[1,0,0,1,0,0]);l.set("Length",c.length);const h=new Dict(e.xref),u=new Dict(e.xref);u.set("PdfJsZaDb",this.fallbackFontDict);h.set("Font",u);l.set("Resources",h);this.checkedAppearance=new StringStream(c);this.checkedAppearance.dict=l;this._streams.push(this.checkedAppearance)}_processCheckBox(e){const t=e.dict.get("AP");if(!(t instanceof Dict))return;const a=t.get("N");if(!(a instanceof Dict))return;const r=this._decodeFormValue(e.dict.get("AS"));"string"==typeof r&&(this.data.fieldValue=r);const i=null!==this.data.fieldValue&&"Off"!==this.data.fieldValue?this.data.fieldValue:"Yes",n=this._decodeFormValue(a.getKeys());if(0===n.length)n.push("Off",i);else if(1===n.length)"Off"===n[0]?n.push(i):n.unshift("Off");else if(n.includes(i)){n.length=0;n.push("Off",i)}else{const e=n.find((e=>"Off"!==e));n.length=0;n.push("Off",e)}n.includes(this.data.fieldValue)||(this.data.fieldValue="Off");this.data.exportValue=n[1];const s=a.get(this.data.exportValue);this.checkedAppearance=s instanceof BaseStream?s:null;const o=a.get("Off");this.uncheckedAppearance=o instanceof BaseStream?o:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"check");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processRadioButton(e){this.data.buttonValue=null;const t=e.dict.get("Parent");if(t instanceof Dict){this.parent=e.dict.getRaw("Parent");const a=t.get("V");a instanceof Name&&(this.data.fieldValue=this._decodeFormValue(a))}const a=e.dict.get("AP");if(!(a instanceof Dict))return;const r=a.get("N");if(!(r instanceof Dict))return;for(const e of r.getKeys())if("Off"!==e){this.data.buttonValue=this._decodeFormValue(e);break}const i=r.get(this.data.buttonValue);this.checkedAppearance=i instanceof BaseStream?i:null;const n=r.get("Off");this.uncheckedAppearance=n instanceof BaseStream?n:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"disc");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processPushButton(e){const{dict:t,annotationGlobals:a}=e;if(t.has("A")||t.has("AA")||this.data.alternativeText){this.data.isTooltipOnly=!t.has("A")&&!t.has("AA");Catalog.parseDestDictionary({destDict:t,resultObj:this.data,docBaseUrl:a.baseUrl,docAttachments:a.attachments})}else warn("Push buttons without action dictionaries are not supported")}getFieldObject(){let e,t="button";if(this.data.checkBox){t="checkbox";e=this.data.exportValue}else if(this.data.radioButton){t="radiobutton";e=this.data.buttonValue}return{id:this.data.id,value:this.data.fieldValue||"Off",defaultValue:this.data.defaultFieldValue,exportValues:e,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,hidden:this.data.hidden,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:t}}get fallbackFontDict(){const e=new Dict;e.set("BaseFont",Name.get("ZapfDingbats"));e.set("Type",Name.get("FallbackType"));e.set("Subtype",Name.get("FallbackType"));e.set("Encoding",Name.get("ZapfDingbatsEncoding"));return shadow(this,"fallbackFontDict",e)}}class ChoiceWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.indices=t.getArray("I");this.hasIndices=Array.isArray(this.indices)&&this.indices.length>0;this.data.options=[];const r=getInheritableProperty({dict:t,key:"Opt"});if(Array.isArray(r))for(let e=0,t=r.length;e=0&&t0&&(this.data.options=this.data.fieldValue.map((e=>({exportValue:e,displayValue:e}))));this.data.combo=this.hasFieldFlag(ce);this.data.multiSelect=this.hasFieldFlag(he);this._hasText=!0}getFieldObject(){const e=this.data.combo?"combobox":"listbox",t=this.data.fieldValue.length>0?this.data.fieldValue[0]:null;return{id:this.data.id,value:t,defaultValue:this.data.defaultFieldValue,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,numItems:this.data.fieldValue.length,multipleSelection:this.data.multiSelect,hidden:this.data.hidden,actions:this.data.actions,items:this.data.options,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:e}}amendSavedDict(e,t){if(!this.hasIndices)return;let a=e?.get(this.data.id)?.value;Array.isArray(a)||(a=[a]);const r=[],{options:i}=this.data;for(let e=0,t=0,n=i.length;ea){a=r;t=e}}[f,g]=this._computeFontSize(e,c-4,t,d,-1)}const p=g*a,m=(p-g)/2,b=Math.floor(l/p);let y=0;if(u.length>0){const e=Math.min(...u),t=Math.max(...u);y=Math.max(0,t-b+1);y>e&&(y=e)}const w=Math.min(y+b+1,h),S=["/Tx BMC q",`1 1 ${c} ${l} re W n`];if(u.length){S.push("0.600006 0.756866 0.854904 rg");for(const e of u)y<=e&&ee.trimEnd()));const{coords:e,bbox:t,matrix:r}=FakeUnicodeFont.getFirstPositionInfo(this.rectangle,this.rotation,a);this.data.textPosition=this._transformPoint(e,t,r)}if(this._isOffscreenCanvasSupported){const i=e.dict.get("CA"),n=new FakeUnicodeFont(r,"sans-serif");this.appearance=n.createAppearance(this._contents.str,this.rectangle,this.rotation,a,t,i);this._streams.push(this.appearance)}else warn("FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly.")}}get hasTextContent(){return this._hasAppearance}static createNewDict(e,t,{apRef:a,ap:r}){const{color:i,fontSize:n,oldAnnotation:s,rect:o,rotation:c,user:l,value:h}=e,u=s||new Dict(t);u.set("Type",Name.get("Annot"));u.set("Subtype",Name.get("FreeText"));if(s){u.set("M",`D:${getModificationDate()}`);u.delete("RC")}else u.set("CreationDate",`D:${getModificationDate()}`);u.set("Rect",o);const d=`/Helv ${n} Tf ${getPdfColor(i,!0)}`;u.set("DA",d);u.set("Contents",stringToAsciiOrUTF16BE(h));u.set("F",4);u.set("Border",[0,0,0]);u.set("Rotate",c);l&&u.set("T",stringToAsciiOrUTF16BE(l));if(a||r){const e=new Dict(t);u.set("AP",e);a?e.set("N",a):e.set("N",r)}return u}static async createNewAppearanceStream(e,t,r){const{baseFontRef:i,evaluator:n,task:s}=r,{color:o,fontSize:c,rect:l,rotation:h,value:u}=e,d=new Dict(t),f=new Dict(t);if(i)f.set("Helv",i);else{const e=new Dict(t);e.set("BaseFont",Name.get("Helvetica"));e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type1"));e.set("Encoding",Name.get("WinAnsiEncoding"));f.set("Helv",e)}d.set("Font",f);const g=await WidgetAnnotation._getFontData(n,s,{fontName:"Helv",fontSize:c},d),[p,m,b,y]=l;let w=b-p,S=y-m;h%180!=0&&([w,S]=[S,w]);const x=u.split("\n"),k=c/1e3;let C=-1/0;const v=[];for(let e of x){const t=g.encodeString(e);if(t.length>1)return null;e=t.join("");v.push(e);let a=0;const r=g.charsToGlyphs(e);for(const e of r)a+=e.width*k;C=Math.max(C,a)}let F=1;C>w&&(F=w/C);let O=1;const T=a*c,M=1*c,D=T*x.length;D>S&&(O=S/D);const R=c*Math.min(F,O);let N,E,L;switch(h){case 0:L=[1,0,0,1];E=[l[0],l[1],w,S];N=[l[0],l[3]-M];break;case 90:L=[0,1,-1,0];E=[l[1],-l[2],w,S];N=[l[1],-l[0]-M];break;case 180:L=[-1,0,0,-1];E=[-l[2],-l[3],w,S];N=[-l[2],-l[1]-M];break;case 270:L=[0,-1,1,0];E=[-l[3],l[0],w,S];N=[-l[3],l[2]-M]}const j=["q",`${L.join(" ")} 0 0 cm`,`${E.join(" ")} re W n`,"BT",`${getPdfColor(o,!0)}`,`0 Tc /Helv ${numberToString(R)} Tf`];j.push(`${N.join(" ")} Td (${escapeString(v[0])}) Tj`);const _=numberToString(T);for(let e=1,t=v.length;e{e.push(`${r[0]} ${r[1]} m`,`${r[2]} ${r[3]} l`,"S");return[t[0]-c,t[7]-c,t[2]+c,t[3]+c]}})}}}class SquareAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=D;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),i=getRgbColor(t.getArray("IC"),null),n=i?getPdfColorArray(i):null,s=n?r:null;if(0===this.borderStyle.width&&!n)return;this._setDefaultAppearance({xref:a,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:n,strokeAlpha:r,fillAlpha:s,pointsCallback:(e,t)=>{const a=t[4]+this.borderStyle.width/2,r=t[5]+this.borderStyle.width/2,i=t[6]-t[4]-this.borderStyle.width,s=t[3]-t[7]-this.borderStyle.width;e.push(`${a} ${r} ${i} ${s} re`);n?e.push("B"):e.push("S");return[t[0],t[7],t[2],t[3]]}})}}}class CircleAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=R;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),i=getRgbColor(t.getArray("IC"),null),n=i?getPdfColorArray(i):null,s=n?r:null;if(0===this.borderStyle.width&&!n)return;const o=4/3*Math.tan(Math.PI/8);this._setDefaultAppearance({xref:a,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:n,strokeAlpha:r,fillAlpha:s,pointsCallback:(e,t)=>{const a=t[0]+this.borderStyle.width/2,r=t[1]-this.borderStyle.width/2,i=t[6]-this.borderStyle.width/2,s=t[7]+this.borderStyle.width/2,c=a+(i-a)/2,l=r+(s-r)/2,h=(i-a)/2*o,u=(s-r)/2*o;e.push(`${c} ${s} m`,`${c+h} ${s} ${i} ${l+u} ${i} ${l} c`,`${i} ${l-u} ${c+h} ${r} ${c} ${r} c`,`${c-h} ${r} ${a} ${l-u} ${a} ${l} c`,`${a} ${l+u} ${c-h} ${s} ${c} ${s} c`,"h");n?e.push("B"):e.push("S");return[t[0],t[7],t[2],t[3]]}})}}}class PolylineAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=E;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;this.data.vertices=null;if(!(this instanceof PolygonAnnotation)){this.setLineEndings(t.getArray("LE"));this.data.lineEndings=this.lineEndings}const r=t.getArray("Vertices");if(!isNumberArray(r,null))return;const i=this.data.vertices=Float32Array.from(r);if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),n=this.borderStyle.width||1,s=2*n,o=[1/0,1/0,-1/0,-1/0];for(let e=0,t=i.length;e{for(let t=0,a=i.length;t{for(const t of this.data.inkLists){for(let a=0,r=t.length;a{e.push(`${t[0]} ${t[1]} m`,`${t[2]} ${t[3]} l`,`${t[6]} ${t[7]} l`,`${t[4]} ${t[5]} l`,"f");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}static createNewDict(e,t,{apRef:a,ap:r}){const{color:i,oldAnnotation:n,opacity:s,rect:o,rotation:c,user:l,quadPoints:h}=e,u=n||new Dict(t);u.set("Type",Name.get("Annot"));u.set("Subtype",Name.get("Highlight"));u.set(n?"M":"CreationDate",`D:${getModificationDate()}`);u.set("CreationDate",`D:${getModificationDate()}`);u.set("Rect",o);u.set("F",4);u.set("Border",[0,0,0]);u.set("Rotate",c);u.set("QuadPoints",h);u.set("C",getPdfColorArray(i));u.set("CA",s);l&&u.set("T",stringToAsciiOrUTF16BE(l));if(a||r){const e=new Dict(t);u.set("AP",e);e.set("N",a||r)}return u}static async createNewAppearanceStream(e,t,a){const{color:r,rect:i,outlines:n,opacity:s}=e,o=[`${getPdfColor(r,!0)}`,"/R0 gs"],c=[];for(const e of n){c.length=0;c.push(`${numberToString(e[0])} ${numberToString(e[1])} m`);for(let t=2,a=e.length;t{e.push(`${t[4]} ${t[5]+1.3} m`,`${t[6]} ${t[7]+1.3} l`,"S");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}}class SquigglyAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=_;if(this.data.quadPoints=getQuadPoints(t,null)){if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA");this._setDefaultAppearance({xref:a,extra:"[] 0 d 1 w",strokeColor:e,strokeAlpha:r,pointsCallback:(e,t)=>{const a=(t[1]-t[5])/6;let r=a,i=t[4];const n=t[5],s=t[6];e.push(`${i} ${n+r} m`);do{i+=2;r=0===r?a:0;e.push(`${i} ${n+r} l`)}while(i{e.push((t[0]+t[4])/2+" "+(t[1]+t[5])/2+" m",(t[2]+t[6])/2+" "+(t[3]+t[7])/2+" l","S");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}}class StampAnnotation extends MarkupAnnotation{#pe=null;constructor(e){super(e);this.data.annotationType=X;this.data.hasOwnCanvas=this.data.noRotate;this.data.isEditable=!this.data.noHTML;this.data.noHTML=!1}mustBeViewedWhenEditing(e,t=null){if(e){if(!this.data.isEditable)return!0;this.#pe??=this.data.hasOwnCanvas;this.data.hasOwnCanvas=!0;return!0}if(null!==this.#pe){this.data.hasOwnCanvas=this.#pe;this.#pe=null}return!t?.has(this.data.id)}static async createImage(e,t){const{width:a,height:r}=e,i=new OffscreenCanvas(a,r),n=i.getContext("2d",{alpha:!0});n.drawImage(e,0,0);const s=n.getImageData(0,0,a,r).data,o=new Uint32Array(s.buffer),c=o.some(FeatureTest.isLittleEndian?e=>e>>>24!=255:e=>!!(255&~e));if(c){n.fillStyle="white";n.fillRect(0,0,a,r);n.drawImage(e,0,0)}const l=i.convertToBlob({type:"image/jpeg",quality:1}).then((e=>e.arrayBuffer())),h=Name.get("XObject"),u=Name.get("Image"),d=new Dict(t);d.set("Type",h);d.set("Subtype",u);d.set("BitsPerComponent",8);d.set("ColorSpace",Name.get("DeviceRGB"));d.set("Filter",Name.get("DCTDecode"));d.set("BBox",[0,0,a,r]);d.set("Width",a);d.set("Height",r);let f=null;if(c){const e=new Uint8Array(o.length);if(FeatureTest.isLittleEndian)for(let t=0,a=o.length;t>>24;else for(let t=0,a=o.length;t=0&&n<=1?n:null}}const fc={get r(){return shadow(this,"r",new Uint8Array([7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21]))},get k(){return shadow(this,"k",new Int32Array([-680876936,-389564586,606105819,-1044525330,-176418897,1200080426,-1473231341,-45705983,1770035416,-1958414417,-42063,-1990404162,1804603682,-40341101,-1502002290,1236535329,-165796510,-1069501632,643717713,-373897302,-701558691,38016083,-660478335,-405537848,568446438,-1019803690,-187363961,1163531501,-1444681467,-51403784,1735328473,-1926607734,-378558,-2022574463,1839030562,-35309556,-1530992060,1272893353,-155497632,-1094730640,681279174,-358537222,-722521979,76029189,-640364487,-421815835,530742520,-995338651,-198630844,1126891415,-1416354905,-57434055,1700485571,-1894986606,-1051523,-2054922799,1873313359,-30611744,-1560198380,1309151649,-145523070,-1120210379,718787259,-343485551]))}};function calculateMD5(e,t,a){let r=1732584193,i=-271733879,n=-1732584194,s=271733878;const o=a+72&-64,c=new Uint8Array(o);let l,h;for(l=0;l>5&255;c[l++]=a>>13&255;c[l++]=a>>21&255;c[l++]=a>>>29&255;l+=3;const d=new Int32Array(16),{k:f,r:g}=fc;for(l=0;l>>32-n)|0;a=r}r=r+a|0;i=i+o|0;n=n+u|0;s=s+p|0}return new Uint8Array([255&r,r>>8&255,r>>16&255,r>>>24&255,255&i,i>>8&255,i>>16&255,i>>>24&255,255&n,n>>8&255,n>>16&255,n>>>24&255,255&s,s>>8&255,s>>16&255,s>>>24&255])}function decodeString(e){try{return stringToUTF8String(e)}catch(t){warn(`UTF-8 decoding failed: "${t}".`);return e}}class DatasetXMLParser extends SimpleXMLParser{constructor(e){super(e);this.node=null}onEndElement(e){const t=super.onEndElement(e);if(t&&"xfa:datasets"===e){this.node=t;throw new Error("Aborting DatasetXMLParser.")}}}class DatasetReader{constructor(e){if(e.datasets)this.node=new SimpleXMLParser({hasAttributes:!0}).parseFromString(e.datasets).documentElement;else{const t=new DatasetXMLParser({hasAttributes:!0});try{t.parseFromString(e["xdp:xdp"])}catch{}this.node=t.node}}getValue(e){if(!this.node||!e)return"";const t=this.node.searchNode(parseXFAPath(e),0);if(!t)return"";const a=t.firstChild;return"value"===a?.nodeName?t.children.map((e=>decodeString(e.textContent))):decodeString(t.textContent)}}class Word64{constructor(e,t){this.high=0|e;this.low=0|t}and(e){this.high&=e.high;this.low&=e.low}xor(e){this.high^=e.high;this.low^=e.low}shiftRight(e){if(e>=32){this.low=this.high>>>e-32|0;this.high=0}else{this.low=this.low>>>e|this.high<<32-e;this.high=this.high>>>e|0}}rotateRight(e){let t,a;if(32&e){a=this.low;t=this.high}else{t=this.low;a=this.high}e&=31;this.low=t>>>e|a<<32-e;this.high=a>>>e|t<<32-e}not(){this.high=~this.high;this.low=~this.low}add(e){const t=(this.low>>>0)+(e.low>>>0);let a=(this.high>>>0)+(e.high>>>0);t>4294967295&&(a+=1);this.low=0|t;this.high=0|a}copyTo(e,t){e[t]=this.high>>>24&255;e[t+1]=this.high>>16&255;e[t+2]=this.high>>8&255;e[t+3]=255&this.high;e[t+4]=this.low>>>24&255;e[t+5]=this.low>>16&255;e[t+6]=this.low>>8&255;e[t+7]=255&this.low}assign(e){this.high=e.high;this.low=e.low}}const gc={get k(){return shadow(this,"k",[new Word64(1116352408,3609767458),new Word64(1899447441,602891725),new Word64(3049323471,3964484399),new Word64(3921009573,2173295548),new Word64(961987163,4081628472),new Word64(1508970993,3053834265),new Word64(2453635748,2937671579),new Word64(2870763221,3664609560),new Word64(3624381080,2734883394),new Word64(310598401,1164996542),new Word64(607225278,1323610764),new Word64(1426881987,3590304994),new Word64(1925078388,4068182383),new Word64(2162078206,991336113),new Word64(2614888103,633803317),new Word64(3248222580,3479774868),new Word64(3835390401,2666613458),new Word64(4022224774,944711139),new Word64(264347078,2341262773),new Word64(604807628,2007800933),new Word64(770255983,1495990901),new Word64(1249150122,1856431235),new Word64(1555081692,3175218132),new Word64(1996064986,2198950837),new Word64(2554220882,3999719339),new Word64(2821834349,766784016),new Word64(2952996808,2566594879),new Word64(3210313671,3203337956),new Word64(3336571891,1034457026),new Word64(3584528711,2466948901),new Word64(113926993,3758326383),new Word64(338241895,168717936),new Word64(666307205,1188179964),new Word64(773529912,1546045734),new Word64(1294757372,1522805485),new Word64(1396182291,2643833823),new Word64(1695183700,2343527390),new Word64(1986661051,1014477480),new Word64(2177026350,1206759142),new Word64(2456956037,344077627),new Word64(2730485921,1290863460),new Word64(2820302411,3158454273),new Word64(3259730800,3505952657),new Word64(3345764771,106217008),new Word64(3516065817,3606008344),new Word64(3600352804,1432725776),new Word64(4094571909,1467031594),new Word64(275423344,851169720),new Word64(430227734,3100823752),new Word64(506948616,1363258195),new Word64(659060556,3750685593),new Word64(883997877,3785050280),new Word64(958139571,3318307427),new Word64(1322822218,3812723403),new Word64(1537002063,2003034995),new Word64(1747873779,3602036899),new Word64(1955562222,1575990012),new Word64(2024104815,1125592928),new Word64(2227730452,2716904306),new Word64(2361852424,442776044),new Word64(2428436474,593698344),new Word64(2756734187,3733110249),new Word64(3204031479,2999351573),new Word64(3329325298,3815920427),new Word64(3391569614,3928383900),new Word64(3515267271,566280711),new Word64(3940187606,3454069534),new Word64(4118630271,4000239992),new Word64(116418474,1914138554),new Word64(174292421,2731055270),new Word64(289380356,3203993006),new Word64(460393269,320620315),new Word64(685471733,587496836),new Word64(852142971,1086792851),new Word64(1017036298,365543100),new Word64(1126000580,2618297676),new Word64(1288033470,3409855158),new Word64(1501505948,4234509866),new Word64(1607167915,987167468),new Word64(1816402316,1246189591)])}};function ch(e,t,a,r,i){e.assign(t);e.and(a);i.assign(t);i.not();i.and(r);e.xor(i)}function maj(e,t,a,r,i){e.assign(t);e.and(a);i.assign(t);i.and(r);e.xor(i);i.assign(a);i.and(r);e.xor(i)}function sigma(e,t,a){e.assign(t);e.rotateRight(28);a.assign(t);a.rotateRight(34);e.xor(a);a.assign(t);a.rotateRight(39);e.xor(a)}function sigmaPrime(e,t,a){e.assign(t);e.rotateRight(14);a.assign(t);a.rotateRight(18);e.xor(a);a.assign(t);a.rotateRight(41);e.xor(a)}function littleSigma(e,t,a){e.assign(t);e.rotateRight(1);a.assign(t);a.rotateRight(8);e.xor(a);a.assign(t);a.shiftRight(7);e.xor(a)}function littleSigmaPrime(e,t,a){e.assign(t);e.rotateRight(19);a.assign(t);a.rotateRight(61);e.xor(a);a.assign(t);a.shiftRight(6);e.xor(a)}function calculateSHA512(e,t,a,r=!1){let i,n,s,o,c,l,h,u;if(r){i=new Word64(3418070365,3238371032);n=new Word64(1654270250,914150663);s=new Word64(2438529370,812702999);o=new Word64(355462360,4144912697);c=new Word64(1731405415,4290775857);l=new Word64(2394180231,1750603025);h=new Word64(3675008525,1694076839);u=new Word64(1203062813,3204075428)}else{i=new Word64(1779033703,4089235720);n=new Word64(3144134277,2227873595);s=new Word64(1013904242,4271175723);o=new Word64(2773480762,1595750129);c=new Word64(1359893119,2917565137);l=new Word64(2600822924,725511199);h=new Word64(528734635,4215389547);u=new Word64(1541459225,327033209)}const d=128*Math.ceil((a+17)/128),f=new Uint8Array(d);let g,p;for(g=0;g>>29&255;f[g++]=a>>21&255;f[g++]=a>>13&255;f[g++]=a>>5&255;f[g++]=a<<3&255;const b=new Array(80);for(g=0;g<80;g++)b[g]=new Word64(0,0);const{k:y}=gc;let w=new Word64(0,0),S=new Word64(0,0),x=new Word64(0,0),k=new Word64(0,0),C=new Word64(0,0),v=new Word64(0,0),F=new Word64(0,0),O=new Word64(0,0);const T=new Word64(0,0),M=new Word64(0,0),D=new Word64(0,0),R=new Word64(0,0);let N,E;for(g=0;g>>t|e<<32-t}function calculate_sha256_ch(e,t,a){return e&t^~e&a}function calculate_sha256_maj(e,t,a){return e&t^e&a^t&a}function calculate_sha256_sigma(e){return rotr(e,2)^rotr(e,13)^rotr(e,22)}function calculate_sha256_sigmaPrime(e){return rotr(e,6)^rotr(e,11)^rotr(e,25)}function calculate_sha256_littleSigma(e){return rotr(e,7)^rotr(e,18)^e>>>3}function calculateSHA256(e,t,a){let r=1779033703,i=3144134277,n=1013904242,s=2773480762,o=1359893119,c=2600822924,l=528734635,h=1541459225;const u=64*Math.ceil((a+9)/64),d=new Uint8Array(u);let f,g;for(f=0;f>>29&255;d[f++]=a>>21&255;d[f++]=a>>13&255;d[f++]=a>>5&255;d[f++]=a<<3&255;const m=new Uint32Array(64),{k:b}=pc;for(f=0;f>>10)+m[g-7]+calculate_sha256_littleSigma(m[g-15])+m[g-16]|0;let e,t,a=r,u=i,p=n,w=s,S=o,x=c,k=l,C=h;for(g=0;g<64;++g){e=C+calculate_sha256_sigmaPrime(S)+calculate_sha256_ch(S,x,k)+b[g]+m[g];t=calculate_sha256_sigma(a)+calculate_sha256_maj(a,u,p);C=k;k=x;x=S;S=w+e|0;w=p;p=u;u=a;a=e+t|0}r=r+a|0;i=i+u|0;n=n+p|0;s=s+w|0;o=o+S|0;c=c+x|0;l=l+k|0;h=h+C|0}var y;return new Uint8Array([r>>24&255,r>>16&255,r>>8&255,255&r,i>>24&255,i>>16&255,i>>8&255,255&i,n>>24&255,n>>16&255,n>>8&255,255&n,s>>24&255,s>>16&255,s>>8&255,255&s,o>>24&255,o>>16&255,o>>8&255,255&o,c>>24&255,c>>16&255,c>>8&255,255&c,l>>24&255,l>>16&255,l>>8&255,255&l,h>>24&255,h>>16&255,h>>8&255,255&h])}class DecryptStream extends DecodeStream{constructor(e,t,a){super(t);this.str=e;this.dict=e.dict;this.decrypt=a;this.nextChunk=null;this.initialized=!1}readBlock(){let e;if(this.initialized)e=this.nextChunk;else{e=this.str.getBytes(512);this.initialized=!0}if(!e?.length){this.eof=!0;return}this.nextChunk=this.str.getBytes(512);const t=this.nextChunk?.length>0;e=(0,this.decrypt)(e,!t);const a=this.bufferLength,r=a+e.length;this.ensureBuffer(r).set(e,a);this.bufferLength=r}}class ARCFourCipher{constructor(e){this.a=0;this.b=0;const t=new Uint8Array(256),a=e.length;for(let e=0;e<256;++e)t[e]=e;for(let r=0,i=0;r<256;++r){const n=t[r];i=i+n+e[r%a]&255;t[r]=t[i];t[i]=n}this.s=t}encryptBlock(e){let t=this.a,a=this.b;const r=this.s,i=e.length,n=new Uint8Array(i);for(let s=0;st<128?t<<1:t<<1^27));constructor(){this.buffer=new Uint8Array(16);this.bufferPosition=0}_expandKey(e){unreachable("Cannot call `_expandKey` on the base class")}_decrypt(e,t){let a,r,i;const n=new Uint8Array(16);n.set(e);for(let e=0,a=this._keySize;e<16;++e,++a)n[e]^=t[a];for(let e=this._cyclesOfRepetition-1;e>=1;--e){a=n[13];n[13]=n[9];n[9]=n[5];n[5]=n[1];n[1]=a;a=n[14];r=n[10];n[14]=n[6];n[10]=n[2];n[6]=a;n[2]=r;a=n[15];r=n[11];i=n[7];n[15]=n[3];n[11]=a;n[7]=r;n[3]=i;for(let e=0;e<16;++e)n[e]=this._inv_s[n[e]];for(let a=0,r=16*e;a<16;++a,++r)n[a]^=t[r];for(let e=0;e<16;e+=4){const t=this._mix[n[e]],r=this._mix[n[e+1]],i=this._mix[n[e+2]],s=this._mix[n[e+3]];a=t^r>>>8^r<<24^i>>>16^i<<16^s>>>24^s<<8;n[e]=a>>>24&255;n[e+1]=a>>16&255;n[e+2]=a>>8&255;n[e+3]=255&a}}a=n[13];n[13]=n[9];n[9]=n[5];n[5]=n[1];n[1]=a;a=n[14];r=n[10];n[14]=n[6];n[10]=n[2];n[6]=a;n[2]=r;a=n[15];r=n[11];i=n[7];n[15]=n[3];n[11]=a;n[7]=r;n[3]=i;for(let e=0;e<16;++e){n[e]=this._inv_s[n[e]];n[e]^=t[e]}return n}_encrypt(e,t){const a=this._s;let r,i,n;const s=new Uint8Array(16);s.set(e);for(let e=0;e<16;++e)s[e]^=t[e];for(let e=1;e=r;--a)if(e[a]!==t){t=0;break}o-=t;n[n.length-1]=e.subarray(0,16-t)}}const c=new Uint8Array(o);for(let e=0,t=0,a=n.length;e=256&&(o=255&(27^o))}for(let t=0;t<4;++t){a[e]=r^=a[e-32];e++;a[e]=i^=a[e-32];e++;a[e]=n^=a[e-32];e++;a[e]=s^=a[e-32];e++}}return a}}class PDFBase{_hash(e,t,a){unreachable("Abstract method `_hash` called")}checkOwnerPassword(e,t,a,r){const i=new Uint8Array(e.length+56);i.set(e,0);i.set(t,e.length);i.set(a,e.length+t.length);return isArrayEqual(this._hash(e,i,a),r)}checkUserPassword(e,t,a){const r=new Uint8Array(e.length+8);r.set(e,0);r.set(t,e.length);return isArrayEqual(this._hash(e,r,[]),a)}getOwnerKey(e,t,a,r){const i=new Uint8Array(e.length+56);i.set(e,0);i.set(t,e.length);i.set(a,e.length+t.length);const n=this._hash(e,i,a);return new AES256Cipher(n).decryptBlock(r,!1,new Uint8Array(16))}getUserKey(e,t,a){const r=new Uint8Array(e.length+8);r.set(e,0);r.set(t,e.length);const i=this._hash(e,r,[]);return new AES256Cipher(i).decryptBlock(a,!1,new Uint8Array(16))}}class PDF17 extends PDFBase{_hash(e,t,a){return calculateSHA256(t,0,t.length)}}class PDF20 extends PDFBase{_hash(e,t,a){let r=calculateSHA256(t,0,t.length).subarray(0,32),i=[0],n=0;for(;n<64||i.at(-1)>n-32;){const t=e.length+r.length+a.length,l=new Uint8Array(t);let h=0;l.set(e,h);h+=e.length;l.set(r,h);h+=r.length;l.set(a,h);const u=new Uint8Array(64*t);for(let e=0,a=0;e<64;e++,a+=t)u.set(l,a);i=new AES128Cipher(r.subarray(0,16)).encrypt(u,r.subarray(16,32));const d=Math.sumPrecise(i.slice(0,16))%3;0===d?r=calculateSHA256(i,0,i.length):1===d?r=(s=i,o=0,c=i.length,calculateSHA512(s,o,c,!0)):2===d&&(r=calculateSHA512(i,0,i.length));n++}var s,o,c;return r.subarray(0,32)}}class CipherTransform{constructor(e,t){this.StringCipherConstructor=e;this.StreamCipherConstructor=t}createStream(e,t){const a=new this.StreamCipherConstructor;return new DecryptStream(e,t,(function cipherTransformDecryptStream(e,t){return a.decryptBlock(e,t)}))}decryptString(e){const t=new this.StringCipherConstructor;let a=stringToBytes(e);a=t.decryptBlock(a,!0);return bytesToString(a)}encryptString(e){const t=new this.StringCipherConstructor;if(t instanceof AESBaseCipher){const a=16-e.length%16;e+=String.fromCharCode(a).repeat(a);const r=new Uint8Array(16);crypto.getRandomValues(r);let i=stringToBytes(e);i=t.encrypt(i,r);const n=new Uint8Array(16+i.length);n.set(r);n.set(i,16);return bytesToString(n)}let a=stringToBytes(e);a=t.encrypt(a);return bytesToString(a)}}class CipherTransformFactory{static get _defaultPasswordBytes(){return shadow(this,"_defaultPasswordBytes",new Uint8Array([40,191,78,94,78,117,138,65,100,0,78,86,255,250,1,8,46,46,0,182,208,104,62,128,47,12,169,254,100,83,105,122]))}#be(e,t,a,r,i,n,s,o,c,l,h,u){if(t){const e=Math.min(127,t.length);t=t.subarray(0,e)}else t=[];const d=6===e?new PDF20:new PDF17;return d.checkUserPassword(t,o,s)?d.getUserKey(t,c,h):t.length&&d.checkOwnerPassword(t,r,n,a)?d.getOwnerKey(t,i,n,l):null}#ye(e,t,a,r,i,n,s,o){const c=40+a.length+e.length,l=new Uint8Array(c);let h,u,d=0;if(t){u=Math.min(32,t.length);for(;d>8&255;l[d++]=i>>16&255;l[d++]=i>>>24&255;l.set(e,d);d+=e.length;if(n>=4&&!o){l.fill(255,d,d+4);d+=4}let f=calculateMD5(l,0,d);const g=s>>3;if(n>=3)for(h=0;h<50;++h)f=calculateMD5(f,0,g);const p=f.subarray(0,g);let m,b;if(n>=3){d=0;l.set(CipherTransformFactory._defaultPasswordBytes,d);d+=32;l.set(e,d);d+=e.length;m=new ARCFourCipher(p);b=m.encryptBlock(calculateMD5(l,0,d));u=p.length;const t=new Uint8Array(u);for(h=1;h<=19;++h){for(let e=0;er[t]===e))?p:null}#we(e,t,a,r){const i=new Uint8Array(32);let n=0;const s=Math.min(32,e.length);for(;n>3;if(a>=3)for(o=0;o<50;++o)c=calculateMD5(c,0,c.length);let h,u;if(a>=3){u=t;const e=new Uint8Array(l);for(o=19;o>=0;o--){for(let t=0;t>8&255;n[s++]=e>>16&255;n[s++]=255&t;n[s++]=t>>8&255;if(r){n[s++]=115;n[s++]=65;n[s++]=108;n[s++]=84}return calculateMD5(n,0,s).subarray(0,Math.min(i+5,16))}#xe(e,t,a,r,i){if(!(t instanceof Name))throw new FormatError("Invalid crypt filter name.");const n=this,s=e.get(t.name),o=s?.get("CFM");if(!o||"None"===o.name)return function(){return new NullCipher};if("V2"===o.name)return function(){return new ARCFourCipher(n.#Se(a,r,i,!1))};if("AESV2"===o.name)return function(){return new AES128Cipher(n.#Se(a,r,i,!0))};if("AESV3"===o.name)return function(){return new AES256Cipher(i)};throw new FormatError("Unknown crypto method")}constructor(e,t,a){const r=e.get("Filter");if(!isName(r,"Standard"))throw new FormatError("unknown encryption method");this.filterName=r.name;this.dict=e;const i=e.get("V");if(!Number.isInteger(i)||1!==i&&2!==i&&4!==i&&5!==i)throw new FormatError("unsupported encryption algorithm");this.algorithm=i;let n=e.get("Length");if(!n)if(i<=3)n=40;else{const t=e.get("CF"),a=e.get("StmF");if(t instanceof Dict&&a instanceof Name){t.suppressEncryption=!0;const e=t.get(a.name);n=e?.get("Length")||128;n<40&&(n<<=3)}}if(!Number.isInteger(n)||n<40||n%8!=0)throw new FormatError("invalid key length");const s=stringToBytes(e.get("O")),o=stringToBytes(e.get("U")),c=s.subarray(0,32),l=o.subarray(0,32),h=e.get("P"),u=e.get("R"),d=(4===i||5===i)&&!1!==e.get("EncryptMetadata");this.encryptMetadata=d;const f=stringToBytes(t);let g,p;if(a){if(6===u)try{a=utf8StringToString(a)}catch{warn("CipherTransformFactory: Unable to convert UTF8 encoded password.")}g=stringToBytes(a)}if(5!==i)p=this.#ye(f,g,c,l,h,u,n,d);else{const t=s.subarray(32,40),a=s.subarray(40,48),r=o.subarray(0,48),i=o.subarray(32,40),n=o.subarray(40,48),h=stringToBytes(e.get("OE")),d=stringToBytes(e.get("UE")),f=stringToBytes(e.get("Perms"));p=this.#be(u,g,c,t,a,r,l,i,n,h,d,f)}if(!p){if(!a)throw new PasswordException("No password given",ha);const e=this.#we(g,c,u,n);p=this.#ye(f,e,c,l,h,u,n,d)}if(!p)throw new PasswordException("Incorrect Password",ua);if(4===i&&p.length<16){this.encryptionKey=new Uint8Array(16);this.encryptionKey.set(p)}else this.encryptionKey=p;if(i>=4){const t=e.get("CF");t instanceof Dict&&(t.suppressEncryption=!0);this.cf=t;this.stmf=e.get("StmF")||Name.get("Identity");this.strf=e.get("StrF")||Name.get("Identity");this.eff=e.get("EFF")||this.stmf}}createCipherTransform(e,t){if(4===this.algorithm||5===this.algorithm)return new CipherTransform(this.#xe(this.cf,this.strf,e,t,this.encryptionKey),this.#xe(this.cf,this.stmf,e,t,this.encryptionKey));const a=this.#Se(e,t,this.encryptionKey,!1),cipherConstructor=function(){return new ARCFourCipher(a)};return new CipherTransform(cipherConstructor,cipherConstructor)}}class XRef{#Ae=null;constructor(e,t){this.stream=e;this.pdfManager=t;this.entries=[];this._xrefStms=new Set;this._cacheMap=new Map;this._pendingRefs=new RefSet;this._newPersistentRefNum=null;this._newTemporaryRefNum=null;this._persistentRefsCache=null}getNewPersistentRef(e){null===this._newPersistentRefNum&&(this._newPersistentRefNum=this.entries.length||1);const t=this._newPersistentRefNum++;this._cacheMap.set(t,e);return Ref.get(t,0)}getNewTemporaryRef(){if(null===this._newTemporaryRefNum){this._newTemporaryRefNum=this.entries.length||1;if(this._newPersistentRefNum){this._persistentRefsCache=new Map;for(let e=this._newTemporaryRefNum;e0;){const[s,o]=n;if(!Number.isInteger(s)||!Number.isInteger(o))throw new FormatError(`Invalid XRef range fields: ${s}, ${o}`);if(!Number.isInteger(a)||!Number.isInteger(r)||!Number.isInteger(i))throw new FormatError(`Invalid XRef entry fields length: ${s}, ${o}`);for(let n=t.entryNum;n=e.length);){a+=String.fromCharCode(r);r=e[t]}return a}function skipUntil(e,t,a){const r=a.length,i=e.length;let n=0;for(;t=r)break;t++;n++}return n}const e=/\b(endobj|\d+\s+\d+\s+obj|xref|trailer\s*<<)\b/g,t=/\b(startxref|\d+\s+\d+\s+obj)\b/g,a=/^(\d+)\s+(\d+)\s+obj\b/,r=new Uint8Array([116,114,97,105,108,101,114]),i=new Uint8Array([115,116,97,114,116,120,114,101,102]),n=new Uint8Array([47,88,82,101,102]);this.entries.length=0;this._cacheMap.clear();const s=this.stream;s.pos=0;const o=s.getBytes(),c=bytesToString(o),l=o.length;let h=s.start;const u=[],d=[];for(;h=l)break;f=o[h]}while(10!==f&&13!==f);continue}const g=readToken(o,h);let p;if(g.startsWith("xref")&&(4===g.length||/\s/.test(g[4]))){h+=skipUntil(o,h,r);u.push(h);h+=skipUntil(o,h,i)}else if(p=a.exec(g)){const t=0|p[1],a=0|p[2],r=h+g.length;let i,u=!1;if(this.entries[t]){if(this.entries[t].gen===a)try{new Parser({lexer:new Lexer(s.makeSubStream(r))}).getObj();u=!0}catch(e){e instanceof ParserEOFException?warn(`indexObjects -- checking object (${g}): "${e}".`):u=!0}}else u=!0;u&&(this.entries[t]={offset:h-s.start,gen:a,uncompressed:!0});e.lastIndex=r;const f=e.exec(c);if(f){i=e.lastIndex+1-h;if("endobj"!==f[1]){warn(`indexObjects: Found "${f[1]}" inside of another "obj", caused by missing "endobj" -- trying to recover.`);i-=f[1].length+1}}else i=l-h;const m=o.subarray(h,h+i),b=skipUntil(m,0,n);if(b0?Math.max(...this._xrefStms):null)}getEntry(e){const t=this.entries[e];return t&&!t.free&&t.offset?t:null}fetchIfRef(e,t=!1){return e instanceof Ref?this.fetch(e,t):e}fetch(e,t=!1){if(!(e instanceof Ref))throw new Error("ref object is not a reference");const a=e.num,r=this._cacheMap.get(a);if(void 0!==r){r instanceof Dict&&!r.objId&&(r.objId=e.toString());return r}let i=this.getEntry(a);if(null===i)return i;if(this._pendingRefs.has(e)){this._pendingRefs.remove(e);warn(`Ignoring circular reference: ${e}.`);return ya}this._pendingRefs.put(e);try{i=i.uncompressed?this.fetchUncompressed(e,i,t):this.fetchCompressed(e,i,t);this._pendingRefs.remove(e)}catch(t){this._pendingRefs.remove(e);throw t}i instanceof Dict?i.objId=e.toString():i instanceof BaseStream&&(i.dict.objId=e.toString());return i}fetchUncompressed(e,t,a=!1){const r=e.gen;let i=e.num;if(t.gen!==r){const n=`Inconsistent generation in XRef: ${e}`;if(this._generationFallback&&t.gen0&&t[3]-t[1]>0)return t;warn(`Empty, or invalid, /${e} entry.`)}return null}get mediaBox(){return shadow(this,"mediaBox",this.#Fe("MediaBox")||mc)}get cropBox(){return shadow(this,"cropBox",this.#Fe("CropBox")||this.mediaBox)}get userUnit(){const e=this.pageDict.get("UserUnit");return shadow(this,"userUnit","number"==typeof e&&e>0?e:1)}get view(){const{cropBox:e,mediaBox:t}=this;if(e!==t&&!isArrayEqual(e,t)){const a=Util.intersect(e,t);if(a&&a[2]-a[0]>0&&a[3]-a[1]>0)return shadow(this,"view",a);warn("Empty /CropBox and /MediaBox intersection.")}return shadow(this,"view",t)}get rotate(){let e=this.#ve("Rotate")||0;e%90!=0?e=0:e>=360?e%=360:e<0&&(e=(e%360+360)%360);return shadow(this,"rotate",e)}#Ie(e,t){if(!this.evaluatorOptions.ignoreErrors)throw e;warn(`getContentStream - ignoring sub-stream (${t}): "${e}".`)}async getContentStream(){const e=await this.pdfManager.ensure(this,"content");return e instanceof BaseStream?e:Array.isArray(e)?new StreamsSequenceStream(e,this.#Ie.bind(this)):new NullStream}get xfaData(){return shadow(this,"xfaData",this.xfaFactory?{bbox:this.xfaFactory.getBoundingBox(this.pageIndex)}:null)}async#Oe(e,t,a){const r=[];for(const i of e)if(i.id){const e=Ref.fromString(i.id);if(!e){warn(`A non-linked annotation cannot be modified: ${i.id}`);continue}if(i.deleted){t.put(e,e);if(i.popupRef){const e=Ref.fromString(i.popupRef);e&&t.put(e,e)}continue}a?.put(e);i.ref=e;r.push(this.xref.fetchAsync(e).then((e=>{e instanceof Dict&&(i.oldAnnotation=e.clone())}),(()=>{warn(`Cannot fetch \`oldAnnotation\` for: ${e}.`)})));delete i.id}await Promise.all(r)}async saveNewAnnotations(e,t,a,r,i){if(this.xfaFactory)throw new Error("XFA: Cannot save new annotations.");const n=this.#Ce(e),s=new RefSetCache,o=new RefSet;await this.#Oe(a,s,o);const c=this.pageDict,l=this.annotations.filter((e=>!(e instanceof Ref&&s.has(e)))),h=await AnnotationFactory.saveNewAnnotations(n,t,a,r,i);for(const{ref:e}of h.annotations)e instanceof Ref&&!o.has(e)&&l.push(e);const u=c.clone();u.set("Annots",l);i.put(this.ref,{data:u});for(const e of s)i.put(e,{data:null})}async save(e,t,a,r){const i=this.#Ce(e),n=await this._parsedAnnotations,s=[];for(const e of n)s.push(e.save(i,t,a,r).catch((function(e){warn(`save - ignoring annotation data during "${t.name}" task: "${e}".`);return null})));return Promise.all(s)}async loadResources(e){await(this.#ke??=this.pdfManager.ensure(this,"resources"));await ObjectLoader.load(this.resources,e,this.xref)}async#Te(e,t){const a=e?.get("Resources");if(!(a instanceof Dict&&a.size))return this.resources;await ObjectLoader.load(a,t,this.xref);return Dict.merge({xref:this.xref,dictArray:[a,this.resources],mergeSubDicts:!0})}async getOperatorList({handler:e,sink:t,task:a,intent:r,cacheKey:i,annotationStorage:c=null,modifiedIds:d=null}){const g=this.getContentStream(),p=this.loadResources(Ia),m=this.#Ce(e),b=this.xfaFactory?null:getNewAnnotationsMap(c),y=b?.get(this.pageIndex);let w=Promise.resolve(null),S=null;if(y){const e=this.pdfManager.ensureDoc("annotationGlobals");let t;const r=new Set;for(const{bitmapId:e,bitmap:t}of y)!e||t||r.has(e)||r.add(e);const{isOffscreenCanvasSupported:i}=this.evaluatorOptions;if(r.size>0){const e=y.slice();for(const[t,a]of c)t.startsWith(f)&&a.bitmap&&r.has(a.bitmapId)&&e.push(a);t=AnnotationFactory.generateImages(e,this.xref,i)}else t=AnnotationFactory.generateImages(y,this.xref,i);S=new RefSet;w=Promise.all([e,this.#Oe(y,S,null)]).then((([e])=>e?AnnotationFactory.printNewAnnotations(e,m,a,y,t):null))}const x=Promise.all([g,p]).then((async([n])=>{const s=await this.#Te(n.dict,Ia),o=new OperatorList(r,t);e.send("StartRenderPage",{transparency:m.hasBlendModes(s,this.nonBlendModesSet),pageIndex:this.pageIndex,cacheKey:i});await m.getOperatorList({stream:n,task:a,resources:s,operatorList:o});return o}));let[k,C,v]=await Promise.all([x,this._parsedAnnotations,w]);if(v){C=C.filter((e=>!(e.ref&&S.has(e.ref))));for(let e=0,t=v.length;ee.ref&&isRefsEqual(e.ref,a.refToReplace)));if(r>=0){C.splice(r,1,a);v.splice(e--,1);t--}}}C=C.concat(v)}if(0===C.length||r&h){k.flush(!0);return{length:k.totalLength}}const F=!!(r&l),O=!!(r&u),T=!!(r&n),M=!!(r&s),D=!!(r&o),R=[];for(const e of C)(T||M&&e.mustBeViewed(c,F)&&e.mustBeViewedWhenEditing(O,d)||D&&e.mustBePrinted(c))&&R.push(e.getOperatorList(m,a,r,c).catch((function(e){warn(`getOperatorList - ignoring annotation data during "${a.name}" task: "${e}".`);return{opList:null,separateForm:!1,separateCanvas:!1}})));const N=await Promise.all(R);let E=!1,L=!1;for(const{opList:e,separateForm:t,separateCanvas:a}of N){k.addOpList(e);E||=t;L||=a}k.flush(!0,{form:E,canvas:L});return{length:k.totalLength}}async extractTextContent({handler:e,task:t,includeMarkedContent:a,disableNormalization:r,sink:i}){const n=this.getContentStream(),s=this.loadResources(Oa),o=this.pdfManager.ensureCatalog("lang"),[c,,l]=await Promise.all([n,s,o]),h=await this.#Te(c.dict,Oa);return this.#Ce(e).getTextContent({stream:c,task:t,resources:h,includeMarkedContent:a,disableNormalization:r,sink:i,viewBox:this.view,lang:l})}async getStructTree(){const e=await this.pdfManager.ensureCatalog("structTreeRoot");if(!e)return null;await this._parsedAnnotations;try{const t=await this.pdfManager.ensure(this,"_parseStructTree",[e]);return await this.pdfManager.ensure(t,"serializable")}catch(e){warn(`getStructTree: "${e}".`);return null}}_parseStructTree(e){const t=new StructTreePage(e,this.pageDict);t.parse(this.ref);return t}async getAnnotationsData(e,t,a){const r=await this._parsedAnnotations;if(0===r.length)return r;const i=[],c=[];let l;const h=!!(a&n),u=!!(a&s),d=!!(a&o);for(const a of r){const r=h||u&&a.viewable;(r||d&&a.printable)&&i.push(a.data);if(a.hasTextContent&&r){l??=this.#Ce(e);c.push(a.extractTextContent(l,t,[-1/0,-1/0,1/0,1/0]).catch((function(e){warn(`getAnnotationsData - ignoring textContent during "${t.name}" task: "${e}".`)})))}}await Promise.all(c);return i}get annotations(){const e=this.#ve("Annots");return shadow(this,"annotations",Array.isArray(e)?e:[])}get _parsedAnnotations(){return shadow(this,"_parsedAnnotations",this.pdfManager.ensure(this,"annotations").then((async e=>{if(0===e.length)return e;const[t,a]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureDoc("fieldObjects")]);if(!t)return[];const r=a?.orphanFields,i=[];for(const a of e)i.push(AnnotationFactory.create(this.xref,a,t,this._localIdFactory,!1,r,this.ref).catch((function(e){warn(`_parsedAnnotations: "${e}".`);return null})));const n=[];let s,o;for(const e of await Promise.all(i))e&&(e instanceof WidgetAnnotation?(o||=[]).push(e):e instanceof PopupAnnotation?(s||=[]).push(e):n.push(e));o&&n.push(...o);s&&n.push(...s);return n})))}get jsActions(){return shadow(this,"jsActions",collectActions(this.xref,this.pageDict,Se))}}const bc=new Uint8Array([37,80,68,70,45]),yc=new Uint8Array([115,116,97,114,116,120,114,101,102]),wc=new Uint8Array([101,110,100,111,98,106]);function find(e,t,a=1024,r=!1){const i=t.length,n=e.peekBytes(a),s=n.length-i;if(s<=0)return!1;if(r){const a=i-1;let r=n.length-1;for(;r>=a;){let s=0;for(;s=i){e.pos+=r-a;return!0}r--}}else{let a=0;for(;a<=s;){let r=0;for(;r=i){e.pos+=a;return!0}a++}}return!1}class PDFDocument{#Me=new Map;#De=null;constructor(e,t){if(t.length<=0)throw new InvalidPDFException("The PDF file is empty, i.e. its size is zero bytes.");this.pdfManager=e;this.stream=t;this.xref=new XRef(t,e);const a={font:0};this._globalIdFactory=class{static getDocId(){return`g_${e.docId}`}static createFontId(){return"f"+ ++a.font}static createObjId(){unreachable("Abstract method `createObjId` called.")}static getPageObjId(){unreachable("Abstract method `getPageObjId` called.")}}}parse(e){this.xref.parse(e);this.catalog=new Catalog(this.pdfManager,this.xref)}get linearization(){let e=null;try{e=Linearization.create(this.stream)}catch(e){if(e instanceof MissingDataException)throw e;info(e)}return shadow(this,"linearization",e)}get startXRef(){const e=this.stream;let t=0;if(this.linearization){e.reset();if(find(e,wc)){e.skip(6);let a=e.peekByte();for(;isWhiteSpace(a);){e.pos++;a=e.peekByte()}t=e.pos-e.start}}else{const a=1024,r=yc.length;let i=!1,n=e.end;for(;!i&&n>0;){n-=a-r;n<0&&(n=0);e.pos=n;i=find(e,yc,a,!0)}if(i){e.skip(9);let a;do{a=e.getByte()}while(isWhiteSpace(a));let r="";for(;a>=32&&a<=57;){r+=String.fromCharCode(a);a=e.getByte()}t=parseInt(r,10);isNaN(t)&&(t=0)}}return shadow(this,"startXRef",t)}checkHeader(){const e=this.stream;e.reset();if(!find(e,bc))return;e.moveStart();e.skip(bc.length);let t,a="";for(;(t=e.getByte())>32&&a.length<7;)a+=String.fromCharCode(t);Ca.test(a)?this.#De=a:warn(`Invalid PDF header version: ${a}`)}parseStartXRef(){this.xref.setStartXRef(this.startXRef)}get numPages(){let e=0;e=this.catalog.hasActualNumPages?this.catalog.numPages:this.xfaFactory?this.xfaFactory.getNumPages():this.linearization?this.linearization.numPages:this.catalog.numPages;return shadow(this,"numPages",e)}#Be(e,t=0){return!!Array.isArray(e)&&e.every((e=>{if(!((e=this.xref.fetchIfRef(e))instanceof Dict))return!1;if(e.has("Kids")){if(++t>10){warn("#hasOnlyDocumentSignatures: maximum recursion depth reached");return!1}return this.#Be(e.get("Kids"),t)}const a=isName(e.get("FT"),"Sig"),r=e.get("Rect"),i=Array.isArray(r)&&r.every((e=>0===e));return a&&i}))}get _xfaStreams(){const{acroForm:e}=this.catalog;if(!e)return null;const t=e.get("XFA"),a=new Map(["xdp:xdp","template","datasets","config","connectionSet","localeSet","stylesheet","/xdp:xdp"].map((e=>[e,null])));if(t instanceof BaseStream&&!t.isEmpty){a.set("xdp:xdp",t);return a}if(!Array.isArray(t)||0===t.length)return null;for(let e=0,r=t.length;el.handleSetFont(r,[Name.get(e),1],null,h,t,d,a,i).catch((e=>{warn(`loadXfaFonts: "${e}".`);return null})),f=[];for(const[e,t]of i){const a=t.get("FontDescriptor");if(!(a instanceof Dict))continue;let r=a.get("FontFamily");r=r.replaceAll(/[ ]+(\d)/g,"$1");const i={fontFamily:r,fontWeight:a.get("FontWeight"),italicAngle:-a.get("ItalicAngle")};validateCSSFont(i)&&f.push(parseFont(e,null,i))}await Promise.all(f);const g=this.xfaFactory.setFonts(u);if(!g)return;n.ignoreErrors=!0;f.length=0;u.length=0;const p=new Set;for(const e of g)getXfaFontName(`${e}-Regular`)||p.add(e);p.size&&g.push("PdfJS-Fallback");for(const e of g)if(!p.has(e))for(const t of[{name:"Regular",fontWeight:400,italicAngle:0},{name:"Bold",fontWeight:700,italicAngle:0},{name:"Italic",fontWeight:400,italicAngle:12},{name:"BoldItalic",fontWeight:700,italicAngle:12}]){const a=`${e}-${t.name}`;f.push(parseFont(a,getXfaFontDict(a),{fontFamily:e,fontWeight:t.fontWeight,italicAngle:t.italicAngle}))}await Promise.all(f);this.xfaFactory.appendFonts(u,p)}loadXfaResources(e,t){return Promise.all([this.#Ne(e,t).catch((()=>{})),this.#Re()])}serializeXfaData(e){return this.xfaFactory?this.xfaFactory.serializeData(e):null}get version(){return this.catalog.version||this.#De}get formInfo(){const e={hasFields:!1,hasAcroForm:!1,hasXfa:!1,hasSignatures:!1},{acroForm:t}=this.catalog;if(!t)return shadow(this,"formInfo",e);try{const a=t.get("Fields"),r=Array.isArray(a)&&a.length>0;e.hasFields=r;const i=t.get("XFA");e.hasXfa=Array.isArray(i)&&i.length>0||i instanceof BaseStream&&!i.isEmpty;const n=!!(1&t.get("SigFlags")),s=n&&this.#Be(a);e.hasAcroForm=r&&!s;e.hasSignatures=n}catch(e){if(e instanceof MissingDataException)throw e;warn(`Cannot fetch form information: "${e}".`)}return shadow(this,"formInfo",e)}get documentInfo(){const{catalog:e,formInfo:t,xref:a}=this,r={PDFFormatVersion:this.version,Language:e.lang,EncryptFilterName:a.encrypt?.filterName??null,IsLinearized:!!this.linearization,IsAcroFormPresent:t.hasAcroForm,IsXFAPresent:t.hasXfa,IsCollectionPresent:!!e.collection,IsSignaturesPresent:t.hasSignatures};let i;try{i=a.trailer.get("Info")}catch(e){if(e instanceof MissingDataException)throw e;info("The document information dictionary is invalid.")}if(!(i instanceof Dict))return shadow(this,"documentInfo",r);for(const[e,t]of i){switch(e){case"Title":case"Author":case"Subject":case"Keywords":case"Creator":case"Producer":case"CreationDate":case"ModDate":if("string"==typeof t){r[e]=stringToPDFString(t);continue}break;case"Trapped":if(t instanceof Name){r[e]=t;continue}break;default:let a;switch(typeof t){case"string":a=stringToPDFString(t);break;case"number":case"boolean":a=t;break;default:t instanceof Name&&(a=t)}if(void 0===a){warn(`Bad value, for custom key "${e}", in Info: ${t}.`);continue}r.Custom??=Object.create(null);r.Custom[e]=a;continue}warn(`Bad value, for key "${e}", in Info: ${t}.`)}return shadow(this,"documentInfo",r)}get fingerprints(){const e="\0".repeat(16);function validate(t){return"string"==typeof t&&16===t.length&&t!==e}const t=this.xref.trailer.get("ID");let a,r;if(Array.isArray(t)&&validate(t[0])){a=stringToBytes(t[0]);t[1]!==t[0]&&validate(t[1])&&(r=stringToBytes(t[1]))}else a=calculateMD5(this.stream.getByteRange(0,1024),0,1024);return shadow(this,"fingerprints",[toHexUtil(a),r?toHexUtil(r):null])}async#Ee(e){const{catalog:t,linearization:a,xref:r}=this,i=Ref.get(a.objectNumberFirst,0);try{const e=await r.fetchAsync(i);if(e instanceof Dict){let a=e.getRaw("Type");a instanceof Ref&&(a=await r.fetchAsync(a));if(isName(a,"Page")||!e.has("Type")&&!e.has("Kids")&&e.has("Contents")){t.pageKidsCountCache.has(i)||t.pageKidsCountCache.put(i,1);t.pageIndexCache.has(i)||t.pageIndexCache.put(i,0);return[e,i]}}throw new FormatError("The Linearization dictionary doesn't point to a valid Page dictionary.")}catch(a){warn(`_getLinearizationPage: "${a.message}".`);return t.getPageDict(e)}}getPage(e){const t=this.#Me.get(e);if(t)return t;const{catalog:a,linearization:r,xfaFactory:i}=this;let n;n=i?Promise.resolve([Dict.empty,null]):r?.pageFirst===e?this.#Ee(e):a.getPageDict(e);n=n.then((([t,r])=>new Page({pdfManager:this.pdfManager,xref:this.xref,pageIndex:e,pageDict:t,ref:r,globalIdFactory:this._globalIdFactory,fontCache:a.fontCache,builtInCMapCache:a.builtInCMapCache,standardFontDataCache:a.standardFontDataCache,globalColorSpaceCache:a.globalColorSpaceCache,globalImageCache:a.globalImageCache,systemFontCache:a.systemFontCache,nonBlendModesSet:a.nonBlendModesSet,xfaFactory:i})));this.#Me.set(e,n);return n}async checkFirstPage(e=!1){if(!e)try{await this.getPage(0)}catch(e){if(e instanceof XRefEntryException){this.#Me.delete(0);await this.cleanup();throw new XRefParseException}}}async checkLastPage(e=!1){const{catalog:t,pdfManager:a}=this;t.setActualNumPages();let r;try{await Promise.all([a.ensureDoc("xfaFactory"),a.ensureDoc("linearization"),a.ensureCatalog("numPages")]);if(this.xfaFactory)return;r=this.linearization?this.linearization.numPages:t.numPages;if(!Number.isInteger(r))throw new FormatError("Page count is not an integer.");if(r<=1)return;await this.getPage(r-1)}catch(i){this.#Me.delete(r-1);await this.cleanup();if(i instanceof XRefEntryException&&!e)throw new XRefParseException;warn(`checkLastPage - invalid /Pages tree /Count: ${r}.`);let n;try{n=await t.getAllPageDicts(e)}catch(a){if(a instanceof XRefEntryException&&!e)throw new XRefParseException;t.setActualNumPages(1);return}for(const[e,[r,i]]of n){let n;if(r instanceof Error){n=Promise.reject(r);n.catch((()=>{}))}else n=Promise.resolve(new Page({pdfManager:a,xref:this.xref,pageIndex:e,pageDict:r,ref:i,globalIdFactory:this._globalIdFactory,fontCache:t.fontCache,builtInCMapCache:t.builtInCMapCache,standardFontDataCache:t.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:t.globalImageCache,systemFontCache:t.systemFontCache,nonBlendModesSet:t.nonBlendModesSet,xfaFactory:null}));this.#Me.set(e,n)}t.setActualNumPages(n.size)}}async fontFallback(e,t){const{catalog:a,pdfManager:r}=this;for(const i of await Promise.all(a.fontCache))if(i.loadedName===e){i.fallback(t,r.evaluatorOptions);return}}async cleanup(e=!1){return this.catalog?this.catalog.cleanup(e):clearGlobalCaches()}async#Pe(e,t,a,r,i,n,s){const{xref:o}=this;if(!(a instanceof Ref)||n.has(a))return;n.put(a);const c=await o.fetchAsync(a);if(!(c instanceof Dict))return;let l=await c.getAsync("Subtype");l=l instanceof Name?l.name:null;if("Link"===l)return;if(c.has("T")){const t=stringToPDFString(await c.getAsync("T"));e=""===e?t:`${e}.${t}`}else{let a=c;for(;;){a=a.getRaw("Parent")||t;if(a instanceof Ref){if(n.has(a))break;a=await o.fetchAsync(a)}if(!(a instanceof Dict))break;if(a.has("T")){const t=stringToPDFString(await a.getAsync("T"));e=""===e?t:`${e}.${t}`;break}}}t&&!c.has("Parent")&&isName(c.get("Subtype"),"Widget")&&s.put(a,t);r.has(e)||r.set(e,[]);r.get(e).push(AnnotationFactory.create(o,a,i,null,!0,s,null).then((e=>e?.getFieldObject())).catch((function(e){warn(`#collectFieldObjects: "${e}".`);return null})));if(!c.has("Kids"))return;const h=await c.getAsync("Kids");if(Array.isArray(h))for(const t of h)await this.#Pe(e,a,t,r,i,n,s)}get fieldObjects(){return shadow(this,"fieldObjects",this.pdfManager.ensureDoc("formInfo").then((async e=>{if(!e.hasFields)return null;const t=await this.annotationGlobals;if(!t)return null;const{acroForm:a}=t,r=new RefSet,i=Object.create(null),n=new Map,s=new RefSetCache;for(const e of a.get("Fields"))await this.#Pe("",null,e,n,t,r,s);const o=[];for(const[e,t]of n)o.push(Promise.all(t).then((t=>{(t=t.filter((e=>!!e))).length>0&&(i[e]=t)})));await Promise.all(o);return{allFields:objectSize(i)>0?i:null,orphanFields:s}})))}get hasJSActions(){return shadow(this,"hasJSActions",this.pdfManager.ensureDoc("_parseHasJSActions"))}async _parseHasJSActions(){const[e,t]=await Promise.all([this.pdfManager.ensureCatalog("jsActions"),this.pdfManager.ensureDoc("fieldObjects")]);return!!e||!!t?.allFields&&Object.values(t.allFields).some((e=>e.some((e=>null!==e.actions))))}get calculationOrderIds(){const e=this.catalog.acroForm?.get("CO");if(!Array.isArray(e)||0===e.length)return shadow(this,"calculationOrderIds",null);const t=[];for(const a of e)a instanceof Ref&&t.push(a.toString());return shadow(this,"calculationOrderIds",t.length?t:null)}get annotationGlobals(){return shadow(this,"annotationGlobals",AnnotationFactory.createGlobals(this.pdfManager))}}class BasePdfManager{constructor({docBaseUrl:e,docId:t,enableXfa:a,evaluatorOptions:r,handler:i,password:n}){this._docBaseUrl=function parseDocBaseUrl(e){if(e){const t=createValidAbsoluteUrl(e);if(t)return t.href;warn(`Invalid absolute docBaseUrl: "${e}".`)}return null}(e);this._docId=t;this._password=n;this.enableXfa=a;r.isOffscreenCanvasSupported&&=FeatureTest.isOffscreenCanvasSupported;r.isImageDecoderSupported&&=FeatureTest.isImageDecoderSupported;this.evaluatorOptions=Object.freeze(r);ImageResizer.setOptions(r);JpegStream.setOptions(r);OperatorList.setOptions(r);const s={...r,handler:i};JpxImage.setOptions(s);IccColorSpace.setOptions(s);CmykICCBasedCS.setOptions(s)}get docId(){return this._docId}get password(){return this._password}get docBaseUrl(){return this._docBaseUrl}ensureDoc(e,t){return this.ensure(this.pdfDocument,e,t)}ensureXRef(e,t){return this.ensure(this.pdfDocument.xref,e,t)}ensureCatalog(e,t){return this.ensure(this.pdfDocument.catalog,e,t)}getPage(e){return this.pdfDocument.getPage(e)}fontFallback(e,t){return this.pdfDocument.fontFallback(e,t)}cleanup(e=!1){return this.pdfDocument.cleanup(e)}async ensure(e,t,a){unreachable("Abstract method `ensure` called")}requestRange(e,t){unreachable("Abstract method `requestRange` called")}requestLoadedStream(e=!1){unreachable("Abstract method `requestLoadedStream` called")}sendProgressiveData(e){unreachable("Abstract method `sendProgressiveData` called")}updatePassword(e){this._password=e}terminate(e){unreachable("Abstract method `terminate` called")}}class LocalPdfManager extends BasePdfManager{constructor(e){super(e);const t=new Stream(e.source);this.pdfDocument=new PDFDocument(this,t);this._loadedStreamPromise=Promise.resolve(t)}async ensure(e,t,a){const r=e[t];return"function"==typeof r?r.apply(e,a):r}requestRange(e,t){return Promise.resolve()}requestLoadedStream(e=!1){return this._loadedStreamPromise}terminate(e){}}class NetworkPdfManager extends BasePdfManager{constructor(e){super(e);this.streamManager=new ChunkedStreamManager(e.source,{msgHandler:e.handler,length:e.length,disableAutoFetch:e.disableAutoFetch,rangeChunkSize:e.rangeChunkSize});this.pdfDocument=new PDFDocument(this,this.streamManager.getStream())}async ensure(e,t,a){try{const r=e[t];return"function"==typeof r?r.apply(e,a):r}catch(r){if(!(r instanceof MissingDataException))throw r;await this.requestRange(r.begin,r.end);return this.ensure(e,t,a)}}requestRange(e,t){return this.streamManager.requestRange(e,t)}requestLoadedStream(e=!1){return this.streamManager.requestAllChunks(e)}sendProgressiveData(e){this.streamManager.onReceiveData({chunk:e})}terminate(e){this.streamManager.abort(e)}}const Sc=1,xc=2,Ac=1,kc=2,Cc=3,vc=4,Fc=5,Ic=6,Oc=7,Tc=8;function onFn(){}function wrapReason(e){if(e instanceof AbortException||e instanceof InvalidPDFException||e instanceof PasswordException||e instanceof ResponseException||e instanceof UnknownErrorException)return e;e instanceof Error||"object"==typeof e&&null!==e||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(e.name){case"AbortException":return new AbortException(e.message);case"InvalidPDFException":return new InvalidPDFException(e.message);case"PasswordException":return new PasswordException(e.message,e.code);case"ResponseException":return new ResponseException(e.message,e.status,e.missing);case"UnknownErrorException":return new UnknownErrorException(e.message,e.details)}return new UnknownErrorException(e.message,e.toString())}class MessageHandler{#Le=new AbortController;constructor(e,t,a){this.sourceName=e;this.targetName=t;this.comObj=a;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);a.addEventListener("message",this.#je.bind(this),{signal:this.#Le.signal})}#je({data:e}){if(e.targetName!==this.sourceName)return;if(e.stream){this.#_e(e);return}if(e.callback){const t=e.callbackId,a=this.callbackCapabilities[t];if(!a)throw new Error(`Cannot resolve callback ${t}`);delete this.callbackCapabilities[t];if(e.callback===Sc)a.resolve(e.data);else{if(e.callback!==xc)throw new Error("Unexpected callback case");a.reject(wrapReason(e.reason))}return}const t=this.actionHandler[e.action];if(!t)throw new Error(`Unknown action from worker: ${e.action}`);if(e.callbackId){const a=this.sourceName,r=e.sourceName,i=this.comObj;Promise.try(t,e.data).then((function(t){i.postMessage({sourceName:a,targetName:r,callback:Sc,callbackId:e.callbackId,data:t})}),(function(t){i.postMessage({sourceName:a,targetName:r,callback:xc,callbackId:e.callbackId,reason:wrapReason(t)})}))}else e.streamId?this.#Ue(e):t(e.data)}on(e,t){const a=this.actionHandler;if(a[e])throw new Error(`There is already an actionName called "${e}"`);a[e]=t}send(e,t,a){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,data:t},a)}sendWithPromise(e,t,a){const r=this.callbackId++,i=Promise.withResolvers();this.callbackCapabilities[r]=i;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,callbackId:r,data:t},a)}catch(e){i.reject(e)}return i.promise}sendWithStream(e,t,a,r){const i=this.streamId++,n=this.sourceName,s=this.targetName,o=this.comObj;return new ReadableStream({start:a=>{const c=Promise.withResolvers();this.streamControllers[i]={controller:a,startCall:c,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:n,targetName:s,action:e,streamId:i,data:t,desiredSize:a.desiredSize},r);return c.promise},pull:e=>{const t=Promise.withResolvers();this.streamControllers[i].pullCall=t;o.postMessage({sourceName:n,targetName:s,stream:Ic,streamId:i,desiredSize:e.desiredSize});return t.promise},cancel:e=>{assert(e instanceof Error,"cancel must have a valid reason");const t=Promise.withResolvers();this.streamControllers[i].cancelCall=t;this.streamControllers[i].isClosed=!0;o.postMessage({sourceName:n,targetName:s,stream:Ac,streamId:i,reason:wrapReason(e)});return t.promise}},a)}#Ue(e){const t=e.streamId,a=this.sourceName,r=e.sourceName,i=this.comObj,n=this,s=this.actionHandler[e.action],o={enqueue(e,n=1,s){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=n;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}i.postMessage({sourceName:a,targetName:r,stream:vc,streamId:t,chunk:e},s)},close(){if(!this.isCancelled){this.isCancelled=!0;i.postMessage({sourceName:a,targetName:r,stream:Cc,streamId:t});delete n.streamSinks[t]}},error(e){assert(e instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;i.postMessage({sourceName:a,targetName:r,stream:Fc,streamId:t,reason:wrapReason(e)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:e.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[t]=o;Promise.try(s,e.data,o).then((function(){i.postMessage({sourceName:a,targetName:r,stream:Tc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:Tc,streamId:t,reason:wrapReason(e)})}))}#_e(e){const t=e.streamId,a=this.sourceName,r=e.sourceName,i=this.comObj,n=this.streamControllers[t],s=this.streamSinks[t];switch(e.stream){case Tc:e.success?n.startCall.resolve():n.startCall.reject(wrapReason(e.reason));break;case Oc:e.success?n.pullCall.resolve():n.pullCall.reject(wrapReason(e.reason));break;case Ic:if(!s){i.postMessage({sourceName:a,targetName:r,stream:Oc,streamId:t,success:!0});break}s.desiredSize<=0&&e.desiredSize>0&&s.sinkCapability.resolve();s.desiredSize=e.desiredSize;Promise.try(s.onPull||onFn).then((function(){i.postMessage({sourceName:a,targetName:r,stream:Oc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:Oc,streamId:t,reason:wrapReason(e)})}));break;case vc:assert(n,"enqueue should have stream controller");if(n.isClosed)break;n.controller.enqueue(e.chunk);break;case Cc:assert(n,"close should have stream controller");if(n.isClosed)break;n.isClosed=!0;n.controller.close();this.#Xe(n,t);break;case Fc:assert(n,"error should have stream controller");n.controller.error(wrapReason(e.reason));this.#Xe(n,t);break;case kc:e.success?n.cancelCall.resolve():n.cancelCall.reject(wrapReason(e.reason));this.#Xe(n,t);break;case Ac:if(!s)break;const o=wrapReason(e.reason);Promise.try(s.onCancel||onFn,o).then((function(){i.postMessage({sourceName:a,targetName:r,stream:kc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:kc,streamId:t,reason:wrapReason(e)})}));s.sinkCapability.reject(o);s.isCancelled=!0;delete this.streamSinks[t];break;default:throw new Error("Unexpected stream case")}}async#Xe(e,t){await Promise.allSettled([e.startCall?.promise,e.pullCall?.promise,e.cancelCall?.promise]);delete this.streamControllers[t]}destroy(){this.#Le?.abort();this.#Le=null}}async function writeObject(e,t,a,{encrypt:r=null}){const i=r?.createCipherTransform(e.num,e.gen);a.push(`${e.num} ${e.gen} obj\n`);t instanceof Dict?await writeDict(t,a,i):t instanceof BaseStream?await writeStream(t,a,i):(Array.isArray(t)||ArrayBuffer.isView(t))&&await writeArray(t,a,i);a.push("\nendobj\n")}async function writeDict(e,t,a){t.push("<<");for(const r of e.getKeys()){t.push(` /${escapePDFName(r)} `);await writeValue(e.getRaw(r),t,a)}t.push(">>")}async function writeStream(e,t,a){let r=e.getBytes();const{dict:i}=e,[n,s]=await Promise.all([i.getAsync("Filter"),i.getAsync("DecodeParms")]),o=isName(Array.isArray(n)?await i.xref.fetchIfRefAsync(n[0]):n,"FlateDecode");if(r.length>=256||o)try{const e=new CompressionStream("deflate"),t=e.writable.getWriter();await t.ready;t.write(r).then((async()=>{await t.ready;await t.close()})).catch((()=>{}));const a=await new Response(e.readable).arrayBuffer();r=new Uint8Array(a);let c,l;if(n){if(!o){c=Array.isArray(n)?[Name.get("FlateDecode"),...n]:[Name.get("FlateDecode"),n];s&&(l=Array.isArray(s)?[null,...s]:[null,s])}}else c=Name.get("FlateDecode");c&&i.set("Filter",c);l&&i.set("DecodeParms",l)}catch(e){info(`writeStream - cannot compress data: "${e}".`)}let c=bytesToString(r);a&&(c=a.encryptString(c));i.set("Length",c.length);await writeDict(i,t,a);t.push(" stream\n",c,"\nendstream")}async function writeArray(e,t,a){t.push("[");let r=!0;for(const i of e){r?r=!1:t.push(" ");await writeValue(i,t,a)}t.push("]")}async function writeValue(e,t,a){if(e instanceof Name)t.push(`/${escapePDFName(e.name)}`);else if(e instanceof Ref)t.push(`${e.num} ${e.gen} R`);else if(Array.isArray(e)||ArrayBuffer.isView(e))await writeArray(e,t,a);else if("string"==typeof e){a&&(e=a.encryptString(e));t.push(`(${escapeString(e)})`)}else"number"==typeof e?t.push(numberToString(e)):"boolean"==typeof e?t.push(e.toString()):e instanceof Dict?await writeDict(e,t,a):e instanceof BaseStream?await writeStream(e,t,a):null===e?t.push("null"):warn(`Unhandled value in writer: ${typeof e}, please file a bug.`)}function writeInt(e,t,a,r){for(let i=t+a-1;i>a-1;i--){r[i]=255&e;e>>=8}return a+t}function writeString(e,t,a){const r=e.length;for(let i=0;i1&&(n=a.documentElement.searchNode([i.at(-1)],0));n?n.childNodes=Array.isArray(r)?r.map((e=>new SimpleDOMNode("value",e))):[new SimpleDOMNode("#text",r)]:warn(`Node not found for path: ${t}`)}const r=[];a.documentElement.dump(r);return r.join("")}(r.fetchIfRef(t).getString(),a)}const i=new StringStream(e);i.dict=new Dict(r);i.dict.set("Type",Name.get("EmbeddedFile"));a.put(t,{data:i})}function getIndexes(e){const t=[];for(const{ref:a}of e)a.num===t.at(-2)+t.at(-1)?t[t.length-1]+=1:t.push(a.num,1);return t}function computeIDs(e,t,a){if(Array.isArray(t.fileIds)&&t.fileIds.length>0){const r=function computeMD5(e,t){const a=Math.floor(Date.now()/1e3),r=t.filename||"",i=[a.toString(),r,e.toString(),...t.infoMap.values()],n=Math.sumPrecise(i.map((e=>e.length))),s=new Uint8Array(n);let o=0;for(const e of i)o=writeString(e,o,s);return bytesToString(calculateMD5(s,0,s.length))}(e,t);a.set("ID",[t.fileIds[0],r])}}async function incrementalUpdate({originalData:e,xrefInfo:t,changes:a,xref:r=null,hasXfa:i=!1,xfaDatasetsRef:n=null,hasXfaDatasetsEntry:s=!1,needAppearances:o,acroFormRef:c=null,acroForm:l=null,xfaData:h=null,useXrefStream:u=!1}){await async function updateAcroform({xref:e,acroForm:t,acroFormRef:a,hasXfa:r,hasXfaDatasetsEntry:i,xfaDatasetsRef:n,needAppearances:s,changes:o}){!r||i||n||warn("XFA - Cannot save it");if(!s&&(!r||!n||i))return;const c=t.clone();if(r&&!i){const e=t.get("XFA").slice();e.splice(2,0,"datasets");e.splice(3,0,n);c.set("XFA",e)}s&&c.set("NeedAppearances",!0);o.put(a,{data:c})}({xref:r,acroForm:l,acroFormRef:c,hasXfa:i,hasXfaDatasetsEntry:s,xfaDatasetsRef:n,needAppearances:o,changes:a});i&&updateXFA({xfaData:h,xfaDatasetsRef:n,changes:a,xref:r});const d=function getTrailerDict(e,t,a){const r=new Dict(null);r.set("Prev",e.startXRef);const i=e.newRef;if(a){t.put(i,{data:""});r.set("Size",i.num+1);r.set("Type",Name.get("XRef"))}else r.set("Size",i.num);null!==e.rootRef&&r.set("Root",e.rootRef);null!==e.infoRef&&r.set("Info",e.infoRef);null!==e.encryptRef&&r.set("Encrypt",e.encryptRef);return r}(t,a,u),f=[],g=await async function writeChanges(e,t,a=[]){const r=[];for(const[i,{data:n}]of e.items())if(null!==n&&"string"!=typeof n){await writeObject(i,n,a,t);r.push({ref:i,data:a.join("")});a.length=0}else r.push({ref:i,data:n});return r.sort(((e,t)=>e.ref.num-t.ref.num))}(a,r,f);let p=e.length;const m=e.at(-1);if(10!==m&&13!==m){f.push("\n");p+=1}for(const{data:e}of g)null!==e&&f.push(e);await(u?async function getXRefStreamTable(e,t,a,r,i){const n=[];let s=0,o=0;for(const{ref:e,data:r}of a){let a;s=Math.max(s,t);if(null!==r){a=Math.min(e.gen,65535);n.push([1,t,a]);t+=r.length}else{a=Math.min(e.gen+1,65535);n.push([0,0,a])}o=Math.max(o,a)}r.set("Index",getIndexes(a));const c=[1,getSizeInBytes(s),getSizeInBytes(o)];r.set("W",c);computeIDs(t,e,r);const l=Math.sumPrecise(c),h=new Uint8Array(l*n.length),u=new Stream(h);u.dict=r;let d=0;for(const[e,t,a]of n){d=writeInt(e,c[0],d,h);d=writeInt(t,c[1],d,h);d=writeInt(a,c[2],d,h)}await writeObject(e.newRef,u,i,{});i.push("startxref\n",t.toString(),"\n%%EOF\n")}(t,p,g,d,f):async function getXRefTable(e,t,a,r,i){i.push("xref\n");const n=getIndexes(a);let s=0;for(const{ref:e,data:r}of a){if(e.num===n[s]){i.push(`${n[s]} ${n[s+1]}\n`);s+=2}if(null!==r){i.push(`${t.toString().padStart(10,"0")} ${Math.min(e.gen,65535).toString().padStart(5,"0")} n\r\n`);t+=r.length}else i.push(`0000000000 ${Math.min(e.gen+1,65535).toString().padStart(5,"0")} f\r\n`)}computeIDs(t,e,r);i.push("trailer\n");await writeDict(r,i);i.push("\nstartxref\n",t.toString(),"\n%%EOF\n")}(t,p,g,d,f));const b=e.length+Math.sumPrecise(f.map((e=>e.length))),y=new Uint8Array(b);y.set(e);let w=e.length;for(const e of f)w=writeString(e,w,y);return y}class PDFWorkerStream{constructor(e){this._msgHandler=e;this._contentLength=null;this._fullRequestReader=null;this._rangeRequestReaders=[]}getFullReader(){assert(!this._fullRequestReader,"PDFWorkerStream.getFullReader can only be called once.");this._fullRequestReader=new PDFWorkerStreamReader(this._msgHandler);return this._fullRequestReader}getRangeReader(e,t){const a=new PDFWorkerStreamRangeReader(e,t,this._msgHandler);this._rangeRequestReaders.push(a);return a}cancelAllRequests(e){this._fullRequestReader?.cancel(e);for(const t of this._rangeRequestReaders.slice(0))t.cancel(e)}}class PDFWorkerStreamReader{constructor(e){this._msgHandler=e;this.onProgress=null;this._contentLength=null;this._isRangeSupported=!1;this._isStreamingSupported=!1;const t=this._msgHandler.sendWithStream("GetReader");this._reader=t.getReader();this._headersReady=this._msgHandler.sendWithPromise("ReaderHeadersReady").then((e=>{this._isStreamingSupported=e.isStreamingSupported;this._isRangeSupported=e.isRangeSupported;this._contentLength=e.contentLength}))}get headersReady(){return this._headersReady}get contentLength(){return this._contentLength}get isStreamingSupported(){return this._isStreamingSupported}get isRangeSupported(){return this._isRangeSupported}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class PDFWorkerStreamRangeReader{constructor(e,t,a){this._msgHandler=a;this.onProgress=null;const r=this._msgHandler.sendWithStream("GetRangeReader",{begin:e,end:t});this._reader=r.getReader()}get isStreamingSupported(){return!1}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class WorkerTask{constructor(e){this.name=e;this.terminated=!1;this._capability=Promise.withResolvers()}get finished(){return this._capability.promise}finish(){this._capability.resolve()}terminate(){this.terminated=!0}ensureNotTerminated(){if(this.terminated)throw new Error("Worker task was terminated")}}class WorkerMessageHandler{static{"undefined"==typeof window&&!e&&"undefined"!=typeof self&&"function"==typeof self.postMessage&&"onmessage"in self&&this.initializeFromPort(self)}static setup(e,t){let a=!1;e.on("test",(t=>{if(!a){a=!0;e.send("test",t instanceof Uint8Array)}}));e.on("configure",(e=>{!function setVerbosityLevel(e){Number.isInteger(e)&&(da=e)}(e.verbosity)}));e.on("GetDocRequest",(e=>this.createDocumentHandler(e,t)))}static createDocumentHandler(e,t){let a,r=!1,i=null;const n=new Set,s=getVerbosityLevel(),{docId:o,apiVersion:c}=e,l="5.3.31";if(c!==l)throw new Error(`The API version "${c}" does not match the Worker version "${l}".`);const buildMsg=(e,t)=>`The \`${e}.prototype\` contains unexpected enumerable property "${t}", thus breaking e.g. \`for...in\` iteration of ${e}s.`;for(const e in{})throw new Error(buildMsg("Object",e));for(const e in[])throw new Error(buildMsg("Array",e));const h=o+"_worker";let u=new MessageHandler(h,o,t);function ensureNotTerminated(){if(r)throw new Error("Worker was terminated")}function startWorkerTask(e){n.add(e)}function finishWorkerTask(e){e.finish();n.delete(e)}async function loadDocument(e){await a.ensureDoc("checkHeader");await a.ensureDoc("parseStartXRef");await a.ensureDoc("parse",[e]);await a.ensureDoc("checkFirstPage",[e]);await a.ensureDoc("checkLastPage",[e]);const t=await a.ensureDoc("isPureXfa");if(t){const e=new WorkerTask("loadXfaResources");startWorkerTask(e);await a.ensureDoc("loadXfaResources",[u,e]);finishWorkerTask(e)}const[r,i]=await Promise.all([a.ensureDoc("numPages"),a.ensureDoc("fingerprints")]);return{numPages:r,fingerprints:i,htmlForXfa:t?await a.ensureDoc("htmlForXfa"):null}}function setupDoc(e){function onSuccess(e){ensureNotTerminated();u.send("GetDoc",{pdfInfo:e})}function onFailure(e){ensureNotTerminated();if(e instanceof PasswordException){const t=new WorkerTask(`PasswordException: response ${e.code}`);startWorkerTask(t);u.sendWithPromise("PasswordRequest",e).then((function({password:e}){finishWorkerTask(t);a.updatePassword(e);pdfManagerReady()})).catch((function(){finishWorkerTask(t);u.send("DocException",e)}))}else u.send("DocException",wrapReason(e))}function pdfManagerReady(){ensureNotTerminated();loadDocument(!1).then(onSuccess,(function(e){ensureNotTerminated();e instanceof XRefParseException?a.requestLoadedStream().then((function(){ensureNotTerminated();loadDocument(!0).then(onSuccess,onFailure)})):onFailure(e)}))}ensureNotTerminated();(async function getPdfManager({data:e,password:t,disableAutoFetch:a,rangeChunkSize:r,length:n,docBaseUrl:s,enableXfa:c,evaluatorOptions:l}){const h={source:null,disableAutoFetch:a,docBaseUrl:s,docId:o,enableXfa:c,evaluatorOptions:l,handler:u,length:n,password:t,rangeChunkSize:r};if(e){h.source=e;return new LocalPdfManager(h)}const d=new PDFWorkerStream(u),f=d.getFullReader(),g=Promise.withResolvers();let p,m=[],b=0;f.headersReady.then((function(){if(f.isRangeSupported){h.source=d;h.length=f.contentLength;h.disableAutoFetch||=f.isStreamingSupported;p=new NetworkPdfManager(h);for(const e of m)p.sendProgressiveData(e);m=[];g.resolve(p);i=null}})).catch((function(e){g.reject(e);i=null}));new Promise((function(e,t){const readChunk=function({value:e,done:a}){try{ensureNotTerminated();if(a){if(!p){const e=arrayBuffersToBytes(m);m=[];n&&e.length!==n&&warn("reported HTTP length is different from actual");h.source=e;p=new LocalPdfManager(h);g.resolve(p)}i=null;return}b+=e.byteLength;f.isStreamingSupported||u.send("DocProgress",{loaded:b,total:Math.max(b,f.contentLength||0)});p?p.sendProgressiveData(e):m.push(e);f.read().then(readChunk,t)}catch(e){t(e)}};f.read().then(readChunk,t)})).catch((function(e){g.reject(e);i=null}));i=e=>{d.cancelAllRequests(e)};return g.promise})(e).then((function(e){if(r){e.terminate(new AbortException("Worker was terminated."));throw new Error("Worker was terminated")}a=e;a.requestLoadedStream(!0).then((e=>{u.send("DataLoaded",{length:e.bytes.byteLength})}))})).then(pdfManagerReady,onFailure)}u.on("GetPage",(function(e){return a.getPage(e.pageIndex).then((function(e){return Promise.all([a.ensure(e,"rotate"),a.ensure(e,"ref"),a.ensure(e,"userUnit"),a.ensure(e,"view")]).then((function([e,t,a,r]){return{rotate:e,ref:t,refStr:t?.toString()??null,userUnit:a,view:r}}))}))}));u.on("GetPageIndex",(function(e){const t=Ref.get(e.num,e.gen);return a.ensureCatalog("getPageIndex",[t])}));u.on("GetDestinations",(function(e){return a.ensureCatalog("destinations")}));u.on("GetDestination",(function(e){return a.ensureCatalog("getDestination",[e.id])}));u.on("GetPageLabels",(function(e){return a.ensureCatalog("pageLabels")}));u.on("GetPageLayout",(function(e){return a.ensureCatalog("pageLayout")}));u.on("GetPageMode",(function(e){return a.ensureCatalog("pageMode")}));u.on("GetViewerPreferences",(function(e){return a.ensureCatalog("viewerPreferences")}));u.on("GetOpenAction",(function(e){return a.ensureCatalog("openAction")}));u.on("GetAttachments",(function(e){return a.ensureCatalog("attachments")}));u.on("GetDocJSActions",(function(e){return a.ensureCatalog("jsActions")}));u.on("GetPageJSActions",(function({pageIndex:e}){return a.getPage(e).then((e=>a.ensure(e,"jsActions")))}));u.on("GetOutline",(function(e){return a.ensureCatalog("documentOutline")}));u.on("GetOptionalContentConfig",(function(e){return a.ensureCatalog("optionalContentConfig")}));u.on("GetPermissions",(function(e){return a.ensureCatalog("permissions")}));u.on("GetMetadata",(function(e){return Promise.all([a.ensureDoc("documentInfo"),a.ensureCatalog("metadata")])}));u.on("GetMarkInfo",(function(e){return a.ensureCatalog("markInfo")}));u.on("GetData",(function(e){return a.requestLoadedStream().then((e=>e.bytes))}));u.on("GetAnnotations",(function({pageIndex:e,intent:t}){return a.getPage(e).then((function(a){const r=new WorkerTask(`GetAnnotations: page ${e}`);startWorkerTask(r);return a.getAnnotationsData(u,r,t).then((e=>{finishWorkerTask(r);return e}),(e=>{finishWorkerTask(r);throw e}))}))}));u.on("GetFieldObjects",(function(e){return a.ensureDoc("fieldObjects").then((e=>e?.allFields||null))}));u.on("HasJSActions",(function(e){return a.ensureDoc("hasJSActions")}));u.on("GetCalculationOrderIds",(function(e){return a.ensureDoc("calculationOrderIds")}));u.on("SaveDocument",(async function({isPureXfa:e,numPages:t,annotationStorage:r,filename:i}){const n=[a.requestLoadedStream(),a.ensureCatalog("acroForm"),a.ensureCatalog("acroFormRef"),a.ensureDoc("startXRef"),a.ensureDoc("xref"),a.ensureDoc("linearization"),a.ensureCatalog("structTreeRoot")],s=new RefSetCache,o=[],c=e?null:getNewAnnotationsMap(r),[l,h,d,f,g,p,m]=await Promise.all(n),b=g.trailer.getRaw("Root")||null;let y;if(c){m?await m.canUpdateStructTree({pdfManager:a,newAnnotationsByPage:c})&&(y=m):await StructTreeRoot.canCreateStructureTree({catalogRef:b,pdfManager:a,newAnnotationsByPage:c})&&(y=null);const e=AnnotationFactory.generateImages(r.values(),g,a.evaluatorOptions.isOffscreenCanvasSupported),t=void 0===y?o:[];for(const[r,i]of c)t.push(a.getPage(r).then((t=>{const a=new WorkerTask(`Save (editor): page ${r}`);startWorkerTask(a);return t.saveNewAnnotations(u,a,i,e,s).finally((function(){finishWorkerTask(a)}))})));null===y?o.push(Promise.all(t).then((async()=>{await StructTreeRoot.createStructureTree({newAnnotationsByPage:c,xref:g,catalogRef:b,pdfManager:a,changes:s})}))):y&&o.push(Promise.all(t).then((async()=>{await y.updateStructureTree({newAnnotationsByPage:c,pdfManager:a,changes:s})})))}if(e)o.push(a.ensureDoc("serializeXfaData",[r]));else for(let e=0;ee.needAppearances)),k=h instanceof Dict&&h.get("XFA")||null;let C=null,v=!1;if(Array.isArray(k)){for(let e=0,t=k.length;e{g.resetNewTemporaryRef()}))}));u.on("GetOperatorList",(function(e,t){const r=e.pageIndex;a.getPage(r).then((function(a){const i=new WorkerTask(`GetOperatorList: page ${r}`);startWorkerTask(i);const n=s>=Ae?Date.now():0;a.getOperatorList({handler:u,sink:t,task:i,intent:e.intent,cacheKey:e.cacheKey,annotationStorage:e.annotationStorage,modifiedIds:e.modifiedIds}).then((function(e){finishWorkerTask(i);n&&info(`page=${r+1} - getOperatorList: time=${Date.now()-n}ms, len=${e.length}`);t.close()}),(function(e){finishWorkerTask(i);i.terminated||t.error(e)}))}))}));u.on("GetTextContent",(function(e,t){const{pageIndex:r,includeMarkedContent:i,disableNormalization:n}=e;a.getPage(r).then((function(e){const a=new WorkerTask("GetTextContent: page "+r);startWorkerTask(a);const o=s>=Ae?Date.now():0;e.extractTextContent({handler:u,task:a,sink:t,includeMarkedContent:i,disableNormalization:n}).then((function(){finishWorkerTask(a);o&&info(`page=${r+1} - getTextContent: time=`+(Date.now()-o)+"ms");t.close()}),(function(e){finishWorkerTask(a);a.terminated||t.error(e)}))}))}));u.on("GetStructTree",(function(e){return a.getPage(e.pageIndex).then((e=>a.ensure(e,"getStructTree")))}));u.on("FontFallback",(function(e){return a.fontFallback(e.id,u)}));u.on("Cleanup",(function(e){return a.cleanup(!0)}));u.on("Terminate",(function(e){r=!0;const t=[];if(a){a.terminate(new AbortException("Worker was terminated."));const e=a.cleanup();t.push(e);a=null}else clearGlobalCaches();i?.(new AbortException("Worker was terminated."));for(const e of n){t.push(e.finished);e.terminate()}return Promise.all(t).then((function(){u.destroy();u=null}))}));u.on("Ready",(function(t){setupDoc(e);e=null}));return h}static initializeFromPort(e){const t=new MessageHandler("worker","main",e);this.setup(t,e);t.send("ready",null)}}globalThis.pdfjsWorker={WorkerMessageHandler};export{WorkerMessageHandler}; \ No newline at end of file diff --git a/src/configs/AI-history.json b/src/configs/AI-history.json deleted file mode 100644 index 0637a08..0000000 --- a/src/configs/AI-history.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/src/configs/agenda-config.json b/src/configs/agenda-config.json deleted file mode 100644 index 89bea0a..0000000 --- a/src/configs/agenda-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "discardedItems": [] -} \ No newline at end of file diff --git a/src/configs/banking-allocations.json b/src/configs/banking-allocations.json deleted file mode 100644 index 0637a08..0000000 --- a/src/configs/banking-allocations.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/src/configs/banking-epochs.json b/src/configs/banking-epochs.json deleted file mode 100644 index fe51488..0000000 --- a/src/configs/banking-epochs.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/src/configs/config-manager.js b/src/configs/config-manager.js index c2aa18e..0861991 100644 --- a/src/configs/config-manager.js +++ b/src/configs/config-manager.js @@ -1,7 +1,58 @@ const fs = require('fs'); const path = require('path'); -const configFilePath = path.join(__dirname, 'oasis-config.json'); +const os = require('os'); +const legacyConfigPath = path.join(__dirname, 'oasis-config.json'); +const stateDir = process.env.OASIS_STATE_DIR || process.env.ssb_path || path.join(os.homedir(), '.ssb'); +const oasisDir = path.join(stateDir, 'oasis'); +const configFilePath = path.join(oasisDir, 'oasis-config.json'); +const defaultServerConfigPath = path.join(__dirname, 'server-config.json'); +const serverConfigFilePath = path.join(oasisDir, 'oasis-server-config.json'); +for (const [from, to] of [[path.join(stateDir, 'oasis-config.json'), configFilePath], [path.join(stateDir, 'oasis-server-config.json'), serverConfigFilePath]]) { + try { + if (!fs.existsSync(to) && fs.existsSync(from)) { + fs.mkdirSync(oasisDir, { recursive: true, mode: 0o700 }); + fs.renameSync(from, to); + } + } catch (_) {} +} +const writeStateFile = (file, data) => { + fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); + const tmp = `${file}.tmp-${process.pid}-${Date.now()}`; + fs.writeFileSync(tmp, data, { encoding: 'utf8', mode: 0o600 }); + fs.renameSync(tmp, file); + try { fs.chmodSync(file, 0o600); } catch (_) {} +}; +let configCache = { at: 0, cfg: null }; +const CONFIG_TTL_MS = 2000; +const writeConfigFile = (data) => { configCache = { at: 0, cfg: null }; return writeStateFile(configFilePath, data); }; +const readJsonFile = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; } }; +const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v); +const mergeDeep = (base, over) => { + if (!isPlainObject(base) || !isPlainObject(over)) return over === undefined ? base : over; + const out = { ...base }; + for (const [k, v] of Object.entries(over)) out[k] = isPlainObject(v) && isPlainObject(base[k]) ? mergeDeep(base[k], v) : v; + return out; +}; +if (!fs.existsSync(serverConfigFilePath)) { + const shipped = readJsonFile(defaultServerConfigPath); + const hops = shipped && shipped.friends ? shipped.friends.hops : undefined; + try { + if (shipped && shipped.pub === true) writeStateFile(serverConfigFilePath, JSON.stringify(shipped, null, 2)); + else if (Number.isFinite(hops) && hops !== 2) writeStateFile(serverConfigFilePath, JSON.stringify({ friends: { hops } }, null, 2)); + } catch (_) {} +} +const readServerConfig = () => mergeDeep(readJsonFile(defaultServerConfigPath) || {}, readJsonFile(serverConfigFilePath) || {}); +const saveServerConfig = (patch) => writeStateFile(serverConfigFilePath, JSON.stringify(mergeDeep(readJsonFile(serverConfigFilePath) || {}, patch), null, 2)); +const inviteCodesPath = path.join(__dirname, 'invite-codes.json'); +const readInviteCodes = () => { + const raw = readJsonFile(inviteCodesPath); + return (Array.isArray(raw) ? raw : raw ? [raw] : []).filter((e) => e && typeof e.code === 'string' && e.code.trim()); +}; +const defaultInvite = () => readInviteCodes()[0] || null; +if (!fs.existsSync(configFilePath) && fs.existsSync(legacyConfigPath)) { + try { writeConfigFile(fs.readFileSync(legacyConfigPath, 'utf8')); } catch (_) {} +} if (!fs.existsSync(configFilePath)) { const defaultConfig = { @@ -12,16 +63,20 @@ if (!fs.existsSync(configFilePath)) { "current": "blocks" }, "modules": { + "blogsMod": "on", + "housingMod": "off", "popularMod": "on", "topicsMod": "on", "summariesMod": "on", "latestMod": "on", "threadsMod": "on", "multiverseMod": "on", - "fediverseMod": "off", + "pollsMod": "on", + "fediverseMod": "on", "invitesMod": "on", "walletMod": "on", - "legacyMod": "on", + "backupMod": "on", + "devMod": "on", "cipherMod": "on", "bookmarksMod": "on", "videosMod": "on", @@ -38,6 +93,14 @@ if (!fs.existsSync(configFilePath)) { "reportsMod": "on", "opinionsMod": "on", "padsMod": "on", + "wikiMod": "on", + "emergenciesMod": "on", + "mailingMod": "on", + "roomsMod": "on", + "phoneMod": "on", + "logisticsMod": "on", + "podcastsMod": "on", + "campaignsMod": "on", "calendarsMod": "on", "transfersMod": "on", "feedMod": "on", @@ -51,7 +114,9 @@ if (!fs.existsSync(configFilePath)) { "jobsMod": "on", "shopsMod": "on", "projectsMod": "on", + "industryMod": "on", "bankingMod": "on", + "schoolMod": "on", "parliamentMod": "on", "courtsMod": "on", "favoritesMod": "on", @@ -59,6 +124,7 @@ if (!fs.existsSync(configFilePath)) { "mapsMod": "on", "chatsMod": "on", "torrentsMod": "on", + "filesMod": "on", "graphosMod": "on", "larpMod": "on" }, @@ -68,9 +134,6 @@ if (!fs.existsSync(configFilePath)) { "pass": "", "fee": "5" }, - "walletPub": { - "pubId": "" - }, "ai": { "prompt": "Provide an informative and precise response." }, @@ -78,25 +141,36 @@ if (!fs.existsSync(configFilePath)) { "limit": 2000 }, "homePage": "activity", - "bottomBarPins": ["search", "inbox", "publish"], "language": "en", "wish": "whole", "pmVisibility": "whole", + "phone": { "visibility": "mutuals", "dnd": false, "relay": true }, "lanBroadcasting": true }; - fs.writeFileSync(configFilePath, JSON.stringify(defaultConfig, null, 2)); + writeConfigFile(JSON.stringify(defaultConfig, null, 2)); } const getConfig = () => { + const now = Date.now(); + if (!configCache.cfg || now - configCache.at > CONFIG_TTL_MS) configCache = { at: now, cfg: readConfig() }; + return structuredClone(configCache.cfg); +}; + +const readConfig = () => { const configData = fs.readFileSync(configFilePath); const cfg = JSON.parse(configData); - if (!['whole', 'mutuals', 'only-lan'].includes(cfg.wish)) cfg.wish = 'whole'; - if (cfg.pmVisibility !== 'whole' && cfg.pmVisibility !== 'mutuals') cfg.pmVisibility = 'whole'; + if (!['whole', 'mutuals', 'only-lan', 'local'].includes(cfg.wish)) cfg.wish = 'whole'; + if (!['whole', 'mutuals', 'following'].includes(cfg.pmVisibility)) cfg.pmVisibility = 'whole'; + if (!['whole', 'mutuals', 'following'].includes(cfg.mentionsVisibility)) cfg.mentionsVisibility = 'whole'; + if (!cfg.phone || typeof cfg.phone !== 'object') cfg.phone = {}; + cfg.phone = { visibility: cfg.phone.visibility === 'whole' ? 'whole' : 'mutuals', dnd: cfg.phone.dnd === true, relay: cfg.phone.relay !== false, video: cfg.phone.video !== false, ...(typeof cfg.phone.videoDevice === 'string' && cfg.phone.videoDevice.trim() ? { videoDevice: cfg.phone.videoDevice.trim() } : {}) }; if (typeof cfg.ux === 'string') cfg.ux = { current: cfg.ux }; if (!cfg.ux || typeof cfg.ux !== 'object') cfg.ux = { current: 'blocks' }; if (cfg.ux.current === 'menus') cfg.ux.current = 'blocks'; - if (cfg.ux.current !== 'blocks' && cfg.ux.current !== 'ainav') cfg.ux.current = 'blocks'; + if (cfg.ux.current !== 'blocks' && cfg.ux.current !== 'ainav' && cfg.ux.current !== 'chats' && cfg.ux.current !== 'feed' && cfg.ux.current !== 'phone' && cfg.ux.current !== 'rooms') cfg.ux.current = 'blocks'; if (cfg.ux.current === 'ainav' && cfg.modules && cfg.modules.aiNavMod !== 'on') cfg.ux.current = 'blocks'; + if (cfg.ux.current === 'chats' && cfg.modules && cfg.modules.chatsMod !== 'on') cfg.ux.current = 'blocks'; + let pins = Array.isArray(cfg.bottomBarPins) ? cfg.bottomBarPins : (typeof cfg.bottomBarPin === 'string' @@ -106,14 +180,31 @@ const getConfig = () => { pins = pins.filter((k, i) => pins.indexOf(k) === i).slice(0, 4); cfg.bottomBarPins = pins; delete cfg.bottomBarPin; + if (cfg.ux.current === 'phone' && cfg.modules && cfg.modules.phoneMod === 'off') cfg.ux.current = 'blocks'; + if (cfg.ux.current === 'feed' && cfg.modules && cfg.modules.feedMod === 'off') cfg.ux.current = 'blocks'; + if (cfg.ux.current === 'rooms' && cfg.modules && cfg.modules.roomsMod === 'off') cfg.ux.current = 'blocks'; + if (!cfg.blobCache || typeof cfg.blobCache !== 'object' || !Number.isFinite(Number(cfg.blobCache.maxMB))) cfg.blobCache = { maxMB: 0 }; + if (!Number.isFinite(Number(cfg.blobCache.pubMaxMB))) cfg.blobCache.pubMaxMB = 0; + if (cfg.modules && typeof cfg.modules === 'object') { + if (cfg.modules.backupMod === undefined) cfg.modules.backupMod = cfg.modules.legacyMod === 'off' ? 'off' : 'on'; + for (const mod of ['wikiMod', 'emergenciesMod', 'mailingMod', 'logisticsMod', 'podcastsMod', 'campaignsMod', 'filesMod', 'phoneMod', 'roomsMod']) { + if (cfg.modules[mod] === undefined) cfg.modules[mod] = 'on'; + } + } return cfg; }; const saveConfig = (newConfig) => { - fs.writeFileSync(configFilePath, JSON.stringify(newConfig, null, 2)); + writeConfigFile(JSON.stringify(newConfig, null, 2)); }; module.exports = { + configFilePath, + serverConfigFilePath, getConfig, saveConfig, + readServerConfig, + saveServerConfig, + readInviteCodes, + defaultInvite, }; diff --git a/src/configs/follow_state.json b/src/configs/follow_state.json deleted file mode 100644 index e66a46e..0000000 --- a/src/configs/follow_state.json +++ /dev/null @@ -1 +0,0 @@ -{"pending":[],"accepted":[],"lastAcceptMs":0} diff --git a/src/configs/invite-codes.json b/src/configs/invite-codes.json new file mode 100644 index 0000000..c5691fe --- /dev/null +++ b/src/configs/invite-codes.json @@ -0,0 +1,23 @@ +[ + { + "name": "SNH \"La Plaza\"", + "description": "A shared place to begin an experimental utopia ...", + "url": "https://solarnethub.com", + "code": "pub.solarnethub.com:8008:@0qSCyK3xyL71X4qKkmf84Cb2riP6OeUqxCvbP2Z6HWs=.ed25519~7mC1c1gBJe8pmnWI9eIUYpFtEPUOiuHAxbzfxP/f0Ak=", + "createdAt": "2026-05-24T00:00:00.000Z" + }, + { + "name": "t0k10 h4x0rs", + "description": "n0 l0gin. n0 m41l. n0 cl0ud...", + "url": "https://pub.elhack.net", + "code": "pub.elhack.net:8008:@R8WWz72Pz66Jw0SObjxe9Boio5WzskGpTK7wsPzeUEc=.ed25519~HaBhhmtYnYYfBA2j+JoYnckWXhPSzFGn0TCY1I9rP98=", + "createdAt": "2026-08-11T00:00:00.000Z" + }, + { + "name": "Acampada 26s", + "description": "Red de la acampada en Madrid del #26s", + "url": "https://acampada26s.net", + "code": "madrid.acampada26s.net:8008:@Wt9tL9XtJCb7sZjX0+FtDlzziFLumWo1DAbou4Q5m3o=.ed25519~tkAoj1gE6dX8AcAOLQXs22VWFWKfOeJdT+7pBPfgbDA=", + "createdAt": "2026-09-26T00:00:00.000Z" + } +] diff --git a/src/configs/media-favorites.json b/src/configs/media-favorites.json deleted file mode 100644 index 8c9d7c2..0000000 --- a/src/configs/media-favorites.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "audios": [], - "bookmarks": [], - "calendars": [], - "chats": [], - "documents": [], - "images": [], - "maps": [], - "pads": [], - "shops": [], - "torrents": [], - "videos": [] -} diff --git a/src/configs/oasis-config.json b/src/configs/oasis-config.json index 4e835c6..2ee9846 100644 --- a/src/configs/oasis-config.json +++ b/src/configs/oasis-config.json @@ -1,66 +1,75 @@ { "themes": { - "current": "OasisMobile" + "current": "Acampada" }, "ux": { "current": "blocks" }, "modules": { - "popularMod": "on", - "topicsMod": "on", - "summariesMod": "on", - "latestMod": "on", - "threadsMod": "on", - "multiverseMod": "on", - "fediverseMod": "on", + "blogsMod": "on", + "pollsMod": "off", + "fediverseMod": "off", "invitesMod": "on", "walletMod": "off", - "legacyMod": "off", - "cipherMod": "on", - "bookmarksMod": "on", + "devMod": "off", + "cipherMod": "off", + "bookmarksMod": "off", "videosMod": "on", "docsMod": "off", - "audiosMod": "on", - "tagsMod": "on", + "audiosMod": "off", + "tagsMod": "off", "imagesMod": "on", - "trendingMod": "on", + "trendingMod": "off", "eventsMod": "on", - "tasksMod": "on", - "marketMod": "on", + "tasksMod": "off", + "marketMod": "off", "votesMod": "on", "tribesMod": "on", - "reportsMod": "on", - "opinionsMod": "on", + "reportsMod": "off", + "opinionsMod": "off", "padsMod": "on", - "calendarsMod": "on", + "calendarsMod": "off", "transfersMod": "off", "feedMod": "on", - "pixeliaMod": "on", - "melodyMod": "on", - "agendaMod": "on", + "pixeliaMod": "off", + "melodyMod": "off", + "agendaMod": "off", "aiMod": "off", "aiNavMod": "off", "forumMod": "on", "gamesMod": "off", - "jobsMod": "on", - "shopsMod": "on", - "projectsMod": "on", + "housingMod": "off", + "jobsMod": "off", + "shopsMod": "off", + "projectsMod": "off", + "industryMod": "off", "bankingMod": "off", + "schoolMod": "off", "parliamentMod": "on", "courtsMod": "off", - "favoritesMod": "on", + "favoritesMod": "off", "logsMod": "off", "mapsMod": "off", "chatsMod": "on", "torrentsMod": "off", - "graphosMod": "on", - "larpMod": "on" + "filesMod": "off", + "phoneMod": "on", + "roomsMod": "on", + "graphosMod": "off", + "larpMod": "off", + "backupMod": "on", + "campaignsMod": "off", + "emergenciesMod": "off", + "logisticsMod": "off", + "mailingMod": "off", + "podcastsMod": "off", + "wikiMod": "on" }, "wallet": { - "url": "http://localhost:7474", + "url": "http://127.0.0.1:7474", "user": "", "pass": "", - "fee": "5" + "fee": "0.01" }, "walletPub": { "pubId": "" @@ -75,5 +84,29 @@ "language": "en", "wish": "whole", "pmVisibility": "whole", - "lanBroadcasting": true -} \ No newline at end of file + "lanBroadcasting": true, + "phone": { + "visibility": "whole", + "dnd": false, + "relay": true + }, + "blobCache": { + "maxMB": 0, + "pubMaxMB": 0 + }, + "bottomBarPins": [ + "activity", + "chats", + "publish", + "inbox" + ], + "rtc": { + "stun": [], + "turn": { + "urls": [], + "secret": "", + "ttlSec": 3600 + }, + "relayOnly": false + } +} diff --git a/src/configs/server-config.json b/src/configs/server-config.json index 7779f6f..5b3060b 100644 --- a/src/configs/server-config.json +++ b/src/configs/server-config.json @@ -11,13 +11,6 @@ "dunbar": 300, "hops": 2 }, - "gossip": { - "connections": 20, - "local": true, - "friends": true, - "seed": true, - "global": true - }, "replicationScheduler": { "autostart": true, "partialReplication": null @@ -54,7 +47,11 @@ } ], "tunnel": [], - "onion": [], + "onion": [ + { + "transform": "shs" + } + ], "ws": [] } } diff --git a/src/configs/shared-state.js b/src/configs/shared-state.js index dfed74b..ad758e3 100644 --- a/src/configs/shared-state.js +++ b/src/configs/shared-state.js @@ -1,25 +1,65 @@ let _inboxCount = 0; +let _phoneCount = 0; +let _phoneCall = null; +let _phoneRoom = null; +let _pubIds = new Set(); +let _inboxPmCount = 0; +let _inboxNotifCount = 0; let _carbonHcT = 0; +let _walletReady = false; +let _donatableAuthors = new Set(); let _carbonHcH = 0; let _lastRefresh = 0; let _onlinePeers = null; +let _syncedPeers = null; let _inboxUnread = null; let _lastSyncTs = null; let _ecoValue = null; let _lastActivity = null; let _maxBlockBytes = 0; let _inhabitantCount = 0; +let _tribesCount = 0; +let _mentionsCount = 0; +let _mentionsTotal = 0; +let _bestMatch = null; +let _matchPool = []; +let _matchIdx = -1; +let _sectionMatches = new Map(); +const _dismissedSuggestions = new Set(); +let _suggestionsQuiet = false; +let _featuredEmergency = null; +let _dismissedEmergency = null; + module.exports = { getInboxCount: () => _inboxCount, setInboxCount: (n) => { _inboxCount = n; }, + getPhoneCount: () => _phoneCount, + setPhoneCount: (n) => { _phoneCount = Number(n) || 0; }, + getPhoneCall: () => _phoneCall, + setPhoneCall: (st) => { _phoneCall = st || null; }, + getPhoneRoom: () => _phoneRoom, + setPhoneRoom: (st) => { _phoneRoom = st || null; }, + getPubIds: () => _pubIds, + setPubIds: (set) => { _pubIds = set instanceof Set ? set : new Set(); }, + isPubId: (id) => _pubIds.has(String(id || '')), + getInboxPmCount: () => _inboxPmCount, + setInboxPmCount: (n) => { _inboxPmCount = n; }, + getInboxNotifCount: () => _inboxNotifCount, + setInboxNotifCount: (n) => { _inboxNotifCount = n; }, getCarbonHcT: () => _carbonHcT, setCarbonHcT: (n) => { _carbonHcT = n; }, + getDonatableAuthors: () => _donatableAuthors, + setDonatableAuthors: (set) => { _donatableAuthors = set instanceof Set ? set : new Set(); }, + getWalletReady: () => _walletReady, + setWalletReady: (v) => { _walletReady = !!v; }, getCarbonHcH: () => _carbonHcH, setCarbonHcH: (n) => { _carbonHcH = n; }, getLastRefresh: () => _lastRefresh, setLastRefresh: (t) => { _lastRefresh = t; }, getOnlinePeerCount: () => _onlinePeers, setOnlinePeerCount: (n) => { _onlinePeers = n; }, + getSyncedPeerCount: () => _syncedPeers, + setSyncedPeerCount: (n) => { _syncedPeers = Math.max(0, Number(n) || 0); }, getInboxUnreadCount: () => _inboxUnread, setInboxUnreadCount: (n) => { _inboxUnread = n; }, getLastSyncTs: () => _lastSyncTs, @@ -30,6 +70,36 @@ module.exports = { setLastActivity: (a) => { _lastActivity = a; }, getMaxBlockBytes: () => _maxBlockBytes, setMaxBlockBytes: (n) => { if (Number(n) > _maxBlockBytes) _maxBlockBytes = Number(n); }, + getMentionsTotal: () => _mentionsTotal, + setMentionsTotal: (n) => { _mentionsTotal = Math.max(0, Number(n) || 0); }, + getMentionsCount: () => _mentionsCount, + setMentionsCount: (n) => { _mentionsCount = Math.max(0, Number(n) || 0); }, getInhabitantCount: () => _inhabitantCount, - setInhabitantCount: (n) => { _inhabitantCount = Math.max(0, Number(n) || 0); } + setInhabitantCount: (n) => { _inhabitantCount = Math.max(0, Number(n) || 0); }, + getTribesCount: () => _tribesCount, + setTribesCount: (n) => { _tribesCount = Math.max(0, Number(n) || 0); }, + getFeaturedEmergency: () => _featuredEmergency, + setFeaturedEmergency: (a) => { _featuredEmergency = a || null; }, + getDismissedEmergency: () => _dismissedEmergency, + setDismissedEmergency: (id) => { _dismissedEmergency = id || null; }, + getBestMatch: () => _bestMatch, + setBestMatch: (m) => { _bestMatch = m || null; }, + setMatchPool: (pool) => { + const before = new Set(_matchPool.map(m => m.href)); + _matchPool = Array.isArray(pool) ? pool.filter(m => m && m.href) : []; + if (_matchPool.some(m => !before.has(m.href) && !_dismissedSuggestions.has(m.href))) _suggestionsQuiet = false; + }, + nextBestMatch: () => { + const avail = _matchPool.filter(m => !_dismissedSuggestions.has(m.href)); + if (!avail.length) { _bestMatch = null; return null; } + _matchIdx = (_matchIdx + 1) % avail.length; + _bestMatch = avail[_matchIdx]; + return _bestMatch; + }, + setSectionMatches: (map) => { _sectionMatches = map instanceof Map ? map : new Map(); }, + getSectionMatch: (section) => (_sectionMatches.get(String(section || '')) || []).find(m => !_dismissedSuggestions.has(m.href)) || null, + isKnownSuggestion: (href) => !!href && (_matchPool.some(m => m.href === href) || [..._sectionMatches.values()].some(list => list.some(m => m.href === href))), + dismissSuggestion: (href) => { if (href) { _dismissedSuggestions.add(href); _suggestionsQuiet = true; } }, + areSuggestionsQuiet: () => _suggestionsQuiet, + isSuggestionDismissed: (href) => _dismissedSuggestions.has(href) }; diff --git a/src/configs/snh-invite-code.json b/src/configs/snh-invite-code.json deleted file mode 100644 index 545d3b1..0000000 --- a/src/configs/snh-invite-code.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "SNH \"La Plaza\"", - "description": "A shared place to begin a utopia ...", - "url": "https://pub.solarnethub.com", - "code": "pub.solarnethub.com:8008:@0qSCyK3xyL71X4qKkmf84Cb2riP6OeUqxCvbP2Z6HWs=.ed25519~7mC1c1gBJe8pmnWI9eIUYpFtEPUOiuHAxbzfxP/f0Ak=", - "createdAt": "2026-05-24T00:00:00.000Z" -} diff --git a/src/configs/state-manager.js b/src/configs/state-manager.js new file mode 100644 index 0000000..5a2aa7e --- /dev/null +++ b/src/configs/state-manager.js @@ -0,0 +1,113 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const LEGACY_CONFIG_DIR = __dirname; +const ROOT = 'oasis'; + +const FOLDERS = { + 'wallet-addresses.json': 'banking', + 'banking-address-book.json': 'banking', + 'banking-allocations.json': 'banking', + 'banking-eco-history.json': 'banking', + 'banking-epochs.json': 'banking', + 'banking-ubi-notice.json': 'banking', + 'banking-confirm-notice.json': 'banking', + 'banking-ubi-paid.json': 'banking', + 'banking-funds-history.json': 'banking', + 'content_favorites.json': 'content', + 'blob-access.json': 'content', + 'follow_state.json': 'content', + 'viewer_hidden.json': 'content', + 'invites-used.json': 'content', + 'agenda-config.json': 'content', + 'AI-history.json': 'ai', + 'AI-vectors.json': 'ai', + 'AI-search-vectors.json': 'ai', + 'fediverse-accounts.json': 'multiverse', + 'nextcloud-account.json': 'multiverse', + 'oasis-backup.json': 'backup', + 'oasis-first-contact': 'flags', + 'oasis-mentions-seen': 'flags', + 'oasis-inbox-read': 'flags', + 'oasis-inbox-archived': 'flags', + 'oasis-political-seen': 'flags', + 'gossip_unfollowed.json': 'peers', + 'lan-peers.json': 'peers', + 'peer-health.json': 'peers', + 'phone-history.json': 'phone', + 'phone-seen.json': 'phone', + 'snapshot.oasissn': 'content', + 'snapshot-recent.oasissn': 'content' +}; + +const ALIASES = { + 'oasis-backup.json': ['backup.json'] +}; + +const ssbDir = () => { + try { + const resolved = require('../server/ssb_config').statePath('.'); + if (resolved) return path.resolve(resolved); + } catch (_) {} + if (process.env.OASIS_STATE_DIR) return process.env.OASIS_STATE_DIR; + return path.join(os.homedir(), '.ssb'); +}; + +const stateDir = (name) => path.join(ssbDir(), ROOT, FOLDERS[name] || ''); + +const statePath = (name) => { + const dir = stateDir(name); + const target = path.join(dir, name); + try { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + if (!fs.existsSync(target)) { + const candidates = [name, ...(ALIASES[name] || [])] + .flatMap(n => [path.join(ssbDir(), n), path.join(LEGACY_CONFIG_DIR, n)]); + const legacy = candidates.find(p => fs.existsSync(p)); + if (legacy) { + try { fs.renameSync(legacy, target); } + catch (_) { fs.copyFileSync(legacy, target); try { fs.unlinkSync(legacy); } catch (_) {} } + } + } + } catch (_) {} + return target; +}; + +const keysDir = (base = null) => { + const root = ssbDir(); + if (base && path.resolve(base) !== root) return path.join(base, 'keys'); + const target = path.join(root, ROOT, 'keys'); + try { + if (!fs.existsSync(target)) { + fs.mkdirSync(path.dirname(target), { recursive: true }); + const legacy = path.join(root, 'keys'); + if (fs.existsSync(legacy)) fs.renameSync(legacy, target); + else fs.mkdirSync(target, { recursive: true, mode: 0o700 }); + } + } catch (_) {} + return target; +}; + +const migrateAll = () => { + for (const name of Object.keys(FOLDERS)) { + try { + const legacy = [name, ...(ALIASES[name] || [])] + .flatMap(n => [path.join(ssbDir(), n), path.join(LEGACY_CONFIG_DIR, n)]) + .find(p => fs.existsSync(p)); + if (legacy) statePath(name); + } catch (_) {} + } + for (const name of Object.keys(FOLDERS)) { + if (FOLDERS[name] !== 'flags' || name.endsWith('.json')) continue; + const legacy = path.join(ssbDir(), name); + const target = path.join(stateDir(name), name); + try { if (fs.existsSync(legacy) && fs.existsSync(target)) fs.unlinkSync(legacy); } catch (_) {} + } + for (const leftover of ['keys', 'node_modules']) { + const dir = path.join(ssbDir(), leftover); + try { if (fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir); } catch (_) {} + } +}; + +module.exports = { statePath, stateDir, ssbDir, keysDir, migrateAll }; diff --git a/src/configs/wallet-addresses.json b/src/configs/wallet-addresses.json deleted file mode 100644 index 0967ef4..0000000 --- a/src/configs/wallet-addresses.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/games/minesweeper/index.html b/src/games/minesweeper/index.html new file mode 100644 index 0000000..a415d6b --- /dev/null +++ b/src/games/minesweeper/index.html @@ -0,0 +1,241 @@ + + + + + +Minesweeper + + + +
+ ← Back to Games + MINESWEEPER +
+
+ LEVEL 1 + MINES 0 + TIME 0 + SCORE 0 + BEST - +
+
+
Click any cell to start
+
+ + +
+
Click — reveal  |  Right click — flag  |  Clear the board to level up
+ + + + diff --git a/src/games/minesweeper/thumbnail.svg b/src/games/minesweeper/thumbnail.svg new file mode 100644 index 0000000..97f31cd --- /dev/null +++ b/src/games/minesweeper/thumbnail.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 2 + 1 + 2 + 3 + 2 + 1 + 1 + 1 + 1 + + + + + + + + + + + + + MINESWEEPER + diff --git a/src/games/visualdj/index.html b/src/games/visualdj/index.html new file mode 100644 index 0000000..f2a2664 --- /dev/null +++ b/src/games/visualdj/index.html @@ -0,0 +1,945 @@ + + + + + + Visual DJ + + + +
+ ← Back to Games + VISUAL DJ +
+
+
+
Status: Paused | FPS: -- | BPM: 120 | Time: 0.0 s
+
Deck A: Plasma | Deck B: Tunnel | Mix: 50% A/B | Blend: Screen | Mic: Off
+
+ + + + + + +
+
+ +
+
+ Live controls:
+ Drag on the stage to move the crossfader, click to flash.
+ 1-8 pattern on deck A · Q-I pattern on deck B · Z-/ effects · ← → crossfade · Space flash · T tap tempo · P play/pause · F fullscreen · Shift+1-4 scenes +
+
+ +
+
+
+

Deck A

+
+
+ + + + + + + + + + +
+
+ +
+

Deck B

+
+
+ + + + + + + + + + +
+
+
+ +
+

Mixer

+
+ + + + + + + + + + + + + + + + +
+
+ +
+

Effects

+
+
+ + + + +
+
+ +
+ Scenes + +
+
+
+
+
+ + + + diff --git a/src/games/visualdj/thumbnail.svg b/src/games/visualdj/thumbnail.svg new file mode 100644 index 0000000..677c916 --- /dev/null +++ b/src/games/visualdj/thumbnail.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + VISUALDJ + diff --git a/src/maps/data/coast_50m.json b/src/maps/data/coast_50m.json new file mode 100644 index 0000000..25d07e1 --- /dev/null +++ b/src/maps/data/coast_50m.json @@ -0,0 +1 @@ +{"f":[{"b":[178.497,-16.976,180,-16.153],"l":[180,-16.153,179.848,-16.214,179.789,-16.221,179.715,-16.208,179.635,-16.223,179.552,-16.25,179.475,-16.294,179.359,-16.38,179.294,-16.399,179.225,-16.405,179.091,-16.437,178.961,-16.483,178.866,-16.54,178.805,-16.631,178.745,-16.634,178.686,-16.666,178.634,-16.649,178.584,-16.622,178.568,-16.664,178.542,-16.7,178.514,-16.726,178.497,-16.788,178.604,-16.801,178.638,-16.851,178.665,-16.92,178.707,-16.976,178.803,-16.952,178.884,-16.886,178.95,-16.904,179.007,-16.9,179.055,-16.814,179.202,-16.713,179.3,-16.71,179.323,-16.718,179.346,-16.77,179.375,-16.792,179.419,-16.807,179.465,-16.806,179.589,-16.787,179.715,-16.744,179.821,-16.737,179.928,-16.744,179.89,-16.667,179.906,-16.584,179.927,-16.552,179.93,-16.519,179.885,-16.518,179.841,-16.537,179.697,-16.632,179.568,-16.747,179.564,-16.637,179.619,-16.528,179.748,-16.446,179.794,-16.37,179.848,-16.302,179.999,-16.169]},{"b":[177.183,-17.164,177.287,-17.049],"l":[177.258,-17.054,177.287,-17.049,177.276,-17.105,177.234,-17.147,177.183,-17.164,177.21,-17.084,177.239,-17.059,177.258,-17.054]},{"b":[127.286,0.758,127.373,0.862],"l":[127.373,0.791,127.354,0.847,127.32,0.862,127.293,0.842,127.286,0.812,127.306,0.769,127.338,0.758,127.373,0.791]},{"b":[-81.422,24.63,-81.32,24.75],"l":[-81.322,24.685,-81.42,24.75,-81.422,24.733,-81.379,24.666,-81.379,24.636,-81.365,24.63,-81.335,24.65,-81.32,24.668,-81.322,24.685]},{"b":[-80.848,24.804,-80.785,24.846],"l":[-80.799,24.846,-80.839,24.818,-80.848,24.804,-80.829,24.804,-80.787,24.821,-80.785,24.835,-80.799,24.846]},{"b":[-80.665,24.898,-80.615,24.941],"l":[-80.626,24.941,-80.665,24.898,-80.638,24.903,-80.615,24.938,-80.626,24.941]},{"b":[113.185,-39.146,153.617,-10.707],"l":[129.569,-31.627,129.188,-31.66,128.946,-31.703,128.546,-31.888,128.068,-32.067,127.678,-32.151,127.32,-32.264,127.084,-32.297,126.779,-32.311,126.137,-32.257,125.917,-32.297,125.567,-32.506,125.464,-32.557,125.267,-32.614,124.759,-32.883,124.525,-32.94,124.373,-32.958,124.244,-33.015,124.126,-33.129,123.967,-33.446,123.868,-33.596,123.65,-33.836,123.507,-33.916,123.365,-33.905,123.208,-33.988,123.068,-33.901,122.956,-33.884,122.778,-33.891,122.151,-33.992,122.061,-33.874,121.946,-33.857,121.73,-33.862,121.405,-33.827,120.815,-33.871,120.531,-33.92,120.418,-33.963,120.209,-33.935,119.854,-33.975,119.729,-34.042,119.635,-34.101,119.451,-34.368,119.248,-34.456,119.081,-34.459,118.895,-34.48,118.52,-34.737,118.136,-34.987,118.006,-35.013,117.863,-35.055,117.675,-35.075,117.582,-35.098,117.144,-35.034,116.865,-35.027,116.517,-34.988,116.217,-34.866,115.987,-34.795,115.726,-34.526,115.565,-34.426,115.278,-34.304,115.195,-34.308,115.128,-34.342,115.009,-34.256,115.006,-34.145,114.973,-34.051,114.976,-33.804,114.994,-33.515,115.099,-33.58,115.182,-33.643,115.359,-33.64,115.515,-33.531,115.604,-33.372,115.683,-33.193,115.671,-33.002,115.619,-32.667,115.654,-32.597,115.708,-32.568,115.725,-32.401,115.738,-31.888,115.698,-31.695,115.455,-31.303,115.294,-30.962,115.177,-30.808,115.078,-30.56,114.995,-30.216,114.969,-30.042,114.942,-29.722,114.971,-29.54,114.959,-29.434,114.857,-29.143,114.628,-28.872,114.591,-28.772,114.592,-28.666,114.537,-28.543,114.354,-28.295,114.165,-28.081,114.133,-27.976,114.098,-27.544,114.028,-27.347,113.709,-26.848,113.333,-26.417,113.231,-26.241,113.185,-26.182,113.211,-26.174,113.253,-26.197,113.3,-26.24,113.323,-26.244,113.345,-26.208,113.343,-26.126,113.356,-26.08,113.389,-26.106,113.427,-26.198,113.547,-26.437,113.582,-26.558,113.734,-26.595,113.78,-26.563,113.836,-26.501,113.853,-26.332,113.776,-26.256,113.706,-26.224,113.589,-26.099,113.513,-25.898,113.395,-25.713,113.397,-25.647,113.451,-25.599,113.539,-25.625,113.621,-25.732,113.713,-25.831,113.698,-26.004,113.684,-26.052,113.692,-26.092,113.724,-26.13,113.766,-26.16,113.812,-26.116,113.854,-26.014,113.88,-26.028,113.942,-26.259,113.992,-26.321,114.09,-26.394,114.176,-26.337,114.216,-26.289,114.203,-26.126,114.229,-25.969,114.214,-25.852,113.993,-25.545,113.792,-25.166,113.671,-24.977,113.569,-24.693,113.504,-24.595,113.418,-24.436,113.413,-24.254,113.421,-24.132,113.49,-23.87,113.553,-23.733,113.757,-23.418,113.767,-23.283,113.765,-23.18,113.795,-23.024,113.795,-22.915,113.768,-22.813,113.683,-22.638,113.795,-22.332,113.958,-21.939,114.023,-21.881,114.124,-21.829,114.143,-21.91,114.093,-22.181,114.164,-22.323,114.142,-22.483,114.205,-22.456,114.304,-22.425,114.378,-22.342,114.417,-22.261,114.603,-21.942,114.709,-21.823,114.859,-21.736,115.162,-21.631,115.456,-21.492,115.596,-21.358,115.771,-21.242,115.894,-21.117,116.011,-21.03,116.606,-20.713,116.707,-20.654,116.836,-20.647,116.995,-20.658,117.139,-20.641,117.293,-20.713,117.406,-20.721,117.684,-20.643,117.832,-20.573,118.087,-20.419,118.199,-20.375,118.458,-20.327,118.751,-20.262,119.104,-19.995,119.359,-20.012,119.586,-20.038,119.768,-19.958,120.196,-19.909,120.434,-19.842,120.878,-19.665,120.998,-19.604,121.18,-19.478,121.338,-19.32,121.494,-19.106,121.589,-18.915,121.631,-18.817,121.722,-18.66,121.785,-18.536,121.834,-18.477,122.006,-18.394,122.262,-18.159,122.345,-18.112,122.361,-18.037,122.306,-17.995,122.237,-17.969,122.191,-17.72,122.147,-17.549,122.143,-17.428,122.16,-17.314,122.261,-17.136,122.333,-17.059,122.432,-16.97,122.523,-16.943,122.598,-16.865,122.72,-16.788,122.772,-16.71,122.848,-16.552,122.917,-16.433,122.971,-16.437,123.074,-16.715,123.142,-16.863,123.266,-17.037,123.383,-17.293,123.479,-17.41,123.525,-17.486,123.563,-17.521,123.571,-17.472,123.562,-17.415,123.608,-17.22,123.586,-17.083,123.594,-17.03,123.618,-17.008,123.664,-17.023,123.754,-17.1,123.799,-17.127,123.831,-17.121,123.829,-16.997,123.874,-16.919,123.856,-16.865,123.778,-16.868,123.745,-16.801,123.68,-16.724,123.607,-16.668,123.518,-16.541,123.49,-16.491,123.525,-16.468,123.581,-16.471,123.626,-16.416,123.646,-16.343,123.607,-16.224,123.647,-16.18,123.729,-16.192,123.859,-16.382,123.915,-16.364,123.961,-16.287,124.044,-16.265,124.13,-16.279,124.186,-16.334,124.3,-16.388,124.453,-16.382,124.53,-16.395,124.692,-16.386,124.772,-16.403,124.757,-16.373,124.669,-16.339,124.57,-16.332,124.454,-16.335,124.405,-16.299,124.388,-16.203,124.416,-16.133,124.435,-16.104,124.51,-16.116,124.577,-16.114,124.585,-16.02,124.609,-15.937,124.649,-15.87,124.648,-15.805,124.607,-15.823,124.504,-15.972,124.455,-15.851,124.382,-15.758,124.397,-15.626,124.44,-15.494,124.506,-15.475,124.562,-15.496,124.644,-15.419,124.691,-15.36,124.68,-15.311,124.693,-15.274,124.75,-15.285,124.972,-15.404,125.016,-15.467,125.063,-15.442,125.078,-15.375,125.073,-15.307,125.024,-15.317,124.909,-15.31,124.883,-15.272,124.893,-15.241,124.839,-15.161,124.914,-15.11,124.979,-15.107,125.023,-15.072,125.024,-15.024,125.038,-15.004,125.073,-15.032,125.189,-15.045,125.302,-15.107,125.356,-15.12,125.376,-15.087,125.384,-15.016,125.243,-14.945,125.239,-14.875,125.18,-14.794,125.179,-14.715,125.267,-14.648,125.285,-14.584,125.335,-14.558,125.436,-14.557,125.504,-14.502,125.58,-14.483,125.598,-14.362,125.597,-14.278,125.628,-14.257,125.705,-14.291,125.681,-14.388,125.681,-14.48,125.662,-14.529,125.691,-14.525,125.708,-14.505,125.738,-14.444,125.82,-14.469,125.84,-14.534,125.85,-14.597,125.891,-14.618,125.946,-14.52,126.021,-14.495,126.017,-14.371,126.045,-14.283,126.054,-14.217,126.101,-14.184,126.111,-14.114,126.073,-14.066,126.054,-13.977,126.119,-13.958,126.184,-14.002,126.228,-14.113,126.258,-14.164,126.299,-14.136,126.323,-14.062,126.403,-14.019,126.482,-14.079,126.57,-14.161,126.679,-14.089,126.781,-13.955,126.764,-13.873,126.776,-13.788,126.903,-13.744,127.006,-13.777,127.099,-13.867,127.293,-13.935,127.458,-14.031,127.531,-14.095,127.673,-14.195,127.763,-14.299,127.888,-14.485,128.18,-14.712,128.199,-14.752,128.16,-14.827,128.124,-14.924,128.08,-15.088,128.069,-15.329,128.112,-15.312,128.155,-15.226,128.202,-15.243,128.255,-15.299,128.259,-15.246,128.227,-15.214,128.173,-15.102,128.175,-15.043,128.218,-14.996,128.285,-14.939,128.358,-14.902,128.403,-14.869,128.41,-14.829,128.477,-14.788,128.576,-14.775,128.636,-14.781,129.058,-14.884,129.165,-14.988,129.175,-15.115,129.216,-15.16,129.238,-15.08,129.234,-14.906,129.268,-14.871,129.381,-14.898,129.459,-14.933,129.567,-15.047,129.588,-15.103,129.635,-15.14,129.65,-15.087,129.628,-15.012,129.613,-14.926,129.637,-14.851,129.763,-14.845,129.849,-14.829,129.808,-14.8,129.754,-14.79,129.663,-14.721,129.605,-14.647,129.699,-14.575,129.698,-14.557,129.608,-14.56,129.484,-14.49,129.379,-14.392,129.459,-14.213,129.62,-14.038,129.71,-13.98,129.718,-13.921,129.762,-13.812,129.789,-13.72,129.797,-13.648,129.839,-13.573,129.938,-13.502,130.073,-13.476,130.136,-13.448,130.199,-13.383,130.26,-13.302,130.135,-13.146,130.145,-13.059,130.168,-12.957,130.318,-12.883,130.4,-12.688,130.454,-12.659,130.572,-12.664,130.617,-12.647,130.61,-12.491,130.623,-12.431,130.672,-12.407,130.736,-12.428,130.777,-12.495,130.867,-12.558,130.898,-12.524,130.883,-12.455,130.874,-12.367,130.957,-12.348,131.023,-12.343,131.03,-12.271,131.02,-12.214,131.046,-12.19,131.22,-12.178,131.265,-12.119,131.292,-12.068,131.314,-12.096,131.342,-12.21,131.438,-12.277,131.726,-12.278,131.888,-12.232,131.957,-12.259,132.064,-12.281,132.182,-12.227,132.253,-12.186,132.372,-12.239,132.411,-12.295,132.442,-12.176,132.511,-12.135,132.584,-12.11,132.676,-12.13,132.713,-12.123,132.63,-12.035,132.635,-11.955,132.63,-11.836,132.645,-11.727,132.674,-11.649,132.475,-11.492,132.278,-11.468,132.134,-11.501,132.073,-11.475,131.945,-11.349,131.822,-11.302,131.812,-11.271,131.962,-11.181,132.019,-11.196,132.106,-11.281,132.155,-11.311,132.198,-11.305,132.225,-11.239,132.263,-11.204,132.334,-11.224,132.557,-11.367,132.683,-11.506,132.747,-11.469,132.857,-11.391,132.961,-11.407,133.025,-11.453,133.114,-11.622,133.185,-11.706,133.356,-11.728,133.443,-11.76,133.533,-11.816,133.654,-11.811,133.904,-11.832,134.139,-11.94,134.237,-12.008,134.351,-12.026,134.417,-12.053,134.538,-12.061,134.73,-11.984,134.816,-12.055,134.855,-12.103,135.03,-12.194,135.218,-12.222,135.352,-12.129,135.549,-12.061,135.686,-11.956,135.788,-11.907,135.885,-11.822,135.922,-11.826,135.844,-11.905,135.834,-11.951,135.896,-11.97,135.889,-11.993,135.804,-12.055,135.703,-12.152,135.704,-12.21,135.744,-12.242,135.791,-12.228,135.857,-12.179,135.938,-12.152,136.008,-12.191,136.031,-12.331,136.082,-12.422,136.193,-12.435,136.261,-12.434,136.329,-12.306,136.292,-12.196,136.25,-12.173,136.27,-12.132,136.443,-11.951,136.54,-11.958,136.61,-12.134,136.719,-12.226,136.836,-12.219,136.897,-12.244,136.947,-12.35,136.537,-12.784,136.518,-12.833,136.573,-12.912,136.594,-13.004,136.461,-13.225,136.412,-13.236,136.365,-13.176,136.294,-13.138,136.232,-13.165,136.166,-13.181,135.927,-13.304,135.929,-13.622,135.99,-13.81,135.954,-13.935,135.883,-14.153,135.806,-14.234,135.745,-14.287,135.539,-14.585,135.473,-14.657,135.405,-14.758,135.428,-14.856,135.453,-14.923,135.531,-15,135.833,-15.16,135.97,-15.27,136.205,-15.403,136.259,-15.495,136.291,-15.57,136.462,-15.655,136.584,-15.707,136.619,-15.693,136.644,-15.676,136.675,-15.675,136.705,-15.685,136.7,-15.752,136.687,-15.788,136.698,-15.835,136.785,-15.894,136.923,-15.892,137.002,-15.878,137.09,-15.941,137.169,-15.982,137.299,-16.066,137.526,-16.167,137.704,-16.233,137.913,-16.477,138.072,-16.617,138.245,-16.718,138.506,-16.79,138.626,-16.778,138.82,-16.861,139.01,-16.899,139.11,-17.014,139.145,-17.101,139.154,-17.168,139.248,-17.329,139.441,-17.381,139.69,-17.541,139.895,-17.611,139.946,-17.654,140.036,-17.703,140.21,-17.704,140.511,-17.625,140.648,-17.544,140.83,-17.414,140.916,-17.193,140.966,-17.015,141.219,-16.646,141.291,-16.463,141.356,-16.221,141.412,-16.07,141.393,-15.905,141.452,-15.605,141.581,-15.195,141.625,-15.057,141.604,-14.853,141.523,-14.47,141.559,-14.338,141.594,-14.153,141.535,-14.019,141.481,-13.927,141.473,-13.798,141.534,-13.554,141.589,-13.425,141.645,-13.259,141.614,-12.943,141.735,-12.833,141.782,-12.779,141.876,-12.778,141.92,-12.803,141.93,-12.74,141.893,-12.681,141.878,-12.613,141.852,-12.579,141.795,-12.567,141.747,-12.529,141.678,-12.491,141.689,-12.351,141.806,-12.08,141.871,-11.976,141.913,-12.019,141.961,-12.054,141.968,-11.976,141.952,-11.896,142.041,-11.632,142.139,-11.273,142.168,-10.947,142.326,-10.884,142.407,-10.802,142.456,-10.707,142.545,-10.707,142.605,-10.748,142.565,-10.819,142.553,-10.874,142.723,-11.01,142.78,-11.115,142.803,-11.214,142.837,-11.307,142.853,-11.432,142.851,-11.632,142.873,-11.821,142.934,-11.881,142.988,-11.919,143.066,-11.924,143.179,-11.954,143.153,-12.076,143.105,-12.17,143.099,-12.226,143.11,-12.304,143.191,-12.361,143.254,-12.398,143.29,-12.499,143.402,-12.64,143.398,-12.736,143.458,-12.856,143.512,-13.095,143.529,-13.304,143.587,-13.444,143.548,-13.741,143.589,-13.863,143.643,-13.964,143.707,-14.165,143.756,-14.349,143.822,-14.401,143.962,-14.463,144.106,-14.395,144.21,-14.302,144.322,-14.279,144.473,-14.232,144.586,-14.355,144.648,-14.492,144.916,-14.674,145.064,-14.791,145.18,-14.857,145.288,-14.943,145.277,-15.029,145.252,-15.097,145.276,-15.204,145.293,-15.327,145.272,-15.477,145.35,-15.702,145.375,-15.881,145.458,-16.056,145.452,-16.237,145.436,-16.305,145.426,-16.406,145.49,-16.532,145.55,-16.625,145.638,-16.726,145.755,-16.879,145.838,-16.91,145.912,-16.912,145.902,-17.07,146.05,-17.381,146.126,-17.635,146.074,-17.977,146.023,-18.176,146.032,-18.273,146.223,-18.51,146.333,-18.554,146.312,-18.667,146.297,-18.841,146.383,-18.977,146.481,-19.079,146.587,-19.139,146.692,-19.187,146.829,-19.236,147.003,-19.256,147.093,-19.333,147.139,-19.393,147.278,-19.414,147.342,-19.403,147.419,-19.378,147.471,-19.419,147.51,-19.474,147.586,-19.623,147.742,-19.77,147.853,-19.795,147.916,-19.869,148.004,-19.89,148.081,-19.899,148.19,-19.956,148.367,-20.087,148.527,-20.109,148.6,-20.145,148.759,-20.29,148.821,-20.366,148.885,-20.481,148.805,-20.492,148.73,-20.468,148.684,-20.58,148.789,-20.736,148.912,-20.845,149.061,-20.961,149.205,-21.125,149.241,-21.25,149.28,-21.3,149.329,-21.476,149.454,-21.579,149.46,-21.765,149.524,-22.024,149.596,-22.258,149.645,-22.328,149.704,-22.441,149.772,-22.426,149.822,-22.39,149.92,-22.501,149.974,-22.551,150.006,-22.522,149.942,-22.308,149.981,-22.184,150.021,-22.168,150.076,-22.164,150.143,-22.265,150.235,-22.373,150.405,-22.469,150.541,-22.559,150.58,-22.556,150.564,-22.486,150.569,-22.384,150.623,-22.367,150.672,-22.418,150.764,-22.576,150.783,-22.903,150.783,-23.177,150.843,-23.458,150.931,-23.532,150.989,-23.602,151.088,-23.696,151.154,-23.784,151.236,-23.825,151.501,-24.012,151.575,-24.034,151.691,-24.038,151.832,-24.123,151.903,-24.201,152.055,-24.494,152.13,-24.598,152.282,-24.699,152.353,-24.733,152.456,-24.802,152.493,-24.904,152.502,-24.964,152.563,-25.072,152.654,-25.202,152.789,-25.274,152.913,-25.432,152.921,-25.689,152.985,-25.816,153.028,-25.87,153.125,-25.923,153.165,-25.964,153.084,-26.304,153.162,-26.983,153.117,-27.194,153.198,-27.405,153.386,-27.769,153.428,-27.898,153.455,-28.048,153.576,-28.241,153.569,-28.533,153.617,-28.673,153.605,-28.854,153.462,-29.05,153.348,-29.29,153.347,-29.497,153.272,-29.892,153.224,-29.999,153.188,-30.164,153.031,-30.563,153.024,-30.72,153.048,-30.907,153.022,-31.087,152.982,-31.209,152.944,-31.435,152.786,-31.786,152.559,-32.046,152.545,-32.243,152.517,-32.33,152.47,-32.439,152.331,-32.558,152.247,-32.609,152.216,-32.678,152.137,-32.678,152.135,-32.7,152.188,-32.722,152.164,-32.757,151.954,-32.82,151.813,-32.901,151.668,-33.099,151.608,-33.202,151.53,-33.301,151.484,-33.347,151.463,-33.397,151.432,-33.522,151.358,-33.544,151.292,-33.581,151.323,-33.699,151.288,-33.835,151.28,-33.927,151.245,-33.985,151.202,-33.964,151.168,-33.973,151.125,-34.005,151.191,-34.015,151.232,-34.03,151.09,-34.162,150.96,-34.297,150.927,-34.387,150.871,-34.499,150.822,-34.749,150.781,-34.892,150.809,-34.994,150.805,-35.013,150.775,-35.02,150.756,-35.007,150.697,-35.042,150.681,-35.077,150.706,-35.12,150.722,-35.135,150.715,-35.155,150.69,-35.178,150.634,-35.178,150.567,-35.214,150.374,-35.584,150.292,-35.682,150.195,-35.834,150.158,-35.971,150.129,-36.12,150.095,-36.373,150.063,-36.55,149.988,-36.723,149.96,-36.846,149.951,-37.08,149.986,-37.258,149.963,-37.353,149.962,-37.444,149.933,-37.529,149.809,-37.548,149.709,-37.617,149.565,-37.73,149.481,-37.771,149.298,-37.802,148.944,-37.788,148.263,-37.831,148.131,-37.856,147.877,-37.934,147.631,-38.056,147.396,-38.219,146.857,-38.663,146.436,-38.712,146.356,-38.712,146.293,-38.7,146.217,-38.727,146.216,-38.783,146.286,-38.84,146.337,-38.894,146.427,-38.82,146.467,-38.84,146.482,-38.978,146.484,-39.065,146.457,-39.112,146.4,-39.146,146.34,-39.124,146.332,-39.077,146.254,-38.964,146.158,-38.866,146.07,-38.834,146.018,-38.867,145.935,-38.902,145.866,-38.776,145.791,-38.667,145.692,-38.656,145.606,-38.657,145.535,-38.61,145.397,-38.535,145.424,-38.477,145.463,-38.416,145.542,-38.394,145.518,-38.311,145.476,-38.244,145.366,-38.226,145.293,-38.238,145.249,-38.291,145.191,-38.384,144.96,-38.501,144.847,-38.436,144.718,-38.34,144.78,-38.347,144.911,-38.344,145.02,-38.258,145.067,-38.205,145.12,-38.091,145.05,-38.011,144.985,-37.952,144.891,-37.9,144.538,-38.077,144.465,-38.103,144.396,-38.137,144.518,-38.166,144.589,-38.158,144.665,-38.21,144.544,-38.284,144.448,-38.304,144.329,-38.348,144.102,-38.462,143.812,-38.699,143.687,-38.767,143.539,-38.821,143.338,-38.758,143.226,-38.743,143.083,-38.646,142.84,-38.581,142.612,-38.452,142.456,-38.386,142.345,-38.372,142.188,-38.399,141.925,-38.284,141.725,-38.271,141.594,-38.388,141.492,-38.38,141.424,-38.363,141.214,-38.172,141.011,-38.077,140.627,-38.028,140.39,-37.897,140.212,-37.642,139.875,-37.352,139.784,-37.246,139.742,-37.142,139.738,-37.06,139.784,-36.903,139.847,-36.748,139.857,-36.662,139.729,-36.371,139.549,-36.097,139.466,-36.01,139.245,-35.827,139.038,-35.689,138.985,-35.618,138.969,-35.581,139.067,-35.598,139.113,-35.542,139.178,-35.523,139.231,-35.598,139.289,-35.611,139.292,-35.486,139.325,-35.427,139.303,-35.399,139.283,-35.375,139.193,-35.347,139.094,-35.39,139.018,-35.443,138.915,-35.489,138.875,-35.537,138.771,-35.538,138.73,-35.551,138.522,-35.642,138.389,-35.645,138.184,-35.613,138.252,-35.487,138.333,-35.412,138.4,-35.326,138.511,-35.024,138.49,-34.764,138.436,-34.656,138.264,-34.44,138.186,-34.307,138.089,-34.17,138.041,-34.25,138.012,-34.334,137.919,-34.456,137.874,-34.727,137.692,-35.143,137.566,-35.148,137.46,-35.131,137.272,-35.179,137.144,-35.236,137.03,-35.237,136.967,-35.255,136.884,-35.24,137.014,-34.916,137.128,-34.925,137.252,-34.912,137.308,-34.917,137.391,-34.913,137.454,-34.764,137.493,-34.598,137.469,-34.49,137.459,-34.379,137.484,-34.252,137.494,-34.161,137.65,-33.859,137.781,-33.703,137.932,-33.579,137.914,-33.461,137.866,-33.314,137.852,-33.201,137.924,-33.165,137.993,-33.094,137.913,-32.771,137.863,-32.674,137.783,-32.578,137.782,-32.702,137.791,-32.823,137.68,-32.978,137.536,-33.089,137.442,-33.194,137.354,-33.43,137.237,-33.629,137.13,-33.703,137.034,-33.72,136.937,-33.75,136.783,-33.83,136.636,-33.897,136.526,-33.984,136.431,-34.03,136.121,-34.429,135.98,-34.562,135.951,-34.616,135.891,-34.661,135.903,-34.724,135.951,-34.767,135.999,-34.944,135.97,-34.982,135.919,-34.962,135.792,-34.863,135.713,-34.899,135.648,-34.94,135.481,-34.758,135.412,-34.716,135.324,-34.643,135.231,-34.58,135.191,-34.573,135.123,-34.586,135.13,-34.537,135.176,-34.497,135.217,-34.487,135.292,-34.546,135.379,-34.598,135.427,-34.602,135.45,-34.581,135.368,-34.376,135.312,-34.196,135.286,-34.142,135.219,-33.96,135.185,-33.907,135.042,-33.778,134.889,-33.626,134.847,-33.445,134.791,-33.328,134.719,-33.255,134.608,-33.19,134.301,-33.165,134.174,-32.979,134.1,-32.749,134.158,-32.733,134.227,-32.731,134.249,-32.659,134.234,-32.549,133.93,-32.412,133.787,-32.269,133.665,-32.207,133.551,-32.183,133.401,-32.188,133.212,-32.184,132.757,-31.956,132.649,-31.949,132.324,-32.02,132.215,-32.007,131.721,-31.696,131.393,-31.549,131.285,-31.521,131.144,-31.496,131.029,-31.532,130.948,-31.566,130.783,-31.604,130.13,-31.579,129.569,-31.627]},{"b":[177.255,-18.264,178.668,-17.313],"l":[178.575,-17.749,178.596,-17.699,178.592,-17.651,178.523,-17.596,178.411,-17.523,178.339,-17.438,178.309,-17.435,178.28,-17.416,178.28,-17.372,178.247,-17.329,178.188,-17.313,178.128,-17.339,177.94,-17.395,177.818,-17.388,177.618,-17.461,177.504,-17.54,177.401,-17.632,177.406,-17.682,177.423,-17.737,177.411,-17.754,177.386,-17.762,177.366,-17.786,177.36,-17.82,177.316,-17.846,177.264,-17.863,177.255,-17.915,177.263,-17.969,177.321,-18.078,177.383,-18.121,177.457,-18.148,177.636,-18.181,177.771,-18.22,177.847,-18.255,177.955,-18.264,178.064,-18.25,178.16,-18.25,178.244,-18.184,178.332,-18.135,178.423,-18.124,178.461,-18.139,178.487,-18.112,178.597,-18.109,178.668,-18.081,178.618,-17.933,178.604,-17.839,178.575,-17.749]},{"b":[164.037,-22.376,167.004,-20.142],"l":[166.942,-22.09,166.82,-22.017,166.69,-21.953,166.588,-21.873,166.493,-21.783,166.303,-21.637,166.058,-21.484,165.95,-21.442,165.885,-21.389,165.823,-21.364,165.775,-21.312,165.663,-21.267,165.582,-21.18,165.447,-21.081,165.421,-21.043,165.413,-20.981,165.381,-20.936,165.307,-20.887,165.252,-20.818,165.192,-20.769,165.112,-20.745,164.976,-20.681,164.588,-20.381,164.436,-20.282,164.315,-20.309,164.202,-20.246,164.06,-20.142,164.041,-20.173,164.037,-20.234,164.065,-20.279,164.124,-20.305,164.158,-20.348,164.152,-20.415,164.17,-20.48,164.313,-20.633,164.375,-20.739,164.455,-20.829,164.559,-20.906,164.656,-20.992,164.855,-21.202,164.927,-21.29,165.01,-21.327,165.242,-21.525,165.329,-21.58,165.428,-21.615,165.62,-21.724,165.744,-21.777,165.823,-21.854,165.933,-21.908,166.096,-21.957,166.124,-21.989,166.143,-22.044,166.177,-22.089,166.292,-22.155,166.416,-22.196,166.438,-22.232,166.468,-22.256,166.522,-22.249,166.571,-22.266,166.774,-22.376,166.835,-22.355,166.9,-22.353,166.97,-22.323,167.004,-22.262,166.942,-22.09]},{"b":[127.42,-0.893,128.9,2.199],"l":[127.687,-0.08,127.681,0.035,127.685,0.149,127.709,0.288,127.669,0.337,127.616,0.383,127.555,0.49,127.537,0.611,127.542,0.681,127.567,0.743,127.601,0.796,127.608,0.848,127.52,0.924,127.429,1.14,127.42,1.252,127.537,1.467,127.535,1.572,127.558,1.634,127.571,1.7,127.632,1.844,127.731,1.966,127.9,2.137,127.964,2.175,128.036,2.199,128.043,2.157,128.031,2.12,127.907,1.946,127.89,1.906,127.887,1.833,127.946,1.79,128.011,1.701,128.024,1.583,128.026,1.458,128.012,1.332,127.988,1.29,127.885,1.163,127.653,1.014,127.633,0.977,127.634,0.936,127.677,0.887,127.733,0.848,127.805,0.826,127.881,0.832,127.919,0.877,127.929,0.935,127.967,1.043,128.055,1.116,128.117,1.127,128.161,1.158,128.153,1.238,128.157,1.317,128.222,1.401,128.424,1.518,128.539,1.559,128.688,1.573,128.705,1.528,128.688,1.464,128.717,1.367,128.703,1.106,128.669,1.069,128.515,0.979,128.346,0.907,128.299,0.877,128.257,0.805,128.261,0.734,128.398,0.639,128.611,0.55,128.655,0.508,128.684,0.438,128.692,0.36,128.743,0.323,128.815,0.305,128.863,0.268,128.9,0.216,128.54,0.338,128.446,0.392,128.333,0.398,128.221,0.414,128.106,0.461,127.983,0.472,127.924,0.438,127.901,0.372,127.887,0.298,127.915,0.206,127.912,0.151,127.889,0.05,127.978,-0.248,128.089,-0.485,128.254,-0.732,128.335,-0.816,128.425,-0.893,128.278,-0.87,128.233,-0.788,128.046,-0.706,128.011,-0.657,127.889,-0.424,127.853,-0.38,127.741,-0.3,127.692,-0.242,127.675,-0.163,127.687,-0.08]},{"b":[68.77,-49.71,70.555,-48.656],"l":[70.32,-49.059,70.285,-49.076,70.208,-49.135,70.061,-49.136,69.984,-49.16,69.854,-49.222,69.771,-49.248,69.667,-49.265,69.611,-49.266,69.542,-49.256,69.405,-49.182,69.436,-49.124,69.572,-49.129,69.644,-49.117,69.587,-49.072,69.593,-48.971,69.535,-48.974,69.314,-49.106,69.265,-49.115,69.185,-49.11,69.052,-49.082,69.033,-49.018,69.099,-48.938,69.104,-48.9,69.136,-48.861,69.123,-48.766,69.072,-48.753,69.093,-48.724,69.081,-48.679,69.057,-48.656,69.002,-48.661,68.959,-48.694,68.9,-48.776,68.832,-48.849,68.837,-48.926,68.797,-48.995,68.77,-49.066,68.79,-49.104,68.817,-49.135,68.854,-49.141,68.883,-49.165,68.814,-49.192,68.799,-49.232,68.841,-49.285,68.818,-49.354,68.862,-49.392,68.872,-49.444,68.848,-49.5,68.811,-49.55,68.791,-49.6,68.783,-49.651,68.815,-49.7,68.873,-49.71,68.993,-49.705,69.086,-49.653,69.153,-49.53,69.275,-49.543,69.353,-49.563,69.478,-49.617,69.613,-49.651,69.682,-49.642,69.747,-49.602,69.804,-49.614,69.826,-49.645,69.919,-49.689,70.075,-49.709,70.124,-49.704,70.207,-49.665,70.216,-49.629,70.259,-49.601,70.307,-49.583,70.248,-49.531,70.166,-49.509,70.073,-49.518,70.063,-49.589,69.986,-49.582,69.856,-49.544,69.78,-49.49,69.749,-49.448,69.76,-49.43,69.818,-49.438,69.861,-49.421,69.902,-49.389,69.916,-49.349,69.993,-49.345,70.166,-49.343,70.238,-49.372,70.298,-49.425,70.338,-49.435,70.386,-49.434,70.411,-49.411,70.39,-49.366,70.485,-49.328,70.537,-49.266,70.555,-49.201,70.531,-49.137,70.484,-49.084,70.406,-49.061,70.32,-49.059]},{"b":[62.52,80.698,65.437,81.198],"l":[63.116,80.967,63.615,80.981,63.856,80.981,64.096,80.998,64.166,81.036,64.21,81.106,64.256,81.144,64.31,81.175,64.575,81.198,64.802,81.197,65.028,81.169,65.172,81.144,65.31,81.096,65.382,81.057,65.36,81.008,65.372,80.968,65.437,80.931,64.997,80.819,64.548,80.755,63.374,80.7,63.188,80.698,63.002,80.713,62.76,80.763,62.52,80.822,62.593,80.853,62.819,80.894,63.116,80.967]},{"b":[57.011,80.104,59.255,80.494],"l":[57.392,80.139,57.332,80.158,57.281,80.194,57.214,80.328,57.212,80.368,57.186,80.396,57.083,80.445,57.011,80.468,57.075,80.494,57.522,80.475,58.48,80.465,58.972,80.416,59.116,80.388,59.255,80.343,58.398,80.319,58.284,80.298,58.286,80.248,58.255,80.202,58.163,80.197,57.956,80.123,57.8,80.104,57.392,80.139]},{"b":[-28.037,70.454,-25.346,71.044],"l":[-25.468,70.78,-25.421,70.795,-25.401,70.811,-25.394,70.835,-25.397,70.862,-25.432,70.921,-25.458,70.943,-25.612,70.976,-25.661,70.998,-25.727,71.042,-25.819,71.044,-26.337,70.919,-26.622,70.876,-26.976,70.863,-27.239,70.868,-27.387,70.876,-27.617,70.914,-27.709,70.897,-27.744,70.79,-27.714,70.713,-27.805,70.642,-27.94,70.615,-27.968,70.595,-28.037,70.514,-28.035,70.487,-28.003,70.467,-27.898,70.454,-27.69,70.479,-27.105,70.531,-26.605,70.553,-26.339,70.511,-26.274,70.454,-26.218,70.454,-26.05,70.509,-25.911,70.573,-25.801,70.599,-25.402,70.653,-25.346,70.693,-25.352,70.714,-25.38,70.741,-25.468,70.78]},{"b":[159.607,-9.929,160.819,-9.269],"l":[160.682,-9.692,160.625,-9.589,160.525,-9.536,160.355,-9.422,160.065,-9.419,159.971,-9.433,159.75,-9.273,159.686,-9.269,159.626,-9.311,159.607,-9.354,159.612,-9.471,159.622,-9.532,159.68,-9.637,159.755,-9.726,159.803,-9.763,159.854,-9.792,160.002,-9.812,160.321,-9.821,160.482,-9.895,160.649,-9.929,160.713,-9.914,160.802,-9.878,160.819,-9.863,160.794,-9.767,160.751,-9.715,160.682,-9.692]},{"b":[117.219,8.367,119.687,11.346],"l":[119.297,10.751,119.313,10.687,119.287,10.574,119.224,10.477,119.186,10.439,119.143,10.409,119.08,10.386,119.024,10.354,118.845,10.131,118.82,10.105,118.728,10.035,118.533,9.794,118.344,9.603,118.115,9.347,118.024,9.27,117.983,9.253,117.932,9.251,117.885,9.241,117.745,9.098,117.593,8.968,117.53,8.903,117.418,8.767,117.35,8.714,117.256,8.541,117.229,8.457,117.219,8.367,117.311,8.44,117.413,8.496,117.469,8.511,117.517,8.538,117.54,8.596,117.572,8.642,117.68,8.678,117.78,8.729,117.889,8.798,117.99,8.877,118.069,8.984,118.134,9.101,118.229,9.168,118.35,9.201,118.435,9.256,118.504,9.333,118.57,9.423,118.774,9.767,118.755,9.862,118.782,9.916,118.835,9.949,118.949,9.993,119.192,10.061,119.219,10.101,119.232,10.152,119.285,10.252,119.369,10.327,119.422,10.354,119.541,10.379,119.595,10.407,119.687,10.5,119.684,10.552,119.616,10.707,119.527,10.953,119.562,11.046,119.533,11.102,119.535,11.157,119.56,11.267,119.553,11.314,119.501,11.346,119.465,11.294,119.341,11.033,119.306,10.974,119.261,10.845,119.297,10.751]},{"b":[121.891,10.444,123.158,11.897],"l":[123.103,11.541,122.895,11.441,122.901,11.487,122.931,11.529,122.838,11.596,122.726,11.608,122.613,11.564,122.496,11.615,122.399,11.702,122.291,11.772,122.087,11.855,122.029,11.895,121.964,11.897,121.916,11.854,121.891,11.791,121.941,11.758,122.067,11.724,122.101,11.681,122.104,11.643,122.06,11.326,122.051,11.097,122.021,10.979,121.964,10.872,121.95,10.757,121.972,10.699,121.98,10.639,121.934,10.494,121.938,10.471,121.954,10.444,121.988,10.458,122.052,10.514,122.109,10.576,122.198,10.623,122.522,10.692,122.673,10.801,122.77,10.824,122.791,10.88,122.789,10.941,122.803,10.99,122.847,11.022,122.939,11.058,123.017,11.117,123.075,11.197,123.12,11.287,123.144,11.364,123.156,11.443,123.158,11.536,123.103,11.541]},{"b":[122.4,9.053,123.568,10.994],"l":[123.511,10.923,123.257,10.994,123.222,10.989,123.024,10.912,122.983,10.887,122.97,10.836,122.969,10.766,122.958,10.698,122.906,10.603,122.856,10.553,122.817,10.504,122.852,10.395,122.867,10.284,122.866,10.125,122.856,10.087,122.713,9.99,122.648,9.982,122.523,9.979,122.471,9.962,122.426,9.896,122.4,9.823,122.411,9.694,122.563,9.483,122.61,9.443,122.665,9.41,122.772,9.371,122.867,9.32,122.948,9.108,122.995,9.059,123.065,9.053,123.131,9.064,123.192,9.088,123.229,9.121,123.293,9.217,123.321,9.273,123.322,9.317,123.308,9.357,123.15,9.606,123.149,9.659,123.163,9.715,123.162,9.864,123.187,9.933,123.266,10.059,123.296,10.125,123.344,10.325,123.407,10.459,123.493,10.582,123.528,10.662,123.568,10.781,123.563,10.816,123.511,10.923]},{"b":[124.308,10.033,125.268,11.535],"l":[125.197,10.457,125.188,10.585,125.164,10.637,125.128,10.685,125.084,10.722,125.034,10.751,125.013,10.786,125.04,10.952,125.044,11.135,125.027,11.212,124.994,11.256,124.93,11.373,124.821,11.401,124.724,11.322,124.644,11.308,124.575,11.343,124.548,11.395,124.511,11.424,124.436,11.457,124.374,11.515,124.331,11.535,124.308,11.486,124.331,11.427,124.366,11.371,124.412,11.15,124.446,10.924,124.503,10.904,124.616,10.962,124.663,10.962,124.739,10.88,124.787,10.781,124.797,10.732,124.799,10.682,124.738,10.44,124.79,10.328,124.792,10.275,124.781,10.168,124.813,10.135,124.929,10.096,125.027,10.033,125.024,10.115,125.005,10.197,124.988,10.368,125.044,10.323,125.105,10.218,125.143,10.189,125.14,10.235,125.148,10.272,125.253,10.264,125.268,10.308,125.26,10.35,125.197,10.457]},{"b":[124.295,11.05,125.749,12.573],"l":[125.627,11.234,125.574,11.238,125.582,11.279,125.609,11.323,125.593,11.378,125.506,11.544,125.492,11.594,125.5,11.655,125.497,11.714,125.464,11.772,125.457,11.953,125.513,12.055,125.503,12.136,125.536,12.191,125.481,12.252,125.409,12.285,125.352,12.293,125.32,12.322,125.328,12.387,125.31,12.446,125.24,12.528,125.15,12.573,124.84,12.535,124.566,12.526,124.295,12.569,124.326,12.404,124.385,12.244,124.446,12.153,124.529,12.079,124.572,12.055,124.677,12.021,124.75,11.933,124.796,11.896,124.821,11.852,124.884,11.775,124.936,11.755,124.995,11.765,124.998,11.702,124.979,11.638,124.917,11.558,124.945,11.479,125.034,11.341,125.088,11.287,125.156,11.267,125.233,11.145,125.312,11.142,125.432,11.113,125.628,11.132,125.674,11.121,125.736,11.05,125.749,11.074,125.704,11.165,125.627,11.234]},{"b":[120.338,12.219,121.541,13.522],"l":[121.48,12.837,121.475,12.932,121.49,13.02,121.539,13.089,121.523,13.131,121.442,13.188,121.357,13.265,121.284,13.374,121.203,13.432,121.122,13.381,121.079,13.411,121.025,13.429,120.981,13.486,120.915,13.501,120.755,13.471,120.704,13.479,120.653,13.498,120.468,13.522,120.401,13.517,120.353,13.473,120.338,13.412,120.388,13.402,120.438,13.405,120.455,13.394,120.481,13.311,120.508,13.26,120.573,13.209,120.651,13.169,120.68,13.131,120.764,12.97,120.769,12.841,120.776,12.791,120.796,12.748,120.855,12.704,120.899,12.646,120.921,12.581,120.922,12.512,120.963,12.447,121.049,12.36,121.083,12.339,121.108,12.304,121.117,12.253,121.155,12.236,121.237,12.219,121.289,12.277,121.322,12.304,121.357,12.313,121.394,12.301,121.4,12.361,121.418,12.389,121.412,12.423,121.458,12.508,121.519,12.584,121.541,12.638,121.48,12.837]},{"b":[154.709,-6.863,155.958,-5.444],"l":[155.823,-6.38,155.734,-6.296,155.638,-6.221,155.581,-6.196,155.519,-6.182,155.467,-6.145,155.373,-5.974,155.323,-5.932,155.228,-5.865,155.198,-5.828,155.187,-5.777,155.094,-5.62,154.997,-5.54,154.871,-5.521,154.818,-5.494,154.773,-5.454,154.729,-5.444,154.741,-5.545,154.709,-5.747,154.721,-5.817,154.759,-5.931,154.782,-5.971,154.87,-6.061,154.94,-6.106,155.01,-6.21,155.045,-6.234,155.202,-6.308,155.234,-6.412,155.209,-6.527,155.261,-6.626,155.344,-6.722,155.427,-6.783,155.521,-6.83,155.617,-6.856,155.719,-6.863,155.763,-6.834,155.805,-6.796,155.892,-6.762,155.915,-6.797,155.933,-6.78,155.958,-6.687,155.928,-6.565,155.882,-6.47,155.823,-6.38]},{"b":[150.746,-4.832,153.133,-2.573],"l":[153.017,-4.106,152.38,-3.582,152.329,-3.521,152.179,-3.41,152.065,-3.28,152.033,-3.251,151.807,-3.173,151.69,-3.073,151.586,-3.003,151.475,-2.942,151.315,-2.875,151.226,-2.87,151.175,-2.789,150.995,-2.688,150.825,-2.573,150.843,-2.644,150.826,-2.713,150.746,-2.739,150.848,-2.78,150.968,-2.78,151.067,-2.829,151.405,-3.037,151.465,-3.101,151.579,-3.154,151.793,-3.338,151.88,-3.4,151.973,-3.453,152.023,-3.469,152.136,-3.487,152.192,-3.506,152.279,-3.582,152.356,-3.668,152.598,-3.995,152.668,-4.132,152.697,-4.282,152.693,-4.356,152.678,-4.429,152.681,-4.498,152.74,-4.636,152.787,-4.699,152.846,-4.762,152.892,-4.832,152.966,-4.756,153.023,-4.666,153.046,-4.576,153.044,-4.476,153.112,-4.392,153.133,-4.352,153.124,-4.252,153.017,-4.106]},{"b":[137.65,-8.412,138.989,-7.38],"l":[138.77,-7.39,138.544,-7.38,138.296,-7.438,138.185,-7.495,138.082,-7.566,138.008,-7.642,137.833,-7.932,137.685,-8.262,137.65,-8.386,137.688,-8.412,137.872,-8.38,137.983,-8.382,138.296,-8.405,138.535,-8.274,138.612,-8.198,138.786,-8.059,138.893,-7.882,138.989,-7.696,138.963,-7.588,138.899,-7.512,138.802,-7.415,138.77,-7.39]},{"b":[127.878,-3.858,130.86,-2.786],"l":[130.773,-3.419,130.718,-3.411,130.671,-3.392,130.642,-3.312,130.626,-3.228,130.57,-3.131,130.379,-2.989,130.304,-2.979,130.103,-2.993,129.984,-2.977,129.755,-2.866,129.6,-2.806,129.543,-2.79,129.484,-2.786,129.427,-2.791,129.371,-2.821,129.28,-2.889,129.174,-2.933,129.116,-2.937,129.074,-2.895,129.058,-2.838,128.991,-2.829,128.911,-2.85,128.791,-2.857,128.57,-2.842,128.199,-2.866,128.113,-2.935,127.878,-3.222,127.897,-3.282,127.928,-3.341,127.928,-3.397,127.902,-3.496,127.92,-3.506,127.97,-3.444,128.03,-3.341,128.044,-3.303,128.056,-3.239,128.082,-3.184,128.132,-3.157,128.181,-3.172,128.233,-3.203,128.28,-3.241,128.419,-3.416,128.466,-3.44,128.517,-3.449,128.639,-3.433,128.677,-3.397,128.751,-3.3,128.802,-3.266,128.863,-3.235,128.925,-3.229,128.958,-3.241,128.964,-3.272,128.952,-3.304,128.967,-3.326,129.108,-3.349,129.212,-3.393,129.333,-3.409,129.468,-3.453,129.522,-3.434,129.52,-3.363,129.512,-3.329,129.545,-3.319,129.627,-3.317,129.844,-3.327,129.953,-3.392,129.981,-3.439,130.02,-3.475,130.27,-3.579,130.363,-3.625,130.58,-3.749,130.805,-3.858,130.86,-3.57,130.846,-3.533,130.773,-3.419]},{"b":[126.026,-3.824,127.244,-3.065],"l":[127.163,-3.338,127.125,-3.311,127.092,-3.278,127.063,-3.217,127.025,-3.166,126.861,-3.088,126.808,-3.069,126.555,-3.065,126.306,-3.103,126.22,-3.148,126.088,-3.105,126.05,-3.128,126.026,-3.171,126.034,-3.356,126.057,-3.421,126.147,-3.523,126.178,-3.579,126.215,-3.605,126.411,-3.711,126.547,-3.772,126.686,-3.824,126.74,-3.814,126.794,-3.789,126.87,-3.783,126.941,-3.765,127.085,-3.671,127.155,-3.647,127.23,-3.633,127.244,-3.471,127.227,-3.391,127.163,-3.338]},{"b":[119.807,-8.935,123.006,-8.093],"l":[122.978,-8.152,122.917,-8.106,122.846,-8.093,122.792,-8.127,122.759,-8.186,122.919,-8.222,122.85,-8.304,122.75,-8.353,122.604,-8.402,122.556,-8.432,122.514,-8.47,122.484,-8.514,122.467,-8.566,122.433,-8.601,122.323,-8.628,122.263,-8.625,122.067,-8.497,122.02,-8.472,121.967,-8.455,121.912,-8.482,121.863,-8.494,121.747,-8.507,121.683,-8.506,121.61,-8.526,121.548,-8.575,121.498,-8.585,121.445,-8.578,121.372,-8.551,121.277,-8.478,121.118,-8.424,121.009,-8.366,120.886,-8.327,120.751,-8.321,120.71,-8.308,120.61,-8.24,120.547,-8.26,120.486,-8.266,120.425,-8.249,120.354,-8.258,120.231,-8.29,120.099,-8.378,119.964,-8.436,119.918,-8.445,119.875,-8.42,119.866,-8.473,119.848,-8.523,119.818,-8.571,119.807,-8.623,119.808,-8.698,119.841,-8.764,119.879,-8.808,119.909,-8.858,120.012,-8.81,120.121,-8.777,120.32,-8.82,120.55,-8.802,120.781,-8.849,120.982,-8.928,121.035,-8.935,121.086,-8.926,121.138,-8.904,121.191,-8.896,121.328,-8.917,121.415,-8.815,121.5,-8.812,121.585,-8.821,121.621,-8.854,121.651,-8.899,121.738,-8.87,121.839,-8.86,122.094,-8.745,122.186,-8.73,122.321,-8.738,122.417,-8.735,122.47,-8.725,122.554,-8.681,122.642,-8.647,122.783,-8.612,122.82,-8.596,122.847,-8.562,122.811,-8.481,122.902,-8.416,122.924,-8.381,122.955,-8.354,123.006,-8.329,122.978,-8.152]},{"b":[118.959,-10.294,120.833,-9.301],"l":[120.7,-9.903,120.633,-9.806,120.556,-9.719,120.504,-9.674,120.444,-9.646,120.365,-9.655,120.291,-9.648,120.258,-9.603,120.248,-9.543,120.221,-9.506,120.058,-9.42,120.013,-9.375,119.974,-9.322,119.942,-9.301,119.851,-9.36,119.795,-9.38,119.615,-9.352,119.424,-9.37,119.296,-9.367,119.186,-9.384,119.031,-9.44,118.994,-9.472,118.959,-9.519,118.977,-9.573,119.008,-9.621,119.042,-9.669,119.085,-9.707,119.363,-9.772,119.417,-9.771,119.47,-9.761,119.601,-9.774,119.813,-9.917,119.931,-9.967,119.998,-10.04,120.052,-10.123,120.145,-10.2,120.255,-10.242,120.395,-10.263,120.439,-10.294,120.562,-10.236,120.64,-10.228,120.698,-10.207,120.804,-10.108,120.833,-10.037,120.784,-9.957,120.7,-9.903]},{"b":[116.768,-9.099,119.13,-8.089],"l":[119.044,-8.457,118.988,-8.338,118.926,-8.298,118.846,-8.293,118.794,-8.306,118.748,-8.331,118.714,-8.415,118.692,-8.393,118.671,-8.323,118.612,-8.281,118.552,-8.27,118.491,-8.271,118.433,-8.293,118.338,-8.354,118.292,-8.357,118.242,-8.318,118.203,-8.267,118.151,-8.15,118.117,-8.122,117.921,-8.089,117.868,-8.101,117.755,-8.15,117.738,-8.205,117.766,-8.279,117.815,-8.342,117.979,-8.459,118.018,-8.467,118.061,-8.464,118.1,-8.475,118.174,-8.528,118.235,-8.592,118.206,-8.652,118.104,-8.65,117.97,-8.728,117.893,-8.704,117.806,-8.711,117.712,-8.583,117.673,-8.563,117.643,-8.536,117.622,-8.46,117.567,-8.426,117.435,-8.435,117.357,-8.429,117.224,-8.375,117.165,-8.367,117.064,-8.444,116.953,-8.503,116.886,-8.508,116.835,-8.532,116.801,-8.598,116.783,-8.665,116.807,-8.811,116.772,-8.894,116.768,-8.955,116.788,-9.006,116.871,-9.046,116.958,-9.076,117.061,-9.099,117.161,-9.069,117.21,-9.034,117.265,-9.026,117.326,-9.034,117.388,-9.032,117.508,-9.008,117.732,-8.92,117.795,-8.92,117.861,-8.931,118.071,-8.851,118.132,-8.856,118.19,-8.841,118.234,-8.808,118.379,-8.675,118.4,-8.704,118.398,-8.813,118.427,-8.855,118.479,-8.856,118.674,-8.812,118.728,-8.805,118.808,-8.838,118.833,-8.833,118.837,-8.809,118.818,-8.791,118.756,-8.774,118.746,-8.735,118.821,-8.712,118.903,-8.703,118.939,-8.713,118.971,-8.741,119.006,-8.75,119.079,-8.73,119.104,-8.71,119.13,-8.668,119.101,-8.628,119.063,-8.6,119.042,-8.561,119.044,-8.457]},{"b":[115.857,-8.929,116.734,-8.204],"l":[116.402,-8.204,116.304,-8.238,116.22,-8.295,116.061,-8.437,116.078,-8.611,116.076,-8.745,116.032,-8.765,115.914,-8.758,115.869,-8.743,115.857,-8.788,115.875,-8.826,116.027,-8.873,116.239,-8.912,116.29,-8.906,116.377,-8.929,116.587,-8.886,116.559,-8.854,116.514,-8.821,116.641,-8.614,116.734,-8.387,116.719,-8.336,116.688,-8.304,116.647,-8.283,116.402,-8.204]},{"b":[114.468,-8.849,115.704,-8.066],"l":[115.549,-8.208,115.448,-8.155,115.34,-8.115,115.191,-8.067,115.154,-8.066,114.998,-8.174,114.938,-8.187,114.833,-8.183,114.62,-8.128,114.504,-8.117,114.475,-8.119,114.468,-8.166,114.479,-8.215,114.502,-8.261,114.571,-8.345,114.613,-8.378,114.731,-8.394,114.842,-8.429,114.952,-8.496,115.055,-8.573,115.106,-8.629,115.142,-8.697,115.14,-8.769,115.092,-8.829,115.145,-8.849,115.194,-8.835,115.22,-8.82,115.236,-8.798,115.247,-8.758,115.295,-8.664,115.334,-8.616,115.56,-8.514,115.661,-8.448,115.704,-8.407,115.691,-8.364,115.549,-8.208]},{"b":[105.133,-3.072,106.818,-1.505],"l":[106.366,-2.465,106.209,-2.189,106.162,-1.867,106.127,-1.8,106.08,-1.738,106.046,-1.669,106.027,-1.593,105.981,-1.539,105.91,-1.505,105.816,-1.506,105.72,-1.534,105.754,-1.659,105.701,-1.731,105.668,-1.68,105.64,-1.61,105.585,-1.527,105.46,-1.575,105.413,-1.611,105.373,-1.657,105.364,-1.705,105.387,-1.751,105.375,-1.813,105.316,-1.861,105.191,-1.917,105.138,-1.973,105.133,-2.043,105.248,-2.079,105.293,-2.114,105.343,-2.125,105.553,-2.079,105.599,-2.103,105.705,-2.133,105.786,-2.181,105.807,-2.307,105.862,-2.415,105.908,-2.452,105.939,-2.493,105.908,-2.643,105.937,-2.744,105.999,-2.825,106.126,-2.855,106.25,-2.894,106.342,-2.949,106.397,-2.967,106.449,-2.994,106.496,-3.029,106.547,-3.056,106.611,-3.071,106.667,-3.072,106.658,-3.001,106.619,-2.936,106.612,-2.896,106.679,-2.704,106.707,-2.658,106.744,-2.618,106.818,-2.573,106.366,-2.465]},{"b":[21.854,57.931,23.323,58.627],"l":[22.964,58.606,22.82,58.622,22.754,58.605,22.688,58.597,22.617,58.621,22.547,58.627,22.474,58.605,22.328,58.581,22.267,58.508,22.206,58.521,22.169,58.516,22.081,58.478,22.002,58.51,21.924,58.514,21.862,58.497,21.984,58.387,21.965,58.349,21.924,58.316,21.891,58.305,21.854,58.302,21.882,58.262,22.035,58.213,22.104,58.172,22.188,58.154,22.153,58.115,21.986,57.995,21.978,57.963,21.997,57.931,22.076,57.936,22.152,57.967,22.227,58.052,22.269,58.161,22.372,58.217,22.498,58.236,22.73,58.231,22.757,58.261,22.885,58.311,22.98,58.364,23.035,58.372,23.083,58.398,23.127,58.436,23.323,58.451,23.293,58.483,22.964,58.606]},{"b":[20.228,77.267,24.902,78.598],"l":[24.238,77.899,23.883,77.865,23.684,77.875,23.331,77.958,23.117,77.992,23.152,78.088,23.365,78.121,23.452,78.149,23.352,78.186,23.119,78.239,22.989,78.252,22.735,78.24,22.449,78.215,22.3,78.228,22.207,78.408,22.043,78.577,21.746,78.572,21.608,78.596,21.455,78.598,21.047,78.557,20.363,78.515,20.228,78.478,20.373,78.412,20.56,78.419,20.528,78.326,20.786,78.252,20.845,78.166,21.035,78.059,21.21,78.006,21.653,77.924,21.608,77.916,21.431,77.812,21.334,77.772,21.251,77.711,21.201,77.619,20.873,77.565,20.928,77.46,21.05,77.441,21.856,77.494,22.057,77.501,22.255,77.529,22.397,77.57,22.448,77.571,22.62,77.55,22.685,77.554,22.733,77.539,22.679,77.5,22.442,77.429,22.487,77.36,22.469,77.331,22.427,77.316,22.554,77.267,22.802,77.276,22.9,77.311,22.997,77.361,23.101,77.385,23.381,77.38,23.505,77.401,23.736,77.462,23.841,77.498,23.955,77.558,24.062,77.631,24.13,77.658,24.902,77.757,24.571,77.834,24.238,77.899]},{"b":[59.288,80.419,62.228,80.893],"l":[62.103,80.867,61.851,80.886,61.597,80.893,61.313,80.863,60.82,80.827,60.482,80.804,60.278,80.801,60.235,80.838,60.095,80.849,59.716,80.836,59.592,80.817,59.549,80.784,59.495,80.767,59.387,80.713,59.306,80.618,59.288,80.573,59.304,80.522,59.346,80.505,59.65,80.431,59.9,80.446,60.278,80.494,60.722,80.435,61.051,80.419,61.285,80.505,61.597,80.535,61.681,80.586,61.769,80.601,62.076,80.617,62.115,80.684,62.192,80.73,62.228,80.794,62.168,80.835,62.103,80.867]},{"b":[44.905,80.447,48.687,80.854],"l":[47.011,80.562,47.145,80.609,47.198,80.615,47.304,80.606,47.414,80.675,47.512,80.688,47.6,80.742,47.705,80.765,47.777,80.756,48.044,80.668,48.625,80.629,48.684,80.633,48.687,80.718,48.547,80.779,48.446,80.806,48.345,80.819,48.243,80.823,47.9,80.813,47.442,80.854,47.352,80.853,47.021,80.814,46.799,80.755,46.327,80.735,45.125,80.652,44.905,80.611,45.149,80.599,45.389,80.56,45.641,80.537,45.969,80.569,46.024,80.541,46.06,80.484,46.141,80.447,46.378,80.457,46.514,80.476,46.624,80.541,46.678,80.561,47.011,80.562]},{"b":[46.644,80.082,51.704,80.927],"l":[51.455,80.745,50.918,80.89,50.801,80.914,50.431,80.911,50.278,80.927,50.124,80.924,49.508,80.865,49.244,80.821,49.147,80.712,49.193,80.656,49.185,80.559,49.088,80.516,48.625,80.508,48.465,80.558,48.403,80.569,48.306,80.562,48.208,80.544,47.896,80.529,47.656,80.501,47.403,80.445,46.644,80.3,46.738,80.258,46.846,80.237,46.991,80.183,47.249,80.18,47.343,80.189,47.444,80.23,47.642,80.245,47.893,80.239,47.978,80.213,47.723,80.151,47.632,80.112,47.737,80.082,47.94,80.089,48.026,80.099,48.096,80.122,48.167,80.133,48.386,80.096,48.467,80.11,48.533,80.158,48.555,80.183,48.582,80.195,48.797,80.161,48.892,80.155,48.978,80.163,49.011,80.207,48.991,80.242,48.96,80.266,48.922,80.277,48.689,80.29,48.677,80.3,48.811,80.354,48.896,80.369,49.586,80.377,49.794,80.425,49.75,80.472,49.846,80.498,50.28,80.527,50.961,80.54,51.146,80.604,51.704,80.688,51.591,80.741,51.455,80.745]},{"b":[48.279,68.733,50.283,69.511],"l":[49.225,69.511,48.953,69.509,48.845,69.495,48.631,69.436,48.414,69.346,48.32,69.269,48.296,69.184,48.28,69.097,48.279,69.04,48.294,68.984,48.316,68.942,48.439,68.805,48.667,68.733,48.91,68.743,49.18,68.778,49.626,68.86,49.84,68.974,49.921,69.053,50.094,69.126,50.141,69.098,50.164,69.038,50.221,69.049,50.283,69.089,50.265,69.186,50.167,69.257,49.996,69.309,49.225,69.511]},{"b":[58.473,69.688,60.481,70.465],"l":[60.393,69.962,60.172,70.023,59.956,70.108,59.636,70.197,59.529,70.249,59.426,70.311,59.31,70.362,59.088,70.437,59.048,70.46,59.005,70.465,58.794,70.433,58.678,70.36,58.615,70.351,58.52,70.318,58.473,70.267,58.568,70.156,58.606,70.129,58.634,70.088,58.68,70.051,58.953,69.893,59.004,69.883,59.083,69.911,59.144,69.922,59.268,69.898,59.382,69.89,59.503,69.866,59.581,69.791,59.578,69.739,59.637,69.721,59.725,69.706,59.813,69.696,59.92,69.697,60.026,69.717,60.216,69.688,60.327,69.715,60.44,69.726,60.477,69.794,60.481,69.885,60.45,69.935,60.393,69.962]},{"b":[69.92,73.037,71.63,73.514],"l":[71.445,73.342,71.351,73.372,71.232,73.448,71.141,73.478,71.023,73.504,70.94,73.514,70.35,73.478,70.15,73.445,69.996,73.359,70.019,73.224,69.986,73.169,69.93,73.127,69.92,73.085,70.041,73.037,70.119,73.056,70.298,73.044,70.38,73.048,70.674,73.095,70.887,73.12,71.356,73.162,71.626,73.174,71.63,73.225,71.59,73.283,71.445,73.342]},{"b":[91.07,79.675,93.803,80.052],"l":[92.593,79.997,92.173,80.045,91.752,80.052,91.426,80.049,91.229,80.031,91.07,79.981,91.126,79.905,91.376,79.835,91.684,79.791,92.154,79.685,92.441,79.675,92.683,79.685,92.926,79.704,93.155,79.738,93.382,79.784,93.604,79.817,93.803,79.905,93.482,79.941,92.593,79.997]},{"b":[111.503,74.095,113.387,74.549],"l":[112.084,74.549,112.008,74.527,111.989,74.496,111.983,74.456,111.949,74.389,111.88,74.364,111.638,74.374,111.57,74.368,111.503,74.353,111.643,74.273,111.912,74.219,112.105,74.163,112.196,74.146,112.782,74.095,112.811,74.103,112.978,74.197,113.19,74.239,113.259,74.273,113.299,74.317,113.353,74.353,113.387,74.4,113.286,74.441,112.952,74.48,112.084,74.549]},{"b":[140.102,73.919,141.097,74.274],"l":[141.039,74.243,140.944,74.265,140.849,74.274,140.407,74.266,140.3,74.257,140.194,74.237,140.102,74.184,140.183,74.005,140.409,73.922,140.507,73.919,141.01,73.999,141.047,74.05,141.097,74.168,141.079,74.209,141.039,74.243]},{"b":[23.548,34.934,26.32,35.656],"l":[26.168,35.215,26.028,35.215,25.893,35.179,25.837,35.133,25.791,35.123,25.745,35.143,25.735,35.184,25.756,35.326,25.73,35.349,25.57,35.328,25.476,35.306,25.297,35.339,25.104,35.347,25.003,35.41,24.721,35.425,24.627,35.41,24.535,35.381,24.445,35.366,24.354,35.359,24.313,35.364,24.275,35.386,24.258,35.423,24.255,35.469,24.179,35.46,24.123,35.484,24.109,35.496,24.124,35.511,24.198,35.537,24.166,35.595,24.093,35.594,24.034,35.535,24.013,35.529,23.921,35.528,23.852,35.535,23.793,35.556,23.771,35.634,23.737,35.656,23.715,35.605,23.715,35.55,23.673,35.514,23.627,35.53,23.609,35.566,23.57,35.535,23.548,35.416,23.562,35.295,23.593,35.257,23.638,35.235,23.704,35.233,23.884,35.246,23.994,35.222,24.464,35.16,24.583,35.115,24.709,35.089,24.735,35.058,24.744,35.014,24.745,34.951,24.8,34.934,25.206,34.959,25.611,35.007,25.83,35.025,26.047,35.014,26.166,35.019,26.244,35.045,26.256,35.095,26.281,35.159,26.299,35.269,26.32,35.315,26.286,35.31,26.168,35.215]},{"b":[22.87,37.97,24.588,39.035],"l":[24.464,38.145,24.36,38.163,24.276,38.22,24.22,38.339,24.188,38.463,24.211,38.504,24.2,38.541,24.155,38.588,24.128,38.648,24.099,38.671,23.878,38.687,23.688,38.765,23.636,38.77,23.525,38.813,23.472,38.85,23.415,38.959,23.313,39.035,23.258,39.031,23.146,39.003,22.986,38.916,22.87,38.871,22.881,38.848,22.936,38.84,23.029,38.873,23.144,38.845,23.252,38.801,23.364,38.735,23.465,38.656,23.505,38.613,23.553,38.582,23.617,38.553,23.651,38.443,23.759,38.401,23.886,38.401,24.04,38.39,24.042,38.374,24.064,38.337,24.103,38.317,24.144,38.243,24.189,38.204,24.193,38.152,24.212,38.118,24.318,38.06,24.359,38.019,24.417,38.017,24.446,38.005,24.473,37.981,24.502,37.97,24.537,37.98,24.579,38.02,24.588,38.124,24.563,38.148,24.464,38.145]},{"b":[32.301,34.57,34.556,35.662],"l":[34.272,35.57,34.192,35.546,34.063,35.474,33.608,35.354,33.459,35.336,33.308,35.342,33.123,35.358,32.942,35.39,32.926,35.278,32.88,35.181,32.772,35.16,32.713,35.171,32.652,35.183,32.556,35.156,32.475,35.09,32.391,35.05,32.301,35.083,32.317,34.953,32.414,34.778,32.449,34.729,32.506,34.706,32.693,34.649,32.75,34.648,32.867,34.661,32.914,34.635,32.942,34.576,33.008,34.57,33.024,34.6,33.025,34.637,33.062,34.675,33.116,34.696,33.176,34.698,33.297,34.718,33.415,34.751,33.514,34.806,33.699,34.97,33.759,34.973,33.822,34.966,33.937,34.971,34.05,34.988,34.024,35.046,34.004,35.065,33.931,35.14,33.908,35.202,33.942,35.292,34.463,35.594,34.556,35.662,34.411,35.629,34.272,35.57]},{"b":[18.105,56.921,19.076,57.915],"l":[18.841,57.9,18.805,57.833,18.722,57.864,18.537,57.831,18.405,57.757,18.283,57.655,18.205,57.611,18.137,57.557,18.129,57.449,18.152,57.339,18.105,57.272,18.164,57.212,18.21,57.133,18.285,57.083,18.207,57.01,18.146,56.921,18.249,56.932,18.34,56.978,18.387,57.088,18.477,57.163,18.538,57.197,18.7,57.243,18.743,57.324,18.785,57.361,18.844,57.386,18.908,57.398,18.791,57.483,18.814,57.706,18.878,57.73,18.945,57.742,18.994,57.812,19.076,57.836,18.956,57.9,18.901,57.915,18.841,57.9]},{"b":[10.979,54.773,12.608,56.122],"l":[12.073,54.977,12.066,55.07,12.09,55.188,12.322,55.237,12.413,55.286,12.385,55.386,12.275,55.414,12.215,55.467,12.243,55.538,12.321,55.588,12.407,55.616,12.507,55.637,12.545,55.656,12.571,55.685,12.569,55.785,12.525,55.918,12.543,55.959,12.608,56.033,12.579,56.064,12.526,56.083,12.428,56.106,12.323,56.122,12.219,56.119,12.04,56.052,11.866,55.968,11.913,55.937,11.935,55.896,11.922,55.828,11.885,55.808,11.858,55.772,11.82,55.698,11.784,55.702,11.691,55.729,11.682,55.829,11.696,55.908,11.628,55.957,11.475,55.943,11.46,55.907,11.464,55.879,11.322,55.753,11.275,55.736,11.224,55.731,11.05,55.74,10.979,55.722,11.009,55.644,11.07,55.629,11.121,55.601,11.12,55.566,11.128,55.535,11.19,55.466,11.171,55.329,11.286,55.204,11.31,55.198,11.407,55.215,11.476,55.212,11.654,55.187,11.697,55.096,11.704,55.039,11.74,54.972,11.741,54.915,11.862,54.773,12.05,54.815,12.069,54.909,12.073,54.977]},{"b":[9.859,55.049,10.819,55.613],"l":[10.785,55.133,10.808,55.203,10.785,55.27,10.819,55.322,10.738,55.446,10.687,55.558,10.645,55.61,10.623,55.613,10.505,55.558,10.424,55.56,10.354,55.599,10.286,55.611,9.994,55.535,9.861,55.515,9.859,55.357,9.93,55.229,9.967,55.205,9.989,55.163,10.255,55.088,10.443,55.049,10.624,55.052,10.785,55.133]},{"b":[2.37,39.301,3.462,39.971],"l":[3.241,39.757,3.145,39.79,3.159,39.837,3.191,39.861,3.199,39.89,3.167,39.908,3.164,39.924,3.198,39.961,3.159,39.971,2.905,39.908,2.785,39.855,2.371,39.613,2.37,39.572,2.394,39.54,2.459,39.53,2.5,39.478,2.576,39.531,2.634,39.556,2.701,39.542,2.746,39.51,2.77,39.41,2.8,39.385,2.9,39.368,3.073,39.301,3.155,39.333,3.245,39.387,3.293,39.477,3.349,39.556,3.415,39.627,3.462,39.698,3.449,39.761,3.396,39.777,3.342,39.787,3.241,39.757]},{"b":[-123.346,-74.403,-120.272,-73.673],"l":[-120.722,-73.752,-121.497,-73.733,-121.967,-73.712,-122.436,-73.682,-122.91,-73.68,-123.012,-73.673,-123.112,-73.682,-123.249,-73.739,-123.292,-73.803,-123.346,-73.843,-123.191,-73.849,-123.035,-73.838,-122.992,-73.844,-122.951,-73.867,-122.625,-73.966,-122.71,-73.994,-122.881,-74.099,-122.875,-74.141,-122.794,-74.19,-122.765,-74.219,-122.891,-74.227,-122.956,-74.24,-122.938,-74.302,-122.859,-74.343,-122.287,-74.403,-121.062,-74.337,-121.002,-74.326,-121.005,-74.293,-121.036,-74.279,-121.054,-74.26,-121.019,-74.173,-120.99,-74.157,-120.272,-73.989,-120.312,-73.922,-120.378,-73.856,-120.556,-73.756,-120.722,-73.752]},{"b":[-127.429,-74.256,-123.8,-73.279],"l":[-127.233,-73.586,-127.232,-73.713,-127.212,-73.724,-127.122,-73.734,-127.006,-73.726,-126.902,-73.677,-126.838,-73.657,-126.711,-73.654,-126.583,-73.67,-126.538,-73.68,-126.496,-73.7,-126.466,-73.746,-126.472,-73.812,-126.244,-73.891,-125.887,-73.955,-125.683,-74.035,-125.549,-74.063,-125.421,-74.07,-125.09,-74.182,-124.873,-74.208,-124.199,-74.226,-123.982,-74.256,-123.937,-74.256,-123.837,-74.226,-123.839,-74.168,-123.811,-74.117,-123.8,-74.076,-123.851,-74.057,-123.932,-74.008,-124.129,-73.971,-124.152,-73.944,-124.101,-73.907,-124.042,-73.88,-124.129,-73.834,-124.54,-73.74,-124.617,-73.735,-124.694,-73.75,-124.993,-73.83,-125.109,-73.826,-125.224,-73.801,-125.327,-73.796,-125.552,-73.82,-125.674,-73.822,-125.799,-73.802,-125.857,-73.78,-125.86,-73.749,-125.828,-73.718,-125.723,-73.703,-125.612,-73.711,-125.276,-73.691,-125.264,-73.666,-125.326,-73.618,-125.504,-73.562,-125.561,-73.536,-125.627,-73.453,-125.736,-73.406,-125.857,-73.388,-125.976,-73.357,-126.065,-73.315,-126.33,-73.286,-126.597,-73.279,-126.83,-73.291,-126.978,-73.308,-127.124,-73.294,-127.268,-73.304,-127.394,-73.382,-127.429,-73.447,-127.414,-73.516,-127.332,-73.567,-127.233,-73.586]},{"b":[-67.809,-80.081,-65.504,-79.528],"l":[-66.785,-79.608,-66.881,-79.582,-66.979,-79.569,-67.438,-79.56,-67.688,-79.528,-67.809,-79.546,-67.771,-79.589,-67.719,-79.62,-67.077,-79.762,-66.994,-79.793,-66.962,-79.873,-66.904,-79.909,-66.41,-79.973,-66.367,-80.054,-66.319,-80.075,-66.267,-80.081,-66.174,-80.078,-65.989,-80.054,-65.899,-80.041,-65.504,-79.954,-65.54,-79.837,-65.579,-79.771,-65.87,-79.738,-66.014,-79.624,-66.274,-79.612,-66.785,-79.608]},{"b":[-58.438,-64.41,-57.222,-63.804],"l":[-57.295,-64.367,-57.338,-64.318,-57.414,-64.296,-57.328,-64.238,-57.222,-64.221,-57.273,-64.166,-57.249,-64.097,-57.517,-64.011,-57.48,-63.962,-57.593,-63.967,-57.71,-64.015,-57.741,-64.048,-57.774,-64.062,-57.809,-64.068,-57.846,-64.054,-57.827,-63.949,-57.781,-63.907,-57.779,-63.868,-57.831,-63.804,-57.926,-63.806,-57.971,-63.835,-58.07,-63.847,-58.146,-63.878,-58.275,-63.916,-58.342,-63.994,-58.425,-64.068,-58.438,-64.113,-58.398,-64.135,-58.352,-64.131,-58.25,-64.107,-58.147,-64.097,-58.162,-64.161,-58.138,-64.206,-58.02,-64.242,-58.304,-64.315,-58.214,-64.37,-58.169,-64.369,-58.022,-64.322,-57.971,-64.32,-57.921,-64.331,-57.952,-64.394,-57.91,-64.41,-57.871,-64.401,-57.823,-64.302,-57.703,-64.293,-57.671,-64.311,-57.683,-64.357,-57.581,-64.35,-57.388,-64.379,-57.295,-64.367]},{"b":[-64.272,-64.834,-62.837,-64.261],"l":[-63.131,-64.572,-63.277,-64.573,-63.181,-64.47,-63.271,-64.381,-63.23,-64.324,-63.334,-64.266,-63.486,-64.261,-63.534,-64.273,-63.606,-64.314,-63.683,-64.343,-63.668,-64.384,-63.674,-64.421,-63.916,-64.457,-63.897,-64.487,-63.867,-64.51,-64.171,-64.582,-64.226,-64.635,-64.272,-64.698,-64.184,-64.71,-64.099,-64.733,-64.007,-64.769,-63.804,-64.792,-63.77,-64.808,-63.74,-64.834,-63.647,-64.803,-63.558,-64.734,-63.458,-64.727,-63.355,-64.734,-63.275,-64.717,-63.203,-64.68,-63.026,-64.611,-62.837,-64.572,-62.928,-64.519,-63.032,-64.535,-63.131,-64.572]},{"b":[-76.512,-71.133,-73.55,-70.553],"l":[-74.205,-70.924,-73.695,-70.794,-73.55,-70.723,-73.707,-70.635,-73.879,-70.578,-73.958,-70.561,-74.038,-70.553,-74.113,-70.577,-74.115,-70.655,-74.225,-70.615,-74.401,-70.576,-74.456,-70.587,-74.469,-70.727,-74.527,-70.77,-74.59,-70.792,-74.79,-70.631,-74.898,-70.591,-74.954,-70.59,-75.007,-70.609,-75.038,-70.651,-75.06,-70.706,-75.127,-70.752,-75.21,-70.773,-76.035,-70.836,-76.249,-70.864,-76.378,-70.894,-76.5,-70.941,-76.512,-70.991,-76.421,-71.09,-76.364,-71.117,-76.271,-71.133,-76.176,-71.132,-74.806,-71.012,-74.505,-70.973,-74.205,-70.924]},{"b":[-75.813,-70.179,-74.438,-69.728],"l":[-74.849,-70.179,-74.672,-70.132,-74.578,-69.998,-74.46,-69.972,-74.438,-69.95,-74.465,-69.917,-74.55,-69.861,-74.81,-69.752,-74.987,-69.728,-75.136,-69.741,-75.179,-69.735,-75.265,-69.749,-75.314,-69.817,-75.34,-69.84,-75.681,-69.882,-75.76,-69.916,-75.813,-69.984,-75.804,-70.038,-75.764,-70.085,-75.727,-70.096,-75.268,-70.149,-74.849,-70.179]},{"b":[-102.314,-72.578,-95.531,-71.764],"l":[-99.735,-72.033,-99.784,-72.044,-99.833,-72.046,-99.985,-71.939,-100.085,-71.837,-100.219,-71.833,-100.401,-71.866,-102.128,-71.985,-102.237,-72.009,-102.288,-72.032,-102.314,-72.081,-102.265,-72.135,-102.022,-72.185,-101.903,-72.19,-101.785,-72.178,-101.602,-72.176,-100.357,-72.278,-100.195,-72.273,-100.104,-72.287,-100.014,-72.312,-99.672,-72.38,-99.434,-72.407,-99.149,-72.472,-98.882,-72.473,-98.641,-72.49,-98.408,-72.548,-98.163,-72.556,-97.828,-72.557,-97.596,-72.548,-97.366,-72.522,-97.25,-72.521,-97.028,-72.574,-96.915,-72.578,-96.804,-72.558,-96.693,-72.548,-96.052,-72.577,-96.03,-72.554,-96.014,-72.525,-96.078,-72.454,-95.826,-72.439,-95.575,-72.41,-95.531,-72.249,-95.61,-72.175,-95.609,-72.068,-95.685,-72.057,-95.906,-72.122,-96.482,-72.208,-96.718,-72.255,-96.799,-72.259,-96.89,-72.247,-96.979,-72.222,-96.715,-72.132,-96.298,-72.045,-96.125,-71.896,-96.383,-71.836,-96.869,-71.851,-97.089,-71.944,-97.155,-72.045,-97.196,-72.091,-97.242,-72.132,-97.345,-72.189,-97.46,-72.188,-97.526,-72.149,-97.582,-72.095,-97.473,-72,-97.585,-71.883,-97.816,-71.919,-97.923,-72.117,-98.168,-72.123,-98.176,-72.018,-98.091,-71.912,-98.189,-71.82,-98.394,-71.782,-98.615,-71.764,-98.965,-71.854,-99.082,-71.933,-99.254,-71.972,-99.563,-71.945,-99.735,-72.033]},{"b":[-76.096,-73.611,-74.224,-72.82],"l":[-75.377,-72.82,-75.731,-72.879,-75.702,-72.911,-75.6,-72.953,-75.439,-72.994,-75.244,-73.009,-75.276,-73.05,-75.417,-73.052,-75.468,-73.101,-75.506,-73.109,-75.775,-73.054,-75.898,-73.056,-76.018,-73.085,-76.062,-73.109,-76.096,-73.15,-76.09,-73.203,-76.053,-73.255,-76.003,-73.288,-75.901,-73.333,-74.575,-73.611,-74.452,-73.565,-74.366,-73.464,-74.467,-73.427,-74.551,-73.369,-74.575,-73.328,-74.615,-73.311,-74.668,-73.275,-74.522,-73.244,-74.499,-73.229,-74.354,-73.098,-74.224,-72.997,-74.276,-72.951,-74.336,-72.919,-74.474,-72.894,-75.377,-72.82]},{"b":[-69.138,-67.753,-67.681,-66.624],"l":[-68.901,-67.744,-68.818,-67.753,-68.734,-67.746,-68.664,-67.723,-68.622,-67.723,-68.58,-67.733,-68.507,-67.707,-68.439,-67.656,-68.381,-67.555,-68.325,-67.532,-68.25,-67.54,-68.175,-67.558,-68.093,-67.539,-67.988,-67.474,-67.969,-67.45,-68.007,-67.418,-68.144,-67.382,-68.235,-67.372,-68.175,-67.344,-68.03,-67.3,-67.956,-67.255,-67.848,-67.219,-67.688,-67.147,-67.761,-67.123,-67.828,-67.082,-67.876,-67.062,-67.949,-67.045,-67.968,-67.032,-67.969,-66.982,-67.932,-66.845,-67.741,-66.746,-67.681,-66.709,-67.711,-66.633,-67.831,-66.624,-67.938,-66.657,-68.336,-66.802,-68.417,-66.853,-68.575,-66.993,-68.656,-67.07,-68.734,-67.157,-68.82,-67.234,-69.082,-67.403,-69.133,-67.453,-69.138,-67.515,-69.12,-67.578,-69.098,-67.603,-68.982,-67.68,-68.901,-67.744]},{"b":[-180,70.919,-177.498,71.596],"l":[-180,71.538,-179.845,71.551,-179.691,71.578,-179.546,71.582,-179.402,71.567,-179.256,71.572,-179.112,71.596,-178.994,71.593,-178.876,71.577,-178.439,71.541,-178.354,71.529,-178.215,71.482,-178.134,71.465,-178.057,71.438,-177.975,71.391,-177.817,71.34,-177.584,71.282,-177.532,71.263,-177.498,71.219,-177.524,71.167,-177.822,71.068,-178.063,71.042,-178.528,71.015,-179.157,70.94,-179.416,70.919,-179.507,70.923,-179.734,70.972,-180,70.993]},{"b":[-59.649,-52.308,-57.792,-51.27],"l":[-57.792,-51.636,-57.866,-51.605,-57.96,-51.583,-57.915,-51.534,-57.808,-51.518,-57.923,-51.404,-57.977,-51.384,-58.206,-51.405,-58.259,-51.417,-58.289,-51.458,-58.276,-51.506,-58.241,-51.551,-58.235,-51.579,-58.272,-51.575,-58.474,-51.509,-58.509,-51.484,-58.519,-51.424,-58.467,-51.412,-58.407,-51.418,-58.379,-51.373,-58.426,-51.324,-58.506,-51.308,-58.698,-51.329,-58.85,-51.27,-58.917,-51.272,-58.887,-51.358,-59.097,-51.491,-59.099,-51.59,-59.065,-51.65,-59.06,-51.685,-59.095,-51.704,-59.18,-51.712,-59.262,-51.737,-59.309,-51.78,-59.571,-51.925,-59.537,-51.971,-59.649,-52.077,-59.649,-52.134,-59.532,-52.236,-59.396,-52.308,-59.342,-52.196,-59.256,-52.183,-59.163,-52.202,-59.068,-52.173,-59.196,-52.018,-59.131,-52.008,-58.653,-52.099,-58.638,-52.023,-58.643,-51.995,-58.683,-51.936,-58.336,-51.864,-58.218,-51.822,-58.151,-51.765,-58.004,-51.743,-57.838,-51.709,-57.831,-51.685,-57.792,-51.636]},{"b":[-38.017,-54.867,-35.799,-53.984],"l":[-35.895,-54.555,-35.965,-54.568,-36.033,-54.568,-36.073,-54.554,-36.117,-54.458,-36.173,-54.382,-36.236,-54.36,-36.285,-54.289,-36.326,-54.251,-36.386,-54.279,-36.407,-54.303,-36.449,-54.308,-36.541,-54.248,-36.647,-54.262,-36.607,-54.19,-36.704,-54.108,-36.76,-54.108,-36.805,-54.101,-36.849,-54.085,-36.929,-54.081,-37.006,-54.114,-37.103,-54.066,-37.233,-54.061,-37.369,-54.009,-37.382,-53.984,-37.536,-53.994,-37.946,-53.996,-38.017,-54.008,-37.913,-54.029,-37.619,-54.042,-37.689,-54.077,-37.692,-54.135,-37.631,-54.167,-37.498,-54.156,-37.158,-54.271,-37.083,-54.312,-37.007,-54.341,-36.886,-54.339,-36.852,-54.366,-36.824,-54.404,-36.735,-54.467,-36.628,-54.496,-36.507,-54.511,-36.472,-54.534,-36.446,-54.571,-36.311,-54.694,-36.252,-54.78,-36.124,-54.853,-36.085,-54.867,-35.939,-54.834,-35.867,-54.792,-35.799,-54.763,-35.913,-54.711,-35.922,-54.637,-35.895,-54.555]},{"b":[-74.387,-43.372,-73.423,-41.796],"l":[-74.114,-43.358,-73.99,-43.357,-73.919,-43.372,-73.849,-43.367,-73.773,-43.346,-73.738,-43.291,-73.75,-43.159,-73.65,-43.127,-73.541,-43.074,-73.473,-42.993,-73.436,-42.937,-73.511,-42.847,-73.568,-42.762,-73.673,-42.704,-73.767,-42.622,-73.789,-42.586,-73.715,-42.545,-73.653,-42.529,-73.634,-42.508,-73.549,-42.493,-73.471,-42.466,-73.525,-42.393,-73.533,-42.314,-73.439,-42.278,-73.423,-42.193,-73.454,-42.166,-73.478,-42.047,-73.517,-41.981,-73.528,-41.896,-73.731,-41.877,-74.037,-41.796,-74.063,-41.823,-74.031,-41.854,-74.019,-41.891,-74.057,-42.002,-74.059,-42.056,-74.072,-42.106,-74.16,-42.216,-74.17,-42.269,-74.164,-42.325,-74.174,-42.382,-74.194,-42.436,-74.199,-42.481,-74.156,-42.591,-74.209,-42.879,-74.289,-43.079,-74.373,-43.186,-74.387,-43.232,-74.355,-43.264,-74.239,-43.319,-74.114,-43.358]},{"b":[-60.961,-52.195,-59.268,-51.354],"l":[-59.573,-51.681,-59.514,-51.627,-59.437,-51.593,-59.392,-51.556,-59.354,-51.511,-59.294,-51.479,-59.268,-51.428,-59.321,-51.384,-59.388,-51.36,-59.465,-51.411,-59.493,-51.396,-59.711,-51.439,-59.788,-51.446,-59.842,-51.403,-59.917,-51.388,-60.009,-51.411,-60.142,-51.481,-60.286,-51.462,-60.445,-51.399,-60.516,-51.354,-60.568,-51.358,-60.518,-51.428,-60.523,-51.463,-60.506,-51.485,-60.415,-51.545,-60.303,-51.58,-60.245,-51.639,-60.281,-51.656,-60.467,-51.697,-60.528,-51.696,-60.583,-51.713,-60.5,-51.757,-60.38,-51.735,-60.328,-51.718,-60.277,-51.717,-60.238,-51.734,-60.238,-51.772,-60.289,-51.801,-60.334,-51.84,-60.45,-51.877,-60.591,-51.952,-60.762,-51.946,-60.961,-52.057,-60.812,-52.148,-60.686,-52.188,-60.508,-52.195,-60.484,-52.17,-60.452,-52.16,-60.384,-52.154,-60.353,-52.14,-60.288,-52.074,-60.246,-51.986,-60.194,-51.983,-60.132,-51.994,-59.99,-51.984,-59.921,-51.97,-59.715,-51.808,-59.573,-51.681]},{"b":[-75.57,-50.019,-74.459,-48.705],"l":[-75.032,-49.836,-74.994,-49.752,-74.991,-49.606,-74.981,-49.564,-74.96,-49.533,-74.744,-49.422,-74.724,-49.424,-74.719,-49.437,-74.727,-49.452,-74.781,-49.489,-74.805,-49.516,-74.812,-49.605,-74.859,-49.634,-74.882,-49.692,-74.88,-49.726,-74.822,-49.814,-74.825,-49.879,-74.811,-49.93,-74.763,-50.011,-74.703,-50.019,-74.595,-50.007,-74.57,-49.991,-74.543,-49.919,-74.496,-49.859,-74.472,-49.786,-74.459,-49.691,-74.471,-49.669,-74.516,-49.66,-74.522,-49.623,-74.484,-49.442,-74.467,-49.295,-74.476,-49.148,-74.531,-48.813,-74.546,-48.767,-74.567,-48.755,-74.652,-48.75,-74.747,-48.709,-74.793,-48.705,-74.896,-48.733,-74.97,-48.791,-74.981,-48.819,-74.945,-48.889,-74.949,-48.96,-75.037,-49.022,-75.184,-49.084,-75.21,-49.148,-75.094,-49.185,-75.086,-49.27,-75.217,-49.293,-75.27,-49.263,-75.327,-49.269,-75.433,-49.322,-75.467,-49.359,-75.429,-49.408,-75.364,-49.462,-75.306,-49.494,-75.337,-49.628,-75.521,-49.622,-75.57,-49.697,-75.55,-49.791,-75.451,-49.77,-75.3,-49.847,-75.167,-49.856,-75.066,-49.852,-75.032,-49.836]},{"b":[-73.445,-44.86,-72.764,-44.335],"l":[-73.397,-44.774,-73.35,-44.833,-73.228,-44.86,-72.986,-44.78,-72.897,-44.712,-72.845,-44.638,-72.764,-44.549,-72.776,-44.509,-72.842,-44.458,-73.028,-44.384,-73.208,-44.335,-73.26,-44.35,-73.272,-44.394,-73.266,-44.44,-73.282,-44.49,-73.315,-44.531,-73.404,-44.596,-73.445,-44.641,-73.42,-44.725,-73.397,-44.774]},{"b":[-64.485,49.079,-61.696,49.944],"l":[-63.885,49.658,-63.777,49.602,-63.676,49.534,-63.626,49.46,-63.566,49.399,-63.042,49.225,-62.8,49.171,-62.553,49.141,-62.22,49.079,-61.801,49.094,-61.746,49.106,-61.696,49.139,-61.736,49.204,-61.817,49.284,-62.043,49.39,-62.133,49.407,-62.633,49.624,-62.859,49.705,-63.089,49.773,-63.292,49.817,-63.76,49.875,-64.131,49.942,-64.244,49.944,-64.373,49.926,-64.485,49.887,-64.44,49.828,-63.885,49.658]},{"b":[-61.495,45.573,-59.828,47.01],"l":[-59.842,45.942,-59.828,45.965,-59.934,46.019,-59.881,46.062,-59.849,46.113,-59.85,46.141,-59.865,46.16,-59.961,46.191,-60.092,46.206,-60.226,46.196,-60.244,46.27,-60.298,46.311,-60.377,46.285,-60.431,46.256,-60.505,46.204,-60.586,46.117,-60.573,46.061,-60.733,45.957,-60.705,45.933,-60.461,45.969,-60.472,45.947,-60.699,45.773,-60.738,45.751,-60.806,45.738,-60.878,45.748,-60.93,45.748,-61.059,45.703,-61.092,45.748,-61.052,45.795,-60.972,45.838,-60.971,45.856,-61.038,45.882,-60.984,45.911,-60.868,45.949,-60.865,45.983,-60.937,45.986,-61.071,45.937,-61.105,45.945,-60.912,46.045,-60.831,46.074,-60.745,46.093,-60.577,46.172,-60.534,46.215,-60.495,46.27,-60.508,46.303,-60.482,46.414,-60.384,46.613,-60.333,46.737,-60.332,46.768,-60.425,46.923,-60.431,46.963,-60.408,47.004,-60.489,47.01,-60.571,46.999,-60.617,46.976,-60.76,46.863,-60.87,46.797,-60.932,46.729,-60.983,46.65,-61.241,46.303,-61.302,46.244,-61.409,46.17,-61.481,46.06,-61.495,45.941,-61.45,45.716,-61.408,45.669,-61.323,45.598,-61.284,45.574,-61.236,45.573,-61.186,45.585,-61.084,45.582,-60.979,45.606,-60.872,45.611,-60.764,45.591,-60.673,45.591,-60.386,45.655,-60.205,45.743,-60.114,45.819,-60.016,45.88,-59.842,45.942]},{"b":[-171.819,62.956,-168.716,63.727],"l":[-168.996,63.347,-169.221,63.349,-169.295,63.358,-169.428,63.348,-169.555,63.373,-169.587,63.407,-169.624,63.431,-169.777,63.448,-170.017,63.492,-170.056,63.527,-170.082,63.577,-170.122,63.618,-170.171,63.641,-170.299,63.681,-170.43,63.699,-170.552,63.688,-170.673,63.669,-170.875,63.594,-171.035,63.585,-171.197,63.609,-171.343,63.62,-171.448,63.616,-171.463,63.64,-171.646,63.727,-171.746,63.703,-171.804,63.581,-171.818,63.53,-171.819,63.477,-171.791,63.425,-171.738,63.394,-171.632,63.351,-171.519,63.332,-171.401,63.339,-171.287,63.372,-171.176,63.416,-171.061,63.446,-170.954,63.453,-170.848,63.444,-170.527,63.379,-170.424,63.349,-170.324,63.311,-170.273,63.284,-170.243,63.232,-170.19,63.196,-170.115,63.194,-169.988,63.173,-169.863,63.14,-169.819,63.122,-169.778,63.094,-169.72,62.99,-169.676,62.956,-169.623,62.969,-169.571,62.997,-169.559,63.058,-169.471,63.121,-169.365,63.171,-169.109,63.185,-168.852,63.171,-168.761,63.214,-168.716,63.311,-168.996,63.347]},{"b":[-167.436,59.764,-165.592,60.397],"l":[-165.631,60.028,-165.712,60.069,-165.707,60.101,-165.714,60.173,-165.689,60.224,-165.696,60.282,-165.73,60.314,-165.784,60.336,-165.841,60.346,-165.995,60.331,-166.044,60.334,-166.135,60.384,-166.185,60.397,-166.247,60.391,-166.364,60.365,-166.42,60.382,-166.476,60.383,-166.599,60.339,-166.731,60.316,-166.784,60.296,-166.836,60.217,-167.252,60.234,-167.344,60.224,-167.436,60.207,-167.295,60.096,-167.139,60.009,-166.985,59.984,-166.628,59.865,-166.343,59.834,-166.262,59.815,-166.188,59.774,-166.149,59.764,-166.107,59.775,-166.131,59.82,-166.1,59.85,-165.947,59.89,-165.769,59.893,-165.592,59.913,-165.605,59.973,-165.631,60.028]},{"b":[-164.904,54.404,-163.083,55.051],"l":[-163.275,54.766,-163.337,54.783,-163.379,54.816,-163.476,54.981,-163.511,55.014,-163.553,55.038,-163.607,55.051,-163.807,55.049,-163.868,55.039,-164.145,54.955,-164.274,54.9,-164.424,54.913,-164.479,54.907,-164.53,54.881,-164.706,54.692,-164.751,54.663,-164.888,54.608,-164.904,54.568,-164.904,54.545,-164.866,54.461,-164.823,54.419,-164.744,54.407,-164.591,54.404,-164.463,54.427,-164.404,54.448,-164.347,54.482,-164.235,54.571,-164.171,54.603,-164.073,54.621,-163.583,54.626,-163.531,54.638,-163.358,54.736,-163.083,54.669,-163.089,54.686,-163.135,54.723,-163.187,54.748,-163.275,54.766]},{"b":[-78.34,17.715,-76.211,18.522],"l":[-76.35,18.152,-76.701,18.257,-76.793,18.304,-76.908,18.39,-76.959,18.402,-77.014,18.403,-77.14,18.421,-77.261,18.457,-77.354,18.466,-77.452,18.467,-77.873,18.522,-77.927,18.501,-77.978,18.468,-78.095,18.445,-78.217,18.448,-78.252,18.426,-78.326,18.35,-78.34,18.287,-78.294,18.218,-78.074,18.191,-78.044,18.174,-77.963,18.048,-77.881,18.019,-77.849,17.988,-77.768,17.877,-77.671,17.86,-77.464,17.856,-77.361,17.834,-77.28,17.78,-77.205,17.715,-77.158,17.845,-77.119,17.88,-77.071,17.901,-77.036,17.854,-76.944,17.849,-76.896,17.904,-76.853,17.974,-76.795,17.976,-76.748,17.965,-76.774,17.94,-76.669,17.928,-76.625,17.901,-76.525,17.866,-76.416,17.868,-76.301,17.88,-76.211,17.914,-76.233,17.97,-76.35,18.152]},{"b":[-67.264,17.947,-65.621,18.522],"l":[-65.629,18.381,-65.756,18.402,-65.879,18.444,-66.07,18.469,-66.093,18.469,-66.068,18.428,-66.098,18.425,-66.129,18.445,-66.153,18.471,-66.189,18.476,-66.813,18.493,-67.06,18.522,-67.113,18.515,-67.159,18.499,-67.172,18.436,-67.213,18.394,-67.264,18.365,-67.239,18.321,-67.204,18.283,-67.172,18.224,-67.174,18.153,-67.197,17.994,-67.142,17.967,-67.013,17.968,-66.961,17.954,-66.9,17.948,-66.838,17.955,-66.772,17.987,-66.598,17.978,-66.511,17.987,-66.409,17.951,-66.326,17.964,-66.286,17.95,-66.245,17.947,-66.135,17.949,-65.971,17.974,-65.834,18.057,-65.782,18.129,-65.718,18.187,-65.621,18.242,-65.629,18.381]},{"b":[-154.712,56.774,-152.215,57.972],"l":[-152.411,57.646,-152.483,57.703,-152.485,57.734,-152.419,57.782,-152.412,57.806,-152.429,57.826,-152.512,57.851,-152.616,57.849,-152.696,57.832,-152.85,57.776,-152.891,57.769,-152.898,57.824,-152.85,57.897,-152.943,57.936,-153.16,57.972,-153.226,57.958,-153.169,57.911,-153.175,57.879,-153.201,57.863,-153.2,57.82,-153.217,57.796,-153.252,57.79,-153.357,57.805,-153.39,57.798,-153.423,57.779,-153.454,57.747,-153.488,57.731,-153.524,57.731,-153.569,57.761,-153.663,57.858,-153.696,57.871,-153.769,57.88,-153.806,57.875,-153.842,57.863,-153.904,57.82,-153.906,57.791,-153.879,57.757,-153.808,57.715,-153.693,57.663,-153.69,57.641,-153.799,57.647,-153.838,57.636,-153.818,57.596,-153.798,57.443,-153.757,57.367,-153.688,57.305,-153.755,57.325,-153.805,57.358,-153.882,57.439,-153.947,57.53,-154.008,57.556,-154.016,57.567,-153.995,57.587,-154.03,57.631,-154.116,57.651,-154.179,57.652,-154.281,57.638,-154.387,57.59,-154.535,57.559,-154.673,57.446,-154.712,57.366,-154.706,57.335,-154.569,57.206,-154.499,57.037,-154.339,56.921,-154.261,56.912,-154.208,56.964,-154.184,57.005,-154.191,57.036,-154.209,57.063,-154.239,57.087,-154.27,57.099,-154.381,57.097,-154.377,57.107,-154.324,57.132,-154.244,57.143,-154.135,57.141,-154.065,57.134,-154.035,57.122,-154.025,57.108,-154.08,57.061,-154.103,57.021,-154.084,57.02,-153.999,57.05,-153.88,57.004,-153.804,56.998,-153.793,56.99,-154.071,56.821,-154.07,56.805,-154.051,56.788,-154.027,56.778,-153.973,56.774,-153.757,56.858,-153.643,56.961,-153.631,56.984,-153.633,57.01,-153.647,57.03,-153.733,57.052,-153.588,57.078,-153.524,57.103,-153.504,57.138,-153.444,57.167,-153.274,57.226,-153.052,57.238,-152.99,57.282,-152.879,57.321,-152.789,57.321,-152.714,57.331,-152.679,57.345,-152.693,57.38,-152.72,57.411,-152.781,57.453,-152.957,57.46,-152.997,57.469,-152.941,57.498,-152.912,57.508,-152.831,57.503,-152.631,57.472,-152.412,57.455,-152.381,57.46,-152.337,57.482,-152.216,57.577,-152.215,57.598,-152.237,57.615,-152.411,57.646]},{"b":[-153.381,57.994,-151.974,58.451],"l":[-152.165,58.178,-152.109,58.161,-152.069,58.178,-151.983,58.244,-151.974,58.31,-151.998,58.314,-152.037,58.307,-152.079,58.312,-152.125,58.374,-152.198,58.363,-152.316,58.413,-152.343,58.412,-152.381,58.352,-152.417,58.36,-152.478,58.4,-152.544,58.428,-152.612,58.446,-152.675,58.451,-152.841,58.416,-152.844,58.396,-152.769,58.346,-152.772,58.279,-152.815,58.276,-152.895,58.294,-152.976,58.297,-153.116,58.239,-153.381,58.087,-153.305,58.063,-152.983,57.997,-152.928,57.994,-152.841,58.014,-152.782,58.016,-152.764,58.031,-152.683,58.063,-152.639,58.102,-152.598,58.163,-152.571,58.168,-152.558,58.119,-152.538,58.101,-152.452,58.129,-152.381,58.124,-152.309,58.134,-152.305,58.154,-152.333,58.187,-152.334,58.208,-152.268,58.252,-152.252,58.251,-152.224,58.214,-152.187,58.185,-152.165,58.178]},{"b":[-131.846,55.166,-130.965,55.955],"l":[-130.998,55.728,-131.121,55.857,-131.236,55.949,-131.269,55.955,-131.625,55.832,-131.648,55.586,-131.759,55.503,-131.846,55.416,-131.842,55.359,-131.811,55.223,-131.762,55.166,-131.724,55.218,-131.641,55.299,-131.522,55.341,-131.475,55.373,-131.448,55.409,-131.422,55.368,-131.451,55.316,-131.421,55.276,-131.367,55.266,-131.316,55.269,-131.262,55.22,-131.188,55.206,-131.083,55.267,-131.014,55.379,-130.979,55.489,-130.965,55.568,-130.966,55.67,-130.998,55.728]},{"b":[-133.755,54.713,-131.976,56.339],"l":[-132.469,54.938,-132.37,54.922,-132.341,54.907,-132.266,54.802,-132.189,54.735,-132.134,54.713,-132.065,54.713,-132.022,54.726,-131.981,54.805,-131.983,54.835,-131.997,54.869,-131.997,54.901,-131.978,54.94,-131.978,54.969,-132,55.034,-131.976,55.209,-132.005,55.231,-132.166,55.218,-132.207,55.224,-132.215,55.237,-132.19,55.255,-132.158,55.3,-132.16,55.323,-132.215,55.384,-132.272,55.399,-132.418,55.483,-132.592,55.464,-132.631,55.473,-132.582,55.503,-132.548,55.544,-132.529,55.59,-132.511,55.594,-132.296,55.507,-132.196,55.479,-132.173,55.481,-132.215,55.519,-132.289,55.558,-132.43,55.687,-132.497,55.798,-132.534,55.842,-132.598,55.895,-132.758,55.995,-133.097,56.09,-133.101,56.1,-133.08,56.129,-133.075,56.156,-133.082,56.194,-133.104,56.235,-133.144,56.279,-133.203,56.32,-133.377,56.318,-133.566,56.339,-133.599,56.316,-133.594,56.216,-133.544,56.177,-133.531,56.146,-133.599,56.094,-133.755,55.999,-133.743,55.965,-133.684,55.943,-133.539,55.999,-133.371,56.036,-133.289,56.019,-133.252,55.957,-133.242,55.921,-133.308,55.886,-133.322,55.845,-133.412,55.798,-133.447,55.797,-133.537,55.832,-133.584,55.837,-133.664,55.804,-133.68,55.785,-133.64,55.749,-133.553,55.691,-133.503,55.696,-133.369,55.689,-133.343,55.651,-133.298,55.607,-133.244,55.595,-133.09,55.613,-133.033,55.59,-133.078,55.535,-133.082,55.504,-132.959,55.396,-132.971,55.376,-133.03,55.378,-133.103,55.36,-133.119,55.328,-133.061,55.301,-132.913,55.188,-132.782,55.048,-132.704,55.03,-132.683,55.074,-132.702,55.131,-132.665,55.147,-132.622,55.136,-132.627,55.11,-132.588,55.052,-132.594,54.996,-132.549,54.953,-132.486,54.95,-132.469,54.938]},{"b":[-133.979,56.452,-132.951,57.069],"l":[-133.979,57.01,-133.917,56.967,-133.823,56.924,-133.758,56.877,-133.681,56.798,-133.688,56.71,-133.658,56.596,-133.649,56.517,-133.631,56.484,-133.603,56.464,-133.484,56.452,-133.383,56.474,-133.213,56.465,-133.181,56.474,-133.158,56.495,-133.145,56.528,-133.144,56.567,-133.157,56.611,-133.178,56.645,-133.227,56.689,-133.24,56.726,-133.309,56.786,-133.332,56.819,-133.329,56.83,-133.244,56.796,-133.132,56.683,-133.035,56.621,-133.004,56.624,-132.976,56.647,-132.959,56.677,-132.954,56.713,-132.963,56.783,-132.951,56.85,-132.954,56.88,-132.996,56.93,-133.071,56.974,-133.196,57.003,-133.264,57.005,-133.3,56.972,-133.366,57.004,-133.708,57.063,-133.866,57.069,-133.962,57.043,-133.979,57.01]},{"b":[-135.823,56.216,-134.611,57.534],"l":[-134.885,57.242,-134.97,57.351,-135.065,57.417,-135.131,57.432,-135.346,57.533,-135.449,57.534,-135.487,57.517,-135.57,57.425,-135.581,57.39,-135.625,57.354,-135.681,57.333,-135.787,57.317,-135.823,57.28,-135.821,57.23,-135.768,57.1,-135.782,57.058,-135.812,57.01,-135.662,57.034,-135.609,57.071,-135.502,57.244,-135.455,57.249,-135.375,57.188,-135.341,57.082,-135.267,57.049,-135.211,57.045,-135.2,57.027,-135.315,56.932,-135.338,56.894,-135.341,56.851,-135.331,56.822,-135.285,56.8,-135.163,56.824,-135.147,56.802,-135.159,56.725,-135.097,56.703,-135.018,56.66,-134.928,56.667,-134.883,56.679,-134.875,56.67,-134.933,56.616,-134.967,56.596,-134.982,56.564,-134.981,56.519,-134.95,56.457,-134.848,56.323,-134.806,56.281,-134.75,56.241,-134.682,56.216,-134.654,56.227,-134.63,56.302,-134.632,56.436,-134.657,56.523,-134.652,56.556,-134.624,56.579,-134.611,56.603,-134.621,56.718,-134.634,56.762,-134.677,56.842,-134.769,57.054,-134.823,57.157,-134.885,57.242]},{"b":[-134.933,57.043,-133.823,58.355],"l":[-134.1,57.3,-133.925,57.337,-133.911,57.353,-133.909,57.369,-133.974,57.451,-133.921,57.492,-133.937,57.582,-133.961,57.614,-134.084,57.712,-134.267,57.885,-134.3,57.963,-134.307,58.034,-134.292,58.045,-134.25,58.049,-134.213,58.038,-134.18,58.011,-134.178,57.982,-134.105,57.879,-134.067,57.84,-134.032,57.821,-133.996,57.778,-133.925,57.671,-133.827,57.618,-133.823,57.629,-133.869,57.708,-133.904,57.789,-133.966,57.874,-134.07,57.995,-134.24,58.144,-134.426,58.139,-134.68,58.162,-134.733,58.225,-134.837,58.32,-134.923,58.355,-134.933,58.329,-134.908,58.263,-134.87,58.202,-134.82,58.147,-134.781,58.078,-134.754,57.995,-134.695,57.736,-134.66,57.638,-134.595,57.568,-134.487,57.482,-134.489,57.42,-134.576,57.232,-134.62,57.196,-134.613,57.138,-134.592,57.092,-134.555,57.058,-134.516,57.043,-134.435,57.057,-134.26,57.147,-134.1,57.3]},{"b":[-136.569,57.42,-134.873,58.269],"l":[-135.692,57.42,-135.618,57.48,-135.621,57.597,-135.609,57.651,-135.564,57.666,-135.498,57.662,-135.22,57.574,-135.085,57.511,-134.931,57.481,-134.873,57.589,-134.897,57.648,-134.979,57.724,-135.25,57.733,-135.338,57.769,-135.231,57.816,-135.165,57.796,-135.103,57.794,-134.971,57.817,-134.928,57.953,-134.955,58.015,-135.002,58.051,-135.163,58.096,-135.347,58.124,-135.375,58.122,-135.421,58.102,-135.572,58.009,-135.613,57.992,-135.671,58.012,-135.693,58.039,-135.615,58.057,-135.586,58.124,-135.587,58.147,-135.73,58.244,-135.787,58.269,-135.882,58.247,-135.947,58.206,-135.994,58.197,-136.094,58.198,-136.142,58.154,-136.144,58.098,-136.246,58.157,-136.322,58.219,-136.37,58.143,-136.454,58.108,-136.512,58.096,-136.525,58.051,-136.569,57.972,-136.46,57.873,-136.378,57.84,-136.077,57.675,-135.997,57.535,-135.911,57.447,-135.692,57.42]},{"b":[-129.184,52.29,-128.507,53.28],"l":[-128.766,52.598,-128.749,52.556,-128.736,52.468,-128.731,52.357,-128.679,52.29,-128.624,52.34,-128.577,52.452,-128.51,52.519,-128.507,52.621,-128.552,52.94,-128.633,53.113,-128.74,53.179,-128.858,53.229,-128.97,53.274,-129.033,53.28,-129.06,53.241,-129.084,53.14,-129.111,53.091,-129.178,53.018,-129.184,52.991,-129.176,52.965,-129.095,52.892,-129.085,52.822,-129.023,52.756,-128.9,52.674,-128.831,52.679,-128.77,52.751,-128.746,52.763,-128.766,52.598]},{"b":[-133.098,53.189,-131.668,54.159],"l":[-132.347,53.189,-132.011,53.265,-131.957,53.309,-131.928,53.379,-131.922,53.588,-131.889,53.714,-131.821,53.842,-131.703,53.986,-131.685,54.023,-131.668,54.141,-131.696,54.143,-131.82,54.077,-131.941,54.042,-132.134,54.034,-132.136,53.996,-132.111,53.9,-132.114,53.86,-132.152,53.807,-132.172,53.707,-132.187,53.685,-132.464,53.653,-132.535,53.652,-132.567,53.664,-132.574,53.675,-132.565,53.688,-132.215,53.815,-132.175,53.847,-132.155,53.875,-132.166,53.955,-132.216,54.028,-132.262,54.076,-132.303,54.099,-132.344,54.106,-132.564,54.069,-132.656,54.127,-132.893,54.141,-132.991,54.158,-133.048,54.159,-133.064,54.144,-133.098,54.006,-133.098,53.92,-133.079,53.837,-133.052,53.778,-132.913,53.629,-132.9,53.605,-132.898,53.563,-132.845,53.508,-132.67,53.459,-132.431,53.35,-132.425,53.337,-132.462,53.338,-132.546,53.359,-132.655,53.371,-132.693,53.368,-132.748,53.31,-132.675,53.263,-132.52,53.194,-132.347,53.189]},{"b":[-132.547,52.154,-131.116,53.231],"l":[-131.116,52.219,-131.143,52.291,-131.259,52.292,-131.32,52.303,-131.327,52.318,-131.26,52.39,-131.26,52.416,-131.274,52.426,-131.383,52.416,-131.43,52.422,-131.444,52.453,-131.591,52.578,-131.573,52.623,-131.455,52.702,-131.611,52.745,-131.727,52.756,-131.81,52.819,-131.904,52.867,-131.972,52.88,-131.916,52.909,-131.88,52.915,-131.795,52.885,-131.635,52.922,-131.622,53.02,-131.652,53.103,-131.754,53.196,-131.853,53.23,-131.893,53.231,-131.99,53.202,-132.036,53.179,-132.154,53.16,-132.345,53.136,-132.524,53.145,-132.547,53.138,-132.505,53.087,-132.469,53.072,-132.144,52.999,-132.145,52.957,-132.23,52.948,-132.258,52.934,-132.26,52.907,-132.239,52.867,-132.165,52.783,-132.092,52.753,-131.81,52.542,-131.624,52.444,-131.562,52.4,-131.511,52.322,-131.422,52.238,-131.222,52.154,-131.116,52.219]},{"b":[-64.403,45.967,-62.024,47.062],"l":[-62.164,46.487,-62.423,46.478,-62.682,46.459,-62.712,46.45,-62.964,46.428,-63.129,46.422,-63.286,46.46,-63.369,46.508,-63.413,46.512,-63.456,46.504,-63.534,46.541,-63.681,46.562,-63.737,46.481,-63.784,46.455,-63.811,46.469,-63.834,46.494,-63.906,46.509,-63.876,46.539,-63.864,46.572,-63.879,46.609,-63.903,46.639,-64.088,46.775,-63.981,46.913,-63.997,46.982,-63.994,47.062,-64.157,46.955,-64.223,46.901,-64.28,46.836,-64.355,46.769,-64.403,46.692,-64.388,46.641,-64.236,46.631,-64.136,46.6,-64.107,46.562,-64.111,46.425,-64.02,46.405,-63.861,46.408,-63.759,46.398,-63.751,46.384,-63.763,46.37,-63.801,46.367,-63.732,46.289,-63.641,46.23,-63.569,46.209,-63.277,46.153,-63.213,46.16,-63.153,46.188,-63.271,46.2,-63.195,46.237,-63.117,46.253,-63.057,46.295,-62.978,46.316,-62.995,46.292,-63.053,46.27,-63.056,46.224,-63.015,46.19,-62.953,46.195,-62.895,46.124,-63.022,46.067,-62.995,46.058,-62.904,46.068,-62.878,46.001,-62.805,45.973,-62.743,45.967,-62.531,45.977,-62.478,46,-62.503,46.023,-62.543,46.029,-62.539,46.098,-62.552,46.166,-62.526,46.203,-62.32,46.278,-62.172,46.355,-62.024,46.422,-62.041,46.446,-62.074,46.466,-62.164,46.487]},{"b":[-74.032,40.571,-71.903,41.153],"l":[-72.102,41.015,-72.151,41.051,-72.184,41.047,-72.287,41.024,-72.409,40.972,-72.461,40.934,-72.517,40.915,-72.581,40.921,-72.51,40.986,-72.427,41.039,-72.274,41.153,-72.373,41.126,-72.544,41.027,-72.625,40.992,-72.829,40.972,-73.034,40.966,-73.111,40.957,-73.186,40.93,-73.278,40.924,-73.373,40.944,-73.407,40.941,-73.441,40.927,-73.487,40.92,-73.574,40.92,-73.61,40.906,-73.643,40.881,-73.652,40.838,-73.695,40.87,-73.757,40.834,-73.879,40.792,-73.965,40.725,-74.003,40.683,-74.032,40.639,-74.015,40.581,-73.929,40.599,-73.875,40.652,-73.823,40.656,-73.799,40.641,-73.801,40.622,-73.9,40.571,-73.767,40.593,-73.621,40.6,-73.266,40.664,-73.229,40.652,-73.194,40.654,-72.763,40.778,-72.676,40.791,-72.556,40.826,-72.428,40.875,-72.339,40.894,-71.903,41.061,-72.004,41.044,-72.102,41.015]},{"b":[148.337,-6.301,152.406,-4.2],"l":[152.197,-4.285,152.117,-4.212,151.968,-4.317,151.916,-4.297,151.865,-4.261,151.819,-4.217,151.704,-4.2,151.593,-4.201,151.561,-4.247,151.544,-4.299,151.552,-4.346,151.665,-4.637,151.679,-4.761,151.671,-4.883,151.573,-4.937,151.44,-4.931,151.381,-4.941,151.327,-4.96,151.138,-5.113,151.069,-5.204,151.022,-5.321,150.953,-5.424,150.9,-5.447,150.843,-5.454,150.784,-5.471,150.734,-5.51,150.626,-5.521,150.519,-5.46,150.404,-5.473,150.299,-5.536,150.183,-5.524,150.106,-5.429,150.072,-5.31,150.082,-5.186,150.109,-5.136,150.17,-5.071,150.122,-5.018,150.09,-5.012,150.045,-5.035,150.012,-5.14,149.963,-5.448,149.831,-5.524,149.681,-5.524,149.632,-5.516,149.475,-5.573,149.359,-5.584,149.245,-5.573,149.124,-5.523,148.999,-5.485,148.783,-5.512,148.724,-5.493,148.666,-5.487,148.616,-5.507,148.565,-5.508,148.432,-5.472,148.345,-5.545,148.337,-5.669,148.401,-5.765,148.51,-5.805,148.625,-5.831,148.719,-5.867,148.808,-5.916,149.099,-6.117,149.127,-6.128,149.273,-6.079,149.382,-6.078,149.483,-6.125,149.598,-6.261,149.653,-6.29,149.75,-6.301,149.851,-6.293,150.191,-6.289,150.428,-6.276,150.474,-6.263,150.588,-6.188,150.706,-6.149,150.76,-6.114,150.809,-6.071,150.92,-6.027,151.043,-6.015,151.09,-5.997,151.229,-5.92,151.331,-5.839,151.422,-5.747,151.455,-5.703,151.48,-5.655,151.481,-5.591,151.515,-5.552,151.695,-5.544,151.865,-5.565,151.968,-5.529,152.077,-5.458,152.143,-5.357,152.077,-5.247,151.994,-5.149,151.984,-5.074,152.013,-5.004,152.167,-4.993,152.216,-4.979,152.258,-4.955,152.351,-4.822,152.4,-4.731,152.404,-4.629,152.376,-4.56,152.364,-4.491,152.406,-4.341,152.299,-4.321,152.197,-4.285]},{"b":[123.589,-10.347,127.296,-8.316],"l":[124.198,-9.256,123.977,-9.373,123.877,-9.453,123.709,-9.615,123.666,-9.705,123.636,-9.838,123.589,-9.967,123.599,-10.015,123.716,-10.079,123.69,-10.129,123.648,-10.168,123.614,-10.215,123.605,-10.27,123.644,-10.311,123.747,-10.347,123.858,-10.344,123.971,-10.295,124.176,-10.183,124.327,-10.17,124.428,-10.149,124.508,-10.086,124.602,-9.993,124.708,-9.914,124.842,-9.76,124.963,-9.666,124.998,-9.565,125.21,-9.404,125.408,-9.276,125.735,-9.161,125.84,-9.13,125.895,-9.132,125.946,-9.124,126.073,-9.044,126.164,-8.997,126.265,-8.973,126.383,-8.958,126.487,-8.913,126.569,-8.833,126.665,-8.782,126.792,-8.755,126.915,-8.715,127.115,-8.584,127.296,-8.425,127.257,-8.395,127.215,-8.373,127.058,-8.348,126.966,-8.316,126.905,-8.342,126.846,-8.377,126.735,-8.423,126.62,-8.459,126.531,-8.471,126.173,-8.489,125.905,-8.487,125.804,-8.492,125.382,-8.575,125.323,-8.591,125.178,-8.648,125.116,-8.708,125.027,-8.859,124.89,-8.968,124.708,-9.062,124.646,-9.117,124.575,-9.155,124.198,-9.256]},{"b":[144.646,-43.619,148.343,-40.672],"l":[147.786,-43.22,147.699,-43.123,147.648,-43.021,147.687,-42.98,147.774,-43.003,147.8,-42.98,147.807,-42.954,147.8,-42.928,147.693,-42.872,147.574,-42.846,147.536,-42.878,147.549,-42.975,147.537,-42.996,147.452,-43.033,147.408,-42.894,147.298,-42.791,147.302,-42.841,147.348,-42.927,147.343,-42.964,147.325,-43.013,147.281,-43.032,147.26,-43.071,147.26,-43.126,147.245,-43.216,147.173,-43.256,146.997,-43.156,146.985,-43.19,146.988,-43.219,147.077,-43.276,147.036,-43.319,147.005,-43.37,146.955,-43.502,146.874,-43.612,146.834,-43.619,146.699,-43.602,146.549,-43.509,146.413,-43.52,146.187,-43.513,146.043,-43.547,146.013,-43.445,145.982,-43.408,145.994,-43.376,146.109,-43.354,146.226,-43.355,146.208,-43.316,146.176,-43.302,146.125,-43.311,145.975,-43.277,145.873,-43.292,145.803,-43.244,145.682,-43.076,145.61,-42.998,145.567,-42.968,145.518,-42.951,145.488,-42.927,145.268,-42.544,145.237,-42.456,145.199,-42.231,145.373,-42.338,145.435,-42.407,145.468,-42.493,145.527,-42.388,145.517,-42.354,145.36,-42.228,145.34,-42.191,145.331,-42.147,145.294,-42.191,145.235,-42.197,145.259,-42.107,145.238,-42.02,145.055,-41.827,144.916,-41.644,144.778,-41.419,144.766,-41.39,144.764,-41.342,144.698,-41.191,144.662,-41.079,144.646,-40.981,144.71,-40.783,144.719,-40.672,144.819,-40.722,145.043,-40.787,145.159,-40.791,145.224,-40.765,145.283,-40.77,145.349,-40.826,145.429,-40.858,145.485,-40.852,145.533,-40.864,145.576,-40.904,145.686,-40.939,145.734,-40.962,145.775,-40.997,145.821,-41.025,146.111,-41.118,146.317,-41.163,146.574,-41.142,146.651,-41.116,146.723,-41.078,146.786,-41.114,146.848,-41.168,146.836,-41.109,146.857,-41.058,146.919,-41.018,146.99,-40.992,147.106,-40.994,147.219,-40.983,147.269,-40.96,147.321,-40.956,147.388,-40.986,147.455,-41.002,147.501,-40.964,147.579,-40.876,147.622,-40.845,147.818,-40.872,147.873,-40.873,147.969,-40.78,148.033,-40.781,148.215,-40.855,148.293,-40.947,148.285,-41.115,148.292,-41.175,148.306,-41.233,148.312,-41.35,148.29,-41.465,148.287,-41.555,148.297,-41.646,148.288,-41.816,148.316,-41.928,148.302,-42.004,148.301,-42.04,148.328,-42.074,148.341,-42.111,148.331,-42.159,148.343,-42.215,148.331,-42.262,148.29,-42.255,148.277,-42.219,148.285,-42.173,148.277,-42.136,148.256,-42.103,148.183,-42.065,148.204,-42.042,148.242,-42.022,148.214,-41.97,148.167,-42.012,148.141,-42.07,148.156,-42.088,148.128,-42.104,148.067,-42.17,148.023,-42.259,148.005,-42.345,148.009,-42.436,147.974,-42.506,147.924,-42.572,147.912,-42.658,147.915,-42.816,147.958,-42.96,147.981,-43.157,147.945,-43.182,147.839,-43.195,147.786,-43.22]},{"b":[91.524,80.01,97.87,81.28],"l":[97.87,80.763,97.832,80.798,97.703,80.827,97.414,80.842,96.755,80.958,96.693,80.994,96.563,81.03,96.527,81.076,96.471,81.099,96.187,81.184,96.075,81.193,95.984,81.211,95.902,81.261,95.801,81.28,95.16,81.271,95.061,81.188,94.838,81.139,94.612,81.115,94.375,81.107,94.14,81.089,93.889,81.058,93.637,81.038,93.497,81.039,93.359,81.032,93.065,80.988,92.939,80.926,92.765,80.893,92.71,80.872,92.61,80.81,92.593,80.781,92.773,80.769,93.263,80.791,92.981,80.703,92.827,80.619,92.578,80.533,92.247,80.499,91.897,80.478,91.688,80.419,91.524,80.359,91.637,80.27,91.892,80.249,92.092,80.223,92.202,80.179,93.002,80.102,93.655,80.01,93.872,80.01,94.328,80.076,94.565,80.126,94.661,80.123,94.961,80.15,95.856,80.177,97.175,80.241,97.298,80.273,97.417,80.323,97.287,80.343,97.25,80.363,97.115,80.497,97.073,80.52,97.025,80.536,97.113,80.614,97.221,80.652,97.665,80.678,97.747,80.699,97.856,80.698,97.87,80.763]},{"b":[93.071,78.788,100.061,80.168],"l":[99.517,79.13,99.317,79.227,99.042,79.293,99.104,79.305,99.167,79.306,99.388,79.275,99.537,79.277,99.681,79.323,99.722,79.385,99.706,79.463,99.721,79.492,99.749,79.515,99.771,79.568,99.782,79.628,99.805,79.653,99.839,79.669,99.916,79.738,100.061,79.777,99.947,79.849,99.818,79.898,99.727,79.92,99.536,79.941,99.473,79.97,99.371,79.986,99.295,80.016,98.866,80.045,98.596,80.052,98.532,80.044,98.472,80.009,98.499,79.953,98.353,79.884,98.273,79.874,98.065,79.901,97.871,79.853,97.725,79.781,97.652,79.761,97.591,79.775,97.627,79.85,97.76,79.896,97.808,79.956,97.907,80.004,98.018,80.023,97.904,80.095,97.675,80.158,97.587,80.168,97.121,80.153,96.417,80.104,96.277,80.11,96.163,80.097,95.858,80.11,95.498,80.106,95.391,80.073,95.338,80.042,95.281,80.031,94.987,80.097,94.947,80.089,94.815,80.035,94.719,80.011,94.347,79.942,94.257,79.83,94.038,79.756,93.847,79.702,93.405,79.632,93.071,79.495,93.272,79.458,93.479,79.463,93.759,79.451,94.219,79.402,94.314,79.308,94.482,79.219,94.62,79.192,94.632,79.141,94.652,79.127,94.791,79.087,95.02,79.053,95.133,79.05,95.437,79.099,95.531,79.098,95.703,79.012,95.796,79.001,96.347,79.016,96.43,79.003,96.808,78.985,96.871,78.964,96.933,78.934,97.248,78.868,97.555,78.827,97.689,78.827,97.905,78.81,98.054,78.821,98.283,78.795,98.411,78.788,98.82,78.818,99.44,78.834,99.541,78.853,99.929,78.961,99.9,79.006,99.815,79.096,99.751,79.108,99.517,79.13]},{"b":[99.287,77.957,105.343,79.433],"l":[105.146,78.819,105.015,78.843,104.881,78.855,104.633,78.835,104.452,78.88,104.477,78.923,104.449,78.964,104.404,78.977,104.091,79.013,104.004,79.063,103.926,79.123,103.801,79.149,103.673,79.15,103.433,79.126,103.199,79.071,103.076,79.056,102.95,79.056,102.845,79.014,102.748,78.95,102.587,78.871,102.412,78.835,102.448,78.877,102.746,79.106,102.787,79.176,102.885,79.254,102.94,79.271,103.052,79.283,103.098,79.299,103.042,79.332,102.79,79.392,102.405,79.433,102.282,79.43,102.225,79.413,102.181,79.373,102.177,79.313,102.251,79.256,102.129,79.252,102.005,79.264,101.912,79.312,101.824,79.37,101.761,79.372,101.643,79.361,101.591,79.35,101.555,79.313,101.543,79.254,101.31,79.233,101.196,79.204,101.149,79.157,101.052,79.123,101.068,79.096,101.031,79.023,100.965,79.007,100.901,78.98,100.865,78.926,100.856,78.898,100.898,78.812,100.956,78.788,100.876,78.784,100.62,78.797,100.516,78.788,100.416,78.753,100.284,78.679,100.263,78.631,100.257,78.574,100.215,78.536,100.163,78.504,100.124,78.47,100.058,78.38,100.019,78.339,99.678,78.233,99.546,78.179,99.439,78.084,99.287,78.038,99.392,78.001,99.5,77.976,99.845,77.957,100.082,77.975,100.541,78.048,101.04,78.143,101.204,78.192,101.692,78.194,102.18,78.205,102.617,78.225,102.673,78.202,102.734,78.19,102.797,78.188,103.003,78.256,103.719,78.258,104.297,78.335,104.519,78.349,104.742,78.34,104.833,78.353,105.313,78.5,105.343,78.594,105.31,78.666,105.256,78.733,105.205,78.78,105.146,78.819]},{"b":[136.948,74.657,145.36,76.2],"l":[143.686,75.864,143.56,75.86,143.311,75.822,143.185,75.814,142.927,75.827,142.67,75.863,142.46,75.904,142.001,76.044,141.742,76.108,141.485,76.137,141.299,76.064,141.033,75.989,140.985,75.965,140.95,75.927,140.926,75.867,140.927,75.799,140.94,75.75,140.944,75.7,140.889,75.652,140.816,75.631,140.657,75.634,140.602,75.644,140.547,75.663,140.496,75.69,140.389,75.796,140.274,75.822,140.152,75.81,140.049,75.829,139.743,75.953,139.529,76.013,139.211,76.081,139.109,76.108,139.018,76.16,138.92,76.197,138.814,76.2,138.431,76.13,138.208,76.115,138.096,76.081,138.039,76.047,137.977,76.028,137.774,76.016,137.625,75.988,137.561,75.955,137.501,75.91,137.594,75.823,137.707,75.76,137.358,75.782,137.269,75.749,137.215,75.554,137.29,75.349,137.166,75.347,136.982,75.365,136.948,75.326,136.962,75.27,137.006,75.235,137.218,75.124,137.447,75.054,137.568,75.041,137.683,75.009,137.915,74.871,138.001,74.827,138.092,74.797,138.866,74.701,138.982,74.674,139.099,74.657,139.215,74.66,139.326,74.687,139.43,74.749,139.512,74.838,139.548,74.904,139.606,74.946,139.681,74.964,139.758,74.964,140.011,74.895,140.268,74.847,140.464,74.856,140.661,74.882,141.31,74.923,141.53,74.947,141.748,74.983,141.987,74.991,142.1,74.951,142.184,74.9,142.287,74.85,142.378,74.829,142.473,74.82,142.626,74.837,142.778,74.868,143.128,74.97,142.93,75.062,142.697,75.103,142.617,75.133,142.265,75.346,142.199,75.393,142.151,75.458,142.086,75.661,142.308,75.692,142.552,75.721,142.942,75.713,143.002,75.66,142.986,75.633,142.868,75.572,142.734,75.545,142.7,75.449,142.729,75.338,142.82,75.268,142.922,75.217,143.17,75.117,143.396,75.083,143.626,75.084,144.02,75.045,144.216,75.059,144.408,75.102,144.883,75.269,144.814,75.325,144.727,75.366,144.803,75.416,145.023,75.49,145.36,75.53,145.31,75.564,145.255,75.586,143.686,75.864]},{"b":[139.686,73.221,143.491,73.915],"l":[143.344,73.569,142.639,73.803,142.435,73.852,142.185,73.896,141.932,73.915,141.682,73.904,141.312,73.872,141.19,73.876,141.085,73.866,140.984,73.832,140.884,73.778,140.697,73.629,140.594,73.565,140.381,73.483,140.155,73.458,139.92,73.449,139.686,73.426,139.786,73.355,139.925,73.355,140.027,73.361,140.392,73.435,140.663,73.452,140.754,73.446,141.183,73.389,141.597,73.311,142.126,73.282,142.342,73.253,142.587,73.253,142.842,73.245,143.193,73.221,143.451,73.231,143.491,73.246,143.464,73.459,143.411,73.521,143.344,73.569]},{"b":[146.149,74.772,150.822,75.582],"l":[150.69,75.155,150.613,75.12,150.531,75.1,150.417,75.134,150.281,75.164,150.104,75.219,149.645,75.245,149.083,75.262,148.892,75.228,148.59,75.236,148.475,75.272,148.489,75.309,148.519,75.336,148.509,75.387,148.432,75.414,147.497,75.441,147.444,75.438,147.06,75.364,146.795,75.371,146.748,75.429,146.751,75.51,146.538,75.582,146.438,75.558,146.343,75.481,146.258,75.394,146.186,75.296,146.149,75.198,146.703,75.114,146.925,75.063,147.144,74.998,147.257,74.984,147.627,74.959,147.741,74.932,147.972,74.857,148.092,74.826,148.297,74.8,149.05,74.772,149.597,74.773,149.838,74.795,150.331,74.867,150.58,74.919,150.646,74.945,150.822,75.157,150.757,75.162,150.69,75.155]},{"b":[172.706,-41.611,178.536,-34.429],"l":[175.781,-36.805,175.842,-36.875,175.876,-36.958,175.921,-37.205,175.99,-37.437,176.115,-37.538,176.129,-37.587,176.053,-37.562,176.03,-37.576,176.038,-37.601,176.108,-37.645,176.191,-37.667,176.243,-37.664,176.292,-37.68,176.615,-37.831,176.77,-37.89,177.162,-37.986,177.274,-37.993,177.336,-37.991,177.453,-37.957,177.558,-37.897,177.649,-37.808,177.727,-37.706,177.813,-37.656,177.909,-37.617,177.958,-37.581,178.009,-37.555,178.272,-37.567,178.361,-37.618,178.476,-37.66,178.536,-37.692,178.516,-37.758,178.447,-37.854,178.394,-37.96,178.347,-38.201,178.315,-38.444,178.268,-38.551,178.181,-38.634,178.085,-38.694,177.976,-38.722,177.932,-38.86,177.91,-39.022,177.917,-39.062,177.951,-39.095,177.966,-39.142,177.909,-39.24,177.875,-39.225,177.829,-39.145,177.786,-39.111,177.656,-39.086,177.523,-39.074,177.408,-39.081,177.297,-39.116,177.129,-39.186,177.077,-39.222,177.031,-39.267,176.954,-39.368,176.936,-39.491,176.939,-39.555,176.967,-39.605,177.11,-39.673,176.968,-39.911,176.842,-40.158,176.771,-40.228,176.689,-40.293,176.612,-40.442,176.476,-40.57,176.385,-40.668,176.314,-40.769,176.252,-40.877,176.119,-41.029,176.06,-41.13,175.983,-41.213,175.84,-41.32,175.687,-41.412,175.447,-41.538,175.38,-41.58,175.31,-41.611,175.222,-41.574,175.204,-41.535,175.185,-41.449,175.166,-41.417,175.054,-41.391,174.906,-41.433,174.881,-41.424,174.875,-41.404,174.875,-41.278,174.9,-41.243,174.866,-41.223,174.832,-41.231,174.82,-41.263,174.841,-41.291,174.757,-41.325,174.67,-41.326,174.643,-41.313,174.635,-41.289,174.657,-41.251,174.685,-41.218,174.848,-41.059,175.017,-40.848,175.163,-40.622,175.2,-40.505,175.254,-40.289,175.21,-40.199,175.156,-40.115,175.009,-39.952,174.814,-39.86,174.687,-39.847,174.567,-39.813,174.455,-39.735,174.352,-39.643,174.149,-39.568,173.934,-39.509,173.812,-39.426,173.783,-39.376,173.764,-39.319,173.766,-39.265,173.782,-39.211,173.806,-39.17,173.844,-39.139,174.071,-39.031,174.312,-38.971,174.356,-38.972,174.398,-38.963,174.458,-38.926,174.566,-38.842,174.597,-38.785,174.619,-38.605,174.653,-38.428,174.715,-38.226,174.809,-38.1,174.84,-38.023,174.802,-37.896,174.837,-37.849,174.88,-37.821,174.928,-37.804,174.846,-37.685,174.749,-37.505,174.729,-37.449,174.744,-37.393,174.768,-37.339,174.707,-37.325,174.673,-37.273,174.586,-37.098,174.61,-37.07,174.66,-37.089,174.734,-37.215,174.746,-37.15,174.804,-37.11,174.864,-37.089,174.929,-37.085,174.782,-36.944,174.733,-36.949,174.668,-36.972,174.601,-36.986,174.537,-36.973,174.476,-36.942,174.445,-36.883,174.406,-36.768,174.382,-36.726,174.189,-36.492,174.246,-36.485,174.402,-36.602,174.432,-36.565,174.454,-36.511,174.447,-36.451,174.41,-36.406,174.354,-36.376,174.353,-36.323,174.395,-36.274,174.393,-36.24,174.304,-36.171,174.268,-36.163,174.252,-36.196,174.278,-36.244,174.254,-36.249,174.036,-36.122,173.969,-36.021,173.914,-35.909,173.909,-35.954,173.917,-36.018,174.003,-36.146,174.142,-36.289,174.166,-36.328,174.146,-36.377,174.097,-36.391,174.055,-36.36,173.991,-36.237,173.945,-36.176,173.412,-35.543,173.48,-35.459,173.586,-35.389,173.61,-35.357,173.626,-35.319,173.582,-35.313,173.542,-35.33,173.496,-35.362,173.454,-35.399,173.402,-35.481,173.376,-35.5,173.314,-35.443,173.29,-35.408,173.291,-35.366,173.275,-35.34,173.228,-35.331,173.16,-35.248,173.117,-35.205,173.189,-35.124,173.191,-35.016,173.117,-34.903,173.03,-34.8,172.861,-34.632,172.706,-34.455,172.874,-34.433,173.044,-34.429,172.964,-34.535,173,-34.596,173.054,-34.648,173.171,-34.807,173.181,-34.853,173.241,-34.899,173.269,-34.935,173.285,-34.981,173.34,-34.948,173.381,-34.896,173.448,-34.844,173.439,-34.929,173.473,-34.947,173.694,-35.006,173.739,-35.055,173.786,-35.069,173.813,-35.041,173.844,-35.026,173.924,-35.057,174.104,-35.143,174.119,-35.172,174.11,-35.216,174.119,-35.263,174.143,-35.3,174.203,-35.309,174.283,-35.254,174.32,-35.247,174.373,-35.325,174.393,-35.369,174.385,-35.367,174.419,-35.411,174.465,-35.454,174.543,-35.582,174.532,-35.627,174.509,-35.667,174.581,-35.786,174.533,-35.794,174.391,-35.774,174.396,-35.797,174.479,-35.884,174.549,-36.007,174.605,-36.081,174.802,-36.309,174.772,-36.391,174.777,-36.445,174.752,-36.491,174.819,-36.612,174.777,-36.65,174.749,-36.774,174.719,-36.796,174.722,-36.841,174.802,-36.853,174.85,-36.873,174.891,-36.909,174.917,-36.865,174.952,-36.853,175.047,-36.912,175.245,-36.971,175.3,-36.993,175.326,-37.041,175.347,-37.156,175.385,-37.207,175.461,-37.217,175.542,-37.201,175.568,-37.159,175.552,-37.046,175.493,-36.866,175.493,-36.807,175.501,-36.748,175.487,-36.69,175.458,-36.634,175.426,-36.592,175.386,-36.556,175.4,-36.501,175.461,-36.476,175.498,-36.523,175.528,-36.579,175.681,-36.747,175.772,-36.735,175.781,-36.805]},{"b":[166.478,-46.631,174.37,-40.49],"l":[172.889,-43.124,173.072,-43.06,173.149,-43.023,173.221,-42.977,173.348,-42.841,173.545,-42.518,173.589,-42.474,173.84,-42.271,173.889,-42.212,173.888,-42.13,173.974,-42.081,174.047,-42.003,174.215,-41.85,174.243,-41.813,174.283,-41.741,174.217,-41.678,174.17,-41.657,174.084,-41.671,174.161,-41.562,174.092,-41.505,174.073,-41.472,174.069,-41.429,174.102,-41.366,174.17,-41.327,174.237,-41.312,174.297,-41.264,174.368,-41.188,174.37,-41.104,174.284,-41.172,174.138,-41.248,174.039,-41.242,174.103,-41.217,174.2,-41.16,174.214,-41.126,174.274,-41.069,174.303,-41.02,174.224,-41.024,174.212,-40.985,174.153,-40.991,174.121,-41.005,174.081,-41.006,173.999,-40.993,173.998,-41.028,174.024,-41.072,173.958,-41.1,173.915,-41.158,173.933,-41.187,173.897,-41.239,173.798,-41.272,173.862,-41.192,173.86,-41.124,173.915,-41.07,173.88,-41.031,173.89,-41.007,173.953,-40.985,174.002,-40.918,173.947,-40.924,173.898,-40.951,173.784,-40.972,173.738,-40.989,173.563,-41.102,173.447,-41.151,173.338,-41.211,173.231,-41.284,173.115,-41.279,173.069,-41.186,173.052,-41.079,173.042,-40.954,172.989,-40.848,172.869,-40.82,172.767,-40.773,172.729,-40.724,172.704,-40.668,172.711,-40.605,172.733,-40.544,172.944,-40.519,172.83,-40.49,172.711,-40.497,172.641,-40.518,172.468,-40.622,172.273,-40.759,172.139,-40.947,172.093,-41.202,172.011,-41.445,171.948,-41.539,171.831,-41.655,171.732,-41.72,171.672,-41.745,171.536,-41.758,171.486,-41.795,171.421,-41.973,171.36,-42.08,171.323,-42.189,171.296,-42.303,171.252,-42.402,171.296,-42.431,171.313,-42.46,171.257,-42.465,171.221,-42.479,171.19,-42.5,171.028,-42.696,171.048,-42.802,171.038,-42.862,171.012,-42.885,171.018,-42.819,171.011,-42.764,170.97,-42.718,170.84,-42.849,170.742,-42.927,170.725,-42.975,170.735,-43.03,170.665,-42.961,170.616,-42.972,170.524,-43.009,170.536,-43.041,170.612,-43.092,170.536,-43.058,170.459,-43.038,170.379,-43.066,170.303,-43.108,170.374,-43.135,170.396,-43.182,170.356,-43.154,170.3,-43.145,170.24,-43.164,170.19,-43.222,170.149,-43.248,170.104,-43.265,170.018,-43.349,169.859,-43.426,169.908,-43.447,169.891,-43.462,169.835,-43.459,169.824,-43.497,169.834,-43.537,169.769,-43.538,169.662,-43.591,169.515,-43.624,169.323,-43.702,169.17,-43.777,169.136,-43.82,169.179,-43.913,169.136,-43.9,169.067,-43.863,168.99,-43.89,168.806,-43.992,168.775,-43.996,168.651,-43.972,168.457,-44.031,168.367,-44.082,168.196,-44.224,168.018,-44.359,167.857,-44.501,167.866,-44.592,167.902,-44.625,167.909,-44.665,167.859,-44.625,167.787,-44.595,167.698,-44.641,167.578,-44.741,167.485,-44.771,167.46,-44.802,167.456,-44.838,167.482,-44.874,167.479,-44.915,167.466,-44.958,167.411,-44.828,167.195,-44.963,167.172,-44.997,167.205,-45.048,167.259,-45.082,167.188,-45.094,167.128,-45.051,167.026,-45.124,167.023,-45.177,167.033,-45.222,167.127,-45.266,167.207,-45.28,167.23,-45.29,167.145,-45.302,167.118,-45.318,167.112,-45.354,167.156,-45.411,167.052,-45.383,166.909,-45.307,166.869,-45.311,166.876,-45.368,166.92,-45.408,166.778,-45.41,166.743,-45.468,166.734,-45.544,166.869,-45.55,166.991,-45.532,166.826,-45.603,166.798,-45.646,166.81,-45.699,167.003,-45.712,166.953,-45.75,166.836,-45.775,166.513,-45.812,166.488,-45.832,166.478,-45.903,166.493,-45.964,166.613,-45.955,166.718,-45.889,166.734,-45.928,166.727,-45.963,166.65,-46.042,166.73,-46.053,166.856,-45.981,166.917,-45.957,166.712,-46.134,166.732,-46.198,166.831,-46.225,167.1,-46.249,167.369,-46.242,167.414,-46.229,167.491,-46.155,167.539,-46.149,167.682,-46.193,167.722,-46.227,167.842,-46.366,167.9,-46.368,168.077,-46.353,168.189,-46.362,168.23,-46.386,168.266,-46.419,168.32,-46.447,168.343,-46.489,168.326,-46.546,168.357,-46.588,168.382,-46.605,168.466,-46.588,168.572,-46.611,168.631,-46.588,168.767,-46.566,168.838,-46.578,168.966,-46.613,169.099,-46.631,169.342,-46.621,169.687,-46.552,169.729,-46.521,169.761,-46.48,169.918,-46.334,170.186,-46.161,170.267,-46.083,170.335,-45.992,170.419,-45.941,170.674,-45.896,170.722,-45.878,170.776,-45.871,170.791,-45.844,170.788,-45.792,170.74,-45.756,170.7,-45.714,170.701,-45.684,170.815,-45.519,170.89,-45.374,170.94,-45.216,170.991,-45.151,171.113,-45.039,171.134,-44.978,171.023,-44.937,170.999,-44.911,171.146,-44.912,171.198,-44.768,171.213,-44.612,171.231,-44.521,171.313,-44.302,171.285,-44.279,171.241,-44.264,171.365,-44.255,171.417,-44.209,171.443,-44.136,171.518,-44.118,171.659,-44.117,171.712,-44.097,171.808,-44.042,171.891,-44.007,171.978,-43.984,172.081,-43.946,172.18,-43.896,172.137,-43.834,172.052,-43.74,172.036,-43.702,172.146,-43.764,172.221,-43.825,172.297,-43.868,172.35,-43.859,172.385,-43.83,172.396,-43.778,172.43,-43.746,172.48,-43.727,172.527,-43.739,172.584,-43.774,172.476,-43.833,172.503,-43.844,172.555,-43.831,172.749,-43.813,172.818,-43.87,172.921,-43.891,173.023,-43.885,173.066,-43.875,173.094,-43.844,173.117,-43.798,173.098,-43.704,173.073,-43.676,172.947,-43.659,172.807,-43.621,172.767,-43.562,172.749,-43.517,172.74,-43.468,172.693,-43.444,172.527,-43.465,172.562,-43.436,172.632,-43.428,172.7,-43.4,172.735,-43.355,172.688,-43.315,172.627,-43.3,172.624,-43.272,172.719,-43.259,172.808,-43.198,172.889,-43.124]},{"b":[121.904,5.599,126.593,9.759],"l":[126.593,7.547,126.57,7.677,126.544,7.725,126.494,7.757,126.435,7.833,126.425,7.927,126.457,8.149,126.459,8.203,126.38,8.327,126.365,8.484,126.282,8.539,126.173,8.56,126.14,8.596,126.142,8.627,126.22,8.696,126.263,8.744,126.32,8.845,126.305,8.952,126.209,9.081,126.192,9.125,126.193,9.277,126.088,9.261,126.006,9.321,125.955,9.427,125.877,9.513,125.642,9.654,125.521,9.759,125.471,9.757,125.414,9.669,125.51,9.276,125.533,9.141,125.499,9.015,125.376,8.992,125.248,9.027,125.21,9.027,125.176,8.922,125.141,8.869,125.046,8.891,124.944,8.957,124.869,8.972,124.806,8.924,124.787,8.874,124.762,8.69,124.731,8.563,124.622,8.523,124.451,8.606,124.405,8.6,124.358,8.559,124.325,8.508,124.283,8.386,124.226,8.271,124.198,8.23,124.159,8.201,123.997,8.159,123.931,8.128,123.799,8.049,123.753,8.058,123.853,8.145,123.877,8.189,123.861,8.376,123.849,8.433,123.783,8.548,123.68,8.621,123.564,8.647,123.499,8.682,123.435,8.703,123.38,8.616,123.341,8.57,123.293,8.541,123.147,8.516,123.096,8.481,123.051,8.434,123.018,8.398,122.999,8.356,123.003,8.287,122.996,8.221,122.911,8.156,122.804,8.134,122.673,8.133,122.589,8.093,122.387,8.046,122.337,8.028,122.243,7.945,122.132,7.81,122.12,7.765,122.115,7.66,122.047,7.364,121.991,7.279,121.925,7.2,121.904,7.075,121.964,6.968,122.028,6.929,122.098,6.914,122.142,6.95,122.176,7.004,122.251,7.17,122.32,7.34,122.449,7.561,122.474,7.639,122.498,7.673,122.616,7.763,122.714,7.774,122.792,7.722,122.819,7.558,122.843,7.529,122.917,7.531,122.99,7.546,123.049,7.614,123.097,7.7,123.121,7.667,123.139,7.63,123.151,7.575,123.178,7.529,123.282,7.464,123.391,7.408,123.476,7.665,123.482,7.71,123.477,7.756,123.493,7.808,123.553,7.832,123.609,7.832,123.666,7.818,123.717,7.785,123.765,7.743,123.968,7.665,124.068,7.578,124.182,7.437,124.207,7.396,124.213,7.332,124.191,7.267,124.158,7.219,124.118,7.175,124.045,7.114,123.985,6.994,123.981,6.93,123.988,6.863,124.048,6.667,124.05,6.533,124.078,6.404,124.213,6.233,124.399,6.12,124.636,5.998,124.927,5.875,124.975,5.866,125.035,5.871,125.076,5.906,125.174,6.047,125.191,6.063,125.232,6.07,125.268,6.033,125.265,5.926,125.233,5.808,125.241,5.757,125.288,5.632,125.346,5.599,125.456,5.664,125.608,5.87,125.668,5.979,125.671,6.225,125.588,6.466,125.565,6.5,125.487,6.574,125.433,6.607,125.381,6.69,125.401,6.796,125.465,6.911,125.542,7.017,125.641,7.105,125.66,7.161,125.67,7.222,125.689,7.263,125.774,7.322,125.824,7.333,125.901,7.117,125.962,7.033,125.985,6.944,126.043,6.843,126.08,6.733,126.11,6.49,126.142,6.398,126.189,6.31,126.221,6.483,126.24,6.734,126.192,6.853,126.217,6.891,126.294,6.882,126.439,7.012,126.547,7.176,126.582,7.248,126.589,7.325,126.593,7.547]},{"b":[119.762,12.567,124.143,18.615],"l":[122.294,18.234,122.223,18.157,122.179,18.064,122.151,17.756,122.152,17.664,122.175,17.576,122.237,17.435,122.269,17.395,122.362,17.345,122.388,17.307,122.393,17.238,122.408,17.178,122.468,17.155,122.519,17.125,122.5,17.058,122.468,16.99,122.426,16.823,122.226,16.435,122.214,16.352,122.135,16.185,121.975,16.158,121.789,16.077,121.685,16.015,121.595,15.933,121.561,15.827,121.59,15.778,121.609,15.726,121.607,15.67,121.579,15.623,121.49,15.51,121.452,15.417,121.412,15.375,121.392,15.324,121.399,15.267,121.435,15.216,121.544,14.999,121.661,14.79,121.686,14.765,121.695,14.737,121.627,14.682,121.628,14.581,121.649,14.481,121.752,14.234,121.767,14.168,121.8,14.114,121.853,14.063,121.912,14.02,122.08,13.947,122.144,13.933,122.212,13.93,122.228,13.979,122.288,13.996,122.274,14.045,122.203,14.112,122.2,14.148,122.238,14.175,122.283,14.191,122.384,14.264,122.491,14.322,122.627,14.318,122.761,14.285,122.856,14.251,122.934,14.188,123.015,14.08,123.071,13.96,123.071,13.903,123.057,13.845,123.06,13.789,123.102,13.75,123.231,13.747,123.297,13.836,123.305,13.937,123.259,13.975,123.28,14.025,123.32,14.062,123.377,14.029,123.432,13.966,123.633,13.898,123.684,13.897,123.726,13.884,123.816,13.837,123.858,13.8,123.806,13.722,123.607,13.704,123.55,13.646,123.608,13.528,123.704,13.432,123.765,13.354,123.819,13.269,123.817,13.192,123.785,13.111,123.873,13.117,123.955,13.1,124.069,13.032,124.105,13.025,124.143,13.036,124.137,12.791,124.06,12.567,123.962,12.595,123.878,12.69,123.895,12.805,123.949,12.916,123.918,12.94,123.864,12.931,123.802,12.906,123.736,12.897,123.627,12.912,123.402,13.033,123.311,13.044,123.29,13.099,123.296,13.216,123.206,13.354,123.192,13.403,123.163,13.442,122.896,13.592,122.863,13.617,122.781,13.737,122.595,13.908,122.543,13.925,122.486,13.93,122.468,13.887,122.494,13.82,122.504,13.763,122.5,13.703,122.508,13.657,122.596,13.562,122.609,13.517,122.668,13.395,122.675,13.253,122.6,13.194,122.515,13.26,122.513,13.314,122.498,13.364,122.407,13.493,122.377,13.521,122.205,13.648,122.073,13.788,121.778,13.938,121.743,13.946,121.692,13.935,121.643,13.916,121.501,13.842,121.451,13.791,121.446,13.712,121.344,13.649,121.204,13.64,121.096,13.679,121.006,13.758,120.932,13.762,120.841,13.885,120.729,13.901,120.637,13.804,120.617,13.995,120.617,14.188,120.643,14.244,120.688,14.291,120.922,14.493,120.952,14.558,120.941,14.645,120.888,14.716,120.804,14.759,120.708,14.777,120.638,14.816,120.584,14.881,120.547,14.766,120.583,14.595,120.589,14.483,120.556,14.441,120.496,14.44,120.439,14.453,120.396,14.493,120.365,14.608,120.284,14.684,120.251,14.793,120.214,14.809,120.138,14.8,120.082,14.851,120.045,14.978,120.037,15.115,120.005,15.229,119.959,15.34,119.933,15.431,119.892,15.838,119.881,15.875,119.86,15.906,119.808,15.952,119.769,16.008,119.762,16.055,119.773,16.255,119.79,16.303,119.831,16.327,119.886,16.287,119.93,16.239,119.985,16.215,120.033,16.185,120.124,16.066,120.16,16.048,120.271,16.051,120.337,16.066,120.369,16.11,120.389,16.161,120.389,16.222,120.325,16.4,120.305,16.529,120.304,16.645,120.321,16.762,120.409,16.956,120.42,17.09,120.412,17.27,120.427,17.377,120.425,17.438,120.372,17.535,120.358,17.638,120.505,18.163,120.551,18.264,120.584,18.369,120.6,18.508,120.709,18.546,120.814,18.603,120.868,18.599,120.925,18.585,121.051,18.614,121.102,18.615,121.254,18.563,121.593,18.376,121.717,18.33,121.846,18.295,121.948,18.285,122.038,18.328,122.077,18.372,122.147,18.487,122.221,18.501,122.266,18.459,122.3,18.403,122.315,18.32,122.294,18.234]},{"b":[130.996,-10.655,150.85,-0.347],"l":[140.662,-8.847,140.581,-8.728,140.49,-8.62,140.102,-8.301,140.003,-8.196,139.983,-8.167,139.993,-8.139,140.037,-8.084,140.117,-7.924,140.034,-8.023,139.935,-8.101,139.791,-8.106,139.649,-8.125,139.519,-8.173,139.386,-8.189,139.319,-8.166,139.279,-8.107,139.258,-8.047,139.249,-7.982,139.193,-8.086,139.083,-8.143,138.933,-8.262,138.891,-8.238,138.865,-8.192,138.856,-8.145,138.885,-8.095,138.905,-8.041,138.936,-7.913,139.003,-7.838,139.046,-7.691,139.074,-7.639,139.088,-7.587,139.049,-7.528,138.983,-7.508,138.938,-7.472,138.886,-7.373,138.853,-7.34,138.794,-7.299,138.748,-7.251,138.798,-7.216,138.865,-7.201,138.919,-7.204,139.018,-7.226,139.063,-7.227,139.177,-7.19,139.113,-7.202,139.049,-7.201,138.846,-7.136,138.72,-7.07,138.601,-6.937,138.6,-6.911,138.684,-6.887,138.865,-6.858,138.808,-6.79,138.727,-6.731,138.698,-6.626,138.642,-6.56,138.522,-6.454,138.439,-6.343,138.368,-6.119,138.296,-5.949,138.314,-5.887,138.375,-5.844,138.283,-5.839,138.2,-5.807,138.244,-5.724,138.34,-5.676,138.252,-5.688,138.167,-5.712,138.127,-5.717,138.087,-5.709,138.066,-5.676,138.063,-5.629,138.076,-5.546,138.061,-5.465,137.985,-5.428,137.922,-5.37,137.887,-5.349,137.84,-5.35,137.795,-5.312,137.759,-5.256,137.307,-5.014,137.28,-4.945,137.238,-4.976,137.196,-4.99,137.144,-4.951,137.089,-4.924,137.03,-4.929,136.975,-4.907,136.917,-4.895,136.857,-4.893,136.619,-4.819,136.394,-4.701,136.211,-4.651,136.097,-4.585,135.98,-4.531,135.717,-4.478,135.45,-4.443,135.354,-4.442,135.273,-4.453,135.196,-4.451,134.754,-4.195,134.68,-4.079,134.687,-4.011,134.707,-3.955,134.887,-3.938,134.76,-3.922,134.708,-3.93,134.603,-3.976,134.547,-3.979,134.467,-3.949,134.391,-3.91,134.266,-3.946,134.202,-3.887,134.18,-3.825,134.147,-3.797,134.1,-3.8,134.037,-3.822,133.974,-3.818,133.933,-3.776,133.904,-3.72,133.861,-3.68,133.808,-3.65,133.723,-3.578,133.678,-3.479,133.683,-3.309,133.697,-3.248,133.782,-3.149,133.842,-3.055,133.767,-3.044,133.7,-3.087,133.672,-3.132,133.661,-3.186,133.653,-3.364,133.599,-3.416,133.518,-3.412,133.542,-3.516,133.509,-3.616,133.415,-3.732,133.407,-3.785,133.422,-3.843,133.401,-3.899,133.249,-4.062,133.198,-4.07,133.085,-4.069,132.969,-4.095,132.914,-4.057,132.87,-4.007,132.837,-3.949,132.791,-3.828,132.754,-3.704,132.87,-3.551,132.83,-3.413,132.751,-3.295,132.554,-3.131,132.348,-2.975,132.255,-2.943,132.102,-2.93,132.054,-2.915,132.006,-2.856,131.971,-2.789,132.067,-2.76,132.231,-2.68,132.323,-2.684,132.575,-2.727,132.653,-2.766,132.725,-2.789,132.897,-2.658,133.034,-2.487,133.119,-2.45,133.191,-2.438,133.265,-2.454,133.411,-2.514,133.527,-2.542,133.609,-2.547,133.652,-2.601,133.7,-2.625,133.711,-2.544,133.753,-2.451,133.835,-2.422,133.878,-2.415,133.905,-2.391,133.899,-2.304,133.791,-2.294,133.85,-2.22,133.902,-2.184,133.921,-2.147,133.922,-2.102,133.71,-2.189,133.488,-2.226,133.356,-2.216,133.225,-2.214,132.963,-2.273,132.863,-2.27,132.631,-2.247,132.503,-2.218,132.403,-2.24,132.308,-2.242,132.207,-2.176,132.122,-2.092,132.08,-2.033,132.023,-1.99,131.998,-1.933,131.936,-1.715,131.93,-1.56,131.83,-1.557,131.731,-1.541,131.294,-1.393,131.241,-1.43,131.179,-1.448,131.118,-1.455,131.057,-1.448,130.996,-1.425,131.001,-1.384,131.046,-1.284,131.091,-1.247,131.152,-1.219,131.191,-1.166,131.254,-1.007,131.259,-0.953,131.252,-0.897,131.257,-0.855,131.296,-0.834,131.462,-0.782,131.804,-0.704,131.891,-0.657,131.962,-0.582,132.046,-0.537,132.084,-0.491,132.128,-0.454,132.394,-0.355,132.508,-0.347,132.625,-0.359,132.856,-0.417,133.077,-0.512,133.268,-0.636,133.473,-0.726,133.724,-0.741,133.85,-0.731,133.975,-0.744,134.025,-0.77,134.112,-0.847,134.087,-0.897,134.072,-1.002,134.116,-1.102,134.188,-1.203,134.247,-1.311,134.26,-1.363,134.237,-1.474,134.217,-1.529,134.145,-1.621,134.106,-1.721,134.131,-1.845,134.145,-1.969,134.143,-2.083,134.156,-2.195,134.195,-2.309,134.362,-2.621,134.46,-2.832,134.491,-2.714,134.483,-2.583,134.518,-2.536,134.567,-2.51,134.627,-2.537,134.645,-2.59,134.649,-2.706,134.702,-2.934,134.77,-2.944,134.843,-2.909,134.855,-2.979,134.853,-3.108,134.887,-3.21,134.917,-3.25,135.037,-3.333,135.092,-3.349,135.252,-3.369,135.372,-3.375,135.487,-3.345,135.561,-3.269,135.628,-3.186,135.859,-2.995,135.926,-2.904,135.991,-2.764,136.013,-2.734,136.243,-2.583,136.27,-2.529,136.303,-2.426,136.352,-2.325,136.39,-2.273,136.612,-2.224,136.843,-2.198,137.072,-2.105,137.171,-2.025,137.176,-1.973,137.125,-1.881,137.123,-1.841,137.176,-1.802,137.381,-1.686,137.617,-1.566,137.806,-1.483,137.911,-1.484,138.008,-1.557,138.111,-1.616,138.65,-1.791,138.736,-1.846,138.811,-1.918,138.919,-1.968,139.039,-1.992,139.149,-2.039,139.253,-2.099,139.482,-2.212,139.79,-2.348,139.868,-2.356,140.155,-2.35,140.204,-2.376,140.251,-2.412,140.295,-2.42,140.623,-2.446,140.673,-2.472,140.721,-2.508,140.747,-2.607,141.105,-2.611,141.186,-2.628,141.687,-2.845,141.837,-2.932,141.888,-2.953,141.938,-2.953,141.986,-2.964,142.212,-3.083,142.549,-3.205,142.905,-3.321,143.016,-3.345,143.13,-3.355,143.378,-3.395,143.509,-3.431,143.701,-3.573,143.797,-3.617,143.888,-3.697,144.016,-3.784,144.066,-3.805,144.122,-3.815,144.248,-3.818,144.374,-3.803,144.427,-3.81,144.478,-3.825,144.525,-3.855,144.548,-3.913,144.627,-3.993,144.738,-4.029,144.843,-4.101,144.938,-4.188,145.008,-4.275,145.088,-4.349,145.208,-4.38,145.335,-4.385,145.767,-4.823,145.788,-4.891,145.793,-5.178,145.745,-5.402,145.853,-5.471,145.999,-5.497,146.205,-5.545,146.403,-5.617,147.034,-5.919,147.121,-5.945,147.248,-5.955,147.377,-5.951,147.423,-5.966,147.519,-6.021,147.567,-6.057,147.653,-6.155,147.73,-6.261,147.763,-6.292,147.802,-6.315,147.825,-6.373,147.854,-6.551,147.846,-6.662,147.81,-6.704,147.71,-6.724,147.356,-6.742,147.119,-6.722,146.954,-6.834,146.949,-6.883,146.961,-6.929,147.105,-7.167,147.19,-7.378,147.26,-7.464,147.365,-7.534,147.459,-7.616,147.545,-7.711,147.724,-7.876,147.822,-7.937,147.936,-7.975,148.127,-8.104,148.152,-8.16,148.206,-8.339,148.23,-8.46,148.234,-8.51,148.247,-8.554,148.414,-8.664,148.451,-8.695,148.526,-8.939,148.583,-9.052,148.679,-9.092,148.792,-9.089,149.097,-9.017,149.142,-9.015,149.198,-9.031,149.248,-9.071,149.264,-9.181,149.216,-9.296,149.203,-9.407,149.263,-9.498,149.419,-9.569,149.476,-9.588,149.756,-9.611,149.866,-9.63,149.974,-9.661,150.011,-9.688,149.985,-9.737,149.928,-9.761,149.864,-9.771,149.761,-9.806,149.763,-9.869,149.821,-9.934,149.874,-10.013,149.919,-10.042,149.968,-10.061,150.089,-10.088,150.206,-10.126,150.284,-10.163,150.364,-10.19,150.539,-10.207,150.667,-10.257,150.85,-10.236,150.691,-10.318,150.637,-10.338,150.446,-10.307,150.41,-10.339,150.489,-10.426,150.605,-10.484,150.647,-10.518,150.618,-10.558,150.482,-10.637,150.426,-10.649,150.32,-10.655,150.142,-10.621,150.017,-10.577,149.982,-10.518,149.948,-10.483,149.835,-10.399,149.754,-10.353,149.651,-10.337,149.544,-10.338,149.353,-10.29,148.937,-10.255,148.838,-10.234,148.713,-10.167,148.654,-10.157,148.591,-10.178,148.431,-10.191,148.383,-10.185,148.269,-10.128,148.15,-10.107,148.101,-10.125,148.051,-10.128,147.89,-10.087,147.769,-10.07,147.669,-10.013,147.614,-9.96,147.553,-9.912,147.496,-9.79,147.408,-9.675,147.299,-9.58,147.064,-9.426,147.017,-9.388,146.925,-9.247,146.93,-9.154,146.964,-9.06,146.913,-9.092,146.856,-9.088,146.697,-9.025,146.631,-8.951,146.524,-8.75,146.456,-8.644,146.296,-8.456,146.251,-8.344,146.184,-8.246,146.143,-8.21,146.109,-8.168,146.079,-8.114,146.033,-8.076,145.811,-7.993,145.772,-7.966,145.729,-7.952,145.563,-7.944,145.468,-7.93,145.288,-7.862,145.194,-7.841,145.082,-7.828,144.974,-7.802,144.921,-7.777,144.885,-7.734,144.864,-7.632,144.773,-7.642,144.684,-7.625,144.598,-7.589,144.51,-7.567,144.45,-7.598,144.431,-7.679,144.403,-7.684,144.352,-7.667,144.326,-7.677,144.27,-7.714,144.225,-7.765,144.143,-7.757,143.974,-7.706,143.898,-7.674,143.834,-7.616,143.779,-7.55,143.723,-7.498,143.655,-7.46,143.742,-7.55,143.942,-7.944,143.892,-7.952,143.841,-7.942,143.888,-8.018,143.833,-8.029,143.779,-8.028,143.665,-7.996,143.552,-7.985,143.518,-8.001,143.542,-8.029,143.582,-8.113,143.614,-8.2,143.45,-8.24,143.282,-8.264,143.095,-8.311,142.905,-8.314,142.808,-8.287,142.709,-8.272,142.615,-8.287,142.524,-8.322,142.448,-8.316,142.399,-8.255,142.376,-8.208,142.347,-8.167,142.276,-8.174,142.207,-8.196,142.325,-8.198,142.361,-8.25,142.391,-8.313,142.475,-8.369,142.576,-8.336,142.798,-8.345,143.014,-8.444,143.065,-8.455,143.112,-8.475,143.223,-8.572,143.307,-8.661,143.377,-8.762,143.392,-8.802,143.388,-8.908,143.366,-8.961,143.227,-9.036,143.078,-9.092,142.859,-9.203,142.647,-9.328,142.536,-9.303,142.435,-9.237,142.396,-9.219,142.293,-9.183,142.23,-9.17,141.979,-9.198,141.727,-9.213,141.622,-9.211,141.519,-9.19,141.406,-9.151,141.294,-9.168,141.217,-9.214,141.133,-9.221,140.925,-9.085,140.787,-8.974,140.662,-8.847]},{"b":[118.783,-5.693,125.234,1.701],"l":[119.772,-0.484,119.722,-0.088,119.736,-0.051,119.787,-0.057,119.838,-0.022,119.866,0.04,119.812,0.187,119.809,0.239,119.913,0.445,119.998,0.52,120.035,0.567,120.056,0.693,120.101,0.74,120.157,0.774,120.23,0.861,120.27,0.971,120.294,0.979,120.322,0.983,120.367,0.888,120.416,0.849,120.517,0.818,120.603,0.854,120.626,0.902,120.659,0.944,120.711,0.987,120.755,1.036,120.804,1.149,120.868,1.253,120.912,1.289,120.965,1.312,121.025,1.326,121.082,1.328,121.208,1.263,121.282,1.25,121.357,1.255,121.404,1.244,121.44,1.214,121.473,1.156,121.513,1.105,121.551,1.08,121.592,1.068,121.867,1.089,122.108,1.031,122.437,1.018,122.549,0.984,122.657,0.941,122.79,0.863,122.838,0.846,122.892,0.85,122.96,0.923,123.013,0.939,123.067,0.942,123.278,0.928,123.847,0.838,123.931,0.85,124.274,1.022,124.411,1.185,124.534,1.23,124.575,1.304,124.6,1.392,124.644,1.416,124.747,1.441,124.788,1.468,124.861,1.576,124.947,1.672,124.989,1.701,125.111,1.686,125.165,1.644,125.234,1.502,125.222,1.479,125.141,1.408,125.117,1.379,125.028,1.18,124.967,1.083,124.889,0.995,124.698,0.826,124.64,0.744,124.589,0.655,124.514,0.557,124.428,0.471,124.384,0.445,124.278,0.398,124.217,0.38,124.101,0.375,123.754,0.306,123.64,0.297,123.526,0.3,123.31,0.318,123.265,0.327,123.179,0.416,123.083,0.486,122.997,0.494,122.91,0.486,122.281,0.481,122.061,0.468,121.842,0.437,121.723,0.451,121.605,0.486,121.516,0.498,121.426,0.495,121.013,0.442,120.909,0.447,120.7,0.515,120.579,0.528,120.46,0.51,120.349,0.449,120.307,0.408,120.192,0.269,120.127,0.167,120.078,0.04,120.036,-0.09,120.013,-0.196,120.012,-0.307,120.032,-0.432,120.063,-0.556,120.097,-0.65,120.241,-0.868,120.27,-0.899,120.425,-0.961,120.518,-1.039,120.605,-1.258,120.667,-1.37,120.729,-1.371,120.797,-1.364,120.916,-1.378,121.034,-1.407,121.149,-1.339,121.213,-1.212,121.277,-1.118,121.431,-0.939,121.519,-0.856,121.576,-0.829,121.633,-0.84,121.681,-0.888,121.738,-0.926,121.853,-0.946,121.97,-0.933,122.094,-0.875,122.138,-0.839,122.175,-0.794,122.28,-0.757,122.53,-0.757,122.659,-0.77,122.889,-0.755,122.886,-0.722,122.841,-0.687,122.829,-0.659,122.872,-0.641,123.02,-0.6,123.171,-0.571,123.281,-0.592,123.38,-0.649,123.417,-0.707,123.434,-0.778,123.396,-0.962,123.378,-1.004,123.3,-1.026,123.226,-1.002,123.153,-0.907,123.049,-0.872,122.903,-0.901,122.853,-0.928,122.807,-0.966,122.725,-1.064,122.656,-1.175,122.507,-1.348,122.334,-1.498,122.251,-1.555,122.158,-1.594,121.859,-1.693,121.78,-1.767,121.719,-1.863,121.651,-1.895,121.573,-1.906,121.514,-1.888,121.395,-1.834,121.355,-1.878,121.349,-1.946,121.408,-1.97,121.502,-2.045,121.575,-2.151,121.622,-2.174,121.726,-2.208,121.77,-2.241,121.848,-2.332,121.972,-2.542,122.014,-2.656,122.083,-2.75,122.292,-2.908,122.303,-2.952,122.29,-3.004,122.307,-3.052,122.381,-3.142,122.399,-3.201,122.317,-3.275,122.313,-3.383,122.263,-3.527,122.251,-3.576,122.253,-3.62,122.288,-3.662,122.329,-3.694,122.385,-3.711,122.435,-3.74,122.529,-3.853,122.579,-3.882,122.61,-3.923,122.607,-3.985,122.65,-4.021,122.69,-4.084,122.75,-4.1,122.779,-4.082,122.798,-4.054,122.848,-4.065,122.877,-4.109,122.894,-4.166,122.9,-4.229,122.897,-4.349,122.872,-4.392,122.818,-4.39,122.72,-4.341,122.715,-4.376,122.722,-4.411,122.672,-4.422,122.615,-4.417,122.471,-4.422,122.207,-4.496,122.114,-4.54,122.054,-4.62,122.05,-4.675,122.073,-4.792,122.038,-4.832,121.917,-4.848,121.748,-4.817,121.646,-4.786,121.589,-4.76,121.514,-4.681,121.487,-4.581,121.541,-4.283,121.557,-4.245,121.583,-4.211,121.612,-4.156,121.618,-4.093,121.537,-4.015,121.416,-3.984,121.313,-3.919,120.914,-3.556,120.892,-3.521,120.891,-3.46,120.907,-3.404,121.038,-3.205,121.054,-3.167,121.07,-3.01,121.067,-2.881,121.052,-2.752,120.99,-2.67,120.879,-2.646,120.765,-2.642,120.654,-2.668,120.544,-2.733,120.341,-2.87,120.261,-2.949,120.254,-3.053,120.3,-3.154,120.36,-3.247,120.392,-3.348,120.437,-3.707,120.435,-3.748,120.383,-3.852,120.363,-4.086,120.385,-4.415,120.42,-4.617,120.405,-4.727,120.31,-4.963,120.281,-5.093,120.279,-5.146,120.391,-5.393,120.417,-5.49,120.43,-5.591,120.312,-5.542,120.256,-5.544,120.201,-5.559,120.077,-5.575,119.952,-5.578,119.908,-5.596,119.818,-5.662,119.764,-5.688,119.717,-5.693,119.557,-5.611,119.463,-5.522,119.376,-5.425,119.36,-5.314,119.391,-5.201,119.434,-5.079,119.52,-4.877,119.516,-4.742,119.545,-4.631,119.594,-4.523,119.612,-4.424,119.624,-4.034,119.611,-4,119.494,-3.769,119.48,-3.73,119.479,-3.667,119.492,-3.608,119.495,-3.554,119.467,-3.513,119.42,-3.475,119.362,-3.459,119.24,-3.475,118.995,-3.538,118.922,-3.483,118.868,-3.398,118.833,-3.28,118.813,-3.157,118.822,-3.041,118.858,-2.929,118.829,-2.85,118.784,-2.765,118.783,-2.721,118.809,-2.682,118.853,-2.65,118.908,-2.631,118.958,-2.597,119.092,-2.483,119.135,-2.382,119.138,-2.258,119.172,-2.14,119.241,-2.031,119.322,-1.93,119.348,-1.825,119.308,-1.66,119.324,-1.584,119.31,-1.496,119.309,-1.408,119.359,-1.243,119.508,-0.907,119.654,-0.728,119.711,-0.681,119.787,-0.764,119.844,-0.862,119.845,-0.773,119.83,-0.686,119.772,-0.484]},{"b":[105.243,-8.77,114.599,-5.904],"l":[110.607,-8.149,110.83,-8.202,111.055,-8.24,111.339,-8.262,111.51,-8.305,112.115,-8.324,112.352,-8.354,112.586,-8.4,112.679,-8.409,112.772,-8.396,112.898,-8.361,113.019,-8.313,113.134,-8.288,113.253,-8.287,113.693,-8.478,113.94,-8.568,114.16,-8.626,114.277,-8.615,114.339,-8.647,114.383,-8.705,114.459,-8.741,114.584,-8.77,114.599,-8.727,114.595,-8.685,114.482,-8.604,114.449,-8.559,114.387,-8.405,114.381,-8.334,114.385,-8.263,114.443,-8.005,114.444,-7.896,114.409,-7.792,114.383,-7.771,114.071,-7.633,114.037,-7.632,113.876,-7.677,113.747,-7.703,113.498,-7.724,113.248,-7.718,113.014,-7.658,112.795,-7.552,112.783,-7.432,112.794,-7.304,112.752,-7.265,112.649,-7.221,112.626,-7.178,112.587,-7.051,112.539,-6.926,112.434,-6.903,112.312,-6.894,112.137,-6.905,112.087,-6.893,111.99,-6.806,111.738,-6.773,111.688,-6.742,111.644,-6.699,111.54,-6.648,111.484,-6.652,111.387,-6.693,111.342,-6.7,111.182,-6.687,111.154,-6.669,111.001,-6.465,110.972,-6.436,110.835,-6.424,110.784,-6.443,110.737,-6.472,110.701,-6.518,110.674,-6.57,110.634,-6.69,110.584,-6.806,110.521,-6.897,110.426,-6.947,110.373,-6.948,110.321,-6.938,110.261,-6.912,110.198,-6.895,110.067,-6.899,109.936,-6.916,109.821,-6.902,109.587,-6.843,109.501,-6.81,109.404,-6.86,109.294,-6.867,109.018,-6.817,108.899,-6.808,108.78,-6.808,108.678,-6.791,108.604,-6.729,108.538,-6.516,108.516,-6.471,108.419,-6.383,108.33,-6.286,108.295,-6.265,108.254,-6.267,108.197,-6.289,108.138,-6.297,108.009,-6.277,107.884,-6.233,107.776,-6.219,107.667,-6.216,107.563,-6.183,107.475,-6.122,107.374,-6.008,107.332,-5.978,107.162,-5.957,107.046,-5.904,107.012,-6.008,106.932,-6.073,106.878,-6.092,106.825,-6.098,106.676,-6.038,106.569,-6.022,106.459,-6.018,106.35,-5.984,106.166,-5.965,106.075,-5.914,106.029,-5.934,105.936,-6.017,105.868,-6.116,105.787,-6.457,105.757,-6.48,105.706,-6.498,105.655,-6.47,105.608,-6.617,105.581,-6.671,105.484,-6.782,105.46,-6.787,105.405,-6.768,105.387,-6.751,105.371,-6.664,105.336,-6.674,105.273,-6.729,105.243,-6.778,105.255,-6.835,105.303,-6.841,105.362,-6.826,105.421,-6.833,105.478,-6.854,105.601,-6.86,105.725,-6.846,105.835,-6.846,105.944,-6.859,106.198,-6.928,106.52,-7.054,106.492,-7.114,106.448,-7.177,106.417,-7.239,106.411,-7.312,106.455,-7.369,106.535,-7.394,106.631,-7.416,107.071,-7.447,107.285,-7.472,107.547,-7.542,107.598,-7.567,107.696,-7.636,107.804,-7.688,107.917,-7.724,108.221,-7.782,108.336,-7.794,108.452,-7.797,108.518,-7.736,108.571,-7.707,108.741,-7.667,108.856,-7.668,108.987,-7.704,109.194,-7.695,109.282,-7.705,109.853,-7.828,110.039,-7.891,110.607,-8.149]},{"b":[95.207,-5.908,106.058,5.629],"l":[100.817,2.14,100.818,2.194,100.828,2.243,100.877,2.283,100.936,2.295,101.046,2.257,101.225,2.102,101.301,2.012,101.358,1.887,101.405,1.757,101.477,1.693,101.575,1.671,101.684,1.661,101.785,1.621,102.02,1.442,102.098,1.358,102.157,1.259,102.198,1.142,102.223,1.019,102.239,0.99,102.39,0.842,102.469,0.779,102.566,0.749,102.849,0.715,102.949,0.664,103.032,0.579,103.067,0.492,103.008,0.415,102.786,0.298,102.55,0.216,102.78,0.244,102.896,0.279,103.003,0.332,103.109,0.4,103.277,0.495,103.339,0.514,103.412,0.507,103.479,0.48,103.579,0.387,103.673,0.289,103.743,0.174,103.787,0.047,103.706,-0.02,103.589,-0.069,103.429,-0.192,103.412,-0.24,103.444,-0.272,103.405,-0.362,103.495,-0.418,103.509,-0.466,103.431,-0.534,103.439,-0.576,103.533,-0.755,103.578,-0.796,103.721,-0.887,103.94,-0.979,104.061,-1.021,104.199,-1.054,104.258,-1.053,104.361,-1.038,104.381,-1.074,104.426,-1.251,104.447,-1.362,104.478,-1.6,104.519,-1.699,104.516,-1.819,104.569,-1.922,104.676,-1.987,104.791,-2.041,104.845,-2.093,104.845,-2.172,104.826,-2.234,104.787,-2.283,104.668,-2.386,104.647,-2.43,104.631,-2.543,104.651,-2.595,104.698,-2.598,104.736,-2.571,104.878,-2.419,104.917,-2.392,104.971,-2.371,105.026,-2.358,105.287,-2.356,105.397,-2.38,105.495,-2.43,105.582,-2.492,105.899,-2.888,106.044,-3.106,106.056,-3.161,106.058,-3.217,106.034,-3.261,105.901,-3.41,105.885,-3.451,105.844,-3.614,105.852,-3.731,105.896,-3.78,105.93,-3.833,105.928,-3.881,105.841,-4.122,105.831,-4.163,105.887,-4.554,105.891,-4.66,105.879,-4.794,105.887,-5.01,105.816,-5.677,105.803,-5.716,105.748,-5.818,105.677,-5.818,105.619,-5.8,105.578,-5.761,105.556,-5.712,105.523,-5.673,105.349,-5.55,105.304,-5.57,105.128,-5.723,105.081,-5.746,105.023,-5.727,104.93,-5.681,104.64,-5.52,104.622,-5.572,104.618,-5.642,104.676,-5.816,104.684,-5.893,104.631,-5.908,104.602,-5.905,104.481,-5.803,104.37,-5.691,104.243,-5.539,104.15,-5.467,104.067,-5.386,103.831,-5.08,103.77,-5.033,103.406,-4.816,103.332,-4.765,103.239,-4.676,103.139,-4.596,102.919,-4.471,102.538,-4.152,102.372,-3.969,102.188,-3.675,102.128,-3.599,101.818,-3.378,101.649,-3.244,101.579,-3.167,101.414,-2.899,101.366,-2.808,101.306,-2.729,101.206,-2.664,101.119,-2.588,100.944,-2.345,100.89,-2.249,100.848,-2.144,100.855,-1.934,100.487,-1.299,100.394,-1.101,100.308,-0.827,100.289,-0.799,100.088,-0.553,100.017,-0.474,99.931,-0.4,99.86,-0.314,99.721,-0.033,99.67,0.045,99.598,0.102,99.335,0.209,99.236,0.268,99.159,0.352,99.112,0.459,99.06,0.686,98.936,1.032,98.796,1.495,98.703,1.702,98.595,1.865,98.564,1.902,98.087,2.195,98.005,2.238,97.919,2.264,97.795,2.283,97.701,2.359,97.662,2.494,97.641,2.676,97.617,2.785,97.591,2.847,97.391,2.975,97.313,3.077,97.248,3.189,97.188,3.276,96.969,3.575,96.894,3.654,96.801,3.709,96.525,3.767,96.445,3.816,96.311,3.986,96.23,4.073,95.988,4.263,95.579,4.662,95.495,4.761,95.432,4.865,95.381,4.976,95.207,5.284,95.221,5.346,95.247,5.411,95.243,5.464,95.224,5.517,95.228,5.565,95.28,5.593,95.396,5.629,95.517,5.625,95.629,5.609,95.737,5.579,95.841,5.515,96.027,5.351,96.133,5.294,96.251,5.267,96.493,5.229,96.615,5.22,96.843,5.274,96.968,5.269,97.086,5.23,97.19,5.207,97.451,5.236,97.5,5.228,97.547,5.206,97.588,5.17,97.707,5.04,97.908,4.88,97.967,4.777,98,4.662,98.021,4.635,98.248,4.415,98.273,4.322,98.241,4.195,98.307,4.093,98.528,3.998,98.659,3.928,98.687,3.886,98.706,3.835,98.778,3.759,98.869,3.71,99.151,3.581,99.521,3.311,99.732,3.183,99.907,2.988,99.969,2.895,100.021,2.794,100.127,2.648,100.307,2.467,100.353,2.411,100.401,2.332,100.457,2.257,100.524,2.189,100.604,2.137,100.685,2.12,100.817,1.989,100.888,1.948,100.877,2.051,100.817,2.14]},{"b":[108.906,-4.17,119.266,6.99],"l":[112.119,2.915,111.728,2.854,111.623,2.818,111.513,2.743,111.443,2.634,111.44,2.498,111.451,2.424,111.444,2.382,111.406,2.368,111.351,2.364,111.296,2.399,111.242,2.436,111.209,2.38,111.196,2.297,111.209,2.198,111.268,2.14,111.251,2.064,111.198,1.985,111.17,1.902,111.154,1.739,111.11,1.684,111.047,1.634,111.029,1.558,111.058,1.487,111.123,1.449,111.223,1.396,111.145,1.387,111.098,1.401,110.94,1.517,110.895,1.532,110.782,1.521,110.675,1.548,110.4,1.7,110.349,1.72,110.298,1.701,110.246,1.695,110.114,1.699,109.985,1.718,109.865,1.764,109.72,1.858,109.694,1.889,109.629,2.028,109.379,1.923,109.318,1.821,109.273,1.705,109.167,1.607,109.076,1.496,109.055,1.438,109.01,1.24,109.065,1.247,109.096,1.258,109.132,1.254,109.088,1.224,109.031,1.204,108.959,1.135,108.917,0.913,108.906,0.794,108.923,0.533,108.945,0.356,109.075,0.253,109.149,0.168,109.181,0.117,109.22,0.074,109.247,0.056,109.258,0.031,109.195,-0.009,109.165,-0.142,109.15,-0.186,109.122,-0.265,109.121,-0.391,109.13,-0.445,109.161,-0.495,109.257,-0.577,109.373,-0.638,109.366,-0.667,109.312,-0.68,109.271,-0.732,109.259,-0.807,109.289,-0.846,109.333,-0.875,109.454,-0.869,109.682,-0.944,109.787,-1.011,109.873,-1.101,109.938,-1.181,109.983,-1.275,110.019,-1.399,110.036,-1.526,110.023,-1.643,109.964,-1.743,109.96,-1.863,110.075,-1.946,110.097,-2.001,110.124,-2.234,110.224,-2.689,110.233,-2.925,110.256,-2.966,110.303,-2.985,110.351,-2.947,110.378,-2.934,110.574,-2.891,110.668,-3.005,110.703,-3.021,110.736,-2.989,110.811,-2.938,110.899,-2.909,110.852,-2.946,110.83,-2.995,110.869,-3.049,110.93,-3.071,111.044,-3.056,111.259,-2.956,111.368,-2.934,111.495,-2.973,111.625,-2.975,111.658,-2.926,111.695,-2.889,111.76,-2.939,111.809,-3.008,111.823,-3.057,111.836,-3.308,111.834,-3.42,111.822,-3.533,111.858,-3.552,111.907,-3.553,111.955,-3.53,112.127,-3.381,112.285,-3.321,112.444,-3.371,112.6,-3.4,112.758,-3.322,112.971,-3.187,113.034,-2.933,113.343,-3.246,113.367,-3.224,113.409,-3.229,113.526,-3.184,113.566,-3.178,113.61,-3.196,113.63,-3.246,113.637,-3.332,113.634,-3.42,113.705,-3.455,113.796,-3.456,113.959,-3.394,114.082,-3.279,114.109,-3.285,114.128,-3.327,114.178,-3.354,114.236,-3.361,114.293,-3.306,114.344,-3.235,114.302,-3.365,114.305,-3.41,114.344,-3.444,114.397,-3.471,114.446,-3.482,114.526,-3.377,114.536,-3.494,114.606,-3.703,114.625,-4.112,114.653,-4.152,114.694,-4.17,115.258,-3.907,115.956,-3.595,115.999,-3.523,116.017,-3.433,116.058,-3.348,116.15,-3.233,116.167,-3.183,116.205,-3.149,116.257,-3.126,116.172,-3.025,116.154,-2.984,116.166,-2.935,116.226,-2.977,116.289,-2.959,116.331,-2.902,116.353,-2.833,116.372,-2.707,116.375,-2.578,116.307,-2.603,116.317,-2.552,116.353,-2.522,116.401,-2.52,116.45,-2.538,116.529,-2.511,116.549,-2.411,116.565,-2.3,116.528,-2.208,116.418,-2.187,116.369,-2.158,116.314,-2.14,116.424,-2.053,116.452,-1.923,116.43,-1.864,116.424,-1.785,116.353,-1.779,116.275,-1.785,116.3,-1.744,116.332,-1.712,116.478,-1.633,116.518,-1.598,116.545,-1.553,116.554,-1.474,116.612,-1.429,116.715,-1.376,116.753,-1.327,116.771,-1.267,116.759,-1.207,116.729,-1.151,116.726,-1.098,116.74,-1.044,116.761,-1.117,116.797,-1.184,116.849,-1.218,116.914,-1.224,117.003,-1.188,117.07,-1.113,117.146,-1.009,117.241,-0.926,117.357,-0.867,117.522,-0.797,117.563,-0.771,117.574,-0.728,117.557,-0.675,117.549,-0.554,117.463,-0.324,117.464,-0.2,117.522,0.236,117.553,0.341,117.745,0.73,117.777,0.754,117.853,0.789,117.923,0.831,117.964,0.89,117.977,0.964,117.952,1.032,117.912,1.099,118.016,1.039,118.096,0.929,118.196,0.874,118.312,0.847,118.535,0.814,118.757,0.839,118.892,0.887,118.985,0.982,118.963,1.044,118.853,1.096,118.639,1.319,118.472,1.416,118.157,1.64,118.08,1.702,117.928,1.867,117.865,1.968,117.831,2.002,117.789,2.027,117.881,2.061,117.889,2.087,117.957,2.16,118.042,2.215,118.066,2.263,118.067,2.318,118.034,2.378,117.886,2.542,117.805,2.669,117.786,2.747,117.75,2.776,117.667,2.807,117.639,2.825,117.665,2.859,117.698,2.887,117.637,2.915,117.569,2.929,117.638,2.951,117.612,3.005,117.611,3.064,117.567,3.098,117.507,3.105,117.422,3.165,117.352,3.194,117.322,3.244,117.385,3.365,117.346,3.427,117.166,3.592,117.114,3.613,117.056,3.623,117.172,3.639,117.288,3.639,117.45,3.629,117.495,3.666,117.51,3.73,117.567,3.678,117.63,3.636,117.714,3.645,117.777,3.689,117.762,3.734,117.732,3.77,117.728,3.797,117.639,3.878,117.566,3.93,117.559,3.988,117.465,4.076,117.497,4.133,117.566,4.162,117.604,4.2,117.65,4.304,117.696,4.343,117.741,4.338,117.896,4.263,118.008,4.25,118.117,4.288,118.229,4.316,118.364,4.336,118.498,4.362,118.548,4.379,118.586,4.41,118.595,4.461,118.563,4.502,118.324,4.669,118.185,4.829,118.261,4.989,118.32,5.012,118.382,5.019,118.551,4.968,118.672,4.964,118.913,5.023,119.132,5.1,119.22,5.16,119.25,5.199,119.263,5.246,119.266,5.308,119.256,5.366,119.223,5.413,119.178,5.431,119.05,5.415,119.003,5.418,118.957,5.429,118.714,5.559,118.595,5.592,118.563,5.685,118.514,5.729,118.456,5.763,118.353,5.806,118.3,5.82,118.249,5.821,118.145,5.754,118.031,5.712,117.974,5.706,117.928,5.769,117.935,5.788,118.072,5.832,118.116,5.863,118.062,5.983,118.004,6.053,117.896,5.972,117.818,5.94,117.501,5.885,117.617,5.941,117.645,6.002,117.65,6.074,117.616,6.197,117.696,6.272,117.694,6.35,117.67,6.427,117.646,6.474,117.61,6.513,117.499,6.571,117.38,6.612,117.294,6.677,117.255,6.783,117.245,6.833,117.252,6.919,117.23,6.94,117.129,6.969,117.078,6.917,117.019,6.797,116.913,6.66,116.812,6.608,116.788,6.606,116.808,6.691,116.842,6.772,116.85,6.827,116.833,6.952,116.776,6.99,116.75,6.977,116.538,6.583,116.495,6.522,116.138,6.13,116.11,6.003,116.06,5.882,115.918,5.725,115.877,5.614,115.797,5.536,115.741,5.533,115.685,5.535,115.625,5.549,115.604,5.603,115.556,5.567,115.419,5.413,115.422,5.331,115.467,5.254,115.582,5.194,115.554,5.094,115.52,5.049,115.428,4.969,115.375,4.933,115.14,4.9,115.027,4.9,115.047,4.962,115.048,5.016,114.995,5.022,114.841,4.946,114.741,4.881,114.646,4.798,114.545,4.725,114.424,4.66,114.299,4.607,114.178,4.591,114.054,4.593,114.013,4.575,113.984,4.546,113.99,4.483,113.988,4.421,113.953,4.289,113.924,4.243,113.712,4.001,113.446,3.741,113.32,3.561,113.14,3.344,113.045,3.205,112.988,3.162,112.921,3.131,112.737,3.07,112.119,2.915]},{"b":[53.763,73.297,68.942,77.012],"l":[58.618,74.227,58.665,74.289,58.646,74.328,58.562,74.422,58.502,74.464,58.535,74.499,58.928,74.463,59.04,74.486,59.101,74.508,59.146,74.552,59.157,74.611,59.182,74.666,59.24,74.693,59.596,74.614,59.674,74.61,59.753,74.637,59.771,74.664,59.735,74.695,59.747,74.746,59.982,74.745,60.08,74.756,60.222,74.797,60.301,74.837,60.439,74.875,60.501,74.905,60.455,74.946,60.241,74.971,60.277,75.008,60.476,75.055,60.534,75.059,60.655,75.055,60.719,75.069,60.829,75.111,60.936,75.164,61.147,75.223,61.249,75.281,61.356,75.315,61.487,75.311,61.616,75.32,62.066,75.428,63.046,75.576,63.317,75.603,63.659,75.669,63.779,75.673,64.263,75.72,64.745,75.788,65.202,75.839,65.619,75.905,66.282,75.984,66.657,76.047,66.893,76.072,67.127,76.108,67.365,76.161,67.765,76.238,68.165,76.285,68.222,76.313,68.559,76.449,68.9,76.573,68.858,76.61,68.891,76.66,68.942,76.708,68.912,76.761,68.873,76.79,68.699,76.871,68.486,76.934,68.017,76.991,67.652,77.012,67.535,77.008,67.264,76.964,66.829,76.924,66.345,76.821,66.063,76.746,65.959,76.688,65.863,76.613,65.755,76.579,65.637,76.579,65.528,76.568,65.31,76.518,65.197,76.5,65.073,76.497,64.95,76.484,64.708,76.426,64.463,76.378,63.526,76.31,62.971,76.237,62.782,76.245,62.471,76.23,62.237,76.242,61.787,76.291,61.569,76.298,61.202,76.282,61.157,76.274,61.034,76.233,61.037,76.169,61.054,76.12,60.998,76.089,60.942,76.071,60.801,76.069,60.731,76.104,60.606,76.109,60.279,76.096,60.118,76.067,60.036,75.984,59.782,75.946,59.347,75.907,59.11,75.874,58.995,75.872,58.881,75.855,58.653,75.777,58.418,75.72,58.058,75.663,58.073,75.619,58.094,75.593,57.783,75.507,57.708,75.454,57.632,75.356,57.607,75.341,57.302,75.373,57.088,75.384,56.989,75.375,56.844,75.351,56.809,75.328,56.829,75.278,56.876,75.244,56.57,75.098,56.485,75.096,56.389,75.138,56.289,75.164,56.162,75.187,56.036,75.194,55.921,75.168,55.81,75.125,55.821,75.091,55.863,75.059,55.998,75.003,56.34,75.013,56.429,74.973,56.499,74.957,56.218,74.898,55.914,74.796,55.66,74.656,55.582,74.628,55.61,74.591,55.662,74.556,55.752,74.541,55.947,74.542,56.137,74.496,56.078,74.481,55.416,74.436,55.341,74.42,55.023,74.187,54.92,74.129,54.831,74.096,54.733,74.034,54.643,73.96,54.606,73.951,54.386,73.936,54.174,73.886,53.963,73.822,53.851,73.801,53.763,73.766,53.839,73.697,54.205,73.542,54.132,73.481,54.3,73.351,54.566,73.419,54.769,73.449,55.007,73.454,55.28,73.392,55.549,73.357,56.035,73.346,56.228,73.314,56.43,73.297,56.634,73.304,56.964,73.367,57.134,73.504,57.46,73.61,57.543,73.658,57.464,73.746,57.291,73.815,57.313,73.838,57.449,73.826,57.604,73.775,57.657,73.768,57.756,73.769,57.845,73.805,57.872,73.85,57.853,73.898,57.778,73.974,57.767,74.014,58.441,74.129,58.618,74.227]},{"b":[51.429,70.563,57.625,73.388],"l":[51.583,72.071,51.653,72.099,51.805,72.142,51.885,72.153,52.069,72.131,52.252,72.13,52.332,72.154,52.407,72.197,52.462,72.252,52.586,72.284,52.622,72.301,52.662,72.337,52.706,72.391,52.714,72.437,52.749,72.483,52.864,72.55,52.823,72.591,52.839,72.619,52.917,72.669,52.683,72.682,52.605,72.704,52.529,72.737,52.551,72.769,52.579,72.791,52.812,72.875,52.913,72.9,53.024,72.914,53.135,72.913,53.254,72.904,53.37,72.917,53.247,72.973,53.237,73.011,53.189,73.104,53.198,73.148,53.251,73.183,53.358,73.225,53.512,73.238,53.634,73.26,53.753,73.293,53.866,73.299,54.091,73.276,54.202,73.281,54.328,73.299,54.676,73.37,54.804,73.388,54.941,73.383,55.121,73.357,55.32,73.308,55.787,73.269,56.138,73.256,56.35,73.226,56.43,73.201,56.397,73.139,56.335,73.114,56.189,73.033,56.167,72.983,56.193,72.905,56.171,72.848,56.122,72.807,56.084,72.789,55.82,72.79,55.723,72.766,55.718,72.722,55.701,72.672,55.616,72.599,55.441,72.575,55.403,72.549,55.417,72.501,55.356,72.465,55.36,72.409,55.39,72.378,55.399,72.314,55.518,72.221,55.495,72.182,55.403,72.107,55.375,72.015,55.298,71.935,55.471,71.869,55.547,71.783,55.614,71.69,55.819,71.508,56.043,71.346,56.454,71.107,56.895,70.927,57.066,70.876,57.484,70.792,57.556,70.766,57.625,70.729,57.447,70.661,57.264,70.636,57.247,70.605,57.146,70.589,56.649,70.647,56.622,70.655,56.569,70.697,56.51,70.729,56.386,70.734,56.26,70.715,56.335,70.677,56.417,70.665,56.561,70.594,56.5,70.566,56.435,70.563,56.142,70.658,56.115,70.646,56.087,70.618,55.942,70.649,55.907,70.626,55.797,70.616,55.707,70.642,55.706,70.675,55.687,70.692,55.237,70.666,55.052,70.667,54.867,70.678,54.645,70.742,54.608,70.713,54.601,70.68,54.517,70.693,54.333,70.745,54.199,70.765,53.722,70.814,53.384,70.874,53.468,70.901,53.614,70.915,53.616,70.951,53.593,71.001,53.588,71.052,53.671,71.087,53.857,71.07,53.834,71.127,53.922,71.138,54.094,71.105,54.156,71.125,53.886,71.196,53.591,71.297,53.622,71.333,53.515,71.343,53.41,71.34,53.319,71.399,53.333,71.477,53.412,71.53,53.364,71.542,52.909,71.495,52.679,71.506,52.419,71.537,52.18,71.49,51.938,71.475,51.813,71.491,51.692,71.525,51.59,71.571,51.511,71.648,51.439,71.777,51.429,71.826,51.444,71.934,51.482,71.98,51.583,72.071]},{"b":[141.661,45.917,144.714,54.416],"l":[142.335,54.281,142.467,54.149,142.67,53.968,142.706,53.896,142.683,53.816,142.643,53.737,142.689,53.73,142.68,53.674,142.553,53.653,142.509,53.588,142.583,53.537,142.526,53.447,142.424,53.411,142.371,53.403,142.319,53.405,142.18,53.484,142.142,53.496,141.964,53.456,141.852,53.389,141.824,53.34,141.839,53.138,141.874,53.039,141.856,52.794,141.803,52.556,141.748,52.455,141.682,52.359,141.661,52.273,141.668,51.933,141.721,51.847,141.808,51.789,141.772,51.752,141.722,51.736,141.772,51.69,141.873,51.63,142.006,51.521,142.091,51.429,142.207,51.223,142.208,50.998,142.147,50.89,142.1,50.776,142.066,50.63,142.071,50.515,142.143,50.312,142.153,50.217,142.142,49.569,142.109,49.44,142.067,49.312,142.02,49.078,141.98,48.972,141.866,48.75,141.873,48.702,141.897,48.655,142.029,48.477,142.135,48.29,142.182,48.013,142.149,47.902,142.076,47.808,142.016,47.701,141.964,47.587,141.963,47.544,141.984,47.348,142.017,47.245,142.039,47.14,142.011,47.03,141.867,46.694,141.83,46.451,141.916,46.171,141.93,46.088,141.962,46.013,142.016,45.962,142.077,45.917,142.15,45.999,142.209,46.089,142.304,46.358,142.35,46.459,142.406,46.555,142.479,46.644,142.578,46.701,142.636,46.716,142.692,46.711,142.747,46.671,142.796,46.62,142.829,46.605,143.048,46.593,143.282,46.559,143.352,46.476,143.37,46.358,143.419,46.222,143.432,46.029,143.463,46.069,143.482,46.116,143.491,46.175,143.509,46.23,143.581,46.361,143.579,46.406,143.54,46.575,143.486,46.752,143.447,46.792,143.384,46.806,143.319,46.807,143.218,46.795,143.178,46.844,143.089,47.001,143.006,47.223,142.94,47.323,142.905,47.362,142.864,47.392,142.801,47.416,142.745,47.452,142.67,47.537,142.579,47.684,142.557,47.738,142.546,47.885,142.574,48.072,142.651,48.247,142.972,48.918,143.027,49.105,143.105,49.199,143.236,49.263,143.382,49.291,143.732,49.312,143.819,49.309,143.968,49.276,144.049,49.249,144.125,49.209,144.284,49.07,144.412,48.986,144.536,48.894,144.621,48.815,144.673,48.679,144.714,48.64,144.707,48.82,144.686,48.871,144.607,48.936,144.432,49.051,144.341,49.181,144.272,49.311,144.24,49.432,144.2,49.55,144.141,49.661,144.048,49.896,143.816,50.283,143.736,50.507,143.534,51.246,143.489,51.277,143.473,51.299,143.467,51.402,143.455,51.471,143.418,51.521,143.321,51.583,143.3,51.632,143.295,51.744,143.251,51.848,143.191,51.944,143.156,52.084,143.172,52.349,143.201,52.443,143.264,52.479,143.295,52.529,143.324,52.614,143.333,52.7,143.325,52.963,143.288,53.134,143.26,53.217,143.224,53.296,143.096,53.489,142.918,53.794,142.936,53.811,142.911,53.878,142.927,53.956,142.967,54.029,142.986,54.086,142.976,54.141,142.761,54.394,142.693,54.416,142.666,54.358,142.616,54.304,142.552,54.279,142.335,54.281]},{"b":[139.821,41.423,145.833,45.51],"l":[145.214,43.578,145.14,43.663,145.101,43.765,145.126,43.869,145.245,44.076,145.352,44.23,145.37,44.281,145.37,44.327,145.343,44.334,145.102,44.166,144.872,43.982,144.799,43.94,144.715,43.928,144.597,43.93,144.482,43.95,144.101,44.102,144.005,44.117,143.95,44.112,143.824,44.117,143.759,44.132,143.655,44.221,143.512,44.278,143.289,44.397,143.075,44.535,142.885,44.67,142.704,44.819,142.416,45.125,142.172,45.326,142.016,45.438,141.981,45.483,141.938,45.51,141.879,45.483,141.829,45.439,141.778,45.419,141.668,45.401,141.654,45.377,141.653,45.349,141.594,45.256,141.583,45.156,141.656,45.051,141.719,44.941,141.782,44.716,141.761,44.483,141.716,44.371,141.661,44.264,141.645,44.019,141.601,43.919,141.447,43.749,141.398,43.643,141.398,43.513,141.412,43.381,141.374,43.28,141.296,43.2,141.245,43.185,141.138,43.18,140.954,43.201,140.819,43.205,140.781,43.215,140.585,43.312,140.487,43.338,140.392,43.303,140.379,43.237,140.397,43.167,140.486,43.05,140.432,42.954,140.329,42.867,140.224,42.796,140.115,42.733,140.015,42.685,139.951,42.671,139.891,42.649,139.86,42.582,139.829,42.448,139.821,42.388,139.835,42.278,139.895,42.19,140.024,42.1,140.057,42.067,140.108,41.913,140.084,41.803,140.021,41.696,139.995,41.576,140.009,41.521,140.037,41.474,140.085,41.434,140.149,41.423,140.27,41.456,140.385,41.519,140.432,41.567,140.489,41.672,140.593,41.769,140.66,41.816,140.816,41.76,140.908,41.743,141,41.737,141.079,41.76,141.151,41.805,141.108,41.848,140.912,41.978,140.734,42.116,140.684,42.123,140.578,42.119,140.528,42.132,140.417,42.201,140.327,42.293,140.315,42.334,140.324,42.376,140.351,42.435,140.385,42.487,140.48,42.559,140.548,42.57,140.617,42.571,140.71,42.556,140.788,42.5,140.948,42.36,140.986,42.342,141.407,42.547,141.851,42.579,142.088,42.472,142.508,42.258,142.906,42.118,143.112,42.022,143.237,42,143.279,42.038,143.314,42.084,143.327,42.151,143.332,42.22,143.369,42.325,143.429,42.419,143.581,42.599,143.762,42.748,143.969,42.881,144.197,42.974,144.302,42.984,144.516,42.944,144.631,42.947,144.807,42.994,144.921,43.001,145.029,43.032,145.127,43.089,145.23,43.135,145.347,43.177,145.405,43.18,145.505,43.174,145.537,43.193,145.624,43.291,145.726,43.343,145.833,43.386,145.751,43.396,145.674,43.389,145.583,43.328,145.488,43.28,145.436,43.282,145.341,43.303,145.273,43.463,145.214,43.578]},{"b":[130.889,33.487,141.993,41.506],"l":[133.142,34.302,133.019,34.33,132.775,34.255,132.657,34.246,132.534,34.287,132.421,34.353,132.313,34.325,132.238,34.227,132.202,34.032,132.159,33.944,132.146,33.839,132.09,33.855,131.763,34.045,131.741,34.052,131.476,34.019,131.408,34.004,131.323,33.965,131.233,33.948,131.15,33.976,131.072,34.021,130.996,34.007,130.919,33.976,130.889,34.262,130.904,34.3,130.952,34.35,131.004,34.393,131.132,34.407,131.262,34.393,131.354,34.413,131.433,34.47,131.515,34.55,131.608,34.615,131.734,34.667,131.856,34.726,131.963,34.809,132.065,34.9,132.158,34.967,132.26,35.022,132.414,35.156,132.619,35.307,132.698,35.418,132.747,35.449,132.923,35.511,133.157,35.559,133.267,35.557,133.376,35.459,133.435,35.472,133.495,35.497,133.615,35.511,133.739,35.495,133.86,35.495,133.981,35.507,134.214,35.539,134.337,35.578,134.456,35.628,134.882,35.663,135.174,35.747,135.221,35.741,135.265,35.722,135.269,35.66,135.232,35.592,135.268,35.551,135.327,35.526,135.602,35.518,135.68,35.503,135.795,35.55,135.903,35.607,136.016,35.683,136.095,35.768,136.022,35.874,136.006,35.991,136.067,36.117,136.156,36.223,136.262,36.288,136.359,36.362,136.556,36.572,136.698,36.742,136.749,36.951,136.719,37.198,136.843,37.382,136.962,37.414,137.199,37.497,137.323,37.522,137.341,37.485,137.338,37.437,137.152,37.283,137.046,37.22,136.982,37.2,136.924,37.172,136.9,37.118,136.994,37.027,137.019,36.96,137.013,36.895,137.017,36.837,137.124,36.774,137.246,36.753,137.298,36.754,137.343,36.77,137.483,36.925,137.514,36.952,137.913,37.065,138.11,37.151,138.218,37.173,138.32,37.218,138.548,37.392,138.633,37.472,138.709,37.561,138.771,37.663,138.819,37.775,138.885,37.844,139.247,38.009,139.364,38.099,139.401,38.143,139.446,38.269,139.477,38.4,139.521,38.503,139.58,38.599,139.66,38.697,139.749,38.788,139.802,38.882,139.879,39.105,139.912,39.229,139.939,39.273,139.977,39.311,140.011,39.358,140.037,39.411,140.048,39.464,140.065,39.624,140.055,39.749,139.995,39.855,139.945,39.885,139.891,39.887,139.81,39.878,139.742,39.921,139.755,39.959,139.826,39.966,139.874,39.986,139.908,40.022,139.972,40.137,140.011,40.26,140.014,40.315,139.964,40.414,139.924,40.534,139.923,40.598,139.967,40.673,140.029,40.733,140.085,40.747,140.146,40.752,140.201,40.775,140.252,40.809,140.281,40.846,140.326,40.948,140.344,41.006,140.315,41.161,140.344,41.203,140.386,41.23,140.441,41.21,140.498,41.206,140.564,41.212,140.628,41.195,140.64,41.156,140.679,40.893,140.702,40.858,140.749,40.83,140.801,40.834,140.846,40.875,140.876,40.93,140.936,40.941,141.119,40.882,141.183,40.924,141.225,40.988,141.262,41.103,141.244,41.206,141.2,41.244,141.155,41.237,141.115,41.208,141.07,41.193,140.801,41.139,140.802,41.254,140.86,41.425,140.892,41.48,140.937,41.506,141.05,41.476,141.106,41.456,141.229,41.373,141.269,41.354,141.455,41.405,141.42,41.251,141.4,41.096,141.414,40.839,141.43,40.723,141.463,40.611,141.542,40.531,141.646,40.474,141.797,40.291,141.878,40.067,141.935,39.958,141.978,39.844,141.991,39.792,141.992,39.74,141.979,39.668,141.993,39.611,141.977,39.429,141.909,39.219,141.901,39.111,141.842,39.09,141.807,39.04,141.776,39.017,141.742,39,141.694,38.995,141.659,38.975,141.645,38.918,141.622,38.865,141.58,38.817,141.546,38.763,141.519,38.632,141.509,38.498,141.467,38.404,141.368,38.38,141.254,38.381,141.108,38.338,141.077,38.313,140.962,38.149,140.929,38.053,140.928,37.95,140.96,37.823,141.003,37.698,141.036,37.467,141.002,37.115,140.968,37.002,140.895,36.926,140.84,36.89,140.792,36.847,140.73,36.732,140.627,36.503,140.619,36.445,140.619,36.386,140.592,36.308,140.574,36.231,140.59,36.142,140.622,36.059,140.76,35.846,140.813,35.783,140.874,35.725,140.639,35.661,140.597,35.632,140.457,35.51,140.413,35.395,140.417,35.267,140.393,35.221,140.355,35.181,140.315,35.155,140.159,35.096,140.059,35.038,139.96,34.947,139.92,34.9,139.844,34.915,139.799,34.957,139.843,35.01,139.83,35.072,139.851,35.232,139.826,35.297,139.906,35.345,139.944,35.423,140.027,35.485,140.086,35.54,140.097,35.585,140.044,35.633,139.988,35.668,139.91,35.668,139.835,35.658,139.786,35.612,139.77,35.55,139.774,35.52,139.768,35.495,139.65,35.409,139.666,35.319,139.7,35.274,139.744,35.252,139.731,35.222,139.675,35.149,139.636,35.142,139.564,35.243,139.474,35.299,139.363,35.298,139.249,35.278,139.163,35.211,139.134,35.155,139.116,35.097,139.122,34.956,139.086,34.839,139.016,34.736,138.983,34.698,138.897,34.628,138.838,34.619,138.795,34.651,138.761,34.699,138.804,34.876,138.803,34.975,138.904,35.025,138.821,35.096,138.72,35.124,138.577,35.086,138.537,35.044,138.51,34.987,138.433,34.915,138.349,34.848,138.253,34.733,138.189,34.596,137.979,34.641,137.864,34.651,137.749,34.647,137.543,34.664,137.318,34.636,137.062,34.583,137.077,34.621,137.288,34.704,137.295,34.728,137.275,34.773,137.223,34.775,137.097,34.759,137.032,34.766,137.006,34.814,136.963,34.835,136.935,34.815,136.944,34.722,136.913,34.709,136.871,34.733,136.885,34.806,136.856,34.913,136.853,34.979,136.897,35.036,136.852,35.06,136.804,35.05,136.744,35.023,136.69,34.984,136.577,34.79,136.533,34.678,136.616,34.589,136.842,34.464,136.88,34.434,136.881,34.38,136.854,34.324,136.792,34.299,136.544,34.258,136.33,34.177,136.268,34.095,136.073,33.778,135.916,33.562,135.695,33.487,135.453,33.553,135.394,33.629,135.347,33.722,135.257,33.806,135.175,33.898,135.128,34.007,135.135,34.183,135.1,34.288,135.132,34.317,135.266,34.381,135.309,34.417,135.385,34.5,135.412,34.547,135.416,34.617,135.355,34.654,135.198,34.653,135.042,34.631,134.93,34.662,134.785,34.747,134.74,34.765,134.584,34.771,134.472,34.755,134.363,34.724,134.247,34.714,134.208,34.698,134.075,34.593,133.968,34.527,133.876,34.495,133.678,34.486,133.579,34.465,133.474,34.43,133.445,34.433,133.336,34.385,133.21,34.344,133.142,34.302]},{"b":[132.033,32.752,134.739,34.358],"l":[134.695,33.928,134.655,33.983,134.635,34.044,134.638,34.227,134.496,34.215,134.357,34.256,134.219,34.319,134.076,34.358,133.948,34.348,133.826,34.307,133.706,34.237,133.656,34.233,133.603,34.244,133.643,34.135,133.627,34.069,133.582,34.017,133.472,33.973,133.35,33.977,133.299,33.969,133.193,33.933,133.134,33.927,133.051,33.997,132.99,34.088,132.935,34.095,132.839,34.021,132.784,33.992,132.752,33.906,132.716,33.852,132.699,33.791,132.643,33.69,132.536,33.633,132.366,33.512,132.288,33.47,132.114,33.395,132.033,33.34,132.086,33.34,132.281,33.417,132.375,33.434,132.413,33.43,132.405,33.331,132.445,33.305,132.511,33.293,132.515,33.255,132.505,33.211,132.477,33.181,132.476,33.126,132.428,33.059,132.493,33.008,132.495,32.917,132.602,32.92,132.709,32.902,132.642,32.762,132.692,32.776,132.804,32.752,132.87,32.755,132.977,32.842,133.016,32.984,133.051,33.012,133.101,33.028,133.146,33.083,133.24,33.25,133.286,33.36,133.632,33.511,133.686,33.516,133.854,33.493,133.959,33.448,134.124,33.287,134.182,33.247,134.206,33.347,134.243,33.439,134.307,33.527,134.377,33.608,134.549,33.729,134.739,33.821,134.675,33.848,134.695,33.928]},{"b":[129.58,31.015,132.009,33.928],"l":[131.977,32.844,132.002,32.882,132.009,32.919,131.91,32.974,131.937,33.01,131.949,33.047,131.903,33.088,131.848,33.118,131.855,33.182,131.897,33.255,131.717,33.252,131.537,33.274,131.616,33.392,131.711,33.502,131.724,33.554,131.696,33.603,131.643,33.638,131.583,33.652,131.499,33.624,131.419,33.584,131.366,33.571,131.309,33.573,131.175,33.603,131.058,33.673,131.009,33.776,130.953,33.872,130.84,33.918,130.716,33.928,130.67,33.915,130.484,33.835,130.457,33.789,130.439,33.734,130.365,33.634,130.275,33.598,130.168,33.598,130.131,33.578,130.103,33.54,130.072,33.522,129.919,33.483,129.826,33.437,129.837,33.404,129.858,33.375,129.844,33.322,129.702,33.36,129.66,33.365,129.61,33.344,129.58,33.236,129.665,33.187,129.799,33.084,129.897,33.022,129.922,32.988,129.992,32.852,129.901,32.852,129.828,32.893,129.778,32.986,129.679,33.06,129.662,32.995,129.668,32.929,129.69,32.875,129.786,32.782,129.827,32.725,129.808,32.645,129.769,32.571,129.853,32.622,129.951,32.722,130.054,32.771,130.152,32.748,130.193,32.706,130.246,32.677,130.298,32.675,130.34,32.702,130.361,32.756,130.354,32.81,130.326,32.853,130.28,32.867,130.222,32.847,130.175,32.851,130.168,32.932,130.173,33.013,130.127,33.105,130.177,33.145,130.238,33.178,130.287,33.155,130.382,33.093,130.44,32.951,130.547,32.832,130.569,32.734,130.498,32.657,130.563,32.626,130.641,32.619,130.56,32.456,130.462,32.305,130.395,32.219,130.319,32.144,130.214,32.115,130.194,32.091,130.196,31.95,130.211,31.849,130.188,31.769,130.224,31.73,130.269,31.696,130.322,31.601,130.307,31.488,130.294,31.451,130.261,31.437,130.147,31.408,130.201,31.292,130.251,31.273,130.311,31.267,130.589,31.179,130.621,31.218,130.645,31.267,130.566,31.352,130.54,31.403,130.528,31.46,130.556,31.563,130.613,31.665,130.655,31.718,130.715,31.718,130.776,31.706,130.797,31.671,130.796,31.624,130.78,31.604,130.749,31.598,130.704,31.577,130.709,31.526,130.774,31.383,130.79,31.269,130.758,31.156,130.736,31.122,130.704,31.094,130.684,31.059,130.686,31.015,130.902,31.112,131.098,31.256,131.035,31.378,131.071,31.437,131.14,31.442,131.25,31.41,131.337,31.405,131.46,31.671,131.475,31.778,131.46,31.883,131.506,32.002,131.531,32.117,131.565,32.223,131.61,32.325,131.66,32.466,131.732,32.593,131.977,32.844]},{"b":[120.072,21.925,121.929,25.277],"l":[121.905,25.056,121.853,25.104,121.733,25.154,121.687,25.182,121.643,25.232,121.594,25.275,121.517,25.277,121.45,25.249,121.365,25.159,121.095,25.065,121.041,25.033,120.964,24.928,120.902,24.813,120.836,24.723,120.757,24.642,120.63,24.479,120.159,23.709,120.132,23.653,120.125,23.527,120.143,23.399,120.121,23.305,120.086,23.212,120.072,23.15,120.083,23.094,120.122,23.037,120.15,22.975,120.233,22.718,120.273,22.627,120.326,22.542,120.316,22.548,120.388,22.485,120.48,22.442,120.581,22.356,120.608,22.313,120.678,22.16,120.69,22.033,120.743,21.956,120.84,21.925,120.864,22.033,120.878,22.142,120.877,22.262,120.897,22.379,120.947,22.503,121.009,22.62,121.161,22.776,121.296,22.967,121.352,23.067,121.397,23.173,121.477,23.424,121.526,23.668,121.583,23.861,121.613,24.053,121.639,24.13,121.737,24.285,121.828,24.534,121.826,24.641,121.813,24.746,121.82,24.825,121.856,24.895,121.929,24.974,121.905,25.056]},{"b":[108.636,18.218,111.014,20.138],"l":[110.971,19.883,110.938,19.948,110.889,19.992,110.809,20.014,110.745,20.059,110.679,20.137,110.652,20.138,110.598,20.098,110.589,20.072,110.588,19.976,110.418,20.055,110.394,20.059,110.388,20.018,110.392,19.976,110.344,20.039,110.213,20.056,110.172,20.054,110.083,19.993,109.906,19.963,109.651,19.984,109.584,19.97,109.514,19.904,109.418,19.889,109.315,19.904,109.263,19.883,109.219,19.843,109.177,19.768,109.22,19.757,109.277,19.761,109.179,19.674,109.063,19.614,108.903,19.481,108.791,19.418,108.694,19.338,108.666,19.304,108.65,19.265,108.636,18.908,108.638,18.866,108.676,18.75,108.702,18.535,108.922,18.416,109.03,18.368,109.183,18.325,109.341,18.3,109.4,18.281,109.519,18.218,109.59,18.226,109.681,18.247,109.703,18.259,109.76,18.348,109.816,18.397,109.968,18.422,110.02,18.416,110.067,18.448,110.066,18.476,110.049,18.505,110.156,18.57,110.252,18.656,110.291,18.67,110.334,18.673,110.4,18.698,110.451,18.748,110.478,18.813,110.519,18.97,110.566,19.099,110.563,19.135,110.572,19.172,110.603,19.207,110.641,19.291,110.822,19.558,110.913,19.586,111.014,19.655,110.998,19.765,110.971,19.883]},{"b":[79.708,5.949,81.877,9.813],"l":[81.832,7.428,81.797,7.465,81.727,7.625,81.683,7.684,81.676,7.711,81.679,7.742,81.665,7.782,81.436,8.119,81.422,8.148,81.422,8.215,81.373,8.431,81.334,8.472,81.275,8.484,81.227,8.506,81.216,8.549,81.219,8.608,81.198,8.662,81.016,8.933,80.979,8.957,80.935,8.971,80.91,9.025,80.893,9.086,80.711,9.366,80.376,9.642,80.253,9.796,80.181,9.81,80.078,9.807,79.982,9.813,79.967,9.793,79.954,9.742,79.979,9.699,80.046,9.65,80.258,9.611,80.385,9.549,80.428,9.481,80.368,9.48,80.318,9.465,80.256,9.495,80.196,9.538,80.086,9.578,80.111,9.453,80.118,9.327,80.1,9.21,80.065,9.096,79.929,8.899,79.928,8.846,79.944,8.741,79.942,8.692,79.851,8.412,79.832,8.304,79.809,8.05,79.783,8.018,79.75,8.049,79.75,8.294,79.713,8.182,79.708,8.066,79.76,7.796,79.792,7.585,79.859,6.829,79.947,6.585,80.007,6.364,80.095,6.153,80.267,6.01,80.496,5.949,80.724,5.979,80.971,6.088,81.306,6.204,81.38,6.241,81.637,6.425,81.713,6.512,81.768,6.614,81.819,6.756,81.861,6.901,81.877,7.02,81.874,7.288,81.832,7.428]},{"b":[43.257,-25.571,50.483,-12.08],"l":[44.405,-19.922,44.432,-19.674,44.453,-19.551,44.449,-19.429,44.387,-19.303,44.239,-19.075,44.234,-19.033,44.246,-18.863,44.233,-18.741,44.179,-18.619,44.109,-18.504,44.04,-18.288,44.007,-17.933,44.014,-17.804,43.994,-17.69,43.944,-17.581,43.979,-17.392,44.421,-16.703,44.436,-16.621,44.418,-16.411,44.427,-16.289,44.442,-16.244,44.476,-16.217,44.552,-16.204,44.909,-16.175,44.955,-16.153,45.044,-16.095,45.167,-15.983,45.223,-15.95,45.271,-15.962,45.302,-16.01,45.342,-16.037,45.486,-15.986,45.542,-15.984,45.598,-15.993,45.625,-15.946,45.641,-15.883,45.662,-15.839,45.7,-15.814,45.886,-15.8,46.004,-15.782,46.158,-15.738,46.191,-15.747,46.314,-15.905,46.352,-15.918,46.4,-15.925,46.442,-15.896,46.341,-15.813,46.326,-15.767,46.331,-15.714,46.385,-15.6,46.475,-15.513,46.675,-15.382,46.882,-15.23,46.942,-15.219,46.993,-15.243,47.032,-15.423,47.027,-15.452,47.061,-15.456,47.099,-15.434,47.133,-15.362,47.135,-15.302,47.107,-15.244,47.094,-15.195,47.093,-15.15,47.198,-15.044,47.28,-14.943,47.319,-14.822,47.352,-14.766,47.439,-14.703,47.465,-14.713,47.485,-14.764,47.496,-14.818,47.474,-14.872,47.442,-14.925,47.429,-14.996,47.478,-15.009,47.525,-14.992,47.593,-14.864,47.67,-14.743,47.716,-14.68,47.774,-14.637,47.87,-14.646,47.964,-14.673,47.812,-14.545,47.773,-14.37,47.955,-14.067,47.957,-14.004,47.983,-13.985,47.996,-13.96,47.901,-13.858,47.884,-13.808,47.896,-13.731,47.941,-13.662,47.982,-13.615,48.04,-13.596,48.086,-13.623,48.187,-13.707,48.255,-13.719,48.338,-13.639,48.405,-13.538,48.506,-13.469,48.621,-13.426,48.796,-13.267,48.91,-12.936,48.919,-12.839,48.894,-12.722,48.854,-12.61,48.786,-12.471,48.804,-12.44,48.9,-12.458,48.932,-12.439,49.036,-12.316,49.207,-12.08,49.263,-12.08,49.312,-12.124,49.33,-12.189,49.364,-12.236,49.48,-12.348,49.538,-12.432,49.584,-12.537,49.638,-12.637,49.805,-12.88,49.876,-12.973,49.938,-13.072,49.967,-13.27,50.073,-13.578,50.174,-14.04,50.205,-14.514,50.235,-14.732,50.313,-14.937,50.441,-15.149,50.483,-15.386,50.405,-15.629,50.292,-15.858,50.262,-15.902,50.209,-15.96,50.185,-15.958,50.094,-15.899,50.02,-15.802,49.927,-15.574,49.893,-15.458,49.853,-15.439,49.744,-15.45,49.664,-15.522,49.65,-15.567,49.667,-15.696,49.697,-15.811,49.71,-15.929,49.713,-16.077,49.742,-16.121,49.786,-16.159,49.831,-16.256,49.839,-16.487,49.811,-16.603,49.734,-16.703,49.739,-16.758,49.767,-16.815,49.74,-16.849,49.637,-16.893,49.595,-16.931,49.54,-17.033,49.449,-17.241,49.437,-17.347,49.494,-17.67,49.478,-17.899,49.363,-18.336,49.297,-18.544,49.203,-18.792,49.06,-19.12,48.918,-19.53,48.797,-19.953,48.708,-20.207,48.607,-20.458,48.469,-20.9,48.351,-21.349,48.176,-21.843,47.934,-22.394,47.908,-22.466,47.858,-22.747,47.804,-22.992,47.739,-23.233,47.604,-23.633,47.589,-23.756,47.558,-23.875,47.428,-24.125,47.373,-24.218,47.334,-24.318,47.312,-24.443,47.273,-24.564,47.177,-24.787,47.035,-24.979,46.938,-25.049,46.729,-25.15,46.622,-25.17,46.387,-25.173,46.159,-25.23,45.921,-25.341,45.692,-25.468,45.605,-25.529,45.508,-25.563,45.206,-25.571,45.115,-25.543,44.813,-25.334,44.696,-25.3,44.474,-25.271,44.407,-25.253,44.346,-25.226,44.256,-25.117,44.078,-25.025,44.035,-24.996,44.008,-24.932,43.99,-24.863,43.944,-24.787,43.91,-24.641,43.852,-24.538,43.688,-24.358,43.67,-24.3,43.657,-24.109,43.662,-23.979,43.646,-23.742,43.665,-23.63,43.722,-23.53,43.699,-23.421,43.638,-23.307,43.615,-23.188,43.57,-23.08,43.398,-22.886,43.358,-22.791,43.33,-22.692,43.265,-22.384,43.257,-22.276,43.267,-22.049,43.291,-21.933,43.332,-21.851,43.343,-21.79,43.37,-21.738,43.411,-21.696,43.438,-21.647,43.502,-21.356,43.583,-21.292,43.704,-21.255,43.8,-21.179,43.856,-21.077,43.911,-20.866,44.063,-20.656,44.117,-20.546,44.24,-20.38,44.348,-20.146,44.381,-20.035,44.405,-19.922]},{"b":[53.019,38.756,53.11,39.097],"l":[53.055,39.038,53.11,38.803,53.1,38.756,53.046,38.897,53.019,39.053,53.053,39.097,53.092,39.094,53.055,39.038]},{"b":[49.995,44.826,50.184,45.082],"l":[50.095,44.831,49.995,44.937,50.023,45.045,50.059,45.067,50.11,45.082,50.117,45.058,50.045,45.01,50.039,44.949,50.098,44.882,50.184,44.855,50.149,44.826,50.095,44.831]},{"b":[50.256,44.959,50.35,45.083],"l":[50.295,45.076,50.35,45.083,50.331,44.998,50.312,44.972,50.277,44.959,50.256,45.022,50.295,45.076]},{"b":[-164.282,-79.845,-158.997,-78.719],"l":[-160.25,-79.271,-160.764,-79.132,-161.283,-79.007,-161.643,-78.901,-162.161,-78.793,-162.39,-78.76,-162.622,-78.742,-162.873,-78.725,-163.124,-78.719,-163.256,-78.722,-163.345,-78.78,-163.66,-78.856,-163.815,-78.929,-164.126,-78.995,-164.2,-79.051,-164.245,-79.138,-164.282,-79.246,-164.226,-79.321,-163.971,-79.389,-163.712,-79.442,-163.317,-79.505,-161.866,-79.704,-160.807,-79.812,-160.302,-79.845,-159.053,-79.807,-159.01,-79.78,-158.997,-79.735,-159.051,-79.695,-159.119,-79.675,-159.19,-79.637,-159.256,-79.591,-159.366,-79.545,-159.419,-79.508,-159.684,-79.402,-159.964,-79.324,-160.25,-79.271]},{"b":[-71.784,-79.683,-66.728,-78.316],"l":[-66.728,-78.384,-66.84,-78.35,-67.038,-78.316,-67.479,-78.362,-69.398,-78.686,-69.748,-78.769,-69.972,-78.809,-70.544,-78.884,-70.626,-78.902,-71.254,-79.06,-71.454,-79.129,-71.667,-79.246,-71.784,-79.444,-71.778,-79.501,-71.735,-79.539,-71.687,-79.568,-71.526,-79.624,-71.414,-79.64,-70.984,-79.674,-70.553,-79.683,-70.334,-79.68,-70.116,-79.666,-69.732,-79.618,-69.634,-79.518,-69.686,-79.443,-69.394,-79.28,-69.251,-79.21,-68.638,-79.013,-68.157,-78.871,-67.481,-78.682,-67.166,-78.57,-67.046,-78.514,-66.787,-78.422,-66.728,-78.384]},{"b":[-54.372,-80.87,-42.945,-77.785],"l":[-43.947,-78.598,-43.852,-78.53,-43.788,-78.433,-43.777,-78.385,-43.785,-78.336,-43.809,-78.287,-43.854,-78.258,-44.094,-78.167,-44.34,-78.093,-44.594,-78.035,-44.852,-77.988,-45.53,-77.881,-45.993,-77.827,-46.258,-77.805,-46.826,-77.785,-47.03,-77.791,-47.463,-77.819,-47.692,-77.84,-49.081,-78.047,-49.144,-78.094,-49.354,-78.222,-49.94,-78.462,-50.142,-78.557,-50.22,-78.605,-50.294,-78.696,-50.377,-78.78,-50.335,-78.818,-50.242,-78.833,-50.298,-78.882,-50.38,-78.923,-50.502,-78.95,-50.514,-78.98,-50.502,-79.022,-50.52,-79.104,-50.573,-79.172,-50.649,-79.233,-50.733,-79.283,-50.464,-79.313,-50.42,-79.321,-50.379,-79.338,-50.331,-79.381,-50.295,-79.43,-50.339,-79.479,-50.402,-79.512,-50.664,-79.627,-51.184,-79.82,-51.711,-79.99,-52.297,-80.141,-52.338,-80.126,-52.357,-80.078,-52.461,-80.067,-52.567,-80.099,-52.807,-80.156,-53.053,-80.175,-53.176,-80.161,-53.346,-80.114,-53.394,-80.109,-53.482,-80.189,-53.676,-80.284,-54.045,-80.487,-54.13,-80.517,-54.347,-80.569,-54.372,-80.624,-54.351,-80.76,-54.241,-80.847,-54.202,-80.864,-54.162,-80.87,-49.773,-80.784,-49.701,-80.753,-49.63,-80.712,-49.41,-80.667,-49.187,-80.643,-43.528,-80.191,-43.489,-80.178,-43.454,-80.155,-43.459,-80.123,-43.49,-80.096,-43.758,-80.021,-43.742,-80.003,-43.704,-79.99,-43.6,-79.974,-43.496,-79.969,-43.267,-79.979,-43.066,-79.891,-42.99,-79.801,-42.945,-79.579,-42.965,-79.477,-43.119,-79.35,-43.211,-79.3,-43.267,-79.163,-43.364,-79.085,-43.451,-78.99,-43.544,-78.902,-43.627,-78.846,-43.722,-78.818,-44.041,-78.807,-44.566,-78.804,-45.092,-78.814,-45.223,-78.811,-45.352,-78.791,-45.295,-78.74,-45.213,-78.687,-45.068,-78.661,-43.947,-78.598]},{"b":[-66.771,-80.948,-59.322,-79.741],"l":[-59.498,-80.115,-59.788,-80.101,-59.786,-80.001,-59.752,-79.938,-59.706,-79.875,-59.873,-79.777,-60.579,-79.741,-61.026,-79.809,-61.115,-79.862,-61.343,-79.887,-61.346,-79.951,-61.246,-79.978,-61.302,-79.996,-61.684,-80.02,-61.717,-80.069,-61.694,-80.134,-61.597,-80.206,-61.485,-80.244,-61.194,-80.257,-61.313,-80.307,-61.633,-80.344,-62.232,-80.369,-62.519,-80.373,-65.98,-80.384,-66.115,-80.361,-66.168,-80.346,-66.217,-80.258,-66.296,-80.235,-66.377,-80.222,-66.483,-80.224,-66.588,-80.239,-66.681,-80.26,-66.771,-80.294,-66.734,-80.319,-66.591,-80.358,-66.184,-80.442,-65.203,-80.607,-64.268,-80.749,-64.22,-80.734,-64.126,-80.668,-64.065,-80.65,-63.715,-80.617,-63.144,-80.595,-63.068,-80.627,-62.986,-80.735,-62.94,-80.766,-62.671,-80.834,-62.023,-80.889,-60.583,-80.948,-60.268,-80.881,-60.125,-80.841,-59.926,-80.774,-59.825,-80.733,-59.771,-80.657,-59.774,-80.558,-59.734,-80.344,-59.683,-80.315,-59.612,-80.255,-59.53,-80.208,-59.427,-80.198,-59.322,-80.196,-59.408,-80.151,-59.498,-80.115]},{"b":[-75.383,-72.67,-68.228,-68.789],"l":[-70.543,-72.664,-70.063,-72.626,-69.209,-72.534,-69.149,-72.427,-68.64,-72.21,-68.543,-72.158,-68.461,-72.085,-68.394,-71.975,-68.241,-71.822,-68.228,-71.725,-68.252,-71.313,-68.278,-71.097,-68.314,-70.912,-68.336,-70.856,-68.451,-70.818,-68.459,-70.683,-68.554,-70.581,-68.731,-70.408,-69.091,-70.09,-69.234,-69.909,-69.353,-69.666,-69.417,-69.583,-69.708,-69.321,-69.855,-69.277,-69.913,-69.267,-70.079,-69.311,-70.051,-69.189,-70.053,-69.14,-70.105,-68.959,-70.154,-68.923,-70.312,-68.832,-70.417,-68.789,-71.392,-68.874,-71.869,-68.941,-71.99,-68.971,-72.058,-69.001,-72.109,-69.06,-72.138,-69.115,-72.135,-69.177,-72.114,-69.225,-72.081,-69.267,-71.963,-69.329,-71.834,-69.367,-71.743,-69.423,-71.718,-69.524,-71.767,-69.649,-71.852,-69.807,-71.868,-69.847,-71.879,-69.909,-71.854,-69.969,-71.81,-70.005,-71.729,-70.054,-71.696,-70.068,-71.121,-70.196,-71.024,-70.201,-70.926,-70.192,-70.72,-70.139,-70.328,-70.16,-70.234,-70.18,-70.118,-70.234,-69.883,-70.305,-69.618,-70.398,-69.66,-70.412,-69.702,-70.415,-69.975,-70.36,-70.09,-70.351,-70.328,-70.361,-70.562,-70.404,-71.061,-70.537,-71.19,-70.66,-71.173,-70.713,-71.049,-70.762,-70.917,-70.786,-70.661,-70.818,-70.299,-70.836,-70.196,-70.851,-70.094,-70.883,-69.993,-70.897,-69.933,-70.88,-69.876,-70.876,-69.83,-70.914,-69.823,-70.973,-69.823,-71.034,-69.835,-71.093,-69.87,-71.126,-69.916,-71.134,-70.268,-70.965,-70.323,-70.951,-70.381,-70.946,-70.741,-70.993,-71.194,-70.985,-71.308,-71.011,-71.504,-71.112,-71.719,-71.145,-72.356,-71.075,-72.71,-71.073,-73.06,-71.127,-72.995,-71.187,-72.905,-71.223,-72.43,-71.275,-72.212,-71.335,-72.622,-71.388,-72.821,-71.384,-73.02,-71.369,-73.397,-71.321,-73.474,-71.325,-73.604,-71.351,-73.617,-71.397,-73.592,-71.448,-73.38,-71.528,-73.427,-71.559,-73.479,-71.579,-73.545,-71.573,-73.724,-71.517,-73.937,-71.438,-74.041,-71.41,-74.187,-71.383,-74.236,-71.388,-74.308,-71.4,-74.375,-71.415,-74.425,-71.457,-74.42,-71.507,-74.38,-71.579,-74.373,-71.618,-74.392,-71.638,-74.419,-71.643,-74.487,-71.642,-74.636,-71.617,-74.863,-71.543,-75.1,-71.555,-75.293,-71.615,-75.335,-71.645,-75.353,-71.68,-75.325,-71.726,-75.331,-71.752,-75.373,-71.78,-75.383,-71.828,-75.353,-71.878,-75.259,-71.914,-75.13,-71.964,-75.024,-71.988,-74.908,-72.033,-74.786,-72.064,-74.663,-72.07,-74.429,-72.056,-74.322,-72.073,-74.209,-72.142,-74.153,-72.159,-73.996,-72.17,-73.899,-72.152,-73.537,-72.022,-73.572,-71.981,-73.691,-71.929,-73.83,-71.87,-73.776,-71.849,-73.633,-71.835,-73.41,-71.853,-73.167,-71.905,-72.972,-71.924,-72.928,-71.922,-72.412,-71.662,-72.337,-71.632,-72.259,-71.641,-72.046,-71.74,-71.816,-71.822,-71.574,-71.851,-71.465,-71.838,-71.355,-71.836,-70.821,-71.907,-70.845,-71.946,-70.891,-71.987,-71.034,-72.035,-71.107,-72.047,-71.898,-72.121,-71.892,-72.153,-71.661,-72.25,-71.413,-72.284,-71.178,-72.264,-70.945,-72.229,-70.641,-72.17,-70.533,-72.163,-70.424,-72.168,-70.314,-72.191,-70.206,-72.228,-70.428,-72.323,-70.671,-72.356,-70.873,-72.366,-71.605,-72.359,-72.135,-72.331,-72.376,-72.296,-72.618,-72.275,-72.737,-72.281,-72.855,-72.304,-73.086,-72.408,-73.057,-72.448,-73.007,-72.484,-72.888,-72.547,-72.78,-72.581,-72.67,-72.596,-72.531,-72.59,-72.48,-72.617,-72.433,-72.658,-72.367,-72.67,-71.846,-72.639,-71.159,-72.627,-70.923,-72.613,-70.731,-72.623,-70.543,-72.664]},{"b":[-71.928,-55.032,-65.179,-52.486],"l":[-65.747,-54.653,-65.993,-54.599,-66.236,-54.533,-66.462,-54.441,-66.67,-54.314,-66.865,-54.223,-67.069,-54.148,-67.294,-54.05,-67.503,-53.922,-67.678,-53.787,-67.861,-53.662,-67.94,-53.619,-68.008,-53.564,-68.144,-53.319,-68.161,-53.306,-68.393,-53.295,-68.489,-53.261,-68.521,-53.222,-68.521,-53.177,-68.479,-53.114,-68.431,-53.055,-68.333,-53.02,-68.24,-53.082,-68.278,-52.984,-68.339,-52.9,-68.571,-52.695,-68.63,-52.653,-68.659,-52.632,-68.758,-52.582,-68.79,-52.577,-69.08,-52.674,-69.167,-52.668,-69.414,-52.486,-69.498,-52.491,-69.572,-52.549,-69.663,-52.646,-69.764,-52.731,-69.883,-52.799,-69.935,-52.821,-69.994,-52.821,-70.088,-52.769,-70.19,-52.724,-70.335,-52.734,-70.38,-52.752,-70.297,-52.817,-70.259,-52.857,-70.163,-52.899,-70.14,-52.919,-70.131,-52.943,-70.161,-52.97,-70.196,-52.99,-70.256,-53.004,-70.32,-53.001,-70.391,-53.026,-70.443,-53.086,-70.46,-53.143,-70.46,-53.206,-70.416,-53.305,-70.329,-53.378,-70.213,-53.414,-70.09,-53.418,-69.874,-53.35,-69.756,-53.337,-69.637,-53.334,-69.513,-53.342,-69.394,-53.373,-69.356,-53.416,-69.352,-53.48,-69.39,-53.499,-69.69,-53.601,-69.95,-53.672,-70.091,-53.722,-70.149,-53.761,-70.151,-53.888,-70.086,-54.011,-69.988,-54.109,-69.196,-54.354,-69.044,-54.407,-69.045,-54.428,-69.077,-54.445,-69.128,-54.458,-69.169,-54.483,-69.253,-54.557,-69.312,-54.571,-69.322,-54.543,-69.325,-54.488,-69.365,-54.438,-69.419,-54.407,-69.622,-54.364,-69.742,-54.306,-69.809,-54.321,-69.867,-54.367,-69.99,-54.381,-70.169,-54.379,-70.243,-54.348,-70.246,-54.277,-70.38,-54.181,-70.535,-54.136,-70.63,-54.006,-70.461,-54.006,-70.38,-53.987,-70.443,-53.893,-70.531,-53.627,-70.619,-53.655,-70.696,-53.727,-70.644,-53.823,-70.868,-53.884,-70.857,-53.996,-70.863,-54.11,-70.76,-54.241,-70.636,-54.262,-70.54,-54.303,-70.468,-54.373,-70.298,-54.486,-70.311,-54.529,-70.418,-54.502,-70.573,-54.504,-70.701,-54.485,-70.688,-54.415,-70.699,-54.349,-70.797,-54.327,-70.898,-54.338,-70.928,-54.36,-70.946,-54.398,-70.966,-54.42,-71.08,-54.444,-71.159,-54.451,-71.355,-54.395,-71.393,-54.4,-71.5,-54.445,-71.573,-54.495,-71.606,-54.497,-71.716,-54.444,-71.8,-54.434,-71.823,-54.474,-71.907,-54.495,-71.928,-54.529,-71.902,-54.602,-71.832,-54.626,-71.441,-54.62,-71.229,-54.694,-70.925,-54.714,-70.735,-54.751,-70.497,-54.81,-70.282,-54.752,-70.259,-54.756,-70.238,-54.778,-70.138,-54.819,-70.031,-54.816,-69.899,-54.782,-69.772,-54.739,-69.723,-54.712,-69.588,-54.813,-69.486,-54.859,-69.082,-54.91,-68.844,-54.877,-68.804,-54.854,-68.653,-54.854,-68.619,-54.834,-68.491,-54.836,-68.332,-54.816,-68.22,-54.818,-68.007,-54.848,-67.793,-54.869,-67.127,-54.904,-66.93,-54.925,-66.628,-55.013,-66.511,-55.032,-66.399,-55.009,-66.287,-54.978,-66.172,-54.975,-66.061,-54.957,-65.954,-54.919,-65.842,-54.91,-65.723,-54.926,-65.603,-54.928,-65.471,-54.915,-65.346,-54.878,-65.252,-54.789,-65.179,-54.678,-65.252,-54.638,-65.369,-54.632,-65.747,-54.653]},{"b":[-84.887,19.855,-74.137,23.19],"l":[-74.513,20.385,-74.662,20.522,-74.732,20.573,-74.883,20.651,-74.96,20.673,-75.213,20.714,-75.338,20.702,-75.525,20.717,-75.643,20.733,-75.725,20.715,-75.753,20.736,-75.76,20.776,-75.74,20.812,-75.597,20.838,-75.663,20.898,-75.639,20.947,-75.596,20.995,-75.634,21.061,-75.723,21.111,-75.899,21.114,-76.074,21.133,-76.259,21.227,-76.455,21.274,-76.552,21.272,-76.647,21.285,-76.689,21.34,-76.726,21.359,-76.765,21.362,-76.867,21.33,-76.86,21.365,-76.836,21.4,-76.928,21.459,-77.099,21.589,-77.141,21.539,-77.208,21.479,-77.253,21.483,-77.27,21.538,-77.366,21.613,-77.245,21.594,-77.181,21.598,-77.144,21.644,-77.222,21.672,-77.3,21.712,-77.342,21.755,-77.497,21.872,-77.583,21.889,-77.573,21.868,-77.507,21.811,-77.497,21.788,-77.545,21.775,-77.637,21.797,-77.865,21.901,-77.971,21.972,-78.143,22.109,-78.686,22.367,-78.719,22.358,-78.776,22.367,-78.835,22.391,-78.902,22.396,-79.183,22.388,-79.276,22.408,-79.358,22.449,-79.456,22.51,-79.549,22.578,-79.677,22.743,-79.851,22.827,-79.82,22.887,-79.924,22.869,-79.96,22.877,-80.075,22.942,-80.168,22.949,-80.266,22.935,-80.365,22.943,-80.459,22.975,-80.55,23.017,-80.613,23.084,-80.65,23.103,-81.008,23.09,-81.145,23.055,-81.179,23.06,-81.272,23.129,-81.262,23.157,-81.364,23.13,-81.575,23.117,-81.837,23.163,-82.101,23.19,-82.351,23.154,-82.588,23.065,-82.666,23.044,-83.177,22.983,-83.258,22.968,-84.045,22.666,-84.122,22.619,-84.281,22.474,-84.361,22.379,-84.383,22.256,-84.326,22.074,-84.373,22.036,-84.433,22.031,-84.494,22.042,-84.533,22.031,-84.877,21.894,-84.887,21.857,-84.838,21.828,-84.786,21.842,-84.683,21.899,-84.627,21.92,-84.56,21.933,-84.501,21.93,-84.491,21.854,-84.503,21.776,-84.449,21.792,-84.241,21.898,-84.138,21.929,-84.031,21.943,-83.998,21.98,-83.963,22.092,-83.933,22.15,-83.901,22.17,-83.687,22.18,-83.643,22.189,-83.602,22.209,-83.544,22.209,-83.486,22.187,-83.38,22.223,-83.292,22.303,-83.189,22.355,-83.144,22.386,-83.107,22.43,-83.009,22.514,-82.861,22.595,-82.786,22.658,-82.738,22.689,-81.903,22.679,-81.839,22.672,-81.79,22.657,-81.746,22.633,-81.703,22.592,-81.683,22.535,-81.71,22.497,-81.757,22.467,-81.973,22.422,-82.078,22.388,-81.973,22.291,-81.849,22.214,-81.816,22.2,-81.441,22.184,-81.355,22.104,-81.284,22.109,-81.222,22.143,-81.2,22.203,-81.185,22.268,-81.141,22.207,-81.117,22.134,-81.083,22.098,-81.036,22.074,-80.962,22.053,-80.499,22.064,-80.485,22.087,-80.485,22.123,-80.393,22.034,-80.311,21.933,-80.231,21.872,-80.138,21.829,-79.91,21.743,-79.357,21.585,-79.274,21.563,-79.189,21.553,-78.823,21.619,-78.728,21.593,-78.636,21.516,-78.577,21.414,-78.537,21.297,-78.491,21.054,-78.454,21.011,-78.406,20.974,-78.314,20.927,-78.116,20.762,-77.997,20.715,-77.857,20.714,-77.593,20.69,-77.467,20.69,-77.348,20.672,-77.23,20.644,-77.205,20.611,-77.189,20.56,-77.108,20.492,-77.093,20.453,-77.104,20.408,-77.149,20.347,-77.213,20.3,-77.554,20.082,-77.715,19.855,-77.463,19.861,-77.212,19.894,-76.999,19.893,-76.89,19.921,-76.78,19.94,-76.516,19.957,-76.253,19.987,-76.158,19.99,-75.765,19.96,-75.657,19.932,-75.552,19.891,-75.29,19.893,-75.219,19.924,-75.177,19.959,-75.152,20.008,-75.122,19.954,-75.124,19.925,-75.116,19.901,-75.003,19.929,-74.955,19.958,-74.85,20.002,-74.635,20.058,-74.412,20.075,-74.253,20.08,-74.217,20.117,-74.154,20.169,-74.137,20.232,-74.167,20.292,-74.198,20.311,-74.234,20.326,-74.273,20.317,-74.384,20.33,-74.513,20.385]},{"b":[17.917,79.194,27.199,80.478],"l":[26.861,80.16,26.437,80.175,25.836,80.175,25.751,80.188,25.667,80.21,25.471,80.233,24.907,80.277,24.786,80.301,24.736,80.301,24.614,80.286,24.547,80.295,24.403,80.355,24.298,80.36,24.28,80.329,24.234,80.303,24.143,80.295,23.953,80.305,23.773,80.244,23.688,80.207,23.353,80.179,23.115,80.187,23.225,80.318,23.25,80.381,23.315,80.425,23.251,80.447,23.008,80.474,22.897,80.469,22.793,80.433,22.672,80.413,22.549,80.416,22.451,80.402,22.418,80.366,22.446,80.308,22.443,80.19,22.376,80.09,22.29,80.049,22.19,80.06,21.898,80.132,21.781,80.139,21.697,80.159,21.655,80.218,21.549,80.243,20.998,80.239,20.898,80.25,20.693,80.299,20.476,80.372,20.359,80.401,20.104,80.43,19.851,80.471,19.733,80.478,19.614,80.463,19.691,80.402,19.777,80.353,19.81,80.327,19.802,80.295,19.751,80.227,19.568,80.25,19.327,80.323,19.264,80.336,19.178,80.332,19.157,80.302,19.191,80.263,19.355,80.185,19.537,80.163,19.343,80.116,19.143,80.139,18.962,80.175,18.779,80.194,18.089,80.171,17.917,80.143,18.129,80.093,18.344,80.06,18.856,80.037,18.726,79.996,18.595,79.967,18.255,79.929,18.285,79.887,18.325,79.86,18.428,79.825,18.725,79.761,18.942,79.736,19.4,79.727,19.638,79.729,19.899,79.744,20.123,79.779,20.461,79.775,20.784,79.749,20.687,79.707,20.565,79.691,20.493,79.633,20.187,79.632,20.015,79.64,19.821,79.634,19.747,79.618,19.675,79.591,19.9,79.534,20.128,79.49,20.4,79.463,20.761,79.442,20.806,79.41,20.861,79.398,21.911,79.381,22.866,79.412,22.696,79.329,22.789,79.264,22.904,79.231,23.759,79.206,23.948,79.194,24.133,79.215,24.257,79.263,24.383,79.302,24.751,79.365,24.843,79.367,25.145,79.339,25.239,79.345,25.641,79.403,25.726,79.44,25.902,79.561,26.006,79.617,26.221,79.677,27.08,79.865,27.199,79.907,27.148,80.059,27.017,80.125,26.861,80.16]},{"b":[10.682,76.579,21.389,80.053],"l":[20.725,78.672,21.096,78.676,21.244,78.699,21.389,78.74,21.353,78.772,21.312,78.796,21.09,78.853,20.72,78.907,20.501,78.981,20.767,79.059,20.611,79.107,20.458,79.129,20.163,79.146,20.114,79.125,20.114,79.077,19.894,79.056,19.751,79.147,19.49,79.176,19.089,79.157,18.979,79.179,18.88,79.234,18.772,79.26,18.678,79.262,18.72,79.281,18.807,79.303,18.823,79.337,18.832,79.385,18.815,79.427,18.785,79.461,18.748,79.488,18.581,79.572,18.397,79.605,18.333,79.611,18.272,79.601,17.861,79.437,17.669,79.386,17.715,79.431,17.734,79.481,17.688,79.533,17.733,79.57,17.86,79.635,17.956,79.704,17.835,79.8,17.685,79.857,17.578,79.885,17.219,79.941,16.966,79.959,16.926,79.943,16.889,79.915,16.838,79.905,16.787,79.907,16.524,80.021,16.387,80.053,16.246,80.049,16.094,80.007,16.057,79.954,16.1,79.884,15.956,79.835,15.845,79.734,15.826,79.709,15.816,79.682,15.841,79.587,15.875,79.519,16.028,79.342,16.254,79.112,16.344,78.976,16.295,78.981,15.858,79.16,15.764,79.174,15.66,79.235,15.444,79.407,15.251,79.545,15.052,79.675,14.832,79.766,14.594,79.799,14.38,79.726,14.178,79.619,14.04,79.586,14.02,79.539,14.011,79.482,14.026,79.429,14.056,79.383,14.03,79.344,13.957,79.34,13.834,79.376,13.716,79.429,13.601,79.457,13.432,79.471,13.384,79.481,13.334,79.575,13.215,79.588,12.555,79.569,13.039,79.685,13.778,79.715,13.907,79.752,13.921,79.762,13.926,79.793,13.914,79.817,13.693,79.861,13.108,79.832,12.754,79.776,12.602,79.773,12.28,79.816,12.219,79.798,12.245,79.75,12.288,79.713,12.205,79.719,12.102,79.738,11.702,79.821,11.344,79.799,11.251,79.785,11.185,79.72,11.15,79.717,11.05,79.76,10.866,79.797,10.804,79.799,10.746,79.789,10.682,79.758,10.686,79.734,10.755,79.69,10.811,79.641,10.737,79.582,10.725,79.556,10.738,79.52,10.834,79.463,10.888,79.415,10.926,79.35,10.975,79.305,11.107,79.233,11.208,79.13,11.339,79.109,11.521,79.151,11.616,79.205,11.58,79.283,11.679,79.291,11.978,79.293,12.084,79.268,12.016,79.213,11.893,79.152,11.902,79.112,11.926,79.077,11.982,79.025,12.046,78.983,12.087,78.975,12.253,78.975,12.375,78.966,12.403,78.953,12.323,78.914,12.275,78.904,11.548,78.983,11.456,78.973,11.365,78.95,11.611,78.883,11.861,78.832,11.755,78.812,11.746,78.766,11.774,78.716,11.866,78.674,11.962,78.642,12.138,78.606,12.258,78.595,12.435,78.483,12.665,78.385,12.822,78.351,12.87,78.331,12.913,78.301,13.15,78.238,13.655,78.245,13.908,78.267,14.11,78.271,14.238,78.31,14.363,78.36,14.5,78.392,14.638,78.415,14.546,78.462,14.432,78.492,14.468,78.541,14.515,78.581,14.505,78.631,14.467,78.675,14.578,78.705,14.689,78.721,14.744,78.721,14.792,78.706,14.839,78.666,14.892,78.639,15.016,78.63,15.137,78.664,15.225,78.732,15.323,78.781,15.384,78.771,15.392,78.721,15.348,78.663,15.265,78.608,15.254,78.589,15.279,78.554,15.36,78.488,15.417,78.473,15.681,78.471,15.944,78.493,16.158,78.538,16.446,78.639,16.53,78.656,16.783,78.664,16.697,78.613,16.449,78.504,16.535,78.449,16.727,78.407,16.811,78.397,16.992,78.4,17.172,78.417,17.003,78.369,16.777,78.35,16.15,78.353,15.875,78.339,15.784,78.327,15.657,78.299,15.659,78.265,15.698,78.228,15.519,78.233,15.341,78.221,14.995,78.151,14.248,78.071,14.307,78.005,14.048,78.067,13.937,78.086,13.824,78.085,13.77,78.075,13.718,78.058,13.681,78.028,13.714,77.919,13.75,77.883,13.791,77.854,13.963,77.796,14.09,77.771,14.604,77.766,14.847,77.779,15.097,77.809,15.345,77.857,15.585,77.869,15.826,77.847,16.06,77.847,16.54,77.88,16.853,77.912,16.914,77.898,16.969,77.842,17.033,77.798,16.619,77.799,16.206,77.782,14.921,77.689,14.695,77.525,14.596,77.538,14.488,77.571,14.378,77.58,14.071,77.564,14.026,77.545,13.996,77.508,14.004,77.445,14.05,77.403,14.145,77.336,14.248,77.282,14.366,77.234,14.487,77.199,14.738,77.162,15.124,77.085,15.547,76.886,16.004,76.761,16.124,76.739,16.238,76.702,16.346,76.645,16.462,76.609,16.7,76.579,16.935,76.606,17.063,76.659,17.036,76.72,16.98,76.779,16.977,76.812,17.142,76.895,17.249,76.969,17.188,77.011,17.153,77.049,17.349,77.157,17.442,77.225,17.623,77.399,17.847,77.497,18.137,77.507,18.228,77.523,18.299,77.579,18.362,77.682,18.404,77.794,18.439,77.942,18.431,77.991,18.439,78.025,18.575,78.048,18.712,78.04,18.822,78.042,18.995,78.081,19.009,78.132,18.958,78.182,18.984,78.234,19.056,78.319,19.15,78.379,19.381,78.48,19.619,78.562,19.655,78.598,19.677,78.61,19.769,78.623,20.387,78.643,20.725,78.672]},{"b":[8.181,38.91,9.805,41.257],"l":[9.682,40.818,9.632,40.882,9.574,40.915,9.554,40.932,9.59,40.992,9.621,41.005,9.615,41.017,9.576,41.031,9.539,41.054,9.5,41.106,9.455,41.15,9.351,41.196,9.283,41.202,9.228,41.257,9.182,41.242,9.163,41.185,9.107,41.143,8.998,41.11,8.821,40.95,8.699,40.895,8.572,40.85,8.468,40.834,8.363,40.846,8.31,40.858,8.245,40.907,8.224,40.913,8.204,40.871,8.181,40.771,8.19,40.652,8.23,40.606,8.296,40.559,8.353,40.501,8.385,40.443,8.409,40.352,8.471,40.293,8.471,40.131,8.455,40.078,8.409,40.038,8.399,39.978,8.408,39.917,8.496,39.897,8.548,39.839,8.539,39.77,8.541,39.732,8.511,39.722,8.471,39.748,8.451,39.722,8.461,39.648,8.447,39.563,8.419,39.523,8.399,39.482,8.411,39.292,8.418,39.206,8.486,39.11,8.553,39.03,8.595,38.964,8.649,38.927,8.719,38.927,8.801,38.91,8.881,38.913,8.967,38.964,9.023,39.043,9.056,39.239,9.102,39.211,9.149,39.197,9.207,39.214,9.264,39.217,9.388,39.168,9.486,39.14,9.563,39.166,9.584,39.254,9.617,39.354,9.686,39.924,9.707,40.017,9.701,40.092,9.659,40.159,9.643,40.268,9.754,40.4,9.783,40.442,9.805,40.5,9.794,40.556,9.682,40.818]},{"b":[8.566,41.385,9.556,43.021],"l":[8.642,42.118,8.701,42.096,8.703,42.043,8.653,41.996,8.615,41.959,8.622,41.931,8.674,41.922,8.74,41.925,8.759,41.87,8.719,41.804,8.718,41.761,8.771,41.737,8.887,41.701,8.879,41.669,8.83,41.628,8.808,41.588,8.842,41.559,8.895,41.516,9.003,41.477,9.186,41.385,9.253,41.46,9.331,41.627,9.374,41.679,9.395,41.731,9.401,41.926,9.428,41.972,9.551,42.13,9.556,42.161,9.526,42.553,9.509,42.586,9.473,42.616,9.454,42.659,9.48,42.805,9.479,42.86,9.461,42.945,9.463,42.981,9.415,43.021,9.363,43.017,9.331,42.944,9.323,42.814,9.338,42.767,9.313,42.713,9.288,42.695,9.254,42.712,9.198,42.729,9.138,42.733,9.088,42.705,9.044,42.662,8.995,42.645,8.815,42.608,8.713,42.55,8.64,42.427,8.588,42.385,8.566,42.358,8.592,42.345,8.626,42.343,8.675,42.284,8.608,42.258,8.566,42.219,8.588,42.161,8.642,42.118]},{"b":[12.436,36.688,15.635,38.296],"l":[15.341,38.217,15.28,38.23,15.224,38.211,15.176,38.168,15.119,38.153,14.982,38.168,14.846,38.172,14.79,38.167,14.737,38.151,14.637,38.085,14.506,38.046,14.416,38.043,14.288,38.017,14.05,38.041,13.937,38.024,13.789,37.981,13.735,37.984,13.682,38.001,13.491,38.103,13.433,38.11,13.383,38.127,13.352,38.181,13.291,38.191,13.16,38.19,13.057,38.131,13.049,38.084,12.955,38.041,12.903,38.035,12.851,38.064,12.734,38.183,12.702,38.142,12.664,38.108,12.602,38.085,12.548,38.053,12.487,37.939,12.436,37.82,12.454,37.774,12.527,37.67,12.64,37.594,12.699,37.572,12.757,37.567,12.871,37.575,12.924,37.571,13.04,37.507,13.17,37.479,13.221,37.452,13.265,37.41,13.361,37.349,13.587,37.254,13.801,37.136,13.905,37.101,14.024,37.107,14.143,37.104,14.259,37.046,14.367,36.973,14.502,36.799,14.555,36.777,14.614,36.767,14.776,36.71,14.89,36.724,15.002,36.694,15.113,36.688,15.116,36.736,15.104,36.785,15.116,36.839,15.142,36.892,15.185,36.935,15.295,37.013,15.296,37.055,15.289,37.097,15.236,37.139,15.174,37.209,15.23,37.244,15.194,37.283,15.146,37.308,15.117,37.335,15.106,37.375,15.1,37.459,15.131,37.532,15.165,37.59,15.19,37.651,15.207,37.721,15.234,37.785,15.476,38.063,15.509,38.107,15.577,38.22,15.635,38.268,15.568,38.296,15.499,38.291,15.341,38.217]},{"b":[-54.919,69.257,-51.9,70.317],"l":[-52.113,69.489,-51.988,69.55,-51.9,69.605,-51.908,69.637,-51.982,69.664,-52.007,69.686,-51.985,69.704,-51.977,69.722,-51.983,69.743,-52.011,69.782,-52.045,69.807,-52.398,69.863,-52.731,69.945,-53.103,70.141,-53.297,70.205,-53.375,70.221,-54.007,70.296,-54.372,70.317,-54.706,70.256,-54.786,70.218,-54.816,70.189,-54.83,70.161,-54.831,70.133,-54.809,70.085,-54.774,70.053,-54.652,70.011,-54.323,69.942,-54.363,69.924,-54.665,69.966,-54.788,69.95,-54.841,69.902,-54.919,69.714,-54.866,69.665,-54.804,69.631,-54.734,69.611,-54.497,69.577,-54.133,69.565,-53.994,69.553,-53.921,69.534,-53.825,69.54,-53.783,69.506,-53.722,69.491,-53.658,69.465,-53.89,69.437,-54.047,69.437,-54.158,69.428,-54.183,69.404,-54.121,69.364,-54.051,69.337,-53.902,69.302,-53.793,69.264,-53.754,69.26,-53.578,69.257,-53.003,69.343,-52.77,69.364,-52.113,69.489]},{"b":[-128.35,48.323,-123.284,50.861],"l":[-123.415,48.698,-123.443,48.69,-123.458,48.674,-123.473,48.602,-123.497,48.582,-123.627,48.824,-123.752,48.951,-123.82,49.083,-123.854,49.119,-123.937,49.171,-123.996,49.224,-124.186,49.301,-124.496,49.38,-124.643,49.429,-124.831,49.53,-124.931,49.643,-124.932,49.67,-124.905,49.685,-124.935,49.732,-125.066,49.848,-125.233,50.012,-125.314,50.107,-125.42,50.255,-125.482,50.317,-125.534,50.342,-125.615,50.359,-125.839,50.381,-126.204,50.454,-126.701,50.516,-127.197,50.64,-127.713,50.821,-127.918,50.861,-128.101,50.858,-128.242,50.828,-128.301,50.794,-128.346,50.744,-128.35,50.697,-128.267,50.609,-128.136,50.521,-128.058,50.498,-127.964,50.493,-127.865,50.499,-127.731,50.536,-127.75,50.578,-127.751,50.607,-127.526,50.597,-127.466,50.583,-127.529,50.537,-127.524,50.496,-127.489,50.427,-127.487,50.405,-127.578,50.465,-127.641,50.479,-127.832,50.471,-127.874,50.464,-127.906,50.445,-127.963,50.346,-127.947,50.326,-127.851,50.314,-127.839,50.293,-127.828,50.211,-127.873,50.15,-127.864,50.128,-127.816,50.118,-127.77,50.121,-127.675,50.163,-127.467,50.163,-127.43,50.131,-127.398,50.085,-127.349,50.052,-127.29,50.071,-127.272,50.096,-127.268,50.129,-127.25,50.138,-127.216,50.121,-127.192,50.1,-127.18,50.073,-127.179,50.05,-127.208,49.992,-127.196,49.949,-127.166,49.91,-127.114,49.88,-127.049,49.872,-126.977,49.883,-126.948,49.903,-126.926,49.935,-126.903,49.944,-126.849,49.923,-126.745,49.905,-126.683,49.876,-126.593,49.764,-126.558,49.733,-126.525,49.72,-126.463,49.72,-126.403,49.678,-126.348,49.661,-126.134,49.672,-126.158,49.65,-126.443,49.619,-126.542,49.59,-126.557,49.579,-126.564,49.543,-126.549,49.419,-126.519,49.397,-126.5,49.4,-126.445,49.451,-126.419,49.449,-126.304,49.382,-126.28,49.392,-126.27,49.432,-126.244,49.443,-126.169,49.415,-126.1,49.421,-126.075,49.409,-126.048,49.379,-126.02,49.368,-125.935,49.401,-125.938,49.38,-125.984,49.288,-125.952,49.248,-125.918,49.25,-125.835,49.277,-125.796,49.26,-125.728,49.2,-125.694,49.19,-125.655,49.193,-125.644,49.186,-125.702,49.139,-125.812,49.107,-125.829,49.092,-125.66,49.029,-125.543,48.953,-125.489,48.934,-125.46,48.941,-125.363,48.998,-125.168,48.991,-124.927,49.014,-124.904,49.031,-124.868,49.079,-124.839,49.139,-124.821,49.207,-124.813,49.213,-124.8,49.142,-124.817,49.083,-124.85,49.028,-124.935,48.956,-125.136,48.822,-125.14,48.803,-125.121,48.761,-125.017,48.711,-124.868,48.654,-124.689,48.597,-124.376,48.515,-124.115,48.436,-123.917,48.387,-123.595,48.334,-123.573,48.323,-123.536,48.345,-123.485,48.4,-123.446,48.427,-123.335,48.406,-123.311,48.411,-123.284,48.455,-123.366,48.606,-123.39,48.67,-123.415,48.698]},{"b":[-74.478,17.636,-68.339,19.928],"l":[-68.685,18.905,-68.901,18.988,-69.031,19.013,-69.163,19.028,-69.28,19.052,-69.395,19.086,-69.508,19.108,-69.624,19.118,-69.623,19.16,-69.606,19.206,-69.52,19.212,-69.323,19.201,-69.264,19.226,-69.232,19.272,-69.325,19.328,-69.739,19.299,-69.823,19.367,-69.878,19.473,-69.891,19.59,-69.957,19.672,-70.014,19.673,-70.129,19.636,-70.194,19.638,-70.305,19.676,-70.436,19.771,-70.479,19.777,-70.636,19.776,-70.686,19.793,-70.785,19.851,-70.834,19.887,-70.954,19.914,-71.082,19.89,-71.236,19.848,-71.281,19.847,-71.442,19.894,-71.558,19.895,-71.616,19.877,-71.667,19.849,-71.706,19.795,-71.735,19.735,-71.779,19.718,-71.835,19.697,-71.954,19.722,-72.22,19.745,-72.43,19.813,-72.637,19.901,-72.877,19.928,-73.118,19.904,-73.218,19.884,-73.315,19.855,-73.401,19.807,-73.438,19.722,-73.396,19.659,-73.316,19.637,-73.053,19.611,-72.863,19.526,-72.703,19.441,-72.742,19.342,-72.768,19.241,-72.741,19.131,-72.811,19.072,-72.649,18.894,-72.465,18.744,-72.348,18.675,-72.347,18.624,-72.376,18.574,-72.418,18.559,-72.618,18.551,-72.66,18.515,-72.696,18.468,-72.739,18.442,-72.789,18.435,-72.917,18.456,-73.592,18.522,-73.687,18.565,-73.862,18.575,-73.976,18.601,-74.1,18.641,-74.228,18.663,-74.284,18.657,-74.387,18.625,-74.478,18.45,-74.46,18.393,-74.419,18.346,-74.195,18.269,-74.085,18.215,-73.989,18.143,-73.885,18.042,-73.839,18.058,-73.825,18.122,-73.747,18.19,-73.644,18.229,-73.515,18.245,-73.385,18.251,-73.272,18.234,-73.16,18.206,-72.877,18.152,-72.755,18.156,-72.633,18.176,-72.592,18.187,-72.553,18.208,-72.504,18.22,-72.06,18.229,-72.002,18.212,-71.946,18.186,-71.853,18.119,-71.768,18.039,-71.712,18.005,-71.674,17.954,-71.657,17.889,-71.658,17.821,-71.632,17.774,-71.569,17.757,-71.518,17.725,-71.439,17.636,-71.396,17.646,-71.358,17.694,-71.267,17.85,-71.106,18.07,-71.083,18.128,-71.082,18.224,-71.07,18.25,-71.028,18.273,-70.924,18.292,-70.759,18.346,-70.645,18.336,-70.565,18.268,-70.48,18.217,-70.183,18.252,-70.142,18.277,-70.063,18.346,-70.018,18.374,-69.896,18.418,-69.771,18.444,-69.645,18.436,-69.519,18.416,-69.397,18.42,-69.275,18.44,-69.072,18.399,-68.935,18.408,-68.82,18.339,-68.778,18.266,-68.721,18.218,-68.687,18.215,-68.659,18.222,-68.612,18.306,-68.564,18.355,-68.493,18.379,-68.445,18.418,-68.359,18.538,-68.339,18.612,-68.381,18.671,-68.445,18.714,-68.685,18.905]},{"b":[-50.796,-1.801,-48.38,-0.116],"l":[-48.38,-0.353,-48.393,-0.297,-48.444,-0.272,-48.515,-0.248,-48.588,-0.232,-48.787,-0.216,-49.117,-0.164,-49.215,-0.159,-49.314,-0.168,-49.403,-0.215,-49.535,-0.234,-49.629,-0.229,-50.248,-0.116,-50.462,-0.157,-50.646,-0.273,-50.694,-0.364,-50.716,-0.47,-50.703,-0.529,-50.72,-0.583,-50.771,-0.645,-50.781,-0.69,-50.796,-0.906,-50.783,-1.01,-50.71,-1.078,-50.593,-1.073,-50.577,-1.103,-50.581,-1.139,-50.596,-1.147,-50.668,-1.131,-50.729,-1.127,-50.76,-1.24,-50.724,-1.371,-50.673,-1.516,-50.617,-1.638,-50.602,-1.698,-50.508,-1.788,-50.443,-1.801,-50.338,-1.756,-50.109,-1.748,-50.066,-1.704,-50.01,-1.708,-49.911,-1.763,-49.805,-1.79,-49.749,-1.755,-49.651,-1.738,-49.588,-1.712,-49.526,-1.63,-49.507,-1.512,-49.407,-1.556,-49.345,-1.595,-49.234,-1.6,-49.205,-1.559,-49.182,-1.485,-49.173,-1.413,-49.087,-1.505,-49.038,-1.514,-48.986,-1.505,-48.929,-1.482,-48.834,-1.39,-48.804,-1.327,-48.829,-1.277,-48.84,-1.227,-48.79,-1.173,-48.729,-1.132,-48.705,-1.107,-48.624,-0.987,-48.571,-0.893,-48.55,-0.848,-48.54,-0.801,-48.567,-0.684,-48.523,-0.691,-48.497,-0.665,-48.464,-0.535,-48.428,-0.442,-48.38,-0.353]},{"b":[-6.134,50.021,1.747,58.65],"l":[-3.988,57.581,-3.868,57.6,-3.628,57.662,-3.403,57.708,-3.295,57.71,-3.084,57.673,-3.036,57.672,-2.947,57.689,-2.856,57.692,-2.244,57.681,-2.074,57.702,-1.962,57.677,-1.867,57.612,-1.778,57.494,-1.781,57.474,-1.835,57.42,-1.934,57.352,-2.02,57.259,-2.046,57.209,-2.062,57.153,-2.09,57.103,-2.26,56.863,-2.427,56.731,-2.501,56.637,-2.593,56.562,-2.681,56.514,-2.775,56.483,-3.047,56.449,-3.124,56.425,-3.214,56.384,-3.31,56.363,-3.198,56.366,-3.087,56.389,-2.885,56.398,-2.653,56.318,-2.674,56.253,-2.768,56.202,-2.98,56.194,-3.178,56.08,-3.268,56.045,-3.362,56.028,-3.48,56.033,-3.695,56.063,-3.789,56.095,-3.704,56.043,-3.608,56.016,-3.049,55.952,-3.015,55.959,-2.837,56.026,-2.599,56.027,-2.147,55.903,-2.017,55.808,-1.83,55.672,-1.729,55.619,-1.655,55.57,-1.61,55.498,-1.523,55.26,-1.423,55.026,-1.292,54.774,-1.232,54.704,-1.154,54.654,-0.759,54.541,-0.671,54.504,-0.518,54.395,-0.37,54.279,-0.233,54.19,-0.084,54.118,-0.156,54.081,-0.206,54.022,-0.169,53.942,-0.108,53.865,0.011,53.743,0.115,53.609,0.077,53.629,0.036,53.641,-0.019,53.637,-0.074,53.644,-0.174,53.685,-0.27,53.737,-0.461,53.716,-0.568,53.725,-0.66,53.724,-0.485,53.694,-0.294,53.692,0.128,53.468,0.271,53.335,0.356,53.16,0.298,53.081,0.208,53.03,0.124,52.972,0.046,52.906,0.28,52.809,0.33,52.812,0.382,52.825,0.432,52.858,0.516,52.938,0.559,52.967,0.704,52.977,0.827,52.971,0.949,52.953,1.056,52.959,1.271,52.925,1.382,52.894,1.657,52.754,1.716,52.677,1.743,52.579,1.747,52.469,1.7,52.369,1.647,52.279,1.615,52.162,1.591,52.12,1.559,52.087,1.413,51.995,1.317,51.957,1.276,51.974,1.232,51.971,1.228,51.949,1.274,51.902,1.274,51.845,1.188,51.803,1.101,51.785,0.955,51.808,0.752,51.73,0.898,51.689,0.927,51.647,0.891,51.571,0.799,51.538,0.698,51.523,0.593,51.519,0.507,51.501,0.425,51.466,0.528,51.484,0.6,51.468,0.646,51.405,0.687,51.387,0.889,51.36,1.015,51.36,1.257,51.375,1.373,51.375,1.415,51.363,1.416,51.311,1.398,51.182,1.366,51.155,1.044,51.047,0.979,50.972,0.96,50.926,0.772,50.934,0.684,50.886,0.532,50.853,0.415,50.819,0.3,50.776,0.205,50.763,-0.204,50.814,-0.451,50.81,-0.785,50.765,-0.871,50.773,-1.001,50.816,-1.133,50.845,-1.285,50.857,-1.416,50.897,-1.334,50.821,-1.517,50.747,-1.601,50.733,-1.688,50.735,-1.866,50.715,-2.031,50.725,-2.006,50.673,-1.962,50.628,-1.998,50.608,-2.036,50.603,-2.35,50.637,-2.395,50.631,-2.433,50.599,-2.548,50.616,-2.659,50.67,-2.777,50.706,-2.901,50.722,-2.999,50.717,-3.405,50.632,-3.485,50.548,-3.526,50.428,-3.584,50.322,-3.68,50.24,-3.793,50.229,-3.9,50.286,-4.103,50.349,-4.173,50.391,-4.195,50.393,-4.217,50.378,-4.297,50.359,-4.379,50.358,-4.507,50.341,-4.728,50.29,-4.817,50.256,-5.01,50.161,-5.049,50.134,-5.119,50.038,-5.225,50.021,-5.323,50.083,-5.434,50.104,-5.551,50.083,-5.622,50.051,-5.655,50.077,-5.656,50.132,-5.571,50.197,-5.342,50.246,-5.142,50.374,-5.043,50.452,-5.004,50.495,-4.956,50.523,-4.894,50.534,-4.861,50.582,-4.583,50.776,-4.56,50.821,-4.546,50.901,-4.523,50.977,-4.296,51.027,-4.188,51.189,-4.158,51.201,-3.842,51.231,-3.608,51.229,-3.375,51.197,-3.256,51.194,-3.136,51.205,-3.042,51.249,-2.881,51.406,-2.791,51.475,-2.687,51.537,-2.59,51.609,-2.433,51.741,-2.539,51.695,-2.668,51.623,-2.742,51.581,-2.979,51.539,-3.08,51.496,-3.259,51.398,-3.293,51.39,-3.562,51.414,-3.763,51.54,-3.891,51.592,-3.944,51.598,-3.998,51.582,-4.115,51.566,-4.235,51.569,-4.174,51.627,-4.091,51.66,-4.276,51.683,-4.328,51.7,-4.386,51.741,-4.531,51.748,-4.601,51.738,-4.718,51.684,-4.902,51.626,-5.125,51.706,-5.168,51.741,-5.167,51.808,-5.201,51.861,-5.262,51.88,-5.183,51.95,-5.088,51.996,-4.879,52.042,-4.561,52.151,-4.383,52.197,-4.218,52.277,-4.149,52.326,-4.1,52.393,-4.051,52.475,-3.98,52.542,-4.048,52.558,-4.079,52.608,-4.071,52.659,-4.039,52.704,-4.067,52.761,-4.118,52.82,-4.115,52.866,-4.101,52.915,-4.229,52.913,-4.356,52.897,-4.472,52.862,-4.584,52.815,-4.683,52.806,-4.681,52.844,-4.638,52.891,-4.526,52.958,-4.405,53.014,-4.362,53.056,-4.328,53.105,-4.269,53.145,-4.111,53.219,-3.809,53.303,-3.764,53.308,-3.646,53.298,-3.53,53.311,-3.428,53.341,-3.326,53.347,-3.098,53.26,-3.166,53.395,-3.065,53.427,-2.919,53.305,-2.864,53.293,-2.75,53.31,-2.794,53.331,-2.845,53.332,-2.913,53.35,-2.97,53.389,-3.065,53.513,-3.059,53.586,-2.996,53.663,-2.925,53.733,-2.984,53.747,-3.032,53.774,-3.045,53.844,-3.027,53.906,-2.9,53.961,-2.862,54.044,-2.846,54.135,-2.868,54.177,-2.994,54.171,-3.055,54.153,-3.11,54.126,-3.166,54.128,-3.322,54.229,-3.41,54.306,-3.569,54.468,-3.592,54.564,-3.465,54.773,-3.268,54.907,-3.036,54.953,-3.081,54.962,-3.434,54.964,-3.55,54.947,-3.658,54.893,-3.719,54.876,-3.783,54.87,-3.842,54.843,-3.899,54.805,-3.958,54.781,-4.076,54.787,-4.133,54.779,-4.174,54.801,-4.208,54.837,-4.253,54.847,-4.304,54.836,-4.41,54.787,-4.517,54.758,-4.648,54.789,-4.818,54.846,-4.852,54.825,-4.89,54.772,-4.911,54.689,-5.032,54.761,-5.135,54.858,-5.17,54.918,-5.173,54.986,-5.117,55.012,-5.056,54.988,-4.965,55.149,-4.785,55.359,-4.721,55.421,-4.677,55.501,-4.684,55.554,-4.724,55.598,-4.892,55.699,-4.89,55.781,-4.872,55.874,-4.826,55.93,-4.807,55.94,-4.584,55.939,-4.671,55.967,-4.844,56.051,-4.841,56.081,-4.8,56.158,-4.819,56.15,-4.856,56.115,-4.927,56.028,-4.97,56.008,-5.093,55.987,-5.115,55.945,-5.135,55.933,-5.196,55.929,-5.215,55.889,-5.228,55.886,-5.246,55.929,-5.247,56,-5.223,56.066,-5.176,56.117,-4.997,56.233,-5.084,56.197,-5.282,56.09,-5.383,56.019,-5.41,55.995,-5.419,55.975,-5.418,55.952,-5.373,55.828,-5.386,55.77,-5.556,55.39,-5.589,55.351,-5.618,55.331,-5.647,55.327,-5.731,55.334,-5.768,55.363,-5.768,55.395,-5.752,55.443,-5.681,55.624,-5.651,55.674,-5.605,55.721,-5.504,55.802,-5.507,55.808,-5.574,55.792,-5.602,55.797,-5.623,55.813,-5.61,56.055,-5.555,56.135,-5.535,56.251,-5.488,56.35,-5.433,56.422,-5.392,56.515,-5.329,56.556,-5.313,56.619,-5.243,56.687,-5.188,56.758,-5.218,56.751,-5.564,56.566,-5.652,56.532,-5.773,56.541,-5.865,56.562,-5.937,56.606,-5.969,56.69,-6.058,56.692,-6.134,56.707,-6.133,56.718,-6.035,56.764,-5.878,56.78,-5.731,56.853,-5.861,56.903,-5.85,56.918,-5.736,56.961,-5.591,57.102,-5.562,57.233,-5.631,57.294,-5.656,57.334,-5.795,57.379,-5.818,57.436,-5.802,57.468,-5.757,57.499,-5.689,57.524,-5.582,57.547,-5.679,57.572,-5.715,57.601,-5.742,57.644,-5.745,57.668,-5.695,57.778,-5.665,57.824,-5.608,57.881,-5.349,57.878,-5.319,57.904,-5.29,57.905,-5.157,57.881,-5.177,57.906,-5.394,58.044,-5.413,58.07,-5.351,58.144,-5.347,58.177,-5.356,58.212,-5.338,58.239,-5.27,58.251,-5.06,58.25,-5.008,58.263,-5.032,58.298,-5.081,58.345,-5.09,58.385,-5.079,58.419,-5.076,58.489,-5.067,58.52,-5.017,58.567,-4.976,58.58,-4.925,58.588,-4.81,58.573,-4.766,58.554,-4.715,58.51,-4.678,58.514,-4.535,58.562,-4.492,58.568,-4.433,58.513,-4.189,58.557,-3.86,58.577,-3.662,58.606,-3.454,58.617,-3.259,58.65,-3.053,58.635,-3.046,58.616,-3.057,58.589,-3.11,58.515,-3.101,58.434,-3.113,58.409,-3.137,58.378,-3.212,58.321,-3.411,58.24,-3.775,58.052,-3.99,57.959,-4.02,57.914,-4.036,57.852,-3.907,57.84,-3.857,57.819,-3.888,57.787,-4.078,57.677,-4.135,57.578,-3.988,57.581]},{"b":[-10.39,51.474,-5.47,55.366],"l":[-9.542,51.664,-9.525,51.681,-9.58,51.689,-9.899,51.647,-10.01,51.611,-10.121,51.601,-10.069,51.656,-9.926,51.731,-9.85,51.766,-9.803,51.78,-9.75,51.824,-9.599,51.874,-10.084,51.771,-10.212,51.784,-10.242,51.812,-10.341,51.799,-10.379,51.869,-10.232,51.975,-10.146,52.02,-10.044,52.045,-9.946,52.08,-9.91,52.123,-9.956,52.137,-10.25,52.126,-10.39,52.135,-10.383,52.169,-10.357,52.207,-10.211,52.272,-10.132,52.282,-10.062,52.276,-9.993,52.259,-9.937,52.238,-9.772,52.25,-9.841,52.291,-9.853,52.375,-9.906,52.404,-9.838,52.443,-9.761,52.466,-9.632,52.547,-9.586,52.559,-9.331,52.579,-9.056,52.621,-8.783,52.68,-8.923,52.712,-8.99,52.755,-9.098,52.668,-9.175,52.635,-9.394,52.617,-9.463,52.627,-9.561,52.654,-9.591,52.644,-9.62,52.623,-9.764,52.58,-9.917,52.57,-9.74,52.648,-9.515,52.781,-9.465,52.823,-9.394,52.896,-9.416,52.929,-9.462,52.947,-9.299,53.098,-9.242,53.125,-9.138,53.129,-9.061,53.153,-9.027,53.153,-8.997,53.162,-8.93,53.207,-9.034,53.236,-9.14,53.25,-9.471,53.235,-9.514,53.238,-9.555,53.252,-9.582,53.272,-9.602,53.323,-9.626,53.334,-9.701,53.334,-9.774,53.319,-9.825,53.32,-9.876,53.343,-9.795,53.395,-9.899,53.407,-10.004,53.397,-10.091,53.413,-10.094,53.446,-10.054,53.478,-10.106,53.509,-10.117,53.549,-10.062,53.568,-10.001,53.561,-9.878,53.59,-9.721,53.604,-9.856,53.633,-9.91,53.658,-9.912,53.695,-9.902,53.727,-9.745,53.781,-9.578,53.805,-9.591,53.841,-9.579,53.88,-9.748,53.891,-9.914,53.864,-9.896,53.938,-9.856,54.004,-9.848,54.048,-9.856,54.095,-9.934,54.075,-9.944,54.142,-9.977,54.187,-10.093,54.156,-10.09,54.216,-10.056,54.258,-9.996,54.276,-9.936,54.268,-9.825,54.269,-9.717,54.3,-9.562,54.309,-9.316,54.299,-9.146,54.21,-9.102,54.226,-9.034,54.282,-9.002,54.288,-8.747,54.263,-8.588,54.231,-8.546,54.241,-8.568,54.304,-8.623,54.347,-8.554,54.404,-8.471,54.442,-8.415,54.461,-8.287,54.485,-8.23,54.507,-8.193,54.58,-8.133,54.641,-8.457,54.609,-8.764,54.681,-8.715,54.732,-8.65,54.761,-8.538,54.783,-8.528,54.809,-8.471,54.832,-8.377,54.889,-8.412,54.965,-8.393,55.02,-8.326,55.056,-8.305,55.108,-8.275,55.146,-8.138,55.16,-8.006,55.195,-7.959,55.192,-7.803,55.2,-7.751,55.186,-7.763,55.248,-7.667,55.256,-7.63,55.244,-7.613,55.2,-7.57,55.171,-7.557,55.122,-7.586,55.084,-7.634,55.055,-7.59,55.025,-7.659,54.971,-7.584,54.994,-7.478,55.047,-7.484,55.09,-7.502,55.145,-7.531,55.194,-7.518,55.248,-7.458,55.282,-7.302,55.299,-7.366,55.36,-7.309,55.366,-7.247,55.353,-7.155,55.305,-7.06,55.268,-6.962,55.238,-7.056,55.178,-7.173,55.137,-7.219,55.092,-7.179,55.057,-7.101,55.048,-7.031,55.081,-6.947,55.183,-6.889,55.189,-6.825,55.181,-6.699,55.193,-6.475,55.241,-6.375,55.242,-6.234,55.217,-6.129,55.217,-6.036,55.145,-5.986,55.03,-5.869,54.916,-5.717,54.817,-5.711,54.757,-5.765,54.725,-5.879,54.684,-5.879,54.641,-5.803,54.663,-5.739,54.673,-5.583,54.663,-5.528,54.62,-5.49,54.554,-5.47,54.5,-5.484,54.442,-5.526,54.46,-5.569,54.513,-5.616,54.537,-5.671,54.55,-5.646,54.478,-5.656,54.382,-5.632,54.373,-5.558,54.371,-5.607,54.273,-5.708,54.246,-5.826,54.236,-5.855,54.201,-5.876,54.156,-5.938,54.089,-6.019,54.051,-6.12,54.059,-6.218,54.089,-6.176,54.054,-6.157,54.017,-6.231,54.004,-6.308,54.011,-6.345,53.987,-6.348,53.941,-6.322,53.882,-6.27,53.84,-6.229,53.746,-6.195,53.641,-6.142,53.578,-6.131,53.499,-6.139,53.46,-6.129,53.391,-6.152,53.366,-6.135,53.301,-6.072,53.166,-6.045,53.091,-6.027,52.927,-6.071,52.866,-6.131,52.807,-6.169,52.738,-6.199,52.663,-6.217,52.543,-6.345,52.402,-6.4,52.367,-6.463,52.345,-6.325,52.247,-6.438,52.203,-6.561,52.189,-6.697,52.214,-6.782,52.21,-6.86,52.179,-6.89,52.159,-6.915,52.169,-6.966,52.25,-7.003,52.166,-7.082,52.139,-7.216,52.145,-7.441,52.123,-7.527,52.099,-7.563,52.062,-7.59,52.019,-7.625,51.993,-7.665,51.98,-7.838,51.948,-7.872,51.935,-7.952,51.866,-8.058,51.826,-8.145,51.814,-8.222,51.854,-8.254,51.878,-8.29,51.891,-8.409,51.889,-8.372,51.876,-8.347,51.848,-8.336,51.793,-8.349,51.739,-8.408,51.712,-8.478,51.707,-8.588,51.651,-8.734,51.636,-8.813,51.585,-9.296,51.498,-9.324,51.497,-9.391,51.519,-9.463,51.529,-9.535,51.522,-9.737,51.474,-9.835,51.483,-9.71,51.604,-9.542,51.664]},{"b":[-180,64.28,-169.729,68.983],"l":[-180,68.983,-179.799,68.94,-179.595,68.906,-179.515,68.917,-179.471,68.912,-179.356,68.853,-179.279,68.825,-178.874,68.754,-178.689,68.675,-178.539,68.586,-178.614,68.603,-178.751,68.66,-178.737,68.593,-178.693,68.546,-178.474,68.502,-178.244,68.467,-178.097,68.425,-178.049,68.388,-178.019,68.323,-178.056,68.265,-177.922,68.287,-177.797,68.338,-177.862,68.378,-178.285,68.519,-178.373,68.566,-178.25,68.541,-177.683,68.363,-177.527,68.294,-177.593,68.281,-177.639,68.241,-177.589,68.224,-177.521,68.237,-177.408,68.245,-177.297,68.223,-177.172,68.175,-176.907,68.119,-175.345,67.678,-175.31,67.602,-175.266,67.567,-175.24,67.521,-175.233,67.447,-175.375,67.357,-175.155,67.365,-175.123,67.377,-175.066,67.413,-175.003,67.438,-174.918,67.408,-174.85,67.349,-174.87,67.269,-174.93,67.203,-174.938,67.093,-174.885,67,-174.829,66.961,-174.784,66.917,-174.771,66.784,-174.87,66.725,-174.925,66.623,-174.864,66.613,-174.675,66.603,-174.612,66.585,-174.504,66.538,-174.478,66.492,-174.454,66.43,-174.419,66.372,-174.394,66.344,-174.366,66.348,-174.257,66.428,-174.206,66.452,-174.085,66.473,-174.018,66.383,-174.065,66.23,-174.025,66.23,-173.994,66.246,-173.955,66.287,-173.9,66.31,-173.832,66.366,-173.774,66.435,-173.843,66.488,-173.921,66.522,-174.102,66.541,-174.196,66.581,-174.232,66.632,-174.14,66.653,-174.061,66.69,-174.006,66.779,-174.019,66.827,-174.041,66.875,-174.086,66.943,-174.154,66.982,-174.284,67.002,-174.342,67.04,-174.431,67.038,-174.519,67.049,-174.554,67.063,-174.55,67.091,-174.448,67.103,-173.884,67.106,-173.68,67.145,-173.587,67.133,-173.494,67.105,-173.158,67.069,-173.168,67.052,-173.224,67.035,-173.324,66.955,-173.343,66.909,-173.347,66.851,-173.259,66.84,-173.175,66.865,-173.216,66.911,-173.228,66.969,-173.193,66.994,-173.147,66.999,-173.058,66.956,-172.963,66.942,-172.641,66.925,-172.549,66.931,-172.52,66.952,-172.583,66.978,-173.002,67.034,-173.008,67.065,-172.621,67.027,-172.447,66.992,-172.274,66.966,-172.031,66.973,-171.796,66.932,-171.57,66.819,-171.36,66.677,-171.149,66.593,-170.927,66.53,-170.556,66.357,-170.51,66.344,-170.473,66.32,-170.543,66.291,-170.604,66.249,-170.483,66.278,-170.361,66.298,-170.301,66.294,-170.247,66.272,-170.212,66.236,-170.192,66.201,-170.244,66.169,-169.889,66.163,-169.778,66.143,-169.729,66.058,-169.832,65.999,-169.892,66.006,-169.949,66.031,-170.004,66.033,-170.159,66.008,-170.401,65.929,-170.541,65.865,-170.563,65.824,-170.541,65.71,-170.561,65.656,-170.666,65.622,-170.897,65.643,-171.001,65.665,-171.119,65.695,-171.232,65.737,-171.377,65.804,-171.422,65.81,-171.451,65.794,-171.402,65.752,-171.303,65.698,-171.134,65.628,-171.054,65.55,-171.106,65.511,-171.17,65.502,-171.216,65.503,-171.364,65.527,-171.466,65.533,-171.79,65.51,-171.907,65.496,-171.947,65.508,-171.957,65.542,-172.131,65.567,-172.234,65.57,-172.282,65.582,-172.322,65.618,-172.436,65.67,-172.608,65.69,-172.719,65.692,-172.783,65.681,-172.557,65.612,-172.354,65.496,-172.392,65.475,-172.418,65.45,-172.306,65.448,-172.233,65.456,-172.212,65.425,-172.27,65.303,-172.309,65.276,-172.662,65.249,-172.573,65.228,-172.482,65.222,-172.379,65.227,-172.286,65.206,-172.224,65.129,-172.213,65.048,-172.304,65.002,-172.399,64.965,-172.593,64.908,-172.792,64.883,-172.897,64.889,-172.999,64.877,-173.066,64.847,-173.086,64.817,-172.998,64.837,-172.897,64.826,-172.801,64.791,-172.812,64.761,-172.903,64.729,-172.924,64.705,-172.889,64.664,-172.901,64.629,-172.854,64.61,-172.747,64.603,-172.616,64.578,-172.487,64.544,-172.437,64.515,-172.394,64.475,-172.379,64.432,-172.401,64.414,-172.695,64.407,-172.739,64.412,-172.756,64.46,-172.792,64.499,-172.903,64.526,-172.949,64.507,-172.916,64.369,-172.96,64.328,-173.009,64.297,-173.157,64.28,-173.275,64.29,-173.376,64.355,-173.376,64.41,-173.309,64.443,-173.309,64.487,-173.327,64.54,-173.396,64.479,-173.475,64.429,-173.604,64.365,-173.666,64.357,-173.73,64.365,-173.898,64.41,-174.001,64.449,-174.205,64.578,-174.318,64.638,-174.571,64.718,-174.83,64.776,-175.036,64.814,-175.146,64.809,-175.256,64.794,-175.395,64.802,-175.442,64.817,-175.483,64.849,-175.521,64.867,-175.716,64.946,-175.854,65.011,-175.859,65.054,-175.83,65.106,-175.856,65.233,-175.923,65.352,-176.093,65.471,-176.547,65.548,-176.922,65.601,-177.056,65.614,-177.175,65.602,-177.489,65.504,-177.699,65.49,-178.31,65.485,-178.412,65.496,-178.505,65.537,-178.526,65.593,-178.499,65.697,-178.502,65.74,-178.526,65.755,-178.559,65.754,-178.679,65.795,-178.791,65.865,-178.879,65.936,-178.939,66.033,-178.858,66.038,-178.747,66.014,-178.731,66.037,-178.694,66.124,-178.616,66.166,-178.587,66.198,-178.534,66.317,-178.527,66.402,-178.616,66.355,-178.753,66.237,-178.821,66.203,-178.868,66.187,-178.916,66.18,-179.026,66.204,-179.105,66.232,-179.107,66.346,-179.143,66.375,-179.178,66.353,-179.193,66.313,-179.293,66.305,-179.34,66.288,-179.316,66.22,-179.327,66.163,-179.423,66.141,-179.616,66.128,-179.683,66.184,-179.741,66.106,-179.784,66.018,-179.79,65.901,-179.728,65.804,-179.641,65.758,-179.449,65.688,-179.366,65.639,-179.344,65.575,-179.352,65.517,-179.452,65.445,-179.519,65.386,-179.635,65.244,-179.705,65.187,-180,65.067]},{"b":[-24.476,63.407,-13.556,66.526],"l":[-14.788,66.331,-14.912,66.284,-15.03,66.178,-15.117,66.126,-15.116,66.102,-15.01,66.061,-14.894,66.038,-14.787,66.059,-14.74,66.051,-14.698,66.02,-14.674,65.99,-14.669,65.96,-14.688,65.897,-14.753,65.834,-14.839,65.781,-14.827,65.764,-14.758,65.756,-14.426,65.79,-14.392,65.787,-14.373,65.77,-14.369,65.739,-14.351,65.71,-14.318,65.684,-14.328,65.658,-14.473,65.575,-14.302,65.628,-14.167,65.642,-13.935,65.616,-13.841,65.586,-13.785,65.533,-13.705,65.551,-13.67,65.55,-13.618,65.519,-13.616,65.487,-13.654,65.441,-13.668,65.399,-13.708,65.382,-13.783,65.369,-13.805,65.355,-13.772,65.323,-13.723,65.291,-13.653,65.29,-13.641,65.275,-13.64,65.257,-13.649,65.237,-13.672,65.223,-13.707,65.215,-13.755,65.193,-13.581,65.143,-13.559,65.125,-13.556,65.098,-13.57,65.068,-13.599,65.036,-13.652,65.017,-13.777,65.014,-13.854,64.993,-13.828,64.958,-13.83,64.914,-13.853,64.862,-13.952,64.784,-14.044,64.742,-14.135,64.715,-14.297,64.724,-14.385,64.745,-14.375,64.677,-14.465,64.636,-14.448,64.601,-14.417,64.583,-14.433,64.538,-14.475,64.494,-14.547,64.446,-14.628,64.416,-14.79,64.38,-14.927,64.32,-15.022,64.296,-15.256,64.297,-15.495,64.258,-15.833,64.177,-16.06,64.111,-16.236,64.037,-16.468,63.916,-16.64,63.865,-16.74,63.852,-16.933,63.841,-17.095,63.808,-17.633,63.747,-17.816,63.713,-17.839,63.682,-17.915,63.636,-17.92,63.62,-17.886,63.607,-17.88,63.59,-17.947,63.536,-18.08,63.496,-18.143,63.497,-18.219,63.531,-18.252,63.53,-18.265,63.525,-18.266,63.514,-18.222,63.473,-18.303,63.454,-18.654,63.407,-19.25,63.442,-19.487,63.479,-19.778,63.537,-19.952,63.552,-20.198,63.556,-20.4,63.637,-20.494,63.687,-20.502,63.708,-20.491,63.732,-20.47,63.748,-20.438,63.757,-20.372,63.758,-20.363,63.765,-20.414,63.805,-20.463,63.792,-20.593,63.735,-20.651,63.737,-20.727,63.766,-20.73,63.793,-20.879,63.804,-21.008,63.838,-21.137,63.888,-21.156,63.907,-21.094,63.934,-21.106,63.94,-21.152,63.945,-21.246,63.935,-21.388,63.873,-21.449,63.858,-22.373,63.844,-22.607,63.837,-22.652,63.828,-22.693,63.869,-22.729,63.959,-22.743,64.019,-22.734,64.048,-22.701,64.083,-22.651,64.077,-22.603,64.05,-22.56,64.01,-22.51,63.991,-22.188,64.039,-22.057,64.071,-22.001,64.102,-21.935,64.154,-21.866,64.18,-21.833,64.205,-21.768,64.285,-21.723,64.322,-21.669,64.349,-21.606,64.367,-21.463,64.379,-21.557,64.398,-21.647,64.398,-21.951,64.314,-22.053,64.314,-22.049,64.327,-22.006,64.351,-21.901,64.392,-21.973,64.395,-22.001,64.413,-22.004,64.452,-21.95,64.515,-21.702,64.598,-21.617,64.61,-21.591,64.626,-21.623,64.64,-21.675,64.648,-21.924,64.563,-22.106,64.533,-22.16,64.539,-22.254,64.572,-22.284,64.587,-22.325,64.624,-22.32,64.647,-22.234,64.714,-22.248,64.727,-22.307,64.733,-22.467,64.795,-22.72,64.789,-23.347,64.824,-23.476,64.809,-23.69,64.757,-23.819,64.739,-23.879,64.751,-23.933,64.779,-23.982,64.816,-24.026,64.863,-24.007,64.896,-23.924,64.915,-23.864,64.924,-23.693,64.913,-23.485,64.946,-23.353,64.953,-23.315,64.958,-23.237,64.993,-23.198,65.002,-23.138,64.99,-23.109,64.966,-22.9,65.003,-22.828,65.022,-22.82,65.033,-22.788,65.046,-22.684,65.026,-22.6,65.026,-22.494,65.04,-22.308,65.046,-21.892,65.049,-21.83,65.079,-21.8,65.106,-21.764,65.174,-21.78,65.188,-22.04,65.125,-22.099,65.126,-22.4,65.159,-22.509,65.197,-22.473,65.227,-22.314,65.292,-22.149,65.344,-21.907,65.4,-21.85,65.422,-21.844,65.447,-22.006,65.493,-22.311,65.481,-22.39,65.535,-22.644,65.568,-22.813,65.547,-22.902,65.58,-23.122,65.535,-23.605,65.469,-23.796,65.423,-23.9,65.408,-24.019,65.445,-24.224,65.487,-24.455,65.5,-24.476,65.525,-24.341,65.601,-24.249,65.615,-24.156,65.608,-23.979,65.555,-23.857,65.538,-24.01,65.616,-24.006,65.646,-24.018,65.691,-24.065,65.71,-24.112,65.76,-24.093,65.776,-24.032,65.782,-23.909,65.766,-23.616,65.68,-23.472,65.695,-23.393,65.727,-23.285,65.75,-23.316,65.762,-23.569,65.764,-23.705,65.781,-23.773,65.806,-23.833,65.849,-23.812,65.869,-23.741,65.885,-23.525,65.88,-23.667,65.954,-23.767,65.997,-23.777,66.018,-23.771,66.043,-23.757,66.061,-23.737,66.069,-23.489,66.026,-23.434,66.024,-23.485,66.052,-23.594,66.093,-23.599,66.109,-23.553,66.122,-23.53,66.145,-23.528,66.164,-23.453,66.181,-23.377,66.182,-23.3,66.167,-23.063,66.086,-23.029,66.064,-23.017,66.034,-23.029,65.997,-23.019,65.982,-22.926,65.995,-22.852,65.979,-22.815,65.983,-22.723,66.039,-22.66,66.026,-22.622,66,-22.61,65.976,-22.604,65.944,-22.62,65.877,-22.616,65.867,-22.552,65.905,-22.442,65.908,-22.428,65.927,-22.424,65.998,-22.433,66.058,-22.445,66.07,-22.806,66.153,-22.869,66.172,-22.948,66.213,-22.932,66.233,-22.862,66.251,-22.756,66.259,-22.509,66.258,-22.484,66.266,-22.532,66.288,-22.646,66.302,-22.673,66.314,-22.686,66.338,-22.821,66.325,-22.972,66.324,-23.117,66.339,-23.12,66.357,-23.063,66.384,-22.944,66.429,-22.889,66.441,-22.724,66.433,-22.559,66.445,-22.426,66.43,-22.32,66.385,-22.17,66.307,-21.967,66.257,-21.948,66.241,-21.84,66.2,-21.625,66.09,-21.407,66.026,-21.397,66.009,-21.433,65.99,-21.517,65.968,-21.497,65.955,-21.388,65.939,-21.309,65.895,-21.303,65.876,-21.375,65.742,-21.413,65.713,-21.457,65.698,-21.658,65.724,-21.61,65.681,-21.466,65.635,-21.434,65.61,-21.455,65.585,-21.439,65.579,-21.387,65.592,-21.365,65.578,-21.374,65.536,-21.396,65.502,-21.432,65.474,-21.422,65.462,-21.365,65.466,-21.313,65.459,-21.23,65.421,-21.163,65.304,-21.13,65.267,-21.106,65.3,-21.076,65.385,-21.047,65.428,-21.021,65.43,-20.998,65.445,-20.979,65.471,-20.94,65.565,-20.804,65.636,-20.74,65.658,-20.679,65.663,-20.649,65.654,-20.548,65.579,-20.487,65.567,-20.455,65.571,-20.412,65.622,-20.357,65.719,-20.344,65.828,-20.374,65.948,-20.357,66.033,-20.292,66.084,-20.208,66.1,-20.103,66.08,-20.026,66.049,-19.875,65.93,-19.753,65.868,-19.648,65.801,-19.594,65.779,-19.49,65.768,-19.462,65.772,-19.443,65.788,-19.434,65.814,-19.456,65.985,-19.427,66.038,-19.383,66.076,-19.195,66.098,-19.093,66.122,-18.994,66.16,-18.911,66.181,-18.846,66.184,-18.778,66.169,-18.706,66.136,-18.595,66.071,-18.455,65.965,-18.277,65.885,-18.184,65.758,-18.164,65.737,-18.142,65.734,-18.118,65.751,-18.103,65.774,-18.099,65.83,-18.149,65.905,-18.315,66.093,-18.318,66.129,-18.297,66.157,-18.18,66.161,-17.907,66.143,-17.82,66.114,-17.634,65.999,-17.582,65.971,-17.55,65.964,-17.539,65.978,-17.467,66,-17.417,66.026,-17.334,66.089,-17.153,66.203,-17.115,66.206,-17.062,66.197,-16.97,66.167,-16.925,66.143,-16.838,66.125,-16.748,66.132,-16.625,66.172,-16.485,66.196,-16.437,66.253,-16.428,66.278,-16.541,66.447,-16.493,66.481,-16.249,66.523,-16.036,66.526,-15.985,66.515,-15.851,66.433,-15.76,66.392,-15.714,66.359,-15.703,66.286,-15.647,66.259,-15.543,66.229,-15.428,66.225,-15.241,66.259,-15.162,66.282,-14.97,66.36,-14.856,66.381,-14.681,66.376,-14.596,66.382,-14.594,66.374,-14.702,66.342,-14.788,66.331]},{"b":[-65.024,18.321,-64.845,18.385],"l":[-64.845,18.33,-64.889,18.374,-64.942,18.385,-65.024,18.368,-64.92,18.321,-64.845,18.33]},{"b":[178.475,51.899,178.607,51.995],"l":[178.571,51.978,178.509,51.995,178.475,51.968,178.478,51.943,178.512,51.899,178.575,51.916,178.607,51.953,178.571,51.978]},{"b":[14.18,36.012,14.321,36.076],"l":[14.18,36.06,14.194,36.042,14.254,36.012,14.313,36.028,14.321,36.036,14.304,36.062,14.263,36.076,14.18,36.06]},{"b":[13.854,40.697,13.962,40.762],"l":[13.871,40.762,13.854,40.724,13.868,40.709,13.894,40.697,13.938,40.706,13.961,40.718,13.962,40.739,13.871,40.762]},{"b":[11.936,36.746,12.051,36.843],"l":[11.948,36.843,11.936,36.829,11.941,36.78,12.003,36.746,12.051,36.757,12.048,36.776,12.024,36.821,11.948,36.843]},{"b":[6.159,53.477,6.333,53.515],"l":[6.193,53.477,6.333,53.511,6.291,53.515,6.168,53.494,6.159,53.484,6.193,53.477]},{"b":[5.654,53.443,5.929,53.475],"l":[5.708,53.473,5.654,53.467,5.665,53.455,5.732,53.443,5.929,53.459,5.928,53.465,5.876,53.475,5.708,53.473]},{"b":[4.908,53.235,5.109,53.31],"l":[5.027,53.31,4.908,53.246,4.924,53.235,5.109,53.308,5.027,53.31]},{"b":[5.19,53.378,5.583,53.444],"l":[5.415,53.431,5.19,53.392,5.233,53.378,5.326,53.386,5.583,53.438,5.557,53.444,5.415,53.431]},{"b":[-179.079,-17.992,-178.988,-17.92],"l":[-179.039,-17.988,-179.018,-17.992,-178.988,-17.977,-178.999,-17.947,-179.048,-17.92,-179.079,-17.944,-179.064,-17.972,-179.039,-17.988]},{"b":[-178.848,-18.252,-178.762,-18.186],"l":[-178.848,-18.202,-178.827,-18.222,-178.774,-18.252,-178.762,-18.234,-178.763,-18.191,-178.791,-18.186,-178.848,-18.202]},{"b":[-178.357,-17.963,-178.251,-17.876],"l":[-178.325,-17.876,-178.357,-17.921,-178.307,-17.963,-178.251,-17.953,-178.255,-17.93,-178.28,-17.886,-178.325,-17.876]},{"b":[-151.512,-16.665,-151.438,-16.574],"l":[-151.506,-16.574,-151.512,-16.619,-151.504,-16.647,-151.485,-16.665,-151.467,-16.658,-151.438,-16.623,-151.457,-16.604,-151.506,-16.574]},{"b":[-149.912,-17.571,-149.782,-17.47],"l":[-149.814,-17.545,-149.782,-17.488,-149.809,-17.474,-149.902,-17.47,-149.912,-17.501,-149.905,-17.528,-149.887,-17.553,-149.845,-17.571,-149.814,-17.545]},{"b":[-120.442,34.022,-120.307,34.073],"l":[-120.353,34.061,-120.368,34.073,-120.413,34.056,-120.442,34.033,-120.36,34.022,-120.307,34.025,-120.353,34.061]},{"b":[-119.575,33.215,-119.438,33.282],"l":[-119.525,33.282,-119.575,33.278,-119.544,33.225,-119.483,33.215,-119.438,33.217,-119.442,33.232,-119.479,33.275,-119.525,33.282]},{"b":[-67.85,-55.892,-67.51,-55.816],"l":[-67.762,-55.816,-67.834,-55.828,-67.85,-55.843,-67.846,-55.857,-67.832,-55.865,-67.7,-55.873,-67.611,-55.892,-67.575,-55.89,-67.545,-55.877,-67.51,-55.844,-67.517,-55.833,-67.545,-55.826,-67.762,-55.816]},{"b":[-70.066,12.423,-69.896,12.614],"l":[-69.973,12.568,-70.035,12.614,-70.051,12.597,-70.066,12.547,-70.004,12.5,-69.942,12.439,-69.896,12.423,-69.899,12.452,-69.912,12.48,-69.973,12.568]},{"b":[-65.415,10.884,-65.213,10.974],"l":[-65.383,10.974,-65.415,10.938,-65.365,10.906,-65.266,10.884,-65.213,10.906,-65.227,10.93,-65.302,10.974,-65.383,10.974]},{"b":[-73.285,18.707,-72.805,18.967],"l":[-73.171,18.967,-73.276,18.954,-73.285,18.897,-73.078,18.791,-72.822,18.707,-72.805,18.778,-72.919,18.861,-73.069,18.932,-73.171,18.967]},{"b":[-25.198,36.942,-25.027,37.024],"l":[-25.164,37.019,-25.198,36.997,-25.16,36.943,-25.088,36.949,-25.032,36.942,-25.027,36.96,-25.044,37,-25.083,37.024,-25.164,37.019]},{"b":[16.651,42.896,17.188,42.997],"l":[16.978,42.928,17.089,42.915,17.188,42.917,17.17,42.933,17.094,42.964,16.971,42.981,16.836,42.969,16.651,42.997,16.666,42.96,16.696,42.934,16.739,42.913,16.851,42.896,16.978,42.928]},{"b":[16.376,43.115,17.194,43.229],"l":[17.124,43.115,17.194,43.126,17.061,43.144,16.697,43.175,16.656,43.214,16.521,43.229,16.376,43.214,16.406,43.197,16.55,43.144,16.679,43.123,17.124,43.115]},{"b":[16.423,43.268,16.891,43.387],"l":[16.627,43.268,16.49,43.286,16.423,43.317,16.428,43.343,16.449,43.387,16.602,43.382,16.834,43.351,16.891,43.315,16.874,43.298,16.785,43.271,16.627,43.268]},{"b":[-2.646,49.429,-2.512,49.507],"l":[-2.646,49.468,-2.639,49.451,-2.547,49.429,-2.512,49.495,-2.521,49.506,-2.542,49.507,-2.646,49.468]},{"b":[-2.236,49.17,-2.01,49.266],"l":[-2.221,49.266,-2.236,49.176,-2.166,49.187,-2.091,49.187,-2.054,49.17,-2.01,49.181,-2.019,49.231,-2.082,49.255,-2.221,49.266]},{"b":[145.646,18.725,145.708,18.807],"l":[145.678,18.725,145.708,18.763,145.707,18.79,145.69,18.802,145.646,18.807,145.653,18.753,145.678,18.725]},{"b":[145.631,16.332,145.72,16.38],"l":[145.631,16.378,145.636,16.352,145.658,16.336,145.69,16.332,145.712,16.339,145.72,16.36,145.696,16.38,145.631,16.378]},{"b":[145.152,14.111,145.265,14.189],"l":[145.152,14.164,145.157,14.137,145.18,14.121,145.215,14.111,145.265,14.158,145.265,14.18,145.232,14.189,145.152,14.164]},{"b":[-60.117,43.904,-59.727,44.003],"l":[-59.866,43.947,-59.936,43.94,-60.117,43.953,-60.114,43.939,-60.038,43.907,-59.922,43.904,-59.788,43.94,-59.727,44.003,-59.866,43.947]},{"b":[-97.036,27.899,-96.84,28.133],"l":[-96.84,28.089,-96.857,28.133,-96.899,28.117,-96.979,28.014,-96.988,27.981,-97.036,27.899,-97.014,27.902,-96.921,28.016,-96.84,28.089]},{"b":[-88.944,29.66,-88.873,29.753],"l":[-88.941,29.68,-88.944,29.66,-88.889,29.713,-88.873,29.753,-88.901,29.733,-88.941,29.68]},{"b":[-88.723,30.205,-88.558,30.264],"l":[-88.723,30.264,-88.713,30.245,-88.659,30.226,-88.571,30.205,-88.558,30.216,-88.574,30.229,-88.723,30.264]},{"b":[-88.316,30.231,-88.071,30.274],"l":[-88.109,30.274,-88.264,30.255,-88.316,30.24,-88.29,30.233,-88.159,30.231,-88.071,30.252,-88.109,30.274]},{"b":[-63.125,18.019,-63.009,18.115],"l":[-63.115,18.091,-63.125,18.064,-63.09,18.041,-63.023,18.019,-63.012,18.045,-63.009,18.104,-63.025,18.113,-63.063,18.115,-63.115,18.091]},{"b":[-65.572,18.1,-65.295,18.165],"l":[-65.572,18.137,-65.555,18.108,-65.504,18.1,-65.426,18.106,-65.295,18.133,-65.303,18.144,-65.366,18.161,-65.477,18.165,-65.572,18.137]},{"b":[-62.625,17.101,-62.532,17.201],"l":[-62.625,17.13,-62.582,17.101,-62.532,17.122,-62.534,17.17,-62.575,17.201,-62.615,17.199,-62.625,17.13]},{"b":[-64.444,18.457,-64.325,18.517],"l":[-64.426,18.513,-64.444,18.473,-64.438,18.459,-64.421,18.457,-64.395,18.465,-64.325,18.517,-64.426,18.513]},{"b":[-64.788,18.328,-64.66,18.372],"l":[-64.788,18.341,-64.771,18.332,-64.726,18.328,-64.66,18.354,-64.752,18.372,-64.788,18.341]},{"b":[-64.695,18.399,-64.545,18.446],"l":[-64.651,18.443,-64.695,18.412,-64.672,18.399,-64.594,18.403,-64.545,18.438,-64.569,18.446,-64.651,18.443]},{"b":[-64.889,17.702,-64.58,17.794],"l":[-64.889,17.702,-64.686,17.706,-64.58,17.75,-64.682,17.75,-64.766,17.794,-64.885,17.772,-64.889,17.702]},{"b":[-77.562,24.993,-77.269,25.083],"l":[-77.451,25.081,-77.527,25.058,-77.562,25.03,-77.541,25.014,-77.46,24.993,-77.348,25.014,-77.269,25.044,-77.276,25.056,-77.329,25.083,-77.451,25.081]},{"b":[-81.137,24.693,-80.93,24.759],"l":[-81.085,24.734,-81.137,24.71,-81.09,24.693,-81.044,24.717,-80.989,24.728,-80.93,24.759,-81.085,24.734]},{"b":[114.134,22.195,114.247,22.297],"l":[114.134,22.292,114.139,22.268,114.207,22.195,114.232,22.211,114.244,22.234,114.247,22.264,114.187,22.297,114.134,22.292]},{"b":[-76.674,34.653,-76.546,34.7],"l":[-76.622,34.695,-76.546,34.655,-76.569,34.653,-76.608,34.664,-76.662,34.685,-76.674,34.7,-76.622,34.695]},{"b":[-175.362,-21.264,-175.078,-21.068],"l":[-175.078,-21.129,-175.084,-21.161,-175.157,-21.264,-175.202,-21.223,-175.335,-21.158,-175.362,-21.107,-175.318,-21.068,-175.323,-21.099,-175.3,-21.113,-175.225,-21.119,-175.158,-21.146,-175.2,-21.156,-175.162,-21.169,-175.148,-21.169,-175.132,-21.14,-175.078,-21.129]},{"b":[52.554,45.401,52.693,45.528],"l":[52.66,45.518,52.609,45.528,52.554,45.474,52.598,45.428,52.665,45.401,52.682,45.412,52.693,45.461,52.66,45.518]},{"b":[47.918,45.47,47.987,45.647],"l":[47.987,45.554,47.947,45.647,47.918,45.618,47.92,45.562,47.968,45.47,47.983,45.488,47.987,45.554]},{"b":[-58.541,-52.099,-58.433,-51.999],"l":[-58.497,-51.999,-58.541,-52.028,-58.513,-52.071,-58.433,-52.099,-58.439,-52.011,-58.461,-52.002,-58.497,-51.999]},{"b":[42.549,15.281,42.625,15.433],"l":[42.57,15.407,42.549,15.32,42.559,15.281,42.59,15.303,42.61,15.332,42.625,15.368,42.602,15.433,42.57,15.407]},{"b":[39.996,15.986,40.11,16.104],"l":[40.048,16.104,40.039,16.081,39.996,16.043,40.012,16.023,40.11,15.986,40.076,16.082,40.048,16.104]},{"b":[36.504,25.383,36.955,25.689],"l":[36.924,25.426,36.748,25.559,36.589,25.62,36.554,25.645,36.534,25.689,36.504,25.645,36.53,25.602,36.722,25.534,36.764,25.501,36.805,25.451,36.875,25.383,36.902,25.383,36.955,25.415,36.924,25.426]},{"b":[36.544,25.699,36.596,25.856],"l":[36.586,25.699,36.596,25.713,36.596,25.735,36.58,25.795,36.583,25.856,36.546,25.812,36.544,25.734,36.586,25.699]},{"b":[101.265,2.97,101.328,3.067],"l":[101.268,2.97,101.319,2.988,101.328,3.048,101.311,3.067,101.274,3.033,101.265,2.996,101.268,2.97]},{"b":[134.634,-6.559,134.729,-6.456],"l":[134.679,-6.456,134.634,-6.477,134.661,-6.559,134.716,-6.549,134.729,-6.506,134.679,-6.456]},{"b":[123.483,-1.711,123.616,-1.503],"l":[123.549,-1.508,123.529,-1.503,123.487,-1.535,123.483,-1.681,123.529,-1.711,123.598,-1.704,123.616,-1.627,123.582,-1.591,123.561,-1.552,123.549,-1.508]},{"b":[126.646,34.296,126.77,34.396],"l":[126.7,34.396,126.652,34.39,126.646,34.351,126.689,34.305,126.77,34.296,126.753,34.344,126.7,34.396]},{"b":[152.559,76.086,152.886,76.195],"l":[152.799,76.195,152.643,76.175,152.559,76.144,152.786,76.086,152.886,76.122,152.864,76.163,152.835,76.185,152.799,76.195]},{"b":[67.026,69.443,67.345,69.575],"l":[67.216,69.575,67.026,69.483,67.047,69.467,67.098,69.447,67.264,69.443,67.345,69.53,67.329,69.572,67.216,69.575]},{"b":[66.395,70.501,66.569,70.773],"l":[66.458,70.699,66.463,70.769,66.44,70.773,66.418,70.757,66.395,70.727,66.408,70.616,66.449,70.561,66.516,70.515,66.569,70.501,66.561,70.542,66.458,70.699]},{"b":[54.98,80.227,55.48,80.325],"l":[55.24,80.325,55.092,80.296,54.98,80.256,55.048,80.228,55.195,80.227,55.48,80.274,55.435,80.302,55.353,80.318,55.24,80.325]},{"b":[18.35,59.02,18.486,59.108],"l":[18.398,59.089,18.377,59.069,18.35,59.023,18.372,59.02,18.416,59.029,18.486,59.105,18.465,59.108,18.398,59.089]},{"b":[18.545,59.437,18.698,59.548],"l":[18.621,59.548,18.572,59.526,18.555,59.486,18.545,59.478,18.57,59.437,18.595,59.47,18.624,59.492,18.698,59.525,18.698,59.535,18.621,59.548]},{"b":[-122.912,47.185,-122.849,47.275],"l":[-122.853,47.205,-122.863,47.185,-122.877,47.186,-122.908,47.226,-122.912,47.254,-122.885,47.275,-122.849,47.216,-122.853,47.205]},{"b":[105.584,-10.564,105.725,-10.431],"l":[105.646,-10.452,105.596,-10.46,105.584,-10.512,105.644,-10.525,105.697,-10.564,105.725,-10.493,105.705,-10.431,105.67,-10.449,105.646,-10.452]},{"b":[-86.63,16.3,-86.256,16.439],"l":[-86.338,16.439,-86.256,16.428,-86.42,16.378,-86.58,16.3,-86.63,16.302,-86.557,16.362,-86.438,16.414,-86.338,16.439]},{"b":[163.737,-74.833,164.208,-74.608],"l":[163.742,-74.712,163.737,-74.734,163.763,-74.803,163.845,-74.833,163.976,-74.833,164.059,-74.753,164.098,-74.732,164.208,-74.608,164.002,-74.629,163.742,-74.712]},{"b":[164.639,-67.569,164.919,-67.259],"l":[164.684,-67.259,164.675,-67.289,164.696,-67.408,164.639,-67.5,164.692,-67.56,164.746,-67.569,164.834,-67.54,164.86,-67.504,164.919,-67.447,164.907,-67.419,164.85,-67.364,164.825,-67.326,164.684,-67.259]},{"b":[166.013,-78.306,167.643,-77.992],"l":[167.594,-78.022,167.498,-77.992,167.422,-78.006,167.364,-78.046,167.138,-78.13,166.864,-78.196,166.76,-78.198,166.567,-78.148,166.111,-78.09,166.013,-78.102,166.013,-78.131,166.051,-78.213,166.122,-78.275,166.285,-78.306,166.626,-78.284,166.936,-78.222,167.377,-78.249,167.517,-78.216,167.643,-78.141,167.64,-78.112,167.594,-78.022]},{"b":[163.09,-66.882,163.302,-66.688],"l":[163.271,-66.768,163.238,-66.709,163.156,-66.688,163.09,-66.701,163.164,-66.819,163.235,-66.868,163.284,-66.882,163.302,-66.821,163.299,-66.798,163.271,-66.768]},{"b":[166.217,-77.851,169.353,-77.162],"l":[168.451,-77.386,167.461,-77.394,167.084,-77.322,167.085,-77.307,167.107,-77.271,166.987,-77.187,166.716,-77.162,166.506,-77.189,166.413,-77.252,166.469,-77.289,166.607,-77.336,166.626,-77.377,166.458,-77.444,166.378,-77.494,166.217,-77.525,166.237,-77.547,166.533,-77.7,166.65,-77.774,166.729,-77.851,167.025,-77.756,167.279,-77.703,167.392,-77.649,167.918,-77.644,168.323,-77.683,168.519,-77.681,168.755,-77.653,169.117,-77.561,169.353,-77.525,169.276,-77.455,168.451,-77.386]},{"b":[169.479,-73.625,169.96,-73.32],"l":[169.887,-73.459,169.783,-73.324,169.74,-73.32,169.672,-73.346,169.645,-73.379,169.659,-73.418,169.479,-73.539,169.522,-73.562,169.709,-73.625,169.844,-73.605,169.859,-73.568,169.96,-73.514,169.887,-73.459]},{"b":[162.591,-75.696,162.968,-75.557],"l":[162.72,-75.597,162.591,-75.669,162.662,-75.692,162.788,-75.696,162.968,-75.567,162.917,-75.557,162.842,-75.566,162.72,-75.597]},{"b":[100.27,-65.701,101.259,-65.378],"l":[100.409,-65.466,100.324,-65.521,100.27,-65.603,100.293,-65.651,100.351,-65.673,100.512,-65.675,100.547,-65.701,100.981,-65.678,101.238,-65.565,101.259,-65.528,101.221,-65.472,101.079,-65.403,100.883,-65.378,100.607,-65.396,100.545,-65.409,100.409,-65.466]},{"b":[85.314,-66.98,85.938,-66.723],"l":[85.34,-66.723,85.314,-66.776,85.359,-66.854,85.617,-66.951,85.622,-66.965,85.65,-66.98,85.822,-66.953,85.938,-66.894,85.806,-66.775,85.553,-66.729,85.34,-66.723]},{"b":[92.248,-65.822,92.67,-65.702],"l":[92.301,-65.707,92.248,-65.74,92.263,-65.76,92.333,-65.807,92.471,-65.822,92.601,-65.808,92.67,-65.775,92.665,-65.76,92.634,-65.731,92.496,-65.702,92.301,-65.707]},{"b":[86.232,-66.792,86.652,-66.675],"l":[86.383,-66.675,86.278,-66.697,86.232,-66.733,86.337,-66.788,86.427,-66.792,86.542,-66.767,86.652,-66.718,86.557,-66.706,86.521,-66.687,86.383,-66.675]},{"b":[85.069,-66.643,85.329,-66.519],"l":[85.165,-66.522,85.121,-66.519,85.069,-66.584,85.08,-66.604,85.136,-66.637,85.222,-66.643,85.329,-66.612,85.194,-66.556,85.165,-66.522]},{"b":[69.693,-72.047,69.918,-71.894],"l":[69.796,-71.894,69.737,-71.922,69.693,-71.968,69.744,-72.044,69.792,-72.047,69.918,-71.918,69.895,-71.908,69.796,-71.894]},{"b":[68.409,-72.3,68.84,-72.089],"l":[68.667,-72.103,68.566,-72.19,68.436,-72.26,68.409,-72.3,68.462,-72.3,68.67,-72.276,68.817,-72.229,68.84,-72.165,68.729,-72.089,68.667,-72.103]},{"b":[96.307,-66.225,97.019,-66.036],"l":[96.727,-66.061,96.613,-66.036,96.5,-66.046,96.399,-66.08,96.307,-66.186,96.395,-66.225,96.934,-66.201,97.016,-66.164,97.019,-66.139,97.006,-66.097,96.932,-66.058,96.727,-66.061]},{"b":[71.637,-70.633,72.097,-70.264],"l":[72.073,-70.525,72.056,-70.501,72,-70.457,71.88,-70.406,71.851,-70.368,71.838,-70.312,71.797,-70.264,71.705,-70.284,71.647,-70.336,71.637,-70.444,71.659,-70.497,71.726,-70.549,71.841,-70.622,71.929,-70.633,72.002,-70.633,72.078,-70.609,72.097,-70.575,72.073,-70.525]},{"b":[162.297,-66.525,162.611,-66.251],"l":[162.311,-66.251,162.302,-66.265,162.297,-66.304,162.326,-66.347,162.303,-66.4,162.511,-66.52,162.557,-66.525,162.611,-66.477,162.563,-66.433,162.311,-66.251]},{"b":[100.076,-66.229,100.291,-66.112],"l":[100.174,-66.131,100.076,-66.188,100.082,-66.203,100.133,-66.229,100.265,-66.217,100.282,-66.18,100.291,-66.112,100.174,-66.131]},{"b":[98.596,-66.482,98.95,-66.369],"l":[98.655,-66.453,98.752,-66.482,98.846,-66.47,98.95,-66.421,98.749,-66.369,98.596,-66.383,98.605,-66.4,98.655,-66.453]},{"b":[102.76,-65.469,103.397,-65.13],"l":[103.186,-65.331,103.182,-65.308,103.191,-65.237,103.137,-65.191,102.893,-65.13,102.796,-65.136,102.76,-65.168,102.789,-65.236,103.054,-65.285,103.113,-65.312,103.124,-65.338,103.138,-65.435,103.176,-65.455,103.337,-65.469,103.397,-65.445,103.379,-65.426,103.261,-65.377,103.186,-65.331]},{"b":[-148.001,-76.663,-147.579,-76.558],"l":[-147.579,-76.663,-147.588,-76.65,-147.649,-76.611,-147.77,-76.577,-147.9,-76.558,-148.001,-76.577,-147.954,-76.597,-147.73,-76.653,-147.579,-76.663]},{"b":[-146.151,-75.716,-145.238,-75.505],"l":[-145.761,-75.514,-145.896,-75.505,-146.076,-75.533,-146.151,-75.574,-146.043,-75.612,-145.541,-75.693,-145.348,-75.716,-145.238,-75.711,-145.252,-75.683,-145.315,-75.641,-145.417,-75.588,-145.761,-75.514]},{"b":[-147.116,-77.006,-146.164,-76.837],"l":[-146.867,-76.837,-147.087,-76.837,-147.116,-76.866,-147.101,-76.887,-147.044,-76.93,-147.079,-76.993,-146.981,-77.006,-146.607,-76.961,-146.164,-76.949,-146.244,-76.883,-146.867,-76.837]},{"b":[-132.857,-74.498,-132.362,-74.387],"l":[-132.832,-74.422,-132.857,-74.462,-132.546,-74.498,-132.391,-74.442,-132.362,-74.41,-132.552,-74.387,-132.832,-74.422]},{"b":[-149.303,-77.019,-148.44,-76.9],"l":[-149.238,-76.9,-149.303,-76.916,-149.245,-76.993,-149.015,-77.019,-148.596,-77.007,-148.474,-76.998,-148.44,-76.977,-148.509,-76.955,-148.704,-76.936,-149.238,-76.9]},{"b":[-149.469,-76.845,-148.321,-76.717],"l":[-148.371,-76.795,-148.321,-76.772,-148.384,-76.744,-148.663,-76.721,-148.928,-76.73,-149.333,-76.717,-149.469,-76.757,-149.238,-76.818,-148.984,-76.845,-148.815,-76.841,-148.67,-76.802,-148.371,-76.795]},{"b":[-132.163,-74.605,-130.957,-74.324],"l":[-131.234,-74.414,-131.56,-74.367,-131.594,-74.33,-131.763,-74.324,-131.938,-74.349,-132.163,-74.426,-132.049,-74.464,-132.025,-74.489,-131.952,-74.514,-131.841,-74.543,-131.598,-74.554,-131.179,-74.605,-131.067,-74.584,-130.967,-74.515,-130.957,-74.456,-130.981,-74.414,-131.234,-74.414]},{"b":[-119.905,-74.122,-118.877,-73.775],"l":[-119.549,-74.11,-119.449,-74.076,-119.059,-73.998,-118.99,-73.967,-118.877,-73.878,-118.91,-73.834,-118.959,-73.809,-119.216,-73.778,-119.516,-73.775,-119.669,-73.809,-119.803,-73.815,-119.662,-73.989,-119.694,-74.006,-119.797,-74.029,-119.905,-74.082,-119.887,-74.097,-119.821,-74.12,-119.758,-74.122,-119.549,-74.11]},{"b":[-117.398,-74.193,-116.155,-73.866],"l":[-116.381,-73.866,-117.376,-74.083,-117.398,-74.122,-117.363,-74.161,-117.23,-74.193,-116.739,-74.165,-116.571,-74.126,-116.514,-74.096,-116.534,-74.083,-116.609,-74.069,-116.585,-74.056,-116.451,-74.018,-116.155,-73.91,-116.203,-73.896,-116.381,-73.866]},{"b":[-128.133,-74.641,-127.145,-74.312],"l":[-127.486,-74.405,-127.853,-74.332,-128.043,-74.312,-128.133,-74.327,-128.096,-74.466,-128.07,-74.478,-128,-74.489,-127.915,-74.543,-127.817,-74.575,-127.518,-74.641,-127.366,-74.623,-127.232,-74.578,-127.145,-74.48,-127.23,-74.425,-127.486,-74.405]},{"b":[-149.662,-77.387,-148.929,-77.275],"l":[-149.218,-77.336,-149.249,-77.315,-149.375,-77.28,-149.519,-77.275,-149.662,-77.301,-149.439,-77.371,-148.929,-77.387,-149.218,-77.336]},{"b":[-163.796,-83.142,-161.635,-82.843],"l":[-162.798,-82.865,-163.796,-82.843,-163.735,-82.857,-163.704,-82.879,-163.634,-82.902,-163.602,-82.927,-163.602,-82.969,-163.552,-82.988,-163.348,-83.022,-163.242,-83.06,-163.047,-83.097,-162.305,-83.142,-161.994,-83.119,-161.828,-83.043,-161.635,-83.027,-162.34,-82.923,-162.411,-82.899,-162.798,-82.865]},{"b":[-150.874,-76.789,-150.085,-76.691],"l":[-150.085,-76.735,-150.104,-76.719,-150.177,-76.691,-150.838,-76.714,-150.874,-76.737,-150.83,-76.762,-150.655,-76.789,-150.233,-76.776,-150.085,-76.735]},{"b":[-147.421,-76.261,-146.69,-76.063],"l":[-147.151,-76.197,-146.894,-76.261,-146.69,-76.246,-146.949,-76.098,-147.361,-76.063,-147.418,-76.073,-147.421,-76.09,-147.408,-76.105,-147.345,-76.147,-147.151,-76.197]},{"b":[-147.355,-76.714,-146.79,-76.532],"l":[-146.947,-76.555,-147.135,-76.532,-147.279,-76.553,-147.355,-76.619,-147.221,-76.671,-146.908,-76.714,-146.79,-76.633,-146.878,-76.563,-146.947,-76.555]},{"b":[-163.951,-81.598,-160.467,-81.281],"l":[-160.467,-81.589,-160.485,-81.567,-160.617,-81.522,-160.938,-81.463,-161.559,-81.397,-162.456,-81.313,-163.201,-81.281,-163.869,-81.324,-163.93,-81.352,-163.951,-81.391,-163.939,-81.404,-163.89,-81.424,-163.766,-81.445,-163.253,-81.482,-160.571,-81.598,-160.467,-81.589]},{"b":[-151.512,-77.374,-150.356,-77.22],"l":[-151.022,-77.22,-151.218,-77.226,-151.512,-77.273,-151.344,-77.296,-150.475,-77.374,-150.397,-77.369,-150.356,-77.349,-150.499,-77.335,-151.022,-77.22]},{"b":[-155.751,-80.036,-153.93,-79.766],"l":[-153.93,-80.033,-154.025,-79.988,-154.535,-79.936,-155.162,-79.851,-155.674,-79.766,-155.751,-79.83,-155.525,-79.846,-155.045,-79.9,-154.942,-79.966,-154.529,-80,-154.349,-80.026,-154.114,-80.036,-153.93,-80.033]},{"b":[-150.789,-77.137,-149.231,-76.875],"l":[-149.293,-77.137,-149.231,-77.121,-149.288,-77.093,-149.416,-77.079,-149.442,-77.049,-149.506,-77.002,-149.743,-76.927,-149.79,-76.889,-149.871,-76.875,-150.393,-76.899,-150.476,-76.926,-150.68,-76.948,-150.789,-76.982,-150.736,-77.004,-150.462,-77.076,-149.856,-77.099,-149.817,-77.114,-149.729,-77.129,-149.293,-77.137]},{"b":[-158.989,-82.112,-157.835,-81.779],"l":[-158.261,-81.947,-158.347,-81.9,-158.914,-81.78,-158.989,-81.779,-158.926,-81.819,-158.773,-81.875,-158.545,-81.949,-158.154,-82.058,-158.076,-82.112,-157.987,-82.105,-157.835,-82.031,-158.261,-81.947]},{"b":[-6.438,-70.612,-5.894,-70.405],"l":[-6.068,-70.405,-6.244,-70.446,-6.438,-70.453,-6.266,-70.55,-6.181,-70.586,-6.156,-70.612,-5.894,-70.552,-5.95,-70.432,-5.972,-70.421,-6.068,-70.405]},{"b":[2.585,-70.624,3.26,-70.382],"l":[3.072,-70.382,2.631,-70.5,2.585,-70.535,2.623,-70.593,2.698,-70.624,3.037,-70.597,3.171,-70.554,3.221,-70.519,3.26,-70.449,3.231,-70.403,3.193,-70.393,3.072,-70.382]},{"b":[-3.404,-71.237,-2.955,-71.052],"l":[-3.399,-71.062,-3.392,-71.081,-3.404,-71.12,-3.386,-71.143,-3.309,-71.201,-3.201,-71.23,-3.061,-71.237,-2.955,-71.214,-3.191,-71.095,-3.213,-71.076,-3.263,-71.052,-3.399,-71.062]},{"b":[0.95,-70.381,1.461,-70.023],"l":[1.315,-70.023,1.027,-70.05,0.95,-70.094,0.953,-70.169,0.99,-70.224,1.105,-70.304,1.156,-70.378,1.212,-70.381,1.299,-70.255,1.461,-70.136,1.412,-70.041,1.315,-70.023]},{"b":[-3.575,-71.167,-2.092,-70.674],"l":[-2.533,-70.768,-2.75,-70.694,-3.04,-70.674,-3.537,-70.683,-3.575,-70.703,-3.489,-70.736,-3.007,-70.851,-2.975,-70.883,-2.963,-70.94,-2.801,-70.982,-2.805,-71.015,-2.822,-71.057,-2.825,-71.113,-2.783,-71.167,-2.607,-71.141,-2.369,-71.044,-2.293,-70.998,-2.213,-70.902,-2.119,-70.855,-2.092,-70.821,-2.256,-70.796,-2.423,-70.8,-2.533,-70.768]},{"b":[4.07,-70.503,4.618,-70.241],"l":[4.13,-70.417,4.18,-70.451,4.365,-70.503,4.526,-70.479,4.59,-70.433,4.618,-70.369,4.586,-70.294,4.495,-70.251,4.256,-70.241,4.112,-70.267,4.07,-70.29,4.076,-70.325,4.13,-70.417]},{"b":[48.294,-66.807,48.786,-66.701],"l":[48.638,-66.701,48.358,-66.704,48.301,-66.724,48.294,-66.75,48.296,-66.774,48.304,-66.798,48.378,-66.807,48.546,-66.784,48.775,-66.778,48.786,-66.768,48.782,-66.731,48.751,-66.72,48.638,-66.701]},{"b":[15.563,-70.072,16.625,-69.705],"l":[15.91,-69.728,15.699,-69.773,15.597,-69.828,15.563,-69.863,15.571,-69.885,15.614,-69.939,15.663,-69.955,15.845,-69.982,16.159,-70.072,16.223,-70.008,16.315,-69.844,16.625,-69.75,16.573,-69.723,16.247,-69.705,15.91,-69.728]},{"b":[25.954,-70.448,26.875,-70.061],"l":[26.686,-70.114,26.605,-70.078,26.426,-70.061,26.301,-70.072,25.983,-70.2,25.954,-70.261,25.964,-70.295,26.005,-70.373,26.358,-70.434,26.47,-70.448,26.609,-70.412,26.793,-70.419,26.857,-70.381,26.875,-70.33,26.737,-70.186,26.686,-70.114]},{"b":[-32,-80.011,-29.614,-79.634],"l":[-30.985,-79.818,-31.119,-79.798,-31.605,-79.645,-31.68,-79.634,-32,-79.732,-31.824,-79.85,-31.594,-79.888,-30.844,-79.938,-30.422,-80.011,-30.029,-79.936,-29.8,-79.926,-29.721,-79.93,-29.614,-79.91,-29.871,-79.823,-30.66,-79.733,-30.78,-79.647,-30.861,-79.726,-30.841,-79.771,-30.985,-79.818]},{"b":[-36.601,-79.357,-33.934,-79.09],"l":[-33.995,-79.279,-34.392,-79.223,-35.535,-79.09,-35.597,-79.092,-35.79,-79.149,-36.048,-79.181,-36.238,-79.196,-36.566,-79.209,-36.601,-79.283,-36.481,-79.294,-34.05,-79.357,-33.934,-79.32,-33.947,-79.305,-33.995,-79.279]},{"b":[-3.497,-70.535,-2.683,-70.28],"l":[-2.738,-70.507,-2.683,-70.462,-2.684,-70.376,-2.714,-70.32,-2.805,-70.288,-2.95,-70.28,-3.173,-70.307,-3.287,-70.344,-3.497,-70.488,-3.49,-70.508,-3.442,-70.535,-3.28,-70.534,-2.738,-70.507]},{"b":[-22.035,-74.493,-20.411,-73.619],"l":[-20.6,-74.197,-20.641,-74.151,-20.654,-74.105,-20.607,-73.887,-20.521,-73.798,-20.521,-73.712,-20.58,-73.619,-20.69,-73.625,-20.867,-73.677,-20.979,-73.79,-21.025,-73.88,-21.126,-73.94,-21.288,-73.989,-21.93,-74.057,-22.035,-74.107,-21.61,-74.092,-21.167,-74.133,-21.051,-74.176,-20.977,-74.225,-20.846,-74.438,-20.818,-74.455,-20.737,-74.481,-20.489,-74.493,-20.417,-74.443,-20.411,-74.408,-20.423,-74.317,-20.6,-74.197]},{"b":[-16.517,-72.703,-16.104,-72.459],"l":[-16.303,-72.478,-16.356,-72.459,-16.455,-72.474,-16.517,-72.531,-16.51,-72.582,-16.453,-72.652,-16.318,-72.702,-16.175,-72.703,-16.104,-72.679,-16.173,-72.6,-16.303,-72.478]},{"b":[-12.963,-72.196,-12.509,-72.001],"l":[-12.789,-72.007,-12.875,-72.001,-12.915,-72.015,-12.963,-72.064,-12.944,-72.099,-12.888,-72.137,-12.72,-72.188,-12.588,-72.196,-12.509,-72.173,-12.535,-72.14,-12.637,-72.071,-12.789,-72.007]},{"b":[-32.583,-79.683,-31.933,-79.53],"l":[-31.957,-79.604,-31.933,-79.568,-32.15,-79.53,-32.377,-79.535,-32.501,-79.592,-32.583,-79.658,-32.515,-79.683,-32.343,-79.674,-32.001,-79.607,-31.957,-79.604]},{"b":[-45.956,-60.733,-45.173,-60.521],"l":[-45.78,-60.586,-45.709,-60.645,-45.398,-60.65,-45.174,-60.733,-45.173,-60.699,-45.186,-60.672,-45.211,-60.648,-45.228,-60.64,-45.357,-60.624,-45.386,-60.583,-45.5,-60.546,-45.718,-60.521,-45.834,-60.543,-45.935,-60.527,-45.956,-60.568,-45.955,-60.597,-45.937,-60.62,-45.78,-60.586]},{"b":[-54.192,-61.308,-54.024,-61.135],"l":[-54.122,-61.202,-54.192,-61.247,-54.184,-61.27,-54.115,-61.308,-54.071,-61.299,-54.041,-61.255,-54.024,-61.135,-54.05,-61.142,-54.122,-61.202]},{"b":[-68.549,-79.501,-67.069,-79.214],"l":[-68.422,-79.333,-68.549,-79.437,-68.408,-79.464,-68.161,-79.479,-67.434,-79.501,-67.262,-79.453,-67.305,-79.394,-67.24,-79.328,-67.173,-79.312,-67.069,-79.268,-67.475,-79.223,-67.714,-79.214,-68.033,-79.227,-68.233,-79.285,-68.324,-79.298,-68.422,-79.333]},{"b":[-55.44,-61.249,-54.671,-61.073],"l":[-55.387,-61.073,-55.44,-61.106,-55.369,-61.146,-55.347,-61.212,-55.297,-61.249,-55.165,-61.22,-55.058,-61.169,-54.71,-61.14,-54.671,-61.117,-55.387,-61.073]},{"b":[-56.505,-63.437,-55.075,-63.128],"l":[-55.466,-63.2,-55.528,-63.174,-55.529,-63.157,-55.59,-63.128,-56.042,-63.157,-56.385,-63.234,-56.466,-63.283,-56.461,-63.302,-56.475,-63.318,-56.505,-63.334,-56.499,-63.358,-56.463,-63.418,-56.379,-63.437,-56.083,-63.383,-56.009,-63.342,-55.83,-63.298,-55.75,-63.297,-55.594,-63.336,-55.157,-63.353,-55.075,-63.324,-55.106,-63.249,-55.156,-63.205,-55.216,-63.199,-55.466,-63.2]},{"b":[-56.235,-63.58,-55.719,-63.407],"l":[-55.762,-63.422,-55.858,-63.407,-56.21,-63.437,-56.235,-63.469,-56.178,-63.513,-55.957,-63.58,-55.873,-63.536,-55.719,-63.492,-55.762,-63.422]},{"b":[-57.448,-64.571,-56.895,-64.333],"l":[-57.446,-64.46,-57.448,-64.488,-57.433,-64.54,-57.326,-64.571,-57.24,-64.567,-56.991,-64.468,-56.945,-64.427,-56.952,-64.382,-56.895,-64.333,-57.023,-64.352,-57.315,-64.435,-57.366,-64.439,-57.446,-64.46]},{"b":[-59.202,-62.348,-58.838,-62.239],"l":[-59.064,-62.239,-59.202,-62.283,-59.175,-62.302,-59.059,-62.348,-58.838,-62.303,-58.879,-62.268,-58.962,-62.264,-58.991,-62.249,-59.064,-62.239]},{"b":[-59.004,-62.248,-57.637,-61.912],"l":[-58.562,-62.244,-58.507,-62.226,-58.467,-62.137,-58.341,-62.119,-58.183,-62.17,-58.133,-62.146,-58.172,-62.118,-58.148,-62.063,-57.963,-62.078,-57.807,-62.012,-57.64,-62.02,-57.637,-61.998,-57.677,-61.942,-57.738,-61.921,-57.849,-61.94,-57.978,-61.912,-58.265,-61.953,-58.399,-61.938,-58.684,-62.008,-58.71,-62.045,-58.955,-62.164,-59.004,-62.21,-58.819,-62.171,-58.755,-62.206,-58.746,-62.218,-58.644,-62.225,-58.594,-62.248,-58.562,-62.244]},{"b":[-62.044,-64.08,-61.798,-63.967],"l":[-62.021,-64.027,-62.044,-64.08,-61.952,-64.077,-61.911,-64.054,-61.886,-64.027,-61.798,-63.967,-61.936,-63.99,-62.021,-64.027]},{"b":[-62.568,-69.729,-61.784,-69.146],"l":[-61.978,-69.3,-62.118,-69.215,-62.239,-69.176,-62.442,-69.146,-62.516,-69.155,-62.568,-69.18,-62.496,-69.288,-62.216,-69.495,-62.172,-69.637,-62.085,-69.729,-61.998,-69.722,-61.97,-69.691,-61.908,-69.588,-61.911,-69.533,-61.807,-69.515,-61.784,-69.442,-61.816,-69.376,-61.978,-69.3]},{"b":[-63.558,-64.907,-63.177,-64.73],"l":[-63.177,-64.739,-63.219,-64.73,-63.367,-64.792,-63.459,-64.796,-63.558,-64.906,-63.474,-64.907,-63.316,-64.861,-63.257,-64.791,-63.177,-64.739]},{"b":[-62.782,-64.514,-62.058,-64.012],"l":[-62.094,-64.235,-62.058,-64.138,-62.269,-64.09,-62.268,-64.04,-62.329,-64.013,-62.451,-64.012,-62.545,-64.046,-62.586,-64.076,-62.611,-64.116,-62.59,-64.14,-62.48,-64.211,-62.504,-64.253,-62.643,-64.392,-62.721,-64.445,-62.747,-64.472,-62.782,-64.479,-62.727,-64.496,-62.58,-64.514,-62.508,-64.454,-62.455,-64.472,-62.396,-64.465,-62.326,-64.424,-62.304,-64.401,-62.186,-64.369,-62.174,-64.296,-62.094,-64.235]},{"b":[-62.655,-63.074,-62.317,-62.874],"l":[-62.411,-62.972,-62.344,-62.918,-62.317,-62.874,-62.527,-62.924,-62.639,-63.032,-62.655,-63.074,-62.615,-63.069,-62.411,-62.972]},{"b":[-59.661,-62.451,-59.353,-62.352],"l":[-59.525,-62.451,-59.389,-62.444,-59.353,-62.413,-59.396,-62.367,-59.479,-62.352,-59.661,-62.354,-59.619,-62.395,-59.525,-62.451]},{"b":[-56.614,-63.173,-56.051,-62.982],"l":[-56.354,-63.169,-56.259,-63.173,-56.06,-63.079,-56.051,-63.055,-56.058,-63.019,-56.062,-63.013,-56.14,-63.005,-56.489,-62.982,-56.614,-63.045,-56.601,-63.062,-56.546,-63.098,-56.354,-63.169]},{"b":[-61.152,-62.745,-59.85,-62.475],"l":[-60.796,-62.662,-60.697,-62.621,-60.62,-62.633,-60.378,-62.617,-60.354,-62.679,-60.322,-62.708,-60.221,-62.745,-59.85,-62.615,-60.003,-62.618,-60.14,-62.549,-60.576,-62.573,-60.625,-62.56,-60.732,-62.491,-60.799,-62.475,-60.838,-62.534,-60.975,-62.592,-61.152,-62.589,-61.15,-62.634,-61.063,-62.679,-60.995,-62.679,-60.796,-62.662]},{"b":[-57.683,-63.879,-57.104,-63.791],"l":[-57.344,-63.879,-57.248,-63.868,-57.218,-63.876,-57.104,-63.841,-57.161,-63.816,-57.36,-63.825,-57.374,-63.807,-57.439,-63.791,-57.683,-63.813,-57.616,-63.854,-57.344,-63.879]},{"b":[-61.404,-69.978,-61.108,-69.856],"l":[-61.327,-69.856,-61.386,-69.893,-61.404,-69.933,-61.378,-69.95,-61.309,-69.978,-61.158,-69.976,-61.108,-69.955,-61.152,-69.883,-61.327,-69.856]},{"b":[-61.015,-68.795,-60.656,-68.681],"l":[-60.693,-68.795,-60.656,-68.768,-60.705,-68.722,-60.814,-68.688,-60.947,-68.681,-61.015,-68.71,-60.894,-68.759,-60.82,-68.778,-60.693,-68.795]},{"b":[-60.976,-70.711,-60.449,-70.509],"l":[-60.554,-70.509,-60.884,-70.518,-60.942,-70.533,-60.976,-70.599,-60.958,-70.629,-60.896,-70.69,-60.826,-70.711,-60.741,-70.711,-60.488,-70.647,-60.449,-70.603,-60.452,-70.544,-60.554,-70.509]},{"b":[-60.946,-71.059,-60.516,-70.914],"l":[-60.516,-71,-60.533,-70.962,-60.613,-70.92,-60.783,-70.914,-60.889,-70.934,-60.946,-70.967,-60.906,-71.007,-60.79,-71.041,-60.652,-71.059,-60.552,-71.053,-60.536,-71.041,-60.516,-71]},{"b":[-60.74,-63.018,-60.505,-62.895],"l":[-60.637,-62.895,-60.706,-62.906,-60.74,-62.949,-60.693,-62.996,-60.622,-63.018,-60.564,-63.009,-60.618,-62.987,-60.62,-62.969,-60.555,-62.978,-60.505,-62.967,-60.637,-62.895]},{"b":[-60.972,-63.902,-60.562,-63.669],"l":[-60.562,-63.696,-60.715,-63.669,-60.797,-63.717,-60.81,-63.837,-60.972,-63.849,-60.852,-63.891,-60.778,-63.902,-60.653,-63.867,-60.655,-63.85,-60.689,-63.808,-60.656,-63.759,-60.562,-63.696]},{"b":[-94.113,-72.92,-93.756,-72.82],"l":[-94.004,-72.82,-94.047,-72.823,-94.113,-72.86,-94.078,-72.884,-93.966,-72.92,-93.796,-72.92,-93.756,-72.908,-93.8,-72.882,-94.004,-72.82]},{"b":[-91.67,-73.207,-90.751,-72.547],"l":[-90.78,-72.732,-90.763,-72.681,-90.807,-72.611,-90.947,-72.556,-91.304,-72.547,-91.612,-72.594,-91.67,-72.624,-91.551,-72.754,-91.382,-72.868,-91.357,-72.909,-91.45,-72.968,-91.511,-73.196,-91.344,-73.207,-91.161,-73.182,-90.998,-73.137,-90.893,-73.084,-90.776,-72.993,-90.751,-72.917,-90.776,-72.854,-90.895,-72.824,-90.78,-72.732]},{"b":[-105.132,-73.212,-104.538,-72.941],"l":[-104.972,-72.941,-105.085,-72.966,-105.132,-72.992,-105.123,-73.026,-105.053,-73.126,-104.881,-73.201,-104.66,-73.212,-104.538,-73.166,-104.972,-72.941]},{"b":[-95.273,-72.67,-94.426,-72.468],"l":[-94.566,-72.468,-94.753,-72.517,-95.216,-72.599,-95.273,-72.647,-95.219,-72.67,-95.027,-72.665,-94.426,-72.613,-94.434,-72.589,-94.514,-72.492,-94.538,-72.476,-94.566,-72.468]},{"b":[-71.795,-70.442,-71.34,-70.265],"l":[-71.551,-70.439,-71.438,-70.392,-71.34,-70.317,-71.355,-70.298,-71.432,-70.267,-71.648,-70.295,-71.695,-70.265,-71.795,-70.288,-71.782,-70.319,-71.685,-70.442,-71.551,-70.439]},{"b":[-72.957,-69.74,-71.985,-69.413],"l":[-72.202,-69.74,-71.985,-69.698,-72.331,-69.492,-72.464,-69.452,-72.726,-69.413,-72.857,-69.433,-72.937,-69.469,-72.957,-69.529,-72.777,-69.645,-72.345,-69.707,-72.202,-69.74]},{"b":[-74.147,-73.376,-73.542,-73.1],"l":[-74.049,-73.22,-74.084,-73.249,-74.134,-73.277,-74.147,-73.315,-74.038,-73.366,-73.975,-73.376,-73.878,-73.357,-73.721,-73.296,-73.682,-73.225,-73.542,-73.124,-73.674,-73.1,-73.832,-73.113,-74.049,-73.22]},{"b":[-66.181,-65.881,-65.637,-65.527],"l":[-66.064,-65.881,-65.845,-65.842,-65.836,-65.814,-65.841,-65.738,-65.814,-65.687,-65.784,-65.674,-65.67,-65.653,-65.668,-65.626,-65.637,-65.548,-65.834,-65.527,-65.968,-65.571,-66,-65.633,-66.067,-65.666,-66.05,-65.745,-66.153,-65.774,-66.181,-65.826,-66.175,-65.867,-66.064,-65.881]},{"b":[-66.868,-66.313,-66.575,-66.067],"l":[-66.867,-66.275,-66.868,-66.294,-66.85,-66.305,-66.819,-66.313,-66.595,-66.201,-66.593,-66.179,-66.623,-66.134,-66.575,-66.082,-66.631,-66.067,-66.779,-66.111,-66.792,-66.234,-66.867,-66.275]},{"b":[-67.743,-67.785,-67.149,-67.591],"l":[-67.149,-67.65,-67.175,-67.625,-67.247,-67.599,-67.418,-67.591,-67.557,-67.604,-67.743,-67.661,-67.731,-67.679,-67.69,-67.688,-67.693,-67.763,-67.545,-67.785,-67.349,-67.766,-67.3,-67.737,-67.28,-67.712,-67.149,-67.65]},{"b":[-67.593,-66.902,-67.257,-66.737],"l":[-67.257,-66.841,-67.269,-66.815,-67.332,-66.754,-67.426,-66.737,-67.511,-66.756,-67.5,-66.804,-67.593,-66.876,-67.521,-66.897,-67.409,-66.902,-67.362,-66.895,-67.257,-66.841]},{"b":[-180,-16.963,-179.822,-16.688],"l":[-180,-16.963,-179.975,-16.925,-179.868,-16.85,-179.822,-16.765,-179.861,-16.688,-179.894,-16.7,-180,-16.786]},{"b":[-180,-16.54,-179.901,-16.432],"l":[-180,-16.54,-179.929,-16.503,-179.927,-16.479,-179.901,-16.432,-179.944,-16.441,-180,-16.489]},{"b":[179.897,-17,180,-16.786],"l":[179.999,-16.963,179.926,-17,179.897,-16.964,179.931,-16.876,180,-16.786]},{"b":[-78.965,8.232,-78.839,8.46],"l":[-78.839,8.348,-78.853,8.302,-78.898,8.274,-78.918,8.232,-78.965,8.326,-78.957,8.351,-78.961,8.436,-78.916,8.458,-78.883,8.46,-78.856,8.448,-78.839,8.348]},{"b":[154.611,49.268,154.9,49.647],"l":[154.613,49.381,154.611,49.294,154.715,49.268,154.81,49.312,154.83,49.348,154.802,49.468,154.883,49.566,154.9,49.63,154.825,49.647,154.613,49.381]},{"b":[58.61,81.292,59.375,81.398],"l":[58.61,81.337,58.634,81.36,58.881,81.392,59.075,81.398,59.281,81.366,59.375,81.325,59.313,81.305,59.097,81.292,58.719,81.314,58.61,81.337]},{"b":[53.812,80.421,54.437,80.605],"l":[53.902,80.542,53.859,80.563,53.877,80.605,54.177,80.574,54.205,80.562,54.407,80.54,54.437,80.499,54.415,80.473,54.276,80.421,53.812,80.476,53.85,80.504,53.9,80.515,53.902,80.542]},{"b":[60.058,80.93,61.567,81.104],"l":[60.587,81.088,61.457,81.104,61.567,81.05,61.472,81.011,61.141,80.95,60.827,80.93,60.321,80.956,60.058,80.985,60.078,80.999,60.148,81.017,60.587,81.088]},{"b":[52.213,80.173,53.852,80.413],"l":[52.344,80.213,52.213,80.264,52.27,80.276,52.577,80.297,52.681,80.319,52.716,80.348,52.854,80.402,53.186,80.413,53.329,80.402,53.346,80.366,53.486,80.323,53.852,80.268,53.778,80.228,53.653,80.223,53.521,80.185,52.856,80.173,52.636,80.179,52.607,80.191,52.55,80.202,52.344,80.213]},{"b":[55.724,80.071,57.123,80.366],"l":[55.942,80.163,56.012,80.204,55.99,80.32,56.024,80.341,56.655,80.33,56.707,80.363,56.945,80.366,57.079,80.351,57.123,80.317,57.119,80.194,57.073,80.139,57.08,80.095,56.987,80.071,56.201,80.076,55.812,80.087,55.724,80.105,55.942,80.163]},{"b":[58.919,79.923,59.911,80.119],"l":[58.946,80.042,59.001,80.054,59.545,80.119,59.802,80.083,59.911,79.994,59.689,79.956,59.331,79.923,59.203,79.933,59.169,79.948,59.1,79.964,58.919,79.985,58.946,80.042]},{"b":[-66.637,-55.273,-66.436,-55.166],"l":[-66.542,-55.169,-66.6,-55.194,-66.625,-55.213,-66.637,-55.234,-66.63,-55.254,-66.611,-55.27,-66.552,-55.273,-66.472,-55.229,-66.436,-55.19,-66.523,-55.166,-66.542,-55.169]},{"b":[-75.142,-44.907,-75.032,-44.795],"l":[-75.124,-44.87,-75.099,-44.902,-75.067,-44.907,-75.042,-44.89,-75.032,-44.871,-75.048,-44.824,-75.079,-44.795,-75.107,-44.795,-75.142,-44.816,-75.124,-44.87]},{"b":[-75.651,-48.765,-75.156,-48.02],"l":[-75.225,-48.671,-75.434,-48.721,-75.51,-48.763,-75.623,-48.765,-75.651,-48.586,-75.518,-48.329,-75.509,-48.231,-75.554,-48.157,-75.571,-48.096,-75.561,-48.071,-75.391,-48.02,-75.338,-48.074,-75.275,-48.218,-75.156,-48.425,-75.158,-48.623,-75.225,-48.671]},{"b":[-75.477,-50.772,-75.115,-50.481],"l":[-75.304,-50.484,-75.428,-50.481,-75.42,-50.53,-75.443,-50.596,-75.477,-50.654,-75.453,-50.683,-75.439,-50.741,-75.411,-50.764,-75.33,-50.772,-75.302,-50.68,-75.292,-50.597,-75.203,-50.581,-75.16,-50.554,-75.115,-50.51,-75.156,-50.497,-75.304,-50.484]},{"b":[-74.712,-53.358,-73.135,-52.734],"l":[-73.31,-53.248,-73.451,-53.144,-73.505,-53.14,-73.549,-53.126,-73.654,-53.07,-73.782,-53.056,-73.879,-53.012,-74.066,-52.965,-74.275,-52.946,-74.33,-52.929,-74.369,-52.931,-74.386,-52.922,-74.422,-52.86,-74.475,-52.836,-74.572,-52.771,-74.67,-52.734,-74.712,-52.749,-74.712,-52.768,-74.62,-52.835,-74.558,-52.922,-74.414,-52.995,-74.27,-53.082,-74.236,-53.076,-74.139,-53.091,-73.994,-53.076,-73.867,-53.097,-73.794,-53.121,-73.617,-53.23,-73.596,-53.253,-73.583,-53.3,-73.567,-53.307,-73.501,-53.318,-73.409,-53.321,-73.226,-53.358,-73.135,-53.354,-73.143,-53.341,-73.302,-53.259,-73.31,-53.248]},{"b":[-69.988,-55.651,-68.046,-54.882],"l":[-68.305,-55.357,-68.327,-55.333,-68.323,-55.308,-68.283,-55.255,-68.33,-55.219,-68.382,-55.192,-68.586,-55.178,-68.613,-55.156,-68.598,-55.128,-68.4,-55.042,-68.458,-54.96,-68.654,-54.958,-68.901,-55.018,-69.703,-54.919,-69.884,-54.882,-69.921,-55.061,-69.947,-55.111,-69.988,-55.131,-69.98,-55.147,-69.887,-55.174,-69.866,-55.191,-69.854,-55.22,-69.824,-55.237,-69.68,-55.219,-69.657,-55.229,-69.656,-55.298,-69.646,-55.321,-69.61,-55.34,-69.509,-55.371,-69.456,-55.424,-69.412,-55.444,-69.241,-55.477,-69.181,-55.475,-69.299,-55.369,-69.359,-55.301,-69.356,-55.274,-69.297,-55.166,-69.193,-55.172,-69.151,-55.183,-69.047,-55.244,-69.008,-55.256,-68.913,-55.239,-68.89,-55.241,-68.889,-55.263,-68.932,-55.347,-68.931,-55.371,-68.896,-55.424,-68.867,-55.45,-68.785,-55.436,-68.694,-55.452,-68.594,-55.45,-68.467,-55.489,-68.338,-55.505,-68.293,-55.521,-68.23,-55.602,-68.157,-55.634,-68.083,-55.651,-68.048,-55.643,-68.046,-55.587,-68.058,-55.518,-68.09,-55.478,-68.153,-55.437,-68.305,-55.357]},{"b":[-75.641,-49.231,-75.107,-48.773],"l":[-75.115,-48.916,-75.107,-48.837,-75.119,-48.773,-75.236,-48.779,-75.297,-48.811,-75.49,-48.85,-75.535,-48.838,-75.583,-48.859,-75.619,-48.886,-75.638,-48.943,-75.576,-48.981,-75.54,-48.988,-75.515,-49.01,-75.488,-49.082,-75.573,-49.139,-75.641,-49.195,-75.58,-49.23,-75.506,-49.231,-75.39,-49.159,-75.263,-49.069,-75.115,-48.916]},{"b":[-60.276,-51.396,-60.07,-51.273],"l":[-60.249,-51.396,-60.276,-51.363,-60.275,-51.281,-60.171,-51.273,-60.07,-51.308,-60.076,-51.343,-60.112,-51.396,-60.249,-51.396]},{"b":[-26.451,-58.492,-26.26,-58.382],"l":[-26.303,-58.382,-26.401,-58.383,-26.451,-58.415,-26.415,-58.44,-26.26,-58.492,-26.264,-58.435,-26.279,-58.402,-26.303,-58.382]},{"b":[53.316,12.319,54.511,12.716],"l":[54.187,12.664,53.919,12.659,53.825,12.625,53.763,12.637,53.638,12.707,53.535,12.716,53.431,12.664,53.404,12.633,53.388,12.602,53.316,12.533,53.499,12.425,53.598,12.342,53.719,12.319,54.129,12.361,54.271,12.447,54.414,12.483,54.45,12.523,54.511,12.553,54.187,12.664]},{"b":[53.191,24.253,53.445,24.411],"l":[53.445,24.371,53.412,24.411,53.371,24.364,53.333,24.342,53.191,24.291,53.258,24.253,53.332,24.259,53.383,24.281,53.409,24.308,53.445,24.371]},{"b":[58.641,20.204,58.951,20.681],"l":[58.788,20.497,58.641,20.337,58.641,20.211,58.659,20.204,58.722,20.219,58.772,20.267,58.835,20.424,58.951,20.516,58.884,20.681,58.788,20.497]},{"b":[52.582,24.281,52.658,24.377],"l":[52.6,24.281,52.617,24.289,52.658,24.333,52.629,24.377,52.584,24.352,52.582,24.335,52.6,24.281]},{"b":[55.295,26.583,56.279,27.003],"l":[56.074,26.983,55.907,26.91,55.758,26.948,55.747,26.931,55.785,26.857,55.763,26.812,55.532,26.71,55.347,26.648,55.296,26.658,55.295,26.639,55.312,26.593,55.34,26.586,55.424,26.583,55.543,26.618,55.675,26.686,55.747,26.692,55.848,26.731,55.894,26.732,55.954,26.701,56.095,26.801,56.188,26.921,56.279,26.952,56.214,27.003,56.074,26.983]},{"b":[53.634,24.136,53.928,24.259],"l":[53.799,24.136,53.826,24.153,53.928,24.143,53.928,24.177,53.894,24.215,53.834,24.259,53.69,24.211,53.634,24.17,53.716,24.145,53.799,24.136]},{"b":[41.776,16.571,42.17,17.003],"l":[42.06,16.804,42.026,16.758,42.033,16.742,42.065,16.71,41.988,16.716,41.947,16.749,41.963,16.779,41.954,16.806,41.948,16.936,41.917,16.994,41.86,17.003,41.885,16.947,41.858,16.893,41.816,16.86,41.776,16.847,41.802,16.779,41.897,16.684,41.964,16.653,42.072,16.671,42.102,16.644,42.108,16.618,42.128,16.595,42.158,16.571,42.167,16.596,42.17,16.709,42.06,16.804]},{"b":[39.94,15.577,40.408,15.889],"l":[40.25,15.703,40.234,15.666,40.211,15.648,40.183,15.643,40.141,15.696,40.132,15.795,40.097,15.838,40.043,15.875,39.957,15.889,40,15.828,39.979,15.807,39.945,15.789,39.94,15.745,40.016,15.733,40.071,15.677,40.063,15.666,40.024,15.656,39.947,15.696,39.975,15.612,40.095,15.591,40.196,15.598,40.305,15.577,40.399,15.58,40.408,15.629,40.25,15.703]},{"b":[42.69,13.674,42.794,13.769],"l":[42.69,13.674,42.756,13.704,42.794,13.766,42.781,13.769,42.735,13.753,42.69,13.674]},{"b":[50.452,25.807,50.617,26.246],"l":[50.47,26.229,50.452,26.191,50.489,26.058,50.466,25.966,50.544,25.833,50.575,25.807,50.607,25.883,50.617,26.002,50.61,26.124,50.558,26.198,50.586,26.241,50.564,26.246,50.47,26.229]},{"b":[42.694,13.95,42.798,14.067],"l":[42.694,14.008,42.756,13.955,42.774,13.95,42.787,13.971,42.798,14.012,42.762,14.067,42.694,14.008]},{"b":[48.081,29.602,48.348,29.979],"l":[48.348,29.783,48.228,29.936,48.185,29.979,48.159,29.96,48.139,29.897,48.12,29.886,48.113,29.87,48.115,29.849,48.081,29.799,48.143,29.665,48.18,29.611,48.218,29.602,48.275,29.624,48.34,29.695,48.347,29.72,48.348,29.783]},{"b":[179.373,-17.394,179.447,-17.256],"l":[179.373,-17.256,179.389,-17.394,179.422,-17.367,179.447,-17.306,179.433,-17.272,179.408,-17.257,179.373,-17.256]},{"b":[179.254,-18.11,179.362,-17.944],"l":[179.306,-17.944,179.272,-17.971,179.256,-17.999,179.254,-18.031,179.34,-18.11,179.349,-18.102,179.362,-18.065,179.338,-17.99,179.306,-17.944]},{"b":[178.748,-17.747,178.853,-17.619],"l":[178.748,-17.686,178.776,-17.747,178.828,-17.729,178.853,-17.681,178.831,-17.619,178.787,-17.624,178.748,-17.686]},{"b":[168.124,-16.835,168.477,-16.594],"l":[168.323,-16.788,168.46,-16.835,168.477,-16.794,168.447,-16.779,168.366,-16.759,168.296,-16.684,168.265,-16.671,168.234,-16.64,168.199,-16.594,168.182,-16.6,168.135,-16.637,168.124,-16.69,168.149,-16.766,168.181,-16.804,168.212,-16.806,168.323,-16.788]},{"b":[167.929,-16.347,168.298,-16.082],"l":[168.297,-16.337,168.298,-16.299,168.276,-16.265,168.235,-16.231,168.198,-16.12,168.164,-16.082,168.064,-16.181,167.985,-16.196,167.929,-16.229,167.957,-16.272,168.022,-16.316,168.182,-16.347,168.297,-16.337]},{"b":[168.158,-17.807,168.585,-17.542],"l":[168.446,-17.542,168.341,-17.552,168.32,-17.544,168.297,-17.545,168.273,-17.552,168.191,-17.645,168.158,-17.711,168.182,-17.717,168.233,-17.698,168.278,-17.706,168.306,-17.746,168.252,-17.781,168.399,-17.807,168.525,-17.798,168.585,-17.696,168.545,-17.685,168.446,-17.542]},{"b":[167.674,-15.482,168.003,-15.283],"l":[168.003,-15.283,167.826,-15.312,167.674,-15.452,167.72,-15.477,167.844,-15.482,167.911,-15.436,168.003,-15.283]},{"b":[166.526,-15.635,167.201,-14.637],"l":[167.182,-15.39,167.132,-15.135,167.106,-15.126,167.069,-15.072,167.054,-14.974,167.076,-14.936,167.027,-14.923,166.987,-14.94,166.968,-15.062,166.923,-15.139,166.885,-15.157,166.81,-15.157,166.746,-14.827,166.663,-14.735,166.608,-14.637,166.567,-14.642,166.526,-14.76,166.527,-14.85,166.648,-15.212,166.631,-15.406,166.699,-15.516,166.759,-15.567,166.758,-15.631,166.826,-15.635,166.937,-15.578,167.094,-15.581,167.2,-15.486,167.201,-15.443,167.182,-15.39]},{"b":[168.123,-15.972,168.268,-15.462],"l":[168.16,-15.462,168.123,-15.681,168.179,-15.926,168.196,-15.972,168.213,-15.97,168.256,-15.955,168.268,-15.892,168.183,-15.508,168.16,-15.462]},{"b":[168.104,-15.391,168.189,-14.986],"l":[168.13,-15.319,168.172,-15.391,168.189,-15.329,168.187,-15.197,168.136,-14.986,168.115,-14.989,168.104,-15.017,168.13,-15.319]},{"b":[167.151,-16.574,167.837,-15.877],"l":[167.793,-16.395,167.776,-16.341,167.714,-16.314,167.681,-16.261,167.642,-16.263,167.499,-16.166,167.484,-16.118,167.459,-16.118,167.413,-16.096,167.336,-15.917,167.254,-15.877,167.2,-15.885,167.183,-15.929,167.151,-16.08,167.218,-16.155,167.246,-16.15,167.316,-16.116,167.349,-16.154,167.38,-16.246,167.401,-16.401,167.447,-16.502,167.436,-16.515,167.449,-16.555,167.526,-16.574,167.611,-16.499,167.76,-16.516,167.837,-16.45,167.793,-16.395]},{"b":[167.095,-15.75,167.234,-15.623],"l":[167.234,-15.645,167.119,-15.623,167.095,-15.685,167.201,-15.75,167.219,-15.724,167.234,-15.645]},{"b":[158.457,-8.557,159.881,-7.545],"l":[159.794,-8.406,159.843,-8.327,159.431,-8.029,159.368,-7.994,159.287,-7.976,159.198,-7.91,159.109,-7.904,159.011,-7.837,158.972,-7.789,158.863,-7.722,158.734,-7.604,158.457,-7.545,158.479,-7.577,158.565,-7.651,158.597,-7.759,158.686,-7.818,158.778,-7.907,158.832,-7.927,158.855,-7.96,158.944,-8.041,159.09,-8.103,159.239,-8.196,159.292,-8.203,159.354,-8.26,159.645,-8.372,159.746,-8.474,159.881,-8.557,159.879,-8.534,159.849,-8.463,159.794,-8.406]},{"b":[156.453,-7.426,157.519,-6.609],"l":[157.412,-7.309,157.336,-7.28,157.193,-7.16,157.148,-7.114,157.103,-6.957,157.03,-6.892,156.765,-6.764,156.604,-6.641,156.479,-6.609,156.453,-6.638,156.457,-6.715,156.495,-6.762,156.696,-6.911,156.904,-7.18,157.102,-7.324,157.243,-7.353,157.315,-7.342,157.317,-7.359,157.339,-7.393,157.441,-7.426,157.519,-7.366,157.487,-7.33,157.452,-7.314,157.412,-7.309]},{"b":[153.592,-4.123,153.663,-4.041],"l":[153.592,-4.096,153.65,-4.123,153.659,-4.099,153.663,-4.041,153.64,-4.045,153.592,-4.096]},{"b":[120.876,5.87,121.415,6.096],"l":[121.283,6.022,121.214,6.004,121.159,6.076,121.038,6.096,120.898,6.007,120.876,5.953,120.931,5.896,121.019,5.923,121.083,5.893,121.218,5.943,121.294,5.87,121.411,5.94,121.415,5.965,121.392,6.002,121.283,6.022]},{"b":[114.299,-7.173,114.413,-7.073],"l":[114.299,-7.098,114.347,-7.163,114.398,-7.173,114.413,-7.133,114.384,-7.081,114.349,-7.073,114.322,-7.08,114.299,-7.098]},{"b":[134.182,34.423,134.372,34.534],"l":[134.182,34.519,134.188,34.496,134.238,34.467,134.252,34.423,134.315,34.469,134.333,34.464,134.352,34.484,134.372,34.522,134.326,34.534,134.182,34.519]},{"b":[-75.261,-47.851,-74.916,-47.691],"l":[-75.084,-47.825,-75.051,-47.8,-74.916,-47.757,-74.926,-47.723,-75.004,-47.695,-75.09,-47.691,-75.203,-47.728,-75.261,-47.764,-75.194,-47.818,-75.186,-47.851,-75.112,-47.838,-75.084,-47.825]},{"b":[-74.843,-43.625,-74.665,-43.536],"l":[-74.665,-43.6,-74.673,-43.577,-74.697,-43.553,-74.745,-43.536,-74.818,-43.549,-74.842,-43.57,-74.843,-43.596,-74.81,-43.625,-74.669,-43.608,-74.665,-43.6]},{"b":[-73.818,-44.833,-73.617,-44.544],"l":[-73.628,-44.681,-73.641,-44.611,-73.686,-44.546,-73.724,-44.544,-73.779,-44.559,-73.817,-44.614,-73.818,-44.652,-73.8,-44.684,-73.735,-44.752,-73.725,-44.797,-73.695,-44.831,-73.665,-44.833,-73.632,-44.821,-73.617,-44.753,-73.628,-44.681]},{"b":[-75.247,-48.626,-74.567,-47.839],"l":[-74.567,-48.592,-74.618,-48.425,-74.6,-48.393,-74.602,-48.37,-74.615,-48.343,-74.664,-48.299,-74.702,-48.206,-74.715,-48.146,-74.729,-48.126,-74.805,-48.078,-74.846,-48.021,-74.827,-47.85,-74.896,-47.839,-74.975,-47.923,-75.198,-47.975,-75.247,-48.027,-75.234,-48.053,-75.213,-48.142,-75.158,-48.225,-75.132,-48.279,-75.079,-48.362,-75.052,-48.391,-75.013,-48.536,-74.923,-48.626,-74.71,-48.601,-74.586,-48.616,-74.567,-48.592]},{"b":[-64.757,-54.903,-63.815,-54.716],"l":[-64.055,-54.73,-64.105,-54.722,-64.221,-54.722,-64.439,-54.739,-64.549,-54.716,-64.581,-54.753,-64.625,-54.774,-64.689,-54.775,-64.757,-54.827,-64.731,-54.863,-64.637,-54.903,-64.509,-54.84,-64.453,-54.84,-64.323,-54.796,-64.028,-54.793,-63.971,-54.811,-63.833,-54.768,-63.815,-54.725,-63.882,-54.723,-64.032,-54.742,-64.055,-54.73]},{"b":[-62.093,-39.235,-61.866,-39.086],"l":[-61.967,-39.112,-62.093,-39.086,-62.083,-39.11,-62.042,-39.167,-61.918,-39.227,-61.866,-39.235,-61.876,-39.172,-61.907,-39.136,-61.967,-39.112]},{"b":[-59.793,-52.251,-59.681,-52.134],"l":[-59.753,-52.141,-59.793,-52.134,-59.786,-52.156,-59.785,-52.205,-59.764,-52.242,-59.747,-52.251,-59.683,-52.232,-59.681,-52.18,-59.753,-52.141]},{"b":[-61.145,-51.946,-60.876,-51.786],"l":[-60.876,-51.794,-60.947,-51.8,-61.019,-51.786,-61.052,-51.814,-61.145,-51.839,-61.116,-51.875,-61.032,-51.942,-60.948,-51.946,-60.916,-51.897,-60.876,-51.794]},{"b":[-73.845,-54.127,-72.205,-53.394],"l":[-72.471,-54.028,-72.409,-54.004,-72.369,-53.981,-72.366,-53.941,-72.306,-53.862,-72.205,-53.807,-72.307,-53.725,-72.373,-53.687,-72.459,-53.599,-72.482,-53.588,-72.685,-53.558,-72.809,-53.565,-72.882,-53.578,-72.896,-53.563,-72.923,-53.482,-72.947,-53.442,-72.971,-53.423,-73.022,-53.415,-73.054,-53.394,-73.074,-53.397,-73.111,-53.425,-73.115,-53.448,-73.099,-53.512,-73.366,-53.47,-73.447,-53.41,-73.687,-53.427,-73.845,-53.546,-73.642,-53.57,-73.582,-53.655,-73.471,-53.736,-73.36,-53.724,-73.325,-53.723,-73.314,-53.729,-73.295,-53.792,-73.293,-53.836,-73.312,-53.92,-73.305,-53.944,-73.211,-53.986,-73.12,-54.009,-73.081,-53.998,-73.071,-53.978,-73.086,-53.916,-73.073,-53.875,-73.039,-53.833,-72.984,-53.861,-72.936,-53.861,-72.872,-53.849,-72.764,-53.865,-72.782,-53.955,-72.882,-54.042,-72.959,-54.066,-72.946,-54.092,-72.907,-54.115,-72.871,-54.127,-72.84,-54.125,-72.789,-54.103,-72.677,-54.079,-72.563,-54.074,-72.471,-54.028]},{"b":[-68.301,-55.293,-67.08,-54.929],"l":[-67.737,-55.256,-67.691,-55.243,-67.585,-55.192,-67.535,-55.179,-67.495,-55.177,-67.463,-55.182,-67.443,-55.201,-67.429,-55.237,-67.399,-55.272,-67.34,-55.293,-67.257,-55.282,-67.173,-55.243,-67.109,-55.192,-67.08,-55.154,-67.085,-55.115,-67.107,-55.064,-67.245,-54.978,-67.425,-54.969,-67.874,-54.93,-68.107,-54.929,-68.301,-54.981,-68.174,-55.071,-68.135,-55.173,-68.1,-55.207,-68.07,-55.221,-67.768,-55.26,-67.737,-55.256]},{"b":[-71.437,-55.177,-70.283,-54.835],"l":[-70.283,-55.066,-70.418,-54.909,-70.535,-54.921,-70.615,-54.946,-70.749,-54.953,-70.805,-54.968,-70.928,-54.943,-70.945,-54.931,-70.992,-54.868,-71.089,-54.867,-71.197,-54.844,-71.374,-54.835,-71.411,-54.839,-71.437,-54.889,-71.427,-54.914,-71.407,-54.931,-71.389,-54.934,-71.325,-54.914,-71.299,-54.892,-71.274,-54.887,-71.203,-54.893,-71.12,-54.938,-70.991,-54.99,-70.967,-55.007,-70.965,-55.04,-70.94,-55.062,-70.815,-55.08,-70.744,-55.104,-70.711,-55.107,-70.641,-55.085,-70.597,-55.082,-70.551,-55.112,-70.539,-55.135,-70.543,-55.161,-70.476,-55.177,-70.404,-55.166,-70.298,-55.114,-70.283,-55.066]},{"b":[-67.563,-55.785,-67.262,-55.585],"l":[-67.289,-55.777,-67.267,-55.763,-67.262,-55.744,-67.31,-55.689,-67.351,-55.612,-67.374,-55.589,-67.397,-55.585,-67.449,-55.641,-67.513,-55.662,-67.546,-55.684,-67.563,-55.708,-67.56,-55.725,-67.393,-55.753,-67.352,-55.766,-67.325,-55.785,-67.289,-55.777]},{"b":[-75.3,-51.625,-74.559,-51.207],"l":[-74.593,-51.387,-74.561,-51.361,-74.559,-51.277,-74.571,-51.245,-74.612,-51.207,-74.737,-51.208,-74.881,-51.279,-75.04,-51.318,-75.154,-51.279,-75.21,-51.383,-75.238,-51.454,-75.3,-51.556,-75.289,-51.625,-75.192,-51.567,-75.146,-51.524,-75.047,-51.398,-74.937,-51.428,-74.853,-51.434,-74.797,-51.412,-74.731,-51.367,-74.691,-51.37,-74.62,-51.396,-74.593,-51.387]},{"b":[-75.449,-50.376,-74.839,-50.012],"l":[-74.963,-50.237,-74.839,-50.197,-74.876,-50.11,-75.004,-50.089,-75.123,-50.055,-75.21,-50.045,-75.327,-50.012,-75.369,-50.113,-75.377,-50.168,-75.398,-50.193,-75.412,-50.257,-75.449,-50.343,-75.308,-50.343,-75.25,-50.376,-75.055,-50.296,-74.963,-50.237]},{"b":[-75.105,-52.279,-74.532,-51.63],"l":[-74.537,-51.965,-74.647,-51.866,-74.75,-51.852,-74.78,-51.825,-74.823,-51.63,-74.91,-51.65,-74.915,-51.738,-75.008,-51.724,-75.105,-51.789,-75.051,-51.904,-75.017,-52.038,-74.918,-52.152,-74.852,-52.271,-74.694,-52.279,-74.666,-52.16,-74.532,-51.992,-74.537,-51.965]},{"b":[-74.143,-43.922,-73.79,-43.784],"l":[-73.938,-43.914,-73.904,-43.875,-73.834,-43.883,-73.79,-43.876,-73.811,-43.827,-73.841,-43.789,-73.857,-43.784,-73.967,-43.817,-74.14,-43.821,-74.143,-43.872,-74.118,-43.887,-73.956,-43.922,-73.938,-43.914]},{"b":[-72.21,-54.374,-71.005,-53.885],"l":[-71.671,-54.225,-71.558,-54.246,-71.473,-54.231,-71.305,-54.314,-71.143,-54.374,-71.118,-54.366,-71.083,-54.316,-71.028,-54.281,-71.005,-54.247,-71.023,-54.162,-71.022,-54.112,-71.169,-54.113,-71.39,-54.033,-71.554,-53.956,-71.705,-53.923,-71.996,-53.885,-72.069,-53.921,-72.146,-53.939,-72.21,-54.048,-72.092,-54.119,-71.972,-54.207,-71.949,-54.301,-71.818,-54.276,-71.761,-54.23,-71.671,-54.225]},{"b":[-74.475,-51.942,-74.115,-51.725],"l":[-74.283,-51.919,-74.172,-51.942,-74.142,-51.931,-74.119,-51.911,-74.115,-51.888,-74.133,-51.871,-74.277,-51.812,-74.326,-51.77,-74.362,-51.751,-74.451,-51.725,-74.475,-51.726,-74.437,-51.791,-74.424,-51.845,-74.339,-51.898,-74.283,-51.919]},{"b":[-74.618,-45.345,-73.703,-44.135],"l":[-73.727,-45.119,-73.786,-45.034,-73.795,-44.979,-73.792,-44.946,-73.828,-44.84,-73.877,-44.729,-73.919,-44.655,-74.002,-44.591,-73.996,-44.538,-73.983,-44.495,-73.862,-44.445,-73.785,-44.437,-73.735,-44.395,-73.704,-44.325,-73.703,-44.274,-73.818,-44.235,-73.865,-44.185,-73.9,-44.135,-73.995,-44.14,-74.083,-44.186,-74.108,-44.276,-74.097,-44.389,-74.133,-44.416,-74.212,-44.427,-74.301,-44.396,-74.422,-44.435,-74.502,-44.474,-74.481,-44.585,-74.618,-44.648,-74.499,-44.748,-74.419,-44.865,-74.35,-44.911,-74.268,-45.059,-74.195,-45.145,-74.089,-45.196,-74.099,-45.325,-74.016,-45.345,-73.849,-45.341,-73.834,-45.327,-73.83,-45.283,-73.771,-45.277,-73.752,-45.267,-73.728,-45.196,-73.722,-45.158,-73.727,-45.119]},{"b":[-74.69,-45.757,-74.229,-45.173],"l":[-74.229,-45.611,-74.24,-45.575,-74.315,-45.464,-74.285,-45.277,-74.311,-45.173,-74.422,-45.203,-74.45,-45.253,-74.502,-45.285,-74.495,-45.426,-74.558,-45.526,-74.646,-45.6,-74.69,-45.663,-74.678,-45.739,-74.562,-45.722,-74.466,-45.757,-74.368,-45.736,-74.313,-45.692,-74.244,-45.654,-74.229,-45.611]},{"b":[-73.961,45.441,-73.476,45.705],"l":[-73.687,45.561,-73.853,45.516,-73.961,45.441,-73.92,45.442,-73.775,45.468,-73.644,45.449,-73.567,45.469,-73.552,45.49,-73.539,45.546,-73.476,45.705,-73.522,45.701,-73.687,45.561]},{"b":[-73.858,45.565,-73.572,45.694],"l":[-73.695,45.585,-73.572,45.694,-73.725,45.672,-73.858,45.574,-73.816,45.565,-73.695,45.585]},{"b":[-61.082,45.482,-60.912,45.577],"l":[-61.026,45.577,-61.082,45.558,-61.076,45.537,-61.012,45.496,-61.003,45.482,-60.962,45.49,-60.953,45.516,-60.912,45.567,-61.026,45.577]},{"b":[-62.008,47.219,-61.396,47.647],"l":[-61.476,47.564,-61.396,47.638,-61.474,47.647,-61.548,47.632,-61.628,47.594,-61.827,47.469,-61.925,47.425,-62.008,47.234,-61.951,47.219,-61.834,47.223,-61.773,47.26,-61.815,47.268,-61.879,47.266,-61.914,47.285,-61.887,47.345,-61.831,47.392,-61.751,47.431,-61.684,47.499,-61.582,47.56,-61.476,47.564]},{"b":[-64.685,47.748,-64.509,47.887],"l":[-64.565,47.866,-64.591,47.872,-64.663,47.863,-64.66,47.794,-64.685,47.754,-64.665,47.748,-64.621,47.752,-64.534,47.814,-64.509,47.887,-64.565,47.866]},{"b":[-64.591,47.907,-64.476,48.014],"l":[-64.591,47.907,-64.476,47.959,-64.481,48.007,-64.5,48.014,-64.52,48.005,-64.541,47.985,-64.591,47.907]},{"b":[-71.117,46.865,-70.826,46.996],"l":[-71.095,46.9,-71.117,46.865,-71.026,46.873,-70.913,46.92,-70.826,46.995,-70.88,46.996,-70.971,46.961,-71.095,46.9]},{"b":[-66.324,44.257,-66.21,44.392],"l":[-66.324,44.257,-66.274,44.292,-66.21,44.392,-66.25,44.379,-66.312,44.292,-66.324,44.257]},{"b":[-68.703,44.176,-68.623,44.256],"l":[-68.691,44.249,-68.703,44.232,-68.702,44.183,-68.661,44.176,-68.623,44.196,-68.656,44.242,-68.677,44.256,-68.691,44.249]},{"b":[-66.897,44.629,-66.745,44.805],"l":[-66.897,44.629,-66.762,44.682,-66.753,44.71,-66.745,44.791,-66.802,44.805,-66.845,44.764,-66.897,44.629]},{"b":[-68.412,44.25,-68.187,44.456],"l":[-68.299,44.456,-68.347,44.43,-68.409,44.364,-68.412,44.294,-68.386,44.277,-68.315,44.25,-68.308,44.269,-68.309,44.321,-68.245,44.313,-68.187,44.332,-68.191,44.364,-68.238,44.438,-68.299,44.456]},{"b":[112.726,-7.224,114.083,-6.873],"l":[113.067,-6.88,112.868,-6.9,112.769,-7.001,112.726,-7.073,112.764,-7.14,113.04,-7.212,113.127,-7.224,113.142,-7.208,113.166,-7.207,113.198,-7.218,113.471,-7.218,113.546,-7.193,113.656,-7.112,113.826,-7.12,113.845,-7.105,113.885,-7.049,114.083,-6.989,114.074,-6.96,113.975,-6.873,113.067,-6.88]},{"b":[158.836,-54.749,158.959,-54.472],"l":[158.836,-54.704,158.845,-54.749,158.879,-54.71,158.946,-54.575,158.959,-54.472,158.897,-54.506,158.836,-54.704]},{"b":[-174.069,-18.699,-173.922,-18.565],"l":[-174.069,-18.64,-174.053,-18.663,-174.009,-18.698,-173.991,-18.699,-173.954,-18.639,-173.924,-18.608,-173.922,-18.589,-173.968,-18.565,-174.002,-18.571,-174.069,-18.64]},{"b":[-151.485,-16.879,-151.365,-16.74],"l":[-151.485,-16.864,-151.449,-16.879,-151.41,-16.878,-151.365,-16.864,-151.411,-16.774,-151.467,-16.74,-151.476,-16.761,-151.485,-16.864]},{"b":[-157.579,1.732,-157.176,2.029],"l":[-157.342,1.856,-157.322,1.969,-157.442,2.025,-157.492,2.029,-157.365,1.946,-157.393,1.928,-157.436,1.847,-157.508,1.886,-157.531,1.927,-157.579,1.902,-157.42,1.788,-157.246,1.732,-157.176,1.74,-157.342,1.856]},{"b":[-139.166,-9.846,-138.827,-9.695],"l":[-139.024,-9.695,-139.166,-9.77,-139.134,-9.829,-139.074,-9.846,-139.024,-9.821,-138.875,-9.793,-138.827,-9.742,-138.874,-9.747,-139.024,-9.695]},{"b":[-139.134,-10.01,-139.06,-9.915],"l":[-139.134,-9.926,-139.134,-10.01,-139.06,-9.931,-139.083,-9.915,-139.107,-9.915,-139.134,-9.926]},{"b":[-138.69,-10.532,-138.624,-10.426],"l":[-138.688,-10.532,-138.651,-10.515,-138.632,-10.492,-138.624,-10.463,-138.643,-10.446,-138.69,-10.426,-138.688,-10.532]},{"b":[-149.635,-17.862,-149.151,-17.496],"l":[-149.322,-17.69,-149.33,-17.589,-149.379,-17.522,-149.508,-17.496,-149.611,-17.532,-149.635,-17.564,-149.633,-17.618,-149.579,-17.735,-149.482,-17.753,-149.341,-17.732,-149.29,-17.822,-149.254,-17.85,-149.182,-17.862,-149.151,-17.812,-149.178,-17.737,-149.322,-17.69]},{"b":[-140.253,-8.934,-140.044,-8.782],"l":[-140.224,-8.782,-140.24,-8.798,-140.253,-8.848,-140.217,-8.93,-140.171,-8.934,-140.073,-8.91,-140.046,-8.874,-140.044,-8.838,-140.058,-8.801,-140.224,-8.782]},{"b":[-140.144,-9.444,-140.031,-9.328],"l":[-140.144,-9.359,-140.138,-9.384,-140.097,-9.444,-140.076,-9.426,-140.031,-9.345,-140.071,-9.328,-140.144,-9.359]},{"b":[-172.779,-13.804,-172.177,-13.465],"l":[-172.333,-13.465,-172.511,-13.483,-172.67,-13.524,-172.779,-13.517,-172.744,-13.579,-172.659,-13.645,-172.536,-13.792,-172.485,-13.8,-172.331,-13.775,-172.225,-13.804,-172.177,-13.685,-172.222,-13.56,-172.333,-13.465]},{"b":[-179.018,-17.307,-178.915,-17.148],"l":[-179.015,-17.182,-178.971,-17.213,-178.976,-17.237,-179.004,-17.295,-178.982,-17.307,-178.956,-17.273,-178.925,-17.249,-178.915,-17.223,-178.921,-17.208,-178.953,-17.182,-179.005,-17.148,-179.018,-17.161,-179.015,-17.182]},{"b":[-159.409,3.797,-159.259,3.924],"l":[-159.409,3.873,-159.378,3.847,-159.373,3.881,-159.354,3.881,-159.327,3.863,-159.306,3.838,-159.314,3.823,-159.359,3.815,-159.332,3.8,-159.275,3.797,-159.259,3.839,-159.339,3.924,-159.369,3.917,-159.391,3.9,-159.409,3.873]},{"b":[-172.046,-14.047,-171.45,-13.807],"l":[-171.858,-13.807,-171.985,-13.824,-172.046,-13.857,-172.028,-13.907,-171.912,-14.002,-171.864,-14.002,-171.728,-14.047,-171.454,-14.046,-171.45,-14.022,-171.461,-13.978,-171.507,-13.95,-171.565,-13.943,-171.604,-13.879,-171.858,-13.807]},{"b":[-170.821,-14.36,-170.568,-14.257],"l":[-170.689,-14.257,-170.721,-14.276,-170.821,-14.312,-170.769,-14.36,-170.726,-14.351,-170.64,-14.282,-170.568,-14.267,-170.689,-14.257]},{"b":[-169.948,-19.138,-169.793,-18.966],"l":[-169.909,-18.99,-169.948,-19.073,-169.904,-19.138,-169.803,-19.083,-169.793,-19.043,-169.834,-18.966,-169.862,-18.969,-169.909,-18.99]},{"b":[-176.195,-13.341,-176.128,-13.222],"l":[-176.195,-13.302,-176.177,-13.341,-176.161,-13.333,-176.128,-13.268,-176.148,-13.222,-176.171,-13.243,-176.195,-13.302]},{"b":[-174.973,-21.451,-174.913,-21.3],"l":[-174.973,-21.35,-174.968,-21.382,-174.919,-21.451,-174.913,-21.3,-174.923,-21.303,-174.973,-21.35]},{"b":[134.506,7.361,134.66,7.712],"l":[134.6,7.616,134.556,7.594,134.516,7.526,134.506,7.437,134.535,7.361,134.595,7.382,134.598,7.438,134.633,7.501,134.66,7.663,134.651,7.712,134.609,7.624,134.6,7.616]},{"b":[145.729,18.057,145.835,18.173],"l":[145.729,18.057,145.778,18.079,145.835,18.137,145.807,18.173,145.789,18.155,145.729,18.057]},{"b":[145.684,15.107,145.822,15.265],"l":[145.713,15.215,145.684,15.125,145.698,15.114,145.749,15.107,145.752,15.133,145.782,15.175,145.789,15.223,145.822,15.265,145.786,15.257,145.713,15.215]},{"b":[144.649,13.258,144.941,13.622],"l":[144.79,13.527,144.649,13.429,144.65,13.313,144.663,13.291,144.7,13.258,144.742,13.259,144.78,13.411,144.941,13.57,144.91,13.599,144.875,13.615,144.837,13.622,144.79,13.527]},{"b":[145.587,14.92,145.662,15.06],"l":[145.587,15.031,145.592,14.999,145.621,14.92,145.662,14.971,145.647,15.059,145.625,15.06,145.587,15.031]},{"b":[138.062,9.419,138.214,9.593],"l":[138.085,9.495,138.062,9.446,138.067,9.419,138.143,9.501,138.183,9.507,138.214,9.547,138.186,9.593,138.147,9.584,138.117,9.55,138.085,9.495]},{"b":[177.959,-19.152,178.488,-18.934],"l":[178.334,-18.934,178.282,-18.957,178.208,-18.97,178.157,-19.028,178.104,-19.066,178.052,-19.06,178.001,-19.101,177.959,-19.122,178.021,-19.152,178.162,-19.121,178.182,-19.112,178.189,-19.092,178.211,-19.067,178.288,-19.004,178.316,-19.01,178.359,-19.046,178.488,-19.017,178.488,-18.974,178.42,-18.951,178.334,-18.934]},{"b":[167.404,-14.312,167.599,-14.142],"l":[167.439,-14.168,167.411,-14.197,167.404,-14.282,167.43,-14.295,167.543,-14.312,167.585,-14.261,167.599,-14.184,167.506,-14.142,167.439,-14.168]},{"b":[167.392,-13.917,167.554,-13.709],"l":[167.499,-13.885,167.543,-13.873,167.553,-13.846,167.554,-13.814,167.547,-13.777,167.481,-13.709,167.407,-13.748,167.392,-13.788,167.451,-13.909,167.474,-13.917,167.489,-13.907,167.499,-13.885]},{"b":[169.738,-20.242,169.896,-20.145],"l":[169.751,-20.153,169.738,-20.202,169.807,-20.241,169.861,-20.242,169.896,-20.187,169.852,-20.148,169.829,-20.145,169.751,-20.153]},{"b":[169.217,-19.649,169.491,-19.322],"l":[169.36,-19.458,169.337,-19.329,169.291,-19.322,169.247,-19.345,169.217,-19.476,169.262,-19.545,169.347,-19.624,169.438,-19.649,169.491,-19.54,169.36,-19.458]},{"b":[168.987,-18.989,169.334,-18.617],"l":[169.296,-18.867,169.201,-18.796,169.256,-18.763,169.178,-18.725,169.144,-18.631,169.088,-18.617,169.016,-18.644,168.987,-18.708,168.998,-18.825,168.987,-18.871,169.248,-18.983,169.288,-18.989,169.334,-18.94,169.296,-18.867]},{"b":[167.815,-21.643,168.139,-21.338],"l":[168.011,-21.43,167.995,-21.407,167.985,-21.37,167.988,-21.338,167.926,-21.373,167.815,-21.393,167.879,-21.524,167.876,-21.582,167.941,-21.606,167.967,-21.642,168.006,-21.643,168.121,-21.616,168.139,-21.445,168.058,-21.448,168.011,-21.43]},{"b":[166.494,-20.709,166.671,-20.413],"l":[166.56,-20.561,166.558,-20.617,166.494,-20.709,166.547,-20.699,166.589,-20.662,166.623,-20.596,166.602,-20.585,166.6,-20.525,166.618,-20.478,166.671,-20.45,166.625,-20.418,166.583,-20.413,166.585,-20.45,166.56,-20.561]},{"b":[167.033,-21.169,167.431,-20.674],"l":[167.293,-20.892,167.298,-20.733,167.269,-20.701,167.204,-20.674,167.056,-20.72,167.045,-20.759,167.136,-20.766,167.189,-20.804,167.112,-20.904,167.033,-20.923,167.073,-20.997,167.134,-21.061,167.273,-21.097,167.346,-21.169,167.401,-21.161,167.43,-21.087,167.431,-21.055,167.361,-20.942,167.293,-20.892]},{"b":[167.422,-22.661,167.544,-22.541],"l":[167.443,-22.541,167.422,-22.619,167.444,-22.639,167.473,-22.653,167.513,-22.661,167.544,-22.623,167.529,-22.579,167.443,-22.541]},{"b":[159.928,-19.333,159.975,-19.115],"l":[159.928,-19.174,159.936,-19.333,159.952,-19.312,159.975,-19.238,159.96,-19.115,159.928,-19.174]},{"b":[166.747,-11.681,166.929,-11.571],"l":[166.855,-11.579,166.791,-11.571,166.747,-11.591,166.806,-11.677,166.841,-11.681,166.929,-11.665,166.875,-11.63,166.855,-11.579]},{"b":[165.79,-10.851,166.162,-10.661],"l":[166.024,-10.661,165.909,-10.674,165.86,-10.703,165.836,-10.761,165.79,-10.756,165.791,-10.785,165.819,-10.844,165.857,-10.841,165.904,-10.851,165.968,-10.779,166.028,-10.77,166.053,-10.775,166.133,-10.758,166.13,-10.745,166.162,-10.693,166.126,-10.68,166.024,-10.661]},{"b":[159.979,-11.832,160.576,-11.472],"l":[160.077,-11.493,160,-11.472,159.986,-11.495,159.979,-11.538,160.004,-11.58,160.087,-11.594,160.1,-11.611,160.15,-11.644,160.27,-11.664,160.355,-11.712,160.395,-11.789,160.443,-11.815,160.507,-11.832,160.576,-11.798,160.537,-11.759,160.449,-11.696,160.077,-11.493]},{"b":[160.59,-9.611,161.378,-8.314],"l":[161.368,-9.49,161.258,-9.317,161.257,-9.192,161.209,-9.133,161.205,-9.092,161.159,-8.962,161.043,-8.855,160.976,-8.837,160.944,-8.799,160.954,-8.699,160.988,-8.665,160.998,-8.612,160.749,-8.314,160.702,-8.317,160.685,-8.336,160.649,-8.338,160.596,-8.328,160.59,-8.373,160.714,-8.539,160.663,-8.621,160.772,-8.964,160.873,-9.157,161.024,-9.271,161.042,-9.308,161.191,-9.393,161.322,-9.59,161.367,-9.611,161.378,-9.574,161.368,-9.49]},{"b":[161.286,-10.832,162.373,-10.204],"l":[162.288,-10.776,162.287,-10.71,162.157,-10.506,162.105,-10.454,162.023,-10.477,161.914,-10.436,161.841,-10.446,161.715,-10.387,161.698,-10.371,161.654,-10.352,161.476,-10.238,161.382,-10.206,161.305,-10.204,161.286,-10.282,161.294,-10.326,161.398,-10.332,161.487,-10.361,161.499,-10.455,161.539,-10.491,161.538,-10.566,161.787,-10.717,161.906,-10.764,162.043,-10.785,162.124,-10.824,162.201,-10.808,162.301,-10.832,162.373,-10.823,162.288,-10.776]},{"b":[161.364,-9.77,161.559,-9.353],"l":[161.412,-9.6,161.41,-9.682,161.442,-9.719,161.478,-9.691,161.554,-9.77,161.559,-9.733,161.548,-9.626,161.407,-9.368,161.364,-9.353,161.402,-9.448,161.417,-9.514,161.412,-9.6]},{"b":[159.036,-9.126,159.234,-8.993],"l":[159.175,-9.126,159.189,-9.124,159.234,-9.094,159.228,-9.03,159.176,-9.022,159.154,-9.001,159.13,-8.993,159.078,-9.025,159.036,-9.075,159.071,-9.11,159.128,-9.114,159.175,-9.126]},{"b":[160.096,-9.181,160.408,-8.996],"l":[160.319,-9.061,160.254,-9.007,160.226,-9.01,160.168,-8.996,160.096,-9.034,160.105,-9.081,160.175,-9.084,160.253,-9.123,160.268,-9.163,160.276,-9.169,160.3,-9.16,160.371,-9.181,160.408,-9.14,160.319,-9.061]},{"b":[159.538,-8.521,159.688,-8.379],"l":[159.646,-8.45,159.642,-8.414,159.595,-8.379,159.553,-8.399,159.538,-8.451,159.569,-8.485,159.64,-8.521,159.688,-8.508,159.646,-8.45]},{"b":[157.218,-8.612,157.898,-7.966],"l":[157.885,-8.569,157.898,-8.506,157.826,-8.324,157.763,-8.242,157.651,-8.217,157.612,-8.165,157.599,-8.006,157.491,-7.966,157.433,-7.985,157.341,-8.096,157.322,-8.161,157.229,-8.212,157.218,-8.263,157.232,-8.315,157.302,-8.333,157.351,-8.275,157.504,-8.258,157.558,-8.27,157.565,-8.338,157.588,-8.445,157.656,-8.5,157.749,-8.524,157.819,-8.612,157.834,-8.573,157.885,-8.569]},{"b":[158.155,-8.826,158.253,-8.678],"l":[158.21,-8.678,158.155,-8.786,158.179,-8.826,158.201,-8.822,158.253,-8.797,158.236,-8.765,158.21,-8.678]},{"b":[157.879,-8.763,158.132,-8.508],"l":[157.909,-8.566,157.898,-8.587,157.879,-8.669,157.938,-8.736,158.009,-8.763,158.108,-8.684,158.104,-8.646,158.09,-8.623,158.068,-8.607,158.132,-8.557,158.105,-8.537,157.998,-8.508,157.967,-8.544,157.938,-8.561,157.909,-8.566]},{"b":[157.212,-8.728,157.411,-8.421],"l":[157.411,-8.475,157.379,-8.421,157.345,-8.432,157.234,-8.52,157.212,-8.565,157.334,-8.7,157.389,-8.728,157.389,-8.713,157.332,-8.651,157.347,-8.575,157.383,-8.555,157.411,-8.475]},{"b":[156.502,-7.937,156.809,-7.574],"l":[156.718,-7.696,156.64,-7.613,156.561,-7.574,156.502,-7.64,156.511,-7.708,156.612,-7.806,156.611,-7.866,156.635,-7.883,156.669,-7.937,156.688,-7.923,156.708,-7.877,156.79,-7.778,156.809,-7.723,156.718,-7.696]},{"b":[156.54,-8.196,156.612,-7.959],"l":[156.57,-7.959,156.551,-7.971,156.542,-8.011,156.54,-8.073,156.592,-8.196,156.604,-8.172,156.612,-8.096,156.57,-7.959]},{"b":[156.958,-8.123,157.201,-7.855],"l":[157.192,-8.082,157.201,-8.016,157.186,-7.941,157.146,-7.883,157.103,-7.855,157.024,-7.868,156.959,-7.938,156.958,-8.014,157.041,-8.117,157.15,-8.123,157.172,-8.108,157.192,-8.082]},{"b":[155.678,-7.121,155.865,-6.973],"l":[155.739,-6.973,155.705,-7.013,155.678,-7.089,155.739,-7.121,155.84,-7.097,155.865,-7.043,155.739,-6.973]},{"b":[162.921,5.277,162.993,5.335],"l":[162.921,5.318,162.93,5.301,162.993,5.277,162.983,5.326,162.958,5.335,162.921,5.318]},{"b":[158.128,6.791,158.335,6.978],"l":[158.186,6.978,158.135,6.945,158.128,6.905,158.161,6.883,158.183,6.801,158.257,6.791,158.315,6.814,158.309,6.855,158.335,6.893,158.295,6.951,158.186,6.978]},{"b":[175.337,-36.334,175.551,-36.071],"l":[175.513,-36.177,175.444,-36.115,175.409,-36.071,175.39,-36.078,175.382,-36.095,175.337,-36.135,175.346,-36.218,175.359,-36.231,175.445,-36.273,175.475,-36.314,175.551,-36.334,175.543,-36.279,175.513,-36.177]},{"b":[173.781,-40.922,173.964,-40.713],"l":[173.873,-40.749,173.812,-40.794,173.786,-40.881,173.781,-40.922,173.915,-40.864,173.958,-40.787,173.964,-40.713,173.903,-40.746,173.873,-40.749]},{"b":[166.532,-45.73,166.746,-45.615],"l":[166.567,-45.644,166.532,-45.7,166.559,-45.708,166.592,-45.702,166.642,-45.724,166.695,-45.73,166.729,-45.73,166.741,-45.705,166.746,-45.656,166.731,-45.639,166.686,-45.615,166.567,-45.644]},{"b":[-176.848,-44.117,-176.178,-43.718],"l":[-176.275,-43.765,-176.214,-43.766,-176.178,-43.74,-176.566,-43.718,-176.667,-43.765,-176.761,-43.758,-176.848,-43.824,-176.808,-43.835,-176.635,-43.82,-176.555,-43.852,-176.524,-43.901,-176.563,-43.954,-176.632,-44.006,-176.629,-44.036,-176.598,-44.107,-176.572,-44.115,-176.516,-44.117,-176.453,-44.077,-176.334,-44.048,-176.334,-44.025,-176.385,-43.951,-176.439,-43.955,-176.5,-43.86,-176.441,-43.816,-176.455,-43.805,-176.517,-43.785,-176.499,-43.768,-176.407,-43.761,-176.375,-43.791,-176.382,-43.867,-176.275,-43.765]},{"b":[-176.229,-44.331,-176.123,-44.225],"l":[-176.229,-44.237,-176.215,-44.274,-176.221,-44.331,-176.176,-44.322,-176.123,-44.268,-176.155,-44.225,-176.229,-44.237]},{"b":[169.022,-52.57,169.233,-52.485],"l":[169.079,-52.499,169.022,-52.495,169.04,-52.529,169.076,-52.552,169.128,-52.57,169.233,-52.548,169.178,-52.497,169.129,-52.485,169.079,-52.499]},{"b":[165.889,-50.846,166.267,-50.531],"l":[166.21,-50.612,166.259,-50.577,166.267,-50.559,166.254,-50.544,166.225,-50.531,166.101,-50.539,166.103,-50.573,166.074,-50.679,165.916,-50.763,165.889,-50.808,165.904,-50.821,165.971,-50.82,166.013,-50.778,166.038,-50.787,166.073,-50.823,166.188,-50.846,166.243,-50.846,166.221,-50.762,166.201,-50.751,166.179,-50.715,166.22,-50.694,166.208,-50.652,166.21,-50.612]},{"b":[167.522,-47.264,168.261,-46.694],"l":[168.156,-46.988,168.125,-46.956,168.043,-46.933,168.041,-46.888,168.145,-46.902,168.145,-46.862,167.956,-46.694,167.784,-46.7,167.765,-46.798,167.801,-46.907,167.742,-46.957,167.741,-47.014,167.654,-47.044,167.631,-47.088,167.629,-47.142,167.539,-47.199,167.522,-47.259,167.555,-47.264,167.676,-47.243,167.785,-47.176,167.811,-47.17,167.906,-47.18,168.015,-47.117,168.184,-47.102,168.241,-47.07,168.261,-47.027,168.241,-46.979,168.156,-46.988]},{"b":[136.943,-15.824,137.094,-15.595],"l":[137.065,-15.663,137.01,-15.595,136.986,-15.652,136.963,-15.666,136.943,-15.712,136.985,-15.726,136.996,-15.776,137.051,-15.824,137.094,-15.778,137.071,-15.738,137.065,-15.663]},{"b":[136.846,-15.627,136.89,-15.503],"l":[136.877,-15.503,136.846,-15.544,136.847,-15.627,136.863,-15.62,136.89,-15.589,136.877,-15.503]},{"b":[136.503,-15.632,136.612,-15.534],"l":[136.523,-15.543,136.503,-15.583,136.514,-15.627,136.531,-15.632,136.591,-15.628,136.612,-15.544,136.586,-15.534,136.523,-15.543]},{"b":[132.484,-11.318,132.629,-10.969],"l":[132.574,-11.318,132.593,-11.303,132.629,-11.169,132.597,-11.106,132.593,-10.998,132.579,-10.969,132.538,-11.028,132.484,-11.037,132.516,-11.116,132.494,-11.164,132.574,-11.318]},{"b":[148.939,-20.303,149.045,-20.154],"l":[149.004,-20.221,148.981,-20.154,148.939,-20.284,148.987,-20.302,149.02,-20.303,149.044,-20.292,149.045,-20.278,149.004,-20.221]},{"b":[148.887,-20.154,148.968,-20.044],"l":[148.968,-20.044,148.932,-20.069,148.906,-20.102,148.887,-20.144,148.913,-20.154,148.936,-20.15,148.956,-20.135,148.968,-20.044]},{"b":[146.099,-18.485,146.342,-18.226],"l":[146.278,-18.231,146.249,-18.226,146.231,-18.241,146.187,-18.255,146.099,-18.252,146.116,-18.292,146.191,-18.363,146.236,-18.451,146.299,-18.485,146.327,-18.449,146.342,-18.4,146.299,-18.326,146.278,-18.231]},{"b":[142.125,-10.762,142.275,-10.592],"l":[142.198,-10.592,142.131,-10.641,142.125,-10.668,142.137,-10.732,142.191,-10.762,142.275,-10.705,142.198,-10.592]},{"b":[142.195,-10.254,142.339,-10.14],"l":[142.219,-10.149,142.195,-10.199,142.216,-10.236,142.279,-10.254,142.339,-10.192,142.299,-10.14,142.219,-10.149]},{"b":[142.098,-10.181,142.192,-10.052],"l":[142.149,-10.052,142.098,-10.122,142.142,-10.181,142.168,-10.154,142.192,-10.085,142.149,-10.052]},{"b":[139.148,-16.742,139.698,-16.395],"l":[139.588,-16.395,139.459,-16.438,139.293,-16.467,139.229,-16.528,139.163,-16.626,139.148,-16.714,139.16,-16.742,139.239,-16.719,139.283,-16.719,139.354,-16.697,139.392,-16.649,139.431,-16.661,139.508,-16.573,139.56,-16.529,139.698,-16.515,139.604,-16.403,139.588,-16.395]},{"b":[139.408,-17.132,139.571,-16.99],"l":[139.493,-16.99,139.459,-17.049,139.408,-17.091,139.422,-17.132,139.459,-17.115,139.571,-17.094,139.56,-17.042,139.493,-16.99]},{"b":[136.335,-14.293,136.951,-13.676],"l":[136.885,-14.197,136.787,-14.158,136.75,-14.115,136.745,-14.073,136.788,-13.946,136.815,-13.907,136.843,-13.897,136.906,-13.827,136.891,-13.787,136.871,-13.764,136.845,-13.751,136.804,-13.842,136.758,-13.845,136.715,-13.804,136.696,-13.726,136.702,-13.682,136.656,-13.676,136.583,-13.721,136.534,-13.794,136.425,-13.865,136.411,-14.011,136.428,-14.126,136.392,-14.175,136.335,-14.212,136.363,-14.229,136.461,-14.235,136.65,-14.28,136.763,-14.273,136.894,-14.293,136.931,-14.246,136.951,-14.184,136.934,-14.179,136.885,-14.197]},{"b":[136.122,-13.836,136.275,-13.665],"l":[136.16,-13.737,136.134,-13.753,136.122,-13.781,136.123,-13.817,136.214,-13.836,136.237,-13.825,136.275,-13.791,136.257,-13.707,136.215,-13.665,136.16,-13.737]},{"b":[136.522,-11.439,136.78,-11.012],"l":[136.688,-11.178,136.649,-11.212,136.56,-11.358,136.522,-11.394,136.527,-11.439,136.599,-11.379,136.741,-11.195,136.78,-11.012,136.732,-11.025,136.727,-11.105,136.711,-11.158,136.688,-11.178]},{"b":[136.18,-11.677,136.479,-11.466],"l":[136.339,-11.602,136.379,-11.583,136.471,-11.509,136.479,-11.466,136.449,-11.487,136.267,-11.576,136.18,-11.677,136.339,-11.602]},{"b":[130.043,-11.836,130.606,-11.337],"l":[130.459,-11.679,130.433,-11.592,130.386,-11.51,130.377,-11.42,130.339,-11.337,130.295,-11.337,130.251,-11.361,130.153,-11.478,130.187,-11.541,130.198,-11.658,130.139,-11.697,130.072,-11.681,130.043,-11.787,130.077,-11.825,130.131,-11.825,130.317,-11.772,130.503,-11.836,130.606,-11.817,130.603,-11.773,130.58,-11.737,130.542,-11.703,130.459,-11.679]},{"b":[130.369,-11.926,131.539,-11.18],"l":[131.437,-11.313,131.321,-11.247,131.268,-11.19,131.217,-11.243,131.141,-11.263,131.023,-11.334,130.987,-11.34,130.913,-11.309,130.752,-11.384,130.619,-11.376,130.56,-11.306,130.519,-11.279,130.427,-11.183,130.403,-11.18,130.385,-11.192,130.369,-11.215,130.405,-11.305,130.423,-11.446,130.512,-11.618,130.645,-11.742,130.951,-11.926,131.292,-11.711,131.383,-11.583,131.459,-11.588,131.468,-11.51,131.539,-11.437,131.522,-11.415,131.473,-11.383,131.437,-11.313]},{"b":[125.091,-14.642,125.199,-14.456],"l":[125.117,-14.492,125.091,-14.592,125.135,-14.642,125.199,-14.579,125.194,-14.553,125.198,-14.475,125.16,-14.456,125.117,-14.492]},{"b":[124.483,-15.43,124.605,-15.267],"l":[124.504,-15.292,124.483,-15.34,124.524,-15.382,124.524,-15.421,124.56,-15.43,124.597,-15.402,124.605,-15.357,124.565,-15.311,124.551,-15.27,124.519,-15.267,124.504,-15.292]},{"b":[115.309,-20.866,115.458,-20.668],"l":[115.435,-20.668,115.354,-20.746,115.309,-20.811,115.318,-20.851,115.388,-20.866,115.446,-20.788,115.458,-20.716,115.435,-20.668]},{"b":[112.908,-26.095,113.183,-25.52],"l":[113.132,-25.883,113.096,-25.815,112.982,-25.52,112.947,-25.532,112.908,-25.57,112.964,-25.783,113.156,-26.095,113.183,-26.053,113.148,-25.974,113.132,-25.952,113.132,-25.883]},{"b":[147.105,-43.501,147.356,-43.279],"l":[147.313,-43.28,147.284,-43.279,147.234,-43.33,147.22,-43.371,147.198,-43.379,147.185,-43.408,147.163,-43.43,147.105,-43.413,147.105,-43.431,147.154,-43.5,147.231,-43.483,147.309,-43.501,147.356,-43.397,147.342,-43.346,147.313,-43.28]},{"b":[148.023,-42.719,148.17,-42.593],"l":[148.023,-42.64,148.031,-42.663,148.03,-42.715,148.048,-42.719,148.104,-42.71,148.101,-42.681,148.17,-42.652,148.143,-42.616,148.073,-42.593,148.023,-42.64]},{"b":[147.296,-43.241,147.435,-43.08],"l":[147.353,-43.08,147.327,-43.115,147.319,-43.145,147.296,-43.162,147.338,-43.183,147.349,-43.232,147.372,-43.241,147.435,-43.241,147.397,-43.118,147.353,-43.08]},{"b":[144.71,-40.589,144.791,-40.435],"l":[144.751,-40.47,144.71,-40.485,144.748,-40.589,144.784,-40.507,144.791,-40.44,144.783,-40.435,144.751,-40.47]},{"b":[143.839,-40.12,144.141,-39.58],"l":[144.121,-39.785,144.091,-39.638,144.001,-39.58,143.949,-39.584,143.939,-39.658,143.879,-39.7,143.862,-39.738,143.865,-39.824,143.839,-39.904,143.888,-39.984,143.876,-40.064,143.899,-40.12,143.928,-40.116,144.035,-40.078,144.112,-40.022,144.141,-39.954,144.106,-39.874,144.121,-39.785]},{"b":[148.01,-40.497,148.474,-40.307],"l":[148.326,-40.307,148.198,-40.358,148.059,-40.357,148.01,-40.381,148.02,-40.404,148.103,-40.452,148.214,-40.458,148.319,-40.435,148.353,-40.497,148.404,-40.487,148.474,-40.432,148.421,-40.367,148.326,-40.307]},{"b":[147.767,-40.262,148.323,-39.726],"l":[148.106,-40.262,148.21,-40.234,148.299,-40.172,148.314,-40.174,148.323,-40.144,148.251,-40.1,148.29,-40.065,148.297,-39.986,148.27,-39.967,148.178,-39.938,148,-39.758,147.933,-39.726,147.839,-39.832,147.767,-39.87,147.812,-39.91,147.876,-39.905,147.906,-39.971,147.891,-40.015,148.025,-40.172,148.047,-40.213,148.074,-40.241,148.106,-40.262]},{"b":[148.117,-40.593,148.237,-40.503],"l":[148.188,-40.593,148.237,-40.515,148.218,-40.505,148.193,-40.503,148.117,-40.521,148.127,-40.544,148.188,-40.593]},{"b":[136.541,-36.075,138.123,-35.592],"l":[138.047,-35.755,137.929,-35.726,137.836,-35.762,137.596,-35.739,137.598,-35.722,137.635,-35.656,137.585,-35.62,137.53,-35.605,137.334,-35.592,137.092,-35.664,136.639,-35.749,136.579,-35.809,136.541,-35.89,136.589,-35.935,136.755,-36.033,136.913,-36.047,137.026,-36.024,137.148,-36.039,137.21,-35.982,137.382,-36.021,137.448,-36.075,137.59,-36.027,137.622,-35.938,137.671,-35.898,137.836,-35.868,138.012,-35.908,138.067,-35.901,138.123,-35.852,138.047,-35.755]},{"b":[145.28,-38.421,145.487,-38.314],"l":[145.295,-38.319,145.286,-38.341,145.28,-38.391,145.336,-38.421,145.487,-38.355,145.427,-38.314,145.295,-38.319]},{"b":[145.128,-38.557,145.355,-38.459],"l":[145.218,-38.459,145.128,-38.528,145.271,-38.52,145.355,-38.557,145.349,-38.538,145.314,-38.491,145.288,-38.472,145.218,-38.459]},{"b":[153.396,-27.712,153.539,-27.405],"l":[153.401,-27.506,153.396,-27.665,153.427,-27.706,153.453,-27.712,153.539,-27.436,153.522,-27.422,153.435,-27.405,153.401,-27.506]},{"b":[153.365,-27.331,153.467,-27.03],"l":[153.467,-27.038,153.432,-27.03,153.38,-27.049,153.365,-27.139,153.377,-27.235,153.421,-27.331,153.442,-27.316,153.426,-27.201,153.467,-27.038]},{"b":[152.977,-25.778,153.359,-24.729],"l":[153.298,-24.915,153.282,-24.738,153.257,-24.729,153.223,-24.74,153.181,-24.765,153.144,-24.815,153.186,-24.833,153.242,-24.923,153.228,-25.006,153.189,-25.071,153.038,-25.193,153.061,-25.302,153.052,-25.354,152.999,-25.448,152.977,-25.551,153.007,-25.729,153.052,-25.778,153.077,-25.751,153.084,-25.683,153.141,-25.513,153.35,-25.063,153.359,-24.978,153.298,-24.915]},{"b":[151.033,-23.776,151.296,-23.461],"l":[151.229,-23.595,151.24,-23.53,151.212,-23.513,151.181,-23.516,151.147,-23.491,151.06,-23.461,151.033,-23.53,151.184,-23.741,151.238,-23.776,151.262,-23.762,151.296,-23.72,151.274,-23.668,151.229,-23.595]},{"b":[150.462,-22.325,150.549,-22.211],"l":[150.485,-22.268,150.462,-22.308,150.488,-22.325,150.517,-22.323,150.549,-22.307,150.521,-22.228,150.488,-22.211,150.485,-22.268]},{"b":[149.87,-22.223,149.928,-22.049],"l":[149.87,-22.15,149.894,-22.223,149.928,-22.193,149.928,-22.149,149.912,-22.049,149.875,-22.074,149.87,-22.15]},{"b":[117.272,8.191,117.355,8.315],"l":[117.281,8.315,117.272,8.254,117.287,8.191,117.355,8.215,117.354,8.289,117.33,8.308,117.281,8.315]},{"b":[116.97,7.808,117.08,8.069],"l":[116.994,8.051,116.976,8.017,116.97,7.895,117.028,7.808,117.08,7.883,117.077,8.069,116.994,8.051]},{"b":[119.764,10.455,120.008,10.64],"l":[119.852,10.64,119.764,10.552,119.793,10.455,119.916,10.486,119.981,10.539,120.008,10.57,119.95,10.605,119.852,10.64]},{"b":[119.726,11.376,119.883,11.525],"l":[119.761,11.474,119.726,11.475,119.73,11.432,119.799,11.409,119.831,11.376,119.855,11.393,119.883,11.472,119.861,11.525,119.827,11.515,119.761,11.474]},{"b":[119.866,11.994,120.341,12.319],"l":[120.228,12.22,120.194,12.167,120.155,12.152,120.1,12.168,120.078,12.198,119.964,12.27,119.916,12.319,119.897,12.313,119.886,12.3,119.88,12.28,119.892,12.273,119.87,12.244,119.866,12.199,119.896,12.179,119.957,12.069,120.011,12.008,120.1,11.994,120.174,12.02,120.243,12.005,120.315,12.012,120.341,12.077,120.261,12.142,120.228,12.22]},{"b":[119.861,11.669,120.073,11.981],"l":[119.957,11.96,119.916,11.981,119.861,11.954,119.933,11.774,119.932,11.74,119.945,11.691,119.964,11.669,120.039,11.703,120.073,11.783,120.062,11.821,120.071,11.861,120.036,11.917,119.998,11.932,119.957,11.96]},{"b":[122.517,10.425,122.737,10.741],"l":[122.626,10.695,122.538,10.608,122.517,10.493,122.538,10.425,122.597,10.461,122.622,10.459,122.65,10.473,122.681,10.498,122.737,10.655,122.729,10.706,122.701,10.741,122.673,10.739,122.648,10.723,122.626,10.695]},{"b":[123.316,9.423,124.058,11.274],"l":[124.053,11.029,124.04,11.054,124.037,11.107,124.058,11.217,124.039,11.274,123.967,11.187,123.964,11.137,123.95,11.079,123.925,11.041,123.93,10.964,123.832,10.731,123.726,10.562,123.711,10.474,123.593,10.303,123.514,10.14,123.386,9.967,123.404,9.889,123.327,9.578,123.316,9.489,123.332,9.423,123.37,9.45,123.494,9.589,123.634,9.922,123.643,10.02,123.7,10.128,123.789,10.221,123.874,10.258,123.952,10.317,124.005,10.4,124.051,10.586,124.028,10.768,124.053,10.926,124.053,11.029]},{"b":[123.708,11.151,123.816,11.283],"l":[123.757,11.283,123.741,11.279,123.708,11.248,123.737,11.151,123.816,11.151,123.757,11.283]},{"b":[123.817,9.599,124.594,10.16],"l":[124.486,10.065,124.406,10.126,124.373,10.13,124.352,10.141,124.336,10.16,124.173,10.135,124.094,10.061,124.06,10,123.909,9.92,123.864,9.879,123.817,9.817,123.83,9.761,123.872,9.676,123.936,9.624,124.122,9.599,124.36,9.63,124.403,9.654,124.478,9.748,124.506,9.754,124.584,9.75,124.594,9.787,124.582,9.83,124.555,9.879,124.577,10.027,124.486,10.065]},{"b":[124.639,9.083,124.807,9.243],"l":[124.737,9.243,124.708,9.243,124.653,9.226,124.639,9.175,124.666,9.132,124.778,9.083,124.807,9.143,124.79,9.19,124.737,9.243]},{"b":[123.493,9.103,123.706,9.279],"l":[123.698,9.237,123.655,9.279,123.626,9.268,123.535,9.214,123.494,9.216,123.493,9.192,123.541,9.13,123.614,9.103,123.706,9.134,123.698,9.237]},{"b":[119.821,5.06,120.25,5.34],"l":[120.208,5.34,120.165,5.332,120.08,5.264,119.983,5.228,119.827,5.133,119.821,5.07,119.878,5.06,119.958,5.08,120.013,5.151,120.101,5.169,120.118,5.215,120.15,5.184,120.192,5.168,120.223,5.196,120.25,5.257,120.229,5.284,120.208,5.34]},{"b":[121.809,6.415,122.324,6.741],"l":[122.058,6.741,121.915,6.676,121.832,6.664,121.809,6.614,121.872,6.563,121.88,6.518,121.959,6.416,121.991,6.415,122.093,6.428,122.201,6.483,122.252,6.58,122.324,6.602,122.288,6.639,122.058,6.741]},{"b":[125.991,9.761,126.173,10.059],"l":[126.129,9.944,126.074,10.059,125.999,9.927,125.991,9.839,126.047,9.761,126.059,9.766,126.137,9.768,126.173,9.8,126.121,9.865,126.129,9.891,126.129,9.944]},{"b":[125.495,9.886,125.703,10.44],"l":[125.703,10.072,125.647,10.245,125.685,10.392,125.667,10.44,125.648,10.437,125.606,10.38,125.58,10.364,125.525,10.31,125.522,10.192,125.495,10.119,125.534,10.09,125.591,9.998,125.649,9.944,125.673,9.886,125.69,9.914,125.692,9.939,125.684,9.963,125.703,10.072]},{"b":[120.099,13.683,120.273,13.858],"l":[120.211,13.821,120.121,13.858,120.103,13.843,120.099,13.817,120.104,13.782,120.273,13.683,120.271,13.751,120.211,13.821]},{"b":[121.815,13.205,122.122,13.566],"l":[121.996,13.547,121.977,13.537,121.915,13.54,121.866,13.566,121.815,13.424,121.829,13.329,121.876,13.282,122.005,13.205,122.042,13.236,122.055,13.269,122.122,13.365,122.107,13.421,122.115,13.463,121.996,13.547]},{"b":[121.923,12.106,122.145,12.653],"l":[122.002,12.599,121.989,12.435,121.941,12.385,121.923,12.331,121.936,12.29,121.982,12.245,121.96,12.191,122.014,12.106,122.094,12.355,122.132,12.538,122.13,12.613,122.145,12.653,122.104,12.651,122.002,12.599]},{"b":[122.248,12.498,122.311,12.593],"l":[122.248,12.557,122.261,12.503,122.28,12.498,122.311,12.529,122.288,12.589,122.278,12.593,122.248,12.557]},{"b":[122.423,12.286,122.683,12.492],"l":[122.604,12.492,122.472,12.492,122.423,12.455,122.439,12.429,122.499,12.384,122.603,12.286,122.654,12.309,122.683,12.382,122.674,12.424,122.604,12.492]},{"b":[122.949,12.701,123.367,13.116],"l":[123.044,13.113,123.017,13.116,122.958,13.107,122.949,13.059,122.973,13.035,123.054,12.993,123.166,12.876,123.274,12.805,123.367,12.701,123.282,12.853,123.206,12.905,123.044,13.113]},{"b":[123.587,12.366,123.779,12.675],"l":[123.775,12.454,123.709,12.611,123.621,12.675,123.587,12.633,123.621,12.571,123.742,12.399,123.779,12.366,123.775,12.454]},{"b":[123.156,11.752,124.056,12.583],"l":[123.211,12.107,123.156,11.968,123.158,11.926,123.293,12.036,123.419,12.194,123.474,12.217,123.531,12.197,123.612,12.09,123.668,12.069,123.675,12.05,123.736,12.003,123.725,11.952,123.754,11.934,123.848,11.914,123.983,11.819,124.046,11.752,124.056,11.812,124.04,11.967,123.908,12.169,123.717,12.287,123.575,12.407,123.559,12.445,123.463,12.501,123.337,12.542,123.236,12.583,123.24,12.495,123.267,12.395,123.245,12.328,123.211,12.107]},{"b":[124.039,13.532,124.417,14.078],"l":[124.337,13.931,124.308,13.947,124.225,14.078,124.186,14.06,124.154,14.026,124.123,13.98,124.124,13.79,124.039,13.663,124.057,13.606,124.175,13.532,124.248,13.587,124.295,13.59,124.327,13.567,124.354,13.632,124.404,13.679,124.396,13.75,124.417,13.871,124.337,13.931]},{"b":[121.946,14.008,122.175,14.229],"l":[121.959,14.229,121.946,14.205,121.946,14.181,121.956,14.156,122.172,14.008,122.175,14.049,121.959,14.229]},{"b":[121.82,14.656,122.052,15.046],"l":[121.84,15.038,121.82,14.964,121.862,14.917,121.889,14.84,121.923,14.8,121.935,14.737,121.922,14.715,121.911,14.667,121.933,14.656,121.99,14.662,122.022,14.759,121.97,14.893,122.017,14.965,122.032,14.972,122.052,14.97,122.033,15.005,121.972,15.046,121.84,15.038]},{"b":[121.825,18.823,121.988,19.01],"l":[121.889,18.992,121.86,18.937,121.861,18.913,121.825,18.843,121.858,18.823,121.922,18.895,121.988,18.957,121.943,19.01,121.889,18.992]},{"b":[121.185,19.015,121.252,19.184],"l":[121.196,19.051,121.247,19.015,121.252,19.082,121.245,19.143,121.213,19.184,121.19,19.139,121.185,19.101,121.196,19.051]},{"b":[121.375,19.271,121.531,19.399],"l":[121.392,19.399,121.376,19.38,121.375,19.356,121.383,19.328,121.472,19.273,121.531,19.271,121.521,19.362,121.392,19.399]},{"b":[121.914,20.354,122.031,20.48],"l":[121.914,20.359,121.941,20.354,121.96,20.366,122.031,20.469,121.991,20.48,121.941,20.454,121.914,20.359]},{"b":[121.791,20.7,121.878,20.841],"l":[121.796,20.747,121.791,20.701,121.83,20.7,121.878,20.782,121.867,20.839,121.848,20.841,121.796,20.747]},{"b":[147.781,-5.834,148.076,-5.491],"l":[147.846,-5.491,147.795,-5.492,147.783,-5.522,147.781,-5.627,147.875,-5.749,147.968,-5.789,147.985,-5.834,148.026,-5.826,148.06,-5.765,148.076,-5.65,148.055,-5.612,147.846,-5.491]},{"b":[147.006,-5.437,147.222,-5.191],"l":[147.131,-5.191,147.015,-5.257,147.006,-5.307,147.029,-5.342,147.12,-5.437,147.176,-5.432,147.222,-5.382,147.206,-5.252,147.131,-5.191]},{"b":[145.884,-4.756,146.053,-4.539],"l":[145.9,-4.604,145.884,-4.667,145.904,-4.733,145.952,-4.756,146.019,-4.726,146.053,-4.64,146.037,-4.573,145.996,-4.539,145.959,-4.554,145.9,-4.604]},{"b":[147.736,-2.336,147.877,-2.247],"l":[147.79,-2.306,147.736,-2.316,147.769,-2.331,147.845,-2.336,147.877,-2.283,147.836,-2.247,147.812,-2.262,147.79,-2.306]},{"b":[146.531,-2.21,147.444,-1.949],"l":[147.401,-2.025,147.068,-1.96,146.857,-1.949,146.76,-1.978,146.656,-1.974,146.596,-2.017,146.607,-2.103,146.532,-2.126,146.531,-2.154,146.546,-2.209,146.572,-2.21,146.635,-2.173,146.699,-2.183,146.748,-2.149,146.926,-2.189,147.064,-2.187,147.142,-2.167,147.206,-2.182,147.301,-2.09,147.337,-2.066,147.385,-2.071,147.438,-2.059,147.444,-2.012,147.424,-1.995,147.419,-2.001,147.423,-2.024,147.401,-2.025]},{"b":[154.54,-5.441,154.727,-5.014],"l":[154.682,-5.054,154.633,-5.014,154.606,-5.035,154.54,-5.111,154.563,-5.152,154.576,-5.221,154.584,-5.314,154.627,-5.441,154.647,-5.433,154.698,-5.383,154.727,-5.218,154.689,-5.143,154.682,-5.054]},{"b":[152.543,-3.221,152.671,-3.043],"l":[152.639,-3.043,152.57,-3.062,152.543,-3.096,152.585,-3.17,152.646,-3.221,152.671,-3.133,152.639,-3.043]},{"b":[151.955,-2.998,152.099,-2.846],"l":[151.975,-2.846,151.955,-2.871,151.971,-2.896,152.057,-2.995,152.088,-2.998,152.099,-2.947,152.075,-2.918,151.975,-2.846]},{"b":[151.93,-2.83,152.011,-2.709],"l":[152.002,-2.738,151.946,-2.709,151.93,-2.751,151.933,-2.83,151.957,-2.83,152.011,-2.809,152.002,-2.738]},{"b":[149.546,-1.589,149.765,-1.353],"l":[149.633,-1.362,149.581,-1.353,149.548,-1.408,149.546,-1.472,149.671,-1.576,149.691,-1.571,149.763,-1.589,149.765,-1.553,149.725,-1.431,149.633,-1.362]},{"b":[149.962,-2.675,150.452,-2.384],"l":[150.429,-2.47,150.227,-2.384,150.103,-2.405,149.962,-2.474,149.985,-2.492,150.043,-2.512,150.102,-2.603,150.166,-2.66,150.238,-2.675,150.437,-2.662,150.446,-2.632,150.452,-2.541,150.45,-2.513,150.429,-2.47]},{"b":[150.776,-10.195,151.296,-9.709],"l":[151.123,-10.02,151.081,-10.02,151.044,-9.983,151.051,-9.939,150.862,-9.802,150.817,-9.736,150.776,-9.709,150.79,-9.774,150.861,-9.876,150.896,-9.968,150.952,-9.998,150.959,-10.093,151.175,-10.159,151.231,-10.195,151.296,-9.957,151.256,-9.923,151.194,-9.946,151.123,-10.02]},{"b":[150.11,-9.519,150.368,-9.206],"l":[150.32,-9.264,150.208,-9.206,150.135,-9.26,150.11,-9.362,150.273,-9.5,150.331,-9.519,150.345,-9.494,150.368,-9.396,150.357,-9.349,150.32,-9.264]},{"b":[150.431,-9.703,150.899,-9.346],"l":[150.848,-9.663,150.844,-9.703,150.894,-9.667,150.899,-9.641,150.884,-9.582,150.879,-9.513,150.789,-9.418,150.746,-9.404,150.669,-9.429,150.528,-9.347,150.499,-9.346,150.437,-9.36,150.431,-9.387,150.435,-9.435,150.508,-9.536,150.495,-9.562,150.436,-9.625,150.576,-9.631,150.678,-9.657,150.81,-9.655,150.848,-9.663]},{"b":[151.005,-8.805,151.139,-8.419],"l":[151.139,-8.568,151.116,-8.522,151.118,-8.419,151.09,-8.426,151.046,-8.451,151.005,-8.524,151.083,-8.569,151.087,-8.595,151.081,-8.642,151.046,-8.728,151.124,-8.805,151.107,-8.733,151.139,-8.568]},{"b":[152.515,-9.224,152.995,-8.959],"l":[152.85,-9.025,152.81,-8.967,152.689,-8.975,152.631,-8.959,152.577,-8.97,152.515,-9.01,152.638,-9.058,152.708,-9.126,152.72,-9.167,152.759,-9.177,152.867,-9.224,152.923,-9.203,152.967,-9.209,152.959,-9.169,152.985,-9.151,152.995,-9.131,152.995,-9.108,152.953,-9.07,152.905,-9.044,152.85,-9.025]},{"b":[150.786,-10.643,150.899,-10.552],"l":[150.799,-10.554,150.786,-10.603,150.802,-10.62,150.885,-10.643,150.899,-10.565,150.872,-10.552,150.799,-10.554]},{"b":[154.023,-11.426,154.281,-11.311],"l":[154.102,-11.311,154.118,-11.366,154.023,-11.348,154.031,-11.371,154.064,-11.419,154.121,-11.426,154.23,-11.397,154.266,-11.416,154.281,-11.361,154.238,-11.339,154.102,-11.311]},{"b":[153.204,-11.631,153.76,-11.324],"l":[153.703,-11.529,153.536,-11.476,153.307,-11.356,153.204,-11.324,153.207,-11.352,153.234,-11.42,153.322,-11.471,153.287,-11.517,153.357,-11.495,153.379,-11.56,153.519,-11.595,153.554,-11.631,153.7,-11.613,153.76,-11.586,153.703,-11.529]},{"b":[143.322,-8.485,143.593,-8.368],"l":[143.543,-8.485,143.587,-8.482,143.593,-8.46,143.581,-8.391,143.528,-8.379,143.322,-8.368,143.367,-8.417,143.543,-8.485]},{"b":[143.207,-8.677,143.608,-8.423],"l":[143.443,-8.519,143.293,-8.473,143.207,-8.423,143.254,-8.49,143.324,-8.517,143.463,-8.617,143.608,-8.677,143.59,-8.633,143.443,-8.519]},{"b":[135.383,-1.217,136.375,-0.651],"l":[136.283,-1.065,136.155,-0.978,136.069,-0.878,135.894,-0.726,135.841,-0.712,135.749,-0.733,135.673,-0.688,135.596,-0.69,135.383,-0.651,135.388,-0.705,135.432,-0.769,135.483,-0.801,135.491,-0.785,135.524,-0.787,135.646,-0.882,135.747,-0.823,135.826,-1.028,135.839,-1.119,135.915,-1.178,136.003,-1.17,136.11,-1.217,136.165,-1.215,136.305,-1.173,136.375,-1.094,136.283,-1.065]},{"b":[135.47,-1.894,136.893,-1.592],"l":[136.719,-1.734,136.39,-1.722,136.202,-1.655,135.976,-1.636,135.869,-1.642,135.474,-1.592,135.47,-1.616,135.488,-1.668,135.866,-1.752,136.049,-1.824,136.193,-1.859,136.228,-1.894,136.326,-1.872,136.461,-1.89,136.622,-1.873,136.709,-1.838,136.893,-1.8,136.817,-1.754,136.719,-1.734]},{"b":[134.809,-1.134,134.996,-0.938],"l":[134.957,-1.031,134.941,-0.979,134.889,-0.938,134.828,-0.979,134.809,-1.038,134.862,-1.114,134.917,-1.134,134.965,-1.116,134.996,-1.034,134.957,-1.031]},{"b":[134.335,-2.139,134.419,-2.028],"l":[134.351,-2.037,134.335,-2.095,134.345,-2.139,134.374,-2.124,134.419,-2.052,134.391,-2.031,134.37,-2.028,134.351,-2.037]},{"b":[138.563,-8.402,138.898,-8.173],"l":[138.796,-8.174,138.763,-8.173,138.677,-8.199,138.621,-8.268,138.563,-8.309,138.567,-8.33,138.594,-8.371,138.846,-8.402,138.895,-8.389,138.898,-8.337,138.796,-8.174]},{"b":[133.321,-4.299,133.622,-4.111],"l":[133.464,-4.2,133.321,-4.111,133.333,-4.17,133.503,-4.257,133.622,-4.299,133.571,-4.246,133.464,-4.2]},{"b":[129.738,-2.064,130.425,-1.691],"l":[130.425,-1.805,130.365,-1.75,130.353,-1.691,130.318,-1.692,130.2,-1.732,130.106,-1.73,129.994,-1.759,129.738,-1.867,129.754,-1.894,129.887,-1.986,130.093,-2.028,130.133,-2.064,130.248,-2.048,130.284,-2.009,130.339,-1.982,130.373,-1.992,130.419,-1.971,130.393,-1.942,130.381,-1.903,130.404,-1.89,130.425,-1.805]},{"b":[130.668,-1.343,131.074,-0.89],"l":[131.033,-0.918,130.939,-0.915,130.897,-0.89,130.673,-0.96,130.668,-0.984,130.704,-1.05,130.712,-1.104,130.739,-1.173,130.782,-1.255,130.845,-1.317,130.967,-1.343,131.002,-1.316,131.046,-1.188,131.074,-0.968,131.033,-0.918]},{"b":[130.402,-0.924,130.905,-0.765],"l":[130.807,-0.765,130.813,-0.814,130.723,-0.822,130.635,-0.812,130.594,-0.827,130.57,-0.822,130.548,-0.826,130.527,-0.837,130.484,-0.833,130.457,-0.851,130.439,-0.887,130.402,-0.924,130.832,-0.863,130.88,-0.828,130.905,-0.777,130.807,-0.765]},{"b":[130.465,-0.53,130.684,-0.417],"l":[130.616,-0.417,130.597,-0.418,130.564,-0.441,130.526,-0.449,130.465,-0.487,130.569,-0.53,130.627,-0.529,130.684,-0.469,130.657,-0.437,130.616,-0.417]},{"b":[130.237,-0.444,131.34,-0.004],"l":[131.277,-0.15,131.026,-0.04,130.987,-0.047,130.813,-0.004,130.722,-0.03,130.584,-0.045,130.548,-0.07,130.5,-0.06,130.431,-0.098,130.363,-0.073,130.295,-0.101,130.288,-0.155,130.237,-0.21,130.341,-0.262,130.496,-0.267,130.551,-0.366,130.575,-0.362,130.607,-0.329,130.689,-0.297,130.7,-0.392,130.75,-0.444,130.896,-0.416,130.899,-0.344,130.843,-0.298,130.802,-0.302,130.761,-0.291,130.691,-0.181,130.638,-0.143,130.622,-0.086,130.683,-0.081,130.808,-0.226,130.897,-0.268,130.946,-0.338,131.005,-0.361,131.098,-0.33,131.178,-0.346,131.218,-0.374,131.258,-0.366,131.34,-0.29,131.303,-0.241,131.317,-0.204,131.277,-0.15]},{"b":[129.309,-0.19,129.549,0.045],"l":[129.309,0.045,129.37,-0.066,129.469,-0.131,129.506,-0.19,129.549,-0.187,129.542,-0.139,129.309,0.045]},{"b":[128.218,2.035,128.688,2.598],"l":[128.602,2.598,128.569,2.596,128.472,2.571,128.33,2.469,128.218,2.297,128.26,2.083,128.296,2.035,128.454,2.052,128.548,2.097,128.623,2.224,128.688,2.474,128.602,2.598]},{"b":[127.397,-0.037,127.453,0.143],"l":[127.431,0.143,127.42,0.124,127.397,0.017,127.418,0.006,127.449,-0.037,127.453,-0.006,127.449,0.069,127.431,0.143]},{"b":[127.363,0.631,127.446,0.744],"l":[127.42,0.642,127.446,0.683,127.443,0.733,127.425,0.744,127.383,0.744,127.363,0.675,127.374,0.635,127.384,0.631,127.42,0.642]},{"b":[127.104,-0.521,127.29,-0.256],"l":[127.281,-0.391,127.253,-0.319,127.29,-0.284,127.19,-0.256,127.126,-0.279,127.104,-0.414,127.119,-0.521,127.187,-0.521,127.25,-0.495,127.281,-0.391]},{"b":[127.156,-0.802,127.301,-0.619],"l":[127.258,-0.623,127.209,-0.619,127.156,-0.761,127.185,-0.775,127.289,-0.802,127.3,-0.781,127.301,-0.758,127.258,-0.623]},{"b":[127.297,-0.884,127.88,-0.307],"l":[127.804,-0.694,127.659,-0.689,127.605,-0.61,127.682,-0.468,127.567,-0.319,127.527,-0.307,127.492,-0.336,127.455,-0.406,127.371,-0.332,127.325,-0.336,127.329,-0.391,127.297,-0.46,127.3,-0.5,127.381,-0.6,127.469,-0.643,127.438,-0.739,127.463,-0.806,127.498,-0.802,127.624,-0.766,127.643,-0.784,127.668,-0.832,127.761,-0.884,127.842,-0.848,127.88,-0.809,127.863,-0.76,127.838,-0.724,127.804,-0.694]},{"b":[127.392,-1.729,128.153,-1.332],"l":[127.905,-1.439,127.743,-1.36,127.647,-1.332,127.592,-1.351,127.457,-1.454,127.395,-1.59,127.392,-1.645,127.458,-1.697,127.562,-1.729,127.741,-1.691,127.914,-1.685,128.061,-1.712,128.092,-1.701,128.153,-1.661,128.149,-1.604,128.033,-1.532,127.905,-1.439]},{"b":[134.153,-6.365,134.758,-5.427],"l":[134.744,-6.202,134.756,-6.171,134.758,-6.1,134.752,-6.05,134.712,-5.95,134.755,-5.883,134.738,-5.817,134.739,-5.746,134.747,-5.707,134.701,-5.603,134.646,-5.581,134.658,-5.539,134.646,-5.492,134.617,-5.439,134.571,-5.427,134.506,-5.438,134.49,-5.525,134.456,-5.558,134.341,-5.713,134.247,-5.682,134.205,-5.707,134.226,-5.744,134.343,-5.833,134.299,-5.971,134.302,-6.01,134.225,-6.008,134.153,-6.02,134.155,-6.063,134.175,-6.09,134.264,-6.172,134.28,-6.201,134.356,-6.271,134.441,-6.335,134.638,-6.365,134.661,-6.337,134.684,-6.328,134.714,-6.295,134.744,-6.202]},{"b":[134.059,-6.909,134.537,-6.176],"l":[134.52,-6.513,134.537,-6.442,134.415,-6.387,134.318,-6.316,134.234,-6.226,134.168,-6.176,134.115,-6.191,134.111,-6.255,134.125,-6.426,134.195,-6.46,134.185,-6.479,134.154,-6.481,134.107,-6.472,134.059,-6.769,134.091,-6.834,134.2,-6.909,134.323,-6.849,134.356,-6.815,134.413,-6.68,134.504,-6.591,134.52,-6.513]},{"b":[132.845,-5.988,133.173,-5.311],"l":[133.009,-5.621,132.938,-5.683,132.922,-5.785,132.845,-5.988,132.926,-5.902,132.971,-5.736,133.12,-5.576,133.173,-5.348,133.138,-5.318,133.115,-5.311,133.009,-5.621]},{"b":[132.63,-5.947,132.807,-5.607],"l":[132.738,-5.662,132.717,-5.648,132.698,-5.609,132.63,-5.607,132.681,-5.739,132.667,-5.856,132.681,-5.913,132.705,-5.913,132.746,-5.947,132.807,-5.851,132.804,-5.789,132.738,-5.662]},{"b":[128.585,-3.603,128.722,-3.495],"l":[128.667,-3.517,128.595,-3.495,128.585,-3.512,128.62,-3.589,128.659,-3.588,128.713,-3.603,128.72,-3.589,128.722,-3.547,128.694,-3.525,128.667,-3.517]},{"b":[128.392,-3.638,128.563,-3.515],"l":[128.452,-3.515,128.428,-3.54,128.392,-3.638,128.563,-3.585,128.536,-3.541,128.452,-3.515]},{"b":[127.925,-3.771,128.329,-3.512],"l":[128.264,-3.512,128.119,-3.587,128.016,-3.601,127.928,-3.679,127.925,-3.699,127.934,-3.743,127.978,-3.771,128.052,-3.715,128.111,-3.686,128.147,-3.677,128.159,-3.698,128.143,-3.733,128.192,-3.735,128.25,-3.711,128.276,-3.675,128.277,-3.633,128.291,-3.598,128.314,-3.564,128.329,-3.516,128.264,-3.512]},{"b":[122.585,-5.671,123.203,-4.387],"l":[123.051,-5.156,123.025,-5.162,122.981,-5.186,122.972,-5.138,122.987,-4.963,123.015,-4.91,123.018,-4.832,123.055,-4.748,123.084,-4.749,123.104,-4.74,123.119,-4.723,123.139,-4.74,123.196,-4.823,123.203,-4.766,123.18,-4.551,123.069,-4.434,123.075,-4.387,123.038,-4.395,122.947,-4.443,122.853,-4.618,122.849,-4.831,122.821,-4.944,122.804,-5,122.794,-5.052,122.768,-5.177,122.767,-5.21,122.731,-5.262,122.67,-5.331,122.643,-5.381,122.642,-5.426,122.586,-5.489,122.585,-5.545,122.645,-5.663,122.684,-5.666,122.733,-5.635,122.812,-5.671,122.85,-5.638,122.916,-5.519,122.909,-5.477,122.935,-5.437,122.969,-5.406,122.986,-5.394,123.043,-5.419,123.121,-5.393,123.187,-5.333,123.202,-5.273,123.15,-5.224,123.051,-5.156]},{"b":[122.283,-5.391,122.76,-4.619],"l":[122.74,-4.675,122.702,-4.619,122.66,-4.634,122.524,-4.707,122.369,-4.767,122.334,-4.847,122.39,-4.999,122.396,-5.07,122.329,-5.138,122.283,-5.32,122.307,-5.381,122.371,-5.383,122.392,-5.335,122.474,-5.381,122.52,-5.391,122.564,-5.387,122.619,-5.336,122.645,-5.269,122.614,-5.139,122.76,-4.934,122.74,-4.675]},{"b":[121.808,-5.465,122.062,-5.072],"l":[122.041,-5.159,122,-5.141,121.966,-5.076,121.914,-5.072,121.866,-5.096,121.874,-5.145,121.857,-5.156,121.821,-5.203,121.808,-5.256,121.859,-5.35,121.98,-5.465,122.043,-5.438,122.062,-5.221,122.041,-5.159]},{"b":[122.969,-4.233,123.247,-3.981],"l":[122.969,-4.03,122.971,-4.061,122.995,-4.148,123.076,-4.227,123.145,-4.233,123.242,-4.113,123.247,-4.041,123.212,-3.998,123.025,-3.981,122.969,-4.03]},{"b":[125.863,-2.469,126.066,-1.975],"l":[125.976,-2.168,125.993,-2.012,125.963,-1.976,125.923,-1.975,125.873,-2.036,125.863,-2.077,125.903,-2.222,125.938,-2.263,125.978,-2.415,126.038,-2.469,126.055,-2.451,126.066,-2.366,125.976,-2.168]},{"b":[125.387,-1.94,126.332,-1.79],"l":[126.024,-1.79,125.72,-1.814,125.521,-1.801,125.445,-1.809,125.387,-1.843,125.426,-1.882,125.433,-1.938,125.479,-1.94,125.839,-1.906,125.956,-1.917,126.288,-1.859,126.332,-1.823,126.024,-1.79]},{"b":[124.33,-2.007,125.32,-1.636],"l":[125.188,-1.713,125.146,-1.693,125.127,-1.699,125.096,-1.741,125.063,-1.741,124.97,-1.705,124.664,-1.636,124.483,-1.644,124.418,-1.659,124.381,-1.687,124.33,-1.859,124.418,-2.005,124.521,-2.007,124.639,-1.978,124.834,-1.894,125.007,-1.943,125.135,-1.889,125.314,-1.877,125.32,-1.81,125.305,-1.794,125.258,-1.771,125.198,-1.78,125.188,-1.713]},{"b":[123.777,-1.996,123.866,-1.878],"l":[123.783,-1.878,123.777,-1.919,123.804,-1.994,123.866,-1.996,123.848,-1.955,123.783,-1.878]},{"b":[122.811,-1.616,123.547,-1.158],"l":[123.435,-1.237,123.339,-1.254,123.238,-1.389,123.198,-1.288,123.234,-1.234,123.212,-1.171,123.158,-1.158,122.972,-1.189,122.908,-1.182,122.832,-1.283,122.811,-1.432,122.858,-1.548,122.89,-1.587,122.984,-1.511,123.105,-1.34,123.15,-1.304,123.183,-1.493,123.123,-1.556,123.13,-1.577,123.173,-1.616,123.221,-1.598,123.237,-1.577,123.275,-1.437,123.329,-1.443,123.367,-1.507,123.449,-1.499,123.512,-1.447,123.547,-1.337,123.527,-1.286,123.489,-1.259,123.435,-1.237]},{"b":[123.071,-1.899,123.153,-1.773],"l":[123.071,-1.855,123.079,-1.899,123.153,-1.817,123.138,-1.773,123.106,-1.787,123.086,-1.815,123.071,-1.855]},{"b":[121.655,-0.526,121.907,-0.407],"l":[121.797,-0.418,121.749,-0.407,121.672,-0.479,121.655,-0.526,121.681,-0.525,121.722,-0.495,121.756,-0.491,121.847,-0.49,121.881,-0.503,121.907,-0.451,121.864,-0.407,121.797,-0.418]},{"b":[126.704,4.013,126.921,4.548],"l":[126.865,4.48,126.813,4.537,126.757,4.548,126.721,4.416,126.722,4.344,126.767,4.283,126.814,4.258,126.77,4.162,126.704,4.071,126.711,4.02,126.776,4.013,126.817,4.033,126.848,4.18,126.921,4.291,126.887,4.373,126.865,4.48]},{"b":[126.638,3.833,126.74,4.042],"l":[126.638,4.042,126.661,3.928,126.722,3.833,126.719,3.875,126.74,3.918,126.686,4.001,126.638,4.042]},{"b":[126.778,3.757,126.858,3.858],"l":[126.8,3.784,126.836,3.757,126.852,3.768,126.858,3.787,126.857,3.812,126.804,3.858,126.779,3.843,126.778,3.813,126.8,3.784]},{"b":[125.455,3.405,125.658,3.733],"l":[125.586,3.571,125.543,3.67,125.469,3.733,125.455,3.684,125.469,3.639,125.501,3.593,125.518,3.55,125.512,3.461,125.633,3.405,125.658,3.436,125.644,3.477,125.586,3.571]},{"b":[125.36,2.63,125.446,2.805],"l":[125.391,2.805,125.36,2.747,125.397,2.63,125.407,2.652,125.404,2.707,125.446,2.763,125.435,2.784,125.391,2.805]},{"b":[120.436,-6.465,120.549,-5.775],"l":[120.477,-5.775,120.446,-5.876,120.452,-6.095,120.436,-6.18,120.461,-6.254,120.468,-6.406,120.487,-6.465,120.528,-6.298,120.549,-5.969,120.534,-5.904,120.477,-5.775]},{"b":[131.751,-7.251,131.982,-7.104],"l":[131.922,-7.104,131.751,-7.117,131.778,-7.144,131.823,-7.159,131.884,-7.167,131.927,-7.225,131.97,-7.251,131.982,-7.202,131.922,-7.104]},{"b":[130.775,-8.35,131.176,-8.091],"l":[131.02,-8.091,130.833,-8.271,130.775,-8.35,130.862,-8.319,130.908,-8.246,131.044,-8.212,131.176,-8.131,131.087,-8.125,131.02,-8.091]},{"b":[131.087,-8.011,131.736,-7.113],"l":[131.701,-7.14,131.643,-7.113,131.561,-7.136,131.531,-7.165,131.535,-7.221,131.483,-7.251,131.446,-7.315,131.411,-7.34,131.349,-7.425,131.297,-7.438,131.26,-7.471,131.197,-7.617,131.19,-7.672,131.138,-7.685,131.137,-7.782,131.087,-7.865,131.123,-7.922,131.114,-7.997,131.185,-7.998,131.309,-8.011,131.326,-8,131.343,-7.981,131.348,-7.948,131.377,-7.869,131.474,-7.777,131.498,-7.731,131.58,-7.682,131.624,-7.626,131.691,-7.439,131.644,-7.267,131.736,-7.197,131.701,-7.14]},{"b":[129.592,-8.046,129.844,-7.795],"l":[129.655,-7.795,129.609,-7.803,129.599,-7.831,129.592,-7.917,129.713,-8.041,129.78,-8.046,129.839,-7.955,129.844,-7.889,129.813,-7.82,129.655,-7.795]},{"b":[128.53,-7.209,128.673,-7.069],"l":[128.628,-7.069,128.577,-7.083,128.53,-7.135,128.55,-7.156,128.625,-7.209,128.67,-7.183,128.667,-7.138,128.673,-7.113,128.658,-7.091,128.628,-7.069]},{"b":[127.786,-8.255,128.119,-8.099],"l":[127.998,-8.139,127.823,-8.099,127.786,-8.12,127.821,-8.19,128.024,-8.255,128.119,-8.171,128.099,-8.135,127.998,-8.139]},{"b":[127.355,-7.646,127.475,-7.513],"l":[127.371,-7.513,127.375,-7.572,127.355,-7.646,127.419,-7.623,127.464,-7.597,127.474,-7.579,127.475,-7.531,127.371,-7.513]},{"b":[125.798,-7.985,126.814,-7.572],"l":[126.726,-7.662,126.61,-7.572,126.463,-7.608,126.359,-7.677,126.214,-7.707,126.085,-7.697,125.975,-7.663,125.843,-7.817,125.808,-7.881,125.798,-7.985,125.826,-7.979,125.952,-7.911,126.04,-7.886,126.108,-7.884,126.171,-7.912,126.313,-7.918,126.472,-7.95,126.518,-7.87,126.577,-7.808,126.693,-7.754,126.813,-7.738,126.814,-7.717,126.801,-7.668,126.726,-7.662]},{"b":[125.507,-8.312,125.646,-8.14],"l":[125.507,-8.275,125.579,-8.312,125.646,-8.14,125.621,-8.15,125.584,-8.179,125.507,-8.275]},{"b":[123.326,-10.337,123.497,-10.14],"l":[123.395,-10.171,123.326,-10.264,123.326,-10.337,123.416,-10.303,123.405,-10.227,123.497,-10.194,123.494,-10.177,123.459,-10.14,123.395,-10.171]},{"b":[122.818,-10.91,123.418,-10.472],"l":[123.34,-10.486,123.265,-10.518,123.146,-10.64,123.061,-10.698,122.846,-10.762,122.818,-10.811,122.826,-10.899,122.856,-10.91,122.949,-10.909,123.005,-10.876,123.215,-10.806,123.311,-10.698,123.418,-10.651,123.413,-10.623,123.383,-10.568,123.371,-10.475,123.358,-10.472,123.34,-10.486]},{"b":[121.705,-10.602,121.998,-10.433],"l":[121.796,-10.507,121.705,-10.556,121.726,-10.573,121.833,-10.602,121.883,-10.59,121.981,-10.528,121.998,-10.447,121.95,-10.433,121.867,-10.439,121.796,-10.507]},{"b":[124.356,-8.445,125.132,-8.135],"l":[124.752,-8.16,124.677,-8.168,124.6,-8.202,124.576,-8.141,124.509,-8.135,124.431,-8.183,124.394,-8.253,124.426,-8.296,124.356,-8.386,124.381,-8.415,124.444,-8.445,125.097,-8.353,125.132,-8.326,125.125,-8.205,125.05,-8.18,124.924,-8.166,124.752,-8.16]},{"b":[123.928,-8.552,124.304,-8.202],"l":[124.24,-8.203,124.111,-8.364,124.096,-8.356,124.069,-8.318,124.014,-8.319,123.971,-8.354,123.928,-8.449,124.017,-8.444,124.066,-8.552,124.147,-8.531,124.184,-8.499,124.226,-8.391,124.287,-8.329,124.304,-8.229,124.287,-8.209,124.266,-8.202,124.24,-8.203]},{"b":[123.23,-8.587,123.925,-8.19],"l":[123.776,-8.19,123.601,-8.291,123.573,-8.292,123.53,-8.265,123.473,-8.267,123.391,-8.28,123.395,-8.301,123.425,-8.313,123.476,-8.322,123.455,-8.354,123.325,-8.439,123.23,-8.531,123.253,-8.539,123.33,-8.536,123.411,-8.587,123.434,-8.576,123.489,-8.532,123.553,-8.567,123.58,-8.545,123.588,-8.524,123.583,-8.502,123.592,-8.478,123.629,-8.422,123.698,-8.424,123.784,-8.3,123.925,-8.272,123.896,-8.239,123.846,-8.213,123.776,-8.19]},{"b":[123.025,-8.399,123.336,-8.235],"l":[123.217,-8.235,123.133,-8.254,123.108,-8.275,123.033,-8.338,123.025,-8.396,123.297,-8.399,123.317,-8.355,123.336,-8.269,123.217,-8.235]},{"b":[119.379,-8.75,119.557,-8.429],"l":[119.471,-8.456,119.446,-8.429,119.43,-8.455,119.42,-8.539,119.379,-8.587,119.402,-8.647,119.386,-8.736,119.425,-8.75,119.464,-8.741,119.444,-8.672,119.483,-8.628,119.536,-8.589,119.555,-8.553,119.557,-8.519,119.547,-8.483,119.502,-8.481,119.482,-8.473,119.471,-8.456]},{"b":[119.021,-8.24,119.135,-8.139],"l":[119.037,-8.158,119.021,-8.2,119.03,-8.24,119.074,-8.239,119.107,-8.223,119.135,-8.197,119.128,-8.177,119.098,-8.139,119.079,-8.14,119.037,-8.158]},{"b":[115.48,-8.804,115.613,-8.67],"l":[115.48,-8.715,115.501,-8.743,115.582,-8.804,115.61,-8.77,115.613,-8.713,115.561,-8.67,115.541,-8.675,115.48,-8.715]},{"b":[115.22,-6.988,115.546,-6.838],"l":[115.414,-6.84,115.354,-6.838,115.241,-6.861,115.222,-6.905,115.22,-6.953,115.296,-6.988,115.377,-6.971,115.424,-6.941,115.546,-6.939,115.524,-6.902,115.479,-6.87,115.414,-6.84]},{"b":[112.586,-5.846,112.727,-5.726],"l":[112.586,-5.804,112.602,-5.844,112.698,-5.846,112.719,-5.811,112.727,-5.753,112.69,-5.726,112.649,-5.731,112.586,-5.804]},{"b":[117.625,4.004,117.923,4.262],"l":[117.708,4.262,117.662,4.25,117.667,4.204,117.625,4.121,117.737,4.004,117.923,4.054,117.918,4.091,117.885,4.186,117.761,4.252,117.708,4.262]},{"b":[117.538,3.248,117.681,3.436],"l":[117.548,3.432,117.538,3.386,117.56,3.328,117.646,3.248,117.658,3.281,117.681,3.408,117.637,3.436,117.548,3.432]},{"b":[116.327,-3.637,116.427,-3.4],"l":[116.327,-3.539,116.388,-3.637,116.424,-3.464,116.427,-3.4,116.395,-3.423,116.327,-3.539]},{"b":[116.018,-4.054,116.319,-3.251],"l":[116.282,-3.535,116.295,-3.495,116.287,-3.449,116.262,-3.395,116.27,-3.251,116.239,-3.26,116.117,-3.34,116.064,-3.458,116.022,-3.612,116.018,-3.7,116.077,-3.817,116.059,-4.007,116.093,-4.054,116.303,-3.868,116.289,-3.821,116.319,-3.763,116.305,-3.719,116.282,-3.535]},{"b":[108.804,-1.662,108.957,-1.54],"l":[108.877,-1.54,108.804,-1.568,108.838,-1.662,108.953,-1.62,108.957,-1.564,108.877,-1.54]},{"b":[109.428,-1.283,109.761,-0.979],"l":[109.7,-1.007,109.615,-0.979,109.476,-0.985,109.45,-1.044,109.428,-1.241,109.464,-1.278,109.511,-1.283,109.71,-1.181,109.751,-1.145,109.761,-1.105,109.743,-1.039,109.7,-1.007]},{"b":[108.787,2.853,108.888,2.999],"l":[108.787,2.886,108.839,2.853,108.888,2.905,108.886,2.999,108.867,2.992,108.787,2.886]},{"b":[108.002,3.653,108.399,4.217],"l":[108.393,3.986,108.256,4.152,108.248,4.217,108.202,4.2,108.004,4.043,108.002,3.983,108.045,3.889,108.088,3.852,108.243,3.81,108.236,3.785,108.216,3.772,108.186,3.768,108.1,3.705,108.18,3.653,108.316,3.69,108.394,3.836,108.399,3.876,108.393,3.986]},{"b":[106.201,3.088,106.285,3.23],"l":[106.215,3.129,106.284,3.088,106.285,3.157,106.271,3.216,106.224,3.23,106.201,3.205,106.215,3.129]},{"b":[105.692,2.859,105.837,3.063],"l":[105.731,3.037,105.692,3.063,105.692,3.011,105.704,2.981,105.708,2.94,105.706,2.889,105.719,2.859,105.76,2.863,105.809,2.904,105.837,2.977,105.822,2.984,105.795,2.996,105.76,3.013,105.731,3.037]},{"b":[101.403,1.716,101.774,2.127],"l":[101.641,2.127,101.545,2.061,101.45,2.068,101.41,2.022,101.403,1.901,101.468,1.759,101.501,1.733,101.603,1.716,101.719,1.789,101.734,1.883,101.774,1.943,101.762,1.997,101.708,2.078,101.641,2.127]},{"b":[102.229,0.96,102.449,1.397],"l":[102.276,1.395,102.256,1.397,102.229,1.348,102.234,1.264,102.255,1.147,102.28,1.076,102.325,1.007,102.381,0.96,102.427,0.99,102.429,1.067,102.449,1.156,102.443,1.234,102.413,1.261,102.359,1.346,102.276,1.395]},{"b":[102.018,1.331,102.499,1.625],"l":[102.492,1.459,102.47,1.51,102.042,1.625,102.024,1.608,102.018,1.586,102.021,1.558,102.079,1.499,102.161,1.465,102.274,1.453,102.367,1.415,102.425,1.364,102.499,1.331,102.492,1.459]},{"b":[102.454,0.708,103.028,1.13],"l":[102.78,0.959,102.726,0.989,102.633,1.054,102.549,1.13,102.507,1.089,102.491,0.987,102.466,0.95,102.454,0.89,102.49,0.857,102.542,0.832,102.711,0.784,102.776,0.78,102.971,0.737,103.009,0.708,103.028,0.747,103.002,0.859,102.944,0.893,102.78,0.959]},{"b":[102.702,0.842,103.166,1.165],"l":[103.068,1.015,102.999,1.068,102.79,1.165,102.726,1.159,102.702,1.054,102.726,1.041,102.788,1.031,102.886,0.997,102.964,0.943,103.033,0.882,103.087,0.848,103.137,0.842,103.166,0.87,103.068,1.015]},{"b":[103.14,0.536,103.295,0.7],"l":[103.238,0.699,103.187,0.7,103.153,0.643,103.14,0.549,103.172,0.536,103.284,0.542,103.295,0.614,103.238,0.699]},{"b":[103.315,0.993,103.43,1.134],"l":[103.38,1.134,103.355,1.117,103.315,1.071,103.363,1.007,103.43,0.993,103.424,1.048,103.405,1.073,103.38,1.134]},{"b":[103.344,0.651,103.497,0.87],"l":[103.386,0.87,103.366,0.851,103.344,0.778,103.43,0.651,103.45,0.664,103.497,0.723,103.47,0.778,103.433,0.825,103.386,0.87]},{"b":[103.74,0.772,103.834,0.891],"l":[103.752,0.891,103.74,0.872,103.742,0.83,103.834,0.772,103.829,0.801,103.807,0.846,103.752,0.891]},{"b":[103.932,0.99,104.14,1.181],"l":[104.025,1.181,104,1.137,103.955,1.137,103.947,1.087,103.932,1.071,103.94,1.046,103.964,1.013,104.066,0.99,104.127,1.092,104.138,1.128,104.14,1.166,104.088,1.137,104.025,1.181]},{"b":[104.244,0.832,104.663,1.216],"l":[104.591,1.141,104.585,1.216,104.5,1.18,104.428,1.196,104.362,1.181,104.25,1.103,104.244,1.077,104.252,1.015,104.294,1.016,104.439,1.05,104.462,0.996,104.429,0.956,104.481,0.933,104.471,0.913,104.481,0.887,104.504,0.853,104.575,0.832,104.599,0.859,104.653,0.961,104.663,1.05,104.648,1.105,104.591,1.141]},{"b":[104.098,0.805,104.239,0.944],"l":[104.171,0.897,104.123,0.944,104.108,0.934,104.101,0.917,104.098,0.896,104.177,0.805,104.239,0.834,104.227,0.88,104.171,0.897]},{"b":[104.499,0.035,104.698,0.232],"l":[104.544,0.223,104.499,0.232,104.604,0.095,104.622,0.08,104.651,0.063,104.698,0.035,104.689,0.06,104.66,0.103,104.544,0.223]},{"b":[104.447,-0.323,105.005,0.018],"l":[104.843,-0.141,104.808,-0.192,104.779,-0.176,104.713,-0.103,104.653,-0.076,104.658,-0.063,104.636,-0.018,104.543,0.018,104.497,-0.126,104.447,-0.189,104.474,-0.212,104.567,-0.246,104.702,-0.209,104.914,-0.323,104.929,-0.317,104.951,-0.284,105.005,-0.283,104.95,-0.247,104.909,-0.212,104.843,-0.141]},{"b":[104.257,-0.659,104.59,-0.335],"l":[104.364,-0.403,104.341,-0.383,104.319,-0.38,104.302,-0.386,104.257,-0.463,104.33,-0.539,104.363,-0.659,104.414,-0.584,104.485,-0.613,104.507,-0.597,104.544,-0.521,104.59,-0.467,104.568,-0.432,104.474,-0.335,104.364,-0.403]},{"b":[103.461,-0.383,103.764,-0.228],"l":[103.611,-0.231,103.549,-0.228,103.479,-0.297,103.461,-0.358,103.606,-0.383,103.737,-0.348,103.764,-0.318,103.724,-0.277,103.611,-0.231]},{"b":[107.563,-3.227,108.291,-2.53],"l":[108.215,-2.697,108.074,-2.597,107.875,-2.56,107.838,-2.53,107.666,-2.566,107.642,-2.732,107.598,-2.8,107.605,-2.863,107.563,-2.92,107.584,-2.941,107.592,-2.977,107.595,-3.058,107.637,-3.125,107.614,-3.209,107.66,-3.206,107.822,-3.161,107.837,-3.097,107.858,-3.086,107.941,-3.129,107.967,-3.167,107.977,-3.222,108.055,-3.227,108.084,-3.195,108.167,-3.143,108.192,-3.103,108.207,-2.998,108.291,-2.83,108.215,-2.697]},{"b":[107.402,-2.925,107.5,-2.835],"l":[107.433,-2.925,107.473,-2.9,107.5,-2.845,107.474,-2.835,107.419,-2.838,107.402,-2.873,107.409,-2.901,107.433,-2.925]},{"b":[106.743,-3.025,106.911,-2.899],"l":[106.797,-2.899,106.743,-2.933,106.749,-2.96,106.774,-2.987,106.814,-3.014,106.87,-3.025,106.886,-3.005,106.911,-2.934,106.797,-2.899]},{"b":[105.121,-6.662,105.277,-6.524],"l":[105.121,-6.615,105.143,-6.643,105.19,-6.662,105.253,-6.64,105.277,-6.561,105.261,-6.524,105.226,-6.529,105.192,-6.546,105.121,-6.615]},{"b":[97.079,0.564,97.932,1.54],"l":[97.786,1.146,97.698,1.184,97.482,1.465,97.356,1.54,97.343,1.528,97.324,1.482,97.244,1.424,97.079,1.425,97.297,1.187,97.369,1.057,97.405,0.947,97.461,0.941,97.604,0.834,97.683,0.641,97.684,0.596,97.82,0.564,97.876,0.628,97.902,0.884,97.932,0.974,97.903,1.018,97.786,1.146]},{"b":[102.111,-5.483,102.405,-5.286],"l":[102.372,-5.366,102.198,-5.289,102.154,-5.286,102.111,-5.323,102.136,-5.361,102.286,-5.483,102.367,-5.479,102.405,-5.405,102.372,-5.366]},{"b":[100.179,-3.329,100.469,-2.783],"l":[100.464,-3.117,100.469,-3.039,100.455,-3.002,100.246,-2.783,100.199,-2.786,100.179,-2.82,100.204,-2.987,100.26,-3.057,100.332,-3.113,100.348,-3.159,100.346,-3.229,100.465,-3.329,100.425,-3.183,100.434,-3.141,100.464,-3.117]},{"b":[99.968,-2.821,100.204,-2.51],"l":[100.012,-2.51,99.988,-2.525,99.969,-2.594,99.968,-2.61,99.997,-2.649,99.992,-2.77,100.015,-2.82,100.133,-2.821,100.204,-2.741,100.202,-2.68,100.012,-2.51]},{"b":[99.537,-2.37,99.848,-2.017],"l":[99.735,-2.178,99.686,-2.063,99.622,-2.017,99.572,-2.026,99.562,-2.051,99.559,-2.115,99.537,-2.162,99.607,-2.258,99.685,-2.282,99.848,-2.37,99.843,-2.343,99.816,-2.284,99.735,-2.178]},{"b":[98.602,-1.783,99.271,-0.916],"l":[99.131,-1.442,99.14,-1.418,99.129,-1.384,99.101,-1.34,99.065,-1.241,98.955,-1.056,98.933,-0.954,98.869,-0.916,98.676,-0.971,98.602,-1.198,98.627,-1.261,98.816,-1.538,98.828,-1.61,98.874,-1.664,99.072,-1.783,99.164,-1.778,99.271,-1.738,99.267,-1.628,99.21,-1.559,99.131,-1.442]},{"b":[98.31,-0.577,98.544,0.007],"l":[98.415,-0.018,98.375,0.007,98.323,-0.001,98.427,-0.226,98.409,-0.309,98.355,-0.379,98.34,-0.468,98.31,-0.532,98.4,-0.577,98.459,-0.53,98.52,-0.38,98.544,-0.258,98.484,-0.168,98.415,-0.018]},{"b":[97.108,2.053,97.334,2.232],"l":[97.291,2.201,97.253,2.216,97.157,2.232,97.108,2.217,97.225,2.158,97.328,2.053,97.334,2.076,97.329,2.149,97.291,2.201]},{"b":[95.717,2.351,96.464,2.916],"l":[96.417,2.515,96.18,2.661,96.13,2.721,96.102,2.741,95.998,2.781,95.896,2.889,95.806,2.916,95.772,2.855,95.717,2.826,95.733,2.767,95.809,2.656,95.88,2.641,95.938,2.598,96.022,2.596,96.29,2.43,96.341,2.372,96.401,2.351,96.464,2.36,96.459,2.416,96.443,2.466,96.417,2.515]},{"b":[95.218,5.784,95.366,5.907],"l":[95.242,5.907,95.218,5.89,95.283,5.799,95.343,5.784,95.362,5.812,95.366,5.843,95.359,5.877,95.283,5.898,95.242,5.907]},{"b":[111.3,2.402,111.389,2.768],"l":[111.376,2.576,111.378,2.709,111.355,2.764,111.333,2.768,111.3,2.741,111.312,2.438,111.359,2.402,111.389,2.415,111.38,2.459,111.376,2.576]},{"b":[117.06,7.115,117.281,7.352],"l":[117.064,7.261,117.06,7.179,117.081,7.115,117.142,7.168,117.239,7.185,117.267,7.221,117.281,7.291,117.264,7.352,117.147,7.337,117.064,7.261]},{"b":[31.509,46.155,32.15,46.373],"l":[31.585,46.303,31.509,46.373,31.529,46.307,31.564,46.258,31.7,46.214,32.009,46.168,32.15,46.155,32.012,46.204,31.638,46.273,31.585,46.303]},{"b":[22.056,58.69,23.009,59.087],"l":[22.411,58.863,22.479,58.754,22.473,58.712,22.542,58.69,22.661,58.709,22.767,58.821,22.793,58.797,22.842,58.777,22.924,58.827,23.009,58.834,22.982,58.92,22.91,58.991,22.725,59.015,22.712,59.032,22.702,59.074,22.649,59.087,22.587,59.081,22.505,59.026,22.463,58.974,22.056,58.944,22.162,58.898,22.307,58.895,22.411,58.863]},{"b":[23.063,58.54,23.356,58.678],"l":[23.165,58.678,23.109,58.659,23.063,58.611,23.26,58.54,23.344,58.55,23.356,58.576,23.333,58.649,23.165,58.678]},{"b":[35.529,64.977,35.858,65.198],"l":[35.858,65.078,35.848,65.143,35.816,65.182,35.729,65.198,35.609,65.157,35.586,65.167,35.529,65.151,35.559,65.094,35.621,65.059,35.68,65.058,35.779,64.977,35.842,65.001,35.827,65.036,35.858,65.078]},{"b":[42.46,66.688,42.714,66.796],"l":[42.631,66.782,42.547,66.796,42.469,66.786,42.46,66.77,42.477,66.735,42.676,66.688,42.714,66.702,42.691,66.735,42.631,66.782]},{"b":[26.408,78.646,27.008,78.811],"l":[26.789,78.724,26.586,78.811,26.456,78.81,26.408,78.784,26.46,78.72,26.729,78.646,26.876,78.649,27.008,78.698,26.789,78.724]},{"b":[27.889,78.829,29.697,78.971],"l":[29.047,78.912,28.845,78.971,28.511,78.967,28.415,78.961,28.374,78.927,28.121,78.908,27.889,78.852,28.038,78.829,28.495,78.887,28.881,78.88,29.311,78.852,29.697,78.905,29.645,78.922,29.345,78.906,29.047,78.912]},{"b":[49.556,80.061,50.319,80.23],"l":[50.319,80.172,50.31,80.186,50.251,80.219,49.884,80.23,49.556,80.159,49.588,80.136,49.971,80.061,50.052,80.074,50.072,80.109,50.319,80.172]},{"b":[50.091,79.921,51.435,80.094],"l":[51.243,79.991,51.238,80.01,51.254,80.049,50.936,80.094,50.676,80.049,50.473,80.035,50.091,79.981,50.454,79.924,51.076,79.932,51.431,79.921,51.435,79.932,51.409,79.944,51.327,79.972,51.243,79.991]},{"b":[31.482,80.081,33.629,80.242],"l":[31.482,80.108,31.578,80.081,32.526,80.119,33.557,80.198,33.629,80.217,33.384,80.242,33.099,80.229,33.019,80.218,31.482,80.108]},{"b":[55.466,81.136,58.564,81.565],"l":[57.456,81.543,57.365,81.535,57.092,81.541,56.973,81.511,56.719,81.423,56.405,81.387,56.157,81.303,55.782,81.329,55.466,81.311,55.573,81.228,55.717,81.188,56.192,81.224,56.364,81.179,56.513,81.175,56.669,81.198,56.822,81.238,57.159,81.178,57.451,81.136,57.77,81.17,57.913,81.198,58.015,81.255,57.912,81.303,57.859,81.368,58.372,81.387,58.564,81.418,58.436,81.464,58.017,81.484,57.863,81.506,57.81,81.546,57.717,81.565,57.456,81.543]},{"b":[54.045,80.628,57.694,81.116],"l":[54.634,81.113,54.417,80.987,54.367,80.904,54.241,80.902,54.045,80.872,54.067,80.814,54.376,80.787,54.533,80.783,54.623,80.765,54.668,80.739,55.117,80.752,55.541,80.703,55.713,80.637,55.883,80.628,56.316,80.633,56.815,80.664,57.58,80.755,57.694,80.792,57.568,80.82,56.91,80.913,56.472,80.998,56.17,81.029,55.471,81.02,54.719,81.116,54.634,81.113]},{"b":[50.368,81.041,50.946,81.171],"l":[50.754,81.047,50.789,81.072,50.946,81.108,50.879,81.151,50.716,81.171,50.592,81.169,50.522,81.158,50.506,81.144,50.465,81.126,50.368,81.123,50.377,81.103,50.412,81.084,50.518,81.046,50.616,81.041,50.754,81.047]},{"b":[57.211,80.765,58.931,81.118],"l":[58.05,81.118,57.656,81.032,57.41,81.047,57.211,81.017,57.405,80.915,57.75,80.889,57.938,80.793,58.286,80.765,58.642,80.768,58.86,80.779,58.931,80.832,58.903,80.898,58.815,80.934,58.762,80.991,58.622,81.042,58.508,81.062,58.19,81.095,58.102,81.114,58.05,81.118]},{"b":[62.106,81.597,63.782,81.719],"l":[62.885,81.609,63.529,81.597,63.651,81.609,63.782,81.65,63.767,81.664,63.71,81.687,62.795,81.719,62.284,81.707,62.106,81.679,62.515,81.659,62.531,81.647,62.573,81.633,62.885,81.609]},{"b":[57.909,81.696,59.408,81.854],"l":[59.356,81.759,59.357,81.781,59.408,81.825,59.262,81.854,58.135,81.828,57.985,81.797,57.945,81.748,57.909,81.722,57.921,81.71,57.965,81.696,58.295,81.715,59.356,81.759]},{"b":[18.162,80.246,18.742,80.358],"l":[18.525,80.246,18.742,80.301,18.519,80.348,18.292,80.358,18.206,80.332,18.162,80.288,18.525,80.246]},{"b":[10.558,78.225,12.116,78.903],"l":[11.929,78.375,11.885,78.409,11.826,78.436,11.616,78.475,11.424,78.549,11.262,78.542,11.25,78.611,11.155,78.641,11.078,78.686,11.153,78.724,11.124,78.753,10.961,78.846,10.773,78.888,10.558,78.903,10.558,78.838,10.628,78.754,10.789,78.687,10.841,78.645,11.121,78.463,11.199,78.441,11.372,78.439,11.587,78.388,11.757,78.329,11.965,78.225,12.116,78.233,12.056,78.306,11.929,78.375]},{"b":[18.797,74.352,19.275,74.518],"l":[18.861,74.514,18.797,74.486,18.918,74.411,19.099,74.352,19.219,74.391,19.275,74.457,19.262,74.479,19.183,74.518,18.861,74.514]},{"b":[52.24,70.969,53.205,71.404],"l":[53.141,71.242,53.074,71.238,52.994,71.291,52.903,71.365,52.777,71.4,52.732,71.404,52.72,71.39,52.73,71.355,52.617,71.383,52.513,71.385,52.297,71.357,52.24,71.325,52.25,71.285,52.289,71.27,52.425,71.239,52.547,71.25,52.738,71.181,52.789,71.115,52.835,71.086,52.95,71.054,53.004,71.012,53.023,70.969,53.121,70.982,53.106,70.999,53.048,71.031,53.071,71.065,53.205,71.16,53.193,71.215,53.141,71.242]},{"b":[74.1,72.864,74.962,73.13],"l":[74.409,73.13,74.199,73.109,74.142,73.074,74.1,73.022,74.181,72.975,74.435,72.908,74.588,72.881,74.638,72.864,74.661,72.873,74.697,72.908,74.66,72.929,74.647,72.969,74.743,73.033,74.962,73.063,74.725,73.108,74.6,73.122,74.409,73.13]},{"b":[75.344,73.432,76.052,73.574],"l":[76.052,73.549,76.039,73.56,75.93,73.574,75.57,73.541,75.375,73.477,75.344,73.432,75.504,73.457,75.827,73.459,75.901,73.481,76.052,73.549]},{"b":[76.083,73.44,76.756,73.555],"l":[76.251,73.555,76.14,73.554,76.083,73.523,76.234,73.476,76.659,73.44,76.756,73.446,76.251,73.555]},{"b":[76.871,72.282,78.365,72.631],"l":[77.749,72.631,77.579,72.631,77.378,72.565,77.26,72.486,77.15,72.439,76.903,72.366,76.871,72.317,76.906,72.298,77.146,72.282,77.633,72.291,77.781,72.309,78.007,72.392,78.154,72.417,78.365,72.482,78.353,72.504,78.279,72.553,77.749,72.631]},{"b":[78.633,72.711,79.541,73.094],"l":[79.413,72.983,79.357,73.039,79.164,73.094,78.657,72.892,78.633,72.851,78.69,72.803,78.881,72.752,79.431,72.711,79.501,72.722,79.538,72.769,79.541,72.919,79.413,72.983]},{"b":[82.329,74.056,82.71,74.161],"l":[82.382,74.149,82.329,74.131,82.382,74.099,82.478,74.076,82.613,74.056,82.71,74.091,82.689,74.111,82.611,74.149,82.526,74.161,82.382,74.149]},{"b":[82.818,74.04,83.618,74.152],"l":[83.15,74.152,82.903,74.129,82.818,74.092,83.159,74.075,83.411,74.04,83.45,74.052,83.496,74.048,83.549,74.072,83.618,74.089,83.513,74.122,83.15,74.152]},{"b":[84.389,74.4,84.873,74.516],"l":[84.54,74.49,84.389,74.454,84.429,74.43,84.71,74.4,84.759,74.459,84.873,74.516,84.68,74.512,84.54,74.49]},{"b":[86.259,74.831,87.124,74.993],"l":[86.653,74.981,86.605,74.993,86.504,74.966,86.331,74.939,86.259,74.894,86.391,74.851,86.692,74.848,86.927,74.831,87.012,74.862,87.124,74.94,87.052,74.983,87.001,74.992,86.737,74.963,86.653,74.981]},{"b":[81.501,75.247,82.222,75.516],"l":[82.022,75.513,81.913,75.498,81.91,75.46,81.927,75.41,81.842,75.407,81.712,75.451,81.501,75.368,81.532,75.34,81.579,75.331,81.655,75.289,81.698,75.281,81.861,75.317,81.905,75.263,81.979,75.247,82.05,75.341,82.179,75.339,82.222,75.351,82.209,75.387,82.172,75.419,82.166,75.516,82.022,75.513]},{"b":[95.311,76.122,96.614,76.305],"l":[96.271,76.305,96.151,76.272,95.786,76.294,95.594,76.25,95.38,76.289,95.322,76.262,95.311,76.215,95.679,76.194,95.845,76.16,96.109,76.155,96.301,76.122,96.353,76.177,96.351,76.212,96.487,76.234,96.59,76.221,96.614,76.264,96.532,76.278,96.271,76.305]},{"b":[97.31,76.584,97.588,76.707],"l":[97.31,76.69,97.342,76.629,97.43,76.591,97.535,76.584,97.588,76.599,97.382,76.707,97.31,76.69]},{"b":[95.27,76.975,96.562,77.206],"l":[95.854,77.098,95.421,77.056,95.27,77.019,95.364,77.012,95.681,77.021,95.766,76.991,95.855,76.975,96.091,77.003,96.21,76.992,96.254,77.007,96.285,77.027,96.424,77.071,96.561,77.13,96.562,77.154,96.528,77.206,95.854,77.098]},{"b":[89.142,77.184,89.68,77.311],"l":[89.616,77.311,89.282,77.301,89.2,77.272,89.142,77.227,89.179,77.21,89.3,77.184,89.514,77.189,89.666,77.254,89.68,77.28,89.616,77.311]},{"b":[76.052,79.49,77.589,79.664],"l":[76.467,79.643,76.373,79.615,76.249,79.651,76.148,79.664,76.052,79.645,76.072,79.626,76.154,79.579,76.458,79.545,76.637,79.544,76.65,79.493,76.81,79.49,77.589,79.502,77.549,79.524,77.36,79.557,76.467,79.643]},{"b":[78.978,80.812,80.428,80.975],"l":[79.217,80.96,79.11,80.924,78.978,80.848,79.007,80.835,79.099,80.812,80.027,80.848,80.345,80.868,80.373,80.883,80.428,80.928,80.28,80.95,79.807,80.975,79.217,80.96]},{"b":[89.901,81.064,91.567,81.214],"l":[91.478,81.184,91.109,81.199,90.07,81.214,89.901,81.171,89.919,81.149,89.976,81.113,91.223,81.064,91.567,81.141,91.478,81.184]},{"b":[106.416,78.058,107.696,78.189],"l":[106.583,78.168,106.416,78.14,107.002,78.096,107.344,78.099,107.482,78.058,107.606,78.083,107.696,78.131,107.573,78.186,107.508,78.189,106.583,78.168]},{"b":[106.024,78.199,106.72,78.34],"l":[106.679,78.265,106.719,78.265,106.72,78.294,106.691,78.317,106.64,78.336,106.457,78.34,106.351,78.273,106.058,78.265,106.024,78.22,106.151,78.199,106.27,78.206,106.472,78.245,106.505,78.262,106.679,78.265]},{"b":[107.27,77.242,107.679,77.347],"l":[107.366,77.347,107.27,77.289,107.302,77.242,107.415,77.243,107.679,77.268,107.665,77.3,107.629,77.32,107.594,77.33,107.486,77.347,107.366,77.347]},{"b":[111.969,76.45,112.661,76.644],"l":[112.154,76.549,112.297,76.538,112.395,76.484,112.532,76.45,112.575,76.452,112.587,76.483,112.614,76.499,112.661,76.51,112.633,76.553,112.478,76.621,112.394,76.644,112.281,76.618,112.011,76.633,111.969,76.626,112.003,76.603,112.154,76.549]},{"b":[119.64,73.045,120.261,73.168],"l":[120.079,73.157,119.964,73.168,119.762,73.155,119.64,73.124,119.792,73.045,120.008,73.045,120.261,73.09,120.237,73.107,120.079,73.157]},{"b":[124.336,73.848,124.653,73.943],"l":[124.43,73.943,124.337,73.928,124.336,73.91,124.366,73.875,124.482,73.848,124.543,73.85,124.653,73.888,124.637,73.9,124.548,73.934,124.43,73.943]},{"b":[135.452,75.382,136.169,75.845],"l":[136.169,75.606,136.127,75.626,135.905,75.694,135.849,75.729,135.788,75.798,135.699,75.845,135.614,75.766,135.578,75.71,135.561,75.636,135.593,75.576,135.523,75.496,135.473,75.463,135.452,75.39,135.746,75.382,135.949,75.41,136.021,75.438,135.965,75.486,135.983,75.522,136.169,75.606]},{"b":[148.399,76.648,149.406,76.782],"l":[149.406,76.782,148.72,76.747,148.448,76.677,148.399,76.648,149.15,76.66,149.205,76.677,149.268,76.747,149.406,76.782]},{"b":[135.387,73.885,136.259,74.253],"l":[136.037,74.09,135.628,74.22,135.387,74.253,135.402,74.202,135.449,74.18,135.633,74.121,135.715,74.06,136.051,73.929,136.122,73.885,136.197,73.914,136.259,73.985,136.037,74.09]},{"b":[137.064,71.423,137.96,71.588],"l":[137.282,71.58,137.168,71.557,137.129,71.556,137.082,71.543,137.064,71.53,137.079,71.502,137.266,71.456,137.344,71.461,137.403,71.477,137.458,71.483,137.512,71.475,137.613,71.434,137.712,71.423,137.96,71.508,137.934,71.543,137.858,71.583,137.817,71.588,137.282,71.58]},{"b":[160.437,70.806,160.719,70.934],"l":[160.566,70.924,160.449,70.934,160.44,70.923,160.437,70.851,160.505,70.82,160.651,70.806,160.719,70.823,160.645,70.884,160.566,70.924]},{"b":[167.789,69.571,169.434,70.016],"l":[168.358,70.016,168.196,70.008,168.06,69.975,167.865,69.901,167.814,69.873,167.789,69.837,167.821,69.82,167.993,69.776,168.144,69.713,168.348,69.664,168.916,69.571,169.201,69.58,169.246,69.601,169.263,69.629,169.299,69.735,169.332,69.77,169.418,69.779,169.434,69.832,169.421,69.856,169.375,69.883,168.358,70.016]},{"b":[163.427,58.509,164.662,59.227],"l":[164.573,59.221,164.517,59.227,164.202,59.096,163.761,59.015,163.767,58.972,163.784,58.93,163.727,58.799,163.577,58.641,163.427,58.579,163.432,58.546,163.447,58.525,163.471,58.509,163.635,58.603,163.96,58.744,164.279,58.838,164.616,58.886,164.662,58.971,164.629,59.112,164.573,59.221]},{"b":[167.442,54.513,168.081,54.857],"l":[167.711,54.77,167.592,54.798,167.512,54.857,167.442,54.856,167.488,54.795,167.677,54.698,168.081,54.513,168.039,54.565,167.883,54.69,167.711,54.77]},{"b":[165.751,54.694,166.65,55.351],"l":[166.577,54.908,166.479,54.95,166.404,55.006,166.248,55.165,166.23,55.242,166.276,55.312,166.212,55.324,165.931,55.351,165.83,55.307,165.751,55.295,165.992,55.19,166.066,55.136,166.082,55.077,166.12,55.03,166.23,54.937,166.325,54.865,166.382,54.838,166.464,54.827,166.521,54.768,166.645,54.694,166.65,54.839,166.577,54.908]},{"b":[156.168,50.634,156.488,50.862],"l":[156.376,50.862,156.213,50.785,156.168,50.732,156.196,50.702,156.326,50.639,156.365,50.634,156.405,50.658,156.483,50.751,156.488,50.843,156.456,50.86,156.376,50.862]},{"b":[155.195,50.041,156.123,50.772],"l":[156.097,50.772,156.002,50.757,155.885,50.684,155.773,50.482,155.68,50.401,155.434,50.369,155.327,50.293,155.218,50.298,155.195,50.265,155.243,50.213,155.243,50.095,155.289,50.061,155.397,50.041,155.449,50.078,155.516,50.146,155.608,50.177,155.792,50.202,155.921,50.302,156.044,50.452,156.101,50.559,156.123,50.671,156.097,50.772]},{"b":[153.992,48.739,154.228,48.905],"l":[154.126,48.904,154.092,48.832,153.992,48.773,154.001,48.756,154.043,48.739,154.081,48.79,154.205,48.857,154.228,48.892,154.199,48.905,154.126,48.904]},{"b":[155.467,50.811,155.654,50.934],"l":[155.645,50.822,155.654,50.845,155.64,50.91,155.569,50.934,155.467,50.914,155.483,50.87,155.513,50.837,155.554,50.811,155.645,50.822]},{"b":[152.984,47.706,153.101,47.809],"l":[152.984,47.728,153.004,47.713,153.054,47.706,153.101,47.763,153.079,47.809,153.049,47.797,152.984,47.728]},{"b":[151.715,46.787,152.289,47.143],"l":[151.864,46.869,151.715,46.853,151.723,46.829,151.754,46.788,151.816,46.787,152.002,46.897,152.289,47.142,152.235,47.143,152.166,47.11,152.04,47.015,151.864,46.869]},{"b":[149.447,45.591,150.553,46.213],"l":[149.962,46.022,149.796,45.876,149.666,45.84,149.447,45.593,149.539,45.591,149.688,45.642,149.883,45.783,149.954,45.822,150.057,45.849,150.195,45.933,150.235,46.012,150.553,46.209,150.349,46.213,150.309,46.2,149.962,46.022]},{"b":[146.897,44.404,148.837,45.526],"l":[148.812,45.51,148.773,45.526,148.707,45.521,148.612,45.485,148.324,45.282,148.13,45.258,148.056,45.262,147.965,45.378,147.924,45.383,147.873,45.3,147.886,45.226,147.769,45.191,147.658,45.093,147.558,45.062,147.43,44.945,147.247,44.856,147.155,44.766,147.141,44.663,146.974,44.566,146.933,44.513,146.897,44.404,147.098,44.531,147.207,44.554,147.31,44.678,147.563,44.836,147.61,44.887,147.622,44.945,147.658,44.977,147.784,44.959,147.914,44.99,148.005,45.07,148.262,45.217,148.415,45.247,148.6,45.318,148.791,45.324,148.837,45.363,148.803,45.414,148.825,45.456,148.826,45.486,148.812,45.51]},{"b":[150.47,59.007,150.728,59.16],"l":[150.666,59.16,150.592,59.097,150.47,59.054,150.472,59.035,150.511,59.007,150.59,59.019,150.728,59.095,150.713,59.122,150.666,59.16]},{"b":[137.233,54.653,138.206,55.197],"l":[137.941,55.093,137.91,55.11,137.577,55.197,137.544,55.163,137.436,55.016,137.384,55.001,137.275,54.891,137.233,54.791,137.276,54.792,137.463,54.873,137.526,54.826,137.661,54.653,137.721,54.663,137.79,54.697,137.87,54.75,137.959,54.789,137.991,54.821,138.017,54.901,138.096,54.991,138.206,55.034,138.172,55.06,138.031,55.053,137.941,55.093]},{"b":[136.715,54.924,137.179,55.108],"l":[137.078,55.092,136.996,55.093,136.795,55.009,136.715,54.956,136.765,54.946,136.903,54.961,136.969,54.924,137.055,54.927,137.179,55.1,137.156,55.108,137.078,55.092]},{"b":[146.609,43.716,146.899,43.86],"l":[146.622,43.813,146.613,43.797,146.609,43.74,146.683,43.716,146.714,43.744,146.899,43.804,146.884,43.829,146.825,43.86,146.622,43.813]},{"b":[145.426,43.665,146.568,44.5],"l":[146.356,44.425,146.208,44.498,146.112,44.5,145.94,44.273,145.89,44.249,145.852,44.193,145.773,44.129,145.748,44.072,145.666,43.999,145.462,43.871,145.426,43.81,145.439,43.737,145.556,43.665,145.587,43.845,145.767,43.941,145.887,44.048,145.914,44.104,146.112,44.246,146.173,44.269,146.296,44.281,146.437,44.376,146.516,44.375,146.568,44.44,146.356,44.425]},{"b":[124.085,24.335,124.324,24.587],"l":[124.17,24.452,124.12,24.47,124.085,24.436,124.136,24.348,124.186,24.335,124.234,24.358,124.293,24.516,124.324,24.566,124.302,24.587,124.211,24.459,124.17,24.452]},{"b":[123.68,24.266,123.935,24.414],"l":[123.752,24.348,123.68,24.318,123.681,24.288,123.75,24.283,123.826,24.266,123.889,24.28,123.928,24.324,123.935,24.362,123.771,24.414,123.754,24.391,123.752,24.348]},{"b":[125.269,24.717,125.444,24.872],"l":[125.315,24.852,125.284,24.872,125.269,24.733,125.359,24.717,125.444,24.743,125.402,24.777,125.335,24.805,125.315,24.852]},{"b":[127.65,26.095,128.332,26.882],"l":[128.255,26.882,128.217,26.797,128.122,26.711,128.098,26.668,128.047,26.643,128.03,26.647,127.994,26.679,127.907,26.694,127.895,26.675,127.891,26.631,127.946,26.594,127.926,26.556,127.82,26.466,127.796,26.449,127.729,26.434,127.727,26.308,127.655,26.199,127.65,26.154,127.653,26.095,127.729,26.097,127.804,26.153,127.806,26.171,127.786,26.209,127.79,26.255,127.849,26.319,127.905,26.328,127.869,26.381,127.867,26.442,127.951,26.456,128.038,26.534,128.127,26.552,128.163,26.607,128.259,26.653,128.311,26.721,128.332,26.812,128.255,26.882]},{"b":[128.883,27.702,129.016,27.91],"l":[128.952,27.91,128.908,27.898,128.883,27.842,128.9,27.728,128.956,27.702,128.998,27.721,129.016,27.77,128.99,27.811,128.952,27.91]},{"b":[129.165,28.128,129.715,28.517],"l":[129.598,28.476,129.577,28.461,129.561,28.431,129.51,28.398,129.465,28.395,129.322,28.36,129.251,28.314,129.248,28.283,129.217,28.263,129.165,28.25,129.275,28.201,129.366,28.128,129.453,28.209,129.439,28.255,129.457,28.272,129.513,28.299,129.575,28.361,129.642,28.411,129.71,28.432,129.715,28.47,129.69,28.517,129.598,28.476]},{"b":[129.192,28.082,129.331,28.192],"l":[129.257,28.176,129.192,28.192,129.232,28.101,129.331,28.082,129.324,28.105,129.277,28.145,129.257,28.176]},{"b":[130.388,30.241,130.673,30.466],"l":[130.497,30.466,130.388,30.388,130.446,30.265,130.508,30.241,130.623,30.263,130.673,30.367,130.644,30.389,130.497,30.466]},{"b":[130.87,30.386,131.083,30.828],"l":[130.947,30.671,130.94,30.575,130.87,30.444,130.872,30.386,130.96,30.397,130.993,30.53,131.057,30.642,131.083,30.791,131.06,30.828,131.04,30.819,131.012,30.792,130.947,30.671]},{"b":[128.649,32.586,128.895,32.784],"l":[128.839,32.763,128.806,32.776,128.761,32.772,128.704,32.757,128.665,32.784,128.649,32.662,128.657,32.628,128.693,32.605,128.75,32.586,128.79,32.637,128.821,32.646,128.895,32.652,128.879,32.693,128.839,32.763]},{"b":[129.96,32.194,130.2,32.541],"l":[130.01,32.522,130.021,32.469,129.979,32.346,130.015,32.314,130.017,32.292,129.96,32.244,129.993,32.228,130.004,32.194,130.083,32.23,130.2,32.341,130.197,32.492,130.168,32.541,130.01,32.522]},{"b":[130.242,32.419,130.461,32.527],"l":[130.242,32.463,130.256,32.431,130.293,32.419,130.381,32.424,130.419,32.458,130.461,32.516,130.365,32.527,130.242,32.463]},{"b":[133.19,36.167,133.381,36.34],"l":[133.206,36.293,133.19,36.233,133.239,36.179,133.325,36.167,133.371,36.204,133.381,36.246,133.296,36.34,133.206,36.293]},{"b":[138.225,37.82,138.575,38.316],"l":[138.51,38.259,138.504,38.316,138.461,38.291,138.306,38.161,138.25,38.078,138.246,37.995,138.322,37.991,138.322,37.97,138.288,37.896,138.283,37.854,138.225,37.829,138.249,37.82,138.344,37.822,138.497,37.904,138.575,38.066,138.454,38.076,138.463,38.124,138.51,38.259]},{"b":[139.412,42.076,139.558,42.235],"l":[139.431,42.2,139.412,42.16,139.435,42.084,139.458,42.076,139.481,42.081,139.505,42.096,139.558,42.235,139.496,42.227,139.431,42.2]},{"b":[140.972,45.269,141.073,45.465],"l":[140.972,45.465,140.982,45.364,141.034,45.269,141.073,45.333,141.07,45.4,141.057,45.45,141.002,45.465,140.972,45.465]},{"b":[141.135,45.112,141.329,45.248],"l":[141.252,45.232,141.194,45.248,141.135,45.206,141.145,45.154,141.226,45.112,141.295,45.119,141.329,45.15,141.31,45.179,141.252,45.232]},{"b":[139.769,33.045,139.874,33.129],"l":[139.776,33.078,139.824,33.045,139.841,33.056,139.874,33.094,139.809,33.129,139.777,33.125,139.769,33.107,139.776,33.078]},{"b":[139.367,34.68,139.456,34.776],"l":[139.37,34.775,139.367,34.721,139.392,34.69,139.446,34.68,139.456,34.727,139.426,34.776,139.37,34.775]},{"b":[134.668,34.203,135.005,34.545],"l":[134.834,34.473,134.757,34.368,134.668,34.294,134.683,34.247,134.731,34.209,134.824,34.203,134.933,34.288,134.905,34.398,135.005,34.544,134.961,34.545,134.904,34.519,134.834,34.473]},{"b":[128.997,32.829,129.182,33.133],"l":[129.124,33.068,129.11,33.133,129.035,32.969,128.997,32.952,129.02,32.92,129.052,32.829,129.077,32.84,129.112,32.929,129.154,32.946,129.182,32.993,129.153,33.003,129.124,33.068]},{"b":[129.37,33.176,129.57,33.361],"l":[129.462,33.331,129.423,33.257,129.417,33.231,129.37,33.176,129.421,33.176,129.492,33.223,129.508,33.284,129.57,33.361,129.538,33.358,129.462,33.331]},{"b":[129.675,33.707,129.796,33.858],"l":[129.717,33.858,129.7,33.829,129.675,33.74,129.727,33.707,129.796,33.749,129.776,33.829,129.717,33.858]},{"b":[129.186,34.083,129.337,34.321],"l":[129.215,34.321,129.186,34.145,129.214,34.083,129.279,34.123,129.335,34.231,129.337,34.285,129.215,34.321]},{"b":[129.267,34.306,129.48,34.687],"l":[129.451,34.687,129.326,34.607,129.322,34.579,129.329,34.522,129.267,34.37,129.297,34.34,129.365,34.306,129.386,34.354,129.381,34.416,129.475,34.54,129.469,34.616,129.48,34.649,129.472,34.671,129.451,34.687]},{"b":[132.461,34.075,132.578,34.173],"l":[132.578,34.115,132.56,34.127,132.543,34.173,132.524,34.164,132.496,34.122,132.461,34.087,132.549,34.075,132.578,34.115]},{"b":[132.201,33.847,132.445,33.948],"l":[132.36,33.847,132.411,33.88,132.445,33.913,132.43,33.924,132.315,33.909,132.266,33.945,132.208,33.948,132.201,33.928,132.209,33.873,132.267,33.871,132.36,33.847]},{"b":[118.287,24.414,118.451,24.522],"l":[118.339,24.469,118.287,24.477,118.295,24.436,118.433,24.414,118.451,24.456,118.407,24.522,118.339,24.469]},{"b":[118.077,24.436,118.183,24.552],"l":[118.077,24.501,118.089,24.489,118.091,24.446,118.15,24.436,118.183,24.496,118.171,24.519,118.104,24.552,118.093,24.541,118.077,24.501]},{"b":[128.489,34.737,128.741,35.014],"l":[128.722,35.014,128.668,35.009,128.586,34.932,128.489,34.865,128.52,34.82,128.647,34.737,128.741,34.799,128.722,35.014]},{"b":[127.832,34.703,128.066,34.921],"l":[127.832,34.875,127.838,34.813,127.873,34.735,127.897,34.735,127.942,34.766,127.984,34.703,128.055,34.708,128.066,34.806,128.038,34.879,127.966,34.893,127.915,34.921,127.832,34.875]},{"b":[127.737,34.584,127.799,34.682],"l":[127.737,34.631,127.788,34.584,127.799,34.615,127.787,34.682,127.737,34.631]},{"b":[126.166,33.202,126.931,33.553],"l":[126.76,33.553,126.696,33.549,126.338,33.46,126.199,33.368,126.166,33.312,126.179,33.283,126.229,33.225,126.24,33.215,126.282,33.202,126.327,33.224,126.582,33.238,126.709,33.272,126.873,33.341,126.905,33.382,126.931,33.444,126.901,33.515,126.76,33.553]},{"b":[126.109,34.355,126.38,34.563],"l":[126.344,34.545,126.247,34.563,126.227,34.533,126.123,34.444,126.109,34.399,126.134,34.39,126.17,34.355,126.234,34.371,126.335,34.426,126.38,34.498,126.344,34.545]},{"b":[126.369,37.605,126.521,37.823],"l":[126.521,37.737,126.494,37.783,126.412,37.823,126.369,37.772,126.407,37.649,126.423,37.624,126.461,37.61,126.516,37.605,126.521,37.737]},{"b":[121.211,31.464,121.863,31.805],"l":[121.577,31.637,121.542,31.674,121.492,31.694,121.464,31.756,121.339,31.797,121.211,31.805,121.227,31.758,121.336,31.644,121.52,31.55,121.78,31.464,121.863,31.492,121.844,31.526,121.808,31.552,121.577,31.637]},{"b":[121.969,29.944,122.322,30.143],"l":[122.284,30.068,122.111,30.14,121.969,30.143,121.978,30.064,122.024,30.013,122.158,30.001,122.282,29.944,122.296,29.963,122.322,30.031,122.284,30.068]},{"b":[122.043,29.66,122.173,29.782],"l":[122.12,29.782,122.062,29.773,122.043,29.736,122.084,29.725,122.169,29.66,122.173,29.679,122.165,29.701,122.12,29.782]},{"b":[122.332,29.846,122.404,29.955],"l":[122.402,29.95,122.351,29.955,122.332,29.935,122.368,29.853,122.394,29.846,122.404,29.892,122.402,29.95]},{"b":[119.696,25.411,119.839,25.653],"l":[119.797,25.623,119.778,25.653,119.723,25.639,119.696,25.591,119.723,25.551,119.699,25.495,119.7,25.433,119.747,25.411,119.821,25.457,119.832,25.48,119.809,25.508,119.839,25.56,119.838,25.591,119.829,25.607,119.797,25.623]},{"b":[110.265,20.968,110.54,21.093],"l":[110.522,21.083,110.422,21.059,110.385,21.093,110.31,21.075,110.265,21.025,110.281,21.001,110.34,20.998,110.422,21.007,110.504,20.968,110.539,21.018,110.54,21.039,110.522,21.083]},{"b":[107.404,21.092,107.603,21.269],"l":[107.476,21.269,107.453,21.235,107.404,21.094,107.459,21.092,107.603,21.217,107.563,21.22,107.476,21.269]},{"b":[107.399,20.901,107.551,21.034],"l":[107.479,20.952,107.399,20.903,107.466,20.901,107.521,20.927,107.551,20.981,107.551,21.034,107.519,21.013,107.479,20.952]},{"b":[106.911,20.743,107.064,20.867],"l":[107.044,20.837,106.953,20.867,106.911,20.824,106.99,20.743,107.031,20.747,107.042,20.761,107.064,20.8,107.064,20.817,107.044,20.837]},{"b":[106.769,20.796,106.866,20.928],"l":[106.795,20.928,106.769,20.864,106.803,20.844,106.854,20.796,106.866,20.816,106.855,20.858,106.795,20.928]},{"b":[106.568,8.681,106.659,8.766],"l":[106.568,8.701,106.589,8.681,106.617,8.683,106.653,8.701,106.65,8.723,106.659,8.766,106.568,8.701]},{"b":[103.85,10.029,104.083,10.428],"l":[104.028,10.428,103.986,10.427,103.898,10.369,103.85,10.371,103.868,10.335,103.952,10.243,104.018,10.029,104.048,10.061,104.037,10.111,104.076,10.225,104.083,10.341,104.064,10.391,104.028,10.428]},{"b":[102.993,11.275,103.045,11.39],"l":[102.993,11.29,103.011,11.276,103.027,11.275,103.045,11.285,103.037,11.39,103.008,11.383,102.995,11.348,102.993,11.29]},{"b":[102.533,11.572,102.606,11.692],"l":[102.533,11.615,102.59,11.572,102.606,11.677,102.569,11.692,102.546,11.668,102.533,11.615]},{"b":[102.273,11.965,102.43,12.152],"l":[102.319,12.142,102.277,12.152,102.273,12.119,102.302,11.981,102.36,11.974,102.378,11.983,102.43,11.965,102.427,11.989,102.408,12.025,102.378,12.073,102.319,12.142]},{"b":[103.65,1.265,103.996,1.447],"l":[103.818,1.447,103.705,1.423,103.65,1.326,103.82,1.265,103.97,1.331,103.996,1.365,103.961,1.392,103.909,1.416,103.818,1.447]},{"b":[104.129,2.721,104.223,2.872],"l":[104.185,2.872,104.17,2.857,104.129,2.767,104.147,2.728,104.173,2.721,104.222,2.732,104.223,2.774,104.185,2.872]},{"b":[100.191,5.267,100.339,5.468],"l":[100.246,5.468,100.204,5.447,100.191,5.283,100.264,5.267,100.289,5.295,100.339,5.41,100.31,5.438,100.246,5.468]},{"b":[99.646,6.263,99.919,6.466],"l":[99.848,6.466,99.822,6.445,99.749,6.41,99.711,6.427,99.646,6.418,99.657,6.367,99.705,6.338,99.744,6.263,99.783,6.272,99.823,6.313,99.866,6.297,99.883,6.311,99.919,6.359,99.848,6.466]},{"b":[99.607,6.516,99.701,6.714],"l":[99.654,6.714,99.607,6.597,99.644,6.516,99.663,6.522,99.701,6.571,99.654,6.714]},{"b":[99.983,9.68,100.074,9.794],"l":[99.983,9.794,99.998,9.748,100.026,9.712,100.064,9.68,100.074,9.697,100.073,9.749,100.043,9.792,99.983,9.794]},{"b":[99.931,9.422,100.075,9.586],"l":[99.954,9.581,99.94,9.56,99.931,9.476,99.962,9.422,100.054,9.461,100.075,9.529,100.071,9.586,100.043,9.577,99.954,9.581]},{"b":[98.529,7.917,98.604,8.109],"l":[98.58,7.917,98.592,7.934,98.604,8.057,98.529,8.109,98.58,7.917]},{"b":[98.262,7.776,98.435,8.166],"l":[98.399,7.965,98.435,8.086,98.351,8.111,98.322,8.166,98.301,8.136,98.262,7.926,98.296,7.776,98.316,7.782,98.357,7.829,98.398,7.828,98.409,7.902,98.399,7.965]},{"b":[98.251,9.041,98.313,9.139],"l":[98.251,9.041,98.308,9.051,98.313,9.08,98.301,9.139,98.274,9.13,98.258,9.095,98.251,9.041]},{"b":[93.656,6.749,93.93,7.237],"l":[93.859,7.207,93.822,7.237,93.684,7.184,93.656,7.136,93.658,7.016,93.709,7.001,93.829,6.749,93.89,6.831,93.93,6.973,93.859,7.207]},{"b":[93.597,7.262,93.734,7.411],"l":[93.655,7.38,93.614,7.358,93.597,7.319,93.638,7.262,93.734,7.356,93.692,7.411,93.655,7.38]},{"b":[93.064,8.212,93.171,8.349],"l":[93.097,8.349,93.078,8.328,93.064,8.275,93.115,8.219,93.171,8.212,93.141,8.25,93.097,8.349]},{"b":[93.309,7.877,93.447,8.018],"l":[93.375,8.018,93.334,8.007,93.309,7.964,93.342,7.919,93.365,7.877,93.443,7.878,93.447,7.899,93.434,7.948,93.375,8.018]},{"b":[93.456,8.019,93.537,8.225],"l":[93.512,8.16,93.532,8.214,93.494,8.225,93.456,8.172,93.461,8.109,93.47,8.073,93.472,8.053,93.478,8.024,93.49,8.019,93.537,8.057,93.512,8.16]},{"b":[92.713,9.131,92.809,9.244],"l":[92.713,9.205,92.717,9.165,92.744,9.131,92.788,9.137,92.809,9.173,92.786,9.241,92.762,9.244,92.739,9.231,92.713,9.205]},{"b":[92.353,10.521,92.574,10.897],"l":[92.51,10.897,92.448,10.866,92.371,10.794,92.353,10.751,92.377,10.651,92.37,10.547,92.473,10.521,92.503,10.555,92.574,10.704,92.554,10.8,92.51,10.897]},{"b":[92.596,11.361,92.693,11.509],"l":[92.69,11.463,92.64,11.509,92.634,11.427,92.596,11.386,92.645,11.361,92.693,11.381,92.687,11.411,92.69,11.463]},{"b":[92.955,11.899,93.062,12.037],"l":[93.017,12.037,92.996,12.032,92.955,12.002,92.982,11.959,93.062,11.899,93.017,12.037]},{"b":[92.68,12.8,92.731,12.962],"l":[92.68,12.939,92.686,12.8,92.718,12.865,92.731,12.949,92.711,12.962,92.694,12.957,92.68,12.939]},{"b":[92.534,11.513,93.077,13.545],"l":[93.016,13.336,93.077,13.401,93.067,13.436,93.062,13.545,93.029,13.544,92.925,13.486,92.857,13.358,92.86,13.231,92.809,13.04,92.831,13.003,92.807,12.879,92.753,12.821,92.74,12.78,92.759,12.669,92.732,12.616,92.721,12.541,92.719,12.357,92.734,12.336,92.778,12.303,92.788,12.226,92.769,12.216,92.695,12.215,92.676,12.192,92.641,12.112,92.632,12.014,92.608,11.95,92.567,11.931,92.534,11.873,92.56,11.833,92.576,11.718,92.668,11.539,92.701,11.513,92.723,11.536,92.765,11.639,92.767,11.765,92.798,11.875,92.797,11.918,92.764,11.94,92.748,11.993,92.786,12.035,92.799,12.079,92.867,12.181,92.879,12.228,92.864,12.436,92.933,12.453,92.99,12.539,92.965,12.85,92.886,12.942,92.91,12.975,92.951,13.063,93.005,13.089,93.043,13.155,93.066,13.222,93.074,13.252,93.016,13.336]},{"b":[98.477,10.959,98.542,11.087],"l":[98.527,11.087,98.477,10.98,98.498,10.964,98.519,10.959,98.542,10.962,98.527,11.087]},{"b":[98.08,10.74,98.293,10.98],"l":[98.21,10.953,98.167,10.98,98.143,10.963,98.08,10.887,98.155,10.898,98.218,10.838,98.252,10.744,98.271,10.74,98.284,10.753,98.293,10.78,98.21,10.953]},{"b":[98.118,9.875,98.292,10.051],"l":[98.221,10.045,98.14,9.975,98.118,9.878,98.134,9.875,98.183,9.933,98.231,9.954,98.283,10.008,98.292,10.051,98.221,10.045]},{"b":[98.187,11.456,98.308,11.783],"l":[98.239,11.645,98.201,11.567,98.187,11.472,98.209,11.457,98.216,11.456,98.222,11.478,98.263,11.524,98.284,11.594,98.308,11.723,98.3,11.783,98.278,11.758,98.239,11.645]},{"b":[98.454,11.899,98.61,12.084],"l":[98.525,12.005,98.466,12.084,98.454,12.061,98.474,11.899,98.516,11.905,98.576,11.925,98.61,11.957,98.525,12.005]},{"b":[98.01,11.637,98.084,11.86],"l":[98.01,11.86,98.021,11.696,98.084,11.637,98.075,11.692,98.081,11.733,98.06,11.757,98.01,11.86]},{"b":[98.376,11.539,98.554,11.805],"l":[98.376,11.792,98.4,11.715,98.397,11.684,98.435,11.567,98.465,11.567,98.528,11.539,98.554,11.745,98.524,11.805,98.376,11.792]},{"b":[98.037,12.145,98.137,12.292],"l":[98.137,12.15,98.12,12.191,98.118,12.223,98.128,12.261,98.122,12.279,98.105,12.288,98.071,12.292,98.057,12.28,98.037,12.232,98.075,12.164,98.108,12.148,98.125,12.145,98.137,12.15]},{"b":[97.939,12.279,98.066,12.398],"l":[98.045,12.387,97.99,12.394,97.939,12.346,97.952,12.322,98.002,12.279,98.06,12.354,98.066,12.39,98.06,12.398,98.045,12.387]},{"b":[98.303,12.336,98.468,12.678],"l":[98.396,12.647,98.312,12.678,98.303,12.612,98.331,12.511,98.314,12.336,98.334,12.336,98.381,12.354,98.459,12.474,98.468,12.571,98.436,12.571,98.414,12.598,98.396,12.647]},{"b":[98.251,12.935,98.315,13.202],"l":[98.265,13.202,98.255,13.189,98.251,13.104,98.259,13.014,98.309,12.935,98.315,13.099,98.299,13.152,98.269,13.189,98.265,13.202]},{"b":[97.469,16.24,97.6,16.505],"l":[97.579,16.486,97.542,16.505,97.516,16.497,97.469,16.461,97.48,16.306,97.537,16.24,97.575,16.253,97.589,16.397,97.6,16.43,97.593,16.461,97.579,16.486]},{"b":[99.038,7.471,99.104,7.637],"l":[99.038,7.548,99.068,7.496,99.104,7.471,99.078,7.592,99.045,7.637,99.038,7.626,99.038,7.548]},{"b":[94.733,15.794,94.838,15.933],"l":[94.733,15.823,94.743,15.812,94.784,15.794,94.805,15.819,94.838,15.892,94.828,15.933,94.733,15.823]},{"b":[94.388,15.848,94.619,16.206],"l":[94.494,16.075,94.388,15.994,94.412,15.848,94.477,15.946,94.566,16.019,94.619,16.141,94.601,16.206,94.546,16.153,94.494,16.075]},{"b":[93.488,18.676,93.746,18.889],"l":[93.745,18.866,93.618,18.889,93.488,18.868,93.57,18.76,93.674,18.676,93.691,18.684,93.718,18.716,93.746,18.808,93.745,18.866]},{"b":[93.413,19.755,93.513,19.95],"l":[93.492,19.893,93.413,19.95,93.42,19.878,93.445,19.806,93.513,19.755,93.492,19.893]},{"b":[92.913,19.829,93.023,20.086],"l":[92.915,20.086,92.913,20,92.975,19.868,93.023,19.829,93.01,19.924,92.96,20.046,92.915,20.086]},{"b":[93.644,19.299,93.947,19.558],"l":[93.875,19.481,93.829,19.475,93.715,19.558,93.688,19.544,93.644,19.495,93.662,19.459,93.732,19.416,93.756,19.326,93.815,19.299,93.902,19.332,93.934,19.365,93.947,19.408,93.946,19.429,93.875,19.481]},{"b":[91.82,21.75,91.883,21.927],"l":[91.851,21.927,91.835,21.885,91.82,21.81,91.838,21.75,91.874,21.832,91.883,21.884,91.861,21.927,91.851,21.927]},{"b":[91.857,21.503,91.962,21.723],"l":[91.908,21.723,91.857,21.709,91.873,21.574,91.859,21.533,91.889,21.503,91.949,21.508,91.962,21.61,91.949,21.683,91.934,21.722,91.908,21.723]},{"b":[91.045,22.105,91.178,22.52],"l":[91.079,22.52,91.045,22.105,91.151,22.175,91.178,22.283,91.158,22.365,91.079,22.52]},{"b":[91.411,22.353,91.557,22.617],"l":[91.411,22.476,91.467,22.378,91.51,22.353,91.557,22.382,91.548,22.425,91.523,22.491,91.484,22.577,91.456,22.617,91.439,22.599,91.411,22.476]},{"b":[90.536,22.92,90.66,23.035],"l":[90.58,23.035,90.536,23.015,90.562,22.975,90.604,22.946,90.66,22.92,90.642,22.963,90.58,23.035]},{"b":[90.503,22.054,90.868,22.864],"l":[90.683,22.785,90.672,22.813,90.596,22.864,90.503,22.835,90.523,22.748,90.56,22.673,90.565,22.618,90.649,22.541,90.675,22.445,90.68,22.327,90.515,22.065,90.604,22.054,90.778,22.089,90.83,22.16,90.866,22.391,90.868,22.485,90.737,22.639,90.699,22.714,90.683,22.785]},{"b":[79.846,9.62,79.97,9.744],"l":[79.97,9.631,79.912,9.679,79.888,9.741,79.872,9.744,79.859,9.734,79.846,9.715,79.857,9.686,79.907,9.62,79.97,9.631]},{"b":[79.748,8.975,79.904,9.105],"l":[79.904,8.975,79.875,9.051,79.86,9.066,79.748,9.105,79.767,9.07,79.821,9.027,79.904,8.975]},{"b":[-9.099,70.833,-7.979,71.178],"l":[-8.344,71.14,-8.521,71.031,-8.965,70.916,-9.099,70.855,-9.046,70.833,-8.954,70.839,-8.635,70.94,-8.302,70.981,-8.002,71.041,-7.979,71.117,-8.001,71.178,-8.344,71.14]},{"b":[-156.049,18.964,-154.804,20.276],"l":[-154.953,19.645,-154.989,19.732,-155.066,19.748,-155.086,19.876,-155.199,19.994,-155.622,20.163,-155.832,20.276,-155.874,20.26,-155.893,20.167,-155.82,20.014,-155.909,19.895,-155.988,19.832,-156.049,19.75,-155.966,19.591,-155.891,19.383,-155.906,19.126,-155.881,19.071,-155.681,18.968,-155.626,18.964,-155.581,19.012,-155.535,19.109,-155.31,19.26,-155.053,19.319,-154.85,19.454,-154.804,19.524,-154.841,19.568,-154.953,19.645]},{"b":[-157.29,21.056,-156.712,21.23],"l":[-156.917,21.177,-156.952,21.2,-157.002,21.188,-157.214,21.215,-157.25,21.23,-157.254,21.181,-157.279,21.152,-157.29,21.113,-157.021,21.098,-156.86,21.056,-156.748,21.104,-156.712,21.155,-156.742,21.164,-156.917,21.177]},{"b":[-156.698,20.599,-155.99,21.034],"l":[-156.104,20.84,-156.148,20.885,-156.278,20.951,-156.354,20.941,-156.461,20.915,-156.487,20.933,-156.532,20.993,-156.585,21.034,-156.657,21.025,-156.698,20.949,-156.69,20.901,-156.615,20.822,-156.544,20.79,-156.48,20.801,-156.449,20.706,-156.438,20.618,-156.409,20.605,-156.31,20.599,-156.235,20.629,-156.107,20.645,-156.014,20.715,-155.99,20.757,-156.019,20.792,-156.104,20.84]},{"b":[-160.243,21.797,-160.049,22.015],"l":[-160.221,21.897,-160.243,21.843,-160.235,21.804,-160.2,21.797,-160.18,21.841,-160.153,21.879,-160.08,21.907,-160.077,21.958,-160.049,22.005,-160.101,22.015,-160.164,21.944,-160.221,21.897]},{"b":[-158.273,21.269,-157.635,21.701],"l":[-157.83,21.471,-157.854,21.512,-157.852,21.553,-157.962,21.701,-158.02,21.692,-158.123,21.6,-158.273,21.585,-158.239,21.533,-158.239,21.489,-158.138,21.377,-158.11,21.319,-158.079,21.312,-157.981,21.316,-158.017,21.368,-157.978,21.379,-157.968,21.367,-157.958,21.327,-157.902,21.341,-157.849,21.291,-157.799,21.269,-157.691,21.28,-157.635,21.308,-157.654,21.334,-157.706,21.378,-157.721,21.458,-157.765,21.451,-157.799,21.457,-157.83,21.471]},{"b":[-159.789,21.876,-159.301,22.223],"l":[-159.579,22.223,-159.727,22.14,-159.789,22.042,-159.748,21.99,-159.646,21.952,-159.609,21.91,-159.512,21.9,-159.461,21.876,-159.373,21.932,-159.344,21.974,-159.33,22.051,-159.301,22.105,-159.305,22.154,-159.352,22.22,-159.579,22.223]},{"b":[-157.051,20.744,-156.809,20.93],"l":[-156.942,20.93,-157.051,20.912,-156.988,20.826,-156.973,20.758,-156.909,20.744,-156.85,20.773,-156.809,20.831,-156.848,20.878,-156.881,20.905,-156.942,20.93]},{"b":[178.648,51.372,179.452,51.656],"l":[179.182,51.47,179.084,51.528,178.908,51.616,178.692,51.656,178.648,51.644,178.747,51.587,178.926,51.535,179.278,51.372,179.452,51.373,179.416,51.401,179.294,51.421,179.182,51.47]},{"b":[179.498,51.88,179.78,52.03],"l":[179.627,52.03,179.504,51.98,179.498,51.933,179.55,51.894,179.645,51.88,179.728,51.905,179.78,51.967,179.627,52.03]},{"b":[177.25,51.841,177.67,52.114],"l":[177.564,52.11,177.521,52.018,177.478,51.992,177.381,51.976,177.25,51.903,177.261,51.884,177.329,51.841,177.415,51.883,177.594,51.948,177.596,51.994,177.653,52.06,177.67,52.103,177.637,52.114,177.564,52.11]},{"b":[173.402,52.357,173.776,52.504],"l":[173.658,52.504,173.517,52.451,173.425,52.438,173.402,52.405,173.616,52.391,173.658,52.357,173.723,52.36,173.745,52.447,173.776,52.495,173.658,52.504]},{"b":[172.495,52.752,173.436,53.013],"l":[173.436,52.852,173.252,52.943,173.102,52.996,172.984,52.98,172.812,53.013,172.678,53.008,172.495,52.938,172.595,52.907,172.722,52.886,172.776,52.797,172.935,52.752,173.08,52.814,173.159,52.811,173.303,52.826,173.348,52.825,173.395,52.835,173.436,52.852]},{"b":[-173.074,60.299,-172.232,60.607],"l":[-172.387,60.398,-172.526,60.392,-172.742,60.457,-172.86,60.506,-172.924,60.607,-173.048,60.568,-173.074,60.493,-172.958,60.463,-172.636,60.329,-172.397,60.331,-172.232,60.299,-172.278,60.344,-172.387,60.398]},{"b":[-170.387,57.137,-170.116,57.242],"l":[-170.387,57.203,-170.386,57.189,-170.358,57.154,-170.264,57.137,-170.161,57.184,-170.116,57.242,-170.387,57.203]},{"b":[-160.517,55.259,-160.329,55.357],"l":[-160.493,55.352,-160.517,55.334,-160.481,55.309,-160.343,55.259,-160.329,55.338,-160.362,55.357,-160.493,55.352]},{"b":[-160.847,55.145,-160.483,55.405],"l":[-160.583,55.308,-160.552,55.363,-160.553,55.381,-160.574,55.378,-160.639,55.322,-160.67,55.314,-160.685,55.315,-160.666,55.359,-160.672,55.379,-160.696,55.398,-160.724,55.405,-160.789,55.383,-160.84,55.335,-160.847,55.311,-160.825,55.174,-160.795,55.145,-160.751,55.171,-160.702,55.178,-160.609,55.159,-160.488,55.185,-160.483,55.197,-160.531,55.233,-160.583,55.308]},{"b":[-162.434,54.842,-162.234,54.983],"l":[-162.434,54.932,-162.416,54.896,-162.391,54.873,-162.322,54.842,-162.298,54.847,-162.273,54.867,-162.234,54.932,-162.238,54.955,-162.265,54.983,-162.294,54.983,-162.434,54.932]},{"b":[-162.821,54.38,-162.554,54.495],"l":[-162.821,54.495,-162.812,54.444,-162.733,54.402,-162.641,54.38,-162.554,54.401,-162.608,54.447,-162.645,54.462,-162.821,54.495]},{"b":[-159.462,54.967,-159.362,55.059],"l":[-159.462,55.059,-159.458,55.035,-159.421,54.978,-159.394,54.967,-159.362,54.972,-159.363,55,-159.39,55.041,-159.462,55.059]},{"b":[-160.227,54.923,-159.839,55.273],"l":[-159.898,55.221,-159.871,55.264,-159.887,55.273,-159.92,55.268,-159.982,55.198,-160.039,55.193,-160.102,55.134,-160.134,55.12,-160.172,55.123,-160.152,55.057,-160.154,55.038,-160.164,55.01,-160.227,54.923,-160.17,54.942,-160.038,55.044,-159.999,55.067,-159.953,55.079,-159.934,55.107,-159.873,55.129,-159.854,55.145,-159.839,55.182,-159.898,55.221]},{"b":[-159.648,55.057,-159.515,55.226],"l":[-159.515,55.152,-159.545,55.226,-159.575,55.218,-159.595,55.182,-159.588,55.165,-159.598,55.126,-159.64,55.124,-159.635,55.102,-159.648,55.075,-159.618,55.057,-159.561,55.081,-159.535,55.06,-159.52,55.072,-159.515,55.152]},{"b":[-169.992,52.792,-169.692,52.884],"l":[-169.983,52.851,-169.992,52.83,-169.981,52.806,-169.877,52.814,-169.723,52.792,-169.708,52.807,-169.692,52.847,-169.711,52.867,-169.755,52.884,-169.821,52.883,-169.983,52.851]},{"b":[-170.827,52.55,-170.585,52.698],"l":[-170.682,52.698,-170.791,52.631,-170.827,52.601,-170.816,52.562,-170.797,52.55,-170.733,52.581,-170.692,52.593,-170.649,52.593,-170.614,52.61,-170.587,52.642,-170.585,52.668,-170.608,52.685,-170.682,52.698]},{"b":[-166.106,54.047,-165.693,54.211],"l":[-165.764,54.152,-165.893,54.207,-165.966,54.211,-166.041,54.191,-166.088,54.169,-166.106,54.145,-166.103,54.114,-166.057,54.054,-166.036,54.047,-165.933,54.059,-165.91,54.049,-165.879,54.053,-165.842,54.071,-165.738,54.081,-165.693,54.1,-165.704,54.12,-165.764,54.152]},{"b":[-165.654,54.129,-165.408,54.285],"l":[-165.442,54.208,-165.488,54.222,-165.534,54.274,-165.551,54.285,-165.59,54.279,-165.654,54.253,-165.621,54.184,-165.615,54.14,-165.605,54.129,-165.561,54.137,-165.468,54.181,-165.408,54.197,-165.442,54.208]},{"b":[-169.089,52.832,-167.805,53.558],"l":[-167.805,53.485,-167.828,53.508,-167.986,53.558,-168.073,53.557,-168.193,53.533,-168.288,53.5,-168.357,53.458,-168.396,53.409,-168.405,53.354,-168.397,53.322,-168.363,53.304,-168.38,53.283,-168.437,53.257,-168.572,53.266,-168.639,53.256,-168.69,53.227,-168.76,53.175,-168.778,53.149,-168.783,53.079,-168.796,53.045,-168.836,53.02,-168.909,52.951,-168.974,52.91,-169.073,52.864,-169.089,52.832,-169.066,52.834,-168.741,52.957,-168.699,52.963,-168.597,53.016,-168.549,53.036,-168.506,53.043,-168.446,53.084,-168.37,53.16,-168.271,53.238,-167.964,53.345,-167.865,53.387,-167.843,53.435,-167.805,53.485]},{"b":[-167.809,53.259,-166.231,54.006],"l":[-166.522,53.61,-166.444,53.652,-166.355,53.674,-166.309,53.698,-166.339,53.718,-166.385,53.721,-166.549,53.701,-166.546,53.726,-166.489,53.785,-166.319,53.874,-166.231,53.933,-166.336,53.971,-166.372,53.999,-166.4,53.978,-166.443,53.925,-166.497,53.884,-166.572,53.853,-166.615,53.901,-166.627,53.996,-166.673,54.006,-166.734,54.002,-166.849,53.978,-166.978,53.963,-167.038,53.942,-167.09,53.906,-167.118,53.873,-167.121,53.843,-167.106,53.813,-167.071,53.783,-167.027,53.769,-166.973,53.771,-166.89,53.759,-166.777,53.733,-166.741,53.713,-166.804,53.685,-166.809,53.646,-166.819,53.641,-166.838,53.648,-166.894,53.697,-167.016,53.698,-167.042,53.655,-167.092,53.636,-167.136,53.526,-167.204,53.495,-167.424,53.437,-167.53,53.394,-167.639,53.387,-167.71,53.371,-167.809,53.324,-167.781,53.3,-167.669,53.26,-167.629,53.259,-167.592,53.273,-167.522,53.276,-167.48,53.292,-167.429,53.326,-167.381,53.342,-167.337,53.341,-167.3,53.35,-167.271,53.371,-167.154,53.408,-166.961,53.447,-166.851,53.453,-166.77,53.476,-166.702,53.537,-166.522,53.61]},{"b":[-166.251,53.72,-166.103,53.843],"l":[-166.234,53.784,-166.251,53.768,-166.249,53.745,-166.224,53.72,-166.21,53.723,-166.184,53.757,-166.139,53.787,-166.103,53.833,-166.114,53.843,-166.155,53.836,-166.188,53.822,-166.234,53.784]},{"b":[-172.62,52.257,-172.314,52.388],"l":[-172.47,52.388,-172.544,52.354,-172.582,52.326,-172.62,52.273,-172.539,52.257,-172.465,52.272,-172.314,52.33,-172.383,52.373,-172.47,52.388]},{"b":[-173.992,52.042,-173.023,52.144],"l":[-173.357,52.096,-173.553,52.136,-173.657,52.144,-173.779,52.118,-173.794,52.104,-173.939,52.131,-173.992,52.123,-173.99,52.104,-173.93,52.072,-173.879,52.054,-173.836,52.048,-173.673,52.063,-173.461,52.042,-173.368,52.046,-173.232,52.068,-173.179,52.063,-173.023,52.079,-173.024,52.091,-173.113,52.1,-173.357,52.096]},{"b":[-175.296,51.994,-174.018,52.42],"l":[-174.169,52.42,-174.307,52.378,-174.365,52.342,-174.436,52.317,-174.406,52.296,-174.259,52.269,-174.306,52.216,-174.474,52.184,-174.668,52.135,-174.916,52.094,-175.118,52.047,-175.214,52.038,-175.296,52.022,-175.214,51.994,-174.677,52.035,-174.344,52.078,-174.121,52.135,-174.179,52.2,-174.163,52.223,-174.055,52.246,-174.03,52.29,-174.018,52.332,-174.046,52.367,-174.169,52.42]},{"b":[-176.204,51.79,-176.009,51.886],"l":[-176.194,51.886,-176.204,51.835,-176.093,51.79,-176.009,51.812,-176.072,51.843,-176.194,51.886]},{"b":[-176.414,51.733,-176.28,51.861],"l":[-176.287,51.792,-176.28,51.803,-176.379,51.861,-176.414,51.841,-176.396,51.76,-176.35,51.733,-176.287,51.792]},{"b":[-176.962,51.604,-176.437,51.986],"l":[-176.588,51.833,-176.593,51.867,-176.552,51.92,-176.55,51.944,-176.597,51.982,-176.698,51.986,-176.745,51.895,-176.736,51.84,-176.774,51.819,-176.874,51.79,-176.962,51.604,-176.837,51.676,-176.771,51.63,-176.558,51.712,-176.511,51.746,-176.47,51.731,-176.452,51.736,-176.437,51.754,-176.437,51.82,-176.473,51.837,-176.588,51.833]},{"b":[-176.185,51.973,-175.975,52.1],"l":[-176.045,51.973,-176.022,52.002,-175.975,52.029,-175.988,52.049,-176.031,52.082,-176.077,52.1,-176.156,52.099,-176.185,52.056,-176.178,52.03,-176.143,52.004,-176.045,51.973]},{"b":[-177.67,51.677,-177.063,51.93],"l":[-177.21,51.841,-177.257,51.805,-177.335,51.776,-177.668,51.721,-177.67,51.701,-177.655,51.677,-177.578,51.694,-177.475,51.701,-177.382,51.705,-177.23,51.694,-177.177,51.704,-177.148,51.717,-177.135,51.807,-177.121,51.836,-177.08,51.867,-177.063,51.902,-177.11,51.929,-177.131,51.93,-177.166,51.909,-177.21,51.841]},{"b":[-178.195,51.616,-177.644,51.918],"l":[-177.644,51.826,-177.8,51.84,-177.866,51.86,-177.954,51.918,-178.117,51.916,-178.168,51.903,-178.195,51.882,-178.153,51.848,-178.045,51.801,-177.986,51.764,-177.977,51.738,-178,51.717,-178.078,51.691,-178.059,51.673,-177.925,51.617,-177.901,51.616,-177.879,51.65,-177.827,51.686,-177.771,51.778,-177.725,51.802,-177.644,51.826]},{"b":[73.251,-53.185,73.838,-52.966],"l":[73.586,-53.027,73.388,-53,73.305,-52.966,73.251,-52.976,73.254,-52.989,73.285,-53.021,73.336,-53.03,73.413,-53.147,73.465,-53.184,73.588,-53.185,73.707,-53.137,73.795,-53.13,73.838,-53.113,73.731,-53.091,73.586,-53.027]},{"b":[69.15,-49.067,69.395,-48.879],"l":[69.221,-49.067,69.282,-49.059,69.321,-49.034,69.395,-48.951,69.369,-48.89,69.266,-48.879,69.167,-48.883,69.15,-48.919,69.17,-48.957,69.204,-48.991,69.202,-49.034,69.221,-49.067]},{"b":[51.659,-46.449,51.835,-46.327],"l":[51.742,-46.327,51.659,-46.374,51.697,-46.428,51.762,-46.449,51.835,-46.44,51.815,-46.395,51.784,-46.359,51.742,-46.327]},{"b":[37.59,-46.963,37.888,-46.824],"l":[37.59,-46.908,37.612,-46.946,37.814,-46.963,37.857,-46.944,37.888,-46.902,37.873,-46.885,37.79,-46.837,37.685,-46.824,37.65,-46.849,37.59,-46.908]},{"b":[27.716,35.908,28.232,36.434],"l":[27.914,36.345,27.815,36.277,27.774,36.214,27.716,36.172,27.719,36.141,27.757,36.069,27.716,35.957,27.746,35.911,27.771,35.908,27.843,35.929,27.966,36.048,28.088,36.065,28.068,36.13,28.144,36.21,28.23,36.37,28.232,36.434,28.171,36.426,27.914,36.345]},{"b":[27.071,35.409,27.234,35.82],"l":[27.157,35.629,27.207,35.714,27.223,35.82,27.158,35.789,27.156,35.726,27.071,35.598,27.116,35.511,27.099,35.456,27.138,35.409,27.176,35.465,27.234,35.479,27.209,35.559,27.157,35.629]},{"b":[27.786,36.538,27.87,36.641],"l":[27.863,36.641,27.837,36.635,27.786,36.608,27.788,36.584,27.838,36.538,27.86,36.554,27.869,36.583,27.87,36.623,27.863,36.641]},{"b":[26.889,36.945,27.04,37.087],"l":[27.04,37.002,27.016,37.01,26.967,37.052,26.889,37.087,26.938,37.025,26.92,36.945,27.02,36.959,27.035,36.976,27.04,37.002]},{"b":[26.918,36.726,27.352,36.905],"l":[27.061,36.84,26.956,36.774,26.918,36.726,26.95,36.727,27.034,36.771,27.151,36.778,27.193,36.809,27.352,36.869,27.266,36.905,27.215,36.899,27.061,36.84]},{"b":[25.362,36.93,25.588,37.196],"l":[25.362,37.07,25.396,36.984,25.457,36.93,25.546,36.968,25.584,37.039,25.588,37.153,25.564,37.185,25.525,37.196,25.362,37.07]},{"b":[25.105,36.991,25.279,37.149],"l":[25.275,37.138,25.235,37.149,25.146,37.107,25.105,37.035,25.133,37,25.199,36.991,25.279,37.068,25.271,37.084,25.275,37.138]},{"b":[25.371,36.34,25.482,36.474],"l":[25.415,36.442,25.413,36.405,25.397,36.379,25.371,36.359,25.436,36.34,25.482,36.393,25.467,36.435,25.409,36.474,25.397,36.465,25.415,36.442]},{"b":[25.26,36.658,25.407,36.789],"l":[25.364,36.658,25.382,36.674,25.407,36.717,25.296,36.789,25.26,36.758,25.289,36.722,25.364,36.658]},{"b":[26.581,37.645,27.055,37.811],"l":[26.982,37.782,26.947,37.778,26.824,37.811,26.743,37.81,26.639,37.781,26.581,37.724,26.613,37.71,26.721,37.705,26.788,37.657,26.845,37.645,26.978,37.7,27.055,37.709,27.04,37.77,26.982,37.782]},{"b":[25.982,37.526,26.351,37.674],"l":[26.212,37.638,26.086,37.635,25.997,37.566,25.982,37.526,26.029,37.529,26.205,37.569,26.297,37.62,26.351,37.674,26.326,37.673,26.212,37.638]},{"b":[25.743,36.782,26.064,36.937],"l":[25.942,36.887,25.852,36.848,25.834,36.825,25.797,36.807,25.743,36.79,25.771,36.782,25.859,36.79,25.985,36.88,26.064,36.903,26.001,36.937,25.942,36.887]},{"b":[26.27,36.511,26.461,36.639],"l":[26.337,36.581,26.27,36.595,26.27,36.547,26.331,36.511,26.382,36.562,26.461,36.585,26.421,36.624,26.37,36.639,26.384,36.608,26.337,36.581]},{"b":[24.277,37.528,24.401,37.683],"l":[24.277,37.601,24.289,37.528,24.356,37.577,24.401,37.649,24.379,37.683,24.32,37.678,24.277,37.601]},{"b":[24.37,37.314,24.481,37.475],"l":[24.398,37.383,24.379,37.314,24.436,37.344,24.481,37.408,24.449,37.45,24.431,37.475,24.395,37.45,24.37,37.42,24.398,37.383]},{"b":[24.326,36.656,24.538,36.764],"l":[24.425,36.713,24.357,36.744,24.345,36.723,24.326,36.656,24.531,36.684,24.538,36.705,24.536,36.764,24.46,36.747,24.45,36.729,24.425,36.713]},{"b":[24.895,37.39,24.943,37.509],"l":[24.943,37.494,24.907,37.509,24.895,37.446,24.896,37.407,24.912,37.391,24.938,37.39,24.943,37.494]},{"b":[24.7,37.692,24.992,37.99],"l":[24.948,37.858,24.956,37.905,24.855,37.914,24.79,37.99,24.763,37.988,24.7,37.962,24.714,37.899,24.767,37.871,24.799,37.824,24.884,37.771,24.962,37.692,24.992,37.76,24.98,37.797,24.948,37.858]},{"b":[24.996,37.535,25.256,37.681],"l":[25.156,37.545,25.219,37.535,25.256,37.6,25.225,37.631,25.092,37.648,25.039,37.681,24.996,37.677,25.016,37.646,25.052,37.614,25.156,37.545]},{"b":[24.671,36.917,24.763,37.024],"l":[24.676,36.959,24.703,36.917,24.721,36.921,24.763,36.949,24.716,37.024,24.681,37.022,24.671,36.999,24.676,36.959]},{"b":[25.846,38.162,26.16,38.602],"l":[26.013,38.602,25.846,38.574,25.851,38.508,25.96,38.416,25.991,38.354,25.953,38.303,25.874,38.27,25.892,38.243,25.999,38.162,26.094,38.218,26.103,38.234,26.111,38.28,26.157,38.303,26.15,38.468,26.141,38.486,26.16,38.541,26.11,38.545,26.013,38.602]},{"b":[25.844,38.973,26.596,39.383],"l":[26.531,39.172,26.393,39.27,26.41,39.329,26.348,39.383,26.165,39.374,26.165,39.332,26.088,39.304,26.026,39.285,25.91,39.288,25.844,39.2,25.855,39.179,25.906,39.139,26.072,39.096,26.176,39.194,26.273,39.198,26.245,39.164,26.108,39.081,26.161,39.026,26.39,38.974,26.469,38.973,26.547,38.994,26.503,39.031,26.489,39.075,26.532,39.064,26.584,39.031,26.596,39.049,26.578,39.11,26.531,39.172]},{"b":[23.594,39.081,23.78,39.209],"l":[23.735,39.081,23.78,39.114,23.594,39.209,23.666,39.095,23.735,39.081]},{"b":[23.841,39.147,23.971,39.268],"l":[23.971,39.268,23.888,39.226,23.841,39.147,23.888,39.158,23.94,39.201,23.971,39.268]},{"b":[24.461,38.785,24.675,38.98],"l":[24.581,38.879,24.564,38.942,24.486,38.98,24.473,38.962,24.461,38.889,24.566,38.832,24.565,38.819,24.541,38.789,24.569,38.785,24.675,38.809,24.581,38.879]},{"b":[25.448,40.4,25.686,40.516],"l":[25.569,40.516,25.448,40.483,25.573,40.4,25.686,40.427,25.664,40.464,25.624,40.492,25.569,40.516]},{"b":[25.052,39.806,25.449,40.035],"l":[25.374,40.016,25.348,39.985,25.286,39.956,25.234,40.005,25.058,40,25.052,39.976,25.065,39.91,25.062,39.852,25.126,39.826,25.185,39.83,25.203,39.849,25.224,39.893,25.249,39.894,25.252,39.854,25.263,39.823,25.299,39.806,25.357,39.808,25.372,39.891,25.4,39.95,25.438,39.983,25.449,40.035,25.374,40.016]},{"b":[24.516,40.579,24.786,40.793],"l":[24.623,40.793,24.586,40.769,24.517,40.687,24.516,40.647,24.646,40.579,24.774,40.615,24.769,40.659,24.786,40.704,24.774,40.73,24.719,40.786,24.623,40.793]},{"b":[25.669,40.105,25.977,40.238],"l":[25.741,40.196,25.669,40.136,25.74,40.105,25.97,40.136,25.977,40.178,25.918,40.238,25.875,40.234,25.741,40.196]},{"b":[-16.236,11.045,-16.053,11.199],"l":[-16.067,11.197,-16.087,11.199,-16.105,11.191,-16.144,11.167,-16.176,11.131,-16.195,11.13,-16.236,11.113,-16.231,11.094,-16.195,11.045,-16.115,11.059,-16.072,11.084,-16.053,11.118,-16.067,11.197]},{"b":[-16.477,19.609,-16.344,19.866],"l":[-16.42,19.802,-16.477,19.71,-16.466,19.646,-16.438,19.609,-16.373,19.706,-16.344,19.866,-16.393,19.849,-16.42,19.802]},{"b":[-17.241,32.648,-16.693,32.869],"l":[-16.693,32.758,-16.774,32.774,-16.929,32.841,-17.054,32.816,-17.191,32.869,-17.241,32.807,-17.226,32.767,-17.171,32.722,-17.018,32.663,-16.837,32.648,-16.765,32.71,-16.693,32.758]},{"b":[-15.964,11.055,-15.896,11.193],"l":[-15.963,11.095,-15.951,11.087,-15.964,11.059,-15.905,11.055,-15.896,11.082,-15.905,11.148,-15.909,11.161,-15.938,11.193,-15.946,11.18,-15.963,11.095]},{"b":[-16.905,28.007,-16.119,28.576],"l":[-16.334,28.38,-16.119,28.528,-16.124,28.576,-16.319,28.558,-16.517,28.413,-16.557,28.4,-16.752,28.37,-16.843,28.376,-16.905,28.34,-16.866,28.293,-16.795,28.149,-16.658,28.007,-16.543,28.032,-16.496,28.062,-16.418,28.151,-16.334,28.38]},{"b":[-13.86,28.845,-13.423,29.237],"l":[-13.535,29.144,-13.65,29.119,-13.788,29.056,-13.824,29.013,-13.86,28.869,-13.784,28.845,-13.716,28.911,-13.555,28.96,-13.478,29.007,-13.454,29.151,-13.423,29.198,-13.464,29.237,-13.501,29.211,-13.535,29.144]},{"b":[-14.492,28.056,-13.827,28.745],"l":[-14.028,28.617,-14.153,28.407,-14.232,28.216,-14.356,28.13,-14.492,28.101,-14.469,28.082,-14.333,28.056,-14.197,28.169,-13.928,28.253,-13.863,28.409,-13.828,28.585,-13.827,28.691,-13.857,28.738,-13.886,28.745,-13.954,28.741,-14.003,28.707,-14.028,28.617]},{"b":[-12.952,7.41,-12.501,7.637],"l":[-12.615,7.637,-12.854,7.622,-12.952,7.571,-12.607,7.475,-12.541,7.41,-12.526,7.436,-12.501,7.535,-12.512,7.582,-12.544,7.607,-12.615,7.637]},{"b":[-15.809,27.747,-15.383,28.159],"l":[-15.401,28.147,-15.415,28.159,-15.433,28.154,-15.453,28.137,-15.683,28.154,-15.721,28.064,-15.809,27.994,-15.807,27.888,-15.71,27.784,-15.656,27.758,-15.559,27.747,-15.437,27.811,-15.389,27.875,-15.383,27.993,-15.407,28.071,-15.401,28.147]},{"b":[-15.78,11.175,-15.658,11.302],"l":[-15.717,11.302,-15.755,11.269,-15.78,11.195,-15.767,11.182,-15.725,11.175,-15.725,11.215,-15.687,11.234,-15.667,11.258,-15.658,11.286,-15.672,11.296,-15.717,11.302]},{"b":[-15.62,11.514,-15.482,11.632],"l":[-15.537,11.618,-15.62,11.533,-15.563,11.514,-15.553,11.537,-15.526,11.554,-15.484,11.568,-15.482,11.632,-15.537,11.618]},{"b":[-25.342,16.926,-24.98,17.194],"l":[-24.98,17.095,-25.035,17.176,-25.113,17.194,-25.337,17.091,-25.342,17.068,-25.322,17.015,-25.308,16.936,-25.267,16.926,-25.17,16.946,-25.017,17.049,-24.98,17.095]},{"b":[-25.093,16.794,-24.887,16.922],"l":[-24.991,16.913,-25.07,16.871,-25.093,16.833,-25.02,16.797,-24.969,16.794,-24.887,16.818,-24.892,16.846,-24.936,16.922,-24.991,16.913]},{"b":[-17.325,28.014,-17.101,28.203],"l":[-17.325,28.118,-17.274,28.038,-17.225,28.014,-17.185,28.022,-17.101,28.083,-17.104,28.111,-17.13,28.156,-17.214,28.199,-17.259,28.203,-17.29,28.176,-17.325,28.118]},{"b":[-16.023,11.434,-15.902,11.598],"l":[-15.949,11.434,-15.902,11.466,-15.915,11.589,-15.965,11.598,-16.019,11.527,-16.023,11.477,-15.997,11.449,-15.949,11.434]},{"b":[-16.152,11.76,-15.986,11.887],"l":[-16.102,11.773,-16.038,11.76,-15.986,11.882,-16.022,11.887,-16.152,11.877,-16.147,11.846,-16.102,11.773]},{"b":[-18.161,27.646,-17.888,27.85],"l":[-18.043,27.768,-18.161,27.761,-18.136,27.728,-18.107,27.707,-17.985,27.646,-17.888,27.81,-17.925,27.85,-18.043,27.768]},{"b":[-22.959,15.986,-22.682,16.237],"l":[-22.693,16.169,-22.749,16.222,-22.803,16.226,-22.834,16.219,-22.918,16.237,-22.916,16.148,-22.959,16.045,-22.884,15.993,-22.821,15.986,-22.71,16.043,-22.682,16.113,-22.693,16.169]},{"b":[-24.517,14.818,-24.296,15.038],"l":[-24.329,15.019,-24.392,15.038,-24.497,14.98,-24.517,14.931,-24.492,14.874,-24.441,14.835,-24.386,14.818,-24.308,14.856,-24.296,14.93,-24.329,15.019]},{"b":[-18.001,28.486,-17.727,28.847],"l":[-17.929,28.845,-18.001,28.758,-17.882,28.565,-17.859,28.486,-17.834,28.493,-17.758,28.569,-17.744,28.616,-17.752,28.689,-17.727,28.724,-17.745,28.787,-17.798,28.847,-17.929,28.845]},{"b":[-24.398,16.493,-24.033,16.678],"l":[-24.271,16.645,-24.377,16.678,-24.393,16.664,-24.398,16.618,-24.322,16.493,-24.283,16.576,-24.243,16.599,-24.094,16.561,-24.033,16.572,-24.046,16.593,-24.088,16.623,-24.271,16.645]},{"b":[-22.991,16.608,-22.888,16.844],"l":[-22.904,16.732,-22.905,16.844,-22.933,16.841,-22.991,16.809,-22.981,16.701,-22.959,16.683,-22.92,16.608,-22.888,16.659,-22.904,16.732]},{"b":[-23.252,15.133,-23.116,15.324],"l":[-23.21,15.324,-23.247,15.257,-23.242,15.241,-23.252,15.178,-23.21,15.133,-23.182,15.137,-23.116,15.167,-23.119,15.268,-23.138,15.318,-23.21,15.324]},{"b":[-23.785,14.916,-23.444,15.329],"l":[-23.535,15.139,-23.58,15.161,-23.701,15.272,-23.707,15.317,-23.748,15.329,-23.759,15.311,-23.754,15.244,-23.783,15.166,-23.785,15.077,-23.705,14.961,-23.637,14.923,-23.505,14.916,-23.444,15.008,-23.535,15.139]},{"b":[44.22,-12.357,44.527,-12.071],"l":[44.476,-12.082,44.452,-12.071,44.413,-12.093,44.407,-12.12,44.379,-12.166,44.334,-12.173,44.292,-12.165,44.22,-12.171,44.377,-12.252,44.46,-12.335,44.505,-12.357,44.526,-12.324,44.527,-12.22,44.476,-12.082]},{"b":[43.631,-12.368,43.859,-12.247],"l":[43.704,-12.256,43.631,-12.247,43.633,-12.288,43.664,-12.343,43.859,-12.368,43.789,-12.307,43.704,-12.256]},{"b":[45.043,-12.985,45.223,-12.653],"l":[45.135,-12.709,45.092,-12.653,45.043,-12.701,45.094,-12.786,45.088,-12.835,45.069,-12.896,45.088,-12.958,45.118,-12.985,45.18,-12.977,45.179,-12.92,45.209,-12.848,45.204,-12.824,45.223,-12.752,45.159,-12.713,45.135,-12.709]},{"b":[43.227,-11.915,43.492,-11.368],"l":[43.379,-11.614,43.393,-11.409,43.342,-11.368,43.299,-11.375,43.281,-11.391,43.256,-11.432,43.227,-11.752,43.303,-11.844,43.355,-11.858,43.447,-11.915,43.466,-11.901,43.492,-11.862,43.448,-11.753,43.379,-11.614]},{"b":[39.603,-7.991,39.907,-7.649],"l":[39.847,-7.73,39.717,-7.832,39.661,-7.901,39.603,-7.936,39.636,-7.978,39.657,-7.991,39.711,-7.977,39.762,-7.912,39.824,-7.901,39.898,-7.728,39.907,-7.649,39.891,-7.663,39.847,-7.73]},{"b":[40.957,-2.167,41.139,-2.037],"l":[40.976,-2.11,40.957,-2.167,40.994,-2.158,41.118,-2.1,41.137,-2.085,41.139,-2.07,41.131,-2.053,41.086,-2.037,40.976,-2.11]},{"b":[39.182,-6.454,39.573,-5.722],"l":[39.488,-6.166,39.433,-6.115,39.368,-5.951,39.357,-5.812,39.309,-5.722,39.267,-5.853,39.192,-5.931,39.206,-6.083,39.182,-6.173,39.243,-6.275,39.313,-6.279,39.383,-6.365,39.424,-6.348,39.447,-6.42,39.481,-6.454,39.509,-6.452,39.563,-6.427,39.573,-6.387,39.496,-6.175,39.488,-6.166]},{"b":[57.318,-20.513,57.792,-19.99],"l":[57.737,-20.098,57.657,-19.99,57.576,-19.997,57.515,-20.056,57.486,-20.144,57.416,-20.184,57.386,-20.229,57.362,-20.338,57.365,-20.406,57.318,-20.428,57.328,-20.45,57.383,-20.504,57.525,-20.513,57.651,-20.485,57.707,-20.435,57.725,-20.369,57.781,-20.327,57.792,-20.213,57.737,-20.098]},{"b":[55.233,-21.369,55.839,-20.865],"l":[55.662,-20.906,55.596,-20.88,55.45,-20.865,55.311,-20.904,55.25,-21.002,55.233,-21.058,55.31,-21.217,55.363,-21.274,55.558,-21.358,55.656,-21.369,55.797,-21.339,55.822,-21.278,55.839,-21.139,55.739,-21.021,55.662,-20.906]},{"b":[48.191,-13.4,48.351,-13.198],"l":[48.351,-13.31,48.309,-13.198,48.27,-13.205,48.256,-13.256,48.191,-13.26,48.212,-13.385,48.344,-13.4,48.342,-13.364,48.351,-13.31]},{"b":[49.824,-17.087,50.023,-16.695],"l":[49.856,-16.933,49.824,-17.087,49.936,-16.903,50.023,-16.695,49.986,-16.712,49.856,-16.933]},{"b":[-5.783,-16.004,-5.66,-15.906],"l":[-5.775,-15.957,-5.783,-16.004,-5.692,-15.998,-5.66,-15.971,-5.662,-15.913,-5.708,-15.906,-5.775,-15.957]},{"b":[39.647,-5.444,39.871,-4.906],"l":[39.749,-5.444,39.796,-5.394,39.853,-5.255,39.859,-5.155,39.856,-5.004,39.871,-4.957,39.865,-4.906,39.781,-4.945,39.673,-4.927,39.701,-5.114,39.647,-5.369,39.673,-5.407,39.708,-5.429,39.749,-5.444]},{"b":[55.383,-4.786,55.543,-4.559],"l":[55.481,-4.695,55.495,-4.755,55.543,-4.786,55.54,-4.693,55.456,-4.559,55.383,-4.609,55.417,-4.65,55.481,-4.695]},{"b":[8.434,3.217,8.951,3.758],"l":[8.76,3.754,8.736,3.758,8.676,3.736,8.638,3.669,8.623,3.58,8.577,3.482,8.55,3.468,8.465,3.451,8.452,3.423,8.434,3.332,8.445,3.294,8.475,3.265,8.652,3.217,8.704,3.224,8.763,3.305,8.792,3.4,8.946,3.628,8.951,3.705,8.91,3.758,8.76,3.754]},{"b":[6.468,0.047,6.75,0.404],"l":[6.626,0.4,6.524,0.34,6.478,0.28,6.468,0.227,6.497,0.117,6.52,0.066,6.557,0.047,6.66,0.121,6.75,0.243,6.75,0.326,6.687,0.404,6.626,0.4]},{"b":[7.331,1.542,7.452,1.699],"l":[7.414,1.699,7.388,1.68,7.331,1.603,7.342,1.564,7.387,1.542,7.424,1.568,7.452,1.631,7.45,1.662,7.437,1.683,7.414,1.699]},{"b":[7.14,4.388,7.328,4.527],"l":[7.227,4.527,7.14,4.395,7.204,4.388,7.301,4.418,7.328,4.487,7.271,4.499,7.227,4.527]},{"b":[-44.36,-23.212,-44.098,-23.074],"l":[-44.22,-23.083,-44.243,-23.074,-44.274,-23.116,-44.36,-23.172,-44.32,-23.212,-44.221,-23.191,-44.156,-23.167,-44.098,-23.169,-44.129,-23.142,-44.192,-23.113,-44.22,-23.083]},{"b":[-48.666,-26.414,-48.498,-26.17],"l":[-48.498,-26.219,-48.54,-26.17,-48.666,-26.29,-48.603,-26.414,-48.584,-26.402,-48.568,-26.38,-48.531,-26.313,-48.498,-26.219]},{"b":[-48.555,-27.812,-48.378,-27.4],"l":[-48.542,-27.575,-48.555,-27.812,-48.486,-27.767,-48.497,-27.707,-48.41,-27.566,-48.378,-27.451,-48.415,-27.4,-48.465,-27.436,-48.505,-27.496,-48.542,-27.575]},{"b":[-45.451,-23.941,-45.233,-23.728],"l":[-45.233,-23.825,-45.249,-23.783,-45.272,-23.752,-45.302,-23.728,-45.451,-23.896,-45.413,-23.935,-45.303,-23.915,-45.261,-23.941,-45.26,-23.889,-45.25,-23.853,-45.233,-23.825]},{"b":[11.124,34.682,11.281,34.82],"l":[11.255,34.82,11.153,34.745,11.124,34.682,11.278,34.754,11.281,34.802,11.255,34.82]},{"b":[10.722,33.687,11.038,33.893],"l":[10.922,33.893,10.745,33.889,10.734,33.856,10.722,33.739,10.757,33.717,10.785,33.718,10.857,33.687,10.883,33.69,10.931,33.717,10.958,33.722,10.993,33.746,11.038,33.785,11.034,33.805,11.018,33.823,10.922,33.893]},{"b":[-4.785,54.059,-4.338,54.407],"l":[-4.412,54.185,-4.392,54.225,-4.338,54.269,-4.377,54.393,-4.396,54.403,-4.425,54.407,-4.509,54.377,-4.615,54.267,-4.699,54.225,-4.746,54.119,-4.785,54.073,-4.766,54.069,-4.696,54.081,-4.614,54.059,-4.412,54.185]},{"b":[-6.669,56.579,-6.484,56.672],"l":[-6.57,56.661,-6.669,56.594,-6.664,56.579,-6.608,56.585,-6.53,56.627,-6.484,56.666,-6.506,56.672,-6.57,56.661]},{"b":[-5.393,55.448,-5.095,55.717],"l":[-5.105,55.574,-5.16,55.667,-5.185,55.691,-5.252,55.717,-5.318,55.709,-5.346,55.691,-5.371,55.667,-5.393,55.618,-5.331,55.481,-5.277,55.457,-5.231,55.448,-5.105,55.449,-5.095,55.494,-5.105,55.574]},{"b":[-6.326,56.289,-5.761,56.65],"l":[-5.836,56.523,-5.947,56.535,-6.03,56.61,-6.103,56.646,-6.138,56.65,-6.182,56.643,-6.286,56.612,-6.306,56.599,-6.32,56.569,-6.311,56.552,-6.139,56.491,-6.185,56.357,-6.298,56.339,-6.326,56.321,-6.313,56.294,-6.176,56.289,-5.778,56.344,-5.761,56.491,-5.836,56.523]},{"b":[-6.496,55.607,-6.055,55.931],"l":[-6.129,55.931,-6.216,55.905,-6.311,55.856,-6.344,55.874,-6.375,55.871,-6.413,55.855,-6.445,55.832,-6.463,55.808,-6.466,55.769,-6.496,55.712,-6.491,55.697,-6.452,55.704,-6.334,55.774,-6.302,55.781,-6.286,55.773,-6.302,55.728,-6.27,55.67,-6.307,55.619,-6.305,55.607,-6.253,55.607,-6.088,55.658,-6.055,55.695,-6.058,55.723,-6.093,55.802,-6.129,55.931]},{"b":[-6.072,55.804,-5.725,56.12],"l":[-5.939,56.045,-5.973,56.004,-5.97,55.992,-5.912,55.975,-6.041,55.926,-6.072,55.893,-6.071,55.848,-6.06,55.823,-6.042,55.807,-5.991,55.804,-5.97,55.815,-5.797,56.006,-5.725,56.119,-5.762,56.12,-5.8,56.109,-5.939,56.045]},{"b":[-1.166,60.502,-0.992,60.72],"l":[-1.093,60.72,-1.166,60.604,-1.068,60.502,-1.043,60.514,-1.034,60.53,-1.035,60.593,-1.049,60.647,-1.045,60.656,-1,60.658,-0.992,60.686,-1.006,60.717,-1.093,60.72]},{"b":[-1.664,59.879,-1.052,60.61],"l":[-1.236,60.485,-1.287,60.467,-1.308,60.538,-1.302,60.608,-1.364,60.61,-1.414,60.599,-1.498,60.53,-1.553,60.517,-1.572,60.494,-1.549,60.481,-1.45,60.469,-1.375,60.333,-1.494,60.292,-1.577,60.298,-1.664,60.283,-1.66,60.262,-1.641,60.237,-1.613,60.229,-1.517,60.231,-1.499,60.222,-1.497,60.194,-1.481,60.173,-1.409,60.19,-1.323,60.188,-1.291,60.153,-1.276,60.115,-1.3,60.04,-1.356,59.911,-1.299,59.879,-1.284,59.887,-1.245,59.971,-1.199,60.007,-1.179,60.114,-1.166,60.124,-1.153,60.177,-1.134,60.207,-1.066,60.382,-1.052,60.444,-1.118,60.418,-1.158,60.418,-1.236,60.485]},{"b":[-0.938,60.684,-0.774,60.832],"l":[-0.825,60.684,-0.826,60.716,-0.774,60.8,-0.774,60.812,-0.802,60.831,-0.823,60.832,-0.865,60.806,-0.891,60.816,-0.916,60.81,-0.928,60.797,-0.938,60.746,-0.922,60.697,-0.909,60.687,-0.825,60.684]},{"b":[-3.401,58.781,-3.159,58.919],"l":[-3.272,58.905,-3.357,58.919,-3.395,58.91,-3.401,58.882,-3.367,58.84,-3.279,58.782,-3.222,58.781,-3.165,58.794,-3.159,58.801,-3.212,58.814,-3.222,58.826,-3.228,58.857,-3.272,58.905]},{"b":[-4.568,53.134,-4.049,53.419],"l":[-4.315,53.417,-4.462,53.419,-4.568,53.386,-4.553,53.26,-4.472,53.176,-4.419,53.178,-4.373,53.134,-4.279,53.172,-4.2,53.218,-4.084,53.264,-4.049,53.306,-4.155,53.303,-4.197,53.321,-4.315,53.417]},{"b":[-2.662,59.23,-2.407,59.304],"l":[-2.536,59.304,-2.604,59.289,-2.662,59.23,-2.549,59.231,-2.43,59.271,-2.407,59.298,-2.536,59.304]},{"b":[-3.035,58.739,-2.896,58.836],"l":[-3.035,58.823,-2.975,58.757,-2.939,58.739,-2.929,58.742,-2.913,58.8,-2.896,58.828,-2.941,58.836,-3.035,58.823]},{"b":[-3.354,58.891,-2.762,59.144],"l":[-2.818,58.982,-2.885,58.985,-2.995,59.006,-3.071,59.005,-3.057,59.03,-3.02,59.058,-3.019,59.076,-3.051,59.099,-3.156,59.136,-3.249,59.144,-3.31,59.131,-3.347,59.065,-3.354,59.019,-3.347,58.987,-3.332,58.971,-3.304,58.967,-3.242,59,-3.233,58.99,-3.233,58.956,-3.223,58.939,-3.201,58.925,-3.167,58.919,-2.995,58.939,-2.864,58.891,-2.826,58.893,-2.793,58.907,-2.762,58.956,-2.818,58.982]},{"b":[-6.885,61.768,-6.623,61.904],"l":[-6.662,61.862,-6.791,61.895,-6.842,61.904,-6.885,61.899,-6.864,61.862,-6.839,61.841,-6.764,61.815,-6.67,61.769,-6.643,61.768,-6.623,61.806,-6.626,61.827,-6.662,61.862]},{"b":[-6.935,61.414,-6.68,61.634],"l":[-6.704,61.496,-6.741,61.536,-6.741,61.571,-6.77,61.584,-6.882,61.603,-6.906,61.631,-6.935,61.634,-6.929,61.603,-6.888,61.535,-6.771,61.452,-6.703,61.418,-6.68,61.414,-6.699,61.445,-6.704,61.496]},{"b":[-7.423,62.04,-7.065,62.151],"l":[-7.379,62.075,-7.255,62.046,-7.179,62.04,-7.117,62.047,-7.065,62.073,-7.097,62.101,-7.187,62.139,-7.235,62.151,-7.337,62.139,-7.423,62.14,-7.379,62.075]},{"b":[-7.172,61.951,-6.631,62.316],"l":[-7.014,62.094,-6.81,61.977,-6.725,61.951,-6.714,61.964,-6.723,61.99,-6.809,62.08,-6.838,62.095,-6.841,62.119,-6.823,62.139,-6.769,62.131,-6.696,62.094,-6.656,62.094,-6.631,62.228,-6.804,62.266,-6.959,62.316,-7.172,62.286,-7.014,62.094]},{"b":[-3.052,59.162,-2.72,59.347],"l":[-2.964,59.274,-2.861,59.247,-2.852,59.182,-2.815,59.162,-2.729,59.187,-2.72,59.219,-2.731,59.227,-2.815,59.241,-2.862,59.288,-2.976,59.347,-3.042,59.334,-3.052,59.324,-3.013,59.291,-2.964,59.274]},{"b":[-6.559,62.187,-6.406,62.356],"l":[-6.454,62.187,-6.406,62.259,-6.473,62.292,-6.555,62.356,-6.552,62.278,-6.559,62.225,-6.544,62.206,-6.525,62.198,-6.454,62.187]},{"b":[-7.096,57.75,-6.194,58.503],"l":[-6.854,57.827,-6.797,57.828,-6.683,57.911,-6.578,57.941,-6.425,58.021,-6.402,58.041,-6.403,58.076,-6.437,58.092,-6.555,58.093,-6.419,58.141,-6.376,58.185,-6.326,58.189,-6.199,58.363,-6.194,58.435,-6.219,58.489,-6.237,58.503,-6.297,58.487,-6.544,58.383,-6.742,58.322,-6.776,58.302,-6.788,58.284,-6.725,58.198,-6.726,58.189,-6.812,58.196,-6.886,58.183,-6.95,58.218,-7.012,58.229,-7.028,58.222,-7.045,58.202,-7.085,58.182,-7.096,58.138,-7.088,58.095,-7.077,58.079,-7.038,58.072,-7.017,58.055,-6.985,58.05,-7.052,58.018,-7.057,58.003,-7.003,57.975,-6.864,57.933,-6.857,57.924,-6.944,57.894,-6.956,57.865,-7.083,57.814,-7.013,57.762,-6.983,57.75,-6.957,57.75,-6.91,57.773,-6.854,57.827]},{"b":[-6.433,56.952,-6.261,57.051],"l":[-6.433,57.018,-6.383,56.971,-6.346,56.954,-6.309,56.952,-6.279,56.965,-6.261,56.985,-6.261,57.01,-6.278,57.031,-6.322,57.051,-6.433,57.018]},{"b":[-6.761,57.044,-5.669,57.672],"l":[-6.362,57.238,-6.323,57.202,-6.266,57.184,-6.163,57.182,-6.034,57.201,-6.015,57.052,-5.987,57.044,-5.949,57.045,-5.914,57.063,-5.795,57.147,-5.696,57.198,-5.669,57.227,-5.672,57.253,-5.706,57.269,-5.88,57.263,-6.068,57.284,-6.093,57.302,-6.136,57.314,-6.141,57.354,-6.164,57.409,-6.146,57.461,-6.145,57.505,-6.166,57.585,-6.247,57.651,-6.306,57.672,-6.358,57.667,-6.379,57.603,-6.617,57.563,-6.615,57.553,-6.583,57.521,-6.583,57.507,-6.606,57.491,-6.643,57.483,-6.704,57.496,-6.753,57.459,-6.761,57.442,-6.741,57.412,-6.675,57.363,-6.442,57.327,-6.362,57.238]},{"b":[-7.516,57.533,-7.093,57.683],"l":[-7.206,57.683,-7.271,57.657,-7.325,57.663,-7.392,57.645,-7.44,57.656,-7.47,57.653,-7.499,57.636,-7.516,57.616,-7.515,57.602,-7.321,57.534,-7.183,57.533,-7.093,57.627,-7.206,57.683]},{"b":[-7.422,57.11,-7.248,57.384],"l":[-7.296,57.384,-7.411,57.381,-7.407,57.298,-7.422,57.229,-7.416,57.192,-7.381,57.131,-7.347,57.115,-7.292,57.11,-7.25,57.115,-7.248,57.126,-7.267,57.372,-7.296,57.384]},{"b":[-10.266,53.885,-9.948,54.017],"l":[-10.14,54.005,-10.181,54.017,-10.266,53.978,-10.062,53.96,-10.027,53.92,-9.953,53.885,-9.948,53.913,-9.956,53.987,-9.996,54.003,-10.14,54.005]},{"b":[-7.543,56.952,-7.399,57.019],"l":[-7.523,57.007,-7.543,56.972,-7.537,56.96,-7.505,56.952,-7.417,56.965,-7.399,56.983,-7.407,57,-7.455,57.019,-7.523,57.007]},{"b":[21.806,60.332,21.994,60.47],"l":[21.908,60.393,21.95,60.402,21.907,60.438,21.827,60.47,21.819,60.452,21.846,60.412,21.806,60.401,21.819,60.382,21.921,60.332,21.994,60.337,21.98,60.355,21.908,60.393]},{"b":[19.519,60.135,19.667,60.246],"l":[19.551,60.244,19.519,60.185,19.537,60.145,19.58,60.135,19.6,60.163,19.629,60.17,19.667,60.165,19.662,60.187,19.629,60.246,19.551,60.244]},{"b":[19.672,60.082,20.259,60.406],"l":[20.073,60.193,20.125,60.201,20.155,60.192,20.195,60.194,20.259,60.261,20.24,60.283,20.184,60.294,20.168,60.315,20.087,60.353,20.034,60.359,20.02,60.351,19.99,60.351,19.945,60.358,19.888,60.406,19.823,60.39,19.788,60.354,19.812,60.332,19.855,60.319,19.872,60.302,19.867,60.268,19.848,60.221,19.785,60.213,19.779,60.286,19.737,60.282,19.687,60.268,19.672,60.233,19.746,60.099,19.8,60.082,20.034,60.094,20.032,60.152,20.043,60.181,20.073,60.193]},{"b":[21.084,63.152,21.422,63.278],"l":[21.366,63.262,21.287,63.228,21.229,63.223,21.218,63.241,21.222,63.259,21.236,63.278,21.084,63.278,21.149,63.199,21.253,63.152,21.31,63.163,21.318,63.179,21.367,63.207,21.378,63.199,21.416,63.197,21.422,63.246,21.366,63.262]},{"b":[19.039,57.86,19.331,57.981],"l":[19.135,57.981,19.039,57.911,19.087,57.865,19.138,57.86,19.156,57.923,19.331,57.963,19.281,57.978,19.135,57.981]},{"b":[16.394,56.24,17.118,57.345],"l":[16.961,57.25,16.902,57.175,16.865,57.091,16.728,56.902,16.63,56.877,16.412,56.569,16.394,56.484,16.401,56.311,16.432,56.244,16.477,56.24,16.529,56.291,16.778,56.805,16.838,56.841,16.884,56.985,17.054,57.208,17.058,57.229,17.05,57.28,17.118,57.32,17.089,57.332,17.025,57.345,16.996,57.318,16.961,57.25]},{"b":[14.684,55.005,15.137,55.297],"l":[14.765,55.297,14.714,55.238,14.684,55.102,14.886,55.033,15.051,55.005,15.088,55.022,15.137,55.087,15.133,55.145,14.765,55.297]},{"b":[13.156,54.246,13.734,54.699],"l":[13.602,54.425,13.58,54.464,13.603,54.488,13.671,54.535,13.658,54.56,13.636,54.577,13.491,54.615,13.45,54.65,13.423,54.699,13.337,54.697,13.24,54.638,13.231,54.583,13.177,54.544,13.181,54.509,13.156,54.397,13.162,54.365,13.19,54.326,13.364,54.246,13.415,54.25,13.482,54.337,13.595,54.338,13.707,54.281,13.734,54.315,13.709,54.383,13.602,54.425]},{"b":[12.119,54.892,12.549,55.064],"l":[12.31,55.041,12.274,55.064,12.259,55.021,12.22,54.994,12.162,54.975,12.144,54.959,12.119,54.914,12.184,54.892,12.358,54.962,12.511,54.951,12.549,54.966,12.513,54.997,12.47,55.017,12.417,55.031,12.31,55.041]},{"b":[12.52,55.554,12.666,55.68],"l":[12.52,55.615,12.551,55.556,12.572,55.554,12.666,55.597,12.648,55.647,12.62,55.679,12.599,55.68,12.57,55.65,12.52,55.615]},{"b":[11.036,54.629,11.766,54.952],"l":[11.658,54.833,11.538,54.83,11.361,54.892,11.258,54.952,11.059,54.941,11.042,54.893,11.036,54.773,11.457,54.629,11.586,54.662,11.68,54.654,11.766,54.679,11.759,54.768,11.74,54.807,11.658,54.833]},{"b":[10.2,54.837,10.505,54.963],"l":[10.347,54.906,10.266,54.949,10.2,54.963,10.216,54.941,10.341,54.859,10.417,54.837,10.484,54.848,10.505,54.861,10.414,54.897,10.347,54.906]},{"b":[9.771,54.872,10.061,55.069],"l":[9.781,55.069,9.771,55.06,9.806,54.906,9.904,54.897,9.957,54.872,10.061,54.886,10.058,54.908,9.999,54.986,9.83,55.058,9.781,55.069]},{"b":[10.622,54.745,10.951,55.158],"l":[10.738,54.962,10.692,54.903,10.622,54.851,10.629,54.826,10.69,54.745,10.734,54.751,10.765,54.8,10.921,55.062,10.951,55.156,10.925,55.158,10.857,55.052,10.738,54.962]},{"b":[10.516,55.765,10.662,55.992],"l":[10.527,55.784,10.59,55.765,10.607,55.783,10.627,55.834,10.662,55.878,10.636,55.914,10.547,55.992,10.516,55.959,10.544,55.907,10.52,55.848,10.527,55.784]},{"b":[11.012,54.416,11.283,54.533],"l":[11.085,54.533,11.043,54.515,11.012,54.466,11.071,54.456,11.129,54.416,11.283,54.418,11.28,54.438,11.234,54.501,11.085,54.533]},{"b":[10.874,57.229,11.175,57.33],"l":[10.935,57.309,10.874,57.262,11.011,57.229,11.052,57.253,11.077,57.277,11.175,57.323,11.086,57.33,10.935,57.309]},{"b":[8.285,54.767,8.63,55.059],"l":[8.451,55.055,8.405,55.059,8.296,54.908,8.285,54.767,8.308,54.787,8.347,54.848,8.601,54.865,8.63,54.892,8.38,54.9,8.371,54.929,8.39,54.986,8.404,55.015,8.451,55.055]},{"b":[-1.389,45.817,-1.178,46.05],"l":[-1.389,46.05,-1.389,46.033,-1.369,45.968,-1.28,45.897,-1.214,45.817,-1.178,45.904,-1.285,46.003,-1.389,46.05]},{"b":[-1.563,50.589,-1.066,50.773],"l":[-1.313,50.773,-1.386,50.734,-1.516,50.703,-1.563,50.666,-1.515,50.67,-1.306,50.589,-1.251,50.589,-1.196,50.599,-1.176,50.615,-1.149,50.656,-1.066,50.69,-1.144,50.735,-1.313,50.773]},{"b":[4.709,53,4.886,53.183],"l":[4.74,53.091,4.709,53.036,4.727,53.02,4.787,53,4.886,53.071,4.886,53.183,4.74,53.091]},{"b":[10.11,42.713,10.432,42.858],"l":[10.359,42.822,10.286,42.828,10.248,42.816,10.128,42.81,10.11,42.785,10.131,42.742,10.209,42.737,10.336,42.761,10.419,42.713,10.41,42.771,10.432,42.797,10.428,42.819,10.395,42.858,10.359,42.822]},{"b":[8.359,38.969,8.479,39.116],"l":[8.367,39.116,8.359,39.099,8.361,39.039,8.421,38.969,8.479,39.068,8.441,39.091,8.367,39.116]},{"b":[8.206,40.994,8.344,41.122],"l":[8.344,41.102,8.32,41.122,8.267,41.099,8.224,41.031,8.206,40.997,8.253,40.994,8.286,41.04,8.319,41.063,8.344,41.102]},{"b":[3.843,39.83,4.322,40.075],"l":[4.059,40.075,3.853,40.063,3.845,40.036,3.843,39.976,3.867,39.959,3.968,39.946,4.275,39.83,4.294,39.842,4.322,39.898,4.315,39.917,4.226,40.032,4.059,40.075]},{"b":[1.402,38.659,1.594,38.768],"l":[1.417,38.74,1.402,38.711,1.406,38.671,1.505,38.671,1.571,38.659,1.594,38.672,1.593,38.701,1.497,38.712,1.436,38.768,1.417,38.74]},{"b":[1.223,38.857,1.624,39.121],"l":[1.349,39.081,1.303,39.031,1.3,38.982,1.256,38.973,1.223,38.904,1.257,38.879,1.409,38.857,1.445,38.919,1.495,38.933,1.624,39.039,1.613,39.087,1.564,39.121,1.349,39.081]},{"b":[-31.283,39.359,-31.137,39.521],"l":[-31.261,39.497,-31.283,39.394,-31.258,39.376,-31.181,39.359,-31.137,39.407,-31.139,39.479,-31.2,39.521,-31.261,39.497]},{"b":[-27.386,38.634,-27.042,38.803],"l":[-27.127,38.79,-27.26,38.803,-27.351,38.789,-27.386,38.766,-27.362,38.698,-27.303,38.661,-27.095,38.634,-27.075,38.643,-27.042,38.679,-27.042,38.741,-27.127,38.79]},{"b":[-28.311,38.544,-27.778,38.744],"l":[-28.311,38.744,-28.187,38.655,-28.092,38.621,-27.826,38.544,-27.778,38.556,-27.963,38.636,-28.311,38.744]},{"b":[-28.842,38.522,-28.606,38.638],"l":[-28.698,38.638,-28.842,38.598,-28.744,38.522,-28.641,38.525,-28.606,38.551,-28.624,38.586,-28.655,38.614,-28.698,38.638]},{"b":[-28.549,38.385,-28.065,38.553],"l":[-28.402,38.553,-28.51,38.553,-28.549,38.519,-28.531,38.463,-28.454,38.409,-28.332,38.413,-28.231,38.385,-28.19,38.404,-28.065,38.413,-28.147,38.453,-28.402,38.553]},{"b":[-25.848,37.715,-25.182,37.911],"l":[-25.585,37.834,-25.649,37.841,-25.784,37.911,-25.846,37.894,-25.848,37.872,-25.834,37.826,-25.734,37.763,-25.439,37.715,-25.251,37.735,-25.191,37.764,-25.182,37.838,-25.267,37.849,-25.585,37.834]},{"b":[14.351,35.82,14.566,35.978],"l":[14.448,35.957,14.351,35.978,14.352,35.872,14.436,35.822,14.533,35.82,14.566,35.853,14.537,35.886,14.448,35.957]},{"b":[15.066,44.027,15.247,44.158],"l":[15.231,44.062,15.066,44.158,15.075,44.138,15.122,44.093,15.247,44.027,15.231,44.062]},{"b":[14.438,44.956,14.81,45.225],"l":[14.739,45.065,14.701,45.09,14.63,45.178,14.571,45.225,14.525,45.147,14.438,45.099,14.45,45.079,14.512,45.035,14.613,45.025,14.628,44.994,14.687,44.956,14.81,44.977,14.739,45.065]},{"b":[14.66,44.715,14.857,44.848],"l":[14.857,44.715,14.831,44.759,14.764,44.821,14.754,44.845,14.691,44.848,14.672,44.824,14.66,44.8,14.678,44.77,14.763,44.755,14.857,44.715]},{"b":[14.742,44.309,15.24,44.697],"l":[14.913,44.486,14.996,44.434,15.039,44.393,15.098,44.358,15.163,44.309,15.188,44.336,15.214,44.348,15.24,44.35,15.113,44.436,15.006,44.534,14.898,44.611,14.855,44.618,14.804,44.649,14.742,44.697,14.76,44.665,14.885,44.545,14.913,44.486]},{"b":[14.865,43.898,15.203,44.168],"l":[14.953,44.117,14.865,44.168,14.891,44.126,15.136,43.907,15.15,43.912,15.202,43.898,15.203,43.908,15.189,43.922,14.953,44.117]},{"b":[15.27,43.9,15.437,44.011],"l":[15.374,43.915,15.437,43.9,15.371,43.974,15.27,44.011,15.309,43.961,15.374,43.915]},{"b":[14.286,44.621,14.488,45.167],"l":[14.468,44.725,14.453,44.869,14.467,44.97,14.394,45.031,14.369,45.081,14.358,45.167,14.331,45.165,14.286,45.145,14.34,45.02,14.342,44.98,14.303,44.94,14.312,44.9,14.389,44.758,14.42,44.67,14.48,44.621,14.488,44.66,14.483,44.693,14.468,44.725]},{"b":[17.344,42.7,17.744,42.8],"l":[17.39,42.799,17.344,42.79,17.744,42.7,17.608,42.769,17.432,42.8,17.39,42.799]},{"b":[20.352,38.071,20.789,38.475],"l":[20.789,38.142,20.695,38.246,20.625,38.268,20.612,38.383,20.563,38.475,20.55,38.457,20.548,38.395,20.52,38.332,20.481,38.318,20.435,38.356,20.409,38.337,20.352,38.222,20.353,38.18,20.391,38.188,20.452,38.234,20.473,38.219,20.499,38.184,20.496,38.164,20.524,38.107,20.569,38.098,20.606,38.12,20.761,38.071,20.781,38.089,20.789,38.142]},{"b":[20.62,37.665,20.994,37.93],"l":[20.84,37.841,20.759,37.853,20.692,37.93,20.62,37.855,20.635,37.823,20.704,37.743,20.819,37.665,20.909,37.732,20.994,37.708,20.888,37.805,20.84,37.841]},{"b":[20.555,38.583,20.72,38.844],"l":[20.648,38.601,20.687,38.609,20.715,38.638,20.72,38.799,20.694,38.844,20.635,38.818,20.592,38.76,20.558,38.662,20.555,38.583,20.584,38.602,20.614,38.606,20.648,38.601]},{"b":[20.624,38.319,20.759,38.484],"l":[20.701,38.451,20.675,38.476,20.65,38.484,20.624,38.48,20.646,38.414,20.709,38.319,20.759,38.329,20.739,38.366,20.712,38.399,20.701,38.426,20.701,38.451]},{"b":[19.646,39.377,20.1,39.82],"l":[19.955,39.47,19.927,39.506,19.903,39.6,19.904,39.619,19.847,39.668,19.862,39.693,19.937,39.747,19.926,39.774,19.892,39.797,19.839,39.82,19.707,39.798,19.646,39.767,19.649,39.726,19.809,39.585,19.884,39.462,19.975,39.411,20.1,39.377,20.078,39.433,20.028,39.442,19.955,39.47]},{"b":[23.419,37.901,23.551,37.991],"l":[23.511,37.901,23.551,37.926,23.535,37.97,23.516,37.986,23.484,37.991,23.462,37.98,23.439,37.941,23.419,37.931,23.435,37.911,23.467,37.902,23.511,37.901]},{"b":[22.906,36.146,23.097,36.384],"l":[22.95,36.384,22.933,36.369,22.906,36.32,22.911,36.221,22.939,36.176,23.042,36.146,23.054,36.19,23.097,36.247,22.998,36.328,22.95,36.384]},{"b":[3.699,51.627,4.075,51.746],"l":[3.949,51.739,3.789,51.746,3.699,51.73,3.699,51.71,3.732,51.678,3.819,51.694,3.951,51.627,4.075,51.649,4.068,51.667,4.047,51.685,3.949,51.739]},{"b":[-51.35,69.517,-50.679,69.924],"l":[-51.35,69.855,-51.319,69.804,-51.339,69.732,-51.315,69.674,-51.234,69.552,-51.202,69.525,-51.17,69.517,-51.014,69.552,-50.97,69.583,-50.978,69.618,-50.967,69.664,-50.912,69.757,-50.754,69.798,-50.698,69.829,-50.679,69.849,-50.94,69.909,-51.095,69.924,-51.209,69.914,-51.35,69.855]},{"b":[-37.238,65.531,-36.953,65.722],"l":[-37.048,65.722,-37.223,65.695,-37.238,65.61,-37.187,65.531,-37.031,65.532,-36.987,65.576,-36.953,65.663,-37.048,65.722]},{"b":[-53.958,71.034,-53.432,71.297],"l":[-53.629,71.034,-53.535,71.041,-53.488,71.056,-53.455,71.083,-53.437,71.115,-53.432,71.153,-53.441,71.186,-53.512,71.25,-53.584,71.297,-53.701,71.283,-53.862,71.207,-53.947,71.156,-53.958,71.128,-53.941,71.104,-53.898,71.085,-53.629,71.034]},{"b":[-56.215,72.564,-55.017,72.842],"l":[-55.524,72.568,-55.274,72.684,-55.158,72.723,-55.017,72.791,-55.033,72.821,-55.206,72.842,-55.235,72.825,-55.428,72.789,-55.516,72.781,-55.574,72.78,-55.666,72.794,-55.994,72.782,-56.078,72.753,-56.215,72.719,-56.141,72.668,-56.043,72.656,-55.936,72.668,-55.869,72.662,-55.827,72.652,-55.814,72.636,-55.781,72.617,-55.687,72.61,-55.634,72.579,-55.567,72.564,-55.524,72.568]},{"b":[-52.148,70.853,-51.607,70.976],"l":[-51.97,70.976,-52.107,70.968,-52.148,70.904,-52.144,70.882,-52.119,70.871,-51.809,70.853,-51.675,70.855,-51.607,70.869,-51.631,70.892,-51.807,70.942,-51.97,70.976]},{"b":[-46.79,60.66,-46.205,60.944],"l":[-46.394,60.909,-46.79,60.78,-46.788,60.758,-46.666,60.766,-46.553,60.741,-46.496,60.687,-46.382,60.66,-46.267,60.781,-46.254,60.842,-46.219,60.889,-46.205,60.944,-46.394,60.909]},{"b":[-126.231,49.264,-126.064,49.38],"l":[-126.115,49.365,-126.209,49.38,-126.231,49.339,-126.23,49.296,-126.187,49.278,-126.064,49.264,-126.092,49.354,-126.115,49.365]},{"b":[-122.51,47.355,-122.394,47.49],"l":[-122.507,47.422,-122.51,47.358,-122.469,47.39,-122.458,47.386,-122.457,47.359,-122.437,47.355,-122.399,47.373,-122.394,47.395,-122.442,47.446,-122.469,47.49,-122.486,47.489,-122.507,47.422]},{"b":[-122.576,47.575,-122.497,47.704],"l":[-122.517,47.691,-122.55,47.704,-122.56,47.698,-122.574,47.667,-122.576,47.619,-122.558,47.598,-122.503,47.575,-122.497,47.595,-122.508,47.683,-122.517,47.691]},{"b":[-123.003,48.594,-122.769,48.71],"l":[-122.918,48.707,-122.977,48.679,-123.003,48.652,-122.986,48.627,-122.893,48.594,-122.887,48.612,-122.903,48.665,-122.883,48.661,-122.838,48.627,-122.809,48.63,-122.769,48.651,-122.782,48.673,-122.898,48.71,-122.918,48.707]},{"b":[-122.932,48.422,-122.815,48.552],"l":[-122.869,48.549,-122.885,48.552,-122.912,48.538,-122.932,48.485,-122.922,48.457,-122.89,48.435,-122.837,48.422,-122.821,48.431,-122.815,48.452,-122.862,48.502,-122.869,48.549]},{"b":[-123.17,48.468,-122.987,48.613],"l":[-123.114,48.613,-123.162,48.606,-123.17,48.587,-123.153,48.526,-123.14,48.508,-123.094,48.489,-122.987,48.468,-123.013,48.501,-123.024,48.538,-123.114,48.613]},{"b":[-122.749,47.918,-122.367,48.384],"l":[-122.503,48.08,-122.524,48.025,-122.573,48.157,-122.598,48.2,-122.624,48.214,-122.697,48.229,-122.692,48.241,-122.542,48.294,-122.536,48.321,-122.572,48.36,-122.604,48.381,-122.629,48.384,-122.669,48.351,-122.725,48.281,-122.749,48.239,-122.742,48.225,-122.69,48.174,-122.657,48.156,-122.623,48.151,-122.606,48.129,-122.603,48.055,-122.591,48.03,-122.558,47.992,-122.492,47.981,-122.462,47.964,-122.438,47.931,-122.411,47.918,-122.383,47.923,-122.367,47.939,-122.367,47.985,-122.503,48.08]},{"b":[-124.65,49.51,-124.14,49.775],"l":[-124.494,49.667,-124.457,49.634,-124.362,49.588,-124.14,49.51,-124.154,49.531,-124.309,49.667,-124.421,49.728,-124.547,49.765,-124.623,49.775,-124.65,49.758,-124.631,49.736,-124.518,49.686,-124.494,49.667]},{"b":[-123.689,48.875,-123.372,49.095],"l":[-123.385,48.875,-123.372,48.886,-123.378,48.908,-123.482,48.955,-123.689,49.095,-123.646,49.039,-123.541,48.946,-123.385,48.875]},{"b":[-123.582,48.729,-123.407,48.926],"l":[-123.468,48.867,-123.555,48.922,-123.582,48.926,-123.518,48.75,-123.5,48.732,-123.477,48.729,-123.435,48.754,-123.407,48.756,-123.423,48.793,-123.488,48.846,-123.468,48.867]},{"b":[-125.026,50.021,-124.907,50.217],"l":[-124.978,50.03,-124.908,50.071,-124.907,50.084,-124.916,50.132,-124.938,50.166,-124.991,50.217,-124.987,50.196,-124.996,50.175,-125.026,50.134,-125.002,50.021,-124.978,50.03]},{"b":[-126.951,49.601,-126.626,49.872],"l":[-126.738,49.844,-126.826,49.872,-126.926,49.838,-126.897,49.783,-126.905,49.763,-126.94,49.75,-126.951,49.736,-126.939,49.718,-126.814,49.642,-126.743,49.613,-126.68,49.601,-126.641,49.606,-126.626,49.627,-126.628,49.675,-126.65,49.746,-126.698,49.808,-126.738,49.844]},{"b":[-125.358,50.044,-125.074,50.418],"l":[-125.126,50.32,-125.14,50.34,-125.196,50.39,-125.261,50.418,-125.301,50.414,-125.345,50.354,-125.358,50.312,-125.26,50.13,-125.195,50.044,-125.184,50.097,-125.113,50.163,-125.074,50.221,-125.091,50.268,-125.126,50.32]},{"b":[-118.59,32.819,-118.35,33.033],"l":[-118.507,32.96,-118.557,33.033,-118.59,33.011,-118.529,32.936,-118.473,32.839,-118.409,32.819,-118.35,32.828,-118.383,32.849,-118.507,32.96]},{"b":[-118.401,28.904,-118.243,29.188],"l":[-118.312,29.131,-118.312,29.183,-118.368,29.188,-118.401,29.163,-118.4,29.112,-118.285,28.904,-118.243,28.942,-118.247,29.043,-118.266,29.086,-118.312,29.131]},{"b":[-120.252,33.905,-119.984,34.027],"l":[-120.252,34.014,-120.167,33.918,-120.114,33.905,-120.044,33.919,-119.984,33.973,-119.994,33.985,-120.072,34.027,-120.252,34.014]},{"b":[-119.918,33.968,-119.549,34.08],"l":[-119.679,34.028,-119.882,34.08,-119.918,34.068,-119.892,34.032,-119.885,33.995,-119.81,33.968,-119.562,34.007,-119.549,34.028,-119.569,34.053,-119.679,34.028]},{"b":[-118.569,33.312,-118.297,33.477],"l":[-118.469,33.357,-118.446,33.317,-118.37,33.321,-118.297,33.312,-118.348,33.386,-118.392,33.415,-118.555,33.477,-118.569,33.464,-118.563,33.437,-118.559,33.432,-118.507,33.427,-118.492,33.413,-118.469,33.357]},{"b":[-71.403,41.467,-71.354,41.572],"l":[-71.393,41.467,-71.365,41.485,-71.354,41.542,-71.364,41.572,-71.384,41.571,-71.403,41.515,-71.393,41.467]},{"b":[-74.253,39.529,-74.107,39.746],"l":[-74.25,39.529,-74.133,39.681,-74.107,39.746,-74.253,39.558,-74.25,39.529]},{"b":[-74.236,40.519,-74.067,40.658],"l":[-74.188,40.615,-74.236,40.519,-74.188,40.523,-74.139,40.542,-74.08,40.586,-74.067,40.615,-74.069,40.649,-74.1,40.658,-74.188,40.615]},{"b":[-64.863,32.26,-64.668,32.387],"l":[-64.771,32.308,-64.863,32.274,-64.845,32.262,-64.82,32.26,-64.73,32.293,-64.668,32.382,-64.695,32.387,-64.771,32.308]},{"b":[-70.829,41.327,-70.51,41.457],"l":[-70.674,41.449,-70.76,41.374,-70.829,41.359,-70.785,41.327,-70.51,41.376,-70.525,41.415,-70.616,41.457,-70.674,41.449]},{"b":[-71.346,41.465,-71.232,41.654],"l":[-71.346,41.469,-71.291,41.465,-71.241,41.492,-71.232,41.654,-71.264,41.638,-71.28,41.62,-71.307,41.56,-71.318,41.506,-71.346,41.469]},{"b":[-70.233,41.249,-69.978,41.397],"l":[-70.063,41.328,-70.087,41.318,-70.233,41.286,-70.055,41.249,-69.978,41.266,-69.986,41.299,-70.041,41.397,-70.044,41.374,-70.063,41.328]},{"b":[-75.984,35.119,-75.782,35.19],"l":[-75.782,35.19,-75.865,35.174,-75.984,35.123,-75.964,35.119,-75.782,35.19]},{"b":[-75.717,35.836,-75.636,35.946],"l":[-75.651,35.836,-75.636,35.856,-75.637,35.881,-75.649,35.91,-75.717,35.946,-75.651,35.836]},{"b":[-75.379,37.872,-75.098,38.298],"l":[-75.226,38.072,-75.252,38.037,-75.379,37.872,-75.333,37.888,-75.226,38.04,-75.203,38.072,-75.136,38.181,-75.098,38.298,-75.137,38.24,-75.226,38.072]},{"b":[-75.69,35.213,-75.456,35.769],"l":[-75.504,35.769,-75.504,35.735,-75.481,35.572,-75.488,35.479,-75.536,35.279,-75.69,35.222,-75.678,35.213,-75.544,35.24,-75.509,35.28,-75.465,35.449,-75.456,35.564,-75.479,35.717,-75.504,35.769]},{"b":[-76.529,34.631,-76.207,34.939],"l":[-76.437,34.756,-76.529,34.631,-76.504,34.643,-76.358,34.804,-76.207,34.939,-76.256,34.915,-76.437,34.756]},{"b":[-96.801,28.148,-96.404,28.382],"l":[-96.519,28.333,-96.682,28.23,-96.756,28.202,-96.801,28.148,-96.764,28.153,-96.544,28.276,-96.413,28.338,-96.404,28.382,-96.419,28.376,-96.453,28.341,-96.519,28.333]},{"b":[-97.385,27.243,-97.061,27.822],"l":[-97.295,27.523,-97.376,27.328,-97.385,27.243,-97.354,27.3,-97.251,27.541,-97.061,27.822,-97.13,27.779,-97.295,27.523]},{"b":[-95.09,29.136,-94.768,29.341],"l":[-95.09,29.136,-95.04,29.146,-94.865,29.253,-94.768,29.339,-94.826,29.341,-94.872,29.29,-95.09,29.136]},{"b":[-97.407,26.113,-97.171,27.196],"l":[-97.386,27.196,-97.407,27.1,-97.402,26.821,-97.267,26.33,-97.185,26.113,-97.171,26.159,-97.202,26.3,-97.351,26.801,-97.386,27.196]},{"b":[-85.117,29.607,-84.737,29.732],"l":[-85.049,29.638,-85.117,29.633,-85.008,29.607,-84.908,29.643,-84.737,29.732,-84.812,29.718,-84.877,29.679,-85.001,29.627,-85.049,29.638]},{"b":[-81.485,30.728,-81.419,30.971],"l":[-81.463,30.728,-81.419,30.971,-81.451,30.947,-81.485,30.898,-81.483,30.814,-81.463,30.728]},{"b":[-89.342,30.038,-89.185,30.169],"l":[-89.185,30.169,-89.276,30.111,-89.288,30.094,-89.31,30.079,-89.342,30.063,-89.269,30.061,-89.22,30.038,-89.224,30.084,-89.211,30.126,-89.185,30.169]},{"b":[-92.007,29.486,-91.754,29.644],"l":[-91.796,29.597,-91.875,29.641,-91.925,29.644,-92.007,29.61,-91.996,29.573,-91.831,29.486,-91.794,29.501,-91.762,29.539,-91.754,29.567,-91.768,29.585,-91.796,29.597]},{"b":[-88.867,29.776,-88.813,30.057],"l":[-88.828,29.928,-88.856,29.776,-88.827,29.808,-88.813,29.933,-88.826,30,-88.867,30.057,-88.828,29.928]},{"b":[-78,23.739,-77.519,24.288],"l":[-77.532,23.939,-77.537,23.962,-77.532,23.988,-77.562,24.137,-77.615,24.216,-77.683,24.118,-77.755,24.163,-77.656,24.227,-77.658,24.249,-77.701,24.288,-77.757,24.27,-77.85,24.258,-77.884,24.242,-77.95,24.253,-78,24.22,-77.914,24.091,-77.852,24.04,-77.806,23.884,-77.776,23.862,-77.771,23.753,-77.574,23.739,-77.519,23.869,-77.521,23.911,-77.532,23.939]},{"b":[-77.919,21.921,-77.634,22.092],"l":[-77.774,22.083,-77.855,22.092,-77.919,22.088,-77.9,22.037,-77.823,21.988,-77.784,21.97,-77.755,21.966,-77.71,21.921,-77.669,21.952,-77.646,21.996,-77.634,22.054,-77.774,22.083]},{"b":[-78.274,22.269,-78.027,22.438],"l":[-78.027,22.285,-78.062,22.306,-78.094,22.387,-78.151,22.431,-78.201,22.438,-78.274,22.424,-78.27,22.402,-78.226,22.38,-78.18,22.322,-78.102,22.306,-78.048,22.269,-78.027,22.285]},{"b":[-78.696,22.445,-78.284,22.552],"l":[-78.284,22.455,-78.351,22.539,-78.4,22.547,-78.445,22.544,-78.493,22.531,-78.63,22.552,-78.696,22.534,-78.674,22.509,-78.629,22.488,-78.548,22.464,-78.425,22.46,-78.39,22.445,-78.343,22.445,-78.284,22.455]},{"b":[-77.944,25.895,-77.038,26.94],"l":[-77.511,26.846,-77.33,26.618,-77.239,26.561,-77.206,26.489,-77.23,26.425,-77.221,26.362,-77.248,26.289,-77.247,26.156,-77.294,26.096,-77.403,26.025,-77.333,25.996,-77.246,25.895,-77.226,25.904,-77.191,25.955,-77.167,26.24,-77.038,26.333,-77.066,26.53,-77.162,26.597,-77.257,26.639,-77.269,26.663,-77.266,26.689,-77.296,26.712,-77.369,26.748,-77.449,26.836,-77.534,26.903,-77.672,26.914,-77.788,26.936,-77.863,26.94,-77.944,26.904,-77.796,26.901,-77.511,26.846]},{"b":[-74.314,22.665,-74.035,22.84],"l":[-74.242,22.715,-74.099,22.665,-74.035,22.706,-74.058,22.723,-74.175,22.76,-74.221,22.812,-74.307,22.84,-74.314,22.804,-74.303,22.764,-74.275,22.712,-74.242,22.715]},{"b":[-74.551,23.96,-74.429,24.127],"l":[-74.429,24.068,-74.45,24.125,-74.472,24.127,-74.527,24.105,-74.551,23.969,-74.509,23.96,-74.429,24.068]},{"b":[-73.687,20.937,-73.012,21.313],"l":[-73.661,20.937,-73.401,20.944,-73.165,20.979,-73.059,21.119,-73.027,21.192,-73.012,21.3,-73.058,21.313,-73.137,21.205,-73.235,21.154,-73.302,21.156,-73.425,21.202,-73.523,21.191,-73.585,21.126,-73.68,21.103,-73.67,21.082,-73.668,21.062,-73.687,21.009,-73.681,20.976,-73.661,20.937]},{"b":[-74.277,22.184,-73.837,22.731],"l":[-73.915,22.568,-73.906,22.527,-73.936,22.478,-73.995,22.449,-74.01,22.428,-74.052,22.401,-74.127,22.323,-74.261,22.236,-74.277,22.184,-74.207,22.214,-74.093,22.306,-73.975,22.361,-73.837,22.538,-73.877,22.681,-73.85,22.731,-73.954,22.716,-73.975,22.682,-73.976,22.635,-73.915,22.568]},{"b":[-76.781,24.649,-76.115,25.565],"l":[-76.344,25.332,-76.484,25.375,-76.649,25.487,-76.711,25.565,-76.727,25.552,-76.749,25.481,-76.781,25.427,-76.693,25.443,-76.621,25.432,-76.5,25.342,-76.369,25.313,-76.284,25.222,-76.16,25.119,-76.153,25.026,-76.204,24.936,-76.214,24.822,-76.32,24.818,-76.3,24.796,-76.241,24.754,-76.205,24.682,-76.17,24.649,-76.175,24.76,-76.141,24.886,-76.115,25.095,-76.127,25.141,-76.192,25.191,-76.344,25.332]},{"b":[-76.037,23.444,-75.665,23.671],"l":[-75.949,23.647,-76.01,23.671,-76.037,23.603,-75.956,23.592,-75.781,23.471,-75.706,23.444,-75.665,23.45,-75.754,23.49,-75.808,23.543,-75.949,23.647]},{"b":[-75.316,22.869,-74.84,23.668],"l":[-75.131,23.268,-75.141,23.205,-75.204,23.193,-75.223,23.165,-75.132,23.117,-74.973,23.069,-74.847,22.869,-74.84,22.894,-74.846,23,-74.937,23.088,-75.064,23.15,-75.109,23.333,-75.175,23.439,-75.217,23.547,-75.316,23.668,-75.31,23.59,-75.288,23.568,-75.241,23.475,-75.158,23.336,-75.131,23.268]},{"b":[-75.744,24.139,-75.302,24.698],"l":[-75.526,24.45,-75.654,24.681,-75.71,24.698,-75.727,24.689,-75.744,24.655,-75.661,24.59,-75.639,24.529,-75.593,24.491,-75.494,24.33,-75.409,24.266,-75.412,24.221,-75.481,24.174,-75.503,24.139,-75.468,24.14,-75.369,24.159,-75.302,24.149,-75.308,24.2,-75.518,24.427,-75.526,24.45]},{"b":[-78.042,22.125,-77.842,22.302],"l":[-77.879,22.128,-77.842,22.149,-77.889,22.201,-77.894,22.215,-77.97,22.241,-77.986,22.302,-77.999,22.299,-78.007,22.248,-78.042,22.201,-78.012,22.166,-77.912,22.125,-77.879,22.128]},{"b":[-82.201,26.428,-82.037,26.548],"l":[-82.201,26.548,-82.184,26.481,-82.145,26.447,-82.073,26.428,-82.037,26.454,-82.116,26.461,-82.139,26.477,-82.201,26.548]},{"b":[-78.435,24.287,-77.735,25.202],"l":[-78.367,24.544,-78.258,24.483,-78.192,24.466,-78.146,24.493,-78.136,24.412,-78.076,24.365,-78.045,24.287,-77.983,24.335,-77.881,24.369,-77.853,24.403,-77.745,24.463,-77.735,24.496,-77.746,24.586,-77.744,24.707,-77.84,24.794,-77.919,24.943,-77.973,25.005,-77.975,25.085,-78.033,25.143,-78.163,25.202,-78.211,25.191,-78.159,25.022,-78.184,24.917,-78.299,24.754,-78.274,24.692,-78.26,24.687,-78.243,24.654,-78.319,24.59,-78.339,24.642,-78.435,24.628,-78.367,24.544]},{"b":[-81.631,24.59,-81.532,24.689],"l":[-81.562,24.689,-81.579,24.629,-81.631,24.59,-81.567,24.6,-81.532,24.614,-81.532,24.642,-81.562,24.689]},{"b":[-80.437,27.205,-80.171,27.851],"l":[-80.262,27.376,-80.171,27.205,-80.187,27.278,-80.356,27.679,-80.396,27.795,-80.437,27.851,-80.376,27.643,-80.262,27.376]},{"b":[-83.184,21.441,-82.562,21.943],"l":[-82.682,21.821,-82.715,21.89,-82.756,21.91,-82.991,21.943,-83.078,21.833,-83.083,21.791,-82.974,21.592,-83.007,21.566,-83.055,21.549,-83.113,21.574,-83.18,21.623,-83.184,21.593,-83.142,21.532,-83.067,21.469,-82.96,21.441,-82.853,21.444,-82.655,21.519,-82.562,21.572,-82.568,21.622,-82.629,21.767,-82.682,21.821]},{"b":[-82.169,26.494,-82.084,26.701],"l":[-82.085,26.494,-82.084,26.552,-82.121,26.666,-82.169,26.701,-82.136,26.592,-82.085,26.494]},{"b":[-79.628,22.638,-79.348,22.807],"l":[-79.382,22.681,-79.579,22.807,-79.628,22.805,-79.598,22.788,-79.523,22.711,-79.348,22.638,-79.35,22.664,-79.382,22.681]},{"b":[-78.986,26.501,-77.922,26.798],"l":[-78.234,26.637,-77.926,26.663,-77.922,26.691,-77.944,26.744,-78.089,26.714,-78.268,26.723,-78.307,26.702,-78.372,26.698,-78.493,26.729,-78.597,26.798,-78.633,26.726,-78.621,26.705,-78.633,26.659,-78.712,26.599,-78.798,26.582,-78.936,26.673,-78.986,26.69,-78.799,26.528,-78.744,26.501,-78.671,26.507,-78.516,26.559,-78.234,26.637]},{"b":[-80.581,24.954,-80.257,25.348],"l":[-80.257,25.348,-80.28,25.341,-80.351,25.297,-80.355,25.234,-80.404,25.179,-80.456,25.149,-80.481,25.102,-80.559,25.001,-80.581,24.954,-80.382,25.142,-80.257,25.348]},{"b":[-87.998,17.906,-87.849,18.155],"l":[-87.849,18.14,-87.859,18.154,-87.898,18.155,-87.953,18.001,-87.959,17.964,-87.998,17.906,-87.951,17.925,-87.849,18.14]},{"b":[-87.935,17.283,-87.789,17.546],"l":[-87.903,17.426,-87.935,17.323,-87.93,17.283,-87.853,17.423,-87.798,17.48,-87.789,17.524,-87.826,17.546,-87.833,17.501,-87.859,17.463,-87.903,17.426]},{"b":[-91.816,18.654,-91.537,18.778],"l":[-91.816,18.676,-91.796,18.654,-91.684,18.677,-91.654,18.711,-91.537,18.76,-91.55,18.774,-91.589,18.778,-91.816,18.676]},{"b":[-82.322,9.334,-82.233,9.432],"l":[-82.244,9.334,-82.233,9.381,-82.259,9.43,-82.276,9.432,-82.322,9.418,-82.244,9.334]},{"b":[-81.419,19.272,-81.107,19.385],"l":[-81.285,19.363,-81.296,19.341,-81.337,19.329,-81.37,19.349,-81.391,19.385,-81.419,19.375,-81.405,19.278,-81.304,19.272,-81.277,19.277,-81.225,19.304,-81.107,19.305,-81.13,19.347,-81.285,19.363]},{"b":[-87.019,20.272,-86.755,20.579],"l":[-86.928,20.552,-86.978,20.49,-87.019,20.382,-86.991,20.272,-86.94,20.303,-86.809,20.468,-86.755,20.552,-86.763,20.579,-86.829,20.559,-86.928,20.552]},{"b":[-62.84,17.224,-62.631,17.403],"l":[-62.714,17.353,-62.795,17.403,-62.827,17.386,-62.839,17.365,-62.84,17.347,-62.839,17.339,-62.776,17.303,-62.702,17.286,-62.656,17.224,-62.631,17.24,-62.641,17.262,-62.676,17.291,-62.714,17.353]},{"b":[-61.073,13.718,-60.887,14.093],"l":[-60.908,14.093,-60.945,14.073,-60.997,14.011,-61.064,13.916,-61.073,13.866,-61.061,13.783,-60.951,13.718,-60.895,13.822,-60.887,14.011,-60.908,14.093]},{"b":[-61.22,14.426,-60.826,14.875],"l":[-60.934,14.686,-60.919,14.735,-60.927,14.755,-60.953,14.756,-61.027,14.826,-61.127,14.875,-61.181,14.872,-61.213,14.849,-61.22,14.804,-61.141,14.652,-61.104,14.621,-61.011,14.602,-61.09,14.53,-61.089,14.51,-61.064,14.467,-60.899,14.474,-60.862,14.426,-60.837,14.437,-60.826,14.494,-60.87,14.614,-60.889,14.645,-60.934,14.686]},{"b":[-61.906,10.065,-60.918,10.84],"l":[-60.918,10.84,-61.079,10.832,-61.174,10.803,-61.37,10.797,-61.465,10.764,-61.592,10.748,-61.651,10.718,-61.635,10.699,-61.541,10.664,-61.499,10.639,-61.478,10.603,-61.465,10.539,-61.499,10.269,-61.529,10.253,-61.633,10.243,-61.661,10.192,-61.906,10.069,-61.772,10.085,-61.597,10.065,-61.174,10.078,-61.012,10.134,-61.004,10.168,-61,10.261,-60.968,10.323,-61.016,10.386,-61.037,10.482,-61.019,10.558,-61.034,10.67,-60.997,10.716,-60.918,10.84]},{"b":[-68.371,12.032,-68.206,12.302],"l":[-68.369,12.302,-68.371,12.258,-68.348,12.228,-68.307,12.207,-68.287,12.172,-68.282,12.082,-68.254,12.032,-68.206,12.145,-68.219,12.231,-68.369,12.302]},{"b":[-60.811,11.169,-60.525,11.325],"l":[-60.709,11.277,-60.804,11.208,-60.811,11.169,-60.756,11.179,-60.546,11.264,-60.525,11.325,-60.563,11.324,-60.709,11.277]},{"b":[-62.223,16.681,-62.148,16.81],"l":[-62.176,16.81,-62.191,16.804,-62.223,16.752,-62.222,16.7,-62.154,16.681,-62.148,16.74,-62.176,16.81]},{"b":[-59.647,13.062,-59.428,13.318],"l":[-59.592,13.318,-59.647,13.303,-59.643,13.15,-59.611,13.102,-59.522,13.062,-59.493,13.082,-59.428,13.153,-59.488,13.197,-59.592,13.318]},{"b":[-64.402,10.881,-63.817,11.167],"l":[-64.007,11.068,-64.028,11.002,-64.113,11.006,-64.185,11.043,-64.214,11.086,-64.25,11.08,-64.349,11.052,-64.402,10.982,-64.362,10.962,-64.219,10.942,-64.161,10.959,-64.101,10.901,-64.055,10.884,-63.994,10.881,-63.918,10.888,-63.827,10.976,-63.817,11,-63.849,11.131,-63.893,11.167,-64.007,11.068]},{"b":[-63.16,18.171,-62.98,18.27],"l":[-63.16,18.171,-63.001,18.222,-62.98,18.265,-63.026,18.27,-63.153,18.2,-63.16,18.171]},{"b":[-61.782,12.008,-61.607,12.237],"l":[-61.66,12.237,-61.715,12.185,-61.75,12.109,-61.756,12.046,-61.782,12.008,-61.715,12.013,-61.627,12.054,-61.607,12.223,-61.66,12.237]},{"b":[-61.869,17.549,-61.747,17.714],"l":[-61.762,17.549,-61.747,17.575,-61.75,17.661,-61.777,17.69,-61.82,17.697,-61.852,17.714,-61.866,17.704,-61.869,17.685,-61.844,17.596,-61.762,17.549]},{"b":[-61.318,15.886,-61.203,16.006],"l":[-61.25,16.006,-61.275,15.996,-61.318,15.955,-61.311,15.895,-61.286,15.886,-61.23,15.89,-61.203,15.921,-61.212,15.96,-61.25,16.006]},{"b":[-61.887,16.997,-61.686,17.169],"l":[-61.86,17.013,-61.748,16.997,-61.716,17.037,-61.695,17.049,-61.686,17.07,-61.686,17.098,-61.708,17.105,-61.739,17.138,-61.817,17.169,-61.887,17.098,-61.882,17.063,-61.86,17.013]},{"b":[-61.481,15.227,-61.251,15.633],"l":[-61.251,15.373,-61.277,15.527,-61.32,15.585,-61.458,15.633,-61.47,15.603,-61.481,15.525,-61.416,15.4,-61.375,15.227,-61.282,15.249,-61.251,15.373]},{"b":[-61.54,16.219,-61.173,16.507],"l":[-61.173,16.256,-61.355,16.363,-61.396,16.413,-61.406,16.468,-61.471,16.507,-61.511,16.478,-61.529,16.434,-61.501,16.36,-61.54,16.3,-61.522,16.228,-61.445,16.219,-61.327,16.23,-61.173,16.256]},{"b":[-61.794,15.962,-61.552,16.355],"l":[-61.642,16.326,-61.748,16.355,-61.767,16.34,-61.794,16.301,-61.759,16.062,-61.71,15.976,-61.67,15.962,-61.59,16.007,-61.564,16.048,-61.575,16.227,-61.552,16.271,-61.597,16.292,-61.642,16.326]},{"b":[-61.277,13.142,-61.124,13.359],"l":[-61.135,13.203,-61.124,13.294,-61.139,13.359,-61.182,13.356,-61.224,13.331,-61.268,13.288,-61.277,13.21,-61.204,13.142,-61.175,13.158,-61.135,13.203]},{"b":[-73.162,22.291,-72.747,22.455],"l":[-72.981,22.369,-72.889,22.36,-72.784,22.291,-72.747,22.327,-72.763,22.344,-72.831,22.386,-72.945,22.416,-72.979,22.415,-73.041,22.429,-73.127,22.455,-73.162,22.381,-73.11,22.368,-72.981,22.369]},{"b":[-69.159,12.045,-68.751,12.38],"l":[-68.827,12.159,-69.013,12.231,-69.077,12.342,-69.118,12.373,-69.159,12.38,-69.154,12.298,-68.995,12.142,-68.803,12.045,-68.751,12.06,-68.827,12.159]},{"b":[-71.848,21.752,-71.637,21.852],"l":[-71.722,21.79,-71.665,21.752,-71.661,21.765,-71.637,21.788,-71.668,21.833,-71.806,21.852,-71.848,21.843,-71.83,21.791,-71.722,21.79]},{"b":[-73.063,21.458,-72.916,21.562],"l":[-72.916,21.507,-72.995,21.562,-73.063,21.515,-73.049,21.458,-72.916,21.507]},{"b":[-72.019,21.83,-71.88,21.952],"l":[-71.955,21.864,-71.897,21.83,-71.88,21.84,-71.9,21.863,-71.932,21.952,-72.011,21.95,-72.019,21.918,-71.985,21.893,-71.964,21.892,-71.955,21.864]},{"b":[-72.342,21.755,-72.144,21.851],"l":[-72.191,21.77,-72.182,21.78,-72.144,21.793,-72.15,21.804,-72.219,21.796,-72.333,21.851,-72.342,21.795,-72.335,21.758,-72.301,21.755,-72.191,21.77]},{"b":[-161.131,58.561,-160.715,58.795],"l":[-161.085,58.671,-161.131,58.668,-161.07,58.569,-160.992,58.561,-160.919,58.577,-160.715,58.795,-160.769,58.789,-160.986,58.736,-161.085,58.671]},{"b":[-153.375,57,-152.908,57.185],"l":[-153.157,57.094,-153.134,57.093,-153.007,57.125,-152.933,57.129,-152.908,57.14,-152.908,57.152,-152.935,57.167,-153.285,57.185,-153.354,57.132,-153.375,57.052,-153.295,57,-153.235,57.029,-153.157,57.094]},{"b":[-152.069,60.358,-151.887,60.504],"l":[-151.96,60.504,-152.004,60.407,-152.069,60.358,-152.021,60.362,-151.987,60.374,-151.887,60.473,-151.899,60.49,-151.96,60.504]},{"b":[-152.637,58.479,-152.357,58.619],"l":[-152.516,58.479,-152.486,58.485,-152.393,58.541,-152.362,58.571,-152.357,58.595,-152.368,58.611,-152.396,58.619,-152.463,58.619,-152.605,58.566,-152.637,58.542,-152.589,58.509,-152.516,58.479]},{"b":[-153.52,57.822,-153.241,57.971],"l":[-153.241,57.85,-153.29,57.898,-153.347,57.933,-153.481,57.971,-153.52,57.956,-153.517,57.942,-153.465,57.909,-153.351,57.862,-153.295,57.829,-153.269,57.822,-153.241,57.85]},{"b":[-155.737,55.79,-155.564,55.924],"l":[-155.737,55.83,-155.723,55.802,-155.681,55.792,-155.605,55.79,-155.566,55.821,-155.564,55.887,-155.573,55.921,-155.594,55.924,-155.621,55.913,-155.737,55.83]},{"b":[-154.777,56.412,-154.445,56.601],"l":[-154.518,56.601,-154.624,56.561,-154.729,56.502,-154.761,56.471,-154.777,56.44,-154.774,56.42,-154.751,56.412,-154.683,56.436,-154.511,56.521,-154.445,56.573,-154.463,56.598,-154.518,56.601]},{"b":[-154.332,56.513,-154.102,56.609],"l":[-154.217,56.609,-154.322,56.571,-154.332,56.539,-154.258,56.513,-154.209,56.515,-154.15,56.53,-154.116,56.544,-154.107,56.558,-154.102,56.582,-154.11,56.603,-154.217,56.609]},{"b":[-146.703,60.268,-146.102,60.481],"l":[-146.372,60.422,-146.394,60.45,-146.56,60.481,-146.606,60.468,-146.67,60.433,-146.703,60.409,-146.703,60.396,-146.683,60.361,-146.65,60.336,-146.618,60.274,-146.595,60.268,-146.419,60.325,-146.202,60.368,-146.128,60.393,-146.102,60.411,-146.124,60.424,-146.18,60.429,-146.372,60.422]},{"b":[-145.284,60.313,-145.102,60.401],"l":[-145.284,60.337,-145.238,60.321,-145.15,60.313,-145.119,60.337,-145.102,60.388,-145.128,60.401,-145.284,60.337]},{"b":[-144.614,59.813,-144.236,60.015],"l":[-144.542,59.878,-144.614,59.813,-144.566,59.818,-144.403,59.921,-144.249,59.982,-144.236,60.015,-144.354,59.996,-144.445,59.951,-144.542,59.878]},{"b":[-147.872,59.799,-146.958,60.363],"l":[-147.02,60.332,-147.12,60.363,-147.181,60.358,-147.205,60.311,-147.337,60.185,-147.466,60.097,-147.607,60.037,-147.734,59.954,-147.768,59.944,-147.814,59.902,-147.872,59.828,-147.846,59.799,-147.736,59.813,-147.669,59.832,-147.645,59.854,-147.602,59.866,-147.54,59.868,-147.499,59.89,-147.479,59.934,-147.448,59.96,-147.404,59.97,-147.377,59.991,-147.346,60.052,-147.318,60.075,-146.987,60.254,-146.958,60.289,-147.02,60.332]},{"b":[-148.272,60.035,-147.914,60.152],"l":[-147.914,60.092,-148.08,60.152,-148.231,60.114,-148.272,60.053,-148.074,60.035,-148.022,60.065,-147.914,60.092]},{"b":[-147.891,60.178,-147.658,60.491],"l":[-147.842,60.351,-147.855,60.321,-147.891,60.299,-147.871,60.23,-147.805,60.231,-147.822,60.203,-147.816,60.185,-147.788,60.178,-147.76,60.19,-147.732,60.222,-147.712,60.273,-147.66,60.352,-147.69,60.399,-147.659,60.424,-147.658,60.45,-147.689,60.491,-147.703,60.487,-147.737,60.447,-147.76,60.439,-147.774,60.445,-147.779,60.466,-147.795,60.46,-147.838,60.371,-147.842,60.351]},{"b":[-148.124,60.818,-147.931,60.924],"l":[-147.931,60.826,-147.943,60.875,-147.964,60.9,-148.038,60.924,-148.102,60.916,-148.1,60.895,-148.124,60.844,-148.115,60.831,-148.057,60.818,-147.931,60.826]},{"b":[-133.454,54.684,-132.617,55.326],"l":[-132.677,54.726,-132.647,54.756,-132.634,54.84,-132.617,54.892,-132.649,54.907,-132.701,54.919,-132.772,54.926,-132.813,54.89,-132.838,54.881,-132.862,54.894,-132.946,55.003,-132.982,55.033,-132.996,55.111,-133.067,55.166,-133.097,55.214,-133.297,55.326,-133.429,55.304,-133.454,55.26,-133.418,55.211,-133.325,55.185,-133.251,55.175,-133.205,55.084,-133.123,54.97,-133.081,54.949,-133.075,54.921,-133.009,54.855,-132.89,54.763,-132.807,54.709,-132.706,54.684,-132.677,54.726]},{"b":[-134.391,56.077,-133.738,56.932],"l":[-133.855,56.582,-133.809,56.611,-133.767,56.6,-133.738,56.65,-133.778,56.729,-133.831,56.781,-133.925,56.776,-133.99,56.845,-134.001,56.869,-134.052,56.898,-134.143,56.932,-134.274,56.918,-134.374,56.839,-134.391,56.749,-134.384,56.724,-134.278,56.617,-134.29,56.58,-134.15,56.513,-134.084,56.456,-134.195,56.414,-134.245,56.203,-134.19,56.077,-134.122,56.077,-134.067,56.133,-134.024,56.119,-133.994,56.101,-133.971,56.108,-133.95,56.128,-133.939,56.194,-133.885,56.292,-133.87,56.389,-133.884,56.485,-133.855,56.582]},{"b":[-133.737,55.269,-133.282,55.559],"l":[-133.634,55.539,-133.737,55.497,-133.635,55.413,-133.65,55.269,-133.547,55.317,-133.493,55.362,-133.463,55.377,-133.429,55.418,-133.426,55.431,-133.282,55.498,-133.283,55.516,-133.305,55.544,-133.346,55.559,-133.455,55.522,-133.567,55.527,-133.634,55.539]},{"b":[-131.595,54.888,-131.232,55.264],"l":[-131.431,54.996,-131.456,54.931,-131.446,54.909,-131.406,54.894,-131.33,54.888,-131.232,54.904,-131.237,54.95,-131.34,55.08,-131.405,55.213,-131.513,55.263,-131.565,55.264,-131.578,55.249,-131.578,55.201,-131.556,55.137,-131.595,55.091,-131.592,55.026,-131.54,55.048,-131.482,55.035,-131.431,54.996]},{"b":[-132.948,56.511,-132.568,56.795],"l":[-132.907,56.637,-132.936,56.607,-132.948,56.567,-132.931,56.524,-132.885,56.512,-132.758,56.511,-132.747,56.526,-132.714,56.543,-132.634,56.553,-132.568,56.576,-132.599,56.636,-132.656,56.685,-132.843,56.795,-132.871,56.696,-132.907,56.637]},{"b":[-132.699,55.929,-132.067,56.499],"l":[-132.38,56.499,-132.506,56.335,-132.539,56.324,-132.599,56.242,-132.675,56.224,-132.699,56.198,-132.691,56.13,-132.66,56.078,-132.603,56.066,-132.451,56.056,-132.406,56.029,-132.421,55.98,-132.407,55.958,-132.369,55.94,-132.287,55.929,-132.21,55.953,-132.173,55.953,-132.133,55.943,-132.112,56.109,-132.067,56.244,-132.206,56.388,-132.317,56.488,-132.38,56.499]},{"b":[-133.038,56.244,-132.629,56.454],"l":[-132.935,56.442,-133.017,56.392,-133.038,56.365,-133.035,56.341,-132.891,56.259,-132.831,56.244,-132.78,56.247,-132.669,56.287,-132.647,56.313,-132.658,56.339,-132.653,56.364,-132.632,56.388,-132.629,56.412,-132.643,56.435,-132.706,56.448,-132.902,56.454,-132.935,56.442]},{"b":[-134.662,58.204,-134.313,58.333],"l":[-134.313,58.229,-134.399,58.287,-134.52,58.333,-134.648,58.312,-134.662,58.291,-134.594,58.243,-134.456,58.207,-134.32,58.204,-134.313,58.229]},{"b":[-129.268,52.464,-128.937,52.811],"l":[-129.215,52.804,-129.246,52.811,-129.264,52.801,-129.268,52.772,-129.25,52.722,-129.151,52.605,-129.102,52.574,-128.969,52.464,-128.937,52.51,-128.94,52.601,-128.994,52.662,-129.186,52.791,-129.215,52.804]},{"b":[-130.959,54.478,-130.753,54.632],"l":[-130.922,54.615,-130.953,54.542,-130.959,54.499,-130.95,54.478,-130.927,54.479,-130.805,54.544,-130.763,54.577,-130.753,54.6,-130.758,54.614,-130.777,54.619,-130.907,54.632,-130.922,54.615]},{"b":[-131.117,51.96,-131.011,52.151],"l":[-131.107,52.137,-131.117,52.101,-131.103,52.014,-131.081,51.98,-131.047,51.96,-131.029,51.962,-131.011,52.095,-131.098,52.151,-131.107,52.137]},{"b":[-130.707,53.844,-130.236,54.089],"l":[-130.316,54.047,-130.397,54.086,-130.448,54.089,-130.495,54.074,-130.647,53.991,-130.696,53.951,-130.707,53.921,-130.703,53.892,-130.683,53.863,-130.664,53.848,-130.644,53.845,-130.638,53.86,-130.646,53.894,-130.642,53.921,-130.625,53.941,-130.59,53.94,-130.537,53.918,-130.47,53.862,-130.407,53.856,-130.384,53.844,-130.338,53.866,-130.267,53.923,-130.236,53.959,-130.298,54.036,-130.316,54.047]},{"b":[-130.518,53.165,-129.755,53.631],"l":[-130.035,53.481,-130.195,53.55,-130.395,53.62,-130.452,53.631,-130.518,53.544,-130.411,53.491,-130.306,53.407,-130.151,53.346,-129.934,53.177,-129.869,53.165,-129.849,53.168,-129.769,53.217,-129.755,53.245,-129.945,53.436,-130.035,53.481]},{"b":[-128.149,51.457,-127.916,51.708],"l":[-127.933,51.605,-127.987,51.674,-127.999,51.704,-128.032,51.708,-128.123,51.667,-128.142,51.647,-128.149,51.627,-128.092,51.511,-128.045,51.474,-127.981,51.457,-127.941,51.457,-127.925,51.474,-127.916,51.506,-127.916,51.585,-127.933,51.605]},{"b":[-128.445,52.388,-128.247,52.784],"l":[-128.298,52.548,-128.248,52.741,-128.247,52.784,-128.365,52.782,-128.44,52.696,-128.436,52.56,-128.426,52.503,-128.412,52.473,-128.42,52.441,-128.445,52.388,-128.369,52.401,-128.344,52.426,-128.324,52.459,-128.298,52.548]},{"b":[-129.515,52.984,-129.314,53.188],"l":[-129.471,53.183,-129.501,53.188,-129.515,53.179,-129.5,53.129,-129.478,53.098,-129.41,53.024,-129.37,52.998,-129.329,52.984,-129.314,52.992,-129.344,53.053,-129.451,53.175,-129.471,53.183]},{"b":[-129.331,53.111,-129.168,53.33],"l":[-129.168,53.118,-129.177,53.259,-129.195,53.293,-129.238,53.33,-129.251,53.317,-129.253,53.285,-129.314,53.212,-129.331,53.174,-129.324,53.142,-129.306,53.121,-129.277,53.111,-129.173,53.111,-129.168,53.118]},{"b":[-139.291,69.539,-138.879,69.65],"l":[-139.291,69.598,-139.257,69.579,-139.126,69.539,-139.043,69.577,-138.879,69.59,-138.932,69.617,-139.073,69.648,-139.14,69.65,-139.291,69.598]},{"b":[-61.069,8.847,-60.849,9.07],"l":[-60.998,8.867,-60.916,8.899,-60.861,8.95,-60.849,8.996,-60.9,9.032,-60.895,9.053,-60.916,9.07,-60.945,9.055,-61.05,8.974,-61.069,8.947,-61.06,8.847,-60.998,8.867]},{"b":[-80.273,-3.012,-79.909,-2.669],"l":[-80.224,-2.753,-80.25,-2.812,-80.272,-2.952,-80.273,-2.996,-80.246,-3.008,-80.151,-3.012,-80.132,-2.973,-80.093,-2.846,-80.071,-2.834,-80.013,-2.82,-79.909,-2.726,-79.997,-2.674,-80.081,-2.669,-80.146,-2.696,-80.224,-2.753]},{"b":[-51.938,-1.453,-51.161,-0.541],"l":[-51.254,-0.541,-51.424,-0.566,-51.546,-0.65,-51.678,-0.855,-51.68,-1.086,-51.802,-1.203,-51.938,-1.453,-51.833,-1.434,-51.638,-1.342,-51.465,-1.211,-51.31,-1.024,-51.276,-1.022,-51.161,-0.667,-51.254,-0.541]},{"b":[-79.128,8.21,-79.065,8.296],"l":[-79.128,8.252,-79.11,8.21,-79.065,8.254,-79.085,8.296,-79.096,8.295,-79.128,8.252]},{"b":[-78.992,1.245,-78.9,1.36],"l":[-78.992,1.293,-78.966,1.245,-78.909,1.253,-78.9,1.36,-78.923,1.349,-78.992,1.293]},{"b":[-78.21,2.52,-78.114,2.646],"l":[-78.178,2.646,-78.21,2.609,-78.192,2.559,-78.141,2.52,-78.114,2.542,-78.119,2.604,-78.138,2.634,-78.178,2.646]},{"b":[-60.941,9.106,-60.736,9.218],"l":[-60.79,9.177,-60.736,9.203,-60.759,9.216,-60.782,9.218,-60.821,9.208,-60.845,9.192,-60.907,9.179,-60.939,9.132,-60.941,9.106,-60.821,9.138,-60.79,9.177]},{"b":[-44.598,-3.038,-44.481,-2.718],"l":[-44.481,-2.718,-44.502,-2.726,-44.569,-2.785,-44.582,-2.846,-44.565,-2.924,-44.598,-3.038,-44.499,-2.94,-44.483,-2.812,-44.487,-2.79,-44.481,-2.718]},{"b":[-38.787,-13.119,-38.6,-12.88],"l":[-38.668,-12.88,-38.685,-12.975,-38.787,-13.055,-38.783,-13.119,-38.744,-13.097,-38.601,-12.993,-38.6,-12.972,-38.615,-12.924,-38.668,-12.88]},{"b":[-39.022,-13.532,-38.904,-13.398],"l":[-39.007,-13.416,-39.022,-13.446,-38.993,-13.484,-38.978,-13.524,-38.938,-13.532,-38.904,-13.473,-38.907,-13.401,-38.98,-13.398,-39.007,-13.416]},{"b":[-45.021,-1.391,-44.883,-1.267],"l":[-44.979,-1.267,-44.996,-1.348,-45.011,-1.345,-45.021,-1.372,-44.968,-1.391,-44.947,-1.366,-44.883,-1.318,-44.888,-1.277,-44.979,-1.267]},{"b":[-50.345,-0.029,-49.697,0.305],"l":[-50.128,0.227,-50.273,0.232,-50.345,0.134,-50.339,0.043,-50.286,0.029,-50.113,0.033,-50.002,-0.029,-49.917,-0.023,-49.839,0.007,-49.697,0.216,-49.738,0.268,-49.879,0.305,-50.128,0.227]},{"b":[-50.509,1.893,-50.292,2.161],"l":[-50.491,2.129,-50.509,2.03,-50.456,1.91,-50.399,1.893,-50.299,1.939,-50.292,1.98,-50.342,2.142,-50.363,2.154,-50.419,2.161,-50.491,2.129]},{"b":[-51.038,-0.327,-50.651,-0.041],"l":[-50.653,-0.132,-50.651,-0.106,-50.667,-0.058,-50.765,-0.041,-50.842,-0.05,-50.995,-0.105,-51.026,-0.172,-51.022,-0.188,-51.038,-0.226,-51.019,-0.263,-50.926,-0.327,-50.653,-0.132]},{"b":[-49.83,-0.144,-49.372,0.084],"l":[-49.709,-0.144,-49.444,-0.112,-49.381,-0.055,-49.372,0.001,-49.4,0.057,-49.503,0.084,-49.602,0.063,-49.712,0.015,-49.803,-0.052,-49.83,-0.094,-49.709,-0.144]},{"b":[-50.624,-0.008,-50.332,0.591],"l":[-50.343,0.382,-50.351,0.582,-50.373,0.591,-50.397,0.581,-50.425,0.558,-50.426,0.425,-50.452,0.327,-50.526,0.247,-50.61,0.205,-50.624,0.054,-50.444,-0.008,-50.426,0.139,-50.332,0.259,-50.343,0.382]},{"b":[-50.282,0.359,-50.037,0.638],"l":[-50.261,0.359,-50.153,0.393,-50.04,0.523,-50.037,0.595,-50.059,0.638,-50.099,0.625,-50.113,0.605,-50.251,0.585,-50.282,0.517,-50.282,0.391,-50.261,0.359]},{"b":[-90.542,-0.773,-90.185,-0.485],"l":[-90.269,-0.485,-90.47,-0.517,-90.532,-0.581,-90.542,-0.676,-90.387,-0.773,-90.335,-0.772,-90.315,-0.757,-90.261,-0.742,-90.193,-0.659,-90.185,-0.545,-90.269,-0.485]},{"b":[-89.609,-0.952,-89.259,-0.68],"l":[-89.423,-0.722,-89.48,-0.793,-89.543,-0.827,-89.609,-0.889,-89.603,-0.913,-89.577,-0.934,-89.537,-0.952,-89.419,-0.911,-89.358,-0.826,-89.295,-0.786,-89.259,-0.728,-89.267,-0.705,-89.288,-0.69,-89.318,-0.68,-89.423,-0.722]},{"b":[-81.859,7.328,-81.603,7.622],"l":[-81.71,7.486,-81.671,7.523,-81.729,7.621,-81.752,7.622,-81.812,7.592,-81.857,7.508,-81.859,7.48,-81.852,7.453,-81.77,7.37,-81.658,7.328,-81.603,7.333,-81.613,7.38,-81.695,7.425,-81.71,7.486]},{"b":[-91.654,-0.478,-91.399,-0.256],"l":[-91.399,-0.322,-91.46,-0.256,-91.647,-0.284,-91.654,-0.311,-91.647,-0.391,-91.611,-0.444,-91.526,-0.478,-91.426,-0.461,-91.4,-0.421,-91.399,-0.322]},{"b":[-90.52,-1.342,-90.379,-1.221],"l":[-90.477,-1.221,-90.52,-1.299,-90.464,-1.342,-90.424,-1.34,-90.379,-1.292,-90.399,-1.262,-90.432,-1.24,-90.477,-1.221]},{"b":[-91.597,-1.02,-90.8,0.126],"l":[-90.959,-0.595,-90.968,-0.576,-90.951,-0.525,-90.976,-0.417,-91.176,-0.223,-91.21,-0.039,-91.272,0.025,-91.306,0.091,-91.361,0.126,-91.491,0.105,-91.509,0.062,-91.597,0.002,-91.59,-0.015,-91.55,-0.047,-91.469,-0.01,-91.429,-0.023,-91.369,-0.287,-91.25,-0.374,-91.197,-0.497,-91.121,-0.559,-91.145,-0.623,-91.334,-0.706,-91.458,-0.8,-91.495,-0.861,-91.484,-0.925,-91.419,-0.997,-91.372,-1.017,-91.131,-1.02,-90.906,-0.941,-90.8,-0.752,-90.863,-0.672,-90.959,-0.595]},{"b":[-90.868,-0.364,-90.553,-0.16],"l":[-90.668,-0.19,-90.78,-0.16,-90.82,-0.192,-90.868,-0.271,-90.809,-0.329,-90.62,-0.364,-90.574,-0.334,-90.553,-0.278,-90.668,-0.19]},{"b":[-109.434,-27.171,-109.223,-27.068],"l":[-109.39,-27.068,-109.429,-27.116,-109.434,-27.171,-109.28,-27.14,-109.223,-27.101,-109.276,-27.096,-109.39,-27.068]},{"b":[-106.639,21.529,-106.502,21.712],"l":[-106.524,21.652,-106.536,21.676,-106.597,21.712,-106.639,21.698,-106.634,21.613,-106.607,21.561,-106.531,21.529,-106.502,21.611,-106.524,21.652]},{"b":[-111.064,18.72,-110.914,18.863],"l":[-110.989,18.863,-111.04,18.83,-111.064,18.782,-110.975,18.72,-110.914,18.741,-110.942,18.802,-110.989,18.863]},{"b":[-110.703,24.892,-110.539,25.088],"l":[-110.703,25.047,-110.657,24.969,-110.59,24.908,-110.539,24.892,-110.567,25.003,-110.595,25.042,-110.69,25.088,-110.699,25.081,-110.703,25.047]},{"b":[-109.9,24.148,-109.794,24.345],"l":[-109.89,24.345,-109.9,24.331,-109.878,24.201,-109.827,24.148,-109.805,24.151,-109.796,24.164,-109.794,24.183,-109.89,24.345]},{"b":[-113.594,29.035,-113.156,29.573],"l":[-113.202,29.302,-113.374,29.339,-113.376,29.417,-113.416,29.486,-113.508,29.56,-113.587,29.573,-113.594,29.463,-113.581,29.413,-113.496,29.308,-113.265,29.097,-113.163,29.035,-113.156,29.052,-113.178,29.132,-113.202,29.302]},{"b":[-114.789,31.701,-114.688,31.794],"l":[-114.789,31.747,-114.727,31.701,-114.694,31.706,-114.688,31.724,-114.709,31.757,-114.771,31.794,-114.785,31.79,-114.789,31.747]},{"b":[-115.353,28.037,-115.149,28.368],"l":[-115.234,28.368,-115.274,28.343,-115.26,28.221,-115.353,28.104,-115.184,28.037,-115.171,28.069,-115.149,28.172,-115.197,28.328,-115.234,28.368]},{"b":[-112.013,24.346,-111.699,24.551],"l":[-111.857,24.538,-111.941,24.551,-112.013,24.533,-111.712,24.346,-111.699,24.394,-111.857,24.538]},{"b":[-111.225,25.836,-111.088,26.076],"l":[-111.091,26.076,-111.139,26.07,-111.183,26.041,-111.225,25.836,-111.204,25.85,-111.135,25.999,-111.094,25.974,-111.088,25.985,-111.1,26.021,-111.091,26.076]},{"b":[-112.531,28.769,-112.203,29.24],"l":[-112.203,29.005,-112.249,29.126,-112.263,29.207,-112.285,29.24,-112.424,29.204,-112.47,29.168,-112.531,28.894,-112.514,28.848,-112.355,28.773,-112.278,28.769,-112.203,29.005]},{"b":[-112.297,24.535,-112.057,25.286],"l":[-112.164,24.8,-112.195,24.841,-112.198,24.885,-112.132,25.224,-112.159,25.286,-112.222,24.951,-112.297,24.79,-112.21,24.763,-112.175,24.73,-112.163,24.65,-112.077,24.535,-112.057,24.546,-112.067,24.584,-112.126,24.654,-112.13,24.73,-112.164,24.8]},{"b":[166.893,-45.3,167.023,-45.18],"l":[166.893,-45.241,166.931,-45.277,167.023,-45.3,166.979,-45.18,166.963,-45.18,166.893,-45.241]},{"b":[124.288,10.601,124.382,10.707],"l":[124.335,10.707,124.288,10.601,124.316,10.606,124.381,10.633,124.382,10.68,124.371,10.691,124.335,10.707]},{"b":[125.133,9.933,125.288,10.155],"l":[125.231,10.116,125.176,10.151,125.133,10.155,125.159,10.063,125.288,9.933,125.281,9.982,125.231,10.116]},{"b":[122.797,7.315,122.948,7.433],"l":[122.915,7.433,122.871,7.397,122.822,7.428,122.797,7.393,122.805,7.316,122.84,7.315,122.944,7.361,122.948,7.386,122.937,7.409,122.915,7.433]},{"b":[125.683,6.906,125.785,7.186],"l":[125.783,7.131,125.714,7.186,125.683,7.073,125.708,7.04,125.769,6.906,125.785,6.963,125.783,7.131]},{"b":[125.922,9.568,125.993,9.759],"l":[125.968,9.759,125.949,9.739,125.922,9.621,125.952,9.568,125.971,9.594,125.993,9.685,125.968,9.759]},{"b":[124.36,11.486,124.622,11.695],"l":[124.565,11.64,124.511,11.687,124.437,11.695,124.36,11.666,124.429,11.532,124.483,11.486,124.608,11.492,124.622,11.55,124.565,11.64]},{"b":[124.731,11.543,124.854,11.715],"l":[124.788,11.683,124.731,11.715,124.744,11.659,124.781,11.581,124.807,11.558,124.836,11.543,124.854,11.595,124.821,11.627,124.788,11.683]},{"b":[134.795,-6.442,134.886,-6.324],"l":[134.795,-6.393,134.795,-6.442,134.82,-6.434,134.886,-6.324,134.852,-6.325,134.823,-6.35,134.795,-6.393]},{"b":[134.629,-6.765,134.736,-6.623],"l":[134.629,-6.713,134.631,-6.733,134.657,-6.765,134.674,-6.75,134.726,-6.669,134.736,-6.623,134.698,-6.626,134.663,-6.658,134.629,-6.713]},{"b":[127.834,-3.02,127.988,-2.937],"l":[127.834,-3.004,127.85,-3.016,127.938,-3.02,127.988,-2.937,127.938,-2.952,127.834,-3.004]},{"b":[127.488,-3.359,127.629,-3.254],"l":[127.554,-3.254,127.53,-3.262,127.488,-3.288,127.531,-3.331,127.629,-3.359,127.606,-3.315,127.554,-3.254]},{"b":[123.972,-6.022,124.051,-5.88],"l":[124.023,-5.902,123.976,-5.88,123.972,-5.939,124.006,-5.967,124.042,-6.022,124.051,-5.974,124.023,-5.902]},{"b":[123.541,-5.373,123.627,-5.25],"l":[123.55,-5.332,123.583,-5.367,123.623,-5.373,123.627,-5.272,123.561,-5.25,123.543,-5.271,123.541,-5.298,123.55,-5.332]},{"b":[120.633,-7.125,120.782,-7.018],"l":[120.633,-7.018,120.641,-7.116,120.672,-7.125,120.774,-7.119,120.782,-7.063,120.746,-7.06,120.633,-7.018]},{"b":[122.888,-8.604,123.153,-8.44],"l":[123.011,-8.448,122.933,-8.497,122.904,-8.531,122.888,-8.587,122.946,-8.604,122.977,-8.545,123.03,-8.495,123.153,-8.476,123.138,-8.457,123.089,-8.44,123.011,-8.448]},{"b":[117.482,-8.368,117.669,-8.148],"l":[117.546,-8.152,117.491,-8.183,117.482,-8.239,117.506,-8.307,117.49,-8.349,117.534,-8.368,117.556,-8.367,117.669,-8.189,117.665,-8.148,117.546,-8.152]},{"b":[69.469,66.49,70.11,66.751],"l":[70.057,66.599,70.058,66.627,70.077,66.696,69.918,66.712,69.8,66.736,69.616,66.739,69.503,66.751,69.469,66.716,69.651,66.565,69.845,66.49,70.021,66.502,70.059,66.518,70.11,66.569,70.057,66.599]},{"b":[96.739,76.188,97.053,76.355],"l":[96.854,76.199,96.974,76.237,97.053,76.303,97.045,76.315,96.99,76.343,96.878,76.355,96.835,76.345,96.833,76.324,96.74,76.258,96.739,76.207,96.754,76.196,96.798,76.188,96.854,76.199]},{"b":[99.915,79.602,100.3,79.701],"l":[100.068,79.701,99.956,79.69,99.942,79.671,99.915,79.602,100.136,79.614,100.3,79.67,100.142,79.684,100.068,79.701]},{"b":[161.083,68.9,161.618,69.639],"l":[161.521,69.634,161.505,69.639,161.41,69.596,161.323,69.541,161.111,69.47,161.083,69.406,161.165,69.334,161.125,69.197,161.137,69.11,161.183,69.082,161.364,69.044,161.461,68.996,161.456,68.966,161.422,68.9,161.467,68.901,161.507,68.928,161.517,68.97,161.495,69.016,161.394,69.106,161.378,69.194,161.373,69.293,161.351,69.369,161.374,69.414,161.54,69.437,161.609,69.501,161.618,69.592,161.521,69.634]},{"b":[129.687,31.64,129.794,31.787],"l":[129.707,31.718,129.687,31.64,129.718,31.657,129.794,31.742,129.787,31.787,129.707,31.718]},{"b":[126.319,36.428,126.418,36.613],"l":[126.387,36.571,126.319,36.613,126.338,36.471,126.404,36.428,126.418,36.493,126.387,36.571]},{"b":[124.846,39.508,124.935,39.608],"l":[124.89,39.602,124.846,39.559,124.849,39.508,124.905,39.536,124.935,39.608,124.89,39.602]},{"b":[121.132,28.063,121.251,28.204],"l":[121.205,28.204,121.134,28.135,121.132,28.063,121.164,28.063,121.251,28.086,121.251,28.145,121.234,28.181,121.205,28.204]},{"b":[113.404,22.758,113.564,22.905],"l":[113.465,22.905,113.404,22.903,113.426,22.859,113.463,22.832,113.486,22.828,113.564,22.758,113.555,22.804,113.521,22.852,113.465,22.905]},{"b":[112.713,21.582,112.863,21.772],"l":[112.782,21.772,112.761,21.733,112.713,21.698,112.733,21.67,112.742,21.618,112.771,21.582,112.79,21.602,112.801,21.695,112.813,21.712,112.863,21.753,112.839,21.765,112.782,21.772]},{"b":[112.525,21.619,112.648,21.71],"l":[112.559,21.675,112.525,21.623,112.546,21.619,112.644,21.64,112.648,21.71,112.559,21.675]},{"b":[113.839,22.21,114.044,22.333],"l":[113.998,22.21,114.003,22.278,114.044,22.333,113.882,22.28,113.839,22.242,113.852,22.22,113.877,22.21,113.998,22.21]},{"b":[107.078,10.337,107.195,10.446],"l":[107.078,10.388,107.084,10.337,107.168,10.397,107.195,10.446,107.177,10.446,107.151,10.42,107.078,10.388]},{"b":[103.223,10.68,103.318,10.782],"l":[103.223,10.782,103.223,10.76,103.281,10.68,103.318,10.719,103.223,10.782]},{"b":[8.4,54.688,8.588,54.76],"l":[8.51,54.76,8.468,54.757,8.418,54.739,8.4,54.714,8.454,54.691,8.549,54.688,8.588,54.713,8.573,54.749,8.51,54.76]},{"b":[6.642,53.579,6.801,53.625],"l":[6.755,53.625,6.669,53.606,6.642,53.579,6.735,53.583,6.801,53.625,6.755,53.625]},{"b":[-90.025,61.868,-61.3,73.855],"l":[-62.624,66.016,-62.497,65.974,-62.448,65.946,-62.41,65.906,-62.388,65.868,-62.382,65.833,-62.486,65.804,-62.61,65.724,-62.659,65.64,-62.772,65.632,-62.817,65.648,-62.969,65.622,-63.169,65.657,-63.241,65.696,-63.459,65.853,-63.464,65.835,-63.41,65.756,-63.421,65.709,-63.652,65.674,-63.651,65.661,-63.509,65.636,-63.337,65.617,-63.364,65.543,-63.363,65.23,-63.402,65.118,-63.486,65.021,-63.607,64.928,-63.737,64.989,-63.789,65.051,-63.833,65.083,-63.896,65.109,-63.976,65.121,-64.061,65.122,-64.152,65.066,-64.25,65.114,-64.346,65.172,-64.31,65.325,-64.27,65.401,-64.286,65.4,-64.34,65.364,-64.47,65.253,-64.555,65.117,-64.665,65.169,-64.765,65.234,-64.847,65.3,-64.98,65.375,-65.108,65.464,-65.176,65.568,-65.207,65.59,-65.282,65.677,-65.311,65.702,-65.337,65.71,-65.402,65.764,-65.378,65.822,-65.277,65.891,-65.185,65.94,-65.032,65.989,-64.854,66.016,-64.773,66.079,-64.673,66.193,-64.564,66.272,-64.445,66.317,-64.504,66.325,-64.655,66.287,-64.761,66.231,-64.887,66.137,-65.004,66.078,-65.305,66.008,-65.416,65.995,-65.544,65.987,-65.826,65.997,-65.891,66.02,-65.857,66.086,-65.656,66.205,-65.688,66.213,-65.759,66.171,-65.856,66.142,-65.94,66.127,-66.064,66.133,-66.209,66.206,-66.277,66.229,-66.419,66.254,-66.477,66.28,-66.712,66.46,-66.76,66.508,-66.787,66.556,-66.863,66.595,-66.986,66.627,-67.015,66.622,-66.97,66.582,-66.969,66.547,-67.077,66.525,-67.19,66.533,-67.307,66.57,-67.318,66.52,-67.192,66.433,-67.19,66.322,-67.225,66.31,-67.311,66.304,-67.369,66.317,-67.56,66.4,-67.741,66.458,-67.868,66.49,-67.883,66.467,-67.801,66.367,-67.704,66.269,-67.547,66.187,-67.297,66.09,-67.183,66.034,-67.273,65.956,-67.35,65.93,-67.399,65.922,-67.551,65.922,-67.828,65.965,-67.958,66.014,-68.147,66.13,-68.46,66.249,-68.528,66.249,-68.749,66.2,-68.714,66.192,-68.572,66.189,-68.467,66.173,-68.217,66.079,-68.198,66.039,-68.261,65.995,-68.257,65.939,-68.187,65.871,-68.115,65.828,-67.968,65.797,-67.894,65.793,-67.866,65.774,-67.954,65.623,-67.962,65.582,-67.937,65.565,-67.906,65.563,-67.717,65.625,-67.638,65.64,-67.57,65.644,-67.49,65.626,-67.4,65.588,-67.346,65.549,-67.33,65.509,-67.303,65.483,-67.118,65.44,-67.135,65.421,-67.326,65.357,-67.337,65.347,-67.298,65.342,-67.178,65.304,-67.067,65.244,-66.999,65.173,-66.985,65.138,-66.986,65.105,-66.97,65.085,-66.912,65.081,-66.887,65.094,-66.861,65.092,-66.831,65.074,-66.8,65.02,-66.733,64.86,-66.697,64.815,-66.677,64.814,-66.667,64.974,-66.635,65,-66.518,64.972,-66.345,64.91,-66.224,64.854,-66.21,64.828,-66.302,64.778,-66.282,64.755,-66.215,64.722,-66.152,64.735,-66.108,64.791,-66.03,64.847,-65.939,64.886,-65.768,64.854,-65.627,64.771,-65.605,64.742,-65.513,64.706,-65.432,64.726,-65.275,64.632,-65.349,64.589,-65.513,64.526,-65.529,64.505,-65.49,64.51,-65.179,64.51,-65.095,64.485,-65.075,64.437,-65.213,64.303,-65.34,64.315,-65.507,64.318,-65.594,64.311,-65.58,64.294,-65.348,64.232,-65.282,64.182,-65.193,64.13,-65.15,64.087,-65.151,64.068,-65.187,64.038,-65.17,64.028,-65.011,64.009,-64.912,64.026,-64.788,64.033,-64.678,64.028,-64.67,64.01,-64.686,63.961,-64.798,63.916,-64.768,63.905,-64.637,63.918,-64.576,63.897,-64.498,63.79,-64.411,63.706,-64.482,63.687,-64.562,63.68,-64.55,63.573,-64.499,63.463,-64.498,63.358,-64.514,63.264,-64.587,63.243,-64.665,63.245,-64.696,63.269,-64.886,63.549,-64.933,63.599,-64.99,63.643,-65.192,63.764,-65.184,63.745,-65.134,63.689,-65.089,63.606,-65.031,63.44,-65.005,63.333,-65.017,63.293,-65.058,63.283,-65.069,63.263,-65.049,63.235,-64.895,63.126,-64.82,63.06,-64.767,62.992,-64.718,62.946,-64.672,62.922,-64.684,62.902,-64.752,62.887,-64.869,62.88,-64.923,62.889,-65.133,62.952,-65.163,62.933,-65.047,62.701,-65.05,62.646,-65.108,62.626,-65.18,62.649,-65.266,62.715,-65.397,62.788,-65.572,62.869,-65.74,62.932,-65.78,62.93,-65.806,62.912,-65.834,62.909,-65.864,62.921,-65.92,62.969,-65.979,63.001,-66.224,63.107,-66.249,63.108,-66.226,63.076,-66.201,63.006,-66.229,62.991,-66.293,62.997,-66.414,63.027,-66.496,63.097,-66.6,63.219,-66.655,63.265,-66.66,63.235,-66.631,63.119,-66.636,63.08,-66.697,63.07,-66.723,63.08,-66.749,63.111,-66.773,63.162,-66.831,63.201,-66.923,63.228,-66.975,63.256,-67,63.305,-67.018,63.317,-67.18,63.305,-67.261,63.341,-67.495,63.481,-67.709,63.634,-67.844,63.715,-67.893,63.734,-67.821,63.635,-67.743,63.489,-67.723,63.423,-67.759,63.42,-67.838,63.449,-68.244,63.637,-68.494,63.725,-68.633,63.741,-68.859,63.752,-68.911,63.703,-68.789,63.595,-68.671,63.514,-68.555,63.459,-68.374,63.352,-68.208,63.215,-68.141,63.172,-67.915,63.114,-67.797,63.098,-67.676,63.094,-67.665,63.073,-67.724,63.034,-67.737,63.01,-67.468,62.948,-67.367,62.914,-67.269,62.858,-67.213,62.844,-66.98,62.701,-66.922,62.678,-66.714,62.632,-66.645,62.602,-66.531,62.51,-66.459,62.463,-66.357,62.352,-66.281,62.303,-66.095,62.246,-66.016,62.23,-65.98,62.209,-66.004,62.158,-66.027,62.137,-66.133,62.102,-66.116,62.054,-66.056,61.967,-66.059,61.914,-66.124,61.893,-66.257,61.868,-66.324,61.87,-66.425,61.891,-66.551,61.926,-66.803,62.013,-67.181,62.073,-67.322,62.105,-67.369,62.134,-67.44,62.151,-68.379,62.235,-68.536,62.256,-68.634,62.281,-68.724,62.319,-69.082,62.405,-69.126,62.424,-69.366,62.572,-69.545,62.745,-69.605,62.768,-69.8,62.79,-69.962,62.776,-70.071,62.757,-70.236,62.763,-70.344,62.792,-70.571,62.869,-70.801,62.91,-71.002,62.978,-71.106,63.002,-71.096,63.02,-70.946,63.121,-70.993,63.119,-71.254,63.043,-71.347,63.066,-71.501,63.126,-71.617,63.187,-71.855,63.355,-71.992,63.416,-71.973,63.43,-71.819,63.435,-71.697,63.43,-71.614,63.444,-71.456,63.512,-71.387,63.555,-71.381,63.58,-71.513,63.587,-71.542,63.599,-71.566,63.627,-71.627,63.663,-71.725,63.706,-71.838,63.725,-72.223,63.709,-72.29,63.728,-72.289,63.757,-72.213,63.839,-72.172,63.872,-72.159,63.89,-72.174,63.893,-72.226,63.891,-72.45,63.818,-72.498,63.823,-72.586,63.901,-72.639,63.989,-72.678,64.02,-72.73,64.03,-72.913,64.117,-73.174,64.282,-73.27,64.333,-73.377,64.38,-73.455,64.399,-73.444,64.423,-73.278,64.56,-73.271,64.583,-73.413,64.574,-73.627,64.603,-73.728,64.568,-73.793,64.566,-73.868,64.585,-73.91,64.578,-73.95,64.466,-73.981,64.438,-74.026,64.423,-74.065,64.425,-74.099,64.444,-74.098,64.47,-74.13,64.608,-74.205,64.628,-74.416,64.633,-74.461,64.645,-74.512,64.67,-74.556,64.717,-74.593,64.786,-74.634,64.824,-74.681,64.831,-74.719,64.825,-74.748,64.807,-74.813,64.796,-74.916,64.792,-74.919,64.766,-74.823,64.717,-74.73,64.647,-74.64,64.557,-74.695,64.497,-74.894,64.466,-75.067,64.457,-75.215,64.469,-75.328,64.49,-75.488,64.541,-75.715,64.524,-75.767,64.392,-75.815,64.385,-76.032,64.388,-76.118,64.376,-76.407,64.303,-76.495,64.293,-76.562,64.302,-76.627,64.284,-76.724,64.242,-76.856,64.238,-77.024,64.271,-77.166,64.285,-77.283,64.28,-77.403,64.3,-77.527,64.344,-77.628,64.363,-77.76,64.36,-77.791,64.367,-77.985,64.461,-78.045,64.499,-78.175,64.618,-78.198,64.665,-78.201,64.715,-78.19,64.752,-78.145,64.808,-78.096,64.939,-78.055,64.983,-77.995,65.023,-77.876,65.073,-77.447,65.162,-77.361,65.197,-77.364,65.22,-77.461,65.328,-77.46,65.356,-77.428,65.372,-77.358,65.435,-77.327,65.453,-77.251,65.463,-77.094,65.431,-76.959,65.418,-76.779,65.414,-76.482,65.37,-76.067,65.285,-75.828,65.227,-75.648,65.141,-75.52,65.056,-75.502,65.013,-75.561,64.947,-75.591,64.928,-75.589,64.905,-75.556,64.879,-75.452,64.842,-75.427,64.856,-75.435,64.901,-75.414,64.939,-75.363,64.969,-75.357,65.009,-75.397,65.058,-75.446,65.1,-75.505,65.135,-75.773,65.257,-75.799,65.298,-75.709,65.316,-75.317,65.275,-75.166,65.284,-75.109,65.331,-75.048,65.364,-74.982,65.381,-74.85,65.389,-74.665,65.367,-74.575,65.364,-74.495,65.372,-74.391,65.398,-74.237,65.484,-74.138,65.503,-73.99,65.517,-73.878,65.519,-73.675,65.484,-73.551,65.485,-73.561,65.543,-73.643,65.653,-73.746,65.767,-73.826,65.805,-74.033,65.877,-74.276,66.013,-74.401,66.097,-74.434,66.139,-74.416,66.167,-74.375,66.208,-73.934,66.358,-73.584,66.507,-73.431,66.583,-73.357,66.636,-73.281,66.675,-73.201,66.699,-73.033,66.728,-72.985,66.765,-72.975,66.829,-72.947,66.883,-72.789,67.031,-72.668,67.07,-72.485,67.098,-72.365,67.133,-72.22,67.254,-72.234,67.284,-72.301,67.307,-72.353,67.342,-72.576,67.659,-72.725,67.812,-72.904,67.945,-73.063,68.107,-73.328,68.267,-73.331,68.309,-73.284,68.357,-73.307,68.368,-73.58,68.298,-73.644,68.295,-73.749,68.325,-73.821,68.363,-73.879,68.429,-73.873,68.464,-73.834,68.497,-73.783,68.578,-73.781,68.619,-73.798,68.659,-73.822,68.686,-73.852,68.701,-73.935,68.711,-74.073,68.715,-74.118,68.701,-73.966,68.579,-73.989,68.549,-74.183,68.535,-74.27,68.541,-74.35,68.556,-74.422,68.58,-74.648,68.708,-74.696,68.756,-74.681,68.79,-74.7,68.808,-74.746,68.797,-74.808,68.796,-74.893,68.808,-74.91,68.823,-74.752,68.892,-74.743,68.913,-74.816,68.936,-74.925,68.941,-74.954,68.961,-74.917,68.983,-74.769,69.021,-74.717,69.046,-74.805,69.064,-74.855,69.066,-74.954,69.025,-75.104,68.941,-75.213,68.909,-75.363,68.948,-75.457,68.961,-75.523,68.953,-75.623,68.888,-75.842,68.84,-76.235,68.728,-76.403,68.692,-76.585,68.699,-76.619,68.721,-76.616,68.76,-76.604,68.792,-76.582,68.816,-76.575,68.847,-76.588,68.974,-76.557,69.009,-76.495,69.03,-76.381,69.052,-76.089,69.026,-75.954,69.031,-75.859,69.06,-75.763,69.103,-75.668,69.159,-75.648,69.213,-75.749,69.3,-75.787,69.319,-76.046,69.386,-76.19,69.411,-76.316,69.422,-76.408,69.441,-76.465,69.469,-76.52,69.517,-76.525,69.549,-76.516,69.591,-76.463,69.62,-76.231,69.653,-76.234,69.662,-76.424,69.687,-76.513,69.684,-76.59,69.656,-76.687,69.591,-76.742,69.573,-76.916,69.611,-77.02,69.617,-77.09,69.635,-77.129,69.653,-77.105,69.671,-77.019,69.689,-76.869,69.745,-76.859,69.775,-76.962,69.825,-77.016,69.836,-77.232,69.855,-77.494,69.836,-77.592,69.846,-77.635,69.9,-77.663,69.966,-77.675,70.042,-77.722,70.171,-77.774,70.239,-77.843,70.247,-78.157,70.219,-78.231,70.219,-78.283,70.229,-78.491,70.316,-78.575,70.346,-78.621,70.353,-78.773,70.445,-78.831,70.463,-78.9,70.509,-78.98,70.581,-79.066,70.604,-79.16,70.575,-79.253,70.535,-79.347,70.482,-79.397,70.437,-79.405,70.401,-79.347,70.372,-79.018,70.325,-78.934,70.294,-78.863,70.242,-78.81,70.179,-78.775,70.104,-78.778,70.048,-78.819,70.01,-78.89,69.977,-79.093,69.925,-79.303,69.895,-79.515,69.888,-79.616,69.895,-80.162,69.996,-80.26,69.997,-80.387,70.01,-80.67,70.052,-80.826,70.057,-81.098,70.091,-81.56,70.111,-81.652,70.095,-81.529,70.048,-81.422,70.025,-81.329,70.024,-81.197,69.983,-81.024,69.9,-80.925,69.851,-80.843,69.792,-80.84,69.771,-80.922,69.731,-81.565,69.943,-81.958,69.869,-82.139,69.841,-82.294,69.837,-82.488,69.866,-82.925,69.968,-83.091,70.004,-83.15,70.009,-83.531,69.965,-83.859,69.963,-84.522,70.005,-84.765,70.034,-84.829,70.063,-84.909,70.078,-85.053,70.078,-85.432,70.111,-85.78,70.037,-86.198,70.105,-86.322,70.145,-86.361,70.173,-86.483,70.289,-86.5,70.35,-86.465,70.406,-86.431,70.445,-86.397,70.465,-86.624,70.401,-86.704,70.391,-86.809,70.388,-87.122,70.412,-87.172,70.4,-87.156,70.377,-87.074,70.345,-87.063,70.325,-87.238,70.31,-87.502,70.326,-87.618,70.319,-87.67,70.31,-87.789,70.258,-87.838,70.247,-87.901,70.252,-88.178,70.369,-88.402,70.442,-88.663,70.471,-88.783,70.494,-88.848,70.523,-89.208,70.76,-89.258,70.811,-89.372,70.996,-89.41,71.036,-89.456,71.062,-89.366,71.067,-89.025,71.045,-88.696,71.046,-88.517,71.031,-88.309,70.984,-88.039,70.951,-87.845,70.944,-87.534,70.957,-87.182,70.988,-87.14,71.012,-87.369,71.053,-87.572,71.108,-87.76,71.179,-87.872,71.209,-88.061,71.227,-88.59,71.24,-89.079,71.288,-89.418,71.352,-89.693,71.423,-89.805,71.462,-89.846,71.492,-89.889,71.586,-89.934,71.743,-89.977,71.848,-90.02,71.902,-90.025,71.949,-89.931,72.049,-89.664,72.158,-89.657,72.175,-89.711,72.18,-89.823,72.208,-89.859,72.248,-89.873,72.313,-89.874,72.367,-89.862,72.412,-89.817,72.468,-89.702,72.568,-89.536,72.69,-89.358,72.804,-89.327,72.842,-89.311,72.943,-89.288,73.017,-89.263,73.069,-89.225,73.108,-89.115,73.182,-88.977,73.252,-88.761,73.312,-88.743,73.335,-88.74,73.365,-88.727,73.388,-88.705,73.403,-88.17,73.595,-87.926,73.673,-87.72,73.723,-87.472,73.759,-86.769,73.834,-86.406,73.855,-85.951,73.85,-85.11,73.808,-85.009,73.779,-84.984,73.764,-84.947,73.722,-84.975,73.695,-85.204,73.604,-85.494,73.528,-85.682,73.461,-86.001,73.313,-86.086,73.26,-86.481,72.96,-86.575,72.911,-86.629,72.871,-86.668,72.763,-86.656,72.724,-86.595,72.661,-86.38,72.525,-86.323,72.461,-86.324,72.402,-86.348,72.262,-86.351,72.191,-86.341,72.123,-86.297,72.026,-86.218,71.899,-86.036,71.771,-85.75,71.641,-85.537,71.555,-85.327,71.492,-85.079,71.398,-85.023,71.353,-85.138,71.303,-85.405,71.227,-85.757,71.194,-85.945,71.163,-86.179,71.096,-86.473,71.043,-86.589,71.011,-86.55,70.989,-86.321,71.017,-86.127,71.049,-85.825,71.126,-85.644,71.152,-85.095,71.152,-85.002,71.137,-85.043,71.092,-85.066,71.079,-85.048,71.059,-84.988,71.032,-84.87,71.002,-84.824,71.029,-84.79,71.093,-84.709,71.359,-84.674,71.439,-84.658,71.515,-84.66,71.586,-84.699,71.631,-84.84,71.659,-85.032,71.654,-85.131,71.661,-85.25,71.675,-85.339,71.697,-85.397,71.727,-85.512,71.817,-85.596,71.866,-85.813,71.956,-85.912,71.987,-85.862,72.022,-85.665,72.063,-85.546,72.102,-85.406,72.215,-85.322,72.233,-85.019,72.218,-84.608,72.129,-84.352,72.053,-84.284,72.044,-84.282,72.058,-84.347,72.094,-84.643,72.19,-84.778,72.259,-84.842,72.308,-84.811,72.33,-84.645,72.351,-84.623,72.377,-84.849,72.406,-84.964,72.406,-85.057,72.384,-85.156,72.383,-85.341,72.422,-85.391,72.444,-85.498,72.511,-85.554,72.569,-85.616,72.605,-85.638,72.633,-85.65,72.722,-85.645,72.774,-85.619,72.819,-85.575,72.856,-85.455,72.925,-85.388,72.945,-85.262,72.954,-84.99,72.92,-84.257,72.797,-84.274,72.836,-85.094,73.003,-85.384,73.045,-85.455,73.105,-85.018,73.335,-84.616,73.39,-84.416,73.456,-84.089,73.459,-83.782,73.417,-83.777,73.428,-83.915,73.508,-83.904,73.528,-83.73,73.576,-83.41,73.632,-83.02,73.676,-82.943,73.699,-82.843,73.715,-82.66,73.73,-82.203,73.736,-81.946,73.73,-81.605,73.696,-81.406,73.635,-81.344,73.598,-81.238,73.48,-81.152,73.314,-81.025,73.245,-80.822,73.207,-80.681,73.166,-80.603,73.121,-80.583,73.065,-80.619,72.997,-80.592,72.928,-80.501,72.857,-80.431,72.816,-80.277,72.77,-80.275,72.746,-80.323,72.717,-80.424,72.679,-80.675,72.559,-80.999,72.426,-81.229,72.312,-81.241,72.278,-80.761,72.457,-80.611,72.451,-80.605,72.426,-80.702,72.338,-80.821,72.26,-80.941,72.21,-80.919,72.191,-80.691,72.103,-80.733,72.089,-80.843,72.096,-80.888,72.088,-80.921,72.072,-80.941,72.048,-80.943,72.014,-80.925,71.971,-80.927,71.938,-80.948,71.917,-80.925,71.908,-80.858,71.911,-80.802,71.929,-80.705,71.988,-80.386,72.149,-80.182,72.209,-80.116,72.214,-79.928,72.175,-79.884,72.177,-80.091,72.301,-80.109,72.332,-80.067,72.378,-80.042,72.394,-79.927,72.428,-79.831,72.446,-79.778,72.439,-79.693,72.376,-79.654,72.332,-79.584,72.315,-79.427,72.337,-79.323,72.391,-79.194,72.356,-79,72.272,-79.018,72.188,-79.018,72.104,-79.008,72.043,-78.776,71.93,-78.614,71.881,-78.585,71.881,-78.589,71.898,-78.623,71.935,-78.711,71.972,-78.791,72.03,-78.863,72.101,-78.82,72.265,-78.699,72.351,-78.582,72.329,-78.429,72.28,-78.307,72.275,-78.116,72.28,-77.726,72.18,-77.517,72.178,-77.536,72.219,-77.694,72.238,-77.926,72.294,-78.287,72.36,-78.453,72.435,-78.484,72.471,-78.479,72.509,-78.459,72.542,-78.422,72.572,-78.35,72.6,-78.001,72.688,-77.753,72.725,-77.567,72.737,-77.255,72.736,-76.894,72.721,-76.698,72.695,-76.473,72.633,-76.189,72.572,-76.087,72.561,-75.969,72.563,-75.833,72.577,-75.704,72.572,-75.294,72.481,-75.186,72.434,-75.12,72.378,-75.071,72.323,-75.04,72.27,-75.053,72.226,-75.394,72.04,-75.543,72.008,-75.641,71.937,-75.787,71.803,-75.911,71.731,-75.923,71.717,-75.897,71.714,-75.822,71.746,-75.693,71.839,-75.6,71.918,-75.428,71.984,-75.148,72.063,-74.903,72.1,-74.695,72.097,-74.52,72.086,-74.377,72.067,-74.293,72.051,-74.266,72.038,-74.209,71.979,-74.213,71.939,-74.248,71.894,-74.316,71.843,-74.621,71.786,-74.789,71.742,-74.893,71.726,-75.205,71.709,-75.191,71.692,-74.959,71.667,-74.701,71.676,-74.707,71.647,-74.829,71.571,-74.868,71.505,-74.834,71.451,-74.841,71.407,-74.931,71.314,-75.035,71.231,-74.996,71.218,-74.759,71.338,-74.695,71.469,-74.6,71.585,-74.488,71.648,-74.404,71.673,-74.139,71.682,-73.992,71.75,-73.867,71.771,-73.814,71.771,-73.707,71.746,-73.714,71.72,-73.869,71.599,-74.197,71.404,-74.063,71.426,-73.973,71.473,-73.851,71.519,-73.713,71.588,-73.622,71.526,-73.482,71.479,-73.398,71.373,-73.262,71.322,-73.181,71.283,-73.192,71.35,-73.31,71.484,-73.278,71.538,-73.187,71.565,-72.902,71.678,-72.703,71.64,-72.581,71.607,-72.519,71.616,-72.365,71.611,-72.117,71.593,-71.875,71.561,-71.641,71.516,-71.46,71.464,-71.333,71.403,-71.256,71.362,-71.229,71.339,-71.187,71.279,-71.219,71.239,-71.397,71.147,-71.495,71.105,-71.593,71.086,-71.856,71.105,-71.938,71.094,-72.024,71.065,-72.298,70.939,-72.449,70.884,-72.598,70.849,-72.633,70.831,-72.313,70.833,-72.224,70.87,-72.15,70.941,-72.009,71.013,-71.743,71.047,-71.371,70.975,-71.186,70.978,-71.045,71.05,-70.888,71.099,-70.826,71.109,-70.792,71.103,-70.673,71.052,-70.636,71.007,-70.639,70.902,-70.655,70.871,-70.762,70.792,-71.022,70.674,-71.192,70.63,-71.38,70.606,-71.586,70.566,-71.658,70.534,-71.729,70.488,-71.8,70.457,-71.89,70.432,-71.772,70.394,-71.727,70.395,-71.684,70.418,-71.565,70.506,-71.477,70.544,-71.427,70.552,-71.375,70.548,-71.325,70.531,-71.276,70.5,-71.28,70.425,-71.429,70.128,-71.405,70.129,-71.313,70.209,-71.045,70.519,-70.98,70.581,-70.851,70.644,-70.561,70.738,-70.337,70.788,-70.085,70.83,-69.95,70.845,-69.796,70.835,-69.696,70.786,-69.56,70.777,-69.395,70.789,-69.289,70.783,-69.169,70.764,-69.066,70.728,-68.891,70.687,-68.496,70.61,-68.447,70.594,-68.401,70.565,-68.358,70.523,-68.364,70.481,-68.417,70.44,-68.483,70.415,-68.561,70.406,-68.643,70.383,-68.794,70.324,-68.843,70.314,-69.079,70.289,-69.299,70.277,-69.436,70.253,-69.699,70.189,-70.061,70.071,-70.058,70.043,-69.913,70.029,-69.796,70.047,-69.635,70.129,-69.483,70.16,-69.246,70.185,-68.919,70.207,-68.778,70.204,-68.753,70.199,-68.735,70.18,-68.723,70.146,-68.777,70.101,-68.839,70.08,-69.008,69.979,-68.897,69.953,-68.744,69.941,-68.657,69.968,-68.578,70.03,-68.489,70.065,-68.391,70.072,-68.305,70.087,-68.231,70.112,-68.21,70.128,-68.319,70.161,-68.327,70.18,-68.283,70.228,-68.204,70.281,-68.121,70.315,-68.059,70.317,-67.855,70.282,-67.716,70.22,-67.364,70.034,-67.318,69.998,-67.196,69.861,-67.173,69.799,-67.193,69.757,-67.222,69.731,-67.259,69.721,-67.337,69.721,-67.806,69.777,-68.02,69.77,-68.114,69.754,-68.189,69.731,-68.248,69.701,-68.29,69.665,-68.372,69.644,-68.67,69.644,-68.837,69.624,-69.125,69.575,-69.228,69.547,-69.251,69.512,-69.075,69.518,-68.785,69.564,-68.513,69.577,-68.058,69.476,-67.908,69.46,-67.825,69.475,-67.725,69.479,-67.361,69.473,-67.237,69.46,-67.053,69.421,-66.771,69.337,-66.717,69.312,-66.685,69.286,-66.676,69.258,-66.679,69.191,-66.707,69.168,-66.803,69.153,-67.208,69.171,-67.332,69.185,-67.484,69.167,-67.607,69.173,-67.765,69.2,-67.938,69.248,-68.198,69.203,-68.406,69.232,-68.619,69.206,-69.041,69.098,-68.993,69.079,-68.416,69.172,-68.304,69.166,-68.121,69.133,-67.833,69.066,-67.752,69.039,-67.751,68.934,-67.795,68.863,-67.883,68.784,-68.016,68.795,-68.324,68.844,-68.45,68.851,-68.543,68.843,-68.667,68.811,-68.725,68.81,-69.219,68.873,-69.33,68.876,-69.343,68.869,-69.319,68.857,-68.871,68.76,-68.541,68.749,-68.333,68.733,-68.21,68.703,-68.152,68.681,-68.148,68.616,-68.038,68.551,-67.938,68.524,-67.875,68.523,-67.766,68.547,-67.656,68.551,-67.567,68.534,-67.456,68.498,-67.321,68.488,-67.202,68.466,-67.111,68.461,-66.854,68.472,-66.743,68.458,-66.714,68.446,-66.762,68.425,-66.997,68.374,-67.033,68.326,-66.9,68.264,-66.831,68.216,-66.834,68.18,-66.905,68.098,-66.923,68.066,-66.9,68.063,-66.729,68.129,-66.702,68.121,-66.685,68.029,-66.663,68.034,-66.605,68.11,-66.631,68.211,-66.531,68.25,-66.212,68.28,-66.266,68.123,-66.275,68.041,-66.414,67.904,-66.53,67.86,-66.526,67.851,-66.444,67.834,-66.392,67.832,-66.343,67.853,-66.225,67.959,-65.986,68.069,-65.942,68.071,-65.944,68.031,-65.975,67.957,-65.864,67.923,-65.759,67.957,-65.702,67.987,-65.569,67.982,-65.509,67.968,-65.491,67.936,-65.552,67.799,-65.541,67.766,-65.401,67.675,-65.387,67.68,-65.413,67.724,-65.442,67.832,-65.415,67.879,-65.3,67.94,-65.064,68.026,-64.977,68.043,-64.922,68.032,-64.835,67.99,-64.863,67.965,-64.956,67.939,-65.026,67.892,-65.071,67.824,-65.021,67.788,-64.83,67.784,-64.638,67.84,-64.528,67.813,-64.396,67.74,-64.156,67.623,-64.019,67.655,-63.85,67.566,-64.077,67.496,-64.008,67.347,-64.303,67.353,-64.469,67.342,-64.58,67.355,-64.7,67.351,-64.589,67.316,-64.376,67.301,-64.356,67.256,-64.189,67.257,-64.063,67.266,-63.836,67.264,-63.824,67.316,-63.676,67.345,-63.592,67.378,-63.521,67.358,-63.316,67.336,-63.04,67.235,-63.162,67.174,-63.195,67.117,-63.236,67.069,-63.258,67.025,-63.307,66.994,-63.702,66.822,-63.636,66.821,-63.469,66.862,-63.144,66.924,-62.962,66.949,-62.833,66.933,-62.768,66.932,-62.71,66.954,-62.603,66.929,-62.38,66.905,-62.124,67.047,-61.969,67.019,-61.824,66.932,-61.515,66.778,-61.353,66.689,-61.3,66.649,-61.307,66.609,-61.453,66.567,-61.528,66.558,-61.724,66.638,-61.904,66.678,-62.014,66.674,-62.123,66.643,-62.089,66.626,-61.653,66.503,-61.576,66.413,-61.571,66.373,-61.863,66.313,-61.956,66.309,-62.158,66.338,-62.277,66.392,-62.375,66.411,-62.51,66.417,-62.553,66.407,-62.406,66.316,-62.42,66.289,-62.496,66.271,-62.534,66.227,-62.242,66.148,-62.024,66.068,-61.992,66.035,-62.139,66.011,-62.244,66.006,-62.468,66.017,-62.59,66.034,-62.624,66.016]},{"b":[-91.684,76.174,-61.207,83.116],"l":[-68.721,81.261,-68.689,81.293,-66.914,81.485,-66.861,81.499,-66.801,81.527,-66.765,81.563,-66.626,81.616,-66.005,81.629,-65.701,81.646,-65.495,81.668,-65.399,81.715,-65.226,81.744,-64.574,81.734,-64.436,81.743,-64.128,81.794,-63.592,81.846,-62.496,82.007,-62.177,82.043,-61.969,82.11,-61.615,82.184,-61.274,82.28,-61.207,82.341,-61.302,82.4,-61.392,82.442,-61.477,82.467,-61.697,82.489,-62.475,82.52,-63.247,82.45,-63.251,82.467,-63.087,82.533,-63.085,82.565,-63.385,82.653,-63.593,82.694,-63.643,82.713,-63.621,82.729,-63.564,82.749,-63.473,82.771,-63.499,82.793,-63.641,82.813,-63.984,82.829,-64.134,82.823,-64.433,82.778,-64.504,82.778,-64.635,82.819,-64.777,82.876,-64.905,82.901,-64.984,82.902,-65.113,82.889,-65.162,82.87,-65.247,82.819,-65.299,82.8,-65.4,82.802,-65.55,82.827,-65.727,82.842,-66.12,82.807,-66.612,82.742,-66.866,82.719,-66.998,82.716,-67.397,82.668,-67.736,82.652,-68.173,82.646,-68.469,82.653,-68.358,82.677,-66.836,82.818,-66.6,82.861,-66.425,82.906,-66.423,82.927,-66.592,82.944,-67.406,82.954,-67.624,82.964,-67.925,82.956,-68.107,82.961,-68.409,83.005,-68.673,82.999,-69.489,83.017,-69.569,83.025,-69.782,83.093,-69.868,83.11,-69.97,83.116,-70.871,83.098,-71.085,83.083,-71.424,83.021,-71.402,83.001,-71.198,82.97,-70.933,82.911,-70.94,82.902,-71.132,82.923,-71.406,82.975,-71.983,83.101,-72.069,83.106,-72.812,83.081,-73.331,82.999,-73.404,82.977,-73.441,82.946,-73.442,82.905,-73.235,82.844,-72.776,82.756,-72.659,82.722,-73.272,82.772,-73.703,82.852,-73.917,82.904,-74.056,82.955,-74.198,82.989,-74.414,83.013,-75.745,83.047,-77.125,83.009,-77.041,82.968,-76.908,82.919,-76.41,82.816,-76.188,82.758,-76.087,82.724,-75.643,82.644,-75.566,82.609,-75.744,82.572,-76.009,82.535,-76.146,82.55,-76.244,82.604,-76.336,82.644,-76.421,82.671,-77.226,82.837,-77.48,82.883,-77.618,82.896,-77.969,82.906,-78.525,82.891,-79.181,82.933,-79.886,82.939,-80.155,82.911,-80.141,82.894,-79.974,82.859,-79.834,82.817,-79.642,82.785,-79.207,82.733,-78.792,82.694,-78.749,82.679,-79.035,82.675,-80.076,82.706,-80.657,82.769,-81.01,82.779,-81.128,82.762,-81.178,82.745,-81.147,82.716,-80.81,82.586,-80.862,82.572,-81.189,82.594,-81.58,82.643,-81.785,82.649,-82.117,82.629,-82.123,82.602,-81.959,82.563,-81.681,82.519,-81.718,82.506,-82.023,82.494,-82.269,82.465,-82.451,82.427,-82.448,82.395,-82.254,82.336,-81.998,82.278,-81.468,82.192,-80.13,82.028,-79.629,81.932,-79.425,81.854,-79.466,81.851,-79.686,81.886,-79.909,81.936,-80.153,81.978,-80.55,82.005,-81.584,82.121,-82.277,82.218,-82.537,82.247,-82.638,82.246,-82.709,82.229,-82.747,82.196,-82.657,82.158,-82.327,82.092,-82.356,82.066,-82.634,82.077,-82.774,82.095,-83.01,82.142,-83.176,82.187,-83.591,82.326,-83.824,82.351,-84.368,82.374,-84.553,82.398,-84.745,82.437,-84.897,82.449,-85.276,82.405,-85.481,82.366,-85.794,82.292,-85.92,82.283,-86.188,82.248,-86.616,82.219,-86.581,82.187,-85.311,82.044,-85.169,82.023,-85.052,81.995,-85.045,81.983,-85.403,81.982,-85.538,81.955,-85.646,81.953,-85.875,81.976,-86.158,82.026,-86.378,82.045,-86.627,82.051,-86.834,82.033,-86.999,81.992,-87.018,81.959,-87.218,82,-87.404,82.054,-87.639,82.085,-88.063,82.096,-88.567,82.061,-88.875,82.018,-89.156,81.955,-89.381,81.917,-89.633,81.895,-90.163,81.894,-90.49,81.877,-90.942,81.827,-91.219,81.788,-91.424,81.744,-91.648,81.684,-91.684,81.636,-91.403,81.578,-91.292,81.571,-91.103,81.592,-90.834,81.64,-90.626,81.656,-90.48,81.639,-90.331,81.632,-89.822,81.635,-89.845,81.612,-90.554,81.464,-90.609,81.43,-90.416,81.405,-90.304,81.401,-88.978,81.542,-88.479,81.565,-88.101,81.559,-87.597,81.526,-87.617,81.509,-88.127,81.519,-88.622,81.501,-88.892,81.474,-89.427,81.387,-89.674,81.329,-89.636,81.302,-89.209,81.25,-89.263,81.239,-89.563,81.226,-89.947,81.173,-89.981,81.125,-89.792,81.065,-89.623,81.032,-89.398,81.025,-88.887,81.058,-87.275,81.081,-86.623,81.123,-85.875,81.241,-85.402,81.285,-85.206,81.295,-84.941,81.286,-85.083,81.247,-85.81,81.124,-86.477,81.036,-86.929,81,-87.389,80.988,-88.413,81,-89.167,80.941,-89.263,80.914,-89.212,80.882,-89.145,80.854,-89.062,80.83,-88.921,80.806,-88.625,80.77,-88.232,80.704,-88.004,80.675,-87.712,80.656,-87.33,80.67,-87.08,80.726,-86.233,80.95,-85.967,81.012,-85.781,81.035,-84.635,81.098,-83.289,81.148,-83.349,81.103,-84.68,81.042,-85.246,80.988,-85.639,80.925,-86.252,80.79,-86.44,80.728,-86.603,80.664,-86.615,80.63,-86.532,80.605,-86.25,80.566,-86.097,80.562,-85.726,80.581,-85.307,80.526,-85.146,80.521,-84.418,80.527,-84.22,80.538,-84.076,80.556,-83.885,80.602,-83.647,80.674,-83.401,80.714,-82.78,80.736,-82.498,80.763,-82.222,80.772,-82.337,80.729,-82.768,80.631,-82.884,80.578,-82.613,80.559,-82.368,80.561,-81.553,80.623,-81.301,80.627,-81.007,80.655,-80.134,80.764,-79.761,80.842,-79.607,80.882,-79.545,80.909,-79.477,80.961,-79.402,81.037,-79.309,81.089,-79.198,81.118,-79.072,81.128,-78.932,81.119,-78.734,81.151,-78.352,81.259,-77.972,81.331,-76.885,81.43,-77.031,81.386,-77.536,81.321,-78.287,81.168,-78.464,81.114,-78.629,81.043,-78.682,81.001,-78.716,80.952,-78.551,80.921,-78.004,80.905,-77.389,80.905,-77.119,80.896,-76.85,80.878,-76.863,80.865,-77.169,80.843,-77.507,80.835,-78.386,80.784,-79.629,80.648,-79.674,80.625,-80.051,80.529,-80.98,80.445,-82.536,80.376,-82.785,80.354,-82.987,80.323,-82.961,80.278,-82.681,80.175,-82.332,80.066,-81.86,79.957,-81.644,79.89,-81.359,79.788,-81.179,79.733,-81.01,79.693,-80.714,79.675,-80.287,79.679,-80.124,79.669,-80.271,79.635,-80.476,79.606,-80.668,79.601,-81.038,79.614,-81.463,79.654,-81.688,79.686,-81.856,79.723,-82.049,79.783,-82.377,79.908,-82.677,79.993,-83.004,80.055,-83.344,80.147,-83.724,80.229,-84.057,80.262,-84.675,80.279,-85.16,80.272,-86.307,80.319,-86.499,80.258,-86.615,80.124,-86.494,80.018,-86.421,79.845,-86.147,79.743,-86.031,79.722,-85.457,79.69,-85.269,79.664,-85.09,79.612,-84.836,79.495,-84.522,79.377,-84.381,79.301,-84.197,79.225,-83.978,79.163,-83.662,79.09,-83.576,79.054,-83.825,79.059,-84.053,79.099,-84.257,79.122,-84.384,79.119,-84.53,79.101,-84.568,79.071,-84.496,79.029,-84.412,78.997,-84.316,78.975,-84.146,78.96,-83.779,78.945,-83.059,78.94,-82.644,78.908,-82.439,78.904,-82.237,78.924,-82.028,78.962,-81.889,78.975,-81.75,78.976,-81.781,78.95,-81.981,78.898,-82.151,78.864,-82.291,78.847,-82.442,78.84,-82.99,78.844,-83.147,78.808,-83.271,78.77,-83.389,78.779,-83.547,78.804,-83.908,78.839,-84.787,78.885,-85.004,78.912,-85.23,78.902,-85.691,78.844,-86.242,78.824,-86.808,78.774,-86.953,78.664,-87.164,78.558,-87.361,78.479,-87.491,78.417,-87.491,78.284,-87.552,78.177,-87.339,78.133,-86.913,78.127,-86.694,78.151,-86.427,78.197,-86.071,78.285,-85.92,78.343,-86.063,78.187,-86.218,78.081,-85.586,78.11,-85.419,78.142,-85.27,78.2,-85.024,78.312,-84.783,78.528,-84.91,78.24,-84.55,78.251,-84.388,78.206,-84.223,78.176,-84.524,78.197,-84.615,78.196,-85.031,78.062,-85.265,78.011,-85.548,77.928,-85.292,77.764,-85.289,77.559,-85.088,77.515,-84.861,77.5,-84.486,77.562,-84.168,77.523,-83.928,77.518,-83.779,77.533,-83.428,77.621,-83.304,77.674,-82.704,77.962,-82.595,77.992,-82.626,77.936,-82.665,77.889,-82.71,77.85,-82.903,77.733,-83.25,77.585,-83.477,77.514,-83.55,77.483,-83.608,77.442,-83.721,77.414,-83.974,77.391,-84.487,77.368,-84.739,77.361,-84.951,77.375,-85.588,77.461,-85.731,77.509,-85.907,77.614,-86.173,77.746,-86.385,77.809,-86.755,77.864,-87.018,77.892,-87.236,77.892,-87.497,77.872,-87.757,77.836,-88.017,77.785,-88.095,77.719,-87.938,77.6,-87.78,77.493,-87.681,77.436,-87.589,77.395,-87.43,77.348,-87.265,77.343,-87.182,77.332,-87.101,77.308,-86.874,77.2,-86.812,77.185,-86.852,77.174,-87.064,77.166,-87.362,77.136,-87.61,77.127,-87.828,77.136,-88.148,77.124,-88.398,77.104,-88.556,77.072,-88.771,76.993,-89.5,76.827,-89.544,76.66,-89.57,76.492,-89.37,76.474,-88.804,76.457,-88.546,76.421,-88.563,76.547,-88.614,76.651,-88.496,76.773,-88.482,76.58,-88.396,76.405,-88.104,76.413,-87.498,76.386,-87.49,76.586,-87.354,76.448,-86.978,76.413,-86.68,76.377,-86.562,76.517,-86.454,76.585,-86.419,76.58,-86.367,76.549,-86.296,76.492,-86.116,76.435,-85.681,76.349,-85.344,76.313,-85.141,76.305,-84.275,76.357,-84.224,76.675,-83.986,76.495,-83.886,76.453,-83.389,76.439,-82.233,76.466,-82.208,76.514,-82.262,76.575,-82.357,76.636,-82.493,76.698,-82.53,76.723,-82.393,76.69,-82.311,76.655,-82.218,76.64,-82.114,76.643,-82.034,76.629,-81.823,76.521,-81.717,76.495,-81.592,76.484,-81.474,76.488,-81.365,76.504,-81.171,76.513,-81.074,76.498,-80.975,76.47,-80.832,76.409,-80.835,76.369,-80.901,76.322,-80.955,76.27,-80.997,76.215,-80.963,76.184,-80.8,76.174,-80.69,76.176,-80.187,76.24,-79.954,76.251,-79.511,76.31,-79.286,76.355,-79.131,76.404,-78.934,76.451,-78.284,76.571,-78.119,76.644,-77.983,76.755,-77.999,76.852,-78.165,76.935,-78.289,76.978,-78.37,76.981,-78.456,76.967,-78.659,76.908,-78.792,76.884,-78.979,76.893,-79.221,76.936,-79.319,76.98,-79.274,77.026,-79.281,77.085,-79.341,77.158,-79.497,77.196,-79.924,77.194,-80.219,77.147,-80.274,77.151,-80.673,77.244,-80.798,77.259,-81.117,77.27,-81.277,77.257,-81.534,77.214,-81.756,77.204,-81.84,77.214,-81.968,77.248,-82.066,77.284,-82.057,77.297,-81.767,77.296,-81.523,77.311,-81.301,77.344,-81.278,77.365,-81.378,77.385,-81.504,77.43,-81.654,77.499,-81.659,77.525,-81.519,77.51,-81.377,77.482,-80.875,77.359,-80.573,77.315,-80.282,77.301,-79.906,77.3,-79.138,77.331,-78.87,77.333,-78.708,77.342,-78.493,77.369,-78.284,77.413,-78.168,77.458,-78.076,77.519,-78.047,77.615,-78.081,77.747,-78.084,77.846,-78.056,77.912,-78.013,77.946,-77.456,77.947,-76.974,77.927,-76.708,77.938,-76.356,77.991,-76.078,77.987,-75.97,77.993,-75.866,78.01,-75.551,78.221,-75.193,78.328,-75.237,78.356,-75.488,78.404,-76.137,78.492,-76.416,78.512,-76.373,78.521,-75.966,78.53,-75.397,78.523,-74.879,78.545,-74.547,78.62,-74.535,78.659,-74.433,78.724,-74.486,78.75,-74.618,78.758,-75.099,78.858,-75.4,78.881,-75.795,78.89,-75.953,78.959,-76.077,78.985,-76.256,79.007,-76.524,79.024,-76.825,79.018,-77.51,78.978,-77.698,78.955,-77.883,78.942,-78.037,78.964,-78.222,79.015,-78.422,79.048,-78.559,79.055,-78.582,79.075,-78.258,79.082,-77.974,79.076,-77.729,79.057,-77.398,79.057,-76.771,79.087,-76.531,79.087,-76.38,79.104,-76.158,79.1,-75.912,79.118,-75.639,79.088,-75.515,79.061,-75.233,79.036,-74.641,79.036,-74.532,79.053,-74.481,79.229,-74.727,79.235,-75.094,79.204,-75.354,79.228,-75.603,79.24,-75.948,79.311,-76.116,79.326,-76.296,79.414,-76.671,79.478,-76.855,79.488,-76.899,79.512,-76.376,79.494,-76.067,79.473,-75.774,79.431,-75.503,79.414,-75.259,79.421,-74.798,79.459,-74.406,79.454,-74.189,79.465,-74.015,79.491,-73.866,79.501,-73.466,79.495,-73.362,79.504,-73.294,79.522,-73.24,79.552,-73.201,79.597,-73.229,79.644,-73.406,79.732,-73.472,79.756,-73.642,79.771,-74.051,79.778,-74.541,79.816,-74.66,79.835,-74.394,79.874,-74.144,79.88,-73.805,79.846,-73.448,79.827,-72.437,79.694,-72.216,79.687,-71.965,79.701,-71.388,79.762,-71.299,79.783,-71.11,79.848,-71.106,79.876,-71.278,79.906,-71.356,79.911,-70.758,79.998,-70.559,80.071,-70.568,80.094,-70.758,80.119,-70.877,80.122,-71.616,80.071,-71.949,80.086,-72.063,80.106,-72.056,80.123,-71.928,80.139,-71.796,80.143,-71.661,80.136,-71.47,80.146,-71.1,80.187,-70.265,80.234,-70.213,80.278,-70.668,80.506,-70.713,80.54,-70.639,80.528,-70.403,80.459,-70.144,80.398,-69.949,80.374,-69.734,80.367,-69.551,80.383,-69.4,80.423,-68.959,80.587,-68.63,80.679,-67.774,80.859,-66.727,81.041,-66.313,81.146,-65.484,81.285,-64.833,81.439,-64.78,81.493,-65.24,81.51,-65.736,81.494,-68.318,81.261,-68.543,81.248,-68.721,81.261]},{"b":[-136.444,68.684,-120.682,70.574],"l":[-120.682,69.567,-120.815,69.617,-120.962,69.66,-121.336,69.742,-121.531,69.776,-121.742,69.798,-122.07,69.816,-122.387,69.808,-122.705,69.817,-122.785,69.808,-122.957,69.819,-123.026,69.81,-123.077,69.782,-123.11,69.738,-123.144,69.632,-123.214,69.542,-123.249,69.52,-123.361,69.497,-123.46,69.42,-123.528,69.389,-123.609,69.377,-124.05,69.373,-124.112,69.359,-124.338,69.365,-124.426,69.379,-124.472,69.4,-124.481,69.425,-124.454,69.455,-124.398,69.494,-124.138,69.653,-124.125,69.69,-124.349,69.735,-124.407,69.767,-124.472,69.919,-124.467,69.983,-124.442,70.062,-124.444,70.111,-124.503,70.141,-124.555,70.151,-124.64,70.141,-124.706,70.117,-124.745,70.08,-124.952,70.042,-124.99,70.027,-124.963,70.013,-124.92,70.006,-124.863,70.006,-124.768,69.99,-124.794,69.969,-124.889,69.936,-124.968,69.894,-125.031,69.844,-125.08,69.818,-125.114,69.815,-125.201,69.829,-125.228,69.757,-125.219,69.732,-125.346,69.662,-125.357,69.626,-125.262,69.566,-125.167,69.48,-125.172,69.428,-125.387,69.349,-125.525,69.352,-125.728,69.38,-125.907,69.419,-126.064,69.467,-126.25,69.545,-126.612,69.73,-126.685,69.777,-126.759,69.853,-126.833,69.959,-126.927,70.062,-127.138,70.239,-127.226,70.296,-127.377,70.369,-127.753,70.517,-127.862,70.549,-127.991,70.574,-128.04,70.566,-128.127,70.524,-128.168,70.48,-128.17,70.418,-128.121,70.397,-127.989,70.363,-128.044,70.329,-128.034,70.315,-127.974,70.293,-127.684,70.26,-127.765,70.222,-128.096,70.161,-128.279,70.108,-128.359,69.988,-128.387,69.96,-128.706,69.81,-128.853,69.751,-128.971,69.712,-129.054,69.701,-129.102,69.717,-129.136,69.75,-129.158,69.8,-129.138,69.833,-128.939,69.875,-128.917,69.895,-128.884,69.963,-128.899,69.966,-128.984,69.933,-129.033,69.905,-129.109,69.882,-129.265,69.855,-129.572,69.827,-130.118,69.72,-130.354,69.656,-130.516,69.57,-130.661,69.481,-130.875,69.32,-130.914,69.285,-130.971,69.209,-130.982,69.253,-130.986,69.363,-131.013,69.429,-131.063,69.451,-131.113,69.459,-131.162,69.455,-131.209,69.432,-131.294,69.364,-131.325,69.361,-131.303,69.415,-131.343,69.435,-131.563,69.461,-131.632,69.459,-131.788,69.432,-131.82,69.402,-131.781,69.389,-131.787,69.371,-131.833,69.336,-131.92,69.291,-132.134,69.234,-132.214,69.202,-132.358,69.167,-132.545,69.141,-132.719,69.079,-132.755,69.042,-132.77,69.012,-132.765,68.972,-132.739,68.922,-132.704,68.896,-132.542,68.89,-132.533,68.876,-132.578,68.848,-132.706,68.815,-133.304,68.847,-133.337,68.835,-133.32,68.82,-133.192,68.777,-133.138,68.747,-133.197,68.74,-133.348,68.77,-133.373,68.788,-133.418,68.844,-133.379,68.887,-133.228,68.967,-133.089,69.029,-132.968,69.101,-132.817,69.206,-132.687,69.26,-132.481,69.273,-132.331,69.308,-132.197,69.365,-132.129,69.402,-131.989,69.518,-131.938,69.535,-131.863,69.549,-131.473,69.579,-131.306,69.597,-131.208,69.616,-130.96,69.632,-130.832,69.651,-130.709,69.686,-130.459,69.78,-129.648,69.998,-129.538,70.074,-129.538,70.105,-129.623,70.168,-129.676,70.193,-129.73,70.192,-129.898,70.106,-129.945,70.091,-130.043,70.095,-130.175,70.086,-130.275,70.098,-130.396,70.129,-130.498,70.143,-130.665,70.127,-130.926,70.052,-130.991,70.018,-131.032,69.979,-131.136,69.907,-131.216,69.901,-131.319,69.924,-131.441,69.918,-131.582,69.882,-131.934,69.753,-132.163,69.705,-132.232,69.708,-132.334,69.752,-132.488,69.738,-132.538,69.727,-132.571,69.707,-132.568,69.698,-132.479,69.693,-132.413,69.674,-132.404,69.659,-132.452,69.647,-132.527,69.643,-132.84,69.651,-132.915,69.63,-133.028,69.508,-133.084,69.471,-133.163,69.434,-133.294,69.412,-133.476,69.405,-133.694,69.368,-133.948,69.301,-134.174,69.253,-134.165,69.281,-134.018,69.388,-133.947,69.429,-133.88,69.508,-133.9,69.528,-134.077,69.558,-134.134,69.587,-134.19,69.639,-134.242,69.669,-134.409,69.682,-134.451,69.665,-134.474,69.633,-134.495,69.572,-134.491,69.545,-134.457,69.478,-134.494,69.468,-134.853,69.486,-135.141,69.468,-135.199,69.45,-135.23,69.425,-135.255,69.324,-135.293,69.308,-135.5,69.337,-135.615,69.291,-135.691,69.311,-135.91,69.111,-135.85,69.081,-135.743,69.049,-135.651,69.031,-135.576,69.027,-135.59,69.008,-135.695,69.001,-135.873,69.001,-135.925,68.993,-135.939,68.974,-135.895,68.927,-135.876,68.917,-135.638,68.892,-135.435,68.842,-135.407,68.829,-135.231,68.694,-135.259,68.684,-135.362,68.696,-135.867,68.833,-136.122,68.882,-136.444,68.895]},{"b":[-168.088,-53.883,-34.805,71.987],"l":[-136.444,68.895,-136.499,68.897,-136.717,68.889,-137.07,68.951,-137.26,68.964,-137.869,69.093,-138.128,69.152,-138.291,69.219,-138.69,69.317,-139.182,69.516,-139.977,69.622,-140.405,69.602,-140.86,69.635,-141.002,69.651,-141.081,69.659,-141.29,69.665,-141.339,69.647,-141.408,69.653,-141.526,69.715,-141.699,69.77,-142.297,69.87,-142.422,69.94,-142.708,70.034,-143.218,70.116,-143.276,70.095,-143.357,70.09,-143.566,70.101,-143.746,70.102,-144.064,70.054,-144.417,70.039,-144.619,69.982,-145.197,70.009,-145.237,70.034,-145.44,70.051,-145.823,70.16,-146.058,70.156,-146.281,70.186,-146.745,70.192,-147.063,70.17,-147.705,70.217,-147.791,70.24,-147.87,70.303,-148.039,70.315,-148.143,70.355,-148.249,70.357,-148.371,70.315,-148.479,70.318,-148.688,70.416,-148.845,70.425,-149.269,70.501,-149.411,70.491,-149.544,70.513,-149.87,70.51,-150.152,70.444,-150.274,70.434,-150.403,70.444,-150.544,70.49,-150.663,70.51,-150.979,70.465,-151.128,70.452,-151.225,70.419,-151.945,70.452,-151.82,70.511,-151.8,70.538,-151.769,70.56,-152.173,70.557,-152.253,70.568,-152.27,70.615,-152.399,70.62,-152.471,70.654,-152.437,70.733,-152.233,70.81,-152.3,70.847,-152.491,70.881,-152.671,70.891,-152.785,70.876,-153.233,70.933,-153.498,70.891,-153.701,70.894,-153.918,70.877,-154.195,70.801,-154.392,70.838,-154.599,70.848,-154.785,70.894,-154.726,70.928,-154.674,70.987,-154.818,71.048,-154.944,71.083,-155.167,71.099,-155.23,71.082,-155.313,71.015,-155.579,70.894,-155.708,70.857,-155.872,70.835,-155.974,70.842,-156.042,70.902,-156.147,70.928,-155.804,70.995,-155.635,71.062,-155.579,71.121,-155.646,71.183,-155.811,71.188,-156.47,71.292,-156.567,71.342,-156.497,71.379,-156.395,71.397,-156.47,71.408,-156.783,71.319,-156.973,71.23,-157.195,71.093,-157.325,71.04,-157.606,70.941,-157.909,70.86,-157.998,70.845,-158.484,70.841,-158.511,70.82,-158.621,70.799,-158.996,70.802,-159.075,70.772,-159.251,70.748,-159.304,70.753,-159.34,70.781,-159.262,70.814,-159.183,70.832,-159.192,70.86,-159.232,70.877,-159.315,70.879,-159.681,70.787,-160.082,70.635,-159.962,70.634,-159.746,70.53,-159.387,70.525,-159.683,70.477,-159.815,70.497,-159.843,70.453,-159.858,70.389,-159.855,70.324,-159.866,70.279,-159.908,70.331,-160.095,70.333,-160.006,70.448,-160.106,70.473,-159.963,70.568,-160.046,70.586,-160.117,70.591,-160.634,70.446,-160.648,70.421,-160.996,70.305,-161.639,70.235,-161.768,70.197,-161.997,70.165,-162.074,70.162,-162.042,70.177,-161.912,70.205,-161.818,70.248,-161.761,70.258,-161.78,70.277,-161.813,70.29,-161.881,70.332,-161.978,70.288,-162.071,70.227,-162.35,70.094,-162.952,69.758,-163.094,69.611,-163.131,69.454,-163.161,69.388,-163.187,69.38,-163.205,69.393,-163.251,69.345,-163.536,69.17,-163.868,69.037,-164.15,68.961,-164.302,68.936,-164.89,68.902,-165.044,68.882,-165.509,68.868,-166.209,68.885,-166.182,68.797,-166.283,68.573,-166.381,68.425,-166.447,68.39,-166.57,68.361,-166.648,68.374,-166.546,68.424,-166.644,68.408,-166.786,68.36,-166.574,68.32,-166.409,68.308,-166.236,68.278,-165.96,68.156,-165.386,68.046,-164.125,67.607,-163.943,67.478,-163.8,67.271,-163.721,67.196,-163.532,67.103,-163.002,67.027,-162.761,67.036,-162.583,67.019,-162.409,67.104,-162.412,67.06,-162.392,67.02,-161.965,67.05,-161.72,67.021,-161.622,66.979,-161.731,66.923,-161.879,66.804,-161.857,66.7,-161.681,66.646,-161.572,66.592,-161.398,66.552,-161.051,66.653,-160.864,66.671,-160.644,66.605,-160.361,66.613,-160.263,66.572,-160.227,66.509,-160.232,66.42,-160.651,66.373,-160.784,66.384,-161.048,66.474,-161.156,66.495,-161.336,66.496,-161.591,66.46,-161.91,66.56,-162.051,66.667,-162.018,66.784,-162.131,66.801,-162.254,66.919,-162.362,66.947,-162.478,66.931,-162.607,66.894,-162.544,66.805,-162.467,66.736,-162.318,66.734,-162.191,66.693,-161.888,66.493,-161.917,66.412,-161.828,66.371,-161.544,66.407,-161.12,66.334,-161.07,66.295,-161.034,66.189,-161.109,66.24,-161.201,66.219,-161.345,66.247,-161.455,66.281,-161.557,66.251,-161.816,66.054,-161.934,66.043,-162.214,66.071,-162.587,66.051,-162.722,66.06,-162.886,66.099,-163.171,66.075,-163.695,66.084,-164.034,66.216,-163.965,66.257,-163.894,66.287,-163.903,66.378,-163.794,66.493,-163.775,66.531,-163.838,66.562,-163.894,66.576,-163.816,66.583,-163.638,66.575,-163.728,66.616,-164.058,66.611,-164.46,66.588,-164.674,66.555,-165.064,66.438,-165.198,66.44,-165.449,66.41,-165.776,66.319,-165.812,66.288,-165.84,66.245,-165.56,66.167,-165.59,66.145,-165.63,66.131,-165.724,66.113,-166.009,66.121,-166.057,66.127,-166.215,66.17,-166.399,66.144,-166.54,66.101,-166.748,66.052,-166.894,65.959,-166.997,65.905,-167.074,65.877,-167.405,65.859,-167.58,65.758,-167.914,65.681,-167.927,65.714,-167.931,65.748,-168.01,65.719,-168.088,65.658,-168.035,65.596,-167.987,65.568,-167.404,65.422,-166.665,65.338,-166.609,65.353,-166.197,65.306,-166.157,65.286,-166.121,65.261,-166.28,65.274,-166.452,65.247,-166.531,65.155,-166.763,65.135,-166.857,65.147,-166.906,65.164,-166.928,65.157,-166.827,65.096,-166.551,64.953,-166.415,64.927,-166.409,64.827,-166.478,64.798,-166.481,64.728,-166.325,64.626,-166.143,64.583,-165.446,64.513,-165.138,64.465,-164.979,64.454,-164.9,64.461,-164.857,64.48,-164.83,64.511,-164.765,64.53,-164.727,64.523,-164.692,64.507,-164.304,64.584,-163.713,64.588,-163.486,64.55,-163.267,64.475,-163.144,64.424,-163.104,64.479,-163.052,64.52,-163.174,64.533,-163.248,64.563,-163.303,64.606,-163.204,64.652,-162.876,64.516,-162.807,64.374,-162.711,64.378,-162.636,64.451,-162.335,64.613,-162.172,64.678,-161.868,64.743,-161.759,64.816,-161.634,64.792,-161.466,64.795,-161.187,64.924,-161.13,64.925,-161.063,64.904,-160.967,64.84,-160.887,64.796,-160.856,64.756,-160.836,64.682,-160.894,64.613,-160.932,64.579,-161.049,64.534,-161.193,64.516,-161.415,64.526,-161.491,64.434,-161.386,64.44,-161.22,64.397,-160.988,64.251,-160.904,64.031,-160.84,63.935,-160.779,63.819,-160.827,63.729,-160.927,63.661,-161.1,63.558,-161.266,63.497,-161.505,63.468,-161.974,63.453,-162.056,63.471,-162.112,63.534,-162.193,63.541,-162.283,63.529,-162.36,63.453,-162.621,63.266,-162.808,63.207,-162.948,63.115,-163.062,63.08,-163.288,63.046,-163.359,63.046,-163.423,63.085,-163.504,63.106,-163.649,63.057,-163.738,63.016,-163.749,63.03,-163.726,63.048,-163.664,63.07,-163.634,63.09,-163.616,63.125,-163.736,63.193,-163.943,63.247,-164.108,63.262,-164.409,63.215,-164.463,63.185,-164.525,63.128,-164.375,63.054,-164.384,63.03,-164.428,63.04,-164.677,63.02,-164.764,62.971,-164.8,62.918,-164.845,62.801,-164.819,62.677,-164.793,62.623,-164.689,62.677,-164.589,62.709,-164.596,62.687,-164.688,62.608,-164.844,62.581,-164.796,62.512,-164.758,62.497,-164.779,62.481,-164.892,62.518,-165,62.534,-165.116,62.513,-165.195,62.474,-165.448,62.304,-165.707,62.1,-165.744,62.012,-165.725,61.959,-165.706,61.927,-165.613,61.869,-165.834,61.837,-165.991,61.834,-166.079,61.803,-166.02,61.748,-165.809,61.696,-165.835,61.679,-166.1,61.645,-166.131,61.657,-166.168,61.651,-166.164,61.589,-166.153,61.546,-166.094,61.507,-165.961,61.551,-165.845,61.536,-165.797,61.491,-165.906,61.404,-165.864,61.336,-165.691,61.3,-165.628,61.165,-165.566,61.102,-165.48,61.095,-165.381,61.106,-165.379,61.169,-165.392,61.212,-165.334,61.266,-165.274,61.275,-165.244,61.269,-165.311,61.228,-165.345,61.198,-165.28,61.17,-165.204,61.153,-165.15,61.187,-165.128,61.192,-165.138,61.13,-165.077,61.094,-164.941,61.115,-164.869,61.112,-164.876,61.087,-165,61.044,-165.175,60.966,-165.115,60.933,-165.066,60.921,-164.754,60.931,-164.442,60.87,-163.995,60.865,-163.749,60.97,-163.659,60.938,-163.587,60.903,-163.655,60.877,-163.837,60.88,-163.907,60.854,-163.623,60.822,-163.512,60.798,-163.421,60.757,-163.529,60.665,-163.73,60.59,-163.821,60.668,-163.895,60.745,-163.936,60.758,-164,60.766,-164.132,60.692,-164.31,60.607,-164.372,60.592,-164.321,60.647,-164.266,60.725,-164.319,60.771,-164.37,60.796,-164.513,60.819,-164.682,60.872,-164.805,60.892,-164.9,60.873,-165.016,60.74,-165.354,60.541,-165.225,60.524,-165.113,60.526,-165.027,60.501,-165.049,60.464,-165.061,60.413,-164.92,60.348,-164.8,60.307,-164.662,60.304,-164.471,60.149,-164.132,59.994,-164.141,59.949,-164.143,59.897,-163.907,59.807,-163.68,59.802,-163.219,59.846,-162.878,59.923,-162.733,59.994,-162.571,59.99,-162.536,60.038,-162.5,60.127,-162.527,60.199,-162.548,60.231,-162.685,60.269,-162.6,60.297,-162.469,60.395,-162.265,60.595,-162.2,60.634,-162.138,60.686,-162.068,60.695,-161.962,60.695,-161.947,60.685,-162.139,60.614,-162.288,60.457,-162.421,60.284,-162.242,60.178,-162.138,59.98,-161.909,59.714,-161.829,59.589,-161.832,59.515,-161.872,59.428,-161.92,59.365,-162.023,59.284,-161.981,59.146,-161.891,59.076,-161.794,59.109,-161.644,59.11,-161.789,59.016,-161.79,58.95,-161.781,58.897,-161.724,58.794,-161.856,58.717,-162.009,58.685,-162.145,58.644,-161.755,58.612,-161.361,58.67,-161.328,58.744,-161.288,58.761,-161.247,58.799,-161.216,58.801,-160.924,58.872,-160.817,58.872,-160.657,58.955,-160.52,59.007,-160.363,59.051,-160.261,58.972,-160.153,58.906,-159.92,58.82,-159.832,58.836,-159.741,58.894,-159.67,58.911,-159.454,58.793,-159.358,58.721,-159.083,58.47,-158.951,58.405,-158.789,58.441,-158.772,58.52,-158.861,58.719,-158.838,58.794,-158.776,58.903,-158.809,58.974,-158.761,58.95,-158.678,58.929,-158.584,58.988,-158.514,59.073,-158.423,59.09,-158.221,59.038,-158.081,58.977,-158.189,58.98,-158.315,59.009,-158.426,58.999,-158.476,58.938,-158.503,58.85,-158.439,58.783,-158.39,58.746,-158.303,58.642,-158.191,58.614,-158.022,58.64,-157.666,58.748,-157.142,58.878,-156.963,58.989,-156.809,59.134,-156.923,58.964,-157.04,58.773,-157.009,58.744,-156.975,58.736,-157.229,58.641,-157.461,58.503,-157.524,58.421,-157.524,58.351,-157.488,58.254,-157.394,58.235,-157.339,58.235,-157.194,58.194,-157.443,58.172,-157.555,58.14,-157.611,58.051,-157.621,57.895,-157.684,57.744,-157.697,57.679,-157.681,57.638,-157.608,57.601,-157.572,57.541,-157.533,57.526,-157.474,57.518,-157.462,57.506,-157.535,57.483,-157.646,57.498,-157.674,57.514,-157.697,57.539,-157.737,57.548,-157.846,57.528,-157.894,57.511,-158.046,57.409,-158.134,57.366,-158.225,57.343,-158.321,57.298,-158.474,57.199,-158.586,57.114,-158.661,57.039,-158.677,56.997,-158.685,56.944,-158.681,56.888,-158.666,56.828,-158.675,56.795,-158.709,56.789,-158.782,56.796,-158.895,56.816,-158.918,56.847,-158.918,56.882,-158.99,56.86,-159.159,56.77,-159.283,56.689,-159.785,56.562,-160.046,56.437,-160.149,56.396,-160.302,56.314,-160.377,56.241,-160.461,56.138,-160.515,56.059,-160.539,56.006,-160.527,55.965,-160.48,55.935,-160.308,55.864,-160.27,55.832,-160.292,55.805,-160.347,55.8,-160.437,55.817,-160.498,55.838,-160.53,55.863,-160.6,55.874,-160.706,55.87,-160.758,55.855,-160.746,55.771,-160.763,55.757,-160.803,55.754,-160.851,55.772,-161.005,55.887,-161.008,55.912,-160.902,55.941,-160.878,55.971,-160.899,55.994,-160.969,55.97,-161.145,55.951,-161.193,55.954,-161.223,55.977,-161.179,56.014,-161.216,56.021,-161.697,55.907,-161.937,55.824,-162.157,55.719,-162.349,55.595,-162.513,55.45,-162.659,55.351,-162.786,55.297,-162.857,55.253,-162.872,55.219,-162.907,55.196,-162.962,55.184,-163.008,55.187,-163.045,55.205,-163.115,55.194,-163.279,55.122,-163.304,55.096,-163.306,55.059,-163.286,55.01,-163.296,54.949,-163.338,54.876,-163.335,54.839,-163.289,54.838,-163.221,54.863,-163.131,54.917,-163.1,54.974,-163.128,55.035,-163.12,55.065,-162.996,55.046,-162.865,54.955,-162.82,54.95,-162.674,54.997,-162.619,55.038,-162.614,55.071,-162.644,55.218,-162.63,55.247,-162.542,55.243,-162.495,55.208,-162.427,55.145,-162.413,55.107,-162.452,55.093,-162.457,55.078,-162.428,55.061,-162.386,55.052,-162.333,55.05,-162.275,55.073,-162.211,55.121,-162.167,55.144,-162.074,55.139,-161.98,55.199,-161.742,55.391,-161.72,55.421,-161.684,55.53,-161.654,55.563,-161.599,55.593,-161.517,55.618,-161.459,55.629,-161.357,55.612,-161.255,55.579,-161.215,55.56,-161.202,55.544,-161.313,55.559,-161.373,55.556,-161.413,55.536,-161.444,55.513,-161.477,55.465,-161.481,55.398,-161.464,55.383,-161.382,55.371,-161.178,55.389,-161.1,55.406,-161.024,55.44,-160.952,55.493,-160.897,55.514,-160.771,55.484,-160.727,55.5,-160.683,55.54,-160.625,55.552,-160.554,55.535,-160.499,55.537,-160.463,55.558,-160.407,55.614,-160.373,55.635,-160.244,55.661,-160.046,55.763,-159.962,55.795,-159.914,55.792,-159.874,55.8,-159.81,55.833,-159.771,55.841,-159.743,55.844,-159.679,55.825,-159.665,55.795,-159.67,55.645,-159.66,55.626,-159.61,55.653,-159.568,55.695,-159.541,55.748,-159.523,55.81,-159.429,55.843,-158.79,55.987,-158.705,56.043,-158.627,56.155,-158.591,56.185,-158.554,56.183,-158.543,56.167,-158.523,56.072,-158.505,56.062,-158.476,56.075,-158.432,56.111,-158.276,56.196,-158.291,56.204,-158.317,56.254,-158.344,56.28,-158.386,56.302,-158.467,56.318,-158.536,56.308,-158.552,56.313,-158.537,56.335,-158.414,56.436,-158.352,56.454,-158.189,56.478,-158.124,56.501,-158.071,56.51,-157.982,56.51,-157.93,56.52,-157.929,56.532,-157.978,56.543,-158.078,56.552,-158.028,56.592,-157.869,56.645,-157.771,56.652,-157.674,56.633,-157.61,56.628,-157.578,56.634,-157.529,56.673,-157.49,56.76,-157.441,56.79,-157.39,56.81,-157.334,56.816,-157.271,56.808,-157.206,56.812,-157.139,56.827,-157.067,56.86,-156.988,56.913,-156.923,56.942,-156.872,56.948,-156.824,56.969,-156.78,57.006,-156.713,57.016,-156.629,57.01,-156.592,57.065,-156.501,57.09,-156.475,57.105,-156.4,57.205,-156.398,57.241,-156.444,57.294,-156.474,57.311,-156.478,57.328,-156.436,57.36,-156.242,57.449,-156.156,57.463,-156.09,57.445,-156.055,57.448,-156.037,57.527,-156,57.545,-155.814,57.559,-155.778,57.568,-155.729,57.627,-155.629,57.673,-155.596,57.701,-155.59,57.734,-155.53,57.759,-155.414,57.777,-155.313,57.807,-155.147,57.882,-155.099,57.913,-155.007,58.016,-154.585,58.056,-154.582,58.11,-154.571,58.118,-154.409,58.147,-154.282,58.147,-154.247,58.159,-154.235,58.235,-154.208,58.289,-154.282,58.293,-154.289,58.304,-154.086,58.366,-154.056,58.397,-154.062,58.442,-154.02,58.493,-153.862,58.588,-153.821,58.604,-153.699,58.626,-153.617,58.655,-153.438,58.755,-153.363,58.822,-153.334,58.858,-153.327,58.884,-153.339,58.909,-153.418,58.96,-153.656,59.039,-153.788,59.068,-153.9,59.078,-154.13,59.12,-154.178,59.156,-154.139,59.24,-154.067,59.336,-154.088,59.363,-153.814,59.474,-153.753,59.51,-153.714,59.545,-153.622,59.598,-153.609,59.615,-153.671,59.635,-153.653,59.647,-153.483,59.721,-153.414,59.74,-153.366,59.73,-153.36,59.717,-153.383,59.667,-153.364,59.66,-153.236,59.671,-153.094,59.709,-153.048,59.73,-153.025,59.794,-153.04,59.81,-153.211,59.843,-153.186,59.857,-153.106,59.875,-152.857,59.898,-152.759,59.921,-152.66,59.997,-152.629,60.041,-152.63,60.084,-152.665,60.125,-152.752,60.177,-152.893,60.24,-153.031,60.289,-153.025,60.296,-152.923,60.293,-152.798,60.247,-152.727,60.237,-152.654,60.238,-152.541,60.265,-152.369,60.336,-152.292,60.381,-152.26,60.409,-152.305,60.453,-152.307,60.472,-152.271,60.528,-151.996,60.682,-151.866,60.734,-151.785,60.74,-151.75,60.755,-151.784,60.833,-151.782,60.858,-151.734,60.911,-151.594,60.98,-151.46,61.014,-151.282,61.042,-151.15,61.086,-151.065,61.146,-150.946,61.198,-150.612,61.301,-150.567,61.307,-150.533,61.3,-150.472,61.26,-150.109,61.268,-149.976,61.279,-149.945,61.294,-149.874,61.373,-149.824,61.413,-149.695,61.471,-149.625,61.486,-149.434,61.501,-149.329,61.497,-149.596,61.417,-149.737,61.363,-149.829,61.308,-149.882,61.264,-149.895,61.232,-149.927,61.213,-150.019,61.194,-150.053,61.171,-149.968,61.122,-149.592,60.994,-149.459,60.965,-149.142,60.936,-149.071,60.886,-149.075,60.876,-149.173,60.88,-149.632,60.952,-149.856,60.962,-149.998,60.935,-150.113,60.933,-150.203,60.955,-150.281,60.985,-150.349,61.023,-150.441,61.024,-150.779,60.915,-150.954,60.841,-151.322,60.743,-151.356,60.723,-151.355,60.66,-151.318,60.554,-151.313,60.466,-151.396,60.274,-151.451,60.203,-151.612,60.092,-151.735,59.988,-151.783,59.921,-151.853,59.782,-151.817,59.721,-151.764,59.7,-151.513,59.651,-151.45,59.65,-151.404,59.662,-151.089,59.789,-151.057,59.782,-151.046,59.772,-151.189,59.638,-151.262,59.586,-151.4,59.516,-151.513,59.483,-151.693,59.462,-151.85,59.406,-151.885,59.386,-151.932,59.343,-151.964,59.285,-151.95,59.265,-151.904,59.26,-151.738,59.189,-151.619,59.187,-151.477,59.231,-151.366,59.246,-151.287,59.232,-151.222,59.229,-151.171,59.237,-151.163,59.257,-151.199,59.29,-151.183,59.301,-151.064,59.278,-150.961,59.244,-150.935,59.249,-150.899,59.303,-150.853,59.342,-150.677,59.427,-150.623,59.48,-150.621,59.535,-150.607,59.563,-150.582,59.565,-150.526,59.537,-150.485,59.535,-150.338,59.581,-150.296,59.583,-150.258,59.571,-150.198,59.567,-149.967,59.69,-149.96,59.713,-150.016,59.777,-150.005,59.784,-149.965,59.782,-149.801,59.738,-149.782,59.75,-149.804,59.833,-149.795,59.856,-149.714,59.92,-149.685,59.895,-149.63,59.785,-149.613,59.767,-149.598,59.77,-149.549,59.894,-149.46,59.966,-149.432,60.001,-149.415,60.1,-149.395,60.106,-149.305,60.014,-149.267,59.998,-149.122,60.033,-149.07,60,-149.004,59.98,-148.843,59.951,-148.751,59.948,-148.644,59.957,-148.574,59.97,-148.542,59.987,-148.506,59.989,-148.465,59.975,-148.431,59.989,-148.333,60.122,-148.291,60.145,-148.245,60.147,-148.214,60.154,-148.198,60.168,-148.219,60.268,-148.216,60.323,-148.204,60.365,-148.182,60.393,-148.129,60.414,-148.046,60.428,-147.991,60.452,-147.964,60.485,-147.984,60.523,-148.051,60.567,-148.119,60.575,-148.189,60.547,-148.296,60.532,-148.44,60.53,-148.549,60.515,-148.624,60.486,-148.64,60.489,-148.597,60.524,-148.51,60.565,-148.468,60.572,-148.338,60.57,-148.305,60.583,-148.284,60.609,-148.257,60.675,-148.268,60.7,-148.341,60.724,-148.399,60.734,-148.557,60.803,-148.556,60.827,-148.471,60.835,-148.393,60.832,-148.344,60.854,-148.293,60.94,-148.209,61.03,-148.226,61.044,-148.287,61.036,-148.396,61.007,-148.411,61.011,-148.389,61.037,-148.342,61.06,-148.27,61.082,-148.209,61.088,-148.158,61.08,-148.049,61.083,-147.986,61.106,-147.845,61.186,-147.774,61.218,-147.752,61.219,-147.971,61.019,-148.005,60.969,-147.991,60.948,-147.891,60.89,-147.808,60.885,-147.656,60.91,-147.623,60.933,-147.593,60.979,-147.567,60.995,-147.523,60.97,-147.433,60.95,-147.391,60.918,-147.361,60.915,-147.321,60.925,-147.286,60.947,-147.255,60.978,-147.195,60.997,-147.106,61.003,-147.034,60.996,-146.98,60.978,-146.874,61.005,-146.716,61.078,-146.583,61.128,-146.384,61.136,-146.285,61.113,-146.599,61.054,-146.636,60.993,-146.638,60.897,-146.604,60.871,-146.532,60.839,-146.392,60.811,-146.496,60.757,-146.546,60.745,-146.57,60.729,-146.503,60.701,-146.347,60.738,-146.251,60.749,-146.182,60.735,-146.167,60.716,-146.166,60.692,-146.149,60.661,-145.675,60.651,-145.69,60.622,-145.76,60.562,-145.811,60.525,-145.899,60.478,-145.848,60.469,-145.718,60.468,-145.563,60.441,-145.382,60.389,-145.248,60.38,-145.163,60.415,-145.096,60.454,-144.984,60.537,-144.863,60.601,-144.724,60.663,-144.691,60.669,-144.787,60.585,-144.824,60.534,-144.862,60.459,-144.901,60.335,-144.852,60.295,-144.741,60.273,-144.672,60.249,-144.643,60.225,-144.53,60.205,-144.333,60.191,-144.185,60.151,-144.089,60.084,-144.084,60.063,-144.161,60.046,-144.147,60.016,-143.979,60.009,-143.805,60.013,-143.506,60.055,-142.946,60.097,-142.549,60.086,-142.104,60.033,-141.67,59.97,-141.53,59.995,-141.447,60.019,-141.41,60.042,-141.422,60.085,-141.422,60.109,-141.409,60.118,-141.362,60.105,-141.33,60.083,-141.29,60.004,-141.295,59.98,-141.408,59.903,-141.332,59.874,-140.843,59.749,-140.648,59.723,-140.42,59.711,-140.217,59.727,-139.917,59.806,-139.85,59.831,-139.612,59.973,-139.519,60.017,-139.431,60.012,-139.402,60.001,-139.242,59.893,-138.988,59.835,-139.048,59.828,-139.179,59.84,-139.221,59.82,-139.246,59.782,-139.259,59.743,-139.266,59.663,-139.276,59.62,-139.287,59.611,-139.32,59.739,-139.315,59.848,-139.331,59.877,-139.447,59.957,-139.483,59.964,-139.512,59.954,-139.554,59.933,-139.569,59.912,-139.581,59.881,-139.582,59.848,-139.558,59.79,-139.506,59.726,-139.513,59.698,-139.612,59.61,-139.674,59.587,-139.766,59.566,-139.799,59.546,-139.773,59.527,-139.714,59.504,-139.577,59.462,-139.341,59.376,-138.884,59.237,-138.704,59.188,-138.515,59.166,-138.521,59.152,-138.56,59.129,-138.537,59.115,-138.451,59.11,-138.352,59.087,-138.241,59.047,-138.027,58.941,-137.961,58.891,-137.934,58.847,-137.864,58.786,-137.75,58.707,-137.661,58.66,-137.597,58.644,-137.565,58.626,-137.557,58.59,-137.544,58.581,-137.072,58.395,-136.865,58.332,-136.699,58.266,-136.607,58.244,-136.583,58.245,-136.462,58.328,-136.13,58.35,-136.081,58.364,-136.056,58.384,-136.061,58.453,-136.103,58.506,-136.225,58.602,-136.32,58.624,-136.484,58.618,-136.404,58.68,-136.41,58.701,-136.533,58.74,-136.549,58.752,-136.568,58.786,-136.614,58.809,-136.74,58.85,-136.879,58.882,-136.963,58.884,-137.038,58.867,-137.059,58.874,-136.988,58.925,-136.948,58.935,-136.953,58.967,-137.002,59.021,-136.989,59.034,-136.831,58.984,-136.566,58.941,-136.511,58.907,-136.478,58.863,-136.451,58.846,-136.38,58.827,-136.299,58.787,-136.226,58.765,-136.186,58.77,-136.147,58.789,-136.124,58.82,-136.118,58.863,-136.124,58.893,-136.159,58.947,-136.15,59.048,-136.134,59.04,-136.101,59,-136.049,58.893,-136.017,58.874,-135.932,58.904,-135.826,58.898,-136.043,58.822,-136.046,58.789,-135.89,58.623,-135.862,58.577,-135.896,58.464,-135.898,58.4,-135.873,58.394,-135.572,58.412,-135.45,58.376,-135.363,58.298,-135.303,58.256,-135.142,58.233,-135.09,58.246,-135.06,58.279,-135.05,58.307,-135.062,58.341,-135.152,58.512,-135.185,58.59,-135.207,58.671,-135.257,58.778,-135.334,58.91,-135.386,59.088,-135.502,59.202,-135.434,59.211,-135.4,59.208,-135.417,59.242,-135.484,59.309,-135.413,59.318,-135.403,59.353,-135.364,59.419,-135.349,59.41,-135.358,59.325,-135.33,59.239,-135.217,59.077,-135.132,58.843,-135.076,58.797,-134.986,58.766,-134.965,58.742,-134.943,58.646,-134.776,58.454,-134.664,58.385,-134.485,58.367,-134.331,58.3,-134.258,58.244,-134.209,58.233,-134.131,58.279,-134.036,58.415,-133.944,58.498,-133.911,58.515,-133.877,58.518,-133.889,58.499,-133.934,58.468,-134.045,58.289,-134.063,58.211,-134.057,58.128,-134.031,58.072,-133.894,57.993,-133.821,57.936,-133.744,57.855,-133.722,57.844,-133.657,57.841,-133.626,57.857,-133.559,57.924,-133.497,57.925,-133.194,57.878,-133.212,57.866,-133.511,57.88,-133.536,57.864,-133.535,57.833,-133.515,57.775,-133.436,57.727,-133.117,57.566,-133.143,57.555,-133.342,57.631,-133.554,57.695,-133.603,57.695,-133.627,57.677,-133.649,57.642,-133.539,57.554,-133.437,57.337,-133.466,57.172,-132.913,57.047,-132.825,57.056,-132.814,57.041,-132.839,56.96,-132.83,56.931,-132.802,56.895,-132.702,56.822,-132.64,56.796,-132.487,56.766,-132.476,56.65,-132.434,56.634,-132.358,56.626,-132.337,56.603,-132.332,56.558,-132.305,56.52,-132.256,56.489,-132.182,56.421,-132.133,56.4,-132.022,56.38,-131.962,56.324,-131.927,56.273,-131.888,56.242,-131.844,56.23,-131.551,56.207,-131.738,56.161,-131.844,56.16,-132.006,55.93,-132.091,55.84,-132.158,55.781,-132.208,55.753,-132.223,55.721,-132.155,55.6,-132.119,55.57,-131.983,55.535,-131.945,55.554,-131.869,55.647,-131.834,55.735,-131.803,55.766,-131.799,55.783,-131.826,55.835,-131.815,55.854,-131.784,55.877,-131.635,55.932,-131.288,56.012,-131.033,56.088,-131.074,56.044,-131.14,55.998,-131.128,55.96,-130.977,55.812,-130.919,55.736,-130.88,55.612,-130.873,55.551,-130.88,55.46,-130.856,55.355,-130.835,55.332,-130.748,55.318,-130.75,55.297,-130.984,55.244,-131.046,55.18,-131.048,55.158,-130.98,55.061,-130.935,54.95,-130.85,54.808,-130.616,54.791,-130.575,54.77,-130.535,54.749,-130.349,54.815,-130.204,54.947,-130.214,55.026,-130.219,55.06,-130.172,55.137,-130.037,55.298,-130.039,55.344,-130.059,55.412,-130.12,55.524,-130.14,55.585,-130.147,55.654,-130.137,55.719,-130.112,55.78,-130.075,55.836,-130.025,55.888,-130.02,55.881,-130.06,55.814,-130.085,55.752,-130.095,55.695,-130.093,55.632,-130.08,55.563,-130.044,55.472,-129.985,55.359,-129.996,55.264,-130.058,55.195,-130.092,55.108,-130.048,55.057,-129.985,55.111,-129.877,55.251,-129.838,55.319,-129.816,55.418,-129.812,55.533,-129.795,55.56,-129.765,55.498,-129.734,55.458,-129.701,55.439,-129.667,55.437,-129.63,55.452,-129.561,55.463,-129.781,55.28,-129.89,55.165,-129.949,55.081,-130.109,54.887,-130.141,54.823,-130.219,54.73,-130.307,54.7,-130.35,54.655,-130.37,54.62,-130.389,54.539,-130.393,54.48,-130.43,54.421,-130.397,54.352,-130.29,54.27,-130.084,54.181,-129.898,54.226,-129.795,54.236,-129.626,54.23,-129.791,54.166,-130.043,54.134,-130.064,54.106,-130.086,53.976,-130.233,53.867,-130.335,53.724,-130.263,53.654,-130.074,53.576,-129.912,53.551,-129.822,53.413,-129.687,53.334,-129.564,53.251,-129.462,53.347,-129.284,53.393,-129.258,53.418,-129.24,53.479,-129.232,53.576,-129.208,53.642,-129.056,53.778,-129.014,53.797,-128.959,53.841,-128.944,53.84,-128.928,53.823,-128.89,53.83,-128.705,53.919,-128.651,53.919,-128.532,53.858,-128.56,53.845,-128.653,53.832,-128.715,53.81,-128.746,53.78,-128.764,53.747,-128.768,53.71,-128.751,53.661,-128.676,53.555,-128.6,53.506,-128.512,53.477,-128.47,53.471,-128.369,53.49,-128.207,53.483,-128.115,53.446,-127.95,53.33,-127.928,53.275,-128.079,53.369,-128.133,53.418,-128.291,53.458,-128.358,53.46,-128.479,53.41,-128.542,53.421,-128.833,53.549,-128.906,53.559,-128.85,53.665,-128.855,53.705,-128.936,53.715,-129.021,53.692,-129.114,53.641,-129.172,53.534,-129.13,53.442,-129.081,53.367,-128.869,53.328,-128.652,53.244,-128.525,53.141,-128.452,52.877,-128.365,52.826,-128.106,52.907,-128.053,52.911,-128.109,52.858,-128.132,52.806,-128.197,52.623,-128.275,52.435,-128.272,52.363,-128.241,52.368,-128.184,52.408,-128.038,52.531,-127.943,52.551,-127.94,52.545,-128.021,52.491,-128.052,52.453,-128.06,52.428,-128.029,52.342,-128.037,52.318,-128.358,52.159,-128.194,51.998,-128.102,51.788,-127.995,51.951,-127.902,52.151,-127.834,52.251,-127.792,52.289,-127.713,52.319,-127.56,52.343,-127.519,52.359,-127.107,52.633,-127.066,52.653,-127.035,52.682,-127.013,52.72,-127.006,52.755,-127.019,52.842,-127.008,52.843,-126.966,52.785,-126.951,52.751,-126.951,52.721,-126.995,52.658,-127.187,52.538,-127.208,52.498,-127.194,52.458,-127.161,52.395,-127.127,52.371,-126.938,52.309,-126.901,52.265,-126.895,52.225,-126.753,52.112,-126.714,52.061,-126.739,52.065,-126.826,52.125,-126.9,52.188,-126.959,52.255,-127.008,52.291,-127.176,52.315,-127.242,52.395,-127.438,52.356,-127.55,52.298,-127.673,52.253,-127.795,52.191,-127.843,52.086,-127.859,51.99,-127.728,51.993,-127.83,51.879,-127.863,51.821,-127.869,51.775,-127.851,51.673,-127.819,51.604,-127.748,51.544,-127.729,51.506,-127.714,51.49,-127.669,51.478,-127.645,51.478,-127.61,51.514,-127.576,51.563,-127.443,51.679,-127.339,51.707,-127.034,51.717,-126.691,51.703,-126.735,51.693,-126.968,51.67,-127.281,51.654,-127.347,51.642,-127.42,51.608,-127.633,51.427,-127.689,51.343,-127.714,51.269,-127.708,51.151,-127.591,51.088,-127.441,50.989,-127.357,50.946,-127.267,50.916,-127.058,50.868,-127.014,50.867,-126.96,50.894,-126.632,50.915,-126.563,50.965,-126.517,51.057,-126.485,50.96,-126.522,50.866,-126.488,50.842,-126.418,50.85,-126.375,50.837,-126.397,50.807,-126.472,50.767,-126.517,50.724,-126.514,50.679,-126.493,50.672,-126.37,50.667,-125.981,50.711,-125.904,50.705,-125.898,50.684,-126.067,50.664,-126.239,50.624,-126.416,50.607,-126.447,50.588,-126.45,50.55,-126.404,50.53,-126.237,50.523,-126.094,50.498,-126.024,50.497,-125.965,50.487,-125.84,50.511,-125.772,50.508,-125.741,50.479,-125.698,50.465,-125.641,50.466,-125.61,50.486,-125.586,50.574,-125.556,50.635,-125.539,50.649,-125.526,50.534,-125.507,50.507,-125.476,50.497,-125.21,50.476,-125.059,50.514,-124.985,50.592,-124.943,50.666,-124.931,50.718,-124.949,50.765,-124.934,50.811,-124.86,50.872,-124.875,50.826,-124.858,50.717,-124.854,50.669,-124.863,50.637,-124.937,50.537,-125.057,50.419,-125.044,50.364,-124.986,50.356,-124.933,50.298,-124.934,50.258,-124.784,50.073,-124.782,50.02,-124.702,49.958,-124.483,49.808,-124.413,49.778,-124.281,49.772,-124.142,49.793,-124.059,49.854,-123.985,49.876,-123.972,49.892,-123.971,49.97,-123.957,49.993,-123.866,50.072,-123.863,50.103,-123.946,50.184,-123.934,50.188,-123.88,50.174,-123.825,50.144,-123.788,50.107,-123.785,50.088,-123.824,50.044,-123.885,50.017,-123.904,49.981,-123.904,49.795,-123.874,49.737,-123.818,49.685,-123.763,49.658,-123.708,49.657,-123.582,49.681,-123.613,49.658,-123.739,49.594,-123.817,49.587,-123.847,49.637,-123.923,49.718,-123.96,49.736,-123.993,49.736,-124.024,49.711,-124.054,49.662,-124.029,49.603,-123.948,49.535,-123.892,49.495,-123.859,49.483,-123.531,49.397,-123.508,49.402,-123.437,49.451,-123.399,49.442,-123.336,49.459,-123.322,49.517,-123.337,49.545,-123.325,49.578,-123.187,49.68,-123.18,49.674,-123.191,49.644,-123.223,49.59,-123.248,49.443,-123.264,49.39,-123.286,49.375,-123.291,49.359,-123.277,49.344,-123.174,49.348,-123.016,49.322,-122.964,49.329,-122.879,49.399,-122.913,49.323,-122.948,49.293,-123.067,49.292,-123.184,49.278,-123.229,49.26,-123.191,49.22,-123.196,49.148,-123.182,49.129,-123.15,49.121,-123.08,49.131,-123.077,49.118,-123.109,49.085,-123.118,49.056,-123.086,48.993,-123.077,48.98,-123.063,48.978,-123.049,48.993,-123.027,49.039,-123.002,49.061,-122.963,49.075,-122.924,49.075,-122.827,49.028,-122.789,48.993,-122.722,48.853,-122.686,48.794,-122.653,48.764,-122.599,48.767,-122.58,48.78,-122.562,48.778,-122.545,48.762,-122.513,48.669,-122.515,48.555,-122.501,48.538,-122.497,48.506,-122.543,48.488,-122.628,48.498,-122.657,48.49,-122.669,48.465,-122.662,48.446,-122.638,48.433,-122.583,48.429,-122.542,48.411,-122.488,48.374,-122.409,48.294,-122.403,48.269,-122.467,48.258,-122.52,48.229,-122.529,48.199,-122.517,48.16,-122.494,48.13,-122.395,48.084,-122.387,48.09,-122.425,48.176,-122.416,48.184,-122.389,48.166,-122.353,48.114,-122.317,48.08,-122.261,48.042,-122.242,48.011,-122.318,47.933,-122.33,47.899,-122.393,47.821,-122.402,47.784,-122.382,47.752,-122.384,47.716,-122.407,47.677,-122.41,47.653,-122.381,47.628,-122.368,47.604,-122.375,47.528,-122.351,47.395,-122.354,47.372,-122.42,47.312,-122.465,47.296,-122.511,47.295,-122.542,47.276,-122.604,47.167,-122.627,47.144,-122.702,47.111,-122.73,47.112,-122.812,47.146,-122.914,47.131,-123.028,47.139,-122.988,47.173,-122.956,47.245,-122.92,47.29,-122.828,47.337,-122.813,47.329,-122.783,47.226,-122.768,47.218,-122.721,47.305,-122.708,47.316,-122.649,47.281,-122.604,47.275,-122.578,47.293,-122.554,47.405,-122.557,47.463,-122.586,47.528,-122.675,47.612,-122.664,47.617,-122.628,47.608,-122.614,47.616,-122.63,47.693,-122.618,47.713,-122.524,47.769,-122.511,47.816,-122.533,47.92,-122.586,47.928,-122.593,47.916,-122.588,47.856,-122.608,47.835,-122.718,47.762,-122.757,47.701,-122.814,47.659,-122.913,47.607,-122.982,47.559,-123.049,47.479,-123.06,47.454,-123.067,47.4,-123.018,47.401,-122.917,47.418,-122.922,47.408,-123.031,47.36,-123.104,47.348,-123.136,47.356,-123.139,47.386,-123.131,47.438,-123.051,47.552,-122.821,47.793,-122.805,47.784,-122.802,47.735,-122.778,47.738,-122.657,47.881,-122.679,47.932,-122.74,48.013,-122.769,48.076,-122.768,48.12,-122.779,48.138,-122.861,48.09,-122.909,48.077,-122.974,48.073,-123.024,48.082,-123.124,48.151,-123.162,48.155,-123.25,48.124,-123.294,48.12,-123.976,48.168,-124.099,48.2,-124.175,48.242,-124.429,48.301,-124.633,48.375,-124.71,48.38,-124.68,48.286,-124.702,48.152,-124.663,47.974,-124.621,47.904,-124.46,47.784,-124.376,47.659,-124.309,47.405,-124.199,47.209,-124.171,47.087,-124.164,47.015,-124.139,46.955,-124.117,47,-124.112,47.035,-124.042,47.03,-123.986,46.984,-123.843,46.963,-124.113,46.863,-124.072,46.745,-123.958,46.709,-123.889,46.66,-123.912,46.533,-123.946,46.433,-124.016,46.521,-124.044,46.605,-124.05,46.491,-124.045,46.373,-124.073,46.279,-123.96,46.301,-123.896,46.268,-123.688,46.3,-123.65,46.268,-123.465,46.271,-123.405,46.221,-123.299,46.171,-123.251,46.167,-123.221,46.154,-123.322,46.144,-123.402,46.155,-123.466,46.209,-123.522,46.223,-123.674,46.183,-123.912,46.182,-123.963,46.225,-123.989,46.219,-123.975,46.178,-123.947,46.141,-123.961,45.843,-123.929,45.577,-123.963,45.476,-123.949,45.401,-124.059,44.778,-124.045,44.648,-124.065,44.52,-124.047,44.425,-124.099,44.334,-124.131,44.056,-124.149,43.692,-124.184,43.652,-124.239,43.54,-124.288,43.41,-124.233,43.436,-124.197,43.423,-124.275,43.367,-124.321,43.368,-124.347,43.342,-124.454,43.012,-124.499,42.937,-124.54,42.813,-124.444,42.67,-124.406,42.584,-124.421,42.381,-124.41,42.304,-124.355,42.123,-124.228,42.001,-124.212,41.985,-124.209,41.889,-124.245,41.788,-124.163,41.719,-124.118,41.622,-124.072,41.46,-124.069,41.384,-124.14,41.156,-124.133,40.97,-124.2,40.822,-124.219,40.791,-124.223,40.775,-124.19,40.772,-124.208,40.746,-124.22,40.696,-124.251,40.704,-124.242,40.728,-124.254,40.74,-124.284,40.711,-124.325,40.598,-124.372,40.491,-124.357,40.371,-124.324,40.252,-124.108,40.095,-123.884,39.861,-123.833,39.775,-123.783,39.619,-123.778,39.515,-123.82,39.368,-123.72,39.111,-123.701,38.907,-123.425,38.676,-123.29,38.536,-123.121,38.449,-123.046,38.305,-122.987,38.277,-122.908,38.197,-122.877,38.123,-122.978,38.227,-122.968,38.097,-123.001,38.019,-122.999,37.989,-122.932,38.055,-122.873,38.026,-122.76,37.946,-122.681,37.902,-122.584,37.874,-122.521,37.826,-122.467,37.838,-122.495,37.954,-122.484,38.109,-122.393,38.145,-122.337,38.136,-122.208,38.073,-122.154,38.066,-122.031,38.124,-121.993,38.12,-121.934,38.087,-121.881,38.075,-121.749,38.08,-121.682,38.075,-121.626,38.084,-121.525,38.056,-121.573,38.052,-121.638,38.061,-121.717,38.034,-122.087,38.05,-122.217,38.041,-122.314,38.007,-122.385,37.961,-122.365,37.921,-122.333,37.897,-122.296,37.79,-122.222,37.732,-122.158,37.626,-122.124,37.544,-122.097,37.518,-122.071,37.478,-122.119,37.483,-122.166,37.502,-122.229,37.564,-122.298,37.592,-122.37,37.656,-122.39,37.741,-122.384,37.789,-122.446,37.798,-122.514,37.772,-122.5,37.653,-122.499,37.543,-122.408,37.373,-122.395,37.208,-122.164,36.991,-121.881,36.939,-121.807,36.851,-121.795,36.801,-121.79,36.732,-121.835,36.657,-121.919,36.572,-121.91,36.433,-121.877,36.331,-121.664,36.154,-121.465,35.927,-121.434,35.864,-121.344,35.792,-121.284,35.676,-121.138,35.607,-121.023,35.481,-120.9,35.425,-120.86,35.365,-120.885,35.275,-120.857,35.21,-120.707,35.158,-120.659,35.122,-120.634,35.076,-120.663,34.949,-120.625,34.812,-120.638,34.749,-120.627,34.669,-120.645,34.58,-120.56,34.544,-120.481,34.472,-120.396,34.46,-120.17,34.476,-120.053,34.469,-119.853,34.412,-119.713,34.4,-119.606,34.418,-119.414,34.339,-119.268,34.257,-119.236,34.164,-119.144,34.112,-118.832,34.024,-118.599,34.035,-118.506,34.017,-118.393,33.858,-118.41,33.744,-118.294,33.712,-118.264,33.759,-118.162,33.751,-118.081,33.722,-117.952,33.62,-117.789,33.538,-117.467,33.296,-117.319,33.1,-117.263,32.939,-117.256,32.873,-117.271,32.806,-117.243,32.664,-117.184,32.688,-117.137,32.649,-117.13,32.54,-117.128,32.533,-117.063,32.344,-117.035,32.305,-116.914,32.199,-116.848,31.997,-116.621,31.851,-116.624,31.758,-116.652,31.74,-116.702,31.744,-116.722,31.735,-116.668,31.699,-116.662,31.565,-116.61,31.499,-116.458,31.361,-116.333,31.203,-116.31,31.127,-116.31,31.051,-116.296,30.971,-116.062,30.804,-116.035,30.705,-116.029,30.564,-115.996,30.414,-115.858,30.36,-115.816,30.304,-115.79,30.084,-115.808,29.96,-115.749,29.936,-115.674,29.756,-115.565,29.68,-115.311,29.532,-115.166,29.427,-114.994,29.384,-114.937,29.352,-114.876,29.282,-114.664,29.095,-114.309,28.73,-114.146,28.605,-114.048,28.426,-114.093,28.221,-114.185,28.013,-114.266,27.934,-114.253,27.908,-114.158,27.92,-114.157,27.868,-114.175,27.831,-114.135,27.727,-114.069,27.676,-114.137,27.671,-114.233,27.718,-114.302,27.776,-114.289,27.839,-114.301,27.873,-114.373,27.841,-114.448,27.797,-114.57,27.784,-114.824,27.83,-115.036,27.842,-115.033,27.799,-114.994,27.736,-114.859,27.659,-114.716,27.54,-114.54,27.431,-114.498,27.376,-114.48,27.284,-114.445,27.218,-114.333,27.158,-114.202,27.144,-114.11,27.106,-113.996,26.988,-113.936,26.985,-113.841,26.967,-113.757,26.871,-113.701,26.791,-113.599,26.721,-113.426,26.796,-113.272,26.791,-113.206,26.857,-113.156,26.946,-113.143,26.792,-113.119,26.717,-113.021,26.583,-112.658,26.317,-112.526,26.273,-112.377,26.214,-112.174,25.913,-112.12,25.766,-112.115,25.63,-112.093,25.584,-112.07,25.573,-112.056,25.488,-112.078,25.324,-112.129,25.043,-112.119,24.934,-112.073,24.84,-111.848,24.67,-111.825,24.632,-111.822,24.573,-111.802,24.543,-111.75,24.554,-111.683,24.556,-111.578,24.443,-111.419,24.329,-111.036,24.105,-110.896,23.97,-110.765,23.877,-110.63,23.737,-110.363,23.605,-110.289,23.518,-110.244,23.412,-110.181,23.342,-110.086,23.005,-110.006,22.894,-109.923,22.886,-109.823,22.922,-109.728,22.982,-109.63,23.079,-109.496,23.16,-109.458,23.215,-109.415,23.406,-109.421,23.48,-109.51,23.598,-109.677,23.662,-109.711,23.804,-109.776,23.865,-109.811,23.939,-109.893,24.033,-109.983,24.109,-110.023,24.175,-110.263,24.345,-110.304,24.339,-110.325,24.306,-110.321,24.259,-110.297,24.195,-110.32,24.139,-110.367,24.1,-110.41,24.131,-110.4,24.165,-110.421,24.183,-110.547,24.214,-110.659,24.341,-110.735,24.59,-110.729,24.672,-110.677,24.789,-110.687,24.868,-110.756,24.995,-110.894,25.144,-111.014,25.42,-111.034,25.527,-111.15,25.573,-111.292,25.79,-111.33,25.931,-111.332,26.125,-111.405,26.265,-111.419,26.35,-111.465,26.408,-111.47,26.507,-111.546,26.579,-111.57,26.708,-111.795,26.88,-111.822,26.865,-111.817,26.756,-111.779,26.687,-111.699,26.581,-111.723,26.564,-111.754,26.573,-111.863,26.679,-111.883,26.84,-112.009,26.967,-112.016,27.01,-112.004,27.079,-112.098,27.146,-112.191,27.187,-112.283,27.347,-112.329,27.523,-112.553,27.657,-112.734,27.826,-112.758,27.901,-112.749,27.995,-112.808,28.092,-112.796,28.207,-112.868,28.292,-112.865,28.351,-112.871,28.424,-112.957,28.456,-113.034,28.473,-113.094,28.512,-113.206,28.799,-113.259,28.819,-113.321,28.813,-113.335,28.839,-113.329,28.873,-113.382,28.947,-113.5,28.927,-113.538,29.023,-113.545,29.102,-113.755,29.367,-113.829,29.439,-114.062,29.61,-114.179,29.734,-114.373,29.83,-114.403,29.896,-114.55,30.022,-114.63,30.156,-114.65,30.238,-114.633,30.507,-114.685,30.621,-114.703,30.765,-114.761,30.959,-114.845,31.08,-114.882,31.156,-114.848,31.538,-114.79,31.647,-114.84,31.799,-114.895,31.851,-114.934,31.901,-114.741,31.806,-114.698,31.777,-114.609,31.762,-114.549,31.734,-114.264,31.554,-114.149,31.507,-114.081,31.51,-114.003,31.525,-113.977,31.593,-113.948,31.629,-113.759,31.558,-113.7,31.523,-113.633,31.468,-113.623,31.346,-113.481,31.294,-113.231,31.256,-113.186,31.236,-113.084,31.207,-113.047,31.179,-113.043,31.087,-113.073,31.061,-113.108,31.077,-113.119,31.048,-113.105,31.027,-113.087,30.938,-113.11,30.793,-113.058,30.651,-112.952,30.51,-112.825,30.3,-112.759,30.126,-112.738,29.985,-112.697,29.917,-112.653,29.87,-112.573,29.72,-112.415,29.536,-112.389,29.46,-112.393,29.42,-112.378,29.348,-112.301,29.323,-112.223,29.269,-112.192,29.118,-112.162,29.019,-112.045,28.896,-111.941,28.823,-111.919,28.798,-111.907,28.752,-111.832,28.648,-111.747,28.564,-111.68,28.471,-111.472,28.384,-111.282,28.115,-111.121,27.967,-110.986,27.926,-110.921,27.889,-110.849,27.918,-110.759,27.915,-110.53,27.864,-110.578,27.796,-110.615,27.654,-110.593,27.544,-110.561,27.45,-110.519,27.396,-110.478,27.323,-110.377,27.233,-110.277,27.162,-109.944,27.079,-109.926,27.029,-109.922,26.978,-109.891,26.883,-109.828,26.77,-109.755,26.703,-109.676,26.697,-109.483,26.71,-109.276,26.534,-109.243,26.45,-109.241,26.405,-109.216,26.355,-109.146,26.306,-109.117,26.253,-109.159,26.258,-109.2,26.305,-109.271,26.243,-109.354,26.138,-109.426,26.033,-109.385,25.727,-109.304,25.633,-109.254,25.609,-109.196,25.593,-109.084,25.615,-109.008,25.642,-108.935,25.69,-108.887,25.733,-108.885,25.696,-108.973,25.588,-109.068,25.552,-109.063,25.517,-109.029,25.48,-108.893,25.512,-108.844,25.543,-108.787,25.538,-108.751,25.424,-108.696,25.383,-108.466,25.265,-108.374,25.194,-108.093,25.094,-108.051,25.067,-108.036,25.035,-108.08,25.018,-108.14,25.018,-108.192,25.031,-108.243,25.074,-108.281,25.082,-108.208,24.975,-108.015,24.783,-108.009,24.694,-107.951,24.615,-107.817,24.539,-107.71,24.525,-107.674,24.504,-107.602,24.49,-107.549,24.505,-107.512,24.489,-107.489,24.424,-107.494,24.369,-107.527,24.36,-107.727,24.472,-107.765,24.472,-107.085,24.016,-106.935,23.881,-106.729,23.611,-106.567,23.449,-106.402,23.196,-106.235,23.061,-106.022,22.829,-105.943,22.777,-105.792,22.627,-105.646,22.327,-105.649,21.988,-105.527,21.818,-105.457,21.672,-105.431,21.618,-105.209,21.491,-105.233,21.38,-105.225,21.25,-105.237,21.119,-105.302,21.027,-105.394,20.926,-105.456,20.844,-105.511,20.809,-105.492,20.777,-105.42,20.775,-105.327,20.753,-105.252,20.669,-105.245,20.634,-105.26,20.579,-105.377,20.512,-105.543,20.498,-105.642,20.436,-105.669,20.386,-105.616,20.316,-105.57,20.228,-105.532,20.075,-105.482,19.976,-105.286,19.706,-105.108,19.562,-105.045,19.443,-104.938,19.309,-104.603,19.153,-104.405,19.091,-104.277,19.011,-104.046,18.912,-103.912,18.828,-103.699,18.633,-103.58,18.484,-103.442,18.325,-103.019,18.187,-102.7,18.063,-102.547,18.041,-102.217,17.957,-101.996,17.973,-101.919,17.96,-101.847,17.922,-101.762,17.842,-101.6,17.652,-101.487,17.615,-101.385,17.514,-101.148,17.393,-101.002,17.276,-100.848,17.2,-100.432,17.064,-100.243,16.984,-100.025,16.921,-99.691,16.72,-99.348,16.665,-99.002,16.581,-98.908,16.545,-98.762,16.535,-98.52,16.305,-98.139,16.206,-97.755,15.967,-97.185,15.909,-96.808,15.726,-96.511,15.652,-96.409,15.683,-96.214,15.693,-95.772,15.888,-95.464,15.975,-95.134,16.177,-94.949,16.21,-94.799,16.21,-94.786,16.229,-94.846,16.247,-95.021,16.278,-95.024,16.306,-94.935,16.379,-94.9,16.417,-94.859,16.42,-94.793,16.365,-94.797,16.327,-94.791,16.287,-94.753,16.291,-94.651,16.352,-94.617,16.348,-94.587,16.316,-94.682,16.228,-94.662,16.202,-94.471,16.187,-94.001,16.019,-94.028,16.062,-94.193,16.146,-94.25,16.168,-94.303,16.169,-94.37,16.195,-94.426,16.226,-94.409,16.287,-94.374,16.285,-94.311,16.239,-94.24,16.205,-94.079,16.145,-93.916,16.054,-93.734,15.888,-93.541,15.75,-93.167,15.448,-93.024,15.31,-92.918,15.236,-92.809,15.139,-92.531,14.84,-92.265,14.568,-92.235,14.545,-91.819,14.228,-91.641,14.115,-91.377,13.99,-91.146,13.926,-90.607,13.929,-90.479,13.901,-90.095,13.737,-89.97,13.683,-89.804,13.56,-89.523,13.509,-89.278,13.478,-88.867,13.283,-88.512,13.184,-88.484,13.197,-88.582,13.245,-88.656,13.259,-88.686,13.281,-88.592,13.281,-88.417,13.214,-88.181,13.164,-88.023,13.169,-87.931,13.181,-87.878,13.224,-87.821,13.285,-87.838,13.386,-87.814,13.399,-87.769,13.377,-87.708,13.36,-87.602,13.386,-87.489,13.353,-87.485,13.311,-87.498,13.275,-87.458,13.215,-87.413,13.127,-87.333,13.085,-87.337,12.979,-87.339,12.95,-87.39,12.921,-87.424,12.921,-87.498,12.984,-87.543,13.04,-87.585,13.043,-87.67,12.966,-87.668,12.904,-87.46,12.758,-87.188,12.508,-87.125,12.434,-86.851,12.248,-86.756,12.157,-86.656,11.982,-86.469,11.738,-85.961,11.331,-85.829,11.199,-85.745,11.089,-85.744,11.062,-85.744,11.043,-85.752,10.985,-85.887,10.921,-85.908,10.898,-85.833,10.85,-85.715,10.791,-85.667,10.745,-85.671,10.68,-85.663,10.635,-85.703,10.563,-85.831,10.398,-85.85,10.292,-85.796,10.133,-85.681,9.959,-85.625,9.902,-85.315,9.811,-85.154,9.62,-85.115,9.582,-85.077,9.602,-85.06,9.668,-85.001,9.699,-84.886,9.821,-84.908,9.885,-84.963,9.933,-85.161,10.017,-85.237,10.107,-85.263,10.257,-85.236,10.242,-85.198,10.195,-85.025,10.116,-84.715,9.899,-84.643,9.789,-84.67,9.703,-84.659,9.647,-84.582,9.568,-84.483,9.526,-84.222,9.463,-84.118,9.379,-83.896,9.276,-83.737,9.15,-83.637,9.035,-83.616,8.96,-83.614,8.804,-83.642,8.729,-83.734,8.614,-83.605,8.48,-83.544,8.446,-83.452,8.438,-83.377,8.415,-83.292,8.406,-83.29,8.464,-83.298,8.507,-83.422,8.619,-83.47,8.707,-83.391,8.718,-83.286,8.664,-83.162,8.588,-83.13,8.505,-83.123,8.353,-83.041,8.288,-82.947,8.182,-82.879,8.071,-82.854,8.1,-82.866,8.246,-82.781,8.304,-82.68,8.322,-82.531,8.287,-82.365,8.275,-82.235,8.311,-82.224,8.23,-82.16,8.195,-82.097,8.223,-81.973,8.215,-81.86,8.165,-81.728,8.138,-81.694,8.071,-81.676,8.016,-81.504,7.721,-81.37,7.675,-81.268,7.625,-81.219,7.621,-81.195,7.668,-81.179,7.808,-81.158,7.854,-81.094,7.876,-81.064,7.9,-81.035,7.711,-80.915,7.438,-80.901,7.277,-80.846,7.22,-80.667,7.226,-80.439,7.275,-80.373,7.325,-80.348,7.386,-80.287,7.426,-80.111,7.433,-80.067,7.453,-80.011,7.5,-80.04,7.6,-80.075,7.667,-80.261,7.852,-80.366,7.998,-80.409,8.029,-80.458,8.077,-80.466,8.14,-80.459,8.214,-80.408,8.262,-80.369,8.289,-80.2,8.314,-80.126,8.35,-79.75,8.596,-79.816,8.639,-79.759,8.712,-79.731,8.775,-79.687,8.851,-79.572,8.903,-79.552,8.924,-79.507,8.97,-79.442,9.006,-79.247,9.02,-79.086,8.997,-78.955,8.933,-78.848,8.842,-78.77,8.811,-78.71,8.753,-78.67,8.742,-78.621,8.714,-78.514,8.628,-78.469,8.447,-78.436,8.403,-78.41,8.355,-78.379,8.359,-78.369,8.405,-78.388,8.443,-78.399,8.506,-78.374,8.489,-78.35,8.46,-78.256,8.454,-78.251,8.421,-78.223,8.397,-78.191,8.417,-78.162,8.454,-78.099,8.497,-78.057,8.397,-78.012,8.325,-77.853,8.216,-77.761,8.133,-77.834,8.151,-77.952,8.23,-78.048,8.285,-78.114,8.38,-78.142,8.386,-78.18,8.33,-78.281,8.248,-78.255,8.139,-78.287,8.092,-78.315,8.067,-78.368,8.071,-78.422,8.061,-78.378,7.9,-78.17,7.544,-77.93,7.256,-77.901,7.229,-77.804,7.137,-77.681,6.96,-77.646,6.87,-77.602,6.837,-77.526,6.693,-77.439,6.69,-77.369,6.576,-77.36,6.504,-77.398,6.275,-77.44,6.272,-77.473,6.286,-77.469,6.177,-77.345,5.995,-77.249,5.78,-77.325,5.676,-77.534,5.537,-77.402,5.416,-77.373,5.324,-77.359,5.215,-77.367,5.077,-77.339,4.839,-77.307,4.785,-77.286,4.722,-77.314,4.594,-77.328,4.475,-77.354,4.398,-77.414,4.348,-77.446,4.301,-77.516,4.256,-77.521,4.213,-77.409,4.248,-77.404,4.201,-77.434,4.131,-77.427,4.06,-77.358,3.945,-77.278,4.058,-77.248,4.041,-77.264,3.893,-77.212,3.867,-77.167,3.862,-77.127,3.906,-77.077,3.913,-77.243,3.585,-77.324,3.475,-77.357,3.349,-77.417,3.342,-77.472,3.234,-77.52,3.16,-77.559,3.076,-77.632,3.051,-77.694,3.04,-77.701,3.008,-77.671,2.919,-77.67,2.879,-77.777,2.787,-77.808,2.746,-77.814,2.716,-77.875,2.726,-77.901,2.699,-77.932,2.629,-77.987,2.569,-78.03,2.543,-78.067,2.509,-78.12,2.488,-78.296,2.51,-78.343,2.461,-78.417,2.483,-78.46,2.47,-78.535,2.424,-78.592,2.357,-78.617,2.307,-78.629,2.056,-78.55,1.924,-78.577,1.774,-78.793,1.849,-78.958,1.752,-79.025,1.624,-78.888,1.524,-78.86,1.455,-78.827,1.296,-78.9,1.206,-79.229,1.105,-79.465,1.06,-79.613,0.971,-79.741,0.98,-79.796,0.922,-79.904,0.86,-80.036,0.835,-80.088,0.785,-80.061,0.592,-80.025,0.41,-80.046,0.155,-80.133,-0.006,-80.237,-0.113,-80.321,-0.166,-80.482,-0.368,-80.468,-0.436,-80.385,-0.584,-80.282,-0.621,-80.358,-0.625,-80.455,-0.585,-80.505,-0.684,-80.554,-0.848,-80.624,-0.899,-80.841,-0.975,-80.902,-1.079,-80.82,-1.286,-80.801,-1.383,-80.835,-1.632,-80.763,-1.823,-80.761,-1.935,-80.77,-2.077,-80.868,-2.141,-80.963,-2.189,-80.952,-2.235,-80.932,-2.269,-80.839,-2.349,-80.685,-2.397,-80.45,-2.626,-80.379,-2.668,-80.285,-2.707,-80.255,-2.665,-80.249,-2.631,-80.127,-2.528,-80.053,-2.391,-80.007,-2.354,-80.03,-2.557,-79.989,-2.579,-79.926,-2.549,-79.88,-2.424,-79.893,-2.146,-79.842,-2.067,-79.833,-2.111,-79.84,-2.168,-79.822,-2.357,-79.746,-2.485,-79.73,-2.579,-79.823,-2.777,-79.922,-3.09,-79.963,-3.158,-80.027,-3.228,-80.1,-3.274,-80.16,-3.324,-80.303,-3.375,-80.325,-3.388,-80.504,-3.496,-80.653,-3.638,-80.799,-3.731,-80.892,-3.882,-81.232,-4.234,-81.283,-4.322,-81.337,-4.67,-81.289,-4.761,-81.195,-4.879,-81.108,-5.028,-81.151,-5.102,-81.168,-5.167,-80.943,-5.475,-80.882,-5.635,-80.883,-5.759,-80.931,-5.841,-80.992,-5.861,-81.092,-5.812,-81.164,-5.875,-81.181,-5.942,-81.142,-6.057,-81.058,-6.129,-80.812,-6.282,-80.11,-6.65,-79.995,-6.769,-79.905,-6.902,-79.762,-7.067,-79.618,-7.296,-79.589,-7.419,-79.377,-7.836,-79.313,-7.923,-79.164,-8.047,-79.012,-8.21,-78.925,-8.405,-78.762,-8.617,-78.755,-8.74,-78.665,-8.971,-78.58,-9.157,-78.446,-9.371,-78.356,-9.652,-78.276,-9.81,-78.186,-10.089,-78.095,-10.261,-77.736,-10.837,-77.664,-11.022,-77.639,-11.194,-77.633,-11.288,-77.31,-11.532,-77.22,-11.663,-77.158,-11.923,-77.153,-12.06,-77.063,-12.107,-77.038,-12.173,-76.994,-12.219,-76.832,-12.349,-76.758,-12.527,-76.637,-12.728,-76.555,-12.823,-76.502,-12.984,-76.427,-13.11,-76.224,-13.371,-76.184,-13.515,-76.259,-13.803,-76.319,-13.821,-76.376,-13.863,-76.297,-13.948,-76.289,-14.133,-76.175,-14.227,-76.136,-14.32,-76.006,-14.496,-75.934,-14.634,-75.738,-14.785,-75.534,-14.899,-75.397,-15.094,-75.275,-15.178,-75.191,-15.32,-75.104,-15.412,-74.555,-15.699,-74.373,-15.834,-74.147,-15.912,-73.825,-16.153,-73.728,-16.202,-73.4,-16.304,-73.264,-16.389,-72.958,-16.521,-72.794,-16.615,-72.468,-16.708,-72.362,-16.775,-72.269,-16.876,-72.111,-17.003,-71.967,-17.064,-71.868,-17.151,-71.774,-17.199,-71.532,-17.294,-71.436,-17.366,-71.399,-17.422,-71.365,-17.621,-71.337,-17.683,-71.057,-17.876,-70.942,-17.932,-70.817,-18.053,-70.492,-18.278,-70.418,-18.346,-70.362,-18.398,-70.336,-18.595,-70.335,-18.828,-70.276,-19.268,-70.21,-19.487,-70.198,-19.613,-70.157,-19.706,-70.148,-19.805,-70.147,-20.23,-70.194,-20.531,-70.197,-20.725,-70.088,-21.253,-70.08,-21.357,-70.088,-21.493,-70.13,-21.641,-70.155,-21.867,-70.185,-21.975,-70.229,-22.193,-70.26,-22.556,-70.332,-22.849,-70.389,-22.97,-70.45,-23.034,-70.563,-23.057,-70.569,-23.173,-70.593,-23.255,-70.588,-23.368,-70.512,-23.483,-70.42,-23.529,-70.392,-23.566,-70.41,-23.656,-70.488,-23.782,-70.507,-23.886,-70.52,-23.969,-70.507,-24.13,-70.546,-24.332,-70.574,-24.644,-70.559,-24.779,-70.445,-25.173,-70.452,-25.252,-70.49,-25.376,-70.578,-25.487,-70.633,-25.546,-70.714,-25.784,-70.7,-25.861,-70.635,-25.993,-70.662,-26.225,-70.647,-26.329,-70.687,-26.422,-70.708,-26.597,-70.803,-26.841,-70.813,-26.951,-70.898,-27.187,-70.914,-27.308,-70.909,-27.505,-70.926,-27.589,-70.946,-27.618,-71.053,-27.727,-71.087,-27.814,-71.154,-28.064,-71.186,-28.378,-71.267,-28.508,-71.307,-28.672,-71.384,-28.779,-71.494,-28.855,-71.519,-28.926,-71.486,-29.198,-71.353,-29.35,-71.327,-29.443,-71.316,-29.65,-71.348,-29.933,-71.4,-30.143,-71.669,-30.33,-71.709,-30.628,-71.706,-30.759,-71.654,-30.987,-71.662,-31.17,-71.577,-31.496,-71.526,-31.806,-71.513,-32.208,-71.421,-32.387,-71.461,-32.538,-71.452,-32.66,-71.592,-32.97,-71.636,-33.023,-71.743,-33.095,-71.697,-33.289,-71.696,-33.429,-71.636,-33.519,-71.664,-33.653,-71.831,-33.82,-71.854,-33.89,-71.927,-34.016,-72.003,-34.165,-71.992,-34.288,-72.031,-34.421,-72.056,-34.616,-72.182,-34.92,-72.224,-35.096,-72.387,-35.24,-72.455,-35.341,-72.505,-35.447,-72.562,-35.505,-72.624,-35.586,-72.587,-35.76,-72.683,-35.877,-72.778,-35.979,-72.875,-36.39,-72.968,-36.538,-73.007,-36.643,-73.118,-36.688,-73.138,-36.8,-73.151,-36.876,-73.173,-37.054,-73.216,-37.167,-73.271,-37.207,-73.375,-37.224,-73.602,-37.188,-73.634,-37.255,-73.662,-37.341,-73.603,-37.479,-73.665,-37.59,-73.662,-37.699,-73.517,-37.911,-73.465,-38.04,-73.472,-38.13,-73.533,-38.367,-73.52,-38.509,-73.481,-38.624,-73.226,-39.224,-73.25,-39.422,-73.41,-39.789,-73.482,-39.854,-73.671,-39.963,-73.669,-40.082,-73.742,-40.263,-73.784,-40.468,-73.92,-40.872,-73.984,-40.974,-73.966,-41.118,-73.876,-41.319,-73.855,-41.446,-73.811,-41.517,-73.711,-41.574,-73.624,-41.581,-73.625,-41.612,-73.688,-41.639,-73.722,-41.692,-73.735,-41.742,-73.624,-41.774,-73.521,-41.797,-73.242,-41.781,-73.174,-41.747,-73.015,-41.544,-72.953,-41.515,-72.88,-41.518,-72.805,-41.544,-72.67,-41.659,-72.601,-41.684,-72.542,-41.691,-72.428,-41.646,-72.359,-41.514,-72.318,-41.499,-72.36,-41.649,-72.486,-41.722,-72.66,-41.742,-72.744,-41.801,-72.784,-41.847,-72.824,-41.909,-72.781,-41.96,-72.738,-41.995,-72.624,-42.011,-72.499,-41.981,-72.46,-42.207,-72.412,-42.388,-72.43,-42.434,-72.548,-42.256,-72.631,-42.2,-72.707,-42.221,-72.773,-42.258,-72.785,-42.301,-72.716,-42.41,-72.632,-42.51,-72.655,-42.517,-72.774,-42.505,-72.848,-42.669,-72.845,-42.808,-72.766,-42.908,-72.755,-42.993,-72.758,-43.039,-72.878,-43.048,-72.915,-43.134,-72.94,-43.211,-73.076,-43.324,-73.101,-43.455,-72.997,-43.632,-73.069,-43.862,-73.224,-43.898,-73.241,-44.066,-73.265,-44.169,-73.141,-44.237,-73.001,-44.292,-72.828,-44.395,-72.664,-44.436,-72.68,-44.594,-72.739,-44.734,-73.078,-44.92,-73.256,-44.961,-73.362,-44.978,-73.405,-45.102,-73.445,-45.238,-73.226,-45.255,-73.064,-45.36,-72.976,-45.393,-72.941,-45.417,-72.934,-45.452,-72.978,-45.451,-73.202,-45.354,-73.266,-45.346,-73.379,-45.383,-73.55,-45.484,-73.731,-45.48,-73.78,-45.628,-73.757,-45.703,-73.662,-45.731,-73.594,-45.777,-73.592,-45.899,-73.629,-45.987,-73.652,-46.159,-73.668,-46.212,-73.662,-46.297,-73.716,-46.415,-73.77,-46.5,-73.845,-46.566,-73.944,-46.572,-73.949,-46.533,-73.935,-46.501,-73.811,-46.377,-73.708,-46.07,-73.708,-45.967,-73.695,-45.86,-73.735,-45.812,-73.813,-45.818,-73.879,-45.847,-73.929,-46.05,-73.968,-46.154,-74.09,-46.222,-74.213,-46.239,-74.372,-46.246,-74.393,-46.217,-74.357,-46.213,-74.082,-46.132,-74.02,-46.056,-74.061,-45.947,-74,-45.895,-73.96,-45.835,-73.882,-45.569,-73.844,-45.502,-73.825,-45.447,-73.92,-45.408,-73.957,-45.404,-74.038,-45.418,-74.099,-45.46,-74.123,-45.496,-74.099,-45.603,-74.083,-45.645,-74.082,-45.678,-74.096,-45.717,-74.158,-45.767,-74.301,-45.803,-74.369,-45.81,-74.463,-45.841,-74.631,-45.845,-74.763,-45.824,-75.067,-45.875,-75.075,-46.004,-74.998,-46.098,-74.924,-46.16,-75.075,-46.235,-75.247,-46.369,-75.376,-46.429,-75.437,-46.483,-75.657,-46.61,-75.706,-46.705,-75.708,-46.775,-75.635,-46.863,-75.497,-46.94,-75.43,-46.935,-75.401,-46.906,-75.386,-46.863,-75.446,-46.751,-75.528,-46.746,-75.565,-46.729,-75.54,-46.699,-75.478,-46.662,-75.337,-46.647,-75.146,-46.6,-75.019,-46.511,-74.984,-46.512,-75.053,-46.628,-75.031,-46.695,-75.006,-46.741,-74.811,-46.8,-74.691,-46.864,-74.512,-46.885,-74.48,-46.886,-74.467,-46.864,-74.489,-46.835,-74.484,-46.795,-74.454,-46.767,-74.314,-46.788,-74.209,-46.885,-74.152,-46.974,-74.208,-47.083,-74.158,-47.183,-74.216,-47.21,-74.403,-47.328,-74.483,-47.43,-74.324,-47.531,-74.243,-47.56,-74.191,-47.568,-74.134,-47.591,-74.152,-47.627,-74.243,-47.679,-74.323,-47.667,-74.404,-47.6,-74.467,-47.578,-74.534,-47.568,-74.588,-47.618,-74.655,-47.702,-74.609,-47.758,-74.569,-47.773,-74.43,-47.8,-74.376,-47.83,-74.379,-47.891,-74.351,-47.944,-74.227,-47.969,-74.085,-47.955,-73.941,-47.929,-73.847,-47.867,-73.779,-47.738,-73.748,-47.661,-73.716,-47.655,-73.635,-47.88,-73.629,-47.942,-73.61,-47.994,-73.57,-48.019,-73.501,-48.107,-73.391,-48.146,-73.384,-48.177,-73.528,-48.198,-73.854,-48.042,-74.25,-48.045,-74.4,-48.013,-74.585,-47.999,-74.591,-48.162,-74.577,-48.274,-74.499,-48.362,-74.474,-48.464,-74.343,-48.493,-74.27,-48.455,-74.172,-48.427,-74.009,-48.475,-74.057,-48.504,-74.129,-48.504,-74.176,-48.494,-74.228,-48.517,-74.341,-48.596,-74.382,-48.794,-74.38,-49.048,-74.358,-49.351,-74.367,-49.4,-74.349,-49.426,-74.302,-49.464,-74.221,-49.501,-74.185,-49.404,-74.168,-49.321,-74.14,-49.25,-74.074,-49.188,-74.061,-49.111,-74.028,-49.026,-73.935,-49.021,-73.938,-49.046,-73.985,-49.06,-74.015,-49.091,-74.006,-49.158,-74.023,-49.244,-74.049,-49.306,-74.083,-49.362,-74.094,-49.43,-73.988,-49.491,-73.892,-49.523,-73.836,-49.609,-73.892,-49.624,-73.956,-49.593,-74.102,-49.555,-74.23,-49.579,-74.291,-49.604,-74.319,-49.72,-74.324,-49.783,-74.171,-49.907,-74.073,-49.949,-74.011,-49.929,-73.959,-49.995,-74.019,-50.023,-74.334,-49.975,-74.434,-50.065,-74.63,-50.194,-74.516,-50.266,-74.425,-50.35,-74.374,-50.363,-74.306,-50.398,-74.031,-50.47,-73.95,-50.511,-74.186,-50.485,-74.197,-50.61,-74.164,-50.638,-74.097,-50.717,-73.978,-50.827,-73.892,-50.783,-73.75,-50.54,-73.68,-50.49,-73.654,-50.493,-73.693,-50.57,-73.614,-50.628,-73.618,-50.651,-73.659,-50.651,-73.741,-50.697,-73.825,-50.836,-73.807,-50.938,-73.847,-50.94,-74.139,-50.818,-74.156,-50.797,-74.19,-50.778,-74.331,-50.56,-74.366,-50.488,-74.564,-50.382,-74.644,-50.361,-74.722,-50.408,-74.776,-50.47,-74.702,-50.535,-74.649,-50.618,-74.686,-50.662,-74.837,-50.679,-75.095,-50.681,-75.055,-50.786,-74.983,-50.881,-74.815,-51.063,-74.69,-51.087,-74.587,-51.131,-74.508,-51.15,-74.414,-51.162,-74.332,-51.195,-74.21,-51.205,-74.121,-51.195,-73.94,-51.266,-73.896,-51.331,-73.93,-51.618,-74.07,-51.579,-74.197,-51.681,-74.146,-51.712,-73.973,-51.784,-73.894,-51.758,-73.858,-51.79,-73.811,-51.801,-73.753,-51.796,-73.65,-51.856,-73.582,-51.96,-73.518,-52.041,-73.383,-52.07,-73.189,-51.991,-72.928,-51.86,-72.6,-51.799,-72.583,-51.737,-72.649,-51.695,-72.705,-51.678,-72.789,-51.614,-73.115,-51.504,-73.163,-51.496,-73.197,-51.478,-73.169,-51.454,-73.127,-51.44,-72.761,-51.573,-72.543,-51.706,-72.49,-51.764,-72.494,-51.848,-72.523,-51.891,-72.625,-51.946,-72.625,-52.007,-72.614,-52.037,-72.524,-52.17,-72.519,-52.217,-72.523,-52.255,-72.533,-52.282,-72.569,-52.334,-72.631,-52.372,-72.677,-52.385,-72.714,-52.357,-72.694,-52.33,-72.583,-52.254,-72.571,-52.2,-72.588,-52.145,-72.65,-52.1,-72.695,-52.045,-72.695,-51.985,-72.735,-51.961,-72.795,-51.95,-72.843,-51.961,-72.944,-52.047,-73.137,-52.13,-73.26,-52.158,-73.327,-52.166,-73.458,-52.146,-73.532,-52.153,-73.649,-52.078,-73.684,-52.078,-73.685,-52.137,-73.703,-52.199,-73.749,-52.216,-73.834,-52.234,-74.04,-52.159,-74.134,-52.155,-74.195,-52.12,-74.265,-52.105,-74.296,-52.118,-74.266,-52.171,-74.238,-52.202,-74.177,-52.317,-74.151,-52.383,-74.094,-52.376,-74.037,-52.403,-74,-52.513,-74.036,-52.577,-74.014,-52.639,-73.915,-52.688,-73.711,-52.662,-73.586,-52.686,-73.382,-52.595,-73.244,-52.624,-73.178,-52.563,-73.184,-52.488,-73.124,-52.488,-73.073,-52.535,-73.145,-52.602,-73.241,-52.707,-73.346,-52.754,-73.645,-52.837,-73.508,-52.904,-73.46,-52.965,-73.338,-53.055,-73.122,-53.074,-73.055,-53.046,-73.023,-53.022,-73.016,-52.977,-73.02,-52.892,-72.932,-52.782,-72.802,-52.712,-72.766,-52.643,-72.777,-52.577,-72.712,-52.536,-72.645,-52.529,-72.504,-52.56,-72.478,-52.604,-72.438,-52.626,-72.315,-52.539,-72.226,-52.521,-71.812,-52.537,-71.665,-52.56,-71.511,-52.605,-71.554,-52.644,-71.591,-52.661,-71.797,-52.683,-71.979,-52.646,-72.118,-52.65,-72.453,-52.814,-72.627,-52.818,-72.632,-52.774,-72.676,-52.749,-72.728,-52.762,-72.832,-52.82,-72.889,-52.872,-72.91,-52.937,-72.916,-53.122,-72.998,-53.177,-73.053,-53.243,-72.998,-53.291,-72.727,-53.42,-72.549,-53.461,-72.531,-53.372,-72.493,-53.291,-72.458,-53.254,-72.278,-53.132,-72.129,-53.064,-71.898,-53.002,-71.388,-52.764,-71.227,-52.811,-71.155,-52.846,-71.163,-52.888,-71.18,-52.921,-71.289,-53.034,-71.4,-53.107,-71.741,-53.233,-71.791,-53.485,-71.892,-53.524,-71.903,-53.496,-71.867,-53.458,-71.828,-53.398,-71.853,-53.286,-71.942,-53.234,-72.081,-53.25,-72.249,-53.247,-72.306,-53.254,-72.413,-53.35,-72.398,-53.418,-72.377,-53.471,-72.174,-53.632,-72.101,-53.666,-71.872,-53.723,-71.694,-53.803,-71.444,-53.841,-71.298,-53.883,-71.083,-53.825,-70.996,-53.779,-70.948,-53.57,-70.985,-53.448,-70.984,-53.374,-70.952,-53.227,-70.821,-52.963,-70.839,-52.89,-70.795,-52.769,-70.68,-52.712,-70.563,-52.673,-70.391,-52.661,-69.907,-52.514,-69.763,-52.506,-69.62,-52.465,-69.561,-52.422,-69.447,-52.269,-69.241,-52.205,-69.134,-52.211,-69.007,-52.263,-68.443,-52.357,-68.394,-52.307,-68.494,-52.198,-68.691,-52.013,-68.917,-51.715,-68.965,-51.677,-69.033,-51.636,-69.18,-51.662,-69.313,-51.601,-69.409,-51.61,-69.465,-51.584,-69.361,-51.559,-69.218,-51.561,-69.058,-51.547,-69.035,-51.489,-69.03,-51.446,-69.066,-51.304,-69.144,-51.097,-69.201,-50.994,-69.268,-51.006,-69.352,-51.046,-69.359,-51.028,-69.235,-50.951,-69.155,-50.864,-69.141,-50.753,-69.09,-50.583,-69.045,-50.499,-68.939,-50.382,-68.75,-50.281,-68.589,-50.225,-68.465,-50.195,-68.422,-50.158,-68.474,-50.091,-68.533,-50.036,-68.598,-50.009,-68.753,-49.988,-68.98,-50.003,-68.913,-49.969,-68.662,-49.936,-68.638,-49.863,-68.673,-49.793,-68.668,-49.753,-68.569,-49.867,-68.488,-49.978,-68.405,-50.043,-68.257,-50.105,-68.146,-50.091,-67.914,-49.984,-67.826,-49.92,-67.783,-49.859,-67.662,-49.342,-67.694,-49.304,-67.685,-49.247,-67.466,-48.952,-67.263,-48.814,-67.131,-48.688,-67.033,-48.628,-66.783,-48.523,-66.596,-48.42,-66.393,-48.342,-66.017,-48.084,-65.943,-48.019,-65.912,-47.977,-65.81,-47.941,-65.864,-47.853,-65.934,-47.827,-66.097,-47.853,-66.172,-47.858,-66.225,-47.827,-66.041,-47.783,-65.886,-47.702,-65.814,-47.638,-65.775,-47.568,-65.738,-47.345,-65.769,-47.257,-65.854,-47.157,-65.999,-47.094,-66.65,-47.045,-66.777,-47.006,-67.387,-46.554,-67.506,-46.443,-67.563,-46.345,-67.586,-46.27,-67.609,-46.167,-67.6,-46.053,-67.557,-45.97,-67.393,-45.776,-67.258,-45.577,-66.941,-45.257,-66.882,-45.228,-66.585,-45.183,-66.533,-45.158,-66.494,-45.118,-66.348,-45.034,-66.19,-44.965,-65.758,-45.007,-65.639,-45.008,-65.606,-44.945,-65.599,-44.876,-65.698,-44.796,-65.648,-44.661,-65.361,-44.477,-65.29,-44.361,-65.266,-44.28,-65.308,-44.158,-65.239,-44.049,-65.305,-43.787,-65.284,-43.63,-65.252,-43.572,-65.19,-43.522,-64.986,-43.294,-64.84,-43.189,-64.715,-43.136,-64.432,-43.059,-64.376,-43.025,-64.319,-42.969,-64.38,-42.949,-64.442,-42.951,-64.629,-42.909,-65.027,-42.759,-64.971,-42.666,-64.812,-42.633,-64.65,-42.531,-64.488,-42.513,-64.324,-42.572,-64.248,-42.646,-64.22,-42.756,-64.131,-42.861,-64.035,-42.881,-63.692,-42.805,-63.644,-42.746,-63.617,-42.696,-63.594,-42.556,-63.596,-42.407,-63.63,-42.283,-63.685,-42.189,-63.729,-42.153,-63.796,-42.114,-63.893,-42.125,-64.083,-42.183,-64.229,-42.218,-64.253,-42.251,-64.061,-42.266,-64.062,-42.353,-64.101,-42.395,-64.265,-42.422,-64.42,-42.434,-64.571,-42.416,-64.574,-42.356,-64.524,-42.299,-64.512,-42.27,-64.538,-42.255,-64.622,-42.261,-64.7,-42.221,-64.898,-42.162,-64.986,-42.102,-65.059,-41.97,-65.007,-41.745,-65.018,-41.567,-65.128,-41.239,-65.155,-41.106,-65.152,-40.947,-65.133,-40.881,-65.069,-40.805,-64.917,-40.731,-64.869,-40.736,-64.804,-40.757,-64.82,-40.793,-64.853,-40.814,-64.621,-40.854,-64.383,-40.922,-64.123,-41.008,-63.773,-41.15,-63.622,-41.16,-63.213,-41.152,-62.959,-41.11,-62.798,-41.047,-62.395,-40.891,-62.302,-40.815,-62.246,-40.675,-62.394,-40.459,-62.427,-40.356,-62.402,-40.197,-62.324,-39.951,-62.287,-39.895,-62.254,-39.88,-62.132,-39.825,-62.083,-39.568,-62.077,-39.462,-62.131,-39.432,-62.179,-39.38,-62.054,-39.374,-62.126,-39.31,-62.209,-39.262,-62.295,-39.243,-62.338,-39.151,-62.304,-38.988,-62.374,-38.853,-62.335,-38.8,-62.189,-38.813,-62.067,-38.919,-61.848,-38.962,-61.603,-38.999,-61.383,-38.981,-61.112,-38.993,-60.904,-38.974,-59.828,-38.838,-59.676,-38.797,-59.007,-38.673,-58.179,-38.436,-57.646,-38.17,-57.547,-38.086,-57.507,-37.909,-57.396,-37.745,-57.088,-37.446,-56.727,-36.958,-56.672,-36.851,-56.668,-36.735,-56.698,-36.426,-56.717,-36.389,-56.749,-36.346,-56.937,-36.353,-57.076,-36.297,-57.265,-36.144,-57.335,-36.027,-57.375,-35.9,-57.354,-35.72,-57.159,-35.506,-57.171,-35.362,-57.304,-35.188,-57.548,-35.019,-57.764,-34.895,-58.283,-34.683,-58.419,-34.532,-58.466,-34.457,-58.525,-34.296,-58.475,-34.263,-58.435,-34.253,-58.392,-34.193,-58.409,-34.061,-58.429,-33.991,-58.457,-33.898,-58.531,-33.753,-58.547,-33.663,-58.455,-33.286,-58.424,-33.112,-58.376,-33.072,-58.309,-33.083,-58.25,-33.078,-58.201,-33.015,-58.171,-32.959,-58.22,-32.564,-58.201,-32.472,-58.162,-32.567,-58.13,-32.757,-58.082,-32.894,-58.093,-32.967,-58.154,-33.065,-58.222,-33.129,-58.292,-33.138,-58.364,-33.182,-58.353,-33.26,-58.411,-33.509,-58.438,-33.719,-58.4,-33.912,-58.207,-34.109,-57.961,-34.307,-57.902,-34.39,-57.873,-34.448,-57.829,-34.477,-57.543,-34.448,-57.171,-34.452,-56.855,-34.677,-56.463,-34.775,-56.388,-34.861,-56.25,-34.901,-56.195,-34.906,-56.118,-34.908,-55.863,-34.811,-55.673,-34.776,-55.371,-34.808,-55.238,-34.896,-55.095,-34.895,-54.902,-34.933,-54.365,-34.733,-54.272,-34.667,-54.169,-34.671,-54.01,-34.517,-53.785,-34.38,-53.743,-34.25,-53.535,-34.017,-53.472,-33.849,-53.42,-33.779,-53.371,-33.742,-52.921,-33.402,-52.763,-33.266,-52.652,-33.138,-52.508,-32.875,-52.342,-32.44,-52.275,-32.324,-52.19,-32.221,-52.127,-32.168,-52.167,-32.088,-52.192,-31.968,-52.194,-31.886,-52.12,-31.695,-52.027,-31.599,-51.995,-31.49,-51.972,-31.384,-51.927,-31.339,-51.717,-31.244,-51.506,-31.104,-51.464,-31.053,-51.485,-30.978,-51.459,-30.913,-51.376,-30.847,-51.359,-30.675,-51.316,-30.703,-51.283,-30.752,-51.288,-30.591,-51.247,-30.468,-51.188,-30.412,-51.157,-30.364,-51.282,-30.244,-51.295,-30.141,-51.298,-30.035,-51.25,-30.06,-51.234,-30.121,-51.179,-30.211,-51.04,-30.261,-51.025,-30.369,-50.932,-30.374,-50.646,-30.237,-50.564,-30.254,-50.547,-30.317,-50.582,-30.439,-50.615,-30.457,-50.685,-30.413,-50.716,-30.426,-50.689,-30.704,-50.77,-30.813,-50.941,-30.904,-50.965,-31.005,-50.954,-31.052,-50.98,-31.094,-51.106,-31.081,-51.161,-31.119,-51.158,-31.267,-51.174,-31.34,-51.272,-31.477,-51.446,-31.557,-51.681,-31.775,-51.803,-31.797,-51.841,-31.832,-51.893,-31.868,-51.995,-31.815,-52.063,-31.83,-52.06,-31.913,-52.043,-31.978,-52.069,-32.063,-52.039,-32.115,-51.92,-31.99,-51.798,-31.9,-51.46,-31.702,-51.152,-31.48,-50.921,-31.258,-50.748,-31.068,-50.62,-30.898,-50.3,-30.426,-50.033,-29.801,-49.746,-29.363,-49.5,-29.075,-49.271,-28.871,-49.024,-28.699,-48.8,-28.575,-48.797,-28.443,-48.693,-28.31,-48.648,-28.207,-48.621,-28.076,-48.606,-27.825,-48.643,-27.558,-48.572,-27.373,-48.596,-27.264,-48.554,-27.196,-48.568,-27.123,-48.593,-27.058,-48.616,-26.878,-48.678,-26.703,-48.677,-26.612,-48.658,-26.519,-48.652,-26.406,-48.701,-26.348,-48.748,-26.269,-48.714,-26.227,-48.679,-26.226,-48.619,-26.179,-48.576,-25.935,-48.613,-25.875,-48.679,-25.875,-48.666,-25.844,-48.545,-25.816,-48.401,-25.597,-48.43,-25.55,-48.507,-25.521,-48.692,-25.492,-48.732,-25.369,-48.644,-25.437,-48.564,-25.447,-48.476,-25.443,-48.428,-25.403,-48.458,-25.311,-48.402,-25.272,-48.273,-25.306,-48.186,-25.31,-48.242,-25.403,-48.203,-25.417,-48.024,-25.237,-47.929,-25.168,-47.908,-25.068,-47.959,-25.065,-47.989,-25.036,-47.914,-25,-47.877,-24.997,-47.831,-24.953,-47.592,-24.781,-47.137,-24.493,-46.867,-24.236,-46.631,-24.11,-45.972,-23.796,-45.843,-23.764,-45.665,-23.765,-45.527,-23.805,-45.464,-23.803,-45.433,-23.758,-45.423,-23.685,-45.325,-23.6,-45.215,-23.576,-44.952,-23.381,-44.667,-23.335,-44.619,-23.316,-44.57,-23.274,-44.621,-23.229,-44.674,-23.207,-44.681,-23.107,-44.637,-23.055,-44.368,-23.005,-44.148,-23.011,-44.047,-22.945,-43.866,-22.911,-43.703,-22.966,-43.676,-23.009,-43.791,-23.046,-43.899,-23.035,-43.974,-23.057,-43.899,-23.101,-43.737,-23.067,-43.533,-23.046,-43.369,-22.998,-43.224,-22.991,-43.194,-22.939,-43.209,-22.878,-43.237,-22.829,-43.242,-22.795,-43.229,-22.748,-43.154,-22.725,-43.086,-22.723,-43.065,-22.771,-43.101,-22.85,-43.081,-22.903,-43.016,-22.943,-42.958,-22.967,-42.829,-22.973,-42.581,-22.941,-42.122,-22.941,-42.042,-22.947,-41.987,-22.845,-41.941,-22.788,-41.986,-22.736,-41.998,-22.645,-41.98,-22.581,-41.706,-22.31,-41.583,-22.244,-41.123,-22.084,-41,-21.999,-40.988,-21.92,-41.022,-21.611,-41.023,-21.597,-41.047,-21.506,-40.955,-21.238,-40.829,-21.031,-40.789,-20.906,-40.727,-20.846,-40.597,-20.784,-40.396,-20.569,-40.319,-20.426,-40.299,-20.293,-40.203,-20.206,-40.142,-19.968,-40.001,-19.742,-39.845,-19.649,-39.783,-19.572,-39.731,-19.454,-39.7,-19.278,-39.742,-18.846,-39.74,-18.64,-39.651,-18.252,-39.487,-17.99,-39.413,-17.92,-39.278,-17.849,-39.154,-17.704,-39.171,-17.642,-39.215,-17.316,-39.203,-17.178,-39.164,-17.044,-39.125,-16.764,-39.063,-16.504,-38.961,-16.187,-38.881,-15.864,-38.885,-15.842,-38.943,-15.564,-38.996,-15.254,-39.013,-14.936,-39.06,-14.655,-38.942,-14.031,-38.967,-14.003,-39.008,-14.101,-39.048,-14.044,-39.035,-13.991,-39.041,-13.758,-39.001,-13.665,-38.989,-13.615,-39.009,-13.581,-39.035,-13.559,-39.089,-13.588,-39.067,-13.48,-39.031,-13.365,-38.959,-13.273,-38.835,-13.147,-38.833,-13.033,-38.764,-12.907,-38.784,-12.844,-38.852,-12.79,-38.788,-12.783,-38.744,-12.749,-38.691,-12.624,-38.654,-12.645,-38.525,-12.762,-38.499,-12.957,-38.447,-12.967,-38.402,-12.966,-38.24,-12.844,-38.019,-12.591,-37.957,-12.475,-37.689,-12.1,-37.469,-11.654,-37.412,-11.497,-37.438,-11.394,-37.359,-11.253,-37.322,-11.215,-37.321,-11.267,-37.332,-11.31,-37.355,-11.35,-37.356,-11.404,-37.315,-11.376,-37.181,-11.187,-37.183,-11.068,-37.125,-11.085,-37.093,-11.055,-36.938,-10.82,-36.768,-10.672,-36.636,-10.59,-36.412,-10.49,-36.398,-10.484,-36.224,-10.225,-36.055,-10.076,-35.885,-9.848,-35.848,-9.772,-35.891,-9.687,-35.83,-9.719,-35.764,-9.703,-35.597,-9.541,-35.341,-9.231,-35.158,-8.931,-34.967,-8.408,-34.891,-8.092,-34.835,-7.971,-34.837,-7.872,-34.879,-7.747,-34.873,-7.692,-34.855,-7.634,-34.861,-7.595,-34.858,-7.533,-34.817,-7.395,-34.805,-7.288,-34.834,-7.024,-34.876,-7.003,-34.88,-6.908,-34.93,-6.785,-34.988,-6.394,-35.095,-6.185,-35.142,-5.917,-35.235,-5.567,-35.393,-5.251,-35.482,-5.166,-35.549,-5.129,-35.98,-5.054,-36.162,-5.094,-36.387,-5.084,-36.591,-5.098,-36.747,-5.051,-36.861,-4.967,-36.955,-4.937,-37.175,-4.912,-37.301,-4.713,-37.626,-4.592,-37.796,-4.404,-38.049,-4.216,-38.272,-3.948,-38.362,-3.876,-38.476,-3.717,-38.686,-3.654,-38.896,-3.502,-39.014,-3.39,-39.353,-3.197,-39.511,-3.126,-39.609,-3.056,-39.772,-2.986,-39.965,-2.862,-40.235,-2.813,-40.475,-2.796,-40.876,-2.87,-41.195,-2.886,-41.318,-2.936,-41.48,-2.917,-41.64,-2.879,-41.722,-2.809,-41.876,-2.747,-42,-2.806,-42.25,-2.792,-42.594,-2.661,-42.676,-2.59,-42.832,-2.53,-42.937,-2.465,-43.23,-2.386,-43.38,-2.376,-43.435,-2.414,-43.455,-2.502,-43.729,-2.518,-43.864,-2.595,-43.933,-2.583,-44.013,-2.642,-44.193,-2.81,-44.225,-2.755,-44.192,-2.7,-44.113,-2.599,-44.101,-2.56,-44.106,-2.493,-44.179,-2.471,-44.229,-2.471,-44.308,-2.535,-44.381,-2.738,-44.438,-2.944,-44.623,-3.138,-44.723,-3.205,-44.721,-3.142,-44.639,-2.762,-44.611,-2.677,-44.589,-2.573,-44.562,-2.524,-44.521,-2.481,-44.52,-2.405,-44.382,-2.366,-44.391,-2.27,-44.435,-2.168,-44.52,-2.19,-44.579,-2.23,-44.662,-2.373,-44.701,-2.32,-44.756,-2.266,-44.708,-2.241,-44.659,-2.228,-44.617,-2.152,-44.58,-2.114,-44.538,-2.053,-44.547,-1.946,-44.592,-1.842,-44.651,-1.746,-44.721,-1.792,-44.779,-1.799,-44.721,-1.733,-44.79,-1.725,-44.828,-1.672,-44.92,-1.589,-45.026,-1.513,-45.076,-1.466,-45.182,-1.507,-45.239,-1.629,-45.282,-1.697,-45.329,-1.717,-45.353,-1.567,-45.459,-1.356,-45.557,-1.331,-45.645,-1.348,-45.779,-1.251,-45.972,-1.187,-46.045,-1.103,-46.14,-1.118,-46.215,-1.1,-46.219,-1.031,-46.321,-1.039,-46.422,-1.03,-46.516,-0.997,-46.617,-0.971,-46.644,-0.916,-46.77,-0.837,-46.811,-0.78,-46.894,-0.78,-46.944,-0.743,-47.025,-0.75,-47.127,-0.745,-47.201,-0.68,-47.269,-0.645,-47.398,-0.627,-47.439,-0.648,-47.46,-0.681,-47.433,-0.722,-47.419,-0.766,-47.471,-0.749,-47.557,-0.67,-47.651,-0.719,-47.687,-0.725,-47.731,-0.71,-47.774,-0.677,-47.808,-0.663,-47.883,-0.693,-47.961,-0.77,-48.033,-0.705,-48.069,-0.714,-48.115,-0.737,-48.128,-0.795,-48.202,-0.828,-48.266,-0.895,-48.318,-0.961,-48.306,-1.04,-48.45,-1.146,-48.409,-1.229,-48.478,-1.324,-48.468,-1.394,-48.451,-1.436,-48.35,-1.482,-48.446,-1.52,-48.463,-1.614,-48.53,-1.567,-48.6,-1.489,-48.71,-1.488,-48.991,-1.83,-49.155,-1.879,-49.211,-1.917,-49.408,-2.344,-49.458,-2.505,-49.524,-2.597,-49.576,-2.631,-49.637,-2.657,-49.599,-2.584,-49.553,-2.52,-49.507,-2.28,-49.46,-2.192,-49.399,-1.972,-49.314,-1.732,-49.585,-1.867,-49.72,-1.926,-49.903,-1.871,-49.999,-1.832,-50.117,-1.858,-50.173,-1.896,-50.26,-1.923,-50.403,-2.016,-50.586,-1.85,-50.639,-1.817,-50.69,-1.762,-50.675,-1.695,-50.679,-1.644,-50.786,-1.49,-50.819,-1.376,-50.826,-1.311,-50.845,-1.226,-50.897,-1.164,-50.918,-1.115,-50.838,-1.039,-50.842,-1,-50.895,-0.938,-50.992,-0.986,-51.029,-1.032,-51.202,-1.137,-51.297,-1.224,-51.531,-1.354,-51.646,-1.394,-51.948,-1.587,-52.197,-1.64,-52.31,-1.56,-52.664,-1.552,-52.553,-1.514,-52.229,-1.362,-52.02,-1.399,-51.981,-1.368,-51.934,-1.32,-51.922,-1.181,-51.819,-1.118,-51.721,-1.018,-51.722,-0.855,-51.703,-0.762,-51.555,-0.549,-51.496,-0.509,-51.404,-0.393,-51.3,-0.179,-51.283,-0.085,-51.102,-0.031,-50.967,0.13,-50.91,0.161,-50.816,0.173,-50.755,0.223,-50.582,0.421,-50.463,0.637,-50.343,0.751,-50.294,0.836,-50.071,1.015,-50.047,1.052,-49.938,1.121,-49.899,1.163,-49.906,1.269,-49.882,1.42,-49.957,1.66,-50.055,1.731,-50.188,1.786,-50.304,1.798,-50.459,1.83,-50.534,1.927,-50.576,1.999,-50.609,2.104,-50.659,2.131,-50.714,2.134,-50.677,2.179,-50.679,2.21,-50.737,2.377,-50.79,2.478,-50.817,2.573,-50.827,2.652,-50.994,3.078,-51.052,3.282,-51.076,3.672,-51.22,4.094,-51.327,4.225,-51.462,4.314,-51.547,4.311,-51.558,4.234,-51.653,4.061,-51.658,4.098,-51.653,4.139,-51.666,4.229,-51.699,4.287,-51.786,4.571,-51.828,4.636,-51.88,4.634,-51.92,4.524,-51.928,4.436,-51.955,4.399,-52.003,4.352,-52.002,4.386,-51.979,4.43,-51.962,4.514,-52.012,4.646,-52.058,4.717,-52.22,4.863,-52.325,4.771,-52.289,4.876,-52.291,4.942,-52.454,5.021,-52.765,5.273,-52.899,5.425,-53.27,5.543,-53.454,5.563,-53.847,5.782,-53.92,5.769,-53.99,5.676,-54.085,5.412,-54.156,5.359,-54.08,5.502,-54.046,5.609,-54.037,5.721,-54.054,5.808,-54.142,5.856,-54.356,5.91,-54.834,5.988,-55.148,5.993,-55.379,5.953,-55.648,5.986,-55.828,5.962,-55.91,5.893,-55.896,5.795,-55.898,5.699,-55.94,5.795,-56.236,5.885,-56.466,5.938,-56.97,5.993,-57.057,5.939,-57.105,5.829,-57.136,5.737,-57.141,5.644,-57.182,5.529,-57.195,5.548,-57.205,5.565,-57.167,5.885,-57.19,6.097,-57.228,6.178,-57.344,6.272,-57.54,6.332,-57.608,6.45,-57.793,6.599,-57.983,6.786,-58.072,6.821,-58.173,6.829,-58.298,6.879,-58.415,6.851,-58.502,6.734,-58.569,6.627,-58.594,6.452,-58.673,6.391,-58.613,6.503,-58.608,6.697,-58.583,6.844,-58.481,7.038,-58.477,7.326,-58.511,7.398,-58.627,7.546,-58.701,7.607,-58.812,7.736,-59.2,8.075,-59.477,8.254,-59.666,8.363,-59.739,8.38,-59.74,8.339,-59.757,8.34,-59.837,8.374,-59.981,8.533,-60.018,8.549,-60.167,8.617,-60.34,8.629,-60.404,8.61,-60.481,8.547,-60.801,8.592,-60.865,8.579,-61.036,8.493,-61.194,8.488,-61.304,8.41,-61.443,8.509,-61.527,8.546,-61.619,8.597,-61.247,8.6,-61.176,8.725,-61.122,8.843,-61.099,8.941,-61.093,8.966,-61.054,9.035,-61.053,9.095,-61.023,9.155,-60.971,9.215,-60.841,9.264,-60.792,9.361,-60.874,9.453,-61.013,9.556,-61.234,9.598,-61.309,9.633,-61.512,9.848,-61.589,9.895,-61.625,9.816,-61.766,9.814,-61.759,9.754,-61.732,9.702,-61.736,9.631,-61.759,9.677,-61.805,9.706,-61.831,9.733,-61.837,9.782,-61.909,9.87,-62.017,9.955,-62.077,9.975,-62.12,9.985,-62.155,9.979,-62.147,9.953,-62.17,9.879,-62.153,9.822,-62.172,9.827,-62.19,9.842,-62.221,9.883,-62.257,9.819,-62.281,9.793,-62.3,9.788,-62.32,9.783,-62.401,9.918,-62.515,10.176,-62.55,10.2,-62.6,10.217,-62.608,10.163,-62.6,10.117,-62.651,10.071,-62.741,10.056,-62.695,10.1,-62.662,10.199,-62.686,10.29,-62.706,10.333,-62.781,10.399,-62.813,10.4,-62.843,10.418,-62.843,10.507,-62.914,10.531,-62.694,10.563,-62.38,10.547,-62.233,10.634,-62.04,10.645,-61.921,10.681,-61.879,10.741,-62.242,10.7,-62.702,10.75,-62.947,10.707,-63.035,10.72,-63.19,10.709,-63.497,10.643,-63.873,10.664,-64.202,10.633,-64.298,10.635,-64.248,10.543,-64.158,10.579,-63.863,10.558,-63.732,10.503,-63.779,10.472,-63.834,10.449,-64.188,10.458,-64.85,10.098,-64.944,10.095,-65.023,10.077,-65.129,10.07,-65.317,10.122,-65.489,10.159,-65.656,10.228,-65.852,10.258,-66.09,10.473,-66.092,10.517,-66.106,10.575,-66.247,10.632,-66.989,10.611,-67.133,10.57,-67.581,10.524,-67.872,10.472,-68.14,10.493,-68.234,10.569,-68.296,10.689,-68.325,10.809,-68.272,10.88,-68.325,10.949,-68.343,11.053,-68.399,11.161,-68.616,11.309,-68.828,11.432,-69.055,11.461,-69.233,11.518,-69.526,11.5,-69.57,11.485,-69.632,11.48,-69.712,11.564,-69.762,11.676,-69.811,11.837,-69.831,11.996,-69.86,12.054,-69.914,12.115,-70.004,12.178,-70.122,12.137,-70.203,12.098,-70.245,12.004,-70.287,11.886,-70.22,11.73,-70.22,11.681,-70.193,11.625,-69.911,11.672,-69.817,11.672,-69.773,11.541,-69.805,11.474,-69.885,11.444,-70.049,11.53,-70.097,11.52,-70.16,11.428,-70.233,11.373,-70.546,11.261,-70.821,11.208,-71.264,11,-71.47,10.964,-71.461,10.836,-71.545,10.779,-71.518,10.622,-71.494,10.533,-71.463,10.469,-71.387,10.264,-71.262,10.144,-71.207,10.015,-71.082,9.833,-71.053,9.706,-71.078,9.511,-71.086,9.348,-71.205,9.222,-71.241,9.16,-71.298,9.126,-71.537,9.048,-71.62,9.048,-71.687,9.073,-71.74,9.134,-71.781,9.25,-71.761,9.336,-71.806,9.386,-71.873,9.428,-71.976,9.553,-71.993,9.642,-72.113,9.816,-71.956,10.108,-71.885,10.167,-71.794,10.316,-71.665,10.444,-71.594,10.657,-71.598,10.726,-71.69,10.835,-71.731,10.995,-71.676,10.997,-71.642,11.014,-71.791,11.135,-71.835,11.19,-71.947,11.414,-71.957,11.483,-71.957,11.57,-71.908,11.608,-71.869,11.627,-71.488,11.719,-71.415,11.755,-71.349,11.815,-71.32,11.862,-71.284,11.918,-71.137,12.046,-71.155,12.164,-71.262,12.335,-71.494,12.432,-71.597,12.434,-71.715,12.42,-71.919,12.309,-71.931,12.27,-71.97,12.238,-72.055,12.238,-72.136,12.189,-72.165,12.06,-72.275,11.889,-72.447,11.802,-72.722,11.712,-73.313,11.296,-73.677,11.271,-73.796,11.276,-73.91,11.309,-74.059,11.341,-74.143,11.321,-74.2,11.266,-74.219,11.105,-74.3,10.952,-74.35,10.814,-74.401,10.765,-74.46,10.787,-74.516,10.863,-74.492,10.934,-74.41,10.967,-74.352,10.975,-74.33,10.997,-74.454,10.989,-74.845,11.11,-74.922,11.058,-75.123,10.87,-75.248,10.783,-75.281,10.727,-75.446,10.611,-75.493,10.528,-75.554,10.328,-75.671,10.196,-75.708,10.143,-75.642,10.172,-75.558,10.236,-75.539,10.205,-75.596,10.126,-75.593,9.993,-75.637,9.834,-75.68,9.73,-75.635,9.658,-75.604,9.538,-75.639,9.45,-75.756,9.416,-75.905,9.431,-76.027,9.366,-76.135,9.266,-76.277,8.989,-76.689,8.695,-76.802,8.641,-76.888,8.62,-76.92,8.574,-76.872,8.513,-76.819,8.465,-76.772,8.311,-76.742,8.002,-76.787,7.932,-76.867,7.918,-76.897,7.939,-76.925,7.973,-76.912,8.033,-76.869,8.063,-76.852,8.09,-76.891,8.128,-76.936,8.147,-76.992,8.25,-77.13,8.401,-77.262,8.494,-77.344,8.637,-77.374,8.658,-77.697,8.889,-77.831,9.068,-78.083,9.236,-78.504,9.406,-78.697,9.435,-78.932,9.428,-78.975,9.453,-79.017,9.51,-79.112,9.537,-79.212,9.532,-79.355,9.569,-79.577,9.598,-79.652,9.558,-79.723,9.479,-79.855,9.378,-79.915,9.361,-79.978,9.344,-80.127,9.21,-80.547,9.082,-80.676,9.022,-80.839,8.887,-81.063,8.813,-81.204,8.787,-81.355,8.781,-81.546,8.827,-81.712,9.019,-81.803,9.074,-81.842,9.119,-81.894,9.14,-81.9,9.111,-81.831,9.046,-81.78,8.957,-81.826,8.944,-81.894,8.956,-82.078,8.935,-82.133,8.98,-82.244,9.031,-82.235,9.142,-82.201,9.168,-82.188,9.192,-82.205,9.215,-82.272,9.191,-82.34,9.209,-82.375,9.337,-82.363,9.382,-82.371,9.429,-82.5,9.523,-82.564,9.577,-82.61,9.616,-82.778,9.67,-82.81,9.735,-82.866,9.771,-83.029,9.991,-83.125,10.042,-83.347,10.315,-83.448,10.466,-83.575,10.735,-83.588,10.815,-83.617,10.877,-83.642,10.917,-83.714,10.934,-83.768,11.01,-83.832,11.131,-83.868,11.3,-83.859,11.354,-83.829,11.428,-83.777,11.504,-83.745,11.567,-83.652,11.642,-83.664,11.724,-83.705,11.825,-83.753,11.821,-83.793,11.836,-83.829,11.861,-83.813,11.896,-83.769,11.932,-83.773,11.977,-83.767,12.059,-83.716,12.057,-83.698,12.03,-83.68,12.024,-83.669,12.228,-83.651,12.287,-83.667,12.337,-83.716,12.407,-83.754,12.502,-83.718,12.553,-83.682,12.568,-83.625,12.613,-83.593,12.713,-83.578,12.667,-83.591,12.579,-83.624,12.515,-83.627,12.459,-83.596,12.396,-83.565,12.393,-83.511,12.412,-83.518,12.514,-83.541,12.596,-83.514,12.944,-83.567,13.32,-83.494,13.739,-83.412,13.996,-83.347,14.057,-83.281,14.154,-83.212,14.267,-83.188,14.34,-83.299,14.749,-83.341,14.765,-83.375,14.766,-83.414,14.825,-83.389,14.871,-83.344,14.902,-83.306,14.891,-83.302,14.802,-83.28,14.813,-83.216,14.932,-83.185,14.956,-83.158,14.993,-83.226,15.042,-83.291,15.079,-83.369,15.24,-83.646,15.368,-83.676,15.365,-83.551,15.294,-83.498,15.222,-83.536,15.219,-83.59,15.266,-83.672,15.261,-83.716,15.219,-83.76,15.22,-83.802,15.289,-83.871,15.353,-83.927,15.394,-84.013,15.414,-84.048,15.398,-84.095,15.401,-84.105,15.43,-84.111,15.492,-84.083,15.511,-83.973,15.52,-83.765,15.405,-83.775,15.437,-84.261,15.823,-84.52,15.873,-84.49,15.847,-84.426,15.829,-84.44,15.813,-84.492,15.794,-84.56,15.802,-84.646,15.884,-84.974,15.99,-85.048,15.974,-85.164,15.918,-85.484,15.9,-85.784,16.003,-85.986,16.024,-85.954,16.002,-85.936,15.953,-86.069,15.906,-86.181,15.885,-86.357,15.783,-86.481,15.801,-86.757,15.794,-86.907,15.762,-87.286,15.834,-87.377,15.826,-87.487,15.79,-87.545,15.832,-87.618,15.91,-87.702,15.911,-87.875,15.879,-87.907,15.863,-88.01,15.786,-88.055,15.765,-88.131,15.701,-88.228,15.729,-88.594,15.95,-88.598,15.927,-88.572,15.901,-88.536,15.85,-88.603,15.764,-88.709,15.807,-88.798,15.863,-88.84,15.869,-88.894,15.891,-88.912,15.956,-88.879,16.017,-88.695,16.248,-88.562,16.29,-88.461,16.434,-88.405,16.489,-88.313,16.633,-88.262,16.963,-88.294,17.192,-88.289,17.313,-88.267,17.393,-88.203,17.517,-88.272,17.61,-88.221,17.751,-88.207,17.846,-88.097,18.122,-88.085,18.226,-88.13,18.351,-88.247,18.355,-88.296,18.344,-88.349,18.359,-88.296,18.472,-88.276,18.515,-88.195,18.643,-88.197,18.72,-88.127,18.773,-88.074,18.834,-88.032,18.839,-88.011,18.727,-88.056,18.524,-88.039,18.484,-87.96,18.441,-87.882,18.274,-87.853,18.269,-87.804,18.357,-87.762,18.446,-87.734,18.655,-87.653,18.799,-87.594,19.046,-87.501,19.288,-87.509,19.317,-87.551,19.321,-87.622,19.25,-87.656,19.258,-87.659,19.352,-87.628,19.383,-87.567,19.416,-87.513,19.426,-87.483,19.444,-87.435,19.502,-87.425,19.583,-87.469,19.586,-87.512,19.575,-87.587,19.573,-87.645,19.554,-87.69,19.594,-87.688,19.637,-87.586,19.779,-87.507,19.827,-87.466,19.824,-87.442,19.862,-87.432,19.898,-87.466,19.999,-87.467,20.102,-87.421,20.231,-87.221,20.507,-87.06,20.631,-86.926,20.786,-86.865,20.885,-86.816,21.005,-86.772,21.151,-86.804,21.2,-86.817,21.234,-86.824,21.422,-86.912,21.463,-87.035,21.592,-87.128,21.621,-87.216,21.582,-87.276,21.572,-87.369,21.574,-87.387,21.551,-87.296,21.525,-87.249,21.527,-87.211,21.544,-87.188,21.546,-87.164,21.514,-87.188,21.477,-87.218,21.458,-87.251,21.447,-87.48,21.472,-87.689,21.536,-87.774,21.55,-88.007,21.604,-88.132,21.616,-88.171,21.604,-88.172,21.591,-88.185,21.579,-88.251,21.567,-88.467,21.569,-88.585,21.539,-88.747,21.448,-88.879,21.414,-89.82,21.275,-89.888,21.253,-90.183,21.121,-90.353,21.009,-90.435,20.758,-90.458,20.714,-90.484,20.556,-90.478,20.38,-90.486,20.224,-90.482,20.026,-90.492,19.947,-90.507,19.912,-90.65,19.796,-90.693,19.73,-90.739,19.352,-90.955,19.152,-91.059,19.098,-91.136,19.038,-91.437,18.89,-91.458,18.865,-91.469,18.833,-91.446,18.833,-91.343,18.901,-91.334,18.877,-91.368,18.806,-91.356,18.777,-91.308,18.773,-91.279,18.721,-91.275,18.624,-91.44,18.542,-91.534,18.457,-91.6,18.447,-91.803,18.471,-91.914,18.529,-91.943,18.563,-91.88,18.6,-91.88,18.638,-91.974,18.716,-92.103,18.704,-92.213,18.685,-92.441,18.675,-92.485,18.665,-92.71,18.612,-92.729,18.575,-92.769,18.524,-92.885,18.469,-93.127,18.423,-93.228,18.444,-93.552,18.43,-93.764,18.358,-93.873,18.304,-94.189,18.195,-94.392,18.166,-94.46,18.167,-94.546,18.175,-94.682,18.348,-94.798,18.515,-95.015,18.571,-95.182,18.701,-95.561,18.719,-95.697,18.775,-95.72,18.768,-95.655,18.724,-95.578,18.69,-95.627,18.691,-95.821,18.755,-95.92,18.82,-95.928,18.85,-95.81,18.804,-95.778,18.806,-95.913,18.897,-95.985,19.054,-96.073,19.106,-96.124,19.199,-96.29,19.344,-96.315,19.473,-96.368,19.567,-96.456,19.87,-96.709,20.188,-97.121,20.615,-97.186,20.717,-97.195,20.8,-97.357,21.104,-97.501,21.398,-97.501,21.432,-97.515,21.478,-97.567,21.508,-97.598,21.536,-97.638,21.604,-97.754,22.027,-97.59,21.762,-97.457,21.612,-97.383,21.567,-97.385,21.524,-97.424,21.465,-97.434,21.356,-97.409,21.273,-97.388,21.374,-97.337,21.438,-97.315,21.564,-97.36,21.615,-97.485,21.705,-97.585,21.809,-97.763,22.106,-97.782,22.279,-97.842,22.51,-97.842,22.557,-97.858,22.625,-97.817,22.776,-97.758,22.886,-97.745,22.942,-97.766,23.306,-97.727,23.732,-97.743,23.761,-97.729,23.788,-97.717,23.981,-97.668,24.39,-97.507,25.015,-97.424,25.233,-97.225,25.585,-97.164,25.755,-97.146,25.961,-97.14,26.03,-97.15,26.065,-97.214,26.068,-97.402,26.397,-97.435,26.486,-97.466,26.692,-97.494,26.76,-97.527,26.908,-97.555,26.967,-97.517,27.053,-97.476,27.118,-97.475,27.173,-97.485,27.237,-97.692,27.287,-97.768,27.458,-97.682,27.395,-97.524,27.314,-97.48,27.317,-97.439,27.328,-97.38,27.419,-97.289,27.671,-97.431,27.837,-97.404,27.859,-97.374,27.87,-97.252,27.854,-97.171,27.88,-97.073,27.986,-97.034,28.094,-97.141,28.061,-97.155,28.103,-97.156,28.144,-97.096,28.158,-97.015,28.163,-96.967,28.19,-96.933,28.224,-96.92,28.185,-96.892,28.158,-96.84,28.194,-96.807,28.22,-96.795,28.321,-96.774,28.422,-96.676,28.341,-96.562,28.367,-96.489,28.406,-96.421,28.457,-96.475,28.479,-96.525,28.489,-96.64,28.709,-96.608,28.723,-96.576,28.716,-96.56,28.684,-96.526,28.648,-96.449,28.594,-96.374,28.631,-96.373,28.657,-96.275,28.655,-96.115,28.622,-96.011,28.632,-96.132,28.561,-96.235,28.489,-96.181,28.502,-96.02,28.587,-95.853,28.64,-95.732,28.712,-95.656,28.745,-95.388,28.898,-95.273,28.964,-95.152,29.079,-95.139,29.168,-95.018,29.259,-94.888,29.371,-94.936,29.46,-94.993,29.531,-95.023,29.702,-94.982,29.713,-94.93,29.68,-94.89,29.677,-94.832,29.753,-94.742,29.75,-94.724,29.655,-94.778,29.548,-94.733,29.535,-94.605,29.568,-94.526,29.548,-94.75,29.418,-94.76,29.384,-94.574,29.485,-94.1,29.67,-93.89,29.689,-93.886,29.723,-93.946,29.815,-93.841,29.98,-93.794,29.977,-93.769,29.952,-93.773,29.914,-93.809,29.851,-93.848,29.819,-93.884,29.81,-93.866,29.756,-93.826,29.725,-93.766,29.753,-93.695,29.77,-93.388,29.777,-93.283,29.789,-93.176,29.779,-92.952,29.714,-92.791,29.635,-92.671,29.597,-92.261,29.557,-92.084,29.593,-92.059,29.617,-92.114,29.668,-92.135,29.699,-92.08,29.761,-92.017,29.8,-91.893,29.836,-91.824,29.751,-91.672,29.746,-91.565,29.605,-91.514,29.555,-91.331,29.514,-91.278,29.563,-91.249,29.564,-91.26,29.505,-91.244,29.457,-91.155,29.351,-91.151,29.318,-91.237,29.331,-91.283,29.321,-91.29,29.289,-91.003,29.194,-90.751,29.131,-90.677,29.151,-90.586,29.272,-90.502,29.3,-90.379,29.295,-90.302,29.256,-90.247,29.131,-90.213,29.105,-90.136,29.136,-90.101,29.182,-90.083,29.24,-90.074,29.297,-90.053,29.337,-90.052,29.431,-90.101,29.463,-90.141,29.48,-90.161,29.504,-90.159,29.537,-89.877,29.458,-89.818,29.416,-89.797,29.381,-89.792,29.333,-89.717,29.313,-89.672,29.317,-89.62,29.302,-89.577,29.268,-89.522,29.249,-89.443,29.194,-89.389,29.105,-89.354,29.07,-89.376,28.981,-89.331,28.999,-89.236,29.081,-89.195,29.054,-89.156,29.017,-89.133,29.046,-89.11,29.099,-89.021,29.143,-89.016,29.203,-89.065,29.218,-89.117,29.248,-89.181,29.336,-89.246,29.333,-89.514,29.42,-89.58,29.486,-89.675,29.539,-89.721,29.619,-89.689,29.646,-89.683,29.675,-89.662,29.684,-89.621,29.674,-89.559,29.698,-89.591,29.725,-89.531,29.772,-89.455,29.784,-89.354,29.82,-89.363,29.84,-89.358,29.921,-89.401,29.978,-89.414,30.011,-89.401,30.046,-89.494,30.058,-89.563,30.002,-89.59,29.915,-89.632,29.904,-89.744,29.93,-89.815,30.007,-89.777,30.046,-89.715,30.078,-89.665,30.117,-89.668,30.145,-89.737,30.172,-89.773,30.137,-89.812,30.124,-89.894,30.126,-89.994,30.059,-90.175,30.029,-90.285,30.065,-90.413,30.14,-90.332,30.278,-90.225,30.379,-90.126,30.369,-90.045,30.351,-89.954,30.269,-89.588,30.166,-89.521,30.193,-89.444,30.223,-89.321,30.345,-89.264,30.344,-89.224,30.332,-89.054,30.368,-88.905,30.415,-88.873,30.416,-88.82,30.406,-88.692,30.355,-88.4,30.371,-88.35,30.373,-88.249,30.363,-88.135,30.367,-88.117,30.415,-88.078,30.566,-88.032,30.681,-88.011,30.694,-87.949,30.627,-87.923,30.562,-87.924,30.45,-87.898,30.414,-87.857,30.407,-87.813,30.347,-87.79,30.292,-87.904,30.259,-87.985,30.254,-88.006,30.231,-87.622,30.265,-87.513,30.368,-87.49,30.378,-87.448,30.394,-87.444,30.364,-87.501,30.309,-87.476,30.294,-87.281,30.339,-87.251,30.397,-87.185,30.454,-87.171,30.539,-87.119,30.539,-87.072,30.5,-87.034,30.554,-86.998,30.57,-86.965,30.502,-86.986,30.431,-87.124,30.397,-87.164,30.374,-87.201,30.339,-86.968,30.372,-86.68,30.403,-86.606,30.425,-86.523,30.467,-86.448,30.496,-86.374,30.482,-86.257,30.493,-86.166,30.464,-86.138,30.442,-86.124,30.406,-86.24,30.429,-86.454,30.399,-86.175,30.333,-85.856,30.214,-85.791,30.172,-85.756,30.167,-85.743,30.201,-85.741,30.244,-85.676,30.279,-85.604,30.287,-85.641,30.237,-85.663,30.189,-85.61,30.148,-85.623,30.117,-85.676,30.122,-85.504,29.976,-85.354,29.876,-85.307,29.798,-85.315,29.758,-85.336,29.74,-85.383,29.785,-85.414,29.842,-85.414,29.768,-85.376,29.695,-85.319,29.68,-85.186,29.708,-85.029,29.721,-84.969,29.745,-84.889,29.778,-84.801,29.773,-84.55,29.898,-84.454,29.91,-84.383,29.907,-84.359,29.929,-84.375,29.982,-84.356,30.029,-84.31,30.065,-84.044,30.104,-83.694,29.926,-83.29,29.452,-82.769,29.052,-82.651,28.888,-82.644,28.812,-82.651,28.77,-82.661,28.486,-82.749,28.237,-82.844,27.846,-82.808,27.777,-82.775,27.734,-82.743,27.709,-82.715,27.733,-82.661,27.718,-82.626,27.746,-82.611,27.777,-82.597,27.873,-82.634,27.898,-82.675,27.964,-82.636,27.981,-82.58,27.958,-82.521,27.878,-82.498,27.868,-82.446,27.903,-82.406,27.863,-82.401,27.835,-82.431,27.771,-82.521,27.678,-82.636,27.525,-82.687,27.515,-82.715,27.5,-82.655,27.449,-82.62,27.401,-82.441,27.06,-82.354,26.936,-82.29,26.871,-82.243,26.849,-82.181,26.84,-82.169,26.874,-82.181,26.937,-82.096,26.963,-82.013,26.962,-82.067,26.892,-82.078,26.704,-82.04,26.552,-82.006,26.54,-81.97,26.552,-81.921,26.631,-81.882,26.665,-81.829,26.687,-81.896,26.597,-81.959,26.49,-81.931,26.467,-81.867,26.435,-81.811,26.146,-81.715,25.983,-81.568,25.892,-81.365,25.831,-81.345,25.732,-81.227,25.583,-81.113,25.367,-81.057,25.338,-80.98,25.312,-80.94,25.264,-80.965,25.224,-81.098,25.319,-81.136,25.31,-81.159,25.269,-81.167,25.229,-81.11,25.138,-81.012,25.133,-80.862,25.176,-80.737,25.156,-80.558,25.232,-80.485,25.23,-80.367,25.331,-80.328,25.427,-80.301,25.619,-80.219,25.742,-80.159,25.878,-80.143,25.874,-80.136,25.843,-80.126,25.833,-80.111,26.132,-80.041,26.569,-80.05,26.808,-80.089,26.994,-80.126,27.083,-80.226,27.207,-80.65,28.181,-80.686,28.272,-80.749,28.381,-80.787,28.561,-80.818,28.636,-80.838,28.758,-80.809,28.759,-80.771,28.732,-80.78,28.683,-80.766,28.633,-80.7,28.601,-80.688,28.579,-80.729,28.516,-80.732,28.463,-80.694,28.345,-80.665,28.375,-80.654,28.452,-80.633,28.518,-80.607,28.523,-80.623,28.32,-80.61,28.178,-80.5,27.934,-80.457,27.901,-80.533,28.07,-80.573,28.181,-80.585,28.272,-80.581,28.365,-80.568,28.426,-80.524,28.486,-80.564,28.556,-80.9,29.05,-81.105,29.457,-81.25,29.794,-81.337,30.141,-81.386,30.27,-81.457,30.641,-81.504,30.731,-81.516,30.802,-81.52,30.875,-81.501,30.914,-81.471,31.009,-81.453,31.088,-81.46,31.127,-81.442,31.2,-81.413,31.179,-81.365,31.172,-81.288,31.264,-81.329,31.314,-81.378,31.332,-81.381,31.353,-81.295,31.371,-81.258,31.436,-81.219,31.472,-81.175,31.531,-81.196,31.539,-81.223,31.528,-81.259,31.539,-81.242,31.574,-81.17,31.61,-81.166,31.646,-81.187,31.667,-81.198,31.704,-81.162,31.744,-81.098,31.753,-81.066,31.788,-81.065,31.813,-81.096,31.841,-81.113,31.879,-81.083,31.894,-81.046,31.892,-80.923,31.945,-80.872,32.03,-80.882,32.069,-80.849,32.114,-80.791,32.126,-80.758,32.142,-80.694,32.216,-80.702,32.246,-80.734,32.265,-80.765,32.298,-80.798,32.363,-80.803,32.448,-80.709,32.337,-80.683,32.349,-80.678,32.381,-80.647,32.396,-80.626,32.326,-80.608,32.293,-80.579,32.287,-80.514,32.324,-80.486,32.352,-80.474,32.423,-80.53,32.475,-80.634,32.512,-80.572,32.534,-80.461,32.521,-80.363,32.501,-80.268,32.537,-80.23,32.577,-80.18,32.593,-80.123,32.589,-80.022,32.62,-79.941,32.667,-79.894,32.729,-79.933,32.81,-79.805,32.787,-79.735,32.825,-79.615,32.909,-79.587,33.001,-79.499,33.027,-79.42,33.043,-79.276,33.135,-79.229,33.185,-79.281,33.315,-79.226,33.405,-79.227,33.363,-79.238,33.312,-79.194,33.244,-79.138,33.406,-78.92,33.659,-78.841,33.724,-78.578,33.873,-78.564,33.877,-78.406,33.918,-78.013,33.912,-77.971,33.993,-77.953,34.169,-77.926,34.073,-77.933,33.989,-77.928,33.94,-77.888,34.05,-77.861,34.149,-77.751,34.285,-77.697,34.332,-77.65,34.358,-77.518,34.451,-77.38,34.527,-77.402,34.555,-77.413,34.592,-77.412,34.731,-77.384,34.694,-77.358,34.62,-77.296,34.603,-77.252,34.616,-77.134,34.708,-77.05,34.697,-76.896,34.701,-76.797,34.704,-76.733,34.707,-76.707,34.752,-76.618,34.77,-76.517,34.777,-76.44,34.843,-76.362,34.937,-76.457,34.989,-76.745,34.941,-76.899,34.97,-76.975,35.025,-77.07,35.155,-76.861,35.005,-76.779,34.99,-76.628,35.073,-76.613,35.104,-76.608,35.153,-76.566,35.215,-76.513,35.27,-76.553,35.306,-76.595,35.33,-76.974,35.458,-77.04,35.527,-76.887,35.463,-76.741,35.431,-76.634,35.453,-76.611,35.53,-76.577,35.532,-76.532,35.508,-76.516,35.436,-76.49,35.397,-76.447,35.408,-76.39,35.401,-76.275,35.369,-76.174,35.354,-76.104,35.38,-75.966,35.508,-75.774,35.647,-75.745,35.765,-75.759,35.843,-75.772,35.9,-75.812,35.956,-75.854,35.96,-75.979,35.896,-76.001,35.722,-76.046,35.691,-76.084,35.691,-76.076,35.788,-76.06,35.879,-76.07,35.97,-76.207,35.991,-76.264,35.967,-76.358,35.953,-76.504,35.956,-76.611,35.944,-76.726,35.958,-76.719,36.033,-76.74,36.133,-76.734,36.229,-76.718,36.148,-76.679,36.075,-76.559,36.015,-76.479,36.028,-76.424,36.068,-76.384,36.134,-76.321,36.138,-76.227,36.116,-76.271,36.19,-76.222,36.167,-76.15,36.146,-76.141,36.215,-76.148,36.279,-76.055,36.235,-75.95,36.209,-75.883,36.176,-75.82,36.113,-75.867,36.268,-75.925,36.383,-75.978,36.429,-75.993,36.474,-75.966,36.551,-75.96,36.571,-75.973,36.6,-75.965,36.638,-75.946,36.659,-75.918,36.633,-75.894,36.567,-75.889,36.55,-75.81,36.271,-75.728,36.104,-75.58,35.872,-75.534,35.819,-75.559,35.879,-75.758,36.229,-75.857,36.551,-75.89,36.657,-75.942,36.766,-75.966,36.862,-75.999,36.913,-76.144,36.931,-76.244,36.953,-76.4,36.89,-76.488,36.897,-76.505,36.961,-76.634,37.047,-76.672,37.173,-76.765,37.184,-76.925,37.225,-77.002,37.271,-77.196,37.296,-77.227,37.309,-77.251,37.329,-77.007,37.318,-76.704,37.218,-76.631,37.222,-76.602,37.143,-76.507,37.072,-76.462,37.031,-76.401,36.991,-76.338,37.013,-76.283,37.053,-76.301,37.111,-76.327,37.149,-76.401,37.213,-76.497,37.247,-76.611,37.323,-76.738,37.449,-76.756,37.479,-76.758,37.505,-76.538,37.309,-76.454,37.274,-76.393,37.3,-76.405,37.332,-76.401,37.386,-76.263,37.357,-76.254,37.431,-76.269,37.495,-76.368,37.53,-76.306,37.571,-76.484,37.629,-76.549,37.669,-76.62,37.755,-76.715,37.81,-76.849,37.94,-76.925,38.033,-77.111,38.166,-77.071,38.167,-76.94,38.095,-76.829,37.962,-76.793,37.938,-76.492,37.682,-76.437,37.67,-76.344,37.676,-76.306,37.722,-76.293,37.794,-76.262,37.848,-76.264,37.894,-76.355,37.963,-76.472,38.011,-76.55,38.094,-76.645,38.134,-76.906,38.197,-77.047,38.357,-77.11,38.37,-77.232,38.34,-77.273,38.352,-77.314,38.397,-77.284,38.529,-77.26,38.6,-77.165,38.677,-77.092,38.72,-77.046,38.776,-77.03,38.889,-77.018,38.778,-77.054,38.706,-77.135,38.65,-77.221,38.541,-77.242,38.495,-77.233,38.408,-77.156,38.397,-77.077,38.442,-77.001,38.445,-76.988,38.394,-76.95,38.347,-76.89,38.292,-76.868,38.337,-76.868,38.39,-76.769,38.263,-76.594,38.228,-76.454,38.174,-76.402,38.125,-76.341,38.087,-76.333,38.141,-76.366,38.197,-76.409,38.268,-76.642,38.454,-76.669,38.538,-76.677,38.612,-76.659,38.58,-76.647,38.539,-76.572,38.436,-76.51,38.404,-76.439,38.362,-76.394,38.369,-76.416,38.42,-76.458,38.475,-76.501,38.532,-76.537,38.743,-76.521,38.788,-76.516,38.841,-76.52,38.898,-76.494,38.945,-76.519,39.001,-76.559,39.065,-76.546,39.068,-76.473,39.031,-76.42,39.074,-76.428,39.126,-76.489,39.159,-76.574,39.254,-76.57,39.269,-76.421,39.225,-76.403,39.253,-76.406,39.304,-76.359,39.325,-76.345,39.365,-76.347,39.388,-76.331,39.404,-76.276,39.323,-76.257,39.352,-76.248,39.439,-76.223,39.42,-76.216,39.38,-76.141,39.403,-76.097,39.433,-76.081,39.47,-76.085,39.527,-76.063,39.561,-76.006,39.569,-75.959,39.585,-75.97,39.505,-75.873,39.511,-75.913,39.468,-75.955,39.46,-76.003,39.411,-75.939,39.399,-75.876,39.376,-75.976,39.367,-76.074,39.369,-76.153,39.315,-76.236,39.192,-76.217,39.064,-76.133,39.123,-76.135,39.082,-76.186,38.991,-76.245,39.009,-76.313,39.009,-76.33,38.953,-76.331,38.909,-76.241,38.943,-76.191,38.916,-76.168,38.853,-76.247,38.823,-76.3,38.818,-76.341,38.71,-76.308,38.723,-76.278,38.772,-76.213,38.758,-76.175,38.707,-76.057,38.621,-76.017,38.625,-76.001,38.602,-76.113,38.602,-76.198,38.619,-76.264,38.6,-76.295,38.495,-76.265,38.436,-76.212,38.361,-76.117,38.318,-76.051,38.28,-76.02,38.295,-76.007,38.323,-75.986,38.332,-75.967,38.291,-75.937,38.31,-75.889,38.356,-75.859,38.362,-75.877,38.319,-75.864,38.261,-75.885,38.214,-75.928,38.169,-75.891,38.147,-75.856,38.14,-75.795,38.087,-75.829,38.033,-75.851,37.972,-75.735,37.974,-75.659,37.954,-75.719,37.821,-75.792,37.756,-75.888,37.619,-75.975,37.398,-75.997,37.264,-75.985,37.212,-75.934,37.152,-75.854,37.297,-75.812,37.425,-75.767,37.473,-75.699,37.516,-75.632,37.535,-75.587,37.559,-75.596,37.631,-75.376,38.025,-75.354,38.065,-75.292,38.129,-75.225,38.242,-75.16,38.255,-75.142,38.298,-75.134,38.384,-75.117,38.406,-75.09,38.425,-75.073,38.41,-75.074,38.366,-75.051,38.383,-75.039,38.426,-75.038,38.456,-75.036,38.503,-75.073,38.579,-75.111,38.599,-75.187,38.591,-75.128,38.632,-75.084,38.723,-75.089,38.778,-75.185,38.819,-75.31,38.967,-75.392,39.093,-75.413,39.281,-75.52,39.403,-75.574,39.477,-75.567,39.553,-75.582,39.589,-75.588,39.641,-75.502,39.717,-75.464,39.781,-75.421,39.815,-75.401,39.832,-75.321,39.865,-75.173,39.895,-75.074,39.983,-75.104,39.932,-75.154,39.871,-75.353,39.83,-75.422,39.79,-75.472,39.712,-75.524,39.602,-75.519,39.532,-75.524,39.49,-75.353,39.34,-75.231,39.284,-75.136,39.208,-75.05,39.211,-74.975,39.188,-74.897,39.145,-74.92,39.047,-74.954,38.95,-74.923,38.941,-74.794,39.002,-74.646,39.208,-74.605,39.248,-74.603,39.293,-74.579,39.316,-74.517,39.347,-74.474,39.343,-74.429,39.387,-74.411,39.455,-74.39,39.487,-74.407,39.549,-74.331,39.536,-74.257,39.614,-74.176,39.727,-74.118,39.938,-74.096,39.976,-74.065,39.993,-74.084,39.829,-74.08,39.788,-74.049,39.923,-74.028,40.073,-74.004,40.171,-73.972,40.251,-73.958,40.328,-73.972,40.4,-73.998,40.452,-74.05,40.43,-74.242,40.456,-74.264,40.529,-74.227,40.608,-74.187,40.648,-74.153,40.673,-74.116,40.687,-74.067,40.72,-74.025,40.756,-73.927,40.914,-73.91,40.992,-73.909,40.996,-73.918,41.136,-73.97,41.25,-73.925,41.218,-73.882,41.171,-73.872,41.055,-73.907,40.912,-73.949,40.839,-73.987,40.751,-73.947,40.777,-73.911,40.816,-73.851,40.831,-73.779,40.878,-73.671,40.966,-73.63,40.992,-73.583,41.022,-73.182,41.176,-73.024,41.216,-72.925,41.285,-72.847,41.266,-72.479,41.276,-72.371,41.312,-72.265,41.292,-72.074,41.326,-71.93,41.341,-71.842,41.335,-71.769,41.331,-71.523,41.379,-71.444,41.454,-71.427,41.633,-71.364,41.703,-71.39,41.795,-71.359,41.786,-71.331,41.762,-71.311,41.72,-71.271,41.681,-71.234,41.707,-71.178,41.744,-71.149,41.746,-71.204,41.641,-71.188,41.516,-71.169,41.489,-71.08,41.538,-70.974,41.549,-70.701,41.715,-70.666,41.71,-70.655,41.608,-70.668,41.558,-70.657,41.534,-70.481,41.582,-70.405,41.627,-70.06,41.677,-69.987,41.684,-69.949,41.677,-69.934,41.71,-69.942,41.808,-69.978,41.961,-70.109,42.078,-70.16,42.097,-70.204,42.101,-70.241,42.091,-70.237,42.071,-70.196,42.035,-70.173,42.063,-70.11,42.03,-70.09,41.98,-70.006,41.872,-70.001,41.826,-70.135,41.77,-70.295,41.729,-70.427,41.757,-70.515,41.803,-70.549,41.939,-70.656,41.987,-70.645,42.022,-70.618,42.04,-70.738,42.229,-70.818,42.265,-70.997,42.3,-71.046,42.331,-70.93,42.432,-70.871,42.497,-70.831,42.553,-70.752,42.57,-70.661,42.617,-70.613,42.623,-70.604,42.65,-70.624,42.672,-70.655,42.674,-70.697,42.665,-70.736,42.669,-70.781,42.721,-70.8,42.774,-70.829,42.825,-70.806,42.877,-70.778,42.941,-70.733,43.07,-70.691,43.109,-70.642,43.134,-70.521,43.349,-70.36,43.48,-70.203,43.626,-70.238,43.656,-70.269,43.672,-70.179,43.766,-70.062,43.835,-69.965,43.855,-69.975,43.818,-69.974,43.788,-69.926,43.797,-69.873,43.82,-69.84,43.79,-69.808,43.772,-69.792,43.805,-69.803,43.867,-69.795,43.911,-69.772,43.899,-69.762,43.861,-69.73,43.852,-69.699,43.955,-69.653,43.994,-69.637,43.949,-69.624,43.881,-69.59,43.887,-69.557,43.983,-69.542,43.963,-69.521,43.897,-69.481,43.905,-69.435,43.956,-69.345,44.001,-69.226,43.986,-69.137,44.038,-69.068,44.098,-69.064,44.172,-68.956,44.348,-68.961,44.434,-68.847,44.485,-68.8,44.549,-68.763,44.571,-68.766,44.51,-68.795,44.454,-68.777,44.446,-68.736,44.454,-68.71,44.443,-68.794,44.382,-68.812,44.339,-68.723,44.342,-68.612,44.311,-68.572,44.271,-68.533,44.259,-68.514,44.304,-68.521,44.38,-68.479,44.446,-68.451,44.508,-68.417,44.469,-68.374,44.445,-68.317,44.474,-68.277,44.507,-68.246,44.515,-68.198,44.515,-68.152,44.502,-68.117,44.491,-68.094,44.439,-68.057,44.384,-68.014,44.401,-67.985,44.42,-67.963,44.464,-67.907,44.454,-67.839,44.576,-67.79,44.586,-67.727,44.567,-67.653,44.562,-67.599,44.577,-67.556,44.645,-67.458,44.657,-67.364,44.697,-67.191,44.676,-66.987,44.828,-66.991,44.85,-67.014,44.868,-67.107,44.885,-67.114,44.944,-67.08,44.989,-67.102,45.088,-67.13,45.139,-67.125,45.169,-67.084,45.144,-66.977,45.157,-66.919,45.146,-66.908,45.098,-66.872,45.067,-66.707,45.083,-66.511,45.143,-66.44,45.096,-66.352,45.133,-66.252,45.189,-66.144,45.228,-66.107,45.257,-66.183,45.335,-66.09,45.376,-66.065,45.401,-66.027,45.418,-66.067,45.359,-66.11,45.317,-65.956,45.222,-65.884,45.223,-65.545,45.337,-65.282,45.473,-65.057,45.544,-64.898,45.626,-64.779,45.638,-64.594,45.814,-64.642,45.913,-64.633,45.947,-64.536,45.867,-64.482,45.806,-64.404,45.827,-64.315,45.836,-64.351,45.783,-64.397,45.756,-64.56,45.625,-64.827,45.476,-64.913,45.375,-64.873,45.355,-64.832,45.35,-64.747,45.324,-64.681,45.383,-64.6,45.41,-64.336,45.39,-64.087,45.411,-63.906,45.378,-63.614,45.394,-63.368,45.365,-63.46,45.321,-63.748,45.311,-64.093,45.217,-64.183,45.147,-64.135,45.023,-64.235,45.114,-64.354,45.138,-64.366,45.187,-64.359,45.238,-64.34,45.268,-64.331,45.309,-64.448,45.337,-64.407,45.306,-64.449,45.256,-64.751,45.18,-64.903,45.121,-65.657,44.76,-65.692,44.738,-65.728,44.697,-65.587,44.729,-65.502,44.76,-65.52,44.733,-65.616,44.68,-65.682,44.651,-65.778,44.646,-65.917,44.615,-66.022,44.562,-66.091,44.505,-66.125,44.47,-66.146,44.436,-65.942,44.576,-65.868,44.569,-66.1,44.367,-66.193,44.144,-66.193,44.08,-66.126,43.814,-66.038,43.742,-66.002,43.778,-65.978,43.815,-65.887,43.795,-65.835,43.734,-65.738,43.561,-65.662,43.534,-65.564,43.553,-65.482,43.518,-65.45,43.524,-65.429,43.561,-65.386,43.565,-65.344,43.55,-65.33,43.668,-65.235,43.727,-65.172,43.731,-65.087,43.727,-64.862,43.868,-64.826,43.929,-64.692,44.021,-64.578,44.142,-64.469,44.185,-64.378,44.304,-64.335,44.292,-64.276,44.334,-64.312,44.415,-64.339,44.445,-64.286,44.55,-64.167,44.587,-64.101,44.487,-64.045,44.545,-64.045,44.588,-64,44.645,-63.924,44.604,-63.891,44.546,-63.821,44.511,-63.761,44.486,-63.61,44.48,-63.568,44.514,-63.545,44.544,-63.558,44.611,-63.604,44.683,-63.544,44.655,-63.457,44.64,-63.381,44.652,-63.306,44.643,-63.156,44.711,-63.089,44.709,-63.032,44.715,-62.768,44.785,-62.514,44.844,-62.265,44.936,-62.027,44.994,-61.794,45.084,-61.719,45.094,-61.647,45.131,-61.569,45.154,-61.498,45.157,-61.387,45.185,-61.284,45.235,-61.165,45.256,-61.101,45.233,-61.068,45.253,-61.032,45.292,-61.071,45.33,-61.107,45.349,-61.461,45.367,-61.376,45.411,-61.282,45.441,-61.277,45.476,-61.35,45.574,-61.428,45.648,-61.492,45.687,-61.657,45.642,-61.777,45.656,-61.877,45.714,-61.912,45.799,-61.924,45.851,-61.956,45.868,-62.218,45.731,-62.422,45.665,-62.447,45.641,-62.483,45.622,-62.586,45.661,-62.75,45.648,-62.718,45.686,-62.701,45.741,-62.911,45.776,-63.108,45.782,-63.217,45.758,-63.293,45.752,-63.316,45.78,-63.358,45.811,-63.509,45.875,-63.568,45.878,-63.703,45.858,-63.875,45.959,-64.031,46.013,-64.056,46.021,-63.832,46.107,-63.873,46.146,-63.916,46.166,-64.145,46.193,-64.212,46.22,-64.542,46.24,-64.557,46.311,-64.648,46.356,-64.641,46.426,-64.69,46.512,-64.726,46.671,-64.817,46.699,-64.883,46.823,-64.906,46.888,-64.866,46.958,-64.831,47.061,-64.942,47.086,-65.042,47.089,-65.192,47.05,-65.26,47.069,-65.319,47.101,-65.086,47.234,-64.912,47.369,-64.852,47.57,-64.766,47.673,-64.703,47.725,-64.874,47.797,-65.046,47.793,-65.002,47.847,-65.228,47.811,-65.344,47.768,-65.483,47.687,-65.607,47.67,-65.666,47.696,-65.756,47.86,-65.849,47.911,-66.21,47.989,-66.36,48.061,-66.429,48.067,-66.632,48.011,-66.704,48.022,-66.449,48.12,-66.324,48.098,-66.249,48.117,-66.083,48.103,-66.013,48.147,-65.927,48.189,-65.755,48.112,-65.476,48.031,-65.36,48.011,-65.259,48.021,-65.036,48.106,-64.96,48.16,-64.822,48.196,-64.765,48.228,-64.706,48.311,-64.633,48.36,-64.349,48.423,-64.254,48.55,-64.246,48.691,-64.415,48.804,-64.514,48.841,-64.371,48.839,-64.209,48.806,-64.216,48.874,-64.262,48.922,-64.568,49.105,-64.836,49.192,-65.396,49.262,-65.523,49.266,-65.883,49.226,-66.178,49.213,-66.598,49.126,-67.117,48.964,-67.561,48.856,-67.889,48.731,-68.238,48.626,-68.431,48.542,-68.552,48.457,-68.746,48.376,-68.816,48.366,-68.987,48.275,-69.306,48.047,-69.471,47.967,-69.581,47.824,-69.802,47.623,-70.017,47.471,-70.07,47.378,-70.218,47.29,-70.388,47.117,-70.519,47.033,-70.993,46.852,-71.152,46.819,-71.261,46.756,-71.439,46.721,-71.671,46.654,-71.901,46.632,-72.109,46.551,-72.187,46.512,-72.24,46.442,-72.366,46.405,-72.496,46.353,-72.733,46.182,-72.99,46.104,-73.16,46.01,-73.253,45.864,-73.369,45.758,-73.465,45.632,-73.484,45.587,-73.519,45.459,-73.558,45.425,-73.765,45.395,-74.05,45.241,-74.269,45.188,-74.566,45.042,-74.709,45.004,-74.358,45.206,-74.34,45.214,-74.098,45.324,-73.974,45.345,-74,45.433,-74.248,45.493,-74.315,45.531,-74.038,45.502,-73.897,45.564,-73.798,45.655,-73.712,45.711,-73.477,45.738,-73.284,45.9,-73.18,46.025,-73.145,46.066,-73.022,46.12,-72.981,46.21,-72.843,46.262,-72.68,46.287,-72.257,46.485,-72.205,46.559,-72.028,46.607,-71.88,46.687,-71.757,46.674,-71.625,46.698,-71.268,46.796,-71.116,46.925,-70.973,47.007,-70.706,47.14,-70.448,47.423,-70.3,47.503,-69.994,47.74,-69.906,47.832,-69.84,47.953,-69.775,48.098,-69.866,48.172,-69.971,48.206,-70.145,48.244,-70.501,48.354,-70.671,48.353,-70.839,48.367,-70.923,48.422,-71.018,48.456,-70.384,48.367,-70.111,48.278,-70.001,48.271,-69.852,48.207,-69.762,48.191,-69.674,48.199,-69.55,48.251,-69.375,48.386,-69.231,48.574,-68.929,48.829,-68.669,48.94,-68.628,49.007,-68.544,49.056,-68.414,49.1,-68.295,49.114,-68.221,49.15,-68.282,49.197,-68.056,49.257,-67.549,49.332,-67.469,49.335,-67.372,49.348,-67.262,49.451,-67.234,49.602,-66.941,49.994,-66.741,50.066,-66.622,50.155,-66.55,50.161,-66.496,50.212,-66.411,50.224,-66.369,50.207,-66.242,50.22,-66.126,50.201,-65.955,50.294,-65.762,50.259,-65.269,50.32,-65.181,50.298,-64.868,50.275,-64.509,50.309,-64.17,50.269,-64.016,50.304,-63.854,50.314,-63.734,50.305,-63.587,50.258,-63.239,50.243,-63.136,50.294,-62.95,50.291,-62.83,50.301,-62.715,50.302,-62.541,50.285,-62.362,50.277,-62.165,50.239,-61.92,50.233,-61.835,50.197,-61.725,50.104,-61.29,50.202,-61.181,50.192,-60.956,50.205,-60.807,50.25,-60.608,50.221,-60.438,50.239,-60.08,50.255,-59.886,50.316,-59.815,50.418,-59.612,50.492,-59.378,50.675,-59.165,50.78,-59.055,50.879,-58.638,51.172,-58.615,51.237,-58.593,51.257,-58.51,51.295,-58.442,51.306,-58.27,51.295,-58.089,51.311,-58.023,51.322,-57.854,51.4,-57.77,51.426,-57.462,51.469,-57.299,51.478,-57.1,51.443,-57.096,51.443,-57.018,51.447,-56.976,51.458,-56.549,51.681,-56.283,51.797,-56.017,51.929,-55.695,52.138,-55.673,52.19,-55.691,52.242,-55.783,52.28,-55.834,52.31,-56.005,52.37,-56.012,52.394,-55.897,52.37,-55.777,52.364,-55.716,52.392,-55.706,52.428,-55.746,52.475,-55.84,52.508,-56.053,52.537,-56.228,52.536,-56.325,52.545,-56.292,52.574,-56.167,52.575,-55.848,52.623,-55.803,52.643,-55.819,52.677,-55.873,52.736,-55.858,52.823,-55.83,52.878,-55.892,53,-55.808,53.135,-55.798,53.212,-55.817,53.246,-55.855,53.286,-55.863,53.312,-55.859,53.344,-55.911,53.391,-55.966,53.471,-56.11,53.588,-56.27,53.6,-56.354,53.624,-56.444,53.718,-56.465,53.765,-56.524,53.766,-56.697,53.758,-56.841,53.739,-57.012,53.673,-57.221,53.529,-57.332,53.469,-57.386,53.561,-57.42,53.583,-57.527,53.6,-57.524,53.611,-57.489,53.633,-57.244,53.715,-57.157,53.757,-57.135,53.792,-57.149,53.848,-57.199,53.924,-57.416,54.163,-57.615,54.191,-58.192,54.228,-58.31,54.202,-58.356,54.172,-58.361,54.154,-58.317,54.114,-58.177,54.131,-58.065,54.127,-57.928,54.104,-57.936,54.091,-58.088,54.09,-58.327,54.052,-58.652,53.978,-58.92,53.875,-59.129,53.744,-59.322,53.644,-59.498,53.575,-59.621,53.537,-59.829,53.505,-59.882,53.48,-59.987,53.393,-60.148,53.307,-60.329,53.266,-60.337,53.277,-60.346,53.289,-60.273,53.317,-60.251,53.344,-60.306,53.36,-60.29,53.391,-60.157,53.45,-60.1,53.487,-60.16,53.53,-60.37,53.607,-60.395,53.653,-60.263,53.61,-60.145,53.596,-60.117,53.61,-60.1,53.634,-60.081,53.701,-60.057,53.733,-60.014,53.762,-59.873,53.808,-59.823,53.834,-59.749,53.842,-59.653,53.831,-59.497,53.834,-59.201,53.929,-59.039,53.964,-59.013,53.976,-58.978,54.01,-58.92,54.033,-58.841,54.044,-58.719,54.039,-58.633,54.05,-58.558,54.103,-58.435,54.228,-58.359,54.253,-58.22,54.286,-58.162,54.32,-58.151,54.35,-57.889,54.384,-57.699,54.387,-57.563,54.44,-57.485,54.517,-57.404,54.57,-57.404,54.591,-57.483,54.64,-57.627,54.65,-57.725,54.674,-57.827,54.719,-57.929,54.773,-57.962,54.876,-58.058,54.882,-58.195,54.866,-58.223,54.813,-58.398,54.774,-58.5,54.783,-58.78,54.838,-58.886,54.952,-58.956,55.055,-58.997,55.149,-59.086,55.183,-59.26,55.2,-59.324,55.153,-59.394,55.081,-59.429,55.056,-59.75,54.887,-59.838,54.814,-59.816,54.867,-59.742,54.943,-59.486,55.13,-59.438,55.176,-59.518,55.197,-59.605,55.173,-59.689,55.196,-59.696,55.269,-59.759,55.31,-59.862,55.295,-59.93,55.259,-60.213,55.236,-60.341,55.194,-60.557,55.067,-60.617,55.06,-60.521,55.129,-60.45,55.2,-60.433,55.243,-60.361,55.366,-60.224,55.444,-60.192,55.481,-60.308,55.557,-60.352,55.612,-60.408,55.65,-60.365,55.709,-60.341,55.785,-60.413,55.789,-60.476,55.805,-60.562,55.727,-60.593,55.815,-60.631,55.825,-60.737,55.887,-60.743,55.941,-60.832,55.958,-60.893,55.914,-60.996,55.862,-61.089,55.866,-61.123,55.889,-61.134,55.93,-61.188,55.955,-61.351,55.974,-61.45,55.996,-61.449,56.022,-61.301,56.047,-61.324,56.076,-61.365,56.216,-61.421,56.222,-61.559,56.208,-61.713,56.231,-61.707,56.289,-61.499,56.328,-61.425,56.361,-61.515,56.39,-61.692,56.397,-61.94,56.424,-62.01,56.454,-61.899,56.505,-61.76,56.511,-61.738,56.526,-61.813,56.571,-61.855,56.584,-61.992,56.591,-62.117,56.667,-62.396,56.73,-62.497,56.802,-62.46,56.818,-62.372,56.836,-62.296,56.833,-62.382,56.788,-62.366,56.767,-62.062,56.699,-61.532,56.655,-61.372,56.681,-61.373,56.776,-61.39,56.853,-61.346,56.922,-61.334,57.011,-61.629,57.183,-61.716,57.196,-61.798,57.186,-61.861,57.198,-61.945,57.228,-61.977,57.248,-61.939,57.274,-61.886,57.348,-61.85,57.37,-61.851,57.381,-61.921,57.421,-62.088,57.453,-62.194,57.455,-62.303,57.441,-62.396,57.448,-62.455,57.462,-62.496,57.489,-62.377,57.478,-62.339,57.485,-62.254,57.529,-62.167,57.537,-62.084,57.562,-61.968,57.612,-61.931,57.669,-61.995,57.769,-61.968,57.803,-61.914,57.825,-61.899,57.861,-61.959,57.912,-62.117,57.964,-62.202,57.955,-62.306,57.972,-62.486,58.154,-62.588,58.158,-62.818,58.129,-62.981,58.093,-63.22,58.002,-63.262,58.015,-63.152,58.084,-63.063,58.127,-62.812,58.2,-62.674,58.319,-62.594,58.474,-62.608,58.496,-62.737,58.492,-62.837,58.479,-63.076,58.415,-63.132,58.411,-63.12,58.442,-63.146,58.46,-63.21,58.467,-63.296,58.441,-63.474,58.331,-63.537,58.33,-63.438,58.399,-63.39,58.453,-63.219,58.52,-63.102,58.546,-62.874,58.672,-62.926,58.765,-63.008,58.855,-63.05,58.878,-63.185,58.858,-63.282,58.867,-63.222,58.911,-63.216,58.928,-63.279,59.003,-63.31,59.026,-63.304,59.034,-63.223,59.057,-63.248,59.068,-63.326,59.082,-63.399,59.08,-63.568,59.047,-63.794,59.027,-63.941,59.027,-63.971,59.054,-63.91,59.066,-63.756,59.063,-63.646,59.079,-63.506,59.115,-63.415,59.194,-63.54,59.333,-63.637,59.341,-63.752,59.277,-63.776,59.277,-63.759,59.319,-63.781,59.349,-63.945,59.38,-63.971,59.409,-63.85,59.448,-63.75,59.513,-63.841,59.574,-63.929,59.645,-63.969,59.698,-63.979,59.754,-64.056,59.823,-64.151,59.794,-64.226,59.741,-64.169,59.847,-64.183,59.973,-64.283,60.064,-64.408,60.065,-64.559,60.043,-64.733,59.998,-64.768,60.012,-64.713,60.037,-64.528,60.095,-64.42,60.171,-64.436,60.228,-64.499,60.268,-64.614,60.306,-64.706,60.336,-64.817,60.331,-64.845,60.308,-64.89,60.287,-64.931,60.252,-65.073,60.062,-65.105,59.993,-65.172,59.908,-65.181,59.867,-65.159,59.83,-65.113,59.802,-65.028,59.771,-65.054,59.753,-65.212,59.81,-65.289,59.818,-65.358,59.809,-65.406,59.795,-65.433,59.777,-65.481,59.69,-65.486,59.649,-65.475,59.617,-65.407,59.539,-65.346,59.511,-65.263,59.495,-65.171,59.462,-65.069,59.411,-65.038,59.388,-65.074,59.378,-65.274,59.464,-65.35,59.479,-65.475,59.47,-65.489,59.448,-65.407,59.33,-65.412,59.315,-65.513,59.35,-65.545,59.32,-65.578,59.245,-65.607,59.213,-65.661,59.23,-65.692,59.229,-65.7,59.213,-65.666,59.153,-65.64,59.128,-65.606,59.111,-65.496,59.091,-65.384,59.06,-65.396,59.038,-65.526,59.036,-65.544,59.012,-65.695,59.032,-65.721,59.024,-65.722,59.003,-65.704,58.971,-65.795,58.98,-65.841,58.977,-65.921,58.915,-65.918,58.896,-65.836,58.86,-65.855,58.847,-65.967,58.839,-66.043,58.821,-66.049,58.788,-66.021,58.735,-65.95,58.65,-65.928,58.611,-65.923,58.572,-65.931,58.535,-66.002,58.431,-66.017,58.431,-66.03,58.567,-66.045,58.606,-66.091,58.659,-66.168,58.727,-66.237,58.772,-66.299,58.795,-66.362,58.791,-66.48,58.731,-66.515,58.697,-66.558,58.637,-66.608,58.549,-66.722,58.491,-66.9,58.463,-67.019,58.433,-67.163,58.37,-67.382,58.3,-67.57,58.213,-67.596,58.186,-67.617,58.14,-67.632,58.076,-67.678,57.991,-67.698,58.009,-67.681,58.107,-67.688,58.14,-67.69,58.244,-67.737,58.385,-67.756,58.405,-67.805,58.365,-67.823,58.31,-67.856,58.273,-68.009,58.152,-68.064,58.139,-67.911,58.267,-67.888,58.296,-67.888,58.329,-67.981,58.461,-68.021,58.485,-68.111,58.473,-68.176,58.403,-68.289,58.178,-68.352,58.091,-68.414,58.052,-68.495,58.012,-68.781,57.976,-69.041,57.902,-69.035,57.926,-68.945,57.969,-68.826,58,-68.597,58.037,-68.468,58.076,-68.357,58.163,-68.315,58.227,-68.234,58.399,-68.229,58.485,-68.235,58.528,-68.253,58.557,-68.326,58.595,-68.381,58.744,-68.414,58.783,-68.475,58.823,-68.563,58.866,-68.637,58.893,-68.698,58.905,-68.942,58.889,-69.064,58.898,-69.173,58.897,-69.271,58.884,-69.382,58.851,-69.651,58.728,-69.79,58.689,-69.879,58.697,-70.033,58.745,-70.154,58.761,-70.16,58.789,-69.979,58.816,-69.868,58.856,-69.842,58.881,-69.829,58.905,-69.829,58.929,-69.814,58.946,-69.784,58.956,-69.753,58.94,-69.677,58.831,-69.648,58.821,-69.608,58.829,-69.532,58.869,-69.5,58.921,-69.475,59,-69.448,59.049,-69.42,59.068,-69.414,59.087,-69.46,59.152,-69.45,59.18,-69.35,59.277,-69.344,59.303,-69.4,59.338,-69.682,59.342,-69.711,59.393,-69.692,59.488,-69.656,59.565,-69.602,59.623,-69.579,59.675,-69.587,59.722,-69.63,59.822,-69.673,59.871,-69.734,59.918,-69.806,59.945,-70.327,59.971,-70.467,59.971,-70.62,59.984,-70.655,60.026,-70.509,60.015,-69.963,60.018,-69.796,60.03,-69.674,60.076,-69.63,60.122,-69.623,60.145,-69.629,60.199,-69.633,60.22,-69.708,60.286,-69.741,60.332,-69.756,60.389,-69.759,60.44,-69.751,60.487,-69.721,60.567,-69.64,60.69,-69.574,60.743,-69.49,60.78,-69.433,60.814,-69.405,60.847,-69.398,60.883,-69.414,60.922,-69.472,61.011,-69.503,61.04,-69.557,61.06,-69.624,61.05,-69.65,61.014,-69.678,60.95,-69.708,60.915,-69.8,60.907,-69.909,60.86,-69.992,60.856,-70.095,60.88,-70.146,60.922,-70.144,60.981,-70.158,61.021,-70.187,61.041,-70.279,61.069,-70.384,61.064,-70.541,61.042,-70.723,61.055,-71.035,61.126,-71.175,61.147,-71.348,61.149,-71.423,61.159,-71.552,61.213,-71.743,61.337,-71.732,61.372,-71.646,61.399,-71.645,61.413,-71.794,61.421,-71.854,61.44,-71.841,61.466,-71.756,61.527,-71.656,61.551,-71.619,61.573,-71.605,61.592,-71.638,61.617,-71.866,61.689,-71.922,61.677,-71.964,61.636,-72.023,61.612,-72.216,61.587,-72.247,61.602,-72.082,61.641,-72.043,61.665,-72.04,61.68,-72.081,61.728,-72.126,61.753,-72.178,61.802,-72.226,61.832,-72.361,61.888,-72.506,61.923,-72.574,61.907,-72.661,61.863,-72.727,61.839,-72.772,61.84,-72.666,61.955,-72.632,62.027,-72.633,62.053,-72.646,62.077,-72.671,62.114,-72.687,62.125,-72.735,62.131,-72.882,62.125,-72.992,62.18,-73.049,62.198,-73.195,62.279,-73.299,62.325,-73.428,62.369,-73.63,62.454,-73.705,62.473,-73.764,62.469,-73.878,62.434,-74.046,62.37,-74.205,62.321,-74.429,62.272,-74.646,62.211,-74.69,62.183,-74.613,62.125,-74.633,62.116,-74.908,62.23,-75.023,62.264,-75.114,62.271,-75.341,62.312,-75.409,62.307,-75.489,62.286,-75.79,62.18,-75.809,62.193,-75.676,62.25,-75.817,62.316,-76.616,62.466,-76.879,62.525,-77.205,62.55,-77.372,62.573,-77.604,62.531,-77.9,62.427,-78.068,62.355,-78.109,62.318,-78.133,62.282,-78.147,62.209,-78.137,62.107,-78.077,61.923,-78.021,61.832,-77.948,61.762,-77.89,61.729,-77.814,61.695,-77.698,61.626,-77.514,61.556,-77.649,61.479,-77.736,61.437,-77.75,61.393,-77.727,61.231,-77.731,61.206,-77.765,61.158,-77.83,61.084,-77.934,61.003,-78.16,60.852,-78.181,60.819,-78.122,60.81,-77.998,60.818,-77.872,60.786,-77.603,60.825,-77.59,60.809,-77.661,60.79,-77.734,60.697,-77.761,60.679,-77.791,60.64,-77.715,60.578,-77.639,60.567,-77.516,60.563,-77.504,60.543,-77.598,60.507,-77.681,60.427,-77.649,60.363,-77.453,60.146,-77.461,60.133,-77.572,60.101,-77.586,60.088,-77.547,60.061,-77.312,60.042,-77.289,60.022,-77.373,59.925,-77.368,59.884,-77.328,59.833,-77.332,59.797,-77.475,59.716,-77.485,59.685,-77.411,59.61,-77.349,59.579,-77.397,59.569,-77.59,59.681,-77.726,59.676,-77.748,59.658,-77.733,59.581,-77.749,59.558,-77.859,59.476,-77.845,59.444,-77.779,59.41,-77.761,59.38,-77.843,59.305,-77.988,59.246,-78.068,59.2,-78.14,59.142,-78.244,59.035,-78.431,58.902,-78.459,58.873,-78.483,58.829,-78.502,58.769,-78.515,58.682,-78.506,58.649,-78.463,58.602,-78.352,58.581,-78.014,58.399,-77.884,58.351,-77.684,58.291,-77.552,58.24,-77.489,58.195,-77.157,58.019,-76.891,57.758,-76.81,57.658,-76.786,57.599,-76.655,57.381,-76.601,57.272,-76.573,57.181,-76.526,56.892,-76.52,56.707,-76.53,56.5,-76.546,56.359,-76.604,56.2,-76.65,56.107,-76.762,55.996,-76.938,55.867,-77.073,55.756,-77.165,55.664,-77.325,55.556,-77.702,55.344,-77.775,55.291,-77.891,55.236,-78.129,55.151,-78.304,55.069,-78.475,55.011,-78.846,54.908,-78.909,54.881,-79.666,54.697,-79.712,54.672,-79.714,54.655,-79.67,54.647,-79.632,54.629,-79.598,54.602,-79.521,54.492,-79.476,54.395,-79.431,54.337,-79.356,54.263,-79.296,54.217,-79.216,54.186,-79.147,54.169,-79.139,54.157,-79.179,54.117,-79.242,54.099,-79.067,54.052,-79.01,54.024,-78.996,54.002,-79.073,53.951,-79.075,53.932,-79.032,53.881,-78.944,53.84,-78.946,53.832,-79.003,53.837,-79.04,53.818,-79.081,53.742,-79.113,53.717,-79.1,53.657,-79.043,53.56,-78.992,53.41,-78.947,53.206,-78.898,53.043,-78.854,52.976,-78.74,52.899,-78.722,52.856,-78.754,52.812,-78.766,52.76,-78.744,52.655,-78.724,52.628,-78.601,52.535,-78.557,52.492,-78.529,52.399,-78.526,52.311,-78.513,52.291,-78.448,52.261,-78.492,52.252,-78.537,52.213,-78.593,52.14,-78.702,52.033,-78.828,51.963,-78.871,51.913,-78.891,51.845,-78.928,51.799,-78.982,51.775,-78.978,51.734,-78.776,51.566,-78.736,51.527,-78.731,51.497,-78.827,51.43,-78.858,51.384,-78.898,51.272,-78.903,51.2,-78.937,51.259,-78.984,51.386,-79.005,51.425,-79.041,51.464,-79.091,51.502,-79.153,51.526,-79.226,51.537,-79.264,51.552,-79.297,51.623,-79.339,51.628,-79.498,51.57,-79.521,51.545,-79.528,51.538,-79.547,51.494,-79.586,51.452,-79.643,51.414,-79.689,51.347,-79.723,51.252,-79.737,51.186,-79.731,51.15,-79.714,51.118,-79.636,51.049,-79.52,50.966,-79.453,50.917,-79.381,50.835,-79.348,50.763,-79.456,50.876,-79.52,50.919,-79.652,51.008,-79.836,51.173,-79.96,51.235,-80.104,51.283,-80.266,51.316,-80.368,51.33,-80.478,51.307,-80.677,51.191,-80.795,51.132,-80.851,51.125,-80.673,51.265,-80.496,51.345,-80.443,51.389,-80.448,51.432,-80.496,51.525,-80.588,51.667,-80.658,51.758,-80.706,51.798,-80.969,51.972,-81.127,52.045,-81.285,52.089,-81.398,52.142,-81.466,52.204,-81.55,52.237,-81.648,52.239,-81.815,52.217,-81.828,52.224,-81.776,52.254,-81.661,52.294,-81.612,52.324,-81.572,52.367,-81.599,52.433,-81.742,52.564,-81.859,52.651,-82.02,52.812,-82.108,52.877,-82.203,52.922,-82.26,52.961,-82.292,53.031,-82.292,53.066,-82.26,53.16,-82.219,53.211,-82.159,53.264,-82.146,53.365,-82.18,53.513,-82.191,53.611,-82.15,53.74,-82.141,53.818,-82.163,53.886,-82.24,54.045,-82.264,54.073,-82.394,54.18,-82.424,54.245,-82.418,54.356,-82.371,54.483,-82.219,54.813,-82.227,54.856,-82.308,54.998,-82.393,55.068,-82.577,55.149,-82.687,55.166,-82.801,55.156,-82.868,55.161,-82.947,55.222,-82.986,55.231,-83.214,55.215,-83.569,55.262,-83.668,55.265,-83.911,55.315,-83.972,55.314,-84.023,55.298,-84.105,55.291,-84.219,55.293,-84.356,55.283,-84.518,55.259,-84.706,55.259,-84.92,55.283,-85.061,55.286,-85.129,55.266,-85.214,55.224,-85.365,55.079,-85.362,55.095,-85.212,55.297,-85.218,55.349,-85.283,55.383,-85.407,55.431,-85.478,55.474,-85.559,55.54,-85.677,55.601,-85.831,55.657,-85.984,55.696,-86.139,55.718,-86.377,55.773,-86.919,55.915,-87.287,55.975,-87.482,56.021,-87.561,56.056,-87.878,56.342,-88.075,56.467,-88.271,56.536,-88.447,56.609,-88.68,56.725,-88.826,56.814,-88.948,56.851,-89.212,56.884,-89.342,56.915,-89.791,56.981,-90.075,57.052,-90.345,57.149,-90.592,57.224,-90.897,57.257,-91.111,57.241,-92.018,57.064,-92.249,57.009,-92.356,56.971,-92.372,56.975,-92.298,57.023,-92.303,57.046,-92.456,57.037,-92.51,57.022,-92.651,56.958,-92.725,56.934,-92.798,56.922,-92.802,56.928,-92.738,56.953,-92.675,56.99,-92.614,57.039,-92.548,57.111,-92.478,57.205,-92.44,57.275,-92.433,57.32,-92.449,57.385,-92.49,57.469,-92.702,57.778,-92.74,57.844,-92.842,58.076,-92.925,58.225,-93.1,58.49,-93.127,58.564,-93.155,58.695,-93.179,58.726,-93.278,58.756,-93.375,58.741,-93.486,58.744,-93.78,58.773,-94.056,58.76,-94.123,58.737,-94.209,58.626,-94.272,58.378,-94.332,58.297,-94.333,58.339,-94.281,58.659,-94.287,58.716,-94.419,58.746,-94.54,58.848,-94.579,58.868,-94.624,58.876,-94.673,58.87,-94.713,58.903,-94.744,58.975,-94.776,59.021,-94.847,59.05,-94.957,59.069,-94.87,59.088,-94.82,59.151,-94.788,59.268,-94.777,59.478,-94.786,59.953,-94.772,60.002,-94.742,60.107,-94.67,60.301,-94.647,60.416,-94.671,60.453,-94.705,60.478,-94.762,60.498,-94.679,60.538,-94.569,60.542,-94.509,60.605,-94.427,60.731,-94.309,60.871,-94.154,61.025,-94.068,61.139,-94.05,61.211,-94.055,61.266,-94.083,61.304,-94.061,61.318,-93.942,61.308,-93.889,61.344,-93.889,61.364,-93.941,61.444,-93.913,61.481,-93.71,61.603,-93.421,61.706,-93.352,61.74,-93.312,61.767,-93.314,61.78,-93.43,61.812,-93.494,61.847,-93.527,61.872,-93.582,61.942,-93.372,61.929,-93.333,61.933,-93.273,61.961,-93.297,61.982,-93.366,62.015,-93.35,62.03,-93.167,62.034,-93.073,62.061,-93.016,62.093,-93.028,62.109,-93.07,62.128,-93.066,62.15,-92.906,62.215,-92.914,62.245,-92.988,62.286,-93.179,62.35,-93.205,62.365,-93.154,62.367,-92.866,62.306,-92.735,62.26,-92.648,62.208,-92.579,62.177,-92.528,62.168,-92.544,62.202,-92.627,62.279,-92.701,62.328,-92.766,62.35,-92.768,62.38,-92.707,62.418,-92.595,62.47,-92.562,62.503,-92.551,62.547,-92.497,62.565,-92.4,62.557,-92.324,62.565,-92.27,62.587,-92.207,62.585,-92.081,62.544,-92.008,62.541,-91.944,62.575,-91.936,62.592,-91.956,62.645,-92.149,62.665,-92.243,62.684,-92.305,62.712,-92.345,62.734,-92.378,62.772,-92.388,62.801,-92.361,62.819,-92.196,62.829,-92.152,62.839,-92.11,62.862,-92.034,62.863,-91.87,62.835,-91.449,62.804,-91.349,62.819,-91.115,62.922,-91.008,62.94,-90.871,62.946,-90.778,62.972,-90.728,63.017,-90.699,63.064,-90.691,63.111,-90.711,63.304,-90.747,63.352,-90.97,63.443,-91.103,63.476,-91.33,63.507,-91.489,63.562,-91.686,63.66,-91.842,63.698,-91.957,63.676,-92.077,63.64,-92.29,63.563,-92.465,63.555,-92.461,63.569,-92.205,63.657,-92.157,63.692,-92.196,63.708,-92.339,63.735,-92.529,63.761,-93.166,63.902,-93.25,63.927,-93.379,63.948,-93.406,63.941,-93.38,63.9,-93.327,63.872,-93.266,63.853,-93.27,63.841,-93.416,63.838,-93.56,63.865,-93.664,63.941,-93.656,63.973,-93.605,64.004,-93.597,64.041,-93.696,64.147,-93.43,64.029,-92.97,63.938,-92.55,63.83,-92.338,63.788,-92.195,63.776,-92.095,63.784,-92.038,63.813,-91.982,63.822,-91.929,63.812,-91.919,63.801,-91.954,63.787,-91.956,63.772,-91.926,63.757,-91.675,63.742,-91.539,63.726,-91.108,63.618,-90.946,63.588,-90.812,63.581,-90.707,63.597,-90.635,63.624,-90.596,63.661,-90.533,63.665,-90.446,63.636,-90.369,63.624,-90.245,63.642,-90.155,63.69,-90.013,63.804,-90.018,63.829,-90.06,63.877,-90.168,63.979,-90.142,63.982,-89.922,63.944,-89.856,63.957,-89.861,63.979,-89.954,64.081,-89.986,64.1,-90.08,64.128,-90.042,64.141,-89.811,64.181,-89.792,64.168,-89.764,64.1,-89.733,64.077,-89.616,64.031,-89.551,64.015,-89.501,64.015,-89.465,64.03,-89.404,64.04,-89.215,63.984,-89.132,63.969,-89.108,63.981,-89.209,64.105,-89.201,64.114,-89.06,64.034,-88.964,64.011,-88.818,63.992,-88.653,64.009,-88.379,64.089,-88.106,64.183,-87.998,64.244,-87.964,64.302,-87.885,64.4,-87.281,64.826,-87.183,64.927,-87.029,65.064,-87.003,65.109,-87.028,65.198,-87.108,65.225,-87.392,65.261,-87.93,65.28,-88.198,65.28,-88.974,65.348,-89.127,65.396,-89.242,65.446,-89.6,65.648,-89.788,65.737,-89.924,65.78,-90.048,65.806,-90.159,65.813,-90.597,65.885,-90.983,65.919,-91.065,65.9,-91.074,65.886,-91.041,65.83,-91.285,65.894,-91.427,65.948,-91.412,65.959,-91.305,65.965,-91.01,65.966,-90.826,65.954,-90.655,65.929,-90.513,65.921,-90.316,65.926,-90.117,65.882,-90.004,65.883,-89.89,65.869,-89.848,65.872,-89.944,65.934,-89.89,65.941,-89.749,65.936,-89.593,65.909,-89.42,65.861,-89.088,65.739,-88.946,65.703,-88.808,65.692,-88.744,65.679,-88.672,65.612,-88.587,65.588,-88.395,65.516,-88.121,65.395,-87.97,65.349,-87.678,65.335,-87.453,65.339,-87.291,65.355,-87.194,65.383,-87.081,65.441,-86.953,65.528,-86.702,65.671,-86.043,66.023,-86.012,66.048,-85.959,66.119,-85.964,66.154,-86.001,66.187,-86.113,66.225,-86.301,66.27,-86.585,66.322,-86.685,66.36,-86.747,66.417,-86.738,66.433,-86.7,66.443,-86.689,66.457,-86.737,66.511,-86.708,66.523,-86.633,66.531,-86.063,66.52,-85.792,66.533,-85.604,66.568,-85.442,66.537,-85.307,66.44,-85.192,66.37,-85.096,66.325,-84.908,66.271,-84.628,66.208,-84.478,66.179,-84.459,66.186,-84.398,66.259,-84.324,66.291,-84.293,66.292,-84.012,66.231,-83.905,66.212,-83.869,66.214,-83.798,66.238,-83.826,66.29,-83.964,66.421,-84.094,66.526,-84.153,66.59,-84.183,66.648,-84.223,66.682,-84.319,66.712,-84.59,66.857,-84.603,66.875,-84.639,66.902,-84.738,66.934,-84.857,66.941,-84.899,66.927,-84.978,66.881,-85.018,66.872,-85.111,66.891,-85.114,66.907,-85.04,66.956,-84.846,67.029,-84.693,67.017,-84.538,66.973,-84.531,66.961,-84.466,66.927,-84.31,66.863,-84.273,66.839,-84.361,66.823,-84.366,66.811,-84.324,66.782,-84.208,66.736,-84.154,66.732,-84.05,66.74,-83.998,66.729,-83.92,66.679,-83.739,66.534,-83.651,66.485,-83.628,66.461,-83.59,66.388,-83.523,66.369,-83.406,66.371,-83.298,66.392,-83.199,66.431,-82.949,66.551,-82.642,66.588,-82.554,66.621,-82.375,66.709,-82.261,66.739,-82.198,66.765,-82.113,66.825,-82.005,66.92,-81.926,66.975,-81.874,66.988,-81.722,66.986,-81.63,67.002,-81.468,67.07,-81.443,67.093,-81.387,67.189,-81.301,67.357,-81.27,67.46,-81.294,67.497,-81.412,67.595,-81.493,67.637,-81.709,67.722,-81.869,67.802,-81.976,67.862,-82.063,67.928,-82.1,67.99,-82.102,68.019,-82.092,68.051,-82.013,68.173,-82.012,68.194,-82.034,68.196,-82.078,68.18,-82.151,68.14,-82.187,68.134,-82.222,68.145,-82.393,68.285,-82.423,68.297,-82.431,68.307,-82.393,68.338,-82.413,68.357,-82.464,68.382,-82.553,68.446,-82.549,68.469,-82.499,68.479,-82.397,68.478,-82.21,68.506,-82.106,68.499,-82.006,68.463,-81.915,68.459,-81.831,68.487,-81.64,68.524,-81.527,68.556,-81.282,68.657,-81.259,68.692,-81.252,68.743,-81.264,68.781,-81.331,68.828,-81.381,68.85,-81.476,68.866,-81.687,68.879,-81.958,68.884,-81.952,68.909,-81.758,68.957,-81.611,69.003,-81.329,69.12,-81.322,69.139,-81.378,69.186,-81.412,69.198,-81.732,69.258,-81.952,69.276,-82.151,69.249,-82.228,69.249,-82.247,69.265,-82.208,69.297,-82.232,69.333,-82.31,69.41,-82.642,69.458,-82.755,69.494,-82.633,69.518,-82.496,69.532,-82.39,69.601,-82.374,69.642,-82.618,69.691,-82.746,69.695,-82.991,69.686,-83.552,69.704,-83.665,69.7,-83.917,69.745,-84.242,69.835,-84.319,69.844,-84.645,69.85,-84.834,69.835,-85.02,69.805,-85.177,69.805,-85.305,69.836,-85.415,69.85,-85.507,69.845,-85.535,69.835,-85.497,69.819,-85.446,69.778,-85.448,69.748,-85.502,69.652,-85.482,69.619,-85.43,69.581,-85.416,69.548,-85.44,69.52,-85.437,69.488,-85.409,69.452,-85.402,69.427,-85.416,69.411,-85.432,69.354,-85.428,69.318,-85.387,69.232,-85.275,69.172,-85.243,69.163,-85.114,69.166,-84.89,69.093,-84.862,69.074,-84.893,69.021,-84.895,68.989,-84.916,68.962,-85.008,68.949,-85.083,68.908,-85.104,68.871,-85.107,68.844,-84.867,68.79,-84.868,68.773,-85.275,68.741,-85.338,68.746,-85.425,68.774,-85.491,68.774,-85.518,68.77,-85.562,68.729,-85.643,68.7,-85.69,68.671,-85.734,68.63,-85.745,68.578,-85.723,68.515,-85.731,68.445,-85.789,68.328,-85.953,68.072,-85.984,68.045,-86.37,67.825,-86.398,67.8,-86.476,67.713,-86.504,67.649,-86.536,67.516,-86.561,67.482,-86.609,67.451,-86.682,67.422,-86.75,67.406,-86.813,67.402,-86.924,67.356,-87.083,67.268,-87.266,67.184,-87.32,67.173,-87.359,67.177,-87.392,67.191,-87.418,67.214,-87.471,67.325,-87.499,67.355,-87.997,67.626,-88.196,67.766,-88.314,67.95,-88.325,67.989,-88.32,68.166,-88.361,68.26,-88.347,68.288,-88.235,68.339,-88.209,68.335,-88.146,68.266,-88.111,68.251,-87.991,68.242,-87.893,68.248,-87.853,68.267,-87.828,68.3,-87.814,68.346,-87.81,68.404,-87.828,68.448,-87.866,68.478,-87.912,68.565,-87.964,68.709,-88.041,68.812,-88.224,68.915,-88.316,68.954,-88.638,69.059,-88.815,69.136,-88.954,69.22,-89.057,69.266,-89.198,69.269,-89.28,69.255,-89.352,69.227,-89.552,69.085,-89.667,69.015,-89.72,68.932,-89.751,68.812,-89.783,68.736,-89.884,68.626,-89.897,68.594,-89.879,68.522,-89.898,68.491,-90.005,68.398,-90.116,68.339,-90.157,68.306,-90.174,68.27,-90.205,68.257,-90.248,68.267,-90.285,68.292,-90.317,68.33,-90.36,68.347,-90.423,68.395,-90.528,68.432,-90.574,68.475,-90.525,68.611,-90.51,68.689,-90.543,68.786,-90.539,68.82,-90.468,68.864,-90.479,68.881,-90.587,68.947,-90.745,69.106,-91.237,69.286,-91.218,69.302,-91.148,69.303,-91.058,69.318,-91.025,69.315,-91.049,69.293,-91.005,69.277,-90.892,69.267,-90.822,69.29,-90.795,69.347,-90.749,69.392,-90.684,69.428,-90.606,69.445,-90.513,69.445,-90.416,69.457,-90.451,69.475,-90.555,69.504,-90.667,69.516,-90.786,69.509,-90.95,69.515,-91.288,69.543,-91.44,69.526,-91.427,69.538,-91.305,69.581,-91.17,69.62,-91.151,69.637,-91.202,69.645,-91.384,69.649,-91.532,69.615,-91.724,69.546,-91.912,69.531,-92.209,69.603,-92.258,69.634,-92.231,69.653,-92.312,69.673,-92.493,69.683,-92.643,69.659,-92.803,69.651,-92.855,69.655,-92.888,69.668,-92.751,69.714,-92.285,69.892,-92.069,69.984,-91.977,70.039,-92.058,70.071,-92.127,70.085,-92.446,70.083,-92.512,70.104,-92.455,70.15,-92.363,70.201,-92.321,70.235,-92.29,70.24,-92.209,70.198,-92.121,70.17,-91.995,70.143,-91.859,70.133,-91.616,70.148,-91.572,70.162,-91.564,70.178,-91.654,70.233,-91.716,70.299,-91.761,70.326,-91.82,70.342,-91.876,70.331,-91.926,70.295,-91.984,70.286,-92.047,70.303,-92.073,70.319,-92.037,70.367,-92.049,70.39,-92.214,70.493,-92.315,70.608,-92.356,70.634,-92.388,70.65,-92.567,70.693,-92.642,70.719,-92.783,70.798,-92.961,70.838,-92.981,70.852,-92.922,70.887,-92.904,70.916,-92.883,71.069,-92.89,71.122,-92.949,71.262,-92.983,71.3,-93.031,71.336,-93.256,71.461,-93.407,71.521,-93.576,71.569,-93.763,71.638,-93.782,71.674,-93.751,71.717,-93.746,71.743,-93.81,71.766,-94.086,71.771,-94.171,71.758,-94.308,71.765,-94.479,71.849,-94.491,71.916,-94.557,71.979,-94.611,71.987,-94.735,71.983,-94.887,71.963,-95.201,71.904,-95.512,71.777,-95.616,71.685,-95.838,71.598,-95.872,71.573,-95.83,71.526,-95.773,71.514,-95.674,71.504,-95.445,71.505,-95.406,71.492,-95.448,71.46,-95.564,71.337,-95.633,71.319,-95.725,71.328,-95.851,71.361,-95.924,71.393,-95.994,71.411,-96.062,71.414,-96.14,71.396,-96.271,71.339,-96.406,71.274,-96.447,71.24,-96.421,71.176,-96.445,71.159,-96.504,71.143,-96.525,71.127,-96.47,71.07,-96.491,71.002,-96.551,70.89,-96.549,70.809,-96.359,70.679,-96.258,70.642,-96.186,70.638,-95.906,70.698,-95.886,70.694,-95.988,70.617,-95.98,70.593,-95.879,70.549,-96.048,70.567,-96.123,70.561,-96.226,70.542,-96.298,70.511,-96.337,70.47,-96.546,70.327,-96.56,70.243,-96.551,70.21,-96.492,70.125,-96.269,69.992,-96.172,69.925,-96.119,69.872,-96.05,69.831,-95.965,69.803,-95.851,69.785,-95.707,69.778,-95.588,69.756,-95.491,69.718,-95.292,69.667,-94.823,69.578,-94.789,69.585,-94.713,69.649,-94.676,69.657,-94.634,69.65,-94.514,69.583,-94.419,69.517,-94.338,69.474,-94.271,69.455,-94.163,69.446,-94.015,69.447,-93.915,69.458,-93.794,69.498,-93.65,69.519,-93.532,69.481,-93.522,69.451,-93.543,69.406,-93.537,69.382,-93.431,69.375,-93.451,69.355,-93.567,69.297,-93.749,69.226,-93.82,69.253,-93.801,69.281,-93.613,69.403,-93.619,69.417,-93.854,69.376,-94.156,69.342,-94.255,69.314,-94.277,69.275,-94.285,69.242,-94.255,69.151,-94.222,69.136,-94.081,69.136,-94.084,69.123,-94.237,69.05,-94.476,68.958,-94.563,68.912,-94.6,68.803,-94.587,68.776,-94.478,68.743,-94.217,68.761,-94.065,68.785,-93.992,68.821,-93.938,68.889,-93.896,68.982,-93.881,68.997,-93.852,69,-93.811,68.993,-93.766,68.97,-93.716,68.931,-93.681,68.887,-93.663,68.838,-93.66,68.784,-93.676,68.686,-93.644,68.633,-93.606,68.624,-93.449,68.619,-93.483,68.599,-93.652,68.543,-93.928,68.474,-94.098,68.399,-94.255,68.297,-94.384,68.227,-94.485,68.19,-94.744,68.071,-94.861,68.042,-94.955,68.05,-95.126,68.083,-95.235,68.06,-95.384,68.056,-95.427,68.045,-95.461,68.021,-95.65,67.737,-95.634,67.704,-95.463,67.61,-95.39,67.518,-95.296,67.361,-95.259,67.263,-95.321,67.152,-95.354,66.981,-95.4,66.949,-95.49,66.924,-95.625,66.916,-95.972,66.952,-96.037,66.938,-96.045,66.923,-96.016,66.87,-95.772,66.726,-95.743,66.69,-95.788,66.617,-95.797,66.617,-95.813,66.69,-95.885,66.741,-96.36,66.989,-96.42,67.036,-96.423,67.052,-96.404,67.063,-96.35,67.07,-96.216,66.998,-96.095,66.994,-96.02,67.019,-95.954,67.011,-95.862,66.978,-95.769,66.967,-95.611,66.976,-95.559,66.973,-95.502,66.98,-95.457,66.989,-95.419,67.013,-95.407,67.056,-95.405,67.116,-95.416,67.156,-95.529,67.209,-95.557,67.215,-95.626,67.212,-95.778,67.185,-95.783,67.194,-95.695,67.299,-95.72,67.317,-95.879,67.298,-96.013,67.271,-96.141,67.272,-96.169,67.289,-96.185,67.376,-96.213,67.404,-96.369,67.51,-96.371,67.554,-96.228,67.679,-96.199,67.718,-96.171,67.832,-96.036,68.158,-95.97,68.249,-96.076,68.237,-96.439,68.151,-96.461,68.136,-96.494,68.085,-96.531,68.063,-96.592,68.048,-96.722,68.039,-96.725,68.061,-96.48,68.243,-96.435,68.29,-96.431,68.311,-96.628,68.25,-96.977,68.255,-97,68.265,-97.072,68.333,-97.136,68.378,-97.266,68.453,-97.336,68.479,-97.41,68.497,-97.481,68.495,-97.548,68.475,-97.64,68.482,-97.829,68.533,-97.925,68.524,-97.939,68.51,-97.911,68.45,-97.794,68.388,-98.091,68.346,-98.219,68.317,-98.469,68.382,-98.522,68.383,-98.562,68.371,-98.65,68.364,-98.633,68.331,-98.491,68.224,-98.449,68.201,-98.381,68.132,-98.386,68.115,-98.5,68.132,-98.5,68.118,-98.438,68.065,-98.193,67.923,-98.11,67.903,-97.913,67.954,-97.739,67.978,-97.547,67.961,-97.336,67.901,-97.207,67.855,-97.158,67.822,-97.14,67.796,-97.157,67.755,-97.155,67.726,-97.194,67.697,-97.274,67.666,-97.455,67.617,-97.607,67.631,-97.931,67.711,-97.978,67.739,-98.063,67.77,-98.415,67.988,-98.54,68.047,-98.632,68.073,-98.69,68.066,-98.72,68.042,-98.722,68,-98.704,67.966,-98.606,67.911,-98.468,67.856,-98.417,67.826,-98.412,67.807,-98.453,67.798,-98.697,67.78,-98.812,67.744,-98.92,67.726,-99.032,67.719,-99.147,67.724,-99.294,67.745,-99.472,67.784,-99.773,67.815,-100.213,67.839,-100.456,67.839,-100.52,67.818,-100.616,67.808,-100.746,67.809,-100.856,67.799,-101.026,67.766,-101.096,67.762,-101.555,67.693,-101.689,67.709,-101.884,67.745,-102.057,67.753,-102.21,67.733,-102.32,67.736,-102.389,67.762,-102.692,67.812,-102.842,67.853,-103.022,67.94,-103.323,68.064,-103.474,68.115,-103.657,68.069,-103.902,68.041,-104.194,68.031,-104.351,68.041,-104.487,68.063,-104.628,68.121,-104.661,68.149,-104.636,68.214,-104.653,68.23,-104.77,68.252,-104.879,68.245,-104.912,68.25,-104.937,68.303,-104.96,68.311,-104.994,68.307,-105.044,68.288,-105.101,68.298,-105.195,68.33,-105.377,68.414,-105.429,68.458,-105.457,68.578,-105.54,68.719,-105.606,68.782,-105.686,68.828,-105.798,68.865,-106.016,68.906,-106.164,68.92,-106.324,68.899,-106.713,68.819,-106.831,68.809,-107.436,68.689,-107.766,68.649,-108.313,68.611,-108.346,68.598,-108.641,68.379,-108.718,68.297,-108.687,68.277,-108.368,68.178,-108.323,68.154,-108.261,68.15,-108.105,68.169,-108.027,68.163,-107.734,68.174,-107.678,68.203,-107.734,68.252,-107.742,68.286,-107.619,68.331,-107.499,68.324,-107.298,68.296,-107.146,68.304,-107.043,68.347,-106.946,68.374,-106.854,68.387,-106.78,68.387,-106.608,68.357,-106.567,68.389,-106.543,68.461,-106.458,68.516,-106.237,68.577,-106.027,68.623,-105.932,68.637,-105.774,68.611,-105.75,68.592,-105.781,68.527,-105.857,68.475,-105.933,68.443,-106.039,68.407,-106.132,68.39,-106.271,68.383,-106.404,68.319,-106.429,68.288,-106.424,68.201,-106.459,68.196,-106.535,68.209,-106.668,68.216,-106.711,68.207,-106.791,68.145,-106.836,68.129,-106.923,68.114,-106.994,68.106,-107.125,68.108,-107.224,68.094,-107.351,68.061,-107.446,68.05,-107.509,68.059,-107.761,68.032,-107.798,68.037,-107.787,68.013,-107.729,67.959,-107.763,67.907,-107.891,67.856,-107.958,67.819,-107.972,67.732,-107.954,67.7,-107.865,67.639,-107.753,67.587,-107.65,67.511,-107.638,67.474,-107.651,67.428,-107.644,67.385,-107.567,67.273,-107.482,67.199,-107.318,67.128,-107.283,67.103,-107.348,67.055,-107.323,67.023,-107.254,66.976,-107.156,66.882,-107.2,66.883,-107.329,66.932,-107.402,66.948,-107.419,66.931,-107.451,66.927,-107.499,66.936,-107.626,67.003,-107.725,66.984,-107.746,66.961,-107.74,66.814,-107.731,66.769,-107.71,66.74,-107.564,66.619,-107.278,66.425,-107.259,66.399,-107.291,66.402,-107.374,66.435,-107.48,66.492,-107.705,66.637,-107.761,66.684,-107.958,66.781,-108.002,66.818,-108.05,66.844,-108.101,66.86,-108.158,66.893,-108.218,66.941,-108.455,67.063,-108.496,67.092,-108.459,67.09,-108.344,67.058,-108.221,67.051,-108.088,67.07,-107.991,67.095,-107.929,67.127,-107.909,67.163,-107.931,67.202,-107.989,67.256,-108.347,67.403,-108.492,67.483,-108.593,67.591,-108.613,67.598,-108.68,67.606,-108.715,67.583,-108.815,67.438,-108.852,67.422,-108.891,67.438,-108.95,67.494,-108.968,67.532,-108.994,67.637,-109.038,67.691,-109.081,67.711,-109.224,67.73,-109.63,67.733,-109.686,67.752,-109.76,67.82,-109.831,67.866,-109.904,67.874,-109.937,67.888,-110.042,67.977,-110.074,67.993,-110.102,67.992,-110.216,67.954,-110.372,67.954,-110.805,67.832,-110.99,67.791,-111.087,67.788,-111.155,67.798,-111.192,67.823,-111.291,67.815,-111.451,67.776,-111.576,67.757,-111.711,67.757,-112.101,67.732,-112.237,67.731,-112.315,67.72,-112.435,67.685,-112.503,67.682,-112.879,67.68,-113.075,67.687,-113.215,67.702,-113.682,67.7,-113.893,67.707,-114.051,67.727,-114.176,67.735,-114.267,67.731,-114.429,67.751,-114.663,67.795,-114.857,67.814,-115.011,67.806,-115.133,67.819,-115.288,67.872,-115.434,67.902,-115.427,67.924,-115.202,67.998,-115.167,68.019,-115.187,68.084,-115.176,68.104,-115.127,68.132,-114.852,68.195,-114.765,68.27,-114.275,68.248,-114.096,68.267,-114.053,68.283,-114.021,68.306,-113.964,68.399,-113.988,68.415,-114.051,68.415,-114.092,68.435,-114.111,68.477,-114.218,68.552,-114.414,68.66,-114.62,68.746,-114.994,68.85,-115.24,68.892,-115.442,68.941,-115.631,68.973,-115.806,68.987,-115.883,68.987,-115.936,68.958,-116.167,68.975,-116.243,68.974,-116.252,68.958,-116.065,68.855,-116.059,68.837,-116.223,68.847,-116.334,68.874,-116.425,68.881,-116.55,68.879,-117.026,68.916,-117.132,68.907,-117.227,68.913,-117.311,68.935,-117.83,69,-118.095,69.043,-118.307,69.093,-118.486,69.145,-118.745,69.234,-118.869,69.257,-119.853,69.342,-120.14,69.381,-120.293,69.421,-120.682,69.567]},{"b":[-82.039,52.734,-80.71,53.224],"l":[-81.136,53.206,-81.335,53.224,-81.847,53.186,-81.901,53.166,-81.951,53.132,-82.039,53.05,-82.005,53.01,-81.839,52.958,-81.352,52.852,-81.097,52.78,-81.01,52.761,-80.802,52.734,-80.732,52.747,-80.71,52.787,-80.71,52.832,-80.765,52.923,-80.9,53.037,-81.136,53.206]},{"b":[-74.749,67.783,-73.398,68.093],"l":[-73.584,68.015,-73.881,68.022,-74.111,68.061,-74.379,68.093,-74.707,68.067,-74.731,68.049,-74.749,68.018,-74.746,67.985,-74.679,67.906,-74.573,67.829,-74.481,67.805,-74.374,67.79,-74.109,67.783,-73.622,67.784,-73.407,67.793,-73.398,67.83,-73.402,67.879,-73.435,67.97,-73.459,67.99,-73.494,68.001,-73.584,68.015]},{"b":[-78.537,63.114,-77.527,63.49],"l":[-77.792,63.428,-77.877,63.471,-77.934,63.479,-78.235,63.49,-78.417,63.47,-78.507,63.451,-78.537,63.424,-78.469,63.358,-78.256,63.24,-78.024,63.139,-77.942,63.114,-77.791,63.13,-77.658,63.165,-77.594,63.188,-77.533,63.234,-77.527,63.269,-77.538,63.287,-77.655,63.396,-77.704,63.431,-77.792,63.428]},{"b":[-83.912,62.16,-81.949,62.978],"l":[-82.706,62.945,-82.966,62.874,-83.026,62.872,-83.111,62.884,-83.289,62.922,-83.376,62.905,-83.739,62.569,-83.899,62.476,-83.91,62.454,-83.912,62.426,-83.903,62.402,-83.761,62.304,-83.729,62.257,-83.714,62.174,-83.699,62.16,-83.377,62.238,-83.252,62.233,-83.13,62.204,-83.071,62.2,-83.016,62.21,-82.568,62.403,-82.491,62.447,-82.388,62.519,-82.114,62.652,-82.026,62.73,-81.99,62.776,-81.964,62.828,-81.949,62.884,-81.961,62.926,-82,62.954,-82.048,62.971,-82.129,62.978,-82.235,62.977,-82.46,62.936,-82.706,62.945]},{"b":[-80.28,61.595,-79.272,62.413],"l":[-79.466,62.385,-79.545,62.412,-79.598,62.413,-79.65,62.398,-79.713,62.395,-79.868,62.404,-79.927,62.393,-80.022,62.343,-80.179,62.213,-80.235,62.153,-80.26,62.109,-80.275,62.055,-80.28,61.99,-80.276,61.859,-80.265,61.818,-80.205,61.777,-80.092,61.747,-80.004,61.703,-79.896,61.63,-79.816,61.595,-79.763,61.596,-79.714,61.613,-79.669,61.644,-79.611,61.71,-79.542,61.808,-79.462,61.894,-79.372,61.968,-79.324,62.026,-79.306,62.104,-79.272,62.186,-79.286,62.248,-79.336,62.294,-79.466,62.385]},{"b":[-106.066,77.124,-104.454,77.754],"l":[-104.77,77.413,-104.955,77.419,-104.994,77.45,-105.007,77.507,-105.074,77.548,-105.29,77.642,-105.456,77.701,-105.588,77.736,-105.863,77.754,-106.036,77.74,-106.066,77.725,-105.883,77.627,-105.848,77.563,-105.747,77.525,-105.695,77.461,-105.556,77.353,-105.38,77.254,-105.215,77.182,-105.016,77.165,-104.711,77.124,-104.558,77.142,-104.493,77.162,-104.457,77.221,-104.454,77.249,-104.501,77.309,-104.542,77.338,-104.77,77.413]},{"b":[-96.277,77.452,-93.129,77.792],"l":[-93.129,77.66,-93.211,77.71,-93.301,77.74,-93.471,77.764,-93.583,77.771,-94.015,77.76,-94.667,77.776,-94.96,77.774,-95.233,77.754,-95.484,77.792,-95.684,77.782,-96.143,77.714,-96.195,77.701,-96.239,77.673,-96.277,77.631,-96.264,77.595,-96.056,77.503,-95.987,77.484,-94.409,77.474,-93.836,77.452,-93.74,77.465,-93.544,77.467,-93.52,77.474,-93.339,77.63,-93.129,77.66]},{"b":[-100.126,79.724,-98.769,80.144],"l":[-99.516,79.887,-99.333,79.84,-99.306,79.803,-99.302,79.784,-99.218,79.762,-98.945,79.724,-98.885,79.726,-98.841,79.737,-98.79,79.785,-98.769,79.851,-98.792,79.981,-98.823,80.037,-98.895,80.082,-99.017,80.111,-99.153,80.124,-99.425,80.126,-99.731,80.144,-99.803,80.14,-100.053,80.093,-100.079,80.081,-100.12,80.03,-100.126,80.001,-100.092,79.919,-100.057,79.898,-100,79.884,-99.857,79.879,-99.516,79.887]},{"b":[-91.185,77.21,-89.694,77.655],"l":[-90.172,77.595,-90.423,77.628,-90.675,77.649,-90.843,77.655,-91.019,77.644,-91.109,77.626,-91.149,77.608,-91.183,77.557,-91.185,77.482,-91.177,77.426,-91.147,77.387,-90.993,77.329,-90.228,77.212,-90.095,77.21,-89.833,77.268,-89.747,77.293,-89.712,77.31,-89.695,77.339,-89.694,77.378,-89.719,77.442,-89.839,77.491,-90.172,77.595]},{"b":[-96.6,74.637,-93.463,75.63],"l":[-93.498,75.137,-93.591,75.23,-93.667,75.274,-93.751,75.349,-93.909,75.423,-94.257,75.544,-94.427,75.593,-94.649,75.623,-94.878,75.63,-95.05,75.622,-95.671,75.529,-95.853,75.469,-95.955,75.444,-96.125,75.358,-96.118,75.301,-96.18,75.24,-96.292,75.219,-96.383,75.211,-96.566,75.099,-96.597,75.058,-96.6,75.032,-96.591,75.002,-96.56,74.99,-96.386,74.999,-96.343,74.982,-96.319,74.948,-96.294,74.927,-96.27,74.92,-96.182,74.951,-96.094,74.933,-95.865,74.83,-95.451,74.797,-95.286,74.794,-94.959,74.7,-94.804,74.66,-94.697,74.642,-94.535,74.637,-94.206,74.647,-93.985,74.644,-93.626,74.661,-93.573,74.669,-93.547,74.691,-93.548,74.728,-93.536,74.749,-93.509,74.756,-93.491,74.772,-93.463,74.856,-93.467,74.921,-93.478,74.952,-93.543,75.028,-93.552,75.051,-93.532,75.1,-93.498,75.137]},{"b":[-113.293,78.275,-110.004,78.758],"l":[-110.004,78.687,-110.14,78.704,-110.408,78.757,-110.618,78.758,-110.878,78.735,-111.071,78.708,-111.4,78.644,-111.52,78.603,-111.709,78.575,-112.214,78.548,-112.641,78.5,-112.856,78.467,-113.15,78.408,-113.282,78.353,-113.293,78.334,-113.223,78.298,-113.173,78.284,-113,78.293,-112.558,78.342,-112.131,78.366,-111.76,78.283,-111.517,78.275,-111.435,78.287,-111.3,78.337,-111.229,78.376,-111.169,78.386,-111.027,78.368,-110.84,78.322,-110.755,78.311,-110.418,78.295,-110.293,78.298,-110.022,78.323,-110.004,78.322]},{"b":[-110.004,78.304,-109.336,78.687],"l":[-110.004,78.322,-109.709,78.304,-109.484,78.316,-109.391,78.337,-109.352,78.369,-109.336,78.408,-109.342,78.456,-109.362,78.493,-109.467,78.567,-109.504,78.582,-109.581,78.593,-109.641,78.592,-109.816,78.65,-109.941,78.678,-110.004,78.687]},{"b":[-119.395,75.515,-117.499,76.115],"l":[-117.626,75.966,-117.499,76.077,-117.513,76.099,-117.634,76.115,-117.752,76.112,-117.889,76.076,-118.137,75.994,-118.379,75.958,-118.626,75.906,-119.003,75.77,-119.227,75.699,-119.32,75.663,-119.395,75.617,-119.383,75.601,-119.306,75.585,-119.087,75.569,-118.817,75.522,-118.614,75.515,-118.328,75.58,-118.227,75.611,-117.891,75.805,-117.716,75.921,-117.626,75.966]},{"b":[-106.95,72.915,-104.552,73.767],"l":[-104.835,73.647,-105.114,73.744,-105.318,73.767,-105.512,73.766,-106.362,73.719,-106.614,73.696,-106.695,73.67,-106.831,73.599,-106.95,73.51,-106.922,73.48,-106.75,73.458,-106.526,73.413,-106.18,73.304,-106.113,73.258,-106.071,73.196,-105.8,73.093,-105.573,72.989,-105.434,72.938,-105.339,72.915,-105.289,72.92,-105.201,72.947,-105.075,72.997,-105.003,73.038,-104.969,73.089,-104.791,73.168,-104.622,73.311,-104.583,73.354,-104.552,73.466,-104.555,73.541,-104.587,73.578,-104.649,73.614,-104.718,73.636,-104.835,73.647]},{"b":[-102.714,71.302,-96.44,73.946],"l":[-96.869,72.687,-97.052,72.637,-97.128,72.628,-97.159,72.643,-97.14,72.673,-97.073,72.718,-97.083,72.763,-97.238,72.837,-97.378,72.865,-97.371,72.878,-97.31,72.898,-97.296,72.918,-97.329,72.938,-97.476,72.992,-97.636,73.028,-97.725,73.037,-97.939,73.036,-98.061,73.021,-98.181,72.993,-98.367,72.934,-98.422,72.941,-98.431,72.958,-98.437,73,-98.417,73.023,-98.376,73.045,-98.176,73.116,-97.796,73.285,-97.484,73.339,-97.273,73.387,-97.23,73.421,-97.287,73.458,-97.35,73.481,-97.47,73.488,-97.532,73.474,-97.586,73.471,-97.615,73.481,-97.626,73.502,-97.597,73.537,-97.49,73.527,-97.395,73.564,-97.284,73.571,-97.156,73.592,-97.095,73.615,-97.002,73.667,-96.997,73.675,-97.011,73.706,-97.112,73.79,-97.171,73.825,-97.225,73.844,-97.327,73.862,-97.582,73.888,-97.67,73.888,-97.832,73.879,-97.928,73.866,-98.152,73.818,-98.519,73.792,-98.785,73.761,-99.04,73.749,-99.158,73.732,-100.002,73.946,-100.138,73.929,-100.227,73.889,-100.225,73.873,-100.154,73.844,-100.04,73.844,-99.94,73.857,-99.912,73.847,-99.991,73.795,-100.182,73.801,-100.484,73.844,-100.915,73.805,-100.963,73.791,-100.985,73.765,-100.982,73.727,-100.953,73.691,-100.898,73.658,-100.783,73.613,-100.658,73.593,-100.607,73.575,-100.536,73.51,-100.509,73.465,-100.522,73.449,-100.677,73.494,-100.854,73.571,-100.976,73.6,-101.115,73.596,-101.323,73.572,-101.463,73.534,-101.518,73.505,-101.523,73.486,-101.482,73.446,-101.451,73.431,-100.889,73.275,-100.755,73.278,-100.587,73.3,-100.498,73.316,-100.366,73.359,-100.258,73.34,-100.006,73.24,-99.825,73.214,-99.966,73.201,-100.067,73.211,-100.226,73.255,-100.341,73.265,-100.439,73.255,-100.489,73.234,-100.536,73.198,-100.55,73.164,-100.531,73.138,-100.446,73.121,-100.334,73.128,-100.283,73.12,-100.236,73.095,-100.184,73.055,-100.097,72.963,-100.092,72.945,-100.128,72.907,-100.188,72.89,-100.228,72.899,-100.368,72.978,-100.396,72.977,-100.443,72.807,-100.468,72.779,-100.485,72.773,-100.896,72.726,-101.088,72.713,-101.273,72.722,-101.351,72.746,-101.435,72.821,-101.544,72.883,-101.618,72.91,-101.755,72.943,-101.798,72.973,-101.835,73.018,-101.922,73.057,-102.02,73.07,-102.204,73.077,-102.336,73.064,-102.504,73.006,-102.551,72.978,-102.628,72.911,-102.687,72.843,-102.714,72.783,-102.709,72.765,-102.657,72.719,-102.402,72.595,-101.974,72.486,-101.909,72.431,-101.833,72.409,-101.804,72.385,-101.775,72.341,-101.724,72.315,-101.498,72.278,-101.319,72.313,-101.251,72.322,-101.209,72.317,-101.093,72.279,-101.026,72.229,-100.984,72.21,-100.8,72.199,-100.707,72.186,-100.594,72.152,-100.326,72.004,-100.124,71.912,-99.735,71.757,-99.581,71.652,-99.404,71.557,-99.276,71.424,-99.224,71.387,-99.167,71.367,-98.986,71.369,-98.899,71.352,-98.784,71.314,-98.663,71.302,-98.536,71.318,-98.412,71.349,-98.199,71.441,-98.19,71.462,-98.195,71.491,-98.231,71.559,-98.421,71.717,-98.459,71.773,-98.389,71.824,-98.323,71.852,-98.306,71.848,-98.303,71.831,-98.313,71.803,-98.307,71.765,-98.284,71.716,-98.242,71.681,-98.181,71.662,-97.582,71.63,-97.461,71.634,-97.222,71.673,-97.117,71.711,-97.025,71.761,-96.946,71.792,-96.613,71.834,-96.624,71.968,-96.717,72.025,-96.758,72.032,-96.766,72.046,-96.618,72.146,-96.601,72.173,-96.593,72.204,-96.616,72.237,-96.669,72.271,-96.796,72.314,-96.801,72.322,-96.746,72.323,-96.638,72.342,-96.52,72.393,-96.473,72.434,-96.44,72.487,-96.446,72.552,-96.489,72.63,-96.542,72.699,-96.592,72.71,-96.671,72.713,-96.869,72.687]},{"b":[-87.194,63.12,-80.261,65.919],"l":[-80.668,63.901,-80.579,63.909,-80.569,63.932,-80.608,63.972,-80.694,64.025,-80.829,64.09,-80.921,64.1,-81.005,64.033,-81.024,64.031,-81.104,64.037,-81.336,64.076,-81.716,64.022,-81.887,64.016,-81.903,64.031,-81.721,64.119,-81.681,64.146,-81.667,64.171,-81.676,64.213,-81.787,64.426,-81.929,64.559,-82.05,64.644,-82.159,64.691,-82.272,64.721,-82.586,64.762,-82.668,64.78,-82.991,64.904,-83.201,64.96,-83.222,64.968,-83.407,65.104,-83.491,65.132,-83.723,65.169,-83.9,65.181,-84.085,65.218,-84.133,65.245,-84.18,65.316,-84.266,65.367,-84.501,65.458,-84.568,65.461,-84.612,65.447,-84.771,65.305,-84.842,65.256,-84.885,65.249,-84.92,65.261,-85.056,65.437,-85.24,65.51,-85.243,65.526,-85.226,65.546,-85.13,65.592,-85.105,65.623,-85.13,65.693,-85.176,65.747,-85.241,65.796,-85.442,65.846,-85.496,65.9,-85.523,65.915,-85.555,65.919,-85.699,65.883,-85.814,65.832,-85.962,65.704,-86.017,65.64,-86.075,65.534,-86.114,65.417,-86.188,65.01,-86.228,64.896,-86.344,64.662,-86.374,64.566,-86.375,64.503,-86.354,64.377,-86.274,64.238,-86.252,64.181,-86.252,64.137,-86.309,64.094,-86.422,64.052,-86.886,63.924,-86.932,63.902,-87.032,63.83,-87.154,63.715,-87.189,63.672,-87.194,63.633,-87.177,63.595,-87.152,63.586,-87.053,63.572,-86.915,63.569,-86.847,63.575,-86.576,63.662,-86.302,63.657,-85.805,63.707,-85.769,63.7,-85.739,63.684,-85.714,63.658,-85.566,63.271,-85.496,63.139,-85.393,63.12,-85.238,63.139,-84.962,63.197,-84.796,63.247,-84.633,63.309,-84.555,63.35,-84.506,63.39,-84.387,63.529,-84.308,63.586,-84.26,63.6,-84.142,63.614,-84.022,63.66,-83.728,63.813,-83.662,63.873,-83.638,63.918,-83.617,64.013,-83.584,64.058,-83.494,64.099,-83.304,64.144,-83.186,64.158,-83.065,64.159,-83.016,64.127,-83.039,64.061,-83.034,64.023,-82.93,64,-82.571,63.961,-82.467,63.927,-82.412,63.737,-82.378,63.707,-82.146,63.691,-81.963,63.664,-81.372,63.538,-81.18,63.483,-81.046,63.462,-81.014,63.463,-80.954,63.48,-80.712,63.596,-80.504,63.674,-80.302,63.762,-80.261,63.802,-80.451,63.863,-80.668,63.901]},{"b":[-77.306,67.236,-75.062,68.337],"l":[-75.676,68.323,-75.867,68.337,-75.983,68.332,-76.088,68.314,-76.173,68.309,-76.364,68.319,-76.596,68.279,-76.688,68.254,-76.74,68.231,-76.945,68.091,-77.126,67.947,-77.229,67.85,-77.306,67.706,-77.304,67.685,-77.224,67.508,-77.157,67.408,-77.076,67.32,-77.005,67.267,-76.944,67.25,-76.859,67.24,-76.694,67.236,-76.333,67.258,-76.049,67.262,-75.78,67.284,-75.4,67.367,-75.315,67.4,-75.202,67.459,-75.127,67.537,-75.091,67.635,-75.086,67.751,-75.127,67.965,-75.124,67.985,-75.073,68.049,-75.062,68.075,-75.063,68.141,-75.078,68.173,-75.103,68.202,-75.154,68.234,-75.676,68.323]},{"b":[-80.861,72.756,-76.09,73.767],"l":[-80.736,73.483,-80.777,73.482,-80.798,73.472,-80.823,73.429,-80.824,73.381,-80.776,73.334,-80.727,73.305,-80.618,73.271,-80.293,73.246,-80.183,73.225,-80.146,73.161,-80.114,73.078,-80.052,72.977,-79.975,72.892,-79.937,72.864,-79.821,72.826,-79.501,72.756,-79.319,72.758,-79.134,72.772,-78.554,72.858,-78.314,72.882,-77.836,72.897,-77.014,72.844,-76.401,72.821,-76.183,72.843,-76.09,72.881,-76.135,72.912,-76.255,72.959,-76.31,72.998,-76.29,73.081,-76.331,73.1,-76.458,73.122,-76.57,73.159,-76.622,73.225,-76.657,73.254,-76.759,73.31,-77.005,73.356,-77.042,73.373,-77.12,73.45,-77.207,73.5,-77.382,73.537,-78.063,73.648,-78.287,73.666,-79.367,73.641,-79.537,73.654,-79.889,73.702,-80.12,73.707,-80.412,73.765,-80.621,73.767,-80.763,73.758,-80.823,73.743,-80.849,73.721,-80.861,73.671,-80.858,73.591,-80.827,73.535,-80.736,73.483]},{"b":[-102.798,74.984,-97.336,76.694],"l":[-97.863,75.738,-97.891,75.76,-97.694,75.803,-97.602,75.851,-97.603,75.879,-97.652,75.94,-97.65,75.979,-97.613,76.053,-97.531,76.109,-97.524,76.139,-97.531,76.182,-97.573,76.224,-97.707,76.304,-97.739,76.335,-97.737,76.363,-97.702,76.387,-97.69,76.422,-97.701,76.467,-97.726,76.496,-97.808,76.519,-97.967,76.533,-98.236,76.575,-98.289,76.599,-98.528,76.667,-98.711,76.694,-98.941,76.643,-99.024,76.615,-98.971,76.537,-98.89,76.466,-99.17,76.454,-99.329,76.521,-99.669,76.624,-99.814,76.632,-100.069,76.635,-100.388,76.614,-100.574,76.585,-100.83,76.524,-100.891,76.475,-100.874,76.457,-100.82,76.437,-100.651,76.396,-100.175,76.359,-100.082,76.343,-99.978,76.312,-99.983,76.3,-100.043,76.291,-100.358,76.271,-100.414,76.257,-100.414,76.243,-100.183,76.197,-99.998,76.196,-99.817,76.168,-99.541,76.146,-99.79,76.133,-100.002,76.139,-100.086,76.134,-100.113,76.117,-100.051,76.067,-99.978,76.029,-99.689,75.96,-99.701,75.941,-99.775,75.927,-99.865,75.924,-100.02,75.94,-100.106,75.96,-100.231,76.008,-100.9,76.207,-101.056,76.246,-101.094,76.272,-101.088,76.308,-101.139,76.345,-101.34,76.41,-101.415,76.425,-101.677,76.451,-101.788,76.451,-101.858,76.439,-101.964,76.399,-102.105,76.331,-102.138,76.285,-101.987,76.243,-101.91,76.234,-101.557,76.236,-101.529,76.217,-101.771,76.15,-101.861,76.101,-101.872,76.083,-101.823,76.041,-101.717,76.008,-101.431,75.992,-101.508,75.944,-101.506,75.918,-101.47,75.882,-101.415,75.846,-101.288,75.789,-101.259,75.784,-101.01,75.802,-100.973,75.798,-101.119,75.763,-101.261,75.758,-101.421,75.782,-101.6,75.833,-101.943,75.884,-102.145,75.875,-102.271,75.813,-102.252,75.778,-102.411,75.713,-102.728,75.639,-102.798,75.6,-102.7,75.544,-102.587,75.514,-102.541,75.514,-101.461,75.608,-101.207,75.59,-100.902,75.62,-99.915,75.681,-99.195,75.698,-99.209,75.669,-99.591,75.655,-99.756,75.633,-99.77,75.612,-99.965,75.569,-100.28,75.461,-100.712,75.406,-100.704,75.394,-100.731,75.347,-100.615,75.321,-100.364,75.29,-100.146,75.246,-100.152,75.236,-100.459,75.219,-100.483,75.188,-100.357,75.067,-100.292,75.028,-100.234,75.008,-99.947,75.003,-99.627,74.984,-99.421,75.044,-99.326,75.049,-99.245,75.026,-99.156,75.016,-99.01,75.021,-98.835,75.018,-98.704,75.006,-98.569,75.009,-98.295,75.032,-98.121,75.033,-97.992,75.046,-97.953,75.06,-97.99,75.111,-98.077,75.153,-98.092,75.176,-98.069,75.199,-98.045,75.201,-97.971,75.153,-97.843,75.122,-97.799,75.117,-97.674,75.127,-97.66,75.151,-97.705,75.191,-97.853,75.26,-97.878,75.416,-97.653,75.508,-97.465,75.459,-97.363,75.417,-97.336,75.42,-97.41,75.552,-97.408,75.673,-97.44,75.685,-97.863,75.738]},{"b":[-105.58,77.794,-99,79.352],"l":[-104.901,79.051,-104.747,79.027,-104.735,78.991,-104.893,78.89,-104.97,78.856,-104.97,78.829,-104.896,78.808,-104.817,78.807,-104.736,78.826,-104.395,78.956,-104.152,78.99,-104.113,78.986,-104.007,78.948,-103.887,78.919,-103.876,78.903,-104.155,78.814,-104.195,78.796,-104.185,78.781,-104.009,78.764,-103.518,78.769,-103.408,78.752,-103.372,78.736,-103.563,78.693,-103.929,78.663,-104.021,78.635,-103.588,78.623,-103.483,78.594,-103.571,78.54,-103.764,78.52,-104.214,78.54,-104.727,78.579,-104.82,78.573,-104.91,78.553,-104.996,78.519,-104.985,78.468,-104.879,78.401,-104.764,78.352,-104.513,78.295,-104.324,78.269,-103.947,78.26,-103.677,78.32,-102.731,78.371,-102.784,78.33,-102.772,78.307,-102.723,78.275,-102.668,78.256,-102.607,78.249,-102.284,78.275,-102.057,78.28,-101.829,78.264,-101.298,78.199,-101.074,78.194,-100.958,78.13,-100.826,78.088,-100.81,78.072,-100.778,77.996,-100.758,77.978,-100.68,77.931,-100.586,77.892,-100.275,77.833,-99.956,77.794,-99.659,77.824,-99.341,77.84,-99.166,77.857,-99.128,77.877,-99.061,77.966,-99,77.997,-99.005,78.016,-99.053,78.072,-99.132,78.118,-99.562,78.279,-99.751,78.303,-99.778,78.325,-99.769,78.365,-99.774,78.393,-99.848,78.438,-99.818,78.455,-99.68,78.494,-99.631,78.545,-99.582,78.563,-99.609,78.583,-99.782,78.62,-100.015,78.729,-100.435,78.82,-100.917,78.783,-101.128,78.802,-101.147,78.824,-101.116,78.858,-101.034,78.915,-101.037,78.939,-101.088,78.962,-101.145,78.973,-101.299,78.982,-101.704,79.079,-101.873,79.088,-101.974,79.079,-102.189,79.038,-102.393,79.01,-102.407,78.954,-102.425,78.933,-102.495,78.901,-102.576,78.879,-102.593,78.901,-102.581,78.93,-102.596,78.943,-102.731,78.969,-102.683,78.991,-102.648,79.027,-102.638,79.055,-102.639,79.078,-102.652,79.095,-102.914,79.231,-103.192,79.295,-103.426,79.316,-103.706,79.352,-103.965,79.348,-104.847,79.311,-105.388,79.324,-105.436,79.302,-105.515,79.242,-105.571,79.164,-105.58,79.114,-105.571,79.061,-105.536,79.033,-105.309,79.033,-104.901,79.051]},{"b":[-96.773,78.15,-85.042,81.364],"l":[-91.755,81.049,-91.886,81.133,-91.998,81.185,-92.212,81.244,-92.413,81.278,-93.035,81.346,-93.333,81.364,-93.605,81.351,-94.06,81.349,-94.179,81.339,-94.22,81.331,-94.233,81.315,-94.231,81.29,-94.219,81.265,-94.194,81.241,-94.11,81.225,-93.894,81.213,-93.407,81.209,-93.286,81.179,-93.235,81.155,-93.236,81.129,-93.287,81.1,-93.345,81.085,-93.444,81.083,-93.826,81.106,-94.216,81.057,-94.519,81.031,-94.981,81.05,-95.27,81.001,-95.509,80.863,-95.515,80.838,-95.196,80.808,-94.788,80.751,-94.596,80.641,-94.202,80.61,-94.029,80.586,-93.928,80.559,-94.485,80.558,-94.734,80.572,-94.893,80.571,-95.026,80.646,-95.226,80.686,-95.505,80.691,-95.714,80.725,-95.927,80.721,-96.133,80.691,-96.152,80.553,-95.901,80.471,-95.614,80.396,-95.549,80.367,-95.747,80.365,-96.012,80.383,-96.112,80.38,-96.334,80.353,-96.385,80.333,-96.394,80.315,-96.368,80.293,-96.308,80.267,-96.215,80.246,-96.026,80.222,-95.904,80.214,-95.646,80.231,-95.405,80.135,-95.192,80.134,-94.59,80.202,-94.263,80.195,-94.304,80.182,-94.583,80.141,-94.607,80.126,-94.613,80.103,-94.6,80.074,-94.611,80.056,-94.646,80.049,-95.394,80.053,-95.782,80.066,-96.773,80.136,-96.639,80.024,-96.607,79.978,-96.589,79.917,-96.463,79.848,-96,79.705,-95.858,79.674,-95.739,79.66,-95.552,79.653,-95.297,79.653,-94.973,79.677,-94.581,79.726,-94.402,79.736,-94.476,79.686,-94.52,79.667,-95.302,79.568,-95.563,79.55,-95.663,79.527,-95.733,79.418,-95.657,79.39,-95.317,79.355,-95.103,79.29,-95.044,79.294,-94.846,79.335,-94.405,79.391,-94.284,79.4,-94.11,79.402,-93.96,79.396,-93.94,79.386,-94.04,79.357,-94.109,79.315,-94.093,79.303,-94.04,79.295,-93.933,79.291,-93.55,79.354,-93.381,79.368,-93.028,79.429,-92.822,79.45,-92.645,79.45,-92.485,79.439,-92.248,79.373,-91.693,79.365,-91.3,79.373,-91.344,79.361,-91.868,79.317,-92.547,79.283,-92.684,79.186,-92.842,79.156,-93.068,79.155,-93.294,79.14,-93.95,79.037,-94.163,78.994,-94.17,78.973,-94.154,78.951,-94.115,78.929,-93.902,78.872,-93.336,78.808,-93.16,78.776,-93.208,78.769,-93.561,78.777,-93.623,78.768,-93.634,78.751,-93.552,78.708,-93.389,78.643,-93.267,78.608,-93.109,78.602,-92.973,78.613,-92.716,78.605,-91.935,78.562,-91.867,78.543,-92.297,78.521,-92.726,78.487,-92.848,78.46,-92.808,78.43,-92.678,78.389,-92.351,78.313,-91.899,78.237,-91.41,78.188,-90.918,78.158,-90.614,78.15,-90.387,78.163,-90.327,78.185,-90.358,78.219,-90.405,78.247,-90.469,78.269,-90.652,78.308,-90.622,78.322,-90.459,78.331,-90.297,78.328,-90.136,78.313,-90.025,78.291,-89.965,78.262,-89.873,78.238,-89.651,78.193,-89.579,78.167,-89.526,78.16,-89.49,78.172,-89.507,78.203,-89.612,78.279,-89.757,78.37,-90.001,78.496,-90.076,78.549,-90.037,78.607,-89.995,78.601,-89.926,78.573,-89.655,78.439,-89.47,78.37,-89.096,78.209,-88.97,78.184,-88.822,78.186,-88.791,78.192,-88.733,78.242,-88.648,78.334,-88.606,78.392,-88.623,78.462,-88.714,78.546,-88.742,78.584,-88.709,78.596,-88.581,78.602,-88.285,78.497,-88.148,78.477,-88.04,78.494,-87.983,78.537,-87.974,78.565,-87.982,78.595,-88.003,78.616,-88.037,78.627,-88.228,78.653,-88.254,78.672,-88.19,78.696,-88.167,78.746,-88.19,78.867,-88.164,78.933,-88.104,78.973,-88.04,78.995,-87.878,79.038,-87.829,79.045,-87.817,79.036,-87.923,78.951,-87.953,78.915,-87.961,78.893,-87.956,78.852,-87.922,78.751,-87.861,78.707,-87.617,78.676,-87.479,78.718,-87.246,78.813,-87.08,78.866,-87.016,78.899,-86.957,78.975,-86.913,78.983,-86.721,78.975,-86.629,78.991,-86.451,79.039,-86.092,79.1,-85.29,79.208,-85.181,79.234,-85.042,79.285,-85.064,79.328,-85.176,79.387,-85.501,79.53,-85.648,79.611,-85.679,79.615,-85.751,79.595,-85.804,79.573,-85.949,79.486,-86.007,79.479,-86.086,79.551,-86.18,79.605,-86.232,79.622,-86.337,79.635,-86.649,79.646,-86.861,79.598,-86.925,79.591,-87.243,79.571,-87.295,79.58,-87.22,79.63,-87.144,79.663,-87.05,79.805,-86.977,79.894,-87.076,79.967,-87.202,80.043,-87.329,80.047,-87.651,80.079,-87.861,80.088,-87.922,80.098,-87.869,80.134,-87.625,80.187,-87.618,80.207,-87.63,80.302,-87.646,80.348,-87.675,80.372,-87.96,80.416,-88.125,80.429,-88.424,80.428,-88.525,80.418,-88.644,80.387,-88.663,80.348,-88.646,80.29,-88.612,80.255,-88.381,80.225,-88.255,80.167,-88.197,80.125,-88.2,80.111,-88.329,80.134,-88.538,80.131,-88.857,80.166,-89.019,80.198,-89.198,80.263,-89.218,80.289,-89.147,80.36,-89.155,80.379,-89.197,80.394,-89.204,80.407,-89.134,80.44,-89.138,80.457,-89.167,80.48,-89.236,80.511,-89.329,80.532,-89.525,80.539,-89.674,80.531,-89.798,80.501,-89.862,80.498,-90.218,80.548,-90.537,80.576,-90.643,80.594,-90.632,80.642,-90.637,80.655,-90.683,80.688,-91.054,80.778,-91.272,80.85,-91.755,81.049]},{"b":[-96.898,74.47,-79.401,77.066],"l":[-87.593,74.47,-87.364,74.502,-86.995,74.48,-86.77,74.479,-86.666,74.489,-86.731,74.557,-86.655,74.555,-86.341,74.513,-86.211,74.536,-86.11,74.54,-85.956,74.499,-85.808,74.499,-85.544,74.535,-85.512,74.545,-85.489,74.567,-85.474,74.6,-85.442,74.601,-85.339,74.543,-85.214,74.519,-85.133,74.517,-85.087,74.528,-85.061,74.607,-85.012,74.604,-84.916,74.568,-84.818,74.542,-84.667,74.52,-84.426,74.508,-84.245,74.515,-83.868,74.564,-83.622,74.566,-83.532,74.586,-83.412,74.655,-83.394,74.67,-83.341,74.765,-83.364,74.802,-83.487,74.834,-83.51,74.848,-83.544,74.892,-83.522,74.901,-83.407,74.885,-83.22,74.828,-83.158,74.817,-83.103,74.817,-83.087,74.788,-83.112,74.732,-83.117,74.693,-83.058,74.63,-82.978,74.583,-82.931,74.566,-82.736,74.53,-82.645,74.525,-82.415,74.535,-82.069,74.482,-81.94,74.473,-81.809,74.477,-81.607,74.502,-81.34,74.554,-81.226,74.567,-80.278,74.582,-80.263,74.584,-80.221,74.657,-80.21,74.704,-80.213,74.749,-80.192,74.78,-80.149,74.796,-80.19,74.828,-80.315,74.876,-80.348,74.903,-80.289,74.908,-80.202,74.895,-79.944,74.834,-79.508,74.88,-79.401,74.918,-79.46,74.959,-79.525,74.99,-79.664,75.021,-79.733,75.021,-80.036,74.991,-80.135,74.988,-80.261,75.002,-80.382,75.034,-80.358,75.052,-79.977,75.119,-79.634,75.199,-79.509,75.26,-79.508,75.295,-79.586,75.385,-79.66,75.45,-79.738,75.461,-80.1,75.467,-80.26,75.479,-80.287,75.49,-80.126,75.542,-80.119,75.562,-80.158,75.581,-80.322,75.629,-80.528,75.642,-81.001,75.643,-81.124,75.658,-81.174,75.669,-81.193,75.684,-81.151,75.736,-81.269,75.756,-81.647,75.795,-82.154,75.831,-82.354,75.833,-82.553,75.818,-83.093,75.756,-83.237,75.751,-83.745,75.813,-83.932,75.819,-84.014,75.78,-84.128,75.763,-84.605,75.653,-84.75,75.655,-84.987,75.645,-85.372,75.573,-85.581,75.58,-85.973,75.529,-86.069,75.502,-85.905,75.442,-85.951,75.395,-86.236,75.406,-86.437,75.436,-86.545,75.463,-86.814,75.491,-87.257,75.618,-87.365,75.591,-87.539,75.485,-87.572,75.494,-87.644,75.547,-87.73,75.576,-88.201,75.512,-88.569,75.645,-88.645,75.658,-88.715,75.659,-88.784,75.647,-88.852,75.625,-88.864,75.589,-88.82,75.539,-88.804,75.502,-88.839,75.463,-88.869,75.452,-88.917,75.454,-89.28,75.564,-89.337,75.572,-89.646,75.565,-89.625,75.584,-89.361,75.646,-89.257,75.698,-89.205,75.737,-89.205,75.762,-89.278,75.795,-89.511,75.857,-89.65,75.844,-89.695,75.854,-89.793,75.925,-89.913,75.966,-90.033,75.971,-90.176,76.03,-90.251,76.053,-90.712,76.076,-90.802,76.106,-91.02,76.142,-91.279,76.16,-91.407,76.22,-91.26,76.23,-90.827,76.186,-90.312,76.158,-89.407,76.189,-89.292,76.218,-89.237,76.239,-89.219,76.258,-89.285,76.302,-90.855,76.437,-91.334,76.446,-91.415,76.456,-91.443,76.499,-91.398,76.51,-91.336,76.511,-91.263,76.5,-90.864,76.484,-90.622,76.465,-90.543,76.496,-90.555,76.516,-90.605,76.543,-90.738,76.581,-91.124,76.662,-91.305,76.681,-91.548,76.685,-91.789,76.676,-92.297,76.616,-92.716,76.603,-92.995,76.62,-93.422,76.474,-93.535,76.448,-93.485,76.492,-93.426,76.527,-93.317,76.574,-93.264,76.626,-93.201,76.669,-93.19,76.686,-93.189,76.708,-93.212,76.755,-93.23,76.77,-93.277,76.784,-93.421,76.812,-93.608,76.874,-93.811,76.914,-93.948,76.917,-94.108,76.904,-94.295,76.912,-94.616,76.958,-95.126,77.017,-95.638,77.064,-95.85,77.066,-96.061,77.05,-96.377,77.005,-96.55,76.988,-96.685,76.985,-96.758,76.972,-96.77,76.948,-96.814,76.913,-96.771,76.889,-96.661,76.855,-96.433,76.811,-96.402,76.797,-96.451,76.774,-96.59,76.763,-96.679,76.766,-96.878,76.803,-96.898,76.773,-96.898,76.754,-96.881,76.738,-96.846,76.726,-96.64,76.703,-95.971,76.57,-95.873,76.566,-95.651,76.585,-95.696,76.563,-95.789,76.537,-96.013,76.513,-96.04,76.487,-95.959,76.446,-95.842,76.416,-95.447,76.363,-95.274,76.264,-94.997,76.258,-94.737,76.293,-94.585,76.297,-94.383,76.282,-93.852,76.27,-93.665,76.273,-93.644,76.289,-93.56,76.311,-93.309,76.36,-93.192,76.366,-93.092,76.354,-92.883,76.214,-92.709,76.114,-92.474,75.986,-92.307,75.915,-92.185,75.847,-92.142,75.797,-92.099,75.727,-92.069,75.658,-92.081,75.634,-92.11,75.611,-92.331,75.479,-92.411,75.406,-92.428,75.383,-92.427,75.346,-92.408,75.297,-92.389,75.263,-92.347,75.23,-92.207,75.181,-92.076,75.124,-92.06,75.101,-92.165,75.072,-92.174,75.051,-92.103,74.948,-91.962,74.793,-91.871,74.744,-91.666,74.699,-91.549,74.656,-91.508,74.651,-91.339,74.667,-91.168,74.646,-91.135,74.65,-91.164,74.71,-91.13,74.736,-90.88,74.818,-90.878,74.801,-90.958,74.745,-90.967,74.715,-90.784,74.696,-90.553,74.613,-90.362,74.61,-90.015,74.561,-89.844,74.549,-89.559,74.555,-89.45,74.568,-89.262,74.609,-89.196,74.638,-89.189,74.667,-89.197,74.698,-89.219,74.732,-89.192,74.745,-89.115,74.738,-89.058,74.747,-89.02,74.774,-88.98,74.788,-88.94,74.79,-88.908,74.764,-88.883,74.711,-88.851,74.69,-88.778,74.715,-88.682,74.802,-88.535,74.832,-88.488,74.829,-88.431,74.804,-88.34,74.785,-88.375,74.744,-88.477,74.667,-88.538,74.609,-88.558,74.57,-88.556,74.541,-88.501,74.51,-88.423,74.494,-88.006,74.489,-87.593,74.47]},{"b":[-98.341,77.806,-94.887,78.805],"l":[-96.603,77.849,-96.477,77.872,-96.012,77.887,-95.671,77.924,-95.452,77.963,-95.371,77.971,-95.199,77.968,-95.087,77.993,-94.934,78.076,-94.936,78.106,-94.988,78.136,-95.103,78.178,-95.329,78.225,-95.268,78.263,-95.014,78.313,-94.887,78.345,-94.888,78.36,-94.915,78.391,-95.031,78.43,-95.413,78.498,-95.561,78.517,-95.968,78.505,-96.204,78.531,-96.256,78.551,-96.243,78.573,-96.265,78.595,-96.475,78.665,-96.587,78.687,-96.936,78.72,-97.169,78.758,-97.382,78.783,-97.596,78.796,-98.043,78.805,-98.212,78.805,-98.333,78.774,-98.341,78.751,-98.29,78.692,-98.096,78.587,-98.06,78.558,-98.316,78.517,-98.324,78.498,-98.317,78.477,-98.276,78.438,-98.255,78.429,-98.114,78.403,-98.069,78.386,-98.05,78.326,-97.843,78.262,-97.819,78.231,-97.323,78.203,-97.027,78.157,-96.945,78.152,-96.958,78.139,-97.041,78.117,-97.227,78.103,-97.658,78.091,-97.648,78.072,-97.621,78.05,-97.427,77.982,-97.093,77.933,-97.019,77.908,-97.052,77.881,-97.064,77.859,-97.04,77.827,-96.99,77.806,-96.834,77.812,-96.603,77.849]},{"b":[-113.283,77.344,-110.004,78.103],"l":[-110.004,78.09,-110.458,78.103,-110.727,78.097,-110.873,78.081,-111.207,78.088,-112.305,78.007,-112.805,77.942,-113.022,77.919,-113.187,77.912,-113.215,77.904,-113.269,77.86,-113.283,77.836,-113.283,77.813,-113.271,77.778,-113.19,77.718,-113.168,77.676,-113.121,77.633,-113.137,77.618,-113.189,77.6,-113.209,77.58,-113.197,77.559,-113.164,77.53,-113.046,77.511,-112.926,77.475,-112.644,77.444,-112.373,77.364,-112.177,77.344,-111.952,77.344,-111.226,77.429,-111.06,77.433,-110.894,77.426,-110.683,77.446,-110.372,77.491,-110.198,77.525,-110.139,77.572,-110.117,77.625,-110.118,77.716,-110.131,77.742,-110.153,77.763,-110.189,77.777,-110.292,77.786,-110.719,77.781,-110.812,77.803,-110.857,77.82,-110.866,77.834,-110.751,77.857,-110.2,77.905,-110.004,77.929]},{"b":[-110.004,77.929,-109.619,78.09],"l":[-110.004,77.929,-109.772,77.957,-109.679,77.999,-109.619,78.057,-109.622,78.075,-109.657,78.079,-110.004,78.09]},{"b":[-122.903,75.825,-115.47,77.548],"l":[-118.644,76.418,-118.8,76.464,-118.821,76.486,-118.791,76.513,-118.732,76.526,-118.574,76.525,-118.468,76.547,-118.431,76.588,-118.409,76.662,-118.37,76.701,-118.301,76.737,-118.203,76.76,-118.076,76.772,-117.881,76.805,-117.818,76.804,-117.78,76.784,-117.808,76.734,-117.9,76.653,-117.965,76.574,-118.005,76.497,-118.02,76.447,-117.993,76.406,-117.841,76.345,-117.732,76.317,-117.492,76.273,-117.347,76.273,-117.234,76.282,-117.154,76.298,-117.108,76.322,-117.044,76.373,-117.026,76.404,-117.013,76.469,-117.017,76.496,-116.999,76.532,-116.468,76.577,-116.22,76.611,-116.076,76.654,-115.985,76.687,-115.946,76.711,-115.945,76.736,-116.016,76.785,-116.234,76.874,-116.253,76.901,-116.183,76.916,-116.11,76.918,-115.913,76.908,-115.81,76.939,-115.857,76.969,-116.073,77.03,-116.286,77.102,-116.329,77.137,-116.214,77.178,-115.624,77.266,-115.507,77.292,-115.47,77.309,-115.476,77.324,-115.551,77.363,-116.008,77.461,-116.209,77.516,-116.363,77.543,-116.511,77.548,-116.835,77.529,-116.947,77.504,-117.04,77.465,-117.046,77.449,-117.03,77.432,-116.766,77.398,-116.704,77.38,-116.795,77.347,-116.844,77.34,-117.061,77.348,-117.149,77.361,-117.211,77.331,-117.279,77.313,-117.419,77.317,-118.005,77.381,-118.82,77.333,-119.09,77.305,-119.324,77.241,-119.495,77.177,-119.831,77.074,-120.2,76.931,-120.311,76.905,-120.358,76.887,-120.438,76.816,-120.485,76.793,-120.998,76.691,-121.102,76.661,-121.204,76.622,-121.561,76.453,-121.614,76.441,-122.365,76.401,-122.423,76.39,-122.519,76.353,-122.774,76.228,-122.878,76.165,-122.903,76.135,-122.685,76.162,-122.624,76.167,-122.593,76.162,-122.588,76.153,-122.609,76.14,-122.609,76.121,-122.548,76.097,-122.546,76.081,-122.607,76.039,-122.646,76.031,-122.64,76.009,-122.591,75.973,-122.533,75.951,-122.4,75.944,-122.303,75.96,-122.057,76.018,-121.908,76.035,-121.695,76.02,-121.428,75.981,-121.32,75.977,-121.213,75.984,-121.019,76.02,-120.9,76.163,-120.848,76.183,-120.813,76.179,-120.772,76.166,-120.729,76.134,-120.637,76.034,-120.563,76.008,-120.514,75.958,-120.458,75.87,-120.409,75.826,-120.365,75.825,-120.161,75.852,-119.913,75.859,-119.735,75.915,-119.667,75.946,-119.608,75.985,-119.538,75.982,-119.526,75.997,-119.527,76.031,-119.55,76.052,-119.725,76.1,-119.74,76.118,-119.64,76.157,-119.636,76.19,-119.651,76.244,-119.649,76.28,-119.58,76.327,-119.524,76.34,-119.489,76.32,-119.448,76.275,-119.368,76.222,-119.249,76.159,-119.168,76.127,-119.081,76.124,-118.994,76.145,-118.955,76.168,-118.851,76.258,-118.812,76.277,-118.643,76.335,-118.625,76.366,-118.644,76.418]},{"b":[-117.6,74.402,-110.003,76.506],"l":[-110.003,75.539,-110.459,75.555,-110.726,75.56,-110.89,75.547,-111.053,75.549,-111.163,75.57,-111.276,75.613,-111.373,75.676,-111.454,75.762,-111.513,75.811,-111.549,75.822,-111.709,75.832,-111.877,75.826,-112.057,75.834,-112.081,75.847,-112.047,75.866,-111.868,75.911,-111.865,75.939,-112.334,76.072,-112.698,76.202,-112.978,76.245,-113.171,76.258,-113.363,76.248,-113.823,76.207,-113.923,76.229,-114.059,76.301,-114.101,76.331,-114.113,76.349,-114.116,76.396,-114.141,76.423,-114.194,76.451,-114.299,76.475,-114.535,76.502,-114.767,76.506,-114.998,76.497,-115.581,76.438,-115.779,76.365,-115.826,76.33,-115.832,76.296,-115.822,76.27,-115.797,76.253,-115.664,76.24,-115.025,76.211,-114.88,76.195,-114.779,76.173,-114.939,76.166,-115.768,76.184,-116.059,76.202,-116.21,76.194,-116.454,76.143,-116.591,76.096,-116.61,76.074,-116.55,76.017,-116.58,75.992,-116.665,75.958,-116.654,75.929,-116.444,75.891,-116.338,75.881,-115.602,75.895,-114.992,75.896,-115.174,75.867,-115.477,75.841,-115.838,75.841,-116.39,75.808,-116.802,75.772,-116.973,75.746,-117.039,75.718,-117.164,75.645,-117.138,75.617,-117.025,75.602,-116.426,75.585,-116.034,75.607,-115.122,75.706,-115.117,75.695,-115.142,75.679,-115.251,75.639,-115.335,75.618,-116.077,75.493,-116.213,75.483,-116.891,75.481,-117.154,75.473,-117.258,75.46,-117.336,75.442,-117.388,75.421,-117.513,75.357,-117.576,75.314,-117.597,75.293,-117.6,75.272,-117.565,75.233,-117.502,75.204,-117.005,75.156,-116.841,75.152,-116.476,75.172,-116.143,75.042,-115.729,74.968,-115.683,74.974,-115.609,75.01,-115.574,75.056,-115.537,75.081,-115.478,75.104,-115.413,75.115,-115.343,75.113,-115.28,75.102,-115.174,75.049,-115.128,75.009,-115.077,74.985,-115.02,74.976,-114.859,75,-114.452,75.088,-114.356,75.141,-114.358,75.171,-114.504,75.258,-114.514,75.275,-114.483,75.285,-114.429,75.281,-114.285,75.25,-114.168,75.24,-114.125,75.291,-114.075,75.392,-114.053,75.417,-114.017,75.434,-113.984,75.43,-113.916,75.388,-113.879,75.375,-113.589,75.412,-113.467,75.416,-113.503,75.397,-113.759,75.322,-113.811,75.296,-113.853,75.259,-113.886,75.211,-113.861,75.188,-113.855,75.129,-113.845,75.112,-113.795,75.084,-113.712,75.069,-113.34,75.093,-112.951,75.108,-112.855,75.121,-112.8,75.138,-112.703,75.187,-112.652,75.205,-112.597,75.212,-112.478,75.2,-112.256,75.134,-112.214,75.133,-112,75.142,-111.781,75.166,-111.621,75.168,-111.474,75.191,-111.181,75.26,-111.093,75.256,-111.033,75.227,-111.079,75.195,-111.258,75.128,-111.503,75.056,-111.671,75.019,-111.784,75.006,-111.956,75,-112.193,75.01,-112.663,74.994,-112.836,74.976,-113.324,74.875,-113.863,74.813,-114.132,74.766,-114.313,74.715,-114.377,74.671,-114.268,74.604,-114.175,74.574,-113.837,74.489,-113.672,74.453,-113.514,74.43,-113.017,74.402,-112.519,74.417,-111.729,74.502,-111.288,74.585,-110.941,74.639,-110.749,74.688,-110.625,74.753,-110.543,74.78,-110.387,74.814,-110.176,74.84,-110.003,74.851]},{"b":[-110.003,75.506,-108.9,76.244],"l":[-110.003,76.244,-109.908,76.223,-109.71,76.212,-109.487,76.145,-109.43,76.109,-109.417,76.072,-109.425,76.043,-109.455,76.021,-109.871,75.929,-109.796,75.863,-108.945,75.699,-108.918,75.675,-108.9,75.624,-108.913,75.587,-108.947,75.542,-109.005,75.515,-109.086,75.506,-110.003,75.539]},{"b":[-110.314,76.244,-110.003,76.48],"l":[-110.003,76.48,-110.27,76.417,-110.309,76.397,-110.314,76.369,-110.285,76.333,-110.247,76.306,-110.201,76.289,-110.003,76.244]},{"b":[-110.003,74.851,-105.481,76.821],"l":[-110.003,74.851,-109.503,74.883,-109.003,75.01,-108.831,75.065,-108.666,75.04,-108.633,75.023,-108.67,75.007,-108.751,74.992,-108.594,74.96,-108.475,74.947,-108.354,74.943,-108.227,74.952,-108.024,74.986,-107.82,75,-107.462,74.952,-107.153,74.927,-107.056,74.928,-106.961,74.94,-106.588,75.015,-106.093,75.089,-105.972,75.131,-105.863,75.192,-105.703,75.413,-105.678,75.501,-105.519,75.632,-105.481,75.702,-105.481,75.746,-105.563,75.881,-105.604,75.93,-105.633,75.945,-105.711,75.967,-105.905,76.009,-106.397,76.06,-106.529,76.053,-106.677,76.024,-106.804,75.975,-106.846,75.952,-106.862,75.93,-106.82,75.872,-106.759,75.842,-106.688,75.819,-106.693,75.81,-106.892,75.782,-106.903,75.742,-106.904,75.689,-106.914,75.68,-106.971,75.773,-107.05,75.845,-107.08,75.863,-107.136,75.879,-107.216,75.892,-107.418,75.907,-107.541,75.901,-107.703,75.878,-107.918,75.802,-107.951,75.796,-108.021,75.805,-107.97,75.84,-107.755,75.94,-107.732,75.956,-107.721,75.974,-107.723,75.995,-107.777,76.035,-107.852,76.058,-108.019,76.065,-108.166,76.054,-108.292,76.057,-108.387,76.067,-108.406,76.085,-108.382,76.116,-108.306,76.154,-108.178,76.2,-108.123,76.233,-108.194,76.33,-108.345,76.392,-108.512,76.439,-108.524,76.447,-108.539,76.503,-108.56,76.536,-108.628,76.587,-108.635,76.609,-108.612,76.63,-108.512,76.68,-108.478,76.708,-108.467,76.738,-108.492,76.754,-108.554,76.758,-108.832,76.821,-109.098,76.812,-109.22,76.792,-109.339,76.76,-109.505,76.692,-109.865,76.522,-109.982,76.485,-110.003,76.48]},{"b":[-95.648,72.001,-90.355,74.179],"l":[-91.088,74.009,-91.63,74.028,-91.874,74.013,-92.223,73.972,-92.314,73.992,-92.493,74.062,-92.587,74.083,-92.778,74.114,-93.171,74.161,-93.41,74.179,-93.549,74.167,-93.785,74.118,-93.939,74.132,-94.483,74.113,-94.729,74.086,-94.974,74.041,-95.04,74.024,-95.121,73.985,-95.145,73.96,-95.153,73.933,-95.149,73.906,-95.134,73.881,-95.059,73.805,-94.897,73.716,-94.797,73.686,-94.691,73.671,-94.698,73.664,-94.817,73.663,-94.996,73.686,-95.386,73.755,-95.447,73.752,-95.569,73.728,-95.633,73.695,-95.645,73.671,-95.648,73.639,-95.644,73.557,-95.604,73.328,-95.589,73.174,-95.592,73.115,-95.612,72.999,-95.613,72.942,-95.602,72.884,-95.58,72.831,-95.548,72.782,-95.251,72.502,-95.193,72.345,-95.167,72.18,-95.193,72.027,-95.008,72.013,-94.611,72.042,-94.497,72.044,-94.144,72.001,-94.038,72.029,-93.973,72.13,-93.871,72.253,-93.555,72.421,-93.542,72.437,-93.534,72.499,-93.546,72.531,-93.572,72.559,-93.771,72.668,-93.92,72.703,-94.152,72.736,-94.211,72.757,-93.579,72.801,-93.341,72.802,-92.392,72.718,-92.235,72.727,-92.118,72.754,-91.905,72.849,-91.788,72.915,-91.621,73.026,-91.46,73.145,-91.426,73.195,-91.466,73.214,-91.554,73.236,-91.298,73.285,-91.249,73.304,-91.068,73.416,-91.002,73.467,-90.975,73.502,-90.934,73.528,-90.765,73.581,-90.566,73.686,-90.466,73.754,-90.381,73.825,-90.355,73.869,-90.458,73.908,-90.627,73.952,-91.088,74.009]},{"b":[-99.564,68.461,-95.268,69.862],"l":[-95.585,68.835,-95.751,68.898,-95.855,68.954,-95.951,69.024,-96.061,69.125,-96.184,69.259,-96.3,69.344,-96.695,69.471,-96.875,69.51,-96.989,69.554,-97.096,69.615,-97.236,69.673,-97.278,69.68,-97.306,69.673,-97.351,69.641,-97.409,69.631,-97.439,69.643,-97.469,69.667,-97.46,69.683,-97.386,69.7,-97.383,69.712,-97.411,69.738,-97.604,69.802,-97.691,69.841,-97.791,69.862,-97.889,69.858,-98.081,69.833,-98.2,69.797,-98.239,69.78,-98.268,69.754,-98.301,69.692,-98.304,69.669,-98.289,69.629,-98.163,69.512,-98.041,69.457,-98.156,69.469,-98.222,69.485,-98.389,69.565,-98.476,69.579,-98.546,69.573,-98.548,69.545,-98.534,69.527,-98.495,69.499,-98.448,69.48,-98.537,69.478,-98.559,69.461,-98.535,69.426,-98.467,69.375,-98.45,69.354,-98.456,69.335,-98.504,69.308,-98.724,69.219,-98.912,69.168,-99.085,69.15,-99.456,69.131,-99.513,69.1,-99.557,69.054,-99.564,69.034,-99.495,68.96,-99.441,68.918,-99.318,68.876,-99.254,68.863,-99.091,68.863,-99.073,68.877,-99.094,68.899,-99.057,68.918,-98.964,68.933,-98.904,68.932,-98.879,68.916,-98.864,68.894,-98.859,68.864,-98.83,68.839,-98.775,68.817,-98.704,68.803,-98.54,68.798,-98.432,68.818,-98.376,68.842,-98.321,68.843,-98.296,68.831,-98.28,68.807,-98.273,68.772,-98.258,68.749,-98.235,68.739,-97.885,68.672,-97.705,68.626,-97.472,68.544,-97.264,68.528,-97.008,68.539,-96.599,68.461,-96.402,68.471,-96.268,68.508,-96.024,68.607,-95.895,68.627,-95.802,68.686,-95.686,68.736,-95.614,68.745,-95.466,68.747,-95.359,68.778,-95.295,68.805,-95.268,68.826,-95.32,68.873,-95.374,68.892,-95.438,68.881,-95.585,68.835]},{"b":[-118.994,69.65,-110.002,73.373],"l":[-110.002,72.982,-110.008,72.984,-110.095,72.992,-110.509,72.999,-110.661,73.008,-110.689,72.945,-110.554,72.861,-110.279,72.792,-110.197,72.759,-110.208,72.681,-110.205,72.661,-110.439,72.633,-110.513,72.6,-110.782,72.534,-110.959,72.432,-111.14,72.365,-111.185,72.357,-111.268,72.364,-111.284,72.378,-111.287,72.401,-111.277,72.425,-111.253,72.449,-111.265,72.459,-111.311,72.455,-111.447,72.408,-111.544,72.351,-111.675,72.3,-111.762,72.335,-111.895,72.356,-111.816,72.386,-111.611,72.436,-111.356,72.572,-111.25,72.669,-111.27,72.714,-111.455,72.766,-112.048,72.888,-112.454,72.937,-112.754,72.986,-113.074,72.995,-113.208,72.981,-113.292,72.95,-113.45,72.863,-113.491,72.822,-113.496,72.754,-113.486,72.722,-113.5,72.694,-113.578,72.652,-113.622,72.647,-113.692,72.673,-113.958,72.651,-114.174,72.624,-114.342,72.591,-114.458,72.58,-114.522,72.593,-114.498,72.626,-114.28,72.739,-114.178,72.805,-114.109,72.861,-114.075,72.907,-114.054,72.958,-114.046,73.015,-114.052,73.071,-114.095,73.18,-114.127,73.231,-114.164,73.27,-114.206,73.298,-114.302,73.331,-114.638,73.373,-115.552,73.213,-116.573,73.055,-116.972,72.959,-117.256,72.914,-117.552,72.831,-118.133,72.633,-118.375,72.534,-118.457,72.473,-118.481,72.428,-118.449,72.399,-118.39,72.37,-118.246,72.311,-118.207,72.287,-118.213,72.263,-118.369,72.205,-118.59,72.167,-118.945,71.986,-118.96,71.972,-118.984,71.913,-118.994,71.803,-118.988,71.764,-118.952,71.732,-118.868,71.687,-118.583,71.649,-118.372,71.64,-117.888,71.661,-117.742,71.659,-117.878,71.561,-118.148,71.526,-118.226,71.467,-118.222,71.449,-118.188,71.436,-117.936,71.392,-117.723,71.391,-117.337,71.435,-116.78,71.444,-115.587,71.546,-115.338,71.511,-115.303,71.494,-115.341,71.472,-115.472,71.466,-115.734,71.485,-115.98,71.469,-116.044,71.454,-116.045,71.423,-115.922,71.401,-115.892,71.382,-116.042,71.362,-116.228,71.359,-116.422,71.338,-116.815,71.277,-117.314,71.212,-117.814,71.158,-117.934,71.135,-118.269,71.035,-118.353,71,-118.377,70.968,-118.264,70.888,-117.587,70.63,-116.993,70.604,-116.327,70.624,-116.226,70.616,-116.086,70.591,-115.991,70.586,-115.311,70.601,-114.841,70.621,-114.593,70.642,-114.331,70.675,-114.232,70.674,-113.966,70.696,-113.757,70.691,-113.397,70.652,-113.146,70.616,-112.114,70.447,-111.726,70.352,-111.633,70.309,-111.705,70.286,-111.784,70.273,-112.19,70.276,-112.266,70.255,-112.523,70.229,-112.638,70.225,-113.211,70.264,-113.666,70.27,-113.917,70.282,-114.167,70.307,-114.592,70.312,-115.529,70.257,-116.554,70.175,-117.135,70.1,-117.163,70.092,-117.195,70.054,-117.184,69.991,-117.149,69.888,-117.122,69.826,-117.104,69.804,-116.993,69.719,-116.856,69.65]},{"b":[-116.856,68.477,-100.906,73.349],"l":[-116.856,69.65,-116.712,69.576,-116.609,69.512,-116.569,69.463,-116.537,69.434,-116.513,69.425,-116.102,69.337,-115.861,69.304,-115.618,69.283,-115.159,69.265,-114.699,69.273,-114.323,69.269,-114.073,69.251,-113.694,69.195,-113.681,69.182,-113.609,69.03,-113.593,68.96,-113.617,68.838,-113.555,68.768,-113.338,68.599,-113.231,68.535,-113.128,68.494,-113.02,68.481,-112.864,68.477,-112.666,68.485,-112.305,68.516,-111.518,68.533,-111.311,68.542,-111.128,68.588,-110.957,68.594,-110.848,68.578,-110.468,68.61,-109.959,68.63,-109.472,68.677,-108.946,68.76,-108.73,68.827,-108.553,68.897,-108.365,68.935,-107.863,68.954,-107.44,69.002,-107.353,69.032,-107.123,69.152,-107.033,69.181,-106.856,69.347,-106.76,69.407,-106.659,69.44,-106.54,69.443,-106.42,69.414,-106.361,69.381,-106.344,69.34,-106.356,69.281,-106.354,69.251,-106.341,69.224,-106.27,69.195,-106.141,69.162,-106.008,69.148,-105.805,69.153,-105.533,69.134,-105.262,69.094,-105.02,69.081,-105.014,69.068,-105.022,69.052,-105.148,68.978,-105.169,68.955,-105.106,68.92,-104.571,68.872,-104.46,68.912,-104.353,68.928,-104.067,68.866,-103.82,68.848,-103.468,68.809,-103.162,68.829,-102.895,68.824,-102.835,68.833,-102.738,68.865,-102.488,68.889,-102.359,68.923,-101.981,68.989,-101.857,69.024,-101.788,69.132,-101.789,69.182,-101.823,69.217,-101.873,69.24,-101.899,69.246,-101.993,69.236,-102.046,69.258,-102.064,69.281,-102.071,69.308,-102.067,69.337,-102.053,69.36,-101.976,69.407,-101.978,69.425,-102.046,69.465,-102.151,69.488,-102.447,69.476,-102.546,69.434,-102.777,69.378,-102.884,69.341,-103.09,69.212,-103.12,69.205,-103.113,69.236,-103.04,69.368,-103.032,69.433,-103.049,69.472,-103.063,69.485,-103.102,69.483,-103.142,69.497,-103.294,69.568,-103.418,69.611,-103.465,69.644,-103.435,69.668,-103.359,69.685,-103.303,69.674,-103.059,69.595,-102.92,69.565,-102.744,69.548,-102.621,69.552,-102.563,69.574,-102.541,69.592,-102.535,69.621,-102.545,69.66,-102.565,69.692,-102.596,69.718,-102.523,69.758,-102.348,69.813,-102.234,69.842,-102.182,69.846,-102.098,69.825,-101.86,69.738,-101.734,69.704,-101.648,69.699,-101.602,69.721,-101.565,69.756,-101.508,69.833,-101.484,69.85,-101.457,69.834,-101.4,69.749,-101.337,69.71,-101.216,69.68,-101.044,69.669,-100.982,69.68,-100.935,69.715,-100.906,69.812,-100.909,69.869,-100.973,70.029,-101.043,70.111,-101.091,70.136,-101.149,70.148,-101.239,70.151,-101.562,70.135,-101.618,70.172,-101.627,70.248,-101.641,70.266,-101.676,70.278,-101.732,70.286,-101.937,70.275,-101.99,70.285,-102.369,70.413,-102.589,70.469,-102.75,70.522,-103.05,70.655,-103.089,70.65,-103.083,70.619,-103.001,70.541,-103.005,70.526,-103.021,70.516,-103.077,70.509,-103.105,70.51,-103.197,70.547,-103.295,70.572,-103.585,70.631,-103.853,70.734,-103.953,70.763,-104.167,70.927,-104.515,71.064,-104.57,71.104,-104.563,71.132,-104.487,71.248,-104.437,71.3,-104.385,71.338,-104.356,71.382,-104.35,71.434,-104.355,71.472,-104.373,71.495,-104.386,71.577,-104.518,71.699,-104.767,71.868,-104.81,71.903,-104.878,71.98,-105.234,72.415,-105.247,72.464,-105.298,72.56,-105.323,72.635,-105.355,72.67,-105.411,72.709,-105.43,72.74,-105.412,72.765,-105.415,72.788,-105.496,72.849,-105.624,72.927,-105.813,73.011,-106.082,73.072,-106.482,73.196,-106.828,73.266,-106.951,73.276,-107.033,73.245,-107.074,73.197,-107.113,73.192,-107.496,73.288,-107.72,73.329,-108.029,73.349,-108.089,73.304,-108.077,73.281,-107.987,73.233,-107.936,73.217,-107.98,73.207,-108.118,73.202,-108.204,73.183,-108.237,73.15,-108.238,73.106,-107.997,72.653,-107.934,72.588,-107.924,72.557,-107.933,72.52,-107.91,72.491,-107.856,72.468,-107.824,72.443,-107.809,72.347,-107.794,72.303,-107.696,72.149,-107.543,72.025,-107.306,71.895,-107.377,71.886,-107.382,71.875,-107.369,71.859,-107.329,71.835,-107.347,71.819,-107.687,71.716,-107.757,71.663,-107.785,71.63,-107.813,71.626,-107.925,71.639,-108.021,71.677,-108.145,71.705,-108.188,71.724,-108.21,71.751,-108.276,71.9,-108.47,72.139,-108.566,72.317,-108.628,72.412,-108.698,72.499,-108.755,72.551,-108.798,72.568,-108.951,72.583,-108.994,72.596,-108.985,72.637,-108.968,72.654,-108.987,72.671,-109.043,72.687,-109.122,72.726,-109.357,72.775,-109.469,72.808,-109.61,72.876,-110.002,72.982]},{"b":[-125.845,71.088,-115.393,74.545],"l":[-122.623,74.464,-123.468,74.436,-124.696,74.348,-124.709,74.327,-124.645,74.304,-124.629,74.27,-124.575,74.248,-124.261,73.953,-124.192,73.902,-124.088,73.857,-123.873,73.828,-123.798,73.785,-123.797,73.768,-124.03,73.644,-124.114,73.527,-124.424,73.419,-124.594,73.243,-124.647,73.204,-124.804,73.126,-124.836,73.076,-124.817,73.059,-124.736,73.023,-124.643,73.019,-124.588,73.005,-124.57,72.989,-124.561,72.965,-124.565,72.944,-124.583,72.926,-124.931,72.863,-124.97,72.843,-125,72.813,-125.015,72.776,-125.015,72.731,-125.03,72.645,-125.019,72.617,-124.985,72.604,-124.987,72.588,-125.07,72.552,-125.168,72.523,-125.306,72.451,-125.383,72.424,-125.438,72.382,-125.512,72.308,-125.575,72.265,-125.627,72.255,-125.647,72.237,-125.634,72.21,-125.613,72.193,-125.584,72.183,-125.763,72.138,-125.769,72.129,-125.76,72.083,-125.768,72.054,-125.79,72.025,-125.845,71.979,-125.829,71.966,-125.767,71.961,-125.297,71.973,-125.215,71.955,-125.126,71.924,-124.76,71.835,-124.008,71.677,-123.953,71.652,-123.756,71.528,-123.682,71.493,-123.595,71.423,-123.393,71.219,-123.315,71.169,-123.211,71.123,-123.096,71.094,-122.937,71.088,-122.84,71.097,-122.72,71.128,-122.55,71.194,-122.157,71.266,-121.749,71.445,-121.701,71.451,-121.622,71.448,-121.547,71.407,-121.472,71.389,-121.16,71.415,-120.93,71.446,-120.619,71.506,-120.52,71.557,-120.461,71.605,-120.443,71.631,-120.366,71.888,-120.31,71.984,-120.194,72.127,-120.18,72.213,-120.09,72.229,-119.767,72.244,-119.513,72.303,-119.408,72.36,-119.132,72.609,-119.078,72.64,-118.962,72.684,-117.983,72.902,-117.464,73.038,-117.065,73.107,-116.483,73.253,-116.239,73.295,-115.992,73.323,-115.524,73.417,-115.447,73.439,-115.412,73.465,-115.393,73.502,-115.408,73.542,-115.456,73.585,-115.511,73.619,-115.634,73.666,-115.958,73.748,-116.722,74.027,-116.95,74.101,-117.199,74.171,-117.515,74.232,-117.707,74.252,-117.966,74.266,-118.2,74.267,-118.544,74.245,-118.625,74.233,-118.744,74.192,-119.026,74.045,-119.083,74.021,-119.118,74.016,-119.132,74.028,-119.139,74.128,-119.15,74.168,-119.171,74.186,-119.206,74.198,-119.315,74.206,-119.471,74.201,-119.729,74.108,-119.736,74.113,-119.737,74.13,-119.715,74.154,-119.563,74.233,-119.944,74.254,-120.554,74.353,-120.882,74.421,-121.129,74.49,-121.315,74.53,-121.504,74.545,-121.748,74.541,-122.623,74.464]},{"b":[-56.243,46.753,-56.137,46.811],"l":[-56.243,46.767,-56.172,46.753,-56.151,46.762,-56.139,46.779,-56.137,46.802,-56.153,46.811,-56.185,46.807,-56.209,46.798,-56.243,46.767]},{"b":[-94.901,75.749,-94.296,75.996],"l":[-94.745,75.957,-94.84,75.954,-94.881,75.946,-94.901,75.931,-94.894,75.912,-94.86,75.889,-94.834,75.859,-94.815,75.821,-94.787,75.791,-94.751,75.77,-94.624,75.749,-94.527,75.749,-94.33,75.766,-94.304,75.776,-94.296,75.788,-94.332,75.826,-94.414,75.885,-94.443,75.917,-94.471,75.971,-94.499,75.992,-94.538,75.996,-94.745,75.957]},{"b":[-79.644,51.945,-79.27,52.098],"l":[-79.644,52.01,-79.597,51.978,-79.521,51.953,-79.426,51.945,-79.384,51.952,-79.352,51.968,-79.329,51.992,-79.317,52.024,-79.27,52.071,-79.271,52.087,-79.335,52.098,-79.644,52.01]},{"b":[-78.914,56.131,-78.657,56.439],"l":[-78.669,56.439,-78.725,56.439,-78.762,56.421,-78.799,56.383,-78.822,56.34,-78.828,56.29,-78.857,56.232,-78.907,56.166,-78.914,56.133,-78.877,56.131,-78.827,56.145,-78.761,56.175,-78.71,56.213,-78.673,56.26,-78.657,56.317,-78.669,56.439]},{"b":[-80.008,55.807,-78.907,56.6],"l":[-79.765,55.807,-79.606,55.876,-79.527,55.871,-79.495,55.875,-79.455,55.896,-79.407,55.935,-79.222,56.176,-79.182,56.212,-79.142,56.181,-79.136,56.16,-79.142,56.136,-79.274,55.922,-79.228,55.879,-79.175,55.885,-79.084,56.068,-79.018,56.165,-78.936,56.266,-78.907,56.32,-78.931,56.328,-78.942,56.345,-78.94,56.371,-78.963,56.422,-78.995,56.436,-79.078,56.454,-79.1,56.474,-79.124,56.52,-79.155,56.538,-79.21,56.549,-79.246,56.568,-79.261,56.596,-79.272,56.6,-79.305,56.463,-79.339,56.376,-79.393,56.276,-79.458,56.211,-79.536,56.18,-79.554,56.192,-79.512,56.247,-79.476,56.313,-79.432,56.447,-79.435,56.513,-79.448,56.537,-79.459,56.54,-79.469,56.523,-79.468,56.46,-79.482,56.404,-79.515,56.327,-79.596,56.244,-79.79,56.114,-80.001,55.932,-80.008,55.911,-79.987,55.892,-79.905,55.871,-79.781,55.941,-79.565,56.121,-79.545,56.128,-79.495,56.115,-79.497,56.093,-79.765,55.807]},{"b":[-79.946,56.757,-79.827,56.865],"l":[-79.946,56.827,-79.944,56.777,-79.894,56.757,-79.867,56.775,-79.835,56.816,-79.827,56.843,-79.861,56.864,-79.898,56.865,-79.946,56.827]},{"b":[-79.838,57.442,-79.713,57.608],"l":[-79.775,57.514,-79.732,57.508,-79.717,57.516,-79.713,57.555,-79.727,57.605,-79.743,57.608,-79.768,57.599,-79.811,57.559,-79.819,57.542,-79.816,57.518,-79.838,57.483,-79.808,57.442,-79.792,57.449,-79.775,57.514]},{"b":[-80.089,56.199,-79.58,56.466],"l":[-79.688,56.327,-79.633,56.387,-79.58,56.466,-79.606,56.459,-79.681,56.404,-79.749,56.377,-79.81,56.377,-79.852,56.367,-79.874,56.348,-80.005,56.318,-80.057,56.287,-80.089,56.214,-80.029,56.199,-79.978,56.207,-79.688,56.327]},{"b":[-80.325,59.621,-80.167,59.725],"l":[-80.299,59.674,-80.325,59.633,-80.317,59.621,-80.285,59.624,-80.241,59.645,-80.183,59.683,-80.167,59.709,-80.21,59.725,-80.257,59.679,-80.299,59.674]},{"b":[-80.074,53.274,-79.938,53.364],"l":[-80.05,53.364,-80.074,53.344,-80.068,53.324,-80.039,53.297,-80.004,53.28,-79.939,53.274,-79.938,53.304,-79.975,53.352,-80.05,53.364]},{"b":[-80.167,59.764,-79.899,59.877],"l":[-79.95,59.81,-79.899,59.853,-79.956,59.877,-80.041,59.87,-80.084,59.852,-80.122,59.823,-80.167,59.764,-80.064,59.771,-79.95,59.81]},{"b":[-79.584,56.644,-79.482,56.799],"l":[-79.551,56.733,-79.577,56.645,-79.553,56.644,-79.518,56.657,-79.497,56.667,-79.485,56.687,-79.482,56.714,-79.491,56.743,-79.511,56.771,-79.553,56.799,-79.57,56.796,-79.584,56.781,-79.582,56.765,-79.551,56.733]},{"b":[-97.098,72.927,-96.598,73.189],"l":[-96.944,72.927,-96.782,72.937,-96.709,72.947,-96.671,72.961,-96.635,72.992,-96.604,73.042,-96.598,73.074,-96.646,73.102,-96.744,73.126,-96.768,73.137,-96.793,73.165,-96.862,73.189,-97.015,73.157,-97.069,73.13,-97.088,73.098,-97.098,73.062,-97.093,72.997,-96.944,72.927]},{"b":[-97.75,74.466,-97.291,74.627],"l":[-97.356,74.526,-97.304,74.56,-97.291,74.576,-97.318,74.598,-97.417,74.627,-97.516,74.602,-97.75,74.511,-97.722,74.489,-97.656,74.466,-97.356,74.526]},{"b":[-99.417,73.812,-97.659,74.115],"l":[-98.974,73.812,-98.817,73.817,-98.761,73.829,-98.691,73.856,-98.558,73.847,-98.27,73.869,-98.147,73.889,-97.861,73.968,-97.755,74.006,-97.673,74.053,-97.659,74.072,-97.667,74.09,-97.698,74.109,-97.8,74.115,-98.061,74.105,-98.585,74.035,-98.818,74.021,-98.904,74.007,-98.967,73.988,-99.005,73.965,-99.097,73.948,-99.346,73.926,-99.404,73.911,-99.417,73.895,-99.385,73.879,-99.298,73.862,-98.974,73.812]},{"b":[-90.492,69.252,-90.177,69.436],"l":[-90.177,69.357,-90.2,69.419,-90.229,69.436,-90.253,69.418,-90.322,69.429,-90.377,69.416,-90.455,69.39,-90.492,69.37,-90.465,69.329,-90.364,69.263,-90.33,69.252,-90.295,69.258,-90.267,69.273,-90.177,69.357]},{"b":[-90.772,69.209,-90.485,69.374],"l":[-90.6,69.368,-90.663,69.374,-90.742,69.357,-90.766,69.336,-90.772,69.293,-90.686,69.287,-90.667,69.259,-90.626,69.251,-90.574,69.209,-90.493,69.221,-90.485,69.247,-90.511,69.29,-90.54,69.325,-90.6,69.368]},{"b":[-74.626,62.61,-73.988,62.733],"l":[-74.395,62.696,-74.501,62.727,-74.564,62.733,-74.62,62.726,-74.626,62.713,-74.5,62.669,-74.254,62.622,-74.054,62.61,-74,62.618,-73.988,62.636,-74.017,62.663,-74.109,62.68,-74.395,62.696]},{"b":[-75.403,68.342,-74.798,68.696],"l":[-74.798,68.458,-74.819,68.494,-74.885,68.545,-74.984,68.648,-75.075,68.685,-75.2,68.696,-75.287,68.688,-75.37,68.636,-75.396,68.589,-75.403,68.55,-75.4,68.525,-75.31,68.474,-75.073,68.404,-74.959,68.342,-74.881,68.349,-74.845,68.366,-74.819,68.394,-74.813,68.413,-74.828,68.424,-74.831,68.441,-74.798,68.458]},{"b":[-78.669,60.717,-78.242,60.819],"l":[-78.612,60.772,-78.669,60.731,-78.669,60.717,-78.532,60.729,-78.372,60.756,-78.279,60.784,-78.242,60.819,-78.4,60.808,-78.612,60.772]},{"b":[-79.175,68.182,-78.829,68.353],"l":[-79.153,68.335,-79.175,68.264,-79.174,68.235,-79.064,68.182,-78.983,68.193,-78.829,68.268,-78.869,68.31,-78.953,68.353,-79.153,68.335]},{"b":[-77.365,63.384,-76.652,63.682],"l":[-76.652,63.504,-76.764,63.574,-77.134,63.682,-77.365,63.588,-77.057,63.45,-76.922,63.406,-76.783,63.384,-76.678,63.394,-76.652,63.504]},{"b":[-64.956,62.384,-64.418,62.559],"l":[-64.465,62.536,-64.515,62.552,-64.632,62.548,-64.824,62.559,-64.849,62.543,-64.85,62.525,-64.827,62.505,-64.842,62.494,-64.931,62.485,-64.956,62.458,-64.901,62.421,-64.837,62.406,-64.657,62.384,-64.546,62.391,-64.478,62.418,-64.418,62.487,-64.465,62.536]},{"b":[-62.872,67.029,-62.396,67.19],"l":[-62.625,67.177,-62.664,67.148,-62.757,67.113,-62.825,67.072,-62.872,67.063,-62.805,67.029,-62.682,67.056,-62.485,67.134,-62.396,67.178,-62.417,67.188,-62.47,67.19,-62.625,67.177]},{"b":[-68.368,60.233,-67.819,60.588],"l":[-68.088,60.588,-68.142,60.562,-68.235,60.456,-68.338,60.361,-68.368,60.315,-68.365,60.254,-68.324,60.233,-68.234,60.241,-68.012,60.305,-67.922,60.34,-67.844,60.392,-67.819,60.45,-67.848,60.489,-67.914,60.54,-67.978,60.57,-68.088,60.588]},{"b":[-71.22,62.545,-70.269,62.878],"l":[-70.367,62.666,-70.443,62.734,-70.674,62.807,-70.835,62.84,-71.014,62.865,-71.135,62.878,-71.22,62.874,-71.137,62.816,-70.986,62.788,-70.851,62.704,-70.838,62.648,-70.766,62.597,-70.687,62.573,-70.542,62.552,-70.406,62.545,-70.337,62.549,-70.289,62.562,-70.269,62.578,-70.292,62.616,-70.367,62.666]},{"b":[-65.432,61.354,-64.67,61.686],"l":[-64.788,61.413,-64.732,61.438,-64.696,61.471,-64.691,61.539,-64.67,61.593,-64.756,61.638,-64.79,61.662,-64.954,61.685,-65.13,61.686,-65.332,61.668,-65.432,61.65,-65.427,61.611,-65.394,61.563,-65.092,61.453,-65.054,61.432,-64.954,61.41,-64.88,61.357,-64.857,61.354,-64.833,61.366,-64.788,61.413]},{"b":[-68.221,69.535,-67.755,69.682],"l":[-67.755,69.631,-67.829,69.675,-67.909,69.682,-67.989,69.679,-68.093,69.657,-68.221,69.617,-68.202,69.58,-67.94,69.535,-67.915,69.541,-67.845,69.592,-67.755,69.631]},{"b":[-80.795,69.51,-79.365,69.81],"l":[-79.553,69.631,-79.402,69.685,-79.365,69.712,-79.39,69.73,-79.431,69.788,-79.594,69.81,-79.715,69.796,-79.87,69.756,-79.971,69.739,-80.062,69.746,-80.125,69.737,-80.169,69.782,-80.214,69.802,-80.295,69.794,-80.424,69.798,-80.438,69.783,-80.451,69.745,-80.466,69.737,-80.653,69.751,-80.727,69.74,-80.778,69.71,-80.795,69.689,-80.778,69.677,-80.448,69.65,-80.398,69.633,-80.33,69.587,-80.3,69.583,-80.269,69.6,-80.244,69.593,-80.227,69.562,-80.161,69.536,-80.047,69.514,-79.978,69.51,-79.954,69.523,-79.971,69.556,-80.048,69.634,-79.882,69.609,-79.553,69.631]},{"b":[-78.848,69.48,-77.969,69.74],"l":[-78.579,69.639,-78.789,69.523,-78.848,69.483,-78.795,69.48,-78.662,69.503,-78.552,69.492,-78.47,69.503,-78.307,69.552,-78.04,69.608,-77.969,69.639,-77.978,69.665,-78.029,69.715,-78.145,69.739,-78.201,69.74,-78.262,69.717,-78.267,69.687,-78.296,69.667,-78.344,69.675,-78.402,69.651,-78.579,69.639]},{"b":[-77.966,63.946,-77.564,64.037],"l":[-77.714,63.946,-77.642,63.992,-77.564,64.022,-77.569,64.03,-77.617,64.037,-77.711,64.036,-77.931,64.015,-77.966,63.993,-77.958,63.976,-77.929,63.962,-77.714,63.946]},{"b":[-83.238,66.199,-82.931,66.336],"l":[-83.06,66.199,-83.011,66.208,-82.931,66.257,-82.948,66.272,-83.024,66.271,-83.123,66.283,-83.222,66.336,-83.238,66.332,-83.233,66.303,-83.214,66.277,-83.148,66.234,-83.06,66.199]},{"b":[-79.406,68.839,-78.229,69.39],"l":[-78.853,68.916,-78.779,68.95,-78.705,69.014,-78.596,69.079,-78.56,69.106,-78.552,69.129,-78.533,69.146,-78.439,69.199,-78.287,69.263,-78.229,69.304,-78.234,69.315,-78.272,69.361,-78.3,69.379,-78.333,69.386,-78.458,69.39,-78.597,69.371,-78.65,69.351,-78.689,69.325,-78.689,69.3,-78.65,69.275,-78.662,69.262,-78.772,69.252,-78.804,69.235,-78.9,69.135,-78.93,69.123,-79.145,69.087,-79.243,69.049,-79.305,68.992,-79.355,68.956,-79.391,68.94,-79.406,68.923,-79.39,68.89,-79.361,68.858,-79.28,68.839,-79.211,68.845,-79.054,68.883,-78.853,68.916]},{"b":[-77.379,69.132,-76.669,69.44],"l":[-76.994,69.412,-77.109,69.437,-77.188,69.44,-77.319,69.416,-77.341,69.404,-77.352,69.379,-77.358,69.312,-77.379,69.274,-77.322,69.194,-77.276,69.162,-77.215,69.138,-77.122,69.132,-76.995,69.144,-76.911,69.175,-76.869,69.225,-76.81,69.267,-76.687,69.328,-76.67,69.349,-76.669,69.366,-76.684,69.38,-76.746,69.404,-76.994,69.412]},{"b":[-65.235,61.733,-64.846,61.943],"l":[-65.166,61.798,-64.928,61.733,-64.897,61.733,-64.847,61.762,-64.846,61.78,-64.865,61.798,-64.924,61.824,-64.947,61.843,-64.961,61.872,-64.981,61.881,-65.008,61.87,-65.031,61.879,-65.068,61.926,-65.126,61.942,-65.174,61.943,-65.211,61.928,-65.235,61.898,-65.23,61.864,-65.166,61.798]},{"b":[-87.323,69.976,-86.515,70.147],"l":[-86.984,70.011,-86.855,69.986,-86.734,69.976,-86.558,69.995,-86.515,70.017,-86.531,70.048,-86.563,70.077,-86.613,70.106,-86.691,70.115,-86.799,70.105,-86.913,70.113,-87.107,70.147,-87.168,70.127,-87.323,70.102,-87.323,70.08,-87.264,70.044,-87.191,70.019,-87.044,70,-86.984,70.011]},{"b":[-84.467,65.63,-83.234,66.131],"l":[-84.37,66.012,-84.271,65.991,-84.223,65.97,-84.193,65.942,-84.143,65.916,-84.13,65.877,-84.118,65.772,-84.008,65.752,-83.939,65.758,-83.814,65.788,-83.787,65.77,-83.702,65.756,-83.798,65.71,-83.809,65.678,-83.788,65.669,-83.65,65.658,-83.631,65.662,-83.644,65.679,-83.636,65.692,-83.607,65.701,-83.583,65.699,-83.537,65.669,-83.495,65.656,-83.381,65.63,-83.332,65.631,-83.264,65.668,-83.234,65.697,-83.234,65.715,-83.263,65.723,-83.469,65.735,-83.598,65.757,-83.726,65.797,-83.765,65.831,-83.715,65.861,-83.694,65.89,-83.701,65.92,-83.787,65.966,-83.95,66.027,-84.122,66.078,-84.407,66.131,-84.456,66.106,-84.467,66.088,-84.451,66.064,-84.37,66.012]},{"b":[-86.96,67.735,-86.382,68.306],"l":[-86.702,68.306,-86.834,68.23,-86.885,68.191,-86.899,68.163,-86.949,68.119,-86.96,68.1,-86.938,68.068,-86.847,68.01,-86.895,67.938,-86.908,67.902,-86.908,67.867,-86.893,67.837,-86.861,67.81,-86.706,67.75,-86.638,67.735,-86.596,67.736,-86.546,67.752,-86.49,67.784,-86.447,67.817,-86.396,67.889,-86.382,67.927,-86.39,67.989,-86.42,68.074,-86.43,68.139,-86.421,68.183,-86.452,68.225,-86.57,68.288,-86.702,68.306]},{"b":[-85.176,65.564,-84.602,66.025],"l":[-85.15,66.015,-85.176,65.972,-85.174,65.944,-85.144,65.885,-85.136,65.821,-85.096,65.756,-85.072,65.737,-84.931,65.689,-84.869,65.651,-84.83,65.599,-84.783,65.57,-84.727,65.564,-84.675,65.575,-84.626,65.604,-84.602,65.631,-84.603,65.657,-84.692,65.793,-84.757,65.859,-84.87,65.942,-84.889,65.972,-84.92,65.997,-84.939,66.009,-85.031,66.025,-85.15,66.015]},{"b":[-93.197,61.842,-92.993,61.935],"l":[-93.076,61.935,-93.197,61.919,-93.177,61.893,-93.085,61.842,-93.044,61.844,-93,61.867,-92.993,61.89,-93.076,61.935]},{"b":[-104.406,75.763,-102.008,76.311],"l":[-102.58,75.78,-102.511,75.808,-102.423,75.869,-102.318,75.895,-102.047,75.928,-102.008,75.939,-102.018,75.954,-102.227,76.015,-102.426,76.086,-102.49,76.095,-102.536,76.196,-102.584,76.282,-102.728,76.307,-103.098,76.311,-103.571,76.258,-104.012,76.223,-104.351,76.182,-104.406,76.108,-104.242,76.047,-103.985,76.047,-103.801,76.037,-103.985,75.933,-103.77,75.892,-103.202,75.958,-103.042,75.919,-103.245,75.823,-103.315,75.764,-102.944,75.763,-102.58,75.78]},{"b":[-101.613,76.575,-100.269,76.752],"l":[-100.269,76.734,-100.467,76.75,-100.622,76.752,-100.886,76.743,-100.962,76.734,-101.165,76.665,-101.509,76.628,-101.613,76.605,-101.605,76.587,-101.485,76.575,-101.226,76.579,-100.747,76.649,-100.269,76.734]},{"b":[-104.603,76.326,-103.034,76.666],"l":[-103.472,76.329,-103.311,76.348,-103.2,76.371,-103.083,76.405,-103.034,76.431,-103.051,76.45,-103.19,76.477,-103.585,76.539,-103.613,76.563,-103.723,76.601,-103.821,76.598,-103.973,76.578,-104.023,76.583,-103.969,76.614,-103.959,76.639,-103.992,76.657,-104.075,76.666,-104.205,76.666,-104.5,76.63,-104.586,76.606,-104.603,76.583,-104.577,76.54,-104.506,76.479,-104.408,76.365,-104.358,76.335,-104.271,76.326,-103.472,76.329]},{"b":[-103.274,78.126,-102.788,78.271],"l":[-103.11,78.246,-103.26,78.183,-103.274,78.166,-103.271,78.151,-103.252,78.138,-103.118,78.126,-103.003,78.146,-102.788,78.218,-102.826,78.25,-102.892,78.271,-102.973,78.267,-103.11,78.246]},{"b":[-102.475,77.687,-101.002,77.9],"l":[-102.08,77.692,-101.831,77.687,-101.694,77.697,-101.585,77.718,-101.398,77.729,-101.002,77.735,-101.02,77.762,-101.046,77.778,-101.128,77.813,-101.193,77.83,-101.322,77.854,-101.639,77.892,-101.918,77.9,-102.263,77.889,-102.448,77.881,-102.472,77.873,-102.475,77.837,-102.458,77.77,-102.378,77.728,-102.08,77.692]},{"b":[-90.562,76.488,-89.694,76.837],"l":[-89.949,76.836,-90.136,76.837,-90.41,76.81,-90.525,76.788,-90.562,76.754,-90.556,76.735,-90.441,76.663,-90.294,76.579,-90.165,76.524,-90.054,76.495,-89.974,76.488,-89.924,76.501,-89.773,76.494,-89.726,76.507,-89.726,76.521,-89.773,76.534,-89.805,76.561,-89.822,76.602,-89.822,76.631,-89.788,76.66,-89.709,76.701,-89.694,76.72,-89.695,76.741,-89.725,76.763,-89.775,76.782,-89.949,76.836]},{"b":[-97.021,75.369,-95.96,75.655],"l":[-96.428,75.606,-96.523,75.584,-96.856,75.538,-96.983,75.51,-97.021,75.468,-96.97,75.413,-96.915,75.38,-96.857,75.369,-96.723,75.381,-96.679,75.394,-96.622,75.431,-96.462,75.494,-96.344,75.506,-96.237,75.475,-96.156,75.477,-96.079,75.51,-95.969,75.542,-95.96,75.554,-96.04,75.586,-96.145,75.614,-96.368,75.655,-96.397,75.647,-96.417,75.631,-96.428,75.606]},{"b":[-95.851,74.5,-95.274,74.637],"l":[-95.51,74.637,-95.66,74.637,-95.746,74.616,-95.774,74.599,-95.851,74.582,-95.834,74.569,-95.777,74.551,-95.442,74.506,-95.352,74.5,-95.307,74.505,-95.274,74.519,-95.278,74.54,-95.353,74.586,-95.51,74.637]},{"b":[-115.029,77.721,-113.618,78.078],"l":[-113.898,77.916,-114.087,77.978,-114.181,77.998,-114.28,78.004,-114.303,78.033,-114.297,78.063,-114.33,78.078,-114.607,78.04,-114.726,78.016,-114.79,77.993,-114.89,77.977,-115.029,77.968,-114.98,77.915,-114.608,77.769,-114.287,77.721,-114.106,77.721,-113.832,77.755,-113.726,77.776,-113.619,77.813,-113.618,77.832,-113.697,77.869,-113.721,77.89,-113.768,77.904,-113.898,77.916]},{"b":[-121.241,75.741,-120.879,75.938],"l":[-121.042,75.903,-121.018,75.884,-121.015,75.868,-121.026,75.848,-121.221,75.777,-121.241,75.752,-121.154,75.741,-121.076,75.745,-121.007,75.766,-120.955,75.789,-120.921,75.814,-120.897,75.845,-120.879,75.907,-120.888,75.928,-120.914,75.938,-120.993,75.927,-121.042,75.903]},{"b":[-114.835,76.711,-113.488,76.895],"l":[-113.712,76.711,-113.561,76.743,-113.488,76.783,-113.517,76.825,-113.585,76.847,-113.708,76.873,-113.892,76.895,-114.42,76.875,-114.647,76.851,-114.835,76.795,-114.808,76.774,-114.751,76.759,-113.712,76.711]},{"b":[-104.887,75.031,-103.642,75.43],"l":[-103.746,75.252,-103.758,75.289,-103.804,75.346,-103.851,75.371,-103.917,75.392,-104.075,75.425,-104.346,75.43,-104.474,75.413,-104.649,75.35,-104.69,75.321,-104.801,75.211,-104.848,75.173,-104.882,75.16,-104.887,75.148,-104.828,75.12,-104.634,75.061,-104.309,75.031,-104.12,75.036,-103.814,75.08,-103.71,75.115,-103.664,75.139,-103.642,75.163,-103.644,75.187,-103.667,75.211,-103.746,75.252]},{"b":[-59.362,46.628,-52.654,51.596],"l":[-52.672,47.622,-52.703,47.693,-52.711,47.745,-52.745,47.769,-52.782,47.769,-52.817,47.728,-52.873,47.619,-52.945,47.553,-53.057,47.483,-53.122,47.455,-53.158,47.488,-53.17,47.512,-53.176,47.653,-53.154,47.735,-53.111,47.812,-52.998,47.976,-52.955,48.029,-52.872,48.094,-52.866,48.113,-52.883,48.131,-52.921,48.147,-53.085,48.069,-53.283,47.998,-53.504,47.744,-53.604,47.662,-53.672,47.648,-53.765,47.65,-53.805,47.682,-53.838,47.727,-53.864,47.787,-53.862,47.799,-53.695,47.921,-53.658,47.969,-53.638,48.015,-53.653,48.026,-53.794,48.01,-53.87,48.02,-53.758,48.042,-53.71,48.057,-53.704,48.068,-53.569,48.088,-53.542,48.108,-53.56,48.174,-53.61,48.208,-53.531,48.232,-53.406,48.294,-53.334,48.356,-53.301,48.368,-53.225,48.364,-53.182,48.374,-53.136,48.402,-53.06,48.48,-53.037,48.516,-53.021,48.572,-53.028,48.635,-53.043,48.657,-53.057,48.659,-53.127,48.633,-53.22,48.578,-53.275,48.563,-53.361,48.573,-53.411,48.562,-53.552,48.482,-53.644,48.511,-53.739,48.496,-53.799,48.449,-53.853,48.449,-53.937,48.437,-54.104,48.388,-54.114,48.394,-54.068,48.419,-53.886,48.485,-53.795,48.526,-53.775,48.576,-53.706,48.656,-53.698,48.68,-53.784,48.695,-53.886,48.685,-53.966,48.707,-53.97,48.725,-53.962,48.739,-53.887,48.768,-53.848,48.797,-53.853,48.811,-53.951,48.807,-54.1,48.785,-54.161,48.788,-53.903,48.889,-53.845,48.925,-53.825,48.951,-53.809,48.993,-53.758,49.035,-53.671,49.078,-53.573,49.141,-53.56,49.192,-53.57,49.264,-53.619,49.322,-53.755,49.385,-53.862,49.426,-53.958,49.442,-54.271,49.419,-54.317,49.424,-54.356,49.415,-54.389,49.392,-54.448,49.329,-54.463,49.342,-54.465,49.401,-54.481,49.469,-54.469,49.53,-54.502,49.527,-54.579,49.491,-54.651,49.445,-54.718,49.389,-54.782,49.355,-54.844,49.345,-54.911,49.316,-54.983,49.268,-55.016,49.26,-55.01,49.293,-55.026,49.305,-55.063,49.297,-55.176,49.244,-55.245,49.2,-55.254,49.18,-55.247,49.139,-55.252,49.121,-55.335,49.078,-55.353,49.079,-55.332,49.126,-55.342,49.168,-55.259,49.267,-55.225,49.335,-55.2,49.408,-55.207,49.482,-55.23,49.508,-55.266,49.524,-55.283,49.514,-55.28,49.413,-55.29,49.392,-55.344,49.373,-55.355,49.381,-55.354,49.438,-55.379,49.473,-55.376,49.49,-55.49,49.463,-55.678,49.435,-55.815,49.515,-56.041,49.457,-56.087,49.452,-55.892,49.58,-55.882,49.646,-55.87,49.67,-55.902,49.681,-55.979,49.678,-56.052,49.658,-56.121,49.622,-56.14,49.619,-56.04,49.705,-55.718,49.829,-55.584,49.892,-55.527,49.937,-55.503,49.983,-55.53,49.997,-55.674,49.967,-55.765,49.96,-55.873,50.013,-55.927,50.018,-56.075,49.983,-56.161,49.94,-56.164,49.957,-56.127,50.015,-56.122,50.063,-56.148,50.1,-56.179,50.115,-56.247,50.09,-56.322,50.014,-56.376,49.934,-56.428,49.897,-56.501,49.87,-56.611,49.788,-56.757,49.652,-56.822,49.613,-56.807,49.673,-56.809,49.71,-56.829,49.725,-56.849,49.765,-56.839,49.788,-56.79,49.834,-56.754,49.883,-56.747,49.908,-56.75,49.967,-56.732,50.008,-56.694,50.06,-56.539,50.207,-56.484,50.271,-56.455,50.35,-56.454,50.38,-56.382,50.417,-56.196,50.585,-56.136,50.651,-56.121,50.734,-56.107,50.759,-56.078,50.781,-55.962,50.838,-55.871,50.907,-55.8,51.033,-55.785,51.087,-55.785,51.131,-55.796,51.166,-55.815,51.191,-55.841,51.205,-55.874,51.208,-55.961,51.191,-56,51.199,-56.031,51.227,-56.044,51.262,-56.031,51.328,-55.941,51.343,-55.731,51.359,-55.631,51.373,-55.583,51.389,-55.532,51.437,-55.459,51.537,-55.453,51.562,-55.496,51.59,-55.522,51.596,-55.666,51.579,-55.701,51.559,-55.66,51.511,-55.69,51.471,-55.866,51.508,-55.902,51.564,-56.026,51.568,-56.207,51.489,-56.518,51.399,-56.619,51.362,-56.682,51.333,-56.75,51.275,-56.805,51.144,-56.825,51.126,-56.976,51.028,-57.036,51.011,-57.037,50.996,-57.013,50.968,-57.006,50.94,-57.053,50.857,-57.132,50.787,-57.242,50.745,-57.275,50.725,-57.298,50.699,-57.294,50.673,-57.264,50.649,-57.18,50.615,-57.237,50.605,-57.331,50.605,-57.36,50.584,-57.433,50.506,-57.466,50.464,-57.608,50.199,-57.712,50.025,-57.926,49.701,-57.929,49.668,-57.897,49.6,-57.799,49.509,-57.791,49.49,-57.856,49.474,-57.961,49.532,-58.016,49.542,-58.107,49.5,-58.183,49.435,-58.213,49.387,-58.219,49.305,-58.191,49.259,-58.097,49.23,-57.98,49.23,-57.991,49.209,-58.049,49.18,-58.099,49.077,-58.082,49.045,-58.041,49.01,-57.991,48.988,-58.006,48.981,-58.05,48.988,-58.186,49.062,-58.319,49.081,-58.359,49.097,-58.404,49.084,-58.494,49.003,-58.546,48.897,-58.642,48.749,-58.687,48.622,-58.716,48.598,-58.843,48.605,-58.877,48.623,-58.906,48.65,-58.887,48.692,-58.819,48.747,-58.842,48.746,-59.063,48.628,-59.168,48.558,-59.167,48.522,-58.944,48.522,-58.723,48.541,-58.606,48.533,-58.492,48.513,-58.33,48.522,-58.336,48.514,-58.503,48.442,-58.605,48.411,-58.711,48.325,-58.961,48.159,-59.272,47.996,-59.341,47.934,-59.362,47.889,-59.362,47.866,-59.321,47.737,-59.26,47.634,-59.219,47.603,-59.117,47.571,-58.941,47.58,-58.613,47.626,-58.509,47.653,-58.428,47.683,-58.337,47.731,-58.327,47.72,-58.333,47.677,-58.239,47.669,-57.926,47.675,-57.884,47.66,-57.66,47.625,-57.473,47.631,-56.952,47.574,-56.774,47.565,-56.722,47.592,-56.46,47.617,-56.326,47.654,-56.263,47.658,-56.221,47.671,-56.151,47.775,-56.121,47.789,-56.09,47.772,-56.02,47.764,-55.918,47.792,-55.858,47.819,-55.844,47.788,-55.867,47.592,-56.084,47.525,-56.127,47.503,-56.081,47.5,-55.862,47.53,-55.811,47.516,-55.775,47.498,-55.576,47.465,-55.499,47.475,-55.461,47.485,-55.435,47.501,-55.413,47.55,-55.391,47.643,-55.366,47.661,-55.197,47.65,-55.075,47.658,-55.035,47.634,-54.946,47.621,-54.891,47.629,-54.785,47.665,-54.795,47.64,-54.87,47.571,-54.976,47.516,-55.191,47.449,-55.361,47.259,-55.401,47.221,-55.492,47.161,-55.61,47.12,-55.772,47.092,-55.838,47.072,-55.919,47.017,-55.954,46.973,-55.958,46.956,-55.95,46.928,-55.881,46.887,-55.845,46.874,-55.789,46.867,-55.652,46.881,-55.531,46.914,-55.479,46.917,-55.401,46.899,-55.316,46.906,-55.255,46.942,-55.14,47.046,-55.099,47.104,-55.09,47.174,-54.857,47.385,-54.802,47.399,-54.745,47.395,-54.651,47.408,-54.575,47.458,-54.474,47.547,-54.463,47.536,-54.542,47.425,-54.563,47.375,-54.488,47.404,-54.456,47.428,-54.434,47.462,-54.405,47.556,-54.234,47.772,-54.218,47.847,-54.192,47.86,-54.047,47.806,-53.989,47.756,-53.94,47.645,-53.901,47.509,-53.878,47.464,-53.85,47.44,-53.869,47.387,-53.971,47.262,-54.093,47.086,-54.155,46.967,-54.173,46.917,-54.174,46.88,-54.133,46.839,-54.102,46.825,-54.076,46.82,-54.01,46.84,-53.86,46.939,-53.774,47.012,-53.695,47.093,-53.636,47.138,-53.597,47.146,-53.578,47.133,-53.58,47.099,-53.612,47.01,-53.581,46.957,-53.595,46.888,-53.616,46.68,-53.59,46.639,-53.568,46.628,-53.536,46.633,-53.382,46.711,-53.323,46.718,-53.291,46.717,-53.255,46.698,-53.214,46.66,-53.167,46.646,-53.115,46.656,-53.07,46.681,-53.032,46.723,-52.962,46.819,-52.889,46.974,-52.882,47.011,-52.888,47.046,-52.912,47.103,-52.684,47.426,-52.669,47.47,-52.654,47.549,-52.672,47.622]},{"b":[-55.634,50.709,-55.469,50.801],"l":[-55.604,50.781,-55.634,50.74,-55.629,50.721,-55.601,50.709,-55.57,50.709,-55.536,50.72,-55.504,50.742,-55.473,50.776,-55.469,50.796,-55.527,50.801,-55.604,50.781]},{"b":[-54.864,49.496,-54.538,49.631],"l":[-54.819,49.514,-54.787,49.496,-54.744,49.508,-54.709,49.531,-54.554,49.589,-54.538,49.62,-54.559,49.631,-54.619,49.622,-54.733,49.562,-54.764,49.562,-54.783,49.572,-54.789,49.591,-54.813,49.599,-54.855,49.597,-54.864,49.576,-54.819,49.514]},{"b":[-55.42,51.889,-55.274,51.995],"l":[-55.274,51.995,-55.346,51.983,-55.4,51.938,-55.42,51.9,-55.409,51.889,-55.361,51.89,-55.294,51.93,-55.274,51.995]},{"b":[-54.289,49.587,-53.981,49.751],"l":[-53.981,49.662,-54.02,49.679,-54.094,49.744,-54.138,49.751,-54.199,49.689,-54.259,49.719,-54.278,49.711,-54.289,49.661,-54.286,49.595,-54.269,49.587,-54.238,49.592,-53.981,49.662]},{"b":[-56.387,46.795,-56.255,47.099],"l":[-56.315,46.954,-56.278,47.035,-56.287,47.071,-56.365,47.099,-56.378,47.09,-56.387,47.068,-56.334,46.936,-56.333,46.916,-56.377,46.848,-56.385,46.819,-56.354,46.795,-56.267,46.838,-56.255,46.861,-56.29,46.9,-56.315,46.954]},{"b":[-54.326,47.407,-54.128,47.647],"l":[-54.168,47.607,-54.215,47.585,-54.226,47.566,-54.227,47.54,-54.259,47.498,-54.32,47.439,-54.326,47.408,-54.276,47.407,-54.227,47.441,-54.148,47.573,-54.128,47.647,-54.168,47.607]},{"b":[-100.625,68.724,-100.178,69.035],"l":[-100.289,68.958,-100.33,68.998,-100.414,69.028,-100.52,69.035,-100.565,69.027,-100.601,69.009,-100.612,68.99,-100.598,68.969,-100.6,68.941,-100.625,68.865,-100.625,68.816,-100.616,68.783,-100.597,68.766,-100.573,68.766,-100.521,68.791,-100.497,68.792,-100.481,68.786,-100.443,68.748,-100.397,68.724,-100.366,68.729,-100.288,68.766,-100.249,68.775,-100.217,68.807,-100.178,68.904,-100.207,68.926,-100.289,68.958]},{"b":[-100.247,68.954,-99.995,69.129],"l":[-100.153,69.129,-100.187,69.114,-100.237,69.071,-100.247,69.053,-100.242,69.04,-100.196,68.991,-100.141,68.97,-100.018,68.954,-99.995,69.014,-100.006,69.047,-100.035,69.087,-100.073,69.111,-100.153,69.129]},{"b":[-100.678,70.488,-100.276,70.67],"l":[-100.321,70.578,-100.276,70.595,-100.434,70.649,-100.538,70.669,-100.635,70.67,-100.678,70.646,-100.667,70.596,-100.648,70.563,-100.621,70.547,-100.537,70.525,-100.321,70.488,-100.308,70.496,-100.306,70.508,-100.323,70.542,-100.321,70.578]},{"b":[-95.995,69.336,-95.381,69.626],"l":[-95.986,69.392,-95.956,69.367,-95.893,69.352,-95.858,69.382,-95.812,69.447,-95.798,69.5,-95.818,69.541,-95.806,69.56,-95.764,69.56,-95.704,69.538,-95.683,69.5,-95.666,69.439,-95.67,69.402,-95.696,69.39,-95.73,69.348,-95.684,69.336,-95.579,69.336,-95.496,69.35,-95.437,69.378,-95.399,69.42,-95.382,69.474,-95.381,69.507,-95.514,69.574,-95.602,69.602,-95.707,69.624,-95.798,69.626,-95.876,69.606,-95.936,69.567,-95.979,69.509,-95.995,69.47,-95.978,69.433,-95.986,69.392]},{"b":[-101.356,69.388,-101.001,69.576],"l":[-101.098,69.541,-101.245,69.574,-101.313,69.576,-101.351,69.559,-101.356,69.54,-101.328,69.517,-101.23,69.493,-101.207,69.48,-101.218,69.463,-101.29,69.441,-101.268,69.431,-101.262,69.418,-101.269,69.391,-101.254,69.388,-101.172,69.397,-101.127,69.415,-101.087,69.443,-101.049,69.457,-101.001,69.462,-101.031,69.495,-101.098,69.541]},{"b":[-102.308,68.585,-101.722,68.825],"l":[-102.013,68.825,-102.074,68.774,-102.153,68.74,-102.271,68.708,-102.308,68.682,-102.266,68.664,-101.945,68.603,-101.887,68.585,-101.846,68.586,-101.794,68.637,-101.732,68.652,-101.722,68.724,-101.733,68.753,-101.759,68.775,-101.828,68.799,-102.013,68.825]},{"b":[-105.051,68.402,-104.44,68.582],"l":[-104.602,68.562,-104.7,68.577,-104.907,68.582,-104.994,68.574,-105.051,68.559,-105.042,68.532,-104.965,68.492,-104.851,68.454,-104.699,68.418,-104.596,68.402,-104.541,68.406,-104.457,68.431,-104.44,68.45,-104.445,68.471,-104.472,68.504,-104.602,68.562]},{"b":[-108.152,67.318,-107.891,67.665],"l":[-107.97,67.326,-107.95,67.318,-107.9,67.402,-107.891,67.437,-107.905,67.467,-107.932,67.476,-107.989,67.514,-107.975,67.549,-107.991,67.622,-108.049,67.665,-108.128,67.629,-108.121,67.568,-108.151,67.525,-108.152,67.429,-108.073,67.385,-108.004,67.366,-107.97,67.326]},{"b":[-109.184,67.879,-108.886,67.982],"l":[-108.971,67.98,-109.054,67.972,-109.166,67.982,-109.184,67.975,-109.162,67.952,-109.096,67.924,-108.92,67.879,-108.894,67.884,-108.886,67.899,-108.91,67.939,-108.971,67.98]},{"b":[-108.093,66.858,-107.806,67.005],"l":[-107.944,66.858,-107.895,66.872,-107.833,66.921,-107.806,66.999,-107.966,66.997,-108.093,67.005,-108.06,66.947,-107.965,66.885,-107.944,66.858]},{"b":[-109.498,67.988,-109.323,68.098],"l":[-109.469,68.098,-109.498,68.047,-109.361,67.988,-109.323,67.991,-109.324,68.013,-109.342,68.046,-109.469,68.098]},{"b":[-61.196,56.015,-60.955,56.141],"l":[-61.049,56.129,-61.087,56.141,-61.158,56.118,-61.188,56.089,-61.196,56.064,-61.191,56.048,-61.137,56.033,-60.983,56.015,-60.994,56.039,-60.955,56.08,-60.966,56.099,-61.049,56.129]},{"b":[-62.011,57.416,-61.637,57.579],"l":[-61.937,57.554,-61.983,57.567,-62.007,57.558,-62.011,57.548,-61.975,57.495,-61.795,57.422,-61.637,57.416,-61.66,57.525,-61.744,57.555,-61.848,57.579,-61.893,57.573,-61.937,57.554]},{"b":[-69.353,58.961,-69.155,59.146],"l":[-69.195,59.146,-69.303,59.145,-69.33,59.121,-69.312,59.075,-69.316,59.029,-69.353,58.961,-69.331,58.962,-69.302,58.977,-69.221,58.968,-69.16,59.04,-69.155,59.064,-69.181,59.073,-69.194,59.093,-69.195,59.146]},{"b":[22.077,60.166,22.416,60.371],"l":[22.306,60.229,22.313,60.27,22.416,60.303,22.355,60.356,22.302,60.348,22.175,60.371,22.126,60.356,22.108,60.315,22.077,60.286,22.141,60.265,22.188,60.237,22.209,60.197,22.258,60.166,22.361,60.166,22.346,60.203,22.306,60.229]},{"b":[21.215,60.48,21.451,60.638],"l":[21.215,60.604,21.244,60.526,21.3,60.48,21.369,60.488,21.437,60.483,21.451,60.53,21.301,60.596,21.268,60.638,21.225,60.621,21.215,60.604]},{"b":[21.695,60.106,21.864,60.202],"l":[21.764,60.199,21.705,60.172,21.695,60.114,21.733,60.106,21.833,60.141,21.864,60.202,21.764,60.199]},{"b":[-64.836,60.298,-64.407,60.515],"l":[-64.5,60.43,-64.533,60.441,-64.646,60.515,-64.783,60.51,-64.836,60.501,-64.834,60.448,-64.809,60.41,-64.738,60.376,-64.558,60.323,-64.442,60.298,-64.407,60.367,-64.5,60.43]},{"b":[126.008,34.707,126.172,34.915],"l":[126.169,34.83,126.078,34.915,126.008,34.867,126.052,34.838,126.071,34.783,126.115,34.714,126.159,34.707,126.172,34.731,126.169,34.83]},{"b":[72.35,-7.435,72.499,-7.22],"l":[72.494,-7.262,72.446,-7.22,72.436,-7.23,72.465,-7.278,72.474,-7.31,72.462,-7.338,72.467,-7.368,72.447,-7.396,72.427,-7.3,72.373,-7.263,72.35,-7.263,72.408,-7.334,72.429,-7.435,72.469,-7.417,72.492,-7.377,72.499,-7.295,72.494,-7.262]},{"b":[-139.632,-8.948,-139.508,-8.86],"l":[-139.535,-8.875,-139.584,-8.86,-139.612,-8.872,-139.632,-8.899,-139.621,-8.948,-139.556,-8.94,-139.51,-8.916,-139.508,-8.897,-139.535,-8.875]},{"b":[157.454,-8.794,157.645,-8.697],"l":[157.623,-8.735,157.579,-8.704,157.526,-8.697,157.454,-8.706,157.458,-8.73,157.586,-8.783,157.643,-8.794,157.645,-8.759,157.623,-8.735]},{"b":[20.398,60.012,20.611,60.075],"l":[20.569,60.07,20.49,60.075,20.43,60.062,20.398,60.041,20.411,60.03,20.488,60.033,20.522,60.012,20.603,60.017,20.611,60.041,20.569,60.07]},{"b":[21.486,60.098,21.648,60.172],"l":[21.634,60.169,21.568,60.172,21.507,60.148,21.486,60.127,21.541,60.098,21.628,60.108,21.648,60.141,21.634,60.169]},{"b":[24.577,64.958,24.998,65.086],"l":[24.786,65.086,24.651,65.074,24.577,65.043,24.579,64.979,24.699,64.958,24.848,64.991,24.892,65.026,24.998,65.039,24.971,65.055,24.786,65.086]},{"b":[29.744,69.768,30.055,69.906],"l":[29.836,69.906,29.786,69.829,29.744,69.792,29.766,69.768,29.956,69.797,30.055,69.838,29.993,69.873,29.914,69.902,29.836,69.906]},{"b":[4.799,61.072,4.973,61.199],"l":[4.862,61.194,4.824,61.178,4.799,61.083,4.87,61.072,4.959,61.085,4.973,61.148,4.915,61.199,4.862,61.194]},{"b":[7.804,63.337,8.141,63.471],"l":[8.103,63.338,8.141,63.366,8.136,63.431,8.074,63.471,7.938,63.45,7.804,63.414,7.815,63.385,7.888,63.352,8.005,63.337,8.103,63.338]},{"b":[8.287,63.665,8.815,63.805],"l":[8.471,63.667,8.787,63.703,8.815,63.726,8.809,63.771,8.765,63.805,8.733,63.801,8.709,63.774,8.451,63.732,8.287,63.687,8.356,63.665,8.471,63.667]},{"b":[12.429,65.899,12.779,66.011],"l":[12.476,65.977,12.43,65.94,12.429,65.899,12.51,65.902,12.719,65.964,12.779,65.992,12.747,66.011,12.642,66.009,12.549,66.002,12.476,65.977]},{"b":[12.327,66.037,12.623,66.211],"l":[12.623,66.122,12.621,66.178,12.527,66.211,12.461,66.185,12.446,66.151,12.418,66.123,12.343,66.081,12.327,66.037,12.42,66.043,12.576,66.072,12.623,66.122]},{"b":[19.599,70.068,20.088,70.274],"l":[19.869,70.212,19.818,70.205,19.767,70.217,19.684,70.274,19.599,70.266,19.613,70.219,19.711,70.165,19.747,70.11,19.781,70.077,19.897,70.068,20.006,70.076,20.088,70.102,20.084,70.129,19.994,70.149,19.91,70.202,19.869,70.212]},{"b":[21.995,70.509,23.441,70.843],"l":[22.605,70.533,22.656,70.559,22.829,70.542,22.885,70.554,22.929,70.574,23.068,70.594,23.305,70.722,23.387,70.754,23.421,70.784,23.441,70.816,23.396,70.843,23.28,70.813,23.205,70.815,22.964,70.711,22.858,70.728,22.571,70.697,22.421,70.703,22.35,70.658,22.233,70.667,22.17,70.656,21.995,70.657,22.056,70.613,22.169,70.562,22.359,70.515,22.432,70.509,22.558,70.516,22.605,70.533]},{"b":[20.405,70.057,20.819,70.231],"l":[20.819,70.205,20.786,70.22,20.655,70.231,20.493,70.203,20.412,70.155,20.405,70.119,20.464,70.077,20.535,70.081,20.598,70.071,20.643,70.057,20.725,70.067,20.779,70.09,20.819,70.205]},{"b":[25.315,70.954,26.147,71.142],"l":[26,70.975,26.134,70.996,26.147,71.04,26.078,71.033,25.945,71.105,25.854,71.104,25.586,71.142,25.423,71.097,25.315,71.053,25.315,71.034,25.482,71.02,25.582,70.961,25.76,70.954,25.791,70.963,26,70.975]},{"b":[22.918,70.283,23.641,70.617],"l":[23.547,70.617,23.248,70.505,23.158,70.516,23.022,70.487,22.941,70.445,22.918,70.417,22.918,70.385,23.006,70.353,23.091,70.378,23.108,70.359,23.1,70.296,23.159,70.283,23.271,70.296,23.345,70.315,23.333,70.335,23.548,70.408,23.641,70.464,23.634,70.503,23.615,70.549,23.579,70.594,23.547,70.617]},{"b":[23.663,70.527,24.078,70.747],"l":[23.852,70.714,23.837,70.729,23.778,70.747,23.689,70.723,23.663,70.675,23.67,70.597,23.717,70.562,23.827,70.527,24.018,70.567,24.078,70.651,23.956,70.7,23.852,70.714]},{"b":[13.2,67.995,14.119,68.276],"l":[13.932,68.248,13.873,68.265,13.784,68.276,13.688,68.273,13.538,68.249,13.429,68.163,13.368,68.167,13.3,68.16,13.256,68.121,13.2,68.087,13.229,67.995,13.352,68.01,13.392,68.021,13.404,68.061,13.424,68.083,13.495,68.052,13.584,68.094,13.656,68.105,13.778,68.105,13.824,68.121,13.888,68.169,14.029,68.188,14.097,68.219,14.119,68.247,14.088,68.253,13.932,68.248]},{"b":[12.824,67.821,13.123,68.071],"l":[12.958,68.015,12.878,67.918,12.824,67.821,12.972,67.874,13.075,67.935,13.098,67.956,13.098,68.003,13.123,68.049,13.068,68.071,12.958,68.015]},{"b":[16.81,69.014,18.077,69.596],"l":[18.004,69.505,17.927,69.507,17.863,69.543,17.784,69.563,17.677,69.557,17.623,69.539,17.503,69.596,17.488,69.587,17.483,69.57,17.454,69.53,17.356,69.527,17.252,69.504,17.23,69.478,17.373,69.439,17.395,69.417,17.361,69.381,17.083,69.399,17.002,69.362,16.997,69.33,16.974,69.285,16.998,69.191,16.972,69.138,16.843,69.112,16.815,69.095,16.81,69.071,16.96,69.069,17.077,69.047,17.083,69.014,17.161,69.026,17.324,69.13,17.488,69.197,17.568,69.16,17.774,69.172,17.951,69.198,17.921,69.274,17.942,69.329,18.021,69.35,18.077,69.396,18.052,69.458,18.004,69.505]},{"b":[18.062,69.535,19.608,70.247],"l":[19.5,70.048,19.446,70.038,19.422,70.017,19.344,70.012,19.255,70.066,19.249,70.179,19.213,70.247,19.133,70.244,19.06,70.167,19.051,70.135,19.075,70.086,19.051,70.038,18.969,70.043,18.883,70.011,18.824,69.96,18.687,69.891,18.674,69.864,18.698,69.825,18.674,69.782,18.624,69.813,18.584,69.807,18.512,69.769,18.406,69.782,18.349,69.768,18.315,69.715,18.268,69.702,18.232,69.677,18.227,69.636,18.083,69.626,18.062,69.602,18.13,69.558,18.274,69.535,18.41,69.553,18.785,69.579,18.801,69.605,18.807,69.64,18.909,69.707,19.008,69.76,19.131,69.81,19.197,69.8,19.335,69.82,19.442,69.908,19.592,69.97,19.608,70.019,19.5,70.048]},{"b":[14.257,68.178,16.547,69.302],"l":[15.095,68.441,14.586,68.4,14.438,68.342,14.257,68.257,14.258,68.191,14.35,68.178,14.629,68.198,14.927,68.307,15.038,68.283,15.098,68.289,15.188,68.31,15.28,68.374,15.337,68.378,15.341,68.325,15.438,68.313,15.683,68.356,15.764,68.409,15.837,68.409,15.873,68.394,15.913,68.389,15.975,68.402,16.048,68.464,16.194,68.538,16.338,68.568,16.519,68.633,16.547,68.717,16.48,68.803,16.425,68.842,16.329,68.876,16.276,68.868,16.228,68.854,16.151,68.842,16.121,68.799,16.127,68.746,16.069,68.714,16.06,68.681,15.909,68.65,15.772,68.554,15.76,68.561,15.791,68.617,15.928,68.733,15.924,68.819,15.906,68.908,15.834,68.961,15.812,69.024,15.993,69.113,16.115,69.216,16.129,69.274,16.048,69.302,15.965,69.302,15.893,69.278,15.742,69.171,15.65,69.133,15.483,69.043,15.438,68.979,15.444,68.919,15.529,68.912,15.564,68.874,15.489,68.805,15.413,68.616,15.095,68.441]},{"b":[14.373,68.606,15.397,69.008],"l":[15.102,69.008,15.038,69.001,15.038,68.894,14.872,68.914,14.838,68.887,14.849,68.848,14.802,68.791,14.725,68.8,14.69,68.815,14.554,68.819,14.497,68.772,14.373,68.711,14.405,68.663,14.521,68.633,14.612,68.638,14.743,68.677,14.793,68.668,14.804,68.638,14.89,68.611,15.027,68.606,15.222,68.616,15.348,68.672,15.397,68.784,15.337,68.842,15.207,68.943,15.176,68.982,15.128,69.004,15.102,69.008]},{"b":[10.74,64.838,11.246,64.979],"l":[11.133,64.976,11.021,64.979,10.813,64.923,10.74,64.87,10.833,64.843,11.063,64.86,11.179,64.838,11.231,64.866,11.246,64.908,11.133,64.976]},{"b":[4.93,60.189,5.089,60.452],"l":[4.956,60.243,4.997,60.198,5.089,60.189,5.086,60.308,5.05,60.389,4.991,60.452,4.957,60.447,4.93,60.412,4.951,60.341,4.944,60.272,4.956,60.243]},{"b":[11.765,65.596,12.003,65.706],"l":[11.972,65.702,11.875,65.706,11.8,65.684,11.765,65.631,11.778,65.605,11.902,65.596,11.968,65.627,12.003,65.679,11.972,65.702]},{"b":[-79.699,75.826,-78.845,76.146],"l":[-79.178,76.092,-79.382,76.011,-79.551,75.958,-79.699,75.883,-79.639,75.843,-79.545,75.826,-79.356,75.831,-79.124,75.87,-79.052,75.867,-79.063,75.926,-79.057,75.985,-78.946,76.025,-78.845,76.106,-78.926,76.135,-79.009,76.146,-79.178,76.092]},{"b":[-72.495,77.316,-71.433,77.467],"l":[-72.089,77.467,-72.247,77.464,-72.436,77.448,-72.49,77.432,-72.495,77.386,-72.374,77.355,-72.024,77.316,-71.667,77.325,-71.467,77.354,-71.433,77.394,-71.552,77.403,-71.733,77.432,-71.983,77.46,-72.089,77.467]},{"b":[-47.351,82.066,-44.75,82.692],"l":[-46.399,82.692,-46.787,82.666,-47.272,82.657,-47.351,82.599,-47.308,82.533,-46.752,82.348,-46.161,82.278,-45.491,82.172,-45.067,82.066,-44.865,82.084,-44.776,82.242,-44.75,82.401,-44.917,82.481,-45.411,82.578,-46.399,82.692]},{"b":[-19.611,81.814,-18.665,82.123],"l":[-19.315,82.123,-19.495,82.117,-19.611,82.078,-19.594,81.991,-19.369,81.917,-19.031,81.827,-18.768,81.814,-18.665,81.846,-18.813,81.949,-19.067,82.049,-19.315,82.123]},{"b":[-19.138,79.711,-17.401,80.055],"l":[-18.036,79.711,-17.612,79.826,-17.401,79.94,-17.471,80.029,-17.983,80.055,-18.547,80.011,-18.997,79.94,-19.138,79.852,-19.032,79.773,-18.662,79.72,-18.036,79.711]},{"b":[-19.315,77.939,-18.882,78.441],"l":[-19.112,78.424,-19.315,78.344,-19.297,78.185,-19.218,78.044,-19.129,77.939,-18.997,77.974,-18.882,78.115,-18.953,78.212,-18.953,78.353,-18.935,78.424,-19.006,78.441,-19.112,78.424]},{"b":[-19.085,76.016,-18.583,76.704],"l":[-18.662,76.404,-18.583,76.042,-18.697,76.016,-19.085,76.43,-19.085,76.58,-19.059,76.695,-18.882,76.704,-18.733,76.642,-18.662,76.404]},{"b":[-18.891,74.993,-17.392,75.407],"l":[-18.636,75.39,-18.856,75.319,-18.882,75.196,-18.891,75.072,-18.671,75.002,-18.353,75.01,-17.586,74.993,-17.392,75.037,-17.498,75.152,-17.762,75.143,-17.886,75.204,-17.921,75.302,-18.001,75.407,-18.23,75.372,-18.45,75.328,-18.636,75.39]},{"b":[-18.22,77.642,-17.641,77.875],"l":[-17.904,77.863,-18.174,77.714,-18.22,77.668,-18.148,77.642,-17.954,77.642,-17.729,77.706,-17.641,77.782,-17.681,77.859,-17.814,77.875,-17.904,77.863]},{"b":[-72.818,59.815,-11.426,83.6],"l":[-51.21,68.42,-51.293,68.416,-51.456,68.394,-51.478,68.384,-51.475,68.365,-51.632,68.273,-51.804,68.252,-52.199,68.221,-52.379,68.219,-52.698,68.262,-52.747,68.278,-52.78,68.31,-53.173,68.303,-53.29,68.293,-53.383,68.297,-53.337,68.352,-53.213,68.413,-53.039,68.611,-52.894,68.662,-52.605,68.709,-52.303,68.701,-51.781,68.548,-51.623,68.535,-51.479,68.547,-51.133,68.598,-51.07,68.619,-50.946,68.683,-50.801,68.791,-50.808,68.817,-51.03,68.756,-51.149,68.74,-51.249,68.74,-51.156,68.938,-51.12,69.091,-51.085,69.128,-50.792,69.117,-50.393,69.137,-50.297,69.171,-50.299,69.185,-50.459,69.206,-50.537,69.248,-50.671,69.234,-50.851,69.206,-51.077,69.209,-51.058,69.275,-50.892,69.412,-50.875,69.474,-50.811,69.599,-50.804,69.663,-50.72,69.725,-50.459,69.77,-50.349,69.796,-50.343,69.825,-50.5,69.936,-50.46,69.966,-50.337,69.994,-50.292,70.014,-50.323,70.027,-50.436,70.039,-50.61,70.015,-50.802,70.003,-50.973,70.04,-51.106,70.057,-51.19,70.052,-51.419,69.989,-51.499,69.987,-51.598,70.005,-52.255,70.059,-52.336,70.078,-52.571,70.172,-52.765,70.234,-53.023,70.302,-53.358,70.353,-53.769,70.389,-54.014,70.422,-54.136,70.468,-54.343,70.571,-54.501,70.657,-54.531,70.699,-54.438,70.752,-54.344,70.789,-54.166,70.82,-53.859,70.81,-53.694,70.796,-53.513,70.767,-53.376,70.761,-53.091,70.769,-52.802,70.751,-52.63,70.73,-52.405,70.687,-51.784,70.503,-51.524,70.439,-51.412,70.432,-50.947,70.364,-50.872,70.365,-50.682,70.397,-50.663,70.418,-50.728,70.438,-50.933,70.454,-51.173,70.529,-51.323,70.589,-51.34,70.688,-51.32,70.743,-51.283,70.768,-51.257,70.853,-51.396,70.903,-51.494,70.919,-51.753,70.992,-51.774,71.01,-51.65,71.019,-51.528,71.014,-51.267,70.977,-51.13,70.972,-51.03,70.986,-51.019,71.001,-51.178,71.043,-51.377,71.119,-51.792,71.13,-52.061,71.122,-52.234,71.148,-52.417,71.19,-52.535,71.2,-52.775,71.174,-52.897,71.171,-53.008,71.18,-53.117,71.313,-53.088,71.353,-53.002,71.37,-52.937,71.413,-52.892,71.458,-52.749,71.502,-51.967,71.599,-51.77,71.672,-51.779,71.683,-51.912,71.669,-52.082,71.637,-52.196,71.63,-52.656,71.672,-52.728,71.663,-52.915,71.602,-53.168,71.536,-53.284,71.54,-53.44,71.579,-53.465,71.607,-53.476,71.64,-53.305,71.686,-53.25,71.71,-53.139,71.775,-53.145,71.807,-53.334,71.79,-53.358,71.82,-53.355,71.871,-53.374,71.936,-53.42,72,-53.575,72.098,-53.64,72.123,-53.693,72.16,-53.81,72.293,-53.776,72.326,-53.672,72.351,-53.652,72.363,-53.901,72.342,-53.928,72.319,-53.881,72.285,-53.847,72.24,-53.828,72.183,-53.793,72.134,-53.703,72.08,-53.631,72.052,-53.514,71.976,-53.462,71.894,-53.478,71.85,-53.569,71.806,-53.715,71.758,-53.76,71.718,-53.78,71.679,-53.894,71.642,-53.964,71.656,-54.02,71.658,-53.954,71.593,-53.912,71.526,-53.963,71.459,-54.099,71.419,-54.173,71.417,-54.318,71.384,-54.689,71.367,-54.818,71.375,-55.055,71.409,-55.336,71.427,-55.448,71.472,-55.594,71.554,-55.668,71.627,-55.669,71.692,-55.63,71.739,-55.549,71.768,-55.452,71.958,-55.316,72.111,-54.971,72.268,-54.873,72.325,-54.84,72.356,-54.841,72.379,-54.896,72.394,-55.32,72.2,-55.581,72.179,-55.659,72.223,-55.636,72.3,-55.589,72.319,-55.378,72.311,-55.296,72.354,-55.428,72.42,-55.569,72.437,-55.602,72.453,-55.457,72.503,-55.122,72.5,-55.046,72.534,-54.925,72.572,-54.79,72.642,-54.74,72.7,-54.729,72.75,-54.758,72.791,-54.761,72.832,-54.738,72.873,-54.773,72.918,-54.866,72.967,-55.073,73.015,-55.134,72.961,-55.198,72.938,-55.289,72.933,-55.372,72.956,-55.46,72.964,-55.545,72.985,-55.634,72.991,-55.669,73.008,-55.691,73.054,-55.693,73.113,-55.592,73.14,-55.452,73.162,-55.359,73.203,-55.297,73.262,-55.288,73.327,-55.332,73.397,-55.446,73.46,-55.656,73.399,-55.739,73.384,-55.758,73.428,-55.787,73.46,-55.876,73.505,-55.992,73.537,-56.104,73.558,-56.109,73.591,-56.083,73.627,-56.033,73.67,-55.968,73.76,-55.897,73.752,-55.838,73.76,-55.872,73.833,-55.929,73.895,-55.997,73.931,-55.999,73.946,-56.014,73.964,-56.066,74.007,-56.124,74.039,-56.225,74.129,-56.298,74.163,-56.392,74.181,-56.493,74.182,-56.655,74.159,-56.954,74.131,-57.191,74.118,-57.231,74.125,-57.112,74.159,-56.937,74.195,-56.706,74.219,-56.639,74.278,-56.664,74.33,-56.654,74.378,-56.718,74.429,-56.656,74.458,-56.446,74.486,-56.35,74.49,-56.255,74.527,-56.522,74.614,-56.801,74.672,-56.871,74.695,-56.933,74.733,-56.986,74.787,-57.072,74.84,-57.191,74.894,-57.365,74.945,-57.813,75.04,-57.967,75.105,-58.109,75.205,-58.18,75.247,-58.253,75.279,-58.566,75.353,-58.603,75.385,-58.281,75.472,-58.25,75.507,-58.381,75.612,-58.516,75.689,-58.663,75.716,-58.881,75.73,-59.082,75.765,-59.264,75.819,-59.445,75.859,-59.717,75.896,-60.173,75.993,-60.875,76.097,-61.188,76.158,-61.375,76.18,-61.621,76.186,-62.097,76.242,-62.496,76.26,-62.743,76.252,-62.823,76.262,-63.006,76.319,-63.291,76.352,-63.439,76.339,-63.622,76.278,-63.843,76.217,-63.96,76.209,-64.135,76.265,-64.223,76.303,-64.307,76.317,-64.387,76.304,-64.543,76.253,-64.692,76.216,-64.912,76.173,-65.088,76.152,-65.313,76.146,-65.37,76.131,-65.457,76.13,-65.574,76.144,-65.683,76.173,-65.785,76.215,-65.876,76.238,-65.954,76.242,-66.134,76.22,-66.362,76.155,-66.466,76.139,-66.553,76.146,-66.66,76.166,-66.874,76.218,-66.993,76.213,-67.079,76.195,-67.055,76.152,-66.854,76.05,-66.675,75.977,-66.826,75.969,-68.149,76.067,-68.317,76.091,-68.561,76.15,-68.763,76.187,-69.108,76.281,-69.373,76.332,-69.461,76.372,-69.484,76.399,-69.4,76.436,-68.865,76.561,-68.661,76.587,-68.245,76.617,-68.147,76.636,-68.114,76.651,-68.223,76.678,-68.767,76.668,-69.252,76.686,-69.674,76.736,-69.747,76.752,-69.819,76.783,-69.888,76.827,-69.872,76.877,-69.771,76.931,-69.712,76.969,-69.694,76.989,-70.224,76.855,-70.441,76.807,-70.613,76.822,-70.734,76.844,-70.793,76.869,-70.791,76.896,-70.771,76.917,-70.735,76.929,-71.015,76.985,-71.141,77.029,-71.155,77.074,-71.055,77.121,-70.958,77.154,-70.863,77.175,-70.604,77.194,-69.657,77.229,-68.978,77.195,-68.747,77.307,-68.592,77.343,-68.136,77.38,-67.434,77.385,-66.938,77.364,-66.706,77.338,-66.389,77.28,-66.371,77.298,-66.448,77.35,-66.453,77.393,-66.325,77.468,-66.266,77.515,-66.306,77.565,-66.445,77.616,-66.691,77.681,-66.824,77.687,-66.971,77.671,-67.147,77.635,-67.515,77.543,-67.688,77.524,-67.977,77.519,-68.137,77.53,-68.292,77.544,-68.533,77.593,-68.622,77.602,-68.728,77.581,-68.854,77.529,-68.975,77.493,-69.091,77.472,-69.2,77.463,-69.351,77.467,-69.977,77.548,-70.118,77.583,-70.126,77.638,-70.318,77.69,-70.535,77.7,-70.562,77.717,-70.287,77.798,-70.081,77.831,-70.114,77.841,-70.412,77.843,-70.614,77.8,-70.729,77.793,-70.994,77.792,-71.272,77.813,-71.39,77.832,-71.512,77.875,-71.65,77.9,-72.065,77.937,-72.159,77.957,-72.247,77.99,-72.586,78.085,-72.792,78.155,-72.818,78.194,-72.581,78.279,-72.571,78.299,-72.672,78.335,-72.715,78.362,-72.68,78.4,-72.473,78.482,-72.396,78.504,-72.024,78.553,-71.651,78.623,-71.516,78.639,-71.395,78.643,-70.906,78.638,-70.754,78.656,-70.625,78.69,-70.414,78.725,-69.974,78.778,-68.993,78.857,-68.93,78.867,-68.924,78.882,-69.012,78.923,-69.031,78.943,-68.83,78.98,-68.377,79.038,-68.068,79.066,-67.868,79.068,-67.708,79.08,-67.482,79.117,-67.355,79.123,-66.584,79.138,-66.243,79.118,-66.075,79.118,-65.968,79.132,-65.826,79.174,-65.56,79.276,-65.42,79.34,-65.288,79.437,-65.117,79.589,-64.989,79.737,-64.905,79.881,-64.839,79.969,-64.792,80.001,-64.632,80.041,-64.466,80.072,-64.179,80.099,-64.205,80.112,-64.327,80.134,-64.44,80.142,-64.545,80.137,-64.735,80.104,-64.982,80.082,-65.222,80.086,-65.395,80.078,-65.553,80.048,-65.81,80.024,-65.982,80.029,-66.292,80.072,-66.448,80.08,-66.844,80.076,-66.959,80.092,-67.061,80.123,-67.141,80.166,-67.201,80.222,-67.193,80.28,-67.051,80.385,-66.996,80.413,-66.61,80.53,-66.372,80.584,-66.136,80.625,-65.963,80.649,-65.801,80.66,-65.645,80.685,-65.358,80.767,-65.062,80.836,-64.694,80.966,-64.516,81,-63.892,81.056,-63.722,81.057,-63.578,81.043,-63.442,81.014,-63.059,80.886,-63.029,80.89,-63.095,80.938,-63.235,81.083,-63.212,81.143,-62.993,81.207,-62.903,81.218,-62.672,81.214,-62.299,81.194,-62.049,81.173,-61.86,81.138,-61.636,81.116,-61.519,81.117,-61.436,81.134,-61.317,81.188,-61.162,81.281,-61.1,81.396,-61.131,81.532,-61.176,81.632,-61.236,81.695,-61.203,81.747,-61.015,81.81,-60.843,81.855,-60.432,81.92,-60.099,81.937,-59.902,81.933,-59.642,81.903,-59.282,81.884,-58.957,81.825,-58.43,81.69,-58.08,81.622,-57.79,81.592,-57.505,81.54,-57.083,81.43,-56.862,81.383,-56.731,81.366,-56.615,81.363,-56.658,81.394,-56.86,81.46,-57.168,81.532,-57.853,81.662,-58.23,81.754,-58.568,81.858,-58.817,81.92,-59.268,81.982,-59.262,82.007,-58.717,82.093,-57.717,82.168,-56.589,82.227,-56.212,82.221,-55.549,82.246,-55.486,82.283,-55.344,82.3,-54.726,82.351,-54.549,82.351,-54.277,82.326,-53.987,82.279,-53.853,82.237,-53.671,82.164,-53.582,82.062,-53.596,81.738,-53.591,81.677,-53.556,81.653,-53.43,81.688,-53.28,81.754,-53.145,81.8,-53.041,81.871,-52.969,81.967,-52.926,82.038,-53.102,82.119,-53.111,82.251,-53.023,82.322,-52.776,82.322,-51.754,82.078,-51.352,82.026,-50.894,81.895,-50.36,81.909,-49.867,81.893,-49.649,81.898,-49.541,81.918,-49.694,81.972,-50.395,82.121,-50.713,82.237,-50.936,82.383,-50.99,82.46,-50.82,82.474,-50.037,82.472,-48.861,82.405,-47.357,82.174,-46.617,82.097,-45.291,81.829,-44.891,81.788,-44.729,81.78,-44.608,81.813,-44.532,81.849,-44.527,81.897,-44.591,81.957,-44.628,82.026,-44.637,82.104,-44.547,82.26,-44.333,82.311,-44.239,82.368,-44.327,82.472,-44.577,82.543,-45.552,82.725,-45.557,82.747,-45.36,82.771,-45.067,82.785,-42.651,82.741,-42.233,82.725,-42.055,82.71,-41.977,82.689,-41.876,82.68,-41.357,82.705,-41.37,82.75,-41.434,82.779,-44.239,82.857,-44.762,82.884,-45.028,82.886,-45.303,82.865,-45.873,82.855,-46.137,82.859,-46.478,82.952,-46.169,83.064,-45.909,83.061,-45.415,83.018,-45.122,83.079,-44.657,83.129,-44.197,83.147,-43.195,83.255,-43.009,83.265,-42.776,83.259,-42.26,83.232,-42.055,83.205,-41.82,83.148,-41.683,83.13,-41.522,83.127,-41.3,83.101,-40.979,83.185,-40.689,83.275,-40.357,83.332,-39.886,83.299,-39.588,83.256,-39.316,83.204,-38.931,83.175,-38.278,82.999,-38.156,82.999,-38.099,83.014,-38.037,83.046,-38.015,83.095,-37.935,83.161,-37.993,83.185,-38.54,83.258,-38.643,83.286,-38.748,83.333,-38.75,83.371,-38.648,83.401,-38.541,83.415,-38.188,83.402,-38.071,83.412,-37.961,83.438,-37.828,83.486,-37.723,83.498,-37.487,83.499,-37.123,83.468,-36.804,83.466,-36.69,83.48,-36.672,83.51,-36.644,83.529,-36.606,83.537,-35.452,83.539,-35.166,83.546,-34.942,83.568,-34.668,83.571,-34.428,83.558,-34.132,83.529,-33.837,83.53,-33.398,83.577,-32.984,83.6,-30.703,83.593,-29.953,83.565,-28.992,83.505,-28.484,83.435,-28.377,83.437,-27.688,83.41,-27.034,83.377,-25.947,83.29,-25.795,83.261,-25.912,83.238,-26.183,83.221,-27.572,83.193,-30.092,83.157,-31.534,83.089,-31.993,83.085,-32.033,82.983,-31.837,82.978,-31.516,82.992,-30.386,83.094,-29.964,83.11,-29.175,83.102,-28.151,83.064,-27.739,83.077,-27.002,83.067,-26.141,83.096,-25.123,83.16,-24.845,83.019,-24.47,82.877,-24.174,82.893,-23.92,82.885,-23.834,82.839,-23.695,82.819,-23.407,82.83,-22.525,82.789,-21.92,82.716,-21.692,82.683,-21.583,82.634,-21.521,82.595,-21.616,82.548,-21.994,82.463,-22.473,82.385,-23.118,82.325,-23.862,82.287,-29.579,82.161,-29.773,82.131,-29.887,82.055,-29.811,81.955,-29.544,81.94,-28.919,81.996,-27.84,82.049,-27.046,82.046,-25.149,82.001,-24.589,81.883,-24.293,81.701,-23.637,81.742,-23.496,81.773,-23.393,81.827,-23.311,81.885,-23.249,81.947,-23.18,81.989,-23.104,82.012,-22.94,82.031,-22.563,82.053,-21.576,82.075,-21.338,82.069,-21.167,81.984,-21.13,81.934,-21.118,81.87,-21.123,81.79,-21.147,81.695,-21.231,81.601,-21.504,81.438,-21.724,81.348,-21.961,81.284,-22.415,81.137,-22.573,81.098,-23.072,80.927,-23.196,80.847,-23.204,80.789,-23.118,80.778,-22.973,80.833,-22.919,80.872,-22.826,80.913,-22.089,81.02,-21.931,81.05,-21.45,81.178,-21.142,81.226,-20.89,81.276,-20.756,81.312,-20.016,81.564,-19.63,81.64,-19.225,81.64,-19.153,81.512,-18.667,81.492,-18.457,81.498,-18.118,81.467,-17.969,81.441,-17.717,81.428,-17.456,81.398,-17.226,81.43,-17.159,81.451,-16.937,81.544,-16.637,81.626,-16.359,81.729,-16.267,81.754,-16.121,81.777,-15.969,81.785,-15.556,81.834,-15.451,81.837,-15.227,81.822,-14.242,81.814,-13.704,81.789,-12.956,81.72,-12.434,81.683,-12.193,81.649,-11.841,81.578,-11.557,81.503,-11.426,81.481,-11.431,81.457,-11.529,81.424,-12.231,81.309,-12.461,81.233,-13.126,81.088,-13.451,81.038,-13.804,81.019,-14.197,81.014,-14.452,80.993,-14.49,80.973,-14.308,80.913,-14.229,80.87,-14.24,80.832,-14.431,80.776,-14.504,80.763,-15.194,80.721,-15.543,80.65,-15.998,80.642,-16.319,80.65,-16.761,80.573,-16.588,80.511,-16.429,80.484,-15.937,80.428,-15.933,80.395,-16.168,80.329,-16.489,80.252,-16.868,80.198,-17.011,80.19,-17.191,80.204,-17.357,80.201,-17.723,80.176,-18.071,80.172,-18.693,80.207,-19.029,80.248,-19.206,80.262,-19.429,80.258,-19.515,80.241,-19.867,80.145,-20.04,80.079,-20.15,80.011,-20.197,79.938,-20.181,79.858,-20.138,79.803,-20.069,79.774,-19.985,79.756,-19.839,79.746,-19.518,79.755,-19.392,79.75,-19.353,79.734,-19.284,79.683,-19.296,79.635,-19.354,79.567,-19.399,79.488,-19.431,79.398,-19.414,79.349,-19.284,79.338,-19.223,79.342,-19.152,79.325,-19.072,79.289,-19.011,79.251,-18.971,79.211,-18.992,79.178,-19.075,79.152,-19.262,79.123,-19.723,79.065,-19.77,79.047,-19.806,79.012,-19.832,78.959,-19.887,78.911,-19.973,78.868,-20.05,78.842,-20.2,78.83,-20.396,78.829,-20.616,78.804,-21.134,78.659,-21.141,78.643,-20.947,78.596,-20.956,78.555,-21.195,78.38,-21.26,78.293,-21.312,78.174,-21.397,78.074,-21.516,77.992,-21.633,77.897,-21.748,77.791,-21.73,77.709,-21.579,77.651,-21.38,77.698,-21.132,77.847,-20.863,77.912,-20.572,77.892,-20.319,77.862,-19.995,77.803,-19.724,77.767,-19.49,77.719,-19.394,77.678,-19.297,77.621,-19.296,77.585,-19.468,77.566,-19.524,77.572,-19.953,77.666,-20.162,77.69,-20.439,77.662,-20.681,77.619,-20.464,77.447,-20.232,77.368,-19.809,77.332,-19.588,77.294,-19.426,77.246,-19.3,77.222,-19.131,77.233,-18.903,77.28,-18.586,77.283,-18.443,77.259,-18.339,77.215,-18.292,77.133,-18.303,77.012,-18.337,76.921,-18.396,76.86,-18.51,76.778,-18.606,76.763,-18.74,76.768,-18.865,76.785,-18.981,76.814,-19.156,76.837,-19.509,76.861,-19.865,76.914,-20.064,76.928,-20.487,76.921,-20.942,76.887,-20.96,76.843,-21.615,76.688,-21.749,76.69,-21.931,76.743,-22.185,76.794,-22.334,76.794,-22.555,76.729,-22.609,76.704,-22.607,76.681,-22.444,76.625,-22.379,76.612,-22.295,76.601,-22.004,76.588,-21.877,76.573,-21.758,76.401,-21.569,76.294,-21.488,76.272,-21.417,76.264,-21.185,76.268,-20.887,76.304,-20.783,76.275,-20.564,76.24,-20.435,76.231,-20.279,76.232,-20.104,76.219,-19.863,76.121,-19.958,75.997,-19.807,75.897,-19.566,75.795,-19.509,75.758,-19.486,75.69,-19.48,75.645,-19.462,75.604,-19.431,75.567,-19.4,75.494,-19.366,75.386,-19.375,75.298,-19.426,75.23,-19.526,75.18,-19.676,75.149,-19.798,75.157,-19.893,75.205,-20.027,75.255,-20.199,75.308,-20.485,75.314,-20.906,75.157,-21.094,75.149,-21.247,75.133,-21.409,75.065,-21.649,75.023,-21.861,75.04,-22.233,75.12,-22.098,75.066,-21.904,75.004,-21.784,74.971,-21.695,74.964,-21.598,74.972,-21.457,74.998,-21.141,75.069,-21.057,75.079,-20.986,75.074,-20.928,75.053,-20.861,74.993,-20.786,74.892,-20.795,74.806,-20.89,74.735,-20.971,74.69,-21.038,74.67,-21.038,74.654,-20.862,74.636,-20.611,74.728,-20.532,74.843,-20.417,74.975,-20.214,75.019,-19.985,74.975,-19.8,74.852,-19.538,74.625,-19.427,74.601,-19.287,74.546,-19.225,74.479,-19.242,74.4,-19.272,74.343,-19.315,74.307,-19.369,74.284,-19.467,74.269,-19.646,74.258,-20.048,74.282,-20.256,74.283,-20.231,74.205,-20.653,74.137,-21.129,74.111,-21.581,74.163,-21.955,74.244,-21.832,74.357,-21.762,74.483,-21.943,74.566,-21.983,74.567,-21.92,74.439,-21.973,74.39,-22.177,74.33,-22.322,74.303,-22.334,74.286,-22.264,74.272,-22.217,74.246,-22.196,74.206,-22.22,74.166,-22.291,74.125,-22.329,74.091,-22.335,74.063,-22.271,74.03,-22.135,73.99,-21.988,73.971,-21.298,73.962,-21.022,73.941,-20.367,73.848,-20.338,73.82,-20.449,73.653,-20.51,73.493,-20.637,73.464,-21.326,73.457,-21.548,73.432,-21.873,73.358,-22.185,73.27,-22.347,73.269,-22.987,73.346,-23.233,73.398,-23.761,73.543,-24.158,73.764,-24.34,73.672,-24.451,73.629,-24.566,73.606,-24.677,73.602,-24.784,73.618,-24.905,73.653,-25.109,73.734,-25.351,73.814,-25.521,73.852,-25.528,73.841,-25.427,73.794,-25.281,73.74,-24.909,73.58,-24.778,73.54,-24.791,73.511,-25.026,73.486,-25.311,73.431,-25.45,73.391,-25.665,73.293,-25.74,73.278,-26.062,73.253,-26.169,73.259,-26.407,73.313,-26.765,73.348,-26.977,73.38,-27.27,73.436,-27.169,73.374,-26.604,73.279,-26.542,73.249,-26.658,73.192,-26.729,73.171,-26.863,73.167,-27.062,73.179,-27.265,73.176,-27.472,73.16,-27.562,73.138,-27.533,73.113,-27.483,73.089,-27.413,73.068,-27.348,73.068,-27.19,73.132,-27.07,73.137,-26.753,73.121,-26.433,73.171,-26.202,73.193,-26.029,73.199,-25.399,73.276,-25.268,73.362,-25.057,73.396,-24.587,73.423,-24.133,73.409,-23.899,73.398,-23.71,73.317,-23.456,73.259,-23.244,73.193,-22.996,73.172,-22.852,73.084,-22.45,72.986,-22.194,72.965,-22.036,72.918,-22.023,72.721,-22.007,72.635,-22.075,72.399,-22.28,72.345,-22.239,72.22,-22.293,72.12,-22.498,72.158,-22.707,72.224,-23.208,72.327,-23.674,72.393,-23.856,72.452,-24.069,72.499,-24.359,72.687,-24.547,72.922,-24.63,73.038,-24.789,73.044,-24.992,73.013,-25.171,72.98,-25.256,72.924,-25.861,72.847,-26.08,72.794,-26.206,72.796,-26.658,72.716,-26.477,72.678,-26.392,72.673,-26.209,72.694,-26.1,72.722,-25.688,72.797,-25.357,72.81,-25.237,72.843,-24.985,72.889,-24.813,72.902,-24.789,72.89,-24.771,72.869,-24.65,72.583,-24.701,72.506,-24.837,72.473,-25.128,72.419,-25.204,72.393,-25.118,72.347,-24.844,72.39,-24.667,72.437,-24.572,72.42,-24.417,72.348,-24.242,72.311,-23.798,72.201,-23.587,72.14,-23.291,72.081,-22.956,71.999,-22.869,71.971,-22.562,71.928,-22.497,71.914,-22.37,71.77,-22.265,71.754,-21.96,71.745,-22.013,71.689,-22.311,71.565,-22.465,71.525,-22.503,71.5,-22.489,71.457,-22.48,71.383,-22.418,71.249,-22.348,71.373,-22.299,71.432,-22.234,71.45,-22.17,71.453,-21.961,71.508,-21.752,71.478,-21.698,71.337,-21.671,71.206,-21.69,71.092,-21.667,70.916,-21.675,70.856,-21.625,70.805,-21.574,70.59,-21.523,70.526,-21.626,70.469,-21.944,70.443,-22.069,70.472,-22.384,70.462,-22.385,70.513,-22.4,70.571,-22.401,70.612,-22.422,70.649,-22.437,70.86,-22.526,70.808,-22.531,70.765,-22.555,70.721,-22.61,70.493,-22.691,70.437,-22.943,70.451,-23.191,70.442,-23.328,70.451,-23.792,70.555,-23.971,70.649,-24.13,70.791,-24.229,70.923,-24.266,71.046,-24.377,71.146,-24.562,71.224,-24.781,71.286,-25.033,71.334,-25.255,71.396,-25.446,71.471,-25.656,71.53,-25.885,71.572,-26.211,71.59,-26.689,71.583,-27.011,71.631,-27.087,71.627,-27.162,71.602,-27.107,71.533,-26.737,71.501,-26.452,71.494,-26.074,71.498,-25.843,71.48,-25.758,71.44,-25.699,71.368,-25.668,71.265,-25.742,71.184,-26.014,71.093,-26.158,71.05,-26.576,70.969,-26.718,70.95,-27.067,70.945,-27.336,70.953,-27.689,70.993,-27.889,71.002,-28.303,71.007,-28.398,70.993,-28.292,70.949,-28.116,70.925,-27.992,70.895,-27.979,70.84,-28.024,70.757,-28.07,70.699,-28.146,70.656,-28.417,70.574,-28.53,70.548,-29.037,70.462,-29.072,70.445,-28.953,70.447,-28.633,70.478,-28.541,70.477,-28.015,70.402,-27.596,70.407,-26.747,70.476,-26.677,70.474,-26.622,70.463,-26.565,70.438,-26.508,70.397,-26.577,70.357,-26.771,70.319,-27.073,70.281,-27.203,70.256,-27.328,70.217,-27.561,70.124,-27.629,70.028,-27.384,69.992,-27.274,70.038,-27.144,70.141,-27.028,70.201,-26.752,70.242,-26.416,70.221,-26.156,70.246,-25.625,70.347,-25.53,70.353,-24.749,70.295,-24.041,70.181,-23.667,70.139,-23.173,70.115,-22.284,70.126,-22.207,70.108,-22.235,70.068,-22.287,70.033,-22.435,69.986,-22.615,69.954,-22.726,69.945,-22.821,69.923,-23.034,69.901,-23.088,69.883,-23.015,69.805,-23.05,69.793,-23.237,69.791,-23.553,69.741,-23.812,69.744,-23.866,69.737,-23.817,69.718,-23.764,69.681,-23.709,69.627,-23.739,69.589,-23.856,69.566,-23.944,69.558,-24.248,69.59,-24.297,69.586,-24.252,69.562,-24.227,69.527,-24.221,69.479,-24.296,69.439,-24.451,69.407,-24.741,69.318,-24.867,69.293,-25.133,69.272,-25.189,69.261,-25.08,69.192,-25.092,69.165,-25.272,69.092,-25.544,69.046,-25.581,69.021,-25.606,68.954,-25.626,68.928,-25.698,68.89,-25.956,68.817,-26.139,68.781,-26.229,68.752,-26.341,68.702,-26.483,68.676,-26.654,68.673,-26.815,68.654,-27.081,68.602,-27.266,68.584,-27.851,68.494,-28.126,68.479,-28.365,68.447,-28.854,68.36,-29.088,68.332,-29.25,68.299,-29.426,68.289,-29.714,68.311,-29.869,68.312,-29.964,68.299,-30.051,68.272,-30.196,68.199,-30.318,68.193,-30.72,68.251,-30.712,68.225,-30.606,68.162,-30.611,68.118,-30.85,68.073,-30.979,68.061,-31.168,68.08,-31.419,68.128,-31.742,68.23,-32.137,68.385,-32.327,68.437,-32.314,68.388,-32.27,68.339,-32.195,68.292,-32.18,68.257,-32.224,68.236,-32.282,68.225,-32.355,68.225,-32.367,68.213,-32.249,68.139,-32.156,68.063,-32.165,67.991,-32.275,67.923,-32.37,67.883,-32.449,67.871,-32.918,67.701,-33.049,67.679,-33.108,67.658,-33.157,67.627,-33.294,67.486,-33.349,67.443,-33.458,67.387,-33.504,67.377,-33.518,67.354,-33.498,67.318,-33.528,67.258,-33.608,67.174,-33.881,66.942,-34.102,66.726,-34.198,66.655,-34.269,66.625,-34.314,66.636,-34.423,66.63,-34.476,66.592,-34.524,66.523,-34.576,66.471,-34.633,66.435,-35.075,66.279,-35.189,66.25,-35.291,66.269,-35.412,66.262,-35.662,66.344,-35.705,66.374,-35.867,66.441,-35.862,66.406,-35.835,66.387,-35.812,66.358,-35.756,66.324,-35.63,66.14,-35.729,66.102,-35.818,66.059,-36.044,65.987,-36.289,65.865,-36.379,65.831,-36.399,65.93,-36.389,65.96,-36.527,66.008,-36.523,65.973,-36.537,65.941,-36.637,65.812,-36.665,65.79,-36.715,65.795,-36.822,65.771,-36.932,65.783,-37.026,65.841,-37.063,65.871,-37.233,65.788,-37.316,65.79,-37.33,65.72,-37.41,65.656,-37.516,65.629,-37.664,65.631,-37.754,65.593,-37.955,65.634,-38.001,65.71,-37.842,65.814,-37.797,65.857,-37.827,65.91,-37.788,65.978,-37.484,66.195,-37.279,66.304,-37.291,66.324,-37.57,66.348,-37.814,66.385,-38.052,66.398,-38.157,66.386,-37.989,66.323,-37.752,66.262,-37.869,66.203,-37.969,66.141,-38.073,65.973,-38.14,65.904,-38.398,65.983,-38.52,66.01,-38.443,65.922,-38.216,65.838,-38.202,65.811,-38.203,65.712,-38.637,65.624,-39.089,65.611,-39.413,65.586,-39.961,65.556,-40.174,65.556,-40.192,65.523,-39.656,65.369,-39.578,65.341,-39.653,65.288,-39.763,65.255,-39.937,65.142,-40.028,65.103,-40.253,65.049,-40.668,65.109,-40.881,65.082,-41.084,65.101,-41.089,65.035,-41.028,64.988,-40.966,64.869,-40.829,64.878,-40.655,64.915,-40.521,64.825,-40.433,64.673,-40.278,64.596,-40.21,64.536,-40.182,64.48,-40.278,64.424,-40.478,64.344,-40.699,64.33,-40.686,64.267,-40.782,64.222,-40.985,64.235,-41.079,64.267,-41.178,64.281,-41.581,64.298,-41.175,64.177,-41.031,64.121,-40.966,64.154,-40.826,64.163,-40.618,64.132,-40.652,63.928,-40.561,63.762,-40.55,63.725,-40.772,63.626,-40.775,63.534,-40.907,63.508,-41.049,63.514,-41.056,63.412,-41.152,63.349,-41.135,63.309,-41.108,63.274,-41.195,63.209,-41.275,63.131,-41.388,63.062,-41.448,63.069,-41.628,63.065,-41.844,63.07,-42.02,63.16,-42.092,63.189,-42.175,63.209,-42.143,63.151,-42.094,63.117,-41.932,63.052,-41.634,62.972,-41.644,62.916,-41.723,62.891,-41.909,62.737,-41.975,62.734,-42.058,62.694,-42.316,62.707,-42.424,62.723,-42.741,62.713,-42.849,62.727,-42.942,62.72,-42.855,62.677,-42.674,62.638,-42.467,62.598,-42.153,62.568,-42.164,62.512,-42.243,62.466,-42.198,62.397,-42.233,62.348,-42.248,62.289,-42.321,62.153,-42.236,62.059,-42.143,62.014,-42.154,61.953,-42.11,61.857,-42.25,61.771,-42.365,61.775,-42.53,61.755,-42.585,61.717,-42.324,61.682,-42.347,61.617,-42.419,61.537,-42.494,61.363,-42.646,61.064,-42.717,60.767,-43.044,60.524,-43.16,60.517,-43.189,60.507,-43.348,60.52,-43.598,60.576,-43.792,60.595,-43.923,60.595,-43.94,60.567,-43.831,60.522,-43.665,60.503,-43.533,60.473,-43.296,60.445,-43.213,60.391,-43.156,60.333,-43.165,60.301,-43.165,60.263,-43.123,60.061,-43.235,59.991,-43.32,59.928,-43.617,59.937,-43.669,59.959,-43.955,60.025,-43.937,59.994,-43.73,59.904,-43.658,59.859,-43.706,59.849,-43.79,59.846,-43.907,59.815,-44.117,59.832,-44.105,59.878,-44.065,59.925,-44.162,59.917,-44.269,59.893,-44.331,59.902,-44.384,59.899,-44.413,59.923,-44.453,60.015,-44.405,60.061,-44.231,60.18,-44.176,60.244,-44.224,60.274,-44.348,60.205,-44.476,60.096,-44.533,60.029,-44.613,60.017,-44.812,60.05,-45.379,60.203,-45.362,60.296,-45.368,60.373,-45.202,60.383,-45.082,60.416,-44.975,60.457,-44.854,60.532,-44.742,60.655,-44.757,60.665,-45.083,60.507,-45.283,60.455,-45.381,60.445,-45.429,60.468,-45.59,60.519,-45.695,60.542,-45.934,60.579,-45.977,60.6,-46.047,60.616,-46.142,60.777,-46.019,60.972,-45.934,61.028,-45.88,61.094,-45.849,61.181,-45.87,61.218,-45.942,61.206,-45.976,61.176,-45.97,61.129,-46.012,61.097,-46.297,61.022,-46.582,60.962,-46.718,60.905,-46.806,60.86,-46.874,60.816,-46.98,60.82,-47.125,60.811,-47.224,60.783,-47.37,60.8,-47.465,60.843,-47.579,60.847,-47.707,60.827,-47.796,60.829,-47.789,60.8,-47.73,60.729,-47.828,60.725,-48.014,60.722,-48.108,60.742,-48.181,60.769,-48.242,60.807,-48.205,60.856,-47.906,60.946,-47.77,60.998,-47.859,61.016,-48.146,60.999,-48.194,61.013,-48.386,61.005,-48.378,61.138,-48.425,61.172,-48.428,61.187,-48.495,61.225,-48.558,61.234,-48.597,61.247,-48.922,61.277,-48.965,61.352,-48.987,61.429,-49.049,61.524,-49.205,61.549,-49.289,61.59,-49.222,61.632,-49.193,61.686,-49.265,61.71,-49.311,61.748,-49.304,61.772,-49.363,61.839,-49.38,61.89,-49.313,61.939,-49.13,61.993,-49.071,62.015,-49.04,62.039,-48.829,62.08,-49.008,62.108,-49.12,62.113,-49.202,62.099,-49.278,62.046,-49.349,62.01,-49.624,61.999,-49.664,62.017,-49.683,62.093,-49.668,62.151,-49.553,62.233,-49.685,62.273,-49.806,62.287,-49.943,62.324,-50.07,62.365,-50.179,62.411,-50.285,62.466,-50.319,62.473,-50.281,62.531,-50.259,62.578,-50.256,62.68,-50.299,62.722,-50.204,62.809,-50.076,62.904,-49.793,63.045,-50.092,62.977,-50.338,62.829,-50.39,62.822,-50.408,62.849,-50.502,62.945,-50.572,62.971,-50.604,63,-50.744,63.051,-50.804,63.091,-50.89,63.167,-51.013,63.258,-51.188,63.436,-51.469,63.642,-51.538,63.758,-51.451,63.905,-51.548,64.006,-51.28,64.053,-50.898,64.106,-50.699,64.149,-50.585,64.162,-50.342,64.17,-50.261,64.214,-50.396,64.203,-50.487,64.209,-50.492,64.229,-50.459,64.266,-50.437,64.313,-50.483,64.304,-50.721,64.223,-51.072,64.159,-51.347,64.123,-51.391,64.125,-51.487,64.103,-51.542,64.097,-51.585,64.103,-51.682,64.165,-51.708,64.205,-51.534,64.314,-51.404,64.463,-51.232,64.561,-51.11,64.573,-50.907,64.568,-50.835,64.559,-50.858,64.617,-50.849,64.645,-50.684,64.678,-50.492,64.693,-50.355,64.683,-50.269,64.615,-50.158,64.49,-50.009,64.447,-50.016,64.507,-50.093,64.585,-50.122,64.704,-50.22,64.754,-50.299,64.779,-50.517,64.767,-50.648,64.853,-50.678,64.885,-50.681,64.928,-50.812,65.052,-50.854,65.114,-50.924,65.197,-50.961,65.201,-50.914,65.097,-50.852,65.024,-50.765,64.863,-50.722,64.798,-50.78,64.746,-50.891,64.695,-50.989,64.665,-51.221,64.628,-51.171,64.708,-51.139,64.786,-51.255,64.758,-51.364,64.702,-51.401,64.623,-51.47,64.552,-51.677,64.377,-51.758,64.28,-51.835,64.232,-51.923,64.219,-51.999,64.257,-52.063,64.346,-52.093,64.416,-52.097,64.597,-52.089,64.682,-52.124,64.795,-52.235,65.061,-52.259,65.155,-52.448,65.205,-52.45,65.221,-52.5,65.275,-52.538,65.329,-52.506,65.348,-52.461,65.363,-52.18,65.442,-51.971,65.531,-51.721,65.67,-51.619,65.713,-51.253,65.746,-51.09,65.751,-51.092,65.776,-51.146,65.786,-51.394,65.779,-51.723,65.723,-51.78,65.703,-51.924,65.617,-52.035,65.569,-52.348,65.461,-52.551,65.461,-52.761,65.591,-52.995,65.566,-53.153,65.575,-53.199,65.594,-53.234,65.771,-53.106,65.977,-53.244,65.979,-53.272,65.987,-53.345,66.034,-53.392,66.048,-53.357,66.073,-53.018,66.171,-52.511,66.362,-52.293,66.438,-52.158,66.47,-52.056,66.507,-51.932,66.588,-51.891,66.623,-51.822,66.652,-51.676,66.684,-51.517,66.732,-51.259,66.841,-51.225,66.882,-51.281,66.891,-51.402,66.854,-51.648,66.754,-51.823,66.698,-52.421,66.447,-52.676,66.355,-52.814,66.297,-52.922,66.241,-53.036,66.201,-53.156,66.178,-53.413,66.16,-53.539,66.139,-53.615,66.154,-53.648,66.274,-53.623,66.344,-53.635,66.414,-53.571,66.513,-53.476,66.584,-53.436,66.622,-53.419,66.649,-53.223,66.721,-53.115,66.754,-53.038,66.827,-52.603,66.853,-52.491,66.85,-52.431,66.86,-52.387,66.881,-52.43,66.898,-52.56,66.909,-52.907,66.907,-53.227,66.919,-53.373,66.932,-53.444,66.925,-53.56,66.946,-53.687,66.986,-53.884,67.136,-53.805,67.327,-53.799,67.418,-53.548,67.498,-53.414,67.525,-53.224,67.585,-52.97,67.687,-52.666,67.75,-52.512,67.761,-52.384,67.752,-51.909,67.664,-51.665,67.646,-51.451,67.668,-51.181,67.637,-50.705,67.509,-50.613,67.528,-50.64,67.559,-51.171,67.694,-51.168,67.734,-51.032,67.744,-50.887,67.784,-50.969,67.807,-51.321,67.787,-51.423,67.754,-51.765,67.738,-51.944,67.765,-52.104,67.779,-52.345,67.837,-52.546,67.818,-52.673,67.795,-52.898,67.773,-52.98,67.758,-53.419,67.575,-53.604,67.536,-53.735,67.549,-53.643,67.668,-53.616,67.715,-53.616,67.767,-53.578,67.837,-53.353,67.971,-53.211,68.117,-53.152,68.208,-53.041,68.218,-52.89,68.205,-52.436,68.146,-52.058,68.075,-51.78,68.057,-51.597,68.055,-51.518,68.077,-51.456,68.116,-51.433,68.143,-51.415,68.198,-51.394,68.218,-51.333,68.242,-51.207,68.326,-51.169,68.385,-51.21,68.42]},{"b":[179.985,-16.541,180,-16.489],"l":[180,-16.54,179.987,-16.541,179.985,-16.522,180,-16.489]},{"b":[-180,-16.168,-179.945,-16.126],"l":[-180,-16.153,-179.969,-16.126,-179.945,-16.126,-179.956,-16.149,-180,-16.168]},{"b":[-85.961,16.404,-85.834,16.514],"l":[-85.961,16.43,-85.947,16.404,-85.871,16.462,-85.844,16.488,-85.834,16.511,-85.878,16.514,-85.924,16.483,-85.961,16.43]},{"b":[25.307,37.413,25.463,37.509],"l":[25.403,37.419,25.457,37.447,25.463,37.471,25.348,37.509,25.313,37.489,25.307,37.413,25.403,37.419]},{"b":[166.907,-0.551,166.958,-0.489],"l":[166.939,-0.489,166.914,-0.499,166.907,-0.524,166.916,-0.546,166.939,-0.551,166.958,-0.517,166.956,-0.497,166.939,-0.489]},{"b":[169.591,5.8,169.735,6.014],"l":[169.591,5.802,169.615,5.8,169.635,5.83,169.673,5.935,169.726,5.976,169.735,6.014,169.7,5.977,169.651,5.945,169.627,5.856,169.612,5.824,169.591,5.802]},{"b":[172.887,3.096,172.97,3.149],"l":[172.963,3.149,172.887,3.101,172.906,3.096,172.97,3.129,172.962,3.143,172.963,3.149]},{"b":[172.75,3.013,172.887,3.074],"l":[172.75,3.033,172.77,3.013,172.844,3.051,172.88,3.054,172.887,3.074,172.827,3.071,172.777,3.034,172.75,3.033]},{"b":[172.933,1.823,173.024,1.944],"l":[172.982,1.897,172.969,1.913,172.95,1.933,172.935,1.944,172.933,1.926,172.967,1.885,173.024,1.823,173.019,1.846,172.982,1.897]},{"b":[172.993,1.713,173.045,1.809],"l":[173.038,1.804,173.024,1.809,173.028,1.747,173.02,1.727,172.993,1.713,173.029,1.717,173.045,1.742,173.043,1.779,173.038,1.804]},{"b":[172.97,0.835,173.087,1.025],"l":[173.033,1.013,173.004,1.025,172.99,1.025,173.004,0.991,173.01,0.991,173.026,0.999,173.065,0.963,173.039,0.915,172.97,0.843,172.991,0.835,173.061,0.915,173.079,0.946,173.087,0.973,173.033,1.013]},{"b":[174.381,-0.829,174.509,-0.592],"l":[174.453,-0.647,174.408,-0.63,174.381,-0.592,174.394,-0.592,174.439,-0.627,174.475,-0.642,174.495,-0.726,174.509,-0.802,174.476,-0.829,174.464,-0.804,174.48,-0.774,174.453,-0.647]},{"b":[174.717,-1.263,174.779,-1.134],"l":[174.767,-1.187,174.744,-1.147,174.717,-1.134,174.741,-1.185,174.748,-1.236,174.756,-1.256,174.779,-1.263,174.773,-1.212,174.767,-1.187]},{"b":[168.675,7.294,168.83,7.336],"l":[168.679,7.336,168.675,7.322,168.719,7.303,168.815,7.294,168.83,7.309,168.755,7.322,168.679,7.336]},{"b":[171.577,6.973,171.757,7.048],"l":[171.757,6.973,171.73,6.977,171.693,7,171.659,7.01,171.614,7.007,171.593,7.016,171.577,7.048,171.615,7.027,171.688,7.028,171.757,6.973]},{"b":[171.036,7.069,171.394,7.172],"l":[171.394,7.111,171.367,7.096,171.305,7.081,171.263,7.069,171.235,7.069,171.202,7.074,171.096,7.109,171.036,7.156,171.05,7.172,171.102,7.138,171.227,7.087,171.394,7.111]},{"b":[-178.194,-14.325,-178.044,-14.232],"l":[-178.142,-14.243,-178.178,-14.232,-178.194,-14.255,-178.159,-14.312,-178.103,-14.325,-178.047,-14.318,-178.044,-14.303,-178.105,-14.284,-178.142,-14.243]},{"b":[-140.852,-17.876,-140.638,-17.67],"l":[-140.809,-17.857,-140.829,-17.849,-140.833,-17.838,-140.815,-17.804,-140.776,-17.754,-140.652,-17.683,-140.638,-17.678,-140.65,-17.67,-140.686,-17.684,-140.761,-17.718,-140.804,-17.752,-140.824,-17.788,-140.851,-17.831,-140.852,-17.867,-140.841,-17.873,-140.804,-17.876,-140.809,-17.857]},{"b":[-136.479,-18.566,-136.294,-18.463],"l":[-136.314,-18.566,-136.294,-18.544,-136.328,-18.519,-136.38,-18.497,-136.426,-18.474,-136.459,-18.463,-136.479,-18.471,-136.464,-18.485,-136.436,-18.489,-136.383,-18.514,-136.344,-18.535,-136.316,-18.545,-136.314,-18.566]},{"b":[-137.068,-18.361,-136.971,-18.265],"l":[-136.971,-18.361,-136.972,-18.342,-137.03,-18.273,-137.068,-18.265,-136.971,-18.361]},{"b":[-138.568,-20.876,-138.506,-20.771],"l":[-138.535,-20.876,-138.506,-20.857,-138.515,-20.813,-138.546,-20.771,-138.568,-20.787,-138.546,-20.795,-138.524,-20.851,-138.535,-20.876]},{"b":[-140.782,-18.416,-140.672,-18.334],"l":[-140.672,-18.416,-140.685,-18.38,-140.782,-18.334,-140.773,-18.364,-140.696,-18.399,-140.672,-18.416]},{"b":[-140.974,-18.217,-140.823,-18.059],"l":[-140.823,-18.217,-140.83,-18.189,-140.893,-18.121,-140.925,-18.084,-140.974,-18.059,-140.959,-18.085,-140.895,-18.148,-140.86,-18.199,-140.823,-18.217]},{"b":[-142.53,-16.107,-142.481,-16.018],"l":[-142.53,-16.107,-142.507,-16.028,-142.481,-16.018,-142.512,-16.096,-142.53,-16.107]},{"b":[-143.551,-16.669,-143.386,-16.612],"l":[-143.386,-16.669,-143.441,-16.62,-143.465,-16.614,-143.516,-16.612,-143.551,-16.621,-143.459,-16.635,-143.386,-16.669]},{"b":[-143.707,-16.64,-143.571,-16.581],"l":[-143.571,-16.635,-143.615,-16.618,-143.67,-16.581,-143.707,-16.581,-143.611,-16.64,-143.571,-16.635]},{"b":[-145.161,-15.901,-145.051,-15.757],"l":[-145.058,-15.901,-145.051,-15.856,-145.134,-15.762,-145.161,-15.757,-145.138,-15.788,-145.076,-15.858,-145.058,-15.901]},{"b":[-145.614,-16.347,-145.482,-16.079],"l":[-145.482,-16.347,-145.487,-16.33,-145.517,-16.278,-145.542,-16.225,-145.577,-16.16,-145.614,-16.079,-145.613,-16.132,-145.609,-16.165,-145.577,-16.201,-145.553,-16.251,-145.54,-16.295,-145.503,-16.346,-145.482,-16.347]},{"b":[173.011,1.338,173.172,1.388],"l":[173.038,1.342,173.011,1.338,173.029,1.359,173.143,1.381,173.153,1.388,173.172,1.375,173.171,1.363,173.163,1.358,173.106,1.357,173.062,1.346,173.038,1.342]},{"b":[-169.766,56.542,-169.474,56.635],"l":[-169.624,56.615,-169.755,56.635,-169.766,56.608,-169.633,56.546,-169.587,56.542,-169.474,56.594,-169.486,56.618,-169.55,56.628,-169.624,56.615]},{"b":[-72.96,19.986,-72.623,20.094],"l":[-72.851,20.094,-72.907,20.086,-72.96,20.062,-72.899,20.031,-72.878,20.027,-72.844,20.035,-72.74,20.003,-72.639,19.986,-72.623,20.014,-72.664,20.038,-72.791,20.092,-72.851,20.094]},{"b":[24.425,37.11,24.536,37.21],"l":[24.484,37.21,24.441,37.187,24.425,37.132,24.487,37.11,24.524,37.125,24.536,37.168,24.529,37.192,24.484,37.21]},{"b":[142.107,26.616,142.202,26.726],"l":[142.162,26.71,142.125,26.726,142.107,26.722,142.17,26.616,142.188,26.617,142.202,26.649,142.162,26.71]},{"b":[130.81,37.449,130.934,37.554],"l":[130.838,37.537,130.81,37.51,130.817,37.478,130.871,37.449,130.916,37.478,130.934,37.53,130.904,37.554,130.838,37.537]},{"b":[-180,-85.192,180,-63.226],"l":[-180,-84.352,-178.39,-84.337,-178.069,-84.352,-177.73,-84.395,-176.986,-84.399,-176.289,-84.418,-176.107,-84.475,-175.875,-84.51,-175.381,-84.48,-174.987,-84.465,-174.663,-84.463,-171.704,-84.542,-168.668,-84.684,-168.049,-84.729,-167.492,-84.834,-166.911,-84.819,-163.464,-84.901,-162.933,-84.901,-160.821,-84.987,-157.127,-85.186,-156.81,-85.192,-156.459,-85.186,-156.643,-85.079,-156.988,-84.982,-157.454,-84.912,-157.15,-84.891,-156.49,-84.889,-156.621,-84.84,-156.986,-84.811,-158.303,-84.778,-163.569,-84.529,-163.685,-84.513,-163.759,-84.493,-163.897,-84.47,-164.114,-84.445,-164.917,-84.431,-165.135,-84.41,-165.24,-84.381,-165.185,-84.37,-165.125,-84.375,-163.899,-84.353,-163.765,-84.324,-163.758,-84.305,-163.821,-84.291,-164.032,-84.274,-164.528,-84.191,-164.685,-84.155,-164.603,-84.097,-164.503,-84.072,-164.124,-84.054,-164.011,-84.016,-164.083,-83.946,-164.951,-83.806,-165.536,-83.757,-165.922,-83.79,-166.649,-83.792,-167.553,-83.811,-167.801,-83.791,-168.053,-83.735,-168.347,-83.637,-168.497,-83.611,-168.785,-83.529,-169.168,-83.45,-171.188,-83.256,-171.539,-83.204,-174.066,-82.9,-174.172,-82.848,-174.236,-82.793,-173.071,-82.916,-172.852,-82.917,-172.593,-82.884,-172.392,-82.893,-172.124,-82.862,-171.821,-82.847,-171.031,-82.943,-169.441,-83.096,-169.016,-83.15,-168.79,-83.188,-168.604,-83.202,-168.418,-83.229,-168.191,-83.213,-168.055,-83.227,-167.724,-83.217,-166.217,-83.201,-165.619,-83.216,-164.916,-83.29,-164.644,-83.412,-164.446,-83.468,-164.058,-83.425,-163.733,-83.373,-163.111,-83.329,-162.912,-83.347,-162.574,-83.411,-162.197,-83.519,-160.595,-83.49,-159.924,-83.495,-159.444,-83.543,-157.699,-83.381,-157.428,-83.346,-157.028,-83.234,-157.356,-83.198,-157.589,-83.187,-157.679,-83.129,-157.522,-83.107,-157.018,-83.075,-156.037,-83.027,-155.459,-82.981,-155.15,-82.858,-153.822,-82.669,-153.399,-82.586,-153.01,-82.45,-153.883,-82.177,-154.717,-81.941,-154.451,-81.868,-154.188,-81.811,-154.061,-81.765,-153.957,-81.7,-154.232,-81.623,-154.485,-81.566,-154.908,-81.51,-156.493,-81.377,-157.033,-81.319,-156.815,-81.231,-156.528,-81.162,-155.921,-81.133,-152.035,-81.029,-148.123,-80.901,-148.023,-80.836,-148.543,-80.76,-148.984,-80.742,-149.147,-80.719,-149.207,-80.67,-149.214,-80.604,-149.264,-80.593,-149.429,-80.586,-150.133,-80.51,-150.282,-80.48,-150.516,-80.409,-150.575,-80.354,-150.435,-80.211,-150.221,-80.15,-149.845,-80.118,-149.578,-80.106,-148.766,-80.108,-148.448,-80.091,-148.317,-80.071,-148.34,-80.003,-148.43,-79.971,-148.433,-79.929,-148.296,-79.907,-148.129,-79.908,-148.083,-79.857,-148.177,-79.776,-148.417,-79.731,-149.051,-79.657,-150.491,-79.546,-151.048,-79.46,-151.368,-79.393,-151.636,-79.318,-151.904,-79.281,-152.091,-79.242,-152.053,-79.193,-152.138,-79.116,-152.244,-79.103,-152.701,-79.135,-153.518,-79.117,-154.518,-79.047,-155.21,-78.965,-156.115,-78.745,-156.469,-78.635,-156.208,-78.559,-155.92,-78.51,-154.716,-78.398,-154.538,-78.359,-154.293,-78.259,-154.695,-78.217,-155.037,-78.221,-155.342,-78.192,-156.569,-78.186,-157.267,-78.2,-157.848,-78.074,-158.286,-77.951,-158.407,-77.888,-158.5,-77.778,-158.351,-77.615,-158.23,-77.498,-158.246,-77.354,-158.214,-77.157,-158.003,-77.091,-157.842,-77.079,-157.465,-77.231,-157.139,-77.242,-156.668,-77.213,-156.368,-77.135,-156.211,-77.106,-155.92,-77.098,-155.359,-77.133,-155.102,-77.12,-154.815,-77.127,-153.91,-77.227,-153.713,-77.274,-153.606,-77.31,-153.573,-77.363,-153.461,-77.416,-153.077,-77.442,-151.998,-77.413,-151.719,-77.426,-150.956,-77.574,-150.306,-77.731,-150.084,-77.771,-149.718,-77.797,-149.588,-77.774,-149.474,-77.715,-149.126,-77.643,-148.34,-77.551,-148.156,-77.462,-148.26,-77.413,-148.559,-77.361,-148.744,-77.343,-148.844,-77.284,-148.839,-77.202,-148.777,-77.125,-148.572,-77.105,-148.196,-77.211,-147.73,-77.31,-147.566,-77.325,-147.442,-77.321,-147.207,-77.286,-146.928,-77.26,-146.391,-77.472,-146.074,-77.487,-145.677,-77.488,-145.601,-77.455,-145.65,-77.398,-145.714,-77.338,-145.794,-77.33,-145.808,-77.273,-145.635,-77.221,-145.516,-77.199,-145.563,-77.162,-145.753,-77.103,-145.864,-77.094,-145.967,-77.069,-145.934,-77.029,-145.806,-77.012,-145.629,-76.954,-145.686,-76.884,-145.676,-76.797,-145.75,-76.749,-146.166,-76.658,-146.777,-76.507,-147.34,-76.438,-148.601,-76.493,-149.046,-76.458,-149.34,-76.419,-149.654,-76.365,-149.285,-76.311,-148.895,-76.272,-148.78,-76.238,-148.632,-76.168,-148.459,-76.118,-148.32,-76.104,-147.86,-76.131,-146.817,-76.318,-146.597,-76.338,-145.886,-76.424,-145.687,-76.429,-145.442,-76.409,-145.642,-76.326,-145.86,-76.267,-146.383,-76.1,-146.323,-76.02,-145.988,-75.889,-145.106,-75.879,-144.721,-75.832,-144.221,-75.731,-143.574,-75.564,-143.022,-75.543,-142.33,-75.491,-142.094,-75.53,-141.506,-75.69,-141.135,-75.746,-141.009,-75.751,-140.874,-75.746,-141.223,-75.546,-140.999,-75.52,-140.709,-75.498,-140.471,-75.447,-140.294,-75.406,-139.691,-75.213,-139.149,-75.16,-137.618,-75.076,-137.09,-75.153,-136.65,-75.162,-136.55,-75.139,-136.462,-75.036,-136.228,-74.836,-136.03,-74.765,-135.362,-74.69,-134.84,-74.694,-134.465,-74.776,-134.117,-74.83,-133.796,-74.855,-133.475,-74.852,-132.992,-74.806,-132.351,-74.789,-132.049,-74.766,-131.707,-74.811,-130.857,-74.826,-130.196,-74.891,-129.791,-74.891,-129.238,-74.829,-128.941,-74.82,-127.863,-74.719,-127.02,-74.698,-126.384,-74.743,-125.353,-74.715,-124.312,-74.736,-123.889,-74.773,-121.544,-74.75,-119.677,-74.655,-119.422,-74.622,-119.022,-74.518,-118.803,-74.422,-118.656,-74.393,-118.342,-74.382,-117.806,-74.403,-117.068,-74.473,-116.433,-74.447,-115.223,-74.487,-115.105,-74.455,-114.991,-74.275,-114.791,-73.989,-114.624,-73.903,-114.346,-73.925,-113.508,-74.089,-113.489,-74.158,-113.575,-74.208,-113.714,-74.228,-113.753,-74.367,-113.641,-74.406,-113.454,-74.394,-113.333,-74.454,-113.597,-74.559,-113.783,-74.618,-113.903,-74.644,-113.985,-74.843,-114.097,-74.909,-114.11,-74.982,-113.932,-74.982,-113.753,-74.952,-113.593,-74.944,-113.092,-74.892,-112.17,-74.832,-111.868,-74.801,-111.696,-74.792,-111.584,-74.751,-111.739,-74.653,-111.789,-74.572,-111.696,-74.504,-111.722,-74.387,-111.806,-74.27,-111.63,-74.181,-111.467,-74.201,-111.18,-74.188,-111.02,-74.23,-110.77,-74.269,-110.534,-74.289,-110.307,-74.367,-110.23,-74.536,-110.3,-74.711,-110.532,-74.836,-110.968,-74.951,-111.463,-75.133,-111.359,-75.22,-111.104,-75.191,-109.99,-75.199,-109.272,-75.185,-108.822,-75.207,-108.254,-75.253,-107.805,-75.322,-107.267,-75.334,-106.932,-75.309,-106.619,-75.344,-105.399,-75.198,-104.902,-75.115,-104.618,-75.156,-104.16,-75.121,-103.901,-75.153,-103.425,-75.101,-103.121,-75.095,-102.771,-75.117,-101.708,-75.127,-101.628,-75.222,-101.304,-75.366,-101.039,-75.422,-100.706,-75.398,-100.463,-75.353,-100.083,-75.37,-99.531,-75.309,-98.98,-75.327,-98.752,-75.317,-98.646,-75.277,-98.558,-75.19,-98.727,-75.141,-99.208,-75.079,-99.652,-74.949,-99.849,-74.922,-100.164,-74.938,-100.313,-74.914,-100.473,-74.872,-100.265,-74.823,-100.013,-74.662,-100.119,-74.515,-100.238,-74.484,-100.531,-74.489,-100.882,-74.541,-101.023,-74.505,-101.252,-74.486,-101.343,-74.35,-101.587,-74.096,-101.715,-74.024,-102.105,-73.958,-102.441,-73.926,-102.766,-73.884,-102.863,-73.784,-102.8,-73.646,-102.411,-73.616,-102.037,-73.631,-101.828,-73.655,-101.587,-73.667,-101.311,-73.695,-101.13,-73.735,-100.985,-73.757,-100.718,-73.758,-99.781,-73.72,-99.656,-73.694,-99.541,-73.645,-99.343,-73.634,-99.162,-73.641,-98.896,-73.611,-99.2,-73.571,-99.528,-73.495,-100.021,-73.403,-100.436,-73.353,-101.189,-73.318,-101.574,-73.33,-101.816,-73.311,-102.675,-73.321,-102.909,-73.285,-103.076,-73.185,-103.308,-72.945,-103.375,-72.819,-103.217,-72.772,-103.11,-72.721,-102.856,-72.716,-102.485,-72.736,-102.363,-72.76,-102.272,-72.835,-102.363,-72.911,-102.482,-72.951,-102.409,-72.987,-102.029,-72.998,-101.842,-73.021,-101.681,-73.03,-101.332,-72.995,-100.821,-72.981,-100.564,-73.016,-100.259,-73.041,-99.811,-73,-98.209,-73.022,-98.012,-73.033,-97.819,-73.102,-97.651,-73.144,-97.476,-73.126,-96.956,-73.206,-96.676,-73.269,-96.394,-73.301,-96.152,-73.309,-95.881,-73.294,-95.529,-73.241,-95.237,-73.22,-95.03,-73.239,-94.586,-73.25,-94.246,-73.313,-93.985,-73.287,-93.706,-73.215,-92.828,-73.165,-92.241,-73.178,-91.169,-73.307,-90.921,-73.319,-90.431,-73.243,-90.274,-73.119,-90.295,-72.978,-90.152,-72.945,-90.035,-72.96,-89.818,-72.863,-89.522,-72.871,-89.341,-72.89,-89.229,-72.826,-89.127,-72.693,-88.78,-72.683,-88.527,-72.702,-88.194,-72.787,-88.195,-72.859,-88.332,-72.934,-88.561,-73.121,-88.419,-73.229,-88.205,-73.22,-87.936,-73.241,-87.608,-73.195,-87.401,-73.192,-87.038,-73.354,-86.791,-73.364,-86.602,-73.354,-85.981,-73.208,-85.801,-73.192,-85.582,-73.259,-85.261,-73.413,-84.981,-73.502,-84.571,-73.557,-84.214,-73.573,-83.796,-73.645,-83.565,-73.706,-83.042,-73.707,-82.815,-73.732,-82.183,-73.857,-81.606,-73.796,-81.309,-73.738,-81.163,-73.632,-81.236,-73.474,-81.262,-73.315,-81.176,-73.249,-81.024,-73.236,-80.336,-73.414,-80.38,-73.308,-80.439,-73.225,-80.614,-73.083,-80.588,-72.978,-80.442,-72.945,-80.152,-73,-79.808,-73.028,-79.522,-73.09,-78.964,-73.312,-78.786,-73.507,-78.408,-73.556,-78.144,-73.547,-77.846,-73.515,-77.444,-73.488,-77.136,-73.496,-76.85,-73.46,-76.765,-73.566,-77.033,-73.718,-77.135,-73.818,-77.049,-73.844,-76.887,-73.821,-76.755,-73.789,-76.291,-73.805,-75.916,-73.736,-75.595,-73.711,-75.293,-73.639,-75.044,-73.645,-74.855,-73.658,-74.594,-73.715,-74.345,-73.684,-74.197,-73.696,-73.996,-73.7,-72.929,-73.448,-72.687,-73.452,-72.381,-73.438,-71.994,-73.379,-71.698,-73.353,-71.453,-73.354,-71.017,-73.263,-70.323,-73.274,-69.969,-73.226,-69.282,-73.17,-68.821,-73.105,-68,-72.936,-67.667,-72.835,-67.307,-72.611,-67.08,-72.388,-66.828,-72.09,-66.952,-71.897,-67.084,-71.812,-67.196,-71.719,-67.46,-71.527,-67.53,-71.285,-67.505,-71.058,-67.598,-70.845,-67.692,-70.686,-67.888,-70.422,-68.126,-70.25,-68.403,-70.02,-68.404,-69.809,-68.47,-69.644,-68.638,-69.526,-68.708,-69.432,-68.58,-69.413,-68.462,-69.384,-68.141,-69.348,-67.372,-69.412,-67.304,-69.318,-67.11,-69.248,-66.975,-69.161,-67.021,-69.029,-67.188,-68.974,-67.391,-68.861,-67.299,-68.771,-67.134,-68.771,-67.054,-68.671,-67.117,-68.575,-67.041,-68.453,-66.894,-68.298,-66.793,-68.24,-66.978,-68.147,-67.15,-68.025,-67.107,-67.93,-67.021,-67.831,-66.915,-67.693,-66.77,-67.593,-66.677,-67.56,-66.705,-67.527,-66.923,-67.492,-67.124,-67.485,-67.487,-67.547,-67.545,-67.535,-67.565,-67.503,-67.586,-67.435,-67.55,-67.269,-67.493,-67.113,-67.44,-67.091,-67.299,-67.071,-67.16,-66.952,-67.034,-66.945,-66.955,-66.985,-66.929,-67.144,-66.886,-67.18,-66.902,-67.256,-66.836,-67.282,-66.757,-67.233,-66.61,-67.209,-66.552,-67.263,-66.499,-67.289,-66.472,-67.243,-66.491,-67.114,-66.515,-67.062,-66.533,-66.979,-66.502,-66.94,-66.465,-66.875,-66.527,-66.741,-66.504,-66.69,-66.371,-66.609,-66.307,-66.592,-66.182,-66.592,-65.954,-66.646,-65.847,-66.65,-65.766,-66.625,-65.718,-66.573,-65.678,-66.403,-65.776,-66.343,-65.775,-66.288,-65.717,-66.254,-65.617,-66.135,-65.465,-66.129,-65.316,-66.14,-65.172,-66.117,-65.222,-66.068,-65.267,-65.994,-65.105,-65.958,-64.999,-65.946,-64.722,-65.993,-64.614,-66.019,-64.514,-65.96,-64.547,-65.9,-64.653,-65.867,-64.673,-65.814,-64.647,-65.748,-64.475,-65.781,-64.435,-65.768,-64.39,-65.708,-64.417,-65.68,-64.439,-65.641,-64.213,-65.633,-64.18,-65.617,-64.132,-65.571,-64.066,-65.554,-63.862,-65.556,-63.818,-65.532,-63.798,-65.48,-63.908,-65.467,-64.051,-65.417,-64.071,-65.278,-64.038,-65.179,-63.912,-65.093,-63.76,-65.033,-63.482,-65.085,-63.264,-65.073,-63.178,-65.126,-63.059,-65.139,-63.033,-65.08,-63.086,-65.028,-63.12,-64.942,-62.775,-64.842,-62.665,-64.858,-62.528,-64.833,-62.576,-64.756,-62.503,-64.656,-62.404,-64.643,-62.338,-64.729,-62.243,-64.747,-62.14,-64.727,-61.883,-64.625,-61.756,-64.61,-61.632,-64.605,-61.5,-64.546,-61.47,-64.476,-61.396,-64.427,-61.174,-64.362,-61.082,-64.315,-60.887,-64.15,-60.922,-64.108,-60.864,-64.073,-60.277,-63.924,-59.99,-63.91,-59.51,-63.821,-59.218,-63.714,-59.036,-63.67,-58.872,-63.552,-58.674,-63.534,-58.216,-63.451,-57.868,-63.319,-57.39,-63.226,-57.168,-63.235,-57.077,-63.262,-57.021,-63.373,-56.927,-63.506,-56.782,-63.572,-56.835,-63.631,-56.974,-63.625,-57.119,-63.638,-57.152,-63.572,-57.098,-63.524,-57.152,-63.479,-57.284,-63.491,-57.461,-63.514,-57.581,-63.547,-57.737,-63.617,-57.857,-63.657,-58.263,-63.763,-58.532,-63.915,-58.723,-64.077,-58.839,-64.187,-59.005,-64.195,-59.047,-64.234,-58.977,-64.266,-58.923,-64.279,-58.799,-64.293,-58.819,-64.339,-58.905,-64.352,-58.895,-64.389,-58.806,-64.445,-58.786,-64.524,-58.892,-64.537,-59.051,-64.451,-59.229,-64.444,-59.37,-64.404,-59.461,-64.346,-59.547,-64.359,-59.612,-64.44,-59.573,-64.531,-59.646,-64.584,-59.734,-64.559,-59.765,-64.451,-59.85,-64.434,-59.963,-64.431,-60.242,-64.547,-60.341,-64.551,-60.394,-64.609,-60.556,-64.677,-60.66,-64.729,-60.915,-64.907,-61.06,-64.981,-61.332,-65.024,-61.439,-65.018,-61.503,-65,-61.603,-64.988,-61.703,-64.987,-61.736,-65.033,-61.577,-65.186,-61.663,-65.239,-61.856,-65.235,-61.948,-65.192,-62.025,-65.233,-62.085,-65.273,-62.145,-65.332,-62.054,-65.457,-61.903,-65.513,-61.796,-65.523,-61.756,-65.569,-61.991,-65.589,-62.151,-65.699,-62.222,-65.775,-62.305,-65.84,-62.293,-65.916,-62.169,-66.031,-62.005,-66.113,-61.839,-66.12,-61.625,-66.095,-61.575,-66.071,-61.359,-66.059,-61.266,-65.98,-61.198,-65.975,-61.138,-65.989,-61.039,-65.992,-60.988,-65.94,-60.913,-65.921,-60.813,-65.934,-60.618,-65.933,-60.565,-65.979,-60.625,-66.032,-60.744,-66.105,-60.856,-66.065,-60.956,-66.072,-61.009,-66.111,-60.903,-66.191,-60.942,-66.264,-61.028,-66.337,-61.134,-66.29,-61.15,-66.212,-61.293,-66.165,-61.432,-66.145,-61.526,-66.226,-61.676,-66.25,-61.696,-66.343,-61.756,-66.429,-61.842,-66.403,-61.875,-66.296,-62.117,-66.209,-62.241,-66.197,-62.494,-66.219,-62.583,-66.217,-62.682,-66.237,-62.755,-66.31,-62.65,-66.364,-62.615,-66.436,-62.618,-66.49,-62.638,-66.511,-62.655,-66.556,-62.543,-66.621,-62.537,-66.707,-62.629,-66.706,-62.705,-66.68,-62.997,-66.453,-63.18,-66.353,-63.258,-66.264,-63.449,-66.244,-63.587,-66.242,-63.753,-66.278,-63.688,-66.32,-63.654,-66.383,-63.756,-66.409,-63.88,-66.506,-63.964,-66.589,-64.015,-66.607,-64.078,-66.654,-63.809,-66.761,-63.769,-66.803,-63.755,-66.873,-63.84,-66.912,-64.043,-66.927,-64.401,-66.853,-64.554,-66.852,-64.607,-66.8,-64.686,-66.806,-64.735,-66.894,-64.793,-66.972,-64.878,-67.025,-64.854,-67.105,-64.785,-67.124,-64.839,-67.156,-64.951,-67.183,-65.027,-67.214,-64.858,-67.243,-64.826,-67.269,-64.819,-67.307,-65.08,-67.335,-65.249,-67.342,-65.35,-67.311,-65.443,-67.326,-65.503,-67.377,-65.523,-67.445,-65.504,-67.528,-65.471,-67.588,-65.447,-67.61,-65.418,-67.66,-65.574,-67.788,-65.589,-67.816,-65.6,-67.876,-65.528,-67.93,-65.469,-68.009,-65.551,-68.048,-65.64,-68.131,-65.546,-68.147,-65.387,-68.15,-65.218,-68.14,-64.959,-68.068,-64.885,-68.056,-64.853,-68.083,-64.829,-68.127,-64.896,-68.168,-65.365,-68.287,-65.452,-68.337,-65.331,-68.364,-65.09,-68.37,-64.996,-68.408,-65.055,-68.449,-65.14,-68.489,-65.242,-68.583,-65.158,-68.618,-64.898,-68.673,-64.429,-68.746,-64.078,-68.771,-64.157,-68.687,-64.169,-68.583,-63.924,-68.498,-63.796,-68.47,-63.217,-68.419,-63.057,-68.421,-62.933,-68.443,-62.98,-68.486,-63.115,-68.471,-63.348,-68.499,-63.707,-68.592,-63.773,-68.632,-63.747,-68.705,-63.443,-68.764,-63.344,-68.81,-63.478,-68.951,-63.456,-69.042,-63.301,-69.141,-63.094,-69.253,-62.994,-69.329,-62.84,-69.372,-62.587,-69.477,-62.451,-69.584,-62.407,-69.827,-62.202,-70.028,-61.961,-70.12,-61.935,-70.2,-62.014,-70.279,-62.218,-70.233,-62.331,-70.279,-62.378,-70.365,-62.232,-70.424,-62.001,-70.497,-61.505,-70.491,-61.491,-70.57,-61.605,-70.617,-61.696,-70.676,-61.809,-70.709,-61.994,-70.729,-62.04,-70.801,-61.961,-70.901,-61.702,-70.857,-61.513,-70.851,-61.313,-70.868,-61.252,-71.002,-61.017,-71.167,-60.962,-71.245,-61.003,-71.319,-61.148,-71.342,-61.237,-71.401,-61.369,-71.452,-61.516,-71.479,-61.79,-71.616,-61.91,-71.631,-61.959,-71.658,-61.725,-71.673,-61.563,-71.675,-61.214,-71.564,-61.081,-71.589,-60.995,-71.661,-60.949,-71.747,-61.035,-71.82,-61.645,-71.863,-61.939,-71.904,-62.257,-72.018,-61.894,-72.071,-61.628,-72.053,-61.493,-72.073,-61.31,-72.113,-61.107,-72.092,-60.952,-72.05,-60.833,-72.052,-60.719,-72.073,-60.704,-72.144,-60.691,-72.27,-60.665,-72.362,-60.73,-72.426,-61.048,-72.471,-61.28,-72.468,-61.286,-72.601,-60.939,-72.7,-60.724,-72.647,-60.532,-72.673,-60.532,-72.832,-60.385,-73.007,-60.254,-73.017,-60.149,-72.938,-60.01,-72.938,-59.957,-73.031,-60.016,-73.189,-60.122,-73.275,-60.404,-73.24,-60.561,-73.211,-60.687,-73.271,-60.896,-73.32,-61.081,-73.328,-61.242,-73.25,-61.428,-73.191,-61.726,-73.161,-62.008,-73.148,-61.915,-73.216,-61.788,-73.255,-61.738,-73.375,-61.637,-73.5,-61.405,-73.467,-61.08,-73.539,-60.879,-73.612,-60.79,-73.712,-60.903,-73.871,-61.088,-73.929,-61.203,-73.957,-61.404,-73.896,-61.545,-73.896,-61.692,-73.924,-61.742,-73.996,-61.838,-74.032,-61.319,-74.036,-61.161,-74.056,-61.042,-74.122,-61.227,-74.208,-61.571,-74.195,-61.718,-74.228,-61.843,-74.29,-61.332,-74.329,-61.121,-74.307,-60.784,-74.241,-60.704,-74.307,-60.838,-74.373,-61.011,-74.478,-61.37,-74.512,-61.64,-74.514,-61.995,-74.476,-62.089,-74.453,-62.235,-74.441,-62.226,-74.506,-62.133,-74.55,-61.894,-74.713,-61.855,-74.777,-61.928,-74.863,-62.138,-74.926,-62.372,-74.952,-62.567,-74.896,-62.708,-74.737,-62.887,-74.691,-63.072,-74.678,-63.178,-74.684,-63.168,-74.765,-63.125,-74.85,-63.198,-74.909,-63.357,-74.878,-63.559,-74.906,-63.751,-74.952,-63.925,-75.004,-63.571,-75.03,-63.337,-75.035,-63.173,-75.115,-63.231,-75.154,-63.551,-75.171,-63.858,-75.206,-64.28,-75.293,-63.972,-75.329,-63.678,-75.328,-63.475,-75.336,-63.304,-75.352,-63.258,-75.399,-63.363,-75.451,-64.053,-75.58,-64.778,-75.738,-65.044,-75.787,-65.322,-75.815,-65.966,-75.952,-66.37,-76.013,-67.518,-76.11,-69.304,-76.351,-69.915,-76.522,-70.096,-76.654,-70.21,-76.674,-70.551,-76.718,-70.895,-76.739,-71.799,-76.753,-72.722,-76.689,-73.472,-76.675,-73.88,-76.697,-75.268,-76.581,-75.444,-76.587,-75.659,-76.608,-75.831,-76.608,-75.963,-76.592,-76.244,-76.585,-77.19,-76.63,-77.287,-76.702,-77.168,-76.834,-76.824,-76.993,-76.249,-77.275,-75.937,-77.334,-75.748,-77.398,-75.387,-77.474,-74.581,-77.478,-73.478,-77.536,-72.852,-77.59,-72.875,-77.694,-73.252,-77.894,-73.485,-77.971,-74.042,-78.109,-74.812,-78.178,-75.398,-78.158,-76.438,-78.044,-77.742,-77.94,-79.679,-77.843,-80.104,-77.797,-80.602,-77.752,-80.889,-77.798,-81.103,-77.842,-81.581,-77.846,-81.441,-77.886,-79.51,-78.154,-77.858,-78.351,-77.665,-78.401,-77.433,-78.435,-77.452,-78.56,-77.545,-78.66,-77.869,-78.746,-78.712,-78.752,-79.767,-78.821,-80.292,-78.823,-80.816,-78.754,-81.929,-78.559,-82.608,-78.412,-83.083,-78.247,-83.412,-78.115,-83.779,-77.984,-83.752,-78.066,-83.688,-78.148,-83.508,-78.248,-83.246,-78.357,-83.227,-78.402,-83.355,-78.408,-83.544,-78.355,-83.706,-78.404,-83.763,-78.461,-83.697,-78.537,-83.595,-78.611,-83.26,-78.774,-82.971,-78.817,-82.589,-78.916,-81.661,-79.1,-81.503,-79.163,-81.222,-79.298,-81.163,-79.4,-80.892,-79.502,-80.705,-79.517,-80.535,-79.513,-80.479,-79.426,-80.489,-79.321,-80.416,-79.295,-80.151,-79.268,-79.456,-79.304,-76.499,-79.326,-76.218,-79.387,-76.105,-79.465,-76.032,-79.627,-76.344,-79.821,-76.558,-79.904,-76.904,-79.955,-77.222,-79.994,-77.702,-80.01,-78.692,-79.995,-79.66,-79.997,-78.907,-80.09,-78.176,-80.167,-77.16,-80.153,-76.757,-80.131,-76.407,-80.095,-76.26,-80.16,-75.986,-80.295,-75.822,-80.338,-75.709,-80.383,-75.555,-80.531,-75.495,-80.617,-75.345,-80.719,-75.237,-80.803,-75.076,-80.86,-74.807,-80.887,-74.511,-80.838,-73.938,-80.816,-73.383,-80.894,-73.029,-80.917,-72.553,-80.853,-72.174,-80.764,-71.38,-80.682,-71.231,-80.647,-71.018,-80.619,-70.688,-80.626,-70.56,-80.647,-70.392,-80.735,-70.239,-80.857,-70.012,-80.918,-69.772,-80.962,-69.634,-80.966,-69.182,-81.005,-68.59,-80.968,-68.327,-81.004,-68.285,-81.074,-68.144,-81.13,-67.965,-81.148,-65.574,-81.461,-64.75,-81.522,-63.478,-81.553,-62.49,-81.557,-62.354,-81.577,-62.165,-81.636,-62.542,-81.678,-62.946,-81.684,-63.554,-81.667,-63.769,-81.676,-64.233,-81.66,-64.476,-81.672,-64.696,-81.652,-65.022,-81.696,-65.62,-81.729,-65.487,-81.775,-65.264,-81.786,-64.811,-81.803,-64.19,-81.795,-64.137,-81.869,-64.706,-81.887,-65.916,-81.902,-66.042,-81.914,-66.134,-81.953,-65.953,-81.971,-65.844,-81.993,-65.787,-82.046,-65.913,-82.183,-65.714,-82.279,-65.572,-82.294,-65.424,-82.28,-65.17,-82.318,-64.92,-82.371,-64.397,-82.374,-63.773,-82.304,-63.466,-82.307,-62.645,-82.263,-61.902,-82.271,-60.859,-82.187,-60.687,-82.189,-60.528,-82.2,-60.817,-82.276,-62.095,-82.467,-62.553,-82.503,-62.736,-82.527,-62.631,-82.621,-62.466,-82.718,-62.129,-82.822,-61.917,-82.977,-61.709,-83.01,-61.313,-82.939,-61.218,-82.992,-61.2,-83.098,-61.303,-83.184,-61.436,-83.232,-61.531,-83.279,-61.59,-83.341,-61.425,-83.396,-60.983,-83.428,-60.397,-83.441,-59.854,-83.442,-59.516,-83.458,-58.29,-83.121,-57.798,-82.959,-57.557,-82.89,-57.354,-82.84,-56.318,-82.633,-56.075,-82.57,-55.801,-82.478,-55.295,-82.465,-54.601,-82.316,-53.986,-82.201,-53.74,-82.178,-53.558,-82.169,-53.339,-82.145,-52.799,-82.154,-52.415,-82.135,-51.731,-82.062,-51.21,-82.015,-50.653,-81.975,-50.029,-81.968,-48.361,-81.892,-47.887,-81.925,-47.36,-82.004,-47.02,-82.003,-46.567,-81.979,-46.258,-81.947,-46.119,-82.04,-46.046,-82.159,-46.199,-82.271,-46.448,-82.34,-46.517,-82.455,-46.175,-82.512,-45.789,-82.495,-45.044,-82.438,-44.455,-82.366,-44.292,-82.318,-44.064,-82.331,-43.669,-82.27,-43.18,-82.017,-42.565,-81.762,-42.046,-81.598,-41.712,-81.408,-41.434,-81.298,-41.126,-81.215,-40.915,-81.172,-40.441,-81.165,-39.762,-81.032,-38.772,-80.882,-38.011,-80.954,-37.209,-81.064,-36.812,-80.975,-36.5,-80.96,-36.234,-80.921,-35.966,-80.891,-35.776,-80.813,-35.521,-80.746,-35.327,-80.651,-34.35,-80.603,-33.329,-80.54,-33.191,-80.519,-33.057,-80.532,-32.706,-80.514,-32.256,-80.461,-31.634,-80.445,-31.312,-80.45,-31.015,-80.308,-30.425,-80.28,-29.797,-80.223,-29.531,-80.182,-29.329,-80.172,-24.24,-80.062,-24.02,-80.009,-23.574,-79.965,-23.407,-79.859,-24.088,-79.815,-24.3,-79.771,-24.534,-79.758,-24.67,-79.775,-25.259,-79.762,-29.949,-79.599,-30.049,-79.585,-30.211,-79.485,-30.178,-79.304,-30.316,-79.163,-30.645,-79.124,-30.985,-79.128,-31.413,-79.145,-32.542,-79.222,-32.994,-79.229,-34.197,-79.11,-34.995,-78.978,-35.516,-78.933,-35.89,-78.844,-36.239,-78.774,-36.266,-78.616,-36.181,-78.468,-35.509,-78.041,-35.088,-77.837,-34.808,-77.821,-34.551,-77.729,-34.29,-77.522,-34.076,-77.425,-33.591,-77.311,-33.377,-77.282,-32.614,-77.141,-32.405,-77.136,-32.063,-77.16,-31.676,-77.033,-30.489,-76.762,-30.222,-76.66,-29.892,-76.598,-28.934,-76.37,-28.079,-76.258,-27.653,-76.226,-27.135,-76.157,-26.56,-76.055,-26.059,-75.957,-24.27,-75.767,-23.197,-75.718,-22.465,-75.661,-21.948,-75.694,-21.434,-75.683,-20.989,-75.634,-20.783,-75.594,-20.487,-75.492,-19.493,-75.54,-18.851,-75.47,-18.585,-75.463,-18.305,-75.431,-18.415,-75.396,-18.517,-75.39,-18.617,-75.342,-18.749,-75.242,-18.617,-75.115,-18.517,-75.052,-18.221,-74.975,-18.068,-74.863,-17.923,-74.699,-17.436,-74.379,-17.299,-74.334,-16.989,-74.32,-16.727,-74.328,-16.43,-74.324,-15.673,-74.407,-15.531,-74.376,-15.29,-74.281,-15.089,-74.163,-14.659,-73.989,-14.574,-73.937,-14.611,-73.852,-15.26,-73.889,-15.749,-73.946,-16.22,-73.916,-16.282,-73.867,-16.181,-73.83,-16.003,-73.816,-15.936,-73.758,-16.097,-73.709,-16.388,-73.681,-16.519,-73.644,-16.507,-73.556,-16.435,-73.426,-16.279,-73.388,-16.149,-73.334,-15.803,-73.152,-15.596,-73.097,-15.007,-73.047,-14.321,-73.123,-14.165,-73.102,-14,-73.001,-14.168,-72.843,-14.298,-72.785,-14.298,-72.733,-13.939,-72.756,-13.603,-72.792,-13.209,-72.785,-12.747,-72.629,-12.095,-72.498,-11.777,-72.444,-11.497,-72.413,-11.346,-72.282,-11.121,-72.032,-10.958,-71.902,-10.961,-71.822,-11.009,-71.758,-11.179,-71.777,-11.333,-71.786,-11.697,-71.719,-12.148,-71.614,-12.285,-71.495,-12.351,-71.39,-12.208,-71.332,-12.074,-71.297,-11.926,-71.289,-11.663,-71.331,-11.328,-71.44,-11.16,-71.481,-10.97,-71.56,-10.825,-71.553,-10.659,-71.443,-10.52,-71.296,-10.407,-71.25,-10.231,-71.201,-10.033,-71.131,-10.122,-71.061,-10.331,-71.024,-10.36,-70.982,-10.271,-70.936,-10.099,-70.926,-9.888,-71.027,-9.599,-71.095,-9.402,-71.118,-9.231,-71.174,-8.966,-71.361,-8.646,-71.673,-8.498,-71.675,-8.216,-71.647,-7.916,-71.635,-7.714,-71.546,-7.669,-71.324,-7.59,-71.224,-7.618,-71.121,-7.757,-71.017,-7.873,-70.94,-7.855,-70.885,-7.753,-70.843,-7.62,-70.829,-7.388,-70.787,-7.032,-70.835,-6.838,-70.845,-6.548,-70.817,-6.245,-70.756,-5.936,-70.713,-5.695,-70.745,-5.588,-70.857,-5.709,-70.968,-5.904,-71.052,-6.08,-71.154,-6.127,-71.266,-6.117,-71.326,-5.95,-71.342,-4.45,-71.328,-4.253,-71.338,-3.995,-71.339,-3.713,-71.375,-3.24,-71.36,-2.812,-71.321,-2.61,-71.321,-2.261,-71.357,-2.015,-71.433,-1.501,-71.412,-1.354,-71.387,-1.216,-71.284,-1.068,-71.266,-0.896,-71.349,-0.84,-71.54,-0.76,-71.63,-0.543,-71.713,-0.327,-71.642,-0.185,-71.559,0.154,-71.398,0.538,-71.274,0.835,-71.202,1.552,-71.08,1.909,-71.004,2.609,-70.9,3.507,-70.844,5.113,-70.656,5.644,-70.636,6.508,-70.586,6.951,-70.535,7.401,-70.494,7.677,-70.356,8.307,-70.462,8.523,-70.474,8.817,-70.391,9.142,-70.184,9.613,-70.269,9.886,-70.403,10.218,-70.508,10.969,-70.688,11.204,-70.729,11.701,-70.767,11.834,-70.737,12.068,-70.617,12.309,-70.443,12.462,-70.37,12.682,-70.309,12.929,-70.213,12.865,-70.162,12.723,-70.144,12.595,-70.117,12.626,-70.066,13.066,-70.054,13.298,-70.23,13.533,-70.287,13.823,-70.343,14.492,-70.3,15.064,-70.295,15.563,-70.331,15.807,-70.324,16.025,-70.193,16.381,-70.145,16.585,-70.204,16.709,-70.397,17.167,-70.451,18.125,-70.54,18.232,-70.518,18.351,-70.416,18.433,-70.29,18.627,-70.269,18.877,-70.201,19.009,-70.212,19.196,-70.293,19.132,-70.492,19.027,-70.674,19.153,-70.821,19.265,-70.902,19.409,-70.917,19.652,-70.921,19.944,-70.91,20.128,-70.918,21.071,-70.843,21.186,-70.681,21.337,-70.495,21.705,-70.258,21.849,-70.277,21.962,-70.3,22.216,-70.417,22.366,-70.475,22.396,-70.561,22.234,-70.643,22.278,-70.696,22.445,-70.74,22.979,-70.81,23.15,-70.796,23.407,-70.723,23.665,-70.575,23.804,-70.405,24.024,-70.413,24.236,-70.449,24.386,-70.537,24.386,-70.704,24.588,-70.82,24.757,-70.892,25.187,-70.971,25.65,-70.991,25.974,-71.037,26.499,-71.02,26.754,-70.967,26.918,-70.954,27.207,-70.911,27.509,-70.813,27.698,-70.772,28.386,-70.682,28.912,-70.583,29.464,-70.406,30.003,-70.3,30.834,-70.246,31.063,-70.225,31.379,-70.226,32.16,-70.1,32.457,-70.026,32.621,-70.001,32.81,-69.909,32.912,-69.734,32.989,-69.624,32.976,-69.517,32.903,-69.379,32.738,-69.255,32.568,-69.074,32.642,-68.869,32.776,-68.783,33.121,-68.689,33.466,-68.671,33.854,-68.683,34.193,-68.702,34.219,-68.791,34.074,-68.885,33.885,-68.979,33.772,-69.02,33.814,-69.099,34.059,-69.111,34.596,-69.095,34.75,-69.168,35.131,-69.487,35.225,-69.637,35.357,-69.681,35.568,-69.66,36.018,-69.662,36.331,-69.639,36.586,-69.638,36.719,-69.652,36.856,-69.726,37.115,-69.81,37.375,-69.748,37.56,-69.718,37.787,-69.726,38.144,-69.824,38.499,-70.056,38.886,-70.172,38.912,-70.098,38.859,-70.006,39.019,-69.924,39.211,-69.786,39.487,-69.608,39.705,-69.426,39.762,-69.173,39.864,-68.967,40.042,-68.868,40.216,-68.805,40.484,-68.739,40.817,-68.724,41.133,-68.575,41.356,-68.515,41.825,-68.433,42.409,-68.352,42.82,-68.123,42.961,-68.095,43.171,-68.06,43.554,-68.046,44.178,-67.972,44.373,-67.961,44.7,-67.904,44.99,-67.769,45.197,-67.731,45.569,-67.736,45.888,-67.66,46.154,-67.657,46.399,-67.618,46.437,-67.533,46.32,-67.477,46.317,-67.402,46.454,-67.304,46.56,-67.268,46.884,-67.275,47.154,-67.357,47.352,-67.362,47.403,-67.409,47.231,-67.468,47.117,-67.573,47.314,-67.665,47.49,-67.728,47.704,-67.716,47.959,-67.66,48.21,-67.699,48.322,-67.785,48.322,-67.917,48.375,-67.988,48.551,-67.926,48.648,-67.794,48.62,-67.625,48.63,-67.521,49.053,-67.352,49.219,-67.227,48.923,-67.2,48.714,-67.217,48.598,-67.171,48.465,-67.043,48.83,-66.938,49.247,-66.942,49.489,-67.031,50.006,-67.175,50.293,-67.172,50.553,-67.194,50.606,-67.15,50.509,-66.939,50.521,-66.82,50.306,-66.753,50.244,-66.603,50.332,-66.445,50.588,-66.356,50.937,-66.315,51.688,-66.072,51.885,-66.02,52.378,-65.969,52.955,-65.946,53.672,-65.859,54.948,-65.916,55.29,-65.954,55.504,-66.003,55.71,-66.08,55.974,-66.209,56.362,-66.373,56.859,-66.423,57,-66.475,57.185,-66.613,56.987,-66.704,56.824,-66.713,56.51,-66.659,56.295,-66.603,56.146,-66.626,56.292,-66.721,56.453,-66.78,56.48,-66.859,56.391,-66.974,55.803,-67.199,56.155,-67.265,56.366,-67.212,56.562,-67.116,56.76,-67.073,56.892,-67.056,57.361,-67.053,57.627,-67.014,57.828,-67.041,58.027,-67.103,58.317,-67.163,58.737,-67.23,59.251,-67.485,59.65,-67.459,59.868,-67.403,60.482,-67.385,61.012,-67.5,61.309,-67.54,62.174,-67.575,62.688,-67.648,63.018,-67.562,63.238,-67.527,63.699,-67.508,63.931,-67.526,64.574,-67.62,65.708,-67.716,66.488,-67.766,67.175,-67.768,67.502,-67.81,68.099,-67.854,68.328,-67.89,68.9,-67.862,69.167,-67.825,69.416,-67.743,69.559,-67.763,69.656,-67.865,69.603,-68.041,69.704,-68.161,69.789,-68.279,69.907,-68.379,69.982,-68.464,69.928,-68.535,69.762,-68.599,69.534,-68.737,69.547,-68.857,69.646,-68.932,69.531,-69.024,69.615,-69.154,69.629,-69.232,69.549,-69.294,69.372,-69.331,69.065,-69.337,68.906,-69.373,68.88,-69.47,68.959,-69.54,69.136,-69.578,69.188,-69.655,69.162,-69.77,69.083,-69.867,68.921,-69.912,68.744,-69.921,68.415,-69.902,68.178,-69.837,68.027,-69.894,67.917,-69.953,67.575,-70.088,67.417,-70.177,67.268,-70.273,67.659,-70.326,67.941,-70.423,68.559,-70.412,68.757,-70.37,69.021,-70.325,69.162,-70.334,69.25,-70.431,69.197,-70.585,69.188,-70.705,68.873,-71.035,68.768,-71.091,68.624,-71.181,68.448,-71.252,68.311,-71.287,68.037,-71.391,67.873,-71.58,67.694,-71.737,67.432,-72.003,67.281,-72.291,67.215,-72.461,67.113,-72.641,66.892,-72.949,66.746,-73,66.498,-73.125,66.569,-73.209,66.765,-73.217,67.003,-73.236,67.322,-73.3,67.749,-73.168,67.971,-73.086,68.016,-72.918,67.971,-72.751,68.107,-72.651,68.42,-72.515,69.157,-72.419,69.309,-72.409,69.555,-72.375,69.77,-72.254,69.962,-72.133,70.294,-72.055,70.573,-71.931,70.616,-71.842,70.732,-71.822,71.079,-71.737,71.168,-71.672,71.277,-71.624,71.349,-71.514,71.379,-71.309,71.465,-71.155,71.634,-70.949,71.771,-70.801,71.905,-70.707,72.263,-70.657,72.418,-70.599,72.622,-70.472,72.76,-70.396,72.744,-70.239,72.822,-70.096,73.041,-70.01,73.325,-69.849,73.676,-69.826,73.942,-69.743,74.227,-69.8,74.571,-69.88,75.148,-69.855,75.424,-69.893,75.636,-69.849,75.821,-69.725,75.891,-69.576,76.112,-69.487,76.36,-69.49,76.77,-69.34,77.192,-69.206,77.541,-69.174,77.817,-69.069,78.015,-68.892,78.229,-68.756,78.489,-68.626,78.563,-68.394,78.726,-68.278,79.035,-68.175,79.288,-68.119,80.363,-67.947,81.187,-67.831,82.017,-67.69,82.273,-67.692,82.607,-67.613,83.158,-67.611,83.304,-67.603,83.494,-67.441,83.904,-67.292,84.161,-67.244,84.485,-67.114,84.748,-67.102,85.117,-67.126,85.429,-67.161,85.711,-67.161,86.118,-67.055,86.75,-67.037,86.947,-66.986,87.085,-66.94,87.98,-66.788,88.314,-66.817,88.789,-66.792,89.077,-66.799,89.352,-66.818,89.698,-66.823,90.293,-66.77,90.547,-66.734,91.022,-66.603,91.546,-66.572,91.777,-66.537,92.073,-66.508,92.312,-66.559,92.486,-66.604,92.592,-66.614,92.731,-66.624,93.075,-66.571,93.358,-66.585,93.722,-66.643,93.964,-66.69,94.089,-66.689,94.313,-66.647,94.587,-66.544,94.84,-66.501,95.084,-66.527,95.248,-66.571,95.541,-66.631,95.991,-66.621,96.424,-66.6,96.789,-66.551,97.101,-66.499,97.388,-66.579,97.72,-66.607,98.258,-66.467,98.462,-66.499,98.603,-66.535,98.72,-66.553,98.859,-66.671,99.37,-66.648,99.824,-66.549,100.212,-66.474,100.591,-66.425,100.889,-66.358,101.327,-66.1,101.321,-66.021,101.381,-65.973,101.474,-65.951,102.174,-65.954,102.392,-65.933,102.674,-65.865,103.167,-65.917,103.639,-65.999,103.763,-65.99,103.951,-65.988,104.289,-66.039,104.667,-66.137,105,-66.164,106.387,-66.411,107.171,-66.47,107.566,-66.552,107.667,-66.58,107.785,-66.664,107.992,-66.672,108.158,-66.639,108.376,-66.766,108.91,-66.862,109.463,-66.909,109.824,-66.834,110.437,-66.621,110.622,-66.524,110.587,-66.312,110.907,-66.077,111.453,-65.961,112.13,-65.9,112.548,-65.848,113.099,-65.8,113.368,-65.849,113.502,-65.886,113.71,-65.93,113.954,-66.06,114.337,-66.36,114.619,-66.468,114.87,-66.477,115.082,-66.493,115.31,-66.561,115.635,-66.771,115.442,-66.958,115.274,-67.028,114.571,-67.108,114.26,-67.172,113.991,-67.212,113.913,-67.368,114.027,-67.441,114.319,-67.406,114.658,-67.388,114.926,-67.357,115.172,-67.308,115.384,-67.238,115.885,-67.202,116.215,-67.143,116.509,-67.108,116.713,-67.047,116.924,-67.055,117.132,-67.114,117.298,-67.109,117.745,-67.129,117.952,-67.085,118.139,-67.082,118.326,-67.115,118.519,-67.161,118.714,-67.172,118.965,-67.145,119.318,-67.071,119.768,-66.992,120.187,-66.966,120.289,-66.967,120.375,-66.984,119.954,-67.076,119.281,-67.199,118.922,-67.32,119.133,-67.371,120.4,-67.236,120.979,-67.136,121.488,-67.091,121.613,-67.057,122.033,-66.902,122.183,-66.859,122.633,-66.805,123.222,-66.745,123.667,-66.677,123.969,-66.608,124.196,-66.601,124.371,-66.652,124.598,-66.708,124.822,-66.695,125.095,-66.641,125.286,-66.516,125.398,-66.424,125.603,-66.393,125.866,-66.364,126.077,-66.396,126.424,-66.462,126.665,-66.498,126.874,-66.759,127.365,-66.99,127.541,-67.051,127.968,-67.028,128.431,-67.119,128.628,-67.107,128.816,-67.08,128.982,-67.098,129.237,-67.042,129.5,-66.753,129.741,-66.469,129.976,-66.345,130.121,-66.292,130.301,-66.268,130.579,-66.209,130.952,-66.191,131.232,-66.216,131.831,-66.236,132.32,-66.165,132.874,-66.178,133.148,-66.095,133.445,-66.081,133.843,-66.154,133.958,-66.204,134.179,-66.277,134.232,-66.348,134.289,-66.477,134.398,-66.48,134.77,-66.353,134.971,-66.33,135.352,-66.127,135.555,-66.18,136.009,-66.267,136.194,-66.292,136.553,-66.439,136.74,-66.408,136.889,-66.34,137.336,-66.346,137.754,-66.406,137.926,-66.457,138.14,-66.544,138.271,-66.564,138.376,-66.54,139.242,-66.574,139.613,-66.638,139.9,-66.715,140.902,-66.752,141.286,-66.832,141.517,-66.794,141.973,-66.807,142.159,-66.874,142.327,-66.948,142.688,-67.013,142.888,-67,143.169,-66.949,143.448,-66.877,143.73,-66.877,143.863,-66.939,143.911,-67.091,144.118,-67.088,144.348,-67.018,144.551,-67.036,144.621,-67.141,144.515,-67.283,144.26,-67.479,144.154,-67.644,143.942,-67.794,143.977,-67.865,144.189,-67.9,144.404,-67.794,144.879,-67.721,145.128,-67.626,145.556,-67.591,145.975,-67.624,146.276,-67.751,146.828,-67.965,146.852,-68.041,146.897,-68.12,146.878,-68.191,146.798,-68.274,147.094,-68.369,147.354,-68.384,147.569,-68.375,148.456,-68.467,148.881,-68.431,149.263,-68.431,149.717,-68.418,150.066,-68.42,150.342,-68.436,150.672,-68.403,150.936,-68.358,151.068,-68.385,151.121,-68.623,151.139,-68.764,151.289,-68.817,151.448,-68.764,151.562,-68.694,152.265,-68.726,152.546,-68.73,152.814,-68.768,153.082,-68.857,153.34,-68.818,153.496,-68.764,153.705,-68.729,153.767,-68.641,153.792,-68.493,153.785,-68.35,153.908,-68.323,154.032,-68.35,154.2,-68.418,154.576,-68.634,154.866,-68.774,154.988,-68.841,155.164,-68.895,155.52,-69.024,156.011,-69.078,156.489,-69.183,157.046,-69.176,157.481,-69.309,157.776,-69.205,157.933,-69.181,158.158,-69.209,158.433,-69.299,158.647,-69.32,159.386,-69.468,159.784,-69.522,159.931,-69.631,160.126,-69.734,160.126,-69.84,160.21,-69.975,160.652,-70.081,160.827,-70.182,161.037,-70.317,161.425,-70.827,161.625,-70.916,161.916,-70.907,162.189,-71.04,162.278,-71.022,162.287,-70.969,162.04,-70.625,162.022,-70.44,162.216,-70.334,162.675,-70.305,163.026,-70.501,163.349,-70.621,163.567,-70.642,163.998,-70.637,164.403,-70.51,164.716,-70.557,165.209,-70.571,165.854,-70.645,166.132,-70.633,166.627,-70.664,167.229,-70.771,167.569,-70.81,167.64,-70.854,167.799,-70.925,167.878,-71.013,167.966,-71.092,168.173,-71.183,168.383,-71.197,168.798,-71.275,169.664,-71.511,169.977,-71.581,170.162,-71.63,170.25,-71.569,170.277,-71.444,170.436,-71.419,170.603,-71.604,170.78,-71.745,170.859,-71.869,170.675,-71.969,170.409,-71.948,170.224,-71.948,170.03,-72.116,169.954,-72.403,170.127,-72.398,170.259,-72.371,170.286,-72.477,170.206,-72.565,170.048,-72.601,169.775,-72.534,169.44,-72.487,169.072,-72.469,168.719,-72.384,168.576,-72.379,168.428,-72.383,168.622,-72.473,168.82,-72.552,169.27,-72.621,169.829,-72.729,169.845,-72.795,169.712,-72.877,169.545,-73.05,169.033,-73.2,168.736,-73.091,168.381,-73.066,168.204,-73.13,167.853,-73.122,167.156,-73.147,166.883,-73.011,166.453,-72.936,166.467,-72.997,166.834,-73.224,167.226,-73.276,167.616,-73.337,167.709,-73.394,167.534,-73.447,167.296,-73.44,166.996,-73.544,166.429,-73.527,166.159,-73.534,166.001,-73.577,165.86,-73.593,165.971,-73.631,166.106,-73.735,165.913,-73.823,165.734,-73.867,165.549,-73.846,165.347,-73.879,165.25,-73.782,165.245,-73.571,165.129,-73.383,165.005,-73.375,164.813,-73.397,164.75,-73.559,164.888,-73.838,164.98,-73.926,164.906,-74.003,164.776,-74.029,165.037,-74.263,165.263,-74.426,165.4,-74.479,165.409,-74.559,165.303,-74.594,165.001,-74.563,164.853,-74.578,164.689,-74.568,164.411,-74.533,164.174,-74.523,163.936,-74.567,163.735,-74.564,163.557,-74.417,163.398,-74.382,163.266,-74.426,163.167,-74.602,162.961,-74.656,162.752,-74.736,162.604,-74.823,162.534,-75.167,162.41,-75.238,162.226,-75.235,162.088,-75.262,161.91,-75.234,161.68,-75.218,160.911,-75.335,161.033,-75.396,161.227,-75.386,161.904,-75.404,162.19,-75.467,162.239,-75.622,162.352,-75.687,162.578,-75.758,162.754,-75.793,162.816,-75.846,162.745,-75.952,162.648,-76.049,162.437,-76.155,162.498,-76.208,162.728,-76.225,162.825,-76.464,162.675,-76.569,162.745,-76.658,162.763,-76.746,162.61,-76.829,162.489,-76.869,162.45,-76.956,162.679,-77.007,162.85,-77.024,163.087,-77.032,163.25,-77.126,163.458,-77.269,163.608,-77.388,163.619,-77.582,163.766,-77.7,164.045,-77.775,164.036,-77.853,164.232,-77.877,164.421,-77.883,164.492,-77.954,164.43,-78.042,164.108,-78.147,163.978,-78.224,164.297,-78.236,164.628,-78.316,165.051,-78.226,165.274,-78.129,165.418,-78.042,165.524,-78.064,165.534,-78.154,165.663,-78.306,166.209,-78.452,166.51,-78.497,166.801,-78.522,167.058,-78.518,167.13,-78.606,167.049,-78.686,166.85,-78.68,166.525,-78.695,166.287,-78.628,166.117,-78.571,164.635,-78.603,164.301,-78.63,163.902,-78.717,163.503,-78.759,162.895,-78.845,162.639,-78.898,161.975,-78.694,161.757,-78.545,161.669,-78.536,161.51,-78.571,161.502,-78.677,161.813,-78.907,161.951,-78.968,161.951,-79.03,161.864,-79.061,161.737,-79.06,161.546,-79.015,161.191,-78.979,160.874,-79.05,160.483,-79.201,160.567,-79.302,160.67,-79.359,160.646,-79.427,160.209,-79.554,160.068,-79.561,159.976,-79.586,160.084,-79.633,160.323,-79.636,160.346,-79.692,160.265,-79.737,159.872,-79.79,159.896,-79.859,160.111,-79.892,160.387,-79.879,160.558,-79.93,160.559,-80.011,160.382,-80.054,160.179,-80.088,158.767,-80.293,158.56,-80.349,158.574,-80.423,159.065,-80.443,160.542,-80.425,160.637,-80.45,160.603,-80.508,160.521,-80.583,160.601,-80.637,160.823,-80.674,160.83,-80.73,160.72,-80.753,160.503,-80.779,160.26,-80.787,160.276,-80.847,160.607,-80.901,160.717,-80.907,160.728,-81.113,160.717,-81.2,160.54,-81.242,160.479,-81.27,160.47,-81.341,160.908,-81.39,161.582,-81.61,161.73,-81.61,161.996,-81.653,162.425,-81.765,162.577,-81.832,162.821,-81.866,162.854,-81.921,163.004,-81.969,163.602,-82.121,163.68,-82.187,162.427,-82.314,161.167,-82.408,161.283,-82.49,162.644,-82.482,163.012,-82.535,163.175,-82.519,163.269,-82.463,164.001,-82.397,164.747,-82.354,164.98,-82.385,165.982,-82.63,166.446,-82.722,166.742,-82.757,166.957,-82.765,167.116,-82.801,167.271,-82.879,167.233,-82.952,167.297,-82.986,167.404,-82.999,167.602,-83.047,167.828,-83.031,168.092,-82.975,168.276,-82.987,168.607,-83.065,168.482,-83.127,168.408,-83.155,168.32,-83.211,168.24,-83.23,167.973,-83.243,167.825,-83.243,167.674,-83.231,167.658,-83.272,167.843,-83.316,168.11,-83.362,169.838,-83.399,170.332,-83.479,170.817,-83.436,171.036,-83.448,171.221,-83.475,171.289,-83.555,171.537,-83.581,171.917,-83.644,172.45,-83.675,172.874,-83.673,173.397,-83.759,173.662,-83.761,173.822,-83.81,175.011,-83.839,175.187,-83.877,175.323,-83.94,175.606,-83.968,175.911,-83.973,177.581,-84.075,178.209,-84.13,178.353,-84.127,178.496,-84.136,178.944,-84.181,179.403,-84.206,179.62,-84.268,180,-84.352]},{"b":[178.628,70.822,180,71.538],"l":[180,71.538,179.886,71.523,179.716,71.466,179.548,71.448,179.235,71.325,178.891,71.231,178.829,71.178,178.684,71.106,178.628,71.047,178.648,71.001,178.793,70.822,178.862,70.826,179.153,70.88,179.648,70.899,179.881,70.976,180,70.993]},{"b":[46.707,36.615,54.723,47.11],"l":[54.71,40.891,54.685,40.873,54.657,40.858,54.547,40.832,54.374,40.871,54.319,40.835,54.336,40.765,54.377,40.693,54.33,40.689,54.283,40.694,54.193,40.72,54.089,40.707,53.87,40.649,53.764,40.666,53.694,40.746,53.615,40.819,53.52,40.831,53.424,40.793,53.333,40.783,53.192,40.809,53.145,40.825,53.06,40.89,52.998,40.96,52.943,41.038,52.889,40.863,52.85,40.686,52.785,40.547,52.734,40.399,52.744,40.22,52.805,40.054,52.898,39.913,52.965,39.834,53.036,39.774,52.952,39.895,52.988,39.988,53.139,39.979,53.289,39.958,53.404,39.96,53.454,39.941,53.487,39.909,53.458,39.831,53.45,39.749,53.472,39.669,53.533,39.642,53.582,39.607,53.603,39.547,53.497,39.533,53.39,39.536,53.305,39.557,53.236,39.609,53.125,39.432,53.124,39.347,53.157,39.265,53.203,39.317,53.267,39.343,53.336,39.341,53.475,39.306,53.539,39.274,53.618,39.216,53.705,39.21,53.71,39.153,53.724,39.103,53.815,39.018,53.869,38.949,53.885,38.864,53.874,38.742,53.852,38.622,53.84,38.515,53.852,38.406,53.854,38.286,53.825,38.047,53.824,37.928,53.848,37.67,53.898,37.414,53.914,37.344,53.952,37.182,54.017,36.952,54.024,36.901,54.016,36.85,53.97,36.818,53.906,36.813,53.769,36.818,53.679,36.853,53.827,36.881,53.915,36.93,53.768,36.93,53.374,36.869,52.19,36.622,51.762,36.615,51.119,36.743,50.927,36.81,50.533,37.014,50.338,37.149,50.214,37.34,50.176,37.381,50.13,37.407,49.981,37.445,49.727,37.481,49.47,37.497,49.372,37.52,49.171,37.601,49.081,37.668,49.015,37.776,48.96,37.89,48.925,38.015,48.901,38.144,48.871,38.393,48.869,38.435,48.851,38.815,48.854,38.839,48.926,38.962,48.962,39.079,49.013,39.134,49.111,39.085,49.109,39.029,49.121,39.004,49.165,39.03,49.2,39.073,49.269,39.285,49.321,39.329,49.363,39.35,49.367,39.398,49.328,39.501,49.324,39.608,49.415,39.84,49.477,40.087,49.551,40.194,49.669,40.249,49.792,40.288,49.919,40.316,50.143,40.323,50.366,40.279,50.307,40.412,50.248,40.462,50.183,40.505,50.119,40.535,49.991,40.577,49.852,40.577,49.776,40.584,49.718,40.608,49.556,40.716,49.457,40.8,49.226,41.026,49.175,41.116,49.143,41.218,49.107,41.302,49.051,41.374,48.824,41.63,48.665,41.787,48.573,41.844,48.477,41.905,48.426,41.924,48.384,41.953,48.303,42.08,48.229,42.181,48.08,42.354,47.822,42.613,47.77,42.645,47.728,42.681,47.709,42.811,47.635,42.903,47.529,42.967,47.489,43,47.463,43.035,47.513,43.219,47.512,43.271,47.49,43.382,47.509,43.51,47.568,43.685,47.628,43.806,47.646,43.885,47.563,43.835,47.463,43.555,47.429,43.78,47.362,43.993,47.307,44.103,47.23,44.192,47.123,44.262,47.024,44.343,46.916,44.387,46.753,44.421,46.721,44.452,46.707,44.503,46.716,44.561,46.755,44.657,46.841,44.718,46.957,44.783,46.984,44.826,47.003,44.876,47.039,44.838,47.084,44.817,47.115,44.906,47.162,44.97,47.221,45.024,47.296,45.149,47.351,45.218,47.391,45.295,47.413,45.421,47.454,45.433,47.489,45.455,47.515,45.491,47.529,45.53,47.524,45.602,47.463,45.68,47.479,45.688,47.508,45.674,47.574,45.634,47.633,45.584,47.65,45.657,47.701,45.686,47.764,45.666,47.83,45.663,48.053,45.721,48.159,45.737,48.258,45.778,48.487,45.935,48.537,45.942,48.589,45.935,48.637,45.906,48.69,45.889,48.73,45.897,48.75,45.921,48.703,45.976,48.687,46.029,48.684,46.086,48.743,46.101,48.81,46.1,49.08,46.189,49.111,46.228,49.125,46.282,49.246,46.292,49.232,46.337,49.206,46.386,49.286,46.437,49.362,46.41,49.344,46.486,49.347,46.519,49.437,46.537,49.584,46.545,49.632,46.568,49.761,46.571,49.886,46.596,50,46.634,50.102,46.696,50.306,46.795,50.419,46.879,50.472,46.883,50.528,46.873,50.583,46.882,50.68,46.939,50.733,46.952,50.92,47.041,51.178,47.11,51.291,47.097,51.615,47.03,51.65,47.018,51.745,46.934,51.945,46.895,52.011,46.902,52.086,46.84,52.138,46.829,52.189,46.84,52.34,46.895,52.385,46.922,52.42,46.964,52.483,46.991,52.678,46.957,52.916,46.954,53.035,46.893,53.069,46.856,53.138,46.742,53.17,46.669,53.132,46.608,53.079,46.547,53.064,46.475,53.109,46.414,53.135,46.192,53.042,45.968,52.888,45.78,52.774,45.573,52.838,45.497,53.086,45.407,53.2,45.332,53.079,45.308,52.911,45.32,52.772,45.344,52.531,45.399,52.427,45.405,52.049,45.388,51.733,45.399,51.54,45.343,51.416,45.358,51.333,45.28,51.294,45.23,51.25,45.122,51.154,45.04,51.04,44.98,51.009,44.922,51.021,44.854,51.058,44.812,51.218,44.709,51.31,44.619,51.366,44.6,51.431,44.602,51.494,44.578,51.544,44.531,51.377,44.541,51.311,44.532,51.177,44.501,51.111,44.508,51.049,44.53,50.86,44.629,50.652,44.633,50.409,44.624,50.297,44.582,50.265,44.527,50.253,44.462,50.253,44.406,50.276,44.355,50.331,44.325,50.472,44.295,50.685,44.265,50.783,44.228,50.831,44.193,50.94,43.959,51.065,43.75,51.14,43.649,51.239,43.577,51.274,43.533,51.302,43.482,51.314,43.421,51.313,43.356,51.292,43.231,51.295,43.174,51.348,43.167,51.514,43.171,51.616,43.158,51.7,43.104,51.785,43.004,51.811,42.954,51.844,42.91,51.898,42.87,51.961,42.851,52.019,42.861,52.076,42.88,52.184,42.869,52.273,42.8,52.324,42.816,52.434,42.824,52.494,42.82,52.55,42.805,52.597,42.76,52.638,42.556,52.618,42.428,52.573,42.331,52.517,42.237,52.462,42.101,52.459,42.048,52.468,41.886,52.494,41.78,52.609,41.529,52.747,41.365,52.826,41.231,52.85,41.2,52.862,41.21,52.83,41.342,52.882,41.614,52.883,41.653,52.815,41.712,52.905,41.896,52.97,41.976,53.108,42.07,53.164,42.094,53.285,42.082,53.496,42.12,53.625,42.136,53.752,42.129,53.805,42.118,53.846,42.091,53.954,41.868,53.995,41.773,54.04,41.643,54.095,41.519,54.181,41.432,54.285,41.364,54.592,41.194,54.671,41.122,54.704,41.071,54.718,41.013,54.723,40.951,54.71,40.891]},{"b":[-17.536,-34.786,180,77.73],"l":[180,68.983,179.868,69.013,179.273,69.26,178.951,69.296,178.925,69.326,178.907,69.362,178.848,69.387,178.443,69.453,177.934,69.496,177.395,69.612,176.924,69.646,176.41,69.769,176.108,69.86,175.921,69.895,175.751,69.904,175.296,69.86,174.786,69.856,174.319,69.882,173.948,69.874,173.733,69.891,173.439,69.947,173.353,69.924,173.277,69.824,173.056,69.865,172.869,69.92,172.56,69.968,171.971,70,171.247,70.076,170.868,70.096,170.487,70.108,170.525,69.938,170.503,69.857,170.36,69.751,170.201,69.683,170.161,69.627,170.582,69.583,170.714,69.388,170.884,69.264,170.997,69.135,170.995,69.045,170.538,68.825,170.066,68.799,169.61,68.786,169.415,68.92,169.311,69.08,168.946,69.163,168.588,69.228,168.423,69.24,168.303,69.271,168.23,69.447,168.15,69.577,168.048,69.626,167.95,69.699,167.857,69.728,167.628,69.74,167.073,69.554,166.884,69.5,166.82,69.5,165.98,69.546,165.761,69.584,164.513,69.609,164.16,69.719,163.946,69.735,163.705,69.702,163.498,69.693,163.201,69.715,162.945,69.683,162.376,69.649,162.166,69.612,161.945,69.545,161.537,69.38,161.48,69.3,161.48,69.202,161.566,69.064,161.566,68.905,161.495,68.85,161.365,68.823,161.23,68.654,161.104,68.563,160.856,68.538,160.997,68.608,161.129,68.654,161.341,68.905,161.31,68.982,161.141,69.039,161.036,69.098,160.982,69.334,160.929,69.459,160.911,69.606,160.739,69.655,160.119,69.73,159.833,69.785,159.729,69.87,159.839,69.99,159.831,70.081,159.89,70.159,159.983,70.221,160.006,70.31,159.959,70.424,159.912,70.506,159.805,70.605,159.728,70.65,159.351,70.791,158.702,70.935,158.037,71.039,157.447,71.075,156.685,71.094,155.895,71.096,155.596,71.039,155.029,71.034,154.414,70.974,153.794,70.88,153.461,70.879,152.798,70.836,152.509,70.834,151.762,70.982,152,71.002,152.093,71.023,151.76,71.218,151.582,71.287,151.145,71.374,150.968,71.38,150.822,71.363,150.243,71.267,150.098,71.227,150.385,71.339,150.525,71.386,150.668,71.455,150.635,71.499,150.6,71.52,150.061,71.511,150.026,71.521,149.913,71.581,149.857,71.601,149.498,71.664,149.238,71.688,148.968,71.69,148.923,71.715,148.955,71.744,148.965,71.763,149.049,71.796,149.28,71.826,149.881,71.843,150.017,71.896,149.998,71.95,149.963,71.992,149.766,72.091,149.502,72.164,148.965,72.252,148.402,72.312,147.434,72.341,147.262,72.328,147.127,72.292,146.895,72.198,146.368,71.922,146.073,71.808,145.805,71.746,145.189,71.696,145.076,71.707,144.99,71.753,145.018,71.794,145.074,71.831,145.078,71.855,145.047,71.901,145.064,71.926,145.126,71.927,145.271,71.895,145.407,71.89,145.757,71.941,145.757,72.021,145.664,72.067,145.71,72.178,145.71,72.206,145.759,72.226,145.799,72.222,146.051,72.142,146.137,72.146,146.23,72.138,146.006,71.945,146.113,71.945,146.402,72.035,146.599,72.124,146.807,72.237,146.832,72.295,146.594,72.302,145.039,72.26,144.471,72.175,144.295,72.193,144.169,72.259,144.361,72.265,144.588,72.306,144.776,72.382,144.897,72.396,145.213,72.393,145.467,72.362,146.235,72.35,146.253,72.442,146.083,72.471,145.714,72.497,145.486,72.542,145.199,72.57,144.569,72.61,144.304,72.643,143.681,72.673,143.516,72.698,142.061,72.721,141.518,72.789,141.31,72.858,140.808,72.891,140.708,72.89,140.676,72.872,140.652,72.843,140.973,72.717,140.983,72.63,141.079,72.587,140.705,72.519,140.451,72.493,139.601,72.496,139.474,72.467,139.141,72.33,139.145,72.264,139.176,72.163,139.43,72.163,139.505,72.208,139.617,72.226,140.134,72.21,140.188,72.191,140.014,72.162,139.847,72.149,139.64,71.998,139.359,71.951,139.552,71.927,139.723,71.885,139.695,71.7,139.939,71.558,139.984,71.492,139.632,71.489,139.32,71.445,139.209,71.445,139.005,71.556,138.78,71.629,138.67,71.635,138.525,71.563,138.318,71.603,138.234,71.596,138.118,71.566,138.048,71.526,137.996,71.464,137.927,71.43,137.918,71.384,138.022,71.363,138.097,71.359,138.314,71.326,138.091,71.307,138.033,71.286,138.013,71.261,137.844,71.227,137.902,71.194,137.974,71.169,137.992,71.143,137.94,71.133,137.798,71.164,137.651,71.208,137.417,71.299,137.315,71.359,137.116,71.416,136.406,71.571,136.09,71.62,135.885,71.631,135.559,71.61,135.359,71.544,135.022,71.515,134.814,71.461,134.703,71.387,134.103,71.379,133.689,71.434,133.426,71.491,133.131,71.607,132.839,71.755,132.804,71.768,132.769,71.799,132.716,71.871,132.654,71.926,132.562,71.895,132.326,71.726,132.228,71.643,132.099,71.484,132.004,71.35,131.991,71.293,132.035,71.244,131.906,71.203,131.769,71.101,131.562,70.901,131.432,70.828,131.268,70.766,131.157,70.742,131.022,70.746,130.898,70.804,130.832,70.936,130.757,70.962,130.668,70.888,130.537,70.893,130.281,70.947,130.026,71.065,129.762,71.12,129.39,71.405,129.225,71.509,129.134,71.593,128.923,71.602,128.843,71.663,128.949,71.708,129.234,71.745,129.461,71.739,129.292,71.85,129.21,71.917,129.122,71.953,129.154,71.879,129.117,71.825,129.04,71.782,128.911,71.755,128.359,72.088,128.027,72.25,127.841,72.308,127.726,72.413,127.803,72.434,128.197,72.31,128.475,72.246,128.935,72.079,129.283,72.092,129.411,72.166,129.412,72.315,129.281,72.438,129.117,72.486,128.549,72.496,128.418,72.535,128.508,72.547,128.633,72.55,128.815,72.586,129.118,72.677,129.25,72.705,129.229,72.776,129.017,72.872,128.674,72.886,128.599,72.895,128.735,72.943,128.854,72.973,129.054,73.045,129.101,73.112,129.059,73.108,128.913,73.111,128.872,73.139,128.889,73.19,128.73,73.233,128.587,73.262,128.258,73.267,128.264,73.301,128.281,73.331,128.142,73.352,128.026,73.391,127.997,73.426,127.955,73.446,127.74,73.482,127.031,73.547,126.955,73.528,126.838,73.434,126.553,73.335,126.335,73.389,126.292,73.394,126.257,73.42,126.309,73.464,126.345,73.506,126.296,73.537,126.254,73.548,126.107,73.517,125.888,73.498,125.794,73.468,125.599,73.447,125.617,73.521,124.796,73.712,124.541,73.751,124.388,73.755,124.019,73.712,123.934,73.689,123.797,73.627,123.491,73.666,123.416,73.637,123.305,73.533,123.323,73.431,123.355,73.402,123.384,73.347,123.501,73.262,123.622,73.193,123.572,73.177,123.522,73.173,123.462,73.144,123.405,73.086,123.301,73.002,123.16,72.955,122.999,72.965,122.615,73.028,122.527,73.017,122.502,72.971,122.731,72.931,122.752,72.906,122.692,72.891,122.538,72.878,122.26,72.881,122.03,72.897,121.886,72.961,121.748,72.97,121.354,72.971,120.997,72.937,120.598,72.981,119.922,72.971,119.75,72.979,119.425,73.064,118.96,73.117,118.43,73.247,118.377,73.367,118.457,73.464,118.754,73.465,118.936,73.481,118.911,73.518,118.871,73.538,118.45,73.59,117.309,73.599,116.496,73.676,115.338,73.703,114.816,73.607,114.061,73.585,113.857,73.533,113.51,73.505,113.539,73.434,113.711,73.379,113.795,73.367,113.886,73.346,113.829,73.327,113.765,73.318,113.639,73.274,113.559,73.233,113.581,73.142,113.543,73.054,113.417,72.932,113.312,72.878,113.216,72.806,113.298,72.739,113.391,72.711,113.63,72.677,113.712,72.654,113.665,72.635,113.312,72.657,113.186,72.73,113.158,72.769,113.128,72.831,113.247,72.897,113.369,72.942,113.475,73.048,113.488,73.145,113.491,73.346,113.277,73.392,113.157,73.46,113.364,73.583,113.416,73.648,113.327,73.707,113.182,73.837,113.033,73.914,112.935,73.946,112.836,73.962,112.94,73.836,112.856,73.771,112.795,73.746,112.4,73.711,112.147,73.709,111.804,73.745,111.4,73.828,111.3,73.885,111.228,73.969,111.46,74.005,111.551,74.029,111.341,74.047,111.131,74.053,111.056,73.939,110.92,73.948,110.261,74.017,110.084,73.994,109.869,73.931,109.774,73.881,109.666,73.8,109.707,73.744,109.753,73.723,110.091,73.709,110.388,73.726,110.722,73.78,110.799,73.76,110.868,73.731,110.773,73.689,110.429,73.629,109.855,73.472,109.637,73.454,109.331,73.487,109.166,73.4,109.09,73.378,108.575,73.319,108.351,73.31,108.285,73.266,108.151,73.258,108.001,73.236,107.75,73.173,107.369,73.163,107.109,73.177,106.478,73.139,106.315,73.106,106.209,73.061,106.16,73.002,106.067,72.95,105.708,72.837,105.403,72.79,105.144,72.777,105.393,72.841,105.677,72.959,106.189,73.308,106.679,73.331,106.794,73.377,107.167,73.589,107.271,73.621,107.765,73.625,108.2,73.694,109.075,74.032,109.511,74.089,109.81,74.169,109.864,74.209,109.911,74.261,109.866,74.293,109.84,74.322,110.226,74.379,110.374,74.466,110.893,74.548,111.299,74.658,111.868,74.74,112.192,74.853,112.925,75.015,113.614,75.293,113.726,75.451,113.559,75.502,113.356,75.534,113.243,75.611,113.162,75.621,112.956,75.572,112.73,75.738,112.453,75.83,112.466,75.844,112.497,75.85,112.629,75.835,113.126,75.699,113.392,75.678,113.469,75.657,113.517,75.622,113.486,75.564,113.568,75.568,113.62,75.593,113.749,75.705,113.871,75.856,113.857,75.921,113.564,75.892,113.428,76.112,113.366,76.179,113.273,76.252,113.086,76.258,112.988,76.24,113.066,76.215,113.15,76.175,113.094,76.133,113.047,76.114,112.819,76.059,112.656,76.054,112.722,76.077,112.798,76.13,112.743,76.187,112.684,76.219,112.62,76.384,112.413,76.408,112.297,76.435,112.143,76.424,111.943,76.38,112.017,76.421,112.094,76.48,111.939,76.553,111.786,76.604,111.601,76.622,111.392,76.687,111.115,76.723,110.471,76.758,109.981,76.712,109.369,76.749,108.638,76.72,108.352,76.72,108.182,76.738,108.028,76.718,107.95,76.661,107.902,76.57,107.722,76.522,107.624,76.51,107.158,76.524,106.825,76.48,106.683,76.515,106.414,76.512,106.385,76.589,106.546,76.586,106.639,76.573,106.941,76.73,107.19,76.822,107.43,76.927,107.279,76.991,106.942,77.034,106.784,77.032,106.705,77.014,106.339,77.048,106.145,77.045,105.822,76.998,105.712,77.001,105.646,77.101,105.32,77.092,104.202,77.102,104.324,77.133,104.912,77.175,105.385,77.238,105.734,77.352,106.06,77.391,105.983,77.448,105.895,77.489,105.71,77.525,105.309,77.549,104.965,77.595,104.814,77.652,104.185,77.73,104.015,77.73,103.561,77.632,103.331,77.641,103.131,77.626,102.61,77.509,101.518,77.198,101.293,77.102,101.186,77.029,100.99,76.99,100.906,76.901,100.92,76.823,101.008,76.781,101.099,76.704,101.006,76.615,100.928,76.557,101.003,76.531,101.213,76.536,101.684,76.485,101.598,76.439,101.311,76.479,101.061,76.477,100.844,76.525,100.322,76.479,99.936,76.49,99.576,76.471,98.869,76.51,98.806,76.481,98.97,76.431,99.094,76.384,99.461,76.275,99.617,76.24,99.825,76.136,99.851,75.93,99.738,75.881,99.609,75.811,99.541,75.799,99.442,75.803,99.602,75.852,99.689,75.956,99.77,76.029,99.663,76.078,99.616,76.082,99.563,76.109,99.187,76.178,98.985,76.208,98.771,76.224,98.662,76.243,98.342,76.181,98.195,76.166,98.02,76.134,97.918,76.089,97.67,76.078,97.638,76.029,97.499,75.98,97.351,76.033,97.205,76.019,96.879,75.931,96.497,75.891,96.538,75.922,96.601,75.99,96.509,76.006,95.935,75.926,95.744,75.872,95.653,75.892,95.986,76.01,96.075,76.082,95.92,76.113,95.579,76.137,95.359,76.14,95.038,76.114,94.576,76.152,94.507,76.108,94.388,76.103,94.102,76.124,93.843,76.101,93.648,76.054,93.36,76.101,93.259,76.099,93.105,76.026,92.972,76.075,92.859,75.979,92.89,75.91,92.987,75.903,93.069,75.913,93.116,75.945,93.178,75.959,93.406,75.901,93.475,75.933,93.574,75.956,93.687,75.922,94.156,75.959,94.075,75.913,93.55,75.854,92.603,75.779,92.408,75.75,91.845,75.724,91.479,75.65,91.005,75.65,90.185,75.591,89.595,75.458,89.31,75.47,88.733,75.369,88.504,75.29,87.671,75.13,87.171,75.192,87.006,75.17,86.922,75.113,86.939,75.068,87.141,75.072,87.287,75.053,87.468,75.013,87.419,74.941,87.042,74.779,86.863,74.718,86.651,74.682,86.201,74.816,86.12,74.757,86.059,74.728,85.881,74.74,85.791,74.645,86.116,74.629,86.426,74.585,86.7,74.522,86.894,74.45,87.106,74.404,87.23,74.364,86.898,74.325,86.665,74.414,86.538,74.444,86.396,74.45,86.183,74.423,86.001,74.316,86.178,74.279,86.571,74.244,86.697,74.195,87.21,73.879,87.338,73.846,87.503,73.832,87.571,73.811,87.37,73.756,87.294,73.705,87.12,73.615,86.376,73.569,86.155,73.535,86.092,73.519,85.999,73.486,85.939,73.456,85.91,73.39,85.971,73.347,86.122,73.307,86.715,73.126,86.677,73.107,86.514,73.14,86.308,73.196,86.098,73.273,85.818,73.327,85.802,73.372,85.793,73.438,85.8,73.459,85.827,73.493,86.094,73.578,86.366,73.62,86.698,73.717,87.029,73.824,86.961,73.861,86.893,73.887,86.591,73.894,85.979,73.857,85.611,73.822,85.448,73.735,85.201,73.722,85.077,73.72,84.738,73.763,84.417,73.722,83.667,73.686,83.545,73.667,81.817,73.659,81.469,73.64,80.583,73.568,80.562,73.515,80.596,73.474,80.458,73.414,80.398,73.357,80.419,73.29,80.425,73.231,80.455,73.155,80.51,73.086,80.639,73.049,80.757,73.025,80.842,72.949,80.774,72.861,80.675,72.759,80.656,72.712,80.72,72.648,80.798,72.52,80.827,72.488,81.098,72.39,81.283,72.359,81.586,72.352,81.793,72.327,82.094,72.265,82.184,72.238,82.209,72.211,82.281,72.105,82.319,72.072,82.645,71.925,82.755,71.903,83.2,71.875,83.34,71.828,83.534,71.684,83.554,71.65,83.571,71.594,83.55,71.544,83.531,71.514,83.458,71.468,83.266,71.276,83.151,71.104,83.334,70.989,83.579,70.766,83.651,70.672,83.736,70.546,83.7,70.466,83.66,70.418,83.497,70.345,83.293,70.321,83.074,70.277,83.094,70.221,83.132,70.157,83.11,70.11,83.081,70.093,82.961,70.088,82.857,70.105,82.767,70.154,82.682,70.218,82.742,70.286,82.92,70.407,83.03,70.581,83.058,70.695,83.051,70.815,83.01,70.895,82.869,70.955,82.738,70.942,82.592,70.89,82.452,70.69,82.258,70.544,82.231,70.483,82.236,70.43,82.221,70.396,82.182,70.511,82.163,70.598,82.271,70.707,82.336,70.807,82.316,70.879,82.239,70.998,82.254,71.056,82.277,71.093,82.323,71.26,82.493,71.293,82.918,71.42,82.977,71.451,83.106,71.562,83.166,71.602,83.234,71.668,83.107,71.721,82.986,71.749,82.758,71.764,82.547,71.759,82.08,71.707,81.662,71.716,81.511,71.746,80.856,71.97,80.815,72.054,80.763,72.089,80.699,72.098,80.474,72.153,79.954,72.223,79.422,72.381,78.483,72.395,78.225,72.377,77.968,72.329,77.733,72.229,77.625,72.201,77.472,72.192,77.44,72.157,77.411,72.108,77.493,72.072,77.781,72.114,78.016,72.092,78.141,72.045,78.232,71.952,78.187,71.907,77.778,71.836,77.551,71.842,77.061,72.004,76.871,72.033,76.422,72.006,76.124,71.927,76.032,71.91,76.104,71.829,76.216,71.683,76.312,71.595,76.433,71.552,76.871,71.447,77.114,71.409,77.481,71.312,77.707,71.301,77.908,71.324,78.213,71.266,78.387,71.087,78.491,71.025,78.588,70.994,78.724,70.976,78.804,70.974,78.889,70.997,79.084,71.002,79.015,70.95,78.942,70.934,78.526,70.912,78.321,70.93,78.068,70.986,77.59,71.168,76.995,71.181,76.929,71.128,76.742,71.202,76.11,71.219,75.734,71.266,75.332,71.342,75.298,71.378,75.28,71.43,75.417,71.495,75.469,71.534,75.503,71.655,75.247,71.813,75.274,71.959,75.395,71.983,75.55,72.171,75.644,72.232,75.694,72.254,75.741,72.296,75.691,72.35,75.644,72.382,75.591,72.457,75.603,72.512,75.604,72.581,75.475,72.685,75.369,72.797,75.152,72.853,74.942,72.854,74.865,72.838,74.787,72.812,74.897,72.71,75.008,72.619,75.06,72.549,75.097,72.421,75.09,72.263,75.053,72.199,74.992,72.145,74.804,72.077,74.489,71.997,74.311,71.958,73.939,71.915,73.672,71.845,73.086,71.445,73.15,71.385,73.365,71.32,73.507,71.264,73.577,71.217,73.732,71.069,74.311,70.654,74.343,70.579,74.207,70.445,73.937,70.273,73.83,70.176,73.578,69.803,73.56,69.707,73.663,69.617,73.833,69.504,73.891,69.418,73.776,69.198,73.836,69.143,73.977,69.115,74.363,69.145,74.815,69.091,75.054,69.116,75.42,69.239,75.561,69.252,76.001,69.235,76.645,69.117,77.328,68.959,77.466,68.905,77.651,68.903,77.785,68.63,77.907,68.482,77.959,68.377,77.995,68.259,77.868,68.235,77.757,68.222,77.665,68.19,77.536,68.008,77.52,67.91,77.588,67.752,78.161,67.678,78.559,67.639,78.839,67.631,78.888,67.613,78.922,67.589,78.59,67.578,77.986,67.559,77.772,67.57,77.675,67.59,77.579,67.644,77.396,67.699,77.325,67.736,77.174,67.779,77.248,67.941,77.261,68.316,77.238,68.47,77.112,68.596,76.735,68.777,76.606,68.898,76.459,68.978,76.316,68.992,76.108,68.976,75.59,68.901,75.125,68.862,74.58,68.751,74.481,68.659,74.391,68.421,74.511,68.303,74.632,68.218,74.743,68.074,74.778,67.986,74.787,67.898,74.77,67.766,74.676,67.695,74.075,67.414,73.986,67.328,73.883,67.085,73.792,66.995,73.514,66.861,73.342,66.807,72.417,66.561,72.384,66.507,72.322,66.332,72.068,66.253,71.917,66.247,71.566,66.334,71.358,66.359,71.146,66.367,70.339,66.342,69.982,66.401,69.701,66.485,69.412,66.511,69.194,66.579,69.144,66.641,69.091,66.724,69.051,66.766,69.013,66.788,69.079,66.816,69.218,66.829,69.74,66.815,69.877,66.845,69.949,66.83,70.094,66.754,70.283,66.686,70.444,66.697,70.579,66.754,70.631,66.754,70.691,66.745,70.568,66.701,70.443,66.668,70.409,66.648,70.383,66.602,70.561,66.549,70.725,66.519,70.939,66.548,71.066,66.604,71.342,66.687,71.54,66.683,71.551,66.76,71.449,66.879,71.365,66.962,71.668,66.94,71.847,67.008,72.594,67.587,72.949,67.696,73.067,67.767,73.152,67.865,73.173,67.973,73.129,68.091,73.139,68.181,73.266,68.294,73.465,68.431,73.592,68.482,73.573,68.533,73.548,68.575,73.191,68.707,72.812,68.815,72.678,68.875,72.577,68.969,72.527,69.081,72.527,69.154,72.557,69.378,72.616,69.484,72.599,69.793,72.53,70.173,72.469,70.275,72.562,70.346,72.653,70.403,72.7,70.457,72.732,70.823,72.704,70.963,72.581,71.151,72.079,71.307,71.867,71.457,71.884,71.511,71.912,71.548,72.13,71.609,72.279,71.696,72.375,71.822,72.574,72.013,72.624,72.079,72.753,72.343,72.787,72.483,72.812,72.691,72.634,72.744,72.446,72.79,72.101,72.829,71.93,72.82,71.617,72.902,71.5,72.914,70.655,72.89,70.172,72.901,69.888,72.883,69.738,72.885,69.645,72.898,69.659,72.932,69.709,72.956,69.694,72.978,69.612,72.982,69.391,72.956,69.039,72.67,68.83,72.392,68.607,72.013,68.469,71.853,68.269,71.683,67.959,71.548,67.542,71.412,67.274,71.348,66.918,71.282,66.768,71.14,66.64,71.081,66.693,71.042,66.847,71.064,66.759,70.962,66.666,70.901,66.675,70.865,66.702,70.819,66.822,70.797,67.143,70.838,67.212,70.798,67.285,70.739,67.247,70.5,67.157,70.295,67.146,70.22,67.197,70.172,67.239,70.108,67.144,70.031,67.069,70.006,66.926,70.014,66.832,69.842,66.803,69.74,66.804,69.659,66.84,69.609,66.897,69.554,66.934,69.597,66.964,69.656,67.064,69.694,67.624,69.584,67.774,69.53,68.006,69.48,68.073,69.421,68.117,69.236,68.355,69.068,68.543,68.967,68.66,68.927,68.763,68.917,68.924,68.956,69.141,68.951,69.024,68.818,68.829,68.567,68.504,68.348,68.371,68.314,68.157,68.404,67.731,68.514,67.64,68.579,67.149,68.754,67.002,68.874,66.756,68.892,66.416,68.948,66.085,69.036,65.813,69.077,65.736,69.132,65.528,69.173,65.327,69.201,65.032,69.27,64.896,69.248,64.929,69.325,64.592,69.436,64.19,69.535,63.361,69.675,62.631,69.743,61.771,69.763,61.016,69.851,60.909,69.847,60.813,69.821,60.559,69.692,60.276,69.653,60.171,69.591,60.337,69.457,60.665,69.11,60.859,69.146,60.934,68.987,60.815,68.895,60.638,68.787,60.489,68.729,60.16,68.7,59.896,68.706,59.865,68.605,59.941,68.51,59.923,68.471,59.897,68.422,59.859,68.396,59.828,68.38,59.726,68.352,59.604,68.351,59.311,68.4,59.099,68.444,59.112,68.616,59.223,68.691,59.298,68.708,59.371,68.738,59.22,68.85,59.11,68.896,59.06,68.973,59.057,69.006,58.919,69.004,58.354,68.916,58.237,68.834,58.173,68.89,57.444,68.642,57.127,68.554,56.909,68.567,56.62,68.619,56.276,68.624,56.044,68.649,55.925,68.637,55.675,68.576,55.418,68.568,55.151,68.48,54.923,68.374,54.861,68.202,54.718,68.184,54.561,68.273,54.476,68.294,54.394,68.275,54.233,68.266,54.099,68.259,53.968,68.227,53.914,68.231,53.515,68.26,53.403,68.257,53.261,68.267,53.293,68.312,53.343,68.343,53.567,68.367,53.69,68.403,53.829,68.383,53.931,68.436,53.918,68.537,53.759,68.634,53.834,68.709,53.891,68.802,53.929,68.812,53.971,68.844,53.92,68.871,53.798,68.885,53.798,68.907,53.874,68.927,54.376,68.965,54.491,68.992,54.186,69.003,53.802,68.996,53.413,68.913,52.684,68.731,52.344,68.608,52.435,68.61,52.55,68.592,52.648,68.506,52.723,68.484,52.67,68.427,52.475,68.382,52.397,68.352,52.322,68.34,52.259,68.351,52.183,68.374,52.227,68.419,52.285,68.459,52.129,68.532,52.056,68.541,51.995,68.539,51.617,68.476,51.336,68.402,51.079,68.363,50.839,68.35,50.699,68.318,50.414,68.218,50.233,68.175,49.931,68.065,49.155,67.87,48.954,67.854,48.841,67.87,48.754,67.896,48.696,67.874,48.763,67.827,48.878,67.731,48.833,67.681,48.654,67.695,48.279,67.65,47.875,67.584,47.883,67.515,47.908,67.455,47.839,67.356,47.768,67.276,47.709,67.045,47.656,66.976,47.496,66.93,46.691,66.826,46.552,66.819,46.492,66.8,46.449,66.819,46.298,66.843,46.084,66.844,45.986,66.853,45.885,66.891,45.753,66.989,45.562,67.186,45.139,67.285,44.939,67.351,44.902,67.413,44.939,67.477,45.374,67.689,45.529,67.758,46.174,67.818,46.429,67.824,46.69,67.849,46.684,67.97,46.43,68.119,46.158,68.291,45.892,68.48,45.519,68.547,45.078,68.578,44.175,68.542,44.048,68.549,43.472,68.68,43.413,68.682,43.333,68.673,43.358,68.636,43.404,68.609,44.169,68.327,44.205,68.254,44.226,68.154,44.214,68.113,44.232,68.071,44.225,67.996,44.036,67.671,43.856,67.439,43.796,67.33,43.782,67.254,43.855,67.189,44.074,67.167,44.292,67.1,44.404,67.004,44.429,66.938,44.437,66.795,44.489,66.672,44.316,66.482,44.221,66.407,44.097,66.235,44.145,66.113,44.132,66.065,44.104,66.009,44.017,66.05,43.944,66.099,43.844,66.142,43.737,66.158,43.624,66.147,43.542,66.123,43.55,66.173,43.653,66.251,43.603,66.291,43.551,66.321,43.233,66.416,43.006,66.421,42.807,66.411,42.602,66.423,42.451,66.482,42.314,66.515,42.211,66.52,42.084,66.466,41.781,66.259,41.476,66.123,41.076,66.021,40.774,65.988,40.692,65.963,40.513,65.844,40.328,65.752,39.817,65.598,39.781,65.535,39.749,65.448,39.798,65.35,39.896,65.255,40.143,65.063,40.281,64.998,40.375,64.896,40.445,64.779,40.408,64.755,40.204,64.784,40.058,64.771,39.849,64.691,39.833,64.656,39.758,64.577,39.567,64.571,39.054,64.714,38.613,64.787,38.541,64.791,38.442,64.827,38.412,64.857,38.228,64.851,38.116,64.855,38.009,64.879,37.528,65.108,37.141,65.194,37.05,65.196,36.883,65.172,36.786,64.987,36.653,64.935,36.535,64.939,36.528,64.847,36.579,64.791,36.624,64.751,36.769,64.685,37.04,64.489,37.184,64.408,37.29,64.378,37.43,64.374,37.741,64.397,37.844,64.366,37.954,64.32,37.977,64.207,38.062,64.091,38.071,64.026,37.968,63.949,37.635,63.893,37.442,63.813,37.373,63.817,36.975,63.91,36.714,63.945,36.365,64.003,36.302,64.034,36.146,64.189,35.802,64.335,35.647,64.378,35.432,64.347,35.284,64.363,35.035,64.44,34.87,64.56,34.858,64.707,34.905,64.739,34.952,64.756,34.833,64.8,34.827,64.913,34.804,64.986,34.671,65.168,34.536,65.278,34.406,65.396,34.544,65.457,34.616,65.51,34.716,65.664,34.735,65.716,34.777,65.768,34.793,65.816,34.786,65.865,34.692,65.952,34.4,66.128,34.113,66.225,33.567,66.321,33.416,66.316,33.361,66.33,33.477,66.347,33.593,66.385,33.656,66.443,33.518,66.471,33.405,66.484,33.217,66.532,33.183,66.574,33.224,66.604,33.181,66.68,32.929,66.704,32.862,66.721,32.857,66.747,32.686,66.83,32.464,66.916,32.501,67.004,32.341,67.068,32.202,67.113,31.983,67.13,31.895,67.161,32.4,67.153,32.93,67.087,32.885,67.061,32.848,67.022,33.002,66.908,33.15,66.844,33.482,66.765,33.523,66.764,33.595,66.785,33.76,66.751,33.894,66.707,34.146,66.703,34.452,66.651,34.431,66.63,34.396,66.613,34.483,66.55,34.61,66.56,34.825,66.611,35.364,66.429,35.513,66.396,36.373,66.302,36.77,66.294,36.984,66.273,37.295,66.225,37.628,66.13,37.901,66.096,38.398,66.064,38.654,66.069,39.289,66.132,40.103,66.3,40.522,66.447,41.189,66.826,41.276,66.914,41.354,67.121,41.359,67.21,41.262,67.218,41.134,67.267,41.134,67.386,41.061,67.444,40.966,67.713,40.766,67.743,40.657,67.774,40.526,67.79,40.381,67.832,40.207,67.942,40.036,68.015,39.896,68.115,39.809,68.151,39.746,68.162,39.79,68.112,39.823,68.059,39.569,68.072,38.832,68.325,38.706,68.345,38.657,68.322,38.43,68.356,38.358,68.415,37.731,68.692,36.618,69.003,35.858,69.192,35.29,69.275,35.233,69.266,35.176,69.231,35.01,69.221,34.864,69.228,34.353,69.303,34.229,69.313,33.684,69.31,33.627,69.289,33.436,69.13,33.333,69.098,33.141,69.069,33.196,69.117,33.328,69.152,33.413,69.267,33.418,69.315,33.464,69.378,33.454,69.428,33.385,69.444,33.256,69.428,32.979,69.367,32.942,69.383,33.021,69.446,33,69.47,32.884,69.461,32.637,69.489,32.378,69.479,32.331,69.554,32.161,69.597,32.092,69.633,32.177,69.674,32.754,69.606,32.915,69.602,32.995,69.626,33.013,69.671,33.008,69.722,32.942,69.752,32.565,69.806,32.392,69.869,31.985,69.954,31.969,69.914,32.031,69.835,31.998,69.81,31.879,69.832,31.789,69.816,31.666,69.721,31.547,69.697,31.453,69.69,31.05,69.769,30.87,69.783,30.714,69.796,30.595,69.79,30.484,69.795,30.428,69.722,30.397,69.733,30.349,69.835,30.238,69.862,30.18,69.841,30.155,69.746,30.088,69.718,29.99,69.737,29.792,69.728,29.695,69.745,29.636,69.78,29.621,69.818,29.621,69.874,29.647,69.944,29.601,69.977,28.804,70.093,28.781,70.145,29.926,70.096,30.263,70.125,30.469,70.198,30.944,70.274,30.961,70.344,30.926,70.401,30.596,70.524,30.422,70.547,30.213,70.543,30.203,70.562,30.238,70.622,30.065,70.703,29.959,70.694,29.796,70.643,29.738,70.647,29.722,70.669,29.639,70.705,29.398,70.734,29.321,70.761,29.219,70.83,29.102,70.861,28.832,70.864,28.75,70.842,28.609,70.76,28.485,70.619,28.437,70.501,28.31,70.443,28.28,70.403,28.193,70.249,28.166,70.288,28.166,70.36,28.191,70.44,28.203,70.577,28.272,70.668,28.216,70.704,27.999,70.664,27.898,70.678,27.951,70.718,28.272,70.798,28.327,70.825,28.383,70.869,28.392,70.975,28.142,71.043,27.815,71.059,27.733,71.081,27.597,71.091,27.332,70.997,27.235,70.947,27.269,70.91,27.556,70.827,27.546,70.804,27.309,70.804,27.184,70.744,27.147,70.681,27.071,70.608,26.989,70.511,26.666,70.422,26.585,70.41,26.584,70.454,26.601,70.503,26.628,70.551,26.645,70.636,26.558,70.669,26.675,70.741,26.734,70.854,26.661,70.94,26.507,70.913,26.231,70.783,25.988,70.625,25.471,70.341,25.419,70.235,25.212,70.136,25.044,70.109,24.983,70.144,24.994,70.218,25.146,70.324,25.209,70.489,25.274,70.552,25.468,70.672,25.666,70.777,25.781,70.817,25.768,70.853,25.712,70.87,25.65,70.873,25.57,70.901,25.436,70.912,25.376,70.892,25.325,70.849,25.265,70.844,25.171,70.872,25.042,70.929,24.832,70.978,24.765,71.008,24.658,71.001,24.442,70.892,24.263,70.826,24.268,70.773,24.404,70.745,24.42,70.702,24.356,70.695,24.286,70.662,24.038,70.485,23.897,70.479,23.661,70.4,23.379,70.247,23.329,70.207,23.286,70.105,23.31,70.064,23.4,70.02,23.354,69.983,23.258,69.993,23.177,70.029,23.046,70.102,22.983,70.237,22.941,70.305,22.852,70.34,22.685,70.375,22.421,70.338,22.385,70.278,22.322,70.265,22.219,70.309,22.054,70.276,21.996,70.293,21.78,70.23,21.539,70.258,21.356,70.233,21.346,70.208,21.4,70.174,21.608,70.098,21.803,70.066,21.893,70.004,21.975,69.835,21.932,69.815,21.78,69.887,21.59,69.938,21.433,70.013,21.254,70.003,21.163,69.89,21.032,69.887,20.971,69.916,20.84,69.907,20.622,69.914,20.546,69.851,20.533,69.692,20.563,69.633,20.662,69.585,20.743,69.535,20.739,69.521,20.487,69.542,20.198,69.371,20.107,69.341,20.054,69.333,20.044,69.356,20.277,69.536,20.338,69.617,20.333,69.677,20.387,69.868,20.355,69.922,20.324,69.945,20.223,69.927,20.146,69.897,20.069,69.883,19.961,69.825,19.865,69.722,19.737,69.504,19.642,69.424,19.64,69.504,19.696,69.613,19.722,69.782,19.687,69.805,19.197,69.748,19.038,69.66,19.011,69.624,19.007,69.588,18.991,69.561,18.883,69.523,18.767,69.517,18.674,69.52,18.614,69.491,18.624,69.434,18.75,69.378,18.916,69.336,18.859,69.314,18.646,69.322,18.483,69.365,18.379,69.44,18.293,69.475,18.26,69.471,18.188,69.433,18.079,69.325,18.075,69.233,18.117,69.181,18.101,69.156,17.705,69.1,17.546,69.001,17.49,68.879,17.391,68.799,17.131,68.693,16.885,68.685,16.652,68.626,16.58,68.593,16.514,68.533,16.525,68.491,16.585,68.466,17.202,68.459,17.426,68.482,17.48,68.474,17.502,68.461,17.571,68.447,17.553,68.426,17.479,68.426,17.336,68.41,17.094,68.368,16.951,68.355,16.865,68.355,16.619,68.406,16.388,68.39,16.204,68.317,16.175,68.281,16.26,68.145,16.319,68.102,16.392,68.092,16.372,68.062,16.309,68.036,16.259,68.001,16.312,67.881,16.261,67.887,16.121,68.027,16.065,68.2,16.038,68.218,16.008,68.229,15.851,68.182,15.657,68.164,15.487,68.103,15.316,68.069,15.293,68.036,15.357,68.004,15.606,67.988,15.621,67.948,15.507,67.926,15.401,67.92,15.274,67.961,15.134,67.973,15.048,67.956,14.799,67.809,14.821,67.75,14.781,67.675,14.855,67.663,15.041,67.683,15.304,67.765,15.346,67.734,15.284,67.708,15.219,67.655,15.249,67.602,15.354,67.544,15.487,67.515,15.661,67.543,15.692,67.521,15.576,67.444,15.594,67.349,15.553,67.352,15.465,67.451,15.409,67.474,15.289,67.483,15.121,67.555,14.962,67.574,14.755,67.499,14.579,67.386,14.537,67.34,14.448,67.298,14.442,67.271,14.479,67.256,14.582,67.267,14.824,67.268,15.3,67.257,15.435,67.247,15.416,67.202,14.776,67.194,14.601,67.174,14.473,67.143,14.34,67.159,14.206,67.111,14.109,67.119,14.022,67.073,13.88,66.965,13.808,66.961,13.727,66.938,13.652,66.907,13.704,66.852,13.917,66.819,13.959,66.794,13.788,66.782,13.621,66.795,13.52,66.742,13.45,66.716,13.312,66.702,13.211,66.641,13.192,66.537,13.105,66.539,13.068,66.431,13.119,66.231,13.352,66.237,13.416,66.253,13.499,66.252,13.681,66.274,13.973,66.32,14.034,66.298,13.916,66.247,13.76,66.221,13.674,66.18,13.387,66.183,12.784,66.1,12.795,66.069,12.976,66.019,13.033,65.956,12.983,65.942,12.817,65.953,12.689,65.902,12.628,65.806,12.345,65.63,12.273,65.568,12.206,65.486,12.122,65.362,12.134,65.279,12.2,65.245,12.263,65.256,12.334,65.241,12.364,65.193,12.418,65.184,12.512,65.195,12.715,65.266,12.82,65.317,12.916,65.339,12.738,65.214,12.508,65.099,12.307,65.086,12.227,65.145,12.16,65.179,11.489,64.976,11.35,64.906,11.304,64.829,11.297,64.755,11.392,64.773,11.562,64.818,11.633,64.814,11.524,64.744,11.331,64.686,11.225,64.679,11.09,64.615,10.932,64.578,10.834,64.494,10.566,64.418,10.236,64.18,10.01,64.083,9.939,63.982,9.864,63.918,9.708,63.865,9.615,63.795,9.567,63.706,9.595,63.679,9.657,63.697,9.767,63.7,9.832,63.617,9.893,63.576,9.924,63.522,10.055,63.513,10.339,63.571,10.935,63.77,11.047,63.845,10.967,63.902,10.914,63.921,11.075,63.988,11.214,64.031,11.307,64.049,11.429,64.025,11.458,64.003,11.295,63.948,11.176,63.899,11.214,63.878,11.308,63.876,11.348,63.838,11.371,63.805,11.226,63.764,11.118,63.719,10.953,63.698,10.779,63.651,10.725,63.625,10.674,63.558,10.707,63.536,10.76,63.461,10.704,63.464,10.591,63.447,10.34,63.469,10.189,63.455,10.081,63.433,10.021,63.391,9.979,63.395,9.936,63.479,9.892,63.492,9.832,63.524,9.697,63.625,9.602,63.61,9.521,63.586,9.324,63.57,9.156,63.459,9.084,63.463,9.076,63.5,9.158,63.566,9.136,63.594,8.842,63.646,8.674,63.623,8.576,63.601,8.398,63.535,8.361,63.499,8.387,63.445,8.48,63.424,8.594,63.426,8.641,63.392,8.636,63.342,8.58,63.313,8.271,63.287,8.184,63.237,8.158,63.162,8.235,63.082,8.339,63.042,8.609,62.881,8.623,62.846,8.311,62.966,8.211,62.996,8.101,63.091,7.86,63.113,7.736,63.104,7.654,63.109,7.572,63.1,7.389,63.023,7.008,62.958,6.94,62.93,6.928,62.903,6.782,62.79,6.735,62.721,6.78,62.701,7.025,62.729,7.111,62.752,7.242,62.752,7.408,62.712,8.046,62.771,8.096,62.732,7.805,62.721,7.538,62.672,7.518,62.646,7.527,62.61,7.691,62.586,7.653,62.564,7.57,62.548,7.492,62.543,7.284,62.602,6.961,62.627,6.745,62.638,6.618,62.621,6.439,62.61,6.353,62.611,6.273,62.584,6.238,62.52,6.165,62.482,6.118,62.447,6.136,62.407,6.262,62.416,6.457,62.448,6.692,62.468,6.62,62.423,6.58,62.407,6.209,62.353,6.083,62.35,6.026,62.376,5.98,62.407,5.908,62.416,5.796,62.385,5.718,62.379,5.533,62.311,5.484,62.239,5.422,62.207,5.358,62.152,5.294,62.154,5.241,62.189,5.143,62.16,5.096,62.027,5.16,61.957,5.267,61.936,5.473,61.946,5.664,61.923,6.131,61.852,6.396,61.851,6.682,61.887,6.731,61.87,6.467,61.807,6.016,61.788,5.793,61.827,5.465,61.897,5.117,61.885,4.985,61.9,4.93,61.878,4.91,61.81,4.928,61.711,4.997,61.645,5.099,61.62,5.168,61.543,5.268,61.505,5.339,61.485,5.258,61.455,5.172,61.457,5.003,61.434,4.99,61.378,5.022,61.251,5.107,61.188,5.325,61.108,5.451,61.102,5.647,61.148,6.083,61.167,6.383,61.134,6.493,61.155,6.543,61.245,6.6,61.29,6.626,61.279,6.61,61.229,6.657,61.207,6.794,61.19,6.943,61.161,7.174,61.166,7.276,61.181,7.298,61.214,7.276,61.284,7.331,61.372,7.443,61.435,7.453,61.419,7.347,61.301,7.404,61.222,7.604,61.211,7.545,61.177,7.04,61.091,7.047,61.015,7.078,60.966,7.039,60.953,6.981,60.994,6.972,61.056,6.903,61.102,6.778,61.142,6.61,61.137,6.418,61.084,6.293,61.081,5.984,61.117,5.505,61.056,5.288,61.047,5.192,61.054,5.095,61.071,5.009,61.038,5.025,60.936,5.011,60.859,5.049,60.708,5.116,60.636,5.244,60.57,5.447,60.617,5.589,60.694,5.648,60.688,5.546,60.625,5.168,60.485,5.137,60.446,5.184,60.308,5.286,60.206,5.417,60.154,5.574,60.158,5.658,60.154,5.689,60.123,5.495,60.07,5.376,60.067,5.265,60.086,5.206,60.088,5.174,60.046,5.22,59.979,5.187,59.907,5.119,59.834,5.105,59.732,5.111,59.668,5.146,59.639,5.186,59.642,5.234,59.692,5.264,59.71,5.353,59.76,5.495,59.826,5.557,59.908,5.699,60.01,5.801,60.026,5.877,60.07,5.904,60.151,5.967,60.206,6.102,60.29,6.153,60.346,6.347,60.419,6.806,60.501,6.996,60.512,6.95,60.478,6.787,60.454,6.72,60.418,6.661,60.367,6.527,60.153,6.526,60.214,6.574,60.361,6.518,60.408,6.349,60.353,6.141,60.233,6.105,60.165,6.112,60.132,6.07,60.083,5.996,60.031,5.784,59.913,5.73,59.863,5.763,59.808,5.834,59.795,5.967,59.813,6.059,59.816,6.212,59.832,6.217,59.818,5.991,59.745,5.867,59.734,5.772,59.661,5.579,59.687,5.53,59.713,5.472,59.713,5.404,59.656,5.305,59.643,5.242,59.564,5.185,59.454,5.132,59.226,5.173,59.163,5.362,59.166,5.468,59.204,5.564,59.291,5.657,59.31,5.718,59.33,5.845,59.353,6.017,59.414,6.159,59.49,6.279,59.535,6.404,59.561,6.415,59.547,6.306,59.506,6.199,59.438,6.051,59.368,5.952,59.299,5.937,59.234,5.969,59.186,5.949,59.135,5.889,59.098,5.889,59.06,6.017,58.988,6.099,58.952,6.321,59.016,6.363,59.001,6.214,58.945,6.137,58.875,6.099,58.87,5.854,58.959,5.612,59.013,5.556,58.975,5.522,58.823,5.517,58.727,5.586,58.62,5.707,58.524,5.977,58.432,6.055,58.375,6.389,58.268,6.492,58.259,6.618,58.266,6.66,58.263,6.677,58.234,6.692,58.224,6.606,58.176,6.555,58.123,6.591,58.097,6.731,58.068,6.767,58.082,6.771,58.132,6.803,58.155,6.877,58.151,6.912,58.143,6.895,58.121,6.89,58.102,6.903,58.071,7.005,58.024,7.194,58.048,7.466,58.021,7.876,58.08,8.037,58.147,8.166,58.145,8.312,58.224,8.521,58.301,8.928,58.57,9.178,58.675,9.194,58.712,9.239,58.739,9.323,58.748,9.396,58.806,9.31,58.857,9.551,58.933,9.618,58.946,9.657,58.971,9.696,59.01,9.627,59.068,9.557,59.113,9.635,59.118,9.8,59.027,9.843,58.958,9.96,58.968,10.083,59.029,10.179,59.009,10.205,59.039,10.243,59.062,10.431,59.28,10.455,59.377,10.446,59.444,10.407,59.456,10.398,59.519,10.494,59.542,10.57,59.587,10.534,59.696,10.595,59.765,10.604,59.68,10.634,59.602,10.631,59.428,10.645,59.389,10.743,59.296,10.834,59.184,10.945,59.171,10.999,59.164,11.091,59.142,11.132,59.143,11.366,59.105,11.386,59.066,11.388,59.037,11.295,59.087,11.196,59.078,11.167,59.046,11.147,58.989,11.169,58.923,11.208,58.866,11.224,58.68,11.272,58.476,11.252,58.424,11.248,58.369,11.33,58.38,11.432,58.34,11.449,58.118,11.549,58.001,11.703,57.973,11.729,57.764,11.735,57.718,11.879,57.679,11.885,57.613,11.917,57.522,11.962,57.426,12.053,57.447,12.152,57.227,12.421,56.906,12.573,56.823,12.718,56.663,12.793,56.649,12.884,56.618,12.92,56.516,12.857,56.452,12.773,56.456,12.656,56.441,12.691,56.384,12.742,56.347,12.802,56.264,12.753,56.242,12.706,56.235,12.507,56.293,12.471,56.291,12.521,56.246,12.593,56.138,12.835,55.882,12.942,55.806,12.974,55.748,12.978,55.694,12.963,55.613,12.939,55.533,12.941,55.482,12.886,55.411,13.321,55.346,13.806,55.429,14.08,55.392,14.174,55.397,14.342,55.528,14.276,55.636,14.203,55.729,14.215,55.833,14.262,55.888,14.402,55.977,14.473,56.014,14.559,56.049,14.656,56.02,14.755,56.033,14.714,56.134,14.782,56.162,15.051,56.172,15.327,56.151,15.51,56.183,15.627,56.186,15.722,56.164,15.827,56.125,15.92,56.167,15.997,56.223,16.151,56.501,16.217,56.59,16.349,56.709,16.408,56.809,16.458,56.927,16.528,57.068,16.507,57.142,16.479,57.188,16.476,57.265,16.631,57.43,16.652,57.501,16.604,57.568,16.584,57.642,16.586,57.761,16.555,57.812,16.597,57.913,16.695,57.918,16.7,58.161,16.77,58.214,16.717,58.303,16.652,58.434,16.824,58.46,16.924,58.493,16.788,58.585,16.683,58.6,16.478,58.613,16.391,58.602,16.318,58.628,16.214,58.637,16.316,58.664,16.639,58.651,16.978,58.654,17.103,58.711,17.348,58.781,17.457,58.858,17.67,58.916,17.765,58.965,17.829,58.955,17.975,59.003,18.098,59.062,18.285,59.109,18.322,59.132,18.373,59.18,18.414,59.29,18.499,59.292,18.618,59.327,18.56,59.394,18.509,59.408,18.459,59.397,18.396,59.369,18.336,59.375,18.271,59.367,18.211,59.331,18.133,59.316,17.98,59.329,17.964,59.359,18.164,59.43,18.217,59.421,18.276,59.438,18.338,59.477,18.402,59.49,18.578,59.566,18.64,59.601,18.719,59.657,18.896,59.733,18.971,59.757,18.99,59.828,18.933,59.942,18.884,59.98,18.853,60.026,18.787,60.079,18.601,60.119,18.535,60.153,18.558,60.254,18.4,60.337,18.25,60.362,18.163,60.408,18.011,60.511,17.956,60.59,17.872,60.58,17.742,60.539,17.661,60.535,17.631,60.585,17.593,60.628,17.555,60.643,17.457,60.642,17.36,60.641,17.251,60.701,17.261,60.763,17.279,60.812,17.203,60.952,17.213,60.986,17.186,61.147,17.18,61.249,17.164,61.278,17.2,61.312,17.178,61.358,17.138,61.382,17.164,61.458,17.147,61.505,17.131,61.576,17.216,61.656,17.196,61.725,17.335,61.692,17.465,61.684,17.417,61.741,17.398,61.782,17.375,61.866,17.412,61.966,17.447,62.023,17.51,62.166,17.563,62.212,17.634,62.233,17.535,62.264,17.429,62.335,17.373,62.427,17.378,62.463,17.41,62.508,17.509,62.483,17.571,62.451,17.646,62.451,17.718,62.501,17.834,62.503,17.947,62.578,18.037,62.601,18.007,62.626,17.93,62.641,17.903,62.659,17.941,62.68,17.974,62.721,17.933,62.786,17.896,62.831,17.88,62.873,17.907,62.887,17.951,62.834,18.094,62.836,18.078,62.812,18.074,62.791,18.17,62.789,18.215,62.812,18.248,62.849,18.463,62.896,18.483,62.928,18.487,62.959,18.502,62.989,18.313,62.996,18.344,63.032,18.408,63.038,18.531,63.064,18.578,63.126,18.606,63.178,18.667,63.177,18.76,63.198,18.819,63.197,18.859,63.207,18.85,63.224,18.792,63.238,18.817,63.257,19.034,63.238,19.236,63.347,19.288,63.429,19.354,63.477,19.495,63.424,19.491,63.46,19.502,63.509,19.59,63.487,19.656,63.458,19.722,63.463,19.782,63.538,19.914,63.611,20.205,63.662,20.371,63.723,20.454,63.774,20.678,63.826,20.763,63.868,21.018,64.178,21.256,64.299,21.465,64.38,21.494,64.416,21.52,64.463,21.394,64.544,21.332,64.629,21.279,64.725,21.205,64.774,21.138,64.809,21.196,64.877,21.294,64.941,21.425,65.013,21.574,65.126,21.581,65.161,21.546,65.207,21.506,65.245,21.438,65.283,21.41,65.317,21.447,65.321,21.567,65.255,21.609,65.261,21.613,65.299,21.596,65.317,21.545,65.331,21.523,65.359,21.533,65.387,21.566,65.408,21.681,65.403,21.88,65.424,21.913,65.437,21.95,65.47,21.903,65.508,21.92,65.532,22.087,65.53,22.148,65.553,22.133,65.57,22.096,65.584,22.086,65.611,22.254,65.598,22.267,65.622,22.275,65.725,22.288,65.751,22.336,65.791,22.366,65.843,22.401,65.862,22.465,65.853,22.539,65.794,22.62,65.807,22.747,65.871,22.919,65.786,23.102,65.735,23.155,65.75,23.221,65.786,23.418,65.804,23.592,65.805,23.691,65.829,23.891,65.782,24.155,65.805,24.238,65.812,24.404,65.78,24.533,65.822,24.592,65.858,24.628,65.859,24.623,65.832,24.582,65.757,24.675,65.671,24.764,65.656,24.839,65.66,25.242,65.546,25.348,65.479,25.308,65.353,25.298,65.243,25.256,65.143,25.34,65.099,25.362,65.065,25.373,65.009,25.271,64.984,25.228,64.951,25.281,64.916,25.288,64.86,25.214,64.853,25.134,64.875,24.942,64.884,24.748,64.852,24.658,64.806,24.558,64.801,24.53,64.739,24.441,64.68,24.278,64.515,24.022,64.386,23.925,64.274,23.861,64.258,23.653,64.134,23.599,64.041,23.494,64.034,23.249,63.896,23.134,63.865,23.014,63.822,22.756,63.683,22.532,63.648,22.528,63.58,22.398,63.491,22.319,63.504,22.313,63.473,22.346,63.442,22.273,63.455,22.243,63.438,22.286,63.377,22.316,63.346,22.32,63.31,22.12,63.244,21.896,63.21,21.8,63.238,21.545,63.204,21.549,63.156,21.569,63.114,21.651,63.039,21.474,63.033,21.458,62.95,21.196,62.791,21.144,62.74,21.118,62.689,21.104,62.623,21.142,62.515,21.166,62.414,21.323,62.343,21.343,62.277,21.354,62.224,21.302,62.113,21.256,61.99,21.385,61.915,21.471,61.812,21.546,61.703,21.552,61.667,21.606,61.592,21.598,61.578,21.592,61.568,21.522,61.567,21.498,61.552,21.527,61.523,21.552,61.51,21.565,61.484,21.507,61.484,21.502,61.455,21.521,61.411,21.513,61.281,21.479,61.171,21.451,61.127,21.378,61.059,21.361,60.967,21.379,60.85,21.404,60.767,21.412,60.697,21.411,60.637,21.436,60.596,21.528,60.57,21.613,60.531,21.727,60.583,21.805,60.594,21.854,60.505,21.934,60.5,22.258,60.401,22.521,60.377,22.56,60.385,22.585,60.381,22.576,60.359,22.512,60.281,22.517,60.263,22.588,60.256,22.59,60.228,22.564,60.206,22.513,60.199,22.47,60.201,22.443,60.157,22.471,60.147,22.439,60.09,22.439,60.072,22.463,60.029,22.646,60.028,22.697,60.038,22.75,60.057,22.793,60.077,22.819,60.101,22.844,60.187,22.867,60.216,22.912,60.21,22.994,60.099,23.08,60.047,23.148,60.041,23.198,60.022,23.188,59.972,23.116,59.913,23.01,59.869,22.964,59.826,23.021,59.816,23.181,59.845,23.327,59.926,23.464,59.986,23.593,59.968,23.722,59.966,24.025,60.009,24.343,60.042,24.446,60.021,24.518,60.046,24.6,60.114,24.849,60.158,24.958,60.157,25.156,60.194,25.268,60.248,25.456,60.261,25.548,60.302,25.656,60.333,25.715,60.267,25.758,60.268,25.846,60.315,25.946,60.347,26.036,60.342,26.04,60.372,26.006,60.425,25.956,60.474,26.036,60.475,26.205,60.407,26.378,60.424,26.456,60.467,26.496,60.552,26.569,60.625,26.606,60.628,26.602,60.596,26.551,60.546,26.52,60.472,26.535,60.413,26.607,60.438,26.721,60.455,26.951,60.471,27.076,60.525,27.205,60.543,27.242,60.539,27.462,60.465,27.525,60.491,27.669,60.499,27.762,60.533,27.798,60.536,28.179,60.571,28.513,60.677,28.578,60.653,28.651,60.611,28.64,60.543,28.622,60.492,28.492,60.54,28.522,60.483,28.643,60.375,28.813,60.332,29.069,60.191,29.37,60.176,29.569,60.202,29.721,60.195,29.872,60.121,29.977,60.026,30.06,60.003,30.173,59.957,30.157,59.904,30.123,59.874,29.67,59.956,29.147,60,29.079,59.961,29.013,59.902,28.982,59.855,28.947,59.829,28.867,59.812,28.748,59.807,28.604,59.818,28.518,59.85,28.454,59.814,28.424,59.734,28.335,59.693,28.213,59.725,28.131,59.787,28.058,59.782,28.014,59.725,28.046,59.647,28.064,59.554,28.013,59.484,28.002,59.47,27.893,59.414,27.336,59.45,26.975,59.451,26.852,59.472,26.625,59.554,26.461,59.554,25.794,59.635,25.616,59.628,25.509,59.639,25.507,59.598,25.521,59.559,25.444,59.521,24.878,59.522,24.584,59.456,24.38,59.473,24.175,59.376,24.054,59.372,24.083,59.292,23.783,59.275,23.641,59.242,23.494,59.196,23.517,59.108,23.48,59.07,23.468,59.032,23.515,58.999,23.49,58.96,23.432,58.921,23.497,58.82,23.504,58.79,23.534,58.782,23.681,58.787,23.647,58.754,23.531,58.716,23.509,58.659,23.563,58.576,23.692,58.506,23.706,58.433,23.768,58.361,24.011,58.307,24.115,58.266,24.236,58.29,24.287,58.328,24.337,58.381,24.392,58.386,24.529,58.354,24.55,58.305,24.536,58.283,24.488,58.262,24.464,58.106,24.332,57.91,24.323,57.871,24.302,57.784,24.363,57.645,24.403,57.325,24.383,57.25,24.281,57.172,24.054,57.066,23.931,57.008,23.648,56.971,23.287,57.09,23.137,57.324,23.038,57.392,22.649,57.595,22.617,57.651,22.555,57.724,22.231,57.667,21.942,57.598,21.729,57.571,21.459,57.322,21.421,57.236,21.405,57.131,21.351,57.018,21.257,56.933,21.071,56.824,21.031,56.637,21.015,56.259,21.046,56.07,21.054,56.023,21.062,55.813,21.171,55.618,21.238,55.455,21.201,55.344,21.236,55.271,21.223,55.108,21.189,54.935,20.996,54.903,20.888,54.909,20.774,54.947,20.678,54.956,20.595,54.982,20.859,55.184,21.032,55.35,21.104,55.488,21.116,55.568,21.115,55.617,21.088,55.583,21.058,55.477,21.014,55.402,20.9,55.287,20.846,55.232,20.679,55.103,20.52,54.995,20.397,54.951,20.108,54.956,19.975,54.921,19.953,54.83,19.944,54.75,19.859,54.634,19.758,54.545,19.604,54.459,19.56,54.435,19.407,54.386,18.976,54.349,18.836,54.37,18.67,54.431,18.587,54.513,18.436,54.745,18.502,54.742,18.678,54.665,18.8,54.633,18.759,54.685,18.535,54.769,18.323,54.838,18.086,54.836,17.843,54.817,17.262,54.73,17.007,54.652,16.885,54.596,16.56,54.554,16.376,54.437,16.292,54.362,16.239,54.333,16.186,54.29,16.043,54.266,15.9,54.254,15.288,54.14,14.716,54.018,14.384,53.925,14.249,53.932,14.211,53.95,14.039,54.035,13.828,54.127,13.82,54.093,13.827,54.06,13.872,54.036,13.922,53.997,13.902,53.939,13.926,53.879,14.048,53.863,14.172,53.874,14.351,53.859,14.558,53.823,14.565,53.754,14.552,53.707,14.572,53.676,14.583,53.639,14.488,53.672,14.25,53.732,14.025,53.767,13.95,53.801,13.866,53.853,13.822,54.019,13.724,54.153,13.448,54.141,13.147,54.283,13.029,54.411,12.898,54.423,12.779,54.446,12.575,54.467,12.379,54.347,12.296,54.284,12.169,54.226,12.111,54.168,11.796,54.145,11.701,54.114,11.461,53.965,11.4,53.945,11.104,54.009,10.918,53.995,10.855,54.01,10.811,54.075,11.009,54.181,11.064,54.281,11.013,54.379,10.956,54.376,10.732,54.316,10.36,54.438,10.212,54.409,10.171,54.45,10.143,54.488,9.869,54.472,9.941,54.515,10.029,54.581,10.022,54.674,9.954,54.738,9.892,54.781,9.746,54.807,9.705,54.928,9.732,54.968,9.688,55,9.645,55.023,9.572,55.041,9.454,55.04,9.505,55.116,9.643,55.205,9.671,55.266,9.64,55.344,9.626,55.414,9.591,55.493,9.661,55.557,9.773,55.608,9.81,55.651,9.899,55.708,9.999,55.736,10.024,55.761,9.962,55.813,9.904,55.843,10.017,55.876,10.107,55.874,10.159,55.854,10.183,55.865,10.227,56.005,10.319,56.213,10.374,56.252,10.427,56.276,10.539,56.2,10.621,56.202,10.753,56.242,10.856,56.296,10.894,56.359,10.926,56.443,10.883,56.493,10.846,56.522,10.49,56.521,10.384,56.555,10.283,56.621,10.297,56.781,10.287,56.823,10.296,56.999,10.338,57.021,10.437,57.172,10.524,57.243,10.518,57.379,10.537,57.449,10.445,57.562,10.46,57.615,10.481,57.649,10.61,57.737,10.533,57.735,10.259,57.617,9.962,57.581,9.815,57.478,9.554,57.232,9.434,57.174,9.299,57.147,9.036,57.155,8.952,57.151,8.812,57.11,8.619,57.111,8.427,56.984,8.284,56.852,8.266,56.815,8.268,56.754,8.347,56.712,8.468,56.665,8.603,56.71,8.772,56.725,8.876,56.887,8.993,57.016,9.11,57.044,9.255,57.012,9.21,56.808,9.196,56.702,9.14,56.75,9.067,56.794,8.995,56.775,8.888,56.735,8.736,56.627,8.718,56.544,8.672,56.496,8.608,56.515,8.553,56.56,8.473,56.565,8.281,56.617,8.232,56.618,8.164,56.607,8.13,56.321,8.121,56.14,8.202,55.982,8.181,55.901,8.132,55.6,8.345,55.51,8.616,55.418,8.651,55.329,8.67,55.156,8.573,55.134,8.638,55.046,8.661,54.986,8.682,54.792,8.79,54.696,8.881,54.594,8.957,54.538,8.952,54.468,8.831,54.428,8.648,54.398,8.626,54.354,8.645,54.295,8.736,54.295,8.78,54.313,8.852,54.3,8.907,54.261,8.904,54,8.92,53.965,8.978,53.926,9.07,53.901,9.216,53.891,9.312,53.859,9.631,53.6,9.784,53.555,9.673,53.566,9.585,53.6,9.322,53.813,9.206,53.856,8.898,53.836,8.619,53.875,8.576,53.838,8.528,53.781,8.506,53.671,8.538,53.557,8.495,53.394,8.493,53.514,8.451,53.552,8.334,53.606,8.302,53.584,8.279,53.511,8.245,53.445,8.201,53.432,8.108,53.468,8.167,53.543,8.009,53.691,7.629,53.697,7.285,53.681,7.206,53.655,7.107,53.557,7.074,53.478,7.053,53.376,7.152,53.327,7.197,53.282,7.058,53.301,6.968,53.327,6.912,53.375,6.816,53.441,6.564,53.434,6.353,53.415,6.062,53.407,5.874,53.375,5.532,53.269,5.446,53.214,5.358,53.096,5.061,52.961,4.888,52.908,4.839,52.928,4.769,52.941,4.713,52.872,4.678,52.81,4.562,52.443,4.483,52.309,4.376,52.197,4.209,52.059,4.132,52.012,4.085,51.994,4.026,51.928,3.979,51.848,3.947,51.811,4.135,51.673,4.158,51.633,4.183,51.61,4.005,51.596,4.08,51.551,4.175,51.519,4.239,51.504,4.274,51.472,4.206,51.457,4.141,51.456,3.886,51.574,3.744,51.596,3.549,51.589,3.5,51.577,3.449,51.541,3.521,51.486,3.587,51.454,3.694,51.45,3.822,51.409,4.007,51.443,4.139,51.402,4.226,51.386,4.112,51.361,4.011,51.396,3.883,51.354,3.717,51.369,3.589,51.399,3.426,51.394,3.35,51.378,3.225,51.352,2.96,51.265,2.525,51.097,2.446,51.067,1.913,50.991,1.768,50.936,1.672,50.885,1.61,50.819,1.579,50.739,1.552,50.294,1.593,50.252,1.548,50.231,1.514,50.205,1.407,50.089,1.246,49.998,0.924,49.91,0.616,49.863,0.187,49.703,0.127,49.602,0.109,49.558,0.129,49.508,0.278,49.463,0.439,49.473,0.417,49.448,0.136,49.402,-0.011,49.33,-0.163,49.297,-0.521,49.355,-0.766,49.36,-0.959,49.393,-1.139,49.388,-1.195,49.445,-1.232,49.495,-1.265,49.598,-1.259,49.68,-1.366,49.707,-1.588,49.668,-1.705,49.681,-1.856,49.684,-1.875,49.631,-1.87,49.595,-1.813,49.49,-1.69,49.313,-1.583,49.202,-1.565,48.806,-1.48,48.698,-1.376,48.653,-1.438,48.641,-1.825,48.631,-1.852,48.669,-1.906,48.697,-1.973,48.635,-2.004,48.582,-2.079,48.645,-2.446,48.648,-2.692,48.537,-2.793,48.601,-3.003,48.791,-3.231,48.841,-3.471,48.813,-3.546,48.766,-3.715,48.71,-3.856,48.695,-4.059,48.708,-4.531,48.62,-4.721,48.54,-4.762,48.45,-4.749,48.41,-4.719,48.363,-4.585,48.358,-4.525,48.372,-4.393,48.368,-4.364,48.357,-4.302,48.347,-4.241,48.304,-4.403,48.293,-4.498,48.299,-4.531,48.31,-4.577,48.29,-4.544,48.247,-4.512,48.23,-4.435,48.218,-4.329,48.17,-4.378,48.129,-4.512,48.097,-4.629,48.086,-4.679,48.04,-4.428,47.969,-4.375,47.877,-4.312,47.823,-4.226,47.81,-4.071,47.848,-3.901,47.838,-3.508,47.753,-3.444,47.711,-3.396,47.72,-3.329,47.713,-3.265,47.685,-3.222,47.694,-3.159,47.695,-3.064,47.621,-2.964,47.601,-2.859,47.614,-2.787,47.626,-2.733,47.602,-2.797,47.537,-2.77,47.514,-2.666,47.526,-2.554,47.527,-2.483,47.512,-2.428,47.471,-2.476,47.413,-2.53,47.382,-2.503,47.312,-2.434,47.291,-2.353,47.279,-1.975,47.311,-1.743,47.216,-1.821,47.225,-1.922,47.261,-2.028,47.274,-2.108,47.263,-2.149,47.224,-2.197,47.163,-2.144,47.126,-2.082,47.112,-2.019,47.038,-2.09,46.921,-2.092,46.865,-2.059,46.81,-1.921,46.685,-1.787,46.515,-1.392,46.35,-1.313,46.327,-1.239,46.325,-1.146,46.311,-1.132,46.253,-1.136,46.205,-1.104,45.925,-1.066,45.806,-1.042,45.773,-1.032,45.741,-1.114,45.769,-1.21,45.771,-1.196,45.714,-1.17,45.686,-0.881,45.538,-0.791,45.468,-0.733,45.385,-0.641,45.09,-0.582,45.051,-0.548,45.001,-0.634,45.047,-0.691,45.093,-0.767,45.314,-0.826,45.381,-0.942,45.457,-1.081,45.532,-1.149,45.343,-1.189,45.161,-1.245,44.667,-1.22,44.687,-1.2,44.726,-1.153,44.764,-1.077,44.69,-1.171,44.662,-1.246,44.56,-1.346,44.02,-1.485,43.564,-1.631,43.438,-1.794,43.407,-1.829,43.401,-1.991,43.345,-2.197,43.322,-2.337,43.328,-2.607,43.413,-2.875,43.454,-2.948,43.44,-3.046,43.372,-3.418,43.452,-3.524,43.511,-3.605,43.519,-3.774,43.478,-3.889,43.499,-4.015,43.463,-4.313,43.415,-4.523,43.416,-5.105,43.502,-5.316,43.553,-5.666,43.582,-5.847,43.645,-6.08,43.595,-6.224,43.604,-6.476,43.579,-6.617,43.592,-6.901,43.586,-7.061,43.554,-7.262,43.595,-7.399,43.696,-7.504,43.74,-7.595,43.727,-7.698,43.765,-7.853,43.707,-8.005,43.694,-8.137,43.629,-8.257,43.58,-8.289,43.54,-8.252,43.497,-8.249,43.439,-8.355,43.397,-8.422,43.386,-8.537,43.337,-8.666,43.317,-8.874,43.334,-9.025,43.239,-9.096,43.214,-9.178,43.174,-9.236,43.036,-9.235,42.977,-9.179,42.911,-9.127,42.865,-9.042,42.814,-8.927,42.799,-8.937,42.767,-9.035,42.662,-9.033,42.594,-8.988,42.586,-8.812,42.64,-8.8,42.6,-8.81,42.562,-8.812,42.47,-8.776,42.435,-8.73,42.412,-8.769,42.358,-8.81,42.334,-8.816,42.285,-8.729,42.287,-8.691,42.274,-8.772,42.211,-8.887,42.105,-8.878,41.947,-8.852,41.927,-8.777,41.941,-8.878,41.832,-8.888,41.765,-8.846,41.705,-8.755,41.698,-8.811,41.652,-8.806,41.56,-8.738,41.285,-8.675,41.154,-8.66,41.086,-8.656,41.029,-8.674,40.917,-8.685,40.753,-8.732,40.651,-8.772,40.606,-8.873,40.259,-8.887,40.179,-8.851,40.152,-8.838,40.116,-9.004,39.821,-9.148,39.543,-9.251,39.426,-9.32,39.391,-9.375,39.338,-9.357,39.284,-9.353,39.248,-9.414,39.112,-9.431,38.96,-9.475,38.853,-9.48,38.799,-9.474,38.731,-9.41,38.708,-9.357,38.698,-9.252,38.713,-9.136,38.743,-9.091,38.835,-8.954,39.016,-8.867,39.066,-8.792,39.078,-8.938,38.998,-9,38.903,-8.977,38.803,-9.021,38.747,-9.093,38.697,-9.178,38.688,-9.25,38.657,-9.203,38.539,-9.213,38.448,-9.187,38.438,-9.096,38.455,-8.915,38.512,-8.862,38.51,-8.799,38.518,-8.734,38.482,-8.668,38.424,-8.881,38.447,-8.811,38.3,-8.802,38.184,-8.879,37.959,-8.823,37.872,-8.792,37.733,-8.819,37.592,-8.814,37.431,-8.926,37.166,-8.998,37.032,-8.935,37.016,-8.848,37.076,-8.739,37.075,-8.598,37.121,-8.484,37.1,-8.137,37.077,-7.94,37.005,-7.834,37.006,-7.494,37.168,-7.406,37.179,-7.175,37.209,-6.975,37.198,-6.929,37.215,-6.864,37.279,-6.859,37.249,-6.885,37.194,-6.492,36.955,-6.396,36.832,-6.321,36.908,-6.217,36.914,-6.259,36.899,-6.328,36.848,-6.412,36.729,-6.384,36.637,-6.269,36.597,-6.258,36.565,-6.266,36.527,-6.226,36.426,-6.17,36.334,-6.041,36.188,-5.961,36.182,-5.808,36.088,-5.625,36.026,-5.551,36.039,-5.462,36.074,-5.444,36.151,-5.407,36.159,-5.382,36.134,-5.361,36.135,-5.33,36.236,-5.231,36.374,-5.171,36.424,-4.935,36.502,-4.674,36.506,-4.502,36.629,-4.435,36.7,-4.367,36.718,-3.828,36.756,-3.579,36.74,-3.431,36.708,-3.259,36.756,-3.149,36.758,-2.902,36.743,-2.788,36.715,-2.671,36.748,-2.596,36.806,-2.453,36.831,-2.306,36.82,-2.188,36.745,-2.112,36.777,-1.939,36.946,-1.798,37.233,-1.641,37.387,-1.328,37.561,-0.938,37.571,-0.822,37.581,-0.772,37.596,-0.722,37.631,-0.823,37.712,-0.815,37.77,-0.753,37.85,-0.742,37.886,-0.683,37.992,-0.647,38.152,-0.551,38.203,-0.521,38.317,-0.381,38.436,-0.053,38.586,0.136,38.697,0.202,38.759,0.155,38.825,-0.034,38.891,-0.134,38.969,-0.205,39.063,-0.329,39.417,-0.327,39.52,-0.075,39.876,0.043,40.014,0.158,40.107,0.364,40.319,0.596,40.615,0.627,40.622,0.66,40.613,0.721,40.63,0.859,40.686,0.891,40.722,0.796,40.804,0.715,40.823,0.817,40.892,1.033,41.062,1.206,41.098,1.567,41.196,2.083,41.287,2.146,41.321,2.311,41.467,3.005,41.767,3.147,41.861,3.248,41.944,3.238,42.082,3.225,42.111,3.175,42.136,3.15,42.162,3.166,42.256,3.219,42.26,3.307,42.289,3.288,42.344,3.24,42.368,3.211,42.431,3.198,42.461,3.091,42.591,3.043,42.838,3.052,42.915,3.163,43.081,3.259,43.193,3.785,43.462,3.862,43.516,3.911,43.563,4.053,43.593,4.075,43.582,4.113,43.563,4.163,43.504,4.224,43.48,4.376,43.456,4.41,43.447,4.629,43.387,4.712,43.373,4.789,43.379,4.787,43.401,4.808,43.405,4.844,43.394,4.874,43.412,4.912,43.427,4.976,43.427,5.06,43.445,5.061,43.406,5.073,43.367,5.12,43.349,5.2,43.352,5.32,43.345,5.407,43.229,5.672,43.178,5.809,43.098,6.031,43.101,6.116,43.072,6.305,43.139,6.494,43.169,6.57,43.199,6.657,43.262,6.687,43.335,6.717,43.374,6.865,43.438,7.181,43.659,7.262,43.696,7.378,43.732,7.439,43.75,7.493,43.767,7.733,43.803,8.005,43.877,8.082,43.919,8.292,44.137,8.552,44.346,8.766,44.422,8.93,44.408,9.196,44.323,9.289,44.319,9.731,44.101,10.048,44.02,10.188,43.948,10.246,43.852,10.321,43.513,10.448,43.371,10.521,43.204,10.532,43.14,10.517,43.065,10.515,42.968,10.59,42.954,10.645,42.957,10.708,42.936,10.737,42.9,10.765,42.845,10.803,42.804,10.938,42.739,11.168,42.535,11.185,42.457,11.142,42.444,11.103,42.417,11.141,42.39,11.189,42.393,11.25,42.416,11.296,42.423,11.498,42.363,11.637,42.288,11.807,42.082,12.075,41.941,12.206,41.813,12.631,41.47,12.849,41.409,13.024,41.301,13.041,41.266,13.089,41.244,13.183,41.278,13.247,41.289,13.362,41.279,13.555,41.232,13.67,41.254,13.733,41.236,13.86,41.13,14.048,40.87,14.044,40.812,14.076,40.794,14.102,40.827,14.147,40.821,14.309,40.813,14.428,40.759,14.461,40.729,14.34,40.599,14.383,40.6,14.459,40.633,14.557,40.626,14.611,40.645,14.766,40.668,14.84,40.63,14.907,40.556,14.948,40.469,14.986,40.377,14.929,40.31,14.927,40.265,14.951,40.239,15.295,40.07,15.391,40.052,15.585,40.053,15.693,39.99,15.764,39.87,15.854,39.627,16.024,39.354,16.071,39.139,16.107,39.024,16.21,38.941,16.197,38.759,16.066,38.736,15.972,38.713,15.927,38.672,15.879,38.614,15.905,38.483,15.822,38.303,15.7,38.262,15.643,38.175,15.646,38.034,15.725,37.939,16.057,37.942,16.11,38.019,16.144,38.086,16.282,38.25,16.546,38.409,16.574,38.494,16.559,38.715,16.617,38.8,16.755,38.89,16.951,38.94,17.099,38.919,17.175,38.998,17.123,39.137,17.115,39.381,16.999,39.482,16.824,39.578,16.598,39.639,16.522,39.748,16.53,39.86,16.67,40.137,16.807,40.326,16.928,40.458,17.031,40.513,17.18,40.503,17.215,40.486,17.249,40.438,17.258,40.399,17.396,40.34,17.476,40.315,17.865,40.28,18.078,39.937,18.219,39.853,18.344,39.821,18.393,39.904,18.423,39.987,18.486,40.105,18.461,40.221,18.328,40.371,18.036,40.565,17.955,40.655,17.474,40.841,17.275,40.975,17.103,41.062,16.552,41.232,16.013,41.435,15.9,41.512,15.914,41.621,16.034,41.701,16.151,41.758,16.189,41.814,16.165,41.896,16.062,41.928,15.964,41.939,15.405,41.913,15.169,41.934,14.866,42.053,14.541,42.244,14.183,42.506,14.01,42.69,13.925,42.852,13.805,43.18,13.693,43.39,13.564,43.571,13.508,43.612,13.295,43.686,12.907,43.921,12.691,43.995,12.487,44.134,12.396,44.224,12.305,44.429,12.248,44.723,12.279,44.832,12.319,44.833,12.384,44.798,12.464,44.845,12.498,44.899,12.523,44.968,12.392,45.04,12.286,45.208,12.226,45.242,12.249,45.369,12.274,45.446,12.354,45.492,12.492,45.546,12.536,45.545,12.432,45.468,12.498,45.462,12.612,45.497,12.761,45.544,12.903,45.611,13.03,45.638,13.12,45.698,13.157,45.747,13.206,45.771,13.465,45.71,13.558,45.771,13.628,45.771,13.783,45.627,13.72,45.588,13.637,45.536,13.578,45.517,13.517,45.482,13.603,45.231,13.613,45.163,13.629,45.108,13.742,44.992,13.861,44.837,13.9,44.829,13.966,44.836,14.042,44.927,14.091,44.998,14.236,45.16,14.269,45.283,14.313,45.338,14.386,45.342,14.55,45.298,14.632,45.223,14.855,45.081,14.907,44.971,14.885,44.818,14.895,44.707,14.981,44.603,15.27,44.383,15.381,44.328,15.471,44.272,15.37,44.289,15.284,44.289,15.231,44.271,15.185,44.273,15.123,44.257,15.186,44.172,15.499,43.909,15.656,43.811,15.821,43.736,15.942,43.657,15.949,43.607,15.943,43.569,15.986,43.52,16.046,43.506,16.131,43.506,16.269,43.531,16.394,43.543,16.6,43.464,16.903,43.392,17.129,43.211,17.33,43.115,17.537,42.962,17.585,42.938,17.668,42.897,17.724,42.851,17.22,43.026,17.126,43.026,17.045,43.015,17.258,42.968,17.585,42.837,17.824,42.797,18.161,42.634,18.333,42.528,18.517,42.433,18.554,42.429,18.592,42.444,18.646,42.443,18.633,42.423,18.619,42.398,18.633,42.378,18.894,42.249,19.122,42.06,19.186,41.949,19.342,41.869,19.468,41.856,19.578,41.788,19.576,41.64,19.546,41.597,19.497,41.563,19.441,41.425,19.453,41.321,19.48,41.236,19.456,41.106,19.461,40.933,19.384,40.791,19.338,40.664,19.345,40.622,19.439,40.47,19.459,40.405,19.441,40.376,19.395,40.394,19.359,40.409,19.322,40.407,19.36,40.348,19.398,40.285,19.485,40.21,19.852,40.044,19.965,39.872,19.996,39.801,20.001,39.709,20.099,39.641,20.191,39.546,20.301,39.327,20.468,39.255,20.572,39.148,20.691,39.067,20.713,39.035,20.78,39.009,20.923,39.037,21.034,39.026,21.069,39.032,21.118,39.03,21.145,38.979,21.152,38.922,21.112,38.896,21.074,38.885,20.893,38.941,20.777,38.928,20.769,38.874,20.777,38.808,20.873,38.776,20.992,38.654,21.06,38.503,21.113,38.385,21.183,38.346,21.303,38.374,21.33,38.424,21.331,38.487,21.355,38.475,21.39,38.408,21.473,38.321,21.568,38.334,21.65,38.354,21.717,38.355,21.805,38.367,21.965,38.412,22.227,38.353,22.32,38.357,22.385,38.386,22.422,38.439,22.583,38.345,22.754,38.29,22.784,38.262,22.834,38.235,22.933,38.202,22.995,38.216,23.034,38.202,23.094,38.196,23.149,38.176,23.183,38.134,23.153,38.096,23.122,38.073,22.955,38.075,22.893,38.051,22.917,38.007,22.92,37.958,22.846,37.968,22.8,37.981,22.712,38.047,22.556,38.113,22.244,38.189,21.953,38.321,21.825,38.328,21.748,38.274,21.658,38.175,21.549,38.165,21.451,38.205,21.404,38.197,21.308,38.027,21.145,37.919,21.125,37.892,21.138,37.854,21.205,37.829,21.288,37.775,21.329,37.669,21.416,37.64,21.571,37.541,21.679,37.387,21.692,37.309,21.579,37.2,21.583,37.081,21.738,36.863,21.892,36.737,21.934,36.804,21.94,36.892,21.956,36.99,22.012,37.017,22.08,37.029,22.134,36.964,22.165,36.903,22.231,36.883,22.376,36.702,22.381,36.646,22.375,36.514,22.428,36.476,22.489,36.447,22.489,36.568,22.608,36.78,22.717,36.794,22.78,36.786,22.832,36.687,22.983,36.528,23.061,36.487,23.107,36.452,23.16,36.448,23.112,36.548,23.041,36.645,23.074,36.775,23.06,36.854,22.995,37.016,22.851,37.291,22.765,37.393,22.725,37.542,22.775,37.585,22.851,37.532,22.941,37.517,23.015,37.482,23.096,37.441,23.1,37.364,23.162,37.334,23.203,37.349,23.253,37.377,23.489,37.44,23.491,37.464,23.458,37.497,23.409,37.542,23.396,37.58,23.348,37.598,23.263,37.595,23.198,37.62,23.147,37.716,23.147,37.795,23.086,37.853,23.036,37.878,23.047,37.903,23.087,37.913,23.194,37.959,23.42,37.992,23.502,38.035,23.537,38.033,23.58,38.011,23.733,37.884,23.877,37.778,23.972,37.677,24.02,37.678,24.055,37.71,24.062,37.775,24.061,37.818,24.033,37.955,24.025,38.14,24.005,38.227,23.967,38.275,23.836,38.325,23.684,38.352,23.57,38.489,23.369,38.526,23.253,38.661,23.138,38.668,23.02,38.742,22.774,38.8,22.688,38.849,22.634,38.851,22.569,38.867,22.597,38.891,22.677,38.899,22.803,38.902,22.93,38.948,23.067,39.038,22.966,39.031,22.939,39.112,22.886,39.17,22.839,39.259,22.921,39.306,22.993,39.331,23.162,39.258,23.169,39.21,23.119,39.133,23.155,39.101,23.218,39.104,23.328,39.175,23.288,39.289,23.233,39.358,23.103,39.492,22.979,39.564,22.919,39.629,22.836,39.801,22.592,40.037,22.569,40.119,22.605,40.276,22.643,40.367,22.625,40.429,22.629,40.496,22.742,40.536,22.811,40.579,22.922,40.591,22.893,40.524,22.851,40.491,22.896,40.4,23.098,40.304,23.312,40.216,23.328,40.09,23.396,39.99,23.627,39.924,23.658,39.934,23.674,39.959,23.467,40.074,23.433,40.115,23.386,40.222,23.426,40.264,23.665,40.224,23.835,40.022,23.947,39.966,23.982,39.994,24.001,40.025,23.967,40.115,23.918,40.155,23.823,40.205,23.721,40.286,23.728,40.33,23.823,40.368,23.913,40.359,24.056,40.304,24.159,40.28,24.232,40.215,24.343,40.148,24.292,40.242,24.213,40.328,24.031,40.409,23.932,40.406,23.867,40.419,23.832,40.482,23.879,40.544,23.779,40.628,23.743,40.677,23.763,40.748,23.946,40.748,24.082,40.724,24.234,40.786,24.384,40.913,24.477,40.948,24.557,40.936,24.679,40.869,24.793,40.858,25.005,40.968,25.104,40.995,25.25,40.933,25.325,40.943,25.497,40.888,25.856,40.844,26.011,40.769,26.039,40.727,26.068,40.683,26.105,40.611,26.224,40.618,26.361,40.606,26.578,40.625,26.792,40.627,26.72,40.544,26.447,40.445,26.355,40.39,26.254,40.315,26.252,40.248,26.26,40.202,26.226,40.142,26.203,40.075,26.272,40.097,26.33,40.123,26.468,40.261,26.772,40.498,26.975,40.564,27.258,40.687,27.43,40.84,27.499,40.973,27.747,41.013,27.925,40.991,28.086,41.061,28.172,41.081,28.295,41.071,28.78,40.974,28.956,41.008,28.996,41.061,29.032,41.14,29.057,41.23,28.947,41.248,28.346,41.466,28.198,41.554,28.05,41.729,27.987,41.855,28.014,41.969,27.983,42.047,27.821,42.208,27.708,42.35,27.64,42.401,27.485,42.468,27.754,42.707,27.818,42.717,27.889,42.75,27.896,43.021,27.929,43.186,27.979,43.231,28.035,43.268,28.134,43.396,28.32,43.427,28.465,43.389,28.562,43.501,28.585,43.742,28.591,43.797,28.659,43.984,28.645,44.296,28.699,44.374,28.852,44.506,28.888,44.575,28.807,44.565,28.814,44.602,28.846,44.637,28.849,44.716,28.87,44.75,28.926,44.81,28.892,44.919,28.931,44.966,28.981,44.993,29.095,44.975,29.048,44.926,29.069,44.871,29.081,44.799,29.048,44.758,29.558,44.843,29.605,44.915,29.635,44.98,29.679,45.152,29.689,45.193,29.706,45.26,29.727,45.343,29.67,45.541,29.601,45.6,29.602,45.683,29.628,45.722,29.685,45.755,29.821,45.732,29.902,45.752,30.007,45.798,30.184,45.85,30.219,45.867,30.493,46.09,30.512,46.105,30.657,46.267,30.672,46.304,30.722,46.366,30.773,46.473,30.796,46.552,31.137,46.624,31.32,46.613,31.403,46.629,31.497,46.738,31.563,46.777,31.532,46.665,31.657,46.642,31.78,46.632,31.873,46.65,31.902,46.722,31.913,46.926,31.866,47.003,31.837,47.087,31.759,47.213,31.838,47.157,31.865,47.095,31.94,46.982,31.964,46.855,31.945,46.784,31.974,46.709,32.044,46.642,32.127,46.597,32.354,46.565,32.578,46.616,32.553,46.592,32.419,46.518,32.361,46.475,32.131,46.509,31.878,46.522,31.716,46.555,31.555,46.554,31.624,46.51,31.714,46.472,31.856,46.462,32.008,46.43,32.013,46.387,31.992,46.364,31.916,46.349,31.843,46.346,31.78,46.325,31.831,46.282,31.925,46.287,32.036,46.261,32.33,46.13,32.477,46.084,32.797,46.131,32.942,46.124,33.202,46.176,33.263,46.126,33.43,46.058,33.499,46.079,33.594,46.096,33.637,46.033,33.665,45.947,33.466,45.838,33.28,45.765,33.142,45.749,32.828,45.593,32.508,45.404,32.552,45.35,32.611,45.328,32.773,45.359,32.919,45.348,33.187,45.195,33.262,45.171,33.392,45.188,33.555,45.098,33.601,44.981,33.612,44.908,33.53,44.681,33.491,44.619,33.463,44.597,33.451,44.554,33.656,44.433,33.756,44.399,33.91,44.388,34.074,44.424,34.282,44.538,34.47,44.722,34.717,44.807,34.888,44.824,35.088,44.803,35.155,44.896,35.358,44.978,35.473,45.098,35.57,45.119,35.678,45.102,35.759,45.071,35.804,45.04,35.87,45.005,36.055,45.031,36.23,45.026,36.393,45.065,36.428,45.153,36.451,45.232,36.514,45.304,36.575,45.394,36.427,45.433,36.29,45.457,36.171,45.453,36.077,45.424,36.013,45.372,35.833,45.402,35.751,45.389,35.558,45.311,35.458,45.316,35.374,45.354,35.26,45.447,35.023,45.701,34.907,45.879,34.857,45.987,34.844,46.074,34.85,46.19,34.97,46.242,35.064,46.267,35.23,46.441,35.292,46.371,35.291,46.314,35.28,46.279,35.218,46.232,35.015,46.106,35.055,46.104,35.132,46.126,35.204,46.169,35.257,46.204,35.4,46.381,35.827,46.624,36.025,46.667,36.195,46.646,36.279,46.659,36.432,46.733,36.559,46.763,36.689,46.764,36.795,46.714,36.932,46.825,37.048,46.876,37.219,46.917,37.34,46.917,37.543,47.075,37.829,47.096,38.178,47.08,38.214,47.091,38.485,47.176,38.577,47.239,38.762,47.262,38.736,47.236,38.644,47.212,38.552,47.15,38.668,47.144,38.928,47.176,39.024,47.272,39.196,47.269,39.245,47.2,39.293,47.106,39.289,47.071,39.271,47.044,39.127,47.023,38.801,46.906,38.631,46.873,38.439,46.813,38.488,46.732,38.501,46.664,38.343,46.678,38.23,46.701,38.159,46.691,37.968,46.618,37.867,46.634,37.767,46.636,37.81,46.532,37.914,46.406,37.978,46.383,38.078,46.394,38.315,46.242,38.492,46.091,38.4,46.08,38.312,46.095,38.184,46.095,38.133,46.003,38.08,45.935,38.07,45.97,38.074,46.017,38.014,46.048,37.933,46.002,37.841,45.8,37.669,45.654,37.612,45.565,37.61,45.5,37.634,45.486,37.672,45.488,37.673,45.43,37.647,45.377,37.264,45.311,37.214,45.272,37.104,45.303,36.978,45.384,36.866,45.427,36.794,45.41,36.72,45.372,36.762,45.348,36.811,45.34,36.941,45.29,36.873,45.252,36.619,45.185,36.628,45.151,36.651,45.126,36.944,45.07,37.205,44.972,37.284,44.905,37.352,44.788,37.411,44.735,37.495,44.695,37.572,44.671,37.705,44.661,37.851,44.699,38.181,44.42,38.312,44.374,38.636,44.318,38.717,44.288,39.329,43.897,39.517,43.728,39.874,43.473,39.978,43.42,40.191,43.312,40.462,43.146,40.524,43.121,40.837,43.063,41.062,42.931,41.129,42.828,41.419,42.738,41.489,42.659,41.578,42.398,41.663,42.147,41.763,41.97,41.761,41.885,41.759,41.817,41.702,41.705,41.51,41.517,41.414,41.424,41.084,41.261,40.959,41.212,40.82,41.19,40.688,41.107,40.265,40.961,40.128,40.943,40,40.977,39.911,40.966,39.808,40.983,39.426,41.106,38.852,41.018,38.557,40.937,38.381,40.925,37.91,41.002,37.766,41.079,37.431,41.114,37.066,41.184,36.992,41.275,36.778,41.363,36.647,41.353,36.587,41.327,36.51,41.263,36.405,41.275,36.278,41.336,36.179,41.427,36.052,41.683,35.978,41.705,35.92,41.714,35.558,41.634,35.298,41.729,35.209,41.794,35.122,41.891,35.114,41.957,35.141,41.99,35.155,42.028,35.006,42.063,34.75,41.957,34.193,41.964,33.381,42.018,33.285,42.005,32.947,41.892,32.542,41.806,32.306,41.73,32.086,41.589,31.458,41.32,31.347,41.158,31.255,41.108,30.81,41.085,30.345,41.197,29.919,41.151,29.322,41.228,29.148,41.221,29.094,41.177,29.067,41.102,29.046,41.008,29.082,40.963,29.114,40.938,29.26,40.847,29.365,40.809,29.801,40.76,29.849,40.76,29.845,40.738,29.508,40.708,29.054,40.649,28.958,40.631,28.788,40.534,28.841,40.503,28.895,40.482,28.974,40.467,29.055,40.424,29.007,40.39,28.739,40.391,28.63,40.376,28.289,40.403,27.963,40.37,27.929,40.38,27.964,40.435,27.995,40.467,27.99,40.489,27.875,40.513,27.769,40.51,27.732,40.481,27.849,40.382,27.789,40.351,27.728,40.329,27.476,40.32,27.333,40.376,27.314,40.415,27.285,40.456,27.122,40.452,27.012,40.396,26.738,40.4,26.475,40.197,26.313,40.025,26.181,39.99,26.15,39.873,26.155,39.657,26.101,39.569,26.096,39.521,26.113,39.467,26.351,39.484,26.484,39.521,26.827,39.563,26.899,39.55,26.911,39.517,26.813,39.419,26.711,39.34,26.682,39.292,26.719,39.261,26.854,39.116,26.849,39.057,26.808,39.014,26.815,38.961,26.866,38.923,26.92,38.934,26.97,38.919,27.014,38.887,26.909,38.776,26.79,38.736,26.764,38.71,26.788,38.66,26.795,38.626,26.838,38.558,26.907,38.482,26.97,38.448,27.144,38.452,27.099,38.416,26.861,38.373,26.77,38.388,26.727,38.419,26.696,38.405,26.674,38.336,26.641,38.352,26.595,38.419,26.61,38.487,26.587,38.557,26.514,38.629,26.441,38.641,26.378,38.624,26.372,38.562,26.43,38.441,26.416,38.368,26.344,38.37,26.291,38.277,26.333,38.242,26.428,38.214,26.525,38.162,26.582,38.149,26.621,38.176,26.683,38.198,26.807,38.138,26.879,38.055,26.944,38.063,27.159,37.987,27.232,37.979,27.255,37.882,27.224,37.725,27.078,37.688,27.068,37.658,27.148,37.604,27.204,37.491,27.219,37.389,27.29,37.349,27.376,37.341,27.401,37.307,27.52,37.249,27.535,37.164,27.368,37.122,27.3,37.127,27.25,37.079,27.263,36.977,27.311,36.982,27.349,37.02,27.668,37.007,28.134,37.029,28.242,37.029,28.224,36.996,28.083,36.92,28.005,36.832,27.934,36.809,27.631,36.787,27.555,36.759,27.467,36.746,27.454,36.712,27.54,36.684,27.656,36.675,27.804,36.736,28.084,36.751,28.014,36.67,28.019,36.634,28.112,36.646,28.196,36.686,28.304,36.812,28.484,36.804,28.718,36.701,28.817,36.675,28.896,36.674,28.97,36.715,29.038,36.693,29.058,36.638,29.066,36.59,29.116,36.52,29.143,36.397,29.224,36.324,29.348,36.259,29.689,36.157,29.789,36.168,30.083,36.249,30.232,36.307,30.295,36.288,30.387,36.243,30.446,36.27,30.484,36.31,30.506,36.451,30.558,36.526,30.582,36.797,30.644,36.866,30.95,36.849,31.241,36.822,31.353,36.801,31.778,36.613,32.022,36.535,32.131,36.449,32.284,36.268,32.378,36.184,32.534,36.101,32.795,36.036,32.929,36.096,33.1,36.103,33.442,36.153,33.523,36.144,33.695,36.182,33.955,36.295,34.023,36.341,34.3,36.604,34.601,36.784,34.704,36.817,34.811,36.799,34.943,36.726,35.176,36.635,35.393,36.575,35.537,36.597,35.626,36.653,35.661,36.724,35.734,36.764,35.802,36.778,35.905,36.848,36.049,36.911,36.135,36.852,36.18,36.807,36.188,36.743,36.188,36.659,36.032,36.523,35.883,36.406,35.811,36.31,35.887,36.159,35.957,35.998,35.893,35.917,35.84,35.849,35.764,35.572,35.902,35.421,35.916,35.351,35.918,35.3,35.943,35.224,35.89,35.06,35.888,34.949,35.899,34.852,35.976,34.629,35.978,34.547,35.921,34.493,35.804,34.437,35.648,34.248,35.612,34.032,35.511,33.88,35.336,33.503,35.251,33.393,35.204,33.259,35.155,33.16,35.109,33.084,35.077,32.967,35.006,32.827,34.922,32.614,34.804,32.196,34.678,31.896,34.484,31.592,34.477,31.585,34.387,31.484,34.198,31.323,34.176,31.304,33.903,31.181,33.667,31.13,33.378,31.131,33.194,31.085,33.157,31.126,33.13,31.168,32.902,31.111,32.854,31.118,32.685,31.074,32.603,31.069,32.533,31.101,32.324,31.256,32.251,31.295,32.216,31.294,32.243,31.247,32.282,31.201,32.207,31.119,32.102,31.093,32.066,31.153,32.008,31.221,31.902,31.24,31.771,31.293,31.876,31.414,31.892,31.482,32.076,31.344,32.136,31.341,31.964,31.502,31.889,31.541,31.839,31.526,31.607,31.456,31.524,31.458,31.194,31.588,31.083,31.603,31.052,31.592,31.031,31.508,31.002,31.463,30.841,31.44,30.7,31.404,30.563,31.417,30.884,31.522,30.924,31.567,30.571,31.473,30.395,31.458,30.344,31.403,30.312,31.357,30.262,31.317,30.223,31.258,30.128,31.256,30.049,31.265,29.93,31.227,29.592,31.012,29.429,30.927,29.279,30.867,29.16,30.835,29.072,30.83,28.973,30.857,28.807,30.943,28.515,31.05,27.968,31.097,27.83,31.195,27.62,31.192,27.54,31.213,27.248,31.378,26.769,31.47,26.457,31.512,25.893,31.621,25.382,31.513,25.225,31.534,25.15,31.655,25.115,31.712,25.025,31.883,24.951,31.954,24.879,31.984,24.684,32.016,24.48,31.997,24.13,32.009,24.039,32.037,23.898,32.127,23.798,32.159,23.286,32.214,23.106,32.331,23.11,32.397,23.13,32.448,23.091,32.619,22.917,32.687,22.754,32.741,22.523,32.794,22.341,32.88,22.187,32.918,21.839,32.909,21.721,32.942,21.636,32.937,21.425,32.799,21.319,32.778,21.062,32.776,20.621,32.58,20.371,32.431,20.121,32.219,20.031,32.108,19.973,31.999,19.926,31.818,19.961,31.556,20.02,31.411,20.104,31.301,20.141,31.196,20.151,31.079,20.112,30.964,20.013,30.801,19.713,30.488,19.59,30.414,19.292,30.288,19.124,30.266,18.936,30.29,18.67,30.416,18.19,30.777,17.949,30.852,17.83,30.928,17.349,31.081,16.782,31.215,16.451,31.227,16.123,31.264,15.832,31.361,15.706,31.426,15.596,31.531,15.496,31.657,15.414,31.834,15.363,31.971,15.359,32.16,15.267,32.312,15.177,32.391,14.513,32.511,14.424,32.55,14.237,32.681,14.156,32.71,13.835,32.792,13.648,32.799,13.536,32.824,13.283,32.915,13.138,32.897,12.754,32.801,12.427,32.829,12.28,32.859,11.813,33.094,11.657,33.119,11.505,33.182,11.401,33.225,11.338,33.209,11.234,33.234,11.203,33.249,11.232,33.272,11.27,33.286,11.257,33.309,11.15,33.369,11.085,33.563,10.958,33.626,10.898,33.534,10.828,33.519,10.723,33.514,10.704,33.61,10.713,33.689,10.454,33.663,10.305,33.728,10.159,33.85,10.049,34.056,10.04,34.14,10.065,34.212,10.118,34.28,10.2,34.346,10.535,34.545,10.691,34.678,10.866,34.884,10.956,35.034,11.12,35.24,11.043,35.335,11.032,35.454,11.001,35.552,11.004,35.634,10.784,35.772,10.689,35.8,10.591,35.887,10.506,36.032,10.477,36.175,10.488,36.255,10.526,36.323,10.642,36.42,10.798,36.493,10.967,36.743,11.057,36.841,11.127,36.874,11.077,36.967,11.054,37.073,10.951,37.059,10.766,36.93,10.571,36.879,10.518,36.791,10.412,36.732,10.293,36.781,10.334,36.865,10.189,37.034,10.196,37.206,10.087,37.251,9.988,37.258,9.876,37.254,9.879,37.213,9.896,37.182,9.83,37.135,9.784,37.211,9.816,37.255,9.838,37.309,9.759,37.33,9.688,37.34,9.142,37.195,9.059,37.156,8.824,36.998,8.577,36.937,8.127,36.91,7.91,36.856,7.792,36.88,7.608,37,7.432,37.059,7.204,37.092,7.238,36.969,7.143,36.943,6.928,36.919,6.576,37.003,6.487,37.086,6.328,37.046,6.249,36.938,6.065,36.864,5.725,36.8,5.425,36.675,5.295,36.648,5.196,36.677,4.995,36.808,4.878,36.862,4.758,36.896,3.779,36.896,3.521,36.795,2.973,36.784,2.846,36.739,2.593,36.601,2.343,36.61,1.975,36.568,1.257,36.52,0.972,36.444,0.791,36.357,0.515,36.262,0.312,36.162,0.152,36.063,0.048,35.901,-0.048,35.833,-0.189,35.819,-0.351,35.863,-0.426,35.862,-0.917,35.668,-1.088,35.579,-1.205,35.496,-1.336,35.364,-1.484,35.303,-1.674,35.183,-1.913,35.094,-2.018,35.085,-2.22,35.104,-2.424,35.123,-2.637,35.113,-2.731,35.135,-2.84,35.128,-2.87,35.173,-2.926,35.287,-2.954,35.315,-2.958,35.363,-2.972,35.407,-3.063,35.317,-3.206,35.239,-3.395,35.212,-3.591,35.228,-3.693,35.28,-3.788,35.245,-3.982,35.243,-4.33,35.161,-4.628,35.206,-4.837,35.281,-5.105,35.468,-5.253,35.615,-5.338,35.745,-5.338,35.857,-5.278,35.903,-5.397,35.93,-5.522,35.862,-5.623,35.829,-5.748,35.816,-5.925,35.786,-5.958,35.681,-6.353,34.776,-6.756,34.133,-6.901,33.969,-7.145,33.83,-7.562,33.64,-8.301,33.374,-8.513,33.252,-8.596,33.187,-8.836,32.92,-9.246,32.572,-9.249,32.486,-9.287,32.241,-9.347,32.086,-9.675,31.711,-9.809,31.425,-9.833,31.07,-9.832,30.847,-9.875,30.718,-9.854,30.645,-9.773,30.603,-9.653,30.448,-9.624,30.353,-9.667,30.109,-9.743,29.958,-9.853,29.809,-10.01,29.641,-10.201,29.38,-10.486,29.065,-10.674,28.939,-11.081,28.714,-11.299,28.526,-11.43,28.382,-11.553,28.31,-11.986,28.129,-12.469,28.009,-12.794,27.978,-12.949,27.914,-13.041,27.77,-13.176,27.656,-13.256,27.435,-13.41,27.147,-13.496,26.873,-13.576,26.735,-13.696,26.643,-13.952,26.489,-14.168,26.415,-14.312,26.297,-14.414,26.254,-14.471,26.163,-14.523,25.925,-14.602,25.809,-14.707,25.548,-14.795,25.404,-14.843,25.22,-14.856,24.872,-14.904,24.72,-15.039,24.549,-15.189,24.479,-15.586,24.073,-15.778,23.953,-15.899,23.844,-15.953,23.741,-15.981,23.67,-15.913,23.728,-15.855,23.8,-15.802,23.842,-15.789,23.793,-15.806,23.75,-15.943,23.553,-15.997,23.425,-16.114,23.228,-16.21,23.098,-16.17,23.032,-16.202,22.945,-16.304,22.835,-16.359,22.595,-16.514,22.333,-16.684,22.274,-16.793,22.16,-16.931,21.9,-17.01,21.377,-17.099,20.857,-17.048,20.806,-16.998,21.04,-16.971,21.076,-16.928,21.115,-16.876,21.086,-16.728,20.806,-16.623,20.634,-16.563,20.604,-16.535,20.654,-16.53,20.71,-16.479,20.69,-16.43,20.652,-16.334,20.416,-16.21,20.228,-16.241,20.141,-16.233,20.001,-16.283,19.787,-16.445,19.473,-16.305,19.513,-16.371,19.41,-16.475,19.391,-16.514,19.362,-16.476,19.285,-16.306,19.154,-16.213,19.003,-16.15,18.718,-16.085,18.521,-16.047,18.223,-16.03,17.888,-16.079,17.546,-16.207,17.193,-16.347,16.926,-16.464,16.602,-16.481,16.454,-16.536,16.287,-16.535,15.838,-16.571,15.734,-16.843,15.294,-17.147,14.922,-17.412,14.792,-17.536,14.755,-17.445,14.652,-17.418,14.723,-17.346,14.729,-17.261,14.701,-17.168,14.641,-17.079,14.483,-16.974,14.403,-16.881,14.208,-16.798,14.093,-16.792,14.004,-16.742,14.006,-16.667,14.036,-16.618,14.041,-16.64,14.007,-16.734,13.961,-16.767,13.905,-16.745,13.84,-16.648,13.771,-16.588,13.69,-16.562,13.587,-16.53,13.458,-16.441,13.353,-16.352,13.343,-16.135,13.448,-15.85,13.46,-15.57,13.5,-15.438,13.483,-15.427,13.468,-15.471,13.459,-15.618,13.46,-15.804,13.425,-15.986,13.409,-16.158,13.384,-16.188,13.326,-16.185,13.283,-16.272,13.294,-16.413,13.27,-16.556,13.303,-16.598,13.357,-16.615,13.435,-16.669,13.475,-16.75,13.425,-16.825,13.341,-16.769,13.148,-16.763,13.064,-16.757,12.98,-16.769,12.883,-16.759,12.702,-16.778,12.67,-16.768,12.628,-16.744,12.585,-16.701,12.603,-16.673,12.622,-16.638,12.685,-16.598,12.715,-16.549,12.664,-16.455,12.625,-16.443,12.609,-16.45,12.581,-16.488,12.582,-16.553,12.605,-16.678,12.56,-16.76,12.526,-16.785,12.473,-16.746,12.4,-16.712,12.355,-16.437,12.204,-16.312,12.243,-16.245,12.237,-16.255,12.206,-16.319,12.144,-16.328,12.052,-16.274,11.978,-16.138,11.917,-15.959,11.96,-15.92,11.938,-15.903,11.92,-15.942,11.787,-15.819,11.763,-15.651,11.818,-15.513,11.918,-15.435,11.944,-15.188,11.927,-15.112,11.97,-15.078,11.969,-15.072,11.947,-15.102,11.914,-15.133,11.907,-15.211,11.871,-15.416,11.872,-15.467,11.843,-15.5,11.778,-15.502,11.724,-15.413,11.615,-15.36,11.623,-15.317,11.669,-15.23,11.687,-15.122,11.662,-15.073,11.598,-15.164,11.581,-15.253,11.573,-15.429,11.499,-15.479,11.41,-15.449,11.39,-15.399,11.401,-15.355,11.396,-15.348,11.378,-15.395,11.334,-15.401,11.266,-15.393,11.217,-15.317,11.152,-15.263,11.161,-15.217,11.156,-15.222,11.031,-15.181,11.034,-15.097,11.14,-15.055,11.142,-15.094,11.011,-15.043,10.94,-15.051,10.835,-15.012,10.804,-14.975,10.803,-14.925,10.945,-14.887,10.968,-14.837,10.963,-14.776,10.932,-14.757,10.862,-14.693,10.741,-14.677,10.689,-14.594,10.767,-14.587,10.735,-14.614,10.618,-14.61,10.55,-14.427,10.248,-14.17,10.129,-14.086,10.127,-14.045,10.141,-14.03,10.115,-14.022,10.048,-13.955,9.969,-13.82,9.887,-13.754,9.87,-13.713,9.923,-13.69,9.928,-13.7,9.851,-13.659,9.776,-13.657,9.639,-13.691,9.536,-13.568,9.543,-13.436,9.42,-13.406,9.361,-13.396,9.314,-13.296,9.219,-13.269,9.171,-13.303,9.078,-13.293,9.049,-13.272,8.987,-13.154,8.898,-13.059,8.881,-13.071,8.856,-13.207,8.843,-13.226,8.766,-13.228,8.696,-13.182,8.577,-13.122,8.589,-13.088,8.626,-12.953,8.615,-12.904,8.656,-12.894,8.63,-12.913,8.582,-12.994,8.526,-13.085,8.425,-13.158,8.442,-13.203,8.484,-13.261,8.488,-13.273,8.43,-13.202,8.336,-13.149,8.215,-13.021,8.201,-12.957,8.145,-12.925,8.055,-12.881,7.857,-12.851,7.819,-12.782,7.791,-12.698,7.716,-12.57,7.701,-12.51,7.753,-12.48,7.753,-12.51,7.666,-12.433,7.545,-12.481,7.442,-12.486,7.386,-12.347,7.342,-11.929,7.184,-11.733,7.089,-11.548,6.947,-11.508,6.907,-11.292,6.688,-11.005,6.557,-10.849,6.465,-10.786,6.31,-10.708,6.258,-10.597,6.211,-10.418,6.167,-10.276,6.078,-9.654,5.519,-9.375,5.241,-9.132,5.055,-8.259,4.59,-7.998,4.509,-7.66,4.367,-7.545,4.351,-7.426,4.376,-7.231,4.486,-7.058,4.545,-6.923,4.638,-6.845,4.671,-6.548,4.762,-6.062,4.953,-5.914,5.011,-5.565,5.089,-5.062,5.131,-5.105,5.162,-5.266,5.16,-5.368,5.151,-5.335,5.192,-5.282,5.21,-5.024,5.204,-4.97,5.148,-4.9,5.138,-4.662,5.173,-4.037,5.23,-4.062,5.257,-4.115,5.262,-4.609,5.236,-4.553,5.28,-4.357,5.301,-4.12,5.31,-3.984,5.293,-3.871,5.221,-3.348,5.131,-3.312,5.161,-3.238,5.335,-3.2,5.354,-3.151,5.348,-3.169,5.203,-3.064,5.158,-3.026,5.151,-3.019,5.131,-3.087,5.128,-3.215,5.147,-3.246,5.114,-3.114,5.089,-3.082,5.082,-2.965,5.046,-2.723,5.014,-2.399,4.929,-2.266,4.874,-2.09,4.764,-2.002,4.762,-1.777,4.88,-1.638,4.981,-1.502,5.038,-1.064,5.183,-0.798,5.227,-0.669,5.319,-0.485,5.394,-0.349,5.501,-0.127,5.568,0.26,5.757,0.672,5.76,0.749,5.76,0.95,5.81,1.008,5.906,1.05,5.994,1.106,6.051,1.187,6.089,1.311,6.147,1.623,6.217,1.818,6.261,2.287,6.328,2.706,6.369,2.772,6.376,3.336,6.397,3.43,6.525,3.503,6.531,3.717,6.598,3.752,6.584,3.546,6.477,3.49,6.457,3.451,6.427,3.487,6.409,4.126,6.411,4.431,6.349,4.634,6.217,4.861,6.026,5.042,5.798,5.093,5.767,5.106,5.728,5.112,5.642,5.173,5.603,5.276,5.642,5.305,5.694,5.327,5.708,5.325,5.648,5.35,5.623,5.418,5.625,5.457,5.612,5.394,5.575,5.289,5.577,5.216,5.572,5.199,5.534,5.232,5.484,5.386,5.402,5.55,5.474,5.532,5.426,5.501,5.379,5.439,5.365,5.368,5.338,5.364,5.259,5.37,5.195,5.388,5.174,5.476,5.154,5.452,5.127,5.403,5.142,5.383,5.129,5.448,4.946,5.493,4.839,5.554,4.733,5.588,4.647,5.799,4.456,5.906,4.388,5.971,4.339,6.077,4.291,6.173,4.277,6.206,4.292,6.215,4.385,6.271,4.432,6.275,4.372,6.256,4.334,6.264,4.309,6.3,4.304,6.462,4.333,6.5,4.332,6.555,4.341,6.58,4.476,6.602,4.455,6.617,4.376,6.633,4.34,6.715,4.342,6.757,4.344,6.86,4.373,6.793,4.469,6.792,4.593,6.786,4.652,6.768,4.725,6.788,4.725,6.825,4.645,6.839,4.523,6.868,4.441,6.923,4.391,7.013,4.397,7.155,4.514,7.164,4.616,7.087,4.686,7.077,4.716,7.144,4.684,7.207,4.612,7.284,4.548,7.46,4.555,7.509,4.595,7.517,4.645,7.531,4.655,7.566,4.561,7.644,4.525,7.801,4.522,8.029,4.555,8.293,4.558,8.328,4.656,8.234,4.907,8.253,4.924,8.342,4.825,8.394,4.814,8.431,4.746,8.515,4.725,8.544,4.758,8.556,4.755,8.571,4.752,8.533,4.606,8.54,4.572,8.574,4.526,8.656,4.516,8.69,4.55,8.66,4.671,8.708,4.646,8.762,4.58,8.807,4.573,8.856,4.579,8.889,4.573,8.918,4.554,8.903,4.435,8.914,4.358,8.932,4.29,8.977,4.23,9,4.092,9.114,4.041,9.249,3.998,9.297,3.973,9.311,3.94,9.362,3.926,9.425,3.922,9.462,3.943,9.501,4.001,9.484,4.066,9.512,4.061,9.551,4.028,9.6,4.027,9.67,4.077,9.689,4.056,9.649,4.008,9.64,3.965,9.736,3.88,9.74,3.853,9.628,3.87,9.593,3.814,9.556,3.798,9.616,3.696,9.642,3.612,9.766,3.624,9.672,3.538,9.876,3.31,9.915,3.24,9.948,3.079,9.885,2.917,9.868,2.735,9.822,2.539,9.801,2.304,9.78,2.068,9.807,1.927,9.719,1.789,9.648,1.618,9.632,1.566,9.584,1.54,9.494,1.435,9.434,1.296,9.386,1.139,9.445,1.121,9.51,1.115,9.599,1.054,9.591,1.032,9.575,0.991,9.626,0.779,9.625,0.632,9.618,0.577,9.601,0.568,9.557,0.594,9.539,0.659,9.495,0.665,9.33,0.611,9.325,0.552,9.399,0.487,9.47,0.362,9.546,0.296,9.777,0.192,9.944,0.22,10.001,0.195,9.813,0.126,9.797,0.044,9.738,0.085,9.574,0.149,9.468,0.16,9.411,0.2,9.386,0.246,9.376,0.307,9.355,0.344,9.302,0.289,9.325,0.116,9.339,-0.058,9.297,-0.351,9.137,-0.573,9.082,-0.624,9.038,-0.637,8.995,-0.635,8.946,-0.689,8.821,-0.708,8.757,-0.615,8.703,-0.591,8.844,-0.914,8.877,-0.946,8.909,-1.025,8.942,-1.071,9.065,-1.298,9.204,-1.382,9.26,-1.374,9.297,-1.361,9.318,-1.333,9.347,-1.325,9.28,-1.482,9.296,-1.515,9.331,-1.535,9.397,-1.53,9.448,-1.509,9.501,-1.555,9.523,-1.598,9.406,-1.635,9.357,-1.638,9.319,-1.632,9.036,-1.309,9.053,-1.379,9.158,-1.528,9.258,-1.726,9.248,-1.779,9.266,-1.825,9.342,-1.829,9.483,-1.895,9.495,-1.935,9.483,-1.962,9.342,-1.894,9.299,-1.903,9.371,-1.975,9.402,-2.028,9.533,-2.164,9.574,-2.23,9.591,-2.293,9.625,-2.367,9.676,-2.416,9.769,-2.413,9.861,-2.443,9.959,-2.49,10.062,-2.55,10.034,-2.576,10.002,-2.588,9.764,-2.474,9.722,-2.468,9.759,-2.519,10.006,-2.748,10.348,-3.013,10.585,-3.278,10.641,-3.398,10.849,-3.561,10.947,-3.662,11.032,-3.826,11.13,-3.916,11.364,-4.131,11.394,-4.2,11.668,-4.434,11.778,-4.566,11.781,-4.677,11.801,-4.705,11.821,-4.755,11.893,-4.866,11.967,-4.954,12.003,-4.982,12.018,-5.004,12.04,-5.035,12.111,-5.197,12.177,-5.325,12.207,-5.468,12.18,-5.539,12.155,-5.633,12.199,-5.732,12.214,-5.759,12.24,-5.807,12.315,-5.895,12.412,-5.986,12.453,-6,12.515,-6.004,12.681,-5.961,12.792,-5.878,12.861,-5.854,13.003,-5.836,13.068,-5.865,13.01,-5.908,12.791,-6.004,12.554,-6.046,12.38,-6.084,12.303,-6.093,12.283,-6.124,12.334,-6.187,12.402,-6.353,12.521,-6.59,12.823,-6.955,12.862,-7.232,13.091,-7.78,13.379,-8.37,13.366,-8.469,13.368,-8.555,13.378,-8.625,13.359,-8.687,13.054,-9.007,13.047,-8.975,13.077,-8.934,13.093,-8.9,13.047,-8.922,12.999,-8.991,12.999,-9.048,13.076,-9.23,13.156,-9.39,13.197,-9.551,13.209,-9.703,13.288,-9.827,13.332,-9.999,13.495,-10.257,13.539,-10.421,13.633,-10.512,13.721,-10.634,13.739,-10.757,13.834,-10.93,13.847,-11.054,13.784,-11.488,13.785,-11.813,13.686,-12.124,13.598,-12.286,13.417,-12.52,13.163,-12.652,12.983,-12.776,12.898,-13.028,12.55,-13.438,12.504,-13.755,12.379,-14.039,12.28,-14.637,12.073,-15.248,12.016,-15.514,11.968,-15.634,11.9,-15.72,11.85,-15.768,11.751,-15.832,11.769,-15.915,11.797,-15.986,11.82,-16.504,11.819,-16.704,11.78,-16.871,11.743,-17.249,11.722,-17.467,11.733,-17.751,11.776,-18.002,11.951,-18.271,12.041,-18.471,12.096,-18.541,12.329,-18.751,12.458,-18.927,13.042,-20.028,13.168,-20.185,13.284,-20.524,13.451,-20.917,13.839,-21.473,13.888,-21.607,13.973,-21.768,14.322,-22.19,14.463,-22.449,14.526,-22.703,14.52,-22.805,14.496,-22.921,14.459,-22.908,14.438,-22.881,14.403,-22.968,14.424,-23.079,14.474,-23.281,14.472,-23.477,14.497,-23.643,14.483,-24.05,14.502,-24.202,14.628,-24.548,14.768,-24.788,14.837,-25.033,14.819,-25.246,14.823,-25.359,14.864,-25.534,14.845,-25.726,14.931,-25.958,14.968,-26.318,15.097,-26.426,15.139,-26.508,15.163,-26.6,15.124,-26.668,15.133,-26.788,15.216,-26.995,15.288,-27.275,15.342,-27.387,15.719,-27.966,15.891,-28.153,16.007,-28.232,16.335,-28.537,16.448,-28.618,16.481,-28.642,16.739,-29.009,16.95,-29.403,17.189,-30.1,17.347,-30.445,17.677,-31.019,17.939,-31.383,18.164,-31.655,18.211,-31.742,18.311,-32.122,18.33,-32.27,18.325,-32.505,18.251,-32.652,18.125,-32.749,18.037,-32.775,17.965,-32.709,17.895,-32.75,17.851,-32.827,17.878,-32.962,17.958,-33.046,17.993,-33.152,18.075,-33.207,18.156,-33.359,18.261,-33.422,18.309,-33.514,18.433,-33.717,18.456,-33.796,18.465,-33.888,18.354,-33.939,18.333,-34.074,18.352,-34.188,18.41,-34.296,18.462,-34.347,18.462,-34.168,18.5,-34.109,18.534,-34.086,18.605,-34.077,18.709,-34.072,18.752,-34.083,18.809,-34.108,18.826,-34.188,18.831,-34.254,18.825,-34.296,18.831,-34.364,18.902,-34.361,18.952,-34.344,19.098,-34.35,19.149,-34.417,19.245,-34.412,19.279,-34.437,19.331,-34.492,19.323,-34.571,19.298,-34.615,19.392,-34.606,19.635,-34.753,19.85,-34.757,19.926,-34.775,20.021,-34.786,20.435,-34.509,20.53,-34.463,20.775,-34.44,20.882,-34.387,20.99,-34.367,21.06,-34.365,21.249,-34.407,21.35,-34.408,21.553,-34.373,21.789,-34.373,22.246,-34.069,22.414,-34.054,22.554,-34.01,22.736,-34.01,22.926,-34.063,23.268,-34.081,23.35,-34.069,23.586,-33.985,23.698,-33.993,24.183,-34.062,24.596,-34.175,24.827,-34.169,24.906,-34.06,25.003,-33.974,25.17,-33.961,25.477,-34.028,25.574,-34.035,25.638,-34.011,25.652,-33.85,25.806,-33.737,25.99,-33.711,26.429,-33.76,26.614,-33.707,27.077,-33.521,27.364,-33.361,27.762,-33.096,27.861,-33.054,28.214,-32.769,28.449,-32.625,28.856,-32.294,29.128,-32.003,29.483,-31.675,29.735,-31.47,29.83,-31.424,29.971,-31.322,30.289,-30.97,30.472,-30.715,30.664,-30.434,30.878,-30.071,31.023,-29.901,31.17,-29.591,31.335,-29.378,31.778,-28.937,31.892,-28.912,31.955,-28.884,32.027,-28.84,32.286,-28.621,32.375,-28.498,32.535,-28.2,32.657,-27.607,32.706,-27.442,32.849,-27.08,32.886,-26.849,32.889,-26.83,32.934,-26.252,32.955,-26.084,32.916,-26.087,32.894,-26.13,32.849,-26.268,32.804,-26.241,32.77,-26.203,32.704,-26.158,32.647,-26.092,32.59,-26.004,32.656,-25.902,32.723,-25.821,32.792,-25.644,32.961,-25.49,33.347,-25.261,33.53,-25.189,33.836,-25.068,34.607,-24.821,34.992,-24.651,35.156,-24.541,35.255,-24.43,35.438,-24.171,35.49,-24.066,35.542,-23.824,35.522,-23.785,35.485,-23.784,35.462,-23.851,35.399,-23.838,35.37,-23.798,35.377,-23.708,35.494,-23.185,35.575,-22.963,35.506,-22.772,35.49,-22.658,35.542,-22.377,35.54,-22.303,35.53,-22.248,35.505,-22.19,35.494,-22.125,35.456,-22.116,35.419,-22.178,35.401,-22.316,35.408,-22.403,35.383,-22.455,35.316,-22.397,35.326,-22.26,35.329,-22.037,35.273,-21.762,35.268,-21.651,35.128,-21.395,35.118,-21.195,34.982,-20.806,34.877,-20.671,34.765,-20.562,34.705,-20.473,34.698,-20.404,34.75,-20.091,34.745,-19.929,34.756,-19.822,34.713,-19.767,34.649,-19.701,34.721,-19.71,34.852,-19.821,34.891,-19.822,34.948,-19.813,35.365,-19.494,35.651,-19.164,35.854,-18.993,35.98,-18.912,36.125,-18.842,36.183,-18.871,36.236,-18.861,36.263,-18.72,36.327,-18.793,36.404,-18.77,36.412,-18.693,36.498,-18.576,36.54,-18.518,36.756,-18.307,36.9,-18.129,36.919,-18.08,36.939,-17.993,37,-17.935,37.051,-17.909,37.245,-17.74,37.512,-17.571,37.839,-17.393,38.048,-17.321,38.087,-17.276,38.145,-17.243,38.381,-17.17,38.633,-17.078,38.67,-17.05,38.713,-17.046,38.758,-17.055,38.885,-17.042,38.956,-17.005,39.084,-16.973,39.182,-16.842,39.242,-16.793,39.625,-16.579,39.765,-16.468,39.845,-16.436,39.791,-16.295,39.86,-16.252,39.984,-16.225,40.099,-16.065,40.109,-16.025,40.109,-15.979,40.208,-15.867,40.314,-15.764,40.559,-15.473,40.651,-15.261,40.653,-15.193,40.618,-15.116,40.642,-15.082,40.694,-15.065,40.687,-15.012,40.701,-14.93,40.776,-14.842,40.835,-14.792,40.845,-14.719,40.821,-14.635,40.827,-14.569,40.812,-14.536,40.818,-14.468,40.775,-14.421,40.727,-14.421,40.646,-14.539,40.636,-14.452,40.64,-14.39,40.713,-14.291,40.716,-14.214,40.65,-14.199,40.603,-14.167,40.596,-14.123,40.591,-13.845,40.56,-13.62,40.558,-13.531,40.545,-13.463,40.583,-13.374,40.552,-13.294,40.57,-13.223,40.564,-13.115,40.573,-13.058,40.569,-12.985,40.437,-12.983,40.435,-12.936,40.448,-12.905,40.553,-12.825,40.572,-12.758,40.581,-12.636,40.548,-12.527,40.487,-12.492,40.523,-12.393,40.509,-12.313,40.501,-12.119,40.532,-12.005,40.51,-11.94,40.494,-11.844,40.433,-11.657,40.465,-11.449,40.403,-11.332,40.421,-11.266,40.491,-11.179,40.545,-11.066,40.527,-11.025,40.506,-10.998,40.516,-10.93,40.597,-10.831,40.487,-10.765,40.555,-10.716,40.612,-10.662,40.517,-10.567,40.464,-10.464,40.453,-10.443,40.436,-10.41,40.389,-10.354,40.216,-10.241,40.138,-10.203,40.084,-10.157,39.984,-10.16,39.945,-10.092,39.864,-10.022,39.725,-10,39.784,-9.915,39.775,-9.837,39.728,-9.725,39.697,-9.578,39.625,-9.409,39.641,-9.192,39.451,-8.943,39.48,-8.906,39.488,-8.862,39.377,-8.721,39.304,-8.444,39.309,-8.351,39.34,-8.243,39.441,-8.012,39.428,-7.813,39.33,-7.747,39.287,-7.788,39.288,-7.518,39.353,-7.341,39.433,-7.207,39.519,-7.124,39.546,-7.024,39.472,-6.879,39.287,-6.815,39.228,-6.685,39.125,-6.556,39.067,-6.499,38.981,-6.455,38.874,-6.331,38.855,-6.205,38.805,-6.07,38.819,-5.878,38.911,-5.626,38.978,-5.519,39.058,-5.232,39.088,-5.165,39.119,-5.065,39.123,-4.98,39.202,-4.776,39.222,-4.692,39.228,-4.666,39.288,-4.609,39.377,-4.625,39.491,-4.478,39.637,-4.153,39.658,-4.119,39.687,-4.068,39.732,-3.993,39.746,-3.955,39.761,-3.913,39.819,-3.786,39.861,-3.577,39.896,-3.536,39.937,-3.442,39.992,-3.351,40.115,-3.251,40.128,-3.173,40.195,-3.019,40.18,-2.819,40.222,-2.688,40.279,-2.629,40.404,-2.556,40.644,-2.539,40.813,-2.392,40.82,-2.336,40.898,-2.27,40.922,-2.194,40.906,-2.137,40.89,-2.024,40.917,-2.042,40.952,-2.056,40.971,-1.992,40.996,-1.951,41.059,-1.975,41.107,-1.982,41.267,-1.945,41.387,-1.867,41.533,-1.695,41.632,-1.579,41.732,-1.43,41.846,-1.203,41.888,-1.151,41.926,-1.056,41.98,-0.973,42.106,-0.856,42.219,-0.738,42.399,-0.51,42.466,-0.457,42.561,-0.321,42.634,-0.251,42.712,-0.176,43.468,0.622,43.718,0.858,44.033,1.106,44.333,1.391,44.92,1.81,45.826,2.31,46.051,2.475,46.879,3.286,47.511,3.968,47.975,4.497,48.234,4.953,48.649,5.494,49.049,6.174,49.093,6.408,49.235,6.777,49.349,6.991,49.57,7.297,49.671,7.47,49.761,7.66,49.852,7.963,50.103,8.2,50.286,8.509,50.321,8.62,50.43,8.845,50.638,9.109,50.685,9.241,50.825,9.428,50.833,9.71,50.874,9.924,50.898,10.253,50.93,10.336,51.036,10.385,51.209,10.431,51.268,10.403,51.385,10.387,51.39,10.423,51.369,10.475,51.296,10.499,51.193,10.555,51.186,10.53,51.188,10.48,51.063,10.434,51.032,10.445,51.051,10.472,51.094,10.489,51.105,10.536,51.131,10.596,51.141,10.657,51.122,11.077,51.084,11.336,51.136,11.505,51.218,11.658,51.232,11.745,51.255,11.831,51.191,11.842,50.792,11.984,50.636,11.944,50.528,11.823,50.466,11.728,50.11,11.529,49.642,11.451,49.388,11.343,49.062,11.271,48.939,11.258,48.903,11.255,48.674,11.323,48.573,11.321,48.439,11.29,48.019,11.139,47.713,11.112,47.474,11.175,47.405,11.174,47.23,11.1,46.973,10.925,46.565,10.746,46.46,10.734,46.254,10.781,46.025,10.794,45.817,10.836,45.696,10.804,45.338,10.65,44.943,10.437,44.387,10.43,44.279,10.472,44.158,10.551,43.853,10.784,43.631,11.035,43.441,11.346,43.246,11.5,43.162,11.566,43.043,11.588,42.912,11.587,42.79,11.562,42.653,11.51,42.584,11.497,42.54,11.504,42.522,11.572,42.64,11.56,42.799,11.739,43.048,11.829,43.272,11.97,43.337,12.027,43.38,12.091,43.41,12.19,43.354,12.367,43.299,12.464,43.131,12.66,43.117,12.709,43.083,12.825,42.999,12.9,42.97,12.808,42.796,12.864,42.734,13.019,42.523,13.221,42.399,13.213,42.346,13.398,42.245,13.588,41.658,13.983,41.48,14.244,41.176,14.62,40.799,14.743,40.634,14.883,40.546,14.934,40.437,14.964,40.305,14.974,40.204,15.014,40.084,15.152,40.058,15.217,40.041,15.335,39.978,15.393,39.864,15.47,39.813,15.414,39.79,15.319,39.816,15.245,39.819,15.201,39.786,15.125,39.721,15.214,39.631,15.453,39.579,15.523,39.507,15.532,39.422,15.787,39.299,15.921,39.223,16.194,39.143,16.729,39.034,17.086,38.912,17.427,38.609,18.005,38.574,18.073,38.333,18.219,38.283,18.287,38.252,18.264,38.202,18.249,38.128,18.333,38.074,18.41,37.922,18.556,37.73,18.694,37.599,18.717,37.532,18.753,37.471,18.82,37.362,19.092,37.248,19.582,37.263,19.792,37.193,20.121,37.188,20.395,37.228,20.557,37.173,20.732,37.157,20.895,37.141,20.982,37.151,21.104,37.217,21.078,37.257,21.039,37.263,21.073,37.259,21.109,37.212,21.186,37.081,21.326,36.927,21.587,36.883,21.769,36.871,21.997,36.87,22.016,36.83,22.098,36.415,22.394,36.23,22.629,35.913,22.74,35.846,22.786,35.797,22.849,35.698,22.946,35.564,23.271,35.523,23.443,35.504,23.779,35.515,23.843,35.541,23.921,35.594,23.943,35.632,23.95,35.784,23.938,35.625,24.066,35.478,24.155,35.397,24.27,35.194,24.475,34.853,25.14,34.679,25.443,34.565,25.691,34.329,26.024,34.05,26.551,33.959,26.649,33.893,27.049,33.849,27.185,33.802,27.268,33.697,27.341,33.657,27.431,33.55,27.607,33.559,27.701,33.547,27.898,33.495,27.974,33.372,28.051,33.202,28.208,33.023,28.442,32.898,28.565,32.857,28.631,32.829,28.703,32.784,28.787,32.659,28.928,32.632,28.992,32.638,29.182,32.599,29.322,32.565,29.386,32.397,29.534,32.36,29.631,32.409,29.749,32.489,29.852,32.473,29.925,32.566,29.974,32.647,29.798,32.721,29.522,32.767,29.45,32.812,29.4,32.871,29.286,33.076,29.073,33.13,28.978,33.204,28.778,33.202,28.696,33.248,28.568,33.416,28.39,33.594,28.256,33.76,28.048,34.045,27.829,34.22,27.764,34.319,27.889,34.4,28.016,34.427,28.106,34.446,28.357,34.617,28.758,34.736,29.271,34.849,29.432,34.904,29.477,34.973,29.555,34.982,29.484,34.951,29.354,34.799,28.721,34.78,28.507,34.683,28.264,34.616,28.148,34.625,28.065,34.722,28.131,34.828,28.109,35.078,28.087,35.18,28.035,35.424,27.734,35.581,27.432,35.763,27.259,35.852,27.07,36.032,26.881,36.094,26.766,36.25,26.595,36.519,26.105,36.675,26.039,36.703,25.903,36.763,25.751,36.86,25.692,36.921,25.641,37.149,25.291,37.218,25.151,37.243,25.073,37.266,24.96,37.22,24.873,37.181,24.82,37.338,24.616,37.431,24.459,37.543,24.292,37.638,24.278,37.713,24.274,37.821,24.188,37.92,24.185,37.978,24.125,38.099,24.058,38.289,23.911,38.464,23.712,38.542,23.558,38.706,23.306,38.757,23.194,38.797,23.049,38.836,22.989,38.941,22.882,38.883,22.882,38.939,22.805,39.007,22.77,39.001,22.699,39.062,22.592,39.096,22.393,39.07,22.294,39.034,22.203,39.021,22.033,38.988,21.882,39.03,21.776,39.091,21.664,39.147,21.519,39.151,21.433,39.094,21.31,39.276,20.974,39.491,20.737,39.614,20.518,39.728,20.39,39.884,20.293,40.081,20.266,40.482,19.993,40.616,19.822,40.759,19.755,40.777,19.717,40.792,19.646,40.848,19.555,40.913,19.49,41.116,19.082,41.144,18.989,41.191,18.871,41.221,18.765,41.229,18.678,41.432,18.452,41.508,18.256,41.658,18.008,41.75,17.886,42.052,17.669,42.294,17.435,42.332,17.257,42.383,17.122,42.475,17.05,42.544,16.96,42.553,16.868,42.647,16.801,42.699,16.737,42.726,16.653,42.731,16.57,42.79,16.452,42.799,16.372,42.84,16.032,42.717,15.655,42.8,15.372,42.799,15.326,42.788,15.266,42.736,15.294,42.698,15.326,42.658,15.233,42.856,15.133,42.897,15.006,42.936,14.939,42.937,14.898,42.913,14.863,42.922,14.817,42.947,14.773,43.021,14.555,43.019,14.521,43.006,14.483,43.045,14.342,43.093,14.204,43.089,14.011,43.234,13.859,43.282,13.693,43.283,13.64,43.232,13.267,43.475,12.839,43.488,12.699,43.634,12.744,43.835,12.674,43.93,12.617,44.006,12.608,44.112,12.639,44.26,12.645,44.358,12.669,44.618,12.817,44.755,12.764,44.89,12.784,45.039,12.816,45.11,12.939,45.164,12.998,45.394,13.067,45.534,13.233,45.657,13.339,45.92,13.394,46.203,13.424,46.502,13.416,46.663,13.433,46.789,13.466,46.976,13.547,47.243,13.609,47.408,13.662,47.633,13.858,47.855,13.957,47.916,14.013,47.99,14.048,48.278,13.998,48.449,14.006,48.594,14.046,48.668,14.05,48.78,14.124,48.929,14.267,49.005,14.355,49.048,14.456,49.103,14.5,49.35,14.638,49.549,14.722,49.906,14.828,50.167,14.851,50.339,14.927,50.527,15.038,51.015,15.141,51.322,15.226,51.604,15.337,51.682,15.379,51.749,15.44,51.831,15.459,51.966,15.536,52.087,15.586,52.217,15.656,52.222,15.761,52.174,15.957,52.237,16.171,52.328,16.294,52.448,16.391,52.581,16.47,53.086,16.648,53.298,16.723,53.61,16.76,53.775,16.856,53.954,16.918,54.068,17.006,54.377,17.034,54.567,17.031,54.665,17.009,54.772,16.965,55.064,17.039,55.174,17.158,55.275,17.321,55.296,17.382,55.281,17.446,55.238,17.505,55.255,17.586,55.479,17.843,55.614,17.886,55.998,17.935,56.27,17.951,56.383,17.988,56.551,18.166,56.655,18.587,56.826,18.754,56.957,18.828,57.177,18.903,57.428,18.944,57.676,18.958,57.738,18.977,57.812,19.017,57.79,19.146,57.764,19.253,57.761,19.432,57.715,19.607,57.714,19.678,57.741,19.804,57.802,19.955,57.844,20.118,57.862,20.244,57.947,20.344,58.103,20.57,58.169,20.59,58.245,20.599,58.232,20.507,58.209,20.424,58.266,20.395,58.349,20.387,58.474,20.407,58.534,20.504,58.69,20.807,58.896,21.113,59.069,21.289,59.304,21.435,59.371,21.499,59.518,21.782,59.653,21.951,59.681,22.054,59.8,22.22,59.824,22.305,59.838,22.421,59.823,22.509,59.696,22.546,59.535,22.579,59.429,22.661,59.311,22.793,59.195,22.972,59.03,23.131,58.983,23.235,58.912,23.334,58.83,23.397,58.773,23.517,58.578,23.643,58.5,23.646,58.393,23.618,58.325,23.624,58.12,23.717,57.825,23.759,57.611,23.804,57.22,23.923,57.123,23.981,56.913,24.15,56.774,24.335,56.641,24.47,56.49,24.716,56.388,24.979,56.373,25.018,56.363,25.569,56.298,25.651,56.307,25.709,56.329,25.752,56.374,25.805,56.416,26.109,56.418,26.208,56.43,26.327,56.413,26.351,56.379,26.356,56.346,26.314,56.306,26.235,56.228,26.22,56.197,26.229,56.164,26.207,56.08,26.063,56.075,26.053,56.025,25.916,55.941,25.794,55.523,25.498,55.433,25.394,55.322,25.3,55.304,25.237,55.098,25.042,54.747,24.81,54.659,24.716,54.624,24.621,54.58,24.564,54.535,24.531,54.499,24.463,54.458,24.358,54.397,24.278,54.304,24.254,54.148,24.171,53.893,24.077,53.802,24.069,53.33,24.098,53.026,24.147,52.648,24.155,52.511,24.113,52.251,23.995,52.119,23.971,51.906,23.985,51.843,24.011,51.792,24.075,51.768,24.254,51.735,24.263,51.665,24.25,51.623,24.301,51.605,24.338,51.568,24.286,51.535,24.286,51.477,24.308,51.395,24.319,51.31,24.34,51.37,24.477,51.418,24.531,51.411,24.571,51.338,24.564,51.268,24.607,51.396,24.645,51.428,24.668,51.533,24.891,51.587,24.965,51.609,25.053,51.602,25.148,51.561,25.284,51.52,25.39,51.51,25.452,51.485,25.525,51.527,25.682,51.572,25.781,51.543,25.902,51.389,26.011,51.262,26.153,51.108,26.081,51.003,25.981,50.904,25.724,50.869,25.613,50.803,25.497,50.763,25.445,50.755,25.399,50.777,25.177,50.847,24.889,50.836,24.85,50.804,24.789,50.726,24.869,50.667,24.964,50.558,25.087,50.508,25.307,50.455,25.425,50.281,25.566,50.239,25.623,50.19,25.756,50.13,25.847,50.081,25.961,50.032,26.111,50.054,26.123,50.096,26.119,50.135,26.101,50.155,26.101,50.214,26.308,50.185,26.405,50.111,26.456,50.027,26.527,50.011,26.609,50.008,26.679,50.026,26.699,50.087,26.676,50.135,26.66,50.15,26.663,49.986,26.829,49.717,26.956,49.538,27.152,49.405,27.181,49.282,27.31,49.175,27.438,49.238,27.493,49.158,27.528,49.087,27.549,48.906,27.629,48.797,27.724,48.807,27.765,48.833,27.801,48.809,27.896,48.774,27.959,48.626,28.133,48.523,28.355,48.499,28.449,48.442,28.543,48.39,28.632,48.371,28.692,48.339,28.763,48.253,28.901,48.184,28.979,48.1,29.211,48.086,29.275,48.051,29.355,47.998,29.386,47.935,29.367,47.845,29.366,47.723,29.393,47.725,29.417,47.817,29.487,47.97,29.617,48.048,29.598,48.089,29.579,48.143,29.572,48.136,29.618,48.077,29.716,48.006,29.836,47.974,29.946,47.979,29.983,47.983,30.011,48.073,30.043,48.142,30.041,48.355,29.957,48.454,29.938,48.546,29.962,48.596,29.975,48.671,30.028,48.832,30.035,48.87,30.062,48.919,30.121,48.909,30.241,48.891,30.328,48.917,30.397,49.037,30.45,49.002,30.507,49.13,30.509,49.225,30.472,49.247,30.413,49.19,30.375,49.096,30.407,49.049,30.397,49.002,30.374,49.028,30.333,49.054,30.307,49.43,30.13,49.555,30.029,49.983,30.209,50.072,30.199,50.129,30.048,50.169,29.921,50.23,29.873,50.387,29.679,50.544,29.548,50.65,29.42,50.668,29.34,50.646,29.212,50.675,29.147,50.796,29.117,50.876,29.063,50.876,29.004,50.843,28.928,50.867,28.87,51.021,28.782,51.062,28.726,51.094,28.512,51.128,28.435,51.276,28.219,51.279,28.131,51.519,27.91,51.589,27.864,51.666,27.845,51.842,27.848,52.031,27.824,52.192,27.717,52.476,27.617,52.603,27.493,52.638,27.392,52.692,27.323,52.983,27.142,53.342,27.004,53.455,26.943,53.507,26.852,53.706,26.726,53.823,26.708,54.069,26.732,54.247,26.697,54.522,26.589,54.645,26.509,54.759,26.505,54.896,26.557,55.155,26.725,55.294,26.786,55.424,26.771,55.519,26.83,55.592,26.932,55.65,26.978,55.941,27.038,56.118,27.143,56.284,27.191,56.356,27.2,56.728,27.128,56.813,27.09,56.91,26.995,56.982,26.905,57.036,26.801,57.072,26.68,57.104,26.371,57.201,26.159,57.206,26.037,57.261,25.919,57.335,25.792,57.733,25.725,57.796,25.653,57.937,25.692,58.022,25.641,58.203,25.592,58.314,25.581,58.531,25.592,58.798,25.555,59.046,25.417,59.227,25.428,59.456,25.481,59.616,25.403,59.818,25.401,59.897,25.362,60.025,25.384,60.4,25.312,60.511,25.437,60.588,25.414,60.615,25.33,60.664,25.282,61.109,25.184,61.243,25.142,61.412,25.102,61.49,25.154,61.533,25.196,61.744,25.138,61.908,25.131,62.089,25.155,62.152,25.207,62.199,25.225,62.249,25.197,62.315,25.135,62.391,25.153,62.445,25.197,62.572,25.255,62.665,25.265,63.015,25.225,63.17,25.255,63.286,25.228,63.491,25.211,63.496,25.298,63.557,25.353,63.721,25.386,63.936,25.343,63.987,25.351,64.059,25.403,64.125,25.374,64.152,25.333,64.544,25.237,64.594,25.206,64.659,25.184,64.777,25.307,65.061,25.311,65.406,25.374,65.68,25.355,65.884,25.42,66.235,25.464,66.328,25.466,66.403,25.447,66.468,25.445,66.407,25.485,66.356,25.507,66.131,25.493,66.162,25.554,66.219,25.59,66.324,25.602,66.429,25.575,66.534,25.484,66.57,25.379,66.699,25.226,66.71,25.111,66.682,24.929,66.703,24.861,67.101,24.792,67.171,24.756,67.289,24.368,67.304,24.263,67.309,24.175,67.365,24.092,67.428,24.065,67.454,24.04,67.477,24.018,67.504,23.94,67.563,23.882,67.646,23.92,67.65,23.867,67.668,23.811,67.819,23.828,67.86,23.903,67.951,23.829,68.001,23.826,68.037,23.848,68.068,23.818,68.116,23.753,68.149,23.797,68.165,23.857,68.192,23.729,68.235,23.597,68.343,23.617,68.425,23.706,68.497,23.748,68.642,23.808,68.777,23.852,68.627,23.754,68.454,23.629,68.417,23.571,68.529,23.364,68.641,23.19,68.817,23.054,69.236,22.849,69.665,22.759,69.74,22.775,69.85,22.856,70.118,22.947,70.192,22.966,70.251,22.971,70.339,22.94,70.368,22.973,70.396,23.03,70.435,23.077,70.489,23.09,70.509,23.04,70.513,23.002,70.44,22.97,70.328,22.816,70.177,22.573,70.084,22.554,70.006,22.548,69.819,22.452,69.728,22.465,69.655,22.404,69.549,22.408,69.277,22.285,69.239,22.3,69.194,22.336,69.131,22.416,69.052,22.437,68.983,22.385,68.97,22.29,69.009,22.197,69.192,21.992,69.385,21.84,69.542,21.679,69.748,21.506,70.034,21.179,70.127,21.095,70.485,20.84,70.719,20.74,70.88,20.715,71.025,20.739,71.396,20.87,71.571,20.971,72.015,21.156,72.077,21.224,72.254,21.531,72.257,21.661,72.21,21.728,72.171,21.774,72.103,21.795,72.037,21.823,72.076,21.863,72.094,21.92,72.162,21.985,72.244,22.028,72.274,22.09,72.306,22.189,72.243,22.245,72.183,22.27,72.333,22.27,72.456,22.248,72.59,22.278,72.702,22.264,72.809,22.233,72.709,22.207,72.628,22.2,72.553,22.16,72.522,21.976,72.617,21.962,72.7,21.972,72.644,21.938,72.592,21.878,72.543,21.697,72.84,21.687,72.979,21.705,73.113,21.75,73.022,21.7,72.811,21.62,72.718,21.551,72.613,21.462,72.668,21.456,72.735,21.471,72.687,21.436,72.624,21.372,72.692,21.178,72.752,21.129,72.814,21.117,72.824,21.084,72.841,20.952,72.879,20.829,72.894,20.673,72.881,20.563,72.709,20.078,72.668,19.831,72.676,19.798,72.697,19.757,72.727,19.578,72.799,19.52,72.756,19.451,72.764,19.413,72.788,19.363,72.987,19.277,72.812,19.299,72.795,19.252,72.803,19.219,72.803,19.079,72.835,18.976,72.901,19.015,72.972,19.153,73.006,19.021,72.977,18.927,72.899,18.779,72.871,18.683,72.875,18.643,72.917,18.576,72.943,18.366,72.972,18.259,72.994,18.098,73.047,17.907,73.156,17.622,73.149,17.527,73.239,17.199,73.338,16.46,73.454,16.152,73.476,16.054,73.608,15.871,73.68,15.709,73.733,15.657,73.833,15.659,73.772,15.573,73.814,15.539,73.852,15.482,73.932,15.397,73.801,15.397,73.884,15.306,73.949,15.075,74.041,14.949,74.089,14.902,74.223,14.709,74.28,14.65,74.335,14.575,74.382,14.495,74.397,14.407,74.467,14.217,74.467,14.169,74.499,14.046,74.608,13.85,74.671,13.668,74.682,13.584,74.682,13.507,74.771,13.077,74.803,12.977,74.868,12.845,74.946,12.565,75.197,12.058,75.23,12.023,75.315,11.958,75.423,11.812,75.525,11.703,75.646,11.468,75.724,11.362,75.845,11.058,75.923,10.784,76.096,10.402,76.123,10.327,76.201,10.201,76.193,10.164,76.196,10.086,76.223,10.024,76.249,10.018,76.346,9.922,76.459,9.536,76.42,9.52,76.376,9.54,76.372,9.707,76.343,9.827,76.285,9.91,76.242,9.927,76.292,9.676,76.325,9.452,76.403,9.237,76.419,9.208,76.452,9.189,76.472,9.161,76.483,9.091,76.553,8.903,76.617,8.847,76.967,8.407,77.066,8.316,77.301,8.145,77.518,8.078,77.587,8.13,77.77,8.19,78.06,8.385,78.126,8.511,78.136,8.663,78.192,8.891,78.275,8.99,78.421,9.105,78.98,9.269,79.213,9.256,79.411,9.192,79.356,9.252,79.275,9.285,79.107,9.309,79.02,9.333,78.953,9.394,78.919,9.453,78.94,9.566,78.996,9.683,79.258,10.035,79.254,10.175,79.315,10.257,79.391,10.306,79.532,10.33,79.589,10.312,79.667,10.3,79.757,10.304,79.838,10.323,79.85,10.769,79.849,11.197,79.835,11.269,79.799,11.339,79.693,11.313,79.749,11.371,79.793,11.447,79.754,11.575,79.771,11.69,79.858,11.989,79.982,12.235,80.038,12.296,80.143,12.452,80.229,12.69,80.342,13.361,80.29,13.437,80.114,13.529,80.062,13.606,80.156,13.714,80.233,13.606,80.266,13.521,80.307,13.485,80.246,13.686,80.244,13.773,80.224,13.858,80.144,14.059,80.112,14.212,80.136,14.287,80.17,14.349,80.179,14.478,80.165,14.578,80.099,14.798,80.053,15.074,80.101,15.324,80.293,15.711,80.385,15.793,80.647,15.895,80.708,15.888,80.782,15.867,80.826,15.766,80.865,15.782,80.918,15.76,80.979,15.758,80.993,15.809,81.03,15.881,81.132,15.962,81.239,16.264,81.286,16.337,81.402,16.365,81.712,16.334,81.762,16.329,82.142,16.485,82.259,16.56,82.327,16.664,82.339,16.707,82.36,16.783,82.35,16.825,82.307,16.879,82.282,16.936,82.287,16.978,82.36,17.096,82.593,17.274,82.977,17.462,83.198,17.609,83.388,17.787,83.572,18.004,83.654,18.07,84.104,18.293,84.182,18.401,84.463,18.69,84.609,18.884,84.691,18.965,84.75,19.05,84.771,19.125,85.226,19.508,85.442,19.627,85.437,19.657,85.371,19.679,85.229,19.601,85.181,19.595,85.163,19.621,85.249,19.758,85.46,19.896,85.504,19.888,85.555,19.867,85.56,19.753,85.511,19.727,85.497,19.697,85.575,19.693,85.853,19.792,86.216,19.896,86.279,19.919,86.303,19.945,86.312,19.988,86.245,20.053,86.294,20.054,86.377,20.007,86.446,20.089,86.499,20.172,86.75,20.313,86.769,20.356,86.763,20.419,86.836,20.534,86.925,20.62,86.975,20.7,86.939,20.745,86.896,20.966,86.842,21.106,86.86,21.237,86.954,21.365,87.101,21.501,87.201,21.545,87.678,21.654,87.824,21.727,87.948,21.825,88.051,22.001,88.104,22.047,88.159,22.122,88.083,22.183,88.011,22.213,87.962,22.255,87.941,22.374,87.994,22.266,88.087,22.218,88.196,22.14,88.181,22.033,88.099,21.794,88.057,21.694,88.122,21.636,88.254,21.622,88.279,21.697,88.288,21.758,88.305,21.723,88.446,21.614,88.585,21.66,88.6,21.714,88.567,21.832,88.642,22.122,88.66,22.067,88.708,22.056,88.73,22.036,88.74,22.005,88.691,21.733,88.695,21.662,88.713,21.622,88.745,21.584,88.834,21.661,88.858,21.745,88.907,21.653,88.967,21.641,89.052,21.654,89.042,21.759,89.02,21.834,88.949,21.938,89.028,21.937,89.051,22.093,89.082,22.015,89.094,21.873,89.167,21.784,89.234,21.722,89.279,21.707,89.354,21.721,89.452,21.821,89.501,21.914,89.503,22.032,89.469,22.213,89.483,22.276,89.547,21.984,89.567,21.861,89.569,21.767,89.628,21.814,89.668,21.878,89.757,21.919,89.812,21.983,89.853,22.091,89.866,22.173,89.853,22.289,89.882,22.388,89.985,22.466,89.894,22.308,89.894,22.203,89.918,22.116,89.954,22.023,90.069,22.098,90.143,22.138,90.21,22.157,90.088,22.017,90.07,21.96,90.071,21.887,90.131,21.847,90.159,21.817,90.231,21.83,90.288,21.899,90.356,22.048,90.494,22.179,90.553,22.218,90.589,22.258,90.616,22.362,90.596,22.436,90.532,22.539,90.487,22.589,90.498,22.635,90.481,22.685,90.435,22.752,90.437,22.828,90.462,22.882,90.552,22.905,90.478,22.987,90.466,23.054,90.528,23.085,90.595,23.134,90.599,23.204,90.591,23.266,90.523,23.346,90.392,23.367,90.269,23.456,90.408,23.432,90.556,23.422,90.568,23.474,90.562,23.537,90.573,23.578,90.604,23.591,90.616,23.532,90.616,23.442,90.656,23.273,90.634,23.094,90.656,23.025,90.827,22.721,90.946,22.597,91.151,22.614,91.216,22.642,91.314,22.735,91.41,22.797,91.48,22.885,91.482,22.797,91.53,22.708,91.693,22.505,91.734,22.407,91.797,22.297,91.845,22.343,91.863,22.35,91.858,22.317,91.825,22.229,91.85,22.157,91.913,21.883,92.008,21.685,92.011,21.516,92.056,21.175,92.195,20.984,92.248,20.884,92.308,20.79,92.324,20.792,92.378,20.718,92.608,20.47,92.723,20.296,92.733,20.453,92.709,20.564,92.736,20.563,92.787,20.469,92.851,20.415,92.891,20.34,92.872,20.302,92.844,20.283,92.791,20.211,92.828,20.178,92.882,20.152,92.991,20.288,93.019,20.346,93.035,20.406,93.067,20.378,93.015,20.185,93.068,20.189,93.096,20.181,93.04,20.13,93.002,20.075,93.129,19.858,93.191,19.851,93.199,19.898,93.157,20.041,93.25,20.07,93.362,20.058,93.41,20.038,93.439,20.009,93.582,19.91,93.707,19.912,93.66,19.854,93.612,19.776,93.669,19.732,93.74,19.697,93.761,19.648,93.77,19.61,93.84,19.534,93.888,19.504,93.961,19.482,93.998,19.441,93.962,19.329,93.886,19.272,93.825,19.238,93.728,19.267,93.579,19.401,93.531,19.398,93.493,19.369,93.598,19.188,93.705,19.027,93.8,18.961,93.929,18.9,93.961,18.958,93.968,18.995,93.941,19.146,94.002,19.182,94.022,19.269,94.045,19.287,94.039,19.146,94.07,18.893,94.091,18.849,94.246,18.741,94.171,18.732,94.252,18.609,94.266,18.507,94.431,18.202,94.494,17.825,94.56,17.699,94.589,17.569,94.564,17.309,94.494,17.167,94.473,17.135,94.452,16.954,94.4,16.868,94.353,16.64,94.327,16.572,94.271,16.517,94.214,16.127,94.224,16.016,94.299,16.008,94.442,16.094,94.496,16.186,94.588,16.289,94.638,16.309,94.665,16.336,94.679,16.426,94.703,16.512,94.717,16.452,94.72,16.399,94.677,16.242,94.681,16.133,94.651,16.065,94.656,15.989,94.662,15.904,94.798,15.971,94.848,16.033,94.86,16.102,94.893,16.183,94.898,16.141,94.882,16.088,94.892,16.038,94.891,15.979,94.943,15.818,95.078,15.839,95.177,15.826,95.226,15.877,95.333,16.033,95.347,16.098,95.365,15.985,95.308,15.88,95.301,15.756,95.348,15.729,95.39,15.723,95.556,15.838,95.679,15.977,95.711,16.073,95.763,16.169,96.012,16.254,96.032,16.285,96.043,16.34,96.081,16.353,96.135,16.343,96.293,16.41,96.324,16.444,96.237,16.567,96.238,16.631,96.189,16.768,96.22,16.781,96.249,16.765,96.262,16.659,96.282,16.596,96.364,16.521,96.431,16.505,96.507,16.514,96.622,16.564,96.765,16.71,96.811,16.778,96.858,16.921,96.909,17.031,96.851,17.203,96.91,17.305,96.878,17.342,96.851,17.401,96.97,17.317,97.075,17.207,97.1,17.165,97.2,17.095,97.178,17.062,97.212,16.893,97.267,16.743,97.331,16.672,97.376,16.523,97.505,16.525,97.62,16.537,97.668,16.552,97.726,16.569,97.665,16.52,97.634,16.458,97.641,16.254,97.609,16.144,97.584,16.02,97.71,15.876,97.774,15.431,97.744,15.307,97.8,15.185,97.812,14.859,97.869,14.739,97.937,14.764,98.019,14.653,97.929,14.696,97.91,14.653,97.977,14.461,97.998,14.335,98.1,14.162,98.073,13.986,98.098,13.898,98.111,13.713,98.15,13.648,98.2,13.98,98.239,13.934,98.248,13.84,98.245,13.733,98.421,13.484,98.487,13.293,98.576,13.162,98.595,12.986,98.637,12.848,98.636,12.771,98.663,12.662,98.665,12.54,98.624,12.441,98.679,12.348,98.619,12.3,98.6,12.245,98.631,12.225,98.696,12.225,98.664,12.127,98.686,12.047,98.689,11.957,98.645,11.91,98.639,11.869,98.625,11.801,98.636,11.738,98.694,11.718,98.805,11.779,98.84,11.739,98.876,11.72,98.791,11.665,98.741,11.592,98.746,11.521,98.733,11.435,98.73,11.33,98.745,11.24,98.683,11.133,98.676,10.987,98.599,10.864,98.536,10.741,98.501,10.719,98.465,10.676,98.523,10.353,98.497,10.183,98.521,10.107,98.563,10.035,98.658,10.179,98.703,10.19,98.562,9.838,98.493,9.561,98.443,9.493,98.371,9.291,98.326,8.969,98.242,8.768,98.227,8.544,98.238,8.423,98.305,8.226,98.361,8.187,98.421,8.178,98.474,8.247,98.5,8.318,98.579,8.344,98.636,8.305,98.704,8.257,98.789,8.06,98.872,8.024,98.974,7.963,99.051,7.888,99.043,7.766,99.078,7.718,99.183,7.718,99.264,7.619,99.3,7.561,99.359,7.372,99.435,7.334,99.529,7.329,99.597,7.356,99.553,7.219,99.602,7.155,99.668,7.151,99.72,7.106,99.696,6.877,99.869,6.75,100.119,6.442,100.158,6.324,100.263,6.183,100.343,5.984,100.374,5.778,100.353,5.588,100.473,5.044,100.615,4.652,100.615,4.373,100.661,4.226,100.76,4.097,100.796,4.023,100.757,4.002,100.715,3.966,100.782,3.864,100.851,3.777,101.025,3.625,101.115,3.472,101.3,3.253,101.33,3.142,101.354,3.011,101.296,2.885,101.351,2.839,101.407,2.813,101.52,2.684,101.781,2.574,101.89,2.449,102.146,2.248,102.548,2.042,102.727,1.856,102.897,1.792,103.357,1.546,103.4,1.498,103.427,1.43,103.48,1.329,103.55,1.333,103.695,1.45,103.817,1.477,103.915,1.447,103.992,1.455,103.991,1.55,103.981,1.624,104.016,1.579,104.076,1.53,104.101,1.488,104.094,1.446,104.115,1.412,104.176,1.365,104.25,1.389,104.28,1.416,104.288,1.481,104.219,1.723,103.968,2.261,103.832,2.508,103.812,2.58,103.537,2.775,103.485,2.837,103.439,2.933,103.445,3.261,103.429,3.379,103.454,3.521,103.373,3.671,103.362,3.769,103.421,3.977,103.469,4.393,103.454,4.669,103.416,4.85,103.197,5.262,103.097,5.408,102.982,5.525,102.899,5.564,102.79,5.645,102.534,5.863,102.34,6.172,102.274,6.203,102.101,6.242,101.799,6.475,101.614,6.754,101.498,6.865,101.401,6.9,101.302,6.908,101.154,6.875,101.018,6.861,100.793,6.995,100.702,7.082,100.586,7.176,100.49,7.161,100.424,7.188,100.371,7.28,100.205,7.501,100.161,7.599,100.158,7.728,100.257,7.775,100.317,7.716,100.324,7.644,100.28,7.584,100.284,7.552,100.343,7.553,100.38,7.542,100.411,7.464,100.439,7.281,100.545,7.227,100.504,7.337,100.454,7.442,100.279,8.269,100.229,8.425,100.163,8.508,100.159,8.474,100.154,8.443,100.129,8.428,100.056,8.511,99.99,8.589,99.961,8.671,99.905,9.113,99.878,9.195,99.836,9.288,99.724,9.314,99.394,9.214,99.335,9.225,99.254,9.265,99.265,9.353,99.288,9.415,99.191,9.627,99.161,9.734,99.169,9.934,99.195,10.175,99.19,10.266,99.165,10.32,99.237,10.388,99.285,10.569,99.487,10.89,99.514,11.101,99.561,11.215,99.627,11.463,99.725,11.662,99.799,11.749,99.837,11.937,99.93,12.047,99.989,12.171,100.006,12.355,99.964,12.69,99.982,12.771,100.09,13.046,100.051,13.171,99.991,13.243,100.017,13.353,100.122,13.44,100.236,13.484,100.536,13.514,100.603,13.568,100.656,13.521,100.907,13.462,100.963,13.432,100.947,13.358,100.926,13.303,100.946,13.187,100.904,13.035,100.896,12.818,100.863,12.715,100.898,12.654,100.954,12.621,101.09,12.674,101.445,12.619,101.724,12.689,101.836,12.64,101.889,12.593,101.945,12.564,102.034,12.532,102.134,12.443,102.23,12.332,102.248,12.361,102.259,12.394,102.343,12.253,102.434,12.179,102.54,12.109,102.575,12.158,102.594,12.203,102.655,12.149,102.763,12.012,102.792,11.889,102.884,11.773,102.912,11.704,102.934,11.707,102.932,11.742,102.949,11.773,103.004,11.711,103.011,11.589,103.125,11.461,103.107,11.368,103.091,11.211,103.106,11.074,103.153,10.914,103.272,10.909,103.354,10.922,103.411,10.977,103.467,11.084,103.532,11.147,103.595,11.108,103.654,11.059,103.722,10.89,103.681,10.759,103.592,10.721,103.54,10.669,103.532,10.605,103.587,10.552,103.662,10.509,103.841,10.581,103.871,10.655,103.902,10.644,103.937,10.587,104.262,10.541,104.426,10.411,104.516,10.34,104.594,10.267,104.613,10.208,104.663,10.17,104.748,10.199,104.802,10.203,104.873,10.115,104.966,10.101,105.028,10.067,105.084,9.996,105.095,9.945,105.093,9.901,104.987,9.869,104.903,9.816,104.845,9.606,104.815,9.185,104.819,8.802,104.896,8.747,104.77,8.598,104.892,8.583,105.114,8.629,105.191,8.711,105.322,8.801,105.401,8.962,105.501,9.093,106.168,9.397,106.193,9.448,106.206,9.502,106.159,9.594,106.113,9.674,105.831,10.001,105.926,9.962,106.204,9.675,106.378,9.556,106.484,9.559,106.539,9.604,106.572,9.641,106.564,9.716,106.507,9.821,106.184,10.142,106.136,10.222,106.449,9.94,106.557,9.868,106.596,9.86,106.657,9.901,106.658,9.949,106.659,9.991,106.714,10.06,106.785,10.116,106.785,10.151,106.729,10.193,106.602,10.232,106.464,10.298,106.492,10.304,106.644,10.289,106.757,10.296,106.776,10.339,106.778,10.376,106.741,10.444,106.698,10.462,106.643,10.456,106.606,10.465,106.727,10.536,106.813,10.433,106.902,10.383,106.947,10.4,106.966,10.441,106.984,10.618,107.007,10.661,107.021,10.631,107.036,10.556,107.088,10.498,107.194,10.472,107.235,10.42,107.262,10.398,107.384,10.459,107.47,10.486,107.564,10.555,107.845,10.7,108.001,10.72,108.095,10.897,108.176,10.92,108.272,10.934,108.419,11.041,108.551,11.156,108.7,11.199,108.821,11.315,108.987,11.336,109.018,11.468,109.04,11.593,109.133,11.601,109.173,11.665,109.199,11.725,109.193,11.773,109.158,11.837,109.167,11.912,109.2,11.972,109.199,11.999,109.215,12.01,109.22,11.959,109.247,11.909,109.259,11.955,109.256,11.993,109.216,12.073,109.207,12.415,109.305,12.391,109.219,12.646,109.274,12.709,109.336,12.752,109.381,12.671,109.445,12.6,109.42,12.719,109.424,12.956,109.377,13.025,109.31,13.219,109.272,13.279,109.288,13.451,109.252,13.591,109.247,13.855,109.288,13.765,109.303,13.856,109.245,14.053,109.224,14.097,109.207,14.154,109.191,14.27,109.137,14.384,109.087,14.553,109.085,14.716,109.022,14.803,108.94,15.001,108.898,15.181,108.821,15.378,108.743,15.427,108.674,15.484,108.578,15.585,108.447,15.763,108.395,15.872,108.286,15.989,108.274,16.029,108.267,16.09,108.241,16.101,108.209,16.091,108.17,16.164,108.088,16.243,108.029,16.331,107.991,16.337,107.936,16.329,107.882,16.31,107.834,16.322,107.803,16.403,107.724,16.488,107.593,16.568,107.541,16.609,107.549,16.643,107.355,16.794,107.18,16.898,107.12,17.056,106.926,17.221,106.736,17.367,106.517,17.663,106.371,17.747,106.356,17.765,106.479,17.72,106.459,17.874,106.499,17.946,106.412,18.053,106.24,18.221,106.145,18.259,106.066,18.316,105.888,18.502,105.839,18.574,105.808,18.646,105.744,18.746,105.732,18.779,105.622,18.966,105.639,19.057,105.716,19.128,105.791,19.294,105.785,19.379,105.812,19.467,105.814,19.587,105.984,19.939,106.062,19.987,106.166,19.992,106.396,20.206,106.518,20.289,106.573,20.392,106.551,20.527,106.753,20.735,106.737,20.806,106.675,20.96,106.683,21,106.725,21,106.76,20.991,106.821,20.958,106.886,20.95,106.936,20.974,106.981,20.971,107.019,20.991,107.075,20.999,107.112,20.96,107.165,20.949,107.354,21.055,107.373,21.128,107.376,21.194,107.41,21.285,107.527,21.336,107.637,21.368,107.707,21.406,107.809,21.497,107.926,21.499,107.973,21.508,108.067,21.526,108.146,21.565,108.246,21.558,108.302,21.622,108.325,21.694,108.355,21.697,108.383,21.679,108.444,21.607,108.502,21.633,108.526,21.671,108.493,21.739,108.481,21.829,108.48,21.905,108.594,21.901,108.616,21.869,108.589,21.816,108.616,21.77,108.675,21.725,108.744,21.651,108.772,21.63,108.846,21.634,108.922,21.624,109.031,21.627,109.102,21.59,109.133,21.544,109.098,21.487,109.082,21.44,109.149,21.426,109.22,21.443,109.347,21.454,109.436,21.479,109.544,21.538,109.521,21.693,109.566,21.691,109.594,21.672,109.687,21.525,109.743,21.528,109.759,21.56,109.83,21.484,109.931,21.481,109.921,21.376,109.78,21.337,109.76,21.228,109.681,21.132,109.704,21.053,109.663,20.917,109.685,20.874,109.726,20.839,109.767,20.781,109.805,20.711,109.792,20.622,109.861,20.514,109.946,20.474,109.968,20.448,109.984,20.403,109.932,20.399,109.886,20.413,109.883,20.364,109.938,20.295,110.123,20.264,110.345,20.295,110.45,20.355,110.487,20.427,110.518,20.46,110.512,20.518,110.313,20.672,110.326,20.72,110.371,20.752,110.388,20.791,110.365,20.838,110.18,20.859,110.154,20.945,110.194,21.038,110.331,21.131,110.375,21.172,110.397,21.248,110.411,21.338,110.435,21.327,110.458,21.231,110.504,21.207,110.567,21.214,110.652,21.279,110.771,21.387,110.878,21.396,110.997,21.43,111.017,21.512,111.061,21.511,111.101,21.485,111.144,21.482,111.221,21.494,111.319,21.486,111.392,21.535,111.603,21.559,111.682,21.608,111.712,21.655,111.776,21.719,111.825,21.71,111.873,21.717,111.926,21.776,111.944,21.85,112.025,21.843,112.117,21.806,112.193,21.763,112.305,21.742,112.356,21.768,112.39,21.801,112.377,21.917,112.36,21.978,112.396,21.981,112.429,21.958,112.439,21.927,112.421,21.881,112.495,21.818,112.586,21.777,112.634,21.82,112.661,21.859,112.725,21.902,112.809,21.945,112.904,21.881,112.954,21.907,112.984,21.938,113.008,22.119,113.089,22.208,113.149,22.075,113.266,22.089,113.328,22.145,113.367,22.165,113.416,22.178,113.499,22.202,113.548,22.223,113.576,22.297,113.589,22.35,113.551,22.404,113.553,22.594,113.485,22.692,113.45,22.726,113.432,22.789,113.345,22.865,113.338,22.889,113.331,22.912,113.442,22.941,113.46,22.996,113.445,23.055,113.52,23.102,113.621,23.127,113.592,23.077,113.586,23.02,113.603,22.969,113.62,22.861,113.661,22.802,113.754,22.734,113.828,22.607,113.931,22.531,114.015,22.512,114.007,22.484,113.896,22.428,113.903,22.396,113.937,22.365,114.033,22.376,114.139,22.348,114.268,22.296,114.288,22.325,114.291,22.374,114.335,22.396,114.325,22.437,114.285,22.458,114.291,22.499,114.266,22.541,114.341,22.593,114.42,22.583,114.496,22.527,114.554,22.529,114.544,22.621,114.572,22.654,114.593,22.698,114.652,22.755,114.711,22.739,114.75,22.626,114.854,22.617,114.896,22.64,114.914,22.685,115.012,22.709,115.092,22.782,115.196,22.817,115.29,22.776,115.383,22.719,115.498,22.719,115.535,22.765,115.561,22.825,115.64,22.853,115.756,22.824,115.852,22.802,116.063,22.879,116.157,22.887,116.206,22.919,116.222,22.95,116.252,22.981,116.346,22.941,116.471,22.946,116.52,23.007,116.538,23.18,116.586,23.218,116.669,23.228,116.699,23.278,116.682,23.327,116.629,23.354,116.712,23.36,116.76,23.383,116.861,23.453,116.911,23.647,117.033,23.623,117.083,23.579,117.148,23.599,117.225,23.647,117.291,23.714,117.331,23.709,117.347,23.636,117.368,23.589,117.417,23.621,117.462,23.736,117.46,23.771,117.433,23.792,117.466,23.841,117.579,23.857,117.628,23.837,117.668,23.939,117.742,24.015,117.839,24.012,117.904,24.106,118.056,24.246,118.051,24.327,118.024,24.38,117.879,24.396,117.848,24.432,117.843,24.474,117.897,24.48,117.935,24.474,118.006,24.482,118.014,24.56,118.087,24.627,118.195,24.626,118.295,24.573,118.412,24.601,118.56,24.58,118.657,24.621,118.719,24.746,118.692,24.782,118.64,24.809,118.637,24.836,118.708,24.85,118.822,24.911,118.909,24.929,118.956,25.005,118.914,25.127,118.978,25.209,119.025,25.223,119.236,25.206,119.286,25.232,119.244,25.307,119.169,25.356,119.146,25.414,119.18,25.45,119.263,25.468,119.344,25.446,119.422,25.46,119.499,25.409,119.593,25.368,119.622,25.391,119.619,25.437,119.539,25.591,119.553,25.699,119.617,25.823,119.648,25.919,119.619,26.004,119.501,26.009,119.418,25.954,119.332,25.949,119.264,25.975,119.139,26.122,119.232,26.104,119.313,26.063,119.37,26.054,119.463,26.055,119.567,26.127,119.693,26.236,119.797,26.3,119.881,26.334,119.876,26.371,119.84,26.414,119.831,26.45,119.785,26.547,119.726,26.609,119.638,26.621,119.624,26.676,119.59,26.73,119.588,26.785,119.652,26.747,119.71,26.729,119.767,26.775,119.789,26.831,119.824,26.846,119.815,26.798,119.821,26.737,119.842,26.689,119.879,26.683,119.882,26.61,119.968,26.586,120.043,26.634,120.087,26.672,120.097,26.781,120.139,26.886,120.279,27.097,120.385,27.156,120.469,27.256,120.54,27.318,120.608,27.412,120.629,27.482,120.588,27.581,120.665,27.639,120.661,27.688,120.685,27.745,120.833,27.891,120.833,27.938,120.763,27.977,120.748,28.01,120.813,28.013,120.892,28.004,120.959,28.037,121.035,28.157,121.098,28.291,121.146,28.327,121.217,28.346,121.272,28.222,121.355,28.23,121.51,28.324,121.61,28.292,121.602,28.367,121.538,28.521,121.475,28.641,121.519,28.714,121.59,28.735,121.63,28.768,121.663,28.851,121.54,28.932,121.641,28.916,121.68,28.953,121.665,29.011,121.521,29.118,121.448,29.131,121.487,29.193,121.534,29.237,121.656,29.236,121.717,29.256,121.791,29.226,121.854,29.129,121.918,29.135,121.968,29.491,121.941,29.606,121.888,29.628,121.822,29.605,121.69,29.511,121.506,29.485,121.575,29.537,121.677,29.584,121.906,29.78,122.083,29.87,122.017,29.888,121.944,29.894,121.812,29.952,121.678,29.979,121.433,30.227,121.341,30.282,121.258,30.304,121.159,30.302,120.904,30.161,120.633,30.133,120.495,30.303,120.353,30.247,120.261,30.263,120.229,30.25,120.195,30.241,120.246,30.284,120.45,30.388,120.63,30.391,120.821,30.355,120.897,30.393,120.938,30.47,120.998,30.558,121.31,30.7,121.419,30.79,121.528,30.841,121.675,30.864,121.769,30.87,121.878,30.917,121.834,31.062,121.786,31.163,121.661,31.32,121.351,31.485,121.205,31.628,121.055,31.719,120.938,31.75,120.788,31.82,120.752,31.923,120.716,31.984,120.497,32.02,120.347,31.952,120.192,31.906,120.036,31.936,120.074,31.96,120.099,31.976,120.184,31.966,120.52,32.106,120.661,32.081,120.792,32.032,120.974,31.869,121.146,31.842,121.266,31.863,121.352,31.859,121.681,31.712,121.764,31.7,121.866,31.704,121.856,31.816,121.832,31.9,121.751,31.993,121.674,32.051,121.491,32.121,121.451,32.153,121.404,32.206,121.401,32.372,121.342,32.425,121.293,32.457,120.99,32.567,120.853,32.661,120.853,32.764,120.897,32.843,120.871,33.017,120.734,33.237,120.616,33.491,120.505,33.638,120.5,33.716,120.426,33.866,120.323,34.169,120.267,34.274,120.201,34.326,119.964,34.448,119.77,34.496,119.583,34.582,119.427,34.714,119.351,34.749,119.201,34.748,119.165,34.849,119.216,35.012,119.353,35.114,119.43,35.301,119.526,35.359,119.608,35.47,119.72,35.589,119.811,35.618,119.866,35.644,119.912,35.693,119.979,35.74,120.027,35.799,120.055,35.861,120.219,35.935,120.285,35.984,120.265,36.007,120.181,36.017,120.094,36.119,120.117,36.15,120.183,36.202,120.27,36.226,120.328,36.228,120.343,36.189,120.33,36.11,120.348,36.079,120.393,36.054,120.519,36.109,120.638,36.13,120.681,36.168,120.682,36.341,120.712,36.413,120.776,36.456,120.847,36.426,120.896,36.444,120.905,36.485,120.883,36.539,120.797,36.607,120.811,36.633,120.879,36.635,120.99,36.598,121.054,36.611,121.144,36.66,121.413,36.738,121.67,36.836,121.933,36.959,122.05,36.971,122.162,36.959,122.203,36.927,122.22,36.88,122.242,36.85,122.274,36.834,122.341,36.832,122.457,36.915,122.52,36.947,122.523,37.003,122.487,37.022,122.447,37.068,122.516,37.138,122.587,37.181,122.573,37.318,122.667,37.403,122.602,37.426,122.493,37.408,122.338,37.405,122.169,37.456,122.11,37.522,122.057,37.529,122.01,37.496,121.965,37.445,121.816,37.457,121.64,37.46,121.505,37.515,121.388,37.579,121.22,37.6,121.049,37.725,120.75,37.834,120.37,37.701,120.285,37.692,120.257,37.679,120.287,37.656,120.312,37.623,120.156,37.495,119.883,37.351,119.88,37.296,119.888,37.253,119.761,37.155,119.45,37.125,119.287,37.138,119.112,37.201,118.998,37.277,118.953,37.331,118.955,37.494,118.991,37.641,119.033,37.661,119.089,37.701,119.07,37.749,119.038,37.777,119.036,37.809,119.028,37.904,118.94,38.043,118.8,38.127,118.667,38.126,118.543,38.095,118.015,38.183,117.767,38.312,117.656,38.424,117.558,38.625,117.554,38.691,117.617,38.853,117.785,39.134,117.866,39.191,118.041,39.227,118.148,39.195,118.298,39.067,118.472,39.118,118.626,39.177,118.752,39.16,118.826,39.172,118.912,39.166,118.977,39.183,119.04,39.222,119.225,39.408,119.261,39.561,119.322,39.662,119.391,39.752,119.591,39.903,119.85,39.987,120.369,40.204,120.479,40.231,120.771,40.589,120.841,40.649,120.922,40.683,121.003,40.749,121.086,40.842,121.175,40.901,121.537,40.878,121.599,40.843,121.729,40.846,121.766,40.876,121.809,40.969,121.835,40.974,121.859,40.842,122.14,40.688,122.179,40.603,122.275,40.542,122.264,40.5,122.203,40.396,122.191,40.358,121.983,40.136,121.869,40.046,121.801,39.951,121.517,39.845,121.474,39.755,121.514,39.685,121.518,39.639,121.47,39.64,121.406,39.621,121.267,39.545,121.286,39.519,121.3,39.452,121.275,39.385,121.356,39.377,121.513,39.375,121.785,39.401,121.818,39.387,121.758,39.348,121.665,39.269,121.628,39.22,121.68,39.109,121.263,38.96,121.188,38.947,121.107,38.921,121.122,38.813,121.164,38.732,121.207,38.744,121.236,38.767,121.32,38.808,121.517,38.831,121.65,38.865,121.67,38.892,121.633,38.955,121.677,39.003,121.745,39.01,121.805,38.991,121.864,38.996,121.923,39.037,121.982,39.053,122.048,39.094,122.121,39.152,122.225,39.267,122.335,39.366,122.84,39.601,122.961,39.62,123.032,39.674,123.227,39.687,123.269,39.727,123.348,39.763,123.49,39.768,123.581,39.786,123.611,39.841,123.651,39.882,123.76,39.822,124.106,39.841,124.267,39.924,124.35,40.012,124.362,40.004,124.375,39.996,124.349,39.907,124.404,39.866,124.557,39.791,124.608,39.717,124.638,39.615,124.699,39.632,124.732,39.652,124.739,39.742,124.775,39.758,124.868,39.702,125.1,39.59,125.18,39.583,125.361,39.527,125.374,39.428,125.413,39.326,125.41,39.288,125.157,38.872,125.169,38.806,125.299,38.743,125.424,38.747,125.489,38.728,125.554,38.686,125.492,38.676,125.415,38.68,125.31,38.665,125.067,38.557,124.974,38.48,124.881,38.342,124.883,38.295,124.875,38.233,124.691,38.129,124.779,38.102,124.907,38.113,124.995,38.078,125.163,38.094,125.207,38.082,125.247,38.057,125.193,38.038,124.989,37.931,125.027,37.923,125.102,37.882,125.311,37.844,125.365,37.748,125.358,37.725,125.407,37.719,125.449,37.73,125.582,37.815,125.676,37.918,125.695,37.963,125.769,37.985,125.942,37.874,126.05,37.87,126.117,37.743,126.161,37.764,126.203,37.829,126.37,37.878,126.573,37.797,126.623,37.79,126.634,37.782,126.621,37.755,126.578,37.745,126.563,37.717,126.58,37.654,126.608,37.617,126.657,37.551,126.65,37.447,126.696,37.411,126.791,37.295,126.746,37.194,126.776,37.158,126.787,37.103,126.869,36.976,126.96,36.958,126.977,36.939,126.958,36.906,126.879,36.862,126.872,36.824,126.839,36.846,126.784,36.948,126.687,36.96,126.578,37.02,126.487,37.007,126.429,36.969,126.352,36.958,126.217,36.871,126.161,36.772,126.181,36.692,126.231,36.689,126.389,36.651,126.433,36.678,126.488,36.694,126.507,36.586,126.548,36.478,126.552,36.43,126.544,36.341,126.557,36.236,126.54,36.166,126.597,36.105,126.682,36.038,126.693,36.014,126.664,35.975,126.647,35.922,126.72,35.898,126.753,35.872,126.717,35.769,126.602,35.714,126.542,35.669,126.488,35.647,126.487,35.606,126.565,35.59,126.614,35.571,126.582,35.534,126.493,35.501,126.46,35.456,126.396,35.314,126.361,35.217,126.291,35.154,126.327,35.045,126.398,34.933,126.421,34.823,126.479,34.81,126.548,34.837,126.593,34.824,126.539,34.779,126.473,34.756,126.505,34.738,126.525,34.698,126.426,34.695,126.301,34.72,126.264,34.673,126.333,34.59,126.482,34.494,126.506,34.428,126.508,34.351,126.531,34.314,126.584,34.318,126.611,34.404,126.755,34.512,126.796,34.494,126.826,34.451,126.897,34.439,127.031,34.607,127.247,34.755,127.269,34.72,127.261,34.662,127.195,34.605,127.173,34.546,127.325,34.463,127.381,34.501,127.401,34.553,127.479,34.625,127.423,34.688,127.39,34.743,127.404,34.823,127.477,34.844,127.524,34.84,127.565,34.766,127.632,34.69,127.715,34.721,127.742,34.783,127.663,34.843,127.639,34.89,127.659,34.926,127.715,34.955,127.873,34.966,127.977,35.019,128.036,35.022,128.095,34.934,128.152,34.916,128.276,34.911,128.388,34.875,128.444,34.87,128.448,34.932,128.419,35.016,128.458,35.069,128.511,35.101,128.643,35.12,128.796,35.094,128.98,35.102,129.077,35.123,129.214,35.182,129.329,35.333,129.419,35.498,129.485,35.687,129.562,35.948,129.573,36.051,129.51,36.038,129.458,36.006,129.426,36.019,129.404,36.052,129.402,36.138,129.391,36.202,129.393,36.323,129.427,36.385,129.445,36.471,129.433,36.637,129.473,36.742,129.426,36.926,129.418,37.059,129.335,37.275,129.052,37.678,128.852,37.887,128.619,38.176,128.375,38.623,128.329,38.681,128.249,38.745,128.163,38.786,128.123,38.816,127.972,38.898,127.786,39.084,127.699,39.125,127.581,39.143,127.497,39.179,127.395,39.208,127.383,39.296,127.422,39.374,127.457,39.401,127.523,39.377,127.549,39.461,127.547,39.563,127.527,39.696,127.568,39.782,127.867,39.896,127.967,39.996,128.106,40.033,128.304,40.036,128.393,40.089,128.511,40.13,128.611,40.198,128.701,40.318,128.843,40.358,128.945,40.428,129.11,40.491,129.245,40.661,129.341,40.726,129.709,40.857,129.742,40.932,129.712,41.124,129.766,41.304,129.759,41.392,129.682,41.494,129.686,41.595,129.756,41.712,129.876,41.806,129.928,41.897,130.007,41.991,130.068,42.046,130.18,42.097,130.236,42.183,130.315,42.214,130.458,42.302,130.569,42.292,130.637,42.275,130.687,42.303,130.73,42.326,130.834,42.523,130.709,42.656,130.756,42.673,130.946,42.634,131.025,42.645,131.158,42.626,131.245,42.697,131.292,42.772,131.393,42.822,131.516,42.996,131.722,43.203,131.795,43.255,131.939,43.302,131.976,43.296,132.013,43.28,131.898,43.171,131.867,43.095,131.947,43.095,132.029,43.119,132.233,43.245,132.31,43.314,132.334,43.239,132.304,42.883,132.481,42.91,132.576,42.872,132.709,42.876,132.864,42.794,132.924,42.805,132.997,42.808,133.059,42.723,133.16,42.697,133.329,42.764,133.587,42.828,133.709,42.83,134.01,42.947,134.156,43.042,134.692,43.291,134.917,43.427,135.131,43.526,135.26,43.685,135.483,43.835,135.489,43.899,135.533,43.971,135.875,44.374,135.987,44.44,136.142,44.489,136.209,44.562,136.251,44.667,136.46,44.822,136.604,44.978,136.737,45.08,136.804,45.171,137.147,45.394,137.425,45.64,137.685,45.818,137.769,45.929,138.106,46.251,138.21,46.463,138.337,46.543,138.392,46.745,138.5,46.89,138.53,46.976,138.587,47.057,139.001,47.383,139.167,47.635,139.373,47.887,139.521,47.975,139.676,48.09,139.761,48.181,139.998,48.324,140.113,48.423,140.171,48.524,140.224,48.773,140.378,48.964,140.334,48.995,140.309,49.054,140.326,49.12,140.349,49.159,140.364,49.221,140.399,49.29,140.431,49.331,140.517,49.596,140.511,49.762,140.465,49.826,140.463,49.911,140.585,50.033,140.613,50.054,140.625,50.082,140.564,50.107,140.535,50.131,140.476,50.546,140.521,50.8,140.646,50.987,140.671,51.051,140.688,51.232,140.839,51.414,140.933,51.62,141.129,51.728,141.258,51.861,141.367,51.921,141.386,52.057,141.485,52.179,141.409,52.234,141.33,52.271,141.17,52.368,141.132,52.436,141.245,52.55,141.266,52.653,141.256,52.84,141.087,52.898,140.875,53.04,140.84,53.088,140.887,53.092,141.181,53.015,141.328,53.097,141.402,53.184,141.374,53.293,141.218,53.334,141.015,53.454,141.006,53.495,140.688,53.596,140.347,53.813,140.242,54.001,140.179,54.052,139.858,54.205,139.796,54.256,139.707,54.277,139.32,54.193,139.105,54.218,138.696,54.32,138.657,54.298,138.716,54.223,138.705,54.148,138.722,54.044,138.699,53.87,138.661,53.745,138.511,53.57,138.451,53.537,138.32,53.523,138.25,53.524,138.292,53.592,138.407,53.674,138.569,53.819,138.568,53.947,138.528,53.96,138.494,53.96,138.379,53.909,138.253,53.726,137.95,53.604,137.738,53.56,137.328,53.539,137.254,53.546,137.221,53.579,137.314,53.632,137.517,53.707,137.645,53.866,137.786,53.903,137.835,53.947,137.623,53.97,137.476,54.028,137.339,54.101,137.403,54.124,137.451,54.13,137.513,54.156,137.666,54.283,137.525,54.291,137.378,54.282,137.142,54.182,137.096,54.129,137.172,54.057,137.258,54.025,137.155,53.822,137.019,53.848,136.886,53.839,136.803,53.782,136.719,53.804,136.683,53.931,136.729,54.061,136.77,54.353,136.82,54.452,136.824,54.561,136.797,54.621,136.715,54.624,136.58,54.614,136.238,54.614,135.852,54.584,135.438,54.692,135.325,54.707,135.258,54.731,135.212,54.841,135.235,54.903,135.263,54.943,135.541,55.114,135.751,55.161,136.175,55.352,136.351,55.51,136.46,55.577,136.794,55.694,137.012,55.795,137.19,55.892,137.384,55.975,137.573,56.112,137.692,56.139,138.074,56.433,138.141,56.499,138.18,56.589,138.218,56.629,138.662,56.966,138.966,57.088,139.182,57.262,139.444,57.33,139.507,57.358,139.619,57.456,139.803,57.514,139.862,57.549,140.002,57.688,140.447,57.814,140.495,57.865,140.685,58.212,140.79,58.303,140.988,58.417,141.347,58.528,141.603,58.649,141.755,58.745,142.025,59,142.33,59.153,142.58,59.24,143.192,59.37,143.524,59.344,143.869,59.411,144.123,59.408,144.483,59.376,145.555,59.414,145.756,59.374,145.829,59.33,145.932,59.198,146.05,59.171,146.273,59.221,146.444,59.43,146.537,59.457,146.804,59.373,147.04,59.366,147.514,59.269,147.688,59.291,147.875,59.388,148.257,59.414,148.491,59.262,148.727,59.258,148.914,59.283,148.965,59.369,148.89,59.4,148.744,59.374,148.709,59.449,148.797,59.532,148.925,59.475,149.133,59.481,149.205,59.488,149.175,59.527,149.128,59.559,149.065,59.631,149.29,59.728,149.425,59.761,149.643,59.77,150.203,59.651,150.326,59.639,150.457,59.591,150.667,59.556,150.54,59.525,150.484,59.494,150.615,59.507,150.729,59.469,150.823,59.461,150.863,59.475,150.912,59.523,150.983,59.571,151.034,59.586,151.17,59.583,151.348,59.561,151.486,59.524,151.798,59.323,151.942,59.284,152.104,59.291,152.17,59.278,152.261,59.224,151.99,59.16,151.733,59.147,151.505,59.164,151.121,59.083,151.327,58.875,151.705,58.867,152.088,58.91,152.165,58.997,152.32,59.031,152.401,59.026,152.576,58.954,152.818,58.926,152.882,58.939,153.078,59.082,153.196,59.094,153.273,59.091,153.361,59.215,153.695,59.225,153.892,59.114,154.011,59.076,154.247,59.109,154.376,59.188,154.458,59.217,154.704,59.141,154.824,59.188,155.017,59.196,155.16,59.19,155.153,59.27,155.167,59.36,154.971,59.45,154.583,59.54,154.358,59.481,154.272,59.475,154.213,59.483,154.15,59.529,154.209,59.6,154.269,59.658,154.267,59.73,154.293,59.833,154.39,59.877,154.441,59.884,154.578,60.095,154.971,60.377,155.428,60.55,155.716,60.682,155.853,60.777,156.056,60.996,156.344,61.155,156.483,61.206,156.63,61.272,156.68,61.481,156.791,61.53,156.892,61.565,157.084,61.676,157.371,61.747,157.469,61.799,157.799,61.795,158.07,61.754,158.152,61.765,158.334,61.826,158.547,61.811,158.824,61.85,159.077,61.922,159.189,61.929,159.295,61.914,159.423,61.808,159.496,61.781,159.552,61.719,159.722,61.758,160.183,61.903,160.238,61.904,160.309,61.894,160.321,61.839,160.317,61.793,160.247,61.648,160.163,61.538,159.931,61.324,159.883,61.292,159.914,61.234,159.949,61.129,159.835,61.014,159.79,60.957,159.883,60.943,160.004,61.007,160.184,61.048,160.281,61.045,160.379,61.025,160.226,60.832,160.201,60.73,160.177,60.691,160.174,60.638,160.287,60.667,160.368,60.709,160.482,60.74,160.767,60.753,160.915,60.893,161.037,60.963,162.188,61.541,162.393,61.662,162.506,61.67,162.608,61.65,162.699,61.653,162.718,61.695,162.752,61.711,162.856,61.705,162.922,61.598,162.994,61.544,163.047,61.554,163.085,61.571,163.139,61.611,163.198,61.645,163.258,61.699,163.208,61.737,163.009,61.792,163.018,61.891,163.131,62.05,163.118,62.153,163.163,62.26,163.213,62.313,163.258,62.337,163.302,62.373,163.244,62.455,163.287,62.511,163.332,62.551,164.256,62.697,164.418,62.705,164.567,62.675,164.792,62.571,165.044,62.517,165.397,62.494,165.417,62.447,165.28,62.463,165.214,62.448,165.226,62.406,165.208,62.374,165.124,62.412,164.888,62.432,164.671,62.474,164.598,62.471,164.288,62.347,164.207,62.292,164.074,62.045,164.068,61.874,164.02,61.711,163.883,61.64,163.837,61.558,163.804,61.461,163.973,61.42,163.992,61.388,164.005,61.344,163.893,61.24,163.62,61.111,163.589,61.084,163.554,61.026,163.71,60.917,163.585,60.877,163.466,60.85,163.352,60.8,162.973,60.783,162.713,60.659,162.266,60.537,162.068,60.466,162.004,60.42,161.846,60.232,161.754,60.152,161.449,60.027,161.219,59.846,160.855,59.627,160.711,59.602,160.547,59.547,160.35,59.394,159.847,59.127,159.592,58.804,159.453,58.696,159.308,58.611,159.211,58.519,159.037,58.424,158.687,58.281,158.449,58.163,158.321,58.083,158.275,58.009,158.21,58.025,157.975,57.986,157.666,58.02,157.45,57.799,157.217,57.777,156.986,57.83,156.872,57.804,156.83,57.78,156.792,57.748,156.9,57.677,156.948,57.616,156.964,57.561,156.977,57.466,156.849,57.29,156.728,57.152,156.529,57.021,156.067,56.782,156.025,56.752,155.983,56.695,155.717,56.072,155.643,55.794,155.555,55.348,155.564,55.199,155.62,54.865,155.706,54.521,155.905,53.928,155.95,53.744,156.099,53.006,156.11,52.866,156.154,52.747,156.229,52.626,156.365,52.509,156.377,52.367,156.49,51.913,156.5,51.475,156.521,51.38,156.543,51.312,156.671,51.227,156.713,51.124,156.724,51.047,156.748,50.969,156.847,51.007,157.202,51.213,157.49,51.409,157.531,51.48,157.629,51.535,157.823,51.605,158.104,51.81,158.332,52.091,158.463,52.305,158.493,52.383,158.5,52.46,158.481,52.627,158.534,52.688,158.609,52.874,158.56,52.922,158.432,52.957,158.472,53.032,158.565,53.05,158.64,53.015,158.684,52.935,158.745,52.909,158.952,53.048,159.136,53.117,159.586,53.238,159.772,53.23,159.947,53.125,160.025,53.13,160.002,53.275,159.898,53.381,159.899,53.448,159.956,53.552,159.914,53.621,159.871,53.673,159.844,53.784,159.922,54.008,160.01,54.131,160.074,54.189,160.289,54.288,160.517,54.431,160.773,54.541,160.936,54.578,161.13,54.598,161.294,54.521,161.625,54.516,161.726,54.533,161.967,54.689,162.106,54.752,162.08,54.886,161.996,54.998,161.824,55.139,161.785,55.205,161.729,55.358,161.724,55.496,161.776,55.655,161.924,55.84,162.085,56.09,162.146,56.128,162.334,56.188,162.461,56.235,162.528,56.261,162.489,56.399,162.589,56.455,162.671,56.49,162.878,56.476,162.944,56.508,163.038,56.522,162.975,56.449,162.893,56.399,162.713,56.331,162.628,56.232,162.84,56.066,162.972,56.034,163.047,56.045,163.189,56.137,163.261,56.174,163.336,56.233,163.294,56.448,163.243,56.565,163.257,56.688,163.165,56.725,163.046,56.741,162.922,56.723,162.85,56.757,162.803,56.811,162.791,56.875,162.815,57.023,162.808,57.103,162.762,57.244,162.762,57.284,162.779,57.358,162.957,57.477,163.109,57.565,163.188,57.637,163.214,57.687,163.226,57.79,163.145,57.837,162.718,57.946,162.654,57.948,162.522,57.904,162.467,57.766,162.391,57.717,162.392,57.745,162.411,57.778,162.197,57.829,162.098,57.875,162.04,57.918,162.002,57.981,161.96,58.077,162.049,58.273,162.142,58.447,162.453,58.709,162.643,58.8,162.847,58.939,162.935,58.964,162.97,58.986,163.004,59.02,162.94,59.114,162.975,59.137,163.01,59.148,163.085,59.131,163.273,59.303,163.269,59.52,163.321,59.705,163.365,59.781,163.41,59.835,163.494,59.887,163.574,59.914,163.69,59.978,163.744,60.028,163.78,60.041,163.913,60.037,164.018,60.017,164.135,59.984,164.113,59.898,164.252,59.974,164.377,60.058,164.44,60.073,164.525,60.061,164.67,59.997,164.779,59.874,164.854,59.841,164.954,59.844,165.019,59.861,165.074,59.946,165.085,60.099,165.193,60.125,165.285,60.135,165.416,60.205,165.583,60.236,165.942,60.357,166.18,60.48,166.352,60.485,166.308,60.414,166.292,60.346,166.23,60.178,166.168,60.089,166.136,59.979,166.149,59.922,166.187,59.849,166.273,59.856,166.332,59.872,166.453,59.947,166.964,60.307,167.227,60.406,167.626,60.469,167.746,60.509,168.138,60.574,168.463,60.592,168.67,60.563,168.788,60.564,169.227,60.596,169.276,60.557,169.618,60.438,169.815,60.265,169.854,60.25,169.887,60.218,169.898,60.148,169.927,60.104,169.983,60.067,170.154,59.986,170.351,59.966,170.396,60.01,170.423,60.048,170.512,60.26,170.589,60.343,170.59,60.394,170.608,60.435,170.799,60.496,170.949,60.523,171.49,60.726,171.729,60.843,171.831,60.837,171.918,60.864,171.998,60.901,172.067,60.916,172.213,60.998,172.393,61.062,172.362,61.117,172.396,61.167,172.497,61.186,172.585,61.19,172.697,61.249,172.69,61.295,172.731,61.314,172.789,61.311,172.868,61.293,172.908,61.312,172.838,61.376,172.807,61.436,172.857,61.469,173.055,61.406,173.132,61.407,173.391,61.557,173.623,61.716,173.822,61.679,174.139,61.795,174.285,61.818,174.514,61.824,174.611,61.868,174.715,61.948,174.798,61.939,175.192,62.034,175.268,62.102,175.366,62.121,175.442,62.128,175.614,62.184,176.328,62.346,176.437,62.411,176.703,62.506,176.907,62.536,177.159,62.561,177.19,62.592,177.008,62.627,176.965,62.659,176.963,62.693,176.99,62.722,177.024,62.777,177.091,62.79,177.173,62.75,177.259,62.75,177.298,62.784,177.339,62.781,177.36,62.737,177.316,62.685,177.296,62.644,177.293,62.599,177.351,62.587,177.663,62.583,178.019,62.547,178.964,62.355,179.045,62.324,179.121,62.32,179.134,62.396,179.177,62.469,179.289,62.51,179.477,62.613,179.571,62.688,179.571,62.773,179.511,62.863,179.381,62.884,179.302,62.94,179.26,63.008,179.329,63.058,179.405,63.078,179.389,63.147,179.332,63.19,179.028,63.282,178.921,63.345,178.919,63.4,178.793,63.54,178.775,63.51,178.787,63.442,178.744,63.395,178.679,63.402,178.669,63.44,178.706,63.522,178.654,63.557,178.466,63.574,178.44,63.606,178.626,63.651,178.681,63.651,178.731,63.667,178.692,63.842,178.65,63.965,178.536,63.976,178.451,64.011,178.475,64.089,178.477,64.128,178.381,64.261,178.313,64.314,178.229,64.364,178.164,64.309,178.131,64.235,178.045,64.22,177.953,64.222,177.688,64.305,177.433,64.444,177.41,64.573,177.467,64.737,177.427,64.763,177.388,64.774,177.05,64.719,176.843,64.634,176.731,64.625,176.508,64.682,176.141,64.586,176.219,64.642,176.283,64.664,176.351,64.705,176.301,64.784,176.247,64.843,176.169,64.885,176.057,64.905,175.946,64.865,175.859,64.825,175.678,64.782,175.331,64.747,175.097,64.747,174.699,64.681,174.549,64.684,175.098,64.777,175.396,64.784,175.781,64.844,176.061,64.961,176.429,64.855,176.557,64.84,176.831,64.849,177.069,64.787,177.148,64.805,177.223,64.862,177.123,64.947,177.037,65,176.94,65.016,176.646,65.007,176.452,65.025,176.341,65.047,176.413,65.071,176.625,65.038,176.881,65.082,177.179,65.014,177.252,64.954,177.337,64.931,177.582,64.778,177.749,64.717,178.285,64.672,178.52,64.603,178.698,64.631,179.15,64.782,179.448,64.822,179.651,64.921,179.827,65.034,180,65.067]},{"b":[145.869,43.42,145.944,43.46],"l":[145.944,43.426,145.941,43.445,145.931,43.457,145.914,43.455,145.896,43.455,145.882,43.46,145.869,43.458,145.869,43.45,145.882,43.444,145.887,43.433,145.894,43.42,145.907,43.422,145.931,43.426,145.944,43.426]},{"b":[146.274,43.62,146.359,43.652],"l":[146.333,43.647,146.31,43.652,146.284,43.639,146.274,43.63,146.288,43.625,146.332,43.62,146.359,43.625,146.35,43.644,146.333,43.647]},{"b":[146.028,43.407,146.101,43.449],"l":[146.089,43.449,146.049,43.434,146.028,43.42,146.032,43.407,146.046,43.409,146.07,43.421,146.086,43.429,146.101,43.44,146.089,43.449]},{"b":[-159.842,-21.25,-159.737,-21.186],"l":[-159.811,-21.186,-159.832,-21.2,-159.842,-21.229,-159.84,-21.238,-159.813,-21.242,-159.773,-21.25,-159.741,-21.249,-159.737,-21.241,-159.74,-21.208,-159.768,-21.188,-159.811,-21.186]},{"b":[-172.499,-8.583,-172.479,-8.546],"l":[-172.494,-8.559,-172.499,-8.548,-172.497,-8.546,-172.487,-8.556,-172.481,-8.567,-172.479,-8.581,-172.484,-8.583,-172.488,-8.572,-172.494,-8.559]},{"b":[-171.204,-9.358,-171.186,-9.333],"l":[-171.2,-9.345,-171.204,-9.333,-171.202,-9.333,-171.194,-9.339,-171.189,-9.347,-171.186,-9.355,-171.189,-9.358,-171.193,-9.352,-171.2,-9.345]},{"b":[-172.228,-4.524,-172.181,-4.492],"l":[-172.215,-4.503,-172.204,-4.5,-172.197,-4.5,-172.196,-4.495,-172.198,-4.492,-172.212,-4.494,-172.228,-4.507,-172.215,-4.524,-172.189,-4.522,-172.181,-4.515,-172.194,-4.516,-172.208,-4.518,-172.215,-4.511,-172.215,-4.503]},{"b":[-171.728,-2.856,-171.628,-2.761],"l":[-171.647,-2.856,-171.628,-2.856,-171.628,-2.847,-171.64,-2.811,-171.665,-2.786,-171.698,-2.766,-171.719,-2.761,-171.725,-2.768,-171.728,-2.774,-171.725,-2.781,-171.718,-2.779,-171.706,-2.773,-171.688,-2.779,-171.673,-2.788,-171.66,-2.799,-171.64,-2.829,-171.639,-2.847,-171.655,-2.84,-171.678,-2.825,-171.698,-2.822,-171.696,-2.826,-171.687,-2.83,-171.671,-2.844,-171.647,-2.856]},{"b":[-171.097,-3.143,-171.081,-3.115],"l":[-171.092,-3.125,-171.088,-3.115,-171.081,-3.12,-171.085,-3.135,-171.09,-3.143,-171.097,-3.137,-171.092,-3.125]},{"b":[-171.262,-4.468,-171.232,-4.442],"l":[-171.252,-4.442,-171.239,-4.444,-171.232,-4.454,-171.233,-4.463,-171.243,-4.468,-171.255,-4.467,-171.262,-4.46,-171.262,-4.449,-171.252,-4.442]},{"b":[-174.541,-4.695,-174.501,-4.657],"l":[-174.501,-4.688,-174.513,-4.675,-174.531,-4.659,-174.541,-4.657,-174.541,-4.662,-174.533,-4.665,-174.523,-4.674,-174.511,-4.686,-174.517,-4.687,-174.529,-4.682,-174.524,-4.69,-174.507,-4.694,-174.501,-4.695,-174.501,-4.688]},{"b":[-155.015,-4.094,-154.943,-4.031],"l":[-155.015,-4.048,-155.015,-4.055,-154.995,-4.071,-154.971,-4.086,-154.959,-4.094,-154.956,-4.088,-154.95,-4.056,-154.943,-4.042,-154.951,-4.031,-154.987,-4.039,-155.015,-4.048]},{"b":[-151.819,-11.457,-151.783,-11.392],"l":[-151.813,-11.392,-151.819,-11.409,-151.816,-11.431,-151.807,-11.451,-151.791,-11.457,-151.783,-11.441,-151.791,-11.414,-151.803,-11.393,-151.813,-11.392]},{"b":[-155.929,-5.632,-155.862,-5.608],"l":[-155.911,-5.609,-155.919,-5.608,-155.929,-5.608,-155.928,-5.619,-155.914,-5.632,-155.887,-5.632,-155.864,-5.627,-155.862,-5.619,-155.872,-5.611,-155.911,-5.609]},{"b":[-178.599,-19.175,-178.535,-19.109],"l":[-178.576,-19.125,-178.563,-19.119,-178.557,-19.113,-178.568,-19.109,-178.589,-19.119,-178.599,-19.137,-178.596,-19.151,-178.574,-19.165,-178.546,-19.175,-178.535,-19.166,-178.541,-19.157,-178.557,-19.154,-178.574,-19.143,-178.576,-19.125]},{"b":[-179.867,-19.003,-179.798,-18.924],"l":[-179.867,-18.978,-179.865,-18.999,-179.851,-19.003,-179.849,-18.991,-179.846,-18.971,-179.839,-18.962,-179.83,-18.956,-179.812,-18.968,-179.798,-18.97,-179.8,-18.94,-179.831,-18.924,-179.856,-18.943,-179.863,-18.964,-179.867,-18.978]},{"b":[-178.731,-20.671,-178.71,-20.645],"l":[-178.723,-20.667,-178.729,-20.66,-178.731,-20.653,-178.728,-20.645,-178.725,-20.646,-178.719,-20.652,-178.714,-20.66,-178.712,-20.668,-178.71,-20.671,-178.715,-20.67,-178.723,-20.667]},{"b":[-78.989,-33.668,-78.769,-33.575],"l":[-78.838,-33.585,-78.859,-33.578,-78.877,-33.575,-78.888,-33.576,-78.938,-33.614,-78.979,-33.644,-78.989,-33.662,-78.983,-33.668,-78.804,-33.646,-78.775,-33.642,-78.769,-33.627,-78.785,-33.61,-78.838,-33.585]},{"b":[-14.415,-7.976,-14.303,-7.883],"l":[-14.384,-7.883,-14.399,-7.906,-14.415,-7.944,-14.409,-7.967,-14.399,-7.976,-14.364,-7.974,-14.317,-7.956,-14.303,-7.935,-14.329,-7.913,-14.36,-7.886,-14.384,-7.883]},{"b":[72.772,11.196,72.796,11.263],"l":[72.788,11.216,72.78,11.202,72.773,11.196,72.772,11.214,72.782,11.243,72.793,11.263,72.796,11.26,72.793,11.242,72.788,11.216]},{"b":[73.023,8.252,73.084,8.317],"l":[73.053,8.257,73.039,8.252,73.029,8.254,73.023,8.266,73.026,8.275,73.039,8.265,73.056,8.275,73.075,8.306,73.079,8.317,73.084,8.311,73.08,8.293,73.067,8.269,73.053,8.257]},{"b":[73.382,3.229,73.443,3.29],"l":[73.417,3.231,73.395,3.229,73.382,3.246,73.385,3.271,73.402,3.289,73.428,3.29,73.443,3.274,73.435,3.25,73.417,3.231]},{"b":[73.473,4.155,73.528,4.248],"l":[73.519,4.187,73.512,4.165,73.495,4.155,73.479,4.159,73.473,4.171,73.481,4.188,73.495,4.21,73.504,4.235,73.518,4.248,73.528,4.243,73.527,4.23,73.522,4.211,73.519,4.187]},{"b":[131.135,3.022,131.188,3.061],"l":[131.137,3.039,131.135,3.025,131.15,3.022,131.172,3.026,131.186,3.042,131.188,3.056,131.172,3.061,131.152,3.054,131.137,3.039]},{"b":[151.57,7.333,151.65,7.39],"l":[151.593,7.379,151.608,7.375,151.604,7.357,151.575,7.351,151.57,7.346,151.578,7.338,151.639,7.333,151.648,7.346,151.65,7.363,151.643,7.379,151.629,7.39,151.606,7.389,151.593,7.379]},{"b":[151.856,7.427,151.913,7.467],"l":[151.882,7.467,151.865,7.466,151.86,7.457,151.856,7.432,151.864,7.427,151.881,7.432,151.913,7.454,151.911,7.46,151.882,7.467]},{"b":[166.845,11.146,166.899,11.169],"l":[166.888,11.169,166.859,11.166,166.845,11.153,166.864,11.146,166.89,11.153,166.899,11.165,166.888,11.169]},{"b":[177.006,-12.516,177.127,-12.477],"l":[177.118,-12.482,177.068,-12.477,177.026,-12.487,177.006,-12.491,177.019,-12.507,177.082,-12.516,177.121,-12.505,177.127,-12.493,177.118,-12.482]},{"b":[174.587,-21.706,174.63,-21.667],"l":[174.593,-21.702,174.587,-21.68,174.604,-21.667,174.628,-21.676,174.63,-21.695,174.622,-21.706,174.593,-21.702]},{"b":[179.196,-8.535,179.217,-8.466],"l":[179.201,-8.535,179.214,-8.524,179.217,-8.515,179.212,-8.488,179.203,-8.466,179.199,-8.47,179.198,-8.489,179.201,-8.512,179.196,-8.535,179.201,-8.535]},{"b":[178.667,-7.495,178.695,-7.458],"l":[178.667,-7.458,178.677,-7.463,178.69,-7.474,178.695,-7.483,178.693,-7.495,178.687,-7.494,178.674,-7.48,178.667,-7.458]},{"b":[178.371,-8.068,178.39,-8.032],"l":[178.387,-8.032,178.379,-8.047,178.371,-8.061,178.377,-8.068,178.39,-8.046,178.387,-8.032]},{"b":[169.523,-0.876,169.555,-0.847],"l":[169.523,-0.866,169.526,-0.853,169.539,-0.847,169.555,-0.857,169.551,-0.874,169.542,-0.876,169.523,-0.866]},{"b":[-128.35,-24.413,-128.29,-24.323],"l":[-128.33,-24.323,-128.35,-24.34,-128.342,-24.371,-128.321,-24.4,-128.3,-24.413,-128.29,-24.397,-128.291,-24.365,-128.304,-24.334,-128.33,-24.323]},{"b":[-177.39,28.196,-177.354,28.221],"l":[-177.354,28.217,-177.371,28.221,-177.389,28.209,-177.39,28.196,-177.371,28.2,-177.362,28.207,-177.354,28.217]},{"b":[-81.811,24.542,-81.739,24.577],"l":[-81.784,24.545,-81.74,24.554,-81.739,24.575,-81.768,24.577,-81.811,24.558,-81.809,24.542,-81.784,24.545]},{"b":[-86.753,21.191,-86.696,21.28],"l":[-86.727,21.264,-86.739,21.28,-86.753,21.279,-86.736,21.233,-86.714,21.197,-86.696,21.191,-86.714,21.239,-86.727,21.264]},{"b":[-166.149,66.222,-165.822,66.328],"l":[-165.822,66.328,-166.033,66.278,-166.146,66.237,-166.149,66.222,-166.11,66.227,-165.942,66.278,-165.83,66.317,-165.822,66.328]},{"b":[54.335,24.423,54.465,24.506],"l":[54.427,24.471,54.398,24.506,54.379,24.505,54.335,24.471,54.358,24.443,54.428,24.425,54.457,24.423,54.465,24.443,54.427,24.471]},{"b":[124.625,37.92,124.755,37.989],"l":[124.74,37.947,124.754,37.955,124.755,37.97,124.745,37.981,124.717,37.989,124.661,37.973,124.636,37.981,124.625,37.945,124.656,37.925,124.697,37.92,124.717,37.933]}]} \ No newline at end of file diff --git a/src/maps/data/countries_110m.json b/src/maps/data/countries_110m.json new file mode 100644 index 0000000..0779ab3 --- /dev/null +++ b/src/maps/data/countries_110m.json @@ -0,0 +1 @@ +{"f":[{"n":"Fiji","c":"FJ","r":6,"ml":3,"lx":177.98,"ly":-17.83,"b":[-180,-18.29,180,-16.02],"p":[[[180,-16.07,180,-16.56,179.36,-16.8,178.73,-17.01,178.6,-16.64,179.1,-16.43,179.41,-16.38,180,-16.07]],[[178.13,-17.5,178.37,-17.34,178.72,-17.63,178.55,-18.15,177.93,-18.29,177.38,-18.16,177.29,-17.72,177.67,-17.38,178.13,-17.5]],[[-179.79,-16.02,-179.92,-16.5,-180,-16.56,-180,-16.07,-179.79,-16.02]]]},{"n":"Tanzania","c":"TZ","r":3,"ml":3,"lx":34.96,"ly":-6.05,"b":[29.34,-11.72,40.32,-0.95],"p":[[[33.9,-0.95,34.07,-1.06,37.7,-3.1,37.77,-3.68,39.2,-4.68,38.74,-5.91,38.8,-6.48,39.44,-6.84,39.47,-7.1,39.19,-7.7,39.25,-8.01,39.19,-8.49,39.54,-9.11,39.95,-10.1,40.32,-10.32,39.52,-10.9,38.43,-11.29,37.83,-11.27,37.47,-11.57,36.78,-11.59,36.51,-11.72,35.31,-11.44,34.56,-11.52,34.28,-10.16,33.94,-9.69,33.74,-9.42,32.76,-9.23,32.19,-8.93,31.56,-8.76,31.16,-8.59,30.74,-8.34,30.2,-7.08,29.62,-6.52,29.42,-5.94,29.52,-5.42,29.34,-4.5,29.75,-4.45,30.12,-4.09,30.51,-3.57,30.75,-3.36,30.74,-3.03,30.53,-2.81,30.47,-2.41,30.76,-2.29,30.82,-1.7,30.42,-1.13,30.77,-1.01,31.87,-1.03,33.9,-0.95]]]},{"n":"Western Sahara","c":"EH","r":7,"ml":6,"lx":-12.63,"ly":23.97,"b":[-17.06,21,-8.67,27.66],"p":[[[-8.67,27.66,-8.67,27.59,-8.68,27.4,-8.69,25.88,-11.97,25.93,-11.94,23.37,-12.87,23.28,-13.12,22.77,-12.93,21.33,-16.85,21.33,-17.06,21,-17.02,21.42,-17,21.42,-14.75,21.5,-14.63,21.86,-14.22,22.31,-13.89,23.69,-12.5,24.77,-12.03,26.03,-11.72,26.1,-11.39,26.88,-10.55,26.99,-10.19,26.86,-9.74,26.86,-9.41,27.09,-8.79,27.12,-8.82,27.66,-8.67,27.66]]]},{"n":"Canada","c":"CA","r":2,"ml":1.7,"lx":-101.91,"ly":60.32,"b":[-141,41.68,-52.65,83.23],"p":[[[-122.84,49,-122.97,49,-124.91,49.98,-125.62,50.42,-127.44,50.83,-127.99,51.72,-127.85,52.33,-129.13,52.76,-129.31,53.56,-130.51,54.29,-130.54,54.8,-129.98,55.29,-130.01,55.92,-131.71,56.55,-132.73,57.69,-133.36,58.41,-134.27,58.86,-134.94,59.27,-135.48,59.79,-136.48,59.46,-137.45,58.91,-138.34,59.56,-139.04,60,-140.01,60.28,-141,60.31,-140.99,66,-140.99,69.71,-139.12,69.47,-137.55,68.99,-136.5,68.9,-135.63,69.32,-134.41,69.63,-132.93,69.51,-131.43,69.94,-129.79,70.19,-129.11,69.78,-128.36,70.01,-128.14,70.48,-127.45,70.38,-125.76,69.48,-124.42,70.16,-124.29,69.4,-123.06,69.56,-122.68,69.86,-121.47,69.8,-119.94,69.38,-117.6,69.01,-116.23,68.84,-115.25,68.91,-113.9,68.4,-115.3,67.9,-113.5,67.69,-110.8,67.81,-109.95,67.98,-108.88,67.38,-107.79,67.89,-108.81,68.31,-108.17,68.65,-106.95,68.7,-106.15,68.8,-105.34,68.56,-104.34,68.02,-103.22,68.1,-101.45,67.65,-99.9,67.81,-98.44,67.78,-98.56,68.4,-97.67,68.58,-96.12,68.24,-96.13,67.29,-95.49,68.09,-94.68,68.06,-94.23,69.07,-95.3,69.69,-96.47,70.09,-96.39,71.19,-95.21,71.92,-93.89,71.76,-92.88,71.32,-91.52,70.19,-92.41,69.7,-90.55,69.5,-90.55,68.47,-89.22,69.26,-88.02,68.62,-88.32,67.87,-87.35,67.2,-86.31,67.92,-85.58,68.78,-85.52,69.88,-84.1,69.81,-82.62,69.66,-81.28,69.16,-81.22,68.67,-81.96,68.13,-81.26,67.6,-81.39,67.11,-83.34,66.41,-84.74,66.26,-85.77,66.56,-86.07,66.06,-87.03,65.21,-87.32,64.78,-88.48,64.1,-89.91,64.03,-90.7,63.61,-90.77,62.96,-91.93,62.84,-93.16,62.02,-94.24,60.9,-94.63,60.11,-94.68,58.95,-93.22,58.78,-92.76,57.85,-92.3,57.09,-90.9,57.28,-89.04,56.85,-88.04,56.47,-87.32,56,-86.07,55.72,-85.01,55.3,-83.36,55.24,-82.27,55.15,-82.44,54.28,-82.13,53.28,-81.4,52.16,-79.91,51.21,-79.14,51.53,-78.6,52.56,-79.12,54.14,-79.83,54.67,-78.23,55.14,-77.1,55.84,-76.54,56.53,-76.62,57.2,-77.3,58.05,-78.52,58.8,-77.34,59.85,-77.77,60.76,-78.11,62.32,-77.41,62.55,-75.7,62.28,-74.67,62.18,-73.84,62.44,-72.91,62.11,-71.68,61.53,-71.37,61.14,-69.59,61.06,-69.62,60.22,-69.29,58.96,-68.37,58.8,-67.65,58.21,-66.2,58.77,-65.25,59.87,-64.58,60.34,-63.8,59.44,-62.5,58.17,-61.4,56.97,-61.8,56.34,-60.47,55.78,-59.57,55.2,-57.98,54.95,-57.33,54.63,-56.94,53.78,-56.16,53.65,-55.76,53.27,-55.68,52.15,-56.41,51.77,-57.13,51.42,-58.77,51.06,-60.03,50.24,-61.72,50.08,-63.86,50.29,-65.36,50.3,-66.4,50.23,-67.24,49.51,-68.51,49.07,-69.95,47.74,-71.1,46.82,-70.26,46.99,-68.65,48.3,-66.55,49.13,-65.06,49.23,-64.17,48.74,-65.12,48.07,-64.8,46.99,-64.47,46.24,-63.17,45.74,-61.52,45.88,-60.52,47.01,-60.45,46.28,-59.8,45.92,-61.04,45.27,-63.25,44.67,-64.25,44.27,-65.36,43.55,-66.12,43.62,-66.16,44.47,-64.43,45.29,-66.03,45.26,-67.14,45.14,-67.79,45.7,-67.79,47.07,-68.23,47.35,-68.9,47.19,-69.24,47.45,-70,46.69,-70.31,45.92,-70.66,45.46,-71.08,45.31,-71.4,45.26,-71.51,45.01,-73.35,45.01,-74.87,45,-75.32,44.82,-76.37,44.1,-76.5,44.02,-76.82,43.63,-77.74,43.63,-78.72,43.63,-79.17,43.47,-79.01,43.27,-78.92,42.97,-78.94,42.86,-80.25,42.37,-81.28,42.21,-82.44,41.68,-82.69,41.68,-83.03,41.83,-83.14,41.98,-83.12,42.08,-82.9,42.43,-82.43,42.98,-82.14,43.57,-82.34,44.44,-82.55,45.35,-83.59,45.82,-83.47,45.99,-83.62,46.12,-83.89,46.12,-84.09,46.28,-84.14,46.51,-84.34,46.41,-84.6,46.44,-84.54,46.54,-84.78,46.64,-84.88,46.9,-85.65,47.22,-86.46,47.55,-87.44,47.94,-88.38,48.3,-89.27,48.02,-89.6,48.01,-90.83,48.27,-91.64,48.14,-92.61,48.45,-93.63,48.61,-94.33,48.67,-94.64,48.84,-94.82,49.39,-95.16,49.38,-95.16,49,-97.23,49,-100.65,49,-104.05,49,-107.05,49,-110.05,49,-113,49,-116.05,49,-117.03,49,-120,49,-122.84,49]],[[-83.99,62.45,-83.25,62.91,-81.88,62.9,-81.9,62.71,-83.07,62.16,-83.77,62.18,-83.99,62.45]],[[-79.78,72.8,-80.88,73.33,-80.83,73.69,-80.35,73.76,-78.06,73.65,-76.34,73.1,-76.25,72.83,-77.31,72.86,-78.39,72.88,-79.49,72.74,-79.78,72.8]],[[-80.32,62.09,-79.93,62.39,-79.52,62.36,-79.27,62.16,-79.66,61.63,-80.1,61.72,-80.36,62.02,-80.32,62.09]],[[-93.61,74.98,-94.16,74.59,-95.61,74.67,-96.82,74.93,-96.29,75.38,-94.85,75.65,-93.98,75.3,-93.61,74.98]],[[-93.84,77.52,-94.3,77.49,-96.17,77.56,-96.44,77.83,-94.42,77.82,-93.72,77.63,-93.84,77.52]],[[-96.75,78.77,-95.56,78.42,-95.83,78.06,-97.31,77.85,-98.12,78.08,-98.55,78.46,-98.63,78.87,-97.34,78.83,-96.75,78.77]],[[-88.15,74.39,-89.76,74.52,-92.42,74.84,-92.77,75.39,-92.89,75.88,-93.89,76.32,-95.96,76.44,-97.12,76.75,-96.75,77.16,-94.68,77.1,-93.57,76.78,-91.61,76.78,-90.74,76.45,-90.97,76.07,-89.82,75.85,-89.19,75.61,-87.84,75.57,-86.38,75.48,-84.79,75.7,-82.75,75.78,-81.13,75.71,-80.06,75.34,-79.83,74.92,-80.46,74.66,-81.95,74.44,-83.23,74.56,-86.1,74.41,-88.15,74.39]],[[-111.26,78.15,-109.85,78,-110.19,77.7,-112.05,77.41,-113.53,77.73,-112.72,78.05,-111.26,78.15]],[[-110.96,78.8,-109.66,78.6,-110.88,78.41,-112.54,78.41,-112.53,78.55,-111.5,78.85,-110.96,78.8]],[[-55.6,51.32,-56.13,50.69,-56.8,49.81,-56.14,50.15,-55.47,49.94,-55.82,49.59,-54.94,49.31,-54.47,49.56,-53.48,49.25,-53.79,48.52,-53.09,48.69,-52.96,48.16,-52.65,47.54,-53.07,46.66,-53.52,46.62,-54.18,46.81,-53.96,47.63,-54.24,47.75,-55.4,46.88,-56,46.92,-55.29,47.39,-56.25,47.63,-57.33,47.57,-59.27,47.6,-59.42,47.9,-58.8,48.25,-59.23,48.52,-58.39,49.13,-57.36,50.72,-56.74,51.29,-55.87,51.63,-55.41,51.59,-55.6,51.32]],[[-83.88,65.11,-82.79,64.77,-81.64,64.46,-81.55,63.98,-80.82,64.06,-80.1,63.73,-80.99,63.41,-82.55,63.65,-83.11,64.1,-84.1,63.57,-85.52,63.05,-85.87,63.64,-87.22,63.54,-86.35,64.04,-86.22,64.82,-85.88,65.74,-85.16,65.66,-84.98,65.22,-84.46,65.37,-83.88,65.11]],[[-78.77,72.35,-77.82,72.75,-75.61,72.24,-74.23,71.77,-74.1,71.33,-72.24,71.56,-71.2,70.92,-68.79,70.53,-67.91,70.12,-66.97,69.19,-68.81,68.72,-66.45,68.07,-64.86,67.85,-63.42,66.93,-61.85,66.86,-62.16,66.16,-63.92,65,-65.15,65.43,-66.72,66.39,-68.02,66.26,-68.14,65.69,-67.09,65.11,-65.73,64.65,-65.32,64.38,-64.67,63.39,-65.01,62.67,-66.28,62.95,-68.78,63.75,-67.37,62.88,-66.33,62.28,-66.17,61.93,-68.88,62.33,-71.02,62.91,-72.24,63.4,-71.89,63.68,-73.38,64.19,-74.83,64.68,-74.82,64.39,-77.71,64.23,-78.56,64.57,-77.9,65.31,-76.02,65.33,-73.96,65.45,-74.29,65.81,-73.94,66.31,-72.65,67.28,-72.93,67.73,-73.31,68.07,-74.84,68.55,-76.87,68.89,-76.23,69.15,-77.29,69.77,-78.17,69.83,-78.96,70.17,-79.49,69.87,-81.31,69.74,-84.94,69.97,-87.06,70.26,-88.68,70.41,-89.51,70.76,-88.47,71.22,-89.89,71.22,-90.21,72.24,-89.44,73.13,-88.41,73.54,-85.83,73.8,-86.56,73.16,-85.77,72.53,-84.85,73.34,-82.32,73.75,-80.6,72.72,-80.75,72.06,-78.77,72.35]],[[-94.5,74.13,-92.42,74.1,-90.51,73.86,-92,72.97,-93.2,72.77,-94.27,72.02,-95.41,72.06,-96.03,72.94,-96.02,73.44,-95.5,73.86,-94.5,74.13]],[[-122.85,76.12,-121.16,76.86,-119.1,77.51,-117.57,77.5,-116.2,77.65,-116.34,76.88,-117.11,76.53,-118.04,76.48,-119.9,76.05,-121.5,75.9,-122.85,76.12]],[[-132.71,54.04,-131.75,54.12,-132.05,52.98,-131.18,52.18,-131.58,52.18,-132.18,52.64,-132.55,53.1,-133.05,53.41,-133.24,53.85,-133.18,54.17,-132.71,54.04]],[[-105.49,79.3,-103.53,79.17,-100.83,78.8,-100.06,78.32,-99.67,77.91,-101.3,78.02,-102.95,78.34,-105.18,78.38,-104.21,78.68,-105.42,78.92,-105.49,79.3]],[[-123.51,48.51,-124.01,48.37,-125.66,48.83,-125.95,49.18,-126.85,49.53,-127.03,49.81,-128.06,49.99,-128.44,50.54,-128.36,50.77,-127.31,50.55,-126.7,50.4,-125.76,50.3,-125.42,49.95,-124.92,49.48,-123.92,49.06,-123.51,48.51]],[[-121.54,74.45,-120.11,74.24,-117.56,74.19,-116.58,73.9,-115.51,73.48,-116.77,73.22,-119.22,72.52,-120.46,71.82,-120.46,71.38,-123.09,70.9,-123.62,71.34,-125.93,71.87,-125.5,72.29,-124.81,73.02,-123.94,73.68,-124.92,74.29,-121.54,74.45]],[[-107.82,75.85,-106.93,76.01,-105.88,75.97,-105.7,75.48,-106.31,75.01,-109.7,74.85,-112.22,74.42,-113.74,74.39,-113.87,74.72,-111.79,75.16,-116.31,75.04,-117.71,75.22,-116.35,76.2,-115.4,76.48,-112.59,76.14,-110.81,75.55,-109.07,75.47,-110.5,76.43,-109.58,76.79,-108.55,76.68,-108.21,76.2,-107.82,75.85]],[[-106.52,73.08,-105.4,72.67,-104.77,71.7,-104.46,70.99,-102.79,70.5,-100.98,70.02,-101.09,69.58,-102.73,69.5,-102.09,69.12,-102.43,68.75,-104.24,68.91,-105.96,69.18,-107.12,69.12,-109,68.78,-111.53,68.63,-113.31,68.54,-113.85,69.01,-115.22,69.28,-116.11,69.17,-117.34,69.96,-116.67,70.07,-115.13,70.24,-113.72,70.19,-112.42,70.37,-114.35,70.6,-116.49,70.52,-117.9,70.54,-118.43,70.91,-116.11,71.31,-117.66,71.3,-119.4,71.56,-118.56,72.31,-117.87,72.71,-115.19,73.31,-114.17,73.12,-114.67,72.65,-112.44,72.96,-111.05,72.45,-109.92,72.96,-109.01,72.63,-108.19,71.65,-107.69,72.07,-108.4,73.09,-107.52,73.24,-106.52,73.08]],[[-100.44,72.71,-101.54,73.36,-100.36,73.84,-99.16,73.63,-97.38,73.76,-97.12,73.47,-98.05,72.99,-96.54,72.56,-96.72,71.66,-98.36,71.27,-99.32,71.36,-100.01,71.74,-102.5,72.51,-102.48,72.83,-100.44,72.71]],[[-106.6,73.6,-105.26,73.64,-104.5,73.42,-105.38,72.76,-106.94,73.46,-106.6,73.6]],[[-98.5,76.72,-97.74,76.26,-97.7,75.74,-98.16,75,-99.81,74.9,-100.88,75.06,-100.86,75.64,-102.5,75.56,-102.57,76.34,-101.49,76.31,-99.98,76.65,-98.58,76.59,-98.5,76.72]],[[-96.02,80.6,-95.32,80.91,-94.3,80.98,-94.74,81.21,-92.41,81.26,-91.13,80.72,-89.45,80.51,-87.81,80.32,-87.02,79.66,-85.81,79.34,-87.19,79.04,-89.04,78.29,-90.8,78.22,-92.88,78.34,-93.95,78.75,-93.94,79.11,-93.15,79.38,-94.97,79.37,-96.08,79.71,-96.71,80.16,-96.02,80.6]],[[-91.59,81.89,-90.1,82.09,-88.93,82.12,-86.97,82.28,-85.5,82.65,-84.26,82.6,-83.18,82.32,-82.42,82.86,-81.1,83.02,-79.31,83.13,-76.25,83.17,-75.72,83.06,-72.83,83.23,-70.67,83.17,-68.5,83.11,-65.83,83.03,-63.68,82.9,-61.85,82.63,-61.89,82.36,-64.33,81.93,-66.75,81.73,-67.66,81.5,-65.48,81.51,-67.84,80.9,-69.47,80.62,-71.18,79.8,-73.24,79.63,-73.88,79.43,-76.91,79.32,-75.53,79.2,-76.22,79.02,-75.39,78.53,-76.34,78.18,-77.89,77.9,-78.36,77.51,-79.76,77.21,-79.62,76.98,-77.91,77.02,-77.89,76.78,-80.56,76.18,-83.17,76.45,-86.11,76.3,-87.6,76.42,-89.49,76.47,-89.62,76.95,-87.77,77.18,-88.26,77.9,-87.65,77.97,-84.98,77.54,-86.34,78.18,-87.96,78.37,-87.15,78.76,-85.38,79,-85.09,79.35,-86.51,79.74,-86.93,80.25,-84.2,80.21,-83.41,80.1,-81.85,80.46,-84.1,80.58,-87.6,80.52,-89.37,80.86,-90.2,81.26,-91.37,81.55,-91.59,81.89]],[[-75.22,67.44,-75.87,67.15,-76.99,67.1,-77.24,67.59,-76.81,68.15,-75.9,68.29,-75.11,68.01,-75.1,67.58,-75.22,67.44]],[[-96.26,69.49,-95.65,69.11,-96.27,68.76,-97.62,69.06,-98.43,68.95,-99.8,69.4,-98.92,69.71,-98.22,70.14,-97.16,69.86,-96.56,69.68,-96.26,69.49]],[[-64.52,49.87,-64.17,49.96,-62.86,49.71,-61.84,49.29,-61.81,49.11,-62.29,49.09,-63.59,49.4,-64.52,49.87]],[[-64.01,47.04,-63.66,46.55,-62.94,46.42,-62.01,46.44,-62.5,46.03,-62.87,45.97,-64.14,46.39,-64.39,46.73,-64.01,47.04]]]},{"n":"United States of America","c":"US","r":2,"ml":1.7,"lx":-97.48,"ly":39.54,"b":[-171.79,18.92,-66.96,71.36],"p":[[[-122.84,49,-120,49,-117.03,49,-116.05,49,-113,49,-110.05,49,-107.05,49,-104.05,49,-100.65,49,-97.23,49,-95.16,49,-95.16,49.38,-94.82,49.39,-94.64,48.84,-94.33,48.67,-93.63,48.61,-92.61,48.45,-91.64,48.14,-90.83,48.27,-89.6,48.01,-89.27,48.02,-88.38,48.3,-87.44,47.94,-86.46,47.55,-85.65,47.22,-84.88,46.9,-84.78,46.64,-84.54,46.54,-84.6,46.44,-84.34,46.41,-84.14,46.51,-84.09,46.28,-83.89,46.12,-83.62,46.12,-83.47,45.99,-83.59,45.82,-82.55,45.35,-82.34,44.44,-82.14,43.57,-82.43,42.98,-82.9,42.43,-83.12,42.08,-83.14,41.98,-83.03,41.83,-82.69,41.68,-82.44,41.68,-81.28,42.21,-80.25,42.37,-78.94,42.86,-78.92,42.97,-79.01,43.27,-79.17,43.47,-78.72,43.63,-77.74,43.63,-76.82,43.63,-76.5,44.02,-76.37,44.1,-75.32,44.82,-74.87,45,-73.35,45.01,-71.51,45.01,-71.4,45.26,-71.08,45.31,-70.66,45.46,-70.31,45.92,-70,46.69,-69.24,47.45,-68.9,47.19,-68.23,47.35,-67.79,47.07,-67.79,45.7,-67.14,45.14,-66.96,44.81,-68.03,44.33,-69.06,43.98,-70.12,43.68,-70.65,43.09,-70.81,42.87,-70.82,42.34,-70.49,41.81,-70.08,41.78,-70.18,42.15,-69.88,41.92,-69.97,41.64,-70.64,41.48,-71.12,41.49,-71.86,41.32,-72.29,41.27,-72.88,41.22,-73.71,40.93,-72.24,41.12,-71.94,40.93,-73.34,40.63,-73.98,40.63,-73.95,40.75,-74.26,40.47,-73.96,40.43,-74.18,39.71,-74.91,38.94,-74.98,39.2,-75.2,39.25,-75.53,39.5,-75.32,38.96,-75.07,38.78,-75.06,38.4,-75.38,38.02,-75.94,37.22,-76.03,37.26,-75.72,37.94,-76.23,38.32,-76.35,39.15,-76.54,38.72,-76.33,38.08,-76.99,38.24,-76.3,37.92,-76.26,36.97,-75.97,36.9,-75.87,36.55,-75.73,35.55,-76.36,34.81,-77.4,34.51,-78.05,33.93,-78.55,33.86,-79.06,33.49,-79.2,33.16,-80.3,32.51,-80.86,32.03,-81.34,31.44,-81.49,30.73,-81.31,30.04,-80.98,29.18,-80.54,28.47,-80.53,28.04,-80.06,26.88,-80.09,26.21,-80.13,25.82,-80.38,25.21,-80.68,25.08,-81.17,25.2,-81.33,25.64,-81.71,25.87,-82.24,26.73,-82.71,27.5,-82.86,27.89,-82.65,28.55,-82.93,29.1,-83.71,29.94,-84.1,30.09,-85.11,29.64,-85.29,29.69,-85.77,30.15,-86.4,30.4,-87.53,30.27,-88.42,30.38,-89.18,30.32,-89.59,30.16,-89.41,29.89,-89.43,29.49,-89.22,29.29,-89.41,29.16,-89.78,29.31,-90.15,29.12,-90.88,29.15,-91.63,29.68,-92.5,29.55,-93.23,29.78,-93.85,29.71,-94.69,29.48,-95.6,28.74,-96.59,28.31,-97.14,27.83,-97.37,27.38,-97.38,26.69,-97.33,26.21,-97.14,25.87,-97.53,25.84,-98.24,26.06,-99.02,26.37,-99.3,26.84,-99.52,27.54,-100.11,28.11,-100.46,28.7,-100.96,29.38,-101.66,29.78,-102.48,29.76,-103.11,28.97,-103.94,29.27,-104.46,29.57,-104.71,30.12,-105.04,30.64,-105.63,31.08,-106.14,31.4,-106.51,31.75,-108.24,31.75,-108.24,31.34,-109.03,31.34,-111.02,31.33,-113.3,32.04,-114.81,32.53,-114.72,32.72,-115.99,32.61,-117.13,32.54,-117.3,33.05,-117.94,33.62,-118.41,33.74,-118.52,34.03,-119.08,34.08,-119.44,34.35,-120.37,34.45,-120.62,34.61,-120.74,35.16,-121.71,36.16,-122.55,37.55,-122.51,37.78,-122.95,38.11,-123.73,38.95,-123.87,39.77,-124.4,40.31,-124.18,41.14,-124.21,42,-124.53,42.77,-124.14,43.71,-124.02,44.62,-123.9,45.52,-124.08,46.86,-124.4,47.72,-124.69,48.18,-124.57,48.38,-123.12,48.04,-122.59,47.1,-122.34,47.36,-122.5,48.18,-122.84,49]],[[-155.4,20.08,-155.22,19.99,-155.06,19.86,-154.81,19.51,-154.83,19.45,-155.22,19.24,-155.54,19.08,-155.69,18.92,-155.94,19.06,-155.91,19.34,-156.07,19.7,-156.02,19.81,-155.85,19.98,-155.92,20.17,-155.86,20.27,-155.79,20.25,-155.4,20.08]],[[-156,20.76,-156.08,20.64,-156.41,20.57,-156.59,20.78,-156.7,20.86,-156.71,20.93,-156.61,21.01,-156.26,20.92,-156,20.76]],[[-156.76,21.18,-156.79,21.07,-157.33,21.1,-157.25,21.22,-156.76,21.18]],[[-158.03,21.72,-157.94,21.65,-157.65,21.32,-157.71,21.26,-157.78,21.28,-158.13,21.31,-158.25,21.54,-158.29,21.58,-158.03,21.72]],[[-159.37,22.21,-159.35,21.98,-159.46,21.88,-159.8,22.07,-159.75,22.14,-159.6,22.24,-159.37,22.21]],[[-166.47,60.38,-165.67,60.29,-165.58,59.91,-166.19,59.75,-166.85,59.94,-167.46,60.21,-166.47,60.38]],[[-153.23,57.97,-152.56,57.9,-152.14,57.59,-153.01,57.12,-154.01,56.73,-154.52,56.99,-154.67,57.46,-153.76,57.82,-153.23,57.97]],[[-140.99,69.71,-140.99,66,-141,60.31,-140.01,60.28,-139.04,60,-138.34,59.56,-137.45,58.91,-136.48,59.46,-135.48,59.79,-134.94,59.27,-134.27,58.86,-133.36,58.41,-132.73,57.69,-131.71,56.55,-130.01,55.92,-129.98,55.29,-130.54,54.8,-131.09,55.18,-131.97,55.5,-132.25,56.37,-133.54,57.18,-134.08,58.12,-135.04,58.19,-136.63,58.21,-137.8,58.5,-139.87,59.54,-140.83,59.73,-142.57,60.08,-143.96,60,-145.93,60.46,-147.11,60.88,-148.22,60.67,-148.02,59.98,-148.57,59.91,-149.73,59.71,-150.61,59.37,-151.72,59.16,-151.86,59.74,-151.41,60.73,-150.35,61.03,-150.62,61.28,-151.9,60.73,-152.58,60.06,-154.02,59.35,-153.29,58.86,-154.23,58.15,-155.31,57.73,-156.31,57.42,-156.56,56.98,-158.12,56.46,-158.43,55.99,-159.6,55.57,-160.29,55.64,-161.22,55.36,-162.24,55.02,-163.07,54.69,-164.79,54.4,-164.94,54.57,-163.85,55.04,-162.87,55.35,-161.8,55.89,-160.56,56.01,-160.07,56.42,-158.68,57.02,-158.46,57.22,-157.72,57.57,-157.55,58.33,-157.04,58.92,-158.19,58.62,-158.52,58.79,-159.06,58.42,-159.71,58.93,-159.98,58.57,-160.36,59.07,-161.36,58.67,-161.97,58.67,-162.05,59.27,-161.87,59.63,-162.52,59.99,-163.82,59.8,-164.66,60.27,-165.35,60.51,-165.35,61.07,-166.12,61.5,-165.73,62.07,-164.92,62.63,-164.56,63.15,-163.75,63.22,-163.07,63.06,-162.26,63.54,-161.53,63.46,-160.77,63.77,-160.96,64.22,-161.52,64.4,-160.78,64.79,-161.39,64.78,-162.45,64.56,-162.76,64.34,-163.55,64.56,-164.96,64.45,-166.43,64.69,-166.85,65.09,-168.11,65.67,-166.71,66.09,-164.47,66.58,-163.65,66.58,-163.79,66.08,-161.68,66.12,-162.49,66.74,-163.72,67.12,-164.43,67.62,-165.39,68.04,-166.76,68.36,-166.2,68.88,-164.43,68.92,-163.17,69.37,-162.93,69.86,-161.91,70.33,-160.93,70.45,-159.04,70.89,-158.12,70.82,-156.58,71.36,-155.07,71.15,-154.34,70.7,-153.9,70.89,-152.21,70.83,-152.27,70.6,-150.74,70.43,-149.72,70.53,-147.61,70.21,-145.69,70.12,-144.92,69.99,-143.59,70.15,-142.07,69.85,-140.99,69.71]],[[-171.73,63.78,-171.11,63.59,-170.49,63.69,-169.68,63.43,-168.69,63.3,-168.77,63.19,-169.53,62.98,-170.29,63.19,-170.67,63.38,-171.55,63.32,-171.79,63.41,-171.73,63.78]]]},{"n":"Kazakhstan","c":"KZ","r":3,"ml":2.7,"lx":68.69,"ly":49.05,"b":[46.47,40.66,87.36,55.39],"p":[[[87.36,49.21,86.6,48.55,85.77,48.46,85.72,47.45,85.16,47,83.18,47.33,82.46,45.54,81.95,45.32,79.97,44.92,80.87,43.18,80.18,42.92,80.26,42.35,79.64,42.5,79.14,42.86,77.66,42.96,76,42.99,75.64,42.88,74.21,43.3,73.65,43.09,73.49,42.5,71.84,42.85,71.19,42.7,70.96,42.27,70.39,42.08,69.07,41.38,68.63,40.67,68.26,40.66,67.99,41.14,66.71,41.17,66.51,41.99,66.02,41.99,66.1,43,64.9,43.73,63.19,43.65,62.01,43.5,61.06,44.41,60.24,44.78,58.69,45.5,58.5,45.59,55.93,45,55.97,41.31,55.46,41.26,54.76,42.04,54.08,42.32,52.94,42.12,52.5,41.78,52.45,42.03,52.69,42.44,52.5,42.79,51.34,43.13,50.89,44.03,50.34,44.28,50.31,44.61,51.28,44.51,51.32,45.25,52.17,45.41,53.04,45.26,53.22,46.23,53.04,46.85,52.04,46.8,51.19,47.05,50.03,46.61,49.1,46.4,48.59,46.56,48.69,47.08,48.06,47.74,47.32,47.72,46.47,48.39,47.04,49.15,46.75,49.36,47.55,50.45,48.58,49.87,48.7,50.61,50.77,51.69,52.33,51.72,54.53,51.03,55.72,50.62,56.78,51.04,58.36,51.06,59.64,50.55,59.93,50.84,61.34,50.8,61.59,51.27,59.97,51.96,60.93,52.45,60.74,52.72,61.7,52.98,60.98,53.66,61.44,54.01,65.18,54.35,65.67,54.6,68.17,54.97,69.07,55.39,70.87,55.17,71.18,54.13,72.22,54.38,73.51,54.04,73.43,53.49,74.38,53.55,76.89,54.49,76.53,54.18,77.8,53.4,80.04,50.86,80.57,51.39,81.95,50.81,83.38,51.07,83.94,50.89,84.42,50.31,85.12,50.12,85.54,49.69,86.83,49.83,87.36,49.21]]]},{"n":"Uzbekistan","c":"UZ","r":3,"ml":3,"lx":64.01,"ly":41.69,"b":[55.93,37.14,73.06,45.59],"p":[[[55.97,41.31,55.93,45,58.5,45.59,58.69,45.5,60.24,44.78,61.06,44.41,62.01,43.5,63.19,43.65,64.9,43.73,66.1,43,66.02,41.99,66.51,41.99,66.71,41.17,67.99,41.14,68.26,40.66,68.63,40.67,69.07,41.38,70.39,42.08,70.96,42.27,71.26,42.17,70.42,41.52,71.16,41.14,71.87,41.39,73.06,40.87,71.77,40.15,71.01,40.24,70.6,40.22,70.46,40.5,70.67,40.96,69.33,40.73,69.01,40.09,68.54,39.53,67.7,39.58,67.44,39.14,68.18,38.9,68.39,38.16,67.83,37.14,67.08,37.36,66.52,37.36,66.55,37.97,65.22,38.4,64.17,38.89,63.52,39.36,62.37,40.05,61.88,41.08,61.55,41.27,60.47,41.22,60.08,41.43,59.98,42.22,58.63,42.75,57.79,42.17,56.93,41.83,57.1,41.32,55.97,41.31]]]},{"n":"Papua New Guinea","c":"PG","r":2,"ml":2.5,"lx":143.91,"ly":-5.7,"b":[141,-10.65,156.02,-2.5],"p":[[[141,-2.6,142.74,-3.29,144.58,-3.86,145.27,-4.37,145.83,-4.88,145.98,-5.47,147.65,-6.08,147.89,-6.61,146.97,-6.72,147.19,-7.39,148.08,-8.04,148.73,-9.1,149.31,-9.07,149.27,-9.51,150.04,-9.68,149.74,-9.87,150.8,-10.29,150.69,-10.58,150.03,-10.65,149.78,-10.39,148.92,-10.28,147.91,-10.13,147.14,-9.49,146.57,-8.94,146.05,-8.07,144.74,-7.63,143.9,-7.92,143.29,-8.25,143.41,-8.98,142.63,-9.33,142.07,-9.16,141.03,-9.12,141.02,-5.86,141,-2.6]],[[152.64,-3.66,153.02,-3.98,153.14,-4.5,152.83,-4.77,152.64,-4.18,152.41,-3.79,151.95,-3.46,151.38,-3.04,150.66,-2.74,150.94,-2.5,151.48,-2.78,151.82,-3,152.24,-3.24,152.64,-3.66]],[[151.3,-5.84,150.75,-6.08,150.24,-6.32,149.71,-6.32,148.89,-6.03,148.32,-5.75,148.4,-5.44,149.3,-5.58,149.85,-5.51,150,-5.03,150.14,-5,150.24,-5.53,150.81,-5.46,151.09,-5.11,151.65,-4.76,151.54,-4.17,152.14,-4.15,152.34,-4.31,152.32,-4.87,151.98,-5.48,151.46,-5.56,151.3,-5.84]],[[154.76,-5.34,155.06,-5.57,155.55,-6.2,156.02,-6.54,155.88,-6.82,155.6,-6.92,155.17,-6.54,154.73,-5.9,154.51,-5.14,154.65,-5.04,154.76,-5.34]]]},{"n":"Indonesia","c":"ID","r":2,"ml":1.7,"lx":101.89,"ly":-0.95,"b":[95.29,-10.36,141.03,5.48],"p":[[[141,-2.6,141.02,-5.86,141.03,-9.12,140.14,-8.3,139.13,-8.1,138.88,-8.38,137.61,-8.41,138.04,-7.6,138.67,-7.32,138.41,-6.23,137.93,-5.39,135.99,-4.55,135.16,-4.46,133.66,-3.54,133.37,-4.02,132.98,-4.11,132.76,-3.75,132.75,-3.31,131.99,-2.82,133.07,-2.46,133.78,-2.48,133.7,-2.21,132.23,-2.21,131.84,-1.62,130.94,-1.43,130.52,-0.94,131.87,-0.7,132.38,-0.37,133.99,-0.78,134.14,-1.15,134.42,-2.77,135.46,-3.37,136.29,-2.31,137.44,-1.7,138.33,-1.7,139.18,-2.05,139.93,-2.41,141,-2.6]],[[124.97,-8.89,125.07,-9.09,125.09,-9.39,124.44,-10.14,123.58,-10.36,123.46,-10.24,123.55,-9.9,123.98,-9.29,124.97,-8.89]],[[134.21,-6.9,134.11,-6.14,134.29,-5.78,134.5,-5.45,134.73,-5.74,134.72,-6.21,134.21,-6.9]],[[117.88,4.14,117.31,3.23,118.05,2.29,117.88,1.83,119,0.9,117.81,0.78,117.48,0.1,117.52,-0.8,116.56,-1.49,116.53,-2.48,116.15,-4.01,116,-3.66,114.86,-4.11,114.47,-3.5,113.76,-3.44,113.26,-3.12,112.07,-3.48,111.7,-2.99,111.05,-3.05,110.22,-2.93,110.07,-1.59,109.57,-1.31,109.09,-0.46,108.95,0.42,109.07,1.34,109.66,2.01,109.83,1.34,110.51,0.77,111.16,0.98,111.8,0.9,112.38,1.41,112.86,1.5,113.81,1.22,114.62,1.43,115.13,2.82,115.52,3.17,115.87,4.31,117.02,4.31,117.88,4.14]],[[129.37,-2.8,130.47,-3.09,130.83,-3.86,129.99,-3.45,129.16,-3.36,128.59,-3.43,127.9,-3.39,128.14,-2.84,129.37,-2.8]],[[126.87,-3.79,126.18,-3.61,125.99,-3.18,127,-3.13,127.25,-3.46,126.87,-3.79]],[[127.93,2.17,128,1.63,128.59,1.54,128.69,1.13,128.64,0.26,128.12,0.36,127.97,-0.25,128.38,-0.78,128.1,-0.9,127.7,-0.27,127.4,1.01,127.6,1.81,127.93,2.17]],[[122.93,0.88,124.08,0.92,125.07,1.64,125.24,1.42,124.44,0.43,123.69,0.24,122.72,0.43,121.06,0.38,120.18,0.24,120.04,-0.52,120.94,-1.41,121.48,-0.96,123.34,-0.62,123.26,-1.08,122.82,-0.93,122.39,-1.52,121.51,-1.9,122.45,-3.19,122.27,-3.53,123.17,-4.68,123.16,-5.34,122.63,-5.63,122.24,-5.28,122.72,-4.46,121.74,-4.85,121.49,-4.57,121.62,-4.19,120.9,-3.6,120.97,-2.63,120.31,-2.93,120.39,-4.1,120.43,-5.53,119.8,-5.67,119.37,-5.38,119.65,-4.46,119.5,-3.49,119.08,-3.49,118.77,-2.8,119.18,-2.15,119.32,-1.35,119.83,0.15,120.04,0.57,120.89,1.31,121.67,1.01,122.93,0.88]],[[120.3,-10.26,118.97,-9.56,119.9,-9.36,120.43,-9.67,120.78,-9.97,120.72,-10.24,120.3,-10.26]],[[121.34,-8.54,122.01,-8.46,122.9,-8.09,122.76,-8.65,121.25,-8.93,119.92,-8.81,119.92,-8.44,120.72,-8.24,121.34,-8.54]],[[118.26,-8.36,118.88,-8.28,119.13,-8.71,117.97,-8.91,117.28,-9.04,116.74,-9.03,117.08,-8.46,117.63,-8.45,117.9,-8.1,118.26,-8.36]],[[108.49,-6.42,108.62,-6.78,110.54,-6.88,110.76,-6.47,112.61,-6.95,112.98,-7.59,114.48,-7.78,115.71,-8.37,114.56,-8.75,113.46,-8.35,112.56,-8.38,111.52,-8.3,110.59,-8.12,109.43,-7.74,108.69,-7.64,108.28,-7.77,106.45,-7.35,106.28,-6.92,105.37,-6.85,106.05,-5.9,107.27,-5.95,108.07,-6.35,108.49,-6.42]],[[104.37,-1.08,104.54,-1.78,104.89,-2.34,105.62,-2.43,106.11,-3.06,105.86,-4.31,105.82,-5.85,104.71,-5.87,103.87,-5.04,102.58,-4.22,102.16,-3.61,101.4,-2.8,100.9,-2.05,100.14,-0.65,99.26,0.18,98.97,1.04,98.6,1.82,97.7,2.45,97.18,3.31,96.42,3.87,95.38,4.97,95.29,5.48,95.94,5.44,97.48,5.25,98.37,4.27,99.14,3.59,99.69,3.17,100.64,2.1,101.66,2.08,102.5,1.4,103.08,0.56,103.84,0.1,103.44,-0.71,104.01,-1.06,104.37,-1.08]]]},{"n":"Argentina","c":"AR","r":2,"ml":2,"lx":-64.17,"ly":-33.5,"b":[-73.42,-55.25,-53.63,-21.83],"p":[[[-68.63,-52.64,-68.25,-53.1,-67.75,-53.85,-66.45,-54.45,-65.05,-54.7,-65.5,-55.2,-66.45,-55.25,-66.96,-54.9,-67.56,-54.87,-68.63,-54.87,-68.63,-52.64]],[[-57.63,-30.22,-57.87,-31.02,-58.14,-32.04,-58.13,-33.04,-58.35,-33.26,-58.43,-33.91,-58.5,-34.43,-57.23,-35.29,-57.36,-35.98,-56.74,-36.41,-56.79,-36.9,-57.75,-38.18,-59.23,-38.72,-61.24,-38.93,-62.34,-38.83,-62.13,-39.42,-62.33,-40.17,-62.15,-40.68,-62.75,-41.03,-63.77,-41.17,-64.73,-40.8,-65.12,-41.06,-64.98,-42.06,-64.3,-42.36,-63.76,-42.04,-63.46,-42.56,-64.38,-42.87,-65.18,-43.5,-65.33,-44.5,-65.57,-45.04,-66.51,-45.04,-67.29,-45.55,-67.58,-46.3,-66.6,-47.03,-65.64,-47.24,-65.99,-48.13,-67.17,-48.7,-67.82,-49.87,-68.73,-50.26,-69.14,-50.73,-68.82,-51.77,-68.15,-52.35,-68.57,-52.3,-69.5,-52.14,-71.91,-52.01,-72.33,-51.43,-72.31,-50.68,-72.98,-50.74,-73.33,-50.38,-73.42,-49.32,-72.65,-48.88,-72.33,-48.24,-72.45,-47.74,-71.92,-46.88,-71.55,-45.56,-71.66,-44.97,-71.22,-44.78,-71.33,-44.41,-71.79,-44.21,-71.46,-43.79,-71.92,-43.41,-72.15,-42.25,-71.75,-42.05,-71.92,-40.83,-71.68,-39.81,-71.41,-38.92,-70.81,-38.55,-71.12,-37.58,-71.12,-36.66,-70.36,-36.01,-70.39,-35.17,-69.82,-34.19,-69.81,-33.27,-70.07,-33.09,-70.54,-31.37,-69.92,-30.34,-70.01,-29.37,-69.66,-28.46,-69,-27.52,-68.3,-26.9,-68.59,-26.51,-68.39,-26.19,-68.42,-24.52,-67.33,-24.03,-66.99,-22.99,-67.11,-22.74,-66.27,-21.83,-64.96,-22.08,-64.38,-22.8,-63.99,-21.99,-62.85,-22.03,-62.69,-22.25,-60.85,-23.88,-60.03,-24.03,-58.81,-24.77,-57.78,-25.16,-57.63,-25.6,-58.62,-27.12,-57.61,-27.4,-56.49,-27.55,-55.7,-27.39,-54.79,-26.62,-54.63,-25.74,-54.13,-25.55,-53.63,-26.12,-53.65,-26.92,-54.49,-27.47,-55.16,-27.88,-56.29,-28.85,-57.63,-30.22]]]},{"n":"Chile","c":"CL","r":2,"ml":1.7,"lx":-72.32,"ly":-38.15,"b":[-75.64,-55.61,-66.96,-17.58],"p":[[[-68.63,-52.64,-68.63,-54.87,-67.56,-54.87,-66.96,-54.9,-67.29,-55.3,-68.15,-55.61,-68.64,-55.58,-69.23,-55.5,-69.96,-55.2,-71.01,-55.05,-72.26,-54.5,-73.29,-53.96,-74.66,-52.84,-73.84,-53.05,-72.43,-53.72,-71.11,-54.07,-70.59,-53.62,-70.27,-52.93,-69.35,-52.52,-68.63,-52.64]],[[-69.59,-17.58,-69.1,-18.26,-68.97,-18.98,-68.44,-19.41,-68.76,-20.37,-68.22,-21.49,-67.83,-22.87,-67.11,-22.74,-66.99,-22.99,-67.33,-24.03,-68.42,-24.52,-68.39,-26.19,-68.59,-26.51,-68.3,-26.9,-69,-27.52,-69.66,-28.46,-70.01,-29.37,-69.92,-30.34,-70.54,-31.37,-70.07,-33.09,-69.81,-33.27,-69.82,-34.19,-70.39,-35.17,-70.36,-36.01,-71.12,-36.66,-71.12,-37.58,-70.81,-38.55,-71.41,-38.92,-71.68,-39.81,-71.92,-40.83,-71.75,-42.05,-72.15,-42.25,-71.92,-43.41,-71.46,-43.79,-71.79,-44.21,-71.33,-44.41,-71.22,-44.78,-71.66,-44.97,-71.55,-45.56,-71.92,-46.88,-72.45,-47.74,-72.33,-48.24,-72.65,-48.88,-73.42,-49.32,-73.33,-50.38,-72.98,-50.74,-72.31,-50.68,-72.33,-51.43,-71.91,-52.01,-69.5,-52.14,-68.57,-52.3,-69.46,-52.29,-69.94,-52.54,-70.85,-52.9,-71.01,-53.83,-71.43,-53.86,-72.56,-53.53,-73.7,-52.84,-74.95,-52.26,-75.26,-51.63,-74.98,-51.04,-75.48,-50.38,-75.61,-48.67,-75.18,-47.71,-74.13,-46.94,-75.64,-46.65,-74.69,-45.76,-74.35,-44.1,-73.24,-44.45,-72.72,-42.38,-73.39,-42.12,-73.7,-43.37,-74.33,-43.22,-74.02,-41.79,-73.68,-39.94,-73.22,-39.26,-73.51,-38.28,-73.59,-37.16,-73.17,-37.12,-72.55,-35.51,-71.86,-33.91,-71.44,-32.42,-71.67,-30.92,-71.37,-30.1,-71.49,-28.86,-70.91,-27.64,-70.72,-25.71,-70.4,-23.63,-70.09,-21.39,-70.16,-19.76,-70.37,-18.35,-69.86,-18.09,-69.59,-17.58]]]},{"n":"Democratic Republic of the Congo","c":"CD","r":2,"ml":2,"lx":23.46,"ly":-1.86,"b":[12.18,-13.26,31.17,5.26],"p":[[[29.34,-4.5,29.52,-5.42,29.42,-5.94,29.62,-6.52,30.2,-7.08,30.74,-8.34,30.35,-8.24,29,-8.41,28.73,-8.53,28.45,-9.16,28.67,-9.61,28.5,-10.79,28.37,-11.79,28.64,-11.97,29.34,-12.36,29.62,-12.18,29.7,-13.26,28.93,-13.25,28.52,-12.7,28.16,-12.27,27.39,-12.13,27.16,-11.61,26.55,-11.92,25.75,-11.78,25.42,-11.33,24.78,-11.24,24.31,-11.26,24.26,-10.95,23.91,-10.93,23.46,-10.87,22.84,-11.02,22.4,-10.99,22.16,-11.08,22.21,-9.89,21.88,-9.52,21.8,-8.91,21.95,-8.31,21.75,-7.92,21.73,-7.29,20.51,-7.3,20.6,-6.94,20.09,-6.94,20.04,-7.12,19.42,-7.16,19.17,-7.74,19.02,-7.99,18.46,-7.85,18.13,-7.99,17.47,-8.07,17.09,-7.55,16.86,-7.22,16.57,-6.62,16.33,-5.88,13.38,-5.86,13.02,-5.98,12.74,-5.97,12.32,-6.1,12.18,-5.79,12.44,-5.68,12.47,-5.25,12.63,-4.99,13,-4.78,13.26,-4.88,13.6,-4.5,14.14,-4.51,14.21,-4.79,14.58,-4.97,15.17,-4.34,15.75,-3.86,16.01,-3.54,15.97,-2.71,16.41,-1.74,16.87,-1.23,17.52,-0.74,17.64,-0.42,17.66,-0.06,17.83,0.29,17.77,0.86,17.9,1.74,18.09,2.37,18.39,2.9,18.45,3.5,18.54,4.2,18.93,4.71,19.47,5.03,20.29,4.69,20.93,4.32,21.66,4.22,22.41,4.03,22.7,4.63,22.84,4.71,23.3,4.61,24.41,5.11,24.81,4.9,25.13,4.93,25.28,5.17,25.65,5.26,26.4,5.15,27.04,5.13,27.37,5.23,27.98,4.41,28.43,4.29,28.7,4.46,29.16,4.39,29.72,4.6,29.95,4.17,30.83,3.51,30.77,2.34,31.17,2.2,30.85,1.85,30.47,1.58,30.09,1.06,29.88,0.6,29.82,-0.21,29.59,-0.59,29.58,-1.34,29.29,-1.62,29.25,-2.22,29.12,-2.29,29.02,-2.84,29.28,-3.29,29.34,-4.5]]]},{"n":"Somalia","c":"SO","r":6,"ml":4,"lx":45.19,"ly":3.57,"b":[40.98,-1.68,51.13,12.02],"p":[[[41.59,-1.68,40.99,-0.86,40.98,2.78,41.86,3.92,42.13,4.23,42.77,4.25,43.66,4.96,44.96,5,47.79,8,48.49,8.84,48.94,9.45,48.94,9.97,48.94,10.98,48.94,11.39,48.95,11.41,49.27,11.43,49.73,11.58,50.26,11.68,50.73,12.02,51.11,12.02,51.13,11.75,51.04,11.17,51.05,10.64,50.83,10.28,50.55,9.2,50.07,8.08,49.45,6.8,48.59,5.34,47.74,4.22,46.56,2.86,45.56,2.05,44.07,1.05,43.14,0.29,42.04,-0.92,41.81,-1.45,41.59,-1.68]]]},{"n":"Kenya","c":"KE","r":2,"ml":1.7,"lx":37.91,"ly":0.55,"b":[33.89,-4.68,41.86,5.51],"p":[[[39.2,-4.68,37.77,-3.68,37.7,-3.1,34.07,-1.06,33.9,-0.95,33.89,0.11,34.18,0.52,34.67,1.18,35.04,1.91,34.6,3.05,34.48,3.56,34.01,4.25,34.62,4.85,35.3,5.51,35.82,5.34,35.82,4.78,36.16,4.45,36.86,4.45,38.12,3.6,38.44,3.59,38.67,3.62,38.89,3.5,39.56,3.42,39.85,3.84,40.77,4.26,41.17,3.92,41.86,3.92,40.98,2.78,40.99,-0.86,41.59,-1.68,40.88,-2.08,40.64,-2.5,40.26,-2.57,40.12,-3.28,39.8,-3.68,39.6,-4.35,39.2,-4.68]]]},{"n":"Sudan","c":"SD","r":3,"ml":2.5,"lx":29.26,"ly":16.33,"b":[21.94,8.23,38.41,22],"p":[[[24.57,8.23,23.81,8.67,23.46,8.95,23.39,9.27,23.56,9.68,23.55,10.09,22.98,10.71,22.86,11.14,22.88,11.38,22.51,11.68,22.5,12.26,22.29,12.65,21.94,12.59,22.04,12.96,22.3,13.37,22.18,13.79,22.51,14.09,22.3,14.33,22.57,14.94,23.02,15.68,23.89,15.61,23.84,19.58,23.85,20,25,20,25,22,29.02,22,32.9,22,36.87,22,37.19,21.02,36.97,20.84,37.11,19.81,37.48,18.61,37.86,18.37,38.41,18,37.9,17.43,37.17,17.26,36.85,16.96,36.75,16.29,36.32,14.82,36.43,14.42,36.27,13.56,35.86,12.58,35.26,12.08,34.83,11.32,34.73,10.91,34.26,10.63,33.96,9.58,33.97,8.68,33.96,9.46,33.82,9.48,33.84,9.98,33.72,10.33,33.21,10.72,33.09,11.44,33.21,12.18,32.74,12.25,32.67,12.02,32.07,11.97,32.31,11.68,32.4,11.08,31.85,10.53,31.35,9.81,30.84,9.71,30,10.29,29.62,10.08,29.52,9.79,29,9.6,28.97,9.4,27.97,9.4,27.83,9.6,27.11,9.64,26.75,9.47,26.48,9.55,25.96,10.14,25.79,10.41,25.07,10.27,24.79,9.81,24.54,8.92,24.19,8.73,23.89,8.62,24.57,8.23]]]},{"n":"Chad","c":"TD","r":3,"ml":3,"lx":18.65,"ly":15.14,"b":[13.54,7.42,23.89,23.41],"p":[[[23.84,19.58,23.89,15.61,23.02,15.68,22.57,14.94,22.3,14.33,22.51,14.09,22.18,13.79,22.3,13.37,22.04,12.96,21.94,12.59,22.29,12.65,22.5,12.26,22.51,11.68,22.88,11.38,22.86,11.14,22.23,10.97,21.72,10.57,21,9.48,20.06,9.01,19.09,9.07,18.81,8.98,18.91,8.63,18.39,8.28,17.96,7.89,16.71,7.51,16.46,7.73,16.29,7.75,16.11,7.5,15.28,7.42,15.44,7.69,15.12,8.38,14.98,8.8,14.54,8.97,13.95,9.55,14.17,10.02,14.63,9.92,14.91,9.99,15.47,9.98,14.92,10.89,14.96,11.56,14.89,12.22,14.5,12.86,14.6,13.33,13.95,13.35,13.96,14,13.54,14.37,13.97,15.68,15.25,16.63,15.3,17.93,15.69,19.96,15.9,20.39,15.49,20.73,15.47,21.05,15.1,21.31,14.85,22.86,15.86,23.41,19.85,21.5,23.84,19.58]]]},{"n":"Haiti","c":"HT","r":5,"ml":4,"lx":-72.22,"ly":19.26,"b":[-74.46,18.03,-71.62,19.92],"p":[[[-71.71,19.71,-71.62,19.17,-71.7,18.79,-71.95,18.62,-71.69,18.32,-71.71,18.04,-72.37,18.21,-72.84,18.15,-73.45,18.22,-73.92,18.03,-74.46,18.34,-74.37,18.66,-73.45,18.53,-72.69,18.45,-72.33,18.67,-72.79,19.1,-72.78,19.48,-73.42,19.64,-73.19,19.92,-72.58,19.87,-71.71,19.71]]]},{"n":"Dominican Republic","c":"DO","r":5,"ml":4.5,"lx":-70.65,"ly":19.1,"b":[-71.95,17.6,-68.32,19.88],"p":[[[-71.71,18.04,-71.69,18.32,-71.95,18.62,-71.7,18.79,-71.62,19.17,-71.71,19.71,-71.59,19.88,-70.81,19.88,-70.21,19.62,-69.95,19.65,-69.77,19.29,-69.22,19.31,-69.25,19.02,-68.81,18.98,-68.32,18.61,-68.69,18.21,-69.16,18.42,-69.62,18.38,-69.95,18.43,-70.13,18.25,-70.52,18.18,-70.67,18.43,-71,18.28,-71.4,17.6,-71.66,17.76,-71.71,18.04]]]},{"n":"Russia","c":"RU","r":2,"ml":1.7,"lx":44.69,"ly":58.25,"b":[-180,41.15,180,81.25],"p":[[[178.73,71.1,180,71.52,180,70.83,178.9,70.78,178.73,71.1]],[[49.1,46.4,48.65,45.81,47.68,45.64,46.68,44.61,47.59,43.66,47.49,42.99,48.58,41.81,47.99,41.41,47.82,41.15,47.37,41.22,46.69,41.83,46.4,41.86,45.78,42.09,45.47,42.5,44.54,42.71,43.93,42.55,43.76,42.74,42.39,43.22,40.92,43.38,40.08,43.55,39.96,43.43,38.68,44.28,37.54,44.66,36.68,45.24,37.4,45.4,38.23,46.24,37.67,46.64,39.15,47.04,39.12,47.26,38.22,47.1,38.26,47.55,38.77,47.83,39.74,47.9,39.9,48.23,39.67,48.78,40.08,49.31,40.07,49.6,38.59,49.93,38.01,49.92,37.39,50.38,36.63,50.23,35.36,50.58,35.38,50.77,35.02,51.21,34.22,51.26,34.14,51.57,34.39,51.77,33.75,52.34,32.72,52.24,32.41,52.29,32.16,52.06,31.79,52.1,31.54,52.74,31.31,53.07,31.5,53.17,32.3,53.13,32.69,53.35,32.41,53.62,31.73,53.79,31.79,53.97,31.38,54.16,30.76,54.81,30.97,55.08,30.87,55.55,29.9,55.79,29.37,55.67,29.23,55.92,28.18,56.17,27.86,56.76,27.77,57.24,27.29,57.47,27.72,57.79,27.42,58.72,28.13,59.3,27.98,59.48,29.12,60.03,28.07,60.5,30.21,61.78,31.14,62.36,31.52,62.87,30.04,63.55,30.44,64.2,29.54,64.95,30.22,65.81,29.05,66.94,29.98,67.7,28.45,68.36,28.59,69.06,29.4,69.16,31.1,69.56,32.13,69.91,33.78,69.3,36.51,69.06,40.29,67.93,41.06,67.46,41.13,66.79,40.02,66.27,38.38,66,33.92,66.76,33.18,66.63,34.81,65.9,34.88,65.44,34.94,64.41,36.23,64.11,37.01,63.85,37.14,64.33,36.54,64.76,37.18,65.14,39.59,64.52,40.44,64.76,39.76,65.5,42.09,66.48,43.02,66.42,43.95,66.07,44.53,66.76,43.7,67.35,44.19,67.95,43.45,68.57,46.25,68.25,46.82,67.69,45.56,67.57,45.56,67.01,46.35,66.67,47.89,66.88,48.14,67.52,50.23,68,53.72,68.86,54.47,68.81,53.49,68.2,54.73,68.1,55.44,68.44,57.32,68.47,58.8,68.88,59.94,68.28,61.08,68.94,60.03,69.52,60.55,69.85,63.5,69.55,64.89,69.23,68.51,68.09,69.18,68.62,68.16,69.14,68.14,69.36,66.93,69.45,67.26,69.93,66.72,70.71,66.69,71.03,68.54,71.93,69.2,72.84,69.94,73.04,72.59,72.78,72.8,72.22,71.85,71.41,72.47,71.09,72.79,70.39,72.56,69.02,73.67,68.41,73.24,67.74,71.28,66.32,72.42,66.17,72.82,66.53,73.92,66.79,74.19,67.28,75.05,67.76,74.47,68.33,74.94,68.99,73.84,69.07,73.6,69.63,74.4,70.63,73.1,71.45,74.89,72.12,74.66,72.83,75.16,72.85,75.68,72.3,75.29,71.34,76.36,71.15,75.9,71.87,77.58,72.27,79.65,72.32,81.5,71.75,80.61,72.58,80.51,73.65,82.25,73.85,84.66,73.81,86.82,73.94,86.01,74.46,87.17,75.12,88.32,75.14,90.26,75.64,92.9,75.77,93.23,76.05,95.86,76.14,96.68,75.92,98.92,76.45,100.76,76.43,101.04,76.86,101.99,77.29,104.35,77.7,106.07,77.37,104.71,77.13,106.97,76.97,107.24,76.48,108.15,76.72,111.08,76.71,113.33,76.22,114.13,75.85,113.89,75.33,112.78,75.03,110.15,74.48,109.4,74.18,110.64,74.04,112.12,73.79,113.02,73.98,113.53,73.34,113.97,73.59,115.57,73.75,118.78,73.59,119.02,73.12,123.2,72.97,123.26,73.74,125.38,73.56,126.98,73.57,128.59,73.04,129.05,72.4,128.46,71.98,129.72,71.19,131.29,70.79,132.25,71.84,133.86,71.39,135.56,71.66,137.5,71.35,138.23,71.63,139.87,71.49,139.15,72.42,140.47,72.85,149.5,72.2,150.35,71.61,152.97,70.84,157.01,71.03,159,70.87,159.83,70.45,159.71,69.72,160.94,69.44,162.28,69.64,164.05,69.67,165.94,69.47,167.84,69.58,169.58,68.69,170.82,69.01,170.01,69.65,170.45,70.1,173.64,69.82,175.72,69.88,178.6,69.4,180,68.96,180,64.98,179.99,64.97,178.71,64.53,177.41,64.61,178.31,64.08,178.91,63.25,179.37,62.98,179.49,62.57,179.23,62.3,177.36,62.52,174.57,61.77,173.68,61.65,172.15,60.95,170.7,60.34,170.33,59.88,168.9,60.57,166.29,59.79,165.84,60.16,164.88,59.73,163.54,59.87,163.22,59.21,162.02,58.24,162.05,57.84,163.19,57.62,163.06,56.16,162.13,56.12,161.7,55.29,162.12,54.86,160.37,54.34,160.02,53.2,158.53,52.96,158.23,51.94,156.79,51.01,156.42,51.7,155.99,53.16,155.43,55.38,155.91,56.77,156.76,57.36,156.81,57.83,158.36,58.06,160.15,59.31,161.87,60.34,163.67,61.14,164.47,62.55,163.26,62.47,162.66,61.64,160.12,60.54,159.3,61.77,156.72,61.43,154.22,59.76,155.04,59.14,152.81,58.88,151.27,58.78,151.34,59.5,149.78,59.66,148.54,59.16,145.49,59.34,142.2,59.04,138.96,57.09,135.13,54.73,136.7,54.6,137.19,53.98,138.16,53.76,138.8,54.25,139.9,54.19,141.35,53.09,141.38,52.24,140.6,51.24,140.51,50.05,140.06,48.45,138.55,47,138.22,46.31,136.86,45.14,135.52,43.99,134.87,43.4,133.54,42.81,132.91,42.8,132.28,43.28,130.94,42.55,130.78,42.22,130.64,42.4,130.63,42.9,131.14,42.93,131.29,44.11,131.03,44.97,131.88,45.32,133.1,45.14,133.77,46.12,134.11,47.21,134.5,47.58,135.03,48.48,133.37,48.18,132.51,47.79,130.99,47.79,130.58,48.73,129.4,49.44,127.66,49.76,127.29,50.74,126.94,51.35,126.56,51.78,125.95,52.79,125.07,53.16,123.57,53.46,122.25,53.43,121,53.25,120.18,52.75,120.73,52.52,120.74,51.96,120.18,51.64,119.28,50.58,119.29,50.14,117.88,49.51,116.68,49.89,115.49,49.81,114.96,50.14,114.36,50.25,112.9,49.54,111.58,49.38,110.66,49.13,109.4,49.29,108.48,49.28,107.87,49.79,106.89,50.27,105.89,50.41,104.62,50.28,103.68,50.09,102.26,50.51,102.07,51.26,100.89,51.52,99.98,51.63,98.86,52.05,97.83,51.01,98.23,50.42,97.26,49.73,95.81,49.98,94.82,50.01,94.15,50.48,93.1,50.5,92.23,50.8,90.71,50.33,88.81,49.47,87.75,49.3,87.36,49.21,86.83,49.83,85.54,49.69,85.12,50.12,84.42,50.31,83.94,50.89,83.38,51.07,81.95,50.81,80.57,51.39,80.04,50.86,77.8,53.4,76.53,54.18,76.89,54.49,74.38,53.55,73.43,53.49,73.51,54.04,72.22,54.38,71.18,54.13,70.87,55.17,69.07,55.39,68.17,54.97,65.67,54.6,65.18,54.35,61.44,54.01,60.98,53.66,61.7,52.98,60.74,52.72,60.93,52.45,59.97,51.96,61.59,51.27,61.34,50.8,59.93,50.84,59.64,50.55,58.36,51.06,56.78,51.04,55.72,50.62,54.53,51.03,52.33,51.72,50.77,51.69,48.7,50.61,48.58,49.87,47.55,50.45,46.75,49.36,47.04,49.15,46.47,48.39,47.32,47.72,48.06,47.74,48.69,47.08,48.59,46.56,49.1,46.4]],[[93.78,81.02,95.94,81.25,97.88,80.75,100.19,79.78,99.94,78.88,97.76,78.76,94.97,79.04,93.31,79.43,92.55,80.14,91.18,80.34,93.78,81.02]],[[102.84,79.28,105.37,78.71,105.08,78.31,99.44,77.92,101.26,79.23,102.09,79.35,102.84,79.28]],[[138.83,76.14,141.47,76.09,145.09,75.56,144.3,74.82,140.61,74.85,138.96,74.61,136.97,75.26,137.51,75.95,138.83,76.14]],[[148.22,75.35,150.73,75.08,149.58,74.69,147.98,74.78,146.12,75.17,146.36,75.5,148.22,75.35]],[[139.86,73.37,140.81,73.77,142.06,73.86,143.48,73.48,143.6,73.21,142.09,73.21,140.04,73.32,139.86,73.37]],[[44.85,80.59,46.8,80.77,48.32,80.78,48.52,80.51,49.1,80.75,50.04,80.92,51.52,80.7,51.14,80.55,49.79,80.42,48.89,80.34,48.75,80.18,47.59,80.01,46.5,80.25,47.07,80.56,44.85,80.59]],[[22.73,54.33,20.89,54.31,19.66,54.43,19.89,54.87,21.27,55.19,22.32,55.02,22.76,54.86,22.65,54.58,22.73,54.33]],[[53.51,73.75,55.9,74.63,55.63,75.08,57.87,75.61,61.17,76.25,64.5,76.44,66.21,76.81,68.16,76.94,68.85,76.54,68.18,76.23,64.64,75.74,61.58,75.26,58.48,74.31,56.99,73.33,55.42,72.37,55.62,71.54,57.54,70.72,56.94,70.63,53.68,70.76,53.41,71.21,51.6,71.47,51.46,72.01,52.48,72.23,52.44,72.77,54.43,73.63,53.51,73.75]],[[142.91,53.7,143.26,52.74,143.24,51.76,143.65,50.75,144.65,48.98,143.17,49.31,142.56,47.86,143.53,46.84,143.51,46.14,142.75,46.74,142.09,45.97,141.91,46.81,142.02,47.78,141.9,48.86,142.14,49.62,142.18,50.95,141.59,51.94,141.68,53.3,142.61,53.76,142.21,54.23,142.65,54.37,142.91,53.7]],[[-174.93,67.21,-175.01,66.58,-174.34,66.34,-174.57,67.06,-171.86,66.91,-169.9,65.98,-170.89,65.54,-172.53,65.44,-172.55,64.46,-172.96,64.25,-173.89,64.28,-174.65,64.63,-175.98,64.92,-176.21,65.36,-177.22,65.52,-178.36,65.39,-178.9,65.74,-178.69,66.11,-179.88,65.87,-179.43,65.4,-180,64.98,-180,68.96,-177.55,68.2,-174.93,67.21]],[[-178.69,70.89,-180,70.83,-180,71.52,-179.87,71.56,-179.02,71.56,-177.58,71.27,-177.66,71.13,-178.69,70.89]],[[33.44,45.97,33.7,46.22,34.41,46.01,34.73,45.97,34.86,45.77,35.01,45.74,35.02,45.65,35.51,45.41,36.53,45.47,36.33,45.11,35.24,44.94,33.88,44.36,33.33,44.56,33.55,45.03,32.45,45.33,32.63,45.52,33.59,45.85,33.44,45.97]]]},{"n":"The Bahamas","c":"BS","r":4,"ml":4,"lx":-77.15,"ly":26.4,"b":[-78.98,23.71,-77,27.04],"p":[[[-78.98,26.79,-78.51,26.87,-77.85,26.84,-77.82,26.58,-78.91,26.42,-78.98,26.79]],[[-77.79,27.04,-77,26.59,-77.17,25.88,-77.36,26.01,-77.34,26.53,-77.79,26.93,-77.79,27.04]],[[-78.19,25.21,-77.89,25.17,-77.54,24.34,-77.53,23.76,-77.78,23.71,-78.03,24.29,-78.41,24.58,-78.19,25.21]]]},{"n":"Falkland Islands","c":"FK","r":5,"ml":4.5,"lx":-58.74,"ly":-51.61,"b":[-61.2,-52.3,-57.75,-51.1],"p":[[[-61.2,-51.85,-60,-51.25,-59.15,-51.5,-58.55,-51.1,-57.75,-51.55,-58.05,-51.9,-59.4,-52.2,-59.85,-51.85,-60.7,-52.3,-61.2,-51.85]]]},{"n":"Norway","c":"NO","r":3,"ml":3,"lx":9.68,"ly":61.36,"b":[4.99,58.08,31.29,80.66],"p":[[[15.14,79.67,15.52,80.02,16.99,80.05,18.25,79.7,21.54,78.96,19.03,78.56,18.47,77.83,17.59,77.64,17.12,76.81,15.91,76.77,13.76,77.38,14.67,77.74,13.17,78.02,11.22,78.87,10.44,79.65,13.17,80.01,13.72,79.66,15.14,79.67]],[[31.1,69.56,29.4,69.16,28.59,69.06,29.02,69.77,27.73,70.16,26.18,69.83,25.69,69.09,24.74,68.65,23.66,68.89,22.36,68.84,21.24,69.37,20.65,69.11,20.03,69.07,19.88,68.41,17.99,68.57,17.73,68.01,16.77,68.01,16.11,67.3,15.11,66.19,13.56,64.79,13.92,64.45,13.57,64.05,12.58,64.07,11.93,63.13,11.99,61.8,12.63,61.29,12.3,60.12,11.47,59.43,11.03,58.86,10.36,59.47,8.38,58.31,7.05,58.08,5.67,58.59,5.31,59.66,4.99,61.97,5.91,62.61,8.55,63.45,10.53,64.49,12.36,65.88,14.76,67.81,16.44,68.56,19.18,69.82,21.38,70.26,23.02,70.2,24.55,71.03,26.37,70.99,28.17,71.19,31.29,70.45,30.01,70.19,31.1,69.56]],[[27.41,80.06,25.92,79.52,23.02,79.4,20.08,79.57,19.9,79.84,18.46,79.86,17.37,80.32,20.46,80.6,21.91,80.36,22.92,80.66,25.45,80.41,27.41,80.06]],[[24.72,77.85,22.49,77.44,20.73,77.68,21.42,77.94,20.81,78.25,22.88,78.45,23.28,78.08,24.72,77.85]]]},{"n":"Greenland","c":"GL","r":3,"ml":1.7,"lx":-39.34,"ly":74.32,"b":[-73.3,60.04,-12.21,83.65],"p":[[[-46.76,82.63,-43.41,83.23,-39.9,83.18,-38.62,83.55,-35.09,83.65,-27.1,83.52,-20.85,82.73,-22.69,82.34,-26.52,82.3,-31.9,82.2,-31.4,82.02,-27.86,82.13,-24.84,81.79,-22.9,82.09,-22.07,81.73,-23.17,81.15,-20.62,81.52,-15.77,81.91,-12.77,81.72,-12.21,81.29,-16.29,80.58,-16.85,80.35,-20.05,80.18,-17.73,80.13,-18.9,79.4,-19.7,78.75,-19.67,77.64,-18.47,76.99,-20.04,76.94,-21.68,76.63,-19.83,76.1,-19.6,75.25,-20.67,75.16,-19.37,74.3,-21.59,74.22,-20.43,73.82,-20.76,73.46,-22.17,73.31,-23.57,73.31,-22.31,72.63,-22.3,72.18,-24.28,72.6,-24.79,72.33,-23.44,72.08,-22.13,71.47,-21.75,70.66,-23.54,70.47,-24.31,70.86,-25.54,71.43,-25.2,70.75,-26.36,70.23,-23.73,70.18,-22.35,70.13,-25.03,69.26,-27.75,68.47,-30.67,68.13,-31.78,68.12,-32.81,67.74,-34.2,66.68,-36.35,65.98,-37.04,65.94,-38.38,65.69,-39.81,65.46,-40.67,64.84,-40.68,64.14,-41.19,63.48,-42.82,62.68,-42.42,61.9,-42.87,61.07,-43.38,60.1,-44.79,60.04,-46.26,60.85,-48.26,60.86,-49.23,61.41,-49.9,62.38,-51.63,63.63,-52.14,64.28,-52.28,65.18,-53.66,66.1,-53.3,66.84,-53.97,67.19,-52.98,68.36,-51.48,68.73,-51.08,69.15,-50.87,69.93,-52.01,69.57,-52.56,69.43,-53.46,69.28,-54.68,69.61,-54.75,70.29,-54.36,70.82,-53.43,70.84,-51.39,70.57,-53.11,71.2,-54,71.55,-55,71.41,-55.83,71.65,-54.72,72.59,-55.33,72.96,-56.12,73.65,-57.32,74.71,-58.6,75.1,-58.59,75.52,-61.27,76.1,-63.39,76.18,-66.06,76.13,-68.5,76.06,-69.66,76.38,-71.4,77.01,-68.78,77.32,-66.76,77.38,-71.04,77.64,-73.3,78.04,-73.16,78.43,-69.37,78.91,-65.71,79.39,-65.32,79.76,-68.02,80.12,-67.15,80.52,-63.69,81.21,-62.23,81.32,-62.65,81.77,-60.28,82.03,-57.21,82.19,-54.13,82.2,-53.04,81.89,-50.39,82.44,-48,82.06,-46.6,81.99,-44.52,81.66,-46.9,82.2,-46.76,82.63]]]},{"n":"French Southern and Antarctic Lands","c":"TF","r":6,"ml":4,"lx":69.12,"ly":-49.3,"b":[68.72,-49.77,70.56,-48.62],"p":[[[68.94,-48.62,69.58,-48.94,70.53,-49.06,70.56,-49.25,70.28,-49.71,68.75,-49.77,68.72,-49.24,68.87,-48.83,68.94,-48.62]]]},{"n":"East Timor","c":"TL","r":5,"ml":4,"lx":125.85,"ly":-8.8,"b":[124.97,-9.39,127.34,-8.27],"p":[[[124.97,-8.89,125.09,-8.66,125.95,-8.43,126.64,-8.4,126.96,-8.27,127.34,-8.4,126.97,-8.67,125.93,-9.11,125.09,-9.39,125.07,-9.09,124.97,-8.89]]]},{"n":"South Africa","c":"ZA","r":2,"ml":1.7,"lx":23.67,"ly":-29.71,"b":[16.34,-34.82,32.83,-22.09],"p":[[[16.34,-28.58,16.82,-28.08,17.22,-28.36,17.39,-28.78,17.84,-28.86,18.46,-29.05,19,-28.97,19.89,-28.46,19.9,-24.77,20.17,-24.92,20.76,-25.87,20.67,-26.48,20.89,-26.83,21.61,-26.73,22.11,-26.28,22.58,-25.98,22.82,-25.5,23.31,-25.27,23.73,-25.39,24.21,-25.67,25.03,-25.72,25.66,-25.49,25.77,-25.17,25.94,-24.7,26.49,-24.62,26.79,-24.24,27.12,-23.57,28.02,-22.83,29.43,-22.09,29.84,-22.1,30.32,-22.27,30.66,-22.15,31.19,-22.25,31.67,-23.66,31.93,-24.37,31.75,-25.48,31.84,-25.84,31.33,-25.66,31.04,-25.73,30.95,-26.02,30.68,-26.4,30.69,-26.74,31.28,-27.29,31.87,-27.18,32.07,-26.73,32.83,-26.74,32.58,-27.47,32.46,-28.3,32.2,-28.75,31.52,-29.26,31.33,-29.4,30.9,-29.91,30.62,-30.42,30.06,-31.14,28.93,-32.17,28.22,-32.77,27.46,-33.23,26.42,-33.61,25.91,-33.67,25.78,-33.94,25.17,-33.8,24.68,-33.99,23.59,-33.79,22.99,-33.92,22.57,-33.86,21.54,-34.26,20.69,-34.42,20.07,-34.8,19.62,-34.82,19.19,-34.46,18.86,-34.44,18.42,-34,18.38,-34.14,18.24,-33.87,18.25,-33.28,17.93,-32.61,18.25,-32.43,18.22,-31.66,17.57,-30.73,17.06,-29.88,16.34,-28.58],[28.98,-28.96,28.54,-28.65,28.07,-28.85,27.53,-29.24,27,-29.88,27.75,-30.65,28.11,-30.55,28.29,-30.23,28.85,-30.07,29.02,-29.74,29.33,-29.26,28.98,-28.96]]]},{"n":"Lesotho","c":"LS","r":6,"ml":4,"lx":28.25,"ly":-29.48,"b":[27,-30.65,29.33,-28.65],"p":[[[28.98,-28.96,29.33,-29.26,29.02,-29.74,28.85,-30.07,28.29,-30.23,28.11,-30.55,27.75,-30.65,27,-29.88,27.53,-29.24,28.07,-28.85,28.54,-28.65,28.98,-28.96]]]},{"n":"Mexico","c":"MX","r":2,"ml":2,"lx":-102.29,"ly":23.92,"b":[-117.13,14.54,-86.81,32.72],"p":[[[-117.13,32.54,-115.99,32.61,-114.72,32.72,-114.81,32.53,-113.3,32.04,-111.02,31.33,-109.03,31.34,-108.24,31.34,-108.24,31.75,-106.51,31.75,-106.14,31.4,-105.63,31.08,-105.04,30.64,-104.71,30.12,-104.46,29.57,-103.94,29.27,-103.11,28.97,-102.48,29.76,-101.66,29.78,-100.96,29.38,-100.46,28.7,-100.11,28.11,-99.52,27.54,-99.3,26.84,-99.02,26.37,-98.24,26.06,-97.53,25.84,-97.14,25.87,-97.53,24.99,-97.7,24.27,-97.78,22.93,-97.87,22.44,-97.7,21.9,-97.39,21.41,-97.19,20.64,-96.53,19.89,-96.29,19.32,-95.9,18.83,-94.84,18.56,-94.43,18.14,-93.55,18.42,-92.79,18.52,-92.04,18.7,-91.41,18.88,-90.77,19.28,-90.53,19.87,-90.45,20.71,-90.28,21,-89.6,21.26,-88.54,21.49,-87.66,21.46,-87.05,21.54,-86.81,21.33,-86.85,20.85,-87.38,20.26,-87.62,19.65,-87.44,19.47,-87.59,19.04,-87.84,18.26,-88.09,18.52,-88.3,18.5,-88.49,18.49,-88.85,17.88,-89.03,18,-89.15,17.96,-89.14,17.81,-90.07,17.82,-91,17.82,-91,17.25,-91.45,17.25,-91.08,16.92,-90.71,16.69,-90.6,16.47,-90.44,16.41,-90.46,16.07,-91.75,16.07,-92.23,15.25,-92.09,15.06,-92.2,14.83,-92.23,14.54,-93.36,15.62,-93.88,15.94,-94.69,16.2,-95.25,16.13,-96.05,15.75,-96.56,15.65,-97.26,15.92,-98.01,16.11,-98.95,16.57,-99.7,16.71,-100.83,17.17,-101.67,17.65,-101.92,17.92,-102.48,17.98,-103.5,18.29,-103.92,18.75,-104.99,19.32,-105.49,19.95,-105.73,20.43,-105.4,20.53,-105.5,20.82,-105.27,21.08,-105.27,21.42,-105.6,21.87,-105.69,22.27,-106.03,22.77,-106.91,23.77,-107.92,24.55,-108.4,25.17,-109.26,25.58,-109.44,25.82,-109.29,26.44,-109.8,26.68,-110.39,27.16,-110.64,27.86,-111.18,27.94,-111.76,28.47,-112.23,28.95,-112.27,29.27,-112.81,30.02,-113.16,30.79,-113.15,31.17,-113.87,31.57,-114.21,31.52,-114.78,31.8,-114.94,31.39,-114.77,30.91,-114.67,30.16,-114.33,29.75,-113.59,29.06,-113.42,28.83,-113.27,28.75,-113.14,28.41,-112.96,28.43,-112.76,27.78,-112.46,27.53,-112.24,27.17,-111.62,26.66,-111.28,25.73,-110.99,25.29,-110.71,24.83,-110.66,24.3,-110.17,24.27,-109.77,23.81,-109.41,23.36,-109.43,23.19,-109.85,22.82,-110.03,22.82,-110.3,23.43,-110.95,24,-111.67,24.48,-112.18,24.74,-112.15,25.47,-112.3,26.01,-112.78,26.32,-113.46,26.77,-113.6,26.64,-113.85,26.9,-114.47,27.14,-115.06,27.72,-114.98,27.8,-114.57,27.74,-114.2,28.12,-114.16,28.57,-114.93,29.28,-115.52,29.56,-115.89,30.18,-116.26,30.84,-116.72,31.64,-117.13,32.54]]]},{"n":"Uruguay","c":"UY","r":4,"ml":3,"lx":-55.97,"ly":-32.96,"b":[-58.43,-34.95,-53.21,-30.11],"p":[[[-57.63,-30.22,-56.98,-30.11,-55.97,-30.88,-55.6,-30.85,-54.57,-31.49,-53.79,-32.05,-53.21,-32.73,-53.65,-33.2,-53.37,-33.77,-53.81,-34.4,-54.94,-34.95,-55.67,-34.75,-56.22,-34.86,-57.14,-34.43,-57.82,-34.46,-58.43,-33.91,-58.35,-33.26,-58.13,-33.04,-58.14,-32.04,-57.87,-31.02,-57.63,-30.22]]]},{"n":"Brazil","c":"BR","r":2,"ml":1.7,"lx":-49.56,"ly":-12.1,"b":[-73.99,-33.77,-34.73,5.24],"p":[[[-53.37,-33.77,-53.65,-33.2,-53.21,-32.73,-53.79,-32.05,-54.57,-31.49,-55.6,-30.85,-55.97,-30.88,-56.98,-30.11,-57.63,-30.22,-56.29,-28.85,-55.16,-27.88,-54.49,-27.47,-53.65,-26.92,-53.63,-26.12,-54.13,-25.55,-54.63,-25.74,-54.43,-25.16,-54.29,-24.57,-54.29,-24.02,-54.65,-23.84,-55.03,-24,-55.4,-23.96,-55.52,-23.57,-55.61,-22.66,-55.8,-22.36,-56.47,-22.09,-56.88,-22.28,-57.94,-22.09,-57.87,-20.73,-58.17,-20.18,-57.85,-19.97,-57.95,-19.4,-57.68,-18.96,-57.5,-18.17,-57.73,-17.55,-58.28,-17.27,-58.39,-16.88,-58.24,-16.3,-60.16,-16.26,-60.54,-15.09,-60.25,-15.08,-60.26,-14.65,-60.46,-14.35,-60.5,-13.78,-61.08,-13.48,-61.71,-13.49,-62.13,-13.2,-62.8,-13,-63.2,-12.63,-64.32,-12.46,-65.4,-11.57,-65.32,-10.9,-65.44,-10.51,-65.34,-9.76,-66.65,-9.93,-67.17,-10.31,-68.05,-10.71,-68.27,-11.01,-68.79,-11.04,-69.53,-10.95,-70.09,-11.12,-70.55,-11.01,-70.48,-9.49,-71.3,-10.08,-72.18,-10.05,-72.56,-9.52,-73.23,-9.46,-73.02,-9.03,-73.57,-8.42,-73.99,-7.52,-73.72,-7.34,-73.72,-6.92,-73.12,-6.63,-73.22,-6.09,-72.96,-5.74,-72.89,-5.27,-71.75,-4.59,-70.93,-4.4,-70.79,-4.25,-69.89,-4.3,-69.44,-1.56,-69.42,-1.12,-69.58,-0.55,-70.02,-0.19,-70.02,0.54,-69.45,0.71,-69.25,0.6,-69.22,0.99,-69.8,1.09,-69.82,1.71,-67.87,1.69,-67.54,2.04,-67.26,1.72,-67.07,1.13,-66.88,1.25,-66.33,0.72,-65.55,0.79,-65.35,1.1,-64.61,1.33,-64.2,1.49,-64.08,1.92,-63.37,2.2,-63.42,2.41,-64.27,2.5,-64.41,3.13,-64.37,3.8,-64.82,4.06,-64.63,4.15,-63.89,4.02,-63.09,3.77,-62.8,4.01,-62.09,4.16,-60.97,4.54,-60.6,4.92,-60.73,5.2,-60.21,5.24,-59.98,5.01,-60.11,4.57,-59.77,4.42,-59.54,3.96,-59.82,3.61,-59.97,2.76,-59.72,2.25,-59.65,1.79,-59.03,1.32,-58.54,1.27,-58.43,1.46,-58.11,1.51,-57.66,1.68,-57.34,1.95,-56.78,1.86,-56.54,1.9,-56,1.82,-55.91,2.02,-56.07,2.22,-55.97,2.51,-55.57,2.42,-55.1,2.52,-54.52,2.31,-54.09,2.11,-53.78,2.38,-53.55,2.33,-53.42,2.05,-52.94,2.12,-52.56,2.5,-52.25,3.24,-51.66,4.16,-51.32,4.2,-51.07,3.65,-50.51,1.9,-49.97,1.74,-49.95,1.05,-50.7,0.22,-50.39,-0.08,-48.62,-0.24,-48.58,-1.24,-47.82,-0.58,-46.57,-0.94,-44.91,-1.55,-44.42,-2.14,-44.58,-2.69,-43.42,-2.38,-41.47,-2.91,-39.98,-2.87,-38.5,-3.7,-37.22,-4.82,-36.45,-5.11,-35.6,-5.15,-35.24,-5.46,-34.9,-6.74,-34.73,-7.34,-35.13,-9,-35.64,-9.65,-37.05,-11.04,-37.68,-12.17,-38.42,-13.04,-38.67,-13.06,-38.95,-13.79,-38.88,-15.67,-39.16,-17.21,-39.27,-17.87,-39.58,-18.26,-39.76,-19.6,-40.77,-20.9,-40.94,-21.94,-41.75,-22.37,-41.99,-22.97,-43.07,-22.97,-44.65,-23.35,-45.35,-23.8,-46.47,-24.09,-47.65,-24.89,-48.5,-25.88,-48.64,-26.62,-48.47,-27.18,-48.66,-28.19,-48.89,-28.67,-49.59,-29.22,-50.7,-30.98,-51.58,-31.78,-52.26,-32.25,-52.71,-33.2,-53.37,-33.77]]]},{"n":"Bolivia","c":"BO","r":3,"ml":3,"lx":-64.59,"ly":-16.67,"b":[-69.59,-22.87,-57.5,-9.76],"p":[[[-69.53,-10.95,-68.79,-11.04,-68.27,-11.01,-68.05,-10.71,-67.17,-10.31,-66.65,-9.93,-65.34,-9.76,-65.44,-10.51,-65.32,-10.9,-65.4,-11.57,-64.32,-12.46,-63.2,-12.63,-62.8,-13,-62.13,-13.2,-61.71,-13.49,-61.08,-13.48,-60.5,-13.78,-60.46,-14.35,-60.26,-14.65,-60.25,-15.08,-60.54,-15.09,-60.16,-16.26,-58.24,-16.3,-58.39,-16.88,-58.28,-17.27,-57.73,-17.55,-57.5,-18.17,-57.68,-18.96,-57.95,-19.4,-57.85,-19.97,-58.17,-20.18,-58.18,-19.87,-59.12,-19.36,-60.04,-19.34,-61.79,-19.63,-62.27,-20.51,-62.29,-21.05,-62.69,-22.25,-62.85,-22.03,-63.99,-21.99,-64.38,-22.8,-64.96,-22.08,-66.27,-21.83,-67.11,-22.74,-67.83,-22.87,-68.22,-21.49,-68.76,-20.37,-68.44,-19.41,-68.97,-18.98,-69.1,-18.26,-69.59,-17.58,-68.96,-16.5,-69.39,-15.66,-69.16,-15.32,-69.34,-14.95,-68.95,-14.45,-68.93,-13.6,-68.88,-12.9,-68.67,-12.56,-69.53,-10.95]]]},{"n":"Peru","c":"PE","r":2,"ml":2,"lx":-72.9,"ly":-12.98,"b":[-81.41,-18.35,-68.67,-0.06],"p":[[[-69.89,-4.3,-70.79,-4.25,-70.93,-4.4,-71.75,-4.59,-72.89,-5.27,-72.96,-5.74,-73.22,-6.09,-73.12,-6.63,-73.72,-6.92,-73.72,-7.34,-73.99,-7.52,-73.57,-8.42,-73.02,-9.03,-73.23,-9.46,-72.56,-9.52,-72.18,-10.05,-71.3,-10.08,-70.48,-9.49,-70.55,-11.01,-70.09,-11.12,-69.53,-10.95,-68.67,-12.56,-68.88,-12.9,-68.93,-13.6,-68.95,-14.45,-69.34,-14.95,-69.16,-15.32,-69.39,-15.66,-68.96,-16.5,-69.59,-17.58,-69.86,-18.09,-70.37,-18.35,-71.38,-17.77,-71.46,-17.36,-73.44,-16.36,-75.24,-15.27,-76.01,-14.65,-76.42,-13.82,-76.26,-13.54,-77.11,-12.22,-78.09,-10.38,-79.04,-8.39,-79.45,-7.93,-79.76,-7.19,-80.54,-6.54,-81.25,-6.14,-80.93,-5.69,-81.41,-4.74,-81.1,-4.04,-80.3,-3.4,-80.18,-3.82,-80.47,-4.06,-80.44,-4.43,-80.03,-4.35,-79.62,-4.45,-79.21,-4.96,-78.64,-4.55,-78.45,-3.87,-77.84,-3,-76.64,-2.61,-75.54,-1.56,-75.23,-0.91,-75.37,-0.15,-75.11,-0.06,-74.44,-0.53,-74.12,-1,-73.66,-1.26,-73.07,-2.31,-72.33,-2.43,-71.77,-2.17,-71.41,-2.34,-70.81,-2.26,-70.05,-2.73,-70.69,-3.74,-70.39,-3.77,-69.89,-4.3]]]},{"n":"Colombia","c":"CO","r":2,"ml":3,"lx":-73.17,"ly":3.37,"b":[-78.99,-4.3,-66.88,12.44],"p":[[[-66.88,1.25,-67.07,1.13,-67.26,1.72,-67.54,2.04,-67.87,1.69,-69.82,1.71,-69.8,1.09,-69.22,0.99,-69.25,0.6,-69.45,0.71,-70.02,0.54,-70.02,-0.19,-69.58,-0.55,-69.42,-1.12,-69.44,-1.56,-69.89,-4.3,-70.39,-3.77,-70.69,-3.74,-70.05,-2.73,-70.81,-2.26,-71.41,-2.34,-71.77,-2.17,-72.33,-2.43,-73.07,-2.31,-73.66,-1.26,-74.12,-1,-74.44,-0.53,-75.11,-0.06,-75.37,-0.15,-75.8,0.08,-76.29,0.42,-76.58,0.26,-77.42,0.4,-77.67,0.83,-77.86,0.81,-78.86,1.38,-78.99,1.69,-78.62,1.77,-78.66,2.27,-78.43,2.63,-77.93,2.7,-77.51,3.33,-77.13,3.85,-77.5,4.09,-77.31,4.67,-77.53,5.58,-77.32,5.85,-77.48,6.69,-77.88,7.22,-77.75,7.71,-77.43,7.64,-77.24,7.94,-77.47,8.52,-77.35,8.67,-76.84,8.64,-76.09,9.34,-75.67,9.44,-75.66,9.77,-75.48,10.62,-74.91,11.08,-74.28,11.1,-74.2,11.31,-73.41,11.23,-72.63,11.73,-72.24,11.96,-71.75,12.44,-71.4,12.38,-71.14,12.11,-71.33,11.78,-71.97,11.61,-72.23,11.11,-72.61,10.82,-72.91,10.45,-73.03,9.74,-73.3,9.15,-72.79,9.09,-72.66,8.63,-72.44,8.41,-72.36,8,-72.48,7.63,-72.44,7.42,-72.2,7.34,-71.96,6.99,-70.67,7.09,-70.09,6.96,-69.39,6.1,-68.99,6.21,-68.27,6.15,-67.7,6.27,-67.34,6.1,-67.52,5.56,-67.74,5.22,-67.82,4.5,-67.62,3.84,-67.34,3.54,-67.3,3.32,-67.81,2.82,-67.45,2.6,-67.18,2.25,-66.88,1.25]]]},{"n":"Panama","c":"PA","r":4,"ml":4,"lx":-80.35,"ly":8.72,"b":[-82.97,7.22,-77.24,9.61],"p":[[[-77.35,8.67,-77.47,8.52,-77.24,7.94,-77.43,7.64,-77.75,7.71,-77.88,7.22,-78.21,7.51,-78.43,8.05,-78.18,8.32,-78.44,8.39,-78.62,8.72,-79.12,9,-79.56,8.93,-79.76,8.58,-80.16,8.33,-80.38,8.3,-80.48,8.09,-80,7.55,-80.28,7.42,-80.42,7.27,-80.89,7.22,-81.06,7.82,-81.19,7.65,-81.52,7.71,-81.72,8.11,-82.13,8.18,-82.39,8.29,-82.82,8.29,-82.85,8.07,-82.97,8.23,-82.91,8.42,-82.83,8.63,-82.87,8.81,-82.72,8.93,-82.93,9.07,-82.93,9.48,-82.55,9.57,-82.19,9.21,-82.21,9,-81.81,8.95,-81.71,9.03,-81.44,8.79,-80.95,8.86,-80.52,9.11,-79.91,9.31,-79.57,9.61,-79.02,9.55,-79.06,9.45,-78.5,9.42,-78.06,9.25,-77.73,8.95,-77.35,8.67]]]},{"n":"Costa Rica","c":"CR","r":5,"ml":2.5,"lx":-84.08,"ly":10.07,"b":[-85.94,8.23,-82.55,11.22],"p":[[[-82.55,9.57,-82.93,9.48,-82.93,9.07,-82.72,8.93,-82.87,8.81,-82.83,8.63,-82.91,8.42,-82.97,8.23,-83.51,8.45,-83.71,8.66,-83.6,8.83,-83.63,9.05,-83.91,9.29,-84.3,9.49,-84.65,9.62,-84.71,9.91,-84.98,10.09,-84.91,9.8,-85.11,9.56,-85.34,9.83,-85.66,9.93,-85.8,10.13,-85.79,10.44,-85.66,10.75,-85.94,10.9,-85.71,11.09,-85.56,11.22,-84.9,10.95,-84.67,11.08,-84.36,11,-84.19,10.79,-83.9,10.73,-83.66,10.94,-83.4,10.4,-83.02,9.99,-82.55,9.57]]]},{"n":"Nicaragua","c":"NI","r":5,"ml":4,"lx":-85.07,"ly":12.67,"b":[-87.67,10.73,-83.15,15.02],"p":[[[-83.66,10.94,-83.9,10.73,-84.19,10.79,-84.36,11,-84.67,11.08,-84.9,10.95,-85.56,11.22,-85.71,11.09,-86.06,11.4,-86.53,11.81,-86.75,12.14,-87.17,12.46,-87.67,12.91,-87.56,13.06,-87.39,12.91,-87.32,12.98,-87.01,13.03,-86.88,13.25,-86.73,13.26,-86.76,13.75,-86.52,13.78,-86.31,13.77,-86.1,14.04,-85.8,13.84,-85.7,13.96,-85.51,14.08,-85.17,14.35,-85.15,14.56,-85.05,14.55,-84.92,14.79,-84.82,14.82,-84.65,14.67,-84.45,14.62,-84.23,14.75,-83.98,14.75,-83.63,14.88,-83.49,15.02,-83.15,15,-83.23,14.9,-83.28,14.68,-83.18,14.31,-83.41,13.97,-83.52,13.57,-83.55,13.13,-83.5,12.87,-83.47,12.42,-83.63,12.32,-83.72,11.89,-83.65,11.63,-83.86,11.37,-83.81,11.1,-83.66,10.94]]]},{"n":"Honduras","c":"HN","r":5,"ml":4.5,"lx":-86.89,"ly":14.79,"b":[-89.35,12.98,-83.15,16.01],"p":[[[-83.15,15,-83.49,15.02,-83.63,14.88,-83.98,14.75,-84.23,14.75,-84.45,14.62,-84.65,14.67,-84.82,14.82,-84.92,14.79,-85.05,14.55,-85.15,14.56,-85.17,14.35,-85.51,14.08,-85.7,13.96,-85.8,13.84,-86.1,14.04,-86.31,13.77,-86.52,13.78,-86.76,13.75,-86.73,13.26,-86.88,13.25,-87.01,13.03,-87.32,12.98,-87.49,13.3,-87.79,13.38,-87.72,13.79,-87.86,13.89,-88.07,13.96,-88.5,13.85,-88.54,13.98,-88.84,14.14,-89.06,14.34,-89.35,14.42,-89.15,14.68,-89.23,14.87,-89.15,15.07,-88.68,15.35,-88.23,15.73,-88.12,15.69,-87.9,15.86,-87.62,15.88,-87.52,15.8,-87.37,15.85,-86.9,15.76,-86.44,15.78,-86.12,15.89,-86,16.01,-85.68,15.95,-85.44,15.89,-85.18,15.91,-84.98,16,-84.53,15.86,-84.37,15.84,-84.06,15.65,-83.77,15.42,-83.41,15.27,-83.15,15]]]},{"n":"El Salvador","c":"SV","r":6,"ml":5,"lx":-88.89,"ly":13.69,"b":[-90.1,13.15,-87.72,14.42],"p":[[[-89.35,14.42,-89.06,14.34,-88.84,14.14,-88.54,13.98,-88.5,13.85,-88.07,13.96,-87.86,13.89,-87.72,13.79,-87.79,13.38,-87.9,13.15,-88.48,13.16,-88.84,13.26,-89.26,13.46,-89.81,13.52,-90.1,13.74,-90.06,13.88,-89.72,14.13,-89.53,14.24,-89.59,14.36,-89.35,14.42]]]},{"n":"Guatemala","c":"GT","r":3,"ml":3,"lx":-90.5,"ly":14.98,"b":[-92.23,13.74,-88.23,17.82],"p":[[[-92.23,14.54,-92.2,14.83,-92.09,15.06,-92.23,15.25,-91.75,16.07,-90.46,16.07,-90.44,16.41,-90.6,16.47,-90.71,16.69,-91.08,16.92,-91.45,17.25,-91,17.25,-91,17.82,-90.07,17.82,-89.14,17.81,-89.15,17.02,-89.23,15.89,-88.93,15.89,-88.6,15.71,-88.52,15.86,-88.23,15.73,-88.68,15.35,-89.15,15.07,-89.23,14.87,-89.15,14.68,-89.35,14.42,-89.59,14.36,-89.53,14.24,-89.72,14.13,-90.06,13.88,-90.1,13.74,-90.61,13.91,-91.23,13.93,-91.69,14.13,-92.23,14.54]]]},{"n":"Belize","c":"BZ","r":6,"ml":5,"lx":-88.71,"ly":17.2,"b":[-89.23,15.89,-88.11,18.5],"p":[[[-89.14,17.81,-89.15,17.96,-89.03,18,-88.85,17.88,-88.49,18.49,-88.3,18.5,-88.3,18.35,-88.11,18.35,-88.12,18.08,-88.29,17.64,-88.2,17.49,-88.3,17.13,-88.24,17.04,-88.36,16.53,-88.55,16.27,-88.73,16.23,-88.93,15.89,-89.23,15.89,-89.15,17.02,-89.14,17.81]]]},{"n":"Venezuela","c":"VE","r":3,"ml":2.5,"lx":-64.6,"ly":7.18,"b":[-73.3,0.72,-59.76,12.16],"p":[[[-60.73,5.2,-60.6,4.92,-60.97,4.54,-62.09,4.16,-62.8,4.01,-63.09,3.77,-63.89,4.02,-64.63,4.15,-64.82,4.06,-64.37,3.8,-64.41,3.13,-64.27,2.5,-63.42,2.41,-63.37,2.2,-64.08,1.92,-64.2,1.49,-64.61,1.33,-65.35,1.1,-65.55,0.79,-66.33,0.72,-66.88,1.25,-67.18,2.25,-67.45,2.6,-67.81,2.82,-67.3,3.32,-67.34,3.54,-67.62,3.84,-67.82,4.5,-67.74,5.22,-67.52,5.56,-67.34,6.1,-67.7,6.27,-68.27,6.15,-68.99,6.21,-69.39,6.1,-70.09,6.96,-70.67,7.09,-71.96,6.99,-72.2,7.34,-72.44,7.42,-72.48,7.63,-72.36,8,-72.44,8.41,-72.66,8.63,-72.79,9.09,-73.3,9.15,-73.03,9.74,-72.91,10.45,-72.61,10.82,-72.23,11.11,-71.97,11.61,-71.33,11.78,-71.36,11.54,-71.95,11.42,-71.62,10.97,-71.63,10.45,-72.07,9.87,-71.7,9.07,-71.26,9.14,-71.04,9.86,-71.35,10.21,-71.4,10.97,-70.16,11.38,-70.29,11.85,-69.94,12.16,-69.58,11.46,-68.88,11.44,-68.23,10.89,-68.19,10.55,-67.3,10.55,-66.23,10.65,-65.66,10.2,-64.89,10.08,-64.33,10.39,-64.32,10.64,-63.08,10.7,-61.88,10.72,-62.73,10.42,-62.39,9.95,-61.59,9.87,-60.83,9.38,-60.67,8.58,-60.15,8.6,-59.76,8.37,-60.55,7.78,-60.64,7.42,-60.3,7.04,-60.54,6.86,-61.16,6.7,-61.14,6.23,-61.41,5.96,-60.73,5.2]]]},{"n":"Guyana","c":"GY","r":4,"ml":4,"lx":-58.94,"ly":5.12,"b":[-61.41,1.27,-56.54,8.37],"p":[[[-56.54,1.9,-56.78,1.86,-57.34,1.95,-57.66,1.68,-58.11,1.51,-58.43,1.46,-58.54,1.27,-59.03,1.32,-59.65,1.79,-59.72,2.25,-59.97,2.76,-59.82,3.61,-59.54,3.96,-59.77,4.42,-60.11,4.57,-59.98,5.01,-60.21,5.24,-60.73,5.2,-61.41,5.96,-61.14,6.23,-61.16,6.7,-60.54,6.86,-60.3,7.04,-60.64,7.42,-60.55,7.78,-59.76,8.37,-59.1,8,-58.48,7.35,-58.45,6.83,-58.08,6.81,-57.54,6.32,-57.15,5.97,-57.31,5.07,-57.91,4.81,-57.86,4.58,-58.04,4.06,-57.6,3.33,-57.28,3.33,-57.15,2.77,-56.54,1.9]]]},{"n":"Suriname","c":"SR","r":4,"ml":4,"lx":-55.91,"ly":4.14,"b":[-58.04,1.82,-53.96,6.03],"p":[[[-54.52,2.31,-55.1,2.52,-55.57,2.42,-55.97,2.51,-56.07,2.22,-55.91,2.02,-56,1.82,-56.54,1.9,-57.15,2.77,-57.28,3.33,-57.6,3.33,-58.04,4.06,-57.86,4.58,-57.91,4.81,-57.31,5.07,-57.15,5.97,-55.95,5.77,-55.84,5.95,-55.03,6.03,-53.96,5.76,-54.48,4.9,-54.4,4.21,-54.01,3.62,-54.18,3.19,-54.27,2.73,-54.52,2.31]]]},{"n":"France","c":"FR","r":2,"ml":1.7,"lx":2.55,"ly":46.7,"b":[-54.52,2.05,9.56,51.15],"p":[[[-51.66,4.16,-52.25,3.24,-52.56,2.5,-52.94,2.12,-53.42,2.05,-53.55,2.33,-53.78,2.38,-54.09,2.11,-54.52,2.31,-54.27,2.73,-54.18,3.19,-54.01,3.62,-54.4,4.21,-54.48,4.9,-53.96,5.76,-53.62,5.65,-52.88,5.41,-51.82,4.57,-51.66,4.16]],[[6.19,49.46,6.66,49.2,8.1,49.02,7.59,48.33,7.47,47.62,7.19,47.45,6.74,47.54,6.77,47.29,6.04,46.73,6.02,46.27,6.5,46.43,6.84,45.99,6.8,45.71,7.1,45.33,6.75,45.03,7.01,44.25,7.55,44.13,7.44,43.69,6.53,43.13,4.56,43.4,3.1,43.08,2.99,42.47,1.83,42.34,0.7,42.8,0.34,42.58,-1.5,43.03,-1.9,43.42,-1.38,44.02,-1.19,46.01,-2.23,47.06,-2.96,47.57,-4.49,47.95,-4.59,48.68,-3.3,48.9,-1.62,48.64,-1.93,49.78,-0.99,49.35,1.34,50.13,1.64,50.95,2.51,51.15,2.66,50.8,3.12,50.78,3.59,50.38,4.29,49.91,4.8,49.99,5.67,49.53,5.9,49.44,6.19,49.46]],[[8.75,42.63,9.39,43.01,9.56,42.15,9.23,41.38,8.78,41.58,8.54,42.26,8.75,42.63]]]},{"n":"Ecuador","c":"EC","r":3,"ml":3,"lx":-78.19,"ly":-1.26,"b":[-80.97,-4.96,-75.23,1.38],"p":[[[-75.37,-0.15,-75.23,-0.91,-75.54,-1.56,-76.64,-2.61,-77.84,-3,-78.45,-3.87,-78.64,-4.55,-79.21,-4.96,-79.62,-4.45,-80.03,-4.35,-80.44,-4.43,-80.47,-4.06,-80.18,-3.82,-80.3,-3.4,-79.77,-2.66,-79.99,-2.22,-80.37,-2.69,-80.97,-2.25,-80.76,-1.97,-80.93,-1.06,-80.58,-0.91,-80.4,-0.28,-80.02,0.36,-80.09,0.77,-79.54,0.98,-78.86,1.38,-77.86,0.81,-77.67,0.83,-77.42,0.4,-76.58,0.26,-76.29,0.42,-75.8,0.08,-75.37,-0.15]]]},{"n":"Puerto Rico","c":"PR","r":5,"ml":3,"lx":-66.48,"ly":18.23,"b":[-67.24,17.95,-65.59,18.52],"p":[[[-66.28,18.51,-65.77,18.43,-65.59,18.23,-65.85,17.98,-66.6,17.98,-67.18,17.95,-67.24,18.37,-67.1,18.52,-66.28,18.51]]]},{"n":"Jamaica","c":"JM","r":4,"ml":4,"lx":-77.32,"ly":18.14,"b":[-78.34,17.7,-76.2,18.52],"p":[[[-77.57,18.49,-76.9,18.4,-76.37,18.16,-76.2,17.89,-76.9,17.87,-77.21,17.7,-77.77,17.86,-78.34,18.23,-78.22,18.45,-77.8,18.52,-77.57,18.49]]]},{"n":"Cuba","c":"CU","r":3,"ml":2.7,"lx":-77.98,"ly":21.33,"b":[-84.97,19.86,-74.18,23.19],"p":[[[-82.27,23.19,-81.4,23.12,-80.62,23.11,-79.68,22.77,-79.28,22.4,-78.35,22.51,-77.99,22.28,-77.15,21.66,-76.52,21.21,-76.19,21.22,-75.6,21.02,-75.67,20.74,-74.93,20.69,-74.18,20.28,-74.3,20.05,-74.96,19.92,-75.63,19.87,-76.32,19.95,-77.76,19.86,-77.09,20.41,-77.49,20.67,-78.14,20.74,-78.48,21.03,-78.72,21.6,-79.28,21.56,-80.22,21.83,-80.52,22.04,-81.82,22.19,-82.17,22.39,-81.8,22.64,-82.78,22.69,-83.49,22.17,-83.91,22.15,-84.05,21.91,-84.55,21.8,-84.97,21.9,-84.45,22.2,-84.23,22.57,-83.78,22.79,-83.27,22.98,-82.51,23.08,-82.27,23.19]]]},{"n":"Zimbabwe","c":"ZW","r":3,"ml":2.5,"lx":29.93,"ly":-18.91,"b":[25.26,-22.27,32.85,-15.51],"p":[[[31.19,-22.25,30.66,-22.15,30.32,-22.27,29.84,-22.1,29.43,-22.09,28.79,-21.64,28.02,-21.49,27.73,-20.85,27.72,-20.5,27.3,-20.39,26.16,-19.29,25.85,-18.71,25.65,-18.54,25.26,-17.74,26.38,-17.85,26.71,-17.96,27.04,-17.94,27.6,-17.29,28.47,-16.47,28.83,-16.39,28.95,-16.04,29.52,-15.64,30.27,-15.51,30.34,-15.88,31.17,-15.86,31.64,-16.07,31.85,-16.32,32.33,-16.39,32.85,-16.71,32.85,-17.98,32.65,-18.67,32.61,-19.42,32.77,-19.72,32.66,-20.3,32.51,-20.4,32.24,-21.12,31.19,-22.25]]]},{"n":"Botswana","c":"BW","r":4,"ml":4,"lx":24.18,"ly":-22.1,"b":[19.9,-26.83,29.43,-17.66],"p":[[[29.43,-22.09,28.02,-22.83,27.12,-23.57,26.79,-24.24,26.49,-24.62,25.94,-24.7,25.77,-25.17,25.66,-25.49,25.03,-25.72,24.21,-25.67,23.73,-25.39,23.31,-25.27,22.82,-25.5,22.58,-25.98,22.11,-26.28,21.61,-26.73,20.89,-26.83,20.67,-26.48,20.76,-25.87,20.17,-24.92,19.9,-24.77,19.9,-21.85,20.88,-21.81,20.91,-18.25,21.66,-18.22,23.2,-17.87,23.58,-18.28,24.22,-17.89,24.52,-17.89,25.08,-17.66,25.26,-17.74,25.65,-18.54,25.85,-18.71,26.16,-19.29,27.3,-20.39,27.72,-20.5,27.73,-20.85,28.02,-21.49,28.79,-21.64,29.43,-22.09]]]},{"n":"Namibia","c":"NA","r":3,"ml":3,"lx":17.11,"ly":-20.58,"b":[11.73,-29.05,25.08,-16.94],"p":[[[19.9,-24.77,19.89,-28.46,19,-28.97,18.46,-29.05,17.84,-28.86,17.39,-28.78,17.22,-28.36,16.82,-28.08,16.34,-28.58,15.6,-27.82,15.21,-27.09,14.99,-26.12,14.74,-25.39,14.41,-23.85,14.39,-22.66,14.26,-22.11,13.87,-21.7,13.35,-20.87,12.83,-19.67,12.61,-19.05,11.79,-18.07,11.73,-17.3,12.22,-17.11,12.81,-16.94,13.46,-16.97,14.06,-17.42,14.21,-17.35,18.26,-17.31,18.96,-17.79,21.38,-17.93,23.22,-17.52,24.03,-17.3,24.68,-17.35,25.08,-17.58,25.08,-17.66,24.52,-17.89,24.22,-17.89,23.58,-18.28,23.2,-17.87,21.66,-18.22,20.91,-18.25,20.88,-21.81,19.9,-21.85,19.9,-24.77]]]},{"n":"Senegal","c":"SN","r":3,"ml":2.7,"lx":-14.78,"ly":15.14,"b":[-17.63,12.33,-11.47,16.6],"p":[[[-16.71,13.59,-17.13,14.37,-17.63,14.73,-17.19,14.92,-16.7,15.62,-16.46,16.14,-16.12,16.46,-15.62,16.37,-15.14,16.59,-14.58,16.6,-14.1,16.3,-13.44,16.04,-12.83,15.3,-12.17,14.62,-12.12,13.99,-11.93,13.42,-11.55,13.14,-11.47,12.75,-11.51,12.44,-11.66,12.39,-12.2,12.47,-12.28,12.35,-12.5,12.33,-13.22,12.58,-13.7,12.59,-15.55,12.63,-15.82,12.52,-16.15,12.55,-16.68,12.38,-16.84,13.15,-15.93,13.13,-15.69,13.27,-15.51,13.28,-15.14,13.51,-14.71,13.3,-14.28,13.28,-13.84,13.51,-14.05,13.79,-14.38,13.63,-14.69,13.63,-15.08,13.88,-15.4,13.86,-15.62,13.62,-16.71,13.59]]]},{"n":"Mali","c":"ML","r":3,"ml":3,"lx":-2.04,"ly":18.69,"b":[-12.17,10.1,4.27,24.97],"p":[[[-11.51,12.44,-11.47,12.75,-11.55,13.14,-11.93,13.42,-12.12,13.99,-12.17,14.62,-11.83,14.8,-11.67,15.39,-11.35,15.41,-10.65,15.13,-10.09,15.33,-9.7,15.26,-9.55,15.49,-5.54,15.5,-5.32,16.2,-5.49,16.33,-5.97,20.64,-6.45,24.96,-4.92,24.97,-1.55,22.79,1.82,20.61,2.06,20.14,2.68,19.86,3.15,19.69,3.16,19.06,4.27,19.16,4.27,16.85,3.72,16.18,3.64,15.57,2.75,15.41,1.39,15.32,1.02,14.97,0.37,14.93,-0.27,14.92,-0.52,15.12,-1.07,14.97,-2,14.56,-2.19,14.25,-2.97,13.8,-3.1,13.54,-3.52,13.34,-4.01,13.47,-4.28,13.23,-4.43,12.54,-5.22,11.71,-5.2,11.38,-5.47,10.95,-5.4,10.37,-5.82,10.22,-6.05,10.1,-6.21,10.52,-6.49,10.41,-6.67,10.43,-6.85,10.14,-7.62,10.15,-7.9,10.3,-8.03,10.21,-8.34,10.49,-8.28,10.79,-8.41,10.91,-8.62,10.81,-8.58,11.14,-8.38,11.39,-8.79,11.81,-8.91,12.09,-9.13,12.31,-9.33,12.33,-9.57,12.19,-9.89,12.06,-10.17,11.84,-10.59,11.92,-10.87,12.18,-11.04,12.21,-11.3,12.08,-11.46,12.08,-11.51,12.44]]]},{"n":"Mauritania","c":"MR","r":3,"ml":3,"lx":-9.74,"ly":19.59,"b":[-17.06,14.62,-4.92,27.4],"p":[[[-17.06,21,-16.85,21.33,-12.93,21.33,-13.12,22.77,-12.87,23.28,-11.94,23.37,-11.97,25.93,-8.69,25.88,-8.68,27.4,-4.92,24.97,-6.45,24.96,-5.97,20.64,-5.49,16.33,-5.32,16.2,-5.54,15.5,-9.55,15.49,-9.7,15.26,-10.09,15.33,-10.65,15.13,-11.35,15.41,-11.67,15.39,-11.83,14.8,-12.17,14.62,-12.83,15.3,-13.44,16.04,-14.1,16.3,-14.58,16.6,-15.14,16.59,-15.62,16.37,-16.12,16.46,-16.46,16.14,-16.55,16.67,-16.27,17.17,-16.15,18.11,-16.26,19.1,-16.38,19.59,-16.28,20.09,-16.54,20.57,-17.06,21]]]},{"n":"Benin","c":"BJ","r":5,"ml":4,"lx":2.35,"ly":10.32,"b":[0.77,6.14,3.8,12.24],"p":[[[2.69,6.26,1.87,6.14,1.62,6.83,1.66,9.13,1.46,9.33,1.43,9.83,1.08,10.18,0.77,10.47,0.9,11,1.24,11.11,1.45,11.55,1.94,11.64,2.15,11.94,2.49,12.23,2.85,12.24,3.61,11.66,3.57,11.33,3.8,10.73,3.6,10.33,3.71,10.06,3.22,9.44,2.91,9.14,2.72,8.51,2.75,7.87,2.69,6.26]]]},{"n":"Niger","c":"NE","r":3,"ml":3,"lx":9.5,"ly":17.45,"b":[0.3,11.66,15.9,23.47],"p":[[[14.85,22.86,15.1,21.31,15.47,21.05,15.49,20.73,15.9,20.39,15.69,19.96,15.3,17.93,15.25,16.63,13.97,15.68,13.54,14.37,13.96,14,13.95,13.35,14.6,13.33,14.5,12.86,14.21,12.8,14.18,12.48,14,12.46,13.32,13.56,13.08,13.6,12.3,13.04,11.53,13.33,10.99,13.39,10.7,13.25,10.11,13.28,9.52,12.85,9.01,12.83,7.8,13.34,7.33,13.1,6.82,13.12,6.45,13.49,5.44,13.87,4.37,13.75,4.11,13.53,3.97,12.96,3.68,12.55,3.61,11.66,2.85,12.24,2.49,12.23,2.15,11.94,2.18,12.63,1.02,12.85,0.99,13.34,0.43,13.99,0.3,14.44,0.37,14.93,1.02,14.97,1.39,15.32,2.75,15.41,3.64,15.57,3.72,16.18,4.27,16.85,4.27,19.16,5.68,19.6,8.57,21.57,12,23.47,13.58,23.04,14.14,22.49,14.85,22.86]]]},{"n":"Nigeria","c":"NG","r":2,"ml":1.7,"lx":7.5,"ly":9.44,"b":[2.69,4.24,14.58,13.87],"p":[[[2.69,6.26,2.75,7.87,2.72,8.51,2.91,9.14,3.22,9.44,3.71,10.06,3.6,10.33,3.8,10.73,3.57,11.33,3.61,11.66,3.68,12.55,3.97,12.96,4.11,13.53,4.37,13.75,5.44,13.87,6.45,13.49,6.82,13.12,7.33,13.1,7.8,13.34,9.01,12.83,9.52,12.85,10.11,13.28,10.7,13.25,10.99,13.39,11.53,13.33,12.3,13.04,13.08,13.6,13.32,13.56,14,12.46,14.18,12.48,14.58,12.09,14.47,11.9,14.42,11.57,13.57,10.8,13.31,10.16,13.17,9.64,12.96,9.42,12.75,8.72,12.22,8.31,12.06,7.8,11.84,7.4,11.75,6.98,11.06,6.64,10.5,7.06,10.12,7.04,9.52,6.45,9.23,6.44,8.76,5.48,8.5,4.77,7.46,4.41,7.08,4.46,6.7,4.24,5.9,4.26,5.36,4.89,5.03,5.61,4.33,6.27,3.57,6.26,2.69,6.26]]]},{"n":"Cameroon","c":"CM","r":3,"ml":3,"lx":12.47,"ly":4.59,"b":[8.49,1.73,16.01,12.86],"p":[[[14.5,12.86,14.89,12.22,14.96,11.56,14.92,10.89,15.47,9.98,14.91,9.99,14.63,9.92,14.17,10.02,13.95,9.55,14.54,8.97,14.98,8.8,15.12,8.38,15.44,7.69,15.28,7.42,14.78,6.41,14.54,6.23,14.46,5.45,14.56,5.03,14.48,4.73,14.95,4.21,15.04,3.85,15.41,3.34,15.86,3.01,15.91,2.56,16.01,2.27,15.94,1.73,15.15,1.96,14.34,2.23,13.08,2.27,12.95,2.32,12.36,2.19,11.75,2.33,11.28,2.26,9.65,2.28,9.8,3.07,9.4,3.73,8.95,3.9,8.74,4.35,8.49,4.5,8.5,4.77,8.76,5.48,9.23,6.44,9.52,6.45,10.12,7.04,10.5,7.06,11.06,6.64,11.75,6.98,11.84,7.4,12.06,7.8,12.22,8.31,12.75,8.72,12.96,9.42,13.17,9.64,13.31,10.16,13.57,10.8,14.42,11.57,14.47,11.9,14.58,12.09,14.18,12.48,14.21,12.8,14.5,12.86]]]},{"n":"Togo","c":"TG","r":6,"ml":5,"lx":1.06,"ly":8.81,"b":[-0.05,5.93,1.87,11.02],"p":[[[0.9,11,0.77,10.47,1.08,10.18,1.43,9.83,1.46,9.33,1.66,9.13,1.62,6.83,1.87,6.14,1.06,5.93,0.84,6.28,0.57,6.91,0.49,7.41,0.71,8.31,0.46,8.68,0.37,9.47,0.37,10.19,-0.05,10.71,0.02,11.02,0.9,11]]]},{"n":"Ghana","c":"GH","r":3,"ml":2.7,"lx":-1.04,"ly":7.72,"b":[-3.24,4.71,1.06,11.1],"p":[[[0.02,11.02,-0.05,10.71,0.37,10.19,0.37,9.47,0.46,8.68,0.71,8.31,0.49,7.41,0.57,6.91,0.84,6.28,1.06,5.93,-0.51,5.34,-1.06,5,-1.96,4.71,-2.86,4.99,-2.81,5.39,-3.24,6.25,-2.98,7.38,-2.56,8.22,-2.83,9.64,-2.96,10.4,-2.94,10.96,-1.2,11.01,-0.76,10.94,-0.44,11.1,0.02,11.02]]]},{"n":"Ivory Coast","c":"CI","r":3,"ml":2.5,"lx":-5.57,"ly":7.49,"b":[-8.6,4.34,-2.56,10.52],"p":[[[-8.03,10.21,-7.9,10.3,-7.62,10.15,-6.85,10.14,-6.67,10.43,-6.49,10.41,-6.21,10.52,-6.05,10.1,-5.82,10.22,-5.4,10.37,-4.95,10.15,-4.78,9.82,-4.33,9.61,-3.98,9.86,-3.51,9.9,-2.83,9.64,-2.56,8.22,-2.98,7.38,-3.24,6.25,-2.81,5.39,-2.86,4.99,-3.31,4.98,-4.01,5.18,-4.65,5.17,-5.83,4.99,-6.53,4.71,-7.52,4.34,-7.71,4.36,-7.64,5.19,-7.54,5.31,-7.57,5.71,-7.99,6.13,-8.31,6.19,-8.6,6.47,-8.39,6.91,-8.49,7.4,-8.44,7.69,-8.28,7.69,-8.22,8.12,-8.3,8.32,-8.2,8.46,-7.83,8.58,-8.08,9.38,-8.31,9.79,-8.23,10.13,-8.03,10.21]]]},{"n":"Guinea","c":"GN","r":3,"ml":3,"lx":-10.02,"ly":10.62,"b":[-15.13,7.31,-7.83,12.59],"p":[[[-13.7,12.59,-13.22,12.58,-12.5,12.33,-12.28,12.35,-12.2,12.47,-11.66,12.39,-11.51,12.44,-11.46,12.08,-11.3,12.08,-11.04,12.21,-10.87,12.18,-10.59,11.92,-10.17,11.84,-9.89,12.06,-9.57,12.19,-9.33,12.33,-9.13,12.31,-8.91,12.09,-8.79,11.81,-8.38,11.39,-8.58,11.14,-8.62,10.81,-8.41,10.91,-8.28,10.79,-8.34,10.49,-8.03,10.21,-8.23,10.13,-8.31,9.79,-8.08,9.38,-7.83,8.58,-8.2,8.46,-8.3,8.32,-8.22,8.12,-8.28,7.69,-8.44,7.69,-8.72,7.71,-8.93,7.31,-9.21,7.31,-9.4,7.53,-9.34,7.93,-9.76,8.54,-10.02,8.43,-10.23,8.41,-10.51,8.35,-10.49,8.72,-10.65,8.98,-10.62,9.27,-10.84,9.69,-11.12,10.05,-11.92,10.05,-12.15,9.86,-12.43,9.84,-12.6,9.62,-12.71,9.34,-13.25,8.9,-13.69,9.49,-14.07,9.89,-14.33,10.02,-14.58,10.21,-14.69,10.66,-14.84,10.88,-15.13,11.04,-14.69,11.53,-14.38,11.51,-14.12,11.68,-13.9,11.68,-13.74,11.81,-13.83,12.14,-13.72,12.25,-13.7,12.59]]]},{"n":"Guinea-Bissau","c":"GW","r":6,"ml":5,"lx":-14.52,"ly":12.16,"b":[-16.68,11.04,-13.7,12.63],"p":[[[-16.68,12.38,-16.15,12.55,-15.82,12.52,-15.55,12.63,-13.7,12.59,-13.72,12.25,-13.83,12.14,-13.74,11.81,-13.9,11.68,-14.12,11.68,-14.38,11.51,-14.69,11.53,-15.13,11.04,-15.66,11.46,-16.09,11.52,-16.31,11.81,-16.31,11.96,-16.61,12.17,-16.68,12.38]]]},{"n":"Liberia","c":"LR","r":4,"ml":4,"lx":-9.46,"ly":6.45,"b":[-11.44,4.36,-7.54,8.54],"p":[[[-8.44,7.69,-8.49,7.4,-8.39,6.91,-8.6,6.47,-8.31,6.19,-7.99,6.13,-7.57,5.71,-7.54,5.31,-7.64,5.19,-7.71,4.36,-7.97,4.36,-9,4.83,-9.91,5.59,-10.77,6.14,-11.44,6.79,-11.2,7.11,-11.15,7.4,-10.7,7.94,-10.23,8.41,-10.02,8.43,-9.76,8.54,-9.34,7.93,-9.4,7.53,-9.21,7.31,-8.93,7.31,-8.72,7.71,-8.44,7.69]]]},{"n":"Sierra Leone","c":"SL","r":4,"ml":4,"lx":-11.76,"ly":8.62,"b":[-13.25,6.79,-10.23,10.05],"p":[[[-13.25,8.9,-12.71,9.34,-12.6,9.62,-12.43,9.84,-12.15,9.86,-11.92,10.05,-11.12,10.05,-10.84,9.69,-10.62,9.27,-10.65,8.98,-10.49,8.72,-10.51,8.35,-10.23,8.41,-10.7,7.94,-11.15,7.4,-11.2,7.11,-11.44,6.79,-11.71,6.86,-12.43,7.26,-12.95,7.8,-13.12,8.16,-13.25,8.9]]]},{"n":"Burkina Faso","c":"BF","r":3,"ml":3,"lx":-1.36,"ly":12.67,"b":[-5.47,9.61,2.18,15.12],"p":[[[-5.4,10.37,-5.47,10.95,-5.2,11.38,-5.22,11.71,-4.43,12.54,-4.28,13.23,-4.01,13.47,-3.52,13.34,-3.1,13.54,-2.97,13.8,-2.19,14.25,-2,14.56,-1.07,14.97,-0.52,15.12,-0.27,14.92,0.37,14.93,0.3,14.44,0.43,13.99,0.99,13.34,1.02,12.85,2.18,12.63,2.15,11.94,1.94,11.64,1.45,11.55,1.24,11.11,0.9,11,0.02,11.02,-0.44,11.1,-0.76,10.94,-1.2,11.01,-2.94,10.96,-2.96,10.4,-2.83,9.64,-3.51,9.9,-3.98,9.86,-4.33,9.61,-4.78,9.82,-4.95,10.15,-5.4,10.37]]]},{"n":"Central African Republic","c":"CF","r":4,"ml":4,"lx":20.91,"ly":6.99,"b":[14.46,2.27,27.37,11.14],"p":[[[27.37,5.23,27.04,5.13,26.4,5.15,25.65,5.26,25.28,5.17,25.13,4.93,24.81,4.9,24.41,5.11,23.3,4.61,22.84,4.71,22.7,4.63,22.41,4.03,21.66,4.22,20.93,4.32,20.29,4.69,19.47,5.03,18.93,4.71,18.54,4.2,18.45,3.5,17.81,3.56,17.13,3.73,16.54,3.2,16.01,2.27,15.91,2.56,15.86,3.01,15.41,3.34,15.04,3.85,14.95,4.21,14.48,4.73,14.56,5.03,14.46,5.45,14.54,6.23,14.78,6.41,15.28,7.42,16.11,7.5,16.29,7.75,16.46,7.73,16.71,7.51,17.96,7.89,18.39,8.28,18.91,8.63,18.81,8.98,19.09,9.07,20.06,9.01,21,9.48,21.72,10.57,22.23,10.97,22.86,11.14,22.98,10.71,23.55,10.09,23.56,9.68,23.39,9.27,23.46,8.95,23.81,8.67,24.57,8.23,25.11,7.83,25.12,7.5,25.8,6.98,26.21,6.55,26.47,5.95,27.21,5.55,27.37,5.23]]]},{"n":"Republic of the Congo","c":"CG","r":4,"ml":4,"lx":15.9,"ly":0.14,"b":[11.09,-5.04,18.45,3.73],"p":[[[18.45,3.5,18.39,2.9,18.09,2.37,17.9,1.74,17.77,0.86,17.83,0.29,17.66,-0.06,17.64,-0.42,17.52,-0.74,16.87,-1.23,16.41,-1.74,15.97,-2.71,16.01,-3.54,15.75,-3.86,15.17,-4.34,14.58,-4.97,14.21,-4.79,14.14,-4.51,13.6,-4.5,13.26,-4.88,13,-4.78,12.62,-4.44,12.32,-4.61,11.91,-5.04,11.09,-3.98,11.86,-3.43,11.48,-2.77,11.82,-2.51,12.5,-2.39,12.58,-1.95,13.11,-2.43,13.99,-2.47,14.3,-2,14.43,-1.33,14.32,-0.55,13.84,0.04,14.28,1.2,14.03,1.4,13.28,1.31,13,1.83,13.08,2.27,14.34,2.23,15.15,1.96,15.94,1.73,16.01,2.27,16.54,3.2,17.13,3.73,17.81,3.56,18.45,3.5]]]},{"n":"Gabon","c":"GA","r":4,"ml":3,"lx":11.84,"ly":-0.44,"b":[8.8,-3.98,14.43,2.33],"p":[[[11.28,2.26,11.75,2.33,12.36,2.19,12.95,2.32,13.08,2.27,13,1.83,13.28,1.31,14.03,1.4,14.28,1.2,13.84,0.04,14.32,-0.55,14.43,-1.33,14.3,-2,13.99,-2.47,13.11,-2.43,12.58,-1.95,12.5,-2.39,11.82,-2.51,11.48,-2.77,11.86,-3.43,11.09,-3.98,10.07,-2.97,9.41,-2.14,8.8,-1.11,8.83,-0.78,9.05,-0.46,9.29,0.27,9.49,1.01,9.83,1.07,11.29,1.06,11.28,2.26]]]},{"n":"Equatorial Guinea","c":"GQ","r":4,"ml":4,"lx":8.99,"ly":2.33,"b":[9.31,1.01,11.29,2.28],"p":[[[9.65,2.28,11.28,2.26,11.29,1.06,9.83,1.07,9.49,1.01,9.31,1.16,9.65,2.28]]]},{"n":"Zambia","c":"ZM","r":3,"ml":3,"lx":26.4,"ly":-14.66,"b":[21.89,-17.96,33.49,-8.24],"p":[[[30.74,-8.34,31.16,-8.59,31.56,-8.76,32.19,-8.93,32.76,-9.23,33.23,-9.68,33.49,-10.53,33.32,-10.8,33.11,-11.61,33.31,-12.44,32.99,-12.78,32.69,-13.71,33.21,-13.97,30.18,-14.8,30.27,-15.51,29.52,-15.64,28.95,-16.04,28.83,-16.39,28.47,-16.47,27.6,-17.29,27.04,-17.94,26.71,-17.96,26.38,-17.85,25.26,-17.74,25.08,-17.66,25.08,-17.58,24.68,-17.35,24.03,-17.3,23.22,-17.52,22.56,-16.9,21.89,-16.08,21.93,-12.9,24.02,-12.91,23.93,-12.57,24.08,-12.19,23.9,-11.72,24.02,-11.24,23.91,-10.93,24.26,-10.95,24.31,-11.26,24.78,-11.24,25.42,-11.33,25.75,-11.78,26.55,-11.92,27.16,-11.61,27.39,-12.13,28.16,-12.27,28.52,-12.7,28.93,-13.25,29.7,-13.26,29.62,-12.18,29.34,-12.36,28.64,-11.97,28.37,-11.79,28.5,-10.79,28.67,-9.61,28.45,-9.16,28.73,-8.53,29,-8.41,30.35,-8.24,30.74,-8.34]]]},{"n":"Malawi","c":"MW","r":6,"ml":4,"lx":33.61,"ly":-13.39,"b":[32.69,-16.8,35.77,-9.23],"p":[[[32.76,-9.23,33.74,-9.42,33.94,-9.69,34.28,-10.16,34.56,-11.52,34.28,-12.28,34.56,-13.58,34.91,-13.57,35.27,-13.89,35.69,-14.61,35.77,-15.9,35.34,-16.11,35.03,-16.8,34.38,-16.18,34.31,-15.48,34.52,-15.01,34.46,-14.61,34.06,-14.36,33.79,-14.45,33.21,-13.97,32.69,-13.71,32.99,-12.78,33.31,-12.44,33.11,-11.61,33.32,-10.8,33.49,-10.53,33.23,-9.68,32.76,-9.23]]]},{"n":"Mozambique","c":"MZ","r":3,"ml":3,"lx":37.84,"ly":-13.94,"b":[30.18,-26.74,40.78,-10.32],"p":[[[34.56,-11.52,35.31,-11.44,36.51,-11.72,36.78,-11.59,37.47,-11.57,37.83,-11.27,38.43,-11.29,39.52,-10.9,40.32,-10.32,40.48,-10.77,40.44,-11.76,40.56,-12.64,40.6,-14.2,40.78,-14.69,40.48,-15.41,40.09,-16.1,39.45,-16.72,38.54,-17.1,37.41,-17.59,36.28,-18.66,35.9,-18.84,35.2,-19.55,34.79,-19.78,34.7,-20.5,35.18,-21.25,35.37,-21.84,35.39,-22.14,35.56,-22.09,35.53,-23.07,35.37,-23.54,35.61,-23.71,35.46,-24.12,35.04,-24.48,34.22,-24.82,33.01,-25.36,32.57,-25.73,32.66,-26.15,32.92,-26.22,32.83,-26.74,32.07,-26.73,31.99,-26.29,31.84,-25.84,31.75,-25.48,31.93,-24.37,31.67,-23.66,31.19,-22.25,32.24,-21.12,32.51,-20.4,32.66,-20.3,32.77,-19.72,32.61,-19.42,32.65,-18.67,32.85,-17.98,32.85,-16.71,32.33,-16.39,31.85,-16.32,31.64,-16.07,31.17,-15.86,30.34,-15.88,30.27,-15.51,30.18,-14.8,33.21,-13.97,33.79,-14.45,34.06,-14.36,34.46,-14.61,34.52,-15.01,34.31,-15.48,34.38,-16.18,35.03,-16.8,35.34,-16.11,35.77,-15.9,35.69,-14.61,35.27,-13.89,34.91,-13.57,34.56,-13.58,34.28,-12.28,34.56,-11.52]]]},{"n":"Eswatini","c":"SZ","r":4,"ml":4,"lx":31.47,"ly":-26.53,"b":[30.68,-27.29,32.07,-25.66],"p":[[[32.07,-26.73,31.87,-27.18,31.28,-27.29,30.69,-26.74,30.68,-26.4,30.95,-26.02,31.04,-25.73,31.33,-25.66,31.84,-25.84,31.99,-26.29,32.07,-26.73]]]},{"n":"Angola","c":"AO","r":3,"ml":3,"lx":17.98,"ly":-12.18,"b":[11.64,-17.93,24.08,-4.44],"p":[[[13,-4.78,12.63,-4.99,12.47,-5.25,12.44,-5.68,12.18,-5.79,11.91,-5.04,12.32,-4.61,12.62,-4.44,13,-4.78]],[[12.32,-6.1,12.74,-5.97,13.02,-5.98,13.38,-5.86,16.33,-5.88,16.57,-6.62,16.86,-7.22,17.09,-7.55,17.47,-8.07,18.13,-7.99,18.46,-7.85,19.02,-7.99,19.17,-7.74,19.42,-7.16,20.04,-7.12,20.09,-6.94,20.6,-6.94,20.51,-7.3,21.73,-7.29,21.75,-7.92,21.95,-8.31,21.8,-8.91,21.88,-9.52,22.21,-9.89,22.16,-11.08,22.4,-10.99,22.84,-11.02,23.46,-10.87,23.91,-10.93,24.02,-11.24,23.9,-11.72,24.08,-12.19,23.93,-12.57,24.02,-12.91,21.93,-12.9,21.89,-16.08,22.56,-16.9,23.22,-17.52,21.38,-17.93,18.96,-17.79,18.26,-17.31,14.21,-17.35,14.06,-17.42,13.46,-16.97,12.81,-16.94,12.22,-17.11,11.73,-17.3,11.64,-16.67,11.78,-15.79,12.12,-14.88,12.18,-14.45,12.5,-13.55,12.74,-13.14,13.31,-12.48,13.63,-12.04,13.74,-11.3,13.69,-10.73,13.39,-10.37,13.12,-9.77,12.88,-9.17,12.93,-8.96,13.24,-8.56,12.93,-7.6,12.73,-6.93,12.23,-6.29,12.32,-6.1]]]},{"n":"Burundi","c":"BI","r":6,"ml":4,"lx":29.92,"ly":-3.33,"b":[29.02,-4.5,30.75,-2.35],"p":[[[30.47,-2.41,30.53,-2.81,30.74,-3.03,30.75,-3.36,30.51,-3.57,30.12,-4.09,29.75,-4.45,29.34,-4.5,29.28,-3.29,29.02,-2.84,29.63,-2.92,29.94,-2.35,30.47,-2.41]]]},{"n":"Israel","c":"IL","r":4,"ml":3,"lx":34.85,"ly":30.91,"b":[34.27,29.5,35.84,33.28],"p":[[[35.72,32.71,35.55,32.39,35.18,32.53,34.97,31.87,35.23,31.75,34.97,31.62,34.93,31.35,35.4,31.49,35.42,31.1,34.92,29.5,34.82,29.76,34.27,31.22,34.56,31.55,34.49,31.61,34.75,32.07,34.96,32.83,35.1,33.08,35.13,33.09,35.46,33.09,35.55,33.26,35.82,33.28,35.84,32.87,35.7,32.72,35.72,32.71]]]},{"n":"Lebanon","c":"LB","r":5,"ml":4,"lx":35.99,"ly":34.13,"b":[35.13,33.09,36.61,34.64],"p":[[[35.82,33.28,35.55,33.26,35.46,33.09,35.13,33.09,35.48,33.91,35.98,34.61,36,34.64,36.45,34.59,36.61,34.2,36.07,33.82,35.82,33.28]]]},{"n":"Madagascar","c":"MG","r":3,"ml":2.7,"lx":46.7,"ly":-18.63,"b":[43.25,-25.6,50.48,-12.04],"p":[[[49.54,-12.47,49.81,-12.9,50.06,-13.56,50.22,-14.76,50.48,-15.23,50.38,-15.71,50.2,-16,49.86,-15.41,49.67,-15.71,49.86,-16.45,49.77,-16.88,49.5,-17.11,49.44,-17.95,49.04,-19.12,48.55,-20.5,47.93,-22.39,47.55,-23.78,47.1,-24.94,46.28,-25.18,45.41,-25.6,44.83,-25.35,44.04,-24.99,43.76,-24.46,43.7,-23.57,43.35,-22.78,43.25,-22.06,43.43,-21.34,43.89,-21.16,43.9,-20.83,44.37,-20.07,44.46,-19.44,44.23,-18.96,44.04,-18.33,43.96,-17.41,44.31,-16.85,44.45,-16.22,44.94,-16.18,45.5,-15.97,45.87,-15.79,46.31,-15.78,46.88,-15.21,47.71,-14.59,48.01,-14.09,47.87,-13.66,48.29,-13.78,48.85,-13.09,48.86,-12.49,49.19,-12.04,49.54,-12.47]]]},{"n":"Palestine","c":"PS","r":5,"ml":4.5,"lx":35.29,"ly":32.05,"b":[34.93,31.35,35.55,32.53],"p":[[[35.4,31.49,34.93,31.35,34.97,31.62,35.23,31.75,34.97,31.87,35.18,32.53,35.55,32.39,35.55,31.78,35.4,31.49]]]},{"n":"The Gambia","c":"GM","r":6,"ml":5,"lx":-15,"ly":13.64,"b":[-16.84,13.13,-13.84,13.88],"p":[[[-16.71,13.59,-15.62,13.62,-15.4,13.86,-15.08,13.88,-14.69,13.63,-14.38,13.63,-14.05,13.79,-13.84,13.51,-14.28,13.28,-14.71,13.3,-15.14,13.51,-15.51,13.28,-15.69,13.27,-15.93,13.13,-16.84,13.15,-16.71,13.59]]]},{"n":"Tunisia","c":"TN","r":3,"ml":3,"lx":9.01,"ly":33.69,"b":[7.52,30.31,11.49,37.35],"p":[[[9.48,30.31,9.06,32.1,8.44,32.51,8.43,32.75,7.61,33.34,7.52,34.1,8.14,34.66,8.38,35.48,8.22,36.43,8.42,36.95,9.51,37.35,10.21,37.23,10.18,36.72,11.03,37.09,11.1,36.9,10.6,36.41,10.59,35.95,10.94,35.7,10.81,34.83,10.15,34.33,10.34,33.79,10.86,33.77,11.11,33.29,11.49,33.14,11.43,32.37,10.94,32.08,10.64,31.76,9.95,31.38,10.06,30.96,9.97,30.54,9.48,30.31]]]},{"n":"Algeria","c":"DZ","r":3,"ml":2.5,"lx":2.81,"ly":27.4,"b":[-8.68,19.06,12,37.12],"p":[[[-8.68,27.4,-8.67,27.59,-8.67,27.66,-8.67,28.84,-7.06,29.58,-6.06,29.73,-5.24,30,-4.86,30.5,-3.69,30.9,-3.65,31.64,-3.07,31.72,-2.62,32.09,-1.31,32.26,-1.12,32.65,-1.39,32.86,-1.73,33.92,-1.79,34.53,-2.17,35.17,-1.21,35.71,-0.13,35.89,0.5,36.3,1.47,36.61,3.16,36.78,4.82,36.87,5.32,36.72,6.26,37.11,7.33,37.12,7.74,36.89,8.42,36.95,8.22,36.43,8.38,35.48,8.14,34.66,7.52,34.1,7.61,33.34,8.43,32.75,8.44,32.51,9.06,32.1,9.48,30.31,9.81,29.42,9.86,28.96,9.68,28.14,9.76,27.69,9.63,27.14,9.72,26.51,9.32,26.09,9.91,25.37,9.95,24.94,10.3,24.38,10.77,24.56,11.56,24.1,12,23.47,8.57,21.57,5.68,19.6,4.27,19.16,3.16,19.06,3.15,19.69,2.68,19.86,2.06,20.14,1.82,20.61,-1.55,22.79,-4.92,24.97,-8.68,27.4]]]},{"n":"Jordan","c":"JO","r":4,"ml":4,"lx":36.38,"ly":30.81,"b":[34.92,29.2,39.2,33.38],"p":[[[35.55,32.39,35.72,32.71,36.83,32.31,38.79,33.38,39.2,32.16,39,32.01,37,31.51,38,30.51,37.67,30.34,37.5,30,36.74,29.87,36.5,29.51,36.07,29.2,34.96,29.36,34.92,29.5,35.42,31.1,35.4,31.49,35.55,31.78,35.55,32.39]]]},{"n":"United Arab Emirates","c":"AE","r":4,"ml":4,"lx":54.55,"ly":23.47,"b":[51.58,22.5,56.4,26.06],"p":[[[51.58,24.25,51.76,24.29,51.79,24.02,52.58,24.18,53.4,24.15,54.01,24.12,54.69,24.8,55.44,25.44,56.07,26.06,56.26,25.71,56.4,24.92,55.89,24.92,55.8,24.27,55.98,24.13,55.53,23.93,55.53,23.52,55.23,23.11,55.21,22.71,55.01,22.5,52,23,51.62,24.01,51.58,24.25]]]},{"n":"Qatar","c":"QA","r":5,"ml":4,"lx":51.14,"ly":25.24,"b":[50.74,24.56,51.61,26.11],"p":[[[50.81,24.75,50.74,25.48,51.01,26.01,51.29,26.11,51.59,25.8,51.61,25.22,51.39,24.63,51.11,24.56,50.81,24.75]]]},{"n":"Kuwait","c":"KW","r":6,"ml":5,"lx":47.31,"ly":29.41,"b":[46.57,28.53,48.42,30.06],"p":[[[47.97,29.98,48.18,29.53,48.09,29.31,48.42,28.55,47.71,28.53,47.46,29,46.57,29.1,47.3,30.06,47.97,29.98]]]},{"n":"Iraq","c":"IQ","r":3,"ml":3,"lx":43.26,"ly":33.09,"b":[38.79,29.1,48.57,37.39],"p":[[[39.2,32.16,38.79,33.38,41.01,34.42,41.38,35.63,41.29,36.36,41.84,36.61,42.35,37.23,42.78,37.39,43.94,37.26,44.29,37,44.77,37.17,45.42,35.98,46.08,35.68,46.15,35.09,45.65,34.75,45.42,33.97,46.11,33.02,47.33,32.47,47.85,31.71,47.69,30.98,48,30.99,48.01,30.45,48.57,29.93,47.97,29.98,47.3,30.06,46.57,29.1,44.71,29.18,41.89,31.19,40.4,31.89,39.2,32.16]]]},{"n":"Oman","c":"OM","r":4,"ml":4,"lx":57.34,"ly":22.12,"b":[52,16.65,59.81,26.4],"p":[[[55.21,22.71,55.23,23.11,55.53,23.52,55.53,23.93,55.98,24.13,55.8,24.27,55.89,24.92,56.4,24.92,56.85,24.24,57.4,23.88,58.14,23.75,58.73,23.57,59.18,22.99,59.45,22.66,59.81,22.53,59.81,22.31,59.44,21.71,59.28,21.43,58.86,21.11,58.49,20.43,58.03,20.48,57.83,20.24,57.67,19.74,57.79,19.07,57.69,18.94,57.23,18.95,56.61,18.57,56.51,18.09,56.28,17.88,55.66,17.88,55.27,17.63,55.27,17.23,54.79,16.95,54.24,17.04,53.57,16.71,53.11,16.65,52.78,17.35,52,19,55,20,55.67,22,55.21,22.71]],[[56.26,25.71,56.07,26.06,56.36,26.4,56.49,26.31,56.39,25.9,56.26,25.71]]]},{"n":"Vanuatu","c":"VU","r":4,"ml":4,"lx":166.91,"ly":-15.37,"b":[166.63,-16.6,167.84,-14.63],"p":[[[167.22,-15.89,167.84,-16.47,167.52,-16.6,167.18,-16.16,167.22,-15.89]],[[166.79,-15.67,166.65,-15.39,166.63,-14.63,167.11,-14.93,167.27,-15.74,167,-15.61,166.79,-15.67]]]},{"n":"Cambodia","c":"KH","r":3,"ml":3,"lx":104.5,"ly":12.65,"b":[102.35,10.49,107.61,14.57],"p":[[[102.58,12.19,102.35,13.39,102.99,14.23,104.28,14.42,105.22,14.27,106.04,13.88,106.5,14.57,107.38,14.2,107.61,13.54,107.49,12.34,105.81,11.57,106.25,10.96,105.2,10.89,104.33,10.49,103.5,10.63,103.09,11.15,102.58,12.19]]]},{"n":"Thailand","c":"TH","r":3,"ml":2.7,"lx":101.07,"ly":15.46,"b":[97.38,5.69,105.59,20.42],"p":[[[105.22,14.27,104.28,14.42,102.99,14.23,102.35,13.39,102.58,12.19,101.69,12.65,100.83,12.63,100.98,13.41,100.1,13.41,100.02,12.31,99.48,10.85,99.15,9.96,99.22,9.24,99.87,9.21,100.28,8.3,100.46,7.43,101.02,6.86,101.62,6.74,102.14,6.22,101.81,5.81,101.15,5.69,101.08,6.2,100.26,6.64,100.09,6.46,99.69,6.85,99.52,7.34,98.99,7.91,98.5,8.38,98.34,7.79,98.15,8.35,98.26,8.97,98.55,9.93,99.04,10.96,99.59,11.89,99.2,12.8,99.21,13.27,99.1,13.83,98.43,14.62,98.19,15.12,98.54,15.31,98.9,16.18,98.49,16.84,97.86,17.57,97.38,18.45,97.8,18.63,98.25,19.71,98.96,19.75,99.54,20.19,100.12,20.42,100.55,20.11,100.61,19.51,101.28,19.46,101.04,18.41,101.06,17.51,102.11,18.11,102.41,17.93,103,17.96,103.2,18.31,103.96,18.24,104.72,17.43,104.78,16.44,105.59,15.57,105.54,14.72,105.22,14.27]]]},{"n":"Laos","c":"LA","r":4,"ml":4,"lx":102.53,"ly":19.43,"b":[100.12,13.88,107.56,22.46],"p":[[[107.38,14.2,106.5,14.57,106.04,13.88,105.22,14.27,105.54,14.72,105.59,15.57,104.78,16.44,104.72,17.43,103.96,18.24,103.2,18.31,103,17.96,102.41,17.93,102.11,18.11,101.06,17.51,101.04,18.41,101.28,19.46,100.61,19.51,100.55,20.11,100.12,20.42,100.33,20.79,101.18,21.44,101.27,21.2,101.8,21.17,101.65,22.32,102.17,22.46,102.75,21.68,103.2,20.77,104.44,20.76,104.82,19.89,104.18,19.62,103.9,19.27,105.09,18.67,105.93,17.49,106.56,16.6,107.31,15.91,107.56,15.2,107.38,14.2]]]},{"n":"Myanmar","c":"MM","r":3,"ml":3,"lx":95.8,"ly":21.57,"b":[92.3,9.93,101.18,28.34],"p":[[[100.12,20.42,99.54,20.19,98.96,19.75,98.25,19.71,97.8,18.63,97.38,18.45,97.86,17.57,98.49,16.84,98.9,16.18,98.54,15.31,98.19,15.12,98.43,14.62,99.1,13.83,99.21,13.27,99.2,12.8,99.59,11.89,99.04,10.96,98.55,9.93,98.46,10.68,98.76,11.44,98.43,12.03,98.51,13.12,98.1,13.64,97.78,14.84,97.6,16.1,97.16,16.93,96.51,16.43,95.37,15.71,94.81,15.8,94.19,16.04,94.53,17.28,94.32,18.21,93.54,19.37,93.66,19.73,93.08,19.86,92.37,20.67,92.3,21.48,92.65,21.32,92.67,22.04,93.17,22.28,93.06,22.7,93.29,23.04,93.33,24.08,94.11,23.85,94.55,24.68,94.6,25.16,95.16,26,95.12,26.57,96.42,27.26,97.13,27.08,97.05,27.7,97.4,27.88,97.33,28.26,97.91,28.34,98.25,27.75,98.68,27.51,98.71,26.74,98.67,25.92,97.72,25.08,97.6,23.9,98.66,24.06,98.9,23.14,99.53,22.95,99.24,22.12,99.98,21.74,100.42,21.56,101.15,21.85,101.18,21.44,100.33,20.79,100.12,20.42]]]},{"n":"Vietnam","c":"VN","r":2,"ml":2,"lx":105.39,"ly":21.72,"b":[102.17,8.6,109.34,23.35],"p":[[[104.33,10.49,105.2,10.89,106.25,10.96,105.81,11.57,107.49,12.34,107.61,13.54,107.38,14.2,107.56,15.2,107.31,15.91,106.56,16.6,105.93,17.49,105.09,18.67,103.9,19.27,104.18,19.62,104.82,19.89,104.44,20.76,103.2,20.77,102.75,21.68,102.17,22.46,102.71,22.71,103.5,22.7,104.48,22.82,105.33,23.35,105.81,22.98,106.73,22.79,106.57,22.22,107.04,21.81,108.05,21.55,106.72,20.7,105.88,19.75,105.66,19.06,106.43,18,107.36,16.7,108.27,16.08,108.88,15.28,109.34,13.43,109.2,11.67,108.37,11.01,107.22,10.36,106.41,9.53,105.16,8.6,104.8,9.24,105.08,9.92,104.33,10.49]]]},{"n":"North Korea","c":"KP","r":3,"ml":3,"lx":126.44,"ly":39.89,"b":[124.27,37.67,130.78,42.99],"p":[[[130.64,42.4,130.78,42.22,130.4,42.28,129.97,41.94,129.67,41.6,129.71,40.88,129.19,40.66,129.01,40.49,128.63,40.19,127.97,40.03,127.53,39.76,127.5,39.32,127.39,39.21,127.78,39.05,128.35,38.61,128.21,38.37,127.78,38.3,127.07,38.26,126.68,37.8,126.24,37.84,126.17,37.75,125.69,37.94,125.57,37.75,125.28,37.67,125.24,37.86,124.98,37.95,124.71,38.11,124.99,38.55,125.22,38.67,125.13,38.85,125.39,39.39,125.32,39.55,124.74,39.66,124.27,39.93,125.08,40.57,126.18,41.11,126.87,41.82,127.34,41.5,128.21,41.47,128.05,41.99,129.6,42.42,129.99,42.99,130.64,42.4]]]},{"n":"South Korea","c":"KR","r":2,"ml":2.5,"lx":128.13,"ly":36.38,"b":[126.12,34.39,129.47,38.61],"p":[[[126.17,37.75,126.24,37.84,126.68,37.8,127.07,38.26,127.78,38.3,128.21,38.37,128.35,38.61,129.21,37.43,129.46,36.78,129.47,35.63,129.09,35.08,128.19,34.89,127.39,34.48,126.49,34.39,126.37,34.93,126.56,35.68,126.12,36.73,126.86,36.89,126.17,37.75]]]},{"n":"Mongolia","c":"MN","r":3,"ml":3,"lx":104.15,"ly":46,"b":[87.75,41.6,119.77,52.05],"p":[[[87.75,49.3,88.81,49.47,90.71,50.33,92.23,50.8,93.1,50.5,94.15,50.48,94.82,50.01,95.81,49.98,97.26,49.73,98.23,50.42,97.83,51.01,98.86,52.05,99.98,51.63,100.89,51.52,102.07,51.26,102.26,50.51,103.68,50.09,104.62,50.28,105.89,50.41,106.89,50.27,107.87,49.79,108.48,49.28,109.4,49.29,110.66,49.13,111.58,49.38,112.9,49.54,114.36,50.25,114.96,50.14,115.49,49.81,116.68,49.89,116.19,49.13,115.49,48.14,115.74,47.73,116.31,47.85,117.3,47.7,118.06,48.07,118.87,47.75,119.77,47.05,119.66,46.69,118.87,46.81,117.42,46.67,116.72,46.39,115.99,45.73,114.46,45.34,113.46,44.81,112.44,45.01,111.87,45.1,111.35,44.46,111.67,44.07,111.83,43.74,111.13,43.41,110.41,42.87,109.24,42.52,107.74,42.48,106.13,42.13,104.96,41.6,104.52,41.91,103.31,41.91,101.83,42.51,100.85,42.66,99.52,42.52,97.45,42.75,96.35,42.73,95.76,43.32,95.31,44.24,94.69,44.35,93.48,44.98,92.13,45.12,90.95,45.29,90.59,45.72,90.97,46.89,90.28,47.69,88.85,48.07,88.01,48.6,87.75,49.3]]]},{"n":"India","c":"IN","r":2,"ml":1.7,"lx":79.36,"ly":22.69,"b":[68.18,7.97,97.4,35.49],"p":[[[97.33,28.26,97.4,27.88,97.05,27.7,97.13,27.08,96.42,27.26,95.12,26.57,95.16,26,94.6,25.16,94.55,24.68,94.11,23.85,93.33,24.08,93.29,23.04,93.06,22.7,93.17,22.28,92.67,22.04,92.15,23.63,91.87,23.62,91.71,22.99,91.16,23.5,91.47,24.07,91.92,24.13,92.38,24.98,91.8,25.15,90.87,25.13,89.92,25.27,89.83,25.97,89.36,26.01,88.56,26.45,88.21,25.77,88.93,25.24,88.31,24.87,88.08,24.5,88.7,24.23,88.53,23.63,88.88,22.88,89.03,22.06,88.89,21.69,88.21,21.7,86.98,21.5,87.03,20.74,86.5,20.15,85.06,19.48,83.94,18.3,83.19,17.67,82.19,17.02,82.19,16.56,81.69,16.31,80.79,15.95,80.32,15.9,80.03,15.14,80.23,13.84,80.29,13.01,79.86,12.06,79.86,10.36,79.34,10.31,78.89,9.55,79.19,9.22,78.28,8.93,77.94,8.25,77.54,7.97,76.59,8.9,76.13,10.3,75.75,11.31,75.4,11.78,74.86,12.74,74.62,13.99,74.44,14.62,73.53,15.99,73.12,17.93,72.82,19.21,72.82,20.42,72.63,21.36,71.18,20.76,70.47,20.88,69.16,22.09,69.64,22.45,69.35,22.84,68.18,23.69,68.84,24.36,71.04,24.36,70.84,25.22,70.28,25.72,70.17,26.49,69.51,26.94,70.62,27.99,71.78,27.91,72.82,28.96,73.45,29.98,74.42,30.98,74.41,31.69,75.26,32.27,74.45,32.76,74.1,33.44,73.75,34.32,74.24,34.75,75.76,34.5,76.87,34.65,77.84,35.49,78.91,34.32,78.81,33.51,79.21,32.99,79.18,32.48,78.46,32.62,78.74,31.52,79.72,30.88,81.11,30.18,80.48,29.73,80.09,28.79,81.06,28.42,82,27.93,83.3,27.36,84.68,27.23,85.25,26.73,86.02,26.63,87.23,26.4,88.06,26.41,88.17,26.81,88.04,27.45,88.12,27.88,88.73,28.09,88.81,27.3,88.84,27.1,89.74,26.72,90.37,26.88,91.22,26.81,92.03,26.84,92.1,27.45,91.7,27.77,92.5,27.9,93.41,28.64,94.57,29.28,95.4,29.03,96.12,29.45,96.59,28.83,96.25,28.41,97.33,28.26]]]},{"n":"Bangladesh","c":"BD","r":3,"ml":3,"lx":89.68,"ly":24.21,"b":[88.08,20.67,92.67,26.45],"p":[[[92.67,22.04,92.65,21.32,92.3,21.48,92.37,20.67,92.08,21.19,92.03,21.7,91.83,22.18,91.42,22.77,90.5,22.81,90.59,22.39,90.27,21.84,89.85,22.04,89.7,21.86,89.42,21.97,89.03,22.06,88.88,22.88,88.53,23.63,88.7,24.23,88.08,24.5,88.31,24.87,88.93,25.24,88.21,25.77,88.56,26.45,89.36,26.01,89.83,25.97,89.92,25.27,90.87,25.13,91.8,25.15,92.38,24.98,91.92,24.13,91.47,24.07,91.16,23.5,91.71,22.99,91.87,23.62,92.15,23.63,92.67,22.04]]]},{"n":"Bhutan","c":"BT","r":5,"ml":4,"lx":90.04,"ly":27.54,"b":[88.81,26.72,92.1,28.3],"p":[[[91.7,27.77,92.1,27.45,92.03,26.84,91.22,26.81,90.37,26.88,89.74,26.72,88.84,27.1,88.81,27.3,89.48,28.04,90.02,28.3,90.73,28.06,91.26,28.04,91.7,27.77]]]},{"n":"Nepal","c":"NP","r":3,"ml":3,"lx":83.64,"ly":28.3,"b":[80.09,26.4,88.17,30.42],"p":[[[88.12,27.88,88.04,27.45,88.17,26.81,88.06,26.41,87.23,26.4,86.02,26.63,85.25,26.73,84.68,27.23,83.3,27.36,82,27.93,81.06,28.42,80.09,28.79,80.48,29.73,81.11,30.18,81.53,30.42,82.33,30.12,83.34,29.46,83.9,29.32,84.23,28.84,85.01,28.64,85.82,28.2,86.95,27.97,88.12,27.88]]]},{"n":"Pakistan","c":"PK","r":2,"ml":2.7,"lx":68.55,"ly":29.33,"b":[60.87,23.69,77.84,37.13],"p":[[[77.84,35.49,76.87,34.65,75.76,34.5,74.24,34.75,73.75,34.32,74.1,33.44,74.45,32.76,75.26,32.27,74.41,31.69,74.42,30.98,73.45,29.98,72.82,28.96,71.78,27.91,70.62,27.99,69.51,26.94,70.17,26.49,70.28,25.72,70.84,25.22,71.04,24.36,68.84,24.36,68.18,23.69,67.44,23.94,67.15,24.66,66.37,25.43,64.53,25.24,62.91,25.22,61.5,25.08,61.87,26.24,63.32,26.76,63.23,27.22,62.76,27.38,62.73,28.26,61.77,28.7,61.37,29.3,60.87,29.83,62.55,29.32,63.55,29.47,64.15,29.34,64.35,29.56,65.05,29.47,66.35,29.89,66.38,30.74,66.94,31.3,67.68,31.3,67.79,31.58,68.56,31.71,68.93,31.62,69.32,31.9,69.26,32.5,69.69,33.11,70.32,33.36,69.93,34.02,70.88,33.99,71.16,34.35,71.12,34.73,71.61,35.15,71.5,35.65,71.26,36.07,71.85,36.51,72.92,36.72,74.07,36.84,74.58,37.02,75.16,37.13,75.9,36.67,76.19,35.9,77.84,35.49]]]},{"n":"Afghanistan","c":"AF","r":3,"ml":3,"lx":66.5,"ly":34.16,"b":[60.53,29.32,75.16,38.49],"p":[[[66.52,37.36,67.08,37.36,67.83,37.14,68.14,37.02,68.86,37.34,69.2,37.15,69.52,37.61,70.12,37.59,70.27,37.74,70.38,38.14,70.81,38.49,71.35,38.26,71.24,37.95,71.54,37.91,71.45,37.07,71.84,36.74,72.19,36.95,72.64,37.05,73.26,37.5,73.95,37.42,74.98,37.42,75.16,37.13,74.58,37.02,74.07,36.84,72.92,36.72,71.85,36.51,71.26,36.07,71.5,35.65,71.61,35.15,71.12,34.73,71.16,34.35,70.88,33.99,69.93,34.02,70.32,33.36,69.69,33.11,69.26,32.5,69.32,31.9,68.93,31.62,68.56,31.71,67.79,31.58,67.68,31.3,66.94,31.3,66.38,30.74,66.35,29.89,65.05,29.47,64.35,29.56,64.15,29.34,63.55,29.47,62.55,29.32,60.87,29.83,61.78,30.74,61.7,31.38,60.94,31.55,60.86,32.18,60.54,32.98,60.96,33.53,60.53,33.68,60.8,34.4,61.21,35.65,62.23,35.27,62.98,35.4,63.19,35.86,63.98,36.01,64.55,36.31,64.75,37.11,65.59,37.31,65.75,37.66,66.22,37.39,66.52,37.36]]]},{"n":"Tajikistan","c":"TJ","r":4,"ml":4,"lx":72.59,"ly":38.2,"b":[67.44,36.74,74.98,40.96],"p":[[[67.83,37.14,68.39,38.16,68.18,38.9,67.44,39.14,67.7,39.58,68.54,39.53,69.01,40.09,69.33,40.73,70.67,40.96,70.46,40.5,70.6,40.22,71.01,40.24,70.65,39.94,69.56,40.1,69.46,39.53,70.55,39.6,71.78,39.28,73.68,39.43,73.93,38.51,74.26,38.61,74.86,38.38,74.83,37.99,74.98,37.42,73.95,37.42,73.26,37.5,72.64,37.05,72.19,36.95,71.84,36.74,71.45,37.07,71.54,37.91,71.24,37.95,71.35,38.26,70.81,38.49,70.38,38.14,70.27,37.74,70.12,37.59,69.52,37.61,69.2,37.15,68.86,37.34,68.14,37.02,67.83,37.14]]]},{"n":"Kyrgyzstan","c":"KG","r":4,"ml":3,"lx":74.53,"ly":41.67,"b":[69.46,39.28,80.26,43.3],"p":[[[70.96,42.27,71.19,42.7,71.84,42.85,73.49,42.5,73.65,43.09,74.21,43.3,75.64,42.88,76,42.99,77.66,42.96,79.14,42.86,79.64,42.5,80.26,42.35,80.12,42.12,78.54,41.58,78.19,41.19,76.9,41.07,76.53,40.43,75.47,40.56,74.78,40.37,73.82,39.89,73.96,39.66,73.68,39.43,71.78,39.28,70.55,39.6,69.46,39.53,69.56,40.1,70.65,39.94,71.01,40.24,71.77,40.15,73.06,40.87,71.87,41.39,71.16,41.14,70.42,41.52,71.26,42.17,70.96,42.27]]]},{"n":"Turkmenistan","c":"TM","r":4,"ml":3,"lx":58.68,"ly":39.86,"b":[52.5,35.27,66.55,42.75],"p":[[[52.5,41.78,52.94,42.12,54.08,42.32,54.76,42.04,55.46,41.26,55.97,41.31,57.1,41.32,56.93,41.83,57.79,42.17,58.63,42.75,59.98,42.22,60.08,41.43,60.47,41.22,61.55,41.27,61.88,41.08,62.37,40.05,63.52,39.36,64.17,38.89,65.22,38.4,66.55,37.97,66.52,37.36,66.22,37.39,65.75,37.66,65.59,37.31,64.75,37.11,64.55,36.31,63.98,36.01,63.19,35.86,62.98,35.4,62.23,35.27,61.21,35.65,61.12,36.49,60.38,36.53,59.23,37.41,58.44,37.52,57.33,38.03,56.62,38.12,56.18,37.94,55.51,37.96,54.8,37.39,53.92,37.2,53.74,37.91,53.88,38.95,53.1,39.29,53.36,39.98,52.69,40.03,52.92,40.88,53.86,40.63,54.74,40.95,54.01,41.55,53.72,42.12,52.92,41.87,52.81,41.14,52.5,41.78]]]},{"n":"Iran","c":"IR","r":2,"ml":2.5,"lx":54.93,"ly":32.17,"b":[44.11,25.08,63.32,39.71],"p":[[[48.57,29.93,48.01,30.45,48,30.99,47.69,30.98,47.85,31.71,47.33,32.47,46.11,33.02,45.42,33.97,45.65,34.75,46.15,35.09,46.08,35.68,45.42,35.98,44.77,37.17,44.23,37.97,44.42,38.28,44.11,39.43,44.79,39.71,44.95,39.34,45.46,38.87,46.14,38.74,46.51,38.77,47.69,39.51,48.06,39.58,48.36,39.29,48.01,38.79,48.63,38.27,48.88,38.32,49.2,37.58,50.15,37.37,50.84,36.87,52.26,36.7,53.83,36.97,53.92,37.2,54.8,37.39,55.51,37.96,56.18,37.94,56.62,38.12,57.33,38.03,58.44,37.52,59.23,37.41,60.38,36.53,61.12,36.49,61.21,35.65,60.8,34.4,60.53,33.68,60.96,33.53,60.54,32.98,60.86,32.18,60.94,31.55,61.7,31.38,61.78,30.74,60.87,29.83,61.37,29.3,61.77,28.7,62.73,28.26,62.76,27.38,63.23,27.22,63.32,26.76,61.87,26.24,61.5,25.08,59.62,25.38,58.53,25.61,57.4,25.74,56.97,26.97,56.49,27.14,55.72,26.96,54.72,26.48,53.49,26.81,52.48,27.58,51.52,27.87,50.85,28.81,50.12,30.15,49.58,29.99,48.94,30.32,48.57,29.93]]]},{"n":"Syria","c":"SY","r":3,"ml":3,"lx":38.28,"ly":35.01,"b":[35.7,32.31,42.35,37.23],"p":[[[35.72,32.71,35.7,32.72,35.84,32.87,35.82,33.28,36.07,33.82,36.61,34.2,36.45,34.59,36,34.64,35.91,35.41,36.15,35.82,36.42,36.04,36.69,36.26,36.74,36.82,37.07,36.62,38.17,36.9,38.7,36.71,39.52,36.72,40.67,37.09,41.21,37.07,42.35,37.23,41.84,36.61,41.29,36.36,41.38,35.63,41.01,34.42,38.79,33.38,36.83,32.31,35.72,32.71]]]},{"n":"Armenia","c":"AM","r":6,"ml":5,"lx":44.8,"ly":40.46,"b":[43.58,38.74,46.51,41.25],"p":[[[46.51,38.77,46.14,38.74,45.74,39.32,45.74,39.47,45.3,39.47,45,39.74,44.79,39.71,44.4,40.01,43.66,40.25,43.75,40.74,43.58,41.09,44.97,41.25,45.18,40.99,45.56,40.81,45.36,40.56,45.89,40.22,45.61,39.9,46.03,39.63,46.48,39.46,46.51,38.77]]]},{"n":"Sweden","c":"SE","r":3,"ml":2,"lx":19.02,"ly":65.86,"b":[11.03,55.36,23.9,69.11],"p":[[[11.03,58.86,11.47,59.43,12.3,60.12,12.63,61.29,11.99,61.8,11.93,63.13,12.58,64.07,13.57,64.05,13.92,64.45,13.56,64.79,15.11,66.19,16.11,67.3,16.77,68.01,17.73,68.01,17.99,68.57,19.88,68.41,20.03,69.07,20.65,69.11,21.98,68.62,23.54,67.94,23.57,66.4,23.9,66.01,22.18,65.72,21.21,65.03,21.37,64.41,19.78,63.61,17.85,62.75,17.12,61.34,17.83,60.64,18.79,60.08,17.87,58.95,16.83,58.72,16.45,57.04,15.88,56.1,14.67,56.2,14.1,55.41,12.94,55.36,12.63,56.31,11.79,57.44,11.03,58.86]]]},{"n":"Belarus","c":"BY","r":4,"ml":3,"lx":28.42,"ly":53.82,"b":[23.2,51.32,32.69,56.17],"p":[[[28.18,56.17,29.23,55.92,29.37,55.67,29.9,55.79,30.87,55.55,30.97,55.08,30.76,54.81,31.38,54.16,31.79,53.97,31.73,53.79,32.41,53.62,32.69,53.35,32.3,53.13,31.5,53.17,31.31,53.07,31.54,52.74,31.79,52.1,30.93,52.04,30.62,51.82,30.56,51.32,30.16,51.42,29.25,51.37,28.99,51.6,28.62,51.43,28.24,51.57,27.45,51.59,26.34,51.83,25.33,51.91,24.55,51.89,24.01,51.62,23.53,51.58,23.51,52.02,23.2,52.49,23.8,52.69,23.8,53.09,23.53,53.47,23.48,53.91,24.45,53.91,25.54,54.28,25.77,54.85,26.59,55.17,26.49,55.62,27.1,55.78,28.18,56.17]]]},{"n":"Ukraine","c":"UA","r":3,"ml":2.7,"lx":32.14,"ly":49.72,"b":[22.09,45.29,40.08,52.34],"p":[[[31.79,52.1,32.16,52.06,32.41,52.29,32.72,52.24,33.75,52.34,34.39,51.77,34.14,51.57,34.22,51.26,35.02,51.21,35.38,50.77,35.36,50.58,36.63,50.23,37.39,50.38,38.01,49.92,38.59,49.93,40.07,49.6,40.08,49.31,39.67,48.78,39.9,48.23,39.74,47.9,38.77,47.83,38.26,47.55,38.22,47.1,37.43,47.02,36.76,46.7,35.82,46.65,34.96,46.27,35.01,45.74,34.86,45.77,34.73,45.97,34.41,46.01,33.7,46.22,33.44,45.97,33.3,46.08,31.74,46.33,31.68,46.71,30.75,46.58,30.38,46.03,29.6,45.29,29.15,45.46,28.68,45.3,28.23,45.49,28.49,45.6,28.66,45.94,28.93,46.26,28.86,46.44,29.07,46.52,29.17,46.38,29.76,46.35,30.02,46.42,29.84,46.53,29.91,46.67,29.56,46.93,29.42,47.35,29.05,47.51,29.12,47.85,28.67,48.12,28.26,48.16,27.52,48.47,26.86,48.37,26.62,48.22,26.2,48.22,25.95,47.99,25.21,47.89,24.87,47.74,24.4,47.98,23.76,47.99,23.14,48.1,22.71,47.88,22.64,48.15,22.09,48.42,22.28,48.83,22.56,49.09,22.78,49.03,22.52,49.48,23.43,50.31,23.92,50.42,24.03,50.71,23.53,51.58,24.01,51.62,24.55,51.89,25.33,51.91,26.34,51.83,27.45,51.59,28.24,51.57,28.62,51.43,28.99,51.6,29.25,51.37,30.16,51.42,30.56,51.32,30.62,51.82,30.93,52.04,31.79,52.1]]]},{"n":"Poland","c":"PL","r":3,"ml":2.5,"lx":19.49,"ly":51.99,"b":[14.07,49.03,24.03,54.85],"p":[[[23.48,53.91,23.53,53.47,23.8,53.09,23.8,52.69,23.2,52.49,23.51,52.02,23.53,51.58,24.03,50.71,23.92,50.42,23.43,50.31,22.52,49.48,22.78,49.03,22.56,49.09,21.61,49.47,20.89,49.33,20.42,49.43,19.83,49.22,19.32,49.57,18.91,49.44,18.85,49.5,18.39,49.99,17.65,50.05,17.55,50.36,16.87,50.47,16.72,50.22,16.18,50.42,16.24,50.7,15.49,50.78,15.02,51.11,14.61,51.75,14.69,52.09,14.44,52.62,14.07,52.98,14.35,53.25,14.12,53.76,14.8,54.05,16.36,54.51,17.62,54.85,18.62,54.68,18.7,54.44,19.66,54.43,20.89,54.31,22.73,54.33,23.24,54.22,23.48,53.91]]]},{"n":"Austria","c":"AT","r":4,"ml":3,"lx":14.13,"ly":47.52,"b":[9.48,46.43,16.98,49.04],"p":[[[16.98,48.12,16.9,47.71,16.34,47.71,16.53,47.5,16.2,46.85,16.01,46.68,15.14,46.66,14.63,46.43,13.81,46.51,12.38,46.77,12.15,47.12,11.16,46.94,11.05,46.75,10.44,46.89,9.93,46.92,9.48,47.1,9.63,47.35,9.59,47.53,9.9,47.58,10.4,47.3,10.54,47.57,11.43,47.52,12.14,47.7,12.62,47.67,12.93,47.47,13.03,47.64,12.88,48.29,13.24,48.42,13.6,48.88,14.34,48.56,14.9,48.96,15.25,49.04,16.03,48.73,16.5,48.79,16.96,48.6,16.88,48.47,16.98,48.12]]]},{"n":"Hungary","c":"HU","r":5,"ml":4,"lx":19.45,"ly":47.09,"b":[16.2,45.76,22.71,48.62],"p":[[[22.09,48.42,22.64,48.15,22.71,47.88,22.1,47.67,21.63,46.99,21.02,46.32,20.22,46.13,19.6,46.17,18.83,45.91,18.46,45.76,17.63,45.95,16.88,46.38,16.56,46.5,16.37,46.84,16.2,46.85,16.53,47.5,16.34,47.71,16.9,47.71,16.98,48.12,17.49,47.87,17.86,47.76,18.7,47.88,18.78,48.08,19.17,48.11,19.66,48.27,19.77,48.2,20.24,48.33,20.47,48.56,20.8,48.62,21.87,48.32,22.09,48.42]]]},{"n":"Moldova","c":"MD","r":6,"ml":5,"lx":28.49,"ly":47.43,"b":[26.62,45.49,30.02,48.47],"p":[[[26.62,48.22,26.86,48.37,27.52,48.47,28.26,48.16,28.67,48.12,29.12,47.85,29.05,47.51,29.42,47.35,29.56,46.93,29.91,46.67,29.84,46.53,30.02,46.42,29.76,46.35,29.17,46.38,29.07,46.52,28.86,46.44,28.93,46.26,28.66,45.94,28.49,45.6,28.23,45.49,28.05,45.94,28.16,46.37,28.13,46.81,27.55,47.41,27.23,47.83,26.92,48.12,26.62,48.22]]]},{"n":"Romania","c":"RO","r":3,"ml":3,"lx":24.97,"ly":45.73,"b":[20.22,43.69,29.63,48.22],"p":[[[28.23,45.49,28.68,45.3,29.15,45.46,29.6,45.29,29.63,45.04,29.14,44.82,28.84,44.91,28.56,43.71,27.97,43.81,27.24,44.18,26.07,43.94,25.57,43.69,24.1,43.74,23.33,43.9,22.94,43.82,22.66,44.23,22.47,44.41,22.71,44.58,22.46,44.7,22.15,44.48,21.56,44.77,21.48,45.18,20.87,45.42,20.76,45.73,20.22,46.13,21.02,46.32,21.63,46.99,22.1,47.67,22.71,47.88,23.14,48.1,23.76,47.99,24.4,47.98,24.87,47.74,25.21,47.89,25.95,47.99,26.2,48.22,26.62,48.22,26.92,48.12,27.23,47.83,27.55,47.41,28.13,46.81,28.16,46.37,28.05,45.94,28.23,45.49]]]},{"n":"Lithuania","c":"LT","r":5,"ml":4,"lx":24.09,"ly":55.1,"b":[21.06,53.91,26.59,56.37],"p":[[[26.49,55.62,26.59,55.17,25.77,54.85,25.54,54.28,24.45,53.91,23.48,53.91,23.24,54.22,22.73,54.33,22.65,54.58,22.76,54.86,22.32,55.02,21.27,55.19,21.06,56.03,22.2,56.34,23.88,56.27,24.86,56.37,25,56.16,25.53,56.1,26.49,55.62]]]},{"n":"Latvia","c":"LV","r":5,"ml":4,"lx":25.46,"ly":57.07,"b":[21.06,55.62,28.18,57.97],"p":[[[27.29,57.47,27.77,57.24,27.86,56.76,28.18,56.17,27.1,55.78,26.49,55.62,25.53,56.1,25,56.16,24.86,56.37,23.88,56.27,22.2,56.34,21.06,56.03,21.09,56.78,21.58,57.41,22.52,57.75,23.32,57.01,24.12,57.03,24.31,57.79,25.16,57.97,25.6,57.85,26.46,57.48,27.29,57.47]]]},{"n":"Estonia","c":"EE","r":6,"ml":3,"lx":25.87,"ly":58.72,"b":[23.34,57.47,28.13,59.61],"p":[[[27.98,59.48,28.13,59.3,27.42,58.72,27.72,57.79,27.29,57.47,26.46,57.48,25.6,57.85,25.16,57.97,24.31,57.79,24.43,58.38,24.06,58.26,23.43,58.61,23.34,59.19,24.6,59.47,25.86,59.61,26.95,59.45,27.98,59.48]]]},{"n":"Germany","c":"DE","r":2,"ml":1.7,"lx":9.68,"ly":50.96,"b":[5.99,47.3,15.02,54.98],"p":[[[14.12,53.76,14.35,53.25,14.07,52.98,14.44,52.62,14.69,52.09,14.61,51.75,15.02,51.11,14.57,51,14.31,51.12,14.06,50.93,13.34,50.73,12.97,50.48,12.24,50.27,12.42,49.97,12.52,49.55,13.03,49.31,13.6,48.88,13.24,48.42,12.88,48.29,13.03,47.64,12.93,47.47,12.62,47.67,12.14,47.7,11.43,47.52,10.54,47.57,10.4,47.3,9.9,47.58,9.59,47.53,8.52,47.83,8.32,47.61,7.47,47.62,7.59,48.33,8.1,49.02,6.66,49.2,6.19,49.46,6.24,49.9,6.04,50.13,6.16,50.8,5.99,51.85,6.59,51.85,6.84,52.23,7.09,53.14,6.91,53.48,7.1,53.69,7.94,53.75,8.12,53.53,8.8,54.02,8.57,54.4,8.53,54.96,9.28,54.83,9.92,54.98,9.94,54.6,10.95,54.36,10.94,54.01,11.96,54.2,12.52,54.47,13.65,54.08,14.12,53.76]]]},{"n":"Bulgaria","c":"BG","r":4,"ml":4,"lx":25.16,"ly":42.51,"b":[22.38,41.23,28.56,44.23],"p":[[[22.66,44.23,22.94,43.82,23.33,43.9,24.1,43.74,25.57,43.69,26.07,43.94,27.24,44.18,27.97,43.81,28.56,43.71,28.04,43.29,27.67,42.58,28,42.01,27.14,42.14,26.12,41.83,26.11,41.33,25.2,41.23,24.49,41.58,23.69,41.31,22.95,41.34,22.88,42,22.38,42.32,22.55,42.46,22.44,42.58,22.6,42.9,22.99,43.21,22.5,43.64,22.41,44.01,22.66,44.23]]]},{"n":"Greece","c":"GR","r":3,"ml":2.7,"lx":21.73,"ly":39.49,"b":[20.15,34.92,26.6,41.83],"p":[[[26.29,35.3,26.16,35,24.72,34.92,24.74,35.08,23.51,35.28,23.7,35.71,24.25,35.37,25.03,35.42,25.77,35.35,25.75,35.18,26.29,35.3]],[[22.95,41.34,23.69,41.31,24.49,41.58,25.2,41.23,26.11,41.33,26.12,41.83,26.6,41.56,26.29,40.94,26.06,40.82,25.45,40.85,24.93,40.95,23.71,40.69,24.41,40.12,23.9,39.96,23.34,39.96,22.81,40.48,22.63,40.26,22.85,39.66,23.35,39.19,22.97,38.97,23.53,38.51,24.03,38.22,24.04,37.66,23.12,37.92,23.41,37.41,22.77,37.31,23.15,36.42,22.49,36.41,21.67,36.84,21.3,37.64,21.12,38.31,20.73,38.77,20.22,39.34,20.15,39.62,20.62,40.11,20.67,40.44,21,40.58,21.02,40.84,21.67,40.93,22.06,41.15,22.6,41.13,22.76,41.3,22.95,41.34]]]},{"n":"Turkey","c":"TR","r":2,"ml":2,"lx":34.51,"ly":39.35,"b":[26.04,35.82,44.79,42.14],"p":[[[44.77,37.17,44.29,37,43.94,37.26,42.78,37.39,42.35,37.23,41.21,37.07,40.67,37.09,39.52,36.72,38.7,36.71,38.17,36.9,37.07,36.62,36.74,36.82,36.69,36.26,36.42,36.04,36.15,35.82,35.78,36.27,36.16,36.65,35.55,36.57,34.71,36.8,34.03,36.22,32.51,36.11,31.7,36.64,30.62,36.68,30.39,36.26,29.7,36.14,28.73,36.68,27.64,36.66,27.05,37.65,26.32,38.21,26.8,38.99,26.17,39.46,27.28,40.42,28.82,40.46,29.24,41.22,31.15,41.09,32.35,41.74,33.51,42.02,35.17,42.04,36.91,41.34,38.35,40.95,39.51,41.1,40.37,41.01,41.55,41.54,42.62,41.58,43.58,41.09,43.75,40.74,43.66,40.25,44.4,40.01,44.79,39.71,44.11,39.43,44.42,38.28,44.23,37.97,44.77,37.17]],[[26.12,41.83,27.14,42.14,28,42.01,28.12,41.62,28.99,41.3,28.81,41.05,27.62,41,27.19,40.69,26.36,40.15,26.04,40.62,26.06,40.82,26.29,40.94,26.6,41.56,26.12,41.83]]]},{"n":"Albania","c":"AL","r":6,"ml":5,"lx":20.11,"ly":40.65,"b":[19.3,39.62,21.02,42.69],"p":[[[21.02,40.84,21,40.58,20.67,40.44,20.62,40.11,20.15,39.62,19.98,39.69,19.96,39.92,19.41,40.25,19.32,40.73,19.4,41.41,19.54,41.72,19.37,41.88,19.3,42.2,19.74,42.69,19.8,42.5,20.07,42.59,20.28,42.32,20.52,42.22,20.59,41.86,20.46,41.52,20.61,41.09,21.02,40.84]]]},{"n":"Croatia","c":"HR","r":6,"ml":4,"lx":16.37,"ly":45.81,"b":[13.66,42.48,19.39,46.5],"p":[[[16.56,46.5,16.88,46.38,17.63,45.95,18.46,45.76,18.83,45.91,19.07,45.52,19.39,45.24,19.01,44.86,18.55,45.08,17.86,45.07,17,45.23,16.53,45.21,16.32,45,15.96,45.23,15.75,44.82,16.24,44.35,16.46,44.04,16.92,43.67,17.3,43.45,17.67,43.03,18.56,42.65,18.45,42.48,17.51,42.85,16.93,43.21,16.02,43.51,15.17,44.24,15.38,44.32,14.92,44.74,14.9,45.08,14.26,45.23,13.95,44.8,13.66,45.14,13.68,45.48,13.72,45.5,14.41,45.47,14.6,45.63,14.94,45.47,15.33,45.45,15.32,45.73,15.67,45.83,15.77,46.24,16.56,46.5]]]},{"n":"Switzerland","c":"CH","r":4,"ml":4,"lx":7.46,"ly":46.72,"b":[6.02,45.78,10.44,47.83],"p":[[[9.59,47.53,9.63,47.35,9.48,47.1,9.93,46.92,10.44,46.89,10.36,46.48,9.92,46.31,9.18,46.44,8.97,46.04,8.49,46.01,8.32,46.16,7.76,45.82,7.27,45.78,6.84,45.99,6.5,46.43,6.02,46.27,6.04,46.73,6.77,47.29,6.74,47.54,7.19,47.45,7.47,47.62,8.32,47.61,8.52,47.83,9.59,47.53]]]},{"n":"Luxembourg","c":"LU","r":6,"ml":5.7,"lx":6.08,"ly":49.73,"b":[5.67,49.44,6.24,50.13],"p":[[[6.04,50.13,6.24,49.9,6.19,49.46,5.9,49.44,5.67,49.53,5.78,50.09,6.04,50.13]]]},{"n":"Belgium","c":"BE","r":2,"ml":4,"lx":4.8,"ly":50.79,"b":[2.51,49.53,6.16,51.48],"p":[[[6.16,50.8,6.04,50.13,5.78,50.09,5.67,49.53,4.8,49.99,4.29,49.91,3.59,50.38,3.12,50.78,2.66,50.8,2.51,51.15,3.31,51.35,3.32,51.35,3.31,51.35,4.05,51.27,4.97,51.48,5.61,51.04,6.16,50.8]]]},{"n":"Netherlands","c":"NL","r":5,"ml":4,"lx":5.61,"ly":52.42,"b":[3.31,50.8,7.09,53.51],"p":[[[6.91,53.48,7.09,53.14,6.84,52.23,6.59,51.85,5.99,51.85,6.16,50.8,5.61,51.04,4.97,51.48,4.05,51.27,3.31,51.35,3.32,51.35,3.83,51.62,4.71,53.09,6.07,53.51,6.91,53.48]]]},{"n":"Portugal","c":"PT","r":2,"ml":3,"lx":-8.27,"ly":39.61,"b":[-9.53,36.84,-6.39,42.28],"p":[[[-9.03,41.88,-8.67,42.13,-8.26,42.28,-8.01,41.79,-7.42,41.79,-7.25,41.92,-6.67,41.88,-6.39,41.38,-6.85,41.11,-6.86,40.33,-7.03,40.18,-7.07,39.71,-7.5,39.63,-7.1,39.03,-7.37,38.37,-7.03,38.08,-7.17,37.8,-7.54,37.43,-7.45,37.1,-7.86,36.84,-8.38,36.98,-8.9,36.87,-8.75,37.65,-8.84,38.27,-9.29,38.36,-9.53,38.74,-9.45,39.39,-9.05,39.76,-8.98,40.16,-8.77,40.76,-8.79,41.18,-8.99,41.54,-9.03,41.88]]]},{"n":"Spain","c":"ES","r":2,"ml":2,"lx":-3.46,"ly":40.09,"b":[-9.39,35.95,3.04,43.75],"p":[[[-7.45,37.1,-7.54,37.43,-7.17,37.8,-7.03,38.08,-7.37,38.37,-7.1,39.03,-7.5,39.63,-7.07,39.71,-7.03,40.18,-6.86,40.33,-6.85,41.11,-6.39,41.38,-6.67,41.88,-7.25,41.92,-7.42,41.79,-8.01,41.79,-8.26,42.28,-8.67,42.13,-9.03,41.88,-8.98,42.59,-9.39,43.03,-7.98,43.75,-6.75,43.57,-5.41,43.57,-4.35,43.4,-3.52,43.46,-1.9,43.42,-1.5,43.03,0.34,42.58,0.7,42.8,1.83,42.34,2.99,42.47,3.04,41.89,2.09,41.23,0.81,41.01,0.72,40.68,0.11,40.12,-0.28,39.31,0.11,38.74,-0.47,38.29,-0.68,37.64,-1.44,37.44,-2.15,36.67,-3.42,36.66,-4.37,36.68,-5,36.32,-5.38,35.95,-5.87,36.03,-6.24,36.37,-6.52,36.94,-7.45,37.1]]]},{"n":"Ireland","c":"IE","r":3,"ml":3,"lx":-7.8,"ly":53.08,"b":[-9.98,51.67,-6.03,55.13],"p":[[[-6.2,53.87,-6.03,53.15,-6.79,52.26,-8.56,51.67,-9.98,51.82,-9.17,52.86,-9.69,53.88,-8.33,54.66,-7.57,55.13,-7.37,54.6,-7.57,54.06,-6.95,54.07,-6.2,53.87]]]},{"n":"New Caledonia","c":"NC","r":3,"ml":4.6,"lx":165.08,"ly":-21.06,"b":[164.03,-22.4,167.12,-20.11],"p":[[[165.78,-21.08,166.6,-21.7,167.12,-22.16,166.74,-22.4,166.19,-22.13,165.47,-21.68,164.83,-21.15,164.17,-20.44,164.03,-20.11,164.46,-20.12,165.02,-20.46,165.46,-20.8,165.78,-21.08]]]},{"n":"Solomon Islands","c":"SB","r":3,"ml":3,"lx":159.17,"ly":-8.03,"b":[156.49,-10.83,162.4,-6.6],"p":[[[162.12,-10.48,162.4,-10.83,161.7,-10.82,161.32,-10.2,161.92,-10.45,162.12,-10.48]],[[161.68,-9.6,161.53,-9.78,160.79,-8.92,160.58,-8.32,160.92,-8.32,161.28,-9.12,161.68,-9.6]],[[160.85,-9.87,160.46,-9.9,159.85,-9.79,159.64,-9.64,159.7,-9.24,160.36,-9.4,160.69,-9.61,160.85,-9.87]],[[159.64,-8.02,159.88,-8.34,159.92,-8.54,159.13,-8.11,158.59,-7.75,158.21,-7.42,158.36,-7.32,158.82,-7.56,159.64,-8.02]],[[157.14,-7.02,157.54,-7.35,157.34,-7.4,156.9,-7.18,156.49,-6.77,156.54,-6.6,157.14,-7.02]]]},{"n":"New Zealand","c":"NZ","r":2,"ml":2,"lx":172.79,"ly":-39.76,"b":[166.51,-46.64,178.52,-34.45],"p":[[[176.89,-40.07,176.51,-40.6,176.01,-41.29,175.24,-41.69,175.07,-41.43,174.65,-41.28,175.23,-40.46,174.9,-39.91,173.82,-39.51,173.85,-39.15,174.57,-38.8,174.74,-38.03,174.7,-37.38,174.29,-36.71,174.32,-36.53,173.84,-36.12,173.05,-35.24,172.64,-34.53,173.01,-34.45,173.55,-35.01,174.33,-35.27,174.61,-36.16,175.34,-37.21,175.36,-36.53,175.81,-36.8,175.96,-37.56,176.76,-37.88,177.44,-37.96,178.01,-37.58,178.52,-37.7,178.27,-38.58,177.97,-39.17,177.21,-39.15,176.94,-39.45,177.03,-39.88,176.89,-40.07]],[[169.67,-43.56,170.52,-43.03,171.13,-42.51,171.57,-41.77,171.95,-41.51,172.1,-40.96,172.8,-40.49,173.02,-40.92,173.25,-41.33,173.96,-40.93,174.25,-41.35,174.25,-41.77,173.88,-42.23,173.22,-42.97,172.71,-43.37,173.08,-43.85,172.31,-43.87,171.45,-44.24,171.19,-44.9,170.62,-45.91,169.83,-46.36,169.33,-46.64,168.41,-46.62,167.76,-46.29,166.68,-46.22,166.51,-45.85,167.05,-45.11,168.3,-44.12,168.95,-43.94,169.67,-43.56]]]},{"n":"Australia","c":"AU","r":2,"ml":1.7,"lx":134.05,"ly":-24.13,"b":[113.34,-43.63,153.57,-10.67],"p":[[[147.69,-40.81,148.29,-40.88,148.36,-42.06,148.02,-42.41,147.91,-43.21,147.56,-42.94,146.87,-43.63,146.66,-43.58,146.05,-43.55,145.43,-42.69,145.3,-42.03,144.72,-41.16,144.74,-40.7,145.4,-40.79,146.36,-41.14,146.91,-41,147.69,-40.81]],[[126.15,-32.22,125.09,-32.73,124.22,-32.96,124.03,-33.48,123.66,-33.89,122.81,-33.91,122.18,-34,121.3,-33.82,120.58,-33.93,119.89,-33.98,119.3,-34.51,119.01,-34.46,118.51,-34.75,118.02,-35.06,117.3,-35.03,116.63,-35.03,115.56,-34.39,115.03,-34.2,115.05,-33.62,115.55,-33.49,115.71,-33.26,115.68,-32.9,115.8,-32.21,115.69,-31.61,115.16,-30.6,115,-30.03,115.04,-29.46,114.64,-28.81,114.62,-28.52,114.17,-28.12,114.05,-27.33,113.48,-26.54,113.34,-26.12,113.78,-26.55,113.44,-25.62,113.94,-25.91,114.23,-26.3,114.22,-25.79,113.72,-25,113.63,-24.68,113.39,-24.38,113.5,-23.81,113.71,-23.56,113.84,-23.06,113.74,-22.48,114.15,-21.76,114.23,-22.52,114.65,-21.83,115.46,-21.5,115.95,-21.07,116.71,-20.7,117.17,-20.62,117.44,-20.75,118.23,-20.37,118.84,-20.26,118.99,-20.04,119.25,-19.95,119.81,-19.98,120.86,-19.68,121.4,-19.24,121.66,-18.71,122.24,-18.2,122.29,-17.8,122.31,-17.25,123.01,-16.41,123.43,-17.27,123.86,-17.07,123.5,-16.6,123.82,-16.11,124.26,-16.33,124.38,-15.57,124.93,-15.08,125.17,-14.68,125.67,-14.51,125.69,-14.23,126.13,-14.35,126.14,-14.1,126.58,-13.95,127.07,-13.82,127.8,-14.28,128.36,-14.87,128.99,-14.88,129.62,-14.97,129.41,-14.42,129.89,-13.62,130.34,-13.36,130.18,-13.11,130.62,-12.54,131.22,-12.18,131.74,-12.3,132.58,-12.11,132.56,-11.6,131.82,-11.27,132.36,-11.13,133.02,-11.38,133.55,-11.79,134.39,-12.04,134.68,-11.94,135.3,-12.25,135.88,-11.96,136.26,-12.05,136.49,-11.86,136.95,-12.35,136.69,-12.89,136.31,-13.29,135.96,-13.32,136.08,-13.72,135.78,-14.22,135.43,-14.72,135.5,-15,136.3,-15.55,137.07,-15.87,137.58,-16.22,138.3,-16.81,138.59,-16.81,139.11,-17.06,139.26,-17.37,140.22,-17.71,140.88,-17.37,141.07,-16.83,141.27,-16.39,141.4,-15.84,141.7,-15.04,141.56,-14.56,141.64,-14.27,141.52,-13.7,141.65,-12.94,141.84,-12.74,141.69,-12.41,141.93,-11.88,142.12,-11.33,142.14,-11.04,142.52,-10.67,142.8,-11.16,142.87,-11.78,143.12,-11.91,143.16,-12.33,143.52,-12.83,143.6,-13.4,143.56,-13.76,143.92,-14.55,144.56,-14.17,144.89,-14.59,145.37,-14.98,145.27,-15.43,145.49,-16.29,145.64,-16.78,145.89,-16.91,146.16,-17.76,146.06,-18.28,146.39,-18.96,147.47,-19.48,148.18,-19.96,148.85,-20.39,148.72,-20.63,149.29,-21.26,149.68,-22.34,150.08,-22.12,150.48,-22.56,150.73,-22.4,150.9,-23.46,151.61,-24.08,152.07,-24.46,152.86,-25.27,153.14,-26.07,153.16,-26.64,153.09,-27.26,153.57,-28.11,153.51,-29,153.34,-29.46,153.07,-30.35,153.09,-30.92,152.89,-31.64,152.45,-32.55,151.71,-33.04,151.34,-33.82,151.01,-34.31,150.71,-35.17,150.33,-35.67,150.08,-36.42,149.95,-37.11,150,-37.43,149.42,-37.77,148.3,-37.81,147.38,-38.22,146.92,-38.61,146.32,-39.04,145.49,-38.59,144.88,-38.42,145.03,-37.9,144.49,-38.09,143.61,-38.81,142.75,-38.54,142.18,-38.38,141.61,-38.31,140.64,-38.02,139.99,-37.4,139.81,-36.64,139.57,-36.14,139.08,-35.73,138.12,-35.61,138.45,-35.13,138.21,-34.38,137.72,-35.08,136.83,-35.26,137.35,-34.71,137.5,-34.13,137.89,-33.64,137.81,-32.9,137,-33.75,136.37,-34.09,135.99,-34.89,135.21,-34.48,135.24,-33.95,134.61,-33.22,134.09,-32.85,134.27,-32.62,132.99,-32.01,132.29,-31.98,131.33,-31.5,129.54,-31.59,128.24,-31.95,127.1,-32.28,126.15,-32.22]]]},{"n":"Sri Lanka","c":"LK","r":3,"ml":3,"lx":80.7,"ly":7.58,"b":[79.7,5.97,81.79,9.82],"p":[[[81.79,7.52,81.64,6.48,81.22,6.2,80.35,5.97,79.87,6.76,79.7,8.2,80.15,9.82,80.84,9.27,81.3,8.56,81.79,7.52]]]},{"n":"People's Republic of China","c":"CN","r":2,"ml":1.7,"lx":106.34,"ly":32.5,"b":[73.68,18.2,135.03,53.46],"p":[[[109.48,18.2,108.66,18.51,108.63,19.37,109.12,19.82,110.21,20.1,110.79,20.08,111.01,19.7,110.57,19.26,110.34,18.68,109.48,18.2]],[[80.26,42.35,80.18,42.92,80.87,43.18,79.97,44.92,81.95,45.32,82.46,45.54,83.18,47.33,85.16,47,85.72,47.45,85.77,48.46,86.6,48.55,87.36,49.21,87.75,49.3,88.01,48.6,88.85,48.07,90.28,47.69,90.97,46.89,90.59,45.72,90.95,45.29,92.13,45.12,93.48,44.98,94.69,44.35,95.31,44.24,95.76,43.32,96.35,42.73,97.45,42.75,99.52,42.52,100.85,42.66,101.83,42.51,103.31,41.91,104.52,41.91,104.96,41.6,106.13,42.13,107.74,42.48,109.24,42.52,110.41,42.87,111.13,43.41,111.83,43.74,111.67,44.07,111.35,44.46,111.87,45.1,112.44,45.01,113.46,44.81,114.46,45.34,115.99,45.73,116.72,46.39,117.42,46.67,118.87,46.81,119.66,46.69,119.77,47.05,118.87,47.75,118.06,48.07,117.3,47.7,116.31,47.85,115.74,47.73,115.49,48.14,116.19,49.13,116.68,49.89,117.88,49.51,119.29,50.14,119.28,50.58,120.18,51.64,120.74,51.96,120.73,52.52,120.18,52.75,121,53.25,122.25,53.43,123.57,53.46,125.07,53.16,125.95,52.79,126.56,51.78,126.94,51.35,127.29,50.74,127.66,49.76,129.4,49.44,130.58,48.73,130.99,47.79,132.51,47.79,133.37,48.18,135.03,48.48,134.5,47.58,134.11,47.21,133.77,46.12,133.1,45.14,131.88,45.32,131.03,44.97,131.29,44.11,131.14,42.93,130.63,42.9,130.64,42.4,129.99,42.99,129.6,42.42,128.05,41.99,128.21,41.47,127.34,41.5,126.87,41.82,126.18,41.11,125.08,40.57,124.27,39.93,122.87,39.64,122.13,39.17,121.05,38.9,121.59,39.36,121.38,39.75,122.17,40.42,121.64,40.95,120.77,40.59,119.64,39.9,119.02,39.25,118.04,39.2,117.53,38.74,118.06,38.06,118.88,37.9,118.91,37.45,119.7,37.16,120.82,37.87,121.71,37.48,122.36,37.45,122.52,36.93,121.1,36.65,120.64,36.11,119.66,35.61,119.15,34.91,120.23,34.36,120.62,33.38,121.23,32.46,121.91,31.69,121.89,30.95,121.26,30.68,121.5,30.14,122.09,29.83,121.94,29.02,121.68,28.23,121.13,28.14,120.4,27.05,119.59,25.74,118.66,24.55,117.28,23.62,115.89,22.78,114.76,22.67,114.15,22.22,113.81,22.55,113.24,22.05,111.84,21.55,110.79,21.4,110.44,20.34,109.89,20.28,109.63,21.01,109.86,21.4,108.52,21.72,108.05,21.55,107.04,21.81,106.57,22.22,106.73,22.79,105.81,22.98,105.33,23.35,104.48,22.82,103.5,22.7,102.71,22.71,102.17,22.46,101.65,22.32,101.8,21.17,101.27,21.2,101.18,21.44,101.15,21.85,100.42,21.56,99.98,21.74,99.24,22.12,99.53,22.95,98.9,23.14,98.66,24.06,97.6,23.9,97.72,25.08,98.67,25.92,98.71,26.74,98.68,27.51,98.25,27.75,97.91,28.34,97.33,28.26,96.25,28.41,96.59,28.83,96.12,29.45,95.4,29.03,94.57,29.28,93.41,28.64,92.5,27.9,91.7,27.77,91.26,28.04,90.73,28.06,90.02,28.3,89.48,28.04,88.81,27.3,88.73,28.09,88.12,27.88,86.95,27.97,85.82,28.2,85.01,28.64,84.23,28.84,83.9,29.32,83.34,29.46,82.33,30.12,81.53,30.42,81.11,30.18,79.72,30.88,78.74,31.52,78.46,32.62,79.18,32.48,79.21,32.99,78.81,33.51,78.91,34.32,77.84,35.49,76.19,35.9,75.9,36.67,75.16,37.13,74.98,37.42,74.83,37.99,74.86,38.38,74.26,38.61,73.93,38.51,73.68,39.43,73.96,39.66,73.82,39.89,74.78,40.37,75.47,40.56,76.53,40.43,76.9,41.07,78.19,41.19,78.54,41.58,80.12,42.12,80.26,42.35]]]},{"n":"Taiwan","c":"TW","r":3,"ml":4.5,"lx":120.87,"ly":23.65,"b":[120.11,21.97,121.95,25.3],"p":[[[121.78,24.39,121.18,22.79,120.75,21.97,120.22,22.81,120.11,23.56,120.69,24.54,121.5,25.3,121.95,25,121.78,24.39]]]},{"n":"Italy","c":"IT","r":2,"ml":2,"lx":11.08,"ly":44.73,"b":[6.75,36.62,18.48,47.12],"p":[[[10.44,46.89,11.05,46.75,11.16,46.94,12.15,47.12,12.38,46.77,13.81,46.51,13.7,46.02,13.94,45.59,13.14,45.74,12.33,45.38,12.38,44.89,12.26,44.6,12.59,44.09,13.53,43.59,14.03,42.76,15.14,41.96,15.93,41.96,16.17,41.74,15.89,41.54,16.79,41.18,17.52,40.88,18.38,40.36,18.48,40.17,18.29,39.81,17.74,40.28,16.87,40.44,16.45,39.8,17.17,39.42,17.05,38.9,16.64,38.84,16.1,37.99,15.68,37.91,15.69,38.21,15.89,38.75,16.11,38.96,15.72,39.54,15.41,40.05,15,40.17,14.7,40.6,14.06,40.79,13.63,41.19,12.89,41.25,12.11,41.7,11.19,42.36,10.51,42.93,10.2,43.92,9.7,44.04,8.89,44.37,8.43,44.23,7.85,43.77,7.44,43.69,7.55,44.13,7.01,44.25,6.75,45.03,7.1,45.33,6.8,45.71,6.84,45.99,7.27,45.78,7.76,45.82,8.32,46.16,8.49,46.01,8.97,46.04,9.18,46.44,9.92,46.31,10.36,46.48,10.44,46.89]],[[14.76,38.14,15.52,38.23,15.16,37.44,15.31,37.13,15.1,36.62,14.34,37,13.83,37.1,12.43,37.61,12.57,38.13,13.74,38.03,14.76,38.14]],[[8.71,40.9,9.21,41.21,9.81,40.5,9.67,39.18,9.21,39.24,8.81,38.91,8.43,39.17,8.39,40.38,8.16,40.95,8.71,40.9]]]},{"n":"Denmark","c":"DK","r":4,"ml":3,"lx":9.02,"ly":55.97,"b":[8.09,54.8,12.69,57.73],"p":[[[9.92,54.98,9.28,54.83,8.53,54.96,8.12,55.52,8.09,56.54,8.26,56.81,8.54,57.11,9.42,57.17,9.78,57.45,10.58,57.73,10.55,57.22,10.25,56.89,10.37,56.61,10.91,56.46,10.67,56.08,10.37,56.19,9.65,55.47,9.92,54.98]],[[12.37,56.11,12.69,55.61,12.09,54.8,11.04,55.36,10.9,55.78,12.37,56.11]]]},{"n":"United Kingdom","c":"GB","r":2,"ml":1.7,"lx":-2.12,"ly":54.4,"b":[-7.57,49.96,1.68,58.64],"p":[[[-6.2,53.87,-6.95,54.07,-7.57,54.06,-7.37,54.6,-7.57,55.13,-6.73,55.17,-5.66,54.55,-6.2,53.87]],[[-3.09,53.4,-2.95,53.99,-3.61,54.6,-3.63,54.62,-4.84,54.79,-5.08,55.06,-4.72,55.51,-5.05,55.78,-5.59,55.31,-5.64,56.28,-6.15,56.79,-5.79,57.82,-5.01,58.63,-4.21,58.55,-3.01,58.64,-4.07,57.55,-3.06,57.69,-1.96,57.68,-2.22,56.87,-3.12,55.97,-2.09,55.91,-2.01,55.8,-1.11,54.62,-0.43,54.46,0.18,53.33,0.47,52.93,1.68,52.74,1.56,52.1,1.05,51.81,1.45,51.29,0.55,50.77,-0.79,50.77,-2.49,50.5,-2.96,50.7,-3.62,50.23,-4.54,50.34,-5.25,49.96,-5.78,50.16,-4.31,51.21,-3.41,51.43,-3.42,51.43,-4.98,51.59,-5.27,51.99,-4.22,52.3,-4.77,52.84,-4.58,53.5,-3.09,53.4]]]},{"n":"Iceland","c":"IS","r":3,"ml":2,"lx":-18.67,"ly":64.78,"b":[-24.33,63.5,-13.61,66.53],"p":[[[-14.51,66.46,-14.74,65.81,-13.61,65.13,-14.91,64.36,-17.79,63.68,-18.66,63.5,-19.97,63.64,-22.76,63.96,-21.78,64.4,-23.96,64.89,-22.18,65.08,-22.23,65.38,-24.33,65.61,-23.65,66.26,-22.13,66.41,-20.58,65.73,-19.06,66.28,-17.8,65.99,-16.17,66.53,-14.51,66.46]]]},{"n":"Azerbaijan","c":"AZ","r":5,"ml":4,"lx":47.21,"ly":40.4,"b":[44.79,38.27,50.39,41.86],"p":[[[46.4,41.86,46.69,41.83,47.37,41.22,47.82,41.15,47.99,41.41,48.58,41.81,49.11,41.28,49.62,40.57,50.08,40.53,50.39,40.26,49.57,40.18,49.4,39.4,49.22,39.05,48.86,38.82,48.88,38.32,48.63,38.27,48.01,38.79,48.36,39.29,48.06,39.58,47.69,39.51,46.51,38.77,46.48,39.46,46.03,39.63,45.61,39.9,45.89,40.22,45.36,40.56,45.56,40.81,45.18,40.99,44.97,41.25,45.22,41.41,45.96,41.12,46.5,41.06,46.64,41.18,46.15,41.72,46.4,41.86]],[[46.14,38.74,45.46,38.87,44.95,39.34,44.79,39.71,45,39.74,45.3,39.47,45.74,39.47,45.74,39.32,46.14,38.74]]]},{"n":"Georgia","c":"GE","r":5,"ml":4,"lx":43.74,"ly":41.87,"b":[39.96,41.06,46.64,43.55],"p":[[[39.96,43.43,40.08,43.55,40.92,43.38,42.39,43.22,43.76,42.74,43.93,42.55,44.54,42.71,45.47,42.5,45.78,42.09,46.4,41.86,46.15,41.72,46.64,41.18,46.5,41.06,45.96,41.12,45.22,41.41,44.97,41.25,43.58,41.09,42.62,41.58,41.55,41.54,41.7,41.96,41.45,42.65,40.88,43.01,40.32,43.13,39.96,43.43]]]},{"n":"Philippines","c":"PH","r":2,"ml":2.5,"lx":122.47,"ly":11.2,"b":[117.17,5.58,126.54,18.51],"p":[[[120.83,12.7,120.32,13.47,121.18,13.43,121.53,13.07,121.26,12.21,120.83,12.7]],[[122.59,9.98,122.84,10.26,122.95,10.88,123.5,10.94,123.34,10.27,124.08,11.23,123.98,10.28,123.62,9.95,123.31,9.32,123,9.02,122.38,9.71,122.59,9.98]],[[126.38,8.41,126.48,7.75,126.54,7.19,126.2,6.27,125.83,7.29,125.36,6.79,125.68,6.05,125.4,5.58,124.22,6.16,123.94,6.89,124.24,7.36,123.61,7.83,123.3,7.42,122.83,7.46,122.09,6.9,121.92,7.19,122.31,8.03,122.94,8.32,123.49,8.69,123.84,8.24,124.6,8.51,124.76,8.96,125.47,8.99,125.41,9.76,126.22,9.29,126.31,8.78,126.38,8.41]],[[118.5,9.32,117.17,8.37,117.66,9.07,118.39,9.68,118.99,10.38,119.51,11.37,119.69,10.55,119.03,10,118.5,9.32]],[[122.34,18.22,122.17,17.81,122.52,17.09,122.25,16.26,121.66,15.93,121.51,15.12,121.73,14.33,122.26,14.22,122.7,14.34,123.95,13.78,123.86,13.24,124.18,13,124.08,12.54,123.3,13.03,122.93,13.55,122.67,13.19,122.03,13.78,121.13,13.64,120.63,13.86,120.68,14.27,120.99,14.53,120.69,14.76,120.56,14.4,120.07,14.97,119.92,15.41,119.88,16.36,120.29,16.03,120.39,17.6,120.72,18.51,121.32,18.5,121.94,18.22,122.25,18.48,122.34,18.22]],[[122.04,11.42,121.88,11.89,122.48,11.58,123.12,11.58,123.1,11.17,122.64,10.74,122,10.44,121.97,10.91,122.04,11.42]],[[125.5,12.16,125.78,11.05,125.01,11.31,125.03,10.98,125.28,10.36,124.8,10.13,124.76,10.84,124.46,10.89,124.3,11.5,124.89,11.42,124.88,11.79,124.27,12.56,125.23,12.54,125.5,12.16]]]},{"n":"Malaysia","c":"MY","r":3,"ml":3,"lx":113.84,"ly":2.53,"b":[100.09,0.77,119.18,6.93],"p":[[[100.09,6.46,100.26,6.64,101.08,6.2,101.15,5.69,101.81,5.81,102.14,6.22,102.37,6.13,102.96,5.52,103.38,4.86,103.44,4.18,103.33,3.73,103.43,3.38,103.5,2.79,103.85,2.52,104.25,1.63,104.23,1.29,103.52,1.23,102.57,1.97,101.39,2.76,101.27,3.27,100.7,3.94,100.56,4.77,100.2,5.31,100.31,6.04,100.09,6.46]],[[117.88,4.14,117.02,4.31,115.87,4.31,115.52,3.17,115.13,2.82,114.62,1.43,113.81,1.22,112.86,1.5,112.38,1.41,111.8,0.9,111.16,0.98,110.51,0.77,109.83,1.34,109.66,2.01,110.4,1.66,111.17,1.85,111.37,2.7,111.8,2.89,113,3.1,113.71,3.89,114.2,4.53,114.66,4.01,114.87,4.35,115.35,4.32,115.41,4.96,115.45,5.45,116.22,6.14,116.73,6.92,117.13,6.93,117.64,6.42,117.69,5.99,118.35,5.71,119.18,5.41,119.11,5.02,118.44,4.97,118.62,4.48,117.88,4.14]]]},{"n":"Brunei","c":"BN","r":6,"ml":4,"lx":114.55,"ly":4.45,"b":[114.2,4.01,115.45,5.45],"p":[[[115.45,5.45,115.41,4.96,115.35,4.32,114.87,4.35,114.66,4.01,114.2,4.53,114.6,4.9,115.45,5.45]]]},{"n":"Slovenia","c":"SI","r":6,"ml":5,"lx":14.92,"ly":46.06,"b":[13.7,45.45,16.56,46.85],"p":[[[13.81,46.51,14.63,46.43,15.14,46.66,16.01,46.68,16.2,46.85,16.37,46.84,16.56,46.5,15.77,46.24,15.67,45.83,15.32,45.73,15.33,45.45,14.94,45.47,14.6,45.63,14.41,45.47,13.72,45.5,13.94,45.59,13.7,46.02,13.81,46.51]]]},{"n":"Finland","c":"FI","r":3,"ml":3,"lx":27.28,"ly":63.25,"b":[20.65,59.85,31.52,70.16],"p":[[[28.59,69.06,28.45,68.36,29.98,67.7,29.05,66.94,30.22,65.81,29.54,64.95,30.44,64.2,30.04,63.55,31.52,62.87,31.14,62.36,30.21,61.78,28.07,60.5,26.26,60.42,24.5,60.06,22.87,59.85,22.29,60.39,21.32,60.72,21.54,61.71,21.06,62.61,21.54,63.19,22.44,63.82,24.73,64.9,25.4,65.11,25.29,65.53,23.9,66.01,23.57,66.4,23.54,67.94,21.98,68.62,20.65,69.11,21.24,69.37,22.36,68.84,23.66,68.89,24.74,68.65,25.69,69.09,26.18,69.83,27.73,70.16,29.02,69.77,28.59,69.06]]]},{"n":"Slovakia","c":"SK","r":6,"ml":4,"lx":19.05,"ly":48.73,"b":[16.88,47.76,22.56,49.57],"p":[[[22.56,49.09,22.28,48.83,22.09,48.42,21.87,48.32,20.8,48.62,20.47,48.56,20.24,48.33,19.77,48.2,19.66,48.27,19.17,48.11,18.78,48.08,18.7,47.88,17.86,47.76,17.49,47.87,16.98,48.12,16.88,48.47,16.96,48.6,17.1,48.82,17.55,48.8,17.89,48.9,17.91,49,18.1,49.04,18.17,49.27,18.4,49.32,18.55,49.5,18.85,49.5,18.91,49.44,19.32,49.57,19.83,49.22,20.42,49.43,20.89,49.33,21.61,49.47,22.56,49.09]]]},{"n":"Czech Republic","c":"CZ","r":5,"ml":4,"lx":15.38,"ly":49.88,"b":[12.24,48.56,18.85,51.12],"p":[[[15.02,51.11,15.49,50.78,16.24,50.7,16.18,50.42,16.72,50.22,16.87,50.47,17.55,50.36,17.65,50.05,18.39,49.99,18.85,49.5,18.55,49.5,18.4,49.32,18.17,49.27,18.1,49.04,17.91,49,17.89,48.9,17.55,48.8,17.1,48.82,16.96,48.6,16.5,48.79,16.03,48.73,15.25,49.04,14.9,48.96,14.34,48.56,13.6,48.88,13.03,49.31,12.52,49.55,12.42,49.97,12.24,50.27,12.97,50.48,13.34,50.73,14.06,50.93,14.31,51.12,14.57,51,15.02,51.11]]]},{"n":"Eritrea","c":"ER","r":4,"ml":4,"lx":38.29,"ly":15.79,"b":[36.32,12.46,43.08,18],"p":[[[36.43,14.42,36.32,14.82,36.75,16.29,36.85,16.96,37.17,17.26,37.9,17.43,38.41,18,38.99,16.84,39.27,15.92,39.81,15.44,41.18,14.49,41.73,13.92,42.28,13.34,42.59,13,43.08,12.7,42.78,12.46,42.35,12.54,42.01,12.87,41.6,13.45,41.16,13.77,40.9,14.12,40.03,14.52,39.34,14.53,39.1,14.74,38.51,14.51,37.91,14.96,37.59,14.21,36.43,14.42]]]},{"n":"Japan","c":"JP","r":2,"ml":1.7,"lx":138.44,"ly":36.14,"b":[129.41,31.03,145.54,45.55],"p":[[[141.88,39.18,140.96,38.17,140.98,37.14,140.6,36.34,140.77,35.84,140.25,35.14,138.98,34.67,137.22,34.61,135.79,33.46,135.12,33.85,135.08,34.6,133.34,34.38,132.16,33.9,130.99,33.89,132,33.15,131.33,31.45,130.69,31.03,130.2,31.42,130.45,32.32,129.81,32.61,129.41,33.3,130.35,33.6,130.88,34.23,131.88,34.75,132.62,35.43,134.61,35.73,135.68,35.53,136.72,37.3,137.39,36.83,138.86,37.83,139.43,38.22,140.05,39.44,139.88,40.56,140.31,41.2,141.37,41.38,141.91,39.99,141.88,39.18]],[[144.61,43.96,145.32,44.38,145.54,43.26,144.06,42.99,143.18,42,141.61,42.68,141.07,41.58,139.96,41.57,139.82,42.56,140.31,43.33,141.38,43.39,141.67,44.77,141.97,45.55,143.14,44.51,143.91,44.17,144.61,43.96]],[[132.37,33.46,132.92,34.06,133.49,33.94,133.9,34.36,134.64,34.15,134.77,33.81,134.2,33.2,133.79,33.52,133.28,33.29,133.01,32.7,132.36,32.99,132.37,33.46]]]},{"n":"Paraguay","c":"PY","r":4,"ml":3,"lx":-60.15,"ly":-21.67,"b":[-62.69,-27.55,-54.29,-19.34],"p":[[[-58.17,-20.18,-57.87,-20.73,-57.94,-22.09,-56.88,-22.28,-56.47,-22.09,-55.8,-22.36,-55.61,-22.66,-55.52,-23.57,-55.4,-23.96,-55.03,-24,-54.65,-23.84,-54.29,-24.02,-54.29,-24.57,-54.43,-25.16,-54.63,-25.74,-54.79,-26.62,-55.7,-27.39,-56.49,-27.55,-57.61,-27.4,-58.62,-27.12,-57.63,-25.6,-57.78,-25.16,-58.81,-24.77,-60.03,-24.03,-60.85,-23.88,-62.69,-22.25,-62.29,-21.05,-62.27,-20.51,-61.79,-19.63,-60.04,-19.34,-59.12,-19.36,-58.18,-19.87,-58.17,-20.18]]]},{"n":"Yemen","c":"YE","r":3,"ml":3,"lx":45.87,"ly":15.33,"b":[42.6,12.59,53.11,19],"p":[[[52,19,52.78,17.35,53.11,16.65,52.39,16.38,52.19,15.94,52.17,15.6,51.17,15.18,49.57,14.71,48.68,14,48.24,13.95,47.94,14.01,47.35,13.59,46.72,13.4,45.88,13.35,45.63,13.29,45.41,13.03,45.14,12.95,44.99,12.7,44.49,12.72,44.18,12.59,43.48,12.64,43.22,13.22,43.25,13.77,43.09,14.06,42.89,14.8,42.6,15.21,42.81,15.26,42.7,15.72,42.82,15.91,42.78,16.35,43.22,16.67,43.12,17.09,43.38,17.58,43.79,17.32,44.06,17.41,45.22,17.43,45.4,17.33,46.37,17.23,46.75,17.28,47,16.95,47.47,17.12,48.18,18.17,49.12,18.62,52,19]]]},{"n":"Saudi Arabia","c":"SA","r":2,"ml":1.7,"lx":44.7,"ly":23.81,"b":[34.63,16.35,55.67,32.16],"p":[[[34.96,29.36,36.07,29.2,36.5,29.51,36.74,29.87,37.5,30,37.67,30.34,38,30.51,37,31.51,39,32.01,39.2,32.16,40.4,31.89,41.89,31.19,44.71,29.18,46.57,29.1,47.46,29,47.71,28.53,48.42,28.55,48.81,27.69,49.3,27.46,49.47,27.11,50.15,26.69,50.21,26.28,50.11,25.94,50.24,25.61,50.53,25.33,50.66,25,50.81,24.75,51.11,24.56,51.39,24.63,51.58,24.25,51.62,24.01,52,23,55.01,22.5,55.21,22.71,55.67,22,55,20,52,19,49.12,18.62,48.18,18.17,47.47,17.12,47,16.95,46.75,17.28,46.37,17.23,45.4,17.33,45.22,17.43,44.06,17.41,43.79,17.32,43.38,17.58,43.12,17.09,43.22,16.67,42.78,16.35,42.65,16.77,42.35,17.08,42.27,17.47,41.75,17.83,41.22,18.67,40.94,19.49,40.25,20.17,39.8,20.34,39.14,21.29,39.02,21.99,39.07,22.58,38.49,23.69,38.02,24.08,37.48,24.29,37.15,24.86,37.21,25.08,36.93,25.6,36.64,25.83,36.25,26.57,35.64,27.38,35.13,28.06,34.63,28.06,34.79,28.61,34.83,28.96,34.96,29.36]]]},{"n":"Antarctica","c":"AQ","r":4,"ml":4,"lx":35.89,"ly":-79.84,"b":[-180,-90,180,-63.27],"p":[[[-48.66,-78.05,-48.15,-78.05,-46.66,-77.83,-45.15,-78.05,-43.92,-78.48,-43.49,-79.09,-43.37,-79.52,-43.33,-80.03,-44.88,-80.34,-46.51,-80.59,-48.39,-80.83,-50.48,-81.03,-52.85,-80.97,-54.16,-80.63,-53.99,-80.22,-51.85,-79.95,-50.99,-79.61,-50.36,-79.18,-49.91,-78.81,-49.31,-78.46,-48.66,-78.05]],[[-66.29,-80.26,-64.04,-80.29,-61.88,-80.39,-61.14,-79.98,-60.61,-79.63,-59.57,-80.04,-59.87,-80.55,-60.16,-81,-62.26,-80.86,-64.49,-80.92,-65.74,-80.59,-65.74,-80.55,-66.29,-80.26]],[[-73.92,-71.27,-73.23,-71.15,-72.07,-71.19,-71.78,-70.68,-71.72,-70.31,-71.74,-69.51,-71.17,-69.04,-70.25,-68.88,-69.72,-69.25,-69.49,-69.62,-69.06,-70.07,-68.73,-70.51,-68.45,-70.96,-68.33,-71.41,-68.51,-71.8,-68.78,-72.17,-69.96,-72.31,-71.08,-72.5,-72.39,-72.48,-71.9,-72.09,-73.07,-72.23,-74.19,-72.37,-74.95,-72.07,-75.01,-71.66,-73.92,-71.27]],[[-102.33,-71.89,-101.7,-71.72,-100.43,-71.85,-98.98,-71.93,-97.88,-72.07,-96.79,-71.95,-96.2,-72.52,-96.98,-72.44,-98.2,-72.48,-99.43,-72.44,-100.78,-72.5,-101.8,-72.31,-102.33,-71.89]],[[-122.62,-73.66,-122.41,-73.32,-121.21,-73.5,-119.92,-73.66,-118.72,-73.48,-119.29,-73.83,-120.23,-74.09,-121.62,-74.01,-122.62,-73.66]],[[-127.28,-73.46,-126.56,-73.25,-125.56,-73.48,-124.03,-73.87,-124.62,-73.83,-125.91,-73.74,-127.28,-73.46]],[[-163.71,-78.6,-163.11,-78.22,-161.25,-78.38,-160.25,-78.69,-159.48,-79.05,-159.21,-79.5,-161.13,-79.63,-162.44,-79.28,-163.03,-78.93,-163.07,-78.87,-163.71,-78.6]],[[180,-84.71,180,-90,-180,-90,-180,-84.71,-179.94,-84.72,-179.06,-84.14,-177.26,-84.45,-177.14,-84.42,-176.08,-84.1,-175.95,-84.11,-175.83,-84.12,-174.38,-84.53,-173.12,-84.12,-172.89,-84.06,-169.95,-83.88,-169,-84.12,-168.53,-84.24,-167.02,-84.57,-164.18,-84.83,-161.93,-85.14,-158.07,-85.37,-155.19,-85.1,-150.94,-85.3,-148.53,-85.61,-145.89,-85.32,-143.11,-85.04,-142.89,-84.57,-146.83,-84.53,-150.06,-84.3,-150.9,-83.9,-153.59,-83.69,-153.41,-83.24,-153.04,-82.83,-152.67,-82.45,-152.86,-82.04,-154.53,-81.77,-155.29,-81.42,-156.84,-81.1,-154.41,-81.16,-152.1,-81,-150.65,-81.34,-148.87,-81.04,-147.22,-80.67,-146.42,-80.34,-146.77,-79.93,-148.06,-79.65,-149.53,-79.36,-151.59,-79.3,-153.39,-79.16,-155.33,-79.06,-155.98,-78.69,-157.27,-78.38,-158.05,-78.03,-158.37,-76.89,-157.88,-76.99,-156.97,-77.3,-155.33,-77.2,-153.74,-77.07,-152.92,-77.5,-151.33,-77.4,-150,-77.18,-148.75,-76.91,-147.61,-76.58,-146.1,-76.48,-146.14,-76.11,-146.5,-75.73,-146.2,-75.38,-144.91,-75.2,-144.32,-75.54,-142.79,-75.34,-141.64,-75.09,-140.21,-75.07,-138.86,-74.97,-137.51,-74.73,-136.43,-74.52,-135.21,-74.3,-134.43,-74.36,-133.75,-74.44,-132.26,-74.3,-130.93,-74.48,-129.55,-74.46,-128.24,-74.32,-126.89,-74.42,-125.4,-74.52,-124.01,-74.48,-122.56,-74.5,-121.07,-74.52,-119.7,-74.48,-118.68,-74.19,-117.47,-74.03,-116.22,-74.24,-115.02,-74.07,-113.94,-73.71,-113.3,-74.03,-112.95,-74.38,-112.3,-74.71,-111.26,-74.42,-110.07,-74.79,-108.71,-74.91,-107.56,-75.18,-106.15,-75.13,-104.88,-74.95,-103.37,-74.99,-102.02,-75.13,-100.65,-75.3,-100.12,-74.87,-100.76,-74.54,-101.25,-74.19,-102.55,-74.11,-103.11,-73.73,-103.33,-73.36,-103.68,-72.62,-102.92,-72.75,-101.61,-72.81,-100.31,-72.75,-99.14,-72.91,-98.12,-73.21,-97.69,-73.56,-96.34,-73.62,-95.04,-73.48,-93.67,-73.28,-92.44,-73.17,-91.42,-73.4,-90.09,-73.32,-89.23,-72.56,-88.42,-73.01,-87.27,-73.19,-86.01,-73.09,-85.19,-73.48,-83.88,-73.52,-82.67,-73.64,-81.47,-73.85,-80.69,-73.48,-80.3,-73.13,-79.3,-73.52,-77.93,-73.42,-76.91,-73.64,-76.22,-73.97,-74.89,-73.87,-73.85,-73.66,-72.83,-73.4,-71.62,-73.26,-70.21,-73.15,-68.94,-73.01,-67.96,-72.79,-67.37,-72.48,-67.13,-72.05,-67.25,-71.64,-67.56,-71.25,-67.92,-70.85,-68.23,-70.46,-68.49,-70.11,-68.54,-69.72,-68.45,-69.33,-67.98,-68.95,-67.58,-68.54,-67.43,-68.15,-67.62,-67.72,-67.74,-67.33,-67.25,-66.88,-66.7,-66.58,-66.06,-66.21,-65.37,-65.9,-64.57,-65.6,-64.18,-65.17,-63.63,-64.9,-63,-64.64,-62.04,-64.58,-61.41,-64.27,-60.71,-64.07,-59.89,-63.96,-59.16,-63.7,-58.59,-63.39,-57.81,-63.27,-57.22,-63.53,-57.6,-63.86,-58.61,-64.15,-59.05,-64.37,-59.79,-64.21,-60.61,-64.31,-61.3,-64.54,-62.02,-64.8,-62.51,-65.09,-62.65,-65.48,-62.59,-65.86,-62.12,-66.19,-62.81,-66.43,-63.75,-66.5,-64.29,-66.84,-64.88,-67.15,-65.51,-67.58,-65.67,-67.95,-65.31,-68.37,-64.78,-68.68,-63.96,-68.91,-63.2,-69.23,-62.79,-69.62,-62.57,-69.99,-62.28,-70.38,-61.81,-70.72,-61.51,-71.09,-61.38,-72.01,-61.08,-72.38,-61,-72.77,-60.69,-73.17,-60.83,-73.7,-61.38,-74.11,-61.96,-74.44,-63.3,-74.58,-63.75,-74.93,-64.35,-75.26,-65.86,-75.64,-67.19,-75.79,-68.45,-76.01,-69.8,-76.22,-70.6,-76.63,-72.21,-76.67,-73.97,-76.63,-75.56,-76.71,-77.24,-76.71,-76.93,-77.1,-75.4,-77.28,-74.28,-77.56,-73.66,-77.91,-74.77,-78.22,-76.5,-78.12,-77.93,-78.38,-77.98,-78.79,-78.02,-79.18,-76.85,-79.51,-76.63,-79.89,-75.36,-80.26,-73.24,-80.42,-71.44,-80.69,-70.01,-81,-68.19,-81.32,-65.7,-81.47,-63.26,-81.75,-61.55,-82.04,-59.69,-82.38,-58.71,-82.85,-58.22,-83.22,-57.01,-82.87,-55.36,-82.57,-53.62,-82.26,-51.54,-82,-49.76,-81.73,-47.27,-81.71,-44.83,-81.85,-42.81,-82.08,-42.16,-81.65,-40.77,-81.36,-38.24,-81.34,-36.27,-81.12,-34.39,-80.91,-32.31,-80.77,-30.1,-80.59,-28.55,-80.34,-29.25,-79.99,-29.69,-79.63,-29.69,-79.26,-31.62,-79.3,-33.68,-79.46,-35.64,-79.46,-35.91,-79.08,-35.78,-78.34,-35.33,-78.12,-33.9,-77.89,-32.21,-77.65,-31,-77.36,-29.78,-77.07,-28.88,-76.67,-27.51,-76.5,-26.16,-76.36,-25.47,-76.28,-23.93,-76.24,-22.46,-76.11,-21.22,-75.91,-20.01,-75.67,-18.91,-75.44,-17.52,-75.13,-16.64,-74.79,-15.7,-74.5,-15.41,-74.11,-16.47,-73.87,-16.11,-73.46,-15.45,-73.15,-14.41,-72.95,-13.31,-72.72,-12.29,-72.4,-11.51,-72.01,-11.02,-71.54,-10.3,-71.27,-9.1,-71.32,-8.61,-71.66,-7.42,-71.7,-7.38,-71.32,-6.87,-70.93,-5.79,-71.03,-5.54,-71.4,-4.34,-71.46,-3.05,-71.29,-1.8,-71.17,-0.66,-71.23,-0.23,-71.64,0.87,-71.3,1.89,-71.13,3.02,-70.99,4.14,-70.85,5.16,-70.62,6.27,-70.46,7.14,-70.25,7.74,-69.89,8.49,-70.15,9.53,-70.01,10.25,-70.48,10.82,-70.83,11.95,-70.64,12.4,-70.25,13.42,-69.97,14.73,-70.03,15.13,-70.4,15.95,-70.03,17.03,-69.91,18.2,-69.87,19.26,-69.89,20.38,-70.01,21.45,-70.07,21.92,-70.4,22.57,-70.7,23.67,-70.52,24.84,-70.48,25.98,-70.48,27.09,-70.46,28.09,-70.32,29.15,-70.21,30.03,-69.93,30.97,-69.76,31.99,-69.66,32.75,-69.38,33.3,-68.84,33.87,-68.5,34.91,-68.66,35.3,-69.01,36.16,-69.25,37.2,-69.17,37.91,-69.52,38.65,-69.78,39.67,-69.54,40.02,-69.11,40.92,-68.93,41.96,-68.6,42.94,-68.46,44.11,-68.27,44.9,-68.05,45.72,-67.82,46.5,-67.6,47.44,-67.72,48.34,-67.37,48.99,-67.09,49.93,-67.11,50.75,-66.88,50.95,-66.52,51.79,-66.25,52.61,-66.05,53.61,-65.9,54.53,-65.82,55.41,-65.88,56.36,-65.97,57.16,-66.25,57.26,-66.68,58.14,-67.01,58.74,-67.29,59.94,-67.41,60.61,-67.68,61.43,-67.95,62.39,-68.01,63.19,-67.82,64.05,-67.41,64.99,-67.62,65.97,-67.74,66.91,-67.86,67.89,-67.93,68.89,-67.93,69.71,-68.97,69.67,-69.23,69.56,-69.68,68.6,-69.93,67.81,-70.31,67.95,-70.7,69.07,-70.68,68.93,-71.07,68.42,-71.44,67.95,-71.85,68.71,-72.17,69.87,-72.26,71.02,-72.09,71.57,-71.7,71.91,-71.32,72.45,-71.01,73.08,-70.72,73.34,-70.36,73.86,-69.87,74.49,-69.78,75.63,-69.74,76.63,-69.62,77.64,-69.46,78.13,-69.07,78.43,-68.7,79.11,-68.33,80.09,-68.07,80.94,-67.88,81.48,-67.54,82.05,-67.37,82.78,-67.21,83.78,-67.31,84.68,-67.21,85.66,-67.09,86.75,-67.15,87.48,-66.88,87.99,-66.21,88.36,-66.48,88.83,-66.95,89.67,-67.15,90.63,-67.23,91.59,-67.11,92.61,-67.19,93.55,-67.21,94.18,-67.11,95.02,-67.17,95.78,-67.39,96.68,-67.25,97.76,-67.25,98.68,-67.11,99.72,-67.25,100.38,-66.92,100.89,-66.58,101.58,-66.31,102.83,-65.56,103.48,-65.7,104.24,-65.97,104.91,-66.33,106.18,-66.93,107.16,-66.95,108.08,-66.95,109.16,-66.84,110.24,-66.7,111.06,-66.43,111.74,-66.13,112.86,-66.09,113.6,-65.88,114.39,-66.07,114.9,-66.39,115.6,-66.7,116.7,-66.66,117.38,-66.92,118.58,-67.17,119.83,-67.27,120.87,-67.19,121.65,-66.88,122.32,-66.56,123.22,-66.48,124.12,-66.62,125.16,-66.72,126.1,-66.56,127,-66.56,127.88,-66.66,128.8,-66.76,129.7,-66.58,130.78,-66.43,131.8,-66.39,132.94,-66.39,133.86,-66.29,134.76,-66.21,135.03,-65.72,135.07,-65.31,135.7,-65.58,135.87,-66.03,136.21,-66.45,136.62,-66.78,137.46,-66.95,138.6,-66.9,139.91,-66.88,140.81,-66.82,142.12,-66.82,143.06,-66.8,144.37,-66.84,145.49,-66.92,146.2,-67.23,146,-67.6,146.65,-67.9,147.72,-68.13,148.84,-68.39,150.13,-68.56,151.48,-68.72,152.5,-68.87,153.64,-68.89,154.28,-68.56,155.17,-68.84,155.93,-69.15,156.81,-69.38,158.03,-69.48,159.18,-69.6,159.67,-69.99,160.81,-70.23,161.57,-70.58,162.69,-70.74,163.84,-70.72,164.92,-70.78,166.11,-70.76,167.31,-70.83,168.43,-70.97,169.46,-71.21,170.5,-71.4,171.21,-71.7,171.09,-72.09,170.56,-72.44,170.11,-72.89,169.76,-73.24,169.29,-73.66,167.98,-73.81,167.39,-74.17,166.09,-74.38,165.64,-74.77,164.96,-75.15,164.23,-75.46,163.82,-75.87,163.57,-76.24,163.47,-76.69,163.49,-77.07,164.06,-77.46,164.27,-77.83,164.74,-78.18,166.6,-78.32,167,-78.75,165.19,-78.91,163.67,-79.12,161.77,-79.16,160.92,-79.73,160.75,-80.2,160.32,-80.57,159.79,-80.95,161.12,-81.28,161.63,-81.69,162.49,-82.06,163.71,-82.4,165.1,-82.71,166.6,-83.02,168.9,-83.34,169.4,-83.83,172.28,-84.04,172.48,-84.12,173.22,-84.41,175.99,-84.16,178.28,-84.47,180,-84.71]]]},{"n":"Turkish Republic of Northern Cyprus","c":"","r":6,"ml":6,"lx":33.69,"ly":35.22,"b":[32.73,35,34.58,35.67],"p":[[[32.73,35.14,32.8,35.15,32.95,35.39,33.67,35.37,34.58,35.67,33.9,35.25,33.97,35.06,33.87,35.09,33.68,35.02,33.53,35.04,33.48,35,33.46,35.1,33.38,35.16,33.19,35.17,32.92,35.09,32.73,35.14]]]},{"n":"Cyprus","c":"CY","r":5,"ml":4.5,"lx":33.08,"ly":34.91,"b":[32.26,34.57,34,35.17],"p":[[[32.73,35.14,32.92,35.09,33.19,35.17,33.38,35.16,33.46,35.1,33.48,35,33.53,35.04,33.68,35.02,33.87,35.09,33.97,35.06,34,34.98,32.98,34.57,32.49,34.7,32.26,35.1,32.73,35.14]]]},{"n":"Morocco","c":"MA","r":3,"ml":2.7,"lx":-7.19,"ly":31.65,"b":[-17.02,21.42,-1.12,35.76],"p":[[[-2.17,35.17,-1.79,34.53,-1.73,33.92,-1.39,32.86,-1.12,32.65,-1.31,32.26,-2.62,32.09,-3.07,31.72,-3.65,31.64,-3.69,30.9,-4.86,30.5,-5.24,30,-6.06,29.73,-7.06,29.58,-8.67,28.84,-8.67,27.66,-8.82,27.66,-8.79,27.12,-9.41,27.09,-9.74,26.86,-10.19,26.86,-10.55,26.99,-11.39,26.88,-11.72,26.1,-12.03,26.03,-12.5,24.77,-13.89,23.69,-14.22,22.31,-14.63,21.86,-14.75,21.5,-17,21.42,-17.02,21.42,-16.97,21.89,-16.59,22.16,-16.26,22.68,-16.33,23.02,-15.98,23.72,-15.43,24.36,-15.09,24.52,-14.82,25.1,-14.8,25.64,-14.44,26.25,-13.77,26.62,-13.14,27.64,-13.12,27.65,-12.62,28.04,-11.69,28.15,-10.9,28.83,-10.4,29.1,-9.56,29.93,-9.81,31.18,-9.43,32.04,-9.3,32.56,-8.66,33.24,-7.65,33.7,-6.91,34.11,-6.24,35.15,-5.93,35.76,-5.19,35.76,-4.59,35.33,-3.64,35.4,-2.6,35.18,-2.17,35.17]]]},{"n":"Egypt","c":"EG","r":2,"ml":1.7,"lx":29.45,"ly":26.19,"b":[24.7,22,36.87,31.59],"p":[[[36.87,22,32.9,22,29.02,22,25,22,25,25.68,25,29.24,24.7,30.04,24.96,30.66,24.8,31.09,25.16,31.57,26.5,31.59,27.46,31.32,28.45,31.03,28.91,30.87,29.68,31.19,30.1,31.47,30.98,31.56,31.69,31.43,31.96,30.93,32.19,31.26,32.99,31.02,33.77,30.97,34.27,31.22,34.82,29.76,34.92,29.5,34.64,29.1,34.43,28.34,34.15,27.82,33.92,27.65,33.59,27.97,33.14,28.42,32.42,29.85,32.32,29.76,32.73,28.71,33.35,27.7,34.1,26.14,34.47,25.6,34.8,25.03,35.69,23.93,35.49,23.75,35.53,23.1,36.69,22.2,36.87,22]]]},{"n":"Libya","c":"LY","r":3,"ml":3,"lx":18.01,"ly":26.64,"b":[9.32,19.58,25.16,33.14],"p":[[[25,22,25,20,23.85,20,23.84,19.58,19.85,21.5,15.86,23.41,14.85,22.86,14.14,22.49,13.58,23.04,12,23.47,11.56,24.1,10.77,24.56,10.3,24.38,9.95,24.94,9.91,25.37,9.32,26.09,9.72,26.51,9.63,27.14,9.76,27.69,9.68,28.14,9.86,28.96,9.81,29.42,9.48,30.31,9.97,30.54,10.06,30.96,9.95,31.38,10.64,31.76,10.94,32.08,11.43,32.37,11.49,33.14,12.66,32.79,13.08,32.88,13.92,32.71,15.25,32.27,15.71,31.38,16.61,31.18,18.02,30.76,19.09,30.27,19.57,30.53,20.05,30.99,19.82,31.75,20.13,32.24,20.85,32.71,21.54,32.84,22.9,32.64,23.24,32.19,23.61,32.19,23.93,32.02,24.92,31.9,25.16,31.57,24.8,31.09,24.96,30.66,24.7,30.04,25,29.24,25,25.68,25,22]]]},{"n":"Ethiopia","c":"ET","r":2,"ml":2,"lx":39.09,"ly":8.03,"b":[32.95,3.42,47.79,14.96],"p":[[[47.79,8,44.96,5,43.66,4.96,42.77,4.25,42.13,4.23,41.86,3.92,41.17,3.92,40.77,4.26,39.85,3.84,39.56,3.42,38.89,3.5,38.67,3.62,38.44,3.59,38.12,3.6,36.86,4.45,36.16,4.45,35.82,4.78,35.82,5.34,35.3,5.51,34.71,6.59,34.25,6.83,34.08,7.23,33.57,7.71,32.95,7.78,33.29,8.35,33.83,8.38,33.97,8.68,33.96,9.58,34.26,10.63,34.73,10.91,34.83,11.32,35.26,12.08,35.86,12.58,36.27,13.56,36.43,14.42,37.59,14.21,37.91,14.96,38.51,14.51,39.1,14.74,39.34,14.53,40.03,14.52,40.9,14.12,41.16,13.77,41.6,13.45,42.01,12.87,42.35,12.54,42,12.1,41.66,11.63,41.74,11.36,41.76,11.05,42.31,11.03,42.55,11.11,42.78,10.93,42.56,10.57,42.93,10.02,43.3,9.54,43.68,9.18,46.95,8,47.79,8]]]},{"n":"Djibouti","c":"DJ","r":5,"ml":4,"lx":42.5,"ly":11.98,"b":[41.66,10.93,43.32,12.7],"p":[[[42.35,12.54,42.78,12.46,43.08,12.7,43.32,12.39,43.29,11.97,42.72,11.74,43.15,11.46,42.78,10.93,42.55,11.11,42.31,11.03,41.76,11.05,41.74,11.36,41.66,11.63,42,12.1,42.35,12.54]]]},{"n":"Somaliland","c":"","r":5,"ml":4.5,"lx":46.73,"ly":9.44,"b":[42.56,8,48.95,11.46],"p":[[[48.95,11.41,48.94,11.39,48.94,10.98,48.94,9.97,48.94,9.45,48.49,8.84,47.79,8,46.95,8,43.68,9.18,43.3,9.54,42.93,10.02,42.56,10.57,42.78,10.93,43.15,11.46,43.47,11.28,43.67,10.86,44.12,10.45,44.61,10.44,45.56,10.7,46.65,10.82,47.53,11.13,48.02,11.19,48.38,11.38,48.95,11.41]]]},{"n":"Uganda","c":"UG","r":3,"ml":3,"lx":32.95,"ly":1.97,"b":[29.58,-1.44,35.04,4.25],"p":[[[33.9,-0.95,31.87,-1.03,30.77,-1.01,30.42,-1.13,29.82,-1.44,29.58,-1.34,29.59,-0.59,29.82,-0.21,29.88,0.6,30.09,1.06,30.47,1.58,30.85,1.85,31.17,2.2,30.77,2.34,30.83,3.51,31.25,3.78,31.88,3.56,32.69,3.79,33.39,3.79,34.01,4.25,34.48,3.56,34.6,3.05,35.04,1.91,34.67,1.18,34.18,0.52,33.89,0.11,33.9,-0.95]]]},{"n":"Rwanda","c":"RW","r":3,"ml":3,"lx":30.1,"ly":-1.9,"b":[29.02,-2.92,30.82,-1.13],"p":[[[30.42,-1.13,30.82,-1.7,30.76,-2.29,30.47,-2.41,29.94,-2.35,29.63,-2.92,29.02,-2.84,29.12,-2.29,29.25,-2.22,29.29,-1.62,29.58,-1.34,29.82,-1.44,30.42,-1.13]]]},{"n":"Bosnia and Herzegovina","c":"BA","r":5,"ml":4.5,"lx":18.07,"ly":44.09,"b":[15.75,42.65,19.6,45.23],"p":[[[18.56,42.65,17.67,43.03,17.3,43.45,16.92,43.67,16.46,44.04,16.24,44.35,15.75,44.82,15.96,45.23,16.32,45,16.53,45.21,17,45.23,17.86,45.07,18.55,45.08,19.01,44.86,19.37,44.86,19.12,44.42,19.6,44.04,19.45,43.57,19.22,43.52,19.03,43.43,18.71,43.2,18.56,42.65]]]},{"n":"North Macedonia","c":"MK","r":6,"ml":5,"lx":21.56,"ly":41.56,"b":[20.46,40.84,22.95,42.32],"p":[[[22.38,42.32,22.88,42,22.95,41.34,22.76,41.3,22.6,41.13,22.06,41.15,21.67,40.93,21.02,40.84,20.61,41.09,20.46,41.52,20.59,41.86,20.72,41.85,20.76,42.05,21.35,42.21,21.58,42.25,21.92,42.3,22.38,42.32]]]},{"n":"Serbia","c":"RS","r":5,"ml":4,"lx":20.79,"ly":44.19,"b":[18.83,42.25,22.99,46.17],"p":[[[18.83,45.91,19.6,46.17,20.22,46.13,20.76,45.73,20.87,45.42,21.48,45.18,21.56,44.77,22.15,44.48,22.46,44.7,22.71,44.58,22.47,44.41,22.66,44.23,22.41,44.01,22.5,43.64,22.99,43.21,22.6,42.9,22.44,42.58,22.55,42.46,22.38,42.32,21.92,42.3,21.58,42.25,21.54,42.32,21.66,42.44,21.78,42.68,21.63,42.68,21.44,42.86,21.27,42.91,21.14,43.07,20.96,43.13,20.81,43.27,20.64,43.22,20.5,42.88,20.26,42.81,20.34,42.9,19.96,43.11,19.63,43.21,19.48,43.35,19.22,43.52,19.45,43.57,19.6,44.04,19.12,44.42,19.37,44.86,19.01,44.86,19.39,45.24,19.07,45.52,18.83,45.91]]]},{"n":"Montenegro","c":"ME","r":6,"ml":5,"lx":19.14,"ly":42.8,"b":[18.45,41.88,20.34,43.52],"p":[[[20.07,42.59,19.8,42.5,19.74,42.69,19.3,42.2,19.37,41.88,19.16,41.96,18.88,42.28,18.45,42.48,18.56,42.65,18.71,43.2,19.03,43.43,19.22,43.52,19.48,43.35,19.63,43.21,19.96,43.11,20.34,42.9,20.26,42.81,20.07,42.59]]]},{"n":"Kosovo","c":"XK","r":6,"ml":5,"lx":20.86,"ly":42.59,"b":[20.07,41.85,21.78,43.27],"p":[[[20.59,41.86,20.52,42.22,20.28,42.32,20.07,42.59,20.26,42.81,20.5,42.88,20.64,43.22,20.81,43.27,20.96,43.13,21.14,43.07,21.27,42.91,21.44,42.86,21.63,42.68,21.78,42.68,21.66,42.44,21.54,42.32,21.58,42.25,21.35,42.21,20.76,42.05,20.72,41.85,20.59,41.86]]]},{"n":"Trinidad and Tobago","c":"TT","r":5,"ml":4.5,"lx":-60.92,"ly":11,"b":[-61.95,10,-60.89,10.89],"p":[[[-61.68,10.76,-61.1,10.89,-60.89,10.86,-60.93,10.11,-61.77,10,-61.95,10.09,-61.66,10.37,-61.68,10.76]]]},{"n":"South Sudan","c":"SS","r":3,"ml":3,"lx":30.39,"ly":7.23,"b":[23.89,3.51,35.3,12.25],"p":[[[30.83,3.51,29.95,4.17,29.72,4.6,29.16,4.39,28.7,4.46,28.43,4.29,27.98,4.41,27.37,5.23,27.21,5.55,26.47,5.95,26.21,6.55,25.8,6.98,25.12,7.5,25.11,7.83,24.57,8.23,23.89,8.62,24.19,8.73,24.54,8.92,24.79,9.81,25.07,10.27,25.79,10.41,25.96,10.14,26.48,9.55,26.75,9.47,27.11,9.64,27.83,9.6,27.97,9.4,28.97,9.4,29,9.6,29.52,9.79,29.62,10.08,30,10.29,30.84,9.71,31.35,9.81,31.85,10.53,32.4,11.08,32.31,11.68,32.07,11.97,32.67,12.02,32.74,12.25,33.21,12.18,33.09,11.44,33.21,10.72,33.72,10.33,33.84,9.98,33.82,9.48,33.96,9.46,33.97,8.68,33.83,8.38,33.29,8.35,32.95,7.78,33.57,7.71,34.08,7.23,34.25,6.83,34.71,6.59,35.3,5.51,34.62,4.85,34.01,4.25,33.39,3.79,32.69,3.79,31.88,3.56,31.25,3.78,30.83,3.51]]]}]} \ No newline at end of file diff --git a/src/maps/data/countries_50m.json b/src/maps/data/countries_50m.json new file mode 100644 index 0000000..9681f4f --- /dev/null +++ b/src/maps/data/countries_50m.json @@ -0,0 +1 @@ +{"f":[{"n":"Zimbabwe","c":"ZW","r":3,"ml":2.5,"lx":29.93,"ly":-18.91,"b":[25.224,-22.402,33.007,-15.643],"p":[[[31.288,-22.402,31.197,-22.345,31.073,-22.308,30.916,-22.291,30.712,-22.298,30.46,-22.329,30.19,-22.291,29.902,-22.184,29.663,-22.146,29.377,-22.193,29.365,-22.194,29.315,-22.158,29.237,-22.079,29.107,-22.066,29.071,-22.047,29.042,-22.018,29.023,-21.981,29.016,-21.94,29.037,-21.811,29.026,-21.797,28.991,-21.781,28.919,-21.766,28.748,-21.708,28.532,-21.651,28.182,-21.589,28.046,-21.573,28.014,-21.554,27.975,-21.507,27.907,-21.359,27.844,-21.262,27.693,-21.111,27.669,-21.064,27.677,-20.945,27.688,-20.848,27.704,-20.766,27.697,-20.69,27.695,-20.595,27.7,-20.531,27.679,-20.503,27.625,-20.484,27.469,-20.475,27.281,-20.479,27.275,-20.382,27.257,-20.232,27.221,-20.146,27.178,-20.101,27.092,-20.054,26.917,-19.99,26.678,-19.893,26.475,-19.749,26.241,-19.569,26.168,-19.538,26.082,-19.37,25.951,-19.082,25.959,-18.986,25.939,-18.939,25.812,-18.797,25.784,-18.724,25.761,-18.649,25.558,-18.442,25.489,-18.351,25.437,-18.235,25.384,-18.142,25.34,-18.104,25.282,-18.041,25.242,-17.969,25.224,-17.915,25.239,-17.843,25.259,-17.794,25.452,-17.845,25.557,-17.85,25.64,-17.824,25.742,-17.858,25.863,-17.952,25.996,-17.97,26.14,-17.912,26.333,-17.929,26.578,-18.023,26.78,-18.042,27.021,-17.958,27.236,-17.728,27.438,-17.512,27.637,-17.262,27.757,-17.06,27.932,-16.896,28.164,-16.77,28.4,-16.663,28.761,-16.532,28.833,-16.424,28.857,-16.306,28.857,-16.142,28.876,-16.036,28.913,-15.988,28.973,-15.95,29.051,-15.901,29.288,-15.776,29.487,-15.697,29.73,-15.645,29.995,-15.644,30.251,-15.643,30.396,-15.643,30.398,-15.801,30.409,-15.978,30.438,-15.995,30.63,-15.999,30.939,-16.012,31.236,-16.024,31.426,-16.152,31.49,-16.18,31.688,-16.214,31.94,-16.429,32.243,-16.449,32.452,-16.516,32.636,-16.589,32.742,-16.678,32.81,-16.698,32.903,-16.704,32.948,-16.712,32.938,-16.776,32.876,-16.884,32.884,-17.038,32.969,-17.252,32.981,-17.437,32.955,-17.765,32.956,-18.083,32.965,-18.196,32.979,-18.271,32.996,-18.313,32.993,-18.36,32.942,-18.493,32.902,-18.633,32.9,-18.689,32.885,-18.729,32.854,-18.764,32.722,-18.828,32.699,-18.868,32.7,-18.941,32.717,-19.002,32.766,-19.024,32.826,-19.059,32.85,-19.104,32.85,-19.152,32.831,-19.241,32.778,-19.389,32.831,-19.558,32.89,-19.668,32.973,-19.795,33.007,-19.874,33.005,-19.93,32.993,-19.985,32.87,-20.217,32.781,-20.362,32.673,-20.516,32.529,-20.613,32.492,-20.66,32.478,-20.713,32.483,-20.829,32.476,-20.95,32.354,-21.137,32.43,-21.297,32.412,-21.312,32.371,-21.335,32.195,-21.515,32.016,-21.698,31.886,-21.832,31.738,-21.983,31.571,-22.154,31.429,-22.299,31.288,-22.402]]]},{"n":"Zambia","c":"ZM","r":3,"ml":3,"lx":26.4,"ly":-14.66,"b":[21.979,-18.042,33.662,-8.194],"p":[[[30.396,-15.643,30.251,-15.643,29.995,-15.644,29.73,-15.645,29.487,-15.697,29.288,-15.776,29.051,-15.901,28.973,-15.95,28.913,-15.988,28.876,-16.036,28.857,-16.142,28.857,-16.306,28.833,-16.424,28.761,-16.532,28.4,-16.663,28.164,-16.77,27.932,-16.896,27.757,-17.06,27.637,-17.262,27.438,-17.512,27.236,-17.728,27.021,-17.958,26.78,-18.042,26.578,-18.023,26.333,-17.929,26.14,-17.912,25.996,-17.97,25.863,-17.952,25.742,-17.858,25.64,-17.824,25.557,-17.85,25.452,-17.845,25.259,-17.794,25.092,-17.634,25.002,-17.569,24.932,-17.543,24.733,-17.518,24.275,-17.481,24.227,-17.49,24.037,-17.521,23.799,-17.56,23.595,-17.599,23.381,-17.641,23.182,-17.474,22.956,-17.286,22.722,-17.075,22.546,-16.91,22.459,-16.815,22.305,-16.69,22.194,-16.628,22.151,-16.597,22.04,-16.263,21.98,-15.956,21.98,-15.724,21.98,-15.403,21.98,-15.082,21.979,-14.761,21.979,-14.441,21.979,-14.12,21.979,-13.799,21.979,-13.478,21.979,-13.157,21.979,-13.001,22.21,-13.001,22.471,-13.001,22.744,-13.001,23.042,-13.001,23.339,-13.001,23.636,-13.001,23.843,-13.001,23.897,-12.998,23.963,-12.988,23.968,-12.957,23.882,-12.799,23.887,-12.743,23.909,-12.636,23.945,-12.544,23.991,-12.422,23.996,-12.351,23.959,-12.118,23.962,-11.988,23.973,-11.853,23.984,-11.725,23.971,-11.636,23.987,-11.587,24.015,-11.518,24.029,-11.439,24.047,-11.405,24.041,-11.374,24.026,-11.316,24.01,-11.185,23.988,-11.003,23.967,-10.872,24.003,-10.879,24.078,-10.892,24.115,-10.956,24.137,-11.026,24.187,-11.03,24.32,-11.072,24.366,-11.13,24.396,-11.255,24.378,-11.319,24.335,-11.371,24.378,-11.417,24.467,-11.448,24.519,-11.438,24.668,-11.353,24.728,-11.338,24.806,-11.321,24.877,-11.299,25.076,-11.26,25.185,-11.243,25.246,-11.212,25.289,-11.212,25.319,-11.237,25.292,-11.325,25.283,-11.405,25.321,-11.554,25.349,-11.623,25.413,-11.674,25.46,-11.7,25.512,-11.753,25.619,-11.744,25.855,-11.82,25.927,-11.855,26.026,-11.89,26.096,-11.903,26.34,-11.93,26.43,-11.948,26.596,-11.972,26.73,-11.976,26.824,-11.965,26.89,-11.944,26.931,-11.919,26.95,-11.899,26.977,-11.825,27.027,-11.664,27.046,-11.616,27.095,-11.594,27.159,-11.579,27.196,-11.605,27.238,-11.783,27.424,-11.945,27.487,-12.08,27.533,-12.195,27.574,-12.227,27.644,-12.267,27.757,-12.281,27.857,-12.285,28.069,-12.368,28.237,-12.435,28.358,-12.482,28.413,-12.518,28.451,-12.577,28.474,-12.623,28.511,-12.742,28.551,-12.836,28.615,-12.854,28.673,-12.861,28.73,-12.925,28.773,-12.982,28.859,-13.119,28.922,-13.215,28.942,-13.307,29.014,-13.369,29.112,-13.395,29.202,-13.398,29.254,-13.371,29.382,-13.323,29.481,-13.268,29.554,-13.249,29.597,-13.261,29.63,-13.299,29.648,-13.373,29.652,-13.414,29.723,-13.454,29.775,-13.438,29.795,-13.393,29.796,-13.37,29.796,-13.167,29.796,-12.992,29.796,-12.827,29.796,-12.626,29.796,-12.451,29.795,-12.306,29.795,-12.155,29.75,-12.164,29.692,-12.198,29.56,-12.202,29.508,-12.228,29.492,-12.267,29.502,-12.318,29.505,-12.386,29.486,-12.418,29.428,-12.431,29.344,-12.405,29.191,-12.37,29.064,-12.349,28.973,-12.258,28.85,-12.121,28.769,-12.051,28.575,-11.908,28.542,-11.879,28.483,-11.812,28.432,-11.698,28.407,-11.623,28.383,-11.567,28.357,-11.483,28.404,-11.354,28.47,-11.11,28.518,-10.933,28.544,-10.802,28.639,-10.669,28.646,-10.55,28.607,-10.397,28.617,-10.313,28.624,-10.099,28.629,-9.919,28.63,-9.831,28.604,-9.679,28.541,-9.51,28.4,-9.275,28.401,-9.225,28.484,-9.169,28.617,-9.072,28.681,-9.015,28.759,-8.933,28.794,-8.891,28.87,-8.786,28.918,-8.701,28.934,-8.59,28.898,-8.485,28.972,-8.465,29.216,-8.428,29.484,-8.387,29.766,-8.344,30.051,-8.3,30.328,-8.258,30.578,-8.22,30.751,-8.194,30.777,-8.266,30.831,-8.386,30.892,-8.474,30.968,-8.551,31.033,-8.598,31.076,-8.612,31.351,-8.607,31.449,-8.654,31.535,-8.713,31.556,-8.805,31.613,-8.863,31.674,-8.909,31.7,-8.914,31.745,-8.903,31.818,-8.902,31.886,-8.922,31.919,-8.942,31.922,-9.019,31.943,-9.054,32.035,-9.067,32.13,-9.073,32.221,-9.126,32.319,-9.135,32.433,-9.156,32.487,-9.213,32.608,-9.271,32.757,-9.322,32.863,-9.381,32.92,-9.407,32.923,-9.434,32.951,-9.484,32.98,-9.52,32.982,-9.574,32.996,-9.623,33.038,-9.635,33.072,-9.638,33.104,-9.603,33.148,-9.604,33.196,-9.626,33.213,-9.683,33.25,-9.76,33.31,-9.812,33.351,-9.862,33.337,-9.954,33.312,-10.038,33.394,-10.121,33.5,-10.2,33.529,-10.235,33.538,-10.352,33.554,-10.391,33.626,-10.489,33.662,-10.553,33.659,-10.591,33.465,-10.783,33.403,-10.802,33.345,-10.813,33.293,-10.852,33.261,-10.893,33.273,-10.915,33.293,-10.981,33.339,-11.085,33.38,-11.158,33.346,-11.249,33.268,-11.404,33.233,-11.418,33.226,-11.535,33.25,-11.578,33.288,-11.611,33.304,-11.691,33.305,-11.8,33.301,-11.888,33.252,-12.113,33.34,-12.308,33.37,-12.33,33.491,-12.331,33.512,-12.348,33.483,-12.403,33.431,-12.46,33.398,-12.49,33.243,-12.557,33.022,-12.63,32.975,-12.701,32.946,-12.804,32.971,-12.865,33,-12.9,32.99,-12.989,32.971,-13.084,32.978,-13.159,32.968,-13.225,32.939,-13.257,32.9,-13.357,32.852,-13.457,32.814,-13.503,32.758,-13.55,32.67,-13.59,32.672,-13.61,32.772,-13.657,32.797,-13.688,32.807,-13.71,32.785,-13.731,32.765,-13.761,32.811,-13.792,32.867,-13.817,32.92,-13.884,32.968,-13.977,32.981,-14.009,32.992,-14.022,33.009,-14.024,33.042,-14.01,33.104,-13.959,33.148,-13.941,33.202,-14.013,32.987,-14.085,32.875,-14.122,32.553,-14.23,32.273,-14.323,32.2,-14.341,32.054,-14.387,31.982,-14.414,31.729,-14.496,31.623,-14.537,31.538,-14.577,31.329,-14.638,31.131,-14.695,30.915,-14.753,30.673,-14.819,30.538,-14.867,30.446,-14.908,30.232,-14.99,30.222,-15.011,30.225,-15.067,30.252,-15.183,30.306,-15.289,30.351,-15.35,30.38,-15.506,30.396,-15.643]]]},{"n":"Yemen","c":"YE","r":3,"ml":3,"lx":45.87,"ly":15.33,"b":[42.549,12.319,54.511,18.996],"p":[[[53.086,16.648,52.581,16.47,52.448,16.391,52.328,16.294,52.237,16.171,52.174,15.957,52.222,15.761,52.217,15.656,52.087,15.586,51.966,15.536,51.831,15.459,51.749,15.44,51.682,15.379,51.604,15.337,51.322,15.226,51.015,15.141,50.527,15.038,50.339,14.927,50.167,14.851,49.906,14.828,49.549,14.722,49.35,14.638,49.103,14.5,49.048,14.456,49.005,14.355,48.929,14.267,48.78,14.124,48.668,14.05,48.594,14.046,48.449,14.006,48.278,13.998,47.99,14.048,47.916,14.013,47.855,13.957,47.633,13.858,47.408,13.662,47.243,13.609,46.976,13.547,46.789,13.466,46.663,13.433,46.502,13.416,46.203,13.424,45.92,13.394,45.657,13.339,45.534,13.233,45.394,13.067,45.164,12.998,45.11,12.939,45.039,12.816,44.89,12.784,44.755,12.764,44.618,12.817,44.358,12.669,44.26,12.645,44.112,12.639,44.006,12.608,43.93,12.617,43.835,12.674,43.634,12.744,43.488,12.699,43.475,12.839,43.232,13.267,43.283,13.64,43.282,13.693,43.234,13.859,43.089,14.011,43.093,14.204,43.045,14.342,43.006,14.483,43.019,14.521,43.021,14.555,42.947,14.773,42.922,14.817,42.913,14.863,42.937,14.898,42.936,14.939,42.897,15.006,42.856,15.133,42.658,15.233,42.698,15.326,42.736,15.294,42.788,15.266,42.799,15.326,42.8,15.372,42.717,15.655,42.84,16.032,42.799,16.372,42.986,16.509,43.034,16.55,43.061,16.587,43.105,16.664,43.165,16.689,43.186,16.771,43.184,16.812,43.146,16.847,43.117,16.942,43.126,17.062,43.136,17.113,43.156,17.205,43.221,17.239,43.237,17.266,43.186,17.325,43.191,17.359,43.302,17.457,43.346,17.486,43.418,17.516,43.474,17.516,43.539,17.499,43.597,17.471,43.653,17.422,43.713,17.366,43.804,17.344,43.866,17.35,43.917,17.325,43.96,17.338,44.008,17.367,44.086,17.366,44.156,17.399,44.355,17.414,44.546,17.404,44.747,17.432,44.946,17.43,45.148,17.427,45.193,17.423,45.237,17.406,45.407,17.32,45.535,17.302,45.794,17.278,46.071,17.253,46.31,17.231,46.513,17.252,46.682,17.269,46.728,17.266,46.779,17.212,46.88,17.079,46.976,16.953,47.144,16.947,47.251,16.994,47.37,17.06,47.442,17.112,47.525,17.316,47.58,17.448,47.704,17.597,47.808,17.721,47.946,17.886,48.022,17.977,48.172,18.157,48.316,18.227,48.593,18.362,48.865,18.495,49.042,18.582,49.192,18.621,49.445,18.655,49.742,18.695,50.039,18.735,50.355,18.778,50.708,18.825,50.95,18.858,51.258,18.899,51.515,18.934,51.743,18.965,51.978,18.996,52.022,18.896,52.066,18.796,52.11,18.696,52.155,18.597,52.199,18.497,52.243,18.397,52.288,18.297,52.332,18.197,52.376,18.097,52.42,17.997,52.465,17.897,52.509,17.798,52.553,17.698,52.597,17.598,52.642,17.498,52.686,17.398,52.729,17.3,52.801,17.268,52.843,17.176,52.904,17.044,52.964,16.912,53.025,16.78,53.086,16.648]],[[42.59,15.303,42.559,15.281,42.549,15.32,42.57,15.407,42.602,15.433,42.625,15.368,42.61,15.332,42.59,15.303]],[[42.756,13.704,42.69,13.674,42.735,13.753,42.781,13.769,42.794,13.766,42.756,13.704]],[[42.787,13.971,42.774,13.95,42.756,13.955,42.694,14.008,42.762,14.067,42.798,14.012,42.787,13.971]],[[53.763,12.637,53.825,12.625,53.919,12.659,54.187,12.664,54.511,12.553,54.45,12.523,54.414,12.483,54.271,12.447,54.129,12.361,53.719,12.319,53.598,12.342,53.499,12.425,53.316,12.533,53.388,12.602,53.404,12.633,53.431,12.664,53.535,12.716,53.638,12.707,53.763,12.637]]]},{"n":"Vietnam","c":"VN","r":2,"ml":2,"lx":105.39,"ly":21.72,"b":[102.127,8.583,109.445,23.345],"p":[[[104.064,10.391,104.083,10.341,104.076,10.225,104.037,10.111,104.048,10.061,104.018,10.029,103.952,10.243,103.868,10.335,103.85,10.371,103.898,10.369,103.986,10.427,104.028,10.428,104.064,10.391]],[[107.973,21.508,107.926,21.499,107.809,21.497,107.707,21.406,107.637,21.368,107.527,21.336,107.41,21.285,107.376,21.194,107.373,21.128,107.354,21.055,107.165,20.949,107.112,20.96,107.075,20.999,107.019,20.991,106.981,20.971,106.936,20.974,106.886,20.95,106.821,20.958,106.76,20.991,106.725,21,106.683,21,106.675,20.96,106.737,20.806,106.753,20.735,106.551,20.527,106.573,20.392,106.518,20.289,106.396,20.206,106.166,19.992,106.062,19.987,105.984,19.939,105.814,19.587,105.812,19.467,105.785,19.379,105.791,19.294,105.716,19.128,105.639,19.057,105.622,18.966,105.732,18.779,105.744,18.746,105.808,18.646,105.839,18.574,105.888,18.502,106.066,18.316,106.145,18.259,106.24,18.221,106.412,18.053,106.499,17.946,106.459,17.874,106.479,17.72,106.356,17.765,106.371,17.747,106.517,17.663,106.736,17.367,106.926,17.221,107.12,17.056,107.18,16.898,107.355,16.794,107.549,16.643,107.541,16.609,107.593,16.568,107.724,16.488,107.803,16.403,107.834,16.322,107.882,16.31,107.936,16.329,107.991,16.337,108.029,16.331,108.088,16.243,108.17,16.164,108.209,16.091,108.241,16.101,108.267,16.09,108.274,16.029,108.286,15.989,108.395,15.872,108.447,15.763,108.578,15.585,108.674,15.484,108.743,15.427,108.821,15.378,108.898,15.181,108.94,15.001,109.022,14.803,109.085,14.716,109.087,14.553,109.137,14.384,109.191,14.27,109.207,14.154,109.224,14.097,109.245,14.053,109.303,13.856,109.288,13.765,109.247,13.855,109.252,13.591,109.288,13.451,109.272,13.279,109.31,13.219,109.377,13.025,109.424,12.956,109.42,12.719,109.445,12.6,109.381,12.671,109.336,12.752,109.274,12.709,109.219,12.646,109.305,12.391,109.207,12.415,109.216,12.073,109.256,11.993,109.259,11.955,109.247,11.909,109.22,11.959,109.215,12.01,109.199,11.999,109.2,11.972,109.167,11.912,109.158,11.837,109.193,11.773,109.199,11.725,109.173,11.665,109.133,11.601,109.04,11.593,109.018,11.468,108.987,11.336,108.821,11.315,108.7,11.199,108.551,11.156,108.419,11.041,108.272,10.934,108.176,10.92,108.095,10.897,108.001,10.72,107.845,10.7,107.564,10.555,107.47,10.486,107.384,10.459,107.262,10.398,107.235,10.42,107.194,10.472,107.088,10.498,107.036,10.556,107.021,10.631,107.007,10.661,106.984,10.618,106.966,10.441,106.947,10.4,106.902,10.383,106.813,10.433,106.727,10.536,106.606,10.465,106.643,10.456,106.698,10.462,106.741,10.444,106.778,10.376,106.776,10.339,106.757,10.296,106.644,10.289,106.492,10.304,106.464,10.298,106.602,10.232,106.729,10.193,106.785,10.151,106.785,10.116,106.714,10.06,106.659,9.991,106.658,9.949,106.657,9.901,106.596,9.86,106.557,9.868,106.449,9.94,106.136,10.222,106.184,10.142,106.507,9.821,106.564,9.716,106.572,9.641,106.539,9.604,106.484,9.559,106.378,9.556,106.204,9.675,105.926,9.962,105.831,10.001,106.113,9.674,106.159,9.594,106.206,9.502,106.193,9.448,106.168,9.397,105.501,9.093,105.401,8.962,105.322,8.801,105.191,8.711,105.114,8.629,104.892,8.583,104.77,8.598,104.896,8.747,104.819,8.802,104.815,9.185,104.845,9.606,104.903,9.816,104.987,9.869,105.093,9.901,105.095,9.945,105.084,9.996,105.028,10.067,104.966,10.101,104.873,10.115,104.802,10.203,104.748,10.199,104.663,10.17,104.613,10.208,104.594,10.267,104.516,10.34,104.426,10.411,104.467,10.422,104.514,10.463,104.564,10.516,104.69,10.523,104.815,10.521,104.851,10.534,104.901,10.59,104.984,10.662,105.046,10.702,105.061,10.734,105.036,10.809,105.022,10.887,105.046,10.911,105.159,10.898,105.284,10.861,105.315,10.845,105.387,10.94,105.406,10.952,105.453,10.951,105.577,10.969,105.698,10.994,105.755,10.99,105.811,10.926,105.853,10.864,105.875,10.858,105.938,10.885,105.99,10.852,106.099,10.797,106.164,10.795,106.132,10.922,106.168,11.012,106.161,11.037,106.1,11.079,105.892,11.245,105.856,11.294,105.861,11.372,105.854,11.487,105.835,11.559,105.838,11.601,105.851,11.635,105.89,11.648,105.927,11.653,105.956,11.682,106.006,11.758,106.103,11.751,106.239,11.708,106.34,11.682,106.399,11.687,106.413,11.698,106.411,11.738,106.418,11.912,106.414,11.948,106.5,11.966,106.631,11.969,106.7,11.979,106.765,12.052,106.931,12.077,107.051,12.176,107.159,12.277,107.212,12.304,107.28,12.322,107.33,12.319,107.393,12.26,107.446,12.296,107.506,12.365,107.538,12.432,107.555,12.54,107.544,12.706,107.512,12.836,107.482,12.933,107.475,13.03,107.546,13.225,107.605,13.438,107.594,13.522,107.529,13.654,107.462,13.816,107.389,13.993,107.362,14.019,107.343,14.069,107.331,14.127,107.36,14.308,107.364,14.369,107.448,14.451,107.493,14.546,107.535,14.65,107.519,14.705,107.514,14.817,107.525,14.872,107.505,14.916,107.48,14.98,107.496,15.021,107.555,15.057,107.59,15.118,107.634,15.19,107.653,15.255,107.622,15.31,107.564,15.392,107.46,15.466,107.339,15.56,107.279,15.619,107.233,15.678,107.19,15.747,107.166,15.802,107.189,15.839,107.361,15.922,107.392,15.952,107.41,15.998,107.396,16.043,107.35,16.067,107.296,16.084,107.217,16.136,107.07,16.28,107.002,16.312,106.931,16.353,106.893,16.397,106.851,16.516,106.832,16.526,106.792,16.49,106.74,16.453,106.696,16.459,106.656,16.493,106.638,16.538,106.594,16.6,106.546,16.651,106.534,16.821,106.526,16.877,106.502,16.954,106.465,16.982,106.426,17.003,106.333,17.144,106.27,17.217,106.006,17.415,105.974,17.447,105.903,17.529,105.779,17.644,105.691,17.738,105.627,17.834,105.598,17.918,105.588,17.984,105.519,18.077,105.458,18.154,105.4,18.179,105.333,18.19,105.273,18.235,105.163,18.339,105.115,18.405,105.086,18.45,105.087,18.496,105.113,18.573,105.145,18.617,105.146,18.651,105.115,18.679,104.993,18.728,104.717,18.803,104.613,18.861,104.518,18.934,104.446,18.984,104.109,19.196,104.006,19.231,103.918,19.269,103.892,19.305,103.896,19.34,103.932,19.366,104.028,19.42,104.063,19.483,104.052,19.564,104.013,19.646,104.032,19.675,104.063,19.678,104.127,19.681,104.26,19.685,104.546,19.611,104.588,19.619,104.743,19.755,104.802,19.836,104.815,19.904,104.846,19.947,104.928,20.018,104.929,20.083,104.889,20.169,104.848,20.202,104.813,20.217,104.699,20.205,104.677,20.225,104.662,20.289,104.656,20.329,104.619,20.375,104.496,20.414,104.392,20.425,104.368,20.441,104.408,20.486,104.479,20.53,104.533,20.555,104.575,20.6,104.583,20.647,104.53,20.688,104.461,20.734,104.35,20.821,104.195,20.914,104.101,20.946,104.052,20.941,103.882,20.861,103.791,20.81,103.714,20.717,103.635,20.697,103.555,20.738,103.464,20.78,103.211,20.841,103.104,20.892,102.884,21.203,102.851,21.266,102.872,21.338,102.888,21.44,102.91,21.506,102.949,21.57,102.959,21.626,102.95,21.681,102.918,21.713,102.876,21.722,102.845,21.735,102.816,21.807,102.798,21.798,102.771,21.71,102.739,21.678,102.695,21.662,102.662,21.676,102.641,21.711,102.631,21.771,102.61,21.852,102.583,21.904,102.488,21.958,102.443,22.027,102.301,22.178,102.183,22.284,102.127,22.379,102.176,22.415,102.237,22.466,102.302,22.546,102.376,22.647,102.406,22.708,102.428,22.733,102.471,22.751,102.517,22.741,102.599,22.7,102.721,22.648,102.83,22.587,102.874,22.525,102.935,22.466,102.982,22.448,103.005,22.453,103.076,22.498,103.136,22.542,103.138,22.593,103.193,22.639,103.266,22.714,103.301,22.764,103.327,22.77,103.356,22.755,103.471,22.597,103.493,22.588,103.525,22.612,103.571,22.734,103.62,22.782,103.637,22.77,103.915,22.538,103.942,22.54,103.971,22.55,103.991,22.586,104.013,22.666,104.054,22.752,104.143,22.8,104.213,22.809,104.238,22.769,104.298,22.712,104.372,22.704,104.527,22.804,104.578,22.82,104.632,22.818,104.687,22.822,104.74,22.86,104.796,22.911,104.815,23.011,104.827,23.1,104.865,23.136,104.91,23.161,104.996,23.194,105.189,23.281,105.239,23.322,105.275,23.345,105.35,23.308,105.44,23.235,105.495,23.181,105.531,23.122,105.548,23.073,105.691,23.03,105.782,22.969,105.843,22.923,105.903,22.925,105.962,22.937,106.001,22.975,106.068,22.976,106.148,22.97,106.184,22.955,106.249,22.869,106.279,22.857,106.338,22.863,106.451,22.894,106.542,22.908,106.624,22.874,106.78,22.779,106.736,22.711,106.702,22.638,106.633,22.586,106.582,22.573,106.55,22.501,106.536,22.395,106.554,22.342,106.593,22.325,106.637,22.289,106.654,22.241,106.66,22.136,106.658,22.018,106.664,21.979,106.698,21.986,106.729,22,106.794,21.982,106.875,21.951,106.925,21.92,106.971,21.924,107.006,21.893,107.02,21.835,107.062,21.794,107.179,21.717,107.272,21.711,107.351,21.609,107.433,21.642,107.471,21.598,107.641,21.614,107.759,21.655,107.802,21.645,107.908,21.56,107.973,21.508]],[[106.617,8.683,106.589,8.681,106.568,8.701,106.659,8.766,106.65,8.723,106.653,8.701,106.617,8.683]],[[107.168,10.397,107.084,10.337,107.078,10.388,107.151,10.42,107.177,10.446,107.195,10.446,107.168,10.397]],[[106.866,20.816,106.854,20.796,106.803,20.844,106.769,20.864,106.795,20.928,106.855,20.858,106.866,20.816]],[[107.603,21.217,107.459,21.092,107.404,21.094,107.453,21.235,107.476,21.269,107.563,21.22,107.603,21.217]],[[107.521,20.927,107.466,20.901,107.399,20.903,107.479,20.952,107.519,21.013,107.551,21.034,107.551,20.981,107.521,20.927]],[[107.031,20.747,106.99,20.743,106.911,20.824,106.953,20.867,107.044,20.837,107.064,20.817,107.064,20.8,107.042,20.761,107.031,20.747]]]},{"n":"Venezuela","c":"VE","r":3,"ml":2.5,"lx":-64.6,"ly":7.18,"b":[-73.366,0.688,-59.829,12.178],"p":[[[-60.821,9.138,-60.941,9.106,-60.939,9.132,-60.907,9.179,-60.845,9.192,-60.821,9.208,-60.782,9.218,-60.759,9.216,-60.736,9.203,-60.79,9.177,-60.821,9.138]],[[-63.849,11.131,-63.817,11,-63.827,10.976,-63.918,10.888,-63.994,10.881,-64.055,10.884,-64.101,10.901,-64.161,10.959,-64.219,10.942,-64.362,10.962,-64.402,10.982,-64.349,11.052,-64.25,11.08,-64.214,11.086,-64.185,11.043,-64.113,11.006,-64.028,11.002,-64.007,11.068,-63.893,11.167,-63.849,11.131]],[[-65.213,10.906,-65.266,10.884,-65.365,10.906,-65.415,10.938,-65.383,10.974,-65.302,10.974,-65.227,10.93,-65.213,10.906]],[[-60.998,8.867,-61.06,8.847,-61.069,8.947,-61.05,8.974,-60.945,9.055,-60.916,9.07,-60.895,9.053,-60.9,9.032,-60.849,8.996,-60.861,8.95,-60.916,8.899,-60.998,8.867]],[[-60.018,8.549,-59.832,8.306,-59.829,8.279,-59.849,8.249,-59.965,8.192,-59.991,8.162,-60.032,8.054,-60.178,7.994,-60.279,7.919,-60.347,7.854,-60.381,7.828,-60.514,7.813,-60.556,7.772,-60.61,7.648,-60.649,7.597,-60.719,7.536,-60.719,7.499,-60.624,7.363,-60.607,7.321,-60.636,7.257,-60.633,7.211,-60.583,7.156,-60.523,7.144,-60.465,7.167,-60.392,7.165,-60.345,7.15,-60.325,7.134,-60.322,7.092,-60.352,7.003,-60.395,6.945,-60.586,6.857,-60.671,6.806,-60.718,6.768,-60.821,6.788,-60.873,6.787,-60.914,6.758,-60.938,6.733,-61.007,6.727,-61.105,6.711,-61.146,6.695,-61.177,6.651,-61.204,6.588,-61.182,6.513,-61.151,6.447,-61.152,6.385,-61.129,6.214,-61.159,6.174,-61.225,6.129,-61.303,6.05,-61.391,5.939,-61.377,5.907,-61.167,5.674,-60.954,5.437,-60.742,5.202,-60.712,5.192,-60.672,5.164,-60.635,5.082,-60.604,4.995,-60.604,4.949,-60.628,4.893,-60.679,4.827,-60.742,4.774,-60.833,4.729,-60.906,4.687,-60.966,4.575,-61.003,4.535,-61.036,4.519,-61.102,4.505,-61.209,4.508,-61.28,4.517,-61.368,4.433,-61.479,4.402,-61.554,4.288,-61.821,4.197,-62.082,4.126,-62.153,4.098,-62.411,4.157,-62.473,4.139,-62.544,4.084,-62.61,4.042,-62.665,4.04,-62.712,4.018,-62.74,3.94,-62.765,3.673,-62.857,3.593,-62.969,3.594,-63.045,3.686,-63.136,3.756,-63.295,3.922,-63.339,3.944,-63.38,3.943,-63.527,3.894,-63.597,3.915,-63.653,3.941,-63.747,3.933,-63.915,3.931,-64.021,3.929,-64.073,3.974,-64.122,4.067,-64.154,4.1,-64.192,4.127,-64.256,4.14,-64.526,4.14,-64.576,4.14,-64.614,4.158,-64.666,4.237,-64.722,4.274,-64.789,4.276,-64.818,4.232,-64.703,4.089,-64.669,4.012,-64.568,3.9,-64.275,3.663,-64.221,3.587,-64.227,3.491,-64.229,3.344,-64.219,3.205,-64.144,3.005,-64.038,2.802,-64.009,2.672,-64.029,2.576,-64.049,2.525,-64.047,2.502,-64.025,2.482,-63.924,2.452,-63.713,2.434,-63.585,2.434,-63.389,2.412,-63.375,2.34,-63.394,2.223,-63.433,2.156,-63.464,2.136,-63.57,2.121,-63.682,2.048,-63.844,1.977,-63.937,1.967,-63.976,1.953,-64.008,1.932,-64.035,1.904,-64.067,1.771,-64.115,1.619,-64.205,1.529,-64.304,1.455,-64.405,1.447,-64.486,1.453,-64.526,1.431,-64.584,1.37,-64.667,1.294,-64.732,1.253,-64.818,1.257,-64.91,1.22,-65.027,1.158,-65.104,1.108,-65.17,1.022,-65.264,0.932,-65.361,0.869,-65.407,0.79,-65.473,0.691,-65.556,0.688,-65.563,0.748,-65.523,0.843,-65.566,0.926,-65.645,0.97,-65.681,0.983,-65.718,0.978,-65.811,0.937,-65.926,0.863,-65.996,0.81,-66.06,0.785,-66.191,0.763,-66.302,0.752,-66.347,0.767,-66.429,0.822,-66.619,0.992,-66.876,1.223,-66.896,1.29,-66.884,1.358,-66.931,1.458,-66.958,1.564,-66.982,1.601,-66.988,1.68,-67.044,1.823,-67.09,1.94,-67.131,2,-67.114,2.051,-67.131,2.101,-67.165,2.143,-67.215,2.275,-67.198,2.333,-67.211,2.39,-67.253,2.429,-67.312,2.472,-67.392,2.56,-67.486,2.644,-67.535,2.677,-67.568,2.69,-67.597,2.769,-67.619,2.794,-67.667,2.8,-67.766,2.833,-67.859,2.794,-67.861,2.855,-67.835,2.893,-67.515,3.187,-67.354,3.323,-67.336,3.343,-67.322,3.374,-67.311,3.416,-67.348,3.464,-67.499,3.691,-67.551,3.734,-67.603,3.769,-67.662,3.864,-67.732,4.087,-67.783,4.198,-67.799,4.284,-67.795,4.381,-67.814,4.455,-67.855,4.507,-67.855,4.665,-67.814,4.931,-67.804,5.133,-67.825,5.27,-67.788,5.375,-67.695,5.448,-67.642,5.559,-67.631,5.709,-67.575,5.833,-67.474,5.93,-67.439,6.026,-67.472,6.12,-67.482,6.18,-67.568,6.242,-67.727,6.285,-67.859,6.29,-67.939,6.242,-68.143,6.198,-68.472,6.157,-68.736,6.157,-68.937,6.198,-69.09,6.184,-69.195,6.115,-69.268,6.1,-69.311,6.138,-69.357,6.148,-69.427,6.124,-69.439,6.135,-69.595,6.321,-69.739,6.494,-69.904,6.7,-70.095,6.938,-70.129,6.954,-70.188,6.952,-70.266,6.948,-70.387,6.973,-70.471,7.007,-70.536,7.041,-70.655,7.083,-70.737,7.09,-70.811,7.078,-71.013,6.994,-71.129,6.987,-71.218,6.985,-71.457,7.026,-71.621,7.033,-71.811,7.006,-71.893,6.99,-72.007,7.033,-72.084,7.097,-72.157,7.25,-72.208,7.37,-72.296,7.395,-72.395,7.415,-72.443,7.455,-72.472,7.524,-72.479,7.613,-72.469,7.758,-72.46,7.81,-72.446,7.966,-72.392,8.048,-72.358,8.087,-72.364,8.153,-72.39,8.287,-72.417,8.382,-72.526,8.49,-72.665,8.628,-72.726,8.848,-72.796,9.109,-72.852,9.135,-72.904,9.122,-72.96,9.135,-73.009,9.24,-73.058,9.26,-73.137,9.223,-73.193,9.194,-73.337,9.168,-73.366,9.194,-73.356,9.227,-73.296,9.322,-73.224,9.444,-73.141,9.555,-73.064,9.668,-73.007,9.789,-72.967,10.03,-72.94,10.196,-72.869,10.491,-72.739,10.727,-72.69,10.836,-72.572,10.977,-72.518,11.054,-72.446,11.114,-72.248,11.196,-72.012,11.602,-71.958,11.666,-71.719,11.727,-71.536,11.774,-71.4,11.824,-71.356,11.85,-71.32,11.862,-71.349,11.815,-71.415,11.755,-71.488,11.719,-71.869,11.627,-71.908,11.608,-71.957,11.57,-71.957,11.483,-71.947,11.414,-71.835,11.19,-71.791,11.135,-71.642,11.014,-71.676,10.997,-71.731,10.995,-71.69,10.835,-71.598,10.726,-71.594,10.657,-71.665,10.444,-71.794,10.316,-71.885,10.167,-71.956,10.108,-72.113,9.816,-71.993,9.642,-71.976,9.553,-71.873,9.428,-71.806,9.386,-71.761,9.336,-71.781,9.25,-71.74,9.134,-71.687,9.073,-71.62,9.048,-71.537,9.048,-71.298,9.126,-71.241,9.16,-71.205,9.222,-71.086,9.348,-71.078,9.511,-71.053,9.706,-71.082,9.833,-71.207,10.015,-71.262,10.144,-71.387,10.264,-71.463,10.469,-71.494,10.533,-71.518,10.622,-71.545,10.779,-71.461,10.836,-71.47,10.964,-71.264,11,-70.821,11.208,-70.546,11.261,-70.233,11.373,-70.16,11.428,-70.097,11.52,-70.049,11.53,-69.885,11.444,-69.805,11.474,-69.773,11.541,-69.817,11.672,-69.911,11.672,-70.193,11.625,-70.22,11.681,-70.22,11.73,-70.287,11.886,-70.245,12.004,-70.203,12.098,-70.122,12.137,-70.004,12.178,-69.914,12.115,-69.86,12.054,-69.831,11.996,-69.811,11.837,-69.762,11.676,-69.712,11.564,-69.632,11.48,-69.57,11.485,-69.526,11.5,-69.233,11.518,-69.055,11.461,-68.828,11.432,-68.616,11.309,-68.399,11.161,-68.343,11.053,-68.325,10.949,-68.272,10.88,-68.325,10.809,-68.296,10.689,-68.234,10.569,-68.14,10.493,-67.872,10.472,-67.581,10.524,-67.133,10.57,-66.989,10.611,-66.247,10.632,-66.106,10.575,-66.092,10.517,-66.09,10.473,-65.852,10.258,-65.656,10.228,-65.489,10.159,-65.317,10.122,-65.129,10.07,-65.023,10.077,-64.944,10.095,-64.85,10.098,-64.188,10.458,-63.834,10.449,-63.779,10.472,-63.732,10.503,-63.863,10.558,-64.158,10.579,-64.248,10.543,-64.298,10.635,-64.202,10.633,-63.873,10.664,-63.497,10.643,-63.19,10.709,-63.035,10.72,-62.947,10.707,-62.702,10.75,-62.242,10.7,-61.879,10.741,-61.921,10.681,-62.04,10.645,-62.233,10.634,-62.38,10.547,-62.694,10.563,-62.914,10.531,-62.843,10.507,-62.843,10.418,-62.813,10.4,-62.781,10.399,-62.706,10.333,-62.686,10.29,-62.662,10.199,-62.695,10.1,-62.741,10.056,-62.651,10.071,-62.6,10.117,-62.608,10.163,-62.6,10.217,-62.55,10.2,-62.515,10.176,-62.401,9.918,-62.32,9.783,-62.3,9.788,-62.281,9.793,-62.257,9.819,-62.221,9.883,-62.19,9.842,-62.172,9.827,-62.153,9.822,-62.17,9.879,-62.147,9.953,-62.155,9.979,-62.12,9.985,-62.077,9.975,-62.017,9.955,-61.909,9.87,-61.837,9.782,-61.831,9.733,-61.805,9.706,-61.759,9.677,-61.736,9.631,-61.732,9.702,-61.759,9.754,-61.766,9.814,-61.625,9.816,-61.589,9.895,-61.512,9.848,-61.309,9.633,-61.234,9.598,-61.013,9.556,-60.874,9.453,-60.792,9.361,-60.841,9.264,-60.971,9.215,-61.023,9.155,-61.053,9.095,-61.054,9.035,-61.093,8.966,-61.099,8.941,-61.122,8.843,-61.176,8.725,-61.247,8.6,-61.619,8.597,-61.527,8.546,-61.443,8.509,-61.304,8.41,-61.194,8.488,-61.036,8.493,-60.865,8.579,-60.801,8.592,-60.481,8.547,-60.404,8.61,-60.34,8.629,-60.167,8.617,-60.018,8.549]]]},{"n":"Vatican City","c":"VA","r":6,"ml":5,"lx":12.45,"ly":41.9,"b":[12.428,41.898,12.439,41.906],"p":[[[12.439,41.898,12.431,41.898,12.428,41.901,12.431,41.905,12.438,41.906,12.439,41.898]]]},{"n":"Vanuatu","c":"VU","r":4,"ml":4,"lx":166.91,"ly":-15.37,"b":[166.526,-20.242,169.896,-13.709],"p":[[[166.746,-14.827,166.81,-15.157,166.885,-15.157,166.923,-15.139,166.968,-15.062,166.987,-14.94,167.027,-14.923,167.076,-14.936,167.054,-14.974,167.069,-15.072,167.106,-15.126,167.132,-15.135,167.182,-15.39,167.201,-15.443,167.2,-15.486,167.094,-15.581,166.937,-15.578,166.826,-15.635,166.758,-15.631,166.759,-15.567,166.699,-15.516,166.631,-15.406,166.648,-15.212,166.527,-14.85,166.526,-14.76,166.567,-14.642,166.608,-14.637,166.663,-14.735,166.746,-14.827]],[[167.413,-16.096,167.459,-16.118,167.484,-16.118,167.499,-16.166,167.642,-16.263,167.681,-16.261,167.714,-16.314,167.776,-16.341,167.793,-16.395,167.837,-16.45,167.76,-16.516,167.611,-16.499,167.526,-16.574,167.449,-16.555,167.436,-16.515,167.447,-16.502,167.401,-16.401,167.38,-16.246,167.349,-16.154,167.316,-16.116,167.246,-16.15,167.218,-16.155,167.151,-16.08,167.183,-15.929,167.2,-15.885,167.254,-15.877,167.336,-15.917,167.413,-16.096]],[[168.447,-16.779,168.477,-16.794,168.46,-16.835,168.323,-16.788,168.212,-16.806,168.181,-16.804,168.149,-16.766,168.124,-16.69,168.135,-16.637,168.182,-16.6,168.199,-16.594,168.234,-16.64,168.265,-16.671,168.296,-16.684,168.366,-16.759,168.447,-16.779]],[[168.297,-16.337,168.182,-16.347,168.022,-16.316,167.957,-16.272,167.929,-16.229,167.985,-16.196,168.064,-16.181,168.164,-16.082,168.198,-16.12,168.235,-16.231,168.276,-16.265,168.298,-16.299,168.297,-16.337]],[[168.446,-17.542,168.545,-17.685,168.585,-17.696,168.525,-17.798,168.399,-17.807,168.252,-17.781,168.306,-17.746,168.278,-17.706,168.233,-17.698,168.182,-17.717,168.158,-17.711,168.191,-17.645,168.273,-17.552,168.297,-17.545,168.32,-17.544,168.341,-17.552,168.446,-17.542]],[[167.911,-15.436,167.844,-15.482,167.72,-15.477,167.674,-15.452,167.826,-15.312,168.003,-15.283,167.911,-15.436]],[[168.213,-15.97,168.196,-15.972,168.179,-15.926,168.123,-15.681,168.16,-15.462,168.183,-15.508,168.268,-15.892,168.256,-15.955,168.213,-15.97]],[[168.189,-15.329,168.172,-15.391,168.13,-15.319,168.104,-15.017,168.115,-14.989,168.136,-14.986,168.187,-15.197,168.189,-15.329]],[[167.219,-15.724,167.201,-15.75,167.095,-15.685,167.119,-15.623,167.234,-15.645,167.219,-15.724]],[[167.585,-14.261,167.543,-14.312,167.43,-14.295,167.404,-14.282,167.411,-14.197,167.439,-14.168,167.506,-14.142,167.599,-14.184,167.585,-14.261]],[[167.489,-13.907,167.474,-13.917,167.451,-13.909,167.392,-13.788,167.407,-13.748,167.481,-13.709,167.547,-13.777,167.554,-13.814,167.553,-13.846,167.543,-13.873,167.499,-13.885,167.489,-13.907]],[[169.896,-20.187,169.861,-20.242,169.807,-20.241,169.738,-20.202,169.751,-20.153,169.829,-20.145,169.852,-20.148,169.896,-20.187]],[[169.491,-19.54,169.438,-19.649,169.347,-19.624,169.262,-19.545,169.217,-19.476,169.247,-19.345,169.291,-19.322,169.337,-19.329,169.36,-19.458,169.491,-19.54]],[[169.334,-18.94,169.288,-18.989,169.248,-18.983,168.987,-18.871,168.998,-18.825,168.987,-18.708,169.016,-18.644,169.088,-18.617,169.144,-18.631,169.178,-18.725,169.256,-18.763,169.201,-18.796,169.296,-18.867,169.334,-18.94]]]},{"n":"Uzbekistan","c":"UZ","r":3,"ml":3,"lx":64.01,"ly":41.69,"b":[55.976,37.172,73.137,45.555],"p":[[[70.947,42.249,70.979,42.267,71.036,42.285,71.13,42.25,71.213,42.206,71.232,42.186,71.229,42.163,71.032,42.078,70.91,42.038,70.857,42.031,70.842,42.02,70.803,41.923,70.728,41.905,70.631,41.875,70.563,41.831,70.455,41.725,70.181,41.571,70.177,41.54,70.201,41.514,70.29,41.497,70.408,41.45,70.471,41.413,70.646,41.46,70.689,41.45,70.734,41.401,70.782,41.263,70.86,41.225,70.963,41.196,71.026,41.187,71.111,41.153,71.223,41.14,71.299,41.152,71.393,41.123,71.408,41.136,71.421,41.342,71.5,41.307,71.546,41.308,71.586,41.333,71.606,41.367,71.62,41.435,71.602,41.503,71.638,41.534,71.665,41.541,71.685,41.533,71.697,41.516,71.701,41.454,71.758,41.428,71.792,41.413,71.826,41.361,71.858,41.311,71.879,41.195,71.958,41.187,72.052,41.165,72.115,41.187,72.164,41.174,72.181,41.118,72.181,41.067,72.187,41.026,72.213,41.014,72.295,41.04,72.364,41.043,72.427,41.019,72.506,40.982,72.62,40.884,72.658,40.87,72.831,40.862,72.867,40.842,72.926,40.842,72.99,40.86,73.132,40.829,73.137,40.811,73.113,40.786,72.774,40.65,72.749,40.609,72.68,40.556,72.604,40.525,72.567,40.524,72.402,40.578,72.383,40.565,72.369,40.543,72.37,40.52,72.406,40.463,72.389,40.427,72.358,40.402,72.254,40.424,72.235,40.439,72.233,40.454,72.193,40.454,72.131,40.439,72.013,40.341,71.971,40.29,71.956,40.259,71.903,40.241,71.845,40.234,71.773,40.188,71.692,40.152,71.667,40.179,71.651,40.208,71.63,40.217,71.58,40.21,71.52,40.209,71.457,40.242,71.376,40.275,71.305,40.287,71.095,40.271,70.991,40.255,70.958,40.239,70.899,40.235,70.653,40.201,70.603,40.214,70.566,40.267,70.534,40.325,70.47,40.345,70.398,40.361,70.372,40.384,70.37,40.412,70.377,40.439,70.383,40.454,70.549,40.563,70.698,40.661,70.712,40.669,70.726,40.688,70.751,40.722,70.751,40.74,70.639,40.779,70.635,40.797,70.657,40.815,70.657,40.84,70.578,40.911,70.442,41.023,70.402,41.035,70.373,41.028,70.319,40.919,70.292,40.892,70.136,40.82,70.006,40.771,69.773,40.684,69.713,40.657,69.671,40.662,69.628,40.679,69.498,40.767,69.414,40.797,69.357,40.767,69.309,40.724,69.314,40.635,69.26,40.588,69.206,40.567,69.304,40.327,69.294,40.297,69.22,40.288,69.275,40.198,69.228,40.188,69.11,40.209,68.952,40.223,68.653,40.183,68.631,40.167,68.622,40.147,68.64,40.129,68.785,40.127,68.927,40.136,68.966,40.12,68.972,40.09,68.956,40.071,68.908,40.068,68.805,40.05,68.793,40.031,68.789,40.013,68.824,39.961,68.864,39.927,68.869,39.907,68.852,39.891,68.832,39.884,68.798,39.909,68.778,39.904,68.768,39.882,68.758,39.856,68.735,39.836,68.687,39.846,68.639,39.839,68.61,39.743,68.586,39.635,68.507,39.563,68.463,39.537,68.399,39.529,68.303,39.538,68.245,39.548,68.077,39.564,67.909,39.594,67.719,39.621,67.542,39.558,67.492,39.519,67.46,39.482,67.426,39.466,67.35,39.242,67.358,39.217,67.4,39.197,67.617,39.15,67.648,39.131,67.667,39.109,67.677,39.008,67.694,38.995,67.769,38.982,67.876,38.983,67.96,38.993,68.044,38.984,68.104,38.962,68.133,38.928,68.149,38.891,68.048,38.669,68.056,38.589,68.087,38.474,68.144,38.383,68.251,38.295,68.333,38.238,68.35,38.211,68.354,38.17,68.341,38.117,68.294,38.033,68.237,37.96,68.174,37.928,68.088,37.835,68.011,37.721,67.864,37.571,67.814,37.487,67.798,37.245,67.759,37.172,67.753,37.2,67.7,37.227,67.607,37.223,67.546,37.236,67.517,37.267,67.442,37.258,67.32,37.21,67.196,37.235,67.069,37.335,66.828,37.371,66.522,37.348,66.511,37.459,66.511,37.599,66.526,37.786,66.629,37.932,66.626,37.96,66.606,37.987,66.575,38.011,66.39,38.051,66.335,38.072,66.264,38.118,66.173,38.167,66.095,38.2,65.971,38.244,65.857,38.269,65.79,38.25,65.729,38.226,65.671,38.226,65.613,38.239,65.4,38.349,65.077,38.539,64.821,38.672,64.66,38.736,64.622,38.756,64.532,38.816,64.31,38.977,64.163,38.954,63.953,39.058,63.764,39.161,63.721,39.188,63.506,39.377,63.292,39.5,63.058,39.633,62.907,39.717,62.651,39.858,62.525,39.944,62.483,39.976,62.442,40.036,62.375,40.332,62.298,40.467,62.188,40.541,62.095,40.683,62.018,40.894,61.954,41.031,61.903,41.094,61.8,41.163,61.645,41.24,61.497,41.276,61.444,41.275,61.417,41.265,61.388,41.252,61.329,41.195,61.242,41.189,61.179,41.191,61.12,41.211,60.933,41.229,60.867,41.249,60.755,41.246,60.514,41.216,60.455,41.222,60.2,41.349,60.09,41.399,60.067,41.427,60.069,41.476,60.106,41.545,60.138,41.594,60.124,41.645,60.076,41.701,60.076,41.76,60.109,41.793,60.176,41.782,60.201,41.803,60.192,41.834,60.156,41.857,60.107,41.907,59.963,41.954,59.942,41.974,59.949,41.995,59.974,42.019,59.979,42.068,59.982,42.132,60.001,42.165,60.006,42.191,59.985,42.212,59.937,42.236,59.858,42.295,59.763,42.302,59.451,42.3,59.354,42.323,59.277,42.356,59.199,42.482,59.16,42.511,59.123,42.524,59.036,42.528,58.931,42.54,58.877,42.561,58.73,42.676,58.589,42.778,58.532,42.682,58.477,42.663,58.353,42.672,58.26,42.688,58.206,42.666,58.152,42.628,58.162,42.603,58.204,42.576,58.289,42.527,58.418,42.407,58.477,42.34,58.486,42.317,58.474,42.299,58.457,42.292,58.431,42.292,58.397,42.292,58.377,42.312,58.371,42.347,58.327,42.399,58.283,42.429,58.234,42.448,58.166,42.462,58.075,42.487,58.029,42.488,57.983,42.459,57.946,42.42,57.923,42.335,57.856,42.231,57.814,42.19,57.686,42.165,57.382,42.156,57.291,42.124,57.229,42.084,57.114,41.957,57.034,41.915,56.964,41.857,56.985,41.669,57.018,41.451,57.077,41.39,57.114,41.372,57.119,41.35,57.095,41.331,57.064,41.307,57.018,41.263,56.966,41.265,56.861,41.276,56.774,41.288,56.48,41.301,56.242,41.311,55.977,41.322,55.977,41.552,55.977,41.781,55.977,42.011,55.977,42.24,55.977,42.47,55.977,42.7,55.977,42.929,55.977,43.159,55.976,43.388,55.976,43.618,55.976,43.847,55.976,44.077,55.976,44.306,55.976,44.536,55.976,44.765,55.976,44.995,56.1,45.023,56.258,45.059,56.409,45.094,56.589,45.135,56.792,45.181,56.965,45.221,57.172,45.268,57.329,45.304,57.477,45.337,57.667,45.377,57.961,45.44,58.125,45.474,58.291,45.509,58.449,45.543,58.555,45.555,58.669,45.508,58.807,45.442,58.945,45.376,59.083,45.31,59.221,45.244,59.36,45.179,59.498,45.113,59.636,45.047,59.774,44.981,59.912,44.916,60.05,44.85,60.188,44.784,60.327,44.718,60.465,44.652,60.603,44.587,60.741,44.521,60.879,44.455,61.008,44.394,61.065,44.348,61.097,44.248,61.161,44.169,61.271,44.082,61.385,43.994,61.526,43.877,61.624,43.796,61.723,43.714,61.888,43.577,61.99,43.492,62.072,43.489,62.238,43.51,62.459,43.537,62.634,43.558,62.846,43.584,63.048,43.608,63.207,43.628,63.445,43.613,63.68,43.599,63.848,43.588,64.013,43.578,64.209,43.566,64.318,43.559,64.443,43.551,64.496,43.572,64.604,43.613,64.706,43.653,64.812,43.694,64.905,43.715,65.003,43.649,65.085,43.574,65.171,43.494,65.271,43.418,65.367,43.372,65.496,43.311,65.571,43.205,65.67,43.065,65.736,42.972,65.803,42.877,65.901,42.915,66.006,42.955,66.1,42.991,66.089,42.873,66.079,42.767,66.063,42.605,66.05,42.473,66.016,42.315,66.013,42.194,66.011,42.089,66.01,42.005,66.193,42.001,66.329,41.998,66.499,41.995,66.515,41.889,66.538,41.741,66.573,41.607,66.602,41.494,66.645,41.349,66.669,41.271,66.71,41.179,66.75,41.157,66.814,41.142,67.039,41.153,67.225,41.162,67.372,41.17,67.528,41.177,67.735,41.187,67.805,41.164,67.866,41.18,67.936,41.197,67.991,41.13,68.02,41.096,68.059,41.061,68.113,41.029,68.09,40.96,68.057,40.861,68.048,40.809,68.112,40.754,68.16,40.722,68.292,40.656,68.415,40.619,68.496,40.609,68.573,40.623,68.601,40.66,68.594,40.711,68.557,40.765,68.559,40.829,68.584,40.876,68.663,40.962,68.737,41.042,68.851,41.124,68.987,41.205,69.043,41.264,69.065,41.367,69.154,41.425,69.249,41.46,69.368,41.491,69.401,41.542,69.565,41.629,69.664,41.672,69.788,41.697,69.96,41.754,70.096,41.821,70.226,41.946,70.329,42.028,70.416,42.079,70.489,42.08,70.54,42.039,70.584,42.036,70.613,42.055,70.663,42.107,70.715,42.169,70.765,42.194,70.86,42.207,70.947,42.249],[70.653,40.937,70.649,40.961,70.618,41.002,70.572,41.025,70.55,41.015,70.569,40.982,70.623,40.934,70.653,40.937]],[[71.206,39.893,71.156,39.883,71.064,39.885,71.012,39.895,71.044,39.976,71.045,39.993,71.041,39.995,71.014,40.006,70.974,40.039,70.961,40.088,70.976,40.133,71.005,40.152,71.024,40.149,71.08,40.08,71.13,40.06,71.229,40.048,71.179,39.98,71.216,39.907,71.206,39.893]],[[71.78,39.95,71.753,39.907,71.706,39.917,71.669,39.946,71.681,39.969,71.737,39.981,71.765,39.993,71.79,39.995,71.78,39.95]]]},{"n":"Uruguay","c":"UY","r":4,"ml":3,"lx":-55.97,"ly":-32.96,"b":[-58.438,-34.933,-53.126,-30.101],"p":[[[-53.371,-33.742,-53.42,-33.779,-53.472,-33.849,-53.535,-34.017,-53.743,-34.25,-53.785,-34.38,-54.01,-34.517,-54.169,-34.671,-54.272,-34.667,-54.365,-34.733,-54.902,-34.933,-55.095,-34.895,-55.238,-34.896,-55.371,-34.808,-55.673,-34.776,-55.863,-34.811,-56.118,-34.908,-56.195,-34.906,-56.25,-34.901,-56.388,-34.861,-56.463,-34.775,-56.855,-34.677,-57.171,-34.452,-57.543,-34.448,-57.829,-34.477,-57.873,-34.448,-57.902,-34.39,-57.961,-34.307,-58.207,-34.109,-58.4,-33.912,-58.438,-33.719,-58.411,-33.509,-58.353,-33.26,-58.364,-33.182,-58.292,-33.138,-58.222,-33.129,-58.154,-33.065,-58.093,-32.967,-58.082,-32.894,-58.13,-32.757,-58.162,-32.567,-58.201,-32.472,-58.123,-32.322,-58.12,-32.249,-58.165,-32.185,-58.177,-32.119,-58.156,-32.052,-58.16,-31.987,-58.189,-31.924,-58.167,-31.873,-58.096,-31.832,-58.042,-31.769,-58.007,-31.685,-57.989,-31.621,-57.988,-31.576,-58.01,-31.534,-58.054,-31.495,-58.033,-31.417,-57.948,-31.299,-57.893,-31.195,-57.868,-31.104,-57.87,-31.031,-57.898,-30.975,-57.886,-30.937,-57.834,-30.917,-57.811,-30.859,-57.819,-30.712,-57.873,-30.591,-57.831,-30.495,-57.713,-30.384,-57.651,-30.295,-57.646,-30.227,-57.609,-30.188,-57.552,-30.261,-57.384,-30.281,-57.214,-30.283,-57.187,-30.265,-57.121,-30.144,-57.033,-30.11,-56.937,-30.101,-56.833,-30.107,-56.722,-30.187,-56.407,-30.447,-56.176,-30.628,-56.106,-30.714,-56.045,-30.778,-55.999,-30.837,-56.018,-30.992,-56.016,-31.06,-56.005,-31.079,-55.952,-31.081,-55.874,-31.07,-55.808,-31.037,-55.756,-30.987,-55.706,-30.947,-55.665,-30.925,-55.65,-30.892,-55.627,-30.858,-55.603,-30.851,-55.557,-30.876,-55.45,-30.964,-55.366,-31.046,-55.346,-31.093,-55.313,-31.142,-55.279,-31.184,-55.255,-31.226,-55.174,-31.28,-55.091,-31.314,-55.036,-31.279,-54.896,-31.391,-54.588,-31.485,-54.531,-31.542,-54.478,-31.623,-54.37,-31.745,-54.221,-31.855,-54.1,-31.902,-53.985,-31.928,-53.921,-31.952,-53.877,-31.995,-53.806,-32.04,-53.762,-32.057,-53.747,-32.097,-53.701,-32.186,-53.654,-32.299,-53.602,-32.403,-53.489,-32.503,-53.363,-32.581,-53.231,-32.625,-53.157,-32.68,-53.126,-32.737,-53.214,-32.821,-53.31,-32.927,-53.395,-33.01,-53.483,-33.069,-53.512,-33.109,-53.531,-33.171,-53.53,-33.5,-53.538,-33.623,-53.531,-33.655,-53.519,-33.677,-53.464,-33.71,-53.398,-33.737,-53.371,-33.742]]]},{"n":"Federated States of Micronesia","c":"FM","r":6,"ml":5,"lx":158.23,"ly":6.89,"b":[138.062,5.277,162.993,9.593],"p":[[[162.983,5.326,162.993,5.277,162.93,5.301,162.921,5.318,162.958,5.335,162.983,5.326]],[[138.143,9.501,138.067,9.419,138.062,9.446,138.085,9.495,138.117,9.55,138.147,9.584,138.186,9.593,138.214,9.547,138.183,9.507,138.143,9.501]],[[151.648,7.346,151.639,7.333,151.578,7.338,151.57,7.346,151.575,7.351,151.604,7.357,151.608,7.375,151.593,7.379,151.606,7.389,151.629,7.39,151.643,7.379,151.65,7.363,151.648,7.346]],[[151.881,7.432,151.864,7.427,151.856,7.432,151.86,7.457,151.865,7.466,151.882,7.467,151.911,7.46,151.913,7.454,151.881,7.432]],[[158.315,6.814,158.257,6.791,158.183,6.801,158.161,6.883,158.128,6.905,158.135,6.945,158.186,6.978,158.295,6.951,158.335,6.893,158.309,6.855,158.315,6.814]]]},{"n":"Marshall Islands","c":"MH","r":6,"ml":5,"lx":171.19,"ly":7.08,"b":[166.845,5.8,171.757,11.169],"p":[[[169.635,5.83,169.615,5.8,169.591,5.802,169.612,5.824,169.627,5.856,169.651,5.945,169.7,5.977,169.735,6.014,169.726,5.976,169.673,5.935,169.635,5.83]],[[168.83,7.309,168.815,7.294,168.719,7.303,168.675,7.322,168.679,7.336,168.755,7.322,168.83,7.309]],[[171.577,7.048,171.615,7.027,171.688,7.028,171.757,6.973,171.73,6.977,171.693,7,171.659,7.01,171.614,7.007,171.593,7.016,171.577,7.048]],[[171.102,7.138,171.227,7.087,171.394,7.111,171.367,7.096,171.305,7.081,171.263,7.069,171.235,7.069,171.202,7.074,171.096,7.109,171.036,7.156,171.05,7.172,171.102,7.138]],[[166.89,11.153,166.864,11.146,166.845,11.153,166.859,11.166,166.888,11.169,166.899,11.165,166.89,11.153]]]},{"n":"Northern Mariana Islands","c":"MP","r":6,"ml":5,"lx":145.73,"ly":15.19,"b":[145.152,14.111,145.835,18.807],"p":[[[145.708,18.763,145.678,18.725,145.653,18.753,145.646,18.807,145.69,18.802,145.707,18.79,145.708,18.763]],[[145.712,16.339,145.69,16.332,145.658,16.336,145.636,16.352,145.631,16.378,145.696,16.38,145.72,16.36,145.712,16.339]],[[145.265,14.158,145.215,14.111,145.18,14.121,145.157,14.137,145.152,14.164,145.232,14.189,145.265,14.18,145.265,14.158]],[[145.778,18.079,145.729,18.057,145.789,18.155,145.807,18.173,145.835,18.137,145.778,18.079]],[[145.752,15.133,145.749,15.107,145.698,15.114,145.684,15.125,145.713,15.215,145.786,15.257,145.822,15.265,145.789,15.223,145.782,15.175,145.752,15.133]],[[145.662,14.971,145.621,14.92,145.592,14.999,145.587,15.031,145.625,15.06,145.647,15.059,145.662,14.971]]]},{"n":"United States Virgin Islands","c":"VI","r":6,"ml":5,"lx":-64.78,"ly":17.75,"b":[-65.024,17.702,-64.58,18.385],"p":[[[-64.845,18.33,-64.92,18.321,-65.024,18.368,-64.942,18.385,-64.889,18.374,-64.845,18.33]],[[-64.66,18.354,-64.726,18.328,-64.771,18.332,-64.788,18.341,-64.752,18.372,-64.66,18.354]],[[-64.766,17.794,-64.682,17.75,-64.58,17.75,-64.686,17.706,-64.889,17.702,-64.885,17.772,-64.766,17.794]]]},{"n":"Guam","c":"GU","r":6,"ml":3,"lx":144.7,"ly":13.35,"b":[144.649,13.258,144.941,13.622],"p":[[[144.742,13.259,144.7,13.258,144.663,13.291,144.65,13.313,144.649,13.429,144.79,13.527,144.837,13.622,144.875,13.615,144.91,13.599,144.941,13.57,144.78,13.411,144.742,13.259]]]},{"n":"American Samoa","c":"AS","r":4,"ml":4,"lx":-170.75,"ly":-14.33,"b":[-170.821,-14.36,-170.568,-14.257],"p":[[[-170.726,-14.351,-170.769,-14.36,-170.821,-14.312,-170.721,-14.276,-170.689,-14.257,-170.568,-14.267,-170.64,-14.282,-170.726,-14.351]]]},{"n":"Puerto Rico","c":"PR","r":5,"ml":3,"lx":-66.48,"ly":18.23,"b":[-67.937,17.947,-65.295,18.522],"p":[[[-66.129,18.445,-66.098,18.425,-66.068,18.428,-66.093,18.469,-66.07,18.469,-65.879,18.444,-65.756,18.402,-65.629,18.381,-65.621,18.242,-65.718,18.187,-65.782,18.129,-65.834,18.057,-65.971,17.974,-66.135,17.949,-66.245,17.947,-66.286,17.95,-66.326,17.964,-66.409,17.951,-66.511,17.987,-66.598,17.978,-66.772,17.987,-66.838,17.955,-66.9,17.948,-66.961,17.954,-67.013,17.968,-67.142,17.967,-67.197,17.994,-67.174,18.153,-67.172,18.224,-67.204,18.283,-67.239,18.321,-67.264,18.365,-67.213,18.394,-67.172,18.436,-67.159,18.499,-67.113,18.515,-67.06,18.522,-66.813,18.493,-66.189,18.476,-66.153,18.471,-66.129,18.445]],[[-65.426,18.106,-65.504,18.1,-65.555,18.108,-65.572,18.137,-65.477,18.165,-65.366,18.161,-65.303,18.144,-65.295,18.133,-65.426,18.106]],[[-67.872,18.06,-67.882,18.059,-67.891,18.06,-67.895,18.063,-67.902,18.072,-67.93,18.087,-67.937,18.101,-67.931,18.115,-67.919,18.121,-67.861,18.123,-67.855,18.121,-67.844,18.111,-67.843,18.104,-67.849,18.097,-67.859,18.08,-67.863,18.075,-67.867,18.071,-67.868,18.063,-67.872,18.06]]]},{"n":"United States of America","c":"US","r":2,"ml":1.7,"lx":-97.48,"ly":39.54,"b":[-178.195,18.964,179.78,71.408],"p":[[[-132.747,56.526,-132.758,56.511,-132.885,56.512,-132.931,56.524,-132.948,56.567,-132.936,56.607,-132.907,56.637,-132.871,56.696,-132.843,56.795,-132.656,56.685,-132.599,56.636,-132.568,56.576,-132.634,56.553,-132.714,56.543,-132.747,56.526]],[[-132.78,56.247,-132.831,56.244,-132.891,56.259,-133.035,56.341,-133.038,56.365,-133.017,56.392,-132.935,56.442,-132.902,56.454,-132.706,56.448,-132.643,56.435,-132.629,56.412,-132.632,56.388,-132.653,56.364,-132.658,56.339,-132.647,56.313,-132.669,56.287,-132.78,56.247]],[[-134.313,58.229,-134.32,58.204,-134.456,58.207,-134.594,58.243,-134.662,58.291,-134.648,58.312,-134.52,58.333,-134.399,58.287,-134.313,58.229]],[[-145.119,60.337,-145.15,60.313,-145.238,60.321,-145.284,60.337,-145.128,60.401,-145.102,60.388,-145.119,60.337]],[[-144.566,59.818,-144.614,59.813,-144.542,59.878,-144.445,59.951,-144.354,59.996,-144.236,60.015,-144.249,59.982,-144.403,59.921,-144.566,59.818]],[[-148.022,60.065,-148.074,60.035,-148.272,60.053,-148.231,60.114,-148.08,60.152,-147.914,60.092,-148.022,60.065]],[[-152.021,60.362,-152.069,60.358,-152.004,60.407,-151.96,60.504,-151.899,60.49,-151.887,60.473,-151.987,60.374,-152.021,60.362]],[[-160.329,55.338,-160.343,55.259,-160.481,55.309,-160.517,55.334,-160.493,55.352,-160.362,55.357,-160.329,55.338]],[[-159.362,54.972,-159.394,54.967,-159.421,54.978,-159.458,55.035,-159.462,55.059,-159.39,55.041,-159.363,55,-159.362,54.972]],[[-159.515,55.152,-159.52,55.072,-159.535,55.06,-159.561,55.081,-159.618,55.057,-159.648,55.075,-159.635,55.102,-159.64,55.124,-159.598,55.126,-159.588,55.165,-159.595,55.182,-159.575,55.218,-159.545,55.226,-159.515,55.152]],[[-166.21,53.723,-166.224,53.72,-166.249,53.745,-166.251,53.768,-166.234,53.784,-166.188,53.822,-166.155,53.836,-166.114,53.843,-166.103,53.833,-166.139,53.787,-166.184,53.757,-166.21,53.723]],[[-176.009,51.812,-176.093,51.79,-176.204,51.835,-176.194,51.886,-176.072,51.843,-176.009,51.812]],[[-166.11,66.227,-166.149,66.222,-166.146,66.237,-166.033,66.278,-165.822,66.328,-165.83,66.317,-165.942,66.278,-166.11,66.227]],[[-171.463,63.64,-171.448,63.616,-171.343,63.62,-171.197,63.609,-171.035,63.585,-170.875,63.594,-170.673,63.669,-170.552,63.688,-170.43,63.699,-170.299,63.681,-170.171,63.641,-170.122,63.618,-170.082,63.577,-170.056,63.527,-170.017,63.492,-169.777,63.448,-169.624,63.431,-169.587,63.407,-169.555,63.373,-169.428,63.348,-169.295,63.358,-169.221,63.349,-168.996,63.347,-168.716,63.311,-168.761,63.214,-168.852,63.171,-169.109,63.185,-169.365,63.171,-169.471,63.121,-169.559,63.058,-169.571,62.997,-169.623,62.969,-169.676,62.956,-169.72,62.99,-169.778,63.094,-169.819,63.122,-169.863,63.14,-169.988,63.173,-170.115,63.194,-170.19,63.196,-170.243,63.232,-170.273,63.284,-170.324,63.311,-170.424,63.349,-170.527,63.379,-170.848,63.444,-170.954,63.453,-171.061,63.446,-171.176,63.416,-171.287,63.372,-171.401,63.339,-171.519,63.332,-171.632,63.351,-171.738,63.394,-171.791,63.425,-171.819,63.477,-171.818,63.53,-171.804,63.581,-171.746,63.703,-171.646,63.727,-171.463,63.64]],[[-166.135,60.384,-166.044,60.334,-165.995,60.331,-165.841,60.346,-165.784,60.336,-165.73,60.314,-165.696,60.282,-165.689,60.224,-165.714,60.173,-165.707,60.101,-165.712,60.069,-165.631,60.028,-165.605,59.973,-165.592,59.913,-165.769,59.893,-165.947,59.89,-166.1,59.85,-166.131,59.82,-166.107,59.775,-166.149,59.764,-166.188,59.774,-166.262,59.815,-166.343,59.834,-166.628,59.865,-166.985,59.984,-167.139,60.009,-167.295,60.096,-167.436,60.207,-167.344,60.224,-167.252,60.234,-166.836,60.217,-166.784,60.296,-166.731,60.316,-166.599,60.339,-166.476,60.383,-166.42,60.382,-166.364,60.365,-166.247,60.391,-166.185,60.397,-166.135,60.384]],[[-152.898,57.824,-152.891,57.769,-152.85,57.776,-152.696,57.832,-152.616,57.849,-152.512,57.851,-152.429,57.826,-152.412,57.806,-152.419,57.782,-152.485,57.734,-152.483,57.703,-152.411,57.646,-152.237,57.615,-152.215,57.598,-152.216,57.577,-152.337,57.482,-152.381,57.46,-152.412,57.455,-152.631,57.472,-152.831,57.503,-152.912,57.508,-152.941,57.498,-152.997,57.469,-152.957,57.46,-152.781,57.453,-152.72,57.411,-152.693,57.38,-152.679,57.345,-152.714,57.331,-152.789,57.321,-152.879,57.321,-152.99,57.282,-153.052,57.238,-153.274,57.226,-153.444,57.167,-153.504,57.138,-153.524,57.103,-153.588,57.078,-153.733,57.052,-153.647,57.03,-153.633,57.01,-153.631,56.984,-153.643,56.961,-153.757,56.858,-153.973,56.774,-154.027,56.778,-154.051,56.788,-154.07,56.805,-154.071,56.821,-153.793,56.99,-153.804,56.998,-153.88,57.004,-153.999,57.05,-154.084,57.02,-154.103,57.021,-154.08,57.061,-154.025,57.108,-154.035,57.122,-154.065,57.134,-154.135,57.141,-154.244,57.143,-154.324,57.132,-154.377,57.107,-154.381,57.097,-154.27,57.099,-154.239,57.087,-154.209,57.063,-154.191,57.036,-154.184,57.005,-154.208,56.964,-154.261,56.912,-154.339,56.921,-154.499,57.037,-154.569,57.206,-154.706,57.335,-154.712,57.366,-154.673,57.446,-154.535,57.559,-154.387,57.59,-154.281,57.638,-154.179,57.652,-154.116,57.651,-154.03,57.631,-153.995,57.587,-154.016,57.567,-154.008,57.556,-153.947,57.53,-153.882,57.439,-153.805,57.358,-153.755,57.325,-153.688,57.305,-153.757,57.367,-153.798,57.443,-153.818,57.596,-153.838,57.636,-153.799,57.647,-153.69,57.641,-153.693,57.663,-153.808,57.715,-153.879,57.757,-153.906,57.791,-153.904,57.82,-153.842,57.863,-153.806,57.875,-153.769,57.88,-153.696,57.871,-153.663,57.858,-153.569,57.761,-153.524,57.731,-153.488,57.731,-153.454,57.747,-153.423,57.779,-153.39,57.798,-153.357,57.805,-153.252,57.79,-153.217,57.796,-153.2,57.82,-153.201,57.863,-153.175,57.879,-153.169,57.911,-153.226,57.958,-153.16,57.972,-152.943,57.936,-152.85,57.897,-152.898,57.824]],[[-130.025,55.888,-130.075,55.836,-130.112,55.78,-130.137,55.719,-130.147,55.654,-130.14,55.585,-130.12,55.524,-130.059,55.412,-130.039,55.344,-130.037,55.298,-130.172,55.137,-130.219,55.06,-130.214,55.026,-130.313,54.946,-130.493,54.834,-130.575,54.77,-130.616,54.791,-130.85,54.808,-130.935,54.95,-130.98,55.061,-131.048,55.158,-131.046,55.18,-130.984,55.244,-130.75,55.297,-130.748,55.318,-130.835,55.332,-130.856,55.355,-130.88,55.46,-130.873,55.551,-130.88,55.612,-130.919,55.736,-130.977,55.812,-131.128,55.96,-131.14,55.998,-131.074,56.044,-131.033,56.088,-131.288,56.012,-131.635,55.932,-131.784,55.877,-131.815,55.854,-131.826,55.835,-131.799,55.783,-131.803,55.766,-131.834,55.735,-131.869,55.647,-131.945,55.554,-131.983,55.535,-132.119,55.57,-132.155,55.6,-132.223,55.721,-132.208,55.753,-132.158,55.781,-132.091,55.84,-132.006,55.93,-131.844,56.16,-131.738,56.161,-131.551,56.207,-131.844,56.23,-131.888,56.242,-131.927,56.273,-131.962,56.324,-132.022,56.38,-132.133,56.4,-132.182,56.421,-132.256,56.489,-132.305,56.52,-132.332,56.558,-132.337,56.603,-132.358,56.626,-132.434,56.634,-132.476,56.65,-132.487,56.766,-132.64,56.796,-132.702,56.822,-132.802,56.895,-132.83,56.931,-132.839,56.96,-132.814,57.041,-132.825,57.056,-132.913,57.047,-133.466,57.172,-133.437,57.337,-133.539,57.554,-133.649,57.642,-133.627,57.677,-133.603,57.695,-133.554,57.695,-133.342,57.631,-133.143,57.555,-133.117,57.566,-133.436,57.727,-133.515,57.775,-133.535,57.833,-133.536,57.864,-133.511,57.88,-133.212,57.866,-133.194,57.878,-133.497,57.925,-133.559,57.924,-133.626,57.857,-133.657,57.841,-133.722,57.844,-133.744,57.855,-133.821,57.936,-133.894,57.993,-134.031,58.072,-134.057,58.128,-134.063,58.211,-134.045,58.289,-133.934,58.468,-133.889,58.499,-133.877,58.518,-133.911,58.515,-133.944,58.498,-134.036,58.415,-134.131,58.279,-134.209,58.233,-134.258,58.244,-134.331,58.3,-134.485,58.367,-134.664,58.385,-134.776,58.454,-134.943,58.646,-134.965,58.742,-134.986,58.766,-135.076,58.797,-135.132,58.843,-135.217,59.077,-135.33,59.239,-135.358,59.325,-135.349,59.41,-135.364,59.419,-135.403,59.353,-135.413,59.318,-135.484,59.309,-135.417,59.242,-135.4,59.208,-135.434,59.211,-135.502,59.202,-135.386,59.088,-135.334,58.91,-135.257,58.778,-135.207,58.671,-135.185,58.59,-135.152,58.512,-135.062,58.341,-135.05,58.307,-135.06,58.279,-135.09,58.246,-135.142,58.233,-135.303,58.256,-135.363,58.298,-135.45,58.376,-135.572,58.412,-135.873,58.394,-135.898,58.4,-135.896,58.464,-135.862,58.577,-135.89,58.623,-136.046,58.789,-136.043,58.822,-135.826,58.898,-135.932,58.904,-136.017,58.874,-136.049,58.893,-136.101,59,-136.134,59.04,-136.15,59.048,-136.159,58.947,-136.124,58.893,-136.118,58.863,-136.124,58.82,-136.147,58.789,-136.186,58.77,-136.226,58.765,-136.299,58.787,-136.38,58.827,-136.451,58.846,-136.478,58.863,-136.511,58.907,-136.566,58.941,-136.831,58.984,-136.989,59.034,-137.002,59.021,-136.953,58.967,-136.948,58.935,-136.988,58.925,-137.059,58.874,-137.038,58.867,-136.963,58.884,-136.879,58.882,-136.74,58.85,-136.614,58.809,-136.568,58.786,-136.549,58.752,-136.533,58.74,-136.41,58.701,-136.404,58.68,-136.484,58.618,-136.32,58.624,-136.225,58.602,-136.103,58.506,-136.061,58.453,-136.056,58.384,-136.081,58.364,-136.13,58.35,-136.462,58.328,-136.583,58.245,-136.607,58.244,-136.699,58.266,-136.865,58.332,-137.072,58.395,-137.544,58.581,-137.557,58.59,-137.565,58.626,-137.597,58.644,-137.661,58.66,-137.75,58.707,-137.864,58.786,-137.934,58.847,-137.961,58.891,-138.027,58.941,-138.241,59.047,-138.352,59.087,-138.451,59.11,-138.537,59.115,-138.56,59.129,-138.521,59.152,-138.515,59.166,-138.704,59.188,-138.884,59.237,-139.341,59.376,-139.577,59.462,-139.714,59.504,-139.773,59.527,-139.799,59.546,-139.766,59.566,-139.674,59.587,-139.612,59.61,-139.513,59.698,-139.506,59.726,-139.558,59.79,-139.582,59.848,-139.581,59.881,-139.569,59.912,-139.554,59.933,-139.512,59.954,-139.483,59.964,-139.447,59.957,-139.331,59.877,-139.315,59.848,-139.32,59.739,-139.287,59.611,-139.276,59.62,-139.266,59.663,-139.259,59.743,-139.246,59.782,-139.221,59.82,-139.179,59.84,-139.048,59.828,-138.988,59.835,-139.242,59.893,-139.402,60.001,-139.431,60.012,-139.519,60.017,-139.612,59.973,-139.85,59.831,-139.917,59.806,-140.217,59.727,-140.42,59.711,-140.648,59.723,-140.843,59.749,-141.332,59.874,-141.408,59.903,-141.295,59.98,-141.29,60.004,-141.33,60.083,-141.362,60.105,-141.409,60.118,-141.422,60.109,-141.422,60.085,-141.41,60.042,-141.447,60.019,-141.53,59.995,-141.67,59.97,-142.104,60.033,-142.549,60.086,-142.946,60.097,-143.506,60.055,-143.805,60.013,-143.979,60.009,-144.147,60.016,-144.161,60.046,-144.084,60.063,-144.089,60.084,-144.185,60.151,-144.333,60.191,-144.53,60.205,-144.643,60.225,-144.672,60.249,-144.741,60.273,-144.852,60.295,-144.901,60.335,-144.862,60.459,-144.824,60.534,-144.787,60.585,-144.691,60.669,-144.724,60.663,-144.863,60.601,-144.984,60.537,-145.096,60.454,-145.163,60.415,-145.248,60.38,-145.382,60.389,-145.563,60.441,-145.718,60.468,-145.848,60.469,-145.899,60.478,-145.811,60.525,-145.76,60.562,-145.69,60.622,-145.675,60.651,-146.149,60.661,-146.166,60.692,-146.167,60.716,-146.182,60.735,-146.251,60.749,-146.347,60.738,-146.503,60.701,-146.57,60.729,-146.546,60.745,-146.496,60.757,-146.392,60.811,-146.532,60.839,-146.604,60.871,-146.638,60.897,-146.636,60.993,-146.599,61.054,-146.285,61.113,-146.384,61.136,-146.583,61.128,-146.716,61.078,-146.874,61.005,-146.98,60.978,-147.034,60.996,-147.106,61.003,-147.195,60.997,-147.255,60.978,-147.286,60.947,-147.321,60.925,-147.361,60.915,-147.391,60.918,-147.433,60.95,-147.523,60.97,-147.567,60.995,-147.593,60.979,-147.623,60.933,-147.656,60.91,-147.808,60.885,-147.891,60.89,-147.991,60.948,-148.005,60.969,-147.971,61.019,-147.752,61.219,-147.774,61.218,-147.845,61.186,-147.986,61.106,-148.049,61.083,-148.158,61.08,-148.209,61.088,-148.27,61.082,-148.342,61.06,-148.389,61.037,-148.411,61.011,-148.396,61.007,-148.287,61.036,-148.226,61.044,-148.209,61.03,-148.293,60.94,-148.344,60.854,-148.393,60.832,-148.471,60.835,-148.556,60.827,-148.557,60.803,-148.399,60.734,-148.341,60.724,-148.268,60.7,-148.257,60.675,-148.284,60.609,-148.305,60.583,-148.338,60.57,-148.468,60.572,-148.51,60.565,-148.597,60.524,-148.64,60.489,-148.624,60.486,-148.549,60.515,-148.44,60.53,-148.296,60.532,-148.189,60.547,-148.119,60.575,-148.051,60.567,-147.984,60.523,-147.964,60.485,-147.991,60.452,-148.046,60.428,-148.129,60.414,-148.182,60.393,-148.204,60.365,-148.216,60.323,-148.219,60.268,-148.198,60.168,-148.214,60.154,-148.245,60.147,-148.291,60.145,-148.333,60.122,-148.431,59.989,-148.465,59.975,-148.506,59.989,-148.542,59.987,-148.574,59.97,-148.644,59.957,-148.751,59.948,-148.843,59.951,-149.004,59.98,-149.07,60,-149.122,60.033,-149.267,59.998,-149.305,60.014,-149.395,60.106,-149.415,60.1,-149.432,60.001,-149.46,59.966,-149.549,59.894,-149.598,59.77,-149.613,59.767,-149.63,59.785,-149.685,59.895,-149.714,59.92,-149.795,59.856,-149.804,59.833,-149.782,59.75,-149.801,59.738,-149.965,59.782,-150.005,59.784,-150.016,59.777,-149.96,59.713,-149.967,59.69,-150.198,59.567,-150.258,59.571,-150.296,59.583,-150.338,59.581,-150.485,59.535,-150.526,59.537,-150.582,59.565,-150.607,59.563,-150.621,59.535,-150.623,59.48,-150.677,59.427,-150.853,59.342,-150.899,59.303,-150.935,59.249,-150.961,59.244,-151.064,59.278,-151.183,59.301,-151.199,59.29,-151.163,59.257,-151.171,59.237,-151.222,59.229,-151.287,59.232,-151.366,59.246,-151.477,59.231,-151.619,59.187,-151.738,59.189,-151.904,59.26,-151.95,59.265,-151.964,59.285,-151.932,59.343,-151.885,59.386,-151.85,59.406,-151.693,59.462,-151.513,59.483,-151.4,59.516,-151.262,59.586,-151.189,59.638,-151.046,59.772,-151.057,59.782,-151.089,59.789,-151.404,59.662,-151.45,59.65,-151.513,59.651,-151.764,59.7,-151.817,59.721,-151.853,59.782,-151.783,59.921,-151.735,59.988,-151.612,60.092,-151.451,60.203,-151.396,60.274,-151.313,60.466,-151.318,60.554,-151.355,60.66,-151.356,60.723,-151.322,60.743,-150.954,60.841,-150.779,60.915,-150.441,61.024,-150.349,61.023,-150.281,60.985,-150.203,60.955,-150.113,60.933,-149.998,60.935,-149.856,60.962,-149.632,60.952,-149.173,60.88,-149.075,60.876,-149.071,60.886,-149.142,60.936,-149.459,60.965,-149.592,60.994,-149.968,61.122,-150.053,61.171,-150.019,61.194,-149.927,61.213,-149.895,61.232,-149.882,61.264,-149.829,61.308,-149.737,61.363,-149.596,61.417,-149.329,61.497,-149.434,61.501,-149.625,61.486,-149.695,61.471,-149.824,61.413,-149.874,61.373,-149.945,61.294,-149.976,61.279,-150.109,61.268,-150.472,61.26,-150.533,61.3,-150.567,61.307,-150.612,61.301,-150.946,61.198,-151.065,61.146,-151.15,61.086,-151.282,61.042,-151.46,61.014,-151.594,60.98,-151.734,60.911,-151.782,60.858,-151.784,60.833,-151.75,60.755,-151.785,60.74,-151.866,60.734,-151.996,60.682,-152.271,60.528,-152.307,60.472,-152.305,60.453,-152.26,60.409,-152.292,60.381,-152.369,60.336,-152.541,60.265,-152.654,60.238,-152.727,60.237,-152.798,60.247,-152.923,60.293,-153.025,60.296,-153.031,60.289,-152.893,60.24,-152.752,60.177,-152.665,60.125,-152.63,60.084,-152.629,60.041,-152.66,59.997,-152.759,59.921,-152.857,59.898,-153.106,59.875,-153.186,59.857,-153.211,59.843,-153.04,59.81,-153.025,59.794,-153.048,59.73,-153.094,59.709,-153.236,59.671,-153.364,59.66,-153.383,59.667,-153.36,59.717,-153.366,59.73,-153.414,59.74,-153.483,59.721,-153.653,59.647,-153.671,59.635,-153.609,59.615,-153.622,59.598,-153.714,59.545,-153.753,59.51,-153.814,59.474,-154.088,59.363,-154.067,59.336,-154.139,59.24,-154.178,59.156,-154.13,59.12,-153.9,59.078,-153.788,59.068,-153.656,59.039,-153.418,58.96,-153.339,58.909,-153.327,58.884,-153.334,58.858,-153.363,58.822,-153.438,58.755,-153.617,58.655,-153.699,58.626,-153.821,58.604,-153.862,58.588,-154.02,58.493,-154.062,58.442,-154.056,58.397,-154.086,58.366,-154.289,58.304,-154.282,58.293,-154.208,58.289,-154.235,58.235,-154.247,58.159,-154.282,58.147,-154.409,58.147,-154.571,58.118,-154.582,58.11,-154.585,58.056,-155.007,58.016,-155.099,57.913,-155.147,57.882,-155.313,57.807,-155.414,57.777,-155.53,57.759,-155.59,57.734,-155.596,57.701,-155.629,57.673,-155.729,57.627,-155.778,57.568,-155.814,57.559,-156,57.545,-156.037,57.527,-156.055,57.448,-156.09,57.445,-156.156,57.463,-156.242,57.449,-156.436,57.36,-156.478,57.328,-156.474,57.311,-156.444,57.294,-156.398,57.241,-156.4,57.205,-156.475,57.105,-156.501,57.09,-156.592,57.065,-156.629,57.01,-156.713,57.016,-156.78,57.006,-156.824,56.969,-156.872,56.948,-156.923,56.942,-156.988,56.913,-157.067,56.86,-157.139,56.827,-157.206,56.812,-157.271,56.808,-157.334,56.816,-157.39,56.81,-157.441,56.79,-157.49,56.76,-157.529,56.673,-157.578,56.634,-157.61,56.628,-157.674,56.633,-157.771,56.652,-157.869,56.645,-158.028,56.592,-158.078,56.552,-157.978,56.543,-157.929,56.532,-157.93,56.52,-157.982,56.51,-158.071,56.51,-158.124,56.501,-158.189,56.478,-158.352,56.454,-158.414,56.436,-158.537,56.335,-158.552,56.313,-158.536,56.308,-158.467,56.318,-158.386,56.302,-158.344,56.28,-158.317,56.254,-158.291,56.204,-158.276,56.196,-158.432,56.111,-158.476,56.075,-158.505,56.062,-158.523,56.072,-158.543,56.167,-158.554,56.183,-158.591,56.185,-158.627,56.155,-158.705,56.043,-158.79,55.987,-159.429,55.843,-159.523,55.81,-159.541,55.748,-159.568,55.695,-159.61,55.653,-159.66,55.626,-159.67,55.645,-159.665,55.795,-159.679,55.825,-159.743,55.844,-159.771,55.841,-159.81,55.833,-159.874,55.8,-159.914,55.792,-159.962,55.795,-160.046,55.763,-160.244,55.661,-160.373,55.635,-160.407,55.614,-160.463,55.558,-160.499,55.537,-160.554,55.535,-160.625,55.552,-160.683,55.54,-160.727,55.5,-160.771,55.484,-160.897,55.514,-160.952,55.493,-161.024,55.44,-161.1,55.406,-161.178,55.389,-161.382,55.371,-161.464,55.383,-161.481,55.398,-161.477,55.465,-161.444,55.513,-161.413,55.536,-161.373,55.556,-161.313,55.559,-161.202,55.544,-161.215,55.56,-161.255,55.579,-161.357,55.612,-161.459,55.629,-161.517,55.618,-161.599,55.593,-161.654,55.563,-161.684,55.53,-161.72,55.421,-161.742,55.391,-161.98,55.199,-162.074,55.139,-162.167,55.144,-162.211,55.121,-162.275,55.073,-162.333,55.05,-162.386,55.052,-162.428,55.061,-162.457,55.078,-162.452,55.093,-162.413,55.107,-162.427,55.145,-162.495,55.208,-162.542,55.243,-162.63,55.247,-162.644,55.218,-162.614,55.071,-162.619,55.038,-162.674,54.997,-162.82,54.95,-162.865,54.955,-162.996,55.046,-163.12,55.065,-163.128,55.035,-163.1,54.974,-163.131,54.917,-163.221,54.863,-163.289,54.838,-163.335,54.839,-163.338,54.876,-163.296,54.949,-163.286,55.01,-163.306,55.059,-163.304,55.096,-163.279,55.122,-163.115,55.194,-163.045,55.205,-163.008,55.187,-162.962,55.184,-162.907,55.196,-162.872,55.219,-162.857,55.253,-162.786,55.297,-162.659,55.351,-162.513,55.45,-162.349,55.595,-162.157,55.719,-161.937,55.824,-161.697,55.907,-161.216,56.021,-161.179,56.014,-161.223,55.977,-161.193,55.954,-161.145,55.951,-160.969,55.97,-160.899,55.994,-160.878,55.971,-160.902,55.941,-161.008,55.912,-161.005,55.887,-160.851,55.772,-160.803,55.754,-160.763,55.757,-160.746,55.771,-160.758,55.855,-160.706,55.87,-160.6,55.874,-160.53,55.863,-160.498,55.838,-160.437,55.817,-160.347,55.8,-160.292,55.805,-160.27,55.832,-160.308,55.864,-160.48,55.935,-160.527,55.965,-160.539,56.006,-160.515,56.059,-160.461,56.138,-160.377,56.241,-160.302,56.314,-160.149,56.396,-160.046,56.437,-159.785,56.562,-159.283,56.689,-159.159,56.77,-158.99,56.86,-158.918,56.882,-158.918,56.847,-158.895,56.816,-158.782,56.796,-158.709,56.789,-158.675,56.795,-158.666,56.828,-158.681,56.888,-158.685,56.944,-158.677,56.997,-158.661,57.039,-158.586,57.114,-158.474,57.199,-158.321,57.298,-158.225,57.343,-158.134,57.366,-158.046,57.409,-157.894,57.511,-157.846,57.528,-157.737,57.548,-157.697,57.539,-157.674,57.514,-157.646,57.498,-157.535,57.483,-157.462,57.506,-157.474,57.518,-157.533,57.526,-157.572,57.541,-157.608,57.601,-157.681,57.638,-157.697,57.679,-157.684,57.744,-157.621,57.895,-157.611,58.051,-157.555,58.14,-157.443,58.172,-157.194,58.194,-157.339,58.235,-157.394,58.235,-157.488,58.254,-157.524,58.351,-157.524,58.421,-157.461,58.503,-157.229,58.641,-156.975,58.736,-157.009,58.744,-157.04,58.773,-156.923,58.964,-156.809,59.134,-156.963,58.989,-157.142,58.878,-157.666,58.748,-158.022,58.64,-158.191,58.614,-158.303,58.642,-158.39,58.746,-158.439,58.783,-158.503,58.85,-158.476,58.938,-158.426,58.999,-158.315,59.009,-158.189,58.98,-158.081,58.977,-158.221,59.038,-158.423,59.09,-158.514,59.073,-158.584,58.988,-158.678,58.929,-158.761,58.95,-158.809,58.974,-158.776,58.903,-158.838,58.794,-158.861,58.719,-158.772,58.52,-158.789,58.441,-158.951,58.405,-159.083,58.47,-159.358,58.721,-159.454,58.793,-159.67,58.911,-159.741,58.894,-159.832,58.836,-159.92,58.82,-160.153,58.906,-160.261,58.972,-160.363,59.051,-160.52,59.007,-160.657,58.955,-160.817,58.872,-160.924,58.872,-161.216,58.801,-161.247,58.799,-161.288,58.761,-161.328,58.744,-161.361,58.67,-161.755,58.612,-162.145,58.644,-162.009,58.685,-161.856,58.717,-161.724,58.794,-161.781,58.897,-161.79,58.95,-161.789,59.016,-161.644,59.11,-161.794,59.109,-161.891,59.076,-161.981,59.146,-162.023,59.284,-161.92,59.365,-161.872,59.428,-161.832,59.515,-161.829,59.589,-161.909,59.714,-162.138,59.98,-162.242,60.178,-162.421,60.284,-162.288,60.457,-162.139,60.614,-161.947,60.685,-161.962,60.695,-162.068,60.695,-162.138,60.686,-162.2,60.634,-162.265,60.595,-162.469,60.395,-162.6,60.297,-162.685,60.269,-162.548,60.231,-162.527,60.199,-162.5,60.127,-162.536,60.038,-162.571,59.99,-162.733,59.994,-162.878,59.923,-163.219,59.846,-163.68,59.802,-163.907,59.807,-164.143,59.897,-164.141,59.949,-164.132,59.994,-164.471,60.149,-164.662,60.304,-164.8,60.307,-164.92,60.348,-165.061,60.413,-165.049,60.464,-165.027,60.501,-165.113,60.526,-165.225,60.524,-165.354,60.541,-165.016,60.74,-164.9,60.873,-164.805,60.892,-164.682,60.872,-164.513,60.819,-164.37,60.796,-164.319,60.771,-164.266,60.725,-164.321,60.647,-164.372,60.592,-164.31,60.607,-164.132,60.692,-164,60.766,-163.936,60.758,-163.895,60.745,-163.821,60.668,-163.73,60.59,-163.529,60.665,-163.421,60.757,-163.512,60.798,-163.623,60.822,-163.907,60.854,-163.837,60.88,-163.655,60.877,-163.587,60.903,-163.659,60.938,-163.749,60.97,-163.995,60.865,-164.442,60.87,-164.754,60.931,-165.066,60.921,-165.115,60.933,-165.175,60.966,-165,61.044,-164.876,61.087,-164.869,61.112,-164.941,61.115,-165.077,61.094,-165.138,61.13,-165.128,61.192,-165.15,61.187,-165.204,61.153,-165.28,61.17,-165.345,61.198,-165.311,61.228,-165.244,61.269,-165.274,61.275,-165.334,61.266,-165.392,61.212,-165.379,61.169,-165.381,61.106,-165.48,61.095,-165.566,61.102,-165.628,61.165,-165.691,61.3,-165.864,61.336,-165.906,61.404,-165.797,61.491,-165.845,61.536,-165.961,61.551,-166.094,61.507,-166.153,61.546,-166.164,61.589,-166.168,61.651,-166.131,61.657,-166.1,61.645,-165.835,61.679,-165.809,61.696,-166.02,61.748,-166.079,61.803,-165.991,61.834,-165.834,61.837,-165.613,61.869,-165.706,61.927,-165.725,61.959,-165.744,62.012,-165.707,62.1,-165.448,62.304,-165.195,62.474,-165.116,62.513,-165,62.534,-164.892,62.518,-164.779,62.481,-164.758,62.497,-164.796,62.512,-164.844,62.581,-164.688,62.608,-164.596,62.687,-164.589,62.709,-164.689,62.677,-164.793,62.623,-164.819,62.677,-164.845,62.801,-164.8,62.918,-164.764,62.971,-164.677,63.02,-164.428,63.04,-164.384,63.03,-164.375,63.054,-164.525,63.128,-164.463,63.185,-164.409,63.215,-164.108,63.262,-163.943,63.247,-163.736,63.193,-163.616,63.125,-163.634,63.09,-163.664,63.07,-163.726,63.048,-163.749,63.03,-163.738,63.016,-163.649,63.057,-163.504,63.106,-163.423,63.085,-163.359,63.046,-163.288,63.046,-163.062,63.08,-162.948,63.115,-162.808,63.207,-162.621,63.266,-162.36,63.453,-162.283,63.529,-162.193,63.541,-162.112,63.534,-162.056,63.471,-161.974,63.453,-161.505,63.468,-161.266,63.497,-161.1,63.558,-160.927,63.661,-160.827,63.729,-160.779,63.819,-160.84,63.935,-160.904,64.031,-160.988,64.251,-161.22,64.397,-161.386,64.44,-161.491,64.434,-161.415,64.526,-161.193,64.516,-161.049,64.534,-160.932,64.579,-160.894,64.613,-160.836,64.682,-160.856,64.756,-160.887,64.796,-160.967,64.84,-161.063,64.904,-161.13,64.925,-161.187,64.924,-161.466,64.795,-161.634,64.792,-161.759,64.816,-161.868,64.743,-162.172,64.678,-162.335,64.613,-162.636,64.451,-162.711,64.378,-162.807,64.374,-162.876,64.516,-163.204,64.652,-163.303,64.606,-163.248,64.563,-163.174,64.533,-163.052,64.52,-163.104,64.479,-163.144,64.424,-163.267,64.475,-163.486,64.55,-163.713,64.588,-164.304,64.584,-164.692,64.507,-164.727,64.523,-164.765,64.53,-164.83,64.511,-164.857,64.48,-164.9,64.461,-164.979,64.454,-165.138,64.465,-165.446,64.513,-166.143,64.583,-166.325,64.626,-166.481,64.728,-166.478,64.798,-166.409,64.827,-166.415,64.927,-166.551,64.953,-166.827,65.096,-166.928,65.157,-166.906,65.164,-166.857,65.147,-166.763,65.135,-166.531,65.155,-166.452,65.247,-166.28,65.274,-166.121,65.261,-166.157,65.286,-166.197,65.306,-166.609,65.353,-166.665,65.338,-167.404,65.422,-167.987,65.568,-168.035,65.596,-168.088,65.658,-168.01,65.719,-167.931,65.748,-167.927,65.714,-167.914,65.681,-167.58,65.758,-167.405,65.859,-167.074,65.877,-166.997,65.905,-166.894,65.959,-166.748,66.052,-166.54,66.101,-166.399,66.144,-166.215,66.17,-166.057,66.127,-166.009,66.121,-165.724,66.113,-165.63,66.131,-165.59,66.145,-165.56,66.167,-165.84,66.245,-165.812,66.288,-165.776,66.319,-165.449,66.41,-165.198,66.44,-165.064,66.438,-164.674,66.555,-164.46,66.588,-164.058,66.611,-163.728,66.616,-163.638,66.575,-163.816,66.583,-163.894,66.576,-163.838,66.562,-163.775,66.531,-163.794,66.493,-163.903,66.378,-163.894,66.287,-163.965,66.257,-164.034,66.216,-163.695,66.084,-163.171,66.075,-162.886,66.099,-162.722,66.06,-162.587,66.051,-162.214,66.071,-161.934,66.043,-161.816,66.054,-161.557,66.251,-161.455,66.281,-161.345,66.247,-161.201,66.219,-161.109,66.24,-161.034,66.189,-161.07,66.295,-161.12,66.334,-161.544,66.407,-161.828,66.371,-161.917,66.412,-161.888,66.493,-162.191,66.693,-162.318,66.734,-162.467,66.736,-162.544,66.805,-162.607,66.894,-162.478,66.931,-162.362,66.947,-162.254,66.919,-162.131,66.801,-162.018,66.784,-162.051,66.667,-161.91,66.56,-161.591,66.46,-161.336,66.496,-161.156,66.495,-161.048,66.474,-160.784,66.384,-160.651,66.373,-160.232,66.42,-160.227,66.509,-160.263,66.572,-160.361,66.613,-160.644,66.605,-160.864,66.671,-161.051,66.653,-161.398,66.552,-161.572,66.592,-161.681,66.646,-161.857,66.7,-161.879,66.804,-161.731,66.923,-161.622,66.979,-161.72,67.021,-161.965,67.05,-162.392,67.02,-162.412,67.06,-162.409,67.104,-162.583,67.019,-162.761,67.036,-163.002,67.027,-163.532,67.103,-163.721,67.196,-163.8,67.271,-163.943,67.478,-164.125,67.607,-165.386,68.046,-165.96,68.156,-166.236,68.278,-166.409,68.308,-166.574,68.32,-166.786,68.36,-166.644,68.408,-166.546,68.424,-166.648,68.374,-166.57,68.361,-166.447,68.39,-166.381,68.425,-166.283,68.573,-166.182,68.797,-166.209,68.885,-165.509,68.868,-165.044,68.882,-164.89,68.902,-164.302,68.936,-164.15,68.961,-163.868,69.037,-163.536,69.17,-163.251,69.345,-163.205,69.393,-163.187,69.38,-163.161,69.388,-163.131,69.454,-163.094,69.611,-162.952,69.758,-162.35,70.094,-162.071,70.227,-161.978,70.288,-161.881,70.332,-161.813,70.29,-161.78,70.277,-161.761,70.258,-161.818,70.248,-161.912,70.205,-162.042,70.177,-162.074,70.162,-161.997,70.165,-161.768,70.197,-161.639,70.235,-160.996,70.305,-160.648,70.421,-160.634,70.446,-160.117,70.591,-160.046,70.586,-159.963,70.568,-160.106,70.473,-160.006,70.448,-160.095,70.333,-159.908,70.331,-159.866,70.279,-159.855,70.324,-159.858,70.389,-159.843,70.453,-159.815,70.497,-159.683,70.477,-159.387,70.525,-159.746,70.53,-159.962,70.634,-160.082,70.635,-159.681,70.787,-159.315,70.879,-159.232,70.877,-159.192,70.86,-159.183,70.832,-159.262,70.814,-159.34,70.781,-159.304,70.753,-159.251,70.748,-159.075,70.772,-158.996,70.802,-158.621,70.799,-158.511,70.82,-158.484,70.841,-157.998,70.845,-157.909,70.86,-157.606,70.941,-157.325,71.04,-157.195,71.093,-156.973,71.23,-156.783,71.319,-156.47,71.408,-156.395,71.397,-156.497,71.379,-156.567,71.342,-156.47,71.292,-155.811,71.188,-155.646,71.183,-155.579,71.121,-155.635,71.062,-155.804,70.995,-156.147,70.928,-156.042,70.902,-155.974,70.842,-155.872,70.835,-155.708,70.857,-155.579,70.894,-155.313,71.015,-155.23,71.082,-155.167,71.099,-154.944,71.083,-154.818,71.048,-154.674,70.987,-154.726,70.928,-154.785,70.894,-154.599,70.848,-154.392,70.838,-154.195,70.801,-153.918,70.877,-153.701,70.894,-153.498,70.891,-153.233,70.933,-152.785,70.876,-152.671,70.891,-152.491,70.881,-152.3,70.847,-152.233,70.81,-152.437,70.733,-152.471,70.654,-152.399,70.62,-152.27,70.615,-152.253,70.568,-152.173,70.557,-151.769,70.56,-151.8,70.538,-151.82,70.511,-151.945,70.452,-151.225,70.419,-151.128,70.452,-150.979,70.465,-150.663,70.51,-150.544,70.49,-150.403,70.444,-150.274,70.434,-150.152,70.444,-149.87,70.51,-149.544,70.513,-149.411,70.491,-149.269,70.501,-148.845,70.425,-148.688,70.416,-148.479,70.318,-148.371,70.315,-148.249,70.357,-148.143,70.355,-148.039,70.315,-147.87,70.303,-147.791,70.24,-147.705,70.217,-147.063,70.17,-146.745,70.192,-146.281,70.186,-146.058,70.156,-145.823,70.16,-145.44,70.051,-145.237,70.034,-145.197,70.009,-144.619,69.982,-144.417,70.039,-144.064,70.054,-143.746,70.102,-143.566,70.101,-143.357,70.09,-143.276,70.095,-143.218,70.116,-142.708,70.034,-142.422,69.94,-142.297,69.87,-141.699,69.77,-141.526,69.715,-141.408,69.653,-141.339,69.647,-141.29,69.665,-141.081,69.659,-141.002,69.651,-141.002,69.359,-141.002,69.066,-141.002,68.774,-141.002,68.482,-141.002,68.19,-141.002,67.898,-141.002,67.605,-141.002,67.313,-141.002,67.021,-141.002,66.729,-141.002,66.437,-141.002,66.144,-141.002,65.852,-141.002,65.56,-141.002,65.268,-141.002,64.976,-141.002,64.683,-141.002,64.391,-141.002,64.099,-141.002,63.807,-141.002,63.514,-141.002,63.222,-141.002,62.93,-141.002,62.638,-141.002,62.346,-141.002,62.053,-141.002,61.761,-141.002,61.469,-141.002,61.177,-141.002,60.885,-141.002,60.592,-141.002,60.3,-140.763,60.259,-140.525,60.218,-140.453,60.3,-140.197,60.238,-139.973,60.183,-139.831,60.253,-139.676,60.328,-139.468,60.334,-139.235,60.34,-139.079,60.344,-139.079,60.279,-139.137,60.173,-139.185,60.084,-139.043,59.993,-138.869,59.946,-138.705,59.901,-138.632,59.778,-138.454,59.683,-138.318,59.611,-138.187,59.542,-138.001,59.443,-137.871,59.374,-137.697,59.281,-137.593,59.226,-137.544,59.119,-137.484,58.991,-137.521,58.915,-137.439,58.903,-137.278,58.988,-137.126,59.041,-136.939,59.106,-136.813,59.15,-136.579,59.152,-136.467,59.28,-136.466,59.459,-136.348,59.456,-136.278,59.48,-136.247,59.533,-136.322,59.605,-136.097,59.638,-135.935,59.663,-135.703,59.729,-135.476,59.793,-135.368,59.743,-135.261,59.695,-135.051,59.579,-135.037,59.551,-135.051,59.496,-135.071,59.441,-134.944,59.288,-134.907,59.271,-134.802,59.25,-134.677,59.199,-134.622,59.155,-134.441,59.085,-134.41,59.056,-134.393,59.009,-134.364,58.969,-134.33,58.94,-134.297,58.898,-134.219,58.85,-134.069,58.796,-133.966,58.758,-133.821,58.705,-133.674,58.597,-133.546,58.503,-133.401,58.411,-133.423,58.337,-133.275,58.223,-133.12,58.078,-133.001,57.949,-132.917,57.877,-132.816,57.773,-132.692,57.645,-132.55,57.5,-132.442,57.407,-132.302,57.276,-132.232,57.199,-132.279,57.145,-132.338,57.079,-132.157,57.048,-132.032,57.027,-132.063,56.953,-132.104,56.857,-131.962,56.819,-131.866,56.793,-131.886,56.742,-131.833,56.685,-131.824,56.59,-131.652,56.596,-131.575,56.599,-131.472,56.557,-131.336,56.501,-131.199,56.449,-131.083,56.405,-130.93,56.379,-130.742,56.341,-130.649,56.264,-130.477,56.231,-130.413,56.123,-130.215,56.083,-130.098,56.109,-130.056,56.065,-130.023,56.015,-130.014,55.951,-130.025,55.888]],[[-163.476,54.981,-163.379,54.816,-163.337,54.783,-163.275,54.766,-163.187,54.748,-163.135,54.723,-163.089,54.686,-163.083,54.669,-163.358,54.736,-163.531,54.638,-163.583,54.626,-164.073,54.621,-164.171,54.603,-164.235,54.571,-164.347,54.482,-164.404,54.448,-164.463,54.427,-164.591,54.404,-164.744,54.407,-164.823,54.419,-164.866,54.461,-164.904,54.545,-164.904,54.568,-164.888,54.608,-164.751,54.663,-164.706,54.692,-164.53,54.881,-164.479,54.907,-164.424,54.913,-164.274,54.9,-164.145,54.955,-163.868,55.039,-163.807,55.049,-163.607,55.051,-163.553,55.038,-163.511,55.014,-163.476,54.981]],[[-133.305,55.544,-133.283,55.516,-133.282,55.498,-133.426,55.431,-133.429,55.418,-133.463,55.377,-133.493,55.362,-133.547,55.317,-133.65,55.269,-133.635,55.413,-133.737,55.497,-133.634,55.539,-133.567,55.527,-133.455,55.522,-133.346,55.559,-133.305,55.544]],[[-131.34,55.08,-131.237,54.95,-131.232,54.904,-131.33,54.888,-131.406,54.894,-131.446,54.909,-131.456,54.931,-131.431,54.996,-131.482,55.035,-131.54,55.048,-131.592,55.026,-131.595,55.091,-131.556,55.137,-131.578,55.201,-131.578,55.249,-131.565,55.264,-131.513,55.263,-131.405,55.213,-131.34,55.08]],[[-132.112,56.109,-132.133,55.943,-132.173,55.953,-132.21,55.953,-132.287,55.929,-132.369,55.94,-132.407,55.958,-132.421,55.98,-132.406,56.029,-132.451,56.056,-132.603,56.066,-132.66,56.078,-132.691,56.13,-132.699,56.198,-132.675,56.224,-132.599,56.242,-132.539,56.324,-132.506,56.335,-132.38,56.499,-132.317,56.488,-132.206,56.388,-132.067,56.244,-132.112,56.109]],[[-130.979,55.489,-131.014,55.379,-131.083,55.267,-131.188,55.206,-131.262,55.22,-131.316,55.269,-131.367,55.266,-131.421,55.276,-131.451,55.316,-131.422,55.368,-131.448,55.409,-131.475,55.373,-131.522,55.341,-131.641,55.299,-131.724,55.218,-131.762,55.166,-131.811,55.223,-131.842,55.359,-131.846,55.416,-131.759,55.503,-131.648,55.586,-131.625,55.832,-131.269,55.955,-131.236,55.949,-131.121,55.857,-130.998,55.728,-130.966,55.67,-130.965,55.568,-130.979,55.489]],[[-133.366,57.004,-133.3,56.972,-133.264,57.005,-133.196,57.003,-133.071,56.974,-132.996,56.93,-132.954,56.88,-132.951,56.85,-132.963,56.783,-132.954,56.713,-132.959,56.677,-132.976,56.647,-133.004,56.624,-133.035,56.621,-133.132,56.683,-133.244,56.796,-133.329,56.83,-133.332,56.819,-133.309,56.786,-133.24,56.726,-133.227,56.689,-133.178,56.645,-133.157,56.611,-133.144,56.567,-133.145,56.528,-133.158,56.495,-133.181,56.474,-133.213,56.465,-133.383,56.474,-133.484,56.452,-133.603,56.464,-133.631,56.484,-133.649,56.517,-133.658,56.596,-133.688,56.71,-133.681,56.798,-133.758,56.877,-133.823,56.924,-133.917,56.967,-133.979,57.01,-133.962,57.043,-133.866,57.069,-133.708,57.063,-133.366,57.004]],[[-132.862,54.894,-132.838,54.881,-132.813,54.89,-132.772,54.926,-132.701,54.919,-132.649,54.907,-132.617,54.892,-132.634,54.84,-132.647,54.756,-132.677,54.726,-132.706,54.684,-132.807,54.709,-132.89,54.763,-133.009,54.855,-133.075,54.921,-133.081,54.949,-133.123,54.97,-133.205,55.084,-133.251,55.175,-133.325,55.185,-133.418,55.211,-133.454,55.26,-133.429,55.304,-133.297,55.326,-133.097,55.214,-133.067,55.166,-132.996,55.111,-132.982,55.033,-132.946,55.003,-132.862,54.894]],[[-146.394,60.45,-146.372,60.422,-146.18,60.429,-146.124,60.424,-146.102,60.411,-146.128,60.393,-146.202,60.368,-146.419,60.325,-146.595,60.268,-146.618,60.274,-146.65,60.336,-146.683,60.361,-146.703,60.396,-146.703,60.409,-146.67,60.433,-146.606,60.468,-146.56,60.481,-146.394,60.45]],[[-147.736,59.813,-147.846,59.799,-147.872,59.828,-147.814,59.902,-147.768,59.944,-147.734,59.954,-147.607,60.037,-147.466,60.097,-147.337,60.185,-147.205,60.311,-147.181,60.358,-147.12,60.363,-147.02,60.332,-146.958,60.289,-146.987,60.254,-147.318,60.075,-147.346,60.052,-147.377,59.991,-147.404,59.97,-147.448,59.96,-147.479,59.934,-147.499,59.89,-147.54,59.868,-147.602,59.866,-147.645,59.854,-147.669,59.832,-147.736,59.813]],[[-147.658,60.45,-147.659,60.424,-147.69,60.399,-147.66,60.352,-147.712,60.273,-147.732,60.222,-147.76,60.19,-147.788,60.178,-147.816,60.185,-147.822,60.203,-147.805,60.231,-147.871,60.23,-147.891,60.299,-147.855,60.321,-147.842,60.351,-147.838,60.371,-147.795,60.46,-147.779,60.466,-147.774,60.445,-147.76,60.439,-147.737,60.447,-147.703,60.487,-147.689,60.491,-147.658,60.45]],[[-147.931,60.826,-148.057,60.818,-148.115,60.831,-148.124,60.844,-148.1,60.895,-148.102,60.916,-148.038,60.924,-147.964,60.9,-147.943,60.875,-147.931,60.826]],[[-153.007,57.125,-153.134,57.093,-153.157,57.094,-153.235,57.029,-153.295,57,-153.375,57.052,-153.354,57.132,-153.285,57.185,-152.935,57.167,-152.908,57.152,-152.908,57.14,-152.933,57.129,-153.007,57.125]],[[-152.486,58.485,-152.516,58.479,-152.589,58.509,-152.637,58.542,-152.605,58.566,-152.463,58.619,-152.396,58.619,-152.368,58.611,-152.357,58.595,-152.362,58.571,-152.393,58.541,-152.486,58.485]],[[-153.241,57.85,-153.269,57.822,-153.295,57.829,-153.351,57.862,-153.465,57.909,-153.517,57.942,-153.52,57.956,-153.481,57.971,-153.347,57.933,-153.29,57.898,-153.241,57.85]],[[-155.566,55.821,-155.605,55.79,-155.681,55.792,-155.723,55.802,-155.737,55.83,-155.621,55.913,-155.594,55.924,-155.573,55.921,-155.564,55.887,-155.566,55.821]],[[-154.683,56.436,-154.751,56.412,-154.774,56.42,-154.777,56.44,-154.761,56.471,-154.729,56.502,-154.624,56.561,-154.518,56.601,-154.463,56.598,-154.445,56.573,-154.511,56.521,-154.683,56.436]],[[-154.209,56.515,-154.258,56.513,-154.332,56.539,-154.322,56.571,-154.217,56.609,-154.11,56.603,-154.102,56.582,-154.107,56.558,-154.116,56.544,-154.15,56.53,-154.209,56.515]],[[-160.685,55.315,-160.67,55.314,-160.639,55.322,-160.574,55.378,-160.553,55.381,-160.552,55.363,-160.583,55.308,-160.531,55.233,-160.483,55.197,-160.488,55.185,-160.609,55.159,-160.702,55.178,-160.751,55.171,-160.795,55.145,-160.825,55.174,-160.847,55.311,-160.84,55.335,-160.789,55.383,-160.724,55.405,-160.696,55.398,-160.672,55.379,-160.666,55.359,-160.685,55.315]],[[-162.298,54.847,-162.322,54.842,-162.391,54.873,-162.416,54.896,-162.434,54.932,-162.294,54.983,-162.265,54.983,-162.238,54.955,-162.234,54.932,-162.273,54.867,-162.298,54.847]],[[-162.554,54.401,-162.641,54.38,-162.733,54.402,-162.812,54.444,-162.821,54.495,-162.645,54.462,-162.608,54.447,-162.554,54.401]],[[-159.873,55.129,-159.934,55.107,-159.953,55.079,-159.999,55.067,-160.038,55.044,-160.17,54.942,-160.227,54.923,-160.164,55.01,-160.154,55.038,-160.152,55.057,-160.172,55.123,-160.134,55.12,-160.102,55.134,-160.039,55.193,-159.982,55.198,-159.92,55.268,-159.887,55.273,-159.871,55.264,-159.898,55.221,-159.839,55.182,-159.854,55.145,-159.873,55.129]],[[-165.842,54.071,-165.879,54.053,-165.91,54.049,-165.933,54.059,-166.036,54.047,-166.057,54.054,-166.103,54.114,-166.106,54.145,-166.088,54.169,-166.041,54.191,-165.966,54.211,-165.893,54.207,-165.764,54.152,-165.704,54.12,-165.693,54.1,-165.738,54.081,-165.842,54.071]],[[-165.561,54.137,-165.605,54.129,-165.615,54.14,-165.621,54.184,-165.654,54.253,-165.59,54.279,-165.551,54.285,-165.534,54.274,-165.488,54.222,-165.442,54.208,-165.408,54.197,-165.468,54.181,-165.561,54.137]],[[-160.919,58.577,-160.992,58.561,-161.07,58.569,-161.131,58.668,-161.085,58.671,-160.986,58.736,-160.769,58.789,-160.715,58.795,-160.919,58.577]],[[-172.742,60.457,-172.526,60.392,-172.387,60.398,-172.278,60.344,-172.232,60.299,-172.397,60.331,-172.636,60.329,-172.958,60.463,-173.074,60.493,-173.048,60.568,-172.924,60.607,-172.86,60.506,-172.742,60.457]],[[-170.161,57.184,-170.264,57.137,-170.358,57.154,-170.386,57.189,-170.387,57.203,-170.116,57.242,-170.161,57.184]],[[-169.692,52.847,-169.708,52.807,-169.723,52.792,-169.877,52.814,-169.981,52.806,-169.992,52.83,-169.983,52.851,-169.821,52.883,-169.755,52.884,-169.711,52.867,-169.692,52.847]],[[-170.733,52.581,-170.797,52.55,-170.816,52.562,-170.827,52.601,-170.791,52.631,-170.682,52.698,-170.608,52.685,-170.585,52.668,-170.587,52.642,-170.614,52.61,-170.649,52.593,-170.692,52.593,-170.733,52.581]],[[-172.465,52.272,-172.539,52.257,-172.62,52.273,-172.582,52.326,-172.544,52.354,-172.47,52.388,-172.383,52.373,-172.314,52.33,-172.465,52.272]],[[-169.755,56.635,-169.624,56.615,-169.55,56.628,-169.486,56.618,-169.474,56.594,-169.587,56.542,-169.633,56.546,-169.766,56.608,-169.755,56.635]],[[-176.287,51.792,-176.35,51.733,-176.396,51.76,-176.414,51.841,-176.379,51.861,-176.28,51.803,-176.287,51.792]],[[-176.022,52.002,-176.045,51.973,-176.143,52.004,-176.178,52.03,-176.185,52.056,-176.156,52.099,-176.077,52.1,-176.031,52.082,-175.988,52.049,-175.975,52.029,-176.022,52.002]],[[-177.148,51.717,-177.177,51.704,-177.23,51.694,-177.382,51.705,-177.475,51.701,-177.578,51.694,-177.655,51.677,-177.67,51.701,-177.668,51.721,-177.335,51.776,-177.257,51.805,-177.21,51.841,-177.166,51.909,-177.131,51.93,-177.11,51.929,-177.063,51.902,-177.08,51.867,-177.121,51.836,-177.135,51.807,-177.148,51.717]],[[178.575,51.916,178.512,51.899,178.478,51.943,178.475,51.968,178.509,51.995,178.571,51.978,178.607,51.953,178.575,51.916]],[[179.452,51.373,179.278,51.372,178.926,51.535,178.747,51.587,178.648,51.644,178.692,51.656,178.908,51.616,179.084,51.528,179.182,51.47,179.294,51.421,179.416,51.401,179.452,51.373]],[[173.723,52.36,173.658,52.357,173.616,52.391,173.402,52.405,173.425,52.438,173.517,52.451,173.658,52.504,173.776,52.495,173.745,52.447,173.723,52.36]],[[-134.97,57.351,-134.885,57.242,-134.823,57.157,-134.769,57.054,-134.677,56.842,-134.634,56.762,-134.621,56.718,-134.611,56.603,-134.624,56.579,-134.652,56.556,-134.657,56.523,-134.632,56.436,-134.63,56.302,-134.654,56.227,-134.682,56.216,-134.75,56.241,-134.806,56.281,-134.848,56.323,-134.95,56.457,-134.981,56.519,-134.982,56.564,-134.967,56.596,-134.933,56.616,-134.875,56.67,-134.883,56.679,-134.928,56.667,-135.018,56.66,-135.097,56.703,-135.159,56.725,-135.147,56.802,-135.163,56.824,-135.285,56.8,-135.331,56.822,-135.341,56.851,-135.338,56.894,-135.315,56.932,-135.2,57.027,-135.211,57.045,-135.267,57.049,-135.341,57.082,-135.375,57.188,-135.455,57.249,-135.502,57.244,-135.609,57.071,-135.662,57.034,-135.812,57.01,-135.782,57.058,-135.768,57.1,-135.821,57.23,-135.823,57.28,-135.787,57.317,-135.681,57.333,-135.625,57.354,-135.581,57.39,-135.57,57.425,-135.487,57.517,-135.449,57.534,-135.346,57.533,-135.131,57.432,-135.065,57.417,-134.97,57.351]],[[-134.68,58.162,-134.426,58.139,-134.24,58.144,-134.07,57.995,-133.966,57.874,-133.904,57.789,-133.869,57.708,-133.823,57.629,-133.827,57.618,-133.925,57.671,-133.996,57.778,-134.032,57.821,-134.067,57.84,-134.105,57.879,-134.178,57.982,-134.18,58.011,-134.213,58.038,-134.25,58.049,-134.292,58.045,-134.307,58.034,-134.3,57.963,-134.267,57.885,-134.084,57.712,-133.961,57.614,-133.937,57.582,-133.921,57.492,-133.974,57.451,-133.909,57.369,-133.911,57.353,-133.925,57.337,-134.1,57.3,-134.26,57.147,-134.435,57.057,-134.516,57.043,-134.555,57.058,-134.592,57.092,-134.613,57.138,-134.62,57.196,-134.576,57.232,-134.489,57.42,-134.487,57.482,-134.595,57.568,-134.66,57.638,-134.695,57.736,-134.754,57.995,-134.781,58.078,-134.82,58.147,-134.87,58.202,-134.908,58.263,-134.933,58.329,-134.923,58.355,-134.837,58.32,-134.733,58.225,-134.68,58.162]],[[-135.73,58.244,-135.587,58.147,-135.586,58.124,-135.615,58.057,-135.693,58.039,-135.671,58.012,-135.613,57.992,-135.572,58.009,-135.421,58.102,-135.375,58.122,-135.347,58.124,-135.163,58.096,-135.002,58.051,-134.955,58.015,-134.928,57.953,-134.971,57.817,-135.103,57.794,-135.165,57.796,-135.231,57.816,-135.338,57.769,-135.25,57.733,-134.979,57.724,-134.897,57.648,-134.873,57.589,-134.931,57.481,-135.085,57.511,-135.22,57.574,-135.498,57.662,-135.564,57.666,-135.609,57.651,-135.621,57.597,-135.618,57.48,-135.692,57.42,-135.911,57.447,-135.997,57.535,-136.077,57.675,-136.378,57.84,-136.46,57.873,-136.569,57.972,-136.525,58.051,-136.512,58.096,-136.454,58.108,-136.37,58.143,-136.322,58.219,-136.246,58.157,-136.144,58.098,-136.142,58.154,-136.094,58.198,-135.994,58.197,-135.947,58.206,-135.882,58.247,-135.787,58.269,-135.73,58.244]],[[-133.566,56.339,-133.377,56.318,-133.203,56.32,-133.144,56.279,-133.104,56.235,-133.082,56.194,-133.075,56.156,-133.08,56.129,-133.101,56.1,-133.097,56.09,-132.758,55.995,-132.598,55.895,-132.534,55.842,-132.497,55.798,-132.43,55.687,-132.289,55.558,-132.215,55.519,-132.173,55.481,-132.196,55.479,-132.296,55.507,-132.511,55.594,-132.529,55.59,-132.548,55.544,-132.582,55.503,-132.631,55.473,-132.592,55.464,-132.418,55.483,-132.272,55.399,-132.215,55.384,-132.16,55.323,-132.158,55.3,-132.19,55.255,-132.215,55.237,-132.207,55.224,-132.166,55.218,-132.005,55.231,-131.976,55.209,-132,55.034,-131.978,54.969,-131.978,54.94,-131.997,54.901,-131.997,54.869,-131.983,54.835,-131.981,54.805,-132.022,54.726,-132.065,54.713,-132.134,54.713,-132.189,54.735,-132.266,54.802,-132.341,54.907,-132.37,54.922,-132.469,54.938,-132.486,54.95,-132.549,54.953,-132.594,54.996,-132.588,55.052,-132.627,55.11,-132.622,55.136,-132.665,55.147,-132.702,55.131,-132.683,55.074,-132.704,55.03,-132.782,55.048,-132.913,55.188,-133.061,55.301,-133.119,55.328,-133.103,55.36,-133.03,55.378,-132.971,55.376,-132.959,55.396,-133.082,55.504,-133.078,55.535,-133.033,55.59,-133.09,55.613,-133.244,55.595,-133.298,55.607,-133.343,55.651,-133.369,55.689,-133.503,55.696,-133.553,55.691,-133.64,55.749,-133.68,55.785,-133.664,55.804,-133.584,55.837,-133.537,55.832,-133.447,55.797,-133.412,55.798,-133.322,55.845,-133.308,55.886,-133.242,55.921,-133.252,55.957,-133.289,56.019,-133.371,56.036,-133.539,55.999,-133.684,55.943,-133.743,55.965,-133.755,55.999,-133.599,56.094,-133.531,56.146,-133.544,56.177,-133.594,56.216,-133.599,56.316,-133.566,56.339]],[[-133.99,56.845,-133.925,56.776,-133.831,56.781,-133.778,56.729,-133.738,56.65,-133.767,56.6,-133.809,56.611,-133.855,56.582,-133.884,56.485,-133.87,56.389,-133.885,56.292,-133.939,56.194,-133.95,56.128,-133.971,56.108,-133.994,56.101,-134.024,56.119,-134.067,56.133,-134.122,56.077,-134.19,56.077,-134.245,56.203,-134.195,56.414,-134.084,56.456,-134.15,56.513,-134.29,56.58,-134.278,56.617,-134.384,56.724,-134.391,56.749,-134.374,56.839,-134.274,56.918,-134.143,56.932,-134.052,56.898,-134.001,56.869,-133.99,56.845]],[[-152.417,58.36,-152.381,58.352,-152.343,58.412,-152.316,58.413,-152.198,58.363,-152.125,58.374,-152.079,58.312,-152.037,58.307,-151.998,58.314,-151.974,58.31,-151.983,58.244,-152.069,58.178,-152.109,58.161,-152.165,58.178,-152.187,58.185,-152.224,58.214,-152.252,58.251,-152.268,58.252,-152.334,58.208,-152.333,58.187,-152.305,58.154,-152.309,58.134,-152.381,58.124,-152.452,58.129,-152.538,58.101,-152.558,58.119,-152.571,58.168,-152.598,58.163,-152.639,58.102,-152.683,58.063,-152.764,58.031,-152.782,58.016,-152.841,58.014,-152.928,57.994,-152.983,57.997,-153.305,58.063,-153.381,58.087,-153.116,58.239,-152.976,58.297,-152.895,58.294,-152.815,58.276,-152.772,58.279,-152.769,58.346,-152.844,58.396,-152.841,58.416,-152.675,58.451,-152.612,58.446,-152.544,58.428,-152.478,58.4,-152.417,58.36]],[[-167.964,53.345,-168.271,53.238,-168.37,53.16,-168.446,53.084,-168.506,53.043,-168.549,53.036,-168.597,53.016,-168.699,52.963,-168.741,52.957,-169.066,52.834,-169.089,52.832,-169.073,52.864,-168.974,52.91,-168.909,52.951,-168.836,53.02,-168.796,53.045,-168.783,53.079,-168.778,53.149,-168.76,53.175,-168.69,53.227,-168.639,53.256,-168.572,53.266,-168.437,53.257,-168.38,53.283,-168.363,53.304,-168.397,53.322,-168.405,53.354,-168.396,53.409,-168.357,53.458,-168.288,53.5,-168.193,53.533,-168.073,53.557,-167.986,53.558,-167.828,53.508,-167.805,53.485,-167.843,53.435,-167.865,53.387,-167.964,53.345]],[[-166.615,53.901,-166.572,53.853,-166.497,53.884,-166.443,53.925,-166.4,53.978,-166.372,53.999,-166.336,53.971,-166.231,53.933,-166.319,53.874,-166.489,53.785,-166.546,53.726,-166.549,53.701,-166.385,53.721,-166.339,53.718,-166.309,53.698,-166.355,53.674,-166.444,53.652,-166.522,53.61,-166.702,53.537,-166.77,53.476,-166.851,53.453,-166.961,53.447,-167.154,53.408,-167.271,53.371,-167.3,53.35,-167.337,53.341,-167.381,53.342,-167.429,53.326,-167.48,53.292,-167.522,53.276,-167.592,53.273,-167.629,53.259,-167.669,53.26,-167.781,53.3,-167.809,53.324,-167.71,53.371,-167.639,53.387,-167.53,53.394,-167.424,53.437,-167.204,53.495,-167.136,53.526,-167.092,53.636,-167.042,53.655,-167.016,53.698,-166.894,53.697,-166.838,53.648,-166.819,53.641,-166.809,53.646,-166.804,53.685,-166.741,53.713,-166.777,53.733,-166.89,53.759,-166.973,53.771,-167.027,53.769,-167.071,53.783,-167.106,53.813,-167.121,53.843,-167.118,53.873,-167.09,53.906,-167.038,53.942,-166.978,53.963,-166.849,53.978,-166.734,54.002,-166.673,54.006,-166.627,53.996,-166.615,53.901]],[[-173.553,52.136,-173.357,52.096,-173.113,52.1,-173.024,52.091,-173.023,52.079,-173.179,52.063,-173.232,52.068,-173.368,52.046,-173.461,52.042,-173.673,52.063,-173.836,52.048,-173.879,52.054,-173.93,52.072,-173.99,52.104,-173.992,52.123,-173.939,52.131,-173.794,52.104,-173.779,52.118,-173.657,52.144,-173.553,52.136]],[[-174.677,52.035,-175.214,51.994,-175.296,52.022,-175.214,52.038,-175.118,52.047,-174.916,52.094,-174.668,52.135,-174.474,52.184,-174.306,52.216,-174.259,52.269,-174.406,52.296,-174.436,52.317,-174.365,52.342,-174.307,52.378,-174.169,52.42,-174.046,52.367,-174.018,52.332,-174.03,52.29,-174.055,52.246,-174.163,52.223,-174.179,52.2,-174.121,52.135,-174.344,52.078,-174.677,52.035]],[[-176.593,51.867,-176.588,51.833,-176.473,51.837,-176.437,51.82,-176.437,51.754,-176.452,51.736,-176.47,51.731,-176.511,51.746,-176.558,51.712,-176.771,51.63,-176.837,51.676,-176.962,51.604,-176.874,51.79,-176.774,51.819,-176.736,51.84,-176.745,51.895,-176.698,51.986,-176.597,51.982,-176.55,51.944,-176.552,51.92,-176.593,51.867]],[[-177.879,51.65,-177.901,51.616,-177.925,51.617,-178.059,51.673,-178.078,51.691,-178,51.717,-177.977,51.738,-177.986,51.764,-178.045,51.801,-178.153,51.848,-178.195,51.882,-178.168,51.903,-178.117,51.916,-177.954,51.918,-177.866,51.86,-177.8,51.84,-177.644,51.826,-177.725,51.802,-177.771,51.778,-177.827,51.686,-177.879,51.65]],[[179.728,51.905,179.645,51.88,179.55,51.894,179.498,51.933,179.504,51.98,179.627,52.03,179.78,51.967,179.728,51.905]],[[177.415,51.883,177.329,51.841,177.261,51.884,177.25,51.903,177.381,51.976,177.478,51.992,177.521,52.018,177.564,52.11,177.637,52.114,177.67,52.103,177.653,52.06,177.596,51.994,177.594,51.948,177.415,51.883]],[[172.812,53.013,172.984,52.98,173.102,52.996,173.252,52.943,173.436,52.852,173.395,52.835,173.348,52.825,173.303,52.826,173.159,52.811,173.08,52.814,172.935,52.752,172.776,52.797,172.722,52.886,172.595,52.907,172.495,52.938,172.678,53.008,172.812,53.013]],[[-155.581,19.012,-155.626,18.964,-155.681,18.968,-155.881,19.071,-155.906,19.126,-155.891,19.383,-155.966,19.591,-156.049,19.75,-155.988,19.832,-155.909,19.895,-155.82,20.014,-155.893,20.167,-155.874,20.26,-155.832,20.276,-155.622,20.163,-155.199,19.994,-155.086,19.876,-155.066,19.748,-154.989,19.732,-154.953,19.645,-154.841,19.568,-154.804,19.524,-154.85,19.454,-155.053,19.319,-155.31,19.26,-155.535,19.109,-155.581,19.012]],[[-157.214,21.215,-157.002,21.188,-156.952,21.2,-156.917,21.177,-156.742,21.164,-156.712,21.155,-156.748,21.104,-156.86,21.056,-157.021,21.098,-157.29,21.113,-157.279,21.152,-157.254,21.181,-157.25,21.23,-157.214,21.215]],[[-156.487,20.933,-156.461,20.915,-156.354,20.941,-156.278,20.951,-156.148,20.885,-156.104,20.84,-156.019,20.792,-155.99,20.757,-156.014,20.715,-156.107,20.645,-156.235,20.629,-156.31,20.599,-156.409,20.605,-156.438,20.618,-156.449,20.706,-156.48,20.801,-156.544,20.79,-156.615,20.822,-156.69,20.901,-156.698,20.949,-156.657,21.025,-156.585,21.034,-156.532,20.993,-156.487,20.933]],[[-157.799,21.457,-157.765,21.451,-157.721,21.458,-157.706,21.378,-157.654,21.334,-157.635,21.308,-157.691,21.28,-157.799,21.269,-157.849,21.291,-157.902,21.341,-157.958,21.327,-157.968,21.367,-157.978,21.379,-158.017,21.368,-157.981,21.316,-158.079,21.312,-158.11,21.319,-158.138,21.377,-158.239,21.489,-158.239,21.533,-158.273,21.585,-158.123,21.6,-158.02,21.692,-157.962,21.701,-157.852,21.553,-157.854,21.512,-157.83,21.471,-157.799,21.457]],[[-159.373,21.932,-159.461,21.876,-159.512,21.9,-159.609,21.91,-159.646,21.952,-159.748,21.99,-159.789,22.042,-159.727,22.14,-159.579,22.223,-159.352,22.22,-159.305,22.154,-159.301,22.105,-159.33,22.051,-159.344,21.974,-159.373,21.932]],[[-160.18,21.841,-160.2,21.797,-160.235,21.804,-160.243,21.843,-160.221,21.897,-160.164,21.944,-160.101,22.015,-160.049,22.005,-160.077,21.958,-160.08,21.907,-160.153,21.879,-160.18,21.841]],[[-156.85,20.773,-156.909,20.744,-156.973,20.758,-156.988,20.826,-157.051,20.912,-156.942,20.93,-156.881,20.905,-156.848,20.878,-156.809,20.831,-156.85,20.773]],[[-74.709,45.004,-74.663,45.004,-74.43,45.004,-74.014,45.005,-73.598,45.005,-73.182,45.006,-72.766,45.006,-72.35,45.007,-71.934,45.007,-71.518,45.008,-71.419,45.2,-71.327,45.29,-71.202,45.26,-71.135,45.263,-71.06,45.309,-71,45.337,-70.96,45.333,-70.926,45.291,-70.898,45.262,-70.865,45.271,-70.837,45.311,-70.838,45.366,-70.799,45.405,-70.753,45.411,-70.711,45.409,-70.69,45.428,-70.692,45.455,-70.707,45.499,-70.702,45.551,-70.596,45.644,-70.467,45.707,-70.421,45.738,-70.408,45.802,-70.333,45.868,-70.296,45.906,-70.287,45.939,-70.306,45.98,-70.304,46.057,-70.279,46.15,-70.248,46.251,-70.18,46.342,-70.067,46.441,-70.038,46.571,-70.008,46.709,-69.872,46.843,-69.718,46.995,-69.63,47.081,-69.471,47.239,-69.359,47.351,-69.302,47.402,-69.243,47.463,-69.146,47.445,-69.05,47.427,-69.064,47.338,-69.049,47.274,-69.003,47.236,-68.937,47.211,-68.887,47.203,-68.829,47.203,-68.669,47.253,-68.48,47.286,-68.377,47.316,-68.358,47.345,-68.311,47.354,-68.235,47.346,-68.097,47.275,-67.935,47.168,-67.807,47.083,-67.803,46.936,-67.8,46.78,-67.798,46.616,-67.796,46.498,-67.793,46.337,-67.79,46.209,-67.786,46.042,-67.785,45.953,-67.767,45.927,-67.778,45.892,-67.782,45.874,-67.781,45.86,-67.774,45.843,-67.775,45.818,-67.792,45.796,-67.8,45.77,-67.802,45.728,-67.785,45.702,-67.755,45.686,-67.731,45.686,-67.699,45.671,-67.658,45.644,-67.596,45.621,-67.531,45.613,-67.487,45.618,-67.433,45.603,-67.414,45.566,-67.424,45.53,-67.455,45.514,-67.488,45.501,-67.494,45.474,-67.477,45.446,-67.454,45.421,-67.428,45.378,-67.439,45.34,-67.462,45.309,-67.473,45.276,-67.453,45.248,-67.4,45.21,-67.367,45.174,-67.315,45.154,-67.291,45.168,-67.271,45.187,-67.25,45.201,-67.213,45.193,-67.171,45.182,-67.125,45.169,-67.13,45.139,-67.102,45.088,-67.08,44.989,-67.114,44.944,-67.107,44.885,-67.014,44.868,-66.991,44.85,-66.987,44.828,-67.191,44.676,-67.364,44.697,-67.458,44.657,-67.556,44.645,-67.599,44.577,-67.653,44.562,-67.727,44.567,-67.79,44.586,-67.839,44.576,-67.907,44.454,-67.963,44.464,-67.985,44.42,-68.014,44.401,-68.057,44.384,-68.094,44.439,-68.117,44.491,-68.152,44.502,-68.198,44.515,-68.246,44.515,-68.277,44.507,-68.317,44.474,-68.374,44.445,-68.417,44.469,-68.451,44.508,-68.479,44.446,-68.521,44.38,-68.514,44.304,-68.533,44.259,-68.572,44.271,-68.612,44.311,-68.723,44.342,-68.812,44.339,-68.794,44.382,-68.71,44.443,-68.736,44.454,-68.777,44.446,-68.795,44.454,-68.766,44.51,-68.763,44.571,-68.8,44.549,-68.847,44.485,-68.961,44.434,-68.956,44.348,-69.064,44.172,-69.068,44.098,-69.137,44.038,-69.226,43.986,-69.345,44.001,-69.435,43.956,-69.481,43.905,-69.521,43.897,-69.542,43.963,-69.557,43.983,-69.59,43.887,-69.624,43.881,-69.637,43.949,-69.653,43.994,-69.699,43.955,-69.73,43.852,-69.762,43.861,-69.772,43.899,-69.795,43.911,-69.803,43.867,-69.792,43.805,-69.808,43.772,-69.84,43.79,-69.873,43.82,-69.926,43.797,-69.974,43.788,-69.975,43.818,-69.965,43.855,-70.062,43.835,-70.179,43.766,-70.269,43.672,-70.238,43.656,-70.203,43.626,-70.36,43.48,-70.521,43.349,-70.642,43.134,-70.691,43.109,-70.733,43.07,-70.778,42.941,-70.829,42.825,-70.8,42.774,-70.781,42.721,-70.736,42.669,-70.697,42.665,-70.655,42.674,-70.624,42.672,-70.604,42.65,-70.613,42.623,-70.661,42.617,-70.752,42.57,-70.831,42.553,-70.871,42.497,-70.93,42.432,-71.046,42.331,-70.997,42.3,-70.818,42.265,-70.738,42.229,-70.618,42.04,-70.645,42.022,-70.656,41.987,-70.549,41.939,-70.515,41.803,-70.427,41.757,-70.295,41.729,-70.135,41.77,-70.001,41.826,-70.006,41.872,-70.09,41.98,-70.11,42.03,-70.173,42.063,-70.196,42.035,-70.237,42.071,-70.241,42.091,-70.204,42.101,-70.16,42.097,-70.109,42.078,-69.978,41.961,-69.942,41.808,-69.934,41.71,-69.949,41.677,-69.987,41.684,-70.06,41.677,-70.405,41.627,-70.481,41.582,-70.657,41.534,-70.668,41.558,-70.655,41.608,-70.666,41.71,-70.701,41.715,-70.974,41.549,-71.08,41.538,-71.169,41.489,-71.188,41.516,-71.204,41.641,-71.149,41.746,-71.178,41.744,-71.271,41.681,-71.311,41.72,-71.331,41.762,-71.359,41.786,-71.39,41.795,-71.364,41.703,-71.427,41.633,-71.444,41.454,-71.523,41.379,-71.769,41.331,-71.93,41.341,-72.074,41.326,-72.265,41.292,-72.371,41.312,-72.479,41.276,-72.847,41.266,-72.925,41.285,-73.024,41.216,-73.182,41.176,-73.583,41.022,-73.671,40.966,-73.779,40.878,-73.851,40.831,-73.911,40.816,-73.947,40.777,-73.987,40.751,-73.949,40.839,-73.907,40.912,-73.872,41.055,-73.882,41.171,-73.925,41.218,-73.97,41.25,-73.918,41.136,-73.909,40.996,-73.927,40.914,-74.025,40.756,-74.067,40.72,-74.116,40.687,-74.153,40.673,-74.187,40.648,-74.227,40.608,-74.264,40.529,-74.242,40.456,-74.05,40.43,-73.998,40.452,-73.972,40.4,-73.958,40.328,-73.972,40.251,-74.004,40.171,-74.028,40.073,-74.049,39.923,-74.08,39.788,-74.084,39.829,-74.065,39.993,-74.096,39.976,-74.118,39.938,-74.176,39.727,-74.257,39.614,-74.331,39.536,-74.407,39.549,-74.39,39.487,-74.411,39.455,-74.429,39.387,-74.474,39.343,-74.517,39.347,-74.579,39.316,-74.603,39.293,-74.605,39.248,-74.646,39.208,-74.794,39.002,-74.923,38.941,-74.954,38.95,-74.92,39.047,-74.897,39.145,-74.975,39.188,-75.05,39.211,-75.136,39.208,-75.231,39.284,-75.353,39.34,-75.524,39.49,-75.519,39.532,-75.524,39.602,-75.472,39.712,-75.422,39.79,-75.353,39.83,-75.154,39.871,-75.104,39.932,-75.074,39.983,-75.173,39.895,-75.321,39.865,-75.401,39.832,-75.464,39.781,-75.502,39.717,-75.588,39.641,-75.582,39.589,-75.567,39.553,-75.574,39.477,-75.52,39.403,-75.413,39.281,-75.392,39.093,-75.31,38.967,-75.185,38.819,-75.089,38.778,-75.084,38.723,-75.128,38.632,-75.187,38.591,-75.111,38.599,-75.073,38.579,-75.036,38.503,-75.039,38.426,-75.051,38.383,-75.074,38.366,-75.073,38.41,-75.09,38.425,-75.117,38.406,-75.134,38.384,-75.142,38.298,-75.16,38.255,-75.225,38.242,-75.292,38.129,-75.354,38.065,-75.596,37.631,-75.587,37.559,-75.632,37.535,-75.699,37.516,-75.767,37.473,-75.812,37.425,-75.854,37.297,-75.934,37.152,-75.985,37.212,-75.997,37.264,-75.975,37.398,-75.888,37.619,-75.792,37.756,-75.719,37.821,-75.659,37.954,-75.735,37.974,-75.851,37.972,-75.829,38.033,-75.795,38.087,-75.856,38.14,-75.891,38.147,-75.928,38.169,-75.885,38.214,-75.864,38.261,-75.877,38.319,-75.859,38.362,-75.889,38.356,-75.937,38.31,-75.967,38.291,-75.986,38.332,-76.007,38.323,-76.02,38.295,-76.051,38.28,-76.117,38.318,-76.212,38.361,-76.265,38.436,-76.295,38.495,-76.264,38.6,-76.198,38.619,-76.113,38.602,-76.001,38.602,-76.017,38.625,-76.057,38.621,-76.175,38.707,-76.213,38.758,-76.278,38.772,-76.308,38.723,-76.341,38.71,-76.3,38.818,-76.247,38.823,-76.168,38.853,-76.191,38.916,-76.241,38.943,-76.331,38.909,-76.33,38.953,-76.313,39.009,-76.245,39.009,-76.186,38.991,-76.135,39.082,-76.133,39.123,-76.217,39.064,-76.236,39.192,-76.153,39.315,-76.074,39.369,-75.976,39.367,-75.876,39.376,-75.939,39.399,-76.003,39.411,-75.955,39.46,-75.913,39.468,-75.873,39.511,-75.97,39.505,-75.959,39.585,-76.006,39.569,-76.063,39.561,-76.085,39.527,-76.081,39.47,-76.097,39.433,-76.141,39.403,-76.216,39.38,-76.223,39.42,-76.248,39.439,-76.257,39.352,-76.276,39.323,-76.331,39.404,-76.347,39.388,-76.345,39.365,-76.359,39.325,-76.406,39.304,-76.403,39.253,-76.421,39.225,-76.57,39.269,-76.574,39.254,-76.489,39.159,-76.428,39.126,-76.42,39.074,-76.473,39.031,-76.546,39.068,-76.559,39.065,-76.519,39.001,-76.494,38.945,-76.52,38.898,-76.516,38.841,-76.521,38.788,-76.537,38.743,-76.501,38.532,-76.458,38.475,-76.416,38.42,-76.394,38.369,-76.439,38.362,-76.51,38.404,-76.572,38.436,-76.647,38.539,-76.659,38.58,-76.677,38.612,-76.669,38.538,-76.642,38.454,-76.409,38.268,-76.366,38.197,-76.333,38.141,-76.341,38.087,-76.402,38.125,-76.454,38.174,-76.594,38.228,-76.769,38.263,-76.868,38.39,-76.868,38.337,-76.89,38.292,-76.95,38.347,-76.988,38.394,-77.001,38.445,-77.077,38.442,-77.156,38.397,-77.233,38.408,-77.242,38.495,-77.221,38.541,-77.135,38.65,-77.054,38.706,-77.018,38.778,-77.03,38.889,-77.046,38.776,-77.092,38.72,-77.165,38.677,-77.26,38.6,-77.284,38.529,-77.314,38.397,-77.273,38.352,-77.232,38.34,-77.11,38.37,-77.047,38.357,-76.906,38.197,-76.645,38.134,-76.55,38.094,-76.472,38.011,-76.355,37.963,-76.264,37.894,-76.262,37.848,-76.293,37.794,-76.306,37.722,-76.344,37.676,-76.437,37.67,-76.492,37.682,-76.793,37.938,-76.829,37.962,-76.94,38.095,-77.071,38.167,-77.111,38.166,-76.925,38.033,-76.849,37.94,-76.715,37.81,-76.62,37.755,-76.549,37.669,-76.484,37.629,-76.306,37.571,-76.368,37.53,-76.269,37.495,-76.254,37.431,-76.263,37.357,-76.401,37.386,-76.405,37.332,-76.393,37.3,-76.454,37.274,-76.538,37.309,-76.758,37.505,-76.756,37.479,-76.738,37.449,-76.611,37.323,-76.497,37.247,-76.401,37.213,-76.327,37.149,-76.301,37.111,-76.283,37.053,-76.338,37.013,-76.401,36.991,-76.462,37.031,-76.507,37.072,-76.602,37.143,-76.631,37.222,-76.704,37.218,-77.007,37.318,-77.251,37.329,-77.227,37.309,-77.196,37.296,-77.002,37.271,-76.925,37.225,-76.765,37.184,-76.672,37.173,-76.634,37.047,-76.505,36.961,-76.488,36.897,-76.4,36.89,-76.244,36.953,-76.144,36.931,-75.999,36.913,-75.966,36.862,-75.942,36.766,-75.89,36.657,-75.758,36.229,-75.559,35.879,-75.534,35.819,-75.58,35.872,-75.728,36.104,-75.81,36.271,-75.894,36.567,-75.918,36.633,-75.946,36.659,-75.965,36.638,-75.973,36.6,-75.96,36.571,-75.993,36.474,-75.978,36.429,-75.925,36.383,-75.867,36.268,-75.82,36.113,-75.883,36.176,-75.95,36.209,-76.055,36.235,-76.148,36.279,-76.141,36.215,-76.15,36.146,-76.222,36.167,-76.271,36.19,-76.227,36.116,-76.321,36.138,-76.384,36.134,-76.424,36.068,-76.479,36.028,-76.559,36.015,-76.679,36.075,-76.718,36.148,-76.734,36.229,-76.74,36.133,-76.719,36.033,-76.726,35.958,-76.611,35.944,-76.504,35.956,-76.358,35.953,-76.264,35.967,-76.207,35.991,-76.07,35.97,-76.06,35.879,-76.076,35.788,-76.084,35.691,-76.046,35.691,-76.001,35.722,-75.979,35.896,-75.854,35.96,-75.812,35.956,-75.772,35.9,-75.759,35.843,-75.745,35.765,-75.774,35.647,-75.966,35.508,-76.104,35.38,-76.174,35.354,-76.275,35.369,-76.39,35.401,-76.447,35.408,-76.49,35.397,-76.516,35.436,-76.532,35.508,-76.577,35.532,-76.611,35.53,-76.634,35.453,-76.741,35.431,-76.887,35.463,-77.04,35.527,-76.974,35.458,-76.595,35.33,-76.553,35.306,-76.513,35.27,-76.566,35.215,-76.608,35.153,-76.613,35.104,-76.628,35.073,-76.779,34.99,-76.861,35.005,-77.07,35.155,-76.975,35.025,-76.899,34.97,-76.745,34.941,-76.457,34.989,-76.362,34.937,-76.44,34.843,-76.517,34.777,-76.618,34.77,-76.707,34.752,-76.733,34.707,-76.797,34.704,-76.896,34.701,-77.05,34.697,-77.134,34.708,-77.252,34.616,-77.296,34.603,-77.358,34.62,-77.384,34.694,-77.412,34.731,-77.413,34.592,-77.402,34.555,-77.38,34.527,-77.518,34.451,-77.65,34.358,-77.697,34.332,-77.751,34.285,-77.861,34.149,-77.888,34.05,-77.928,33.94,-77.933,33.989,-77.926,34.073,-77.953,34.169,-77.971,33.993,-78.013,33.912,-78.406,33.918,-78.578,33.873,-78.841,33.724,-78.92,33.659,-79.138,33.406,-79.194,33.244,-79.238,33.312,-79.227,33.363,-79.226,33.405,-79.281,33.315,-79.229,33.185,-79.276,33.135,-79.42,33.043,-79.499,33.027,-79.587,33.001,-79.615,32.909,-79.735,32.825,-79.805,32.787,-79.933,32.81,-79.894,32.729,-79.941,32.667,-80.022,32.62,-80.123,32.589,-80.18,32.593,-80.23,32.577,-80.268,32.537,-80.363,32.501,-80.461,32.521,-80.572,32.534,-80.634,32.512,-80.53,32.475,-80.474,32.423,-80.486,32.352,-80.514,32.324,-80.579,32.287,-80.608,32.293,-80.626,32.326,-80.647,32.396,-80.678,32.381,-80.683,32.349,-80.709,32.337,-80.803,32.448,-80.798,32.363,-80.765,32.298,-80.734,32.265,-80.702,32.246,-80.694,32.216,-80.758,32.142,-80.791,32.126,-80.849,32.114,-80.882,32.069,-80.872,32.03,-80.923,31.945,-81.046,31.892,-81.083,31.894,-81.113,31.879,-81.096,31.841,-81.065,31.813,-81.066,31.788,-81.098,31.753,-81.162,31.744,-81.198,31.704,-81.187,31.667,-81.166,31.646,-81.17,31.61,-81.242,31.574,-81.259,31.539,-81.223,31.528,-81.196,31.539,-81.175,31.531,-81.219,31.472,-81.258,31.436,-81.295,31.371,-81.381,31.353,-81.378,31.332,-81.329,31.314,-81.288,31.264,-81.365,31.172,-81.413,31.179,-81.442,31.2,-81.46,31.127,-81.453,31.088,-81.471,31.009,-81.501,30.914,-81.52,30.875,-81.516,30.802,-81.504,30.731,-81.457,30.641,-81.386,30.27,-81.337,30.141,-81.25,29.794,-81.105,29.457,-80.9,29.05,-80.564,28.556,-80.524,28.486,-80.568,28.426,-80.581,28.365,-80.585,28.272,-80.573,28.181,-80.533,28.07,-80.457,27.901,-80.5,27.934,-80.61,28.178,-80.623,28.32,-80.607,28.523,-80.633,28.518,-80.654,28.452,-80.665,28.375,-80.694,28.345,-80.732,28.463,-80.729,28.516,-80.688,28.579,-80.7,28.601,-80.766,28.633,-80.78,28.683,-80.771,28.732,-80.809,28.759,-80.838,28.758,-80.818,28.636,-80.787,28.561,-80.749,28.381,-80.686,28.272,-80.65,28.181,-80.226,27.207,-80.126,27.083,-80.089,26.994,-80.05,26.808,-80.041,26.569,-80.111,26.132,-80.126,25.833,-80.136,25.843,-80.143,25.874,-80.159,25.878,-80.219,25.742,-80.301,25.619,-80.328,25.427,-80.367,25.331,-80.485,25.23,-80.558,25.232,-80.737,25.156,-80.862,25.176,-81.012,25.133,-81.11,25.138,-81.167,25.229,-81.159,25.269,-81.136,25.31,-81.098,25.319,-80.965,25.224,-80.94,25.264,-80.98,25.312,-81.057,25.338,-81.113,25.367,-81.227,25.583,-81.345,25.732,-81.365,25.831,-81.568,25.892,-81.715,25.983,-81.811,26.146,-81.867,26.435,-81.931,26.467,-81.959,26.49,-81.896,26.597,-81.829,26.687,-81.882,26.665,-81.921,26.631,-81.97,26.552,-82.006,26.54,-82.04,26.552,-82.078,26.704,-82.067,26.892,-82.013,26.962,-82.096,26.963,-82.181,26.937,-82.169,26.874,-82.181,26.84,-82.243,26.849,-82.29,26.871,-82.354,26.936,-82.441,27.06,-82.62,27.401,-82.655,27.449,-82.715,27.5,-82.687,27.515,-82.636,27.525,-82.521,27.678,-82.431,27.771,-82.401,27.835,-82.406,27.863,-82.446,27.903,-82.498,27.868,-82.521,27.878,-82.58,27.958,-82.636,27.981,-82.675,27.964,-82.634,27.898,-82.597,27.873,-82.611,27.777,-82.626,27.746,-82.661,27.718,-82.715,27.733,-82.743,27.709,-82.775,27.734,-82.808,27.777,-82.844,27.846,-82.749,28.237,-82.661,28.486,-82.651,28.77,-82.644,28.812,-82.651,28.888,-82.769,29.052,-83.29,29.452,-83.694,29.926,-84.044,30.104,-84.31,30.065,-84.356,30.029,-84.375,29.982,-84.359,29.929,-84.383,29.907,-84.454,29.91,-84.55,29.898,-84.801,29.773,-84.889,29.778,-84.969,29.745,-85.029,29.721,-85.186,29.708,-85.319,29.68,-85.376,29.695,-85.414,29.768,-85.414,29.842,-85.383,29.785,-85.336,29.74,-85.315,29.758,-85.307,29.798,-85.354,29.876,-85.504,29.976,-85.676,30.122,-85.623,30.117,-85.61,30.148,-85.663,30.189,-85.641,30.237,-85.604,30.287,-85.676,30.279,-85.741,30.244,-85.743,30.201,-85.756,30.167,-85.791,30.172,-85.856,30.214,-86.175,30.333,-86.454,30.399,-86.24,30.429,-86.124,30.406,-86.138,30.442,-86.166,30.464,-86.257,30.493,-86.374,30.482,-86.448,30.496,-86.523,30.467,-86.606,30.425,-86.68,30.403,-86.968,30.372,-87.201,30.339,-87.164,30.374,-87.124,30.397,-86.986,30.431,-86.965,30.502,-86.998,30.57,-87.034,30.554,-87.072,30.5,-87.119,30.539,-87.171,30.539,-87.185,30.454,-87.251,30.397,-87.281,30.339,-87.476,30.294,-87.501,30.309,-87.444,30.364,-87.448,30.394,-87.513,30.368,-87.622,30.265,-88.006,30.231,-87.985,30.254,-87.904,30.259,-87.79,30.292,-87.813,30.347,-87.857,30.407,-87.898,30.414,-87.924,30.45,-87.923,30.562,-87.949,30.627,-88.011,30.694,-88.032,30.681,-88.078,30.566,-88.117,30.415,-88.135,30.367,-88.249,30.363,-88.35,30.373,-88.692,30.355,-88.82,30.406,-88.873,30.416,-88.905,30.415,-89.054,30.368,-89.224,30.332,-89.264,30.344,-89.321,30.345,-89.444,30.223,-89.588,30.166,-89.954,30.269,-90.045,30.351,-90.126,30.369,-90.225,30.379,-90.332,30.278,-90.413,30.14,-90.285,30.065,-90.175,30.029,-89.994,30.059,-89.894,30.126,-89.812,30.124,-89.773,30.137,-89.737,30.172,-89.668,30.145,-89.665,30.117,-89.715,30.078,-89.777,30.046,-89.815,30.007,-89.744,29.93,-89.632,29.904,-89.59,29.915,-89.563,30.002,-89.494,30.058,-89.401,30.046,-89.414,30.011,-89.401,29.978,-89.358,29.921,-89.363,29.84,-89.354,29.82,-89.455,29.784,-89.531,29.772,-89.591,29.725,-89.559,29.698,-89.621,29.674,-89.662,29.684,-89.683,29.675,-89.689,29.646,-89.721,29.619,-89.675,29.539,-89.58,29.486,-89.514,29.42,-89.246,29.333,-89.181,29.336,-89.117,29.248,-89.065,29.218,-89.016,29.203,-89.021,29.143,-89.11,29.099,-89.133,29.046,-89.156,29.017,-89.195,29.054,-89.236,29.081,-89.331,28.999,-89.376,28.981,-89.354,29.07,-89.389,29.105,-89.443,29.194,-89.522,29.249,-89.577,29.268,-89.62,29.302,-89.672,29.317,-89.717,29.313,-89.792,29.333,-89.797,29.381,-89.818,29.416,-89.877,29.458,-90.159,29.537,-90.161,29.504,-90.141,29.48,-90.101,29.463,-90.052,29.431,-90.053,29.337,-90.074,29.297,-90.083,29.24,-90.101,29.182,-90.136,29.136,-90.213,29.105,-90.247,29.131,-90.302,29.256,-90.379,29.295,-90.502,29.3,-90.586,29.272,-90.677,29.151,-90.751,29.131,-91.003,29.194,-91.29,29.289,-91.283,29.321,-91.237,29.331,-91.151,29.318,-91.155,29.351,-91.244,29.457,-91.26,29.505,-91.249,29.564,-91.278,29.563,-91.331,29.514,-91.514,29.555,-91.565,29.605,-91.672,29.746,-91.824,29.751,-91.893,29.836,-92.017,29.8,-92.08,29.761,-92.135,29.699,-92.114,29.668,-92.059,29.617,-92.084,29.593,-92.261,29.557,-92.671,29.597,-92.791,29.635,-92.952,29.714,-93.176,29.779,-93.283,29.789,-93.388,29.777,-93.695,29.77,-93.766,29.753,-93.826,29.725,-93.866,29.756,-93.884,29.81,-93.848,29.819,-93.809,29.851,-93.773,29.914,-93.769,29.952,-93.794,29.977,-93.841,29.98,-93.946,29.815,-93.886,29.723,-93.89,29.689,-94.1,29.67,-94.574,29.485,-94.76,29.384,-94.75,29.418,-94.526,29.548,-94.605,29.568,-94.733,29.535,-94.778,29.548,-94.724,29.655,-94.742,29.75,-94.832,29.753,-94.89,29.677,-94.93,29.68,-94.982,29.713,-95.023,29.702,-94.993,29.531,-94.936,29.46,-94.888,29.371,-95.018,29.259,-95.139,29.168,-95.152,29.079,-95.273,28.964,-95.388,28.898,-95.656,28.745,-95.732,28.712,-95.853,28.64,-96.02,28.587,-96.181,28.502,-96.235,28.489,-96.132,28.561,-96.011,28.632,-96.115,28.622,-96.275,28.655,-96.373,28.657,-96.374,28.631,-96.449,28.594,-96.526,28.648,-96.56,28.684,-96.576,28.716,-96.608,28.723,-96.64,28.709,-96.525,28.489,-96.475,28.479,-96.421,28.457,-96.489,28.406,-96.562,28.367,-96.676,28.341,-96.774,28.422,-96.795,28.321,-96.807,28.22,-96.84,28.194,-96.892,28.158,-96.92,28.185,-96.933,28.224,-96.967,28.19,-97.015,28.163,-97.096,28.158,-97.156,28.144,-97.155,28.103,-97.141,28.061,-97.034,28.094,-97.073,27.986,-97.171,27.88,-97.252,27.854,-97.374,27.87,-97.404,27.859,-97.431,27.837,-97.289,27.671,-97.38,27.419,-97.439,27.328,-97.48,27.317,-97.524,27.314,-97.682,27.395,-97.768,27.458,-97.692,27.287,-97.485,27.237,-97.475,27.173,-97.476,27.118,-97.517,27.053,-97.555,26.967,-97.527,26.908,-97.494,26.76,-97.466,26.692,-97.435,26.486,-97.402,26.397,-97.214,26.068,-97.15,26.065,-97.14,26.03,-97.146,25.961,-97.282,25.942,-97.339,25.911,-97.35,25.885,-97.358,25.871,-97.376,25.872,-97.44,25.891,-97.587,25.984,-97.801,26.042,-98.083,26.064,-98.275,26.111,-98.378,26.182,-98.486,26.225,-98.598,26.238,-98.691,26.276,-98.765,26.34,-98.873,26.381,-99.015,26.399,-99.108,26.447,-99.172,26.564,-99.172,26.566,-99.23,26.762,-99.302,26.885,-99.444,27.037,-99.456,27.057,-99.457,27.057,-99.458,27.082,-99.44,27.17,-99.455,27.234,-99.5,27.285,-99.51,27.34,-99.486,27.398,-99.484,27.467,-99.505,27.548,-99.595,27.636,-99.754,27.73,-99.89,27.867,-100.001,28.048,-100.112,28.173,-100.221,28.243,-100.296,28.328,-100.336,28.428,-100.348,28.486,-100.332,28.503,-100.399,28.614,-100.55,28.821,-100.636,28.973,-100.659,29.069,-100.755,29.183,-100.924,29.315,-101.016,29.401,-101.039,29.46,-101.304,29.634,-101.38,29.743,-101.44,29.777,-101.509,29.773,-101.545,29.784,-101.546,29.808,-101.569,29.809,-101.612,29.787,-101.752,29.782,-101.991,29.796,-102.163,29.825,-102.269,29.871,-102.343,29.865,-102.386,29.807,-102.476,29.769,-102.615,29.752,-102.734,29.644,-102.834,29.444,-102.878,29.315,-102.866,29.258,-102.892,29.216,-102.957,29.19,-103.023,29.132,-103.09,29.042,-103.168,28.998,-103.258,29.001,-103.423,29.071,-103.664,29.207,-103.853,29.291,-103.99,29.323,-104.111,29.386,-104.216,29.48,-104.312,29.542,-104.401,29.574,-104.504,29.678,-104.622,29.854,-104.681,29.991,-104.681,30.134,-104.836,30.448,-104.918,30.583,-104.979,30.646,-105.098,30.721,-105.276,30.807,-105.514,30.981,-105.813,31.241,-106.024,31.398,-106.148,31.451,-106.256,31.545,-106.347,31.679,-106.436,31.764,-106.445,31.768,-106.453,31.77,-106.673,31.771,-106.893,31.772,-107.113,31.774,-107.333,31.775,-107.552,31.776,-107.772,31.777,-107.992,31.778,-108.212,31.779,-108.212,31.667,-108.213,31.554,-108.214,31.442,-108.214,31.329,-108.568,31.329,-108.921,31.328,-109.275,31.327,-109.628,31.327,-109.982,31.326,-110.335,31.326,-110.689,31.325,-111.042,31.324,-111.516,31.472,-111.99,31.62,-112.465,31.768,-112.939,31.916,-113.413,32.064,-113.887,32.212,-114.362,32.36,-114.836,32.508,-114.788,32.565,-114.725,32.715,-114.839,32.705,-115.125,32.683,-115.411,32.662,-115.698,32.64,-115.984,32.619,-116.27,32.598,-116.556,32.576,-116.842,32.555,-117.128,32.533,-117.13,32.54,-117.137,32.649,-117.184,32.688,-117.243,32.664,-117.271,32.806,-117.256,32.873,-117.263,32.939,-117.319,33.1,-117.467,33.296,-117.789,33.538,-117.952,33.62,-118.081,33.722,-118.162,33.751,-118.264,33.759,-118.294,33.712,-118.41,33.744,-118.393,33.858,-118.506,34.017,-118.599,34.035,-118.832,34.024,-119.144,34.112,-119.236,34.164,-119.268,34.257,-119.414,34.339,-119.606,34.418,-119.713,34.4,-119.853,34.412,-120.053,34.469,-120.17,34.476,-120.396,34.46,-120.481,34.472,-120.56,34.544,-120.645,34.58,-120.627,34.669,-120.638,34.749,-120.625,34.812,-120.663,34.949,-120.634,35.076,-120.659,35.122,-120.707,35.158,-120.857,35.21,-120.885,35.275,-120.86,35.365,-120.9,35.425,-121.023,35.481,-121.138,35.607,-121.284,35.676,-121.344,35.792,-121.434,35.864,-121.465,35.927,-121.664,36.154,-121.877,36.331,-121.91,36.433,-121.919,36.572,-121.835,36.657,-121.79,36.732,-121.795,36.801,-121.807,36.851,-121.881,36.939,-122.164,36.991,-122.395,37.208,-122.408,37.373,-122.499,37.543,-122.5,37.653,-122.514,37.772,-122.446,37.798,-122.384,37.789,-122.39,37.741,-122.37,37.656,-122.298,37.592,-122.229,37.564,-122.166,37.502,-122.119,37.483,-122.071,37.478,-122.097,37.518,-122.124,37.544,-122.158,37.626,-122.222,37.732,-122.296,37.79,-122.333,37.897,-122.365,37.921,-122.385,37.961,-122.314,38.007,-122.217,38.041,-122.087,38.05,-121.717,38.034,-121.638,38.061,-121.573,38.052,-121.525,38.056,-121.626,38.084,-121.682,38.075,-121.749,38.08,-121.881,38.075,-121.934,38.087,-121.993,38.12,-122.031,38.124,-122.154,38.066,-122.208,38.073,-122.337,38.136,-122.393,38.145,-122.484,38.109,-122.495,37.954,-122.467,37.838,-122.521,37.826,-122.584,37.874,-122.681,37.902,-122.76,37.946,-122.873,38.026,-122.932,38.055,-122.999,37.989,-123.001,38.019,-122.968,38.097,-122.978,38.227,-122.877,38.123,-122.908,38.197,-122.987,38.277,-123.046,38.305,-123.121,38.449,-123.29,38.536,-123.425,38.676,-123.701,38.907,-123.72,39.111,-123.82,39.368,-123.778,39.515,-123.783,39.619,-123.833,39.775,-123.884,39.861,-124.108,40.095,-124.324,40.252,-124.357,40.371,-124.372,40.491,-124.325,40.598,-124.284,40.711,-124.254,40.74,-124.242,40.728,-124.251,40.704,-124.22,40.696,-124.208,40.746,-124.19,40.772,-124.223,40.775,-124.219,40.791,-124.2,40.822,-124.133,40.97,-124.14,41.156,-124.069,41.384,-124.072,41.46,-124.118,41.622,-124.163,41.719,-124.245,41.788,-124.209,41.889,-124.212,41.985,-124.355,42.123,-124.41,42.304,-124.421,42.381,-124.406,42.584,-124.444,42.67,-124.54,42.813,-124.499,42.937,-124.454,43.012,-124.347,43.342,-124.321,43.368,-124.275,43.367,-124.197,43.423,-124.233,43.436,-124.288,43.41,-124.239,43.54,-124.184,43.652,-124.149,43.692,-124.131,44.056,-124.099,44.334,-124.047,44.425,-124.065,44.52,-124.045,44.648,-124.059,44.778,-123.949,45.401,-123.963,45.476,-123.929,45.577,-123.961,45.843,-123.947,46.141,-123.975,46.178,-123.989,46.219,-123.963,46.225,-123.912,46.182,-123.674,46.183,-123.522,46.223,-123.466,46.209,-123.402,46.155,-123.322,46.144,-123.221,46.154,-123.251,46.167,-123.299,46.171,-123.405,46.221,-123.465,46.271,-123.65,46.268,-123.688,46.3,-123.896,46.268,-123.96,46.301,-124.073,46.279,-124.045,46.373,-124.05,46.491,-124.044,46.605,-124.016,46.521,-123.946,46.433,-123.912,46.533,-123.889,46.66,-123.958,46.709,-124.072,46.745,-124.113,46.863,-123.843,46.963,-123.986,46.984,-124.042,47.03,-124.112,47.035,-124.117,47,-124.139,46.955,-124.164,47.015,-124.171,47.087,-124.199,47.209,-124.309,47.405,-124.376,47.659,-124.46,47.784,-124.621,47.904,-124.663,47.974,-124.702,48.152,-124.68,48.286,-124.71,48.38,-124.633,48.375,-124.429,48.301,-124.175,48.242,-124.099,48.2,-123.976,48.168,-123.294,48.12,-123.25,48.124,-123.162,48.155,-123.124,48.151,-123.024,48.082,-122.974,48.073,-122.909,48.077,-122.861,48.09,-122.779,48.138,-122.768,48.12,-122.769,48.076,-122.74,48.013,-122.679,47.932,-122.657,47.881,-122.778,47.738,-122.802,47.735,-122.805,47.784,-122.821,47.793,-123.051,47.552,-123.131,47.438,-123.139,47.386,-123.136,47.356,-123.104,47.348,-123.031,47.36,-122.922,47.408,-122.917,47.418,-123.018,47.401,-123.067,47.4,-123.06,47.454,-123.049,47.479,-122.982,47.559,-122.913,47.607,-122.814,47.659,-122.757,47.701,-122.718,47.762,-122.608,47.835,-122.588,47.856,-122.593,47.916,-122.586,47.928,-122.533,47.92,-122.511,47.816,-122.524,47.769,-122.618,47.713,-122.63,47.693,-122.614,47.616,-122.628,47.608,-122.664,47.617,-122.675,47.612,-122.586,47.528,-122.557,47.463,-122.554,47.405,-122.578,47.293,-122.604,47.275,-122.649,47.281,-122.708,47.316,-122.721,47.305,-122.768,47.218,-122.783,47.226,-122.813,47.329,-122.828,47.337,-122.92,47.29,-122.956,47.245,-122.988,47.173,-123.028,47.139,-122.914,47.131,-122.812,47.146,-122.73,47.112,-122.702,47.111,-122.627,47.144,-122.604,47.167,-122.542,47.276,-122.511,47.295,-122.465,47.296,-122.42,47.312,-122.354,47.372,-122.351,47.395,-122.375,47.528,-122.368,47.604,-122.381,47.628,-122.41,47.653,-122.407,47.677,-122.384,47.716,-122.382,47.752,-122.402,47.784,-122.393,47.821,-122.33,47.899,-122.318,47.933,-122.242,48.011,-122.261,48.042,-122.317,48.08,-122.353,48.114,-122.389,48.166,-122.416,48.184,-122.425,48.176,-122.387,48.09,-122.395,48.084,-122.494,48.13,-122.517,48.16,-122.529,48.199,-122.52,48.229,-122.467,48.258,-122.403,48.269,-122.409,48.294,-122.488,48.374,-122.542,48.411,-122.583,48.429,-122.638,48.433,-122.662,48.446,-122.669,48.465,-122.657,48.49,-122.628,48.498,-122.543,48.488,-122.497,48.506,-122.501,48.538,-122.515,48.555,-122.513,48.669,-122.545,48.762,-122.562,48.778,-122.58,48.78,-122.599,48.767,-122.653,48.764,-122.686,48.794,-122.722,48.853,-122.789,48.993,-122.686,48.993,-122.26,48.993,-121.834,48.993,-121.407,48.993,-120.981,48.993,-120.554,48.993,-120.128,48.993,-119.702,48.993,-119.275,48.993,-118.849,48.993,-118.423,48.993,-117.996,48.993,-117.57,48.993,-117.143,48.993,-116.717,48.993,-116.291,48.993,-115.864,48.993,-115.438,48.993,-115.012,48.993,-114.585,48.993,-114.159,48.993,-113.732,48.993,-113.306,48.993,-112.88,48.993,-112.453,48.993,-112.027,48.993,-111.6,48.993,-111.174,48.993,-110.748,48.993,-110.321,48.993,-109.895,48.993,-109.469,48.993,-109.042,48.993,-108.616,48.993,-108.189,48.993,-107.763,48.993,-107.337,48.993,-106.91,48.993,-106.484,48.993,-106.057,48.993,-105.631,48.993,-105.205,48.993,-104.778,48.993,-104.352,48.993,-103.926,48.993,-103.499,48.993,-103.073,48.993,-102.646,48.993,-102.22,48.993,-101.794,48.993,-101.367,48.993,-100.941,48.993,-100.515,48.993,-100.088,48.993,-99.662,48.993,-99.235,48.993,-98.809,48.993,-98.383,48.993,-97.956,48.993,-97.53,48.993,-97.103,48.993,-96.677,48.993,-96.251,48.993,-95.824,48.993,-95.398,48.993,-95.162,48.992,-95.158,49.203,-95.155,49.37,-94.939,49.349,-94.875,49.319,-94.854,49.305,-94.86,49.259,-94.843,49.119,-94.803,49.003,-94.713,48.863,-94.705,48.808,-94.675,48.774,-94.621,48.743,-94.414,48.704,-94.055,48.659,-93.852,48.607,-93.804,48.549,-93.708,48.525,-93.564,48.537,-93.464,48.561,-93.378,48.617,-93.258,48.629,-93.155,48.625,-93.052,48.62,-92.996,48.612,-92.837,48.568,-92.733,48.532,-92.583,48.465,-92.501,48.435,-92.461,48.366,-92.415,48.277,-92.348,48.277,-92.299,48.329,-92.172,48.338,-92.005,48.302,-91.858,48.198,-91.647,48.105,-91.518,48.058,-91.387,48.059,-91.221,48.105,-91.043,48.194,-90.916,48.209,-90.84,48.201,-90.797,48.131,-90.744,48.105,-90.607,48.113,-90.32,48.099,-90.092,48.118,-90.04,48.078,-89.994,48.015,-89.901,47.995,-89.775,48.015,-89.551,48,-89.456,47.996,-89.273,48.02,-89.186,48.047,-89.063,48.094,-88.899,48.156,-88.612,48.264,-88.378,48.303,-88.161,48.225,-87.987,48.157,-87.921,48.13,-87.744,48.061,-87.494,47.962,-87.208,47.848,-86.922,47.735,-86.672,47.636,-86.496,47.567,-86.429,47.54,-86.234,47.46,-86.04,47.38,-85.846,47.3,-85.652,47.22,-85.458,47.14,-85.264,47.06,-85.07,46.98,-84.876,46.9,-84.827,46.767,-84.779,46.637,-84.666,46.543,-84.562,46.457,-84.502,46.462,-84.44,46.498,-84.402,46.516,-84.337,46.519,-84.192,46.55,-84.149,46.543,-84.125,46.527,-84.123,46.503,-84.128,46.484,-84.15,46.445,-84.115,46.371,-84.108,46.289,-84.088,46.227,-84.029,46.147,-83.978,46.085,-83.913,46.073,-83.763,46.109,-83.669,46.123,-83.616,46.117,-83.525,46.059,-83.48,46.024,-83.469,45.995,-83.593,45.817,-83.397,45.729,-83.179,45.633,-82.919,45.518,-82.76,45.448,-82.551,45.347,-82.515,45.204,-82.485,45.084,-82.447,44.916,-82.407,44.744,-82.368,44.573,-82.327,44.392,-82.281,44.192,-82.241,44.015,-82.197,43.822,-82.138,43.571,-82.19,43.474,-82.305,43.263,-82.408,43.073,-82.417,43.017,-82.488,42.74,-82.545,42.625,-82.645,42.558,-82.744,42.493,-82.868,42.385,-83.004,42.332,-83.073,42.3,-83.11,42.251,-83.15,42.142,-83.142,41.976,-83.03,41.833,-82.866,41.753,-82.69,41.675,-82.439,41.675,-82.213,41.779,-81.974,41.889,-81.761,41.987,-81.507,42.103,-81.278,42.209,-81.028,42.247,-80.683,42.3,-80.248,42.366,-80.036,42.441,-79.762,42.539,-79.446,42.651,-79.174,42.749,-79.037,42.802,-78.939,42.864,-78.915,42.909,-78.921,42.935,-78.946,42.961,-78.981,42.981,-79.012,42.997,-79.026,43.017,-79.029,43.062,-79.048,43.087,-79.066,43.106,-79.059,43.278,-79.083,43.331,-79.172,43.467,-79.002,43.527,-78.846,43.583,-78.72,43.625,-78.458,43.631,-78.215,43.631,-77.879,43.63,-77.597,43.629,-77.267,43.627,-77.073,43.627,-76.82,43.629,-76.696,43.785,-76.586,43.924,-76.465,44.058,-76.249,44.214,-76.186,44.242,-76.151,44.304,-76.02,44.363,-75.876,44.417,-75.819,44.468,-75.792,44.497,-75.401,44.772,-75.179,44.899,-74.996,44.97,-74.857,45.004,-74.762,44.999,-74.709,45.004]],[[-72.51,40.986,-72.581,40.921,-72.517,40.915,-72.461,40.934,-72.409,40.972,-72.287,41.024,-72.184,41.047,-72.151,41.051,-72.102,41.015,-72.004,41.044,-71.903,41.061,-72.339,40.894,-72.428,40.875,-72.556,40.826,-72.676,40.791,-72.763,40.778,-73.194,40.654,-73.229,40.652,-73.266,40.664,-73.621,40.6,-73.767,40.593,-73.9,40.571,-73.801,40.622,-73.799,40.641,-73.823,40.656,-73.875,40.652,-73.929,40.599,-74.015,40.581,-74.032,40.639,-74.003,40.683,-73.965,40.725,-73.879,40.792,-73.757,40.834,-73.695,40.87,-73.652,40.838,-73.643,40.881,-73.61,40.906,-73.574,40.92,-73.487,40.92,-73.441,40.927,-73.407,40.941,-73.373,40.944,-73.278,40.924,-73.186,40.93,-73.111,40.957,-73.034,40.966,-72.829,40.972,-72.625,40.992,-72.544,41.027,-72.373,41.126,-72.274,41.153,-72.427,41.039,-72.51,40.986]],[[-68.187,44.332,-68.245,44.313,-68.309,44.321,-68.308,44.269,-68.315,44.25,-68.386,44.277,-68.412,44.294,-68.409,44.364,-68.347,44.43,-68.299,44.456,-68.238,44.438,-68.191,44.364,-68.187,44.332]],[[-74.188,40.523,-74.236,40.519,-74.188,40.615,-74.1,40.658,-74.069,40.649,-74.067,40.615,-74.08,40.586,-74.139,40.542,-74.188,40.523]],[[-70.51,41.376,-70.785,41.327,-70.829,41.359,-70.76,41.374,-70.674,41.449,-70.616,41.457,-70.525,41.415,-70.51,41.376]],[[-71.241,41.492,-71.291,41.465,-71.346,41.469,-71.318,41.506,-71.307,41.56,-71.28,41.62,-71.264,41.638,-71.232,41.654,-71.241,41.492]],[[-69.978,41.266,-70.055,41.249,-70.233,41.286,-70.087,41.318,-70.063,41.328,-70.044,41.374,-70.041,41.397,-69.986,41.299,-69.978,41.266]],[[-75.636,35.856,-75.651,35.836,-75.717,35.946,-75.649,35.91,-75.637,35.881,-75.636,35.856]],[[-75.544,35.24,-75.678,35.213,-75.69,35.222,-75.536,35.279,-75.488,35.479,-75.481,35.572,-75.504,35.735,-75.504,35.769,-75.479,35.717,-75.456,35.564,-75.465,35.449,-75.509,35.28,-75.544,35.24]],[[-75.782,35.19,-75.964,35.119,-75.984,35.123,-75.865,35.174,-75.782,35.19]],[[-76.504,34.643,-76.529,34.631,-76.437,34.756,-76.256,34.915,-76.207,34.939,-76.358,34.804,-76.504,34.643]],[[-82.037,26.454,-82.073,26.428,-82.145,26.447,-82.184,26.481,-82.201,26.548,-82.139,26.477,-82.116,26.461,-82.037,26.454]],[[-82.084,26.552,-82.085,26.494,-82.136,26.592,-82.169,26.701,-82.121,26.666,-82.084,26.552]],[[-80.382,25.142,-80.581,24.954,-80.559,25.001,-80.481,25.102,-80.456,25.149,-80.404,25.179,-80.355,25.234,-80.351,25.297,-80.28,25.341,-80.257,25.348,-80.382,25.142]],[[-91.794,29.501,-91.831,29.486,-91.996,29.573,-92.007,29.61,-91.925,29.644,-91.875,29.641,-91.796,29.597,-91.768,29.585,-91.754,29.567,-91.762,29.539,-91.794,29.501]],[[-97.014,27.902,-97.036,27.899,-96.988,27.981,-96.979,28.014,-96.899,28.117,-96.857,28.133,-96.84,28.089,-96.921,28.016,-97.014,27.902]],[[-96.764,28.153,-96.801,28.148,-96.756,28.202,-96.682,28.23,-96.519,28.333,-96.453,28.341,-96.419,28.376,-96.404,28.382,-96.413,28.338,-96.544,28.276,-96.764,28.153]],[[-97.354,27.3,-97.385,27.243,-97.376,27.328,-97.295,27.523,-97.13,27.779,-97.061,27.822,-97.251,27.541,-97.354,27.3]],[[-95.04,29.146,-95.09,29.136,-94.872,29.29,-94.826,29.341,-94.768,29.339,-94.865,29.253,-95.04,29.146]],[[-97.171,26.159,-97.185,26.113,-97.267,26.33,-97.402,26.821,-97.407,27.1,-97.386,27.196,-97.351,26.801,-97.202,26.3,-97.171,26.159]],[[-120.307,34.025,-120.36,34.022,-120.442,34.033,-120.413,34.056,-120.368,34.073,-120.353,34.061,-120.307,34.025]],[[-119.438,33.217,-119.483,33.215,-119.544,33.225,-119.575,33.278,-119.525,33.282,-119.479,33.275,-119.442,33.232,-119.438,33.217]],[[-118.35,32.828,-118.409,32.819,-118.473,32.839,-118.529,32.936,-118.59,33.011,-118.557,33.033,-118.507,32.96,-118.383,32.849,-118.35,32.828]],[[-120.044,33.919,-120.114,33.905,-120.167,33.918,-120.252,34.014,-120.072,34.027,-119.994,33.985,-119.984,33.973,-120.044,33.919]],[[-119.882,34.08,-119.679,34.028,-119.569,34.053,-119.549,34.028,-119.562,34.007,-119.81,33.968,-119.885,33.995,-119.892,34.032,-119.918,34.068,-119.882,34.08]],[[-118.348,33.386,-118.297,33.312,-118.37,33.321,-118.446,33.317,-118.469,33.357,-118.492,33.413,-118.507,33.427,-118.559,33.432,-118.563,33.437,-118.569,33.464,-118.555,33.477,-118.392,33.415,-118.348,33.386]],[[-122.782,48.673,-122.769,48.651,-122.809,48.63,-122.838,48.627,-122.883,48.661,-122.903,48.665,-122.887,48.612,-122.893,48.594,-122.986,48.627,-123.003,48.652,-122.977,48.679,-122.918,48.707,-122.898,48.71,-122.782,48.673]],[[-123.013,48.501,-122.987,48.468,-123.094,48.489,-123.14,48.508,-123.153,48.526,-123.17,48.587,-123.162,48.606,-123.114,48.613,-123.024,48.538,-123.013,48.501]],[[-122.573,48.157,-122.524,48.025,-122.503,48.08,-122.367,47.985,-122.367,47.939,-122.383,47.923,-122.411,47.918,-122.438,47.931,-122.462,47.964,-122.492,47.981,-122.558,47.992,-122.591,48.03,-122.603,48.055,-122.606,48.129,-122.623,48.151,-122.657,48.156,-122.69,48.174,-122.742,48.225,-122.749,48.239,-122.725,48.281,-122.669,48.351,-122.629,48.384,-122.604,48.381,-122.572,48.36,-122.536,48.321,-122.542,48.294,-122.692,48.241,-122.697,48.229,-122.624,48.214,-122.598,48.2,-122.573,48.157]],[[-71.365,41.485,-71.393,41.467,-71.403,41.515,-71.384,41.571,-71.364,41.572,-71.354,41.542,-71.365,41.485]],[[-74.133,39.681,-74.25,39.529,-74.253,39.558,-74.107,39.746,-74.133,39.681]],[[-75.333,37.888,-75.379,37.872,-75.226,38.072,-75.137,38.24,-75.098,38.298,-75.136,38.181,-75.203,38.072,-75.333,37.888]],[[-76.546,34.655,-76.569,34.653,-76.608,34.664,-76.662,34.685,-76.674,34.7,-76.622,34.695,-76.546,34.655]],[[-81.335,24.65,-81.365,24.63,-81.379,24.636,-81.379,24.666,-81.422,24.733,-81.42,24.75,-81.322,24.685,-81.32,24.668,-81.335,24.65]],[[-80.829,24.804,-80.848,24.804,-80.839,24.818,-80.799,24.846,-80.785,24.835,-80.787,24.821,-80.829,24.804]],[[-80.638,24.903,-80.665,24.898,-80.626,24.941,-80.615,24.938,-80.638,24.903]],[[-81.044,24.717,-81.09,24.693,-81.137,24.71,-81.085,24.734,-80.93,24.759,-80.989,24.728,-81.044,24.717]],[[-81.419,30.971,-81.463,30.728,-81.483,30.814,-81.485,30.898,-81.451,30.947,-81.419,30.971]],[[-81.567,24.6,-81.631,24.59,-81.579,24.629,-81.562,24.689,-81.532,24.642,-81.532,24.614,-81.567,24.6]],[[-80.187,27.278,-80.171,27.205,-80.262,27.376,-80.376,27.643,-80.437,27.851,-80.396,27.795,-80.356,27.679,-80.187,27.278]],[[-81.784,24.545,-81.809,24.542,-81.811,24.558,-81.768,24.577,-81.739,24.575,-81.74,24.554,-81.784,24.545]],[[-88.889,29.713,-88.944,29.66,-88.941,29.68,-88.901,29.733,-88.873,29.753,-88.889,29.713]],[[-88.558,30.216,-88.571,30.205,-88.659,30.226,-88.713,30.245,-88.723,30.264,-88.574,30.229,-88.558,30.216]],[[-88.071,30.252,-88.159,30.231,-88.29,30.233,-88.316,30.24,-88.264,30.255,-88.109,30.274,-88.071,30.252]],[[-89.224,30.084,-89.22,30.038,-89.269,30.061,-89.342,30.063,-89.31,30.079,-89.288,30.094,-89.276,30.111,-89.185,30.169,-89.211,30.126,-89.224,30.084]],[[-88.827,29.808,-88.856,29.776,-88.828,29.928,-88.867,30.057,-88.826,30,-88.813,29.933,-88.827,29.808]],[[-84.908,29.643,-85.008,29.607,-85.117,29.633,-85.049,29.638,-85.001,29.627,-84.877,29.679,-84.812,29.718,-84.737,29.732,-84.908,29.643]],[[-122.853,47.205,-122.863,47.185,-122.877,47.186,-122.908,47.226,-122.912,47.254,-122.885,47.275,-122.849,47.216,-122.853,47.205]],[[-122.394,47.395,-122.399,47.373,-122.437,47.355,-122.457,47.359,-122.458,47.386,-122.469,47.39,-122.51,47.358,-122.507,47.422,-122.486,47.489,-122.469,47.49,-122.442,47.446,-122.394,47.395]],[[-122.497,47.595,-122.503,47.575,-122.558,47.598,-122.576,47.619,-122.574,47.667,-122.56,47.698,-122.55,47.704,-122.517,47.691,-122.508,47.683,-122.497,47.595]],[[-122.821,48.431,-122.837,48.422,-122.89,48.435,-122.922,48.457,-122.932,48.485,-122.912,48.538,-122.885,48.552,-122.869,48.549,-122.862,48.502,-122.815,48.452,-122.821,48.431]],[[-68.623,44.196,-68.661,44.176,-68.702,44.183,-68.703,44.232,-68.691,44.249,-68.677,44.256,-68.656,44.242,-68.623,44.196]]]},{"n":"South Georgia and the South Sandwich Islands","c":"GS","r":4,"ml":5,"lx":-31.06,"ly":-55.68,"b":[-38.017,-58.492,-26.26,-53.984],"p":[[[-26.264,-58.435,-26.26,-58.492,-26.415,-58.44,-26.451,-58.415,-26.401,-58.383,-26.303,-58.382,-26.279,-58.402,-26.264,-58.435]],[[-37.103,-54.066,-37.006,-54.114,-36.929,-54.081,-36.849,-54.085,-36.805,-54.101,-36.76,-54.108,-36.704,-54.108,-36.607,-54.19,-36.647,-54.262,-36.541,-54.248,-36.449,-54.308,-36.407,-54.303,-36.386,-54.279,-36.326,-54.251,-36.285,-54.289,-36.236,-54.36,-36.173,-54.382,-36.117,-54.458,-36.073,-54.554,-36.033,-54.568,-35.965,-54.568,-35.895,-54.555,-35.922,-54.637,-35.913,-54.711,-35.799,-54.763,-35.867,-54.792,-35.939,-54.834,-36.085,-54.867,-36.124,-54.853,-36.252,-54.78,-36.311,-54.694,-36.446,-54.571,-36.472,-54.534,-36.507,-54.511,-36.628,-54.496,-36.735,-54.467,-36.824,-54.404,-36.852,-54.366,-36.886,-54.339,-37.007,-54.341,-37.083,-54.312,-37.158,-54.271,-37.498,-54.156,-37.631,-54.167,-37.692,-54.135,-37.689,-54.077,-37.619,-54.042,-37.913,-54.029,-38.017,-54.008,-37.946,-53.996,-37.536,-53.994,-37.382,-53.984,-37.369,-54.009,-37.233,-54.061,-37.103,-54.066]]]},{"n":"British Indian Ocean Territory","c":"IO","r":5,"ml":5,"lx":71.35,"ly":-6.19,"b":[72.35,-7.435,72.499,-7.22],"p":[[[72.492,-7.377,72.469,-7.417,72.429,-7.435,72.408,-7.334,72.35,-7.263,72.373,-7.263,72.427,-7.3,72.447,-7.396,72.467,-7.368,72.462,-7.338,72.474,-7.31,72.465,-7.278,72.436,-7.23,72.446,-7.22,72.494,-7.262,72.499,-7.295,72.492,-7.377]]]},{"n":"Saint Helena","c":"SH","r":6,"ml":5,"lx":-5.71,"ly":-15.95,"b":[-14.415,-16.004,-5.66,-7.883],"p":[[[-5.692,-15.998,-5.783,-16.004,-5.775,-15.957,-5.708,-15.906,-5.662,-15.913,-5.66,-15.971,-5.692,-15.998]],[[-14.364,-7.974,-14.399,-7.976,-14.409,-7.967,-14.415,-7.944,-14.399,-7.906,-14.384,-7.883,-14.36,-7.886,-14.329,-7.913,-14.303,-7.935,-14.317,-7.956,-14.364,-7.974]]]},{"n":"Pitcairn Islands","c":"PN","r":4,"ml":5,"lx":-128.32,"ly":-24.36,"b":[-128.35,-24.413,-128.29,-24.323],"p":[[[-128.29,-24.397,-128.3,-24.413,-128.321,-24.4,-128.342,-24.371,-128.35,-24.34,-128.33,-24.323,-128.304,-24.334,-128.291,-24.365,-128.29,-24.397]]]},{"n":"Anguilla","c":"AI","r":6,"ml":5,"lx":-63.03,"ly":18.24,"b":[-63.16,18.171,-62.98,18.27],"p":[[[-63.001,18.222,-63.16,18.171,-63.153,18.2,-63.026,18.27,-62.98,18.265,-63.001,18.222]]]},{"n":"Falkland Islands","c":"FK","r":5,"ml":4.5,"lx":-58.74,"ly":-51.61,"b":[-61.145,-52.308,-57.792,-51.27],"p":[[[-58.85,-51.27,-58.698,-51.329,-58.506,-51.308,-58.426,-51.324,-58.379,-51.373,-58.407,-51.418,-58.467,-51.412,-58.519,-51.424,-58.509,-51.484,-58.474,-51.509,-58.272,-51.575,-58.235,-51.579,-58.241,-51.551,-58.276,-51.506,-58.289,-51.458,-58.259,-51.417,-58.206,-51.405,-57.977,-51.384,-57.923,-51.404,-57.808,-51.518,-57.915,-51.534,-57.96,-51.583,-57.866,-51.605,-57.792,-51.636,-57.831,-51.685,-57.838,-51.709,-58.004,-51.743,-58.151,-51.765,-58.218,-51.822,-58.336,-51.864,-58.683,-51.936,-58.643,-51.995,-58.638,-52.023,-58.653,-52.099,-59.131,-52.008,-59.196,-52.018,-59.068,-52.173,-59.163,-52.202,-59.256,-52.183,-59.342,-52.196,-59.396,-52.308,-59.532,-52.236,-59.649,-52.134,-59.649,-52.077,-59.537,-51.971,-59.571,-51.925,-59.309,-51.78,-59.262,-51.737,-59.18,-51.712,-59.095,-51.704,-59.06,-51.685,-59.065,-51.65,-59.099,-51.59,-59.097,-51.491,-58.887,-51.358,-58.917,-51.272,-58.85,-51.27]],[[-60.286,-51.462,-60.142,-51.481,-60.009,-51.411,-59.917,-51.388,-59.842,-51.403,-59.788,-51.446,-59.711,-51.439,-59.493,-51.396,-59.465,-51.411,-59.388,-51.36,-59.321,-51.384,-59.268,-51.428,-59.294,-51.479,-59.354,-51.511,-59.392,-51.556,-59.437,-51.593,-59.514,-51.627,-59.573,-51.681,-59.715,-51.808,-59.921,-51.97,-59.99,-51.984,-60.132,-51.994,-60.194,-51.983,-60.246,-51.986,-60.288,-52.074,-60.353,-52.14,-60.384,-52.154,-60.452,-52.16,-60.484,-52.17,-60.508,-52.195,-60.686,-52.188,-60.812,-52.148,-60.961,-52.057,-60.762,-51.946,-60.591,-51.952,-60.45,-51.877,-60.334,-51.84,-60.289,-51.801,-60.238,-51.772,-60.238,-51.734,-60.277,-51.717,-60.328,-51.718,-60.38,-51.735,-60.5,-51.757,-60.583,-51.713,-60.528,-51.696,-60.467,-51.697,-60.281,-51.656,-60.245,-51.639,-60.303,-51.58,-60.415,-51.545,-60.506,-51.485,-60.523,-51.463,-60.518,-51.428,-60.568,-51.358,-60.516,-51.354,-60.445,-51.399,-60.286,-51.462]],[[-61.019,-51.786,-60.947,-51.8,-60.876,-51.794,-60.916,-51.897,-60.948,-51.946,-61.032,-51.942,-61.116,-51.875,-61.145,-51.839,-61.052,-51.814,-61.019,-51.786]],[[-58.439,-52.011,-58.433,-52.099,-58.513,-52.071,-58.541,-52.028,-58.497,-51.999,-58.461,-52.002,-58.439,-52.011]],[[-60.112,-51.396,-60.249,-51.396,-60.276,-51.363,-60.275,-51.281,-60.171,-51.273,-60.07,-51.308,-60.076,-51.343,-60.112,-51.396]],[[-59.683,-52.232,-59.747,-52.251,-59.764,-52.242,-59.785,-52.205,-59.786,-52.156,-59.793,-52.134,-59.753,-52.141,-59.681,-52.18,-59.683,-52.232]]]},{"n":"Cayman Islands","c":"KY","r":5,"ml":5,"lx":-81.24,"ly":19.32,"b":[-81.419,19.272,-79.742,19.766],"p":[[[-81.37,19.349,-81.337,19.329,-81.296,19.341,-81.285,19.363,-81.13,19.347,-81.107,19.305,-81.225,19.304,-81.277,19.277,-81.304,19.272,-81.405,19.278,-81.419,19.375,-81.391,19.385,-81.37,19.349]],[[-79.979,19.708,-79.989,19.703,-80.021,19.707,-80.094,19.666,-80.126,19.668,-80.116,19.683,-80.101,19.696,-80.084,19.706,-80.068,19.71,-80.016,19.718,-79.992,19.719,-79.975,19.71,-79.979,19.708]],[[-79.823,19.712,-79.87,19.697,-79.906,19.703,-79.824,19.744,-79.803,19.758,-79.785,19.766,-79.766,19.766,-79.742,19.757,-79.742,19.751,-79.823,19.712]]]},{"n":"Bermuda","c":"BM","r":6,"ml":4,"lx":-64.76,"ly":32.3,"b":[-64.863,32.26,-64.668,32.387],"p":[[[-64.73,32.293,-64.82,32.26,-64.845,32.262,-64.863,32.274,-64.771,32.308,-64.695,32.387,-64.668,32.382,-64.73,32.293]]]},{"n":"British Virgin Islands","c":"VG","r":6,"ml":5,"lx":-64.64,"ly":18.43,"b":[-64.695,18.399,-64.274,18.753],"p":[[[-64.395,18.465,-64.421,18.457,-64.438,18.459,-64.444,18.473,-64.426,18.513,-64.325,18.517,-64.395,18.465]],[[-64.594,18.403,-64.672,18.399,-64.695,18.412,-64.651,18.443,-64.569,18.446,-64.545,18.438,-64.594,18.403]],[[-64.288,18.741,-64.274,18.707,-64.282,18.708,-64.339,18.731,-64.383,18.733,-64.401,18.739,-64.411,18.751,-64.323,18.753,-64.288,18.741]]]},{"n":"Turks and Caicos Islands","c":"TC","r":6,"ml":5,"lx":-71.75,"ly":21.82,"b":[-72.342,21.752,-71.637,21.952],"p":[[[-71.661,21.765,-71.665,21.752,-71.722,21.79,-71.83,21.791,-71.848,21.843,-71.806,21.852,-71.668,21.833,-71.637,21.788,-71.661,21.765]],[[-71.88,21.84,-71.897,21.83,-71.955,21.864,-71.964,21.892,-71.985,21.893,-72.019,21.918,-72.011,21.95,-71.932,21.952,-71.9,21.863,-71.88,21.84]],[[-72.333,21.851,-72.219,21.796,-72.15,21.804,-72.144,21.793,-72.182,21.78,-72.191,21.77,-72.301,21.755,-72.335,21.758,-72.342,21.795,-72.333,21.851]]]},{"n":"Montserrat","c":"MS","r":6,"ml":5,"lx":-62.19,"ly":16.74,"b":[-62.223,16.681,-62.148,16.81],"p":[[[-62.148,16.74,-62.154,16.681,-62.222,16.7,-62.223,16.752,-62.191,16.804,-62.176,16.81,-62.148,16.74]]]},{"n":"Jersey","c":"JE","r":6,"ml":5,"lx":-2.09,"ly":49.22,"b":[-2.236,49.17,-2.01,49.266],"p":[[[-2.019,49.231,-2.01,49.181,-2.054,49.17,-2.091,49.187,-2.166,49.187,-2.236,49.176,-2.221,49.266,-2.082,49.255,-2.019,49.231]]]},{"n":"Guernsey","c":"GG","r":6,"ml":5,"lx":-2.56,"ly":49.46,"b":[-2.646,49.429,-2.512,49.507],"p":[[[-2.512,49.495,-2.547,49.429,-2.639,49.451,-2.646,49.468,-2.542,49.507,-2.521,49.506,-2.512,49.495]]]},{"n":"Isle of Man","c":"IM","r":6,"ml":5,"lx":-4.53,"ly":54.22,"b":[-4.785,54.059,-4.338,54.407],"p":[[[-4.412,54.185,-4.614,54.059,-4.696,54.081,-4.766,54.069,-4.785,54.073,-4.746,54.119,-4.699,54.225,-4.615,54.267,-4.509,54.377,-4.425,54.407,-4.396,54.403,-4.377,54.393,-4.338,54.269,-4.392,54.225,-4.412,54.185]]]},{"n":"United Kingdom","c":"GB","r":2,"ml":1.7,"lx":-2.12,"ly":54.4,"b":[-8.145,50.021,1.747,60.832],"p":[[[-2.668,51.623,-2.742,51.581,-2.979,51.539,-3.08,51.496,-3.259,51.398,-3.293,51.39,-3.562,51.414,-3.763,51.54,-3.891,51.592,-3.944,51.598,-3.998,51.582,-4.115,51.566,-4.235,51.569,-4.174,51.627,-4.091,51.66,-4.276,51.683,-4.328,51.7,-4.386,51.741,-4.531,51.748,-4.601,51.738,-4.718,51.684,-4.902,51.626,-5.125,51.706,-5.168,51.741,-5.167,51.808,-5.201,51.861,-5.262,51.88,-5.183,51.95,-5.088,51.996,-4.879,52.042,-4.561,52.151,-4.383,52.197,-4.218,52.277,-4.149,52.326,-4.1,52.393,-4.051,52.475,-3.98,52.542,-4.048,52.558,-4.079,52.608,-4.071,52.659,-4.039,52.704,-4.067,52.761,-4.118,52.82,-4.115,52.866,-4.101,52.915,-4.229,52.913,-4.356,52.897,-4.472,52.862,-4.584,52.815,-4.683,52.806,-4.681,52.844,-4.638,52.891,-4.526,52.958,-4.405,53.014,-4.362,53.056,-4.328,53.105,-4.269,53.145,-4.111,53.219,-3.809,53.303,-3.764,53.308,-3.646,53.298,-3.53,53.311,-3.428,53.341,-3.326,53.347,-3.098,53.26,-3.166,53.395,-3.065,53.427,-2.919,53.305,-2.864,53.293,-2.75,53.31,-2.794,53.331,-2.845,53.332,-2.913,53.35,-2.97,53.389,-3.065,53.513,-3.059,53.586,-2.996,53.663,-2.925,53.733,-2.984,53.747,-3.032,53.774,-3.045,53.844,-3.027,53.906,-2.9,53.961,-2.862,54.044,-2.846,54.135,-2.868,54.177,-2.994,54.171,-3.055,54.153,-3.11,54.126,-3.166,54.128,-3.322,54.229,-3.41,54.306,-3.569,54.468,-3.592,54.564,-3.465,54.773,-3.268,54.907,-3.036,54.953,-3.081,54.962,-3.434,54.964,-3.55,54.947,-3.658,54.893,-3.719,54.876,-3.783,54.87,-3.842,54.843,-3.899,54.805,-3.958,54.781,-4.076,54.787,-4.133,54.779,-4.174,54.801,-4.208,54.837,-4.253,54.847,-4.304,54.836,-4.41,54.787,-4.517,54.758,-4.648,54.789,-4.818,54.846,-4.852,54.825,-4.89,54.772,-4.911,54.689,-5.032,54.761,-5.135,54.858,-5.17,54.918,-5.173,54.986,-5.117,55.012,-5.056,54.988,-4.965,55.149,-4.785,55.359,-4.721,55.421,-4.677,55.501,-4.684,55.554,-4.724,55.598,-4.892,55.699,-4.89,55.781,-4.872,55.874,-4.826,55.93,-4.807,55.94,-4.584,55.939,-4.671,55.967,-4.844,56.051,-4.841,56.081,-4.8,56.158,-4.819,56.15,-4.856,56.115,-4.927,56.028,-4.97,56.008,-5.093,55.987,-5.115,55.945,-5.135,55.933,-5.196,55.929,-5.215,55.889,-5.228,55.886,-5.246,55.929,-5.247,56,-5.223,56.066,-5.176,56.117,-4.997,56.233,-5.084,56.197,-5.282,56.09,-5.383,56.019,-5.41,55.995,-5.419,55.975,-5.418,55.952,-5.373,55.828,-5.386,55.77,-5.556,55.39,-5.589,55.351,-5.618,55.331,-5.647,55.327,-5.731,55.334,-5.768,55.363,-5.768,55.395,-5.752,55.443,-5.681,55.624,-5.651,55.674,-5.605,55.721,-5.504,55.802,-5.507,55.808,-5.574,55.792,-5.602,55.797,-5.623,55.813,-5.61,56.055,-5.555,56.135,-5.535,56.251,-5.488,56.35,-5.433,56.422,-5.392,56.515,-5.329,56.556,-5.313,56.619,-5.243,56.687,-5.188,56.758,-5.218,56.751,-5.564,56.566,-5.652,56.532,-5.773,56.541,-5.865,56.562,-5.937,56.606,-5.969,56.69,-6.058,56.692,-6.134,56.707,-6.133,56.718,-6.035,56.764,-5.878,56.78,-5.731,56.853,-5.861,56.903,-5.85,56.918,-5.736,56.961,-5.591,57.102,-5.562,57.233,-5.631,57.294,-5.656,57.334,-5.795,57.379,-5.818,57.436,-5.802,57.468,-5.757,57.499,-5.689,57.524,-5.582,57.547,-5.679,57.572,-5.715,57.601,-5.742,57.644,-5.745,57.668,-5.695,57.778,-5.665,57.824,-5.608,57.881,-5.349,57.878,-5.319,57.904,-5.29,57.905,-5.157,57.881,-5.177,57.906,-5.394,58.044,-5.413,58.07,-5.351,58.144,-5.347,58.177,-5.356,58.212,-5.338,58.239,-5.27,58.251,-5.06,58.25,-5.008,58.263,-5.032,58.298,-5.081,58.345,-5.09,58.385,-5.079,58.419,-5.076,58.489,-5.067,58.52,-5.017,58.567,-4.976,58.58,-4.925,58.588,-4.81,58.573,-4.766,58.554,-4.715,58.51,-4.678,58.514,-4.535,58.562,-4.492,58.568,-4.433,58.513,-4.189,58.557,-3.86,58.577,-3.662,58.606,-3.454,58.617,-3.259,58.65,-3.053,58.635,-3.046,58.616,-3.057,58.589,-3.11,58.515,-3.101,58.434,-3.113,58.409,-3.137,58.378,-3.212,58.321,-3.411,58.24,-3.775,58.052,-3.99,57.959,-4.02,57.914,-4.036,57.852,-3.907,57.84,-3.857,57.819,-3.888,57.787,-4.078,57.677,-4.135,57.578,-3.988,57.581,-3.868,57.6,-3.628,57.662,-3.403,57.708,-3.295,57.71,-3.084,57.673,-3.036,57.672,-2.947,57.689,-2.856,57.692,-2.244,57.681,-2.074,57.702,-1.962,57.677,-1.867,57.612,-1.778,57.494,-1.781,57.474,-1.835,57.42,-1.934,57.352,-2.02,57.259,-2.046,57.209,-2.062,57.153,-2.09,57.103,-2.26,56.863,-2.427,56.731,-2.501,56.637,-2.593,56.562,-2.681,56.514,-2.775,56.483,-3.047,56.449,-3.124,56.425,-3.214,56.384,-3.31,56.363,-3.198,56.366,-3.087,56.389,-2.885,56.398,-2.653,56.318,-2.674,56.253,-2.768,56.202,-2.98,56.194,-3.178,56.08,-3.268,56.045,-3.362,56.028,-3.48,56.033,-3.695,56.063,-3.789,56.095,-3.704,56.043,-3.608,56.016,-3.049,55.952,-3.015,55.959,-2.837,56.026,-2.599,56.027,-2.147,55.903,-2.017,55.808,-1.83,55.672,-1.729,55.619,-1.655,55.57,-1.61,55.498,-1.523,55.26,-1.423,55.026,-1.292,54.774,-1.232,54.704,-1.154,54.654,-0.759,54.541,-0.671,54.504,-0.518,54.395,-0.37,54.279,-0.233,54.19,-0.084,54.118,-0.156,54.081,-0.206,54.022,-0.169,53.942,-0.108,53.865,0.011,53.743,0.115,53.609,0.077,53.629,0.036,53.641,-0.019,53.637,-0.074,53.644,-0.174,53.685,-0.27,53.737,-0.461,53.716,-0.568,53.725,-0.66,53.724,-0.485,53.694,-0.294,53.692,0.128,53.468,0.271,53.335,0.356,53.16,0.298,53.081,0.208,53.03,0.124,52.972,0.046,52.906,0.28,52.809,0.33,52.812,0.382,52.825,0.432,52.858,0.516,52.938,0.559,52.967,0.704,52.977,0.827,52.971,0.949,52.953,1.056,52.959,1.271,52.925,1.382,52.894,1.657,52.754,1.716,52.677,1.743,52.579,1.747,52.469,1.7,52.369,1.647,52.279,1.615,52.162,1.591,52.12,1.559,52.087,1.413,51.995,1.317,51.957,1.276,51.974,1.232,51.971,1.228,51.949,1.274,51.902,1.274,51.845,1.188,51.803,1.101,51.785,0.955,51.808,0.752,51.73,0.898,51.689,0.927,51.647,0.891,51.571,0.799,51.538,0.698,51.523,0.593,51.519,0.507,51.501,0.425,51.466,0.528,51.484,0.6,51.468,0.646,51.405,0.687,51.387,0.889,51.36,1.015,51.36,1.257,51.375,1.373,51.375,1.415,51.363,1.416,51.311,1.398,51.182,1.366,51.155,1.044,51.047,0.979,50.972,0.96,50.926,0.772,50.934,0.684,50.886,0.532,50.853,0.415,50.819,0.3,50.776,0.205,50.763,-0.204,50.814,-0.451,50.81,-0.785,50.765,-0.871,50.773,-1.001,50.816,-1.133,50.845,-1.285,50.857,-1.416,50.897,-1.334,50.821,-1.517,50.747,-1.601,50.733,-1.688,50.735,-1.866,50.715,-2.031,50.725,-2.006,50.673,-1.962,50.628,-1.998,50.608,-2.036,50.603,-2.35,50.637,-2.395,50.631,-2.433,50.599,-2.548,50.616,-2.659,50.67,-2.777,50.706,-2.901,50.722,-2.999,50.717,-3.405,50.632,-3.485,50.548,-3.526,50.428,-3.584,50.322,-3.68,50.24,-3.793,50.229,-3.9,50.286,-4.103,50.349,-4.173,50.391,-4.195,50.393,-4.217,50.378,-4.297,50.359,-4.379,50.358,-4.507,50.341,-4.728,50.29,-4.817,50.256,-5.01,50.161,-5.049,50.134,-5.119,50.038,-5.225,50.021,-5.323,50.083,-5.434,50.104,-5.551,50.083,-5.622,50.051,-5.655,50.077,-5.656,50.132,-5.571,50.197,-5.342,50.246,-5.142,50.374,-5.043,50.452,-5.004,50.495,-4.956,50.523,-4.894,50.534,-4.861,50.582,-4.583,50.776,-4.56,50.821,-4.546,50.901,-4.523,50.977,-4.296,51.027,-4.188,51.189,-4.158,51.201,-3.842,51.231,-3.608,51.229,-3.375,51.197,-3.256,51.194,-3.136,51.205,-3.042,51.249,-2.881,51.406,-2.791,51.475,-2.687,51.537,-2.59,51.609,-2.433,51.741,-2.539,51.695,-2.668,51.623]],[[-4.197,53.321,-4.155,53.303,-4.049,53.306,-4.084,53.264,-4.2,53.218,-4.279,53.172,-4.373,53.134,-4.419,53.178,-4.472,53.176,-4.553,53.26,-4.568,53.386,-4.462,53.419,-4.315,53.417,-4.197,53.321]],[[-2.549,59.231,-2.662,59.23,-2.604,59.289,-2.536,59.304,-2.407,59.298,-2.43,59.271,-2.549,59.231]],[[-1.043,60.514,-1.068,60.502,-1.166,60.604,-1.093,60.72,-1.006,60.717,-0.992,60.686,-1,60.658,-1.045,60.656,-1.049,60.647,-1.035,60.593,-1.034,60.53,-1.043,60.514]],[[-1.308,60.538,-1.287,60.467,-1.236,60.485,-1.158,60.418,-1.118,60.418,-1.052,60.444,-1.066,60.382,-1.134,60.207,-1.153,60.177,-1.166,60.124,-1.179,60.114,-1.199,60.007,-1.245,59.971,-1.284,59.887,-1.299,59.879,-1.356,59.911,-1.3,60.04,-1.276,60.115,-1.291,60.153,-1.323,60.188,-1.409,60.19,-1.481,60.173,-1.497,60.194,-1.499,60.222,-1.517,60.231,-1.613,60.229,-1.641,60.237,-1.66,60.262,-1.664,60.283,-1.577,60.298,-1.494,60.292,-1.375,60.333,-1.45,60.469,-1.549,60.481,-1.572,60.494,-1.553,60.517,-1.498,60.53,-1.414,60.599,-1.364,60.61,-1.302,60.608,-1.308,60.538]],[[-0.774,60.812,-0.774,60.8,-0.826,60.716,-0.825,60.684,-0.909,60.687,-0.922,60.697,-0.938,60.746,-0.928,60.797,-0.916,60.81,-0.891,60.816,-0.865,60.806,-0.823,60.832,-0.802,60.831,-0.774,60.812]],[[-3.165,58.794,-3.222,58.781,-3.279,58.782,-3.367,58.84,-3.401,58.882,-3.395,58.91,-3.357,58.919,-3.272,58.905,-3.228,58.857,-3.222,58.826,-3.212,58.814,-3.159,58.801,-3.165,58.794]],[[-2.929,58.742,-2.939,58.739,-2.975,58.757,-3.035,58.823,-2.941,58.836,-2.896,58.828,-2.913,58.8,-2.929,58.742]],[[-3.057,59.03,-3.071,59.005,-2.995,59.006,-2.885,58.985,-2.818,58.982,-2.762,58.956,-2.793,58.907,-2.826,58.893,-2.864,58.891,-2.995,58.939,-3.167,58.919,-3.201,58.925,-3.223,58.939,-3.233,58.956,-3.233,58.99,-3.242,59,-3.304,58.967,-3.332,58.971,-3.347,58.987,-3.354,59.019,-3.347,59.065,-3.31,59.131,-3.249,59.144,-3.156,59.136,-3.051,59.099,-3.019,59.076,-3.02,59.058,-3.057,59.03]],[[-2.729,59.187,-2.815,59.162,-2.852,59.182,-2.861,59.247,-2.964,59.274,-3.013,59.291,-3.052,59.324,-3.042,59.334,-2.976,59.347,-2.862,59.288,-2.815,59.241,-2.731,59.227,-2.72,59.219,-2.729,59.187]],[[-6.608,56.585,-6.664,56.579,-6.669,56.594,-6.57,56.661,-6.506,56.672,-6.484,56.666,-6.53,56.627,-6.608,56.585]],[[-5.105,55.449,-5.231,55.448,-5.277,55.457,-5.331,55.481,-5.393,55.618,-5.371,55.667,-5.346,55.691,-5.318,55.709,-5.252,55.717,-5.185,55.691,-5.16,55.667,-5.105,55.574,-5.095,55.494,-5.105,55.449]],[[-5.778,56.344,-6.176,56.289,-6.313,56.294,-6.326,56.321,-6.298,56.339,-6.185,56.357,-6.139,56.491,-6.311,56.552,-6.32,56.569,-6.306,56.599,-6.286,56.612,-6.182,56.643,-6.138,56.65,-6.103,56.646,-6.03,56.61,-5.947,56.535,-5.836,56.523,-5.761,56.491,-5.778,56.344]],[[-6.129,55.931,-6.093,55.802,-6.058,55.723,-6.055,55.695,-6.088,55.658,-6.253,55.607,-6.305,55.607,-6.307,55.619,-6.27,55.67,-6.302,55.728,-6.286,55.773,-6.302,55.781,-6.334,55.774,-6.452,55.704,-6.491,55.697,-6.496,55.712,-6.466,55.769,-6.463,55.808,-6.445,55.832,-6.413,55.855,-6.375,55.871,-6.344,55.874,-6.311,55.856,-6.216,55.905,-6.129,55.931]],[[-5.97,55.815,-5.991,55.804,-6.042,55.807,-6.06,55.823,-6.071,55.848,-6.072,55.893,-6.041,55.926,-5.912,55.975,-5.97,55.992,-5.973,56.004,-5.939,56.045,-5.8,56.109,-5.762,56.12,-5.725,56.119,-5.797,56.006,-5.97,55.815]],[[-6.199,58.363,-6.326,58.189,-6.376,58.185,-6.419,58.141,-6.555,58.093,-6.437,58.092,-6.403,58.076,-6.402,58.041,-6.425,58.021,-6.578,57.941,-6.683,57.911,-6.797,57.828,-6.854,57.827,-6.91,57.773,-6.957,57.75,-6.983,57.75,-7.013,57.762,-7.083,57.814,-6.956,57.865,-6.944,57.894,-6.857,57.924,-6.864,57.933,-7.003,57.975,-7.057,58.003,-7.052,58.018,-6.985,58.05,-7.017,58.055,-7.038,58.072,-7.077,58.079,-7.088,58.095,-7.096,58.138,-7.085,58.182,-7.045,58.202,-7.028,58.222,-7.012,58.229,-6.95,58.218,-6.886,58.183,-6.812,58.196,-6.726,58.189,-6.725,58.198,-6.788,58.284,-6.776,58.302,-6.742,58.322,-6.544,58.383,-6.297,58.487,-6.237,58.503,-6.219,58.489,-6.194,58.435,-6.199,58.363]],[[-6.279,56.965,-6.309,56.952,-6.346,56.954,-6.383,56.971,-6.433,57.018,-6.322,57.051,-6.278,57.031,-6.261,57.01,-6.261,56.985,-6.279,56.965]],[[-6.145,57.505,-6.146,57.461,-6.164,57.409,-6.141,57.354,-6.136,57.314,-6.093,57.302,-6.068,57.284,-5.88,57.263,-5.706,57.269,-5.672,57.253,-5.669,57.227,-5.696,57.198,-5.795,57.147,-5.914,57.063,-5.949,57.045,-5.987,57.044,-6.015,57.052,-6.034,57.201,-6.163,57.182,-6.266,57.184,-6.323,57.202,-6.362,57.238,-6.442,57.327,-6.675,57.363,-6.741,57.412,-6.761,57.442,-6.753,57.459,-6.704,57.496,-6.643,57.483,-6.606,57.491,-6.583,57.507,-6.583,57.521,-6.615,57.553,-6.617,57.563,-6.379,57.603,-6.358,57.667,-6.306,57.672,-6.247,57.651,-6.166,57.585,-6.145,57.505]],[[-7.206,57.683,-7.093,57.627,-7.183,57.533,-7.321,57.534,-7.515,57.602,-7.516,57.616,-7.499,57.636,-7.47,57.653,-7.44,57.656,-7.392,57.645,-7.325,57.663,-7.271,57.657,-7.206,57.683]],[[-7.25,57.115,-7.292,57.11,-7.347,57.115,-7.381,57.131,-7.416,57.192,-7.422,57.229,-7.407,57.298,-7.411,57.381,-7.296,57.384,-7.267,57.372,-7.248,57.126,-7.25,57.115]],[[-7.417,56.965,-7.505,56.952,-7.537,56.96,-7.543,56.972,-7.523,57.007,-7.455,57.019,-7.407,57,-7.399,56.983,-7.417,56.965]],[[-6.218,54.089,-6.304,54.095,-6.364,54.077,-6.403,54.061,-6.44,54.064,-6.548,54.057,-6.65,54.059,-6.664,54.085,-6.647,54.163,-6.67,54.185,-6.767,54.196,-6.803,54.214,-6.858,54.269,-6.869,54.294,-6.877,54.329,-6.936,54.374,-7.008,54.407,-7.05,54.408,-7.133,54.355,-7.203,54.302,-7.178,54.275,-7.155,54.24,-7.193,54.214,-7.307,54.156,-7.325,54.133,-7.355,54.121,-7.409,54.137,-7.544,54.134,-7.607,54.144,-7.679,54.187,-7.855,54.215,-7.884,54.284,-7.918,54.297,-8.118,54.414,-8.145,54.454,-8.119,54.477,-8.044,54.512,-7.794,54.571,-7.754,54.595,-7.746,54.616,-7.82,54.64,-7.886,54.666,-7.909,54.683,-7.911,54.698,-7.873,54.718,-7.797,54.719,-7.737,54.71,-7.69,54.728,-7.606,54.746,-7.55,54.768,-7.502,54.825,-7.451,54.877,-7.446,54.905,-7.401,55.003,-7.377,55.028,-7.219,55.092,-7.179,55.057,-7.101,55.048,-7.031,55.081,-6.947,55.183,-6.889,55.189,-6.825,55.181,-6.699,55.193,-6.475,55.241,-6.375,55.242,-6.234,55.217,-6.129,55.217,-6.036,55.145,-5.986,55.03,-5.869,54.916,-5.717,54.817,-5.711,54.757,-5.765,54.725,-5.879,54.684,-5.879,54.641,-5.803,54.663,-5.739,54.673,-5.583,54.663,-5.528,54.62,-5.49,54.554,-5.47,54.5,-5.484,54.442,-5.526,54.46,-5.569,54.513,-5.616,54.537,-5.671,54.55,-5.646,54.478,-5.656,54.382,-5.632,54.373,-5.558,54.371,-5.607,54.273,-5.708,54.246,-5.826,54.236,-5.855,54.201,-5.876,54.156,-5.938,54.089,-6.019,54.051,-6.12,54.059,-6.218,54.089]],[[-1.066,50.69,-1.149,50.656,-1.176,50.615,-1.196,50.599,-1.251,50.589,-1.306,50.589,-1.515,50.67,-1.563,50.666,-1.516,50.703,-1.386,50.734,-1.313,50.773,-1.144,50.735,-1.066,50.69]]]},{"n":"United Arab Emirates","c":"AE","r":4,"ml":4,"lx":54.55,"ly":23.47,"b":[51.568,22.621,56.388,26.068],"p":[[[56.298,25.651,56.363,25.569,56.373,25.018,56.388,24.979,56.353,24.973,56.314,24.931,56.268,24.867,56.205,24.833,56.154,24.796,56.107,24.749,56.064,24.739,56.008,24.798,55.97,24.859,55.98,24.872,56.006,24.876,56.017,24.908,56.001,24.953,55.963,24.97,55.916,24.972,55.871,24.951,55.823,24.911,55.796,24.868,55.792,24.781,55.804,24.684,55.804,24.636,55.778,24.577,55.768,24.491,55.787,24.424,55.804,24.384,55.806,24.35,55.761,24.243,55.8,24.223,55.929,24.215,55.966,24.143,55.992,24.093,55.985,24.063,55.894,24.041,55.779,24.017,55.697,24.024,55.548,23.991,55.468,23.941,55.492,23.91,55.519,23.885,55.532,23.819,55.508,23.725,55.466,23.633,55.414,23.519,55.353,23.387,55.27,23.19,55.2,23.035,55.192,22.923,55.194,22.85,55.186,22.704,55.119,22.624,55.104,22.621,55.025,22.631,54.998,22.634,54.922,22.644,54.805,22.658,54.652,22.677,54.472,22.699,54.27,22.723,54.055,22.75,53.832,22.777,53.61,22.804,53.394,22.83,53.192,22.855,53.012,22.877,52.859,22.896,52.742,22.91,52.666,22.919,52.639,22.923,52.555,22.933,52.51,22.987,52.455,23.052,52.4,23.118,52.345,23.183,52.29,23.249,52.235,23.314,52.179,23.38,52.125,23.445,52.069,23.511,52.014,23.576,51.959,23.642,51.904,23.708,51.849,23.773,51.794,23.838,51.739,23.904,51.684,23.97,51.629,24.035,51.593,24.079,51.572,24.128,51.568,24.258,51.568,24.286,51.605,24.338,51.623,24.301,51.665,24.25,51.735,24.263,51.768,24.254,51.792,24.075,51.843,24.011,51.906,23.985,52.119,23.971,52.251,23.995,52.511,24.113,52.648,24.155,53.026,24.147,53.33,24.098,53.802,24.069,53.893,24.077,54.148,24.171,54.304,24.254,54.397,24.278,54.458,24.358,54.499,24.463,54.535,24.531,54.58,24.564,54.624,24.621,54.659,24.716,54.747,24.81,55.098,25.042,55.304,25.237,55.322,25.3,55.433,25.394,55.523,25.498,55.941,25.794,56.025,25.916,56.075,26.053,56.08,26.063,56.117,26.068,56.167,26.047,56.173,25.945,56.154,25.848,56.152,25.746,56.145,25.691,56.184,25.645,56.25,25.625,56.279,25.628,56.298,25.651],[56.282,25.236,56.288,25.279,56.277,25.301,56.234,25.304,56.217,25.267,56.211,25.213,56.24,25.209,56.282,25.236]],[[53.332,24.259,53.258,24.253,53.191,24.291,53.333,24.342,53.371,24.364,53.412,24.411,53.445,24.371,53.409,24.308,53.383,24.281,53.332,24.259]],[[52.617,24.289,52.6,24.281,52.582,24.335,52.584,24.352,52.629,24.377,52.658,24.333,52.617,24.289]],[[53.928,24.177,53.928,24.143,53.826,24.153,53.799,24.136,53.716,24.145,53.634,24.17,53.69,24.211,53.834,24.259,53.894,24.215,53.928,24.177]],[[54.465,24.443,54.457,24.423,54.428,24.425,54.358,24.443,54.335,24.471,54.379,24.505,54.398,24.506,54.427,24.471,54.465,24.443]]]},{"n":"Ukraine","c":"UA","r":3,"ml":2.7,"lx":32.14,"ly":49.72,"b":[22.132,45.234,40.128,52.354],"p":[[[38.214,47.091,38.178,47.08,37.829,47.096,37.761,47.091,37.543,47.075,37.34,46.917,37.219,46.917,37.048,46.876,36.932,46.825,36.795,46.714,36.689,46.764,36.559,46.763,36.432,46.733,36.279,46.659,36.195,46.646,36.025,46.667,35.827,46.624,35.4,46.381,35.257,46.204,35.204,46.169,35.132,46.126,35.055,46.104,35.015,46.106,35.218,46.232,35.28,46.279,35.291,46.314,35.292,46.371,35.23,46.441,35.064,46.267,34.97,46.242,34.85,46.19,34.844,46.074,34.857,45.987,34.907,45.879,35.002,45.733,34.946,45.729,34.8,45.791,34.794,45.892,34.687,45.977,34.523,45.977,34.45,45.966,34.354,46.062,34.224,46.101,34.128,46.09,34.027,46.107,33.807,46.208,33.66,46.22,33.654,46.146,33.594,46.096,33.499,46.079,33.43,46.058,33.263,46.126,33.202,46.176,32.942,46.124,32.797,46.131,32.477,46.084,32.33,46.13,32.036,46.261,31.925,46.287,31.831,46.282,31.78,46.325,31.843,46.346,31.916,46.349,31.992,46.364,32.013,46.387,32.008,46.43,31.856,46.462,31.714,46.472,31.624,46.51,31.555,46.554,31.716,46.555,31.878,46.522,32.131,46.509,32.361,46.475,32.419,46.518,32.553,46.592,32.578,46.616,32.354,46.565,32.127,46.597,32.044,46.642,31.974,46.709,31.945,46.784,31.964,46.855,31.94,46.982,31.865,47.095,31.838,47.157,31.759,47.213,31.837,47.087,31.866,47.003,31.913,46.926,31.902,46.722,31.873,46.65,31.78,46.632,31.657,46.642,31.532,46.665,31.563,46.777,31.497,46.738,31.403,46.629,31.32,46.613,31.137,46.624,30.796,46.552,30.773,46.473,30.722,46.366,30.672,46.304,30.657,46.267,30.512,46.105,30.493,46.09,30.219,45.867,30.184,45.85,30.007,45.798,29.902,45.752,29.821,45.732,29.685,45.755,29.628,45.722,29.602,45.683,29.601,45.6,29.67,45.541,29.727,45.343,29.706,45.26,29.652,45.314,29.568,45.371,29.404,45.42,29.224,45.403,29.027,45.321,28.894,45.29,28.824,45.311,28.782,45.31,28.767,45.286,28.77,45.267,28.791,45.252,28.788,45.241,28.761,45.234,28.451,45.292,28.318,45.347,28.213,45.45,28.265,45.484,28.31,45.499,28.471,45.507,28.499,45.518,28.502,45.542,28.514,45.572,28.509,45.618,28.492,45.666,28.562,45.736,28.668,45.794,28.729,45.852,28.739,45.937,28.85,45.979,28.948,46.05,28.972,46.128,29.006,46.176,28.944,46.288,28.931,46.362,28.927,46.424,28.958,46.458,29.05,46.497,29.146,46.527,29.186,46.524,29.201,46.505,29.205,46.379,29.224,46.377,29.255,46.393,29.305,46.467,29.34,46.445,29.393,46.437,29.433,46.456,29.459,46.454,29.491,46.435,29.555,46.408,29.615,46.399,29.665,46.417,29.707,46.449,29.752,46.438,29.838,46.351,29.878,46.36,30.076,46.378,30.108,46.402,30.131,46.423,29.924,46.539,29.935,46.625,29.942,46.724,29.918,46.782,29.878,46.829,29.72,46.883,29.598,46.939,29.572,46.964,29.569,46.997,29.563,47.048,29.515,47.091,29.511,47.128,29.542,47.186,29.549,47.247,29.539,47.271,29.511,47.291,29.456,47.293,29.383,47.328,29.334,47.376,29.201,47.444,29.16,47.456,29.135,47.49,29.123,47.53,29.151,47.581,29.186,47.659,29.211,47.732,29.211,47.775,29.195,47.882,29.125,47.965,29.093,47.975,29.037,47.952,28.973,47.933,28.923,47.951,28.866,47.996,28.774,48.12,28.602,48.144,28.53,48.15,28.463,48.091,28.442,48.109,28.423,48.147,28.388,48.162,28.341,48.144,28.327,48.161,28.347,48.213,28.291,48.239,28.159,48.238,28.088,48.257,28.08,48.296,28.038,48.321,27.963,48.334,27.891,48.365,27.82,48.416,27.714,48.45,27.574,48.465,27.562,48.47,27.549,48.478,27.458,48.443,27.404,48.416,27.337,48.433,27.229,48.371,27.008,48.368,26.901,48.372,26.847,48.387,26.64,48.294,26.619,48.26,26.572,48.248,26.442,48.23,26.306,48.204,26.277,48.113,26.236,48.064,26.163,47.993,25.909,47.968,25.689,47.932,25.464,47.911,25.17,47.823,25.074,47.746,24.979,47.724,24.893,47.718,24.838,47.761,24.651,47.877,24.579,47.931,24.484,47.947,24.381,47.938,24.282,47.911,24.178,47.906,24.06,47.945,24.047,47.941,24.002,47.936,23.709,47.983,23.682,47.99,23.669,47.992,23.629,47.996,23.408,47.99,23.203,48.085,23.139,48.087,23.091,48.049,23.055,48.007,22.913,47.964,22.877,47.947,22.856,47.96,22.846,47.997,22.857,48.03,22.836,48.06,22.782,48.095,22.769,48.11,22.702,48.107,22.683,48.104,22.676,48.104,22.582,48.134,22.52,48.205,22.424,48.243,22.35,48.256,22.317,48.287,22.295,48.327,22.272,48.358,22.269,48.361,22.254,48.407,22.231,48.412,22.227,48.413,22.132,48.405,22.143,48.569,22.295,48.686,22.333,48.745,22.389,48.873,22.432,48.934,22.483,48.983,22.524,49.031,22.539,49.073,22.58,49.077,22.701,49.04,22.81,49.021,22.84,49.039,22.852,49.063,22.847,49.081,22.76,49.136,22.706,49.171,22.702,49.193,22.722,49.241,22.732,49.295,22.72,49.354,22.661,49.484,22.649,49.539,22.706,49.606,22.891,49.766,22.952,49.826,23.036,49.899,23.264,50.073,23.409,50.174,23.506,50.23,23.649,50.327,23.712,50.377,23.973,50.41,24.005,50.457,24.053,50.508,24.09,50.53,24.095,50.617,24.046,50.723,24.007,50.76,23.978,50.786,23.997,50.809,24.026,50.816,24.062,50.82,24.106,50.845,24.096,50.873,23.986,50.94,23.938,50.993,23.863,51.126,23.712,51.265,23.664,51.31,23.658,51.352,23.68,51.395,23.659,51.449,23.605,51.518,23.614,51.525,23.609,51.61,23.647,51.629,23.707,51.641,23.792,51.637,23.864,51.624,23.951,51.585,23.978,51.591,24.127,51.665,24.28,51.775,24.324,51.838,24.362,51.868,24.495,51.883,24.611,51.89,24.685,51.888,24.866,51.899,24.974,51.911,25.067,51.931,25.267,51.938,25.58,51.925,25.786,51.924,25.925,51.914,26.267,51.855,26.394,51.844,26.453,51.813,26.567,51.802,26.773,51.771,26.953,51.754,27.074,51.761,27.142,51.752,27.27,51.614,27.296,51.597,27.348,51.594,27.452,51.606,27.601,51.602,27.69,51.572,27.677,51.49,27.7,51.478,27.741,51.483,27.789,51.529,27.829,51.577,27.859,51.592,28.011,51.56,28.08,51.565,28.144,51.602,28.184,51.608,28.292,51.582,28.425,51.564,28.532,51.562,28.599,51.543,28.648,51.457,28.69,51.439,28.731,51.433,28.793,51.51,28.85,51.54,28.928,51.562,28.978,51.572,29.013,51.599,29.061,51.625,29.102,51.628,29.136,51.617,29.174,51.581,29.23,51.497,29.299,51.413,29.346,51.383,29.47,51.408,29.553,51.435,29.706,51.44,29.909,51.458,30.064,51.482,30.161,51.478,30.22,51.451,30.309,51.4,30.333,51.326,30.45,51.274,30.545,51.265,30.577,51.318,30.633,51.355,30.612,51.406,30.602,51.471,30.561,51.531,30.533,51.596,30.584,51.689,30.639,51.77,30.667,51.814,30.755,51.895,30.846,51.953,30.981,52.046,31.079,52.077,31.168,52.063,31.218,52.05,31.346,52.105,31.574,52.108,31.763,52.101,31.782,52.099,31.876,52.071,31.974,52.047,32.042,52.045,32.122,52.051,32.217,52.083,32.283,52.114,32.363,52.272,32.391,52.295,32.435,52.307,32.508,52.309,32.645,52.279,32.806,52.253,32.9,52.256,33.148,52.34,33.287,52.354,33.452,52.334,33.613,52.333,33.735,52.345,33.819,52.316,33.922,52.251,34.015,52.156,34.113,51.98,34.398,51.78,34.403,51.742,34.379,51.717,34.239,51.692,34.121,51.679,34.115,51.645,34.147,51.608,34.201,51.554,34.209,51.484,34.207,51.42,34.23,51.363,34.275,51.34,34.281,51.312,34.228,51.277,34.214,51.255,34.234,51.244,34.491,51.237,34.617,51.203,34.712,51.172,34.76,51.169,34.869,51.189,34.99,51.202,35.064,51.203,35.093,51.181,35.115,51.121,35.158,51.061,35.198,51.044,35.269,51.047,35.312,51.044,35.335,51.021,35.309,50.987,35.315,50.95,35.346,50.904,35.383,50.799,35.417,50.768,35.44,50.728,35.44,50.682,35.412,50.642,35.392,50.611,35.412,50.54,35.488,50.46,35.546,50.44,35.591,50.369,35.674,50.346,35.796,50.406,35.89,50.437,36.008,50.42,36.116,50.409,36.189,50.368,36.243,50.312,36.306,50.28,36.369,50.297,36.5,50.28,36.56,50.235,36.619,50.209,36.696,50.246,36.759,50.292,36.988,50.34,37.131,50.352,37.171,50.361,37.255,50.395,37.343,50.418,37.423,50.411,37.501,50.341,37.582,50.292,37.605,50.215,37.704,50.109,37.95,49.964,38.047,49.92,38.113,49.928,38.147,49.939,38.163,49.955,38.178,50.025,38.209,50.051,38.259,50.052,38.451,49.964,38.552,49.955,38.648,49.953,38.777,49.884,38.918,49.825,39.028,49.818,39.115,49.842,39.175,49.856,39.212,49.833,39.246,49.782,39.303,49.742,39.368,49.731,39.463,49.728,39.627,49.651,39.781,49.572,39.877,49.568,39.958,49.591,40.031,49.597,40.081,49.577,40.095,49.543,40.058,49.497,40.058,49.432,40.126,49.369,40.128,49.307,40.109,49.252,40.07,49.2,39.976,49.13,39.89,49.064,39.759,49.037,39.687,49.008,39.706,48.96,39.753,48.914,39.864,48.878,39.989,48.851,40.004,48.822,39.984,48.807,39.904,48.794,39.793,48.808,39.756,48.782,39.705,48.739,39.67,48.662,39.645,48.591,39.765,48.572,39.836,48.543,39.858,48.484,39.883,48.419,39.89,48.36,39.85,48.332,39.847,48.303,39.866,48.288,39.918,48.282,39.958,48.269,39.961,48.238,39.885,48.168,39.814,48.035,39.776,47.964,39.779,47.888,39.736,47.845,39.658,47.841,39.391,47.834,39.158,47.837,39.058,47.848,38.9,47.855,38.822,47.837,38.719,47.714,38.641,47.666,38.511,47.622,38.369,47.61,38.287,47.559,38.259,47.48,38.257,47.409,38.243,47.374,38.212,47.343,38.201,47.321,38.208,47.297,38.241,47.288,38.281,47.277,38.281,47.259,38.265,47.237,38.221,47.213,38.201,47.175,38.206,47.136,38.214,47.091]],[[32.012,46.204,32.15,46.155,32.009,46.168,31.7,46.214,31.564,46.258,31.529,46.307,31.509,46.373,31.585,46.303,31.638,46.273,32.012,46.204]]]},{"n":"Uganda","c":"UG","r":3,"ml":3,"lx":32.95,"ly":1.97,"b":[29.562,-1.47,34.978,4.22],"p":[[[33.903,-1.002,33.794,-1.002,33.616,-1.002,33.438,-1.002,33.261,-1.002,33.083,-1.002,32.905,-1.002,32.727,-1.002,32.55,-1.002,32.372,-1.002,32.194,-1.002,32.016,-1.002,31.839,-1.002,31.661,-1.002,31.483,-1.002,31.305,-1.002,31.128,-1.002,30.95,-1.002,30.845,-1.002,30.824,-0.999,30.809,-0.995,30.742,-1.008,30.673,-1.051,30.599,-1.07,30.52,-1.062,30.51,-1.067,30.47,-1.066,30.412,-1.063,30.36,-1.075,30.321,-1.113,30.28,-1.179,30.207,-1.254,30.15,-1.321,30.102,-1.369,29.991,-1.447,29.93,-1.47,29.9,-1.466,29.882,-1.452,29.847,-1.352,29.825,-1.336,29.61,-1.387,29.577,-1.388,29.58,-1.357,29.564,-1.121,29.562,-0.977,29.59,-0.887,29.606,-0.783,29.608,-0.691,29.648,-0.535,29.633,-0.442,29.684,-0.114,29.698,-0.06,29.718,0.098,29.75,0.147,29.778,0.166,29.815,0.264,29.885,0.419,29.934,0.499,29.924,0.674,29.932,0.793,29.943,0.819,30.047,0.864,30.183,0.973,30.24,1.103,30.321,1.185,30.478,1.239,30.943,1.683,31.159,1.922,31.253,2.045,31.256,2.088,31.274,2.146,31.236,2.191,31.191,2.232,31.176,2.27,31.138,2.289,31.082,2.288,31.045,2.316,31.004,2.369,30.962,2.403,30.83,2.4,30.729,2.455,30.73,2.53,30.77,2.678,30.847,2.847,30.851,2.894,30.84,2.933,30.821,2.968,30.787,3.001,30.754,3.042,30.779,3.163,30.828,3.283,30.868,3.342,30.906,3.409,30.895,3.464,30.839,3.491,30.868,3.544,30.929,3.634,31.048,3.725,31.152,3.786,31.222,3.786,31.357,3.738,31.48,3.68,31.547,3.678,31.629,3.701,31.798,3.803,31.839,3.77,31.888,3.709,31.942,3.608,32.048,3.561,32.099,3.529,32.136,3.52,32.156,3.528,32.197,3.608,32.246,3.651,32.336,3.706,32.535,3.75,32.677,3.763,32.737,3.773,32.838,3.798,32.997,3.88,33.154,3.775,33.324,3.754,33.489,3.755,33.54,3.787,33.568,3.812,33.742,3.985,33.976,4.22,34.132,3.889,34.186,3.87,34.178,3.841,34.165,3.813,34.267,3.733,34.393,3.692,34.438,3.651,34.442,3.606,34.399,3.413,34.407,3.358,34.448,3.163,34.523,3.12,34.589,2.925,34.723,2.842,34.742,2.818,34.773,2.723,34.814,2.62,34.847,2.596,34.866,2.59,34.906,2.48,34.883,2.418,34.914,2.23,34.964,2.062,34.978,1.862,34.978,1.774,34.976,1.72,34.965,1.643,34.941,1.599,34.898,1.556,34.851,1.489,34.81,1.417,34.784,1.381,34.804,1.273,34.799,1.245,34.788,1.231,34.727,1.214,34.649,1.185,34.602,1.156,34.535,1.102,34.482,1.042,34.411,0.867,34.293,0.731,34.273,0.686,34.161,0.605,34.112,0.505,34.081,0.382,34.037,0.295,33.943,0.174,33.921,-0.017,33.924,-0.398,33.9,-0.832,33.903,-1.002]]]},{"n":"Turkmenistan","c":"TM","r":4,"ml":3,"lx":58.68,"ly":39.86,"b":[52.494,35.171,66.629,42.778],"p":[[[53.11,38.803,53.1,38.756,53.046,38.897,53.019,39.053,53.053,39.097,53.092,39.094,53.055,39.038,53.11,38.803]],[[55.977,41.322,56.242,41.311,56.48,41.301,56.774,41.288,56.861,41.276,56.966,41.265,57.018,41.263,57.064,41.307,57.095,41.331,57.119,41.35,57.114,41.372,57.077,41.39,57.018,41.451,56.985,41.669,56.964,41.857,57.034,41.915,57.114,41.957,57.229,42.084,57.291,42.124,57.382,42.156,57.686,42.165,57.814,42.19,57.856,42.231,57.923,42.335,57.946,42.42,57.983,42.459,58.029,42.488,58.075,42.487,58.166,42.462,58.234,42.448,58.283,42.429,58.327,42.399,58.371,42.347,58.377,42.312,58.397,42.292,58.431,42.292,58.457,42.292,58.474,42.299,58.486,42.317,58.477,42.34,58.418,42.407,58.289,42.527,58.204,42.576,58.162,42.603,58.152,42.628,58.206,42.666,58.26,42.688,58.353,42.672,58.477,42.663,58.532,42.682,58.589,42.778,58.73,42.676,58.877,42.561,58.931,42.54,59.036,42.528,59.123,42.524,59.16,42.511,59.199,42.482,59.277,42.356,59.354,42.323,59.451,42.3,59.763,42.302,59.858,42.295,59.937,42.236,59.985,42.212,60.006,42.191,60.001,42.165,59.982,42.132,59.979,42.068,59.974,42.019,59.949,41.995,59.942,41.974,59.963,41.954,60.107,41.907,60.156,41.857,60.192,41.834,60.201,41.803,60.176,41.782,60.109,41.793,60.076,41.76,60.076,41.701,60.124,41.645,60.138,41.594,60.106,41.545,60.069,41.476,60.067,41.427,60.09,41.399,60.2,41.349,60.455,41.222,60.514,41.216,60.755,41.246,60.867,41.249,60.933,41.229,61.12,41.211,61.179,41.191,61.242,41.189,61.329,41.195,61.388,41.252,61.417,41.265,61.444,41.275,61.497,41.276,61.645,41.24,61.8,41.163,61.903,41.094,61.954,41.031,62.018,40.894,62.095,40.683,62.188,40.541,62.298,40.467,62.375,40.332,62.442,40.036,62.483,39.976,62.525,39.944,62.651,39.858,62.907,39.717,63.058,39.633,63.292,39.5,63.506,39.377,63.721,39.188,63.764,39.161,63.953,39.058,64.163,38.954,64.31,38.977,64.532,38.816,64.622,38.756,64.66,38.736,64.821,38.672,65.077,38.539,65.4,38.349,65.613,38.239,65.671,38.226,65.729,38.226,65.79,38.25,65.857,38.269,65.971,38.244,66.095,38.2,66.173,38.167,66.264,38.118,66.335,38.072,66.39,38.051,66.575,38.011,66.606,37.987,66.626,37.96,66.629,37.932,66.526,37.786,66.511,37.599,66.511,37.459,66.522,37.348,66.472,37.345,66.35,37.368,66.108,37.415,65.901,37.508,65.765,37.569,65.744,37.561,65.683,37.519,65.641,37.468,65.608,37.368,65.555,37.251,65.304,37.247,65.09,37.238,64.952,37.194,64.816,37.132,64.782,37.059,64.753,36.965,64.674,36.75,64.603,36.555,64.566,36.428,64.511,36.341,64.358,36.226,64.184,36.149,64.092,36.113,64.051,36.068,64.042,36.025,64.01,36.012,63.938,36.02,63.863,36.012,63.697,35.968,63.517,35.913,63.302,35.858,63.179,35.858,63.13,35.846,63.109,35.819,63.13,35.767,63.151,35.728,63.17,35.678,63.119,35.638,63.084,35.568,63.057,35.446,62.98,35.409,62.858,35.35,62.723,35.271,62.688,35.255,62.611,35.233,62.533,35.24,62.463,35.251,62.387,35.231,62.308,35.171,62.271,35.189,62.253,35.25,62.213,35.29,62.09,35.38,61.984,35.444,61.938,35.448,61.841,35.431,61.72,35.419,61.621,35.432,61.543,35.458,61.422,35.546,61.378,35.593,61.345,35.629,61.262,35.62,61.239,35.659,61.236,35.706,61.259,35.762,61.252,35.868,61.206,35.944,61.153,35.977,61.159,36,61.183,36.053,61.212,36.099,61.212,36.191,61.175,36.29,61.16,36.433,61.17,36.572,61.12,36.643,60.708,36.643,60.341,36.638,60.321,36.654,60.178,36.829,60.063,36.963,59.949,37.042,59.687,37.138,59.562,37.179,59.455,37.253,59.367,37.334,59.345,37.445,59.327,37.481,59.302,37.511,59.274,37.524,59.241,37.521,58.937,37.65,58.815,37.683,58.701,37.656,58.65,37.652,58.55,37.688,58.436,37.639,58.387,37.635,58.318,37.647,58.262,37.666,58.109,37.783,57.981,37.83,57.888,37.861,57.711,37.905,57.521,37.928,57.424,37.948,57.354,37.973,57.336,37.99,57.337,38.033,57.331,38.089,57.308,38.13,57.26,38.18,57.194,38.216,57.079,38.21,56.907,38.213,56.775,38.25,56.67,38.257,56.544,38.25,56.441,38.249,56.367,38.223,56.324,38.191,56.297,38.095,56.272,38.08,56.229,38.073,56.171,38.078,56.05,38.078,55.841,38.095,55.578,38.1,55.381,38.051,55.225,37.981,55.076,37.902,54.9,37.778,54.849,37.723,54.745,37.502,54.699,37.47,54.64,37.445,54.579,37.44,54.459,37.408,54.3,37.354,54.192,37.332,53.914,37.344,53.898,37.414,53.848,37.67,53.824,37.928,53.825,38.047,53.854,38.286,53.852,38.406,53.84,38.515,53.852,38.622,53.874,38.742,53.885,38.864,53.869,38.949,53.815,39.018,53.724,39.103,53.71,39.153,53.705,39.21,53.618,39.216,53.539,39.274,53.475,39.306,53.336,39.341,53.267,39.343,53.203,39.317,53.157,39.265,53.124,39.347,53.125,39.432,53.236,39.609,53.305,39.557,53.39,39.536,53.497,39.533,53.603,39.547,53.582,39.607,53.533,39.642,53.472,39.669,53.45,39.749,53.458,39.831,53.487,39.909,53.454,39.941,53.404,39.96,53.289,39.958,53.139,39.979,52.988,39.988,52.952,39.895,53.036,39.774,52.965,39.834,52.898,39.913,52.805,40.054,52.744,40.22,52.734,40.399,52.785,40.547,52.85,40.686,52.889,40.863,52.943,41.038,52.998,40.96,53.06,40.89,53.145,40.825,53.192,40.809,53.333,40.783,53.424,40.793,53.52,40.831,53.615,40.819,53.694,40.746,53.764,40.666,53.87,40.649,54.089,40.707,54.193,40.72,54.283,40.694,54.33,40.689,54.377,40.693,54.336,40.765,54.319,40.835,54.374,40.871,54.547,40.832,54.657,40.858,54.685,40.873,54.71,40.891,54.723,40.951,54.718,41.013,54.704,41.071,54.671,41.122,54.592,41.194,54.285,41.364,54.181,41.432,54.095,41.519,54.04,41.643,53.995,41.773,53.954,41.868,53.846,42.091,53.805,42.118,53.752,42.129,53.625,42.136,53.496,42.12,53.285,42.082,53.164,42.094,53.108,42.07,52.97,41.976,52.905,41.896,52.815,41.712,52.883,41.653,52.882,41.614,52.83,41.342,52.862,41.21,52.85,41.2,52.826,41.231,52.747,41.365,52.609,41.529,52.494,41.78,52.697,41.944,52.871,42.061,53.013,42.131,53.056,42.148,53.25,42.206,53.501,42.258,53.685,42.297,53.926,42.33,54.005,42.336,54.121,42.335,54.215,42.304,54.272,42.28,54.473,42.18,54.678,42.078,54.854,41.965,54.904,41.919,54.932,41.864,54.952,41.81,55.102,41.639,55.162,41.56,55.25,41.458,55.32,41.408,55.388,41.347,55.434,41.296,55.487,41.272,55.545,41.263,55.679,41.279,55.839,41.311,55.935,41.324,55.977,41.322]]]},{"n":"Turkey","c":"TR","r":2,"ml":2,"lx":34.51,"ly":39.35,"b":[25.669,35.831,44.817,42.093],"p":[[[25.97,40.136,25.74,40.105,25.669,40.136,25.741,40.196,25.875,40.234,25.918,40.238,25.977,40.178,25.97,40.136]],[[41.51,41.517,41.577,41.497,41.702,41.472,41.779,41.441,41.824,41.432,41.926,41.496,42.078,41.494,42.211,41.487,42.28,41.475,42.364,41.454,42.466,41.44,42.508,41.47,42.567,41.559,42.59,41.571,42.607,41.579,42.682,41.586,42.754,41.579,42.788,41.564,42.822,41.492,42.907,41.467,43.057,41.353,43.149,41.307,43.171,41.288,43.141,41.265,43.153,41.236,43.205,41.199,43.279,41.185,43.359,41.19,43.402,41.177,43.433,41.156,43.442,41.126,43.439,41.107,43.455,41.065,43.517,41.005,43.592,40.968,43.632,40.929,43.696,40.794,43.723,40.72,43.713,40.648,43.668,40.574,43.569,40.482,43.594,40.444,43.616,40.393,43.608,40.357,43.678,40.239,43.71,40.167,43.683,40.15,43.666,40.126,43.792,40.07,43.942,40.023,44.005,40.014,44.178,40.036,44.289,40.04,44.4,39.996,44.56,39.888,44.734,39.746,44.768,39.704,44.783,39.685,44.817,39.65,44.782,39.651,44.725,39.682,44.587,39.769,44.517,39.731,44.456,39.667,44.389,39.422,44.335,39.396,44.24,39.397,44.124,39.405,44.044,39.393,44.023,39.377,44.034,39.351,44.058,39.311,44.074,39.26,44.079,39.218,44.121,39.181,44.178,39.145,44.181,39.108,44.172,39.056,44.159,39.017,44.145,38.994,44.171,38.934,44.232,38.863,44.272,38.836,44.257,38.701,44.28,38.641,44.298,38.558,44.291,38.42,44.299,38.386,44.32,38.375,44.376,38.37,44.431,38.357,44.45,38.334,44.45,38.318,44.381,38.255,44.373,38.21,44.349,38.146,44.329,38.109,44.268,38.039,44.229,37.967,44.211,37.908,44.223,37.88,44.336,37.872,44.398,37.829,44.561,37.745,44.59,37.71,44.545,37.658,44.546,37.636,44.567,37.609,44.577,37.56,44.573,37.506,44.574,37.435,44.604,37.424,44.715,37.357,44.794,37.29,44.797,37.27,44.758,37.217,44.767,37.156,44.765,37.142,44.731,37.165,44.669,37.174,44.606,37.176,44.566,37.158,44.496,37.111,44.402,37.058,44.326,37.011,44.282,36.978,44.246,36.983,44.217,37.012,44.202,37.052,44.208,37.203,44.192,37.25,44.156,37.283,44.114,37.302,44.065,37.312,44.013,37.314,43.94,37.269,43.836,37.224,43.676,37.227,43.568,37.236,43.516,37.245,43.307,37.315,43.263,37.317,43.185,37.345,43.092,37.367,42.937,37.325,42.869,37.335,42.775,37.372,42.741,37.362,42.635,37.249,42.456,37.129,42.359,37.109,42.313,37.23,42.269,37.277,42.248,37.282,42.203,37.297,42.168,37.289,42.06,37.206,41.887,37.156,41.744,37.126,41.516,37.089,41.34,37.071,41.265,37.069,41.102,37.086,40.959,37.109,40.816,37.108,40.706,37.098,40.45,37.009,40.016,36.826,39.687,36.739,39.501,36.702,39.357,36.682,39.108,36.681,38.906,36.695,38.767,36.693,38.689,36.715,38.578,36.789,38.444,36.862,38.384,36.879,38.306,36.893,38.192,36.902,37.907,36.795,37.818,36.766,37.72,36.744,37.524,36.678,37.436,36.643,37.327,36.647,37.187,36.656,37.066,36.653,36.985,36.702,36.942,36.758,36.777,36.793,36.659,36.803,36.628,36.778,36.597,36.701,36.547,36.506,36.538,36.457,36.641,36.264,36.637,36.234,36.562,36.224,36.477,36.221,36.421,36.203,36.375,36.171,36.348,36.004,36.249,35.973,36.202,35.938,36.154,35.834,36.127,35.831,35.968,35.91,35.893,35.917,35.957,35.998,35.887,36.159,35.811,36.31,35.883,36.406,36.032,36.523,36.188,36.659,36.188,36.743,36.18,36.807,36.135,36.852,36.049,36.911,35.905,36.848,35.802,36.778,35.734,36.764,35.661,36.724,35.626,36.653,35.537,36.597,35.393,36.575,35.176,36.635,34.943,36.726,34.811,36.799,34.704,36.817,34.601,36.784,34.3,36.604,34.023,36.341,33.955,36.295,33.695,36.182,33.523,36.144,33.442,36.153,33.1,36.103,32.929,36.096,32.795,36.036,32.534,36.101,32.378,36.184,32.284,36.268,32.131,36.449,32.022,36.535,31.778,36.613,31.353,36.801,31.241,36.822,30.95,36.849,30.644,36.866,30.582,36.797,30.558,36.526,30.506,36.451,30.484,36.31,30.446,36.27,30.387,36.243,30.295,36.288,30.232,36.307,30.083,36.249,29.789,36.168,29.689,36.157,29.348,36.259,29.224,36.324,29.143,36.397,29.116,36.52,29.066,36.59,29.058,36.638,29.038,36.693,28.97,36.715,28.896,36.674,28.817,36.675,28.718,36.701,28.484,36.804,28.304,36.812,28.196,36.686,28.112,36.646,28.019,36.634,28.014,36.67,28.084,36.751,27.804,36.736,27.656,36.675,27.54,36.684,27.454,36.712,27.467,36.746,27.555,36.759,27.631,36.787,27.934,36.809,28.005,36.832,28.083,36.92,28.224,36.996,28.242,37.029,28.134,37.029,27.668,37.007,27.349,37.02,27.311,36.982,27.263,36.977,27.25,37.079,27.3,37.127,27.368,37.122,27.535,37.164,27.52,37.249,27.401,37.307,27.376,37.341,27.29,37.349,27.219,37.389,27.204,37.491,27.148,37.604,27.068,37.658,27.078,37.688,27.224,37.725,27.255,37.882,27.232,37.979,27.159,37.987,26.944,38.063,26.879,38.055,26.807,38.138,26.683,38.198,26.621,38.176,26.582,38.149,26.525,38.162,26.428,38.214,26.333,38.242,26.291,38.277,26.344,38.37,26.416,38.368,26.43,38.441,26.372,38.562,26.378,38.624,26.441,38.641,26.514,38.629,26.587,38.557,26.61,38.487,26.595,38.419,26.641,38.352,26.674,38.336,26.696,38.405,26.727,38.419,26.77,38.388,26.861,38.373,27.099,38.416,27.144,38.452,26.97,38.448,26.907,38.482,26.838,38.558,26.795,38.626,26.788,38.66,26.764,38.71,26.79,38.736,26.909,38.776,27.014,38.887,26.97,38.919,26.92,38.934,26.866,38.923,26.815,38.961,26.808,39.014,26.849,39.057,26.854,39.116,26.719,39.261,26.682,39.292,26.711,39.34,26.813,39.419,26.911,39.517,26.899,39.55,26.827,39.563,26.484,39.521,26.351,39.484,26.113,39.467,26.096,39.521,26.101,39.569,26.155,39.657,26.15,39.873,26.181,39.99,26.313,40.025,26.475,40.197,26.738,40.4,27.012,40.396,27.122,40.452,27.285,40.456,27.314,40.415,27.333,40.376,27.476,40.32,27.728,40.329,27.789,40.351,27.849,40.382,27.732,40.481,27.769,40.51,27.875,40.513,27.99,40.489,27.995,40.467,27.964,40.435,27.929,40.38,27.963,40.37,28.289,40.403,28.63,40.376,28.739,40.391,29.007,40.39,29.055,40.424,28.974,40.467,28.895,40.482,28.841,40.503,28.788,40.534,28.958,40.631,29.054,40.649,29.508,40.708,29.845,40.738,29.849,40.76,29.801,40.76,29.365,40.809,29.26,40.847,29.114,40.938,29.082,40.963,29.046,41.008,29.067,41.102,29.094,41.177,29.148,41.221,29.322,41.228,29.919,41.151,30.345,41.197,30.81,41.085,31.255,41.108,31.347,41.158,31.458,41.32,32.086,41.589,32.306,41.73,32.542,41.806,32.947,41.892,33.285,42.005,33.381,42.018,34.193,41.964,34.75,41.957,35.006,42.063,35.155,42.028,35.141,41.99,35.114,41.957,35.122,41.891,35.209,41.794,35.298,41.729,35.558,41.634,35.92,41.714,35.978,41.705,36.052,41.683,36.179,41.427,36.278,41.336,36.405,41.275,36.51,41.263,36.587,41.327,36.647,41.353,36.778,41.363,36.992,41.275,37.066,41.184,37.431,41.114,37.766,41.079,37.91,41.002,38.381,40.925,38.557,40.937,38.852,41.018,39.426,41.106,39.808,40.983,39.911,40.966,40,40.977,40.128,40.943,40.265,40.961,40.688,41.107,40.82,41.19,40.959,41.212,41.084,41.261,41.414,41.424,41.51,41.517]],[[28.014,41.969,27.987,41.855,28.05,41.729,28.198,41.554,28.346,41.466,28.947,41.248,29.057,41.23,29.032,41.14,28.996,41.061,28.956,41.008,28.78,40.974,28.295,41.071,28.172,41.081,28.086,41.061,27.925,40.991,27.747,41.013,27.499,40.973,27.43,40.84,27.258,40.687,26.975,40.564,26.772,40.498,26.468,40.261,26.33,40.123,26.272,40.097,26.203,40.075,26.226,40.142,26.26,40.202,26.252,40.248,26.254,40.315,26.355,40.39,26.447,40.445,26.72,40.544,26.792,40.627,26.578,40.625,26.361,40.606,26.224,40.618,26.105,40.611,26.068,40.683,26.039,40.727,26.07,40.74,26.109,40.75,26.179,40.827,26.241,40.883,26.331,40.954,26.354,40.997,26.354,41.037,26.333,41.064,26.328,41.097,26.326,41.143,26.331,41.239,26.536,41.343,26.602,41.354,26.625,41.402,26.61,41.512,26.581,41.601,26.545,41.607,26.495,41.633,26.463,41.663,26.411,41.696,26.321,41.717,26.318,41.745,26.327,41.773,26.36,41.802,26.511,41.826,26.529,41.847,26.55,41.897,26.58,41.948,26.615,41.965,26.679,41.963,26.8,41.975,26.885,41.992,26.969,42.027,27.012,42.059,27.193,42.077,27.244,42.093,27.295,42.08,27.363,42.025,27.475,41.947,27.535,41.921,27.58,41.933,27.661,41.961,27.739,41.962,27.802,41.957,27.832,41.981,27.879,41.987,28.014,41.969]]]},{"n":"Tunisia","c":"TN","r":3,"ml":3,"lx":9.01,"ly":33.69,"b":[7.496,30.229,11.536,37.34],"p":[[[11.505,33.182,11.502,33.156,11.467,32.966,11.459,32.897,11.454,32.782,11.454,32.643,11.534,32.525,11.536,32.473,11.505,32.414,11.358,32.345,11.168,32.257,11.005,32.173,10.826,32.081,10.772,32.021,10.683,31.975,10.609,31.93,10.596,31.886,10.544,31.803,10.476,31.736,10.306,31.705,10.275,31.685,10.196,31.585,10.16,31.546,10.115,31.464,10.173,31.251,10.243,31.032,10.257,30.941,10.256,30.865,10.216,30.783,10.126,30.666,10.06,30.58,9.933,30.425,9.895,30.387,9.807,30.342,9.638,30.282,9.519,30.229,9.458,30.465,9.406,30.667,9.363,30.833,9.288,31.125,9.224,31.374,9.16,31.621,9.102,31.846,9.044,32.072,9.019,32.105,8.844,32.212,8.683,32.31,8.515,32.422,8.333,32.544,8.304,32.696,8.211,32.927,8.113,33.055,8.076,33.089,7.877,33.172,7.763,33.233,7.731,33.269,7.709,33.362,7.628,33.549,7.534,33.718,7.5,33.832,7.496,33.977,7.514,34.081,7.554,34.125,7.749,34.254,7.838,34.41,7.949,34.469,8.046,34.513,8.123,34.564,8.193,34.646,8.246,34.734,8.255,34.829,8.277,34.979,8.312,35.085,8.394,35.204,8.36,35.3,8.316,35.403,8.329,35.582,8.318,35.655,8.283,35.719,8.247,35.802,8.246,35.871,8.28,36.051,8.307,36.189,8.349,36.368,8.334,36.418,8.303,36.456,8.209,36.495,8.208,36.519,8.231,36.545,8.37,36.633,8.444,36.761,8.507,36.788,8.601,36.834,8.598,36.884,8.577,36.937,8.824,36.998,9.059,37.156,9.142,37.195,9.688,37.34,9.759,37.33,9.838,37.309,9.816,37.255,9.784,37.211,9.83,37.135,9.896,37.182,9.879,37.213,9.876,37.254,9.988,37.258,10.087,37.251,10.196,37.206,10.189,37.034,10.334,36.865,10.293,36.781,10.412,36.732,10.518,36.791,10.571,36.879,10.766,36.93,10.951,37.059,11.054,37.073,11.077,36.967,11.127,36.874,11.057,36.841,10.967,36.743,10.798,36.493,10.642,36.42,10.526,36.323,10.488,36.255,10.477,36.175,10.506,36.032,10.591,35.887,10.689,35.8,10.784,35.772,11.004,35.634,11.001,35.552,11.032,35.454,11.043,35.335,11.12,35.24,10.956,35.034,10.866,34.884,10.691,34.678,10.535,34.545,10.2,34.346,10.118,34.28,10.065,34.212,10.04,34.14,10.049,34.056,10.159,33.85,10.305,33.728,10.454,33.663,10.713,33.689,10.704,33.61,10.723,33.514,10.828,33.519,10.898,33.534,10.958,33.626,11.085,33.563,11.15,33.369,11.257,33.309,11.27,33.286,11.232,33.272,11.203,33.249,11.234,33.234,11.338,33.209,11.401,33.225,11.505,33.182]],[[11.278,34.754,11.124,34.682,11.153,34.745,11.255,34.82,11.281,34.802,11.278,34.754]],[[10.958,33.722,10.931,33.717,10.883,33.69,10.857,33.687,10.785,33.718,10.757,33.717,10.722,33.739,10.734,33.856,10.745,33.889,10.922,33.893,11.018,33.823,11.034,33.805,11.038,33.785,10.993,33.746,10.958,33.722]]]},{"n":"Trinidad and Tobago","c":"TT","r":5,"ml":4.5,"lx":-60.92,"ly":11,"b":[-61.906,10.065,-60.525,11.325],"p":[[[-60.756,11.179,-60.811,11.169,-60.804,11.208,-60.709,11.277,-60.563,11.324,-60.525,11.325,-60.546,11.264,-60.756,11.179]],[[-61.012,10.134,-61.174,10.078,-61.597,10.065,-61.772,10.085,-61.906,10.069,-61.661,10.192,-61.633,10.243,-61.529,10.253,-61.499,10.269,-61.465,10.539,-61.478,10.603,-61.499,10.639,-61.541,10.664,-61.635,10.699,-61.651,10.718,-61.592,10.748,-61.465,10.764,-61.37,10.797,-61.174,10.803,-61.079,10.832,-60.918,10.84,-60.997,10.716,-61.034,10.67,-61.019,10.558,-61.037,10.482,-61.016,10.386,-60.968,10.323,-61,10.261,-61.004,10.168,-61.012,10.134]]]},{"n":"Tonga","c":"TO","r":4,"ml":4,"lx":-175.16,"ly":-21.21,"b":[-175.362,-21.451,-173.922,-18.565],"p":[[[-175.162,-21.169,-175.148,-21.169,-175.132,-21.14,-175.078,-21.129,-175.084,-21.161,-175.157,-21.264,-175.202,-21.223,-175.335,-21.158,-175.362,-21.107,-175.318,-21.068,-175.323,-21.099,-175.3,-21.113,-175.225,-21.119,-175.158,-21.146,-175.2,-21.156,-175.162,-21.169]],[[-173.954,-18.639,-173.991,-18.699,-174.009,-18.698,-174.053,-18.663,-174.069,-18.64,-174.002,-18.571,-173.968,-18.565,-173.922,-18.589,-173.924,-18.608,-173.954,-18.639]],[[-174.913,-21.3,-174.919,-21.451,-174.968,-21.382,-174.973,-21.35,-174.923,-21.303,-174.913,-21.3]]]},{"n":"Togo","c":"TG","r":6,"ml":5,"lx":1.06,"ly":8.81,"b":[-0.09,6.089,1.778,11.116],"p":[[[0.9,10.993,0.875,10.886,0.822,10.753,0.788,10.71,0.763,10.387,0.78,10.36,0.792,10.352,0.958,10.242,1.176,10.098,1.33,9.997,1.343,9.963,1.345,9.75,1.347,9.568,1.379,9.463,1.386,9.362,1.424,9.285,1.566,9.137,1.6,9.05,1.604,8.771,1.607,8.559,1.625,8.271,1.625,8.03,1.625,7.726,1.625,7.369,1.625,6.997,1.531,6.992,1.582,6.877,1.591,6.772,1.603,6.738,1.578,6.687,1.599,6.61,1.639,6.582,1.743,6.426,1.778,6.295,1.611,6.251,1.623,6.217,1.311,6.147,1.187,6.089,1.185,6.145,1.14,6.155,1.084,6.174,1.05,6.203,1.002,6.269,0.985,6.32,0.912,6.329,0.822,6.386,0.737,6.453,0.707,6.519,0.715,6.549,0.702,6.581,0.673,6.593,0.596,6.742,0.548,6.802,0.526,6.851,0.533,6.888,0.523,6.939,0.538,6.98,0.579,7.004,0.592,7.034,0.596,7.097,0.62,7.227,0.635,7.354,0.591,7.389,0.537,7.399,0.51,7.435,0.499,7.495,0.5,7.547,0.605,7.728,0.584,8.146,0.599,8.21,0.647,8.253,0.688,8.304,0.686,8.355,0.616,8.48,0.483,8.575,0.415,8.653,0.379,8.722,0.373,8.759,0.453,8.814,0.489,8.851,0.493,8.895,0.46,8.974,0.466,9.115,0.497,9.221,0.529,9.358,0.526,9.398,0.448,9.48,0.405,9.491,0.371,9.486,0.289,9.432,0.26,9.426,0.242,9.442,0.233,9.464,0.262,9.496,0.252,9.536,0.275,9.571,0.327,9.587,0.343,9.604,0.273,9.621,0.265,9.645,0.27,9.668,0.29,9.672,0.312,9.671,0.324,9.688,0.335,9.804,0.343,9.845,0.352,9.925,0.363,10.236,0.379,10.269,0.381,10.292,0.332,10.307,0.216,10.391,0.148,10.455,0.089,10.521,0.039,10.564,-0.058,10.631,-0.086,10.673,-0.09,10.716,-0.061,10.801,-0.014,10.891,0.009,11.021,-0.005,11.056,-0.069,11.116,0.159,11.07,0.484,10.992,0.491,10.978,0.493,10.955,0.549,10.955,0.643,10.983,0.9,10.993]]]},{"n":"East Timor","c":"TL","r":5,"ml":4,"lx":125.85,"ly":-8.8,"b":[124.036,-9.512,127.296,-8.14],"p":[[[125.646,-8.14,125.579,-8.312,125.507,-8.275,125.584,-8.179,125.621,-8.15,125.646,-8.14]],[[125.068,-9.512,125.034,-9.382,124.997,-9.326,124.968,-9.294,124.959,-9.255,124.96,-9.214,124.978,-9.195,125.1,-9.19,125.149,-9.123,125.149,-9.043,125.124,-9.015,125.1,-9.004,124.973,-9.064,124.937,-9.053,124.915,-9.032,124.922,-8.942,125.027,-8.859,125.116,-8.708,125.178,-8.648,125.323,-8.591,125.382,-8.575,125.804,-8.492,125.905,-8.487,126.173,-8.489,126.531,-8.471,126.62,-8.459,126.735,-8.423,126.846,-8.377,126.905,-8.342,126.966,-8.316,127.058,-8.348,127.215,-8.373,127.257,-8.395,127.296,-8.425,127.115,-8.584,126.915,-8.715,126.792,-8.755,126.665,-8.782,126.569,-8.833,126.487,-8.913,126.383,-8.958,126.265,-8.973,126.164,-8.997,126.073,-9.044,125.946,-9.124,125.895,-9.132,125.84,-9.13,125.735,-9.161,125.408,-9.276,125.21,-9.404,125.068,-9.512]],[[124.036,-9.342,124.198,-9.256,124.444,-9.19,124.438,-9.239,124.413,-9.314,124.376,-9.35,124.319,-9.414,124.282,-9.428,124.135,-9.414,124.116,-9.423,124.09,-9.416,124.052,-9.375,124.036,-9.342]]]},{"n":"Thailand","c":"TH","r":3,"ml":2.7,"lx":101.07,"ly":15.46,"b":[97.374,5.637,105.641,20.424],"p":[[[102.606,11.677,102.59,11.572,102.533,11.615,102.546,11.668,102.569,11.692,102.606,11.677]],[[102.427,11.989,102.43,11.965,102.378,11.983,102.36,11.974,102.302,11.981,102.273,12.119,102.277,12.152,102.319,12.142,102.378,12.073,102.408,12.025,102.427,11.989]],[[98.409,7.902,98.398,7.828,98.357,7.829,98.316,7.782,98.296,7.776,98.262,7.926,98.301,8.136,98.322,8.166,98.351,8.111,98.435,8.086,98.399,7.965,98.409,7.902]],[[100.122,20.317,100.115,20.258,100.14,20.245,100.174,20.273,100.218,20.34,100.266,20.377,100.318,20.386,100.373,20.34,100.432,20.241,100.492,20.184,100.52,20.178,100.54,20.132,100.543,20.089,100.515,19.996,100.466,19.889,100.398,19.756,100.42,19.644,100.514,19.553,100.625,19.5,100.744,19.515,100.807,19.542,100.858,19.585,100.906,19.605,100.967,19.611,101.155,19.579,101.212,19.548,101.221,19.487,101.198,19.328,101.227,19.212,101.28,19.089,101.286,18.977,101.221,18.793,101.166,18.618,101.106,18.534,101.06,18.479,101.047,18.442,101.051,18.407,101.093,18.355,101.138,18.287,101.149,18.222,101.144,18.143,101.113,18.034,100.999,17.797,100.908,17.584,100.956,17.541,101.046,17.51,101.105,17.48,101.167,17.499,101.3,17.625,101.414,17.719,101.555,17.812,101.564,17.821,101.688,17.889,101.744,17.953,101.775,18.033,101.819,18.065,101.875,18.046,101.947,18.081,102.035,18.17,102.101,18.211,102.148,18.204,102.232,18.149,102.352,18.046,102.459,17.985,102.553,17.965,102.598,17.927,102.596,17.87,102.617,17.833,102.661,17.818,102.68,17.824,102.675,17.852,102.718,17.892,102.807,17.946,102.899,17.977,102.991,17.986,103.051,18.029,103.091,18.138,103.149,18.222,103.2,18.259,103.263,18.278,103.28,18.305,103.249,18.339,103.252,18.373,103.288,18.408,103.367,18.423,103.488,18.418,103.63,18.383,103.792,18.317,103.899,18.295,103.95,18.319,104.049,18.217,104.196,17.988,104.323,17.816,104.428,17.699,104.539,17.609,104.656,17.547,104.74,17.462,104.816,17.3,104.759,17.077,104.744,16.884,104.751,16.648,104.819,16.466,104.95,16.34,105.026,16.238,105.047,16.16,105.149,16.094,105.331,16.038,105.406,15.987,105.376,15.942,105.373,15.89,105.399,15.83,105.462,15.78,105.562,15.741,105.622,15.7,105.641,15.657,105.639,15.586,105.616,15.488,105.574,15.413,105.513,15.361,105.506,15.32,105.49,15.257,105.49,15.128,105.533,15.042,105.547,14.932,105.523,14.843,105.5,14.661,105.497,14.591,105.476,14.53,105.423,14.472,105.342,14.417,105.244,14.368,105.183,14.346,105.169,14.336,105.126,14.281,105.074,14.227,105.034,14.227,105.003,14.254,104.982,14.289,104.97,14.366,104.879,14.404,104.779,14.428,104.576,14.39,104.412,14.37,104.228,14.396,104.054,14.363,103.982,14.358,103.899,14.363,103.818,14.362,103.742,14.374,103.6,14.421,103.546,14.417,103.432,14.379,103.313,14.351,103.199,14.333,103.031,14.253,102.909,14.137,102.873,14.055,102.813,13.972,102.729,13.842,102.62,13.717,102.545,13.66,102.566,13.626,102.547,13.586,102.429,13.568,102.336,13.56,102.32,13.54,102.331,13.288,102.363,13.193,102.423,13.078,102.462,13.015,102.491,12.828,102.5,12.67,102.63,12.57,102.703,12.494,102.756,12.426,102.737,12.383,102.706,12.256,102.737,12.09,102.918,11.732,102.934,11.707,102.912,11.704,102.884,11.773,102.792,11.889,102.763,12.012,102.655,12.149,102.594,12.203,102.575,12.158,102.54,12.109,102.434,12.179,102.343,12.253,102.259,12.394,102.248,12.361,102.23,12.332,102.134,12.443,102.034,12.532,101.945,12.564,101.889,12.593,101.836,12.64,101.724,12.689,101.445,12.619,101.09,12.674,100.954,12.621,100.898,12.654,100.863,12.715,100.896,12.818,100.904,13.035,100.946,13.187,100.926,13.303,100.947,13.358,100.963,13.432,100.907,13.462,100.656,13.521,100.603,13.568,100.536,13.514,100.236,13.484,100.122,13.44,100.017,13.353,99.991,13.243,100.051,13.171,100.09,13.046,99.982,12.771,99.964,12.69,100.006,12.355,99.989,12.171,99.93,12.047,99.837,11.937,99.799,11.749,99.725,11.662,99.627,11.463,99.561,11.215,99.514,11.101,99.487,10.89,99.285,10.569,99.237,10.388,99.165,10.32,99.19,10.266,99.195,10.175,99.169,9.934,99.161,9.734,99.191,9.627,99.288,9.415,99.265,9.353,99.254,9.265,99.335,9.225,99.394,9.214,99.724,9.314,99.836,9.288,99.878,9.195,99.905,9.113,99.961,8.671,99.99,8.589,100.056,8.511,100.129,8.428,100.154,8.443,100.159,8.474,100.163,8.508,100.229,8.425,100.279,8.269,100.454,7.442,100.504,7.337,100.545,7.227,100.439,7.281,100.411,7.464,100.38,7.542,100.343,7.553,100.284,7.552,100.28,7.584,100.324,7.644,100.317,7.716,100.257,7.775,100.158,7.728,100.161,7.599,100.205,7.501,100.371,7.28,100.424,7.188,100.49,7.161,100.586,7.176,100.702,7.082,100.793,6.995,101.018,6.861,101.154,6.875,101.302,6.908,101.401,6.9,101.498,6.865,101.614,6.754,101.799,6.475,102.101,6.242,102.068,6.185,102.055,6.097,101.936,5.979,101.917,5.911,101.874,5.825,101.791,5.779,101.72,5.771,101.678,5.779,101.65,5.796,101.601,5.877,101.577,5.902,101.556,5.908,101.404,5.852,101.257,5.789,101.23,5.734,101.191,5.669,101.148,5.643,101.114,5.637,101.082,5.675,101.025,5.725,100.982,5.771,100.993,5.846,101.076,5.956,101.087,6.034,101.076,6.166,101.054,6.243,101.029,6.245,100.989,6.258,100.874,6.245,100.817,6.332,100.794,6.426,100.754,6.46,100.716,6.481,100.629,6.448,100.564,6.468,100.345,6.55,100.261,6.683,100.217,6.687,100.177,6.672,100.161,6.642,100.138,6.489,100.119,6.442,99.869,6.75,99.696,6.877,99.72,7.106,99.668,7.151,99.602,7.155,99.553,7.219,99.597,7.356,99.529,7.329,99.435,7.334,99.359,7.372,99.3,7.561,99.264,7.619,99.183,7.718,99.078,7.718,99.043,7.766,99.051,7.888,98.974,7.963,98.872,8.024,98.789,8.06,98.704,8.257,98.636,8.305,98.579,8.344,98.5,8.318,98.474,8.247,98.421,8.178,98.361,8.187,98.305,8.226,98.238,8.423,98.227,8.544,98.242,8.768,98.326,8.969,98.371,9.291,98.443,9.493,98.493,9.561,98.562,9.838,98.703,10.19,98.718,10.266,98.747,10.351,98.768,10.431,98.775,10.557,98.757,10.624,98.757,10.661,98.787,10.708,98.887,10.788,99.025,10.92,99.19,11.105,99.359,11.389,99.443,11.554,99.478,11.613,99.515,11.631,99.573,11.687,99.613,11.75,99.615,11.781,99.523,12.09,99.463,12.19,99.432,12.309,99.416,12.395,99.394,12.474,99.405,12.548,99.372,12.594,99.297,12.653,99.22,12.74,99.174,12.882,99.174,12.961,99.124,13.031,99.107,13.104,99.137,13.173,99.176,13.233,99.172,13.497,99.156,13.576,99.137,13.717,99.086,13.823,99.015,13.947,98.934,14.05,98.721,14.236,98.57,14.36,98.495,14.473,98.4,14.603,98.332,14.696,98.246,14.815,98.202,14.976,98.178,15.147,98.191,15.204,98.232,15.241,98.286,15.272,98.329,15.279,98.452,15.357,98.537,15.351,98.557,15.368,98.565,15.404,98.554,15.56,98.558,15.769,98.574,15.939,98.592,16.051,98.818,16.181,98.866,16.237,98.888,16.298,98.888,16.352,98.869,16.394,98.835,16.418,98.689,16.305,98.661,16.33,98.594,16.515,98.565,16.571,98.523,16.638,98.478,16.732,98.471,16.895,98.439,16.976,98.257,17.148,98.175,17.24,98.063,17.373,97.929,17.533,97.793,17.681,97.729,17.776,97.706,17.797,97.699,17.834,97.74,17.935,97.72,18.037,97.652,18.174,97.622,18.258,97.632,18.29,97.599,18.303,97.524,18.296,97.451,18.36,97.381,18.494,97.374,18.518,97.397,18.518,97.485,18.494,97.515,18.498,97.577,18.529,97.672,18.561,97.728,18.572,97.746,18.588,97.754,18.621,97.706,18.932,97.714,18.996,97.804,19.13,97.794,19.266,97.817,19.46,97.916,19.593,97.991,19.654,98.015,19.75,98.049,19.77,98.111,19.762,98.239,19.691,98.294,19.687,98.371,19.689,98.455,19.694,98.494,19.701,98.761,19.771,98.82,19.778,98.876,19.77,98.917,19.773,98.958,19.805,98.987,19.861,99.021,20.042,99.04,20.074,99.074,20.099,99.131,20.117,99.197,20.115,99.284,20.08,99.338,20.079,99.399,20.093,99.452,20.118,99.486,20.15,99.502,20.188,99.488,20.261,99.448,20.352,99.459,20.363,99.532,20.343,99.639,20.32,99.72,20.325,99.773,20.341,99.825,20.384,99.89,20.424,99.954,20.415,100.004,20.38,100.122,20.317]],[[100.074,9.697,100.064,9.68,100.026,9.712,99.998,9.748,99.983,9.794,100.043,9.792,100.073,9.749,100.074,9.697]],[[100.071,9.586,100.075,9.529,100.054,9.461,99.962,9.422,99.931,9.476,99.94,9.56,99.954,9.581,100.043,9.577,100.071,9.586]],[[99.663,6.522,99.644,6.516,99.607,6.597,99.654,6.714,99.701,6.571,99.663,6.522]],[[98.592,7.934,98.58,7.917,98.529,8.109,98.604,8.057,98.592,7.934]],[[98.308,9.051,98.251,9.041,98.258,9.095,98.274,9.13,98.301,9.139,98.313,9.08,98.308,9.051]],[[99.078,7.592,99.104,7.471,99.068,7.496,99.038,7.548,99.038,7.626,99.045,7.637,99.078,7.592]]]},{"n":"Tanzania","c":"TZ","r":3,"ml":3,"lx":34.96,"ly":-6.05,"b":[29.323,-11.716,40.464,-0.995],"p":[[[39.496,-6.175,39.573,-6.387,39.563,-6.427,39.509,-6.452,39.481,-6.454,39.447,-6.42,39.424,-6.348,39.383,-6.365,39.313,-6.279,39.243,-6.275,39.182,-6.173,39.206,-6.083,39.192,-5.931,39.267,-5.853,39.309,-5.722,39.357,-5.812,39.368,-5.951,39.433,-6.115,39.488,-6.166,39.496,-6.175]],[[39.865,-4.906,39.871,-4.957,39.856,-5.004,39.859,-5.155,39.853,-5.255,39.796,-5.394,39.749,-5.444,39.708,-5.429,39.673,-5.407,39.647,-5.369,39.701,-5.114,39.673,-4.927,39.781,-4.945,39.865,-4.906]],[[32.92,-9.407,32.863,-9.381,32.757,-9.322,32.608,-9.271,32.487,-9.213,32.433,-9.156,32.319,-9.135,32.221,-9.126,32.13,-9.073,32.035,-9.067,31.943,-9.054,31.922,-9.019,31.919,-8.942,31.886,-8.922,31.818,-8.902,31.745,-8.903,31.7,-8.914,31.674,-8.909,31.613,-8.863,31.556,-8.805,31.535,-8.713,31.449,-8.654,31.351,-8.607,31.076,-8.612,31.033,-8.598,30.968,-8.551,30.892,-8.474,30.831,-8.386,30.777,-8.266,30.751,-8.194,30.721,-8.104,30.654,-7.971,30.559,-7.782,30.486,-7.627,30.407,-7.461,30.375,-7.339,30.313,-7.204,30.213,-7.038,30.162,-6.973,30.106,-6.915,29.962,-6.803,29.798,-6.692,29.71,-6.617,29.591,-6.394,29.541,-6.314,29.506,-6.172,29.48,-6.025,29.491,-5.965,29.596,-5.776,29.607,-5.723,29.594,-5.651,29.542,-5.5,29.504,-5.401,29.476,-5.317,29.42,-5.176,29.343,-4.983,29.323,-4.899,29.326,-4.836,29.368,-4.669,29.404,-4.497,29.403,-4.449,29.718,-4.456,29.77,-4.418,29.947,-4.307,30.147,-4.085,30.187,-3.993,30.269,-3.85,30.348,-3.78,30.379,-3.731,30.4,-3.654,30.425,-3.589,30.53,-3.492,30.632,-3.419,30.625,-3.389,30.611,-3.366,30.626,-3.347,30.682,-3.309,30.79,-3.275,30.811,-3.201,30.811,-3.116,30.794,-3.069,30.797,-3.015,30.78,-2.985,30.709,-2.977,30.604,-2.935,30.515,-2.918,30.456,-2.893,30.433,-2.875,30.424,-2.824,30.441,-2.769,30.45,-2.753,30.473,-2.694,30.434,-2.659,30.424,-2.642,30.442,-2.613,30.534,-2.426,30.554,-2.4,30.593,-2.397,30.657,-2.374,30.715,-2.363,30.763,-2.372,30.798,-2.363,30.829,-2.338,30.855,-2.265,30.877,-2.143,30.865,-2.044,30.819,-1.967,30.807,-1.851,30.828,-1.694,30.813,-1.563,30.762,-1.459,30.711,-1.397,30.632,-1.367,30.508,-1.208,30.47,-1.131,30.477,-1.083,30.51,-1.067,30.52,-1.062,30.599,-1.07,30.673,-1.051,30.742,-1.008,30.809,-0.995,30.824,-0.999,30.845,-1.002,30.95,-1.002,31.128,-1.002,31.305,-1.002,31.483,-1.002,31.661,-1.002,31.839,-1.002,32.016,-1.002,32.194,-1.002,32.372,-1.002,32.55,-1.002,32.727,-1.002,32.905,-1.002,33.083,-1.002,33.261,-1.002,33.438,-1.002,33.616,-1.002,33.794,-1.002,33.903,-1.002,33.979,-1.002,34.052,-1.04,34.132,-1.085,34.345,-1.204,34.558,-1.323,34.771,-1.442,34.984,-1.561,35.197,-1.68,35.411,-1.799,35.624,-1.918,35.837,-2.037,36.05,-2.156,36.263,-2.275,36.476,-2.394,36.689,-2.513,36.903,-2.632,37.116,-2.751,37.329,-2.87,37.542,-2.989,37.644,-3.045,37.659,-3.07,37.677,-3.178,37.688,-3.246,37.682,-3.306,37.625,-3.407,37.609,-3.46,37.608,-3.497,37.622,-3.512,37.67,-3.517,37.711,-3.541,37.726,-3.56,37.757,-3.636,37.797,-3.674,37.887,-3.739,38.041,-3.85,38.194,-3.96,38.348,-4.071,38.501,-4.181,38.655,-4.292,38.808,-4.402,38.962,-4.513,39.115,-4.624,39.19,-4.677,39.222,-4.692,39.202,-4.776,39.123,-4.98,39.119,-5.065,39.088,-5.165,39.058,-5.232,38.978,-5.519,38.911,-5.626,38.819,-5.878,38.805,-6.07,38.855,-6.205,38.874,-6.331,38.981,-6.455,39.067,-6.499,39.125,-6.556,39.228,-6.685,39.287,-6.815,39.472,-6.879,39.546,-7.024,39.519,-7.124,39.433,-7.207,39.353,-7.341,39.288,-7.518,39.287,-7.788,39.33,-7.747,39.428,-7.813,39.441,-8.012,39.34,-8.243,39.309,-8.351,39.304,-8.444,39.377,-8.721,39.488,-8.862,39.48,-8.906,39.451,-8.943,39.641,-9.192,39.625,-9.409,39.697,-9.578,39.728,-9.725,39.775,-9.837,39.784,-9.915,39.725,-10,39.864,-10.022,39.945,-10.092,39.984,-10.16,40.084,-10.157,40.138,-10.203,40.216,-10.241,40.389,-10.354,40.436,-10.41,40.453,-10.443,40.464,-10.464,40.347,-10.552,40.166,-10.687,39.989,-10.821,39.817,-10.912,39.694,-10.955,39.563,-10.979,39.439,-11.035,39.322,-11.123,39.171,-11.167,38.988,-11.167,38.795,-11.229,38.603,-11.345,38.492,-11.413,38.315,-11.311,38.177,-11.279,38.017,-11.282,37.92,-11.295,37.885,-11.317,37.855,-11.379,37.829,-11.482,37.725,-11.581,37.542,-11.675,37.373,-11.71,37.218,-11.687,37.114,-11.647,37.059,-11.592,36.979,-11.567,36.873,-11.571,36.771,-11.61,36.674,-11.684,36.519,-11.716,36.306,-11.706,36.191,-11.671,36.175,-11.609,36.082,-11.537,35.911,-11.455,35.785,-11.453,35.705,-11.532,35.631,-11.582,35.564,-11.602,35.504,-11.605,35.451,-11.59,35.418,-11.583,35.183,-11.575,34.959,-11.578,34.953,-11.544,34.937,-11.463,34.891,-11.394,34.851,-11.352,34.801,-11.341,34.774,-11.342,34.752,-11.309,34.726,-11.238,34.688,-11.177,34.638,-11.127,34.608,-11.08,34.598,-11.037,34.606,-10.99,34.652,-10.873,34.667,-10.792,34.662,-10.71,34.637,-10.626,34.584,-10.525,34.59,-10.496,34.572,-10.428,34.57,-10.38,34.58,-10.32,34.57,-10.241,34.524,-10.073,34.524,-10.03,34.476,-9.949,34.328,-9.757,34.321,-9.732,34.089,-9.538,33.996,-9.495,33.962,-9.532,33.95,-9.565,33.959,-9.627,33.954,-9.658,33.944,-9.672,33.889,-9.67,33.854,-9.663,33.766,-9.611,33.698,-9.598,33.528,-9.608,33.468,-9.62,33.421,-9.608,33.331,-9.519,33.225,-9.5,33.13,-9.496,32.974,-9.395,32.937,-9.4,32.92,-9.407]],[[39.711,-7.977,39.657,-7.991,39.636,-7.978,39.603,-7.936,39.661,-7.901,39.717,-7.832,39.847,-7.73,39.891,-7.663,39.907,-7.649,39.898,-7.728,39.824,-7.901,39.762,-7.912,39.711,-7.977]]]},{"n":"Tajikistan","c":"TJ","r":4,"ml":4,"lx":72.59,"ly":38.2,"b":[67.35,36.684,75.119,41.035],"p":[[[67.759,37.172,67.798,37.245,67.814,37.487,67.864,37.571,68.011,37.721,68.088,37.835,68.174,37.928,68.237,37.96,68.294,38.033,68.341,38.117,68.354,38.17,68.35,38.211,68.333,38.238,68.251,38.295,68.144,38.383,68.087,38.474,68.056,38.589,68.048,38.669,68.149,38.891,68.133,38.928,68.104,38.962,68.044,38.984,67.96,38.993,67.876,38.983,67.769,38.982,67.694,38.995,67.677,39.008,67.667,39.109,67.648,39.131,67.617,39.15,67.4,39.197,67.358,39.217,67.35,39.242,67.426,39.466,67.46,39.482,67.492,39.519,67.542,39.558,67.719,39.621,67.909,39.594,68.077,39.564,68.245,39.548,68.303,39.538,68.399,39.529,68.463,39.537,68.507,39.563,68.586,39.635,68.61,39.743,68.639,39.839,68.687,39.846,68.735,39.836,68.758,39.856,68.768,39.882,68.778,39.904,68.798,39.909,68.832,39.884,68.852,39.891,68.869,39.907,68.864,39.927,68.824,39.961,68.789,40.013,68.793,40.031,68.805,40.05,68.908,40.068,68.956,40.071,68.972,40.09,68.966,40.12,68.927,40.136,68.785,40.127,68.64,40.129,68.622,40.147,68.631,40.167,68.653,40.183,68.952,40.223,69.11,40.209,69.228,40.188,69.275,40.198,69.22,40.288,69.294,40.297,69.304,40.327,69.206,40.567,69.26,40.588,69.314,40.635,69.309,40.724,69.357,40.767,69.414,40.797,69.498,40.767,69.628,40.679,69.671,40.662,69.713,40.657,69.773,40.684,70.006,40.771,70.136,40.82,70.292,40.892,70.319,40.919,70.373,41.028,70.402,41.035,70.442,41.023,70.578,40.911,70.657,40.84,70.657,40.815,70.635,40.797,70.639,40.779,70.751,40.74,70.751,40.722,70.726,40.688,70.712,40.669,70.698,40.661,70.549,40.563,70.383,40.454,70.377,40.439,70.37,40.412,70.372,40.384,70.398,40.361,70.47,40.345,70.534,40.325,70.566,40.267,70.603,40.214,70.653,40.201,70.899,40.235,70.958,40.239,70.961,40.221,70.946,40.188,70.739,40.131,70.644,40.083,70.624,39.999,70.599,39.975,70.557,39.954,70.515,39.95,70.451,40.049,70.379,40.07,70.274,40.105,70.071,40.173,69.967,40.202,69.765,40.158,69.53,40.097,69.494,40.06,69.469,40.021,69.471,39.991,69.488,39.95,69.476,39.92,69.432,39.91,69.365,39.947,69.307,39.969,69.279,39.918,69.245,39.827,69.229,39.761,69.28,39.666,69.298,39.525,69.392,39.532,69.463,39.532,69.599,39.574,69.667,39.575,69.772,39.557,69.956,39.553,70.102,39.561,70.137,39.558,70.171,39.584,70.209,39.575,70.245,39.543,70.392,39.582,70.501,39.587,70.568,39.576,70.608,39.564,70.679,39.471,70.733,39.413,70.799,39.395,71.005,39.412,71.065,39.493,71.118,39.514,71.203,39.52,71.273,39.535,71.329,39.569,71.404,39.598,71.47,39.604,71.503,39.582,71.517,39.554,71.506,39.517,71.503,39.479,71.546,39.453,71.673,39.447,71.732,39.423,71.735,39.378,71.726,39.307,71.779,39.278,71.806,39.276,71.991,39.351,72.043,39.352,72.084,39.311,72.147,39.261,72.23,39.208,72.25,39.216,72.287,39.274,72.358,39.337,72.49,39.357,72.563,39.377,72.64,39.386,72.872,39.36,72.949,39.357,73.109,39.362,73.235,39.375,73.336,39.412,73.387,39.443,73.47,39.461,73.576,39.458,73.632,39.449,73.636,39.397,73.623,39.298,73.607,39.229,73.69,39.105,73.744,39.045,73.796,39.002,73.805,38.969,73.795,38.941,73.73,38.915,73.707,38.886,73.696,38.854,73.717,38.817,73.754,38.699,73.802,38.607,73.869,38.563,73.97,38.534,74.026,38.54,74.065,38.608,74.131,38.661,74.187,38.658,74.277,38.66,74.514,38.6,74.745,38.51,74.812,38.46,74.836,38.404,74.772,38.275,74.775,38.192,74.79,38.104,74.842,38.038,74.891,37.926,74.9,37.833,74.921,37.805,74.938,37.773,74.912,37.687,74.894,37.601,74.916,37.573,74.986,37.53,75.097,37.451,75.119,37.386,75.079,37.344,75.008,37.294,74.918,37.25,74.891,37.232,74.875,37.242,74.83,37.286,74.731,37.357,74.659,37.394,74.524,37.382,74.445,37.396,74.349,37.419,74.26,37.415,74.204,37.372,74.167,37.329,74.078,37.316,73.949,37.283,73.75,37.232,73.654,37.239,73.628,37.262,73.649,37.291,73.717,37.329,73.734,37.376,73.721,37.419,73.657,37.43,73.633,37.437,73.605,37.446,73.481,37.472,73.383,37.462,73.211,37.408,72.896,37.268,72.757,37.173,72.657,37.029,72.359,36.983,72.154,36.901,71.942,36.766,71.802,36.694,71.734,36.684,71.666,36.697,71.597,36.733,71.531,36.845,71.472,37.015,71.433,37.128,71.455,37.272,71.48,37.436,71.505,37.603,71.546,37.796,71.58,37.864,71.582,37.91,71.552,37.933,71.488,37.932,71.39,37.906,71.32,37.902,71.279,37.918,71.283,38.008,71.333,38.17,71.256,38.307,71.052,38.418,70.879,38.456,70.736,38.423,70.616,38.334,70.519,38.192,70.418,38.075,70.313,37.985,70.239,37.941,70.215,37.924,70.199,37.886,70.255,37.765,70.251,37.664,70.189,37.582,70.12,37.544,70.045,37.547,69.985,37.566,69.941,37.6,69.821,37.61,69.626,37.594,69.492,37.553,69.42,37.487,69.399,37.399,69.43,37.291,69.414,37.208,69.354,37.15,69.304,37.117,69.265,37.108,69.18,37.158,69.05,37.267,68.96,37.325,68.912,37.334,68.885,37.328,68.855,37.317,68.838,37.303,68.824,37.271,68.782,37.258,68.723,37.268,68.669,37.258,68.637,37.224,68.546,37.183,68.387,37.138,68.3,37.088,68.285,37.036,68.261,37.013,68.212,37.022,68.068,36.95,67.958,36.972,67.834,37.064,67.766,37.14,67.759,37.172]],[[70.653,40.937,70.623,40.934,70.569,40.982,70.55,41.015,70.572,41.025,70.618,41.002,70.649,40.961,70.653,40.937]],[[70.702,39.825,70.612,39.787,70.56,39.791,70.519,39.828,70.489,39.863,70.483,39.883,70.498,39.882,70.567,39.867,70.664,39.855,70.698,39.846,70.702,39.825]]]},{"n":"Taiwan","c":"TW","r":3,"ml":4.5,"lx":120.87,"ly":23.65,"b":[118.287,21.925,121.929,25.277],"p":[[[121.009,22.62,120.947,22.503,120.897,22.379,120.877,22.262,120.878,22.142,120.864,22.033,120.84,21.925,120.743,21.956,120.69,22.033,120.678,22.16,120.608,22.313,120.581,22.356,120.48,22.442,120.388,22.485,120.316,22.548,120.326,22.542,120.273,22.627,120.233,22.718,120.15,22.975,120.122,23.037,120.083,23.094,120.072,23.15,120.086,23.212,120.121,23.305,120.143,23.399,120.125,23.527,120.132,23.653,120.159,23.709,120.63,24.479,120.757,24.642,120.836,24.723,120.902,24.813,120.964,24.928,121.041,25.033,121.095,25.065,121.365,25.159,121.45,25.249,121.517,25.277,121.594,25.275,121.643,25.232,121.687,25.182,121.733,25.154,121.853,25.104,121.905,25.056,121.929,24.974,121.856,24.895,121.82,24.825,121.813,24.746,121.826,24.641,121.828,24.534,121.737,24.285,121.639,24.13,121.613,24.053,121.583,23.861,121.526,23.668,121.477,23.424,121.397,23.173,121.352,23.067,121.296,22.967,121.161,22.776,121.009,22.62]],[[118.407,24.522,118.451,24.456,118.433,24.414,118.295,24.436,118.287,24.477,118.339,24.469,118.407,24.522]]]},{"n":"Syria","c":"SY","r":3,"ml":3,"lx":38.28,"ly":35.01,"b":[35.764,32.317,42.359,37.297],"p":[[[35.893,35.917,35.968,35.91,36.127,35.831,36.154,35.834,36.202,35.938,36.249,35.973,36.348,36.004,36.375,36.171,36.421,36.203,36.477,36.221,36.562,36.224,36.637,36.234,36.641,36.264,36.538,36.457,36.547,36.506,36.597,36.701,36.628,36.778,36.659,36.803,36.777,36.793,36.942,36.758,36.985,36.702,37.066,36.653,37.187,36.656,37.327,36.647,37.436,36.643,37.524,36.678,37.72,36.744,37.818,36.766,37.907,36.795,38.192,36.902,38.306,36.893,38.384,36.879,38.444,36.862,38.578,36.789,38.689,36.715,38.767,36.693,38.906,36.695,39.108,36.681,39.357,36.682,39.501,36.702,39.687,36.739,40.016,36.826,40.45,37.009,40.706,37.098,40.816,37.108,40.959,37.109,41.102,37.086,41.265,37.069,41.34,37.071,41.516,37.089,41.744,37.126,41.887,37.156,42.06,37.206,42.168,37.289,42.203,37.297,42.248,37.282,42.269,37.277,42.313,37.23,42.359,37.109,42.359,37.095,42.35,37.061,42.237,36.961,42.084,36.826,41.974,36.741,41.789,36.597,41.65,36.566,41.417,36.515,41.354,36.464,41.296,36.383,41.262,36.272,41.252,36.203,41.246,36.073,41.3,35.939,41.353,35.81,41.359,35.725,41.354,35.64,41.303,35.551,41.248,35.427,41.216,35.288,41.2,35.027,41.199,34.805,41.195,34.769,41.099,34.612,40.987,34.429,40.935,34.387,40.689,34.332,40.421,34.198,40.122,34.048,39.85,33.911,39.564,33.768,39.268,33.62,39.057,33.514,38.774,33.372,38.516,33.237,38.254,33.099,38.056,32.995,37.754,32.83,37.577,32.733,37.318,32.591,37.089,32.466,36.818,32.317,36.479,32.361,36.372,32.387,36.284,32.457,36.22,32.495,36.059,32.534,35.956,32.667,35.895,32.714,35.787,32.735,35.833,32.82,35.842,32.876,35.888,32.944,35.859,32.989,35.845,33.085,35.825,33.108,35.817,33.133,35.833,33.164,35.827,33.195,35.809,33.209,35.8,33.256,35.785,33.291,35.793,33.334,35.816,33.362,35.841,33.416,35.869,33.432,35.915,33.465,35.927,33.5,35.968,33.535,36.022,33.563,36.034,33.585,36.027,33.598,35.972,33.623,35.942,33.668,35.968,33.732,35.986,33.753,36.019,33.784,36.092,33.832,36.15,33.84,36.199,33.84,36.283,33.836,36.349,33.827,36.365,33.839,36.363,33.855,36.282,33.894,36.278,33.925,36.298,33.959,36.355,34.011,36.423,34.05,36.458,34.057,36.535,34.134,36.585,34.221,36.504,34.432,36.456,34.466,36.376,34.495,36.33,34.5,36.326,34.513,36.389,34.567,36.433,34.613,36.384,34.658,36.296,34.679,36.264,34.633,36.151,34.629,35.976,34.629,35.899,34.852,35.888,34.949,35.89,35.06,35.943,35.224,35.918,35.3,35.916,35.351,35.902,35.421,35.764,35.572,35.84,35.849,35.893,35.917]]]},{"n":"Switzerland","c":"CH","r":4,"ml":4,"lx":7.46,"ly":46.72,"b":[5.97,45.83,10.455,47.776],"p":[[[9.524,47.524,9.554,47.511,9.626,47.467,9.609,47.392,9.528,47.271,9.484,47.173,9.479,47.098,9.488,47.062,9.502,47.063,9.58,47.057,9.62,47.057,9.745,47.037,9.845,47.007,9.865,46.976,9.878,46.938,9.997,46.885,10.133,46.852,10.18,46.862,10.349,46.985,10.415,46.964,10.455,46.899,10.453,46.865,10.406,46.735,10.398,46.665,10.438,46.619,10.442,46.583,10.431,46.55,10.363,46.547,10.272,46.565,10.196,46.621,10.138,46.614,10.087,46.6,10.061,46.547,10.038,46.483,10.046,46.448,10.082,46.421,10.11,46.363,10.13,46.288,10.145,46.254,10.128,46.238,10.081,46.228,10.041,46.238,9.972,46.328,9.939,46.362,9.884,46.368,9.788,46.346,9.639,46.296,9.58,46.296,9.529,46.306,9.481,46.349,9.441,46.431,9.428,46.482,9.399,46.481,9.304,46.496,9.26,46.475,9.26,46.391,9.251,46.287,9.203,46.219,9.071,46.102,9.022,46.051,9.003,46.015,8.999,45.983,9.019,45.928,9.047,45.876,9.024,45.846,8.954,45.83,8.904,45.862,8.885,45.919,8.778,45.996,8.827,46.061,8.819,46.077,8.642,46.111,8.565,46.16,8.458,46.246,8.438,46.283,8.443,46.403,8.437,46.432,8.423,46.446,8.371,46.445,8.299,46.403,8.232,46.341,8.096,46.271,8.082,46.256,8.127,46.188,8.125,46.161,8.014,46.052,7.993,46.016,7.852,45.947,7.788,45.922,7.593,45.972,7.539,45.978,7.452,45.944,7.328,45.912,7.129,45.88,7.056,45.904,7.021,45.926,7.004,45.959,6.954,46.017,6.897,46.052,6.858,46.089,6.806,46.131,6.772,46.165,6.817,46.275,6.784,46.314,6.767,46.369,6.776,46.407,6.758,46.416,6.578,46.437,6.429,46.431,6.322,46.394,6.235,46.333,6.224,46.319,6.23,46.308,6.273,46.252,6.199,46.193,6.087,46.147,6.007,46.142,5.971,46.151,5.97,46.215,6.036,46.238,6.096,46.279,6.116,46.338,6.123,46.379,6.06,46.428,6.068,46.459,6.107,46.516,6.13,46.567,6.161,46.611,6.285,46.683,6.41,46.755,6.429,46.832,6.439,46.926,6.456,46.948,6.625,47.004,6.667,47.027,6.688,47.058,6.821,47.163,6.952,47.267,6.979,47.302,7.001,47.323,7.001,47.339,6.984,47.353,6.921,47.361,6.9,47.394,6.968,47.453,7.053,47.489,7.136,47.49,7.169,47.473,7.167,47.454,7.203,47.433,7.266,47.426,7.343,47.433,7.42,47.455,7.467,47.508,7.495,47.547,7.616,47.593,7.698,47.57,7.927,47.564,8.094,47.576,8.198,47.607,8.328,47.607,8.415,47.59,8.43,47.592,8.454,47.596,8.478,47.613,8.559,47.624,8.571,47.638,8.567,47.652,8.552,47.659,8.452,47.652,8.413,47.663,8.403,47.688,8.436,47.731,8.51,47.767,8.573,47.776,8.618,47.766,8.728,47.7,8.755,47.698,8.77,47.71,8.793,47.717,8.831,47.704,8.874,47.663,8.881,47.656,9.128,47.671,9.183,47.671,9.35,47.599,9.524,47.524]]]},{"n":"Sweden","c":"SE","r":3,"ml":2,"lx":19.02,"ly":65.86,"b":[11.147,55.346,24.155,69.037],"p":[[[19.076,57.836,18.994,57.812,18.945,57.742,18.878,57.73,18.814,57.706,18.791,57.483,18.908,57.398,18.844,57.386,18.785,57.361,18.743,57.324,18.7,57.243,18.538,57.197,18.477,57.163,18.387,57.088,18.34,56.978,18.249,56.932,18.146,56.921,18.207,57.01,18.285,57.083,18.21,57.133,18.164,57.212,18.105,57.272,18.152,57.339,18.129,57.449,18.137,57.557,18.205,57.611,18.283,57.655,18.405,57.757,18.537,57.831,18.722,57.864,18.805,57.833,18.841,57.9,18.901,57.915,18.956,57.9,19.076,57.836]],[[16.529,56.291,16.477,56.24,16.432,56.244,16.401,56.311,16.394,56.484,16.412,56.569,16.63,56.877,16.728,56.902,16.865,57.091,16.902,57.175,16.961,57.25,16.996,57.318,17.025,57.345,17.089,57.332,17.118,57.32,17.05,57.28,17.058,57.229,17.054,57.208,16.884,56.985,16.838,56.841,16.778,56.805,16.529,56.291]],[[11.388,59.037,11.471,58.91,11.544,58.893,11.643,58.926,11.712,59.019,11.752,59.158,11.798,59.29,11.743,59.431,11.685,59.556,11.681,59.592,11.834,59.697,11.881,59.782,11.932,59.864,11.988,59.891,12.072,59.898,12.169,59.913,12.292,59.967,12.402,60.04,12.486,60.107,12.515,60.239,12.516,60.305,12.553,60.354,12.589,60.451,12.554,60.546,12.445,60.69,12.315,60.892,12.294,61.003,12.354,61.023,12.468,61.042,12.683,61.047,12.706,61.06,12.728,61.108,12.776,61.174,12.828,61.222,12.864,61.29,12.881,61.352,12.758,61.446,12.596,61.541,12.487,61.573,12.292,61.653,12.155,61.721,12.234,61.977,12.292,62.167,12.301,62.214,12.304,62.286,12.115,62.592,12.122,62.66,12.14,62.721,12.12,62.826,12.109,62.919,12.141,62.948,12.218,63.001,12.145,63.083,12.139,63.089,12,63.292,12.212,63.492,12.175,63.596,12.302,63.671,12.533,63.844,12.663,63.94,12.69,63.957,12.793,64,12.988,64.05,13.204,64.075,13.3,64.075,13.671,64.041,13.961,64.014,14.003,64.041,14.063,64.096,14.141,64.174,14.148,64.26,14.12,64.388,14.078,64.464,13.874,64.514,13.65,64.582,13.925,64.797,14.115,64.946,14.352,65.171,14.426,65.264,14.48,65.301,14.55,65.646,14.596,65.743,14.635,65.793,14.635,65.845,14.61,65.932,14.543,66.129,14.918,66.154,15.04,66.168,15.153,66.191,15.375,66.252,15.484,66.306,15.423,66.49,15.557,66.552,15.884,66.769,16.238,66.976,16.404,67.055,16.421,67.093,16.434,67.155,16.361,67.252,16.282,67.312,16.127,67.426,16.194,67.505,16.307,67.521,16.457,67.552,16.574,67.62,16.586,67.628,16.784,67.895,17.171,68.03,17.325,68.104,17.565,68.048,17.917,67.965,18.073,68.088,18.125,68.133,18.177,68.201,18.156,68.317,18.147,68.468,18.163,68.528,18.303,68.555,18.379,68.562,18.77,68.5,18.868,68.501,19.053,68.493,19.259,68.465,19.691,68.392,19.87,68.362,19.97,68.356,20.056,68.39,20.24,68.478,19.969,68.542,20.147,68.607,20.24,68.673,20.319,68.754,20.348,68.849,20.337,68.9,20.282,68.934,20.117,69.021,20.492,69.033,20.622,69.037,20.895,68.98,20.907,68.967,20.909,68.938,20.919,68.907,21.183,68.829,21.26,68.787,21.422,68.725,21.465,68.691,21.616,68.651,21.724,68.609,21.85,68.574,21.997,68.521,22.195,68.478,22.362,68.464,22.782,68.391,22.854,68.367,22.975,68.316,23.098,68.258,23.183,68.137,23.319,68.13,23.355,68.089,23.474,68.017,23.639,67.954,23.633,67.933,23.502,67.875,23.488,67.797,23.5,67.696,23.541,67.614,23.537,67.59,23.504,67.562,23.465,67.518,23.451,67.479,23.455,67.46,23.468,67.45,23.537,67.449,23.661,67.44,23.734,67.423,23.775,67.329,23.761,67.31,23.657,67.268,23.626,67.234,23.623,67.184,23.642,67.129,23.677,67.068,23.759,67.003,23.869,66.934,23.942,66.878,23.976,66.838,23.989,66.811,23.939,66.776,23.894,66.707,23.886,66.628,23.866,66.577,23.768,66.506,23.701,66.481,23.682,66.443,23.674,66.381,23.694,66.304,23.7,66.253,23.721,66.215,23.751,66.191,23.907,66.148,23.995,66.06,24.049,65.99,24.155,65.805,23.891,65.782,23.691,65.829,23.592,65.805,23.418,65.804,23.221,65.786,23.155,65.75,23.102,65.735,22.919,65.786,22.747,65.871,22.62,65.807,22.539,65.794,22.465,65.853,22.401,65.862,22.366,65.843,22.336,65.791,22.288,65.751,22.275,65.725,22.267,65.622,22.254,65.598,22.086,65.611,22.096,65.584,22.133,65.57,22.148,65.553,22.087,65.53,21.92,65.532,21.903,65.508,21.95,65.47,21.913,65.437,21.88,65.424,21.681,65.403,21.566,65.408,21.533,65.387,21.523,65.359,21.545,65.331,21.596,65.317,21.613,65.299,21.609,65.261,21.567,65.255,21.447,65.321,21.41,65.317,21.438,65.283,21.506,65.245,21.546,65.207,21.581,65.161,21.574,65.126,21.425,65.013,21.294,64.941,21.196,64.877,21.138,64.809,21.205,64.774,21.279,64.725,21.332,64.629,21.394,64.544,21.52,64.463,21.494,64.416,21.465,64.38,21.256,64.299,21.018,64.178,20.763,63.868,20.678,63.826,20.454,63.774,20.371,63.723,20.205,63.662,19.914,63.611,19.782,63.538,19.722,63.463,19.656,63.458,19.59,63.487,19.502,63.509,19.491,63.46,19.495,63.424,19.354,63.477,19.288,63.429,19.236,63.347,19.034,63.238,18.817,63.257,18.792,63.238,18.85,63.224,18.859,63.207,18.819,63.197,18.76,63.198,18.667,63.177,18.606,63.178,18.578,63.126,18.531,63.064,18.408,63.038,18.344,63.032,18.313,62.996,18.502,62.989,18.487,62.959,18.483,62.928,18.463,62.896,18.248,62.849,18.215,62.812,18.17,62.789,18.074,62.791,18.078,62.812,18.094,62.836,17.951,62.834,17.907,62.887,17.88,62.873,17.896,62.831,17.933,62.786,17.974,62.721,17.941,62.68,17.903,62.659,17.93,62.641,18.007,62.626,18.037,62.601,17.947,62.578,17.834,62.503,17.718,62.501,17.646,62.451,17.571,62.451,17.509,62.483,17.41,62.508,17.378,62.463,17.373,62.427,17.429,62.335,17.535,62.264,17.634,62.233,17.563,62.212,17.51,62.166,17.447,62.023,17.412,61.966,17.375,61.866,17.398,61.782,17.417,61.741,17.465,61.684,17.335,61.692,17.196,61.725,17.216,61.656,17.131,61.576,17.147,61.505,17.164,61.458,17.138,61.382,17.178,61.358,17.2,61.312,17.164,61.278,17.18,61.249,17.186,61.147,17.213,60.986,17.203,60.952,17.279,60.812,17.261,60.763,17.251,60.701,17.36,60.641,17.457,60.642,17.555,60.643,17.593,60.628,17.631,60.585,17.661,60.535,17.742,60.539,17.872,60.58,17.956,60.59,18.011,60.511,18.163,60.408,18.25,60.362,18.4,60.337,18.558,60.254,18.535,60.153,18.601,60.119,18.787,60.079,18.853,60.026,18.884,59.98,18.933,59.942,18.99,59.828,18.971,59.757,18.896,59.733,18.719,59.657,18.64,59.601,18.578,59.566,18.402,59.49,18.338,59.477,18.276,59.438,18.217,59.421,18.164,59.43,17.964,59.359,17.98,59.329,18.133,59.316,18.211,59.331,18.271,59.367,18.336,59.375,18.396,59.369,18.459,59.397,18.509,59.408,18.56,59.394,18.618,59.327,18.499,59.292,18.414,59.29,18.373,59.18,18.322,59.132,18.285,59.109,18.098,59.062,17.975,59.003,17.829,58.955,17.765,58.965,17.67,58.916,17.457,58.858,17.348,58.781,17.103,58.711,16.978,58.654,16.639,58.651,16.316,58.664,16.214,58.637,16.318,58.628,16.391,58.602,16.478,58.613,16.683,58.6,16.788,58.585,16.924,58.493,16.824,58.46,16.652,58.434,16.717,58.303,16.77,58.214,16.7,58.161,16.695,57.918,16.597,57.913,16.555,57.812,16.586,57.761,16.584,57.642,16.604,57.568,16.652,57.501,16.631,57.43,16.476,57.265,16.479,57.188,16.507,57.142,16.528,57.068,16.458,56.927,16.408,56.809,16.349,56.709,16.217,56.59,16.151,56.501,15.997,56.223,15.92,56.167,15.827,56.125,15.722,56.164,15.627,56.186,15.51,56.183,15.327,56.151,15.051,56.172,14.782,56.162,14.714,56.134,14.755,56.033,14.656,56.02,14.559,56.049,14.473,56.014,14.402,55.977,14.262,55.888,14.215,55.833,14.203,55.729,14.276,55.636,14.342,55.528,14.174,55.397,14.08,55.392,13.806,55.429,13.321,55.346,12.886,55.411,12.941,55.482,12.939,55.533,12.963,55.613,12.978,55.694,12.974,55.748,12.942,55.806,12.835,55.882,12.593,56.138,12.521,56.246,12.471,56.291,12.507,56.293,12.706,56.235,12.753,56.242,12.802,56.264,12.742,56.347,12.691,56.384,12.656,56.441,12.773,56.456,12.857,56.452,12.92,56.516,12.884,56.618,12.793,56.649,12.718,56.663,12.573,56.823,12.421,56.906,12.152,57.227,12.053,57.447,11.962,57.426,11.917,57.522,11.885,57.613,11.879,57.679,11.735,57.718,11.729,57.764,11.703,57.973,11.549,58.001,11.449,58.118,11.432,58.34,11.33,58.38,11.248,58.369,11.252,58.424,11.272,58.476,11.224,58.68,11.208,58.866,11.169,58.923,11.147,58.989,11.167,59.046,11.196,59.078,11.295,59.087,11.388,59.037]],[[19.156,57.923,19.138,57.86,19.087,57.865,19.039,57.911,19.135,57.981,19.281,57.978,19.331,57.963,19.156,57.923]],[[18.416,59.029,18.372,59.02,18.35,59.023,18.377,59.069,18.398,59.089,18.465,59.108,18.486,59.105,18.416,59.029]],[[18.595,59.47,18.57,59.437,18.545,59.478,18.555,59.486,18.572,59.526,18.621,59.548,18.698,59.535,18.698,59.525,18.624,59.492,18.595,59.47]]]},{"n":"Eswatini","c":"SZ","r":4,"ml":4,"lx":31.47,"ly":-26.53,"b":[30.788,-27.31,32.113,-25.743],"p":[[[31.948,-25.958,31.968,-25.972,32.061,-26.018,32.069,-26.11,32.06,-26.215,32.041,-26.281,32.048,-26.347,32.078,-26.45,32.106,-26.52,32.113,-26.839,32.082,-26.825,32.025,-26.811,31.995,-26.817,31.967,-26.961,31.946,-27.174,31.958,-27.306,31.743,-27.31,31.47,-27.296,31.274,-27.238,31.063,-27.112,30.938,-26.916,30.883,-26.792,30.807,-26.785,30.794,-26.764,30.788,-26.614,30.789,-26.455,30.803,-26.413,30.945,-26.219,31.033,-26.098,31.088,-25.981,31.207,-25.843,31.335,-25.756,31.383,-25.743,31.415,-25.747,31.64,-25.867,31.871,-25.982,31.922,-25.969,31.948,-25.958]]]},{"n":"Suriname","c":"SR","r":4,"ml":4,"lx":-55.91,"ly":4.14,"b":[-58.054,1.842,-53.99,5.993],"p":[[[-54.156,5.359,-54.24,5.288,-54.332,5.187,-54.452,5.013,-54.447,4.959,-54.473,4.915,-54.48,4.837,-54.471,4.749,-54.44,4.692,-54.426,4.583,-54.45,4.485,-54.441,4.428,-54.416,4.338,-54.396,4.241,-54.398,4.202,-54.369,4.171,-54.342,4.14,-54.351,4.054,-54.256,3.901,-54.197,3.834,-54.113,3.769,-54.082,3.706,-54.034,3.629,-54.006,3.62,-53.99,3.59,-54.006,3.531,-54.01,3.449,-54.063,3.353,-54.188,3.179,-54.203,3.138,-54.171,2.994,-54.188,2.875,-54.196,2.818,-54.257,2.714,-54.402,2.462,-54.486,2.416,-54.536,2.343,-54.568,2.343,-54.605,2.336,-54.616,2.327,-54.662,2.328,-54.697,2.36,-54.703,2.398,-54.722,2.442,-54.767,2.455,-54.852,2.44,-54.876,2.45,-54.927,2.497,-54.968,2.548,-54.979,2.598,-55.006,2.593,-55.07,2.548,-55.114,2.539,-55.149,2.551,-55.188,2.548,-55.286,2.5,-55.344,2.489,-55.385,2.441,-55.659,2.419,-55.731,2.406,-55.894,2.49,-55.936,2.517,-55.957,2.52,-55.976,2.516,-55.994,2.498,-56.02,2.393,-56.045,2.364,-56.088,2.341,-56.129,2.3,-56.138,2.259,-56.074,2.237,-56.02,2.158,-55.962,2.095,-55.915,2.04,-55.922,1.977,-55.93,1.888,-55.963,1.857,-56.02,1.842,-56.227,1.885,-56.386,1.924,-56.453,1.932,-56.483,1.942,-56.522,1.975,-56.563,2.005,-56.627,2.016,-56.704,2.036,-56.761,2.115,-56.82,2.227,-56.841,2.277,-56.886,2.326,-56.931,2.395,-56.945,2.457,-56.979,2.513,-56.997,2.532,-57.023,2.609,-57.029,2.638,-57.042,2.641,-57.06,2.666,-57.097,2.748,-57.105,2.768,-57.121,2.776,-57.164,2.833,-57.197,2.853,-57.21,2.883,-57.207,2.963,-57.225,3.003,-57.231,3.079,-57.232,3.109,-57.249,3.142,-57.278,3.164,-57.283,3.219,-57.29,3.354,-57.304,3.377,-57.426,3.375,-57.438,3.362,-57.491,3.354,-57.55,3.353,-57.603,3.371,-57.647,3.395,-57.656,3.424,-57.649,3.517,-57.72,3.588,-57.833,3.676,-57.867,3.787,-57.908,3.857,-58.032,4.002,-58.054,4.102,-58.054,4.172,-58.011,4.236,-57.95,4.35,-57.925,4.453,-57.906,4.507,-57.875,4.577,-57.846,4.668,-57.868,4.724,-57.905,4.779,-57.917,4.82,-57.881,4.881,-57.845,4.923,-57.804,4.929,-57.752,4.954,-57.711,4.991,-57.649,5.001,-57.571,5.004,-57.412,5.005,-57.331,5.02,-57.306,5.05,-57.31,5.106,-57.269,5.157,-57.227,5.179,-57.21,5.195,-57.207,5.214,-57.218,5.232,-57.235,5.243,-57.28,5.247,-57.319,5.335,-57.292,5.374,-57.258,5.445,-57.248,5.485,-57.195,5.548,-57.182,5.529,-57.141,5.644,-57.136,5.737,-57.105,5.829,-57.057,5.939,-56.97,5.993,-56.466,5.938,-56.236,5.885,-55.94,5.795,-55.898,5.699,-55.896,5.795,-55.91,5.893,-55.828,5.962,-55.648,5.986,-55.379,5.953,-55.148,5.993,-54.834,5.988,-54.356,5.91,-54.142,5.856,-54.054,5.808,-54.037,5.721,-54.046,5.609,-54.08,5.502,-54.156,5.359]]]},{"n":"South Sudan","c":"SS","r":3,"ml":3,"lx":30.39,"ly":7.23,"b":[24.147,3.491,35.268,12.223],"p":[[[33.976,4.22,33.742,3.985,33.568,3.812,33.54,3.787,33.489,3.755,33.324,3.754,33.154,3.775,32.997,3.88,32.838,3.798,32.737,3.773,32.677,3.763,32.535,3.75,32.336,3.706,32.246,3.651,32.197,3.608,32.156,3.528,32.136,3.52,32.099,3.529,32.048,3.561,31.942,3.608,31.888,3.709,31.839,3.77,31.798,3.803,31.629,3.701,31.547,3.678,31.48,3.68,31.357,3.738,31.222,3.786,31.152,3.786,31.048,3.725,30.929,3.634,30.868,3.544,30.839,3.491,30.817,3.533,30.797,3.573,30.757,3.624,30.7,3.644,30.648,3.634,30.587,3.624,30.559,3.653,30.554,3.723,30.537,3.787,30.508,3.836,30.421,3.884,30.195,3.982,30.021,4.178,29.934,4.269,29.87,4.327,29.78,4.481,29.677,4.587,29.552,4.636,29.47,4.612,29.385,4.498,29.225,4.392,29.151,4.388,29.057,4.446,28.939,4.487,28.727,4.505,28.64,4.454,28.525,4.373,28.428,4.324,28.367,4.319,28.311,4.338,28.247,4.349,28.192,4.35,28.079,4.425,28.02,4.479,27.981,4.532,27.917,4.568,27.842,4.598,27.788,4.645,27.761,4.703,27.719,4.778,27.664,4.846,27.491,4.968,27.439,5.039,27.403,5.109,27.332,5.186,27.257,5.29,27.233,5.441,27.229,5.563,27.213,5.619,27.181,5.675,27.144,5.723,27.083,5.777,26.942,5.855,26.796,5.946,26.726,5.998,26.594,6.018,26.514,6.069,26.447,6.183,26.421,6.274,26.353,6.345,26.325,6.396,26.309,6.455,26.362,6.635,26.285,6.699,26.169,6.782,26.087,6.872,26.037,6.955,25.889,7.065,25.567,7.229,25.381,7.333,25.279,7.427,25.19,7.519,25.181,7.557,25.239,7.649,25.247,7.725,25.2,7.808,25.007,7.965,24.853,8.138,24.737,8.192,24.456,8.239,24.375,8.258,24.291,8.291,24.208,8.369,24.18,8.461,24.221,8.608,24.195,8.653,24.147,8.666,24.16,8.696,24.214,8.768,24.3,8.814,24.532,8.887,24.545,8.915,24.549,9.007,24.568,9.052,24.648,9.179,24.659,9.23,24.663,9.338,24.674,9.389,24.697,9.426,24.76,9.489,24.783,9.527,24.792,9.61,24.785,9.775,24.818,9.84,24.964,9.989,25.003,10.055,25.016,10.115,25.015,10.176,25.024,10.236,25.067,10.294,25.104,10.312,25.212,10.33,25.285,10.319,25.798,10.421,25.858,10.406,25.885,10.346,25.883,10.25,25.892,10.203,25.919,10.169,26.001,10.123,26.057,10.047,26.087,10.018,26.17,9.966,26.551,9.526,26.659,9.484,26.763,9.499,26.971,9.591,27.074,9.614,27.8,9.588,27.881,9.602,27.886,9.6,27.996,9.379,28.049,9.329,28.845,9.326,28.829,9.389,28.839,9.459,28.932,9.549,28.98,9.594,29,9.61,29.122,9.675,29.242,9.718,29.473,9.769,29.557,9.848,29.604,9.921,29.605,10.065,29.636,10.089,29.691,10.122,29.958,10.25,30.003,10.277,30.475,9.979,30.739,9.743,30.755,9.731,30.769,9.727,30.783,9.735,30.795,9.746,30.814,9.753,30.827,9.756,30.94,9.759,31.154,9.771,31.225,9.799,31.655,10.221,31.764,10.356,31.792,10.383,31.854,10.479,31.92,10.644,31.933,10.663,32.404,11.058,32.421,11.089,32.425,11.114,32.354,11.247,32.339,11.315,32.336,11.419,32.35,11.58,32.345,11.683,32.343,11.694,32.338,11.71,32.335,11.716,32.072,12.007,32.737,12.01,32.738,12.034,32.738,12.046,32.736,12.058,32.723,12.093,32.716,12.139,32.715,12.152,32.716,12.165,32.72,12.189,32.721,12.202,32.72,12.208,32.719,12.214,32.719,12.219,32.722,12.223,33.199,12.217,33.193,12.135,33.135,11.942,33.136,11.826,33.122,11.693,33.119,11.682,33.106,11.654,33.095,11.638,33.082,11.622,33.078,11.616,33.073,11.606,33.073,11.592,33.172,10.85,33.168,10.831,33.165,10.819,33.138,10.773,33.131,10.758,33.13,10.746,33.141,10.738,33.361,10.658,33.371,10.653,33.38,10.646,33.459,10.551,33.892,10.199,33.907,10.181,33.952,10.071,33.957,10.054,33.958,10.028,33.957,10.007,33.946,9.941,33.95,9.911,33.957,9.891,33.963,9.869,33.963,9.862,33.963,9.856,33.959,9.845,33.895,9.718,33.874,9.627,33.868,9.55,33.871,9.506,33.879,9.478,33.882,9.471,33.885,9.466,33.888,9.464,33.891,9.462,34.077,9.462,34.078,9.462,34.077,9.421,34.085,9.219,34.091,9.041,34.102,8.752,34.102,8.676,34.095,8.582,34.073,8.545,34.02,8.492,33.953,8.444,33.785,8.431,33.645,8.433,33.545,8.443,33.409,8.448,33.281,8.437,33.234,8.396,33.165,8.251,33.065,8.04,33.013,7.952,32.999,7.9,33.015,7.869,33.081,7.824,33.226,7.761,33.392,7.724,33.516,7.708,33.601,7.69,33.666,7.671,33.902,7.51,33.978,7.435,34.02,7.368,34.03,7.297,34.064,7.226,34.2,7.085,34.279,7.003,34.484,6.898,34.563,6.78,34.639,6.722,34.711,6.66,34.749,6.568,34.838,6.3,34.898,6.16,34.959,6.045,34.984,5.858,35.032,5.775,35.082,5.673,35.164,5.581,35.252,5.511,35.268,5.492,35.084,5.312,34.878,5.11,34.64,4.875,34.38,4.621,34.177,4.419,33.976,4.22]]]},{"n":"Sudan","c":"SD","r":3,"ml":2.5,"lx":29.26,"ly":16.33,"b":[21.825,8.666,38.609,22.202],"p":[[[34.078,9.462,34.077,9.462,33.891,9.462,33.888,9.464,33.885,9.466,33.882,9.471,33.879,9.478,33.871,9.506,33.868,9.55,33.874,9.627,33.895,9.718,33.959,9.845,33.963,9.856,33.963,9.862,33.963,9.869,33.957,9.891,33.95,9.911,33.946,9.941,33.957,10.007,33.958,10.028,33.957,10.054,33.952,10.071,33.907,10.181,33.892,10.199,33.459,10.551,33.38,10.646,33.371,10.653,33.361,10.658,33.141,10.738,33.13,10.746,33.131,10.758,33.138,10.773,33.165,10.819,33.168,10.831,33.172,10.85,33.073,11.592,33.073,11.606,33.078,11.616,33.082,11.622,33.095,11.638,33.106,11.654,33.119,11.682,33.122,11.693,33.136,11.826,33.135,11.942,33.193,12.135,33.199,12.217,32.722,12.223,32.719,12.219,32.719,12.214,32.72,12.208,32.721,12.202,32.72,12.189,32.716,12.165,32.715,12.152,32.716,12.139,32.723,12.093,32.736,12.058,32.738,12.046,32.738,12.034,32.737,12.01,32.072,12.007,32.335,11.716,32.338,11.71,32.343,11.694,32.345,11.683,32.35,11.58,32.336,11.419,32.339,11.315,32.354,11.247,32.425,11.114,32.421,11.089,32.404,11.058,31.933,10.663,31.92,10.644,31.854,10.479,31.792,10.383,31.764,10.356,31.655,10.221,31.225,9.799,31.154,9.771,30.94,9.759,30.827,9.756,30.814,9.753,30.795,9.746,30.783,9.735,30.769,9.727,30.755,9.731,30.739,9.743,30.475,9.979,30.003,10.277,29.958,10.25,29.691,10.122,29.636,10.089,29.605,10.065,29.604,9.921,29.557,9.848,29.473,9.769,29.242,9.718,29.122,9.675,29,9.61,28.98,9.594,28.932,9.549,28.839,9.459,28.829,9.389,28.845,9.326,28.049,9.329,27.996,9.379,27.886,9.6,27.881,9.602,27.8,9.588,27.074,9.614,26.971,9.591,26.763,9.499,26.659,9.484,26.551,9.526,26.17,9.966,26.087,10.018,26.057,10.047,26.001,10.123,25.919,10.169,25.892,10.203,25.883,10.25,25.885,10.346,25.858,10.406,25.798,10.421,25.285,10.319,25.212,10.33,25.104,10.312,25.067,10.294,25.024,10.236,25.015,10.176,25.016,10.115,25.003,10.055,24.964,9.989,24.818,9.84,24.785,9.775,24.792,9.61,24.783,9.527,24.76,9.489,24.697,9.426,24.674,9.389,24.663,9.338,24.659,9.23,24.648,9.179,24.568,9.052,24.549,9.007,24.545,8.915,24.532,8.887,24.3,8.814,24.214,8.768,24.16,8.696,24.147,8.666,24.048,8.691,23.922,8.71,23.679,8.732,23.583,8.766,23.537,8.816,23.552,8.943,23.528,8.971,23.489,8.993,23.463,9.048,23.468,9.115,23.596,9.262,23.623,9.341,23.643,9.614,23.656,9.71,23.646,9.823,23.545,10.03,23.457,10.174,23.312,10.388,23.256,10.458,22.964,10.752,22.931,10.795,22.86,10.92,22.895,11.029,22.938,11.192,22.943,11.267,22.923,11.345,22.849,11.403,22.783,11.41,22.754,11.44,22.697,11.483,22.641,11.516,22.591,11.58,22.556,11.67,22.581,11.99,22.564,12.033,22.49,12.045,22.472,12.068,22.475,12.129,22.435,12.312,22.39,12.463,22.414,12.546,22.352,12.66,22.233,12.709,22.121,12.695,22.001,12.672,21.928,12.678,21.878,12.699,21.843,12.741,21.825,12.791,21.842,12.865,21.908,13.001,21.99,13.113,22.158,13.215,22.203,13.269,22.228,13.33,22.233,13.399,22.221,13.472,22.202,13.538,22.153,13.626,22.108,13.73,22.106,13.8,22.128,13.85,22.173,13.911,22.262,13.979,22.283,13.992,22.339,14.029,22.388,14.056,22.51,14.127,22.539,14.162,22.528,14.203,22.498,14.237,22.449,14.284,22.439,14.342,22.425,14.441,22.4,14.504,22.382,14.55,22.416,14.585,22.468,14.633,22.532,14.663,22.632,14.688,22.671,14.722,22.682,14.789,22.679,14.851,22.715,14.898,22.763,14.999,22.802,15.044,22.867,15.097,22.932,15.162,22.961,15.238,22.97,15.311,22.934,15.533,23.009,15.626,23.105,15.703,23.243,15.697,23.458,15.714,23.604,15.746,23.708,15.745,23.946,15.704,23.965,15.713,23.971,15.722,23.983,15.78,23.983,15.928,23.983,16.374,23.983,16.82,23.982,17.266,23.982,17.712,23.981,18.158,23.981,18.605,23.981,19.051,23.98,19.497,23.98,19.621,23.98,19.746,23.98,19.871,23.98,19.996,24.227,19.996,24.474,19.996,24.72,19.996,24.967,19.995,24.97,19.997,24.973,19.999,24.976,20.001,24.979,20.003,24.98,20.501,24.98,20.999,24.98,21.498,24.98,21.996,25.362,21.996,25.744,21.996,26.126,21.996,26.508,21.996,26.89,21.996,27.272,21.996,27.654,21.995,28.036,21.995,28.419,21.995,28.801,21.995,29.183,21.995,29.565,21.995,29.947,21.995,30.329,21.995,30.711,21.995,31.093,21.995,31.209,21.995,31.261,22.002,31.358,22.189,31.4,22.202,31.464,22.192,31.486,22.148,31.466,22.085,31.434,21.996,31.622,21.996,31.95,21.996,32.278,21.996,32.606,21.996,32.934,21.996,33.262,21.996,33.59,21.996,33.918,21.996,34.246,21.996,34.575,21.996,34.903,21.996,35.231,21.996,35.559,21.996,35.887,21.997,36.215,21.997,36.543,21.997,36.871,21.997,36.883,21.769,36.927,21.587,37.081,21.326,37.212,21.186,37.259,21.109,37.263,21.073,37.257,21.039,37.217,21.078,37.151,21.104,37.141,20.982,37.157,20.895,37.173,20.732,37.228,20.557,37.188,20.395,37.193,20.121,37.263,19.792,37.248,19.582,37.362,19.092,37.471,18.82,37.532,18.753,37.599,18.717,37.73,18.694,37.922,18.556,38.074,18.41,38.128,18.333,38.202,18.249,38.252,18.264,38.283,18.287,38.333,18.219,38.574,18.073,38.609,18.005,38.523,17.939,38.422,17.824,38.397,17.778,38.386,17.751,38.374,17.717,38.347,17.684,38.29,17.637,38.267,17.617,38.254,17.585,38.219,17.564,38.182,17.563,38.149,17.549,38.099,17.526,38.025,17.538,37.95,17.518,37.923,17.492,37.863,17.47,37.803,17.466,37.782,17.458,37.726,17.421,37.657,17.368,37.576,17.335,37.547,17.324,37.51,17.288,37.453,17.109,37.411,17.062,37.34,17.057,37.249,17.057,37.17,17.041,37.062,17.061,37.009,17.059,36.995,17.021,36.976,16.867,36.979,16.801,36.936,16.722,36.888,16.625,36.905,16.46,36.914,16.296,36.826,16.05,36.813,15.994,36.725,15.799,36.679,15.726,36.566,15.362,36.522,15.25,36.427,15.132,36.448,14.94,36.471,14.736,36.492,14.544,36.524,14.257,36.444,13.988,36.447,13.842,36.391,13.626,36.346,13.526,36.307,13.467,36.274,13.406,36.212,13.271,36.16,13.093,36.137,12.911,36.135,12.805,36.125,12.757,36.108,12.726,35.988,12.706,35.821,12.685,35.731,12.661,35.67,12.624,35.596,12.537,35.45,12.301,35.373,12.156,35.252,11.957,35.112,11.817,35.083,11.748,35.06,11.621,35.008,11.42,34.961,11.277,34.969,11.162,34.925,10.962,34.931,10.865,34.882,10.811,34.816,10.759,34.771,10.746,34.675,10.805,34.602,10.865,34.572,10.88,34.508,10.843,34.431,10.788,34.344,10.659,34.276,10.528,34.315,10.252,34.311,10.191,34.292,10.125,34.185,9.919,34.159,9.853,34.12,9.73,34.079,9.513,34.078,9.462]]]},{"n":"Sri Lanka","c":"LK","r":3,"ml":3,"lx":80.7,"ly":7.58,"b":[79.708,5.949,81.877,9.813],"p":[[[79.97,9.631,79.907,9.62,79.857,9.686,79.846,9.715,79.859,9.734,79.872,9.744,79.888,9.741,79.912,9.679,79.97,9.631]],[[79.875,9.051,79.904,8.975,79.821,9.027,79.767,9.07,79.748,9.105,79.86,9.066,79.875,9.051]],[[79.982,9.813,80.078,9.807,80.181,9.81,80.253,9.796,80.376,9.642,80.711,9.366,80.893,9.086,80.91,9.025,80.935,8.971,80.979,8.957,81.016,8.933,81.198,8.662,81.219,8.608,81.216,8.549,81.227,8.506,81.275,8.484,81.334,8.472,81.373,8.431,81.422,8.215,81.422,8.148,81.436,8.119,81.665,7.782,81.679,7.742,81.676,7.711,81.683,7.684,81.727,7.625,81.797,7.465,81.832,7.428,81.874,7.288,81.877,7.02,81.861,6.901,81.819,6.756,81.768,6.614,81.713,6.512,81.637,6.425,81.38,6.241,81.306,6.204,80.971,6.088,80.724,5.979,80.496,5.949,80.267,6.01,80.095,6.153,80.007,6.364,79.947,6.585,79.859,6.829,79.792,7.585,79.76,7.796,79.708,8.066,79.713,8.182,79.75,8.294,79.75,8.049,79.783,8.018,79.809,8.05,79.832,8.304,79.851,8.412,79.942,8.692,79.944,8.741,79.928,8.846,79.929,8.899,80.065,9.096,80.1,9.21,80.118,9.327,80.111,9.453,80.086,9.578,80.196,9.538,80.256,9.495,80.318,9.465,80.368,9.48,80.428,9.481,80.385,9.549,80.258,9.611,80.046,9.65,79.979,9.699,79.954,9.742,79.967,9.793,79.982,9.813]]]},{"n":"Spain","c":"ES","r":2,"ml":2,"lx":-3.46,"ly":40.09,"b":[-18.161,27.646,4.322,43.765],"p":[[[1.594,38.672,1.571,38.659,1.505,38.671,1.406,38.671,1.402,38.711,1.417,38.74,1.436,38.768,1.497,38.712,1.593,38.701,1.594,38.672]],[[3.145,39.79,3.241,39.757,3.342,39.787,3.396,39.777,3.449,39.761,3.462,39.698,3.415,39.627,3.349,39.556,3.293,39.477,3.245,39.387,3.155,39.333,3.073,39.301,2.9,39.368,2.8,39.385,2.77,39.41,2.746,39.51,2.701,39.542,2.634,39.556,2.576,39.531,2.5,39.478,2.459,39.53,2.394,39.54,2.37,39.572,2.371,39.613,2.785,39.855,2.905,39.908,3.159,39.971,3.198,39.961,3.164,39.924,3.167,39.908,3.199,39.89,3.191,39.861,3.159,39.837,3.145,39.79]],[[4.294,39.842,4.275,39.83,3.968,39.946,3.867,39.959,3.843,39.976,3.845,40.036,3.853,40.063,4.059,40.075,4.226,40.032,4.315,39.917,4.322,39.898,4.294,39.842]],[[1.445,38.919,1.409,38.857,1.257,38.879,1.223,38.904,1.256,38.973,1.3,38.982,1.303,39.031,1.349,39.081,1.564,39.121,1.613,39.087,1.624,39.039,1.495,38.933,1.445,38.919]],[[-1.794,43.407,-1.793,43.373,-1.753,43.325,-1.713,43.307,-1.627,43.282,-1.561,43.279,-1.472,43.268,-1.411,43.24,-1.407,43.197,-1.423,43.149,-1.459,43.105,-1.48,43.071,-1.461,43.052,-1.429,43.037,-1.394,43.033,-1.371,43.038,-1.353,43.064,-1.319,43.097,-1.3,43.101,-1.302,43.082,-1.285,43.06,-1.175,43.021,-0.934,42.95,-0.839,42.948,-0.763,42.94,-0.74,42.91,-0.586,42.799,-0.55,42.802,-0.481,42.799,-0.398,42.808,-0.339,42.829,-0.299,42.825,-0.256,42.804,-0.205,42.785,-0.14,42.749,-0.081,42.704,-0.041,42.689,0.201,42.719,0.255,42.693,0.313,42.693,0.377,42.7,0.518,42.686,0.632,42.69,0.642,42.701,0.652,42.8,0.67,42.836,0.697,42.845,0.764,42.838,1.01,42.779,1.111,42.742,1.208,42.713,1.293,42.71,1.349,42.691,1.428,42.596,1.415,42.548,1.422,42.531,1.43,42.498,1.428,42.461,1.449,42.437,1.486,42.434,1.534,42.442,1.586,42.456,1.679,42.497,1.706,42.503,1.86,42.457,1.928,42.426,1.951,42.393,1.987,42.358,2.033,42.354,2.098,42.386,2.2,42.421,2.374,42.39,2.568,42.346,2.652,42.34,2.655,42.362,2.67,42.393,2.702,42.408,2.749,42.413,2.816,42.429,2.891,42.456,2.97,42.467,3.053,42.447,3.152,42.431,3.211,42.431,3.24,42.368,3.288,42.344,3.307,42.289,3.219,42.26,3.166,42.256,3.15,42.162,3.175,42.136,3.225,42.111,3.238,42.082,3.248,41.944,3.147,41.861,3.005,41.767,2.311,41.467,2.146,41.321,2.083,41.287,1.567,41.196,1.206,41.098,1.033,41.062,0.817,40.892,0.715,40.823,0.796,40.804,0.891,40.722,0.859,40.686,0.721,40.63,0.66,40.613,0.627,40.622,0.596,40.615,0.364,40.319,0.158,40.107,0.043,40.014,-0.075,39.876,-0.327,39.52,-0.329,39.417,-0.205,39.063,-0.134,38.969,-0.034,38.891,0.155,38.825,0.202,38.759,0.136,38.697,-0.053,38.586,-0.381,38.436,-0.521,38.317,-0.551,38.203,-0.647,38.152,-0.683,37.992,-0.742,37.886,-0.753,37.85,-0.815,37.77,-0.823,37.712,-0.722,37.631,-0.772,37.596,-0.822,37.581,-0.938,37.571,-1.328,37.561,-1.641,37.387,-1.798,37.233,-1.939,36.946,-2.112,36.777,-2.188,36.745,-2.306,36.82,-2.453,36.831,-2.596,36.806,-2.671,36.748,-2.788,36.715,-2.902,36.743,-3.149,36.758,-3.259,36.756,-3.431,36.708,-3.579,36.74,-3.828,36.756,-4.367,36.718,-4.435,36.7,-4.502,36.629,-4.674,36.506,-4.935,36.502,-5.171,36.424,-5.231,36.374,-5.33,36.236,-5.361,36.135,-5.382,36.134,-5.407,36.159,-5.444,36.151,-5.462,36.074,-5.551,36.039,-5.625,36.026,-5.808,36.088,-5.961,36.182,-6.041,36.188,-6.17,36.334,-6.226,36.426,-6.266,36.527,-6.258,36.565,-6.269,36.597,-6.384,36.637,-6.412,36.729,-6.328,36.848,-6.259,36.899,-6.217,36.914,-6.321,36.908,-6.396,36.832,-6.492,36.955,-6.885,37.194,-6.859,37.249,-6.864,37.279,-6.929,37.215,-6.975,37.198,-7.175,37.209,-7.406,37.179,-7.467,37.428,-7.496,37.524,-7.504,37.585,-7.444,37.728,-7.379,37.786,-7.292,37.906,-7.185,38.006,-7.073,38.03,-7.023,38.045,-6.981,38.122,-6.958,38.188,-6.975,38.194,-7.106,38.181,-7.343,38.457,-7.336,38.501,-7.306,38.567,-7.286,38.649,-7.282,38.715,-7.22,38.771,-7.125,38.827,-7.046,38.907,-7.006,38.985,-6.998,39.056,-7.043,39.107,-7.172,39.135,-7.306,39.338,-7.335,39.465,-7.363,39.478,-7.445,39.536,-7.524,39.645,-7.536,39.662,-7.454,39.681,-7.118,39.682,-7.047,39.706,-7.037,39.714,-6.975,39.798,-6.911,39.937,-6.896,40.022,-6.916,40.057,-7.028,40.143,-7.033,40.168,-7.015,40.208,-6.948,40.252,-6.859,40.301,-6.81,40.343,-6.822,40.376,-6.848,40.411,-6.852,40.443,-6.836,40.483,-6.83,40.619,-6.818,40.654,-6.836,40.777,-6.858,40.878,-6.928,41.009,-6.916,41.038,-6.883,41.062,-6.776,41.108,-6.69,41.215,-6.566,41.304,-6.403,41.375,-6.289,41.455,-6.244,41.516,-6.212,41.532,-6.222,41.56,-6.243,41.602,-6.308,41.642,-6.392,41.665,-6.485,41.664,-6.542,41.673,-6.559,41.704,-6.553,41.79,-6.558,41.874,-6.575,41.913,-6.618,41.942,-6.704,41.935,-6.777,41.958,-6.833,41.964,-6.866,41.945,-7.03,41.951,-7.099,41.964,-7.147,41.981,-7.178,41.972,-7.195,41.955,-7.198,41.929,-7.21,41.895,-7.269,41.864,-7.404,41.834,-7.513,41.836,-7.613,41.858,-7.645,41.874,-7.693,41.888,-7.896,41.871,-7.921,41.884,-7.991,41.852,-8.094,41.814,-8.152,41.812,-8.174,41.82,-8.181,41.837,-8.225,41.896,-8.213,41.927,-8.13,42.018,-8.139,42.04,-8.174,42.069,-8.204,42.112,-8.213,42.134,-8.266,42.137,-8.323,42.115,-8.538,42.069,-8.59,42.053,-8.683,42.008,-8.777,41.941,-8.852,41.927,-8.878,41.947,-8.887,42.105,-8.772,42.211,-8.691,42.274,-8.729,42.287,-8.816,42.285,-8.81,42.334,-8.769,42.358,-8.73,42.412,-8.776,42.435,-8.812,42.47,-8.81,42.562,-8.8,42.6,-8.812,42.64,-8.988,42.586,-9.033,42.594,-9.035,42.662,-8.937,42.767,-8.927,42.799,-9.042,42.814,-9.127,42.865,-9.179,42.911,-9.235,42.977,-9.236,43.036,-9.178,43.174,-9.096,43.214,-9.025,43.239,-8.874,43.334,-8.666,43.317,-8.537,43.337,-8.422,43.386,-8.355,43.397,-8.249,43.439,-8.252,43.497,-8.289,43.54,-8.257,43.58,-8.137,43.629,-8.005,43.694,-7.853,43.707,-7.698,43.765,-7.595,43.727,-7.504,43.74,-7.399,43.696,-7.262,43.595,-7.061,43.554,-6.901,43.586,-6.617,43.592,-6.476,43.579,-6.224,43.604,-6.08,43.595,-5.847,43.645,-5.666,43.582,-5.316,43.553,-5.105,43.502,-4.523,43.416,-4.313,43.415,-4.015,43.463,-3.889,43.499,-3.774,43.478,-3.605,43.519,-3.524,43.511,-3.418,43.452,-3.046,43.372,-2.948,43.44,-2.875,43.454,-2.607,43.413,-2.337,43.328,-2.197,43.322,-1.991,43.345,-1.829,43.401,-1.794,43.407]],[[-16.334,28.38,-16.418,28.151,-16.496,28.062,-16.543,28.032,-16.658,28.007,-16.795,28.149,-16.866,28.293,-16.905,28.34,-16.843,28.376,-16.752,28.37,-16.557,28.4,-16.517,28.413,-16.319,28.558,-16.124,28.576,-16.119,28.528,-16.334,28.38]],[[-13.716,28.911,-13.784,28.845,-13.86,28.869,-13.824,29.013,-13.788,29.056,-13.65,29.119,-13.535,29.144,-13.501,29.211,-13.464,29.237,-13.423,29.198,-13.454,29.151,-13.478,29.007,-13.555,28.96,-13.716,28.911]],[[-14.197,28.169,-14.333,28.056,-14.469,28.082,-14.492,28.101,-14.356,28.13,-14.232,28.216,-14.153,28.407,-14.028,28.617,-14.003,28.707,-13.954,28.741,-13.886,28.745,-13.857,28.738,-13.827,28.691,-13.828,28.585,-13.863,28.409,-13.928,28.253,-14.197,28.169]],[[-15.401,28.147,-15.407,28.071,-15.383,27.993,-15.389,27.875,-15.437,27.811,-15.559,27.747,-15.656,27.758,-15.71,27.784,-15.807,27.888,-15.809,27.994,-15.721,28.064,-15.683,28.154,-15.453,28.137,-15.433,28.154,-15.415,28.159,-15.401,28.147]],[[-17.185,28.022,-17.225,28.014,-17.274,28.038,-17.325,28.118,-17.29,28.176,-17.259,28.203,-17.214,28.199,-17.13,28.156,-17.104,28.111,-17.101,28.083,-17.185,28.022]],[[-17.888,27.81,-17.985,27.646,-18.107,27.707,-18.136,27.728,-18.161,27.761,-18.043,27.768,-17.925,27.85,-17.888,27.81]],[[-17.834,28.493,-17.859,28.486,-17.882,28.565,-18.001,28.758,-17.929,28.845,-17.798,28.847,-17.745,28.787,-17.727,28.724,-17.752,28.689,-17.744,28.616,-17.758,28.569,-17.834,28.493]]]},{"n":"South Korea","c":"KR","r":2,"ml":2.5,"lx":128.13,"ly":36.38,"b":[126.008,33.202,130.934,38.623],"p":[[[126.634,37.782,126.665,37.801,126.667,37.828,126.667,37.917,126.754,37.979,126.879,38.106,126.94,38.176,127.01,38.241,127.09,38.284,127.17,38.305,127.294,38.313,127.533,38.305,127.579,38.313,127.746,38.319,127.785,38.308,127.905,38.3,128.039,38.309,128.106,38.327,128.169,38.359,128.223,38.417,128.279,38.524,128.339,38.608,128.375,38.623,128.619,38.176,128.852,37.887,129.052,37.678,129.335,37.275,129.418,37.059,129.426,36.926,129.473,36.742,129.433,36.637,129.445,36.471,129.427,36.385,129.393,36.323,129.391,36.202,129.402,36.138,129.404,36.052,129.426,36.019,129.458,36.006,129.51,36.038,129.573,36.051,129.562,35.948,129.485,35.687,129.419,35.498,129.329,35.333,129.214,35.182,129.077,35.123,128.98,35.102,128.796,35.094,128.643,35.12,128.511,35.101,128.458,35.069,128.419,35.016,128.448,34.932,128.444,34.87,128.388,34.875,128.276,34.911,128.152,34.916,128.095,34.934,128.036,35.022,127.977,35.019,127.873,34.966,127.715,34.955,127.659,34.926,127.639,34.89,127.663,34.843,127.742,34.783,127.715,34.721,127.632,34.69,127.565,34.766,127.524,34.84,127.477,34.844,127.404,34.823,127.39,34.743,127.423,34.688,127.479,34.625,127.401,34.553,127.381,34.501,127.325,34.463,127.173,34.546,127.195,34.605,127.261,34.662,127.269,34.72,127.247,34.755,127.031,34.607,126.897,34.439,126.826,34.451,126.796,34.494,126.755,34.512,126.611,34.404,126.584,34.318,126.531,34.314,126.508,34.351,126.506,34.428,126.482,34.494,126.333,34.59,126.264,34.673,126.301,34.72,126.426,34.695,126.525,34.698,126.505,34.738,126.473,34.756,126.539,34.779,126.593,34.824,126.548,34.837,126.479,34.81,126.421,34.823,126.398,34.933,126.327,35.045,126.291,35.154,126.361,35.217,126.396,35.314,126.46,35.456,126.493,35.501,126.582,35.534,126.614,35.571,126.565,35.59,126.487,35.606,126.488,35.647,126.542,35.669,126.602,35.714,126.717,35.769,126.753,35.872,126.72,35.898,126.647,35.922,126.664,35.975,126.693,36.014,126.682,36.038,126.597,36.105,126.54,36.166,126.557,36.236,126.544,36.341,126.552,36.43,126.548,36.478,126.507,36.586,126.488,36.694,126.433,36.678,126.389,36.651,126.231,36.689,126.181,36.692,126.161,36.772,126.217,36.871,126.352,36.958,126.429,36.969,126.487,37.007,126.578,37.02,126.687,36.96,126.784,36.948,126.839,36.846,126.872,36.824,126.879,36.862,126.958,36.906,126.977,36.939,126.96,36.958,126.869,36.976,126.787,37.103,126.776,37.158,126.746,37.194,126.791,37.295,126.696,37.411,126.65,37.447,126.657,37.551,126.608,37.617,126.58,37.654,126.563,37.717,126.578,37.745,126.621,37.755,126.634,37.782]],[[128.741,34.799,128.647,34.737,128.52,34.82,128.489,34.865,128.586,34.932,128.668,35.009,128.722,35.014,128.741,34.799]],[[128.066,34.806,128.055,34.708,127.984,34.703,127.942,34.766,127.897,34.735,127.873,34.735,127.838,34.813,127.832,34.875,127.915,34.921,127.966,34.893,128.038,34.879,128.066,34.806]],[[126.234,34.371,126.17,34.355,126.134,34.39,126.109,34.399,126.123,34.444,126.227,34.533,126.247,34.563,126.344,34.545,126.38,34.498,126.335,34.426,126.234,34.371]],[[126.521,37.737,126.516,37.605,126.461,37.61,126.423,37.624,126.407,37.649,126.369,37.772,126.412,37.823,126.494,37.783,126.521,37.737]],[[126.753,34.344,126.77,34.296,126.689,34.305,126.646,34.351,126.652,34.39,126.7,34.396,126.753,34.344]],[[127.799,34.615,127.788,34.584,127.737,34.631,127.787,34.682,127.799,34.615]],[[126.418,36.493,126.404,36.428,126.338,36.471,126.319,36.613,126.387,36.571,126.418,36.493]],[[126.172,34.731,126.159,34.707,126.115,34.714,126.071,34.783,126.052,34.838,126.008,34.867,126.078,34.915,126.169,34.83,126.172,34.731]],[[130.916,37.478,130.871,37.449,130.817,37.478,130.81,37.51,130.838,37.537,130.904,37.554,130.934,37.53,130.916,37.478]],[[126.327,33.224,126.282,33.202,126.24,33.215,126.229,33.225,126.179,33.283,126.166,33.312,126.199,33.368,126.338,33.46,126.696,33.549,126.76,33.553,126.901,33.515,126.931,33.444,126.905,33.382,126.873,33.341,126.709,33.272,126.582,33.238,126.327,33.224]]]},{"n":"South Africa","c":"ZA","r":2,"ml":1.7,"lx":23.67,"ly":-29.71,"b":[16.448,-46.963,37.888,-22.146],"p":[[[29.365,-22.194,29.377,-22.193,29.663,-22.146,29.902,-22.184,30.19,-22.291,30.46,-22.329,30.712,-22.298,30.916,-22.291,31.073,-22.308,31.197,-22.345,31.288,-22.402,31.293,-22.455,31.3,-22.479,31.348,-22.618,31.419,-22.825,31.467,-23.017,31.532,-23.279,31.53,-23.426,31.546,-23.482,31.604,-23.553,31.676,-23.674,31.7,-23.743,31.724,-23.795,31.8,-23.892,31.858,-24.04,31.908,-24.236,31.951,-24.33,31.967,-24.376,31.986,-24.461,31.983,-24.638,31.984,-24.844,31.986,-25.074,31.987,-25.263,31.979,-25.359,31.985,-25.632,31.92,-25.774,31.928,-25.885,31.948,-25.958,31.922,-25.969,31.871,-25.982,31.64,-25.867,31.415,-25.747,31.383,-25.743,31.335,-25.756,31.207,-25.843,31.088,-25.981,31.033,-26.098,30.945,-26.219,30.803,-26.413,30.789,-26.455,30.788,-26.614,30.794,-26.764,30.807,-26.785,30.883,-26.792,30.938,-26.916,31.063,-27.112,31.274,-27.238,31.47,-27.296,31.743,-27.31,31.958,-27.306,31.946,-27.174,31.967,-26.961,31.995,-26.817,32.025,-26.811,32.082,-26.825,32.113,-26.839,32.2,-26.833,32.354,-26.862,32.478,-26.858,32.589,-26.856,32.777,-26.851,32.886,-26.849,32.849,-27.08,32.706,-27.442,32.657,-27.607,32.535,-28.2,32.375,-28.498,32.286,-28.621,32.027,-28.84,31.955,-28.884,31.892,-28.912,31.778,-28.937,31.335,-29.378,31.17,-29.591,31.023,-29.901,30.878,-30.071,30.664,-30.434,30.472,-30.715,30.289,-30.97,29.971,-31.322,29.83,-31.424,29.735,-31.47,29.483,-31.675,29.128,-32.003,28.856,-32.294,28.449,-32.625,28.214,-32.769,27.861,-33.054,27.762,-33.096,27.364,-33.361,27.077,-33.521,26.614,-33.707,26.429,-33.76,25.99,-33.711,25.806,-33.737,25.652,-33.85,25.638,-34.011,25.574,-34.035,25.477,-34.028,25.17,-33.961,25.003,-33.974,24.906,-34.06,24.827,-34.169,24.596,-34.175,24.183,-34.062,23.698,-33.993,23.586,-33.985,23.35,-34.069,23.268,-34.081,22.926,-34.063,22.736,-34.01,22.554,-34.01,22.414,-34.054,22.246,-34.069,21.789,-34.373,21.553,-34.373,21.35,-34.408,21.249,-34.407,21.06,-34.365,20.99,-34.367,20.882,-34.387,20.775,-34.44,20.53,-34.463,20.435,-34.509,20.021,-34.786,19.926,-34.775,19.85,-34.757,19.635,-34.753,19.392,-34.606,19.298,-34.615,19.323,-34.571,19.331,-34.492,19.279,-34.437,19.245,-34.412,19.149,-34.417,19.098,-34.35,18.952,-34.344,18.902,-34.361,18.831,-34.364,18.825,-34.296,18.831,-34.254,18.826,-34.188,18.809,-34.108,18.752,-34.083,18.709,-34.072,18.605,-34.077,18.534,-34.086,18.5,-34.109,18.462,-34.168,18.462,-34.347,18.41,-34.296,18.352,-34.188,18.333,-34.074,18.354,-33.939,18.465,-33.888,18.456,-33.796,18.433,-33.717,18.309,-33.514,18.261,-33.422,18.156,-33.359,18.075,-33.207,17.993,-33.152,17.958,-33.046,17.878,-32.962,17.851,-32.827,17.895,-32.75,17.965,-32.709,18.037,-32.775,18.125,-32.749,18.251,-32.652,18.325,-32.505,18.33,-32.27,18.311,-32.122,18.211,-31.742,18.164,-31.655,17.939,-31.383,17.677,-31.019,17.347,-30.445,17.189,-30.1,16.95,-29.403,16.739,-29.009,16.481,-28.642,16.448,-28.618,16.487,-28.573,16.626,-28.488,16.689,-28.465,16.723,-28.475,16.756,-28.452,16.788,-28.395,16.795,-28.341,16.81,-28.265,16.841,-28.219,16.875,-28.128,16.933,-28.07,17.056,-28.031,17.149,-28.082,17.188,-28.133,17.205,-28.199,17.246,-28.231,17.312,-28.229,17.359,-28.269,17.386,-28.353,17.38,-28.414,17.343,-28.452,17.348,-28.501,17.396,-28.563,17.416,-28.621,17.448,-28.698,17.617,-28.743,17.699,-28.768,17.842,-28.777,17.976,-28.811,18.103,-28.872,18.311,-28.886,18.6,-28.855,18.839,-28.869,19.026,-28.928,19.162,-28.939,19.246,-28.902,19.282,-28.848,19.271,-28.778,19.313,-28.733,19.407,-28.714,19.483,-28.662,19.54,-28.575,19.671,-28.504,19.878,-28.449,19.98,-28.451,19.98,-28.31,19.98,-27.866,19.98,-27.421,19.98,-26.976,19.98,-26.531,19.98,-26.086,19.98,-25.642,19.98,-25.197,19.98,-24.777,20.029,-24.807,20.345,-25.03,20.431,-25.147,20.473,-25.221,20.609,-25.491,20.711,-25.733,20.793,-25.916,20.799,-25.999,20.811,-26.081,20.823,-26.121,20.815,-26.165,20.757,-26.264,20.698,-26.34,20.627,-26.444,20.62,-26.581,20.641,-26.742,20.685,-26.822,20.74,-26.849,20.871,-26.809,20.954,-26.821,21.071,-26.852,21.455,-26.833,21.501,-26.843,21.646,-26.854,21.695,-26.841,21.738,-26.807,21.788,-26.71,21.833,-26.678,21.915,-26.662,22.011,-26.636,22.091,-26.58,22.218,-26.389,22.471,-26.219,22.549,-26.178,22.598,-26.133,22.64,-26.071,22.729,-25.857,22.796,-25.679,22.819,-25.595,22.879,-25.458,22.951,-25.37,23.022,-25.324,23.058,-25.312,23.149,-25.289,23.266,-25.267,23.389,-25.291,23.521,-25.344,23.671,-25.434,23.823,-25.545,23.894,-25.601,23.97,-25.626,24.104,-25.635,24.193,-25.633,24.331,-25.743,24.4,-25.75,24.556,-25.783,24.748,-25.817,24.869,-25.813,24.999,-25.754,25.092,-25.751,25.213,-25.756,25.346,-25.74,25.444,-25.714,25.518,-25.663,25.584,-25.606,25.659,-25.438,25.703,-25.302,25.77,-25.146,25.852,-24.935,25.882,-24.788,25.912,-24.747,26.032,-24.702,26.131,-24.671,26.397,-24.614,26.452,-24.583,26.502,-24.513,26.618,-24.396,26.761,-24.297,26.835,-24.241,26.971,-23.763,26.987,-23.705,27.086,-23.578,27.146,-23.524,27.186,-23.523,27.241,-23.49,27.313,-23.424,27.399,-23.384,27.499,-23.368,27.563,-23.325,27.593,-23.253,27.644,-23.218,27.717,-23.22,27.758,-23.197,27.769,-23.149,27.813,-23.108,27.891,-23.074,27.931,-23.034,27.935,-22.987,28.028,-22.874,28.21,-22.694,28.382,-22.593,28.543,-22.573,28.696,-22.535,28.84,-22.481,28.946,-22.395,29.013,-22.278,29.13,-22.213,29.365,-22.194],[28.737,-30.102,28.901,-30.038,28.975,-29.999,29.029,-29.968,29.098,-29.919,29.122,-29.801,29.142,-29.701,29.195,-29.652,29.249,-29.619,29.294,-29.567,29.349,-29.442,29.387,-29.32,29.391,-29.27,29.371,-29.218,29.336,-29.164,29.301,-29.09,29.26,-29.078,29.178,-29.037,29.058,-28.954,28.954,-28.881,28.856,-28.776,28.816,-28.759,28.722,-28.688,28.681,-28.647,28.653,-28.598,28.626,-28.582,28.583,-28.594,28.472,-28.616,28.233,-28.701,28.084,-28.78,27.96,-28.873,27.83,-28.909,27.736,-28.94,27.66,-29.047,27.59,-29.146,27.527,-29.236,27.491,-29.277,27.458,-29.303,27.425,-29.36,27.357,-29.455,27.295,-29.519,27.207,-29.554,27.095,-29.599,27.057,-29.626,27.052,-29.664,27.092,-29.754,27.13,-29.84,27.194,-29.941,27.24,-30.015,27.313,-30.106,27.355,-30.159,27.35,-30.247,27.364,-30.279,27.388,-30.316,27.409,-30.325,27.431,-30.338,27.492,-30.364,27.507,-30.381,27.549,-30.411,27.59,-30.466,27.667,-30.542,27.753,-30.6,27.902,-30.624,28.018,-30.642,28.057,-30.631,28.096,-30.585,28.129,-30.525,28.139,-30.45,28.176,-30.41,28.315,-30.218,28.392,-30.148,28.439,-30.142,28.5,-30.129,28.577,-30.123,28.634,-30.129,28.647,-30.127,28.737,-30.102]],[[37.857,-46.944,37.814,-46.963,37.612,-46.946,37.59,-46.908,37.65,-46.849,37.685,-46.824,37.79,-46.837,37.873,-46.885,37.888,-46.902,37.857,-46.944]]]},{"n":"Somalia","c":"SO","r":6,"ml":4,"lx":45.19,"ly":3.57,"b":[40.964,-1.695,51.39,11.984],"p":[[[41.533,-1.695,41.538,-1.613,41.522,-1.572,41.427,-1.45,41.25,-1.221,41.116,-1.047,40.979,-0.87,40.978,-0.729,40.977,-0.307,40.973,0.535,40.97,1.378,40.967,2.221,40.965,2.642,40.964,2.815,40.979,2.842,41.135,2.997,41.342,3.202,41.613,3.59,41.761,3.802,41.884,3.978,41.915,4.031,42.024,4.138,42.228,4.202,42.355,4.212,42.792,4.292,42.857,4.324,42.895,4.361,42.931,4.445,43.016,4.563,43.126,4.644,43.334,4.75,43.538,4.84,43.583,4.855,43.829,4.911,43.889,4.931,43.989,4.951,44.028,4.951,44.37,4.931,44.637,4.916,44.912,4.9,44.941,4.912,45.133,5.122,45.438,5.455,45.634,5.668,45.935,5.997,46.167,6.235,46.423,6.497,46.672,6.737,46.971,7.026,47.16,7.208,47.453,7.49,47.732,7.759,47.978,7.997,48.127,8.222,48.273,8.443,48.429,8.68,48.617,8.965,48.794,9.233,48.938,9.452,48.938,9.564,48.938,9.808,48.938,9.973,48.938,10.433,48.938,10.714,48.938,10.982,48.939,11.258,49.062,11.271,49.388,11.343,49.642,11.451,50.11,11.529,50.466,11.728,50.528,11.823,50.636,11.944,50.792,11.984,51.191,11.842,51.255,11.831,51.232,11.745,51.218,11.658,51.136,11.505,51.084,11.336,51.122,11.077,51.141,10.657,51.131,10.596,51.105,10.536,51.094,10.489,51.051,10.472,51.032,10.445,51.063,10.434,51.188,10.48,51.186,10.53,51.193,10.555,51.296,10.499,51.369,10.475,51.39,10.423,51.385,10.387,51.268,10.403,51.209,10.431,51.036,10.385,50.93,10.336,50.898,10.253,50.874,9.924,50.833,9.71,50.825,9.428,50.685,9.241,50.638,9.109,50.43,8.845,50.321,8.62,50.286,8.509,50.103,8.2,49.852,7.963,49.761,7.66,49.671,7.47,49.57,7.297,49.349,6.991,49.235,6.777,49.093,6.408,49.049,6.174,48.649,5.494,48.234,4.953,47.975,4.497,47.511,3.968,46.879,3.286,46.051,2.475,45.826,2.31,44.92,1.81,44.333,1.391,44.033,1.106,43.718,0.858,43.468,0.622,42.712,-0.176,42.634,-0.251,42.561,-0.321,42.466,-0.457,42.399,-0.51,42.219,-0.738,42.106,-0.856,41.98,-0.973,41.926,-1.056,41.888,-1.151,41.846,-1.203,41.732,-1.43,41.632,-1.579,41.533,-1.695]]]},{"n":"Somaliland","c":"","r":5,"ml":4.5,"lx":46.73,"ly":9.44,"b":[42.656,7.997,48.939,11.5],"p":[[[48.939,11.258,48.938,10.982,48.938,10.714,48.938,10.433,48.938,9.973,48.938,9.808,48.938,9.564,48.938,9.452,48.794,9.233,48.617,8.965,48.429,8.68,48.273,8.443,48.127,8.222,47.978,7.997,47.638,7.997,47.306,7.997,46.978,7.997,46.92,8.026,46.645,8.118,46.296,8.235,45.863,8.38,45.555,8.483,45.227,8.591,44.894,8.7,44.632,8.786,44.306,8.893,44.023,8.986,43.984,9.009,43.827,9.151,43.621,9.337,43.581,9.341,43.483,9.379,43.394,9.48,43.303,9.609,43.218,9.77,43.182,9.88,43.069,9.926,43.015,10.013,42.913,10.141,42.842,10.203,42.816,10.257,42.784,10.37,42.725,10.492,42.669,10.568,42.656,10.6,42.66,10.621,42.763,10.787,42.81,10.846,42.863,10.903,42.906,10.96,42.923,10.999,43.049,11.194,43.159,11.366,43.246,11.5,43.441,11.346,43.631,11.035,43.853,10.784,44.158,10.551,44.279,10.472,44.387,10.43,44.943,10.437,45.338,10.65,45.696,10.804,45.817,10.836,46.025,10.794,46.254,10.781,46.46,10.734,46.565,10.746,46.973,10.925,47.23,11.1,47.405,11.174,47.474,11.175,47.713,11.112,48.019,11.139,48.439,11.29,48.573,11.321,48.674,11.323,48.903,11.255,48.939,11.258]]]},{"n":"Solomon Islands","c":"SB","r":3,"ml":3,"lx":159.17,"ly":-8.03,"b":[155.678,-11.832,166.929,-6.609],"p":[[[159.688,-8.508,159.64,-8.521,159.569,-8.485,159.538,-8.451,159.553,-8.399,159.595,-8.379,159.642,-8.414,159.646,-8.45,159.688,-8.508]],[[166.929,-11.665,166.841,-11.681,166.806,-11.677,166.747,-11.591,166.791,-11.571,166.855,-11.579,166.875,-11.63,166.929,-11.665]],[[166.133,-10.758,166.053,-10.775,166.028,-10.77,165.968,-10.779,165.904,-10.851,165.857,-10.841,165.819,-10.844,165.791,-10.785,165.79,-10.756,165.836,-10.761,165.86,-10.703,165.909,-10.674,166.024,-10.661,166.126,-10.68,166.162,-10.693,166.13,-10.745,166.133,-10.758]],[[160.576,-11.798,160.507,-11.832,160.443,-11.815,160.395,-11.789,160.355,-11.712,160.27,-11.664,160.15,-11.644,160.1,-11.611,160.087,-11.594,160.004,-11.58,159.979,-11.538,159.986,-11.495,160,-11.472,160.077,-11.493,160.449,-11.696,160.537,-11.759,160.576,-11.798]],[[161.548,-9.626,161.559,-9.733,161.554,-9.77,161.478,-9.691,161.442,-9.719,161.41,-9.682,161.412,-9.6,161.417,-9.514,161.402,-9.448,161.364,-9.353,161.407,-9.368,161.548,-9.626]],[[159.189,-9.124,159.175,-9.126,159.128,-9.114,159.071,-9.11,159.036,-9.075,159.078,-9.025,159.13,-8.993,159.154,-9.001,159.176,-9.022,159.228,-9.03,159.234,-9.094,159.189,-9.124]],[[160.168,-8.996,160.226,-9.01,160.254,-9.007,160.319,-9.061,160.408,-9.14,160.371,-9.181,160.3,-9.16,160.276,-9.169,160.268,-9.163,160.253,-9.123,160.175,-9.084,160.105,-9.081,160.096,-9.034,160.168,-8.996]],[[157.763,-8.242,157.826,-8.324,157.898,-8.506,157.885,-8.569,157.834,-8.573,157.819,-8.612,157.749,-8.524,157.656,-8.5,157.588,-8.445,157.565,-8.338,157.558,-8.27,157.504,-8.258,157.351,-8.275,157.302,-8.333,157.232,-8.315,157.218,-8.263,157.229,-8.212,157.322,-8.161,157.341,-8.096,157.433,-7.985,157.491,-7.966,157.599,-8.006,157.612,-8.165,157.651,-8.217,157.763,-8.242]],[[158.201,-8.822,158.179,-8.826,158.155,-8.786,158.21,-8.678,158.236,-8.765,158.253,-8.797,158.201,-8.822]],[[158.108,-8.684,158.009,-8.763,157.938,-8.736,157.879,-8.669,157.898,-8.587,157.909,-8.566,157.938,-8.561,157.967,-8.544,157.998,-8.508,158.105,-8.537,158.132,-8.557,158.068,-8.607,158.09,-8.623,158.104,-8.646,158.108,-8.684]],[[157.389,-8.713,157.389,-8.728,157.334,-8.7,157.212,-8.565,157.234,-8.52,157.345,-8.432,157.379,-8.421,157.411,-8.475,157.383,-8.555,157.347,-8.575,157.332,-8.651,157.389,-8.713]],[[156.688,-7.923,156.669,-7.937,156.635,-7.883,156.611,-7.866,156.612,-7.806,156.511,-7.708,156.502,-7.64,156.561,-7.574,156.64,-7.613,156.718,-7.696,156.809,-7.723,156.79,-7.778,156.708,-7.877,156.688,-7.923]],[[156.604,-8.172,156.592,-8.196,156.54,-8.073,156.542,-8.011,156.551,-7.971,156.57,-7.959,156.612,-8.096,156.604,-8.172]],[[157.172,-8.108,157.15,-8.123,157.041,-8.117,156.958,-8.014,156.959,-7.938,157.024,-7.868,157.103,-7.855,157.146,-7.883,157.186,-7.941,157.201,-8.016,157.192,-8.082,157.172,-8.108]],[[155.84,-7.097,155.739,-7.121,155.678,-7.089,155.705,-7.013,155.739,-6.973,155.865,-7.043,155.84,-7.097]],[[157.645,-8.759,157.643,-8.794,157.586,-8.783,157.458,-8.73,157.454,-8.706,157.526,-8.697,157.579,-8.704,157.623,-8.735,157.645,-8.759]],[[159.75,-9.273,159.971,-9.433,160.065,-9.419,160.355,-9.422,160.525,-9.536,160.625,-9.589,160.682,-9.692,160.751,-9.715,160.794,-9.767,160.819,-9.863,160.802,-9.878,160.713,-9.914,160.649,-9.929,160.482,-9.895,160.321,-9.821,160.002,-9.812,159.854,-9.792,159.803,-9.763,159.755,-9.726,159.68,-9.637,159.622,-9.532,159.612,-9.471,159.607,-9.354,159.626,-9.311,159.686,-9.269,159.75,-9.273]],[[159.879,-8.534,159.881,-8.557,159.746,-8.474,159.645,-8.372,159.354,-8.26,159.292,-8.203,159.239,-8.196,159.09,-8.103,158.944,-8.041,158.855,-7.96,158.832,-7.927,158.778,-7.907,158.686,-7.818,158.597,-7.759,158.565,-7.651,158.479,-7.577,158.457,-7.545,158.734,-7.604,158.863,-7.722,158.972,-7.789,159.011,-7.837,159.109,-7.904,159.198,-7.91,159.287,-7.976,159.368,-7.994,159.431,-8.029,159.843,-8.327,159.794,-8.406,159.849,-8.463,159.879,-8.534]],[[157.487,-7.33,157.519,-7.366,157.441,-7.426,157.339,-7.393,157.317,-7.359,157.315,-7.342,157.243,-7.353,157.102,-7.324,156.904,-7.18,156.696,-6.911,156.495,-6.762,156.457,-6.715,156.453,-6.638,156.479,-6.609,156.604,-6.641,156.765,-6.764,157.03,-6.892,157.103,-6.957,157.148,-7.114,157.193,-7.16,157.336,-7.28,157.412,-7.309,157.452,-7.314,157.487,-7.33]],[[160.749,-8.314,160.998,-8.612,160.988,-8.665,160.954,-8.699,160.944,-8.799,160.976,-8.837,161.043,-8.855,161.159,-8.962,161.205,-9.092,161.209,-9.133,161.257,-9.192,161.258,-9.317,161.368,-9.49,161.378,-9.574,161.367,-9.611,161.322,-9.59,161.191,-9.393,161.042,-9.308,161.024,-9.271,160.873,-9.157,160.772,-8.964,160.663,-8.621,160.714,-8.539,160.59,-8.373,160.596,-8.328,160.649,-8.338,160.685,-8.336,160.702,-8.317,160.749,-8.314]],[[161.715,-10.387,161.841,-10.446,161.914,-10.436,162.023,-10.477,162.105,-10.454,162.157,-10.506,162.287,-10.71,162.288,-10.776,162.373,-10.823,162.301,-10.832,162.201,-10.808,162.124,-10.824,162.043,-10.785,161.906,-10.764,161.787,-10.717,161.538,-10.566,161.539,-10.491,161.499,-10.455,161.487,-10.361,161.398,-10.332,161.294,-10.326,161.286,-10.282,161.305,-10.204,161.382,-10.206,161.476,-10.238,161.654,-10.352,161.698,-10.371,161.715,-10.387]]]},{"n":"Slovakia","c":"SK","r":6,"ml":4,"lx":19.05,"ly":48.73,"b":[16.863,47.763,22.539,49.598],"p":[[[22.539,49.073,22.524,49.031,22.483,48.983,22.432,48.934,22.389,48.873,22.333,48.745,22.295,48.686,22.143,48.569,22.132,48.405,22.111,48.393,21.767,48.338,21.721,48.347,21.675,48.378,21.649,48.401,21.633,48.419,21.603,48.464,21.563,48.496,21.505,48.522,21.451,48.552,21.382,48.553,21.196,48.511,21.067,48.506,20.981,48.52,20.867,48.546,20.643,48.55,20.49,48.527,20.475,48.495,20.334,48.296,20.129,48.222,19.95,48.147,19.899,48.131,19.81,48.155,19.709,48.2,19.625,48.223,19.564,48.213,19.497,48.162,19.467,48.111,19.265,48.073,18.914,48.051,18.792,48,18.75,47.939,18.748,47.893,18.778,47.853,18.741,47.806,18.724,47.787,18.476,47.777,18.146,47.763,17.948,47.767,17.762,47.77,17.635,47.81,17.481,47.888,17.317,47.991,17.302,47.993,17.277,48.004,17.175,48.012,17.147,48.006,17.086,48.04,17.068,48.083,16.973,48.198,16.865,48.387,16.863,48.441,16.904,48.504,16.943,48.551,16.949,48.589,16.953,48.599,16.985,48.677,17.063,48.781,17.136,48.841,17.188,48.861,17.297,48.843,17.483,48.828,17.625,48.842,17.758,48.888,17.831,48.929,17.893,48.971,17.913,48.999,17.941,49.012,18.051,49.037,18.086,49.065,18.1,49.119,18.11,49.18,18.133,49.225,18.161,49.257,18.365,49.336,18.383,49.364,18.416,49.391,18.476,49.421,18.535,49.465,18.596,49.491,18.676,49.488,18.75,49.494,18.807,49.509,18.832,49.511,18.938,49.498,18.957,49.448,18.968,49.396,19.149,49.4,19.25,49.511,19.302,49.525,19.386,49.564,19.442,49.598,19.48,49.576,19.535,49.505,19.593,49.447,19.627,49.424,19.63,49.407,19.664,49.396,19.73,49.39,19.774,49.372,19.787,49.319,19.788,49.27,19.767,49.235,19.757,49.204,19.802,49.192,19.869,49.204,19.916,49.221,20.058,49.181,20.108,49.271,20.164,49.316,20.237,49.338,20.303,49.366,20.363,49.385,20.405,49.384,20.423,49.392,20.475,49.39,20.535,49.381,20.616,49.392,20.729,49.37,20.8,49.329,20.868,49.315,20.947,49.317,21.001,49.34,21.079,49.418,21.136,49.417,21.225,49.429,21.35,49.429,21.64,49.412,21.712,49.382,21.89,49.343,21.968,49.299,22.002,49.246,22.02,49.21,22.203,49.153,22.473,49.081,22.539,49.073]]]},{"n":"Slovenia","c":"SI","r":6,"ml":5,"lx":14.92,"ly":46.06,"b":[13.378,45.428,16.516,46.863],"p":[[[16.516,46.5,16.428,46.524,16.321,46.535,16.301,46.521,16.258,46.508,16.237,46.484,16.253,46.389,16.227,46.373,16.106,46.382,16.067,46.371,16.001,46.305,15.933,46.278,15.848,46.258,15.784,46.234,15.704,46.213,15.636,46.201,15.609,46.172,15.593,46.14,15.597,46.109,15.666,46.048,15.676,45.984,15.668,45.904,15.652,45.862,15.625,45.834,15.454,45.798,15.277,45.733,15.273,45.718,15.354,45.66,15.357,45.646,15.29,45.613,15.284,45.58,15.291,45.542,15.327,45.502,15.339,45.467,15.242,45.441,15.11,45.451,14.955,45.5,14.9,45.493,14.847,45.467,14.793,45.478,14.734,45.508,14.65,45.571,14.609,45.61,14.592,45.651,14.569,45.657,14.534,45.645,14.505,45.595,14.427,45.506,14.37,45.481,14.283,45.487,14.161,45.485,14.086,45.478,13.993,45.509,13.97,45.503,13.97,45.483,13.936,45.45,13.879,45.428,13.615,45.477,13.578,45.517,13.637,45.536,13.72,45.588,13.776,45.582,13.845,45.593,13.875,45.615,13.831,45.68,13.722,45.761,13.663,45.792,13.583,45.812,13.57,45.834,13.614,45.962,13.601,45.98,13.509,45.974,13.488,45.987,13.48,46.009,13.486,46.04,13.548,46.089,13.617,46.133,13.635,46.158,13.633,46.177,13.545,46.197,13.492,46.217,13.45,46.224,13.421,46.212,13.4,46.225,13.378,46.262,13.4,46.318,13.479,46.369,13.563,46.415,13.637,46.449,13.68,46.463,13.7,46.52,13.744,46.514,13.831,46.511,13.929,46.498,14.02,46.482,14.1,46.462,14.267,46.441,14.42,46.428,14.466,46.416,14.504,46.417,14.55,46.4,14.577,46.413,14.597,46.436,14.68,46.463,14.757,46.499,14.811,46.545,14.841,46.58,14.893,46.606,14.949,46.613,15.001,46.626,15.217,46.643,15.439,46.63,15.545,46.655,15.633,46.698,15.76,46.711,15.767,46.711,15.958,46.678,15.972,46.697,15.98,46.706,15.977,46.801,16.037,46.845,16.093,46.863,16.284,46.857,16.308,46.828,16.318,46.783,16.335,46.722,16.367,46.705,16.385,46.681,16.381,46.639,16.418,46.607,16.506,46.522,16.516,46.5]]]},{"n":"Singapore","c":"SG","r":6,"ml":4,"lx":103.82,"ly":1.37,"b":[103.65,1.265,103.996,1.447],"p":[[[103.97,1.331,103.82,1.265,103.65,1.326,103.705,1.423,103.818,1.447,103.909,1.416,103.961,1.392,103.996,1.365,103.97,1.331]]]},{"n":"Sierra Leone","c":"SL","r":4,"ml":4,"lx":-11.76,"ly":8.62,"b":[-13.293,6.907,-10.283,9.997],"p":[[[-10.283,8.485,-10.286,8.454,-10.315,8.311,-10.36,8.188,-10.39,8.158,-10.517,8.125,-10.571,8.071,-10.618,7.896,-10.647,7.759,-10.691,7.736,-10.878,7.538,-11,7.463,-11.085,7.399,-11.166,7.314,-11.268,7.233,-11.377,7.095,-11.455,6.951,-11.508,6.907,-11.548,6.947,-11.733,7.089,-11.929,7.184,-12.347,7.342,-12.486,7.386,-12.481,7.442,-12.433,7.545,-12.51,7.666,-12.48,7.753,-12.51,7.753,-12.57,7.701,-12.698,7.716,-12.782,7.791,-12.851,7.819,-12.881,7.857,-12.925,8.055,-12.957,8.145,-13.021,8.201,-13.149,8.215,-13.202,8.336,-13.273,8.43,-13.261,8.488,-13.203,8.484,-13.158,8.442,-13.085,8.425,-12.994,8.526,-12.913,8.582,-12.894,8.63,-12.904,8.656,-12.953,8.615,-13.088,8.626,-13.122,8.589,-13.182,8.577,-13.228,8.696,-13.226,8.766,-13.207,8.843,-13.071,8.856,-13.059,8.881,-13.154,8.898,-13.272,8.987,-13.293,9.049,-13.234,9.07,-13.178,9.061,-13.13,9.048,-13.077,9.07,-13.028,9.104,-12.999,9.147,-12.959,9.263,-12.831,9.302,-12.756,9.374,-12.684,9.484,-12.652,9.562,-12.622,9.601,-12.604,9.634,-12.59,9.671,-12.558,9.705,-12.524,9.787,-12.501,9.862,-12.428,9.898,-12.278,9.93,-12.142,9.875,-11.923,9.923,-11.911,9.993,-11.71,9.994,-11.472,9.995,-11.274,9.997,-11.206,9.978,-11.181,9.925,-11.116,9.843,-11.047,9.786,-10.963,9.662,-10.865,9.516,-10.759,9.385,-10.691,9.314,-10.683,9.289,-10.688,9.261,-10.721,9.194,-10.75,9.122,-10.747,9.095,-10.727,9.082,-10.616,9.059,-10.606,8.979,-10.606,8.868,-10.552,8.764,-10.501,8.688,-10.503,8.66,-10.628,8.53,-10.677,8.401,-10.702,8.364,-10.712,8.335,-10.687,8.322,-10.653,8.33,-10.604,8.319,-10.558,8.316,-10.496,8.362,-10.394,8.481,-10.36,8.496,-10.283,8.485]],[[-12.526,7.436,-12.541,7.41,-12.607,7.475,-12.952,7.571,-12.854,7.622,-12.615,7.637,-12.544,7.607,-12.512,7.582,-12.501,7.535,-12.526,7.436]]]},{"n":"Seychelles","c":"SC","r":6,"ml":5,"lx":55.48,"ly":-4.68,"b":[55.383,-4.786,55.543,-4.559],"p":[[[55.54,-4.693,55.543,-4.786,55.495,-4.755,55.481,-4.695,55.417,-4.65,55.383,-4.609,55.456,-4.559,55.54,-4.693]]]},{"n":"Serbia","c":"RS","r":5,"ml":4,"lx":20.79,"ly":44.19,"b":[18.839,42.242,22.977,46.169],"p":[[[21.36,44.827,21.523,44.79,21.597,44.755,21.636,44.71,21.74,44.681,21.909,44.666,22.027,44.62,22.093,44.542,22.201,44.561,22.351,44.676,22.498,44.706,22.642,44.651,22.721,44.606,22.734,44.57,22.701,44.556,22.62,44.562,22.554,44.54,22.502,44.49,22.495,44.435,22.531,44.378,22.582,44.338,22.648,44.316,22.683,44.286,22.688,44.248,22.705,44.238,22.667,44.22,22.627,44.194,22.603,44.149,22.597,44.075,22.469,44.018,22.421,44.007,22.399,43.97,22.365,43.862,22.37,43.781,22.387,43.74,22.395,43.707,22.436,43.665,22.474,43.602,22.499,43.519,22.555,43.454,22.697,43.391,22.768,43.354,22.82,43.301,22.86,43.252,22.977,43.188,22.968,43.142,22.942,43.097,22.915,43.076,22.857,43.018,22.8,42.986,22.706,42.884,22.558,42.878,22.523,42.87,22.467,42.842,22.439,42.792,22.466,42.751,22.463,42.709,22.436,42.629,22.472,42.543,22.524,42.504,22.532,42.481,22.524,42.441,22.446,42.359,22.422,42.329,22.344,42.314,22.317,42.322,22.277,42.35,22.24,42.358,22.147,42.325,22.052,42.305,21.978,42.32,21.904,42.322,21.853,42.308,21.815,42.303,21.739,42.268,21.618,42.242,21.563,42.248,21.542,42.281,21.519,42.328,21.53,42.35,21.61,42.387,21.619,42.423,21.731,42.595,21.752,42.652,21.753,42.67,21.724,42.682,21.663,42.681,21.391,42.751,21.403,42.832,21.323,42.875,21.237,42.913,21.223,42.956,21.127,43.043,21.057,43.092,20.968,43.116,20.891,43.152,20.844,43.173,20.824,43.214,20.824,43.238,20.801,43.261,20.763,43.259,20.701,43.226,20.623,43.199,20.61,43.178,20.638,43.13,20.658,43.1,20.649,43.071,20.624,43.034,20.475,42.953,20.458,42.925,20.487,42.879,20.469,42.858,20.344,42.828,20.348,42.853,20.34,42.893,20.268,42.935,20.168,42.969,19.944,43.082,19.858,43.097,19.781,43.11,19.671,43.164,19.614,43.173,19.552,43.212,19.415,43.343,19.298,43.414,19.219,43.45,19.196,43.485,19.192,43.521,19.194,43.533,19.254,43.584,19.301,43.592,19.36,43.593,19.4,43.568,19.451,43.562,19.48,43.595,19.495,43.643,19.488,43.704,19.364,43.845,19.257,43.943,19.245,43.965,19.268,43.983,19.305,43.993,19.345,43.985,19.449,43.978,19.55,43.987,19.584,44.011,19.584,44.043,19.547,44.073,19.43,44.154,19.339,44.226,19.232,44.281,19.152,44.303,19.128,44.33,19.118,44.36,19.127,44.415,19.132,44.484,19.151,44.527,19.223,44.61,19.292,44.697,19.334,44.781,19.357,44.859,19.349,44.881,19.313,44.897,19.237,44.914,19.132,44.9,19.042,44.871,19.007,44.869,18.996,44.904,19.01,44.919,19.038,44.918,19.061,44.911,19.085,44.927,19.1,44.974,19.063,45.137,19.13,45.152,19.131,45.175,19.137,45.196,19.206,45.168,19.303,45.167,19.388,45.173,19.401,45.189,19.4,45.213,19.382,45.231,19.352,45.245,19.33,45.268,19.273,45.278,19.093,45.337,19.005,45.4,19.008,45.466,19.033,45.502,19.064,45.515,19.055,45.527,18.954,45.558,18.918,45.601,18.947,45.656,18.895,45.767,18.839,45.836,18.894,45.866,18.901,45.908,18.905,45.932,18.928,45.931,19.016,45.96,19.048,45.983,19.066,46.01,19.087,46.016,19.146,45.987,19.208,45.984,19.278,46.003,19.33,46.029,19.393,46.05,19.421,46.064,19.458,46.087,19.531,46.155,19.613,46.169,19.725,46.152,19.844,46.146,19.934,46.161,20.161,46.142,20.21,46.126,20.242,46.109,20.301,46.051,20.359,45.975,20.438,45.941,20.533,45.9,20.581,45.869,20.653,45.779,20.709,45.735,20.728,45.737,20.747,45.749,20.76,45.758,20.775,45.75,20.776,45.723,20.779,45.662,20.766,45.597,20.786,45.536,20.787,45.517,20.772,45.5,20.774,45.484,20.794,45.468,20.871,45.428,20.942,45.365,21.024,45.322,21.1,45.294,21.148,45.292,21.226,45.241,21.382,45.205,21.431,45.193,21.465,45.172,21.49,45.148,21.492,45.122,21.468,45.11,21.434,45.075,21.421,45.033,21.396,45.022,21.371,45.021,21.353,45.009,21.357,44.991,21.378,44.973,21.41,44.958,21.472,44.942,21.533,44.919,21.532,44.901,21.52,44.881,21.442,44.873,21.384,44.87,21.358,44.862,21.36,44.827]]]},{"n":"Senegal","c":"SN","r":3,"ml":2.7,"lx":-14.78,"ly":15.14,"b":[-17.536,12.328,-11.382,16.679],"p":[[[-12.281,14.809,-12.187,14.648,-12.207,14.571,-12.228,14.459,-12.175,14.377,-12.113,14.323,-12.068,14.274,-12.019,14.206,-12.011,14.072,-12.02,13.975,-11.988,13.931,-11.961,13.875,-11.966,13.829,-11.984,13.788,-12.044,13.734,-12.054,13.633,-11.957,13.511,-11.895,13.444,-11.895,13.406,-11.878,13.365,-11.832,13.316,-11.803,13.327,-11.772,13.367,-11.758,13.395,-11.674,13.382,-11.635,13.37,-11.581,13.29,-11.562,13.237,-11.549,13.17,-11.493,13.087,-11.444,13.028,-11.434,12.992,-11.39,12.942,-11.417,12.832,-11.414,12.775,-11.444,12.628,-11.451,12.558,-11.449,12.532,-11.382,12.479,-11.389,12.404,-11.457,12.418,-11.574,12.426,-11.808,12.387,-11.889,12.403,-12.042,12.398,-12.152,12.377,-12.291,12.328,-12.399,12.34,-12.457,12.378,-12.534,12.376,-12.621,12.396,-12.713,12.433,-12.797,12.452,-12.888,12.52,-12.931,12.532,-12.961,12.514,-12.986,12.492,-13.012,12.478,-13.061,12.49,-13.08,12.536,-13.064,12.581,-13.06,12.615,-13.083,12.634,-13.138,12.64,-13.228,12.64,-13.373,12.654,-13.406,12.662,-13.729,12.674,-14.065,12.675,-14.349,12.676,-14.708,12.678,-14.961,12.679,-15.196,12.68,-15.378,12.589,-15.575,12.49,-15.84,12.438,-16.144,12.457,-16.242,12.443,-16.342,12.4,-16.416,12.368,-16.521,12.349,-16.657,12.364,-16.712,12.355,-16.746,12.4,-16.785,12.473,-16.76,12.526,-16.678,12.56,-16.553,12.605,-16.488,12.582,-16.45,12.581,-16.443,12.609,-16.455,12.625,-16.549,12.664,-16.598,12.715,-16.638,12.685,-16.673,12.622,-16.701,12.603,-16.744,12.585,-16.768,12.628,-16.778,12.67,-16.759,12.702,-16.769,12.883,-16.757,12.98,-16.763,13.064,-16.705,13.12,-16.649,13.154,-16.431,13.157,-16.228,13.16,-16.033,13.158,-15.834,13.156,-15.814,13.325,-15.752,13.338,-15.657,13.356,-15.482,13.376,-15.286,13.396,-15.245,13.429,-15.212,13.485,-15.192,13.535,-15.151,13.556,-15.096,13.54,-15.025,13.513,-14.95,13.473,-14.865,13.435,-14.808,13.411,-14.672,13.352,-14.439,13.269,-14.247,13.236,-14.015,13.296,-13.848,13.335,-13.827,13.408,-13.853,13.479,-13.977,13.543,-14.147,13.536,-14.199,13.519,-14.278,13.497,-14.326,13.489,-14.405,13.504,-14.507,13.56,-14.571,13.616,-14.66,13.643,-14.766,13.669,-14.936,13.785,-15.024,13.806,-15.108,13.812,-15.27,13.789,-15.427,13.727,-15.51,13.586,-15.667,13.588,-16.002,13.593,-16.309,13.597,-16.562,13.587,-16.588,13.69,-16.648,13.771,-16.745,13.84,-16.767,13.905,-16.734,13.961,-16.64,14.007,-16.618,14.041,-16.667,14.036,-16.742,14.006,-16.792,14.004,-16.798,14.093,-16.881,14.208,-16.974,14.403,-17.079,14.483,-17.168,14.641,-17.261,14.701,-17.346,14.729,-17.418,14.723,-17.445,14.652,-17.536,14.755,-17.412,14.792,-17.147,14.922,-16.843,15.294,-16.571,15.734,-16.535,15.838,-16.502,15.917,-16.48,16.097,-16.441,16.205,-16.404,16.225,-16.358,16.307,-16.302,16.451,-16.239,16.531,-16.168,16.547,-16.113,16.54,-16.074,16.51,-15.959,16.492,-15.768,16.485,-15.621,16.507,-15.517,16.557,-15.38,16.582,-15.211,16.583,-15.121,16.604,-15.113,16.645,-15.091,16.657,-15.055,16.641,-15.022,16.647,-14.991,16.677,-14.96,16.679,-14.929,16.654,-14.787,16.646,-14.534,16.656,-14.3,16.58,-14.086,16.419,-13.975,16.311,-13.968,16.257,-13.933,16.203,-13.868,16.148,-13.81,16.138,-13.757,16.173,-13.715,16.169,-13.685,16.127,-13.624,16.118,-13.556,16.144,-13.507,16.135,-13.498,16.11,-13.487,16.097,-13.454,16.091,-13.41,16.059,-13.348,15.973,-13.297,15.854,-13.258,15.7,-13.206,15.617,-13.142,15.603,-13.105,15.572,-13.098,15.535,-13.079,15.51,-13.049,15.497,-12.994,15.505,-12.931,15.453,-12.863,15.34,-12.852,15.29,-12.863,15.262,-12.858,15.243,-12.813,15.224,-12.77,15.187,-12.735,15.131,-12.66,15.082,-12.544,15.039,-12.46,14.975,-12.409,14.889,-12.303,14.817,-12.281,14.809]]]},{"n":"Saudi Arabia","c":"SA","r":2,"ml":1.7,"lx":44.7,"ly":23.81,"b":[34.616,16.372,55.641,32.125],"p":[[[36.902,25.383,36.875,25.383,36.805,25.451,36.764,25.501,36.722,25.534,36.53,25.602,36.504,25.645,36.534,25.689,36.554,25.645,36.589,25.62,36.748,25.559,36.924,25.426,36.955,25.415,36.902,25.383]],[[41.988,16.716,42.065,16.71,42.033,16.742,42.026,16.758,42.06,16.804,42.17,16.709,42.167,16.596,42.158,16.571,42.128,16.595,42.108,16.618,42.102,16.644,42.072,16.671,41.964,16.653,41.897,16.684,41.802,16.779,41.776,16.847,41.816,16.86,41.858,16.893,41.885,16.947,41.86,17.003,41.917,16.994,41.948,16.936,41.954,16.806,41.963,16.779,41.947,16.749,41.988,16.716]],[[51.978,18.996,51.743,18.965,51.515,18.934,51.258,18.899,50.95,18.858,50.708,18.825,50.355,18.778,50.039,18.735,49.742,18.695,49.445,18.655,49.192,18.621,49.042,18.582,48.865,18.495,48.593,18.362,48.316,18.227,48.172,18.157,48.022,17.977,47.946,17.886,47.808,17.721,47.704,17.597,47.58,17.448,47.525,17.316,47.442,17.112,47.37,17.06,47.251,16.994,47.144,16.947,46.976,16.953,46.88,17.079,46.779,17.212,46.728,17.266,46.682,17.269,46.513,17.252,46.31,17.231,46.071,17.253,45.794,17.278,45.535,17.302,45.407,17.32,45.237,17.406,45.193,17.423,45.148,17.427,44.946,17.43,44.747,17.432,44.546,17.404,44.355,17.414,44.156,17.399,44.086,17.366,44.008,17.367,43.96,17.338,43.917,17.325,43.866,17.35,43.804,17.344,43.713,17.366,43.653,17.422,43.597,17.471,43.539,17.499,43.474,17.516,43.418,17.516,43.346,17.486,43.302,17.457,43.191,17.359,43.186,17.325,43.237,17.266,43.221,17.239,43.156,17.205,43.136,17.113,43.126,17.062,43.117,16.942,43.146,16.847,43.184,16.812,43.186,16.771,43.165,16.689,43.105,16.664,43.061,16.587,43.034,16.55,42.986,16.509,42.799,16.372,42.79,16.452,42.731,16.57,42.726,16.653,42.699,16.737,42.647,16.801,42.553,16.868,42.544,16.96,42.475,17.05,42.383,17.122,42.332,17.257,42.294,17.435,42.052,17.669,41.75,17.886,41.658,18.008,41.508,18.256,41.432,18.452,41.229,18.678,41.221,18.765,41.191,18.871,41.144,18.989,41.116,19.082,40.913,19.49,40.848,19.555,40.792,19.646,40.777,19.717,40.759,19.755,40.616,19.822,40.482,19.993,40.081,20.266,39.884,20.293,39.728,20.39,39.614,20.518,39.491,20.737,39.276,20.974,39.094,21.31,39.151,21.433,39.147,21.519,39.091,21.664,39.03,21.776,38.988,21.882,39.021,22.033,39.034,22.203,39.07,22.294,39.096,22.393,39.062,22.592,39.001,22.699,39.007,22.77,38.939,22.805,38.883,22.882,38.941,22.882,38.836,22.989,38.797,23.049,38.757,23.194,38.706,23.306,38.542,23.558,38.464,23.712,38.289,23.911,38.099,24.058,37.978,24.125,37.92,24.185,37.821,24.188,37.713,24.274,37.638,24.278,37.543,24.292,37.431,24.459,37.338,24.616,37.181,24.82,37.22,24.873,37.266,24.96,37.243,25.073,37.218,25.151,37.149,25.291,36.921,25.641,36.86,25.692,36.763,25.751,36.703,25.903,36.675,26.039,36.519,26.105,36.25,26.595,36.094,26.766,36.032,26.881,35.852,27.07,35.763,27.259,35.581,27.432,35.424,27.734,35.18,28.035,35.078,28.087,34.828,28.109,34.722,28.131,34.625,28.065,34.616,28.148,34.683,28.264,34.78,28.507,34.799,28.721,34.951,29.354,35.164,29.321,35.339,29.294,35.595,29.255,35.86,29.214,36.015,29.19,36.068,29.201,36.283,29.355,36.476,29.495,36.592,29.666,36.704,29.832,36.755,29.866,36.927,29.897,37.199,29.946,37.469,29.995,37.491,30.012,37.554,30.145,37.634,30.313,37.65,30.331,37.67,30.348,37.863,30.443,37.98,30.5,37.813,30.669,37.655,30.829,37.479,31.008,37.329,31.147,37.105,31.355,36.959,31.492,37.216,31.556,37.493,31.626,37.774,31.696,38.111,31.781,38.375,31.847,38.77,31.946,38.962,31.995,38.997,32.007,39.145,32.125,39.369,32.092,39.704,32.043,40.028,31.995,40.369,31.939,40.479,31.893,40.808,31.725,41.022,31.616,41.272,31.489,41.585,31.33,41.8,31.22,42.074,31.08,42.289,30.92,42.56,30.718,42.858,30.495,43.103,30.322,43.441,30.084,43.774,29.849,44.1,29.619,44.361,29.435,44.691,29.202,44.717,29.194,45.05,29.167,45.499,29.132,45.95,29.096,46.356,29.064,46.531,29.096,46.725,29.075,46.982,29.046,47.139,29.026,47.433,28.99,47.521,28.838,47.553,28.732,47.583,28.628,47.671,28.533,47.872,28.535,48.05,28.538,48.269,28.541,48.442,28.543,48.499,28.449,48.523,28.355,48.626,28.133,48.774,27.959,48.809,27.896,48.833,27.801,48.807,27.765,48.797,27.724,48.906,27.629,49.087,27.549,49.158,27.528,49.238,27.493,49.175,27.438,49.282,27.31,49.405,27.181,49.538,27.152,49.717,26.956,49.986,26.829,50.15,26.663,50.135,26.66,50.087,26.676,50.026,26.699,50.008,26.679,50.011,26.609,50.027,26.527,50.111,26.456,50.185,26.405,50.214,26.308,50.155,26.101,50.135,26.101,50.096,26.119,50.054,26.123,50.032,26.111,50.081,25.961,50.13,25.847,50.19,25.756,50.239,25.623,50.281,25.566,50.455,25.425,50.508,25.307,50.558,25.087,50.667,24.964,50.726,24.869,50.804,24.789,50.856,24.68,50.928,24.595,50.966,24.574,51.023,24.565,51.093,24.565,51.178,24.587,51.268,24.607,51.338,24.564,51.411,24.571,51.418,24.531,51.37,24.477,51.31,24.34,51.395,24.319,51.477,24.308,51.535,24.286,51.568,24.286,51.568,24.258,51.572,24.128,51.593,24.079,51.629,24.035,51.684,23.97,51.739,23.904,51.794,23.838,51.849,23.773,51.904,23.708,51.959,23.642,52.014,23.576,52.069,23.511,52.125,23.445,52.179,23.38,52.235,23.314,52.29,23.249,52.345,23.183,52.4,23.118,52.455,23.052,52.51,22.987,52.555,22.933,52.639,22.923,52.666,22.919,52.742,22.91,52.859,22.896,53.012,22.877,53.192,22.855,53.394,22.83,53.61,22.804,53.832,22.777,54.055,22.75,54.27,22.723,54.472,22.699,54.652,22.677,54.805,22.658,54.922,22.644,54.998,22.634,55.025,22.631,55.104,22.621,55.119,22.624,55.186,22.704,55.259,22.591,55.32,22.497,55.404,22.368,55.493,22.231,55.578,22.1,55.641,22.002,55.607,21.9,55.571,21.79,55.534,21.679,55.498,21.568,55.461,21.458,55.424,21.347,55.388,21.236,55.351,21.126,55.314,21.015,55.278,20.904,55.241,20.793,55.205,20.683,55.168,20.572,55.131,20.461,55.095,20.351,55.058,20.24,55.021,20.129,54.977,19.996,54.871,19.96,54.699,19.903,54.527,19.846,54.355,19.788,54.183,19.731,54.011,19.674,53.839,19.617,53.667,19.559,53.495,19.502,53.323,19.444,53.151,19.387,52.979,19.33,52.807,19.272,52.635,19.215,52.463,19.158,52.291,19.1,52.119,19.043,51.978,18.996]],[[36.596,25.713,36.586,25.699,36.544,25.734,36.546,25.812,36.583,25.856,36.58,25.795,36.596,25.735,36.596,25.713]]]},{"n":"São Tomé and Príncipe","c":"ST","r":6,"ml":5,"lx":7.02,"ly":0.97,"b":[6.468,0.047,7.452,1.699],"p":[[[7.424,1.568,7.387,1.542,7.342,1.564,7.331,1.603,7.388,1.68,7.414,1.699,7.437,1.683,7.45,1.662,7.452,1.631,7.424,1.568]],[[6.66,0.121,6.557,0.047,6.52,0.066,6.497,0.117,6.468,0.227,6.478,0.28,6.524,0.34,6.626,0.4,6.687,0.404,6.75,0.326,6.75,0.243,6.66,0.121]]]},{"n":"San Marino","c":"SM","r":6,"ml":5,"lx":12.44,"ly":43.93,"b":[12.397,43.894,12.515,43.99],"p":[[[12.485,43.901,12.426,43.894,12.397,43.935,12.441,43.982,12.504,43.99,12.515,43.953,12.485,43.901]]]},{"n":"Samoa","c":"WS","r":4,"ml":3,"lx":-172.44,"ly":-13.64,"b":[-172.779,-14.047,-171.45,-13.465],"p":[[[-172.333,-13.465,-172.222,-13.56,-172.177,-13.685,-172.225,-13.804,-172.331,-13.775,-172.485,-13.8,-172.536,-13.792,-172.659,-13.645,-172.744,-13.579,-172.779,-13.517,-172.67,-13.524,-172.511,-13.483,-172.333,-13.465]],[[-171.454,-14.046,-171.728,-14.047,-171.864,-14.002,-171.912,-14.002,-172.028,-13.907,-172.046,-13.857,-171.985,-13.824,-171.858,-13.807,-171.604,-13.879,-171.565,-13.943,-171.507,-13.95,-171.461,-13.978,-171.45,-14.022,-171.454,-14.046]]]},{"n":"Saint Vincent and the Grenadines","c":"VC","r":6,"ml":5,"lx":-61.34,"ly":13.09,"b":[-61.354,12.695,-61.124,13.359],"p":[[[-61.175,13.158,-61.204,13.142,-61.277,13.21,-61.268,13.288,-61.224,13.331,-61.182,13.356,-61.139,13.359,-61.124,13.294,-61.135,13.203,-61.175,13.158]],[[-61.226,12.995,-61.242,12.984,-61.235,12.984,-61.242,12.976,-61.247,12.984,-61.255,12.988,-61.265,12.99,-61.277,12.99,-61.277,12.997,-61.262,12.993,-61.239,12.997,-61.239,13.005,-61.246,13.013,-61.249,13.018,-61.241,13.026,-61.213,13.043,-61.201,13.053,-61.2,13.049,-61.199,13.045,-61.198,13.042,-61.194,13.038,-61.208,13.025,-61.226,12.995]],[[-61.334,12.695,-61.345,12.695,-61.354,12.698,-61.351,12.701,-61.34,12.704,-61.334,12.71,-61.336,12.719,-61.336,12.729,-61.327,12.735,-61.32,12.735,-61.317,12.732,-61.315,12.723,-61.32,12.716,-61.326,12.71,-61.329,12.701,-61.334,12.695]]]},{"n":"Saint Lucia","c":"LC","r":6,"ml":5,"lx":-60.98,"ly":13.89,"b":[-61.073,13.718,-60.887,14.093],"p":[[[-60.895,13.822,-60.951,13.718,-61.061,13.783,-61.073,13.866,-61.064,13.916,-60.997,14.011,-60.945,14.073,-60.908,14.093,-60.887,14.011,-60.895,13.822]]]},{"n":"Saint Kitts and Nevis","c":"KN","r":6,"ml":5,"lx":-62.76,"ly":17.34,"b":[-62.84,17.101,-62.532,17.403],"p":[[[-62.532,17.122,-62.582,17.101,-62.625,17.13,-62.615,17.199,-62.575,17.201,-62.534,17.17,-62.532,17.122]],[[-62.631,17.24,-62.656,17.224,-62.702,17.286,-62.776,17.303,-62.839,17.339,-62.84,17.347,-62.839,17.365,-62.827,17.386,-62.795,17.403,-62.714,17.353,-62.676,17.291,-62.641,17.262,-62.631,17.24]]]},{"n":"Rwanda","c":"RW","r":3,"ml":3,"lx":30.1,"ly":-1.9,"b":[28.858,-2.809,30.877,-1.063],"p":[[[29.577,-1.388,29.61,-1.387,29.825,-1.336,29.847,-1.352,29.882,-1.452,29.9,-1.466,29.93,-1.47,29.991,-1.447,30.102,-1.369,30.15,-1.321,30.207,-1.254,30.28,-1.179,30.321,-1.113,30.36,-1.075,30.412,-1.063,30.47,-1.066,30.51,-1.067,30.477,-1.083,30.47,-1.131,30.508,-1.208,30.632,-1.367,30.711,-1.397,30.762,-1.459,30.813,-1.563,30.828,-1.694,30.807,-1.851,30.819,-1.967,30.865,-2.044,30.877,-2.143,30.855,-2.265,30.829,-2.338,30.798,-2.363,30.763,-2.372,30.715,-2.363,30.657,-2.374,30.593,-2.397,30.554,-2.4,30.529,-2.396,30.482,-2.376,30.408,-2.313,30.271,-2.348,30.234,-2.347,30.183,-2.377,30.142,-2.414,30.117,-2.417,30.092,-2.412,29.973,-2.337,29.93,-2.34,29.912,-2.549,29.893,-2.665,29.868,-2.716,29.783,-2.766,29.698,-2.795,29.651,-2.793,29.464,-2.808,29.39,-2.809,29.35,-2.792,29.297,-2.673,29.198,-2.62,29.102,-2.596,29.063,-2.603,29.029,-2.665,29.014,-2.72,28.922,-2.682,28.894,-2.635,28.891,-2.556,28.858,-2.447,28.876,-2.4,28.913,-2.37,28.99,-2.313,29.106,-2.233,29.132,-2.195,29.148,-2.132,29.141,-1.985,29.129,-1.86,29.143,-1.816,29.197,-1.72,29.268,-1.622,29.352,-1.518,29.402,-1.507,29.468,-1.468,29.538,-1.41,29.577,-1.388]]]},{"n":"Russia","c":"RU","r":2,"ml":1.7,"lx":44.69,"ly":58.25,"b":[-180,41.199,180,81.854],"p":[[[145.882,43.46,145.896,43.455,145.914,43.455,145.931,43.457,145.941,43.445,145.944,43.426,145.931,43.426,145.907,43.422,145.894,43.42,145.887,43.433,145.882,43.444,145.869,43.45,145.869,43.458,145.882,43.46]],[[146.359,43.625,146.332,43.62,146.288,43.625,146.274,43.63,146.284,43.639,146.31,43.652,146.333,43.647,146.35,43.644,146.359,43.625]],[[146.046,43.409,146.032,43.407,146.028,43.42,146.049,43.434,146.089,43.449,146.101,43.44,146.086,43.429,146.07,43.421,146.046,43.409]],[[137.179,55.1,137.055,54.927,136.969,54.924,136.903,54.961,136.765,54.946,136.715,54.956,136.795,55.009,136.996,55.093,137.078,55.092,137.156,55.108,137.179,55.1]],[[150.59,59.019,150.511,59.007,150.472,59.035,150.47,59.054,150.592,59.097,150.666,59.16,150.713,59.122,150.728,59.095,150.59,59.019]],[[120.261,73.09,120.008,73.045,119.792,73.045,119.64,73.124,119.762,73.155,119.964,73.168,120.079,73.157,120.237,73.107,120.261,73.09]],[[124.543,73.85,124.482,73.848,124.366,73.875,124.336,73.91,124.337,73.928,124.43,73.943,124.548,73.934,124.637,73.9,124.653,73.888,124.543,73.85]],[[106.27,78.206,106.151,78.199,106.024,78.22,106.058,78.265,106.351,78.273,106.457,78.34,106.64,78.336,106.691,78.317,106.72,78.294,106.719,78.265,106.679,78.265,106.505,78.262,106.472,78.245,106.27,78.206]],[[107.415,77.243,107.302,77.242,107.27,77.289,107.366,77.347,107.486,77.347,107.594,77.33,107.629,77.32,107.665,77.3,107.679,77.268,107.415,77.243]],[[97.588,76.599,97.535,76.584,97.43,76.591,97.342,76.629,97.31,76.69,97.382,76.707,97.588,76.599]],[[96.854,76.199,96.798,76.188,96.754,76.196,96.739,76.207,96.74,76.258,96.833,76.324,96.835,76.345,96.878,76.355,96.99,76.343,97.045,76.315,97.053,76.303,96.974,76.237,96.854,76.199]],[[100.136,79.614,99.915,79.602,99.942,79.671,99.956,79.69,100.068,79.701,100.142,79.684,100.3,79.67,100.136,79.614]],[[76.756,73.446,76.659,73.44,76.234,73.476,76.083,73.523,76.14,73.554,76.251,73.555,76.756,73.446]],[[82.71,74.091,82.613,74.056,82.478,74.076,82.382,74.099,82.329,74.131,82.382,74.149,82.526,74.161,82.611,74.149,82.689,74.111,82.71,74.091]],[[84.759,74.459,84.71,74.4,84.429,74.43,84.389,74.454,84.54,74.49,84.68,74.512,84.873,74.516,84.759,74.459]],[[86.653,74.981,86.737,74.963,87.001,74.992,87.052,74.983,87.124,74.94,87.012,74.862,86.927,74.831,86.692,74.848,86.391,74.851,86.259,74.894,86.331,74.939,86.504,74.966,86.605,74.993,86.653,74.981]],[[59.313,81.305,59.097,81.292,58.719,81.314,58.61,81.337,58.634,81.36,58.881,81.392,59.075,81.398,59.281,81.366,59.375,81.325,59.313,81.305]],[[-179.799,68.94,-179.595,68.906,-179.515,68.917,-179.471,68.912,-179.356,68.853,-179.279,68.825,-178.874,68.754,-178.689,68.675,-178.539,68.586,-178.614,68.603,-178.751,68.66,-178.737,68.593,-178.693,68.546,-178.474,68.502,-178.244,68.467,-178.097,68.425,-178.049,68.388,-178.019,68.323,-178.056,68.265,-177.922,68.287,-177.797,68.338,-177.862,68.378,-178.285,68.519,-178.373,68.566,-178.25,68.541,-177.683,68.363,-177.527,68.294,-177.593,68.281,-177.639,68.241,-177.589,68.224,-177.521,68.237,-177.408,68.245,-177.297,68.223,-177.172,68.175,-176.907,68.119,-175.345,67.678,-175.31,67.602,-175.266,67.567,-175.24,67.521,-175.233,67.447,-175.375,67.357,-175.155,67.365,-175.123,67.377,-175.066,67.413,-175.003,67.438,-174.918,67.408,-174.85,67.349,-174.87,67.269,-174.93,67.203,-174.938,67.093,-174.885,67,-174.829,66.961,-174.784,66.917,-174.771,66.784,-174.87,66.725,-174.925,66.623,-174.864,66.613,-174.675,66.603,-174.612,66.585,-174.504,66.538,-174.478,66.492,-174.454,66.43,-174.419,66.372,-174.394,66.344,-174.366,66.348,-174.257,66.428,-174.206,66.452,-174.085,66.473,-174.018,66.383,-174.065,66.23,-174.025,66.23,-173.994,66.246,-173.955,66.287,-173.9,66.31,-173.832,66.366,-173.774,66.435,-173.843,66.488,-173.921,66.522,-174.102,66.541,-174.196,66.581,-174.232,66.632,-174.14,66.653,-174.061,66.69,-174.006,66.779,-174.019,66.827,-174.041,66.875,-174.086,66.943,-174.154,66.982,-174.284,67.002,-174.342,67.04,-174.431,67.038,-174.519,67.049,-174.554,67.063,-174.55,67.091,-174.448,67.103,-173.884,67.106,-173.68,67.145,-173.587,67.133,-173.494,67.105,-173.158,67.069,-173.168,67.052,-173.224,67.035,-173.324,66.955,-173.343,66.909,-173.347,66.851,-173.259,66.84,-173.175,66.865,-173.216,66.911,-173.228,66.969,-173.193,66.994,-173.147,66.999,-173.058,66.956,-172.963,66.942,-172.641,66.925,-172.549,66.931,-172.52,66.952,-172.583,66.978,-173.002,67.034,-173.008,67.065,-172.621,67.027,-172.447,66.992,-172.274,66.966,-172.031,66.973,-171.796,66.932,-171.57,66.819,-171.36,66.677,-171.149,66.593,-170.927,66.53,-170.556,66.357,-170.51,66.344,-170.473,66.32,-170.543,66.291,-170.604,66.249,-170.483,66.278,-170.361,66.298,-170.301,66.294,-170.247,66.272,-170.212,66.236,-170.192,66.201,-170.244,66.169,-169.889,66.163,-169.778,66.143,-169.729,66.058,-169.832,65.999,-169.892,66.006,-169.949,66.031,-170.004,66.033,-170.159,66.008,-170.401,65.929,-170.541,65.865,-170.563,65.824,-170.541,65.71,-170.561,65.656,-170.666,65.622,-170.897,65.643,-171.001,65.665,-171.119,65.695,-171.232,65.737,-171.377,65.804,-171.422,65.81,-171.451,65.794,-171.402,65.752,-171.303,65.698,-171.134,65.628,-171.054,65.55,-171.106,65.511,-171.17,65.502,-171.216,65.503,-171.364,65.527,-171.466,65.533,-171.79,65.51,-171.907,65.496,-171.947,65.508,-171.957,65.542,-172.131,65.567,-172.234,65.57,-172.282,65.582,-172.322,65.618,-172.436,65.67,-172.608,65.69,-172.719,65.692,-172.783,65.681,-172.557,65.612,-172.354,65.496,-172.392,65.475,-172.418,65.45,-172.306,65.448,-172.233,65.456,-172.212,65.425,-172.27,65.303,-172.309,65.276,-172.662,65.249,-172.573,65.228,-172.482,65.222,-172.379,65.227,-172.286,65.206,-172.224,65.129,-172.213,65.048,-172.304,65.002,-172.399,64.965,-172.593,64.908,-172.792,64.883,-172.897,64.889,-172.999,64.877,-173.066,64.847,-173.086,64.817,-172.998,64.837,-172.897,64.826,-172.801,64.791,-172.812,64.761,-172.903,64.729,-172.924,64.705,-172.889,64.664,-172.901,64.629,-172.854,64.61,-172.747,64.603,-172.616,64.578,-172.487,64.544,-172.437,64.515,-172.394,64.475,-172.379,64.432,-172.401,64.414,-172.695,64.407,-172.739,64.412,-172.756,64.46,-172.792,64.499,-172.903,64.526,-172.949,64.507,-172.916,64.369,-172.96,64.328,-173.009,64.297,-173.157,64.28,-173.275,64.29,-173.376,64.355,-173.376,64.41,-173.309,64.443,-173.309,64.487,-173.327,64.54,-173.396,64.479,-173.475,64.429,-173.604,64.365,-173.666,64.357,-173.73,64.365,-173.898,64.41,-174.001,64.449,-174.205,64.578,-174.318,64.638,-174.571,64.718,-174.83,64.776,-175.036,64.814,-175.146,64.809,-175.256,64.794,-175.395,64.802,-175.442,64.817,-175.483,64.849,-175.521,64.867,-175.716,64.946,-175.854,65.011,-175.859,65.054,-175.83,65.106,-175.856,65.233,-175.923,65.352,-176.093,65.471,-176.547,65.548,-176.922,65.601,-177.056,65.614,-177.175,65.602,-177.489,65.504,-177.699,65.49,-178.31,65.485,-178.412,65.496,-178.505,65.537,-178.526,65.593,-178.499,65.697,-178.502,65.74,-178.526,65.755,-178.559,65.754,-178.679,65.795,-178.791,65.865,-178.879,65.936,-178.939,66.033,-178.858,66.038,-178.747,66.014,-178.731,66.037,-178.694,66.124,-178.616,66.166,-178.587,66.198,-178.534,66.317,-178.527,66.402,-178.616,66.355,-178.753,66.237,-178.821,66.203,-178.868,66.187,-178.916,66.18,-179.026,66.204,-179.105,66.232,-179.107,66.346,-179.143,66.375,-179.178,66.353,-179.193,66.313,-179.293,66.305,-179.34,66.288,-179.316,66.22,-179.327,66.163,-179.423,66.141,-179.616,66.128,-179.683,66.184,-179.741,66.106,-179.784,66.018,-179.79,65.901,-179.728,65.804,-179.641,65.758,-179.449,65.688,-179.366,65.639,-179.344,65.575,-179.352,65.517,-179.452,65.445,-179.519,65.386,-179.635,65.244,-179.705,65.187,-180,65.067,-180,65.312,-180,65.557,-180,65.802,-180,66.046,-180,66.291,-180,66.536,-180,66.781,-180,67.025,-180,67.27,-180,67.515,-180,67.76,-180,68.004,-180,68.249,-180,68.494,-180,68.739,-180,68.983,-179.799,68.94]],[[130.687,42.303,130.658,42.328,130.652,42.373,130.618,42.416,130.554,42.475,130.527,42.535,130.584,42.567,130.577,42.623,130.521,42.674,130.439,42.686,130.42,42.7,130.425,42.727,130.453,42.755,130.493,42.779,130.577,42.812,130.722,42.836,130.803,42.857,130.869,42.863,130.943,42.852,131.006,42.883,131.069,42.902,131.083,42.956,131.086,43.038,131.109,43.062,131.136,43.098,131.176,43.142,131.212,43.258,131.239,43.338,131.257,43.378,131.262,43.433,131.244,43.469,131.209,43.49,131.182,43.506,131.18,43.567,131.184,43.651,131.174,43.705,131.213,44.003,131.255,44.072,131.126,44.469,131.087,44.596,131.061,44.66,131.004,44.753,130.968,44.8,130.982,44.844,131.033,44.889,131.082,44.91,131.228,44.92,131.268,44.936,131.447,44.984,131.488,45.013,131.579,45.084,131.614,45.137,131.654,45.205,131.742,45.243,131.795,45.305,131.852,45.327,131.909,45.274,131.978,45.244,132.067,45.226,132.181,45.203,132.363,45.16,132.549,45.123,132.666,45.094,132.723,45.081,132.839,45.061,132.889,45.046,132.936,45.03,133.012,45.075,133.113,45.131,133.097,45.22,133.113,45.321,133.186,45.495,133.267,45.545,133.31,45.553,133.355,45.572,133.436,45.605,133.466,45.651,133.449,45.705,133.476,45.758,133.485,45.81,133.513,45.879,133.551,45.898,133.608,45.92,133.648,45.955,133.686,46.009,133.711,46.07,133.701,46.14,133.75,46.186,133.833,46.224,133.861,46.248,133.875,46.309,133.88,46.336,133.903,46.367,133.887,46.431,133.867,46.499,133.958,46.614,134.023,46.713,134.039,46.858,134.046,46.882,134.071,46.951,134.086,46.978,134.137,47.069,134.202,47.128,134.189,47.194,134.163,47.259,134.168,47.302,134.225,47.353,134.26,47.378,134.291,47.414,134.339,47.429,134.383,47.438,134.483,47.447,134.542,47.485,134.596,47.524,134.696,47.625,134.728,47.684,134.752,47.715,134.699,47.801,134.65,47.874,134.591,47.975,134.566,48.023,134.605,48.083,134.647,48.12,134.669,48.153,134.681,48.21,134.665,48.254,134.564,48.322,134.456,48.355,134.335,48.369,134.293,48.373,134.206,48.36,133.842,48.274,133.672,48.208,133.573,48.133,133.468,48.097,133.301,48.102,133.144,48.106,133.02,48.064,132.877,47.979,132.773,47.94,132.707,47.947,132.637,47.89,132.562,47.769,132.476,47.715,132.38,47.729,132.15,47.718,131.785,47.681,131.557,47.682,131.464,47.723,131.319,47.728,131.122,47.698,131.003,47.691,130.962,47.709,130.933,47.76,130.915,47.843,130.849,47.929,130.733,48.019,130.712,48.128,130.787,48.255,130.804,48.342,130.763,48.388,130.747,48.43,130.659,48.483,130.597,48.575,130.552,48.602,130.566,48.68,130.617,48.773,130.553,48.861,130.355,48.866,130.196,48.892,130.037,48.972,129.793,49.199,129.671,49.279,129.591,49.287,129.534,49.323,129.498,49.389,129.441,49.389,129.385,49.389,129.35,49.362,129.31,49.354,129.248,49.379,129.185,49.381,129.12,49.362,129.065,49.375,129.02,49.419,128.938,49.449,128.819,49.464,128.77,49.495,128.791,49.542,128.769,49.577,128.704,49.6,128.527,49.594,128.237,49.559,128,49.569,127.814,49.622,127.711,49.672,127.69,49.717,127.637,49.76,127.551,49.802,127.502,49.873,127.492,49.975,127.512,50.072,127.59,50.209,127.395,50.299,127.337,50.35,127.351,50.394,127.341,50.428,127.306,50.454,127.308,50.494,127.347,50.55,127.347,50.621,127.307,50.708,127.198,50.829,127.02,50.986,126.925,51.1,126.912,51.172,126.888,51.23,126.854,51.261,126.834,51.315,126.848,51.374,126.827,51.412,126.802,51.448,126.805,51.506,126.775,51.545,126.709,51.566,126.689,51.61,126.701,51.703,126.654,51.781,126.511,51.926,126.468,52.031,126.456,52.126,126.395,52.173,126.392,52.215,126.383,52.287,126.346,52.306,126.324,52.332,126.342,52.362,126.313,52.4,126.238,52.445,126.203,52.484,126.194,52.519,126.157,52.547,126.046,52.573,126.016,52.61,126.023,52.643,126.047,52.673,126.06,52.692,126.056,52.716,126.048,52.739,126.004,52.768,125.942,52.801,125.872,52.872,125.783,52.891,125.728,52.891,125.681,52.931,125.695,52.956,125.692,53.004,125.649,53.042,125.596,53.057,125.546,53.048,125.422,53.084,125.226,53.166,125.075,53.204,124.971,53.197,124.907,53.173,124.882,53.13,124.812,53.134,124.64,53.211,124.466,53.23,124.369,53.271,124.291,53.341,124.22,53.37,124.154,53.359,123.995,53.406,123.741,53.511,123.608,53.547,123.56,53.527,123.535,53.526,123.489,53.529,123.424,53.531,123.31,53.556,123.154,53.545,122.958,53.498,122.745,53.469,122.516,53.457,122.38,53.463,122.338,53.485,122.089,53.451,121.744,53.384,121.405,53.317,120.985,53.285,120.704,53.172,120.421,52.968,120.218,52.84,120.095,52.787,120.044,52.718,120.068,52.633,120.173,52.602,120.36,52.627,120.521,52.615,120.656,52.567,120.699,52.494,120.65,52.396,120.665,52.3,120.745,52.205,120.75,52.097,120.681,51.973,120.511,51.849,120.237,51.723,120.067,51.601,119.967,51.422,119.813,51.267,119.757,51.179,119.746,51.108,119.685,51.03,119.573,50.947,119.512,50.863,119.502,50.779,119.446,50.703,119.344,50.634,119.281,50.561,119.256,50.484,119.217,50.433,119.164,50.406,119.192,50.38,119.302,50.354,119.346,50.279,119.326,50.155,119.26,50.066,119.147,50.013,118.979,49.979,118.756,49.963,118.452,49.844,118.187,49.693,117.873,49.513,117.813,49.514,117.698,49.536,117.477,49.609,117.246,49.625,117.022,49.693,116.889,49.738,116.683,49.824,116.632,49.877,116.551,49.92,116.351,49.978,116.217,50.009,116.135,50.011,115.926,49.952,115.795,49.906,115.718,49.881,115.588,49.886,115.429,49.896,115.365,49.912,115.275,49.949,115.098,50.059,115.003,50.139,114.88,50.183,114.743,50.234,114.675,50.246,114.554,50.241,114.386,50.255,114.297,50.274,114.222,50.257,114.071,50.205,113.881,50.101,113.732,50.062,113.574,50.007,113.446,49.942,113.319,49.874,113.164,49.797,113.092,49.693,113.056,49.616,112.915,49.569,112.806,49.524,112.697,49.507,112.495,49.532,112.375,49.515,112.08,49.424,111.934,49.416,111.833,49.404,111.736,49.398,111.575,49.376,111.512,49.361,111.429,49.343,111.337,49.356,111.204,49.304,110.828,49.166,110.71,49.143,110.631,49.138,110.53,49.187,110.428,49.22,110.321,49.216,110.2,49.17,109.995,49.206,109.75,49.239,109.529,49.27,109.454,49.296,109.237,49.335,108.92,49.335,108.733,49.336,108.614,49.323,108.522,49.342,108.407,49.396,108.213,49.525,108.098,49.563,108.034,49.594,108.01,49.647,107.965,49.654,107.937,49.691,107.939,49.741,107.935,49.849,107.948,49.925,107.917,49.948,107.787,49.96,107.631,49.983,107.347,49.987,107.233,49.989,107.143,50.033,107.04,50.086,106.941,50.197,106.854,50.248,106.711,50.313,106.574,50.329,106.368,50.318,106.218,50.305,106.083,50.333,105.996,50.368,105.875,50.405,105.693,50.414,105.542,50.441,105.384,50.474,105.267,50.46,105.186,50.43,105.095,50.39,104.977,50.383,104.685,50.342,104.596,50.317,104.466,50.306,104.354,50.275,104.26,50.214,104.18,50.169,104.079,50.154,103.958,50.157,103.856,50.172,103.803,50.176,103.723,50.154,103.633,50.139,103.496,50.165,103.421,50.187,103.304,50.2,103.234,50.264,103.162,50.291,103.039,50.301,102.86,50.333,102.765,50.367,102.683,50.387,102.546,50.461,102.469,50.526,102.407,50.536,102.336,50.544,102.288,50.585,102.286,50.635,102.303,50.666,102.317,50.718,102.277,50.769,102.235,50.791,102.215,50.829,102.226,50.901,102.21,50.974,102.195,51.051,102.152,51.108,102.142,51.216,102.16,51.261,102.156,51.314,102.112,51.353,101.979,51.382,101.821,51.421,101.571,51.467,101.464,51.471,101.381,51.453,101.304,51.475,101.223,51.513,101.085,51.553,100.904,51.604,100.711,51.662,100.536,51.713,100.469,51.726,100.23,51.73,100.035,51.737,99.922,51.756,99.788,51.828,99.719,51.872,99.613,51.893,99.532,51.9,99.407,51.924,99.176,51.999,99.091,52.035,99.034,52.035,98.958,52.102,98.893,52.117,98.849,52.07,98.803,51.957,98.76,51.905,98.641,51.801,98.353,51.718,98.303,51.674,98.277,51.635,98.238,51.578,98.22,51.506,98.185,51.486,98.103,51.484,98.038,51.45,97.989,51.377,97.947,51.348,97.923,51.28,97.927,51.251,97.918,51.218,97.911,51.165,97.836,51.052,97.825,50.985,97.856,50.943,97.92,50.887,97.953,50.855,97.964,50.818,97.962,50.769,98.001,50.702,98.03,50.645,98.079,50.604,98.145,50.569,98.221,50.557,98.279,50.533,98.293,50.487,98.277,50.423,98.25,50.302,98.2,50.228,98.17,50.181,98.122,50.107,98.103,50.078,98.004,50.014,97.937,49.997,97.854,49.947,97.786,49.945,97.721,49.945,97.651,49.934,97.589,49.911,97.541,49.843,97.418,49.773,97.36,49.741,97.209,49.731,97.137,49.762,97.098,49.805,97.049,49.83,96.986,49.883,96.712,49.912,96.64,49.898,96.598,49.878,96.543,49.893,96.506,49.919,96.466,49.912,96.381,49.896,96.315,49.901,96.23,49.954,96.112,49.982,96.066,49.999,96.019,49.999,95.99,49.974,95.936,49.96,95.899,49.991,95.852,50.013,95.789,50.013,95.708,49.966,95.567,49.944,95.523,49.911,95.442,49.916,95.386,49.941,95.329,49.944,95.166,49.944,95.111,49.935,95.044,49.962,95.013,50.008,94.93,50.044,94.811,50.048,94.718,50.043,94.675,50.028,94.615,50.024,94.565,50.088,94.497,50.133,94.458,50.166,94.4,50.18,94.355,50.222,94.347,50.303,94.319,50.405,94.287,50.511,94.251,50.556,94.076,50.573,93.99,50.569,93.795,50.578,93.662,50.584,93.626,50.586,93.501,50.597,93.387,50.608,93.271,50.616,93.223,50.607,93.103,50.604,93.01,50.655,92.971,50.713,92.964,50.745,92.941,50.778,92.856,50.789,92.779,50.779,92.739,50.711,92.681,50.683,92.627,50.688,92.579,50.725,92.486,50.765,92.426,50.803,92.355,50.864,92.296,50.85,92.279,50.812,92.265,50.775,92.192,50.701,92.104,50.692,91.957,50.698,91.804,50.694,91.706,50.666,91.634,50.615,91.597,50.576,91.522,50.562,91.446,50.522,91.415,50.468,91.341,50.47,91.301,50.463,91.234,50.452,91.063,50.423,91.022,50.415,90.917,50.364,90.838,50.324,90.761,50.306,90.714,50.259,90.655,50.222,90.517,50.213,90.365,50.167,90.311,50.151,90.225,50.117,90.104,50.103,90.054,50.094,90.005,50.069,89.977,49.984,89.878,49.954,89.744,49.948,89.644,49.903,89.634,49.823,89.67,49.75,89.654,49.717,89.579,49.7,89.475,49.661,89.396,49.612,89.299,49.611,89.244,49.627,89.203,49.596,89.18,49.532,89.109,49.501,89.008,49.473,88.971,49.484,88.945,49.508,88.9,49.54,88.864,49.528,88.86,49.482,88.832,49.448,88.748,49.446,88.683,49.465,88.633,49.486,88.544,49.483,88.452,49.473,88.393,49.483,88.338,49.473,88.193,49.452,88.136,49.381,88.134,49.298,88.116,49.256,88.029,49.22,87.988,49.187,87.935,49.165,87.818,49.162,87.814,49.162,87.763,49.166,87.668,49.147,87.577,49.132,87.516,49.122,87.476,49.091,87.417,49.077,87.323,49.086,87.297,49.148,87.234,49.216,87.148,49.24,87.071,49.255,87.001,49.287,86.953,49.322,86.812,49.488,86.714,49.559,86.626,49.563,86.614,49.61,86.665,49.657,86.731,49.696,86.729,49.749,86.675,49.777,86.61,49.769,86.522,49.708,86.418,49.638,86.292,49.588,86.242,49.546,86.181,49.499,86.093,49.505,86.03,49.503,85.974,49.499,85.934,49.55,85.88,49.557,85.498,49.605,85.372,49.624,85.292,49.599,85.233,49.616,85.21,49.665,85.137,49.751,85.076,49.822,85.001,49.894,84.975,49.951,85,50.01,84.989,50.061,84.924,50.088,84.839,50.091,84.607,50.202,84.499,50.219,84.401,50.239,84.323,50.239,84.258,50.288,84.195,50.437,84.176,50.521,84.099,50.605,84.002,50.677,83.945,50.775,83.86,50.818,83.718,50.887,83.581,50.936,83.357,50.995,83.274,50.995,83.16,50.989,83.093,50.961,83.019,50.897,82.919,50.893,82.761,50.893,82.719,50.869,82.693,50.826,82.612,50.771,82.494,50.728,82.326,50.742,82.212,50.719,82.098,50.711,81.934,50.766,81.752,50.764,81.634,50.739,81.466,50.74,81.431,50.771,81.452,50.824,81.438,50.871,81.41,50.91,81.388,50.956,81.319,50.966,81.125,50.946,81.071,50.969,81.078,51.015,81.112,51.072,81.141,51.147,81.127,51.191,81.027,51.186,80.966,51.19,80.934,51.243,80.877,51.281,80.813,51.283,80.735,51.293,80.65,51.277,80.605,51.224,80.551,51.217,80.491,51.202,80.448,51.183,80.421,51.136,80.434,51.093,80.452,50.998,80.424,50.946,80.345,50.919,80.27,50.925,80.22,50.912,80.127,50.858,80.086,50.84,80.072,50.807,80.066,50.758,79.986,50.775,79.86,50.955,79.716,51.16,79.554,51.378,79.469,51.493,79.149,51.868,78.992,52.047,78.721,52.357,78.475,52.638,78.198,52.93,78.033,53.095,77.86,53.269,77.799,53.317,77.704,53.379,77.469,53.499,77.132,53.67,76.821,53.823,76.576,53.943,76.513,53.993,76.485,54.023,76.459,54.055,76.422,54.114,76.422,54.152,76.655,54.145,76.703,54.182,76.789,54.322,76.837,54.442,76.759,54.437,76.616,54.387,76.539,54.351,76.496,54.336,76.267,54.312,76.141,54.259,75.881,54.168,75.693,54.115,75.657,54.106,75.437,54.09,75.398,54.069,75.392,54.022,75.377,53.97,75.22,53.894,75.052,53.827,74.989,53.819,74.887,53.834,74.834,53.826,74.681,53.754,74.452,53.647,74.43,53.604,74.429,53.551,74.403,53.504,74.352,53.488,74.277,53.528,74.21,53.576,74.069,53.611,73.859,53.62,73.731,53.603,73.643,53.576,73.47,53.469,73.407,53.448,73.372,53.454,73.362,53.506,73.327,53.543,73.286,53.598,73.306,53.707,73.399,53.811,73.554,53.868,73.679,53.929,73.716,53.996,73.712,54.042,73.666,54.063,73.618,54.067,73.59,54.045,73.506,53.999,73.381,53.963,73.277,53.956,73.23,53.958,73.119,53.981,72.914,54.107,72.741,54.125,72.622,54.134,72.583,54.122,72.564,54.09,72.576,54.056,72.599,54.023,72.586,53.996,72.53,53.976,72.447,53.942,72.404,53.964,72.383,54.054,72.387,54.123,72.329,54.181,72.269,54.272,72.186,54.326,72.105,54.308,72.066,54.232,72.004,54.206,71.887,54.221,71.677,54.178,71.336,54.158,71.093,54.212,71.053,54.26,71.152,54.364,71.16,54.455,71.159,54.539,71.186,54.599,71.126,54.715,70.992,54.95,70.91,55.128,70.79,55.261,70.738,55.305,70.486,55.282,70.417,55.253,70.371,55.212,70.293,55.184,70.182,55.162,70.087,55.177,69.982,55.199,69.87,55.246,69.74,55.307,69.493,55.357,69.247,55.373,68.977,55.39,68.843,55.358,68.713,55.308,68.525,55.205,68.438,55.194,68.302,55.187,68.206,55.161,68.225,55.115,68.244,55.052,68.209,55.003,68.156,54.977,68.074,54.96,67.94,54.954,67.83,54.944,67.693,54.872,67.485,54.854,67.257,54.829,67.098,54.788,66.754,54.738,66.555,54.715,66.223,54.667,65.955,54.66,65.914,54.693,65.708,54.619,65.477,54.623,65.434,54.593,65.378,54.564,65.316,54.552,65.237,54.516,65.192,54.441,65.158,54.364,65.088,54.34,64.995,54.369,64.927,54.397,64.809,54.369,64.65,54.352,64.525,54.362,64.461,54.384,64.199,54.347,64.063,54.303,64.037,54.28,64.004,54.267,63.847,54.236,63.721,54.245,63.701,54.243,63.582,54.222,63.414,54.183,63.293,54.17,63.191,54.171,63.127,54.139,63.074,54.105,62.633,54.069,62.588,54.044,62.499,54.013,62.04,54.003,62.002,53.98,61.986,53.954,61.929,53.946,61.598,53.995,61.334,54.049,61.231,54.019,61.144,53.964,61.113,53.882,61.113,53.813,61.113,53.753,61.074,53.71,60.986,53.657,60.979,53.622,61.099,53.583,61.248,53.551,61.336,53.565,61.41,53.587,61.474,53.58,61.519,53.554,61.535,53.523,61.527,53.502,61.499,53.485,61.401,53.456,61.312,53.466,61.229,53.446,61.186,53.406,61.163,53.337,61.199,53.287,61.311,53.275,61.437,53.239,61.576,53.222,61.66,53.228,61.766,53.174,62.015,53.108,62.081,53.057,62.083,53.005,62.037,52.966,61.974,52.944,61.889,52.956,61.719,52.969,61.534,52.979,61.401,52.996,61.207,52.989,61.047,52.972,61.007,52.933,60.945,52.86,60.893,52.819,60.802,52.745,60.774,52.676,60.821,52.57,60.979,52.395,60.995,52.337,60.938,52.281,60.828,52.233,60.67,52.151,60.499,52.146,60.425,52.126,60.234,52.025,60.066,51.976,60.03,51.933,60.067,51.891,60.28,51.835,60.388,51.773,60.418,51.704,60.465,51.651,60.63,51.617,60.974,51.537,60.993,51.529,61.015,51.492,61.363,51.442,61.411,51.415,61.555,51.325,61.585,51.23,61.512,51.137,61.465,50.99,61.389,50.861,61.227,50.775,60.942,50.696,60.638,50.664,60.508,50.669,60.425,50.679,60.288,50.704,60.187,50.77,60.112,50.834,60.059,50.85,60.005,50.84,59.955,50.799,59.888,50.69,59.812,50.582,59.751,50.544,59.523,50.493,59.498,50.511,59.524,50.583,59.495,50.604,59.452,50.62,59.171,50.648,59.064,50.668,58.985,50.676,58.884,50.694,58.814,50.737,58.665,50.868,58.547,50.971,58.359,51.064,58.188,51.082,58.175,51.072,58.045,51.069,57.839,51.092,57.829,51.089,57.765,51.047,57.717,50.981,57.654,50.925,57.558,50.896,57.442,50.889,57.313,50.947,57.179,51.036,57.012,51.065,56.85,51.046,56.79,51.032,56.62,50.981,56.567,51.004,56.491,51.02,56.326,50.936,56.144,50.845,56.104,50.776,56.05,50.714,55.929,50.654,55.798,50.602,55.686,50.583,55.542,50.602,55.361,50.665,55.195,50.745,55.015,50.87,54.868,50.941,54.727,50.998,54.642,51.012,54.573,50.99,54.546,50.946,54.566,50.911,54.606,50.88,54.638,50.781,54.65,50.66,54.636,50.592,54.596,50.551,54.555,50.536,54.517,50.541,54.471,50.584,54.443,50.674,54.421,50.78,54.298,50.914,54.191,50.996,54.14,51.041,54.042,51.115,53.957,51.161,53.776,51.214,53.688,51.252,53.535,51.4,53.449,51.445,53.338,51.482,53.247,51.494,53.227,51.485,53.038,51.464,52.903,51.467,52.821,51.495,52.735,51.498,52.728,51.498,52.635,51.48,52.618,51.481,52.571,51.482,52.496,51.512,52.423,51.594,52.331,51.681,52.219,51.709,52.007,51.673,51.775,51.554,51.609,51.484,51.473,51.482,51.396,51.471,51.345,51.475,51.301,51.497,51.291,51.54,51.27,51.594,51.163,51.647,51.018,51.682,50.882,51.719,50.794,51.729,50.756,51.675,50.644,51.589,50.516,51.506,50.354,51.37,50.309,51.322,50.247,51.29,50.105,51.255,49.932,51.197,49.822,51.132,49.666,51.102,49.498,51.084,49.425,51.027,49.379,50.935,49.323,50.852,49.059,50.726,48.914,50.645,48.808,50.601,48.735,50.607,48.655,50.62,48.625,50.613,48.666,50.55,48.7,50.354,48.749,50.228,48.785,50.156,48.818,50.1,48.843,50.013,48.81,49.962,48.759,49.928,48.6,49.875,48.434,49.829,48.335,49.858,48.225,49.932,48.181,49.97,48.061,50.094,47.85,50.282,47.706,50.378,47.6,50.414,47.504,50.403,47.429,50.358,47.376,50.318,47.326,50.274,47.295,50.217,47.298,50.14,47.295,50.058,47.248,50.001,47.13,49.939,46.992,49.853,46.89,49.697,46.823,49.502,46.802,49.367,46.853,49.304,46.953,49.253,47.018,49.2,47.031,49.15,47.014,49.098,46.962,49.038,46.853,48.97,46.703,48.806,46.609,48.574,46.661,48.412,46.853,48.324,47.004,48.284,47.065,48.232,47.119,48.127,47.112,48.02,47.093,47.948,47.131,47.877,47.202,47.792,47.292,47.741,47.387,47.769,47.482,47.804,47.6,47.79,47.935,47.761,48.11,47.745,48.167,47.709,48.276,47.59,48.413,47.456,48.553,47.321,48.601,47.262,48.714,47.1,48.832,46.955,48.959,46.775,48.95,46.726,48.884,46.705,48.776,46.71,48.694,46.737,48.647,46.759,48.605,46.766,48.558,46.757,48.519,46.734,48.502,46.699,48.509,46.65,48.541,46.606,48.586,46.577,48.61,46.566,48.774,46.508,48.959,46.442,49.184,46.349,49.232,46.337,49.246,46.292,49.125,46.282,49.111,46.228,49.08,46.189,48.81,46.1,48.743,46.101,48.684,46.086,48.687,46.029,48.703,45.976,48.75,45.921,48.73,45.897,48.69,45.889,48.637,45.906,48.589,45.935,48.537,45.942,48.487,45.935,48.258,45.778,48.159,45.737,48.053,45.721,47.83,45.663,47.764,45.666,47.701,45.686,47.65,45.657,47.633,45.584,47.574,45.634,47.508,45.674,47.479,45.688,47.463,45.68,47.524,45.602,47.529,45.53,47.515,45.491,47.489,45.455,47.454,45.433,47.413,45.421,47.391,45.295,47.351,45.218,47.296,45.149,47.221,45.024,47.162,44.97,47.115,44.906,47.084,44.817,47.039,44.838,47.003,44.876,46.984,44.826,46.957,44.783,46.841,44.718,46.755,44.657,46.716,44.561,46.707,44.503,46.721,44.452,46.753,44.421,46.916,44.387,47.024,44.343,47.123,44.262,47.23,44.192,47.307,44.103,47.362,43.993,47.429,43.78,47.463,43.555,47.563,43.835,47.646,43.885,47.628,43.806,47.568,43.685,47.509,43.51,47.49,43.382,47.512,43.271,47.513,43.219,47.463,43.035,47.489,43,47.529,42.967,47.635,42.903,47.709,42.811,47.728,42.681,47.77,42.645,47.822,42.613,48.08,42.354,48.229,42.181,48.303,42.08,48.384,41.953,48.426,41.924,48.477,41.905,48.573,41.844,48.519,41.779,48.431,41.663,48.391,41.602,48.298,41.545,48.142,41.485,48.056,41.459,47.964,41.334,47.861,41.213,47.791,41.199,47.592,41.218,47.521,41.229,47.318,41.282,47.261,41.315,47.205,41.456,47.143,41.516,47.064,41.555,47.01,41.588,46.988,41.621,46.931,41.67,46.826,41.743,46.749,41.813,46.69,41.831,46.616,41.807,46.571,41.8,46.552,41.812,46.538,41.87,46.43,41.891,46.412,41.905,46.268,41.96,46.213,41.99,46.16,41.992,46.048,42.009,45.954,42.035,45.91,42.071,45.846,42.11,45.727,42.159,45.639,42.205,45.634,42.235,45.688,42.357,45.728,42.475,45.705,42.498,45.656,42.518,45.563,42.536,45.344,42.53,45.208,42.648,45.16,42.675,45.072,42.694,44.943,42.73,44.871,42.756,44.85,42.747,44.771,42.617,44.692,42.71,44.644,42.735,44.576,42.748,44.506,42.749,44.329,42.704,44.2,42.654,44.103,42.616,44.005,42.596,43.957,42.567,43.826,42.572,43.76,42.594,43.738,42.617,43.75,42.658,43.795,42.703,43.799,42.728,43.783,42.747,43.623,42.808,43.558,42.844,43.348,42.897,43.089,42.989,43,43.05,42.992,43.092,42.89,43.133,42.761,43.17,42.66,43.159,42.566,43.155,42.419,43.224,42.28,43.228,42.122,43.207,42.088,43.199,42.05,43.19,41.581,43.219,41.461,43.276,41.358,43.333,41.083,43.374,40.942,43.418,40.802,43.48,40.648,43.534,40.519,43.512,40.342,43.543,40.15,43.57,40.085,43.553,40.024,43.485,39.978,43.42,39.874,43.473,39.517,43.728,39.329,43.897,38.717,44.288,38.636,44.318,38.312,44.374,38.181,44.42,37.851,44.699,37.705,44.661,37.572,44.671,37.495,44.695,37.411,44.735,37.352,44.788,37.284,44.905,37.205,44.972,36.944,45.07,36.651,45.126,36.628,45.151,36.619,45.185,36.873,45.252,36.941,45.29,36.811,45.34,36.762,45.348,36.72,45.372,36.794,45.41,36.866,45.427,36.978,45.384,37.104,45.303,37.214,45.272,37.264,45.311,37.647,45.377,37.673,45.43,37.672,45.488,37.634,45.486,37.61,45.5,37.612,45.565,37.669,45.654,37.841,45.8,37.933,46.002,38.014,46.048,38.074,46.017,38.07,45.97,38.08,45.935,38.133,46.003,38.184,46.095,38.312,46.095,38.4,46.08,38.492,46.091,38.315,46.242,38.078,46.394,37.978,46.383,37.914,46.406,37.81,46.532,37.767,46.636,37.867,46.634,37.968,46.618,38.159,46.691,38.23,46.701,38.343,46.678,38.501,46.664,38.488,46.732,38.439,46.813,38.631,46.873,38.801,46.906,39.127,47.023,39.271,47.044,39.289,47.071,39.293,47.106,39.245,47.2,39.196,47.269,39.024,47.272,38.928,47.176,38.668,47.144,38.552,47.15,38.644,47.212,38.736,47.236,38.762,47.262,38.577,47.239,38.485,47.176,38.214,47.091,38.206,47.136,38.201,47.175,38.221,47.213,38.265,47.237,38.281,47.259,38.281,47.277,38.241,47.288,38.208,47.297,38.201,47.321,38.212,47.343,38.243,47.374,38.257,47.409,38.259,47.48,38.287,47.559,38.369,47.61,38.511,47.622,38.641,47.666,38.719,47.714,38.822,47.837,38.9,47.855,39.058,47.848,39.158,47.837,39.391,47.834,39.658,47.841,39.736,47.845,39.779,47.888,39.776,47.964,39.814,48.035,39.885,48.168,39.961,48.238,39.958,48.269,39.918,48.282,39.866,48.288,39.847,48.303,39.85,48.332,39.89,48.36,39.883,48.419,39.858,48.484,39.836,48.543,39.765,48.572,39.645,48.591,39.67,48.662,39.705,48.739,39.756,48.782,39.793,48.808,39.904,48.794,39.984,48.807,40.004,48.822,39.989,48.851,39.864,48.878,39.753,48.914,39.706,48.96,39.687,49.008,39.759,49.037,39.89,49.064,39.976,49.13,40.07,49.2,40.109,49.252,40.128,49.307,40.126,49.369,40.058,49.432,40.058,49.497,40.095,49.543,40.081,49.577,40.031,49.597,39.958,49.591,39.877,49.568,39.781,49.572,39.627,49.651,39.463,49.728,39.368,49.731,39.303,49.742,39.246,49.782,39.212,49.833,39.175,49.856,39.115,49.842,39.028,49.818,38.918,49.825,38.777,49.884,38.648,49.953,38.552,49.955,38.451,49.964,38.259,50.052,38.209,50.051,38.178,50.025,38.163,49.955,38.147,49.939,38.113,49.928,38.047,49.92,37.95,49.964,37.704,50.109,37.605,50.215,37.582,50.292,37.501,50.341,37.423,50.411,37.343,50.418,37.255,50.395,37.171,50.361,37.131,50.352,36.988,50.34,36.759,50.292,36.696,50.246,36.619,50.209,36.56,50.235,36.5,50.28,36.369,50.297,36.306,50.28,36.243,50.312,36.189,50.368,36.116,50.409,36.008,50.42,35.89,50.437,35.796,50.406,35.674,50.346,35.591,50.369,35.546,50.44,35.488,50.46,35.412,50.54,35.392,50.611,35.412,50.642,35.44,50.682,35.44,50.728,35.417,50.768,35.383,50.799,35.346,50.904,35.315,50.95,35.309,50.987,35.335,51.021,35.312,51.044,35.269,51.047,35.198,51.044,35.158,51.061,35.115,51.121,35.093,51.181,35.064,51.203,34.99,51.202,34.869,51.189,34.76,51.169,34.712,51.172,34.617,51.203,34.491,51.237,34.234,51.244,34.214,51.255,34.228,51.277,34.281,51.312,34.275,51.34,34.23,51.363,34.207,51.42,34.209,51.484,34.201,51.554,34.147,51.608,34.115,51.645,34.121,51.679,34.239,51.692,34.379,51.717,34.403,51.742,34.398,51.78,34.113,51.98,34.015,52.156,33.922,52.251,33.819,52.316,33.735,52.345,33.613,52.333,33.452,52.334,33.287,52.354,33.148,52.34,32.9,52.256,32.806,52.253,32.645,52.279,32.508,52.309,32.435,52.307,32.391,52.295,32.363,52.272,32.283,52.114,32.217,52.083,32.122,52.051,32.042,52.045,31.974,52.047,31.876,52.071,31.782,52.099,31.763,52.101,31.759,52.126,31.691,52.221,31.65,52.262,31.602,52.285,31.577,52.312,31.577,52.426,31.586,52.532,31.616,52.546,31.526,52.633,31.519,52.699,31.563,52.731,31.565,52.759,31.535,52.798,31.443,52.862,31.353,52.933,31.295,52.99,31.259,53.017,31.303,53.061,31.365,53.139,31.388,53.185,31.418,53.196,31.563,53.202,31.668,53.201,31.747,53.184,31.777,53.147,31.85,53.106,32.055,53.089,32.142,53.091,32.251,53.128,32.426,53.211,32.469,53.27,32.578,53.312,32.644,53.329,32.704,53.336,32.71,53.371,32.706,53.419,32.686,53.448,32.47,53.547,32.442,53.579,32.425,53.617,32.451,53.653,32.45,53.693,32.2,53.781,31.992,53.797,31.821,53.792,31.754,53.81,31.783,53.855,31.825,53.935,31.838,54.001,31.826,54.031,31.792,54.056,31.628,54.111,31.404,54.196,31.299,54.292,31.246,54.392,31.185,54.453,31.075,54.492,31.082,54.517,31.155,54.611,31.152,54.625,31.121,54.648,30.984,54.696,30.799,54.783,30.791,54.806,30.804,54.861,30.83,54.915,30.867,54.941,30.978,55.05,30.978,55.088,30.959,55.138,30.877,55.223,30.814,55.279,30.811,55.307,30.821,55.33,30.862,55.36,30.901,55.397,30.909,55.525,30.907,55.57,30.882,55.596,30.856,55.608,30.801,55.601,30.722,55.622,30.662,55.655,30.626,55.666,30.587,55.7,30.475,55.769,30.456,55.787,30.234,55.845,30.043,55.836,29.937,55.845,29.882,55.832,29.824,55.795,29.744,55.77,29.685,55.77,29.63,55.751,29.482,55.685,29.413,55.725,29.353,55.784,29.373,55.835,29.398,55.881,29.396,55.912,29.375,55.939,29.283,55.968,29.087,56.021,29.032,56.022,28.947,56.002,28.795,55.943,28.741,55.955,28.691,56.003,28.637,56.062,28.564,56.092,28.407,56.089,28.392,56.087,28.316,56.053,28.284,56.056,28.148,56.143,28.173,56.19,28.202,56.26,28.192,56.316,28.169,56.387,28.111,56.511,28.103,56.546,28.008,56.6,27.992,56.645,27.941,56.704,27.892,56.741,27.882,56.824,27.849,56.853,27.806,56.867,27.656,56.843,27.639,56.846,27.711,56.978,27.717,57.055,27.763,57.135,27.815,57.167,27.83,57.194,27.838,57.248,27.829,57.293,27.797,57.317,27.673,57.368,27.539,57.43,27.511,57.508,27.47,57.524,27.352,57.528,27.354,57.55,27.372,57.613,27.4,57.667,27.492,57.725,27.515,57.764,27.542,57.799,27.753,57.841,27.777,57.857,27.779,57.871,27.769,57.884,27.722,57.905,27.673,57.935,27.644,58.014,27.571,58.138,27.502,58.221,27.488,58.27,27.506,58.326,27.53,58.381,27.531,58.435,27.427,58.733,27.434,58.787,27.464,58.841,27.513,58.886,27.622,58.945,27.758,59.052,27.85,59.193,27.898,59.278,27.938,59.297,28.016,59.302,28.046,59.328,28.061,59.343,28.128,59.358,28.151,59.374,28.133,59.403,28.066,59.453,28.013,59.484,28.064,59.554,28.046,59.647,28.014,59.725,28.058,59.782,28.131,59.787,28.213,59.725,28.335,59.693,28.424,59.734,28.454,59.814,28.518,59.85,28.604,59.818,28.748,59.807,28.867,59.812,28.947,59.829,28.982,59.855,29.013,59.902,29.079,59.961,29.147,60,29.67,59.956,30.123,59.874,30.157,59.904,30.173,59.957,30.06,60.003,29.977,60.026,29.872,60.121,29.721,60.195,29.569,60.202,29.37,60.176,29.069,60.191,28.813,60.332,28.643,60.375,28.522,60.483,28.492,60.54,28.622,60.492,28.64,60.543,28.651,60.611,28.578,60.653,28.513,60.677,28.179,60.571,27.798,60.536,28.152,60.746,28.407,60.897,28.455,60.92,28.568,60.96,28.663,61.003,28.739,61.059,28.993,61.169,29.252,61.288,29.492,61.444,29.579,61.493,29.69,61.546,29.933,61.712,30.01,61.757,30.306,61.965,30.48,62.068,30.566,62.128,30.936,62.324,31.187,62.481,31.286,62.568,31.382,62.692,31.437,62.776,31.534,62.885,31.537,62.922,31.509,62.955,31.437,63.008,31.337,63.068,31.247,63.142,31.181,63.208,30.975,63.301,30.655,63.417,30.419,63.504,30.055,63.689,29.992,63.735,30.004,63.747,30.21,63.803,30.415,63.948,30.504,64.021,30.526,64.077,30.528,64.141,30.514,64.2,30.488,64.237,30.391,64.282,30.108,64.366,30.042,64.443,29.987,64.524,29.986,64.558,30.12,64.645,30.126,64.688,30.11,64.733,30.073,64.765,29.783,64.804,29.702,64.846,29.638,64.912,29.604,64.968,29.601,65.002,29.622,65.04,29.72,65.08,29.811,65.108,29.827,65.145,29.826,65.185,29.811,65.205,29.63,65.224,29.612,65.235,29.608,65.249,29.617,65.265,29.715,65.337,29.728,65.473,29.819,65.569,29.716,65.625,29.724,65.634,29.883,65.664,30.029,65.671,30.095,65.682,30.103,65.726,30.088,65.787,29.937,66.023,29.903,66.091,29.804,66.177,29.721,66.235,29.671,66.276,29.591,66.357,29.544,66.44,29.464,66.532,29.371,66.617,29.293,66.696,29.093,66.849,29.066,66.892,29.069,66.93,29.087,66.971,29.243,67.097,29.388,67.201,29.572,67.324,29.751,67.426,29.941,67.547,29.988,67.668,29.979,67.689,29.822,67.754,29.524,67.929,29.344,68.062,29.063,68.118,28.685,68.19,28.56,68.351,28.471,68.488,28.479,68.538,28.752,68.771,28.778,68.814,28.773,68.84,28.745,68.856,28.706,68.866,28.454,68.872,28.414,68.904,28.566,68.928,28.692,68.961,28.899,69.01,28.966,69.022,29.119,69.05,29.171,69.072,29.21,69.097,29.353,69.271,29.388,69.298,29.833,69.36,29.994,69.392,30.087,69.433,30.132,69.464,30.164,69.502,30.187,69.543,30.196,69.581,30.16,69.63,30.18,69.636,30.228,69.634,30.38,69.585,30.615,69.533,30.789,69.529,30.861,69.538,30.897,69.561,30.922,69.606,30.924,69.652,30.87,69.783,31.05,69.769,31.453,69.69,31.547,69.697,31.666,69.721,31.789,69.816,31.879,69.832,31.998,69.81,32.031,69.835,31.969,69.914,31.985,69.954,32.392,69.869,32.565,69.806,32.942,69.752,33.008,69.722,33.013,69.671,32.995,69.626,32.915,69.602,32.754,69.606,32.177,69.674,32.092,69.633,32.161,69.597,32.331,69.554,32.378,69.479,32.637,69.489,32.884,69.461,33,69.47,33.021,69.446,32.942,69.383,32.979,69.367,33.256,69.428,33.385,69.444,33.454,69.428,33.464,69.378,33.418,69.315,33.413,69.267,33.328,69.152,33.196,69.117,33.141,69.069,33.333,69.098,33.436,69.13,33.627,69.289,33.684,69.31,34.229,69.313,34.353,69.303,34.864,69.228,35.01,69.221,35.176,69.231,35.233,69.266,35.29,69.275,35.858,69.192,36.618,69.003,37.731,68.692,38.358,68.415,38.43,68.356,38.657,68.322,38.706,68.345,38.832,68.325,39.569,68.072,39.823,68.059,39.79,68.112,39.746,68.162,39.809,68.151,39.896,68.115,40.036,68.015,40.207,67.942,40.381,67.832,40.526,67.79,40.657,67.774,40.766,67.743,40.966,67.713,41.061,67.444,41.134,67.386,41.134,67.267,41.262,67.218,41.359,67.21,41.354,67.121,41.276,66.914,41.189,66.826,40.522,66.447,40.103,66.3,39.289,66.132,38.654,66.069,38.398,66.064,37.901,66.096,37.628,66.13,37.295,66.225,36.984,66.273,36.77,66.294,36.373,66.302,35.513,66.396,35.364,66.429,34.825,66.611,34.61,66.56,34.483,66.55,34.396,66.613,34.431,66.63,34.452,66.651,34.146,66.703,33.894,66.707,33.76,66.751,33.595,66.785,33.523,66.764,33.482,66.765,33.15,66.844,33.002,66.908,32.848,67.022,32.885,67.061,32.93,67.087,32.4,67.153,31.895,67.161,31.983,67.13,32.202,67.113,32.341,67.068,32.501,67.004,32.464,66.916,32.686,66.83,32.857,66.747,32.862,66.721,32.929,66.704,33.181,66.68,33.224,66.604,33.183,66.574,33.217,66.532,33.405,66.484,33.518,66.471,33.656,66.443,33.593,66.385,33.477,66.347,33.361,66.33,33.416,66.316,33.567,66.321,34.113,66.225,34.4,66.128,34.692,65.952,34.786,65.865,34.793,65.816,34.777,65.768,34.735,65.716,34.716,65.664,34.616,65.51,34.544,65.457,34.406,65.396,34.536,65.278,34.671,65.168,34.804,64.986,34.827,64.913,34.833,64.8,34.952,64.756,34.905,64.739,34.858,64.707,34.87,64.56,35.035,64.44,35.284,64.363,35.432,64.347,35.647,64.378,35.802,64.335,36.146,64.189,36.302,64.034,36.365,64.003,36.714,63.945,36.975,63.91,37.373,63.817,37.442,63.813,37.635,63.893,37.968,63.949,38.071,64.026,38.062,64.091,37.977,64.207,37.954,64.32,37.844,64.366,37.741,64.397,37.43,64.374,37.29,64.378,37.184,64.408,37.04,64.489,36.769,64.685,36.624,64.751,36.579,64.791,36.528,64.847,36.535,64.939,36.653,64.935,36.786,64.987,36.883,65.172,37.05,65.196,37.141,65.194,37.528,65.108,38.009,64.879,38.116,64.855,38.228,64.851,38.412,64.857,38.442,64.827,38.541,64.791,38.613,64.787,39.054,64.714,39.567,64.571,39.758,64.577,39.833,64.656,39.849,64.691,40.058,64.771,40.204,64.784,40.408,64.755,40.445,64.779,40.375,64.896,40.281,64.998,40.143,65.063,39.896,65.255,39.798,65.35,39.749,65.448,39.781,65.535,39.817,65.598,40.328,65.752,40.513,65.844,40.692,65.963,40.774,65.988,41.076,66.021,41.476,66.123,41.781,66.259,42.084,66.466,42.211,66.52,42.314,66.515,42.451,66.482,42.602,66.423,42.807,66.411,43.006,66.421,43.233,66.416,43.551,66.321,43.603,66.291,43.653,66.251,43.55,66.173,43.542,66.123,43.624,66.147,43.737,66.158,43.844,66.142,43.944,66.099,44.017,66.05,44.104,66.009,44.132,66.065,44.145,66.113,44.097,66.235,44.221,66.407,44.316,66.482,44.489,66.672,44.437,66.795,44.429,66.938,44.404,67.004,44.292,67.1,44.074,67.167,43.855,67.189,43.782,67.254,43.796,67.33,43.856,67.439,44.036,67.671,44.225,67.996,44.232,68.071,44.214,68.113,44.226,68.154,44.205,68.254,44.169,68.327,43.404,68.609,43.358,68.636,43.333,68.673,43.413,68.682,43.472,68.68,44.048,68.549,44.175,68.542,45.078,68.578,45.519,68.547,45.892,68.48,46.158,68.291,46.43,68.119,46.684,67.97,46.69,67.849,46.429,67.824,46.174,67.818,45.529,67.758,45.374,67.689,44.939,67.477,44.902,67.413,44.939,67.351,45.139,67.285,45.562,67.186,45.753,66.989,45.885,66.891,45.986,66.853,46.084,66.844,46.298,66.843,46.449,66.819,46.492,66.8,46.552,66.819,46.691,66.826,47.496,66.93,47.656,66.976,47.709,67.045,47.768,67.276,47.839,67.356,47.908,67.455,47.883,67.515,47.875,67.584,48.279,67.65,48.654,67.695,48.833,67.681,48.878,67.731,48.763,67.827,48.696,67.874,48.754,67.896,48.841,67.87,48.954,67.854,49.155,67.87,49.931,68.065,50.233,68.175,50.414,68.218,50.699,68.318,50.839,68.35,51.079,68.363,51.336,68.402,51.617,68.476,51.995,68.539,52.056,68.541,52.129,68.532,52.285,68.459,52.227,68.419,52.183,68.374,52.259,68.351,52.322,68.34,52.397,68.352,52.475,68.382,52.67,68.427,52.723,68.484,52.648,68.506,52.55,68.592,52.435,68.61,52.344,68.608,52.684,68.731,53.413,68.913,53.802,68.996,54.186,69.003,54.491,68.992,54.376,68.965,53.874,68.927,53.798,68.907,53.798,68.885,53.92,68.871,53.971,68.844,53.929,68.812,53.891,68.802,53.834,68.709,53.759,68.634,53.918,68.537,53.931,68.436,53.829,68.383,53.69,68.403,53.567,68.367,53.343,68.343,53.293,68.312,53.261,68.267,53.403,68.257,53.515,68.26,53.914,68.231,53.968,68.227,54.099,68.259,54.233,68.266,54.394,68.275,54.476,68.294,54.561,68.273,54.718,68.184,54.861,68.202,54.923,68.374,55.151,68.48,55.418,68.568,55.675,68.576,55.925,68.637,56.044,68.649,56.276,68.624,56.62,68.619,56.909,68.567,57.127,68.554,57.444,68.642,58.173,68.89,58.237,68.834,58.354,68.916,58.919,69.004,59.057,69.006,59.06,68.973,59.11,68.896,59.22,68.85,59.371,68.738,59.298,68.708,59.223,68.691,59.112,68.616,59.099,68.444,59.311,68.4,59.604,68.351,59.726,68.352,59.828,68.38,59.859,68.396,59.897,68.422,59.923,68.471,59.941,68.51,59.865,68.605,59.896,68.706,60.16,68.7,60.489,68.729,60.638,68.787,60.815,68.895,60.934,68.987,60.859,69.146,60.665,69.11,60.337,69.457,60.171,69.591,60.276,69.653,60.559,69.692,60.813,69.821,60.909,69.847,61.016,69.851,61.771,69.763,62.631,69.743,63.361,69.675,64.19,69.535,64.592,69.436,64.929,69.325,64.896,69.248,65.032,69.27,65.327,69.201,65.528,69.173,65.736,69.132,65.813,69.077,66.085,69.036,66.366,68.961,66.416,68.948,66.756,68.892,67.002,68.874,67.149,68.754,67.64,68.579,67.731,68.514,68.157,68.404,68.371,68.314,68.504,68.348,68.829,68.567,69.024,68.818,69.141,68.951,68.924,68.956,68.763,68.917,68.66,68.927,68.543,68.967,68.355,69.068,68.117,69.236,68.073,69.421,68.006,69.48,67.774,69.53,67.624,69.584,67.064,69.694,66.964,69.656,66.934,69.597,66.897,69.554,66.84,69.609,66.804,69.659,66.803,69.74,66.832,69.842,66.926,70.014,67.069,70.006,67.144,70.031,67.239,70.108,67.197,70.172,67.146,70.22,67.157,70.295,67.247,70.5,67.285,70.739,67.212,70.798,67.143,70.838,66.822,70.797,66.702,70.819,66.675,70.865,66.666,70.901,66.759,70.962,66.847,71.064,66.693,71.042,66.64,71.081,66.768,71.14,66.918,71.282,67.274,71.348,67.542,71.412,67.959,71.548,68.269,71.683,68.469,71.853,68.607,72.013,68.83,72.392,69.039,72.67,69.391,72.956,69.612,72.982,69.694,72.978,69.709,72.956,69.659,72.932,69.645,72.898,69.738,72.885,69.888,72.883,70.172,72.901,70.655,72.89,71.5,72.914,71.617,72.902,71.93,72.82,72.101,72.829,72.446,72.79,72.634,72.744,72.812,72.691,72.787,72.483,72.753,72.343,72.624,72.079,72.574,72.013,72.375,71.822,72.279,71.696,72.13,71.609,71.912,71.548,71.884,71.511,71.867,71.457,72.079,71.307,72.581,71.151,72.704,70.963,72.732,70.823,72.7,70.457,72.653,70.403,72.562,70.346,72.469,70.275,72.53,70.173,72.599,69.793,72.616,69.484,72.557,69.378,72.527,69.154,72.527,69.081,72.577,68.969,72.678,68.875,72.812,68.815,73.191,68.707,73.548,68.575,73.573,68.533,73.592,68.482,73.465,68.431,73.266,68.294,73.139,68.181,73.129,68.091,73.173,67.973,73.152,67.865,73.067,67.767,72.949,67.696,72.594,67.587,71.847,67.008,71.668,66.94,71.365,66.962,71.449,66.879,71.551,66.76,71.54,66.683,71.342,66.687,71.066,66.604,70.939,66.548,70.725,66.519,70.561,66.549,70.383,66.602,70.409,66.648,70.443,66.668,70.568,66.701,70.691,66.745,70.631,66.754,70.579,66.754,70.444,66.697,70.283,66.686,70.094,66.754,69.949,66.83,69.877,66.845,69.74,66.815,69.218,66.829,69.079,66.816,69.013,66.788,69.051,66.766,69.091,66.724,69.144,66.641,69.194,66.579,69.412,66.511,69.701,66.485,69.982,66.401,70.339,66.342,71.146,66.367,71.358,66.359,71.566,66.334,71.917,66.247,72.068,66.253,72.322,66.332,72.384,66.507,72.417,66.561,73.342,66.807,73.514,66.861,73.792,66.995,73.883,67.085,73.986,67.328,74.075,67.414,74.676,67.695,74.77,67.766,74.787,67.898,74.778,67.986,74.743,68.074,74.632,68.218,74.511,68.303,74.391,68.421,74.481,68.659,74.58,68.751,75.125,68.862,75.59,68.901,76.108,68.976,76.316,68.992,76.459,68.978,76.606,68.898,76.735,68.777,77.112,68.596,77.238,68.47,77.261,68.316,77.248,67.941,77.174,67.779,77.325,67.736,77.396,67.699,77.579,67.644,77.675,67.59,77.772,67.57,77.986,67.559,78.59,67.578,78.922,67.589,78.888,67.613,78.839,67.631,78.559,67.639,78.161,67.678,77.588,67.752,77.52,67.91,77.536,68.008,77.665,68.19,77.757,68.222,77.868,68.235,77.995,68.259,77.959,68.377,77.907,68.482,77.785,68.63,77.651,68.903,77.466,68.905,77.328,68.959,76.645,69.117,76.001,69.235,75.561,69.252,75.42,69.239,75.054,69.116,74.815,69.091,74.363,69.145,73.977,69.115,73.836,69.143,73.776,69.198,73.891,69.418,73.833,69.504,73.663,69.617,73.56,69.707,73.578,69.803,73.83,70.176,73.937,70.273,74.207,70.445,74.343,70.579,74.311,70.654,73.732,71.069,73.577,71.217,73.507,71.264,73.365,71.32,73.15,71.385,73.086,71.445,73.672,71.845,73.939,71.915,74.311,71.958,74.489,71.997,74.804,72.077,74.992,72.145,75.053,72.199,75.09,72.263,75.097,72.421,75.06,72.549,75.008,72.619,74.897,72.71,74.787,72.812,74.865,72.838,74.942,72.854,75.152,72.853,75.369,72.797,75.475,72.685,75.604,72.581,75.603,72.512,75.591,72.457,75.644,72.382,75.691,72.35,75.741,72.296,75.694,72.254,75.644,72.232,75.55,72.171,75.395,71.983,75.274,71.959,75.247,71.813,75.503,71.655,75.469,71.534,75.417,71.495,75.28,71.43,75.298,71.378,75.332,71.342,75.734,71.266,76.11,71.219,76.742,71.202,76.929,71.128,76.995,71.181,77.59,71.168,78.068,70.986,78.321,70.93,78.526,70.912,78.942,70.934,79.015,70.95,79.084,71.002,78.889,70.997,78.804,70.974,78.724,70.976,78.588,70.994,78.491,71.025,78.387,71.087,78.213,71.266,77.908,71.324,77.707,71.301,77.481,71.312,77.114,71.409,76.871,71.447,76.433,71.552,76.312,71.595,76.216,71.683,76.104,71.829,76.032,71.91,76.124,71.927,76.422,72.006,76.871,72.033,77.061,72.004,77.551,71.842,77.778,71.836,78.187,71.907,78.232,71.952,78.141,72.045,78.016,72.092,77.781,72.114,77.493,72.072,77.411,72.108,77.44,72.157,77.472,72.192,77.625,72.201,77.733,72.229,77.968,72.329,78.225,72.377,78.483,72.395,79.422,72.381,79.954,72.223,80.474,72.153,80.699,72.098,80.763,72.089,80.815,72.054,80.856,71.97,81.511,71.746,81.662,71.716,82.08,71.707,82.547,71.759,82.758,71.764,82.986,71.749,83.107,71.721,83.234,71.668,83.166,71.602,83.106,71.562,82.977,71.451,82.918,71.42,82.493,71.293,82.323,71.26,82.277,71.093,82.254,71.056,82.239,70.998,82.316,70.879,82.336,70.807,82.271,70.707,82.163,70.598,82.182,70.511,82.221,70.396,82.236,70.43,82.231,70.483,82.258,70.544,82.452,70.69,82.592,70.89,82.738,70.942,82.869,70.955,83.01,70.895,83.051,70.815,83.058,70.695,83.03,70.581,82.92,70.407,82.742,70.286,82.682,70.218,82.767,70.154,82.857,70.105,82.961,70.088,83.081,70.093,83.11,70.11,83.132,70.157,83.094,70.221,83.074,70.277,83.293,70.321,83.497,70.345,83.66,70.418,83.7,70.466,83.736,70.546,83.651,70.672,83.579,70.766,83.334,70.989,83.151,71.104,83.266,71.276,83.458,71.468,83.531,71.514,83.55,71.544,83.571,71.594,83.554,71.65,83.534,71.684,83.34,71.828,83.2,71.875,82.755,71.903,82.645,71.925,82.319,72.072,82.281,72.105,82.209,72.211,82.184,72.238,82.094,72.265,81.793,72.327,81.586,72.352,81.283,72.359,81.098,72.39,80.827,72.488,80.798,72.52,80.72,72.648,80.656,72.712,80.675,72.759,80.774,72.861,80.842,72.949,80.757,73.025,80.639,73.049,80.51,73.086,80.455,73.155,80.425,73.231,80.419,73.29,80.398,73.357,80.458,73.414,80.596,73.474,80.562,73.515,80.583,73.568,81.469,73.64,81.817,73.659,83.545,73.667,83.667,73.686,84.417,73.722,84.738,73.763,85.077,73.72,85.201,73.722,85.448,73.735,85.611,73.822,85.979,73.857,86.591,73.894,86.893,73.887,86.961,73.861,87.029,73.824,86.698,73.717,86.366,73.62,86.094,73.578,85.827,73.493,85.8,73.459,85.793,73.438,85.802,73.372,85.818,73.327,86.098,73.273,86.308,73.196,86.514,73.14,86.677,73.107,86.715,73.126,86.122,73.307,85.971,73.347,85.91,73.39,85.939,73.456,85.999,73.486,86.092,73.519,86.155,73.535,86.376,73.569,87.12,73.615,87.294,73.705,87.37,73.756,87.571,73.811,87.503,73.832,87.338,73.846,87.21,73.879,86.697,74.195,86.571,74.244,86.178,74.279,86.001,74.316,86.183,74.423,86.396,74.45,86.538,74.444,86.665,74.414,86.898,74.325,87.23,74.364,87.106,74.404,86.894,74.45,86.7,74.522,86.426,74.585,86.116,74.629,85.791,74.645,85.881,74.74,86.059,74.728,86.12,74.757,86.201,74.816,86.651,74.682,86.863,74.718,87.042,74.779,87.419,74.941,87.468,75.013,87.287,75.053,87.141,75.072,86.939,75.068,86.922,75.113,87.006,75.17,87.171,75.192,87.671,75.13,88.504,75.29,88.733,75.369,89.31,75.47,89.595,75.458,90.185,75.591,91.005,75.65,91.479,75.65,91.845,75.724,92.408,75.75,92.603,75.779,93.55,75.854,94.075,75.913,94.156,75.959,93.687,75.922,93.574,75.956,93.475,75.933,93.406,75.901,93.178,75.959,93.116,75.945,93.069,75.913,92.987,75.903,92.89,75.91,92.859,75.979,92.972,76.075,93.105,76.026,93.259,76.099,93.36,76.101,93.648,76.054,93.843,76.101,94.102,76.124,94.388,76.103,94.507,76.108,94.576,76.152,95.038,76.114,95.359,76.14,95.579,76.137,95.92,76.113,96.075,76.082,95.986,76.01,95.653,75.892,95.744,75.872,95.935,75.926,96.509,76.006,96.601,75.99,96.538,75.922,96.497,75.891,96.879,75.931,97.205,76.019,97.351,76.033,97.499,75.98,97.638,76.029,97.67,76.078,97.918,76.089,98.02,76.134,98.195,76.166,98.342,76.181,98.662,76.243,98.771,76.224,98.985,76.208,99.187,76.178,99.563,76.109,99.616,76.082,99.663,76.078,99.77,76.029,99.689,75.956,99.602,75.852,99.442,75.803,99.541,75.799,99.609,75.811,99.738,75.881,99.851,75.93,99.825,76.136,99.617,76.24,99.461,76.275,99.094,76.384,98.97,76.431,98.806,76.481,98.869,76.51,99.576,76.471,99.936,76.49,100.322,76.479,100.844,76.525,101.061,76.477,101.311,76.479,101.598,76.439,101.684,76.485,101.213,76.536,101.003,76.531,100.928,76.557,101.006,76.615,101.099,76.704,101.008,76.781,100.92,76.823,100.906,76.901,100.99,76.99,101.186,77.029,101.293,77.102,101.518,77.198,102.61,77.509,103.131,77.626,103.331,77.641,103.561,77.632,104.015,77.73,104.185,77.73,104.814,77.652,104.965,77.595,105.309,77.549,105.71,77.525,105.895,77.489,105.983,77.448,106.06,77.391,105.734,77.352,105.385,77.238,104.912,77.175,104.324,77.133,104.202,77.102,105.32,77.092,105.646,77.101,105.712,77.001,105.822,76.998,106.145,77.045,106.339,77.048,106.705,77.014,106.784,77.032,106.942,77.034,107.279,76.991,107.43,76.927,107.19,76.822,106.941,76.73,106.639,76.573,106.546,76.586,106.385,76.589,106.414,76.512,106.683,76.515,106.825,76.48,107.158,76.524,107.624,76.51,107.722,76.522,107.902,76.57,107.95,76.661,108.028,76.718,108.182,76.738,108.352,76.72,108.638,76.72,109.369,76.749,109.981,76.712,110.471,76.758,111.115,76.723,111.392,76.687,111.601,76.622,111.786,76.604,111.939,76.553,112.094,76.48,112.017,76.421,111.943,76.38,112.143,76.424,112.297,76.435,112.413,76.408,112.62,76.384,112.684,76.219,112.743,76.187,112.798,76.13,112.722,76.077,112.656,76.054,112.819,76.059,113.047,76.114,113.094,76.133,113.15,76.175,113.066,76.215,112.988,76.24,113.086,76.258,113.273,76.252,113.366,76.179,113.428,76.112,113.564,75.892,113.857,75.921,113.871,75.856,113.749,75.705,113.62,75.593,113.568,75.568,113.486,75.564,113.517,75.622,113.469,75.657,113.392,75.678,113.126,75.699,112.629,75.835,112.497,75.85,112.466,75.844,112.453,75.83,112.73,75.738,112.956,75.572,113.162,75.621,113.243,75.611,113.356,75.534,113.559,75.502,113.726,75.451,113.614,75.293,112.925,75.015,112.192,74.853,111.868,74.74,111.299,74.658,110.893,74.548,110.374,74.466,110.226,74.379,109.84,74.322,109.866,74.293,109.911,74.261,109.864,74.209,109.81,74.169,109.511,74.089,109.075,74.032,108.2,73.694,107.765,73.625,107.271,73.621,107.167,73.589,106.794,73.377,106.679,73.331,106.189,73.308,105.677,72.959,105.393,72.841,105.144,72.777,105.403,72.79,105.708,72.837,106.067,72.95,106.16,73.002,106.209,73.061,106.315,73.106,106.478,73.139,107.109,73.177,107.369,73.163,107.75,73.173,108.001,73.236,108.151,73.258,108.285,73.266,108.351,73.31,108.575,73.319,109.09,73.378,109.166,73.4,109.331,73.487,109.637,73.454,109.855,73.472,110.429,73.629,110.773,73.689,110.868,73.731,110.799,73.76,110.722,73.78,110.388,73.726,110.091,73.709,109.753,73.723,109.707,73.744,109.666,73.8,109.774,73.881,109.869,73.931,110.084,73.994,110.261,74.017,110.92,73.948,111.056,73.939,111.131,74.053,111.341,74.047,111.551,74.029,111.46,74.005,111.228,73.969,111.3,73.885,111.4,73.828,111.804,73.745,112.147,73.709,112.4,73.711,112.795,73.746,112.856,73.771,112.94,73.836,112.836,73.962,112.935,73.946,113.033,73.914,113.182,73.837,113.327,73.707,113.416,73.648,113.364,73.583,113.157,73.46,113.277,73.392,113.491,73.346,113.488,73.145,113.475,73.048,113.369,72.942,113.247,72.897,113.128,72.831,113.158,72.769,113.186,72.73,113.312,72.657,113.665,72.635,113.712,72.654,113.63,72.677,113.391,72.711,113.298,72.739,113.216,72.806,113.312,72.878,113.417,72.932,113.543,73.054,113.581,73.142,113.559,73.233,113.639,73.274,113.765,73.318,113.829,73.327,113.886,73.346,113.795,73.367,113.711,73.379,113.539,73.434,113.51,73.505,113.857,73.533,114.061,73.585,114.816,73.607,115.338,73.703,116.496,73.676,117.309,73.599,118.45,73.59,118.871,73.538,118.911,73.518,118.936,73.481,118.754,73.465,118.457,73.464,118.377,73.367,118.43,73.247,118.96,73.117,119.425,73.064,119.75,72.979,119.922,72.971,120.598,72.981,120.997,72.937,121.354,72.971,121.748,72.97,121.886,72.961,122.03,72.897,122.26,72.881,122.538,72.878,122.692,72.891,122.752,72.906,122.731,72.931,122.502,72.971,122.527,73.017,122.615,73.028,122.999,72.965,123.16,72.955,123.301,73.002,123.405,73.086,123.462,73.144,123.522,73.173,123.572,73.177,123.622,73.193,123.501,73.262,123.384,73.347,123.355,73.402,123.323,73.431,123.305,73.533,123.416,73.637,123.491,73.666,123.797,73.627,123.934,73.689,124.019,73.712,124.388,73.755,124.541,73.751,124.796,73.712,125.617,73.521,125.599,73.447,125.794,73.468,125.888,73.498,126.107,73.517,126.254,73.548,126.296,73.537,126.345,73.506,126.309,73.464,126.257,73.42,126.292,73.394,126.335,73.389,126.553,73.335,126.838,73.434,126.955,73.528,127.031,73.547,127.74,73.482,127.955,73.446,127.997,73.426,128.026,73.391,128.142,73.352,128.281,73.331,128.264,73.301,128.258,73.267,128.587,73.262,128.73,73.233,128.889,73.19,128.872,73.139,128.913,73.111,129.059,73.108,129.101,73.112,129.054,73.045,128.854,72.973,128.735,72.943,128.599,72.895,128.674,72.886,129.017,72.872,129.229,72.776,129.25,72.705,129.118,72.677,128.815,72.586,128.633,72.55,128.508,72.547,128.418,72.535,128.549,72.496,129.117,72.486,129.281,72.438,129.412,72.315,129.411,72.166,129.283,72.092,128.935,72.079,128.475,72.246,128.197,72.31,127.803,72.434,127.726,72.413,127.841,72.308,128.027,72.25,128.359,72.088,128.911,71.755,129.04,71.782,129.117,71.825,129.154,71.879,129.122,71.953,129.21,71.917,129.292,71.85,129.461,71.739,129.234,71.745,128.949,71.708,128.843,71.663,128.923,71.602,129.134,71.593,129.225,71.509,129.39,71.405,129.762,71.12,130.026,71.065,130.281,70.947,130.537,70.893,130.668,70.888,130.757,70.962,130.832,70.936,130.898,70.804,131.022,70.746,131.157,70.742,131.268,70.766,131.432,70.828,131.562,70.901,131.769,71.101,131.906,71.203,132.035,71.244,131.991,71.293,132.004,71.35,132.099,71.484,132.228,71.643,132.326,71.726,132.562,71.895,132.654,71.926,132.716,71.871,132.769,71.799,132.804,71.768,132.839,71.755,133.131,71.607,133.426,71.491,133.689,71.434,134.103,71.379,134.703,71.387,134.814,71.461,135.022,71.515,135.359,71.544,135.559,71.61,135.885,71.631,136.09,71.62,136.406,71.571,137.116,71.416,137.315,71.359,137.417,71.299,137.651,71.208,137.798,71.164,137.94,71.133,137.992,71.143,137.974,71.169,137.902,71.194,137.844,71.227,138.013,71.261,138.033,71.286,138.091,71.307,138.314,71.326,138.097,71.359,138.022,71.363,137.918,71.384,137.927,71.43,137.996,71.464,138.048,71.526,138.118,71.566,138.234,71.596,138.318,71.603,138.525,71.563,138.67,71.635,138.78,71.629,139.005,71.556,139.209,71.445,139.32,71.445,139.632,71.489,139.984,71.492,139.939,71.558,139.695,71.7,139.723,71.885,139.552,71.927,139.359,71.951,139.64,71.998,139.847,72.149,140.014,72.162,140.188,72.191,140.134,72.21,139.617,72.226,139.505,72.208,139.43,72.163,139.176,72.163,139.145,72.264,139.141,72.33,139.474,72.467,139.601,72.496,140.451,72.493,140.705,72.519,141.079,72.587,140.983,72.63,140.973,72.717,140.652,72.843,140.676,72.872,140.708,72.89,140.808,72.891,141.31,72.858,141.518,72.789,142.061,72.721,143.516,72.698,143.681,72.673,144.304,72.643,144.569,72.61,145.199,72.57,145.486,72.542,145.714,72.497,146.083,72.471,146.253,72.442,146.235,72.35,145.467,72.362,145.213,72.393,144.897,72.396,144.776,72.382,144.588,72.306,144.361,72.265,144.169,72.259,144.295,72.193,144.471,72.175,145.039,72.26,146.594,72.302,146.832,72.295,146.807,72.237,146.599,72.124,146.402,72.035,146.113,71.945,146.006,71.945,146.23,72.138,146.137,72.146,146.051,72.142,145.799,72.222,145.759,72.226,145.71,72.206,145.71,72.178,145.664,72.067,145.757,72.021,145.757,71.941,145.407,71.89,145.271,71.895,145.126,71.927,145.064,71.926,145.047,71.901,145.078,71.855,145.074,71.831,145.018,71.794,144.99,71.753,145.076,71.707,145.189,71.696,145.805,71.746,146.073,71.808,146.368,71.922,146.895,72.198,147.127,72.292,147.262,72.328,147.434,72.341,148.402,72.312,148.965,72.252,149.502,72.164,149.766,72.091,149.963,71.992,149.998,71.95,150.017,71.896,149.881,71.843,149.28,71.826,149.049,71.796,148.965,71.763,148.955,71.744,148.923,71.715,148.968,71.69,149.238,71.688,149.498,71.664,149.857,71.601,149.913,71.581,150.026,71.521,150.061,71.511,150.6,71.52,150.635,71.499,150.668,71.455,150.525,71.386,150.385,71.339,150.098,71.227,150.243,71.267,150.822,71.363,150.968,71.38,151.145,71.374,151.582,71.287,151.76,71.218,152.093,71.023,152,71.002,151.762,70.982,152.509,70.834,152.798,70.836,153.461,70.879,153.794,70.88,154.414,70.974,155.029,71.034,155.596,71.039,155.895,71.096,156.685,71.094,157.447,71.075,158.037,71.039,158.702,70.935,159.351,70.791,159.728,70.65,159.805,70.605,159.912,70.506,159.959,70.424,160.006,70.31,159.983,70.221,159.89,70.159,159.831,70.081,159.839,69.99,159.729,69.87,159.833,69.785,160.119,69.73,160.739,69.655,160.911,69.606,160.929,69.459,160.982,69.334,161.036,69.098,161.141,69.039,161.31,68.982,161.341,68.905,161.129,68.654,160.997,68.608,160.856,68.538,161.104,68.563,161.23,68.654,161.365,68.823,161.495,68.85,161.566,68.905,161.566,69.064,161.48,69.202,161.48,69.3,161.537,69.38,161.945,69.545,162.166,69.612,162.376,69.649,162.945,69.683,163.201,69.715,163.498,69.693,163.705,69.702,163.946,69.735,164.16,69.719,164.513,69.609,165.761,69.584,165.98,69.546,166.82,69.5,166.884,69.5,167.073,69.554,167.628,69.74,167.857,69.728,167.95,69.699,168.048,69.626,168.15,69.577,168.23,69.447,168.303,69.271,168.423,69.24,168.588,69.228,168.946,69.163,169.311,69.08,169.415,68.92,169.61,68.786,170.066,68.799,170.538,68.825,170.995,69.045,170.997,69.135,170.884,69.264,170.714,69.388,170.582,69.583,170.161,69.627,170.201,69.683,170.36,69.751,170.503,69.857,170.525,69.938,170.487,70.108,170.868,70.096,171.247,70.076,171.971,70,172.56,69.968,172.869,69.92,173.056,69.865,173.277,69.824,173.353,69.924,173.439,69.947,173.733,69.891,173.948,69.874,174.319,69.882,174.786,69.856,175.296,69.86,175.751,69.904,175.921,69.895,176.108,69.86,176.41,69.769,176.924,69.646,177.395,69.612,177.934,69.496,178.443,69.453,178.848,69.387,178.907,69.362,178.925,69.326,178.951,69.296,179.273,69.26,179.868,69.013,180,68.983,180,65.067,179.827,65.034,179.651,64.921,179.448,64.822,179.15,64.782,178.698,64.631,178.52,64.603,178.285,64.672,177.749,64.717,177.582,64.778,177.337,64.931,177.252,64.954,177.179,65.014,176.881,65.082,176.625,65.038,176.413,65.071,176.341,65.047,176.452,65.025,176.646,65.007,176.94,65.016,177.037,65,177.123,64.947,177.223,64.862,177.148,64.805,177.069,64.787,176.831,64.849,176.557,64.84,176.429,64.855,176.061,64.961,175.781,64.844,175.396,64.784,175.098,64.777,174.549,64.684,174.699,64.681,175.097,64.747,175.331,64.747,175.678,64.782,175.859,64.825,175.946,64.865,176.057,64.905,176.169,64.885,176.247,64.843,176.301,64.784,176.351,64.705,176.283,64.664,176.219,64.642,176.141,64.586,176.508,64.682,176.731,64.625,176.843,64.634,177.05,64.719,177.388,64.774,177.427,64.763,177.467,64.737,177.41,64.573,177.433,64.444,177.688,64.305,177.953,64.222,178.045,64.22,178.131,64.235,178.164,64.309,178.229,64.364,178.313,64.314,178.381,64.261,178.477,64.128,178.475,64.089,178.451,64.011,178.536,63.976,178.65,63.965,178.692,63.842,178.731,63.667,178.681,63.651,178.626,63.651,178.44,63.606,178.466,63.574,178.654,63.557,178.706,63.522,178.669,63.44,178.679,63.402,178.744,63.395,178.787,63.442,178.775,63.51,178.793,63.54,178.919,63.4,178.921,63.345,179.028,63.282,179.332,63.19,179.389,63.147,179.405,63.078,179.329,63.058,179.26,63.008,179.302,62.94,179.381,62.884,179.511,62.863,179.571,62.773,179.571,62.688,179.477,62.613,179.289,62.51,179.177,62.469,179.134,62.396,179.121,62.32,179.045,62.324,178.964,62.355,178.019,62.547,177.663,62.583,177.351,62.587,177.293,62.599,177.296,62.644,177.316,62.685,177.36,62.737,177.339,62.781,177.298,62.784,177.259,62.75,177.173,62.75,177.091,62.79,177.024,62.777,176.99,62.722,176.963,62.693,176.965,62.659,177.008,62.627,177.19,62.592,177.159,62.561,176.907,62.536,176.703,62.506,176.437,62.411,176.328,62.346,175.614,62.184,175.442,62.128,175.366,62.121,175.268,62.102,175.192,62.034,174.798,61.939,174.715,61.948,174.611,61.868,174.514,61.824,174.285,61.818,174.139,61.795,173.822,61.679,173.623,61.716,173.391,61.557,173.132,61.407,173.055,61.406,172.857,61.469,172.807,61.436,172.838,61.376,172.908,61.312,172.868,61.293,172.789,61.311,172.731,61.314,172.69,61.295,172.697,61.249,172.585,61.19,172.497,61.186,172.396,61.167,172.362,61.117,172.393,61.062,172.213,60.998,172.067,60.916,171.998,60.901,171.918,60.864,171.831,60.837,171.729,60.843,171.49,60.726,170.949,60.523,170.799,60.496,170.608,60.435,170.59,60.394,170.589,60.343,170.512,60.26,170.423,60.048,170.396,60.01,170.351,59.966,170.154,59.986,169.983,60.067,169.927,60.104,169.898,60.148,169.887,60.218,169.854,60.25,169.815,60.265,169.618,60.438,169.276,60.557,169.227,60.596,168.788,60.564,168.67,60.563,168.463,60.592,168.138,60.574,167.746,60.509,167.626,60.469,167.227,60.406,166.964,60.307,166.453,59.947,166.332,59.872,166.273,59.856,166.187,59.849,166.149,59.922,166.136,59.979,166.168,60.089,166.23,60.178,166.292,60.346,166.308,60.414,166.352,60.485,166.18,60.48,165.942,60.357,165.583,60.236,165.416,60.205,165.285,60.135,165.193,60.125,165.085,60.099,165.074,59.946,165.019,59.861,164.954,59.844,164.854,59.841,164.779,59.874,164.67,59.997,164.525,60.061,164.44,60.073,164.377,60.058,164.252,59.974,164.113,59.898,164.135,59.984,164.018,60.017,163.913,60.037,163.78,60.041,163.744,60.028,163.69,59.978,163.574,59.914,163.494,59.887,163.41,59.835,163.365,59.781,163.321,59.705,163.269,59.52,163.273,59.303,163.085,59.131,163.01,59.148,162.975,59.137,162.94,59.114,163.004,59.02,162.97,58.986,162.935,58.964,162.847,58.939,162.643,58.8,162.453,58.709,162.142,58.447,162.049,58.273,161.96,58.077,162.002,57.981,162.04,57.918,162.098,57.875,162.197,57.829,162.411,57.778,162.392,57.745,162.391,57.717,162.467,57.766,162.522,57.904,162.654,57.948,162.718,57.946,163.145,57.837,163.226,57.79,163.214,57.687,163.188,57.637,163.109,57.565,162.957,57.477,162.779,57.358,162.762,57.284,162.762,57.244,162.808,57.103,162.815,57.023,162.791,56.875,162.803,56.811,162.85,56.757,162.922,56.723,163.046,56.741,163.165,56.725,163.257,56.688,163.243,56.565,163.294,56.448,163.336,56.233,163.261,56.174,163.189,56.137,163.047,56.045,162.972,56.034,162.84,56.066,162.628,56.232,162.713,56.331,162.893,56.399,162.975,56.449,163.038,56.522,162.944,56.508,162.878,56.476,162.671,56.49,162.589,56.455,162.489,56.399,162.528,56.261,162.461,56.235,162.334,56.188,162.146,56.128,162.085,56.09,161.924,55.84,161.776,55.655,161.724,55.496,161.729,55.358,161.785,55.205,161.824,55.139,161.996,54.998,162.08,54.886,162.106,54.752,161.967,54.689,161.726,54.533,161.625,54.516,161.294,54.521,161.13,54.598,160.936,54.578,160.773,54.541,160.517,54.431,160.289,54.288,160.074,54.189,160.01,54.131,159.922,54.008,159.844,53.784,159.871,53.673,159.914,53.621,159.956,53.552,159.899,53.448,159.898,53.381,160.002,53.275,160.025,53.13,159.947,53.125,159.772,53.23,159.586,53.238,159.136,53.117,158.952,53.048,158.745,52.909,158.684,52.935,158.64,53.015,158.565,53.05,158.472,53.032,158.432,52.957,158.56,52.922,158.609,52.874,158.534,52.688,158.481,52.627,158.5,52.46,158.493,52.383,158.463,52.305,158.332,52.091,158.104,51.81,157.823,51.605,157.629,51.535,157.531,51.48,157.49,51.409,157.202,51.213,156.847,51.007,156.748,50.969,156.724,51.047,156.713,51.124,156.671,51.227,156.543,51.312,156.521,51.38,156.5,51.475,156.49,51.913,156.377,52.367,156.365,52.509,156.229,52.626,156.154,52.747,156.11,52.866,156.099,53.006,155.95,53.744,155.905,53.928,155.706,54.521,155.62,54.865,155.564,55.199,155.555,55.348,155.643,55.794,155.717,56.072,155.983,56.695,156.025,56.752,156.067,56.782,156.529,57.021,156.728,57.152,156.849,57.29,156.977,57.466,156.964,57.561,156.948,57.616,156.9,57.677,156.792,57.748,156.83,57.78,156.872,57.804,156.986,57.83,157.217,57.777,157.45,57.799,157.666,58.02,157.975,57.986,158.21,58.025,158.275,58.009,158.321,58.083,158.449,58.163,158.687,58.281,159.037,58.424,159.211,58.519,159.308,58.611,159.453,58.696,159.592,58.804,159.847,59.127,160.35,59.394,160.547,59.547,160.711,59.602,160.855,59.627,161.219,59.846,161.449,60.027,161.754,60.152,161.846,60.232,162.004,60.42,162.068,60.466,162.266,60.537,162.713,60.659,162.973,60.783,163.352,60.8,163.466,60.85,163.585,60.877,163.71,60.917,163.554,61.026,163.589,61.084,163.62,61.111,163.893,61.24,164.005,61.344,163.992,61.388,163.973,61.42,163.804,61.461,163.837,61.558,163.883,61.64,164.02,61.711,164.068,61.874,164.074,62.045,164.207,62.292,164.288,62.347,164.598,62.471,164.671,62.474,164.888,62.432,165.124,62.412,165.208,62.374,165.226,62.406,165.214,62.448,165.28,62.463,165.417,62.447,165.397,62.494,165.044,62.517,164.792,62.571,164.567,62.675,164.418,62.705,164.256,62.697,163.332,62.551,163.287,62.511,163.244,62.455,163.302,62.373,163.258,62.337,163.213,62.313,163.163,62.26,163.118,62.153,163.131,62.05,163.018,61.891,163.009,61.792,163.208,61.737,163.258,61.699,163.198,61.645,163.139,61.611,163.085,61.571,163.047,61.554,162.994,61.544,162.922,61.598,162.856,61.705,162.752,61.711,162.718,61.695,162.699,61.653,162.608,61.65,162.506,61.67,162.393,61.662,162.188,61.541,161.037,60.963,160.915,60.893,160.767,60.753,160.482,60.74,160.368,60.709,160.287,60.667,160.174,60.638,160.177,60.691,160.201,60.73,160.226,60.832,160.379,61.025,160.281,61.045,160.184,61.048,160.004,61.007,159.883,60.943,159.79,60.957,159.835,61.014,159.949,61.129,159.914,61.234,159.883,61.292,159.931,61.324,160.163,61.538,160.247,61.648,160.317,61.793,160.321,61.839,160.309,61.894,160.238,61.904,160.183,61.903,159.722,61.758,159.552,61.719,159.496,61.781,159.423,61.808,159.295,61.914,159.189,61.929,159.077,61.922,158.824,61.85,158.547,61.811,158.334,61.826,158.152,61.765,158.07,61.754,157.799,61.795,157.469,61.799,157.371,61.747,157.084,61.676,156.892,61.565,156.791,61.53,156.68,61.481,156.63,61.272,156.483,61.206,156.344,61.155,156.056,60.996,155.853,60.777,155.716,60.682,155.428,60.55,154.971,60.377,154.578,60.095,154.441,59.884,154.39,59.877,154.293,59.833,154.267,59.73,154.269,59.658,154.209,59.6,154.15,59.529,154.213,59.483,154.272,59.475,154.358,59.481,154.583,59.54,154.971,59.45,155.167,59.36,155.153,59.27,155.16,59.19,155.017,59.196,154.824,59.188,154.704,59.141,154.458,59.217,154.376,59.188,154.247,59.109,154.011,59.076,153.892,59.114,153.695,59.225,153.361,59.215,153.273,59.091,153.196,59.094,153.078,59.082,152.882,58.939,152.818,58.926,152.576,58.954,152.401,59.026,152.32,59.031,152.165,58.997,152.088,58.91,151.705,58.867,151.327,58.875,151.121,59.083,151.505,59.164,151.733,59.147,151.99,59.16,152.261,59.224,152.17,59.278,152.104,59.291,151.942,59.284,151.798,59.323,151.486,59.524,151.348,59.561,151.17,59.583,151.034,59.586,150.983,59.571,150.912,59.523,150.863,59.475,150.823,59.461,150.729,59.469,150.615,59.507,150.484,59.494,150.54,59.525,150.667,59.556,150.457,59.591,150.326,59.639,150.203,59.651,149.643,59.77,149.425,59.761,149.29,59.728,149.065,59.631,149.128,59.559,149.175,59.527,149.205,59.488,149.133,59.481,148.925,59.475,148.797,59.532,148.709,59.449,148.744,59.374,148.89,59.4,148.965,59.369,148.914,59.283,148.727,59.258,148.491,59.262,148.257,59.414,147.875,59.388,147.688,59.291,147.514,59.269,147.04,59.366,146.804,59.373,146.537,59.457,146.444,59.43,146.273,59.221,146.05,59.171,145.932,59.198,145.829,59.33,145.756,59.374,145.555,59.414,144.483,59.376,144.123,59.408,143.869,59.411,143.524,59.344,143.192,59.37,142.58,59.24,142.33,59.153,142.025,59,141.755,58.745,141.603,58.649,141.347,58.528,140.988,58.417,140.79,58.303,140.685,58.212,140.495,57.865,140.447,57.814,140.002,57.688,139.862,57.549,139.803,57.514,139.619,57.456,139.507,57.358,139.444,57.33,139.182,57.262,138.966,57.088,138.662,56.966,138.218,56.629,138.18,56.589,138.141,56.499,138.074,56.433,137.692,56.139,137.573,56.112,137.384,55.975,137.19,55.892,137.012,55.795,136.794,55.694,136.46,55.577,136.351,55.51,136.175,55.352,135.751,55.161,135.541,55.114,135.263,54.943,135.235,54.903,135.212,54.841,135.258,54.731,135.325,54.707,135.438,54.692,135.852,54.584,136.238,54.614,136.58,54.614,136.715,54.624,136.797,54.621,136.824,54.561,136.82,54.452,136.77,54.353,136.729,54.061,136.683,53.931,136.719,53.804,136.803,53.782,136.886,53.839,137.019,53.848,137.155,53.822,137.258,54.025,137.172,54.057,137.096,54.129,137.142,54.182,137.378,54.282,137.525,54.291,137.666,54.283,137.513,54.156,137.451,54.13,137.403,54.124,137.339,54.101,137.476,54.028,137.623,53.97,137.835,53.947,137.786,53.903,137.645,53.866,137.517,53.707,137.314,53.632,137.221,53.579,137.254,53.546,137.328,53.539,137.738,53.56,137.95,53.604,138.253,53.726,138.379,53.909,138.494,53.96,138.528,53.96,138.568,53.947,138.569,53.819,138.407,53.674,138.292,53.592,138.25,53.524,138.32,53.523,138.451,53.537,138.511,53.57,138.661,53.745,138.699,53.87,138.722,54.044,138.705,54.148,138.716,54.223,138.657,54.298,138.696,54.32,139.105,54.218,139.32,54.193,139.707,54.277,139.796,54.256,139.858,54.205,140.179,54.052,140.242,54.001,140.347,53.813,140.688,53.596,141.006,53.495,141.015,53.454,141.218,53.334,141.374,53.293,141.402,53.184,141.328,53.097,141.181,53.015,140.887,53.092,140.84,53.088,140.875,53.04,141.087,52.898,141.256,52.84,141.266,52.653,141.245,52.55,141.132,52.436,141.17,52.368,141.33,52.271,141.409,52.234,141.485,52.179,141.386,52.057,141.367,51.921,141.258,51.861,141.129,51.728,140.933,51.62,140.839,51.414,140.688,51.232,140.671,51.051,140.646,50.987,140.521,50.8,140.476,50.546,140.535,50.131,140.564,50.107,140.625,50.082,140.613,50.054,140.585,50.033,140.463,49.911,140.465,49.826,140.511,49.762,140.517,49.596,140.431,49.331,140.399,49.29,140.364,49.221,140.349,49.159,140.326,49.12,140.309,49.054,140.334,48.995,140.378,48.964,140.224,48.773,140.171,48.524,140.113,48.423,139.998,48.324,139.761,48.181,139.676,48.09,139.521,47.975,139.373,47.887,139.167,47.635,139.001,47.383,138.587,47.057,138.53,46.976,138.5,46.89,138.392,46.745,138.337,46.543,138.21,46.463,138.106,46.251,137.769,45.929,137.685,45.818,137.425,45.64,137.147,45.394,136.804,45.171,136.737,45.08,136.604,44.978,136.46,44.822,136.251,44.667,136.209,44.562,136.142,44.489,135.987,44.44,135.875,44.374,135.533,43.971,135.489,43.899,135.483,43.835,135.26,43.685,135.131,43.526,134.917,43.427,134.692,43.291,134.156,43.042,134.01,42.947,133.709,42.83,133.587,42.828,133.329,42.764,133.16,42.697,133.059,42.723,132.997,42.808,132.924,42.805,132.864,42.794,132.709,42.876,132.576,42.872,132.481,42.91,132.304,42.883,132.334,43.239,132.31,43.314,132.233,43.245,132.029,43.119,131.947,43.095,131.867,43.095,131.898,43.171,132.013,43.28,131.976,43.296,131.939,43.302,131.795,43.255,131.722,43.203,131.516,42.996,131.393,42.822,131.292,42.772,131.245,42.697,131.158,42.626,131.025,42.645,130.946,42.634,130.756,42.673,130.709,42.656,130.834,42.523,130.73,42.326,130.687,42.303]],[[47.442,80.854,47.9,80.813,48.243,80.823,48.345,80.819,48.446,80.806,48.547,80.779,48.687,80.718,48.684,80.633,48.625,80.629,48.044,80.668,47.777,80.756,47.705,80.765,47.6,80.742,47.512,80.688,47.414,80.675,47.304,80.606,47.198,80.615,47.145,80.609,47.011,80.562,46.678,80.561,46.624,80.541,46.514,80.476,46.378,80.457,46.141,80.447,46.06,80.484,46.024,80.541,45.969,80.569,45.641,80.537,45.389,80.56,45.149,80.599,44.905,80.611,45.125,80.652,46.327,80.735,46.799,80.755,47.021,80.814,47.352,80.853,47.442,80.854]],[[50.278,80.927,50.431,80.911,50.801,80.914,50.918,80.89,51.455,80.745,51.591,80.741,51.704,80.688,51.146,80.604,50.961,80.54,50.28,80.527,49.846,80.498,49.75,80.472,49.794,80.425,49.586,80.377,48.896,80.369,48.811,80.354,48.677,80.3,48.689,80.29,48.922,80.277,48.96,80.266,48.991,80.242,49.011,80.207,48.978,80.163,48.892,80.155,48.797,80.161,48.582,80.195,48.555,80.183,48.533,80.158,48.467,80.11,48.386,80.096,48.167,80.133,48.096,80.122,48.026,80.099,47.94,80.089,47.737,80.082,47.632,80.112,47.723,80.151,47.978,80.213,47.893,80.239,47.642,80.245,47.444,80.23,47.343,80.189,47.249,80.18,46.991,80.183,46.846,80.237,46.738,80.258,46.644,80.3,47.403,80.445,47.656,80.501,47.896,80.529,48.208,80.544,48.306,80.562,48.403,80.569,48.465,80.558,48.625,80.508,49.088,80.516,49.185,80.559,49.193,80.656,49.147,80.712,49.244,80.821,49.508,80.865,50.124,80.924,50.278,80.927]],[[67.765,76.238,67.365,76.161,67.127,76.108,66.893,76.072,66.657,76.047,66.282,75.984,65.619,75.905,65.202,75.839,64.745,75.788,64.263,75.72,63.779,75.673,63.659,75.669,63.317,75.603,63.046,75.576,62.066,75.428,61.616,75.32,61.487,75.311,61.356,75.315,61.249,75.281,61.147,75.223,60.936,75.164,60.829,75.111,60.719,75.069,60.655,75.055,60.534,75.059,60.476,75.055,60.277,75.008,60.241,74.971,60.455,74.946,60.501,74.905,60.439,74.875,60.301,74.837,60.222,74.797,60.08,74.756,59.982,74.745,59.747,74.746,59.735,74.695,59.771,74.664,59.753,74.637,59.674,74.61,59.596,74.614,59.24,74.693,59.182,74.666,59.157,74.611,59.146,74.552,59.101,74.508,59.04,74.486,58.928,74.463,58.535,74.499,58.502,74.464,58.562,74.422,58.646,74.328,58.665,74.289,58.618,74.227,58.441,74.129,57.767,74.014,57.778,73.974,57.853,73.898,57.872,73.85,57.845,73.805,57.756,73.769,57.657,73.768,57.604,73.775,57.449,73.826,57.313,73.838,57.291,73.815,57.464,73.746,57.543,73.658,57.46,73.61,57.134,73.504,56.964,73.367,56.634,73.304,56.43,73.297,56.228,73.314,56.035,73.346,55.549,73.357,55.28,73.392,55.007,73.454,54.769,73.449,54.566,73.419,54.3,73.351,54.132,73.481,54.205,73.542,53.839,73.697,53.763,73.766,53.851,73.801,53.963,73.822,54.174,73.886,54.386,73.936,54.606,73.951,54.643,73.96,54.733,74.034,54.831,74.096,54.92,74.129,55.023,74.187,55.341,74.42,55.416,74.436,56.078,74.481,56.137,74.496,55.947,74.542,55.752,74.541,55.662,74.556,55.61,74.591,55.582,74.628,55.66,74.656,55.914,74.796,56.218,74.898,56.499,74.957,56.429,74.973,56.34,75.013,55.998,75.003,55.863,75.059,55.821,75.091,55.81,75.125,55.921,75.168,56.036,75.194,56.162,75.187,56.289,75.164,56.389,75.138,56.485,75.096,56.57,75.098,56.876,75.244,56.829,75.278,56.809,75.328,56.844,75.351,56.989,75.375,57.088,75.384,57.302,75.373,57.607,75.341,57.632,75.356,57.708,75.454,57.783,75.507,58.094,75.593,58.073,75.619,58.058,75.663,58.418,75.72,58.653,75.777,58.881,75.855,58.995,75.872,59.11,75.874,59.347,75.907,59.782,75.946,60.036,75.984,60.118,76.067,60.279,76.096,60.606,76.109,60.731,76.104,60.801,76.069,60.942,76.071,60.998,76.089,61.054,76.12,61.037,76.169,61.034,76.233,61.157,76.274,61.202,76.282,61.569,76.298,61.787,76.291,62.237,76.242,62.471,76.23,62.782,76.245,62.971,76.237,63.526,76.31,64.463,76.378,64.708,76.426,64.95,76.484,65.073,76.497,65.197,76.5,65.31,76.518,65.528,76.568,65.637,76.579,65.755,76.579,65.863,76.613,65.959,76.688,66.063,76.746,66.345,76.821,66.829,76.924,67.264,76.964,67.535,77.008,67.652,77.012,68.017,76.991,68.486,76.934,68.699,76.871,68.873,76.79,68.912,76.761,68.942,76.708,68.891,76.66,68.858,76.61,68.9,76.573,68.559,76.449,68.222,76.313,68.165,76.285,67.765,76.238]],[[55.32,73.308,55.787,73.269,56.138,73.256,56.35,73.226,56.43,73.201,56.397,73.139,56.335,73.114,56.189,73.033,56.167,72.983,56.193,72.905,56.171,72.848,56.122,72.807,56.084,72.789,55.82,72.79,55.723,72.766,55.718,72.722,55.701,72.672,55.616,72.599,55.441,72.575,55.403,72.549,55.417,72.501,55.356,72.465,55.36,72.409,55.39,72.378,55.399,72.314,55.518,72.221,55.495,72.182,55.403,72.107,55.375,72.015,55.298,71.935,55.471,71.869,55.547,71.783,55.614,71.69,55.819,71.508,56.043,71.346,56.454,71.107,56.895,70.927,57.066,70.876,57.484,70.792,57.556,70.766,57.625,70.729,57.447,70.661,57.264,70.636,57.247,70.605,57.146,70.589,56.649,70.647,56.622,70.655,56.569,70.697,56.51,70.729,56.386,70.734,56.26,70.715,56.335,70.677,56.417,70.665,56.561,70.594,56.5,70.566,56.435,70.563,56.142,70.658,56.115,70.646,56.087,70.618,55.942,70.649,55.907,70.626,55.797,70.616,55.707,70.642,55.706,70.675,55.687,70.692,55.237,70.666,55.052,70.667,54.867,70.678,54.645,70.742,54.608,70.713,54.601,70.68,54.517,70.693,54.333,70.745,54.199,70.765,53.722,70.814,53.384,70.874,53.468,70.901,53.614,70.915,53.616,70.951,53.593,71.001,53.588,71.052,53.671,71.087,53.857,71.07,53.834,71.127,53.922,71.138,54.094,71.105,54.156,71.125,53.886,71.196,53.591,71.297,53.622,71.333,53.515,71.343,53.41,71.34,53.319,71.399,53.333,71.477,53.412,71.53,53.364,71.542,52.909,71.495,52.679,71.506,52.419,71.537,52.18,71.49,51.938,71.475,51.813,71.491,51.692,71.525,51.59,71.571,51.511,71.648,51.439,71.777,51.429,71.826,51.444,71.934,51.482,71.98,51.583,72.071,51.653,72.099,51.805,72.142,51.885,72.153,52.069,72.131,52.252,72.13,52.332,72.154,52.407,72.197,52.462,72.252,52.586,72.284,52.622,72.301,52.662,72.337,52.706,72.391,52.714,72.437,52.749,72.483,52.864,72.55,52.823,72.591,52.839,72.619,52.917,72.669,52.683,72.682,52.605,72.704,52.529,72.737,52.551,72.769,52.579,72.791,52.812,72.875,52.913,72.9,53.024,72.914,53.135,72.913,53.254,72.904,53.37,72.917,53.247,72.973,53.237,73.011,53.189,73.104,53.198,73.148,53.251,73.183,53.358,73.225,53.512,73.238,53.634,73.26,53.753,73.293,53.866,73.299,54.091,73.276,54.202,73.281,54.328,73.299,54.676,73.37,54.804,73.388,54.941,73.383,55.121,73.357,55.32,73.308]],[[96.527,81.076,96.563,81.03,96.693,80.994,96.755,80.958,97.414,80.842,97.703,80.827,97.832,80.798,97.87,80.763,97.856,80.698,97.747,80.699,97.665,80.678,97.221,80.652,97.113,80.614,97.025,80.536,97.073,80.52,97.115,80.497,97.25,80.363,97.287,80.343,97.417,80.323,97.298,80.273,97.175,80.241,95.856,80.177,94.961,80.15,94.661,80.123,94.565,80.126,94.328,80.076,93.872,80.01,93.655,80.01,93.002,80.102,92.202,80.179,92.092,80.223,91.892,80.249,91.637,80.27,91.524,80.359,91.688,80.419,91.897,80.478,92.247,80.499,92.578,80.533,92.827,80.619,92.981,80.703,93.263,80.791,92.773,80.769,92.593,80.781,92.61,80.81,92.71,80.872,92.765,80.893,92.939,80.926,93.065,80.988,93.359,81.032,93.497,81.039,93.637,81.038,93.889,81.058,94.14,81.089,94.375,81.107,94.612,81.115,94.838,81.139,95.061,81.188,95.16,81.271,95.801,81.28,95.902,81.261,95.984,81.211,96.075,81.193,96.187,81.184,96.471,81.099,96.527,81.076]],[[97.675,80.158,97.904,80.095,98.018,80.023,97.907,80.004,97.808,79.956,97.76,79.896,97.627,79.85,97.591,79.775,97.652,79.761,97.725,79.781,97.871,79.853,98.065,79.901,98.273,79.874,98.353,79.884,98.499,79.953,98.472,80.009,98.532,80.044,98.596,80.052,98.866,80.045,99.295,80.016,99.371,79.986,99.473,79.97,99.536,79.941,99.727,79.92,99.818,79.898,99.947,79.849,100.061,79.777,99.916,79.738,99.839,79.669,99.805,79.653,99.782,79.628,99.771,79.568,99.749,79.515,99.721,79.492,99.706,79.463,99.722,79.385,99.681,79.323,99.537,79.277,99.388,79.275,99.167,79.306,99.104,79.305,99.042,79.293,99.317,79.227,99.517,79.13,99.751,79.108,99.815,79.096,99.9,79.006,99.929,78.961,99.541,78.853,99.44,78.834,98.82,78.818,98.411,78.788,98.283,78.795,98.054,78.821,97.905,78.81,97.689,78.827,97.555,78.827,97.248,78.868,96.933,78.934,96.871,78.964,96.808,78.985,96.43,79.003,96.347,79.016,95.796,79.001,95.703,79.012,95.531,79.098,95.437,79.099,95.133,79.05,95.02,79.053,94.791,79.087,94.652,79.127,94.632,79.141,94.62,79.192,94.482,79.219,94.314,79.308,94.219,79.402,93.759,79.451,93.479,79.463,93.272,79.458,93.071,79.495,93.405,79.632,93.847,79.702,94.038,79.756,94.257,79.83,94.347,79.942,94.719,80.011,94.815,80.035,94.947,80.089,94.987,80.097,95.281,80.031,95.338,80.042,95.391,80.073,95.498,80.106,95.858,80.11,96.163,80.097,96.277,80.11,96.417,80.104,97.121,80.153,97.587,80.168,97.675,80.158]],[[102.885,79.254,102.787,79.176,102.746,79.106,102.448,78.877,102.412,78.835,102.587,78.871,102.748,78.95,102.845,79.014,102.95,79.056,103.076,79.056,103.199,79.071,103.433,79.126,103.673,79.15,103.801,79.149,103.926,79.123,104.004,79.063,104.091,79.013,104.404,78.977,104.449,78.964,104.477,78.923,104.452,78.88,104.633,78.835,104.881,78.855,105.015,78.843,105.146,78.819,105.205,78.78,105.256,78.733,105.31,78.666,105.343,78.594,105.313,78.5,104.833,78.353,104.742,78.34,104.519,78.349,104.297,78.335,103.719,78.258,103.003,78.256,102.797,78.188,102.734,78.19,102.673,78.202,102.617,78.225,102.18,78.205,101.692,78.194,101.204,78.192,101.04,78.143,100.541,78.048,100.082,77.975,99.845,77.957,99.5,77.976,99.392,78.001,99.287,78.038,99.439,78.084,99.546,78.179,99.678,78.233,100.019,78.339,100.058,78.38,100.124,78.47,100.163,78.504,100.215,78.536,100.257,78.574,100.263,78.631,100.284,78.679,100.416,78.753,100.516,78.788,100.62,78.797,100.876,78.784,100.956,78.788,100.898,78.812,100.856,78.898,100.865,78.926,100.901,78.98,100.965,79.007,101.031,79.023,101.068,79.096,101.052,79.123,101.149,79.157,101.196,79.204,101.31,79.233,101.543,79.254,101.555,79.313,101.591,79.35,101.643,79.361,101.761,79.372,101.824,79.37,101.912,79.312,102.005,79.264,102.129,79.252,102.251,79.256,102.177,79.313,102.181,79.373,102.225,79.413,102.282,79.43,102.405,79.433,102.79,79.392,103.042,79.332,103.098,79.299,103.052,79.283,102.94,79.271,102.885,79.254]],[[140.049,75.829,140.152,75.81,140.274,75.822,140.389,75.796,140.496,75.69,140.547,75.663,140.602,75.644,140.657,75.634,140.816,75.631,140.889,75.652,140.944,75.7,140.94,75.75,140.927,75.799,140.926,75.867,140.95,75.927,140.985,75.965,141.033,75.989,141.299,76.064,141.485,76.137,141.742,76.108,142.001,76.044,142.46,75.904,142.67,75.863,142.927,75.827,143.185,75.814,143.311,75.822,143.56,75.86,143.686,75.864,145.255,75.586,145.31,75.564,145.36,75.53,145.023,75.49,144.803,75.416,144.727,75.366,144.814,75.325,144.883,75.269,144.408,75.102,144.216,75.059,144.02,75.045,143.626,75.084,143.396,75.083,143.17,75.117,142.922,75.217,142.82,75.268,142.729,75.338,142.7,75.449,142.734,75.545,142.868,75.572,142.986,75.633,143.002,75.66,142.942,75.713,142.552,75.721,142.308,75.692,142.086,75.661,142.151,75.458,142.199,75.393,142.265,75.346,142.617,75.133,142.697,75.103,142.93,75.062,143.128,74.97,142.778,74.868,142.626,74.837,142.473,74.82,142.378,74.829,142.287,74.85,142.184,74.9,142.1,74.951,141.987,74.991,141.748,74.983,141.53,74.947,141.31,74.923,140.661,74.882,140.464,74.856,140.268,74.847,140.011,74.895,139.758,74.964,139.681,74.964,139.606,74.946,139.548,74.904,139.512,74.838,139.43,74.749,139.326,74.687,139.215,74.66,139.099,74.657,138.982,74.674,138.866,74.701,138.092,74.797,138.001,74.827,137.915,74.871,137.683,75.009,137.568,75.041,137.447,75.054,137.218,75.124,137.006,75.235,136.962,75.27,136.948,75.326,136.982,75.365,137.166,75.347,137.29,75.349,137.215,75.554,137.269,75.749,137.358,75.782,137.707,75.76,137.594,75.823,137.501,75.91,137.561,75.955,137.625,75.988,137.774,76.016,137.977,76.028,138.039,76.047,138.096,76.081,138.208,76.115,138.431,76.13,138.814,76.2,138.92,76.197,139.018,76.16,139.109,76.108,139.211,76.081,139.529,76.013,139.743,75.953,140.049,75.829]],[[146.795,75.371,147.06,75.364,147.444,75.438,147.497,75.441,148.432,75.414,148.509,75.387,148.519,75.336,148.489,75.309,148.475,75.272,148.59,75.236,148.892,75.228,149.083,75.262,149.645,75.245,150.104,75.219,150.281,75.164,150.417,75.134,150.531,75.1,150.613,75.12,150.69,75.155,150.757,75.162,150.822,75.157,150.646,74.945,150.58,74.919,150.331,74.867,149.838,74.795,149.597,74.773,149.05,74.772,148.297,74.8,148.092,74.826,147.972,74.857,147.741,74.932,147.627,74.959,147.257,74.984,147.144,74.998,146.925,75.063,146.703,75.114,146.149,75.198,146.186,75.296,146.258,75.394,146.343,75.481,146.438,75.558,146.538,75.582,146.751,75.51,146.748,75.429,146.795,75.371]],[[178.862,70.826,178.793,70.822,178.648,71.001,178.628,71.047,178.684,71.106,178.829,71.178,178.891,71.231,179.235,71.325,179.548,71.448,179.716,71.466,179.886,71.523,180,71.538,180,70.993,179.881,70.976,179.648,70.899,179.153,70.88,178.862,70.826]],[[142.761,54.394,142.976,54.141,142.986,54.086,142.967,54.029,142.927,53.956,142.911,53.878,142.936,53.811,142.918,53.794,143.096,53.489,143.224,53.296,143.26,53.217,143.288,53.134,143.325,52.963,143.333,52.7,143.324,52.614,143.295,52.529,143.264,52.479,143.201,52.443,143.172,52.349,143.156,52.084,143.191,51.944,143.251,51.848,143.295,51.744,143.3,51.632,143.321,51.583,143.418,51.521,143.455,51.471,143.467,51.402,143.473,51.299,143.489,51.277,143.534,51.246,143.736,50.507,143.816,50.283,144.048,49.896,144.141,49.661,144.2,49.55,144.24,49.432,144.272,49.311,144.341,49.181,144.432,49.051,144.607,48.936,144.686,48.871,144.707,48.82,144.714,48.64,144.673,48.679,144.621,48.815,144.536,48.894,144.412,48.986,144.284,49.07,144.125,49.209,144.049,49.249,143.968,49.276,143.819,49.309,143.732,49.312,143.382,49.291,143.236,49.263,143.105,49.199,143.027,49.105,142.972,48.918,142.651,48.247,142.574,48.072,142.546,47.885,142.557,47.738,142.579,47.684,142.67,47.537,142.745,47.452,142.801,47.416,142.864,47.392,142.905,47.362,142.94,47.323,143.006,47.223,143.089,47.001,143.178,46.844,143.218,46.795,143.319,46.807,143.384,46.806,143.447,46.792,143.486,46.752,143.54,46.575,143.579,46.406,143.581,46.361,143.509,46.23,143.491,46.175,143.482,46.116,143.463,46.069,143.432,46.029,143.419,46.222,143.37,46.358,143.352,46.476,143.282,46.559,143.048,46.593,142.829,46.605,142.796,46.62,142.747,46.671,142.692,46.711,142.636,46.716,142.578,46.701,142.479,46.644,142.406,46.555,142.35,46.459,142.304,46.358,142.209,46.089,142.15,45.999,142.077,45.917,142.016,45.962,141.962,46.013,141.93,46.088,141.916,46.171,141.83,46.451,141.867,46.694,142.011,47.03,142.039,47.14,142.017,47.245,141.984,47.348,141.963,47.544,141.964,47.587,142.016,47.701,142.076,47.808,142.149,47.902,142.182,48.013,142.135,48.29,142.029,48.477,141.897,48.655,141.873,48.702,141.866,48.75,141.98,48.972,142.02,49.078,142.067,49.312,142.109,49.44,142.142,49.569,142.153,50.217,142.143,50.312,142.071,50.515,142.066,50.63,142.1,50.776,142.147,50.89,142.208,50.998,142.207,51.223,142.091,51.429,142.006,51.521,141.873,51.63,141.772,51.69,141.722,51.736,141.772,51.752,141.808,51.789,141.721,51.847,141.668,51.933,141.661,52.273,141.682,52.359,141.748,52.455,141.803,52.556,141.856,52.794,141.874,53.039,141.839,53.138,141.824,53.34,141.852,53.389,141.964,53.456,142.142,53.496,142.18,53.484,142.319,53.405,142.371,53.403,142.424,53.411,142.526,53.447,142.583,53.537,142.509,53.588,142.553,53.653,142.68,53.674,142.689,53.73,142.643,53.737,142.683,53.816,142.706,53.896,142.67,53.968,142.467,54.149,142.335,54.281,142.552,54.279,142.616,54.304,142.666,54.358,142.693,54.416,142.761,54.394]],[[-178.876,71.577,-178.439,71.541,-178.354,71.529,-178.215,71.482,-178.134,71.465,-178.057,71.438,-177.975,71.391,-177.817,71.34,-177.584,71.282,-177.532,71.263,-177.498,71.219,-177.524,71.167,-177.822,71.068,-178.063,71.042,-178.528,71.015,-179.157,70.94,-179.416,70.919,-179.507,70.923,-179.734,70.972,-180,70.993,-180,71.184,-180,71.4,-180,71.538,-179.845,71.551,-179.691,71.578,-179.546,71.582,-179.402,71.567,-179.256,71.572,-179.112,71.596,-178.994,71.593,-178.876,71.577]],[[35.816,65.182,35.848,65.143,35.858,65.078,35.827,65.036,35.842,65.001,35.779,64.977,35.68,65.058,35.621,65.059,35.559,65.094,35.529,65.151,35.586,65.167,35.609,65.157,35.729,65.198,35.816,65.182]],[[42.714,66.702,42.676,66.688,42.477,66.735,42.46,66.77,42.469,66.786,42.547,66.796,42.631,66.782,42.691,66.735,42.714,66.702]],[[52.903,71.365,52.994,71.291,53.074,71.238,53.141,71.242,53.193,71.215,53.205,71.16,53.071,71.065,53.048,71.031,53.106,70.999,53.121,70.982,53.023,70.969,53.004,71.012,52.95,71.054,52.835,71.086,52.789,71.115,52.738,71.181,52.547,71.25,52.425,71.239,52.289,71.27,52.25,71.285,52.24,71.325,52.297,71.357,52.513,71.385,52.617,71.383,52.73,71.355,52.72,71.39,52.732,71.404,52.777,71.4,52.903,71.365]],[[168.039,54.565,168.081,54.513,167.677,54.698,167.488,54.795,167.442,54.856,167.512,54.857,167.592,54.798,167.711,54.77,167.883,54.69,168.039,54.565]],[[160.719,70.823,160.651,70.806,160.505,70.82,160.437,70.851,160.44,70.923,160.449,70.934,160.566,70.924,160.645,70.884,160.719,70.823]],[[161.467,68.901,161.422,68.9,161.456,68.966,161.461,68.996,161.364,69.044,161.183,69.082,161.137,69.11,161.125,69.197,161.165,69.334,161.083,69.406,161.111,69.47,161.323,69.541,161.41,69.596,161.505,69.639,161.521,69.634,161.618,69.592,161.609,69.501,161.54,69.437,161.374,69.414,161.351,69.369,161.373,69.293,161.378,69.194,161.394,69.106,161.495,69.016,161.517,68.97,161.507,68.928,161.467,68.901]],[[152.886,76.122,152.786,76.086,152.559,76.144,152.643,76.175,152.799,76.195,152.835,76.185,152.864,76.163,152.886,76.122]],[[149.15,76.66,148.399,76.648,148.448,76.677,148.72,76.747,149.406,76.782,149.268,76.747,149.205,76.677,149.15,76.66]],[[137.96,71.508,137.712,71.423,137.613,71.434,137.512,71.475,137.458,71.483,137.403,71.477,137.344,71.461,137.266,71.456,137.079,71.502,137.064,71.53,137.082,71.543,137.129,71.556,137.168,71.557,137.282,71.58,137.817,71.588,137.858,71.583,137.934,71.543,137.96,71.508]],[[107.696,78.131,107.606,78.083,107.482,78.058,107.344,78.099,107.002,78.096,106.416,78.14,106.583,78.168,107.508,78.189,107.573,78.186,107.696,78.131]],[[112.478,76.621,112.633,76.553,112.661,76.51,112.614,76.499,112.587,76.483,112.575,76.452,112.532,76.45,112.395,76.484,112.297,76.538,112.154,76.549,112.003,76.603,111.969,76.626,112.011,76.633,112.281,76.618,112.394,76.644,112.478,76.621]],[[96.532,76.278,96.614,76.264,96.59,76.221,96.487,76.234,96.351,76.212,96.353,76.177,96.301,76.122,96.109,76.155,95.845,76.16,95.679,76.194,95.311,76.215,95.322,76.262,95.38,76.289,95.594,76.25,95.786,76.294,96.151,76.272,96.271,76.305,96.532,76.278]],[[96.285,77.027,96.254,77.007,96.21,76.992,96.091,77.003,95.855,76.975,95.766,76.991,95.681,77.021,95.364,77.012,95.27,77.019,95.421,77.056,95.854,77.098,96.528,77.206,96.562,77.154,96.561,77.13,96.424,77.071,96.285,77.027]],[[89.514,77.189,89.3,77.184,89.179,77.21,89.142,77.227,89.2,77.272,89.282,77.301,89.616,77.311,89.68,77.28,89.666,77.254,89.514,77.189]],[[74.661,72.873,74.638,72.864,74.588,72.881,74.435,72.908,74.181,72.975,74.1,73.022,74.142,73.074,74.199,73.109,74.409,73.13,74.6,73.122,74.725,73.108,74.962,73.063,74.743,73.033,74.647,72.969,74.66,72.929,74.697,72.908,74.661,72.873]],[[75.504,73.457,75.344,73.432,75.375,73.477,75.57,73.541,75.93,73.574,76.039,73.56,76.052,73.549,75.901,73.481,75.827,73.459,75.504,73.457]],[[83.549,74.072,83.496,74.048,83.45,74.052,83.411,74.04,83.159,74.075,82.818,74.092,82.903,74.129,83.15,74.152,83.513,74.122,83.618,74.089,83.549,74.072]],[[67.345,69.53,67.264,69.443,67.098,69.447,67.047,69.467,67.026,69.483,67.216,69.575,67.329,69.572,67.345,69.53]],[[66.561,70.542,66.569,70.501,66.516,70.515,66.449,70.561,66.408,70.616,66.395,70.727,66.418,70.757,66.44,70.773,66.463,70.769,66.458,70.699,66.561,70.542]],[[70.021,66.502,69.845,66.49,69.651,66.565,69.469,66.716,69.503,66.751,69.616,66.739,69.8,66.736,69.918,66.712,70.077,66.696,70.058,66.627,70.057,66.599,70.11,66.569,70.059,66.518,70.021,66.502]],[[50.052,80.074,49.971,80.061,49.588,80.136,49.556,80.159,49.884,80.23,50.251,80.219,50.31,80.186,50.319,80.172,50.072,80.109,50.052,80.074]],[[51.409,79.944,51.435,79.932,51.431,79.921,51.076,79.932,50.454,79.924,50.091,79.981,50.473,80.035,50.676,80.049,50.936,80.094,51.254,80.049,51.238,80.01,51.243,79.991,51.327,79.972,51.409,79.944]],[[50.754,81.047,50.616,81.041,50.518,81.046,50.412,81.084,50.377,81.103,50.368,81.123,50.465,81.126,50.506,81.144,50.522,81.158,50.592,81.169,50.716,81.171,50.879,81.151,50.946,81.108,50.789,81.072,50.754,81.047]],[[55.48,80.274,55.195,80.227,55.048,80.228,54.98,80.256,55.092,80.296,55.24,80.325,55.353,80.318,55.435,80.302,55.48,80.274]],[[54.415,80.473,54.276,80.421,53.812,80.476,53.85,80.504,53.9,80.515,53.902,80.542,53.859,80.563,53.877,80.605,54.177,80.574,54.205,80.562,54.407,80.54,54.437,80.499,54.415,80.473]],[[59.689,79.956,59.331,79.923,59.203,79.933,59.169,79.948,59.1,79.964,58.919,79.985,58.946,80.042,59.001,80.054,59.545,80.119,59.802,80.083,59.911,79.994,59.689,79.956]],[[58.622,81.042,58.762,80.991,58.815,80.934,58.903,80.898,58.931,80.832,58.86,80.779,58.642,80.768,58.286,80.765,57.938,80.793,57.75,80.889,57.405,80.915,57.211,81.017,57.41,81.047,57.656,81.032,58.05,81.118,58.102,81.114,58.19,81.095,58.508,81.062,58.622,81.042]],[[47.983,45.488,47.968,45.47,47.92,45.562,47.918,45.618,47.947,45.647,47.987,45.554,47.983,45.488]],[[50.265,69.186,50.283,69.089,50.221,69.049,50.164,69.038,50.141,69.098,50.094,69.126,49.921,69.053,49.84,68.974,49.626,68.86,49.18,68.778,48.91,68.743,48.667,68.733,48.439,68.805,48.316,68.942,48.294,68.984,48.279,69.04,48.28,69.097,48.296,69.184,48.32,69.269,48.414,69.346,48.631,69.436,48.845,69.495,48.953,69.509,49.225,69.511,49.996,69.309,50.167,69.257,50.265,69.186]],[[63.374,80.7,63.188,80.698,63.002,80.713,62.76,80.763,62.52,80.822,62.593,80.853,62.819,80.894,63.116,80.967,63.615,80.981,63.856,80.981,64.096,80.998,64.166,81.036,64.21,81.106,64.256,81.144,64.31,81.175,64.575,81.198,64.802,81.197,65.028,81.169,65.172,81.144,65.31,81.096,65.382,81.057,65.36,81.008,65.372,80.968,65.437,80.931,64.997,80.819,64.548,80.755,63.374,80.7]],[[57.956,80.123,57.8,80.104,57.392,80.139,57.332,80.158,57.281,80.194,57.214,80.328,57.212,80.368,57.186,80.396,57.083,80.445,57.011,80.468,57.075,80.494,57.522,80.475,58.48,80.465,58.972,80.416,59.116,80.388,59.255,80.343,58.398,80.319,58.284,80.298,58.286,80.248,58.255,80.202,58.163,80.197,57.956,80.123]],[[62.168,80.835,62.228,80.794,62.192,80.73,62.115,80.684,62.076,80.617,61.769,80.601,61.681,80.586,61.597,80.535,61.285,80.505,61.051,80.419,60.722,80.435,60.278,80.494,59.9,80.446,59.65,80.431,59.346,80.505,59.304,80.522,59.288,80.573,59.306,80.618,59.387,80.713,59.495,80.767,59.549,80.784,59.592,80.817,59.716,80.836,60.095,80.849,60.235,80.838,60.278,80.801,60.482,80.804,60.82,80.827,61.313,80.863,61.597,80.893,61.851,80.886,62.103,80.867,62.168,80.835]],[[61.141,80.95,60.827,80.93,60.321,80.956,60.058,80.985,60.078,80.999,60.148,81.017,60.587,81.088,61.457,81.104,61.567,81.05,61.472,81.011,61.141,80.95]],[[53.521,80.185,52.856,80.173,52.636,80.179,52.607,80.191,52.55,80.202,52.344,80.213,52.213,80.264,52.27,80.276,52.577,80.297,52.681,80.319,52.716,80.348,52.854,80.402,53.186,80.413,53.329,80.402,53.346,80.366,53.486,80.323,53.852,80.268,53.778,80.228,53.653,80.223,53.521,80.185]],[[57.079,80.351,57.123,80.317,57.119,80.194,57.073,80.139,57.08,80.095,56.987,80.071,56.201,80.076,55.812,80.087,55.724,80.105,55.942,80.163,56.012,80.204,55.99,80.32,56.024,80.341,56.655,80.33,56.707,80.363,56.945,80.366,57.079,80.351]],[[57.81,81.546,57.863,81.506,58.017,81.484,58.436,81.464,58.564,81.418,58.372,81.387,57.859,81.368,57.912,81.303,58.015,81.255,57.913,81.198,57.77,81.17,57.451,81.136,57.159,81.178,56.822,81.238,56.669,81.198,56.513,81.175,56.364,81.179,56.192,81.224,55.717,81.188,55.573,81.228,55.466,81.311,55.782,81.329,56.157,81.303,56.405,81.387,56.719,81.423,56.973,81.511,57.092,81.541,57.365,81.535,57.456,81.543,57.717,81.565,57.81,81.546]],[[54.719,81.116,55.471,81.02,56.17,81.029,56.472,80.998,56.91,80.913,57.568,80.82,57.694,80.792,57.58,80.755,56.815,80.664,56.316,80.633,55.883,80.628,55.713,80.637,55.541,80.703,55.117,80.752,54.668,80.739,54.623,80.765,54.533,80.783,54.376,80.787,54.067,80.814,54.045,80.872,54.241,80.902,54.367,80.904,54.417,80.987,54.634,81.113,54.719,81.116]],[[63.651,81.609,63.529,81.597,62.885,81.609,62.573,81.633,62.531,81.647,62.515,81.659,62.106,81.679,62.284,81.707,62.795,81.719,63.71,81.687,63.767,81.664,63.782,81.65,63.651,81.609]],[[58.295,81.715,57.965,81.696,57.921,81.71,57.909,81.722,57.945,81.748,57.985,81.797,58.135,81.828,59.262,81.854,59.408,81.825,59.357,81.781,59.356,81.759,58.295,81.715]],[[92.683,79.685,92.441,79.675,92.154,79.685,91.684,79.791,91.376,79.835,91.126,79.905,91.07,79.981,91.229,80.031,91.426,80.049,91.752,80.052,92.173,80.045,92.593,79.997,93.482,79.941,93.803,79.905,93.604,79.817,93.382,79.784,93.155,79.738,92.926,79.704,92.683,79.685]],[[91.567,81.141,91.223,81.064,89.976,81.113,89.919,81.149,89.901,81.171,90.07,81.214,91.109,81.199,91.478,81.184,91.567,81.141]],[[141.01,73.999,140.507,73.919,140.409,73.922,140.183,74.005,140.102,74.184,140.194,74.237,140.3,74.257,140.407,74.266,140.849,74.274,140.944,74.265,141.039,74.243,141.079,74.209,141.097,74.168,141.047,74.05,141.01,73.999]],[[142.185,73.896,142.435,73.852,142.639,73.803,143.344,73.569,143.411,73.521,143.464,73.459,143.491,73.246,143.451,73.231,143.193,73.221,142.842,73.245,142.587,73.253,142.342,73.253,142.126,73.282,141.597,73.311,141.183,73.389,140.754,73.446,140.663,73.452,140.392,73.435,140.027,73.361,139.925,73.355,139.786,73.355,139.686,73.426,139.92,73.449,140.155,73.458,140.381,73.483,140.594,73.565,140.697,73.629,140.884,73.778,140.984,73.832,141.085,73.866,141.19,73.876,141.312,73.872,141.682,73.904,141.932,73.915,142.185,73.896]],[[135.949,75.41,135.746,75.382,135.452,75.39,135.473,75.463,135.523,75.496,135.593,75.576,135.561,75.636,135.578,75.71,135.614,75.766,135.699,75.845,135.788,75.798,135.849,75.729,135.905,75.694,136.127,75.626,136.169,75.606,135.983,75.522,135.965,75.486,136.021,75.438,135.949,75.41]],[[136.197,73.914,136.122,73.885,136.051,73.929,135.715,74.06,135.633,74.121,135.449,74.18,135.402,74.202,135.387,74.253,135.628,74.22,136.037,74.09,136.259,73.985,136.197,73.914]],[[137.941,55.093,138.031,55.053,138.172,55.06,138.206,55.034,138.096,54.991,138.017,54.901,137.991,54.821,137.959,54.789,137.87,54.75,137.79,54.697,137.721,54.663,137.661,54.653,137.526,54.826,137.463,54.873,137.276,54.792,137.233,54.791,137.275,54.891,137.384,55.001,137.436,55.016,137.544,55.163,137.577,55.197,137.91,55.11,137.941,55.093]],[[169.201,69.58,168.916,69.571,168.348,69.664,168.144,69.713,167.993,69.776,167.821,69.82,167.789,69.837,167.814,69.873,167.865,69.901,168.06,69.975,168.196,70.008,168.358,70.016,169.375,69.883,169.421,69.856,169.434,69.832,169.418,69.779,169.332,69.77,169.299,69.735,169.263,69.629,169.246,69.601,169.201,69.58]],[[163.635,58.603,163.471,58.509,163.447,58.525,163.432,58.546,163.427,58.579,163.577,58.641,163.727,58.799,163.784,58.93,163.767,58.972,163.761,59.015,164.202,59.096,164.517,59.227,164.573,59.221,164.629,59.112,164.662,58.971,164.616,58.886,164.279,58.838,163.96,58.744,163.635,58.603]],[[166.65,54.839,166.645,54.694,166.521,54.768,166.464,54.827,166.382,54.838,166.325,54.865,166.23,54.937,166.12,55.03,166.082,55.077,166.066,55.136,165.992,55.19,165.751,55.295,165.83,55.307,165.931,55.351,166.212,55.324,166.276,55.312,166.23,55.242,166.248,55.165,166.404,55.006,166.479,54.95,166.577,54.908,166.65,54.839]],[[154.81,49.312,154.715,49.268,154.611,49.294,154.613,49.381,154.825,49.647,154.9,49.63,154.883,49.566,154.802,49.468,154.83,49.348,154.81,49.312]],[[156.405,50.658,156.365,50.634,156.326,50.639,156.196,50.702,156.168,50.732,156.213,50.785,156.376,50.862,156.456,50.86,156.488,50.843,156.483,50.751,156.405,50.658]],[[155.921,50.302,155.792,50.202,155.608,50.177,155.516,50.146,155.449,50.078,155.397,50.041,155.289,50.061,155.243,50.095,155.243,50.213,155.195,50.265,155.218,50.298,155.327,50.293,155.434,50.369,155.68,50.401,155.773,50.482,155.885,50.684,156.002,50.757,156.097,50.772,156.123,50.671,156.101,50.559,156.044,50.452,155.921,50.302]],[[154.081,48.79,154.043,48.739,154.001,48.756,153.992,48.773,154.092,48.832,154.126,48.904,154.199,48.905,154.228,48.892,154.205,48.857,154.081,48.79]],[[155.645,50.822,155.554,50.811,155.513,50.837,155.483,50.87,155.467,50.914,155.569,50.934,155.64,50.91,155.654,50.845,155.645,50.822]],[[153.101,47.763,153.054,47.706,153.004,47.713,152.984,47.728,153.049,47.797,153.079,47.809,153.101,47.763]],[[152.002,46.897,151.816,46.787,151.754,46.788,151.723,46.829,151.715,46.853,151.864,46.869,152.04,47.015,152.166,47.11,152.235,47.143,152.289,47.142,152.002,46.897]],[[149.688,45.642,149.539,45.591,149.447,45.593,149.666,45.84,149.796,45.876,149.962,46.022,150.309,46.2,150.349,46.213,150.553,46.209,150.235,46.012,150.195,45.933,150.057,45.849,149.954,45.822,149.883,45.783,149.688,45.642]],[[148.6,45.318,148.415,45.247,148.262,45.217,148.005,45.07,147.914,44.99,147.784,44.959,147.658,44.977,147.622,44.945,147.61,44.887,147.563,44.836,147.31,44.678,147.207,44.554,147.098,44.531,146.897,44.404,146.933,44.513,146.974,44.566,147.141,44.663,147.155,44.766,147.247,44.856,147.43,44.945,147.558,45.062,147.658,45.093,147.769,45.191,147.886,45.226,147.873,45.3,147.924,45.383,147.965,45.378,148.056,45.262,148.13,45.258,148.324,45.282,148.612,45.485,148.707,45.521,148.773,45.526,148.812,45.51,148.826,45.486,148.825,45.456,148.803,45.414,148.837,45.363,148.791,45.324,148.6,45.318]],[[146.714,43.744,146.683,43.716,146.609,43.74,146.613,43.797,146.622,43.813,146.825,43.86,146.884,43.829,146.899,43.804,146.714,43.744]],[[146.208,44.498,146.356,44.425,146.568,44.44,146.516,44.375,146.437,44.376,146.296,44.281,146.173,44.269,146.112,44.246,145.914,44.104,145.887,44.048,145.767,43.941,145.587,43.845,145.556,43.665,145.439,43.737,145.426,43.81,145.462,43.871,145.666,43.999,145.748,44.072,145.773,44.129,145.852,44.193,145.89,44.249,145.94,44.273,146.112,44.5,146.208,44.498]],[[113.387,74.4,113.353,74.353,113.299,74.317,113.259,74.273,113.19,74.239,112.978,74.197,112.811,74.103,112.782,74.095,112.196,74.146,112.105,74.163,111.912,74.219,111.643,74.273,111.503,74.353,111.57,74.368,111.638,74.374,111.88,74.364,111.949,74.389,111.983,74.456,111.989,74.496,112.008,74.527,112.084,74.549,112.952,74.48,113.286,74.441,113.387,74.4]],[[76.249,79.651,76.373,79.615,76.467,79.643,77.36,79.557,77.549,79.524,77.589,79.502,76.81,79.49,76.65,79.493,76.637,79.544,76.458,79.545,76.154,79.579,76.072,79.626,76.052,79.645,76.148,79.664,76.249,79.651]],[[80.027,80.848,79.099,80.812,79.007,80.835,78.978,80.848,79.11,80.924,79.217,80.96,79.807,80.975,80.28,80.95,80.428,80.928,80.373,80.883,80.345,80.868,80.027,80.848]],[[70.674,73.095,70.38,73.048,70.298,73.044,70.119,73.056,70.041,73.037,69.92,73.085,69.93,73.127,69.986,73.169,70.019,73.224,69.996,73.359,70.15,73.445,70.35,73.478,70.94,73.514,71.023,73.504,71.141,73.478,71.232,73.448,71.351,73.372,71.445,73.342,71.59,73.283,71.63,73.225,71.626,73.174,71.356,73.162,70.887,73.12,70.674,73.095]],[[77.633,72.291,77.146,72.282,76.906,72.298,76.871,72.317,76.903,72.366,77.15,72.439,77.26,72.486,77.378,72.565,77.579,72.631,77.749,72.631,78.279,72.553,78.353,72.504,78.365,72.482,78.154,72.417,78.007,72.392,77.781,72.309,77.633,72.291]],[[79.501,72.722,79.431,72.711,78.881,72.752,78.69,72.803,78.633,72.851,78.657,72.892,79.164,73.094,79.357,73.039,79.413,72.983,79.541,72.919,79.538,72.769,79.501,72.722]],[[82.172,75.419,82.209,75.387,82.222,75.351,82.179,75.339,82.05,75.341,81.979,75.247,81.905,75.263,81.861,75.317,81.698,75.281,81.655,75.289,81.579,75.331,81.532,75.34,81.501,75.368,81.712,75.451,81.842,75.407,81.927,75.41,81.91,75.46,81.913,75.498,82.022,75.513,82.166,75.516,82.172,75.419]],[[60.45,69.935,60.481,69.885,60.477,69.794,60.44,69.726,60.327,69.715,60.216,69.688,60.026,69.717,59.92,69.697,59.813,69.696,59.725,69.706,59.637,69.721,59.578,69.739,59.581,69.791,59.503,69.866,59.382,69.89,59.268,69.898,59.144,69.922,59.083,69.911,59.004,69.883,58.953,69.893,58.68,70.051,58.634,70.088,58.606,70.129,58.568,70.156,58.473,70.267,58.52,70.318,58.615,70.351,58.678,70.36,58.794,70.433,59.005,70.465,59.048,70.46,59.088,70.437,59.31,70.362,59.426,70.311,59.529,70.249,59.636,70.197,59.956,70.108,60.172,70.023,60.393,69.962,60.45,69.935]],[[20.958,55.279,20.859,55.184,20.595,54.982,20.678,54.956,20.774,54.947,20.888,54.909,20.996,54.903,21.189,54.935,21.223,55.108,21.236,55.264,21.298,55.264,21.389,55.276,21.447,55.234,21.555,55.195,21.683,55.16,21.874,55.101,22.072,55.064,22.138,55.059,22.346,55.064,22.567,55.059,22.627,54.971,22.737,54.929,22.825,54.871,22.831,54.838,22.71,54.633,22.684,54.563,22.68,54.493,22.724,54.406,22.766,54.357,22.732,54.35,22.168,54.36,21.634,54.376,21.141,54.392,20.665,54.407,20.208,54.421,19.924,54.434,19.644,54.447,19.604,54.459,19.758,54.545,19.859,54.634,19.944,54.75,19.953,54.83,19.975,54.921,20.108,54.956,20.397,54.951,20.52,54.995,20.679,55.103,20.846,55.232,20.9,55.287,20.958,55.279]],[[33.594,46.096,33.654,46.146,33.66,46.22,33.807,46.208,34.027,46.107,34.128,46.09,34.224,46.101,34.354,46.062,34.45,45.966,34.523,45.977,34.687,45.977,34.794,45.892,34.8,45.791,34.946,45.729,35.002,45.733,35.023,45.701,35.26,45.447,35.374,45.354,35.458,45.316,35.558,45.311,35.751,45.389,35.833,45.402,36.013,45.372,36.077,45.424,36.171,45.453,36.29,45.457,36.427,45.433,36.575,45.394,36.514,45.304,36.451,45.232,36.428,45.153,36.393,45.065,36.23,45.026,36.055,45.031,35.87,45.005,35.804,45.04,35.759,45.071,35.678,45.102,35.57,45.119,35.473,45.098,35.358,44.978,35.155,44.896,35.088,44.803,34.888,44.824,34.717,44.807,34.47,44.722,34.282,44.538,34.074,44.424,33.91,44.388,33.756,44.399,33.656,44.433,33.451,44.554,33.463,44.597,33.491,44.619,33.53,44.681,33.612,44.908,33.601,44.981,33.555,45.098,33.392,45.188,33.262,45.171,33.187,45.195,32.919,45.348,32.773,45.359,32.611,45.328,32.552,45.35,32.508,45.404,32.828,45.593,33.142,45.749,33.28,45.765,33.466,45.838,33.665,45.947,33.637,46.033,33.594,46.096]]]},{"n":"Romania","c":"RO","r":3,"ml":3,"lx":24.97,"ly":45.73,"b":[20.242,43.671,29.706,48.263],"p":[[[28.213,45.45,28.318,45.347,28.451,45.292,28.761,45.234,28.788,45.241,28.791,45.252,28.77,45.267,28.767,45.286,28.782,45.31,28.824,45.311,28.894,45.29,29.027,45.321,29.224,45.403,29.404,45.42,29.568,45.371,29.652,45.314,29.706,45.26,29.689,45.193,29.679,45.152,29.635,44.98,29.605,44.915,29.558,44.843,29.048,44.758,29.081,44.799,29.069,44.871,29.048,44.926,29.095,44.975,28.981,44.993,28.931,44.966,28.892,44.919,28.926,44.81,28.87,44.75,28.849,44.716,28.846,44.637,28.814,44.602,28.807,44.565,28.888,44.575,28.852,44.506,28.699,44.374,28.645,44.296,28.659,43.984,28.591,43.797,28.585,43.742,28.423,43.74,28.375,43.745,28.222,43.773,28.05,43.822,27.949,43.919,27.884,43.987,27.739,43.956,27.711,43.965,27.671,43.998,27.561,44.02,27.425,44.021,27.121,44.146,27.087,44.167,26.848,44.146,26.489,44.084,26.216,44.007,25.933,43.871,25.819,43.767,25.686,43.712,25.497,43.671,25.16,43.686,24.808,43.738,24.431,43.794,24.227,43.763,23.951,43.787,23.535,43.854,23.225,43.874,22.919,43.834,22.868,43.865,22.856,43.899,22.868,43.948,22.911,43.987,22.985,44.017,23.024,44.047,23.029,44.078,22.945,44.127,22.775,44.195,22.705,44.238,22.688,44.248,22.683,44.286,22.648,44.316,22.582,44.338,22.531,44.378,22.495,44.435,22.502,44.49,22.554,44.54,22.62,44.562,22.701,44.556,22.734,44.57,22.721,44.606,22.642,44.651,22.498,44.706,22.351,44.676,22.201,44.561,22.093,44.542,22.027,44.62,21.909,44.666,21.74,44.681,21.636,44.71,21.597,44.755,21.523,44.79,21.36,44.827,21.358,44.862,21.384,44.87,21.442,44.873,21.52,44.881,21.532,44.901,21.533,44.919,21.472,44.942,21.41,44.958,21.378,44.973,21.357,44.991,21.353,45.009,21.371,45.021,21.396,45.022,21.421,45.033,21.434,45.075,21.468,45.11,21.492,45.122,21.49,45.148,21.465,45.172,21.431,45.193,21.382,45.205,21.226,45.241,21.148,45.292,21.1,45.294,21.024,45.322,20.942,45.365,20.871,45.428,20.794,45.468,20.774,45.484,20.772,45.5,20.787,45.517,20.786,45.536,20.766,45.597,20.779,45.662,20.776,45.723,20.775,45.75,20.76,45.758,20.747,45.749,20.728,45.737,20.709,45.735,20.653,45.779,20.581,45.869,20.533,45.9,20.438,45.941,20.359,45.975,20.301,46.051,20.242,46.109,20.281,46.133,20.508,46.167,20.614,46.133,20.661,46.146,20.707,46.173,20.733,46.194,20.737,46.217,20.76,46.246,20.837,46.26,21.04,46.242,21.122,46.282,21.152,46.304,21.17,46.353,21.192,46.392,21.265,46.412,21.263,46.448,21.252,46.486,21.295,46.572,21.32,46.608,21.361,46.621,21.411,46.648,21.497,46.704,21.477,46.753,21.494,46.79,21.584,46.878,21.653,46.964,21.651,47.007,21.661,47.044,21.722,47.085,21.785,47.138,21.869,47.305,21.899,47.333,21.954,47.364,21.995,47.396,22,47.505,22.038,47.537,22.112,47.572,22.185,47.629,22.245,47.696,22.291,47.728,22.351,47.736,22.417,47.763,22.491,47.773,22.563,47.76,22.608,47.766,22.677,47.799,22.852,47.923,22.877,47.947,22.913,47.964,23.055,48.007,23.091,48.049,23.139,48.087,23.203,48.085,23.408,47.99,23.629,47.996,23.669,47.992,23.682,47.99,23.709,47.983,24.002,47.936,24.047,47.941,24.06,47.945,24.178,47.906,24.282,47.911,24.381,47.938,24.484,47.947,24.579,47.931,24.651,47.877,24.838,47.761,24.893,47.718,24.979,47.724,25.074,47.746,25.17,47.823,25.464,47.911,25.689,47.932,25.909,47.968,26.163,47.993,26.236,48.064,26.277,48.113,26.306,48.204,26.442,48.23,26.572,48.248,26.619,48.26,26.714,48.263,26.787,48.256,26.901,48.211,26.981,48.155,27.012,48.11,27.08,48.048,27.152,47.959,27.231,47.842,27.248,47.782,27.278,47.718,27.337,47.64,27.449,47.553,27.465,47.537,27.516,47.476,27.614,47.341,27.696,47.286,27.768,47.228,27.802,47.168,27.854,47.115,27.974,47.043,28.072,46.978,28.15,46.792,28.205,46.706,28.239,46.641,28.223,46.508,28.244,46.451,28.2,46.348,28.119,46.139,28.1,45.973,28.114,45.883,28.116,45.826,28.135,45.789,28.156,45.713,28.16,45.647,28.131,45.628,28.09,45.613,28.075,45.599,28.112,45.569,28.163,45.514,28.213,45.45]]]},{"n":"Qatar","c":"QA","r":5,"ml":4,"lx":51.14,"ly":25.24,"b":[50.755,24.565,51.609,26.153],"p":[[[51.268,24.607,51.178,24.587,51.093,24.565,51.023,24.565,50.966,24.574,50.928,24.595,50.856,24.68,50.804,24.789,50.836,24.85,50.847,24.889,50.777,25.177,50.755,25.399,50.763,25.445,50.803,25.497,50.869,25.613,50.904,25.724,51.003,25.981,51.108,26.081,51.262,26.153,51.389,26.011,51.543,25.902,51.572,25.781,51.527,25.682,51.485,25.525,51.51,25.452,51.52,25.39,51.561,25.284,51.602,25.148,51.609,25.053,51.587,24.965,51.533,24.891,51.428,24.668,51.396,24.645,51.268,24.607]]]},{"n":"Portugal","c":"PT","r":2,"ml":3,"lx":-8.27,"ly":39.61,"b":[-31.283,32.648,-6.212,42.137],"p":[[[-17.191,32.869,-17.054,32.816,-16.929,32.841,-16.774,32.774,-16.693,32.758,-16.765,32.71,-16.837,32.648,-17.018,32.663,-17.171,32.722,-17.226,32.767,-17.241,32.807,-17.191,32.869]],[[-8.777,41.941,-8.683,42.008,-8.59,42.053,-8.538,42.069,-8.323,42.115,-8.266,42.137,-8.213,42.134,-8.204,42.112,-8.174,42.069,-8.139,42.04,-8.13,42.018,-8.213,41.927,-8.225,41.896,-8.181,41.837,-8.174,41.82,-8.152,41.812,-8.094,41.814,-7.991,41.852,-7.921,41.884,-7.896,41.871,-7.693,41.888,-7.645,41.874,-7.613,41.858,-7.513,41.836,-7.404,41.834,-7.269,41.864,-7.21,41.895,-7.198,41.929,-7.195,41.955,-7.178,41.972,-7.147,41.981,-7.099,41.964,-7.03,41.951,-6.866,41.945,-6.833,41.964,-6.777,41.958,-6.704,41.935,-6.618,41.942,-6.575,41.913,-6.558,41.874,-6.553,41.79,-6.559,41.704,-6.542,41.673,-6.485,41.664,-6.392,41.665,-6.308,41.642,-6.243,41.602,-6.222,41.56,-6.212,41.532,-6.244,41.516,-6.289,41.455,-6.403,41.375,-6.566,41.304,-6.69,41.215,-6.776,41.108,-6.883,41.062,-6.916,41.038,-6.928,41.009,-6.858,40.878,-6.836,40.777,-6.818,40.654,-6.83,40.619,-6.836,40.483,-6.852,40.443,-6.848,40.411,-6.822,40.376,-6.81,40.343,-6.859,40.301,-6.948,40.252,-7.015,40.208,-7.033,40.168,-7.028,40.143,-6.916,40.057,-6.896,40.022,-6.911,39.937,-6.975,39.798,-7.037,39.714,-7.047,39.706,-7.118,39.682,-7.454,39.681,-7.536,39.662,-7.524,39.645,-7.445,39.536,-7.363,39.478,-7.335,39.465,-7.306,39.338,-7.172,39.135,-7.043,39.107,-6.998,39.056,-7.006,38.985,-7.046,38.907,-7.125,38.827,-7.22,38.771,-7.282,38.715,-7.286,38.649,-7.306,38.567,-7.336,38.501,-7.343,38.457,-7.106,38.181,-6.975,38.194,-6.958,38.188,-6.981,38.122,-7.023,38.045,-7.073,38.03,-7.185,38.006,-7.292,37.906,-7.379,37.786,-7.444,37.728,-7.504,37.585,-7.496,37.524,-7.467,37.428,-7.406,37.179,-7.494,37.168,-7.834,37.006,-7.94,37.005,-8.137,37.077,-8.484,37.1,-8.598,37.121,-8.739,37.075,-8.848,37.076,-8.935,37.016,-8.998,37.032,-8.926,37.166,-8.814,37.431,-8.819,37.592,-8.792,37.733,-8.823,37.872,-8.879,37.959,-8.802,38.184,-8.811,38.3,-8.881,38.447,-8.668,38.424,-8.734,38.482,-8.799,38.518,-8.862,38.51,-8.915,38.512,-9.096,38.455,-9.187,38.438,-9.213,38.448,-9.203,38.539,-9.25,38.657,-9.178,38.688,-9.093,38.697,-9.021,38.747,-8.977,38.803,-9,38.903,-8.938,38.998,-8.792,39.078,-8.867,39.066,-8.954,39.016,-9.091,38.835,-9.136,38.743,-9.252,38.713,-9.357,38.698,-9.41,38.708,-9.474,38.731,-9.48,38.799,-9.475,38.853,-9.431,38.96,-9.414,39.112,-9.353,39.248,-9.357,39.284,-9.375,39.338,-9.32,39.391,-9.251,39.426,-9.148,39.543,-9.004,39.821,-8.838,40.116,-8.851,40.152,-8.887,40.179,-8.873,40.259,-8.772,40.606,-8.732,40.651,-8.685,40.753,-8.674,40.917,-8.656,41.029,-8.66,41.086,-8.675,41.154,-8.738,41.285,-8.806,41.56,-8.811,41.652,-8.755,41.698,-8.846,41.705,-8.888,41.765,-8.878,41.832,-8.777,41.941]],[[-25.027,36.96,-25.032,36.942,-25.088,36.949,-25.16,36.943,-25.198,36.997,-25.164,37.019,-25.083,37.024,-25.044,37,-25.027,36.96]],[[-31.137,39.407,-31.181,39.359,-31.258,39.376,-31.283,39.394,-31.261,39.497,-31.2,39.521,-31.139,39.479,-31.137,39.407]],[[-27.075,38.643,-27.095,38.634,-27.303,38.661,-27.362,38.698,-27.386,38.766,-27.351,38.789,-27.26,38.803,-27.127,38.79,-27.042,38.741,-27.042,38.679,-27.075,38.643]],[[-27.778,38.556,-27.826,38.544,-28.092,38.621,-28.187,38.655,-28.311,38.744,-27.963,38.636,-27.778,38.556]],[[-28.641,38.525,-28.744,38.522,-28.842,38.598,-28.698,38.638,-28.655,38.614,-28.624,38.586,-28.606,38.551,-28.641,38.525]],[[-28.147,38.453,-28.065,38.413,-28.19,38.404,-28.231,38.385,-28.332,38.413,-28.454,38.409,-28.531,38.463,-28.549,38.519,-28.51,38.553,-28.402,38.553,-28.147,38.453]],[[-25.649,37.841,-25.585,37.834,-25.267,37.849,-25.182,37.838,-25.191,37.764,-25.251,37.735,-25.439,37.715,-25.734,37.763,-25.834,37.826,-25.848,37.872,-25.846,37.894,-25.784,37.911,-25.649,37.841]]]},{"n":"Poland","c":"PL","r":3,"ml":2.5,"lx":19.49,"ly":51.99,"b":[14.129,49.021,24.106,54.838],"p":[[[23.605,51.518,23.659,51.449,23.68,51.395,23.658,51.352,23.664,51.31,23.712,51.265,23.863,51.126,23.938,50.993,23.986,50.94,24.096,50.873,24.106,50.845,24.062,50.82,24.026,50.816,23.997,50.809,23.978,50.786,24.007,50.76,24.046,50.723,24.095,50.617,24.09,50.53,24.053,50.508,24.005,50.457,23.973,50.41,23.712,50.377,23.649,50.327,23.506,50.23,23.409,50.174,23.264,50.073,23.036,49.899,22.952,49.826,22.891,49.766,22.706,49.606,22.649,49.539,22.661,49.484,22.72,49.354,22.732,49.295,22.722,49.241,22.702,49.193,22.706,49.171,22.76,49.136,22.847,49.081,22.852,49.063,22.84,49.039,22.81,49.021,22.701,49.04,22.58,49.077,22.539,49.073,22.473,49.081,22.203,49.153,22.02,49.21,22.002,49.246,21.968,49.299,21.89,49.343,21.712,49.382,21.64,49.412,21.35,49.429,21.225,49.429,21.136,49.417,21.079,49.418,21.001,49.34,20.947,49.317,20.868,49.315,20.8,49.329,20.729,49.37,20.616,49.392,20.535,49.381,20.475,49.39,20.423,49.392,20.405,49.384,20.363,49.385,20.303,49.366,20.237,49.338,20.164,49.316,20.108,49.271,20.058,49.181,19.916,49.221,19.869,49.204,19.802,49.192,19.757,49.204,19.767,49.235,19.788,49.27,19.787,49.319,19.774,49.372,19.73,49.39,19.664,49.396,19.63,49.407,19.627,49.424,19.593,49.447,19.535,49.505,19.48,49.576,19.442,49.598,19.386,49.564,19.302,49.525,19.25,49.511,19.149,49.4,18.968,49.396,18.957,49.448,18.938,49.498,18.832,49.511,18.829,49.54,18.807,49.614,18.595,49.758,18.569,49.818,18.577,49.841,18.562,49.879,18.516,49.902,18.348,49.93,18.305,49.914,18.266,49.93,18.205,49.965,18.099,49.993,18.088,50.007,18.05,50.032,18.028,50.035,18.015,50.02,17.984,49.999,17.875,49.972,17.831,49.983,17.792,50.007,17.747,50.057,17.681,50.101,17.627,50.116,17.596,50.14,17.589,50.157,17.709,50.194,17.735,50.231,17.72,50.299,17.702,50.307,17.655,50.284,17.555,50.264,17.462,50.255,17.415,50.255,17.152,50.378,16.981,50.416,16.88,50.427,16.869,50.415,16.915,50.345,16.993,50.26,16.99,50.237,16.895,50.202,16.842,50.187,16.779,50.157,16.725,50.116,16.679,50.097,16.639,50.102,16.597,50.122,16.488,50.248,16.35,50.345,16.334,50.367,16.291,50.372,16.231,50.394,16.21,50.424,16.241,50.455,16.283,50.483,16.357,50.5,16.379,50.517,16.392,50.542,16.42,50.574,16.413,50.585,16.36,50.621,16.282,50.656,16.066,50.63,16.007,50.612,15.974,50.635,15.949,50.67,15.894,50.677,15.819,50.709,15.731,50.74,15.644,50.749,15.464,50.794,15.395,50.796,15.354,50.812,15.313,50.846,15.277,50.883,15.259,50.959,15.126,50.993,14.994,51.014,14.984,51.003,14.99,50.927,14.983,50.887,14.896,50.861,14.809,50.859,14.814,50.872,14.917,51.009,14.964,51.095,15.017,51.253,14.953,51.377,14.936,51.435,14.906,51.463,14.725,51.524,14.711,51.545,14.739,51.627,14.725,51.662,14.681,51.698,14.624,51.771,14.602,51.832,14.675,51.905,14.693,51.958,14.725,52.031,14.748,52.071,14.753,52.082,14.705,52.11,14.692,52.15,14.705,52.207,14.68,52.25,14.616,52.278,14.574,52.314,14.555,52.36,14.57,52.431,14.619,52.529,14.514,52.646,14.254,52.783,14.129,52.878,14.139,52.933,14.194,52.982,14.293,53.027,14.369,53.106,14.411,53.199,14.412,53.217,14.415,53.283,14.299,53.556,14.28,53.625,14.266,53.707,14.259,53.73,14.488,53.672,14.583,53.639,14.572,53.676,14.552,53.707,14.565,53.754,14.558,53.823,14.351,53.859,14.214,53.871,14.198,53.919,14.211,53.95,14.249,53.932,14.384,53.925,14.716,54.018,15.288,54.14,15.9,54.254,16.043,54.266,16.186,54.29,16.239,54.333,16.292,54.362,16.376,54.437,16.56,54.554,16.885,54.596,17.007,54.652,17.262,54.73,17.843,54.817,18.086,54.836,18.323,54.838,18.535,54.769,18.759,54.685,18.8,54.633,18.678,54.665,18.502,54.742,18.436,54.745,18.587,54.513,18.67,54.431,18.836,54.37,18.976,54.349,19.407,54.386,19.56,54.435,19.604,54.459,19.644,54.447,19.924,54.434,20.208,54.421,20.665,54.407,21.141,54.392,21.634,54.376,22.168,54.36,22.732,54.35,22.766,54.357,22.824,54.396,22.894,54.391,22.977,54.366,23.016,54.348,23.032,54.328,23.042,54.304,23.088,54.299,23.17,54.281,23.282,54.24,23.37,54.2,23.454,54.143,23.481,54.079,23.483,54.006,23.478,53.959,23.485,53.94,23.599,53.599,23.789,53.271,23.859,53.112,23.887,53.028,23.909,52.905,23.916,52.819,23.915,52.77,23.901,52.704,23.845,52.664,23.48,52.552,23.411,52.516,23.303,52.428,23.204,52.338,23.181,52.307,23.175,52.287,23.197,52.257,23.327,52.208,23.458,52.17,23.501,52.14,23.598,52.103,23.633,52.07,23.652,52.04,23.651,51.973,23.607,51.88,23.626,51.809,23.581,51.762,23.545,51.71,23.54,51.619,23.605,51.518]]]},{"n":"Philippines","c":"PH","r":2,"ml":2.5,"lx":122.47,"ly":11.2,"b":[116.97,5.06,126.593,20.841],"p":[[[121.102,18.615,121.254,18.563,121.593,18.376,121.717,18.33,121.846,18.295,121.948,18.285,122.038,18.328,122.077,18.372,122.147,18.487,122.221,18.501,122.266,18.459,122.3,18.403,122.315,18.32,122.294,18.234,122.223,18.157,122.179,18.064,122.151,17.756,122.152,17.664,122.175,17.576,122.237,17.435,122.269,17.395,122.362,17.345,122.388,17.307,122.393,17.238,122.408,17.178,122.468,17.155,122.519,17.125,122.5,17.058,122.468,16.99,122.426,16.823,122.226,16.435,122.214,16.352,122.135,16.185,121.975,16.158,121.789,16.077,121.685,16.015,121.595,15.933,121.561,15.827,121.59,15.778,121.609,15.726,121.607,15.67,121.579,15.623,121.49,15.51,121.452,15.417,121.412,15.375,121.392,15.324,121.399,15.267,121.435,15.216,121.544,14.999,121.661,14.79,121.686,14.765,121.695,14.737,121.627,14.682,121.628,14.581,121.649,14.481,121.752,14.234,121.767,14.168,121.8,14.114,121.853,14.063,121.912,14.02,122.08,13.947,122.144,13.933,122.212,13.93,122.228,13.979,122.288,13.996,122.274,14.045,122.203,14.112,122.2,14.148,122.238,14.175,122.283,14.191,122.384,14.264,122.491,14.322,122.627,14.318,122.761,14.285,122.856,14.251,122.934,14.188,123.015,14.08,123.071,13.96,123.071,13.903,123.057,13.845,123.06,13.789,123.102,13.75,123.231,13.747,123.297,13.836,123.305,13.937,123.259,13.975,123.28,14.025,123.32,14.062,123.377,14.029,123.432,13.966,123.633,13.898,123.684,13.897,123.726,13.884,123.816,13.837,123.858,13.8,123.806,13.722,123.607,13.704,123.55,13.646,123.608,13.528,123.704,13.432,123.765,13.354,123.819,13.269,123.817,13.192,123.785,13.111,123.873,13.117,123.955,13.1,124.069,13.032,124.105,13.025,124.143,13.036,124.137,12.791,124.06,12.567,123.962,12.595,123.878,12.69,123.895,12.805,123.949,12.916,123.918,12.94,123.864,12.931,123.802,12.906,123.736,12.897,123.627,12.912,123.402,13.033,123.311,13.044,123.29,13.099,123.296,13.216,123.206,13.354,123.192,13.403,123.163,13.442,122.896,13.592,122.863,13.617,122.781,13.737,122.595,13.908,122.543,13.925,122.486,13.93,122.468,13.887,122.494,13.82,122.504,13.763,122.5,13.703,122.508,13.657,122.596,13.562,122.609,13.517,122.668,13.395,122.675,13.253,122.6,13.194,122.515,13.26,122.513,13.314,122.498,13.364,122.407,13.493,122.377,13.521,122.205,13.648,122.073,13.788,121.778,13.938,121.743,13.946,121.692,13.935,121.643,13.916,121.501,13.842,121.451,13.791,121.446,13.712,121.344,13.649,121.204,13.64,121.096,13.679,121.006,13.758,120.932,13.762,120.841,13.885,120.729,13.901,120.637,13.804,120.617,13.995,120.617,14.188,120.643,14.244,120.688,14.291,120.922,14.493,120.952,14.558,120.941,14.645,120.888,14.716,120.804,14.759,120.708,14.777,120.638,14.816,120.584,14.881,120.547,14.766,120.583,14.595,120.589,14.483,120.556,14.441,120.496,14.44,120.439,14.453,120.396,14.493,120.365,14.608,120.284,14.684,120.251,14.793,120.214,14.809,120.138,14.8,120.082,14.851,120.045,14.978,120.037,15.115,120.005,15.229,119.959,15.34,119.933,15.431,119.892,15.838,119.881,15.875,119.86,15.906,119.808,15.952,119.769,16.008,119.762,16.055,119.773,16.255,119.79,16.303,119.831,16.327,119.886,16.287,119.93,16.239,119.985,16.215,120.033,16.185,120.124,16.066,120.16,16.048,120.271,16.051,120.337,16.066,120.369,16.11,120.389,16.161,120.389,16.222,120.325,16.4,120.305,16.529,120.304,16.645,120.321,16.762,120.409,16.956,120.42,17.09,120.412,17.27,120.427,17.377,120.425,17.438,120.372,17.535,120.358,17.638,120.505,18.163,120.551,18.264,120.584,18.369,120.6,18.508,120.709,18.546,120.814,18.603,120.868,18.599,120.925,18.585,121.051,18.614,121.102,18.615]],[[117.311,8.44,117.219,8.367,117.229,8.457,117.256,8.541,117.35,8.714,117.418,8.767,117.53,8.903,117.593,8.968,117.745,9.098,117.885,9.241,117.932,9.251,117.983,9.253,118.024,9.27,118.115,9.347,118.344,9.603,118.533,9.794,118.728,10.035,118.82,10.105,118.845,10.131,119.024,10.354,119.08,10.386,119.143,10.409,119.186,10.439,119.224,10.477,119.287,10.574,119.313,10.687,119.297,10.751,119.261,10.845,119.306,10.974,119.341,11.033,119.465,11.294,119.501,11.346,119.553,11.314,119.56,11.267,119.535,11.157,119.533,11.102,119.562,11.046,119.527,10.953,119.616,10.707,119.684,10.552,119.687,10.5,119.595,10.407,119.541,10.379,119.422,10.354,119.369,10.327,119.285,10.252,119.232,10.152,119.219,10.101,119.192,10.061,118.949,9.993,118.835,9.949,118.782,9.916,118.755,9.862,118.774,9.767,118.57,9.423,118.504,9.333,118.435,9.256,118.35,9.201,118.229,9.168,118.134,9.101,118.069,8.984,117.99,8.877,117.889,8.798,117.78,8.729,117.68,8.678,117.572,8.642,117.54,8.596,117.517,8.538,117.469,8.511,117.413,8.496,117.311,8.44]],[[122.496,11.615,122.613,11.564,122.726,11.608,122.838,11.596,122.931,11.529,122.901,11.487,122.895,11.441,123.103,11.541,123.158,11.536,123.156,11.443,123.144,11.364,123.12,11.287,123.075,11.197,123.017,11.117,122.939,11.058,122.847,11.022,122.803,10.99,122.789,10.941,122.791,10.88,122.77,10.824,122.673,10.801,122.522,10.692,122.198,10.623,122.109,10.576,122.052,10.514,121.988,10.458,121.954,10.444,121.938,10.471,121.934,10.494,121.98,10.639,121.972,10.699,121.95,10.757,121.964,10.872,122.021,10.979,122.051,11.097,122.06,11.326,122.104,11.643,122.101,11.681,122.067,11.724,121.941,11.758,121.891,11.791,121.916,11.854,121.964,11.897,122.029,11.895,122.087,11.855,122.291,11.772,122.399,11.702,122.496,11.615]],[[123.131,9.064,123.065,9.053,122.995,9.059,122.948,9.108,122.867,9.32,122.772,9.371,122.665,9.41,122.61,9.443,122.563,9.483,122.411,9.694,122.4,9.823,122.426,9.896,122.471,9.962,122.523,9.979,122.648,9.982,122.713,9.99,122.856,10.087,122.866,10.125,122.867,10.284,122.852,10.395,122.817,10.504,122.856,10.553,122.906,10.603,122.958,10.698,122.969,10.766,122.97,10.836,122.983,10.887,123.024,10.912,123.222,10.989,123.257,10.994,123.511,10.923,123.563,10.816,123.568,10.781,123.528,10.662,123.493,10.582,123.407,10.459,123.344,10.325,123.296,10.125,123.266,10.059,123.187,9.933,123.162,9.864,123.163,9.715,123.149,9.659,123.15,9.606,123.308,9.357,123.322,9.317,123.321,9.273,123.293,9.217,123.229,9.121,123.192,9.088,123.131,9.064]],[[124.575,11.343,124.644,11.308,124.724,11.322,124.821,11.401,124.93,11.373,124.994,11.256,125.027,11.212,125.044,11.135,125.04,10.952,125.013,10.786,125.034,10.751,125.084,10.722,125.128,10.685,125.164,10.637,125.188,10.585,125.197,10.457,125.26,10.35,125.268,10.308,125.253,10.264,125.148,10.272,125.14,10.235,125.143,10.189,125.105,10.218,125.044,10.323,124.988,10.368,125.005,10.197,125.024,10.115,125.027,10.033,124.929,10.096,124.813,10.135,124.781,10.168,124.792,10.275,124.79,10.328,124.738,10.44,124.799,10.682,124.797,10.732,124.787,10.781,124.739,10.88,124.663,10.962,124.616,10.962,124.503,10.904,124.446,10.924,124.412,11.15,124.366,11.371,124.331,11.427,124.308,11.486,124.331,11.535,124.374,11.515,124.436,11.457,124.511,11.424,124.548,11.395,124.575,11.343]],[[125.24,12.528,125.31,12.446,125.328,12.387,125.32,12.322,125.352,12.293,125.409,12.285,125.481,12.252,125.536,12.191,125.503,12.136,125.513,12.055,125.457,11.953,125.464,11.772,125.497,11.714,125.5,11.655,125.492,11.594,125.506,11.544,125.593,11.378,125.609,11.323,125.582,11.279,125.574,11.238,125.627,11.234,125.704,11.165,125.749,11.074,125.736,11.05,125.674,11.121,125.628,11.132,125.432,11.113,125.312,11.142,125.233,11.145,125.156,11.267,125.088,11.287,125.034,11.341,124.945,11.479,124.917,11.558,124.979,11.638,124.998,11.702,124.995,11.765,124.936,11.755,124.884,11.775,124.821,11.852,124.796,11.896,124.75,11.933,124.677,12.021,124.572,12.055,124.529,12.079,124.446,12.153,124.385,12.244,124.326,12.404,124.295,12.569,124.566,12.526,124.84,12.535,125.15,12.573,125.24,12.528]],[[120.704,13.479,120.755,13.471,120.915,13.501,120.981,13.486,121.025,13.429,121.079,13.411,121.122,13.381,121.203,13.432,121.284,13.374,121.357,13.265,121.442,13.188,121.523,13.131,121.539,13.089,121.49,13.02,121.475,12.932,121.48,12.837,121.541,12.638,121.519,12.584,121.458,12.508,121.412,12.423,121.418,12.389,121.4,12.361,121.394,12.301,121.357,12.313,121.322,12.304,121.289,12.277,121.237,12.219,121.155,12.236,121.117,12.253,121.108,12.304,121.083,12.339,121.049,12.36,120.963,12.447,120.922,12.512,120.921,12.581,120.899,12.646,120.855,12.704,120.796,12.748,120.776,12.791,120.769,12.841,120.764,12.97,120.68,13.131,120.651,13.169,120.573,13.209,120.508,13.26,120.481,13.311,120.455,13.394,120.438,13.405,120.388,13.402,120.338,13.412,120.353,13.473,120.401,13.517,120.468,13.522,120.653,13.498,120.704,13.479]],[[126.006,9.321,126.088,9.261,126.193,9.277,126.192,9.125,126.209,9.081,126.305,8.952,126.32,8.845,126.263,8.744,126.22,8.696,126.142,8.627,126.14,8.596,126.173,8.56,126.282,8.539,126.365,8.484,126.38,8.327,126.459,8.203,126.457,8.149,126.425,7.927,126.435,7.833,126.494,7.757,126.544,7.725,126.57,7.677,126.593,7.547,126.589,7.325,126.582,7.248,126.547,7.176,126.439,7.012,126.294,6.882,126.217,6.891,126.192,6.853,126.24,6.734,126.221,6.483,126.189,6.31,126.142,6.398,126.11,6.49,126.08,6.733,126.043,6.843,125.985,6.944,125.962,7.033,125.901,7.117,125.824,7.333,125.774,7.322,125.689,7.263,125.67,7.222,125.66,7.161,125.641,7.105,125.542,7.017,125.465,6.911,125.401,6.796,125.381,6.69,125.433,6.607,125.487,6.574,125.565,6.5,125.588,6.466,125.671,6.225,125.668,5.979,125.608,5.87,125.456,5.664,125.346,5.599,125.288,5.632,125.241,5.757,125.233,5.808,125.265,5.926,125.268,6.033,125.232,6.07,125.191,6.063,125.174,6.047,125.076,5.906,125.035,5.871,124.975,5.866,124.927,5.875,124.636,5.998,124.399,6.12,124.213,6.233,124.078,6.404,124.05,6.533,124.048,6.667,123.988,6.863,123.981,6.93,123.985,6.994,124.045,7.114,124.118,7.175,124.158,7.219,124.191,7.267,124.213,7.332,124.207,7.396,124.182,7.437,124.068,7.578,123.968,7.665,123.765,7.743,123.717,7.785,123.666,7.818,123.609,7.832,123.553,7.832,123.493,7.808,123.477,7.756,123.482,7.71,123.476,7.665,123.391,7.408,123.282,7.464,123.178,7.529,123.151,7.575,123.139,7.63,123.121,7.667,123.097,7.7,123.049,7.614,122.99,7.546,122.917,7.531,122.843,7.529,122.819,7.558,122.792,7.722,122.714,7.774,122.616,7.763,122.498,7.673,122.474,7.639,122.449,7.561,122.32,7.34,122.251,7.17,122.176,7.004,122.142,6.95,122.098,6.914,122.028,6.929,121.964,6.968,121.904,7.075,121.925,7.2,121.991,7.279,122.047,7.364,122.115,7.66,122.12,7.765,122.132,7.81,122.243,7.945,122.337,8.028,122.387,8.046,122.589,8.093,122.673,8.133,122.804,8.134,122.911,8.156,122.996,8.221,123.003,8.287,122.999,8.356,123.018,8.398,123.051,8.434,123.096,8.481,123.147,8.516,123.293,8.541,123.341,8.57,123.38,8.616,123.435,8.703,123.499,8.682,123.564,8.647,123.68,8.621,123.783,8.548,123.849,8.433,123.861,8.376,123.877,8.189,123.853,8.145,123.753,8.058,123.799,8.049,123.931,8.128,123.997,8.159,124.159,8.201,124.198,8.23,124.226,8.271,124.283,8.386,124.325,8.508,124.358,8.559,124.405,8.6,124.451,8.606,124.622,8.523,124.731,8.563,124.762,8.69,124.787,8.874,124.806,8.924,124.869,8.972,124.944,8.957,125.046,8.891,125.141,8.869,125.176,8.922,125.21,9.027,125.248,9.027,125.376,8.992,125.499,9.015,125.533,9.141,125.51,9.276,125.414,9.669,125.471,9.757,125.521,9.759,125.642,9.654,125.877,9.513,125.955,9.427,126.006,9.321]],[[123.37,9.45,123.332,9.423,123.316,9.489,123.327,9.578,123.404,9.889,123.386,9.967,123.514,10.14,123.593,10.303,123.711,10.474,123.726,10.562,123.832,10.731,123.93,10.964,123.925,11.041,123.95,11.079,123.964,11.137,123.967,11.187,124.039,11.274,124.058,11.217,124.037,11.107,124.04,11.054,124.053,11.029,124.053,10.926,124.028,10.768,124.051,10.586,124.005,10.4,123.952,10.317,123.874,10.258,123.789,10.221,123.7,10.128,123.643,10.02,123.634,9.922,123.494,9.589,123.37,9.45]],[[121.922,18.895,121.858,18.823,121.825,18.843,121.861,18.913,121.86,18.937,121.889,18.992,121.943,19.01,121.988,18.957,121.922,18.895]],[[121.521,19.362,121.531,19.271,121.472,19.273,121.383,19.328,121.375,19.356,121.376,19.38,121.392,19.399,121.521,19.362]],[[121.96,20.366,121.941,20.354,121.914,20.359,121.941,20.454,121.991,20.48,122.031,20.469,121.96,20.366]],[[121.878,20.782,121.83,20.7,121.791,20.701,121.796,20.747,121.848,20.841,121.867,20.839,121.878,20.782]],[[121.159,6.076,121.214,6.004,121.283,6.022,121.392,6.002,121.415,5.965,121.411,5.94,121.294,5.87,121.218,5.943,121.083,5.893,121.019,5.923,120.931,5.896,120.876,5.953,120.898,6.007,121.038,6.096,121.159,6.076]],[[117.08,7.883,117.028,7.808,116.97,7.895,116.976,8.017,116.994,8.051,117.077,8.069,117.08,7.883]],[[119.916,10.486,119.793,10.455,119.764,10.552,119.852,10.64,119.95,10.605,120.008,10.57,119.981,10.539,119.916,10.486]],[[120.1,12.168,120.155,12.152,120.194,12.167,120.228,12.22,120.261,12.142,120.341,12.077,120.315,12.012,120.243,12.005,120.174,12.02,120.1,11.994,120.011,12.008,119.957,12.069,119.896,12.179,119.866,12.199,119.87,12.244,119.892,12.273,119.88,12.28,119.886,12.3,119.897,12.313,119.916,12.319,119.964,12.27,120.078,12.198,120.1,12.168]],[[120.039,11.703,119.964,11.669,119.945,11.691,119.932,11.74,119.933,11.774,119.861,11.954,119.916,11.981,119.957,11.96,119.998,11.932,120.036,11.917,120.071,11.861,120.062,11.821,120.073,11.783,120.039,11.703]],[[122.65,10.473,122.622,10.459,122.597,10.461,122.538,10.425,122.517,10.493,122.538,10.608,122.626,10.695,122.648,10.723,122.673,10.739,122.701,10.741,122.729,10.706,122.737,10.655,122.681,10.498,122.65,10.473]],[[124.594,9.787,124.584,9.75,124.506,9.754,124.478,9.748,124.403,9.654,124.36,9.63,124.122,9.599,123.936,9.624,123.872,9.676,123.83,9.761,123.817,9.817,123.864,9.879,123.909,9.92,124.06,10,124.094,10.061,124.173,10.135,124.336,10.16,124.352,10.141,124.373,10.13,124.406,10.126,124.486,10.065,124.577,10.027,124.555,9.879,124.582,9.83,124.594,9.787]],[[120.25,5.257,120.223,5.196,120.192,5.168,120.15,5.184,120.118,5.215,120.101,5.169,120.013,5.151,119.958,5.08,119.878,5.06,119.821,5.07,119.827,5.133,119.983,5.228,120.08,5.264,120.165,5.332,120.208,5.34,120.229,5.284,120.25,5.257]],[[122.093,6.428,121.991,6.415,121.959,6.416,121.88,6.518,121.872,6.563,121.809,6.614,121.832,6.664,121.915,6.676,122.058,6.741,122.288,6.639,122.324,6.602,122.252,6.58,122.201,6.483,122.093,6.428]],[[126.059,9.766,126.047,9.761,125.991,9.839,125.999,9.927,126.074,10.059,126.129,9.944,126.129,9.891,126.121,9.865,126.173,9.8,126.137,9.768,126.059,9.766]],[[125.69,9.914,125.673,9.886,125.649,9.944,125.591,9.998,125.534,10.09,125.495,10.119,125.522,10.192,125.525,10.31,125.58,10.364,125.606,10.38,125.648,10.437,125.667,10.44,125.685,10.392,125.647,10.245,125.703,10.072,125.684,9.963,125.692,9.939,125.69,9.914]],[[120.271,13.751,120.273,13.683,120.104,13.782,120.099,13.817,120.103,13.843,120.121,13.858,120.211,13.821,120.271,13.751]],[[121.915,13.54,121.977,13.537,121.996,13.547,122.115,13.463,122.107,13.421,122.122,13.365,122.055,13.269,122.042,13.236,122.005,13.205,121.876,13.282,121.829,13.329,121.815,13.424,121.866,13.566,121.915,13.54]],[[122.094,12.355,122.014,12.106,121.96,12.191,121.982,12.245,121.936,12.29,121.923,12.331,121.941,12.385,121.989,12.435,122.002,12.599,122.104,12.651,122.145,12.653,122.13,12.613,122.132,12.538,122.094,12.355]],[[122.654,12.309,122.603,12.286,122.499,12.384,122.439,12.429,122.423,12.455,122.472,12.492,122.604,12.492,122.674,12.424,122.683,12.382,122.654,12.309]],[[123.282,12.853,123.367,12.701,123.274,12.805,123.166,12.876,123.054,12.993,122.973,13.035,122.949,13.059,122.958,13.107,123.017,13.116,123.044,13.113,123.206,12.905,123.282,12.853]],[[123.775,12.454,123.779,12.366,123.742,12.399,123.621,12.571,123.587,12.633,123.621,12.675,123.709,12.611,123.775,12.454]],[[123.717,12.287,123.908,12.169,124.04,11.967,124.056,11.812,124.046,11.752,123.983,11.819,123.848,11.914,123.754,11.934,123.725,11.952,123.736,12.003,123.675,12.05,123.668,12.069,123.612,12.09,123.531,12.197,123.474,12.217,123.419,12.194,123.293,12.036,123.158,11.926,123.156,11.968,123.211,12.107,123.245,12.328,123.267,12.395,123.24,12.495,123.236,12.583,123.337,12.542,123.463,12.501,123.559,12.445,123.575,12.407,123.717,12.287]],[[124.354,13.632,124.327,13.567,124.295,13.59,124.248,13.587,124.175,13.532,124.057,13.606,124.039,13.663,124.124,13.79,124.123,13.98,124.154,14.026,124.186,14.06,124.225,14.078,124.308,13.947,124.337,13.931,124.417,13.871,124.396,13.75,124.404,13.679,124.354,13.632]],[[122.175,14.049,122.172,14.008,121.956,14.156,121.946,14.181,121.946,14.205,121.959,14.229,122.175,14.049]],[[122.033,15.005,122.052,14.97,122.032,14.972,122.017,14.965,121.97,14.893,122.022,14.759,121.99,14.662,121.933,14.656,121.911,14.667,121.922,14.715,121.935,14.737,121.923,14.8,121.889,14.84,121.862,14.917,121.82,14.964,121.84,15.038,121.972,15.046,122.033,15.005]],[[124.316,10.606,124.288,10.601,124.335,10.707,124.371,10.691,124.382,10.68,124.381,10.633,124.316,10.606]],[[125.281,9.982,125.288,9.933,125.159,10.063,125.133,10.155,125.176,10.151,125.231,10.116,125.281,9.982]],[[122.937,7.409,122.948,7.386,122.944,7.361,122.84,7.315,122.805,7.316,122.797,7.393,122.822,7.428,122.871,7.397,122.915,7.433,122.937,7.409]],[[125.785,6.963,125.769,6.906,125.708,7.04,125.683,7.073,125.714,7.186,125.783,7.131,125.785,6.963]],[[124.608,11.492,124.483,11.486,124.429,11.532,124.36,11.666,124.437,11.695,124.511,11.687,124.565,11.64,124.622,11.55,124.608,11.492]],[[124.807,9.143,124.778,9.083,124.666,9.132,124.639,9.175,124.653,9.226,124.708,9.243,124.737,9.243,124.79,9.19,124.807,9.143]],[[121.252,19.082,121.247,19.015,121.196,19.051,121.185,19.101,121.19,19.139,121.213,19.184,121.245,19.143,121.252,19.082]],[[119.861,11.525,119.883,11.472,119.855,11.393,119.831,11.376,119.799,11.409,119.73,11.432,119.726,11.475,119.761,11.474,119.827,11.515,119.861,11.525]],[[123.757,11.283,123.816,11.151,123.737,11.151,123.708,11.248,123.741,11.279,123.757,11.283]],[[122.311,12.529,122.28,12.498,122.261,12.503,122.248,12.557,122.278,12.593,122.288,12.589,122.311,12.529]],[[125.971,9.594,125.952,9.568,125.922,9.621,125.949,9.739,125.968,9.759,125.993,9.685,125.971,9.594]],[[124.854,11.595,124.836,11.543,124.807,11.558,124.781,11.581,124.744,11.659,124.731,11.715,124.788,11.683,124.821,11.627,124.854,11.595]],[[117.355,8.215,117.287,8.191,117.272,8.254,117.281,8.315,117.33,8.308,117.354,8.289,117.355,8.215]],[[123.698,9.237,123.706,9.134,123.614,9.103,123.541,9.13,123.493,9.192,123.494,9.216,123.535,9.214,123.626,9.268,123.655,9.279,123.698,9.237]]]},{"n":"Peru","c":"PE","r":2,"ml":2,"lx":-72.9,"ly":-12.98,"b":[-81.337,-18.346,-68.685,-0.042],"p":[[[-69.966,-4.236,-69.972,-4.301,-70.004,-4.327,-70.053,-4.333,-70.129,-4.287,-70.184,-4.298,-70.239,-4.301,-70.317,-4.247,-70.344,-4.194,-70.405,-4.15,-70.531,-4.168,-70.635,-4.169,-70.722,-4.159,-70.8,-4.173,-70.866,-4.23,-70.916,-4.295,-70.974,-4.35,-71.144,-4.387,-71.235,-4.388,-71.317,-4.424,-71.438,-4.438,-71.521,-4.47,-71.668,-4.487,-71.845,-4.504,-71.943,-4.553,-71.982,-4.575,-72.083,-4.642,-72.257,-4.749,-72.353,-4.786,-72.469,-4.901,-72.608,-5.01,-72.699,-5.067,-72.832,-5.094,-72.887,-5.123,-72.907,-5.158,-72.896,-5.198,-72.918,-5.303,-72.959,-5.495,-72.97,-5.59,-72.98,-5.635,-73.068,-5.79,-73.163,-5.933,-73.209,-6.029,-73.236,-6.098,-73.206,-6.156,-73.168,-6.261,-73.135,-6.344,-73.126,-6.401,-73.137,-6.466,-73.177,-6.525,-73.24,-6.564,-73.325,-6.575,-73.5,-6.679,-73.695,-6.834,-73.758,-6.906,-73.776,-6.974,-73.805,-7.08,-73.793,-7.135,-73.758,-7.173,-73.723,-7.263,-73.72,-7.309,-73.749,-7.335,-73.805,-7.341,-73.854,-7.35,-73.892,-7.373,-73.929,-7.367,-73.964,-7.379,-73.964,-7.417,-73.953,-7.46,-73.958,-7.507,-73.982,-7.536,-74.002,-7.556,-73.982,-7.585,-73.947,-7.611,-73.895,-7.655,-73.822,-7.739,-73.767,-7.754,-73.72,-7.783,-73.715,-7.829,-73.732,-7.875,-73.773,-7.896,-73.776,-7.936,-73.72,-7.986,-73.683,-8.021,-73.645,-8.073,-73.61,-8.145,-73.61,-8.192,-73.572,-8.25,-73.549,-8.299,-73.549,-8.346,-73.488,-8.392,-73.436,-8.427,-73.398,-8.459,-73.36,-8.479,-73.352,-8.514,-73.357,-8.567,-73.302,-8.654,-73.203,-8.719,-73.123,-8.814,-73.071,-8.883,-72.974,-8.993,-72.97,-9.12,-73.09,-9.266,-73.209,-9.411,-73.014,-9.407,-72.814,-9.41,-72.605,-9.452,-72.465,-9.492,-72.379,-9.51,-72.318,-9.557,-72.289,-9.629,-72.266,-9.688,-72.26,-9.774,-72.173,-9.844,-72.179,-9.91,-72.182,-10.004,-72.143,-10.005,-71.887,-10.006,-71.608,-10.006,-71.339,-9.989,-71.238,-9.966,-71.115,-9.852,-71.042,-9.819,-70.971,-9.766,-70.885,-9.669,-70.816,-9.625,-70.758,-9.572,-70.672,-9.518,-70.637,-9.478,-70.608,-9.464,-70.541,-9.437,-70.57,-9.49,-70.592,-9.543,-70.599,-9.621,-70.567,-9.705,-70.594,-9.767,-70.637,-9.824,-70.638,-9.972,-70.639,-10.182,-70.639,-10.361,-70.64,-10.586,-70.642,-10.841,-70.642,-11.01,-70.597,-10.977,-70.533,-10.947,-70.451,-11.025,-70.392,-11.059,-70.342,-11.067,-70.29,-11.064,-70.22,-11.048,-70.066,-10.982,-69.96,-10.93,-69.84,-10.933,-69.674,-10.954,-69.579,-10.952,-69.454,-11.169,-69.362,-11.328,-69.258,-11.509,-69.174,-11.654,-69.046,-11.876,-68.936,-12.067,-68.819,-12.27,-68.685,-12.502,-68.728,-12.561,-68.763,-12.608,-68.759,-12.687,-68.812,-12.73,-68.868,-12.755,-68.934,-12.822,-68.979,-12.88,-68.981,-12.963,-68.972,-13.382,-68.983,-13.496,-69.018,-13.594,-69.053,-13.644,-69.074,-13.683,-69.023,-13.78,-68.974,-13.976,-68.937,-14.015,-68.892,-14.094,-68.871,-14.17,-68.88,-14.199,-68.972,-14.234,-69.004,-14.265,-69.013,-14.377,-69.053,-14.418,-69.12,-14.47,-69.163,-14.531,-69.199,-14.573,-69.235,-14.597,-69.252,-14.671,-69.276,-14.746,-69.359,-14.795,-69.374,-14.887,-69.375,-14.963,-69.331,-15.039,-69.187,-15.199,-69.172,-15.237,-69.254,-15.333,-69.302,-15.399,-69.419,-15.603,-69.421,-15.641,-69.392,-15.737,-69.218,-16.149,-69.188,-16.183,-69.134,-16.222,-69.046,-16.218,-68.913,-16.262,-68.849,-16.313,-68.843,-16.338,-68.858,-16.355,-68.928,-16.389,-69.006,-16.434,-69.033,-16.476,-69.038,-16.543,-69.021,-16.642,-69.055,-16.674,-69.133,-16.713,-69.2,-16.768,-69.267,-16.861,-69.382,-17.001,-69.421,-17.04,-69.438,-17.088,-69.503,-17.105,-69.625,-17.2,-69.646,-17.249,-69.626,-17.294,-69.564,-17.333,-69.522,-17.389,-69.511,-17.46,-69.511,-17.505,-69.511,-17.506,-69.586,-17.573,-69.685,-17.65,-69.806,-17.665,-69.852,-17.704,-69.842,-17.785,-69.802,-17.9,-69.803,-17.99,-69.84,-18.093,-69.926,-18.206,-70.059,-18.283,-70.184,-18.325,-70.282,-18.325,-70.377,-18.334,-70.418,-18.346,-70.492,-18.278,-70.817,-18.053,-70.942,-17.932,-71.057,-17.876,-71.337,-17.683,-71.365,-17.621,-71.399,-17.422,-71.436,-17.366,-71.532,-17.294,-71.774,-17.199,-71.868,-17.151,-71.967,-17.064,-72.111,-17.003,-72.269,-16.876,-72.362,-16.775,-72.468,-16.708,-72.794,-16.615,-72.958,-16.521,-73.264,-16.389,-73.4,-16.304,-73.728,-16.202,-73.825,-16.153,-74.147,-15.912,-74.373,-15.834,-74.555,-15.699,-75.104,-15.412,-75.191,-15.32,-75.275,-15.178,-75.397,-15.094,-75.534,-14.899,-75.738,-14.785,-75.934,-14.634,-76.006,-14.496,-76.136,-14.32,-76.175,-14.227,-76.289,-14.133,-76.297,-13.948,-76.376,-13.863,-76.319,-13.821,-76.259,-13.803,-76.184,-13.515,-76.224,-13.371,-76.427,-13.11,-76.502,-12.984,-76.555,-12.823,-76.637,-12.728,-76.758,-12.527,-76.832,-12.349,-76.994,-12.219,-77.038,-12.173,-77.063,-12.107,-77.153,-12.06,-77.158,-11.923,-77.22,-11.663,-77.31,-11.532,-77.633,-11.288,-77.639,-11.194,-77.664,-11.022,-77.736,-10.837,-78.095,-10.261,-78.186,-10.089,-78.276,-9.81,-78.356,-9.652,-78.446,-9.371,-78.58,-9.157,-78.665,-8.971,-78.755,-8.74,-78.762,-8.617,-78.925,-8.405,-79.012,-8.21,-79.164,-8.047,-79.313,-7.923,-79.377,-7.836,-79.589,-7.419,-79.618,-7.296,-79.762,-7.067,-79.905,-6.902,-79.995,-6.769,-80.11,-6.65,-80.812,-6.282,-81.058,-6.129,-81.142,-6.057,-81.181,-5.942,-81.164,-5.875,-81.092,-5.812,-80.992,-5.861,-80.931,-5.841,-80.883,-5.759,-80.882,-5.635,-80.943,-5.475,-81.168,-5.167,-81.151,-5.102,-81.108,-5.028,-81.195,-4.879,-81.289,-4.761,-81.337,-4.67,-81.283,-4.322,-81.232,-4.234,-80.892,-3.882,-80.799,-3.731,-80.653,-3.638,-80.504,-3.496,-80.325,-3.388,-80.298,-3.406,-80.274,-3.425,-80.272,-3.461,-80.265,-3.492,-80.245,-3.522,-80.244,-3.577,-80.221,-3.613,-80.219,-3.654,-80.217,-3.711,-80.229,-3.739,-80.218,-3.788,-80.179,-3.878,-80.194,-3.906,-80.231,-3.924,-80.267,-3.949,-80.303,-4.005,-80.358,-4.003,-80.437,-3.979,-80.49,-4.01,-80.51,-4.07,-80.493,-4.119,-80.488,-4.166,-80.454,-4.205,-80.353,-4.208,-80.444,-4.336,-80.488,-4.394,-80.479,-4.43,-80.424,-4.461,-80.383,-4.464,-80.293,-4.417,-80.232,-4.349,-80.197,-4.311,-80.14,-4.296,-80.064,-4.328,-79.963,-4.39,-79.845,-4.446,-79.797,-4.476,-79.711,-4.468,-79.639,-4.455,-79.578,-4.501,-79.516,-4.539,-79.502,-4.671,-79.456,-4.766,-79.399,-4.84,-79.331,-4.928,-79.268,-4.958,-79.187,-4.958,-79.076,-4.991,-79.033,-4.969,-78.995,-4.908,-78.975,-4.873,-78.919,-4.858,-78.914,-4.819,-78.926,-4.771,-78.908,-4.714,-78.862,-4.665,-78.743,-4.593,-78.686,-4.562,-78.674,-4.518,-78.653,-4.458,-78.661,-4.425,-78.685,-4.384,-78.679,-4.326,-78.648,-4.248,-78.603,-4.157,-78.565,-4.042,-78.55,-3.987,-78.509,-3.952,-78.493,-3.902,-78.471,-3.843,-78.42,-3.777,-78.421,-3.706,-78.4,-3.674,-78.398,-3.595,-78.347,-3.431,-78.345,-3.397,-78.323,-3.388,-78.284,-3.399,-78.251,-3.436,-78.24,-3.473,-78.226,-3.489,-78.195,-3.486,-78.158,-3.465,-78.161,-3.432,-78.187,-3.4,-78.195,-3.38,-78.183,-3.35,-78.128,-3.284,-78.068,-3.207,-77.938,-3.047,-77.861,-2.982,-77.659,-2.912,-77.506,-2.86,-77.36,-2.81,-77.161,-2.738,-76.881,-2.636,-76.679,-2.563,-76.499,-2.432,-76.36,-2.331,-76.241,-2.244,-76.09,-2.133,-75.885,-1.893,-75.745,-1.728,-75.642,-1.607,-75.571,-1.531,-75.514,-1.316,-75.449,-1.071,-75.42,-0.962,-75.408,-0.924,-75.38,-0.94,-75.348,-0.967,-75.309,-0.968,-75.272,-0.967,-75.25,-0.952,-75.284,-0.707,-75.279,-0.654,-75.259,-0.59,-75.263,-0.555,-75.325,-0.507,-75.425,-0.409,-75.466,-0.322,-75.491,-0.248,-75.561,-0.2,-75.632,-0.158,-75.626,-0.123,-75.584,-0.123,-75.476,-0.157,-75.398,-0.146,-75.34,-0.142,-75.284,-0.107,-75.225,-0.042,-75.184,-0.042,-75.138,-0.05,-75.055,-0.117,-75.005,-0.156,-74.945,-0.188,-74.889,-0.199,-74.837,-0.203,-74.802,-0.2,-74.78,-0.245,-74.755,-0.299,-74.692,-0.335,-74.616,-0.37,-74.555,-0.43,-74.514,-0.47,-74.465,-0.518,-74.418,-0.581,-74.375,-0.691,-74.353,-0.767,-74.329,-0.808,-74.334,-0.851,-74.284,-0.928,-74.246,-0.971,-74.181,-0.998,-74.054,-1.029,-73.987,-1.098,-73.927,-1.125,-73.863,-1.197,-73.807,-1.218,-73.736,-1.214,-73.664,-1.249,-73.61,-1.316,-73.575,-1.401,-73.521,-1.45,-73.494,-1.537,-73.525,-1.639,-73.496,-1.693,-73.44,-1.737,-73.35,-1.784,-73.266,-1.772,-73.224,-1.788,-73.197,-1.83,-73.181,-1.88,-73.145,-2.003,-73.127,-2.081,-73.16,-2.156,-73.173,-2.208,-73.154,-2.278,-73.068,-2.312,-72.99,-2.34,-72.941,-2.394,-72.887,-2.408,-72.811,-2.405,-72.714,-2.392,-72.66,-2.361,-72.625,-2.352,-72.587,-2.365,-72.501,-2.395,-72.396,-2.429,-72.301,-2.409,-72.218,-2.4,-72.137,-2.381,-72.054,-2.325,-71.984,-2.327,-71.932,-2.289,-71.867,-2.228,-71.803,-2.166,-71.753,-2.153,-71.671,-2.182,-71.559,-2.224,-71.496,-2.279,-71.447,-2.294,-71.397,-2.334,-71.3,-2.335,-71.196,-2.313,-71.113,-2.245,-71.027,-2.226,-70.969,-2.207,-70.915,-2.219,-70.705,-2.342,-70.648,-2.406,-70.576,-2.418,-70.517,-2.453,-70.418,-2.491,-70.364,-2.529,-70.295,-2.553,-70.244,-2.607,-70.165,-2.64,-70.096,-2.658,-70.065,-2.702,-70.064,-2.731,-70.074,-2.75,-70.147,-2.864,-70.29,-3.087,-70.419,-3.288,-70.622,-3.605,-70.735,-3.782,-70.706,-3.789,-70.53,-3.866,-70.486,-3.869,-70.421,-3.85,-70.379,-3.819,-70.34,-3.814,-70.298,-3.844,-70.24,-3.883,-70.198,-3.995,-70.168,-4.05,-70.095,-4.092,-70.017,-4.162,-69.966,-4.236]]]},{"n":"Paraguay","c":"PY","r":4,"ml":3,"lx":-60.15,"ly":-21.67,"b":[-62.651,-27.554,-54.242,-19.286],"p":[[[-58.16,-20.165,-58.138,-20.237,-58.125,-20.293,-58.092,-20.333,-58.058,-20.386,-58.025,-20.416,-58.002,-20.465,-58.009,-20.522,-57.996,-20.594,-57.979,-20.657,-57.962,-20.674,-57.915,-20.69,-57.891,-20.747,-57.908,-20.776,-57.902,-20.809,-57.885,-20.842,-57.9,-20.873,-57.892,-20.897,-57.86,-20.919,-57.83,-20.998,-57.827,-21.134,-57.86,-21.206,-57.886,-21.266,-57.893,-21.302,-57.873,-21.355,-57.906,-21.418,-57.946,-21.494,-57.936,-21.547,-57.929,-21.597,-57.926,-21.65,-57.916,-21.699,-57.929,-21.752,-57.943,-21.798,-57.949,-21.851,-57.933,-21.911,-57.962,-21.967,-57.979,-22.007,-57.986,-22.046,-57.956,-22.109,-57.88,-22.136,-57.82,-22.142,-57.764,-22.109,-57.721,-22.099,-57.642,-22.129,-57.569,-22.182,-57.476,-22.189,-57.394,-22.198,-57.331,-22.215,-57.238,-22.195,-57.142,-22.215,-57.03,-22.245,-56.937,-22.271,-56.845,-22.265,-56.775,-22.261,-56.702,-22.232,-56.633,-22.235,-56.58,-22.182,-56.55,-22.136,-56.524,-22.103,-56.448,-22.076,-56.395,-22.093,-56.352,-22.179,-56.276,-22.228,-56.246,-22.265,-56.19,-22.281,-56.067,-22.284,-55.991,-22.281,-55.905,-22.308,-55.849,-22.308,-55.8,-22.354,-55.753,-22.41,-55.747,-22.513,-55.704,-22.592,-55.647,-22.622,-55.618,-22.671,-55.628,-22.741,-55.654,-22.81,-55.651,-22.886,-55.621,-22.956,-55.621,-23.025,-55.601,-23.095,-55.561,-23.154,-55.548,-23.25,-55.555,-23.32,-55.528,-23.359,-55.518,-23.416,-55.535,-23.462,-55.542,-23.525,-55.538,-23.581,-55.518,-23.627,-55.459,-23.687,-55.442,-23.793,-55.442,-23.865,-55.416,-23.951,-55.366,-23.991,-55.287,-24.004,-55.194,-24.017,-55.082,-23.998,-54.983,-23.975,-54.926,-23.951,-54.817,-23.888,-54.721,-23.852,-54.672,-23.829,-54.625,-23.812,-54.53,-23.852,-54.44,-23.902,-54.371,-23.971,-54.242,-24.047,-54.267,-24.066,-54.318,-24.128,-54.317,-24.201,-54.281,-24.306,-54.313,-24.528,-54.413,-24.867,-54.454,-25.065,-54.436,-25.121,-54.473,-25.22,-54.611,-25.433,-54.616,-25.576,-54.632,-26.006,-54.678,-26.309,-54.755,-26.533,-54.825,-26.652,-54.889,-26.667,-54.934,-26.703,-54.962,-26.759,-55.014,-26.807,-55.089,-26.845,-55.13,-26.886,-55.136,-26.931,-55.208,-26.96,-55.346,-26.973,-55.427,-27.009,-55.451,-27.068,-55.497,-27.115,-55.565,-27.15,-55.597,-27.208,-55.594,-27.288,-55.633,-27.357,-55.715,-27.415,-55.79,-27.416,-55.859,-27.362,-55.951,-27.326,-56.067,-27.308,-56.164,-27.321,-56.242,-27.367,-56.311,-27.439,-56.371,-27.537,-56.437,-27.554,-56.511,-27.488,-56.603,-27.468,-56.716,-27.494,-56.805,-27.485,-56.872,-27.441,-56.974,-27.436,-57.112,-27.47,-57.391,-27.43,-57.812,-27.317,-58.168,-27.273,-58.605,-27.314,-58.642,-27.196,-58.619,-27.132,-58.548,-27.084,-58.503,-27.029,-58.485,-26.968,-58.436,-26.922,-58.356,-26.89,-58.323,-26.858,-58.335,-26.825,-58.318,-26.796,-58.272,-26.771,-58.246,-26.731,-58.239,-26.677,-58.222,-26.65,-58.191,-26.63,-58.188,-26.593,-58.205,-26.477,-58.203,-26.381,-58.181,-26.307,-58.155,-26.263,-58.136,-26.251,-58.118,-26.225,-58.111,-26.18,-58.082,-26.139,-57.943,-26.053,-57.891,-26.007,-57.886,-25.964,-57.865,-25.907,-57.782,-25.784,-57.757,-25.726,-57.755,-25.697,-57.725,-25.667,-57.626,-25.599,-57.572,-25.534,-57.563,-25.474,-57.587,-25.405,-57.644,-25.328,-57.822,-25.136,-57.96,-25.049,-58.136,-24.977,-58.253,-24.954,-58.309,-24.979,-58.365,-24.959,-58.423,-24.894,-58.52,-24.843,-58.724,-24.787,-59.187,-24.562,-59.373,-24.454,-59.435,-24.387,-59.609,-24.267,-59.892,-24.094,-60.11,-24.009,-60.262,-24.014,-60.505,-23.964,-60.84,-23.858,-61.033,-23.756,-61.085,-23.656,-61.208,-23.557,-61.404,-23.458,-61.506,-23.392,-61.513,-23.36,-61.571,-23.319,-61.679,-23.269,-61.799,-23.182,-61.928,-23.059,-62.067,-22.869,-62.214,-22.612,-62.373,-22.439,-62.542,-22.35,-62.626,-22.29,-62.626,-22.262,-62.651,-22.234,-62.629,-22.184,-62.567,-21.989,-62.478,-21.705,-62.385,-21.412,-62.277,-21.066,-62.277,-20.821,-62.276,-20.562,-62.122,-20.35,-62.012,-20.199,-61.917,-20.055,-61.821,-19.809,-61.757,-19.645,-61.512,-19.606,-61.096,-19.521,-60.889,-19.479,-60.452,-19.389,-60.007,-19.298,-59.541,-19.292,-59.091,-19.286,-58.741,-19.49,-58.474,-19.646,-58.18,-19.818,-58.16,-19.855,-58.14,-19.999,-58.16,-20.165]]]},{"n":"Papua New Guinea","c":"PG","r":2,"ml":2.5,"lx":143.91,"ly":-5.7,"b":[140.862,-11.631,155.958,-1.353],"p":[[[152.966,-4.756,152.892,-4.832,152.846,-4.762,152.787,-4.699,152.74,-4.636,152.681,-4.498,152.678,-4.429,152.693,-4.356,152.697,-4.282,152.668,-4.132,152.598,-3.995,152.356,-3.668,152.279,-3.582,152.192,-3.506,152.136,-3.487,152.023,-3.469,151.973,-3.453,151.88,-3.4,151.793,-3.338,151.579,-3.154,151.465,-3.101,151.405,-3.037,151.067,-2.829,150.968,-2.78,150.848,-2.78,150.746,-2.739,150.826,-2.713,150.843,-2.644,150.825,-2.573,150.995,-2.688,151.175,-2.789,151.226,-2.87,151.315,-2.875,151.475,-2.942,151.586,-3.003,151.69,-3.073,151.807,-3.173,152.033,-3.251,152.065,-3.28,152.179,-3.41,152.329,-3.521,152.38,-3.582,153.017,-4.106,153.124,-4.252,153.133,-4.352,153.112,-4.392,153.044,-4.476,153.046,-4.576,153.023,-4.666,152.966,-4.756]],[[151.916,-4.297,151.968,-4.317,152.117,-4.212,152.197,-4.285,152.299,-4.321,152.406,-4.341,152.364,-4.491,152.376,-4.56,152.404,-4.629,152.4,-4.731,152.351,-4.822,152.258,-4.955,152.216,-4.979,152.167,-4.993,152.013,-5.004,151.984,-5.074,151.994,-5.149,152.077,-5.247,152.143,-5.357,152.077,-5.458,151.968,-5.529,151.865,-5.565,151.695,-5.544,151.515,-5.552,151.481,-5.591,151.48,-5.655,151.455,-5.703,151.422,-5.747,151.331,-5.839,151.229,-5.92,151.09,-5.997,151.043,-6.015,150.92,-6.027,150.809,-6.071,150.76,-6.114,150.706,-6.149,150.588,-6.188,150.474,-6.263,150.428,-6.276,150.191,-6.289,149.851,-6.293,149.75,-6.301,149.653,-6.29,149.598,-6.261,149.483,-6.125,149.382,-6.078,149.273,-6.079,149.127,-6.128,149.099,-6.117,148.808,-5.916,148.719,-5.867,148.625,-5.831,148.51,-5.805,148.401,-5.765,148.337,-5.669,148.345,-5.545,148.432,-5.472,148.565,-5.508,148.616,-5.507,148.666,-5.487,148.724,-5.493,148.783,-5.512,148.999,-5.485,149.124,-5.523,149.245,-5.573,149.359,-5.584,149.475,-5.573,149.632,-5.516,149.681,-5.524,149.831,-5.524,149.963,-5.448,150.012,-5.14,150.045,-5.035,150.09,-5.012,150.122,-5.018,150.17,-5.071,150.109,-5.136,150.082,-5.186,150.072,-5.31,150.106,-5.429,150.183,-5.524,150.299,-5.536,150.404,-5.473,150.519,-5.46,150.626,-5.521,150.734,-5.51,150.784,-5.471,150.843,-5.454,150.9,-5.447,150.953,-5.424,151.022,-5.321,151.069,-5.204,151.138,-5.113,151.327,-4.96,151.381,-4.941,151.44,-4.931,151.573,-4.937,151.671,-4.883,151.679,-4.761,151.665,-4.637,151.552,-4.346,151.544,-4.299,151.561,-4.247,151.593,-4.201,151.704,-4.2,151.819,-4.217,151.865,-4.261,151.916,-4.297]],[[140.976,-9.119,140.976,-9.106,140.976,-8.902,140.976,-8.699,140.976,-8.496,140.976,-8.292,140.976,-8.089,140.976,-7.886,140.975,-7.683,140.975,-7.479,140.975,-7.276,140.975,-7.073,140.975,-6.905,140.92,-6.84,140.862,-6.74,140.875,-6.612,140.944,-6.452,140.975,-6.346,140.975,-6.259,140.975,-6.056,140.975,-5.853,140.975,-5.65,140.975,-5.446,140.975,-5.243,140.975,-5.04,140.974,-4.836,140.974,-4.633,140.974,-4.43,140.974,-4.226,140.974,-4.023,140.974,-3.82,140.974,-3.617,140.974,-3.413,140.974,-3.21,140.974,-3.007,140.974,-2.803,140.973,-2.681,140.973,-2.614,140.973,-2.61,141.003,-2.61,141.105,-2.611,141.186,-2.628,141.687,-2.845,141.837,-2.932,141.888,-2.953,141.938,-2.953,141.986,-2.964,142.212,-3.083,142.549,-3.205,142.905,-3.321,143.016,-3.345,143.13,-3.355,143.378,-3.395,143.509,-3.431,143.701,-3.573,143.797,-3.617,143.888,-3.697,144.016,-3.784,144.066,-3.805,144.122,-3.815,144.248,-3.818,144.374,-3.803,144.427,-3.81,144.478,-3.825,144.525,-3.855,144.548,-3.913,144.627,-3.993,144.738,-4.029,144.843,-4.101,144.938,-4.188,145.008,-4.275,145.088,-4.349,145.208,-4.38,145.335,-4.385,145.767,-4.823,145.788,-4.891,145.793,-5.178,145.745,-5.402,145.853,-5.471,145.999,-5.497,146.205,-5.545,146.403,-5.617,147.034,-5.919,147.121,-5.945,147.248,-5.955,147.377,-5.951,147.423,-5.966,147.519,-6.021,147.567,-6.057,147.653,-6.155,147.73,-6.261,147.763,-6.292,147.802,-6.315,147.825,-6.373,147.854,-6.551,147.846,-6.662,147.81,-6.704,147.71,-6.724,147.356,-6.742,147.119,-6.722,146.954,-6.834,146.949,-6.883,146.961,-6.929,147.105,-7.167,147.19,-7.378,147.26,-7.464,147.365,-7.534,147.459,-7.616,147.545,-7.711,147.724,-7.876,147.822,-7.937,147.936,-7.975,148.127,-8.104,148.152,-8.16,148.206,-8.339,148.23,-8.46,148.234,-8.51,148.247,-8.554,148.414,-8.664,148.451,-8.695,148.526,-8.939,148.583,-9.052,148.679,-9.092,148.792,-9.089,149.097,-9.017,149.142,-9.015,149.198,-9.031,149.248,-9.071,149.264,-9.181,149.216,-9.296,149.203,-9.407,149.263,-9.498,149.419,-9.569,149.476,-9.588,149.756,-9.611,149.866,-9.63,149.974,-9.661,150.011,-9.688,149.985,-9.737,149.928,-9.761,149.864,-9.771,149.761,-9.806,149.763,-9.869,149.821,-9.934,149.874,-10.013,149.919,-10.042,149.968,-10.061,150.089,-10.088,150.206,-10.126,150.284,-10.163,150.364,-10.19,150.539,-10.207,150.667,-10.257,150.85,-10.236,150.691,-10.318,150.637,-10.338,150.446,-10.307,150.41,-10.339,150.489,-10.426,150.605,-10.484,150.647,-10.518,150.618,-10.558,150.482,-10.637,150.426,-10.649,150.32,-10.655,150.142,-10.621,150.017,-10.577,149.982,-10.518,149.948,-10.483,149.835,-10.399,149.754,-10.353,149.651,-10.337,149.544,-10.338,149.353,-10.29,148.937,-10.255,148.838,-10.234,148.713,-10.167,148.654,-10.157,148.591,-10.178,148.431,-10.191,148.383,-10.185,148.269,-10.128,148.15,-10.107,148.101,-10.125,148.051,-10.128,147.89,-10.087,147.769,-10.07,147.669,-10.013,147.614,-9.96,147.553,-9.912,147.496,-9.79,147.408,-9.675,147.299,-9.58,147.064,-9.426,147.017,-9.388,146.925,-9.247,146.93,-9.154,146.964,-9.06,146.913,-9.092,146.856,-9.088,146.697,-9.025,146.631,-8.951,146.524,-8.75,146.456,-8.644,146.296,-8.456,146.251,-8.344,146.184,-8.246,146.143,-8.21,146.109,-8.168,146.079,-8.114,146.033,-8.076,145.811,-7.993,145.772,-7.966,145.729,-7.952,145.563,-7.944,145.468,-7.93,145.288,-7.862,145.194,-7.841,145.082,-7.828,144.974,-7.802,144.921,-7.777,144.885,-7.734,144.864,-7.632,144.773,-7.642,144.684,-7.625,144.598,-7.589,144.51,-7.567,144.45,-7.598,144.431,-7.679,144.403,-7.684,144.352,-7.667,144.326,-7.677,144.27,-7.714,144.225,-7.765,144.143,-7.757,143.974,-7.706,143.898,-7.674,143.834,-7.616,143.779,-7.55,143.723,-7.498,143.655,-7.46,143.742,-7.55,143.942,-7.944,143.892,-7.952,143.841,-7.942,143.888,-8.018,143.833,-8.029,143.779,-8.028,143.665,-7.996,143.552,-7.985,143.518,-8.001,143.542,-8.029,143.582,-8.113,143.614,-8.2,143.45,-8.24,143.282,-8.264,143.095,-8.311,142.905,-8.314,142.808,-8.287,142.709,-8.272,142.615,-8.287,142.524,-8.322,142.448,-8.316,142.399,-8.255,142.376,-8.208,142.347,-8.167,142.276,-8.174,142.207,-8.196,142.325,-8.198,142.361,-8.25,142.391,-8.313,142.475,-8.369,142.576,-8.336,142.798,-8.345,143.014,-8.444,143.065,-8.455,143.112,-8.475,143.223,-8.572,143.307,-8.661,143.377,-8.762,143.392,-8.802,143.388,-8.908,143.366,-8.961,143.227,-9.036,143.078,-9.092,142.859,-9.203,142.647,-9.328,142.536,-9.303,142.435,-9.237,142.396,-9.219,142.293,-9.183,142.23,-9.17,141.979,-9.198,141.727,-9.213,141.622,-9.211,141.519,-9.19,141.406,-9.151,141.294,-9.168,141.217,-9.214,141.133,-9.221,140.976,-9.119]],[[148.026,-5.826,147.985,-5.834,147.968,-5.789,147.875,-5.749,147.781,-5.627,147.783,-5.522,147.795,-5.492,147.846,-5.491,148.055,-5.612,148.076,-5.65,148.06,-5.765,148.026,-5.826]],[[146.019,-4.726,145.952,-4.756,145.904,-4.733,145.884,-4.667,145.9,-4.604,145.959,-4.554,145.996,-4.539,146.037,-4.573,146.053,-4.64,146.019,-4.726]],[[152.671,-3.133,152.646,-3.221,152.585,-3.17,152.543,-3.096,152.57,-3.062,152.639,-3.043,152.671,-3.133]],[[152.099,-2.947,152.088,-2.998,152.057,-2.995,151.971,-2.896,151.955,-2.871,151.975,-2.846,152.075,-2.918,152.099,-2.947]],[[149.765,-1.553,149.763,-1.589,149.691,-1.571,149.671,-1.576,149.546,-1.472,149.548,-1.408,149.581,-1.353,149.633,-1.362,149.725,-1.431,149.765,-1.553]],[[151.081,-10.02,151.123,-10.02,151.194,-9.946,151.256,-9.923,151.296,-9.957,151.231,-10.195,151.175,-10.159,150.959,-10.093,150.952,-9.998,150.896,-9.968,150.861,-9.876,150.79,-9.774,150.776,-9.709,150.817,-9.736,150.862,-9.802,151.051,-9.939,151.044,-9.983,151.081,-10.02]],[[150.345,-9.494,150.331,-9.519,150.273,-9.5,150.11,-9.362,150.135,-9.26,150.208,-9.206,150.32,-9.264,150.357,-9.349,150.368,-9.396,150.345,-9.494]],[[151.107,-8.733,151.124,-8.805,151.046,-8.728,151.081,-8.642,151.087,-8.595,151.083,-8.569,151.005,-8.524,151.046,-8.451,151.09,-8.426,151.118,-8.419,151.116,-8.522,151.139,-8.568,151.107,-8.733]],[[154.281,-11.361,154.266,-11.416,154.23,-11.397,154.121,-11.426,154.064,-11.419,154.031,-11.371,154.023,-11.348,154.118,-11.366,154.102,-11.311,154.238,-11.339,154.281,-11.361]],[[143.587,-8.482,143.543,-8.485,143.367,-8.417,143.322,-8.368,143.528,-8.379,143.581,-8.391,143.593,-8.46,143.587,-8.482]],[[147.176,-5.432,147.12,-5.437,147.029,-5.342,147.006,-5.307,147.015,-5.257,147.131,-5.191,147.206,-5.252,147.222,-5.382,147.176,-5.432]],[[147.068,-1.96,147.401,-2.025,147.423,-2.024,147.419,-2.001,147.424,-1.995,147.444,-2.012,147.438,-2.059,147.385,-2.071,147.337,-2.066,147.301,-2.09,147.206,-2.182,147.142,-2.167,147.064,-2.187,146.926,-2.189,146.748,-2.149,146.699,-2.183,146.635,-2.173,146.572,-2.21,146.546,-2.209,146.531,-2.154,146.532,-2.126,146.607,-2.103,146.596,-2.017,146.656,-1.974,146.76,-1.978,146.857,-1.949,147.068,-1.96]],[[150.437,-2.662,150.238,-2.675,150.166,-2.66,150.102,-2.603,150.043,-2.512,149.985,-2.492,149.962,-2.474,150.103,-2.405,150.227,-2.384,150.429,-2.47,150.45,-2.513,150.452,-2.541,150.446,-2.632,150.437,-2.662]],[[150.528,-9.347,150.669,-9.429,150.746,-9.404,150.789,-9.418,150.879,-9.513,150.884,-9.582,150.899,-9.641,150.894,-9.667,150.844,-9.703,150.848,-9.663,150.81,-9.655,150.678,-9.657,150.576,-9.631,150.436,-9.625,150.495,-9.562,150.508,-9.536,150.435,-9.435,150.431,-9.387,150.437,-9.36,150.499,-9.346,150.528,-9.347]],[[152.631,-8.959,152.689,-8.975,152.81,-8.967,152.85,-9.025,152.905,-9.044,152.953,-9.07,152.995,-9.108,152.995,-9.131,152.985,-9.151,152.959,-9.169,152.967,-9.209,152.923,-9.203,152.867,-9.224,152.759,-9.177,152.72,-9.167,152.708,-9.126,152.638,-9.058,152.515,-9.01,152.577,-8.97,152.631,-8.959]],[[153.536,-11.476,153.703,-11.529,153.76,-11.586,153.7,-11.613,153.554,-11.631,153.519,-11.595,153.379,-11.56,153.357,-11.495,153.287,-11.517,153.322,-11.471,153.234,-11.42,153.207,-11.352,153.204,-11.324,153.307,-11.356,153.536,-11.476]],[[143.59,-8.633,143.608,-8.677,143.463,-8.617,143.324,-8.517,143.254,-8.49,143.207,-8.423,143.293,-8.473,143.443,-8.519,143.59,-8.633]],[[147.877,-2.283,147.845,-2.336,147.769,-2.331,147.736,-2.316,147.79,-2.306,147.812,-2.262,147.836,-2.247,147.877,-2.283]],[[151.957,-2.83,151.933,-2.83,151.93,-2.751,151.946,-2.709,152.002,-2.738,152.011,-2.809,151.957,-2.83]],[[150.899,-10.565,150.885,-10.643,150.802,-10.62,150.786,-10.603,150.799,-10.554,150.872,-10.552,150.899,-10.565]],[[153.659,-4.099,153.65,-4.123,153.592,-4.096,153.64,-4.045,153.663,-4.041,153.659,-4.099]],[[155.958,-6.687,155.933,-6.78,155.915,-6.797,155.892,-6.762,155.805,-6.796,155.763,-6.834,155.719,-6.863,155.617,-6.856,155.521,-6.83,155.427,-6.783,155.344,-6.722,155.261,-6.626,155.209,-6.527,155.234,-6.412,155.202,-6.308,155.045,-6.234,155.01,-6.21,154.94,-6.106,154.87,-6.061,154.782,-5.971,154.759,-5.931,154.721,-5.817,154.709,-5.747,154.741,-5.545,154.729,-5.444,154.773,-5.454,154.818,-5.494,154.871,-5.521,154.997,-5.54,155.094,-5.62,155.187,-5.777,155.198,-5.828,155.228,-5.865,155.323,-5.932,155.373,-5.974,155.467,-6.145,155.519,-6.182,155.581,-6.196,155.638,-6.221,155.734,-6.296,155.823,-6.38,155.882,-6.47,155.928,-6.565,155.958,-6.687]],[[154.647,-5.433,154.627,-5.441,154.584,-5.314,154.576,-5.221,154.563,-5.152,154.54,-5.111,154.606,-5.035,154.633,-5.014,154.682,-5.054,154.689,-5.143,154.727,-5.218,154.698,-5.383,154.647,-5.433]]]},{"n":"Panama","c":"PA","r":4,"ml":4,"lx":-80.35,"ly":8.72,"b":[-83.027,7.22,-77.196,9.598],"p":[[[-77.374,8.658,-77.393,8.645,-77.448,8.566,-77.479,8.498,-77.407,8.427,-77.386,8.352,-77.346,8.27,-77.283,8.187,-77.212,8.034,-77.196,7.972,-77.216,7.933,-77.283,7.908,-77.346,7.837,-77.363,7.749,-77.351,7.706,-77.538,7.566,-77.587,7.543,-77.619,7.565,-77.659,7.635,-77.706,7.691,-77.732,7.711,-77.747,7.712,-77.762,7.699,-77.769,7.668,-77.744,7.537,-77.765,7.484,-77.828,7.443,-77.901,7.229,-77.93,7.256,-78.17,7.544,-78.378,7.9,-78.422,8.061,-78.368,8.071,-78.315,8.067,-78.287,8.092,-78.255,8.139,-78.281,8.248,-78.18,8.33,-78.142,8.386,-78.114,8.38,-78.048,8.285,-77.952,8.23,-77.834,8.151,-77.761,8.133,-77.853,8.216,-78.012,8.325,-78.057,8.397,-78.099,8.497,-78.162,8.454,-78.191,8.417,-78.223,8.397,-78.251,8.421,-78.256,8.454,-78.35,8.46,-78.374,8.489,-78.399,8.506,-78.388,8.443,-78.369,8.405,-78.379,8.359,-78.41,8.355,-78.436,8.403,-78.469,8.447,-78.514,8.628,-78.621,8.714,-78.67,8.742,-78.71,8.753,-78.77,8.811,-78.848,8.842,-78.955,8.933,-79.086,8.997,-79.247,9.02,-79.442,9.006,-79.507,8.97,-79.552,8.924,-79.572,8.903,-79.687,8.851,-79.731,8.775,-79.759,8.712,-79.816,8.639,-79.75,8.596,-80.126,8.35,-80.2,8.314,-80.369,8.289,-80.408,8.262,-80.459,8.214,-80.466,8.14,-80.458,8.077,-80.409,8.029,-80.366,7.998,-80.261,7.852,-80.075,7.667,-80.04,7.6,-80.011,7.5,-80.067,7.453,-80.111,7.433,-80.287,7.426,-80.348,7.386,-80.373,7.325,-80.439,7.275,-80.667,7.226,-80.846,7.22,-80.901,7.277,-80.915,7.438,-81.035,7.711,-81.064,7.9,-81.094,7.876,-81.158,7.854,-81.179,7.808,-81.195,7.668,-81.219,7.621,-81.268,7.625,-81.37,7.675,-81.504,7.721,-81.676,8.016,-81.694,8.071,-81.728,8.138,-81.86,8.165,-81.973,8.215,-82.097,8.223,-82.16,8.195,-82.224,8.23,-82.235,8.311,-82.365,8.275,-82.531,8.287,-82.68,8.322,-82.781,8.304,-82.866,8.246,-82.854,8.1,-82.879,8.071,-82.883,8.131,-82.913,8.2,-82.948,8.257,-83.023,8.316,-83.027,8.338,-82.998,8.368,-82.862,8.454,-82.845,8.489,-82.843,8.564,-82.856,8.635,-82.917,8.74,-82.882,8.805,-82.812,8.857,-82.74,8.899,-82.728,8.916,-82.741,8.952,-82.783,8.99,-82.881,9.056,-82.94,9.06,-82.943,9.249,-82.94,9.449,-82.925,9.469,-82.889,9.481,-82.86,9.511,-82.844,9.571,-82.801,9.592,-82.723,9.546,-82.644,9.506,-82.611,9.519,-82.587,9.539,-82.569,9.558,-82.564,9.577,-82.5,9.523,-82.371,9.429,-82.363,9.382,-82.375,9.337,-82.34,9.209,-82.272,9.191,-82.205,9.215,-82.188,9.192,-82.201,9.168,-82.235,9.142,-82.244,9.031,-82.133,8.98,-82.078,8.935,-81.894,8.956,-81.826,8.944,-81.78,8.957,-81.831,9.046,-81.9,9.111,-81.894,9.14,-81.842,9.119,-81.803,9.074,-81.712,9.019,-81.546,8.827,-81.355,8.781,-81.204,8.787,-81.063,8.813,-80.839,8.887,-80.676,9.022,-80.547,9.082,-80.127,9.21,-79.978,9.344,-79.915,9.361,-79.855,9.378,-79.723,9.479,-79.652,9.558,-79.577,9.598,-79.355,9.569,-79.212,9.532,-79.112,9.537,-79.017,9.51,-78.975,9.453,-78.932,9.428,-78.697,9.435,-78.504,9.406,-78.083,9.236,-77.831,9.068,-77.697,8.889,-77.374,8.658]],[[-78.898,8.274,-78.918,8.232,-78.965,8.326,-78.957,8.351,-78.961,8.436,-78.916,8.458,-78.883,8.46,-78.856,8.448,-78.839,8.348,-78.853,8.302,-78.898,8.274]],[[-81.603,7.333,-81.658,7.328,-81.77,7.37,-81.852,7.453,-81.859,7.48,-81.857,7.508,-81.812,7.592,-81.752,7.622,-81.729,7.621,-81.671,7.523,-81.71,7.486,-81.695,7.425,-81.613,7.38,-81.603,7.333]],[[-82.233,9.381,-82.244,9.334,-82.322,9.418,-82.276,9.432,-82.259,9.43,-82.233,9.381]],[[-79.065,8.254,-79.11,8.21,-79.128,8.252,-79.096,8.295,-79.085,8.296,-79.065,8.254]]]},{"n":"Palau","c":"PW","r":6,"ml":5,"lx":134.58,"ly":7.52,"b":[131.135,3.022,134.66,7.712],"p":[[[131.172,3.026,131.15,3.022,131.135,3.025,131.137,3.039,131.152,3.054,131.172,3.061,131.188,3.056,131.186,3.042,131.172,3.026]],[[134.595,7.382,134.535,7.361,134.506,7.437,134.516,7.526,134.556,7.594,134.6,7.616,134.609,7.624,134.651,7.712,134.66,7.663,134.633,7.501,134.598,7.438,134.595,7.382]]]},{"n":"Pakistan","c":"PK","r":2,"ml":2.7,"lx":68.55,"ly":29.33,"b":[60.843,23.753,77.049,37.037],"p":[[[76.767,35.662,76.813,35.572,76.882,35.436,76.928,35.347,76.979,35.246,77.004,35.196,77.049,35.11,77.031,35.062,77.001,34.992,76.892,34.939,76.783,34.9,76.758,34.878,76.749,34.848,76.696,34.787,76.594,34.736,76.51,34.741,76.457,34.756,76.172,34.668,76.041,34.67,75.938,34.613,75.862,34.56,75.709,34.503,75.606,34.503,75.453,34.537,75.264,34.601,75.188,34.639,75.118,34.637,74.952,34.646,74.789,34.678,74.594,34.716,74.498,34.732,74.3,34.765,74.172,34.721,74.056,34.681,73.961,34.653,73.883,34.529,73.85,34.485,73.812,34.422,73.795,34.378,73.81,34.325,73.925,34.288,73.972,34.237,73.979,34.191,73.938,34.145,73.904,34.108,73.904,34.076,73.922,34.043,73.95,34.019,74.113,34.004,74.209,34.003,74.246,33.99,74.251,33.946,74.216,33.887,74.078,33.839,74.001,33.788,73.976,33.721,73.978,33.668,74.004,33.632,74.07,33.592,74.131,33.545,74.15,33.507,74.143,33.455,74.118,33.384,74.05,33.301,73.994,33.242,73.99,33.221,74.004,33.189,74.049,33.143,74.126,33.075,74.222,33.02,74.284,33.005,74.304,32.992,74.323,32.928,74.33,32.861,74.305,32.81,74.355,32.769,74.483,32.771,74.588,32.753,74.632,32.771,74.663,32.758,74.643,32.608,74.658,32.519,74.686,32.494,74.789,32.458,74.987,32.462,75.104,32.42,75.234,32.372,75.303,32.319,75.333,32.279,75.325,32.215,75.254,32.14,75.139,32.105,75.071,32.089,74.739,31.949,74.636,31.89,74.556,31.819,74.526,31.765,74.51,31.713,74.582,31.524,74.594,31.465,74.535,31.261,74.518,31.186,74.54,31.133,74.61,31.113,74.626,31.069,74.633,31.035,74.51,30.96,74.38,30.893,74.339,30.894,74.216,30.769,74.009,30.52,73.899,30.435,73.892,30.394,73.883,30.352,73.925,30.282,73.933,30.222,73.887,30.162,73.809,30.093,73.658,30.033,73.467,29.972,73.382,29.934,73.317,29.773,73.258,29.611,73.231,29.551,73.128,29.364,72.949,29.089,72.903,29.029,72.626,28.896,72.342,28.752,72.292,28.697,72.234,28.566,72.179,28.422,72.129,28.346,71.948,28.177,71.889,28.047,71.87,27.963,71.717,27.915,71.543,27.87,71.29,27.855,71.185,27.832,70.875,27.714,70.798,27.71,70.737,27.729,70.692,27.769,70.649,27.835,70.629,27.937,70.569,27.984,70.489,28.023,70.404,28.025,70.318,27.982,70.244,27.934,70.194,27.895,70.145,27.849,70.05,27.695,69.896,27.474,69.725,27.313,69.661,27.265,69.622,27.228,69.568,27.175,69.537,27.123,69.495,26.954,69.47,26.804,69.481,26.771,69.507,26.743,69.601,26.699,69.736,26.627,69.911,26.586,70.059,26.579,70.115,26.548,70.148,26.506,70.157,26.471,70.149,26.348,70.133,26.215,70.078,26.072,70.079,25.99,70.1,25.91,70.265,25.707,70.325,25.686,70.449,25.681,70.506,25.685,70.57,25.706,70.615,25.692,70.648,25.667,70.657,25.626,70.652,25.423,70.703,25.331,70.8,25.206,70.878,25.063,70.951,24.892,71.021,24.758,71.048,24.688,71.002,24.654,70.976,24.619,70.97,24.572,70.979,24.522,70.973,24.487,71.006,24.444,71.045,24.43,71.044,24.4,70.983,24.361,70.928,24.362,70.886,24.344,70.805,24.262,70.767,24.245,70.716,24.238,70.659,24.246,70.579,24.279,70.556,24.331,70.565,24.386,70.547,24.418,70.489,24.412,70.289,24.356,70.098,24.288,70.065,24.241,70.021,24.192,69.934,24.171,69.805,24.165,69.716,24.173,69.634,24.225,69.559,24.273,69.443,24.275,69.235,24.268,69.12,24.269,69.052,24.286,68.985,24.273,68.901,24.292,68.863,24.267,68.828,24.264,68.8,24.309,68.781,24.314,68.759,24.307,68.74,24.292,68.728,24.266,68.724,23.965,68.587,23.967,68.489,23.967,68.381,23.951,68.283,23.928,68.234,23.901,68.165,23.857,68.149,23.797,68.116,23.753,68.068,23.818,68.037,23.848,68.001,23.826,67.951,23.829,67.86,23.903,67.819,23.828,67.668,23.811,67.65,23.867,67.646,23.92,67.563,23.882,67.504,23.94,67.477,24.018,67.454,24.04,67.428,24.065,67.365,24.092,67.309,24.175,67.304,24.263,67.289,24.368,67.171,24.756,67.101,24.792,66.703,24.861,66.682,24.929,66.71,25.111,66.699,25.226,66.57,25.379,66.534,25.484,66.429,25.575,66.324,25.602,66.219,25.59,66.162,25.554,66.131,25.493,66.356,25.507,66.407,25.485,66.468,25.445,66.403,25.447,66.328,25.466,66.235,25.464,65.884,25.42,65.68,25.355,65.406,25.374,65.061,25.311,64.777,25.307,64.659,25.184,64.594,25.206,64.544,25.237,64.152,25.333,64.125,25.374,64.059,25.403,63.987,25.351,63.936,25.343,63.721,25.386,63.557,25.353,63.496,25.298,63.491,25.211,63.286,25.228,63.17,25.255,63.015,25.225,62.665,25.265,62.572,25.255,62.445,25.197,62.391,25.153,62.315,25.135,62.249,25.197,62.199,25.225,62.152,25.207,62.089,25.155,61.908,25.131,61.744,25.138,61.567,25.186,61.588,25.202,61.615,25.286,61.64,25.585,61.671,25.692,61.662,25.751,61.669,25.769,61.738,25.821,61.754,25.843,61.781,25.996,61.81,26.165,61.842,26.226,61.87,26.242,62.089,26.318,62.126,26.369,62.239,26.357,62.25,26.369,62.26,26.427,62.312,26.491,62.385,26.543,62.439,26.561,62.636,26.594,62.752,26.639,62.787,26.644,63.093,26.632,63.158,26.65,63.168,26.666,63.186,26.838,63.242,26.865,63.25,26.879,63.231,26.998,63.242,27.078,63.305,27.125,63.302,27.151,63.256,27.208,63.196,27.244,63.167,27.252,62.915,27.218,62.812,27.229,62.763,27.25,62.753,27.266,62.763,27.3,62.764,27.357,62.801,27.445,62.812,27.497,62.782,27.801,62.74,28.002,62.763,28.202,62.758,28.244,62.749,28.253,62.718,28.253,62.565,28.235,62.434,28.364,62.353,28.415,62.131,28.479,62.033,28.491,61.89,28.547,61.758,28.668,61.623,28.792,61.569,28.871,61.509,29.006,61.338,29.265,61.339,29.332,61.318,29.373,61.152,29.543,61.034,29.663,60.843,29.859,61.224,29.749,61.521,29.666,62.001,29.53,62.373,29.425,62.477,29.408,63.568,29.498,63.971,29.43,64.099,29.392,64.118,29.414,64.172,29.46,64.266,29.507,64.394,29.544,64.521,29.565,64.704,29.567,64.827,29.564,64.919,29.553,65.096,29.559,65.18,29.578,65.471,29.652,65.666,29.701,65.962,29.779,66.177,29.836,66.231,29.866,66.287,29.92,66.313,29.969,66.247,30.044,66.238,30.11,66.282,30.193,66.305,30.321,66.301,30.503,66.287,30.608,66.347,30.803,66.397,30.912,66.497,30.965,66.567,30.997,66.596,31.02,66.624,31.046,66.731,31.195,66.829,31.264,66.924,31.306,67.028,31.3,67.116,31.243,67.287,31.218,67.453,31.235,67.596,31.278,67.662,31.313,67.738,31.344,67.733,31.379,67.647,31.41,67.598,31.453,67.578,31.506,67.627,31.539,67.74,31.548,68.017,31.678,68.13,31.763,68.161,31.803,68.214,31.807,68.32,31.768,68.443,31.754,68.521,31.794,68.598,31.803,68.673,31.76,68.714,31.708,68.782,31.646,68.869,31.634,68.973,31.667,69.083,31.738,69.187,31.838,69.279,31.937,69.257,32.249,69.241,32.434,69.29,32.531,69.359,32.59,69.405,32.683,69.405,32.764,69.453,32.833,69.502,33.02,69.568,33.064,69.704,33.095,69.92,33.113,70.09,33.198,70.261,33.289,70.284,33.369,70.22,33.455,70.134,33.621,70.057,33.72,69.868,33.898,69.89,34.007,69.995,34.052,70.254,33.976,70.326,33.961,70.416,33.95,70.654,33.952,70.848,33.982,71.052,34.05,71.091,34.12,71.089,34.204,71.092,34.273,71.096,34.369,71.023,34.431,70.979,34.486,70.966,34.53,71.016,34.555,71.066,34.6,71.113,34.682,71.226,34.78,71.294,34.868,71.358,34.91,71.455,34.967,71.517,35.051,71.546,35.101,71.602,35.151,71.621,35.183,71.605,35.212,71.577,35.248,71.546,35.289,71.546,35.329,71.572,35.37,71.601,35.408,71.587,35.461,71.572,35.547,71.519,35.598,71.484,35.715,71.428,35.834,71.398,35.88,71.343,35.939,71.22,36.001,71.185,36.042,71.233,36.122,71.313,36.171,71.463,36.293,71.546,36.378,71.621,36.436,71.716,36.427,71.773,36.432,71.822,36.486,71.921,36.534,72.096,36.634,72.157,36.701,72.25,36.735,72.327,36.742,72.431,36.766,72.531,36.802,72.623,36.83,72.766,36.835,72.994,36.852,73.117,36.869,73.411,36.882,73.732,36.888,73.769,36.888,73.908,36.853,74.002,36.823,74.039,36.826,74.195,36.897,74.431,36.984,74.541,37.022,74.601,37.037,74.692,37.036,74.766,37.013,74.841,36.979,74.889,36.952,74.949,36.968,75.054,36.987,75.145,36.973,75.347,36.913,75.377,36.884,75.424,36.738,75.46,36.725,75.574,36.759,75.667,36.742,75.772,36.695,75.84,36.65,75.885,36.601,75.933,36.522,75.952,36.458,75.974,36.382,75.969,36.169,75.934,36.134,75.905,36.088,75.912,36.049,75.945,36.018,76.01,35.996,76.071,35.983,76.103,35.949,76.148,35.829,76.178,35.811,76.252,35.811,76.386,35.837,76.502,35.878,76.551,35.887,76.563,35.773,76.632,35.729,76.728,35.679,76.767,35.662]]]},{"n":"Oman","c":"OM","r":4,"ml":4,"lx":57.34,"ly":22.12,"b":[51.978,16.648,59.838,26.356],"p":[[[58.722,20.219,58.659,20.204,58.641,20.211,58.641,20.337,58.788,20.497,58.884,20.681,58.951,20.516,58.835,20.424,58.772,20.267,58.722,20.219]],[[53.086,16.648,53.025,16.78,52.964,16.912,52.904,17.044,52.843,17.176,52.801,17.268,52.729,17.3,52.686,17.398,52.642,17.498,52.597,17.598,52.553,17.698,52.509,17.798,52.465,17.897,52.42,17.997,52.376,18.097,52.332,18.197,52.288,18.297,52.243,18.397,52.199,18.497,52.155,18.597,52.11,18.696,52.066,18.796,52.022,18.896,51.978,18.996,52.119,19.043,52.291,19.1,52.463,19.158,52.635,19.215,52.807,19.272,52.979,19.33,53.151,19.387,53.323,19.444,53.495,19.502,53.667,19.559,53.839,19.617,54.011,19.674,54.183,19.731,54.355,19.788,54.527,19.846,54.699,19.903,54.871,19.96,54.977,19.996,55.021,20.129,55.058,20.24,55.095,20.351,55.131,20.461,55.168,20.572,55.205,20.683,55.241,20.793,55.278,20.904,55.314,21.015,55.351,21.126,55.388,21.236,55.424,21.347,55.461,21.458,55.498,21.568,55.534,21.679,55.571,21.79,55.607,21.9,55.641,22.002,55.578,22.1,55.493,22.231,55.404,22.368,55.32,22.497,55.259,22.591,55.186,22.704,55.194,22.85,55.192,22.923,55.2,23.035,55.27,23.19,55.353,23.387,55.414,23.519,55.466,23.633,55.508,23.725,55.532,23.819,55.519,23.885,55.492,23.91,55.468,23.941,55.548,23.991,55.697,24.024,55.779,24.017,55.894,24.041,55.985,24.063,55.992,24.093,55.966,24.143,55.929,24.215,55.8,24.223,55.761,24.243,55.806,24.35,55.804,24.384,55.787,24.424,55.768,24.491,55.778,24.577,55.804,24.636,55.804,24.684,55.792,24.781,55.796,24.868,55.823,24.911,55.871,24.951,55.916,24.972,55.963,24.97,56.001,24.953,56.017,24.908,56.006,24.876,55.98,24.872,55.97,24.859,56.008,24.798,56.064,24.739,56.107,24.749,56.154,24.796,56.205,24.833,56.268,24.867,56.314,24.931,56.353,24.973,56.388,24.979,56.49,24.716,56.641,24.47,56.774,24.335,56.913,24.15,57.123,23.981,57.22,23.923,57.611,23.804,57.825,23.759,58.12,23.717,58.325,23.624,58.393,23.618,58.5,23.646,58.578,23.643,58.773,23.517,58.83,23.397,58.912,23.334,58.983,23.235,59.03,23.131,59.195,22.972,59.311,22.793,59.429,22.661,59.535,22.579,59.696,22.546,59.823,22.509,59.838,22.421,59.824,22.305,59.8,22.22,59.681,22.054,59.653,21.951,59.518,21.782,59.371,21.499,59.304,21.435,59.069,21.289,58.896,21.113,58.69,20.807,58.534,20.504,58.474,20.407,58.349,20.387,58.266,20.395,58.209,20.424,58.232,20.507,58.245,20.599,58.169,20.59,58.103,20.57,57.947,20.344,57.862,20.244,57.844,20.118,57.802,19.955,57.741,19.804,57.714,19.678,57.715,19.607,57.761,19.432,57.764,19.253,57.79,19.146,57.812,19.017,57.738,18.977,57.676,18.958,57.428,18.944,57.177,18.903,56.957,18.828,56.826,18.754,56.655,18.587,56.551,18.166,56.383,17.988,56.27,17.951,55.998,17.935,55.614,17.886,55.479,17.843,55.255,17.586,55.238,17.505,55.281,17.446,55.296,17.382,55.275,17.321,55.174,17.158,55.064,17.039,54.772,16.965,54.665,17.009,54.567,17.031,54.377,17.034,54.068,17.006,53.954,16.918,53.775,16.856,53.61,16.76,53.298,16.723,53.086,16.648]],[[56.298,25.651,56.279,25.628,56.25,25.625,56.184,25.645,56.145,25.691,56.152,25.746,56.154,25.848,56.173,25.945,56.167,26.047,56.117,26.068,56.08,26.063,56.164,26.207,56.197,26.229,56.228,26.22,56.306,26.235,56.346,26.314,56.379,26.356,56.413,26.351,56.43,26.327,56.418,26.208,56.416,26.109,56.374,25.805,56.329,25.752,56.307,25.709,56.298,25.651]],[[56.282,25.236,56.24,25.209,56.211,25.213,56.217,25.267,56.234,25.304,56.277,25.301,56.288,25.279,56.282,25.236]]]},{"n":"Norway","c":"NO","r":3,"ml":3,"lx":9.68,"ly":61.36,"b":[-9.099,58.021,33.629,80.478],"p":[[[20.622,69.037,20.492,69.033,20.117,69.021,20.282,68.934,20.337,68.9,20.348,68.849,20.319,68.754,20.24,68.673,20.147,68.607,19.969,68.542,20.24,68.478,20.056,68.39,19.97,68.356,19.87,68.362,19.691,68.392,19.259,68.465,19.053,68.493,18.868,68.501,18.77,68.5,18.379,68.562,18.303,68.555,18.163,68.528,18.147,68.468,18.156,68.317,18.177,68.201,18.125,68.133,18.073,68.088,17.917,67.965,17.565,68.048,17.325,68.104,17.171,68.03,16.784,67.895,16.586,67.628,16.574,67.62,16.457,67.552,16.307,67.521,16.194,67.505,16.127,67.426,16.282,67.312,16.361,67.252,16.434,67.155,16.421,67.093,16.404,67.055,16.238,66.976,15.884,66.769,15.557,66.552,15.423,66.49,15.484,66.306,15.375,66.252,15.153,66.191,15.04,66.168,14.918,66.154,14.543,66.129,14.61,65.932,14.635,65.845,14.635,65.793,14.596,65.743,14.55,65.646,14.48,65.301,14.426,65.264,14.352,65.171,14.115,64.946,13.925,64.797,13.65,64.582,13.874,64.514,14.078,64.464,14.12,64.388,14.148,64.26,14.141,64.174,14.063,64.096,14.003,64.041,13.961,64.014,13.671,64.041,13.3,64.075,13.204,64.075,12.988,64.05,12.793,64,12.69,63.957,12.663,63.94,12.533,63.844,12.302,63.671,12.175,63.596,12.212,63.492,12,63.292,12.139,63.089,12.145,63.083,12.218,63.001,12.141,62.948,12.109,62.919,12.12,62.826,12.14,62.721,12.122,62.66,12.115,62.592,12.304,62.286,12.301,62.214,12.292,62.167,12.234,61.977,12.155,61.721,12.292,61.653,12.487,61.573,12.596,61.541,12.758,61.446,12.881,61.352,12.864,61.29,12.828,61.222,12.776,61.174,12.728,61.108,12.706,61.06,12.683,61.047,12.468,61.042,12.354,61.023,12.294,61.003,12.315,60.892,12.445,60.69,12.554,60.546,12.589,60.451,12.553,60.354,12.516,60.305,12.515,60.239,12.486,60.107,12.402,60.04,12.292,59.967,12.169,59.913,12.072,59.898,11.988,59.891,11.932,59.864,11.881,59.782,11.834,59.697,11.681,59.592,11.685,59.556,11.743,59.431,11.798,59.29,11.752,59.158,11.712,59.019,11.643,58.926,11.544,58.893,11.471,58.91,11.388,59.037,11.386,59.066,11.366,59.105,11.132,59.143,11.091,59.142,10.999,59.164,10.945,59.171,10.834,59.184,10.743,59.296,10.645,59.389,10.631,59.428,10.634,59.602,10.604,59.68,10.595,59.765,10.534,59.696,10.57,59.587,10.494,59.542,10.398,59.519,10.407,59.456,10.446,59.444,10.455,59.377,10.431,59.28,10.243,59.062,10.205,59.039,10.179,59.009,10.083,59.029,9.96,58.968,9.843,58.958,9.8,59.027,9.635,59.118,9.557,59.113,9.627,59.068,9.696,59.01,9.657,58.971,9.618,58.946,9.551,58.933,9.31,58.857,9.396,58.806,9.323,58.748,9.239,58.739,9.194,58.712,9.178,58.675,8.928,58.57,8.521,58.301,8.312,58.224,8.166,58.145,8.037,58.147,7.876,58.08,7.466,58.021,7.194,58.048,7.005,58.024,6.903,58.071,6.89,58.102,6.895,58.121,6.912,58.143,6.877,58.151,6.803,58.155,6.771,58.132,6.767,58.082,6.731,58.068,6.591,58.097,6.555,58.123,6.606,58.176,6.692,58.224,6.677,58.234,6.66,58.263,6.618,58.266,6.492,58.259,6.389,58.268,6.055,58.375,5.977,58.432,5.707,58.524,5.586,58.62,5.517,58.727,5.522,58.823,5.556,58.975,5.612,59.013,5.854,58.959,6.099,58.87,6.137,58.875,6.214,58.945,6.363,59.001,6.321,59.016,6.099,58.952,6.017,58.988,5.889,59.06,5.889,59.098,5.949,59.135,5.969,59.186,5.937,59.234,5.952,59.299,6.051,59.368,6.199,59.438,6.306,59.506,6.415,59.547,6.404,59.561,6.279,59.535,6.159,59.49,6.017,59.414,5.845,59.353,5.718,59.33,5.657,59.31,5.564,59.291,5.468,59.204,5.362,59.166,5.173,59.163,5.132,59.226,5.185,59.454,5.242,59.564,5.305,59.643,5.404,59.656,5.472,59.713,5.53,59.713,5.579,59.687,5.772,59.661,5.867,59.734,5.991,59.745,6.217,59.818,6.212,59.832,6.059,59.816,5.967,59.813,5.834,59.795,5.763,59.808,5.73,59.863,5.784,59.913,5.996,60.031,6.07,60.083,6.112,60.132,6.105,60.165,6.141,60.233,6.349,60.353,6.518,60.408,6.574,60.361,6.526,60.214,6.527,60.153,6.661,60.367,6.72,60.418,6.787,60.454,6.95,60.478,6.996,60.512,6.806,60.501,6.347,60.419,6.153,60.346,6.102,60.29,5.967,60.206,5.904,60.151,5.877,60.07,5.801,60.026,5.699,60.01,5.557,59.908,5.495,59.826,5.353,59.76,5.264,59.71,5.234,59.692,5.186,59.642,5.146,59.639,5.111,59.668,5.105,59.732,5.119,59.834,5.187,59.907,5.22,59.979,5.174,60.046,5.206,60.088,5.265,60.086,5.376,60.067,5.495,60.07,5.689,60.123,5.658,60.154,5.574,60.158,5.417,60.154,5.286,60.206,5.184,60.308,5.137,60.446,5.168,60.485,5.546,60.625,5.648,60.688,5.589,60.694,5.447,60.617,5.244,60.57,5.116,60.636,5.049,60.708,5.011,60.859,5.025,60.936,5.009,61.038,5.095,61.071,5.192,61.054,5.288,61.047,5.505,61.056,5.984,61.117,6.293,61.081,6.418,61.084,6.61,61.137,6.778,61.142,6.903,61.102,6.972,61.056,6.981,60.994,7.039,60.953,7.078,60.966,7.047,61.015,7.04,61.091,7.545,61.177,7.604,61.211,7.404,61.222,7.347,61.301,7.453,61.419,7.443,61.435,7.331,61.372,7.276,61.284,7.298,61.214,7.276,61.181,7.174,61.166,6.943,61.161,6.794,61.19,6.657,61.207,6.61,61.229,6.626,61.279,6.6,61.29,6.543,61.245,6.493,61.155,6.383,61.134,6.083,61.167,5.647,61.148,5.451,61.102,5.325,61.108,5.107,61.188,5.022,61.251,4.99,61.378,5.003,61.434,5.172,61.457,5.258,61.455,5.339,61.485,5.268,61.505,5.168,61.543,5.099,61.62,4.997,61.645,4.928,61.711,4.91,61.81,4.93,61.878,4.985,61.9,5.117,61.885,5.465,61.897,5.793,61.827,6.016,61.788,6.467,61.807,6.731,61.87,6.682,61.887,6.396,61.851,6.131,61.852,5.664,61.923,5.473,61.946,5.267,61.936,5.16,61.957,5.096,62.027,5.143,62.16,5.241,62.189,5.294,62.154,5.358,62.152,5.422,62.207,5.484,62.239,5.533,62.311,5.718,62.379,5.796,62.385,5.908,62.416,5.98,62.407,6.026,62.376,6.083,62.35,6.209,62.353,6.58,62.407,6.62,62.423,6.692,62.468,6.457,62.448,6.262,62.416,6.136,62.407,6.118,62.447,6.165,62.482,6.238,62.52,6.273,62.584,6.353,62.611,6.439,62.61,6.618,62.621,6.745,62.638,6.961,62.627,7.284,62.602,7.492,62.543,7.57,62.548,7.653,62.564,7.691,62.586,7.527,62.61,7.518,62.646,7.538,62.672,7.805,62.721,8.096,62.732,8.046,62.771,7.408,62.712,7.242,62.752,7.111,62.752,7.025,62.729,6.78,62.701,6.735,62.721,6.782,62.79,6.928,62.903,6.94,62.93,7.008,62.958,7.389,63.023,7.572,63.1,7.654,63.109,7.736,63.104,7.86,63.113,8.101,63.091,8.211,62.996,8.311,62.966,8.623,62.846,8.609,62.881,8.339,63.042,8.235,63.082,8.158,63.162,8.184,63.237,8.271,63.287,8.58,63.313,8.636,63.342,8.641,63.392,8.594,63.426,8.48,63.424,8.387,63.445,8.361,63.499,8.398,63.535,8.576,63.601,8.674,63.623,8.842,63.646,9.136,63.594,9.158,63.566,9.076,63.5,9.084,63.463,9.156,63.459,9.324,63.57,9.521,63.586,9.602,63.61,9.697,63.625,9.832,63.524,9.892,63.492,9.936,63.479,9.979,63.395,10.021,63.391,10.081,63.433,10.189,63.455,10.34,63.469,10.591,63.447,10.704,63.464,10.76,63.461,10.707,63.536,10.674,63.558,10.725,63.625,10.779,63.651,10.953,63.698,11.118,63.719,11.226,63.764,11.371,63.805,11.348,63.838,11.308,63.876,11.214,63.878,11.176,63.899,11.295,63.948,11.458,64.003,11.429,64.025,11.307,64.049,11.214,64.031,11.075,63.988,10.914,63.921,10.967,63.902,11.047,63.845,10.935,63.77,10.339,63.571,10.055,63.513,9.924,63.522,9.893,63.576,9.832,63.617,9.767,63.7,9.657,63.697,9.595,63.679,9.567,63.706,9.615,63.795,9.708,63.865,9.864,63.918,9.939,63.982,10.01,64.083,10.236,64.18,10.566,64.418,10.834,64.494,10.932,64.578,11.09,64.615,11.225,64.679,11.331,64.686,11.524,64.744,11.633,64.814,11.562,64.818,11.392,64.773,11.297,64.755,11.304,64.829,11.35,64.906,11.489,64.976,12.16,65.179,12.227,65.145,12.307,65.086,12.508,65.099,12.738,65.214,12.916,65.339,12.82,65.317,12.715,65.266,12.512,65.195,12.418,65.184,12.364,65.193,12.334,65.241,12.263,65.256,12.2,65.245,12.134,65.279,12.122,65.362,12.206,65.486,12.273,65.568,12.345,65.63,12.628,65.806,12.689,65.902,12.817,65.953,12.983,65.942,13.033,65.956,12.976,66.019,12.795,66.069,12.784,66.1,13.387,66.183,13.674,66.18,13.76,66.221,13.916,66.247,14.034,66.298,13.973,66.32,13.681,66.274,13.499,66.252,13.416,66.253,13.352,66.237,13.119,66.231,13.068,66.431,13.105,66.539,13.192,66.537,13.211,66.641,13.312,66.702,13.45,66.716,13.52,66.742,13.621,66.795,13.788,66.782,13.959,66.794,13.917,66.819,13.704,66.852,13.652,66.907,13.727,66.938,13.808,66.961,13.88,66.965,14.022,67.073,14.109,67.119,14.206,67.111,14.34,67.159,14.473,67.143,14.601,67.174,14.776,67.194,15.416,67.202,15.435,67.247,15.3,67.257,14.824,67.268,14.582,67.267,14.479,67.256,14.442,67.271,14.448,67.298,14.537,67.34,14.579,67.386,14.755,67.499,14.962,67.574,15.121,67.555,15.289,67.483,15.409,67.474,15.465,67.451,15.553,67.352,15.594,67.349,15.576,67.444,15.692,67.521,15.661,67.543,15.487,67.515,15.354,67.544,15.249,67.602,15.219,67.655,15.284,67.708,15.346,67.734,15.304,67.765,15.041,67.683,14.855,67.663,14.781,67.675,14.821,67.75,14.799,67.809,15.048,67.956,15.134,67.973,15.274,67.961,15.401,67.92,15.507,67.926,15.621,67.948,15.606,67.988,15.357,68.004,15.293,68.036,15.316,68.069,15.487,68.103,15.657,68.164,15.851,68.182,16.008,68.229,16.038,68.218,16.065,68.2,16.121,68.027,16.261,67.887,16.312,67.881,16.259,68.001,16.309,68.036,16.372,68.062,16.392,68.092,16.319,68.102,16.26,68.145,16.175,68.281,16.204,68.317,16.388,68.39,16.619,68.406,16.865,68.355,16.951,68.355,17.094,68.368,17.336,68.41,17.479,68.426,17.553,68.426,17.571,68.447,17.502,68.461,17.48,68.474,17.426,68.482,17.202,68.459,16.585,68.466,16.525,68.491,16.514,68.533,16.58,68.593,16.652,68.626,16.885,68.685,17.131,68.693,17.391,68.799,17.49,68.879,17.546,69.001,17.705,69.1,18.101,69.156,18.117,69.181,18.075,69.233,18.079,69.325,18.188,69.433,18.26,69.471,18.293,69.475,18.379,69.44,18.483,69.365,18.646,69.322,18.859,69.314,18.916,69.336,18.75,69.378,18.624,69.434,18.614,69.491,18.674,69.52,18.767,69.517,18.883,69.523,18.991,69.561,19.007,69.588,19.011,69.624,19.038,69.66,19.197,69.748,19.687,69.805,19.722,69.782,19.696,69.613,19.64,69.504,19.642,69.424,19.737,69.504,19.865,69.722,19.961,69.825,20.069,69.883,20.146,69.897,20.223,69.927,20.324,69.945,20.355,69.922,20.387,69.868,20.333,69.677,20.338,69.617,20.277,69.536,20.044,69.356,20.054,69.333,20.107,69.341,20.198,69.371,20.487,69.542,20.739,69.521,20.743,69.535,20.662,69.585,20.563,69.633,20.533,69.692,20.546,69.851,20.622,69.914,20.84,69.907,20.971,69.916,21.032,69.887,21.163,69.89,21.254,70.003,21.433,70.013,21.59,69.938,21.78,69.887,21.932,69.815,21.975,69.835,21.893,70.004,21.803,70.066,21.608,70.098,21.4,70.174,21.346,70.208,21.356,70.233,21.539,70.258,21.78,70.23,21.996,70.293,22.054,70.276,22.219,70.309,22.322,70.265,22.385,70.278,22.421,70.338,22.685,70.375,22.852,70.34,22.941,70.305,22.983,70.237,23.046,70.102,23.177,70.029,23.258,69.993,23.354,69.983,23.4,70.02,23.31,70.064,23.286,70.105,23.329,70.207,23.379,70.247,23.661,70.4,23.897,70.479,24.038,70.485,24.286,70.662,24.356,70.695,24.42,70.702,24.404,70.745,24.268,70.773,24.263,70.826,24.442,70.892,24.658,71.001,24.765,71.008,24.832,70.978,25.042,70.929,25.171,70.872,25.265,70.844,25.325,70.849,25.376,70.892,25.436,70.912,25.57,70.901,25.65,70.873,25.712,70.87,25.768,70.853,25.781,70.817,25.666,70.777,25.468,70.672,25.274,70.552,25.209,70.489,25.146,70.324,24.994,70.218,24.983,70.144,25.044,70.109,25.212,70.136,25.419,70.235,25.471,70.341,25.988,70.625,26.231,70.783,26.507,70.913,26.661,70.94,26.734,70.854,26.675,70.741,26.558,70.669,26.645,70.636,26.628,70.551,26.601,70.503,26.584,70.454,26.585,70.41,26.666,70.422,26.989,70.511,27.071,70.608,27.147,70.681,27.184,70.744,27.309,70.804,27.546,70.804,27.556,70.827,27.269,70.91,27.235,70.947,27.332,70.997,27.597,71.091,27.733,71.081,27.815,71.059,28.142,71.043,28.392,70.975,28.383,70.869,28.327,70.825,28.272,70.798,27.951,70.718,27.898,70.678,27.999,70.664,28.216,70.704,28.272,70.668,28.203,70.577,28.191,70.44,28.166,70.36,28.166,70.288,28.193,70.249,28.28,70.403,28.31,70.443,28.437,70.501,28.485,70.619,28.609,70.76,28.75,70.842,28.832,70.864,29.102,70.861,29.219,70.83,29.321,70.761,29.398,70.734,29.639,70.705,29.722,70.669,29.738,70.647,29.796,70.643,29.959,70.694,30.065,70.703,30.238,70.622,30.203,70.562,30.213,70.543,30.422,70.547,30.596,70.524,30.926,70.401,30.961,70.344,30.944,70.274,30.469,70.198,30.263,70.125,29.926,70.096,28.781,70.145,28.804,70.093,29.601,69.977,29.647,69.944,29.621,69.874,29.621,69.818,29.636,69.78,29.695,69.745,29.792,69.728,29.99,69.737,30.088,69.718,30.155,69.746,30.18,69.841,30.238,69.862,30.349,69.835,30.397,69.733,30.428,69.722,30.484,69.795,30.595,69.79,30.714,69.796,30.87,69.783,30.924,69.652,30.922,69.606,30.897,69.561,30.861,69.538,30.789,69.529,30.615,69.533,30.38,69.585,30.228,69.634,30.18,69.636,30.16,69.63,30.196,69.581,30.187,69.543,30.164,69.502,30.132,69.464,30.087,69.433,29.994,69.392,29.833,69.36,29.388,69.298,29.353,69.271,29.21,69.097,29.171,69.072,29.119,69.05,28.966,69.022,28.892,69.061,28.833,69.119,28.846,69.177,29.025,69.288,29.192,69.367,29.239,69.394,29.333,69.473,29.142,69.671,28.8,69.731,28.412,69.823,28.269,69.871,28.047,69.972,27.89,70.062,27.748,70.065,27.592,70.042,27.348,69.96,27.206,69.919,27.128,69.906,27.109,69.905,26.934,69.928,26.74,69.933,26.584,69.926,26.525,69.915,26.308,69.782,26.156,69.715,26.072,69.692,26.012,69.653,25.962,69.589,25.85,69.367,25.767,69.283,25.749,69.231,25.768,69.076,25.748,68.99,25.647,68.919,25.575,68.887,25.481,68.881,25.357,68.862,25.249,68.821,25.173,68.765,25.087,68.64,24.941,68.593,24.802,68.606,24.703,68.653,24.491,68.689,24.332,68.712,24.154,68.761,23.997,68.798,23.854,68.806,23.773,68.758,23.707,68.714,23.463,68.678,23.324,68.649,23.144,68.643,23.072,68.674,22.811,68.695,22.501,68.72,22.411,68.72,22.383,68.777,22.3,68.856,22.08,68.993,21.989,69.041,21.82,69.154,21.622,69.271,21.594,69.274,21.461,69.277,21.267,69.274,21.144,69.247,21.066,69.214,21.053,69.187,21.128,69.081,21.104,69.054,21.066,69.042,20.889,69.071,20.676,69.069,20.622,69.037]],[[4.959,61.085,4.87,61.072,4.799,61.083,4.824,61.178,4.862,61.194,4.915,61.199,4.973,61.148,4.959,61.085]],[[5.086,60.308,5.089,60.189,4.997,60.198,4.956,60.243,4.944,60.272,4.951,60.341,4.93,60.412,4.957,60.447,4.991,60.452,5.05,60.389,5.086,60.308]],[[29.956,69.797,29.766,69.768,29.744,69.792,29.786,69.829,29.836,69.906,29.914,69.902,29.993,69.873,30.055,69.838,29.956,69.797]],[[11.968,65.627,11.902,65.596,11.778,65.605,11.765,65.631,11.8,65.684,11.875,65.706,11.972,65.702,12.003,65.679,11.968,65.627]],[[8.471,63.667,8.356,63.665,8.287,63.687,8.451,63.732,8.709,63.774,8.733,63.801,8.765,63.805,8.809,63.771,8.815,63.726,8.787,63.703,8.471,63.667]],[[8.103,63.338,8.005,63.337,7.888,63.352,7.815,63.385,7.804,63.414,7.938,63.45,8.074,63.471,8.136,63.431,8.141,63.366,8.103,63.338]],[[23.441,70.816,23.421,70.784,23.387,70.754,23.305,70.722,23.068,70.594,22.929,70.574,22.885,70.554,22.829,70.542,22.656,70.559,22.605,70.533,22.558,70.516,22.432,70.509,22.359,70.515,22.169,70.562,22.056,70.613,21.995,70.657,22.17,70.656,22.233,70.667,22.35,70.658,22.421,70.703,22.571,70.697,22.858,70.728,22.964,70.711,23.205,70.815,23.28,70.813,23.396,70.843,23.441,70.816]],[[25.586,71.142,25.854,71.104,25.945,71.105,26.078,71.033,26.147,71.04,26.134,70.996,26,70.975,25.791,70.963,25.76,70.954,25.582,70.961,25.482,71.02,25.315,71.034,25.315,71.053,25.423,71.097,25.586,71.142]],[[23.615,70.549,23.634,70.503,23.641,70.464,23.548,70.408,23.333,70.335,23.345,70.315,23.271,70.296,23.159,70.283,23.1,70.296,23.108,70.359,23.091,70.378,23.006,70.353,22.918,70.385,22.918,70.417,22.941,70.445,23.022,70.487,23.158,70.516,23.248,70.505,23.547,70.617,23.579,70.594,23.615,70.549]],[[24.018,70.567,23.827,70.527,23.717,70.562,23.67,70.597,23.663,70.675,23.689,70.723,23.778,70.747,23.837,70.729,23.852,70.714,23.956,70.7,24.078,70.651,24.018,70.567]],[[13.873,68.265,13.932,68.248,14.088,68.253,14.119,68.247,14.097,68.219,14.029,68.188,13.888,68.169,13.824,68.121,13.778,68.105,13.656,68.105,13.584,68.094,13.495,68.052,13.424,68.083,13.404,68.061,13.392,68.021,13.352,68.01,13.229,67.995,13.2,68.087,13.256,68.121,13.3,68.16,13.368,68.167,13.429,68.163,13.538,68.249,13.688,68.273,13.784,68.276,13.873,68.265]],[[12.972,67.874,12.824,67.821,12.878,67.918,12.958,68.015,13.068,68.071,13.123,68.049,13.098,68.003,13.098,67.956,13.075,67.935,12.972,67.874]],[[15.207,68.943,15.337,68.842,15.397,68.784,15.348,68.672,15.222,68.616,15.027,68.606,14.89,68.611,14.804,68.638,14.793,68.668,14.743,68.677,14.612,68.638,14.521,68.633,14.405,68.663,14.373,68.711,14.497,68.772,14.554,68.819,14.69,68.815,14.725,68.8,14.802,68.791,14.849,68.848,14.838,68.887,14.872,68.914,15.038,68.894,15.038,69.001,15.102,69.008,15.128,69.004,15.176,68.982,15.207,68.943]],[[19.767,70.217,19.818,70.205,19.869,70.212,19.91,70.202,19.994,70.149,20.084,70.129,20.088,70.102,20.006,70.076,19.897,70.068,19.781,70.077,19.747,70.11,19.711,70.165,19.613,70.219,19.599,70.266,19.684,70.274,19.767,70.217]],[[20.779,70.09,20.725,70.067,20.643,70.057,20.598,70.071,20.535,70.081,20.464,70.077,20.405,70.119,20.412,70.155,20.493,70.203,20.655,70.231,20.786,70.22,20.819,70.205,20.779,70.09]],[[19.255,70.066,19.344,70.012,19.422,70.017,19.446,70.038,19.5,70.048,19.608,70.019,19.592,69.97,19.442,69.908,19.335,69.82,19.197,69.8,19.131,69.81,19.008,69.76,18.909,69.707,18.807,69.64,18.801,69.605,18.785,69.579,18.41,69.553,18.274,69.535,18.13,69.558,18.062,69.602,18.083,69.626,18.227,69.636,18.232,69.677,18.268,69.702,18.315,69.715,18.349,69.768,18.406,69.782,18.512,69.769,18.584,69.807,18.624,69.813,18.674,69.782,18.698,69.825,18.674,69.864,18.687,69.891,18.824,69.96,18.883,70.011,18.969,70.043,19.051,70.038,19.075,70.086,19.051,70.135,19.06,70.167,19.133,70.244,19.213,70.247,19.249,70.179,19.255,70.066]],[[12.51,65.902,12.429,65.899,12.43,65.94,12.476,65.977,12.549,66.002,12.642,66.009,12.747,66.011,12.779,65.992,12.719,65.964,12.51,65.902]],[[12.42,66.043,12.327,66.037,12.343,66.081,12.418,66.123,12.446,66.151,12.461,66.185,12.527,66.211,12.621,66.178,12.623,66.122,12.576,66.072,12.42,66.043]],[[11.231,64.866,11.179,64.838,11.063,64.86,10.833,64.843,10.74,64.87,10.813,64.923,11.021,64.979,11.133,64.976,11.246,64.908,11.231,64.866]],[[17.503,69.596,17.623,69.539,17.677,69.557,17.784,69.563,17.863,69.543,17.927,69.507,18.004,69.505,18.052,69.458,18.077,69.396,18.021,69.35,17.942,69.329,17.921,69.274,17.951,69.198,17.774,69.172,17.568,69.16,17.488,69.197,17.324,69.13,17.161,69.026,17.083,69.014,17.077,69.047,16.96,69.069,16.81,69.071,16.815,69.095,16.843,69.112,16.972,69.138,16.998,69.191,16.974,69.285,16.997,69.33,17.002,69.362,17.083,69.399,17.361,69.381,17.395,69.417,17.373,69.439,17.23,69.478,17.252,69.504,17.356,69.527,17.454,69.53,17.483,69.57,17.488,69.587,17.503,69.596]],[[15.76,68.561,15.772,68.554,15.909,68.65,16.06,68.681,16.069,68.714,16.127,68.746,16.121,68.799,16.151,68.842,16.228,68.854,16.276,68.868,16.329,68.876,16.425,68.842,16.48,68.803,16.547,68.717,16.519,68.633,16.338,68.568,16.194,68.538,16.048,68.464,15.975,68.402,15.913,68.389,15.873,68.394,15.837,68.409,15.764,68.409,15.683,68.356,15.438,68.313,15.341,68.325,15.337,68.378,15.28,68.374,15.188,68.31,15.098,68.289,15.038,68.283,14.927,68.307,14.629,68.198,14.35,68.178,14.258,68.191,14.257,68.257,14.438,68.342,14.586,68.4,15.095,68.441,15.413,68.616,15.489,68.805,15.564,68.874,15.529,68.912,15.444,68.919,15.438,68.979,15.483,69.043,15.65,69.133,15.742,69.171,15.893,69.278,15.965,69.302,16.048,69.302,16.129,69.274,16.115,69.216,15.993,69.113,15.812,69.024,15.834,68.961,15.906,68.908,15.924,68.819,15.928,68.733,15.791,68.617,15.76,68.561]],[[-8.954,70.839,-9.046,70.833,-9.099,70.855,-8.965,70.916,-8.521,71.031,-8.344,71.14,-8.001,71.178,-7.979,71.117,-8.002,71.041,-8.302,70.981,-8.635,70.94,-8.954,70.839]],[[19.219,74.391,19.099,74.352,18.918,74.411,18.797,74.486,18.861,74.514,19.183,74.518,19.262,74.479,19.275,74.457,19.219,74.391]],[[26.876,78.649,26.729,78.646,26.46,78.72,26.408,78.784,26.456,78.81,26.586,78.811,26.789,78.724,27.008,78.698,26.876,78.649]],[[32.526,80.119,31.578,80.081,31.482,80.108,33.019,80.218,33.099,80.229,33.384,80.242,33.629,80.217,33.557,80.198,32.526,80.119]],[[21.608,78.596,21.746,78.572,22.043,78.577,22.207,78.408,22.3,78.228,22.449,78.215,22.735,78.24,22.989,78.252,23.119,78.239,23.352,78.186,23.452,78.149,23.365,78.121,23.152,78.088,23.117,77.992,23.331,77.958,23.684,77.875,23.883,77.865,24.238,77.899,24.571,77.834,24.902,77.757,24.13,77.658,24.062,77.631,23.955,77.558,23.841,77.498,23.736,77.462,23.505,77.401,23.381,77.38,23.101,77.385,22.997,77.361,22.9,77.311,22.802,77.276,22.554,77.267,22.427,77.316,22.469,77.331,22.487,77.36,22.442,77.429,22.679,77.5,22.733,77.539,22.685,77.554,22.62,77.55,22.448,77.571,22.397,77.57,22.255,77.529,22.057,77.501,21.856,77.494,21.05,77.441,20.928,77.46,20.873,77.565,21.201,77.619,21.251,77.711,21.334,77.772,21.431,77.812,21.608,77.916,21.653,77.924,21.21,78.006,21.035,78.059,20.845,78.166,20.786,78.252,20.528,78.326,20.56,78.419,20.373,78.412,20.228,78.478,20.363,78.515,21.047,78.557,21.455,78.598,21.608,78.596]],[[20.898,80.25,20.998,80.239,21.549,80.243,21.655,80.218,21.697,80.159,21.781,80.139,21.898,80.132,22.19,80.06,22.29,80.049,22.376,80.09,22.443,80.19,22.446,80.308,22.418,80.366,22.451,80.402,22.549,80.416,22.672,80.413,22.793,80.433,22.897,80.469,23.008,80.474,23.251,80.447,23.315,80.425,23.25,80.381,23.225,80.318,23.115,80.187,23.353,80.179,23.688,80.207,23.773,80.244,23.953,80.305,24.143,80.295,24.234,80.303,24.28,80.329,24.298,80.36,24.403,80.355,24.547,80.295,24.614,80.286,24.736,80.301,24.786,80.301,24.907,80.277,25.471,80.233,25.667,80.21,25.751,80.188,25.836,80.175,26.437,80.175,26.861,80.16,27.017,80.125,27.148,80.059,27.199,79.907,27.08,79.865,26.221,79.677,26.006,79.617,25.902,79.561,25.726,79.44,25.641,79.403,25.239,79.345,25.145,79.339,24.843,79.367,24.751,79.365,24.383,79.302,24.257,79.263,24.133,79.215,23.948,79.194,23.759,79.206,22.904,79.231,22.789,79.264,22.696,79.329,22.866,79.412,21.911,79.381,20.861,79.398,20.806,79.41,20.761,79.442,20.4,79.463,20.128,79.49,19.9,79.534,19.675,79.591,19.747,79.618,19.821,79.634,20.015,79.64,20.187,79.632,20.493,79.633,20.565,79.691,20.687,79.707,20.784,79.749,20.461,79.775,20.123,79.779,19.899,79.744,19.638,79.729,19.4,79.727,18.942,79.736,18.725,79.761,18.428,79.825,18.325,79.86,18.285,79.887,18.255,79.929,18.595,79.967,18.726,79.996,18.856,80.037,18.344,80.06,18.129,80.093,17.917,80.143,18.089,80.171,18.779,80.194,18.962,80.175,19.143,80.139,19.343,80.116,19.537,80.163,19.355,80.185,19.191,80.263,19.157,80.302,19.178,80.332,19.264,80.336,19.327,80.323,19.568,80.25,19.751,80.227,19.802,80.295,19.81,80.327,19.777,80.353,19.691,80.402,19.614,80.463,19.733,80.478,19.851,80.471,20.104,80.43,20.359,80.401,20.476,80.372,20.693,80.299,20.898,80.25]],[[16.787,79.907,16.838,79.905,16.889,79.915,16.926,79.943,16.966,79.959,17.219,79.941,17.578,79.885,17.685,79.857,17.835,79.8,17.956,79.704,17.86,79.635,17.733,79.57,17.688,79.533,17.734,79.481,17.715,79.431,17.669,79.386,17.861,79.437,18.272,79.601,18.333,79.611,18.397,79.605,18.581,79.572,18.748,79.488,18.785,79.461,18.815,79.427,18.832,79.385,18.823,79.337,18.807,79.303,18.72,79.281,18.678,79.262,18.772,79.26,18.88,79.234,18.979,79.179,19.089,79.157,19.49,79.176,19.751,79.147,19.894,79.056,20.114,79.077,20.114,79.125,20.163,79.146,20.458,79.129,20.611,79.107,20.767,79.059,20.501,78.981,20.72,78.907,21.09,78.853,21.312,78.796,21.353,78.772,21.389,78.74,21.244,78.699,21.096,78.676,20.725,78.672,20.387,78.643,19.769,78.623,19.677,78.61,19.655,78.598,19.619,78.562,19.381,78.48,19.15,78.379,19.056,78.319,18.984,78.234,18.958,78.182,19.009,78.132,18.995,78.081,18.822,78.042,18.712,78.04,18.575,78.048,18.439,78.025,18.431,77.991,18.439,77.942,18.404,77.794,18.362,77.682,18.299,77.579,18.228,77.523,18.137,77.507,17.847,77.497,17.623,77.399,17.442,77.225,17.349,77.157,17.153,77.049,17.188,77.011,17.249,76.969,17.142,76.895,16.977,76.812,16.98,76.779,17.036,76.72,17.063,76.659,16.935,76.606,16.7,76.579,16.462,76.609,16.346,76.645,16.238,76.702,16.124,76.739,16.004,76.761,15.547,76.886,15.124,77.085,14.738,77.162,14.487,77.199,14.366,77.234,14.248,77.282,14.145,77.336,14.05,77.403,14.004,77.445,13.996,77.508,14.026,77.545,14.071,77.564,14.378,77.58,14.488,77.571,14.596,77.538,14.695,77.525,14.921,77.689,16.206,77.782,16.619,77.799,17.033,77.798,16.969,77.842,16.914,77.898,16.853,77.912,16.54,77.88,16.06,77.847,15.826,77.847,15.585,77.869,15.345,77.857,15.097,77.809,14.847,77.779,14.604,77.766,14.09,77.771,13.963,77.796,13.791,77.854,13.75,77.883,13.714,77.919,13.681,78.028,13.718,78.058,13.77,78.075,13.824,78.085,13.937,78.086,14.048,78.067,14.307,78.005,14.248,78.071,14.995,78.151,15.341,78.221,15.519,78.233,15.698,78.228,15.659,78.265,15.657,78.299,15.784,78.327,15.875,78.339,16.15,78.353,16.777,78.35,17.003,78.369,17.172,78.417,16.992,78.4,16.811,78.397,16.727,78.407,16.535,78.449,16.449,78.504,16.697,78.613,16.783,78.664,16.53,78.656,16.446,78.639,16.158,78.538,15.944,78.493,15.681,78.471,15.417,78.473,15.36,78.488,15.279,78.554,15.254,78.589,15.265,78.608,15.348,78.663,15.392,78.721,15.384,78.771,15.323,78.781,15.225,78.732,15.137,78.664,15.016,78.63,14.892,78.639,14.839,78.666,14.792,78.706,14.744,78.721,14.689,78.721,14.578,78.705,14.467,78.675,14.505,78.631,14.515,78.581,14.468,78.541,14.432,78.492,14.546,78.462,14.638,78.415,14.5,78.392,14.363,78.36,14.238,78.31,14.11,78.271,13.908,78.267,13.655,78.245,13.15,78.238,12.913,78.301,12.87,78.331,12.822,78.351,12.665,78.385,12.435,78.483,12.258,78.595,12.138,78.606,11.962,78.642,11.866,78.674,11.774,78.716,11.746,78.766,11.755,78.812,11.861,78.832,11.611,78.883,11.365,78.95,11.456,78.973,11.548,78.983,12.275,78.904,12.323,78.914,12.403,78.953,12.375,78.966,12.253,78.975,12.087,78.975,12.046,78.983,11.982,79.025,11.926,79.077,11.902,79.112,11.893,79.152,12.016,79.213,12.084,79.268,11.978,79.293,11.679,79.291,11.58,79.283,11.616,79.205,11.521,79.151,11.339,79.109,11.208,79.13,11.107,79.233,10.975,79.305,10.926,79.35,10.888,79.415,10.834,79.463,10.738,79.52,10.725,79.556,10.737,79.582,10.811,79.641,10.755,79.69,10.686,79.734,10.682,79.758,10.746,79.789,10.804,79.799,10.866,79.797,11.05,79.76,11.15,79.717,11.185,79.72,11.251,79.785,11.344,79.799,11.702,79.821,12.102,79.738,12.205,79.719,12.288,79.713,12.245,79.75,12.219,79.798,12.28,79.816,12.602,79.773,12.754,79.776,13.108,79.832,13.693,79.861,13.914,79.817,13.926,79.793,13.921,79.762,13.907,79.752,13.778,79.715,13.039,79.685,12.555,79.569,13.215,79.588,13.334,79.575,13.384,79.481,13.432,79.471,13.601,79.457,13.716,79.429,13.834,79.376,13.957,79.34,14.03,79.344,14.056,79.383,14.026,79.429,14.011,79.482,14.02,79.539,14.04,79.586,14.178,79.619,14.38,79.726,14.594,79.799,14.832,79.766,15.052,79.675,15.251,79.545,15.444,79.407,15.66,79.235,15.764,79.174,15.858,79.16,16.295,78.981,16.344,78.976,16.254,79.112,16.028,79.342,15.875,79.519,15.841,79.587,15.816,79.682,15.826,79.709,15.845,79.734,15.956,79.835,16.1,79.884,16.057,79.954,16.094,80.007,16.246,80.049,16.387,80.053,16.524,80.021,16.787,79.907]],[[11.25,78.611,11.262,78.542,11.424,78.549,11.616,78.475,11.826,78.436,11.885,78.409,11.929,78.375,12.056,78.306,12.116,78.233,11.965,78.225,11.757,78.329,11.587,78.388,11.372,78.439,11.199,78.441,11.121,78.463,10.841,78.645,10.789,78.687,10.628,78.754,10.558,78.838,10.558,78.903,10.773,78.888,10.961,78.846,11.124,78.753,11.153,78.724,11.078,78.686,11.155,78.641,11.25,78.611]],[[18.742,80.301,18.525,80.246,18.162,80.288,18.206,80.332,18.292,80.358,18.519,80.348,18.742,80.301]],[[29.047,78.912,29.345,78.906,29.645,78.922,29.697,78.905,29.311,78.852,28.881,78.88,28.495,78.887,28.038,78.829,27.889,78.852,28.121,78.908,28.374,78.927,28.415,78.961,28.511,78.967,28.845,78.971,29.047,78.912]]]},{"n":"North Korea","c":"KP","r":3,"ml":3,"lx":126.44,"ly":39.89,"b":[124.349,37.719,130.687,42.998],"p":[[[128.375,38.623,128.339,38.608,128.279,38.524,128.223,38.417,128.169,38.359,128.106,38.327,128.039,38.309,127.905,38.3,127.785,38.308,127.746,38.319,127.579,38.313,127.533,38.305,127.294,38.313,127.17,38.305,127.09,38.284,127.01,38.241,126.94,38.176,126.879,38.106,126.754,37.979,126.667,37.917,126.667,37.828,126.665,37.801,126.634,37.782,126.623,37.79,126.573,37.797,126.37,37.878,126.203,37.829,126.161,37.764,126.117,37.743,126.05,37.87,125.942,37.874,125.769,37.985,125.695,37.963,125.676,37.918,125.582,37.815,125.449,37.73,125.407,37.719,125.358,37.725,125.365,37.748,125.311,37.844,125.102,37.882,125.027,37.923,124.989,37.931,125.193,38.038,125.247,38.057,125.207,38.082,125.163,38.094,124.995,38.078,124.907,38.113,124.779,38.102,124.691,38.129,124.875,38.233,124.883,38.295,124.881,38.342,124.974,38.48,125.067,38.557,125.31,38.665,125.415,38.68,125.492,38.676,125.554,38.686,125.489,38.728,125.424,38.747,125.299,38.743,125.169,38.806,125.157,38.872,125.41,39.288,125.413,39.326,125.374,39.428,125.361,39.527,125.18,39.583,125.1,39.59,124.868,39.702,124.775,39.758,124.739,39.742,124.732,39.652,124.699,39.632,124.638,39.615,124.608,39.717,124.557,39.791,124.404,39.866,124.349,39.907,124.375,39.996,124.362,40.004,124.387,40.104,124.481,40.182,124.712,40.319,124.772,40.384,124.889,40.46,124.942,40.458,124.997,40.465,125.013,40.498,125.026,40.524,125.073,40.547,125.186,40.589,125.314,40.645,125.417,40.66,125.543,40.743,125.594,40.779,125.645,40.779,125.659,40.796,125.688,40.839,125.728,40.867,125.784,40.872,125.875,40.892,125.989,40.905,126.067,40.974,126.093,41.024,126.145,41.078,126.254,41.138,126.329,41.226,126.412,41.321,126.451,41.352,126.49,41.358,126.514,41.394,126.54,41.496,126.578,41.594,126.601,41.641,126.697,41.692,126.722,41.717,126.743,41.725,126.788,41.718,126.847,41.748,126.904,41.781,126.955,41.769,127.007,41.742,127.061,41.687,127.085,41.644,127.128,41.607,127.137,41.555,127.18,41.531,127.271,41.52,127.42,41.484,127.517,41.482,127.572,41.455,127.688,41.44,127.919,41.461,128.013,41.449,128.053,41.416,128.111,41.389,128.149,41.388,128.2,41.433,128.255,41.507,128.291,41.563,128.289,41.607,128.258,41.655,128.182,41.7,128.132,41.769,128.084,41.841,128.056,41.864,128.033,41.898,128.029,41.952,128.045,41.988,128.16,42.012,128.308,42.026,128.427,42.011,128.627,42.021,128.749,42.041,128.84,42.038,128.923,42.038,128.961,42.069,129.077,42.142,129.134,42.169,129.196,42.218,129.205,42.271,129.218,42.313,129.253,42.358,129.314,42.414,129.366,42.439,129.424,42.436,129.485,42.41,129.524,42.385,129.568,42.392,129.604,42.436,129.628,42.444,129.698,42.448,129.72,42.475,129.746,42.604,129.773,42.705,129.779,42.777,129.842,42.894,129.861,42.965,129.898,42.998,129.941,42.996,129.977,42.975,130.022,42.963,130.083,42.974,130.125,42.956,130.151,42.918,130.24,42.892,130.249,42.873,130.247,42.745,130.296,42.685,130.361,42.631,130.45,42.582,130.498,42.571,130.527,42.535,130.554,42.475,130.618,42.416,130.652,42.373,130.658,42.328,130.687,42.303,130.637,42.275,130.569,42.292,130.458,42.302,130.315,42.214,130.236,42.183,130.18,42.097,130.068,42.046,130.007,41.991,129.928,41.897,129.876,41.806,129.756,41.712,129.686,41.595,129.682,41.494,129.759,41.392,129.766,41.304,129.712,41.124,129.742,40.932,129.709,40.857,129.341,40.726,129.245,40.661,129.11,40.491,128.945,40.428,128.843,40.358,128.701,40.318,128.611,40.198,128.511,40.13,128.393,40.089,128.304,40.036,128.106,40.033,127.967,39.996,127.867,39.896,127.568,39.782,127.527,39.696,127.547,39.563,127.549,39.461,127.523,39.377,127.457,39.401,127.422,39.374,127.383,39.296,127.395,39.208,127.497,39.179,127.581,39.143,127.699,39.125,127.786,39.084,127.972,38.898,128.123,38.816,128.163,38.786,128.249,38.745,128.329,38.681,128.375,38.623]],[[124.905,39.536,124.849,39.508,124.846,39.559,124.89,39.602,124.935,39.608,124.905,39.536]]]},{"n":"Nigeria","c":"NG","r":2,"ml":1.7,"lx":7.5,"ly":9.44,"b":[2.686,4.277,14.627,13.873],"p":[[[7.301,4.418,7.204,4.388,7.14,4.395,7.227,4.527,7.271,4.499,7.328,4.487,7.301,4.418]],[[13.606,13.705,13.763,13.49,13.932,13.258,14.064,13.079,14.16,12.613,14.17,12.524,14.178,12.484,14.185,12.447,14.197,12.384,14.273,12.356,14.415,12.344,14.519,12.298,14.581,12.222,14.587,12.209,14.62,12.151,14.627,12.109,14.618,11.987,14.597,11.83,14.562,11.729,14.582,11.591,14.575,11.532,14.56,11.492,14.496,11.446,14.409,11.401,14.202,11.268,14.143,11.249,14.057,11.245,13.981,11.212,13.892,11.14,13.7,10.873,13.535,10.605,13.479,10.383,13.415,10.171,13.27,10.036,13.25,9.96,13.244,9.916,13.239,9.814,13.221,9.645,13.199,9.564,13.175,9.54,13.019,9.488,12.929,9.426,12.876,9.304,12.856,9.171,12.824,9.019,12.807,8.887,12.782,8.818,12.731,8.746,12.652,8.668,12.583,8.624,12.404,8.596,12.311,8.42,12.233,8.282,12.231,8.227,12.156,7.942,12.025,7.728,12.017,7.652,12.016,7.59,11.852,7.401,11.809,7.345,11.767,7.272,11.809,7.202,11.855,7.138,11.861,7.116,11.787,7.056,11.658,6.952,11.58,6.889,11.563,6.855,11.552,6.697,11.529,6.655,11.478,6.597,11.402,6.534,11.325,6.485,11.237,6.451,11.153,6.438,11.106,6.458,11.08,6.506,11.033,6.698,11.009,6.739,10.954,6.777,10.846,6.882,10.738,6.988,10.606,7.063,10.578,7.058,10.556,7.037,10.519,6.93,10.482,6.891,10.413,6.878,10.293,6.877,10.205,6.892,10.186,6.913,10.168,6.959,10.144,6.996,10.039,6.921,9.874,6.803,9.821,6.784,9.78,6.76,9.726,6.65,9.66,6.532,9.574,6.47,9.49,6.419,9.442,6.373,9.373,6.32,9.239,6.186,9.06,6.009,8.997,5.918,8.935,5.781,8.899,5.63,8.859,5.464,8.801,5.197,8.716,5.047,8.641,4.927,8.585,4.833,8.556,4.755,8.544,4.758,8.515,4.725,8.431,4.746,8.394,4.814,8.342,4.825,8.253,4.924,8.234,4.907,8.328,4.656,8.293,4.558,8.029,4.555,7.801,4.522,7.644,4.525,7.566,4.561,7.531,4.655,7.517,4.645,7.509,4.595,7.46,4.555,7.284,4.548,7.207,4.612,7.144,4.684,7.077,4.716,7.087,4.686,7.164,4.616,7.155,4.514,7.013,4.397,6.923,4.391,6.868,4.441,6.839,4.523,6.825,4.645,6.788,4.725,6.768,4.725,6.786,4.652,6.792,4.593,6.793,4.469,6.86,4.373,6.757,4.344,6.715,4.342,6.633,4.34,6.617,4.376,6.602,4.455,6.58,4.476,6.555,4.341,6.5,4.332,6.462,4.333,6.3,4.304,6.264,4.309,6.256,4.334,6.275,4.372,6.271,4.432,6.215,4.385,6.206,4.292,6.173,4.277,6.077,4.291,5.971,4.339,5.906,4.388,5.799,4.456,5.588,4.647,5.554,4.733,5.493,4.839,5.448,4.946,5.383,5.129,5.403,5.142,5.452,5.127,5.476,5.154,5.388,5.174,5.37,5.195,5.364,5.259,5.368,5.338,5.439,5.365,5.501,5.379,5.532,5.426,5.55,5.474,5.386,5.402,5.232,5.484,5.199,5.534,5.216,5.572,5.289,5.577,5.394,5.575,5.457,5.612,5.418,5.625,5.35,5.623,5.325,5.648,5.327,5.708,5.305,5.694,5.276,5.642,5.173,5.603,5.112,5.642,5.106,5.728,5.093,5.767,5.042,5.798,4.861,6.026,4.634,6.217,4.431,6.349,4.126,6.411,3.487,6.409,3.451,6.427,3.49,6.457,3.546,6.477,3.752,6.584,3.717,6.598,3.503,6.531,3.43,6.525,3.336,6.397,2.772,6.376,2.706,6.369,2.708,6.428,2.736,6.596,2.754,6.662,2.775,6.712,2.753,6.772,2.732,6.853,2.721,6.98,2.748,7.02,2.757,7.068,2.751,7.143,2.75,7.395,2.766,7.423,2.784,7.443,2.785,7.477,2.751,7.542,2.719,7.616,2.72,7.723,2.708,7.827,2.686,7.874,2.702,8.05,2.712,8.273,2.703,8.372,2.724,8.442,2.735,8.614,2.733,8.783,2.775,9.049,2.898,9.061,3.045,9.084,3.11,9.188,3.148,9.321,3.136,9.452,3.165,9.495,3.223,9.566,3.329,9.667,3.325,9.778,3.354,9.813,3.405,9.839,3.477,9.852,3.557,9.907,3.602,10.005,3.646,10.16,3.577,10.268,3.578,10.292,3.604,10.351,3.647,10.409,3.68,10.428,3.758,10.413,3.772,10.418,3.784,10.436,3.834,10.607,3.83,10.654,3.757,10.769,3.745,10.85,3.734,10.972,3.716,11.08,3.695,11.12,3.656,11.155,3.639,11.177,3.488,11.395,3.491,11.499,3.554,11.632,3.595,11.696,3.653,11.732,3.665,11.762,3.647,11.8,3.618,11.828,3.612,11.887,3.62,11.927,3.641,11.97,3.633,12.062,3.634,12.202,3.644,12.405,3.647,12.53,3.769,12.622,3.948,12.775,4.039,12.935,4.087,13.055,4.148,13.458,4.191,13.482,4.242,13.501,4.421,13.648,4.559,13.702,4.665,13.733,4.823,13.76,4.922,13.749,5.101,13.743,5.242,13.757,5.362,13.837,5.416,13.859,5.492,13.873,5.838,13.765,6.184,13.664,6.247,13.673,6.3,13.659,6.386,13.604,6.514,13.485,6.59,13.409,6.627,13.364,6.804,13.108,6.871,13.043,6.937,13.008,7.005,12.996,7.057,13,7.106,13.029,7.173,13.086,7.275,13.112,7.358,13.107,7.789,13.338,7.83,13.341,7.956,13.323,8.095,13.291,8.456,13.06,8.751,12.908,8.958,12.857,9.202,12.821,9.616,12.811,9.929,13.135,10.045,13.206,10.185,13.27,10.23,13.281,10.476,13.33,10.959,13.372,11.412,13.354,11.501,13.341,11.693,13.298,11.99,13.192,12.118,13.09,12.319,13.074,12.463,13.094,12.51,13.194,12.655,13.327,12.76,13.38,12.872,13.449,13.048,13.535,13.194,13.573,13.324,13.671,13.427,13.702,13.606,13.705]]]},{"n":"Niger","c":"NE","r":3,"ml":3,"lx":9.5,"ly":17.45,"b":[0.164,11.696,15.963,23.518],"p":[[[13.606,13.705,13.427,13.702,13.324,13.671,13.194,13.573,13.048,13.535,12.872,13.449,12.76,13.38,12.655,13.327,12.51,13.194,12.463,13.094,12.319,13.074,12.118,13.09,11.99,13.192,11.693,13.298,11.501,13.341,11.412,13.354,10.959,13.372,10.476,13.33,10.23,13.281,10.185,13.27,10.045,13.206,9.929,13.135,9.616,12.811,9.202,12.821,8.958,12.857,8.751,12.908,8.456,13.06,8.095,13.291,7.956,13.323,7.83,13.341,7.789,13.338,7.358,13.107,7.275,13.112,7.173,13.086,7.106,13.029,7.057,13,7.005,12.996,6.937,13.008,6.871,13.043,6.804,13.108,6.627,13.364,6.59,13.409,6.514,13.485,6.386,13.604,6.3,13.659,6.247,13.673,6.184,13.664,5.838,13.765,5.492,13.873,5.416,13.859,5.362,13.837,5.242,13.757,5.101,13.743,4.922,13.749,4.823,13.76,4.665,13.733,4.559,13.702,4.421,13.648,4.242,13.501,4.191,13.482,4.148,13.458,4.087,13.055,4.039,12.935,3.948,12.775,3.769,12.622,3.647,12.53,3.644,12.405,3.634,12.202,3.633,12.062,3.641,11.97,3.62,11.927,3.612,11.887,3.618,11.828,3.647,11.8,3.665,11.762,3.653,11.732,3.595,11.696,3.532,11.787,3.45,11.852,3.36,11.88,3.299,11.927,3.267,11.992,3.15,12.118,2.878,12.368,2.85,12.374,2.805,12.384,2.729,12.354,2.681,12.313,2.648,12.297,2.598,12.294,2.469,12.263,2.366,12.222,2.363,12.188,2.413,11.999,2.389,11.897,2.343,11.946,2.194,12.136,2.091,12.278,2.073,12.309,2.058,12.358,2.069,12.379,2.109,12.394,2.204,12.413,2.221,12.427,2.226,12.466,2.212,12.538,2.16,12.636,2.105,12.701,2.074,12.714,2.017,12.716,1.956,12.707,1.841,12.628,1.79,12.613,1.671,12.62,1.565,12.635,1.5,12.676,1.309,12.834,1.097,13.001,1.008,13.025,0.987,13.042,0.973,13.17,0.977,13.325,0.988,13.365,1.077,13.341,1.171,13.33,1.201,13.358,1.126,13.412,1.018,13.468,0.978,13.552,0.947,13.581,0.898,13.611,0.842,13.626,0.786,13.65,0.748,13.675,0.685,13.685,0.618,13.703,0.522,13.84,0.429,13.972,0.374,14.076,0.355,14.139,0.383,14.246,0.355,14.288,0.251,14.396,0.164,14.497,0.185,14.653,0.203,14.783,0.204,14.865,0.217,14.911,0.229,14.964,0.286,14.98,0.433,14.979,0.719,14.955,0.947,14.982,0.96,14.987,1.121,15.126,1.3,15.272,1.569,15.286,1.859,15.302,2.088,15.309,2.421,15.32,2.69,15.33,3.001,15.341,3.011,15.408,3.029,15.425,3.06,15.427,3.289,15.391,3.504,15.356,3.521,15.483,3.71,15.642,3.817,15.674,3.843,15.702,3.877,15.755,3.898,15.838,3.907,15.897,3.947,15.946,3.976,16.036,4.015,16.193,4.121,16.358,4.182,16.582,4.191,16.798,4.203,16.963,4.235,16.996,4.234,17.288,4.233,17.582,4.232,17.831,4.231,18.139,4.23,18.411,4.229,18.704,4.228,18.968,4.228,19.143,4.446,19.185,4.671,19.228,5.001,19.291,5.359,19.36,5.748,19.434,5.837,19.479,6.131,19.732,6.263,19.846,6.527,20.073,6.731,20.248,6.989,20.471,7.263,20.694,7.482,20.873,7.825,21.076,8.343,21.381,8.861,21.686,9.379,21.991,9.896,22.297,10.414,22.602,10.932,22.907,11.45,23.213,11.968,23.518,12.489,23.402,12.984,23.291,13.481,23.18,13.599,23.12,13.863,22.902,14.201,22.624,14.216,22.62,14.231,22.618,14.556,22.783,14.979,22.996,15.089,22.418,15.172,21.922,15.178,21.606,15.182,21.523,15.216,21.467,15.294,21.412,15.607,20.954,15.54,20.875,15.587,20.733,15.668,20.672,15.929,20.4,15.963,20.346,15.949,20.303,15.766,19.983,15.735,19.904,15.699,19.495,15.673,19.207,15.638,18.811,15.596,18.337,15.562,17.937,15.517,17.408,15.474,16.908,15.212,16.634,14.747,16.147,14.368,15.75,14.178,15.485,13.807,14.966,13.642,14.631,13.514,14.456,13.448,14.381,13.506,14.134,13.606,13.705]]]},{"n":"Nicaragua","c":"NI","r":5,"ml":4,"lx":-85.07,"ly":12.67,"b":[-87.67,10.735,-83.158,15.008],"p":[[[-83.158,14.993,-83.185,14.956,-83.216,14.932,-83.28,14.813,-83.302,14.802,-83.306,14.891,-83.344,14.902,-83.389,14.871,-83.414,14.825,-83.375,14.766,-83.341,14.765,-83.299,14.749,-83.188,14.34,-83.212,14.267,-83.281,14.154,-83.347,14.057,-83.412,13.996,-83.494,13.739,-83.567,13.32,-83.514,12.944,-83.541,12.596,-83.518,12.514,-83.511,12.412,-83.565,12.393,-83.596,12.396,-83.627,12.459,-83.624,12.515,-83.591,12.579,-83.578,12.667,-83.593,12.713,-83.625,12.613,-83.682,12.568,-83.718,12.553,-83.754,12.502,-83.716,12.407,-83.667,12.337,-83.651,12.287,-83.669,12.228,-83.68,12.024,-83.698,12.03,-83.716,12.057,-83.767,12.059,-83.773,11.977,-83.769,11.932,-83.813,11.896,-83.829,11.861,-83.793,11.836,-83.753,11.821,-83.705,11.825,-83.664,11.724,-83.652,11.642,-83.745,11.567,-83.777,11.504,-83.829,11.428,-83.859,11.354,-83.868,11.3,-83.832,11.131,-83.768,11.01,-83.714,10.934,-83.642,10.917,-83.659,10.837,-83.713,10.786,-83.811,10.743,-83.919,10.735,-84.096,10.776,-84.168,10.78,-84.197,10.802,-84.205,10.841,-84.256,10.901,-84.348,10.98,-84.402,10.974,-84.489,10.992,-84.634,11.046,-84.701,11.052,-84.797,11.006,-84.909,10.945,-85.179,11.04,-85.368,11.106,-85.539,11.166,-85.584,11.189,-85.621,11.184,-85.654,11.153,-85.691,11.097,-85.703,11.082,-85.722,11.066,-85.744,11.062,-85.745,11.089,-85.829,11.199,-85.961,11.331,-86.469,11.738,-86.656,11.982,-86.756,12.157,-86.851,12.248,-87.125,12.434,-87.188,12.508,-87.46,12.758,-87.668,12.904,-87.67,12.966,-87.585,13.043,-87.543,13.04,-87.498,12.984,-87.424,12.921,-87.39,12.921,-87.339,12.95,-87.337,12.979,-87.059,12.991,-87.009,13.008,-86.959,13.054,-86.933,13.118,-86.929,13.179,-86.918,13.224,-86.874,13.267,-86.792,13.28,-86.729,13.284,-86.711,13.313,-86.73,13.407,-86.764,13.635,-86.771,13.699,-86.759,13.746,-86.734,13.763,-86.61,13.775,-86.377,13.756,-86.332,13.77,-86.238,13.899,-86.151,13.995,-86.089,14.037,-86.04,14.05,-85.984,13.966,-85.787,13.844,-85.753,13.852,-85.734,13.859,-85.728,13.876,-85.731,13.932,-85.682,13.983,-85.58,14.028,-85.477,14.109,-85.374,14.224,-85.284,14.292,-85.208,14.312,-85.179,14.343,-85.198,14.386,-85.192,14.447,-85.161,14.525,-85.117,14.571,-85.059,14.583,-85.037,14.608,-85.049,14.645,-85.037,14.686,-84.985,14.752,-84.86,14.81,-84.789,14.79,-84.73,14.713,-84.646,14.661,-84.538,14.633,-84.454,14.644,-84.394,14.692,-84.34,14.706,-84.292,14.687,-84.267,14.698,-84.264,14.739,-84.239,14.748,-84.192,14.726,-84.151,14.72,-84.114,14.731,-84.1,14.751,-84.093,14.771,-84.066,14.786,-83.972,14.771,-83.867,14.794,-83.751,14.856,-83.674,14.884,-83.635,14.876,-83.59,14.908,-83.537,14.977,-83.415,15.008,-83.158,14.993]]]},{"n":"New Zealand","c":"NZ","r":2,"ml":2,"lx":172.79,"ly":-39.76,"b":[-176.848,-52.57,178.536,-8.546],"p":[[[173.115,-41.279,173.231,-41.284,173.338,-41.211,173.447,-41.151,173.563,-41.102,173.738,-40.989,173.784,-40.972,173.898,-40.951,173.947,-40.924,174.002,-40.918,173.953,-40.985,173.89,-41.007,173.88,-41.031,173.915,-41.07,173.86,-41.124,173.862,-41.192,173.798,-41.272,173.897,-41.239,173.933,-41.187,173.915,-41.158,173.958,-41.1,174.024,-41.072,173.998,-41.028,173.999,-40.993,174.081,-41.006,174.121,-41.005,174.153,-40.991,174.212,-40.985,174.224,-41.024,174.303,-41.02,174.274,-41.069,174.214,-41.126,174.2,-41.16,174.103,-41.217,174.039,-41.242,174.138,-41.248,174.284,-41.172,174.37,-41.104,174.368,-41.188,174.297,-41.264,174.237,-41.312,174.17,-41.327,174.102,-41.366,174.069,-41.429,174.073,-41.472,174.092,-41.505,174.161,-41.562,174.084,-41.671,174.17,-41.657,174.217,-41.678,174.283,-41.741,174.243,-41.813,174.215,-41.85,174.047,-42.003,173.974,-42.081,173.888,-42.13,173.889,-42.212,173.84,-42.271,173.589,-42.474,173.545,-42.518,173.348,-42.841,173.221,-42.977,173.149,-43.023,173.072,-43.06,172.889,-43.124,172.808,-43.198,172.719,-43.259,172.624,-43.272,172.627,-43.3,172.688,-43.315,172.735,-43.355,172.7,-43.4,172.632,-43.428,172.562,-43.436,172.527,-43.465,172.693,-43.444,172.74,-43.468,172.749,-43.517,172.767,-43.562,172.807,-43.621,172.947,-43.659,173.073,-43.676,173.098,-43.704,173.117,-43.798,173.094,-43.844,173.066,-43.875,173.023,-43.885,172.921,-43.891,172.818,-43.87,172.749,-43.813,172.555,-43.831,172.503,-43.844,172.476,-43.833,172.584,-43.774,172.527,-43.739,172.48,-43.727,172.43,-43.746,172.396,-43.778,172.385,-43.83,172.35,-43.859,172.297,-43.868,172.221,-43.825,172.146,-43.764,172.036,-43.702,172.052,-43.74,172.137,-43.834,172.18,-43.896,172.081,-43.946,171.978,-43.984,171.891,-44.007,171.808,-44.042,171.712,-44.097,171.659,-44.117,171.518,-44.118,171.443,-44.136,171.417,-44.209,171.365,-44.255,171.241,-44.264,171.285,-44.279,171.313,-44.302,171.231,-44.521,171.213,-44.612,171.198,-44.768,171.146,-44.912,170.999,-44.911,171.023,-44.937,171.134,-44.978,171.113,-45.039,170.991,-45.151,170.94,-45.216,170.89,-45.374,170.815,-45.519,170.701,-45.684,170.7,-45.714,170.74,-45.756,170.788,-45.792,170.791,-45.844,170.776,-45.871,170.722,-45.878,170.674,-45.896,170.419,-45.941,170.335,-45.992,170.267,-46.083,170.186,-46.161,169.918,-46.334,169.761,-46.48,169.729,-46.521,169.687,-46.552,169.342,-46.621,169.099,-46.631,168.966,-46.613,168.838,-46.578,168.767,-46.566,168.631,-46.588,168.572,-46.611,168.466,-46.588,168.382,-46.605,168.357,-46.588,168.326,-46.546,168.343,-46.489,168.32,-46.447,168.266,-46.419,168.23,-46.386,168.189,-46.362,168.077,-46.353,167.9,-46.368,167.842,-46.366,167.722,-46.227,167.682,-46.193,167.539,-46.149,167.491,-46.155,167.414,-46.229,167.369,-46.242,167.1,-46.249,166.831,-46.225,166.732,-46.198,166.712,-46.134,166.917,-45.957,166.856,-45.981,166.73,-46.053,166.65,-46.042,166.727,-45.963,166.734,-45.928,166.718,-45.889,166.613,-45.955,166.493,-45.964,166.478,-45.903,166.488,-45.832,166.513,-45.812,166.836,-45.775,166.953,-45.75,167.003,-45.712,166.81,-45.699,166.798,-45.646,166.826,-45.603,166.991,-45.532,166.869,-45.55,166.734,-45.544,166.743,-45.468,166.778,-45.41,166.92,-45.408,166.876,-45.368,166.869,-45.311,166.909,-45.307,167.052,-45.383,167.156,-45.411,167.112,-45.354,167.118,-45.318,167.145,-45.302,167.23,-45.29,167.207,-45.28,167.127,-45.266,167.033,-45.222,167.023,-45.177,167.026,-45.124,167.128,-45.051,167.188,-45.094,167.259,-45.082,167.205,-45.048,167.172,-44.997,167.195,-44.963,167.411,-44.828,167.466,-44.958,167.479,-44.915,167.482,-44.874,167.456,-44.838,167.46,-44.802,167.485,-44.771,167.578,-44.741,167.698,-44.641,167.787,-44.595,167.859,-44.625,167.909,-44.665,167.902,-44.625,167.866,-44.592,167.857,-44.501,168.018,-44.359,168.196,-44.224,168.367,-44.082,168.457,-44.031,168.651,-43.972,168.775,-43.996,168.806,-43.992,168.99,-43.89,169.067,-43.863,169.136,-43.9,169.179,-43.913,169.136,-43.82,169.17,-43.777,169.323,-43.702,169.515,-43.624,169.662,-43.591,169.769,-43.538,169.834,-43.537,169.824,-43.497,169.835,-43.459,169.891,-43.462,169.908,-43.447,169.859,-43.426,170.018,-43.349,170.104,-43.265,170.149,-43.248,170.19,-43.222,170.24,-43.164,170.3,-43.145,170.356,-43.154,170.396,-43.182,170.374,-43.135,170.303,-43.108,170.379,-43.066,170.459,-43.038,170.536,-43.058,170.612,-43.092,170.536,-43.041,170.524,-43.009,170.616,-42.972,170.665,-42.961,170.735,-43.03,170.725,-42.975,170.742,-42.927,170.84,-42.849,170.97,-42.718,171.011,-42.764,171.018,-42.819,171.012,-42.885,171.038,-42.862,171.048,-42.802,171.028,-42.696,171.19,-42.5,171.221,-42.479,171.257,-42.465,171.313,-42.46,171.296,-42.431,171.252,-42.402,171.296,-42.303,171.323,-42.189,171.36,-42.08,171.421,-41.973,171.486,-41.795,171.536,-41.758,171.672,-41.745,171.732,-41.72,171.831,-41.655,171.948,-41.539,172.011,-41.445,172.093,-41.202,172.139,-40.947,172.273,-40.759,172.468,-40.622,172.641,-40.518,172.711,-40.497,172.83,-40.49,172.944,-40.519,172.733,-40.544,172.711,-40.605,172.704,-40.668,172.729,-40.724,172.767,-40.773,172.869,-40.82,172.989,-40.848,173.042,-40.954,173.052,-41.079,173.069,-41.186,173.115,-41.279]],[[173.915,-40.864,173.781,-40.922,173.786,-40.881,173.812,-40.794,173.873,-40.749,173.903,-40.746,173.964,-40.713,173.958,-40.787,173.915,-40.864]],[[166.746,-45.656,166.741,-45.705,166.729,-45.73,166.695,-45.73,166.642,-45.724,166.592,-45.702,166.559,-45.708,166.532,-45.7,166.567,-45.644,166.686,-45.615,166.731,-45.639,166.746,-45.656]],[[166.979,-45.18,167.023,-45.3,166.931,-45.277,166.893,-45.241,166.963,-45.18,166.979,-45.18]],[[168.145,-46.862,168.145,-46.902,168.041,-46.888,168.043,-46.933,168.125,-46.956,168.156,-46.988,168.241,-46.979,168.261,-47.027,168.241,-47.07,168.184,-47.102,168.015,-47.117,167.906,-47.18,167.811,-47.17,167.785,-47.176,167.676,-47.243,167.555,-47.264,167.522,-47.259,167.539,-47.199,167.629,-47.142,167.631,-47.088,167.654,-47.044,167.741,-47.014,167.742,-46.957,167.801,-46.907,167.765,-46.798,167.784,-46.7,167.956,-46.694,168.145,-46.862]],[[175.543,-36.279,175.551,-36.334,175.475,-36.314,175.445,-36.273,175.359,-36.231,175.346,-36.218,175.337,-36.135,175.382,-36.095,175.39,-36.078,175.409,-36.071,175.444,-36.115,175.513,-36.177,175.543,-36.279]],[[173.269,-34.935,173.285,-34.981,173.34,-34.948,173.381,-34.896,173.448,-34.844,173.439,-34.929,173.473,-34.947,173.694,-35.006,173.739,-35.055,173.786,-35.069,173.813,-35.041,173.844,-35.026,173.924,-35.057,174.104,-35.143,174.119,-35.172,174.11,-35.216,174.119,-35.263,174.143,-35.3,174.203,-35.309,174.283,-35.254,174.32,-35.247,174.373,-35.325,174.393,-35.369,174.385,-35.367,174.419,-35.411,174.465,-35.454,174.543,-35.582,174.532,-35.627,174.509,-35.667,174.581,-35.786,174.533,-35.794,174.391,-35.774,174.396,-35.797,174.479,-35.884,174.549,-36.007,174.605,-36.081,174.802,-36.309,174.772,-36.391,174.777,-36.445,174.752,-36.491,174.819,-36.612,174.777,-36.65,174.749,-36.774,174.719,-36.796,174.722,-36.841,174.802,-36.853,174.85,-36.873,174.891,-36.909,174.917,-36.865,174.952,-36.853,175.047,-36.912,175.245,-36.971,175.3,-36.993,175.326,-37.041,175.347,-37.156,175.385,-37.207,175.461,-37.217,175.542,-37.201,175.568,-37.159,175.552,-37.046,175.493,-36.866,175.493,-36.807,175.501,-36.748,175.487,-36.69,175.458,-36.634,175.426,-36.592,175.386,-36.556,175.4,-36.501,175.461,-36.476,175.498,-36.523,175.528,-36.579,175.681,-36.747,175.772,-36.735,175.781,-36.805,175.842,-36.875,175.876,-36.958,175.921,-37.205,175.99,-37.437,176.115,-37.538,176.129,-37.587,176.053,-37.562,176.03,-37.576,176.038,-37.601,176.108,-37.645,176.191,-37.667,176.243,-37.664,176.292,-37.68,176.615,-37.831,176.77,-37.89,177.162,-37.986,177.274,-37.993,177.336,-37.991,177.453,-37.957,177.558,-37.897,177.649,-37.808,177.727,-37.706,177.813,-37.656,177.909,-37.617,177.958,-37.581,178.009,-37.555,178.272,-37.567,178.361,-37.618,178.476,-37.66,178.536,-37.692,178.516,-37.758,178.447,-37.854,178.394,-37.96,178.347,-38.201,178.315,-38.444,178.268,-38.551,178.181,-38.634,178.085,-38.694,177.976,-38.722,177.932,-38.86,177.91,-39.022,177.917,-39.062,177.951,-39.095,177.966,-39.142,177.909,-39.24,177.875,-39.225,177.829,-39.145,177.786,-39.111,177.656,-39.086,177.523,-39.074,177.408,-39.081,177.297,-39.116,177.129,-39.186,177.077,-39.222,177.031,-39.267,176.954,-39.368,176.936,-39.491,176.939,-39.555,176.967,-39.605,177.11,-39.673,176.968,-39.911,176.842,-40.158,176.771,-40.228,176.689,-40.293,176.612,-40.442,176.476,-40.57,176.385,-40.668,176.314,-40.769,176.252,-40.877,176.119,-41.029,176.06,-41.13,175.983,-41.213,175.84,-41.32,175.687,-41.412,175.447,-41.538,175.38,-41.58,175.31,-41.611,175.222,-41.574,175.204,-41.535,175.185,-41.449,175.166,-41.417,175.054,-41.391,174.906,-41.433,174.881,-41.424,174.875,-41.404,174.875,-41.278,174.9,-41.243,174.866,-41.223,174.832,-41.231,174.82,-41.263,174.841,-41.291,174.757,-41.325,174.67,-41.326,174.643,-41.313,174.635,-41.289,174.657,-41.251,174.685,-41.218,174.848,-41.059,175.017,-40.848,175.163,-40.622,175.2,-40.505,175.254,-40.289,175.21,-40.199,175.156,-40.115,175.009,-39.952,174.814,-39.86,174.687,-39.847,174.567,-39.813,174.455,-39.735,174.352,-39.643,174.149,-39.568,173.934,-39.509,173.812,-39.426,173.783,-39.376,173.764,-39.319,173.766,-39.265,173.782,-39.211,173.806,-39.17,173.844,-39.139,174.071,-39.031,174.312,-38.971,174.356,-38.972,174.398,-38.963,174.458,-38.926,174.566,-38.842,174.597,-38.785,174.619,-38.605,174.653,-38.428,174.715,-38.226,174.809,-38.1,174.84,-38.023,174.802,-37.896,174.837,-37.849,174.88,-37.821,174.928,-37.804,174.846,-37.685,174.749,-37.505,174.729,-37.449,174.744,-37.393,174.768,-37.339,174.707,-37.325,174.673,-37.273,174.586,-37.098,174.61,-37.07,174.66,-37.089,174.734,-37.215,174.746,-37.15,174.804,-37.11,174.864,-37.089,174.929,-37.085,174.782,-36.944,174.733,-36.949,174.668,-36.972,174.601,-36.986,174.537,-36.973,174.476,-36.942,174.445,-36.883,174.406,-36.768,174.382,-36.726,174.189,-36.492,174.246,-36.485,174.402,-36.602,174.432,-36.565,174.454,-36.511,174.447,-36.451,174.41,-36.406,174.354,-36.376,174.353,-36.323,174.395,-36.274,174.393,-36.24,174.304,-36.171,174.268,-36.163,174.252,-36.196,174.278,-36.244,174.254,-36.249,174.036,-36.122,173.969,-36.021,173.914,-35.909,173.909,-35.954,173.917,-36.018,174.003,-36.146,174.142,-36.289,174.166,-36.328,174.146,-36.377,174.097,-36.391,174.055,-36.36,173.991,-36.237,173.945,-36.176,173.412,-35.543,173.48,-35.459,173.586,-35.389,173.61,-35.357,173.626,-35.319,173.582,-35.313,173.542,-35.33,173.496,-35.362,173.454,-35.399,173.402,-35.481,173.376,-35.5,173.314,-35.443,173.29,-35.408,173.291,-35.366,173.275,-35.34,173.228,-35.331,173.16,-35.248,173.117,-35.205,173.189,-35.124,173.191,-35.016,173.117,-34.903,173.03,-34.8,172.861,-34.632,172.706,-34.455,172.874,-34.433,173.044,-34.429,172.964,-34.535,173,-34.596,173.054,-34.648,173.171,-34.807,173.181,-34.853,173.241,-34.899,173.269,-34.935]],[[169.178,-52.497,169.233,-52.548,169.128,-52.57,169.076,-52.552,169.04,-52.529,169.022,-52.495,169.079,-52.499,169.129,-52.485,169.178,-52.497]],[[166.221,-50.762,166.243,-50.846,166.188,-50.846,166.073,-50.823,166.038,-50.787,166.013,-50.778,165.971,-50.82,165.904,-50.821,165.889,-50.808,165.916,-50.763,166.074,-50.679,166.103,-50.573,166.101,-50.539,166.225,-50.531,166.254,-50.544,166.267,-50.559,166.259,-50.577,166.21,-50.612,166.208,-50.652,166.22,-50.694,166.179,-50.715,166.201,-50.751,166.221,-50.762]],[[-176.178,-43.74,-176.214,-43.766,-176.275,-43.765,-176.382,-43.867,-176.375,-43.791,-176.407,-43.761,-176.499,-43.768,-176.517,-43.785,-176.455,-43.805,-176.441,-43.816,-176.5,-43.86,-176.439,-43.955,-176.385,-43.951,-176.334,-44.025,-176.334,-44.048,-176.453,-44.077,-176.516,-44.117,-176.572,-44.115,-176.598,-44.107,-176.629,-44.036,-176.632,-44.006,-176.563,-43.954,-176.524,-43.901,-176.555,-43.852,-176.635,-43.82,-176.808,-43.835,-176.848,-43.824,-176.761,-43.758,-176.667,-43.765,-176.566,-43.718,-176.178,-43.74]],[[-176.176,-44.322,-176.221,-44.331,-176.215,-44.274,-176.229,-44.237,-176.155,-44.225,-176.123,-44.268,-176.176,-44.322]],[[-172.479,-8.581,-172.484,-8.583,-172.488,-8.572,-172.494,-8.559,-172.499,-8.548,-172.497,-8.546,-172.487,-8.556,-172.481,-8.567,-172.479,-8.581]],[[-171.186,-9.355,-171.189,-9.358,-171.193,-9.352,-171.2,-9.345,-171.204,-9.333,-171.202,-9.333,-171.194,-9.339,-171.189,-9.347,-171.186,-9.355]]]},{"n":"Niue","c":"NU","r":4,"ml":4,"lx":-169.86,"ly":-19.05,"b":[-169.948,-19.138,-169.793,-18.966],"p":[[[-169.803,-19.083,-169.904,-19.138,-169.948,-19.073,-169.909,-18.99,-169.862,-18.969,-169.834,-18.966,-169.793,-19.043,-169.803,-19.083]]]},{"n":"Cook Islands","c":"CK","r":4,"ml":4,"lx":-159.79,"ly":-21.22,"b":[-159.842,-21.25,-159.737,-21.186],"p":[[[-159.741,-21.249,-159.773,-21.25,-159.813,-21.242,-159.84,-21.238,-159.842,-21.229,-159.832,-21.2,-159.811,-21.186,-159.768,-21.188,-159.74,-21.208,-159.737,-21.241,-159.741,-21.249]]]},{"n":"Netherlands","c":"NL","r":5,"ml":4,"lx":5.61,"ly":52.42,"b":[-68.371,12.032,7.197,53.625],"p":[[[5.994,50.75,5.892,50.753,5.797,50.755,5.747,50.76,5.694,50.775,5.669,50.806,5.639,50.844,5.648,50.867,5.737,50.932,5.75,50.95,5.741,50.96,5.75,50.989,5.818,51.086,5.827,51.126,5.796,51.153,5.752,51.169,5.609,51.198,5.54,51.239,5.509,51.275,5.477,51.285,5.43,51.273,5.311,51.26,5.214,51.279,5.1,51.346,5.073,51.407,5.059,51.453,5.031,51.469,4.993,51.445,4.944,51.408,4.848,51.403,4.821,51.412,4.816,51.433,4.811,51.453,4.784,51.477,4.756,51.491,4.634,51.422,4.589,51.422,4.532,51.449,4.503,51.475,4.441,51.46,4.385,51.428,4.404,51.367,4.374,51.356,4.304,51.362,4.226,51.386,4.139,51.402,4.007,51.443,3.822,51.409,3.694,51.45,3.587,51.454,3.521,51.486,3.449,51.541,3.5,51.577,3.549,51.589,3.744,51.596,3.886,51.574,4.141,51.456,4.206,51.457,4.274,51.472,4.239,51.504,4.175,51.519,4.08,51.551,4.005,51.596,4.183,51.61,4.158,51.633,4.135,51.673,3.947,51.811,3.979,51.848,4.026,51.928,4.085,51.994,4.132,52.012,4.209,52.059,4.376,52.197,4.483,52.309,4.562,52.443,4.678,52.81,4.713,52.872,4.769,52.941,4.839,52.928,4.888,52.908,5.061,52.961,5.358,53.096,5.446,53.214,5.532,53.269,5.874,53.375,6.062,53.407,6.353,53.415,6.564,53.434,6.816,53.441,6.912,53.375,6.968,53.327,7.058,53.301,7.197,53.282,7.189,53.187,7.19,53,7.179,52.966,7.117,52.887,7.051,52.745,7.033,52.651,7.013,52.634,6.748,52.634,6.711,52.618,6.705,52.598,6.719,52.574,6.712,52.55,6.692,52.53,6.703,52.499,6.749,52.464,6.833,52.442,6.922,52.44,6.968,52.444,7.002,52.419,7.035,52.38,7.033,52.331,7.02,52.266,6.977,52.206,6.855,52.136,6.8,52.111,6.749,52.099,6.725,52.08,6.713,52.057,6.716,52.036,6.802,51.98,6.8,51.967,6.775,51.938,6.742,51.911,6.518,51.854,6.425,51.858,6.372,51.83,6.356,51.825,6.297,51.851,6.167,51.881,6.117,51.87,6.09,51.854,6.008,51.834,5.949,51.803,5.949,51.762,6.053,51.658,6.089,51.638,6.091,51.599,6.142,51.55,6.193,51.489,6.199,51.45,6.193,51.411,6.166,51.355,6.076,51.224,6.075,51.199,6.082,51.18,6.113,51.175,6.137,51.165,6.13,51.147,5.961,51.057,5.939,51.041,5.868,51.045,5.858,51.03,5.867,51.006,5.895,50.984,5.955,50.973,6.007,50.95,6.048,50.905,5.994,50.75]],[[4.226,51.386,4.211,51.349,4.173,51.307,4.04,51.247,3.902,51.208,3.831,51.213,3.782,51.233,3.756,51.255,3.682,51.276,3.58,51.286,3.517,51.264,3.472,51.242,3.433,51.246,3.403,51.264,3.38,51.291,3.35,51.378,3.426,51.394,3.589,51.399,3.717,51.369,3.883,51.354,4.011,51.396,4.112,51.361,4.226,51.386]],[[6.333,53.511,6.193,53.477,6.159,53.484,6.168,53.494,6.291,53.515,6.333,53.511]],[[5.929,53.459,5.732,53.443,5.665,53.455,5.654,53.467,5.708,53.473,5.876,53.475,5.928,53.465,5.929,53.459]],[[5.109,53.308,4.924,53.235,4.908,53.246,5.027,53.31,5.109,53.308]],[[5.326,53.386,5.233,53.378,5.19,53.392,5.415,53.431,5.557,53.444,5.583,53.438,5.326,53.386]],[[4.886,53.071,4.787,53,4.727,53.02,4.709,53.036,4.74,53.091,4.886,53.183,4.886,53.071]],[[3.949,51.739,4.047,51.685,4.068,51.667,4.075,51.649,3.951,51.627,3.819,51.694,3.732,51.678,3.699,51.71,3.699,51.73,3.789,51.746,3.949,51.739]],[[6.735,53.583,6.642,53.579,6.669,53.606,6.755,53.625,6.801,53.625,6.735,53.583]],[[-68.206,12.145,-68.254,12.032,-68.282,12.082,-68.287,12.172,-68.307,12.207,-68.348,12.228,-68.371,12.258,-68.369,12.302,-68.219,12.231,-68.206,12.145]],[[-62.937,17.496,-62.962,17.475,-62.983,17.477,-62.997,17.497,-63,17.53,-62.979,17.521,-62.972,17.516,-62.965,17.509,-62.937,17.496]],[[-63.233,17.623,-63.242,17.62,-63.254,17.629,-63.252,17.645,-63.242,17.652,-63.233,17.647,-63.227,17.634,-63.233,17.623]]]},{"n":"Aruba","c":"AW","r":5,"ml":5,"lx":-69.97,"ly":12.52,"b":[-70.066,12.423,-69.896,12.614],"p":[[[-69.899,12.452,-69.896,12.423,-69.942,12.439,-70.004,12.5,-70.066,12.547,-70.051,12.597,-70.035,12.614,-69.973,12.568,-69.912,12.48,-69.899,12.452]]]},{"n":"Curaçao","c":"CW","r":5,"ml":5,"lx":-68.92,"ly":12.15,"b":[-69.159,12.045,-68.751,12.38],"p":[[[-68.751,12.06,-68.803,12.045,-68.995,12.142,-69.154,12.298,-69.159,12.38,-69.118,12.373,-69.077,12.342,-69.013,12.231,-68.827,12.159,-68.751,12.06]]]},{"n":"Nepal","c":"NP","r":3,"ml":3,"lx":83.64,"ly":28.3,"b":[80.052,26.36,88.162,30.388],"p":[[[88.11,27.871,88.15,27.843,88.154,27.799,88.147,27.749,88.106,27.642,88.068,27.567,88.024,27.409,87.984,27.134,87.993,27.086,88.111,26.928,88.157,26.807,88.162,26.725,88.112,26.586,88.055,26.43,88.027,26.395,87.995,26.382,87.849,26.437,87.749,26.429,87.633,26.399,87.513,26.405,87.414,26.423,87.287,26.36,87.167,26.394,87.09,26.433,87.038,26.542,87.016,26.555,86.763,26.442,86.701,26.435,86.544,26.496,86.414,26.556,86.366,26.574,86.242,26.598,86.129,26.612,86.007,26.649,85.856,26.6,85.795,26.604,85.737,26.64,85.707,26.713,85.7,26.782,85.648,26.829,85.568,26.84,85.456,26.797,85.293,26.741,85.24,26.75,85.192,26.767,85.174,26.782,85.152,26.847,85.125,26.861,85.087,26.863,85.02,26.879,84.937,26.927,84.685,27.041,84.654,27.092,84.655,27.204,84.641,27.25,84.61,27.299,84.481,27.348,84.23,27.428,84.091,27.491,84.025,27.462,83.897,27.435,83.829,27.378,83.747,27.396,83.552,27.456,83.447,27.465,83.384,27.445,83.369,27.41,83.29,27.371,83.214,27.402,83.064,27.445,82.933,27.468,82.733,27.519,82.711,27.597,82.677,27.673,82.63,27.687,82.451,27.672,82.288,27.757,82.112,27.865,82.037,27.901,81.988,27.914,81.945,27.899,81.897,27.874,81.853,27.867,81.757,27.914,81.636,27.98,81.486,28.062,81.311,28.176,81.239,28.241,81.206,28.289,81.169,28.335,81.017,28.41,80.896,28.469,80.751,28.54,80.726,28.554,80.671,28.596,80.587,28.65,80.518,28.665,80.496,28.636,80.479,28.605,80.419,28.612,80.325,28.666,80.227,28.723,80.15,28.776,80.071,28.83,80.052,28.87,80.085,28.994,80.13,29.1,80.17,29.124,80.233,29.195,80.256,29.318,80.255,29.423,80.317,29.572,80.402,29.73,80.549,29.9,80.613,29.956,80.684,29.994,80.82,30.119,80.848,30.14,80.908,30.172,80.966,30.18,81.01,30.165,81.056,30.099,81.11,30.037,81.177,30.04,81.255,30.093,81.417,30.338,81.642,30.388,81.855,30.362,82.043,30.327,82.099,30.245,82.135,30.159,82.159,30.115,82.221,30.064,82.487,29.942,82.641,29.831,82.854,29.683,83.014,29.618,83.155,29.613,83.235,29.555,83.355,29.439,83.457,29.306,83.583,29.184,83.671,29.188,83.79,29.227,83.936,29.279,84.022,29.254,84.101,29.22,84.128,29.156,84.176,29.036,84.229,28.912,84.312,28.868,84.411,28.804,84.465,28.753,84.651,28.66,84.677,28.622,84.714,28.596,84.759,28.579,84.797,28.56,84.855,28.554,85.069,28.61,85.126,28.603,85.159,28.592,85.16,28.572,85.121,28.484,85.089,28.372,85.122,28.316,85.212,28.293,85.411,28.276,85.678,28.277,85.759,28.221,85.84,28.135,85.922,27.99,85.954,27.928,85.995,27.91,86.064,27.935,86.075,27.995,86.079,28.084,86.137,28.114,86.174,28.092,86.218,28.022,86.329,27.96,86.409,27.929,86.485,27.94,86.517,27.964,86.554,28.085,86.614,28.103,86.691,28.095,86.72,28.071,86.75,28.022,86.842,27.999,86.934,27.968,87.02,27.929,87.141,27.838,87.291,27.822,87.464,27.824,87.555,27.822,87.623,27.815,87.683,27.821,87.861,27.886,87.933,27.891,88.023,27.883,88.11,27.871]]]},{"n":"Nauru","c":"NR","r":6,"ml":5,"lx":166.93,"ly":-0.52,"b":[166.907,-0.551,166.958,-0.489],"p":[[[166.958,-0.517,166.939,-0.551,166.916,-0.546,166.907,-0.524,166.914,-0.499,166.939,-0.489,166.956,-0.497,166.958,-0.517]]]},{"n":"Namibia","c":"NA","r":3,"ml":3,"lx":17.11,"ly":-20.58,"b":[11.722,-28.939,25.259,-16.968],"p":[[[23.381,-17.641,23.595,-17.599,23.799,-17.56,24.037,-17.521,24.227,-17.49,24.275,-17.481,24.733,-17.518,24.932,-17.543,25.002,-17.569,25.092,-17.634,25.259,-17.794,25.216,-17.788,24.909,-17.821,24.792,-17.865,24.531,-18.053,24.475,-18.029,24.412,-17.989,24.359,-17.978,24.244,-18.023,24.129,-18.078,24.003,-18.154,23.898,-18.229,23.864,-18.27,23.7,-18.424,23.647,-18.449,23.6,-18.46,23.581,-18.453,23.56,-18.386,23.46,-18.231,23.299,-18.027,23.252,-18.008,23.219,-18,23.1,-18.01,22.753,-18.067,22.46,-18.116,22.011,-18.199,21.53,-18.266,21.233,-18.307,20.974,-18.319,20.974,-18.521,20.975,-18.929,20.976,-19.336,20.976,-19.744,20.977,-20.152,20.977,-20.56,20.978,-20.968,20.979,-21.376,20.979,-21.784,20.979,-21.962,20.971,-22,20.823,-22,20.488,-22,20.205,-22,19.977,-22,19.978,-22.243,19.978,-22.529,19.978,-22.816,19.979,-23.103,19.979,-23.389,19.979,-23.676,19.98,-23.962,19.98,-24.249,19.98,-24.536,19.98,-24.752,19.98,-24.777,19.98,-25.197,19.98,-25.642,19.98,-26.086,19.98,-26.531,19.98,-26.976,19.98,-27.421,19.98,-27.866,19.98,-28.31,19.98,-28.451,19.878,-28.449,19.671,-28.504,19.54,-28.575,19.483,-28.662,19.407,-28.714,19.313,-28.733,19.271,-28.778,19.282,-28.848,19.246,-28.902,19.162,-28.939,19.026,-28.928,18.839,-28.869,18.6,-28.855,18.311,-28.886,18.103,-28.872,17.976,-28.811,17.842,-28.777,17.699,-28.768,17.617,-28.743,17.448,-28.698,17.416,-28.621,17.396,-28.563,17.348,-28.501,17.343,-28.452,17.38,-28.414,17.386,-28.353,17.359,-28.269,17.312,-28.229,17.246,-28.231,17.205,-28.199,17.188,-28.133,17.149,-28.082,17.056,-28.031,16.933,-28.07,16.875,-28.128,16.841,-28.219,16.81,-28.265,16.795,-28.341,16.788,-28.395,16.756,-28.452,16.723,-28.475,16.689,-28.465,16.626,-28.488,16.487,-28.573,16.448,-28.618,16.335,-28.537,16.007,-28.232,15.891,-28.153,15.719,-27.966,15.342,-27.387,15.288,-27.275,15.216,-26.995,15.133,-26.788,15.124,-26.668,15.163,-26.6,15.139,-26.508,15.097,-26.426,14.968,-26.318,14.931,-25.958,14.845,-25.726,14.864,-25.534,14.823,-25.359,14.819,-25.246,14.837,-25.033,14.768,-24.788,14.628,-24.548,14.502,-24.202,14.483,-24.05,14.497,-23.643,14.472,-23.477,14.474,-23.281,14.424,-23.079,14.403,-22.968,14.438,-22.881,14.459,-22.908,14.496,-22.921,14.52,-22.805,14.526,-22.703,14.463,-22.449,14.322,-22.19,13.973,-21.768,13.888,-21.607,13.839,-21.473,13.451,-20.917,13.284,-20.524,13.168,-20.185,13.042,-20.028,12.458,-18.927,12.329,-18.751,12.096,-18.541,12.041,-18.471,11.951,-18.271,11.776,-18.002,11.733,-17.751,11.722,-17.467,11.743,-17.249,11.903,-17.227,12.014,-17.169,12.114,-17.165,12.213,-17.21,12.318,-17.213,12.359,-17.206,12.548,-17.213,12.657,-17.161,12.785,-17.108,12.859,-17.063,12.963,-17.015,13.101,-16.968,13.179,-16.972,13.276,-16.99,13.404,-17.008,13.476,-17.04,13.562,-17.141,13.694,-17.233,13.792,-17.288,13.904,-17.361,13.938,-17.389,13.987,-17.404,14.017,-17.409,14.226,-17.398,14.415,-17.388,14.618,-17.388,15.001,-17.389,15.383,-17.389,15.766,-17.39,16.148,-17.39,16.531,-17.391,16.914,-17.391,17.296,-17.392,17.679,-17.393,17.835,-17.393,18.109,-17.396,18.396,-17.399,18.428,-17.405,18.46,-17.425,18.487,-17.443,18.588,-17.57,18.718,-17.703,18.826,-17.766,18.955,-17.804,19.076,-17.818,19.189,-17.808,19.377,-17.825,19.639,-17.869,19.912,-17.881,20.194,-17.864,20.393,-17.887,20.508,-17.953,20.625,-17.997,20.746,-18.02,20.908,-18.006,21.113,-17.956,21.288,-17.963,21.369,-18,21.417,-18.001,21.718,-17.948,21.961,-17.905,22.324,-17.837,22.624,-17.782,23.068,-17.699,23.381,-17.641]]]},{"n":"Mozambique","c":"MZ","r":3,"ml":3,"lx":37.84,"ly":-13.94,"b":[30.222,-26.862,40.845,-10.464],"p":[[[31.288,-22.402,31.429,-22.299,31.571,-22.154,31.738,-21.983,31.886,-21.832,32.016,-21.698,32.195,-21.515,32.371,-21.335,32.412,-21.312,32.43,-21.297,32.354,-21.137,32.476,-20.95,32.483,-20.829,32.478,-20.713,32.492,-20.66,32.529,-20.613,32.673,-20.516,32.781,-20.362,32.87,-20.217,32.993,-19.985,33.005,-19.93,33.007,-19.874,32.973,-19.795,32.89,-19.668,32.831,-19.558,32.778,-19.389,32.831,-19.241,32.85,-19.152,32.85,-19.104,32.826,-19.059,32.766,-19.024,32.717,-19.002,32.7,-18.941,32.699,-18.868,32.722,-18.828,32.854,-18.764,32.885,-18.729,32.9,-18.689,32.902,-18.633,32.942,-18.493,32.993,-18.36,32.996,-18.313,32.979,-18.271,32.965,-18.196,32.956,-18.083,32.955,-17.765,32.981,-17.437,32.969,-17.252,32.884,-17.038,32.876,-16.884,32.938,-16.776,32.948,-16.712,32.903,-16.704,32.81,-16.698,32.742,-16.678,32.636,-16.589,32.452,-16.516,32.243,-16.449,31.94,-16.429,31.688,-16.214,31.49,-16.18,31.426,-16.152,31.236,-16.024,30.939,-16.012,30.63,-15.999,30.438,-15.995,30.409,-15.978,30.398,-15.801,30.396,-15.643,30.38,-15.506,30.351,-15.35,30.306,-15.289,30.252,-15.183,30.225,-15.067,30.222,-15.011,30.232,-14.99,30.446,-14.908,30.538,-14.867,30.673,-14.819,30.915,-14.753,31.131,-14.695,31.329,-14.638,31.538,-14.577,31.623,-14.537,31.729,-14.496,31.982,-14.414,32.054,-14.387,32.2,-14.341,32.273,-14.323,32.553,-14.23,32.875,-14.122,32.987,-14.085,33.202,-14.013,33.244,-14.043,33.39,-14.289,33.505,-14.434,33.636,-14.568,33.658,-14.562,33.696,-14.53,33.761,-14.517,33.97,-14.487,34.049,-14.485,34.102,-14.449,34.209,-14.424,34.333,-14.409,34.375,-14.425,34.505,-14.598,34.524,-14.731,34.551,-14.922,34.558,-15.016,34.555,-15.141,34.541,-15.297,34.435,-15.477,34.415,-15.567,34.358,-15.705,34.283,-15.773,34.246,-15.829,34.248,-15.887,34.288,-15.936,34.376,-16.024,34.403,-16.08,34.395,-16.131,34.396,-16.199,34.416,-16.247,34.441,-16.274,34.528,-16.319,34.613,-16.432,34.759,-16.567,34.933,-16.76,35.015,-16.82,35.08,-16.834,35.112,-16.899,35.094,-16.974,35.044,-17.017,35.065,-17.079,35.093,-17.111,35.125,-17.127,35.201,-17.131,35.273,-17.118,35.29,-17.097,35.281,-16.808,35.23,-16.639,35.178,-16.573,35.167,-16.56,35.185,-16.505,35.243,-16.375,35.292,-16.247,35.322,-16.193,35.358,-16.161,35.599,-16.126,35.709,-16.096,35.755,-16.058,35.791,-15.959,35.82,-15.68,35.83,-15.419,35.805,-15.266,35.84,-15.035,35.893,-14.892,35.867,-14.864,35.847,-14.671,35.69,-14.466,35.488,-14.201,35.376,-14.059,35.247,-13.897,35.014,-13.643,34.907,-13.552,34.85,-13.516,34.662,-13.487,34.612,-13.438,34.564,-13.36,34.546,-13.216,34.543,-13.109,34.521,-12.926,34.483,-12.667,34.466,-12.591,34.412,-12.396,34.361,-12.211,34.358,-12.165,34.376,-12.12,34.463,-11.984,34.525,-11.887,34.554,-11.834,34.606,-11.69,34.619,-11.62,34.66,-11.589,34.827,-11.576,34.959,-11.578,35.183,-11.575,35.418,-11.583,35.451,-11.59,35.504,-11.605,35.564,-11.602,35.631,-11.582,35.705,-11.532,35.785,-11.453,35.911,-11.455,36.082,-11.537,36.175,-11.609,36.191,-11.671,36.306,-11.706,36.519,-11.716,36.674,-11.684,36.771,-11.61,36.873,-11.571,36.979,-11.567,37.059,-11.592,37.114,-11.647,37.218,-11.687,37.373,-11.71,37.542,-11.675,37.725,-11.581,37.829,-11.482,37.855,-11.379,37.885,-11.317,37.92,-11.295,38.017,-11.282,38.177,-11.279,38.315,-11.311,38.492,-11.413,38.603,-11.345,38.795,-11.229,38.988,-11.167,39.171,-11.167,39.322,-11.123,39.439,-11.035,39.563,-10.979,39.694,-10.955,39.817,-10.912,39.989,-10.821,40.166,-10.687,40.347,-10.552,40.464,-10.464,40.517,-10.567,40.612,-10.662,40.555,-10.716,40.487,-10.765,40.597,-10.831,40.516,-10.93,40.506,-10.998,40.527,-11.025,40.545,-11.066,40.491,-11.179,40.421,-11.266,40.403,-11.332,40.465,-11.449,40.433,-11.657,40.494,-11.844,40.51,-11.94,40.532,-12.005,40.501,-12.119,40.509,-12.313,40.523,-12.393,40.487,-12.492,40.548,-12.527,40.581,-12.636,40.572,-12.758,40.553,-12.825,40.448,-12.905,40.435,-12.936,40.437,-12.983,40.569,-12.985,40.573,-13.058,40.564,-13.115,40.57,-13.223,40.552,-13.294,40.583,-13.374,40.545,-13.463,40.558,-13.531,40.56,-13.62,40.591,-13.845,40.596,-14.123,40.603,-14.167,40.65,-14.199,40.716,-14.214,40.713,-14.291,40.64,-14.39,40.636,-14.452,40.646,-14.539,40.727,-14.421,40.775,-14.421,40.818,-14.468,40.812,-14.536,40.827,-14.569,40.821,-14.635,40.845,-14.719,40.835,-14.792,40.776,-14.842,40.701,-14.93,40.687,-15.012,40.694,-15.065,40.642,-15.082,40.618,-15.116,40.653,-15.193,40.651,-15.261,40.559,-15.473,40.314,-15.764,40.208,-15.867,40.109,-15.979,40.109,-16.025,40.099,-16.065,39.984,-16.225,39.86,-16.252,39.791,-16.295,39.845,-16.436,39.765,-16.468,39.625,-16.579,39.242,-16.793,39.182,-16.842,39.084,-16.973,38.956,-17.005,38.885,-17.042,38.758,-17.055,38.713,-17.046,38.67,-17.05,38.633,-17.078,38.381,-17.17,38.145,-17.243,38.087,-17.276,38.048,-17.321,37.839,-17.393,37.512,-17.571,37.245,-17.74,37.051,-17.909,37,-17.935,36.939,-17.993,36.919,-18.08,36.9,-18.129,36.756,-18.307,36.54,-18.518,36.498,-18.576,36.412,-18.693,36.404,-18.77,36.327,-18.793,36.263,-18.72,36.236,-18.861,36.183,-18.871,36.125,-18.842,35.98,-18.912,35.854,-18.993,35.651,-19.164,35.365,-19.494,34.948,-19.813,34.891,-19.822,34.852,-19.821,34.721,-19.71,34.649,-19.701,34.713,-19.767,34.756,-19.822,34.745,-19.929,34.75,-20.091,34.698,-20.404,34.705,-20.473,34.765,-20.562,34.877,-20.671,34.982,-20.806,35.118,-21.195,35.128,-21.395,35.268,-21.651,35.273,-21.762,35.329,-22.037,35.326,-22.26,35.316,-22.397,35.383,-22.455,35.408,-22.403,35.401,-22.316,35.419,-22.178,35.456,-22.116,35.494,-22.125,35.505,-22.19,35.53,-22.248,35.54,-22.303,35.542,-22.377,35.49,-22.658,35.506,-22.772,35.575,-22.963,35.494,-23.185,35.377,-23.708,35.37,-23.798,35.399,-23.838,35.462,-23.851,35.485,-23.784,35.522,-23.785,35.542,-23.824,35.49,-24.066,35.438,-24.171,35.255,-24.43,35.156,-24.541,34.992,-24.651,34.607,-24.821,33.836,-25.068,33.53,-25.189,33.347,-25.261,32.961,-25.49,32.792,-25.644,32.723,-25.821,32.656,-25.902,32.59,-26.004,32.647,-26.092,32.704,-26.158,32.77,-26.203,32.804,-26.241,32.849,-26.268,32.894,-26.13,32.916,-26.087,32.955,-26.084,32.934,-26.252,32.889,-26.83,32.886,-26.849,32.777,-26.851,32.589,-26.856,32.478,-26.858,32.354,-26.862,32.2,-26.833,32.113,-26.839,32.106,-26.52,32.078,-26.45,32.048,-26.347,32.041,-26.281,32.06,-26.215,32.069,-26.11,32.061,-26.018,31.968,-25.972,31.948,-25.958,31.928,-25.885,31.92,-25.774,31.985,-25.632,31.979,-25.359,31.987,-25.263,31.986,-25.074,31.984,-24.844,31.983,-24.638,31.986,-24.461,31.967,-24.376,31.951,-24.33,31.908,-24.236,31.858,-24.04,31.8,-23.892,31.724,-23.795,31.7,-23.743,31.676,-23.674,31.604,-23.553,31.546,-23.482,31.53,-23.426,31.532,-23.279,31.467,-23.017,31.419,-22.825,31.348,-22.618,31.3,-22.479,31.293,-22.455,31.288,-22.402],[34.642,-12.014,34.624,-11.985,34.591,-11.971,34.554,-11.982,34.542,-12.019,34.58,-12.066,34.622,-12.067,34.642,-12.014],[34.719,-12.111,34.746,-12.088,34.756,-12.059,34.756,-12.031,34.739,-12.013,34.715,-12.003,34.68,-12.009,34.667,-12.048,34.662,-12.101,34.684,-12.119,34.719,-12.111]]]},{"n":"Morocco","c":"MA","r":3,"ml":2.7,"lx":-7.19,"ly":31.65,"b":[-17.003,21.421,-1.066,35.93],"p":[[[-2.22,35.104,-2.191,35.03,-2.132,34.971,-1.921,34.836,-1.796,34.752,-1.792,34.723,-1.832,34.655,-1.85,34.607,-1.817,34.557,-1.739,34.496,-1.733,34.467,-1.752,34.433,-1.792,34.368,-1.707,34.176,-1.693,33.99,-1.715,33.858,-1.714,33.782,-1.703,33.717,-1.631,33.567,-1.679,33.319,-1.625,33.183,-1.551,33.074,-1.51,32.878,-1.45,32.785,-1.352,32.703,-1.296,32.676,-1.188,32.608,-1.111,32.552,-1.066,32.468,-1.163,32.399,-1.24,32.338,-1.262,32.271,-1.226,32.165,-1.226,32.107,-1.275,32.089,-1.477,32.095,-1.635,32.1,-1.817,32.105,-2.073,32.115,-2.231,32.121,-2.448,32.13,-2.523,32.126,-2.723,32.096,-2.863,32.075,-2.887,32.069,-2.931,32.043,-2.961,31.964,-2.988,31.874,-3.017,31.834,-3.44,31.705,-3.605,31.687,-3.7,31.7,-3.768,31.69,-3.827,31.662,-3.847,31.62,-3.85,31.566,-3.837,31.512,-3.796,31.437,-3.789,31.362,-3.815,31.309,-3.821,31.255,-3.833,31.198,-3.812,31.167,-3.771,31.162,-3.73,31.135,-3.673,31.111,-3.625,31.066,-3.627,31.001,-3.667,30.964,-3.702,30.944,-3.86,30.927,-3.985,30.914,-4.149,30.81,-4.323,30.699,-4.529,30.626,-4.62,30.605,-4.779,30.552,-4.968,30.465,-5.062,30.326,-5.18,30.166,-5.294,30.059,-5.449,29.957,-5.593,29.918,-5.775,29.869,-6.004,29.831,-6.167,29.819,-6.215,29.811,-6.358,29.808,-6.428,29.816,-6.48,29.82,-6.501,29.809,-6.508,29.784,-6.511,29.726,-6.521,29.66,-6.566,29.604,-6.598,29.579,-6.635,29.569,-6.755,29.584,-6.856,29.602,-7.095,29.625,-7.142,29.62,-7.16,29.613,-7.235,29.575,-7.35,29.495,-7.428,29.425,-7.486,29.392,-7.625,29.375,-7.685,29.35,-7.944,29.175,-7.999,29.132,-8.265,28.981,-8.34,28.93,-8.399,28.88,-8.558,28.768,-8.66,28.719,-8.678,28.689,-8.683,28.621,-8.683,28.469,-8.683,28.324,-8.683,28.112,-8.683,27.9,-8.683,27.656,-8.818,27.656,-8.814,27.614,-8.785,27.531,-8.774,27.461,-8.789,27.417,-8.803,27.361,-8.797,27.308,-8.774,27.251,-8.754,27.191,-8.754,27.151,-8.795,27.121,-8.889,27.104,-9.002,27.09,-9.084,27.09,-9.208,27.1,-9.286,27.098,-9.353,27.098,-9.413,27.088,-9.487,27.05,-9.57,26.991,-9.673,26.911,-9.735,26.861,-9.818,26.85,-9.9,26.85,-9.981,26.89,-10.033,26.911,-10.067,26.909,-10.123,26.88,-10.189,26.861,-10.251,26.861,-10.355,26.901,-10.479,26.961,-10.551,26.991,-10.654,27.001,-10.758,27.02,-10.83,27.01,-10.923,27.01,-11.047,26.97,-11.15,26.941,-11.264,26.911,-11.393,26.883,-11.361,26.794,-11.317,26.745,-11.317,26.684,-11.338,26.633,-11.4,26.584,-11.471,26.52,-11.512,26.47,-11.553,26.401,-11.584,26.361,-11.637,26.296,-11.685,26.213,-11.699,26.163,-11.718,26.104,-11.755,26.087,-11.881,26.071,-11.961,26.05,-12.031,26.031,-12.057,25.996,-12.061,25.991,-12.081,25.921,-12.081,25.871,-12.101,25.831,-12.131,25.731,-12.171,25.64,-12.201,25.52,-12.231,25.421,-12.271,25.26,-12.311,25.111,-12.361,24.97,-12.401,24.88,-12.431,24.831,-12.501,24.77,-12.561,24.731,-12.631,24.68,-12.711,24.63,-12.821,24.571,-12.911,24.52,-12.948,24.497,-12.991,24.47,-13.061,24.401,-13.121,24.3,-13.161,24.22,-13.231,24.09,-13.281,24.02,-13.311,23.981,-13.391,23.941,-13.481,23.911,-13.581,23.871,-13.661,23.831,-13.771,23.791,-13.841,23.751,-13.891,23.691,-13.931,23.621,-13.981,23.52,-14.021,23.411,-14.041,23.34,-14.101,23.1,-14.121,22.961,-14.141,22.871,-14.171,22.76,-14.191,22.59,-14.191,22.451,-14.211,22.371,-14.221,22.31,-14.271,22.241,-14.311,22.191,-14.381,22.121,-14.441,22.081,-14.461,22.041,-14.521,21.991,-14.581,21.911,-14.631,21.861,-14.621,21.821,-14.611,21.751,-14.641,21.68,-14.671,21.6,-14.751,21.501,-14.841,21.451,-14.971,21.441,-15.151,21.441,-15.291,21.451,-15.461,21.451,-15.611,21.47,-15.751,21.491,-15.921,21.501,-16.041,21.501,-16.191,21.481,-16.581,21.481,-16.731,21.47,-16.951,21.43,-17.003,21.421,-16.931,21.9,-16.793,22.16,-16.684,22.274,-16.514,22.333,-16.359,22.595,-16.304,22.835,-16.202,22.945,-16.17,23.032,-16.21,23.098,-16.114,23.228,-15.997,23.425,-15.943,23.553,-15.806,23.75,-15.789,23.793,-15.802,23.842,-15.855,23.8,-15.913,23.728,-15.981,23.67,-15.953,23.741,-15.899,23.844,-15.778,23.953,-15.586,24.073,-15.189,24.479,-15.039,24.549,-14.904,24.72,-14.856,24.872,-14.843,25.22,-14.795,25.404,-14.707,25.548,-14.602,25.809,-14.523,25.925,-14.471,26.163,-14.414,26.254,-14.312,26.297,-14.168,26.415,-13.952,26.489,-13.696,26.643,-13.576,26.735,-13.496,26.873,-13.41,27.147,-13.256,27.435,-13.177,27.652,-13.176,27.656,-13.041,27.77,-12.949,27.914,-12.794,27.978,-12.469,28.009,-11.986,28.129,-11.553,28.31,-11.43,28.382,-11.299,28.526,-11.081,28.714,-10.674,28.939,-10.486,29.065,-10.201,29.38,-10.01,29.641,-9.853,29.809,-9.743,29.958,-9.667,30.109,-9.624,30.353,-9.653,30.448,-9.773,30.603,-9.854,30.645,-9.875,30.718,-9.832,30.847,-9.833,31.07,-9.809,31.425,-9.675,31.711,-9.347,32.086,-9.287,32.241,-9.249,32.486,-9.246,32.572,-8.836,32.92,-8.596,33.187,-8.513,33.252,-8.301,33.374,-7.562,33.64,-7.145,33.83,-6.901,33.969,-6.756,34.133,-6.353,34.776,-5.958,35.681,-5.925,35.786,-5.748,35.816,-5.623,35.829,-5.522,35.862,-5.397,35.93,-5.278,35.903,-5.338,35.857,-5.338,35.745,-5.253,35.615,-5.105,35.468,-4.837,35.281,-4.628,35.206,-4.33,35.161,-3.982,35.243,-3.788,35.245,-3.693,35.28,-3.591,35.228,-3.395,35.212,-3.206,35.239,-3.063,35.317,-2.972,35.407,-2.958,35.363,-2.954,35.315,-2.926,35.287,-2.87,35.173,-2.84,35.128,-2.731,35.135,-2.637,35.113,-2.424,35.123,-2.22,35.104]]]},{"n":"Western Sahara","c":"EH","r":7,"ml":6,"lx":-12.63,"ly":23.97,"b":[-17.099,20.806,-8.682,27.656],"p":[[[-8.818,27.656,-8.683,27.656,-8.683,27.49,-8.683,27.286,-8.683,27.119,-8.683,26.921,-8.683,26.723,-8.682,26.498,-8.682,26.273,-8.682,26.109,-8.682,25.996,-8.886,25.996,-9.072,25.996,-9.258,25.996,-9.445,25.996,-9.631,25.996,-9.817,25.995,-10.004,25.995,-10.19,25.995,-10.376,25.995,-10.562,25.995,-10.749,25.995,-10.935,25.995,-11.121,25.995,-11.308,25.995,-11.494,25.995,-11.68,25.995,-11.867,25.995,-12.016,25.995,-12.016,25.876,-12.016,25.74,-12.016,25.604,-12.016,25.468,-12.016,25.332,-12.016,25.196,-12.016,25.059,-12.016,24.923,-12.016,24.787,-12.016,24.651,-12.016,24.515,-12.016,24.379,-12.016,24.242,-12.016,24.106,-12.016,23.97,-12.016,23.834,-12.016,23.698,-12.016,23.576,-12.023,23.468,-12.083,23.435,-12.226,23.377,-12.373,23.318,-12.559,23.291,-12.62,23.271,-12.74,23.193,-12.896,23.09,-13.031,23,-13.121,22.884,-13.153,22.821,-13.167,22.753,-13.156,22.689,-13.107,22.561,-13.094,22.496,-13.087,22.383,-13.078,22.26,-13.07,22.128,-13.061,21.996,-13.051,21.855,-13.042,21.714,-13.032,21.572,-13.025,21.467,-13.016,21.334,-13.167,21.334,-13.397,21.334,-13.626,21.333,-13.855,21.333,-14.085,21.333,-14.314,21.332,-14.543,21.332,-14.773,21.332,-15.002,21.332,-15.231,21.331,-15.461,21.331,-15.69,21.331,-15.919,21.33,-16.148,21.33,-16.378,21.33,-16.607,21.33,-16.836,21.329,-16.965,21.329,-17.006,21.142,-17.042,21.008,-17.064,20.899,-17.048,20.806,-17.099,20.857,-17.01,21.377,-17.003,21.421,-16.951,21.43,-16.731,21.47,-16.581,21.481,-16.191,21.481,-16.041,21.501,-15.921,21.501,-15.751,21.491,-15.611,21.47,-15.461,21.451,-15.291,21.451,-15.151,21.441,-14.971,21.441,-14.841,21.451,-14.751,21.501,-14.671,21.6,-14.641,21.68,-14.611,21.751,-14.621,21.821,-14.631,21.861,-14.581,21.911,-14.521,21.991,-14.461,22.041,-14.441,22.081,-14.381,22.121,-14.311,22.191,-14.271,22.241,-14.221,22.31,-14.211,22.371,-14.191,22.451,-14.191,22.59,-14.171,22.76,-14.141,22.871,-14.121,22.961,-14.101,23.1,-14.041,23.34,-14.021,23.411,-13.981,23.52,-13.931,23.621,-13.891,23.691,-13.841,23.751,-13.771,23.791,-13.661,23.831,-13.581,23.871,-13.481,23.911,-13.391,23.941,-13.311,23.981,-13.281,24.02,-13.231,24.09,-13.161,24.22,-13.121,24.3,-13.061,24.401,-12.991,24.47,-12.948,24.497,-12.911,24.52,-12.821,24.571,-12.711,24.63,-12.631,24.68,-12.561,24.731,-12.501,24.77,-12.431,24.831,-12.401,24.88,-12.361,24.97,-12.311,25.111,-12.271,25.26,-12.231,25.421,-12.201,25.52,-12.171,25.64,-12.131,25.731,-12.101,25.831,-12.081,25.871,-12.081,25.921,-12.061,25.991,-12.057,25.996,-12.031,26.031,-11.961,26.05,-11.881,26.071,-11.755,26.087,-11.718,26.104,-11.699,26.163,-11.685,26.213,-11.637,26.296,-11.584,26.361,-11.553,26.401,-11.512,26.47,-11.471,26.52,-11.4,26.584,-11.338,26.633,-11.317,26.684,-11.317,26.745,-11.361,26.794,-11.393,26.883,-11.264,26.911,-11.15,26.941,-11.047,26.97,-10.923,27.01,-10.83,27.01,-10.758,27.02,-10.654,27.001,-10.551,26.991,-10.479,26.961,-10.355,26.901,-10.251,26.861,-10.189,26.861,-10.123,26.88,-10.067,26.909,-10.033,26.911,-9.981,26.89,-9.9,26.85,-9.818,26.85,-9.735,26.861,-9.673,26.911,-9.57,26.991,-9.487,27.05,-9.413,27.088,-9.353,27.098,-9.286,27.098,-9.208,27.1,-9.084,27.09,-9.002,27.09,-8.889,27.104,-8.795,27.121,-8.754,27.151,-8.754,27.191,-8.774,27.251,-8.797,27.308,-8.803,27.361,-8.789,27.417,-8.774,27.461,-8.785,27.531,-8.814,27.614,-8.818,27.656]]]},{"n":"Montenegro","c":"ME","r":6,"ml":5,"lx":19.14,"ly":42.8,"b":[18.436,41.869,20.348,43.542],"p":[[[19.194,43.533,19.192,43.521,19.196,43.485,19.219,43.45,19.298,43.414,19.415,43.343,19.552,43.212,19.614,43.173,19.671,43.164,19.781,43.11,19.858,43.097,19.944,43.082,20.168,42.969,20.268,42.935,20.34,42.893,20.348,42.853,20.344,42.828,20.215,42.799,20.193,42.755,20.13,42.76,20.054,42.76,20.029,42.732,20.066,42.686,20.089,42.632,20.07,42.557,20.064,42.547,20.046,42.55,19.939,42.507,19.86,42.486,19.788,42.476,19.754,42.497,19.738,42.525,19.741,42.607,19.728,42.635,19.703,42.648,19.654,42.629,19.597,42.565,19.545,42.492,19.465,42.415,19.4,42.342,19.329,42.249,19.281,42.173,19.331,42.129,19.361,42.069,19.352,42.024,19.361,41.998,19.346,41.919,19.342,41.869,19.186,41.949,19.122,42.06,18.894,42.249,18.633,42.378,18.619,42.398,18.633,42.423,18.646,42.443,18.592,42.444,18.554,42.429,18.517,42.433,18.477,42.481,18.438,42.523,18.436,42.56,18.454,42.565,18.48,42.579,18.535,42.62,18.546,42.642,18.543,42.674,18.466,42.777,18.455,42.844,18.444,42.968,18.46,42.998,18.488,43.012,18.624,43.028,18.622,43.125,18.63,43.154,18.657,43.194,18.674,43.231,18.749,43.284,18.851,43.346,18.896,43.348,18.935,43.339,18.979,43.285,19.027,43.292,19.037,43.357,18.974,43.442,18.94,43.497,18.951,43.527,18.974,43.542,19.028,43.533,19.08,43.518,19.113,43.528,19.164,43.535,19.194,43.533]]]},{"n":"Mongolia","c":"MN","r":3,"ml":3,"lx":104.15,"ly":46,"b":[87.743,41.596,119.898,52.117],"p":[[[87.814,49.162,87.818,49.162,87.935,49.165,87.988,49.187,88.029,49.22,88.116,49.256,88.134,49.298,88.136,49.381,88.193,49.452,88.338,49.473,88.393,49.483,88.452,49.473,88.544,49.483,88.633,49.486,88.683,49.465,88.748,49.446,88.832,49.448,88.86,49.482,88.864,49.528,88.9,49.54,88.945,49.508,88.971,49.484,89.008,49.473,89.109,49.501,89.18,49.532,89.203,49.596,89.244,49.627,89.299,49.611,89.396,49.612,89.475,49.661,89.579,49.7,89.654,49.717,89.67,49.75,89.634,49.823,89.644,49.903,89.744,49.948,89.878,49.954,89.977,49.984,90.005,50.069,90.054,50.094,90.104,50.103,90.225,50.117,90.311,50.151,90.365,50.167,90.517,50.213,90.655,50.222,90.714,50.259,90.761,50.306,90.838,50.324,90.917,50.364,91.022,50.415,91.063,50.423,91.234,50.452,91.301,50.463,91.341,50.47,91.415,50.468,91.446,50.522,91.522,50.562,91.597,50.576,91.634,50.615,91.706,50.666,91.804,50.694,91.957,50.698,92.104,50.692,92.192,50.701,92.265,50.775,92.279,50.812,92.296,50.85,92.355,50.864,92.426,50.803,92.486,50.765,92.579,50.725,92.627,50.688,92.681,50.683,92.739,50.711,92.779,50.779,92.856,50.789,92.941,50.778,92.964,50.745,92.971,50.713,93.01,50.655,93.103,50.604,93.223,50.607,93.271,50.616,93.387,50.608,93.501,50.597,93.626,50.586,93.662,50.584,93.795,50.578,93.99,50.569,94.076,50.573,94.251,50.556,94.287,50.511,94.319,50.405,94.347,50.303,94.355,50.222,94.4,50.18,94.458,50.166,94.497,50.133,94.565,50.088,94.615,50.024,94.675,50.028,94.718,50.043,94.811,50.048,94.93,50.044,95.013,50.008,95.044,49.962,95.111,49.935,95.166,49.944,95.329,49.944,95.386,49.941,95.442,49.916,95.523,49.911,95.567,49.944,95.708,49.966,95.789,50.013,95.852,50.013,95.899,49.991,95.936,49.96,95.99,49.974,96.019,49.999,96.066,49.999,96.112,49.982,96.23,49.954,96.315,49.901,96.381,49.896,96.466,49.912,96.506,49.919,96.543,49.893,96.598,49.878,96.64,49.898,96.712,49.912,96.986,49.883,97.049,49.83,97.098,49.805,97.137,49.762,97.209,49.731,97.36,49.741,97.418,49.773,97.541,49.843,97.589,49.911,97.651,49.934,97.721,49.945,97.786,49.945,97.854,49.947,97.937,49.997,98.004,50.014,98.103,50.078,98.122,50.107,98.17,50.181,98.2,50.228,98.25,50.302,98.277,50.423,98.293,50.487,98.279,50.533,98.221,50.557,98.145,50.569,98.079,50.604,98.03,50.645,98.001,50.702,97.962,50.769,97.964,50.818,97.953,50.855,97.92,50.887,97.856,50.943,97.825,50.985,97.836,51.052,97.911,51.165,97.918,51.218,97.927,51.251,97.923,51.28,97.947,51.348,97.989,51.377,98.038,51.45,98.103,51.484,98.185,51.486,98.22,51.506,98.238,51.578,98.277,51.635,98.303,51.674,98.353,51.718,98.641,51.801,98.76,51.905,98.803,51.957,98.849,52.07,98.893,52.117,98.958,52.102,99.034,52.035,99.091,52.035,99.176,51.999,99.407,51.924,99.532,51.9,99.613,51.893,99.719,51.872,99.788,51.828,99.922,51.756,100.035,51.737,100.23,51.73,100.469,51.726,100.536,51.713,100.711,51.662,100.904,51.604,101.085,51.553,101.223,51.513,101.304,51.475,101.381,51.453,101.464,51.471,101.571,51.467,101.821,51.421,101.979,51.382,102.112,51.353,102.156,51.314,102.16,51.261,102.142,51.216,102.152,51.108,102.195,51.051,102.21,50.974,102.226,50.901,102.215,50.829,102.235,50.791,102.277,50.769,102.317,50.718,102.303,50.666,102.286,50.635,102.288,50.585,102.336,50.544,102.407,50.536,102.469,50.526,102.546,50.461,102.683,50.387,102.765,50.367,102.86,50.333,103.039,50.301,103.162,50.291,103.234,50.264,103.304,50.2,103.421,50.187,103.496,50.165,103.633,50.139,103.723,50.154,103.803,50.176,103.856,50.172,103.958,50.157,104.079,50.154,104.18,50.169,104.26,50.214,104.354,50.275,104.466,50.306,104.596,50.317,104.685,50.342,104.977,50.383,105.095,50.39,105.186,50.43,105.267,50.46,105.384,50.474,105.542,50.441,105.693,50.414,105.875,50.405,105.996,50.368,106.083,50.333,106.218,50.305,106.368,50.318,106.574,50.329,106.711,50.313,106.854,50.248,106.941,50.197,107.04,50.086,107.143,50.033,107.233,49.989,107.347,49.987,107.631,49.983,107.787,49.96,107.917,49.948,107.948,49.925,107.935,49.849,107.939,49.741,107.937,49.691,107.965,49.654,108.01,49.647,108.034,49.594,108.098,49.563,108.213,49.525,108.407,49.396,108.522,49.342,108.614,49.323,108.733,49.336,108.92,49.335,109.237,49.335,109.454,49.296,109.529,49.27,109.75,49.239,109.995,49.206,110.2,49.17,110.321,49.216,110.428,49.22,110.53,49.187,110.631,49.138,110.71,49.143,110.828,49.166,111.204,49.304,111.337,49.356,111.429,49.343,111.512,49.361,111.575,49.376,111.736,49.398,111.833,49.404,111.934,49.416,112.08,49.424,112.375,49.515,112.495,49.532,112.697,49.507,112.806,49.524,112.915,49.569,113.056,49.616,113.092,49.693,113.164,49.797,113.319,49.874,113.446,49.942,113.574,50.007,113.732,50.062,113.881,50.101,114.071,50.205,114.222,50.257,114.297,50.274,114.386,50.255,114.554,50.241,114.675,50.246,114.743,50.234,114.88,50.183,115.003,50.139,115.098,50.059,115.275,49.949,115.365,49.912,115.429,49.896,115.588,49.886,115.718,49.881,115.795,49.906,115.926,49.952,116.135,50.011,116.217,50.009,116.351,49.978,116.551,49.92,116.632,49.877,116.683,49.824,116.59,49.685,116.402,49.406,116.243,49.17,116.16,49.037,116.098,48.936,116.034,48.84,116.025,48.782,115.954,48.689,115.821,48.577,115.792,48.456,115.797,48.346,115.786,48.248,115.639,48.186,115.525,48.131,115.558,47.945,115.616,47.875,115.712,47.799,115.812,47.738,115.898,47.687,115.994,47.711,116.075,47.79,116.231,47.858,116.317,47.86,116.378,47.844,116.513,47.84,116.652,47.865,116.761,47.87,116.901,47.853,116.952,47.837,117.07,47.806,117.197,47.74,117.286,47.666,117.351,47.652,117.384,47.676,117.455,47.741,117.555,47.805,117.677,47.908,117.768,47.988,117.84,48,117.979,48,118.042,48.019,118.147,48.029,118.24,48,118.498,47.984,118.568,47.943,118.691,47.822,118.76,47.758,118.88,47.725,118.953,47.703,119.018,47.685,119.082,47.654,119.097,47.616,119.123,47.558,119.162,47.525,119.235,47.493,119.291,47.473,119.309,47.431,119.326,47.41,119.377,47.381,119.527,47.256,119.6,47.222,119.711,47.15,119.757,47.09,119.76,47.027,119.788,46.979,119.863,46.907,119.898,46.858,119.884,46.791,119.896,46.733,119.867,46.672,119.747,46.627,119.707,46.606,119.62,46.604,119.474,46.627,119.332,46.614,119.162,46.639,119.029,46.692,118.957,46.735,118.844,46.76,118.79,46.747,118.723,46.692,118.649,46.702,118.58,46.692,118.404,46.703,118.309,46.717,118.157,46.679,118.071,46.667,117.91,46.619,117.813,46.538,117.741,46.518,117.671,46.522,117.621,46.552,117.547,46.588,117.438,46.586,117.406,46.571,117.392,46.538,117.356,46.437,117.357,46.391,117.333,46.362,117.269,46.352,117.156,46.355,116.979,46.362,116.859,46.388,116.787,46.377,116.689,46.322,116.619,46.313,116.563,46.29,116.517,46.209,116.445,46.159,116.358,46.097,116.265,45.963,116.213,45.887,116.229,45.846,116.241,45.796,116.198,45.739,116.11,45.687,116.04,45.677,115.934,45.626,115.789,45.535,115.681,45.458,115.539,45.44,115.439,45.42,115.217,45.396,115.163,45.39,114.919,45.378,114.739,45.42,114.644,45.413,114.56,45.39,114.517,45.365,114.502,45.316,114.487,45.272,114.419,45.203,114.281,45.111,114.167,45.05,114.08,44.971,114.03,44.943,113.931,44.912,113.877,44.896,113.752,44.826,113.653,44.763,113.587,44.746,113.508,44.762,113.456,44.767,113.301,44.792,113.196,44.795,113.049,44.81,112.707,44.883,112.597,44.918,112.499,45.011,112.411,45.058,112.292,45.063,112.113,45.063,112.033,45.082,111.898,45.064,111.751,44.97,111.681,44.899,111.621,44.827,111.547,44.673,111.515,44.57,111.489,44.512,111.411,44.419,111.402,44.367,111.43,44.322,111.486,44.272,111.52,44.192,111.603,44.107,111.684,44.041,111.837,43.935,111.88,43.879,111.932,43.815,111.943,43.752,111.933,43.711,111.878,43.68,111.771,43.665,111.72,43.621,111.641,43.563,111.547,43.496,111.504,43.493,111.451,43.475,111.187,43.392,111.087,43.369,111.007,43.341,110.913,43.257,110.84,43.194,110.749,43.111,110.709,43.074,110.628,42.991,110.521,42.895,110.462,42.844,110.43,42.814,110.4,42.774,110.289,42.743,110.197,42.71,110.058,42.661,109.859,42.606,109.698,42.554,109.596,42.511,109.443,42.456,109.34,42.438,109.132,42.441,108.875,42.426,108.687,42.416,108.546,42.429,108.334,42.437,108.171,42.447,108.062,42.427,107.806,42.406,107.749,42.401,107.292,42.349,107.091,42.322,106.906,42.309,106.77,42.289,106.693,42.264,106.579,42.227,106.519,42.212,106.317,42.141,105.868,41.994,105.566,41.875,105.517,41.855,105.314,41.771,105.197,41.738,105.115,41.663,105.051,41.616,104.982,41.596,104.86,41.644,104.774,41.641,104.498,41.659,104.498,41.877,104.305,41.846,103.997,41.797,103.711,41.751,103.45,41.856,103.248,41.937,103.073,42.006,102.807,42.052,102.575,42.092,102.157,42.158,101.973,42.216,101.88,42.292,101.714,42.466,101.66,42.5,101.579,42.524,101.495,42.539,101.314,42.538,101.092,42.551,100.773,42.588,100.519,42.617,100.086,42.671,99.984,42.677,99.757,42.629,99.468,42.568,98.947,42.616,98.716,42.639,98.248,42.685,97.719,42.736,97.206,42.79,96.833,42.76,96.625,42.744,96.385,42.72,96.352,42.747,96.342,42.849,96.3,42.929,96.168,43.015,96.08,43.096,95.913,43.206,95.86,43.276,95.842,43.384,95.687,43.664,95.591,43.854,95.567,43.892,95.526,43.954,95.471,43.986,95.356,44.006,95.326,44.039,95.326,44.105,95.344,44.195,95.367,44.262,95.35,44.278,95.05,44.259,94.866,44.303,94.712,44.351,94.494,44.473,94.365,44.519,94.199,44.645,93.958,44.675,93.868,44.724,93.755,44.832,93.656,44.901,93.516,44.944,93.294,44.983,92.916,45.02,92.788,45.036,92.579,45.011,92.424,45.009,92.173,45.035,92.03,45.069,91.853,45.069,91.738,45.069,91.584,45.077,91.51,45.098,91.441,45.125,91.312,45.118,91.222,45.145,91.138,45.194,91.05,45.217,90.954,45.216,90.914,45.194,90.877,45.196,90.853,45.263,90.763,45.371,90.75,45.419,90.694,45.475,90.662,45.525,90.671,45.595,90.71,45.731,90.796,45.854,90.852,45.885,90.887,45.922,90.96,45.985,91.002,46.036,90.997,46.105,90.948,46.177,90.912,46.271,90.918,46.324,90.971,46.388,91.034,46.529,91.029,46.566,91.004,46.596,90.998,46.661,90.986,46.749,90.911,46.883,90.87,46.954,90.799,46.985,90.716,47.004,90.643,47.1,90.553,47.214,90.496,47.285,90.476,47.329,90.467,47.408,90.425,47.504,90.381,47.557,90.347,47.597,90.331,47.655,90.313,47.676,90.191,47.702,90.103,47.745,90.067,47.804,90.054,47.85,90.028,47.878,89.959,47.886,89.91,47.844,89.831,47.823,89.778,47.827,89.726,47.852,89.693,47.879,89.638,47.909,89.561,48.004,89.479,48.029,89.33,48.025,89.196,47.981,89.116,47.988,89.048,48.003,88.971,48.05,88.918,48.089,88.838,48.102,88.682,48.171,88.576,48.22,88.567,48.317,88.517,48.384,88.414,48.403,88.31,48.472,88.158,48.509,88.063,48.538,87.98,48.555,87.967,48.581,87.972,48.603,88.011,48.64,88.05,48.675,88.06,48.707,88.028,48.736,87.942,48.765,87.832,48.792,87.809,48.836,87.743,48.882,87.755,48.919,87.807,48.946,87.86,48.966,87.872,49,87.835,49.032,87.816,49.08,87.825,49.116,87.814,49.162]]]},{"n":"Moldova","c":"MD","r":6,"ml":5,"lx":28.49,"ly":47.43,"b":[26.619,45.45,30.131,48.478],"p":[[[26.619,48.26,26.64,48.294,26.847,48.387,26.901,48.372,27.008,48.368,27.229,48.371,27.337,48.433,27.404,48.416,27.458,48.443,27.549,48.478,27.562,48.47,27.574,48.465,27.714,48.45,27.82,48.416,27.891,48.365,27.963,48.334,28.038,48.321,28.08,48.296,28.088,48.257,28.159,48.238,28.291,48.239,28.347,48.213,28.327,48.161,28.341,48.144,28.388,48.162,28.423,48.147,28.442,48.109,28.463,48.091,28.53,48.15,28.602,48.144,28.774,48.12,28.866,47.996,28.923,47.951,28.973,47.933,29.037,47.952,29.093,47.975,29.125,47.965,29.195,47.882,29.211,47.775,29.211,47.732,29.186,47.659,29.151,47.581,29.123,47.53,29.135,47.49,29.16,47.456,29.201,47.444,29.334,47.376,29.383,47.328,29.456,47.293,29.511,47.291,29.539,47.271,29.549,47.247,29.542,47.186,29.511,47.128,29.515,47.091,29.563,47.048,29.569,46.997,29.572,46.964,29.598,46.939,29.72,46.883,29.878,46.829,29.918,46.782,29.942,46.724,29.935,46.625,29.924,46.539,30.131,46.423,30.108,46.402,30.076,46.378,29.878,46.36,29.838,46.351,29.752,46.438,29.707,46.449,29.665,46.417,29.615,46.399,29.555,46.408,29.491,46.435,29.459,46.454,29.433,46.456,29.393,46.437,29.34,46.445,29.305,46.467,29.255,46.393,29.224,46.377,29.205,46.379,29.201,46.505,29.186,46.524,29.146,46.527,29.05,46.497,28.958,46.458,28.927,46.424,28.931,46.362,28.944,46.288,29.006,46.176,28.972,46.128,28.948,46.05,28.85,45.979,28.739,45.937,28.729,45.852,28.668,45.794,28.562,45.736,28.492,45.666,28.509,45.618,28.514,45.572,28.502,45.542,28.499,45.518,28.471,45.507,28.31,45.499,28.265,45.484,28.213,45.45,28.163,45.514,28.112,45.569,28.075,45.599,28.09,45.613,28.131,45.628,28.16,45.647,28.156,45.713,28.135,45.789,28.116,45.826,28.114,45.883,28.1,45.973,28.119,46.139,28.2,46.348,28.244,46.451,28.223,46.508,28.239,46.641,28.205,46.706,28.15,46.792,28.072,46.978,27.974,47.043,27.854,47.115,27.802,47.168,27.768,47.228,27.696,47.286,27.614,47.341,27.516,47.476,27.465,47.537,27.449,47.553,27.337,47.64,27.278,47.718,27.248,47.782,27.231,47.842,27.152,47.959,27.08,48.048,27.012,48.11,26.981,48.155,26.901,48.211,26.787,48.256,26.714,48.263,26.619,48.26]]]},{"n":"Monaco","c":"MC","r":6,"ml":5,"lx":7.4,"ly":43.74,"b":[7.378,43.732,7.439,43.771],"p":[[[7.439,43.75,7.378,43.732,7.38,43.753,7.395,43.765,7.414,43.771,7.437,43.761,7.439,43.75]]]},{"n":"Mexico","c":"MX","r":2,"ml":2,"lx":-102.29,"ly":23.92,"b":[-118.401,14.545,-86.696,32.715],"p":[[[-117.128,32.533,-116.842,32.555,-116.556,32.576,-116.27,32.598,-115.984,32.619,-115.698,32.64,-115.411,32.662,-115.125,32.683,-114.839,32.705,-114.725,32.715,-114.788,32.565,-114.836,32.508,-114.362,32.36,-113.887,32.212,-113.413,32.064,-112.939,31.916,-112.465,31.768,-111.99,31.62,-111.516,31.472,-111.042,31.324,-110.689,31.325,-110.335,31.326,-109.982,31.326,-109.628,31.327,-109.275,31.327,-108.921,31.328,-108.568,31.329,-108.214,31.329,-108.214,31.442,-108.213,31.554,-108.212,31.667,-108.212,31.779,-107.992,31.778,-107.772,31.777,-107.552,31.776,-107.333,31.775,-107.113,31.774,-106.893,31.772,-106.673,31.771,-106.453,31.77,-106.445,31.768,-106.436,31.764,-106.347,31.679,-106.256,31.545,-106.148,31.451,-106.024,31.398,-105.813,31.241,-105.514,30.981,-105.276,30.807,-105.098,30.721,-104.979,30.646,-104.918,30.583,-104.836,30.448,-104.681,30.134,-104.681,29.991,-104.622,29.854,-104.504,29.678,-104.401,29.574,-104.312,29.542,-104.216,29.48,-104.111,29.386,-103.99,29.323,-103.853,29.291,-103.664,29.207,-103.423,29.071,-103.258,29.001,-103.168,28.998,-103.09,29.042,-103.023,29.132,-102.957,29.19,-102.892,29.216,-102.866,29.258,-102.878,29.315,-102.834,29.444,-102.734,29.644,-102.615,29.752,-102.476,29.769,-102.386,29.807,-102.343,29.865,-102.269,29.871,-102.163,29.825,-101.991,29.796,-101.752,29.782,-101.612,29.787,-101.569,29.809,-101.546,29.808,-101.545,29.784,-101.509,29.773,-101.44,29.777,-101.38,29.743,-101.304,29.634,-101.039,29.46,-101.016,29.401,-100.924,29.315,-100.755,29.183,-100.659,29.069,-100.636,28.973,-100.55,28.821,-100.399,28.614,-100.332,28.503,-100.348,28.486,-100.336,28.428,-100.296,28.328,-100.221,28.243,-100.112,28.173,-100.001,28.048,-99.89,27.867,-99.754,27.73,-99.595,27.636,-99.505,27.548,-99.484,27.467,-99.486,27.398,-99.51,27.34,-99.5,27.285,-99.455,27.234,-99.44,27.17,-99.458,27.082,-99.457,27.057,-99.456,27.057,-99.444,27.037,-99.302,26.885,-99.23,26.762,-99.172,26.566,-99.172,26.564,-99.108,26.447,-99.015,26.399,-98.873,26.381,-98.765,26.34,-98.691,26.276,-98.598,26.238,-98.486,26.225,-98.378,26.182,-98.275,26.111,-98.083,26.064,-97.801,26.042,-97.587,25.984,-97.44,25.891,-97.376,25.872,-97.358,25.871,-97.35,25.885,-97.339,25.911,-97.282,25.942,-97.146,25.961,-97.164,25.755,-97.225,25.585,-97.424,25.233,-97.507,25.015,-97.668,24.39,-97.717,23.981,-97.729,23.788,-97.743,23.761,-97.727,23.732,-97.766,23.306,-97.745,22.942,-97.758,22.886,-97.817,22.776,-97.858,22.625,-97.842,22.557,-97.842,22.51,-97.782,22.279,-97.763,22.106,-97.585,21.809,-97.485,21.705,-97.36,21.615,-97.315,21.564,-97.337,21.438,-97.388,21.374,-97.409,21.273,-97.434,21.356,-97.424,21.465,-97.385,21.524,-97.383,21.567,-97.457,21.612,-97.59,21.762,-97.754,22.027,-97.638,21.604,-97.598,21.536,-97.567,21.508,-97.515,21.478,-97.501,21.432,-97.501,21.398,-97.357,21.104,-97.195,20.8,-97.186,20.717,-97.121,20.615,-96.709,20.188,-96.456,19.87,-96.368,19.567,-96.315,19.473,-96.29,19.344,-96.124,19.199,-96.073,19.106,-95.985,19.054,-95.913,18.897,-95.778,18.806,-95.81,18.804,-95.928,18.85,-95.92,18.82,-95.821,18.755,-95.627,18.691,-95.578,18.69,-95.655,18.724,-95.72,18.768,-95.697,18.775,-95.561,18.719,-95.182,18.701,-95.015,18.571,-94.798,18.515,-94.682,18.348,-94.546,18.175,-94.46,18.167,-94.392,18.166,-94.189,18.195,-93.873,18.304,-93.764,18.358,-93.552,18.43,-93.228,18.444,-93.127,18.423,-92.885,18.469,-92.769,18.524,-92.729,18.575,-92.71,18.612,-92.485,18.665,-92.441,18.675,-92.213,18.685,-92.103,18.704,-91.974,18.716,-91.88,18.638,-91.88,18.6,-91.943,18.563,-91.914,18.529,-91.803,18.471,-91.6,18.447,-91.534,18.457,-91.44,18.542,-91.275,18.624,-91.279,18.721,-91.308,18.773,-91.356,18.777,-91.368,18.806,-91.334,18.877,-91.343,18.901,-91.446,18.833,-91.469,18.833,-91.458,18.865,-91.437,18.89,-91.136,19.038,-91.059,19.098,-90.955,19.152,-90.739,19.352,-90.693,19.73,-90.65,19.796,-90.507,19.912,-90.492,19.947,-90.482,20.026,-90.486,20.224,-90.478,20.38,-90.484,20.556,-90.458,20.714,-90.435,20.758,-90.353,21.009,-90.183,21.121,-89.888,21.253,-89.82,21.275,-88.879,21.414,-88.747,21.448,-88.585,21.539,-88.467,21.569,-88.251,21.567,-88.185,21.579,-88.172,21.591,-88.171,21.604,-88.132,21.616,-88.007,21.604,-87.774,21.55,-87.689,21.536,-87.48,21.472,-87.251,21.447,-87.218,21.458,-87.188,21.477,-87.164,21.514,-87.188,21.546,-87.211,21.544,-87.249,21.527,-87.296,21.525,-87.387,21.551,-87.369,21.574,-87.276,21.572,-87.216,21.582,-87.128,21.621,-87.035,21.592,-86.912,21.463,-86.824,21.422,-86.817,21.234,-86.804,21.2,-86.772,21.151,-86.816,21.005,-86.865,20.885,-86.926,20.786,-87.06,20.631,-87.221,20.507,-87.421,20.231,-87.467,20.102,-87.466,19.999,-87.432,19.898,-87.442,19.862,-87.466,19.824,-87.507,19.827,-87.586,19.779,-87.688,19.637,-87.69,19.594,-87.645,19.554,-87.587,19.573,-87.512,19.575,-87.469,19.586,-87.425,19.583,-87.435,19.502,-87.483,19.444,-87.513,19.426,-87.567,19.416,-87.628,19.383,-87.659,19.352,-87.656,19.258,-87.622,19.25,-87.551,19.321,-87.509,19.317,-87.501,19.288,-87.594,19.046,-87.653,18.799,-87.734,18.655,-87.762,18.446,-87.804,18.357,-87.853,18.269,-87.882,18.274,-87.96,18.441,-88.039,18.484,-88.056,18.524,-88.011,18.727,-88.032,18.839,-88.074,18.834,-88.127,18.773,-88.197,18.72,-88.195,18.643,-88.276,18.515,-88.296,18.472,-88.372,18.482,-88.461,18.477,-88.523,18.446,-88.586,18.291,-88.744,18.072,-88.806,17.966,-88.857,17.929,-88.898,17.915,-88.943,17.94,-89.05,18,-89.134,17.971,-89.162,17.902,-89.161,17.815,-89.372,17.815,-89.729,17.815,-90.184,17.816,-90.622,17.816,-90.989,17.816,-90.99,17.621,-90.992,17.447,-90.993,17.252,-91.196,17.254,-91.41,17.256,-91.392,17.236,-91.319,17.2,-91.224,17.112,-91.112,16.976,-90.976,16.868,-90.816,16.787,-90.711,16.708,-90.66,16.631,-90.634,16.565,-90.634,16.511,-90.576,16.468,-90.471,16.44,-90.417,16.391,-90.417,16.351,-90.45,16.261,-90.46,16.162,-90.447,16.073,-90.522,16.071,-90.703,16.071,-90.98,16.071,-91.234,16.071,-91.434,16.07,-91.737,16.07,-91.819,15.932,-91.957,15.703,-92.082,15.496,-92.187,15.321,-92.204,15.275,-92.204,15.238,-92.075,15.074,-92.099,15.027,-92.144,15.002,-92.159,14.964,-92.156,14.901,-92.186,14.818,-92.176,14.761,-92.16,14.691,-92.187,14.63,-92.209,14.571,-92.235,14.545,-92.265,14.568,-92.531,14.84,-92.809,15.139,-92.918,15.236,-93.024,15.31,-93.167,15.448,-93.541,15.75,-93.734,15.888,-93.916,16.054,-94.079,16.145,-94.24,16.205,-94.311,16.239,-94.374,16.285,-94.409,16.287,-94.426,16.226,-94.37,16.195,-94.303,16.169,-94.25,16.168,-94.193,16.146,-94.028,16.062,-94.001,16.019,-94.471,16.187,-94.662,16.202,-94.682,16.228,-94.587,16.316,-94.617,16.348,-94.651,16.352,-94.753,16.291,-94.791,16.287,-94.797,16.327,-94.793,16.365,-94.859,16.42,-94.9,16.417,-94.935,16.379,-95.024,16.306,-95.021,16.278,-94.846,16.247,-94.786,16.229,-94.799,16.21,-94.949,16.21,-95.134,16.177,-95.464,15.975,-95.772,15.888,-96.214,15.693,-96.409,15.683,-96.511,15.652,-96.808,15.726,-97.185,15.909,-97.755,15.967,-98.139,16.206,-98.52,16.305,-98.762,16.535,-98.908,16.545,-99.002,16.581,-99.348,16.665,-99.691,16.72,-100.025,16.921,-100.243,16.984,-100.432,17.064,-100.848,17.2,-101.002,17.276,-101.148,17.393,-101.385,17.514,-101.487,17.615,-101.6,17.652,-101.762,17.842,-101.847,17.922,-101.919,17.96,-101.996,17.973,-102.217,17.957,-102.547,18.041,-102.7,18.063,-103.019,18.187,-103.442,18.325,-103.58,18.484,-103.699,18.633,-103.912,18.828,-104.046,18.912,-104.277,19.011,-104.405,19.091,-104.603,19.153,-104.938,19.309,-105.045,19.443,-105.108,19.562,-105.286,19.706,-105.482,19.976,-105.532,20.075,-105.57,20.228,-105.616,20.316,-105.669,20.386,-105.642,20.436,-105.543,20.498,-105.377,20.512,-105.26,20.579,-105.245,20.634,-105.252,20.669,-105.327,20.753,-105.42,20.775,-105.492,20.777,-105.511,20.809,-105.456,20.844,-105.394,20.926,-105.302,21.027,-105.237,21.119,-105.225,21.25,-105.233,21.38,-105.209,21.491,-105.431,21.618,-105.457,21.672,-105.527,21.818,-105.649,21.988,-105.646,22.327,-105.792,22.627,-105.943,22.777,-106.022,22.829,-106.235,23.061,-106.402,23.196,-106.567,23.449,-106.729,23.611,-106.935,23.881,-107.085,24.016,-107.765,24.472,-107.727,24.472,-107.527,24.36,-107.494,24.369,-107.489,24.424,-107.512,24.489,-107.549,24.505,-107.602,24.49,-107.674,24.504,-107.71,24.525,-107.817,24.539,-107.951,24.615,-108.009,24.694,-108.015,24.783,-108.208,24.975,-108.281,25.082,-108.243,25.074,-108.192,25.031,-108.14,25.018,-108.08,25.018,-108.036,25.035,-108.051,25.067,-108.093,25.094,-108.374,25.194,-108.466,25.265,-108.696,25.383,-108.751,25.424,-108.787,25.538,-108.844,25.543,-108.893,25.512,-109.029,25.48,-109.063,25.517,-109.068,25.552,-108.973,25.588,-108.885,25.696,-108.887,25.733,-108.935,25.69,-109.008,25.642,-109.084,25.615,-109.196,25.593,-109.254,25.609,-109.304,25.633,-109.385,25.727,-109.426,26.033,-109.354,26.138,-109.271,26.243,-109.2,26.305,-109.159,26.258,-109.117,26.253,-109.146,26.306,-109.216,26.355,-109.241,26.405,-109.243,26.45,-109.276,26.534,-109.483,26.71,-109.676,26.697,-109.755,26.703,-109.828,26.77,-109.891,26.883,-109.922,26.978,-109.926,27.029,-109.944,27.079,-110.277,27.162,-110.377,27.233,-110.478,27.323,-110.519,27.396,-110.561,27.45,-110.593,27.544,-110.615,27.654,-110.578,27.796,-110.53,27.864,-110.759,27.915,-110.849,27.918,-110.921,27.889,-110.986,27.926,-111.121,27.967,-111.282,28.115,-111.472,28.384,-111.68,28.471,-111.747,28.564,-111.832,28.648,-111.907,28.752,-111.919,28.798,-111.941,28.823,-112.045,28.896,-112.162,29.019,-112.192,29.118,-112.223,29.269,-112.301,29.323,-112.378,29.348,-112.393,29.42,-112.389,29.46,-112.415,29.536,-112.573,29.72,-112.653,29.87,-112.697,29.917,-112.738,29.985,-112.759,30.126,-112.825,30.3,-112.952,30.51,-113.058,30.651,-113.11,30.793,-113.087,30.938,-113.105,31.027,-113.119,31.048,-113.108,31.077,-113.073,31.061,-113.043,31.087,-113.047,31.179,-113.084,31.207,-113.186,31.236,-113.231,31.256,-113.481,31.294,-113.623,31.346,-113.633,31.468,-113.7,31.523,-113.759,31.558,-113.948,31.629,-113.977,31.593,-114.003,31.525,-114.081,31.51,-114.149,31.507,-114.264,31.554,-114.549,31.734,-114.609,31.762,-114.698,31.777,-114.741,31.806,-114.934,31.901,-114.895,31.851,-114.84,31.799,-114.79,31.647,-114.848,31.538,-114.882,31.156,-114.845,31.08,-114.761,30.959,-114.703,30.765,-114.685,30.621,-114.633,30.507,-114.65,30.238,-114.63,30.156,-114.55,30.022,-114.403,29.896,-114.373,29.83,-114.179,29.734,-114.062,29.61,-113.829,29.439,-113.755,29.367,-113.545,29.102,-113.538,29.023,-113.5,28.927,-113.382,28.947,-113.329,28.873,-113.335,28.839,-113.321,28.813,-113.259,28.819,-113.206,28.799,-113.094,28.512,-113.034,28.473,-112.957,28.456,-112.871,28.424,-112.865,28.351,-112.868,28.292,-112.796,28.207,-112.808,28.092,-112.749,27.995,-112.758,27.901,-112.734,27.826,-112.553,27.657,-112.329,27.523,-112.283,27.347,-112.191,27.187,-112.098,27.146,-112.004,27.079,-112.016,27.01,-112.009,26.967,-111.883,26.84,-111.863,26.679,-111.754,26.573,-111.723,26.564,-111.699,26.581,-111.779,26.687,-111.817,26.756,-111.822,26.865,-111.795,26.88,-111.57,26.708,-111.546,26.579,-111.47,26.507,-111.465,26.408,-111.419,26.35,-111.405,26.265,-111.332,26.125,-111.33,25.931,-111.292,25.79,-111.15,25.573,-111.034,25.527,-111.014,25.42,-110.894,25.144,-110.756,24.995,-110.687,24.868,-110.677,24.789,-110.729,24.672,-110.735,24.59,-110.659,24.341,-110.547,24.214,-110.421,24.183,-110.4,24.165,-110.41,24.131,-110.367,24.1,-110.32,24.139,-110.297,24.195,-110.321,24.259,-110.325,24.306,-110.304,24.339,-110.263,24.345,-110.023,24.175,-109.983,24.109,-109.893,24.033,-109.811,23.939,-109.776,23.865,-109.711,23.804,-109.677,23.662,-109.51,23.598,-109.421,23.48,-109.415,23.406,-109.458,23.215,-109.496,23.16,-109.63,23.079,-109.728,22.982,-109.823,22.922,-109.923,22.886,-110.006,22.894,-110.086,23.005,-110.181,23.342,-110.244,23.412,-110.289,23.518,-110.363,23.605,-110.63,23.737,-110.765,23.877,-110.896,23.97,-111.036,24.105,-111.419,24.329,-111.578,24.443,-111.683,24.556,-111.75,24.554,-111.802,24.543,-111.822,24.573,-111.825,24.632,-111.848,24.67,-112.073,24.84,-112.119,24.934,-112.129,25.043,-112.078,25.324,-112.056,25.488,-112.07,25.573,-112.093,25.584,-112.115,25.63,-112.12,25.766,-112.174,25.913,-112.377,26.214,-112.526,26.273,-112.658,26.317,-113.021,26.583,-113.119,26.717,-113.143,26.792,-113.156,26.946,-113.206,26.857,-113.272,26.791,-113.426,26.796,-113.599,26.721,-113.701,26.791,-113.757,26.871,-113.841,26.967,-113.936,26.985,-113.996,26.988,-114.11,27.106,-114.202,27.144,-114.333,27.158,-114.445,27.218,-114.48,27.284,-114.498,27.376,-114.54,27.431,-114.716,27.54,-114.859,27.659,-114.994,27.736,-115.033,27.799,-115.036,27.842,-114.824,27.83,-114.57,27.784,-114.448,27.797,-114.373,27.841,-114.301,27.873,-114.289,27.839,-114.302,27.776,-114.233,27.718,-114.137,27.671,-114.069,27.676,-114.135,27.727,-114.175,27.831,-114.157,27.868,-114.158,27.92,-114.253,27.908,-114.266,27.934,-114.185,28.013,-114.093,28.221,-114.048,28.426,-114.146,28.605,-114.309,28.73,-114.664,29.095,-114.876,29.282,-114.937,29.352,-114.994,29.384,-115.166,29.427,-115.311,29.532,-115.565,29.68,-115.674,29.756,-115.749,29.936,-115.808,29.96,-115.79,30.084,-115.816,30.304,-115.858,30.36,-115.996,30.414,-116.029,30.564,-116.035,30.705,-116.062,30.804,-116.296,30.971,-116.31,31.051,-116.31,31.127,-116.333,31.203,-116.458,31.361,-116.61,31.499,-116.662,31.565,-116.668,31.699,-116.722,31.735,-116.702,31.744,-116.652,31.74,-116.624,31.758,-116.621,31.851,-116.848,31.997,-116.914,32.199,-117.035,32.305,-117.063,32.344,-117.128,32.533]],[[-86.94,20.303,-86.991,20.272,-87.019,20.382,-86.978,20.49,-86.928,20.552,-86.829,20.559,-86.763,20.579,-86.755,20.552,-86.809,20.468,-86.94,20.303]],[[-106.502,21.611,-106.531,21.529,-106.607,21.561,-106.634,21.613,-106.639,21.698,-106.597,21.712,-106.536,21.676,-106.524,21.652,-106.502,21.611]],[[-110.914,18.741,-110.975,18.72,-111.064,18.782,-111.04,18.83,-110.989,18.863,-110.942,18.802,-110.914,18.741]],[[-110.567,25.003,-110.539,24.892,-110.59,24.908,-110.657,24.969,-110.703,25.047,-110.699,25.081,-110.69,25.088,-110.595,25.042,-110.567,25.003]],[[-113.156,29.052,-113.163,29.035,-113.265,29.097,-113.496,29.308,-113.581,29.413,-113.594,29.463,-113.587,29.573,-113.508,29.56,-113.416,29.486,-113.376,29.417,-113.374,29.339,-113.202,29.302,-113.178,29.132,-113.156,29.052]],[[-115.171,28.069,-115.184,28.037,-115.353,28.104,-115.26,28.221,-115.274,28.343,-115.234,28.368,-115.197,28.328,-115.149,28.172,-115.171,28.069]],[[-112.203,29.005,-112.278,28.769,-112.355,28.773,-112.514,28.848,-112.531,28.894,-112.47,29.168,-112.424,29.204,-112.285,29.24,-112.263,29.207,-112.249,29.126,-112.203,29.005]],[[-118.243,28.942,-118.285,28.904,-118.4,29.112,-118.401,29.163,-118.368,29.188,-118.312,29.183,-118.312,29.131,-118.266,29.086,-118.247,29.043,-118.243,28.942]],[[-86.714,21.239,-86.696,21.191,-86.714,21.197,-86.736,21.233,-86.753,21.279,-86.739,21.28,-86.727,21.264,-86.714,21.239]],[[-91.684,18.677,-91.796,18.654,-91.816,18.676,-91.589,18.778,-91.55,18.774,-91.537,18.76,-91.654,18.711,-91.684,18.677]],[[-109.805,24.151,-109.827,24.148,-109.878,24.201,-109.9,24.331,-109.89,24.345,-109.794,24.183,-109.796,24.164,-109.805,24.151]],[[-114.694,31.706,-114.727,31.701,-114.789,31.747,-114.785,31.79,-114.771,31.794,-114.709,31.757,-114.688,31.724,-114.694,31.706]],[[-111.1,26.021,-111.088,25.985,-111.094,25.974,-111.135,25.999,-111.204,25.85,-111.225,25.836,-111.183,26.041,-111.139,26.07,-111.091,26.076,-111.1,26.021]],[[-111.699,24.394,-111.712,24.346,-112.013,24.533,-111.941,24.551,-111.857,24.538,-111.699,24.394]],[[-112.057,24.546,-112.077,24.535,-112.163,24.65,-112.175,24.73,-112.21,24.763,-112.297,24.79,-112.222,24.951,-112.159,25.286,-112.132,25.224,-112.198,24.885,-112.195,24.841,-112.164,24.8,-112.13,24.73,-112.126,24.654,-112.067,24.584,-112.057,24.546]]]},{"n":"Mauritius","c":"MU","r":5,"ml":4,"lx":57.57,"ly":-20.3,"b":[57.318,-20.513,57.792,-19.99],"p":[[[57.651,-20.485,57.525,-20.513,57.383,-20.504,57.328,-20.45,57.318,-20.428,57.365,-20.406,57.362,-20.338,57.386,-20.229,57.416,-20.184,57.486,-20.144,57.515,-20.056,57.576,-19.997,57.657,-19.99,57.737,-20.098,57.792,-20.213,57.781,-20.327,57.725,-20.369,57.707,-20.435,57.651,-20.485]]]},{"n":"Mauritania","c":"MR","r":3,"ml":3,"lx":-9.74,"ly":19.59,"b":[-17.064,14.745,-4.823,27.286],"p":[[[-16.373,19.706,-16.438,19.609,-16.466,19.646,-16.477,19.71,-16.42,19.802,-16.393,19.849,-16.344,19.866,-16.373,19.706]],[[-12.281,14.809,-12.303,14.817,-12.409,14.889,-12.46,14.975,-12.544,15.039,-12.66,15.082,-12.735,15.131,-12.77,15.187,-12.813,15.224,-12.858,15.243,-12.863,15.262,-12.852,15.29,-12.863,15.34,-12.931,15.453,-12.994,15.505,-13.049,15.497,-13.079,15.51,-13.098,15.535,-13.105,15.572,-13.142,15.603,-13.206,15.617,-13.258,15.7,-13.297,15.854,-13.348,15.973,-13.41,16.059,-13.454,16.091,-13.487,16.097,-13.498,16.11,-13.507,16.135,-13.556,16.144,-13.624,16.118,-13.685,16.127,-13.715,16.169,-13.757,16.173,-13.81,16.138,-13.868,16.148,-13.933,16.203,-13.968,16.257,-13.975,16.311,-14.086,16.419,-14.3,16.58,-14.534,16.656,-14.787,16.646,-14.929,16.654,-14.96,16.679,-14.991,16.677,-15.022,16.647,-15.055,16.641,-15.091,16.657,-15.113,16.645,-15.121,16.604,-15.211,16.583,-15.38,16.582,-15.517,16.557,-15.621,16.507,-15.768,16.485,-15.959,16.492,-16.074,16.51,-16.113,16.54,-16.168,16.547,-16.239,16.531,-16.302,16.451,-16.358,16.307,-16.404,16.225,-16.441,16.205,-16.48,16.097,-16.502,15.917,-16.535,15.838,-16.536,16.287,-16.481,16.454,-16.464,16.602,-16.347,16.926,-16.207,17.193,-16.079,17.546,-16.03,17.888,-16.047,18.223,-16.085,18.521,-16.15,18.718,-16.213,19.003,-16.306,19.154,-16.476,19.285,-16.514,19.362,-16.475,19.391,-16.371,19.41,-16.305,19.513,-16.445,19.473,-16.283,19.787,-16.233,20.001,-16.241,20.141,-16.21,20.228,-16.334,20.416,-16.43,20.652,-16.479,20.69,-16.53,20.71,-16.535,20.654,-16.563,20.604,-16.623,20.634,-16.728,20.806,-16.876,21.086,-16.928,21.115,-16.971,21.076,-16.998,21.04,-17.048,20.806,-17.064,20.899,-17.042,21.008,-17.006,21.142,-16.965,21.329,-16.836,21.329,-16.607,21.33,-16.378,21.33,-16.148,21.33,-15.919,21.33,-15.69,21.331,-15.461,21.331,-15.231,21.331,-15.002,21.332,-14.773,21.332,-14.543,21.332,-14.314,21.332,-14.085,21.333,-13.855,21.333,-13.626,21.333,-13.397,21.334,-13.167,21.334,-13.016,21.334,-13.025,21.467,-13.032,21.572,-13.042,21.714,-13.051,21.855,-13.061,21.996,-13.07,22.128,-13.078,22.26,-13.087,22.383,-13.094,22.496,-13.107,22.561,-13.156,22.689,-13.167,22.753,-13.153,22.821,-13.121,22.884,-13.031,23,-12.896,23.09,-12.74,23.193,-12.62,23.271,-12.559,23.291,-12.373,23.318,-12.226,23.377,-12.083,23.435,-12.023,23.468,-12.016,23.576,-12.016,23.698,-12.016,23.834,-12.016,23.97,-12.016,24.106,-12.016,24.242,-12.016,24.379,-12.016,24.515,-12.016,24.651,-12.016,24.787,-12.016,24.923,-12.016,25.059,-12.016,25.196,-12.016,25.332,-12.016,25.468,-12.016,25.604,-12.016,25.74,-12.016,25.876,-12.016,25.995,-11.867,25.995,-11.68,25.995,-11.494,25.995,-11.308,25.995,-11.121,25.995,-10.935,25.995,-10.749,25.995,-10.562,25.995,-10.376,25.995,-10.19,25.995,-10.004,25.995,-9.817,25.995,-9.631,25.996,-9.445,25.996,-9.258,25.996,-9.072,25.996,-8.886,25.996,-8.682,25.996,-8.682,26.109,-8.682,26.273,-8.682,26.498,-8.683,26.723,-8.683,26.921,-8.683,27.119,-8.683,27.286,-8.495,27.175,-8.307,27.065,-8.119,26.954,-7.931,26.844,-7.743,26.733,-7.555,26.622,-7.367,26.512,-7.179,26.401,-6.991,26.291,-6.803,26.18,-6.615,26.069,-6.427,25.959,-6.239,25.848,-6.051,25.738,-5.863,25.627,-5.675,25.516,-5.517,25.424,-5.275,25.275,-5.05,25.135,-4.823,24.996,-5.173,24.995,-5.641,24.995,-5.96,24.995,-6.287,24.995,-6.594,24.995,-6.567,24.767,-6.539,24.518,-6.51,24.269,-6.482,24.021,-6.454,23.772,-6.425,23.523,-6.397,23.275,-6.368,23.026,-6.34,22.777,-6.311,22.529,-6.283,22.28,-6.254,22.032,-6.226,21.783,-6.197,21.534,-6.169,21.286,-6.14,21.037,-6.112,20.788,-6.083,20.54,-6.055,20.291,-6.026,20.042,-5.998,19.794,-5.969,19.545,-5.941,19.296,-5.912,19.048,-5.884,18.799,-5.856,18.55,-5.827,18.302,-5.799,18.053,-5.77,17.804,-5.742,17.556,-5.713,17.307,-5.685,17.058,-5.656,16.81,-5.629,16.569,-5.51,16.442,-5.36,16.283,-5.404,16.058,-5.456,15.789,-5.512,15.496,-5.724,15.496,-5.928,15.496,-6.132,15.496,-6.336,15.496,-6.54,15.496,-6.744,15.496,-6.948,15.496,-7.152,15.496,-7.355,15.496,-7.559,15.496,-7.763,15.496,-7.967,15.496,-8.171,15.496,-8.375,15.496,-8.579,15.496,-8.783,15.496,-8.987,15.496,-9.177,15.496,-9.294,15.503,-9.335,15.526,-9.351,15.677,-9.385,15.668,-9.427,15.623,-9.448,15.575,-9.44,15.512,-9.447,15.458,-9.578,15.437,-9.755,15.401,-9.941,15.374,-10.13,15.384,-10.194,15.396,-10.262,15.416,-10.412,15.438,-10.493,15.44,-10.587,15.435,-10.697,15.423,-10.732,15.395,-10.815,15.282,-10.896,15.15,-10.948,15.151,-11.007,15.223,-11.169,15.359,-11.366,15.537,-11.455,15.625,-11.503,15.637,-11.597,15.573,-11.676,15.512,-11.76,15.426,-11.798,15.343,-11.829,15.245,-11.842,15.129,-11.873,14.995,-11.941,14.887,-12.022,14.805,-12.082,14.766,-12.105,14.745,-12.281,14.809]]]},{"n":"Malta","c":"MT","r":5,"ml":4,"lx":14.43,"ly":35.89,"b":[14.18,35.82,14.566,36.076],"p":[[[14.566,35.853,14.533,35.82,14.436,35.822,14.352,35.872,14.351,35.978,14.448,35.957,14.537,35.886,14.566,35.853]],[[14.313,36.028,14.254,36.012,14.194,36.042,14.18,36.06,14.263,36.076,14.304,36.062,14.321,36.036,14.313,36.028]]]},{"n":"Mali","c":"ML","r":3,"ml":3,"lx":-2.04,"ly":18.69,"b":[-12.281,10.143,4.235,24.996],"p":[[[-11.389,12.404,-11.382,12.479,-11.449,12.532,-11.451,12.558,-11.444,12.628,-11.414,12.775,-11.417,12.832,-11.39,12.942,-11.434,12.992,-11.444,13.028,-11.493,13.087,-11.549,13.17,-11.562,13.237,-11.581,13.29,-11.635,13.37,-11.674,13.382,-11.758,13.395,-11.772,13.367,-11.803,13.327,-11.832,13.316,-11.878,13.365,-11.895,13.406,-11.895,13.444,-11.957,13.511,-12.054,13.633,-12.044,13.734,-11.984,13.788,-11.966,13.829,-11.961,13.875,-11.988,13.931,-12.02,13.975,-12.011,14.072,-12.019,14.206,-12.068,14.274,-12.113,14.323,-12.175,14.377,-12.228,14.459,-12.207,14.571,-12.187,14.648,-12.281,14.809,-12.105,14.745,-12.082,14.766,-12.022,14.805,-11.941,14.887,-11.873,14.995,-11.842,15.129,-11.829,15.245,-11.798,15.343,-11.76,15.426,-11.676,15.512,-11.597,15.573,-11.503,15.637,-11.455,15.625,-11.366,15.537,-11.169,15.359,-11.007,15.223,-10.948,15.151,-10.896,15.15,-10.815,15.282,-10.732,15.395,-10.697,15.423,-10.587,15.435,-10.493,15.44,-10.412,15.438,-10.262,15.416,-10.194,15.396,-10.13,15.384,-9.941,15.374,-9.755,15.401,-9.578,15.437,-9.447,15.458,-9.44,15.512,-9.448,15.575,-9.427,15.623,-9.385,15.668,-9.351,15.677,-9.335,15.526,-9.294,15.503,-9.177,15.496,-8.987,15.496,-8.783,15.496,-8.579,15.496,-8.375,15.496,-8.171,15.496,-7.967,15.496,-7.763,15.496,-7.559,15.496,-7.355,15.496,-7.152,15.496,-6.948,15.496,-6.744,15.496,-6.54,15.496,-6.336,15.496,-6.132,15.496,-5.928,15.496,-5.724,15.496,-5.512,15.496,-5.456,15.789,-5.404,16.058,-5.36,16.283,-5.51,16.442,-5.629,16.569,-5.656,16.81,-5.685,17.058,-5.713,17.307,-5.742,17.556,-5.77,17.804,-5.799,18.053,-5.827,18.302,-5.856,18.55,-5.884,18.799,-5.912,19.048,-5.941,19.296,-5.969,19.545,-5.998,19.794,-6.026,20.042,-6.055,20.291,-6.083,20.54,-6.112,20.788,-6.14,21.037,-6.169,21.286,-6.197,21.534,-6.226,21.783,-6.254,22.032,-6.283,22.28,-6.311,22.529,-6.34,22.777,-6.368,23.026,-6.397,23.275,-6.425,23.523,-6.454,23.772,-6.482,24.021,-6.51,24.269,-6.539,24.518,-6.567,24.767,-6.594,24.995,-6.287,24.995,-5.96,24.995,-5.641,24.995,-5.173,24.995,-4.823,24.996,-4.517,24.804,-4.24,24.624,-3.913,24.409,-3.585,24.195,-3.258,23.981,-2.93,23.767,-2.603,23.553,-2.275,23.339,-1.948,23.125,-1.62,22.911,-1.293,22.697,-0.965,22.482,-0.638,22.268,-0.311,22.054,0.017,21.84,0.344,21.626,0.672,21.412,0.999,21.198,1.146,21.102,1.159,21.063,1.173,20.982,1.164,20.891,1.166,20.817,1.209,20.767,1.29,20.714,1.611,20.556,1.636,20.524,1.647,20.459,1.685,20.378,1.753,20.332,1.832,20.297,1.929,20.273,2.219,20.248,2.281,20.21,2.406,20.064,2.474,20.035,2.668,19.993,2.808,19.969,2.866,19.956,2.992,19.917,3.13,19.85,3.204,19.79,3.203,19.771,3.203,19.718,3.202,19.56,3.227,19.474,3.256,19.411,3.254,19.373,3.22,19.345,3.192,19.312,3.177,19.268,3.138,19.212,3.106,19.15,3.12,19.103,3.174,19.073,3.256,19.013,3.323,18.988,3.356,18.987,3.401,18.988,3.439,18.996,3.683,19.042,3.91,19.084,4.228,19.143,4.228,18.968,4.229,18.704,4.23,18.411,4.231,18.139,4.232,17.831,4.233,17.582,4.234,17.288,4.235,16.996,4.203,16.963,4.191,16.798,4.182,16.582,4.121,16.358,4.015,16.193,3.976,16.036,3.947,15.946,3.907,15.897,3.898,15.838,3.877,15.755,3.843,15.702,3.817,15.674,3.71,15.642,3.521,15.483,3.504,15.356,3.289,15.391,3.06,15.427,3.029,15.425,3.011,15.408,3.001,15.341,2.69,15.33,2.421,15.32,2.088,15.309,1.859,15.302,1.569,15.286,1.3,15.272,1.121,15.126,0.96,14.987,0.947,14.982,0.719,14.955,0.433,14.979,0.286,14.98,0.229,14.964,0.217,14.911,0.007,14.985,-0.236,15.059,-0.405,15.013,-0.432,15.029,-0.454,15.06,-0.537,15.078,-0.666,15.07,-0.76,15.048,-0.908,14.937,-1.019,14.841,-1.05,14.82,-1.205,14.762,-1.494,14.626,-1.657,14.527,-1.695,14.508,-1.768,14.486,-1.88,14.481,-1.973,14.457,-2.057,14.195,-2.113,14.168,-2.457,14.274,-2.527,14.258,-2.587,14.228,-2.779,14.074,-2.874,13.951,-2.926,13.787,-2.919,13.736,-2.917,13.679,-2.951,13.648,-2.997,13.637,-3.039,13.639,-3.198,13.673,-3.249,13.658,-3.27,13.577,-3.267,13.401,-3.302,13.281,-3.397,13.244,-3.47,13.196,-3.528,13.183,-3.576,13.194,-3.853,13.374,-3.947,13.402,-4.051,13.382,-4.151,13.306,-4.196,13.256,-4.259,13.197,-4.329,13.119,-4.31,13.052,-4.261,12.975,-4.225,12.879,-4.227,12.794,-4.481,12.672,-4.46,12.63,-4.422,12.582,-4.422,12.493,-4.429,12.338,-4.48,12.282,-4.546,12.226,-4.587,12.155,-4.627,12.12,-4.699,12.076,-4.798,12.032,-4.969,11.993,-5.106,11.968,-5.158,11.942,-5.23,11.89,-5.288,11.828,-5.302,11.76,-5.291,11.683,-5.27,11.62,-5.245,11.577,-5.229,11.522,-5.25,11.376,-5.3,11.206,-5.347,11.13,-5.424,11.089,-5.49,11.042,-5.469,10.931,-5.457,10.771,-5.476,10.644,-5.479,10.565,-5.507,10.483,-5.524,10.426,-5.557,10.44,-5.694,10.433,-5.844,10.39,-5.896,10.355,-5.908,10.307,-5.941,10.275,-5.989,10.239,-6.035,10.195,-6.117,10.202,-6.197,10.232,-6.238,10.262,-6.241,10.279,-6.215,10.322,-6.193,10.369,-6.191,10.4,-6.218,10.476,-6.24,10.558,-6.231,10.598,-6.25,10.718,-6.261,10.724,-6.366,10.693,-6.404,10.685,-6.426,10.672,-6.433,10.649,-6.408,10.572,-6.424,10.559,-6.483,10.561,-6.565,10.586,-6.654,10.656,-6.676,10.634,-6.686,10.578,-6.692,10.512,-6.669,10.392,-6.693,10.349,-6.753,10.357,-6.834,10.357,-6.904,10.345,-6.95,10.342,-6.979,10.3,-6.992,10.252,-6.964,10.199,-6.968,10.176,-6.989,10.156,-7.017,10.143,-7.04,10.145,-7.105,10.204,-7.182,10.226,-7.363,10.259,-7.385,10.34,-7.415,10.341,-7.457,10.384,-7.498,10.44,-7.533,10.437,-7.562,10.421,-7.661,10.427,-7.749,10.342,-7.814,10.237,-7.884,10.186,-7.961,10.163,-7.991,10.163,-7.974,10.23,-7.986,10.278,-8.007,10.322,-8.231,10.438,-8.267,10.486,-8.302,10.618,-8.324,10.75,-8.322,10.827,-8.306,10.896,-8.313,10.95,-8.337,10.991,-8.404,11.03,-8.475,11.048,-8.564,10.997,-8.606,10.987,-8.646,10.99,-8.667,11.009,-8.664,11.036,-8.567,11.177,-8.52,11.236,-8.464,11.281,-8.425,11.305,-8.401,11.339,-8.399,11.367,-8.407,11.386,-8.471,11.412,-8.569,11.478,-8.621,11.485,-8.665,11.515,-8.711,11.618,-8.733,11.638,-8.78,11.648,-8.822,11.673,-8.82,11.807,-8.818,11.923,-8.914,12.109,-8.951,12.226,-8.999,12.346,-9.043,12.402,-9.12,12.45,-9.216,12.483,-9.3,12.49,-9.365,12.479,-9.395,12.465,-9.394,12.442,-9.341,12.366,-9.332,12.324,-9.34,12.283,-9.358,12.255,-9.405,12.252,-9.487,12.229,-9.588,12.182,-9.658,12.143,-9.715,12.042,-9.754,12.03,-9.821,12.042,-10.011,12.116,-10.167,12.177,-10.275,12.213,-10.34,12.19,-10.373,12.18,-10.466,12.139,-10.59,11.99,-10.619,11.941,-10.644,11.926,-10.677,11.899,-10.709,11.899,-10.735,11.916,-10.743,11.927,-10.806,12.034,-10.876,12.152,-10.933,12.205,-11.005,12.208,-11.066,12.171,-11.129,12.095,-11.21,12.025,-11.261,12.004,-11.305,12.015,-11.415,12.104,-11.492,12.167,-11.502,12.199,-11.475,12.247,-11.448,12.319,-11.418,12.378,-11.389,12.404]]]},{"n":"Maldives","c":"MV","r":5,"ml":4,"lx":73.51,"ly":4.17,"b":[73.382,3.229,73.528,4.248],"p":[[[73.417,3.231,73.395,3.229,73.382,3.246,73.385,3.271,73.402,3.289,73.428,3.29,73.443,3.274,73.435,3.25,73.417,3.231]],[[73.512,4.165,73.495,4.155,73.479,4.159,73.473,4.171,73.481,4.188,73.495,4.21,73.504,4.235,73.518,4.248,73.528,4.243,73.527,4.23,73.522,4.211,73.519,4.187,73.512,4.165]]]},{"n":"Malaysia","c":"MY","r":3,"ml":3,"lx":113.84,"ly":2.53,"b":[99.646,0.862,119.266,7.352],"p":[[[100.119,6.442,100.138,6.489,100.161,6.642,100.177,6.672,100.217,6.687,100.261,6.683,100.345,6.55,100.564,6.468,100.629,6.448,100.716,6.481,100.754,6.46,100.794,6.426,100.817,6.332,100.874,6.245,100.989,6.258,101.029,6.245,101.054,6.243,101.076,6.166,101.087,6.034,101.076,5.956,100.993,5.846,100.982,5.771,101.025,5.725,101.082,5.675,101.114,5.637,101.148,5.643,101.191,5.669,101.23,5.734,101.257,5.789,101.404,5.852,101.556,5.908,101.577,5.902,101.601,5.877,101.65,5.796,101.678,5.779,101.72,5.771,101.791,5.779,101.874,5.825,101.917,5.911,101.936,5.979,102.055,6.097,102.068,6.185,102.101,6.242,102.274,6.203,102.34,6.172,102.534,5.863,102.79,5.645,102.899,5.564,102.982,5.525,103.097,5.408,103.197,5.262,103.416,4.85,103.454,4.669,103.469,4.393,103.421,3.977,103.362,3.769,103.373,3.671,103.454,3.521,103.429,3.379,103.445,3.261,103.439,2.933,103.485,2.837,103.537,2.775,103.812,2.58,103.832,2.508,103.968,2.261,104.219,1.723,104.288,1.481,104.28,1.416,104.25,1.389,104.176,1.365,104.115,1.412,104.094,1.446,104.101,1.488,104.076,1.53,104.016,1.579,103.981,1.624,103.991,1.55,103.992,1.455,103.915,1.447,103.817,1.477,103.695,1.45,103.55,1.333,103.48,1.329,103.427,1.43,103.4,1.498,103.357,1.546,102.897,1.792,102.727,1.856,102.548,2.042,102.146,2.248,101.89,2.449,101.781,2.574,101.52,2.684,101.407,2.813,101.351,2.839,101.296,2.885,101.354,3.011,101.33,3.142,101.3,3.253,101.115,3.472,101.025,3.625,100.851,3.777,100.782,3.864,100.715,3.966,100.757,4.002,100.796,4.023,100.76,4.097,100.661,4.226,100.615,4.373,100.615,4.652,100.473,5.044,100.353,5.588,100.374,5.778,100.343,5.984,100.263,6.183,100.158,6.324,100.119,6.442]],[[117.574,4.171,117.537,4.171,117.451,4.193,117.278,4.299,117.101,4.337,116.844,4.34,116.698,4.355,116.639,4.339,116.589,4.338,116.553,4.36,116.515,4.371,116.415,4.308,116.368,4.327,116.32,4.354,116.236,4.363,116.134,4.355,116.022,4.291,115.896,4.349,115.861,4.348,115.837,4.333,115.782,4.254,115.679,4.193,115.628,4.082,115.596,3.976,115.568,3.939,115.561,3.733,115.545,3.634,115.571,3.502,115.566,3.446,115.52,3.362,115.514,3.342,115.49,3.209,115.499,3.173,115.493,3.128,115.454,3.034,115.384,3.009,115.31,2.994,115.247,3.026,115.19,2.974,115.118,2.895,115.086,2.841,115.087,2.791,115.094,2.758,115.079,2.723,115.077,2.687,115.081,2.634,115.13,2.612,115.181,2.567,115.179,2.523,115.151,2.493,115.087,2.446,114.969,2.351,114.836,2.269,114.786,2.25,114.768,2.213,114.759,2.162,114.788,2.052,114.816,2.019,114.831,1.98,114.813,1.934,114.8,1.894,114.751,1.869,114.704,1.851,114.686,1.819,114.661,1.686,114.632,1.617,114.567,1.514,114.546,1.467,114.513,1.452,114.387,1.5,114.275,1.471,114.126,1.452,114,1.455,113.902,1.434,113.835,1.38,113.76,1.311,113.682,1.261,113.622,1.236,113.513,1.308,113.458,1.302,113.359,1.327,113.126,1.408,113.069,1.432,113.007,1.434,112.988,1.457,112.998,1.496,112.988,1.548,112.943,1.567,112.476,1.559,112.342,1.515,112.251,1.48,112.186,1.439,112.167,1.338,112.129,1.244,112.079,1.143,111.923,1.113,111.809,1.012,111.77,0.999,111.691,1.014,111.607,1.023,111.547,0.994,111.483,0.996,111.287,1.043,111.101,1.051,110.996,1.026,110.938,1.017,110.615,0.878,110.506,0.862,110.461,0.882,110.399,0.939,110.315,0.996,110.115,1.19,110.041,1.236,109.992,1.283,109.945,1.338,109.879,1.398,109.818,1.439,109.736,1.523,109.654,1.615,109.636,1.777,109.571,1.806,109.549,1.848,109.539,1.896,109.629,2.028,109.694,1.889,109.72,1.858,109.865,1.764,109.985,1.718,110.114,1.699,110.246,1.695,110.298,1.701,110.349,1.72,110.4,1.7,110.675,1.548,110.782,1.521,110.895,1.532,110.94,1.517,111.098,1.401,111.145,1.387,111.223,1.396,111.123,1.449,111.058,1.487,111.029,1.558,111.047,1.634,111.11,1.684,111.154,1.739,111.17,1.902,111.198,1.985,111.251,2.064,111.268,2.14,111.209,2.198,111.196,2.297,111.209,2.38,111.242,2.436,111.296,2.399,111.351,2.364,111.406,2.368,111.444,2.382,111.451,2.424,111.44,2.498,111.443,2.634,111.513,2.743,111.623,2.818,111.728,2.854,112.119,2.915,112.737,3.07,112.921,3.131,112.988,3.162,113.045,3.205,113.14,3.344,113.32,3.561,113.446,3.741,113.712,4.001,113.924,4.243,113.953,4.289,113.988,4.421,113.99,4.483,113.984,4.546,114.013,4.575,114.054,4.593,114.064,4.593,114.095,4.565,114.169,4.527,114.224,4.478,114.261,4.414,114.288,4.355,114.29,4.304,114.323,4.263,114.417,4.256,114.447,4.204,114.512,4.114,114.572,4.049,114.608,4.024,114.654,4.038,114.725,4.097,114.776,4.169,114.81,4.267,114.783,4.281,114.831,4.354,114.84,4.393,114.818,4.429,114.79,4.464,114.779,4.553,114.76,4.667,114.747,4.718,114.784,4.755,114.865,4.802,114.945,4.856,115.027,4.9,115.029,4.821,115.027,4.691,115.052,4.583,115.107,4.39,115.171,4.364,115.247,4.347,115.291,4.353,115.319,4.365,115.327,4.381,115.279,4.456,115.267,4.634,115.228,4.751,115.168,4.867,115.14,4.9,115.375,4.933,115.428,4.969,115.52,5.049,115.554,5.094,115.582,5.194,115.467,5.254,115.422,5.331,115.419,5.413,115.556,5.567,115.604,5.603,115.625,5.549,115.685,5.535,115.741,5.533,115.797,5.536,115.877,5.614,115.918,5.725,116.06,5.882,116.11,6.003,116.138,6.13,116.495,6.522,116.538,6.583,116.75,6.977,116.776,6.99,116.833,6.952,116.85,6.827,116.842,6.772,116.808,6.691,116.788,6.606,116.812,6.608,116.913,6.66,117.019,6.797,117.078,6.917,117.129,6.969,117.23,6.94,117.252,6.919,117.245,6.833,117.255,6.783,117.294,6.677,117.38,6.612,117.499,6.571,117.61,6.513,117.646,6.474,117.67,6.427,117.694,6.35,117.696,6.272,117.616,6.197,117.65,6.074,117.645,6.002,117.617,5.941,117.501,5.885,117.818,5.94,117.896,5.972,118.004,6.053,118.062,5.983,118.116,5.863,118.072,5.832,117.935,5.788,117.928,5.769,117.974,5.706,118.031,5.712,118.145,5.754,118.249,5.821,118.3,5.82,118.353,5.806,118.456,5.763,118.514,5.729,118.563,5.685,118.595,5.592,118.714,5.559,118.957,5.429,119.003,5.418,119.05,5.415,119.178,5.431,119.223,5.413,119.256,5.366,119.266,5.308,119.263,5.246,119.25,5.199,119.22,5.16,119.132,5.1,118.913,5.023,118.672,4.964,118.551,4.968,118.382,5.019,118.32,5.012,118.261,4.989,118.185,4.829,118.324,4.669,118.563,4.502,118.595,4.461,118.586,4.41,118.548,4.379,118.498,4.362,118.364,4.336,118.229,4.316,118.117,4.288,118.008,4.25,117.896,4.263,117.741,4.338,117.696,4.343,117.65,4.304,117.604,4.2,117.574,4.171]],[[104.222,2.732,104.173,2.721,104.147,2.728,104.129,2.767,104.17,2.857,104.185,2.872,104.223,2.774,104.222,2.732]],[[100.289,5.295,100.264,5.267,100.191,5.283,100.204,5.447,100.246,5.468,100.31,5.438,100.339,5.41,100.289,5.295]],[[99.848,6.466,99.919,6.359,99.883,6.311,99.866,6.297,99.823,6.313,99.783,6.272,99.744,6.263,99.705,6.338,99.657,6.367,99.646,6.418,99.711,6.427,99.749,6.41,99.822,6.445,99.848,6.466]],[[117.142,7.168,117.081,7.115,117.06,7.179,117.064,7.261,117.147,7.337,117.264,7.352,117.281,7.291,117.267,7.221,117.239,7.185,117.142,7.168]],[[101.319,2.988,101.268,2.97,101.265,2.996,101.274,3.033,101.311,3.067,101.328,3.048,101.319,2.988]],[[111.389,2.415,111.359,2.402,111.312,2.438,111.3,2.741,111.333,2.768,111.355,2.764,111.378,2.709,111.376,2.576,111.38,2.459,111.389,2.415]],[[117.885,4.186,117.745,4.167,117.649,4.169,117.667,4.204,117.662,4.25,117.708,4.262,117.761,4.252,117.885,4.186]]]},{"n":"Malawi","c":"MW","r":6,"ml":4,"lx":33.61,"ly":-13.39,"b":[32.67,-17.131,35.893,-9.395],"p":[[[34.719,-12.111,34.684,-12.119,34.662,-12.101,34.667,-12.048,34.68,-12.009,34.715,-12.003,34.739,-12.013,34.756,-12.031,34.756,-12.059,34.746,-12.088,34.719,-12.111]],[[34.642,-12.014,34.622,-12.067,34.58,-12.066,34.542,-12.019,34.554,-11.982,34.591,-11.971,34.624,-11.985,34.642,-12.014]],[[33.202,-14.013,33.148,-13.941,33.104,-13.959,33.042,-14.01,33.009,-14.024,32.992,-14.022,32.981,-14.009,32.968,-13.977,32.92,-13.884,32.867,-13.817,32.811,-13.792,32.765,-13.761,32.785,-13.731,32.807,-13.71,32.797,-13.688,32.772,-13.657,32.672,-13.61,32.67,-13.59,32.758,-13.55,32.814,-13.503,32.852,-13.457,32.9,-13.357,32.939,-13.257,32.968,-13.225,32.978,-13.159,32.971,-13.084,32.99,-12.989,33,-12.9,32.971,-12.865,32.946,-12.804,32.975,-12.701,33.022,-12.63,33.243,-12.557,33.398,-12.49,33.431,-12.46,33.483,-12.403,33.512,-12.348,33.491,-12.331,33.37,-12.33,33.34,-12.308,33.252,-12.113,33.301,-11.888,33.305,-11.8,33.304,-11.691,33.288,-11.611,33.25,-11.578,33.226,-11.535,33.233,-11.418,33.268,-11.404,33.346,-11.249,33.38,-11.158,33.339,-11.085,33.293,-10.981,33.273,-10.915,33.261,-10.893,33.293,-10.852,33.345,-10.813,33.403,-10.802,33.465,-10.783,33.659,-10.591,33.662,-10.553,33.626,-10.489,33.554,-10.391,33.538,-10.352,33.529,-10.235,33.5,-10.2,33.394,-10.121,33.312,-10.038,33.337,-9.954,33.351,-9.862,33.31,-9.812,33.25,-9.76,33.213,-9.683,33.196,-9.626,33.148,-9.604,33.104,-9.603,33.072,-9.638,33.038,-9.635,32.996,-9.623,32.982,-9.574,32.98,-9.52,32.951,-9.484,32.923,-9.434,32.92,-9.407,32.937,-9.4,32.974,-9.395,33.13,-9.496,33.225,-9.5,33.331,-9.519,33.421,-9.608,33.468,-9.62,33.528,-9.608,33.698,-9.598,33.766,-9.611,33.854,-9.663,33.889,-9.67,33.944,-9.672,33.954,-9.658,33.959,-9.627,33.95,-9.565,33.962,-9.532,33.996,-9.495,34.089,-9.538,34.321,-9.732,34.328,-9.757,34.476,-9.949,34.524,-10.03,34.524,-10.073,34.57,-10.241,34.58,-10.32,34.57,-10.38,34.572,-10.428,34.59,-10.496,34.584,-10.525,34.637,-10.626,34.662,-10.71,34.667,-10.792,34.652,-10.873,34.606,-10.99,34.598,-11.037,34.608,-11.08,34.638,-11.127,34.688,-11.177,34.726,-11.238,34.752,-11.309,34.774,-11.342,34.801,-11.341,34.851,-11.352,34.891,-11.394,34.937,-11.463,34.953,-11.544,34.959,-11.578,34.827,-11.576,34.66,-11.589,34.619,-11.62,34.606,-11.69,34.554,-11.834,34.525,-11.887,34.463,-11.984,34.376,-12.12,34.358,-12.165,34.361,-12.211,34.412,-12.396,34.466,-12.591,34.483,-12.667,34.521,-12.926,34.543,-13.109,34.546,-13.216,34.564,-13.36,34.612,-13.438,34.662,-13.487,34.85,-13.516,34.907,-13.552,35.014,-13.643,35.247,-13.897,35.376,-14.059,35.488,-14.201,35.69,-14.466,35.847,-14.671,35.867,-14.864,35.893,-14.892,35.84,-15.035,35.805,-15.266,35.83,-15.419,35.82,-15.68,35.791,-15.959,35.755,-16.058,35.709,-16.096,35.599,-16.126,35.358,-16.161,35.322,-16.193,35.292,-16.247,35.243,-16.375,35.185,-16.505,35.167,-16.56,35.178,-16.573,35.23,-16.639,35.281,-16.808,35.29,-17.097,35.273,-17.118,35.201,-17.131,35.125,-17.127,35.093,-17.111,35.065,-17.079,35.044,-17.017,35.094,-16.974,35.112,-16.899,35.08,-16.834,35.015,-16.82,34.933,-16.76,34.759,-16.567,34.613,-16.432,34.528,-16.319,34.441,-16.274,34.416,-16.247,34.396,-16.199,34.395,-16.131,34.403,-16.08,34.376,-16.024,34.288,-15.936,34.248,-15.887,34.246,-15.829,34.283,-15.773,34.358,-15.705,34.415,-15.567,34.435,-15.477,34.541,-15.297,34.555,-15.141,34.558,-15.016,34.551,-14.922,34.524,-14.731,34.505,-14.598,34.375,-14.425,34.333,-14.409,34.209,-14.424,34.102,-14.449,34.049,-14.485,33.97,-14.487,33.761,-14.517,33.696,-14.53,33.658,-14.562,33.636,-14.568,33.505,-14.434,33.39,-14.289,33.244,-14.043,33.202,-14.013]]]},{"n":"Madagascar","c":"MG","r":3,"ml":2.7,"lx":46.7,"ly":-18.63,"b":[43.257,-25.571,50.483,-12.08],"p":[[[49.538,-12.432,49.584,-12.537,49.638,-12.637,49.805,-12.88,49.876,-12.973,49.938,-13.072,49.967,-13.27,50.073,-13.578,50.174,-14.04,50.205,-14.514,50.235,-14.732,50.313,-14.937,50.441,-15.149,50.483,-15.386,50.405,-15.629,50.292,-15.858,50.262,-15.902,50.209,-15.96,50.185,-15.958,50.094,-15.899,50.02,-15.802,49.927,-15.574,49.893,-15.458,49.853,-15.439,49.744,-15.45,49.664,-15.522,49.65,-15.567,49.667,-15.696,49.697,-15.811,49.71,-15.929,49.713,-16.077,49.742,-16.121,49.786,-16.159,49.831,-16.256,49.839,-16.487,49.811,-16.603,49.734,-16.703,49.739,-16.758,49.767,-16.815,49.74,-16.849,49.637,-16.893,49.595,-16.931,49.54,-17.033,49.449,-17.241,49.437,-17.347,49.494,-17.67,49.478,-17.899,49.363,-18.336,49.297,-18.544,49.203,-18.792,49.06,-19.12,48.918,-19.53,48.797,-19.953,48.708,-20.207,48.607,-20.458,48.469,-20.9,48.351,-21.349,48.176,-21.843,47.934,-22.394,47.908,-22.466,47.858,-22.747,47.804,-22.992,47.739,-23.233,47.604,-23.633,47.589,-23.756,47.558,-23.875,47.428,-24.125,47.373,-24.218,47.334,-24.318,47.312,-24.443,47.273,-24.564,47.177,-24.787,47.035,-24.979,46.938,-25.049,46.729,-25.15,46.622,-25.17,46.387,-25.173,46.159,-25.23,45.921,-25.341,45.692,-25.468,45.605,-25.529,45.508,-25.563,45.206,-25.571,45.115,-25.543,44.813,-25.334,44.696,-25.3,44.474,-25.271,44.407,-25.253,44.346,-25.226,44.256,-25.117,44.078,-25.025,44.035,-24.996,44.008,-24.932,43.99,-24.863,43.944,-24.787,43.91,-24.641,43.852,-24.538,43.688,-24.358,43.67,-24.3,43.657,-24.109,43.662,-23.979,43.646,-23.742,43.665,-23.63,43.722,-23.53,43.699,-23.421,43.638,-23.307,43.615,-23.188,43.57,-23.08,43.398,-22.886,43.358,-22.791,43.33,-22.692,43.265,-22.384,43.257,-22.276,43.267,-22.049,43.291,-21.933,43.332,-21.851,43.343,-21.79,43.37,-21.738,43.411,-21.696,43.438,-21.647,43.502,-21.356,43.583,-21.292,43.704,-21.255,43.8,-21.179,43.856,-21.077,43.911,-20.866,44.063,-20.656,44.117,-20.546,44.24,-20.38,44.348,-20.146,44.381,-20.035,44.405,-19.922,44.432,-19.674,44.453,-19.551,44.449,-19.429,44.387,-19.303,44.239,-19.075,44.234,-19.033,44.246,-18.863,44.233,-18.741,44.179,-18.619,44.109,-18.504,44.04,-18.288,44.007,-17.933,44.014,-17.804,43.994,-17.69,43.944,-17.581,43.979,-17.392,44.421,-16.703,44.436,-16.621,44.418,-16.411,44.427,-16.289,44.442,-16.244,44.476,-16.217,44.552,-16.204,44.909,-16.175,44.955,-16.153,45.044,-16.095,45.167,-15.983,45.223,-15.95,45.271,-15.962,45.302,-16.01,45.342,-16.037,45.486,-15.986,45.542,-15.984,45.598,-15.993,45.625,-15.946,45.641,-15.883,45.662,-15.839,45.7,-15.814,45.886,-15.8,46.004,-15.782,46.158,-15.738,46.191,-15.747,46.314,-15.905,46.352,-15.918,46.4,-15.925,46.442,-15.896,46.341,-15.813,46.326,-15.767,46.331,-15.714,46.385,-15.6,46.475,-15.513,46.675,-15.382,46.882,-15.23,46.942,-15.219,46.993,-15.243,47.032,-15.423,47.027,-15.452,47.061,-15.456,47.099,-15.434,47.133,-15.362,47.135,-15.302,47.107,-15.244,47.094,-15.195,47.093,-15.15,47.198,-15.044,47.28,-14.943,47.319,-14.822,47.352,-14.766,47.439,-14.703,47.465,-14.713,47.485,-14.764,47.496,-14.818,47.474,-14.872,47.442,-14.925,47.429,-14.996,47.478,-15.009,47.525,-14.992,47.593,-14.864,47.67,-14.743,47.716,-14.68,47.774,-14.637,47.87,-14.646,47.964,-14.673,47.812,-14.545,47.773,-14.37,47.955,-14.067,47.957,-14.004,47.983,-13.985,47.996,-13.96,47.901,-13.858,47.884,-13.808,47.896,-13.731,47.941,-13.662,47.982,-13.615,48.04,-13.596,48.086,-13.623,48.187,-13.707,48.255,-13.719,48.338,-13.639,48.405,-13.538,48.506,-13.469,48.621,-13.426,48.796,-13.267,48.91,-12.936,48.919,-12.839,48.894,-12.722,48.854,-12.61,48.786,-12.471,48.804,-12.44,48.9,-12.458,48.932,-12.439,49.036,-12.316,49.207,-12.08,49.263,-12.08,49.312,-12.124,49.33,-12.189,49.364,-12.236,49.48,-12.348,49.538,-12.432]],[[48.342,-13.364,48.344,-13.4,48.212,-13.385,48.191,-13.26,48.256,-13.256,48.27,-13.205,48.309,-13.198,48.351,-13.31,48.342,-13.364]],[[49.936,-16.903,49.824,-17.087,49.856,-16.933,49.986,-16.712,50.023,-16.695,49.936,-16.903]]]},{"n":"North Macedonia","c":"MK","r":6,"ml":5,"lx":21.56,"ly":41.56,"b":[20.449,40.85,23.006,42.358],"p":[[[21.563,42.248,21.618,42.242,21.739,42.268,21.815,42.303,21.853,42.308,21.904,42.322,21.978,42.32,22.052,42.305,22.147,42.325,22.24,42.358,22.277,42.35,22.317,42.322,22.344,42.314,22.498,42.165,22.583,42.105,22.682,42.059,22.796,42.026,22.837,41.994,22.909,41.835,22.944,41.775,22.992,41.757,23.004,41.74,23.006,41.717,22.951,41.606,22.93,41.356,22.916,41.336,22.859,41.337,22.784,41.332,22.755,41.313,22.725,41.179,22.604,41.14,22.494,41.119,22.401,41.123,22.238,41.155,22.184,41.159,22.139,41.141,21.993,41.131,21.929,41.107,21.779,40.95,21.628,40.896,21.576,40.869,21.46,40.904,21.404,40.907,21.324,40.867,21.148,40.863,21.1,40.856,20.964,40.85,20.959,40.872,20.933,40.903,20.87,40.918,20.741,40.905,20.709,40.928,20.656,41.062,20.614,41.083,20.568,41.128,20.489,41.273,20.487,41.336,20.492,41.391,20.449,41.521,20.476,41.554,20.516,41.575,20.517,41.627,20.505,41.706,20.553,41.862,20.566,41.874,20.579,41.866,20.695,41.854,20.725,41.874,20.744,41.904,20.75,42.018,20.778,42.071,21.06,42.171,21.142,42.175,21.206,42.129,21.256,42.1,21.287,42.1,21.298,42.13,21.332,42.187,21.39,42.22,21.561,42.248,21.563,42.248]]]},{"n":"Luxembourg","c":"LU","r":6,"ml":5.7,"lx":6.08,"ly":49.73,"b":[5.725,49.445,6.494,50.167],"p":[[[6.117,50.121,6.108,50.094,6.11,50.034,6.138,49.974,6.205,49.915,6.256,49.872,6.325,49.838,6.441,49.805,6.487,49.798,6.494,49.754,6.485,49.708,6.445,49.682,6.407,49.645,6.378,49.6,6.348,49.513,6.344,49.453,6.277,49.478,6.242,49.494,6.181,49.499,6.12,49.485,6.074,49.455,6.011,49.445,5.959,49.455,5.929,49.478,5.901,49.49,5.823,49.505,5.79,49.538,5.815,49.554,5.838,49.578,5.857,49.613,5.88,49.645,5.804,49.732,5.788,49.759,5.725,49.808,5.726,49.833,5.741,49.857,5.735,49.876,5.744,49.92,5.788,49.961,5.817,50.013,5.867,50.083,5.976,50.167,6.055,50.154,6.089,50.155,6.11,50.124,6.117,50.121]]]},{"n":"Lithuania","c":"LT","r":5,"ml":4,"lx":24.09,"ly":55.1,"b":[20.9,53.893,26.776,56.411],"p":[[[20.958,55.279,20.9,55.287,21.014,55.402,21.058,55.477,21.088,55.583,21.115,55.617,21.116,55.568,21.104,55.488,21.032,55.35,20.958,55.279]],[[22.766,54.357,22.724,54.406,22.68,54.493,22.684,54.563,22.71,54.633,22.831,54.838,22.825,54.871,22.737,54.929,22.627,54.971,22.567,55.059,22.346,55.064,22.138,55.059,22.072,55.064,21.874,55.101,21.683,55.16,21.555,55.195,21.447,55.234,21.389,55.276,21.298,55.264,21.236,55.264,21.236,55.271,21.201,55.344,21.238,55.455,21.171,55.618,21.062,55.813,21.054,56.023,21.046,56.07,21.315,56.188,21.654,56.315,21.731,56.326,22.043,56.401,22.085,56.407,22.366,56.393,22.587,56.375,22.773,56.377,22.876,56.396,22.968,56.38,23.043,56.324,23.12,56.331,23.196,56.367,23.613,56.334,23.707,56.335,23.813,56.329,24.008,56.295,24.121,56.264,24.368,56.283,24.474,56.284,24.529,56.296,24.7,56.381,24.773,56.396,24.841,56.411,24.903,56.398,24.944,56.326,25.07,56.2,25.207,56.178,25.586,56.13,25.663,56.105,25.876,55.994,26.004,55.94,26.086,55.897,26.21,55.812,26.281,55.75,26.401,55.704,26.543,55.672,26.594,55.668,26.591,55.623,26.567,55.546,26.519,55.448,26.47,55.372,26.458,55.342,26.495,55.318,26.681,55.306,26.76,55.293,26.776,55.273,26.734,55.247,26.675,55.225,26.648,55.204,26.601,55.13,26.292,55.14,26.251,55.125,26.231,55.09,26.216,55.05,26.175,55.003,26.093,54.962,25.964,54.947,25.859,54.919,25.781,54.833,25.722,54.718,25.724,54.636,25.732,54.59,25.725,54.564,25.685,54.536,25.62,54.46,25.568,54.377,25.547,54.332,25.558,54.311,25.617,54.31,25.703,54.293,25.748,54.26,25.765,54.221,25.765,54.18,25.749,54.157,25.681,54.14,25.573,54.14,25.51,54.16,25.497,54.175,25.527,54.215,25.506,54.265,25.461,54.293,25.371,54.251,25.284,54.251,25.179,54.214,25.111,54.155,25.046,54.133,24.87,54.145,24.826,54.119,24.789,53.998,24.768,53.975,24.621,53.98,24.479,53.932,24.318,53.893,24.237,53.92,24.191,53.95,24.104,53.945,24.008,53.932,23.944,53.939,23.873,53.936,23.734,53.912,23.559,53.92,23.485,53.94,23.478,53.959,23.483,54.006,23.481,54.079,23.454,54.143,23.37,54.2,23.282,54.24,23.17,54.281,23.088,54.299,23.042,54.304,23.032,54.328,23.016,54.348,22.977,54.366,22.894,54.391,22.824,54.396,22.766,54.357]]]},{"n":"Liechtenstein","c":"LI","r":6,"ml":5,"lx":9.56,"ly":47.11,"b":[9.479,47.057,9.611,47.271],"p":[[[9.58,47.057,9.502,47.063,9.488,47.062,9.479,47.098,9.484,47.173,9.528,47.271,9.537,47.255,9.542,47.234,9.551,47.212,9.556,47.185,9.572,47.158,9.601,47.132,9.611,47.107,9.596,47.076,9.58,47.057]]]},{"n":"Libya","c":"LY","r":3,"ml":3,"lx":18.01,"ly":26.64,"b":[9.31,19.497,25.15,33.182],"p":[[[9.519,30.229,9.638,30.282,9.807,30.342,9.895,30.387,9.933,30.425,10.06,30.58,10.126,30.666,10.216,30.783,10.256,30.865,10.257,30.941,10.243,31.032,10.173,31.251,10.115,31.464,10.16,31.546,10.196,31.585,10.275,31.685,10.306,31.705,10.476,31.736,10.544,31.803,10.596,31.886,10.609,31.93,10.683,31.975,10.772,32.021,10.826,32.081,11.005,32.173,11.168,32.257,11.358,32.345,11.505,32.414,11.536,32.473,11.534,32.525,11.454,32.643,11.454,32.782,11.459,32.897,11.467,32.966,11.502,33.156,11.505,33.182,11.657,33.119,11.813,33.094,12.28,32.859,12.427,32.829,12.754,32.801,13.138,32.897,13.283,32.915,13.536,32.824,13.648,32.799,13.835,32.792,14.156,32.71,14.237,32.681,14.424,32.55,14.513,32.511,15.177,32.391,15.267,32.312,15.359,32.16,15.363,31.971,15.414,31.834,15.496,31.657,15.596,31.531,15.706,31.426,15.832,31.361,16.123,31.264,16.451,31.227,16.782,31.215,17.349,31.081,17.83,30.928,17.949,30.852,18.19,30.777,18.67,30.416,18.936,30.29,19.124,30.266,19.292,30.288,19.59,30.414,19.713,30.488,20.013,30.801,20.112,30.964,20.151,31.079,20.141,31.196,20.104,31.301,20.02,31.411,19.961,31.556,19.926,31.818,19.973,31.999,20.031,32.108,20.121,32.219,20.371,32.431,20.621,32.58,21.062,32.776,21.319,32.778,21.425,32.799,21.636,32.937,21.721,32.942,21.839,32.909,22.187,32.918,22.341,32.88,22.523,32.794,22.754,32.741,22.917,32.687,23.091,32.619,23.13,32.448,23.11,32.397,23.106,32.331,23.286,32.214,23.798,32.159,23.898,32.127,24.039,32.037,24.13,32.009,24.48,31.997,24.684,32.016,24.879,31.984,24.951,31.954,25.025,31.883,25.115,31.712,25.15,31.655,25.112,31.627,25.057,31.567,25.023,31.514,24.93,31.427,24.853,31.335,24.86,31.199,24.878,31.061,24.929,30.926,24.974,30.777,24.961,30.679,24.923,30.558,24.878,30.458,24.726,30.251,24.703,30.201,24.712,30.132,24.804,29.886,24.811,29.809,24.866,29.57,24.916,29.376,24.972,29.224,24.98,29.182,24.98,28.957,24.98,28.733,24.98,28.508,24.98,28.284,24.98,28.059,24.98,27.835,24.98,27.61,24.98,27.385,24.98,27.161,24.98,26.936,24.98,26.712,24.98,26.487,24.98,26.263,24.98,26.038,24.98,25.813,24.98,25.589,24.98,25.364,24.98,25.14,24.98,24.915,24.98,24.691,24.98,24.466,24.98,24.242,24.98,24.017,24.98,23.792,24.98,23.568,24.98,23.343,24.98,23.119,24.98,22.894,24.98,22.67,24.98,22.445,24.98,22.22,24.98,21.996,24.98,21.498,24.98,20.999,24.98,20.501,24.979,20.003,24.976,20.001,24.973,19.999,24.97,19.997,24.967,19.995,24.72,19.996,24.474,19.996,24.227,19.996,23.98,19.996,23.98,19.871,23.98,19.746,23.98,19.621,23.98,19.497,23.501,19.733,23.022,19.97,22.543,20.206,22.064,20.443,21.585,20.679,21.106,20.916,20.627,21.153,20.148,21.389,19.669,21.626,19.189,21.862,18.71,22.099,18.231,22.336,17.752,22.572,17.273,22.809,16.794,23.045,16.315,23.282,15.984,23.445,15.627,23.286,15.347,23.161,14.979,22.996,14.556,22.783,14.231,22.618,14.216,22.62,14.201,22.624,13.863,22.902,13.599,23.12,13.481,23.18,12.984,23.291,12.489,23.402,11.968,23.518,11.873,23.695,11.767,23.893,11.624,24.14,11.537,24.291,11.508,24.314,11.108,24.434,10.686,24.551,10.439,24.48,10.396,24.486,10.326,24.53,10.256,24.591,10.219,24.676,10.12,24.79,10.028,25.051,10.019,25.259,10.001,25.332,9.781,25.624,9.581,25.89,9.448,26.067,9.422,26.147,9.438,26.246,9.491,26.334,9.685,26.438,9.859,26.552,9.883,26.631,9.894,26.848,9.837,26.916,9.795,27.045,9.753,27.219,9.748,27.331,9.825,27.553,9.916,27.786,9.858,28.043,9.816,28.56,9.843,28.967,9.821,29.115,9.805,29.177,9.746,29.369,9.673,29.567,9.64,29.636,9.546,29.796,9.391,29.994,9.31,30.115,9.421,30.179,9.519,30.229]]]},{"n":"Liberia","c":"LR","r":4,"ml":4,"lx":-9.46,"ly":6.45,"b":[-11.508,4.351,-7.4,8.538],"p":[[[-11.508,6.907,-11.455,6.951,-11.377,7.095,-11.268,7.233,-11.166,7.314,-11.085,7.399,-11,7.463,-10.878,7.538,-10.691,7.736,-10.647,7.759,-10.618,7.896,-10.571,8.071,-10.517,8.125,-10.39,8.158,-10.36,8.188,-10.315,8.311,-10.286,8.454,-10.283,8.485,-10.233,8.489,-10.147,8.52,-10.098,8.506,-10.076,8.465,-10.064,8.43,-9.805,8.519,-9.782,8.538,-9.768,8.535,-9.736,8.454,-9.717,8.459,-9.701,8.482,-9.684,8.484,-9.664,8.474,-9.643,8.436,-9.61,8.402,-9.554,8.379,-9.518,8.346,-9.522,8.26,-9.508,8.176,-9.484,8.157,-9.471,8.107,-9.465,8.052,-9.451,8.023,-9.442,7.968,-9.446,7.908,-9.436,7.867,-9.395,7.795,-9.369,7.704,-9.369,7.64,-9.384,7.572,-9.411,7.51,-9.46,7.443,-9.464,7.416,-9.435,7.398,-9.392,7.395,-9.355,7.409,-9.263,7.378,-9.215,7.333,-9.173,7.278,-9.135,7.251,-9.118,7.216,-9.052,7.225,-8.977,7.259,-8.961,7.275,-8.938,7.266,-8.89,7.263,-8.856,7.323,-8.828,7.392,-8.769,7.467,-8.74,7.496,-8.733,7.544,-8.729,7.605,-8.708,7.659,-8.66,7.688,-8.607,7.688,-8.579,7.677,-8.564,7.625,-8.522,7.586,-8.486,7.558,-8.467,7.547,-8.437,7.516,-8.409,7.412,-8.297,7.074,-8.302,6.981,-8.325,6.92,-8.325,6.86,-8.333,6.802,-8.401,6.705,-8.604,6.508,-8.588,6.491,-8.54,6.468,-8.49,6.456,-8.45,6.463,-8.399,6.413,-8.345,6.351,-8.287,6.319,-8.204,6.291,-8.131,6.288,-8.069,6.298,-7.982,6.286,-7.889,6.235,-7.856,6.15,-7.833,6.076,-7.801,6.039,-7.797,5.975,-7.73,5.919,-7.636,5.908,-7.514,5.842,-7.483,5.846,-7.469,5.854,-7.454,5.841,-7.424,5.651,-7.4,5.551,-7.412,5.51,-7.429,5.478,-7.43,5.325,-7.485,5.236,-7.494,5.14,-7.51,5.108,-7.569,5.081,-7.569,5.006,-7.585,4.917,-7.591,4.822,-7.575,4.572,-7.572,4.386,-7.545,4.351,-7.66,4.367,-7.998,4.509,-8.259,4.59,-9.132,5.055,-9.375,5.241,-9.654,5.519,-10.276,6.078,-10.418,6.167,-10.597,6.211,-10.708,6.258,-10.786,6.31,-10.849,6.465,-11.005,6.557,-11.292,6.688,-11.508,6.907]]]},{"n":"Lesotho","c":"LS","r":6,"ml":4,"lx":28.25,"ly":-29.48,"b":[27.052,-30.642,29.391,-28.582],"p":[[[28.737,-30.102,28.647,-30.127,28.634,-30.129,28.577,-30.123,28.5,-30.129,28.439,-30.142,28.392,-30.148,28.315,-30.218,28.176,-30.41,28.139,-30.45,28.129,-30.525,28.096,-30.585,28.057,-30.631,28.018,-30.642,27.902,-30.624,27.753,-30.6,27.667,-30.542,27.59,-30.466,27.549,-30.411,27.507,-30.381,27.492,-30.364,27.431,-30.338,27.409,-30.325,27.388,-30.316,27.364,-30.279,27.35,-30.247,27.355,-30.159,27.313,-30.106,27.24,-30.015,27.194,-29.941,27.13,-29.84,27.092,-29.754,27.052,-29.664,27.057,-29.626,27.095,-29.599,27.207,-29.554,27.295,-29.519,27.357,-29.455,27.425,-29.36,27.458,-29.303,27.491,-29.277,27.527,-29.236,27.59,-29.146,27.66,-29.047,27.736,-28.94,27.83,-28.909,27.96,-28.873,28.084,-28.78,28.233,-28.701,28.472,-28.616,28.583,-28.594,28.626,-28.582,28.653,-28.598,28.681,-28.647,28.722,-28.688,28.816,-28.759,28.856,-28.776,28.954,-28.881,29.058,-28.954,29.178,-29.037,29.26,-29.078,29.301,-29.09,29.336,-29.164,29.371,-29.218,29.391,-29.27,29.387,-29.32,29.349,-29.442,29.294,-29.567,29.249,-29.619,29.195,-29.652,29.142,-29.701,29.122,-29.801,29.098,-29.919,29.029,-29.968,28.975,-29.999,28.901,-30.038,28.737,-30.102]]]},{"n":"Lebanon","c":"LB","r":5,"ml":4,"lx":35.99,"ly":34.13,"b":[35.109,33.076,36.585,34.679],"p":[[[35.976,34.629,36.151,34.629,36.264,34.633,36.296,34.679,36.384,34.658,36.433,34.613,36.389,34.567,36.326,34.513,36.33,34.5,36.376,34.495,36.456,34.466,36.504,34.432,36.585,34.221,36.535,34.134,36.458,34.057,36.423,34.05,36.355,34.011,36.298,33.959,36.278,33.925,36.282,33.894,36.363,33.855,36.365,33.839,36.349,33.827,36.283,33.836,36.199,33.84,36.15,33.84,36.092,33.832,36.019,33.784,35.986,33.753,35.968,33.732,35.942,33.668,35.972,33.623,36.027,33.598,36.034,33.585,36.022,33.563,35.968,33.535,35.927,33.5,35.915,33.465,35.869,33.432,35.841,33.416,35.788,33.37,35.734,33.333,35.627,33.275,35.603,33.241,35.579,33.271,35.533,33.25,35.493,33.119,35.411,33.076,35.309,33.08,35.223,33.092,35.109,33.084,35.155,33.16,35.204,33.259,35.251,33.393,35.336,33.503,35.511,33.88,35.612,34.032,35.648,34.248,35.804,34.437,35.921,34.493,35.978,34.547,35.976,34.629]]]},{"n":"Latvia","c":"LV","r":5,"ml":4,"lx":25.46,"ly":57.07,"b":[21.015,55.668,28.202,58.063],"p":[[[26.594,55.668,26.543,55.672,26.401,55.704,26.281,55.75,26.21,55.812,26.086,55.897,26.004,55.94,25.876,55.994,25.663,56.105,25.586,56.13,25.207,56.178,25.07,56.2,24.944,56.326,24.903,56.398,24.841,56.411,24.773,56.396,24.7,56.381,24.529,56.296,24.474,56.284,24.368,56.283,24.121,56.264,24.008,56.295,23.813,56.329,23.707,56.335,23.613,56.334,23.196,56.367,23.12,56.331,23.043,56.324,22.968,56.38,22.876,56.396,22.773,56.377,22.587,56.375,22.366,56.393,22.085,56.407,22.043,56.401,21.731,56.326,21.654,56.315,21.315,56.188,21.046,56.07,21.015,56.259,21.031,56.637,21.071,56.824,21.257,56.933,21.351,57.018,21.405,57.131,21.421,57.236,21.459,57.322,21.729,57.571,21.942,57.598,22.231,57.667,22.555,57.724,22.617,57.651,22.649,57.595,23.038,57.392,23.137,57.324,23.287,57.09,23.648,56.971,23.931,57.008,24.054,57.066,24.281,57.172,24.383,57.25,24.403,57.325,24.363,57.645,24.302,57.784,24.323,57.871,24.363,57.866,24.459,57.908,24.776,57.985,24.839,57.989,24.911,58.005,25.111,58.063,25.175,58.032,25.229,57.997,25.258,57.996,25.273,58.009,25.269,58.032,25.283,58.048,25.34,58.039,25.571,57.943,25.66,57.92,25.721,57.914,25.794,57.869,25.991,57.838,26.015,57.815,26.03,57.786,26.215,57.663,26.298,57.601,26.462,57.544,26.533,57.531,26.82,57.589,26.9,57.609,26.966,57.609,27.033,57.579,27.187,57.538,27.327,57.525,27.352,57.528,27.47,57.524,27.511,57.508,27.539,57.43,27.673,57.368,27.797,57.317,27.829,57.293,27.838,57.248,27.83,57.194,27.815,57.167,27.763,57.135,27.717,57.055,27.711,56.978,27.639,56.846,27.656,56.843,27.806,56.867,27.849,56.853,27.882,56.824,27.892,56.741,27.941,56.704,27.992,56.645,28.008,56.6,28.103,56.546,28.111,56.511,28.169,56.387,28.192,56.316,28.202,56.26,28.173,56.19,28.148,56.143,28.118,56.146,28.032,56.133,27.896,56.076,27.694,55.942,27.642,55.912,27.589,55.809,27.577,55.799,27.459,55.804,27.427,55.806,27.309,55.804,27.053,55.831,26.953,55.813,26.822,55.709,26.772,55.694,26.62,55.68,26.594,55.668]]]},{"n":"Laos","c":"LA","r":4,"ml":4,"lx":102.53,"ly":19.43,"b":[100.115,13.921,107.653,22.495],"p":[[[102.127,22.379,102.183,22.284,102.301,22.178,102.443,22.027,102.488,21.958,102.583,21.904,102.61,21.852,102.631,21.771,102.641,21.711,102.662,21.676,102.695,21.662,102.739,21.678,102.771,21.71,102.798,21.798,102.816,21.807,102.845,21.735,102.876,21.722,102.918,21.713,102.95,21.681,102.959,21.626,102.949,21.57,102.91,21.506,102.888,21.44,102.872,21.338,102.851,21.266,102.884,21.203,103.104,20.892,103.211,20.841,103.464,20.78,103.555,20.738,103.635,20.697,103.714,20.717,103.791,20.81,103.882,20.861,104.052,20.941,104.101,20.946,104.195,20.914,104.35,20.821,104.461,20.734,104.53,20.688,104.583,20.647,104.575,20.6,104.533,20.555,104.479,20.53,104.408,20.486,104.368,20.441,104.392,20.425,104.496,20.414,104.619,20.375,104.656,20.329,104.662,20.289,104.677,20.225,104.699,20.205,104.813,20.217,104.848,20.202,104.889,20.169,104.929,20.083,104.928,20.018,104.846,19.947,104.815,19.904,104.802,19.836,104.743,19.755,104.588,19.619,104.546,19.611,104.26,19.685,104.127,19.681,104.063,19.678,104.032,19.675,104.013,19.646,104.052,19.564,104.063,19.483,104.028,19.42,103.932,19.366,103.896,19.34,103.892,19.305,103.918,19.269,104.006,19.231,104.109,19.196,104.446,18.984,104.518,18.934,104.613,18.861,104.717,18.803,104.993,18.728,105.115,18.679,105.146,18.651,105.145,18.617,105.113,18.573,105.087,18.496,105.086,18.45,105.115,18.405,105.163,18.339,105.273,18.235,105.333,18.19,105.4,18.179,105.458,18.154,105.519,18.077,105.588,17.984,105.598,17.918,105.627,17.834,105.691,17.738,105.779,17.644,105.903,17.529,105.974,17.447,106.006,17.415,106.27,17.217,106.333,17.144,106.426,17.003,106.465,16.982,106.502,16.954,106.526,16.877,106.534,16.821,106.546,16.651,106.594,16.6,106.638,16.538,106.656,16.493,106.696,16.459,106.74,16.453,106.792,16.49,106.832,16.526,106.851,16.516,106.893,16.397,106.931,16.353,107.002,16.312,107.07,16.28,107.217,16.136,107.296,16.084,107.35,16.067,107.396,16.043,107.41,15.998,107.392,15.952,107.361,15.922,107.189,15.839,107.166,15.802,107.19,15.747,107.233,15.678,107.279,15.619,107.339,15.56,107.46,15.466,107.564,15.392,107.622,15.31,107.653,15.255,107.634,15.19,107.59,15.118,107.555,15.057,107.496,15.021,107.48,14.98,107.505,14.916,107.525,14.872,107.514,14.817,107.519,14.705,107.465,14.665,107.415,14.563,107.38,14.555,107.293,14.592,107.262,14.572,107.207,14.498,107.109,14.417,107.062,14.416,107.03,14.426,106.992,14.391,106.938,14.327,106.913,14.329,106.82,14.315,106.783,14.335,106.738,14.388,106.665,14.441,106.599,14.479,106.564,14.505,106.531,14.549,106.501,14.578,106.447,14.515,106.355,14.455,106.268,14.466,106.225,14.476,106.191,14.388,106.165,14.372,106.008,14.357,105.979,14.343,106.004,14.263,106.097,14.127,106.125,14.049,106.067,13.921,105.904,13.925,105.831,13.977,105.764,14.049,105.74,14.085,105.532,14.156,105.393,14.107,105.35,14.11,105.285,14.161,105.246,14.201,105.207,14.259,105.186,14.319,105.183,14.346,105.244,14.368,105.342,14.417,105.423,14.472,105.476,14.53,105.497,14.591,105.5,14.661,105.523,14.843,105.547,14.932,105.533,15.042,105.49,15.128,105.49,15.257,105.506,15.32,105.513,15.361,105.574,15.413,105.616,15.488,105.639,15.586,105.641,15.657,105.622,15.7,105.562,15.741,105.462,15.78,105.399,15.83,105.373,15.89,105.376,15.942,105.406,15.987,105.331,16.038,105.149,16.094,105.047,16.16,105.026,16.238,104.95,16.34,104.819,16.466,104.751,16.648,104.744,16.884,104.759,17.077,104.816,17.3,104.74,17.462,104.656,17.547,104.539,17.609,104.428,17.699,104.323,17.816,104.196,17.988,104.049,18.217,103.95,18.319,103.899,18.295,103.792,18.317,103.63,18.383,103.488,18.418,103.367,18.423,103.288,18.408,103.252,18.373,103.249,18.339,103.28,18.305,103.263,18.278,103.2,18.259,103.149,18.222,103.091,18.138,103.051,18.029,102.991,17.986,102.899,17.977,102.807,17.946,102.718,17.892,102.675,17.852,102.68,17.824,102.661,17.818,102.617,17.833,102.596,17.87,102.598,17.927,102.553,17.965,102.459,17.985,102.352,18.046,102.232,18.149,102.148,18.204,102.101,18.211,102.035,18.17,101.947,18.081,101.875,18.046,101.819,18.065,101.775,18.033,101.744,17.953,101.688,17.889,101.564,17.821,101.555,17.812,101.414,17.719,101.3,17.625,101.167,17.499,101.105,17.48,101.046,17.51,100.956,17.541,100.908,17.584,100.999,17.797,101.113,18.034,101.144,18.143,101.149,18.222,101.138,18.287,101.093,18.355,101.051,18.407,101.047,18.442,101.06,18.479,101.106,18.534,101.166,18.618,101.221,18.793,101.286,18.977,101.28,19.089,101.227,19.212,101.198,19.328,101.221,19.487,101.212,19.548,101.155,19.579,100.967,19.611,100.906,19.605,100.858,19.585,100.807,19.542,100.744,19.515,100.625,19.5,100.514,19.553,100.42,19.644,100.398,19.756,100.466,19.889,100.515,19.996,100.543,20.089,100.54,20.132,100.52,20.178,100.492,20.184,100.432,20.241,100.373,20.34,100.318,20.386,100.266,20.377,100.218,20.34,100.174,20.273,100.14,20.245,100.115,20.258,100.122,20.317,100.13,20.372,100.184,20.589,100.249,20.73,100.326,20.796,100.407,20.823,100.493,20.813,100.565,20.825,100.623,20.86,100.618,20.879,100.549,20.884,100.522,20.922,100.536,20.992,100.567,21.038,100.614,21.059,100.659,21.13,100.703,21.251,100.757,21.313,100.82,21.314,100.928,21.366,101.08,21.469,101.139,21.567,101.197,21.522,101.175,21.408,101.206,21.383,101.22,21.342,101.212,21.278,101.224,21.224,101.248,21.197,101.281,21.184,101.444,21.231,101.542,21.234,101.584,21.204,101.622,21.184,101.669,21.17,101.705,21.15,101.728,21.156,101.783,21.204,101.801,21.213,101.802,21.236,101.763,21.279,101.723,21.315,101.724,21.395,101.743,21.534,101.747,21.606,101.744,21.778,101.737,21.827,101.7,21.882,101.603,21.99,101.576,22.055,101.56,22.121,101.562,22.162,101.537,22.21,101.525,22.254,101.568,22.276,101.62,22.327,101.646,22.405,101.671,22.462,101.708,22.487,101.739,22.495,101.76,22.49,101.842,22.388,101.945,22.439,102.024,22.439,102.092,22.412,102.127,22.379]]]},{"n":"Kyrgyzstan","c":"KG","r":4,"ml":3,"lx":74.53,"ly":41.67,"b":[69.229,39.208,80.246,43.24],"p":[[[70.958,40.239,70.991,40.255,71.095,40.271,71.305,40.287,71.376,40.275,71.457,40.242,71.52,40.209,71.58,40.21,71.63,40.217,71.651,40.208,71.667,40.179,71.692,40.152,71.773,40.188,71.845,40.234,71.903,40.241,71.956,40.259,71.971,40.29,72.013,40.341,72.131,40.439,72.193,40.454,72.233,40.454,72.235,40.439,72.254,40.424,72.358,40.402,72.389,40.427,72.406,40.463,72.37,40.52,72.369,40.543,72.383,40.565,72.402,40.578,72.567,40.524,72.604,40.525,72.68,40.556,72.749,40.609,72.774,40.65,73.113,40.786,73.137,40.811,73.132,40.829,72.99,40.86,72.926,40.842,72.867,40.842,72.831,40.862,72.658,40.87,72.62,40.884,72.506,40.982,72.427,41.019,72.364,41.043,72.295,41.04,72.213,41.014,72.187,41.026,72.181,41.067,72.181,41.118,72.164,41.174,72.115,41.187,72.052,41.165,71.958,41.187,71.879,41.195,71.858,41.311,71.826,41.361,71.792,41.413,71.758,41.428,71.701,41.454,71.697,41.516,71.685,41.533,71.665,41.541,71.638,41.534,71.602,41.503,71.62,41.435,71.606,41.367,71.586,41.333,71.546,41.308,71.5,41.307,71.421,41.342,71.408,41.136,71.393,41.123,71.299,41.152,71.223,41.14,71.111,41.153,71.026,41.187,70.963,41.196,70.86,41.225,70.782,41.263,70.734,41.401,70.689,41.45,70.646,41.46,70.471,41.413,70.408,41.45,70.29,41.497,70.201,41.514,70.177,41.54,70.181,41.571,70.455,41.725,70.563,41.831,70.631,41.875,70.728,41.905,70.803,41.923,70.842,42.02,70.857,42.031,70.91,42.038,71.032,42.078,71.229,42.163,71.232,42.186,71.213,42.206,71.13,42.25,71.036,42.285,70.979,42.267,70.947,42.249,70.893,42.294,70.893,42.34,70.952,42.419,71.002,42.459,71.023,42.535,71.094,42.587,71.167,42.667,71.257,42.734,71.422,42.783,71.514,42.767,71.601,42.779,71.735,42.819,71.761,42.821,71.817,42.822,72.162,42.761,72.276,42.758,72.543,42.678,72.666,42.66,72.753,42.638,72.792,42.603,72.855,42.561,73.191,42.527,73.283,42.504,73.316,42.467,73.412,42.42,73.493,42.409,73.422,42.594,73.45,42.703,73.556,43.003,73.612,43.048,73.719,43.088,73.886,43.133,73.949,43.195,74.086,43.189,74.146,43.194,74.187,43.205,74.209,43.24,74.364,43.179,74.622,43.056,74.818,42.978,75.048,42.904,75.366,42.837,75.636,42.815,75.682,42.83,75.79,42.933,75.84,42.938,75.932,42.929,76.218,42.924,76.509,42.919,76.646,42.929,76.944,42.971,76.988,42.974,77.057,42.971,77.236,42.913,77.369,42.904,77.459,42.905,77.512,42.9,77.622,42.902,77.802,42.895,78.023,42.858,78.29,42.864,78.376,42.871,78.524,42.865,78.642,42.829,78.792,42.791,78.885,42.775,78.948,42.767,79.06,42.764,79.127,42.776,79.165,42.759,79.203,42.666,79.296,42.605,79.368,42.547,79.428,42.483,79.49,42.458,79.598,42.457,79.803,42.438,79.921,42.413,80.071,42.303,80.209,42.19,80.229,42.13,80.246,42.06,80.235,42.043,80.216,42.032,79.91,42.015,79.84,41.996,79.766,41.899,79.504,41.821,79.354,41.781,79.294,41.783,79.148,41.719,78.743,41.56,78.543,41.46,78.443,41.418,78.362,41.372,78.349,41.325,78.346,41.281,78.123,41.076,77.956,41.051,77.815,41.056,77.719,41.024,77.582,40.993,77.284,41.014,77.182,41.011,76.987,41.039,76.908,41.024,76.824,40.982,76.708,40.818,76.661,40.78,76.64,40.742,76.622,40.662,76.578,40.578,76.521,40.511,76.48,40.45,76.396,40.39,76.319,40.352,76.258,40.431,76.206,40.408,76.157,40.376,76.062,40.388,76.004,40.371,75.872,40.303,75.677,40.306,75.656,40.329,75.617,40.517,75.583,40.605,75.556,40.625,75.521,40.628,75.241,40.48,75.111,40.454,75.004,40.45,74.866,40.494,74.835,40.483,74.811,40.459,74.801,40.429,74.842,40.345,74.83,40.329,74.768,40.33,74.68,40.311,74.613,40.272,74.412,40.137,74.243,40.092,74.085,40.074,74.021,40.059,73.992,40.043,73.939,39.979,73.885,39.878,73.856,39.829,73.835,39.8,73.84,39.763,73.883,39.715,73.915,39.606,73.907,39.579,73.873,39.533,73.823,39.489,73.716,39.462,73.632,39.449,73.576,39.458,73.47,39.461,73.387,39.443,73.336,39.412,73.235,39.375,73.109,39.362,72.949,39.357,72.872,39.36,72.64,39.386,72.563,39.377,72.49,39.357,72.358,39.337,72.287,39.274,72.25,39.216,72.23,39.208,72.147,39.261,72.084,39.311,72.043,39.352,71.991,39.351,71.806,39.276,71.779,39.278,71.726,39.307,71.735,39.378,71.732,39.423,71.673,39.447,71.546,39.453,71.503,39.479,71.506,39.517,71.517,39.554,71.503,39.582,71.47,39.604,71.404,39.598,71.329,39.569,71.273,39.535,71.203,39.52,71.118,39.514,71.065,39.493,71.005,39.412,70.799,39.395,70.733,39.413,70.679,39.471,70.608,39.564,70.568,39.576,70.501,39.587,70.392,39.582,70.245,39.543,70.209,39.575,70.171,39.584,70.137,39.558,70.102,39.561,69.956,39.553,69.772,39.557,69.667,39.575,69.599,39.574,69.463,39.532,69.392,39.532,69.298,39.525,69.28,39.666,69.229,39.761,69.245,39.827,69.279,39.918,69.307,39.969,69.365,39.947,69.432,39.91,69.476,39.92,69.488,39.95,69.471,39.991,69.469,40.021,69.494,40.06,69.53,40.097,69.765,40.158,69.967,40.202,70.071,40.173,70.274,40.105,70.379,40.07,70.451,40.049,70.515,39.95,70.557,39.954,70.599,39.975,70.624,39.999,70.644,40.083,70.739,40.131,70.946,40.188,70.961,40.221,70.958,40.239],[71.206,39.893,71.216,39.907,71.179,39.98,71.229,40.048,71.13,40.06,71.08,40.08,71.024,40.149,71.005,40.152,70.976,40.133,70.961,40.088,70.974,40.039,71.014,40.006,71.041,39.995,71.045,39.993,71.044,39.976,71.012,39.895,71.064,39.885,71.156,39.883,71.206,39.893],[71.78,39.95,71.79,39.995,71.765,39.993,71.737,39.981,71.681,39.969,71.669,39.946,71.706,39.917,71.753,39.907,71.78,39.95],[70.702,39.825,70.698,39.846,70.664,39.855,70.567,39.867,70.498,39.882,70.483,39.883,70.489,39.863,70.519,39.828,70.56,39.791,70.612,39.787,70.702,39.825]]]},{"n":"Kuwait","c":"KW","r":6,"ml":5,"lx":47.31,"ly":29.41,"b":[46.531,28.533,48.442,30.097],"p":[[[48.275,29.624,48.218,29.602,48.18,29.611,48.143,29.665,48.081,29.799,48.115,29.849,48.113,29.87,48.12,29.886,48.139,29.897,48.159,29.96,48.185,29.979,48.228,29.936,48.348,29.783,48.347,29.72,48.34,29.695,48.275,29.624]],[[48.442,28.543,48.269,28.541,48.05,28.538,47.872,28.535,47.671,28.533,47.583,28.628,47.553,28.732,47.521,28.838,47.433,28.99,47.139,29.026,46.982,29.046,46.725,29.075,46.531,29.096,46.694,29.26,46.769,29.347,46.906,29.538,46.976,29.673,47.044,29.823,47.102,29.94,47.114,29.961,47.148,30.001,47.223,30.042,47.331,30.08,47.515,30.096,47.644,30.097,47.673,30.096,47.754,30.077,47.979,29.983,47.974,29.946,48.006,29.836,48.077,29.716,48.136,29.618,48.143,29.572,48.089,29.579,48.048,29.598,47.97,29.617,47.817,29.487,47.725,29.417,47.723,29.393,47.845,29.366,47.935,29.367,47.998,29.386,48.051,29.355,48.086,29.275,48.1,29.211,48.184,28.979,48.253,28.901,48.339,28.763,48.371,28.692,48.39,28.632,48.442,28.543]]]},{"n":"Kosovo","c":"XK","r":6,"ml":5,"lx":20.86,"ly":42.59,"b":[20.029,41.854,21.753,43.261],"p":[[[20.344,42.828,20.469,42.858,20.487,42.879,20.458,42.925,20.475,42.953,20.624,43.034,20.649,43.071,20.658,43.1,20.638,43.13,20.61,43.178,20.623,43.199,20.701,43.226,20.763,43.259,20.801,43.261,20.824,43.238,20.824,43.214,20.844,43.173,20.891,43.152,20.968,43.116,21.057,43.092,21.127,43.043,21.223,42.956,21.237,42.913,21.323,42.875,21.403,42.832,21.391,42.751,21.663,42.681,21.724,42.682,21.753,42.67,21.752,42.652,21.731,42.595,21.619,42.423,21.61,42.387,21.53,42.35,21.519,42.328,21.542,42.281,21.563,42.248,21.561,42.248,21.39,42.22,21.332,42.187,21.298,42.13,21.287,42.1,21.256,42.1,21.206,42.129,21.142,42.175,21.06,42.171,20.778,42.071,20.75,42.018,20.744,41.904,20.725,41.874,20.695,41.854,20.579,41.866,20.566,41.874,20.581,41.917,20.575,42.013,20.523,42.171,20.485,42.223,20.408,42.275,20.348,42.309,20.241,42.339,20.186,42.426,20.104,42.525,20.064,42.547,20.07,42.557,20.089,42.632,20.066,42.686,20.029,42.732,20.054,42.76,20.13,42.76,20.193,42.755,20.215,42.799,20.344,42.828]]]},{"n":"Kiribati","c":"KI","r":6,"ml":5,"lx":-157.38,"ly":1.82,"b":[-174.541,-11.457,174.779,3.924],"p":[[[172.97,3.129,172.906,3.096,172.887,3.101,172.963,3.149,172.962,3.143,172.97,3.129]],[[172.844,3.051,172.77,3.013,172.75,3.033,172.777,3.034,172.827,3.071,172.887,3.074,172.88,3.054,172.844,3.051]],[[173.019,1.846,173.024,1.823,172.967,1.885,172.933,1.926,172.935,1.944,172.95,1.933,172.969,1.913,172.982,1.897,173.019,1.846]],[[173.029,1.717,172.993,1.713,173.02,1.727,173.028,1.747,173.024,1.809,173.038,1.804,173.043,1.779,173.045,1.742,173.029,1.717]],[[173.033,1.013,173.087,0.973,173.079,0.946,173.061,0.915,172.991,0.835,172.97,0.843,173.039,0.915,173.065,0.963,173.026,0.999,173.01,0.991,173.004,0.991,172.99,1.025,173.004,1.025,173.033,1.013]],[[174.509,-0.802,174.476,-0.829,174.464,-0.804,174.48,-0.774,174.453,-0.647,174.408,-0.63,174.381,-0.592,174.394,-0.592,174.439,-0.627,174.475,-0.642,174.495,-0.726,174.509,-0.802]],[[174.773,-1.212,174.779,-1.263,174.756,-1.256,174.748,-1.236,174.741,-1.185,174.717,-1.134,174.744,-1.147,174.767,-1.187,174.773,-1.212]],[[173.038,1.342,173.011,1.338,173.029,1.359,173.143,1.381,173.153,1.388,173.172,1.375,173.171,1.363,173.163,1.358,173.106,1.357,173.062,1.346,173.038,1.342]],[[169.551,-0.874,169.542,-0.876,169.523,-0.866,169.526,-0.853,169.539,-0.847,169.555,-0.857,169.551,-0.874]],[[-172.215,-4.511,-172.208,-4.518,-172.194,-4.516,-172.181,-4.515,-172.189,-4.522,-172.215,-4.524,-172.228,-4.507,-172.212,-4.494,-172.198,-4.492,-172.196,-4.495,-172.197,-4.5,-172.204,-4.5,-172.215,-4.503,-172.215,-4.511]],[[-171.085,-3.135,-171.09,-3.143,-171.097,-3.137,-171.092,-3.125,-171.088,-3.115,-171.081,-3.12,-171.085,-3.135]],[[-171.233,-4.463,-171.243,-4.468,-171.255,-4.467,-171.262,-4.46,-171.262,-4.449,-171.252,-4.442,-171.239,-4.444,-171.232,-4.454,-171.233,-4.463]],[[-174.513,-4.675,-174.501,-4.688,-174.501,-4.695,-174.507,-4.694,-174.524,-4.69,-174.529,-4.682,-174.517,-4.687,-174.511,-4.686,-174.523,-4.674,-174.533,-4.665,-174.541,-4.662,-174.541,-4.657,-174.531,-4.659,-174.513,-4.675]],[[-154.956,-4.088,-154.959,-4.094,-154.971,-4.086,-154.995,-4.071,-155.015,-4.055,-155.015,-4.048,-154.987,-4.039,-154.951,-4.031,-154.943,-4.042,-154.95,-4.056,-154.956,-4.088]],[[-151.783,-11.441,-151.791,-11.457,-151.807,-11.451,-151.816,-11.431,-151.819,-11.409,-151.813,-11.392,-151.803,-11.393,-151.791,-11.414,-151.783,-11.441]],[[-155.864,-5.627,-155.887,-5.632,-155.914,-5.632,-155.928,-5.619,-155.929,-5.608,-155.919,-5.608,-155.911,-5.609,-155.872,-5.611,-155.862,-5.619,-155.864,-5.627]],[[-157.342,1.856,-157.176,1.74,-157.246,1.732,-157.42,1.788,-157.579,1.902,-157.531,1.927,-157.508,1.886,-157.436,1.847,-157.393,1.928,-157.365,1.946,-157.492,2.029,-157.442,2.025,-157.322,1.969,-157.342,1.856]],[[-159.339,3.924,-159.259,3.839,-159.275,3.797,-159.332,3.8,-159.359,3.815,-159.314,3.823,-159.306,3.838,-159.327,3.863,-159.354,3.881,-159.373,3.881,-159.378,3.847,-159.409,3.873,-159.391,3.9,-159.369,3.917,-159.339,3.924]],[[-171.698,-2.766,-171.665,-2.786,-171.64,-2.811,-171.628,-2.847,-171.628,-2.856,-171.647,-2.856,-171.671,-2.844,-171.687,-2.83,-171.696,-2.826,-171.698,-2.822,-171.678,-2.825,-171.655,-2.84,-171.639,-2.847,-171.64,-2.829,-171.66,-2.799,-171.673,-2.788,-171.688,-2.779,-171.706,-2.773,-171.718,-2.779,-171.725,-2.781,-171.728,-2.774,-171.725,-2.768,-171.719,-2.761,-171.698,-2.766]]]},{"n":"Kenya","c":"KE","r":2,"ml":1.7,"lx":37.91,"ly":0.55,"b":[33.9,-4.692,41.884,5.492],"p":[[[40.994,-2.158,40.957,-2.167,40.976,-2.11,41.086,-2.037,41.131,-2.053,41.139,-2.07,41.137,-2.085,41.118,-2.1,40.994,-2.158]],[[33.903,-1.002,33.9,-0.832,33.924,-0.398,33.921,-0.017,33.943,0.174,34.037,0.295,34.081,0.382,34.112,0.505,34.161,0.605,34.273,0.686,34.293,0.731,34.411,0.867,34.482,1.042,34.535,1.102,34.602,1.156,34.649,1.185,34.727,1.214,34.788,1.231,34.799,1.245,34.804,1.273,34.784,1.381,34.81,1.417,34.851,1.489,34.898,1.556,34.941,1.599,34.965,1.643,34.976,1.72,34.978,1.774,34.978,1.862,34.964,2.062,34.914,2.23,34.883,2.418,34.906,2.48,34.866,2.59,34.847,2.596,34.814,2.62,34.773,2.723,34.742,2.818,34.723,2.842,34.589,2.925,34.523,3.12,34.448,3.163,34.407,3.358,34.399,3.413,34.442,3.606,34.438,3.651,34.393,3.692,34.267,3.733,34.165,3.813,34.178,3.841,34.186,3.87,34.132,3.889,33.976,4.22,34.177,4.419,34.38,4.621,34.64,4.875,34.878,5.11,35.084,5.312,35.268,5.492,35.264,5.458,35.265,5.412,35.288,5.384,35.325,5.365,35.378,5.385,35.424,5.413,35.469,5.419,35.745,5.344,35.791,5.279,35.788,5.208,35.8,5.157,35.779,5.106,35.756,4.95,35.763,4.808,35.846,4.703,35.92,4.62,35.979,4.504,36.022,4.468,36.082,4.45,36.272,4.445,36.553,4.437,36.824,4.43,36.848,4.427,36.906,4.411,37.155,4.255,37.383,4.111,37.575,3.986,37.763,3.865,37.945,3.747,38.086,3.649,38.225,3.619,38.452,3.605,38.608,3.6,38.753,3.559,38.968,3.521,39.128,3.501,39.225,3.479,39.494,3.456,39.539,3.469,39.658,3.578,39.79,3.754,39.842,3.851,40.014,3.948,40.316,4.083,40.529,4.178,40.765,4.273,40.873,4.19,41.021,4.057,41.087,3.992,41.14,3.963,41.221,3.944,41.319,3.943,41.372,3.946,41.482,3.963,41.738,3.979,41.884,3.978,41.761,3.802,41.613,3.59,41.342,3.202,41.135,2.997,40.979,2.842,40.964,2.815,40.965,2.642,40.967,2.221,40.97,1.378,40.973,0.535,40.977,-0.307,40.978,-0.729,40.979,-0.87,41.116,-1.047,41.25,-1.221,41.427,-1.45,41.522,-1.572,41.538,-1.613,41.533,-1.695,41.387,-1.867,41.267,-1.945,41.107,-1.982,41.059,-1.975,40.996,-1.951,40.971,-1.992,40.952,-2.056,40.917,-2.042,40.89,-2.024,40.906,-2.137,40.922,-2.194,40.898,-2.27,40.82,-2.336,40.813,-2.392,40.644,-2.539,40.404,-2.556,40.279,-2.629,40.222,-2.688,40.18,-2.819,40.195,-3.019,40.128,-3.173,40.115,-3.251,39.992,-3.351,39.937,-3.442,39.896,-3.536,39.861,-3.577,39.819,-3.786,39.761,-3.913,39.746,-3.955,39.732,-3.993,39.687,-4.068,39.658,-4.119,39.637,-4.153,39.491,-4.478,39.377,-4.625,39.288,-4.609,39.228,-4.666,39.222,-4.692,39.19,-4.677,39.115,-4.624,38.962,-4.513,38.808,-4.402,38.655,-4.292,38.501,-4.181,38.348,-4.071,38.194,-3.96,38.041,-3.85,37.887,-3.739,37.797,-3.674,37.757,-3.636,37.726,-3.56,37.711,-3.541,37.67,-3.517,37.622,-3.512,37.608,-3.497,37.609,-3.46,37.625,-3.407,37.682,-3.306,37.688,-3.246,37.677,-3.178,37.659,-3.07,37.644,-3.045,37.542,-2.989,37.329,-2.87,37.116,-2.751,36.903,-2.632,36.689,-2.513,36.476,-2.394,36.263,-2.275,36.05,-2.156,35.837,-2.037,35.624,-1.918,35.411,-1.799,35.197,-1.68,34.984,-1.561,34.771,-1.442,34.558,-1.323,34.345,-1.204,34.132,-1.085,34.052,-1.04,33.979,-1.002,33.903,-1.002]]]},{"n":"Kazakhstan","c":"KZ","r":3,"ml":2.7,"lx":68.69,"ly":49.05,"b":[46.609,40.609,87.323,55.39],"p":[[[50.184,44.855,50.149,44.826,50.095,44.831,49.995,44.937,50.023,45.045,50.059,45.067,50.11,45.082,50.117,45.058,50.045,45.01,50.039,44.949,50.098,44.882,50.184,44.855]],[[50.312,44.972,50.277,44.959,50.256,45.022,50.295,45.076,50.35,45.083,50.331,44.998,50.312,44.972]],[[52.682,45.412,52.665,45.401,52.598,45.428,52.554,45.474,52.609,45.528,52.66,45.518,52.693,45.461,52.682,45.412]],[[70.947,42.249,70.86,42.207,70.765,42.194,70.715,42.169,70.663,42.107,70.613,42.055,70.584,42.036,70.54,42.039,70.489,42.08,70.416,42.079,70.329,42.028,70.226,41.946,70.096,41.821,69.96,41.754,69.788,41.697,69.664,41.672,69.565,41.629,69.401,41.542,69.368,41.491,69.249,41.46,69.154,41.425,69.065,41.367,69.043,41.264,68.987,41.205,68.851,41.124,68.737,41.042,68.663,40.962,68.584,40.876,68.559,40.829,68.557,40.765,68.594,40.711,68.601,40.66,68.573,40.623,68.496,40.609,68.415,40.619,68.292,40.656,68.16,40.722,68.112,40.754,68.048,40.809,68.057,40.861,68.09,40.96,68.113,41.029,68.059,41.061,68.02,41.096,67.991,41.13,67.936,41.197,67.866,41.18,67.805,41.164,67.735,41.187,67.528,41.177,67.372,41.17,67.225,41.162,67.039,41.153,66.814,41.142,66.75,41.157,66.71,41.179,66.669,41.271,66.645,41.349,66.602,41.494,66.573,41.607,66.538,41.741,66.515,41.889,66.499,41.995,66.329,41.998,66.193,42.001,66.01,42.005,66.011,42.089,66.013,42.194,66.016,42.315,66.05,42.473,66.063,42.605,66.079,42.767,66.089,42.873,66.1,42.991,66.006,42.955,65.901,42.915,65.803,42.877,65.736,42.972,65.67,43.065,65.571,43.205,65.496,43.311,65.367,43.372,65.271,43.418,65.171,43.494,65.085,43.574,65.003,43.649,64.905,43.715,64.812,43.694,64.706,43.653,64.604,43.613,64.496,43.572,64.443,43.551,64.318,43.559,64.209,43.566,64.013,43.578,63.848,43.588,63.68,43.599,63.445,43.613,63.207,43.628,63.048,43.608,62.846,43.584,62.634,43.558,62.459,43.537,62.238,43.51,62.072,43.489,61.99,43.492,61.888,43.577,61.723,43.714,61.624,43.796,61.526,43.877,61.385,43.994,61.271,44.082,61.161,44.169,61.097,44.248,61.065,44.348,61.008,44.394,60.879,44.455,60.741,44.521,60.603,44.587,60.465,44.652,60.327,44.718,60.188,44.784,60.05,44.85,59.912,44.916,59.774,44.981,59.636,45.047,59.498,45.113,59.36,45.179,59.221,45.244,59.083,45.31,58.945,45.376,58.807,45.442,58.669,45.508,58.555,45.555,58.449,45.543,58.291,45.509,58.125,45.474,57.961,45.44,57.667,45.377,57.477,45.337,57.329,45.304,57.172,45.268,56.965,45.221,56.792,45.181,56.589,45.135,56.409,45.094,56.258,45.059,56.1,45.023,55.976,44.995,55.976,44.765,55.976,44.536,55.976,44.306,55.976,44.077,55.976,43.847,55.976,43.618,55.976,43.388,55.977,43.159,55.977,42.929,55.977,42.7,55.977,42.47,55.977,42.24,55.977,42.011,55.977,41.781,55.977,41.552,55.977,41.322,55.935,41.324,55.839,41.311,55.679,41.279,55.545,41.263,55.487,41.272,55.434,41.296,55.388,41.347,55.32,41.408,55.25,41.458,55.162,41.56,55.102,41.639,54.952,41.81,54.932,41.864,54.904,41.919,54.854,41.965,54.678,42.078,54.473,42.18,54.272,42.28,54.215,42.304,54.121,42.335,54.005,42.336,53.926,42.33,53.685,42.297,53.501,42.258,53.25,42.206,53.056,42.148,53.013,42.131,52.871,42.061,52.697,41.944,52.494,41.78,52.468,41.886,52.459,42.048,52.462,42.101,52.517,42.237,52.573,42.331,52.618,42.428,52.638,42.556,52.597,42.76,52.55,42.805,52.494,42.82,52.434,42.824,52.324,42.816,52.273,42.8,52.184,42.869,52.076,42.88,52.019,42.861,51.961,42.851,51.898,42.87,51.844,42.91,51.811,42.954,51.785,43.004,51.7,43.104,51.616,43.158,51.514,43.171,51.348,43.167,51.295,43.174,51.292,43.231,51.313,43.356,51.314,43.421,51.302,43.482,51.274,43.533,51.239,43.577,51.14,43.649,51.065,43.75,50.94,43.959,50.831,44.193,50.783,44.228,50.685,44.265,50.472,44.295,50.331,44.325,50.276,44.355,50.253,44.406,50.253,44.462,50.265,44.527,50.297,44.582,50.409,44.624,50.652,44.633,50.86,44.629,51.049,44.53,51.111,44.508,51.177,44.501,51.311,44.532,51.377,44.541,51.544,44.531,51.494,44.578,51.431,44.602,51.366,44.6,51.31,44.619,51.218,44.709,51.058,44.812,51.021,44.854,51.009,44.922,51.04,44.98,51.154,45.04,51.25,45.122,51.294,45.23,51.333,45.28,51.416,45.358,51.54,45.343,51.733,45.399,52.049,45.388,52.427,45.405,52.531,45.399,52.772,45.344,52.911,45.32,53.079,45.308,53.2,45.332,53.086,45.407,52.838,45.497,52.774,45.573,52.888,45.78,53.042,45.968,53.135,46.192,53.109,46.414,53.064,46.475,53.079,46.547,53.132,46.608,53.17,46.669,53.138,46.742,53.069,46.856,53.035,46.893,52.916,46.954,52.678,46.957,52.483,46.991,52.42,46.964,52.385,46.922,52.34,46.895,52.189,46.84,52.138,46.829,52.086,46.84,52.011,46.902,51.945,46.895,51.745,46.934,51.65,47.018,51.615,47.03,51.291,47.097,51.178,47.11,50.92,47.041,50.733,46.952,50.68,46.939,50.583,46.882,50.528,46.873,50.472,46.883,50.419,46.879,50.306,46.795,50.102,46.696,50,46.634,49.886,46.596,49.761,46.571,49.632,46.568,49.584,46.545,49.437,46.537,49.347,46.519,49.344,46.486,49.362,46.41,49.286,46.437,49.206,46.386,49.232,46.337,49.184,46.349,48.959,46.442,48.774,46.508,48.61,46.566,48.586,46.577,48.541,46.606,48.509,46.65,48.502,46.699,48.519,46.734,48.558,46.757,48.605,46.766,48.647,46.759,48.694,46.737,48.776,46.71,48.884,46.705,48.95,46.726,48.959,46.775,48.832,46.955,48.714,47.1,48.601,47.262,48.553,47.321,48.413,47.456,48.276,47.59,48.167,47.709,48.11,47.745,47.935,47.761,47.6,47.79,47.482,47.804,47.387,47.769,47.292,47.741,47.202,47.792,47.131,47.877,47.093,47.948,47.112,48.02,47.119,48.127,47.065,48.232,47.004,48.284,46.853,48.324,46.661,48.412,46.609,48.574,46.703,48.806,46.853,48.97,46.962,49.038,47.014,49.098,47.031,49.15,47.018,49.2,46.953,49.253,46.853,49.304,46.802,49.367,46.823,49.502,46.89,49.697,46.992,49.853,47.13,49.939,47.248,50.001,47.295,50.058,47.298,50.14,47.295,50.217,47.326,50.274,47.376,50.318,47.429,50.358,47.504,50.403,47.6,50.414,47.706,50.378,47.85,50.282,48.061,50.094,48.181,49.97,48.225,49.932,48.335,49.858,48.434,49.829,48.6,49.875,48.759,49.928,48.81,49.962,48.843,50.013,48.818,50.1,48.785,50.156,48.749,50.228,48.7,50.354,48.666,50.55,48.625,50.613,48.655,50.62,48.735,50.607,48.808,50.601,48.914,50.645,49.059,50.726,49.323,50.852,49.379,50.935,49.425,51.027,49.498,51.084,49.666,51.102,49.822,51.132,49.932,51.197,50.105,51.255,50.247,51.29,50.309,51.322,50.354,51.37,50.516,51.506,50.644,51.589,50.756,51.675,50.794,51.729,50.882,51.719,51.018,51.682,51.163,51.647,51.27,51.594,51.291,51.54,51.301,51.497,51.345,51.475,51.396,51.471,51.473,51.482,51.609,51.484,51.775,51.554,52.007,51.673,52.219,51.709,52.331,51.681,52.423,51.594,52.496,51.512,52.571,51.482,52.618,51.481,52.635,51.48,52.728,51.498,52.735,51.498,52.821,51.495,52.903,51.467,53.038,51.464,53.227,51.485,53.247,51.494,53.338,51.482,53.449,51.445,53.535,51.4,53.688,51.252,53.776,51.214,53.957,51.161,54.042,51.115,54.14,51.041,54.191,50.996,54.298,50.914,54.421,50.78,54.443,50.674,54.471,50.584,54.517,50.541,54.555,50.536,54.596,50.551,54.636,50.592,54.65,50.66,54.638,50.781,54.606,50.88,54.566,50.911,54.546,50.946,54.573,50.99,54.642,51.012,54.727,50.998,54.868,50.941,55.015,50.87,55.195,50.745,55.361,50.665,55.542,50.602,55.686,50.583,55.798,50.602,55.929,50.654,56.05,50.714,56.104,50.776,56.144,50.845,56.326,50.936,56.491,51.02,56.567,51.004,56.62,50.981,56.79,51.032,56.85,51.046,57.012,51.065,57.179,51.036,57.313,50.947,57.442,50.889,57.558,50.896,57.654,50.925,57.717,50.981,57.765,51.047,57.829,51.089,57.839,51.092,58.045,51.069,58.175,51.072,58.188,51.082,58.359,51.064,58.547,50.971,58.665,50.868,58.814,50.737,58.884,50.694,58.985,50.676,59.064,50.668,59.171,50.648,59.452,50.62,59.495,50.604,59.524,50.583,59.498,50.511,59.523,50.493,59.751,50.544,59.812,50.582,59.888,50.69,59.955,50.799,60.005,50.84,60.059,50.85,60.112,50.834,60.187,50.77,60.288,50.704,60.425,50.679,60.508,50.669,60.638,50.664,60.942,50.696,61.227,50.775,61.389,50.861,61.465,50.99,61.512,51.137,61.585,51.23,61.555,51.325,61.411,51.415,61.363,51.442,61.015,51.492,60.993,51.529,60.974,51.537,60.63,51.617,60.465,51.651,60.418,51.704,60.388,51.773,60.28,51.835,60.067,51.891,60.03,51.933,60.066,51.976,60.234,52.025,60.425,52.126,60.499,52.146,60.67,52.151,60.828,52.233,60.938,52.281,60.995,52.337,60.979,52.395,60.821,52.57,60.774,52.676,60.802,52.745,60.893,52.819,60.945,52.86,61.007,52.933,61.047,52.972,61.207,52.989,61.401,52.996,61.534,52.979,61.719,52.969,61.889,52.956,61.974,52.944,62.037,52.966,62.083,53.005,62.081,53.057,62.015,53.108,61.766,53.174,61.66,53.228,61.576,53.222,61.437,53.239,61.311,53.275,61.199,53.287,61.163,53.337,61.186,53.406,61.229,53.446,61.312,53.466,61.401,53.456,61.499,53.485,61.527,53.502,61.535,53.523,61.519,53.554,61.474,53.58,61.41,53.587,61.336,53.565,61.248,53.551,61.099,53.583,60.979,53.622,60.986,53.657,61.074,53.71,61.113,53.753,61.113,53.813,61.113,53.882,61.144,53.964,61.231,54.019,61.334,54.049,61.598,53.995,61.929,53.946,61.986,53.954,62.002,53.98,62.04,54.003,62.499,54.013,62.588,54.044,62.633,54.069,63.074,54.105,63.127,54.139,63.191,54.171,63.293,54.17,63.414,54.183,63.582,54.222,63.701,54.243,63.721,54.245,63.847,54.236,64.004,54.267,64.037,54.28,64.063,54.303,64.199,54.347,64.461,54.384,64.525,54.362,64.65,54.352,64.809,54.369,64.927,54.397,64.995,54.369,65.088,54.34,65.158,54.364,65.192,54.441,65.237,54.516,65.316,54.552,65.378,54.564,65.434,54.593,65.477,54.623,65.708,54.619,65.914,54.693,65.955,54.66,66.223,54.667,66.555,54.715,66.754,54.738,67.098,54.788,67.257,54.829,67.485,54.854,67.693,54.872,67.83,54.944,67.94,54.954,68.074,54.96,68.156,54.977,68.209,55.003,68.244,55.052,68.225,55.115,68.206,55.161,68.302,55.187,68.438,55.194,68.525,55.205,68.713,55.308,68.843,55.358,68.977,55.39,69.247,55.373,69.493,55.357,69.74,55.307,69.87,55.246,69.982,55.199,70.087,55.177,70.182,55.162,70.293,55.184,70.371,55.212,70.417,55.253,70.486,55.282,70.738,55.305,70.79,55.261,70.91,55.128,70.992,54.95,71.126,54.715,71.186,54.599,71.159,54.539,71.16,54.455,71.152,54.364,71.053,54.26,71.093,54.212,71.336,54.158,71.677,54.178,71.887,54.221,72.004,54.206,72.066,54.232,72.105,54.308,72.186,54.326,72.269,54.272,72.329,54.181,72.387,54.123,72.383,54.054,72.404,53.964,72.447,53.942,72.53,53.976,72.586,53.996,72.599,54.023,72.576,54.056,72.564,54.09,72.583,54.122,72.622,54.134,72.741,54.125,72.914,54.107,73.119,53.981,73.23,53.958,73.277,53.956,73.381,53.963,73.506,53.999,73.59,54.045,73.618,54.067,73.666,54.063,73.712,54.042,73.716,53.996,73.679,53.929,73.554,53.868,73.399,53.811,73.306,53.707,73.286,53.598,73.327,53.543,73.362,53.506,73.372,53.454,73.407,53.448,73.47,53.469,73.643,53.576,73.731,53.603,73.859,53.62,74.069,53.611,74.21,53.576,74.277,53.528,74.352,53.488,74.403,53.504,74.429,53.551,74.43,53.604,74.452,53.647,74.681,53.754,74.834,53.826,74.887,53.834,74.989,53.819,75.052,53.827,75.22,53.894,75.377,53.97,75.392,54.022,75.398,54.069,75.437,54.09,75.657,54.106,75.693,54.115,75.881,54.168,76.141,54.259,76.267,54.312,76.496,54.336,76.539,54.351,76.616,54.387,76.759,54.437,76.837,54.442,76.789,54.322,76.703,54.182,76.655,54.145,76.422,54.152,76.422,54.114,76.459,54.055,76.485,54.023,76.513,53.993,76.576,53.943,76.821,53.823,77.132,53.67,77.469,53.499,77.704,53.379,77.799,53.317,77.86,53.269,78.033,53.095,78.198,52.93,78.475,52.638,78.721,52.357,78.992,52.047,79.149,51.868,79.469,51.493,79.554,51.378,79.716,51.16,79.86,50.955,79.986,50.775,80.066,50.758,80.072,50.807,80.086,50.84,80.127,50.858,80.22,50.912,80.27,50.925,80.345,50.919,80.424,50.946,80.452,50.998,80.434,51.093,80.421,51.136,80.448,51.183,80.491,51.202,80.551,51.217,80.605,51.224,80.65,51.277,80.735,51.293,80.813,51.283,80.877,51.281,80.934,51.243,80.966,51.19,81.027,51.186,81.127,51.191,81.141,51.147,81.112,51.072,81.078,51.015,81.071,50.969,81.125,50.946,81.319,50.966,81.388,50.956,81.41,50.91,81.438,50.871,81.452,50.824,81.431,50.771,81.466,50.74,81.634,50.739,81.752,50.764,81.934,50.766,82.098,50.711,82.212,50.719,82.326,50.742,82.494,50.728,82.612,50.771,82.693,50.826,82.719,50.869,82.761,50.893,82.919,50.893,83.019,50.897,83.093,50.961,83.16,50.989,83.274,50.995,83.357,50.995,83.581,50.936,83.718,50.887,83.86,50.818,83.945,50.775,84.002,50.677,84.099,50.605,84.176,50.521,84.195,50.437,84.258,50.288,84.323,50.239,84.401,50.239,84.499,50.219,84.607,50.202,84.839,50.091,84.924,50.088,84.989,50.061,85,50.01,84.975,49.951,85.001,49.894,85.076,49.822,85.137,49.751,85.21,49.665,85.233,49.616,85.292,49.599,85.372,49.624,85.498,49.605,85.88,49.557,85.934,49.55,85.974,49.499,86.03,49.503,86.093,49.505,86.181,49.499,86.242,49.546,86.292,49.588,86.418,49.638,86.522,49.708,86.61,49.769,86.675,49.777,86.729,49.749,86.731,49.696,86.665,49.657,86.614,49.61,86.626,49.563,86.714,49.559,86.812,49.488,86.953,49.322,87.001,49.287,87.071,49.255,87.148,49.24,87.234,49.216,87.297,49.148,87.323,49.086,87.23,49.106,87.049,49.11,86.938,49.098,86.886,49.091,86.808,49.05,86.753,49.009,86.729,48.939,86.758,48.861,86.718,48.697,86.664,48.636,86.549,48.529,86.483,48.505,86.373,48.486,86.266,48.455,86.056,48.424,85.83,48.408,85.749,48.385,85.692,48.312,85.652,48.251,85.626,48.204,85.562,48.052,85.526,47.916,85.562,47.746,85.588,47.558,85.587,47.494,85.642,47.397,85.67,47.338,85.657,47.255,85.577,47.188,85.53,47.101,85.485,47.064,85.355,47.047,85.233,47.036,85.111,46.961,85.012,46.909,84.858,46.843,84.786,46.831,84.746,46.864,84.72,46.939,84.667,46.972,84.592,46.975,84.532,46.976,84.339,46.996,84.215,46.995,84.122,46.979,84.016,46.971,83.833,46.998,83.714,47.021,83.634,47.043,83.444,47.109,83.193,47.187,83.09,47.209,83.029,47.186,83.02,47.141,83.004,47.033,82.975,46.966,82.8,46.624,82.692,46.387,82.555,46.159,82.512,46.006,82.43,45.812,82.348,45.672,82.315,45.595,82.312,45.564,82.327,45.52,82.452,45.472,82.583,45.443,82.612,45.424,82.626,45.374,82.621,45.293,82.597,45.216,82.559,45.155,82.521,45.125,82.479,45.124,82.397,45.162,82.323,45.206,82.267,45.219,82.123,45.195,81.989,45.162,81.945,45.161,81.867,45.182,81.79,45.226,81.759,45.311,81.692,45.349,81.602,45.311,81.335,45.246,81.04,45.169,80.853,45.129,80.78,45.136,80.635,45.127,80.509,45.105,80.415,45.075,80.228,45.034,80.059,45.006,79.95,44.944,79.872,44.884,79.875,44.861,79.932,44.825,79.997,44.797,80.128,44.804,80.255,44.808,80.361,44.77,80.455,44.746,80.482,44.715,80.455,44.684,80.401,44.677,80.381,44.655,80.391,44.627,80.355,44.552,80.336,44.438,80.355,44.327,80.365,44.223,80.359,44.171,80.355,44.097,80.396,44.047,80.432,43.952,80.496,43.892,80.593,43.685,80.651,43.564,80.704,43.427,80.665,43.353,80.668,43.31,80.73,43.274,80.757,43.204,80.786,43.162,80.778,43.119,80.751,43.102,80.617,43.128,80.507,43.086,80.39,43.043,80.375,43.02,80.371,42.996,80.383,42.974,80.451,42.936,80.544,42.912,80.539,42.873,80.424,42.856,80.25,42.797,80.202,42.734,80.165,42.666,80.162,42.626,80.179,42.518,80.206,42.399,80.255,42.274,80.259,42.235,80.233,42.208,80.209,42.19,80.071,42.303,79.921,42.413,79.803,42.438,79.598,42.457,79.49,42.458,79.428,42.483,79.368,42.547,79.296,42.605,79.203,42.666,79.165,42.759,79.127,42.776,79.06,42.764,78.948,42.767,78.885,42.775,78.792,42.791,78.642,42.829,78.524,42.865,78.376,42.871,78.29,42.864,78.023,42.858,77.802,42.895,77.622,42.902,77.512,42.9,77.459,42.905,77.369,42.904,77.236,42.913,77.057,42.971,76.988,42.974,76.944,42.971,76.646,42.929,76.509,42.919,76.218,42.924,75.932,42.929,75.84,42.938,75.79,42.933,75.682,42.83,75.636,42.815,75.366,42.837,75.048,42.904,74.818,42.978,74.622,43.056,74.364,43.179,74.209,43.24,74.187,43.205,74.146,43.194,74.086,43.189,73.949,43.195,73.886,43.133,73.719,43.088,73.612,43.048,73.556,43.003,73.45,42.703,73.422,42.594,73.493,42.409,73.412,42.42,73.316,42.467,73.283,42.504,73.191,42.527,72.855,42.561,72.792,42.603,72.753,42.638,72.666,42.66,72.543,42.678,72.276,42.758,72.162,42.761,71.817,42.822,71.761,42.821,71.735,42.819,71.601,42.779,71.514,42.767,71.422,42.783,71.257,42.734,71.167,42.667,71.094,42.587,71.023,42.535,71.002,42.459,70.952,42.419,70.893,42.34,70.893,42.294,70.947,42.249]]]},{"n":"Jordan","c":"JO","r":4,"ml":4,"lx":36.38,"ly":30.81,"b":[34.951,29.19,39.293,33.372],"p":[[[35.787,32.735,35.895,32.714,35.956,32.667,36.059,32.534,36.22,32.495,36.284,32.457,36.372,32.387,36.479,32.361,36.818,32.317,37.089,32.466,37.318,32.591,37.577,32.733,37.754,32.83,38.056,32.995,38.254,33.099,38.516,33.237,38.774,33.372,38.845,33.151,38.915,32.935,38.987,32.711,39.058,32.493,38.982,32.473,39.041,32.306,39.14,32.331,39.247,32.351,39.293,32.244,39.145,32.125,38.997,32.007,38.962,31.995,38.77,31.946,38.375,31.847,38.111,31.781,37.774,31.696,37.493,31.626,37.216,31.556,36.959,31.492,37.105,31.355,37.329,31.147,37.479,31.008,37.655,30.829,37.813,30.669,37.98,30.5,37.863,30.443,37.67,30.348,37.65,30.331,37.634,30.313,37.554,30.145,37.491,30.012,37.469,29.995,37.199,29.946,36.927,29.897,36.755,29.866,36.704,29.832,36.592,29.666,36.476,29.495,36.283,29.355,36.068,29.201,36.015,29.19,35.86,29.214,35.595,29.255,35.339,29.294,35.164,29.321,34.951,29.354,34.982,29.484,34.973,29.555,35.024,29.787,35.053,29.897,35.068,29.978,35.142,30.142,35.133,30.195,35.148,30.384,35.141,30.421,35.174,30.524,35.237,30.673,35.298,30.802,35.32,30.86,35.383,30.982,35.439,31.132,35.41,31.214,35.401,31.231,35.424,31.325,35.423,31.325,35.451,31.479,35.465,31.562,35.499,31.672,35.559,31.766,35.531,31.985,35.535,32.103,35.572,32.238,35.551,32.396,35.569,32.62,35.573,32.641,35.595,32.668,35.611,32.682,35.734,32.729,35.787,32.735]]]},{"n":"Japan","c":"JP","r":2,"ml":1.7,"lx":138.44,"ly":36.14,"b":[123.68,24.266,145.833,45.51],"p":[[[133.371,36.204,133.325,36.167,133.239,36.179,133.19,36.233,133.206,36.293,133.296,36.34,133.381,36.246,133.371,36.204]],[[138.344,37.822,138.249,37.82,138.225,37.829,138.283,37.854,138.288,37.896,138.322,37.97,138.322,37.991,138.246,37.995,138.25,38.078,138.306,38.161,138.461,38.291,138.504,38.316,138.51,38.259,138.463,38.124,138.454,38.076,138.575,38.066,138.497,37.904,138.344,37.822]],[[134.933,34.288,134.824,34.203,134.731,34.209,134.683,34.247,134.668,34.294,134.757,34.368,134.834,34.473,134.904,34.519,134.961,34.545,135.005,34.544,134.905,34.398,134.933,34.288]],[[130.623,30.263,130.508,30.241,130.446,30.265,130.388,30.388,130.497,30.466,130.644,30.389,130.673,30.367,130.623,30.263]],[[130.96,30.397,130.872,30.386,130.87,30.444,130.94,30.575,130.947,30.671,131.012,30.792,131.04,30.819,131.06,30.828,131.083,30.791,131.057,30.642,130.993,30.53,130.96,30.397]],[[130.083,32.23,130.004,32.194,129.993,32.228,129.96,32.244,130.017,32.292,130.015,32.314,129.979,32.346,130.021,32.469,130.01,32.522,130.168,32.541,130.197,32.492,130.2,32.341,130.083,32.23]],[[129.279,34.123,129.214,34.083,129.186,34.145,129.215,34.321,129.337,34.285,129.335,34.231,129.279,34.123]],[[129.386,34.354,129.365,34.306,129.297,34.34,129.267,34.37,129.329,34.522,129.322,34.579,129.326,34.607,129.451,34.687,129.472,34.671,129.48,34.649,129.469,34.616,129.475,34.54,129.381,34.416,129.386,34.354]],[[128.665,32.784,128.704,32.757,128.761,32.772,128.806,32.776,128.839,32.763,128.879,32.693,128.895,32.652,128.821,32.646,128.79,32.637,128.75,32.586,128.693,32.605,128.657,32.628,128.649,32.662,128.665,32.784]],[[130.381,32.424,130.293,32.419,130.256,32.431,130.242,32.463,130.365,32.527,130.461,32.516,130.419,32.458,130.381,32.424]],[[141.073,45.333,141.034,45.269,140.982,45.364,140.972,45.465,141.002,45.465,141.057,45.45,141.07,45.4,141.073,45.333]],[[141.295,45.119,141.226,45.112,141.145,45.154,141.135,45.206,141.194,45.248,141.252,45.232,141.31,45.179,141.329,45.15,141.295,45.119]],[[139.481,42.081,139.458,42.076,139.435,42.084,139.412,42.16,139.431,42.2,139.496,42.227,139.558,42.235,139.505,42.096,139.481,42.081]],[[134.352,34.484,134.333,34.464,134.315,34.469,134.252,34.423,134.238,34.467,134.188,34.496,134.182,34.519,134.326,34.534,134.372,34.522,134.352,34.484]],[[139.456,34.727,139.446,34.68,139.392,34.69,139.367,34.721,139.37,34.775,139.426,34.776,139.456,34.727]],[[129.077,32.84,129.052,32.829,129.02,32.92,128.997,32.952,129.035,32.969,129.11,33.133,129.124,33.068,129.153,33.003,129.182,32.993,129.154,32.946,129.112,32.929,129.077,32.84]],[[129.492,33.223,129.421,33.176,129.37,33.176,129.417,33.231,129.423,33.257,129.462,33.331,129.538,33.358,129.57,33.361,129.508,33.284,129.492,33.223]],[[129.796,33.749,129.727,33.707,129.675,33.74,129.7,33.829,129.717,33.858,129.776,33.829,129.796,33.749]],[[132.578,34.115,132.549,34.075,132.461,34.087,132.496,34.122,132.524,34.164,132.543,34.173,132.56,34.127,132.578,34.115]],[[132.266,33.945,132.315,33.909,132.43,33.924,132.445,33.913,132.411,33.88,132.36,33.847,132.267,33.871,132.209,33.873,132.201,33.928,132.208,33.948,132.266,33.945]],[[129.718,31.657,129.687,31.64,129.707,31.718,129.787,31.787,129.794,31.742,129.718,31.657]],[[142.188,26.617,142.17,26.616,142.107,26.722,142.125,26.726,142.162,26.71,142.202,26.649,142.188,26.617]],[[143.824,44.117,143.95,44.112,144.005,44.117,144.101,44.102,144.482,43.95,144.597,43.93,144.715,43.928,144.799,43.94,144.872,43.982,145.102,44.166,145.343,44.334,145.37,44.327,145.37,44.281,145.352,44.23,145.245,44.076,145.126,43.869,145.101,43.765,145.14,43.663,145.214,43.578,145.273,43.463,145.341,43.303,145.436,43.282,145.488,43.28,145.583,43.328,145.674,43.389,145.751,43.396,145.833,43.386,145.726,43.343,145.624,43.291,145.537,43.193,145.505,43.174,145.405,43.18,145.347,43.177,145.23,43.135,145.127,43.089,145.029,43.032,144.921,43.001,144.807,42.994,144.631,42.947,144.516,42.944,144.302,42.984,144.197,42.974,143.969,42.881,143.762,42.748,143.581,42.599,143.429,42.419,143.369,42.325,143.332,42.22,143.327,42.151,143.314,42.084,143.279,42.038,143.237,42,143.112,42.022,142.906,42.118,142.508,42.258,142.088,42.472,141.851,42.579,141.407,42.547,140.986,42.342,140.948,42.36,140.788,42.5,140.71,42.556,140.617,42.571,140.548,42.57,140.48,42.559,140.385,42.487,140.351,42.435,140.324,42.376,140.315,42.334,140.327,42.293,140.417,42.201,140.528,42.132,140.578,42.119,140.684,42.123,140.734,42.116,140.912,41.978,141.108,41.848,141.151,41.805,141.079,41.76,141,41.737,140.908,41.743,140.816,41.76,140.66,41.816,140.593,41.769,140.489,41.672,140.432,41.567,140.385,41.519,140.27,41.456,140.149,41.423,140.085,41.434,140.037,41.474,140.009,41.521,139.995,41.576,140.021,41.696,140.084,41.803,140.108,41.913,140.057,42.067,140.024,42.1,139.895,42.19,139.835,42.278,139.821,42.388,139.829,42.448,139.86,42.582,139.891,42.649,139.951,42.671,140.015,42.685,140.115,42.733,140.224,42.796,140.329,42.867,140.432,42.954,140.486,43.05,140.397,43.167,140.379,43.237,140.392,43.303,140.487,43.338,140.585,43.312,140.781,43.215,140.819,43.205,140.954,43.201,141.138,43.18,141.245,43.185,141.296,43.2,141.374,43.28,141.412,43.381,141.398,43.513,141.398,43.643,141.447,43.749,141.601,43.919,141.645,44.019,141.661,44.264,141.716,44.371,141.761,44.483,141.782,44.716,141.719,44.941,141.656,45.051,141.583,45.156,141.594,45.256,141.653,45.349,141.654,45.377,141.668,45.401,141.778,45.419,141.829,45.439,141.879,45.483,141.938,45.51,141.981,45.483,142.016,45.438,142.172,45.326,142.416,45.125,142.704,44.819,142.885,44.67,143.075,44.535,143.289,44.397,143.512,44.278,143.655,44.221,143.759,44.132,143.824,44.117]],[[131.175,33.603,131.309,33.573,131.366,33.571,131.419,33.584,131.499,33.624,131.583,33.652,131.643,33.638,131.696,33.603,131.724,33.554,131.711,33.502,131.616,33.392,131.537,33.274,131.717,33.252,131.897,33.255,131.855,33.182,131.848,33.118,131.903,33.088,131.949,33.047,131.937,33.01,131.91,32.974,132.009,32.919,132.002,32.882,131.977,32.844,131.732,32.593,131.66,32.466,131.61,32.325,131.565,32.223,131.531,32.117,131.506,32.002,131.46,31.883,131.475,31.778,131.46,31.671,131.337,31.405,131.25,31.41,131.14,31.442,131.071,31.437,131.035,31.378,131.098,31.256,130.902,31.112,130.686,31.015,130.684,31.059,130.704,31.094,130.736,31.122,130.758,31.156,130.79,31.269,130.774,31.383,130.709,31.526,130.704,31.577,130.749,31.598,130.78,31.604,130.796,31.624,130.797,31.671,130.776,31.706,130.715,31.718,130.655,31.718,130.613,31.665,130.556,31.563,130.528,31.46,130.54,31.403,130.566,31.352,130.645,31.267,130.621,31.218,130.589,31.179,130.311,31.267,130.251,31.273,130.201,31.292,130.147,31.408,130.261,31.437,130.294,31.451,130.307,31.488,130.322,31.601,130.269,31.696,130.224,31.73,130.188,31.769,130.211,31.849,130.196,31.95,130.194,32.091,130.214,32.115,130.319,32.144,130.395,32.219,130.462,32.305,130.56,32.456,130.641,32.619,130.563,32.626,130.498,32.657,130.569,32.734,130.547,32.832,130.44,32.951,130.382,33.093,130.287,33.155,130.238,33.178,130.177,33.145,130.127,33.105,130.173,33.013,130.168,32.932,130.175,32.851,130.222,32.847,130.28,32.867,130.326,32.853,130.354,32.81,130.361,32.756,130.34,32.702,130.298,32.675,130.246,32.677,130.193,32.706,130.152,32.748,130.054,32.771,129.951,32.722,129.853,32.622,129.769,32.571,129.808,32.645,129.827,32.725,129.786,32.782,129.69,32.875,129.668,32.929,129.662,32.995,129.679,33.06,129.778,32.986,129.828,32.893,129.901,32.852,129.992,32.852,129.922,32.988,129.897,33.022,129.799,33.084,129.665,33.187,129.58,33.236,129.61,33.344,129.66,33.365,129.702,33.36,129.844,33.322,129.858,33.375,129.837,33.404,129.826,33.437,129.919,33.483,130.072,33.522,130.103,33.54,130.131,33.578,130.168,33.598,130.275,33.598,130.365,33.634,130.439,33.734,130.457,33.789,130.484,33.835,130.67,33.915,130.716,33.928,130.84,33.918,130.953,33.872,131.009,33.776,131.058,33.673,131.175,33.603]],[[134.357,34.256,134.496,34.215,134.638,34.227,134.635,34.044,134.655,33.983,134.695,33.928,134.675,33.848,134.739,33.821,134.549,33.729,134.377,33.608,134.307,33.527,134.243,33.439,134.206,33.347,134.182,33.247,134.124,33.287,133.959,33.448,133.854,33.493,133.686,33.516,133.632,33.511,133.286,33.36,133.24,33.25,133.146,33.083,133.101,33.028,133.051,33.012,133.016,32.984,132.977,32.842,132.87,32.755,132.804,32.752,132.692,32.776,132.642,32.762,132.709,32.902,132.602,32.92,132.495,32.917,132.493,33.008,132.428,33.059,132.476,33.126,132.477,33.181,132.505,33.211,132.515,33.255,132.511,33.293,132.445,33.305,132.405,33.331,132.413,33.43,132.375,33.434,132.281,33.417,132.086,33.34,132.033,33.34,132.114,33.395,132.288,33.47,132.366,33.512,132.536,33.633,132.643,33.69,132.699,33.791,132.716,33.852,132.752,33.906,132.784,33.992,132.839,34.021,132.935,34.095,132.99,34.088,133.051,33.997,133.134,33.927,133.193,33.933,133.299,33.969,133.35,33.977,133.472,33.973,133.582,34.017,133.627,34.069,133.643,34.135,133.603,34.244,133.656,34.233,133.706,34.237,133.826,34.307,133.948,34.348,134.076,34.358,134.219,34.319,134.357,34.256]],[[141.229,41.373,141.269,41.354,141.455,41.405,141.42,41.251,141.4,41.096,141.414,40.839,141.43,40.723,141.463,40.611,141.542,40.531,141.646,40.474,141.797,40.291,141.878,40.067,141.935,39.958,141.978,39.844,141.991,39.792,141.992,39.74,141.979,39.668,141.993,39.611,141.977,39.429,141.909,39.219,141.901,39.111,141.842,39.09,141.807,39.04,141.776,39.017,141.742,39,141.694,38.995,141.659,38.975,141.645,38.918,141.622,38.865,141.58,38.817,141.546,38.763,141.519,38.632,141.509,38.498,141.467,38.404,141.368,38.38,141.254,38.381,141.108,38.338,141.077,38.313,140.962,38.149,140.929,38.053,140.928,37.95,140.96,37.823,141.003,37.698,141.036,37.467,141.002,37.115,140.968,37.002,140.895,36.926,140.84,36.89,140.792,36.847,140.73,36.732,140.627,36.503,140.619,36.445,140.619,36.386,140.592,36.308,140.574,36.231,140.59,36.142,140.622,36.059,140.76,35.846,140.813,35.783,140.874,35.725,140.639,35.661,140.597,35.632,140.457,35.51,140.413,35.395,140.417,35.267,140.393,35.221,140.355,35.181,140.315,35.155,140.159,35.096,140.059,35.038,139.96,34.947,139.92,34.9,139.844,34.915,139.799,34.957,139.843,35.01,139.83,35.072,139.851,35.232,139.826,35.297,139.906,35.345,139.944,35.423,140.027,35.485,140.086,35.54,140.097,35.585,140.044,35.633,139.988,35.668,139.91,35.668,139.835,35.658,139.786,35.612,139.77,35.55,139.774,35.52,139.768,35.495,139.65,35.409,139.666,35.319,139.7,35.274,139.744,35.252,139.731,35.222,139.675,35.149,139.636,35.142,139.564,35.243,139.474,35.299,139.363,35.298,139.249,35.278,139.163,35.211,139.134,35.155,139.116,35.097,139.122,34.956,139.086,34.839,139.016,34.736,138.983,34.698,138.897,34.628,138.838,34.619,138.795,34.651,138.761,34.699,138.804,34.876,138.803,34.975,138.904,35.025,138.821,35.096,138.72,35.124,138.577,35.086,138.537,35.044,138.51,34.987,138.433,34.915,138.349,34.848,138.253,34.733,138.189,34.596,137.979,34.641,137.864,34.651,137.749,34.647,137.543,34.664,137.318,34.636,137.062,34.583,137.077,34.621,137.288,34.704,137.295,34.728,137.275,34.773,137.223,34.775,137.097,34.759,137.032,34.766,137.006,34.814,136.963,34.835,136.935,34.815,136.944,34.722,136.913,34.709,136.871,34.733,136.885,34.806,136.856,34.913,136.853,34.979,136.897,35.036,136.852,35.06,136.804,35.05,136.744,35.023,136.69,34.984,136.577,34.79,136.533,34.678,136.616,34.589,136.842,34.464,136.88,34.434,136.881,34.38,136.854,34.324,136.792,34.299,136.544,34.258,136.33,34.177,136.268,34.095,136.073,33.778,135.916,33.562,135.695,33.487,135.453,33.553,135.394,33.629,135.347,33.722,135.257,33.806,135.175,33.898,135.128,34.007,135.135,34.183,135.1,34.288,135.132,34.317,135.266,34.381,135.309,34.417,135.385,34.5,135.412,34.547,135.416,34.617,135.355,34.654,135.198,34.653,135.042,34.631,134.93,34.662,134.785,34.747,134.74,34.765,134.584,34.771,134.472,34.755,134.363,34.724,134.247,34.714,134.208,34.698,134.075,34.593,133.968,34.527,133.876,34.495,133.678,34.486,133.579,34.465,133.474,34.43,133.445,34.433,133.336,34.385,133.21,34.344,133.142,34.302,133.019,34.33,132.775,34.255,132.657,34.246,132.534,34.287,132.421,34.353,132.313,34.325,132.238,34.227,132.202,34.032,132.159,33.944,132.146,33.839,132.09,33.855,131.763,34.045,131.741,34.052,131.476,34.019,131.408,34.004,131.323,33.965,131.233,33.948,131.15,33.976,131.072,34.021,130.996,34.007,130.919,33.976,130.889,34.262,130.904,34.3,130.952,34.35,131.004,34.393,131.132,34.407,131.262,34.393,131.354,34.413,131.433,34.47,131.515,34.55,131.608,34.615,131.734,34.667,131.856,34.726,131.963,34.809,132.065,34.9,132.158,34.967,132.26,35.022,132.414,35.156,132.619,35.307,132.698,35.418,132.747,35.449,132.923,35.511,133.157,35.559,133.267,35.557,133.376,35.459,133.435,35.472,133.495,35.497,133.615,35.511,133.739,35.495,133.86,35.495,133.981,35.507,134.214,35.539,134.337,35.578,134.456,35.628,134.882,35.663,135.174,35.747,135.221,35.741,135.265,35.722,135.269,35.66,135.232,35.592,135.268,35.551,135.327,35.526,135.602,35.518,135.68,35.503,135.795,35.55,135.903,35.607,136.016,35.683,136.095,35.768,136.022,35.874,136.006,35.991,136.067,36.117,136.156,36.223,136.262,36.288,136.359,36.362,136.556,36.572,136.698,36.742,136.749,36.951,136.719,37.198,136.843,37.382,136.962,37.414,137.199,37.497,137.323,37.522,137.341,37.485,137.338,37.437,137.152,37.283,137.046,37.22,136.982,37.2,136.924,37.172,136.9,37.118,136.994,37.027,137.019,36.96,137.013,36.895,137.017,36.837,137.124,36.774,137.246,36.753,137.298,36.754,137.343,36.77,137.483,36.925,137.514,36.952,137.913,37.065,138.11,37.151,138.218,37.173,138.32,37.218,138.548,37.392,138.633,37.472,138.709,37.561,138.771,37.663,138.819,37.775,138.885,37.844,139.247,38.009,139.364,38.099,139.401,38.143,139.446,38.269,139.477,38.4,139.521,38.503,139.58,38.599,139.66,38.697,139.749,38.788,139.802,38.882,139.879,39.105,139.912,39.229,139.939,39.273,139.977,39.311,140.011,39.358,140.037,39.411,140.048,39.464,140.065,39.624,140.055,39.749,139.995,39.855,139.945,39.885,139.891,39.887,139.81,39.878,139.742,39.921,139.755,39.959,139.826,39.966,139.874,39.986,139.908,40.022,139.972,40.137,140.011,40.26,140.014,40.315,139.964,40.414,139.924,40.534,139.923,40.598,139.967,40.673,140.029,40.733,140.085,40.747,140.146,40.752,140.201,40.775,140.252,40.809,140.281,40.846,140.326,40.948,140.344,41.006,140.315,41.161,140.344,41.203,140.386,41.23,140.441,41.21,140.498,41.206,140.564,41.212,140.628,41.195,140.64,41.156,140.679,40.893,140.702,40.858,140.749,40.83,140.801,40.834,140.846,40.875,140.876,40.93,140.936,40.941,141.119,40.882,141.183,40.924,141.225,40.988,141.262,41.103,141.244,41.206,141.2,41.244,141.155,41.237,141.115,41.208,141.07,41.193,140.801,41.139,140.802,41.254,140.86,41.425,140.892,41.48,140.937,41.506,141.05,41.476,141.106,41.456,141.229,41.373]],[[124.293,24.516,124.234,24.358,124.186,24.335,124.136,24.348,124.085,24.436,124.12,24.47,124.17,24.452,124.211,24.459,124.302,24.587,124.324,24.566,124.293,24.516]],[[123.889,24.28,123.826,24.266,123.75,24.283,123.681,24.288,123.68,24.318,123.752,24.348,123.754,24.391,123.771,24.414,123.935,24.362,123.928,24.324,123.889,24.28]],[[125.444,24.743,125.359,24.717,125.269,24.733,125.284,24.872,125.315,24.852,125.335,24.805,125.402,24.777,125.444,24.743]],[[128.259,26.653,128.163,26.607,128.127,26.552,128.038,26.534,127.951,26.456,127.867,26.442,127.869,26.381,127.905,26.328,127.849,26.319,127.79,26.255,127.786,26.209,127.806,26.171,127.804,26.153,127.729,26.097,127.653,26.095,127.65,26.154,127.655,26.199,127.727,26.308,127.729,26.434,127.796,26.449,127.82,26.466,127.926,26.556,127.946,26.594,127.891,26.631,127.895,26.675,127.907,26.694,127.994,26.679,128.03,26.647,128.047,26.643,128.098,26.668,128.122,26.711,128.217,26.797,128.255,26.882,128.332,26.812,128.311,26.721,128.259,26.653]],[[128.998,27.721,128.956,27.702,128.9,27.728,128.883,27.842,128.908,27.898,128.952,27.91,128.99,27.811,129.016,27.77,128.998,27.721]],[[129.453,28.209,129.366,28.128,129.275,28.201,129.165,28.25,129.217,28.263,129.248,28.283,129.251,28.314,129.322,28.36,129.465,28.395,129.51,28.398,129.561,28.431,129.577,28.461,129.598,28.476,129.69,28.517,129.715,28.47,129.71,28.432,129.642,28.411,129.575,28.361,129.513,28.299,129.457,28.272,129.439,28.255,129.453,28.209]],[[129.324,28.105,129.331,28.082,129.232,28.101,129.192,28.192,129.257,28.176,129.277,28.145,129.324,28.105]],[[139.841,33.056,139.824,33.045,139.776,33.078,139.769,33.107,139.777,33.125,139.809,33.129,139.874,33.094,139.841,33.056]]]},{"n":"Jamaica","c":"JM","r":4,"ml":4,"lx":-77.32,"ly":18.14,"b":[-78.34,17.715,-76.211,18.522],"p":[[[-77.261,18.457,-77.14,18.421,-77.014,18.403,-76.959,18.402,-76.908,18.39,-76.793,18.304,-76.701,18.257,-76.35,18.152,-76.233,17.97,-76.211,17.914,-76.301,17.88,-76.416,17.868,-76.525,17.866,-76.625,17.901,-76.669,17.928,-76.774,17.94,-76.748,17.965,-76.795,17.976,-76.853,17.974,-76.896,17.904,-76.944,17.849,-77.036,17.854,-77.071,17.901,-77.119,17.88,-77.158,17.845,-77.205,17.715,-77.28,17.78,-77.361,17.834,-77.464,17.856,-77.671,17.86,-77.768,17.877,-77.849,17.988,-77.881,18.019,-77.963,18.048,-78.044,18.174,-78.074,18.191,-78.294,18.218,-78.34,18.287,-78.326,18.35,-78.252,18.426,-78.217,18.448,-78.095,18.445,-77.978,18.468,-77.927,18.501,-77.873,18.522,-77.452,18.467,-77.354,18.466,-77.261,18.457]]]},{"n":"Italy","c":"IT","r":2,"ml":2,"lx":11.08,"ly":44.73,"b":[6.628,36.688,18.486,47.082],"p":[[[7.021,45.926,7.056,45.904,7.129,45.88,7.328,45.912,7.452,45.944,7.539,45.978,7.593,45.972,7.788,45.922,7.852,45.947,7.993,46.016,8.014,46.052,8.125,46.161,8.127,46.188,8.082,46.256,8.096,46.271,8.232,46.341,8.299,46.403,8.371,46.445,8.423,46.446,8.437,46.432,8.443,46.403,8.438,46.283,8.458,46.246,8.565,46.16,8.642,46.111,8.819,46.077,8.827,46.061,8.778,45.996,8.885,45.919,8.904,45.862,8.954,45.83,9.024,45.846,9.047,45.876,9.019,45.928,8.999,45.983,9.003,46.015,9.022,46.051,9.071,46.102,9.203,46.219,9.251,46.287,9.26,46.391,9.26,46.475,9.304,46.496,9.399,46.481,9.428,46.482,9.441,46.431,9.481,46.349,9.529,46.306,9.58,46.296,9.639,46.296,9.788,46.346,9.884,46.368,9.939,46.362,9.972,46.328,10.041,46.238,10.081,46.228,10.128,46.238,10.145,46.254,10.13,46.288,10.11,46.363,10.082,46.421,10.046,46.448,10.038,46.483,10.061,46.547,10.087,46.6,10.138,46.614,10.196,46.621,10.272,46.565,10.363,46.547,10.431,46.55,10.442,46.583,10.438,46.619,10.398,46.665,10.406,46.735,10.453,46.865,10.479,46.855,10.58,46.854,10.689,46.846,10.76,46.793,10.829,46.775,10.927,46.769,10.993,46.777,11.025,46.797,11.063,46.859,11.134,46.936,11.244,46.976,11.433,46.983,11.528,46.997,11.625,46.997,11.699,46.985,11.776,46.986,11.97,47.04,12.169,47.082,12.197,47.075,12.201,47.061,12.166,47.028,12.131,46.985,12.154,46.935,12.268,46.836,12.33,46.76,12.388,46.703,12.479,46.673,12.599,46.654,12.7,46.647,12.806,46.626,13.169,46.573,13.352,46.558,13.49,46.556,13.7,46.52,13.68,46.463,13.637,46.449,13.563,46.415,13.479,46.369,13.4,46.318,13.378,46.262,13.4,46.225,13.421,46.212,13.45,46.224,13.492,46.217,13.545,46.197,13.633,46.177,13.635,46.158,13.617,46.133,13.548,46.089,13.486,46.04,13.48,46.009,13.488,45.987,13.509,45.974,13.601,45.98,13.614,45.962,13.57,45.834,13.583,45.812,13.663,45.792,13.722,45.761,13.831,45.68,13.875,45.615,13.845,45.593,13.776,45.582,13.72,45.588,13.783,45.627,13.628,45.771,13.558,45.771,13.465,45.71,13.206,45.771,13.157,45.747,13.12,45.698,13.03,45.638,12.903,45.611,12.761,45.544,12.612,45.497,12.498,45.462,12.432,45.468,12.536,45.545,12.492,45.546,12.354,45.492,12.274,45.446,12.249,45.369,12.226,45.242,12.286,45.208,12.392,45.04,12.523,44.968,12.498,44.899,12.464,44.845,12.384,44.798,12.319,44.833,12.279,44.832,12.248,44.723,12.305,44.429,12.396,44.224,12.487,44.134,12.691,43.995,12.907,43.921,13.295,43.686,13.508,43.612,13.564,43.571,13.693,43.39,13.805,43.18,13.925,42.852,14.01,42.69,14.183,42.506,14.541,42.244,14.866,42.053,15.169,41.934,15.405,41.913,15.964,41.939,16.062,41.928,16.165,41.896,16.189,41.814,16.151,41.758,16.034,41.701,15.914,41.621,15.9,41.512,16.013,41.435,16.552,41.232,17.103,41.062,17.275,40.975,17.474,40.841,17.955,40.655,18.036,40.565,18.328,40.371,18.461,40.221,18.486,40.105,18.423,39.987,18.393,39.904,18.344,39.821,18.219,39.853,18.078,39.937,17.865,40.28,17.476,40.315,17.396,40.34,17.258,40.399,17.249,40.438,17.215,40.486,17.18,40.503,17.031,40.513,16.928,40.458,16.807,40.326,16.67,40.137,16.53,39.86,16.522,39.748,16.598,39.639,16.824,39.578,16.999,39.482,17.115,39.381,17.123,39.137,17.175,38.998,17.099,38.919,16.951,38.94,16.755,38.89,16.617,38.8,16.559,38.715,16.574,38.494,16.546,38.409,16.282,38.25,16.144,38.086,16.11,38.019,16.057,37.942,15.725,37.939,15.646,38.034,15.643,38.175,15.7,38.262,15.822,38.303,15.905,38.483,15.879,38.614,15.927,38.672,15.972,38.713,16.066,38.736,16.197,38.759,16.21,38.941,16.107,39.024,16.071,39.139,16.024,39.354,15.854,39.627,15.764,39.87,15.693,39.99,15.585,40.053,15.391,40.052,15.295,40.07,14.951,40.239,14.927,40.265,14.929,40.31,14.986,40.377,14.948,40.469,14.907,40.556,14.84,40.63,14.766,40.668,14.611,40.645,14.557,40.626,14.459,40.633,14.383,40.6,14.34,40.599,14.461,40.729,14.428,40.759,14.309,40.813,14.147,40.821,14.102,40.827,14.076,40.794,14.044,40.812,14.048,40.87,13.86,41.13,13.733,41.236,13.67,41.254,13.555,41.232,13.362,41.279,13.247,41.289,13.183,41.278,13.089,41.244,13.041,41.266,13.024,41.301,12.849,41.409,12.631,41.47,12.206,41.813,12.075,41.941,11.807,42.082,11.637,42.288,11.498,42.363,11.296,42.423,11.25,42.416,11.189,42.393,11.141,42.39,11.103,42.417,11.142,42.444,11.185,42.457,11.168,42.535,10.938,42.739,10.803,42.804,10.765,42.845,10.737,42.9,10.708,42.936,10.645,42.957,10.59,42.954,10.515,42.968,10.517,43.065,10.532,43.14,10.521,43.204,10.448,43.371,10.321,43.513,10.246,43.852,10.188,43.948,10.048,44.02,9.731,44.101,9.289,44.319,9.196,44.323,8.93,44.408,8.766,44.422,8.552,44.346,8.292,44.137,8.082,43.919,8.005,43.877,7.733,43.803,7.493,43.767,7.491,43.823,7.482,43.865,7.523,43.911,7.59,43.965,7.651,44.034,7.677,44.083,7.665,44.116,7.637,44.165,7.599,44.168,7.371,44.127,7.319,44.138,7.149,44.202,6.967,44.28,6.9,44.336,6.875,44.392,6.894,44.428,6.879,44.463,6.843,44.511,6.875,44.565,6.932,44.632,6.96,44.677,7.008,44.689,7.031,44.717,6.993,44.827,6.973,44.845,6.94,44.859,6.889,44.86,6.801,44.883,6.738,44.921,6.725,44.973,6.691,45.023,6.635,45.068,6.628,45.118,6.692,45.144,6.78,45.145,6.842,45.136,6.981,45.216,7.032,45.223,7.078,45.24,7.117,45.349,7.146,45.382,7.153,45.401,7.126,45.424,7.014,45.5,6.962,45.581,6.881,45.67,6.806,45.71,6.791,45.741,6.789,45.78,6.804,45.815,6.941,45.868,7.021,45.926],[12.439,41.898,12.438,41.906,12.431,41.905,12.428,41.901,12.431,41.898,12.439,41.898],[12.485,43.901,12.515,43.953,12.504,43.99,12.441,43.982,12.397,43.935,12.426,43.894,12.485,43.901]],[[10.395,42.858,10.428,42.819,10.432,42.797,10.41,42.771,10.419,42.713,10.336,42.761,10.209,42.737,10.131,42.742,10.11,42.785,10.128,42.81,10.248,42.816,10.286,42.828,10.359,42.822,10.395,42.858]],[[13.938,40.706,13.894,40.697,13.868,40.709,13.854,40.724,13.871,40.762,13.962,40.739,13.961,40.718,13.938,40.706]],[[12.051,36.757,12.003,36.746,11.941,36.78,11.936,36.829,11.948,36.843,12.024,36.821,12.048,36.776,12.051,36.757]],[[15.577,38.22,15.509,38.107,15.476,38.063,15.234,37.785,15.207,37.721,15.19,37.651,15.165,37.59,15.131,37.532,15.1,37.459,15.106,37.375,15.117,37.335,15.146,37.308,15.194,37.283,15.23,37.244,15.174,37.209,15.236,37.139,15.289,37.097,15.296,37.055,15.295,37.013,15.185,36.935,15.142,36.892,15.116,36.839,15.104,36.785,15.116,36.736,15.113,36.688,15.002,36.694,14.89,36.724,14.776,36.71,14.614,36.767,14.555,36.777,14.502,36.799,14.367,36.973,14.259,37.046,14.143,37.104,14.024,37.107,13.905,37.101,13.801,37.136,13.587,37.254,13.361,37.349,13.265,37.41,13.221,37.452,13.17,37.479,13.04,37.507,12.924,37.571,12.871,37.575,12.757,37.567,12.699,37.572,12.64,37.594,12.527,37.67,12.454,37.774,12.436,37.82,12.487,37.939,12.548,38.053,12.602,38.085,12.664,38.108,12.702,38.142,12.734,38.183,12.851,38.064,12.903,38.035,12.955,38.041,13.049,38.084,13.057,38.131,13.16,38.19,13.291,38.191,13.352,38.181,13.383,38.127,13.433,38.11,13.491,38.103,13.682,38.001,13.735,37.984,13.789,37.981,13.937,38.024,14.05,38.041,14.288,38.017,14.416,38.043,14.506,38.046,14.637,38.085,14.737,38.151,14.79,38.167,14.846,38.172,14.982,38.168,15.119,38.153,15.176,38.168,15.224,38.211,15.28,38.23,15.341,38.217,15.499,38.291,15.568,38.296,15.635,38.268,15.577,38.22]],[[9.632,40.882,9.682,40.818,9.794,40.556,9.805,40.5,9.783,40.442,9.754,40.4,9.643,40.268,9.659,40.159,9.701,40.092,9.707,40.017,9.686,39.924,9.617,39.354,9.584,39.254,9.563,39.166,9.486,39.14,9.388,39.168,9.264,39.217,9.207,39.214,9.149,39.197,9.102,39.211,9.056,39.239,9.023,39.043,8.967,38.964,8.881,38.913,8.801,38.91,8.719,38.927,8.649,38.927,8.595,38.964,8.553,39.03,8.486,39.11,8.418,39.206,8.411,39.292,8.399,39.482,8.419,39.523,8.447,39.563,8.461,39.648,8.451,39.722,8.471,39.748,8.511,39.722,8.541,39.732,8.539,39.77,8.548,39.839,8.496,39.897,8.408,39.917,8.399,39.978,8.409,40.038,8.455,40.078,8.471,40.131,8.471,40.293,8.409,40.352,8.385,40.443,8.353,40.501,8.296,40.559,8.23,40.606,8.19,40.652,8.181,40.771,8.204,40.871,8.224,40.913,8.245,40.907,8.31,40.858,8.363,40.846,8.468,40.834,8.572,40.85,8.699,40.895,8.821,40.95,8.998,41.11,9.107,41.143,9.163,41.185,9.182,41.242,9.228,41.257,9.283,41.202,9.351,41.196,9.455,41.15,9.5,41.106,9.539,41.054,9.576,41.031,9.615,41.017,9.621,41.005,9.59,40.992,9.554,40.932,9.574,40.915,9.632,40.882]],[[8.479,39.068,8.421,38.969,8.361,39.039,8.359,39.099,8.367,39.116,8.441,39.091,8.479,39.068]],[[8.286,41.04,8.253,40.994,8.206,40.997,8.224,41.031,8.267,41.099,8.32,41.122,8.344,41.102,8.319,41.063,8.286,41.04]]]},{"n":"Israel","c":"IL","r":4,"ml":3,"lx":34.85,"ly":30.91,"b":[34.245,29.477,35.888,33.416],"p":[[[35.841,33.416,35.816,33.362,35.793,33.334,35.785,33.291,35.8,33.256,35.809,33.209,35.827,33.195,35.833,33.164,35.817,33.133,35.825,33.108,35.845,33.085,35.859,32.989,35.888,32.944,35.842,32.876,35.833,32.82,35.787,32.735,35.734,32.729,35.611,32.682,35.595,32.668,35.573,32.641,35.569,32.62,35.551,32.396,35.484,32.402,35.403,32.451,35.387,32.493,35.362,32.507,35.304,32.513,35.193,32.534,35.065,32.46,35.011,32.338,35,32.281,34.956,32.161,34.971,32.087,34.979,31.992,34.99,31.913,34.978,31.866,34.954,31.841,34.961,31.823,34.983,31.817,35.053,31.838,35.127,31.817,35.198,31.776,35.204,31.75,35.153,31.734,35.035,31.673,34.951,31.602,34.929,31.537,34.873,31.397,34.88,31.368,34.908,31.351,35.101,31.366,35.277,31.423,35.409,31.483,35.451,31.479,35.423,31.325,35.424,31.325,35.401,31.231,35.41,31.214,35.439,31.132,35.383,30.982,35.32,30.86,35.298,30.802,35.237,30.673,35.174,30.524,35.141,30.421,35.148,30.384,35.133,30.195,35.142,30.142,35.068,29.978,35.053,29.897,35.024,29.787,34.973,29.555,34.904,29.477,34.87,29.564,34.791,29.812,34.735,29.982,34.659,30.191,34.53,30.446,34.518,30.507,34.49,30.596,34.401,30.828,34.329,30.995,34.245,31.208,34.348,31.293,34.35,31.363,34.526,31.526,34.524,31.542,34.477,31.585,34.484,31.592,34.678,31.896,34.804,32.196,34.922,32.614,35.006,32.827,35.077,32.967,35.109,33.084,35.223,33.092,35.309,33.08,35.411,33.076,35.493,33.119,35.533,33.25,35.579,33.271,35.603,33.241,35.627,33.275,35.734,33.333,35.788,33.37,35.841,33.416]]]},{"n":"Palestine","c":"PS","r":5,"ml":4.5,"lx":35.29,"ly":32.05,"b":[34.198,31.208,35.572,32.534],"p":[[[34.477,31.585,34.524,31.542,34.526,31.526,34.35,31.363,34.348,31.293,34.245,31.208,34.213,31.292,34.198,31.323,34.387,31.484,34.477,31.585]],[[35.551,32.396,35.572,32.238,35.535,32.103,35.531,31.985,35.559,31.766,35.499,31.672,35.465,31.562,35.451,31.479,35.409,31.483,35.277,31.423,35.101,31.366,34.908,31.351,34.88,31.368,34.873,31.397,34.929,31.537,34.951,31.602,35.035,31.673,35.153,31.734,35.204,31.75,35.198,31.776,35.127,31.817,35.053,31.838,34.983,31.817,34.961,31.823,34.954,31.841,34.978,31.866,34.99,31.913,34.979,31.992,34.971,32.087,34.956,32.161,35,32.281,35.011,32.338,35.065,32.46,35.193,32.534,35.304,32.513,35.362,32.507,35.387,32.493,35.403,32.451,35.484,32.402,35.551,32.396]]]},{"n":"Ireland","c":"IE","r":3,"ml":3,"lx":-7.8,"ly":53.08,"b":[-10.39,51.474,-6.027,55.366],"p":[[[-9.948,53.913,-9.952,53.885,-10.027,53.921,-10.062,53.96,-10.266,53.978,-10.181,54.017,-10.14,54.005,-9.996,54.004,-9.956,53.987,-9.948,53.913]],[[-7.219,55.092,-7.377,55.028,-7.401,55.003,-7.446,54.905,-7.451,54.877,-7.502,54.825,-7.55,54.768,-7.606,54.746,-7.69,54.728,-7.737,54.71,-7.797,54.719,-7.873,54.718,-7.911,54.698,-7.909,54.683,-7.886,54.666,-7.82,54.64,-7.746,54.616,-7.754,54.595,-7.794,54.571,-8.044,54.512,-8.119,54.477,-8.145,54.454,-8.118,54.414,-7.918,54.297,-7.884,54.284,-7.855,54.215,-7.679,54.187,-7.607,54.144,-7.544,54.134,-7.409,54.137,-7.355,54.121,-7.325,54.133,-7.307,54.156,-7.193,54.214,-7.155,54.24,-7.178,54.275,-7.203,54.302,-7.133,54.355,-7.05,54.408,-7.008,54.407,-6.936,54.374,-6.877,54.329,-6.869,54.294,-6.858,54.269,-6.803,54.214,-6.767,54.196,-6.67,54.185,-6.647,54.163,-6.664,54.085,-6.65,54.059,-6.548,54.057,-6.44,54.064,-6.403,54.061,-6.364,54.077,-6.304,54.095,-6.218,54.089,-6.176,54.054,-6.157,54.017,-6.231,54.004,-6.308,54.011,-6.345,53.987,-6.348,53.941,-6.322,53.882,-6.27,53.84,-6.229,53.746,-6.195,53.641,-6.142,53.578,-6.131,53.499,-6.139,53.46,-6.129,53.391,-6.152,53.366,-6.135,53.301,-6.072,53.166,-6.045,53.091,-6.027,52.927,-6.071,52.866,-6.131,52.807,-6.169,52.738,-6.199,52.663,-6.217,52.543,-6.345,52.402,-6.4,52.367,-6.463,52.345,-6.325,52.247,-6.438,52.203,-6.561,52.189,-6.697,52.214,-6.782,52.21,-6.86,52.179,-6.89,52.159,-6.915,52.169,-6.966,52.25,-7.003,52.166,-7.082,52.139,-7.216,52.145,-7.441,52.123,-7.527,52.099,-7.563,52.062,-7.59,52.019,-7.625,51.993,-7.665,51.98,-7.838,51.948,-7.872,51.935,-7.952,51.866,-8.058,51.826,-8.145,51.814,-8.222,51.854,-8.254,51.878,-8.29,51.891,-8.409,51.889,-8.372,51.876,-8.347,51.848,-8.336,51.793,-8.349,51.739,-8.408,51.712,-8.478,51.707,-8.588,51.651,-8.734,51.636,-8.813,51.585,-9.296,51.498,-9.324,51.497,-9.391,51.519,-9.463,51.529,-9.535,51.522,-9.737,51.474,-9.835,51.483,-9.71,51.604,-9.542,51.664,-9.525,51.681,-9.58,51.689,-9.899,51.647,-10.01,51.611,-10.121,51.601,-10.069,51.656,-9.926,51.731,-9.85,51.766,-9.803,51.78,-9.75,51.824,-9.599,51.874,-10.084,51.771,-10.212,51.784,-10.242,51.812,-10.341,51.799,-10.379,51.869,-10.232,51.975,-10.146,52.02,-10.044,52.045,-9.946,52.08,-9.91,52.123,-9.956,52.137,-10.25,52.126,-10.39,52.135,-10.383,52.169,-10.357,52.207,-10.211,52.272,-10.132,52.282,-10.062,52.276,-9.993,52.259,-9.937,52.238,-9.772,52.25,-9.841,52.291,-9.853,52.375,-9.906,52.404,-9.838,52.443,-9.761,52.466,-9.632,52.547,-9.586,52.559,-9.331,52.579,-9.056,52.621,-8.783,52.68,-8.923,52.712,-8.99,52.755,-9.098,52.668,-9.175,52.635,-9.394,52.617,-9.463,52.627,-9.561,52.654,-9.591,52.644,-9.62,52.623,-9.764,52.58,-9.917,52.57,-9.74,52.648,-9.515,52.781,-9.465,52.823,-9.394,52.896,-9.416,52.929,-9.462,52.947,-9.299,53.098,-9.242,53.125,-9.138,53.129,-9.061,53.153,-9.027,53.153,-8.997,53.162,-8.93,53.207,-9.034,53.236,-9.14,53.25,-9.471,53.235,-9.514,53.238,-9.555,53.252,-9.582,53.272,-9.602,53.323,-9.626,53.334,-9.701,53.334,-9.774,53.319,-9.825,53.32,-9.876,53.343,-9.795,53.395,-9.899,53.407,-10.004,53.397,-10.091,53.413,-10.094,53.446,-10.054,53.478,-10.106,53.509,-10.117,53.549,-10.062,53.568,-10.001,53.561,-9.878,53.59,-9.721,53.604,-9.856,53.633,-9.91,53.658,-9.912,53.695,-9.902,53.727,-9.745,53.781,-9.578,53.805,-9.591,53.841,-9.579,53.88,-9.748,53.891,-9.914,53.864,-9.896,53.938,-9.856,54.004,-9.848,54.048,-9.856,54.095,-9.934,54.075,-9.944,54.142,-9.977,54.187,-10.093,54.156,-10.09,54.216,-10.056,54.258,-9.996,54.276,-9.936,54.268,-9.825,54.269,-9.717,54.3,-9.562,54.309,-9.316,54.299,-9.146,54.21,-9.102,54.226,-9.034,54.282,-9.002,54.288,-8.747,54.263,-8.588,54.231,-8.546,54.241,-8.568,54.304,-8.623,54.347,-8.554,54.404,-8.471,54.442,-8.415,54.461,-8.287,54.485,-8.23,54.507,-8.193,54.58,-8.133,54.641,-8.457,54.609,-8.764,54.681,-8.715,54.732,-8.65,54.761,-8.538,54.783,-8.528,54.809,-8.471,54.832,-8.377,54.889,-8.412,54.965,-8.393,55.02,-8.326,55.056,-8.305,55.108,-8.275,55.146,-8.138,55.16,-8.006,55.195,-7.959,55.192,-7.803,55.2,-7.751,55.186,-7.763,55.248,-7.667,55.256,-7.63,55.244,-7.613,55.2,-7.57,55.171,-7.557,55.122,-7.586,55.084,-7.634,55.055,-7.59,55.025,-7.659,54.971,-7.584,54.994,-7.478,55.047,-7.484,55.09,-7.502,55.145,-7.531,55.194,-7.518,55.248,-7.458,55.282,-7.302,55.299,-7.366,55.36,-7.309,55.366,-7.247,55.353,-7.155,55.305,-7.06,55.268,-6.962,55.238,-7.056,55.178,-7.173,55.137,-7.219,55.092]]]},{"n":"Iraq","c":"IQ","r":3,"ml":3,"lx":43.26,"ly":33.09,"b":[38.774,29.064,48.546,37.372],"p":[[[42.359,37.109,42.456,37.129,42.635,37.249,42.741,37.362,42.775,37.372,42.869,37.335,42.937,37.325,43.092,37.367,43.185,37.345,43.263,37.317,43.307,37.315,43.516,37.245,43.568,37.236,43.676,37.227,43.836,37.224,43.94,37.269,44.013,37.314,44.065,37.312,44.114,37.302,44.156,37.283,44.192,37.25,44.208,37.203,44.202,37.052,44.217,37.012,44.246,36.983,44.282,36.978,44.326,37.011,44.402,37.058,44.496,37.111,44.566,37.158,44.606,37.176,44.669,37.174,44.731,37.165,44.765,37.142,44.765,37.135,44.798,37.064,44.881,36.799,44.928,36.766,44.981,36.738,45.019,36.698,45.034,36.659,45.029,36.598,45.031,36.526,45.053,36.472,45.084,36.43,45.112,36.409,45.155,36.407,45.207,36.397,45.241,36.356,45.351,36.055,45.362,36.015,45.408,36.003,45.484,36.009,45.562,35.977,45.645,35.928,45.723,35.837,45.776,35.822,45.941,35.835,46.167,35.821,46.273,35.773,46.263,35.744,46.181,35.711,46.037,35.673,45.995,35.608,45.971,35.524,45.975,35.477,46.011,35.425,46.112,35.322,46.118,35.284,46.136,35.232,46.155,35.197,46.134,35.128,46.042,35.08,45.921,35.029,45.678,34.798,45.66,34.749,45.662,34.613,45.638,34.574,45.561,34.575,45.501,34.582,45.498,34.534,45.459,34.47,45.438,34.415,45.527,34.285,45.543,34.216,45.529,34.153,45.446,34.044,45.397,33.971,45.409,33.954,45.473,33.925,45.674,33.687,45.738,33.603,45.823,33.625,45.854,33.623,45.879,33.61,45.895,33.582,45.895,33.546,45.873,33.492,45.981,33.47,46.02,33.416,46.146,33.23,46.141,33.174,46.081,33.087,46.08,33.028,46.093,32.976,46.113,32.958,46.298,32.95,46.377,32.929,46.57,32.834,46.789,32.688,46.969,32.568,47.121,32.467,47.285,32.474,47.33,32.456,47.371,32.424,47.418,32.34,47.512,32.151,47.592,32.088,47.715,31.936,47.83,31.794,47.754,31.601,47.679,31.401,47.679,31.142,47.679,31.002,47.836,30.996,48.011,30.99,48.012,30.824,48.013,30.656,48.015,30.466,48.066,30.458,48.148,30.417,48.182,30.355,48.226,30.321,48.279,30.316,48.331,30.285,48.383,30.23,48.401,30.188,48.388,30.16,48.399,30.11,48.435,30.038,48.479,30.004,48.546,29.962,48.454,29.938,48.355,29.957,48.142,30.041,48.073,30.043,47.983,30.011,47.979,29.983,47.754,30.077,47.673,30.096,47.644,30.097,47.515,30.096,47.331,30.08,47.223,30.042,47.148,30.001,47.114,29.961,47.102,29.94,47.044,29.823,46.976,29.673,46.906,29.538,46.769,29.347,46.694,29.26,46.531,29.096,46.356,29.064,45.95,29.096,45.499,29.132,45.05,29.167,44.717,29.194,44.691,29.202,44.361,29.435,44.1,29.619,43.774,29.849,43.441,30.084,43.103,30.322,42.858,30.495,42.56,30.718,42.289,30.92,42.074,31.08,41.8,31.22,41.585,31.33,41.272,31.489,41.022,31.616,40.808,31.725,40.479,31.893,40.369,31.939,40.028,31.995,39.704,32.043,39.369,32.092,39.145,32.125,39.293,32.244,39.247,32.351,39.14,32.331,39.041,32.306,38.982,32.473,39.058,32.493,38.987,32.711,38.915,32.935,38.845,33.151,38.774,33.372,39.057,33.514,39.268,33.62,39.564,33.768,39.85,33.911,40.122,34.048,40.421,34.198,40.689,34.332,40.935,34.387,40.987,34.429,41.099,34.612,41.195,34.769,41.199,34.805,41.2,35.027,41.216,35.288,41.248,35.427,41.303,35.551,41.354,35.64,41.359,35.725,41.353,35.81,41.3,35.939,41.246,36.073,41.252,36.203,41.262,36.272,41.296,36.383,41.354,36.464,41.417,36.515,41.65,36.566,41.789,36.597,41.974,36.741,42.084,36.826,42.237,36.961,42.35,37.061,42.359,37.095,42.359,37.109]]]},{"n":"Iran","c":"IR","r":2,"ml":2.5,"lx":54.93,"ly":32.17,"b":[44.023,25.102,63.305,39.769],"p":[[[56.188,26.921,56.095,26.801,55.954,26.701,55.894,26.732,55.848,26.731,55.747,26.692,55.675,26.686,55.543,26.618,55.424,26.583,55.34,26.586,55.312,26.593,55.295,26.639,55.296,26.658,55.347,26.648,55.532,26.71,55.763,26.812,55.785,26.857,55.747,26.931,55.758,26.948,55.907,26.91,56.074,26.983,56.214,27.003,56.279,26.952,56.188,26.921]],[[53.914,37.344,54.192,37.332,54.3,37.354,54.459,37.408,54.579,37.44,54.64,37.445,54.699,37.47,54.745,37.502,54.849,37.723,54.9,37.778,55.076,37.902,55.225,37.981,55.381,38.051,55.578,38.1,55.841,38.095,56.05,38.078,56.171,38.078,56.229,38.073,56.272,38.08,56.297,38.095,56.324,38.191,56.367,38.223,56.441,38.249,56.544,38.25,56.67,38.257,56.775,38.25,56.907,38.213,57.079,38.21,57.194,38.216,57.26,38.18,57.308,38.13,57.331,38.089,57.337,38.033,57.336,37.99,57.354,37.973,57.424,37.948,57.521,37.928,57.711,37.905,57.888,37.861,57.981,37.83,58.109,37.783,58.262,37.666,58.318,37.647,58.387,37.635,58.436,37.639,58.55,37.688,58.65,37.652,58.701,37.656,58.815,37.683,58.937,37.65,59.241,37.521,59.274,37.524,59.302,37.511,59.327,37.481,59.345,37.445,59.367,37.334,59.455,37.253,59.562,37.179,59.687,37.138,59.949,37.042,60.063,36.963,60.178,36.829,60.321,36.654,60.341,36.638,60.708,36.643,61.12,36.643,61.17,36.572,61.16,36.433,61.175,36.29,61.212,36.191,61.212,36.099,61.183,36.053,61.159,36,61.153,35.977,61.206,35.944,61.252,35.868,61.259,35.762,61.236,35.706,61.239,35.659,61.262,35.62,61.282,35.553,61.279,35.514,61.246,35.474,61.226,35.424,61.199,35.362,61.189,35.312,61.14,35.289,61.1,35.272,61.107,35.209,61.126,35.157,61.15,35.094,61.123,35.051,61.107,35.001,61.07,34.922,61.08,34.856,61.04,34.799,60.991,34.75,60.958,34.71,60.951,34.654,60.915,34.634,60.845,34.588,60.802,34.555,60.739,34.545,60.726,34.518,60.736,34.492,60.763,34.475,60.804,34.418,60.889,34.319,60.643,34.307,60.57,34.22,60.486,34.095,60.527,33.842,60.486,33.712,60.511,33.639,60.574,33.588,60.655,33.56,60.806,33.559,60.907,33.539,60.917,33.505,60.859,33.456,60.767,33.364,60.718,33.324,60.561,33.138,60.562,33.059,60.577,32.995,60.645,32.794,60.71,32.6,60.829,32.249,60.827,32.168,60.79,31.987,60.788,31.877,60.804,31.734,60.792,31.661,60.821,31.495,60.854,31.483,61.111,31.451,61.346,31.422,61.66,31.382,61.755,31.285,61.814,31.073,61.811,30.913,61.784,30.832,61.559,30.599,61.332,30.364,61.104,30.128,60.843,29.859,61.034,29.663,61.152,29.543,61.318,29.373,61.339,29.332,61.338,29.265,61.509,29.006,61.569,28.871,61.623,28.792,61.758,28.668,61.89,28.547,62.033,28.491,62.131,28.479,62.353,28.415,62.434,28.364,62.565,28.235,62.718,28.253,62.749,28.253,62.758,28.244,62.763,28.202,62.74,28.002,62.782,27.801,62.812,27.497,62.801,27.445,62.764,27.357,62.763,27.3,62.753,27.266,62.763,27.25,62.812,27.229,62.915,27.218,63.167,27.252,63.196,27.244,63.256,27.208,63.302,27.151,63.305,27.125,63.242,27.078,63.231,26.998,63.25,26.879,63.242,26.865,63.186,26.838,63.168,26.666,63.158,26.65,63.093,26.632,62.787,26.644,62.752,26.639,62.636,26.594,62.439,26.561,62.385,26.543,62.312,26.491,62.26,26.427,62.25,26.369,62.239,26.357,62.126,26.369,62.089,26.318,61.87,26.242,61.842,26.226,61.81,26.165,61.781,25.996,61.754,25.843,61.738,25.821,61.669,25.769,61.662,25.751,61.671,25.692,61.64,25.585,61.615,25.286,61.588,25.202,61.533,25.196,61.49,25.154,61.412,25.102,61.243,25.142,61.109,25.184,60.664,25.282,60.615,25.33,60.588,25.414,60.511,25.437,60.4,25.312,60.025,25.384,59.897,25.362,59.818,25.401,59.616,25.403,59.456,25.481,59.227,25.428,59.046,25.417,58.798,25.555,58.531,25.592,58.314,25.581,58.203,25.592,58.022,25.641,57.937,25.692,57.796,25.653,57.733,25.725,57.335,25.792,57.261,25.919,57.206,26.037,57.201,26.159,57.104,26.371,57.072,26.68,57.036,26.801,56.982,26.905,56.91,26.995,56.813,27.09,56.728,27.128,56.356,27.2,56.284,27.191,56.118,27.143,55.941,27.038,55.65,26.978,55.592,26.932,55.519,26.83,55.424,26.771,55.294,26.786,55.155,26.725,54.896,26.557,54.759,26.505,54.645,26.509,54.522,26.589,54.247,26.697,54.069,26.732,53.823,26.708,53.706,26.726,53.507,26.852,53.455,26.943,53.342,27.004,52.983,27.142,52.692,27.323,52.638,27.392,52.603,27.493,52.476,27.617,52.192,27.717,52.031,27.824,51.842,27.848,51.666,27.845,51.589,27.864,51.519,27.91,51.279,28.131,51.276,28.219,51.128,28.435,51.094,28.512,51.062,28.726,51.021,28.782,50.867,28.87,50.843,28.928,50.876,29.004,50.876,29.063,50.796,29.117,50.675,29.147,50.646,29.212,50.668,29.34,50.65,29.42,50.544,29.548,50.387,29.679,50.23,29.873,50.169,29.921,50.129,30.048,50.072,30.199,49.983,30.209,49.555,30.029,49.43,30.13,49.054,30.307,49.028,30.333,49.002,30.374,49.049,30.397,49.096,30.407,49.19,30.375,49.247,30.413,49.225,30.472,49.13,30.509,49.002,30.507,49.037,30.45,48.917,30.397,48.891,30.328,48.909,30.241,48.919,30.121,48.87,30.062,48.832,30.035,48.671,30.028,48.596,29.975,48.546,29.962,48.479,30.004,48.435,30.038,48.399,30.11,48.388,30.16,48.401,30.188,48.383,30.23,48.331,30.285,48.279,30.316,48.226,30.321,48.182,30.355,48.148,30.417,48.066,30.458,48.015,30.466,48.013,30.656,48.012,30.824,48.011,30.99,47.836,30.996,47.679,31.002,47.679,31.142,47.679,31.401,47.754,31.601,47.83,31.794,47.715,31.936,47.592,32.088,47.512,32.151,47.418,32.34,47.371,32.424,47.33,32.456,47.285,32.474,47.121,32.467,46.969,32.568,46.789,32.688,46.57,32.834,46.377,32.929,46.298,32.95,46.113,32.958,46.093,32.976,46.08,33.028,46.081,33.087,46.141,33.174,46.146,33.23,46.02,33.416,45.981,33.47,45.873,33.492,45.895,33.546,45.895,33.582,45.879,33.61,45.854,33.623,45.823,33.625,45.738,33.603,45.674,33.687,45.473,33.925,45.409,33.954,45.397,33.971,45.446,34.044,45.529,34.153,45.543,34.216,45.527,34.285,45.438,34.415,45.459,34.47,45.498,34.534,45.501,34.582,45.561,34.575,45.638,34.574,45.662,34.613,45.66,34.749,45.678,34.798,45.921,35.029,46.042,35.08,46.134,35.128,46.155,35.197,46.136,35.232,46.118,35.284,46.112,35.322,46.011,35.425,45.975,35.477,45.971,35.524,45.995,35.608,46.037,35.673,46.181,35.711,46.263,35.744,46.273,35.773,46.167,35.821,45.941,35.835,45.776,35.822,45.723,35.837,45.645,35.928,45.562,35.977,45.484,36.009,45.408,36.003,45.362,36.015,45.351,36.055,45.241,36.356,45.207,36.397,45.155,36.407,45.112,36.409,45.084,36.43,45.053,36.472,45.031,36.526,45.029,36.598,45.034,36.659,45.019,36.698,44.981,36.738,44.928,36.766,44.881,36.799,44.798,37.064,44.765,37.135,44.765,37.142,44.767,37.156,44.758,37.217,44.797,37.27,44.794,37.29,44.715,37.357,44.604,37.424,44.574,37.435,44.573,37.506,44.577,37.56,44.567,37.609,44.546,37.636,44.545,37.658,44.59,37.71,44.561,37.745,44.398,37.829,44.336,37.872,44.223,37.88,44.211,37.908,44.229,37.967,44.268,38.039,44.329,38.109,44.349,38.146,44.373,38.21,44.381,38.255,44.45,38.318,44.45,38.334,44.431,38.357,44.376,38.37,44.32,38.375,44.299,38.386,44.291,38.42,44.298,38.558,44.28,38.641,44.257,38.701,44.272,38.836,44.232,38.863,44.171,38.934,44.145,38.994,44.159,39.017,44.172,39.056,44.181,39.108,44.178,39.145,44.121,39.181,44.079,39.218,44.074,39.26,44.058,39.311,44.034,39.351,44.023,39.377,44.044,39.393,44.124,39.405,44.24,39.397,44.335,39.396,44.389,39.422,44.456,39.667,44.517,39.731,44.587,39.769,44.725,39.682,44.782,39.651,44.817,39.65,44.838,39.629,45,39.424,45.072,39.363,45.113,39.312,45.141,39.254,45.191,39.216,45.256,39.195,45.336,39.139,45.389,39.096,45.48,39.006,45.575,38.973,45.922,38.908,46.114,38.878,46.17,38.869,46.318,38.913,46.491,38.907,46.555,38.904,46.783,39.087,46.853,39.148,46.989,39.18,47.065,39.253,47.188,39.341,47.338,39.424,47.476,39.498,47.582,39.543,47.773,39.649,47.892,39.685,47.996,39.684,48.151,39.561,48.282,39.448,48.322,39.399,48.257,39.355,48.136,39.312,48.113,39.281,48.104,39.241,48.109,39.203,48.125,39.172,48.274,39.099,48.291,39.059,48.292,39.019,48.275,38.994,48.242,38.979,48.139,38.959,48.05,38.935,48.019,38.912,47.993,38.884,47.996,38.854,48.023,38.819,48.205,38.724,48.225,38.689,48.261,38.642,48.306,38.613,48.381,38.606,48.417,38.586,48.593,38.411,48.636,38.399,48.84,38.437,48.869,38.435,48.871,38.393,48.901,38.144,48.925,38.015,48.96,37.89,49.015,37.776,49.081,37.668,49.171,37.601,49.372,37.52,49.47,37.497,49.727,37.481,49.981,37.445,50.13,37.407,50.176,37.381,50.214,37.34,50.338,37.149,50.533,37.014,50.927,36.81,51.119,36.743,51.762,36.615,52.19,36.622,53.374,36.869,53.768,36.93,53.915,36.93,53.827,36.881,53.679,36.853,53.769,36.818,53.906,36.813,53.97,36.818,54.016,36.85,54.024,36.901,54.017,36.952,53.952,37.182,53.914,37.344]]]},{"n":"Indonesia","c":"ID","r":2,"ml":1.7,"lx":101.89,"ly":-0.95,"b":[95.207,-10.91,140.976,5.907],"p":[[[97.482,1.465,97.698,1.184,97.786,1.146,97.903,1.018,97.932,0.974,97.902,0.884,97.876,0.628,97.82,0.564,97.684,0.596,97.683,0.641,97.604,0.834,97.461,0.941,97.405,0.947,97.369,1.057,97.297,1.187,97.079,1.425,97.244,1.424,97.324,1.482,97.343,1.528,97.356,1.54,97.482,1.465]],[[99.164,-1.778,99.072,-1.783,98.874,-1.664,98.828,-1.61,98.816,-1.538,98.627,-1.261,98.602,-1.198,98.676,-0.971,98.869,-0.916,98.933,-0.954,98.955,-1.056,99.065,-1.241,99.101,-1.34,99.129,-1.384,99.14,-1.418,99.131,-1.442,99.21,-1.559,99.267,-1.628,99.271,-1.738,99.164,-1.778]],[[116.641,-8.614,116.514,-8.821,116.559,-8.854,116.587,-8.886,116.377,-8.929,116.29,-8.906,116.239,-8.912,116.027,-8.873,115.875,-8.826,115.857,-8.788,115.869,-8.743,115.914,-8.758,116.032,-8.765,116.076,-8.745,116.078,-8.611,116.061,-8.437,116.22,-8.295,116.304,-8.238,116.402,-8.204,116.647,-8.283,116.688,-8.304,116.719,-8.336,116.734,-8.387,116.641,-8.614]],[[115.448,-8.155,115.549,-8.208,115.691,-8.364,115.704,-8.407,115.661,-8.448,115.56,-8.514,115.334,-8.616,115.295,-8.664,115.247,-8.758,115.236,-8.798,115.22,-8.82,115.194,-8.835,115.145,-8.849,115.092,-8.829,115.14,-8.769,115.142,-8.697,115.106,-8.629,115.055,-8.573,114.952,-8.496,114.842,-8.429,114.731,-8.394,114.613,-8.378,114.571,-8.345,114.502,-8.261,114.479,-8.215,114.468,-8.166,114.475,-8.119,114.504,-8.117,114.62,-8.128,114.833,-8.183,114.938,-8.187,114.998,-8.174,115.154,-8.066,115.191,-8.067,115.34,-8.115,115.448,-8.155]],[[106.046,-1.669,106.08,-1.738,106.127,-1.8,106.162,-1.867,106.209,-2.189,106.366,-2.465,106.818,-2.573,106.744,-2.618,106.707,-2.658,106.679,-2.704,106.612,-2.896,106.619,-2.936,106.658,-3.001,106.667,-3.072,106.611,-3.071,106.547,-3.056,106.496,-3.029,106.449,-2.994,106.397,-2.967,106.342,-2.949,106.25,-2.894,106.126,-2.855,105.999,-2.825,105.937,-2.744,105.908,-2.643,105.939,-2.493,105.908,-2.452,105.862,-2.415,105.807,-2.307,105.786,-2.181,105.705,-2.133,105.599,-2.103,105.553,-2.079,105.343,-2.125,105.293,-2.114,105.248,-2.079,105.133,-2.043,105.138,-1.973,105.191,-1.917,105.316,-1.861,105.375,-1.813,105.387,-1.751,105.364,-1.705,105.373,-1.657,105.413,-1.611,105.46,-1.575,105.585,-1.527,105.64,-1.61,105.668,-1.68,105.701,-1.731,105.754,-1.659,105.72,-1.534,105.816,-1.506,105.91,-1.505,105.981,-1.539,106.027,-1.593,106.046,-1.669]],[[123.18,-4.551,123.203,-4.766,123.196,-4.823,123.139,-4.74,123.119,-4.723,123.104,-4.74,123.084,-4.749,123.055,-4.748,123.018,-4.832,123.015,-4.91,122.987,-4.963,122.972,-5.138,122.981,-5.186,123.025,-5.162,123.051,-5.156,123.15,-5.224,123.202,-5.273,123.187,-5.333,123.121,-5.393,123.043,-5.419,122.986,-5.394,122.969,-5.406,122.935,-5.437,122.909,-5.477,122.916,-5.519,122.85,-5.638,122.812,-5.671,122.733,-5.635,122.684,-5.666,122.645,-5.663,122.585,-5.545,122.586,-5.489,122.642,-5.426,122.643,-5.381,122.67,-5.331,122.731,-5.262,122.767,-5.21,122.768,-5.177,122.794,-5.052,122.804,-5,122.821,-4.944,122.849,-4.831,122.853,-4.618,122.947,-4.443,123.038,-4.395,123.075,-4.387,123.069,-4.434,123.18,-4.551]],[[122.645,-5.269,122.619,-5.336,122.564,-5.387,122.52,-5.391,122.474,-5.381,122.392,-5.335,122.371,-5.383,122.307,-5.381,122.283,-5.32,122.329,-5.138,122.396,-5.07,122.39,-4.999,122.334,-4.847,122.369,-4.767,122.524,-4.707,122.66,-4.634,122.702,-4.619,122.74,-4.675,122.76,-4.934,122.614,-5.139,122.645,-5.269]],[[108.316,3.69,108.18,3.653,108.1,3.705,108.186,3.768,108.216,3.772,108.236,3.785,108.243,3.81,108.088,3.852,108.045,3.889,108.002,3.983,108.004,4.043,108.202,4.2,108.248,4.217,108.256,4.152,108.393,3.986,108.399,3.876,108.394,3.836,108.316,3.69]],[[108.207,-2.998,108.192,-3.103,108.167,-3.143,108.084,-3.195,108.055,-3.227,107.977,-3.222,107.967,-3.167,107.941,-3.129,107.858,-3.086,107.837,-3.097,107.822,-3.161,107.66,-3.206,107.614,-3.209,107.637,-3.125,107.595,-3.058,107.592,-2.977,107.584,-2.941,107.563,-2.92,107.605,-2.863,107.598,-2.8,107.642,-2.732,107.666,-2.566,107.838,-2.53,107.875,-2.56,108.074,-2.597,108.215,-2.697,108.291,-2.83,108.207,-2.998]],[[135.383,-0.651,135.596,-0.69,135.673,-0.688,135.749,-0.733,135.841,-0.712,135.894,-0.726,136.069,-0.878,136.155,-0.978,136.283,-1.065,136.375,-1.094,136.305,-1.173,136.165,-1.215,136.11,-1.217,136.003,-1.17,135.915,-1.178,135.839,-1.119,135.826,-1.028,135.747,-0.823,135.646,-0.882,135.524,-0.787,135.491,-0.785,135.483,-0.801,135.432,-0.769,135.388,-0.705,135.383,-0.651]],[[135.474,-1.592,135.869,-1.642,135.976,-1.636,136.202,-1.655,136.39,-1.722,136.719,-1.734,136.817,-1.754,136.893,-1.8,136.709,-1.838,136.622,-1.873,136.461,-1.89,136.326,-1.872,136.228,-1.894,136.193,-1.859,136.049,-1.824,135.866,-1.752,135.488,-1.668,135.47,-1.616,135.474,-1.592]],[[130.813,-0.004,130.987,-0.047,131.026,-0.04,131.277,-0.15,131.317,-0.204,131.303,-0.241,131.34,-0.29,131.258,-0.366,131.218,-0.374,131.178,-0.346,131.098,-0.33,131.005,-0.361,130.946,-0.338,130.897,-0.268,130.808,-0.226,130.683,-0.081,130.622,-0.086,130.638,-0.143,130.691,-0.181,130.761,-0.291,130.802,-0.302,130.843,-0.298,130.899,-0.344,130.896,-0.416,130.75,-0.444,130.7,-0.392,130.689,-0.297,130.607,-0.329,130.575,-0.362,130.551,-0.366,130.496,-0.267,130.341,-0.262,130.237,-0.21,130.288,-0.155,130.295,-0.101,130.363,-0.073,130.431,-0.098,130.5,-0.06,130.548,-0.07,130.584,-0.045,130.722,-0.03,130.813,-0.004]],[[128.454,2.052,128.296,2.035,128.26,2.083,128.218,2.297,128.33,2.469,128.472,2.571,128.569,2.596,128.602,2.598,128.688,2.474,128.623,2.224,128.548,2.097,128.454,2.052]],[[128.153,-1.661,128.092,-1.701,128.061,-1.712,127.914,-1.685,127.741,-1.691,127.562,-1.729,127.458,-1.697,127.392,-1.645,127.395,-1.59,127.457,-1.454,127.592,-1.351,127.647,-1.332,127.743,-1.36,127.905,-1.439,128.033,-1.532,128.149,-1.604,128.153,-1.661]],[[126.055,-2.451,126.038,-2.469,125.978,-2.415,125.938,-2.263,125.903,-2.222,125.863,-2.077,125.873,-2.036,125.923,-1.975,125.963,-1.976,125.993,-2.012,125.976,-2.168,126.066,-2.366,126.055,-2.451]],[[126.024,-1.79,126.332,-1.823,126.288,-1.859,125.956,-1.917,125.839,-1.906,125.479,-1.94,125.433,-1.938,125.426,-1.882,125.387,-1.843,125.445,-1.809,125.521,-1.801,125.72,-1.814,126.024,-1.79]],[[124.97,-1.705,125.063,-1.741,125.096,-1.741,125.127,-1.699,125.146,-1.693,125.188,-1.713,125.198,-1.78,125.258,-1.771,125.305,-1.794,125.32,-1.81,125.314,-1.877,125.135,-1.889,125.007,-1.943,124.834,-1.894,124.639,-1.978,124.521,-2.007,124.418,-2.005,124.33,-1.859,124.381,-1.687,124.418,-1.659,124.483,-1.644,124.664,-1.636,124.97,-1.705]],[[131.326,-8,131.309,-8.011,131.185,-7.998,131.114,-7.997,131.123,-7.922,131.087,-7.865,131.137,-7.782,131.138,-7.685,131.19,-7.672,131.197,-7.617,131.26,-7.471,131.297,-7.438,131.349,-7.425,131.411,-7.34,131.446,-7.315,131.483,-7.251,131.535,-7.221,131.531,-7.165,131.561,-7.136,131.643,-7.113,131.701,-7.14,131.736,-7.197,131.644,-7.267,131.691,-7.439,131.624,-7.626,131.58,-7.682,131.498,-7.731,131.474,-7.777,131.377,-7.869,131.348,-7.948,131.343,-7.981,131.326,-8]],[[126.801,-7.668,126.814,-7.717,126.813,-7.738,126.693,-7.754,126.577,-7.808,126.518,-7.87,126.472,-7.95,126.313,-7.918,126.171,-7.912,126.108,-7.884,126.04,-7.886,125.952,-7.911,125.826,-7.979,125.798,-7.985,125.808,-7.881,125.843,-7.817,125.975,-7.663,126.085,-7.697,126.214,-7.707,126.359,-7.677,126.463,-7.608,126.61,-7.572,126.726,-7.662,126.801,-7.668]],[[124.576,-8.141,124.6,-8.202,124.677,-8.168,124.752,-8.16,124.924,-8.166,125.05,-8.18,125.125,-8.205,125.132,-8.326,125.097,-8.353,124.444,-8.445,124.381,-8.415,124.356,-8.386,124.426,-8.296,124.394,-8.253,124.431,-8.183,124.509,-8.135,124.576,-8.141]],[[131.002,-1.316,130.967,-1.343,130.845,-1.317,130.782,-1.255,130.739,-1.173,130.712,-1.104,130.704,-1.05,130.668,-0.984,130.673,-0.96,130.897,-0.89,130.939,-0.915,131.033,-0.918,131.074,-0.968,131.046,-1.188,131.002,-1.316]],[[96.493,5.229,96.615,5.22,96.843,5.274,96.968,5.269,97.086,5.23,97.19,5.207,97.451,5.236,97.5,5.228,97.547,5.206,97.588,5.17,97.707,5.04,97.908,4.88,97.967,4.777,98,4.662,98.021,4.635,98.248,4.415,98.273,4.322,98.241,4.195,98.307,4.093,98.528,3.998,98.659,3.928,98.687,3.886,98.706,3.835,98.778,3.759,98.869,3.71,99.151,3.581,99.521,3.311,99.732,3.183,99.907,2.988,99.969,2.895,100.021,2.794,100.127,2.648,100.307,2.467,100.353,2.411,100.401,2.332,100.457,2.257,100.524,2.189,100.604,2.137,100.685,2.12,100.817,1.989,100.888,1.948,100.877,2.051,100.817,2.14,100.818,2.194,100.828,2.243,100.877,2.283,100.936,2.295,101.046,2.257,101.225,2.102,101.301,2.012,101.358,1.887,101.405,1.757,101.477,1.693,101.575,1.671,101.684,1.661,101.785,1.621,102.02,1.442,102.098,1.358,102.157,1.259,102.198,1.142,102.223,1.019,102.239,0.99,102.39,0.842,102.469,0.779,102.566,0.749,102.849,0.715,102.949,0.664,103.032,0.579,103.067,0.492,103.008,0.415,102.786,0.298,102.55,0.216,102.78,0.244,102.896,0.279,103.003,0.332,103.109,0.4,103.277,0.495,103.339,0.514,103.412,0.507,103.479,0.48,103.579,0.387,103.673,0.289,103.743,0.174,103.787,0.047,103.706,-0.02,103.589,-0.069,103.429,-0.192,103.412,-0.24,103.444,-0.272,103.405,-0.362,103.495,-0.418,103.509,-0.466,103.431,-0.534,103.439,-0.576,103.533,-0.755,103.578,-0.796,103.721,-0.887,103.94,-0.979,104.061,-1.021,104.199,-1.054,104.258,-1.053,104.361,-1.038,104.381,-1.074,104.426,-1.251,104.447,-1.362,104.478,-1.6,104.519,-1.699,104.516,-1.819,104.569,-1.922,104.676,-1.987,104.791,-2.041,104.845,-2.093,104.845,-2.172,104.826,-2.234,104.787,-2.283,104.668,-2.386,104.647,-2.43,104.631,-2.543,104.651,-2.595,104.698,-2.598,104.736,-2.571,104.878,-2.419,104.917,-2.392,104.971,-2.371,105.026,-2.358,105.287,-2.356,105.397,-2.38,105.495,-2.43,105.582,-2.492,105.899,-2.888,106.044,-3.106,106.056,-3.161,106.058,-3.217,106.034,-3.261,105.901,-3.41,105.885,-3.451,105.844,-3.614,105.852,-3.731,105.896,-3.78,105.93,-3.833,105.928,-3.881,105.841,-4.122,105.831,-4.163,105.887,-4.554,105.891,-4.66,105.879,-4.794,105.887,-5.01,105.816,-5.677,105.803,-5.716,105.748,-5.818,105.677,-5.818,105.619,-5.8,105.578,-5.761,105.556,-5.712,105.523,-5.673,105.349,-5.55,105.304,-5.57,105.128,-5.723,105.081,-5.746,105.023,-5.727,104.93,-5.681,104.64,-5.52,104.622,-5.572,104.618,-5.642,104.676,-5.816,104.684,-5.893,104.631,-5.908,104.602,-5.905,104.481,-5.803,104.37,-5.691,104.243,-5.539,104.15,-5.467,104.067,-5.386,103.831,-5.08,103.77,-5.033,103.406,-4.816,103.332,-4.765,103.239,-4.676,103.139,-4.596,102.919,-4.471,102.538,-4.152,102.372,-3.969,102.188,-3.675,102.128,-3.599,101.818,-3.378,101.649,-3.244,101.579,-3.167,101.414,-2.899,101.366,-2.808,101.306,-2.729,101.206,-2.664,101.119,-2.588,100.944,-2.345,100.89,-2.249,100.848,-2.144,100.855,-1.934,100.487,-1.299,100.394,-1.101,100.308,-0.827,100.289,-0.799,100.088,-0.553,100.017,-0.474,99.931,-0.4,99.86,-0.314,99.721,-0.033,99.67,0.045,99.598,0.102,99.335,0.209,99.236,0.268,99.159,0.352,99.112,0.459,99.06,0.686,98.936,1.032,98.796,1.495,98.703,1.702,98.595,1.865,98.564,1.902,98.087,2.195,98.005,2.238,97.919,2.264,97.795,2.283,97.701,2.359,97.662,2.494,97.641,2.676,97.617,2.785,97.591,2.847,97.391,2.975,97.313,3.077,97.248,3.189,97.188,3.276,96.969,3.575,96.894,3.654,96.801,3.709,96.525,3.767,96.445,3.816,96.311,3.986,96.23,4.073,95.988,4.263,95.579,4.662,95.495,4.761,95.432,4.865,95.381,4.976,95.207,5.284,95.221,5.346,95.247,5.411,95.243,5.464,95.224,5.517,95.228,5.565,95.28,5.593,95.396,5.629,95.517,5.625,95.629,5.609,95.737,5.579,95.841,5.515,96.027,5.351,96.133,5.294,96.251,5.267,96.493,5.229]],[[122.783,-8.612,122.642,-8.647,122.554,-8.681,122.47,-8.725,122.417,-8.735,122.321,-8.738,122.186,-8.73,122.094,-8.745,121.839,-8.86,121.738,-8.87,121.651,-8.899,121.621,-8.854,121.585,-8.821,121.5,-8.812,121.415,-8.815,121.328,-8.917,121.191,-8.896,121.138,-8.904,121.086,-8.926,121.035,-8.935,120.982,-8.928,120.781,-8.849,120.55,-8.802,120.32,-8.82,120.121,-8.777,120.012,-8.81,119.909,-8.858,119.879,-8.808,119.841,-8.764,119.808,-8.698,119.807,-8.623,119.818,-8.571,119.848,-8.523,119.866,-8.473,119.875,-8.42,119.918,-8.445,119.964,-8.436,120.099,-8.378,120.231,-8.29,120.354,-8.258,120.425,-8.249,120.486,-8.266,120.547,-8.26,120.61,-8.24,120.71,-8.308,120.751,-8.321,120.886,-8.327,121.009,-8.366,121.118,-8.424,121.277,-8.478,121.372,-8.551,121.445,-8.578,121.498,-8.585,121.548,-8.575,121.61,-8.526,121.683,-8.506,121.747,-8.507,121.863,-8.494,121.912,-8.482,121.967,-8.455,122.02,-8.472,122.067,-8.497,122.263,-8.625,122.323,-8.628,122.433,-8.601,122.467,-8.566,122.484,-8.514,122.514,-8.47,122.556,-8.432,122.604,-8.402,122.75,-8.353,122.85,-8.304,122.919,-8.222,122.759,-8.186,122.792,-8.127,122.846,-8.093,122.917,-8.106,122.978,-8.152,123.006,-8.329,122.955,-8.354,122.924,-8.381,122.902,-8.416,122.811,-8.481,122.847,-8.562,122.82,-8.596,122.783,-8.612]],[[120.013,-9.375,120.058,-9.42,120.221,-9.506,120.248,-9.543,120.258,-9.603,120.291,-9.648,120.365,-9.655,120.444,-9.646,120.504,-9.674,120.556,-9.719,120.633,-9.806,120.7,-9.903,120.784,-9.957,120.833,-10.037,120.804,-10.108,120.698,-10.207,120.64,-10.228,120.562,-10.236,120.439,-10.294,120.395,-10.263,120.255,-10.242,120.145,-10.2,120.052,-10.123,119.998,-10.04,119.931,-9.967,119.813,-9.917,119.601,-9.774,119.47,-9.761,119.417,-9.771,119.363,-9.772,119.085,-9.707,119.042,-9.669,119.008,-9.621,118.977,-9.573,118.959,-9.519,118.994,-9.472,119.031,-9.44,119.186,-9.384,119.296,-9.367,119.424,-9.37,119.615,-9.352,119.795,-9.38,119.851,-9.36,119.942,-9.301,119.974,-9.322,120.013,-9.375]],[[118.242,-8.318,118.292,-8.357,118.338,-8.354,118.433,-8.293,118.491,-8.271,118.552,-8.27,118.612,-8.281,118.671,-8.323,118.692,-8.393,118.714,-8.415,118.748,-8.331,118.794,-8.306,118.846,-8.293,118.926,-8.298,118.988,-8.338,119.044,-8.457,119.042,-8.561,119.063,-8.6,119.101,-8.628,119.13,-8.668,119.104,-8.71,119.079,-8.73,119.006,-8.75,118.971,-8.741,118.939,-8.713,118.903,-8.703,118.821,-8.712,118.746,-8.735,118.756,-8.774,118.818,-8.791,118.837,-8.809,118.833,-8.833,118.808,-8.838,118.728,-8.805,118.674,-8.812,118.479,-8.856,118.427,-8.855,118.398,-8.813,118.4,-8.704,118.379,-8.675,118.234,-8.808,118.19,-8.841,118.132,-8.856,118.071,-8.851,117.861,-8.931,117.795,-8.92,117.732,-8.92,117.508,-9.008,117.388,-9.032,117.326,-9.034,117.265,-9.026,117.21,-9.034,117.161,-9.069,117.061,-9.099,116.958,-9.076,116.871,-9.046,116.788,-9.006,116.768,-8.955,116.772,-8.894,116.807,-8.811,116.783,-8.665,116.801,-8.598,116.835,-8.532,116.886,-8.508,116.953,-8.503,117.064,-8.444,117.165,-8.367,117.224,-8.375,117.357,-8.429,117.435,-8.435,117.567,-8.426,117.622,-8.46,117.643,-8.536,117.673,-8.563,117.712,-8.583,117.806,-8.711,117.893,-8.704,117.97,-8.728,118.104,-8.65,118.206,-8.652,118.235,-8.592,118.174,-8.528,118.1,-8.475,118.061,-8.464,118.018,-8.467,117.979,-8.459,117.815,-8.342,117.766,-8.279,117.738,-8.205,117.755,-8.15,117.868,-8.101,117.921,-8.089,118.117,-8.122,118.151,-8.15,118.203,-8.267,118.242,-8.318]],[[124.889,0.995,124.698,0.826,124.64,0.744,124.589,0.655,124.514,0.557,124.428,0.471,124.384,0.445,124.278,0.398,124.217,0.38,124.101,0.375,123.754,0.306,123.64,0.297,123.526,0.3,123.31,0.318,123.265,0.327,123.179,0.416,123.083,0.486,122.997,0.494,122.91,0.486,122.281,0.481,122.061,0.468,121.842,0.437,121.723,0.451,121.605,0.486,121.516,0.498,121.426,0.495,121.013,0.442,120.909,0.447,120.7,0.515,120.579,0.528,120.46,0.51,120.349,0.449,120.307,0.408,120.192,0.269,120.127,0.167,120.078,0.04,120.036,-0.09,120.013,-0.196,120.012,-0.307,120.032,-0.432,120.063,-0.556,120.097,-0.65,120.241,-0.868,120.27,-0.899,120.425,-0.961,120.518,-1.039,120.605,-1.258,120.667,-1.37,120.729,-1.371,120.797,-1.364,120.916,-1.378,121.034,-1.407,121.149,-1.339,121.213,-1.212,121.277,-1.118,121.431,-0.939,121.519,-0.856,121.576,-0.829,121.633,-0.84,121.681,-0.888,121.738,-0.926,121.853,-0.946,121.97,-0.933,122.094,-0.875,122.138,-0.839,122.175,-0.794,122.28,-0.757,122.53,-0.757,122.659,-0.77,122.889,-0.755,122.886,-0.722,122.841,-0.687,122.829,-0.659,122.872,-0.641,123.02,-0.6,123.171,-0.571,123.281,-0.592,123.38,-0.649,123.417,-0.707,123.434,-0.778,123.396,-0.962,123.378,-1.004,123.3,-1.026,123.226,-1.002,123.153,-0.907,123.049,-0.872,122.903,-0.901,122.853,-0.928,122.807,-0.966,122.725,-1.064,122.656,-1.175,122.507,-1.348,122.334,-1.498,122.251,-1.555,122.158,-1.594,121.859,-1.693,121.78,-1.767,121.719,-1.863,121.651,-1.895,121.573,-1.906,121.514,-1.888,121.395,-1.834,121.355,-1.878,121.349,-1.946,121.408,-1.97,121.502,-2.045,121.575,-2.151,121.622,-2.174,121.726,-2.208,121.77,-2.241,121.848,-2.332,121.972,-2.542,122.014,-2.656,122.083,-2.75,122.292,-2.908,122.303,-2.952,122.29,-3.004,122.307,-3.052,122.381,-3.142,122.399,-3.201,122.317,-3.275,122.313,-3.383,122.263,-3.527,122.251,-3.576,122.253,-3.62,122.288,-3.662,122.329,-3.694,122.385,-3.711,122.435,-3.74,122.529,-3.853,122.579,-3.882,122.61,-3.923,122.607,-3.985,122.65,-4.021,122.69,-4.084,122.75,-4.1,122.779,-4.082,122.798,-4.054,122.848,-4.065,122.877,-4.109,122.894,-4.166,122.9,-4.229,122.897,-4.349,122.872,-4.392,122.818,-4.39,122.72,-4.341,122.715,-4.376,122.722,-4.411,122.672,-4.422,122.615,-4.417,122.471,-4.422,122.207,-4.496,122.114,-4.54,122.054,-4.62,122.05,-4.675,122.073,-4.792,122.038,-4.832,121.917,-4.848,121.748,-4.817,121.646,-4.786,121.589,-4.76,121.514,-4.681,121.487,-4.581,121.541,-4.283,121.557,-4.245,121.583,-4.211,121.612,-4.156,121.618,-4.093,121.537,-4.015,121.416,-3.984,121.313,-3.919,120.914,-3.556,120.892,-3.521,120.891,-3.46,120.907,-3.404,121.038,-3.205,121.054,-3.167,121.07,-3.01,121.067,-2.881,121.052,-2.752,120.99,-2.67,120.879,-2.646,120.765,-2.642,120.654,-2.668,120.544,-2.733,120.341,-2.87,120.261,-2.949,120.254,-3.053,120.3,-3.154,120.36,-3.247,120.392,-3.348,120.437,-3.707,120.435,-3.748,120.383,-3.852,120.363,-4.086,120.385,-4.415,120.42,-4.617,120.405,-4.727,120.31,-4.963,120.281,-5.093,120.279,-5.146,120.391,-5.393,120.417,-5.49,120.43,-5.591,120.312,-5.542,120.256,-5.544,120.201,-5.559,120.077,-5.575,119.952,-5.578,119.908,-5.596,119.818,-5.662,119.764,-5.688,119.717,-5.693,119.557,-5.611,119.463,-5.522,119.376,-5.425,119.36,-5.314,119.391,-5.201,119.434,-5.079,119.52,-4.877,119.516,-4.742,119.545,-4.631,119.594,-4.523,119.612,-4.424,119.624,-4.034,119.611,-4,119.494,-3.769,119.48,-3.73,119.479,-3.667,119.492,-3.608,119.495,-3.554,119.467,-3.513,119.42,-3.475,119.362,-3.459,119.24,-3.475,118.995,-3.538,118.922,-3.483,118.868,-3.398,118.833,-3.28,118.813,-3.157,118.822,-3.041,118.858,-2.929,118.829,-2.85,118.784,-2.765,118.783,-2.721,118.809,-2.682,118.853,-2.65,118.908,-2.631,118.958,-2.597,119.092,-2.483,119.135,-2.382,119.138,-2.258,119.172,-2.14,119.241,-2.031,119.322,-1.93,119.348,-1.825,119.308,-1.66,119.324,-1.584,119.31,-1.496,119.309,-1.408,119.359,-1.243,119.508,-0.907,119.654,-0.728,119.711,-0.681,119.787,-0.764,119.844,-0.862,119.845,-0.773,119.83,-0.686,119.772,-0.484,119.722,-0.088,119.736,-0.051,119.787,-0.057,119.838,-0.022,119.866,0.04,119.812,0.187,119.809,0.239,119.913,0.445,119.998,0.52,120.035,0.567,120.056,0.693,120.101,0.74,120.157,0.774,120.23,0.861,120.27,0.971,120.294,0.979,120.322,0.983,120.367,0.888,120.416,0.849,120.517,0.818,120.603,0.854,120.626,0.902,120.659,0.944,120.711,0.987,120.755,1.036,120.804,1.149,120.868,1.253,120.912,1.289,120.965,1.312,121.025,1.326,121.082,1.328,121.208,1.263,121.282,1.25,121.357,1.255,121.404,1.244,121.44,1.214,121.473,1.156,121.513,1.105,121.551,1.08,121.592,1.068,121.867,1.089,122.108,1.031,122.437,1.018,122.549,0.984,122.657,0.941,122.79,0.863,122.838,0.846,122.892,0.85,122.96,0.923,123.013,0.939,123.067,0.942,123.278,0.928,123.847,0.838,123.931,0.85,124.274,1.022,124.411,1.185,124.534,1.23,124.575,1.304,124.6,1.392,124.644,1.416,124.747,1.441,124.788,1.468,124.861,1.576,124.947,1.672,124.989,1.701,125.111,1.686,125.165,1.644,125.234,1.502,125.222,1.479,125.141,1.408,125.117,1.379,125.028,1.18,124.967,1.083,124.889,0.995]],[[107.374,-6.008,107.475,-6.122,107.563,-6.183,107.667,-6.216,107.776,-6.219,107.884,-6.233,108.009,-6.277,108.138,-6.297,108.197,-6.289,108.254,-6.267,108.295,-6.265,108.33,-6.286,108.419,-6.383,108.516,-6.471,108.538,-6.516,108.604,-6.729,108.678,-6.791,108.78,-6.808,108.899,-6.808,109.018,-6.817,109.294,-6.867,109.404,-6.86,109.501,-6.81,109.587,-6.843,109.821,-6.902,109.936,-6.916,110.067,-6.899,110.198,-6.895,110.261,-6.912,110.321,-6.938,110.373,-6.948,110.426,-6.947,110.521,-6.897,110.584,-6.806,110.634,-6.69,110.674,-6.57,110.701,-6.518,110.737,-6.472,110.784,-6.443,110.835,-6.424,110.972,-6.436,111.001,-6.465,111.154,-6.669,111.182,-6.687,111.342,-6.7,111.387,-6.693,111.484,-6.652,111.54,-6.648,111.644,-6.699,111.688,-6.742,111.738,-6.773,111.99,-6.806,112.087,-6.893,112.137,-6.905,112.312,-6.894,112.434,-6.903,112.539,-6.926,112.587,-7.051,112.626,-7.178,112.649,-7.221,112.752,-7.265,112.794,-7.304,112.783,-7.432,112.795,-7.552,113.014,-7.658,113.248,-7.718,113.498,-7.724,113.747,-7.703,113.876,-7.677,114.037,-7.632,114.071,-7.633,114.383,-7.771,114.409,-7.792,114.444,-7.896,114.443,-8.005,114.385,-8.263,114.381,-8.334,114.387,-8.405,114.449,-8.559,114.482,-8.604,114.595,-8.685,114.599,-8.727,114.584,-8.77,114.459,-8.741,114.383,-8.705,114.339,-8.647,114.277,-8.615,114.16,-8.626,113.94,-8.568,113.693,-8.478,113.253,-8.287,113.134,-8.288,113.019,-8.313,112.898,-8.361,112.772,-8.396,112.679,-8.409,112.586,-8.4,112.352,-8.354,112.115,-8.324,111.51,-8.305,111.339,-8.262,111.055,-8.24,110.83,-8.202,110.607,-8.149,110.039,-7.891,109.853,-7.828,109.282,-7.705,109.194,-7.695,108.987,-7.704,108.856,-7.668,108.741,-7.667,108.571,-7.707,108.518,-7.736,108.452,-7.797,108.336,-7.794,108.221,-7.782,107.917,-7.724,107.804,-7.688,107.696,-7.636,107.598,-7.567,107.547,-7.542,107.285,-7.472,107.071,-7.447,106.631,-7.416,106.535,-7.394,106.455,-7.369,106.411,-7.312,106.417,-7.239,106.448,-7.177,106.492,-7.114,106.52,-7.054,106.198,-6.928,105.944,-6.859,105.835,-6.846,105.725,-6.846,105.601,-6.86,105.478,-6.854,105.421,-6.833,105.362,-6.826,105.303,-6.841,105.255,-6.835,105.243,-6.778,105.273,-6.729,105.336,-6.674,105.371,-6.664,105.387,-6.751,105.405,-6.768,105.46,-6.787,105.484,-6.782,105.581,-6.671,105.608,-6.617,105.655,-6.47,105.706,-6.498,105.757,-6.48,105.787,-6.457,105.868,-6.116,105.936,-6.017,106.029,-5.934,106.075,-5.914,106.166,-5.965,106.35,-5.984,106.459,-6.018,106.569,-6.022,106.676,-6.038,106.825,-6.098,106.878,-6.092,106.932,-6.073,107.012,-6.008,107.046,-5.904,107.162,-5.957,107.332,-5.978,107.374,-6.008]],[[109.629,2.028,109.539,1.896,109.549,1.848,109.571,1.806,109.636,1.777,109.654,1.615,109.736,1.523,109.818,1.439,109.879,1.398,109.945,1.338,109.992,1.283,110.041,1.236,110.115,1.19,110.315,0.996,110.399,0.939,110.461,0.882,110.506,0.862,110.615,0.878,110.938,1.017,110.996,1.026,111.101,1.051,111.287,1.043,111.483,0.996,111.547,0.994,111.607,1.023,111.691,1.014,111.77,0.999,111.809,1.012,111.923,1.113,112.079,1.143,112.129,1.244,112.167,1.338,112.186,1.439,112.251,1.48,112.342,1.515,112.476,1.559,112.943,1.567,112.988,1.548,112.998,1.496,112.988,1.457,113.007,1.434,113.069,1.432,113.126,1.408,113.359,1.327,113.458,1.302,113.513,1.308,113.622,1.236,113.682,1.261,113.76,1.311,113.835,1.38,113.902,1.434,114,1.455,114.126,1.452,114.275,1.471,114.387,1.5,114.513,1.452,114.546,1.467,114.567,1.514,114.632,1.617,114.661,1.686,114.686,1.819,114.704,1.851,114.751,1.869,114.8,1.894,114.813,1.934,114.831,1.98,114.816,2.019,114.788,2.052,114.759,2.162,114.768,2.213,114.786,2.25,114.836,2.269,114.969,2.351,115.087,2.446,115.151,2.493,115.179,2.523,115.181,2.567,115.13,2.612,115.081,2.634,115.077,2.687,115.079,2.723,115.094,2.758,115.087,2.791,115.086,2.841,115.118,2.895,115.19,2.974,115.247,3.026,115.31,2.994,115.384,3.009,115.454,3.034,115.493,3.128,115.499,3.173,115.49,3.209,115.514,3.342,115.52,3.362,115.566,3.446,115.571,3.502,115.545,3.634,115.561,3.733,115.568,3.939,115.596,3.976,115.628,4.082,115.679,4.193,115.782,4.254,115.837,4.333,115.861,4.348,115.896,4.349,116.022,4.291,116.134,4.355,116.236,4.363,116.32,4.354,116.368,4.327,116.415,4.308,116.515,4.371,116.553,4.36,116.589,4.338,116.639,4.339,116.698,4.355,116.844,4.34,117.101,4.337,117.278,4.299,117.451,4.193,117.537,4.171,117.574,4.171,117.566,4.162,117.497,4.133,117.465,4.076,117.559,3.988,117.566,3.93,117.639,3.878,117.728,3.797,117.732,3.77,117.762,3.734,117.777,3.689,117.714,3.645,117.63,3.636,117.567,3.678,117.51,3.73,117.495,3.666,117.45,3.629,117.288,3.639,117.172,3.639,117.056,3.623,117.114,3.613,117.166,3.592,117.346,3.427,117.385,3.365,117.322,3.244,117.352,3.194,117.422,3.165,117.507,3.105,117.567,3.098,117.611,3.064,117.612,3.005,117.638,2.951,117.569,2.929,117.637,2.915,117.698,2.887,117.665,2.859,117.639,2.825,117.667,2.807,117.75,2.776,117.786,2.747,117.805,2.669,117.886,2.542,118.034,2.378,118.067,2.318,118.066,2.263,118.042,2.215,117.957,2.16,117.889,2.087,117.881,2.061,117.789,2.027,117.831,2.002,117.865,1.968,117.928,1.867,118.08,1.702,118.157,1.64,118.472,1.416,118.639,1.319,118.853,1.096,118.963,1.044,118.985,0.982,118.892,0.887,118.757,0.839,118.535,0.814,118.312,0.847,118.196,0.874,118.096,0.929,118.016,1.039,117.912,1.099,117.952,1.032,117.977,0.964,117.964,0.89,117.923,0.831,117.853,0.789,117.777,0.754,117.745,0.73,117.553,0.341,117.522,0.236,117.464,-0.2,117.463,-0.324,117.549,-0.554,117.557,-0.675,117.574,-0.728,117.563,-0.771,117.522,-0.797,117.357,-0.867,117.241,-0.926,117.146,-1.009,117.07,-1.113,117.003,-1.188,116.914,-1.224,116.849,-1.218,116.797,-1.184,116.761,-1.117,116.74,-1.044,116.726,-1.098,116.729,-1.151,116.759,-1.207,116.771,-1.267,116.753,-1.327,116.715,-1.376,116.612,-1.429,116.554,-1.474,116.545,-1.553,116.518,-1.598,116.478,-1.633,116.332,-1.712,116.3,-1.744,116.275,-1.785,116.353,-1.779,116.424,-1.785,116.43,-1.864,116.452,-1.923,116.424,-2.053,116.314,-2.14,116.369,-2.158,116.418,-2.187,116.528,-2.208,116.565,-2.3,116.549,-2.411,116.529,-2.511,116.45,-2.538,116.401,-2.52,116.353,-2.522,116.317,-2.552,116.307,-2.603,116.375,-2.578,116.372,-2.707,116.353,-2.833,116.331,-2.902,116.289,-2.959,116.226,-2.977,116.166,-2.935,116.154,-2.984,116.172,-3.025,116.257,-3.126,116.205,-3.149,116.167,-3.183,116.15,-3.233,116.058,-3.348,116.017,-3.433,115.999,-3.523,115.956,-3.595,115.258,-3.907,114.694,-4.17,114.653,-4.152,114.625,-4.112,114.606,-3.703,114.536,-3.494,114.526,-3.377,114.446,-3.482,114.397,-3.471,114.344,-3.444,114.305,-3.41,114.302,-3.365,114.344,-3.235,114.293,-3.306,114.236,-3.361,114.178,-3.354,114.128,-3.327,114.109,-3.285,114.082,-3.279,113.959,-3.394,113.796,-3.456,113.705,-3.455,113.634,-3.42,113.637,-3.332,113.63,-3.246,113.61,-3.196,113.566,-3.178,113.526,-3.184,113.409,-3.229,113.367,-3.224,113.343,-3.246,113.034,-2.933,112.971,-3.187,112.758,-3.322,112.6,-3.4,112.444,-3.371,112.285,-3.321,112.127,-3.381,111.955,-3.53,111.907,-3.553,111.858,-3.552,111.822,-3.533,111.834,-3.42,111.836,-3.308,111.823,-3.057,111.809,-3.008,111.76,-2.939,111.695,-2.889,111.658,-2.926,111.625,-2.975,111.495,-2.973,111.368,-2.934,111.259,-2.956,111.044,-3.056,110.93,-3.071,110.869,-3.049,110.83,-2.995,110.852,-2.946,110.899,-2.909,110.811,-2.938,110.736,-2.989,110.703,-3.021,110.668,-3.005,110.574,-2.891,110.378,-2.934,110.351,-2.947,110.303,-2.985,110.256,-2.966,110.233,-2.925,110.224,-2.689,110.124,-2.234,110.097,-2.001,110.075,-1.946,109.96,-1.863,109.964,-1.743,110.023,-1.643,110.036,-1.526,110.019,-1.399,109.983,-1.275,109.938,-1.181,109.873,-1.101,109.787,-1.011,109.682,-0.944,109.454,-0.869,109.333,-0.875,109.289,-0.846,109.259,-0.807,109.271,-0.732,109.312,-0.68,109.366,-0.667,109.373,-0.638,109.257,-0.577,109.161,-0.495,109.13,-0.445,109.121,-0.391,109.122,-0.265,109.15,-0.186,109.165,-0.142,109.195,-0.009,109.258,0.031,109.247,0.056,109.22,0.074,109.181,0.117,109.149,0.168,109.075,0.253,108.945,0.356,108.923,0.533,108.906,0.794,108.917,0.913,108.959,1.135,109.031,1.204,109.088,1.224,109.132,1.254,109.096,1.258,109.065,1.247,109.01,1.24,109.055,1.438,109.076,1.496,109.167,1.607,109.273,1.705,109.318,1.821,109.379,1.923,109.629,2.028]],[[127.733,0.848,127.805,0.826,127.881,0.832,127.919,0.877,127.929,0.935,127.967,1.043,128.055,1.116,128.117,1.127,128.161,1.158,128.153,1.238,128.157,1.317,128.222,1.401,128.424,1.518,128.539,1.559,128.688,1.573,128.705,1.528,128.688,1.464,128.717,1.367,128.703,1.106,128.669,1.069,128.515,0.979,128.346,0.907,128.299,0.877,128.257,0.805,128.261,0.734,128.398,0.639,128.611,0.55,128.655,0.508,128.684,0.438,128.692,0.36,128.743,0.323,128.815,0.305,128.863,0.268,128.9,0.216,128.54,0.338,128.446,0.392,128.333,0.398,128.221,0.414,128.106,0.461,127.983,0.472,127.924,0.438,127.901,0.372,127.887,0.298,127.915,0.206,127.912,0.151,127.889,0.05,127.978,-0.248,128.089,-0.485,128.254,-0.732,128.335,-0.816,128.425,-0.893,128.278,-0.87,128.233,-0.788,128.046,-0.706,128.011,-0.657,127.889,-0.424,127.853,-0.38,127.741,-0.3,127.692,-0.242,127.675,-0.163,127.687,-0.08,127.681,0.035,127.685,0.149,127.709,0.288,127.669,0.337,127.616,0.383,127.555,0.49,127.537,0.611,127.542,0.681,127.567,0.743,127.601,0.796,127.608,0.848,127.52,0.924,127.429,1.14,127.42,1.252,127.537,1.467,127.535,1.572,127.558,1.634,127.571,1.7,127.632,1.844,127.731,1.966,127.9,2.137,127.964,2.175,128.036,2.199,128.043,2.157,128.031,2.12,127.907,1.946,127.89,1.906,127.887,1.833,127.946,1.79,128.011,1.701,128.024,1.583,128.026,1.458,128.012,1.332,127.988,1.29,127.885,1.163,127.653,1.014,127.633,0.977,127.634,0.936,127.677,0.887,127.733,0.848]],[[129.755,-2.866,129.984,-2.977,130.103,-2.993,130.304,-2.979,130.379,-2.989,130.57,-3.131,130.626,-3.228,130.642,-3.312,130.671,-3.392,130.718,-3.411,130.773,-3.419,130.846,-3.533,130.86,-3.57,130.805,-3.858,130.58,-3.749,130.363,-3.625,130.27,-3.579,130.02,-3.475,129.981,-3.439,129.953,-3.392,129.844,-3.327,129.627,-3.317,129.545,-3.319,129.512,-3.329,129.52,-3.363,129.522,-3.434,129.468,-3.453,129.333,-3.409,129.212,-3.393,129.108,-3.349,128.967,-3.326,128.952,-3.304,128.964,-3.272,128.958,-3.241,128.925,-3.229,128.863,-3.235,128.802,-3.266,128.751,-3.3,128.677,-3.397,128.639,-3.433,128.517,-3.449,128.466,-3.44,128.419,-3.416,128.28,-3.241,128.233,-3.203,128.181,-3.172,128.132,-3.157,128.082,-3.184,128.056,-3.239,128.044,-3.303,128.03,-3.341,127.97,-3.444,127.92,-3.506,127.902,-3.496,127.928,-3.397,127.928,-3.341,127.897,-3.282,127.878,-3.222,128.113,-2.935,128.199,-2.866,128.57,-2.842,128.791,-2.857,128.911,-2.85,128.991,-2.829,129.058,-2.838,129.074,-2.895,129.116,-2.937,129.174,-2.933,129.28,-2.889,129.371,-2.821,129.427,-2.791,129.484,-2.786,129.543,-2.79,129.6,-2.806,129.755,-2.866]],[[126.861,-3.088,127.025,-3.166,127.063,-3.217,127.092,-3.278,127.125,-3.311,127.163,-3.338,127.227,-3.391,127.244,-3.471,127.23,-3.633,127.155,-3.647,127.085,-3.671,126.941,-3.765,126.87,-3.783,126.794,-3.789,126.74,-3.814,126.686,-3.824,126.547,-3.772,126.411,-3.711,126.215,-3.605,126.178,-3.579,126.147,-3.523,126.057,-3.421,126.034,-3.356,126.026,-3.171,126.05,-3.128,126.088,-3.105,126.22,-3.148,126.306,-3.103,126.555,-3.065,126.808,-3.069,126.861,-3.088]],[[124.922,-8.942,124.915,-9.032,124.937,-9.053,124.973,-9.064,125.1,-9.004,125.124,-9.015,125.149,-9.043,125.149,-9.123,125.1,-9.19,124.978,-9.195,124.96,-9.214,124.959,-9.255,124.968,-9.294,124.997,-9.326,125.034,-9.382,125.068,-9.512,124.998,-9.565,124.963,-9.666,124.842,-9.76,124.708,-9.914,124.602,-9.993,124.508,-10.086,124.428,-10.149,124.327,-10.17,124.176,-10.183,123.971,-10.295,123.858,-10.344,123.747,-10.347,123.644,-10.311,123.605,-10.27,123.614,-10.215,123.648,-10.168,123.69,-10.129,123.716,-10.079,123.599,-10.015,123.589,-9.967,123.636,-9.838,123.666,-9.705,123.709,-9.615,123.877,-9.453,123.977,-9.373,124.036,-9.342,124.052,-9.375,124.09,-9.416,124.116,-9.423,124.135,-9.414,124.282,-9.428,124.319,-9.414,124.376,-9.35,124.413,-9.314,124.438,-9.239,124.444,-9.19,124.575,-9.155,124.646,-9.117,124.708,-9.062,124.89,-8.968,124.922,-8.942]],[[134.747,-5.707,134.739,-5.746,134.738,-5.817,134.755,-5.883,134.712,-5.95,134.752,-6.05,134.758,-6.1,134.756,-6.171,134.744,-6.202,134.714,-6.295,134.684,-6.328,134.661,-6.337,134.638,-6.365,134.441,-6.335,134.356,-6.271,134.28,-6.201,134.264,-6.172,134.175,-6.09,134.155,-6.063,134.153,-6.02,134.225,-6.008,134.302,-6.01,134.299,-5.971,134.343,-5.833,134.226,-5.744,134.205,-5.707,134.247,-5.682,134.341,-5.713,134.456,-5.558,134.49,-5.525,134.506,-5.438,134.571,-5.427,134.617,-5.439,134.646,-5.492,134.658,-5.539,134.646,-5.581,134.701,-5.603,134.747,-5.707]],[[134.537,-6.442,134.52,-6.513,134.504,-6.591,134.413,-6.68,134.356,-6.815,134.323,-6.849,134.2,-6.909,134.091,-6.834,134.059,-6.769,134.107,-6.472,134.154,-6.481,134.185,-6.479,134.195,-6.46,134.125,-6.426,134.111,-6.255,134.115,-6.191,134.168,-6.176,134.234,-6.226,134.318,-6.316,134.415,-6.387,134.537,-6.442]],[[138.535,-8.274,138.296,-8.405,137.983,-8.382,137.872,-8.38,137.688,-8.412,137.65,-8.386,137.685,-8.262,137.833,-7.932,138.008,-7.642,138.082,-7.566,138.185,-7.495,138.296,-7.438,138.544,-7.38,138.77,-7.39,138.802,-7.415,138.899,-7.512,138.963,-7.588,138.989,-7.696,138.893,-7.882,138.786,-8.059,138.612,-8.198,138.535,-8.274]],[[140.973,-2.61,140.973,-2.614,140.973,-2.681,140.974,-2.803,140.974,-3.007,140.974,-3.21,140.974,-3.413,140.974,-3.617,140.974,-3.82,140.974,-4.023,140.974,-4.226,140.974,-4.43,140.974,-4.633,140.974,-4.836,140.975,-5.04,140.975,-5.243,140.975,-5.446,140.975,-5.65,140.975,-5.853,140.975,-6.056,140.975,-6.259,140.975,-6.346,140.944,-6.452,140.875,-6.612,140.862,-6.74,140.92,-6.84,140.975,-6.905,140.975,-7.073,140.975,-7.276,140.975,-7.479,140.975,-7.683,140.976,-7.886,140.976,-8.089,140.976,-8.292,140.976,-8.496,140.976,-8.699,140.976,-8.902,140.976,-9.106,140.976,-9.119,140.925,-9.085,140.787,-8.974,140.662,-8.847,140.581,-8.728,140.49,-8.62,140.102,-8.301,140.003,-8.196,139.983,-8.167,139.993,-8.139,140.037,-8.084,140.117,-7.924,140.034,-8.023,139.935,-8.101,139.791,-8.106,139.649,-8.125,139.519,-8.173,139.386,-8.189,139.319,-8.166,139.279,-8.107,139.258,-8.047,139.249,-7.982,139.193,-8.086,139.083,-8.143,138.933,-8.262,138.891,-8.238,138.865,-8.192,138.856,-8.145,138.885,-8.095,138.905,-8.041,138.936,-7.913,139.003,-7.838,139.046,-7.691,139.074,-7.639,139.088,-7.587,139.049,-7.528,138.983,-7.508,138.938,-7.472,138.886,-7.373,138.853,-7.34,138.794,-7.299,138.748,-7.251,138.798,-7.216,138.865,-7.201,138.919,-7.204,139.018,-7.226,139.063,-7.227,139.177,-7.19,139.113,-7.202,139.049,-7.201,138.846,-7.136,138.72,-7.07,138.601,-6.937,138.6,-6.911,138.684,-6.887,138.865,-6.858,138.808,-6.79,138.727,-6.731,138.698,-6.626,138.642,-6.56,138.522,-6.454,138.439,-6.343,138.368,-6.119,138.296,-5.949,138.314,-5.887,138.375,-5.844,138.283,-5.839,138.2,-5.807,138.244,-5.724,138.34,-5.676,138.252,-5.688,138.167,-5.712,138.127,-5.717,138.087,-5.709,138.066,-5.676,138.063,-5.629,138.076,-5.546,138.061,-5.465,137.985,-5.428,137.922,-5.37,137.887,-5.349,137.84,-5.35,137.795,-5.312,137.759,-5.256,137.307,-5.014,137.28,-4.945,137.238,-4.976,137.196,-4.99,137.144,-4.951,137.089,-4.924,137.03,-4.929,136.975,-4.907,136.917,-4.895,136.857,-4.893,136.619,-4.819,136.394,-4.701,136.211,-4.651,136.097,-4.585,135.98,-4.531,135.717,-4.478,135.45,-4.443,135.354,-4.442,135.273,-4.453,135.196,-4.451,134.754,-4.195,134.68,-4.079,134.687,-4.011,134.707,-3.955,134.887,-3.938,134.76,-3.922,134.708,-3.93,134.603,-3.976,134.547,-3.979,134.467,-3.949,134.391,-3.91,134.266,-3.946,134.202,-3.887,134.18,-3.825,134.147,-3.797,134.1,-3.8,134.037,-3.822,133.974,-3.818,133.933,-3.776,133.904,-3.72,133.861,-3.68,133.808,-3.65,133.723,-3.578,133.678,-3.479,133.683,-3.309,133.697,-3.248,133.782,-3.149,133.842,-3.055,133.767,-3.044,133.7,-3.087,133.672,-3.132,133.661,-3.186,133.653,-3.364,133.599,-3.416,133.518,-3.412,133.542,-3.516,133.509,-3.616,133.415,-3.732,133.407,-3.785,133.422,-3.843,133.401,-3.899,133.249,-4.062,133.198,-4.07,133.085,-4.069,132.969,-4.095,132.914,-4.057,132.87,-4.007,132.837,-3.949,132.791,-3.828,132.754,-3.704,132.87,-3.551,132.83,-3.413,132.751,-3.295,132.554,-3.131,132.348,-2.975,132.255,-2.943,132.102,-2.93,132.054,-2.915,132.006,-2.856,131.971,-2.789,132.067,-2.76,132.231,-2.68,132.323,-2.684,132.575,-2.727,132.653,-2.766,132.725,-2.789,132.897,-2.658,133.034,-2.487,133.119,-2.45,133.191,-2.438,133.265,-2.454,133.411,-2.514,133.527,-2.542,133.609,-2.547,133.652,-2.601,133.7,-2.625,133.711,-2.544,133.753,-2.451,133.835,-2.422,133.878,-2.415,133.905,-2.391,133.899,-2.304,133.791,-2.294,133.85,-2.22,133.902,-2.184,133.921,-2.147,133.922,-2.102,133.71,-2.189,133.488,-2.226,133.356,-2.216,133.225,-2.214,132.963,-2.273,132.863,-2.27,132.631,-2.247,132.503,-2.218,132.403,-2.24,132.308,-2.242,132.207,-2.176,132.122,-2.092,132.08,-2.033,132.023,-1.99,131.998,-1.933,131.936,-1.715,131.93,-1.56,131.83,-1.557,131.731,-1.541,131.294,-1.393,131.241,-1.43,131.179,-1.448,131.118,-1.455,131.057,-1.448,130.996,-1.425,131.001,-1.384,131.046,-1.284,131.091,-1.247,131.152,-1.219,131.191,-1.166,131.254,-1.007,131.259,-0.953,131.252,-0.897,131.257,-0.855,131.296,-0.834,131.462,-0.782,131.804,-0.704,131.891,-0.657,131.962,-0.582,132.046,-0.537,132.084,-0.491,132.128,-0.454,132.394,-0.355,132.508,-0.347,132.625,-0.359,132.856,-0.417,133.077,-0.512,133.268,-0.636,133.473,-0.726,133.724,-0.741,133.85,-0.731,133.975,-0.744,134.025,-0.77,134.112,-0.847,134.087,-0.897,134.072,-1.002,134.116,-1.102,134.188,-1.203,134.247,-1.311,134.26,-1.363,134.237,-1.474,134.217,-1.529,134.145,-1.621,134.106,-1.721,134.131,-1.845,134.145,-1.969,134.143,-2.083,134.156,-2.195,134.195,-2.309,134.362,-2.621,134.46,-2.832,134.491,-2.714,134.483,-2.583,134.518,-2.536,134.567,-2.51,134.627,-2.537,134.645,-2.59,134.649,-2.706,134.702,-2.934,134.77,-2.944,134.843,-2.909,134.855,-2.979,134.853,-3.108,134.887,-3.21,134.917,-3.25,135.037,-3.333,135.092,-3.349,135.252,-3.369,135.372,-3.375,135.487,-3.345,135.561,-3.269,135.628,-3.186,135.859,-2.995,135.926,-2.904,135.991,-2.764,136.013,-2.734,136.243,-2.583,136.27,-2.529,136.303,-2.426,136.352,-2.325,136.39,-2.273,136.612,-2.224,136.843,-2.198,137.072,-2.105,137.171,-2.025,137.176,-1.973,137.125,-1.881,137.123,-1.841,137.176,-1.802,137.381,-1.686,137.617,-1.566,137.806,-1.483,137.911,-1.484,138.008,-1.557,138.111,-1.616,138.65,-1.791,138.736,-1.846,138.811,-1.918,138.919,-1.968,139.039,-1.992,139.149,-2.039,139.253,-2.099,139.482,-2.212,139.79,-2.348,139.868,-2.356,140.155,-2.35,140.204,-2.376,140.251,-2.412,140.295,-2.42,140.623,-2.446,140.673,-2.472,140.721,-2.508,140.747,-2.607,140.973,-2.61]],[[138.895,-8.389,138.846,-8.402,138.594,-8.371,138.567,-8.33,138.563,-8.309,138.621,-8.268,138.677,-8.199,138.763,-8.173,138.796,-8.174,138.898,-8.337,138.895,-8.389]],[[101.708,2.078,101.762,1.997,101.774,1.943,101.734,1.883,101.719,1.789,101.603,1.716,101.501,1.733,101.468,1.759,101.403,1.901,101.41,2.022,101.45,2.068,101.545,2.061,101.641,2.127,101.708,2.078]],[[102.427,0.99,102.381,0.96,102.325,1.007,102.28,1.076,102.255,1.147,102.234,1.264,102.229,1.348,102.256,1.397,102.276,1.395,102.359,1.346,102.413,1.261,102.443,1.234,102.449,1.156,102.429,1.067,102.427,0.99]],[[102.492,1.459,102.499,1.331,102.425,1.364,102.367,1.415,102.274,1.453,102.161,1.465,102.079,1.499,102.021,1.558,102.018,1.586,102.024,1.608,102.042,1.625,102.47,1.51,102.492,1.459]],[[103.028,0.747,103.009,0.708,102.971,0.737,102.776,0.78,102.711,0.784,102.542,0.832,102.49,0.857,102.454,0.89,102.466,0.95,102.491,0.987,102.507,1.089,102.549,1.13,102.633,1.054,102.726,0.989,102.78,0.959,102.944,0.893,103.002,0.859,103.028,0.747]],[[103.166,0.87,103.137,0.842,103.087,0.848,103.033,0.882,102.964,0.943,102.886,0.997,102.788,1.031,102.726,1.041,102.702,1.054,102.726,1.159,102.79,1.165,102.999,1.068,103.068,1.015,103.166,0.87]],[[103.284,0.542,103.172,0.536,103.14,0.549,103.153,0.643,103.187,0.7,103.238,0.699,103.295,0.614,103.284,0.542]],[[103.45,0.664,103.43,0.651,103.344,0.778,103.366,0.851,103.386,0.87,103.433,0.825,103.47,0.778,103.497,0.723,103.45,0.664]],[[104.025,1.181,104.088,1.137,104.14,1.166,104.138,1.128,104.127,1.092,104.066,0.99,103.964,1.013,103.94,1.046,103.932,1.071,103.947,1.087,103.955,1.137,104,1.137,104.025,1.181]],[[104.585,1.216,104.591,1.141,104.648,1.105,104.663,1.05,104.653,0.961,104.599,0.859,104.575,0.832,104.504,0.853,104.481,0.887,104.471,0.913,104.481,0.933,104.429,0.956,104.462,0.996,104.439,1.05,104.294,1.016,104.252,1.015,104.244,1.077,104.25,1.103,104.362,1.181,104.428,1.196,104.5,1.18,104.585,1.216]],[[104.779,-0.176,104.808,-0.192,104.843,-0.141,104.909,-0.212,104.95,-0.247,105.005,-0.283,104.951,-0.284,104.929,-0.317,104.914,-0.323,104.702,-0.209,104.567,-0.246,104.474,-0.212,104.447,-0.189,104.497,-0.126,104.543,0.018,104.636,-0.018,104.658,-0.063,104.653,-0.076,104.713,-0.103,104.779,-0.176]],[[104.474,-0.335,104.568,-0.432,104.59,-0.467,104.544,-0.521,104.507,-0.597,104.485,-0.613,104.414,-0.584,104.363,-0.659,104.33,-0.539,104.257,-0.463,104.302,-0.386,104.319,-0.38,104.341,-0.383,104.364,-0.403,104.474,-0.335]],[[103.737,-0.348,103.606,-0.383,103.461,-0.358,103.479,-0.297,103.549,-0.228,103.611,-0.231,103.724,-0.277,103.764,-0.318,103.737,-0.348]],[[109.71,-1.181,109.511,-1.283,109.464,-1.278,109.428,-1.241,109.45,-1.044,109.476,-0.985,109.615,-0.979,109.7,-1.007,109.743,-1.039,109.761,-1.105,109.751,-1.145,109.71,-1.181]],[[113.845,-7.105,113.826,-7.12,113.656,-7.112,113.546,-7.193,113.471,-7.218,113.198,-7.218,113.166,-7.207,113.142,-7.208,113.127,-7.224,113.04,-7.212,112.764,-7.14,112.726,-7.073,112.769,-7.001,112.868,-6.9,113.067,-6.88,113.975,-6.873,114.074,-6.96,114.083,-6.989,113.885,-7.049,113.845,-7.105]],[[134.965,-1.116,134.917,-1.134,134.862,-1.114,134.809,-1.038,134.828,-0.979,134.889,-0.938,134.941,-0.979,134.957,-1.031,134.996,-1.034,134.965,-1.116]],[[127.25,-0.495,127.187,-0.521,127.119,-0.521,127.104,-0.414,127.126,-0.279,127.19,-0.256,127.29,-0.284,127.253,-0.319,127.281,-0.391,127.25,-0.495]],[[127.567,-0.319,127.682,-0.468,127.605,-0.61,127.659,-0.689,127.804,-0.694,127.838,-0.724,127.863,-0.76,127.88,-0.809,127.842,-0.848,127.761,-0.884,127.668,-0.832,127.643,-0.784,127.624,-0.766,127.498,-0.802,127.463,-0.806,127.438,-0.739,127.469,-0.643,127.381,-0.6,127.3,-0.5,127.297,-0.46,127.329,-0.391,127.325,-0.336,127.371,-0.332,127.455,-0.406,127.492,-0.336,127.527,-0.307,127.567,-0.319]],[[132.926,-5.902,132.845,-5.988,132.922,-5.785,132.938,-5.683,133.009,-5.621,133.115,-5.311,133.138,-5.318,133.173,-5.348,133.12,-5.576,132.971,-5.736,132.926,-5.902]],[[132.807,-5.851,132.746,-5.947,132.705,-5.913,132.681,-5.913,132.667,-5.856,132.681,-5.739,132.63,-5.607,132.698,-5.609,132.717,-5.648,132.738,-5.662,132.804,-5.789,132.807,-5.851]],[[128.276,-3.675,128.25,-3.711,128.192,-3.735,128.143,-3.733,128.159,-3.698,128.147,-3.677,128.111,-3.686,128.052,-3.715,127.978,-3.771,127.934,-3.743,127.925,-3.699,127.928,-3.679,128.016,-3.601,128.119,-3.587,128.264,-3.512,128.329,-3.516,128.314,-3.564,128.291,-3.598,128.277,-3.633,128.276,-3.675]],[[126.817,4.033,126.776,4.013,126.711,4.02,126.704,4.071,126.77,4.162,126.814,4.258,126.767,4.283,126.722,4.344,126.721,4.416,126.757,4.548,126.813,4.537,126.865,4.48,126.887,4.373,126.921,4.291,126.848,4.18,126.817,4.033]],[[125.658,3.436,125.633,3.405,125.512,3.461,125.518,3.55,125.501,3.593,125.469,3.639,125.455,3.684,125.469,3.733,125.543,3.67,125.586,3.571,125.644,3.477,125.658,3.436]],[[130.862,-8.319,130.775,-8.35,130.833,-8.271,131.02,-8.091,131.087,-8.125,131.176,-8.131,131.044,-8.212,130.908,-8.246,130.862,-8.319]],[[129.839,-7.955,129.78,-8.046,129.713,-8.041,129.592,-7.917,129.599,-7.831,129.609,-7.803,129.655,-7.795,129.813,-7.82,129.844,-7.889,129.839,-7.955]],[[127.823,-8.099,127.998,-8.139,128.099,-8.135,128.119,-8.171,128.024,-8.255,127.821,-8.19,127.786,-8.12,127.823,-8.099]],[[130.353,-1.691,130.365,-1.75,130.425,-1.805,130.404,-1.89,130.381,-1.903,130.393,-1.942,130.419,-1.971,130.373,-1.992,130.339,-1.982,130.284,-2.009,130.248,-2.048,130.133,-2.064,130.093,-2.028,129.887,-1.986,129.754,-1.894,129.738,-1.867,129.994,-1.759,130.106,-1.73,130.2,-1.732,130.318,-1.692,130.353,-1.691]],[[102.367,-5.479,102.286,-5.483,102.136,-5.361,102.111,-5.323,102.154,-5.286,102.198,-5.289,102.372,-5.366,102.405,-5.405,102.367,-5.479]],[[100.425,-3.183,100.465,-3.329,100.346,-3.229,100.348,-3.159,100.332,-3.113,100.26,-3.057,100.204,-2.987,100.179,-2.82,100.199,-2.786,100.246,-2.783,100.455,-3.002,100.469,-3.039,100.464,-3.117,100.434,-3.141,100.425,-3.183]],[[100.204,-2.741,100.133,-2.821,100.015,-2.82,99.992,-2.77,99.997,-2.649,99.968,-2.61,99.969,-2.594,99.988,-2.525,100.012,-2.51,100.202,-2.68,100.204,-2.741]],[[99.843,-2.343,99.848,-2.37,99.685,-2.282,99.607,-2.258,99.537,-2.162,99.559,-2.115,99.562,-2.051,99.572,-2.026,99.622,-2.017,99.686,-2.063,99.735,-2.178,99.816,-2.284,99.843,-2.343]],[[98.459,-0.53,98.4,-0.577,98.31,-0.532,98.34,-0.468,98.355,-0.379,98.409,-0.309,98.427,-0.226,98.323,-0.001,98.375,0.007,98.415,-0.018,98.484,-0.168,98.544,-0.258,98.52,-0.38,98.459,-0.53]],[[96.464,2.36,96.401,2.351,96.341,2.372,96.29,2.43,96.022,2.596,95.938,2.598,95.88,2.641,95.809,2.656,95.733,2.767,95.717,2.826,95.772,2.855,95.806,2.916,95.896,2.889,95.998,2.781,96.102,2.741,96.13,2.721,96.18,2.661,96.417,2.515,96.443,2.466,96.459,2.416,96.464,2.36]],[[122.043,-5.438,121.98,-5.465,121.859,-5.35,121.808,-5.256,121.821,-5.203,121.857,-5.156,121.874,-5.145,121.866,-5.096,121.914,-5.072,121.966,-5.076,122,-5.141,122.041,-5.159,122.062,-5.221,122.043,-5.438]],[[123.212,-1.171,123.234,-1.234,123.198,-1.288,123.238,-1.389,123.339,-1.254,123.435,-1.237,123.489,-1.259,123.527,-1.286,123.547,-1.337,123.512,-1.447,123.449,-1.499,123.367,-1.507,123.329,-1.443,123.275,-1.437,123.237,-1.577,123.221,-1.598,123.173,-1.616,123.13,-1.577,123.123,-1.556,123.183,-1.493,123.15,-1.304,123.105,-1.34,122.984,-1.511,122.89,-1.587,122.858,-1.548,122.811,-1.432,122.832,-1.283,122.908,-1.182,122.972,-1.189,123.158,-1.158,123.212,-1.171]],[[121.864,-0.407,121.907,-0.451,121.881,-0.503,121.847,-0.49,121.756,-0.491,121.722,-0.495,121.681,-0.525,121.655,-0.526,121.672,-0.479,121.749,-0.407,121.797,-0.418,121.864,-0.407]],[[120.528,-6.298,120.487,-6.465,120.468,-6.406,120.461,-6.254,120.436,-6.18,120.452,-6.095,120.446,-5.876,120.477,-5.775,120.534,-5.904,120.549,-5.969,120.528,-6.298]],[[115.377,-6.971,115.296,-6.988,115.22,-6.953,115.222,-6.905,115.241,-6.861,115.354,-6.838,115.414,-6.84,115.479,-6.87,115.524,-6.902,115.546,-6.939,115.424,-6.941,115.377,-6.971]],[[116.303,-3.868,116.093,-4.054,116.059,-4.007,116.077,-3.817,116.018,-3.7,116.022,-3.612,116.064,-3.458,116.117,-3.34,116.239,-3.26,116.27,-3.251,116.262,-3.395,116.287,-3.449,116.295,-3.495,116.282,-3.535,116.305,-3.719,116.319,-3.763,116.289,-3.821,116.303,-3.868]],[[122.949,-10.909,122.856,-10.91,122.826,-10.899,122.818,-10.811,122.846,-10.762,123.061,-10.698,123.146,-10.64,123.265,-10.518,123.34,-10.486,123.358,-10.472,123.371,-10.475,123.383,-10.568,123.413,-10.623,123.418,-10.651,123.311,-10.698,123.215,-10.806,123.005,-10.876,122.949,-10.909]],[[124.287,-8.329,124.226,-8.391,124.184,-8.499,124.147,-8.531,124.066,-8.552,124.017,-8.444,123.928,-8.449,123.971,-8.354,124.014,-8.319,124.069,-8.318,124.096,-8.356,124.111,-8.364,124.24,-8.203,124.266,-8.202,124.287,-8.209,124.304,-8.229,124.287,-8.329]],[[123.925,-8.272,123.784,-8.3,123.698,-8.424,123.629,-8.422,123.592,-8.478,123.583,-8.502,123.588,-8.524,123.58,-8.545,123.553,-8.567,123.489,-8.532,123.434,-8.576,123.411,-8.587,123.33,-8.536,123.253,-8.539,123.23,-8.531,123.325,-8.439,123.455,-8.354,123.476,-8.322,123.425,-8.313,123.395,-8.301,123.391,-8.28,123.473,-8.267,123.53,-8.265,123.573,-8.292,123.601,-8.291,123.776,-8.19,123.846,-8.213,123.896,-8.239,123.925,-8.272]],[[123.317,-8.355,123.297,-8.399,123.025,-8.396,123.033,-8.338,123.108,-8.275,123.133,-8.254,123.217,-8.235,123.336,-8.269,123.317,-8.355]],[[134.716,-6.549,134.661,-6.559,134.634,-6.477,134.679,-6.456,134.729,-6.506,134.716,-6.549]],[[103.424,1.048,103.43,0.993,103.363,1.007,103.315,1.071,103.355,1.117,103.38,1.134,103.405,1.073,103.424,1.048]],[[103.829,0.801,103.834,0.772,103.742,0.83,103.74,0.872,103.752,0.891,103.807,0.846,103.829,0.801]],[[104.239,0.834,104.177,0.805,104.098,0.896,104.101,0.917,104.108,0.934,104.123,0.944,104.171,0.897,104.227,0.88,104.239,0.834]],[[104.689,0.06,104.698,0.035,104.651,0.063,104.622,0.08,104.604,0.095,104.499,0.232,104.544,0.223,104.66,0.103,104.689,0.06]],[[106.285,3.157,106.284,3.088,106.215,3.129,106.201,3.205,106.224,3.23,106.271,3.216,106.285,3.157]],[[105.76,2.863,105.719,2.859,105.706,2.889,105.708,2.94,105.704,2.981,105.692,3.011,105.692,3.063,105.731,3.037,105.76,3.013,105.795,2.996,105.822,2.984,105.837,2.977,105.809,2.904,105.76,2.863]],[[108.888,2.905,108.839,2.853,108.787,2.886,108.867,2.992,108.886,2.999,108.888,2.905]],[[108.953,-1.62,108.838,-1.662,108.804,-1.568,108.877,-1.54,108.957,-1.564,108.953,-1.62]],[[107.473,-2.9,107.433,-2.925,107.409,-2.901,107.402,-2.873,107.419,-2.838,107.474,-2.835,107.5,-2.845,107.473,-2.9]],[[106.886,-3.005,106.87,-3.025,106.814,-3.014,106.774,-2.987,106.749,-2.96,106.743,-2.933,106.797,-2.899,106.911,-2.934,106.886,-3.005]],[[114.413,-7.133,114.398,-7.173,114.347,-7.163,114.299,-7.098,114.322,-7.08,114.349,-7.073,114.384,-7.081,114.413,-7.133]],[[112.719,-5.811,112.698,-5.846,112.602,-5.844,112.586,-5.804,112.649,-5.731,112.69,-5.726,112.727,-5.753,112.719,-5.811]],[[105.253,-6.64,105.19,-6.662,105.143,-6.643,105.121,-6.615,105.192,-6.546,105.226,-6.529,105.261,-6.524,105.277,-6.561,105.253,-6.64]],[[97.334,2.076,97.328,2.053,97.225,2.158,97.108,2.217,97.157,2.232,97.253,2.216,97.291,2.201,97.329,2.149,97.334,2.076]],[[95.362,5.812,95.343,5.784,95.283,5.799,95.218,5.89,95.242,5.907,95.283,5.898,95.359,5.877,95.366,5.843,95.362,5.812]],[[123.598,-1.704,123.529,-1.711,123.483,-1.681,123.487,-1.535,123.529,-1.503,123.549,-1.508,123.561,-1.552,123.582,-1.591,123.616,-1.627,123.598,-1.704]],[[123.242,-4.113,123.145,-4.233,123.076,-4.227,122.995,-4.148,122.971,-4.061,122.969,-4.03,123.025,-3.981,123.212,-3.998,123.247,-4.041,123.242,-4.113]],[[123.848,-1.955,123.866,-1.996,123.804,-1.994,123.777,-1.919,123.783,-1.878,123.848,-1.955]],[[123.153,-1.817,123.079,-1.899,123.071,-1.855,123.086,-1.815,123.106,-1.787,123.138,-1.773,123.153,-1.817]],[[119.464,-8.741,119.425,-8.75,119.386,-8.736,119.402,-8.647,119.379,-8.587,119.42,-8.539,119.43,-8.455,119.446,-8.429,119.471,-8.456,119.482,-8.473,119.502,-8.481,119.547,-8.483,119.557,-8.519,119.555,-8.553,119.536,-8.589,119.483,-8.628,119.444,-8.672,119.464,-8.741]],[[119.074,-8.239,119.03,-8.24,119.021,-8.2,119.037,-8.158,119.079,-8.14,119.098,-8.139,119.128,-8.177,119.135,-8.197,119.107,-8.223,119.074,-8.239]],[[115.61,-8.77,115.582,-8.804,115.501,-8.743,115.48,-8.715,115.541,-8.675,115.561,-8.67,115.613,-8.713,115.61,-8.77]],[[117.649,4.169,117.745,4.167,117.885,4.186,117.918,4.091,117.923,4.054,117.737,4.004,117.625,4.121,117.649,4.169]],[[117.658,3.281,117.646,3.248,117.56,3.328,117.538,3.386,117.548,3.432,117.637,3.436,117.681,3.408,117.658,3.281]],[[124.051,-5.974,124.042,-6.022,124.006,-5.967,123.972,-5.939,123.976,-5.88,124.023,-5.902,124.051,-5.974]],[[123.627,-5.272,123.623,-5.373,123.583,-5.367,123.55,-5.332,123.541,-5.298,123.543,-5.271,123.561,-5.25,123.627,-5.272]],[[120.774,-7.119,120.672,-7.125,120.641,-7.116,120.633,-7.018,120.746,-7.06,120.782,-7.063,120.774,-7.119]],[[117.556,-8.367,117.534,-8.368,117.49,-8.349,117.506,-8.307,117.482,-8.239,117.491,-8.183,117.546,-8.152,117.665,-8.148,117.669,-8.189,117.556,-8.367]],[[116.424,-3.464,116.388,-3.637,116.327,-3.539,116.395,-3.423,116.427,-3.4,116.424,-3.464]],[[127.373,0.791,127.338,0.758,127.306,0.769,127.286,0.812,127.293,0.842,127.32,0.862,127.354,0.847,127.373,0.791]],[[134.374,-2.124,134.345,-2.139,134.335,-2.095,134.351,-2.037,134.37,-2.028,134.391,-2.031,134.419,-2.052,134.374,-2.124]],[[133.571,-4.246,133.622,-4.299,133.503,-4.257,133.333,-4.17,133.321,-4.111,133.464,-4.2,133.571,-4.246]],[[130.905,-0.777,130.88,-0.828,130.832,-0.863,130.402,-0.924,130.439,-0.887,130.457,-0.851,130.484,-0.833,130.527,-0.837,130.548,-0.826,130.57,-0.822,130.594,-0.827,130.635,-0.812,130.723,-0.822,130.813,-0.814,130.807,-0.765,130.905,-0.777]],[[130.627,-0.529,130.569,-0.53,130.465,-0.487,130.526,-0.449,130.564,-0.441,130.597,-0.418,130.616,-0.417,130.657,-0.437,130.684,-0.469,130.627,-0.529]],[[129.549,-0.187,129.506,-0.19,129.469,-0.131,129.37,-0.066,129.309,0.045,129.542,-0.139,129.549,-0.187]],[[127.453,-0.006,127.449,-0.037,127.418,0.006,127.397,0.017,127.42,0.124,127.431,0.143,127.449,0.069,127.453,-0.006]],[[127.42,0.642,127.384,0.631,127.374,0.635,127.363,0.675,127.383,0.744,127.425,0.744,127.443,0.733,127.446,0.683,127.42,0.642]],[[127.3,-0.781,127.289,-0.802,127.185,-0.775,127.156,-0.761,127.209,-0.619,127.258,-0.623,127.301,-0.758,127.3,-0.781]],[[128.722,-3.547,128.72,-3.589,128.713,-3.603,128.659,-3.588,128.62,-3.589,128.585,-3.512,128.595,-3.495,128.667,-3.517,128.694,-3.525,128.722,-3.547]],[[128.563,-3.585,128.392,-3.638,128.428,-3.54,128.452,-3.515,128.536,-3.541,128.563,-3.585]],[[126.719,3.875,126.722,3.833,126.661,3.928,126.638,4.042,126.686,4.001,126.74,3.918,126.719,3.875]],[[126.852,3.768,126.836,3.757,126.8,3.784,126.778,3.813,126.779,3.843,126.804,3.858,126.857,3.812,126.858,3.787,126.852,3.768]],[[125.407,2.652,125.397,2.63,125.36,2.747,125.391,2.805,125.435,2.784,125.446,2.763,125.404,2.707,125.407,2.652]],[[131.982,-7.202,131.97,-7.251,131.927,-7.225,131.884,-7.167,131.823,-7.159,131.778,-7.144,131.751,-7.117,131.922,-7.104,131.982,-7.202]],[[128.67,-7.183,128.625,-7.209,128.55,-7.156,128.53,-7.135,128.577,-7.083,128.628,-7.069,128.658,-7.091,128.673,-7.113,128.667,-7.138,128.67,-7.183]],[[127.419,-7.623,127.355,-7.646,127.375,-7.572,127.371,-7.513,127.475,-7.531,127.474,-7.579,127.464,-7.597,127.419,-7.623]],[[123.416,-10.303,123.326,-10.337,123.326,-10.264,123.395,-10.171,123.459,-10.14,123.494,-10.177,123.497,-10.194,123.405,-10.227,123.416,-10.303]],[[121.883,-10.59,121.833,-10.602,121.726,-10.573,121.705,-10.556,121.796,-10.507,121.867,-10.439,121.95,-10.433,121.998,-10.447,121.981,-10.528,121.883,-10.59]],[[134.82,-6.434,134.795,-6.442,134.795,-6.393,134.823,-6.35,134.852,-6.325,134.886,-6.324,134.82,-6.434]],[[134.674,-6.75,134.657,-6.765,134.631,-6.733,134.629,-6.713,134.663,-6.658,134.698,-6.626,134.736,-6.623,134.726,-6.669,134.674,-6.75]],[[127.988,-2.937,127.938,-3.02,127.85,-3.016,127.834,-3.004,127.938,-2.952,127.988,-2.937]],[[127.606,-3.315,127.629,-3.359,127.531,-3.331,127.488,-3.288,127.53,-3.262,127.554,-3.254,127.606,-3.315]],[[122.977,-8.545,122.946,-8.604,122.888,-8.587,122.904,-8.531,122.933,-8.497,123.011,-8.448,123.089,-8.44,123.138,-8.457,123.153,-8.476,123.03,-8.495,122.977,-8.545]]]},{"n":"India","c":"IN","r":2,"ml":1.7,"lx":79.36,"ly":22.69,"b":[68.165,6.749,97.344,35.496],"p":[[[68.165,23.857,68.234,23.901,68.283,23.928,68.381,23.951,68.489,23.967,68.587,23.967,68.724,23.965,68.728,24.266,68.74,24.292,68.759,24.307,68.781,24.314,68.8,24.309,68.828,24.264,68.863,24.267,68.901,24.292,68.985,24.273,69.052,24.286,69.12,24.269,69.235,24.268,69.443,24.275,69.559,24.273,69.634,24.225,69.716,24.173,69.805,24.165,69.934,24.171,70.021,24.192,70.065,24.241,70.098,24.288,70.289,24.356,70.489,24.412,70.547,24.418,70.565,24.386,70.556,24.331,70.579,24.279,70.659,24.246,70.716,24.238,70.767,24.245,70.805,24.262,70.886,24.344,70.928,24.362,70.983,24.361,71.044,24.4,71.045,24.43,71.006,24.444,70.973,24.487,70.979,24.522,70.97,24.572,70.976,24.619,71.002,24.654,71.048,24.688,71.021,24.758,70.951,24.892,70.878,25.063,70.8,25.206,70.703,25.331,70.652,25.423,70.657,25.626,70.648,25.667,70.615,25.692,70.57,25.706,70.506,25.685,70.449,25.681,70.325,25.686,70.265,25.707,70.1,25.91,70.079,25.99,70.078,26.072,70.133,26.215,70.149,26.348,70.157,26.471,70.148,26.506,70.115,26.548,70.059,26.579,69.911,26.586,69.736,26.627,69.601,26.699,69.507,26.743,69.481,26.771,69.47,26.804,69.495,26.954,69.537,27.123,69.568,27.175,69.622,27.228,69.661,27.265,69.725,27.313,69.896,27.474,70.05,27.695,70.145,27.849,70.194,27.895,70.244,27.934,70.318,27.982,70.404,28.025,70.489,28.023,70.569,27.984,70.629,27.937,70.649,27.835,70.692,27.769,70.737,27.729,70.798,27.71,70.875,27.714,71.185,27.832,71.29,27.855,71.543,27.87,71.717,27.915,71.87,27.963,71.889,28.047,71.948,28.177,72.129,28.346,72.179,28.422,72.234,28.566,72.292,28.697,72.342,28.752,72.626,28.896,72.903,29.029,72.949,29.089,73.128,29.364,73.231,29.551,73.258,29.611,73.317,29.773,73.382,29.934,73.467,29.972,73.658,30.033,73.809,30.093,73.887,30.162,73.933,30.222,73.925,30.282,73.883,30.352,73.892,30.394,73.899,30.435,74.009,30.52,74.216,30.769,74.339,30.894,74.38,30.893,74.51,30.96,74.633,31.035,74.626,31.069,74.61,31.113,74.54,31.133,74.518,31.186,74.535,31.261,74.594,31.465,74.582,31.524,74.51,31.713,74.526,31.765,74.556,31.819,74.636,31.89,74.739,31.949,75.071,32.089,75.139,32.105,75.254,32.14,75.325,32.215,75.333,32.279,75.303,32.319,75.234,32.372,75.104,32.42,74.987,32.462,74.789,32.458,74.686,32.494,74.658,32.519,74.643,32.608,74.663,32.758,74.632,32.771,74.588,32.753,74.483,32.771,74.355,32.769,74.305,32.81,74.33,32.861,74.323,32.928,74.304,32.992,74.284,33.005,74.222,33.02,74.126,33.075,74.049,33.143,74.004,33.189,73.99,33.221,73.994,33.242,74.05,33.301,74.118,33.384,74.143,33.455,74.15,33.507,74.131,33.545,74.07,33.592,74.004,33.632,73.978,33.668,73.976,33.721,74.001,33.788,74.078,33.839,74.216,33.887,74.251,33.946,74.246,33.99,74.209,34.003,74.113,34.004,73.95,34.019,73.922,34.043,73.904,34.076,73.904,34.108,73.938,34.145,73.979,34.191,73.972,34.237,73.925,34.288,73.81,34.325,73.795,34.378,73.812,34.422,73.85,34.485,73.883,34.529,73.961,34.653,74.056,34.681,74.172,34.721,74.3,34.765,74.498,34.732,74.594,34.716,74.789,34.678,74.952,34.646,75.118,34.637,75.188,34.639,75.264,34.601,75.453,34.537,75.606,34.503,75.709,34.503,75.862,34.56,75.938,34.613,76.041,34.67,76.172,34.668,76.457,34.756,76.51,34.741,76.594,34.736,76.696,34.787,76.749,34.848,76.758,34.878,76.783,34.9,76.892,34.939,77.001,34.992,77.031,35.062,77.049,35.11,77.169,35.172,77.293,35.236,77.423,35.303,77.572,35.379,77.697,35.443,77.799,35.496,77.803,35.493,77.811,35.485,77.852,35.461,77.895,35.449,77.946,35.472,78.009,35.49,78.043,35.48,78.047,35.449,78.009,35.307,78.012,35.251,78.076,35.135,78.158,34.946,78.236,34.77,78.282,34.654,78.327,34.606,78.516,34.558,78.671,34.518,78.763,34.453,78.865,34.39,78.936,34.352,78.97,34.303,78.977,34.258,78.971,34.228,78.932,34.189,78.753,34.088,78.732,34.056,78.727,34.013,78.762,33.888,78.784,33.809,78.79,33.65,78.802,33.5,78.865,33.431,78.917,33.387,78.948,33.347,79.013,33.291,79.067,33.25,79.113,33.226,79.135,33.172,79.122,33.108,79.103,33.053,79.109,33.023,79.146,33.001,79.202,32.946,79.21,32.865,79.206,32.809,79.228,32.759,79.234,32.703,79.217,32.564,79.219,32.508,79.219,32.501,79.17,32.497,79.127,32.476,79.067,32.388,78.998,32.365,78.919,32.358,78.838,32.412,78.771,32.468,78.754,32.499,78.737,32.558,78.701,32.597,78.632,32.579,78.526,32.571,78.413,32.558,78.392,32.545,78.39,32.52,78.417,32.467,78.441,32.397,78.455,32.3,78.486,32.236,78.496,32.216,78.678,32.023,78.726,31.984,78.735,31.958,78.72,31.888,78.687,31.806,78.693,31.74,78.754,31.668,78.803,31.618,78.755,31.55,78.727,31.472,78.759,31.437,78.744,31.324,78.758,31.302,78.792,31.294,78.845,31.302,78.9,31.331,78.946,31.337,78.974,31.329,79.011,31.414,79.044,31.426,79.107,31.403,79.233,31.242,79.339,31.106,79.37,31.08,79.388,31.064,79.493,30.994,79.565,30.949,79.664,30.965,79.795,30.968,79.872,30.925,79.917,30.894,79.919,30.89,79.925,30.889,80.081,30.782,80.149,30.79,80.194,30.759,80.207,30.684,80.186,30.605,80.191,30.568,80.261,30.561,80.41,30.509,80.541,30.464,80.609,30.449,80.682,30.415,80.747,30.36,80.874,30.291,80.985,30.237,81.01,30.165,80.966,30.18,80.908,30.172,80.848,30.14,80.82,30.119,80.684,29.994,80.613,29.956,80.549,29.9,80.402,29.73,80.317,29.572,80.255,29.423,80.256,29.318,80.233,29.195,80.17,29.124,80.13,29.1,80.085,28.994,80.052,28.87,80.071,28.83,80.15,28.776,80.227,28.723,80.325,28.666,80.419,28.612,80.479,28.605,80.496,28.636,80.518,28.665,80.587,28.65,80.671,28.596,80.726,28.554,80.751,28.54,80.896,28.469,81.017,28.41,81.169,28.335,81.206,28.289,81.239,28.241,81.311,28.176,81.486,28.062,81.636,27.98,81.757,27.914,81.853,27.867,81.897,27.874,81.945,27.899,81.988,27.914,82.037,27.901,82.112,27.865,82.288,27.757,82.451,27.672,82.63,27.687,82.677,27.673,82.711,27.597,82.733,27.519,82.933,27.468,83.064,27.445,83.214,27.402,83.29,27.371,83.369,27.41,83.384,27.445,83.447,27.465,83.552,27.456,83.747,27.396,83.829,27.378,83.897,27.435,84.025,27.462,84.091,27.491,84.23,27.428,84.481,27.348,84.61,27.299,84.641,27.25,84.655,27.204,84.654,27.092,84.685,27.041,84.937,26.927,85.02,26.879,85.087,26.863,85.125,26.861,85.152,26.847,85.174,26.782,85.192,26.767,85.24,26.75,85.293,26.741,85.456,26.797,85.568,26.84,85.648,26.829,85.7,26.782,85.707,26.713,85.737,26.64,85.795,26.604,85.856,26.6,86.007,26.649,86.129,26.612,86.242,26.598,86.366,26.574,86.414,26.556,86.544,26.496,86.701,26.435,86.763,26.442,87.016,26.555,87.038,26.542,87.09,26.433,87.167,26.394,87.287,26.36,87.414,26.423,87.513,26.405,87.633,26.399,87.749,26.429,87.849,26.437,87.995,26.382,88.027,26.395,88.055,26.43,88.112,26.586,88.162,26.725,88.157,26.807,88.111,26.928,87.993,27.086,87.984,27.134,88.024,27.409,88.068,27.567,88.106,27.642,88.147,27.749,88.154,27.799,88.15,27.843,88.11,27.871,88.099,27.905,88.109,27.933,88.141,27.949,88.275,27.969,88.426,28.012,88.486,28.034,88.532,28.057,88.578,28.093,88.621,28.092,88.756,28.04,88.804,28.007,88.829,27.907,88.849,27.869,88.83,27.767,88.749,27.522,88.765,27.43,88.833,27.363,88.891,27.316,88.882,27.297,88.76,27.218,88.739,27.176,88.766,27.134,88.814,27.099,88.835,27.066,88.858,26.961,88.919,26.932,89.041,26.865,89.148,26.816,89.332,26.849,89.384,26.827,89.475,26.803,89.545,26.796,89.586,26.779,89.609,26.762,89.606,26.741,89.61,26.719,89.711,26.714,89.764,26.702,89.943,26.724,90.123,26.755,90.206,26.848,90.242,26.854,90.346,26.89,90.448,26.851,90.56,26.797,90.62,26.78,90.74,26.772,90.856,26.778,91.134,26.803,91.287,26.79,91.427,26.867,91.456,26.867,91.518,26.807,91.672,26.802,91.754,26.831,91.842,26.853,91.899,26.86,91.944,26.861,91.998,26.855,92.05,26.875,92.073,26.915,92.068,26.975,92.031,27.041,91.999,27.079,91.992,27.1,92.003,27.147,92.031,27.214,92.083,27.291,92.045,27.365,91.991,27.45,91.951,27.458,91.851,27.439,91.743,27.443,91.658,27.494,91.595,27.558,91.579,27.611,91.598,27.677,91.626,27.737,91.632,27.76,91.713,27.76,91.825,27.746,91.909,27.73,91.978,27.73,92.101,27.808,92.158,27.812,92.222,27.827,92.25,27.842,92.27,27.83,92.341,27.821,92.415,27.825,92.481,27.846,92.547,27.879,92.664,27.949,92.688,27.989,92.688,28.026,92.666,28.05,92.643,28.062,92.653,28.093,92.702,28.147,92.882,28.228,93.035,28.328,93.119,28.402,93.158,28.493,93.207,28.591,93.252,28.629,93.361,28.654,93.665,28.69,93.761,28.73,93.902,28.803,93.974,28.861,94.013,28.908,94.018,28.96,94.112,28.976,94.193,29.06,94.293,29.145,94.468,29.216,94.623,29.312,94.677,29.297,94.733,29.252,94.763,29.201,94.769,29.176,94.967,29.144,94.999,29.149,95.145,29.104,95.279,29.05,95.353,29.036,95.389,29.037,95.42,29.054,95.457,29.102,95.494,29.137,95.517,29.151,95.516,29.206,95.71,29.314,95.885,29.391,96.035,29.447,96.08,29.424,96.129,29.381,96.195,29.272,96.235,29.246,96.337,29.261,96.356,29.249,96.34,29.21,96.271,29.161,96.181,29.118,96.122,29.082,96.141,28.963,96.137,28.923,96.162,28.91,96.347,29.027,96.436,29.051,96.467,29.022,96.477,28.959,96.55,28.83,96.581,28.764,96.396,28.607,96.327,28.525,96.33,28.497,96.326,28.469,96.279,28.428,96.281,28.412,96.32,28.387,96.366,28.367,96.389,28.368,96.428,28.406,96.603,28.46,96.653,28.45,96.776,28.367,96.833,28.362,96.981,28.338,97.075,28.369,97.145,28.34,97.289,28.237,97.322,28.218,97.31,28.155,97.303,28.086,97.339,28.031,97.344,27.982,97.335,27.938,97.306,27.907,97.226,27.89,97.158,27.837,97.05,27.76,96.963,27.698,96.9,27.644,96.877,27.587,96.884,27.515,96.902,27.44,97.104,27.163,97.102,27.115,97.038,27.102,96.953,27.133,96.88,27.178,96.798,27.296,96.732,27.331,96.666,27.339,96.274,27.278,96.191,27.261,96.061,27.217,95.971,27.128,95.905,27.047,95.837,27.014,95.738,26.95,95.464,26.756,95.305,26.672,95.201,26.641,95.129,26.597,95.089,26.525,95.06,26.474,95.051,26.347,95.069,26.191,95.108,26.091,95.129,26.07,95.132,26.041,95.093,25.987,95.041,25.941,95.015,25.913,94.992,25.77,94.946,25.7,94.861,25.597,94.786,25.519,94.668,25.459,94.623,25.41,94.58,25.32,94.554,25.243,94.553,25.216,94.567,25.192,94.616,25.165,94.675,25.139,94.704,25.098,94.708,25.049,94.663,24.931,94.584,24.767,94.493,24.638,94.399,24.514,94.377,24.474,94.293,24.322,94.22,24.113,94.17,23.973,94.128,23.876,94.075,23.872,94.011,23.903,93.855,23.944,93.756,23.977,93.683,24.007,93.633,24.005,93.564,23.986,93.494,23.973,93.452,23.987,93.356,24.074,93.326,24.064,93.307,24.022,93.373,23.774,93.415,23.682,93.408,23.528,93.391,23.339,93.366,23.133,93.349,23.085,93.308,23.03,93.254,23.015,93.204,23.037,93.164,23.032,93.151,22.997,93.163,22.908,93.114,22.806,93.079,22.718,93.088,22.633,93.105,22.547,93.162,22.36,93.162,22.292,93.151,22.231,93.121,22.205,93.071,22.209,93.043,22.184,93.022,22.146,92.965,22.004,92.909,21.989,92.854,22.01,92.771,22.105,92.721,22.132,92.689,22.131,92.675,22.106,92.653,22.049,92.63,22.011,92.575,21.978,92.561,22.048,92.532,22.41,92.51,22.526,92.491,22.685,92.464,22.734,92.43,22.822,92.393,22.897,92.362,22.929,92.341,23.07,92.334,23.242,92.334,23.324,92.289,23.492,92.246,23.684,92.187,23.676,92.152,23.722,92.127,23.721,92.044,23.678,91.979,23.692,91.93,23.686,91.929,23.598,91.938,23.505,91.919,23.471,91.79,23.361,91.754,23.287,91.758,23.21,91.774,23.106,91.751,23.054,91.695,23.005,91.62,22.98,91.554,22.992,91.511,23.034,91.471,23.141,91.436,23.2,91.399,23.214,91.371,23.198,91.367,23.13,91.369,23.075,91.359,23.068,91.339,23.077,91.315,23.104,91.254,23.374,91.166,23.581,91.16,23.661,91.192,23.763,91.232,23.92,91.336,24.019,91.35,24.06,91.367,24.094,91.393,24.1,91.526,24.091,91.571,24.107,91.611,24.153,91.669,24.19,91.727,24.205,91.772,24.211,91.846,24.175,91.877,24.195,91.899,24.261,91.931,24.326,91.952,24.357,92.001,24.371,92.064,24.374,92.085,24.386,92.102,24.408,92.117,24.494,92.198,24.686,92.227,24.771,92.231,24.786,92.228,24.881,92.251,24.895,92.385,24.849,92.443,24.849,92.475,24.869,92.485,24.903,92.468,24.944,92.373,25.015,92.205,25.111,92.05,25.169,91.763,25.161,91.48,25.142,91.397,25.152,91.293,25.178,91.038,25.174,90.73,25.159,90.613,25.168,90.555,25.167,90.439,25.158,90.25,25.185,90.12,25.22,90.004,25.258,89.866,25.293,89.833,25.293,89.814,25.305,89.801,25.336,89.796,25.376,89.825,25.56,89.8,25.84,89.823,25.941,89.71,26.171,89.671,26.214,89.619,26.216,89.586,26.186,89.573,26.132,89.591,26.072,89.55,26.005,89.467,25.984,89.37,26.006,89.289,26.038,89.186,26.106,89.108,26.202,89.102,26.308,89.067,26.377,89.019,26.41,88.983,26.42,88.952,26.412,88.924,26.375,88.948,26.338,88.982,26.286,88.97,26.251,88.941,26.245,88.896,26.26,88.828,26.252,88.762,26.279,88.722,26.282,88.683,26.292,88.681,26.353,88.62,26.431,88.518,26.518,88.418,26.572,88.37,26.564,88.346,26.505,88.351,26.483,88.386,26.472,88.437,26.437,88.448,26.401,88.44,26.369,88.378,26.312,88.334,26.258,88.235,26.178,88.151,26.087,88.129,26.018,88.097,25.956,88.085,25.888,88.107,25.841,88.147,25.811,88.253,25.79,88.363,25.698,88.452,25.574,88.502,25.537,88.593,25.495,88.769,25.49,88.795,25.456,88.82,25.366,88.855,25.334,88.944,25.291,88.952,25.259,88.93,25.223,88.89,25.194,88.817,25.176,88.748,25.169,88.678,25.18,88.574,25.188,88.456,25.188,88.373,24.962,88.313,24.882,88.279,24.882,88.189,24.921,88.15,24.915,88.045,24.713,88.03,24.664,88.023,24.628,88.079,24.55,88.146,24.486,88.225,24.461,88.287,24.48,88.338,24.454,88.397,24.389,88.499,24.347,88.642,24.326,88.724,24.275,88.734,24.231,88.727,24.186,88.714,24.07,88.7,24.003,88.623,23.826,88.567,23.674,88.596,23.602,88.616,23.573,88.636,23.55,88.698,23.493,88.741,23.437,88.704,23.293,88.724,23.255,88.808,23.23,88.897,23.21,88.928,23.187,88.851,23.041,88.867,22.939,88.9,22.844,88.923,22.688,88.927,22.671,88.921,22.632,88.971,22.511,89.05,22.275,89.056,22.186,89.051,22.093,89.028,21.937,88.949,21.938,89.02,21.834,89.042,21.759,89.052,21.654,88.967,21.641,88.907,21.653,88.858,21.745,88.834,21.661,88.745,21.584,88.713,21.622,88.695,21.662,88.691,21.733,88.74,22.005,88.73,22.036,88.708,22.056,88.66,22.067,88.642,22.122,88.567,21.832,88.6,21.714,88.585,21.66,88.446,21.614,88.305,21.723,88.288,21.758,88.279,21.697,88.254,21.622,88.122,21.636,88.057,21.694,88.099,21.794,88.181,22.033,88.196,22.14,88.087,22.218,87.994,22.266,87.941,22.374,87.962,22.255,88.011,22.213,88.083,22.183,88.159,22.122,88.104,22.047,88.051,22.001,87.948,21.825,87.824,21.727,87.678,21.654,87.201,21.545,87.101,21.501,86.954,21.365,86.86,21.237,86.842,21.106,86.896,20.966,86.939,20.745,86.975,20.7,86.925,20.62,86.836,20.534,86.763,20.419,86.769,20.356,86.75,20.313,86.499,20.172,86.446,20.089,86.377,20.007,86.294,20.054,86.245,20.053,86.312,19.988,86.303,19.945,86.279,19.919,86.216,19.896,85.853,19.792,85.575,19.693,85.497,19.697,85.511,19.727,85.56,19.753,85.555,19.867,85.504,19.888,85.46,19.896,85.249,19.758,85.163,19.621,85.181,19.595,85.229,19.601,85.371,19.679,85.437,19.657,85.442,19.627,85.226,19.508,84.771,19.125,84.75,19.05,84.691,18.965,84.609,18.884,84.463,18.69,84.182,18.401,84.104,18.293,83.654,18.07,83.572,18.004,83.388,17.787,83.198,17.609,82.977,17.462,82.593,17.274,82.36,17.096,82.287,16.978,82.282,16.936,82.307,16.879,82.35,16.825,82.36,16.783,82.339,16.707,82.327,16.664,82.259,16.56,82.142,16.485,81.762,16.329,81.712,16.334,81.402,16.365,81.286,16.337,81.239,16.264,81.132,15.962,81.03,15.881,80.993,15.809,80.979,15.758,80.918,15.76,80.865,15.782,80.826,15.766,80.782,15.867,80.708,15.888,80.647,15.895,80.385,15.793,80.293,15.711,80.101,15.324,80.053,15.074,80.099,14.798,80.165,14.578,80.179,14.478,80.17,14.349,80.136,14.287,80.112,14.212,80.144,14.059,80.224,13.858,80.244,13.773,80.246,13.686,80.307,13.485,80.266,13.521,80.233,13.606,80.156,13.714,80.062,13.606,80.114,13.529,80.29,13.437,80.342,13.361,80.229,12.69,80.143,12.452,80.038,12.296,79.982,12.235,79.858,11.989,79.771,11.69,79.754,11.575,79.793,11.447,79.749,11.371,79.693,11.313,79.799,11.339,79.835,11.269,79.849,11.197,79.85,10.769,79.838,10.323,79.757,10.304,79.667,10.3,79.589,10.312,79.532,10.33,79.391,10.306,79.315,10.257,79.254,10.175,79.258,10.035,78.996,9.683,78.94,9.566,78.919,9.453,78.953,9.394,79.02,9.333,79.107,9.309,79.275,9.285,79.356,9.252,79.411,9.192,79.213,9.256,78.98,9.269,78.421,9.105,78.275,8.99,78.192,8.891,78.136,8.663,78.126,8.511,78.06,8.385,77.77,8.19,77.587,8.13,77.518,8.078,77.301,8.145,77.066,8.316,76.967,8.407,76.617,8.847,76.553,8.903,76.483,9.091,76.472,9.161,76.452,9.189,76.419,9.208,76.403,9.237,76.325,9.452,76.292,9.676,76.242,9.927,76.285,9.91,76.343,9.827,76.372,9.707,76.376,9.54,76.42,9.52,76.459,9.536,76.346,9.922,76.249,10.018,76.223,10.024,76.196,10.086,76.193,10.164,76.201,10.201,76.123,10.327,76.096,10.402,75.923,10.784,75.845,11.058,75.724,11.362,75.646,11.468,75.525,11.703,75.423,11.812,75.315,11.958,75.23,12.023,75.197,12.058,74.946,12.565,74.868,12.845,74.803,12.977,74.771,13.077,74.682,13.507,74.682,13.584,74.671,13.668,74.608,13.85,74.499,14.046,74.467,14.169,74.467,14.217,74.397,14.407,74.382,14.495,74.335,14.575,74.28,14.65,74.223,14.709,74.089,14.902,74.041,14.949,73.949,15.075,73.884,15.306,73.801,15.397,73.932,15.397,73.852,15.482,73.814,15.539,73.772,15.573,73.833,15.659,73.733,15.657,73.68,15.709,73.608,15.871,73.476,16.054,73.454,16.152,73.338,16.46,73.239,17.199,73.149,17.527,73.156,17.622,73.047,17.907,72.994,18.098,72.972,18.259,72.943,18.366,72.917,18.576,72.875,18.643,72.871,18.683,72.899,18.779,72.977,18.927,73.006,19.021,72.972,19.153,72.901,19.015,72.835,18.976,72.803,19.079,72.803,19.219,72.795,19.252,72.812,19.299,72.987,19.277,72.788,19.363,72.764,19.413,72.756,19.451,72.799,19.52,72.727,19.578,72.697,19.757,72.676,19.798,72.668,19.831,72.709,20.078,72.881,20.563,72.894,20.673,72.879,20.829,72.841,20.952,72.824,21.084,72.814,21.117,72.752,21.129,72.692,21.178,72.624,21.372,72.687,21.436,72.735,21.471,72.668,21.456,72.613,21.462,72.718,21.551,72.811,21.62,73.022,21.7,73.113,21.75,72.979,21.705,72.84,21.687,72.543,21.697,72.592,21.878,72.644,21.938,72.7,21.972,72.617,21.962,72.522,21.976,72.553,22.16,72.628,22.2,72.709,22.207,72.809,22.233,72.702,22.264,72.59,22.278,72.456,22.248,72.333,22.27,72.183,22.27,72.243,22.245,72.306,22.189,72.274,22.09,72.244,22.028,72.162,21.985,72.094,21.92,72.076,21.863,72.037,21.823,72.103,21.795,72.171,21.774,72.21,21.728,72.257,21.661,72.254,21.531,72.077,21.224,72.015,21.156,71.571,20.971,71.396,20.87,71.025,20.739,70.88,20.715,70.719,20.74,70.485,20.84,70.127,21.095,70.034,21.179,69.748,21.506,69.542,21.679,69.385,21.84,69.192,21.992,69.009,22.197,68.97,22.29,68.983,22.385,69.052,22.437,69.131,22.416,69.194,22.336,69.239,22.3,69.277,22.285,69.549,22.408,69.655,22.404,69.728,22.465,69.819,22.452,70.006,22.548,70.084,22.554,70.177,22.573,70.328,22.816,70.44,22.97,70.513,23.002,70.509,23.04,70.489,23.09,70.435,23.077,70.396,23.03,70.368,22.973,70.339,22.94,70.251,22.971,70.192,22.966,70.118,22.947,69.85,22.856,69.74,22.775,69.665,22.759,69.236,22.849,68.817,23.054,68.641,23.19,68.529,23.364,68.417,23.571,68.454,23.629,68.627,23.754,68.777,23.852,68.642,23.808,68.497,23.748,68.425,23.706,68.343,23.617,68.235,23.597,68.192,23.729,68.165,23.857]],[[93.89,6.831,93.829,6.749,93.709,7.001,93.658,7.016,93.656,7.136,93.684,7.184,93.822,7.237,93.859,7.207,93.93,6.973,93.89,6.831]],[[93.734,7.356,93.638,7.262,93.597,7.319,93.614,7.358,93.655,7.38,93.692,7.411,93.734,7.356]],[[93.141,8.25,93.171,8.212,93.115,8.219,93.064,8.275,93.078,8.328,93.097,8.349,93.141,8.25]],[[93.443,7.878,93.365,7.877,93.342,7.919,93.309,7.964,93.334,8.007,93.375,8.018,93.434,7.948,93.447,7.899,93.443,7.878]],[[93.537,8.057,93.49,8.019,93.478,8.024,93.472,8.053,93.47,8.073,93.461,8.109,93.456,8.172,93.494,8.225,93.532,8.214,93.512,8.16,93.537,8.057]],[[92.788,9.137,92.744,9.131,92.717,9.165,92.713,9.205,92.739,9.231,92.762,9.244,92.786,9.241,92.809,9.173,92.788,9.137]],[[92.503,10.555,92.473,10.521,92.37,10.547,92.377,10.651,92.353,10.751,92.371,10.794,92.448,10.866,92.51,10.897,92.554,10.8,92.574,10.704,92.503,10.555]],[[92.693,11.381,92.645,11.361,92.596,11.386,92.634,11.427,92.64,11.509,92.69,11.463,92.687,11.411,92.693,11.381]],[[92.723,11.536,92.701,11.513,92.668,11.539,92.576,11.718,92.56,11.833,92.534,11.873,92.567,11.931,92.608,11.95,92.632,12.014,92.641,12.112,92.676,12.192,92.695,12.215,92.769,12.216,92.788,12.226,92.778,12.303,92.734,12.336,92.719,12.357,92.721,12.541,92.732,12.616,92.759,12.669,92.74,12.78,92.753,12.821,92.807,12.879,92.831,13.003,92.809,13.04,92.86,13.231,92.857,13.358,92.925,13.486,93.029,13.544,93.062,13.545,93.067,13.436,93.077,13.401,93.016,13.336,93.074,13.252,93.066,13.222,93.043,13.155,93.005,13.089,92.951,13.063,92.91,12.975,92.886,12.942,92.965,12.85,92.99,12.539,92.933,12.453,92.864,12.436,92.879,12.228,92.867,12.181,92.799,12.079,92.786,12.035,92.748,11.993,92.764,11.94,92.797,11.918,92.798,11.875,92.767,11.765,92.765,11.639,92.723,11.536]],[[93.017,12.037,93.062,11.899,92.982,11.959,92.955,12.002,92.996,12.032,93.017,12.037]],[[92.718,12.865,92.686,12.8,92.68,12.939,92.694,12.957,92.711,12.962,92.731,12.949,92.718,12.865]],[[72.78,11.202,72.773,11.196,72.772,11.214,72.782,11.243,72.793,11.263,72.796,11.26,72.793,11.242,72.788,11.216,72.78,11.202]],[[73.067,8.269,73.053,8.257,73.039,8.252,73.029,8.254,73.023,8.266,73.026,8.275,73.039,8.265,73.056,8.275,73.075,8.306,73.079,8.317,73.084,8.311,73.08,8.293,73.067,8.269]]]},{"n":"Iceland","c":"IS","r":3,"ml":2,"lx":-18.67,"ly":64.78,"b":[-24.476,63.407,-13.556,66.526],"p":[[[-15.543,66.229,-15.428,66.225,-15.241,66.259,-15.162,66.282,-14.97,66.36,-14.856,66.381,-14.681,66.376,-14.596,66.382,-14.594,66.374,-14.702,66.342,-14.788,66.331,-14.912,66.284,-15.03,66.178,-15.117,66.126,-15.116,66.102,-15.01,66.061,-14.894,66.038,-14.787,66.059,-14.74,66.051,-14.698,66.02,-14.674,65.99,-14.669,65.96,-14.688,65.897,-14.753,65.834,-14.839,65.781,-14.827,65.764,-14.758,65.756,-14.426,65.79,-14.392,65.787,-14.373,65.77,-14.369,65.739,-14.351,65.71,-14.318,65.684,-14.328,65.658,-14.473,65.575,-14.302,65.628,-14.167,65.642,-13.935,65.616,-13.841,65.586,-13.785,65.533,-13.705,65.551,-13.67,65.55,-13.618,65.519,-13.616,65.487,-13.654,65.441,-13.668,65.399,-13.708,65.382,-13.783,65.369,-13.805,65.355,-13.772,65.323,-13.723,65.291,-13.653,65.29,-13.641,65.275,-13.64,65.257,-13.649,65.237,-13.672,65.223,-13.707,65.215,-13.755,65.193,-13.581,65.143,-13.559,65.125,-13.556,65.098,-13.57,65.068,-13.599,65.036,-13.652,65.017,-13.777,65.014,-13.854,64.993,-13.828,64.958,-13.83,64.914,-13.853,64.862,-13.952,64.784,-14.044,64.742,-14.135,64.715,-14.297,64.724,-14.385,64.745,-14.375,64.677,-14.465,64.636,-14.448,64.601,-14.417,64.583,-14.433,64.538,-14.475,64.494,-14.547,64.446,-14.628,64.416,-14.79,64.38,-14.927,64.32,-15.022,64.296,-15.256,64.297,-15.495,64.258,-15.833,64.177,-16.06,64.111,-16.236,64.037,-16.468,63.916,-16.64,63.865,-16.74,63.852,-16.933,63.841,-17.095,63.808,-17.633,63.747,-17.816,63.713,-17.839,63.682,-17.915,63.636,-17.92,63.62,-17.886,63.607,-17.88,63.59,-17.947,63.536,-18.08,63.496,-18.143,63.497,-18.219,63.531,-18.252,63.53,-18.265,63.525,-18.266,63.514,-18.222,63.473,-18.303,63.454,-18.654,63.407,-19.25,63.442,-19.487,63.479,-19.778,63.537,-19.952,63.552,-20.198,63.556,-20.4,63.637,-20.494,63.687,-20.502,63.708,-20.491,63.732,-20.47,63.748,-20.438,63.757,-20.372,63.758,-20.363,63.765,-20.414,63.805,-20.463,63.792,-20.593,63.735,-20.651,63.737,-20.727,63.766,-20.73,63.793,-20.879,63.804,-21.008,63.838,-21.137,63.888,-21.156,63.907,-21.094,63.934,-21.106,63.94,-21.152,63.945,-21.246,63.935,-21.388,63.873,-21.449,63.858,-22.373,63.844,-22.607,63.837,-22.652,63.828,-22.693,63.869,-22.729,63.959,-22.743,64.019,-22.734,64.048,-22.701,64.083,-22.651,64.077,-22.603,64.05,-22.56,64.01,-22.51,63.991,-22.188,64.039,-22.057,64.071,-22.001,64.102,-21.935,64.154,-21.866,64.18,-21.833,64.205,-21.768,64.285,-21.723,64.322,-21.669,64.349,-21.606,64.367,-21.463,64.379,-21.557,64.398,-21.647,64.398,-21.951,64.314,-22.053,64.314,-22.049,64.327,-22.006,64.351,-21.901,64.392,-21.973,64.395,-22.001,64.413,-22.004,64.452,-21.95,64.515,-21.702,64.598,-21.617,64.61,-21.591,64.626,-21.623,64.64,-21.675,64.648,-21.924,64.563,-22.106,64.533,-22.16,64.539,-22.254,64.572,-22.284,64.587,-22.325,64.624,-22.32,64.647,-22.234,64.714,-22.248,64.727,-22.307,64.733,-22.467,64.795,-22.72,64.789,-23.347,64.824,-23.476,64.809,-23.69,64.757,-23.819,64.739,-23.879,64.751,-23.933,64.779,-23.982,64.816,-24.026,64.863,-24.007,64.896,-23.924,64.915,-23.864,64.924,-23.693,64.913,-23.485,64.946,-23.353,64.953,-23.315,64.958,-23.237,64.993,-23.198,65.002,-23.138,64.99,-23.109,64.966,-22.9,65.003,-22.828,65.022,-22.82,65.033,-22.788,65.046,-22.684,65.026,-22.6,65.026,-22.494,65.04,-22.308,65.046,-21.892,65.049,-21.83,65.079,-21.8,65.106,-21.764,65.174,-21.78,65.188,-22.04,65.125,-22.099,65.126,-22.4,65.159,-22.509,65.197,-22.473,65.227,-22.314,65.292,-22.149,65.344,-21.907,65.4,-21.85,65.422,-21.844,65.447,-22.006,65.493,-22.311,65.481,-22.39,65.535,-22.644,65.568,-22.813,65.547,-22.902,65.58,-23.122,65.535,-23.605,65.469,-23.796,65.423,-23.9,65.408,-24.019,65.445,-24.224,65.487,-24.455,65.5,-24.476,65.525,-24.341,65.601,-24.249,65.615,-24.156,65.608,-23.979,65.555,-23.857,65.538,-24.01,65.616,-24.006,65.646,-24.018,65.691,-24.065,65.71,-24.112,65.76,-24.093,65.776,-24.032,65.782,-23.909,65.766,-23.616,65.68,-23.472,65.695,-23.393,65.727,-23.285,65.75,-23.316,65.762,-23.569,65.764,-23.705,65.781,-23.773,65.806,-23.833,65.849,-23.812,65.869,-23.741,65.885,-23.525,65.88,-23.667,65.954,-23.767,65.997,-23.777,66.018,-23.771,66.043,-23.757,66.061,-23.737,66.069,-23.489,66.026,-23.434,66.024,-23.485,66.052,-23.594,66.093,-23.599,66.109,-23.553,66.122,-23.53,66.145,-23.528,66.164,-23.453,66.181,-23.377,66.182,-23.3,66.167,-23.063,66.086,-23.029,66.064,-23.017,66.034,-23.029,65.997,-23.019,65.982,-22.926,65.995,-22.852,65.979,-22.815,65.983,-22.723,66.039,-22.66,66.026,-22.622,66,-22.61,65.976,-22.604,65.944,-22.62,65.877,-22.616,65.867,-22.552,65.905,-22.442,65.908,-22.428,65.927,-22.424,65.998,-22.433,66.058,-22.445,66.07,-22.806,66.153,-22.869,66.172,-22.948,66.213,-22.932,66.233,-22.862,66.251,-22.756,66.259,-22.509,66.258,-22.484,66.266,-22.532,66.288,-22.646,66.302,-22.673,66.314,-22.686,66.338,-22.821,66.325,-22.972,66.324,-23.117,66.339,-23.12,66.357,-23.063,66.384,-22.944,66.429,-22.889,66.441,-22.724,66.433,-22.559,66.445,-22.426,66.43,-22.32,66.385,-22.17,66.307,-21.967,66.257,-21.948,66.241,-21.84,66.2,-21.625,66.09,-21.407,66.026,-21.397,66.009,-21.433,65.99,-21.517,65.968,-21.497,65.955,-21.388,65.939,-21.309,65.895,-21.303,65.876,-21.375,65.742,-21.413,65.713,-21.457,65.698,-21.658,65.724,-21.61,65.681,-21.466,65.635,-21.434,65.61,-21.455,65.585,-21.439,65.579,-21.387,65.592,-21.365,65.578,-21.374,65.536,-21.396,65.502,-21.432,65.474,-21.422,65.462,-21.365,65.466,-21.313,65.459,-21.23,65.421,-21.163,65.304,-21.13,65.267,-21.106,65.3,-21.076,65.385,-21.047,65.428,-21.021,65.43,-20.998,65.445,-20.979,65.471,-20.94,65.565,-20.804,65.636,-20.74,65.658,-20.679,65.663,-20.649,65.654,-20.548,65.579,-20.487,65.567,-20.455,65.571,-20.412,65.622,-20.357,65.719,-20.344,65.828,-20.374,65.948,-20.357,66.033,-20.292,66.084,-20.208,66.1,-20.103,66.08,-20.026,66.049,-19.875,65.93,-19.753,65.868,-19.648,65.801,-19.594,65.779,-19.49,65.768,-19.462,65.772,-19.443,65.788,-19.434,65.814,-19.456,65.985,-19.427,66.038,-19.383,66.076,-19.195,66.098,-19.093,66.122,-18.994,66.16,-18.911,66.181,-18.846,66.184,-18.778,66.169,-18.706,66.136,-18.595,66.071,-18.455,65.965,-18.277,65.885,-18.184,65.758,-18.164,65.737,-18.142,65.734,-18.118,65.751,-18.103,65.774,-18.099,65.83,-18.149,65.905,-18.315,66.093,-18.318,66.129,-18.297,66.157,-18.18,66.161,-17.907,66.143,-17.82,66.114,-17.634,65.999,-17.582,65.971,-17.55,65.964,-17.539,65.978,-17.467,66,-17.417,66.026,-17.334,66.089,-17.153,66.203,-17.115,66.206,-17.062,66.197,-16.97,66.167,-16.925,66.143,-16.838,66.125,-16.748,66.132,-16.625,66.172,-16.485,66.196,-16.437,66.253,-16.428,66.278,-16.541,66.447,-16.493,66.481,-16.249,66.523,-16.036,66.526,-15.985,66.515,-15.851,66.433,-15.76,66.392,-15.714,66.359,-15.703,66.286,-15.647,66.259,-15.543,66.229]]]},{"n":"Hungary","c":"HU","r":5,"ml":4,"lx":19.45,"ly":47.09,"b":[16.093,45.753,22.877,48.553],"p":[[[22.132,48.405,22.227,48.413,22.231,48.412,22.254,48.407,22.269,48.361,22.272,48.358,22.295,48.327,22.317,48.287,22.35,48.256,22.424,48.243,22.52,48.205,22.582,48.134,22.676,48.104,22.683,48.104,22.702,48.107,22.769,48.11,22.782,48.095,22.836,48.06,22.857,48.03,22.846,47.997,22.856,47.96,22.877,47.947,22.852,47.923,22.677,47.799,22.608,47.766,22.563,47.76,22.491,47.773,22.417,47.763,22.351,47.736,22.291,47.728,22.245,47.696,22.185,47.629,22.112,47.572,22.038,47.537,22,47.505,21.995,47.396,21.954,47.364,21.899,47.333,21.869,47.305,21.785,47.138,21.722,47.085,21.661,47.044,21.651,47.007,21.653,46.964,21.584,46.878,21.494,46.79,21.477,46.753,21.497,46.704,21.411,46.648,21.361,46.621,21.32,46.608,21.295,46.572,21.252,46.486,21.263,46.448,21.265,46.412,21.192,46.392,21.17,46.353,21.152,46.304,21.122,46.282,21.04,46.242,20.837,46.26,20.76,46.246,20.737,46.217,20.733,46.194,20.707,46.173,20.661,46.146,20.614,46.133,20.508,46.167,20.281,46.133,20.242,46.109,20.21,46.126,20.161,46.142,19.934,46.161,19.844,46.146,19.725,46.152,19.613,46.169,19.531,46.155,19.458,46.087,19.421,46.064,19.393,46.05,19.33,46.029,19.278,46.003,19.208,45.984,19.146,45.987,19.087,46.016,19.066,46.01,19.048,45.983,19.016,45.96,18.928,45.931,18.905,45.932,18.9,45.932,18.833,45.911,18.722,45.899,18.666,45.907,18.565,45.813,18.534,45.796,18.437,45.767,18.358,45.753,18.291,45.764,18.264,45.765,17.964,45.77,17.807,45.79,17.706,45.827,17.64,45.868,17.607,45.914,17.529,45.941,17.406,45.951,17.311,45.996,17.242,46.077,17.15,46.14,17.033,46.187,16.94,46.254,16.871,46.339,16.748,46.416,16.57,46.485,16.516,46.5,16.506,46.522,16.418,46.607,16.381,46.639,16.385,46.681,16.367,46.705,16.335,46.722,16.318,46.783,16.308,46.828,16.284,46.857,16.093,46.863,16.253,46.972,16.332,47.002,16.424,46.997,16.453,47.007,16.461,47.022,16.477,47.058,16.485,47.091,16.493,47.123,16.483,47.14,16.438,47.146,16.417,47.223,16.44,47.253,16.463,47.273,16.434,47.367,16.443,47.4,16.515,47.405,16.574,47.425,16.623,47.448,16.637,47.477,16.677,47.536,16.64,47.609,16.432,47.656,16.421,47.674,16.47,47.695,16.521,47.724,16.551,47.747,16.591,47.751,16.647,47.739,16.748,47.686,16.786,47.679,16.823,47.694,16.863,47.697,16.973,47.695,17.067,47.708,17.046,47.764,17.046,47.805,17.03,47.837,17.04,47.873,17.078,47.901,17.089,47.964,17.147,48.006,17.175,48.012,17.277,48.004,17.302,47.993,17.317,47.991,17.481,47.888,17.635,47.81,17.762,47.77,17.948,47.767,18.146,47.763,18.476,47.777,18.724,47.787,18.741,47.806,18.778,47.853,18.748,47.893,18.75,47.939,18.792,48,18.914,48.051,19.265,48.073,19.467,48.111,19.497,48.162,19.564,48.213,19.625,48.223,19.709,48.2,19.81,48.155,19.899,48.131,19.95,48.147,20.129,48.222,20.334,48.296,20.475,48.495,20.49,48.527,20.643,48.55,20.867,48.546,20.981,48.52,21.067,48.506,21.196,48.511,21.382,48.553,21.451,48.552,21.505,48.522,21.563,48.496,21.603,48.464,21.633,48.419,21.649,48.401,21.675,48.378,21.721,48.347,21.767,48.338,22.111,48.393,22.132,48.405]]]},{"n":"Honduras","c":"HN","r":5,"ml":4.5,"lx":-86.89,"ly":14.79,"b":[-89.363,12.979,-83.158,16.514],"p":[[[-83.158,14.993,-83.415,15.008,-83.537,14.977,-83.59,14.908,-83.635,14.876,-83.674,14.884,-83.751,14.856,-83.867,14.794,-83.972,14.771,-84.066,14.786,-84.093,14.771,-84.1,14.751,-84.114,14.731,-84.151,14.72,-84.192,14.726,-84.239,14.748,-84.264,14.739,-84.267,14.698,-84.292,14.687,-84.34,14.706,-84.394,14.692,-84.454,14.644,-84.538,14.633,-84.646,14.661,-84.73,14.713,-84.789,14.79,-84.86,14.81,-84.985,14.752,-85.037,14.686,-85.049,14.645,-85.037,14.608,-85.059,14.583,-85.117,14.571,-85.161,14.525,-85.192,14.447,-85.198,14.386,-85.179,14.343,-85.208,14.312,-85.284,14.292,-85.374,14.224,-85.477,14.109,-85.58,14.028,-85.682,13.983,-85.731,13.932,-85.728,13.876,-85.734,13.859,-85.753,13.852,-85.787,13.844,-85.984,13.966,-86.04,14.05,-86.089,14.037,-86.151,13.995,-86.238,13.899,-86.332,13.77,-86.377,13.756,-86.61,13.775,-86.734,13.763,-86.759,13.746,-86.771,13.699,-86.764,13.635,-86.73,13.407,-86.711,13.313,-86.729,13.284,-86.792,13.28,-86.874,13.267,-86.918,13.224,-86.929,13.179,-86.933,13.118,-86.959,13.054,-87.009,13.008,-87.059,12.991,-87.337,12.979,-87.333,13.085,-87.413,13.127,-87.458,13.215,-87.498,13.275,-87.485,13.311,-87.489,13.353,-87.602,13.386,-87.708,13.36,-87.769,13.377,-87.814,13.399,-87.737,13.451,-87.732,13.483,-87.756,13.506,-87.782,13.521,-87.774,13.58,-87.759,13.65,-87.715,13.813,-87.731,13.841,-87.802,13.89,-87.892,13.895,-87.991,13.88,-88.039,13.905,-88.08,13.961,-88.151,13.987,-88.276,13.943,-88.408,13.875,-88.449,13.851,-88.483,13.854,-88.498,13.905,-88.504,13.964,-88.513,13.979,-88.583,14,-88.666,14.016,-88.708,14.032,-88.747,14.072,-88.846,14.125,-88.868,14.164,-89,14.253,-89.027,14.297,-89.057,14.329,-89.121,14.37,-89.17,14.36,-89.337,14.411,-89.363,14.416,-89.339,14.461,-89.287,14.53,-89.172,14.607,-89.162,14.669,-89.192,14.789,-89.222,14.866,-89.206,14.901,-89.17,15.04,-89.143,15.072,-88.976,15.143,-88.961,15.152,-88.83,15.251,-88.684,15.36,-88.533,15.481,-88.365,15.616,-88.271,15.695,-88.228,15.729,-88.131,15.701,-88.055,15.765,-88.01,15.786,-87.907,15.863,-87.875,15.879,-87.702,15.911,-87.618,15.91,-87.545,15.832,-87.487,15.79,-87.377,15.826,-87.286,15.834,-86.907,15.762,-86.757,15.794,-86.481,15.801,-86.357,15.783,-86.181,15.885,-86.069,15.906,-85.936,15.953,-85.954,16.002,-85.986,16.024,-85.784,16.003,-85.484,15.9,-85.164,15.918,-85.048,15.974,-84.974,15.99,-84.646,15.884,-84.56,15.802,-84.492,15.794,-84.44,15.813,-84.426,15.829,-84.49,15.847,-84.52,15.873,-84.261,15.823,-83.775,15.437,-83.765,15.405,-83.973,15.52,-84.083,15.511,-84.111,15.492,-84.105,15.43,-84.095,15.401,-84.048,15.398,-84.013,15.414,-83.927,15.394,-83.871,15.353,-83.802,15.289,-83.76,15.22,-83.716,15.219,-83.672,15.261,-83.59,15.266,-83.536,15.219,-83.498,15.222,-83.551,15.294,-83.676,15.365,-83.646,15.368,-83.369,15.24,-83.291,15.079,-83.226,15.042,-83.158,14.993]],[[-86.42,16.378,-86.58,16.3,-86.63,16.302,-86.557,16.362,-86.438,16.414,-86.338,16.439,-86.256,16.428,-86.42,16.378]],[[-85.871,16.462,-85.947,16.404,-85.961,16.43,-85.924,16.483,-85.878,16.514,-85.834,16.511,-85.844,16.488,-85.871,16.462]]]},{"n":"Haiti","c":"HT","r":5,"ml":4,"lx":-72.22,"ly":19.26,"b":[-74.478,18.039,-71.645,20.094],"p":[[[-71.779,19.718,-71.757,19.688,-71.711,19.487,-71.707,19.422,-71.753,19.324,-71.746,19.286,-71.647,19.196,-71.645,19.164,-71.657,19.131,-71.742,19.046,-71.807,18.987,-71.786,18.92,-71.734,18.856,-71.727,18.803,-71.743,18.733,-71.824,18.646,-71.867,18.614,-71.987,18.61,-72,18.598,-71.94,18.513,-71.873,18.416,-71.762,18.341,-71.737,18.271,-71.764,18.204,-71.768,18.039,-71.853,18.119,-71.946,18.186,-72.002,18.212,-72.06,18.229,-72.504,18.22,-72.553,18.208,-72.592,18.187,-72.633,18.176,-72.755,18.156,-72.877,18.152,-73.16,18.206,-73.272,18.234,-73.385,18.251,-73.515,18.245,-73.644,18.229,-73.747,18.19,-73.825,18.122,-73.839,18.058,-73.885,18.042,-73.989,18.143,-74.085,18.215,-74.195,18.269,-74.419,18.346,-74.46,18.393,-74.478,18.45,-74.387,18.625,-74.284,18.657,-74.228,18.663,-74.1,18.641,-73.976,18.601,-73.862,18.575,-73.687,18.565,-73.592,18.522,-72.917,18.456,-72.789,18.435,-72.739,18.442,-72.696,18.468,-72.66,18.515,-72.618,18.551,-72.418,18.559,-72.376,18.574,-72.347,18.624,-72.348,18.675,-72.465,18.744,-72.649,18.894,-72.811,19.072,-72.741,19.131,-72.768,19.241,-72.742,19.342,-72.703,19.441,-72.863,19.526,-73.053,19.611,-73.316,19.637,-73.396,19.659,-73.438,19.722,-73.401,19.807,-73.315,19.855,-73.218,19.884,-73.118,19.904,-72.877,19.928,-72.637,19.901,-72.43,19.813,-72.22,19.745,-71.954,19.722,-71.835,19.697,-71.779,19.718]],[[-72.805,18.778,-72.822,18.707,-73.078,18.791,-73.285,18.897,-73.276,18.954,-73.171,18.967,-73.069,18.932,-72.919,18.861,-72.805,18.778]],[[-72.664,20.038,-72.623,20.014,-72.639,19.986,-72.74,20.003,-72.844,20.035,-72.878,20.027,-72.899,20.031,-72.96,20.062,-72.907,20.086,-72.851,20.094,-72.791,20.092,-72.664,20.038]]]},{"n":"Guyana","c":"GY","r":4,"ml":4,"lx":-58.94,"ly":5.12,"b":[-61.391,1.201,-56.483,8.549],"p":[[[-60.742,5.202,-60.954,5.437,-61.167,5.674,-61.377,5.907,-61.391,5.939,-61.303,6.05,-61.225,6.129,-61.159,6.174,-61.129,6.214,-61.152,6.385,-61.151,6.447,-61.182,6.513,-61.204,6.588,-61.177,6.651,-61.146,6.695,-61.105,6.711,-61.007,6.727,-60.938,6.733,-60.914,6.758,-60.873,6.787,-60.821,6.788,-60.718,6.768,-60.671,6.806,-60.586,6.857,-60.395,6.945,-60.352,7.003,-60.322,7.092,-60.325,7.134,-60.345,7.15,-60.392,7.165,-60.465,7.167,-60.523,7.144,-60.583,7.156,-60.633,7.211,-60.636,7.257,-60.607,7.321,-60.624,7.363,-60.719,7.499,-60.719,7.536,-60.649,7.597,-60.61,7.648,-60.556,7.772,-60.514,7.813,-60.381,7.828,-60.347,7.854,-60.279,7.919,-60.178,7.994,-60.032,8.054,-59.991,8.162,-59.965,8.192,-59.849,8.249,-59.829,8.279,-59.832,8.306,-60.018,8.549,-59.981,8.533,-59.837,8.374,-59.757,8.34,-59.74,8.339,-59.739,8.38,-59.666,8.363,-59.477,8.254,-59.2,8.075,-58.812,7.736,-58.701,7.607,-58.627,7.546,-58.511,7.398,-58.477,7.326,-58.481,7.038,-58.583,6.844,-58.608,6.697,-58.613,6.503,-58.673,6.391,-58.594,6.452,-58.569,6.627,-58.502,6.734,-58.415,6.851,-58.298,6.879,-58.173,6.829,-58.072,6.821,-57.983,6.786,-57.793,6.599,-57.608,6.45,-57.54,6.332,-57.344,6.272,-57.228,6.178,-57.19,6.097,-57.167,5.885,-57.205,5.565,-57.195,5.548,-57.248,5.485,-57.258,5.445,-57.292,5.374,-57.319,5.335,-57.28,5.247,-57.235,5.243,-57.218,5.232,-57.207,5.214,-57.21,5.195,-57.227,5.179,-57.269,5.157,-57.31,5.106,-57.306,5.05,-57.331,5.02,-57.412,5.005,-57.571,5.004,-57.649,5.001,-57.711,4.991,-57.752,4.954,-57.804,4.929,-57.845,4.923,-57.881,4.881,-57.917,4.82,-57.905,4.779,-57.868,4.724,-57.846,4.668,-57.875,4.577,-57.906,4.507,-57.925,4.453,-57.95,4.35,-58.011,4.236,-58.054,4.172,-58.054,4.102,-58.032,4.002,-57.908,3.857,-57.867,3.787,-57.833,3.676,-57.72,3.588,-57.649,3.517,-57.656,3.424,-57.647,3.395,-57.603,3.371,-57.55,3.353,-57.491,3.354,-57.438,3.362,-57.426,3.375,-57.304,3.377,-57.29,3.354,-57.283,3.219,-57.278,3.164,-57.249,3.142,-57.232,3.109,-57.231,3.079,-57.225,3.003,-57.207,2.963,-57.21,2.883,-57.197,2.853,-57.164,2.833,-57.121,2.776,-57.105,2.768,-57.097,2.748,-57.06,2.666,-57.042,2.641,-57.029,2.638,-57.023,2.609,-56.997,2.532,-56.979,2.513,-56.945,2.457,-56.931,2.395,-56.886,2.326,-56.841,2.277,-56.82,2.227,-56.761,2.115,-56.704,2.036,-56.627,2.016,-56.563,2.005,-56.522,1.975,-56.483,1.942,-56.525,1.927,-56.564,1.907,-56.616,1.923,-56.69,1.914,-56.766,1.892,-56.837,1.881,-56.97,1.916,-57.01,1.921,-57.038,1.936,-57.093,2.006,-57.119,2.014,-57.19,1.982,-57.276,1.959,-57.317,1.963,-57.367,1.94,-57.413,1.909,-57.5,1.774,-57.546,1.726,-57.594,1.704,-57.692,1.705,-57.796,1.7,-57.873,1.667,-57.946,1.651,-57.983,1.648,-57.995,1.574,-58.012,1.54,-58.035,1.52,-58.091,1.514,-58.142,1.517,-58.173,1.548,-58.23,1.563,-58.281,1.574,-58.314,1.592,-58.341,1.588,-58.363,1.557,-58.38,1.53,-58.396,1.482,-58.473,1.466,-58.506,1.439,-58.487,1.348,-58.496,1.312,-58.512,1.285,-58.605,1.279,-58.685,1.281,-58.73,1.248,-58.787,1.208,-58.822,1.201,-58.862,1.204,-58.917,1.249,-58.969,1.305,-59.1,1.344,-59.231,1.376,-59.317,1.465,-59.337,1.508,-59.378,1.527,-59.479,1.632,-59.536,1.7,-59.597,1.718,-59.667,1.746,-59.664,1.795,-59.669,1.842,-59.699,1.861,-59.741,1.874,-59.756,1.901,-59.752,1.962,-59.744,2.122,-59.755,2.274,-59.849,2.327,-59.89,2.363,-59.961,2.588,-59.994,2.69,-59.996,2.765,-59.972,2.99,-59.946,3.088,-59.873,3.283,-59.831,3.349,-59.829,3.399,-59.833,3.462,-59.854,3.588,-59.732,3.667,-59.679,3.7,-59.67,3.753,-59.604,3.82,-59.575,3.883,-59.551,3.934,-59.558,3.96,-59.586,3.975,-59.62,4.023,-59.691,4.16,-59.717,4.188,-59.739,4.227,-59.727,4.288,-59.7,4.354,-59.703,4.381,-59.746,4.417,-59.833,4.476,-59.906,4.48,-59.962,4.502,-60.045,4.505,-60.111,4.511,-60.149,4.533,-60.141,4.57,-60.125,4.598,-60.069,4.667,-60.032,4.741,-60.027,4.813,-60.015,4.908,-59.999,4.99,-59.991,5.083,-60.078,5.144,-60.106,5.194,-60.142,5.239,-60.182,5.239,-60.242,5.258,-60.335,5.199,-60.409,5.21,-60.46,5.188,-60.576,5.192,-60.651,5.221,-60.742,5.202]]]},{"n":"Guinea-Bissau","c":"GW","r":6,"ml":5,"lx":-14.52,"ly":12.16,"b":[-16.712,10.94,-13.674,12.68],"p":[[[-16.712,12.355,-16.657,12.364,-16.521,12.349,-16.416,12.368,-16.342,12.4,-16.242,12.443,-16.144,12.457,-15.84,12.438,-15.575,12.49,-15.378,12.589,-15.196,12.68,-14.961,12.679,-14.708,12.678,-14.349,12.676,-14.065,12.675,-13.729,12.674,-13.733,12.593,-13.674,12.479,-13.682,12.393,-13.708,12.313,-13.73,12.281,-13.76,12.262,-13.849,12.263,-13.887,12.247,-13.947,12.215,-13.949,12.178,-13.901,12.143,-13.862,12.093,-13.816,12.054,-13.738,12.01,-13.731,11.96,-13.729,11.834,-13.733,11.736,-13.953,11.665,-14.122,11.652,-14.266,11.66,-14.328,11.63,-14.452,11.556,-14.605,11.512,-14.683,11.508,-14.72,11.482,-14.779,11.406,-14.944,11.072,-14.999,10.992,-15.043,10.94,-15.094,11.011,-15.055,11.142,-15.097,11.14,-15.181,11.034,-15.222,11.031,-15.217,11.156,-15.263,11.161,-15.317,11.152,-15.393,11.217,-15.401,11.266,-15.395,11.334,-15.348,11.378,-15.355,11.396,-15.399,11.401,-15.449,11.39,-15.479,11.41,-15.429,11.499,-15.253,11.573,-15.164,11.581,-15.073,11.598,-15.122,11.662,-15.23,11.687,-15.317,11.669,-15.36,11.623,-15.413,11.615,-15.502,11.724,-15.5,11.778,-15.467,11.843,-15.416,11.872,-15.211,11.871,-15.133,11.907,-15.102,11.914,-15.072,11.947,-15.078,11.969,-15.112,11.97,-15.188,11.927,-15.435,11.944,-15.513,11.918,-15.651,11.818,-15.819,11.763,-15.942,11.787,-15.903,11.92,-15.92,11.938,-15.959,11.96,-16.138,11.917,-16.274,11.978,-16.328,12.052,-16.319,12.144,-16.255,12.206,-16.245,12.237,-16.312,12.243,-16.437,12.204,-16.712,12.355]],[[-16.115,11.059,-16.195,11.045,-16.231,11.094,-16.236,11.113,-16.195,11.13,-16.176,11.131,-16.144,11.167,-16.105,11.191,-16.087,11.199,-16.067,11.197,-16.053,11.118,-16.072,11.084,-16.115,11.059]],[[-15.896,11.082,-15.905,11.055,-15.964,11.059,-15.951,11.087,-15.963,11.095,-15.946,11.18,-15.938,11.193,-15.909,11.161,-15.905,11.148,-15.896,11.082]],[[-15.725,11.215,-15.725,11.175,-15.767,11.182,-15.78,11.195,-15.755,11.269,-15.717,11.302,-15.672,11.296,-15.658,11.286,-15.667,11.258,-15.687,11.234,-15.725,11.215]],[[-15.553,11.537,-15.563,11.514,-15.62,11.533,-15.537,11.618,-15.482,11.632,-15.484,11.568,-15.526,11.554,-15.553,11.537]],[[-15.902,11.466,-15.949,11.434,-15.997,11.449,-16.023,11.477,-16.019,11.527,-15.965,11.598,-15.915,11.589,-15.902,11.466]],[[-15.986,11.882,-16.038,11.76,-16.102,11.773,-16.147,11.846,-16.152,11.877,-16.022,11.887,-15.986,11.882]]]},{"n":"Guinea","c":"GN","r":3,"ml":3,"lx":-10.02,"ly":10.62,"b":[-15.051,7.216,-7.681,12.674],"p":[[[-10.283,8.485,-10.36,8.496,-10.394,8.481,-10.496,8.362,-10.558,8.316,-10.604,8.319,-10.653,8.33,-10.687,8.322,-10.712,8.335,-10.702,8.364,-10.677,8.401,-10.628,8.53,-10.503,8.66,-10.501,8.688,-10.552,8.764,-10.606,8.868,-10.606,8.979,-10.616,9.059,-10.727,9.082,-10.747,9.095,-10.75,9.122,-10.721,9.194,-10.688,9.261,-10.683,9.289,-10.691,9.314,-10.759,9.385,-10.865,9.516,-10.963,9.662,-11.047,9.786,-11.116,9.843,-11.181,9.925,-11.206,9.978,-11.274,9.997,-11.472,9.995,-11.71,9.994,-11.911,9.993,-11.923,9.923,-12.142,9.875,-12.278,9.93,-12.428,9.898,-12.501,9.862,-12.524,9.787,-12.558,9.705,-12.59,9.671,-12.604,9.634,-12.622,9.601,-12.652,9.562,-12.684,9.484,-12.756,9.374,-12.831,9.302,-12.959,9.263,-12.999,9.147,-13.028,9.104,-13.077,9.07,-13.13,9.048,-13.178,9.061,-13.234,9.07,-13.293,9.049,-13.303,9.078,-13.269,9.171,-13.296,9.219,-13.396,9.314,-13.406,9.361,-13.436,9.42,-13.568,9.543,-13.691,9.536,-13.657,9.639,-13.659,9.776,-13.7,9.851,-13.69,9.928,-13.713,9.923,-13.754,9.87,-13.82,9.887,-13.955,9.969,-14.022,10.048,-14.03,10.115,-14.045,10.141,-14.086,10.127,-14.17,10.129,-14.427,10.248,-14.61,10.55,-14.614,10.618,-14.587,10.735,-14.594,10.767,-14.677,10.689,-14.693,10.741,-14.757,10.862,-14.776,10.932,-14.837,10.963,-14.887,10.968,-14.925,10.945,-14.975,10.803,-15.012,10.804,-15.051,10.835,-15.043,10.94,-14.999,10.992,-14.944,11.072,-14.779,11.406,-14.72,11.482,-14.683,11.508,-14.605,11.512,-14.452,11.556,-14.328,11.63,-14.266,11.66,-14.122,11.652,-13.953,11.665,-13.733,11.736,-13.729,11.834,-13.731,11.96,-13.738,12.01,-13.816,12.054,-13.862,12.093,-13.901,12.143,-13.949,12.178,-13.947,12.215,-13.887,12.247,-13.849,12.263,-13.76,12.262,-13.73,12.281,-13.708,12.313,-13.682,12.393,-13.674,12.479,-13.733,12.593,-13.729,12.674,-13.406,12.662,-13.373,12.654,-13.228,12.64,-13.138,12.64,-13.083,12.634,-13.06,12.615,-13.064,12.581,-13.08,12.536,-13.061,12.49,-13.012,12.478,-12.986,12.492,-12.961,12.514,-12.931,12.532,-12.888,12.52,-12.797,12.452,-12.713,12.433,-12.621,12.396,-12.534,12.376,-12.457,12.378,-12.399,12.34,-12.291,12.328,-12.152,12.377,-12.042,12.398,-11.889,12.403,-11.808,12.387,-11.574,12.426,-11.457,12.418,-11.389,12.404,-11.418,12.378,-11.448,12.319,-11.475,12.247,-11.502,12.199,-11.492,12.167,-11.415,12.104,-11.305,12.015,-11.261,12.004,-11.21,12.025,-11.129,12.095,-11.066,12.171,-11.005,12.208,-10.933,12.205,-10.876,12.152,-10.806,12.034,-10.743,11.927,-10.735,11.916,-10.709,11.899,-10.677,11.899,-10.644,11.926,-10.619,11.941,-10.59,11.99,-10.466,12.139,-10.373,12.18,-10.34,12.19,-10.275,12.213,-10.167,12.177,-10.011,12.116,-9.821,12.042,-9.754,12.03,-9.715,12.042,-9.658,12.143,-9.588,12.182,-9.487,12.229,-9.405,12.252,-9.358,12.255,-9.34,12.283,-9.332,12.324,-9.341,12.366,-9.394,12.442,-9.395,12.465,-9.365,12.479,-9.3,12.49,-9.216,12.483,-9.12,12.45,-9.043,12.402,-8.999,12.346,-8.951,12.226,-8.914,12.109,-8.818,11.923,-8.82,11.807,-8.822,11.673,-8.78,11.648,-8.733,11.638,-8.711,11.618,-8.665,11.515,-8.621,11.485,-8.569,11.478,-8.471,11.412,-8.407,11.386,-8.399,11.367,-8.401,11.339,-8.425,11.305,-8.464,11.281,-8.52,11.236,-8.567,11.177,-8.664,11.036,-8.667,11.009,-8.646,10.99,-8.606,10.987,-8.564,10.997,-8.475,11.048,-8.404,11.03,-8.337,10.991,-8.313,10.95,-8.306,10.896,-8.322,10.827,-8.324,10.75,-8.302,10.618,-8.267,10.486,-8.231,10.438,-8.007,10.322,-7.986,10.278,-7.974,10.23,-7.991,10.163,-8.014,10.125,-8.078,10.067,-8.137,10.022,-8.155,9.973,-8.146,9.882,-8.146,9.675,-8.137,9.496,-8.089,9.431,-8.031,9.398,-7.963,9.404,-7.896,9.416,-7.9,9.309,-7.918,9.189,-7.839,9.152,-7.8,9.115,-7.778,9.081,-7.902,9.017,-7.938,8.98,-7.955,8.879,-7.951,8.787,-7.784,8.721,-7.72,8.643,-7.691,8.563,-7.681,8.41,-7.696,8.376,-7.739,8.375,-7.787,8.422,-7.824,8.468,-7.869,8.468,-7.953,8.478,-8.049,8.495,-8.168,8.491,-8.21,8.483,-8.237,8.456,-8.244,8.408,-8.256,8.254,-8.217,8.22,-8.141,8.181,-8.091,8.165,-8.049,8.17,-8.017,8.145,-8.01,8.079,-8.032,8.03,-8.074,7.984,-8.127,7.868,-8.118,7.824,-8.115,7.761,-8.206,7.59,-8.232,7.557,-8.352,7.591,-8.43,7.602,-8.486,7.558,-8.522,7.586,-8.564,7.625,-8.579,7.677,-8.607,7.688,-8.66,7.688,-8.708,7.659,-8.729,7.605,-8.733,7.544,-8.74,7.496,-8.769,7.467,-8.828,7.392,-8.856,7.323,-8.89,7.263,-8.938,7.266,-8.961,7.275,-8.977,7.259,-9.052,7.225,-9.118,7.216,-9.135,7.251,-9.173,7.278,-9.215,7.333,-9.263,7.378,-9.355,7.409,-9.392,7.395,-9.435,7.398,-9.464,7.416,-9.46,7.443,-9.411,7.51,-9.384,7.572,-9.369,7.64,-9.369,7.704,-9.395,7.795,-9.436,7.867,-9.446,7.908,-9.442,7.968,-9.451,8.023,-9.465,8.052,-9.471,8.107,-9.484,8.157,-9.508,8.176,-9.522,8.26,-9.518,8.346,-9.554,8.379,-9.61,8.402,-9.643,8.436,-9.664,8.474,-9.684,8.484,-9.701,8.482,-9.717,8.459,-9.736,8.454,-9.768,8.535,-9.782,8.538,-9.805,8.519,-10.064,8.43,-10.076,8.465,-10.098,8.506,-10.147,8.52,-10.233,8.489,-10.283,8.485]]]},{"n":"Guatemala","c":"GT","r":3,"ml":3,"lx":-90.5,"ly":14.98,"b":[-92.235,13.737,-88.228,17.816],"p":[[[-92.235,14.545,-92.209,14.571,-92.187,14.63,-92.16,14.691,-92.176,14.761,-92.186,14.818,-92.156,14.901,-92.159,14.964,-92.144,15.002,-92.099,15.027,-92.075,15.074,-92.204,15.238,-92.204,15.275,-92.187,15.321,-92.082,15.496,-91.957,15.703,-91.819,15.932,-91.737,16.07,-91.434,16.07,-91.234,16.071,-90.98,16.071,-90.703,16.071,-90.522,16.071,-90.447,16.073,-90.46,16.162,-90.45,16.261,-90.417,16.351,-90.417,16.391,-90.471,16.44,-90.576,16.468,-90.634,16.511,-90.634,16.565,-90.66,16.631,-90.711,16.708,-90.816,16.787,-90.976,16.868,-91.112,16.976,-91.224,17.112,-91.319,17.2,-91.392,17.236,-91.41,17.256,-91.196,17.254,-90.993,17.252,-90.992,17.447,-90.99,17.621,-90.989,17.816,-90.622,17.816,-90.184,17.816,-89.729,17.815,-89.372,17.815,-89.161,17.815,-89.171,17.572,-89.182,17.291,-89.19,17.085,-89.201,16.809,-89.212,16.527,-89.228,16.143,-89.237,15.894,-89.233,15.889,-89.114,15.901,-88.937,15.89,-88.894,15.891,-88.84,15.869,-88.798,15.863,-88.709,15.807,-88.603,15.764,-88.536,15.85,-88.572,15.901,-88.598,15.927,-88.594,15.95,-88.228,15.729,-88.271,15.695,-88.365,15.616,-88.533,15.481,-88.684,15.36,-88.83,15.251,-88.961,15.152,-88.976,15.143,-89.143,15.072,-89.17,15.04,-89.206,14.901,-89.222,14.866,-89.192,14.789,-89.162,14.669,-89.172,14.607,-89.287,14.53,-89.339,14.461,-89.363,14.416,-89.383,14.428,-89.419,14.431,-89.501,14.414,-89.541,14.41,-89.574,14.39,-89.577,14.347,-89.555,14.277,-89.547,14.241,-89.57,14.225,-89.671,14.183,-89.711,14.141,-89.749,14.077,-89.794,14.05,-89.84,14.055,-89.873,14.046,-89.943,13.997,-90.048,13.904,-90.105,13.835,-90.106,13.783,-90.095,13.737,-90.479,13.901,-90.607,13.929,-91.146,13.926,-91.377,13.99,-91.641,14.115,-91.819,14.228,-92.235,14.545]]]},{"n":"Grenada","c":"GD","r":6,"ml":4,"lx":-61.68,"ly":12.11,"b":[-61.782,12.008,-61.607,12.237],"p":[[[-61.716,12.013,-61.782,12.008,-61.756,12.045,-61.75,12.108,-61.715,12.185,-61.66,12.237,-61.607,12.223,-61.627,12.054,-61.716,12.013]]]},{"n":"Greece","c":"GR","r":3,"ml":2.7,"lx":21.73,"ly":39.49,"b":[19.646,34.934,28.232,41.744],"p":[[[27.86,36.554,27.838,36.538,27.788,36.584,27.786,36.608,27.837,36.635,27.863,36.641,27.87,36.623,27.869,36.583,27.86,36.554]],[[20.612,38.383,20.625,38.268,20.695,38.246,20.789,38.142,20.781,38.089,20.761,38.071,20.606,38.12,20.569,38.098,20.524,38.107,20.496,38.164,20.499,38.184,20.473,38.219,20.452,38.234,20.391,38.188,20.353,38.18,20.352,38.222,20.409,38.337,20.435,38.356,20.481,38.318,20.52,38.332,20.548,38.395,20.55,38.457,20.563,38.475,20.612,38.383]],[[20.888,37.805,20.994,37.708,20.909,37.732,20.819,37.665,20.704,37.743,20.635,37.823,20.62,37.855,20.692,37.93,20.759,37.853,20.84,37.841,20.888,37.805]],[[20.687,38.609,20.648,38.601,20.614,38.606,20.584,38.602,20.555,38.583,20.558,38.662,20.592,38.76,20.635,38.818,20.694,38.844,20.72,38.799,20.715,38.638,20.687,38.609]],[[20.759,38.329,20.709,38.319,20.646,38.414,20.624,38.48,20.65,38.484,20.675,38.476,20.701,38.451,20.701,38.426,20.712,38.399,20.739,38.366,20.759,38.329]],[[20.078,39.433,20.1,39.377,19.975,39.411,19.884,39.462,19.809,39.585,19.649,39.726,19.646,39.767,19.707,39.798,19.839,39.82,19.892,39.797,19.926,39.774,19.937,39.747,19.862,39.693,19.847,39.668,19.904,39.619,19.903,39.6,19.927,39.506,19.955,39.47,20.028,39.442,20.078,39.433]],[[23.415,38.959,23.472,38.85,23.525,38.813,23.636,38.77,23.688,38.765,23.878,38.687,24.099,38.671,24.128,38.648,24.155,38.588,24.2,38.541,24.211,38.504,24.188,38.463,24.22,38.339,24.276,38.22,24.36,38.163,24.464,38.145,24.563,38.148,24.588,38.124,24.579,38.02,24.537,37.98,24.502,37.97,24.473,37.981,24.446,38.005,24.417,38.017,24.359,38.019,24.318,38.06,24.212,38.118,24.193,38.152,24.189,38.204,24.144,38.243,24.103,38.317,24.064,38.337,24.042,38.374,24.04,38.39,23.886,38.401,23.759,38.401,23.651,38.443,23.617,38.553,23.553,38.582,23.505,38.613,23.465,38.656,23.364,38.735,23.252,38.801,23.144,38.845,23.029,38.873,22.936,38.84,22.881,38.848,22.87,38.871,22.986,38.916,23.146,39.003,23.258,39.031,23.313,39.035,23.415,38.959]],[[23.78,39.114,23.735,39.081,23.666,39.095,23.594,39.209,23.78,39.114]],[[23.888,39.158,23.841,39.147,23.888,39.226,23.971,39.268,23.94,39.201,23.888,39.158]],[[24.675,38.809,24.569,38.785,24.541,38.789,24.565,38.819,24.566,38.832,24.461,38.889,24.473,38.962,24.486,38.98,24.564,38.942,24.581,38.879,24.675,38.809]],[[24.774,40.615,24.646,40.579,24.516,40.647,24.517,40.687,24.586,40.769,24.623,40.793,24.719,40.786,24.774,40.73,24.786,40.704,24.769,40.659,24.774,40.615]],[[23.551,37.926,23.511,37.901,23.467,37.902,23.435,37.911,23.419,37.931,23.439,37.941,23.462,37.98,23.484,37.991,23.516,37.986,23.535,37.97,23.551,37.926]],[[23.054,36.19,23.042,36.146,22.939,36.176,22.911,36.221,22.906,36.32,22.933,36.369,22.95,36.384,22.998,36.328,23.097,36.247,23.054,36.19]],[[27.176,35.465,27.138,35.409,27.099,35.456,27.116,35.511,27.071,35.598,27.156,35.726,27.158,35.789,27.223,35.82,27.207,35.714,27.157,35.629,27.209,35.559,27.234,35.479,27.176,35.465]],[[27.02,36.959,26.92,36.945,26.938,37.025,26.889,37.087,26.967,37.052,27.016,37.01,27.04,37.002,27.035,36.976,27.02,36.959]],[[26.95,36.727,26.918,36.726,26.956,36.774,27.061,36.84,27.215,36.899,27.266,36.905,27.352,36.869,27.193,36.809,27.151,36.778,27.034,36.771,26.95,36.727]],[[25.546,36.968,25.457,36.93,25.396,36.984,25.362,37.07,25.525,37.196,25.564,37.185,25.588,37.153,25.584,37.039,25.546,36.968]],[[25.279,37.068,25.199,36.991,25.133,37,25.105,37.035,25.146,37.107,25.235,37.149,25.275,37.138,25.271,37.084,25.279,37.068]],[[25.482,36.393,25.436,36.34,25.371,36.359,25.397,36.379,25.413,36.405,25.415,36.442,25.397,36.465,25.409,36.474,25.467,36.435,25.482,36.393]],[[25.382,36.674,25.364,36.658,25.289,36.722,25.26,36.758,25.296,36.789,25.407,36.717,25.382,36.674]],[[26.824,37.811,26.947,37.778,26.982,37.782,27.04,37.77,27.055,37.709,26.978,37.7,26.845,37.645,26.788,37.657,26.721,37.705,26.613,37.71,26.581,37.724,26.639,37.781,26.743,37.81,26.824,37.811]],[[26.029,37.529,25.982,37.526,25.997,37.566,26.086,37.635,26.212,37.638,26.326,37.673,26.351,37.674,26.297,37.62,26.205,37.569,26.029,37.529]],[[25.859,36.79,25.771,36.782,25.743,36.79,25.797,36.807,25.834,36.825,25.852,36.848,25.942,36.887,26.001,36.937,26.064,36.903,25.985,36.88,25.859,36.79]],[[26.461,36.585,26.382,36.562,26.331,36.511,26.27,36.547,26.27,36.595,26.337,36.581,26.384,36.608,26.37,36.639,26.421,36.624,26.461,36.585]],[[24.356,37.577,24.289,37.528,24.277,37.601,24.32,37.678,24.379,37.683,24.401,37.649,24.356,37.577]],[[24.436,37.344,24.379,37.314,24.398,37.383,24.37,37.42,24.395,37.45,24.431,37.475,24.449,37.45,24.481,37.408,24.436,37.344]],[[24.536,36.764,24.538,36.705,24.531,36.684,24.326,36.656,24.345,36.723,24.357,36.744,24.425,36.713,24.45,36.729,24.46,36.747,24.536,36.764]],[[24.943,37.494,24.938,37.39,24.912,37.391,24.896,37.407,24.895,37.446,24.907,37.509,24.943,37.494]],[[24.992,37.76,24.962,37.692,24.884,37.771,24.799,37.824,24.767,37.871,24.714,37.899,24.7,37.962,24.763,37.988,24.79,37.99,24.855,37.914,24.956,37.905,24.948,37.858,24.98,37.797,24.992,37.76]],[[25.256,37.6,25.219,37.535,25.156,37.545,25.052,37.614,25.016,37.646,24.996,37.677,25.039,37.681,25.092,37.648,25.225,37.631,25.256,37.6]],[[24.721,36.921,24.703,36.917,24.676,36.959,24.671,36.999,24.681,37.022,24.716,37.024,24.763,36.949,24.721,36.921]],[[26.094,38.218,25.999,38.162,25.892,38.243,25.874,38.27,25.953,38.303,25.991,38.354,25.96,38.416,25.851,38.508,25.846,38.574,26.013,38.602,26.11,38.545,26.16,38.541,26.141,38.486,26.15,38.468,26.157,38.303,26.111,38.28,26.103,38.234,26.094,38.218]],[[26.41,39.329,26.393,39.27,26.531,39.172,26.578,39.11,26.596,39.049,26.584,39.031,26.532,39.064,26.489,39.075,26.503,39.031,26.547,38.994,26.469,38.973,26.39,38.974,26.161,39.026,26.108,39.081,26.245,39.164,26.273,39.198,26.176,39.194,26.072,39.096,25.906,39.139,25.855,39.179,25.844,39.2,25.91,39.288,26.026,39.285,26.088,39.304,26.165,39.332,26.165,39.374,26.348,39.383,26.41,39.329]],[[25.686,40.427,25.573,40.4,25.448,40.483,25.569,40.516,25.624,40.492,25.664,40.464,25.686,40.427]],[[25.438,39.983,25.4,39.95,25.372,39.891,25.357,39.808,25.299,39.806,25.263,39.823,25.252,39.854,25.249,39.894,25.224,39.893,25.203,39.849,25.185,39.83,25.126,39.826,25.062,39.852,25.065,39.91,25.052,39.976,25.058,40,25.234,40.005,25.286,39.956,25.348,39.985,25.374,40.016,25.449,40.035,25.438,39.983]],[[25.403,37.419,25.307,37.413,25.313,37.489,25.348,37.509,25.463,37.471,25.457,37.447,25.403,37.419]],[[24.524,37.125,24.487,37.11,24.425,37.132,24.441,37.187,24.484,37.21,24.529,37.192,24.536,37.168,24.524,37.125]],[[27.843,35.929,27.771,35.908,27.746,35.911,27.716,35.957,27.757,36.069,27.719,36.141,27.716,36.172,27.774,36.214,27.815,36.277,27.914,36.345,28.171,36.426,28.232,36.434,28.23,36.37,28.144,36.21,28.068,36.13,28.088,36.065,27.966,36.048,27.843,35.929]],[[23.852,35.535,23.921,35.528,24.013,35.529,24.034,35.535,24.093,35.594,24.166,35.595,24.198,35.537,24.124,35.511,24.109,35.496,24.123,35.484,24.179,35.46,24.255,35.469,24.258,35.423,24.275,35.386,24.313,35.364,24.354,35.359,24.445,35.366,24.535,35.381,24.627,35.41,24.721,35.425,25.003,35.41,25.104,35.347,25.297,35.339,25.476,35.306,25.57,35.328,25.73,35.349,25.756,35.326,25.735,35.184,25.745,35.143,25.791,35.123,25.837,35.133,25.893,35.179,26.028,35.215,26.168,35.215,26.286,35.31,26.32,35.315,26.299,35.269,26.281,35.159,26.256,35.095,26.244,35.045,26.166,35.019,26.047,35.014,25.83,35.025,25.611,35.007,25.206,34.959,24.8,34.934,24.745,34.951,24.744,35.014,24.735,35.058,24.709,35.089,24.583,35.115,24.464,35.16,23.994,35.222,23.884,35.246,23.704,35.233,23.638,35.235,23.593,35.257,23.562,35.295,23.548,35.416,23.57,35.535,23.609,35.566,23.627,35.53,23.673,35.514,23.715,35.55,23.715,35.605,23.737,35.656,23.771,35.634,23.793,35.556,23.852,35.535]],[[26.321,41.717,26.411,41.696,26.463,41.663,26.495,41.633,26.545,41.607,26.581,41.601,26.61,41.512,26.625,41.402,26.602,41.354,26.536,41.343,26.331,41.239,26.326,41.143,26.328,41.097,26.333,41.064,26.354,41.037,26.354,40.997,26.331,40.954,26.241,40.883,26.179,40.827,26.109,40.75,26.07,40.74,26.039,40.727,26.011,40.769,25.856,40.844,25.497,40.888,25.325,40.943,25.25,40.933,25.104,40.995,25.005,40.968,24.793,40.858,24.679,40.869,24.557,40.936,24.477,40.948,24.384,40.913,24.234,40.786,24.082,40.724,23.946,40.748,23.763,40.748,23.743,40.677,23.779,40.628,23.879,40.544,23.832,40.482,23.867,40.419,23.932,40.406,24.031,40.409,24.213,40.328,24.292,40.242,24.343,40.148,24.232,40.215,24.159,40.28,24.056,40.304,23.913,40.359,23.823,40.368,23.728,40.33,23.721,40.286,23.823,40.205,23.918,40.155,23.967,40.115,24.001,40.025,23.982,39.994,23.947,39.966,23.835,40.022,23.665,40.224,23.426,40.264,23.386,40.222,23.433,40.115,23.467,40.074,23.674,39.959,23.658,39.934,23.627,39.924,23.396,39.99,23.328,40.09,23.312,40.216,23.098,40.304,22.896,40.4,22.851,40.491,22.893,40.524,22.922,40.591,22.811,40.579,22.742,40.536,22.629,40.496,22.625,40.429,22.643,40.367,22.605,40.276,22.569,40.119,22.592,40.037,22.836,39.801,22.919,39.629,22.979,39.564,23.103,39.492,23.233,39.358,23.288,39.289,23.328,39.175,23.218,39.104,23.155,39.101,23.119,39.133,23.169,39.21,23.162,39.258,22.993,39.331,22.921,39.306,22.839,39.259,22.886,39.17,22.939,39.112,22.966,39.031,23.067,39.038,22.93,38.948,22.803,38.902,22.677,38.899,22.597,38.891,22.569,38.867,22.634,38.851,22.688,38.849,22.774,38.8,23.02,38.742,23.138,38.668,23.253,38.661,23.369,38.526,23.57,38.489,23.684,38.352,23.836,38.325,23.967,38.275,24.005,38.227,24.025,38.14,24.033,37.955,24.061,37.818,24.062,37.775,24.055,37.71,24.02,37.678,23.972,37.677,23.877,37.778,23.733,37.884,23.58,38.011,23.537,38.033,23.502,38.035,23.42,37.992,23.194,37.959,23.087,37.913,23.047,37.903,23.036,37.878,23.086,37.853,23.147,37.795,23.147,37.716,23.198,37.62,23.263,37.595,23.348,37.598,23.396,37.58,23.409,37.542,23.458,37.497,23.491,37.464,23.489,37.44,23.253,37.377,23.203,37.349,23.162,37.334,23.1,37.364,23.096,37.441,23.015,37.482,22.941,37.517,22.851,37.532,22.775,37.585,22.725,37.542,22.765,37.393,22.851,37.291,22.995,37.016,23.06,36.854,23.074,36.775,23.041,36.645,23.112,36.548,23.16,36.448,23.107,36.452,23.061,36.487,22.983,36.528,22.832,36.687,22.78,36.786,22.717,36.794,22.608,36.78,22.489,36.568,22.489,36.447,22.428,36.476,22.375,36.514,22.381,36.646,22.376,36.702,22.231,36.883,22.165,36.903,22.134,36.964,22.08,37.029,22.012,37.017,21.956,36.99,21.94,36.892,21.934,36.804,21.892,36.737,21.738,36.863,21.583,37.081,21.579,37.2,21.692,37.309,21.679,37.387,21.571,37.541,21.416,37.64,21.329,37.669,21.288,37.775,21.205,37.829,21.138,37.854,21.125,37.892,21.145,37.919,21.308,38.027,21.404,38.197,21.451,38.205,21.549,38.165,21.658,38.175,21.748,38.274,21.825,38.328,21.953,38.321,22.244,38.189,22.556,38.113,22.712,38.047,22.8,37.981,22.846,37.968,22.92,37.958,22.917,38.007,22.893,38.051,22.955,38.075,23.122,38.073,23.153,38.096,23.183,38.134,23.149,38.176,23.094,38.196,23.034,38.202,22.995,38.216,22.933,38.202,22.834,38.235,22.784,38.262,22.754,38.29,22.583,38.345,22.422,38.439,22.385,38.386,22.32,38.357,22.227,38.353,21.965,38.412,21.805,38.367,21.717,38.355,21.65,38.354,21.568,38.334,21.473,38.321,21.39,38.408,21.355,38.475,21.331,38.487,21.33,38.424,21.303,38.374,21.183,38.346,21.113,38.385,21.06,38.503,20.992,38.654,20.873,38.776,20.777,38.808,20.769,38.874,20.777,38.928,20.893,38.941,21.074,38.885,21.112,38.896,21.152,38.922,21.145,38.979,21.118,39.03,21.069,39.032,21.034,39.026,20.923,39.037,20.78,39.009,20.713,39.035,20.691,39.067,20.572,39.148,20.468,39.255,20.301,39.327,20.191,39.546,20.099,39.641,20.001,39.709,20.023,39.711,20.06,39.699,20.131,39.662,20.207,39.654,20.248,39.678,20.272,39.701,20.288,39.739,20.294,39.782,20.306,39.797,20.364,39.792,20.382,39.803,20.382,39.842,20.344,39.891,20.311,39.951,20.311,39.979,20.338,39.991,20.384,40.017,20.408,40.049,20.456,40.066,20.527,40.069,20.606,40.083,20.657,40.117,20.665,40.152,20.697,40.246,20.718,40.293,20.752,40.335,20.77,40.392,20.806,40.445,20.882,40.468,20.95,40.494,21.002,40.563,21.031,40.622,21.031,40.659,20.988,40.718,20.956,40.775,20.964,40.85,21.1,40.856,21.148,40.863,21.324,40.867,21.404,40.907,21.46,40.904,21.576,40.869,21.628,40.896,21.779,40.95,21.929,41.107,21.993,41.131,22.139,41.141,22.184,41.159,22.238,41.155,22.401,41.123,22.494,41.119,22.604,41.14,22.725,41.179,22.755,41.313,22.784,41.332,22.859,41.337,22.916,41.336,23.026,41.326,23.156,41.322,23.24,41.385,23.372,41.39,23.433,41.399,23.536,41.386,23.635,41.387,23.762,41.413,23.881,41.456,23.974,41.452,24.011,41.46,24.033,41.469,24.056,41.527,24.23,41.531,24.289,41.525,24.387,41.524,24.488,41.555,24.518,41.553,24.569,41.467,24.596,41.443,24.651,41.42,24.774,41.356,24.796,41.373,24.847,41.394,24.994,41.365,25.133,41.316,25.251,41.244,25.382,41.264,25.527,41.3,25.621,41.31,25.724,41.315,25.785,41.33,25.923,41.312,26.066,41.351,26.135,41.386,26.155,41.435,26.144,41.522,26.111,41.608,26.077,41.64,26.066,41.673,26.086,41.704,26.107,41.726,26.201,41.744,26.321,41.717]]]},{"n":"Ghana","c":"GH","r":3,"ml":2.7,"lx":-1.04,"ly":7.72,"b":[-3.244,4.762,1.187,11.167],"p":[[[-0.069,11.116,-0.005,11.056,0.009,11.021,-0.014,10.891,-0.061,10.801,-0.09,10.716,-0.086,10.673,-0.058,10.631,0.039,10.564,0.089,10.521,0.148,10.455,0.216,10.391,0.332,10.307,0.381,10.292,0.379,10.269,0.363,10.236,0.352,9.925,0.343,9.845,0.335,9.804,0.324,9.688,0.312,9.671,0.29,9.672,0.27,9.668,0.265,9.645,0.273,9.621,0.343,9.604,0.327,9.587,0.275,9.571,0.252,9.536,0.262,9.496,0.233,9.464,0.242,9.442,0.26,9.426,0.289,9.432,0.371,9.486,0.405,9.491,0.448,9.48,0.526,9.398,0.529,9.358,0.497,9.221,0.466,9.115,0.46,8.974,0.493,8.895,0.489,8.851,0.453,8.814,0.373,8.759,0.379,8.722,0.415,8.653,0.483,8.575,0.616,8.48,0.686,8.355,0.688,8.304,0.647,8.253,0.599,8.21,0.584,8.146,0.605,7.728,0.5,7.547,0.499,7.495,0.51,7.435,0.537,7.399,0.591,7.389,0.635,7.354,0.62,7.227,0.596,7.097,0.592,7.034,0.579,7.004,0.538,6.98,0.523,6.939,0.533,6.888,0.526,6.851,0.548,6.802,0.596,6.742,0.673,6.593,0.702,6.581,0.715,6.549,0.707,6.519,0.737,6.453,0.822,6.386,0.912,6.329,0.985,6.32,1.002,6.269,1.05,6.203,1.084,6.174,1.14,6.155,1.185,6.145,1.187,6.089,1.106,6.051,1.05,5.994,1.008,5.906,0.95,5.81,0.749,5.76,0.672,5.76,0.26,5.757,-0.127,5.568,-0.349,5.501,-0.485,5.394,-0.669,5.319,-0.798,5.227,-1.064,5.183,-1.502,5.038,-1.638,4.981,-1.777,4.88,-2.002,4.762,-2.09,4.764,-2.266,4.874,-2.399,4.929,-2.723,5.014,-2.965,5.046,-3.082,5.082,-3.114,5.089,-3.087,5.128,-3.019,5.131,-2.948,5.119,-2.895,5.149,-2.816,5.153,-2.795,5.185,-2.789,5.264,-2.79,5.328,-2.762,5.357,-2.755,5.433,-2.794,5.6,-2.821,5.619,-2.962,5.643,-2.973,5.676,-2.998,5.711,-3.025,5.798,-3.056,5.926,-3.106,6.086,-3.201,6.348,-3.224,6.441,-3.24,6.536,-3.244,6.649,-3.224,6.691,-3.227,6.749,-3.236,6.807,-3.169,6.941,-3.038,7.105,-3.01,7.164,-2.986,7.205,-2.982,7.264,-2.959,7.455,-2.896,7.685,-2.857,7.772,-2.83,7.819,-2.798,7.896,-2.79,7.932,-2.669,8.022,-2.613,8.047,-2.601,8.082,-2.62,8.121,-2.612,8.148,-2.583,8.161,-2.538,8.172,-2.506,8.209,-2.557,8.493,-2.598,8.776,-2.6,8.8,-2.625,8.84,-2.649,8.957,-2.69,9.025,-2.747,9.045,-2.747,9.11,-2.689,9.219,-2.674,9.283,-2.702,9.302,-2.706,9.351,-2.686,9.432,-2.696,9.481,-2.706,9.534,-2.766,9.658,-2.781,9.746,-2.75,9.797,-2.751,9.91,-2.783,10.083,-2.788,10.193,-2.767,10.238,-2.777,10.282,-2.82,10.323,-2.823,10.363,-2.787,10.402,-2.791,10.432,-2.837,10.455,-2.878,10.508,-2.915,10.592,-2.907,10.728,-2.839,10.977,-2.83,10.998,-2.752,10.997,-2.752,10.986,-2.509,10.989,-2.232,10.991,-1.901,10.995,-1.6,10.998,-1.586,11.009,-1.537,11.023,-1.233,10.997,-1.042,11.01,-0.962,11.002,-0.903,10.985,-0.772,10.995,-0.701,10.989,-0.649,10.927,-0.627,10.927,-0.598,10.954,-0.545,10.984,-0.492,11.008,-0.454,11.056,-0.43,11.093,-0.396,11.086,-0.346,11.088,-0.313,11.119,-0.299,11.167,-0.069,11.116]]]},{"n":"Germany","c":"DE","r":2,"ml":1.7,"lx":9.68,"ly":50.96,"b":[5.858,47.279,15.017,55.059],"p":[[[9.524,47.524,9.35,47.599,9.183,47.671,9.128,47.671,8.881,47.656,8.874,47.663,8.831,47.704,8.793,47.717,8.77,47.71,8.755,47.698,8.728,47.7,8.618,47.766,8.573,47.776,8.51,47.767,8.436,47.731,8.403,47.688,8.413,47.663,8.452,47.652,8.552,47.659,8.567,47.652,8.571,47.638,8.559,47.624,8.478,47.613,8.454,47.596,8.43,47.592,8.415,47.59,8.328,47.607,8.198,47.607,8.094,47.576,7.927,47.564,7.698,47.57,7.616,47.593,7.565,47.607,7.529,47.674,7.539,47.774,7.593,47.906,7.608,48.003,7.584,48.064,7.617,48.157,7.706,48.28,7.765,48.41,7.795,48.547,7.838,48.636,7.923,48.699,8.124,48.873,8.14,48.886,8.135,48.974,8.081,48.986,8.001,49.011,7.799,49.042,7.611,49.062,7.525,49.086,7.451,49.152,7.404,49.153,7.313,49.13,7.2,49.114,7.117,49.128,7.066,49.125,7.037,49.113,7.022,49.123,7.001,49.18,6.958,49.195,6.891,49.208,6.85,49.202,6.821,49.174,6.776,49.154,6.735,49.161,6.608,49.291,6.575,49.32,6.566,49.346,6.534,49.395,6.458,49.443,6.382,49.458,6.344,49.453,6.348,49.513,6.378,49.6,6.407,49.645,6.445,49.682,6.485,49.708,6.494,49.754,6.487,49.798,6.441,49.805,6.325,49.838,6.256,49.872,6.205,49.915,6.138,49.974,6.11,50.034,6.108,50.094,6.117,50.121,6.121,50.139,6.175,50.233,6.364,50.316,6.344,50.4,6.341,50.452,6.295,50.485,6.203,50.499,6.179,50.523,6.168,50.545,6.236,50.597,6.154,50.637,6.119,50.679,6.006,50.732,5.994,50.75,6.048,50.905,6.007,50.95,5.955,50.973,5.895,50.984,5.867,51.006,5.858,51.03,5.868,51.045,5.939,51.041,5.961,51.057,6.13,51.147,6.137,51.165,6.113,51.175,6.082,51.18,6.075,51.199,6.076,51.224,6.166,51.355,6.193,51.411,6.199,51.45,6.193,51.489,6.142,51.55,6.091,51.599,6.089,51.638,6.053,51.658,5.949,51.762,5.949,51.803,6.008,51.834,6.09,51.854,6.117,51.87,6.167,51.881,6.297,51.851,6.356,51.825,6.372,51.83,6.425,51.858,6.518,51.854,6.742,51.911,6.775,51.938,6.8,51.967,6.802,51.98,6.716,52.036,6.713,52.057,6.725,52.08,6.749,52.099,6.8,52.111,6.855,52.136,6.977,52.206,7.02,52.266,7.033,52.331,7.035,52.38,7.002,52.419,6.968,52.444,6.922,52.44,6.833,52.442,6.749,52.464,6.703,52.499,6.692,52.53,6.712,52.55,6.719,52.574,6.705,52.598,6.711,52.618,6.748,52.634,7.013,52.634,7.033,52.651,7.051,52.745,7.117,52.887,7.179,52.966,7.19,53,7.189,53.187,7.197,53.282,7.152,53.327,7.053,53.376,7.074,53.478,7.107,53.557,7.206,53.655,7.285,53.681,7.629,53.697,8.009,53.691,8.167,53.543,8.108,53.468,8.201,53.432,8.245,53.445,8.279,53.511,8.302,53.584,8.334,53.606,8.451,53.552,8.493,53.514,8.495,53.394,8.538,53.557,8.506,53.671,8.528,53.781,8.576,53.838,8.619,53.875,8.898,53.836,9.206,53.856,9.322,53.813,9.585,53.6,9.673,53.566,9.784,53.555,9.631,53.6,9.312,53.859,9.216,53.891,9.07,53.901,8.978,53.926,8.92,53.965,8.904,54,8.907,54.261,8.852,54.3,8.78,54.313,8.736,54.295,8.645,54.295,8.626,54.354,8.648,54.398,8.831,54.428,8.952,54.468,8.957,54.538,8.881,54.594,8.79,54.696,8.682,54.792,8.67,54.903,8.671,54.903,8.857,54.901,8.903,54.897,9.186,54.845,9.255,54.808,9.342,54.806,9.499,54.84,9.616,54.855,9.661,54.834,9.725,54.826,9.74,54.826,9.746,54.807,9.892,54.781,9.954,54.738,10.022,54.674,10.029,54.581,9.941,54.515,9.869,54.472,10.143,54.488,10.171,54.45,10.212,54.409,10.36,54.438,10.732,54.316,10.956,54.376,11.013,54.379,11.064,54.281,11.009,54.181,10.811,54.075,10.855,54.01,10.918,53.995,11.104,54.009,11.4,53.945,11.461,53.965,11.701,54.114,11.796,54.145,12.111,54.168,12.169,54.226,12.296,54.284,12.379,54.347,12.575,54.467,12.779,54.446,12.898,54.423,13.029,54.411,13.147,54.283,13.448,54.141,13.724,54.153,13.822,54.019,13.866,53.853,13.95,53.801,14.025,53.767,14.25,53.732,14.259,53.73,14.266,53.707,14.28,53.625,14.299,53.556,14.415,53.283,14.412,53.217,14.411,53.199,14.369,53.106,14.293,53.027,14.194,52.982,14.139,52.933,14.129,52.878,14.254,52.783,14.514,52.646,14.619,52.529,14.57,52.431,14.555,52.36,14.574,52.314,14.616,52.278,14.68,52.25,14.705,52.207,14.692,52.15,14.705,52.11,14.753,52.082,14.748,52.071,14.725,52.031,14.693,51.958,14.675,51.905,14.602,51.832,14.624,51.771,14.681,51.698,14.725,51.662,14.739,51.627,14.711,51.545,14.725,51.524,14.906,51.463,14.936,51.435,14.953,51.377,15.017,51.253,14.964,51.095,14.917,51.009,14.814,50.872,14.809,50.859,14.797,50.842,14.767,50.818,14.723,50.815,14.658,50.833,14.614,50.856,14.624,50.915,14.595,50.919,14.56,50.955,14.546,50.994,14.507,51.01,14.367,51.026,14.32,51.038,14.283,51.029,14.256,51.002,14.273,50.977,14.299,50.953,14.377,50.914,14.369,50.899,14.202,50.861,14.096,50.823,13.998,50.801,13.899,50.761,13.701,50.717,13.557,50.705,13.527,50.693,13.473,50.617,13.436,50.601,13.401,50.609,13.375,50.622,13.341,50.611,13.306,50.586,13.27,50.576,13.238,50.577,13.181,50.51,13.016,50.49,12.997,50.456,12.967,50.416,12.943,50.406,12.868,50.422,12.765,50.431,12.706,50.409,12.636,50.397,12.549,50.393,12.453,50.35,12.359,50.273,12.306,50.206,12.277,50.181,12.231,50.245,12.175,50.288,12.135,50.311,12.099,50.311,12.09,50.302,12.09,50.269,12.128,50.213,12.175,50.176,12.183,50.148,12.208,50.098,12.276,50.042,12.384,49.999,12.458,49.956,12.512,49.896,12.513,49.877,12.498,49.853,12.472,49.83,12.45,49.8,12.391,49.74,12.408,49.713,12.457,49.68,12.5,49.64,12.556,49.575,12.632,49.461,12.681,49.415,12.748,49.366,12.813,49.329,12.917,49.33,13.024,49.26,13.141,49.158,13.228,49.112,13.289,49.097,13.339,49.061,13.384,49.008,13.401,48.978,13.441,48.956,13.548,48.96,13.685,48.877,13.77,48.816,13.815,48.767,13.803,48.748,13.797,48.686,13.799,48.622,13.785,48.587,13.724,48.542,13.692,48.533,13.675,48.523,13.487,48.582,13.472,48.572,13.46,48.565,13.409,48.394,13.375,48.361,13.323,48.331,13.215,48.302,13.14,48.29,13.082,48.275,12.897,48.204,12.814,48.161,12.76,48.107,12.76,48.076,12.85,47.985,12.954,47.891,12.954,47.808,12.908,47.746,12.898,47.722,12.928,47.713,12.986,47.709,13.034,47.699,13.054,47.655,13.048,47.579,13.032,47.508,13.014,47.478,12.968,47.476,12.879,47.506,12.809,47.542,12.783,47.564,12.781,47.59,12.796,47.607,12.771,47.639,12.686,47.669,12.594,47.656,12.527,47.636,12.483,47.637,12.436,47.666,12.363,47.688,12.268,47.703,12.209,47.718,12.197,47.709,12.204,47.647,12.186,47.62,11.717,47.583,11.574,47.55,11.47,47.506,11.393,47.487,11.374,47.46,11.298,47.425,11.212,47.414,11.191,47.425,11.136,47.409,11.042,47.393,10.981,47.398,10.952,47.427,10.894,47.47,10.871,47.501,10.873,47.52,10.742,47.524,10.659,47.547,10.483,47.542,10.439,47.552,10.43,47.541,10.404,47.417,10.369,47.366,10.313,47.313,10.241,47.284,10.183,47.279,10.186,47.317,10.2,47.363,10.159,47.374,10.096,47.38,10.066,47.393,10.074,47.429,10.06,47.449,10.034,47.474,9.972,47.505,9.839,47.552,9.749,47.576,9.715,47.551,9.651,47.526,9.549,47.534,9.524,47.524]],[[13.709,54.383,13.734,54.315,13.707,54.281,13.595,54.338,13.482,54.337,13.415,54.25,13.364,54.246,13.19,54.326,13.162,54.365,13.156,54.397,13.181,54.509,13.177,54.544,13.231,54.583,13.24,54.638,13.337,54.697,13.423,54.699,13.45,54.65,13.491,54.615,13.636,54.577,13.658,54.56,13.671,54.535,13.603,54.488,13.58,54.464,13.602,54.425,13.709,54.383]],[[14.211,53.95,14.198,53.919,14.214,53.871,14.172,53.874,14.048,53.863,13.926,53.879,13.902,53.939,13.922,53.997,13.872,54.036,13.827,54.06,13.82,54.093,13.828,54.127,14.039,54.035,14.211,53.95]],[[11.283,54.418,11.129,54.416,11.071,54.456,11.012,54.466,11.043,54.515,11.085,54.533,11.234,54.501,11.28,54.438,11.283,54.418]],[[8.308,54.787,8.285,54.767,8.296,54.908,8.405,55.059,8.451,55.055,8.404,55.015,8.39,54.986,8.371,54.929,8.38,54.9,8.63,54.892,8.601,54.865,8.347,54.848,8.308,54.787]],[[8.588,54.713,8.549,54.688,8.454,54.691,8.4,54.714,8.418,54.739,8.468,54.757,8.51,54.76,8.573,54.749,8.588,54.713]]]},{"n":"Georgia","c":"GE","r":5,"ml":4,"lx":43.74,"ly":41.87,"b":[39.978,41.07,46.673,43.57],"p":[[[43.439,41.107,43.442,41.126,43.433,41.156,43.402,41.177,43.359,41.19,43.279,41.185,43.205,41.199,43.153,41.236,43.141,41.265,43.171,41.288,43.149,41.307,43.057,41.353,42.907,41.467,42.822,41.492,42.788,41.564,42.754,41.579,42.682,41.586,42.607,41.579,42.59,41.571,42.567,41.559,42.508,41.47,42.466,41.44,42.364,41.454,42.28,41.475,42.211,41.487,42.078,41.494,41.926,41.496,41.824,41.432,41.779,41.441,41.702,41.472,41.577,41.497,41.51,41.517,41.702,41.705,41.759,41.817,41.761,41.885,41.763,41.97,41.663,42.147,41.578,42.398,41.489,42.659,41.419,42.738,41.129,42.828,41.062,42.931,40.837,43.063,40.524,43.121,40.462,43.146,40.191,43.312,39.978,43.42,40.024,43.485,40.085,43.553,40.15,43.57,40.342,43.543,40.519,43.512,40.648,43.534,40.802,43.48,40.942,43.418,41.083,43.374,41.358,43.333,41.461,43.276,41.581,43.219,42.05,43.19,42.088,43.199,42.122,43.207,42.28,43.228,42.419,43.224,42.566,43.155,42.66,43.159,42.761,43.17,42.89,43.133,42.992,43.092,43,43.05,43.089,42.989,43.348,42.897,43.558,42.844,43.623,42.808,43.783,42.747,43.799,42.728,43.795,42.703,43.75,42.658,43.738,42.617,43.76,42.594,43.826,42.572,43.957,42.567,44.005,42.596,44.103,42.616,44.2,42.654,44.329,42.704,44.506,42.749,44.576,42.748,44.644,42.735,44.692,42.71,44.771,42.617,44.85,42.747,44.871,42.756,44.943,42.73,45.072,42.694,45.16,42.675,45.208,42.648,45.344,42.53,45.563,42.536,45.656,42.518,45.705,42.498,45.728,42.475,45.688,42.357,45.634,42.235,45.639,42.205,45.727,42.159,45.846,42.11,45.91,42.071,45.954,42.035,46.048,42.009,46.16,41.992,46.213,41.99,46.268,41.96,46.412,41.905,46.43,41.891,46.405,41.855,46.348,41.79,46.303,41.757,46.252,41.752,46.202,41.737,46.184,41.702,46.182,41.657,46.191,41.625,46.204,41.613,46.255,41.602,46.305,41.508,46.385,41.46,46.509,41.406,46.619,41.344,46.673,41.287,46.662,41.246,46.626,41.16,46.534,41.089,46.458,41.07,46.431,41.077,46.381,41.099,46.28,41.154,46.171,41.198,46.087,41.184,46.031,41.167,45.922,41.187,45.793,41.224,45.725,41.262,45.696,41.289,45.716,41.338,45.422,41.425,45.281,41.45,45.217,41.423,45.001,41.291,44.976,41.277,44.811,41.259,44.811,41.249,44.849,41.22,44.841,41.211,44.565,41.208,44.473,41.191,44.227,41.213,44.146,41.203,44.077,41.183,43.909,41.159,43.793,41.131,43.645,41.117,43.492,41.116,43.439,41.107]]]},{"n":"The Gambia","c":"GM","r":6,"ml":5,"lx":-15,"ly":13.64,"b":[-16.825,13.064,-13.827,13.812],"p":[[[-16.562,13.587,-16.309,13.597,-16.002,13.593,-15.667,13.588,-15.51,13.586,-15.427,13.727,-15.27,13.789,-15.108,13.812,-15.024,13.806,-14.936,13.785,-14.766,13.669,-14.66,13.643,-14.571,13.616,-14.507,13.56,-14.405,13.504,-14.326,13.489,-14.278,13.497,-14.199,13.519,-14.147,13.536,-13.977,13.543,-13.853,13.479,-13.827,13.408,-13.848,13.335,-14.015,13.296,-14.247,13.236,-14.439,13.269,-14.672,13.352,-14.808,13.411,-14.865,13.435,-14.95,13.473,-15.025,13.513,-15.096,13.54,-15.151,13.556,-15.192,13.535,-15.212,13.485,-15.245,13.429,-15.286,13.396,-15.482,13.376,-15.657,13.356,-15.752,13.338,-15.814,13.325,-15.834,13.156,-16.033,13.158,-16.228,13.16,-16.431,13.157,-16.649,13.154,-16.705,13.12,-16.763,13.064,-16.769,13.148,-16.825,13.341,-16.75,13.425,-16.669,13.475,-16.615,13.435,-16.598,13.357,-16.556,13.303,-16.413,13.27,-16.272,13.294,-16.185,13.283,-16.188,13.326,-16.158,13.384,-15.986,13.409,-15.804,13.425,-15.618,13.46,-15.471,13.459,-15.427,13.468,-15.438,13.483,-15.57,13.5,-15.85,13.46,-16.135,13.448,-16.352,13.343,-16.441,13.353,-16.53,13.458,-16.562,13.587]]]},{"n":"Gabon","c":"GA","r":4,"ml":3,"lx":11.84,"ly":-0.44,"b":[8.703,-3.916,14.481,2.302],"p":[[[13.294,2.162,13.289,2.092,13.209,1.92,13.172,1.789,13.163,1.648,13.185,1.535,13.223,1.455,13.247,1.367,13.228,1.305,13.19,1.279,13.216,1.248,13.274,1.241,13.372,1.268,13.523,1.315,13.721,1.382,13.851,1.419,14.066,1.396,14.181,1.37,14.24,1.323,14.303,1.121,14.334,1.09,14.386,1.004,14.43,0.901,14.439,0.849,14.434,0.811,14.391,0.756,14.342,0.674,14.324,0.624,14.283,0.587,14.231,0.551,14.088,0.537,14.066,0.515,14.025,0.428,13.95,0.354,13.915,0.284,13.885,0.191,13.891,0.075,13.875,-0.091,13.86,-0.203,13.898,-0.243,14.069,-0.27,14.103,-0.292,14.148,-0.362,14.207,-0.427,14.364,-0.469,14.425,-0.519,14.474,-0.573,14.481,-0.618,14.445,-0.799,14.411,-0.972,14.424,-1.104,14.437,-1.23,14.456,-1.413,14.447,-1.525,14.403,-1.593,14.403,-1.647,14.423,-1.712,14.384,-1.89,14.359,-1.92,14.288,-1.954,14.251,-2.001,14.24,-2.077,14.202,-2.18,14.163,-2.218,14.163,-2.266,14.2,-2.301,14.2,-2.354,14.13,-2.418,14.087,-2.467,13.994,-2.491,13.887,-2.465,13.862,-2.43,13.888,-2.375,13.879,-2.33,13.842,-2.284,13.784,-2.164,13.734,-2.138,13.706,-2.187,13.619,-2.279,13.465,-2.395,13.357,-2.405,13.159,-2.369,12.992,-2.313,12.914,-2.176,12.864,-2.063,12.794,-1.932,12.714,-1.869,12.628,-1.83,12.59,-1.827,12.469,-1.9,12.432,-1.929,12.432,-1.99,12.444,-2.048,12.463,-2.075,12.479,-2.112,12.476,-2.169,12.454,-2.246,12.446,-2.33,12.064,-2.413,11.998,-2.383,11.95,-2.345,11.892,-2.351,11.727,-2.395,11.666,-2.365,11.605,-2.343,11.578,-2.361,11.575,-2.397,11.603,-2.595,11.595,-2.671,11.557,-2.77,11.538,-2.837,11.639,-2.855,11.676,-2.887,11.711,-2.937,11.76,-2.983,11.763,-3.011,11.708,-3.063,11.689,-3.127,11.715,-3.177,11.784,-3.229,11.885,-3.283,11.934,-3.319,11.929,-3.351,11.883,-3.42,11.865,-3.479,11.833,-3.531,11.839,-3.58,11.885,-3.625,11.88,-3.666,11.849,-3.697,11.786,-3.69,11.733,-3.695,11.686,-3.682,11.537,-3.525,11.504,-3.52,11.288,-3.641,11.234,-3.691,11.19,-3.762,11.13,-3.916,11.032,-3.826,10.947,-3.662,10.849,-3.561,10.641,-3.398,10.585,-3.278,10.348,-3.013,10.006,-2.748,9.759,-2.519,9.722,-2.468,9.764,-2.474,10.002,-2.588,10.034,-2.576,10.062,-2.55,9.959,-2.49,9.861,-2.443,9.769,-2.413,9.676,-2.416,9.625,-2.367,9.591,-2.293,9.574,-2.23,9.533,-2.164,9.402,-2.028,9.371,-1.975,9.299,-1.903,9.342,-1.894,9.483,-1.962,9.495,-1.935,9.483,-1.895,9.342,-1.829,9.266,-1.825,9.248,-1.779,9.258,-1.726,9.158,-1.528,9.053,-1.379,9.036,-1.309,9.319,-1.632,9.357,-1.638,9.406,-1.635,9.523,-1.598,9.501,-1.555,9.448,-1.509,9.397,-1.53,9.331,-1.535,9.296,-1.515,9.28,-1.482,9.347,-1.325,9.318,-1.333,9.297,-1.361,9.26,-1.374,9.204,-1.382,9.065,-1.298,8.942,-1.071,8.909,-1.025,8.877,-0.946,8.844,-0.914,8.703,-0.591,8.757,-0.615,8.821,-0.708,8.946,-0.689,8.995,-0.635,9.038,-0.637,9.082,-0.624,9.137,-0.573,9.297,-0.351,9.339,-0.058,9.325,0.116,9.302,0.289,9.355,0.344,9.376,0.307,9.386,0.246,9.411,0.2,9.468,0.16,9.574,0.149,9.738,0.085,9.797,0.044,9.813,0.126,10.001,0.195,9.944,0.22,9.777,0.192,9.546,0.296,9.47,0.362,9.399,0.487,9.325,0.552,9.33,0.611,9.495,0.665,9.539,0.659,9.557,0.594,9.601,0.568,9.618,0.577,9.625,0.632,9.626,0.779,9.575,0.991,9.591,1.032,9.636,1.047,9.676,1.075,9.705,1.08,9.761,1.075,9.789,1.026,9.804,0.999,9.86,0.986,9.907,0.96,9.947,0.967,9.98,0.998,10.029,1.004,10.179,1.004,10.315,1.003,10.587,1.002,10.859,1.001,11.131,1,11.335,1,11.335,1.121,11.334,1.308,11.332,1.528,11.331,1.74,11.33,1.936,11.329,2.167,11.34,2.234,11.353,2.261,11.348,2.3,11.559,2.302,11.94,2.285,12.106,2.288,12.153,2.284,12.361,2.296,12.53,2.281,12.601,2.265,12.666,2.257,12.867,2.247,13.131,2.259,13.22,2.256,13.27,2.224,13.294,2.162]]]},{"n":"France","c":"FR","r":2,"ml":1.7,"lx":2.55,"ly":46.7,"b":[-61.794,-21.369,55.839,51.097],"p":[[[9.48,42.805,9.454,42.659,9.473,42.616,9.509,42.586,9.526,42.553,9.556,42.161,9.551,42.13,9.428,41.972,9.401,41.926,9.395,41.731,9.374,41.679,9.331,41.627,9.253,41.46,9.186,41.385,9.003,41.477,8.895,41.516,8.842,41.559,8.808,41.588,8.83,41.628,8.879,41.669,8.887,41.701,8.771,41.737,8.718,41.761,8.719,41.804,8.759,41.87,8.74,41.925,8.674,41.922,8.622,41.931,8.615,41.959,8.653,41.996,8.703,42.043,8.701,42.096,8.642,42.118,8.588,42.161,8.566,42.219,8.608,42.258,8.675,42.284,8.626,42.343,8.592,42.345,8.566,42.358,8.588,42.385,8.64,42.427,8.713,42.55,8.815,42.608,8.995,42.645,9.044,42.662,9.088,42.705,9.138,42.733,9.198,42.729,9.254,42.712,9.288,42.695,9.313,42.713,9.338,42.767,9.323,42.814,9.331,42.944,9.363,43.017,9.415,43.021,9.463,42.981,9.461,42.945,9.479,42.86,9.48,42.805]],[[7.616,47.593,7.495,47.547,7.467,47.508,7.42,47.455,7.343,47.433,7.266,47.426,7.203,47.433,7.167,47.454,7.169,47.473,7.136,47.49,7.053,47.489,6.968,47.453,6.9,47.394,6.921,47.361,6.984,47.353,7.001,47.339,7.001,47.323,6.979,47.302,6.952,47.267,6.821,47.163,6.688,47.058,6.667,47.027,6.625,47.004,6.456,46.948,6.439,46.926,6.429,46.832,6.41,46.755,6.285,46.683,6.161,46.611,6.13,46.567,6.107,46.516,6.068,46.459,6.06,46.428,6.123,46.379,6.116,46.338,6.096,46.279,6.036,46.238,5.97,46.215,5.971,46.151,6.007,46.142,6.087,46.147,6.199,46.193,6.273,46.252,6.23,46.308,6.224,46.319,6.235,46.333,6.322,46.394,6.429,46.431,6.578,46.437,6.758,46.416,6.776,46.407,6.767,46.369,6.784,46.314,6.817,46.275,6.772,46.165,6.806,46.131,6.858,46.089,6.897,46.052,6.954,46.017,7.004,45.959,7.021,45.926,6.941,45.868,6.804,45.815,6.789,45.78,6.791,45.741,6.806,45.71,6.881,45.67,6.962,45.581,7.014,45.5,7.126,45.424,7.153,45.401,7.146,45.382,7.117,45.349,7.078,45.24,7.032,45.223,6.981,45.216,6.842,45.136,6.78,45.145,6.692,45.144,6.628,45.118,6.635,45.068,6.691,45.023,6.725,44.973,6.738,44.921,6.801,44.883,6.889,44.86,6.94,44.859,6.973,44.845,6.993,44.827,7.031,44.717,7.008,44.689,6.96,44.677,6.932,44.632,6.875,44.565,6.843,44.511,6.879,44.463,6.894,44.428,6.875,44.392,6.9,44.336,6.967,44.28,7.149,44.202,7.319,44.138,7.371,44.127,7.599,44.168,7.637,44.165,7.665,44.116,7.677,44.083,7.651,44.034,7.59,43.965,7.523,43.911,7.482,43.865,7.491,43.823,7.493,43.767,7.439,43.75,7.437,43.761,7.414,43.771,7.395,43.765,7.38,43.753,7.378,43.732,7.262,43.696,7.181,43.659,6.865,43.438,6.717,43.374,6.687,43.335,6.657,43.262,6.57,43.199,6.494,43.169,6.305,43.139,6.116,43.072,6.031,43.101,5.809,43.098,5.672,43.178,5.407,43.229,5.32,43.345,5.2,43.352,5.12,43.349,5.073,43.367,5.061,43.406,5.06,43.445,4.976,43.427,4.912,43.427,4.874,43.412,4.844,43.394,4.808,43.405,4.787,43.401,4.789,43.379,4.712,43.373,4.629,43.387,4.41,43.447,4.376,43.456,4.224,43.48,4.163,43.504,4.113,43.563,4.075,43.582,4.053,43.593,3.911,43.563,3.862,43.516,3.785,43.462,3.259,43.193,3.163,43.081,3.052,42.915,3.043,42.838,3.091,42.591,3.198,42.461,3.211,42.431,3.152,42.431,3.053,42.447,2.97,42.467,2.891,42.456,2.816,42.429,2.749,42.413,2.702,42.408,2.67,42.393,2.655,42.362,2.652,42.34,2.568,42.346,2.374,42.39,2.2,42.421,2.098,42.386,2.033,42.354,1.987,42.358,1.951,42.393,1.928,42.426,1.86,42.457,1.706,42.503,1.714,42.526,1.74,42.557,1.739,42.576,1.71,42.604,1.568,42.635,1.501,42.643,1.459,42.622,1.428,42.596,1.349,42.691,1.293,42.71,1.208,42.713,1.111,42.742,1.01,42.779,0.764,42.838,0.697,42.845,0.67,42.836,0.652,42.8,0.642,42.701,0.632,42.69,0.518,42.686,0.377,42.7,0.313,42.693,0.255,42.693,0.201,42.719,-0.041,42.689,-0.081,42.704,-0.14,42.749,-0.205,42.785,-0.256,42.804,-0.299,42.825,-0.339,42.829,-0.398,42.808,-0.481,42.799,-0.55,42.802,-0.586,42.799,-0.74,42.91,-0.763,42.94,-0.839,42.948,-0.934,42.95,-1.175,43.021,-1.285,43.06,-1.302,43.082,-1.3,43.101,-1.319,43.097,-1.353,43.064,-1.371,43.038,-1.394,43.033,-1.429,43.037,-1.461,43.052,-1.48,43.071,-1.459,43.105,-1.423,43.149,-1.407,43.197,-1.411,43.24,-1.472,43.268,-1.561,43.279,-1.627,43.282,-1.713,43.307,-1.753,43.325,-1.793,43.373,-1.794,43.407,-1.631,43.438,-1.485,43.564,-1.346,44.02,-1.246,44.56,-1.171,44.662,-1.077,44.69,-1.153,44.764,-1.2,44.726,-1.22,44.687,-1.245,44.667,-1.189,45.161,-1.149,45.343,-1.081,45.532,-0.942,45.457,-0.826,45.381,-0.767,45.314,-0.691,45.093,-0.634,45.047,-0.548,45.001,-0.582,45.051,-0.641,45.09,-0.733,45.385,-0.791,45.468,-0.881,45.538,-1.17,45.686,-1.196,45.714,-1.21,45.771,-1.114,45.769,-1.032,45.741,-1.042,45.773,-1.066,45.806,-1.104,45.925,-1.136,46.205,-1.132,46.253,-1.146,46.311,-1.239,46.325,-1.313,46.327,-1.392,46.35,-1.787,46.515,-1.921,46.685,-2.059,46.81,-2.092,46.865,-2.09,46.921,-2.019,47.038,-2.082,47.112,-2.144,47.126,-2.197,47.163,-2.149,47.224,-2.108,47.263,-2.028,47.274,-1.922,47.261,-1.821,47.225,-1.743,47.216,-1.975,47.311,-2.353,47.279,-2.434,47.291,-2.503,47.312,-2.53,47.382,-2.476,47.413,-2.428,47.471,-2.483,47.512,-2.554,47.527,-2.666,47.526,-2.77,47.514,-2.797,47.537,-2.733,47.602,-2.787,47.626,-2.859,47.614,-2.964,47.601,-3.064,47.621,-3.159,47.695,-3.222,47.694,-3.265,47.685,-3.329,47.713,-3.396,47.72,-3.444,47.711,-3.508,47.753,-3.901,47.838,-4.071,47.848,-4.226,47.81,-4.312,47.823,-4.375,47.877,-4.428,47.969,-4.679,48.04,-4.629,48.086,-4.512,48.097,-4.378,48.129,-4.329,48.17,-4.435,48.218,-4.512,48.23,-4.544,48.247,-4.577,48.29,-4.531,48.31,-4.498,48.299,-4.403,48.293,-4.241,48.304,-4.302,48.347,-4.364,48.357,-4.393,48.368,-4.525,48.372,-4.585,48.358,-4.719,48.363,-4.749,48.41,-4.762,48.45,-4.721,48.54,-4.531,48.62,-4.059,48.708,-3.856,48.695,-3.715,48.71,-3.546,48.766,-3.471,48.813,-3.231,48.841,-3.003,48.791,-2.793,48.601,-2.692,48.537,-2.446,48.648,-2.079,48.645,-2.004,48.582,-1.973,48.635,-1.906,48.697,-1.852,48.669,-1.825,48.631,-1.438,48.641,-1.376,48.653,-1.48,48.698,-1.565,48.806,-1.583,49.202,-1.69,49.313,-1.813,49.49,-1.87,49.595,-1.875,49.631,-1.856,49.684,-1.705,49.681,-1.588,49.668,-1.366,49.707,-1.259,49.68,-1.265,49.598,-1.232,49.495,-1.195,49.445,-1.139,49.388,-0.959,49.393,-0.766,49.36,-0.521,49.355,-0.163,49.297,-0.011,49.33,0.136,49.402,0.417,49.448,0.439,49.473,0.278,49.463,0.129,49.508,0.109,49.558,0.127,49.602,0.187,49.703,0.616,49.863,0.924,49.91,1.246,49.998,1.407,50.089,1.514,50.205,1.548,50.231,1.593,50.252,1.552,50.294,1.579,50.739,1.61,50.819,1.672,50.885,1.768,50.936,1.913,50.991,2.446,51.067,2.525,51.097,2.536,51.05,2.575,50.989,2.601,50.955,2.579,50.912,2.597,50.876,2.669,50.811,2.759,50.751,2.84,50.712,2.862,50.716,2.922,50.727,3.023,50.767,3.107,50.779,3.155,50.749,3.182,50.732,3.235,50.663,3.25,50.591,3.273,50.532,3.316,50.507,3.477,50.499,3.595,50.477,3.627,50.457,3.667,50.325,3.689,50.306,3.719,50.322,3.748,50.344,3.789,50.347,3.858,50.339,3.95,50.336,4.044,50.321,4.175,50.246,4.17,50.222,4.144,50.178,4.135,50.144,4.158,50.13,4.192,50.094,4.184,50.053,4.15,50.024,4.137,50,4.137,49.984,4.149,49.972,4.176,49.96,4.369,49.945,4.545,49.96,4.656,50.002,4.675,50.047,4.707,50.097,4.773,50.139,4.819,50.153,4.861,50.136,4.79,49.96,4.842,49.915,4.849,49.847,4.868,49.788,4.931,49.789,5.007,49.778,5.061,49.757,5.124,49.721,5.215,49.689,5.279,49.678,5.302,49.651,5.354,49.62,5.435,49.554,5.507,49.511,5.542,49.511,5.61,49.528,5.71,49.539,5.79,49.538,5.823,49.505,5.901,49.49,5.929,49.478,5.959,49.455,6.011,49.445,6.074,49.455,6.12,49.485,6.181,49.499,6.242,49.494,6.277,49.478,6.344,49.453,6.382,49.458,6.458,49.443,6.534,49.395,6.566,49.346,6.575,49.32,6.608,49.291,6.735,49.161,6.776,49.154,6.821,49.174,6.85,49.202,6.891,49.208,6.958,49.195,7.001,49.18,7.022,49.123,7.037,49.113,7.066,49.125,7.117,49.128,7.2,49.114,7.313,49.13,7.404,49.153,7.451,49.152,7.525,49.086,7.611,49.062,7.799,49.042,8.001,49.011,8.081,48.986,8.135,48.974,8.14,48.886,8.124,48.873,7.923,48.699,7.838,48.636,7.795,48.547,7.765,48.41,7.706,48.28,7.617,48.157,7.584,48.064,7.608,48.003,7.593,47.906,7.539,47.774,7.529,47.674,7.565,47.607,7.616,47.593]],[[-1.178,45.904,-1.214,45.817,-1.28,45.897,-1.369,45.968,-1.389,46.033,-1.389,46.05,-1.285,46.003,-1.178,45.904]],[[45.18,-12.977,45.118,-12.985,45.088,-12.958,45.069,-12.896,45.088,-12.835,45.094,-12.786,45.043,-12.701,45.092,-12.653,45.135,-12.709,45.159,-12.713,45.223,-12.752,45.204,-12.824,45.209,-12.848,45.179,-12.92,45.18,-12.977]],[[55.797,-21.339,55.656,-21.369,55.558,-21.358,55.363,-21.274,55.31,-21.217,55.233,-21.058,55.25,-21.002,55.311,-20.904,55.45,-20.865,55.596,-20.88,55.662,-20.906,55.739,-21.021,55.839,-21.139,55.822,-21.278,55.797,-21.339]],[[-60.826,14.494,-60.837,14.437,-60.862,14.426,-60.899,14.474,-61.064,14.467,-61.089,14.51,-61.09,14.53,-61.011,14.602,-61.104,14.621,-61.141,14.652,-61.22,14.804,-61.213,14.849,-61.181,14.872,-61.127,14.875,-61.027,14.826,-60.953,14.756,-60.927,14.755,-60.919,14.735,-60.934,14.686,-60.889,14.645,-60.87,14.614,-60.826,14.494]],[[-61.327,16.23,-61.445,16.219,-61.522,16.228,-61.54,16.3,-61.501,16.36,-61.529,16.434,-61.511,16.478,-61.471,16.507,-61.406,16.468,-61.396,16.413,-61.355,16.363,-61.173,16.256,-61.327,16.23]],[[-61.59,16.007,-61.67,15.962,-61.71,15.976,-61.759,16.062,-61.794,16.301,-61.767,16.34,-61.748,16.355,-61.642,16.326,-61.597,16.292,-61.552,16.271,-61.575,16.227,-61.564,16.048,-61.59,16.007]],[[-61.23,15.89,-61.286,15.886,-61.311,15.895,-61.318,15.955,-61.275,15.996,-61.25,16.006,-61.212,15.96,-61.203,15.921,-61.23,15.89]],[[-54.616,2.327,-54.605,2.336,-54.568,2.343,-54.536,2.343,-54.486,2.416,-54.402,2.462,-54.257,2.714,-54.196,2.818,-54.188,2.875,-54.171,2.994,-54.203,3.138,-54.188,3.179,-54.063,3.353,-54.01,3.449,-54.006,3.531,-53.99,3.59,-54.006,3.62,-54.034,3.629,-54.082,3.706,-54.113,3.769,-54.197,3.834,-54.256,3.901,-54.351,4.054,-54.342,4.14,-54.369,4.171,-54.398,4.202,-54.396,4.241,-54.416,4.338,-54.441,4.428,-54.45,4.485,-54.426,4.583,-54.44,4.692,-54.471,4.749,-54.48,4.837,-54.473,4.915,-54.447,4.959,-54.452,5.013,-54.332,5.187,-54.24,5.288,-54.156,5.359,-54.085,5.412,-53.99,5.676,-53.92,5.769,-53.847,5.782,-53.454,5.563,-53.27,5.543,-52.899,5.425,-52.765,5.273,-52.454,5.021,-52.291,4.942,-52.289,4.876,-52.325,4.771,-52.22,4.863,-52.058,4.717,-52.012,4.646,-51.962,4.514,-51.979,4.43,-52.002,4.386,-52.003,4.352,-51.955,4.399,-51.928,4.436,-51.92,4.524,-51.88,4.634,-51.828,4.636,-51.786,4.571,-51.699,4.287,-51.666,4.229,-51.653,4.139,-51.658,4.098,-51.653,4.061,-51.683,4.04,-51.767,3.993,-51.805,3.93,-51.827,3.87,-51.879,3.829,-51.929,3.777,-51.944,3.735,-51.991,3.702,-52,3.647,-52.116,3.452,-52.163,3.365,-52.229,3.272,-52.271,3.237,-52.328,3.182,-52.357,3.118,-52.357,3.052,-52.396,2.972,-52.418,2.904,-52.456,2.864,-52.555,2.648,-52.559,2.573,-52.583,2.529,-52.653,2.426,-52.701,2.364,-52.783,2.317,-52.87,2.267,-52.903,2.212,-52.965,2.184,-53.01,2.182,-53.082,2.202,-53.18,2.211,-53.23,2.205,-53.252,2.232,-53.285,2.295,-53.334,2.34,-53.366,2.324,-53.432,2.279,-53.509,2.253,-53.564,2.262,-53.684,2.293,-53.735,2.309,-53.75,2.335,-53.768,2.355,-53.794,2.346,-53.83,2.313,-53.877,2.278,-53.946,2.233,-54.09,2.15,-54.13,2.121,-54.167,2.137,-54.228,2.153,-54.293,2.154,-54.433,2.208,-54.515,2.245,-54.55,2.293,-54.592,2.314,-54.616,2.327]]]},{"n":"Saint Pierre and Miquelon","c":"PM","r":4,"ml":5,"lx":-56.33,"ly":47.04,"b":[-56.387,46.753,-56.137,47.099],"p":[[[-56.151,46.762,-56.172,46.753,-56.243,46.767,-56.209,46.798,-56.185,46.807,-56.153,46.811,-56.137,46.802,-56.139,46.779,-56.151,46.762]],[[-56.267,46.838,-56.354,46.795,-56.385,46.819,-56.377,46.848,-56.333,46.916,-56.334,46.936,-56.387,47.068,-56.378,47.09,-56.365,47.099,-56.287,47.071,-56.278,47.035,-56.315,46.954,-56.29,46.9,-56.255,46.861,-56.267,46.838]]]},{"n":"Wallis and Futuna","c":"WF","r":4,"ml":4.7,"lx":-178.14,"ly":-14.29,"b":[-178.194,-14.325,-176.128,-13.222],"p":[[[-176.161,-13.333,-176.177,-13.341,-176.195,-13.302,-176.171,-13.243,-176.148,-13.222,-176.128,-13.268,-176.161,-13.333]],[[-178.047,-14.318,-178.103,-14.325,-178.159,-14.312,-178.194,-14.255,-178.178,-14.232,-178.142,-14.243,-178.105,-14.284,-178.044,-14.303,-178.047,-14.318]]]},{"n":"Saint Martin","c":"MF","r":6,"ml":5,"lx":-63.05,"ly":18.08,"b":[-63.123,18.069,-63.009,18.115],"p":[[[-63.011,18.069,-63.123,18.069,-63.115,18.091,-63.063,18.115,-63.025,18.113,-63.009,18.104,-63.011,18.069]]]},{"n":"Saint Barthélemy","c":"BL","r":6,"ml":5.7,"lx":-62.83,"ly":17.9,"b":[-62.875,17.875,-62.8,17.922],"p":[[[-62.832,17.876,-62.847,17.875,-62.859,17.884,-62.869,17.899,-62.875,17.914,-62.874,17.922,-62.865,17.918,-62.8,17.909,-62.807,17.898,-62.818,17.885,-62.832,17.876]]]},{"n":"French Polynesia","c":"PF","r":4,"ml":3.5,"lx":-149.46,"ly":-17.63,"b":[-151.512,-20.876,-136.294,-8.782],"p":[[[-151.467,-16.658,-151.485,-16.665,-151.504,-16.647,-151.512,-16.619,-151.506,-16.574,-151.457,-16.604,-151.438,-16.623,-151.467,-16.658]],[[-149.814,-17.545,-149.845,-17.571,-149.887,-17.553,-149.905,-17.528,-149.912,-17.501,-149.902,-17.47,-149.809,-17.474,-149.782,-17.488,-149.814,-17.545]],[[-151.41,-16.878,-151.449,-16.879,-151.485,-16.864,-151.476,-16.761,-151.467,-16.74,-151.411,-16.774,-151.365,-16.864,-151.41,-16.878]],[[-149.322,-17.69,-149.178,-17.737,-149.151,-17.812,-149.182,-17.862,-149.254,-17.85,-149.29,-17.822,-149.341,-17.732,-149.482,-17.753,-149.579,-17.735,-149.633,-17.618,-149.635,-17.564,-149.611,-17.532,-149.508,-17.496,-149.379,-17.522,-149.33,-17.589,-149.322,-17.69]],[[-139.024,-9.695,-138.874,-9.747,-138.827,-9.742,-138.875,-9.793,-139.024,-9.821,-139.074,-9.846,-139.134,-9.829,-139.166,-9.77,-139.024,-9.695]],[[-139.06,-9.931,-139.134,-10.01,-139.134,-9.926,-139.107,-9.915,-139.083,-9.915,-139.06,-9.931]],[[-138.651,-10.515,-138.688,-10.532,-138.69,-10.426,-138.643,-10.446,-138.624,-10.463,-138.632,-10.492,-138.651,-10.515]],[[-140.073,-8.91,-140.171,-8.934,-140.217,-8.93,-140.253,-8.848,-140.24,-8.798,-140.224,-8.782,-140.058,-8.801,-140.044,-8.838,-140.046,-8.874,-140.073,-8.91]],[[-140.076,-9.426,-140.097,-9.444,-140.138,-9.384,-140.144,-9.359,-140.071,-9.328,-140.031,-9.345,-140.076,-9.426]],[[-140.809,-17.857,-140.804,-17.876,-140.841,-17.873,-140.852,-17.867,-140.851,-17.831,-140.824,-17.788,-140.804,-17.752,-140.761,-17.718,-140.686,-17.684,-140.65,-17.67,-140.638,-17.678,-140.652,-17.683,-140.776,-17.754,-140.815,-17.804,-140.833,-17.838,-140.829,-17.849,-140.809,-17.857]],[[-139.556,-8.94,-139.621,-8.948,-139.632,-8.899,-139.612,-8.872,-139.584,-8.86,-139.535,-8.875,-139.508,-8.897,-139.51,-8.916,-139.556,-8.94]],[[-140.685,-18.38,-140.672,-18.416,-140.696,-18.399,-140.773,-18.364,-140.782,-18.334,-140.685,-18.38]],[[-140.83,-18.189,-140.823,-18.217,-140.86,-18.199,-140.895,-18.148,-140.959,-18.085,-140.974,-18.059,-140.925,-18.084,-140.893,-18.121,-140.83,-18.189]],[[-136.294,-18.544,-136.314,-18.566,-136.316,-18.545,-136.344,-18.535,-136.383,-18.514,-136.436,-18.489,-136.464,-18.485,-136.479,-18.471,-136.459,-18.463,-136.426,-18.474,-136.38,-18.497,-136.328,-18.519,-136.294,-18.544]],[[-136.972,-18.342,-136.971,-18.361,-137.068,-18.265,-137.03,-18.273,-136.972,-18.342]],[[-138.506,-20.857,-138.535,-20.876,-138.524,-20.851,-138.546,-20.795,-138.568,-20.787,-138.546,-20.771,-138.515,-20.813,-138.506,-20.857]],[[-142.512,-16.096,-142.53,-16.107,-142.507,-16.028,-142.481,-16.018,-142.512,-16.096]],[[-143.441,-16.62,-143.386,-16.669,-143.459,-16.635,-143.551,-16.621,-143.516,-16.612,-143.465,-16.614,-143.441,-16.62]],[[-143.571,-16.635,-143.611,-16.64,-143.707,-16.581,-143.67,-16.581,-143.615,-16.618,-143.571,-16.635]],[[-145.051,-15.856,-145.058,-15.901,-145.076,-15.858,-145.138,-15.788,-145.161,-15.757,-145.134,-15.762,-145.051,-15.856]],[[-145.487,-16.33,-145.482,-16.347,-145.503,-16.346,-145.54,-16.295,-145.553,-16.251,-145.577,-16.201,-145.609,-16.165,-145.613,-16.132,-145.614,-16.079,-145.577,-16.16,-145.542,-16.225,-145.517,-16.278,-145.487,-16.33]]]},{"n":"New Caledonia","c":"NC","r":3,"ml":4.6,"lx":165.08,"ly":-21.06,"b":[159.928,-22.661,168.139,-19.115],"p":[[[164.202,-20.246,164.315,-20.309,164.436,-20.282,164.588,-20.381,164.976,-20.681,165.112,-20.745,165.192,-20.769,165.252,-20.818,165.307,-20.887,165.381,-20.936,165.413,-20.981,165.421,-21.043,165.447,-21.081,165.582,-21.18,165.663,-21.267,165.775,-21.312,165.823,-21.364,165.885,-21.389,165.95,-21.442,166.058,-21.484,166.303,-21.637,166.493,-21.783,166.588,-21.873,166.69,-21.953,166.82,-22.017,166.942,-22.09,167.004,-22.262,166.97,-22.323,166.9,-22.353,166.835,-22.355,166.774,-22.376,166.571,-22.266,166.522,-22.249,166.468,-22.256,166.438,-22.232,166.416,-22.196,166.292,-22.155,166.177,-22.089,166.143,-22.044,166.124,-21.989,166.096,-21.957,165.933,-21.908,165.823,-21.854,165.744,-21.777,165.62,-21.724,165.428,-21.615,165.329,-21.58,165.242,-21.525,165.01,-21.327,164.927,-21.29,164.855,-21.202,164.656,-20.992,164.559,-20.906,164.455,-20.829,164.375,-20.739,164.313,-20.633,164.17,-20.48,164.152,-20.415,164.158,-20.348,164.124,-20.305,164.065,-20.279,164.037,-20.234,164.041,-20.173,164.06,-20.142,164.202,-20.246]],[[167.544,-22.623,167.513,-22.661,167.473,-22.653,167.444,-22.639,167.422,-22.619,167.443,-22.541,167.529,-22.579,167.544,-22.623]],[[159.952,-19.312,159.936,-19.333,159.928,-19.174,159.96,-19.115,159.975,-19.238,159.952,-19.312]],[[168.011,-21.43,168.058,-21.448,168.139,-21.445,168.121,-21.616,168.006,-21.643,167.967,-21.642,167.941,-21.606,167.876,-21.582,167.879,-21.524,167.815,-21.393,167.926,-21.373,167.988,-21.338,167.985,-21.37,167.995,-21.407,168.011,-21.43]],[[166.547,-20.699,166.494,-20.709,166.558,-20.617,166.56,-20.561,166.585,-20.45,166.583,-20.413,166.625,-20.418,166.671,-20.45,166.618,-20.478,166.6,-20.525,166.602,-20.585,166.623,-20.596,166.589,-20.662,166.547,-20.699]],[[167.401,-21.161,167.346,-21.169,167.273,-21.097,167.134,-21.061,167.073,-20.997,167.033,-20.923,167.112,-20.904,167.189,-20.804,167.136,-20.766,167.045,-20.759,167.056,-20.72,167.204,-20.674,167.269,-20.701,167.298,-20.733,167.293,-20.892,167.361,-20.942,167.431,-21.055,167.43,-21.087,167.401,-21.161]]]},{"n":"French Southern and Antarctic Lands","c":"TF","r":6,"ml":4,"lx":69.12,"ly":-49.3,"b":[51.659,-49.71,70.555,-46.327],"p":[[[69.185,-49.11,69.265,-49.115,69.314,-49.106,69.535,-48.974,69.593,-48.971,69.587,-49.072,69.644,-49.117,69.572,-49.129,69.436,-49.124,69.405,-49.182,69.542,-49.256,69.611,-49.266,69.667,-49.265,69.771,-49.248,69.854,-49.222,69.984,-49.16,70.061,-49.136,70.208,-49.135,70.285,-49.076,70.32,-49.059,70.406,-49.061,70.484,-49.084,70.531,-49.137,70.555,-49.201,70.537,-49.266,70.485,-49.328,70.39,-49.366,70.411,-49.411,70.386,-49.434,70.338,-49.435,70.298,-49.425,70.238,-49.372,70.166,-49.343,69.993,-49.345,69.916,-49.349,69.902,-49.389,69.861,-49.421,69.818,-49.438,69.76,-49.43,69.749,-49.448,69.78,-49.49,69.856,-49.544,69.986,-49.582,70.063,-49.589,70.073,-49.518,70.166,-49.509,70.248,-49.531,70.307,-49.583,70.259,-49.601,70.216,-49.629,70.207,-49.665,70.124,-49.704,70.075,-49.709,69.919,-49.689,69.826,-49.645,69.804,-49.614,69.747,-49.602,69.682,-49.642,69.613,-49.651,69.478,-49.617,69.353,-49.563,69.275,-49.543,69.153,-49.53,69.086,-49.653,68.993,-49.705,68.873,-49.71,68.815,-49.7,68.783,-49.651,68.791,-49.6,68.811,-49.55,68.848,-49.5,68.872,-49.444,68.862,-49.392,68.818,-49.354,68.841,-49.285,68.799,-49.232,68.814,-49.192,68.883,-49.165,68.854,-49.141,68.817,-49.135,68.79,-49.104,68.77,-49.066,68.797,-48.995,68.837,-48.926,68.832,-48.849,68.9,-48.776,68.959,-48.694,69.002,-48.661,69.057,-48.656,69.081,-48.679,69.093,-48.724,69.072,-48.753,69.123,-48.766,69.136,-48.861,69.104,-48.9,69.099,-48.938,69.033,-49.018,69.052,-49.082,69.185,-49.11]],[[69.282,-49.059,69.221,-49.067,69.202,-49.034,69.204,-48.991,69.17,-48.957,69.15,-48.919,69.167,-48.883,69.266,-48.879,69.369,-48.89,69.395,-48.951,69.321,-49.034,69.282,-49.059]],[[51.835,-46.44,51.762,-46.449,51.697,-46.428,51.659,-46.374,51.742,-46.327,51.784,-46.359,51.815,-46.395,51.835,-46.44]]]},{"n":"Åland","c":"AX","r":6,"ml":5,"lx":19.87,"ly":60.16,"b":[19.519,60.012,20.611,60.406],"p":[[[19.99,60.351,20.02,60.351,20.034,60.359,20.087,60.353,20.168,60.315,20.184,60.294,20.24,60.283,20.259,60.261,20.195,60.194,20.155,60.192,20.125,60.201,20.073,60.193,20.043,60.181,20.032,60.152,20.034,60.094,19.8,60.082,19.746,60.099,19.672,60.233,19.687,60.268,19.737,60.282,19.779,60.286,19.785,60.213,19.848,60.221,19.867,60.268,19.872,60.302,19.855,60.319,19.812,60.332,19.788,60.354,19.823,60.39,19.888,60.406,19.945,60.358,19.99,60.351]],[[19.662,60.187,19.667,60.165,19.629,60.17,19.6,60.163,19.58,60.135,19.537,60.145,19.519,60.185,19.551,60.244,19.629,60.246,19.662,60.187]],[[20.611,60.041,20.603,60.017,20.522,60.012,20.488,60.033,20.411,60.03,20.398,60.041,20.43,60.062,20.49,60.075,20.569,60.07,20.611,60.041]]]},{"n":"Finland","c":"FI","r":3,"ml":3,"lx":27.28,"ly":63.25,"b":[20.622,59.816,31.537,70.065],"p":[[[24.155,65.805,24.049,65.99,23.995,66.06,23.907,66.148,23.751,66.191,23.721,66.215,23.7,66.253,23.694,66.304,23.674,66.381,23.682,66.443,23.701,66.481,23.768,66.506,23.866,66.577,23.886,66.628,23.894,66.707,23.939,66.776,23.989,66.811,23.976,66.838,23.942,66.878,23.869,66.934,23.759,67.003,23.677,67.068,23.642,67.129,23.623,67.184,23.626,67.234,23.657,67.268,23.761,67.31,23.775,67.329,23.734,67.423,23.661,67.44,23.537,67.449,23.468,67.45,23.455,67.46,23.451,67.479,23.465,67.518,23.504,67.562,23.537,67.59,23.541,67.614,23.5,67.696,23.488,67.797,23.502,67.875,23.633,67.933,23.639,67.954,23.474,68.017,23.355,68.089,23.319,68.13,23.183,68.137,23.098,68.258,22.975,68.316,22.854,68.367,22.782,68.391,22.362,68.464,22.195,68.478,21.997,68.521,21.85,68.574,21.724,68.609,21.616,68.651,21.465,68.691,21.422,68.725,21.26,68.787,21.183,68.829,20.919,68.907,20.909,68.938,20.907,68.967,20.895,68.98,20.622,69.037,20.676,69.069,20.889,69.071,21.066,69.042,21.104,69.054,21.128,69.081,21.053,69.187,21.066,69.214,21.144,69.247,21.267,69.274,21.461,69.277,21.594,69.274,21.622,69.271,21.82,69.154,21.989,69.041,22.08,68.993,22.3,68.856,22.383,68.777,22.411,68.72,22.501,68.72,22.811,68.695,23.072,68.674,23.144,68.643,23.324,68.649,23.463,68.678,23.707,68.714,23.773,68.758,23.854,68.806,23.997,68.798,24.154,68.761,24.332,68.712,24.491,68.689,24.703,68.653,24.802,68.606,24.941,68.593,25.087,68.64,25.173,68.765,25.249,68.821,25.357,68.862,25.481,68.881,25.575,68.887,25.647,68.919,25.748,68.99,25.768,69.076,25.749,69.231,25.767,69.283,25.85,69.367,25.962,69.589,26.012,69.653,26.072,69.692,26.156,69.715,26.308,69.782,26.525,69.915,26.584,69.926,26.74,69.933,26.934,69.928,27.109,69.905,27.128,69.906,27.206,69.919,27.348,69.96,27.592,70.042,27.748,70.065,27.89,70.062,28.047,69.972,28.269,69.871,28.412,69.823,28.8,69.731,29.142,69.671,29.333,69.473,29.239,69.394,29.192,69.367,29.025,69.288,28.846,69.177,28.833,69.119,28.892,69.061,28.966,69.022,28.899,69.01,28.692,68.961,28.566,68.928,28.414,68.904,28.454,68.872,28.706,68.866,28.745,68.856,28.773,68.84,28.778,68.814,28.752,68.771,28.479,68.538,28.471,68.488,28.56,68.351,28.685,68.19,29.063,68.118,29.344,68.062,29.524,67.929,29.822,67.754,29.979,67.689,29.988,67.668,29.941,67.547,29.751,67.426,29.572,67.324,29.388,67.201,29.243,67.097,29.087,66.971,29.069,66.93,29.066,66.892,29.093,66.849,29.293,66.696,29.371,66.617,29.464,66.532,29.544,66.44,29.591,66.357,29.671,66.276,29.721,66.235,29.804,66.177,29.903,66.091,29.937,66.023,30.088,65.787,30.103,65.726,30.095,65.682,30.029,65.671,29.883,65.664,29.724,65.634,29.716,65.625,29.819,65.569,29.728,65.473,29.715,65.337,29.617,65.265,29.608,65.249,29.612,65.235,29.63,65.224,29.811,65.205,29.826,65.185,29.827,65.145,29.811,65.108,29.72,65.08,29.622,65.04,29.601,65.002,29.604,64.968,29.638,64.912,29.702,64.846,29.783,64.804,30.073,64.765,30.11,64.733,30.126,64.688,30.12,64.645,29.986,64.558,29.987,64.524,30.042,64.443,30.108,64.366,30.391,64.282,30.488,64.237,30.514,64.2,30.528,64.141,30.526,64.077,30.504,64.021,30.415,63.948,30.21,63.803,30.004,63.747,29.992,63.735,30.055,63.689,30.419,63.504,30.655,63.417,30.975,63.301,31.181,63.208,31.247,63.142,31.337,63.068,31.437,63.008,31.509,62.955,31.537,62.922,31.534,62.885,31.437,62.776,31.382,62.692,31.286,62.568,31.187,62.481,30.936,62.324,30.566,62.128,30.48,62.068,30.306,61.965,30.01,61.757,29.933,61.712,29.69,61.546,29.579,61.493,29.492,61.444,29.252,61.288,28.993,61.169,28.739,61.059,28.663,61.003,28.568,60.96,28.455,60.92,28.407,60.897,28.152,60.746,27.798,60.536,27.762,60.533,27.669,60.499,27.525,60.491,27.462,60.465,27.242,60.539,27.205,60.543,27.076,60.525,26.951,60.471,26.721,60.455,26.607,60.438,26.535,60.413,26.52,60.472,26.551,60.546,26.602,60.596,26.606,60.628,26.569,60.625,26.496,60.552,26.456,60.467,26.378,60.424,26.205,60.407,26.036,60.475,25.956,60.474,26.006,60.425,26.04,60.372,26.036,60.342,25.946,60.347,25.846,60.315,25.758,60.268,25.715,60.267,25.656,60.333,25.548,60.302,25.456,60.261,25.268,60.248,25.156,60.194,24.958,60.157,24.849,60.158,24.6,60.114,24.518,60.046,24.446,60.021,24.343,60.042,24.025,60.009,23.722,59.966,23.593,59.968,23.464,59.986,23.327,59.926,23.181,59.845,23.021,59.816,22.964,59.826,23.01,59.869,23.116,59.913,23.188,59.972,23.198,60.022,23.148,60.041,23.08,60.047,22.994,60.099,22.912,60.21,22.867,60.216,22.844,60.187,22.819,60.101,22.793,60.077,22.75,60.057,22.697,60.038,22.646,60.028,22.463,60.029,22.439,60.072,22.439,60.09,22.471,60.147,22.443,60.157,22.47,60.201,22.513,60.199,22.564,60.206,22.59,60.228,22.588,60.256,22.517,60.263,22.512,60.281,22.576,60.359,22.585,60.381,22.56,60.385,22.521,60.377,22.258,60.401,21.934,60.5,21.854,60.505,21.805,60.594,21.727,60.583,21.613,60.531,21.528,60.57,21.436,60.596,21.411,60.637,21.412,60.697,21.404,60.767,21.379,60.85,21.361,60.967,21.378,61.059,21.451,61.127,21.479,61.171,21.513,61.281,21.521,61.411,21.502,61.455,21.507,61.484,21.565,61.484,21.552,61.51,21.527,61.523,21.498,61.552,21.522,61.567,21.592,61.568,21.598,61.578,21.606,61.592,21.552,61.667,21.546,61.703,21.471,61.812,21.385,61.915,21.256,61.99,21.302,62.113,21.354,62.224,21.343,62.277,21.323,62.343,21.166,62.414,21.142,62.515,21.104,62.623,21.118,62.689,21.144,62.74,21.196,62.791,21.458,62.95,21.474,63.033,21.651,63.039,21.569,63.114,21.549,63.156,21.545,63.204,21.8,63.238,21.896,63.21,22.12,63.244,22.32,63.31,22.316,63.346,22.286,63.377,22.243,63.438,22.273,63.455,22.346,63.442,22.313,63.473,22.319,63.504,22.398,63.491,22.528,63.58,22.532,63.648,22.756,63.683,23.014,63.822,23.134,63.865,23.249,63.896,23.494,64.034,23.599,64.041,23.653,64.134,23.861,64.258,23.925,64.274,24.022,64.386,24.278,64.515,24.441,64.68,24.53,64.739,24.558,64.801,24.658,64.806,24.748,64.852,24.942,64.884,25.134,64.875,25.214,64.853,25.288,64.86,25.281,64.916,25.228,64.951,25.271,64.984,25.373,65.009,25.362,65.065,25.34,65.099,25.256,65.143,25.298,65.243,25.308,65.353,25.348,65.479,25.242,65.546,24.839,65.66,24.764,65.656,24.675,65.671,24.582,65.757,24.623,65.832,24.628,65.859,24.592,65.858,24.533,65.822,24.404,65.78,24.238,65.812,24.155,65.805]],[[21.994,60.337,21.921,60.332,21.819,60.382,21.806,60.401,21.846,60.412,21.819,60.452,21.827,60.47,21.907,60.438,21.95,60.402,21.908,60.393,21.98,60.355,21.994,60.337]],[[21.218,63.241,21.229,63.223,21.287,63.228,21.366,63.262,21.422,63.246,21.416,63.197,21.378,63.199,21.367,63.207,21.318,63.179,21.31,63.163,21.253,63.152,21.149,63.199,21.084,63.278,21.236,63.278,21.222,63.259,21.218,63.241]],[[22.175,60.371,22.302,60.348,22.355,60.356,22.416,60.303,22.313,60.27,22.306,60.229,22.346,60.203,22.361,60.166,22.258,60.166,22.209,60.197,22.188,60.237,22.141,60.265,22.077,60.286,22.108,60.315,22.126,60.356,22.175,60.371]],[[21.451,60.53,21.437,60.483,21.369,60.488,21.3,60.48,21.244,60.526,21.215,60.604,21.225,60.621,21.268,60.638,21.301,60.596,21.451,60.53]],[[21.833,60.141,21.733,60.106,21.695,60.114,21.705,60.172,21.764,60.199,21.864,60.202,21.833,60.141]],[[21.628,60.108,21.541,60.098,21.486,60.127,21.507,60.148,21.568,60.172,21.634,60.169,21.648,60.141,21.628,60.108]],[[24.848,64.991,24.699,64.958,24.579,64.979,24.577,65.043,24.651,65.074,24.786,65.086,24.971,65.055,24.998,65.039,24.892,65.026,24.848,64.991]]]},{"n":"Fiji","c":"FJ","r":6,"ml":3,"lx":177.98,"ly":-17.83,"b":[-180,-21.706,180,-12.477],"p":[[[179.999,-16.169,179.848,-16.302,179.794,-16.37,179.748,-16.446,179.619,-16.528,179.564,-16.637,179.568,-16.747,179.697,-16.632,179.841,-16.537,179.885,-16.518,179.93,-16.519,179.927,-16.552,179.906,-16.584,179.89,-16.667,179.928,-16.744,179.821,-16.737,179.715,-16.744,179.589,-16.787,179.465,-16.806,179.419,-16.807,179.375,-16.792,179.346,-16.77,179.323,-16.718,179.3,-16.71,179.202,-16.713,179.055,-16.814,179.007,-16.9,178.95,-16.904,178.884,-16.886,178.803,-16.952,178.707,-16.976,178.665,-16.92,178.638,-16.851,178.604,-16.801,178.497,-16.788,178.514,-16.726,178.542,-16.7,178.568,-16.664,178.584,-16.622,178.634,-16.649,178.686,-16.666,178.745,-16.634,178.805,-16.631,178.866,-16.54,178.961,-16.483,179.091,-16.437,179.225,-16.405,179.294,-16.399,179.359,-16.38,179.475,-16.294,179.552,-16.25,179.635,-16.223,179.715,-16.208,179.789,-16.221,179.848,-16.214,180,-16.153,179.999,-16.169]],[[178.28,-17.372,178.28,-17.416,178.309,-17.435,178.339,-17.438,178.411,-17.523,178.523,-17.596,178.592,-17.651,178.596,-17.699,178.575,-17.749,178.604,-17.839,178.618,-17.933,178.668,-18.081,178.597,-18.109,178.487,-18.112,178.461,-18.139,178.423,-18.124,178.332,-18.135,178.244,-18.184,178.16,-18.25,178.064,-18.25,177.955,-18.264,177.847,-18.255,177.771,-18.22,177.636,-18.181,177.457,-18.148,177.383,-18.121,177.321,-18.078,177.263,-17.969,177.255,-17.915,177.264,-17.863,177.316,-17.846,177.36,-17.82,177.366,-17.786,177.386,-17.762,177.411,-17.754,177.423,-17.737,177.406,-17.682,177.401,-17.632,177.504,-17.54,177.618,-17.461,177.818,-17.388,177.94,-17.395,178.128,-17.339,178.188,-17.313,178.247,-17.329,178.28,-17.372]],[[-179.975,-16.925,-180,-16.963,-180,-16.908,-180,-16.859,-180,-16.824,-180,-16.786,-179.894,-16.7,-179.861,-16.688,-179.822,-16.765,-179.868,-16.85,-179.975,-16.925]],[[180,-16.963,179.926,-17,179.897,-16.964,179.931,-16.876,180,-16.786,179.999,-16.859,180,-16.963]],[[178.488,-18.974,178.488,-19.017,178.359,-19.046,178.316,-19.01,178.288,-19.004,178.211,-19.067,178.189,-19.092,178.182,-19.112,178.162,-19.121,178.021,-19.152,177.959,-19.122,178.001,-19.101,178.052,-19.06,178.104,-19.066,178.157,-19.028,178.208,-18.97,178.282,-18.957,178.334,-18.934,178.42,-18.951,178.488,-18.974]],[[-178.988,-17.977,-179.018,-17.992,-179.039,-17.988,-179.064,-17.972,-179.079,-17.944,-179.048,-17.92,-178.999,-17.947,-178.988,-17.977]],[[-178.762,-18.234,-178.774,-18.252,-178.827,-18.222,-178.848,-18.202,-178.791,-18.186,-178.763,-18.191,-178.762,-18.234]],[[-178.251,-17.953,-178.307,-17.963,-178.357,-17.921,-178.325,-17.876,-178.28,-17.886,-178.255,-17.93,-178.251,-17.953]],[[-178.956,-17.273,-178.982,-17.307,-179.004,-17.295,-178.976,-17.237,-178.971,-17.213,-179.015,-17.182,-179.018,-17.161,-179.005,-17.148,-178.953,-17.182,-178.921,-17.208,-178.915,-17.223,-178.925,-17.249,-178.956,-17.273]],[[-178.535,-19.166,-178.546,-19.175,-178.574,-19.165,-178.596,-19.151,-178.599,-19.137,-178.589,-19.119,-178.568,-19.109,-178.557,-19.113,-178.563,-19.119,-178.576,-19.125,-178.574,-19.143,-178.557,-19.154,-178.541,-19.157,-178.535,-19.166]],[[-179.8,-18.94,-179.798,-18.97,-179.812,-18.968,-179.83,-18.956,-179.839,-18.962,-179.846,-18.971,-179.849,-18.991,-179.851,-19.003,-179.865,-18.999,-179.867,-18.978,-179.863,-18.964,-179.856,-18.943,-179.831,-18.924,-179.8,-18.94]],[[177.234,-17.147,177.183,-17.164,177.21,-17.084,177.239,-17.059,177.258,-17.054,177.287,-17.049,177.276,-17.105,177.234,-17.147]],[[179.422,-17.367,179.389,-17.394,179.373,-17.256,179.408,-17.257,179.433,-17.272,179.447,-17.306,179.422,-17.367]],[[179.349,-18.102,179.34,-18.11,179.254,-18.031,179.256,-17.999,179.272,-17.971,179.306,-17.944,179.338,-17.99,179.362,-18.065,179.349,-18.102]],[[178.828,-17.729,178.776,-17.747,178.748,-17.686,178.787,-17.624,178.831,-17.619,178.853,-17.681,178.828,-17.729]],[[-179.929,-16.503,-180,-16.54,-180,-16.489,-179.944,-16.441,-179.901,-16.432,-179.927,-16.479,-179.929,-16.503]],[[-179.956,-16.149,-180,-16.168,-180,-16.156,-180,-16.153,-179.969,-16.126,-179.945,-16.126,-179.956,-16.149]],[[180,-16.54,179.987,-16.541,179.985,-16.522,180,-16.489,180,-16.54]],[[177.121,-12.505,177.082,-12.516,177.019,-12.507,177.006,-12.491,177.026,-12.487,177.068,-12.477,177.118,-12.482,177.127,-12.493,177.121,-12.505]],[[174.63,-21.695,174.622,-21.706,174.593,-21.702,174.587,-21.68,174.604,-21.667,174.628,-21.676,174.63,-21.695]],[[-178.712,-20.668,-178.71,-20.671,-178.715,-20.67,-178.723,-20.667,-178.729,-20.66,-178.731,-20.653,-178.728,-20.645,-178.725,-20.646,-178.719,-20.652,-178.714,-20.66,-178.712,-20.668]]]},{"n":"Ethiopia","c":"ET","r":2,"ml":2,"lx":39.09,"ly":8.03,"b":[32.999,3.456,47.978,14.852],"p":[[[35.268,5.492,35.252,5.511,35.164,5.581,35.082,5.673,35.032,5.775,34.984,5.858,34.959,6.045,34.898,6.16,34.838,6.3,34.749,6.568,34.711,6.66,34.639,6.722,34.563,6.78,34.484,6.898,34.279,7.003,34.2,7.085,34.064,7.226,34.03,7.297,34.02,7.368,33.978,7.435,33.902,7.51,33.666,7.671,33.601,7.69,33.516,7.708,33.392,7.724,33.226,7.761,33.081,7.824,33.015,7.869,32.999,7.9,33.013,7.952,33.065,8.04,33.165,8.251,33.234,8.396,33.281,8.437,33.409,8.448,33.545,8.443,33.645,8.433,33.785,8.431,33.953,8.444,34.02,8.492,34.073,8.545,34.095,8.582,34.102,8.676,34.102,8.752,34.091,9.041,34.085,9.219,34.077,9.421,34.078,9.462,34.079,9.513,34.12,9.73,34.159,9.853,34.185,9.919,34.292,10.125,34.311,10.191,34.315,10.252,34.276,10.528,34.344,10.659,34.431,10.788,34.508,10.843,34.572,10.88,34.602,10.865,34.675,10.805,34.771,10.746,34.816,10.759,34.882,10.811,34.931,10.865,34.925,10.962,34.969,11.162,34.961,11.277,35.008,11.42,35.06,11.621,35.083,11.748,35.112,11.817,35.252,11.957,35.373,12.156,35.45,12.301,35.596,12.537,35.67,12.624,35.731,12.661,35.821,12.685,35.988,12.706,36.108,12.726,36.125,12.757,36.135,12.805,36.137,12.911,36.16,13.093,36.212,13.271,36.274,13.406,36.307,13.467,36.346,13.526,36.391,13.626,36.447,13.842,36.444,13.988,36.524,14.257,36.542,14.258,36.679,14.308,36.812,14.315,36.941,14.281,37.025,14.272,37.063,14.289,37.099,14.334,37.133,14.406,37.185,14.446,37.257,14.454,37.354,14.372,37.507,14.156,37.547,14.144,37.571,14.149,37.648,14.323,37.708,14.457,37.82,14.708,37.884,14.852,37.943,14.811,38.003,14.737,38.07,14.703,38.142,14.681,38.177,14.679,38.221,14.65,38.377,14.47,38.431,14.429,38.504,14.424,38.812,14.482,38.996,14.587,39.024,14.628,39.074,14.628,39.135,14.582,39.159,14.538,39.198,14.479,39.27,14.47,39.446,14.512,39.532,14.537,39.605,14.516,39.698,14.499,39.756,14.499,39.895,14.441,40.062,14.459,40.141,14.456,40.221,14.431,40.353,14.338,40.524,14.225,40.77,14.144,40.82,14.112,40.939,13.983,41.122,13.736,41.363,13.5,41.625,13.313,41.765,13.184,41.86,13.026,41.952,12.882,42.047,12.821,42.134,12.771,42.225,12.662,42.29,12.57,42.379,12.466,42.28,12.324,42.149,12.134,41.996,11.912,41.95,11.858,41.816,11.724,41.793,11.686,41.767,11.589,41.765,11.413,41.782,11.188,41.798,10.98,41.872,10.956,41.957,10.941,42.052,10.968,42.166,10.992,42.308,11.005,42.465,11.047,42.558,11.081,42.655,11.078,42.741,11.042,42.783,11.009,42.844,10.998,42.923,10.999,42.906,10.96,42.863,10.903,42.81,10.846,42.763,10.787,42.66,10.621,42.656,10.6,42.669,10.568,42.725,10.492,42.784,10.37,42.816,10.257,42.842,10.203,42.913,10.141,43.015,10.013,43.069,9.926,43.182,9.88,43.218,9.77,43.303,9.609,43.394,9.48,43.483,9.379,43.581,9.341,43.621,9.337,43.827,9.151,43.984,9.009,44.023,8.986,44.306,8.893,44.632,8.786,44.894,8.7,45.227,8.591,45.555,8.483,45.863,8.38,46.296,8.235,46.645,8.118,46.92,8.026,46.978,7.997,47.306,7.997,47.638,7.997,47.978,7.997,47.732,7.759,47.453,7.49,47.16,7.208,46.971,7.026,46.672,6.737,46.423,6.497,46.167,6.235,45.935,5.997,45.634,5.668,45.438,5.455,45.133,5.122,44.941,4.912,44.912,4.9,44.637,4.916,44.37,4.931,44.028,4.951,43.989,4.951,43.889,4.931,43.829,4.911,43.583,4.855,43.538,4.84,43.334,4.75,43.126,4.644,43.016,4.563,42.931,4.445,42.895,4.361,42.857,4.324,42.792,4.292,42.355,4.212,42.228,4.202,42.024,4.138,41.915,4.031,41.884,3.978,41.738,3.979,41.482,3.963,41.372,3.946,41.319,3.943,41.221,3.944,41.14,3.963,41.087,3.992,41.021,4.057,40.873,4.19,40.765,4.273,40.529,4.178,40.316,4.083,40.014,3.948,39.842,3.851,39.79,3.754,39.658,3.578,39.539,3.469,39.494,3.456,39.225,3.479,39.128,3.501,38.968,3.521,38.753,3.559,38.608,3.6,38.452,3.605,38.225,3.619,38.086,3.649,37.945,3.747,37.763,3.865,37.575,3.986,37.383,4.111,37.155,4.255,36.906,4.411,36.848,4.427,36.824,4.43,36.553,4.437,36.272,4.445,36.082,4.45,36.022,4.468,35.979,4.504,35.92,4.62,35.846,4.703,35.763,4.808,35.756,4.95,35.779,5.106,35.8,5.157,35.788,5.208,35.791,5.279,35.745,5.344,35.469,5.419,35.424,5.413,35.378,5.385,35.325,5.365,35.288,5.384,35.265,5.412,35.264,5.458,35.268,5.492]]]},{"n":"Estonia","c":"EE","r":6,"ml":3,"lx":25.87,"ly":58.72,"b":[21.854,57.525,28.151,59.639],"p":[[[27.352,57.528,27.327,57.525,27.187,57.538,27.033,57.579,26.966,57.609,26.9,57.609,26.82,57.589,26.533,57.531,26.462,57.544,26.298,57.601,26.215,57.663,26.03,57.786,26.015,57.815,25.991,57.838,25.794,57.869,25.721,57.914,25.66,57.92,25.571,57.943,25.34,58.039,25.283,58.048,25.269,58.032,25.273,58.009,25.258,57.996,25.229,57.997,25.175,58.032,25.111,58.063,24.911,58.005,24.839,57.989,24.776,57.985,24.459,57.908,24.363,57.866,24.323,57.871,24.332,57.91,24.464,58.106,24.488,58.262,24.536,58.283,24.55,58.305,24.529,58.354,24.392,58.386,24.337,58.381,24.287,58.328,24.236,58.29,24.115,58.266,24.011,58.307,23.768,58.361,23.706,58.433,23.692,58.506,23.563,58.576,23.509,58.659,23.531,58.716,23.647,58.754,23.681,58.787,23.534,58.782,23.504,58.79,23.497,58.82,23.432,58.921,23.49,58.96,23.515,58.999,23.468,59.032,23.48,59.07,23.517,59.108,23.494,59.196,23.641,59.242,23.783,59.275,24.083,59.292,24.054,59.372,24.175,59.376,24.38,59.473,24.584,59.456,24.878,59.522,25.444,59.521,25.521,59.559,25.507,59.598,25.509,59.639,25.616,59.628,25.794,59.635,26.461,59.554,26.625,59.554,26.852,59.472,26.975,59.451,27.336,59.45,27.893,59.414,28.002,59.47,28.013,59.484,28.066,59.453,28.133,59.403,28.151,59.374,28.128,59.358,28.061,59.343,28.046,59.328,28.016,59.302,27.938,59.297,27.898,59.278,27.85,59.193,27.758,59.052,27.622,58.945,27.513,58.886,27.464,58.841,27.434,58.787,27.427,58.733,27.531,58.435,27.53,58.381,27.506,58.326,27.488,58.27,27.502,58.221,27.571,58.138,27.644,58.014,27.673,57.935,27.722,57.905,27.769,57.884,27.779,57.871,27.777,57.857,27.753,57.841,27.542,57.799,27.515,57.764,27.492,57.725,27.4,57.667,27.372,57.613,27.354,57.55,27.352,57.528]],[[22.617,58.621,22.688,58.597,22.754,58.605,22.82,58.622,22.964,58.606,23.293,58.483,23.323,58.451,23.127,58.436,23.083,58.398,23.035,58.372,22.98,58.364,22.885,58.311,22.757,58.261,22.73,58.231,22.498,58.236,22.372,58.217,22.269,58.161,22.227,58.052,22.152,57.967,22.076,57.936,21.997,57.931,21.978,57.963,21.986,57.995,22.153,58.115,22.188,58.154,22.104,58.172,22.035,58.213,21.882,58.262,21.854,58.302,21.891,58.305,21.924,58.316,21.965,58.349,21.984,58.387,21.862,58.497,21.924,58.514,22.002,58.51,22.081,58.478,22.169,58.516,22.206,58.521,22.267,58.508,22.328,58.581,22.474,58.605,22.547,58.627,22.617,58.621]],[[22.924,58.827,22.842,58.777,22.793,58.797,22.767,58.821,22.661,58.709,22.542,58.69,22.473,58.712,22.479,58.754,22.411,58.863,22.307,58.895,22.162,58.898,22.056,58.944,22.463,58.974,22.505,59.026,22.587,59.081,22.649,59.087,22.702,59.074,22.712,59.032,22.725,59.015,22.91,58.991,22.982,58.92,23.009,58.834,22.924,58.827]],[[23.344,58.55,23.26,58.54,23.063,58.611,23.109,58.659,23.165,58.678,23.333,58.649,23.356,58.576,23.344,58.55]]]},{"n":"Eritrea","c":"ER","r":4,"ml":4,"lx":38.29,"ly":15.79,"b":[36.427,12.377,43.117,18.005],"p":[[[36.524,14.257,36.492,14.544,36.471,14.736,36.448,14.94,36.427,15.132,36.522,15.25,36.566,15.362,36.679,15.726,36.725,15.799,36.813,15.994,36.826,16.05,36.914,16.296,36.905,16.46,36.888,16.625,36.936,16.722,36.979,16.801,36.976,16.867,36.995,17.021,37.009,17.059,37.062,17.061,37.17,17.041,37.249,17.057,37.34,17.057,37.411,17.062,37.453,17.109,37.51,17.288,37.547,17.324,37.576,17.335,37.657,17.368,37.726,17.421,37.782,17.458,37.803,17.466,37.863,17.47,37.923,17.492,37.95,17.518,38.025,17.538,38.099,17.526,38.149,17.549,38.182,17.563,38.219,17.564,38.254,17.585,38.267,17.617,38.29,17.637,38.347,17.684,38.374,17.717,38.386,17.751,38.397,17.778,38.422,17.824,38.523,17.939,38.609,18.005,38.912,17.427,39.034,17.086,39.143,16.729,39.223,16.194,39.299,15.921,39.422,15.787,39.507,15.532,39.579,15.523,39.631,15.453,39.721,15.214,39.786,15.125,39.819,15.201,39.816,15.245,39.79,15.319,39.813,15.414,39.864,15.47,39.978,15.393,40.041,15.335,40.058,15.217,40.084,15.152,40.204,15.014,40.305,14.974,40.437,14.964,40.546,14.934,40.634,14.883,40.799,14.743,41.176,14.62,41.48,14.244,41.658,13.983,42.245,13.588,42.346,13.398,42.399,13.213,42.523,13.221,42.734,13.019,42.796,12.864,42.97,12.808,42.999,12.9,43.083,12.825,43.117,12.709,43.006,12.662,42.883,12.621,42.866,12.623,42.825,12.569,42.767,12.423,42.704,12.38,42.67,12.377,42.479,12.514,42.45,12.521,42.409,12.494,42.379,12.466,42.29,12.57,42.225,12.662,42.134,12.771,42.047,12.821,41.952,12.882,41.86,13.026,41.765,13.184,41.625,13.313,41.363,13.5,41.122,13.736,40.939,13.983,40.82,14.112,40.77,14.144,40.524,14.225,40.353,14.338,40.221,14.431,40.141,14.456,40.062,14.459,39.895,14.441,39.756,14.499,39.698,14.499,39.605,14.516,39.532,14.537,39.446,14.512,39.27,14.47,39.198,14.479,39.159,14.538,39.135,14.582,39.074,14.628,39.024,14.628,38.996,14.587,38.812,14.482,38.504,14.424,38.431,14.429,38.377,14.47,38.221,14.65,38.177,14.679,38.142,14.681,38.07,14.703,38.003,14.737,37.943,14.811,37.884,14.852,37.82,14.708,37.708,14.457,37.648,14.323,37.571,14.149,37.547,14.144,37.507,14.156,37.354,14.372,37.257,14.454,37.185,14.446,37.133,14.406,37.099,14.334,37.063,14.289,37.025,14.272,36.941,14.281,36.812,14.315,36.679,14.308,36.542,14.258,36.524,14.257]],[[40.076,16.082,40.11,15.986,40.012,16.023,39.996,16.043,40.039,16.081,40.048,16.104,40.076,16.082]],[[40.141,15.696,40.183,15.643,40.211,15.648,40.234,15.666,40.25,15.703,40.408,15.629,40.399,15.58,40.305,15.577,40.196,15.598,40.095,15.591,39.975,15.612,39.947,15.696,40.024,15.656,40.063,15.666,40.071,15.677,40.016,15.733,39.94,15.745,39.945,15.789,39.979,15.807,40,15.828,39.957,15.889,40.043,15.875,40.097,15.838,40.132,15.795,40.141,15.696]]]},{"n":"Equatorial Guinea","c":"GQ","r":4,"ml":4,"lx":8.99,"ly":2.33,"b":[8.434,0.96,11.335,3.758],"p":[[[8.736,3.758,8.76,3.754,8.91,3.758,8.951,3.705,8.946,3.628,8.792,3.4,8.763,3.305,8.704,3.224,8.652,3.217,8.475,3.265,8.445,3.294,8.434,3.332,8.452,3.423,8.465,3.451,8.55,3.468,8.577,3.482,8.623,3.58,8.638,3.669,8.676,3.736,8.736,3.758]],[[11.329,2.167,11.33,1.936,11.331,1.74,11.332,1.528,11.334,1.308,11.335,1.121,11.335,1,11.131,1,10.859,1.001,10.587,1.002,10.315,1.003,10.179,1.004,10.029,1.004,9.98,0.998,9.947,0.967,9.907,0.96,9.86,0.986,9.804,0.999,9.789,1.026,9.761,1.075,9.705,1.08,9.676,1.075,9.636,1.047,9.591,1.032,9.599,1.054,9.51,1.115,9.445,1.121,9.386,1.139,9.434,1.296,9.494,1.435,9.584,1.54,9.632,1.566,9.648,1.618,9.719,1.789,9.807,1.927,9.78,2.068,9.801,2.304,9.826,2.298,9.83,2.275,9.837,2.242,9.87,2.213,9.98,2.168,10.307,2.168,10.502,2.168,10.791,2.168,11.097,2.167,11.329,2.167]]]},{"n":"El Salvador","c":"SV","r":6,"ml":5,"lx":-88.89,"ly":13.69,"b":[-90.106,13.164,-87.715,14.431],"p":[[[-89.363,14.416,-89.337,14.411,-89.17,14.36,-89.121,14.37,-89.057,14.329,-89.027,14.297,-89,14.253,-88.868,14.164,-88.846,14.125,-88.747,14.072,-88.708,14.032,-88.666,14.016,-88.583,14,-88.513,13.979,-88.504,13.964,-88.498,13.905,-88.483,13.854,-88.449,13.851,-88.408,13.875,-88.276,13.943,-88.151,13.987,-88.08,13.961,-88.039,13.905,-87.991,13.88,-87.892,13.895,-87.802,13.89,-87.731,13.841,-87.715,13.813,-87.759,13.65,-87.774,13.58,-87.782,13.521,-87.756,13.506,-87.732,13.483,-87.737,13.451,-87.814,13.399,-87.838,13.386,-87.821,13.285,-87.878,13.224,-87.931,13.181,-88.023,13.169,-88.181,13.164,-88.417,13.214,-88.592,13.281,-88.686,13.281,-88.656,13.259,-88.582,13.245,-88.484,13.197,-88.512,13.184,-88.867,13.283,-89.278,13.478,-89.523,13.509,-89.804,13.56,-89.97,13.683,-90.095,13.737,-90.106,13.783,-90.105,13.835,-90.048,13.904,-89.943,13.997,-89.873,14.046,-89.84,14.055,-89.794,14.05,-89.749,14.077,-89.711,14.141,-89.671,14.183,-89.57,14.225,-89.547,14.241,-89.555,14.277,-89.577,14.347,-89.574,14.39,-89.541,14.41,-89.501,14.414,-89.419,14.431,-89.383,14.428,-89.363,14.416]]]},{"n":"Egypt","c":"EG","r":2,"ml":1.7,"lx":29.45,"ly":26.19,"b":[24.703,21.995,36.871,31.655],"p":[[[36.871,21.997,36.543,21.997,36.215,21.997,35.887,21.997,35.559,21.996,35.231,21.996,34.903,21.996,34.575,21.996,34.246,21.996,33.918,21.996,33.59,21.996,33.262,21.996,32.934,21.996,32.606,21.996,32.278,21.996,31.95,21.996,31.622,21.996,31.434,21.996,31.466,22.085,31.486,22.148,31.464,22.192,31.4,22.202,31.358,22.189,31.261,22.002,31.209,21.995,31.093,21.995,30.711,21.995,30.329,21.995,29.947,21.995,29.565,21.995,29.183,21.995,28.801,21.995,28.419,21.995,28.036,21.995,27.654,21.995,27.272,21.996,26.89,21.996,26.508,21.996,26.126,21.996,25.744,21.996,25.362,21.996,24.98,21.996,24.98,22.22,24.98,22.445,24.98,22.67,24.98,22.894,24.98,23.119,24.98,23.343,24.98,23.568,24.98,23.792,24.98,24.017,24.98,24.242,24.98,24.466,24.98,24.691,24.98,24.915,24.98,25.14,24.98,25.364,24.98,25.589,24.98,25.813,24.98,26.038,24.98,26.263,24.98,26.487,24.98,26.712,24.98,26.936,24.98,27.161,24.98,27.385,24.98,27.61,24.98,27.835,24.98,28.059,24.98,28.284,24.98,28.508,24.98,28.733,24.98,28.957,24.98,29.182,24.972,29.224,24.916,29.376,24.866,29.57,24.811,29.809,24.804,29.886,24.712,30.132,24.703,30.201,24.726,30.251,24.878,30.458,24.923,30.558,24.961,30.679,24.974,30.777,24.929,30.926,24.878,31.061,24.86,31.199,24.853,31.335,24.93,31.427,25.023,31.514,25.057,31.567,25.112,31.627,25.15,31.655,25.225,31.534,25.382,31.513,25.893,31.621,26.457,31.512,26.769,31.47,27.248,31.378,27.54,31.213,27.62,31.192,27.83,31.195,27.968,31.097,28.515,31.05,28.807,30.943,28.973,30.857,29.072,30.83,29.16,30.835,29.279,30.867,29.429,30.927,29.592,31.012,29.93,31.227,30.049,31.265,30.128,31.256,30.223,31.258,30.262,31.317,30.312,31.357,30.344,31.403,30.395,31.458,30.571,31.473,30.924,31.567,30.884,31.522,30.563,31.417,30.7,31.404,30.841,31.44,31.002,31.463,31.031,31.508,31.052,31.592,31.083,31.603,31.194,31.588,31.524,31.458,31.607,31.456,31.839,31.526,31.889,31.541,31.964,31.502,32.136,31.341,32.076,31.344,31.892,31.482,31.876,31.414,31.771,31.293,31.902,31.24,32.008,31.221,32.066,31.153,32.102,31.093,32.207,31.119,32.282,31.201,32.243,31.247,32.216,31.294,32.251,31.295,32.324,31.256,32.533,31.101,32.603,31.069,32.685,31.074,32.854,31.118,32.902,31.111,33.13,31.168,33.157,31.126,33.194,31.085,33.378,31.131,33.667,31.13,33.903,31.181,34.176,31.304,34.198,31.323,34.213,31.292,34.245,31.208,34.329,30.995,34.401,30.828,34.49,30.596,34.518,30.507,34.53,30.446,34.659,30.191,34.735,29.982,34.791,29.812,34.87,29.564,34.904,29.477,34.849,29.432,34.736,29.271,34.617,28.758,34.446,28.357,34.427,28.106,34.4,28.016,34.319,27.889,34.22,27.764,34.045,27.829,33.76,28.048,33.594,28.256,33.416,28.39,33.248,28.568,33.202,28.696,33.204,28.778,33.13,28.978,33.076,29.073,32.871,29.286,32.812,29.4,32.767,29.45,32.721,29.522,32.647,29.798,32.566,29.974,32.473,29.925,32.489,29.852,32.409,29.749,32.36,29.631,32.397,29.534,32.565,29.386,32.599,29.322,32.638,29.182,32.632,28.992,32.659,28.928,32.784,28.787,32.829,28.703,32.857,28.631,32.898,28.565,33.023,28.442,33.202,28.208,33.372,28.051,33.495,27.974,33.547,27.898,33.559,27.701,33.55,27.607,33.657,27.431,33.697,27.341,33.802,27.268,33.849,27.185,33.893,27.049,33.959,26.649,34.05,26.551,34.329,26.024,34.565,25.691,34.679,25.443,34.853,25.14,35.194,24.475,35.397,24.27,35.478,24.155,35.625,24.066,35.784,23.938,35.632,23.95,35.594,23.943,35.541,23.921,35.515,23.843,35.504,23.779,35.523,23.443,35.564,23.271,35.698,22.946,35.797,22.849,35.846,22.786,35.913,22.74,36.23,22.629,36.415,22.394,36.83,22.098,36.87,22.016,36.871,21.997]]]},{"n":"Ecuador","c":"EC","r":3,"ml":3,"lx":-78.19,"ly":-1.26,"b":[-91.654,-4.991,-75.25,1.455],"p":[[[-75.284,-0.107,-75.34,-0.142,-75.398,-0.146,-75.476,-0.157,-75.584,-0.123,-75.626,-0.123,-75.632,-0.158,-75.561,-0.2,-75.491,-0.248,-75.466,-0.322,-75.425,-0.409,-75.325,-0.507,-75.263,-0.555,-75.259,-0.59,-75.279,-0.654,-75.284,-0.707,-75.25,-0.952,-75.272,-0.967,-75.309,-0.968,-75.348,-0.967,-75.38,-0.94,-75.408,-0.924,-75.42,-0.962,-75.449,-1.071,-75.514,-1.316,-75.571,-1.531,-75.642,-1.607,-75.745,-1.728,-75.885,-1.893,-76.09,-2.133,-76.241,-2.244,-76.36,-2.331,-76.499,-2.432,-76.679,-2.563,-76.881,-2.636,-77.161,-2.738,-77.36,-2.81,-77.506,-2.86,-77.659,-2.912,-77.861,-2.982,-77.938,-3.047,-78.068,-3.207,-78.128,-3.284,-78.183,-3.35,-78.195,-3.38,-78.187,-3.4,-78.161,-3.432,-78.158,-3.465,-78.195,-3.486,-78.226,-3.489,-78.24,-3.473,-78.251,-3.436,-78.284,-3.399,-78.323,-3.388,-78.345,-3.397,-78.347,-3.431,-78.398,-3.595,-78.4,-3.674,-78.421,-3.706,-78.42,-3.777,-78.471,-3.843,-78.493,-3.902,-78.509,-3.952,-78.55,-3.987,-78.565,-4.042,-78.603,-4.157,-78.648,-4.248,-78.679,-4.326,-78.685,-4.384,-78.661,-4.425,-78.653,-4.458,-78.674,-4.518,-78.686,-4.562,-78.743,-4.593,-78.862,-4.665,-78.908,-4.714,-78.926,-4.771,-78.914,-4.819,-78.919,-4.858,-78.975,-4.873,-78.995,-4.908,-79.033,-4.969,-79.076,-4.991,-79.187,-4.958,-79.268,-4.958,-79.331,-4.928,-79.399,-4.84,-79.456,-4.766,-79.502,-4.671,-79.516,-4.539,-79.578,-4.501,-79.639,-4.455,-79.711,-4.468,-79.797,-4.476,-79.845,-4.446,-79.963,-4.39,-80.064,-4.328,-80.14,-4.296,-80.197,-4.311,-80.232,-4.349,-80.293,-4.417,-80.383,-4.464,-80.424,-4.461,-80.479,-4.43,-80.488,-4.394,-80.444,-4.336,-80.353,-4.208,-80.454,-4.205,-80.488,-4.166,-80.493,-4.119,-80.51,-4.07,-80.49,-4.01,-80.437,-3.979,-80.358,-4.003,-80.303,-4.005,-80.267,-3.949,-80.231,-3.924,-80.194,-3.906,-80.179,-3.878,-80.218,-3.788,-80.229,-3.739,-80.217,-3.711,-80.219,-3.654,-80.221,-3.613,-80.244,-3.577,-80.245,-3.522,-80.265,-3.492,-80.272,-3.461,-80.274,-3.425,-80.298,-3.406,-80.325,-3.388,-80.303,-3.375,-80.16,-3.324,-80.1,-3.274,-80.027,-3.228,-79.963,-3.158,-79.922,-3.09,-79.823,-2.777,-79.73,-2.579,-79.746,-2.485,-79.822,-2.357,-79.84,-2.168,-79.833,-2.111,-79.842,-2.067,-79.893,-2.146,-79.88,-2.424,-79.926,-2.549,-79.989,-2.579,-80.03,-2.557,-80.007,-2.354,-80.053,-2.391,-80.127,-2.528,-80.249,-2.631,-80.255,-2.665,-80.285,-2.707,-80.379,-2.668,-80.45,-2.626,-80.685,-2.397,-80.839,-2.349,-80.932,-2.269,-80.952,-2.235,-80.963,-2.189,-80.868,-2.141,-80.77,-2.077,-80.761,-1.935,-80.763,-1.823,-80.835,-1.632,-80.801,-1.383,-80.82,-1.286,-80.902,-1.079,-80.841,-0.975,-80.624,-0.899,-80.554,-0.848,-80.505,-0.684,-80.455,-0.585,-80.358,-0.625,-80.282,-0.621,-80.385,-0.584,-80.468,-0.436,-80.482,-0.368,-80.321,-0.166,-80.237,-0.113,-80.133,-0.006,-80.046,0.155,-80.025,0.41,-80.061,0.592,-80.088,0.785,-80.036,0.835,-79.904,0.86,-79.796,0.922,-79.741,0.98,-79.613,0.971,-79.465,1.06,-79.229,1.105,-78.9,1.206,-78.827,1.296,-78.86,1.455,-78.829,1.435,-78.737,1.359,-78.682,1.283,-78.588,1.237,-78.512,1.199,-78.312,1.046,-78.181,0.969,-78.037,0.899,-77.83,0.825,-77.703,0.838,-77.673,0.782,-77.649,0.724,-77.601,0.69,-77.526,0.66,-77.481,0.651,-77.468,0.637,-77.423,0.425,-77.396,0.394,-77.293,0.36,-77.166,0.348,-77.114,0.355,-77.002,0.296,-76.92,0.269,-76.829,0.248,-76.768,0.242,-76.739,0.251,-76.729,0.272,-76.679,0.268,-76.603,0.241,-76.495,0.235,-76.427,0.261,-76.418,0.304,-76.413,0.379,-76.388,0.405,-76.311,0.448,-76.271,0.439,-76.068,0.346,-76.026,0.313,-75.975,0.248,-75.88,0.151,-75.777,0.089,-75.617,0.063,-75.464,-0.038,-75.284,-0.107]],[[-80.132,-2.973,-80.151,-3.012,-80.246,-3.008,-80.273,-2.996,-80.272,-2.952,-80.25,-2.812,-80.224,-2.753,-80.146,-2.696,-80.081,-2.669,-79.997,-2.674,-79.909,-2.726,-80.013,-2.82,-80.071,-2.834,-80.093,-2.846,-80.132,-2.973]],[[-78.909,1.253,-78.966,1.245,-78.992,1.293,-78.923,1.349,-78.9,1.36,-78.909,1.253]],[[-90.335,-0.772,-90.387,-0.773,-90.542,-0.676,-90.532,-0.581,-90.47,-0.517,-90.269,-0.485,-90.185,-0.545,-90.193,-0.659,-90.261,-0.742,-90.315,-0.757,-90.335,-0.772]],[[-89.419,-0.911,-89.537,-0.952,-89.577,-0.934,-89.603,-0.913,-89.609,-0.889,-89.543,-0.827,-89.48,-0.793,-89.423,-0.722,-89.318,-0.68,-89.288,-0.69,-89.267,-0.705,-89.259,-0.728,-89.295,-0.786,-89.358,-0.826,-89.419,-0.911]],[[-91.426,-0.461,-91.526,-0.478,-91.611,-0.444,-91.647,-0.391,-91.654,-0.311,-91.647,-0.284,-91.46,-0.256,-91.399,-0.322,-91.4,-0.421,-91.426,-0.461]],[[-90.424,-1.34,-90.464,-1.342,-90.52,-1.299,-90.477,-1.221,-90.432,-1.24,-90.399,-1.262,-90.379,-1.292,-90.424,-1.34]],[[-91.272,0.025,-91.21,-0.039,-91.176,-0.223,-90.976,-0.417,-90.951,-0.525,-90.968,-0.576,-90.959,-0.595,-90.863,-0.672,-90.8,-0.752,-90.906,-0.941,-91.131,-1.02,-91.372,-1.017,-91.419,-0.997,-91.484,-0.925,-91.495,-0.861,-91.458,-0.8,-91.334,-0.706,-91.145,-0.623,-91.121,-0.559,-91.197,-0.497,-91.25,-0.374,-91.369,-0.287,-91.429,-0.023,-91.469,-0.01,-91.55,-0.047,-91.59,-0.015,-91.597,0.002,-91.509,0.062,-91.491,0.105,-91.361,0.126,-91.306,0.091,-91.272,0.025]],[[-90.574,-0.334,-90.62,-0.364,-90.809,-0.329,-90.868,-0.271,-90.82,-0.192,-90.78,-0.16,-90.668,-0.19,-90.553,-0.278,-90.574,-0.334]]]},{"n":"Dominican Republic","c":"DO","r":5,"ml":4.5,"lx":-70.65,"ly":19.1,"b":[-72,17.636,-68.339,19.914],"p":[[[-71.768,18.039,-71.764,18.204,-71.737,18.271,-71.762,18.341,-71.873,18.416,-71.94,18.513,-72,18.598,-71.987,18.61,-71.867,18.614,-71.824,18.646,-71.743,18.733,-71.727,18.803,-71.734,18.856,-71.786,18.92,-71.807,18.987,-71.742,19.046,-71.657,19.131,-71.645,19.164,-71.647,19.196,-71.746,19.286,-71.753,19.324,-71.707,19.422,-71.711,19.487,-71.757,19.688,-71.779,19.718,-71.735,19.735,-71.706,19.795,-71.667,19.849,-71.616,19.877,-71.558,19.895,-71.442,19.894,-71.281,19.847,-71.236,19.848,-71.082,19.89,-70.954,19.914,-70.834,19.887,-70.785,19.851,-70.686,19.793,-70.636,19.776,-70.479,19.777,-70.436,19.771,-70.305,19.676,-70.194,19.638,-70.129,19.636,-70.014,19.673,-69.957,19.672,-69.891,19.59,-69.878,19.473,-69.823,19.367,-69.739,19.299,-69.325,19.328,-69.232,19.272,-69.264,19.226,-69.323,19.201,-69.52,19.212,-69.606,19.206,-69.623,19.16,-69.624,19.118,-69.508,19.108,-69.395,19.086,-69.28,19.052,-69.163,19.028,-69.031,19.013,-68.901,18.988,-68.685,18.905,-68.445,18.714,-68.381,18.671,-68.339,18.612,-68.359,18.538,-68.445,18.418,-68.493,18.379,-68.564,18.355,-68.612,18.306,-68.659,18.222,-68.687,18.215,-68.721,18.218,-68.778,18.266,-68.82,18.339,-68.935,18.408,-69.072,18.399,-69.275,18.44,-69.397,18.42,-69.519,18.416,-69.645,18.436,-69.771,18.444,-69.896,18.418,-70.018,18.374,-70.063,18.346,-70.142,18.277,-70.183,18.252,-70.48,18.217,-70.565,18.268,-70.645,18.336,-70.759,18.346,-70.924,18.292,-71.028,18.273,-71.07,18.25,-71.082,18.224,-71.083,18.128,-71.106,18.07,-71.267,17.85,-71.358,17.694,-71.396,17.646,-71.439,17.636,-71.518,17.725,-71.569,17.757,-71.632,17.774,-71.658,17.821,-71.657,17.889,-71.674,17.954,-71.712,18.005,-71.768,18.039]]]},{"n":"Dominica","c":"DM","r":6,"ml":4,"lx":-61.34,"ly":15.46,"b":[-61.481,15.227,-61.251,15.633],"p":[[[-61.282,15.249,-61.375,15.227,-61.416,15.4,-61.481,15.525,-61.47,15.603,-61.458,15.633,-61.32,15.585,-61.277,15.527,-61.251,15.373,-61.282,15.249]]]},{"n":"Djibouti","c":"DJ","r":5,"ml":4,"lx":42.5,"ly":11.98,"b":[41.765,10.941,43.41,12.709],"p":[[[43.246,11.5,43.159,11.366,43.049,11.194,42.923,10.999,42.844,10.998,42.783,11.009,42.741,11.042,42.655,11.078,42.558,11.081,42.465,11.047,42.308,11.005,42.166,10.992,42.052,10.968,41.957,10.941,41.872,10.956,41.798,10.98,41.782,11.188,41.765,11.413,41.767,11.589,41.793,11.686,41.816,11.724,41.95,11.858,41.996,11.912,42.149,12.134,42.28,12.324,42.379,12.466,42.409,12.494,42.45,12.521,42.479,12.514,42.67,12.377,42.704,12.38,42.767,12.423,42.825,12.569,42.866,12.623,42.883,12.621,43.006,12.662,43.117,12.709,43.131,12.66,43.299,12.464,43.354,12.367,43.41,12.19,43.38,12.091,43.337,12.027,43.272,11.97,43.048,11.829,42.799,11.739,42.64,11.56,42.522,11.572,42.54,11.504,42.584,11.497,42.653,11.51,42.79,11.562,42.912,11.587,43.043,11.588,43.162,11.566,43.246,11.5]]]},{"n":"Greenland","c":"GL","r":3,"ml":1.7,"lx":-39.34,"ly":74.32,"b":[-72.818,59.815,-11.426,83.6],"p":[[[-29.953,83.565,-28.992,83.505,-28.484,83.435,-28.377,83.437,-27.688,83.41,-27.034,83.377,-25.947,83.29,-25.795,83.261,-25.912,83.238,-26.183,83.221,-27.572,83.193,-30.092,83.157,-31.534,83.089,-31.993,83.085,-32.033,82.983,-31.837,82.978,-31.516,82.992,-30.386,83.094,-29.964,83.11,-29.175,83.102,-28.151,83.064,-27.739,83.077,-27.002,83.067,-26.141,83.096,-25.123,83.16,-24.845,83.019,-24.47,82.877,-24.174,82.893,-23.92,82.885,-23.834,82.839,-23.695,82.819,-23.407,82.83,-22.525,82.789,-21.92,82.716,-21.692,82.683,-21.583,82.634,-21.521,82.595,-21.616,82.548,-21.994,82.463,-22.473,82.385,-23.118,82.325,-23.862,82.287,-29.579,82.161,-29.773,82.131,-29.887,82.055,-29.811,81.955,-29.544,81.94,-28.919,81.996,-27.84,82.049,-27.046,82.046,-25.149,82.001,-24.589,81.883,-24.293,81.701,-23.637,81.742,-23.496,81.773,-23.393,81.827,-23.311,81.885,-23.249,81.947,-23.18,81.989,-23.104,82.012,-22.94,82.031,-22.563,82.053,-21.576,82.075,-21.338,82.069,-21.167,81.984,-21.13,81.934,-21.118,81.87,-21.123,81.79,-21.147,81.695,-21.231,81.601,-21.504,81.438,-21.724,81.348,-21.961,81.284,-22.415,81.137,-22.573,81.098,-23.072,80.927,-23.196,80.847,-23.204,80.789,-23.118,80.778,-22.973,80.833,-22.919,80.872,-22.826,80.913,-22.089,81.02,-21.931,81.05,-21.45,81.178,-21.142,81.226,-20.89,81.276,-20.756,81.312,-20.016,81.564,-19.63,81.64,-19.225,81.64,-19.153,81.512,-18.667,81.492,-18.457,81.498,-18.118,81.467,-17.969,81.441,-17.717,81.428,-17.456,81.398,-17.226,81.43,-17.159,81.451,-16.937,81.544,-16.637,81.626,-16.359,81.729,-16.267,81.754,-16.121,81.777,-15.969,81.785,-15.556,81.834,-15.451,81.837,-15.227,81.822,-14.242,81.814,-13.704,81.789,-12.956,81.72,-12.434,81.683,-12.193,81.649,-11.841,81.578,-11.557,81.503,-11.426,81.481,-11.431,81.457,-11.529,81.424,-12.231,81.309,-12.461,81.233,-13.126,81.088,-13.451,81.038,-13.804,81.019,-14.197,81.014,-14.452,80.993,-14.49,80.973,-14.308,80.913,-14.229,80.87,-14.24,80.832,-14.431,80.776,-14.504,80.763,-15.194,80.721,-15.543,80.65,-15.998,80.642,-16.319,80.65,-16.761,80.573,-16.588,80.511,-16.429,80.484,-15.937,80.428,-15.933,80.395,-16.168,80.329,-16.489,80.252,-16.868,80.198,-17.011,80.19,-17.191,80.204,-17.357,80.201,-17.723,80.176,-18.071,80.172,-18.693,80.207,-19.029,80.248,-19.206,80.262,-19.429,80.258,-19.515,80.241,-19.867,80.145,-20.04,80.079,-20.15,80.011,-20.197,79.938,-20.181,79.858,-20.138,79.803,-20.069,79.774,-19.985,79.756,-19.839,79.746,-19.518,79.755,-19.392,79.75,-19.353,79.734,-19.284,79.683,-19.296,79.635,-19.354,79.567,-19.399,79.488,-19.431,79.398,-19.414,79.349,-19.284,79.338,-19.223,79.342,-19.152,79.325,-19.072,79.289,-19.011,79.251,-18.971,79.211,-18.992,79.178,-19.075,79.152,-19.262,79.123,-19.723,79.065,-19.77,79.047,-19.806,79.012,-19.832,78.959,-19.887,78.911,-19.973,78.868,-20.05,78.842,-20.2,78.83,-20.396,78.829,-20.616,78.804,-21.134,78.659,-21.141,78.643,-20.947,78.596,-20.956,78.555,-21.195,78.38,-21.26,78.293,-21.312,78.174,-21.397,78.074,-21.516,77.992,-21.633,77.897,-21.748,77.791,-21.73,77.709,-21.579,77.651,-21.38,77.698,-21.132,77.847,-20.863,77.912,-20.572,77.892,-20.319,77.862,-19.995,77.803,-19.724,77.767,-19.49,77.719,-19.394,77.678,-19.297,77.621,-19.296,77.585,-19.468,77.566,-19.524,77.572,-19.953,77.666,-20.162,77.69,-20.439,77.662,-20.681,77.619,-20.464,77.447,-20.232,77.368,-19.809,77.332,-19.588,77.294,-19.426,77.246,-19.3,77.222,-19.131,77.233,-18.903,77.28,-18.586,77.283,-18.443,77.259,-18.339,77.215,-18.292,77.133,-18.303,77.012,-18.337,76.921,-18.396,76.86,-18.51,76.778,-18.606,76.763,-18.74,76.768,-18.865,76.785,-18.981,76.814,-19.156,76.837,-19.509,76.861,-19.865,76.914,-20.064,76.928,-20.487,76.921,-20.942,76.887,-20.96,76.843,-21.615,76.688,-21.749,76.69,-21.931,76.743,-22.185,76.794,-22.334,76.794,-22.555,76.729,-22.609,76.704,-22.607,76.681,-22.444,76.625,-22.379,76.612,-22.295,76.601,-22.004,76.588,-21.877,76.573,-21.758,76.401,-21.569,76.294,-21.488,76.272,-21.417,76.264,-21.185,76.268,-20.887,76.304,-20.783,76.275,-20.564,76.24,-20.435,76.231,-20.279,76.232,-20.104,76.219,-19.863,76.121,-19.958,75.997,-19.807,75.897,-19.566,75.795,-19.509,75.758,-19.486,75.69,-19.48,75.645,-19.462,75.604,-19.431,75.567,-19.4,75.494,-19.366,75.386,-19.375,75.298,-19.426,75.23,-19.526,75.18,-19.676,75.149,-19.798,75.157,-19.893,75.205,-20.027,75.255,-20.199,75.308,-20.485,75.314,-20.906,75.157,-21.094,75.149,-21.247,75.133,-21.409,75.065,-21.649,75.023,-21.861,75.04,-22.233,75.12,-22.098,75.066,-21.904,75.004,-21.784,74.971,-21.695,74.964,-21.598,74.972,-21.457,74.998,-21.141,75.069,-21.057,75.079,-20.986,75.074,-20.928,75.053,-20.861,74.993,-20.786,74.892,-20.795,74.806,-20.89,74.735,-20.971,74.69,-21.038,74.67,-21.038,74.654,-20.862,74.636,-20.611,74.728,-20.532,74.843,-20.417,74.975,-20.214,75.019,-19.985,74.975,-19.8,74.852,-19.538,74.625,-19.427,74.601,-19.287,74.546,-19.225,74.479,-19.242,74.4,-19.272,74.343,-19.315,74.307,-19.369,74.284,-19.467,74.269,-19.646,74.258,-20.048,74.282,-20.256,74.283,-20.231,74.205,-20.653,74.137,-21.129,74.111,-21.581,74.163,-21.955,74.244,-21.832,74.357,-21.762,74.483,-21.943,74.566,-21.983,74.567,-21.92,74.439,-21.973,74.39,-22.177,74.33,-22.322,74.303,-22.334,74.286,-22.264,74.272,-22.217,74.246,-22.196,74.206,-22.22,74.166,-22.291,74.125,-22.329,74.091,-22.335,74.063,-22.271,74.03,-22.135,73.99,-21.988,73.971,-21.298,73.962,-21.022,73.941,-20.367,73.848,-20.338,73.82,-20.449,73.653,-20.51,73.493,-20.637,73.464,-21.326,73.457,-21.548,73.432,-21.873,73.358,-22.185,73.27,-22.347,73.269,-22.987,73.346,-23.233,73.398,-23.761,73.543,-24.158,73.764,-24.34,73.672,-24.451,73.629,-24.566,73.606,-24.677,73.602,-24.784,73.618,-24.905,73.653,-25.109,73.734,-25.351,73.814,-25.521,73.852,-25.528,73.841,-25.427,73.794,-25.281,73.74,-24.909,73.58,-24.778,73.54,-24.791,73.511,-25.026,73.486,-25.311,73.431,-25.45,73.391,-25.665,73.293,-25.74,73.278,-26.062,73.253,-26.169,73.259,-26.407,73.313,-26.765,73.348,-26.977,73.38,-27.27,73.436,-27.169,73.374,-26.604,73.279,-26.542,73.249,-26.658,73.192,-26.729,73.171,-26.863,73.167,-27.062,73.179,-27.265,73.176,-27.472,73.16,-27.562,73.138,-27.533,73.113,-27.483,73.089,-27.413,73.068,-27.348,73.068,-27.19,73.132,-27.07,73.137,-26.753,73.121,-26.433,73.171,-26.202,73.193,-26.029,73.199,-25.399,73.276,-25.268,73.362,-25.057,73.396,-24.587,73.423,-24.133,73.409,-23.899,73.398,-23.71,73.317,-23.456,73.259,-23.244,73.193,-22.996,73.172,-22.852,73.084,-22.45,72.986,-22.194,72.965,-22.036,72.918,-22.023,72.721,-22.007,72.635,-22.075,72.399,-22.28,72.345,-22.239,72.22,-22.293,72.12,-22.498,72.158,-22.707,72.224,-23.208,72.327,-23.674,72.393,-23.856,72.452,-24.069,72.499,-24.359,72.687,-24.547,72.922,-24.63,73.038,-24.789,73.044,-24.992,73.013,-25.171,72.98,-25.256,72.924,-25.861,72.847,-26.08,72.794,-26.206,72.796,-26.658,72.716,-26.477,72.678,-26.392,72.673,-26.209,72.694,-26.1,72.722,-25.688,72.797,-25.357,72.81,-25.237,72.843,-24.985,72.889,-24.813,72.902,-24.789,72.89,-24.771,72.869,-24.65,72.583,-24.701,72.506,-24.837,72.473,-25.128,72.419,-25.204,72.393,-25.118,72.347,-24.844,72.39,-24.667,72.437,-24.572,72.42,-24.417,72.348,-24.242,72.311,-23.798,72.201,-23.587,72.14,-23.291,72.081,-22.956,71.999,-22.869,71.971,-22.562,71.928,-22.497,71.914,-22.37,71.77,-22.265,71.754,-21.96,71.745,-22.013,71.689,-22.311,71.565,-22.465,71.525,-22.503,71.5,-22.489,71.457,-22.48,71.383,-22.418,71.249,-22.348,71.373,-22.299,71.432,-22.234,71.45,-22.17,71.453,-21.961,71.508,-21.752,71.478,-21.698,71.337,-21.671,71.206,-21.69,71.092,-21.667,70.916,-21.675,70.856,-21.625,70.805,-21.574,70.59,-21.523,70.526,-21.626,70.469,-21.944,70.443,-22.069,70.472,-22.384,70.462,-22.385,70.513,-22.4,70.571,-22.401,70.612,-22.422,70.649,-22.437,70.86,-22.526,70.808,-22.531,70.765,-22.555,70.721,-22.61,70.493,-22.691,70.437,-22.943,70.451,-23.191,70.442,-23.328,70.451,-23.792,70.555,-23.971,70.649,-24.13,70.791,-24.229,70.923,-24.266,71.046,-24.377,71.146,-24.562,71.224,-24.781,71.286,-25.033,71.334,-25.255,71.396,-25.446,71.471,-25.656,71.53,-25.885,71.572,-26.211,71.59,-26.689,71.583,-27.011,71.631,-27.087,71.627,-27.162,71.602,-27.107,71.533,-26.737,71.501,-26.452,71.494,-26.074,71.498,-25.843,71.48,-25.758,71.44,-25.699,71.368,-25.668,71.265,-25.742,71.184,-26.014,71.093,-26.158,71.05,-26.576,70.969,-26.718,70.95,-27.067,70.945,-27.336,70.953,-27.689,70.993,-27.889,71.002,-28.303,71.007,-28.398,70.993,-28.292,70.949,-28.116,70.925,-27.992,70.895,-27.979,70.84,-28.024,70.757,-28.07,70.699,-28.146,70.656,-28.417,70.574,-28.53,70.548,-29.037,70.462,-29.072,70.445,-28.953,70.447,-28.633,70.478,-28.541,70.477,-28.015,70.402,-27.596,70.407,-26.747,70.476,-26.677,70.474,-26.622,70.463,-26.565,70.438,-26.508,70.397,-26.577,70.357,-26.771,70.319,-27.073,70.281,-27.203,70.256,-27.328,70.217,-27.561,70.124,-27.629,70.028,-27.384,69.992,-27.274,70.038,-27.144,70.141,-27.028,70.201,-26.752,70.242,-26.416,70.221,-26.156,70.246,-25.625,70.347,-25.53,70.353,-24.749,70.295,-24.041,70.181,-23.667,70.139,-23.173,70.115,-22.284,70.126,-22.207,70.108,-22.235,70.068,-22.287,70.033,-22.435,69.986,-22.615,69.954,-22.726,69.945,-22.821,69.923,-23.034,69.901,-23.088,69.883,-23.015,69.805,-23.05,69.793,-23.237,69.791,-23.553,69.741,-23.812,69.744,-23.866,69.737,-23.817,69.718,-23.764,69.681,-23.709,69.627,-23.739,69.589,-23.856,69.566,-23.944,69.558,-24.248,69.59,-24.297,69.586,-24.252,69.562,-24.227,69.527,-24.221,69.479,-24.296,69.439,-24.451,69.407,-24.741,69.318,-24.867,69.293,-25.133,69.272,-25.189,69.261,-25.08,69.192,-25.092,69.165,-25.272,69.092,-25.544,69.046,-25.581,69.021,-25.606,68.954,-25.626,68.928,-25.698,68.89,-25.956,68.817,-26.139,68.781,-26.229,68.752,-26.341,68.702,-26.483,68.676,-26.654,68.673,-26.815,68.654,-27.081,68.602,-27.266,68.584,-27.851,68.494,-28.126,68.479,-28.365,68.447,-28.854,68.36,-29.088,68.332,-29.25,68.299,-29.426,68.289,-29.714,68.311,-29.869,68.312,-29.964,68.299,-30.051,68.272,-30.196,68.199,-30.318,68.193,-30.72,68.251,-30.712,68.225,-30.606,68.162,-30.611,68.118,-30.85,68.073,-30.979,68.061,-31.168,68.08,-31.419,68.128,-31.742,68.23,-32.137,68.385,-32.327,68.437,-32.314,68.388,-32.27,68.339,-32.195,68.292,-32.18,68.257,-32.224,68.236,-32.282,68.225,-32.355,68.225,-32.367,68.213,-32.249,68.139,-32.156,68.063,-32.165,67.991,-32.275,67.923,-32.37,67.883,-32.449,67.871,-32.918,67.701,-33.049,67.679,-33.108,67.658,-33.157,67.627,-33.294,67.486,-33.349,67.443,-33.458,67.387,-33.504,67.377,-33.518,67.354,-33.498,67.318,-33.528,67.258,-33.608,67.174,-33.881,66.942,-34.102,66.726,-34.198,66.655,-34.269,66.625,-34.314,66.636,-34.423,66.63,-34.476,66.592,-34.524,66.523,-34.576,66.471,-34.633,66.435,-35.075,66.279,-35.189,66.25,-35.291,66.269,-35.412,66.262,-35.662,66.344,-35.705,66.374,-35.867,66.441,-35.862,66.406,-35.835,66.387,-35.812,66.358,-35.756,66.324,-35.63,66.14,-35.729,66.102,-35.818,66.059,-36.044,65.987,-36.289,65.865,-36.379,65.831,-36.399,65.93,-36.389,65.96,-36.527,66.008,-36.523,65.973,-36.537,65.941,-36.637,65.812,-36.665,65.79,-36.715,65.795,-36.822,65.771,-36.932,65.783,-37.026,65.841,-37.063,65.871,-37.233,65.788,-37.316,65.79,-37.33,65.72,-37.41,65.656,-37.516,65.629,-37.664,65.631,-37.754,65.593,-37.955,65.634,-38.001,65.71,-37.842,65.814,-37.797,65.857,-37.827,65.91,-37.788,65.978,-37.484,66.195,-37.279,66.304,-37.291,66.324,-37.57,66.348,-37.814,66.385,-38.052,66.398,-38.157,66.386,-37.989,66.323,-37.752,66.262,-37.869,66.203,-37.969,66.141,-38.073,65.973,-38.14,65.904,-38.398,65.983,-38.52,66.01,-38.443,65.922,-38.216,65.838,-38.202,65.811,-38.203,65.712,-38.637,65.624,-39.089,65.611,-39.413,65.586,-39.961,65.556,-40.174,65.556,-40.192,65.523,-39.656,65.369,-39.578,65.341,-39.653,65.288,-39.763,65.255,-39.937,65.142,-40.028,65.103,-40.253,65.049,-40.668,65.109,-40.881,65.082,-41.084,65.101,-41.089,65.035,-41.028,64.988,-40.966,64.869,-40.829,64.878,-40.655,64.915,-40.521,64.825,-40.433,64.673,-40.278,64.596,-40.21,64.536,-40.182,64.48,-40.278,64.424,-40.478,64.344,-40.699,64.33,-40.686,64.267,-40.782,64.222,-40.985,64.235,-41.079,64.267,-41.178,64.281,-41.581,64.298,-41.175,64.177,-41.031,64.121,-40.966,64.154,-40.826,64.163,-40.618,64.132,-40.652,63.928,-40.561,63.762,-40.55,63.725,-40.772,63.626,-40.775,63.534,-40.907,63.508,-41.049,63.514,-41.056,63.412,-41.152,63.349,-41.135,63.309,-41.108,63.274,-41.195,63.209,-41.275,63.131,-41.388,63.062,-41.448,63.069,-41.628,63.065,-41.844,63.07,-42.02,63.16,-42.092,63.189,-42.175,63.209,-42.143,63.151,-42.094,63.117,-41.932,63.052,-41.634,62.972,-41.644,62.916,-41.723,62.891,-41.909,62.737,-41.975,62.734,-42.058,62.694,-42.316,62.707,-42.424,62.723,-42.741,62.713,-42.849,62.727,-42.942,62.72,-42.855,62.677,-42.674,62.638,-42.467,62.598,-42.153,62.568,-42.164,62.512,-42.243,62.466,-42.198,62.397,-42.233,62.348,-42.248,62.289,-42.321,62.153,-42.236,62.059,-42.143,62.014,-42.154,61.953,-42.11,61.857,-42.25,61.771,-42.365,61.775,-42.53,61.755,-42.585,61.717,-42.324,61.682,-42.347,61.617,-42.419,61.537,-42.494,61.363,-42.646,61.064,-42.717,60.767,-43.044,60.524,-43.16,60.517,-43.189,60.507,-43.348,60.52,-43.598,60.576,-43.792,60.595,-43.923,60.595,-43.94,60.567,-43.831,60.522,-43.665,60.503,-43.533,60.473,-43.296,60.445,-43.213,60.391,-43.156,60.333,-43.165,60.301,-43.165,60.263,-43.123,60.061,-43.235,59.991,-43.32,59.928,-43.617,59.937,-43.669,59.959,-43.955,60.025,-43.937,59.994,-43.73,59.904,-43.658,59.859,-43.706,59.849,-43.79,59.846,-43.907,59.815,-44.117,59.832,-44.105,59.878,-44.065,59.925,-44.162,59.917,-44.269,59.893,-44.331,59.902,-44.384,59.899,-44.413,59.923,-44.453,60.015,-44.405,60.061,-44.231,60.18,-44.176,60.244,-44.224,60.274,-44.348,60.205,-44.476,60.096,-44.533,60.029,-44.613,60.017,-44.812,60.05,-45.379,60.203,-45.362,60.296,-45.368,60.373,-45.202,60.383,-45.082,60.416,-44.975,60.457,-44.854,60.532,-44.742,60.655,-44.757,60.665,-45.083,60.507,-45.283,60.455,-45.381,60.445,-45.429,60.468,-45.59,60.519,-45.695,60.542,-45.934,60.579,-45.977,60.6,-46.047,60.616,-46.142,60.777,-46.019,60.972,-45.934,61.028,-45.88,61.094,-45.849,61.181,-45.87,61.218,-45.942,61.206,-45.976,61.176,-45.97,61.129,-46.012,61.097,-46.297,61.022,-46.582,60.962,-46.718,60.905,-46.806,60.86,-46.874,60.816,-46.98,60.82,-47.125,60.811,-47.224,60.783,-47.37,60.8,-47.465,60.843,-47.579,60.847,-47.707,60.827,-47.796,60.829,-47.789,60.8,-47.73,60.729,-47.828,60.725,-48.014,60.722,-48.108,60.742,-48.181,60.769,-48.242,60.807,-48.205,60.856,-47.906,60.946,-47.77,60.998,-47.859,61.016,-48.146,60.999,-48.194,61.013,-48.386,61.005,-48.378,61.138,-48.425,61.172,-48.428,61.187,-48.495,61.225,-48.558,61.234,-48.597,61.247,-48.922,61.277,-48.965,61.352,-48.987,61.429,-49.049,61.524,-49.205,61.549,-49.289,61.59,-49.222,61.632,-49.193,61.686,-49.265,61.71,-49.311,61.748,-49.304,61.772,-49.363,61.839,-49.38,61.89,-49.313,61.939,-49.13,61.993,-49.071,62.015,-49.04,62.039,-48.829,62.08,-49.008,62.108,-49.12,62.113,-49.202,62.099,-49.278,62.046,-49.349,62.01,-49.624,61.999,-49.664,62.017,-49.683,62.093,-49.668,62.151,-49.553,62.233,-49.685,62.273,-49.806,62.287,-49.943,62.324,-50.07,62.365,-50.179,62.411,-50.285,62.466,-50.319,62.473,-50.281,62.531,-50.259,62.578,-50.256,62.68,-50.299,62.722,-50.204,62.809,-50.076,62.904,-49.793,63.045,-50.092,62.977,-50.338,62.829,-50.39,62.822,-50.408,62.849,-50.502,62.945,-50.572,62.971,-50.604,63,-50.744,63.051,-50.804,63.091,-50.89,63.167,-51.013,63.258,-51.188,63.436,-51.469,63.642,-51.538,63.758,-51.451,63.905,-51.548,64.006,-51.28,64.053,-50.898,64.106,-50.699,64.149,-50.585,64.162,-50.342,64.17,-50.261,64.214,-50.396,64.203,-50.487,64.209,-50.492,64.229,-50.459,64.266,-50.437,64.313,-50.483,64.304,-50.721,64.223,-51.072,64.159,-51.347,64.123,-51.391,64.125,-51.487,64.103,-51.542,64.097,-51.585,64.103,-51.682,64.165,-51.708,64.205,-51.534,64.314,-51.404,64.463,-51.232,64.561,-51.11,64.573,-50.907,64.568,-50.835,64.559,-50.858,64.617,-50.849,64.645,-50.684,64.678,-50.492,64.693,-50.355,64.683,-50.269,64.615,-50.158,64.49,-50.009,64.447,-50.016,64.507,-50.093,64.585,-50.122,64.704,-50.22,64.754,-50.299,64.779,-50.517,64.767,-50.648,64.853,-50.678,64.885,-50.681,64.928,-50.812,65.052,-50.854,65.114,-50.924,65.197,-50.961,65.201,-50.914,65.097,-50.852,65.024,-50.765,64.863,-50.722,64.798,-50.78,64.746,-50.891,64.695,-50.989,64.665,-51.221,64.628,-51.171,64.708,-51.139,64.786,-51.255,64.758,-51.364,64.702,-51.401,64.623,-51.47,64.552,-51.677,64.377,-51.758,64.28,-51.835,64.232,-51.923,64.219,-51.999,64.257,-52.063,64.346,-52.093,64.416,-52.097,64.597,-52.089,64.682,-52.124,64.795,-52.235,65.061,-52.259,65.155,-52.448,65.205,-52.45,65.221,-52.5,65.275,-52.538,65.329,-52.506,65.348,-52.461,65.363,-52.18,65.442,-51.971,65.531,-51.721,65.67,-51.619,65.713,-51.253,65.746,-51.09,65.751,-51.092,65.776,-51.146,65.786,-51.394,65.779,-51.723,65.723,-51.78,65.703,-51.924,65.617,-52.035,65.569,-52.348,65.461,-52.551,65.461,-52.761,65.591,-52.995,65.566,-53.153,65.575,-53.199,65.594,-53.234,65.771,-53.106,65.977,-53.244,65.979,-53.272,65.987,-53.345,66.034,-53.392,66.048,-53.357,66.073,-53.018,66.171,-52.511,66.362,-52.293,66.438,-52.158,66.47,-52.056,66.507,-51.932,66.588,-51.891,66.623,-51.822,66.652,-51.676,66.684,-51.517,66.732,-51.259,66.841,-51.225,66.882,-51.281,66.891,-51.402,66.854,-51.648,66.754,-51.823,66.698,-52.421,66.447,-52.676,66.355,-52.814,66.297,-52.922,66.241,-53.036,66.201,-53.156,66.178,-53.413,66.16,-53.539,66.139,-53.615,66.154,-53.648,66.274,-53.623,66.344,-53.635,66.414,-53.571,66.513,-53.476,66.584,-53.436,66.622,-53.419,66.649,-53.223,66.721,-53.115,66.754,-53.038,66.827,-52.603,66.853,-52.491,66.85,-52.431,66.86,-52.387,66.881,-52.43,66.898,-52.56,66.909,-52.907,66.907,-53.227,66.919,-53.373,66.932,-53.444,66.925,-53.56,66.946,-53.687,66.986,-53.884,67.136,-53.805,67.327,-53.799,67.418,-53.548,67.498,-53.414,67.525,-53.224,67.585,-52.97,67.687,-52.666,67.75,-52.512,67.761,-52.384,67.752,-51.909,67.664,-51.665,67.646,-51.451,67.668,-51.181,67.637,-50.705,67.509,-50.613,67.528,-50.64,67.559,-51.171,67.694,-51.168,67.734,-51.032,67.744,-50.887,67.784,-50.969,67.807,-51.321,67.787,-51.423,67.754,-51.765,67.738,-51.944,67.765,-52.104,67.779,-52.345,67.837,-52.546,67.818,-52.673,67.795,-52.898,67.773,-52.98,67.758,-53.419,67.575,-53.604,67.536,-53.735,67.549,-53.643,67.668,-53.616,67.715,-53.616,67.767,-53.578,67.837,-53.353,67.971,-53.211,68.117,-53.152,68.208,-53.041,68.218,-52.89,68.205,-52.436,68.146,-52.058,68.075,-51.78,68.057,-51.597,68.055,-51.518,68.077,-51.456,68.116,-51.433,68.143,-51.415,68.198,-51.394,68.218,-51.333,68.242,-51.207,68.326,-51.169,68.385,-51.21,68.42,-51.293,68.416,-51.456,68.394,-51.478,68.384,-51.475,68.365,-51.632,68.273,-51.804,68.252,-52.199,68.221,-52.379,68.219,-52.698,68.262,-52.747,68.278,-52.78,68.31,-53.173,68.303,-53.29,68.293,-53.383,68.297,-53.337,68.352,-53.213,68.413,-53.039,68.611,-52.894,68.662,-52.605,68.709,-52.303,68.701,-51.781,68.548,-51.623,68.535,-51.479,68.547,-51.133,68.598,-51.07,68.619,-50.946,68.683,-50.801,68.791,-50.808,68.817,-51.03,68.756,-51.149,68.74,-51.249,68.74,-51.156,68.938,-51.12,69.091,-51.085,69.128,-50.792,69.117,-50.393,69.137,-50.297,69.171,-50.299,69.185,-50.459,69.206,-50.537,69.248,-50.671,69.234,-50.851,69.206,-51.077,69.209,-51.058,69.275,-50.892,69.412,-50.875,69.474,-50.811,69.599,-50.804,69.663,-50.72,69.725,-50.459,69.77,-50.349,69.796,-50.343,69.825,-50.5,69.936,-50.46,69.966,-50.337,69.994,-50.292,70.014,-50.323,70.027,-50.436,70.039,-50.61,70.015,-50.802,70.003,-50.973,70.04,-51.106,70.057,-51.19,70.052,-51.419,69.989,-51.499,69.987,-51.598,70.005,-52.255,70.059,-52.336,70.078,-52.571,70.172,-52.765,70.234,-53.023,70.302,-53.358,70.353,-53.769,70.389,-54.014,70.422,-54.136,70.468,-54.343,70.571,-54.501,70.657,-54.531,70.699,-54.438,70.752,-54.344,70.789,-54.166,70.82,-53.859,70.81,-53.694,70.796,-53.513,70.767,-53.376,70.761,-53.091,70.769,-52.802,70.751,-52.63,70.73,-52.405,70.687,-51.784,70.503,-51.524,70.439,-51.412,70.432,-50.947,70.364,-50.872,70.365,-50.682,70.397,-50.663,70.418,-50.728,70.438,-50.933,70.454,-51.173,70.529,-51.323,70.589,-51.34,70.688,-51.32,70.743,-51.283,70.768,-51.257,70.853,-51.396,70.903,-51.494,70.919,-51.753,70.992,-51.774,71.01,-51.65,71.019,-51.528,71.014,-51.267,70.977,-51.13,70.972,-51.03,70.986,-51.019,71.001,-51.178,71.043,-51.377,71.119,-51.792,71.13,-52.061,71.122,-52.234,71.148,-52.417,71.19,-52.535,71.2,-52.775,71.174,-52.897,71.171,-53.008,71.18,-53.117,71.313,-53.088,71.353,-53.002,71.37,-52.937,71.413,-52.892,71.458,-52.749,71.502,-51.967,71.599,-51.77,71.672,-51.779,71.683,-51.912,71.669,-52.082,71.637,-52.196,71.63,-52.656,71.672,-52.728,71.663,-52.915,71.602,-53.168,71.536,-53.284,71.54,-53.44,71.579,-53.465,71.607,-53.476,71.64,-53.305,71.686,-53.25,71.71,-53.139,71.775,-53.145,71.807,-53.334,71.79,-53.358,71.82,-53.355,71.871,-53.374,71.936,-53.42,72,-53.575,72.098,-53.64,72.123,-53.693,72.16,-53.81,72.293,-53.776,72.326,-53.672,72.351,-53.652,72.363,-53.901,72.342,-53.928,72.319,-53.881,72.285,-53.847,72.24,-53.828,72.183,-53.793,72.134,-53.703,72.08,-53.631,72.052,-53.514,71.976,-53.462,71.894,-53.478,71.85,-53.569,71.806,-53.715,71.758,-53.76,71.718,-53.78,71.679,-53.894,71.642,-53.964,71.656,-54.02,71.658,-53.954,71.593,-53.912,71.526,-53.963,71.459,-54.099,71.419,-54.173,71.417,-54.318,71.384,-54.689,71.367,-54.818,71.375,-55.055,71.409,-55.336,71.427,-55.448,71.472,-55.594,71.554,-55.668,71.627,-55.669,71.692,-55.63,71.739,-55.549,71.768,-55.452,71.958,-55.316,72.111,-54.971,72.268,-54.873,72.325,-54.84,72.356,-54.841,72.379,-54.896,72.394,-55.32,72.2,-55.581,72.179,-55.659,72.223,-55.636,72.3,-55.589,72.319,-55.378,72.311,-55.296,72.354,-55.428,72.42,-55.569,72.437,-55.602,72.453,-55.457,72.503,-55.122,72.5,-55.046,72.534,-54.925,72.572,-54.79,72.642,-54.74,72.7,-54.729,72.75,-54.758,72.791,-54.761,72.832,-54.738,72.873,-54.773,72.918,-54.866,72.967,-55.073,73.015,-55.134,72.961,-55.198,72.938,-55.289,72.933,-55.372,72.956,-55.46,72.964,-55.545,72.985,-55.634,72.991,-55.669,73.008,-55.691,73.054,-55.693,73.113,-55.592,73.14,-55.452,73.162,-55.359,73.203,-55.297,73.262,-55.288,73.327,-55.332,73.397,-55.446,73.46,-55.656,73.399,-55.739,73.384,-55.758,73.428,-55.787,73.46,-55.876,73.505,-55.992,73.537,-56.104,73.558,-56.109,73.591,-56.083,73.627,-56.033,73.67,-55.968,73.76,-55.897,73.752,-55.838,73.76,-55.872,73.833,-55.929,73.895,-55.997,73.931,-55.999,73.946,-56.014,73.964,-56.066,74.007,-56.124,74.039,-56.225,74.129,-56.298,74.163,-56.392,74.181,-56.493,74.182,-56.655,74.159,-56.954,74.131,-57.191,74.118,-57.231,74.125,-57.112,74.159,-56.937,74.195,-56.706,74.219,-56.639,74.278,-56.664,74.33,-56.654,74.378,-56.718,74.429,-56.656,74.458,-56.446,74.486,-56.35,74.49,-56.255,74.527,-56.522,74.614,-56.801,74.672,-56.871,74.695,-56.933,74.733,-56.986,74.787,-57.072,74.84,-57.191,74.894,-57.365,74.945,-57.813,75.04,-57.967,75.105,-58.109,75.205,-58.18,75.247,-58.253,75.279,-58.566,75.353,-58.603,75.385,-58.281,75.472,-58.25,75.507,-58.381,75.612,-58.516,75.689,-58.663,75.716,-58.881,75.73,-59.082,75.765,-59.264,75.819,-59.445,75.859,-59.717,75.896,-60.173,75.993,-60.875,76.097,-61.188,76.158,-61.375,76.18,-61.621,76.186,-62.097,76.242,-62.496,76.26,-62.743,76.252,-62.823,76.262,-63.006,76.319,-63.291,76.352,-63.439,76.339,-63.622,76.278,-63.843,76.217,-63.96,76.209,-64.135,76.265,-64.223,76.303,-64.307,76.317,-64.387,76.304,-64.543,76.253,-64.692,76.216,-64.912,76.173,-65.088,76.152,-65.313,76.146,-65.37,76.131,-65.457,76.13,-65.574,76.144,-65.683,76.173,-65.785,76.215,-65.876,76.238,-65.954,76.242,-66.134,76.22,-66.362,76.155,-66.466,76.139,-66.553,76.146,-66.66,76.166,-66.874,76.218,-66.993,76.213,-67.079,76.195,-67.055,76.152,-66.854,76.05,-66.675,75.977,-66.826,75.969,-68.149,76.067,-68.317,76.091,-68.561,76.15,-68.763,76.187,-69.108,76.281,-69.373,76.332,-69.461,76.372,-69.484,76.399,-69.4,76.436,-68.865,76.561,-68.661,76.587,-68.245,76.617,-68.147,76.636,-68.114,76.651,-68.223,76.678,-68.767,76.668,-69.252,76.686,-69.674,76.736,-69.747,76.752,-69.819,76.783,-69.888,76.827,-69.872,76.877,-69.771,76.931,-69.712,76.969,-69.694,76.989,-70.224,76.855,-70.441,76.807,-70.613,76.822,-70.734,76.844,-70.793,76.869,-70.791,76.896,-70.771,76.917,-70.735,76.929,-71.015,76.985,-71.141,77.029,-71.155,77.074,-71.055,77.121,-70.958,77.154,-70.863,77.175,-70.604,77.194,-69.657,77.229,-68.978,77.195,-68.747,77.307,-68.592,77.343,-68.136,77.38,-67.434,77.385,-66.938,77.364,-66.706,77.338,-66.389,77.28,-66.371,77.298,-66.448,77.35,-66.453,77.393,-66.325,77.468,-66.266,77.515,-66.306,77.565,-66.445,77.616,-66.691,77.681,-66.824,77.687,-66.971,77.671,-67.147,77.635,-67.515,77.543,-67.688,77.524,-67.977,77.519,-68.137,77.53,-68.292,77.544,-68.533,77.593,-68.622,77.602,-68.728,77.581,-68.854,77.529,-68.975,77.493,-69.091,77.472,-69.2,77.463,-69.351,77.467,-69.977,77.548,-70.118,77.583,-70.126,77.638,-70.318,77.69,-70.535,77.7,-70.562,77.717,-70.287,77.798,-70.081,77.831,-70.114,77.841,-70.412,77.843,-70.614,77.8,-70.729,77.793,-70.994,77.792,-71.272,77.813,-71.39,77.832,-71.512,77.875,-71.65,77.9,-72.065,77.937,-72.159,77.957,-72.247,77.99,-72.586,78.085,-72.792,78.155,-72.818,78.194,-72.581,78.279,-72.571,78.299,-72.672,78.335,-72.715,78.362,-72.68,78.4,-72.473,78.482,-72.396,78.504,-72.024,78.553,-71.651,78.623,-71.516,78.639,-71.395,78.643,-70.906,78.638,-70.754,78.656,-70.625,78.69,-70.414,78.725,-69.974,78.778,-68.993,78.857,-68.93,78.867,-68.924,78.882,-69.012,78.923,-69.031,78.943,-68.83,78.98,-68.377,79.038,-68.068,79.066,-67.868,79.068,-67.708,79.08,-67.482,79.117,-67.355,79.123,-66.584,79.138,-66.243,79.118,-66.075,79.118,-65.968,79.132,-65.826,79.174,-65.56,79.276,-65.42,79.34,-65.288,79.437,-65.117,79.589,-64.989,79.737,-64.905,79.881,-64.839,79.969,-64.792,80.001,-64.632,80.041,-64.466,80.072,-64.179,80.099,-64.205,80.112,-64.327,80.134,-64.44,80.142,-64.545,80.137,-64.735,80.104,-64.982,80.082,-65.222,80.086,-65.395,80.078,-65.553,80.048,-65.81,80.024,-65.982,80.029,-66.292,80.072,-66.448,80.08,-66.844,80.076,-66.959,80.092,-67.061,80.123,-67.141,80.166,-67.201,80.222,-67.193,80.28,-67.051,80.385,-66.996,80.413,-66.61,80.53,-66.372,80.584,-66.136,80.625,-65.963,80.649,-65.801,80.66,-65.645,80.685,-65.358,80.767,-65.062,80.836,-64.694,80.966,-64.516,81,-63.892,81.056,-63.722,81.057,-63.578,81.043,-63.442,81.014,-63.059,80.886,-63.029,80.89,-63.095,80.938,-63.235,81.083,-63.212,81.143,-62.993,81.207,-62.903,81.218,-62.672,81.214,-62.299,81.194,-62.049,81.173,-61.86,81.138,-61.636,81.116,-61.519,81.117,-61.436,81.134,-61.317,81.188,-61.162,81.281,-61.1,81.396,-61.131,81.532,-61.176,81.632,-61.236,81.695,-61.203,81.747,-61.015,81.81,-60.843,81.855,-60.432,81.92,-60.099,81.937,-59.902,81.933,-59.642,81.903,-59.282,81.884,-58.957,81.825,-58.43,81.69,-58.08,81.622,-57.79,81.592,-57.505,81.54,-57.083,81.43,-56.862,81.383,-56.731,81.366,-56.615,81.363,-56.658,81.394,-56.86,81.46,-57.168,81.532,-57.853,81.662,-58.23,81.754,-58.568,81.858,-58.817,81.92,-59.268,81.982,-59.262,82.007,-58.717,82.093,-57.717,82.168,-56.589,82.227,-56.212,82.221,-55.549,82.246,-55.486,82.283,-55.344,82.3,-54.726,82.351,-54.549,82.351,-54.277,82.326,-53.987,82.279,-53.853,82.237,-53.671,82.164,-53.582,82.062,-53.596,81.738,-53.591,81.677,-53.556,81.653,-53.43,81.688,-53.28,81.754,-53.145,81.8,-53.041,81.871,-52.969,81.967,-52.926,82.038,-53.102,82.119,-53.111,82.251,-53.023,82.322,-52.776,82.322,-51.754,82.078,-51.352,82.026,-50.894,81.895,-50.36,81.909,-49.867,81.893,-49.649,81.898,-49.541,81.918,-49.694,81.972,-50.395,82.121,-50.713,82.237,-50.936,82.383,-50.99,82.46,-50.82,82.474,-50.037,82.472,-48.861,82.405,-47.357,82.174,-46.617,82.097,-45.291,81.829,-44.891,81.788,-44.729,81.78,-44.608,81.813,-44.532,81.849,-44.527,81.897,-44.591,81.957,-44.628,82.026,-44.637,82.104,-44.547,82.26,-44.333,82.311,-44.239,82.368,-44.327,82.472,-44.577,82.543,-45.552,82.725,-45.557,82.747,-45.36,82.771,-45.067,82.785,-42.651,82.741,-42.233,82.725,-42.055,82.71,-41.977,82.689,-41.876,82.68,-41.357,82.705,-41.37,82.75,-41.434,82.779,-44.239,82.857,-44.762,82.884,-45.028,82.886,-45.303,82.865,-45.873,82.855,-46.137,82.859,-46.478,82.952,-46.169,83.064,-45.909,83.061,-45.415,83.018,-45.122,83.079,-44.657,83.129,-44.197,83.147,-43.195,83.255,-43.009,83.265,-42.776,83.259,-42.26,83.232,-42.055,83.205,-41.82,83.148,-41.683,83.13,-41.522,83.127,-41.3,83.101,-40.979,83.185,-40.689,83.275,-40.357,83.332,-39.886,83.299,-39.588,83.256,-39.316,83.204,-38.931,83.175,-38.278,82.999,-38.156,82.999,-38.099,83.014,-38.037,83.046,-38.015,83.095,-37.935,83.161,-37.993,83.185,-38.54,83.258,-38.643,83.286,-38.748,83.333,-38.75,83.371,-38.648,83.401,-38.541,83.415,-38.188,83.402,-38.071,83.412,-37.961,83.438,-37.828,83.486,-37.723,83.498,-37.487,83.499,-37.123,83.468,-36.804,83.466,-36.69,83.48,-36.672,83.51,-36.644,83.529,-36.606,83.537,-35.452,83.539,-35.166,83.546,-34.942,83.568,-34.668,83.571,-34.428,83.558,-34.132,83.529,-33.837,83.53,-33.398,83.577,-32.984,83.6,-30.703,83.593,-29.953,83.565]],[[-52.731,69.945,-52.398,69.863,-52.045,69.807,-52.011,69.782,-51.983,69.743,-51.977,69.722,-51.985,69.704,-52.007,69.686,-51.982,69.664,-51.908,69.637,-51.9,69.605,-51.988,69.55,-52.113,69.489,-52.77,69.364,-53.003,69.343,-53.578,69.257,-53.754,69.26,-53.793,69.264,-53.902,69.302,-54.051,69.337,-54.121,69.364,-54.183,69.404,-54.158,69.428,-54.047,69.437,-53.89,69.437,-53.658,69.465,-53.722,69.491,-53.783,69.506,-53.825,69.54,-53.921,69.534,-53.994,69.553,-54.133,69.565,-54.497,69.577,-54.734,69.611,-54.804,69.631,-54.866,69.665,-54.919,69.714,-54.841,69.902,-54.788,69.95,-54.665,69.966,-54.363,69.924,-54.323,69.942,-54.652,70.011,-54.774,70.053,-54.809,70.085,-54.831,70.133,-54.83,70.161,-54.816,70.189,-54.786,70.218,-54.706,70.256,-54.372,70.317,-54.007,70.296,-53.375,70.221,-53.297,70.205,-53.103,70.141,-52.731,69.945]],[[-51.014,69.552,-51.17,69.517,-51.202,69.525,-51.234,69.552,-51.315,69.674,-51.339,69.732,-51.319,69.804,-51.35,69.855,-51.209,69.914,-51.095,69.924,-50.94,69.909,-50.679,69.849,-50.698,69.829,-50.754,69.798,-50.912,69.757,-50.967,69.664,-50.978,69.618,-50.97,69.583,-51.014,69.552]],[[-53.535,71.041,-53.629,71.034,-53.898,71.085,-53.941,71.104,-53.958,71.128,-53.947,71.156,-53.862,71.207,-53.701,71.283,-53.584,71.297,-53.512,71.25,-53.441,71.186,-53.432,71.153,-53.437,71.115,-53.455,71.083,-53.488,71.056,-53.535,71.041]],[[-55.017,72.791,-55.158,72.723,-55.274,72.684,-55.524,72.568,-55.567,72.564,-55.634,72.579,-55.687,72.61,-55.781,72.617,-55.814,72.636,-55.827,72.652,-55.869,72.662,-55.936,72.668,-56.043,72.656,-56.141,72.668,-56.215,72.719,-56.078,72.753,-55.994,72.782,-55.666,72.794,-55.574,72.78,-55.516,72.781,-55.428,72.789,-55.235,72.825,-55.206,72.842,-55.033,72.821,-55.017,72.791]],[[-71.667,77.325,-72.024,77.316,-72.374,77.355,-72.495,77.386,-72.49,77.432,-72.436,77.448,-72.247,77.464,-72.089,77.467,-71.983,77.46,-71.733,77.432,-71.552,77.403,-71.433,77.394,-71.467,77.354,-71.667,77.325]],[[-44.865,82.084,-45.067,82.066,-45.491,82.172,-46.161,82.278,-46.752,82.348,-47.308,82.533,-47.351,82.599,-47.272,82.657,-46.787,82.666,-46.399,82.692,-45.411,82.578,-44.917,82.481,-44.75,82.401,-44.776,82.242,-44.865,82.084]],[[-25.432,70.921,-25.397,70.862,-25.394,70.835,-25.401,70.811,-25.421,70.795,-25.468,70.78,-25.38,70.741,-25.352,70.714,-25.346,70.693,-25.402,70.653,-25.801,70.599,-25.911,70.573,-26.05,70.509,-26.218,70.454,-26.274,70.454,-26.339,70.511,-26.605,70.553,-27.105,70.531,-27.69,70.479,-27.898,70.454,-28.003,70.467,-28.035,70.487,-28.037,70.514,-27.968,70.595,-27.94,70.615,-27.805,70.642,-27.714,70.713,-27.744,70.79,-27.709,70.897,-27.617,70.914,-27.387,70.876,-27.239,70.868,-26.976,70.863,-26.622,70.876,-26.337,70.919,-25.819,71.044,-25.727,71.042,-25.661,70.998,-25.612,70.976,-25.458,70.943,-25.432,70.921]],[[-18.665,81.846,-18.768,81.814,-19.031,81.827,-19.369,81.917,-19.594,81.991,-19.611,82.078,-19.495,82.117,-19.315,82.123,-19.067,82.049,-18.813,81.949,-18.665,81.846]],[[-17.612,79.826,-18.036,79.711,-18.662,79.72,-19.032,79.773,-19.138,79.852,-18.997,79.94,-18.547,80.011,-17.983,80.055,-17.471,80.029,-17.401,79.94,-17.612,79.826]],[[-18.997,77.974,-19.129,77.939,-19.218,78.044,-19.297,78.185,-19.315,78.344,-19.112,78.424,-19.006,78.441,-18.935,78.424,-18.953,78.353,-18.953,78.212,-18.882,78.115,-18.997,77.974]],[[-18.583,76.042,-18.697,76.016,-19.085,76.43,-19.085,76.58,-19.059,76.695,-18.882,76.704,-18.733,76.642,-18.662,76.404,-18.583,76.042]],[[-18.001,75.407,-17.921,75.302,-17.886,75.204,-17.762,75.143,-17.498,75.152,-17.392,75.037,-17.586,74.993,-18.353,75.01,-18.671,75.002,-18.891,75.072,-18.882,75.196,-18.856,75.319,-18.636,75.39,-18.45,75.328,-18.23,75.372,-18.001,75.407]],[[-17.954,77.642,-18.148,77.642,-18.22,77.668,-18.174,77.714,-17.904,77.863,-17.814,77.875,-17.681,77.859,-17.641,77.782,-17.729,77.706,-17.954,77.642]],[[-37.031,65.532,-37.187,65.531,-37.238,65.61,-37.223,65.695,-37.048,65.722,-36.953,65.663,-36.987,65.576,-37.031,65.532]],[[-46.267,60.781,-46.382,60.66,-46.496,60.687,-46.553,60.741,-46.666,60.766,-46.788,60.758,-46.79,60.78,-46.394,60.909,-46.205,60.944,-46.219,60.889,-46.254,60.842,-46.267,60.781]],[[-51.675,70.855,-51.809,70.853,-52.119,70.871,-52.144,70.882,-52.148,70.904,-52.107,70.968,-51.97,70.976,-51.807,70.942,-51.631,70.892,-51.607,70.869,-51.675,70.855]]]},{"n":"Faroe Islands","c":"FO","r":6,"ml":4,"lx":-7.06,"ly":62.19,"b":[-7.423,61.414,-6.406,62.356],"p":[[[-6.623,61.806,-6.643,61.768,-6.67,61.769,-6.764,61.815,-6.839,61.841,-6.864,61.862,-6.885,61.899,-6.842,61.904,-6.791,61.895,-6.662,61.862,-6.626,61.827,-6.623,61.806]],[[-6.699,61.445,-6.68,61.414,-6.703,61.418,-6.771,61.452,-6.888,61.535,-6.929,61.603,-6.935,61.634,-6.906,61.631,-6.882,61.603,-6.77,61.584,-6.741,61.571,-6.741,61.536,-6.704,61.496,-6.699,61.445]],[[-7.187,62.139,-7.097,62.101,-7.065,62.073,-7.117,62.047,-7.179,62.04,-7.255,62.046,-7.379,62.075,-7.423,62.14,-7.337,62.139,-7.235,62.151,-7.187,62.139]],[[-6.631,62.228,-6.656,62.094,-6.696,62.094,-6.769,62.131,-6.823,62.139,-6.841,62.119,-6.838,62.095,-6.809,62.08,-6.723,61.99,-6.714,61.964,-6.725,61.951,-6.81,61.977,-7.014,62.094,-7.172,62.286,-6.959,62.316,-6.804,62.266,-6.631,62.228]],[[-6.406,62.259,-6.454,62.187,-6.525,62.198,-6.544,62.206,-6.559,62.225,-6.552,62.278,-6.555,62.356,-6.473,62.292,-6.406,62.259]]]},{"n":"Denmark","c":"DK","r":4,"ml":3,"lx":9.02,"ly":55.97,"b":[8.121,54.629,15.137,57.737],"p":[[[12.569,55.785,12.571,55.685,12.545,55.656,12.507,55.637,12.407,55.616,12.321,55.588,12.243,55.538,12.215,55.467,12.275,55.414,12.385,55.386,12.413,55.286,12.322,55.237,12.09,55.188,12.066,55.07,12.073,54.977,12.069,54.909,12.05,54.815,11.862,54.773,11.741,54.915,11.74,54.972,11.704,55.039,11.697,55.096,11.654,55.187,11.476,55.212,11.407,55.215,11.31,55.198,11.286,55.204,11.171,55.329,11.19,55.466,11.128,55.535,11.12,55.566,11.121,55.601,11.07,55.629,11.009,55.644,10.979,55.722,11.05,55.74,11.224,55.731,11.275,55.736,11.322,55.753,11.464,55.879,11.46,55.907,11.475,55.943,11.628,55.957,11.696,55.908,11.682,55.829,11.691,55.729,11.784,55.702,11.82,55.698,11.858,55.772,11.885,55.808,11.922,55.828,11.935,55.896,11.913,55.937,11.866,55.968,12.04,56.052,12.219,56.119,12.323,56.122,12.428,56.106,12.526,56.083,12.579,56.064,12.608,56.033,12.543,55.959,12.525,55.918,12.569,55.785]],[[9.74,54.826,9.725,54.826,9.661,54.834,9.616,54.855,9.499,54.84,9.342,54.806,9.255,54.808,9.186,54.845,8.903,54.897,8.857,54.901,8.671,54.903,8.67,54.903,8.661,54.986,8.638,55.046,8.573,55.134,8.67,55.156,8.651,55.329,8.616,55.418,8.345,55.51,8.132,55.6,8.181,55.901,8.202,55.982,8.121,56.14,8.13,56.321,8.164,56.607,8.232,56.618,8.281,56.617,8.473,56.565,8.553,56.56,8.608,56.515,8.672,56.496,8.718,56.544,8.736,56.627,8.888,56.735,8.995,56.775,9.067,56.794,9.14,56.75,9.196,56.702,9.21,56.808,9.255,57.012,9.11,57.044,8.993,57.016,8.876,56.887,8.772,56.725,8.603,56.71,8.468,56.665,8.347,56.712,8.268,56.754,8.266,56.815,8.284,56.852,8.427,56.984,8.619,57.111,8.812,57.11,8.952,57.151,9.036,57.155,9.299,57.147,9.434,57.174,9.554,57.232,9.815,57.478,9.962,57.581,10.259,57.617,10.533,57.735,10.61,57.737,10.481,57.649,10.46,57.615,10.445,57.562,10.537,57.449,10.518,57.379,10.524,57.243,10.437,57.172,10.338,57.021,10.296,56.999,10.287,56.823,10.297,56.781,10.283,56.621,10.384,56.555,10.49,56.521,10.846,56.522,10.883,56.493,10.926,56.443,10.894,56.359,10.856,56.296,10.753,56.242,10.621,56.202,10.539,56.2,10.427,56.276,10.374,56.252,10.319,56.213,10.227,56.005,10.183,55.865,10.159,55.854,10.107,55.874,10.017,55.876,9.904,55.843,9.962,55.813,10.024,55.761,9.999,55.736,9.899,55.708,9.81,55.651,9.773,55.608,9.661,55.557,9.591,55.493,9.626,55.414,9.64,55.344,9.671,55.266,9.643,55.205,9.505,55.116,9.454,55.04,9.572,55.041,9.645,55.023,9.688,55,9.732,54.968,9.705,54.928,9.74,54.826]],[[10.645,55.61,10.687,55.558,10.738,55.446,10.819,55.322,10.785,55.27,10.808,55.203,10.785,55.133,10.624,55.052,10.443,55.049,10.255,55.088,9.989,55.163,9.967,55.205,9.93,55.229,9.859,55.357,9.861,55.515,9.994,55.535,10.286,55.611,10.354,55.599,10.424,55.56,10.505,55.558,10.623,55.613,10.645,55.61]],[[11.361,54.892,11.538,54.83,11.658,54.833,11.74,54.807,11.759,54.768,11.766,54.679,11.68,54.654,11.586,54.662,11.457,54.629,11.036,54.773,11.042,54.893,11.059,54.941,11.258,54.952,11.361,54.892]],[[10.734,54.751,10.69,54.745,10.629,54.826,10.622,54.851,10.692,54.903,10.738,54.962,10.857,55.052,10.925,55.158,10.951,55.156,10.921,55.062,10.765,54.8,10.734,54.751]],[[12.549,54.966,12.511,54.951,12.358,54.962,12.184,54.892,12.119,54.914,12.144,54.959,12.162,54.975,12.22,54.994,12.259,55.021,12.274,55.064,12.31,55.041,12.417,55.031,12.47,55.017,12.513,54.997,12.549,54.966]],[[12.666,55.597,12.572,55.554,12.551,55.556,12.52,55.615,12.57,55.65,12.599,55.68,12.62,55.679,12.648,55.647,12.666,55.597]],[[10.484,54.848,10.417,54.837,10.341,54.859,10.216,54.941,10.2,54.963,10.266,54.949,10.347,54.906,10.414,54.897,10.505,54.861,10.484,54.848]],[[10.061,54.886,9.957,54.872,9.904,54.897,9.806,54.906,9.771,55.06,9.781,55.069,9.83,55.058,9.999,54.986,10.058,54.908,10.061,54.886]],[[10.607,55.783,10.59,55.765,10.527,55.784,10.52,55.848,10.544,55.907,10.516,55.959,10.547,55.992,10.636,55.914,10.662,55.878,10.627,55.834,10.607,55.783]],[[11.052,57.253,11.011,57.229,10.874,57.262,10.935,57.309,11.086,57.33,11.175,57.323,11.077,57.277,11.052,57.253]],[[15.088,55.022,15.051,55.005,14.886,55.033,14.684,55.102,14.714,55.238,14.765,55.297,15.133,55.145,15.137,55.087,15.088,55.022]]]},{"n":"Czech Republic","c":"CZ","r":5,"ml":4,"lx":15.38,"ly":49.88,"b":[12.09,48.576,18.832,51.038],"p":[[[18.832,49.511,18.807,49.509,18.75,49.494,18.676,49.488,18.596,49.491,18.535,49.465,18.476,49.421,18.416,49.391,18.383,49.364,18.365,49.336,18.161,49.257,18.133,49.225,18.11,49.18,18.1,49.119,18.086,49.065,18.051,49.037,17.941,49.012,17.913,48.999,17.893,48.971,17.831,48.929,17.758,48.888,17.625,48.842,17.483,48.828,17.297,48.843,17.188,48.861,17.136,48.841,17.063,48.781,16.985,48.677,16.953,48.599,16.928,48.621,16.884,48.704,16.833,48.714,16.764,48.722,16.713,48.734,16.601,48.782,16.544,48.796,16.478,48.8,16.415,48.772,16.367,48.739,16.219,48.739,16.057,48.755,15.825,48.864,15.765,48.865,15.701,48.86,15.599,48.886,15.403,48.957,15.311,48.974,15.253,48.964,15.2,48.948,15.162,48.946,15.14,48.969,15.067,48.998,14.993,49.001,14.972,48.984,14.947,48.828,14.923,48.771,14.822,48.774,14.786,48.747,14.707,48.672,14.691,48.599,14.554,48.613,14.489,48.626,14.431,48.616,14.368,48.576,14.19,48.579,14.049,48.602,13.989,48.692,13.924,48.728,13.843,48.76,13.815,48.767,13.77,48.816,13.685,48.877,13.548,48.96,13.441,48.956,13.401,48.978,13.384,49.008,13.339,49.061,13.289,49.097,13.228,49.112,13.141,49.158,13.024,49.26,12.917,49.33,12.813,49.329,12.748,49.366,12.681,49.415,12.632,49.461,12.556,49.575,12.5,49.64,12.457,49.68,12.408,49.713,12.391,49.74,12.45,49.8,12.472,49.83,12.498,49.853,12.513,49.877,12.512,49.896,12.458,49.956,12.384,49.999,12.276,50.042,12.208,50.098,12.183,50.148,12.175,50.176,12.128,50.213,12.09,50.269,12.09,50.302,12.099,50.311,12.135,50.311,12.175,50.288,12.231,50.245,12.277,50.181,12.306,50.206,12.359,50.273,12.453,50.35,12.549,50.393,12.636,50.397,12.706,50.409,12.765,50.431,12.868,50.422,12.943,50.406,12.967,50.416,12.997,50.456,13.016,50.49,13.181,50.51,13.238,50.577,13.27,50.576,13.306,50.586,13.341,50.611,13.375,50.622,13.401,50.609,13.436,50.601,13.473,50.617,13.527,50.693,13.557,50.705,13.701,50.717,13.899,50.761,13.998,50.801,14.096,50.823,14.202,50.861,14.369,50.899,14.377,50.914,14.299,50.953,14.273,50.977,14.256,51.002,14.283,51.029,14.32,51.038,14.367,51.026,14.507,51.01,14.546,50.994,14.56,50.955,14.595,50.919,14.624,50.915,14.614,50.856,14.658,50.833,14.723,50.815,14.767,50.818,14.797,50.842,14.809,50.859,14.896,50.861,14.983,50.887,14.99,50.927,14.984,51.003,14.994,51.014,15.126,50.993,15.259,50.959,15.277,50.883,15.313,50.846,15.354,50.812,15.395,50.796,15.464,50.794,15.644,50.749,15.731,50.74,15.819,50.709,15.894,50.677,15.949,50.67,15.974,50.635,16.007,50.612,16.066,50.63,16.282,50.656,16.36,50.621,16.413,50.585,16.42,50.574,16.392,50.542,16.379,50.517,16.357,50.5,16.283,50.483,16.241,50.455,16.21,50.424,16.231,50.394,16.291,50.372,16.334,50.367,16.35,50.345,16.488,50.248,16.597,50.122,16.639,50.102,16.679,50.097,16.725,50.116,16.779,50.157,16.842,50.187,16.895,50.202,16.99,50.237,16.993,50.26,16.915,50.345,16.869,50.415,16.88,50.427,16.981,50.416,17.152,50.378,17.415,50.255,17.462,50.255,17.555,50.264,17.655,50.284,17.702,50.307,17.72,50.299,17.735,50.231,17.709,50.194,17.589,50.157,17.596,50.14,17.627,50.116,17.681,50.101,17.747,50.057,17.792,50.007,17.831,49.983,17.875,49.972,17.984,49.999,18.015,50.02,18.028,50.035,18.05,50.032,18.088,50.007,18.099,49.993,18.205,49.965,18.266,49.93,18.305,49.914,18.348,49.93,18.516,49.902,18.562,49.879,18.577,49.841,18.569,49.818,18.595,49.758,18.807,49.614,18.829,49.54,18.832,49.511]]]},{"n":"Turkish Republic of Northern Cyprus","c":"","r":6,"ml":6,"lx":33.69,"ly":35.22,"b":[32.713,35,34.556,35.662],"p":[[[34.004,35.065,33.966,35.057,33.903,35.085,33.866,35.094,33.832,35.067,33.792,35.048,33.757,35.04,33.726,35.037,33.675,35.018,33.614,35.023,33.526,35.039,33.476,35,33.464,35.005,33.456,35.101,33.424,35.141,33.384,35.163,33.326,35.154,33.248,35.157,33.191,35.173,33.078,35.146,32.986,35.116,32.92,35.088,32.869,35.089,32.784,35.116,32.72,35.145,32.713,35.171,32.772,35.16,32.88,35.181,32.926,35.278,32.942,35.39,33.123,35.358,33.308,35.342,33.459,35.336,33.608,35.354,34.063,35.474,34.192,35.546,34.272,35.57,34.411,35.629,34.556,35.662,34.463,35.594,33.942,35.292,33.908,35.202,33.931,35.14,34.004,35.065]]]},{"n":"Cyprus","c":"CY","r":5,"ml":4.5,"lx":33.08,"ly":34.91,"b":[32.301,34.57,34.05,35.183],"p":[[[32.713,35.171,32.72,35.145,32.784,35.116,32.869,35.089,32.92,35.088,32.986,35.116,33.078,35.146,33.191,35.173,33.248,35.157,33.326,35.154,33.384,35.163,33.424,35.141,33.456,35.101,33.464,35.005,33.476,35,33.526,35.039,33.614,35.023,33.675,35.018,33.726,35.037,33.757,35.04,33.792,35.048,33.832,35.067,33.866,35.094,33.903,35.085,33.966,35.057,34.004,35.065,34.024,35.046,34.05,34.988,33.937,34.971,33.822,34.966,33.759,34.973,33.699,34.97,33.514,34.806,33.415,34.751,33.297,34.718,33.176,34.698,33.116,34.696,33.062,34.675,33.025,34.637,33.024,34.6,33.008,34.57,32.942,34.576,32.914,34.635,32.867,34.661,32.75,34.648,32.693,34.649,32.506,34.706,32.449,34.729,32.414,34.778,32.317,34.953,32.301,35.083,32.391,35.05,32.475,35.09,32.556,35.156,32.652,35.183,32.713,35.171]]]},{"n":"Cuba","c":"CU","r":3,"ml":2.7,"lx":-77.98,"ly":21.33,"b":[-84.887,19.855,-74.137,23.19],"p":[[[-81.837,23.163,-81.575,23.117,-81.364,23.13,-81.262,23.157,-81.272,23.129,-81.179,23.06,-81.145,23.055,-81.008,23.09,-80.65,23.103,-80.613,23.084,-80.55,23.017,-80.459,22.975,-80.365,22.943,-80.266,22.935,-80.168,22.949,-80.075,22.942,-79.96,22.877,-79.924,22.869,-79.82,22.887,-79.851,22.827,-79.677,22.743,-79.549,22.578,-79.456,22.51,-79.358,22.449,-79.276,22.408,-79.183,22.388,-78.902,22.396,-78.835,22.391,-78.776,22.367,-78.719,22.358,-78.686,22.367,-78.143,22.109,-77.971,21.972,-77.865,21.901,-77.637,21.797,-77.545,21.775,-77.497,21.788,-77.507,21.811,-77.573,21.868,-77.583,21.889,-77.497,21.872,-77.342,21.755,-77.3,21.712,-77.222,21.672,-77.144,21.644,-77.181,21.598,-77.245,21.594,-77.366,21.613,-77.27,21.538,-77.253,21.483,-77.208,21.479,-77.141,21.539,-77.099,21.589,-76.928,21.459,-76.836,21.4,-76.86,21.365,-76.867,21.33,-76.765,21.362,-76.726,21.359,-76.689,21.34,-76.647,21.285,-76.552,21.272,-76.455,21.274,-76.259,21.227,-76.074,21.133,-75.899,21.114,-75.723,21.111,-75.634,21.061,-75.596,20.995,-75.639,20.947,-75.663,20.898,-75.597,20.838,-75.74,20.812,-75.76,20.776,-75.753,20.736,-75.725,20.715,-75.643,20.733,-75.525,20.717,-75.338,20.702,-75.213,20.714,-74.96,20.673,-74.883,20.651,-74.732,20.573,-74.662,20.522,-74.513,20.385,-74.384,20.33,-74.273,20.317,-74.234,20.326,-74.198,20.311,-74.167,20.292,-74.137,20.232,-74.154,20.169,-74.217,20.117,-74.253,20.08,-74.412,20.075,-74.635,20.058,-74.85,20.002,-74.955,19.958,-75.003,19.929,-75.116,19.901,-75.124,19.925,-75.122,19.954,-75.152,20.008,-75.177,19.959,-75.219,19.924,-75.29,19.893,-75.552,19.891,-75.657,19.932,-75.765,19.96,-76.158,19.99,-76.253,19.987,-76.516,19.957,-76.78,19.94,-76.89,19.921,-76.999,19.893,-77.212,19.894,-77.463,19.861,-77.715,19.855,-77.554,20.082,-77.213,20.3,-77.149,20.347,-77.104,20.408,-77.093,20.453,-77.108,20.492,-77.189,20.56,-77.205,20.611,-77.23,20.644,-77.348,20.672,-77.467,20.69,-77.593,20.69,-77.857,20.714,-77.997,20.715,-78.116,20.762,-78.314,20.927,-78.406,20.974,-78.454,21.011,-78.491,21.054,-78.537,21.297,-78.577,21.414,-78.636,21.516,-78.728,21.593,-78.823,21.619,-79.189,21.553,-79.274,21.563,-79.357,21.585,-79.91,21.743,-80.138,21.829,-80.231,21.872,-80.311,21.933,-80.393,22.034,-80.485,22.123,-80.485,22.087,-80.499,22.064,-80.962,22.053,-81.036,22.074,-81.083,22.098,-81.117,22.134,-81.141,22.207,-81.185,22.268,-81.2,22.203,-81.222,22.143,-81.284,22.109,-81.355,22.104,-81.441,22.184,-81.816,22.2,-81.849,22.214,-81.973,22.291,-82.078,22.388,-81.973,22.422,-81.757,22.467,-81.71,22.497,-81.683,22.535,-81.703,22.592,-81.746,22.633,-81.79,22.657,-81.839,22.672,-81.903,22.679,-82.738,22.689,-82.786,22.658,-82.861,22.595,-83.009,22.514,-83.107,22.43,-83.144,22.386,-83.189,22.355,-83.292,22.303,-83.38,22.223,-83.486,22.187,-83.544,22.209,-83.602,22.209,-83.643,22.189,-83.687,22.18,-83.901,22.17,-83.933,22.15,-83.963,22.092,-83.998,21.98,-84.031,21.943,-84.138,21.929,-84.241,21.898,-84.449,21.792,-84.503,21.776,-84.491,21.854,-84.501,21.93,-84.56,21.933,-84.627,21.92,-84.683,21.899,-84.786,21.842,-84.838,21.828,-84.887,21.857,-84.877,21.894,-84.533,22.031,-84.494,22.042,-84.433,22.031,-84.373,22.036,-84.326,22.074,-84.383,22.256,-84.361,22.379,-84.281,22.474,-84.122,22.619,-84.045,22.666,-83.258,22.968,-83.177,22.983,-82.666,23.044,-82.588,23.065,-82.351,23.154,-82.101,23.19,-81.837,23.163]],[[-77.669,21.952,-77.71,21.921,-77.755,21.966,-77.784,21.97,-77.823,21.988,-77.9,22.037,-77.919,22.088,-77.855,22.092,-77.774,22.083,-77.634,22.054,-77.646,21.996,-77.669,21.952]],[[-78.027,22.285,-78.048,22.269,-78.102,22.306,-78.18,22.322,-78.226,22.38,-78.27,22.402,-78.274,22.424,-78.201,22.438,-78.151,22.431,-78.094,22.387,-78.062,22.306,-78.027,22.285]],[[-78.63,22.552,-78.493,22.531,-78.445,22.544,-78.4,22.547,-78.351,22.539,-78.284,22.455,-78.343,22.445,-78.39,22.445,-78.425,22.46,-78.548,22.464,-78.629,22.488,-78.674,22.509,-78.696,22.534,-78.63,22.552]],[[-77.879,22.128,-77.912,22.125,-78.012,22.166,-78.042,22.201,-78.007,22.248,-77.999,22.299,-77.986,22.302,-77.97,22.241,-77.894,22.215,-77.889,22.201,-77.842,22.149,-77.879,22.128]],[[-82.562,21.572,-82.655,21.519,-82.853,21.444,-82.96,21.441,-83.067,21.469,-83.142,21.532,-83.184,21.593,-83.18,21.623,-83.113,21.574,-83.055,21.549,-83.007,21.566,-82.974,21.592,-83.083,21.791,-83.078,21.833,-82.991,21.943,-82.756,21.91,-82.715,21.89,-82.682,21.821,-82.629,21.767,-82.568,21.622,-82.562,21.572]],[[-79.35,22.664,-79.348,22.638,-79.523,22.711,-79.598,22.788,-79.628,22.805,-79.579,22.807,-79.382,22.681,-79.35,22.664]]]},{"n":"Croatia","c":"HR","r":6,"ml":4,"lx":16.37,"ly":45.81,"b":[13.517,42.433,19.401,46.535],"p":[[[13.578,45.517,13.615,45.477,13.879,45.428,13.936,45.45,13.97,45.483,13.97,45.503,13.993,45.509,14.086,45.478,14.161,45.485,14.283,45.487,14.37,45.481,14.427,45.506,14.505,45.595,14.534,45.645,14.569,45.657,14.592,45.651,14.609,45.61,14.65,45.571,14.734,45.508,14.793,45.478,14.847,45.467,14.9,45.493,14.955,45.5,15.11,45.451,15.242,45.441,15.339,45.467,15.327,45.502,15.291,45.542,15.284,45.58,15.29,45.613,15.357,45.646,15.354,45.66,15.273,45.718,15.277,45.733,15.454,45.798,15.625,45.834,15.652,45.862,15.668,45.904,15.676,45.984,15.666,46.048,15.597,46.109,15.593,46.14,15.609,46.172,15.636,46.201,15.704,46.213,15.784,46.234,15.848,46.258,15.933,46.278,16.001,46.305,16.067,46.371,16.106,46.382,16.227,46.373,16.253,46.389,16.237,46.484,16.258,46.508,16.301,46.521,16.321,46.535,16.428,46.524,16.516,46.5,16.57,46.485,16.748,46.416,16.871,46.339,16.94,46.254,17.033,46.187,17.15,46.14,17.242,46.077,17.311,45.996,17.406,45.951,17.529,45.941,17.607,45.914,17.64,45.868,17.706,45.827,17.807,45.79,17.964,45.77,18.264,45.765,18.291,45.764,18.358,45.753,18.437,45.767,18.534,45.796,18.565,45.813,18.666,45.907,18.722,45.899,18.833,45.911,18.9,45.932,18.905,45.932,18.901,45.908,18.894,45.866,18.839,45.836,18.895,45.767,18.947,45.656,18.918,45.601,18.954,45.558,19.055,45.527,19.064,45.515,19.033,45.502,19.008,45.466,19.005,45.4,19.093,45.337,19.273,45.278,19.33,45.268,19.352,45.245,19.382,45.231,19.4,45.213,19.401,45.189,19.388,45.173,19.303,45.167,19.206,45.168,19.137,45.196,19.131,45.175,19.13,45.152,19.063,45.137,19.1,44.974,19.085,44.927,19.061,44.911,19.038,44.918,19.01,44.919,18.996,44.904,19.007,44.869,18.941,44.865,18.836,44.883,18.788,44.915,18.78,44.947,18.779,44.977,18.746,45.027,18.663,45.077,18.488,45.086,18.424,45.102,18.358,45.121,18.285,45.134,18.218,45.133,18.137,45.119,17.996,45.142,17.949,45.112,17.874,45.077,17.813,45.078,17.69,45.158,17.654,45.163,17.546,45.123,17.503,45.12,17.469,45.133,17.324,45.164,17.259,45.171,17.211,45.156,17.125,45.172,16.919,45.277,16.791,45.197,16.531,45.217,16.454,45.162,16.365,45.058,16.293,45.009,16.231,45.027,16.157,45.072,16.028,45.19,15.963,45.211,15.888,45.216,15.823,45.203,15.788,45.179,15.762,45.008,15.738,44.856,15.737,44.766,15.88,44.682,16.049,44.538,16.103,44.521,16.13,44.474,16.17,44.352,16.214,44.215,16.3,44.125,16.378,44.06,16.472,44.003,16.591,43.913,16.688,43.815,16.713,43.779,16.902,43.649,17.085,43.517,17.248,43.47,17.274,43.446,17.275,43.344,17.293,43.306,17.402,43.199,17.625,43.043,17.65,43.007,17.658,42.98,17.643,42.96,17.585,42.938,17.537,42.962,17.33,43.115,17.129,43.211,16.903,43.392,16.6,43.464,16.394,43.543,16.269,43.531,16.131,43.506,16.046,43.506,15.986,43.52,15.943,43.569,15.949,43.607,15.942,43.657,15.821,43.736,15.656,43.811,15.499,43.909,15.186,44.172,15.123,44.257,15.185,44.273,15.231,44.271,15.284,44.289,15.37,44.289,15.471,44.272,15.381,44.328,15.27,44.383,14.981,44.603,14.895,44.707,14.885,44.818,14.907,44.971,14.855,45.081,14.632,45.223,14.55,45.298,14.386,45.342,14.313,45.338,14.269,45.283,14.236,45.16,14.091,44.998,14.042,44.927,13.966,44.836,13.9,44.829,13.861,44.837,13.742,44.992,13.629,45.108,13.613,45.163,13.603,45.231,13.517,45.482,13.578,45.517]],[[16.651,42.997,16.836,42.969,16.971,42.981,17.094,42.964,17.17,42.933,17.188,42.917,17.089,42.915,16.978,42.928,16.851,42.896,16.739,42.913,16.696,42.934,16.666,42.96,16.651,42.997]],[[17.194,43.126,17.124,43.115,16.679,43.123,16.55,43.144,16.406,43.197,16.376,43.214,16.521,43.229,16.656,43.214,16.697,43.175,17.061,43.144,17.194,43.126]],[[16.785,43.271,16.627,43.268,16.49,43.286,16.423,43.317,16.428,43.343,16.449,43.387,16.602,43.382,16.834,43.351,16.891,43.315,16.874,43.298,16.785,43.271]],[[15.231,44.062,15.247,44.027,15.122,44.093,15.075,44.138,15.066,44.158,15.231,44.062]],[[14.81,44.977,14.687,44.956,14.628,44.994,14.613,45.025,14.512,45.035,14.45,45.079,14.438,45.099,14.525,45.147,14.571,45.225,14.63,45.178,14.701,45.09,14.739,45.065,14.81,44.977]],[[14.831,44.759,14.857,44.715,14.763,44.755,14.678,44.77,14.66,44.8,14.672,44.824,14.691,44.848,14.754,44.845,14.764,44.821,14.831,44.759]],[[15.188,44.336,15.163,44.309,15.098,44.358,15.039,44.393,14.996,44.434,14.913,44.486,14.885,44.545,14.76,44.665,14.742,44.697,14.804,44.649,14.855,44.618,14.898,44.611,15.006,44.534,15.113,44.436,15.24,44.35,15.214,44.348,15.188,44.336]],[[15.189,43.922,15.203,43.908,15.202,43.898,15.15,43.912,15.136,43.907,14.891,44.126,14.865,44.168,14.953,44.117,15.189,43.922]],[[15.371,43.974,15.437,43.9,15.374,43.915,15.309,43.961,15.27,44.011,15.371,43.974]],[[14.488,44.66,14.48,44.621,14.42,44.67,14.389,44.758,14.312,44.9,14.303,44.94,14.342,44.98,14.34,45.02,14.286,45.145,14.331,45.165,14.358,45.167,14.369,45.081,14.394,45.031,14.467,44.97,14.453,44.869,14.468,44.725,14.483,44.693,14.488,44.66]],[[17.608,42.769,17.744,42.7,17.344,42.79,17.39,42.799,17.432,42.8,17.608,42.769]],[[18.436,42.56,18.438,42.523,18.477,42.481,18.517,42.433,18.333,42.528,18.161,42.634,17.824,42.797,17.585,42.837,17.258,42.968,17.045,43.015,17.126,43.026,17.22,43.026,17.724,42.851,17.668,42.897,17.74,42.915,17.802,42.902,17.841,42.845,17.919,42.807,18.045,42.741,18.124,42.691,18.304,42.599,18.347,42.587,18.436,42.56]]]},{"n":"Ivory Coast","c":"CI","r":3,"ml":2.5,"lx":-5.57,"ly":7.49,"b":[-8.604,4.351,-2.506,10.724],"p":[[[-3.087,5.128,-3.114,5.089,-3.246,5.114,-3.215,5.147,-3.087,5.128]],[[-7.991,10.163,-7.961,10.163,-7.884,10.186,-7.814,10.237,-7.749,10.342,-7.661,10.427,-7.562,10.421,-7.533,10.437,-7.498,10.44,-7.457,10.384,-7.415,10.341,-7.385,10.34,-7.363,10.259,-7.182,10.226,-7.105,10.204,-7.04,10.145,-7.017,10.143,-6.989,10.156,-6.968,10.176,-6.964,10.199,-6.992,10.252,-6.979,10.3,-6.95,10.342,-6.904,10.345,-6.834,10.357,-6.753,10.357,-6.693,10.349,-6.669,10.392,-6.692,10.512,-6.686,10.578,-6.676,10.634,-6.654,10.656,-6.565,10.586,-6.483,10.561,-6.424,10.559,-6.408,10.572,-6.433,10.649,-6.426,10.672,-6.404,10.685,-6.366,10.693,-6.261,10.724,-6.25,10.718,-6.231,10.598,-6.24,10.558,-6.218,10.476,-6.191,10.4,-6.193,10.369,-6.215,10.322,-6.241,10.279,-6.238,10.262,-6.197,10.232,-6.117,10.202,-6.035,10.195,-5.989,10.239,-5.941,10.275,-5.908,10.307,-5.896,10.355,-5.844,10.39,-5.694,10.433,-5.557,10.44,-5.524,10.426,-5.461,10.36,-5.382,10.314,-5.262,10.32,-5.175,10.293,-5.1,10.242,-5.049,10.128,-4.994,10.046,-4.97,9.93,-4.883,9.869,-4.814,9.841,-4.722,9.757,-4.626,9.714,-4.527,9.723,-4.48,9.679,-4.406,9.648,-4.332,9.646,-4.267,9.743,-4.181,9.782,-3.963,9.86,-3.878,9.895,-3.791,9.917,-3.581,9.924,-3.386,9.9,-3.29,9.882,-3.224,9.895,-3.161,9.849,-3.096,9.752,-3.043,9.721,-2.988,9.687,-2.948,9.611,-2.901,9.535,-2.875,9.501,-2.817,9.426,-2.767,9.425,-2.717,9.457,-2.696,9.481,-2.686,9.432,-2.706,9.351,-2.702,9.302,-2.674,9.283,-2.689,9.219,-2.747,9.11,-2.747,9.045,-2.69,9.025,-2.649,8.957,-2.625,8.84,-2.6,8.8,-2.598,8.776,-2.557,8.493,-2.506,8.209,-2.538,8.172,-2.583,8.161,-2.612,8.148,-2.62,8.121,-2.601,8.082,-2.613,8.047,-2.669,8.022,-2.79,7.932,-2.798,7.896,-2.83,7.819,-2.857,7.772,-2.896,7.685,-2.959,7.455,-2.982,7.264,-2.986,7.205,-3.01,7.164,-3.038,7.105,-3.169,6.941,-3.236,6.807,-3.227,6.749,-3.224,6.691,-3.244,6.649,-3.24,6.536,-3.224,6.441,-3.201,6.348,-3.106,6.086,-3.056,5.926,-3.025,5.798,-2.998,5.711,-2.973,5.676,-2.962,5.643,-2.821,5.619,-2.794,5.6,-2.755,5.433,-2.762,5.357,-2.79,5.328,-2.789,5.264,-2.795,5.185,-2.816,5.153,-2.895,5.149,-2.948,5.119,-3.019,5.131,-3.026,5.151,-3.064,5.158,-3.169,5.203,-3.151,5.348,-3.2,5.354,-3.238,5.335,-3.312,5.161,-3.348,5.131,-3.871,5.221,-3.984,5.293,-4.12,5.31,-4.357,5.301,-4.553,5.28,-4.609,5.236,-4.115,5.262,-4.062,5.257,-4.037,5.23,-4.662,5.173,-4.9,5.138,-4.97,5.148,-5.024,5.204,-5.282,5.21,-5.335,5.192,-5.368,5.151,-5.266,5.16,-5.105,5.162,-5.062,5.131,-5.565,5.089,-5.914,5.011,-6.062,4.953,-6.548,4.762,-6.845,4.671,-6.923,4.638,-7.058,4.545,-7.231,4.486,-7.426,4.376,-7.545,4.351,-7.572,4.386,-7.575,4.572,-7.591,4.822,-7.585,4.917,-7.569,5.006,-7.569,5.081,-7.51,5.108,-7.494,5.14,-7.485,5.236,-7.43,5.325,-7.429,5.478,-7.412,5.51,-7.4,5.551,-7.424,5.651,-7.454,5.841,-7.469,5.854,-7.483,5.846,-7.514,5.842,-7.636,5.908,-7.73,5.919,-7.797,5.975,-7.801,6.039,-7.833,6.076,-7.856,6.15,-7.889,6.235,-7.982,6.286,-8.069,6.298,-8.131,6.288,-8.204,6.291,-8.287,6.319,-8.345,6.351,-8.399,6.413,-8.45,6.463,-8.49,6.456,-8.54,6.468,-8.588,6.491,-8.604,6.508,-8.401,6.705,-8.333,6.802,-8.325,6.86,-8.325,6.92,-8.302,6.981,-8.297,7.074,-8.409,7.412,-8.437,7.516,-8.467,7.547,-8.486,7.558,-8.43,7.602,-8.352,7.591,-8.232,7.557,-8.206,7.59,-8.115,7.761,-8.118,7.824,-8.127,7.868,-8.074,7.984,-8.032,8.03,-8.01,8.079,-8.017,8.145,-8.049,8.17,-8.091,8.165,-8.141,8.181,-8.217,8.22,-8.256,8.254,-8.244,8.408,-8.237,8.456,-8.21,8.483,-8.168,8.491,-8.049,8.495,-7.953,8.478,-7.869,8.468,-7.824,8.468,-7.787,8.422,-7.739,8.375,-7.696,8.376,-7.681,8.41,-7.691,8.563,-7.72,8.643,-7.784,8.721,-7.951,8.787,-7.955,8.879,-7.938,8.98,-7.902,9.017,-7.778,9.081,-7.8,9.115,-7.839,9.152,-7.918,9.189,-7.9,9.309,-7.896,9.416,-7.963,9.404,-8.031,9.398,-8.089,9.431,-8.137,9.496,-8.146,9.675,-8.146,9.882,-8.155,9.973,-8.137,10.022,-8.078,10.067,-8.014,10.125,-7.991,10.163]]]},{"n":"Costa Rica","c":"CR","r":5,"ml":2.5,"lx":-84.08,"ly":10.07,"b":[-85.908,8.071,-82.564,11.189],"p":[[[-82.564,9.577,-82.569,9.558,-82.587,9.539,-82.611,9.519,-82.644,9.506,-82.723,9.546,-82.801,9.592,-82.844,9.571,-82.86,9.511,-82.889,9.481,-82.925,9.469,-82.94,9.449,-82.943,9.249,-82.94,9.06,-82.881,9.056,-82.783,8.99,-82.741,8.952,-82.728,8.916,-82.74,8.899,-82.812,8.857,-82.882,8.805,-82.917,8.74,-82.856,8.635,-82.843,8.564,-82.845,8.489,-82.862,8.454,-82.998,8.368,-83.027,8.338,-83.023,8.316,-82.948,8.257,-82.913,8.2,-82.883,8.131,-82.879,8.071,-82.947,8.182,-83.041,8.288,-83.123,8.353,-83.13,8.505,-83.162,8.588,-83.286,8.664,-83.391,8.718,-83.47,8.707,-83.422,8.619,-83.298,8.507,-83.29,8.464,-83.292,8.406,-83.377,8.415,-83.452,8.438,-83.544,8.446,-83.605,8.48,-83.734,8.614,-83.642,8.729,-83.614,8.804,-83.616,8.96,-83.637,9.035,-83.737,9.15,-83.896,9.276,-84.118,9.379,-84.222,9.463,-84.483,9.526,-84.582,9.568,-84.659,9.647,-84.67,9.703,-84.643,9.789,-84.715,9.899,-85.025,10.116,-85.198,10.195,-85.236,10.242,-85.263,10.257,-85.237,10.107,-85.161,10.017,-84.963,9.933,-84.908,9.885,-84.886,9.821,-85.001,9.699,-85.06,9.668,-85.077,9.602,-85.115,9.582,-85.154,9.62,-85.315,9.811,-85.625,9.902,-85.681,9.959,-85.796,10.133,-85.85,10.292,-85.831,10.398,-85.703,10.563,-85.663,10.635,-85.671,10.68,-85.667,10.745,-85.715,10.791,-85.833,10.85,-85.908,10.898,-85.887,10.921,-85.752,10.985,-85.744,11.043,-85.744,11.062,-85.722,11.066,-85.703,11.082,-85.691,11.097,-85.654,11.153,-85.621,11.184,-85.584,11.189,-85.539,11.166,-85.368,11.106,-85.179,11.04,-84.909,10.945,-84.797,11.006,-84.701,11.052,-84.634,11.046,-84.489,10.992,-84.402,10.974,-84.348,10.98,-84.256,10.901,-84.205,10.841,-84.197,10.802,-84.168,10.78,-84.096,10.776,-83.919,10.735,-83.811,10.743,-83.713,10.786,-83.659,10.837,-83.642,10.917,-83.617,10.877,-83.588,10.815,-83.575,10.735,-83.448,10.466,-83.347,10.315,-83.125,10.042,-83.029,9.991,-82.866,9.771,-82.81,9.735,-82.778,9.67,-82.61,9.616,-82.564,9.577]]]},{"n":"Democratic Republic of the Congo","c":"CD","r":2,"ml":2,"lx":23.46,"ly":-1.86,"b":[12.214,-13.454,31.274,5.312],"p":[[[30.751,-8.194,30.578,-8.22,30.328,-8.258,30.051,-8.3,29.766,-8.344,29.484,-8.387,29.216,-8.428,28.972,-8.465,28.898,-8.485,28.934,-8.59,28.918,-8.701,28.87,-8.786,28.794,-8.891,28.759,-8.933,28.681,-9.015,28.617,-9.072,28.484,-9.169,28.401,-9.225,28.4,-9.275,28.541,-9.51,28.604,-9.679,28.63,-9.831,28.629,-9.919,28.624,-10.099,28.617,-10.313,28.607,-10.397,28.646,-10.55,28.639,-10.669,28.544,-10.802,28.518,-10.933,28.47,-11.11,28.404,-11.354,28.357,-11.483,28.383,-11.567,28.407,-11.623,28.432,-11.698,28.483,-11.812,28.542,-11.879,28.575,-11.908,28.769,-12.051,28.85,-12.121,28.973,-12.258,29.064,-12.349,29.191,-12.37,29.344,-12.405,29.428,-12.431,29.486,-12.418,29.505,-12.386,29.502,-12.318,29.492,-12.267,29.508,-12.228,29.56,-12.202,29.692,-12.198,29.75,-12.164,29.795,-12.155,29.795,-12.306,29.796,-12.451,29.796,-12.626,29.796,-12.827,29.796,-12.992,29.796,-13.167,29.796,-13.37,29.795,-13.393,29.775,-13.438,29.723,-13.454,29.652,-13.414,29.648,-13.373,29.63,-13.299,29.597,-13.261,29.554,-13.249,29.481,-13.268,29.382,-13.323,29.254,-13.371,29.202,-13.398,29.112,-13.395,29.014,-13.369,28.942,-13.307,28.922,-13.215,28.859,-13.119,28.773,-12.982,28.73,-12.925,28.673,-12.861,28.615,-12.854,28.551,-12.836,28.511,-12.742,28.474,-12.623,28.451,-12.577,28.413,-12.518,28.358,-12.482,28.237,-12.435,28.069,-12.368,27.857,-12.285,27.757,-12.281,27.644,-12.267,27.574,-12.227,27.533,-12.195,27.487,-12.08,27.424,-11.945,27.238,-11.783,27.196,-11.605,27.159,-11.579,27.095,-11.594,27.046,-11.616,27.027,-11.664,26.977,-11.825,26.95,-11.899,26.931,-11.919,26.89,-11.944,26.824,-11.965,26.73,-11.976,26.596,-11.972,26.43,-11.948,26.34,-11.93,26.096,-11.903,26.026,-11.89,25.927,-11.855,25.855,-11.82,25.619,-11.744,25.512,-11.753,25.46,-11.7,25.413,-11.674,25.349,-11.623,25.321,-11.554,25.283,-11.405,25.292,-11.325,25.319,-11.237,25.289,-11.212,25.246,-11.212,25.185,-11.243,25.076,-11.26,24.877,-11.299,24.806,-11.321,24.728,-11.338,24.668,-11.353,24.519,-11.438,24.467,-11.448,24.378,-11.417,24.335,-11.371,24.378,-11.319,24.396,-11.255,24.366,-11.13,24.32,-11.072,24.187,-11.03,24.137,-11.026,24.115,-10.956,24.078,-10.892,24.003,-10.879,23.967,-10.872,23.929,-10.892,23.907,-10.943,23.901,-10.983,23.834,-11.014,23.696,-11.008,23.56,-10.979,23.464,-10.969,23.4,-10.976,23.157,-11.075,23.076,-11.088,22.815,-11.08,22.667,-11.06,22.561,-11.056,22.486,-11.087,22.393,-11.159,22.315,-11.199,22.279,-11.194,22.257,-11.164,22.226,-11.122,22.217,-11.013,22.178,-10.892,22.204,-10.829,22.28,-10.784,22.307,-10.691,22.283,-10.552,22.282,-10.453,22.302,-10.397,22.275,-10.259,22.198,-10.041,22.089,-9.863,21.949,-9.726,21.857,-9.594,21.813,-9.469,21.829,-9.168,21.872,-8.904,21.905,-8.693,21.896,-8.341,21.801,-8.112,21.78,-7.865,21.834,-7.602,21.842,-7.421,21.806,-7.329,21.782,-7.315,21.751,-7.305,21.511,-7.297,21.19,-7.285,20.911,-7.281,20.608,-7.278,20.558,-7.244,20.536,-7.183,20.537,-7.122,20.599,-6.935,20.59,-6.92,20.482,-6.916,20.19,-6.946,19.997,-6.976,19.875,-6.986,19.66,-7.037,19.528,-7.144,19.484,-7.279,19.487,-7.391,19.48,-7.472,19.419,-7.557,19.372,-7.655,19.37,-7.707,19.341,-7.967,19.143,-8.001,18.944,-8.001,18.898,-7.998,18.653,-7.936,18.563,-7.936,18.485,-7.969,18.335,-8,18.192,-8.024,18.047,-8.101,18.009,-8.108,17.913,-8.068,17.779,-8.071,17.643,-8.091,17.58,-8.099,17.536,-8.076,17.411,-7.882,17.245,-7.623,17.155,-7.461,17.122,-7.419,17.064,-7.363,16.985,-7.257,16.952,-7.157,16.966,-7.062,16.919,-6.934,16.813,-6.773,16.743,-6.618,16.709,-6.472,16.701,-6.346,16.718,-6.241,16.697,-6.164,16.64,-6.115,16.608,-6.052,16.585,-6.025,16.537,-5.966,16.431,-5.9,16.315,-5.866,16.06,-5.865,15.727,-5.864,15.425,-5.869,15.089,-5.875,14.749,-5.88,14.658,-5.889,14.399,-5.893,14.191,-5.876,14.114,-5.865,13.979,-5.857,13.765,-5.855,13.649,-5.862,13.371,-5.862,13.346,-5.863,13.303,-5.882,13.184,-5.856,13.068,-5.865,13.003,-5.836,12.861,-5.854,12.792,-5.878,12.681,-5.961,12.515,-6.004,12.453,-6,12.412,-5.986,12.315,-5.895,12.24,-5.807,12.214,-5.759,12.255,-5.746,12.386,-5.728,12.485,-5.719,12.504,-5.696,12.519,-5.425,12.522,-5.149,12.487,-5.113,12.453,-5.091,12.451,-5.071,12.503,-5.037,12.574,-4.997,12.596,-4.978,12.675,-4.905,12.83,-4.737,12.947,-4.695,13.057,-4.651,13.073,-4.635,13.087,-4.602,13.137,-4.604,13.152,-4.62,13.176,-4.656,13.22,-4.706,13.297,-4.765,13.376,-4.829,13.415,-4.837,13.478,-4.805,13.552,-4.757,13.66,-4.721,13.685,-4.689,13.699,-4.618,13.708,-4.543,13.717,-4.454,13.739,-4.442,13.778,-4.434,13.85,-4.459,13.882,-4.485,13.941,-4.485,13.978,-4.461,14.047,-4.417,14.134,-4.4,14.227,-4.358,14.316,-4.304,14.358,-4.299,14.403,-4.37,14.443,-4.419,14.45,-4.45,14.41,-4.508,14.365,-4.586,14.403,-4.682,14.412,-4.775,14.411,-4.831,14.441,-4.854,14.462,-4.865,14.494,-4.852,14.558,-4.856,14.634,-4.885,14.708,-4.882,14.779,-4.846,14.912,-4.706,15.106,-4.461,15.267,-4.308,15.395,-4.245,15.481,-4.172,15.526,-4.088,15.6,-4.031,15.755,-3.986,15.872,-3.934,15.99,-3.766,16.147,-3.464,16.191,-3.194,16.217,-3.03,16.202,-2.465,16.192,-2.279,16.215,-2.178,16.274,-2.108,16.434,-1.961,16.541,-1.84,16.622,-1.699,16.78,-1.376,16.849,-1.272,16.88,-1.226,16.975,-1.14,17.108,-1.064,17.279,-1,17.543,-0.775,17.753,-0.549,17.724,-0.278,17.773,-0.052,17.888,0.234,17.925,0.537,17.886,0.857,17.902,1.118,18.012,1.422,18.058,1.535,18.073,1.719,18.072,2.013,18.212,2.415,18.343,2.655,18.491,2.924,18.547,3.087,18.622,3.304,18.61,3.478,18.597,3.679,18.634,3.954,18.62,4.117,18.567,4.258,18.594,4.346,18.7,4.383,18.832,4.523,19.069,4.891,19.323,5.071,19.501,5.127,19.686,5.121,19.807,5.089,19.863,5.031,20.002,4.945,20.226,4.83,20.394,4.686,20.487,4.542,20.558,4.463,20.647,4.436,20.793,4.447,20.956,4.413,21.126,4.332,21.23,4.302,21.268,4.323,21.35,4.311,21.538,4.245,21.687,4.281,21.908,4.254,22.422,4.135,22.45,4.155,22.462,4.16,22.506,4.208,22.617,4.446,22.712,4.592,22.756,4.647,22.865,4.724,22.993,4.744,23.116,4.737,23.219,4.703,23.313,4.664,23.417,4.663,23.524,4.701,23.682,4.771,23.848,4.816,23.992,4.866,24.228,4.954,24.32,4.994,24.437,5.01,24.766,4.93,24.978,4.983,25.065,4.967,25.249,5.025,25.283,5.063,25.4,5.256,25.525,5.312,25.714,5.284,25.82,5.254,26.174,5.171,26.633,5.085,26.768,5.072,26.822,5.062,26.87,5.076,27.021,5.184,27.072,5.2,27.115,5.198,27.403,5.109,27.439,5.039,27.491,4.968,27.664,4.846,27.719,4.778,27.761,4.703,27.788,4.645,27.842,4.598,27.917,4.568,27.981,4.532,28.02,4.479,28.079,4.425,28.192,4.35,28.247,4.349,28.311,4.338,28.367,4.319,28.428,4.324,28.525,4.373,28.64,4.454,28.727,4.505,28.939,4.487,29.057,4.446,29.151,4.388,29.225,4.392,29.385,4.498,29.47,4.612,29.552,4.636,29.677,4.587,29.78,4.481,29.87,4.327,29.934,4.269,30.021,4.178,30.195,3.982,30.421,3.884,30.508,3.836,30.537,3.787,30.554,3.723,30.559,3.653,30.587,3.624,30.648,3.634,30.7,3.644,30.757,3.624,30.797,3.573,30.817,3.533,30.839,3.491,30.895,3.464,30.906,3.409,30.868,3.342,30.828,3.283,30.779,3.163,30.754,3.042,30.787,3.001,30.821,2.968,30.84,2.933,30.851,2.894,30.847,2.847,30.77,2.678,30.73,2.53,30.729,2.455,30.83,2.4,30.962,2.403,31.004,2.369,31.045,2.316,31.082,2.288,31.138,2.289,31.176,2.27,31.191,2.232,31.236,2.191,31.274,2.146,31.256,2.088,31.253,2.045,31.159,1.922,30.943,1.683,30.478,1.239,30.321,1.185,30.24,1.103,30.183,0.973,30.047,0.864,29.943,0.819,29.932,0.793,29.924,0.674,29.934,0.499,29.885,0.419,29.815,0.264,29.778,0.166,29.75,0.147,29.718,0.098,29.698,-0.06,29.684,-0.114,29.633,-0.442,29.648,-0.535,29.608,-0.691,29.606,-0.783,29.59,-0.887,29.562,-0.977,29.564,-1.121,29.58,-1.357,29.577,-1.388,29.538,-1.41,29.468,-1.468,29.402,-1.507,29.352,-1.518,29.268,-1.622,29.197,-1.72,29.143,-1.816,29.129,-1.86,29.141,-1.985,29.148,-2.132,29.132,-2.195,29.106,-2.233,28.99,-2.313,28.913,-2.37,28.876,-2.4,28.858,-2.447,28.891,-2.556,28.894,-2.635,28.922,-2.682,29.014,-2.72,29.014,-2.758,29.017,-2.8,29.065,-2.851,29.153,-2.955,29.224,-3.054,29.226,-3.139,29.212,-3.281,29.21,-3.363,29.217,-3.476,29.217,-3.685,29.212,-3.834,29.223,-3.911,29.331,-4.095,29.379,-4.3,29.403,-4.449,29.404,-4.497,29.368,-4.669,29.326,-4.836,29.323,-4.899,29.343,-4.983,29.42,-5.176,29.476,-5.317,29.504,-5.401,29.542,-5.5,29.594,-5.651,29.607,-5.723,29.596,-5.776,29.491,-5.965,29.48,-6.025,29.506,-6.172,29.541,-6.314,29.591,-6.394,29.71,-6.617,29.798,-6.692,29.962,-6.803,30.106,-6.915,30.162,-6.973,30.213,-7.038,30.313,-7.204,30.375,-7.339,30.407,-7.461,30.486,-7.627,30.559,-7.782,30.654,-7.971,30.721,-8.104,30.751,-8.194]]]},{"n":"Republic of the Congo","c":"CG","r":4,"ml":4,"lx":15.9,"ly":0.14,"b":[11.13,-5.004,18.622,3.687],"p":[[[11.13,-3.916,11.19,-3.762,11.234,-3.691,11.288,-3.641,11.504,-3.52,11.537,-3.525,11.686,-3.682,11.733,-3.695,11.786,-3.69,11.849,-3.697,11.88,-3.666,11.885,-3.625,11.839,-3.58,11.833,-3.531,11.865,-3.479,11.883,-3.42,11.929,-3.351,11.934,-3.319,11.885,-3.283,11.784,-3.229,11.715,-3.177,11.689,-3.127,11.708,-3.063,11.763,-3.011,11.76,-2.983,11.711,-2.937,11.676,-2.887,11.639,-2.855,11.538,-2.837,11.557,-2.77,11.595,-2.671,11.603,-2.595,11.575,-2.397,11.578,-2.361,11.605,-2.343,11.666,-2.365,11.727,-2.395,11.892,-2.351,11.95,-2.345,11.998,-2.383,12.064,-2.413,12.446,-2.33,12.454,-2.246,12.476,-2.169,12.479,-2.112,12.463,-2.075,12.444,-2.048,12.432,-1.99,12.432,-1.929,12.469,-1.9,12.59,-1.827,12.628,-1.83,12.714,-1.869,12.794,-1.932,12.864,-2.063,12.914,-2.176,12.992,-2.313,13.159,-2.369,13.357,-2.405,13.465,-2.395,13.619,-2.279,13.706,-2.187,13.734,-2.138,13.784,-2.164,13.842,-2.284,13.879,-2.33,13.888,-2.375,13.862,-2.43,13.887,-2.465,13.994,-2.491,14.087,-2.467,14.13,-2.418,14.2,-2.354,14.2,-2.301,14.163,-2.266,14.163,-2.218,14.202,-2.18,14.24,-2.077,14.251,-2.001,14.288,-1.954,14.359,-1.92,14.384,-1.89,14.423,-1.712,14.403,-1.647,14.403,-1.593,14.447,-1.525,14.456,-1.413,14.437,-1.23,14.424,-1.104,14.411,-0.972,14.445,-0.799,14.481,-0.618,14.474,-0.573,14.425,-0.519,14.364,-0.469,14.207,-0.427,14.148,-0.362,14.103,-0.292,14.069,-0.27,13.898,-0.243,13.86,-0.203,13.875,-0.091,13.891,0.075,13.885,0.191,13.915,0.284,13.95,0.354,14.025,0.428,14.066,0.515,14.088,0.537,14.231,0.551,14.283,0.587,14.324,0.624,14.342,0.674,14.391,0.756,14.434,0.811,14.439,0.849,14.43,0.901,14.386,1.004,14.334,1.09,14.303,1.121,14.24,1.323,14.181,1.37,14.066,1.396,13.851,1.419,13.721,1.382,13.523,1.315,13.372,1.268,13.274,1.241,13.216,1.248,13.19,1.279,13.228,1.305,13.247,1.367,13.223,1.455,13.185,1.535,13.163,1.648,13.172,1.789,13.209,1.92,13.289,2.092,13.294,2.162,13.533,2.16,13.773,2.157,14.034,2.159,14.287,2.16,14.484,2.155,14.579,2.199,14.669,2.132,14.713,2.117,14.728,2.122,14.763,2.075,14.875,2.08,14.893,2.069,14.902,2.012,15.006,2.014,15.058,2.001,15.1,2.002,15.16,2.036,15.204,2.024,15.282,1.982,15.339,1.945,15.417,1.957,15.6,1.95,15.742,1.915,15.882,1.817,15.975,1.76,16.059,1.676,16.09,1.691,16.12,1.714,16.136,1.724,16.135,1.796,16.088,1.918,16.07,2.022,16.08,2.107,16.116,2.168,16.177,2.205,16.183,2.262,16.183,2.27,16.252,2.407,16.32,2.543,16.401,2.701,16.469,2.832,16.46,2.897,16.466,2.993,16.48,3.101,16.477,3.165,16.496,3.209,16.543,3.37,16.57,3.463,16.611,3.505,16.673,3.535,16.764,3.536,17.003,3.557,17.225,3.598,17.298,3.617,17.438,3.685,17.492,3.687,17.538,3.662,17.807,3.584,17.88,3.554,17.907,3.558,17.948,3.552,18.011,3.551,18.072,3.56,18.111,3.551,18.161,3.5,18.194,3.505,18.237,3.543,18.318,3.581,18.474,3.623,18.5,3.604,18.554,3.51,18.61,3.478,18.622,3.304,18.547,3.087,18.491,2.924,18.343,2.655,18.212,2.415,18.072,2.013,18.073,1.719,18.058,1.535,18.012,1.422,17.902,1.118,17.886,0.857,17.925,0.537,17.888,0.234,17.773,-0.052,17.724,-0.278,17.753,-0.549,17.543,-0.775,17.279,-1,17.108,-1.064,16.975,-1.14,16.88,-1.226,16.849,-1.272,16.78,-1.376,16.622,-1.699,16.541,-1.84,16.434,-1.961,16.274,-2.108,16.215,-2.178,16.192,-2.279,16.202,-2.465,16.217,-3.03,16.191,-3.194,16.147,-3.464,15.99,-3.766,15.872,-3.934,15.755,-3.986,15.6,-4.031,15.526,-4.088,15.481,-4.172,15.395,-4.245,15.267,-4.308,15.106,-4.461,14.912,-4.706,14.779,-4.846,14.708,-4.882,14.634,-4.885,14.558,-4.856,14.494,-4.852,14.462,-4.865,14.441,-4.854,14.411,-4.831,14.412,-4.775,14.403,-4.682,14.365,-4.586,14.41,-4.508,14.45,-4.45,14.443,-4.419,14.403,-4.37,14.358,-4.299,14.316,-4.304,14.227,-4.358,14.134,-4.4,14.047,-4.417,13.978,-4.461,13.941,-4.485,13.882,-4.485,13.85,-4.459,13.778,-4.434,13.739,-4.442,13.717,-4.454,13.708,-4.543,13.699,-4.618,13.685,-4.689,13.66,-4.721,13.552,-4.757,13.478,-4.805,13.415,-4.837,13.376,-4.829,13.297,-4.765,13.22,-4.706,13.176,-4.656,13.152,-4.62,13.137,-4.604,13.087,-4.602,13.073,-4.635,13.048,-4.619,12.971,-4.552,12.881,-4.445,12.848,-4.429,12.798,-4.431,12.719,-4.47,12.642,-4.531,12.501,-4.587,12.385,-4.619,12.374,-4.658,12.347,-4.724,12.308,-4.766,12.204,-4.779,12.167,-4.838,12.078,-4.952,12.018,-5.004,12.003,-4.982,11.967,-4.954,11.893,-4.866,11.821,-4.755,11.801,-4.705,11.781,-4.677,11.778,-4.566,11.668,-4.434,11.394,-4.2,11.364,-4.131,11.13,-3.916]]]},{"n":"Comoros","c":"KM","r":6,"ml":4,"lx":43.32,"ly":-11.73,"b":[43.227,-12.368,44.527,-11.368],"p":[[[44.476,-12.082,44.527,-12.22,44.526,-12.324,44.505,-12.357,44.46,-12.335,44.377,-12.252,44.22,-12.171,44.292,-12.165,44.334,-12.173,44.379,-12.166,44.407,-12.12,44.413,-12.093,44.452,-12.071,44.476,-12.082]],[[43.789,-12.307,43.859,-12.368,43.664,-12.343,43.633,-12.288,43.631,-12.247,43.704,-12.256,43.789,-12.307]],[[43.466,-11.901,43.447,-11.915,43.355,-11.858,43.303,-11.844,43.227,-11.752,43.256,-11.432,43.281,-11.391,43.299,-11.375,43.342,-11.368,43.393,-11.409,43.379,-11.614,43.448,-11.753,43.492,-11.862,43.466,-11.901]]]},{"n":"Colombia","c":"CO","r":2,"ml":3,"lx":-73.17,"ly":3.37,"b":[-79.025,-4.236,-66.876,12.434],"p":[[[-71.32,11.862,-71.356,11.85,-71.4,11.824,-71.536,11.774,-71.719,11.727,-71.958,11.666,-72.012,11.602,-72.248,11.196,-72.446,11.114,-72.518,11.054,-72.572,10.977,-72.69,10.836,-72.739,10.727,-72.869,10.491,-72.94,10.196,-72.967,10.03,-73.007,9.789,-73.064,9.668,-73.141,9.555,-73.224,9.444,-73.296,9.322,-73.356,9.227,-73.366,9.194,-73.337,9.168,-73.193,9.194,-73.137,9.223,-73.058,9.26,-73.009,9.24,-72.96,9.135,-72.904,9.122,-72.852,9.135,-72.796,9.109,-72.726,8.848,-72.665,8.628,-72.526,8.49,-72.417,8.382,-72.39,8.287,-72.364,8.153,-72.358,8.087,-72.392,8.048,-72.446,7.966,-72.46,7.81,-72.469,7.758,-72.479,7.613,-72.472,7.524,-72.443,7.455,-72.395,7.415,-72.296,7.395,-72.208,7.37,-72.157,7.25,-72.084,7.097,-72.007,7.033,-71.893,6.99,-71.811,7.006,-71.621,7.033,-71.457,7.026,-71.218,6.985,-71.129,6.987,-71.013,6.994,-70.811,7.078,-70.737,7.09,-70.655,7.083,-70.536,7.041,-70.471,7.007,-70.387,6.973,-70.266,6.948,-70.188,6.952,-70.129,6.954,-70.095,6.938,-69.904,6.7,-69.739,6.494,-69.595,6.321,-69.439,6.135,-69.427,6.124,-69.357,6.148,-69.311,6.138,-69.268,6.1,-69.195,6.115,-69.09,6.184,-68.937,6.198,-68.736,6.157,-68.472,6.157,-68.143,6.198,-67.939,6.242,-67.859,6.29,-67.727,6.285,-67.568,6.242,-67.482,6.18,-67.472,6.12,-67.439,6.026,-67.474,5.93,-67.575,5.833,-67.631,5.709,-67.642,5.559,-67.695,5.448,-67.788,5.375,-67.825,5.27,-67.804,5.133,-67.814,4.931,-67.855,4.665,-67.855,4.507,-67.814,4.455,-67.795,4.381,-67.799,4.284,-67.783,4.198,-67.732,4.087,-67.662,3.864,-67.603,3.769,-67.551,3.734,-67.499,3.691,-67.348,3.464,-67.311,3.416,-67.322,3.374,-67.336,3.343,-67.354,3.323,-67.515,3.187,-67.835,2.893,-67.861,2.855,-67.859,2.794,-67.766,2.833,-67.667,2.8,-67.619,2.794,-67.597,2.769,-67.568,2.69,-67.535,2.677,-67.486,2.644,-67.392,2.56,-67.312,2.472,-67.253,2.429,-67.211,2.39,-67.198,2.333,-67.215,2.275,-67.165,2.143,-67.131,2.101,-67.114,2.051,-67.131,2,-67.09,1.94,-67.044,1.823,-66.988,1.68,-66.982,1.601,-66.958,1.564,-66.931,1.458,-66.884,1.358,-66.896,1.29,-66.876,1.223,-67.065,1.178,-67.082,1.185,-67.094,1.21,-67.088,1.401,-67.09,1.616,-67.119,1.704,-67.206,1.845,-67.321,2.032,-67.352,2.086,-67.4,2.117,-67.458,2.121,-67.5,2.108,-67.556,2.073,-67.609,2.035,-67.712,1.922,-67.815,1.79,-67.876,1.761,-67.936,1.748,-67.99,1.753,-68.033,1.788,-68.077,1.86,-68.13,1.956,-68.194,1.987,-68.218,1.958,-68.239,1.901,-68.256,1.846,-68.213,1.775,-68.177,1.72,-68.24,1.722,-68.443,1.722,-68.678,1.721,-68.913,1.721,-69.124,1.721,-69.32,1.721,-69.394,1.726,-69.47,1.758,-69.543,1.773,-69.581,1.771,-69.65,1.739,-69.74,1.735,-69.8,1.705,-69.849,1.709,-69.849,1.544,-69.851,1.309,-69.852,1.06,-69.798,1.078,-69.751,1.077,-69.717,1.059,-69.621,1.073,-69.568,1.066,-69.517,1.059,-69.47,1.059,-69.442,1.039,-69.403,1.042,-69.361,1.064,-69.312,1.05,-69.259,1.015,-69.224,0.963,-69.194,0.898,-69.163,0.864,-69.165,0.802,-69.166,0.753,-69.177,0.713,-69.163,0.687,-69.153,0.659,-69.156,0.643,-69.174,0.635,-69.213,0.63,-69.254,0.625,-69.283,0.627,-69.306,0.652,-69.327,0.655,-69.359,0.652,-69.392,0.667,-69.421,0.698,-69.472,0.73,-69.527,0.716,-69.565,0.7,-69.604,0.68,-69.639,0.66,-69.674,0.665,-69.719,0.65,-69.757,0.626,-69.807,0.607,-69.862,0.598,-69.925,0.589,-69.985,0.586,-70.054,0.579,-70.058,0.447,-70.066,0.189,-70.071,0.019,-70.071,-0.139,-70.044,-0.196,-69.923,-0.317,-69.828,-0.381,-69.747,-0.453,-69.667,-0.482,-69.634,-0.509,-69.612,-0.553,-69.601,-0.6,-69.592,-0.639,-69.601,-0.681,-69.621,-0.721,-69.612,-0.763,-69.583,-0.796,-69.574,-0.838,-69.555,-0.877,-69.544,-0.917,-69.519,-0.946,-69.488,-0.966,-69.449,-0.999,-69.444,-1.03,-69.449,-1.065,-69.449,-1.092,-69.411,-1.152,-69.4,-1.195,-69.418,-1.246,-69.435,-1.422,-69.479,-1.622,-69.506,-1.775,-69.552,-2.024,-69.605,-2.314,-69.669,-2.668,-69.733,-3.017,-69.794,-3.355,-69.85,-3.66,-69.911,-3.997,-69.948,-4.201,-69.966,-4.236,-70.017,-4.162,-70.095,-4.092,-70.168,-4.05,-70.198,-3.995,-70.24,-3.883,-70.298,-3.844,-70.34,-3.814,-70.379,-3.819,-70.421,-3.85,-70.486,-3.869,-70.53,-3.866,-70.706,-3.789,-70.735,-3.782,-70.622,-3.605,-70.419,-3.288,-70.29,-3.087,-70.147,-2.864,-70.074,-2.75,-70.064,-2.731,-70.065,-2.702,-70.096,-2.658,-70.165,-2.64,-70.244,-2.607,-70.295,-2.553,-70.364,-2.529,-70.418,-2.491,-70.517,-2.453,-70.576,-2.418,-70.648,-2.406,-70.705,-2.342,-70.915,-2.219,-70.969,-2.207,-71.027,-2.226,-71.113,-2.245,-71.196,-2.313,-71.3,-2.335,-71.397,-2.334,-71.447,-2.294,-71.496,-2.279,-71.559,-2.224,-71.671,-2.182,-71.753,-2.153,-71.803,-2.166,-71.867,-2.228,-71.932,-2.289,-71.984,-2.327,-72.054,-2.325,-72.137,-2.381,-72.218,-2.4,-72.301,-2.409,-72.396,-2.429,-72.501,-2.395,-72.587,-2.365,-72.625,-2.352,-72.66,-2.361,-72.714,-2.392,-72.811,-2.405,-72.887,-2.408,-72.941,-2.394,-72.99,-2.34,-73.068,-2.312,-73.154,-2.278,-73.173,-2.208,-73.16,-2.156,-73.127,-2.081,-73.145,-2.003,-73.181,-1.88,-73.197,-1.83,-73.224,-1.788,-73.266,-1.772,-73.35,-1.784,-73.44,-1.737,-73.496,-1.693,-73.525,-1.639,-73.494,-1.537,-73.521,-1.45,-73.575,-1.401,-73.61,-1.316,-73.664,-1.249,-73.736,-1.214,-73.807,-1.218,-73.863,-1.197,-73.927,-1.125,-73.987,-1.098,-74.054,-1.029,-74.181,-0.998,-74.246,-0.971,-74.284,-0.928,-74.334,-0.851,-74.329,-0.808,-74.353,-0.767,-74.375,-0.691,-74.418,-0.581,-74.465,-0.518,-74.514,-0.47,-74.555,-0.43,-74.616,-0.37,-74.692,-0.335,-74.755,-0.299,-74.78,-0.245,-74.802,-0.2,-74.837,-0.203,-74.889,-0.199,-74.945,-0.188,-75.005,-0.156,-75.055,-0.117,-75.138,-0.05,-75.184,-0.042,-75.225,-0.042,-75.284,-0.107,-75.464,-0.038,-75.617,0.063,-75.777,0.089,-75.88,0.151,-75.975,0.248,-76.026,0.313,-76.068,0.346,-76.271,0.439,-76.311,0.448,-76.388,0.405,-76.413,0.379,-76.418,0.304,-76.427,0.261,-76.495,0.235,-76.603,0.241,-76.679,0.268,-76.729,0.272,-76.739,0.251,-76.768,0.242,-76.829,0.248,-76.92,0.269,-77.002,0.296,-77.114,0.355,-77.166,0.348,-77.293,0.36,-77.396,0.394,-77.423,0.425,-77.468,0.637,-77.481,0.651,-77.526,0.66,-77.601,0.69,-77.649,0.724,-77.673,0.782,-77.703,0.838,-77.83,0.825,-78.037,0.899,-78.181,0.969,-78.312,1.046,-78.512,1.199,-78.588,1.237,-78.682,1.283,-78.737,1.359,-78.829,1.435,-78.86,1.455,-78.888,1.524,-79.025,1.624,-78.958,1.752,-78.793,1.849,-78.577,1.774,-78.55,1.924,-78.629,2.056,-78.617,2.307,-78.592,2.357,-78.535,2.424,-78.46,2.47,-78.417,2.483,-78.343,2.461,-78.296,2.51,-78.12,2.488,-78.067,2.509,-78.03,2.543,-77.987,2.569,-77.932,2.629,-77.901,2.699,-77.875,2.726,-77.814,2.716,-77.808,2.746,-77.777,2.787,-77.67,2.879,-77.671,2.919,-77.701,3.008,-77.694,3.04,-77.632,3.051,-77.559,3.076,-77.52,3.16,-77.472,3.234,-77.417,3.342,-77.357,3.349,-77.324,3.475,-77.243,3.585,-77.077,3.913,-77.127,3.906,-77.167,3.862,-77.212,3.867,-77.264,3.893,-77.248,4.041,-77.278,4.058,-77.358,3.945,-77.427,4.06,-77.434,4.131,-77.404,4.201,-77.409,4.248,-77.521,4.213,-77.516,4.256,-77.446,4.301,-77.414,4.348,-77.354,4.398,-77.328,4.475,-77.314,4.594,-77.286,4.722,-77.307,4.785,-77.339,4.839,-77.367,5.077,-77.359,5.215,-77.373,5.324,-77.402,5.416,-77.534,5.537,-77.325,5.676,-77.249,5.78,-77.345,5.995,-77.469,6.177,-77.473,6.286,-77.44,6.272,-77.398,6.275,-77.36,6.504,-77.369,6.576,-77.439,6.69,-77.526,6.693,-77.602,6.837,-77.646,6.87,-77.681,6.96,-77.804,7.137,-77.901,7.229,-77.828,7.443,-77.765,7.484,-77.744,7.537,-77.769,7.668,-77.762,7.699,-77.747,7.712,-77.732,7.711,-77.706,7.691,-77.659,7.635,-77.619,7.565,-77.587,7.543,-77.538,7.566,-77.351,7.706,-77.363,7.749,-77.346,7.837,-77.283,7.908,-77.216,7.933,-77.196,7.972,-77.212,8.034,-77.283,8.187,-77.346,8.27,-77.386,8.352,-77.407,8.427,-77.479,8.498,-77.448,8.566,-77.393,8.645,-77.374,8.658,-77.344,8.637,-77.262,8.494,-77.13,8.401,-76.992,8.25,-76.936,8.147,-76.891,8.128,-76.852,8.09,-76.869,8.063,-76.912,8.033,-76.925,7.973,-76.897,7.939,-76.867,7.918,-76.787,7.932,-76.742,8.002,-76.772,8.311,-76.819,8.465,-76.872,8.513,-76.92,8.574,-76.888,8.62,-76.802,8.641,-76.689,8.695,-76.277,8.989,-76.135,9.266,-76.027,9.366,-75.905,9.431,-75.756,9.416,-75.639,9.45,-75.604,9.538,-75.635,9.658,-75.68,9.73,-75.637,9.834,-75.593,9.993,-75.596,10.126,-75.539,10.205,-75.558,10.236,-75.642,10.172,-75.708,10.143,-75.671,10.196,-75.554,10.328,-75.493,10.528,-75.446,10.611,-75.281,10.727,-75.248,10.783,-75.123,10.87,-74.922,11.058,-74.845,11.11,-74.454,10.989,-74.33,10.997,-74.352,10.975,-74.41,10.967,-74.492,10.934,-74.516,10.863,-74.46,10.787,-74.401,10.765,-74.35,10.814,-74.3,10.952,-74.219,11.105,-74.2,11.266,-74.143,11.321,-74.059,11.341,-73.91,11.309,-73.796,11.276,-73.677,11.271,-73.313,11.296,-72.722,11.712,-72.447,11.802,-72.275,11.889,-72.165,12.06,-72.136,12.189,-72.055,12.238,-71.97,12.238,-71.931,12.27,-71.919,12.309,-71.715,12.42,-71.597,12.434,-71.494,12.432,-71.262,12.335,-71.155,12.164,-71.137,12.046,-71.284,11.918,-71.32,11.862]],[[-78.114,2.542,-78.141,2.52,-78.192,2.559,-78.21,2.609,-78.178,2.646,-78.138,2.634,-78.119,2.604,-78.114,2.542]]]},{"n":"People's Republic of China","c":"CN","r":2,"ml":1.7,"lx":106.34,"ly":32.5,"b":[73.607,18.218,134.752,53.556],"p":[[[118.183,24.496,118.15,24.436,118.091,24.446,118.089,24.489,118.077,24.501,118.093,24.541,118.104,24.552,118.171,24.519,118.183,24.496]],[[121.863,31.492,121.78,31.464,121.52,31.55,121.336,31.644,121.227,31.758,121.211,31.805,121.339,31.797,121.464,31.756,121.492,31.694,121.542,31.674,121.577,31.637,121.808,31.552,121.844,31.526,121.863,31.492]],[[122.296,29.963,122.282,29.944,122.158,30.001,122.024,30.013,121.978,30.064,121.969,30.143,122.111,30.14,122.284,30.068,122.322,30.031,122.296,29.963]],[[122.173,29.679,122.169,29.66,122.084,29.725,122.043,29.736,122.062,29.773,122.12,29.782,122.165,29.701,122.173,29.679]],[[122.404,29.892,122.394,29.846,122.368,29.853,122.332,29.935,122.351,29.955,122.402,29.95,122.404,29.892]],[[119.821,25.457,119.747,25.411,119.7,25.433,119.699,25.495,119.723,25.551,119.696,25.591,119.723,25.639,119.778,25.653,119.797,25.623,119.829,25.607,119.838,25.591,119.839,25.56,119.809,25.508,119.832,25.48,119.821,25.457]],[[110.385,21.093,110.422,21.059,110.522,21.083,110.54,21.039,110.539,21.018,110.504,20.968,110.422,21.007,110.34,20.998,110.281,21.001,110.265,21.025,110.31,21.075,110.385,21.093]],[[121.251,28.086,121.164,28.063,121.132,28.063,121.134,28.135,121.205,28.204,121.234,28.181,121.251,28.145,121.251,28.086]],[[113.555,22.804,113.564,22.758,113.486,22.828,113.463,22.832,113.426,22.859,113.404,22.903,113.465,22.905,113.521,22.852,113.555,22.804]],[[112.79,21.602,112.771,21.582,112.742,21.618,112.733,21.67,112.713,21.698,112.761,21.733,112.782,21.772,112.839,21.765,112.863,21.753,112.813,21.712,112.801,21.695,112.79,21.602]],[[112.644,21.64,112.546,21.619,112.525,21.623,112.559,21.675,112.648,21.71,112.644,21.64]],[[107.973,21.508,107.908,21.56,107.802,21.645,107.759,21.655,107.641,21.614,107.471,21.598,107.433,21.642,107.351,21.609,107.272,21.711,107.179,21.717,107.062,21.794,107.02,21.835,107.006,21.893,106.971,21.924,106.925,21.92,106.875,21.951,106.794,21.982,106.729,22,106.698,21.986,106.664,21.979,106.658,22.018,106.66,22.136,106.654,22.241,106.637,22.289,106.593,22.325,106.554,22.342,106.536,22.395,106.55,22.501,106.582,22.573,106.633,22.586,106.702,22.638,106.736,22.711,106.78,22.779,106.624,22.874,106.542,22.908,106.451,22.894,106.338,22.863,106.279,22.857,106.249,22.869,106.184,22.955,106.148,22.97,106.068,22.976,106.001,22.975,105.962,22.937,105.903,22.925,105.843,22.923,105.782,22.969,105.691,23.03,105.548,23.073,105.531,23.122,105.495,23.181,105.44,23.235,105.35,23.308,105.275,23.345,105.239,23.322,105.189,23.281,104.996,23.194,104.91,23.161,104.865,23.136,104.827,23.1,104.815,23.011,104.796,22.911,104.74,22.86,104.687,22.822,104.632,22.818,104.578,22.82,104.527,22.804,104.372,22.704,104.298,22.712,104.238,22.769,104.213,22.809,104.143,22.8,104.054,22.752,104.013,22.666,103.991,22.586,103.971,22.55,103.942,22.54,103.915,22.538,103.637,22.77,103.62,22.782,103.571,22.734,103.525,22.612,103.493,22.588,103.471,22.597,103.356,22.755,103.327,22.77,103.301,22.764,103.266,22.714,103.193,22.639,103.138,22.593,103.136,22.542,103.076,22.498,103.005,22.453,102.982,22.448,102.935,22.466,102.874,22.525,102.83,22.587,102.721,22.648,102.599,22.7,102.517,22.741,102.471,22.751,102.428,22.733,102.406,22.708,102.376,22.647,102.302,22.546,102.237,22.466,102.176,22.415,102.127,22.379,102.092,22.412,102.024,22.439,101.945,22.439,101.842,22.388,101.76,22.49,101.739,22.495,101.708,22.487,101.671,22.462,101.646,22.405,101.62,22.327,101.568,22.276,101.525,22.254,101.537,22.21,101.562,22.162,101.56,22.121,101.576,22.055,101.603,21.99,101.7,21.882,101.737,21.827,101.744,21.778,101.747,21.606,101.743,21.534,101.724,21.395,101.723,21.315,101.763,21.279,101.802,21.236,101.801,21.213,101.783,21.204,101.728,21.156,101.705,21.15,101.669,21.17,101.622,21.184,101.584,21.204,101.542,21.234,101.444,21.231,101.281,21.184,101.248,21.197,101.224,21.224,101.212,21.278,101.22,21.342,101.206,21.383,101.175,21.408,101.197,21.522,101.139,21.567,101.147,21.582,101.128,21.705,101.131,21.736,101.121,21.746,101.08,21.756,101.019,21.736,100.835,21.655,100.677,21.505,100.605,21.472,100.531,21.458,100.446,21.484,100.351,21.501,100.215,21.463,100.148,21.481,100.117,21.511,100.089,21.558,100.106,21.617,100.096,21.661,100.041,21.683,99.978,21.702,99.941,21.759,99.926,21.821,99.94,21.902,99.948,21.988,99.918,22.028,99.825,22.05,99.593,22.089,99.389,22.111,99.303,22.101,99.233,22.11,99.193,22.126,99.173,22.153,99.172,22.192,99.205,22.283,99.243,22.37,99.338,22.498,99.343,22.587,99.338,22.689,99.385,22.825,99.467,22.927,99.507,22.959,99.497,23.005,99.465,23.046,99.418,23.069,99.341,23.096,99.22,23.103,99.055,23.131,98.864,23.191,98.886,23.307,98.883,23.38,98.859,23.44,98.82,23.483,98.798,23.52,98.832,23.624,98.788,23.738,98.735,23.783,98.681,23.842,98.677,23.905,98.702,23.964,98.834,24.091,98.835,24.121,98.802,24.119,98.764,24.116,98.583,24.07,98.564,24.099,98.499,24.116,98.367,24.119,98.213,24.111,98.017,24.065,97.838,23.986,97.756,23.932,97.686,23.898,97.63,23.887,97.565,23.911,97.568,23.988,97.691,24.131,97.708,24.229,97.671,24.313,97.667,24.38,97.624,24.423,97.563,24.444,97.531,24.492,97.529,24.631,97.583,24.775,97.671,24.82,97.724,24.842,97.738,24.87,97.711,24.97,97.715,25.034,97.767,25.158,97.82,25.252,97.918,25.236,97.962,25.259,98.011,25.293,98.064,25.349,98.1,25.416,98.143,25.571,98.173,25.595,98.297,25.569,98.334,25.587,98.402,25.678,98.466,25.789,98.558,25.823,98.625,25.827,98.656,25.864,98.655,25.918,98.591,26.004,98.564,26.072,98.572,26.114,98.663,26.139,98.686,26.189,98.672,26.299,98.709,26.43,98.732,26.583,98.739,26.698,98.738,26.786,98.729,26.877,98.717,27.045,98.675,27.191,98.682,27.245,98.677,27.422,98.651,27.572,98.6,27.599,98.504,27.648,98.453,27.657,98.409,27.639,98.392,27.587,98.35,27.538,98.299,27.55,98.274,27.599,98.241,27.663,98.13,27.968,98.118,28.055,98.099,28.142,98.062,28.186,98.022,28.212,97.934,28.314,97.888,28.356,97.865,28.364,97.817,28.356,97.769,28.356,97.73,28.407,97.695,28.469,97.659,28.5,97.599,28.517,97.538,28.51,97.502,28.456,97.478,28.426,97.431,28.354,97.356,28.254,97.322,28.218,97.289,28.237,97.145,28.34,97.075,28.369,96.981,28.338,96.833,28.362,96.776,28.367,96.653,28.45,96.603,28.46,96.428,28.406,96.389,28.368,96.366,28.367,96.32,28.387,96.281,28.412,96.279,28.428,96.326,28.469,96.33,28.497,96.327,28.525,96.396,28.607,96.581,28.764,96.55,28.83,96.477,28.959,96.467,29.022,96.436,29.051,96.347,29.027,96.162,28.91,96.137,28.923,96.141,28.963,96.122,29.082,96.181,29.118,96.271,29.161,96.34,29.21,96.356,29.249,96.337,29.261,96.235,29.246,96.195,29.272,96.129,29.381,96.08,29.424,96.035,29.447,95.885,29.391,95.71,29.314,95.516,29.206,95.517,29.151,95.494,29.137,95.457,29.102,95.42,29.054,95.389,29.037,95.353,29.036,95.279,29.05,95.145,29.104,94.999,29.149,94.967,29.144,94.769,29.176,94.763,29.201,94.733,29.252,94.677,29.297,94.623,29.312,94.468,29.216,94.293,29.145,94.193,29.06,94.112,28.976,94.018,28.96,94.013,28.908,93.974,28.861,93.902,28.803,93.761,28.73,93.665,28.69,93.361,28.654,93.252,28.629,93.207,28.591,93.158,28.493,93.119,28.402,93.035,28.328,92.882,28.228,92.702,28.147,92.653,28.093,92.643,28.062,92.666,28.05,92.688,28.026,92.688,27.989,92.664,27.949,92.547,27.879,92.481,27.846,92.415,27.825,92.341,27.821,92.27,27.83,92.25,27.842,92.222,27.827,92.158,27.812,92.101,27.808,91.978,27.73,91.909,27.73,91.825,27.746,91.713,27.76,91.632,27.76,91.629,27.801,91.642,27.923,91.606,27.952,91.493,27.982,91.368,28.022,91.307,28.064,91.273,28.078,91.226,28.071,91.15,28.027,91.078,27.974,91.021,27.97,90.963,27.995,90.907,28.027,90.716,28.072,90.63,28.079,90.477,28.071,90.353,28.08,90.333,28.094,90.334,28.119,90.352,28.168,90.363,28.217,90.348,28.244,90.221,28.278,90.104,28.302,89.981,28.311,89.898,28.294,89.817,28.256,89.75,28.188,89.653,28.158,89.537,28.107,89.481,28.06,89.396,27.958,89.273,27.833,89.16,27.711,89.102,27.593,89.025,27.518,88.948,27.464,88.891,27.316,88.833,27.363,88.765,27.43,88.749,27.522,88.83,27.767,88.849,27.869,88.829,27.907,88.804,28.007,88.756,28.04,88.621,28.092,88.578,28.093,88.532,28.057,88.486,28.034,88.426,28.012,88.275,27.969,88.141,27.949,88.109,27.933,88.099,27.905,88.11,27.871,88.023,27.883,87.933,27.891,87.861,27.886,87.683,27.821,87.623,27.815,87.555,27.822,87.464,27.824,87.291,27.822,87.141,27.838,87.02,27.929,86.934,27.968,86.842,27.999,86.75,28.022,86.72,28.071,86.691,28.095,86.614,28.103,86.554,28.085,86.517,27.964,86.485,27.94,86.409,27.929,86.329,27.96,86.218,28.022,86.174,28.092,86.137,28.114,86.079,28.084,86.075,27.995,86.064,27.935,85.995,27.91,85.954,27.928,85.922,27.99,85.84,28.135,85.759,28.221,85.678,28.277,85.411,28.276,85.212,28.293,85.122,28.316,85.089,28.372,85.121,28.484,85.16,28.572,85.159,28.592,85.126,28.603,85.069,28.61,84.855,28.554,84.797,28.56,84.759,28.579,84.714,28.596,84.677,28.622,84.651,28.66,84.465,28.753,84.411,28.804,84.312,28.868,84.229,28.912,84.176,29.036,84.128,29.156,84.101,29.22,84.022,29.254,83.936,29.279,83.79,29.227,83.671,29.188,83.583,29.184,83.457,29.306,83.355,29.439,83.235,29.555,83.155,29.613,83.014,29.618,82.854,29.683,82.641,29.831,82.487,29.942,82.221,30.064,82.159,30.115,82.135,30.159,82.099,30.245,82.043,30.327,81.855,30.362,81.642,30.388,81.417,30.338,81.255,30.093,81.177,30.04,81.11,30.037,81.056,30.099,81.01,30.165,80.985,30.237,80.874,30.291,80.747,30.36,80.682,30.415,80.609,30.449,80.541,30.464,80.41,30.509,80.261,30.561,80.191,30.568,80.186,30.605,80.207,30.684,80.194,30.759,80.149,30.79,80.081,30.782,79.925,30.889,79.919,30.89,79.917,30.894,79.872,30.925,79.795,30.968,79.664,30.965,79.565,30.949,79.493,30.994,79.388,31.064,79.37,31.08,79.339,31.106,79.233,31.242,79.107,31.403,79.044,31.426,79.011,31.414,78.974,31.329,78.946,31.337,78.9,31.331,78.845,31.302,78.792,31.294,78.758,31.302,78.744,31.324,78.759,31.437,78.727,31.472,78.755,31.55,78.803,31.618,78.754,31.668,78.693,31.74,78.687,31.806,78.72,31.888,78.735,31.958,78.726,31.984,78.678,32.023,78.496,32.216,78.486,32.236,78.455,32.3,78.441,32.397,78.417,32.467,78.39,32.52,78.392,32.545,78.413,32.558,78.526,32.571,78.632,32.579,78.701,32.597,78.737,32.558,78.754,32.499,78.771,32.468,78.838,32.412,78.919,32.358,78.998,32.365,79.067,32.388,79.127,32.476,79.17,32.497,79.219,32.501,79.219,32.508,79.217,32.564,79.234,32.703,79.228,32.759,79.206,32.809,79.21,32.865,79.202,32.946,79.146,33.001,79.109,33.023,79.103,33.053,79.122,33.108,79.135,33.172,79.113,33.226,79.067,33.25,79.013,33.291,78.948,33.347,78.917,33.387,78.865,33.431,78.802,33.5,78.79,33.65,78.784,33.809,78.762,33.888,78.727,34.013,78.732,34.056,78.753,34.088,78.932,34.189,78.971,34.228,78.977,34.258,78.97,34.303,78.936,34.352,78.865,34.39,78.763,34.453,78.671,34.518,78.516,34.558,78.327,34.606,78.282,34.654,78.236,34.77,78.158,34.946,78.076,35.135,78.012,35.251,78.009,35.307,78.047,35.449,78.043,35.48,78.009,35.49,77.946,35.472,77.895,35.449,77.852,35.461,77.811,35.485,77.803,35.493,77.799,35.496,77.724,35.481,77.573,35.472,77.52,35.473,77.446,35.476,77.295,35.508,77.09,35.552,76.879,35.613,76.767,35.662,76.728,35.679,76.632,35.729,76.563,35.773,76.551,35.887,76.502,35.878,76.386,35.837,76.252,35.811,76.178,35.811,76.148,35.829,76.103,35.949,76.071,35.983,76.01,35.996,75.945,36.018,75.912,36.049,75.905,36.088,75.934,36.134,75.969,36.169,75.974,36.382,75.952,36.458,75.933,36.522,75.885,36.601,75.84,36.65,75.772,36.695,75.667,36.742,75.574,36.759,75.46,36.725,75.424,36.738,75.377,36.884,75.347,36.913,75.145,36.973,75.054,36.987,74.949,36.968,74.889,36.952,74.841,36.979,74.766,37.013,74.692,37.036,74.601,37.037,74.541,37.022,74.526,37.031,74.498,37.057,74.376,37.137,74.372,37.158,74.559,37.237,74.669,37.267,74.727,37.291,74.739,37.286,74.767,37.249,74.84,37.225,74.891,37.232,74.918,37.25,75.008,37.294,75.079,37.344,75.119,37.386,75.097,37.451,74.986,37.53,74.916,37.573,74.894,37.601,74.912,37.687,74.938,37.773,74.921,37.805,74.9,37.833,74.891,37.926,74.842,38.038,74.79,38.104,74.775,38.192,74.772,38.275,74.836,38.404,74.812,38.46,74.745,38.51,74.514,38.6,74.277,38.66,74.187,38.658,74.131,38.661,74.065,38.608,74.026,38.54,73.97,38.534,73.869,38.563,73.802,38.607,73.754,38.699,73.717,38.817,73.696,38.854,73.707,38.886,73.73,38.915,73.795,38.941,73.805,38.969,73.796,39.002,73.744,39.045,73.69,39.105,73.607,39.229,73.623,39.298,73.636,39.397,73.632,39.449,73.716,39.462,73.823,39.489,73.873,39.533,73.907,39.579,73.915,39.606,73.883,39.715,73.84,39.763,73.835,39.8,73.856,39.829,73.885,39.878,73.939,39.979,73.992,40.043,74.021,40.059,74.085,40.074,74.243,40.092,74.412,40.137,74.613,40.272,74.68,40.311,74.768,40.33,74.83,40.329,74.842,40.345,74.801,40.429,74.811,40.459,74.835,40.483,74.866,40.494,75.004,40.45,75.111,40.454,75.241,40.48,75.521,40.628,75.556,40.625,75.583,40.605,75.617,40.517,75.656,40.329,75.677,40.306,75.872,40.303,76.004,40.371,76.062,40.388,76.157,40.376,76.206,40.408,76.258,40.431,76.319,40.352,76.396,40.39,76.48,40.45,76.521,40.511,76.578,40.578,76.622,40.662,76.64,40.742,76.661,40.78,76.708,40.818,76.824,40.982,76.908,41.024,76.987,41.039,77.182,41.011,77.284,41.014,77.582,40.993,77.719,41.024,77.815,41.056,77.956,41.051,78.123,41.076,78.346,41.281,78.349,41.325,78.362,41.372,78.443,41.418,78.543,41.46,78.743,41.56,79.148,41.719,79.294,41.783,79.354,41.781,79.504,41.821,79.766,41.899,79.84,41.996,79.91,42.015,80.216,42.032,80.235,42.043,80.246,42.06,80.229,42.13,80.209,42.19,80.233,42.208,80.259,42.235,80.255,42.274,80.206,42.399,80.179,42.518,80.162,42.626,80.165,42.666,80.202,42.734,80.25,42.797,80.424,42.856,80.539,42.873,80.544,42.912,80.451,42.936,80.383,42.974,80.371,42.996,80.375,43.02,80.39,43.043,80.507,43.086,80.617,43.128,80.751,43.102,80.778,43.119,80.786,43.162,80.757,43.204,80.73,43.274,80.668,43.31,80.665,43.353,80.704,43.427,80.651,43.564,80.593,43.685,80.496,43.892,80.432,43.952,80.396,44.047,80.355,44.097,80.359,44.171,80.365,44.223,80.355,44.327,80.336,44.438,80.355,44.552,80.391,44.627,80.381,44.655,80.401,44.677,80.455,44.684,80.482,44.715,80.455,44.746,80.361,44.77,80.255,44.808,80.128,44.804,79.997,44.797,79.932,44.825,79.875,44.861,79.872,44.884,79.95,44.944,80.059,45.006,80.228,45.034,80.415,45.075,80.509,45.105,80.635,45.127,80.78,45.136,80.853,45.129,81.04,45.169,81.335,45.246,81.602,45.311,81.692,45.349,81.759,45.311,81.79,45.226,81.867,45.182,81.945,45.161,81.989,45.162,82.123,45.195,82.267,45.219,82.323,45.206,82.397,45.162,82.479,45.124,82.521,45.125,82.559,45.155,82.597,45.216,82.621,45.293,82.626,45.374,82.612,45.424,82.583,45.443,82.452,45.472,82.327,45.52,82.312,45.564,82.315,45.595,82.348,45.672,82.43,45.812,82.512,46.006,82.555,46.159,82.692,46.387,82.8,46.624,82.975,46.966,83.004,47.033,83.02,47.141,83.029,47.186,83.09,47.209,83.193,47.187,83.444,47.109,83.634,47.043,83.714,47.021,83.833,46.998,84.016,46.971,84.122,46.979,84.215,46.995,84.339,46.996,84.532,46.976,84.592,46.975,84.667,46.972,84.72,46.939,84.746,46.864,84.786,46.831,84.858,46.843,85.012,46.909,85.111,46.961,85.233,47.036,85.355,47.047,85.485,47.064,85.53,47.101,85.577,47.188,85.657,47.255,85.67,47.338,85.642,47.397,85.587,47.494,85.588,47.558,85.562,47.746,85.526,47.916,85.562,48.052,85.626,48.204,85.652,48.251,85.692,48.312,85.749,48.385,85.83,48.408,86.056,48.424,86.266,48.455,86.373,48.486,86.483,48.505,86.549,48.529,86.664,48.636,86.718,48.697,86.758,48.861,86.729,48.939,86.753,49.009,86.808,49.05,86.886,49.091,86.938,49.098,87.049,49.11,87.23,49.106,87.323,49.086,87.417,49.077,87.476,49.091,87.516,49.122,87.577,49.132,87.668,49.147,87.763,49.166,87.814,49.162,87.825,49.116,87.816,49.08,87.835,49.032,87.872,49,87.86,48.966,87.807,48.946,87.755,48.919,87.743,48.882,87.809,48.836,87.832,48.792,87.942,48.765,88.028,48.736,88.06,48.707,88.05,48.675,88.011,48.64,87.972,48.603,87.967,48.581,87.98,48.555,88.063,48.538,88.158,48.509,88.31,48.472,88.414,48.403,88.517,48.384,88.567,48.317,88.576,48.22,88.682,48.171,88.838,48.102,88.918,48.089,88.971,48.05,89.048,48.003,89.116,47.988,89.196,47.981,89.33,48.025,89.479,48.029,89.561,48.004,89.638,47.909,89.693,47.879,89.726,47.852,89.778,47.827,89.831,47.823,89.91,47.844,89.959,47.886,90.028,47.878,90.054,47.85,90.067,47.804,90.103,47.745,90.191,47.702,90.313,47.676,90.331,47.655,90.347,47.597,90.381,47.557,90.425,47.504,90.467,47.408,90.476,47.329,90.496,47.285,90.553,47.214,90.643,47.1,90.716,47.004,90.799,46.985,90.87,46.954,90.911,46.883,90.986,46.749,90.998,46.661,91.004,46.596,91.029,46.566,91.034,46.529,90.971,46.388,90.918,46.324,90.912,46.271,90.948,46.177,90.997,46.105,91.002,46.036,90.96,45.985,90.887,45.922,90.852,45.885,90.796,45.854,90.71,45.731,90.671,45.595,90.662,45.525,90.694,45.475,90.75,45.419,90.763,45.371,90.853,45.263,90.877,45.196,90.914,45.194,90.954,45.216,91.05,45.217,91.138,45.194,91.222,45.145,91.312,45.118,91.441,45.125,91.51,45.098,91.584,45.077,91.738,45.069,91.853,45.069,92.03,45.069,92.173,45.035,92.424,45.009,92.579,45.011,92.788,45.036,92.916,45.02,93.294,44.983,93.516,44.944,93.656,44.901,93.755,44.832,93.868,44.724,93.958,44.675,94.199,44.645,94.365,44.519,94.494,44.473,94.712,44.351,94.866,44.303,95.05,44.259,95.35,44.278,95.367,44.262,95.344,44.195,95.326,44.105,95.326,44.039,95.356,44.006,95.471,43.986,95.526,43.954,95.567,43.892,95.591,43.854,95.687,43.664,95.842,43.384,95.86,43.276,95.913,43.206,96.08,43.096,96.168,43.015,96.3,42.929,96.342,42.849,96.352,42.747,96.385,42.72,96.625,42.744,96.833,42.76,97.206,42.79,97.719,42.736,98.248,42.685,98.716,42.639,98.947,42.616,99.468,42.568,99.757,42.629,99.984,42.677,100.086,42.671,100.519,42.617,100.773,42.588,101.092,42.551,101.314,42.538,101.495,42.539,101.579,42.524,101.66,42.5,101.714,42.466,101.88,42.292,101.973,42.216,102.157,42.158,102.575,42.092,102.807,42.052,103.073,42.006,103.248,41.937,103.45,41.856,103.711,41.751,103.997,41.797,104.305,41.846,104.498,41.877,104.498,41.659,104.774,41.641,104.86,41.644,104.982,41.596,105.051,41.616,105.115,41.663,105.197,41.738,105.314,41.771,105.517,41.855,105.566,41.875,105.868,41.994,106.317,42.141,106.519,42.212,106.579,42.227,106.693,42.264,106.77,42.289,106.906,42.309,107.091,42.322,107.292,42.349,107.749,42.401,107.806,42.406,108.062,42.427,108.171,42.447,108.334,42.437,108.546,42.429,108.687,42.416,108.875,42.426,109.132,42.441,109.34,42.438,109.443,42.456,109.596,42.511,109.698,42.554,109.859,42.606,110.058,42.661,110.197,42.71,110.289,42.743,110.4,42.774,110.43,42.814,110.462,42.844,110.521,42.895,110.628,42.991,110.709,43.074,110.749,43.111,110.84,43.194,110.913,43.257,111.007,43.341,111.087,43.369,111.187,43.392,111.451,43.475,111.504,43.493,111.547,43.496,111.641,43.563,111.72,43.621,111.771,43.665,111.878,43.68,111.933,43.711,111.943,43.752,111.932,43.815,111.88,43.879,111.837,43.935,111.684,44.041,111.603,44.107,111.52,44.192,111.486,44.272,111.43,44.322,111.402,44.367,111.411,44.419,111.489,44.512,111.515,44.57,111.547,44.673,111.621,44.827,111.681,44.899,111.751,44.97,111.898,45.064,112.033,45.082,112.113,45.063,112.292,45.063,112.411,45.058,112.499,45.011,112.597,44.918,112.707,44.883,113.049,44.81,113.196,44.795,113.301,44.792,113.456,44.767,113.508,44.762,113.587,44.746,113.653,44.763,113.752,44.826,113.877,44.896,113.931,44.912,114.03,44.943,114.08,44.971,114.167,45.05,114.281,45.111,114.419,45.203,114.487,45.272,114.502,45.316,114.517,45.365,114.56,45.39,114.644,45.413,114.739,45.42,114.919,45.378,115.163,45.39,115.217,45.396,115.439,45.42,115.539,45.44,115.681,45.458,115.789,45.535,115.934,45.626,116.04,45.677,116.11,45.687,116.198,45.739,116.241,45.796,116.229,45.846,116.213,45.887,116.265,45.963,116.358,46.097,116.445,46.159,116.517,46.209,116.563,46.29,116.619,46.313,116.689,46.322,116.787,46.377,116.859,46.388,116.979,46.362,117.156,46.355,117.269,46.352,117.333,46.362,117.357,46.391,117.356,46.437,117.392,46.538,117.406,46.571,117.438,46.586,117.547,46.588,117.621,46.552,117.671,46.522,117.741,46.518,117.813,46.538,117.91,46.619,118.071,46.667,118.157,46.679,118.309,46.717,118.404,46.703,118.58,46.692,118.649,46.702,118.723,46.692,118.79,46.747,118.844,46.76,118.957,46.735,119.029,46.692,119.162,46.639,119.332,46.614,119.474,46.627,119.62,46.604,119.707,46.606,119.747,46.627,119.867,46.672,119.896,46.733,119.884,46.791,119.898,46.858,119.863,46.907,119.788,46.979,119.76,47.027,119.757,47.09,119.711,47.15,119.6,47.222,119.527,47.256,119.377,47.381,119.326,47.41,119.309,47.431,119.291,47.473,119.235,47.493,119.162,47.525,119.123,47.558,119.097,47.616,119.082,47.654,119.018,47.685,118.953,47.703,118.88,47.725,118.76,47.758,118.691,47.822,118.568,47.943,118.498,47.984,118.24,48,118.147,48.029,118.042,48.019,117.979,48,117.84,48,117.768,47.988,117.677,47.908,117.555,47.805,117.455,47.741,117.384,47.676,117.351,47.652,117.286,47.666,117.197,47.74,117.07,47.806,116.952,47.837,116.901,47.853,116.761,47.87,116.652,47.865,116.513,47.84,116.378,47.844,116.317,47.86,116.231,47.858,116.075,47.79,115.994,47.711,115.898,47.687,115.812,47.738,115.712,47.799,115.616,47.875,115.558,47.945,115.525,48.131,115.639,48.186,115.786,48.248,115.797,48.346,115.792,48.456,115.821,48.577,115.954,48.689,116.025,48.782,116.034,48.84,116.098,48.936,116.16,49.037,116.243,49.17,116.402,49.406,116.59,49.685,116.683,49.824,116.889,49.738,117.022,49.693,117.246,49.625,117.477,49.609,117.698,49.536,117.813,49.514,117.873,49.513,118.187,49.693,118.452,49.844,118.756,49.963,118.979,49.979,119.147,50.013,119.26,50.066,119.326,50.155,119.346,50.279,119.302,50.354,119.192,50.38,119.164,50.406,119.217,50.433,119.256,50.484,119.281,50.561,119.344,50.634,119.446,50.703,119.502,50.779,119.512,50.863,119.573,50.947,119.685,51.03,119.746,51.108,119.757,51.179,119.813,51.267,119.967,51.422,120.067,51.601,120.237,51.723,120.511,51.849,120.681,51.973,120.75,52.097,120.745,52.205,120.665,52.3,120.65,52.396,120.699,52.494,120.656,52.567,120.521,52.615,120.36,52.627,120.173,52.602,120.068,52.633,120.044,52.718,120.095,52.787,120.218,52.84,120.421,52.968,120.704,53.172,120.985,53.285,121.405,53.317,121.744,53.384,122.089,53.451,122.338,53.485,122.38,53.463,122.516,53.457,122.745,53.469,122.958,53.498,123.154,53.545,123.31,53.556,123.424,53.531,123.489,53.529,123.535,53.526,123.56,53.527,123.608,53.547,123.741,53.511,123.995,53.406,124.154,53.359,124.22,53.37,124.291,53.341,124.369,53.271,124.466,53.23,124.64,53.211,124.812,53.134,124.882,53.13,124.907,53.173,124.971,53.197,125.075,53.204,125.226,53.166,125.422,53.084,125.546,53.048,125.596,53.057,125.649,53.042,125.692,53.004,125.695,52.956,125.681,52.931,125.728,52.891,125.783,52.891,125.872,52.872,125.942,52.801,126.004,52.768,126.048,52.739,126.056,52.716,126.06,52.692,126.047,52.673,126.023,52.643,126.016,52.61,126.046,52.573,126.157,52.547,126.194,52.519,126.203,52.484,126.238,52.445,126.313,52.4,126.342,52.362,126.324,52.332,126.346,52.306,126.383,52.287,126.392,52.215,126.395,52.173,126.456,52.126,126.468,52.031,126.511,51.926,126.654,51.781,126.701,51.703,126.689,51.61,126.709,51.566,126.775,51.545,126.805,51.506,126.802,51.448,126.827,51.412,126.848,51.374,126.834,51.315,126.854,51.261,126.888,51.23,126.912,51.172,126.925,51.1,127.02,50.986,127.198,50.829,127.307,50.708,127.347,50.621,127.347,50.55,127.308,50.494,127.306,50.454,127.341,50.428,127.351,50.394,127.337,50.35,127.395,50.299,127.59,50.209,127.512,50.072,127.492,49.975,127.502,49.873,127.551,49.802,127.637,49.76,127.69,49.717,127.711,49.672,127.814,49.622,128,49.569,128.237,49.559,128.527,49.594,128.704,49.6,128.769,49.577,128.791,49.542,128.77,49.495,128.819,49.464,128.938,49.449,129.02,49.419,129.065,49.375,129.12,49.362,129.185,49.381,129.248,49.379,129.31,49.354,129.35,49.362,129.385,49.389,129.441,49.389,129.498,49.389,129.534,49.323,129.591,49.287,129.671,49.279,129.793,49.199,130.037,48.972,130.196,48.892,130.355,48.866,130.553,48.861,130.617,48.773,130.566,48.68,130.552,48.602,130.597,48.575,130.659,48.483,130.747,48.43,130.763,48.388,130.804,48.342,130.787,48.255,130.712,48.128,130.733,48.019,130.849,47.929,130.915,47.843,130.933,47.76,130.962,47.709,131.003,47.691,131.122,47.698,131.319,47.728,131.464,47.723,131.557,47.682,131.785,47.681,132.15,47.718,132.38,47.729,132.476,47.715,132.562,47.769,132.637,47.89,132.707,47.947,132.773,47.94,132.877,47.979,133.02,48.064,133.144,48.106,133.301,48.102,133.468,48.097,133.573,48.133,133.672,48.208,133.842,48.274,134.206,48.36,134.293,48.373,134.335,48.369,134.456,48.355,134.564,48.322,134.665,48.254,134.681,48.21,134.669,48.153,134.647,48.12,134.605,48.083,134.566,48.023,134.591,47.975,134.65,47.874,134.699,47.801,134.752,47.715,134.728,47.684,134.696,47.625,134.596,47.524,134.542,47.485,134.483,47.447,134.383,47.438,134.339,47.429,134.291,47.414,134.26,47.378,134.225,47.353,134.168,47.302,134.163,47.259,134.189,47.194,134.202,47.128,134.137,47.069,134.086,46.978,134.071,46.951,134.046,46.882,134.039,46.858,134.023,46.713,133.958,46.614,133.867,46.499,133.887,46.431,133.903,46.367,133.88,46.336,133.875,46.309,133.861,46.248,133.833,46.224,133.75,46.186,133.701,46.14,133.711,46.07,133.686,46.009,133.648,45.955,133.608,45.92,133.551,45.898,133.513,45.879,133.485,45.81,133.476,45.758,133.449,45.705,133.466,45.651,133.436,45.605,133.355,45.572,133.31,45.553,133.267,45.545,133.186,45.495,133.113,45.321,133.097,45.22,133.113,45.131,133.012,45.075,132.936,45.03,132.889,45.046,132.839,45.061,132.723,45.081,132.666,45.094,132.549,45.123,132.363,45.16,132.181,45.203,132.067,45.226,131.978,45.244,131.909,45.274,131.852,45.327,131.795,45.305,131.742,45.243,131.654,45.205,131.614,45.137,131.579,45.084,131.488,45.013,131.447,44.984,131.268,44.936,131.228,44.92,131.082,44.91,131.033,44.889,130.982,44.844,130.968,44.8,131.004,44.753,131.061,44.66,131.087,44.596,131.126,44.469,131.255,44.072,131.213,44.003,131.174,43.705,131.184,43.651,131.18,43.567,131.182,43.506,131.209,43.49,131.244,43.469,131.262,43.433,131.257,43.378,131.239,43.338,131.212,43.258,131.176,43.142,131.136,43.098,131.109,43.062,131.086,43.038,131.083,42.956,131.069,42.902,131.006,42.883,130.943,42.852,130.869,42.863,130.803,42.857,130.722,42.836,130.577,42.812,130.493,42.779,130.453,42.755,130.425,42.727,130.42,42.7,130.439,42.686,130.521,42.674,130.577,42.623,130.584,42.567,130.527,42.535,130.498,42.571,130.45,42.582,130.361,42.631,130.296,42.685,130.247,42.745,130.249,42.873,130.24,42.892,130.151,42.918,130.125,42.956,130.083,42.974,130.022,42.963,129.977,42.975,129.941,42.996,129.898,42.998,129.861,42.965,129.842,42.894,129.779,42.777,129.773,42.705,129.746,42.604,129.72,42.475,129.698,42.448,129.628,42.444,129.604,42.436,129.568,42.392,129.524,42.385,129.485,42.41,129.424,42.436,129.366,42.439,129.314,42.414,129.253,42.358,129.218,42.313,129.205,42.271,129.196,42.218,129.134,42.169,129.077,42.142,128.961,42.069,128.923,42.038,128.84,42.038,128.749,42.041,128.627,42.021,128.427,42.011,128.308,42.026,128.16,42.012,128.045,41.988,128.029,41.952,128.033,41.898,128.056,41.864,128.084,41.841,128.132,41.769,128.182,41.7,128.258,41.655,128.289,41.607,128.291,41.563,128.255,41.507,128.2,41.433,128.149,41.388,128.111,41.389,128.053,41.416,128.013,41.449,127.919,41.461,127.688,41.44,127.572,41.455,127.517,41.482,127.42,41.484,127.271,41.52,127.18,41.531,127.137,41.555,127.128,41.607,127.085,41.644,127.061,41.687,127.007,41.742,126.955,41.769,126.904,41.781,126.847,41.748,126.788,41.718,126.743,41.725,126.722,41.717,126.697,41.692,126.601,41.641,126.578,41.594,126.54,41.496,126.514,41.394,126.49,41.358,126.451,41.352,126.412,41.321,126.329,41.226,126.254,41.138,126.145,41.078,126.093,41.024,126.067,40.974,125.989,40.905,125.875,40.892,125.784,40.872,125.728,40.867,125.688,40.839,125.659,40.796,125.645,40.779,125.594,40.779,125.543,40.743,125.417,40.66,125.314,40.645,125.186,40.589,125.073,40.547,125.026,40.524,125.013,40.498,124.997,40.465,124.942,40.458,124.889,40.46,124.772,40.384,124.712,40.319,124.481,40.182,124.387,40.104,124.362,40.004,124.35,40.012,124.267,39.924,124.106,39.841,123.76,39.822,123.651,39.882,123.611,39.841,123.581,39.786,123.49,39.768,123.348,39.763,123.269,39.727,123.227,39.687,123.032,39.674,122.961,39.62,122.84,39.601,122.335,39.366,122.225,39.267,122.121,39.152,122.048,39.094,121.982,39.053,121.923,39.037,121.864,38.996,121.805,38.991,121.745,39.01,121.677,39.003,121.633,38.955,121.67,38.892,121.65,38.865,121.517,38.831,121.32,38.808,121.236,38.767,121.207,38.744,121.164,38.732,121.122,38.813,121.107,38.921,121.188,38.947,121.263,38.96,121.68,39.109,121.628,39.22,121.665,39.269,121.758,39.348,121.818,39.387,121.785,39.401,121.513,39.375,121.356,39.377,121.275,39.385,121.3,39.452,121.286,39.519,121.267,39.545,121.406,39.621,121.47,39.64,121.518,39.639,121.514,39.685,121.474,39.755,121.517,39.845,121.801,39.951,121.869,40.046,121.983,40.136,122.191,40.358,122.203,40.396,122.264,40.5,122.275,40.542,122.179,40.603,122.14,40.688,121.859,40.842,121.835,40.974,121.809,40.969,121.766,40.876,121.729,40.846,121.599,40.843,121.537,40.878,121.175,40.901,121.086,40.842,121.003,40.749,120.922,40.683,120.841,40.649,120.771,40.589,120.479,40.231,120.369,40.204,119.85,39.987,119.591,39.903,119.391,39.752,119.322,39.662,119.261,39.561,119.225,39.408,119.04,39.222,118.977,39.183,118.912,39.166,118.826,39.172,118.752,39.16,118.626,39.177,118.472,39.118,118.298,39.067,118.148,39.195,118.041,39.227,117.866,39.191,117.785,39.134,117.617,38.853,117.554,38.691,117.558,38.625,117.656,38.424,117.767,38.312,118.015,38.183,118.543,38.095,118.667,38.126,118.8,38.127,118.94,38.043,119.028,37.904,119.036,37.809,119.038,37.777,119.07,37.749,119.089,37.701,119.033,37.661,118.991,37.641,118.955,37.494,118.953,37.331,118.998,37.277,119.112,37.201,119.287,37.138,119.45,37.125,119.761,37.155,119.888,37.253,119.88,37.296,119.883,37.351,120.156,37.495,120.312,37.623,120.287,37.656,120.257,37.679,120.285,37.692,120.37,37.701,120.75,37.834,121.049,37.725,121.22,37.6,121.388,37.579,121.505,37.515,121.64,37.46,121.816,37.457,121.965,37.445,122.01,37.496,122.057,37.529,122.11,37.522,122.169,37.456,122.338,37.405,122.493,37.408,122.602,37.426,122.667,37.403,122.573,37.318,122.587,37.181,122.516,37.138,122.447,37.068,122.487,37.022,122.523,37.003,122.52,36.947,122.457,36.915,122.341,36.832,122.274,36.834,122.242,36.85,122.22,36.88,122.203,36.927,122.162,36.959,122.05,36.971,121.933,36.959,121.67,36.836,121.413,36.738,121.144,36.66,121.054,36.611,120.99,36.598,120.879,36.635,120.811,36.633,120.797,36.607,120.883,36.539,120.905,36.485,120.896,36.444,120.847,36.426,120.776,36.456,120.712,36.413,120.682,36.341,120.681,36.168,120.638,36.13,120.519,36.109,120.393,36.054,120.348,36.079,120.33,36.11,120.343,36.189,120.328,36.228,120.27,36.226,120.183,36.202,120.117,36.15,120.094,36.119,120.181,36.017,120.265,36.007,120.285,35.984,120.219,35.935,120.055,35.861,120.027,35.799,119.979,35.74,119.912,35.693,119.866,35.644,119.811,35.618,119.72,35.589,119.608,35.47,119.526,35.359,119.43,35.301,119.353,35.114,119.216,35.012,119.165,34.849,119.201,34.748,119.351,34.749,119.427,34.714,119.583,34.582,119.77,34.496,119.964,34.448,120.201,34.326,120.267,34.274,120.323,34.169,120.426,33.866,120.5,33.716,120.505,33.638,120.616,33.491,120.734,33.237,120.871,33.017,120.897,32.843,120.853,32.764,120.853,32.661,120.99,32.567,121.293,32.457,121.342,32.425,121.401,32.372,121.404,32.206,121.451,32.153,121.491,32.121,121.674,32.051,121.751,31.993,121.832,31.9,121.856,31.816,121.866,31.704,121.764,31.7,121.681,31.712,121.352,31.859,121.266,31.863,121.146,31.842,120.974,31.869,120.792,32.032,120.661,32.081,120.52,32.106,120.184,31.966,120.099,31.976,120.074,31.96,120.036,31.936,120.192,31.906,120.347,31.952,120.497,32.02,120.716,31.984,120.752,31.923,120.788,31.82,120.938,31.75,121.055,31.719,121.205,31.628,121.351,31.485,121.661,31.32,121.786,31.163,121.834,31.062,121.878,30.917,121.769,30.87,121.675,30.864,121.528,30.841,121.419,30.79,121.31,30.7,120.998,30.558,120.938,30.47,120.897,30.393,120.821,30.355,120.63,30.391,120.45,30.388,120.246,30.284,120.195,30.241,120.229,30.25,120.261,30.263,120.353,30.247,120.495,30.303,120.633,30.133,120.904,30.161,121.159,30.302,121.258,30.304,121.341,30.282,121.433,30.227,121.678,29.979,121.812,29.952,121.944,29.894,122.017,29.888,122.083,29.87,121.906,29.78,121.677,29.584,121.575,29.537,121.506,29.485,121.69,29.511,121.822,29.605,121.888,29.628,121.941,29.606,121.968,29.491,121.918,29.135,121.854,29.129,121.791,29.226,121.717,29.256,121.656,29.236,121.534,29.237,121.487,29.193,121.448,29.131,121.521,29.118,121.665,29.011,121.68,28.953,121.641,28.916,121.54,28.932,121.663,28.851,121.63,28.768,121.59,28.735,121.519,28.714,121.475,28.641,121.538,28.521,121.602,28.367,121.61,28.292,121.51,28.324,121.355,28.23,121.272,28.222,121.217,28.346,121.146,28.327,121.098,28.291,121.035,28.157,120.959,28.037,120.892,28.004,120.813,28.013,120.748,28.01,120.763,27.977,120.833,27.938,120.833,27.891,120.685,27.745,120.661,27.688,120.665,27.639,120.588,27.581,120.629,27.482,120.608,27.412,120.54,27.318,120.469,27.256,120.385,27.156,120.279,27.097,120.139,26.886,120.097,26.781,120.087,26.672,120.043,26.634,119.968,26.586,119.882,26.61,119.879,26.683,119.842,26.689,119.821,26.737,119.815,26.798,119.824,26.846,119.789,26.831,119.767,26.775,119.71,26.729,119.652,26.747,119.588,26.785,119.59,26.73,119.624,26.676,119.638,26.621,119.726,26.609,119.785,26.547,119.831,26.45,119.84,26.414,119.876,26.371,119.881,26.334,119.797,26.3,119.693,26.236,119.567,26.127,119.463,26.055,119.37,26.054,119.313,26.063,119.232,26.104,119.139,26.122,119.264,25.975,119.332,25.949,119.418,25.954,119.501,26.009,119.619,26.004,119.648,25.919,119.617,25.823,119.553,25.699,119.539,25.591,119.619,25.437,119.622,25.391,119.593,25.368,119.499,25.409,119.422,25.46,119.344,25.446,119.263,25.468,119.18,25.45,119.146,25.414,119.169,25.356,119.244,25.307,119.286,25.232,119.236,25.206,119.025,25.223,118.978,25.209,118.914,25.127,118.956,25.005,118.909,24.929,118.822,24.911,118.708,24.85,118.637,24.836,118.64,24.809,118.692,24.782,118.719,24.746,118.657,24.621,118.56,24.58,118.412,24.601,118.295,24.573,118.195,24.626,118.087,24.627,118.014,24.56,118.006,24.482,117.935,24.474,117.897,24.48,117.843,24.474,117.848,24.432,117.879,24.396,118.024,24.38,118.051,24.327,118.056,24.246,117.904,24.106,117.839,24.012,117.742,24.015,117.668,23.939,117.628,23.837,117.579,23.857,117.466,23.841,117.433,23.792,117.46,23.771,117.462,23.736,117.417,23.621,117.368,23.589,117.347,23.636,117.331,23.709,117.291,23.714,117.225,23.647,117.148,23.599,117.083,23.579,117.033,23.623,116.911,23.647,116.861,23.453,116.76,23.383,116.712,23.36,116.629,23.354,116.682,23.327,116.699,23.278,116.669,23.228,116.586,23.218,116.538,23.18,116.52,23.007,116.471,22.946,116.346,22.941,116.252,22.981,116.222,22.95,116.206,22.919,116.157,22.887,116.063,22.879,115.852,22.802,115.756,22.824,115.64,22.853,115.561,22.825,115.535,22.765,115.498,22.719,115.383,22.719,115.29,22.776,115.196,22.817,115.092,22.782,115.012,22.709,114.914,22.685,114.896,22.64,114.854,22.617,114.75,22.626,114.711,22.739,114.652,22.755,114.593,22.698,114.572,22.654,114.544,22.621,114.554,22.529,114.496,22.527,114.42,22.583,114.341,22.593,114.266,22.541,114.228,22.554,114.188,22.565,114.123,22.565,114.098,22.551,114.05,22.543,114.018,22.514,114.015,22.512,113.931,22.531,113.828,22.607,113.754,22.734,113.661,22.802,113.62,22.861,113.603,22.969,113.586,23.02,113.592,23.077,113.621,23.127,113.52,23.102,113.445,23.055,113.46,22.996,113.442,22.941,113.331,22.912,113.338,22.889,113.345,22.865,113.432,22.789,113.45,22.726,113.485,22.692,113.553,22.594,113.551,22.404,113.589,22.35,113.576,22.297,113.549,22.225,113.547,22.224,113.527,22.246,113.494,22.242,113.481,22.217,113.479,22.196,113.473,22.194,113.416,22.178,113.367,22.165,113.328,22.145,113.266,22.089,113.149,22.075,113.089,22.208,113.008,22.119,112.984,21.938,112.954,21.907,112.904,21.881,112.809,21.945,112.725,21.902,112.661,21.859,112.634,21.82,112.586,21.777,112.495,21.818,112.421,21.881,112.439,21.927,112.429,21.958,112.396,21.981,112.36,21.978,112.377,21.917,112.39,21.801,112.356,21.768,112.305,21.742,112.193,21.763,112.117,21.806,112.025,21.843,111.944,21.85,111.926,21.776,111.873,21.717,111.825,21.71,111.776,21.719,111.712,21.655,111.682,21.608,111.603,21.559,111.392,21.535,111.319,21.486,111.221,21.494,111.144,21.482,111.101,21.485,111.061,21.511,111.017,21.512,110.997,21.43,110.878,21.396,110.771,21.387,110.652,21.279,110.567,21.214,110.504,21.207,110.458,21.231,110.435,21.327,110.411,21.338,110.397,21.248,110.375,21.172,110.331,21.131,110.194,21.038,110.154,20.945,110.18,20.859,110.365,20.838,110.388,20.791,110.371,20.752,110.326,20.72,110.313,20.672,110.512,20.518,110.518,20.46,110.487,20.427,110.45,20.355,110.345,20.295,110.123,20.264,109.938,20.295,109.883,20.364,109.886,20.413,109.932,20.399,109.984,20.403,109.968,20.448,109.946,20.474,109.861,20.514,109.792,20.622,109.805,20.711,109.767,20.781,109.726,20.839,109.685,20.874,109.663,20.917,109.704,21.053,109.681,21.132,109.76,21.228,109.78,21.337,109.921,21.376,109.931,21.481,109.83,21.484,109.759,21.56,109.743,21.528,109.687,21.525,109.594,21.672,109.566,21.691,109.521,21.693,109.544,21.538,109.436,21.479,109.347,21.454,109.22,21.443,109.149,21.426,109.082,21.44,109.098,21.487,109.133,21.544,109.102,21.59,109.031,21.627,108.922,21.624,108.846,21.634,108.772,21.63,108.744,21.651,108.675,21.725,108.616,21.77,108.589,21.816,108.616,21.869,108.594,21.901,108.48,21.905,108.481,21.829,108.493,21.739,108.526,21.671,108.502,21.633,108.444,21.607,108.383,21.679,108.355,21.697,108.325,21.694,108.302,21.622,108.246,21.558,108.146,21.565,108.067,21.526,107.973,21.508]],[[110.889,19.992,110.938,19.948,110.971,19.883,110.998,19.765,111.014,19.655,110.913,19.586,110.822,19.558,110.641,19.291,110.603,19.207,110.572,19.172,110.563,19.135,110.566,19.099,110.519,18.97,110.478,18.813,110.451,18.748,110.4,18.698,110.334,18.673,110.291,18.67,110.252,18.656,110.156,18.57,110.049,18.505,110.066,18.476,110.067,18.448,110.02,18.416,109.968,18.422,109.816,18.397,109.76,18.348,109.703,18.259,109.681,18.247,109.59,18.226,109.519,18.218,109.4,18.281,109.341,18.3,109.183,18.325,109.03,18.368,108.922,18.416,108.702,18.535,108.676,18.75,108.638,18.866,108.636,18.908,108.65,19.265,108.666,19.304,108.694,19.338,108.791,19.418,108.903,19.481,109.063,19.614,109.179,19.674,109.277,19.761,109.22,19.757,109.177,19.768,109.219,19.843,109.263,19.883,109.315,19.904,109.418,19.889,109.514,19.904,109.584,19.97,109.651,19.984,109.906,19.963,110.083,19.993,110.172,20.054,110.213,20.056,110.344,20.039,110.392,19.976,110.388,20.018,110.394,20.059,110.418,20.055,110.588,19.976,110.589,20.072,110.598,20.098,110.652,20.138,110.679,20.137,110.745,20.059,110.809,20.014,110.889,19.992]]]},{"n":"Macau","c":"MO","r":4,"ml":4,"lx":113.56,"ly":22.13,"b":[113.479,22.196,113.548,22.246],"p":[[[113.479,22.196,113.481,22.217,113.494,22.242,113.527,22.246,113.548,22.223,113.546,22.221,113.499,22.202,113.484,22.198,113.479,22.196]]]},{"n":"Hong Kong","c":"HK","r":4,"ml":4,"lx":114.1,"ly":22.45,"b":[113.839,22.195,114.335,22.565],"p":[[[114.015,22.512,114.018,22.514,114.05,22.543,114.098,22.551,114.123,22.565,114.188,22.565,114.228,22.554,114.266,22.541,114.27,22.537,114.291,22.499,114.285,22.458,114.325,22.437,114.335,22.396,114.291,22.374,114.288,22.325,114.268,22.296,114.139,22.348,114.033,22.376,113.937,22.365,113.903,22.396,113.896,22.428,114.007,22.484,114.015,22.512]],[[114.232,22.211,114.207,22.195,114.139,22.268,114.134,22.292,114.187,22.297,114.247,22.264,114.244,22.234,114.232,22.211]],[[113.998,22.21,113.877,22.21,113.852,22.22,113.839,22.242,113.882,22.28,114.044,22.333,114.003,22.278,113.998,22.21]]]},{"n":"Chile","c":"CL","r":2,"ml":1.7,"lx":-72.32,"ly":-38.15,"b":[-109.434,-55.892,-66.436,-17.506],"p":[[[-109.28,-27.14,-109.434,-27.171,-109.429,-27.116,-109.39,-27.068,-109.276,-27.096,-109.223,-27.101,-109.28,-27.14]],[[-78.804,-33.646,-78.983,-33.668,-78.989,-33.662,-78.979,-33.644,-78.938,-33.614,-78.888,-33.576,-78.877,-33.575,-78.859,-33.578,-78.838,-33.585,-78.785,-33.61,-78.769,-33.627,-78.775,-33.642,-78.804,-33.646]],[[-70.418,-18.346,-70.377,-18.334,-70.282,-18.325,-70.184,-18.325,-70.059,-18.283,-69.926,-18.206,-69.84,-18.093,-69.803,-17.99,-69.802,-17.9,-69.842,-17.785,-69.852,-17.704,-69.806,-17.665,-69.685,-17.65,-69.586,-17.573,-69.511,-17.506,-69.495,-17.62,-69.358,-17.772,-69.313,-17.943,-69.282,-17.965,-69.094,-18.05,-69.09,-18.071,-69.118,-18.103,-69.145,-18.144,-69.126,-18.202,-69.092,-18.282,-69.081,-18.357,-69.06,-18.433,-69.039,-18.55,-69.027,-18.656,-68.979,-18.813,-68.969,-18.91,-68.968,-18.968,-68.931,-19.025,-68.858,-19.093,-68.759,-19.162,-68.681,-19.242,-68.621,-19.297,-68.548,-19.341,-68.492,-19.382,-68.47,-19.41,-68.463,-19.433,-68.487,-19.454,-68.575,-19.56,-68.698,-19.721,-68.696,-19.741,-68.578,-19.857,-68.559,-19.902,-68.561,-19.967,-68.6,-20.045,-68.727,-20.07,-68.756,-20.091,-68.759,-20.116,-68.73,-20.148,-68.735,-20.225,-68.689,-20.31,-68.712,-20.339,-68.759,-20.378,-68.761,-20.416,-68.745,-20.459,-68.696,-20.493,-68.5,-20.612,-68.484,-20.628,-68.487,-20.641,-68.563,-20.72,-68.571,-20.769,-68.569,-20.85,-68.558,-20.902,-68.534,-20.924,-68.435,-20.948,-68.314,-21.13,-68.197,-21.3,-68.199,-21.447,-68.186,-21.619,-68.112,-21.753,-68.102,-21.861,-68.077,-21.983,-67.988,-22.057,-67.954,-22.204,-67.945,-22.282,-67.95,-22.334,-67.882,-22.493,-67.874,-22.631,-67.89,-22.729,-67.889,-22.784,-67.879,-22.823,-67.821,-22.858,-67.794,-22.879,-67.707,-22.889,-67.58,-22.892,-67.362,-22.855,-67.195,-22.822,-67.009,-23.001,-67.09,-23.245,-67.219,-23.634,-67.319,-23.935,-67.336,-23.975,-67.356,-24.034,-67.572,-24.119,-67.886,-24.243,-68.047,-24.308,-68.25,-24.392,-68.3,-24.46,-68.358,-24.497,-68.423,-24.545,-68.447,-24.597,-68.507,-24.63,-68.562,-24.747,-68.562,-24.838,-68.527,-24.899,-68.466,-24.925,-68.447,-24.999,-68.428,-25.051,-68.384,-25.092,-68.395,-25.125,-68.431,-25.149,-68.496,-25.163,-68.541,-25.237,-68.592,-25.42,-68.6,-25.486,-68.542,-25.652,-68.511,-25.741,-68.427,-26.065,-68.415,-26.154,-68.53,-26.277,-68.576,-26.352,-68.592,-26.418,-68.592,-26.47,-68.581,-26.518,-68.485,-26.67,-68.373,-26.806,-68.319,-26.878,-68.319,-26.973,-68.346,-27.028,-68.405,-27.048,-68.537,-27.085,-68.592,-27.14,-68.652,-27.148,-68.71,-27.104,-68.77,-27.115,-68.846,-27.154,-68.875,-27.247,-68.942,-27.405,-68.999,-27.449,-69.042,-27.57,-69.119,-27.744,-69.155,-27.848,-69.174,-27.925,-69.251,-27.974,-69.341,-28.071,-69.41,-28.165,-69.437,-28.193,-69.489,-28.201,-69.527,-28.286,-69.657,-28.414,-69.688,-28.562,-69.735,-28.641,-69.743,-28.784,-69.815,-29.046,-69.828,-29.103,-69.9,-29.149,-69.996,-29.25,-70.027,-29.324,-69.983,-29.545,-69.928,-29.769,-69.924,-29.874,-69.945,-30.016,-69.96,-30.078,-69.924,-30.104,-69.863,-30.12,-69.844,-30.175,-69.888,-30.213,-69.907,-30.282,-69.956,-30.358,-70.102,-30.388,-70.153,-30.361,-70.17,-30.386,-70.161,-30.44,-70.194,-30.505,-70.269,-30.677,-70.319,-30.834,-70.348,-30.902,-70.336,-30.96,-70.312,-30.993,-70.309,-31.023,-70.351,-31.06,-70.388,-31.121,-70.429,-31.129,-70.473,-31.113,-70.52,-31.148,-70.529,-31.223,-70.555,-31.317,-70.566,-31.428,-70.585,-31.569,-70.526,-31.666,-70.45,-31.842,-70.394,-31.884,-70.331,-31.881,-70.282,-31.917,-70.254,-31.958,-70.291,-32.031,-70.356,-32.042,-70.364,-32.083,-70.345,-32.176,-70.32,-32.267,-70.258,-32.31,-70.23,-32.431,-70.17,-32.472,-70.177,-32.626,-70.116,-32.807,-70.052,-32.86,-70.022,-32.885,-70.042,-32.964,-70.093,-33.027,-70.104,-33.128,-70.085,-33.202,-70.02,-33.271,-69.969,-33.279,-69.896,-33.251,-69.82,-33.284,-69.809,-33.344,-69.798,-33.399,-69.839,-33.47,-69.883,-33.601,-69.894,-33.731,-69.881,-33.93,-69.862,-34.084,-69.857,-34.18,-69.852,-34.224,-69.88,-34.254,-69.946,-34.27,-70.003,-34.276,-70.052,-34.301,-70.063,-34.35,-70.101,-34.432,-70.141,-34.493,-70.211,-34.581,-70.255,-34.673,-70.29,-34.733,-70.287,-34.775,-70.312,-34.855,-70.338,-34.922,-70.393,-35.147,-70.467,-35.194,-70.525,-35.217,-70.555,-35.247,-70.532,-35.308,-70.47,-35.326,-70.449,-35.375,-70.457,-35.452,-70.416,-35.523,-70.42,-35.609,-70.38,-35.772,-70.416,-35.879,-70.404,-35.971,-70.405,-36.062,-70.457,-36.133,-70.563,-36.146,-70.622,-36.212,-70.722,-36.283,-70.733,-36.341,-70.749,-36.393,-70.79,-36.412,-70.853,-36.412,-70.905,-36.42,-70.978,-36.487,-71.056,-36.524,-71.073,-36.578,-71.066,-36.644,-71.107,-36.685,-71.159,-36.762,-71.192,-36.844,-71.159,-36.92,-71.124,-37.057,-71.118,-37.114,-71.163,-37.227,-71.2,-37.3,-71.165,-37.393,-71.135,-37.445,-71.163,-37.559,-71.187,-37.631,-71.168,-37.762,-71.096,-37.91,-71.028,-38.041,-71.018,-38.194,-71,-38.315,-70.968,-38.446,-70.9,-38.498,-70.848,-38.542,-70.859,-38.604,-70.897,-38.681,-70.952,-38.738,-71.087,-38.758,-71.197,-38.809,-71.286,-38.845,-71.353,-38.889,-71.402,-38.935,-71.426,-38.986,-71.409,-39.206,-71.42,-39.287,-71.465,-39.402,-71.508,-39.495,-71.526,-39.523,-71.531,-39.564,-71.539,-39.602,-71.587,-39.611,-71.654,-39.594,-71.693,-39.605,-71.72,-39.635,-71.697,-39.707,-71.672,-39.833,-71.638,-39.887,-71.647,-39.929,-71.66,-40.021,-71.704,-40.095,-71.764,-40.095,-71.802,-40.125,-71.818,-40.177,-71.801,-40.244,-71.723,-40.3,-71.695,-40.335,-71.709,-40.382,-71.769,-40.401,-71.805,-40.439,-71.839,-40.524,-71.884,-40.621,-71.932,-40.692,-71.941,-40.789,-71.873,-40.893,-71.881,-40.995,-71.886,-41.292,-71.892,-41.393,-71.871,-41.561,-71.898,-41.607,-71.911,-41.65,-71.844,-41.772,-71.77,-41.969,-71.75,-42.047,-71.761,-42.101,-71.861,-42.148,-71.944,-42.167,-71.993,-42.134,-72.026,-42.148,-72.064,-42.205,-72.108,-42.252,-72.125,-42.298,-72.078,-42.358,-72.053,-42.473,-72.105,-42.522,-72.144,-42.577,-72.13,-42.648,-72.114,-42.777,-72.146,-42.99,-72.102,-43.066,-72.055,-43.102,-71.899,-43.145,-71.781,-43.167,-71.751,-43.237,-71.764,-43.295,-71.82,-43.323,-71.905,-43.348,-71.905,-43.44,-71.832,-43.527,-71.751,-43.59,-71.733,-43.647,-71.737,-43.705,-71.795,-43.753,-71.716,-43.858,-71.68,-43.93,-71.716,-43.984,-71.767,-44.067,-71.812,-44.106,-71.812,-44.151,-71.831,-44.241,-71.835,-44.33,-71.82,-44.383,-71.326,-44.425,-71.213,-44.441,-71.151,-44.494,-71.16,-44.56,-71.221,-44.631,-71.261,-44.763,-71.358,-44.785,-71.455,-44.75,-71.56,-44.762,-71.652,-44.77,-71.783,-44.774,-71.957,-44.792,-72.064,-44.772,-72.073,-44.82,-72.042,-44.904,-71.812,-44.931,-71.596,-44.979,-71.531,-45.068,-71.443,-45.168,-71.354,-45.23,-71.349,-45.332,-71.49,-45.438,-71.508,-45.513,-71.693,-45.535,-71.746,-45.579,-71.773,-45.724,-71.751,-45.839,-71.68,-45.879,-71.632,-45.954,-71.684,-46.042,-71.809,-46.103,-71.876,-46.161,-71.834,-46.207,-71.778,-46.28,-71.762,-46.32,-71.731,-46.428,-71.695,-46.578,-71.7,-46.651,-71.733,-46.706,-71.856,-46.792,-71.94,-46.831,-71.957,-46.937,-71.963,-47.016,-71.954,-47.087,-71.901,-47.144,-71.905,-47.202,-71.979,-47.214,-72.042,-47.241,-72.103,-47.343,-72.283,-47.446,-72.346,-47.493,-72.342,-47.572,-72.413,-47.686,-72.472,-47.784,-72.518,-47.876,-72.509,-47.973,-72.408,-48.016,-72.328,-48.11,-72.293,-48.229,-72.355,-48.366,-72.498,-48.417,-72.583,-48.475,-72.608,-48.519,-72.586,-48.662,-72.592,-48.73,-72.614,-48.793,-72.651,-48.842,-72.728,-48.896,-72.865,-48.944,-72.982,-48.977,-73.034,-49.014,-73.095,-49.097,-73.149,-49.188,-73.135,-49.301,-73.462,-49.314,-73.484,-49.398,-73.554,-49.464,-73.576,-49.583,-73.505,-49.698,-73.47,-49.795,-73.529,-49.911,-73.508,-50.03,-73.501,-50.125,-73.387,-50.231,-73.312,-50.362,-73.274,-50.473,-73.252,-50.558,-73.222,-50.611,-73.175,-50.67,-73.153,-50.738,-73.082,-50.76,-72.956,-50.696,-72.866,-50.653,-72.804,-50.638,-72.62,-50.648,-72.51,-50.608,-72.46,-50.612,-72.393,-50.634,-72.34,-50.682,-72.301,-50.79,-72.276,-50.91,-72.307,-51.033,-72.359,-51.06,-72.377,-51.095,-72.359,-51.17,-72.302,-51.223,-72.303,-51.299,-72.366,-51.47,-72.408,-51.541,-72.335,-51.62,-72.269,-51.691,-72.137,-51.744,-72.028,-51.819,-71.953,-51.88,-71.971,-51.964,-71.919,-51.99,-71.717,-51.991,-71.415,-51.994,-70.943,-51.998,-70.483,-52.002,-69.96,-52.008,-69.713,-52.075,-69.488,-52.136,-69.206,-52.136,-68.925,-52.208,-68.715,-52.255,-68.59,-52.273,-68.461,-52.29,-68.443,-52.357,-69.007,-52.263,-69.134,-52.211,-69.241,-52.205,-69.447,-52.269,-69.561,-52.422,-69.62,-52.465,-69.763,-52.506,-69.907,-52.514,-70.391,-52.661,-70.563,-52.673,-70.68,-52.712,-70.795,-52.769,-70.839,-52.89,-70.821,-52.963,-70.952,-53.227,-70.984,-53.374,-70.985,-53.448,-70.948,-53.57,-70.996,-53.779,-71.083,-53.825,-71.298,-53.883,-71.444,-53.841,-71.694,-53.803,-71.872,-53.723,-72.101,-53.666,-72.174,-53.632,-72.377,-53.471,-72.398,-53.418,-72.413,-53.35,-72.306,-53.254,-72.249,-53.247,-72.081,-53.25,-71.942,-53.234,-71.853,-53.286,-71.828,-53.398,-71.867,-53.458,-71.903,-53.496,-71.892,-53.524,-71.791,-53.485,-71.741,-53.233,-71.4,-53.107,-71.289,-53.034,-71.18,-52.921,-71.163,-52.888,-71.155,-52.846,-71.227,-52.811,-71.388,-52.764,-71.898,-53.002,-72.129,-53.064,-72.278,-53.132,-72.458,-53.254,-72.493,-53.291,-72.531,-53.372,-72.549,-53.461,-72.727,-53.42,-72.998,-53.291,-73.053,-53.243,-72.998,-53.177,-72.916,-53.122,-72.91,-52.937,-72.889,-52.872,-72.832,-52.82,-72.728,-52.762,-72.676,-52.749,-72.632,-52.774,-72.627,-52.818,-72.453,-52.814,-72.118,-52.65,-71.979,-52.646,-71.797,-52.683,-71.591,-52.661,-71.554,-52.644,-71.511,-52.605,-71.665,-52.56,-71.812,-52.537,-72.226,-52.521,-72.315,-52.539,-72.438,-52.626,-72.478,-52.604,-72.504,-52.56,-72.645,-52.529,-72.712,-52.536,-72.777,-52.577,-72.766,-52.643,-72.802,-52.712,-72.932,-52.782,-73.02,-52.892,-73.016,-52.977,-73.023,-53.022,-73.055,-53.046,-73.122,-53.074,-73.338,-53.055,-73.46,-52.965,-73.508,-52.904,-73.645,-52.837,-73.346,-52.754,-73.241,-52.707,-73.145,-52.602,-73.073,-52.535,-73.124,-52.488,-73.184,-52.488,-73.178,-52.563,-73.244,-52.624,-73.382,-52.595,-73.586,-52.686,-73.711,-52.662,-73.915,-52.688,-74.014,-52.639,-74.036,-52.577,-74,-52.513,-74.037,-52.403,-74.094,-52.376,-74.151,-52.383,-74.177,-52.317,-74.238,-52.202,-74.266,-52.171,-74.296,-52.118,-74.265,-52.105,-74.195,-52.12,-74.134,-52.155,-74.04,-52.159,-73.834,-52.234,-73.749,-52.216,-73.703,-52.199,-73.685,-52.137,-73.684,-52.078,-73.649,-52.078,-73.532,-52.153,-73.458,-52.146,-73.327,-52.166,-73.26,-52.158,-73.137,-52.13,-72.944,-52.047,-72.843,-51.961,-72.795,-51.95,-72.735,-51.961,-72.695,-51.985,-72.695,-52.045,-72.65,-52.1,-72.588,-52.145,-72.571,-52.2,-72.583,-52.254,-72.694,-52.33,-72.714,-52.357,-72.677,-52.385,-72.631,-52.372,-72.569,-52.334,-72.533,-52.282,-72.523,-52.255,-72.519,-52.217,-72.524,-52.17,-72.614,-52.037,-72.625,-52.007,-72.625,-51.946,-72.523,-51.891,-72.494,-51.848,-72.49,-51.764,-72.543,-51.706,-72.761,-51.573,-73.127,-51.44,-73.169,-51.454,-73.197,-51.478,-73.163,-51.496,-73.115,-51.504,-72.789,-51.614,-72.705,-51.678,-72.649,-51.695,-72.583,-51.737,-72.6,-51.799,-72.928,-51.86,-73.189,-51.991,-73.383,-52.07,-73.518,-52.041,-73.582,-51.96,-73.65,-51.856,-73.753,-51.796,-73.811,-51.801,-73.858,-51.79,-73.894,-51.758,-73.973,-51.784,-74.146,-51.712,-74.197,-51.681,-74.07,-51.579,-73.93,-51.618,-73.896,-51.331,-73.94,-51.266,-74.121,-51.195,-74.21,-51.205,-74.332,-51.195,-74.414,-51.162,-74.508,-51.15,-74.587,-51.131,-74.69,-51.087,-74.815,-51.063,-74.983,-50.881,-75.055,-50.786,-75.095,-50.681,-74.837,-50.679,-74.686,-50.662,-74.649,-50.618,-74.702,-50.535,-74.776,-50.47,-74.722,-50.408,-74.644,-50.361,-74.564,-50.382,-74.366,-50.488,-74.331,-50.56,-74.19,-50.778,-74.156,-50.797,-74.139,-50.818,-73.847,-50.94,-73.807,-50.938,-73.825,-50.836,-73.741,-50.697,-73.659,-50.651,-73.618,-50.651,-73.614,-50.628,-73.693,-50.57,-73.654,-50.493,-73.68,-50.49,-73.75,-50.54,-73.892,-50.783,-73.978,-50.827,-74.097,-50.717,-74.164,-50.638,-74.197,-50.61,-74.186,-50.485,-73.95,-50.511,-74.031,-50.47,-74.306,-50.398,-74.374,-50.363,-74.425,-50.35,-74.516,-50.266,-74.63,-50.194,-74.434,-50.065,-74.334,-49.975,-74.019,-50.023,-73.959,-49.995,-74.011,-49.929,-74.073,-49.949,-74.171,-49.907,-74.324,-49.783,-74.319,-49.72,-74.291,-49.604,-74.23,-49.579,-74.102,-49.555,-73.956,-49.593,-73.892,-49.624,-73.836,-49.609,-73.892,-49.523,-73.988,-49.491,-74.094,-49.43,-74.083,-49.362,-74.049,-49.306,-74.023,-49.244,-74.006,-49.158,-74.015,-49.091,-73.985,-49.06,-73.938,-49.046,-73.935,-49.021,-74.028,-49.026,-74.061,-49.111,-74.074,-49.188,-74.14,-49.25,-74.168,-49.321,-74.185,-49.404,-74.221,-49.501,-74.302,-49.464,-74.349,-49.426,-74.367,-49.4,-74.358,-49.351,-74.38,-49.048,-74.382,-48.794,-74.341,-48.596,-74.228,-48.517,-74.176,-48.494,-74.129,-48.504,-74.057,-48.504,-74.009,-48.475,-74.172,-48.427,-74.27,-48.455,-74.343,-48.493,-74.474,-48.464,-74.499,-48.362,-74.577,-48.274,-74.591,-48.162,-74.585,-47.999,-74.4,-48.013,-74.25,-48.045,-73.854,-48.042,-73.528,-48.198,-73.384,-48.177,-73.391,-48.146,-73.501,-48.107,-73.57,-48.019,-73.61,-47.994,-73.629,-47.942,-73.635,-47.88,-73.716,-47.655,-73.748,-47.661,-73.779,-47.738,-73.847,-47.867,-73.941,-47.929,-74.085,-47.955,-74.227,-47.969,-74.351,-47.944,-74.379,-47.891,-74.376,-47.83,-74.43,-47.8,-74.569,-47.773,-74.609,-47.758,-74.655,-47.702,-74.588,-47.618,-74.534,-47.568,-74.467,-47.578,-74.404,-47.6,-74.323,-47.667,-74.243,-47.679,-74.152,-47.627,-74.134,-47.591,-74.191,-47.568,-74.243,-47.56,-74.324,-47.531,-74.483,-47.43,-74.403,-47.328,-74.216,-47.21,-74.158,-47.183,-74.208,-47.083,-74.152,-46.974,-74.209,-46.885,-74.314,-46.788,-74.454,-46.767,-74.484,-46.795,-74.489,-46.835,-74.467,-46.864,-74.48,-46.886,-74.512,-46.885,-74.691,-46.864,-74.811,-46.8,-75.006,-46.741,-75.031,-46.695,-75.053,-46.628,-74.984,-46.512,-75.019,-46.511,-75.146,-46.6,-75.337,-46.647,-75.478,-46.662,-75.54,-46.699,-75.565,-46.729,-75.528,-46.746,-75.446,-46.751,-75.386,-46.863,-75.401,-46.906,-75.43,-46.935,-75.497,-46.94,-75.635,-46.863,-75.708,-46.775,-75.706,-46.705,-75.657,-46.61,-75.437,-46.483,-75.376,-46.429,-75.247,-46.369,-75.075,-46.235,-74.924,-46.16,-74.998,-46.098,-75.075,-46.004,-75.067,-45.875,-74.763,-45.824,-74.631,-45.845,-74.463,-45.841,-74.369,-45.81,-74.301,-45.803,-74.158,-45.767,-74.096,-45.717,-74.082,-45.678,-74.083,-45.645,-74.099,-45.603,-74.123,-45.496,-74.099,-45.46,-74.038,-45.418,-73.957,-45.404,-73.92,-45.408,-73.825,-45.447,-73.844,-45.502,-73.882,-45.569,-73.96,-45.835,-74,-45.895,-74.061,-45.947,-74.02,-46.056,-74.082,-46.132,-74.357,-46.213,-74.393,-46.217,-74.372,-46.246,-74.213,-46.239,-74.09,-46.222,-73.968,-46.154,-73.929,-46.05,-73.879,-45.847,-73.813,-45.818,-73.735,-45.812,-73.695,-45.86,-73.708,-45.967,-73.708,-46.07,-73.811,-46.377,-73.935,-46.501,-73.949,-46.533,-73.944,-46.572,-73.845,-46.566,-73.77,-46.5,-73.716,-46.415,-73.662,-46.297,-73.668,-46.212,-73.652,-46.159,-73.629,-45.987,-73.592,-45.899,-73.594,-45.777,-73.662,-45.731,-73.757,-45.703,-73.78,-45.628,-73.731,-45.48,-73.55,-45.484,-73.379,-45.383,-73.266,-45.346,-73.202,-45.354,-72.978,-45.451,-72.934,-45.452,-72.941,-45.417,-72.976,-45.393,-73.064,-45.36,-73.226,-45.255,-73.445,-45.238,-73.405,-45.102,-73.362,-44.978,-73.256,-44.961,-73.078,-44.92,-72.739,-44.734,-72.68,-44.594,-72.664,-44.436,-72.828,-44.395,-73.001,-44.292,-73.141,-44.237,-73.265,-44.169,-73.241,-44.066,-73.224,-43.898,-73.069,-43.862,-72.997,-43.632,-73.101,-43.455,-73.076,-43.324,-72.94,-43.211,-72.915,-43.134,-72.878,-43.048,-72.758,-43.039,-72.755,-42.993,-72.766,-42.908,-72.845,-42.808,-72.848,-42.669,-72.774,-42.505,-72.655,-42.517,-72.632,-42.51,-72.716,-42.41,-72.785,-42.301,-72.773,-42.258,-72.707,-42.221,-72.631,-42.2,-72.548,-42.256,-72.43,-42.434,-72.412,-42.388,-72.46,-42.207,-72.499,-41.981,-72.624,-42.011,-72.738,-41.995,-72.781,-41.96,-72.824,-41.909,-72.784,-41.847,-72.744,-41.801,-72.66,-41.742,-72.486,-41.722,-72.36,-41.649,-72.318,-41.499,-72.359,-41.514,-72.428,-41.646,-72.542,-41.691,-72.601,-41.684,-72.67,-41.659,-72.805,-41.544,-72.88,-41.518,-72.953,-41.515,-73.015,-41.544,-73.174,-41.747,-73.242,-41.781,-73.521,-41.797,-73.624,-41.774,-73.735,-41.742,-73.722,-41.692,-73.688,-41.639,-73.625,-41.612,-73.624,-41.581,-73.711,-41.574,-73.811,-41.517,-73.855,-41.446,-73.876,-41.319,-73.966,-41.118,-73.984,-40.974,-73.92,-40.872,-73.784,-40.468,-73.742,-40.263,-73.669,-40.082,-73.671,-39.963,-73.482,-39.854,-73.41,-39.789,-73.25,-39.422,-73.226,-39.224,-73.481,-38.624,-73.52,-38.509,-73.533,-38.367,-73.472,-38.13,-73.465,-38.04,-73.517,-37.911,-73.662,-37.699,-73.665,-37.59,-73.603,-37.479,-73.662,-37.341,-73.634,-37.255,-73.602,-37.188,-73.375,-37.224,-73.271,-37.207,-73.216,-37.167,-73.173,-37.054,-73.151,-36.876,-73.138,-36.8,-73.118,-36.688,-73.007,-36.643,-72.968,-36.538,-72.875,-36.39,-72.778,-35.979,-72.683,-35.877,-72.587,-35.76,-72.624,-35.586,-72.562,-35.505,-72.505,-35.447,-72.455,-35.341,-72.387,-35.24,-72.224,-35.096,-72.182,-34.92,-72.056,-34.616,-72.031,-34.421,-71.992,-34.288,-72.003,-34.165,-71.927,-34.016,-71.854,-33.89,-71.831,-33.82,-71.664,-33.653,-71.636,-33.519,-71.696,-33.429,-71.697,-33.289,-71.743,-33.095,-71.636,-33.023,-71.592,-32.97,-71.452,-32.66,-71.461,-32.538,-71.421,-32.387,-71.513,-32.208,-71.526,-31.806,-71.577,-31.496,-71.662,-31.17,-71.654,-30.987,-71.706,-30.759,-71.709,-30.628,-71.669,-30.33,-71.4,-30.143,-71.348,-29.933,-71.316,-29.65,-71.327,-29.443,-71.353,-29.35,-71.486,-29.198,-71.519,-28.926,-71.494,-28.855,-71.384,-28.779,-71.307,-28.672,-71.267,-28.508,-71.186,-28.378,-71.154,-28.064,-71.087,-27.814,-71.053,-27.727,-70.946,-27.618,-70.926,-27.589,-70.909,-27.505,-70.914,-27.308,-70.898,-27.187,-70.813,-26.951,-70.803,-26.841,-70.708,-26.597,-70.687,-26.422,-70.647,-26.329,-70.662,-26.225,-70.635,-25.993,-70.7,-25.861,-70.714,-25.784,-70.633,-25.546,-70.578,-25.487,-70.49,-25.376,-70.452,-25.252,-70.445,-25.173,-70.559,-24.779,-70.574,-24.644,-70.546,-24.332,-70.507,-24.13,-70.52,-23.969,-70.507,-23.886,-70.488,-23.782,-70.41,-23.656,-70.392,-23.566,-70.42,-23.529,-70.512,-23.483,-70.588,-23.368,-70.593,-23.255,-70.569,-23.173,-70.563,-23.057,-70.45,-23.034,-70.389,-22.97,-70.332,-22.849,-70.26,-22.556,-70.229,-22.193,-70.185,-21.975,-70.155,-21.867,-70.13,-21.641,-70.088,-21.493,-70.08,-21.357,-70.088,-21.253,-70.197,-20.725,-70.194,-20.531,-70.147,-20.23,-70.148,-19.805,-70.157,-19.706,-70.198,-19.613,-70.21,-19.487,-70.276,-19.268,-70.335,-18.828,-70.336,-18.595,-70.362,-18.398,-70.418,-18.346]],[[-68.63,-52.653,-68.632,-52.95,-68.633,-53.242,-68.635,-53.515,-68.637,-53.789,-68.638,-54.053,-68.64,-54.324,-68.648,-54.628,-68.653,-54.854,-68.804,-54.854,-68.844,-54.877,-69.082,-54.91,-69.486,-54.859,-69.588,-54.813,-69.723,-54.712,-69.772,-54.739,-69.899,-54.782,-70.031,-54.816,-70.138,-54.819,-70.238,-54.778,-70.259,-54.756,-70.282,-54.752,-70.497,-54.81,-70.735,-54.751,-70.925,-54.714,-71.229,-54.694,-71.441,-54.62,-71.832,-54.626,-71.902,-54.602,-71.928,-54.529,-71.907,-54.495,-71.823,-54.474,-71.8,-54.434,-71.716,-54.444,-71.606,-54.497,-71.573,-54.495,-71.5,-54.445,-71.393,-54.4,-71.355,-54.395,-71.159,-54.451,-71.08,-54.444,-70.966,-54.42,-70.946,-54.398,-70.928,-54.36,-70.898,-54.338,-70.797,-54.327,-70.699,-54.349,-70.688,-54.415,-70.701,-54.485,-70.573,-54.504,-70.418,-54.502,-70.311,-54.529,-70.298,-54.486,-70.468,-54.373,-70.54,-54.303,-70.636,-54.262,-70.76,-54.241,-70.863,-54.11,-70.857,-53.996,-70.868,-53.884,-70.644,-53.823,-70.696,-53.727,-70.619,-53.655,-70.531,-53.627,-70.443,-53.893,-70.38,-53.987,-70.461,-54.006,-70.63,-54.006,-70.535,-54.136,-70.38,-54.181,-70.246,-54.277,-70.243,-54.348,-70.169,-54.379,-69.99,-54.381,-69.867,-54.367,-69.809,-54.321,-69.742,-54.306,-69.622,-54.364,-69.419,-54.407,-69.365,-54.438,-69.325,-54.488,-69.322,-54.543,-69.312,-54.571,-69.253,-54.557,-69.169,-54.483,-69.128,-54.458,-69.077,-54.445,-69.045,-54.428,-69.044,-54.407,-69.196,-54.354,-69.988,-54.109,-70.086,-54.011,-70.151,-53.888,-70.149,-53.761,-70.091,-53.722,-69.95,-53.672,-69.69,-53.601,-69.39,-53.499,-69.352,-53.48,-69.356,-53.416,-69.394,-53.373,-69.513,-53.342,-69.637,-53.334,-69.756,-53.337,-69.874,-53.35,-70.09,-53.418,-70.213,-53.414,-70.329,-53.378,-70.416,-53.305,-70.46,-53.206,-70.46,-53.143,-70.443,-53.086,-70.391,-53.026,-70.32,-53.001,-70.256,-53.004,-70.196,-52.99,-70.161,-52.97,-70.131,-52.943,-70.14,-52.919,-70.163,-52.899,-70.259,-52.857,-70.297,-52.817,-70.38,-52.752,-70.335,-52.734,-70.19,-52.724,-70.088,-52.769,-69.994,-52.821,-69.935,-52.821,-69.883,-52.799,-69.764,-52.731,-69.663,-52.646,-69.572,-52.549,-69.498,-52.491,-69.414,-52.486,-69.167,-52.668,-69.08,-52.674,-68.79,-52.577,-68.758,-52.582,-68.659,-52.632,-68.63,-52.653]],[[-67.08,-55.154,-67.109,-55.192,-67.173,-55.243,-67.257,-55.282,-67.34,-55.293,-67.399,-55.272,-67.429,-55.237,-67.443,-55.201,-67.463,-55.182,-67.495,-55.177,-67.535,-55.179,-67.585,-55.192,-67.691,-55.243,-67.737,-55.256,-67.768,-55.26,-68.07,-55.221,-68.1,-55.207,-68.135,-55.173,-68.174,-55.071,-68.301,-54.981,-68.107,-54.929,-67.874,-54.93,-67.425,-54.969,-67.245,-54.978,-67.107,-55.064,-67.085,-55.115,-67.08,-55.154]],[[-73.773,-43.346,-73.849,-43.367,-73.919,-43.372,-73.99,-43.357,-74.114,-43.358,-74.239,-43.319,-74.355,-43.264,-74.387,-43.232,-74.373,-43.186,-74.289,-43.079,-74.209,-42.879,-74.156,-42.591,-74.199,-42.481,-74.194,-42.436,-74.174,-42.382,-74.164,-42.325,-74.17,-42.269,-74.16,-42.216,-74.072,-42.106,-74.059,-42.056,-74.057,-42.002,-74.019,-41.891,-74.031,-41.854,-74.063,-41.823,-74.037,-41.796,-73.731,-41.877,-73.528,-41.896,-73.517,-41.981,-73.478,-42.047,-73.454,-42.166,-73.423,-42.193,-73.439,-42.278,-73.533,-42.314,-73.525,-42.393,-73.471,-42.466,-73.549,-42.493,-73.634,-42.508,-73.653,-42.529,-73.715,-42.545,-73.789,-42.586,-73.767,-42.622,-73.673,-42.704,-73.568,-42.762,-73.511,-42.847,-73.436,-42.937,-73.473,-42.993,-73.541,-43.074,-73.65,-43.127,-73.75,-43.159,-73.738,-43.291,-73.773,-43.346]],[[-74.476,-49.148,-74.467,-49.295,-74.484,-49.442,-74.522,-49.623,-74.516,-49.66,-74.471,-49.669,-74.459,-49.691,-74.472,-49.786,-74.496,-49.859,-74.543,-49.919,-74.57,-49.991,-74.595,-50.007,-74.703,-50.019,-74.763,-50.011,-74.811,-49.93,-74.825,-49.879,-74.822,-49.814,-74.88,-49.726,-74.882,-49.692,-74.859,-49.634,-74.812,-49.605,-74.805,-49.516,-74.781,-49.489,-74.727,-49.452,-74.719,-49.437,-74.724,-49.424,-74.744,-49.422,-74.96,-49.533,-74.981,-49.564,-74.991,-49.606,-74.994,-49.752,-75.032,-49.836,-75.066,-49.852,-75.167,-49.856,-75.3,-49.847,-75.451,-49.77,-75.55,-49.791,-75.57,-49.697,-75.521,-49.622,-75.337,-49.628,-75.306,-49.494,-75.364,-49.462,-75.429,-49.408,-75.467,-49.359,-75.433,-49.322,-75.327,-49.269,-75.27,-49.263,-75.217,-49.293,-75.086,-49.27,-75.094,-49.185,-75.21,-49.148,-75.184,-49.084,-75.037,-49.022,-74.949,-48.96,-74.945,-48.889,-74.981,-48.819,-74.97,-48.791,-74.896,-48.733,-74.793,-48.705,-74.747,-48.709,-74.652,-48.75,-74.567,-48.755,-74.546,-48.767,-74.531,-48.813,-74.476,-49.148]],[[-75.51,-48.763,-75.623,-48.765,-75.651,-48.586,-75.518,-48.329,-75.509,-48.231,-75.554,-48.157,-75.571,-48.096,-75.561,-48.071,-75.391,-48.02,-75.338,-48.074,-75.275,-48.218,-75.156,-48.425,-75.158,-48.623,-75.225,-48.671,-75.434,-48.721,-75.51,-48.763]],[[-74.386,-52.922,-74.369,-52.931,-74.33,-52.929,-74.275,-52.946,-74.066,-52.965,-73.879,-53.012,-73.782,-53.056,-73.654,-53.07,-73.549,-53.126,-73.505,-53.14,-73.451,-53.144,-73.31,-53.248,-73.302,-53.259,-73.143,-53.341,-73.135,-53.354,-73.226,-53.358,-73.409,-53.321,-73.501,-53.318,-73.567,-53.307,-73.583,-53.3,-73.596,-53.253,-73.617,-53.23,-73.794,-53.121,-73.867,-53.097,-73.994,-53.076,-74.139,-53.091,-74.236,-53.076,-74.27,-53.082,-74.414,-52.995,-74.558,-52.922,-74.62,-52.835,-74.712,-52.768,-74.712,-52.749,-74.67,-52.734,-74.572,-52.771,-74.475,-52.836,-74.422,-52.86,-74.386,-52.922]],[[-74.567,-48.592,-74.586,-48.616,-74.71,-48.601,-74.923,-48.626,-75.013,-48.536,-75.052,-48.391,-75.079,-48.362,-75.132,-48.279,-75.158,-48.225,-75.213,-48.142,-75.234,-48.053,-75.247,-48.027,-75.198,-47.975,-74.975,-47.923,-74.896,-47.839,-74.827,-47.85,-74.846,-48.021,-74.805,-48.078,-74.729,-48.126,-74.715,-48.146,-74.702,-48.206,-74.664,-48.299,-74.615,-48.343,-74.602,-48.37,-74.6,-48.393,-74.618,-48.425,-74.567,-48.592]],[[-72.923,-53.482,-72.896,-53.563,-72.882,-53.578,-72.809,-53.565,-72.685,-53.558,-72.482,-53.588,-72.459,-53.599,-72.373,-53.687,-72.307,-53.725,-72.205,-53.807,-72.306,-53.862,-72.366,-53.941,-72.369,-53.981,-72.409,-54.004,-72.471,-54.028,-72.563,-54.074,-72.677,-54.079,-72.789,-54.103,-72.84,-54.125,-72.871,-54.127,-72.907,-54.115,-72.946,-54.092,-72.959,-54.066,-72.882,-54.042,-72.782,-53.955,-72.764,-53.865,-72.872,-53.849,-72.936,-53.861,-72.984,-53.861,-73.039,-53.833,-73.073,-53.875,-73.086,-53.916,-73.071,-53.978,-73.081,-53.998,-73.12,-54.009,-73.211,-53.986,-73.305,-53.944,-73.312,-53.92,-73.293,-53.836,-73.295,-53.792,-73.314,-53.729,-73.325,-53.723,-73.36,-53.724,-73.471,-53.736,-73.582,-53.655,-73.642,-53.57,-73.845,-53.546,-73.687,-53.427,-73.447,-53.41,-73.366,-53.47,-73.099,-53.512,-73.115,-53.448,-73.111,-53.425,-73.074,-53.397,-73.054,-53.394,-73.022,-53.415,-72.971,-53.423,-72.947,-53.442,-72.923,-53.482]],[[-74.823,-51.63,-74.78,-51.825,-74.75,-51.852,-74.647,-51.866,-74.537,-51.965,-74.532,-51.992,-74.666,-52.16,-74.694,-52.279,-74.852,-52.271,-74.918,-52.152,-75.017,-52.038,-75.051,-51.904,-75.105,-51.789,-75.008,-51.724,-74.915,-51.738,-74.91,-51.65,-74.823,-51.63]],[[-69.703,-54.919,-68.901,-55.018,-68.654,-54.958,-68.458,-54.96,-68.4,-55.042,-68.598,-55.128,-68.613,-55.156,-68.586,-55.178,-68.382,-55.192,-68.33,-55.219,-68.283,-55.255,-68.323,-55.308,-68.327,-55.333,-68.305,-55.357,-68.153,-55.437,-68.09,-55.478,-68.058,-55.518,-68.046,-55.587,-68.048,-55.643,-68.083,-55.651,-68.157,-55.634,-68.23,-55.602,-68.293,-55.521,-68.338,-55.505,-68.467,-55.489,-68.594,-55.45,-68.694,-55.452,-68.785,-55.436,-68.867,-55.45,-68.896,-55.424,-68.931,-55.371,-68.932,-55.347,-68.889,-55.263,-68.89,-55.241,-68.913,-55.239,-69.008,-55.256,-69.047,-55.244,-69.151,-55.183,-69.193,-55.172,-69.297,-55.166,-69.356,-55.274,-69.359,-55.301,-69.299,-55.369,-69.181,-55.475,-69.241,-55.477,-69.412,-55.444,-69.456,-55.424,-69.509,-55.371,-69.61,-55.34,-69.646,-55.321,-69.656,-55.298,-69.657,-55.229,-69.68,-55.219,-69.824,-55.237,-69.854,-55.22,-69.866,-55.191,-69.887,-55.174,-69.98,-55.147,-69.988,-55.131,-69.947,-55.111,-69.921,-55.061,-69.884,-54.882,-69.703,-54.919]],[[-71.39,-54.033,-71.169,-54.113,-71.022,-54.112,-71.023,-54.162,-71.005,-54.247,-71.028,-54.281,-71.083,-54.316,-71.118,-54.366,-71.143,-54.374,-71.305,-54.314,-71.473,-54.231,-71.558,-54.246,-71.671,-54.225,-71.761,-54.23,-71.818,-54.276,-71.949,-54.301,-71.972,-54.207,-72.092,-54.119,-72.21,-54.048,-72.146,-53.939,-72.069,-53.921,-71.996,-53.885,-71.705,-53.923,-71.554,-53.956,-71.39,-54.033]],[[-73.735,-44.395,-73.785,-44.437,-73.862,-44.445,-73.983,-44.495,-73.996,-44.538,-74.002,-44.591,-73.919,-44.655,-73.877,-44.729,-73.828,-44.84,-73.792,-44.946,-73.795,-44.979,-73.786,-45.034,-73.727,-45.119,-73.722,-45.158,-73.728,-45.196,-73.752,-45.267,-73.771,-45.277,-73.83,-45.283,-73.834,-45.327,-73.849,-45.341,-74.016,-45.345,-74.099,-45.325,-74.089,-45.196,-74.195,-45.145,-74.268,-45.059,-74.35,-44.911,-74.419,-44.865,-74.499,-44.748,-74.618,-44.648,-74.481,-44.585,-74.502,-44.474,-74.422,-44.435,-74.301,-44.396,-74.212,-44.427,-74.133,-44.416,-74.097,-44.389,-74.108,-44.276,-74.083,-44.186,-73.995,-44.14,-73.9,-44.135,-73.865,-44.185,-73.818,-44.235,-73.703,-44.274,-73.704,-44.325,-73.735,-44.395]],[[-72.986,-44.78,-73.228,-44.86,-73.35,-44.833,-73.397,-44.774,-73.42,-44.725,-73.445,-44.641,-73.404,-44.596,-73.315,-44.531,-73.282,-44.49,-73.266,-44.44,-73.272,-44.394,-73.26,-44.35,-73.208,-44.335,-73.028,-44.384,-72.842,-44.458,-72.776,-44.509,-72.764,-44.549,-72.845,-44.638,-72.897,-44.712,-72.986,-44.78]],[[-75.042,-44.89,-75.067,-44.907,-75.099,-44.902,-75.124,-44.87,-75.142,-44.816,-75.107,-44.795,-75.079,-44.795,-75.048,-44.824,-75.032,-44.871,-75.042,-44.89]],[[-75.302,-50.68,-75.33,-50.772,-75.411,-50.764,-75.439,-50.741,-75.453,-50.683,-75.477,-50.654,-75.443,-50.596,-75.42,-50.53,-75.428,-50.481,-75.304,-50.484,-75.156,-50.497,-75.115,-50.51,-75.16,-50.554,-75.203,-50.581,-75.292,-50.597,-75.302,-50.68]],[[-75.107,-48.837,-75.115,-48.916,-75.263,-49.069,-75.39,-49.159,-75.506,-49.231,-75.58,-49.23,-75.641,-49.195,-75.573,-49.139,-75.488,-49.082,-75.515,-49.01,-75.54,-48.988,-75.576,-48.981,-75.638,-48.943,-75.619,-48.886,-75.583,-48.859,-75.535,-48.838,-75.49,-48.85,-75.297,-48.811,-75.236,-48.779,-75.119,-48.773,-75.107,-48.837]],[[-75.112,-47.838,-75.186,-47.851,-75.194,-47.818,-75.261,-47.764,-75.203,-47.728,-75.09,-47.691,-75.004,-47.695,-74.926,-47.723,-74.916,-47.757,-75.051,-47.8,-75.084,-47.825,-75.112,-47.838]],[[-74.669,-43.608,-74.81,-43.625,-74.843,-43.596,-74.842,-43.57,-74.818,-43.549,-74.745,-43.536,-74.697,-43.553,-74.673,-43.577,-74.665,-43.6,-74.669,-43.608]],[[-73.632,-44.821,-73.665,-44.833,-73.695,-44.831,-73.725,-44.797,-73.735,-44.752,-73.8,-44.684,-73.818,-44.652,-73.817,-44.614,-73.779,-44.559,-73.724,-44.544,-73.686,-44.546,-73.641,-44.611,-73.628,-44.681,-73.617,-44.753,-73.632,-44.821]],[[-74.559,-51.277,-74.561,-51.361,-74.593,-51.387,-74.62,-51.396,-74.691,-51.37,-74.731,-51.367,-74.797,-51.412,-74.853,-51.434,-74.937,-51.428,-75.047,-51.398,-75.146,-51.524,-75.192,-51.567,-75.289,-51.625,-75.3,-51.556,-75.238,-51.454,-75.21,-51.383,-75.154,-51.279,-75.04,-51.318,-74.881,-51.279,-74.737,-51.208,-74.612,-51.207,-74.571,-51.245,-74.559,-51.277]],[[-75.055,-50.296,-75.25,-50.376,-75.308,-50.343,-75.449,-50.343,-75.412,-50.257,-75.398,-50.193,-75.377,-50.168,-75.369,-50.113,-75.327,-50.012,-75.21,-50.045,-75.123,-50.055,-75.004,-50.089,-74.876,-50.11,-74.839,-50.197,-74.963,-50.237,-75.055,-50.296]],[[-73.811,-43.827,-73.79,-43.876,-73.834,-43.883,-73.904,-43.875,-73.938,-43.914,-73.956,-43.922,-74.118,-43.887,-74.143,-43.872,-74.14,-43.821,-73.967,-43.817,-73.857,-43.784,-73.841,-43.789,-73.811,-43.827]],[[-74.142,-51.931,-74.172,-51.942,-74.283,-51.919,-74.339,-51.898,-74.424,-51.845,-74.437,-51.791,-74.475,-51.726,-74.451,-51.725,-74.362,-51.751,-74.326,-51.77,-74.277,-51.812,-74.133,-51.871,-74.115,-51.888,-74.119,-51.911,-74.142,-51.931]],[[-74.313,-45.692,-74.368,-45.736,-74.466,-45.757,-74.562,-45.722,-74.678,-45.739,-74.69,-45.663,-74.646,-45.6,-74.558,-45.526,-74.495,-45.426,-74.502,-45.285,-74.45,-45.253,-74.422,-45.203,-74.311,-45.173,-74.285,-45.277,-74.315,-45.464,-74.24,-45.575,-74.229,-45.611,-74.244,-45.654,-74.313,-45.692]],[[-67.575,-55.89,-67.611,-55.892,-67.7,-55.873,-67.832,-55.865,-67.846,-55.857,-67.85,-55.843,-67.834,-55.828,-67.762,-55.816,-67.545,-55.826,-67.517,-55.833,-67.51,-55.844,-67.545,-55.877,-67.575,-55.89]],[[-66.472,-55.229,-66.552,-55.273,-66.611,-55.27,-66.63,-55.254,-66.637,-55.234,-66.625,-55.213,-66.6,-55.194,-66.542,-55.169,-66.523,-55.166,-66.436,-55.19,-66.472,-55.229]],[[-70.992,-54.868,-70.945,-54.931,-70.928,-54.943,-70.805,-54.968,-70.749,-54.953,-70.615,-54.946,-70.535,-54.921,-70.418,-54.909,-70.283,-55.066,-70.298,-55.114,-70.404,-55.166,-70.476,-55.177,-70.543,-55.161,-70.539,-55.135,-70.551,-55.112,-70.597,-55.082,-70.641,-55.085,-70.711,-55.107,-70.744,-55.104,-70.815,-55.08,-70.94,-55.062,-70.965,-55.04,-70.967,-55.007,-70.991,-54.99,-71.12,-54.938,-71.203,-54.893,-71.274,-54.887,-71.299,-54.892,-71.325,-54.914,-71.389,-54.934,-71.407,-54.931,-71.427,-54.914,-71.437,-54.889,-71.411,-54.839,-71.374,-54.835,-71.197,-54.844,-71.089,-54.867,-70.992,-54.868]],[[-67.289,-55.777,-67.325,-55.785,-67.352,-55.766,-67.393,-55.753,-67.56,-55.725,-67.563,-55.708,-67.546,-55.684,-67.513,-55.662,-67.449,-55.641,-67.397,-55.585,-67.374,-55.589,-67.351,-55.612,-67.31,-55.689,-67.262,-55.744,-67.267,-55.763,-67.289,-55.777]]]},{"n":"Chad","c":"TD","r":3,"ml":3,"lx":18.65,"ly":15.14,"b":[13.448,7.475,23.983,23.445],"p":[[[23.98,19.497,23.981,19.051,23.981,18.605,23.981,18.158,23.982,17.712,23.982,17.266,23.983,16.82,23.983,16.374,23.983,15.928,23.983,15.78,23.971,15.722,23.965,15.713,23.946,15.704,23.708,15.745,23.604,15.746,23.458,15.714,23.243,15.697,23.105,15.703,23.009,15.626,22.934,15.533,22.97,15.311,22.961,15.238,22.932,15.162,22.867,15.097,22.802,15.044,22.763,14.999,22.715,14.898,22.679,14.851,22.682,14.789,22.671,14.722,22.632,14.688,22.532,14.663,22.468,14.633,22.416,14.585,22.382,14.55,22.4,14.504,22.425,14.441,22.439,14.342,22.449,14.284,22.498,14.237,22.528,14.203,22.539,14.162,22.51,14.127,22.388,14.056,22.339,14.029,22.283,13.992,22.262,13.979,22.173,13.911,22.128,13.85,22.106,13.8,22.108,13.73,22.153,13.626,22.202,13.538,22.221,13.472,22.233,13.399,22.228,13.33,22.203,13.269,22.158,13.215,21.99,13.113,21.908,13.001,21.842,12.865,21.825,12.791,21.843,12.741,21.878,12.699,21.928,12.678,22.001,12.672,22.121,12.695,22.233,12.709,22.352,12.66,22.414,12.546,22.39,12.463,22.435,12.312,22.475,12.129,22.472,12.068,22.49,12.045,22.564,12.033,22.581,11.99,22.556,11.67,22.591,11.58,22.641,11.516,22.697,11.483,22.754,11.44,22.783,11.41,22.849,11.403,22.923,11.345,22.943,11.267,22.938,11.192,22.895,11.029,22.86,10.92,22.817,10.927,22.73,10.954,22.624,10.977,22.494,10.996,22.37,10.952,22.236,10.894,22.194,10.851,22.156,10.826,22.097,10.83,22.043,10.823,22.014,10.782,21.965,10.737,21.771,10.643,21.731,10.609,21.707,10.575,21.707,10.538,21.726,10.462,21.726,10.367,21.683,10.29,21.633,10.238,21.576,10.219,21.528,10.208,21.497,10.176,21.396,10.001,21.352,9.969,21.264,9.975,21.009,9.713,20.984,9.636,20.891,9.527,20.773,9.406,20.668,9.347,20.66,9.325,20.631,9.301,20.567,9.275,20.342,9.127,20.073,9.133,19.954,9.075,19.838,9.049,19.668,9.021,19.617,9.024,19.4,9.012,19.146,9.016,19.048,8.995,18.956,8.939,18.888,8.89,18.878,8.873,18.889,8.852,18.886,8.836,19.064,8.715,19.109,8.656,19.064,8.599,19.042,8.59,19.04,8.587,19.011,8.541,18.906,8.405,18.747,8.244,18.666,8.198,18.634,8.168,18.592,8.061,18.564,8.046,18.455,8.032,18.239,8.02,17.94,7.985,17.761,7.974,17.649,7.984,17.493,7.91,17.436,7.891,17.402,7.885,17.247,7.813,17.118,7.702,17.072,7.681,16.89,7.634,16.818,7.557,16.785,7.551,16.668,7.652,16.589,7.743,16.55,7.836,16.545,7.865,16.523,7.86,16.459,7.819,16.404,7.772,16.379,7.684,16.191,7.623,16.031,7.572,15.958,7.508,15.845,7.475,15.701,7.488,15.589,7.515,15.48,7.524,15.532,7.604,15.553,7.665,15.558,7.738,15.55,7.788,15.484,7.813,15.443,7.852,15.349,8.084,15.252,8.322,15.116,8.557,14.968,8.707,14.861,8.799,14.826,8.81,14.771,8.839,14.733,8.866,14.536,9.025,14.332,9.204,14.28,9.285,14.178,9.406,14.064,9.532,14.005,9.589,13.977,9.692,14.056,9.784,14.14,9.902,14.243,9.98,14.377,9.985,14.598,9.953,14.836,9.942,15.072,9.966,15.133,9.983,15.193,9.981,15.32,9.954,15.541,9.96,15.655,10.008,15.532,10.088,15.4,10.217,15.276,10.357,15.201,10.485,15.132,10.648,15.069,10.851,15.03,11.114,15.036,11.263,15.055,11.369,15.122,11.541,15.078,11.643,15.088,11.724,15.081,11.846,15.06,11.907,14.974,12.108,14.957,12.13,14.881,12.269,14.847,12.502,14.761,12.656,14.623,12.73,14.545,12.82,14.516,12.98,14.462,13.022,14.245,13.077,14.064,13.079,13.932,13.258,13.763,13.49,13.606,13.705,13.506,14.134,13.448,14.381,13.514,14.456,13.642,14.631,13.807,14.966,14.178,15.485,14.368,15.75,14.747,16.147,15.212,16.634,15.474,16.908,15.517,17.408,15.562,17.937,15.596,18.337,15.638,18.811,15.673,19.207,15.699,19.495,15.735,19.904,15.766,19.983,15.949,20.303,15.963,20.346,15.929,20.4,15.668,20.672,15.587,20.733,15.54,20.875,15.607,20.954,15.294,21.412,15.216,21.467,15.182,21.523,15.178,21.606,15.172,21.922,15.089,22.418,14.979,22.996,15.347,23.161,15.627,23.286,15.984,23.445,16.315,23.282,16.794,23.045,17.273,22.809,17.752,22.572,18.231,22.336,18.71,22.099,19.189,21.862,19.669,21.626,20.148,21.389,20.627,21.153,21.106,20.916,21.585,20.679,22.064,20.443,22.543,20.206,23.022,19.97,23.501,19.733,23.98,19.497]]]},{"n":"Central African Republic","c":"CF","r":4,"ml":4,"lx":20.91,"ly":6.99,"b":[14.431,2.27,27.403,10.996],"p":[[[24.147,8.666,24.195,8.653,24.221,8.608,24.18,8.461,24.208,8.369,24.291,8.291,24.375,8.258,24.456,8.239,24.737,8.192,24.853,8.138,25.007,7.965,25.2,7.808,25.247,7.725,25.239,7.649,25.181,7.557,25.19,7.519,25.279,7.427,25.381,7.333,25.567,7.229,25.889,7.065,26.037,6.955,26.087,6.872,26.169,6.782,26.285,6.699,26.362,6.635,26.309,6.455,26.325,6.396,26.353,6.345,26.421,6.274,26.447,6.183,26.514,6.069,26.594,6.018,26.726,5.998,26.796,5.946,26.942,5.855,27.083,5.777,27.144,5.723,27.181,5.675,27.213,5.619,27.229,5.563,27.233,5.441,27.257,5.29,27.332,5.186,27.403,5.109,27.115,5.198,27.072,5.2,27.021,5.184,26.87,5.076,26.822,5.062,26.768,5.072,26.633,5.085,26.174,5.171,25.82,5.254,25.714,5.284,25.525,5.312,25.4,5.256,25.283,5.063,25.249,5.025,25.065,4.967,24.978,4.983,24.766,4.93,24.437,5.01,24.32,4.994,24.228,4.954,23.992,4.866,23.848,4.816,23.682,4.771,23.524,4.701,23.417,4.663,23.313,4.664,23.219,4.703,23.116,4.737,22.993,4.744,22.865,4.724,22.756,4.647,22.712,4.592,22.617,4.446,22.506,4.208,22.462,4.16,22.45,4.155,22.422,4.135,21.908,4.254,21.687,4.281,21.538,4.245,21.35,4.311,21.268,4.323,21.23,4.302,21.126,4.332,20.956,4.413,20.793,4.447,20.647,4.436,20.558,4.463,20.487,4.542,20.394,4.686,20.226,4.83,20.002,4.945,19.863,5.031,19.807,5.089,19.686,5.121,19.501,5.127,19.323,5.071,19.069,4.891,18.832,4.523,18.7,4.383,18.594,4.346,18.567,4.258,18.62,4.117,18.634,3.954,18.597,3.679,18.61,3.478,18.554,3.51,18.5,3.604,18.474,3.623,18.318,3.581,18.237,3.543,18.194,3.505,18.161,3.5,18.111,3.551,18.072,3.56,18.011,3.551,17.948,3.552,17.907,3.558,17.88,3.554,17.807,3.584,17.538,3.662,17.492,3.687,17.438,3.685,17.298,3.617,17.225,3.598,17.003,3.557,16.764,3.536,16.673,3.535,16.611,3.505,16.57,3.463,16.543,3.37,16.496,3.209,16.477,3.165,16.48,3.101,16.466,2.993,16.46,2.897,16.469,2.832,16.401,2.701,16.32,2.543,16.252,2.407,16.183,2.27,16.136,2.364,16.107,2.473,16.096,2.599,16.102,2.633,16.083,2.67,16.082,2.678,16.059,2.773,16.082,2.839,16.063,2.909,16.008,2.977,15.958,3.029,15.929,3.076,15.905,3.096,15.849,3.103,15.775,3.127,15.677,3.23,15.581,3.329,15.458,3.457,15.36,3.567,15.24,3.702,15.129,3.827,15.062,3.947,15.035,4.016,15.067,4.023,15.115,4.024,15.136,4.037,15.137,4.069,15.088,4.164,15.064,4.285,15.023,4.359,14.894,4.472,14.77,4.558,14.731,4.602,14.709,4.666,14.662,5.066,14.641,5.179,14.602,5.229,14.574,5.252,14.563,5.28,14.568,5.351,14.584,5.415,14.584,5.44,14.617,5.496,14.617,5.865,14.599,5.884,14.577,5.916,14.542,5.914,14.503,5.917,14.464,5.971,14.431,6.039,14.441,6.087,14.475,6.127,14.512,6.162,14.559,6.191,14.7,6.25,14.739,6.28,14.764,6.316,14.78,6.366,14.862,6.556,14.983,6.745,15.035,6.784,15.086,6.91,15.157,7.064,15.186,7.135,15.207,7.206,15.246,7.264,15.379,7.358,15.48,7.524,15.589,7.515,15.701,7.488,15.845,7.475,15.958,7.508,16.031,7.572,16.191,7.623,16.379,7.684,16.404,7.772,16.459,7.819,16.523,7.86,16.545,7.865,16.55,7.836,16.589,7.743,16.668,7.652,16.785,7.551,16.818,7.557,16.89,7.634,17.072,7.681,17.118,7.702,17.247,7.813,17.402,7.885,17.436,7.891,17.493,7.91,17.649,7.984,17.761,7.974,17.94,7.985,18.239,8.02,18.455,8.032,18.564,8.046,18.592,8.061,18.634,8.168,18.666,8.198,18.747,8.244,18.906,8.405,19.011,8.541,19.04,8.587,19.042,8.59,19.064,8.599,19.109,8.656,19.064,8.715,18.886,8.836,18.889,8.852,18.878,8.873,18.888,8.89,18.956,8.939,19.048,8.995,19.146,9.016,19.4,9.012,19.617,9.024,19.668,9.021,19.838,9.049,19.954,9.075,20.073,9.133,20.342,9.127,20.567,9.275,20.631,9.301,20.66,9.325,20.668,9.347,20.773,9.406,20.891,9.527,20.984,9.636,21.009,9.713,21.264,9.975,21.352,9.969,21.396,10.001,21.497,10.176,21.528,10.208,21.576,10.219,21.633,10.238,21.683,10.29,21.726,10.367,21.726,10.462,21.707,10.538,21.707,10.575,21.731,10.609,21.771,10.643,21.965,10.737,22.014,10.782,22.043,10.823,22.097,10.83,22.156,10.826,22.194,10.851,22.236,10.894,22.37,10.952,22.494,10.996,22.624,10.977,22.73,10.954,22.817,10.927,22.86,10.92,22.931,10.795,22.964,10.752,23.256,10.458,23.312,10.388,23.457,10.174,23.545,10.03,23.646,9.823,23.656,9.71,23.643,9.614,23.623,9.341,23.596,9.262,23.468,9.115,23.463,9.048,23.489,8.993,23.528,8.971,23.552,8.943,23.537,8.816,23.583,8.766,23.679,8.732,23.922,8.71,24.048,8.691,24.147,8.666]]]},{"n":"Cape Verde","c":"CV","r":4,"ml":4,"lx":-23.64,"ly":15.07,"b":[-25.342,14.818,-22.682,17.194],"p":[[[-25.17,16.946,-25.267,16.926,-25.308,16.936,-25.322,17.015,-25.342,17.068,-25.337,17.091,-25.113,17.194,-25.035,17.176,-24.98,17.095,-25.017,17.049,-25.17,16.946]],[[-23.444,15.008,-23.505,14.916,-23.637,14.923,-23.705,14.961,-23.785,15.077,-23.783,15.166,-23.754,15.244,-23.759,15.311,-23.748,15.329,-23.707,15.317,-23.701,15.272,-23.58,15.161,-23.535,15.139,-23.444,15.008]],[[-24.887,16.818,-24.969,16.794,-25.02,16.797,-25.093,16.833,-25.07,16.871,-24.991,16.913,-24.936,16.922,-24.892,16.846,-24.887,16.818]],[[-22.918,16.237,-22.834,16.219,-22.803,16.226,-22.749,16.222,-22.693,16.169,-22.682,16.113,-22.71,16.043,-22.821,15.986,-22.884,15.993,-22.959,16.045,-22.916,16.148,-22.918,16.237]],[[-24.308,14.856,-24.386,14.818,-24.441,14.835,-24.492,14.874,-24.517,14.931,-24.497,14.98,-24.392,15.038,-24.329,15.019,-24.296,14.93,-24.308,14.856]],[[-24.088,16.623,-24.046,16.593,-24.033,16.572,-24.094,16.561,-24.243,16.599,-24.283,16.576,-24.322,16.493,-24.398,16.618,-24.393,16.664,-24.377,16.678,-24.271,16.645,-24.088,16.623]],[[-22.888,16.659,-22.92,16.608,-22.959,16.683,-22.981,16.701,-22.991,16.809,-22.933,16.841,-22.905,16.844,-22.904,16.732,-22.888,16.659]],[[-23.182,15.137,-23.21,15.133,-23.252,15.178,-23.242,15.241,-23.247,15.257,-23.21,15.324,-23.138,15.318,-23.119,15.268,-23.116,15.167,-23.182,15.137]]]},{"n":"Canada","c":"CA","r":2,"ml":1.7,"lx":-101.91,"ly":60.32,"b":[-141.002,41.675,-52.654,83.116],"p":[[[-132.656,54.127,-132.564,54.069,-132.344,54.106,-132.303,54.099,-132.262,54.076,-132.216,54.028,-132.166,53.955,-132.155,53.875,-132.175,53.847,-132.215,53.815,-132.565,53.688,-132.574,53.675,-132.567,53.664,-132.535,53.652,-132.464,53.653,-132.187,53.685,-132.172,53.707,-132.152,53.807,-132.114,53.86,-132.111,53.9,-132.136,53.996,-132.134,54.034,-131.941,54.042,-131.82,54.077,-131.696,54.143,-131.668,54.141,-131.685,54.023,-131.703,53.986,-131.821,53.842,-131.889,53.714,-131.922,53.588,-131.928,53.379,-131.957,53.309,-132.011,53.265,-132.347,53.189,-132.52,53.194,-132.675,53.263,-132.748,53.31,-132.693,53.368,-132.655,53.371,-132.546,53.359,-132.462,53.338,-132.425,53.337,-132.431,53.35,-132.67,53.459,-132.845,53.508,-132.898,53.563,-132.9,53.605,-132.913,53.629,-133.052,53.778,-133.079,53.837,-133.098,53.92,-133.098,54.006,-133.064,54.144,-133.048,54.159,-132.991,54.158,-132.893,54.141,-132.656,54.127]],[[-131.754,53.196,-131.652,53.103,-131.622,53.02,-131.635,52.922,-131.795,52.885,-131.88,52.915,-131.916,52.909,-131.972,52.88,-131.904,52.867,-131.81,52.819,-131.727,52.756,-131.611,52.745,-131.455,52.702,-131.573,52.623,-131.591,52.578,-131.444,52.453,-131.43,52.422,-131.383,52.416,-131.274,52.426,-131.26,52.416,-131.26,52.39,-131.327,52.318,-131.32,52.303,-131.259,52.292,-131.143,52.291,-131.116,52.219,-131.222,52.154,-131.422,52.238,-131.511,52.322,-131.562,52.4,-131.624,52.444,-131.81,52.542,-132.092,52.753,-132.165,52.783,-132.239,52.867,-132.26,52.907,-132.258,52.934,-132.23,52.948,-132.145,52.957,-132.144,52.999,-132.469,53.072,-132.505,53.087,-132.547,53.138,-132.524,53.145,-132.345,53.136,-132.154,53.16,-132.036,53.179,-131.99,53.202,-131.893,53.231,-131.853,53.23,-131.754,53.196]],[[-127.197,50.64,-126.701,50.516,-126.204,50.454,-125.839,50.381,-125.615,50.359,-125.534,50.342,-125.482,50.317,-125.42,50.255,-125.314,50.107,-125.233,50.012,-125.066,49.848,-124.935,49.732,-124.905,49.685,-124.932,49.67,-124.931,49.643,-124.831,49.53,-124.643,49.429,-124.496,49.38,-124.186,49.301,-123.996,49.224,-123.937,49.171,-123.854,49.119,-123.82,49.083,-123.752,48.951,-123.627,48.824,-123.497,48.582,-123.473,48.602,-123.458,48.674,-123.443,48.69,-123.415,48.698,-123.39,48.67,-123.366,48.606,-123.284,48.455,-123.311,48.411,-123.335,48.406,-123.446,48.427,-123.485,48.4,-123.536,48.345,-123.573,48.323,-123.595,48.334,-123.917,48.387,-124.115,48.436,-124.376,48.515,-124.689,48.597,-124.868,48.654,-125.017,48.711,-125.121,48.761,-125.14,48.803,-125.136,48.822,-124.935,48.956,-124.85,49.028,-124.817,49.083,-124.8,49.142,-124.813,49.213,-124.821,49.207,-124.839,49.139,-124.868,49.079,-124.904,49.031,-124.927,49.014,-125.168,48.991,-125.363,48.998,-125.46,48.941,-125.489,48.934,-125.543,48.953,-125.66,49.029,-125.829,49.092,-125.812,49.107,-125.702,49.139,-125.644,49.186,-125.655,49.193,-125.694,49.19,-125.728,49.2,-125.796,49.26,-125.835,49.277,-125.918,49.25,-125.952,49.248,-125.984,49.288,-125.938,49.38,-125.935,49.401,-126.02,49.368,-126.048,49.379,-126.075,49.409,-126.1,49.421,-126.169,49.415,-126.244,49.443,-126.27,49.432,-126.28,49.392,-126.304,49.382,-126.419,49.449,-126.445,49.451,-126.5,49.4,-126.519,49.397,-126.549,49.419,-126.564,49.543,-126.557,49.579,-126.542,49.59,-126.443,49.619,-126.158,49.65,-126.134,49.672,-126.348,49.661,-126.403,49.678,-126.463,49.72,-126.525,49.72,-126.558,49.733,-126.593,49.764,-126.683,49.876,-126.745,49.905,-126.849,49.923,-126.903,49.944,-126.926,49.935,-126.948,49.903,-126.977,49.883,-127.049,49.872,-127.114,49.88,-127.166,49.91,-127.196,49.949,-127.208,49.992,-127.179,50.05,-127.18,50.073,-127.192,50.1,-127.216,50.121,-127.25,50.138,-127.268,50.129,-127.272,50.096,-127.29,50.071,-127.349,50.052,-127.398,50.085,-127.43,50.131,-127.467,50.163,-127.675,50.163,-127.77,50.121,-127.816,50.118,-127.864,50.128,-127.873,50.15,-127.828,50.211,-127.839,50.293,-127.851,50.314,-127.947,50.326,-127.963,50.346,-127.906,50.445,-127.874,50.464,-127.832,50.471,-127.641,50.479,-127.578,50.465,-127.487,50.405,-127.489,50.427,-127.524,50.496,-127.529,50.537,-127.466,50.583,-127.526,50.597,-127.751,50.607,-127.75,50.578,-127.731,50.536,-127.865,50.499,-127.964,50.493,-128.058,50.498,-128.136,50.521,-128.267,50.609,-128.35,50.697,-128.346,50.744,-128.301,50.794,-128.242,50.828,-128.101,50.858,-127.918,50.861,-127.713,50.821,-127.197,50.64]],[[-130.025,55.888,-130.014,55.951,-130.023,56.015,-130.056,56.065,-130.098,56.109,-130.215,56.083,-130.413,56.123,-130.477,56.231,-130.649,56.264,-130.742,56.341,-130.93,56.379,-131.083,56.405,-131.199,56.449,-131.336,56.501,-131.472,56.557,-131.575,56.599,-131.652,56.596,-131.824,56.59,-131.833,56.685,-131.886,56.742,-131.866,56.793,-131.962,56.819,-132.104,56.857,-132.063,56.953,-132.032,57.027,-132.157,57.048,-132.338,57.079,-132.279,57.145,-132.232,57.199,-132.302,57.276,-132.442,57.407,-132.55,57.5,-132.692,57.645,-132.816,57.773,-132.917,57.877,-133.001,57.949,-133.12,58.078,-133.275,58.223,-133.423,58.337,-133.401,58.411,-133.546,58.503,-133.674,58.597,-133.821,58.705,-133.966,58.758,-134.069,58.796,-134.219,58.85,-134.297,58.898,-134.33,58.94,-134.364,58.969,-134.393,59.009,-134.41,59.056,-134.441,59.085,-134.622,59.155,-134.677,59.199,-134.802,59.25,-134.907,59.271,-134.944,59.288,-135.071,59.441,-135.051,59.496,-135.037,59.551,-135.051,59.579,-135.261,59.695,-135.368,59.743,-135.476,59.793,-135.703,59.729,-135.935,59.663,-136.097,59.638,-136.322,59.605,-136.247,59.533,-136.278,59.48,-136.348,59.456,-136.466,59.459,-136.467,59.28,-136.579,59.152,-136.813,59.15,-136.939,59.106,-137.126,59.041,-137.278,58.988,-137.439,58.903,-137.521,58.915,-137.484,58.991,-137.544,59.119,-137.593,59.226,-137.697,59.281,-137.871,59.374,-138.001,59.443,-138.187,59.542,-138.318,59.611,-138.454,59.683,-138.632,59.778,-138.705,59.901,-138.869,59.946,-139.043,59.993,-139.185,60.084,-139.137,60.173,-139.079,60.279,-139.079,60.344,-139.235,60.34,-139.468,60.334,-139.676,60.328,-139.831,60.253,-139.973,60.183,-140.197,60.238,-140.453,60.3,-140.525,60.218,-140.763,60.259,-141.002,60.3,-141.002,60.592,-141.002,60.885,-141.002,61.177,-141.002,61.469,-141.002,61.761,-141.002,62.053,-141.002,62.346,-141.002,62.638,-141.002,62.93,-141.002,63.222,-141.002,63.514,-141.002,63.807,-141.002,64.099,-141.002,64.391,-141.002,64.683,-141.002,64.976,-141.002,65.268,-141.002,65.56,-141.002,65.852,-141.002,66.144,-141.002,66.437,-141.002,66.729,-141.002,67.021,-141.002,67.313,-141.002,67.605,-141.002,67.898,-141.002,68.19,-141.002,68.482,-141.002,68.774,-141.002,69.066,-141.002,69.359,-141.002,69.651,-140.86,69.635,-140.405,69.602,-139.977,69.622,-139.182,69.516,-138.69,69.317,-138.291,69.219,-138.128,69.152,-137.869,69.093,-137.26,68.964,-137.07,68.951,-136.717,68.889,-136.499,68.897,-136.122,68.882,-135.867,68.833,-135.362,68.696,-135.259,68.684,-135.231,68.694,-135.407,68.829,-135.435,68.842,-135.638,68.892,-135.876,68.917,-135.895,68.927,-135.939,68.974,-135.925,68.993,-135.873,69.001,-135.695,69.001,-135.59,69.008,-135.576,69.027,-135.651,69.031,-135.743,69.049,-135.85,69.081,-135.91,69.111,-135.691,69.311,-135.615,69.291,-135.5,69.337,-135.293,69.308,-135.255,69.324,-135.23,69.425,-135.199,69.45,-135.141,69.468,-134.853,69.486,-134.494,69.468,-134.457,69.478,-134.491,69.545,-134.495,69.572,-134.474,69.633,-134.451,69.665,-134.409,69.682,-134.242,69.669,-134.19,69.639,-134.134,69.587,-134.077,69.558,-133.9,69.528,-133.88,69.508,-133.947,69.429,-134.018,69.388,-134.165,69.281,-134.174,69.253,-133.948,69.301,-133.694,69.368,-133.476,69.405,-133.294,69.412,-133.163,69.434,-133.084,69.471,-133.028,69.508,-132.915,69.63,-132.84,69.651,-132.527,69.643,-132.452,69.647,-132.404,69.659,-132.413,69.674,-132.479,69.693,-132.568,69.698,-132.571,69.707,-132.538,69.727,-132.488,69.738,-132.334,69.752,-132.232,69.708,-132.163,69.705,-131.934,69.753,-131.582,69.882,-131.441,69.918,-131.319,69.924,-131.216,69.901,-131.136,69.907,-131.032,69.979,-130.991,70.018,-130.926,70.052,-130.665,70.127,-130.498,70.143,-130.396,70.129,-130.275,70.098,-130.175,70.086,-130.043,70.095,-129.945,70.091,-129.898,70.106,-129.73,70.192,-129.676,70.193,-129.623,70.168,-129.538,70.105,-129.538,70.074,-129.648,69.998,-130.459,69.78,-130.709,69.686,-130.832,69.651,-130.96,69.632,-131.208,69.616,-131.306,69.597,-131.473,69.579,-131.863,69.549,-131.938,69.535,-131.989,69.518,-132.129,69.402,-132.197,69.365,-132.331,69.308,-132.481,69.273,-132.687,69.26,-132.817,69.206,-132.968,69.101,-133.089,69.029,-133.228,68.967,-133.379,68.887,-133.418,68.844,-133.373,68.788,-133.348,68.77,-133.197,68.74,-133.138,68.747,-133.192,68.777,-133.32,68.82,-133.337,68.835,-133.304,68.847,-132.706,68.815,-132.578,68.848,-132.533,68.876,-132.542,68.89,-132.704,68.896,-132.739,68.922,-132.765,68.972,-132.77,69.012,-132.755,69.042,-132.719,69.079,-132.545,69.141,-132.358,69.167,-132.214,69.202,-132.134,69.234,-131.92,69.291,-131.833,69.336,-131.787,69.371,-131.781,69.389,-131.82,69.402,-131.788,69.432,-131.632,69.459,-131.563,69.461,-131.343,69.435,-131.303,69.415,-131.325,69.361,-131.294,69.364,-131.209,69.432,-131.162,69.455,-131.113,69.459,-131.063,69.451,-131.013,69.429,-130.986,69.363,-130.982,69.253,-130.971,69.209,-130.914,69.285,-130.875,69.32,-130.661,69.481,-130.516,69.57,-130.354,69.656,-130.118,69.72,-129.572,69.827,-129.265,69.855,-129.109,69.882,-129.033,69.905,-128.984,69.933,-128.899,69.966,-128.884,69.963,-128.917,69.895,-128.939,69.875,-129.138,69.833,-129.158,69.8,-129.136,69.75,-129.102,69.717,-129.054,69.701,-128.971,69.712,-128.853,69.751,-128.706,69.81,-128.387,69.96,-128.359,69.988,-128.279,70.108,-128.096,70.161,-127.765,70.222,-127.684,70.26,-127.974,70.293,-128.034,70.315,-128.044,70.329,-127.989,70.363,-128.121,70.397,-128.17,70.418,-128.168,70.48,-128.127,70.524,-128.04,70.566,-127.991,70.574,-127.862,70.549,-127.753,70.517,-127.377,70.369,-127.226,70.296,-127.138,70.239,-126.927,70.062,-126.833,69.959,-126.759,69.853,-126.685,69.777,-126.612,69.73,-126.25,69.545,-126.064,69.467,-125.907,69.419,-125.728,69.38,-125.525,69.352,-125.387,69.349,-125.172,69.428,-125.167,69.48,-125.262,69.566,-125.357,69.626,-125.346,69.662,-125.219,69.732,-125.228,69.757,-125.201,69.829,-125.114,69.815,-125.08,69.818,-125.031,69.844,-124.968,69.894,-124.889,69.936,-124.794,69.969,-124.768,69.99,-124.863,70.006,-124.92,70.006,-124.963,70.013,-124.99,70.027,-124.952,70.042,-124.745,70.08,-124.706,70.117,-124.64,70.141,-124.555,70.151,-124.503,70.141,-124.444,70.111,-124.442,70.062,-124.467,69.983,-124.472,69.919,-124.407,69.767,-124.349,69.735,-124.125,69.69,-124.138,69.653,-124.398,69.494,-124.454,69.455,-124.481,69.425,-124.472,69.4,-124.426,69.379,-124.338,69.365,-124.112,69.359,-124.05,69.373,-123.609,69.377,-123.528,69.389,-123.46,69.42,-123.361,69.497,-123.249,69.52,-123.214,69.542,-123.144,69.632,-123.11,69.738,-123.077,69.782,-123.026,69.81,-122.957,69.819,-122.785,69.808,-122.705,69.817,-122.387,69.808,-122.07,69.816,-121.742,69.798,-121.531,69.776,-121.336,69.742,-120.962,69.66,-120.815,69.617,-120.293,69.421,-120.14,69.381,-119.853,69.342,-118.869,69.257,-118.745,69.234,-118.486,69.145,-118.307,69.093,-118.095,69.043,-117.83,69,-117.311,68.935,-117.227,68.913,-117.132,68.907,-117.026,68.916,-116.55,68.879,-116.425,68.881,-116.334,68.874,-116.223,68.847,-116.059,68.837,-116.065,68.855,-116.252,68.958,-116.243,68.974,-116.167,68.975,-115.936,68.958,-115.883,68.987,-115.806,68.987,-115.631,68.973,-115.442,68.941,-115.24,68.892,-114.994,68.85,-114.62,68.746,-114.414,68.66,-114.218,68.552,-114.111,68.477,-114.092,68.435,-114.051,68.415,-113.988,68.415,-113.964,68.399,-114.021,68.306,-114.053,68.283,-114.096,68.267,-114.275,68.248,-114.765,68.27,-114.852,68.195,-115.127,68.132,-115.176,68.104,-115.187,68.084,-115.167,68.019,-115.202,67.998,-115.427,67.924,-115.434,67.902,-115.288,67.872,-115.133,67.819,-115.011,67.806,-114.857,67.814,-114.663,67.795,-114.429,67.751,-114.267,67.731,-114.176,67.735,-114.051,67.727,-113.893,67.707,-113.682,67.7,-113.215,67.702,-113.075,67.687,-112.879,67.68,-112.503,67.682,-112.435,67.685,-112.315,67.72,-112.237,67.731,-112.101,67.732,-111.711,67.757,-111.576,67.757,-111.451,67.776,-111.291,67.815,-111.192,67.823,-111.155,67.798,-111.087,67.788,-110.99,67.791,-110.805,67.832,-110.372,67.954,-110.216,67.954,-110.102,67.992,-110.074,67.993,-110.042,67.977,-109.937,67.888,-109.904,67.874,-109.831,67.866,-109.76,67.82,-109.686,67.752,-109.63,67.733,-109.224,67.73,-109.081,67.711,-109.038,67.691,-108.994,67.637,-108.968,67.532,-108.95,67.494,-108.891,67.438,-108.852,67.422,-108.815,67.438,-108.715,67.583,-108.68,67.606,-108.613,67.598,-108.593,67.591,-108.492,67.483,-108.347,67.403,-107.989,67.256,-107.931,67.202,-107.909,67.163,-107.929,67.127,-107.991,67.095,-108.088,67.07,-108.221,67.051,-108.344,67.058,-108.459,67.09,-108.496,67.092,-108.455,67.063,-108.218,66.941,-108.158,66.893,-108.101,66.86,-108.05,66.844,-108.002,66.818,-107.958,66.781,-107.761,66.684,-107.705,66.637,-107.48,66.492,-107.374,66.435,-107.291,66.402,-107.259,66.399,-107.278,66.425,-107.564,66.619,-107.71,66.74,-107.731,66.769,-107.74,66.814,-107.746,66.961,-107.725,66.984,-107.626,67.003,-107.499,66.936,-107.451,66.927,-107.419,66.931,-107.402,66.948,-107.329,66.932,-107.2,66.883,-107.156,66.882,-107.254,66.976,-107.323,67.023,-107.348,67.055,-107.283,67.103,-107.318,67.128,-107.482,67.199,-107.567,67.273,-107.644,67.385,-107.651,67.428,-107.638,67.474,-107.65,67.511,-107.753,67.587,-107.865,67.639,-107.954,67.7,-107.972,67.732,-107.958,67.819,-107.891,67.856,-107.763,67.907,-107.729,67.959,-107.787,68.013,-107.798,68.037,-107.761,68.032,-107.509,68.059,-107.446,68.05,-107.351,68.061,-107.224,68.094,-107.125,68.108,-106.994,68.106,-106.923,68.114,-106.836,68.129,-106.791,68.145,-106.711,68.207,-106.668,68.216,-106.535,68.209,-106.459,68.196,-106.424,68.201,-106.429,68.288,-106.404,68.319,-106.271,68.383,-106.132,68.39,-106.039,68.407,-105.933,68.443,-105.857,68.475,-105.781,68.527,-105.75,68.592,-105.774,68.611,-105.932,68.637,-106.027,68.623,-106.237,68.577,-106.458,68.516,-106.543,68.461,-106.567,68.389,-106.608,68.357,-106.78,68.387,-106.854,68.387,-106.946,68.374,-107.043,68.347,-107.146,68.304,-107.298,68.296,-107.499,68.324,-107.619,68.331,-107.742,68.286,-107.734,68.252,-107.678,68.203,-107.734,68.174,-108.027,68.163,-108.105,68.169,-108.261,68.15,-108.323,68.154,-108.368,68.178,-108.687,68.277,-108.718,68.297,-108.641,68.379,-108.346,68.598,-108.313,68.611,-107.766,68.649,-107.436,68.689,-106.831,68.809,-106.713,68.819,-106.324,68.899,-106.164,68.92,-106.016,68.906,-105.798,68.865,-105.686,68.828,-105.606,68.782,-105.54,68.719,-105.457,68.578,-105.429,68.458,-105.377,68.414,-105.195,68.33,-105.101,68.298,-105.044,68.288,-104.994,68.307,-104.96,68.311,-104.937,68.303,-104.912,68.25,-104.879,68.245,-104.77,68.252,-104.653,68.23,-104.636,68.214,-104.661,68.149,-104.628,68.121,-104.487,68.063,-104.351,68.041,-104.194,68.031,-103.902,68.041,-103.657,68.069,-103.474,68.115,-103.323,68.064,-103.022,67.94,-102.842,67.853,-102.692,67.812,-102.389,67.762,-102.32,67.736,-102.21,67.733,-102.057,67.753,-101.884,67.745,-101.689,67.709,-101.555,67.693,-101.096,67.762,-101.026,67.766,-100.856,67.799,-100.746,67.809,-100.616,67.808,-100.52,67.818,-100.456,67.839,-100.213,67.839,-99.773,67.815,-99.472,67.784,-99.294,67.745,-99.147,67.724,-99.032,67.719,-98.92,67.726,-98.812,67.744,-98.697,67.78,-98.453,67.798,-98.412,67.807,-98.417,67.826,-98.468,67.856,-98.606,67.911,-98.704,67.966,-98.722,68,-98.72,68.042,-98.69,68.066,-98.632,68.073,-98.54,68.047,-98.415,67.988,-98.063,67.77,-97.978,67.739,-97.931,67.711,-97.607,67.631,-97.455,67.617,-97.274,67.666,-97.194,67.697,-97.155,67.726,-97.157,67.755,-97.14,67.796,-97.158,67.822,-97.207,67.855,-97.336,67.901,-97.547,67.961,-97.739,67.978,-97.913,67.954,-98.11,67.903,-98.193,67.923,-98.438,68.065,-98.5,68.118,-98.5,68.132,-98.386,68.115,-98.381,68.132,-98.449,68.201,-98.491,68.224,-98.633,68.331,-98.65,68.364,-98.562,68.371,-98.522,68.383,-98.469,68.382,-98.219,68.317,-98.091,68.346,-97.794,68.388,-97.911,68.45,-97.939,68.51,-97.925,68.524,-97.829,68.533,-97.64,68.482,-97.548,68.475,-97.481,68.495,-97.41,68.497,-97.336,68.479,-97.266,68.453,-97.136,68.378,-97.072,68.333,-97,68.265,-96.977,68.255,-96.628,68.25,-96.431,68.311,-96.435,68.29,-96.48,68.243,-96.725,68.061,-96.722,68.039,-96.592,68.048,-96.531,68.063,-96.494,68.085,-96.461,68.136,-96.439,68.151,-96.076,68.237,-95.97,68.249,-96.036,68.158,-96.171,67.832,-96.199,67.718,-96.228,67.679,-96.371,67.554,-96.369,67.51,-96.213,67.404,-96.185,67.376,-96.169,67.289,-96.141,67.272,-96.013,67.271,-95.879,67.298,-95.72,67.317,-95.695,67.299,-95.783,67.194,-95.778,67.185,-95.626,67.212,-95.557,67.215,-95.529,67.209,-95.416,67.156,-95.405,67.116,-95.407,67.056,-95.419,67.013,-95.457,66.989,-95.502,66.98,-95.559,66.973,-95.611,66.976,-95.769,66.967,-95.862,66.978,-95.954,67.011,-96.02,67.019,-96.095,66.994,-96.216,66.998,-96.35,67.07,-96.404,67.063,-96.423,67.052,-96.42,67.036,-96.36,66.989,-95.885,66.741,-95.813,66.69,-95.797,66.617,-95.788,66.617,-95.743,66.69,-95.772,66.726,-96.016,66.87,-96.045,66.923,-96.037,66.938,-95.972,66.952,-95.625,66.916,-95.49,66.924,-95.4,66.949,-95.354,66.981,-95.321,67.152,-95.259,67.263,-95.296,67.361,-95.39,67.518,-95.463,67.61,-95.634,67.704,-95.65,67.737,-95.461,68.021,-95.427,68.045,-95.384,68.056,-95.235,68.06,-95.126,68.083,-94.955,68.05,-94.861,68.042,-94.744,68.071,-94.485,68.19,-94.384,68.227,-94.255,68.297,-94.098,68.399,-93.928,68.474,-93.652,68.543,-93.483,68.599,-93.449,68.619,-93.606,68.624,-93.644,68.633,-93.676,68.686,-93.66,68.784,-93.663,68.838,-93.681,68.887,-93.716,68.931,-93.766,68.97,-93.811,68.993,-93.852,69,-93.881,68.997,-93.896,68.982,-93.938,68.889,-93.992,68.821,-94.065,68.785,-94.217,68.761,-94.478,68.743,-94.587,68.776,-94.6,68.803,-94.563,68.912,-94.476,68.958,-94.237,69.05,-94.084,69.123,-94.081,69.136,-94.222,69.136,-94.255,69.151,-94.285,69.242,-94.277,69.275,-94.255,69.314,-94.156,69.342,-93.854,69.376,-93.619,69.417,-93.613,69.403,-93.801,69.281,-93.82,69.253,-93.749,69.226,-93.567,69.297,-93.451,69.355,-93.431,69.375,-93.537,69.382,-93.543,69.406,-93.522,69.451,-93.532,69.481,-93.65,69.519,-93.794,69.498,-93.915,69.458,-94.015,69.447,-94.163,69.446,-94.271,69.455,-94.338,69.474,-94.419,69.517,-94.514,69.583,-94.634,69.65,-94.676,69.657,-94.713,69.649,-94.789,69.585,-94.823,69.578,-95.292,69.667,-95.491,69.718,-95.588,69.756,-95.707,69.778,-95.851,69.785,-95.965,69.803,-96.05,69.831,-96.119,69.872,-96.172,69.925,-96.269,69.992,-96.492,70.125,-96.551,70.21,-96.56,70.243,-96.546,70.327,-96.337,70.47,-96.298,70.511,-96.226,70.542,-96.123,70.561,-96.048,70.567,-95.879,70.549,-95.98,70.593,-95.988,70.617,-95.886,70.694,-95.906,70.698,-96.186,70.638,-96.258,70.642,-96.359,70.679,-96.549,70.809,-96.551,70.89,-96.491,71.002,-96.47,71.07,-96.525,71.127,-96.504,71.143,-96.445,71.159,-96.421,71.176,-96.447,71.24,-96.406,71.274,-96.271,71.339,-96.14,71.396,-96.062,71.414,-95.994,71.411,-95.924,71.393,-95.851,71.361,-95.725,71.328,-95.633,71.319,-95.564,71.337,-95.448,71.46,-95.406,71.492,-95.445,71.505,-95.674,71.504,-95.773,71.514,-95.83,71.526,-95.872,71.573,-95.838,71.598,-95.616,71.685,-95.512,71.777,-95.201,71.904,-94.887,71.963,-94.735,71.983,-94.611,71.987,-94.557,71.979,-94.491,71.916,-94.479,71.849,-94.308,71.765,-94.171,71.758,-94.086,71.771,-93.81,71.766,-93.746,71.743,-93.751,71.717,-93.782,71.674,-93.763,71.638,-93.576,71.569,-93.407,71.521,-93.256,71.461,-93.031,71.336,-92.983,71.3,-92.949,71.262,-92.89,71.122,-92.883,71.069,-92.904,70.916,-92.922,70.887,-92.981,70.852,-92.961,70.838,-92.783,70.798,-92.642,70.719,-92.567,70.693,-92.388,70.65,-92.356,70.634,-92.315,70.608,-92.214,70.493,-92.049,70.39,-92.037,70.367,-92.073,70.319,-92.047,70.303,-91.984,70.286,-91.926,70.295,-91.876,70.331,-91.82,70.342,-91.761,70.326,-91.716,70.299,-91.654,70.233,-91.564,70.178,-91.572,70.162,-91.616,70.148,-91.859,70.133,-91.995,70.143,-92.121,70.17,-92.209,70.198,-92.29,70.24,-92.321,70.235,-92.363,70.201,-92.455,70.15,-92.512,70.104,-92.446,70.083,-92.127,70.085,-92.058,70.071,-91.977,70.039,-92.069,69.984,-92.285,69.892,-92.751,69.714,-92.888,69.668,-92.855,69.655,-92.803,69.651,-92.643,69.659,-92.493,69.683,-92.312,69.673,-92.231,69.653,-92.258,69.634,-92.209,69.603,-91.912,69.531,-91.724,69.546,-91.532,69.615,-91.384,69.649,-91.202,69.645,-91.151,69.637,-91.17,69.62,-91.305,69.581,-91.427,69.538,-91.44,69.526,-91.288,69.543,-90.95,69.515,-90.786,69.509,-90.667,69.516,-90.555,69.504,-90.451,69.475,-90.416,69.457,-90.513,69.445,-90.606,69.445,-90.684,69.428,-90.749,69.392,-90.795,69.347,-90.822,69.29,-90.892,69.267,-91.005,69.277,-91.049,69.293,-91.025,69.315,-91.058,69.318,-91.148,69.303,-91.218,69.302,-91.237,69.286,-90.745,69.106,-90.587,68.947,-90.479,68.881,-90.468,68.864,-90.539,68.82,-90.543,68.786,-90.51,68.689,-90.525,68.611,-90.574,68.475,-90.528,68.432,-90.423,68.395,-90.36,68.347,-90.317,68.33,-90.285,68.292,-90.248,68.267,-90.205,68.257,-90.174,68.27,-90.157,68.306,-90.116,68.339,-90.005,68.398,-89.898,68.491,-89.879,68.522,-89.897,68.594,-89.884,68.626,-89.783,68.736,-89.751,68.812,-89.72,68.932,-89.667,69.015,-89.552,69.085,-89.352,69.227,-89.28,69.255,-89.198,69.269,-89.057,69.266,-88.954,69.22,-88.815,69.136,-88.638,69.059,-88.316,68.954,-88.224,68.915,-88.041,68.812,-87.964,68.709,-87.912,68.565,-87.866,68.478,-87.828,68.448,-87.81,68.404,-87.814,68.346,-87.828,68.3,-87.853,68.267,-87.893,68.248,-87.991,68.242,-88.111,68.251,-88.146,68.266,-88.209,68.335,-88.235,68.339,-88.347,68.288,-88.361,68.26,-88.32,68.166,-88.325,67.989,-88.314,67.95,-88.196,67.766,-87.997,67.626,-87.499,67.355,-87.471,67.325,-87.418,67.214,-87.392,67.191,-87.359,67.177,-87.32,67.173,-87.266,67.184,-87.083,67.268,-86.924,67.356,-86.813,67.402,-86.75,67.406,-86.682,67.422,-86.609,67.451,-86.561,67.482,-86.536,67.516,-86.504,67.649,-86.476,67.713,-86.398,67.8,-86.37,67.825,-85.984,68.045,-85.953,68.072,-85.789,68.328,-85.731,68.445,-85.723,68.515,-85.745,68.578,-85.734,68.63,-85.69,68.671,-85.643,68.7,-85.562,68.729,-85.518,68.77,-85.491,68.774,-85.425,68.774,-85.338,68.746,-85.275,68.741,-84.868,68.773,-84.867,68.79,-85.107,68.844,-85.104,68.871,-85.083,68.908,-85.008,68.949,-84.916,68.962,-84.895,68.989,-84.893,69.021,-84.862,69.074,-84.89,69.093,-85.114,69.166,-85.243,69.163,-85.275,69.172,-85.387,69.232,-85.428,69.318,-85.432,69.354,-85.416,69.411,-85.402,69.427,-85.409,69.452,-85.437,69.488,-85.44,69.52,-85.416,69.548,-85.43,69.581,-85.482,69.619,-85.502,69.652,-85.448,69.748,-85.446,69.778,-85.497,69.819,-85.535,69.835,-85.507,69.845,-85.415,69.85,-85.305,69.836,-85.177,69.805,-85.02,69.805,-84.834,69.835,-84.645,69.85,-84.319,69.844,-84.242,69.835,-83.917,69.745,-83.665,69.7,-83.552,69.704,-82.991,69.686,-82.746,69.695,-82.618,69.691,-82.374,69.642,-82.39,69.601,-82.496,69.532,-82.633,69.518,-82.755,69.494,-82.642,69.458,-82.31,69.41,-82.232,69.333,-82.208,69.297,-82.247,69.265,-82.228,69.249,-82.151,69.249,-81.952,69.276,-81.732,69.258,-81.412,69.198,-81.378,69.186,-81.322,69.139,-81.329,69.12,-81.611,69.003,-81.758,68.957,-81.952,68.909,-81.958,68.884,-81.687,68.879,-81.476,68.866,-81.381,68.85,-81.331,68.828,-81.264,68.781,-81.252,68.743,-81.259,68.692,-81.282,68.657,-81.527,68.556,-81.64,68.524,-81.831,68.487,-81.915,68.459,-82.006,68.463,-82.106,68.499,-82.21,68.506,-82.397,68.478,-82.499,68.479,-82.549,68.469,-82.553,68.446,-82.464,68.382,-82.413,68.357,-82.393,68.338,-82.431,68.307,-82.423,68.297,-82.393,68.285,-82.222,68.145,-82.187,68.134,-82.151,68.14,-82.078,68.18,-82.034,68.196,-82.012,68.194,-82.013,68.173,-82.092,68.051,-82.102,68.019,-82.1,67.99,-82.063,67.928,-81.976,67.862,-81.869,67.802,-81.709,67.722,-81.493,67.637,-81.412,67.595,-81.294,67.497,-81.27,67.46,-81.301,67.357,-81.387,67.189,-81.443,67.093,-81.468,67.07,-81.63,67.002,-81.722,66.986,-81.874,66.988,-81.926,66.975,-82.005,66.92,-82.113,66.825,-82.198,66.765,-82.261,66.739,-82.375,66.709,-82.554,66.621,-82.642,66.588,-82.949,66.551,-83.199,66.431,-83.298,66.392,-83.406,66.371,-83.523,66.369,-83.59,66.388,-83.628,66.461,-83.651,66.485,-83.739,66.534,-83.92,66.679,-83.998,66.729,-84.05,66.74,-84.154,66.732,-84.208,66.736,-84.324,66.782,-84.366,66.811,-84.361,66.823,-84.273,66.839,-84.31,66.863,-84.466,66.927,-84.531,66.961,-84.538,66.973,-84.693,67.017,-84.846,67.029,-85.04,66.956,-85.114,66.907,-85.111,66.891,-85.018,66.872,-84.978,66.881,-84.899,66.927,-84.857,66.941,-84.738,66.934,-84.639,66.902,-84.603,66.875,-84.59,66.857,-84.319,66.712,-84.223,66.682,-84.183,66.648,-84.153,66.59,-84.094,66.526,-83.964,66.421,-83.826,66.29,-83.798,66.238,-83.869,66.214,-83.905,66.212,-84.012,66.231,-84.293,66.292,-84.324,66.291,-84.398,66.259,-84.459,66.186,-84.478,66.179,-84.628,66.208,-84.908,66.271,-85.096,66.325,-85.192,66.37,-85.307,66.44,-85.442,66.537,-85.604,66.568,-85.792,66.533,-86.063,66.52,-86.633,66.531,-86.708,66.523,-86.737,66.511,-86.689,66.457,-86.7,66.443,-86.738,66.433,-86.747,66.417,-86.685,66.36,-86.585,66.322,-86.301,66.27,-86.113,66.225,-86.001,66.187,-85.964,66.154,-85.959,66.119,-86.012,66.048,-86.043,66.023,-86.702,65.671,-86.953,65.528,-87.081,65.441,-87.194,65.383,-87.291,65.355,-87.453,65.339,-87.678,65.335,-87.97,65.349,-88.121,65.395,-88.395,65.516,-88.587,65.588,-88.672,65.612,-88.744,65.679,-88.808,65.692,-88.946,65.703,-89.088,65.739,-89.42,65.861,-89.593,65.909,-89.749,65.936,-89.89,65.941,-89.944,65.934,-89.848,65.872,-89.89,65.869,-90.004,65.883,-90.117,65.882,-90.316,65.926,-90.513,65.921,-90.655,65.929,-90.826,65.954,-91.01,65.966,-91.305,65.965,-91.412,65.959,-91.427,65.948,-91.285,65.894,-91.041,65.83,-91.074,65.886,-91.065,65.9,-90.983,65.919,-90.597,65.885,-90.159,65.813,-90.048,65.806,-89.924,65.78,-89.788,65.737,-89.6,65.648,-89.242,65.446,-89.127,65.396,-88.974,65.348,-88.198,65.28,-87.93,65.28,-87.392,65.261,-87.108,65.225,-87.028,65.198,-87.003,65.109,-87.029,65.064,-87.183,64.927,-87.281,64.826,-87.885,64.4,-87.964,64.302,-87.998,64.244,-88.106,64.183,-88.379,64.089,-88.653,64.009,-88.818,63.992,-88.964,64.011,-89.06,64.034,-89.201,64.114,-89.209,64.105,-89.108,63.981,-89.132,63.969,-89.215,63.984,-89.404,64.04,-89.465,64.03,-89.501,64.015,-89.551,64.015,-89.616,64.031,-89.733,64.077,-89.764,64.1,-89.792,64.168,-89.811,64.181,-90.042,64.141,-90.08,64.128,-89.986,64.1,-89.954,64.081,-89.861,63.979,-89.856,63.957,-89.922,63.944,-90.142,63.982,-90.168,63.979,-90.06,63.877,-90.018,63.829,-90.013,63.804,-90.155,63.69,-90.245,63.642,-90.369,63.624,-90.446,63.636,-90.533,63.665,-90.596,63.661,-90.635,63.624,-90.707,63.597,-90.812,63.581,-90.946,63.588,-91.108,63.618,-91.539,63.726,-91.675,63.742,-91.926,63.757,-91.956,63.772,-91.954,63.787,-91.919,63.801,-91.929,63.812,-91.982,63.822,-92.038,63.813,-92.095,63.784,-92.195,63.776,-92.338,63.788,-92.55,63.83,-92.97,63.938,-93.43,64.029,-93.696,64.147,-93.597,64.041,-93.605,64.004,-93.656,63.973,-93.664,63.941,-93.56,63.865,-93.416,63.838,-93.27,63.841,-93.266,63.853,-93.327,63.872,-93.38,63.9,-93.406,63.941,-93.379,63.948,-93.25,63.927,-93.166,63.902,-92.529,63.761,-92.339,63.735,-92.196,63.708,-92.157,63.692,-92.205,63.657,-92.461,63.569,-92.465,63.555,-92.29,63.563,-92.077,63.64,-91.957,63.676,-91.842,63.698,-91.686,63.66,-91.489,63.562,-91.33,63.507,-91.103,63.476,-90.97,63.443,-90.747,63.352,-90.711,63.304,-90.691,63.111,-90.699,63.064,-90.728,63.017,-90.778,62.972,-90.871,62.946,-91.008,62.94,-91.115,62.922,-91.349,62.819,-91.449,62.804,-91.87,62.835,-92.034,62.863,-92.11,62.862,-92.152,62.839,-92.196,62.829,-92.361,62.819,-92.388,62.801,-92.378,62.772,-92.345,62.734,-92.305,62.712,-92.243,62.684,-92.149,62.665,-91.956,62.645,-91.936,62.592,-91.944,62.575,-92.008,62.541,-92.081,62.544,-92.207,62.585,-92.27,62.587,-92.324,62.565,-92.4,62.557,-92.497,62.565,-92.551,62.547,-92.562,62.503,-92.595,62.47,-92.707,62.418,-92.768,62.38,-92.766,62.35,-92.701,62.328,-92.627,62.279,-92.544,62.202,-92.528,62.168,-92.579,62.177,-92.648,62.208,-92.735,62.26,-92.866,62.306,-93.154,62.367,-93.205,62.365,-93.179,62.35,-92.988,62.286,-92.914,62.245,-92.906,62.215,-93.066,62.15,-93.07,62.128,-93.028,62.109,-93.016,62.093,-93.073,62.061,-93.167,62.034,-93.35,62.03,-93.366,62.015,-93.297,61.982,-93.273,61.961,-93.333,61.933,-93.372,61.929,-93.582,61.942,-93.527,61.872,-93.494,61.847,-93.43,61.812,-93.314,61.78,-93.312,61.767,-93.352,61.74,-93.421,61.706,-93.71,61.603,-93.913,61.481,-93.941,61.444,-93.889,61.364,-93.889,61.344,-93.942,61.308,-94.061,61.318,-94.083,61.304,-94.055,61.266,-94.05,61.211,-94.068,61.139,-94.154,61.025,-94.309,60.871,-94.427,60.731,-94.509,60.605,-94.569,60.542,-94.679,60.538,-94.762,60.498,-94.705,60.478,-94.671,60.453,-94.647,60.416,-94.67,60.301,-94.742,60.107,-94.786,59.953,-94.777,59.478,-94.788,59.268,-94.82,59.151,-94.87,59.088,-94.957,59.069,-94.847,59.05,-94.776,59.021,-94.744,58.975,-94.713,58.903,-94.673,58.87,-94.624,58.876,-94.579,58.868,-94.54,58.848,-94.419,58.746,-94.287,58.716,-94.281,58.659,-94.333,58.339,-94.332,58.297,-94.272,58.378,-94.209,58.626,-94.123,58.737,-94.056,58.76,-93.78,58.773,-93.486,58.744,-93.375,58.741,-93.278,58.756,-93.179,58.726,-93.155,58.695,-93.127,58.564,-93.1,58.49,-92.925,58.225,-92.842,58.076,-92.74,57.844,-92.702,57.778,-92.49,57.469,-92.449,57.385,-92.433,57.32,-92.44,57.275,-92.478,57.205,-92.548,57.111,-92.614,57.039,-92.675,56.99,-92.738,56.953,-92.802,56.928,-92.798,56.922,-92.725,56.934,-92.651,56.958,-92.51,57.022,-92.456,57.037,-92.303,57.046,-92.298,57.023,-92.372,56.975,-92.356,56.971,-92.249,57.009,-92.018,57.064,-91.111,57.241,-90.897,57.257,-90.592,57.224,-90.345,57.149,-90.075,57.052,-89.791,56.981,-89.342,56.915,-89.212,56.884,-88.948,56.851,-88.826,56.814,-88.68,56.725,-88.447,56.609,-88.271,56.536,-88.075,56.467,-87.878,56.342,-87.561,56.056,-87.482,56.021,-87.287,55.975,-86.919,55.915,-86.377,55.773,-86.139,55.718,-85.984,55.696,-85.831,55.657,-85.677,55.601,-85.559,55.54,-85.478,55.474,-85.407,55.431,-85.283,55.383,-85.218,55.349,-85.212,55.297,-85.362,55.095,-85.365,55.079,-85.214,55.224,-85.129,55.266,-85.061,55.286,-84.92,55.283,-84.706,55.259,-84.518,55.259,-84.356,55.283,-84.219,55.293,-84.105,55.291,-84.023,55.298,-83.972,55.314,-83.911,55.315,-83.668,55.265,-83.569,55.262,-83.214,55.215,-82.986,55.231,-82.947,55.222,-82.868,55.161,-82.801,55.156,-82.687,55.166,-82.577,55.149,-82.393,55.068,-82.308,54.998,-82.227,54.856,-82.219,54.813,-82.371,54.483,-82.418,54.356,-82.424,54.245,-82.394,54.18,-82.264,54.073,-82.24,54.045,-82.163,53.886,-82.141,53.818,-82.15,53.74,-82.191,53.611,-82.18,53.513,-82.146,53.365,-82.159,53.264,-82.219,53.211,-82.26,53.16,-82.292,53.066,-82.292,53.031,-82.26,52.961,-82.203,52.922,-82.108,52.877,-82.02,52.812,-81.859,52.651,-81.742,52.564,-81.599,52.433,-81.572,52.367,-81.612,52.324,-81.661,52.294,-81.776,52.254,-81.828,52.224,-81.815,52.217,-81.648,52.239,-81.55,52.237,-81.466,52.204,-81.398,52.142,-81.285,52.089,-81.127,52.045,-80.969,51.972,-80.706,51.798,-80.658,51.758,-80.588,51.667,-80.496,51.525,-80.448,51.432,-80.443,51.389,-80.496,51.345,-80.673,51.265,-80.851,51.125,-80.795,51.132,-80.677,51.191,-80.478,51.307,-80.368,51.33,-80.266,51.316,-80.104,51.283,-79.96,51.235,-79.836,51.173,-79.652,51.008,-79.456,50.876,-79.348,50.763,-79.381,50.835,-79.453,50.917,-79.636,51.049,-79.714,51.118,-79.731,51.15,-79.737,51.186,-79.723,51.252,-79.689,51.347,-79.643,51.414,-79.586,51.452,-79.547,51.494,-79.528,51.538,-79.498,51.57,-79.339,51.628,-79.297,51.623,-79.264,51.552,-79.226,51.537,-79.153,51.526,-79.091,51.502,-79.041,51.464,-79.005,51.425,-78.984,51.386,-78.937,51.259,-78.903,51.2,-78.898,51.272,-78.858,51.384,-78.827,51.43,-78.731,51.497,-78.736,51.527,-78.776,51.566,-78.978,51.734,-78.982,51.775,-78.928,51.799,-78.891,51.845,-78.871,51.913,-78.828,51.963,-78.702,52.033,-78.593,52.14,-78.537,52.213,-78.492,52.252,-78.448,52.261,-78.513,52.291,-78.526,52.311,-78.529,52.399,-78.557,52.492,-78.601,52.535,-78.724,52.628,-78.744,52.655,-78.766,52.76,-78.754,52.812,-78.722,52.856,-78.74,52.899,-78.854,52.976,-78.898,53.043,-78.947,53.206,-78.992,53.41,-79.043,53.56,-79.1,53.657,-79.113,53.717,-79.081,53.742,-79.04,53.818,-79.003,53.837,-78.946,53.832,-78.944,53.84,-79.032,53.881,-79.075,53.932,-79.073,53.951,-78.996,54.002,-79.01,54.024,-79.067,54.052,-79.242,54.099,-79.179,54.117,-79.139,54.157,-79.147,54.169,-79.216,54.186,-79.296,54.217,-79.356,54.263,-79.431,54.337,-79.476,54.395,-79.521,54.492,-79.598,54.602,-79.632,54.629,-79.67,54.647,-79.714,54.655,-79.712,54.672,-79.666,54.697,-78.909,54.881,-78.846,54.908,-78.475,55.011,-78.304,55.069,-78.129,55.151,-77.891,55.236,-77.775,55.291,-77.702,55.344,-77.325,55.556,-77.165,55.664,-77.073,55.756,-76.938,55.867,-76.762,55.996,-76.65,56.107,-76.604,56.2,-76.546,56.359,-76.53,56.5,-76.52,56.707,-76.526,56.892,-76.573,57.181,-76.601,57.272,-76.655,57.381,-76.786,57.599,-76.81,57.658,-76.891,57.758,-77.157,58.019,-77.489,58.195,-77.552,58.24,-77.684,58.291,-77.884,58.351,-78.014,58.399,-78.352,58.581,-78.463,58.602,-78.506,58.649,-78.515,58.682,-78.502,58.769,-78.483,58.829,-78.459,58.873,-78.431,58.902,-78.244,59.035,-78.14,59.142,-78.068,59.2,-77.988,59.246,-77.843,59.305,-77.761,59.38,-77.779,59.41,-77.845,59.444,-77.859,59.476,-77.749,59.558,-77.733,59.581,-77.748,59.658,-77.726,59.676,-77.59,59.681,-77.397,59.569,-77.349,59.579,-77.411,59.61,-77.485,59.685,-77.475,59.716,-77.332,59.797,-77.328,59.833,-77.368,59.884,-77.373,59.925,-77.289,60.022,-77.312,60.042,-77.547,60.061,-77.586,60.088,-77.572,60.101,-77.461,60.133,-77.453,60.146,-77.649,60.363,-77.681,60.427,-77.598,60.507,-77.504,60.543,-77.516,60.563,-77.639,60.567,-77.715,60.578,-77.791,60.64,-77.761,60.679,-77.734,60.697,-77.661,60.79,-77.59,60.809,-77.603,60.825,-77.872,60.786,-77.998,60.818,-78.122,60.81,-78.181,60.819,-78.16,60.852,-77.934,61.003,-77.83,61.084,-77.765,61.158,-77.731,61.206,-77.727,61.231,-77.75,61.393,-77.736,61.437,-77.649,61.479,-77.514,61.556,-77.698,61.626,-77.814,61.695,-77.89,61.729,-77.948,61.762,-78.021,61.832,-78.077,61.923,-78.137,62.107,-78.147,62.209,-78.133,62.282,-78.109,62.318,-78.068,62.355,-77.9,62.427,-77.604,62.531,-77.372,62.573,-77.205,62.55,-76.879,62.525,-76.616,62.466,-75.817,62.316,-75.676,62.25,-75.809,62.193,-75.79,62.18,-75.489,62.286,-75.409,62.307,-75.341,62.312,-75.114,62.271,-75.023,62.264,-74.908,62.23,-74.633,62.116,-74.613,62.125,-74.69,62.183,-74.646,62.211,-74.429,62.272,-74.205,62.321,-74.046,62.37,-73.878,62.434,-73.764,62.469,-73.705,62.473,-73.63,62.454,-73.428,62.369,-73.299,62.325,-73.195,62.279,-73.049,62.198,-72.992,62.18,-72.882,62.125,-72.735,62.131,-72.687,62.125,-72.671,62.114,-72.646,62.077,-72.633,62.053,-72.632,62.027,-72.666,61.955,-72.772,61.84,-72.727,61.839,-72.661,61.863,-72.574,61.907,-72.506,61.923,-72.361,61.888,-72.226,61.832,-72.178,61.802,-72.126,61.753,-72.081,61.728,-72.04,61.68,-72.043,61.665,-72.082,61.641,-72.247,61.602,-72.216,61.587,-72.023,61.612,-71.964,61.636,-71.922,61.677,-71.866,61.689,-71.638,61.617,-71.605,61.592,-71.619,61.573,-71.656,61.551,-71.756,61.527,-71.841,61.466,-71.854,61.44,-71.794,61.421,-71.645,61.413,-71.646,61.399,-71.732,61.372,-71.743,61.337,-71.552,61.213,-71.423,61.159,-71.348,61.149,-71.175,61.147,-71.035,61.126,-70.723,61.055,-70.541,61.042,-70.384,61.064,-70.279,61.069,-70.187,61.041,-70.158,61.021,-70.144,60.981,-70.146,60.922,-70.095,60.88,-69.992,60.856,-69.909,60.86,-69.8,60.907,-69.708,60.915,-69.678,60.95,-69.65,61.014,-69.624,61.05,-69.557,61.06,-69.503,61.04,-69.472,61.011,-69.414,60.922,-69.398,60.883,-69.405,60.847,-69.433,60.814,-69.49,60.78,-69.574,60.743,-69.64,60.69,-69.721,60.567,-69.751,60.487,-69.759,60.44,-69.756,60.389,-69.741,60.332,-69.708,60.286,-69.633,60.22,-69.629,60.199,-69.623,60.145,-69.63,60.122,-69.674,60.076,-69.796,60.03,-69.963,60.018,-70.509,60.015,-70.655,60.026,-70.62,59.984,-70.467,59.971,-70.327,59.971,-69.806,59.945,-69.734,59.918,-69.673,59.871,-69.63,59.822,-69.587,59.722,-69.579,59.675,-69.602,59.623,-69.656,59.565,-69.692,59.488,-69.711,59.393,-69.682,59.342,-69.4,59.338,-69.344,59.303,-69.35,59.277,-69.45,59.18,-69.46,59.152,-69.414,59.087,-69.42,59.068,-69.448,59.049,-69.475,59,-69.5,58.921,-69.532,58.869,-69.608,58.829,-69.648,58.821,-69.677,58.831,-69.753,58.94,-69.784,58.956,-69.814,58.946,-69.829,58.929,-69.829,58.905,-69.842,58.881,-69.868,58.856,-69.979,58.816,-70.16,58.789,-70.154,58.761,-70.033,58.745,-69.879,58.697,-69.79,58.689,-69.651,58.728,-69.382,58.851,-69.271,58.884,-69.173,58.897,-69.064,58.898,-68.942,58.889,-68.698,58.905,-68.637,58.893,-68.563,58.866,-68.475,58.823,-68.414,58.783,-68.381,58.744,-68.326,58.595,-68.253,58.557,-68.235,58.528,-68.229,58.485,-68.234,58.399,-68.315,58.227,-68.357,58.163,-68.468,58.076,-68.597,58.037,-68.826,58,-68.945,57.969,-69.035,57.926,-69.041,57.902,-68.781,57.976,-68.495,58.012,-68.414,58.052,-68.352,58.091,-68.289,58.178,-68.176,58.403,-68.111,58.473,-68.021,58.485,-67.981,58.461,-67.888,58.329,-67.888,58.296,-67.911,58.267,-68.064,58.139,-68.009,58.152,-67.856,58.273,-67.823,58.31,-67.805,58.365,-67.756,58.405,-67.737,58.385,-67.69,58.244,-67.688,58.14,-67.681,58.107,-67.698,58.009,-67.678,57.991,-67.632,58.076,-67.617,58.14,-67.596,58.186,-67.57,58.213,-67.382,58.3,-67.163,58.37,-67.019,58.433,-66.9,58.463,-66.722,58.491,-66.608,58.549,-66.558,58.637,-66.515,58.697,-66.48,58.731,-66.362,58.791,-66.299,58.795,-66.237,58.772,-66.168,58.727,-66.091,58.659,-66.045,58.606,-66.03,58.567,-66.017,58.431,-66.002,58.431,-65.931,58.535,-65.923,58.572,-65.928,58.611,-65.95,58.65,-66.021,58.735,-66.049,58.788,-66.043,58.821,-65.967,58.839,-65.855,58.847,-65.836,58.86,-65.918,58.896,-65.921,58.915,-65.841,58.977,-65.795,58.98,-65.704,58.971,-65.722,59.003,-65.721,59.024,-65.695,59.032,-65.544,59.012,-65.526,59.036,-65.396,59.038,-65.384,59.06,-65.496,59.091,-65.606,59.111,-65.64,59.128,-65.666,59.153,-65.7,59.213,-65.692,59.229,-65.661,59.23,-65.607,59.213,-65.578,59.245,-65.545,59.32,-65.513,59.35,-65.412,59.315,-65.407,59.33,-65.489,59.448,-65.475,59.47,-65.35,59.479,-65.274,59.464,-65.074,59.378,-65.038,59.388,-65.069,59.411,-65.171,59.462,-65.263,59.495,-65.346,59.511,-65.407,59.539,-65.475,59.617,-65.486,59.649,-65.481,59.69,-65.433,59.777,-65.406,59.795,-65.358,59.809,-65.289,59.818,-65.212,59.81,-65.054,59.753,-65.028,59.771,-65.113,59.802,-65.159,59.83,-65.181,59.867,-65.172,59.908,-65.105,59.993,-65.073,60.062,-64.931,60.252,-64.89,60.287,-64.845,60.308,-64.817,60.331,-64.706,60.336,-64.499,60.268,-64.436,60.228,-64.42,60.171,-64.528,60.095,-64.713,60.037,-64.768,60.012,-64.733,59.998,-64.559,60.043,-64.408,60.065,-64.283,60.064,-64.183,59.973,-64.169,59.847,-64.226,59.741,-64.151,59.794,-64.056,59.823,-63.979,59.754,-63.969,59.698,-63.929,59.645,-63.841,59.574,-63.75,59.513,-63.85,59.448,-63.971,59.409,-63.945,59.38,-63.781,59.349,-63.759,59.319,-63.776,59.277,-63.752,59.277,-63.637,59.341,-63.54,59.333,-63.415,59.194,-63.506,59.115,-63.646,59.079,-63.756,59.063,-63.91,59.066,-63.971,59.054,-63.941,59.027,-63.794,59.027,-63.568,59.047,-63.399,59.08,-63.326,59.082,-63.248,59.068,-63.223,59.057,-63.304,59.034,-63.31,59.026,-63.279,59.003,-63.216,58.928,-63.222,58.911,-63.282,58.867,-63.185,58.858,-63.05,58.878,-63.008,58.855,-62.926,58.765,-62.874,58.672,-63.102,58.546,-63.219,58.52,-63.39,58.453,-63.438,58.399,-63.537,58.33,-63.474,58.331,-63.296,58.441,-63.21,58.467,-63.146,58.46,-63.12,58.442,-63.132,58.411,-63.076,58.415,-62.837,58.479,-62.737,58.492,-62.608,58.496,-62.594,58.474,-62.674,58.319,-62.812,58.2,-63.063,58.127,-63.152,58.084,-63.262,58.015,-63.22,58.002,-62.981,58.093,-62.818,58.129,-62.588,58.158,-62.486,58.154,-62.306,57.972,-62.202,57.955,-62.117,57.964,-61.959,57.912,-61.899,57.861,-61.914,57.825,-61.968,57.803,-61.995,57.769,-61.931,57.669,-61.968,57.612,-62.084,57.562,-62.167,57.537,-62.254,57.529,-62.339,57.485,-62.377,57.478,-62.496,57.489,-62.455,57.462,-62.396,57.448,-62.303,57.441,-62.194,57.455,-62.088,57.453,-61.921,57.421,-61.851,57.381,-61.85,57.37,-61.886,57.348,-61.939,57.274,-61.977,57.248,-61.945,57.228,-61.861,57.198,-61.798,57.186,-61.716,57.196,-61.629,57.183,-61.334,57.011,-61.346,56.922,-61.39,56.853,-61.373,56.776,-61.372,56.681,-61.532,56.655,-62.062,56.699,-62.366,56.767,-62.382,56.788,-62.296,56.833,-62.372,56.836,-62.46,56.818,-62.497,56.802,-62.396,56.73,-62.117,56.667,-61.992,56.591,-61.855,56.584,-61.813,56.571,-61.738,56.526,-61.76,56.511,-61.899,56.505,-62.01,56.454,-61.94,56.424,-61.692,56.397,-61.515,56.39,-61.425,56.361,-61.499,56.328,-61.707,56.289,-61.713,56.231,-61.559,56.208,-61.421,56.222,-61.365,56.216,-61.324,56.076,-61.301,56.047,-61.449,56.022,-61.45,55.996,-61.351,55.974,-61.188,55.955,-61.134,55.93,-61.123,55.889,-61.089,55.866,-60.996,55.862,-60.893,55.914,-60.832,55.958,-60.743,55.941,-60.737,55.887,-60.631,55.825,-60.593,55.815,-60.562,55.727,-60.476,55.805,-60.413,55.789,-60.341,55.785,-60.365,55.709,-60.408,55.65,-60.352,55.612,-60.308,55.557,-60.192,55.481,-60.224,55.444,-60.361,55.366,-60.433,55.243,-60.45,55.2,-60.521,55.129,-60.617,55.06,-60.557,55.067,-60.341,55.194,-60.213,55.236,-59.93,55.259,-59.862,55.295,-59.759,55.31,-59.696,55.269,-59.689,55.196,-59.605,55.173,-59.518,55.197,-59.438,55.176,-59.486,55.13,-59.742,54.943,-59.816,54.867,-59.838,54.814,-59.75,54.887,-59.429,55.056,-59.394,55.081,-59.324,55.153,-59.26,55.2,-59.086,55.183,-58.997,55.149,-58.956,55.055,-58.886,54.952,-58.78,54.838,-58.5,54.783,-58.398,54.774,-58.223,54.813,-58.195,54.866,-58.058,54.882,-57.962,54.876,-57.929,54.773,-57.827,54.719,-57.725,54.674,-57.627,54.65,-57.483,54.64,-57.404,54.591,-57.404,54.57,-57.485,54.517,-57.563,54.44,-57.699,54.387,-57.889,54.384,-58.151,54.35,-58.162,54.32,-58.22,54.286,-58.359,54.253,-58.435,54.228,-58.558,54.103,-58.633,54.05,-58.719,54.039,-58.841,54.044,-58.92,54.033,-58.978,54.01,-59.013,53.976,-59.039,53.964,-59.201,53.929,-59.497,53.834,-59.653,53.831,-59.749,53.842,-59.823,53.834,-59.873,53.808,-60.014,53.762,-60.057,53.733,-60.081,53.701,-60.1,53.634,-60.117,53.61,-60.145,53.596,-60.263,53.61,-60.395,53.653,-60.37,53.607,-60.16,53.53,-60.1,53.487,-60.157,53.45,-60.29,53.391,-60.306,53.36,-60.251,53.344,-60.273,53.317,-60.346,53.289,-60.337,53.277,-60.329,53.266,-60.148,53.307,-59.987,53.393,-59.882,53.48,-59.829,53.505,-59.621,53.537,-59.498,53.575,-59.322,53.644,-59.129,53.744,-58.92,53.875,-58.652,53.978,-58.327,54.052,-58.088,54.09,-57.936,54.091,-57.928,54.104,-58.065,54.127,-58.177,54.131,-58.317,54.114,-58.361,54.154,-58.356,54.172,-58.31,54.202,-58.192,54.228,-57.615,54.191,-57.416,54.163,-57.199,53.924,-57.149,53.848,-57.135,53.792,-57.157,53.757,-57.244,53.715,-57.489,53.633,-57.524,53.611,-57.527,53.6,-57.42,53.583,-57.386,53.561,-57.332,53.469,-57.221,53.529,-57.012,53.673,-56.841,53.739,-56.697,53.758,-56.524,53.766,-56.465,53.765,-56.444,53.718,-56.354,53.624,-56.27,53.6,-56.11,53.588,-55.966,53.471,-55.911,53.391,-55.859,53.344,-55.863,53.312,-55.855,53.286,-55.817,53.246,-55.798,53.212,-55.808,53.135,-55.892,53,-55.83,52.878,-55.858,52.823,-55.873,52.736,-55.819,52.677,-55.803,52.643,-55.848,52.623,-56.167,52.575,-56.292,52.574,-56.325,52.545,-56.228,52.536,-56.053,52.537,-55.84,52.508,-55.746,52.475,-55.706,52.428,-55.716,52.392,-55.777,52.364,-55.897,52.37,-56.012,52.394,-56.005,52.37,-55.834,52.31,-55.783,52.28,-55.691,52.242,-55.673,52.19,-55.695,52.138,-56.017,51.929,-56.283,51.797,-56.549,51.681,-56.976,51.458,-57.018,51.447,-57.096,51.443,-57.299,51.478,-57.462,51.469,-57.77,51.426,-57.854,51.4,-58.023,51.322,-58.089,51.311,-58.27,51.295,-58.442,51.306,-58.51,51.295,-58.593,51.257,-58.615,51.237,-58.638,51.172,-59.055,50.879,-59.165,50.78,-59.378,50.675,-59.612,50.492,-59.815,50.418,-59.886,50.316,-60.08,50.255,-60.438,50.239,-60.608,50.221,-60.807,50.25,-60.956,50.205,-61.181,50.192,-61.29,50.202,-61.725,50.104,-61.835,50.197,-61.92,50.233,-62.165,50.239,-62.362,50.277,-62.541,50.285,-62.715,50.302,-62.83,50.301,-62.95,50.291,-63.136,50.294,-63.239,50.243,-63.587,50.258,-63.734,50.305,-63.854,50.314,-64.016,50.304,-64.17,50.269,-64.509,50.309,-64.868,50.275,-65.181,50.298,-65.269,50.32,-65.762,50.259,-65.955,50.294,-66.126,50.201,-66.242,50.22,-66.369,50.207,-66.411,50.224,-66.496,50.212,-66.55,50.161,-66.622,50.155,-66.741,50.066,-66.941,49.994,-67.234,49.602,-67.262,49.451,-67.372,49.348,-67.469,49.335,-67.549,49.332,-68.056,49.257,-68.282,49.197,-68.221,49.15,-68.295,49.114,-68.414,49.1,-68.544,49.056,-68.628,49.007,-68.669,48.94,-68.929,48.829,-69.231,48.574,-69.375,48.386,-69.55,48.251,-69.674,48.199,-69.762,48.191,-69.852,48.207,-70.001,48.271,-70.111,48.278,-70.384,48.367,-71.018,48.456,-70.923,48.422,-70.839,48.367,-70.671,48.353,-70.501,48.354,-70.145,48.244,-69.971,48.206,-69.866,48.172,-69.775,48.098,-69.84,47.953,-69.906,47.832,-69.994,47.74,-70.3,47.503,-70.448,47.423,-70.706,47.14,-70.973,47.007,-71.116,46.925,-71.268,46.796,-71.625,46.698,-71.757,46.674,-71.88,46.687,-72.028,46.607,-72.205,46.559,-72.257,46.485,-72.68,46.287,-72.843,46.262,-72.981,46.21,-73.022,46.12,-73.145,46.066,-73.18,46.025,-73.284,45.9,-73.477,45.738,-73.712,45.711,-73.798,45.655,-73.897,45.564,-74.038,45.502,-74.315,45.531,-74.248,45.493,-74,45.433,-73.974,45.345,-74.098,45.324,-74.358,45.206,-74.709,45.004,-74.762,44.999,-74.857,45.004,-74.996,44.97,-75.179,44.899,-75.401,44.772,-75.792,44.497,-75.819,44.468,-75.876,44.417,-76.02,44.363,-76.151,44.304,-76.186,44.242,-76.249,44.214,-76.465,44.058,-76.586,43.924,-76.696,43.785,-76.82,43.629,-77.073,43.627,-77.267,43.627,-77.597,43.629,-77.879,43.63,-78.215,43.631,-78.458,43.631,-78.72,43.625,-78.846,43.583,-79.002,43.527,-79.172,43.467,-79.083,43.331,-79.059,43.278,-79.066,43.106,-79.048,43.087,-79.029,43.062,-79.026,43.017,-79.012,42.997,-78.981,42.981,-78.946,42.961,-78.921,42.935,-78.915,42.909,-78.939,42.864,-79.037,42.802,-79.174,42.749,-79.446,42.651,-79.762,42.539,-80.036,42.441,-80.248,42.366,-80.683,42.3,-81.028,42.247,-81.278,42.209,-81.507,42.103,-81.761,41.987,-81.974,41.889,-82.213,41.779,-82.439,41.675,-82.69,41.675,-82.866,41.753,-83.03,41.833,-83.142,41.976,-83.15,42.142,-83.11,42.251,-83.073,42.3,-83.004,42.332,-82.868,42.385,-82.744,42.493,-82.645,42.558,-82.545,42.625,-82.488,42.74,-82.417,43.017,-82.408,43.073,-82.305,43.263,-82.19,43.474,-82.138,43.571,-82.197,43.822,-82.241,44.015,-82.281,44.192,-82.327,44.392,-82.368,44.573,-82.407,44.744,-82.447,44.916,-82.485,45.084,-82.515,45.204,-82.551,45.347,-82.76,45.448,-82.919,45.518,-83.179,45.633,-83.397,45.729,-83.593,45.817,-83.469,45.995,-83.48,46.024,-83.525,46.059,-83.616,46.117,-83.669,46.123,-83.763,46.109,-83.913,46.073,-83.978,46.085,-84.029,46.147,-84.088,46.227,-84.108,46.289,-84.115,46.371,-84.15,46.445,-84.128,46.484,-84.123,46.503,-84.125,46.527,-84.149,46.543,-84.192,46.55,-84.337,46.519,-84.402,46.516,-84.44,46.498,-84.502,46.462,-84.562,46.457,-84.666,46.543,-84.779,46.637,-84.827,46.767,-84.876,46.9,-85.07,46.98,-85.264,47.06,-85.458,47.14,-85.652,47.22,-85.846,47.3,-86.04,47.38,-86.234,47.46,-86.429,47.54,-86.496,47.567,-86.672,47.636,-86.922,47.735,-87.208,47.848,-87.494,47.962,-87.744,48.061,-87.921,48.13,-87.987,48.157,-88.161,48.225,-88.378,48.303,-88.612,48.264,-88.899,48.156,-89.063,48.094,-89.186,48.047,-89.273,48.02,-89.456,47.996,-89.551,48,-89.775,48.015,-89.901,47.995,-89.994,48.015,-90.04,48.078,-90.092,48.118,-90.32,48.099,-90.607,48.113,-90.744,48.105,-90.797,48.131,-90.84,48.201,-90.916,48.209,-91.043,48.194,-91.221,48.105,-91.387,48.059,-91.518,48.058,-91.647,48.105,-91.858,48.198,-92.005,48.302,-92.172,48.338,-92.299,48.329,-92.348,48.277,-92.415,48.277,-92.461,48.366,-92.501,48.435,-92.583,48.465,-92.733,48.532,-92.837,48.568,-92.996,48.612,-93.052,48.62,-93.155,48.625,-93.258,48.629,-93.378,48.617,-93.464,48.561,-93.564,48.537,-93.708,48.525,-93.804,48.549,-93.852,48.607,-94.055,48.659,-94.414,48.704,-94.621,48.743,-94.675,48.774,-94.705,48.808,-94.713,48.863,-94.803,49.003,-94.843,49.119,-94.86,49.259,-94.854,49.305,-94.875,49.319,-94.939,49.349,-95.155,49.37,-95.158,49.203,-95.162,48.992,-95.398,48.993,-95.824,48.993,-96.251,48.993,-96.677,48.993,-97.103,48.993,-97.53,48.993,-97.956,48.993,-98.383,48.993,-98.809,48.993,-99.235,48.993,-99.662,48.993,-100.088,48.993,-100.515,48.993,-100.941,48.993,-101.367,48.993,-101.794,48.993,-102.22,48.993,-102.646,48.993,-103.073,48.993,-103.499,48.993,-103.926,48.993,-104.352,48.993,-104.778,48.993,-105.205,48.993,-105.631,48.993,-106.057,48.993,-106.484,48.993,-106.91,48.993,-107.337,48.993,-107.763,48.993,-108.189,48.993,-108.616,48.993,-109.042,48.993,-109.469,48.993,-109.895,48.993,-110.321,48.993,-110.748,48.993,-111.174,48.993,-111.6,48.993,-112.027,48.993,-112.453,48.993,-112.88,48.993,-113.306,48.993,-113.732,48.993,-114.159,48.993,-114.585,48.993,-115.012,48.993,-115.438,48.993,-115.864,48.993,-116.291,48.993,-116.717,48.993,-117.143,48.993,-117.57,48.993,-117.996,48.993,-118.423,48.993,-118.849,48.993,-119.275,48.993,-119.702,48.993,-120.128,48.993,-120.554,48.993,-120.981,48.993,-121.407,48.993,-121.834,48.993,-122.26,48.993,-122.686,48.993,-122.789,48.993,-122.827,49.028,-122.924,49.075,-122.963,49.075,-123.002,49.061,-123.027,49.039,-123.049,48.993,-123.063,48.978,-123.077,48.98,-123.086,48.993,-123.118,49.056,-123.109,49.085,-123.077,49.118,-123.08,49.131,-123.15,49.121,-123.182,49.129,-123.196,49.148,-123.191,49.22,-123.229,49.26,-123.184,49.278,-123.067,49.292,-122.948,49.293,-122.913,49.323,-122.879,49.399,-122.964,49.329,-123.016,49.322,-123.174,49.348,-123.277,49.344,-123.291,49.359,-123.286,49.375,-123.264,49.39,-123.248,49.443,-123.223,49.59,-123.191,49.644,-123.18,49.674,-123.187,49.68,-123.325,49.578,-123.337,49.545,-123.322,49.517,-123.336,49.459,-123.399,49.442,-123.437,49.451,-123.508,49.402,-123.531,49.397,-123.859,49.483,-123.892,49.495,-123.948,49.535,-124.029,49.603,-124.054,49.662,-124.024,49.711,-123.993,49.736,-123.96,49.736,-123.923,49.718,-123.847,49.637,-123.817,49.587,-123.739,49.594,-123.613,49.658,-123.582,49.681,-123.708,49.657,-123.763,49.658,-123.818,49.685,-123.874,49.737,-123.904,49.795,-123.904,49.981,-123.885,50.017,-123.824,50.044,-123.785,50.088,-123.788,50.107,-123.825,50.144,-123.88,50.174,-123.934,50.188,-123.946,50.184,-123.863,50.103,-123.866,50.072,-123.957,49.993,-123.971,49.97,-123.972,49.892,-123.985,49.876,-124.059,49.854,-124.142,49.793,-124.281,49.772,-124.413,49.778,-124.483,49.808,-124.702,49.958,-124.782,50.02,-124.784,50.073,-124.934,50.258,-124.933,50.298,-124.986,50.356,-125.044,50.364,-125.057,50.419,-124.937,50.537,-124.863,50.637,-124.854,50.669,-124.858,50.717,-124.875,50.826,-124.86,50.872,-124.934,50.811,-124.949,50.765,-124.931,50.718,-124.943,50.666,-124.985,50.592,-125.059,50.514,-125.21,50.476,-125.476,50.497,-125.507,50.507,-125.526,50.534,-125.539,50.649,-125.556,50.635,-125.586,50.574,-125.61,50.486,-125.641,50.466,-125.698,50.465,-125.741,50.479,-125.772,50.508,-125.84,50.511,-125.965,50.487,-126.024,50.497,-126.094,50.498,-126.237,50.523,-126.404,50.53,-126.45,50.55,-126.447,50.588,-126.416,50.607,-126.239,50.624,-126.067,50.664,-125.898,50.684,-125.904,50.705,-125.981,50.711,-126.37,50.667,-126.493,50.672,-126.514,50.679,-126.517,50.724,-126.472,50.767,-126.397,50.807,-126.375,50.837,-126.418,50.85,-126.488,50.842,-126.522,50.866,-126.485,50.96,-126.517,51.057,-126.563,50.965,-126.632,50.915,-126.96,50.894,-127.014,50.867,-127.058,50.868,-127.267,50.916,-127.357,50.946,-127.441,50.989,-127.591,51.088,-127.708,51.151,-127.714,51.269,-127.689,51.343,-127.633,51.427,-127.42,51.608,-127.347,51.642,-127.281,51.654,-126.968,51.67,-126.735,51.693,-126.691,51.703,-127.034,51.717,-127.339,51.707,-127.443,51.679,-127.576,51.563,-127.61,51.514,-127.645,51.478,-127.669,51.478,-127.714,51.49,-127.729,51.506,-127.748,51.544,-127.819,51.604,-127.851,51.673,-127.869,51.775,-127.863,51.821,-127.83,51.879,-127.728,51.993,-127.859,51.99,-127.843,52.086,-127.795,52.191,-127.673,52.253,-127.55,52.298,-127.438,52.356,-127.242,52.395,-127.176,52.315,-127.008,52.291,-126.959,52.255,-126.9,52.188,-126.826,52.125,-126.739,52.065,-126.714,52.061,-126.753,52.112,-126.895,52.225,-126.901,52.265,-126.938,52.309,-127.127,52.371,-127.161,52.395,-127.194,52.458,-127.208,52.498,-127.187,52.538,-126.995,52.658,-126.951,52.721,-126.951,52.751,-126.966,52.785,-127.008,52.843,-127.019,52.842,-127.006,52.755,-127.013,52.72,-127.035,52.682,-127.066,52.653,-127.107,52.633,-127.519,52.359,-127.56,52.343,-127.713,52.319,-127.792,52.289,-127.834,52.251,-127.902,52.151,-127.995,51.951,-128.102,51.788,-128.194,51.998,-128.358,52.159,-128.037,52.318,-128.029,52.342,-128.06,52.428,-128.052,52.453,-128.021,52.491,-127.94,52.545,-127.943,52.551,-128.038,52.531,-128.184,52.408,-128.241,52.368,-128.272,52.363,-128.275,52.435,-128.197,52.623,-128.132,52.806,-128.109,52.858,-128.053,52.911,-128.106,52.907,-128.365,52.826,-128.452,52.877,-128.525,53.141,-128.652,53.244,-128.869,53.328,-129.081,53.367,-129.13,53.442,-129.172,53.534,-129.114,53.641,-129.021,53.692,-128.936,53.715,-128.855,53.705,-128.85,53.665,-128.906,53.559,-128.833,53.549,-128.542,53.421,-128.479,53.41,-128.358,53.46,-128.291,53.458,-128.133,53.418,-128.079,53.369,-127.928,53.275,-127.95,53.33,-128.115,53.446,-128.207,53.483,-128.369,53.49,-128.47,53.471,-128.512,53.477,-128.6,53.506,-128.676,53.555,-128.751,53.661,-128.768,53.71,-128.764,53.747,-128.746,53.78,-128.715,53.81,-128.653,53.832,-128.56,53.845,-128.532,53.858,-128.651,53.919,-128.705,53.919,-128.89,53.83,-128.928,53.823,-128.944,53.84,-128.959,53.841,-129.014,53.797,-129.056,53.778,-129.208,53.642,-129.232,53.576,-129.24,53.479,-129.258,53.418,-129.284,53.393,-129.462,53.347,-129.564,53.251,-129.687,53.334,-129.822,53.413,-129.912,53.551,-130.074,53.576,-130.263,53.654,-130.335,53.724,-130.233,53.867,-130.086,53.976,-130.064,54.106,-130.043,54.134,-129.791,54.166,-129.626,54.23,-129.795,54.236,-129.898,54.226,-130.084,54.181,-130.29,54.27,-130.397,54.352,-130.43,54.421,-130.393,54.48,-130.389,54.539,-130.37,54.62,-130.35,54.655,-130.307,54.7,-130.219,54.73,-130.141,54.823,-130.109,54.887,-129.949,55.081,-129.89,55.165,-129.781,55.28,-129.561,55.463,-129.63,55.452,-129.667,55.437,-129.701,55.439,-129.734,55.458,-129.765,55.498,-129.795,55.56,-129.812,55.533,-129.816,55.418,-129.838,55.319,-129.877,55.251,-129.985,55.111,-130.048,55.057,-130.092,55.108,-130.058,55.195,-129.996,55.264,-129.985,55.359,-130.044,55.472,-130.08,55.563,-130.093,55.632,-130.095,55.695,-130.085,55.752,-130.06,55.814,-130.02,55.881,-130.025,55.888]],[[-109.816,78.65,-109.641,78.592,-109.581,78.593,-109.504,78.582,-109.467,78.567,-109.362,78.493,-109.342,78.456,-109.336,78.408,-109.352,78.369,-109.391,78.337,-109.484,78.316,-109.709,78.304,-110.022,78.323,-110.293,78.298,-110.418,78.295,-110.755,78.311,-110.84,78.322,-111.027,78.368,-111.169,78.386,-111.229,78.376,-111.3,78.337,-111.435,78.287,-111.517,78.275,-111.76,78.283,-112.131,78.366,-112.558,78.342,-113,78.293,-113.173,78.284,-113.223,78.298,-113.293,78.334,-113.282,78.353,-113.15,78.408,-112.856,78.467,-112.641,78.5,-112.214,78.548,-111.709,78.575,-111.52,78.603,-111.4,78.644,-111.071,78.708,-110.878,78.735,-110.618,78.758,-110.408,78.757,-110.14,78.704,-109.941,78.678,-109.816,78.65]],[[-110.458,78.103,-109.657,78.079,-109.622,78.075,-109.619,78.057,-109.679,77.999,-109.772,77.957,-110.2,77.905,-110.751,77.857,-110.866,77.834,-110.857,77.82,-110.812,77.803,-110.719,77.781,-110.292,77.786,-110.189,77.777,-110.153,77.763,-110.131,77.742,-110.118,77.716,-110.117,77.625,-110.139,77.572,-110.198,77.525,-110.372,77.491,-110.683,77.446,-110.894,77.426,-111.06,77.433,-111.226,77.429,-111.952,77.344,-112.177,77.344,-112.373,77.364,-112.644,77.444,-112.926,77.475,-113.046,77.511,-113.164,77.53,-113.197,77.559,-113.209,77.58,-113.189,77.6,-113.137,77.618,-113.121,77.633,-113.168,77.676,-113.19,77.718,-113.271,77.778,-113.283,77.813,-113.283,77.836,-113.269,77.86,-113.215,77.904,-113.187,77.912,-113.022,77.919,-112.805,77.942,-112.305,78.007,-111.207,78.088,-110.873,78.081,-110.727,78.097,-110.458,78.103]],[[-115.551,77.363,-115.476,77.324,-115.47,77.309,-115.507,77.292,-115.624,77.266,-116.214,77.178,-116.329,77.137,-116.286,77.102,-116.073,77.03,-115.857,76.969,-115.81,76.939,-115.913,76.908,-116.11,76.918,-116.183,76.916,-116.253,76.901,-116.234,76.874,-116.016,76.785,-115.945,76.736,-115.946,76.711,-115.985,76.687,-116.076,76.654,-116.22,76.611,-116.468,76.577,-116.999,76.532,-117.017,76.496,-117.013,76.469,-117.026,76.404,-117.044,76.373,-117.108,76.322,-117.154,76.298,-117.234,76.282,-117.347,76.273,-117.492,76.273,-117.732,76.317,-117.841,76.345,-117.993,76.406,-118.02,76.447,-118.005,76.497,-117.965,76.574,-117.9,76.653,-117.808,76.734,-117.78,76.784,-117.818,76.804,-117.881,76.805,-118.076,76.772,-118.203,76.76,-118.301,76.737,-118.37,76.701,-118.409,76.662,-118.431,76.588,-118.468,76.547,-118.574,76.525,-118.732,76.526,-118.791,76.513,-118.821,76.486,-118.8,76.464,-118.644,76.418,-118.625,76.366,-118.643,76.335,-118.812,76.277,-118.851,76.258,-118.955,76.168,-118.994,76.145,-119.081,76.124,-119.168,76.127,-119.249,76.159,-119.368,76.222,-119.448,76.275,-119.489,76.32,-119.524,76.34,-119.58,76.327,-119.649,76.28,-119.651,76.244,-119.636,76.19,-119.64,76.157,-119.74,76.118,-119.725,76.1,-119.55,76.052,-119.527,76.031,-119.526,75.997,-119.538,75.982,-119.608,75.985,-119.667,75.946,-119.735,75.915,-119.913,75.859,-120.161,75.852,-120.365,75.825,-120.409,75.826,-120.458,75.87,-120.514,75.958,-120.563,76.008,-120.637,76.034,-120.729,76.134,-120.772,76.166,-120.813,76.179,-120.848,76.183,-120.9,76.163,-121.019,76.02,-121.213,75.984,-121.32,75.977,-121.428,75.981,-121.695,76.02,-121.908,76.035,-122.057,76.018,-122.303,75.96,-122.4,75.944,-122.533,75.951,-122.591,75.973,-122.64,76.009,-122.646,76.031,-122.607,76.039,-122.546,76.081,-122.548,76.097,-122.609,76.121,-122.609,76.14,-122.588,76.153,-122.593,76.162,-122.624,76.167,-122.685,76.162,-122.903,76.135,-122.878,76.165,-122.774,76.228,-122.519,76.353,-122.423,76.39,-122.365,76.401,-121.614,76.441,-121.561,76.453,-121.204,76.622,-121.102,76.661,-120.998,76.691,-120.485,76.793,-120.438,76.816,-120.358,76.887,-120.311,76.905,-120.2,76.931,-119.831,77.074,-119.495,77.177,-119.324,77.241,-119.09,77.305,-118.82,77.333,-118.005,77.381,-117.419,77.317,-117.279,77.313,-117.211,77.331,-117.149,77.361,-117.061,77.348,-116.844,77.34,-116.795,77.347,-116.704,77.38,-116.766,77.398,-117.03,77.432,-117.046,77.449,-117.04,77.465,-116.947,77.504,-116.835,77.529,-116.511,77.548,-116.363,77.543,-116.209,77.516,-116.008,77.461,-115.551,77.363]],[[-108.292,76.057,-108.166,76.054,-108.019,76.065,-107.852,76.058,-107.777,76.035,-107.723,75.995,-107.721,75.974,-107.732,75.956,-107.755,75.94,-107.97,75.84,-108.021,75.805,-107.951,75.796,-107.918,75.802,-107.703,75.878,-107.541,75.901,-107.418,75.907,-107.216,75.892,-107.136,75.879,-107.08,75.863,-107.05,75.845,-106.971,75.773,-106.914,75.68,-106.904,75.689,-106.903,75.742,-106.892,75.782,-106.693,75.81,-106.688,75.819,-106.759,75.842,-106.82,75.872,-106.862,75.93,-106.846,75.952,-106.804,75.975,-106.677,76.024,-106.529,76.053,-106.397,76.06,-105.905,76.009,-105.711,75.967,-105.633,75.945,-105.604,75.93,-105.563,75.881,-105.481,75.746,-105.481,75.702,-105.519,75.632,-105.678,75.501,-105.703,75.413,-105.863,75.192,-105.972,75.131,-106.093,75.089,-106.588,75.015,-106.961,74.94,-107.056,74.928,-107.153,74.927,-107.462,74.952,-107.82,75,-108.024,74.986,-108.227,74.952,-108.354,74.943,-108.475,74.947,-108.594,74.96,-108.751,74.992,-108.67,75.007,-108.633,75.023,-108.666,75.04,-108.831,75.065,-109.003,75.01,-109.503,74.883,-110.176,74.84,-110.387,74.814,-110.543,74.78,-110.625,74.753,-110.749,74.688,-110.941,74.639,-111.288,74.585,-111.729,74.502,-112.519,74.417,-113.017,74.402,-113.514,74.43,-113.672,74.453,-113.837,74.489,-114.175,74.574,-114.268,74.604,-114.377,74.671,-114.313,74.715,-114.132,74.766,-113.863,74.813,-113.324,74.875,-112.836,74.976,-112.663,74.994,-112.193,75.01,-111.956,75,-111.784,75.006,-111.671,75.019,-111.503,75.056,-111.258,75.128,-111.079,75.195,-111.033,75.227,-111.093,75.256,-111.181,75.26,-111.474,75.191,-111.621,75.168,-111.781,75.166,-112,75.142,-112.214,75.133,-112.256,75.134,-112.478,75.2,-112.597,75.212,-112.652,75.205,-112.703,75.187,-112.8,75.138,-112.855,75.121,-112.951,75.108,-113.34,75.093,-113.712,75.069,-113.795,75.084,-113.845,75.112,-113.855,75.129,-113.861,75.188,-113.886,75.211,-113.853,75.259,-113.811,75.296,-113.759,75.322,-113.503,75.397,-113.467,75.416,-113.589,75.412,-113.879,75.375,-113.916,75.388,-113.984,75.43,-114.017,75.434,-114.053,75.417,-114.075,75.392,-114.125,75.291,-114.168,75.24,-114.285,75.25,-114.429,75.281,-114.483,75.285,-114.514,75.275,-114.504,75.258,-114.358,75.171,-114.356,75.141,-114.452,75.088,-114.859,75,-115.02,74.976,-115.077,74.985,-115.128,75.009,-115.174,75.049,-115.28,75.102,-115.343,75.113,-115.413,75.115,-115.478,75.104,-115.537,75.081,-115.574,75.056,-115.609,75.01,-115.683,74.974,-115.729,74.968,-116.143,75.042,-116.476,75.172,-116.841,75.152,-117.005,75.156,-117.502,75.204,-117.565,75.233,-117.6,75.272,-117.597,75.293,-117.576,75.314,-117.513,75.357,-117.388,75.421,-117.336,75.442,-117.258,75.46,-117.154,75.473,-116.891,75.481,-116.213,75.483,-116.077,75.493,-115.335,75.618,-115.251,75.639,-115.142,75.679,-115.117,75.695,-115.122,75.706,-116.034,75.607,-116.426,75.585,-117.025,75.602,-117.138,75.617,-117.164,75.645,-117.039,75.718,-116.973,75.746,-116.802,75.772,-116.39,75.808,-115.838,75.841,-115.477,75.841,-115.174,75.867,-114.992,75.896,-115.602,75.895,-116.338,75.881,-116.444,75.891,-116.654,75.929,-116.665,75.958,-116.58,75.992,-116.55,76.017,-116.61,76.074,-116.591,76.096,-116.454,76.143,-116.21,76.194,-116.059,76.202,-115.768,76.184,-114.939,76.166,-114.779,76.173,-114.88,76.195,-115.025,76.211,-115.664,76.24,-115.797,76.253,-115.822,76.27,-115.832,76.296,-115.826,76.33,-115.779,76.365,-115.581,76.438,-114.998,76.497,-114.767,76.506,-114.535,76.502,-114.299,76.475,-114.194,76.451,-114.141,76.423,-114.116,76.396,-114.113,76.349,-114.101,76.331,-114.059,76.301,-113.923,76.229,-113.823,76.207,-113.363,76.248,-113.171,76.258,-112.978,76.245,-112.698,76.202,-112.334,76.072,-111.865,75.939,-111.868,75.911,-112.047,75.866,-112.081,75.847,-112.057,75.834,-111.877,75.826,-111.709,75.832,-111.549,75.822,-111.513,75.811,-111.454,75.762,-111.373,75.676,-111.276,75.613,-111.163,75.57,-111.053,75.549,-110.89,75.547,-110.726,75.56,-110.459,75.555,-109.086,75.506,-109.005,75.515,-108.947,75.542,-108.913,75.587,-108.9,75.624,-108.918,75.675,-108.945,75.699,-109.796,75.863,-109.871,75.929,-109.455,76.021,-109.425,76.043,-109.417,76.072,-109.43,76.109,-109.487,76.145,-109.71,76.212,-109.908,76.223,-110.201,76.289,-110.247,76.306,-110.285,76.333,-110.314,76.369,-110.309,76.397,-110.27,76.417,-109.982,76.485,-109.865,76.522,-109.505,76.692,-109.339,76.76,-109.22,76.792,-109.098,76.812,-108.832,76.821,-108.554,76.758,-108.492,76.754,-108.467,76.738,-108.478,76.708,-108.512,76.68,-108.612,76.63,-108.635,76.609,-108.628,76.587,-108.56,76.536,-108.539,76.503,-108.524,76.447,-108.512,76.439,-108.345,76.392,-108.194,76.33,-108.123,76.233,-108.178,76.2,-108.306,76.154,-108.382,76.116,-108.406,76.085,-108.387,76.067,-108.292,76.057]],[[-114.522,72.593,-114.458,72.58,-114.342,72.591,-114.174,72.624,-113.958,72.651,-113.692,72.673,-113.622,72.647,-113.578,72.652,-113.5,72.694,-113.486,72.722,-113.496,72.754,-113.491,72.822,-113.45,72.863,-113.292,72.95,-113.208,72.981,-113.074,72.995,-112.754,72.986,-112.454,72.937,-112.048,72.888,-111.455,72.766,-111.27,72.714,-111.25,72.669,-111.356,72.572,-111.611,72.436,-111.816,72.386,-111.895,72.356,-111.762,72.335,-111.675,72.3,-111.544,72.351,-111.447,72.408,-111.311,72.455,-111.265,72.459,-111.253,72.449,-111.277,72.425,-111.287,72.401,-111.284,72.378,-111.268,72.364,-111.185,72.357,-111.14,72.365,-110.959,72.432,-110.782,72.534,-110.513,72.6,-110.439,72.633,-110.205,72.661,-110.208,72.681,-110.197,72.759,-110.279,72.792,-110.554,72.861,-110.689,72.945,-110.661,73.008,-110.509,72.999,-110.095,72.992,-110.008,72.984,-109.61,72.876,-109.469,72.808,-109.357,72.775,-109.122,72.726,-109.043,72.687,-108.987,72.671,-108.968,72.654,-108.985,72.637,-108.994,72.596,-108.951,72.583,-108.798,72.568,-108.755,72.551,-108.698,72.499,-108.628,72.412,-108.566,72.317,-108.47,72.139,-108.276,71.9,-108.21,71.751,-108.188,71.724,-108.145,71.705,-108.021,71.677,-107.925,71.639,-107.813,71.626,-107.785,71.63,-107.757,71.663,-107.687,71.716,-107.347,71.819,-107.329,71.835,-107.369,71.859,-107.382,71.875,-107.377,71.886,-107.306,71.895,-107.543,72.025,-107.696,72.149,-107.794,72.303,-107.809,72.347,-107.824,72.443,-107.856,72.468,-107.91,72.491,-107.933,72.52,-107.924,72.557,-107.934,72.588,-107.997,72.653,-108.238,73.106,-108.237,73.15,-108.204,73.183,-108.118,73.202,-107.98,73.207,-107.936,73.217,-107.987,73.233,-108.077,73.281,-108.089,73.304,-108.029,73.349,-107.72,73.329,-107.496,73.288,-107.113,73.192,-107.074,73.197,-107.033,73.245,-106.951,73.276,-106.828,73.266,-106.482,73.196,-106.082,73.072,-105.813,73.011,-105.624,72.927,-105.496,72.849,-105.415,72.788,-105.412,72.765,-105.43,72.74,-105.411,72.709,-105.355,72.67,-105.323,72.635,-105.298,72.56,-105.247,72.464,-105.234,72.415,-104.878,71.98,-104.81,71.903,-104.767,71.868,-104.518,71.699,-104.386,71.577,-104.373,71.495,-104.355,71.472,-104.35,71.434,-104.356,71.382,-104.385,71.338,-104.437,71.3,-104.487,71.248,-104.563,71.132,-104.57,71.104,-104.515,71.064,-104.167,70.927,-103.953,70.763,-103.853,70.734,-103.585,70.631,-103.295,70.572,-103.197,70.547,-103.105,70.51,-103.077,70.509,-103.021,70.516,-103.005,70.526,-103.001,70.541,-103.083,70.619,-103.089,70.65,-103.05,70.655,-102.75,70.522,-102.589,70.469,-102.369,70.413,-101.99,70.285,-101.937,70.275,-101.732,70.286,-101.676,70.278,-101.641,70.266,-101.627,70.248,-101.618,70.172,-101.562,70.135,-101.239,70.151,-101.149,70.148,-101.091,70.136,-101.043,70.111,-100.973,70.029,-100.909,69.869,-100.906,69.812,-100.935,69.715,-100.982,69.68,-101.044,69.669,-101.216,69.68,-101.337,69.71,-101.4,69.749,-101.457,69.834,-101.484,69.85,-101.508,69.833,-101.565,69.756,-101.602,69.721,-101.648,69.699,-101.734,69.704,-101.86,69.738,-102.098,69.825,-102.182,69.846,-102.234,69.842,-102.348,69.813,-102.523,69.758,-102.596,69.718,-102.565,69.692,-102.545,69.66,-102.535,69.621,-102.541,69.592,-102.563,69.574,-102.621,69.552,-102.744,69.548,-102.92,69.565,-103.059,69.595,-103.303,69.674,-103.359,69.685,-103.435,69.668,-103.465,69.644,-103.418,69.611,-103.294,69.568,-103.142,69.497,-103.102,69.483,-103.063,69.485,-103.049,69.472,-103.032,69.433,-103.04,69.368,-103.113,69.236,-103.12,69.205,-103.09,69.212,-102.884,69.341,-102.777,69.378,-102.546,69.434,-102.447,69.476,-102.151,69.488,-102.046,69.465,-101.978,69.425,-101.976,69.407,-102.053,69.36,-102.067,69.337,-102.071,69.308,-102.064,69.281,-102.046,69.258,-101.993,69.236,-101.899,69.246,-101.873,69.24,-101.823,69.217,-101.789,69.182,-101.788,69.132,-101.857,69.024,-101.981,68.989,-102.359,68.923,-102.488,68.889,-102.738,68.865,-102.835,68.833,-102.895,68.824,-103.162,68.829,-103.468,68.809,-103.82,68.848,-104.067,68.866,-104.353,68.928,-104.46,68.912,-104.571,68.872,-105.106,68.92,-105.169,68.955,-105.148,68.978,-105.022,69.052,-105.014,69.068,-105.02,69.081,-105.262,69.094,-105.533,69.134,-105.805,69.153,-106.008,69.148,-106.141,69.162,-106.27,69.195,-106.341,69.224,-106.354,69.251,-106.356,69.281,-106.344,69.34,-106.361,69.381,-106.42,69.414,-106.54,69.443,-106.659,69.44,-106.76,69.407,-106.856,69.347,-107.033,69.181,-107.123,69.152,-107.353,69.032,-107.44,69.002,-107.863,68.954,-108.365,68.935,-108.553,68.897,-108.73,68.827,-108.946,68.76,-109.472,68.677,-109.959,68.63,-110.468,68.61,-110.848,68.578,-110.957,68.594,-111.128,68.588,-111.311,68.542,-111.518,68.533,-112.305,68.516,-112.666,68.485,-112.864,68.477,-113.02,68.481,-113.128,68.494,-113.231,68.535,-113.338,68.599,-113.555,68.768,-113.617,68.838,-113.593,68.96,-113.609,69.03,-113.681,69.182,-113.694,69.195,-114.073,69.251,-114.323,69.269,-114.699,69.273,-115.159,69.265,-115.618,69.283,-115.861,69.304,-116.102,69.337,-116.513,69.425,-116.537,69.434,-116.569,69.463,-116.609,69.512,-116.712,69.576,-116.993,69.719,-117.104,69.804,-117.122,69.826,-117.149,69.888,-117.184,69.991,-117.195,70.054,-117.163,70.092,-117.135,70.1,-116.554,70.175,-115.529,70.257,-114.592,70.312,-114.167,70.307,-113.917,70.282,-113.666,70.27,-113.211,70.264,-112.638,70.225,-112.523,70.229,-112.266,70.255,-112.19,70.276,-111.784,70.273,-111.705,70.286,-111.633,70.309,-111.726,70.352,-112.114,70.447,-113.146,70.616,-113.397,70.652,-113.757,70.691,-113.966,70.696,-114.232,70.674,-114.331,70.675,-114.593,70.642,-114.841,70.621,-115.311,70.601,-115.991,70.586,-116.086,70.591,-116.226,70.616,-116.327,70.624,-116.993,70.604,-117.587,70.63,-118.264,70.888,-118.377,70.968,-118.353,71,-118.269,71.035,-117.934,71.135,-117.814,71.158,-117.314,71.212,-116.815,71.277,-116.422,71.338,-116.228,71.359,-116.042,71.362,-115.892,71.382,-115.922,71.401,-116.045,71.423,-116.044,71.454,-115.98,71.469,-115.734,71.485,-115.472,71.466,-115.341,71.472,-115.303,71.494,-115.338,71.511,-115.587,71.546,-116.78,71.444,-117.337,71.435,-117.723,71.391,-117.936,71.392,-118.188,71.436,-118.222,71.449,-118.226,71.467,-118.148,71.526,-117.878,71.561,-117.742,71.659,-117.888,71.661,-118.372,71.64,-118.583,71.649,-118.868,71.687,-118.952,71.732,-118.988,71.764,-118.994,71.803,-118.984,71.913,-118.96,71.972,-118.945,71.986,-118.59,72.167,-118.369,72.205,-118.213,72.263,-118.207,72.287,-118.246,72.311,-118.39,72.37,-118.449,72.399,-118.481,72.428,-118.457,72.473,-118.375,72.534,-118.133,72.633,-117.552,72.831,-117.256,72.914,-116.972,72.959,-116.573,73.055,-115.552,73.213,-114.638,73.373,-114.302,73.331,-114.206,73.298,-114.164,73.27,-114.127,73.231,-114.095,73.18,-114.052,73.071,-114.046,73.015,-114.054,72.958,-114.075,72.907,-114.109,72.861,-114.178,72.805,-114.28,72.739,-114.498,72.626,-114.522,72.593]],[[-119.736,74.113,-119.729,74.108,-119.471,74.201,-119.315,74.206,-119.206,74.198,-119.171,74.186,-119.15,74.168,-119.139,74.128,-119.132,74.028,-119.118,74.016,-119.083,74.021,-119.026,74.045,-118.744,74.192,-118.625,74.233,-118.544,74.245,-118.2,74.267,-117.966,74.266,-117.707,74.252,-117.515,74.232,-117.199,74.171,-116.95,74.101,-116.722,74.027,-115.958,73.748,-115.634,73.666,-115.511,73.619,-115.456,73.585,-115.408,73.542,-115.393,73.502,-115.412,73.465,-115.447,73.439,-115.524,73.417,-115.992,73.323,-116.239,73.295,-116.483,73.253,-117.065,73.107,-117.464,73.038,-117.983,72.902,-118.962,72.684,-119.078,72.64,-119.132,72.609,-119.408,72.36,-119.513,72.303,-119.767,72.244,-120.09,72.229,-120.18,72.213,-120.194,72.127,-120.31,71.984,-120.366,71.888,-120.443,71.631,-120.461,71.605,-120.52,71.557,-120.619,71.506,-120.93,71.446,-121.16,71.415,-121.472,71.389,-121.547,71.407,-121.622,71.448,-121.701,71.451,-121.749,71.445,-122.157,71.266,-122.55,71.194,-122.72,71.128,-122.84,71.097,-122.937,71.088,-123.096,71.094,-123.211,71.123,-123.315,71.169,-123.393,71.219,-123.595,71.423,-123.682,71.493,-123.756,71.528,-123.953,71.652,-124.008,71.677,-124.76,71.835,-125.126,71.924,-125.215,71.955,-125.297,71.973,-125.767,71.961,-125.829,71.966,-125.845,71.979,-125.79,72.025,-125.768,72.054,-125.76,72.083,-125.769,72.129,-125.763,72.138,-125.584,72.183,-125.613,72.193,-125.634,72.21,-125.647,72.237,-125.627,72.255,-125.575,72.265,-125.512,72.308,-125.438,72.382,-125.383,72.424,-125.306,72.451,-125.168,72.523,-125.07,72.552,-124.987,72.588,-124.985,72.604,-125.019,72.617,-125.03,72.645,-125.015,72.731,-125.015,72.776,-125,72.813,-124.97,72.843,-124.931,72.863,-124.583,72.926,-124.565,72.944,-124.561,72.965,-124.57,72.989,-124.588,73.005,-124.643,73.019,-124.736,73.023,-124.817,73.059,-124.836,73.076,-124.804,73.126,-124.647,73.204,-124.594,73.243,-124.424,73.419,-124.114,73.527,-124.03,73.644,-123.797,73.768,-123.798,73.785,-123.873,73.828,-124.088,73.857,-124.192,73.902,-124.261,73.953,-124.575,74.248,-124.629,74.27,-124.645,74.304,-124.709,74.327,-124.696,74.348,-123.468,74.436,-122.623,74.464,-121.748,74.541,-121.504,74.545,-121.315,74.53,-121.129,74.49,-120.882,74.421,-120.554,74.353,-119.944,74.254,-119.563,74.233,-119.715,74.154,-119.737,74.13,-119.736,74.113]],[[-69.489,83.017,-68.673,82.999,-68.409,83.005,-68.107,82.961,-67.925,82.956,-67.624,82.964,-67.406,82.954,-66.592,82.944,-66.423,82.927,-66.425,82.906,-66.6,82.861,-66.836,82.818,-68.358,82.677,-68.469,82.653,-68.173,82.646,-67.736,82.652,-67.397,82.668,-66.998,82.716,-66.866,82.719,-66.612,82.742,-66.12,82.807,-65.727,82.842,-65.55,82.827,-65.4,82.802,-65.299,82.8,-65.247,82.819,-65.162,82.87,-65.113,82.889,-64.984,82.902,-64.905,82.901,-64.777,82.876,-64.635,82.819,-64.504,82.778,-64.433,82.778,-64.134,82.823,-63.984,82.829,-63.641,82.813,-63.499,82.793,-63.473,82.771,-63.564,82.749,-63.621,82.729,-63.643,82.713,-63.593,82.694,-63.385,82.653,-63.085,82.565,-63.087,82.533,-63.251,82.467,-63.247,82.45,-62.475,82.52,-61.697,82.489,-61.477,82.467,-61.392,82.442,-61.302,82.4,-61.207,82.341,-61.274,82.28,-61.615,82.184,-61.969,82.11,-62.177,82.043,-62.496,82.007,-63.592,81.846,-64.128,81.794,-64.436,81.743,-64.574,81.734,-65.226,81.744,-65.399,81.715,-65.495,81.668,-65.701,81.646,-66.005,81.629,-66.626,81.616,-66.765,81.563,-66.801,81.527,-66.861,81.499,-66.914,81.485,-68.689,81.293,-68.721,81.261,-68.543,81.248,-68.318,81.261,-65.736,81.494,-65.24,81.51,-64.78,81.493,-64.833,81.439,-65.484,81.285,-66.313,81.146,-66.727,81.041,-67.774,80.859,-68.63,80.679,-68.959,80.587,-69.4,80.423,-69.551,80.383,-69.734,80.367,-69.949,80.374,-70.144,80.398,-70.403,80.459,-70.639,80.528,-70.713,80.54,-70.668,80.506,-70.213,80.278,-70.265,80.234,-71.1,80.187,-71.47,80.146,-71.661,80.136,-71.796,80.143,-71.928,80.139,-72.056,80.123,-72.063,80.106,-71.949,80.086,-71.616,80.071,-70.877,80.122,-70.758,80.119,-70.568,80.094,-70.559,80.071,-70.758,79.998,-71.356,79.911,-71.278,79.906,-71.106,79.876,-71.11,79.848,-71.299,79.783,-71.388,79.762,-71.965,79.701,-72.216,79.687,-72.437,79.694,-73.448,79.827,-73.805,79.846,-74.144,79.88,-74.394,79.874,-74.66,79.835,-74.541,79.816,-74.051,79.778,-73.642,79.771,-73.472,79.756,-73.406,79.732,-73.229,79.644,-73.201,79.597,-73.24,79.552,-73.294,79.522,-73.362,79.504,-73.466,79.495,-73.866,79.501,-74.015,79.491,-74.189,79.465,-74.406,79.454,-74.798,79.459,-75.259,79.421,-75.503,79.414,-75.774,79.431,-76.067,79.473,-76.376,79.494,-76.899,79.512,-76.855,79.488,-76.671,79.478,-76.296,79.414,-76.116,79.326,-75.948,79.311,-75.603,79.24,-75.354,79.228,-75.094,79.204,-74.727,79.235,-74.481,79.229,-74.532,79.053,-74.641,79.036,-75.233,79.036,-75.515,79.061,-75.639,79.088,-75.912,79.118,-76.158,79.1,-76.38,79.104,-76.531,79.087,-76.771,79.087,-77.398,79.057,-77.729,79.057,-77.974,79.076,-78.258,79.082,-78.582,79.075,-78.559,79.055,-78.422,79.048,-78.222,79.015,-78.037,78.964,-77.883,78.942,-77.698,78.955,-77.51,78.978,-76.825,79.018,-76.524,79.024,-76.256,79.007,-76.077,78.985,-75.953,78.959,-75.795,78.89,-75.4,78.881,-75.099,78.858,-74.618,78.758,-74.486,78.75,-74.433,78.724,-74.535,78.659,-74.547,78.62,-74.879,78.545,-75.397,78.523,-75.966,78.53,-76.373,78.521,-76.416,78.512,-76.137,78.492,-75.488,78.404,-75.237,78.356,-75.193,78.328,-75.551,78.221,-75.866,78.01,-75.97,77.993,-76.078,77.987,-76.356,77.991,-76.708,77.938,-76.974,77.927,-77.456,77.947,-78.013,77.946,-78.056,77.912,-78.084,77.846,-78.081,77.747,-78.047,77.615,-78.076,77.519,-78.168,77.458,-78.284,77.413,-78.493,77.369,-78.708,77.342,-78.87,77.333,-79.138,77.331,-79.906,77.3,-80.282,77.301,-80.573,77.315,-80.875,77.359,-81.377,77.482,-81.519,77.51,-81.659,77.525,-81.654,77.499,-81.504,77.43,-81.378,77.385,-81.278,77.365,-81.301,77.344,-81.523,77.311,-81.767,77.296,-82.057,77.297,-82.066,77.284,-81.968,77.248,-81.84,77.214,-81.756,77.204,-81.534,77.214,-81.277,77.257,-81.117,77.27,-80.798,77.259,-80.673,77.244,-80.274,77.151,-80.219,77.147,-79.924,77.194,-79.497,77.196,-79.341,77.158,-79.281,77.085,-79.274,77.026,-79.319,76.98,-79.221,76.936,-78.979,76.893,-78.792,76.884,-78.659,76.908,-78.456,76.967,-78.37,76.981,-78.289,76.978,-78.165,76.935,-77.999,76.852,-77.983,76.755,-78.119,76.644,-78.284,76.571,-78.934,76.451,-79.131,76.404,-79.286,76.355,-79.511,76.31,-79.954,76.251,-80.187,76.24,-80.69,76.176,-80.8,76.174,-80.963,76.184,-80.997,76.215,-80.955,76.27,-80.901,76.322,-80.835,76.369,-80.832,76.409,-80.975,76.47,-81.074,76.498,-81.171,76.513,-81.365,76.504,-81.474,76.488,-81.592,76.484,-81.717,76.495,-81.823,76.521,-82.034,76.629,-82.114,76.643,-82.218,76.64,-82.311,76.655,-82.393,76.69,-82.53,76.723,-82.493,76.698,-82.357,76.636,-82.262,76.575,-82.208,76.514,-82.233,76.466,-83.389,76.439,-83.886,76.453,-83.986,76.495,-84.224,76.675,-84.275,76.357,-85.141,76.305,-85.344,76.313,-85.681,76.349,-86.116,76.435,-86.296,76.492,-86.367,76.549,-86.419,76.58,-86.454,76.585,-86.562,76.517,-86.68,76.377,-86.978,76.413,-87.354,76.448,-87.49,76.586,-87.498,76.386,-88.104,76.413,-88.396,76.405,-88.482,76.58,-88.496,76.773,-88.614,76.651,-88.563,76.547,-88.546,76.421,-88.804,76.457,-89.37,76.474,-89.57,76.492,-89.544,76.66,-89.5,76.827,-88.771,76.993,-88.556,77.072,-88.398,77.104,-88.148,77.124,-87.828,77.136,-87.61,77.127,-87.362,77.136,-87.064,77.166,-86.852,77.174,-86.812,77.185,-86.874,77.2,-87.101,77.308,-87.182,77.332,-87.265,77.343,-87.43,77.348,-87.589,77.395,-87.681,77.436,-87.78,77.493,-87.938,77.6,-88.095,77.719,-88.017,77.785,-87.757,77.836,-87.497,77.872,-87.236,77.892,-87.018,77.892,-86.755,77.864,-86.385,77.809,-86.173,77.746,-85.907,77.614,-85.731,77.509,-85.588,77.461,-84.951,77.375,-84.739,77.361,-84.487,77.368,-83.974,77.391,-83.721,77.414,-83.608,77.442,-83.55,77.483,-83.477,77.514,-83.25,77.585,-82.903,77.733,-82.71,77.85,-82.665,77.889,-82.626,77.936,-82.595,77.992,-82.704,77.962,-83.304,77.674,-83.428,77.621,-83.779,77.533,-83.928,77.518,-84.168,77.523,-84.486,77.562,-84.861,77.5,-85.088,77.515,-85.289,77.559,-85.292,77.764,-85.548,77.928,-85.265,78.011,-85.031,78.062,-84.615,78.196,-84.524,78.197,-84.223,78.176,-84.388,78.206,-84.55,78.251,-84.91,78.24,-84.783,78.528,-85.024,78.312,-85.27,78.2,-85.419,78.142,-85.586,78.11,-86.218,78.081,-86.063,78.187,-85.92,78.343,-86.071,78.285,-86.427,78.197,-86.694,78.151,-86.913,78.127,-87.339,78.133,-87.552,78.177,-87.491,78.284,-87.491,78.417,-87.361,78.479,-87.164,78.558,-86.953,78.664,-86.808,78.774,-86.242,78.824,-85.691,78.844,-85.23,78.902,-85.004,78.912,-84.787,78.885,-83.908,78.839,-83.547,78.804,-83.389,78.779,-83.271,78.77,-83.147,78.808,-82.99,78.844,-82.442,78.84,-82.291,78.847,-82.151,78.864,-81.981,78.898,-81.781,78.95,-81.75,78.976,-81.889,78.975,-82.028,78.962,-82.237,78.924,-82.439,78.904,-82.644,78.908,-83.059,78.94,-83.779,78.945,-84.146,78.96,-84.316,78.975,-84.412,78.997,-84.496,79.029,-84.568,79.071,-84.53,79.101,-84.384,79.119,-84.257,79.122,-84.053,79.099,-83.825,79.059,-83.576,79.054,-83.662,79.09,-83.978,79.163,-84.197,79.225,-84.381,79.301,-84.522,79.377,-84.836,79.495,-85.09,79.612,-85.269,79.664,-85.457,79.69,-86.031,79.722,-86.147,79.743,-86.421,79.845,-86.494,80.018,-86.615,80.124,-86.499,80.258,-86.307,80.319,-85.16,80.272,-84.675,80.279,-84.057,80.262,-83.724,80.229,-83.344,80.147,-83.004,80.055,-82.677,79.993,-82.377,79.908,-82.049,79.783,-81.856,79.723,-81.688,79.686,-81.463,79.654,-81.038,79.614,-80.668,79.601,-80.476,79.606,-80.271,79.635,-80.124,79.669,-80.287,79.679,-80.714,79.675,-81.01,79.693,-81.179,79.733,-81.359,79.788,-81.644,79.89,-81.86,79.957,-82.332,80.066,-82.681,80.175,-82.961,80.278,-82.987,80.323,-82.785,80.354,-82.536,80.376,-80.98,80.445,-80.051,80.529,-79.674,80.625,-79.629,80.648,-78.386,80.784,-77.507,80.835,-77.169,80.843,-76.863,80.865,-76.85,80.878,-77.119,80.896,-77.389,80.905,-78.004,80.905,-78.551,80.921,-78.716,80.952,-78.682,81.001,-78.629,81.043,-78.464,81.114,-78.287,81.168,-77.536,81.321,-77.031,81.386,-76.885,81.43,-77.972,81.331,-78.352,81.259,-78.734,81.151,-78.932,81.119,-79.072,81.128,-79.198,81.118,-79.309,81.089,-79.402,81.037,-79.477,80.961,-79.545,80.909,-79.607,80.882,-79.761,80.842,-80.134,80.764,-81.007,80.655,-81.301,80.627,-81.553,80.623,-82.368,80.561,-82.613,80.559,-82.884,80.578,-82.768,80.631,-82.337,80.729,-82.222,80.772,-82.498,80.763,-82.78,80.736,-83.401,80.714,-83.647,80.674,-83.885,80.602,-84.076,80.556,-84.22,80.538,-84.418,80.527,-85.146,80.521,-85.307,80.526,-85.726,80.581,-86.097,80.562,-86.25,80.566,-86.532,80.605,-86.615,80.63,-86.603,80.664,-86.44,80.728,-86.252,80.79,-85.639,80.925,-85.246,80.988,-84.68,81.042,-83.349,81.103,-83.289,81.148,-84.635,81.098,-85.781,81.035,-85.967,81.012,-86.233,80.95,-87.08,80.726,-87.33,80.67,-87.712,80.656,-88.004,80.675,-88.232,80.704,-88.625,80.77,-88.921,80.806,-89.062,80.83,-89.145,80.854,-89.212,80.882,-89.263,80.914,-89.167,80.941,-88.413,81,-87.389,80.988,-86.929,81,-86.477,81.036,-85.81,81.124,-85.083,81.247,-84.941,81.286,-85.206,81.295,-85.402,81.285,-85.875,81.241,-86.623,81.123,-87.275,81.081,-88.887,81.058,-89.398,81.025,-89.623,81.032,-89.792,81.065,-89.981,81.125,-89.947,81.173,-89.563,81.226,-89.263,81.239,-89.209,81.25,-89.636,81.302,-89.674,81.329,-89.427,81.387,-88.892,81.474,-88.622,81.501,-88.127,81.519,-87.617,81.509,-87.597,81.526,-88.101,81.559,-88.479,81.565,-88.978,81.542,-90.304,81.401,-90.416,81.405,-90.609,81.43,-90.554,81.464,-89.845,81.612,-89.822,81.635,-90.331,81.632,-90.48,81.639,-90.626,81.656,-90.834,81.64,-91.103,81.592,-91.292,81.571,-91.403,81.578,-91.684,81.636,-91.648,81.684,-91.424,81.744,-91.219,81.788,-90.942,81.827,-90.49,81.877,-90.163,81.894,-89.633,81.895,-89.381,81.917,-89.156,81.955,-88.875,82.018,-88.567,82.061,-88.063,82.096,-87.639,82.085,-87.404,82.054,-87.218,82,-87.018,81.959,-86.999,81.992,-86.834,82.033,-86.627,82.051,-86.378,82.045,-86.158,82.026,-85.875,81.976,-85.646,81.953,-85.538,81.955,-85.403,81.982,-85.045,81.983,-85.052,81.995,-85.169,82.023,-85.311,82.044,-86.581,82.187,-86.616,82.219,-86.188,82.248,-85.92,82.283,-85.794,82.292,-85.481,82.366,-85.276,82.405,-84.897,82.449,-84.745,82.437,-84.553,82.398,-84.368,82.374,-83.824,82.351,-83.591,82.326,-83.176,82.187,-83.01,82.142,-82.774,82.095,-82.634,82.077,-82.356,82.066,-82.327,82.092,-82.657,82.158,-82.747,82.196,-82.709,82.229,-82.638,82.246,-82.537,82.247,-82.277,82.218,-81.584,82.121,-80.55,82.005,-80.153,81.978,-79.909,81.936,-79.686,81.886,-79.466,81.851,-79.425,81.854,-79.629,81.932,-80.13,82.028,-81.468,82.192,-81.998,82.278,-82.254,82.336,-82.448,82.395,-82.451,82.427,-82.269,82.465,-82.023,82.494,-81.718,82.506,-81.681,82.519,-81.959,82.563,-82.123,82.602,-82.117,82.629,-81.785,82.649,-81.58,82.643,-81.189,82.594,-80.862,82.572,-80.81,82.586,-81.147,82.716,-81.178,82.745,-81.128,82.762,-81.01,82.779,-80.657,82.769,-80.076,82.706,-79.035,82.675,-78.749,82.679,-78.792,82.694,-79.207,82.733,-79.642,82.785,-79.834,82.817,-79.974,82.859,-80.141,82.894,-80.155,82.911,-79.886,82.939,-79.181,82.933,-78.525,82.891,-77.969,82.906,-77.618,82.896,-77.48,82.883,-77.226,82.837,-76.421,82.671,-76.336,82.644,-76.244,82.604,-76.146,82.55,-76.009,82.535,-75.744,82.572,-75.566,82.609,-75.643,82.644,-76.087,82.724,-76.188,82.758,-76.41,82.816,-76.908,82.919,-77.041,82.968,-77.125,83.009,-75.745,83.047,-74.414,83.013,-74.198,82.989,-74.056,82.955,-73.917,82.904,-73.703,82.852,-73.272,82.772,-72.659,82.722,-72.776,82.756,-73.235,82.844,-73.442,82.905,-73.441,82.946,-73.404,82.977,-73.331,82.999,-72.812,83.081,-72.069,83.106,-71.983,83.101,-71.406,82.975,-71.132,82.923,-70.94,82.902,-70.933,82.911,-71.198,82.97,-71.402,83.001,-71.424,83.021,-71.085,83.083,-70.871,83.098,-69.97,83.116,-69.868,83.11,-69.782,83.093,-69.569,83.025,-69.489,83.017]],[[-95.484,77.792,-95.233,77.754,-94.96,77.774,-94.667,77.776,-94.015,77.76,-93.583,77.771,-93.471,77.764,-93.301,77.74,-93.211,77.71,-93.129,77.66,-93.339,77.63,-93.52,77.474,-93.544,77.467,-93.74,77.465,-93.836,77.452,-94.409,77.474,-95.987,77.484,-96.056,77.503,-96.264,77.595,-96.277,77.631,-96.239,77.673,-96.195,77.701,-96.143,77.714,-95.684,77.782,-95.484,77.792]],[[-93.543,75.028,-93.478,74.952,-93.467,74.921,-93.463,74.856,-93.491,74.772,-93.509,74.756,-93.536,74.749,-93.548,74.728,-93.547,74.691,-93.573,74.669,-93.626,74.661,-93.985,74.644,-94.206,74.647,-94.535,74.637,-94.697,74.642,-94.804,74.66,-94.959,74.7,-95.286,74.794,-95.451,74.797,-95.865,74.83,-96.094,74.933,-96.182,74.951,-96.27,74.92,-96.294,74.927,-96.319,74.948,-96.343,74.982,-96.386,74.999,-96.56,74.99,-96.591,75.002,-96.6,75.032,-96.597,75.058,-96.566,75.099,-96.383,75.211,-96.292,75.219,-96.18,75.24,-96.118,75.301,-96.125,75.358,-95.955,75.444,-95.853,75.469,-95.671,75.529,-95.05,75.622,-94.878,75.63,-94.649,75.623,-94.427,75.593,-94.257,75.544,-93.909,75.423,-93.751,75.349,-93.667,75.274,-93.591,75.23,-93.498,75.137,-93.532,75.1,-93.552,75.051,-93.543,75.028]],[[-100.002,73.946,-99.158,73.732,-99.04,73.749,-98.785,73.761,-98.519,73.792,-98.152,73.818,-97.928,73.866,-97.832,73.879,-97.67,73.888,-97.582,73.888,-97.327,73.862,-97.225,73.844,-97.171,73.825,-97.112,73.79,-97.011,73.706,-96.997,73.675,-97.002,73.667,-97.095,73.615,-97.156,73.592,-97.284,73.571,-97.395,73.564,-97.49,73.527,-97.597,73.537,-97.626,73.502,-97.615,73.481,-97.586,73.471,-97.532,73.474,-97.47,73.488,-97.35,73.481,-97.287,73.458,-97.23,73.421,-97.273,73.387,-97.484,73.339,-97.796,73.285,-98.176,73.116,-98.376,73.045,-98.417,73.023,-98.437,73,-98.431,72.958,-98.422,72.941,-98.367,72.934,-98.181,72.993,-98.061,73.021,-97.939,73.036,-97.725,73.037,-97.636,73.028,-97.476,72.992,-97.329,72.938,-97.296,72.918,-97.31,72.898,-97.371,72.878,-97.378,72.865,-97.238,72.837,-97.083,72.763,-97.073,72.718,-97.14,72.673,-97.159,72.643,-97.128,72.628,-97.052,72.637,-96.869,72.687,-96.671,72.713,-96.592,72.71,-96.542,72.699,-96.489,72.63,-96.446,72.552,-96.44,72.487,-96.473,72.434,-96.52,72.393,-96.638,72.342,-96.746,72.323,-96.801,72.322,-96.796,72.314,-96.669,72.271,-96.616,72.237,-96.593,72.204,-96.601,72.173,-96.618,72.146,-96.766,72.046,-96.758,72.032,-96.717,72.025,-96.624,71.968,-96.613,71.834,-96.946,71.792,-97.025,71.761,-97.117,71.711,-97.222,71.673,-97.461,71.634,-97.582,71.63,-98.181,71.662,-98.242,71.681,-98.284,71.716,-98.307,71.765,-98.313,71.803,-98.303,71.831,-98.306,71.848,-98.323,71.852,-98.389,71.824,-98.459,71.773,-98.421,71.717,-98.231,71.559,-98.195,71.491,-98.19,71.462,-98.199,71.441,-98.412,71.349,-98.536,71.318,-98.663,71.302,-98.784,71.314,-98.899,71.352,-98.986,71.369,-99.167,71.367,-99.224,71.387,-99.276,71.424,-99.404,71.557,-99.581,71.652,-99.735,71.757,-100.124,71.912,-100.326,72.004,-100.594,72.152,-100.707,72.186,-100.8,72.199,-100.984,72.21,-101.026,72.229,-101.093,72.279,-101.209,72.317,-101.251,72.322,-101.319,72.313,-101.498,72.278,-101.724,72.315,-101.775,72.341,-101.804,72.385,-101.833,72.409,-101.909,72.431,-101.974,72.486,-102.402,72.595,-102.657,72.719,-102.709,72.765,-102.714,72.783,-102.687,72.843,-102.628,72.911,-102.551,72.978,-102.504,73.006,-102.336,73.064,-102.204,73.077,-102.02,73.07,-101.922,73.057,-101.835,73.018,-101.798,72.973,-101.755,72.943,-101.618,72.91,-101.544,72.883,-101.435,72.821,-101.351,72.746,-101.273,72.722,-101.088,72.713,-100.896,72.726,-100.485,72.773,-100.468,72.779,-100.443,72.807,-100.396,72.977,-100.368,72.978,-100.228,72.899,-100.188,72.89,-100.128,72.907,-100.092,72.945,-100.097,72.963,-100.184,73.055,-100.236,73.095,-100.283,73.12,-100.334,73.128,-100.446,73.121,-100.531,73.138,-100.55,73.164,-100.536,73.198,-100.489,73.234,-100.439,73.255,-100.341,73.265,-100.226,73.255,-100.067,73.211,-99.966,73.201,-99.825,73.214,-100.006,73.24,-100.258,73.34,-100.366,73.359,-100.498,73.316,-100.587,73.3,-100.755,73.278,-100.889,73.275,-101.451,73.431,-101.482,73.446,-101.523,73.486,-101.518,73.505,-101.463,73.534,-101.323,73.572,-101.115,73.596,-100.976,73.6,-100.854,73.571,-100.677,73.494,-100.522,73.449,-100.509,73.465,-100.536,73.51,-100.607,73.575,-100.658,73.593,-100.783,73.613,-100.898,73.658,-100.953,73.691,-100.982,73.727,-100.985,73.765,-100.963,73.791,-100.915,73.805,-100.484,73.844,-100.182,73.801,-99.991,73.795,-99.912,73.847,-99.94,73.857,-100.04,73.844,-100.154,73.844,-100.225,73.873,-100.227,73.889,-100.138,73.929,-100.002,73.946]],[[-84.92,65.261,-84.885,65.249,-84.842,65.256,-84.771,65.305,-84.612,65.447,-84.568,65.461,-84.501,65.458,-84.266,65.367,-84.18,65.316,-84.133,65.245,-84.085,65.218,-83.9,65.181,-83.723,65.169,-83.491,65.132,-83.407,65.104,-83.222,64.968,-83.201,64.96,-82.991,64.904,-82.668,64.78,-82.586,64.762,-82.272,64.721,-82.159,64.691,-82.05,64.644,-81.929,64.559,-81.787,64.426,-81.676,64.213,-81.667,64.171,-81.681,64.146,-81.721,64.119,-81.903,64.031,-81.887,64.016,-81.716,64.022,-81.336,64.076,-81.104,64.037,-81.024,64.031,-81.005,64.033,-80.921,64.1,-80.829,64.09,-80.694,64.025,-80.608,63.972,-80.569,63.932,-80.579,63.909,-80.668,63.901,-80.451,63.863,-80.261,63.802,-80.302,63.762,-80.504,63.674,-80.712,63.596,-80.954,63.48,-81.014,63.463,-81.046,63.462,-81.18,63.483,-81.372,63.538,-81.963,63.664,-82.146,63.691,-82.378,63.707,-82.412,63.737,-82.467,63.927,-82.571,63.961,-82.93,64,-83.034,64.023,-83.039,64.061,-83.016,64.127,-83.065,64.159,-83.186,64.158,-83.304,64.144,-83.494,64.099,-83.584,64.058,-83.617,64.013,-83.638,63.918,-83.662,63.873,-83.728,63.813,-84.022,63.66,-84.142,63.614,-84.26,63.6,-84.308,63.586,-84.387,63.529,-84.506,63.39,-84.555,63.35,-84.633,63.309,-84.796,63.247,-84.962,63.197,-85.238,63.139,-85.393,63.12,-85.496,63.139,-85.566,63.271,-85.714,63.658,-85.739,63.684,-85.769,63.7,-85.805,63.707,-86.302,63.657,-86.576,63.662,-86.847,63.575,-86.915,63.569,-87.053,63.572,-87.152,63.586,-87.177,63.595,-87.194,63.633,-87.189,63.672,-87.154,63.715,-87.032,63.83,-86.932,63.902,-86.886,63.924,-86.422,64.052,-86.309,64.094,-86.252,64.137,-86.252,64.181,-86.274,64.238,-86.354,64.377,-86.375,64.503,-86.374,64.566,-86.344,64.662,-86.228,64.896,-86.188,65.01,-86.114,65.417,-86.075,65.534,-86.017,65.64,-85.962,65.704,-85.814,65.832,-85.699,65.883,-85.555,65.919,-85.523,65.915,-85.496,65.9,-85.442,65.846,-85.241,65.796,-85.176,65.747,-85.13,65.693,-85.105,65.623,-85.13,65.592,-85.226,65.546,-85.243,65.526,-85.24,65.51,-85.056,65.437,-84.92,65.261]],[[-97.701,76.467,-97.69,76.422,-97.702,76.387,-97.737,76.363,-97.739,76.335,-97.707,76.304,-97.573,76.224,-97.531,76.182,-97.524,76.139,-97.531,76.109,-97.613,76.053,-97.65,75.979,-97.652,75.94,-97.603,75.879,-97.602,75.851,-97.694,75.803,-97.891,75.76,-97.863,75.738,-97.44,75.685,-97.408,75.673,-97.41,75.552,-97.336,75.42,-97.363,75.417,-97.465,75.459,-97.653,75.508,-97.878,75.416,-97.853,75.26,-97.705,75.191,-97.66,75.151,-97.674,75.127,-97.799,75.117,-97.843,75.122,-97.971,75.153,-98.045,75.201,-98.069,75.199,-98.092,75.176,-98.077,75.153,-97.99,75.111,-97.953,75.06,-97.992,75.046,-98.121,75.033,-98.295,75.032,-98.569,75.009,-98.704,75.006,-98.835,75.018,-99.01,75.021,-99.156,75.016,-99.245,75.026,-99.326,75.049,-99.421,75.044,-99.627,74.984,-99.947,75.003,-100.234,75.008,-100.292,75.028,-100.357,75.067,-100.483,75.188,-100.459,75.219,-100.152,75.236,-100.146,75.246,-100.364,75.29,-100.615,75.321,-100.731,75.347,-100.704,75.394,-100.712,75.406,-100.28,75.461,-99.965,75.569,-99.77,75.612,-99.756,75.633,-99.591,75.655,-99.209,75.669,-99.195,75.698,-99.915,75.681,-100.902,75.62,-101.207,75.59,-101.461,75.608,-102.541,75.514,-102.587,75.514,-102.7,75.544,-102.798,75.6,-102.728,75.639,-102.411,75.713,-102.252,75.778,-102.271,75.813,-102.145,75.875,-101.943,75.884,-101.6,75.833,-101.421,75.782,-101.261,75.758,-101.119,75.763,-100.973,75.798,-101.01,75.802,-101.259,75.784,-101.288,75.789,-101.415,75.846,-101.47,75.882,-101.506,75.918,-101.508,75.944,-101.431,75.992,-101.717,76.008,-101.823,76.041,-101.872,76.083,-101.861,76.101,-101.771,76.15,-101.529,76.217,-101.557,76.236,-101.91,76.234,-101.987,76.243,-102.138,76.285,-102.105,76.331,-101.964,76.399,-101.858,76.439,-101.788,76.451,-101.677,76.451,-101.415,76.425,-101.34,76.41,-101.139,76.345,-101.088,76.308,-101.094,76.272,-101.056,76.246,-100.9,76.207,-100.231,76.008,-100.106,75.96,-100.02,75.94,-99.865,75.924,-99.775,75.927,-99.701,75.941,-99.689,75.96,-99.978,76.029,-100.051,76.067,-100.113,76.117,-100.086,76.134,-100.002,76.139,-99.79,76.133,-99.541,76.146,-99.817,76.168,-99.998,76.196,-100.183,76.197,-100.414,76.243,-100.414,76.257,-100.358,76.271,-100.043,76.291,-99.983,76.3,-99.978,76.312,-100.082,76.343,-100.175,76.359,-100.651,76.396,-100.82,76.437,-100.874,76.457,-100.891,76.475,-100.83,76.524,-100.574,76.585,-100.388,76.614,-100.069,76.635,-99.814,76.632,-99.669,76.624,-99.329,76.521,-99.17,76.454,-98.89,76.466,-98.971,76.537,-99.024,76.615,-98.941,76.643,-98.711,76.694,-98.528,76.667,-98.289,76.599,-98.236,76.575,-97.967,76.533,-97.808,76.519,-97.726,76.496,-97.701,76.467]],[[-103.426,79.316,-103.192,79.295,-102.914,79.231,-102.652,79.095,-102.639,79.078,-102.638,79.055,-102.648,79.027,-102.683,78.991,-102.731,78.969,-102.596,78.943,-102.581,78.93,-102.593,78.901,-102.576,78.879,-102.495,78.901,-102.425,78.933,-102.407,78.954,-102.393,79.01,-102.189,79.038,-101.974,79.079,-101.873,79.088,-101.704,79.079,-101.299,78.982,-101.145,78.973,-101.088,78.962,-101.037,78.939,-101.034,78.915,-101.116,78.858,-101.147,78.824,-101.128,78.802,-100.917,78.783,-100.435,78.82,-100.015,78.729,-99.782,78.62,-99.609,78.583,-99.582,78.563,-99.631,78.545,-99.68,78.494,-99.818,78.455,-99.848,78.438,-99.774,78.393,-99.769,78.365,-99.778,78.325,-99.751,78.303,-99.562,78.279,-99.132,78.118,-99.053,78.072,-99.005,78.016,-99,77.997,-99.061,77.966,-99.128,77.877,-99.166,77.857,-99.341,77.84,-99.659,77.824,-99.956,77.794,-100.275,77.833,-100.586,77.892,-100.68,77.931,-100.758,77.978,-100.778,77.996,-100.81,78.072,-100.826,78.088,-100.958,78.13,-101.074,78.194,-101.298,78.199,-101.829,78.264,-102.057,78.28,-102.284,78.275,-102.607,78.249,-102.668,78.256,-102.723,78.275,-102.772,78.307,-102.784,78.33,-102.731,78.371,-103.677,78.32,-103.947,78.26,-104.324,78.269,-104.513,78.295,-104.764,78.352,-104.879,78.401,-104.985,78.468,-104.996,78.519,-104.91,78.553,-104.82,78.573,-104.727,78.579,-104.214,78.54,-103.764,78.52,-103.571,78.54,-103.483,78.594,-103.588,78.623,-104.021,78.635,-103.929,78.663,-103.563,78.693,-103.372,78.736,-103.408,78.752,-103.518,78.769,-104.009,78.764,-104.185,78.781,-104.195,78.796,-104.155,78.814,-103.876,78.903,-103.887,78.919,-104.007,78.948,-104.113,78.986,-104.152,78.99,-104.395,78.956,-104.736,78.826,-104.817,78.807,-104.896,78.808,-104.97,78.829,-104.97,78.856,-104.893,78.89,-104.735,78.991,-104.747,79.027,-104.901,79.051,-105.309,79.033,-105.536,79.033,-105.571,79.061,-105.58,79.114,-105.571,79.164,-105.515,79.242,-105.436,79.302,-105.388,79.324,-104.847,79.311,-103.965,79.348,-103.706,79.352,-103.426,79.316]],[[-91.886,81.133,-91.755,81.049,-91.272,80.85,-91.054,80.778,-90.683,80.688,-90.637,80.655,-90.632,80.642,-90.643,80.594,-90.537,80.576,-90.218,80.548,-89.862,80.498,-89.798,80.501,-89.674,80.531,-89.525,80.539,-89.329,80.532,-89.236,80.511,-89.167,80.48,-89.138,80.457,-89.134,80.44,-89.204,80.407,-89.197,80.394,-89.155,80.379,-89.147,80.36,-89.218,80.289,-89.198,80.263,-89.019,80.198,-88.857,80.166,-88.538,80.131,-88.329,80.134,-88.2,80.111,-88.197,80.125,-88.255,80.167,-88.381,80.225,-88.612,80.255,-88.646,80.29,-88.663,80.348,-88.644,80.387,-88.525,80.418,-88.424,80.428,-88.125,80.429,-87.96,80.416,-87.675,80.372,-87.646,80.348,-87.63,80.302,-87.618,80.207,-87.625,80.187,-87.869,80.134,-87.922,80.098,-87.861,80.088,-87.651,80.079,-87.329,80.047,-87.202,80.043,-87.076,79.967,-86.977,79.894,-87.05,79.805,-87.144,79.663,-87.22,79.63,-87.295,79.58,-87.243,79.571,-86.925,79.591,-86.861,79.598,-86.649,79.646,-86.337,79.635,-86.232,79.622,-86.18,79.605,-86.086,79.551,-86.007,79.479,-85.949,79.486,-85.804,79.573,-85.751,79.595,-85.679,79.615,-85.648,79.611,-85.501,79.53,-85.176,79.387,-85.064,79.328,-85.042,79.285,-85.181,79.234,-85.29,79.208,-86.092,79.1,-86.451,79.039,-86.629,78.991,-86.721,78.975,-86.913,78.983,-86.957,78.975,-87.016,78.899,-87.08,78.866,-87.246,78.813,-87.479,78.718,-87.617,78.676,-87.861,78.707,-87.922,78.751,-87.956,78.852,-87.961,78.893,-87.953,78.915,-87.923,78.951,-87.817,79.036,-87.829,79.045,-87.878,79.038,-88.04,78.995,-88.104,78.973,-88.164,78.933,-88.19,78.867,-88.167,78.746,-88.19,78.696,-88.254,78.672,-88.228,78.653,-88.037,78.627,-88.003,78.616,-87.982,78.595,-87.974,78.565,-87.983,78.537,-88.04,78.494,-88.148,78.477,-88.285,78.497,-88.581,78.602,-88.709,78.596,-88.742,78.584,-88.714,78.546,-88.623,78.462,-88.606,78.392,-88.648,78.334,-88.733,78.242,-88.791,78.192,-88.822,78.186,-88.97,78.184,-89.096,78.209,-89.47,78.37,-89.655,78.439,-89.926,78.573,-89.995,78.601,-90.037,78.607,-90.076,78.549,-90.001,78.496,-89.757,78.37,-89.612,78.279,-89.507,78.203,-89.49,78.172,-89.526,78.16,-89.579,78.167,-89.651,78.193,-89.873,78.238,-89.965,78.262,-90.025,78.291,-90.136,78.313,-90.297,78.328,-90.459,78.331,-90.622,78.322,-90.652,78.308,-90.469,78.269,-90.405,78.247,-90.358,78.219,-90.327,78.185,-90.387,78.163,-90.614,78.15,-90.918,78.158,-91.41,78.188,-91.899,78.237,-92.351,78.313,-92.678,78.389,-92.808,78.43,-92.848,78.46,-92.726,78.487,-92.297,78.521,-91.867,78.543,-91.935,78.562,-92.716,78.605,-92.973,78.613,-93.109,78.602,-93.267,78.608,-93.389,78.643,-93.552,78.708,-93.634,78.751,-93.623,78.768,-93.561,78.777,-93.208,78.769,-93.16,78.776,-93.336,78.808,-93.902,78.872,-94.115,78.929,-94.154,78.951,-94.17,78.973,-94.163,78.994,-93.95,79.037,-93.294,79.14,-93.068,79.155,-92.842,79.156,-92.684,79.186,-92.547,79.283,-91.868,79.317,-91.344,79.361,-91.3,79.373,-91.693,79.365,-92.248,79.373,-92.485,79.439,-92.645,79.45,-92.822,79.45,-93.028,79.429,-93.381,79.368,-93.55,79.354,-93.933,79.291,-94.04,79.295,-94.093,79.303,-94.109,79.315,-94.04,79.357,-93.94,79.386,-93.96,79.396,-94.11,79.402,-94.284,79.4,-94.405,79.391,-94.846,79.335,-95.044,79.294,-95.103,79.29,-95.317,79.355,-95.657,79.39,-95.733,79.418,-95.663,79.527,-95.563,79.55,-95.302,79.568,-94.52,79.667,-94.476,79.686,-94.402,79.736,-94.581,79.726,-94.973,79.677,-95.297,79.653,-95.552,79.653,-95.739,79.66,-95.858,79.674,-96,79.705,-96.463,79.848,-96.589,79.917,-96.607,79.978,-96.639,80.024,-96.773,80.136,-95.782,80.066,-95.394,80.053,-94.646,80.049,-94.611,80.056,-94.6,80.074,-94.613,80.103,-94.607,80.126,-94.583,80.141,-94.304,80.182,-94.263,80.195,-94.59,80.202,-95.192,80.134,-95.405,80.135,-95.646,80.231,-95.904,80.214,-96.026,80.222,-96.215,80.246,-96.308,80.267,-96.368,80.293,-96.394,80.315,-96.385,80.333,-96.334,80.353,-96.112,80.38,-96.012,80.383,-95.747,80.365,-95.549,80.367,-95.614,80.396,-95.901,80.471,-96.152,80.553,-96.133,80.691,-95.927,80.721,-95.714,80.725,-95.505,80.691,-95.226,80.686,-95.026,80.646,-94.893,80.571,-94.734,80.572,-94.485,80.558,-93.928,80.559,-94.029,80.586,-94.202,80.61,-94.596,80.641,-94.788,80.751,-95.196,80.808,-95.515,80.838,-95.509,80.863,-95.27,81.001,-94.981,81.05,-94.519,81.031,-94.216,81.057,-93.826,81.106,-93.444,81.083,-93.345,81.085,-93.287,81.1,-93.236,81.129,-93.235,81.155,-93.286,81.179,-93.407,81.209,-93.894,81.213,-94.11,81.225,-94.194,81.241,-94.219,81.265,-94.231,81.29,-94.233,81.315,-94.22,81.331,-94.179,81.339,-94.06,81.349,-93.605,81.351,-93.333,81.364,-93.035,81.346,-92.413,81.278,-92.212,81.244,-91.998,81.185,-91.886,81.133]],[[-94.295,76.912,-94.108,76.904,-93.948,76.917,-93.811,76.914,-93.608,76.874,-93.421,76.812,-93.277,76.784,-93.23,76.77,-93.212,76.755,-93.189,76.708,-93.19,76.686,-93.201,76.669,-93.264,76.626,-93.317,76.574,-93.426,76.527,-93.485,76.492,-93.535,76.448,-93.422,76.474,-92.995,76.62,-92.716,76.603,-92.297,76.616,-91.789,76.676,-91.548,76.685,-91.305,76.681,-91.124,76.662,-90.738,76.581,-90.605,76.543,-90.555,76.516,-90.543,76.496,-90.622,76.465,-90.864,76.484,-91.263,76.5,-91.336,76.511,-91.398,76.51,-91.443,76.499,-91.415,76.456,-91.334,76.446,-90.855,76.437,-89.285,76.302,-89.219,76.258,-89.237,76.239,-89.292,76.218,-89.407,76.189,-90.312,76.158,-90.827,76.186,-91.26,76.23,-91.407,76.22,-91.279,76.16,-91.02,76.142,-90.802,76.106,-90.712,76.076,-90.251,76.053,-90.176,76.03,-90.033,75.971,-89.913,75.966,-89.793,75.925,-89.695,75.854,-89.65,75.844,-89.511,75.857,-89.278,75.795,-89.205,75.762,-89.205,75.737,-89.257,75.698,-89.361,75.646,-89.625,75.584,-89.646,75.565,-89.337,75.572,-89.28,75.564,-88.917,75.454,-88.869,75.452,-88.839,75.463,-88.804,75.502,-88.82,75.539,-88.864,75.589,-88.852,75.625,-88.784,75.647,-88.715,75.659,-88.645,75.658,-88.569,75.645,-88.201,75.512,-87.73,75.576,-87.644,75.547,-87.572,75.494,-87.539,75.485,-87.365,75.591,-87.257,75.618,-86.814,75.491,-86.545,75.463,-86.437,75.436,-86.236,75.406,-85.951,75.395,-85.905,75.442,-86.069,75.502,-85.973,75.529,-85.581,75.58,-85.372,75.573,-84.987,75.645,-84.75,75.655,-84.605,75.653,-84.128,75.763,-84.014,75.78,-83.932,75.819,-83.745,75.813,-83.237,75.751,-83.093,75.756,-82.553,75.818,-82.354,75.833,-82.154,75.831,-81.647,75.795,-81.269,75.756,-81.151,75.736,-81.193,75.684,-81.174,75.669,-81.124,75.658,-81.001,75.643,-80.528,75.642,-80.322,75.629,-80.158,75.581,-80.119,75.562,-80.126,75.542,-80.287,75.49,-80.26,75.479,-80.1,75.467,-79.738,75.461,-79.66,75.45,-79.586,75.385,-79.508,75.295,-79.509,75.26,-79.634,75.199,-79.977,75.119,-80.358,75.052,-80.382,75.034,-80.261,75.002,-80.135,74.988,-80.036,74.991,-79.733,75.021,-79.664,75.021,-79.525,74.99,-79.46,74.959,-79.401,74.918,-79.508,74.88,-79.944,74.834,-80.202,74.895,-80.289,74.908,-80.348,74.903,-80.315,74.876,-80.19,74.828,-80.149,74.796,-80.192,74.78,-80.213,74.749,-80.21,74.704,-80.221,74.657,-80.263,74.584,-80.278,74.582,-81.226,74.567,-81.34,74.554,-81.607,74.502,-81.809,74.477,-81.94,74.473,-82.069,74.482,-82.415,74.535,-82.645,74.525,-82.736,74.53,-82.931,74.566,-82.978,74.583,-83.058,74.63,-83.117,74.693,-83.112,74.732,-83.087,74.788,-83.103,74.817,-83.158,74.817,-83.22,74.828,-83.407,74.885,-83.522,74.901,-83.544,74.892,-83.51,74.848,-83.487,74.834,-83.364,74.802,-83.341,74.765,-83.394,74.67,-83.412,74.655,-83.532,74.586,-83.622,74.566,-83.868,74.564,-84.245,74.515,-84.426,74.508,-84.667,74.52,-84.818,74.542,-84.916,74.568,-85.012,74.604,-85.061,74.607,-85.087,74.528,-85.133,74.517,-85.214,74.519,-85.339,74.543,-85.442,74.601,-85.474,74.6,-85.489,74.567,-85.512,74.545,-85.544,74.535,-85.808,74.499,-85.956,74.499,-86.11,74.54,-86.211,74.536,-86.341,74.513,-86.655,74.555,-86.731,74.557,-86.666,74.489,-86.77,74.479,-86.995,74.48,-87.364,74.502,-87.593,74.47,-88.006,74.489,-88.423,74.494,-88.501,74.51,-88.556,74.541,-88.558,74.57,-88.538,74.609,-88.477,74.667,-88.375,74.744,-88.34,74.785,-88.431,74.804,-88.488,74.829,-88.535,74.832,-88.682,74.802,-88.778,74.715,-88.851,74.69,-88.883,74.711,-88.908,74.764,-88.94,74.79,-88.98,74.788,-89.02,74.774,-89.058,74.747,-89.115,74.738,-89.192,74.745,-89.219,74.732,-89.197,74.698,-89.189,74.667,-89.196,74.638,-89.262,74.609,-89.45,74.568,-89.559,74.555,-89.844,74.549,-90.015,74.561,-90.362,74.61,-90.553,74.613,-90.784,74.696,-90.967,74.715,-90.958,74.745,-90.878,74.801,-90.88,74.818,-91.13,74.736,-91.164,74.71,-91.135,74.65,-91.168,74.646,-91.339,74.667,-91.508,74.651,-91.549,74.656,-91.666,74.699,-91.871,74.744,-91.962,74.793,-92.103,74.948,-92.174,75.051,-92.165,75.072,-92.06,75.101,-92.076,75.124,-92.207,75.181,-92.347,75.23,-92.389,75.263,-92.408,75.297,-92.427,75.346,-92.428,75.383,-92.411,75.406,-92.331,75.479,-92.11,75.611,-92.081,75.634,-92.069,75.658,-92.099,75.727,-92.142,75.797,-92.185,75.847,-92.307,75.915,-92.474,75.986,-92.709,76.114,-92.883,76.214,-93.092,76.354,-93.192,76.366,-93.309,76.36,-93.56,76.311,-93.644,76.289,-93.665,76.273,-93.852,76.27,-94.383,76.282,-94.585,76.297,-94.737,76.293,-94.997,76.258,-95.274,76.264,-95.447,76.363,-95.842,76.416,-95.959,76.446,-96.04,76.487,-96.013,76.513,-95.789,76.537,-95.696,76.563,-95.651,76.585,-95.873,76.566,-95.971,76.57,-96.64,76.703,-96.846,76.726,-96.881,76.738,-96.898,76.754,-96.898,76.773,-96.878,76.803,-96.679,76.766,-96.59,76.763,-96.451,76.774,-96.402,76.797,-96.433,76.811,-96.661,76.855,-96.771,76.889,-96.814,76.913,-96.77,76.948,-96.758,76.972,-96.685,76.985,-96.55,76.988,-96.377,77.005,-96.061,77.05,-95.85,77.066,-95.638,77.064,-95.126,77.017,-94.616,76.958,-94.295,76.912]],[[-96.204,78.531,-95.968,78.505,-95.561,78.517,-95.413,78.498,-95.031,78.43,-94.915,78.391,-94.888,78.36,-94.887,78.345,-95.014,78.313,-95.268,78.263,-95.329,78.225,-95.103,78.178,-94.988,78.136,-94.936,78.106,-94.934,78.076,-95.087,77.993,-95.199,77.968,-95.371,77.971,-95.452,77.963,-95.671,77.924,-96.012,77.887,-96.477,77.872,-96.603,77.849,-96.834,77.812,-96.99,77.806,-97.04,77.827,-97.064,77.859,-97.052,77.881,-97.019,77.908,-97.093,77.933,-97.427,77.982,-97.621,78.05,-97.648,78.072,-97.658,78.091,-97.227,78.103,-97.041,78.117,-96.958,78.139,-96.945,78.152,-97.027,78.157,-97.323,78.203,-97.819,78.231,-97.843,78.262,-98.05,78.326,-98.069,78.386,-98.114,78.403,-98.255,78.429,-98.276,78.438,-98.317,78.477,-98.324,78.498,-98.316,78.517,-98.06,78.558,-98.096,78.587,-98.29,78.692,-98.341,78.751,-98.333,78.774,-98.212,78.805,-98.043,78.805,-97.596,78.796,-97.382,78.783,-97.169,78.758,-96.936,78.72,-96.587,78.687,-96.475,78.665,-96.265,78.595,-96.243,78.573,-96.256,78.551,-96.204,78.531]],[[-93.171,74.161,-92.778,74.114,-92.587,74.083,-92.493,74.062,-92.314,73.992,-92.223,73.972,-91.874,74.013,-91.63,74.028,-91.088,74.009,-90.627,73.952,-90.458,73.908,-90.355,73.869,-90.381,73.825,-90.466,73.754,-90.566,73.686,-90.765,73.581,-90.934,73.528,-90.975,73.502,-91.002,73.467,-91.068,73.416,-91.249,73.304,-91.298,73.285,-91.554,73.236,-91.466,73.214,-91.426,73.195,-91.46,73.145,-91.621,73.026,-91.788,72.915,-91.905,72.849,-92.118,72.754,-92.235,72.727,-92.392,72.718,-93.341,72.802,-93.579,72.801,-94.211,72.757,-94.152,72.736,-93.92,72.703,-93.771,72.668,-93.572,72.559,-93.546,72.531,-93.534,72.499,-93.542,72.437,-93.555,72.421,-93.871,72.253,-93.973,72.13,-94.038,72.029,-94.144,72.001,-94.497,72.044,-94.611,72.042,-95.008,72.013,-95.193,72.027,-95.167,72.18,-95.193,72.345,-95.251,72.502,-95.548,72.782,-95.58,72.831,-95.602,72.884,-95.613,72.942,-95.612,72.999,-95.592,73.115,-95.589,73.174,-95.604,73.328,-95.644,73.557,-95.648,73.639,-95.645,73.671,-95.633,73.695,-95.569,73.728,-95.447,73.752,-95.386,73.755,-94.996,73.686,-94.817,73.663,-94.698,73.664,-94.691,73.671,-94.797,73.686,-94.897,73.716,-95.059,73.805,-95.134,73.881,-95.149,73.906,-95.153,73.933,-95.145,73.96,-95.121,73.985,-95.04,74.024,-94.974,74.041,-94.729,74.086,-94.483,74.113,-93.939,74.132,-93.785,74.118,-93.549,74.167,-93.41,74.179,-93.171,74.161]],[[-97.439,69.643,-97.409,69.631,-97.351,69.641,-97.306,69.673,-97.278,69.68,-97.236,69.673,-97.096,69.615,-96.989,69.554,-96.875,69.51,-96.695,69.471,-96.3,69.344,-96.184,69.259,-96.061,69.125,-95.951,69.024,-95.855,68.954,-95.751,68.898,-95.585,68.835,-95.438,68.881,-95.374,68.892,-95.32,68.873,-95.268,68.826,-95.295,68.805,-95.359,68.778,-95.466,68.747,-95.614,68.745,-95.686,68.736,-95.802,68.686,-95.895,68.627,-96.024,68.607,-96.268,68.508,-96.402,68.471,-96.599,68.461,-97.008,68.539,-97.264,68.528,-97.472,68.544,-97.705,68.626,-97.885,68.672,-98.235,68.739,-98.258,68.749,-98.273,68.772,-98.28,68.807,-98.296,68.831,-98.321,68.843,-98.376,68.842,-98.432,68.818,-98.54,68.798,-98.704,68.803,-98.775,68.817,-98.83,68.839,-98.859,68.864,-98.864,68.894,-98.879,68.916,-98.904,68.932,-98.964,68.933,-99.057,68.918,-99.094,68.899,-99.073,68.877,-99.091,68.863,-99.254,68.863,-99.318,68.876,-99.441,68.918,-99.495,68.96,-99.564,69.034,-99.557,69.054,-99.513,69.1,-99.456,69.131,-99.085,69.15,-98.912,69.168,-98.724,69.219,-98.504,69.308,-98.456,69.335,-98.45,69.354,-98.467,69.375,-98.535,69.426,-98.559,69.461,-98.537,69.478,-98.448,69.48,-98.495,69.499,-98.534,69.527,-98.548,69.545,-98.546,69.573,-98.476,69.579,-98.389,69.565,-98.222,69.485,-98.156,69.469,-98.041,69.457,-98.163,69.512,-98.289,69.629,-98.304,69.669,-98.301,69.692,-98.268,69.754,-98.239,69.78,-98.2,69.797,-98.081,69.833,-97.889,69.858,-97.791,69.862,-97.691,69.841,-97.604,69.802,-97.411,69.738,-97.383,69.712,-97.386,69.7,-97.46,69.683,-97.469,69.667,-97.439,69.643]],[[-61.105,45.945,-61.071,45.937,-60.937,45.986,-60.865,45.983,-60.868,45.949,-60.984,45.911,-61.038,45.882,-60.971,45.856,-60.972,45.838,-61.052,45.795,-61.092,45.748,-61.059,45.703,-60.93,45.748,-60.878,45.748,-60.806,45.738,-60.738,45.751,-60.699,45.773,-60.472,45.947,-60.461,45.969,-60.705,45.933,-60.733,45.957,-60.573,46.061,-60.586,46.117,-60.505,46.204,-60.431,46.256,-60.377,46.285,-60.298,46.311,-60.244,46.27,-60.226,46.196,-60.092,46.206,-59.961,46.191,-59.865,46.16,-59.85,46.141,-59.849,46.113,-59.881,46.062,-59.934,46.019,-59.828,45.965,-59.842,45.942,-60.016,45.88,-60.114,45.819,-60.205,45.743,-60.386,45.655,-60.673,45.591,-60.764,45.591,-60.872,45.611,-60.979,45.606,-61.084,45.582,-61.186,45.585,-61.236,45.573,-61.284,45.574,-61.323,45.598,-61.408,45.669,-61.45,45.716,-61.495,45.941,-61.481,46.06,-61.409,46.17,-61.302,46.244,-61.241,46.303,-60.983,46.65,-60.932,46.729,-60.87,46.797,-60.76,46.863,-60.617,46.976,-60.571,46.999,-60.489,47.01,-60.408,47.004,-60.431,46.963,-60.425,46.923,-60.332,46.768,-60.333,46.737,-60.384,46.613,-60.482,46.414,-60.508,46.303,-60.495,46.27,-60.534,46.215,-60.577,46.172,-60.745,46.093,-60.831,46.074,-60.912,46.045,-61.105,45.945]],[[-67.125,45.169,-67.171,45.182,-67.213,45.193,-67.25,45.201,-67.271,45.187,-67.291,45.168,-67.315,45.154,-67.367,45.174,-67.4,45.21,-67.453,45.248,-67.473,45.276,-67.462,45.309,-67.439,45.34,-67.428,45.378,-67.454,45.421,-67.477,45.446,-67.494,45.474,-67.488,45.501,-67.455,45.514,-67.424,45.53,-67.414,45.566,-67.433,45.603,-67.487,45.618,-67.531,45.613,-67.596,45.621,-67.658,45.644,-67.699,45.671,-67.731,45.686,-67.755,45.686,-67.785,45.702,-67.802,45.728,-67.8,45.77,-67.792,45.796,-67.775,45.818,-67.774,45.843,-67.781,45.86,-67.782,45.874,-67.778,45.892,-67.767,45.927,-67.785,45.953,-67.786,46.042,-67.79,46.209,-67.793,46.337,-67.796,46.498,-67.798,46.616,-67.8,46.78,-67.803,46.936,-67.807,47.083,-67.935,47.168,-68.097,47.275,-68.235,47.346,-68.311,47.354,-68.358,47.345,-68.377,47.316,-68.48,47.286,-68.669,47.253,-68.829,47.203,-68.887,47.203,-68.937,47.211,-69.003,47.236,-69.049,47.274,-69.064,47.338,-69.05,47.427,-69.146,47.445,-69.243,47.463,-69.302,47.402,-69.359,47.351,-69.471,47.239,-69.63,47.081,-69.718,46.995,-69.872,46.843,-70.008,46.709,-70.038,46.571,-70.067,46.441,-70.18,46.342,-70.248,46.251,-70.279,46.15,-70.304,46.057,-70.306,45.98,-70.287,45.939,-70.296,45.906,-70.333,45.868,-70.408,45.802,-70.421,45.738,-70.467,45.707,-70.596,45.644,-70.702,45.551,-70.707,45.499,-70.692,45.455,-70.69,45.428,-70.711,45.409,-70.753,45.411,-70.799,45.405,-70.838,45.366,-70.837,45.311,-70.865,45.271,-70.898,45.262,-70.926,45.291,-70.96,45.333,-71,45.337,-71.06,45.309,-71.135,45.263,-71.202,45.26,-71.327,45.29,-71.419,45.2,-71.518,45.008,-71.934,45.007,-72.35,45.007,-72.766,45.006,-73.182,45.006,-73.598,45.005,-74.014,45.005,-74.43,45.004,-74.663,45.004,-74.709,45.004,-74.566,45.042,-74.269,45.188,-74.05,45.241,-73.765,45.395,-73.558,45.425,-73.519,45.459,-73.484,45.587,-73.465,45.632,-73.369,45.758,-73.253,45.864,-73.16,46.01,-72.99,46.104,-72.733,46.182,-72.496,46.353,-72.366,46.405,-72.24,46.442,-72.187,46.512,-72.109,46.551,-71.901,46.632,-71.671,46.654,-71.439,46.721,-71.261,46.756,-71.152,46.819,-70.993,46.852,-70.519,47.033,-70.388,47.117,-70.218,47.29,-70.07,47.378,-70.017,47.471,-69.802,47.623,-69.581,47.824,-69.471,47.967,-69.306,48.047,-68.987,48.275,-68.816,48.366,-68.746,48.376,-68.552,48.457,-68.431,48.542,-68.238,48.626,-67.889,48.731,-67.561,48.856,-67.117,48.964,-66.598,49.126,-66.178,49.213,-65.883,49.226,-65.523,49.266,-65.396,49.262,-64.836,49.192,-64.568,49.105,-64.262,48.922,-64.216,48.874,-64.209,48.806,-64.371,48.839,-64.514,48.841,-64.415,48.804,-64.246,48.691,-64.254,48.55,-64.349,48.423,-64.633,48.36,-64.706,48.311,-64.765,48.228,-64.822,48.196,-64.96,48.16,-65.036,48.106,-65.259,48.021,-65.36,48.011,-65.476,48.031,-65.755,48.112,-65.927,48.189,-66.013,48.147,-66.083,48.103,-66.249,48.117,-66.324,48.098,-66.449,48.12,-66.704,48.022,-66.632,48.011,-66.429,48.067,-66.36,48.061,-66.21,47.989,-65.849,47.911,-65.756,47.86,-65.666,47.696,-65.607,47.67,-65.483,47.687,-65.344,47.768,-65.228,47.811,-65.002,47.847,-65.046,47.793,-64.874,47.797,-64.703,47.725,-64.766,47.673,-64.852,47.57,-64.912,47.369,-65.086,47.234,-65.319,47.101,-65.26,47.069,-65.192,47.05,-65.042,47.089,-64.942,47.086,-64.831,47.061,-64.866,46.958,-64.906,46.888,-64.883,46.823,-64.817,46.699,-64.726,46.671,-64.69,46.512,-64.641,46.426,-64.648,46.356,-64.557,46.311,-64.542,46.24,-64.212,46.22,-64.145,46.193,-63.916,46.166,-63.873,46.146,-63.832,46.107,-64.056,46.021,-63.875,45.959,-63.703,45.858,-63.568,45.878,-63.509,45.875,-63.358,45.811,-63.316,45.78,-63.293,45.752,-63.217,45.758,-63.108,45.782,-62.911,45.776,-62.701,45.741,-62.718,45.686,-62.75,45.648,-62.586,45.661,-62.483,45.622,-62.447,45.641,-62.422,45.665,-62.218,45.731,-61.956,45.868,-61.924,45.851,-61.912,45.799,-61.877,45.714,-61.777,45.656,-61.657,45.642,-61.492,45.687,-61.428,45.648,-61.35,45.574,-61.277,45.476,-61.282,45.441,-61.376,45.411,-61.461,45.367,-61.107,45.349,-61.071,45.33,-61.032,45.292,-61.068,45.253,-61.101,45.233,-61.165,45.256,-61.284,45.235,-61.387,45.185,-61.498,45.157,-61.569,45.154,-61.647,45.131,-61.719,45.094,-61.794,45.084,-62.027,44.994,-62.265,44.936,-62.514,44.844,-62.768,44.785,-63.032,44.715,-63.089,44.709,-63.156,44.711,-63.306,44.643,-63.381,44.652,-63.457,44.64,-63.544,44.655,-63.604,44.683,-63.558,44.611,-63.545,44.544,-63.568,44.514,-63.61,44.48,-63.761,44.486,-63.821,44.511,-63.891,44.546,-63.924,44.604,-64,44.645,-64.045,44.588,-64.045,44.545,-64.101,44.487,-64.167,44.587,-64.286,44.55,-64.339,44.445,-64.312,44.415,-64.276,44.334,-64.335,44.292,-64.378,44.304,-64.469,44.185,-64.578,44.142,-64.692,44.021,-64.826,43.929,-64.862,43.868,-65.087,43.727,-65.172,43.731,-65.235,43.727,-65.33,43.668,-65.344,43.55,-65.386,43.565,-65.429,43.561,-65.45,43.524,-65.482,43.518,-65.564,43.553,-65.662,43.534,-65.738,43.561,-65.835,43.734,-65.887,43.795,-65.978,43.815,-66.002,43.778,-66.038,43.742,-66.126,43.814,-66.193,44.08,-66.193,44.144,-66.1,44.367,-65.868,44.569,-65.942,44.576,-66.146,44.436,-66.125,44.47,-66.091,44.505,-66.022,44.562,-65.917,44.615,-65.778,44.646,-65.682,44.651,-65.616,44.68,-65.52,44.733,-65.502,44.76,-65.587,44.729,-65.728,44.697,-65.692,44.738,-65.657,44.76,-64.903,45.121,-64.751,45.18,-64.449,45.256,-64.407,45.306,-64.448,45.337,-64.331,45.309,-64.34,45.268,-64.359,45.238,-64.366,45.187,-64.354,45.138,-64.235,45.114,-64.135,45.023,-64.183,45.147,-64.093,45.217,-63.748,45.311,-63.46,45.321,-63.368,45.365,-63.614,45.394,-63.906,45.378,-64.087,45.411,-64.336,45.39,-64.6,45.41,-64.681,45.383,-64.747,45.324,-64.832,45.35,-64.873,45.355,-64.913,45.375,-64.827,45.476,-64.56,45.625,-64.397,45.756,-64.351,45.783,-64.315,45.836,-64.404,45.827,-64.482,45.806,-64.536,45.867,-64.633,45.947,-64.642,45.913,-64.594,45.814,-64.779,45.638,-64.898,45.626,-65.057,45.544,-65.282,45.473,-65.545,45.337,-65.884,45.223,-65.956,45.222,-66.11,45.317,-66.067,45.359,-66.027,45.418,-66.065,45.401,-66.09,45.376,-66.183,45.335,-66.107,45.257,-66.144,45.228,-66.252,45.189,-66.352,45.133,-66.44,45.096,-66.511,45.143,-66.707,45.083,-66.872,45.067,-66.908,45.098,-66.919,45.146,-66.977,45.157,-67.084,45.144,-67.125,45.169]],[[-55.459,51.537,-55.532,51.437,-55.583,51.389,-55.631,51.373,-55.731,51.359,-55.941,51.343,-56.031,51.328,-56.044,51.262,-56.031,51.227,-56,51.199,-55.961,51.191,-55.874,51.208,-55.841,51.205,-55.815,51.191,-55.796,51.166,-55.785,51.131,-55.785,51.087,-55.8,51.033,-55.871,50.907,-55.962,50.838,-56.078,50.781,-56.107,50.759,-56.121,50.734,-56.136,50.651,-56.196,50.585,-56.382,50.417,-56.454,50.38,-56.455,50.35,-56.484,50.271,-56.539,50.207,-56.694,50.06,-56.732,50.008,-56.75,49.967,-56.747,49.908,-56.754,49.883,-56.79,49.834,-56.839,49.788,-56.849,49.765,-56.829,49.725,-56.809,49.71,-56.807,49.673,-56.822,49.613,-56.757,49.652,-56.611,49.788,-56.501,49.87,-56.428,49.897,-56.376,49.934,-56.322,50.014,-56.247,50.09,-56.179,50.115,-56.148,50.1,-56.122,50.063,-56.127,50.015,-56.164,49.957,-56.161,49.94,-56.075,49.983,-55.927,50.018,-55.873,50.013,-55.765,49.96,-55.674,49.967,-55.53,49.997,-55.503,49.983,-55.527,49.937,-55.584,49.892,-55.718,49.829,-56.04,49.705,-56.14,49.619,-56.121,49.622,-56.052,49.658,-55.979,49.678,-55.902,49.681,-55.87,49.67,-55.882,49.646,-55.892,49.58,-56.087,49.452,-56.041,49.457,-55.815,49.515,-55.678,49.435,-55.49,49.463,-55.376,49.49,-55.379,49.473,-55.354,49.438,-55.355,49.381,-55.344,49.373,-55.29,49.392,-55.28,49.413,-55.283,49.514,-55.266,49.524,-55.23,49.508,-55.207,49.482,-55.2,49.408,-55.225,49.335,-55.259,49.267,-55.342,49.168,-55.332,49.126,-55.353,49.079,-55.335,49.078,-55.252,49.121,-55.247,49.139,-55.254,49.18,-55.245,49.2,-55.176,49.244,-55.063,49.297,-55.026,49.305,-55.01,49.293,-55.016,49.26,-54.983,49.268,-54.911,49.316,-54.844,49.345,-54.782,49.355,-54.718,49.389,-54.651,49.445,-54.579,49.491,-54.502,49.527,-54.469,49.53,-54.481,49.469,-54.465,49.401,-54.463,49.342,-54.448,49.329,-54.389,49.392,-54.356,49.415,-54.317,49.424,-54.271,49.419,-53.958,49.442,-53.862,49.426,-53.755,49.385,-53.619,49.322,-53.57,49.264,-53.56,49.192,-53.573,49.141,-53.671,49.078,-53.758,49.035,-53.809,48.993,-53.825,48.951,-53.845,48.925,-53.903,48.889,-54.161,48.788,-54.1,48.785,-53.951,48.807,-53.853,48.811,-53.848,48.797,-53.887,48.768,-53.962,48.739,-53.97,48.725,-53.966,48.707,-53.886,48.685,-53.784,48.695,-53.698,48.68,-53.706,48.656,-53.775,48.576,-53.795,48.526,-53.886,48.485,-54.068,48.419,-54.114,48.394,-54.104,48.388,-53.937,48.437,-53.853,48.449,-53.799,48.449,-53.739,48.496,-53.644,48.511,-53.552,48.482,-53.411,48.562,-53.361,48.573,-53.275,48.563,-53.22,48.578,-53.127,48.633,-53.057,48.659,-53.043,48.657,-53.028,48.635,-53.021,48.572,-53.037,48.516,-53.06,48.48,-53.136,48.402,-53.182,48.374,-53.225,48.364,-53.301,48.368,-53.334,48.356,-53.406,48.294,-53.531,48.232,-53.61,48.208,-53.56,48.174,-53.542,48.108,-53.569,48.088,-53.704,48.068,-53.71,48.057,-53.758,48.042,-53.87,48.02,-53.794,48.01,-53.653,48.026,-53.638,48.015,-53.658,47.969,-53.695,47.921,-53.862,47.799,-53.864,47.787,-53.838,47.727,-53.805,47.682,-53.765,47.65,-53.672,47.648,-53.604,47.662,-53.504,47.744,-53.283,47.998,-53.085,48.069,-52.921,48.147,-52.883,48.131,-52.866,48.113,-52.872,48.094,-52.955,48.029,-52.998,47.976,-53.111,47.812,-53.154,47.735,-53.176,47.653,-53.17,47.512,-53.158,47.488,-53.122,47.455,-53.057,47.483,-52.945,47.553,-52.873,47.619,-52.817,47.728,-52.782,47.769,-52.745,47.769,-52.711,47.745,-52.703,47.693,-52.672,47.622,-52.654,47.549,-52.669,47.47,-52.684,47.426,-52.912,47.103,-52.888,47.046,-52.882,47.011,-52.889,46.974,-52.962,46.819,-53.032,46.723,-53.07,46.681,-53.115,46.656,-53.167,46.646,-53.214,46.66,-53.255,46.698,-53.291,46.717,-53.323,46.718,-53.382,46.711,-53.536,46.633,-53.568,46.628,-53.59,46.639,-53.616,46.68,-53.595,46.888,-53.581,46.957,-53.612,47.01,-53.58,47.099,-53.578,47.133,-53.597,47.146,-53.636,47.138,-53.695,47.093,-53.774,47.012,-53.86,46.939,-54.01,46.84,-54.076,46.82,-54.102,46.825,-54.133,46.839,-54.174,46.88,-54.173,46.917,-54.155,46.967,-54.093,47.086,-53.971,47.262,-53.869,47.387,-53.85,47.44,-53.878,47.464,-53.901,47.509,-53.94,47.645,-53.989,47.756,-54.047,47.806,-54.192,47.86,-54.218,47.847,-54.234,47.772,-54.405,47.556,-54.434,47.462,-54.456,47.428,-54.488,47.404,-54.563,47.375,-54.542,47.425,-54.463,47.536,-54.474,47.547,-54.575,47.458,-54.651,47.408,-54.745,47.395,-54.802,47.399,-54.857,47.385,-55.09,47.174,-55.099,47.104,-55.14,47.046,-55.255,46.942,-55.316,46.906,-55.401,46.899,-55.479,46.917,-55.531,46.914,-55.652,46.881,-55.789,46.867,-55.845,46.874,-55.881,46.887,-55.95,46.928,-55.958,46.956,-55.954,46.973,-55.919,47.017,-55.838,47.072,-55.772,47.092,-55.61,47.12,-55.492,47.161,-55.401,47.221,-55.361,47.259,-55.191,47.449,-54.976,47.516,-54.87,47.571,-54.795,47.64,-54.785,47.665,-54.891,47.629,-54.946,47.621,-55.035,47.634,-55.075,47.658,-55.197,47.65,-55.366,47.661,-55.391,47.643,-55.413,47.55,-55.435,47.501,-55.461,47.485,-55.499,47.475,-55.576,47.465,-55.775,47.498,-55.811,47.516,-55.862,47.53,-56.081,47.5,-56.127,47.503,-56.084,47.525,-55.867,47.592,-55.844,47.788,-55.858,47.819,-55.918,47.792,-56.02,47.764,-56.09,47.772,-56.121,47.789,-56.151,47.775,-56.221,47.671,-56.263,47.658,-56.326,47.654,-56.46,47.617,-56.722,47.592,-56.774,47.565,-56.952,47.574,-57.473,47.631,-57.66,47.625,-57.884,47.66,-57.926,47.675,-58.239,47.669,-58.333,47.677,-58.327,47.72,-58.337,47.731,-58.428,47.683,-58.509,47.653,-58.613,47.626,-58.941,47.58,-59.117,47.571,-59.219,47.603,-59.26,47.634,-59.321,47.737,-59.362,47.866,-59.362,47.889,-59.341,47.934,-59.272,47.996,-58.961,48.159,-58.711,48.325,-58.605,48.411,-58.503,48.442,-58.336,48.514,-58.33,48.522,-58.492,48.513,-58.606,48.533,-58.723,48.541,-58.944,48.522,-59.167,48.522,-59.168,48.558,-59.063,48.628,-58.842,48.746,-58.819,48.747,-58.887,48.692,-58.906,48.65,-58.877,48.623,-58.843,48.605,-58.716,48.598,-58.687,48.622,-58.642,48.749,-58.546,48.897,-58.494,49.003,-58.404,49.084,-58.359,49.097,-58.319,49.081,-58.186,49.062,-58.05,48.988,-58.006,48.981,-57.991,48.988,-58.041,49.01,-58.082,49.045,-58.099,49.077,-58.049,49.18,-57.991,49.209,-57.98,49.23,-58.097,49.23,-58.191,49.259,-58.219,49.305,-58.213,49.387,-58.183,49.435,-58.107,49.5,-58.016,49.542,-57.961,49.532,-57.856,49.474,-57.791,49.49,-57.799,49.509,-57.897,49.6,-57.929,49.668,-57.926,49.701,-57.712,50.025,-57.608,50.199,-57.466,50.464,-57.433,50.506,-57.36,50.584,-57.331,50.605,-57.237,50.605,-57.18,50.615,-57.264,50.649,-57.294,50.673,-57.298,50.699,-57.275,50.725,-57.242,50.745,-57.132,50.787,-57.053,50.857,-57.006,50.94,-57.013,50.968,-57.037,50.996,-57.036,51.011,-56.976,51.028,-56.825,51.126,-56.805,51.144,-56.75,51.275,-56.682,51.333,-56.619,51.362,-56.518,51.399,-56.207,51.489,-56.026,51.568,-55.902,51.564,-55.866,51.508,-55.69,51.471,-55.66,51.511,-55.701,51.559,-55.666,51.579,-55.522,51.596,-55.496,51.59,-55.453,51.562,-55.459,51.537]],[[-86.589,71.011,-86.55,70.989,-86.321,71.017,-86.127,71.049,-85.825,71.126,-85.644,71.152,-85.095,71.152,-85.002,71.137,-85.043,71.092,-85.066,71.079,-85.048,71.059,-84.988,71.032,-84.87,71.002,-84.824,71.029,-84.79,71.093,-84.709,71.359,-84.674,71.439,-84.658,71.515,-84.66,71.586,-84.699,71.631,-84.84,71.659,-85.032,71.654,-85.131,71.661,-85.25,71.675,-85.339,71.697,-85.397,71.727,-85.512,71.817,-85.596,71.866,-85.813,71.956,-85.912,71.987,-85.862,72.022,-85.665,72.063,-85.546,72.102,-85.406,72.215,-85.322,72.233,-85.019,72.218,-84.608,72.129,-84.352,72.053,-84.284,72.044,-84.282,72.058,-84.347,72.094,-84.643,72.19,-84.778,72.259,-84.842,72.308,-84.811,72.33,-84.645,72.351,-84.623,72.377,-84.849,72.406,-84.964,72.406,-85.057,72.384,-85.156,72.383,-85.341,72.422,-85.391,72.444,-85.498,72.511,-85.554,72.569,-85.616,72.605,-85.638,72.633,-85.65,72.722,-85.645,72.774,-85.619,72.819,-85.575,72.856,-85.455,72.925,-85.388,72.945,-85.262,72.954,-84.99,72.92,-84.257,72.797,-84.274,72.836,-85.094,73.003,-85.384,73.045,-85.455,73.105,-85.018,73.335,-84.616,73.39,-84.416,73.456,-84.089,73.459,-83.782,73.417,-83.777,73.428,-83.915,73.508,-83.904,73.528,-83.73,73.576,-83.41,73.632,-83.02,73.676,-82.943,73.699,-82.843,73.715,-82.66,73.73,-82.203,73.736,-81.946,73.73,-81.605,73.696,-81.406,73.635,-81.344,73.598,-81.238,73.48,-81.152,73.314,-81.025,73.245,-80.822,73.207,-80.681,73.166,-80.603,73.121,-80.583,73.065,-80.619,72.997,-80.592,72.928,-80.501,72.857,-80.431,72.816,-80.277,72.77,-80.275,72.746,-80.323,72.717,-80.424,72.679,-80.675,72.559,-80.999,72.426,-81.229,72.312,-81.241,72.278,-80.761,72.457,-80.611,72.451,-80.605,72.426,-80.702,72.338,-80.821,72.26,-80.941,72.21,-80.919,72.191,-80.691,72.103,-80.733,72.089,-80.843,72.096,-80.888,72.088,-80.921,72.072,-80.941,72.048,-80.943,72.014,-80.925,71.971,-80.927,71.938,-80.948,71.917,-80.925,71.908,-80.858,71.911,-80.802,71.929,-80.705,71.988,-80.386,72.149,-80.182,72.209,-80.116,72.214,-79.928,72.175,-79.884,72.177,-80.091,72.301,-80.109,72.332,-80.067,72.378,-80.042,72.394,-79.927,72.428,-79.831,72.446,-79.778,72.439,-79.693,72.376,-79.654,72.332,-79.584,72.315,-79.427,72.337,-79.323,72.391,-79.194,72.356,-79,72.272,-79.018,72.188,-79.018,72.104,-79.008,72.043,-78.776,71.93,-78.614,71.881,-78.585,71.881,-78.589,71.898,-78.623,71.935,-78.711,71.972,-78.791,72.03,-78.863,72.101,-78.82,72.265,-78.699,72.351,-78.582,72.329,-78.429,72.28,-78.307,72.275,-78.116,72.28,-77.726,72.18,-77.517,72.178,-77.536,72.219,-77.694,72.238,-77.926,72.294,-78.287,72.36,-78.453,72.435,-78.484,72.471,-78.479,72.509,-78.459,72.542,-78.422,72.572,-78.35,72.6,-78.001,72.688,-77.753,72.725,-77.567,72.737,-77.255,72.736,-76.894,72.721,-76.698,72.695,-76.473,72.633,-76.189,72.572,-76.087,72.561,-75.969,72.563,-75.833,72.577,-75.704,72.572,-75.294,72.481,-75.186,72.434,-75.12,72.378,-75.071,72.323,-75.04,72.27,-75.053,72.226,-75.394,72.04,-75.543,72.008,-75.641,71.937,-75.787,71.803,-75.911,71.731,-75.923,71.717,-75.897,71.714,-75.822,71.746,-75.693,71.839,-75.6,71.918,-75.428,71.984,-75.148,72.063,-74.903,72.1,-74.695,72.097,-74.52,72.086,-74.377,72.067,-74.293,72.051,-74.266,72.038,-74.209,71.979,-74.213,71.939,-74.248,71.894,-74.316,71.843,-74.621,71.786,-74.789,71.742,-74.893,71.726,-75.205,71.709,-75.191,71.692,-74.959,71.667,-74.701,71.676,-74.707,71.647,-74.829,71.571,-74.868,71.505,-74.834,71.451,-74.841,71.407,-74.931,71.314,-75.035,71.231,-74.996,71.218,-74.759,71.338,-74.695,71.469,-74.6,71.585,-74.488,71.648,-74.404,71.673,-74.139,71.682,-73.992,71.75,-73.867,71.771,-73.814,71.771,-73.707,71.746,-73.714,71.72,-73.869,71.599,-74.197,71.404,-74.063,71.426,-73.973,71.473,-73.851,71.519,-73.713,71.588,-73.622,71.526,-73.482,71.479,-73.398,71.373,-73.262,71.322,-73.181,71.283,-73.192,71.35,-73.31,71.484,-73.278,71.538,-73.187,71.565,-72.902,71.678,-72.703,71.64,-72.581,71.607,-72.519,71.616,-72.365,71.611,-72.117,71.593,-71.875,71.561,-71.641,71.516,-71.46,71.464,-71.333,71.403,-71.256,71.362,-71.229,71.339,-71.187,71.279,-71.219,71.239,-71.397,71.147,-71.495,71.105,-71.593,71.086,-71.856,71.105,-71.938,71.094,-72.024,71.065,-72.298,70.939,-72.449,70.884,-72.598,70.849,-72.633,70.831,-72.313,70.833,-72.224,70.87,-72.15,70.941,-72.009,71.013,-71.743,71.047,-71.371,70.975,-71.186,70.978,-71.045,71.05,-70.888,71.099,-70.826,71.109,-70.792,71.103,-70.673,71.052,-70.636,71.007,-70.639,70.902,-70.655,70.871,-70.762,70.792,-71.022,70.674,-71.192,70.63,-71.38,70.606,-71.586,70.566,-71.658,70.534,-71.729,70.488,-71.8,70.457,-71.89,70.432,-71.772,70.394,-71.727,70.395,-71.684,70.418,-71.565,70.506,-71.477,70.544,-71.427,70.552,-71.375,70.548,-71.325,70.531,-71.276,70.5,-71.28,70.425,-71.429,70.128,-71.405,70.129,-71.313,70.209,-71.045,70.519,-70.98,70.581,-70.851,70.644,-70.561,70.738,-70.337,70.788,-70.085,70.83,-69.95,70.845,-69.796,70.835,-69.696,70.786,-69.56,70.777,-69.395,70.789,-69.289,70.783,-69.169,70.764,-69.066,70.728,-68.891,70.687,-68.496,70.61,-68.447,70.594,-68.401,70.565,-68.358,70.523,-68.364,70.481,-68.417,70.44,-68.483,70.415,-68.561,70.406,-68.643,70.383,-68.794,70.324,-68.843,70.314,-69.079,70.289,-69.299,70.277,-69.436,70.253,-69.699,70.189,-70.061,70.071,-70.058,70.043,-69.913,70.029,-69.796,70.047,-69.635,70.129,-69.483,70.16,-69.246,70.185,-68.919,70.207,-68.778,70.204,-68.753,70.199,-68.735,70.18,-68.723,70.146,-68.777,70.101,-68.839,70.08,-69.008,69.979,-68.897,69.953,-68.744,69.941,-68.657,69.968,-68.578,70.03,-68.489,70.065,-68.391,70.072,-68.305,70.087,-68.231,70.112,-68.21,70.128,-68.319,70.161,-68.327,70.18,-68.283,70.228,-68.204,70.281,-68.121,70.315,-68.059,70.317,-67.855,70.282,-67.716,70.22,-67.364,70.034,-67.318,69.998,-67.196,69.861,-67.173,69.799,-67.193,69.757,-67.222,69.731,-67.259,69.721,-67.337,69.721,-67.806,69.777,-68.02,69.77,-68.114,69.754,-68.189,69.731,-68.248,69.701,-68.29,69.665,-68.372,69.644,-68.67,69.644,-68.837,69.624,-69.125,69.575,-69.228,69.547,-69.251,69.512,-69.075,69.518,-68.785,69.564,-68.513,69.577,-68.058,69.476,-67.908,69.46,-67.825,69.475,-67.725,69.479,-67.361,69.473,-67.237,69.46,-67.053,69.421,-66.771,69.337,-66.717,69.312,-66.685,69.286,-66.676,69.258,-66.679,69.191,-66.707,69.168,-66.803,69.153,-67.208,69.171,-67.332,69.185,-67.484,69.167,-67.607,69.173,-67.765,69.2,-67.938,69.248,-68.198,69.203,-68.406,69.232,-68.619,69.206,-69.041,69.098,-68.993,69.079,-68.416,69.172,-68.304,69.166,-68.121,69.133,-67.833,69.066,-67.752,69.039,-67.751,68.934,-67.795,68.863,-67.883,68.784,-68.016,68.795,-68.324,68.844,-68.45,68.851,-68.543,68.843,-68.667,68.811,-68.725,68.81,-69.219,68.873,-69.33,68.876,-69.343,68.869,-69.319,68.857,-68.871,68.76,-68.541,68.749,-68.333,68.733,-68.21,68.703,-68.152,68.681,-68.148,68.616,-68.038,68.551,-67.938,68.524,-67.875,68.523,-67.766,68.547,-67.656,68.551,-67.567,68.534,-67.456,68.498,-67.321,68.488,-67.202,68.466,-67.111,68.461,-66.854,68.472,-66.743,68.458,-66.714,68.446,-66.762,68.425,-66.997,68.374,-67.033,68.326,-66.9,68.264,-66.831,68.216,-66.834,68.18,-66.905,68.098,-66.923,68.066,-66.9,68.063,-66.729,68.129,-66.702,68.121,-66.685,68.029,-66.663,68.034,-66.605,68.11,-66.631,68.211,-66.531,68.25,-66.212,68.28,-66.266,68.123,-66.275,68.041,-66.414,67.904,-66.53,67.86,-66.526,67.851,-66.444,67.834,-66.392,67.832,-66.343,67.853,-66.225,67.959,-65.986,68.069,-65.942,68.071,-65.944,68.031,-65.975,67.957,-65.864,67.923,-65.759,67.957,-65.702,67.987,-65.569,67.982,-65.509,67.968,-65.491,67.936,-65.552,67.799,-65.541,67.766,-65.401,67.675,-65.387,67.68,-65.413,67.724,-65.442,67.832,-65.415,67.879,-65.3,67.94,-65.064,68.026,-64.977,68.043,-64.922,68.032,-64.835,67.99,-64.863,67.965,-64.956,67.939,-65.026,67.892,-65.071,67.824,-65.021,67.788,-64.83,67.784,-64.638,67.84,-64.528,67.813,-64.396,67.74,-64.156,67.623,-64.019,67.655,-63.85,67.566,-64.077,67.496,-64.008,67.347,-64.303,67.353,-64.469,67.342,-64.58,67.355,-64.7,67.351,-64.589,67.316,-64.376,67.301,-64.356,67.256,-64.189,67.257,-64.063,67.266,-63.836,67.264,-63.824,67.316,-63.676,67.345,-63.592,67.378,-63.521,67.358,-63.316,67.336,-63.04,67.235,-63.162,67.174,-63.195,67.117,-63.236,67.069,-63.258,67.025,-63.307,66.994,-63.702,66.822,-63.636,66.821,-63.469,66.862,-63.144,66.924,-62.962,66.949,-62.833,66.933,-62.768,66.932,-62.71,66.954,-62.603,66.929,-62.38,66.905,-62.124,67.047,-61.969,67.019,-61.824,66.932,-61.515,66.778,-61.353,66.689,-61.3,66.649,-61.307,66.609,-61.453,66.567,-61.528,66.558,-61.724,66.638,-61.904,66.678,-62.014,66.674,-62.123,66.643,-62.089,66.626,-61.653,66.503,-61.576,66.413,-61.571,66.373,-61.863,66.313,-61.956,66.309,-62.158,66.338,-62.277,66.392,-62.375,66.411,-62.51,66.417,-62.553,66.407,-62.406,66.316,-62.42,66.289,-62.496,66.271,-62.534,66.227,-62.242,66.148,-62.024,66.068,-61.992,66.035,-62.139,66.011,-62.244,66.006,-62.468,66.017,-62.59,66.034,-62.624,66.016,-62.497,65.974,-62.448,65.946,-62.41,65.906,-62.388,65.868,-62.382,65.833,-62.486,65.804,-62.61,65.724,-62.659,65.64,-62.772,65.632,-62.817,65.648,-62.969,65.622,-63.169,65.657,-63.241,65.696,-63.459,65.853,-63.464,65.835,-63.41,65.756,-63.421,65.709,-63.652,65.674,-63.651,65.661,-63.509,65.636,-63.337,65.617,-63.364,65.543,-63.363,65.23,-63.402,65.118,-63.486,65.021,-63.607,64.928,-63.737,64.989,-63.789,65.051,-63.833,65.083,-63.896,65.109,-63.976,65.121,-64.061,65.122,-64.152,65.066,-64.25,65.114,-64.346,65.172,-64.31,65.325,-64.27,65.401,-64.286,65.4,-64.34,65.364,-64.47,65.253,-64.555,65.117,-64.665,65.169,-64.765,65.234,-64.847,65.3,-64.98,65.375,-65.108,65.464,-65.176,65.568,-65.207,65.59,-65.282,65.677,-65.311,65.702,-65.337,65.71,-65.402,65.764,-65.378,65.822,-65.277,65.891,-65.185,65.94,-65.032,65.989,-64.854,66.016,-64.773,66.079,-64.673,66.193,-64.564,66.272,-64.445,66.317,-64.504,66.325,-64.655,66.287,-64.761,66.231,-64.887,66.137,-65.004,66.078,-65.305,66.008,-65.416,65.995,-65.544,65.987,-65.826,65.997,-65.891,66.02,-65.857,66.086,-65.656,66.205,-65.688,66.213,-65.759,66.171,-65.856,66.142,-65.94,66.127,-66.064,66.133,-66.209,66.206,-66.277,66.229,-66.419,66.254,-66.477,66.28,-66.712,66.46,-66.76,66.508,-66.787,66.556,-66.863,66.595,-66.986,66.627,-67.015,66.622,-66.97,66.582,-66.969,66.547,-67.077,66.525,-67.19,66.533,-67.307,66.57,-67.318,66.52,-67.192,66.433,-67.19,66.322,-67.225,66.31,-67.311,66.304,-67.369,66.317,-67.56,66.4,-67.741,66.458,-67.868,66.49,-67.883,66.467,-67.801,66.367,-67.704,66.269,-67.547,66.187,-67.297,66.09,-67.183,66.034,-67.273,65.956,-67.35,65.93,-67.399,65.922,-67.551,65.922,-67.828,65.965,-67.958,66.014,-68.147,66.13,-68.46,66.249,-68.528,66.249,-68.749,66.2,-68.714,66.192,-68.572,66.189,-68.467,66.173,-68.217,66.079,-68.198,66.039,-68.261,65.995,-68.257,65.939,-68.187,65.871,-68.115,65.828,-67.968,65.797,-67.894,65.793,-67.866,65.774,-67.954,65.623,-67.962,65.582,-67.937,65.565,-67.906,65.563,-67.717,65.625,-67.638,65.64,-67.57,65.644,-67.49,65.626,-67.4,65.588,-67.346,65.549,-67.33,65.509,-67.303,65.483,-67.118,65.44,-67.135,65.421,-67.326,65.357,-67.337,65.347,-67.298,65.342,-67.178,65.304,-67.067,65.244,-66.999,65.173,-66.985,65.138,-66.986,65.105,-66.97,65.085,-66.912,65.081,-66.887,65.094,-66.861,65.092,-66.831,65.074,-66.8,65.02,-66.733,64.86,-66.697,64.815,-66.677,64.814,-66.667,64.974,-66.635,65,-66.518,64.972,-66.345,64.91,-66.224,64.854,-66.21,64.828,-66.302,64.778,-66.282,64.755,-66.215,64.722,-66.152,64.735,-66.108,64.791,-66.03,64.847,-65.939,64.886,-65.768,64.854,-65.627,64.771,-65.605,64.742,-65.513,64.706,-65.432,64.726,-65.275,64.632,-65.349,64.589,-65.513,64.526,-65.529,64.505,-65.49,64.51,-65.179,64.51,-65.095,64.485,-65.075,64.437,-65.213,64.303,-65.34,64.315,-65.507,64.318,-65.594,64.311,-65.58,64.294,-65.348,64.232,-65.282,64.182,-65.193,64.13,-65.15,64.087,-65.151,64.068,-65.187,64.038,-65.17,64.028,-65.011,64.009,-64.912,64.026,-64.788,64.033,-64.678,64.028,-64.67,64.01,-64.686,63.961,-64.798,63.916,-64.768,63.905,-64.637,63.918,-64.576,63.897,-64.498,63.79,-64.411,63.706,-64.482,63.687,-64.562,63.68,-64.55,63.573,-64.499,63.463,-64.498,63.358,-64.514,63.264,-64.587,63.243,-64.665,63.245,-64.696,63.269,-64.886,63.549,-64.933,63.599,-64.99,63.643,-65.192,63.764,-65.184,63.745,-65.134,63.689,-65.089,63.606,-65.031,63.44,-65.005,63.333,-65.017,63.293,-65.058,63.283,-65.069,63.263,-65.049,63.235,-64.895,63.126,-64.82,63.06,-64.767,62.992,-64.718,62.946,-64.672,62.922,-64.684,62.902,-64.752,62.887,-64.869,62.88,-64.923,62.889,-65.133,62.952,-65.163,62.933,-65.047,62.701,-65.05,62.646,-65.108,62.626,-65.18,62.649,-65.266,62.715,-65.397,62.788,-65.572,62.869,-65.74,62.932,-65.78,62.93,-65.806,62.912,-65.834,62.909,-65.864,62.921,-65.92,62.969,-65.979,63.001,-66.224,63.107,-66.249,63.108,-66.226,63.076,-66.201,63.006,-66.229,62.991,-66.293,62.997,-66.414,63.027,-66.496,63.097,-66.6,63.219,-66.655,63.265,-66.66,63.235,-66.631,63.119,-66.636,63.08,-66.697,63.07,-66.723,63.08,-66.749,63.111,-66.773,63.162,-66.831,63.201,-66.923,63.228,-66.975,63.256,-67,63.305,-67.018,63.317,-67.18,63.305,-67.261,63.341,-67.495,63.481,-67.709,63.634,-67.844,63.715,-67.893,63.734,-67.821,63.635,-67.743,63.489,-67.723,63.423,-67.759,63.42,-67.838,63.449,-68.244,63.637,-68.494,63.725,-68.633,63.741,-68.859,63.752,-68.911,63.703,-68.789,63.595,-68.671,63.514,-68.555,63.459,-68.374,63.352,-68.208,63.215,-68.141,63.172,-67.915,63.114,-67.797,63.098,-67.676,63.094,-67.665,63.073,-67.724,63.034,-67.737,63.01,-67.468,62.948,-67.367,62.914,-67.269,62.858,-67.213,62.844,-66.98,62.701,-66.922,62.678,-66.714,62.632,-66.645,62.602,-66.531,62.51,-66.459,62.463,-66.357,62.352,-66.281,62.303,-66.095,62.246,-66.016,62.23,-65.98,62.209,-66.004,62.158,-66.027,62.137,-66.133,62.102,-66.116,62.054,-66.056,61.967,-66.059,61.914,-66.124,61.893,-66.257,61.868,-66.324,61.87,-66.425,61.891,-66.551,61.926,-66.803,62.013,-67.181,62.073,-67.322,62.105,-67.369,62.134,-67.44,62.151,-68.379,62.235,-68.536,62.256,-68.634,62.281,-68.724,62.319,-69.082,62.405,-69.126,62.424,-69.366,62.572,-69.545,62.745,-69.605,62.768,-69.8,62.79,-69.962,62.776,-70.071,62.757,-70.236,62.763,-70.344,62.792,-70.571,62.869,-70.801,62.91,-71.002,62.978,-71.106,63.002,-71.096,63.02,-70.946,63.121,-70.993,63.119,-71.254,63.043,-71.347,63.066,-71.501,63.126,-71.617,63.187,-71.855,63.355,-71.992,63.416,-71.973,63.43,-71.819,63.435,-71.697,63.43,-71.614,63.444,-71.456,63.512,-71.387,63.555,-71.381,63.58,-71.513,63.587,-71.542,63.599,-71.566,63.627,-71.627,63.663,-71.725,63.706,-71.838,63.725,-72.223,63.709,-72.29,63.728,-72.289,63.757,-72.213,63.839,-72.172,63.872,-72.159,63.89,-72.174,63.893,-72.226,63.891,-72.45,63.818,-72.498,63.823,-72.586,63.901,-72.639,63.989,-72.678,64.02,-72.73,64.03,-72.913,64.117,-73.174,64.282,-73.27,64.333,-73.377,64.38,-73.455,64.399,-73.444,64.423,-73.278,64.56,-73.271,64.583,-73.413,64.574,-73.627,64.603,-73.728,64.568,-73.793,64.566,-73.868,64.585,-73.91,64.578,-73.95,64.466,-73.981,64.438,-74.026,64.423,-74.065,64.425,-74.099,64.444,-74.098,64.47,-74.13,64.608,-74.205,64.628,-74.416,64.633,-74.461,64.645,-74.512,64.67,-74.556,64.717,-74.593,64.786,-74.634,64.824,-74.681,64.831,-74.719,64.825,-74.748,64.807,-74.813,64.796,-74.916,64.792,-74.919,64.766,-74.823,64.717,-74.73,64.647,-74.64,64.557,-74.695,64.497,-74.894,64.466,-75.067,64.457,-75.215,64.469,-75.328,64.49,-75.488,64.541,-75.715,64.524,-75.767,64.392,-75.815,64.385,-76.032,64.388,-76.118,64.376,-76.407,64.303,-76.495,64.293,-76.562,64.302,-76.627,64.284,-76.724,64.242,-76.856,64.238,-77.024,64.271,-77.166,64.285,-77.283,64.28,-77.403,64.3,-77.527,64.344,-77.628,64.363,-77.76,64.36,-77.791,64.367,-77.985,64.461,-78.045,64.499,-78.175,64.618,-78.198,64.665,-78.201,64.715,-78.19,64.752,-78.145,64.808,-78.096,64.939,-78.055,64.983,-77.995,65.023,-77.876,65.073,-77.447,65.162,-77.361,65.197,-77.364,65.22,-77.461,65.328,-77.46,65.356,-77.428,65.372,-77.358,65.435,-77.327,65.453,-77.251,65.463,-77.094,65.431,-76.959,65.418,-76.779,65.414,-76.482,65.37,-76.067,65.285,-75.828,65.227,-75.648,65.141,-75.52,65.056,-75.502,65.013,-75.561,64.947,-75.591,64.928,-75.589,64.905,-75.556,64.879,-75.452,64.842,-75.427,64.856,-75.435,64.901,-75.414,64.939,-75.363,64.969,-75.357,65.009,-75.397,65.058,-75.446,65.1,-75.505,65.135,-75.773,65.257,-75.799,65.298,-75.709,65.316,-75.317,65.275,-75.166,65.284,-75.109,65.331,-75.048,65.364,-74.982,65.381,-74.85,65.389,-74.665,65.367,-74.575,65.364,-74.495,65.372,-74.391,65.398,-74.237,65.484,-74.138,65.503,-73.99,65.517,-73.878,65.519,-73.675,65.484,-73.551,65.485,-73.561,65.543,-73.643,65.653,-73.746,65.767,-73.826,65.805,-74.033,65.877,-74.276,66.013,-74.401,66.097,-74.434,66.139,-74.416,66.167,-74.375,66.208,-73.934,66.358,-73.584,66.507,-73.431,66.583,-73.357,66.636,-73.281,66.675,-73.201,66.699,-73.033,66.728,-72.985,66.765,-72.975,66.829,-72.947,66.883,-72.789,67.031,-72.668,67.07,-72.485,67.098,-72.365,67.133,-72.22,67.254,-72.234,67.284,-72.301,67.307,-72.353,67.342,-72.576,67.659,-72.725,67.812,-72.904,67.945,-73.063,68.107,-73.328,68.267,-73.331,68.309,-73.284,68.357,-73.307,68.368,-73.58,68.298,-73.644,68.295,-73.749,68.325,-73.821,68.363,-73.879,68.429,-73.873,68.464,-73.834,68.497,-73.783,68.578,-73.781,68.619,-73.798,68.659,-73.822,68.686,-73.852,68.701,-73.935,68.711,-74.073,68.715,-74.118,68.701,-73.966,68.579,-73.989,68.549,-74.183,68.535,-74.27,68.541,-74.35,68.556,-74.422,68.58,-74.648,68.708,-74.696,68.756,-74.681,68.79,-74.7,68.808,-74.746,68.797,-74.808,68.796,-74.893,68.808,-74.91,68.823,-74.752,68.892,-74.743,68.913,-74.816,68.936,-74.925,68.941,-74.954,68.961,-74.917,68.983,-74.769,69.021,-74.717,69.046,-74.805,69.064,-74.855,69.066,-74.954,69.025,-75.104,68.941,-75.213,68.909,-75.363,68.948,-75.457,68.961,-75.523,68.953,-75.623,68.888,-75.842,68.84,-76.235,68.728,-76.403,68.692,-76.585,68.699,-76.619,68.721,-76.616,68.76,-76.604,68.792,-76.582,68.816,-76.575,68.847,-76.588,68.974,-76.557,69.009,-76.495,69.03,-76.381,69.052,-76.089,69.026,-75.954,69.031,-75.859,69.06,-75.763,69.103,-75.668,69.159,-75.648,69.213,-75.749,69.3,-75.787,69.319,-76.046,69.386,-76.19,69.411,-76.316,69.422,-76.408,69.441,-76.465,69.469,-76.52,69.517,-76.525,69.549,-76.516,69.591,-76.463,69.62,-76.231,69.653,-76.234,69.662,-76.424,69.687,-76.513,69.684,-76.59,69.656,-76.687,69.591,-76.742,69.573,-76.916,69.611,-77.02,69.617,-77.09,69.635,-77.129,69.653,-77.105,69.671,-77.019,69.689,-76.869,69.745,-76.859,69.775,-76.962,69.825,-77.016,69.836,-77.232,69.855,-77.494,69.836,-77.592,69.846,-77.635,69.9,-77.663,69.966,-77.675,70.042,-77.722,70.171,-77.774,70.239,-77.843,70.247,-78.157,70.219,-78.231,70.219,-78.283,70.229,-78.491,70.316,-78.575,70.346,-78.621,70.353,-78.773,70.445,-78.831,70.463,-78.9,70.509,-78.98,70.581,-79.066,70.604,-79.16,70.575,-79.253,70.535,-79.347,70.482,-79.397,70.437,-79.405,70.401,-79.347,70.372,-79.018,70.325,-78.934,70.294,-78.863,70.242,-78.81,70.179,-78.775,70.104,-78.778,70.048,-78.819,70.01,-78.89,69.977,-79.093,69.925,-79.303,69.895,-79.515,69.888,-79.616,69.895,-80.162,69.996,-80.26,69.997,-80.387,70.01,-80.67,70.052,-80.826,70.057,-81.098,70.091,-81.56,70.111,-81.652,70.095,-81.529,70.048,-81.422,70.025,-81.329,70.024,-81.197,69.983,-81.024,69.9,-80.925,69.851,-80.843,69.792,-80.84,69.771,-80.922,69.731,-81.565,69.943,-81.958,69.869,-82.139,69.841,-82.294,69.837,-82.488,69.866,-82.925,69.968,-83.091,70.004,-83.15,70.009,-83.531,69.965,-83.859,69.963,-84.522,70.005,-84.765,70.034,-84.829,70.063,-84.909,70.078,-85.053,70.078,-85.432,70.111,-85.78,70.037,-86.198,70.105,-86.322,70.145,-86.361,70.173,-86.483,70.289,-86.5,70.35,-86.465,70.406,-86.431,70.445,-86.397,70.465,-86.624,70.401,-86.704,70.391,-86.809,70.388,-87.122,70.412,-87.172,70.4,-87.156,70.377,-87.074,70.345,-87.063,70.325,-87.238,70.31,-87.502,70.326,-87.618,70.319,-87.67,70.31,-87.789,70.258,-87.838,70.247,-87.901,70.252,-88.178,70.369,-88.402,70.442,-88.663,70.471,-88.783,70.494,-88.848,70.523,-89.208,70.76,-89.258,70.811,-89.372,70.996,-89.41,71.036,-89.456,71.062,-89.366,71.067,-89.025,71.045,-88.696,71.046,-88.517,71.031,-88.309,70.984,-88.039,70.951,-87.845,70.944,-87.534,70.957,-87.182,70.988,-87.14,71.012,-87.369,71.053,-87.572,71.108,-87.76,71.179,-87.872,71.209,-88.061,71.227,-88.59,71.24,-89.079,71.288,-89.418,71.352,-89.693,71.423,-89.805,71.462,-89.846,71.492,-89.889,71.586,-89.934,71.743,-89.977,71.848,-90.02,71.902,-90.025,71.949,-89.931,72.049,-89.664,72.158,-89.657,72.175,-89.711,72.18,-89.823,72.208,-89.859,72.248,-89.873,72.313,-89.874,72.367,-89.862,72.412,-89.817,72.468,-89.702,72.568,-89.536,72.69,-89.358,72.804,-89.327,72.842,-89.311,72.943,-89.288,73.017,-89.263,73.069,-89.225,73.108,-89.115,73.182,-88.977,73.252,-88.761,73.312,-88.743,73.335,-88.74,73.365,-88.727,73.388,-88.705,73.403,-88.17,73.595,-87.926,73.673,-87.72,73.723,-87.472,73.759,-86.769,73.834,-86.406,73.855,-85.951,73.85,-85.11,73.808,-85.009,73.779,-84.984,73.764,-84.947,73.722,-84.975,73.695,-85.204,73.604,-85.494,73.528,-85.682,73.461,-86.001,73.313,-86.086,73.26,-86.481,72.96,-86.575,72.911,-86.629,72.871,-86.668,72.763,-86.656,72.724,-86.595,72.661,-86.38,72.525,-86.323,72.461,-86.324,72.402,-86.348,72.262,-86.351,72.191,-86.341,72.123,-86.297,72.026,-86.218,71.899,-86.036,71.771,-85.75,71.641,-85.537,71.555,-85.327,71.492,-85.079,71.398,-85.023,71.353,-85.138,71.303,-85.405,71.227,-85.757,71.194,-85.945,71.163,-86.179,71.096,-86.473,71.043,-86.589,71.011]],[[-61.801,49.094,-62.22,49.079,-62.553,49.141,-62.8,49.171,-63.042,49.225,-63.566,49.399,-63.626,49.46,-63.676,49.534,-63.777,49.602,-63.885,49.658,-64.44,49.828,-64.485,49.887,-64.373,49.926,-64.244,49.944,-64.131,49.942,-63.76,49.875,-63.292,49.817,-63.089,49.773,-62.859,49.705,-62.633,49.624,-62.133,49.407,-62.043,49.39,-61.817,49.284,-61.736,49.204,-61.696,49.139,-61.746,49.106,-61.801,49.094]],[[-63.811,46.469,-63.784,46.455,-63.737,46.481,-63.681,46.562,-63.534,46.541,-63.456,46.504,-63.413,46.512,-63.369,46.508,-63.286,46.46,-63.129,46.422,-62.964,46.428,-62.712,46.45,-62.682,46.459,-62.423,46.478,-62.164,46.487,-62.074,46.466,-62.041,46.446,-62.024,46.422,-62.172,46.355,-62.32,46.278,-62.526,46.203,-62.552,46.166,-62.539,46.098,-62.543,46.029,-62.503,46.023,-62.478,46,-62.531,45.977,-62.743,45.967,-62.805,45.973,-62.878,46.001,-62.904,46.068,-62.995,46.058,-63.022,46.067,-62.895,46.124,-62.953,46.195,-63.015,46.19,-63.056,46.224,-63.053,46.27,-62.995,46.292,-62.978,46.316,-63.057,46.295,-63.117,46.253,-63.195,46.237,-63.271,46.2,-63.153,46.188,-63.213,46.16,-63.277,46.153,-63.569,46.209,-63.641,46.23,-63.732,46.289,-63.801,46.367,-63.763,46.37,-63.751,46.384,-63.759,46.398,-63.861,46.408,-64.02,46.405,-64.111,46.425,-64.107,46.562,-64.136,46.6,-64.236,46.631,-64.388,46.641,-64.403,46.692,-64.355,46.769,-64.28,46.836,-64.223,46.901,-64.157,46.955,-63.994,47.062,-63.997,46.982,-63.981,46.913,-64.088,46.775,-63.903,46.639,-63.879,46.609,-63.864,46.572,-63.876,46.539,-63.906,46.509,-63.834,46.494,-63.811,46.469]],[[-82,62.954,-81.961,62.926,-81.949,62.884,-81.964,62.828,-81.99,62.776,-82.026,62.73,-82.114,62.652,-82.388,62.519,-82.491,62.447,-82.568,62.403,-83.016,62.21,-83.071,62.2,-83.13,62.204,-83.252,62.233,-83.377,62.238,-83.699,62.16,-83.714,62.174,-83.729,62.257,-83.761,62.304,-83.903,62.402,-83.912,62.426,-83.91,62.454,-83.899,62.476,-83.739,62.569,-83.376,62.905,-83.289,62.922,-83.111,62.884,-83.026,62.872,-82.966,62.874,-82.706,62.945,-82.46,62.936,-82.235,62.977,-82.129,62.978,-82.048,62.971,-82,62.954]],[[-79.545,62.412,-79.466,62.385,-79.336,62.294,-79.286,62.248,-79.272,62.186,-79.306,62.104,-79.324,62.026,-79.372,61.968,-79.462,61.894,-79.542,61.808,-79.611,61.71,-79.669,61.644,-79.714,61.613,-79.763,61.596,-79.816,61.595,-79.896,61.63,-80.004,61.703,-80.092,61.747,-80.205,61.777,-80.265,61.818,-80.276,61.859,-80.28,61.99,-80.275,62.055,-80.26,62.109,-80.235,62.153,-80.179,62.213,-80.022,62.343,-79.927,62.393,-79.868,62.404,-79.713,62.395,-79.65,62.398,-79.598,62.413,-79.545,62.412]],[[-75.676,68.323,-75.154,68.234,-75.103,68.202,-75.078,68.173,-75.063,68.141,-75.062,68.075,-75.073,68.049,-75.124,67.985,-75.127,67.965,-75.086,67.751,-75.091,67.635,-75.127,67.537,-75.202,67.459,-75.315,67.4,-75.4,67.367,-75.78,67.284,-76.049,67.262,-76.333,67.258,-76.694,67.236,-76.859,67.24,-76.944,67.25,-77.005,67.267,-77.076,67.32,-77.157,67.408,-77.224,67.508,-77.304,67.685,-77.306,67.706,-77.229,67.85,-77.126,67.947,-76.945,68.091,-76.74,68.231,-76.688,68.254,-76.596,68.279,-76.364,68.319,-76.173,68.309,-76.088,68.314,-75.983,68.332,-75.867,68.337,-75.676,68.323]],[[-79.537,73.654,-79.367,73.641,-78.287,73.666,-78.063,73.648,-77.382,73.537,-77.207,73.5,-77.12,73.45,-77.042,73.373,-77.005,73.356,-76.759,73.31,-76.657,73.254,-76.622,73.225,-76.57,73.159,-76.458,73.122,-76.331,73.1,-76.29,73.081,-76.31,72.998,-76.255,72.959,-76.135,72.912,-76.09,72.881,-76.183,72.843,-76.401,72.821,-77.014,72.844,-77.836,72.897,-78.314,72.882,-78.554,72.858,-79.134,72.772,-79.319,72.758,-79.501,72.756,-79.821,72.826,-79.937,72.864,-79.975,72.892,-80.052,72.977,-80.114,73.078,-80.146,73.161,-80.183,73.225,-80.293,73.246,-80.618,73.271,-80.727,73.305,-80.776,73.334,-80.824,73.381,-80.823,73.429,-80.798,73.472,-80.777,73.482,-80.736,73.483,-80.827,73.535,-80.858,73.591,-80.861,73.671,-80.849,73.721,-80.823,73.743,-80.763,73.758,-80.621,73.767,-80.412,73.765,-80.12,73.707,-79.889,73.702,-79.537,73.654]],[[-80.732,52.747,-80.802,52.734,-81.01,52.761,-81.097,52.78,-81.352,52.852,-81.839,52.958,-82.005,53.01,-82.039,53.05,-81.951,53.132,-81.901,53.166,-81.847,53.186,-81.335,53.224,-81.136,53.206,-80.9,53.037,-80.765,52.923,-80.71,52.832,-80.71,52.787,-80.732,52.747]],[[-78.827,56.145,-78.877,56.131,-78.914,56.133,-78.907,56.166,-78.857,56.232,-78.828,56.29,-78.822,56.34,-78.799,56.383,-78.762,56.421,-78.725,56.439,-78.669,56.439,-78.657,56.317,-78.673,56.26,-78.71,56.213,-78.761,56.175,-78.827,56.145]],[[-78.936,56.266,-79.018,56.165,-79.084,56.068,-79.175,55.885,-79.228,55.879,-79.274,55.922,-79.142,56.136,-79.136,56.16,-79.142,56.181,-79.182,56.212,-79.222,56.176,-79.407,55.935,-79.455,55.896,-79.495,55.875,-79.527,55.871,-79.606,55.876,-79.765,55.807,-79.497,56.093,-79.495,56.115,-79.545,56.128,-79.565,56.121,-79.781,55.941,-79.905,55.871,-79.987,55.892,-80.008,55.911,-80.001,55.932,-79.79,56.114,-79.596,56.244,-79.515,56.327,-79.482,56.404,-79.468,56.46,-79.469,56.523,-79.459,56.54,-79.448,56.537,-79.435,56.513,-79.432,56.447,-79.476,56.313,-79.512,56.247,-79.554,56.192,-79.536,56.18,-79.458,56.211,-79.393,56.276,-79.339,56.376,-79.305,56.463,-79.272,56.6,-79.261,56.596,-79.246,56.568,-79.21,56.549,-79.155,56.538,-79.124,56.52,-79.1,56.474,-79.078,56.454,-78.995,56.436,-78.963,56.422,-78.94,56.371,-78.942,56.345,-78.931,56.328,-78.907,56.32,-78.936,56.266]],[[-79.978,56.207,-80.029,56.199,-80.089,56.214,-80.057,56.287,-80.005,56.318,-79.874,56.348,-79.852,56.367,-79.81,56.377,-79.749,56.377,-79.681,56.404,-79.606,56.459,-79.58,56.466,-79.633,56.387,-79.688,56.327,-79.978,56.207]],[[-89.833,77.268,-90.095,77.21,-90.228,77.212,-90.993,77.329,-91.147,77.387,-91.177,77.426,-91.185,77.482,-91.183,77.557,-91.149,77.608,-91.109,77.626,-91.019,77.644,-90.843,77.655,-90.675,77.649,-90.423,77.628,-90.172,77.595,-89.839,77.491,-89.719,77.442,-89.694,77.378,-89.695,77.339,-89.712,77.31,-89.747,77.293,-89.833,77.268]],[[-104.558,77.142,-104.711,77.124,-105.016,77.165,-105.215,77.182,-105.38,77.254,-105.556,77.353,-105.695,77.461,-105.747,77.525,-105.848,77.563,-105.883,77.627,-106.066,77.725,-106.036,77.74,-105.863,77.754,-105.588,77.736,-105.456,77.701,-105.29,77.642,-105.074,77.548,-105.007,77.507,-104.994,77.45,-104.955,77.419,-104.77,77.413,-104.542,77.338,-104.501,77.309,-104.454,77.249,-104.457,77.221,-104.493,77.162,-104.558,77.142]],[[-98.792,79.981,-98.769,79.851,-98.79,79.785,-98.841,79.737,-98.885,79.726,-98.945,79.724,-99.218,79.762,-99.302,79.784,-99.306,79.803,-99.333,79.84,-99.516,79.887,-99.857,79.879,-100,79.884,-100.057,79.898,-100.092,79.919,-100.126,80.001,-100.12,80.03,-100.079,80.081,-100.053,80.093,-99.803,80.14,-99.731,80.144,-99.425,80.126,-99.153,80.124,-99.017,80.111,-98.895,80.082,-98.823,80.037,-98.792,79.981]],[[-105.289,72.92,-105.339,72.915,-105.434,72.938,-105.573,72.989,-105.8,73.093,-106.071,73.196,-106.113,73.258,-106.18,73.304,-106.526,73.413,-106.75,73.458,-106.922,73.48,-106.95,73.51,-106.831,73.599,-106.695,73.67,-106.614,73.696,-106.362,73.719,-105.512,73.766,-105.318,73.767,-105.114,73.744,-104.835,73.647,-104.718,73.636,-104.649,73.614,-104.587,73.578,-104.555,73.541,-104.552,73.466,-104.583,73.354,-104.622,73.311,-104.791,73.168,-104.969,73.089,-105.003,73.038,-105.075,72.997,-105.201,72.947,-105.289,72.92]],[[-102.227,76.015,-102.018,75.954,-102.008,75.939,-102.047,75.928,-102.318,75.895,-102.423,75.869,-102.511,75.808,-102.58,75.78,-102.944,75.763,-103.315,75.764,-103.245,75.823,-103.042,75.919,-103.202,75.958,-103.77,75.892,-103.985,75.933,-103.801,76.037,-103.985,76.047,-104.242,76.047,-104.406,76.108,-104.351,76.182,-104.012,76.223,-103.571,76.258,-103.098,76.311,-102.728,76.307,-102.584,76.282,-102.536,76.196,-102.49,76.095,-102.426,76.086,-102.227,76.015]],[[-104.023,76.583,-103.973,76.578,-103.821,76.598,-103.723,76.601,-103.613,76.563,-103.585,76.539,-103.19,76.477,-103.051,76.45,-103.034,76.431,-103.083,76.405,-103.2,76.371,-103.311,76.348,-103.472,76.329,-104.271,76.326,-104.358,76.335,-104.408,76.365,-104.506,76.479,-104.577,76.54,-104.603,76.583,-104.586,76.606,-104.5,76.63,-104.205,76.666,-104.075,76.666,-103.992,76.657,-103.959,76.639,-103.969,76.614,-104.023,76.583]],[[-118.328,75.58,-118.614,75.515,-118.817,75.522,-119.087,75.569,-119.306,75.585,-119.383,75.601,-119.395,75.617,-119.32,75.663,-119.227,75.699,-119.003,75.77,-118.626,75.906,-118.379,75.958,-118.137,75.994,-117.889,76.076,-117.752,76.112,-117.634,76.115,-117.513,76.099,-117.499,76.077,-117.626,75.966,-117.716,75.921,-117.891,75.805,-118.227,75.611,-118.328,75.58]],[[-113.832,77.755,-114.106,77.721,-114.287,77.721,-114.608,77.769,-114.98,77.915,-115.029,77.968,-114.89,77.977,-114.79,77.993,-114.726,78.016,-114.607,78.04,-114.33,78.078,-114.297,78.063,-114.303,78.033,-114.28,78.004,-114.181,77.998,-114.087,77.978,-113.898,77.916,-113.768,77.904,-113.721,77.89,-113.697,77.869,-113.618,77.832,-113.619,77.813,-113.726,77.776,-113.832,77.755]],[[-130.927,54.479,-130.95,54.478,-130.959,54.499,-130.953,54.542,-130.922,54.615,-130.907,54.632,-130.777,54.619,-130.758,54.614,-130.753,54.6,-130.763,54.577,-130.805,54.544,-130.927,54.479]],[[-131.029,51.962,-131.047,51.96,-131.081,51.98,-131.103,52.014,-131.117,52.101,-131.107,52.137,-131.098,52.151,-131.011,52.095,-131.029,51.962]],[[-130.236,53.959,-130.267,53.923,-130.338,53.866,-130.384,53.844,-130.407,53.856,-130.47,53.862,-130.537,53.918,-130.59,53.94,-130.625,53.941,-130.642,53.921,-130.646,53.894,-130.638,53.86,-130.644,53.845,-130.664,53.848,-130.683,53.863,-130.703,53.892,-130.707,53.921,-130.696,53.951,-130.647,53.991,-130.495,54.074,-130.448,54.089,-130.397,54.086,-130.316,54.047,-130.298,54.036,-130.236,53.959]],[[-129.849,53.168,-129.869,53.165,-129.934,53.177,-130.151,53.346,-130.306,53.407,-130.411,53.491,-130.518,53.544,-130.452,53.631,-130.395,53.62,-130.195,53.55,-130.035,53.481,-129.945,53.436,-129.755,53.245,-129.769,53.217,-129.849,53.168]],[[-130.575,54.77,-130.493,54.834,-130.313,54.946,-130.214,55.026,-130.204,54.947,-130.349,54.815,-130.535,54.749,-130.575,54.77]],[[-128.552,52.94,-128.507,52.621,-128.51,52.519,-128.577,52.452,-128.624,52.34,-128.679,52.29,-128.731,52.357,-128.736,52.468,-128.749,52.556,-128.766,52.598,-128.746,52.763,-128.77,52.751,-128.831,52.679,-128.9,52.674,-129.023,52.756,-129.085,52.822,-129.095,52.892,-129.176,52.965,-129.184,52.991,-129.178,53.018,-129.111,53.091,-129.084,53.14,-129.06,53.241,-129.033,53.28,-128.97,53.274,-128.858,53.229,-128.74,53.179,-128.633,53.113,-128.552,52.94]],[[-126.092,49.354,-126.064,49.264,-126.187,49.278,-126.23,49.296,-126.231,49.339,-126.209,49.38,-126.115,49.365,-126.092,49.354]],[[-124.154,49.531,-124.14,49.51,-124.362,49.588,-124.457,49.634,-124.494,49.667,-124.518,49.686,-124.631,49.736,-124.65,49.758,-124.623,49.775,-124.547,49.765,-124.421,49.728,-124.309,49.667,-124.154,49.531]],[[-126.641,49.606,-126.68,49.601,-126.743,49.613,-126.814,49.642,-126.939,49.718,-126.951,49.736,-126.94,49.75,-126.905,49.763,-126.897,49.783,-126.926,49.838,-126.826,49.872,-126.738,49.844,-126.698,49.808,-126.65,49.746,-126.628,49.675,-126.626,49.627,-126.641,49.606]],[[-125.184,50.097,-125.195,50.044,-125.26,50.13,-125.358,50.312,-125.345,50.354,-125.301,50.414,-125.261,50.418,-125.196,50.39,-125.14,50.34,-125.126,50.32,-125.091,50.268,-125.074,50.221,-125.113,50.163,-125.184,50.097]],[[-128.937,52.51,-128.969,52.464,-129.102,52.574,-129.151,52.605,-129.25,52.722,-129.268,52.772,-129.264,52.801,-129.246,52.811,-129.215,52.804,-129.186,52.791,-128.994,52.662,-128.94,52.601,-128.937,52.51]],[[-127.925,51.474,-127.941,51.457,-127.981,51.457,-128.045,51.474,-128.092,51.511,-128.149,51.627,-128.142,51.647,-128.123,51.667,-128.032,51.708,-127.999,51.704,-127.987,51.674,-127.933,51.605,-127.916,51.585,-127.916,51.506,-127.925,51.474]],[[-128.369,52.401,-128.445,52.388,-128.42,52.441,-128.412,52.473,-128.426,52.503,-128.436,52.56,-128.44,52.696,-128.365,52.782,-128.247,52.784,-128.248,52.741,-128.298,52.548,-128.324,52.459,-128.344,52.426,-128.369,52.401]],[[-129.314,52.992,-129.329,52.984,-129.37,52.998,-129.41,53.024,-129.478,53.098,-129.5,53.129,-129.515,53.179,-129.501,53.188,-129.471,53.183,-129.451,53.175,-129.344,53.053,-129.314,52.992]],[[-129.168,53.118,-129.173,53.111,-129.277,53.111,-129.306,53.121,-129.324,53.142,-129.331,53.174,-129.314,53.212,-129.253,53.285,-129.251,53.317,-129.238,53.33,-129.195,53.293,-129.177,53.259,-129.168,53.118]],[[-123.435,48.754,-123.477,48.729,-123.5,48.732,-123.518,48.75,-123.582,48.926,-123.555,48.922,-123.468,48.867,-123.488,48.846,-123.423,48.793,-123.407,48.756,-123.435,48.754]],[[-59.788,43.94,-59.922,43.904,-60.038,43.907,-60.114,43.939,-60.117,43.953,-59.936,43.94,-59.866,43.947,-59.727,44.003,-59.788,43.94]],[[-60.962,45.49,-61.003,45.482,-61.012,45.496,-61.076,45.537,-61.082,45.558,-61.026,45.577,-60.912,45.567,-60.953,45.516,-60.962,45.49]],[[-61.914,47.285,-61.879,47.266,-61.815,47.268,-61.773,47.26,-61.834,47.223,-61.951,47.219,-62.008,47.234,-61.925,47.425,-61.827,47.469,-61.628,47.594,-61.548,47.632,-61.474,47.647,-61.396,47.638,-61.476,47.564,-61.582,47.56,-61.684,47.499,-61.751,47.431,-61.831,47.392,-61.887,47.345,-61.914,47.285]],[[-64.509,47.887,-64.534,47.814,-64.621,47.752,-64.665,47.748,-64.685,47.754,-64.66,47.794,-64.663,47.863,-64.591,47.872,-64.565,47.866,-64.509,47.887]],[[-64.476,47.959,-64.591,47.907,-64.541,47.985,-64.52,48.005,-64.5,48.014,-64.481,48.007,-64.476,47.959]],[[-66.274,44.292,-66.324,44.257,-66.312,44.292,-66.25,44.379,-66.21,44.392,-66.274,44.292]],[[-66.762,44.682,-66.897,44.629,-66.845,44.764,-66.802,44.805,-66.745,44.791,-66.753,44.71,-66.762,44.682]],[[-73.567,45.469,-73.644,45.449,-73.775,45.468,-73.92,45.442,-73.961,45.441,-73.853,45.516,-73.687,45.561,-73.522,45.701,-73.476,45.705,-73.539,45.546,-73.552,45.49,-73.567,45.469]],[[-73.695,45.585,-73.816,45.565,-73.858,45.574,-73.725,45.672,-73.572,45.694,-73.695,45.585]],[[-71.026,46.873,-71.117,46.865,-71.095,46.9,-70.971,46.961,-70.88,46.996,-70.826,46.995,-70.913,46.92,-71.026,46.873]],[[-55.536,50.72,-55.57,50.709,-55.601,50.709,-55.629,50.721,-55.634,50.74,-55.604,50.781,-55.527,50.801,-55.469,50.796,-55.473,50.776,-55.504,50.742,-55.536,50.72]],[[-54.554,49.589,-54.709,49.531,-54.744,49.508,-54.787,49.496,-54.819,49.514,-54.864,49.576,-54.855,49.597,-54.813,49.599,-54.789,49.591,-54.783,49.572,-54.764,49.562,-54.733,49.562,-54.619,49.622,-54.559,49.631,-54.538,49.62,-54.554,49.589]],[[-55.361,51.89,-55.409,51.889,-55.42,51.9,-55.4,51.938,-55.346,51.983,-55.274,51.995,-55.294,51.93,-55.361,51.89]],[[-54.094,49.744,-54.02,49.679,-53.981,49.662,-54.238,49.592,-54.269,49.587,-54.286,49.595,-54.289,49.661,-54.278,49.711,-54.259,49.719,-54.199,49.689,-54.138,49.751,-54.094,49.744]],[[-54.227,47.441,-54.276,47.407,-54.326,47.408,-54.32,47.439,-54.259,47.498,-54.227,47.54,-54.226,47.566,-54.215,47.585,-54.168,47.607,-54.128,47.647,-54.148,47.573,-54.227,47.441]],[[-64.824,62.559,-64.632,62.548,-64.515,62.552,-64.465,62.536,-64.418,62.487,-64.478,62.418,-64.546,62.391,-64.657,62.384,-64.837,62.406,-64.901,62.421,-64.956,62.458,-64.931,62.485,-64.842,62.494,-64.827,62.505,-64.85,62.525,-64.849,62.543,-64.824,62.559]],[[-68.234,60.241,-68.324,60.233,-68.365,60.254,-68.368,60.315,-68.338,60.361,-68.235,60.456,-68.142,60.562,-68.088,60.588,-67.978,60.57,-67.914,60.54,-67.848,60.489,-67.819,60.45,-67.844,60.392,-67.922,60.34,-68.012,60.305,-68.234,60.241]],[[-70.337,62.549,-70.406,62.545,-70.542,62.552,-70.687,62.573,-70.766,62.597,-70.838,62.648,-70.851,62.704,-70.986,62.788,-71.137,62.816,-71.22,62.874,-71.135,62.878,-71.014,62.865,-70.835,62.84,-70.674,62.807,-70.443,62.734,-70.367,62.666,-70.292,62.616,-70.269,62.578,-70.289,62.562,-70.337,62.549]],[[-64.833,61.366,-64.857,61.354,-64.88,61.357,-64.954,61.41,-65.054,61.432,-65.092,61.453,-65.394,61.563,-65.427,61.611,-65.432,61.65,-65.332,61.668,-65.13,61.686,-64.954,61.685,-64.79,61.662,-64.756,61.638,-64.67,61.593,-64.691,61.539,-64.696,61.471,-64.732,61.438,-64.788,61.413,-64.833,61.366]],[[-65.031,61.879,-65.008,61.87,-64.981,61.881,-64.961,61.872,-64.947,61.843,-64.924,61.824,-64.865,61.798,-64.846,61.78,-64.847,61.762,-64.897,61.733,-64.928,61.733,-65.166,61.798,-65.23,61.864,-65.235,61.898,-65.211,61.928,-65.174,61.943,-65.126,61.942,-65.068,61.926,-65.031,61.879]],[[-69.16,59.04,-69.221,58.968,-69.302,58.977,-69.331,58.962,-69.353,58.961,-69.316,59.029,-69.312,59.075,-69.33,59.121,-69.303,59.145,-69.195,59.146,-69.194,59.093,-69.181,59.073,-69.155,59.064,-69.16,59.04]],[[-64.407,60.367,-64.442,60.298,-64.558,60.323,-64.738,60.376,-64.809,60.41,-64.834,60.448,-64.836,60.501,-64.783,60.51,-64.646,60.515,-64.533,60.441,-64.5,60.43,-64.407,60.367]],[[-60.994,56.039,-60.983,56.015,-61.137,56.033,-61.191,56.048,-61.196,56.064,-61.188,56.089,-61.158,56.118,-61.087,56.141,-61.049,56.129,-60.966,56.099,-60.955,56.08,-60.994,56.039]],[[-61.744,57.555,-61.66,57.525,-61.637,57.416,-61.795,57.422,-61.975,57.495,-62.011,57.548,-62.007,57.558,-61.983,57.567,-61.937,57.554,-61.893,57.573,-61.848,57.579,-61.744,57.555]],[[-67.915,69.541,-67.94,69.535,-68.202,69.58,-68.221,69.617,-68.093,69.657,-67.989,69.679,-67.909,69.682,-67.829,69.675,-67.755,69.631,-67.845,69.592,-67.915,69.541]],[[-62.682,67.056,-62.805,67.029,-62.872,67.063,-62.825,67.072,-62.757,67.113,-62.664,67.148,-62.625,67.177,-62.47,67.19,-62.417,67.188,-62.396,67.178,-62.485,67.134,-62.682,67.056]],[[-79.063,75.926,-79.052,75.867,-79.124,75.87,-79.356,75.831,-79.545,75.826,-79.639,75.843,-79.699,75.883,-79.551,75.958,-79.382,76.011,-79.178,76.092,-79.009,76.146,-78.926,76.135,-78.845,76.106,-78.946,76.025,-79.057,75.985,-79.063,75.926]],[[-79.867,56.775,-79.894,56.757,-79.944,56.777,-79.946,56.827,-79.898,56.865,-79.861,56.864,-79.827,56.843,-79.835,56.816,-79.867,56.775]],[[-79.717,57.516,-79.732,57.508,-79.775,57.514,-79.792,57.449,-79.808,57.442,-79.838,57.483,-79.816,57.518,-79.819,57.542,-79.811,57.559,-79.768,57.599,-79.743,57.608,-79.727,57.605,-79.713,57.555,-79.717,57.516]],[[-79.518,56.657,-79.553,56.644,-79.577,56.645,-79.551,56.733,-79.582,56.765,-79.584,56.781,-79.57,56.796,-79.553,56.799,-79.511,56.771,-79.491,56.743,-79.482,56.714,-79.485,56.687,-79.497,56.667,-79.518,56.657]],[[-79.938,53.304,-79.939,53.274,-80.004,53.28,-80.039,53.297,-80.068,53.324,-80.074,53.344,-80.05,53.364,-79.975,53.352,-79.938,53.304]],[[-79.384,51.952,-79.426,51.945,-79.521,51.953,-79.597,51.978,-79.644,52.01,-79.335,52.098,-79.271,52.087,-79.27,52.071,-79.317,52.024,-79.329,51.992,-79.352,51.968,-79.384,51.952]],[[-123.372,48.886,-123.385,48.875,-123.541,48.946,-123.646,49.039,-123.689,49.095,-123.482,48.955,-123.378,48.908,-123.372,48.886]],[[-124.978,50.03,-125.002,50.021,-125.026,50.134,-124.996,50.175,-124.987,50.196,-124.991,50.217,-124.938,50.166,-124.916,50.132,-124.907,50.084,-124.908,50.071,-124.978,50.03]],[[-73.622,67.784,-74.109,67.783,-74.374,67.79,-74.481,67.805,-74.573,67.829,-74.679,67.906,-74.746,67.985,-74.749,68.018,-74.731,68.049,-74.707,68.067,-74.379,68.093,-74.111,68.061,-73.881,68.022,-73.584,68.015,-73.494,68.001,-73.459,67.99,-73.435,67.97,-73.402,67.879,-73.398,67.83,-73.407,67.793,-73.622,67.784]],[[-77.877,63.471,-77.792,63.428,-77.704,63.431,-77.655,63.396,-77.538,63.287,-77.527,63.269,-77.533,63.234,-77.594,63.188,-77.658,63.165,-77.791,63.13,-77.942,63.114,-78.024,63.139,-78.256,63.24,-78.469,63.358,-78.537,63.424,-78.507,63.451,-78.417,63.47,-78.235,63.49,-77.934,63.479,-77.877,63.471]],[[-94.527,75.749,-94.624,75.749,-94.751,75.77,-94.787,75.791,-94.815,75.821,-94.834,75.859,-94.86,75.889,-94.894,75.912,-94.901,75.931,-94.881,75.946,-94.84,75.954,-94.745,75.957,-94.538,75.996,-94.499,75.992,-94.471,75.971,-94.443,75.917,-94.414,75.885,-94.332,75.826,-94.296,75.788,-94.304,75.776,-94.33,75.766,-94.527,75.749]],[[-80.285,59.624,-80.317,59.621,-80.325,59.633,-80.299,59.674,-80.257,59.679,-80.21,59.725,-80.167,59.709,-80.183,59.683,-80.241,59.645,-80.285,59.624]],[[-80.064,59.771,-80.167,59.764,-80.122,59.823,-80.084,59.852,-80.041,59.87,-79.956,59.877,-79.899,59.853,-79.95,59.81,-80.064,59.771]],[[-96.782,72.937,-96.944,72.927,-97.093,72.997,-97.098,73.062,-97.088,73.098,-97.069,73.13,-97.015,73.157,-96.862,73.189,-96.793,73.165,-96.768,73.137,-96.744,73.126,-96.646,73.102,-96.598,73.074,-96.604,73.042,-96.635,72.992,-96.671,72.961,-96.709,72.947,-96.782,72.937]],[[-97.356,74.526,-97.656,74.466,-97.722,74.489,-97.75,74.511,-97.516,74.602,-97.417,74.627,-97.318,74.598,-97.291,74.576,-97.304,74.56,-97.356,74.526]],[[-98.27,73.869,-98.558,73.847,-98.691,73.856,-98.761,73.829,-98.817,73.817,-98.974,73.812,-99.298,73.862,-99.385,73.879,-99.417,73.895,-99.404,73.911,-99.346,73.926,-99.097,73.948,-99.005,73.965,-98.967,73.988,-98.904,74.007,-98.818,74.021,-98.585,74.035,-98.061,74.105,-97.8,74.115,-97.698,74.109,-97.667,74.09,-97.659,74.072,-97.673,74.053,-97.755,74.006,-97.861,73.968,-98.147,73.889,-98.27,73.869]],[[-90.2,69.419,-90.177,69.357,-90.267,69.273,-90.295,69.258,-90.33,69.252,-90.364,69.263,-90.465,69.329,-90.492,69.37,-90.455,69.39,-90.377,69.416,-90.322,69.429,-90.253,69.418,-90.229,69.436,-90.2,69.419]],[[-90.493,69.221,-90.574,69.209,-90.626,69.251,-90.667,69.259,-90.686,69.287,-90.772,69.293,-90.766,69.336,-90.742,69.357,-90.663,69.374,-90.6,69.368,-90.54,69.325,-90.511,69.29,-90.485,69.247,-90.493,69.221]],[[-74,62.618,-74.054,62.61,-74.254,62.622,-74.5,62.669,-74.626,62.713,-74.62,62.726,-74.564,62.733,-74.501,62.727,-74.395,62.696,-74.109,62.68,-74.017,62.663,-73.988,62.636,-74,62.618]],[[-74.881,68.349,-74.959,68.342,-75.073,68.404,-75.31,68.474,-75.4,68.525,-75.403,68.55,-75.396,68.589,-75.37,68.636,-75.287,68.688,-75.2,68.696,-75.075,68.685,-74.984,68.648,-74.885,68.545,-74.819,68.494,-74.798,68.458,-74.831,68.441,-74.828,68.424,-74.813,68.413,-74.819,68.394,-74.845,68.366,-74.881,68.349]],[[-78.532,60.729,-78.669,60.717,-78.669,60.731,-78.612,60.772,-78.4,60.808,-78.242,60.819,-78.279,60.784,-78.372,60.756,-78.532,60.729]],[[-78.983,68.193,-79.064,68.182,-79.174,68.235,-79.175,68.264,-79.153,68.335,-78.953,68.353,-78.869,68.31,-78.829,68.268,-78.983,68.193]],[[-76.678,63.394,-76.783,63.384,-76.922,63.406,-77.057,63.45,-77.365,63.588,-77.134,63.682,-76.764,63.574,-76.652,63.504,-76.678,63.394]],[[-79.431,69.788,-79.39,69.73,-79.365,69.712,-79.402,69.685,-79.553,69.631,-79.882,69.609,-80.048,69.634,-79.971,69.556,-79.954,69.523,-79.978,69.51,-80.047,69.514,-80.161,69.536,-80.227,69.562,-80.244,69.593,-80.269,69.6,-80.3,69.583,-80.33,69.587,-80.398,69.633,-80.448,69.65,-80.778,69.677,-80.795,69.689,-80.778,69.71,-80.727,69.74,-80.653,69.751,-80.466,69.737,-80.451,69.745,-80.438,69.783,-80.424,69.798,-80.295,69.794,-80.214,69.802,-80.169,69.782,-80.125,69.737,-80.062,69.746,-79.971,69.739,-79.87,69.756,-79.715,69.796,-79.594,69.81,-79.431,69.788]],[[-78.029,69.715,-77.978,69.665,-77.969,69.639,-78.04,69.608,-78.307,69.552,-78.47,69.503,-78.552,69.492,-78.662,69.503,-78.795,69.48,-78.848,69.483,-78.789,69.523,-78.579,69.639,-78.402,69.651,-78.344,69.675,-78.296,69.667,-78.267,69.687,-78.262,69.717,-78.201,69.74,-78.145,69.739,-78.029,69.715]],[[-77.642,63.992,-77.714,63.946,-77.929,63.962,-77.958,63.976,-77.966,63.993,-77.931,64.015,-77.711,64.036,-77.617,64.037,-77.569,64.03,-77.564,64.022,-77.642,63.992]],[[-83.123,66.283,-83.024,66.271,-82.948,66.272,-82.931,66.257,-83.011,66.208,-83.06,66.199,-83.148,66.234,-83.214,66.277,-83.233,66.303,-83.238,66.332,-83.222,66.336,-83.123,66.283]],[[-79.211,68.845,-79.28,68.839,-79.361,68.858,-79.39,68.89,-79.406,68.923,-79.391,68.94,-79.355,68.956,-79.305,68.992,-79.243,69.049,-79.145,69.087,-78.93,69.123,-78.9,69.135,-78.804,69.235,-78.772,69.252,-78.662,69.262,-78.65,69.275,-78.689,69.3,-78.689,69.325,-78.65,69.351,-78.597,69.371,-78.458,69.39,-78.333,69.386,-78.3,69.379,-78.272,69.361,-78.234,69.315,-78.229,69.304,-78.287,69.263,-78.439,69.199,-78.533,69.146,-78.552,69.129,-78.56,69.106,-78.596,69.079,-78.705,69.014,-78.779,68.95,-78.853,68.916,-79.054,68.883,-79.211,68.845]],[[-76.995,69.144,-77.122,69.132,-77.215,69.138,-77.276,69.162,-77.322,69.194,-77.379,69.274,-77.358,69.312,-77.352,69.379,-77.341,69.404,-77.319,69.416,-77.188,69.44,-77.109,69.437,-76.994,69.412,-76.746,69.404,-76.684,69.38,-76.669,69.366,-76.67,69.349,-76.687,69.328,-76.81,69.267,-76.869,69.225,-76.911,69.175,-76.995,69.144]],[[-86.913,70.113,-86.799,70.105,-86.691,70.115,-86.613,70.106,-86.563,70.077,-86.531,70.048,-86.515,70.017,-86.558,69.995,-86.734,69.976,-86.855,69.986,-86.984,70.011,-87.044,70,-87.191,70.019,-87.264,70.044,-87.323,70.08,-87.323,70.102,-87.168,70.127,-87.107,70.147,-86.913,70.113]],[[-83.726,65.797,-83.598,65.757,-83.469,65.735,-83.263,65.723,-83.234,65.715,-83.234,65.697,-83.264,65.668,-83.332,65.631,-83.381,65.63,-83.495,65.656,-83.537,65.669,-83.583,65.699,-83.607,65.701,-83.636,65.692,-83.644,65.679,-83.631,65.662,-83.65,65.658,-83.788,65.669,-83.809,65.678,-83.798,65.71,-83.702,65.756,-83.787,65.77,-83.814,65.788,-83.939,65.758,-84.008,65.752,-84.118,65.772,-84.13,65.877,-84.143,65.916,-84.193,65.942,-84.223,65.97,-84.271,65.991,-84.37,66.012,-84.451,66.064,-84.467,66.088,-84.456,66.106,-84.407,66.131,-84.122,66.078,-83.95,66.027,-83.787,65.966,-83.701,65.92,-83.694,65.89,-83.715,65.861,-83.765,65.831,-83.726,65.797]],[[-86.596,67.736,-86.638,67.735,-86.706,67.75,-86.861,67.81,-86.893,67.837,-86.908,67.867,-86.908,67.902,-86.895,67.938,-86.847,68.01,-86.938,68.068,-86.96,68.1,-86.949,68.119,-86.899,68.163,-86.885,68.191,-86.834,68.23,-86.702,68.306,-86.57,68.288,-86.452,68.225,-86.421,68.183,-86.43,68.139,-86.42,68.074,-86.39,67.989,-86.382,67.927,-86.396,67.889,-86.447,67.817,-86.49,67.784,-86.546,67.752,-86.596,67.736]],[[-84.675,65.575,-84.727,65.564,-84.783,65.57,-84.83,65.599,-84.869,65.651,-84.931,65.689,-85.072,65.737,-85.096,65.756,-85.136,65.821,-85.144,65.885,-85.174,65.944,-85.176,65.972,-85.15,66.015,-85.031,66.025,-84.939,66.009,-84.92,65.997,-84.889,65.972,-84.87,65.942,-84.757,65.859,-84.692,65.793,-84.603,65.657,-84.602,65.631,-84.626,65.604,-84.675,65.575]],[[-93.044,61.844,-93.085,61.842,-93.177,61.893,-93.197,61.919,-93.076,61.935,-92.993,61.89,-93,61.867,-93.044,61.844]],[[-101.226,76.579,-101.485,76.575,-101.605,76.587,-101.613,76.605,-101.509,76.628,-101.165,76.665,-100.962,76.734,-100.886,76.743,-100.622,76.752,-100.467,76.75,-100.269,76.734,-100.747,76.649,-101.226,76.579]],[[-103.003,78.146,-103.118,78.126,-103.252,78.138,-103.271,78.151,-103.274,78.166,-103.26,78.183,-103.11,78.246,-102.973,78.267,-102.892,78.271,-102.826,78.25,-102.788,78.218,-103.003,78.146]],[[-101.694,77.697,-101.831,77.687,-102.08,77.692,-102.378,77.728,-102.458,77.77,-102.475,77.837,-102.472,77.873,-102.448,77.881,-102.263,77.889,-101.918,77.9,-101.639,77.892,-101.322,77.854,-101.193,77.83,-101.128,77.813,-101.046,77.778,-101.02,77.762,-101.002,77.735,-101.398,77.729,-101.585,77.718,-101.694,77.697]],[[-89.726,76.507,-89.773,76.494,-89.924,76.501,-89.974,76.488,-90.054,76.495,-90.165,76.524,-90.294,76.579,-90.441,76.663,-90.556,76.735,-90.562,76.754,-90.525,76.788,-90.41,76.81,-90.136,76.837,-89.949,76.836,-89.775,76.782,-89.725,76.763,-89.695,76.741,-89.694,76.72,-89.709,76.701,-89.788,76.66,-89.822,76.631,-89.822,76.602,-89.805,76.561,-89.773,76.534,-89.726,76.521,-89.726,76.507]],[[-96.079,75.51,-96.156,75.477,-96.237,75.475,-96.344,75.506,-96.462,75.494,-96.622,75.431,-96.679,75.394,-96.723,75.381,-96.857,75.369,-96.915,75.38,-96.97,75.413,-97.021,75.468,-96.983,75.51,-96.856,75.538,-96.523,75.584,-96.428,75.606,-96.417,75.631,-96.397,75.647,-96.368,75.655,-96.145,75.614,-96.04,75.586,-95.96,75.554,-95.969,75.542,-96.079,75.51]],[[-95.307,74.505,-95.352,74.5,-95.442,74.506,-95.777,74.551,-95.834,74.569,-95.851,74.582,-95.774,74.599,-95.746,74.616,-95.66,74.637,-95.51,74.637,-95.353,74.586,-95.278,74.54,-95.274,74.519,-95.307,74.505]],[[-121.076,75.745,-121.154,75.741,-121.241,75.752,-121.221,75.777,-121.026,75.848,-121.015,75.868,-121.018,75.884,-121.042,75.903,-120.993,75.927,-120.914,75.938,-120.888,75.928,-120.879,75.907,-120.897,75.845,-120.921,75.814,-120.955,75.789,-121.007,75.766,-121.076,75.745]],[[-113.561,76.743,-113.712,76.711,-114.751,76.759,-114.808,76.774,-114.835,76.795,-114.647,76.851,-114.42,76.875,-113.892,76.895,-113.708,76.873,-113.585,76.847,-113.517,76.825,-113.488,76.783,-113.561,76.743]],[[-104.12,75.036,-104.309,75.031,-104.634,75.061,-104.828,75.12,-104.887,75.148,-104.882,75.16,-104.848,75.173,-104.801,75.211,-104.69,75.321,-104.649,75.35,-104.474,75.413,-104.346,75.43,-104.075,75.425,-103.917,75.392,-103.851,75.371,-103.804,75.346,-103.758,75.289,-103.746,75.252,-103.667,75.211,-103.644,75.187,-103.642,75.163,-103.664,75.139,-103.71,75.115,-103.814,75.08,-104.12,75.036]],[[-100.217,68.807,-100.249,68.775,-100.288,68.766,-100.366,68.729,-100.397,68.724,-100.443,68.748,-100.481,68.786,-100.497,68.792,-100.521,68.791,-100.573,68.766,-100.597,68.766,-100.616,68.783,-100.625,68.816,-100.625,68.865,-100.6,68.941,-100.598,68.969,-100.612,68.99,-100.601,69.009,-100.565,69.027,-100.52,69.035,-100.414,69.028,-100.33,68.998,-100.289,68.958,-100.207,68.926,-100.178,68.904,-100.217,68.807]],[[-99.995,69.014,-100.018,68.954,-100.141,68.97,-100.196,68.991,-100.242,69.04,-100.247,69.053,-100.237,69.071,-100.187,69.114,-100.153,69.129,-100.073,69.111,-100.035,69.087,-100.006,69.047,-99.995,69.014]],[[-100.308,70.496,-100.321,70.488,-100.537,70.525,-100.621,70.547,-100.648,70.563,-100.667,70.596,-100.678,70.646,-100.635,70.67,-100.538,70.669,-100.434,70.649,-100.276,70.595,-100.321,70.578,-100.323,70.542,-100.306,70.508,-100.308,70.496]],[[-95.514,69.574,-95.381,69.507,-95.382,69.474,-95.399,69.42,-95.437,69.378,-95.496,69.35,-95.579,69.336,-95.684,69.336,-95.73,69.348,-95.696,69.39,-95.67,69.402,-95.666,69.439,-95.683,69.5,-95.704,69.538,-95.764,69.56,-95.806,69.56,-95.818,69.541,-95.798,69.5,-95.812,69.447,-95.858,69.382,-95.893,69.352,-95.956,69.367,-95.986,69.392,-95.978,69.433,-95.995,69.47,-95.979,69.509,-95.936,69.567,-95.876,69.606,-95.798,69.626,-95.707,69.624,-95.602,69.602,-95.514,69.574]],[[-101.172,69.397,-101.254,69.388,-101.269,69.391,-101.262,69.418,-101.268,69.431,-101.29,69.441,-101.218,69.463,-101.207,69.48,-101.23,69.493,-101.328,69.517,-101.356,69.54,-101.351,69.559,-101.313,69.576,-101.245,69.574,-101.098,69.541,-101.031,69.495,-101.001,69.462,-101.049,69.457,-101.087,69.443,-101.127,69.415,-101.172,69.397]],[[-101.846,68.586,-101.887,68.585,-101.945,68.603,-102.266,68.664,-102.308,68.682,-102.271,68.708,-102.153,68.74,-102.074,68.774,-102.013,68.825,-101.828,68.799,-101.759,68.775,-101.733,68.753,-101.722,68.724,-101.732,68.652,-101.794,68.637,-101.846,68.586]],[[-104.541,68.406,-104.596,68.402,-104.699,68.418,-104.851,68.454,-104.965,68.492,-105.042,68.532,-105.051,68.559,-104.994,68.574,-104.907,68.582,-104.7,68.577,-104.602,68.562,-104.472,68.504,-104.445,68.471,-104.44,68.45,-104.457,68.431,-104.541,68.406]],[[-107.9,67.402,-107.95,67.318,-107.97,67.326,-108.004,67.366,-108.073,67.385,-108.152,67.429,-108.151,67.525,-108.121,67.568,-108.128,67.629,-108.049,67.665,-107.991,67.622,-107.975,67.549,-107.989,67.514,-107.932,67.476,-107.905,67.467,-107.891,67.437,-107.9,67.402]],[[-109.166,67.982,-109.054,67.972,-108.971,67.98,-108.91,67.939,-108.886,67.899,-108.894,67.884,-108.92,67.879,-109.096,67.924,-109.162,67.952,-109.184,67.975,-109.166,67.982]],[[-108.093,67.005,-107.966,66.997,-107.806,66.999,-107.833,66.921,-107.895,66.872,-107.944,66.858,-107.965,66.885,-108.06,66.947,-108.093,67.005]],[[-109.323,67.991,-109.361,67.988,-109.498,68.047,-109.469,68.098,-109.342,68.046,-109.324,68.013,-109.323,67.991]],[[-139.043,69.577,-139.126,69.539,-139.257,69.579,-139.291,69.598,-139.14,69.65,-139.073,69.648,-138.932,69.617,-138.879,69.59,-139.043,69.577]]]},{"n":"Cameroon","c":"CM","r":3,"ml":3,"lx":12.47,"ly":4.59,"b":[8.533,1.676,16.183,13.079],"p":[[[8.556,4.755,8.585,4.833,8.641,4.927,8.716,5.047,8.801,5.197,8.859,5.464,8.899,5.63,8.935,5.781,8.997,5.918,9.06,6.009,9.239,6.186,9.373,6.32,9.442,6.373,9.49,6.419,9.574,6.47,9.66,6.532,9.726,6.65,9.78,6.76,9.821,6.784,9.874,6.803,10.039,6.921,10.144,6.996,10.168,6.959,10.186,6.913,10.205,6.892,10.293,6.877,10.413,6.878,10.482,6.891,10.519,6.93,10.556,7.037,10.578,7.058,10.606,7.063,10.738,6.988,10.846,6.882,10.954,6.777,11.009,6.739,11.033,6.698,11.08,6.506,11.106,6.458,11.153,6.438,11.237,6.451,11.325,6.485,11.402,6.534,11.478,6.597,11.529,6.655,11.552,6.697,11.563,6.855,11.58,6.889,11.658,6.952,11.787,7.056,11.861,7.116,11.855,7.138,11.809,7.202,11.767,7.272,11.809,7.345,11.852,7.401,12.016,7.59,12.017,7.652,12.025,7.728,12.156,7.942,12.231,8.227,12.233,8.282,12.311,8.42,12.404,8.596,12.583,8.624,12.652,8.668,12.731,8.746,12.782,8.818,12.807,8.887,12.824,9.019,12.856,9.171,12.876,9.304,12.929,9.426,13.019,9.488,13.175,9.54,13.199,9.564,13.221,9.645,13.239,9.814,13.244,9.916,13.25,9.96,13.27,10.036,13.415,10.171,13.479,10.383,13.535,10.605,13.7,10.873,13.892,11.14,13.981,11.212,14.057,11.245,14.143,11.249,14.202,11.268,14.409,11.401,14.496,11.446,14.56,11.492,14.575,11.532,14.582,11.591,14.562,11.729,14.597,11.83,14.618,11.987,14.627,12.109,14.62,12.151,14.587,12.209,14.581,12.222,14.519,12.298,14.415,12.344,14.273,12.356,14.197,12.384,14.185,12.447,14.178,12.484,14.17,12.524,14.16,12.613,14.064,13.079,14.245,13.077,14.462,13.022,14.516,12.98,14.545,12.82,14.623,12.73,14.761,12.656,14.847,12.502,14.881,12.269,14.957,12.13,14.974,12.108,15.06,11.907,15.081,11.846,15.088,11.724,15.078,11.643,15.122,11.541,15.055,11.369,15.036,11.263,15.03,11.114,15.069,10.851,15.132,10.648,15.201,10.485,15.276,10.357,15.4,10.217,15.532,10.088,15.655,10.008,15.541,9.96,15.32,9.954,15.193,9.981,15.133,9.983,15.072,9.966,14.836,9.942,14.598,9.953,14.377,9.985,14.243,9.98,14.14,9.902,14.056,9.784,13.977,9.692,14.005,9.589,14.064,9.532,14.178,9.406,14.28,9.285,14.332,9.204,14.536,9.025,14.733,8.866,14.771,8.839,14.826,8.81,14.861,8.799,14.968,8.707,15.116,8.557,15.252,8.322,15.349,8.084,15.443,7.852,15.484,7.813,15.55,7.788,15.558,7.738,15.553,7.665,15.532,7.604,15.48,7.524,15.379,7.358,15.246,7.264,15.207,7.206,15.186,7.135,15.157,7.064,15.086,6.91,15.035,6.784,14.983,6.745,14.862,6.556,14.78,6.366,14.764,6.316,14.739,6.28,14.7,6.25,14.559,6.191,14.512,6.162,14.475,6.127,14.441,6.087,14.431,6.039,14.464,5.971,14.503,5.917,14.542,5.914,14.577,5.916,14.599,5.884,14.617,5.865,14.617,5.496,14.584,5.44,14.584,5.415,14.568,5.351,14.563,5.28,14.574,5.252,14.602,5.229,14.641,5.179,14.662,5.066,14.709,4.666,14.731,4.602,14.77,4.558,14.894,4.472,15.023,4.359,15.064,4.285,15.088,4.164,15.137,4.069,15.136,4.037,15.115,4.024,15.067,4.023,15.035,4.016,15.062,3.947,15.129,3.827,15.24,3.702,15.36,3.567,15.458,3.457,15.581,3.329,15.677,3.23,15.775,3.127,15.849,3.103,15.905,3.096,15.929,3.076,15.958,3.029,16.008,2.977,16.063,2.909,16.082,2.839,16.059,2.773,16.082,2.678,16.083,2.67,16.102,2.633,16.096,2.599,16.107,2.473,16.136,2.364,16.183,2.27,16.183,2.262,16.177,2.205,16.116,2.168,16.08,2.107,16.07,2.022,16.088,1.918,16.135,1.796,16.136,1.724,16.12,1.714,16.09,1.691,16.059,1.676,15.975,1.76,15.882,1.817,15.742,1.915,15.6,1.95,15.417,1.957,15.339,1.945,15.282,1.982,15.204,2.024,15.16,2.036,15.1,2.002,15.058,2.001,15.006,2.014,14.902,2.012,14.893,2.069,14.875,2.08,14.763,2.075,14.728,2.122,14.713,2.117,14.669,2.132,14.579,2.199,14.484,2.155,14.287,2.16,14.034,2.159,13.773,2.157,13.533,2.16,13.294,2.162,13.27,2.224,13.22,2.256,13.131,2.259,12.867,2.247,12.666,2.257,12.601,2.265,12.53,2.281,12.361,2.296,12.153,2.284,12.106,2.288,11.94,2.285,11.559,2.302,11.348,2.3,11.353,2.261,11.34,2.234,11.329,2.167,11.097,2.167,10.791,2.168,10.502,2.168,10.307,2.168,9.98,2.168,9.87,2.213,9.837,2.242,9.83,2.275,9.826,2.298,9.801,2.304,9.822,2.539,9.868,2.735,9.885,2.917,9.948,3.079,9.915,3.24,9.876,3.31,9.672,3.538,9.766,3.624,9.642,3.612,9.616,3.696,9.556,3.798,9.593,3.814,9.628,3.87,9.74,3.853,9.736,3.88,9.64,3.965,9.649,4.008,9.689,4.056,9.67,4.077,9.6,4.027,9.551,4.028,9.512,4.061,9.484,4.066,9.501,4.001,9.462,3.943,9.425,3.922,9.362,3.926,9.311,3.94,9.297,3.973,9.249,3.998,9.114,4.041,9,4.092,8.977,4.23,8.932,4.29,8.914,4.358,8.903,4.435,8.918,4.554,8.889,4.573,8.856,4.579,8.807,4.573,8.762,4.58,8.708,4.646,8.66,4.671,8.69,4.55,8.656,4.516,8.574,4.526,8.54,4.572,8.533,4.606,8.571,4.752,8.556,4.755]]]},{"n":"Cambodia","c":"KH","r":3,"ml":3,"lx":104.5,"ly":12.65,"b":[102.32,10.411,107.605,14.705],"p":[[[103.045,11.285,103.027,11.275,103.011,11.276,102.993,11.29,102.995,11.348,103.008,11.383,103.037,11.39,103.045,11.285]],[[103.318,10.719,103.281,10.68,103.223,10.76,103.223,10.782,103.318,10.719]],[[107.519,14.705,107.535,14.65,107.493,14.546,107.448,14.451,107.364,14.369,107.36,14.308,107.331,14.127,107.343,14.069,107.362,14.019,107.389,13.993,107.462,13.816,107.529,13.654,107.594,13.522,107.605,13.438,107.546,13.225,107.475,13.03,107.482,12.933,107.512,12.836,107.544,12.706,107.555,12.54,107.538,12.432,107.506,12.365,107.446,12.296,107.393,12.26,107.33,12.319,107.28,12.322,107.212,12.304,107.159,12.277,107.051,12.176,106.931,12.077,106.765,12.052,106.7,11.979,106.631,11.969,106.5,11.966,106.414,11.948,106.418,11.912,106.411,11.738,106.413,11.698,106.399,11.687,106.34,11.682,106.239,11.708,106.103,11.751,106.006,11.758,105.956,11.682,105.927,11.653,105.89,11.648,105.851,11.635,105.838,11.601,105.835,11.559,105.854,11.487,105.861,11.372,105.856,11.294,105.892,11.245,106.1,11.079,106.161,11.037,106.168,11.012,106.132,10.922,106.164,10.795,106.099,10.797,105.99,10.852,105.938,10.885,105.875,10.858,105.853,10.864,105.811,10.926,105.755,10.99,105.698,10.994,105.577,10.969,105.453,10.951,105.406,10.952,105.387,10.94,105.315,10.845,105.284,10.861,105.159,10.898,105.046,10.911,105.022,10.887,105.036,10.809,105.061,10.734,105.046,10.702,104.984,10.662,104.901,10.59,104.851,10.534,104.815,10.521,104.69,10.523,104.564,10.516,104.514,10.463,104.467,10.422,104.426,10.411,104.262,10.541,103.937,10.587,103.902,10.644,103.871,10.655,103.841,10.581,103.662,10.509,103.587,10.552,103.532,10.605,103.54,10.669,103.592,10.721,103.681,10.759,103.722,10.89,103.654,11.059,103.595,11.108,103.532,11.147,103.467,11.084,103.411,10.977,103.354,10.922,103.272,10.909,103.153,10.914,103.106,11.074,103.091,11.211,103.107,11.368,103.125,11.461,103.011,11.589,103.004,11.711,102.949,11.773,102.932,11.742,102.934,11.707,102.918,11.732,102.737,12.09,102.706,12.256,102.737,12.383,102.756,12.426,102.703,12.494,102.63,12.57,102.5,12.67,102.491,12.828,102.462,13.015,102.423,13.078,102.363,13.193,102.331,13.288,102.32,13.54,102.336,13.56,102.429,13.568,102.547,13.586,102.566,13.626,102.545,13.66,102.62,13.717,102.729,13.842,102.813,13.972,102.873,14.055,102.909,14.137,103.031,14.253,103.199,14.333,103.313,14.351,103.432,14.379,103.546,14.417,103.6,14.421,103.742,14.374,103.818,14.362,103.899,14.363,103.982,14.358,104.054,14.363,104.228,14.396,104.412,14.37,104.576,14.39,104.779,14.428,104.879,14.404,104.97,14.366,104.982,14.289,105.003,14.254,105.034,14.227,105.074,14.227,105.126,14.281,105.169,14.336,105.183,14.346,105.186,14.319,105.207,14.259,105.246,14.201,105.285,14.161,105.35,14.11,105.393,14.107,105.532,14.156,105.74,14.085,105.764,14.049,105.831,13.977,105.904,13.925,106.067,13.921,106.125,14.049,106.097,14.127,106.004,14.263,105.979,14.343,106.008,14.357,106.165,14.372,106.191,14.388,106.225,14.476,106.268,14.466,106.355,14.455,106.447,14.515,106.501,14.578,106.531,14.549,106.564,14.505,106.599,14.479,106.665,14.441,106.738,14.388,106.783,14.335,106.82,14.315,106.913,14.329,106.938,14.327,106.992,14.391,107.03,14.426,107.062,14.416,107.109,14.417,107.207,14.498,107.262,14.572,107.293,14.592,107.38,14.555,107.415,14.563,107.465,14.665,107.519,14.705]]]},{"n":"Myanmar","c":"MM","r":3,"ml":3,"lx":95.8,"ly":21.57,"b":[92.18,9.875,101.147,28.517],"p":[[[100.122,20.317,100.004,20.38,99.954,20.415,99.89,20.424,99.825,20.384,99.773,20.341,99.72,20.325,99.639,20.32,99.532,20.343,99.459,20.363,99.448,20.352,99.488,20.261,99.502,20.188,99.486,20.15,99.452,20.118,99.399,20.093,99.338,20.079,99.284,20.08,99.197,20.115,99.131,20.117,99.074,20.099,99.04,20.074,99.021,20.042,98.987,19.861,98.958,19.805,98.917,19.773,98.876,19.77,98.82,19.778,98.761,19.771,98.494,19.701,98.455,19.694,98.371,19.689,98.294,19.687,98.239,19.691,98.111,19.762,98.049,19.77,98.015,19.75,97.991,19.654,97.916,19.593,97.817,19.46,97.794,19.266,97.804,19.13,97.714,18.996,97.706,18.932,97.754,18.621,97.746,18.588,97.728,18.572,97.672,18.561,97.577,18.529,97.515,18.498,97.485,18.494,97.397,18.518,97.374,18.518,97.381,18.494,97.451,18.36,97.524,18.296,97.599,18.303,97.632,18.29,97.622,18.258,97.652,18.174,97.72,18.037,97.74,17.935,97.699,17.834,97.706,17.797,97.729,17.776,97.793,17.681,97.929,17.533,98.063,17.373,98.175,17.24,98.257,17.148,98.439,16.976,98.471,16.895,98.478,16.732,98.523,16.638,98.565,16.571,98.594,16.515,98.661,16.33,98.689,16.305,98.835,16.418,98.869,16.394,98.888,16.352,98.888,16.298,98.866,16.237,98.818,16.181,98.592,16.051,98.574,15.939,98.558,15.769,98.554,15.56,98.565,15.404,98.557,15.368,98.537,15.351,98.452,15.357,98.329,15.279,98.286,15.272,98.232,15.241,98.191,15.204,98.178,15.147,98.202,14.976,98.246,14.815,98.332,14.696,98.4,14.603,98.495,14.473,98.57,14.36,98.721,14.236,98.934,14.05,99.015,13.947,99.086,13.823,99.137,13.717,99.156,13.576,99.172,13.497,99.176,13.233,99.137,13.173,99.107,13.104,99.124,13.031,99.174,12.961,99.174,12.882,99.22,12.74,99.297,12.653,99.372,12.594,99.405,12.548,99.394,12.474,99.416,12.395,99.432,12.309,99.463,12.19,99.523,12.09,99.615,11.781,99.613,11.75,99.573,11.687,99.515,11.631,99.478,11.613,99.443,11.554,99.359,11.389,99.19,11.105,99.025,10.92,98.887,10.788,98.787,10.708,98.757,10.661,98.757,10.624,98.775,10.557,98.768,10.431,98.747,10.351,98.718,10.266,98.703,10.19,98.658,10.179,98.563,10.035,98.521,10.107,98.497,10.183,98.523,10.353,98.465,10.676,98.501,10.719,98.536,10.741,98.599,10.864,98.676,10.987,98.683,11.133,98.745,11.24,98.73,11.33,98.733,11.435,98.746,11.521,98.741,11.592,98.791,11.665,98.876,11.72,98.84,11.739,98.805,11.779,98.694,11.718,98.636,11.738,98.625,11.801,98.639,11.869,98.645,11.91,98.689,11.957,98.686,12.047,98.664,12.127,98.696,12.225,98.631,12.225,98.6,12.245,98.619,12.3,98.679,12.348,98.624,12.441,98.665,12.54,98.663,12.662,98.636,12.771,98.637,12.848,98.595,12.986,98.576,13.162,98.487,13.293,98.421,13.484,98.245,13.733,98.248,13.84,98.239,13.934,98.2,13.98,98.15,13.648,98.111,13.713,98.098,13.898,98.073,13.986,98.1,14.162,97.998,14.335,97.977,14.461,97.91,14.653,97.929,14.696,98.019,14.653,97.937,14.764,97.869,14.739,97.812,14.859,97.8,15.185,97.744,15.307,97.774,15.431,97.71,15.876,97.584,16.02,97.609,16.144,97.641,16.254,97.634,16.458,97.665,16.52,97.726,16.569,97.668,16.552,97.62,16.537,97.505,16.525,97.376,16.523,97.331,16.672,97.267,16.743,97.212,16.893,97.178,17.062,97.2,17.095,97.1,17.165,97.075,17.207,96.97,17.317,96.851,17.401,96.878,17.342,96.91,17.305,96.851,17.203,96.909,17.031,96.858,16.921,96.811,16.778,96.765,16.71,96.622,16.564,96.507,16.514,96.431,16.505,96.364,16.521,96.282,16.596,96.262,16.659,96.249,16.765,96.22,16.781,96.189,16.768,96.238,16.631,96.237,16.567,96.324,16.444,96.293,16.41,96.135,16.343,96.081,16.353,96.043,16.34,96.032,16.285,96.012,16.254,95.763,16.169,95.711,16.073,95.679,15.977,95.556,15.838,95.39,15.723,95.348,15.729,95.301,15.756,95.308,15.88,95.365,15.985,95.347,16.098,95.333,16.033,95.226,15.877,95.177,15.826,95.078,15.839,94.943,15.818,94.891,15.979,94.892,16.038,94.882,16.088,94.898,16.141,94.893,16.183,94.86,16.102,94.848,16.033,94.798,15.971,94.662,15.904,94.656,15.989,94.651,16.065,94.681,16.133,94.677,16.242,94.72,16.399,94.717,16.452,94.703,16.512,94.679,16.426,94.665,16.336,94.638,16.309,94.588,16.289,94.496,16.186,94.442,16.094,94.299,16.008,94.224,16.016,94.214,16.127,94.271,16.517,94.327,16.572,94.353,16.64,94.4,16.868,94.452,16.954,94.473,17.135,94.494,17.167,94.564,17.309,94.589,17.569,94.56,17.699,94.494,17.825,94.431,18.202,94.266,18.507,94.252,18.609,94.171,18.732,94.246,18.741,94.091,18.849,94.07,18.893,94.039,19.146,94.045,19.287,94.022,19.269,94.002,19.182,93.941,19.146,93.968,18.995,93.961,18.958,93.929,18.9,93.8,18.961,93.705,19.027,93.598,19.188,93.493,19.369,93.531,19.398,93.579,19.401,93.728,19.267,93.825,19.238,93.886,19.272,93.962,19.329,93.998,19.441,93.961,19.482,93.888,19.504,93.84,19.534,93.77,19.61,93.761,19.648,93.74,19.697,93.669,19.732,93.612,19.776,93.66,19.854,93.707,19.912,93.582,19.91,93.439,20.009,93.41,20.038,93.362,20.058,93.25,20.07,93.157,20.041,93.199,19.898,93.191,19.851,93.129,19.858,93.002,20.075,93.04,20.13,93.096,20.181,93.068,20.189,93.015,20.185,93.067,20.378,93.035,20.406,93.019,20.346,92.991,20.288,92.882,20.152,92.828,20.178,92.791,20.211,92.844,20.283,92.872,20.302,92.891,20.34,92.851,20.415,92.787,20.469,92.736,20.563,92.709,20.564,92.733,20.453,92.723,20.296,92.608,20.47,92.378,20.718,92.324,20.792,92.312,20.864,92.286,20.932,92.268,21.005,92.264,21.061,92.215,21.113,92.192,21.202,92.18,21.293,92.208,21.358,92.28,21.428,92.331,21.44,92.373,21.409,92.472,21.363,92.539,21.32,92.569,21.263,92.6,21.27,92.632,21.306,92.625,21.351,92.593,21.467,92.584,21.609,92.583,21.94,92.575,21.978,92.63,22.011,92.653,22.049,92.675,22.106,92.689,22.131,92.721,22.132,92.771,22.105,92.854,22.01,92.909,21.989,92.965,22.004,93.022,22.146,93.043,22.184,93.071,22.209,93.121,22.205,93.151,22.231,93.162,22.292,93.162,22.36,93.105,22.547,93.088,22.633,93.079,22.718,93.114,22.806,93.163,22.908,93.151,22.997,93.164,23.032,93.204,23.037,93.254,23.015,93.308,23.03,93.349,23.085,93.366,23.133,93.391,23.339,93.408,23.528,93.415,23.682,93.373,23.774,93.307,24.022,93.326,24.064,93.356,24.074,93.452,23.987,93.494,23.973,93.564,23.986,93.633,24.005,93.683,24.007,93.756,23.977,93.855,23.944,94.011,23.903,94.075,23.872,94.128,23.876,94.17,23.973,94.22,24.113,94.293,24.322,94.377,24.474,94.399,24.514,94.493,24.638,94.584,24.767,94.663,24.931,94.708,25.049,94.704,25.098,94.675,25.139,94.616,25.165,94.567,25.192,94.553,25.216,94.554,25.243,94.58,25.32,94.623,25.41,94.668,25.459,94.786,25.519,94.861,25.597,94.946,25.7,94.992,25.77,95.015,25.913,95.041,25.941,95.093,25.987,95.132,26.041,95.129,26.07,95.108,26.091,95.069,26.191,95.051,26.347,95.06,26.474,95.089,26.525,95.129,26.597,95.201,26.641,95.305,26.672,95.464,26.756,95.738,26.95,95.837,27.014,95.905,27.047,95.971,27.128,96.061,27.217,96.191,27.261,96.274,27.278,96.666,27.339,96.732,27.331,96.798,27.296,96.88,27.178,96.953,27.133,97.038,27.102,97.102,27.115,97.104,27.163,96.902,27.44,96.884,27.515,96.877,27.587,96.9,27.644,96.963,27.698,97.05,27.76,97.158,27.837,97.226,27.89,97.306,27.907,97.335,27.938,97.344,27.982,97.339,28.031,97.303,28.086,97.31,28.155,97.322,28.218,97.356,28.254,97.431,28.354,97.478,28.426,97.502,28.456,97.538,28.51,97.599,28.517,97.659,28.5,97.695,28.469,97.73,28.407,97.769,28.356,97.817,28.356,97.865,28.364,97.888,28.356,97.934,28.314,98.022,28.212,98.062,28.186,98.099,28.142,98.118,28.055,98.13,27.968,98.241,27.663,98.274,27.599,98.299,27.55,98.35,27.538,98.392,27.587,98.409,27.639,98.453,27.657,98.504,27.648,98.6,27.599,98.651,27.572,98.677,27.422,98.682,27.245,98.675,27.191,98.717,27.045,98.729,26.877,98.738,26.786,98.739,26.698,98.732,26.583,98.709,26.43,98.672,26.299,98.686,26.189,98.663,26.139,98.572,26.114,98.564,26.072,98.591,26.004,98.655,25.918,98.656,25.864,98.625,25.827,98.558,25.823,98.466,25.789,98.402,25.678,98.334,25.587,98.297,25.569,98.173,25.595,98.143,25.571,98.1,25.416,98.064,25.349,98.011,25.293,97.962,25.259,97.918,25.236,97.82,25.252,97.767,25.158,97.715,25.034,97.711,24.97,97.738,24.87,97.724,24.842,97.671,24.82,97.583,24.775,97.529,24.631,97.531,24.492,97.563,24.444,97.624,24.423,97.667,24.38,97.671,24.313,97.708,24.229,97.691,24.131,97.568,23.988,97.565,23.911,97.63,23.887,97.686,23.898,97.756,23.932,97.838,23.986,98.017,24.065,98.213,24.111,98.367,24.119,98.499,24.116,98.564,24.099,98.583,24.07,98.764,24.116,98.802,24.119,98.835,24.121,98.834,24.091,98.702,23.964,98.677,23.905,98.681,23.842,98.735,23.783,98.788,23.738,98.832,23.624,98.798,23.52,98.82,23.483,98.859,23.44,98.883,23.38,98.886,23.307,98.864,23.191,99.055,23.131,99.22,23.103,99.341,23.096,99.418,23.069,99.465,23.046,99.497,23.005,99.507,22.959,99.467,22.927,99.385,22.825,99.338,22.689,99.343,22.587,99.338,22.498,99.243,22.37,99.205,22.283,99.172,22.192,99.173,22.153,99.193,22.126,99.233,22.11,99.303,22.101,99.389,22.111,99.593,22.089,99.825,22.05,99.918,22.028,99.948,21.988,99.94,21.902,99.926,21.821,99.941,21.759,99.978,21.702,100.041,21.683,100.096,21.661,100.106,21.617,100.089,21.558,100.117,21.511,100.148,21.481,100.215,21.463,100.351,21.501,100.446,21.484,100.531,21.458,100.605,21.472,100.677,21.505,100.835,21.655,101.019,21.736,101.08,21.756,101.121,21.746,101.131,21.736,101.128,21.705,101.147,21.582,101.139,21.567,101.08,21.469,100.928,21.366,100.82,21.314,100.757,21.313,100.703,21.251,100.659,21.13,100.614,21.059,100.567,21.038,100.536,20.992,100.522,20.922,100.549,20.884,100.618,20.879,100.623,20.86,100.565,20.825,100.493,20.813,100.407,20.823,100.326,20.796,100.249,20.73,100.184,20.589,100.13,20.372,100.122,20.317]],[[98.21,10.953,98.293,10.78,98.284,10.753,98.271,10.74,98.252,10.744,98.218,10.838,98.155,10.898,98.08,10.887,98.143,10.963,98.167,10.98,98.21,10.953]],[[98.183,9.933,98.134,9.875,98.118,9.878,98.14,9.975,98.221,10.045,98.292,10.051,98.283,10.008,98.231,9.954,98.183,9.933]],[[98.222,11.478,98.216,11.456,98.209,11.457,98.187,11.472,98.201,11.567,98.239,11.645,98.278,11.758,98.3,11.783,98.308,11.723,98.284,11.594,98.263,11.524,98.222,11.478]],[[98.516,11.905,98.474,11.899,98.454,12.061,98.466,12.084,98.525,12.005,98.61,11.957,98.576,11.925,98.516,11.905]],[[98.554,11.745,98.528,11.539,98.465,11.567,98.435,11.567,98.397,11.684,98.4,11.715,98.376,11.792,98.524,11.805,98.554,11.745]],[[98.414,12.598,98.436,12.571,98.468,12.571,98.459,12.474,98.381,12.354,98.334,12.336,98.314,12.336,98.331,12.511,98.303,12.612,98.312,12.678,98.396,12.647,98.414,12.598]],[[98.137,12.15,98.125,12.145,98.108,12.148,98.075,12.164,98.037,12.232,98.057,12.28,98.071,12.292,98.105,12.288,98.122,12.279,98.128,12.261,98.118,12.223,98.12,12.191,98.137,12.15]],[[94.477,15.946,94.412,15.848,94.388,15.994,94.494,16.075,94.546,16.153,94.601,16.206,94.619,16.141,94.566,16.019,94.477,15.946]],[[97.575,16.253,97.537,16.24,97.48,16.306,97.469,16.461,97.516,16.497,97.542,16.505,97.579,16.486,97.593,16.461,97.6,16.43,97.589,16.397,97.575,16.253]],[[93.691,18.684,93.674,18.676,93.57,18.76,93.488,18.868,93.618,18.889,93.745,18.866,93.746,18.808,93.718,18.716,93.691,18.684]],[[93.492,19.893,93.513,19.755,93.445,19.806,93.42,19.878,93.413,19.95,93.492,19.893]],[[93.715,19.558,93.829,19.475,93.875,19.481,93.946,19.429,93.947,19.408,93.934,19.365,93.902,19.332,93.815,19.299,93.756,19.326,93.732,19.416,93.662,19.459,93.644,19.495,93.688,19.544,93.715,19.558]],[[98.542,10.962,98.519,10.959,98.498,10.964,98.477,10.98,98.527,11.087,98.542,10.962]],[[98.075,11.692,98.084,11.637,98.021,11.696,98.01,11.86,98.06,11.757,98.081,11.733,98.075,11.692]],[[98.066,12.39,98.06,12.354,98.002,12.279,97.952,12.322,97.939,12.346,97.99,12.394,98.045,12.387,98.06,12.398,98.066,12.39]],[[98.315,13.099,98.309,12.935,98.259,13.014,98.251,13.104,98.255,13.189,98.265,13.202,98.269,13.189,98.299,13.152,98.315,13.099]],[[94.805,15.819,94.784,15.794,94.743,15.812,94.733,15.823,94.828,15.933,94.838,15.892,94.805,15.819]],[[93.01,19.924,93.023,19.829,92.975,19.868,92.913,20,92.915,20.086,92.96,20.046,93.01,19.924]]]},{"n":"Burundi","c":"BI","r":6,"ml":4,"lx":29.92,"ly":-3.33,"b":[29.014,-4.456,30.811,-2.313],"p":[[[30.554,-2.4,30.534,-2.426,30.442,-2.613,30.424,-2.642,30.434,-2.659,30.473,-2.694,30.45,-2.753,30.441,-2.769,30.424,-2.824,30.433,-2.875,30.456,-2.893,30.515,-2.918,30.604,-2.935,30.709,-2.977,30.78,-2.985,30.797,-3.015,30.794,-3.069,30.811,-3.116,30.811,-3.201,30.79,-3.275,30.682,-3.309,30.626,-3.347,30.611,-3.366,30.625,-3.389,30.632,-3.419,30.53,-3.492,30.425,-3.589,30.4,-3.654,30.379,-3.731,30.348,-3.78,30.269,-3.85,30.187,-3.993,30.147,-4.085,29.947,-4.307,29.77,-4.418,29.718,-4.456,29.403,-4.449,29.379,-4.3,29.331,-4.095,29.223,-3.911,29.212,-3.834,29.217,-3.685,29.217,-3.476,29.21,-3.363,29.212,-3.281,29.226,-3.139,29.224,-3.054,29.153,-2.955,29.065,-2.851,29.017,-2.8,29.014,-2.758,29.014,-2.72,29.029,-2.665,29.063,-2.603,29.102,-2.596,29.198,-2.62,29.297,-2.673,29.35,-2.792,29.39,-2.809,29.464,-2.808,29.651,-2.793,29.698,-2.795,29.783,-2.766,29.868,-2.716,29.893,-2.665,29.912,-2.549,29.93,-2.34,29.973,-2.337,30.092,-2.412,30.117,-2.417,30.142,-2.414,30.183,-2.377,30.234,-2.347,30.271,-2.348,30.408,-2.313,30.482,-2.376,30.529,-2.396,30.554,-2.4]]]},{"n":"Burkina Faso","c":"BF","r":3,"ml":3,"lx":-1.36,"ly":12.67,"b":[-5.524,9.425,2.389,15.078],"p":[[[0.9,10.993,0.643,10.983,0.549,10.955,0.493,10.955,0.491,10.978,0.484,10.992,0.159,11.07,-0.069,11.116,-0.299,11.167,-0.313,11.119,-0.346,11.088,-0.396,11.086,-0.43,11.093,-0.454,11.056,-0.492,11.008,-0.545,10.984,-0.598,10.954,-0.627,10.927,-0.649,10.927,-0.701,10.989,-0.772,10.995,-0.903,10.985,-0.962,11.002,-1.042,11.01,-1.233,10.997,-1.537,11.023,-1.586,11.009,-1.6,10.998,-1.901,10.995,-2.232,10.991,-2.509,10.989,-2.752,10.986,-2.752,10.997,-2.83,10.998,-2.839,10.977,-2.907,10.728,-2.915,10.592,-2.878,10.508,-2.837,10.455,-2.791,10.432,-2.787,10.402,-2.823,10.363,-2.82,10.323,-2.777,10.282,-2.767,10.238,-2.788,10.193,-2.783,10.083,-2.751,9.91,-2.75,9.797,-2.781,9.746,-2.766,9.658,-2.706,9.534,-2.696,9.481,-2.717,9.457,-2.767,9.425,-2.817,9.426,-2.875,9.501,-2.901,9.535,-2.948,9.611,-2.988,9.687,-3.043,9.721,-3.096,9.752,-3.161,9.849,-3.224,9.895,-3.29,9.882,-3.386,9.9,-3.581,9.924,-3.791,9.917,-3.878,9.895,-3.963,9.86,-4.181,9.782,-4.267,9.743,-4.332,9.646,-4.406,9.648,-4.48,9.679,-4.527,9.723,-4.626,9.714,-4.722,9.757,-4.814,9.841,-4.883,9.869,-4.97,9.93,-4.994,10.046,-5.049,10.128,-5.1,10.242,-5.175,10.293,-5.262,10.32,-5.382,10.314,-5.461,10.36,-5.524,10.426,-5.507,10.483,-5.479,10.565,-5.476,10.644,-5.457,10.771,-5.469,10.931,-5.49,11.042,-5.424,11.089,-5.347,11.13,-5.3,11.206,-5.25,11.376,-5.229,11.522,-5.245,11.577,-5.27,11.62,-5.291,11.683,-5.302,11.76,-5.288,11.828,-5.23,11.89,-5.158,11.942,-5.106,11.968,-4.969,11.993,-4.798,12.032,-4.699,12.076,-4.627,12.12,-4.587,12.155,-4.546,12.226,-4.48,12.282,-4.429,12.338,-4.422,12.493,-4.422,12.582,-4.46,12.63,-4.481,12.672,-4.227,12.794,-4.225,12.879,-4.261,12.975,-4.31,13.052,-4.329,13.119,-4.259,13.197,-4.196,13.256,-4.151,13.306,-4.051,13.382,-3.947,13.402,-3.853,13.374,-3.576,13.194,-3.528,13.183,-3.47,13.196,-3.397,13.244,-3.302,13.281,-3.267,13.401,-3.27,13.577,-3.249,13.658,-3.198,13.673,-3.039,13.639,-2.997,13.637,-2.951,13.648,-2.917,13.679,-2.919,13.736,-2.926,13.787,-2.874,13.951,-2.779,14.074,-2.587,14.228,-2.527,14.258,-2.457,14.274,-2.113,14.168,-2.057,14.195,-1.973,14.457,-1.88,14.481,-1.768,14.486,-1.695,14.508,-1.657,14.527,-1.494,14.626,-1.205,14.762,-1.05,14.82,-1.019,14.841,-0.908,14.937,-0.76,15.048,-0.666,15.07,-0.537,15.078,-0.454,15.06,-0.432,15.029,-0.405,15.013,-0.236,15.059,0.007,14.985,0.217,14.911,0.204,14.865,0.203,14.783,0.185,14.653,0.164,14.497,0.251,14.396,0.355,14.288,0.383,14.246,0.355,14.139,0.374,14.076,0.429,13.972,0.522,13.84,0.618,13.703,0.685,13.685,0.748,13.675,0.786,13.65,0.842,13.626,0.898,13.611,0.947,13.581,0.978,13.552,1.018,13.468,1.126,13.412,1.201,13.358,1.171,13.33,1.077,13.341,0.988,13.365,0.977,13.325,0.973,13.17,0.987,13.042,1.008,13.025,1.097,13.001,1.309,12.834,1.5,12.676,1.565,12.635,1.671,12.62,1.79,12.613,1.841,12.628,1.956,12.707,2.017,12.716,2.074,12.714,2.105,12.701,2.16,12.636,2.212,12.538,2.226,12.466,2.221,12.427,2.204,12.413,2.109,12.394,2.069,12.379,2.058,12.358,2.073,12.309,2.091,12.278,2.194,12.136,2.343,11.946,2.389,11.897,2.363,11.84,2.287,11.691,2.231,11.629,1.98,11.418,1.858,11.443,1.6,11.401,1.561,11.449,1.501,11.456,1.427,11.447,1.4,11.429,1.392,11.408,1.365,11.379,1.317,11.295,1.28,11.274,1.235,11.261,1.179,11.263,1.146,11.252,1.146,11.21,1.136,11.174,1.098,11.156,1.082,11.116,1.085,11.076,1.062,11.058,1.014,11.068,0.985,11.079,0.958,11.028,0.925,10.993,0.9,10.993]]]},{"n":"Bulgaria","c":"BG","r":4,"ml":4,"lx":25.16,"ly":42.51,"b":[22.344,41.244,28.585,44.238],"p":[[[28.014,41.969,27.879,41.987,27.832,41.981,27.802,41.957,27.739,41.962,27.661,41.961,27.58,41.933,27.535,41.921,27.475,41.947,27.363,42.025,27.295,42.08,27.244,42.093,27.193,42.077,27.012,42.059,26.969,42.027,26.885,41.992,26.8,41.975,26.679,41.963,26.615,41.965,26.58,41.948,26.55,41.897,26.529,41.847,26.511,41.826,26.36,41.802,26.327,41.773,26.318,41.745,26.321,41.717,26.201,41.744,26.107,41.726,26.086,41.704,26.066,41.673,26.077,41.64,26.111,41.608,26.144,41.522,26.155,41.435,26.135,41.386,26.066,41.351,25.923,41.312,25.785,41.33,25.724,41.315,25.621,41.31,25.527,41.3,25.382,41.264,25.251,41.244,25.133,41.316,24.994,41.365,24.847,41.394,24.796,41.373,24.774,41.356,24.651,41.42,24.596,41.443,24.569,41.467,24.518,41.553,24.488,41.555,24.387,41.524,24.289,41.525,24.23,41.531,24.056,41.527,24.033,41.469,24.011,41.46,23.974,41.452,23.881,41.456,23.762,41.413,23.635,41.387,23.536,41.386,23.433,41.399,23.372,41.39,23.24,41.385,23.156,41.322,23.026,41.326,22.916,41.336,22.93,41.356,22.951,41.606,23.006,41.717,23.004,41.74,22.992,41.757,22.944,41.775,22.909,41.835,22.837,41.994,22.796,42.026,22.682,42.059,22.583,42.105,22.498,42.165,22.344,42.314,22.422,42.329,22.446,42.359,22.524,42.441,22.532,42.481,22.524,42.504,22.472,42.543,22.436,42.629,22.463,42.709,22.466,42.751,22.439,42.792,22.467,42.842,22.523,42.87,22.558,42.878,22.706,42.884,22.8,42.986,22.857,43.018,22.915,43.076,22.942,43.097,22.968,43.142,22.977,43.188,22.86,43.252,22.82,43.301,22.768,43.354,22.697,43.391,22.555,43.454,22.499,43.519,22.474,43.602,22.436,43.665,22.395,43.707,22.387,43.74,22.37,43.781,22.365,43.862,22.399,43.97,22.421,44.007,22.469,44.018,22.597,44.075,22.603,44.149,22.627,44.194,22.667,44.22,22.705,44.238,22.775,44.195,22.945,44.127,23.029,44.078,23.024,44.047,22.985,44.017,22.911,43.987,22.868,43.948,22.856,43.899,22.868,43.865,22.919,43.834,23.225,43.874,23.535,43.854,23.951,43.787,24.227,43.763,24.431,43.794,24.808,43.738,25.16,43.686,25.497,43.671,25.686,43.712,25.819,43.767,25.933,43.871,26.216,44.007,26.489,44.084,26.848,44.146,27.087,44.167,27.121,44.146,27.425,44.021,27.561,44.02,27.671,43.998,27.711,43.965,27.739,43.956,27.884,43.987,27.949,43.919,28.05,43.822,28.222,43.773,28.375,43.745,28.423,43.74,28.585,43.742,28.562,43.501,28.465,43.389,28.32,43.427,28.134,43.396,28.035,43.268,27.979,43.231,27.929,43.186,27.896,43.021,27.889,42.75,27.818,42.717,27.754,42.707,27.485,42.468,27.64,42.401,27.708,42.35,27.821,42.208,27.983,42.047,28.014,41.969]]]},{"n":"Brunei","c":"BN","r":6,"ml":4,"lx":114.55,"ly":4.45,"b":[114.064,4.024,115.327,5.022],"p":[[[115.14,4.9,115.168,4.867,115.228,4.751,115.267,4.634,115.279,4.456,115.327,4.381,115.319,4.365,115.291,4.353,115.247,4.347,115.171,4.364,115.107,4.39,115.052,4.583,115.027,4.691,115.029,4.821,115.027,4.9,115.14,4.9]],[[115.027,4.9,114.945,4.856,114.865,4.802,114.784,4.755,114.747,4.718,114.76,4.667,114.779,4.553,114.79,4.464,114.818,4.429,114.84,4.393,114.831,4.354,114.783,4.281,114.81,4.267,114.776,4.169,114.725,4.097,114.654,4.038,114.608,4.024,114.572,4.049,114.512,4.114,114.447,4.204,114.417,4.256,114.323,4.263,114.29,4.304,114.288,4.355,114.261,4.414,114.224,4.478,114.169,4.527,114.095,4.565,114.064,4.593,114.178,4.591,114.299,4.607,114.424,4.66,114.545,4.725,114.646,4.798,114.741,4.881,114.841,4.946,114.995,5.022,115.048,5.016,115.047,4.962,115.027,4.9]]]},{"n":"Brazil","c":"BR","r":2,"ml":1.7,"lx":-49.56,"ly":-12.1,"b":[-74.002,-33.742,-34.805,5.258],"p":[[[-66.876,1.223,-66.619,0.992,-66.429,0.822,-66.347,0.767,-66.302,0.752,-66.191,0.763,-66.06,0.785,-65.996,0.81,-65.926,0.863,-65.811,0.937,-65.718,0.978,-65.681,0.983,-65.645,0.97,-65.566,0.926,-65.523,0.843,-65.563,0.748,-65.556,0.688,-65.473,0.691,-65.407,0.79,-65.361,0.869,-65.264,0.932,-65.17,1.022,-65.104,1.108,-65.027,1.158,-64.91,1.22,-64.818,1.257,-64.732,1.253,-64.667,1.294,-64.584,1.37,-64.526,1.431,-64.486,1.453,-64.405,1.447,-64.304,1.455,-64.205,1.529,-64.115,1.619,-64.067,1.771,-64.035,1.904,-64.008,1.932,-63.976,1.953,-63.937,1.967,-63.844,1.977,-63.682,2.048,-63.57,2.121,-63.464,2.136,-63.433,2.156,-63.394,2.223,-63.375,2.34,-63.389,2.412,-63.585,2.434,-63.713,2.434,-63.924,2.452,-64.025,2.482,-64.047,2.502,-64.049,2.525,-64.029,2.576,-64.009,2.672,-64.038,2.802,-64.144,3.005,-64.219,3.205,-64.229,3.344,-64.227,3.491,-64.221,3.587,-64.275,3.663,-64.568,3.9,-64.669,4.012,-64.703,4.089,-64.818,4.232,-64.789,4.276,-64.722,4.274,-64.666,4.237,-64.614,4.158,-64.576,4.14,-64.526,4.14,-64.256,4.14,-64.192,4.127,-64.154,4.1,-64.122,4.067,-64.073,3.974,-64.021,3.929,-63.915,3.931,-63.747,3.933,-63.653,3.941,-63.597,3.915,-63.527,3.894,-63.38,3.943,-63.339,3.944,-63.295,3.922,-63.136,3.756,-63.045,3.686,-62.969,3.594,-62.857,3.593,-62.765,3.673,-62.74,3.94,-62.712,4.018,-62.665,4.04,-62.61,4.042,-62.544,4.084,-62.473,4.139,-62.411,4.157,-62.153,4.098,-62.082,4.126,-61.821,4.197,-61.554,4.288,-61.479,4.402,-61.368,4.433,-61.28,4.517,-61.209,4.508,-61.102,4.505,-61.036,4.519,-61.003,4.535,-60.966,4.575,-60.906,4.687,-60.833,4.729,-60.742,4.774,-60.679,4.827,-60.628,4.893,-60.604,4.949,-60.604,4.995,-60.635,5.082,-60.672,5.164,-60.712,5.192,-60.742,5.202,-60.651,5.221,-60.576,5.192,-60.46,5.188,-60.409,5.21,-60.335,5.199,-60.242,5.258,-60.182,5.239,-60.142,5.239,-60.106,5.194,-60.078,5.144,-59.991,5.083,-59.999,4.99,-60.015,4.908,-60.027,4.813,-60.032,4.741,-60.069,4.667,-60.125,4.598,-60.141,4.57,-60.149,4.533,-60.111,4.511,-60.045,4.505,-59.962,4.502,-59.906,4.48,-59.833,4.476,-59.746,4.417,-59.703,4.381,-59.7,4.354,-59.727,4.288,-59.739,4.227,-59.717,4.188,-59.691,4.16,-59.62,4.023,-59.586,3.975,-59.558,3.96,-59.551,3.934,-59.575,3.883,-59.604,3.82,-59.67,3.753,-59.679,3.7,-59.732,3.667,-59.854,3.588,-59.833,3.462,-59.829,3.399,-59.831,3.349,-59.873,3.283,-59.946,3.088,-59.972,2.99,-59.996,2.765,-59.994,2.69,-59.961,2.588,-59.89,2.363,-59.849,2.327,-59.755,2.274,-59.744,2.122,-59.752,1.962,-59.756,1.901,-59.741,1.874,-59.699,1.861,-59.669,1.842,-59.664,1.795,-59.667,1.746,-59.597,1.718,-59.536,1.7,-59.479,1.632,-59.378,1.527,-59.337,1.508,-59.317,1.465,-59.231,1.376,-59.1,1.344,-58.969,1.305,-58.917,1.249,-58.862,1.204,-58.822,1.201,-58.787,1.208,-58.73,1.248,-58.685,1.281,-58.605,1.279,-58.512,1.285,-58.496,1.312,-58.487,1.348,-58.506,1.439,-58.473,1.466,-58.396,1.482,-58.38,1.53,-58.363,1.557,-58.341,1.588,-58.314,1.592,-58.281,1.574,-58.23,1.563,-58.173,1.548,-58.142,1.517,-58.091,1.514,-58.035,1.52,-58.012,1.54,-57.995,1.574,-57.983,1.648,-57.946,1.651,-57.873,1.667,-57.796,1.7,-57.692,1.705,-57.594,1.704,-57.546,1.726,-57.5,1.774,-57.413,1.909,-57.367,1.94,-57.317,1.963,-57.276,1.959,-57.19,1.982,-57.119,2.014,-57.093,2.006,-57.038,1.936,-57.01,1.921,-56.97,1.916,-56.837,1.881,-56.766,1.892,-56.69,1.914,-56.616,1.923,-56.564,1.907,-56.525,1.927,-56.483,1.942,-56.453,1.932,-56.386,1.924,-56.227,1.885,-56.02,1.842,-55.963,1.857,-55.93,1.888,-55.922,1.977,-55.915,2.04,-55.962,2.095,-56.02,2.158,-56.074,2.237,-56.138,2.259,-56.129,2.3,-56.088,2.341,-56.045,2.364,-56.02,2.393,-55.994,2.498,-55.976,2.516,-55.957,2.52,-55.936,2.517,-55.894,2.49,-55.731,2.406,-55.659,2.419,-55.385,2.441,-55.344,2.489,-55.286,2.5,-55.188,2.548,-55.149,2.551,-55.114,2.539,-55.07,2.548,-55.006,2.593,-54.979,2.598,-54.968,2.548,-54.927,2.497,-54.876,2.45,-54.852,2.44,-54.767,2.455,-54.722,2.442,-54.703,2.398,-54.697,2.36,-54.662,2.328,-54.616,2.327,-54.592,2.314,-54.55,2.293,-54.515,2.245,-54.433,2.208,-54.293,2.154,-54.228,2.153,-54.167,2.137,-54.13,2.121,-54.09,2.15,-53.946,2.233,-53.877,2.278,-53.83,2.313,-53.794,2.346,-53.768,2.355,-53.75,2.335,-53.735,2.309,-53.684,2.293,-53.564,2.262,-53.509,2.253,-53.432,2.279,-53.366,2.324,-53.334,2.34,-53.285,2.295,-53.252,2.232,-53.23,2.205,-53.18,2.211,-53.082,2.202,-53.01,2.182,-52.965,2.184,-52.903,2.212,-52.87,2.267,-52.783,2.317,-52.701,2.364,-52.653,2.426,-52.583,2.529,-52.559,2.573,-52.555,2.648,-52.456,2.864,-52.418,2.904,-52.396,2.972,-52.357,3.052,-52.357,3.118,-52.328,3.182,-52.271,3.237,-52.229,3.272,-52.163,3.365,-52.116,3.452,-52,3.647,-51.991,3.702,-51.944,3.735,-51.929,3.777,-51.879,3.829,-51.827,3.87,-51.805,3.93,-51.767,3.993,-51.683,4.04,-51.653,4.061,-51.558,4.234,-51.547,4.311,-51.462,4.314,-51.327,4.225,-51.22,4.094,-51.076,3.672,-51.052,3.282,-50.994,3.078,-50.827,2.652,-50.817,2.573,-50.79,2.478,-50.737,2.377,-50.679,2.21,-50.677,2.179,-50.714,2.134,-50.659,2.131,-50.609,2.104,-50.576,1.999,-50.534,1.927,-50.459,1.83,-50.304,1.798,-50.188,1.786,-50.055,1.731,-49.957,1.66,-49.882,1.42,-49.906,1.269,-49.899,1.163,-49.938,1.121,-50.047,1.052,-50.071,1.015,-50.294,0.836,-50.343,0.751,-50.463,0.637,-50.582,0.421,-50.755,0.223,-50.816,0.173,-50.91,0.161,-50.967,0.13,-51.102,-0.031,-51.283,-0.085,-51.3,-0.179,-51.404,-0.393,-51.496,-0.509,-51.555,-0.549,-51.703,-0.762,-51.722,-0.855,-51.721,-1.018,-51.819,-1.118,-51.922,-1.181,-51.934,-1.32,-51.981,-1.368,-52.02,-1.399,-52.229,-1.362,-52.553,-1.514,-52.664,-1.552,-52.31,-1.56,-52.197,-1.64,-51.948,-1.587,-51.646,-1.394,-51.531,-1.354,-51.297,-1.224,-51.202,-1.137,-51.029,-1.032,-50.992,-0.986,-50.895,-0.938,-50.842,-1,-50.838,-1.039,-50.918,-1.115,-50.897,-1.164,-50.845,-1.226,-50.826,-1.311,-50.819,-1.376,-50.786,-1.49,-50.679,-1.644,-50.675,-1.695,-50.69,-1.762,-50.639,-1.817,-50.586,-1.85,-50.403,-2.016,-50.26,-1.923,-50.173,-1.896,-50.117,-1.858,-49.999,-1.832,-49.903,-1.871,-49.72,-1.926,-49.585,-1.867,-49.314,-1.732,-49.399,-1.972,-49.46,-2.192,-49.507,-2.28,-49.553,-2.52,-49.599,-2.584,-49.637,-2.657,-49.576,-2.631,-49.524,-2.597,-49.458,-2.505,-49.408,-2.344,-49.211,-1.917,-49.155,-1.879,-48.991,-1.83,-48.71,-1.488,-48.6,-1.489,-48.53,-1.567,-48.463,-1.614,-48.446,-1.52,-48.35,-1.482,-48.451,-1.436,-48.468,-1.394,-48.478,-1.324,-48.409,-1.229,-48.45,-1.146,-48.306,-1.04,-48.318,-0.961,-48.266,-0.895,-48.202,-0.828,-48.128,-0.795,-48.115,-0.737,-48.069,-0.714,-48.033,-0.705,-47.961,-0.77,-47.883,-0.693,-47.808,-0.663,-47.774,-0.677,-47.731,-0.71,-47.687,-0.725,-47.651,-0.719,-47.557,-0.67,-47.471,-0.749,-47.419,-0.766,-47.433,-0.722,-47.46,-0.681,-47.439,-0.648,-47.398,-0.627,-47.269,-0.645,-47.201,-0.68,-47.127,-0.745,-47.025,-0.75,-46.944,-0.743,-46.894,-0.78,-46.811,-0.78,-46.77,-0.837,-46.644,-0.916,-46.617,-0.971,-46.516,-0.997,-46.422,-1.03,-46.321,-1.039,-46.219,-1.031,-46.215,-1.1,-46.14,-1.118,-46.045,-1.103,-45.972,-1.187,-45.779,-1.251,-45.645,-1.348,-45.557,-1.331,-45.459,-1.356,-45.353,-1.567,-45.329,-1.717,-45.282,-1.697,-45.239,-1.629,-45.182,-1.507,-45.076,-1.466,-45.026,-1.513,-44.92,-1.589,-44.828,-1.672,-44.79,-1.725,-44.721,-1.733,-44.779,-1.799,-44.721,-1.792,-44.651,-1.746,-44.592,-1.842,-44.547,-1.946,-44.538,-2.053,-44.58,-2.114,-44.617,-2.152,-44.659,-2.228,-44.708,-2.241,-44.756,-2.266,-44.701,-2.32,-44.662,-2.373,-44.579,-2.23,-44.52,-2.19,-44.435,-2.168,-44.391,-2.27,-44.382,-2.366,-44.52,-2.405,-44.521,-2.481,-44.562,-2.524,-44.589,-2.573,-44.611,-2.677,-44.639,-2.762,-44.721,-3.142,-44.723,-3.205,-44.623,-3.138,-44.438,-2.944,-44.381,-2.738,-44.308,-2.535,-44.229,-2.471,-44.179,-2.471,-44.106,-2.493,-44.101,-2.56,-44.113,-2.599,-44.192,-2.7,-44.225,-2.755,-44.193,-2.81,-44.013,-2.642,-43.933,-2.583,-43.864,-2.595,-43.729,-2.518,-43.455,-2.502,-43.435,-2.414,-43.38,-2.376,-43.23,-2.386,-42.937,-2.465,-42.832,-2.53,-42.676,-2.59,-42.594,-2.661,-42.25,-2.792,-42,-2.806,-41.876,-2.747,-41.722,-2.809,-41.64,-2.879,-41.48,-2.917,-41.318,-2.936,-41.195,-2.886,-40.876,-2.87,-40.475,-2.796,-40.235,-2.813,-39.965,-2.862,-39.772,-2.986,-39.609,-3.056,-39.511,-3.126,-39.353,-3.197,-39.014,-3.39,-38.896,-3.502,-38.686,-3.654,-38.476,-3.717,-38.362,-3.876,-38.272,-3.948,-38.049,-4.216,-37.796,-4.404,-37.626,-4.592,-37.301,-4.713,-37.175,-4.912,-36.955,-4.937,-36.861,-4.967,-36.747,-5.051,-36.591,-5.098,-36.387,-5.084,-36.162,-5.094,-35.98,-5.054,-35.549,-5.129,-35.482,-5.166,-35.393,-5.251,-35.235,-5.567,-35.142,-5.917,-35.095,-6.185,-34.988,-6.394,-34.93,-6.785,-34.88,-6.908,-34.876,-7.003,-34.834,-7.024,-34.805,-7.288,-34.817,-7.395,-34.858,-7.533,-34.861,-7.595,-34.855,-7.634,-34.873,-7.692,-34.879,-7.747,-34.837,-7.872,-34.835,-7.971,-34.891,-8.092,-34.967,-8.408,-35.158,-8.931,-35.341,-9.231,-35.597,-9.541,-35.764,-9.703,-35.83,-9.719,-35.891,-9.687,-35.848,-9.772,-35.885,-9.848,-36.055,-10.076,-36.224,-10.225,-36.398,-10.484,-36.412,-10.49,-36.636,-10.59,-36.768,-10.672,-36.938,-10.82,-37.093,-11.055,-37.125,-11.085,-37.183,-11.068,-37.181,-11.187,-37.315,-11.376,-37.356,-11.404,-37.355,-11.35,-37.332,-11.31,-37.321,-11.267,-37.322,-11.215,-37.359,-11.253,-37.438,-11.394,-37.412,-11.497,-37.469,-11.654,-37.689,-12.1,-37.957,-12.475,-38.019,-12.591,-38.24,-12.844,-38.402,-12.966,-38.447,-12.967,-38.499,-12.957,-38.525,-12.762,-38.654,-12.645,-38.691,-12.624,-38.744,-12.749,-38.788,-12.783,-38.852,-12.79,-38.784,-12.844,-38.764,-12.907,-38.833,-13.033,-38.835,-13.147,-38.959,-13.273,-39.031,-13.365,-39.067,-13.48,-39.089,-13.588,-39.035,-13.559,-39.009,-13.581,-38.989,-13.615,-39.001,-13.665,-39.041,-13.758,-39.035,-13.991,-39.048,-14.044,-39.008,-14.101,-38.967,-14.003,-38.942,-14.031,-39.06,-14.655,-39.013,-14.936,-38.996,-15.254,-38.943,-15.564,-38.885,-15.842,-38.881,-15.864,-38.961,-16.187,-39.063,-16.504,-39.125,-16.764,-39.164,-17.044,-39.203,-17.178,-39.215,-17.316,-39.171,-17.642,-39.154,-17.704,-39.278,-17.849,-39.413,-17.92,-39.487,-17.99,-39.651,-18.252,-39.74,-18.64,-39.742,-18.846,-39.7,-19.278,-39.731,-19.454,-39.783,-19.572,-39.845,-19.649,-40.001,-19.742,-40.142,-19.968,-40.203,-20.206,-40.299,-20.293,-40.319,-20.426,-40.396,-20.569,-40.597,-20.784,-40.727,-20.846,-40.789,-20.906,-40.829,-21.031,-40.955,-21.238,-41.047,-21.506,-41.023,-21.597,-41.022,-21.611,-40.988,-21.92,-41,-21.999,-41.123,-22.084,-41.583,-22.244,-41.706,-22.31,-41.98,-22.581,-41.998,-22.645,-41.986,-22.736,-41.941,-22.788,-41.987,-22.845,-42.042,-22.947,-42.122,-22.941,-42.581,-22.941,-42.829,-22.973,-42.958,-22.967,-43.016,-22.943,-43.081,-22.903,-43.101,-22.85,-43.065,-22.771,-43.086,-22.723,-43.154,-22.725,-43.229,-22.748,-43.242,-22.795,-43.237,-22.829,-43.209,-22.878,-43.194,-22.939,-43.224,-22.991,-43.369,-22.998,-43.533,-23.046,-43.737,-23.067,-43.899,-23.101,-43.974,-23.057,-43.899,-23.035,-43.791,-23.046,-43.676,-23.009,-43.703,-22.966,-43.866,-22.911,-44.047,-22.945,-44.148,-23.011,-44.368,-23.005,-44.637,-23.055,-44.681,-23.107,-44.674,-23.207,-44.621,-23.229,-44.57,-23.274,-44.619,-23.316,-44.667,-23.335,-44.952,-23.381,-45.215,-23.576,-45.325,-23.6,-45.423,-23.685,-45.433,-23.758,-45.464,-23.803,-45.527,-23.805,-45.665,-23.765,-45.843,-23.764,-45.972,-23.796,-46.631,-24.11,-46.867,-24.236,-47.137,-24.493,-47.592,-24.781,-47.831,-24.953,-47.877,-24.997,-47.914,-25,-47.989,-25.036,-47.959,-25.065,-47.908,-25.068,-47.929,-25.168,-48.024,-25.237,-48.203,-25.417,-48.242,-25.403,-48.186,-25.31,-48.273,-25.306,-48.402,-25.272,-48.458,-25.311,-48.428,-25.403,-48.476,-25.443,-48.564,-25.447,-48.644,-25.437,-48.732,-25.369,-48.692,-25.492,-48.507,-25.521,-48.43,-25.55,-48.401,-25.597,-48.545,-25.816,-48.666,-25.844,-48.679,-25.875,-48.613,-25.875,-48.576,-25.935,-48.619,-26.179,-48.679,-26.226,-48.714,-26.227,-48.748,-26.269,-48.701,-26.348,-48.652,-26.406,-48.658,-26.519,-48.677,-26.612,-48.678,-26.703,-48.616,-26.878,-48.593,-27.058,-48.568,-27.123,-48.554,-27.196,-48.596,-27.264,-48.572,-27.373,-48.643,-27.558,-48.606,-27.825,-48.621,-28.076,-48.648,-28.207,-48.693,-28.31,-48.797,-28.443,-48.8,-28.575,-49.024,-28.699,-49.271,-28.871,-49.5,-29.075,-49.746,-29.363,-50.033,-29.801,-50.3,-30.426,-50.62,-30.898,-50.748,-31.068,-50.921,-31.258,-51.152,-31.48,-51.46,-31.702,-51.798,-31.9,-51.92,-31.99,-52.039,-32.115,-52.069,-32.063,-52.043,-31.978,-52.06,-31.913,-52.063,-31.83,-51.995,-31.815,-51.893,-31.868,-51.841,-31.832,-51.803,-31.797,-51.681,-31.775,-51.446,-31.557,-51.272,-31.477,-51.174,-31.34,-51.158,-31.267,-51.161,-31.119,-51.106,-31.081,-50.98,-31.094,-50.954,-31.052,-50.965,-31.005,-50.941,-30.904,-50.77,-30.813,-50.689,-30.704,-50.716,-30.426,-50.685,-30.413,-50.615,-30.457,-50.582,-30.439,-50.547,-30.317,-50.564,-30.254,-50.646,-30.237,-50.932,-30.374,-51.025,-30.369,-51.04,-30.261,-51.179,-30.211,-51.234,-30.121,-51.25,-30.06,-51.298,-30.035,-51.295,-30.141,-51.282,-30.244,-51.157,-30.364,-51.188,-30.412,-51.247,-30.468,-51.288,-30.591,-51.283,-30.752,-51.316,-30.703,-51.359,-30.675,-51.376,-30.847,-51.459,-30.913,-51.485,-30.978,-51.464,-31.053,-51.506,-31.104,-51.717,-31.244,-51.927,-31.339,-51.972,-31.384,-51.995,-31.49,-52.027,-31.599,-52.12,-31.695,-52.194,-31.886,-52.192,-31.968,-52.167,-32.088,-52.127,-32.168,-52.19,-32.221,-52.275,-32.324,-52.342,-32.44,-52.508,-32.875,-52.652,-33.138,-52.763,-33.266,-52.921,-33.402,-53.371,-33.742,-53.398,-33.737,-53.464,-33.71,-53.519,-33.677,-53.531,-33.655,-53.538,-33.623,-53.53,-33.5,-53.531,-33.171,-53.512,-33.109,-53.483,-33.069,-53.395,-33.01,-53.31,-32.927,-53.214,-32.821,-53.126,-32.737,-53.157,-32.68,-53.231,-32.625,-53.363,-32.581,-53.489,-32.503,-53.602,-32.403,-53.654,-32.299,-53.701,-32.186,-53.747,-32.097,-53.762,-32.057,-53.806,-32.04,-53.877,-31.995,-53.921,-31.952,-53.985,-31.928,-54.1,-31.902,-54.221,-31.855,-54.37,-31.745,-54.478,-31.623,-54.531,-31.542,-54.588,-31.485,-54.896,-31.391,-55.036,-31.279,-55.091,-31.314,-55.174,-31.28,-55.255,-31.226,-55.279,-31.184,-55.313,-31.142,-55.346,-31.093,-55.366,-31.046,-55.45,-30.964,-55.557,-30.876,-55.603,-30.851,-55.627,-30.858,-55.65,-30.892,-55.665,-30.925,-55.706,-30.947,-55.756,-30.987,-55.808,-31.037,-55.874,-31.07,-55.952,-31.081,-56.005,-31.079,-56.016,-31.06,-56.018,-30.992,-55.999,-30.837,-56.045,-30.778,-56.106,-30.714,-56.176,-30.628,-56.407,-30.447,-56.722,-30.187,-56.833,-30.107,-56.937,-30.101,-57.033,-30.11,-57.121,-30.144,-57.187,-30.265,-57.214,-30.283,-57.384,-30.281,-57.552,-30.261,-57.609,-30.188,-57.564,-30.14,-57.405,-30.034,-57.317,-29.939,-57.301,-29.857,-57.225,-29.782,-57.089,-29.716,-56.939,-29.595,-56.772,-29.418,-56.672,-29.287,-56.636,-29.203,-56.571,-29.138,-56.476,-29.092,-56.393,-28.997,-56.322,-28.852,-56.226,-28.737,-56.103,-28.652,-56.034,-28.581,-56.02,-28.525,-55.985,-28.489,-55.93,-28.473,-55.904,-28.443,-55.905,-28.4,-55.891,-28.37,-55.859,-28.354,-55.806,-28.36,-55.732,-28.387,-55.687,-28.382,-55.672,-28.345,-55.692,-28.303,-55.746,-28.255,-55.725,-28.204,-55.582,-28.121,-55.477,-28.089,-55.41,-28.038,-55.346,-27.956,-55.244,-27.899,-55.102,-27.867,-55.064,-27.836,-55.069,-27.796,-55.04,-27.768,-54.956,-27.747,-54.91,-27.709,-54.903,-27.652,-54.876,-27.599,-54.829,-27.551,-54.777,-27.533,-54.72,-27.545,-54.666,-27.527,-54.615,-27.477,-54.555,-27.454,-54.484,-27.457,-54.448,-27.446,-54.327,-27.424,-54.26,-27.382,-54.205,-27.29,-54.156,-27.254,-54.114,-27.275,-54.04,-27.244,-53.935,-27.161,-53.916,-27.16,-53.838,-27.121,-53.758,-26.978,-53.717,-26.883,-53.727,-26.805,-53.753,-26.749,-53.745,-26.667,-53.718,-26.443,-53.711,-26.352,-53.669,-26.288,-53.671,-26.225,-53.747,-26.084,-53.823,-25.96,-53.864,-25.749,-53.891,-25.669,-53.955,-25.648,-54.012,-25.578,-54.085,-25.572,-54.119,-25.545,-54.155,-25.523,-54.206,-25.53,-54.25,-25.57,-54.332,-25.572,-54.383,-25.589,-54.444,-25.625,-54.502,-25.608,-54.538,-25.576,-54.616,-25.576,-54.611,-25.433,-54.473,-25.22,-54.436,-25.121,-54.454,-25.065,-54.413,-24.867,-54.313,-24.528,-54.281,-24.306,-54.317,-24.201,-54.318,-24.128,-54.267,-24.066,-54.242,-24.047,-54.371,-23.971,-54.44,-23.902,-54.53,-23.852,-54.625,-23.812,-54.672,-23.829,-54.721,-23.852,-54.817,-23.888,-54.926,-23.951,-54.983,-23.975,-55.082,-23.998,-55.194,-24.017,-55.287,-24.004,-55.366,-23.991,-55.416,-23.951,-55.442,-23.865,-55.442,-23.793,-55.459,-23.687,-55.518,-23.627,-55.538,-23.581,-55.542,-23.525,-55.535,-23.462,-55.518,-23.416,-55.528,-23.359,-55.555,-23.32,-55.548,-23.25,-55.561,-23.154,-55.601,-23.095,-55.621,-23.025,-55.621,-22.956,-55.651,-22.886,-55.654,-22.81,-55.628,-22.741,-55.618,-22.671,-55.647,-22.622,-55.704,-22.592,-55.747,-22.513,-55.753,-22.41,-55.8,-22.354,-55.849,-22.308,-55.905,-22.308,-55.991,-22.281,-56.067,-22.284,-56.19,-22.281,-56.246,-22.265,-56.276,-22.228,-56.352,-22.179,-56.395,-22.093,-56.448,-22.076,-56.524,-22.103,-56.55,-22.136,-56.58,-22.182,-56.633,-22.235,-56.702,-22.232,-56.775,-22.261,-56.845,-22.265,-56.937,-22.271,-57.03,-22.245,-57.142,-22.215,-57.238,-22.195,-57.331,-22.215,-57.394,-22.198,-57.476,-22.189,-57.569,-22.182,-57.642,-22.129,-57.721,-22.099,-57.764,-22.109,-57.82,-22.142,-57.88,-22.136,-57.956,-22.109,-57.986,-22.046,-57.979,-22.007,-57.962,-21.967,-57.933,-21.911,-57.949,-21.851,-57.943,-21.798,-57.929,-21.752,-57.916,-21.699,-57.926,-21.65,-57.929,-21.597,-57.936,-21.547,-57.946,-21.494,-57.906,-21.418,-57.873,-21.355,-57.893,-21.302,-57.886,-21.266,-57.86,-21.206,-57.827,-21.134,-57.83,-20.998,-57.86,-20.919,-57.892,-20.897,-57.9,-20.873,-57.885,-20.842,-57.902,-20.809,-57.908,-20.776,-57.891,-20.747,-57.915,-20.69,-57.962,-20.674,-57.979,-20.657,-57.996,-20.594,-58.009,-20.522,-58.002,-20.465,-58.025,-20.416,-58.058,-20.386,-58.092,-20.333,-58.125,-20.293,-58.138,-20.237,-58.16,-20.165,-58.094,-20.151,-58.068,-20.11,-58.021,-20.055,-57.96,-20.041,-57.888,-20.02,-57.861,-19.98,-58.03,-19.833,-58.131,-19.745,-58.072,-19.625,-57.972,-19.424,-57.875,-19.229,-57.8,-19.081,-57.781,-19.054,-57.717,-19.044,-57.729,-18.967,-57.731,-18.917,-57.783,-18.914,-57.725,-18.733,-57.639,-18.475,-57.574,-18.279,-57.553,-18.246,-57.506,-18.237,-57.496,-18.215,-57.552,-18.183,-57.586,-18.122,-57.662,-17.947,-57.78,-17.672,-57.789,-17.573,-57.832,-17.512,-57.905,-17.532,-57.991,-17.513,-58.206,-17.363,-58.348,-17.282,-58.396,-17.234,-58.417,-17.081,-58.46,-16.911,-58.478,-16.701,-58.471,-16.65,-58.35,-16.491,-58.351,-16.41,-58.341,-16.34,-58.346,-16.284,-58.375,-16.284,-58.424,-16.308,-58.497,-16.327,-58.538,-16.328,-58.957,-16.313,-59.434,-16.296,-59.831,-16.282,-60.176,-16.269,-60.187,-16.132,-60.207,-15.902,-60.22,-15.739,-60.242,-15.48,-60.38,-15.318,-60.53,-15.143,-60.583,-15.098,-60.402,-15.093,-60.273,-15.089,-60.299,-14.619,-60.338,-14.571,-60.373,-14.419,-60.396,-14.333,-60.46,-14.263,-60.475,-14.185,-60.463,-14.132,-60.428,-14.1,-60.405,-14.019,-60.422,-13.938,-60.46,-13.862,-60.507,-13.79,-60.595,-13.745,-60.722,-13.664,-60.915,-13.561,-61.077,-13.49,-61.129,-13.499,-61.416,-13.527,-61.512,-13.541,-61.576,-13.525,-61.79,-13.526,-61.874,-13.47,-61.945,-13.406,-62.095,-13.242,-62.118,-13.16,-62.176,-13.134,-62.264,-13.144,-62.353,-13.132,-62.526,-13.064,-62.687,-12.994,-62.765,-12.997,-62.835,-12.954,-62.958,-12.847,-63.015,-12.806,-63.041,-12.75,-63.067,-12.669,-63.117,-12.652,-63.181,-12.666,-63.25,-12.708,-63.347,-12.68,-63.465,-12.605,-63.542,-12.547,-63.586,-12.519,-63.689,-12.478,-63.788,-12.469,-63.939,-12.53,-64.062,-12.505,-64.255,-12.483,-64.421,-12.44,-64.481,-12.326,-64.513,-12.251,-64.612,-12.204,-64.69,-12.146,-64.783,-12.059,-64.83,-12.03,-64.914,-12.006,-64.993,-11.975,-65.001,-11.92,-65.03,-11.847,-65.037,-11.829,-65.09,-11.741,-65.115,-11.735,-65.143,-11.752,-65.163,-11.765,-65.186,-11.75,-65.19,-11.71,-65.175,-11.647,-65.206,-11.581,-65.282,-11.511,-65.322,-11.439,-65.325,-11.365,-65.342,-11.315,-65.373,-11.29,-65.39,-11.246,-65.394,-11.184,-65.372,-11.11,-65.324,-11.025,-65.334,-10.893,-65.402,-10.715,-65.44,-10.586,-65.447,-10.507,-65.437,-10.449,-65.395,-10.392,-65.313,-10.253,-65.299,-10.147,-65.325,-10.027,-65.328,-9.936,-65.309,-9.873,-65.338,-9.79,-65.396,-9.712,-65.437,-9.71,-65.492,-9.732,-65.559,-9.797,-65.637,-9.809,-65.707,-9.768,-65.925,-9.785,-66.264,-9.826,-66.399,-9.868,-66.479,-9.886,-66.575,-9.9,-66.73,-9.975,-67.112,-10.269,-67.19,-10.311,-67.28,-10.317,-67.333,-10.358,-67.417,-10.39,-67.582,-10.506,-67.667,-10.599,-67.722,-10.683,-67.786,-10.686,-67.835,-10.663,-67.992,-10.674,-68.072,-10.703,-68.159,-10.785,-68.267,-10.933,-68.311,-10.975,-68.398,-11.019,-68.498,-11.055,-68.623,-11.109,-68.678,-11.113,-68.727,-11.122,-68.77,-11.098,-68.784,-11.045,-68.848,-11.011,-69.002,-10.994,-69.229,-10.956,-69.463,-10.948,-69.579,-10.952,-69.674,-10.954,-69.84,-10.933,-69.96,-10.93,-70.066,-10.982,-70.22,-11.048,-70.29,-11.064,-70.342,-11.067,-70.392,-11.059,-70.451,-11.025,-70.533,-10.947,-70.597,-10.977,-70.642,-11.01,-70.642,-10.841,-70.64,-10.586,-70.639,-10.361,-70.639,-10.182,-70.638,-9.972,-70.637,-9.824,-70.594,-9.767,-70.567,-9.705,-70.599,-9.621,-70.592,-9.543,-70.57,-9.49,-70.541,-9.437,-70.608,-9.464,-70.637,-9.478,-70.672,-9.518,-70.758,-9.572,-70.816,-9.625,-70.885,-9.669,-70.971,-9.766,-71.042,-9.819,-71.115,-9.852,-71.238,-9.966,-71.339,-9.989,-71.608,-10.006,-71.887,-10.006,-72.143,-10.005,-72.182,-10.004,-72.179,-9.91,-72.173,-9.844,-72.26,-9.774,-72.266,-9.688,-72.289,-9.629,-72.318,-9.557,-72.379,-9.51,-72.465,-9.492,-72.605,-9.452,-72.814,-9.41,-73.014,-9.407,-73.209,-9.411,-73.09,-9.266,-72.97,-9.12,-72.974,-8.993,-73.071,-8.883,-73.123,-8.814,-73.203,-8.719,-73.302,-8.654,-73.357,-8.567,-73.352,-8.514,-73.36,-8.479,-73.398,-8.459,-73.436,-8.427,-73.488,-8.392,-73.549,-8.346,-73.549,-8.299,-73.572,-8.25,-73.61,-8.192,-73.61,-8.145,-73.645,-8.073,-73.683,-8.021,-73.72,-7.986,-73.776,-7.936,-73.773,-7.896,-73.732,-7.875,-73.715,-7.829,-73.72,-7.783,-73.767,-7.754,-73.822,-7.739,-73.895,-7.655,-73.947,-7.611,-73.982,-7.585,-74.002,-7.556,-73.982,-7.536,-73.958,-7.507,-73.953,-7.46,-73.964,-7.417,-73.964,-7.379,-73.929,-7.367,-73.892,-7.373,-73.854,-7.35,-73.805,-7.341,-73.749,-7.335,-73.72,-7.309,-73.723,-7.263,-73.758,-7.173,-73.793,-7.135,-73.805,-7.08,-73.776,-6.974,-73.758,-6.906,-73.695,-6.834,-73.5,-6.679,-73.325,-6.575,-73.24,-6.564,-73.177,-6.525,-73.137,-6.466,-73.126,-6.401,-73.135,-6.344,-73.168,-6.261,-73.206,-6.156,-73.236,-6.098,-73.209,-6.029,-73.163,-5.933,-73.068,-5.79,-72.98,-5.635,-72.97,-5.59,-72.959,-5.495,-72.918,-5.303,-72.896,-5.198,-72.907,-5.158,-72.887,-5.123,-72.832,-5.094,-72.699,-5.067,-72.608,-5.01,-72.469,-4.901,-72.353,-4.786,-72.257,-4.749,-72.083,-4.642,-71.982,-4.575,-71.943,-4.553,-71.845,-4.504,-71.668,-4.487,-71.521,-4.47,-71.438,-4.438,-71.317,-4.424,-71.235,-4.388,-71.144,-4.387,-70.974,-4.35,-70.916,-4.295,-70.866,-4.23,-70.8,-4.173,-70.722,-4.159,-70.635,-4.169,-70.531,-4.168,-70.405,-4.15,-70.344,-4.194,-70.317,-4.247,-70.239,-4.301,-70.184,-4.298,-70.129,-4.287,-70.053,-4.333,-70.004,-4.327,-69.972,-4.301,-69.966,-4.236,-69.948,-4.201,-69.911,-3.997,-69.85,-3.66,-69.794,-3.355,-69.733,-3.017,-69.669,-2.668,-69.605,-2.314,-69.552,-2.024,-69.506,-1.775,-69.479,-1.622,-69.435,-1.422,-69.418,-1.246,-69.4,-1.195,-69.411,-1.152,-69.449,-1.092,-69.449,-1.065,-69.444,-1.03,-69.449,-0.999,-69.488,-0.966,-69.519,-0.946,-69.544,-0.917,-69.555,-0.877,-69.574,-0.838,-69.583,-0.796,-69.612,-0.763,-69.621,-0.721,-69.601,-0.681,-69.592,-0.639,-69.601,-0.6,-69.612,-0.553,-69.634,-0.509,-69.667,-0.482,-69.747,-0.453,-69.828,-0.381,-69.923,-0.317,-70.044,-0.196,-70.071,-0.139,-70.071,0.019,-70.066,0.189,-70.058,0.447,-70.054,0.579,-69.985,0.586,-69.925,0.589,-69.862,0.598,-69.807,0.607,-69.757,0.626,-69.719,0.65,-69.674,0.665,-69.639,0.66,-69.604,0.68,-69.565,0.7,-69.527,0.716,-69.472,0.73,-69.421,0.698,-69.392,0.667,-69.359,0.652,-69.327,0.655,-69.306,0.652,-69.283,0.627,-69.254,0.625,-69.213,0.63,-69.174,0.635,-69.156,0.643,-69.153,0.659,-69.163,0.687,-69.177,0.713,-69.166,0.753,-69.165,0.802,-69.163,0.864,-69.194,0.898,-69.224,0.963,-69.259,1.015,-69.312,1.05,-69.361,1.064,-69.403,1.042,-69.442,1.039,-69.47,1.059,-69.517,1.059,-69.568,1.066,-69.621,1.073,-69.717,1.059,-69.751,1.077,-69.798,1.078,-69.852,1.06,-69.851,1.309,-69.849,1.544,-69.849,1.709,-69.8,1.705,-69.74,1.735,-69.65,1.739,-69.581,1.771,-69.543,1.773,-69.47,1.758,-69.394,1.726,-69.32,1.721,-69.124,1.721,-68.913,1.721,-68.678,1.721,-68.443,1.722,-68.24,1.722,-68.177,1.72,-68.213,1.775,-68.256,1.846,-68.239,1.901,-68.218,1.958,-68.194,1.987,-68.13,1.956,-68.077,1.86,-68.033,1.788,-67.99,1.753,-67.936,1.748,-67.876,1.761,-67.815,1.79,-67.712,1.922,-67.609,2.035,-67.556,2.073,-67.5,2.108,-67.458,2.121,-67.4,2.117,-67.352,2.086,-67.321,2.032,-67.206,1.845,-67.119,1.704,-67.09,1.616,-67.088,1.401,-67.094,1.21,-67.082,1.185,-67.065,1.178,-66.876,1.223]],[[-49.629,-0.229,-49.535,-0.234,-49.403,-0.215,-49.314,-0.168,-49.215,-0.159,-49.117,-0.164,-48.787,-0.216,-48.588,-0.232,-48.515,-0.248,-48.444,-0.272,-48.393,-0.297,-48.38,-0.353,-48.428,-0.442,-48.464,-0.535,-48.497,-0.665,-48.523,-0.691,-48.567,-0.684,-48.54,-0.801,-48.55,-0.848,-48.571,-0.893,-48.624,-0.987,-48.705,-1.107,-48.729,-1.132,-48.79,-1.173,-48.84,-1.227,-48.829,-1.277,-48.804,-1.327,-48.834,-1.39,-48.929,-1.482,-48.986,-1.505,-49.038,-1.514,-49.087,-1.505,-49.173,-1.413,-49.182,-1.485,-49.205,-1.559,-49.234,-1.6,-49.345,-1.595,-49.407,-1.556,-49.507,-1.512,-49.526,-1.63,-49.588,-1.712,-49.651,-1.738,-49.749,-1.755,-49.805,-1.79,-49.911,-1.763,-50.01,-1.708,-50.066,-1.704,-50.109,-1.748,-50.338,-1.756,-50.443,-1.801,-50.508,-1.788,-50.602,-1.698,-50.617,-1.638,-50.673,-1.516,-50.724,-1.371,-50.76,-1.24,-50.729,-1.127,-50.668,-1.131,-50.596,-1.147,-50.581,-1.139,-50.577,-1.103,-50.593,-1.073,-50.71,-1.078,-50.783,-1.01,-50.796,-0.906,-50.781,-0.69,-50.771,-0.645,-50.72,-0.583,-50.703,-0.529,-50.716,-0.47,-50.694,-0.364,-50.646,-0.273,-50.462,-0.157,-50.248,-0.116,-49.629,-0.229]],[[-44.129,-23.142,-44.098,-23.169,-44.156,-23.167,-44.221,-23.191,-44.32,-23.212,-44.36,-23.172,-44.274,-23.116,-44.243,-23.074,-44.22,-23.083,-44.192,-23.113,-44.129,-23.142]],[[-48.584,-26.402,-48.603,-26.414,-48.666,-26.29,-48.54,-26.17,-48.498,-26.219,-48.531,-26.313,-48.568,-26.38,-48.584,-26.402]],[[-45.26,-23.889,-45.261,-23.941,-45.303,-23.915,-45.413,-23.935,-45.451,-23.896,-45.302,-23.728,-45.272,-23.752,-45.249,-23.783,-45.233,-23.825,-45.25,-23.853,-45.26,-23.889]],[[-44.499,-2.94,-44.598,-3.038,-44.565,-2.924,-44.582,-2.846,-44.569,-2.785,-44.502,-2.726,-44.481,-2.718,-44.487,-2.79,-44.483,-2.812,-44.499,-2.94]],[[-38.744,-13.097,-38.783,-13.119,-38.787,-13.055,-38.685,-12.975,-38.668,-12.88,-38.615,-12.924,-38.6,-12.972,-38.601,-12.993,-38.744,-13.097]],[[-38.904,-13.473,-38.938,-13.532,-38.978,-13.524,-38.993,-13.484,-39.022,-13.446,-39.007,-13.416,-38.98,-13.398,-38.907,-13.401,-38.904,-13.473]],[[-44.883,-1.318,-44.947,-1.366,-44.968,-1.391,-45.021,-1.372,-45.011,-1.345,-44.996,-1.348,-44.979,-1.267,-44.888,-1.277,-44.883,-1.318]],[[-49.738,0.268,-49.697,0.216,-49.839,0.007,-49.917,-0.023,-50.002,-0.029,-50.113,0.033,-50.286,0.029,-50.339,0.043,-50.345,0.134,-50.273,0.232,-50.128,0.227,-49.879,0.305,-49.738,0.268]],[[-50.299,1.939,-50.399,1.893,-50.456,1.91,-50.509,2.03,-50.491,2.129,-50.419,2.161,-50.363,2.154,-50.342,2.142,-50.292,1.98,-50.299,1.939]],[[-50.653,-0.132,-50.926,-0.327,-51.019,-0.263,-51.038,-0.226,-51.022,-0.188,-51.026,-0.172,-50.995,-0.105,-50.842,-0.05,-50.765,-0.041,-50.667,-0.058,-50.651,-0.106,-50.653,-0.132]],[[-49.444,-0.112,-49.709,-0.144,-49.83,-0.094,-49.803,-0.052,-49.712,0.015,-49.602,0.063,-49.503,0.084,-49.4,0.057,-49.372,0.001,-49.381,-0.055,-49.444,-0.112]],[[-50.426,0.139,-50.444,-0.008,-50.624,0.054,-50.61,0.205,-50.526,0.247,-50.452,0.327,-50.426,0.425,-50.425,0.558,-50.397,0.581,-50.373,0.591,-50.351,0.582,-50.343,0.382,-50.332,0.259,-50.426,0.139]],[[-50.153,0.393,-50.261,0.359,-50.282,0.391,-50.282,0.517,-50.251,0.585,-50.113,0.605,-50.099,0.625,-50.059,0.638,-50.037,0.595,-50.04,0.523,-50.153,0.393]],[[-51.833,-1.434,-51.938,-1.453,-51.802,-1.203,-51.68,-1.086,-51.678,-0.855,-51.546,-0.65,-51.424,-0.566,-51.254,-0.541,-51.161,-0.667,-51.276,-1.022,-51.31,-1.024,-51.465,-1.211,-51.638,-1.342,-51.833,-1.434]],[[-48.486,-27.767,-48.555,-27.812,-48.542,-27.575,-48.505,-27.496,-48.465,-27.436,-48.415,-27.4,-48.378,-27.451,-48.41,-27.566,-48.497,-27.707,-48.486,-27.767]]]},{"n":"Botswana","c":"BW","r":4,"ml":4,"lx":24.18,"ly":-22.1,"b":[19.977,-26.854,29.365,-17.788],"p":[[[25.259,-17.794,25.239,-17.843,25.224,-17.915,25.242,-17.969,25.282,-18.041,25.34,-18.104,25.384,-18.142,25.437,-18.235,25.489,-18.351,25.558,-18.442,25.761,-18.649,25.784,-18.724,25.812,-18.797,25.939,-18.939,25.959,-18.986,25.951,-19.082,26.082,-19.37,26.168,-19.538,26.241,-19.569,26.475,-19.749,26.678,-19.893,26.917,-19.99,27.092,-20.054,27.178,-20.101,27.221,-20.146,27.257,-20.232,27.275,-20.382,27.281,-20.479,27.469,-20.475,27.625,-20.484,27.679,-20.503,27.7,-20.531,27.695,-20.595,27.697,-20.69,27.704,-20.766,27.688,-20.848,27.677,-20.945,27.669,-21.064,27.693,-21.111,27.844,-21.262,27.907,-21.359,27.975,-21.507,28.014,-21.554,28.046,-21.573,28.182,-21.589,28.532,-21.651,28.748,-21.708,28.919,-21.766,28.991,-21.781,29.026,-21.797,29.037,-21.811,29.016,-21.94,29.023,-21.981,29.042,-22.018,29.071,-22.047,29.107,-22.066,29.237,-22.079,29.315,-22.158,29.365,-22.194,29.13,-22.213,29.013,-22.278,28.946,-22.395,28.84,-22.481,28.696,-22.535,28.543,-22.573,28.382,-22.593,28.21,-22.694,28.028,-22.874,27.935,-22.987,27.931,-23.034,27.891,-23.074,27.813,-23.108,27.769,-23.149,27.758,-23.197,27.717,-23.22,27.644,-23.218,27.593,-23.253,27.563,-23.325,27.499,-23.368,27.399,-23.384,27.313,-23.424,27.241,-23.49,27.186,-23.523,27.146,-23.524,27.086,-23.578,26.987,-23.705,26.971,-23.763,26.835,-24.241,26.761,-24.297,26.618,-24.396,26.502,-24.513,26.452,-24.583,26.397,-24.614,26.131,-24.671,26.032,-24.702,25.912,-24.747,25.882,-24.788,25.852,-24.935,25.77,-25.146,25.703,-25.302,25.659,-25.438,25.584,-25.606,25.518,-25.663,25.444,-25.714,25.346,-25.74,25.213,-25.756,25.092,-25.751,24.999,-25.754,24.869,-25.813,24.748,-25.817,24.556,-25.783,24.4,-25.75,24.331,-25.743,24.193,-25.633,24.104,-25.635,23.97,-25.626,23.894,-25.601,23.823,-25.545,23.671,-25.434,23.521,-25.344,23.389,-25.291,23.266,-25.267,23.149,-25.289,23.058,-25.312,23.022,-25.324,22.951,-25.37,22.879,-25.458,22.819,-25.595,22.796,-25.679,22.729,-25.857,22.64,-26.071,22.598,-26.133,22.549,-26.178,22.471,-26.219,22.218,-26.389,22.091,-26.58,22.011,-26.636,21.915,-26.662,21.833,-26.678,21.788,-26.71,21.738,-26.807,21.695,-26.841,21.646,-26.854,21.501,-26.843,21.455,-26.833,21.071,-26.852,20.954,-26.821,20.871,-26.809,20.74,-26.849,20.685,-26.822,20.641,-26.742,20.62,-26.581,20.627,-26.444,20.698,-26.34,20.757,-26.264,20.815,-26.165,20.823,-26.121,20.811,-26.081,20.799,-25.999,20.793,-25.916,20.711,-25.733,20.609,-25.491,20.473,-25.221,20.431,-25.147,20.345,-25.03,20.029,-24.807,19.98,-24.777,19.98,-24.752,19.98,-24.536,19.98,-24.249,19.98,-23.962,19.979,-23.676,19.979,-23.389,19.979,-23.103,19.978,-22.816,19.978,-22.529,19.978,-22.243,19.977,-22,20.205,-22,20.488,-22,20.823,-22,20.971,-22,20.979,-21.962,20.979,-21.784,20.979,-21.376,20.978,-20.968,20.977,-20.56,20.977,-20.152,20.976,-19.744,20.976,-19.336,20.975,-18.929,20.974,-18.521,20.974,-18.319,21.233,-18.307,21.53,-18.266,22.011,-18.199,22.46,-18.116,22.753,-18.067,23.1,-18.01,23.219,-18,23.252,-18.008,23.299,-18.027,23.46,-18.231,23.56,-18.386,23.581,-18.453,23.6,-18.46,23.647,-18.449,23.7,-18.424,23.864,-18.27,23.898,-18.229,24.003,-18.154,24.129,-18.078,24.244,-18.023,24.359,-17.978,24.412,-17.989,24.475,-18.029,24.531,-18.053,24.792,-17.865,24.909,-17.821,25.216,-17.788,25.259,-17.794]]]},{"n":"Bosnia and Herzegovina","c":"BA","r":5,"ml":4.5,"lx":18.07,"ly":44.09,"b":[15.737,42.56,19.584,45.277],"p":[[[19.349,44.881,19.357,44.859,19.334,44.781,19.292,44.697,19.223,44.61,19.151,44.527,19.132,44.484,19.127,44.415,19.118,44.36,19.128,44.33,19.152,44.303,19.232,44.281,19.339,44.226,19.43,44.154,19.547,44.073,19.584,44.043,19.584,44.011,19.55,43.987,19.449,43.978,19.345,43.985,19.305,43.993,19.268,43.983,19.245,43.965,19.257,43.943,19.364,43.845,19.488,43.704,19.495,43.643,19.48,43.595,19.451,43.562,19.4,43.568,19.36,43.593,19.301,43.592,19.254,43.584,19.194,43.533,19.164,43.535,19.113,43.528,19.08,43.518,19.028,43.533,18.974,43.542,18.951,43.527,18.94,43.497,18.974,43.442,19.037,43.357,19.027,43.292,18.979,43.285,18.935,43.339,18.896,43.348,18.851,43.346,18.749,43.284,18.674,43.231,18.657,43.194,18.63,43.154,18.622,43.125,18.624,43.028,18.488,43.012,18.46,42.998,18.444,42.968,18.455,42.844,18.466,42.777,18.543,42.674,18.546,42.642,18.535,42.62,18.48,42.579,18.454,42.565,18.436,42.56,18.347,42.587,18.304,42.599,18.124,42.691,18.045,42.741,17.919,42.807,17.841,42.845,17.802,42.902,17.74,42.915,17.668,42.897,17.585,42.938,17.643,42.96,17.658,42.98,17.65,43.007,17.625,43.043,17.402,43.199,17.293,43.306,17.275,43.344,17.274,43.446,17.248,43.47,17.085,43.517,16.902,43.649,16.713,43.779,16.688,43.815,16.591,43.913,16.472,44.003,16.378,44.06,16.3,44.125,16.214,44.215,16.17,44.352,16.13,44.474,16.103,44.521,16.049,44.538,15.88,44.682,15.737,44.766,15.738,44.856,15.762,45.008,15.788,45.179,15.823,45.203,15.888,45.216,15.963,45.211,16.028,45.19,16.157,45.072,16.231,45.027,16.293,45.009,16.365,45.058,16.454,45.162,16.531,45.217,16.791,45.197,16.919,45.277,17.125,45.172,17.211,45.156,17.259,45.171,17.324,45.164,17.469,45.133,17.503,45.12,17.546,45.123,17.654,45.163,17.69,45.158,17.813,45.078,17.874,45.077,17.949,45.112,17.996,45.142,18.137,45.119,18.218,45.133,18.285,45.134,18.358,45.121,18.424,45.102,18.488,45.086,18.663,45.077,18.746,45.027,18.779,44.977,18.78,44.947,18.788,44.915,18.836,44.883,18.941,44.865,19.007,44.869,19.042,44.871,19.132,44.9,19.237,44.914,19.313,44.897,19.349,44.881]]]},{"n":"Bolivia","c":"BO","r":3,"ml":3,"lx":-64.59,"ly":-16.67,"b":[-69.646,-22.892,-57.496,-9.71],"p":[[[-69.511,-17.506,-69.511,-17.505,-69.511,-17.46,-69.522,-17.389,-69.564,-17.333,-69.626,-17.294,-69.646,-17.249,-69.625,-17.2,-69.503,-17.105,-69.438,-17.088,-69.421,-17.04,-69.382,-17.001,-69.267,-16.861,-69.2,-16.768,-69.133,-16.713,-69.055,-16.674,-69.021,-16.642,-69.038,-16.543,-69.033,-16.476,-69.006,-16.434,-68.928,-16.389,-68.858,-16.355,-68.843,-16.338,-68.849,-16.313,-68.913,-16.262,-69.046,-16.218,-69.134,-16.222,-69.188,-16.183,-69.218,-16.149,-69.392,-15.737,-69.421,-15.641,-69.419,-15.603,-69.302,-15.399,-69.254,-15.333,-69.172,-15.237,-69.187,-15.199,-69.331,-15.039,-69.375,-14.963,-69.374,-14.887,-69.359,-14.795,-69.276,-14.746,-69.252,-14.671,-69.235,-14.597,-69.199,-14.573,-69.163,-14.531,-69.12,-14.47,-69.053,-14.418,-69.013,-14.377,-69.004,-14.265,-68.972,-14.234,-68.88,-14.199,-68.871,-14.17,-68.892,-14.094,-68.937,-14.015,-68.974,-13.976,-69.023,-13.78,-69.074,-13.683,-69.053,-13.644,-69.018,-13.594,-68.983,-13.496,-68.972,-13.382,-68.981,-12.963,-68.979,-12.88,-68.934,-12.822,-68.868,-12.755,-68.812,-12.73,-68.759,-12.687,-68.763,-12.608,-68.728,-12.561,-68.685,-12.502,-68.819,-12.27,-68.936,-12.067,-69.046,-11.876,-69.174,-11.654,-69.258,-11.509,-69.362,-11.328,-69.454,-11.169,-69.579,-10.952,-69.463,-10.948,-69.229,-10.956,-69.002,-10.994,-68.848,-11.011,-68.784,-11.045,-68.77,-11.098,-68.727,-11.122,-68.678,-11.113,-68.623,-11.109,-68.498,-11.055,-68.398,-11.019,-68.311,-10.975,-68.267,-10.933,-68.159,-10.785,-68.072,-10.703,-67.992,-10.674,-67.835,-10.663,-67.786,-10.686,-67.722,-10.683,-67.667,-10.599,-67.582,-10.506,-67.417,-10.39,-67.333,-10.358,-67.28,-10.317,-67.19,-10.311,-67.112,-10.269,-66.73,-9.975,-66.575,-9.9,-66.479,-9.886,-66.399,-9.868,-66.264,-9.826,-65.925,-9.785,-65.707,-9.768,-65.637,-9.809,-65.559,-9.797,-65.492,-9.732,-65.437,-9.71,-65.396,-9.712,-65.338,-9.79,-65.309,-9.873,-65.328,-9.936,-65.325,-10.027,-65.299,-10.147,-65.313,-10.253,-65.395,-10.392,-65.437,-10.449,-65.447,-10.507,-65.44,-10.586,-65.402,-10.715,-65.334,-10.893,-65.324,-11.025,-65.372,-11.11,-65.394,-11.184,-65.39,-11.246,-65.373,-11.29,-65.342,-11.315,-65.325,-11.365,-65.322,-11.439,-65.282,-11.511,-65.206,-11.581,-65.175,-11.647,-65.19,-11.71,-65.186,-11.75,-65.163,-11.765,-65.143,-11.752,-65.115,-11.735,-65.09,-11.741,-65.037,-11.829,-65.03,-11.847,-65.001,-11.92,-64.993,-11.975,-64.914,-12.006,-64.83,-12.03,-64.783,-12.059,-64.69,-12.146,-64.612,-12.204,-64.513,-12.251,-64.481,-12.326,-64.421,-12.44,-64.255,-12.483,-64.062,-12.505,-63.939,-12.53,-63.788,-12.469,-63.689,-12.478,-63.586,-12.519,-63.542,-12.547,-63.465,-12.605,-63.347,-12.68,-63.25,-12.708,-63.181,-12.666,-63.117,-12.652,-63.067,-12.669,-63.041,-12.75,-63.015,-12.806,-62.958,-12.847,-62.835,-12.954,-62.765,-12.997,-62.687,-12.994,-62.526,-13.064,-62.353,-13.132,-62.264,-13.144,-62.176,-13.134,-62.118,-13.16,-62.095,-13.242,-61.945,-13.406,-61.874,-13.47,-61.79,-13.526,-61.576,-13.525,-61.512,-13.541,-61.416,-13.527,-61.129,-13.499,-61.077,-13.49,-60.915,-13.561,-60.722,-13.664,-60.595,-13.745,-60.507,-13.79,-60.46,-13.862,-60.422,-13.938,-60.405,-14.019,-60.428,-14.1,-60.463,-14.132,-60.475,-14.185,-60.46,-14.263,-60.396,-14.333,-60.373,-14.419,-60.338,-14.571,-60.299,-14.619,-60.273,-15.089,-60.402,-15.093,-60.583,-15.098,-60.53,-15.143,-60.38,-15.318,-60.242,-15.48,-60.22,-15.739,-60.207,-15.902,-60.187,-16.132,-60.176,-16.269,-59.831,-16.282,-59.434,-16.296,-58.957,-16.313,-58.538,-16.328,-58.497,-16.327,-58.424,-16.308,-58.375,-16.284,-58.346,-16.284,-58.341,-16.34,-58.351,-16.41,-58.35,-16.491,-58.471,-16.65,-58.478,-16.701,-58.46,-16.911,-58.417,-17.081,-58.396,-17.234,-58.348,-17.282,-58.206,-17.363,-57.991,-17.513,-57.905,-17.532,-57.832,-17.512,-57.789,-17.573,-57.78,-17.672,-57.662,-17.947,-57.586,-18.122,-57.552,-18.183,-57.496,-18.215,-57.506,-18.237,-57.553,-18.246,-57.574,-18.279,-57.639,-18.475,-57.725,-18.733,-57.783,-18.914,-57.731,-18.917,-57.729,-18.967,-57.717,-19.044,-57.781,-19.054,-57.8,-19.081,-57.875,-19.229,-57.972,-19.424,-58.072,-19.625,-58.131,-19.745,-58.03,-19.833,-57.861,-19.98,-57.888,-20.02,-57.96,-20.041,-58.021,-20.055,-58.068,-20.11,-58.094,-20.151,-58.16,-20.165,-58.14,-19.999,-58.16,-19.855,-58.18,-19.818,-58.474,-19.646,-58.741,-19.49,-59.091,-19.286,-59.541,-19.292,-60.007,-19.298,-60.452,-19.389,-60.889,-19.479,-61.096,-19.521,-61.512,-19.606,-61.757,-19.645,-61.821,-19.809,-61.917,-20.055,-62.012,-20.199,-62.122,-20.35,-62.276,-20.562,-62.277,-20.821,-62.277,-21.066,-62.385,-21.412,-62.478,-21.705,-62.567,-21.989,-62.629,-22.184,-62.651,-22.234,-62.665,-22.218,-62.745,-22.16,-62.815,-22.05,-62.834,-21.999,-62.843,-21.997,-63.267,-22.001,-63.675,-22.004,-63.717,-22.028,-63.776,-22.027,-63.819,-22.005,-63.861,-22.007,-63.922,-22.029,-63.976,-22.073,-64.132,-22.366,-64.209,-22.491,-64.266,-22.603,-64.308,-22.795,-64.325,-22.828,-64.374,-22.761,-64.446,-22.585,-64.478,-22.485,-64.524,-22.372,-64.606,-22.229,-64.7,-22.186,-64.759,-22.171,-64.843,-22.144,-64.993,-22.11,-65.058,-22.103,-65.485,-22.098,-65.519,-22.095,-65.686,-22.11,-65.771,-22.1,-65.86,-22.02,-66.059,-21.879,-66.099,-21.835,-66.175,-21.806,-66.22,-21.803,-66.248,-21.83,-66.282,-21.947,-66.322,-22.053,-66.365,-22.114,-66.507,-22.158,-66.639,-22.205,-66.712,-22.216,-66.751,-22.269,-66.767,-22.343,-66.8,-22.41,-66.991,-22.51,-67.034,-22.552,-67.055,-22.651,-67.162,-22.774,-67.195,-22.822,-67.362,-22.855,-67.58,-22.892,-67.707,-22.889,-67.794,-22.879,-67.821,-22.858,-67.879,-22.823,-67.889,-22.784,-67.89,-22.729,-67.874,-22.631,-67.882,-22.493,-67.95,-22.334,-67.945,-22.282,-67.954,-22.204,-67.988,-22.057,-68.077,-21.983,-68.102,-21.861,-68.112,-21.753,-68.186,-21.619,-68.199,-21.447,-68.197,-21.3,-68.314,-21.13,-68.435,-20.948,-68.534,-20.924,-68.558,-20.902,-68.569,-20.85,-68.571,-20.769,-68.563,-20.72,-68.487,-20.641,-68.484,-20.628,-68.5,-20.612,-68.696,-20.493,-68.745,-20.459,-68.761,-20.416,-68.759,-20.378,-68.712,-20.339,-68.689,-20.31,-68.735,-20.225,-68.73,-20.148,-68.759,-20.116,-68.756,-20.091,-68.727,-20.07,-68.6,-20.045,-68.561,-19.967,-68.559,-19.902,-68.578,-19.857,-68.696,-19.741,-68.698,-19.721,-68.575,-19.56,-68.487,-19.454,-68.463,-19.433,-68.47,-19.41,-68.492,-19.382,-68.548,-19.341,-68.621,-19.297,-68.681,-19.242,-68.759,-19.162,-68.858,-19.093,-68.931,-19.025,-68.968,-18.968,-68.969,-18.91,-68.979,-18.813,-69.027,-18.656,-69.039,-18.55,-69.06,-18.433,-69.081,-18.357,-69.092,-18.282,-69.126,-18.202,-69.145,-18.144,-69.118,-18.103,-69.09,-18.071,-69.094,-18.05,-69.282,-17.965,-69.313,-17.943,-69.358,-17.772,-69.495,-17.62,-69.511,-17.506]]]},{"n":"Bhutan","c":"BT","r":5,"ml":4,"lx":90.04,"ly":27.54,"b":[88.739,26.702,92.083,28.311],"p":[[[91.632,27.76,91.626,27.737,91.598,27.677,91.579,27.611,91.595,27.558,91.658,27.494,91.743,27.443,91.851,27.439,91.951,27.458,91.991,27.45,92.045,27.365,92.083,27.291,92.031,27.214,92.003,27.147,91.992,27.1,91.999,27.079,92.031,27.041,92.068,26.975,92.073,26.915,92.05,26.875,91.998,26.855,91.944,26.861,91.899,26.86,91.842,26.853,91.754,26.831,91.672,26.802,91.518,26.807,91.456,26.867,91.427,26.867,91.287,26.79,91.134,26.803,90.856,26.778,90.74,26.772,90.62,26.78,90.56,26.797,90.448,26.851,90.346,26.89,90.242,26.854,90.206,26.848,90.123,26.755,89.943,26.724,89.764,26.702,89.711,26.714,89.61,26.719,89.606,26.741,89.609,26.762,89.586,26.779,89.545,26.796,89.475,26.803,89.384,26.827,89.332,26.849,89.148,26.816,89.041,26.865,88.919,26.932,88.858,26.961,88.835,27.066,88.814,27.099,88.766,27.134,88.739,27.176,88.76,27.218,88.882,27.297,88.891,27.316,88.948,27.464,89.025,27.518,89.102,27.593,89.16,27.711,89.273,27.833,89.396,27.958,89.481,28.06,89.537,28.107,89.653,28.158,89.75,28.188,89.817,28.256,89.898,28.294,89.981,28.311,90.104,28.302,90.221,28.278,90.348,28.244,90.363,28.217,90.352,28.168,90.334,28.119,90.333,28.094,90.353,28.08,90.477,28.071,90.63,28.079,90.716,28.072,90.907,28.027,90.963,27.995,91.021,27.97,91.078,27.974,91.15,28.027,91.226,28.071,91.273,28.078,91.307,28.064,91.368,28.022,91.493,27.982,91.606,27.952,91.642,27.923,91.629,27.801,91.632,27.76]]]},{"n":"Benin","c":"BJ","r":5,"ml":4,"lx":2.35,"ly":10.32,"b":[0.763,6.217,3.834,12.384],"p":[[[1.623,6.217,1.611,6.251,1.778,6.295,1.743,6.426,1.639,6.582,1.599,6.61,1.578,6.687,1.603,6.738,1.591,6.772,1.582,6.877,1.531,6.992,1.625,6.997,1.625,7.369,1.625,7.726,1.625,8.03,1.625,8.271,1.607,8.559,1.604,8.771,1.6,9.05,1.566,9.137,1.424,9.285,1.386,9.362,1.379,9.463,1.347,9.568,1.345,9.75,1.343,9.963,1.33,9.997,1.176,10.098,0.958,10.242,0.792,10.352,0.78,10.36,0.763,10.387,0.788,10.71,0.822,10.753,0.875,10.886,0.9,10.993,0.925,10.993,0.958,11.028,0.985,11.079,1.014,11.068,1.062,11.058,1.085,11.076,1.082,11.116,1.098,11.156,1.136,11.174,1.146,11.21,1.146,11.252,1.179,11.263,1.235,11.261,1.28,11.274,1.317,11.295,1.365,11.379,1.392,11.408,1.4,11.429,1.427,11.447,1.501,11.456,1.561,11.449,1.6,11.401,1.858,11.443,1.98,11.418,2.231,11.629,2.287,11.691,2.363,11.84,2.389,11.897,2.413,11.999,2.363,12.188,2.366,12.222,2.469,12.263,2.598,12.294,2.648,12.297,2.681,12.313,2.729,12.354,2.805,12.384,2.85,12.374,2.878,12.368,3.15,12.118,3.267,11.992,3.299,11.927,3.36,11.88,3.45,11.852,3.532,11.787,3.595,11.696,3.554,11.632,3.491,11.499,3.488,11.395,3.639,11.177,3.656,11.155,3.695,11.12,3.716,11.08,3.734,10.972,3.745,10.85,3.757,10.769,3.83,10.654,3.834,10.607,3.784,10.436,3.772,10.418,3.758,10.413,3.68,10.428,3.647,10.409,3.604,10.351,3.578,10.292,3.577,10.268,3.646,10.16,3.602,10.005,3.557,9.907,3.477,9.852,3.405,9.839,3.354,9.813,3.325,9.778,3.329,9.667,3.223,9.566,3.165,9.495,3.136,9.452,3.148,9.321,3.11,9.188,3.045,9.084,2.898,9.061,2.775,9.049,2.733,8.783,2.735,8.614,2.724,8.442,2.703,8.372,2.712,8.273,2.702,8.05,2.686,7.874,2.708,7.827,2.72,7.723,2.719,7.616,2.751,7.542,2.785,7.477,2.784,7.443,2.766,7.423,2.75,7.395,2.751,7.143,2.757,7.068,2.748,7.02,2.721,6.98,2.732,6.853,2.753,6.772,2.775,6.712,2.754,6.662,2.736,6.596,2.708,6.428,2.706,6.369,2.287,6.328,1.818,6.261,1.623,6.217]]]},{"n":"Belize","c":"BZ","r":6,"ml":5,"lx":-88.71,"ly":17.2,"b":[-89.237,15.889,-87.789,18.482],"p":[[[-89.161,17.815,-89.162,17.902,-89.134,17.971,-89.05,18,-88.943,17.94,-88.898,17.915,-88.857,17.929,-88.806,17.966,-88.744,18.072,-88.586,18.291,-88.523,18.446,-88.461,18.477,-88.372,18.482,-88.296,18.472,-88.349,18.359,-88.296,18.344,-88.247,18.355,-88.13,18.351,-88.085,18.226,-88.097,18.122,-88.207,17.846,-88.221,17.751,-88.272,17.61,-88.203,17.517,-88.267,17.393,-88.289,17.313,-88.294,17.192,-88.262,16.963,-88.313,16.633,-88.405,16.489,-88.461,16.434,-88.562,16.29,-88.695,16.248,-88.879,16.017,-88.912,15.956,-88.894,15.891,-88.937,15.89,-89.114,15.901,-89.233,15.889,-89.237,15.894,-89.228,16.143,-89.212,16.527,-89.201,16.809,-89.19,17.085,-89.182,17.291,-89.171,17.572,-89.161,17.815]],[[-87.951,17.925,-87.998,17.906,-87.959,17.964,-87.953,18.001,-87.898,18.155,-87.859,18.154,-87.849,18.14,-87.951,17.925]],[[-87.853,17.423,-87.93,17.283,-87.935,17.323,-87.903,17.426,-87.859,17.463,-87.833,17.501,-87.826,17.546,-87.789,17.524,-87.798,17.48,-87.853,17.423]]]},{"n":"Belgium","c":"BE","r":2,"ml":4,"lx":4.8,"ly":50.79,"b":[2.525,49.511,6.364,51.491],"p":[[[4.226,51.386,4.304,51.362,4.374,51.356,4.404,51.367,4.385,51.428,4.441,51.46,4.503,51.475,4.532,51.449,4.589,51.422,4.634,51.422,4.756,51.491,4.784,51.477,4.811,51.453,4.816,51.433,4.821,51.412,4.848,51.403,4.944,51.408,4.993,51.445,5.031,51.469,5.059,51.453,5.073,51.407,5.1,51.346,5.214,51.279,5.311,51.26,5.43,51.273,5.477,51.285,5.509,51.275,5.54,51.239,5.609,51.198,5.752,51.169,5.796,51.153,5.827,51.126,5.818,51.086,5.75,50.989,5.741,50.96,5.75,50.95,5.737,50.932,5.648,50.867,5.639,50.844,5.669,50.806,5.694,50.775,5.747,50.76,5.797,50.755,5.892,50.753,5.994,50.75,6.006,50.732,6.119,50.679,6.154,50.637,6.236,50.597,6.168,50.545,6.179,50.523,6.203,50.499,6.295,50.485,6.341,50.452,6.344,50.4,6.364,50.316,6.175,50.233,6.121,50.139,6.117,50.121,6.11,50.124,6.089,50.155,6.055,50.154,5.976,50.167,5.867,50.083,5.817,50.013,5.788,49.961,5.744,49.92,5.735,49.876,5.741,49.857,5.726,49.833,5.725,49.808,5.788,49.759,5.804,49.732,5.88,49.645,5.857,49.613,5.838,49.578,5.815,49.554,5.79,49.538,5.71,49.539,5.61,49.528,5.542,49.511,5.507,49.511,5.435,49.554,5.354,49.62,5.302,49.651,5.279,49.678,5.215,49.689,5.124,49.721,5.061,49.757,5.007,49.778,4.931,49.789,4.868,49.788,4.849,49.847,4.842,49.915,4.79,49.96,4.861,50.136,4.819,50.153,4.773,50.139,4.707,50.097,4.675,50.047,4.656,50.002,4.545,49.96,4.369,49.945,4.176,49.96,4.149,49.972,4.137,49.984,4.137,50,4.15,50.024,4.184,50.053,4.192,50.094,4.158,50.13,4.135,50.144,4.144,50.178,4.17,50.222,4.175,50.246,4.044,50.321,3.95,50.336,3.858,50.339,3.789,50.347,3.748,50.344,3.719,50.322,3.689,50.306,3.667,50.325,3.627,50.457,3.595,50.477,3.477,50.499,3.316,50.507,3.273,50.532,3.25,50.591,3.235,50.663,3.182,50.732,3.155,50.749,3.107,50.779,3.023,50.767,2.922,50.727,2.862,50.716,2.84,50.712,2.759,50.751,2.669,50.811,2.597,50.876,2.579,50.912,2.601,50.955,2.575,50.989,2.536,51.05,2.525,51.097,2.96,51.265,3.225,51.352,3.35,51.378,3.38,51.291,3.403,51.264,3.433,51.246,3.472,51.242,3.517,51.264,3.58,51.286,3.682,51.276,3.756,51.255,3.782,51.233,3.831,51.213,3.902,51.208,4.04,51.247,4.173,51.307,4.211,51.349,4.226,51.386]]]},{"n":"Belarus","c":"BY","r":4,"ml":3,"lx":28.42,"ly":53.82,"b":[23.175,51.265,32.71,56.146],"p":[[[31.763,52.101,31.574,52.108,31.346,52.105,31.218,52.05,31.168,52.063,31.079,52.077,30.981,52.046,30.846,51.953,30.755,51.895,30.667,51.814,30.639,51.77,30.584,51.689,30.533,51.596,30.561,51.531,30.602,51.471,30.612,51.406,30.633,51.355,30.577,51.318,30.545,51.265,30.45,51.274,30.333,51.326,30.309,51.4,30.22,51.451,30.161,51.478,30.064,51.482,29.909,51.458,29.706,51.44,29.553,51.435,29.47,51.408,29.346,51.383,29.299,51.413,29.23,51.497,29.174,51.581,29.136,51.617,29.102,51.628,29.061,51.625,29.013,51.599,28.978,51.572,28.928,51.562,28.85,51.54,28.793,51.51,28.731,51.433,28.69,51.439,28.648,51.457,28.599,51.543,28.532,51.562,28.425,51.564,28.292,51.582,28.184,51.608,28.144,51.602,28.08,51.565,28.011,51.56,27.859,51.592,27.829,51.577,27.789,51.529,27.741,51.483,27.7,51.478,27.677,51.49,27.69,51.572,27.601,51.602,27.452,51.606,27.348,51.594,27.296,51.597,27.27,51.614,27.142,51.752,27.074,51.761,26.953,51.754,26.773,51.771,26.567,51.802,26.453,51.813,26.394,51.844,26.267,51.855,25.925,51.914,25.786,51.924,25.58,51.925,25.267,51.938,25.067,51.931,24.974,51.911,24.866,51.899,24.685,51.888,24.611,51.89,24.495,51.883,24.362,51.868,24.324,51.838,24.28,51.775,24.127,51.665,23.978,51.591,23.951,51.585,23.864,51.624,23.792,51.637,23.707,51.641,23.647,51.629,23.609,51.61,23.614,51.525,23.605,51.518,23.54,51.619,23.545,51.71,23.581,51.762,23.626,51.809,23.607,51.88,23.651,51.973,23.652,52.04,23.633,52.07,23.598,52.103,23.501,52.14,23.458,52.17,23.327,52.208,23.197,52.257,23.175,52.287,23.181,52.307,23.204,52.338,23.303,52.428,23.411,52.516,23.48,52.552,23.845,52.664,23.901,52.704,23.915,52.77,23.916,52.819,23.909,52.905,23.887,53.028,23.859,53.112,23.789,53.271,23.599,53.599,23.485,53.94,23.559,53.92,23.734,53.912,23.873,53.936,23.944,53.939,24.008,53.932,24.104,53.945,24.191,53.95,24.237,53.92,24.318,53.893,24.479,53.932,24.621,53.98,24.768,53.975,24.789,53.998,24.826,54.119,24.87,54.145,25.046,54.133,25.111,54.155,25.179,54.214,25.284,54.251,25.371,54.251,25.461,54.293,25.506,54.265,25.527,54.215,25.497,54.175,25.51,54.16,25.573,54.14,25.681,54.14,25.749,54.157,25.765,54.18,25.765,54.221,25.748,54.26,25.703,54.293,25.617,54.31,25.558,54.311,25.547,54.332,25.568,54.377,25.62,54.46,25.685,54.536,25.725,54.564,25.732,54.59,25.724,54.636,25.722,54.718,25.781,54.833,25.859,54.919,25.964,54.947,26.093,54.962,26.175,55.003,26.216,55.05,26.231,55.09,26.251,55.125,26.292,55.14,26.601,55.13,26.648,55.204,26.675,55.225,26.734,55.247,26.776,55.273,26.76,55.293,26.681,55.306,26.495,55.318,26.458,55.342,26.47,55.372,26.519,55.448,26.567,55.546,26.591,55.623,26.594,55.668,26.62,55.68,26.772,55.694,26.822,55.709,26.953,55.813,27.053,55.831,27.309,55.804,27.427,55.806,27.459,55.804,27.577,55.799,27.589,55.809,27.642,55.912,27.694,55.942,27.896,56.076,28.032,56.133,28.118,56.146,28.148,56.143,28.284,56.056,28.316,56.053,28.392,56.087,28.407,56.089,28.564,56.092,28.637,56.062,28.691,56.003,28.741,55.955,28.795,55.943,28.947,56.002,29.032,56.022,29.087,56.021,29.283,55.968,29.375,55.939,29.396,55.912,29.398,55.881,29.373,55.835,29.353,55.784,29.413,55.725,29.482,55.685,29.63,55.751,29.685,55.77,29.744,55.77,29.824,55.795,29.882,55.832,29.937,55.845,30.043,55.836,30.234,55.845,30.456,55.787,30.475,55.769,30.587,55.7,30.626,55.666,30.662,55.655,30.722,55.622,30.801,55.601,30.856,55.608,30.882,55.596,30.907,55.57,30.909,55.525,30.901,55.397,30.862,55.36,30.821,55.33,30.811,55.307,30.814,55.279,30.877,55.223,30.959,55.138,30.978,55.088,30.978,55.05,30.867,54.941,30.83,54.915,30.804,54.861,30.791,54.806,30.799,54.783,30.984,54.696,31.121,54.648,31.152,54.625,31.155,54.611,31.082,54.517,31.075,54.492,31.185,54.453,31.246,54.392,31.299,54.292,31.404,54.196,31.628,54.111,31.792,54.056,31.826,54.031,31.838,54.001,31.825,53.935,31.783,53.855,31.754,53.81,31.821,53.792,31.992,53.797,32.2,53.781,32.45,53.693,32.451,53.653,32.425,53.617,32.442,53.579,32.47,53.547,32.686,53.448,32.706,53.419,32.71,53.371,32.704,53.336,32.644,53.329,32.578,53.312,32.469,53.27,32.426,53.211,32.251,53.128,32.142,53.091,32.055,53.089,31.85,53.106,31.777,53.147,31.747,53.184,31.668,53.201,31.563,53.202,31.418,53.196,31.388,53.185,31.365,53.139,31.303,53.061,31.259,53.017,31.295,52.99,31.353,52.933,31.443,52.862,31.535,52.798,31.565,52.759,31.563,52.731,31.519,52.699,31.526,52.633,31.616,52.546,31.586,52.532,31.577,52.426,31.577,52.312,31.602,52.285,31.65,52.262,31.691,52.221,31.759,52.126,31.763,52.101]]]},{"n":"Barbados","c":"BB","r":5,"ml":4.5,"lx":-59.57,"ly":13.16,"b":[-59.647,13.062,-59.428,13.318],"p":[[[-59.493,13.082,-59.522,13.062,-59.611,13.102,-59.643,13.15,-59.647,13.303,-59.592,13.318,-59.488,13.197,-59.428,13.153,-59.493,13.082]]]},{"n":"Bangladesh","c":"BD","r":3,"ml":3,"lx":89.68,"ly":24.21,"b":[88.023,20.79,92.632,26.572],"p":[[[89.051,22.093,89.056,22.186,89.05,22.275,88.971,22.511,88.921,22.632,88.927,22.671,88.923,22.688,88.9,22.844,88.867,22.939,88.851,23.041,88.928,23.187,88.897,23.21,88.808,23.23,88.724,23.255,88.704,23.293,88.741,23.437,88.698,23.493,88.636,23.55,88.616,23.573,88.596,23.602,88.567,23.674,88.623,23.826,88.7,24.003,88.714,24.07,88.727,24.186,88.734,24.231,88.724,24.275,88.642,24.326,88.499,24.347,88.397,24.389,88.338,24.454,88.287,24.48,88.225,24.461,88.146,24.486,88.079,24.55,88.023,24.628,88.03,24.664,88.045,24.713,88.15,24.915,88.189,24.921,88.279,24.882,88.313,24.882,88.373,24.962,88.456,25.188,88.574,25.188,88.678,25.18,88.748,25.169,88.817,25.176,88.89,25.194,88.93,25.223,88.952,25.259,88.944,25.291,88.855,25.334,88.82,25.366,88.795,25.456,88.769,25.49,88.593,25.495,88.502,25.537,88.452,25.574,88.363,25.698,88.253,25.79,88.147,25.811,88.107,25.841,88.085,25.888,88.097,25.956,88.129,26.018,88.151,26.087,88.235,26.178,88.334,26.258,88.378,26.312,88.44,26.369,88.448,26.401,88.437,26.437,88.386,26.472,88.351,26.483,88.346,26.505,88.37,26.564,88.418,26.572,88.518,26.518,88.62,26.431,88.681,26.353,88.683,26.292,88.722,26.282,88.762,26.279,88.828,26.252,88.896,26.26,88.941,26.245,88.97,26.251,88.982,26.286,88.948,26.338,88.924,26.375,88.952,26.412,88.983,26.42,89.019,26.41,89.067,26.377,89.102,26.308,89.108,26.202,89.186,26.106,89.289,26.038,89.37,26.006,89.467,25.984,89.55,26.005,89.591,26.072,89.573,26.132,89.586,26.186,89.619,26.216,89.671,26.214,89.71,26.171,89.823,25.941,89.8,25.84,89.825,25.56,89.796,25.376,89.801,25.336,89.814,25.305,89.833,25.293,89.866,25.293,90.004,25.258,90.12,25.22,90.25,25.185,90.439,25.158,90.555,25.167,90.613,25.168,90.73,25.159,91.038,25.174,91.293,25.178,91.397,25.152,91.48,25.142,91.763,25.161,92.05,25.169,92.205,25.111,92.373,25.015,92.468,24.944,92.485,24.903,92.475,24.869,92.443,24.849,92.385,24.849,92.251,24.895,92.228,24.881,92.231,24.786,92.227,24.771,92.198,24.686,92.117,24.494,92.102,24.408,92.085,24.386,92.064,24.374,92.001,24.371,91.952,24.357,91.931,24.326,91.899,24.261,91.877,24.195,91.846,24.175,91.772,24.211,91.727,24.205,91.669,24.19,91.611,24.153,91.571,24.107,91.526,24.091,91.393,24.1,91.367,24.094,91.35,24.06,91.336,24.019,91.232,23.92,91.192,23.763,91.16,23.661,91.166,23.581,91.254,23.374,91.315,23.104,91.339,23.077,91.359,23.068,91.369,23.075,91.367,23.13,91.371,23.198,91.399,23.214,91.436,23.2,91.471,23.141,91.511,23.034,91.554,22.992,91.62,22.98,91.695,23.005,91.751,23.054,91.774,23.106,91.758,23.21,91.754,23.287,91.79,23.361,91.919,23.471,91.938,23.505,91.929,23.598,91.93,23.686,91.979,23.692,92.044,23.678,92.127,23.721,92.152,23.722,92.187,23.676,92.246,23.684,92.289,23.492,92.334,23.324,92.334,23.242,92.341,23.07,92.362,22.929,92.393,22.897,92.43,22.822,92.464,22.734,92.491,22.685,92.51,22.526,92.532,22.41,92.561,22.048,92.575,21.978,92.583,21.94,92.584,21.609,92.593,21.467,92.625,21.351,92.632,21.306,92.6,21.27,92.569,21.263,92.539,21.32,92.472,21.363,92.373,21.409,92.331,21.44,92.28,21.428,92.208,21.358,92.18,21.293,92.192,21.202,92.215,21.113,92.264,21.061,92.268,21.005,92.286,20.932,92.312,20.864,92.324,20.792,92.308,20.79,92.248,20.884,92.195,20.984,92.056,21.175,92.011,21.516,92.008,21.685,91.913,21.883,91.85,22.157,91.825,22.229,91.858,22.317,91.863,22.35,91.845,22.343,91.797,22.297,91.734,22.407,91.693,22.505,91.53,22.708,91.482,22.797,91.48,22.885,91.41,22.797,91.314,22.735,91.216,22.642,91.151,22.614,90.946,22.597,90.827,22.721,90.656,23.025,90.634,23.094,90.656,23.273,90.616,23.442,90.616,23.532,90.604,23.591,90.573,23.578,90.562,23.537,90.568,23.474,90.556,23.422,90.408,23.432,90.269,23.456,90.392,23.367,90.523,23.346,90.591,23.266,90.599,23.204,90.595,23.134,90.528,23.085,90.466,23.054,90.478,22.987,90.552,22.905,90.462,22.882,90.437,22.828,90.435,22.752,90.481,22.685,90.498,22.635,90.487,22.589,90.532,22.539,90.596,22.436,90.616,22.362,90.589,22.258,90.553,22.218,90.494,22.179,90.356,22.048,90.288,21.899,90.231,21.83,90.159,21.817,90.131,21.847,90.071,21.887,90.07,21.96,90.088,22.017,90.21,22.157,90.143,22.138,90.069,22.098,89.954,22.023,89.918,22.116,89.894,22.203,89.894,22.308,89.985,22.466,89.882,22.388,89.853,22.289,89.866,22.173,89.853,22.091,89.812,21.983,89.757,21.919,89.668,21.878,89.628,21.814,89.569,21.767,89.567,21.861,89.547,21.984,89.483,22.276,89.469,22.213,89.503,22.032,89.501,21.914,89.452,21.821,89.354,21.721,89.279,21.707,89.234,21.722,89.167,21.784,89.094,21.873,89.082,22.015,89.051,22.093]],[[91.151,22.175,91.045,22.105,91.079,22.52,91.158,22.365,91.178,22.283,91.151,22.175]],[[91.557,22.382,91.51,22.353,91.467,22.378,91.411,22.476,91.439,22.599,91.456,22.617,91.484,22.577,91.523,22.491,91.548,22.425,91.557,22.382]],[[90.778,22.089,90.604,22.054,90.515,22.065,90.68,22.327,90.675,22.445,90.649,22.541,90.565,22.618,90.56,22.673,90.523,22.748,90.503,22.835,90.596,22.864,90.672,22.813,90.683,22.785,90.699,22.714,90.737,22.639,90.868,22.485,90.866,22.391,90.83,22.16,90.778,22.089]],[[91.874,21.832,91.838,21.75,91.82,21.81,91.835,21.885,91.851,21.927,91.861,21.927,91.883,21.884,91.874,21.832]],[[91.949,21.508,91.889,21.503,91.859,21.533,91.873,21.574,91.857,21.709,91.908,21.723,91.934,21.722,91.949,21.683,91.962,21.61,91.949,21.508]],[[90.642,22.963,90.66,22.92,90.604,22.946,90.562,22.975,90.536,23.015,90.58,23.035,90.642,22.963]]]},{"n":"Bahrain","c":"BH","r":4,"ml":4,"lx":50.55,"ly":26.06,"b":[50.452,25.807,50.617,26.246],"p":[[[50.607,25.883,50.575,25.807,50.544,25.833,50.466,25.966,50.489,26.058,50.452,26.191,50.47,26.229,50.564,26.246,50.586,26.241,50.558,26.198,50.61,26.124,50.617,26.002,50.607,25.883]]]},{"n":"The Bahamas","c":"BS","r":4,"ml":4,"lx":-77.15,"ly":26.4,"b":[-78.986,20.937,-72.747,26.94],"p":[[[-77.658,24.249,-77.656,24.227,-77.755,24.163,-77.683,24.118,-77.615,24.216,-77.562,24.137,-77.532,23.988,-77.537,23.962,-77.532,23.939,-77.521,23.911,-77.519,23.869,-77.574,23.739,-77.771,23.753,-77.776,23.862,-77.806,23.884,-77.852,24.04,-77.914,24.091,-78,24.22,-77.95,24.253,-77.884,24.242,-77.85,24.258,-77.757,24.27,-77.701,24.288,-77.658,24.249]],[[-77.226,25.904,-77.246,25.895,-77.333,25.996,-77.403,26.025,-77.294,26.096,-77.247,26.156,-77.248,26.289,-77.221,26.362,-77.23,26.425,-77.206,26.489,-77.239,26.561,-77.33,26.618,-77.511,26.846,-77.796,26.901,-77.944,26.904,-77.863,26.94,-77.788,26.936,-77.672,26.914,-77.534,26.903,-77.449,26.836,-77.369,26.748,-77.296,26.712,-77.266,26.689,-77.269,26.663,-77.257,26.639,-77.162,26.597,-77.066,26.53,-77.038,26.333,-77.167,26.24,-77.191,25.955,-77.226,25.904]],[[-73.027,21.192,-73.059,21.119,-73.165,20.979,-73.401,20.944,-73.661,20.937,-73.681,20.976,-73.687,21.009,-73.668,21.062,-73.67,21.082,-73.68,21.103,-73.585,21.126,-73.523,21.191,-73.425,21.202,-73.302,21.156,-73.235,21.154,-73.137,21.205,-73.058,21.313,-73.012,21.3,-73.027,21.192]],[[-77.744,24.707,-77.746,24.586,-77.735,24.496,-77.745,24.463,-77.853,24.403,-77.881,24.369,-77.983,24.335,-78.045,24.287,-78.076,24.365,-78.136,24.412,-78.146,24.493,-78.192,24.466,-78.258,24.483,-78.367,24.544,-78.435,24.628,-78.339,24.642,-78.319,24.59,-78.243,24.654,-78.26,24.687,-78.274,24.692,-78.299,24.754,-78.184,24.917,-78.159,25.022,-78.211,25.191,-78.163,25.202,-78.033,25.143,-77.975,25.085,-77.973,25.005,-77.919,24.943,-77.84,24.794,-77.744,24.707]],[[-78.493,26.729,-78.372,26.698,-78.307,26.702,-78.268,26.723,-78.089,26.714,-77.944,26.744,-77.922,26.691,-77.926,26.663,-78.234,26.637,-78.516,26.559,-78.671,26.507,-78.744,26.501,-78.799,26.528,-78.986,26.69,-78.936,26.673,-78.798,26.582,-78.712,26.599,-78.633,26.659,-78.621,26.705,-78.633,26.726,-78.597,26.798,-78.493,26.729]],[[-77.348,25.014,-77.46,24.993,-77.541,25.014,-77.562,25.03,-77.527,25.058,-77.451,25.081,-77.329,25.083,-77.276,25.056,-77.269,25.044,-77.348,25.014]],[[-74.058,22.723,-74.035,22.706,-74.099,22.665,-74.242,22.715,-74.275,22.712,-74.303,22.764,-74.314,22.804,-74.307,22.84,-74.221,22.812,-74.175,22.76,-74.058,22.723]],[[-74.429,24.068,-74.509,23.96,-74.551,23.969,-74.527,24.105,-74.472,24.127,-74.45,24.125,-74.429,24.068]],[[-74.207,22.214,-74.277,22.184,-74.261,22.236,-74.127,22.323,-74.052,22.401,-74.01,22.428,-73.995,22.449,-73.936,22.478,-73.906,22.527,-73.915,22.568,-73.976,22.635,-73.975,22.682,-73.954,22.716,-73.85,22.731,-73.877,22.681,-73.837,22.538,-73.975,22.361,-74.093,22.306,-74.207,22.214]],[[-76.649,25.487,-76.484,25.375,-76.344,25.332,-76.192,25.191,-76.127,25.141,-76.115,25.095,-76.141,24.886,-76.175,24.76,-76.17,24.649,-76.205,24.682,-76.241,24.754,-76.3,24.796,-76.32,24.818,-76.214,24.822,-76.204,24.936,-76.153,25.026,-76.16,25.119,-76.284,25.222,-76.369,25.313,-76.5,25.342,-76.621,25.432,-76.693,25.443,-76.781,25.427,-76.749,25.481,-76.727,25.552,-76.711,25.565,-76.649,25.487]],[[-75.665,23.45,-75.706,23.444,-75.781,23.471,-75.956,23.592,-76.037,23.603,-76.01,23.671,-75.949,23.647,-75.808,23.543,-75.754,23.49,-75.665,23.45]],[[-74.84,22.894,-74.847,22.869,-74.973,23.069,-75.132,23.117,-75.223,23.165,-75.204,23.193,-75.141,23.205,-75.131,23.268,-75.158,23.336,-75.241,23.475,-75.288,23.568,-75.31,23.59,-75.316,23.668,-75.217,23.547,-75.175,23.439,-75.109,23.333,-75.064,23.15,-74.937,23.088,-74.846,23,-74.84,22.894]],[[-75.308,24.2,-75.302,24.149,-75.369,24.159,-75.468,24.14,-75.503,24.139,-75.481,24.174,-75.412,24.221,-75.409,24.266,-75.494,24.33,-75.593,24.491,-75.639,24.529,-75.661,24.59,-75.744,24.655,-75.727,24.689,-75.71,24.698,-75.654,24.681,-75.526,24.45,-75.518,24.427,-75.308,24.2]],[[-73.041,22.429,-72.979,22.415,-72.945,22.416,-72.831,22.386,-72.763,22.344,-72.747,22.327,-72.784,22.291,-72.889,22.36,-72.981,22.369,-73.11,22.368,-73.162,22.381,-73.127,22.455,-73.041,22.429]],[[-72.916,21.507,-73.049,21.458,-73.063,21.515,-72.995,21.562,-72.916,21.507]]]},{"n":"Azerbaijan","c":"AZ","r":5,"ml":4,"lx":47.21,"ly":40.4,"b":[44.768,38.399,50.366,41.891],"p":[[[44.817,39.65,44.783,39.685,44.768,39.704,44.867,39.719,45.032,39.765,45.076,39.743,45.125,39.696,45.149,39.657,45.153,39.583,45.173,39.571,45.253,39.595,45.288,39.566,45.35,39.53,45.457,39.494,45.611,39.55,45.687,39.564,45.75,39.563,45.784,39.546,45.796,39.488,45.784,39.417,45.766,39.378,45.799,39.35,45.925,39.282,45.977,39.244,45.952,39.178,46.046,39.018,46.077,38.955,46.114,38.878,45.922,38.908,45.575,38.973,45.48,39.006,45.389,39.096,45.336,39.139,45.256,39.195,45.191,39.216,45.141,39.254,45.113,39.312,45.072,39.363,45,39.424,44.838,39.629,44.817,39.65]],[[48.869,38.435,48.84,38.437,48.636,38.399,48.593,38.411,48.417,38.586,48.381,38.606,48.306,38.613,48.261,38.642,48.225,38.689,48.205,38.724,48.023,38.819,47.996,38.854,47.993,38.884,48.019,38.912,48.05,38.935,48.139,38.959,48.242,38.979,48.275,38.994,48.292,39.019,48.291,39.059,48.274,39.099,48.125,39.172,48.109,39.203,48.104,39.241,48.113,39.281,48.136,39.312,48.257,39.355,48.322,39.399,48.282,39.448,48.151,39.561,47.996,39.684,47.892,39.685,47.773,39.649,47.582,39.543,47.476,39.498,47.338,39.424,47.188,39.341,47.065,39.253,46.989,39.18,46.853,39.148,46.783,39.087,46.555,38.904,46.491,38.907,46.487,38.997,46.49,39.069,46.475,39.111,46.401,39.168,46.4,39.192,46.42,39.207,46.477,39.198,46.55,39.201,46.585,39.224,46.507,39.299,46.437,39.349,46.378,39.382,46.365,39.402,46.365,39.417,46.378,39.434,46.478,39.475,46.488,39.513,46.481,39.555,46.322,39.617,46.202,39.594,46.095,39.664,46.026,39.719,45.94,39.777,45.863,39.808,45.79,39.881,45.662,39.956,45.58,39.978,45.581,39.989,45.596,40.003,45.63,40.014,45.858,40.011,45.886,40.025,45.9,40.057,45.931,40.105,45.968,40.175,45.965,40.234,45.736,40.329,45.57,40.417,45.454,40.532,45.376,40.638,45.379,40.674,45.401,40.707,45.579,40.804,45.591,40.83,45.588,40.847,45.524,40.897,45.444,40.948,45.419,40.986,45.369,41.005,45.273,41.006,45.106,41.069,45.071,41.076,45.063,41.088,45.071,41.101,45.19,41.126,45.189,41.147,45.152,41.175,45.085,41.195,45.023,41.246,45.001,41.291,45.217,41.423,45.281,41.45,45.422,41.425,45.716,41.338,45.696,41.289,45.725,41.262,45.793,41.224,45.922,41.187,46.031,41.167,46.087,41.184,46.171,41.198,46.28,41.154,46.381,41.099,46.431,41.077,46.458,41.07,46.534,41.089,46.626,41.16,46.662,41.246,46.673,41.287,46.619,41.344,46.509,41.406,46.385,41.46,46.305,41.508,46.255,41.602,46.204,41.613,46.191,41.625,46.182,41.657,46.184,41.702,46.202,41.737,46.252,41.752,46.303,41.757,46.348,41.79,46.405,41.855,46.43,41.891,46.538,41.87,46.552,41.812,46.571,41.8,46.616,41.807,46.69,41.831,46.749,41.813,46.826,41.743,46.931,41.67,46.988,41.621,47.01,41.588,47.064,41.555,47.143,41.516,47.205,41.456,47.261,41.315,47.318,41.282,47.521,41.229,47.592,41.218,47.791,41.199,47.861,41.213,47.964,41.334,48.056,41.459,48.142,41.485,48.298,41.545,48.391,41.602,48.431,41.663,48.519,41.779,48.573,41.844,48.665,41.787,48.824,41.63,49.051,41.374,49.107,41.302,49.143,41.218,49.175,41.116,49.226,41.026,49.457,40.8,49.556,40.716,49.718,40.608,49.776,40.584,49.852,40.577,49.991,40.577,50.119,40.535,50.183,40.505,50.248,40.462,50.307,40.412,50.366,40.279,50.143,40.323,49.919,40.316,49.792,40.288,49.669,40.249,49.551,40.194,49.477,40.087,49.415,39.84,49.324,39.608,49.328,39.501,49.367,39.398,49.363,39.35,49.321,39.329,49.269,39.285,49.2,39.073,49.165,39.03,49.121,39.004,49.109,39.029,49.111,39.085,49.013,39.134,48.962,39.079,48.926,38.962,48.854,38.839,48.851,38.815,48.869,38.435],[45.552,40.616,45.562,40.649,45.534,40.664,45.504,40.665,45.479,40.648,45.479,40.607,45.514,40.6,45.552,40.616]],[[45.024,41.027,45.002,41.016,44.969,41.027,44.959,41.053,44.961,41.079,44.994,41.086,45.021,41.078,45.029,41.054,45.024,41.027]]]},{"n":"Austria","c":"AT","r":4,"ml":3,"lx":14.13,"ly":47.52,"b":[9.524,46.4,17.147,49.001],"p":[[[9.528,47.271,9.609,47.392,9.626,47.467,9.554,47.511,9.524,47.524,9.549,47.534,9.651,47.526,9.715,47.551,9.749,47.576,9.839,47.552,9.972,47.505,10.034,47.474,10.06,47.449,10.074,47.429,10.066,47.393,10.096,47.38,10.159,47.374,10.2,47.363,10.186,47.317,10.183,47.279,10.241,47.284,10.313,47.313,10.369,47.366,10.404,47.417,10.43,47.541,10.439,47.552,10.483,47.542,10.659,47.547,10.742,47.524,10.873,47.52,10.871,47.501,10.894,47.47,10.952,47.427,10.981,47.398,11.042,47.393,11.136,47.409,11.191,47.425,11.212,47.414,11.298,47.425,11.374,47.46,11.393,47.487,11.47,47.506,11.574,47.55,11.717,47.583,12.186,47.62,12.204,47.647,12.197,47.709,12.209,47.718,12.268,47.703,12.363,47.688,12.436,47.666,12.483,47.637,12.527,47.636,12.594,47.656,12.686,47.669,12.771,47.639,12.796,47.607,12.781,47.59,12.783,47.564,12.809,47.542,12.879,47.506,12.968,47.476,13.014,47.478,13.032,47.508,13.048,47.579,13.054,47.655,13.034,47.699,12.986,47.709,12.928,47.713,12.898,47.722,12.908,47.746,12.954,47.808,12.954,47.891,12.85,47.985,12.76,48.076,12.76,48.107,12.814,48.161,12.897,48.204,13.082,48.275,13.14,48.29,13.215,48.302,13.323,48.331,13.375,48.361,13.409,48.394,13.46,48.565,13.472,48.572,13.487,48.582,13.675,48.523,13.692,48.533,13.724,48.542,13.785,48.587,13.799,48.622,13.797,48.686,13.803,48.748,13.815,48.767,13.843,48.76,13.924,48.728,13.989,48.692,14.049,48.602,14.19,48.579,14.368,48.576,14.431,48.616,14.489,48.626,14.554,48.613,14.691,48.599,14.707,48.672,14.786,48.747,14.822,48.774,14.923,48.771,14.947,48.828,14.972,48.984,14.993,49.001,15.067,48.998,15.14,48.969,15.162,48.946,15.2,48.948,15.253,48.964,15.311,48.974,15.403,48.957,15.599,48.886,15.701,48.86,15.765,48.865,15.825,48.864,16.057,48.755,16.219,48.739,16.367,48.739,16.415,48.772,16.478,48.8,16.544,48.796,16.601,48.782,16.713,48.734,16.764,48.722,16.833,48.714,16.884,48.704,16.928,48.621,16.953,48.599,16.949,48.589,16.943,48.551,16.904,48.504,16.863,48.441,16.865,48.387,16.973,48.198,17.068,48.083,17.086,48.04,17.147,48.006,17.089,47.964,17.078,47.901,17.04,47.873,17.03,47.837,17.046,47.805,17.046,47.764,17.067,47.708,16.973,47.695,16.863,47.697,16.823,47.694,16.786,47.679,16.748,47.686,16.647,47.739,16.591,47.751,16.551,47.747,16.521,47.724,16.47,47.695,16.421,47.674,16.432,47.656,16.64,47.609,16.677,47.536,16.637,47.477,16.623,47.448,16.574,47.425,16.515,47.405,16.443,47.4,16.434,47.367,16.463,47.273,16.44,47.253,16.417,47.223,16.438,47.146,16.483,47.14,16.493,47.123,16.485,47.091,16.477,47.058,16.461,47.022,16.453,47.007,16.424,46.997,16.332,47.002,16.253,46.972,16.093,46.863,16.037,46.845,15.977,46.801,15.98,46.706,15.972,46.697,15.958,46.678,15.767,46.711,15.76,46.711,15.633,46.698,15.545,46.655,15.439,46.63,15.217,46.643,15.001,46.626,14.949,46.613,14.893,46.606,14.841,46.58,14.811,46.545,14.757,46.499,14.68,46.463,14.597,46.436,14.577,46.413,14.55,46.4,14.504,46.417,14.466,46.416,14.42,46.428,14.267,46.441,14.1,46.462,14.02,46.482,13.929,46.498,13.831,46.511,13.744,46.514,13.7,46.52,13.49,46.556,13.352,46.558,13.169,46.573,12.806,46.626,12.7,46.647,12.599,46.654,12.479,46.673,12.388,46.703,12.33,46.76,12.268,46.836,12.154,46.935,12.131,46.985,12.166,47.028,12.201,47.061,12.197,47.075,12.169,47.082,11.97,47.04,11.776,46.986,11.699,46.985,11.625,46.997,11.528,46.997,11.433,46.983,11.244,46.976,11.134,46.936,11.063,46.859,11.025,46.797,10.993,46.777,10.927,46.769,10.829,46.775,10.76,46.793,10.689,46.846,10.58,46.854,10.479,46.855,10.453,46.865,10.455,46.899,10.415,46.964,10.349,46.985,10.18,46.862,10.133,46.852,9.997,46.885,9.878,46.938,9.865,46.976,9.845,47.007,9.745,47.037,9.62,47.057,9.58,47.057,9.596,47.076,9.611,47.107,9.601,47.132,9.572,47.158,9.556,47.185,9.551,47.212,9.542,47.234,9.537,47.255,9.528,47.271]]]},{"n":"Australia","c":"AU","r":2,"ml":1.7,"lx":134.05,"ly":-24.13,"b":[112.908,-54.749,158.959,-10.052],"p":[[[143.179,-11.954,143.153,-12.076,143.105,-12.17,143.099,-12.226,143.11,-12.304,143.191,-12.361,143.254,-12.398,143.29,-12.499,143.402,-12.64,143.398,-12.736,143.458,-12.856,143.512,-13.095,143.529,-13.304,143.587,-13.444,143.548,-13.741,143.589,-13.863,143.643,-13.964,143.707,-14.165,143.756,-14.349,143.822,-14.401,143.962,-14.463,144.106,-14.395,144.21,-14.302,144.322,-14.279,144.473,-14.232,144.586,-14.355,144.648,-14.492,144.916,-14.674,145.064,-14.791,145.18,-14.857,145.288,-14.943,145.277,-15.029,145.252,-15.097,145.276,-15.204,145.293,-15.327,145.272,-15.477,145.35,-15.702,145.375,-15.881,145.458,-16.056,145.452,-16.237,145.436,-16.305,145.426,-16.406,145.49,-16.532,145.55,-16.625,145.638,-16.726,145.755,-16.879,145.838,-16.91,145.912,-16.912,145.902,-17.07,146.05,-17.381,146.126,-17.635,146.074,-17.977,146.023,-18.176,146.032,-18.273,146.223,-18.51,146.333,-18.554,146.312,-18.667,146.297,-18.841,146.383,-18.977,146.481,-19.079,146.587,-19.139,146.692,-19.187,146.829,-19.236,147.003,-19.256,147.093,-19.333,147.139,-19.393,147.278,-19.414,147.342,-19.403,147.419,-19.378,147.471,-19.419,147.51,-19.474,147.586,-19.623,147.742,-19.77,147.853,-19.795,147.916,-19.869,148.004,-19.89,148.081,-19.899,148.19,-19.956,148.367,-20.087,148.527,-20.109,148.6,-20.145,148.759,-20.29,148.821,-20.366,148.885,-20.481,148.805,-20.492,148.73,-20.468,148.684,-20.58,148.789,-20.736,148.912,-20.845,149.061,-20.961,149.205,-21.125,149.241,-21.25,149.28,-21.3,149.329,-21.476,149.454,-21.579,149.46,-21.765,149.524,-22.024,149.596,-22.258,149.645,-22.328,149.704,-22.441,149.772,-22.426,149.822,-22.39,149.92,-22.501,149.974,-22.551,150.006,-22.522,149.942,-22.308,149.981,-22.184,150.021,-22.168,150.076,-22.164,150.143,-22.265,150.235,-22.373,150.405,-22.469,150.541,-22.559,150.58,-22.556,150.564,-22.486,150.569,-22.384,150.623,-22.367,150.672,-22.418,150.764,-22.576,150.783,-22.903,150.783,-23.177,150.843,-23.458,150.931,-23.532,150.989,-23.602,151.088,-23.696,151.154,-23.784,151.236,-23.825,151.501,-24.012,151.575,-24.034,151.691,-24.038,151.832,-24.123,151.903,-24.201,152.055,-24.494,152.13,-24.598,152.282,-24.699,152.353,-24.733,152.456,-24.802,152.493,-24.904,152.502,-24.964,152.563,-25.072,152.654,-25.202,152.789,-25.274,152.913,-25.432,152.921,-25.689,152.985,-25.816,153.028,-25.87,153.125,-25.923,153.165,-25.964,153.084,-26.304,153.162,-26.983,153.117,-27.194,153.198,-27.405,153.386,-27.769,153.428,-27.898,153.455,-28.048,153.576,-28.241,153.569,-28.533,153.617,-28.673,153.605,-28.854,153.462,-29.05,153.348,-29.29,153.347,-29.497,153.272,-29.892,153.224,-29.999,153.188,-30.164,153.031,-30.563,153.024,-30.72,153.048,-30.907,153.022,-31.087,152.982,-31.209,152.944,-31.435,152.786,-31.786,152.559,-32.046,152.545,-32.243,152.517,-32.33,152.47,-32.439,152.331,-32.558,152.247,-32.609,152.216,-32.678,152.137,-32.678,152.135,-32.7,152.188,-32.722,152.164,-32.757,151.954,-32.82,151.813,-32.901,151.668,-33.099,151.608,-33.202,151.53,-33.301,151.484,-33.347,151.463,-33.397,151.432,-33.522,151.358,-33.544,151.292,-33.581,151.323,-33.699,151.288,-33.835,151.28,-33.927,151.245,-33.985,151.202,-33.964,151.168,-33.973,151.125,-34.005,151.191,-34.015,151.232,-34.03,151.09,-34.162,150.96,-34.297,150.927,-34.387,150.871,-34.499,150.822,-34.749,150.781,-34.892,150.809,-34.994,150.805,-35.013,150.775,-35.02,150.756,-35.007,150.697,-35.042,150.681,-35.077,150.706,-35.12,150.722,-35.135,150.715,-35.155,150.69,-35.178,150.634,-35.178,150.567,-35.214,150.374,-35.584,150.292,-35.682,150.195,-35.834,150.158,-35.971,150.129,-36.12,150.095,-36.373,150.063,-36.55,149.988,-36.723,149.96,-36.846,149.951,-37.08,149.986,-37.258,149.963,-37.353,149.962,-37.444,149.933,-37.529,149.809,-37.548,149.709,-37.617,149.565,-37.73,149.481,-37.771,149.298,-37.802,148.944,-37.788,148.263,-37.831,148.131,-37.856,147.877,-37.934,147.631,-38.056,147.396,-38.219,146.857,-38.663,146.436,-38.712,146.356,-38.712,146.293,-38.7,146.217,-38.727,146.216,-38.783,146.286,-38.84,146.337,-38.894,146.427,-38.82,146.467,-38.84,146.482,-38.978,146.484,-39.065,146.457,-39.112,146.4,-39.146,146.34,-39.124,146.332,-39.077,146.254,-38.964,146.158,-38.866,146.07,-38.834,146.018,-38.867,145.935,-38.902,145.866,-38.776,145.791,-38.667,145.692,-38.656,145.606,-38.657,145.535,-38.61,145.397,-38.535,145.424,-38.477,145.463,-38.416,145.542,-38.394,145.518,-38.311,145.476,-38.244,145.366,-38.226,145.293,-38.238,145.249,-38.291,145.191,-38.384,144.96,-38.501,144.847,-38.436,144.718,-38.34,144.78,-38.347,144.911,-38.344,145.02,-38.258,145.067,-38.205,145.12,-38.091,145.05,-38.011,144.985,-37.952,144.891,-37.9,144.538,-38.077,144.465,-38.103,144.396,-38.137,144.518,-38.166,144.589,-38.158,144.665,-38.21,144.544,-38.284,144.448,-38.304,144.329,-38.348,144.102,-38.462,143.812,-38.699,143.687,-38.767,143.539,-38.821,143.338,-38.758,143.226,-38.743,143.083,-38.646,142.84,-38.581,142.612,-38.452,142.456,-38.386,142.345,-38.372,142.188,-38.399,141.925,-38.284,141.725,-38.271,141.594,-38.388,141.492,-38.38,141.424,-38.363,141.214,-38.172,141.011,-38.077,140.627,-38.028,140.39,-37.897,140.212,-37.642,139.875,-37.352,139.784,-37.246,139.742,-37.142,139.738,-37.06,139.784,-36.903,139.847,-36.748,139.857,-36.662,139.729,-36.371,139.549,-36.097,139.466,-36.01,139.245,-35.827,139.038,-35.689,138.985,-35.618,138.969,-35.581,139.067,-35.598,139.113,-35.542,139.178,-35.523,139.231,-35.598,139.289,-35.611,139.292,-35.486,139.325,-35.427,139.303,-35.399,139.283,-35.375,139.193,-35.347,139.094,-35.39,139.018,-35.443,138.915,-35.489,138.875,-35.537,138.771,-35.538,138.73,-35.551,138.522,-35.642,138.389,-35.645,138.184,-35.613,138.252,-35.487,138.333,-35.412,138.4,-35.326,138.511,-35.024,138.49,-34.764,138.436,-34.656,138.264,-34.44,138.186,-34.307,138.089,-34.17,138.041,-34.25,138.012,-34.334,137.919,-34.456,137.874,-34.727,137.692,-35.143,137.566,-35.148,137.46,-35.131,137.272,-35.179,137.144,-35.236,137.03,-35.237,136.967,-35.255,136.884,-35.24,137.014,-34.916,137.128,-34.925,137.252,-34.912,137.308,-34.917,137.391,-34.913,137.454,-34.764,137.493,-34.598,137.469,-34.49,137.459,-34.379,137.484,-34.252,137.494,-34.161,137.65,-33.859,137.781,-33.703,137.932,-33.579,137.914,-33.461,137.866,-33.314,137.852,-33.201,137.924,-33.165,137.993,-33.094,137.913,-32.771,137.863,-32.674,137.783,-32.578,137.782,-32.702,137.791,-32.823,137.68,-32.978,137.536,-33.089,137.442,-33.194,137.354,-33.43,137.237,-33.629,137.13,-33.703,137.034,-33.72,136.937,-33.75,136.783,-33.83,136.636,-33.897,136.526,-33.984,136.431,-34.03,136.121,-34.429,135.98,-34.562,135.951,-34.616,135.891,-34.661,135.903,-34.724,135.951,-34.767,135.999,-34.944,135.97,-34.982,135.919,-34.962,135.792,-34.863,135.713,-34.899,135.648,-34.94,135.481,-34.758,135.412,-34.716,135.324,-34.643,135.231,-34.58,135.191,-34.573,135.123,-34.586,135.13,-34.537,135.176,-34.497,135.217,-34.487,135.292,-34.546,135.379,-34.598,135.427,-34.602,135.45,-34.581,135.368,-34.376,135.312,-34.196,135.286,-34.142,135.219,-33.96,135.185,-33.907,135.042,-33.778,134.889,-33.626,134.847,-33.445,134.791,-33.328,134.719,-33.255,134.608,-33.19,134.301,-33.165,134.174,-32.979,134.1,-32.749,134.158,-32.733,134.227,-32.731,134.249,-32.659,134.234,-32.549,133.93,-32.412,133.787,-32.269,133.665,-32.207,133.551,-32.183,133.401,-32.188,133.212,-32.184,132.757,-31.956,132.649,-31.949,132.324,-32.02,132.215,-32.007,131.721,-31.696,131.393,-31.549,131.285,-31.521,131.144,-31.496,131.029,-31.532,130.948,-31.566,130.783,-31.604,130.13,-31.579,129.569,-31.627,129.188,-31.66,128.946,-31.703,128.546,-31.888,128.068,-32.067,127.678,-32.151,127.32,-32.264,127.084,-32.297,126.779,-32.311,126.137,-32.257,125.917,-32.297,125.567,-32.506,125.464,-32.557,125.267,-32.614,124.759,-32.883,124.525,-32.94,124.373,-32.958,124.244,-33.015,124.126,-33.129,123.967,-33.446,123.868,-33.596,123.65,-33.836,123.507,-33.916,123.365,-33.905,123.208,-33.988,123.068,-33.901,122.956,-33.884,122.778,-33.891,122.151,-33.992,122.061,-33.874,121.946,-33.857,121.73,-33.862,121.405,-33.827,120.815,-33.871,120.531,-33.92,120.418,-33.963,120.209,-33.935,119.854,-33.975,119.729,-34.042,119.635,-34.101,119.451,-34.368,119.248,-34.456,119.081,-34.459,118.895,-34.48,118.52,-34.737,118.136,-34.987,118.006,-35.013,117.863,-35.055,117.675,-35.075,117.582,-35.098,117.144,-35.034,116.865,-35.027,116.517,-34.988,116.217,-34.866,115.987,-34.795,115.726,-34.526,115.565,-34.426,115.278,-34.304,115.195,-34.308,115.128,-34.342,115.009,-34.256,115.006,-34.145,114.973,-34.051,114.976,-33.804,114.994,-33.515,115.099,-33.58,115.182,-33.643,115.359,-33.64,115.515,-33.531,115.604,-33.372,115.683,-33.193,115.671,-33.002,115.619,-32.667,115.654,-32.597,115.708,-32.568,115.725,-32.401,115.738,-31.888,115.698,-31.695,115.455,-31.303,115.294,-30.962,115.177,-30.808,115.078,-30.56,114.995,-30.216,114.969,-30.042,114.942,-29.722,114.971,-29.54,114.959,-29.434,114.857,-29.143,114.628,-28.872,114.591,-28.772,114.592,-28.666,114.537,-28.543,114.354,-28.295,114.165,-28.081,114.133,-27.976,114.098,-27.544,114.028,-27.347,113.709,-26.848,113.333,-26.417,113.231,-26.241,113.185,-26.182,113.211,-26.174,113.253,-26.197,113.3,-26.24,113.323,-26.244,113.345,-26.208,113.343,-26.126,113.356,-26.08,113.389,-26.106,113.427,-26.198,113.547,-26.437,113.582,-26.558,113.734,-26.595,113.78,-26.563,113.836,-26.501,113.853,-26.332,113.776,-26.256,113.706,-26.224,113.589,-26.099,113.513,-25.898,113.395,-25.713,113.397,-25.647,113.451,-25.599,113.539,-25.625,113.621,-25.732,113.713,-25.831,113.698,-26.004,113.684,-26.052,113.692,-26.092,113.724,-26.13,113.766,-26.16,113.812,-26.116,113.854,-26.014,113.88,-26.028,113.942,-26.259,113.992,-26.321,114.09,-26.394,114.176,-26.337,114.216,-26.289,114.203,-26.126,114.229,-25.969,114.214,-25.852,113.993,-25.545,113.792,-25.166,113.671,-24.977,113.569,-24.693,113.504,-24.595,113.418,-24.436,113.413,-24.254,113.421,-24.132,113.49,-23.87,113.553,-23.733,113.757,-23.418,113.767,-23.283,113.765,-23.18,113.795,-23.024,113.795,-22.915,113.768,-22.813,113.683,-22.638,113.795,-22.332,113.958,-21.939,114.023,-21.881,114.124,-21.829,114.143,-21.91,114.093,-22.181,114.164,-22.323,114.142,-22.483,114.205,-22.456,114.304,-22.425,114.378,-22.342,114.417,-22.261,114.603,-21.942,114.709,-21.823,114.859,-21.736,115.162,-21.631,115.456,-21.492,115.596,-21.358,115.771,-21.242,115.894,-21.117,116.011,-21.03,116.606,-20.713,116.707,-20.654,116.836,-20.647,116.995,-20.658,117.139,-20.641,117.293,-20.713,117.406,-20.721,117.684,-20.643,117.832,-20.573,118.087,-20.419,118.199,-20.375,118.458,-20.327,118.751,-20.262,119.104,-19.995,119.359,-20.012,119.586,-20.038,119.768,-19.958,120.196,-19.909,120.434,-19.842,120.878,-19.665,120.998,-19.604,121.18,-19.478,121.338,-19.32,121.494,-19.106,121.589,-18.915,121.631,-18.817,121.722,-18.66,121.785,-18.536,121.834,-18.477,122.006,-18.394,122.262,-18.159,122.345,-18.112,122.361,-18.037,122.306,-17.995,122.237,-17.969,122.191,-17.72,122.147,-17.549,122.143,-17.428,122.16,-17.314,122.261,-17.136,122.333,-17.059,122.432,-16.97,122.523,-16.943,122.598,-16.865,122.72,-16.788,122.772,-16.71,122.848,-16.552,122.917,-16.433,122.971,-16.437,123.074,-16.715,123.142,-16.863,123.266,-17.037,123.383,-17.293,123.479,-17.41,123.525,-17.486,123.563,-17.521,123.571,-17.472,123.562,-17.415,123.608,-17.22,123.586,-17.083,123.594,-17.03,123.618,-17.008,123.664,-17.023,123.754,-17.1,123.799,-17.127,123.831,-17.121,123.829,-16.997,123.874,-16.919,123.856,-16.865,123.778,-16.868,123.745,-16.801,123.68,-16.724,123.607,-16.668,123.518,-16.541,123.49,-16.491,123.525,-16.468,123.581,-16.471,123.626,-16.416,123.646,-16.343,123.607,-16.224,123.647,-16.18,123.729,-16.192,123.859,-16.382,123.915,-16.364,123.961,-16.287,124.044,-16.265,124.13,-16.279,124.186,-16.334,124.3,-16.388,124.453,-16.382,124.53,-16.395,124.692,-16.386,124.772,-16.403,124.757,-16.373,124.669,-16.339,124.57,-16.332,124.454,-16.335,124.405,-16.299,124.388,-16.203,124.416,-16.133,124.435,-16.104,124.51,-16.116,124.577,-16.114,124.585,-16.02,124.609,-15.937,124.649,-15.87,124.648,-15.805,124.607,-15.823,124.504,-15.972,124.455,-15.851,124.382,-15.758,124.397,-15.626,124.44,-15.494,124.506,-15.475,124.562,-15.496,124.644,-15.419,124.691,-15.36,124.68,-15.311,124.693,-15.274,124.75,-15.285,124.972,-15.404,125.016,-15.467,125.063,-15.442,125.078,-15.375,125.073,-15.307,125.024,-15.317,124.909,-15.31,124.883,-15.272,124.893,-15.241,124.839,-15.161,124.914,-15.11,124.979,-15.107,125.023,-15.072,125.024,-15.024,125.038,-15.004,125.073,-15.032,125.189,-15.045,125.302,-15.107,125.356,-15.12,125.376,-15.087,125.384,-15.016,125.243,-14.945,125.239,-14.875,125.18,-14.794,125.179,-14.715,125.267,-14.648,125.285,-14.584,125.335,-14.558,125.436,-14.557,125.504,-14.502,125.58,-14.483,125.598,-14.362,125.597,-14.278,125.628,-14.257,125.705,-14.291,125.681,-14.388,125.681,-14.48,125.662,-14.529,125.691,-14.525,125.708,-14.505,125.738,-14.444,125.82,-14.469,125.84,-14.534,125.85,-14.597,125.891,-14.618,125.946,-14.52,126.021,-14.495,126.017,-14.371,126.045,-14.283,126.054,-14.217,126.101,-14.184,126.111,-14.114,126.073,-14.066,126.054,-13.977,126.119,-13.958,126.184,-14.002,126.228,-14.113,126.258,-14.164,126.299,-14.136,126.323,-14.062,126.403,-14.019,126.482,-14.079,126.57,-14.161,126.679,-14.089,126.781,-13.955,126.764,-13.873,126.776,-13.788,126.903,-13.744,127.006,-13.777,127.099,-13.867,127.293,-13.935,127.458,-14.031,127.531,-14.095,127.673,-14.195,127.763,-14.299,127.888,-14.485,128.18,-14.712,128.199,-14.752,128.16,-14.827,128.124,-14.924,128.08,-15.088,128.069,-15.329,128.112,-15.312,128.155,-15.226,128.202,-15.243,128.255,-15.299,128.259,-15.246,128.227,-15.214,128.173,-15.102,128.175,-15.043,128.218,-14.996,128.285,-14.939,128.358,-14.902,128.403,-14.869,128.41,-14.829,128.477,-14.788,128.576,-14.775,128.636,-14.781,129.058,-14.884,129.165,-14.988,129.175,-15.115,129.216,-15.16,129.238,-15.08,129.234,-14.906,129.268,-14.871,129.381,-14.898,129.459,-14.933,129.567,-15.047,129.588,-15.103,129.635,-15.14,129.65,-15.087,129.628,-15.012,129.613,-14.926,129.637,-14.851,129.763,-14.845,129.849,-14.829,129.808,-14.8,129.754,-14.79,129.663,-14.721,129.605,-14.647,129.699,-14.575,129.698,-14.557,129.608,-14.56,129.484,-14.49,129.379,-14.392,129.459,-14.213,129.62,-14.038,129.71,-13.98,129.718,-13.921,129.762,-13.812,129.789,-13.72,129.797,-13.648,129.839,-13.573,129.938,-13.502,130.073,-13.476,130.136,-13.448,130.199,-13.383,130.26,-13.302,130.135,-13.146,130.145,-13.059,130.168,-12.957,130.318,-12.883,130.4,-12.688,130.454,-12.659,130.572,-12.664,130.617,-12.647,130.61,-12.491,130.623,-12.431,130.672,-12.407,130.736,-12.428,130.777,-12.495,130.867,-12.558,130.898,-12.524,130.883,-12.455,130.874,-12.367,130.957,-12.348,131.023,-12.343,131.03,-12.271,131.02,-12.214,131.046,-12.19,131.22,-12.178,131.265,-12.119,131.292,-12.068,131.314,-12.096,131.342,-12.21,131.438,-12.277,131.726,-12.278,131.888,-12.232,131.957,-12.259,132.064,-12.281,132.182,-12.227,132.253,-12.186,132.372,-12.239,132.411,-12.295,132.442,-12.176,132.511,-12.135,132.584,-12.11,132.676,-12.13,132.713,-12.123,132.63,-12.035,132.635,-11.955,132.63,-11.836,132.645,-11.727,132.674,-11.649,132.475,-11.492,132.278,-11.468,132.134,-11.501,132.073,-11.475,131.945,-11.349,131.822,-11.302,131.812,-11.271,131.962,-11.181,132.019,-11.196,132.106,-11.281,132.155,-11.311,132.198,-11.305,132.225,-11.239,132.263,-11.204,132.334,-11.224,132.557,-11.367,132.683,-11.506,132.747,-11.469,132.857,-11.391,132.961,-11.407,133.025,-11.453,133.114,-11.622,133.185,-11.706,133.356,-11.728,133.443,-11.76,133.533,-11.816,133.654,-11.811,133.904,-11.832,134.139,-11.94,134.237,-12.008,134.351,-12.026,134.417,-12.053,134.538,-12.061,134.73,-11.984,134.816,-12.055,134.855,-12.103,135.03,-12.194,135.218,-12.222,135.352,-12.129,135.549,-12.061,135.686,-11.956,135.788,-11.907,135.885,-11.822,135.922,-11.826,135.844,-11.905,135.834,-11.951,135.896,-11.97,135.889,-11.993,135.804,-12.055,135.703,-12.152,135.704,-12.21,135.744,-12.242,135.791,-12.228,135.857,-12.179,135.938,-12.152,136.008,-12.191,136.031,-12.331,136.082,-12.422,136.193,-12.435,136.261,-12.434,136.329,-12.306,136.292,-12.196,136.25,-12.173,136.27,-12.132,136.443,-11.951,136.54,-11.958,136.61,-12.134,136.719,-12.226,136.836,-12.219,136.897,-12.244,136.947,-12.35,136.537,-12.784,136.518,-12.833,136.573,-12.912,136.594,-13.004,136.461,-13.225,136.412,-13.236,136.365,-13.176,136.294,-13.138,136.232,-13.165,136.166,-13.181,135.927,-13.304,135.929,-13.622,135.99,-13.81,135.954,-13.935,135.883,-14.153,135.806,-14.234,135.745,-14.287,135.539,-14.585,135.473,-14.657,135.405,-14.758,135.428,-14.856,135.453,-14.923,135.531,-15,135.833,-15.16,135.97,-15.27,136.205,-15.403,136.259,-15.495,136.291,-15.57,136.462,-15.655,136.584,-15.707,136.619,-15.693,136.644,-15.676,136.675,-15.675,136.705,-15.685,136.7,-15.752,136.687,-15.788,136.698,-15.835,136.785,-15.894,136.923,-15.892,137.002,-15.878,137.09,-15.941,137.169,-15.982,137.299,-16.066,137.526,-16.167,137.704,-16.233,137.913,-16.477,138.072,-16.617,138.245,-16.718,138.506,-16.79,138.626,-16.778,138.82,-16.861,139.01,-16.899,139.11,-17.014,139.145,-17.101,139.154,-17.168,139.248,-17.329,139.441,-17.381,139.69,-17.541,139.895,-17.611,139.946,-17.654,140.036,-17.703,140.21,-17.704,140.511,-17.625,140.648,-17.544,140.83,-17.414,140.916,-17.193,140.966,-17.015,141.219,-16.646,141.291,-16.463,141.356,-16.221,141.412,-16.07,141.393,-15.905,141.452,-15.605,141.581,-15.195,141.625,-15.057,141.604,-14.853,141.523,-14.47,141.559,-14.338,141.594,-14.153,141.535,-14.019,141.481,-13.927,141.473,-13.798,141.534,-13.554,141.589,-13.425,141.645,-13.259,141.614,-12.943,141.735,-12.833,141.782,-12.779,141.876,-12.778,141.92,-12.803,141.93,-12.74,141.893,-12.681,141.878,-12.613,141.852,-12.579,141.795,-12.567,141.747,-12.529,141.678,-12.491,141.689,-12.351,141.806,-12.08,141.871,-11.976,141.913,-12.019,141.961,-12.054,141.968,-11.976,141.952,-11.896,142.041,-11.632,142.139,-11.273,142.168,-10.947,142.326,-10.884,142.407,-10.802,142.456,-10.707,142.545,-10.707,142.605,-10.748,142.565,-10.819,142.553,-10.874,142.723,-11.01,142.78,-11.115,142.803,-11.214,142.837,-11.307,142.853,-11.432,142.851,-11.632,142.873,-11.821,142.934,-11.881,142.988,-11.919,143.066,-11.924,143.179,-11.954]],[[153.077,-25.751,153.052,-25.778,153.007,-25.729,152.977,-25.551,152.999,-25.448,153.052,-25.354,153.061,-25.302,153.038,-25.193,153.189,-25.071,153.228,-25.006,153.242,-24.923,153.186,-24.833,153.144,-24.815,153.181,-24.765,153.223,-24.74,153.257,-24.729,153.282,-24.738,153.298,-24.915,153.359,-24.978,153.35,-25.063,153.141,-25.513,153.084,-25.683,153.077,-25.751]],[[139.508,-16.573,139.431,-16.661,139.392,-16.649,139.354,-16.697,139.283,-16.719,139.239,-16.719,139.16,-16.742,139.148,-16.714,139.163,-16.626,139.229,-16.528,139.293,-16.467,139.459,-16.438,139.588,-16.395,139.604,-16.403,139.698,-16.515,139.56,-16.529,139.508,-16.573]],[[136.715,-13.804,136.758,-13.845,136.804,-13.842,136.845,-13.751,136.871,-13.764,136.891,-13.787,136.906,-13.827,136.843,-13.897,136.815,-13.907,136.788,-13.946,136.745,-14.073,136.75,-14.115,136.787,-14.158,136.885,-14.197,136.934,-14.179,136.951,-14.184,136.931,-14.246,136.894,-14.293,136.763,-14.273,136.65,-14.28,136.461,-14.235,136.363,-14.229,136.335,-14.212,136.392,-14.175,136.428,-14.126,136.411,-14.011,136.425,-13.865,136.534,-13.794,136.583,-13.721,136.656,-13.676,136.702,-13.682,136.696,-13.726,136.715,-13.804]],[[130.459,-11.679,130.542,-11.703,130.58,-11.737,130.603,-11.773,130.606,-11.817,130.503,-11.836,130.317,-11.772,130.131,-11.825,130.077,-11.825,130.043,-11.787,130.072,-11.681,130.139,-11.697,130.198,-11.658,130.187,-11.541,130.153,-11.478,130.251,-11.361,130.295,-11.337,130.339,-11.337,130.377,-11.42,130.386,-11.51,130.433,-11.592,130.459,-11.679]],[[130.619,-11.376,130.752,-11.384,130.913,-11.309,130.987,-11.34,131.023,-11.334,131.141,-11.263,131.217,-11.243,131.268,-11.19,131.321,-11.247,131.437,-11.313,131.473,-11.383,131.522,-11.415,131.539,-11.437,131.468,-11.51,131.459,-11.588,131.383,-11.583,131.292,-11.711,130.951,-11.926,130.645,-11.742,130.512,-11.618,130.423,-11.446,130.405,-11.305,130.369,-11.215,130.385,-11.192,130.403,-11.18,130.427,-11.183,130.519,-11.279,130.56,-11.306,130.619,-11.376]],[[113.183,-26.053,113.156,-26.095,112.964,-25.783,112.908,-25.57,112.947,-25.532,112.982,-25.52,113.096,-25.815,113.132,-25.883,113.132,-25.952,113.148,-25.974,113.183,-26.053]],[[137.596,-35.739,137.836,-35.762,137.929,-35.726,138.047,-35.755,138.123,-35.852,138.067,-35.901,138.012,-35.908,137.836,-35.868,137.671,-35.898,137.622,-35.938,137.59,-36.027,137.448,-36.075,137.382,-36.021,137.21,-35.982,137.148,-36.039,137.026,-36.024,136.913,-36.047,136.755,-36.033,136.589,-35.935,136.541,-35.89,136.579,-35.809,136.639,-35.749,137.092,-35.664,137.334,-35.592,137.53,-35.605,137.585,-35.62,137.635,-35.656,137.598,-35.722,137.596,-35.739]],[[145.487,-38.355,145.336,-38.421,145.28,-38.391,145.286,-38.341,145.295,-38.319,145.427,-38.314,145.487,-38.355]],[[145.314,-38.491,145.349,-38.538,145.355,-38.557,145.271,-38.52,145.128,-38.528,145.218,-38.459,145.288,-38.472,145.314,-38.491]],[[149.044,-20.292,149.02,-20.303,148.987,-20.302,148.939,-20.284,148.981,-20.154,149.004,-20.221,149.045,-20.278,149.044,-20.292]],[[148.936,-20.15,148.913,-20.154,148.887,-20.144,148.906,-20.102,148.932,-20.069,148.968,-20.044,148.956,-20.135,148.936,-20.15]],[[151.147,-23.491,151.181,-23.516,151.212,-23.513,151.24,-23.53,151.229,-23.595,151.274,-23.668,151.296,-23.72,151.262,-23.762,151.238,-23.776,151.184,-23.741,151.033,-23.53,151.06,-23.461,151.147,-23.491]],[[150.517,-22.323,150.488,-22.325,150.462,-22.308,150.485,-22.268,150.488,-22.211,150.521,-22.228,150.549,-22.307,150.517,-22.323]],[[149.928,-22.193,149.894,-22.223,149.87,-22.15,149.875,-22.074,149.912,-22.049,149.928,-22.149,149.928,-22.193]],[[153.539,-27.436,153.453,-27.712,153.427,-27.706,153.396,-27.665,153.401,-27.506,153.435,-27.405,153.522,-27.422,153.539,-27.436]],[[153.442,-27.316,153.421,-27.331,153.377,-27.235,153.365,-27.139,153.38,-27.049,153.432,-27.03,153.467,-27.038,153.426,-27.201,153.442,-27.316]],[[142.275,-10.705,142.191,-10.762,142.137,-10.732,142.125,-10.668,142.131,-10.641,142.198,-10.592,142.275,-10.705]],[[142.339,-10.192,142.279,-10.254,142.216,-10.236,142.195,-10.199,142.219,-10.149,142.299,-10.14,142.339,-10.192]],[[142.168,-10.154,142.142,-10.181,142.098,-10.122,142.149,-10.052,142.192,-10.085,142.168,-10.154]],[[146.278,-18.231,146.299,-18.326,146.342,-18.4,146.327,-18.449,146.299,-18.485,146.236,-18.451,146.191,-18.363,146.116,-18.292,146.099,-18.252,146.187,-18.255,146.231,-18.241,146.249,-18.226,146.278,-18.231]],[[136.599,-11.379,136.527,-11.439,136.522,-11.394,136.56,-11.358,136.649,-11.212,136.688,-11.178,136.711,-11.158,136.727,-11.105,136.732,-11.025,136.78,-11.012,136.741,-11.195,136.599,-11.379]],[[136.339,-11.602,136.18,-11.677,136.267,-11.576,136.449,-11.487,136.479,-11.466,136.471,-11.509,136.379,-11.583,136.339,-11.602]],[[137.094,-15.778,137.051,-15.824,136.996,-15.776,136.985,-15.726,136.943,-15.712,136.963,-15.666,136.986,-15.652,137.01,-15.595,137.065,-15.663,137.071,-15.738,137.094,-15.778]],[[136.863,-15.62,136.847,-15.627,136.846,-15.544,136.877,-15.503,136.89,-15.589,136.863,-15.62]],[[136.591,-15.628,136.531,-15.632,136.514,-15.627,136.503,-15.583,136.523,-15.543,136.586,-15.534,136.612,-15.544,136.591,-15.628]],[[139.459,-17.115,139.422,-17.132,139.408,-17.091,139.459,-17.049,139.493,-16.99,139.56,-17.042,139.571,-17.094,139.459,-17.115]],[[136.237,-13.825,136.214,-13.836,136.123,-13.817,136.122,-13.781,136.134,-13.753,136.16,-13.737,136.215,-13.665,136.257,-13.707,136.275,-13.791,136.237,-13.825]],[[132.593,-11.303,132.574,-11.318,132.494,-11.164,132.516,-11.116,132.484,-11.037,132.538,-11.028,132.579,-10.969,132.593,-10.998,132.597,-11.106,132.629,-11.169,132.593,-11.303]],[[125.199,-14.579,125.135,-14.642,125.091,-14.592,125.117,-14.492,125.16,-14.456,125.198,-14.475,125.194,-14.553,125.199,-14.579]],[[124.597,-15.402,124.56,-15.43,124.524,-15.421,124.524,-15.382,124.483,-15.34,124.504,-15.292,124.519,-15.267,124.551,-15.27,124.565,-15.311,124.605,-15.357,124.597,-15.402]],[[115.446,-20.788,115.388,-20.866,115.318,-20.851,115.309,-20.811,115.354,-20.746,115.435,-20.668,115.458,-20.716,115.446,-20.788]],[[145.043,-40.787,145.159,-40.791,145.224,-40.765,145.283,-40.77,145.349,-40.826,145.429,-40.858,145.485,-40.852,145.533,-40.864,145.576,-40.904,145.686,-40.939,145.734,-40.962,145.775,-40.997,145.821,-41.025,146.111,-41.118,146.317,-41.163,146.574,-41.142,146.651,-41.116,146.723,-41.078,146.786,-41.114,146.848,-41.168,146.836,-41.109,146.857,-41.058,146.919,-41.018,146.99,-40.992,147.106,-40.994,147.219,-40.983,147.269,-40.96,147.321,-40.956,147.388,-40.986,147.455,-41.002,147.501,-40.964,147.579,-40.876,147.622,-40.845,147.818,-40.872,147.873,-40.873,147.969,-40.78,148.033,-40.781,148.215,-40.855,148.293,-40.947,148.285,-41.115,148.292,-41.175,148.306,-41.233,148.312,-41.35,148.29,-41.465,148.287,-41.555,148.297,-41.646,148.288,-41.816,148.316,-41.928,148.302,-42.004,148.301,-42.04,148.328,-42.074,148.341,-42.111,148.331,-42.159,148.343,-42.215,148.331,-42.262,148.29,-42.255,148.277,-42.219,148.285,-42.173,148.277,-42.136,148.256,-42.103,148.183,-42.065,148.204,-42.042,148.242,-42.022,148.214,-41.97,148.167,-42.012,148.141,-42.07,148.156,-42.088,148.128,-42.104,148.067,-42.17,148.023,-42.259,148.005,-42.345,148.009,-42.436,147.974,-42.506,147.924,-42.572,147.912,-42.658,147.915,-42.816,147.958,-42.96,147.981,-43.157,147.945,-43.182,147.839,-43.195,147.786,-43.22,147.699,-43.123,147.648,-43.021,147.687,-42.98,147.774,-43.003,147.8,-42.98,147.807,-42.954,147.8,-42.928,147.693,-42.872,147.574,-42.846,147.536,-42.878,147.549,-42.975,147.537,-42.996,147.452,-43.033,147.408,-42.894,147.298,-42.791,147.302,-42.841,147.348,-42.927,147.343,-42.964,147.325,-43.013,147.281,-43.032,147.26,-43.071,147.26,-43.126,147.245,-43.216,147.173,-43.256,146.997,-43.156,146.985,-43.19,146.988,-43.219,147.077,-43.276,147.036,-43.319,147.005,-43.37,146.955,-43.502,146.874,-43.612,146.834,-43.619,146.699,-43.602,146.549,-43.509,146.413,-43.52,146.187,-43.513,146.043,-43.547,146.013,-43.445,145.982,-43.408,145.994,-43.376,146.109,-43.354,146.226,-43.355,146.208,-43.316,146.176,-43.302,146.125,-43.311,145.975,-43.277,145.873,-43.292,145.803,-43.244,145.682,-43.076,145.61,-42.998,145.567,-42.968,145.518,-42.951,145.488,-42.927,145.268,-42.544,145.237,-42.456,145.199,-42.231,145.373,-42.338,145.435,-42.407,145.468,-42.493,145.527,-42.388,145.517,-42.354,145.36,-42.228,145.34,-42.191,145.331,-42.147,145.294,-42.191,145.235,-42.197,145.259,-42.107,145.238,-42.02,145.055,-41.827,144.916,-41.644,144.778,-41.419,144.766,-41.39,144.764,-41.342,144.698,-41.191,144.662,-41.079,144.646,-40.981,144.71,-40.783,144.719,-40.672,144.819,-40.722,145.043,-40.787]],[[143.928,-40.116,143.899,-40.12,143.876,-40.064,143.888,-39.984,143.839,-39.904,143.865,-39.824,143.862,-39.738,143.879,-39.7,143.939,-39.658,143.949,-39.584,144.001,-39.58,144.091,-39.638,144.121,-39.785,144.106,-39.874,144.141,-39.954,144.112,-40.022,144.035,-40.078,143.928,-40.116]],[[148,-39.758,148.178,-39.938,148.27,-39.967,148.297,-39.986,148.29,-40.065,148.251,-40.1,148.323,-40.144,148.314,-40.174,148.299,-40.172,148.21,-40.234,148.106,-40.262,148.074,-40.241,148.047,-40.213,148.025,-40.172,147.891,-40.015,147.906,-39.971,147.876,-39.905,147.812,-39.91,147.767,-39.87,147.839,-39.832,147.933,-39.726,148,-39.758]],[[147.356,-43.397,147.309,-43.501,147.231,-43.483,147.154,-43.5,147.105,-43.431,147.105,-43.413,147.163,-43.43,147.185,-43.408,147.198,-43.379,147.22,-43.371,147.234,-43.33,147.284,-43.279,147.313,-43.28,147.342,-43.346,147.356,-43.397]],[[148.104,-42.71,148.048,-42.719,148.03,-42.715,148.031,-42.663,148.023,-42.64,148.073,-42.593,148.143,-42.616,148.17,-42.652,148.101,-42.681,148.104,-42.71]],[[147.435,-43.241,147.372,-43.241,147.349,-43.232,147.338,-43.183,147.296,-43.162,147.319,-43.145,147.327,-43.115,147.353,-43.08,147.397,-43.118,147.435,-43.241]],[[144.784,-40.507,144.748,-40.589,144.71,-40.485,144.751,-40.47,144.783,-40.435,144.791,-40.44,144.784,-40.507]],[[148.326,-40.307,148.421,-40.367,148.474,-40.432,148.404,-40.487,148.353,-40.497,148.319,-40.435,148.214,-40.458,148.103,-40.452,148.02,-40.404,148.01,-40.381,148.059,-40.357,148.198,-40.358,148.326,-40.307]],[[148.237,-40.515,148.188,-40.593,148.127,-40.544,148.117,-40.521,148.193,-40.503,148.218,-40.505,148.237,-40.515]],[[158.879,-54.71,158.845,-54.749,158.836,-54.704,158.897,-54.506,158.959,-54.472,158.946,-54.575,158.879,-54.71]]]},{"n":"Australian Indian Ocean Territories","c":"AU","r":5,"ml":5,"lx":105.67,"ly":-10.49,"b":[96.826,-12.2,105.725,-10.431],"p":[[[105.725,-10.493,105.697,-10.564,105.644,-10.525,105.584,-10.512,105.596,-10.46,105.646,-10.452,105.67,-10.449,105.705,-10.431,105.725,-10.493]],[[96.84,-12.182,96.852,-12.187,96.867,-12.181,96.874,-12.188,96.85,-12.197,96.835,-12.18,96.828,-12.151,96.826,-12.126,96.833,-12.126,96.833,-12.136,96.834,-12.144,96.839,-12.16,96.836,-12.171,96.84,-12.182]],[[96.918,-12.194,96.907,-12.2,96.897,-12.196,96.894,-12.193,96.893,-12.187,96.904,-12.187,96.913,-12.182,96.919,-12.173,96.921,-12.162,96.925,-12.173,96.924,-12.185,96.918,-12.194]]]},{"n":"Heard Island and McDonald Islands","c":"HM","r":5,"ml":4.5,"lx":73.51,"ly":-53.1,"b":[73.251,-53.185,73.838,-52.966],"p":[[[73.707,-53.137,73.588,-53.185,73.465,-53.184,73.413,-53.147,73.336,-53.03,73.285,-53.021,73.254,-52.989,73.251,-52.976,73.305,-52.966,73.388,-53,73.586,-53.027,73.731,-53.091,73.838,-53.113,73.795,-53.13,73.707,-53.137]]]},{"n":"Norfolk Island","c":"NF","r":5,"ml":4.5,"lx":167.95,"ly":-29.03,"b":[167.906,-29.096,167.99,-29.014],"p":[[[167.939,-29.018,167.96,-29.028,167.978,-29.034,167.99,-29.042,167.989,-29.059,167.979,-29.076,167.968,-29.083,167.964,-29.085,167.962,-29.088,167.961,-29.092,167.961,-29.096,167.955,-29.082,167.944,-29.073,167.934,-29.072,167.927,-29.083,167.92,-29.083,167.918,-29.072,167.914,-29.062,167.912,-29.053,167.916,-29.045,167.924,-29.036,167.925,-29.029,167.919,-29.025,167.906,-29.028,167.921,-29.014,167.939,-29.018]]]},{"n":"Ashmore and Cartier Islands","c":"AU","r":5,"ml":4.5,"lx":123.59,"ly":-12.43,"b":[123.572,-12.436,123.595,-12.424],"p":[[[123.595,-12.426,123.595,-12.436,123.573,-12.434,123.572,-12.424,123.595,-12.426]]]},{"n":"Armenia","c":"AM","r":6,"ml":5,"lx":44.8,"ly":40.46,"b":[43.439,38.869,46.585,41.291],"p":[[[44.768,39.704,44.734,39.746,44.56,39.888,44.4,39.996,44.289,40.04,44.178,40.036,44.005,40.014,43.942,40.023,43.792,40.07,43.666,40.126,43.683,40.15,43.71,40.167,43.678,40.239,43.608,40.357,43.616,40.393,43.594,40.444,43.569,40.482,43.668,40.574,43.713,40.648,43.723,40.72,43.696,40.794,43.632,40.929,43.592,40.968,43.517,41.005,43.455,41.065,43.439,41.107,43.492,41.116,43.645,41.117,43.793,41.131,43.909,41.159,44.077,41.183,44.146,41.203,44.227,41.213,44.473,41.191,44.565,41.208,44.841,41.211,44.849,41.22,44.811,41.249,44.811,41.259,44.976,41.277,45.001,41.291,45.023,41.246,45.085,41.195,45.152,41.175,45.189,41.147,45.19,41.126,45.071,41.101,45.063,41.088,45.071,41.076,45.106,41.069,45.273,41.006,45.369,41.005,45.419,40.986,45.444,40.948,45.524,40.897,45.588,40.847,45.591,40.83,45.579,40.804,45.401,40.707,45.379,40.674,45.376,40.638,45.454,40.532,45.57,40.417,45.736,40.329,45.965,40.234,45.968,40.175,45.931,40.105,45.9,40.057,45.886,40.025,45.858,40.011,45.63,40.014,45.596,40.003,45.581,39.989,45.58,39.978,45.662,39.956,45.79,39.881,45.863,39.808,45.94,39.777,46.026,39.719,46.095,39.664,46.202,39.594,46.322,39.617,46.481,39.555,46.488,39.513,46.478,39.475,46.378,39.434,46.365,39.417,46.365,39.402,46.378,39.382,46.437,39.349,46.507,39.299,46.585,39.224,46.55,39.201,46.477,39.198,46.42,39.207,46.4,39.192,46.401,39.168,46.475,39.111,46.49,39.069,46.487,38.997,46.491,38.907,46.318,38.913,46.17,38.869,46.114,38.878,46.077,38.955,46.046,39.018,45.952,39.178,45.977,39.244,45.925,39.282,45.799,39.35,45.766,39.378,45.784,39.417,45.796,39.488,45.784,39.546,45.75,39.563,45.687,39.564,45.611,39.55,45.457,39.494,45.35,39.53,45.288,39.566,45.253,39.595,45.173,39.571,45.153,39.583,45.149,39.657,45.125,39.696,45.076,39.743,45.032,39.765,44.867,39.719,44.768,39.704],[45.024,41.027,45.029,41.054,45.021,41.078,44.994,41.086,44.961,41.079,44.959,41.053,44.969,41.027,45.002,41.016,45.024,41.027]],[[45.552,40.616,45.514,40.6,45.479,40.607,45.479,40.648,45.504,40.665,45.534,40.664,45.562,40.649,45.552,40.616]]]},{"n":"Argentina","c":"AR","r":2,"ml":2,"lx":-64.17,"ly":-33.5,"b":[-73.576,-55.032,-53.669,-21.803],"p":[[[-57.609,-30.188,-57.646,-30.227,-57.651,-30.295,-57.713,-30.384,-57.831,-30.495,-57.873,-30.591,-57.819,-30.712,-57.811,-30.859,-57.834,-30.917,-57.886,-30.937,-57.898,-30.975,-57.87,-31.031,-57.868,-31.104,-57.893,-31.195,-57.948,-31.299,-58.033,-31.417,-58.054,-31.495,-58.01,-31.534,-57.988,-31.576,-57.989,-31.621,-58.007,-31.685,-58.042,-31.769,-58.096,-31.832,-58.167,-31.873,-58.189,-31.924,-58.16,-31.987,-58.156,-32.052,-58.177,-32.119,-58.165,-32.185,-58.12,-32.249,-58.123,-32.322,-58.201,-32.472,-58.22,-32.564,-58.171,-32.959,-58.201,-33.015,-58.25,-33.078,-58.309,-33.083,-58.376,-33.072,-58.424,-33.112,-58.455,-33.286,-58.547,-33.663,-58.531,-33.753,-58.457,-33.898,-58.429,-33.991,-58.409,-34.061,-58.392,-34.193,-58.435,-34.253,-58.475,-34.263,-58.525,-34.296,-58.466,-34.457,-58.419,-34.532,-58.283,-34.683,-57.764,-34.895,-57.548,-35.019,-57.304,-35.188,-57.171,-35.362,-57.159,-35.506,-57.354,-35.72,-57.375,-35.9,-57.335,-36.027,-57.265,-36.144,-57.076,-36.297,-56.937,-36.353,-56.749,-36.346,-56.717,-36.389,-56.698,-36.426,-56.668,-36.735,-56.672,-36.851,-56.727,-36.958,-57.088,-37.446,-57.396,-37.745,-57.507,-37.909,-57.547,-38.086,-57.646,-38.17,-58.179,-38.436,-59.007,-38.673,-59.676,-38.797,-59.828,-38.838,-60.904,-38.974,-61.112,-38.993,-61.383,-38.981,-61.603,-38.999,-61.848,-38.962,-62.067,-38.919,-62.189,-38.813,-62.335,-38.8,-62.374,-38.853,-62.304,-38.988,-62.338,-39.151,-62.295,-39.243,-62.209,-39.262,-62.126,-39.31,-62.054,-39.374,-62.179,-39.38,-62.131,-39.432,-62.077,-39.462,-62.083,-39.568,-62.132,-39.825,-62.254,-39.88,-62.287,-39.895,-62.324,-39.951,-62.402,-40.197,-62.427,-40.356,-62.394,-40.459,-62.246,-40.675,-62.302,-40.815,-62.395,-40.891,-62.798,-41.047,-62.959,-41.11,-63.213,-41.152,-63.622,-41.16,-63.773,-41.15,-64.123,-41.008,-64.383,-40.922,-64.621,-40.854,-64.853,-40.814,-64.82,-40.793,-64.804,-40.757,-64.869,-40.736,-64.917,-40.731,-65.069,-40.805,-65.133,-40.881,-65.152,-40.947,-65.155,-41.106,-65.128,-41.239,-65.018,-41.567,-65.007,-41.745,-65.059,-41.97,-64.986,-42.102,-64.898,-42.162,-64.7,-42.221,-64.622,-42.261,-64.538,-42.255,-64.512,-42.27,-64.524,-42.299,-64.574,-42.356,-64.571,-42.416,-64.42,-42.434,-64.265,-42.422,-64.101,-42.395,-64.062,-42.353,-64.061,-42.266,-64.253,-42.251,-64.229,-42.218,-64.083,-42.183,-63.893,-42.125,-63.796,-42.114,-63.729,-42.153,-63.685,-42.189,-63.63,-42.283,-63.596,-42.407,-63.594,-42.556,-63.617,-42.696,-63.644,-42.746,-63.692,-42.805,-64.035,-42.881,-64.131,-42.861,-64.22,-42.756,-64.248,-42.646,-64.324,-42.572,-64.488,-42.513,-64.65,-42.531,-64.812,-42.633,-64.971,-42.666,-65.027,-42.759,-64.629,-42.909,-64.442,-42.951,-64.38,-42.949,-64.319,-42.969,-64.376,-43.025,-64.432,-43.059,-64.715,-43.136,-64.84,-43.189,-64.986,-43.294,-65.19,-43.522,-65.252,-43.572,-65.284,-43.63,-65.305,-43.787,-65.239,-44.049,-65.308,-44.158,-65.266,-44.28,-65.29,-44.361,-65.361,-44.477,-65.648,-44.661,-65.698,-44.796,-65.599,-44.876,-65.606,-44.945,-65.639,-45.008,-65.758,-45.007,-66.19,-44.965,-66.348,-45.034,-66.494,-45.118,-66.533,-45.158,-66.585,-45.183,-66.882,-45.228,-66.941,-45.257,-67.258,-45.577,-67.393,-45.776,-67.557,-45.97,-67.6,-46.053,-67.609,-46.167,-67.586,-46.27,-67.563,-46.345,-67.506,-46.443,-67.387,-46.554,-66.777,-47.006,-66.65,-47.045,-65.999,-47.094,-65.854,-47.157,-65.769,-47.257,-65.738,-47.345,-65.775,-47.568,-65.814,-47.638,-65.886,-47.702,-66.041,-47.783,-66.225,-47.827,-66.172,-47.858,-66.097,-47.853,-65.934,-47.827,-65.864,-47.853,-65.81,-47.941,-65.912,-47.977,-65.943,-48.019,-66.017,-48.084,-66.393,-48.342,-66.596,-48.42,-66.783,-48.523,-67.033,-48.628,-67.131,-48.688,-67.263,-48.814,-67.466,-48.952,-67.685,-49.247,-67.694,-49.304,-67.662,-49.342,-67.783,-49.859,-67.826,-49.92,-67.914,-49.984,-68.146,-50.091,-68.257,-50.105,-68.405,-50.043,-68.488,-49.978,-68.569,-49.867,-68.668,-49.753,-68.673,-49.793,-68.638,-49.863,-68.662,-49.936,-68.913,-49.969,-68.98,-50.003,-68.753,-49.988,-68.598,-50.009,-68.533,-50.036,-68.474,-50.091,-68.422,-50.158,-68.465,-50.195,-68.589,-50.225,-68.75,-50.281,-68.939,-50.382,-69.045,-50.499,-69.09,-50.583,-69.141,-50.753,-69.155,-50.864,-69.235,-50.951,-69.359,-51.028,-69.352,-51.046,-69.268,-51.006,-69.201,-50.994,-69.144,-51.097,-69.066,-51.304,-69.03,-51.446,-69.035,-51.489,-69.058,-51.547,-69.218,-51.561,-69.361,-51.559,-69.465,-51.584,-69.409,-51.61,-69.313,-51.601,-69.18,-51.662,-69.033,-51.636,-68.965,-51.677,-68.917,-51.715,-68.691,-52.013,-68.494,-52.198,-68.394,-52.307,-68.443,-52.357,-68.461,-52.29,-68.59,-52.273,-68.715,-52.255,-68.925,-52.208,-69.206,-52.136,-69.488,-52.136,-69.713,-52.075,-69.96,-52.008,-70.483,-52.002,-70.943,-51.998,-71.415,-51.994,-71.717,-51.991,-71.919,-51.99,-71.971,-51.964,-71.953,-51.88,-72.028,-51.819,-72.137,-51.744,-72.269,-51.691,-72.335,-51.62,-72.408,-51.541,-72.366,-51.47,-72.303,-51.299,-72.302,-51.223,-72.359,-51.17,-72.377,-51.095,-72.359,-51.06,-72.307,-51.033,-72.276,-50.91,-72.301,-50.79,-72.34,-50.682,-72.393,-50.634,-72.46,-50.612,-72.51,-50.608,-72.62,-50.648,-72.804,-50.638,-72.866,-50.653,-72.956,-50.696,-73.082,-50.76,-73.153,-50.738,-73.175,-50.67,-73.222,-50.611,-73.252,-50.558,-73.274,-50.473,-73.312,-50.362,-73.387,-50.231,-73.501,-50.125,-73.508,-50.03,-73.529,-49.911,-73.47,-49.795,-73.505,-49.698,-73.576,-49.583,-73.554,-49.464,-73.484,-49.398,-73.462,-49.314,-73.135,-49.301,-73.149,-49.188,-73.095,-49.097,-73.034,-49.014,-72.982,-48.977,-72.865,-48.944,-72.728,-48.896,-72.651,-48.842,-72.614,-48.793,-72.592,-48.73,-72.586,-48.662,-72.608,-48.519,-72.583,-48.475,-72.498,-48.417,-72.355,-48.366,-72.293,-48.229,-72.328,-48.11,-72.408,-48.016,-72.509,-47.973,-72.518,-47.876,-72.472,-47.784,-72.413,-47.686,-72.342,-47.572,-72.346,-47.493,-72.283,-47.446,-72.103,-47.343,-72.042,-47.241,-71.979,-47.214,-71.905,-47.202,-71.901,-47.144,-71.954,-47.087,-71.963,-47.016,-71.957,-46.937,-71.94,-46.831,-71.856,-46.792,-71.733,-46.706,-71.7,-46.651,-71.695,-46.578,-71.731,-46.428,-71.762,-46.32,-71.778,-46.28,-71.834,-46.207,-71.876,-46.161,-71.809,-46.103,-71.684,-46.042,-71.632,-45.954,-71.68,-45.879,-71.751,-45.839,-71.773,-45.724,-71.746,-45.579,-71.693,-45.535,-71.508,-45.513,-71.49,-45.438,-71.349,-45.332,-71.354,-45.23,-71.443,-45.168,-71.531,-45.068,-71.596,-44.979,-71.812,-44.931,-72.042,-44.904,-72.073,-44.82,-72.064,-44.772,-71.957,-44.792,-71.783,-44.774,-71.652,-44.77,-71.56,-44.762,-71.455,-44.75,-71.358,-44.785,-71.261,-44.763,-71.221,-44.631,-71.16,-44.56,-71.151,-44.494,-71.213,-44.441,-71.326,-44.425,-71.82,-44.383,-71.835,-44.33,-71.831,-44.241,-71.812,-44.151,-71.812,-44.106,-71.767,-44.067,-71.716,-43.984,-71.68,-43.93,-71.716,-43.858,-71.795,-43.753,-71.737,-43.705,-71.733,-43.647,-71.751,-43.59,-71.832,-43.527,-71.905,-43.44,-71.905,-43.348,-71.82,-43.323,-71.764,-43.295,-71.751,-43.237,-71.781,-43.167,-71.899,-43.145,-72.055,-43.102,-72.102,-43.066,-72.146,-42.99,-72.114,-42.777,-72.13,-42.648,-72.144,-42.577,-72.105,-42.522,-72.053,-42.473,-72.078,-42.358,-72.125,-42.298,-72.108,-42.252,-72.064,-42.205,-72.026,-42.148,-71.993,-42.134,-71.944,-42.167,-71.861,-42.148,-71.761,-42.101,-71.75,-42.047,-71.77,-41.969,-71.844,-41.772,-71.911,-41.65,-71.898,-41.607,-71.871,-41.561,-71.892,-41.393,-71.886,-41.292,-71.881,-40.995,-71.873,-40.893,-71.941,-40.789,-71.932,-40.692,-71.884,-40.621,-71.839,-40.524,-71.805,-40.439,-71.769,-40.401,-71.709,-40.382,-71.695,-40.335,-71.723,-40.3,-71.801,-40.244,-71.818,-40.177,-71.802,-40.125,-71.764,-40.095,-71.704,-40.095,-71.66,-40.021,-71.647,-39.929,-71.638,-39.887,-71.672,-39.833,-71.697,-39.707,-71.72,-39.635,-71.693,-39.605,-71.654,-39.594,-71.587,-39.611,-71.539,-39.602,-71.531,-39.564,-71.526,-39.523,-71.508,-39.495,-71.465,-39.402,-71.42,-39.287,-71.409,-39.206,-71.426,-38.986,-71.402,-38.935,-71.353,-38.889,-71.286,-38.845,-71.197,-38.809,-71.087,-38.758,-70.952,-38.738,-70.897,-38.681,-70.859,-38.604,-70.848,-38.542,-70.9,-38.498,-70.968,-38.446,-71,-38.315,-71.018,-38.194,-71.028,-38.041,-71.096,-37.91,-71.168,-37.762,-71.187,-37.631,-71.163,-37.559,-71.135,-37.445,-71.165,-37.393,-71.2,-37.3,-71.163,-37.227,-71.118,-37.114,-71.124,-37.057,-71.159,-36.92,-71.192,-36.844,-71.159,-36.762,-71.107,-36.685,-71.066,-36.644,-71.073,-36.578,-71.056,-36.524,-70.978,-36.487,-70.905,-36.42,-70.853,-36.412,-70.79,-36.412,-70.749,-36.393,-70.733,-36.341,-70.722,-36.283,-70.622,-36.212,-70.563,-36.146,-70.457,-36.133,-70.405,-36.062,-70.404,-35.971,-70.416,-35.879,-70.38,-35.772,-70.42,-35.609,-70.416,-35.523,-70.457,-35.452,-70.449,-35.375,-70.47,-35.326,-70.532,-35.308,-70.555,-35.247,-70.525,-35.217,-70.467,-35.194,-70.393,-35.147,-70.338,-34.922,-70.312,-34.855,-70.287,-34.775,-70.29,-34.733,-70.255,-34.673,-70.211,-34.581,-70.141,-34.493,-70.101,-34.432,-70.063,-34.35,-70.052,-34.301,-70.003,-34.276,-69.946,-34.27,-69.88,-34.254,-69.852,-34.224,-69.857,-34.18,-69.862,-34.084,-69.881,-33.93,-69.894,-33.731,-69.883,-33.601,-69.839,-33.47,-69.798,-33.399,-69.809,-33.344,-69.82,-33.284,-69.896,-33.251,-69.969,-33.279,-70.02,-33.271,-70.085,-33.202,-70.104,-33.128,-70.093,-33.027,-70.042,-32.964,-70.022,-32.885,-70.052,-32.86,-70.116,-32.807,-70.177,-32.626,-70.17,-32.472,-70.23,-32.431,-70.258,-32.31,-70.32,-32.267,-70.345,-32.176,-70.364,-32.083,-70.356,-32.042,-70.291,-32.031,-70.254,-31.958,-70.282,-31.917,-70.331,-31.881,-70.394,-31.884,-70.45,-31.842,-70.526,-31.666,-70.585,-31.569,-70.566,-31.428,-70.555,-31.317,-70.529,-31.223,-70.52,-31.148,-70.473,-31.113,-70.429,-31.129,-70.388,-31.121,-70.351,-31.06,-70.309,-31.023,-70.312,-30.993,-70.336,-30.96,-70.348,-30.902,-70.319,-30.834,-70.269,-30.677,-70.194,-30.505,-70.161,-30.44,-70.17,-30.386,-70.153,-30.361,-70.102,-30.388,-69.956,-30.358,-69.907,-30.282,-69.888,-30.213,-69.844,-30.175,-69.863,-30.12,-69.924,-30.104,-69.96,-30.078,-69.945,-30.016,-69.924,-29.874,-69.928,-29.769,-69.983,-29.545,-70.027,-29.324,-69.996,-29.25,-69.9,-29.149,-69.828,-29.103,-69.815,-29.046,-69.743,-28.784,-69.735,-28.641,-69.688,-28.562,-69.657,-28.414,-69.527,-28.286,-69.489,-28.201,-69.437,-28.193,-69.41,-28.165,-69.341,-28.071,-69.251,-27.974,-69.174,-27.925,-69.155,-27.848,-69.119,-27.744,-69.042,-27.57,-68.999,-27.449,-68.942,-27.405,-68.875,-27.247,-68.846,-27.154,-68.77,-27.115,-68.71,-27.104,-68.652,-27.148,-68.592,-27.14,-68.537,-27.085,-68.405,-27.048,-68.346,-27.028,-68.319,-26.973,-68.319,-26.878,-68.373,-26.806,-68.485,-26.67,-68.581,-26.518,-68.592,-26.47,-68.592,-26.418,-68.576,-26.352,-68.53,-26.277,-68.415,-26.154,-68.427,-26.065,-68.511,-25.741,-68.542,-25.652,-68.6,-25.486,-68.592,-25.42,-68.541,-25.237,-68.496,-25.163,-68.431,-25.149,-68.395,-25.125,-68.384,-25.092,-68.428,-25.051,-68.447,-24.999,-68.466,-24.925,-68.527,-24.899,-68.562,-24.838,-68.562,-24.747,-68.507,-24.63,-68.447,-24.597,-68.423,-24.545,-68.358,-24.497,-68.3,-24.46,-68.25,-24.392,-68.047,-24.308,-67.886,-24.243,-67.572,-24.119,-67.356,-24.034,-67.336,-23.975,-67.319,-23.935,-67.219,-23.634,-67.09,-23.245,-67.009,-23.001,-67.195,-22.822,-67.162,-22.774,-67.055,-22.651,-67.034,-22.552,-66.991,-22.51,-66.8,-22.41,-66.767,-22.343,-66.751,-22.269,-66.712,-22.216,-66.639,-22.205,-66.507,-22.158,-66.365,-22.114,-66.322,-22.053,-66.282,-21.947,-66.248,-21.83,-66.22,-21.803,-66.175,-21.806,-66.099,-21.835,-66.059,-21.879,-65.86,-22.02,-65.771,-22.1,-65.686,-22.11,-65.519,-22.095,-65.485,-22.098,-65.058,-22.103,-64.993,-22.11,-64.843,-22.144,-64.759,-22.171,-64.7,-22.186,-64.606,-22.229,-64.524,-22.372,-64.478,-22.485,-64.446,-22.585,-64.374,-22.761,-64.325,-22.828,-64.308,-22.795,-64.266,-22.603,-64.209,-22.491,-64.132,-22.366,-63.976,-22.073,-63.922,-22.029,-63.861,-22.007,-63.819,-22.005,-63.776,-22.027,-63.717,-22.028,-63.675,-22.004,-63.267,-22.001,-62.843,-21.997,-62.834,-21.999,-62.815,-22.05,-62.745,-22.16,-62.665,-22.218,-62.651,-22.234,-62.626,-22.262,-62.626,-22.29,-62.542,-22.35,-62.373,-22.439,-62.214,-22.612,-62.067,-22.869,-61.928,-23.059,-61.799,-23.182,-61.679,-23.269,-61.571,-23.319,-61.513,-23.36,-61.506,-23.392,-61.404,-23.458,-61.208,-23.557,-61.085,-23.656,-61.033,-23.756,-60.84,-23.858,-60.505,-23.964,-60.262,-24.014,-60.11,-24.009,-59.892,-24.094,-59.609,-24.267,-59.435,-24.387,-59.373,-24.454,-59.187,-24.562,-58.724,-24.787,-58.52,-24.843,-58.423,-24.894,-58.365,-24.959,-58.309,-24.979,-58.253,-24.954,-58.136,-24.977,-57.96,-25.049,-57.822,-25.136,-57.644,-25.328,-57.587,-25.405,-57.563,-25.474,-57.572,-25.534,-57.626,-25.599,-57.725,-25.667,-57.755,-25.697,-57.757,-25.726,-57.782,-25.784,-57.865,-25.907,-57.886,-25.964,-57.891,-26.007,-57.943,-26.053,-58.082,-26.139,-58.111,-26.18,-58.118,-26.225,-58.136,-26.251,-58.155,-26.263,-58.181,-26.307,-58.203,-26.381,-58.205,-26.477,-58.188,-26.593,-58.191,-26.63,-58.222,-26.65,-58.239,-26.677,-58.246,-26.731,-58.272,-26.771,-58.318,-26.796,-58.335,-26.825,-58.323,-26.858,-58.356,-26.89,-58.436,-26.922,-58.485,-26.968,-58.503,-27.029,-58.548,-27.084,-58.619,-27.132,-58.642,-27.196,-58.605,-27.314,-58.168,-27.273,-57.812,-27.317,-57.391,-27.43,-57.112,-27.47,-56.974,-27.436,-56.872,-27.441,-56.805,-27.485,-56.716,-27.494,-56.603,-27.468,-56.511,-27.488,-56.437,-27.554,-56.371,-27.537,-56.311,-27.439,-56.242,-27.367,-56.164,-27.321,-56.067,-27.308,-55.951,-27.326,-55.859,-27.362,-55.79,-27.416,-55.715,-27.415,-55.633,-27.357,-55.594,-27.288,-55.597,-27.208,-55.565,-27.15,-55.497,-27.115,-55.451,-27.068,-55.427,-27.009,-55.346,-26.973,-55.208,-26.96,-55.136,-26.931,-55.13,-26.886,-55.089,-26.845,-55.014,-26.807,-54.962,-26.759,-54.934,-26.703,-54.889,-26.667,-54.825,-26.652,-54.755,-26.533,-54.678,-26.309,-54.632,-26.006,-54.616,-25.576,-54.538,-25.576,-54.502,-25.608,-54.444,-25.625,-54.383,-25.589,-54.332,-25.572,-54.25,-25.57,-54.206,-25.53,-54.155,-25.523,-54.119,-25.545,-54.085,-25.572,-54.012,-25.578,-53.955,-25.648,-53.891,-25.669,-53.864,-25.749,-53.823,-25.96,-53.747,-26.084,-53.671,-26.225,-53.669,-26.288,-53.711,-26.352,-53.718,-26.443,-53.745,-26.667,-53.753,-26.749,-53.727,-26.805,-53.717,-26.883,-53.758,-26.978,-53.838,-27.121,-53.916,-27.16,-53.935,-27.161,-54.04,-27.244,-54.114,-27.275,-54.156,-27.254,-54.205,-27.29,-54.26,-27.382,-54.327,-27.424,-54.448,-27.446,-54.484,-27.457,-54.555,-27.454,-54.615,-27.477,-54.666,-27.527,-54.72,-27.545,-54.777,-27.533,-54.829,-27.551,-54.876,-27.599,-54.903,-27.652,-54.91,-27.709,-54.956,-27.747,-55.04,-27.768,-55.069,-27.796,-55.064,-27.836,-55.102,-27.867,-55.244,-27.899,-55.346,-27.956,-55.41,-28.038,-55.477,-28.089,-55.582,-28.121,-55.725,-28.204,-55.746,-28.255,-55.692,-28.303,-55.672,-28.345,-55.687,-28.382,-55.732,-28.387,-55.806,-28.36,-55.859,-28.354,-55.891,-28.37,-55.905,-28.4,-55.904,-28.443,-55.93,-28.473,-55.985,-28.489,-56.02,-28.525,-56.034,-28.581,-56.103,-28.652,-56.226,-28.737,-56.322,-28.852,-56.393,-28.997,-56.476,-29.092,-56.571,-29.138,-56.636,-29.203,-56.672,-29.287,-56.772,-29.418,-56.939,-29.595,-57.089,-29.716,-57.225,-29.782,-57.301,-29.857,-57.317,-29.939,-57.405,-30.034,-57.564,-30.14,-57.609,-30.188]],[[-68.653,-54.854,-68.648,-54.628,-68.64,-54.324,-68.638,-54.053,-68.637,-53.789,-68.635,-53.515,-68.633,-53.242,-68.632,-52.95,-68.63,-52.653,-68.571,-52.695,-68.339,-52.9,-68.278,-52.984,-68.24,-53.082,-68.333,-53.02,-68.431,-53.055,-68.479,-53.114,-68.521,-53.177,-68.521,-53.222,-68.489,-53.261,-68.393,-53.295,-68.161,-53.306,-68.144,-53.319,-68.008,-53.564,-67.94,-53.619,-67.861,-53.662,-67.678,-53.787,-67.503,-53.922,-67.294,-54.05,-67.069,-54.148,-66.865,-54.223,-66.67,-54.314,-66.462,-54.441,-66.236,-54.533,-65.993,-54.599,-65.747,-54.653,-65.369,-54.632,-65.252,-54.638,-65.179,-54.678,-65.252,-54.789,-65.346,-54.878,-65.471,-54.915,-65.603,-54.928,-65.723,-54.926,-65.842,-54.91,-65.954,-54.919,-66.061,-54.957,-66.172,-54.975,-66.287,-54.978,-66.399,-55.009,-66.511,-55.032,-66.628,-55.013,-66.93,-54.925,-67.127,-54.904,-67.793,-54.869,-68.007,-54.848,-68.22,-54.818,-68.332,-54.816,-68.491,-54.836,-68.619,-54.834,-68.653,-54.854]],[[-64.549,-54.716,-64.439,-54.739,-64.221,-54.722,-64.105,-54.722,-64.055,-54.73,-64.032,-54.742,-63.882,-54.723,-63.815,-54.725,-63.833,-54.768,-63.971,-54.811,-64.028,-54.793,-64.323,-54.796,-64.453,-54.84,-64.509,-54.84,-64.637,-54.903,-64.731,-54.863,-64.757,-54.827,-64.689,-54.775,-64.625,-54.774,-64.581,-54.753,-64.549,-54.716]],[[-61.876,-39.172,-61.866,-39.235,-61.918,-39.227,-62.042,-39.167,-62.083,-39.11,-62.093,-39.086,-61.967,-39.112,-61.907,-39.136,-61.876,-39.172]]]},{"n":"Antigua and Barbuda","c":"AG","r":6,"ml":5,"lx":-61.79,"ly":17.35,"b":[-61.887,16.997,-61.686,17.714],"p":[[[-61.716,17.037,-61.748,16.997,-61.86,17.013,-61.882,17.063,-61.887,17.098,-61.817,17.169,-61.739,17.138,-61.708,17.105,-61.686,17.098,-61.686,17.07,-61.695,17.049,-61.716,17.037]],[[-61.747,17.575,-61.762,17.549,-61.844,17.596,-61.869,17.685,-61.866,17.704,-61.852,17.714,-61.82,17.697,-61.777,17.69,-61.75,17.661,-61.747,17.575]]]},{"n":"Angola","c":"AO","r":3,"ml":3,"lx":17.98,"ly":-12.18,"b":[11.743,-18.02,24.047,-4.429],"p":[[[13.073,-4.635,13.057,-4.651,12.947,-4.695,12.83,-4.737,12.675,-4.905,12.596,-4.978,12.574,-4.997,12.503,-5.037,12.451,-5.071,12.453,-5.091,12.487,-5.113,12.522,-5.149,12.519,-5.425,12.504,-5.696,12.485,-5.719,12.386,-5.728,12.255,-5.746,12.214,-5.759,12.199,-5.732,12.155,-5.633,12.18,-5.539,12.207,-5.468,12.177,-5.325,12.111,-5.197,12.04,-5.035,12.018,-5.004,12.078,-4.952,12.167,-4.838,12.204,-4.779,12.308,-4.766,12.347,-4.724,12.374,-4.658,12.385,-4.619,12.501,-4.587,12.642,-4.531,12.719,-4.47,12.798,-4.431,12.848,-4.429,12.881,-4.445,12.971,-4.552,13.048,-4.619,13.073,-4.635]],[[23.967,-10.872,23.988,-11.003,24.01,-11.185,24.026,-11.316,24.041,-11.374,24.047,-11.405,24.029,-11.439,24.015,-11.518,23.987,-11.587,23.971,-11.636,23.984,-11.725,23.973,-11.853,23.962,-11.988,23.959,-12.118,23.996,-12.351,23.991,-12.422,23.945,-12.544,23.909,-12.636,23.887,-12.743,23.882,-12.799,23.968,-12.957,23.963,-12.988,23.897,-12.998,23.843,-13.001,23.636,-13.001,23.339,-13.001,23.042,-13.001,22.744,-13.001,22.471,-13.001,22.21,-13.001,21.979,-13.001,21.979,-13.157,21.979,-13.478,21.979,-13.799,21.979,-14.12,21.979,-14.441,21.979,-14.761,21.98,-15.082,21.98,-15.403,21.98,-15.724,21.98,-15.956,22.04,-16.263,22.151,-16.597,22.194,-16.628,22.305,-16.69,22.459,-16.815,22.546,-16.91,22.722,-17.075,22.956,-17.286,23.182,-17.474,23.381,-17.641,23.068,-17.699,22.624,-17.782,22.324,-17.837,21.961,-17.905,21.718,-17.948,21.417,-18.001,21.369,-18,21.288,-17.963,21.113,-17.956,20.908,-18.006,20.746,-18.02,20.625,-17.997,20.508,-17.953,20.393,-17.887,20.194,-17.864,19.912,-17.881,19.639,-17.869,19.377,-17.825,19.189,-17.808,19.076,-17.818,18.955,-17.804,18.826,-17.766,18.718,-17.703,18.588,-17.57,18.487,-17.443,18.46,-17.425,18.428,-17.405,18.396,-17.399,18.109,-17.396,17.835,-17.393,17.679,-17.393,17.296,-17.392,16.914,-17.391,16.531,-17.391,16.148,-17.39,15.766,-17.39,15.383,-17.389,15.001,-17.389,14.618,-17.388,14.415,-17.388,14.226,-17.398,14.017,-17.409,13.987,-17.404,13.938,-17.389,13.904,-17.361,13.792,-17.288,13.694,-17.233,13.562,-17.141,13.476,-17.04,13.404,-17.008,13.276,-16.99,13.179,-16.972,13.101,-16.968,12.963,-17.015,12.859,-17.063,12.785,-17.108,12.657,-17.161,12.548,-17.213,12.359,-17.206,12.318,-17.213,12.213,-17.21,12.114,-17.165,12.014,-17.169,11.903,-17.227,11.743,-17.249,11.78,-16.871,11.819,-16.704,11.82,-16.504,11.797,-15.986,11.769,-15.915,11.751,-15.832,11.85,-15.768,11.9,-15.72,11.968,-15.634,12.016,-15.514,12.073,-15.248,12.28,-14.637,12.379,-14.039,12.504,-13.755,12.55,-13.438,12.898,-13.028,12.983,-12.776,13.163,-12.652,13.417,-12.52,13.598,-12.286,13.686,-12.124,13.785,-11.813,13.784,-11.488,13.847,-11.054,13.834,-10.93,13.739,-10.757,13.721,-10.634,13.633,-10.512,13.539,-10.421,13.495,-10.257,13.332,-9.999,13.288,-9.827,13.209,-9.703,13.197,-9.551,13.156,-9.39,13.076,-9.23,12.999,-9.048,12.999,-8.991,13.047,-8.922,13.093,-8.9,13.077,-8.934,13.047,-8.975,13.054,-9.007,13.359,-8.687,13.378,-8.625,13.368,-8.555,13.366,-8.469,13.379,-8.37,13.091,-7.78,12.862,-7.232,12.823,-6.955,12.521,-6.59,12.402,-6.353,12.334,-6.187,12.283,-6.124,12.303,-6.093,12.38,-6.084,12.554,-6.046,12.791,-6.004,13.01,-5.908,13.068,-5.865,13.184,-5.856,13.303,-5.882,13.346,-5.863,13.371,-5.862,13.649,-5.862,13.765,-5.855,13.979,-5.857,14.114,-5.865,14.191,-5.876,14.399,-5.893,14.658,-5.889,14.749,-5.88,15.089,-5.875,15.425,-5.869,15.727,-5.864,16.06,-5.865,16.315,-5.866,16.431,-5.9,16.537,-5.966,16.585,-6.025,16.608,-6.052,16.64,-6.115,16.697,-6.164,16.718,-6.241,16.701,-6.346,16.709,-6.472,16.743,-6.618,16.813,-6.773,16.919,-6.934,16.966,-7.062,16.952,-7.157,16.985,-7.257,17.064,-7.363,17.122,-7.419,17.155,-7.461,17.245,-7.623,17.411,-7.882,17.536,-8.076,17.58,-8.099,17.643,-8.091,17.779,-8.071,17.913,-8.068,18.009,-8.108,18.047,-8.101,18.192,-8.024,18.335,-8,18.485,-7.969,18.563,-7.936,18.653,-7.936,18.898,-7.998,18.944,-8.001,19.143,-8.001,19.341,-7.967,19.37,-7.707,19.372,-7.655,19.419,-7.557,19.48,-7.472,19.487,-7.391,19.484,-7.279,19.528,-7.144,19.66,-7.037,19.875,-6.986,19.997,-6.976,20.19,-6.946,20.482,-6.916,20.59,-6.92,20.599,-6.935,20.537,-7.122,20.536,-7.183,20.558,-7.244,20.608,-7.278,20.911,-7.281,21.19,-7.285,21.511,-7.297,21.751,-7.305,21.782,-7.315,21.806,-7.329,21.842,-7.421,21.834,-7.602,21.78,-7.865,21.801,-8.112,21.896,-8.341,21.905,-8.693,21.872,-8.904,21.829,-9.168,21.813,-9.469,21.857,-9.594,21.949,-9.726,22.089,-9.863,22.198,-10.041,22.275,-10.259,22.302,-10.397,22.282,-10.453,22.283,-10.552,22.307,-10.691,22.28,-10.784,22.204,-10.829,22.178,-10.892,22.217,-11.013,22.226,-11.122,22.257,-11.164,22.279,-11.194,22.315,-11.199,22.393,-11.159,22.486,-11.087,22.561,-11.056,22.667,-11.06,22.815,-11.08,23.076,-11.088,23.157,-11.075,23.4,-10.976,23.464,-10.969,23.56,-10.979,23.696,-11.008,23.834,-11.014,23.901,-10.983,23.907,-10.943,23.929,-10.892,23.967,-10.872]]]},{"n":"Andorra","c":"AD","r":6,"ml":5,"lx":1.54,"ly":42.55,"b":[1.415,42.434,1.74,42.643],"p":[[[1.706,42.503,1.679,42.497,1.586,42.456,1.534,42.442,1.486,42.434,1.449,42.437,1.428,42.461,1.43,42.498,1.422,42.531,1.415,42.548,1.428,42.596,1.459,42.622,1.501,42.643,1.568,42.635,1.71,42.604,1.739,42.576,1.74,42.557,1.714,42.526,1.706,42.503]]]},{"n":"Algeria","c":"DZ","r":3,"ml":2.5,"lx":2.81,"ly":27.4,"b":[-8.683,18.987,11.968,37.092],"p":[[[8.577,36.937,8.598,36.884,8.601,36.834,8.507,36.788,8.444,36.761,8.37,36.633,8.231,36.545,8.208,36.519,8.209,36.495,8.303,36.456,8.334,36.418,8.349,36.368,8.307,36.189,8.28,36.051,8.246,35.871,8.247,35.802,8.283,35.719,8.318,35.655,8.329,35.582,8.316,35.403,8.36,35.3,8.394,35.204,8.312,35.085,8.277,34.979,8.255,34.829,8.246,34.734,8.193,34.646,8.123,34.564,8.046,34.513,7.949,34.469,7.838,34.41,7.749,34.254,7.554,34.125,7.514,34.081,7.496,33.977,7.5,33.832,7.534,33.718,7.628,33.549,7.709,33.362,7.731,33.269,7.763,33.233,7.877,33.172,8.076,33.089,8.113,33.055,8.211,32.927,8.304,32.696,8.333,32.544,8.515,32.422,8.683,32.31,8.844,32.212,9.019,32.105,9.044,32.072,9.102,31.846,9.16,31.621,9.224,31.374,9.288,31.125,9.363,30.833,9.406,30.667,9.458,30.465,9.519,30.229,9.421,30.179,9.31,30.115,9.391,29.994,9.546,29.796,9.64,29.636,9.673,29.567,9.746,29.369,9.805,29.177,9.821,29.115,9.843,28.967,9.816,28.56,9.858,28.043,9.916,27.786,9.825,27.553,9.748,27.331,9.753,27.219,9.795,27.045,9.837,26.916,9.894,26.848,9.883,26.631,9.859,26.552,9.685,26.438,9.491,26.334,9.438,26.246,9.422,26.147,9.448,26.067,9.581,25.89,9.781,25.624,10.001,25.332,10.019,25.259,10.028,25.051,10.12,24.79,10.219,24.676,10.256,24.591,10.326,24.53,10.396,24.486,10.439,24.48,10.686,24.551,11.108,24.434,11.508,24.314,11.537,24.291,11.624,24.14,11.767,23.893,11.873,23.695,11.968,23.518,11.45,23.213,10.932,22.907,10.414,22.602,9.896,22.297,9.379,21.991,8.861,21.686,8.343,21.381,7.825,21.076,7.482,20.873,7.263,20.694,6.989,20.471,6.731,20.248,6.527,20.073,6.263,19.846,6.131,19.732,5.837,19.479,5.748,19.434,5.359,19.36,5.001,19.291,4.671,19.228,4.446,19.185,4.228,19.143,3.91,19.084,3.683,19.042,3.439,18.996,3.401,18.988,3.356,18.987,3.323,18.988,3.256,19.013,3.174,19.073,3.12,19.103,3.106,19.15,3.138,19.212,3.177,19.268,3.192,19.312,3.22,19.345,3.254,19.373,3.256,19.411,3.227,19.474,3.202,19.56,3.203,19.718,3.203,19.771,3.204,19.79,3.13,19.85,2.992,19.917,2.866,19.956,2.808,19.969,2.668,19.993,2.474,20.035,2.406,20.064,2.281,20.21,2.219,20.248,1.929,20.273,1.832,20.297,1.753,20.332,1.685,20.378,1.647,20.459,1.636,20.524,1.611,20.556,1.29,20.714,1.209,20.767,1.166,20.817,1.164,20.891,1.173,20.982,1.159,21.063,1.146,21.102,0.999,21.198,0.672,21.412,0.344,21.626,0.017,21.84,-0.311,22.054,-0.638,22.268,-0.965,22.482,-1.293,22.697,-1.62,22.911,-1.948,23.125,-2.275,23.339,-2.603,23.553,-2.93,23.767,-3.258,23.981,-3.585,24.195,-3.913,24.409,-4.24,24.624,-4.517,24.804,-4.823,24.996,-5.05,25.135,-5.275,25.275,-5.517,25.424,-5.675,25.516,-5.863,25.627,-6.051,25.738,-6.239,25.848,-6.427,25.959,-6.615,26.069,-6.803,26.18,-6.991,26.291,-7.179,26.401,-7.367,26.512,-7.555,26.622,-7.743,26.733,-7.931,26.844,-8.119,26.954,-8.307,27.065,-8.495,27.175,-8.683,27.286,-8.683,27.49,-8.683,27.656,-8.683,27.9,-8.683,28.112,-8.683,28.324,-8.683,28.469,-8.683,28.621,-8.678,28.689,-8.66,28.719,-8.558,28.768,-8.399,28.88,-8.34,28.93,-8.265,28.981,-7.999,29.132,-7.944,29.175,-7.685,29.35,-7.625,29.375,-7.486,29.392,-7.428,29.425,-7.35,29.495,-7.235,29.575,-7.16,29.613,-7.142,29.62,-7.095,29.625,-6.856,29.602,-6.755,29.584,-6.635,29.569,-6.598,29.579,-6.566,29.604,-6.521,29.66,-6.511,29.726,-6.508,29.784,-6.501,29.809,-6.48,29.82,-6.428,29.816,-6.358,29.808,-6.215,29.811,-6.167,29.819,-6.004,29.831,-5.775,29.869,-5.593,29.918,-5.449,29.957,-5.294,30.059,-5.18,30.166,-5.062,30.326,-4.968,30.465,-4.779,30.552,-4.62,30.605,-4.529,30.626,-4.323,30.699,-4.149,30.81,-3.985,30.914,-3.86,30.927,-3.702,30.944,-3.667,30.964,-3.627,31.001,-3.625,31.066,-3.673,31.111,-3.73,31.135,-3.771,31.162,-3.812,31.167,-3.833,31.198,-3.821,31.255,-3.815,31.309,-3.789,31.362,-3.796,31.437,-3.837,31.512,-3.85,31.566,-3.847,31.62,-3.827,31.662,-3.768,31.69,-3.7,31.7,-3.605,31.687,-3.44,31.705,-3.017,31.834,-2.988,31.874,-2.961,31.964,-2.931,32.043,-2.887,32.069,-2.863,32.075,-2.723,32.096,-2.523,32.126,-2.448,32.13,-2.231,32.121,-2.073,32.115,-1.817,32.105,-1.635,32.1,-1.477,32.095,-1.275,32.089,-1.226,32.107,-1.226,32.165,-1.262,32.271,-1.24,32.338,-1.163,32.399,-1.066,32.468,-1.111,32.552,-1.188,32.608,-1.296,32.676,-1.352,32.703,-1.45,32.785,-1.51,32.878,-1.551,33.074,-1.625,33.183,-1.679,33.319,-1.631,33.567,-1.703,33.717,-1.714,33.782,-1.715,33.858,-1.693,33.99,-1.707,34.176,-1.792,34.368,-1.752,34.433,-1.733,34.467,-1.739,34.496,-1.817,34.557,-1.85,34.607,-1.832,34.655,-1.792,34.723,-1.796,34.752,-1.921,34.836,-2.132,34.971,-2.191,35.03,-2.22,35.104,-2.018,35.085,-1.913,35.094,-1.674,35.183,-1.484,35.303,-1.336,35.364,-1.205,35.496,-1.088,35.579,-0.917,35.668,-0.426,35.862,-0.351,35.863,-0.189,35.819,-0.048,35.833,0.048,35.901,0.152,36.063,0.312,36.162,0.515,36.262,0.791,36.357,0.972,36.444,1.257,36.52,1.975,36.568,2.343,36.61,2.593,36.601,2.846,36.739,2.973,36.784,3.521,36.795,3.779,36.896,4.758,36.896,4.878,36.862,4.995,36.808,5.196,36.677,5.295,36.648,5.425,36.675,5.725,36.8,6.065,36.864,6.249,36.938,6.328,37.046,6.487,37.086,6.576,37.003,6.928,36.919,7.143,36.943,7.238,36.969,7.204,37.092,7.432,37.059,7.608,37,7.792,36.88,7.91,36.856,8.127,36.91,8.577,36.937]]]},{"n":"Albania","c":"AL","r":6,"ml":5,"lx":20.11,"ly":40.65,"b":[19.281,39.654,21.031,42.648],"p":[[[19.342,41.869,19.346,41.919,19.361,41.998,19.352,42.024,19.361,42.069,19.331,42.129,19.281,42.173,19.329,42.249,19.4,42.342,19.465,42.415,19.545,42.492,19.597,42.565,19.654,42.629,19.703,42.648,19.728,42.635,19.741,42.607,19.738,42.525,19.754,42.497,19.788,42.476,19.86,42.486,19.939,42.507,20.046,42.55,20.064,42.547,20.104,42.525,20.186,42.426,20.241,42.339,20.348,42.309,20.408,42.275,20.485,42.223,20.523,42.171,20.575,42.013,20.581,41.917,20.566,41.874,20.553,41.862,20.505,41.706,20.517,41.627,20.516,41.575,20.476,41.554,20.449,41.521,20.492,41.391,20.487,41.336,20.489,41.273,20.568,41.128,20.614,41.083,20.656,41.062,20.709,40.928,20.741,40.905,20.87,40.918,20.933,40.903,20.959,40.872,20.964,40.85,20.956,40.775,20.988,40.718,21.031,40.659,21.031,40.622,21.002,40.563,20.95,40.494,20.882,40.468,20.806,40.445,20.77,40.392,20.752,40.335,20.718,40.293,20.697,40.246,20.665,40.152,20.657,40.117,20.606,40.083,20.527,40.069,20.456,40.066,20.408,40.049,20.384,40.017,20.338,39.991,20.311,39.979,20.311,39.951,20.344,39.891,20.382,39.842,20.382,39.803,20.364,39.792,20.306,39.797,20.294,39.782,20.288,39.739,20.272,39.701,20.248,39.678,20.207,39.654,20.131,39.662,20.06,39.699,20.023,39.711,20.001,39.709,19.996,39.801,19.965,39.872,19.852,40.044,19.485,40.21,19.398,40.285,19.36,40.348,19.322,40.407,19.359,40.409,19.395,40.394,19.441,40.376,19.459,40.405,19.439,40.47,19.345,40.622,19.338,40.664,19.384,40.791,19.461,40.933,19.456,41.106,19.48,41.236,19.453,41.321,19.441,41.425,19.497,41.563,19.546,41.597,19.576,41.64,19.578,41.788,19.468,41.856,19.342,41.869]]]},{"n":"Afghanistan","c":"AF","r":3,"ml":3,"lx":66.5,"ly":34.16,"b":[60.486,29.392,74.891,38.456],"p":[[[66.522,37.348,66.828,37.371,67.069,37.335,67.196,37.235,67.32,37.21,67.442,37.258,67.517,37.267,67.546,37.236,67.607,37.223,67.7,37.227,67.753,37.2,67.759,37.172,67.766,37.14,67.834,37.064,67.958,36.972,68.068,36.95,68.212,37.022,68.261,37.013,68.285,37.036,68.3,37.088,68.387,37.138,68.546,37.183,68.637,37.224,68.669,37.258,68.723,37.268,68.782,37.258,68.824,37.271,68.838,37.303,68.855,37.317,68.885,37.328,68.912,37.334,68.96,37.325,69.05,37.267,69.18,37.158,69.265,37.108,69.304,37.117,69.354,37.15,69.414,37.208,69.43,37.291,69.399,37.399,69.42,37.487,69.492,37.553,69.626,37.594,69.821,37.61,69.941,37.6,69.985,37.566,70.045,37.547,70.12,37.544,70.189,37.582,70.251,37.664,70.255,37.765,70.199,37.886,70.215,37.924,70.239,37.941,70.313,37.985,70.418,38.075,70.519,38.192,70.616,38.334,70.736,38.423,70.879,38.456,71.052,38.418,71.256,38.307,71.333,38.17,71.283,38.008,71.279,37.918,71.32,37.902,71.39,37.906,71.488,37.932,71.552,37.933,71.582,37.91,71.58,37.864,71.546,37.796,71.505,37.603,71.48,37.436,71.455,37.272,71.433,37.128,71.472,37.015,71.531,36.845,71.597,36.733,71.666,36.697,71.734,36.684,71.802,36.694,71.942,36.766,72.154,36.901,72.359,36.983,72.657,37.029,72.757,37.173,72.896,37.268,73.211,37.408,73.383,37.462,73.481,37.472,73.605,37.446,73.633,37.437,73.657,37.43,73.721,37.419,73.734,37.376,73.717,37.329,73.649,37.291,73.628,37.262,73.654,37.239,73.75,37.232,73.949,37.283,74.078,37.316,74.167,37.329,74.204,37.372,74.26,37.415,74.349,37.419,74.445,37.396,74.524,37.382,74.659,37.394,74.731,37.357,74.83,37.286,74.875,37.242,74.891,37.232,74.84,37.225,74.767,37.249,74.739,37.286,74.727,37.291,74.669,37.267,74.559,37.237,74.372,37.158,74.376,37.137,74.498,37.057,74.526,37.031,74.541,37.022,74.431,36.984,74.195,36.897,74.039,36.826,74.002,36.823,73.908,36.853,73.769,36.888,73.732,36.888,73.411,36.882,73.117,36.869,72.994,36.852,72.766,36.835,72.623,36.83,72.531,36.802,72.431,36.766,72.327,36.742,72.25,36.735,72.157,36.701,72.096,36.634,71.921,36.534,71.822,36.486,71.773,36.432,71.716,36.427,71.621,36.436,71.546,36.378,71.463,36.293,71.313,36.171,71.233,36.122,71.185,36.042,71.22,36.001,71.343,35.939,71.398,35.88,71.428,35.834,71.484,35.715,71.519,35.598,71.572,35.547,71.587,35.461,71.601,35.408,71.572,35.37,71.546,35.329,71.546,35.289,71.577,35.248,71.605,35.212,71.621,35.183,71.602,35.151,71.546,35.101,71.517,35.051,71.455,34.967,71.358,34.91,71.294,34.868,71.226,34.78,71.113,34.682,71.066,34.6,71.016,34.555,70.966,34.53,70.979,34.486,71.023,34.431,71.096,34.369,71.092,34.273,71.089,34.204,71.091,34.12,71.052,34.05,70.848,33.982,70.654,33.952,70.416,33.95,70.326,33.961,70.254,33.976,69.995,34.052,69.89,34.007,69.868,33.898,70.057,33.72,70.134,33.621,70.22,33.455,70.284,33.369,70.261,33.289,70.09,33.198,69.92,33.113,69.704,33.095,69.568,33.064,69.502,33.02,69.453,32.833,69.405,32.764,69.405,32.683,69.359,32.59,69.29,32.531,69.241,32.434,69.257,32.249,69.279,31.937,69.187,31.838,69.083,31.738,68.973,31.667,68.869,31.634,68.782,31.646,68.714,31.708,68.673,31.76,68.598,31.803,68.521,31.794,68.443,31.754,68.32,31.768,68.214,31.807,68.161,31.803,68.13,31.763,68.017,31.678,67.74,31.548,67.627,31.539,67.578,31.506,67.598,31.453,67.647,31.41,67.733,31.379,67.738,31.344,67.662,31.313,67.596,31.278,67.453,31.235,67.287,31.218,67.116,31.243,67.028,31.3,66.924,31.306,66.829,31.264,66.731,31.195,66.624,31.046,66.596,31.02,66.567,30.997,66.497,30.965,66.397,30.912,66.347,30.803,66.287,30.608,66.301,30.503,66.305,30.321,66.282,30.193,66.238,30.11,66.247,30.044,66.313,29.969,66.287,29.92,66.231,29.866,66.177,29.836,65.962,29.779,65.666,29.701,65.471,29.652,65.18,29.578,65.096,29.559,64.919,29.553,64.827,29.564,64.704,29.567,64.521,29.565,64.394,29.544,64.266,29.507,64.172,29.46,64.118,29.414,64.099,29.392,63.971,29.43,63.568,29.498,62.477,29.408,62.373,29.425,62.001,29.53,61.521,29.666,61.224,29.749,60.843,29.859,61.104,30.128,61.332,30.364,61.559,30.599,61.784,30.832,61.811,30.913,61.814,31.073,61.755,31.285,61.66,31.382,61.346,31.422,61.111,31.451,60.854,31.483,60.821,31.495,60.792,31.661,60.804,31.734,60.788,31.877,60.79,31.987,60.827,32.168,60.829,32.249,60.71,32.6,60.645,32.794,60.577,32.995,60.562,33.059,60.561,33.138,60.718,33.324,60.767,33.364,60.859,33.456,60.917,33.505,60.907,33.539,60.806,33.559,60.655,33.56,60.574,33.588,60.511,33.639,60.486,33.712,60.527,33.842,60.486,34.095,60.57,34.22,60.643,34.307,60.889,34.319,60.804,34.418,60.763,34.475,60.736,34.492,60.726,34.518,60.739,34.545,60.802,34.555,60.845,34.588,60.915,34.634,60.951,34.654,60.958,34.71,60.991,34.75,61.04,34.799,61.08,34.856,61.07,34.922,61.107,35.001,61.123,35.051,61.15,35.094,61.126,35.157,61.107,35.209,61.1,35.272,61.14,35.289,61.189,35.312,61.199,35.362,61.226,35.424,61.246,35.474,61.279,35.514,61.282,35.553,61.262,35.62,61.345,35.629,61.378,35.593,61.422,35.546,61.543,35.458,61.621,35.432,61.72,35.419,61.841,35.431,61.938,35.448,61.984,35.444,62.09,35.38,62.213,35.29,62.253,35.25,62.271,35.189,62.308,35.171,62.387,35.231,62.463,35.251,62.533,35.24,62.611,35.233,62.688,35.255,62.723,35.271,62.858,35.35,62.98,35.409,63.057,35.446,63.084,35.568,63.119,35.638,63.17,35.678,63.151,35.728,63.13,35.767,63.109,35.819,63.13,35.846,63.179,35.858,63.302,35.858,63.517,35.913,63.697,35.968,63.863,36.012,63.938,36.02,64.01,36.012,64.042,36.025,64.051,36.068,64.092,36.113,64.184,36.149,64.358,36.226,64.511,36.341,64.566,36.428,64.603,36.555,64.674,36.75,64.753,36.965,64.782,37.059,64.816,37.132,64.952,37.194,65.09,37.238,65.304,37.247,65.555,37.251,65.608,37.368,65.641,37.468,65.683,37.519,65.744,37.561,65.765,37.569,65.901,37.508,66.108,37.415,66.35,37.368,66.472,37.345,66.522,37.348]]]},{"n":"Siachen Glacier","c":"","r":5,"ml":6.5,"lx":77.13,"ly":35.34,"b":[76.767,35.11,77.799,35.662],"p":[[[77.049,35.11,77.004,35.196,76.979,35.246,76.928,35.347,76.882,35.436,76.813,35.572,76.767,35.662,76.879,35.613,77.09,35.552,77.295,35.508,77.446,35.476,77.52,35.473,77.573,35.472,77.724,35.481,77.799,35.496,77.697,35.443,77.572,35.379,77.423,35.303,77.293,35.236,77.169,35.172,77.049,35.11]]]},{"n":"Antarctica","c":"AQ","r":4,"ml":4,"lx":35.89,"ly":-79.84,"b":[-180,-89.999,180,-60.521],"p":[[[-45.718,-60.521,-45.5,-60.546,-45.386,-60.583,-45.357,-60.624,-45.228,-60.64,-45.211,-60.648,-45.186,-60.672,-45.173,-60.699,-45.174,-60.733,-45.398,-60.65,-45.709,-60.645,-45.78,-60.586,-45.937,-60.62,-45.955,-60.597,-45.956,-60.568,-45.935,-60.527,-45.834,-60.543,-45.718,-60.521]],[[-90.536,-68.798,-90.547,-68.798,-90.567,-68.798,-90.581,-68.799,-90.595,-68.8,-90.609,-68.802,-90.617,-68.803,-90.62,-68.803,-90.624,-68.801,-90.628,-68.797,-90.638,-68.788,-90.647,-68.78,-90.652,-68.771,-90.648,-68.76,-90.637,-68.754,-90.637,-68.747,-90.637,-68.738,-90.631,-68.73,-90.628,-68.721,-90.629,-68.716,-90.626,-68.714,-90.617,-68.712,-90.6,-68.712,-90.59,-68.713,-90.571,-68.714,-90.562,-68.715,-90.548,-68.718,-90.535,-68.728,-90.525,-68.738,-90.525,-68.743,-90.523,-68.749,-90.521,-68.756,-90.519,-68.764,-90.515,-68.771,-90.518,-68.783,-90.526,-68.795,-90.533,-68.797,-90.536,-68.798]],[[-57.021,-63.373,-56.927,-63.506,-56.782,-63.572,-56.835,-63.631,-56.974,-63.625,-57.119,-63.638,-57.152,-63.572,-57.098,-63.524,-57.152,-63.479,-57.284,-63.491,-57.461,-63.514,-57.581,-63.547,-57.737,-63.617,-57.857,-63.657,-58.263,-63.763,-58.532,-63.915,-58.723,-64.077,-58.839,-64.187,-59.005,-64.195,-59.047,-64.234,-58.977,-64.266,-58.923,-64.279,-58.799,-64.293,-58.819,-64.339,-58.905,-64.352,-58.895,-64.389,-58.806,-64.445,-58.786,-64.524,-58.892,-64.537,-59.051,-64.451,-59.229,-64.444,-59.37,-64.404,-59.461,-64.346,-59.547,-64.359,-59.612,-64.44,-59.573,-64.531,-59.646,-64.584,-59.734,-64.559,-59.765,-64.451,-59.85,-64.434,-59.963,-64.431,-60.242,-64.547,-60.341,-64.551,-60.394,-64.609,-60.556,-64.677,-60.66,-64.729,-60.915,-64.907,-61.06,-64.981,-61.332,-65.024,-61.439,-65.018,-61.503,-65,-61.603,-64.988,-61.703,-64.987,-61.736,-65.033,-61.577,-65.186,-61.663,-65.239,-61.856,-65.235,-61.948,-65.192,-62.025,-65.233,-62.085,-65.273,-62.145,-65.332,-62.054,-65.457,-61.903,-65.513,-61.796,-65.523,-61.756,-65.569,-61.991,-65.589,-62.151,-65.699,-62.222,-65.775,-62.305,-65.84,-62.293,-65.916,-62.169,-66.031,-62.005,-66.113,-61.839,-66.12,-61.625,-66.095,-61.575,-66.071,-61.359,-66.059,-61.266,-65.98,-61.198,-65.975,-61.138,-65.989,-61.039,-65.992,-60.988,-65.94,-60.913,-65.921,-60.813,-65.934,-60.618,-65.933,-60.565,-65.979,-60.625,-66.032,-60.744,-66.105,-60.856,-66.065,-60.956,-66.072,-61.009,-66.111,-60.903,-66.191,-60.942,-66.264,-61.028,-66.337,-61.134,-66.29,-61.15,-66.212,-61.293,-66.165,-61.432,-66.145,-61.526,-66.226,-61.676,-66.25,-61.696,-66.343,-61.756,-66.429,-61.842,-66.403,-61.875,-66.296,-62.117,-66.209,-62.241,-66.197,-62.494,-66.219,-62.583,-66.217,-62.682,-66.237,-62.755,-66.31,-62.65,-66.364,-62.615,-66.436,-62.618,-66.49,-62.638,-66.511,-62.655,-66.556,-62.543,-66.621,-62.537,-66.707,-62.629,-66.706,-62.705,-66.68,-62.997,-66.453,-63.18,-66.353,-63.258,-66.264,-63.449,-66.244,-63.587,-66.242,-63.753,-66.278,-63.688,-66.32,-63.654,-66.383,-63.756,-66.409,-63.88,-66.506,-63.964,-66.589,-64.015,-66.607,-64.078,-66.654,-63.809,-66.761,-63.769,-66.803,-63.755,-66.873,-63.84,-66.912,-64.043,-66.927,-64.401,-66.853,-64.554,-66.852,-64.607,-66.8,-64.686,-66.806,-64.735,-66.894,-64.793,-66.972,-64.878,-67.025,-64.854,-67.105,-64.785,-67.124,-64.839,-67.156,-64.951,-67.183,-65.027,-67.214,-64.858,-67.243,-64.826,-67.269,-64.819,-67.307,-65.08,-67.335,-65.249,-67.342,-65.35,-67.311,-65.443,-67.326,-65.503,-67.377,-65.523,-67.445,-65.504,-67.528,-65.471,-67.588,-65.447,-67.61,-65.418,-67.66,-65.574,-67.788,-65.589,-67.816,-65.6,-67.876,-65.528,-67.93,-65.469,-68.009,-65.551,-68.048,-65.64,-68.131,-65.546,-68.147,-65.387,-68.15,-65.218,-68.14,-64.959,-68.068,-64.885,-68.056,-64.853,-68.083,-64.829,-68.127,-64.896,-68.168,-65.365,-68.287,-65.452,-68.337,-65.331,-68.364,-65.09,-68.37,-64.996,-68.408,-65.055,-68.449,-65.14,-68.489,-65.242,-68.583,-65.158,-68.618,-64.898,-68.673,-64.429,-68.746,-64.078,-68.771,-64.157,-68.687,-64.169,-68.583,-63.924,-68.498,-63.796,-68.47,-63.217,-68.419,-63.057,-68.421,-62.933,-68.443,-62.98,-68.486,-63.115,-68.471,-63.348,-68.499,-63.707,-68.592,-63.773,-68.632,-63.747,-68.705,-63.443,-68.764,-63.344,-68.81,-63.478,-68.951,-63.456,-69.042,-63.301,-69.141,-63.094,-69.253,-62.994,-69.329,-62.84,-69.372,-62.587,-69.477,-62.451,-69.584,-62.407,-69.827,-62.202,-70.028,-61.961,-70.12,-61.935,-70.2,-62.014,-70.279,-62.218,-70.233,-62.331,-70.279,-62.378,-70.365,-62.232,-70.424,-62.001,-70.497,-61.505,-70.491,-61.491,-70.57,-61.605,-70.617,-61.696,-70.676,-61.809,-70.709,-61.994,-70.729,-62.04,-70.801,-61.961,-70.901,-61.702,-70.857,-61.513,-70.851,-61.313,-70.868,-61.252,-71.002,-61.017,-71.167,-60.962,-71.245,-61.003,-71.319,-61.148,-71.342,-61.237,-71.401,-61.369,-71.452,-61.516,-71.479,-61.79,-71.616,-61.91,-71.631,-61.959,-71.658,-61.725,-71.673,-61.563,-71.675,-61.214,-71.564,-61.081,-71.589,-60.995,-71.661,-60.949,-71.747,-61.035,-71.82,-61.645,-71.863,-61.939,-71.904,-62.257,-72.018,-61.894,-72.071,-61.628,-72.053,-61.493,-72.073,-61.31,-72.113,-61.107,-72.092,-60.952,-72.05,-60.833,-72.052,-60.719,-72.073,-60.704,-72.144,-60.691,-72.27,-60.665,-72.362,-60.73,-72.426,-61.048,-72.471,-61.28,-72.468,-61.286,-72.601,-60.939,-72.7,-60.724,-72.647,-60.532,-72.673,-60.532,-72.832,-60.385,-73.007,-60.254,-73.017,-60.149,-72.938,-60.01,-72.938,-59.957,-73.031,-60.016,-73.189,-60.122,-73.275,-60.404,-73.24,-60.561,-73.211,-60.687,-73.271,-60.896,-73.32,-61.081,-73.328,-61.242,-73.25,-61.428,-73.191,-61.726,-73.161,-62.008,-73.148,-61.915,-73.216,-61.788,-73.255,-61.738,-73.375,-61.637,-73.5,-61.405,-73.467,-61.08,-73.539,-60.879,-73.612,-60.79,-73.712,-60.903,-73.871,-61.088,-73.929,-61.203,-73.957,-61.404,-73.896,-61.545,-73.896,-61.692,-73.924,-61.742,-73.996,-61.838,-74.032,-61.319,-74.036,-61.161,-74.056,-61.042,-74.122,-61.227,-74.208,-61.571,-74.195,-61.718,-74.228,-61.843,-74.29,-61.332,-74.329,-61.121,-74.307,-60.784,-74.241,-60.704,-74.307,-60.838,-74.373,-61.011,-74.478,-61.37,-74.512,-61.64,-74.514,-61.995,-74.476,-62.089,-74.453,-62.235,-74.441,-62.226,-74.506,-62.133,-74.55,-61.894,-74.713,-61.855,-74.777,-61.928,-74.863,-62.138,-74.926,-62.372,-74.952,-62.567,-74.896,-62.708,-74.737,-62.887,-74.691,-63.072,-74.678,-63.178,-74.684,-63.168,-74.765,-63.125,-74.85,-63.198,-74.909,-63.357,-74.878,-63.559,-74.906,-63.751,-74.952,-63.925,-75.004,-63.571,-75.03,-63.337,-75.035,-63.173,-75.115,-63.231,-75.154,-63.551,-75.171,-63.858,-75.206,-64.28,-75.293,-63.972,-75.329,-63.678,-75.328,-63.475,-75.336,-63.304,-75.352,-63.258,-75.399,-63.363,-75.451,-64.053,-75.58,-64.778,-75.738,-65.044,-75.787,-65.322,-75.815,-65.966,-75.952,-66.37,-76.013,-67.518,-76.11,-69.304,-76.351,-69.915,-76.522,-70.096,-76.654,-70.21,-76.674,-70.551,-76.718,-70.895,-76.739,-71.799,-76.753,-72.722,-76.689,-73.472,-76.675,-73.88,-76.697,-75.268,-76.581,-75.444,-76.587,-75.659,-76.608,-75.831,-76.608,-75.963,-76.592,-76.244,-76.585,-77.19,-76.63,-77.287,-76.702,-77.168,-76.834,-76.824,-76.993,-76.249,-77.275,-75.937,-77.334,-75.748,-77.398,-75.387,-77.474,-74.581,-77.478,-73.478,-77.536,-72.852,-77.59,-72.875,-77.694,-73.252,-77.894,-73.485,-77.971,-74.042,-78.109,-74.812,-78.178,-75.398,-78.158,-76.438,-78.044,-77.742,-77.94,-79.679,-77.843,-80.104,-77.797,-80.602,-77.752,-80.889,-77.798,-81.103,-77.842,-81.581,-77.846,-81.441,-77.886,-79.51,-78.154,-77.858,-78.351,-77.665,-78.401,-77.433,-78.435,-77.452,-78.56,-77.545,-78.66,-77.869,-78.746,-78.712,-78.752,-79.767,-78.821,-80.292,-78.823,-80.816,-78.754,-81.929,-78.559,-82.608,-78.412,-83.083,-78.247,-83.412,-78.115,-83.779,-77.984,-83.752,-78.066,-83.688,-78.148,-83.508,-78.248,-83.246,-78.357,-83.227,-78.402,-83.355,-78.408,-83.544,-78.355,-83.706,-78.404,-83.763,-78.461,-83.697,-78.537,-83.595,-78.611,-83.26,-78.774,-82.971,-78.817,-82.589,-78.916,-81.661,-79.1,-81.503,-79.163,-81.222,-79.298,-81.163,-79.4,-80.892,-79.502,-80.705,-79.517,-80.535,-79.513,-80.479,-79.426,-80.489,-79.321,-80.416,-79.295,-80.151,-79.268,-79.456,-79.304,-76.499,-79.326,-76.218,-79.387,-76.105,-79.465,-76.032,-79.627,-76.344,-79.821,-76.558,-79.904,-76.904,-79.955,-77.222,-79.994,-77.702,-80.01,-78.692,-79.995,-79.66,-79.997,-78.907,-80.09,-78.176,-80.167,-77.16,-80.153,-76.757,-80.131,-76.407,-80.095,-76.26,-80.16,-75.986,-80.295,-75.822,-80.338,-75.709,-80.383,-75.555,-80.531,-75.495,-80.617,-75.345,-80.719,-75.237,-80.803,-75.076,-80.86,-74.807,-80.887,-74.511,-80.838,-73.938,-80.816,-73.383,-80.894,-73.029,-80.917,-72.553,-80.853,-72.174,-80.764,-71.38,-80.682,-71.231,-80.647,-71.018,-80.619,-70.688,-80.626,-70.56,-80.647,-70.392,-80.735,-70.239,-80.857,-70.012,-80.918,-69.772,-80.962,-69.634,-80.966,-69.182,-81.005,-68.59,-80.968,-68.327,-81.004,-68.285,-81.074,-68.144,-81.13,-67.965,-81.148,-65.574,-81.461,-64.75,-81.522,-63.478,-81.553,-62.49,-81.557,-62.354,-81.577,-62.165,-81.636,-62.542,-81.678,-62.946,-81.684,-63.554,-81.667,-63.769,-81.676,-64.233,-81.66,-64.476,-81.672,-64.696,-81.652,-65.022,-81.696,-65.62,-81.729,-65.487,-81.775,-65.264,-81.786,-64.811,-81.803,-64.19,-81.795,-64.137,-81.869,-64.706,-81.887,-65.916,-81.902,-66.042,-81.914,-66.134,-81.953,-65.953,-81.971,-65.844,-81.993,-65.787,-82.046,-65.913,-82.183,-65.714,-82.279,-65.572,-82.294,-65.424,-82.28,-65.17,-82.318,-64.92,-82.371,-64.397,-82.374,-63.773,-82.304,-63.466,-82.307,-62.645,-82.263,-61.902,-82.271,-60.859,-82.187,-60.687,-82.189,-60.528,-82.2,-60.817,-82.276,-62.095,-82.467,-62.553,-82.503,-62.736,-82.527,-62.631,-82.621,-62.466,-82.718,-62.129,-82.822,-61.917,-82.977,-61.709,-83.01,-61.313,-82.939,-61.218,-82.992,-61.2,-83.098,-61.303,-83.184,-61.436,-83.232,-61.531,-83.279,-61.59,-83.341,-61.425,-83.396,-60.983,-83.428,-60.397,-83.441,-59.854,-83.442,-59.516,-83.458,-58.29,-83.121,-57.798,-82.959,-57.557,-82.89,-57.354,-82.84,-56.318,-82.633,-56.075,-82.57,-55.801,-82.478,-55.295,-82.465,-54.601,-82.316,-53.986,-82.201,-53.74,-82.178,-53.558,-82.169,-53.339,-82.145,-52.799,-82.154,-52.415,-82.135,-51.731,-82.062,-51.21,-82.015,-50.653,-81.975,-50.029,-81.968,-48.361,-81.892,-47.887,-81.925,-47.36,-82.004,-47.02,-82.003,-46.567,-81.979,-46.258,-81.947,-46.119,-82.04,-46.046,-82.159,-46.199,-82.271,-46.448,-82.34,-46.517,-82.455,-46.175,-82.512,-45.789,-82.495,-45.044,-82.438,-44.455,-82.366,-44.292,-82.318,-44.064,-82.331,-43.669,-82.27,-43.18,-82.017,-42.565,-81.762,-42.046,-81.598,-41.712,-81.408,-41.434,-81.298,-41.126,-81.215,-40.915,-81.172,-40.441,-81.165,-39.762,-81.032,-38.772,-80.882,-38.011,-80.954,-37.209,-81.064,-36.812,-80.975,-36.5,-80.96,-36.234,-80.921,-35.966,-80.891,-35.776,-80.813,-35.521,-80.746,-35.327,-80.651,-34.35,-80.603,-33.329,-80.54,-33.191,-80.519,-33.057,-80.532,-32.706,-80.514,-32.256,-80.461,-31.634,-80.445,-31.312,-80.45,-31.015,-80.308,-30.425,-80.28,-29.797,-80.223,-29.531,-80.182,-29.329,-80.172,-24.24,-80.062,-24.02,-80.009,-23.574,-79.965,-23.407,-79.859,-24.088,-79.815,-24.3,-79.771,-24.534,-79.758,-24.67,-79.775,-25.259,-79.762,-29.949,-79.599,-30.049,-79.585,-30.211,-79.485,-30.178,-79.304,-30.316,-79.163,-30.645,-79.124,-30.985,-79.128,-31.413,-79.145,-32.542,-79.222,-32.994,-79.229,-34.197,-79.11,-34.995,-78.978,-35.516,-78.933,-35.89,-78.844,-36.239,-78.774,-36.266,-78.616,-36.181,-78.468,-35.509,-78.041,-35.088,-77.837,-34.808,-77.821,-34.551,-77.729,-34.29,-77.522,-34.076,-77.425,-33.591,-77.311,-33.377,-77.282,-32.614,-77.141,-32.405,-77.136,-32.063,-77.16,-31.676,-77.033,-30.489,-76.762,-30.222,-76.66,-29.892,-76.598,-28.934,-76.37,-28.079,-76.258,-27.653,-76.226,-27.135,-76.157,-26.56,-76.055,-26.059,-75.957,-24.27,-75.767,-23.197,-75.718,-22.465,-75.661,-21.948,-75.694,-21.434,-75.683,-20.989,-75.634,-20.783,-75.594,-20.487,-75.492,-19.493,-75.54,-18.851,-75.47,-18.585,-75.463,-18.305,-75.431,-18.415,-75.396,-18.517,-75.39,-18.617,-75.342,-18.749,-75.242,-18.617,-75.115,-18.517,-75.052,-18.221,-74.975,-18.068,-74.863,-17.923,-74.699,-17.436,-74.379,-17.299,-74.334,-16.989,-74.32,-16.727,-74.328,-16.43,-74.324,-15.673,-74.407,-15.531,-74.376,-15.29,-74.281,-15.089,-74.163,-14.659,-73.989,-14.574,-73.937,-14.611,-73.852,-15.26,-73.889,-15.749,-73.946,-16.22,-73.916,-16.282,-73.867,-16.181,-73.83,-16.003,-73.816,-15.936,-73.758,-16.097,-73.709,-16.388,-73.681,-16.519,-73.644,-16.507,-73.556,-16.435,-73.426,-16.279,-73.388,-16.149,-73.334,-15.803,-73.152,-15.596,-73.097,-15.007,-73.047,-14.321,-73.123,-14.165,-73.102,-14,-73.001,-14.168,-72.843,-14.298,-72.785,-14.298,-72.733,-13.939,-72.756,-13.603,-72.792,-13.209,-72.785,-12.747,-72.629,-12.095,-72.498,-11.777,-72.444,-11.497,-72.413,-11.346,-72.282,-11.121,-72.032,-10.958,-71.902,-10.961,-71.822,-11.009,-71.758,-11.179,-71.777,-11.333,-71.786,-11.697,-71.719,-12.148,-71.614,-12.285,-71.495,-12.351,-71.39,-12.208,-71.332,-12.074,-71.297,-11.926,-71.289,-11.663,-71.331,-11.328,-71.44,-11.16,-71.481,-10.97,-71.56,-10.825,-71.553,-10.659,-71.443,-10.52,-71.296,-10.407,-71.25,-10.231,-71.201,-10.033,-71.131,-10.122,-71.061,-10.331,-71.024,-10.36,-70.982,-10.271,-70.936,-10.099,-70.926,-9.888,-71.027,-9.599,-71.095,-9.402,-71.118,-9.231,-71.174,-8.966,-71.361,-8.646,-71.673,-8.498,-71.675,-8.216,-71.647,-7.916,-71.635,-7.714,-71.546,-7.669,-71.324,-7.59,-71.224,-7.618,-71.121,-7.757,-71.017,-7.873,-70.94,-7.855,-70.885,-7.753,-70.843,-7.62,-70.829,-7.388,-70.787,-7.032,-70.835,-6.838,-70.845,-6.548,-70.817,-6.245,-70.756,-5.936,-70.713,-5.695,-70.745,-5.588,-70.857,-5.709,-70.968,-5.904,-71.052,-6.08,-71.154,-6.127,-71.266,-6.117,-71.326,-5.95,-71.342,-4.45,-71.328,-4.253,-71.338,-3.995,-71.339,-3.713,-71.375,-3.24,-71.36,-2.812,-71.321,-2.61,-71.321,-2.261,-71.357,-2.015,-71.433,-1.501,-71.412,-1.354,-71.387,-1.216,-71.284,-1.068,-71.266,-0.896,-71.349,-0.84,-71.54,-0.76,-71.63,-0.543,-71.713,-0.327,-71.642,-0.185,-71.559,0.154,-71.398,0.538,-71.274,0.835,-71.202,1.552,-71.08,1.909,-71.004,2.609,-70.9,3.507,-70.844,5.113,-70.656,5.644,-70.636,6.508,-70.586,6.951,-70.535,7.401,-70.494,7.677,-70.356,8.307,-70.462,8.523,-70.474,8.817,-70.391,9.142,-70.184,9.613,-70.269,9.886,-70.403,10.218,-70.508,10.969,-70.688,11.204,-70.729,11.701,-70.767,11.834,-70.737,12.068,-70.617,12.309,-70.443,12.462,-70.37,12.682,-70.309,12.929,-70.213,12.865,-70.162,12.723,-70.144,12.595,-70.117,12.626,-70.066,13.066,-70.054,13.298,-70.23,13.533,-70.287,13.823,-70.343,14.492,-70.3,15.064,-70.295,15.563,-70.331,15.807,-70.324,16.025,-70.193,16.381,-70.145,16.585,-70.204,16.709,-70.397,17.167,-70.451,18.125,-70.54,18.232,-70.518,18.351,-70.416,18.433,-70.29,18.627,-70.269,18.877,-70.201,19.009,-70.212,19.196,-70.293,19.132,-70.492,19.027,-70.674,19.153,-70.821,19.265,-70.902,19.409,-70.917,19.652,-70.921,19.944,-70.91,20.128,-70.918,21.071,-70.843,21.186,-70.681,21.337,-70.495,21.705,-70.258,21.849,-70.277,21.962,-70.3,22.216,-70.417,22.366,-70.475,22.396,-70.561,22.234,-70.643,22.278,-70.696,22.445,-70.74,22.979,-70.81,23.15,-70.796,23.407,-70.723,23.665,-70.575,23.804,-70.405,24.024,-70.413,24.236,-70.449,24.386,-70.537,24.386,-70.704,24.588,-70.82,24.757,-70.892,25.187,-70.971,25.65,-70.991,25.974,-71.037,26.499,-71.02,26.754,-70.967,26.918,-70.954,27.207,-70.911,27.509,-70.813,27.698,-70.772,28.386,-70.682,28.912,-70.583,29.464,-70.406,30.003,-70.3,30.834,-70.246,31.063,-70.225,31.379,-70.226,32.16,-70.1,32.457,-70.026,32.621,-70.001,32.81,-69.909,32.912,-69.734,32.989,-69.624,32.976,-69.517,32.903,-69.379,32.738,-69.255,32.568,-69.074,32.642,-68.869,32.776,-68.783,33.121,-68.689,33.466,-68.671,33.854,-68.683,34.193,-68.702,34.219,-68.791,34.074,-68.885,33.885,-68.979,33.772,-69.02,33.814,-69.099,34.059,-69.111,34.596,-69.095,34.75,-69.168,35.131,-69.487,35.225,-69.637,35.357,-69.681,35.568,-69.66,36.018,-69.662,36.331,-69.639,36.586,-69.638,36.719,-69.652,36.856,-69.726,37.115,-69.81,37.375,-69.748,37.56,-69.718,37.787,-69.726,38.144,-69.824,38.499,-70.056,38.886,-70.172,38.912,-70.098,38.859,-70.006,39.019,-69.924,39.211,-69.786,39.487,-69.608,39.705,-69.426,39.762,-69.173,39.864,-68.967,40.042,-68.868,40.216,-68.805,40.484,-68.739,40.817,-68.724,41.133,-68.575,41.356,-68.515,41.825,-68.433,42.409,-68.352,42.82,-68.123,42.961,-68.095,43.171,-68.06,43.554,-68.046,44.178,-67.972,44.373,-67.961,44.7,-67.904,44.99,-67.769,45.197,-67.731,45.569,-67.736,45.888,-67.66,46.154,-67.657,46.399,-67.618,46.437,-67.533,46.32,-67.477,46.317,-67.402,46.454,-67.304,46.56,-67.268,46.884,-67.275,47.154,-67.357,47.352,-67.362,47.403,-67.409,47.231,-67.468,47.117,-67.573,47.314,-67.665,47.49,-67.728,47.704,-67.716,47.959,-67.66,48.21,-67.699,48.322,-67.785,48.322,-67.917,48.375,-67.988,48.551,-67.926,48.648,-67.794,48.62,-67.625,48.63,-67.521,49.053,-67.352,49.219,-67.227,48.923,-67.2,48.714,-67.217,48.598,-67.171,48.465,-67.043,48.83,-66.938,49.247,-66.942,49.489,-67.031,50.006,-67.175,50.293,-67.172,50.553,-67.194,50.606,-67.15,50.509,-66.939,50.521,-66.82,50.306,-66.753,50.244,-66.603,50.332,-66.445,50.588,-66.356,50.937,-66.315,51.688,-66.072,51.885,-66.02,52.378,-65.969,52.955,-65.946,53.672,-65.859,54.948,-65.916,55.29,-65.954,55.504,-66.003,55.71,-66.08,55.974,-66.209,56.362,-66.373,56.859,-66.423,57,-66.475,57.185,-66.613,56.987,-66.704,56.824,-66.713,56.51,-66.659,56.295,-66.603,56.146,-66.626,56.292,-66.721,56.453,-66.78,56.48,-66.859,56.391,-66.974,55.803,-67.199,56.155,-67.265,56.366,-67.212,56.562,-67.116,56.76,-67.073,56.892,-67.056,57.361,-67.053,57.627,-67.014,57.828,-67.041,58.027,-67.103,58.317,-67.163,58.737,-67.23,59.251,-67.485,59.65,-67.459,59.868,-67.403,60.482,-67.385,61.012,-67.5,61.309,-67.54,62.174,-67.575,62.688,-67.648,63.018,-67.562,63.238,-67.527,63.699,-67.508,63.931,-67.526,64.574,-67.62,65.708,-67.716,66.488,-67.766,67.175,-67.768,67.502,-67.81,68.099,-67.854,68.328,-67.89,68.9,-67.862,69.167,-67.825,69.416,-67.743,69.559,-67.763,69.656,-67.865,69.603,-68.041,69.704,-68.161,69.789,-68.279,69.907,-68.379,69.982,-68.464,69.928,-68.535,69.762,-68.599,69.534,-68.737,69.547,-68.857,69.646,-68.932,69.531,-69.024,69.615,-69.154,69.629,-69.232,69.549,-69.294,69.372,-69.331,69.065,-69.337,68.906,-69.373,68.88,-69.47,68.959,-69.54,69.136,-69.578,69.188,-69.655,69.162,-69.77,69.083,-69.867,68.921,-69.912,68.744,-69.921,68.415,-69.902,68.178,-69.837,68.027,-69.894,67.917,-69.953,67.575,-70.088,67.417,-70.177,67.268,-70.273,67.659,-70.326,67.941,-70.423,68.559,-70.412,68.757,-70.37,69.021,-70.325,69.162,-70.334,69.25,-70.431,69.197,-70.585,69.188,-70.705,68.873,-71.035,68.768,-71.091,68.624,-71.181,68.448,-71.252,68.311,-71.287,68.037,-71.391,67.873,-71.58,67.694,-71.737,67.432,-72.003,67.281,-72.291,67.215,-72.461,67.113,-72.641,66.892,-72.949,66.746,-73,66.498,-73.125,66.569,-73.209,66.765,-73.217,67.003,-73.236,67.322,-73.3,67.749,-73.168,67.971,-73.086,68.016,-72.918,67.971,-72.751,68.107,-72.651,68.42,-72.515,69.157,-72.419,69.309,-72.409,69.555,-72.375,69.77,-72.254,69.962,-72.133,70.294,-72.055,70.573,-71.931,70.616,-71.842,70.732,-71.822,71.079,-71.737,71.168,-71.672,71.277,-71.624,71.349,-71.514,71.379,-71.309,71.465,-71.155,71.634,-70.949,71.771,-70.801,71.905,-70.707,72.263,-70.657,72.418,-70.599,72.622,-70.472,72.76,-70.396,72.744,-70.239,72.822,-70.096,73.041,-70.01,73.325,-69.849,73.676,-69.826,73.942,-69.743,74.227,-69.8,74.571,-69.88,75.148,-69.855,75.424,-69.893,75.636,-69.849,75.821,-69.725,75.891,-69.576,76.112,-69.487,76.36,-69.49,76.77,-69.34,77.192,-69.206,77.541,-69.174,77.817,-69.069,78.015,-68.892,78.229,-68.756,78.489,-68.626,78.563,-68.394,78.726,-68.278,79.035,-68.175,79.288,-68.119,80.363,-67.947,81.187,-67.831,82.017,-67.69,82.273,-67.692,82.607,-67.613,83.158,-67.611,83.304,-67.603,83.494,-67.441,83.904,-67.292,84.161,-67.244,84.485,-67.114,84.748,-67.102,85.117,-67.126,85.429,-67.161,85.711,-67.161,86.118,-67.055,86.75,-67.037,86.947,-66.986,87.085,-66.94,87.98,-66.788,88.314,-66.817,88.789,-66.792,89.077,-66.799,89.352,-66.818,89.698,-66.823,90.293,-66.77,90.547,-66.734,91.022,-66.603,91.546,-66.572,91.777,-66.537,92.073,-66.508,92.312,-66.559,92.486,-66.604,92.592,-66.614,92.731,-66.624,93.075,-66.571,93.358,-66.585,93.722,-66.643,93.964,-66.69,94.089,-66.689,94.313,-66.647,94.587,-66.544,94.84,-66.501,95.084,-66.527,95.248,-66.571,95.541,-66.631,95.991,-66.621,96.424,-66.6,96.789,-66.551,97.101,-66.499,97.388,-66.579,97.72,-66.607,98.258,-66.467,98.462,-66.499,98.603,-66.535,98.72,-66.553,98.859,-66.671,99.37,-66.648,99.824,-66.549,100.212,-66.474,100.591,-66.425,100.889,-66.358,101.327,-66.1,101.321,-66.021,101.381,-65.973,101.474,-65.951,102.174,-65.954,102.392,-65.933,102.674,-65.865,103.167,-65.917,103.639,-65.999,103.763,-65.99,103.951,-65.988,104.289,-66.039,104.667,-66.137,105,-66.164,106.387,-66.411,107.171,-66.47,107.566,-66.552,107.667,-66.58,107.785,-66.664,107.992,-66.672,108.158,-66.639,108.376,-66.766,108.91,-66.862,109.463,-66.909,109.824,-66.834,110.437,-66.621,110.622,-66.524,110.587,-66.312,110.907,-66.077,111.453,-65.961,112.13,-65.9,112.548,-65.848,113.099,-65.8,113.368,-65.849,113.502,-65.886,113.71,-65.93,113.954,-66.06,114.337,-66.36,114.619,-66.468,114.87,-66.477,115.082,-66.493,115.31,-66.561,115.635,-66.771,115.442,-66.958,115.274,-67.028,114.571,-67.108,114.26,-67.172,113.991,-67.212,113.913,-67.368,114.027,-67.441,114.319,-67.406,114.658,-67.388,114.926,-67.357,115.172,-67.308,115.384,-67.238,115.885,-67.202,116.215,-67.143,116.509,-67.108,116.713,-67.047,116.924,-67.055,117.132,-67.114,117.298,-67.109,117.745,-67.129,117.952,-67.085,118.139,-67.082,118.326,-67.115,118.519,-67.161,118.714,-67.172,118.965,-67.145,119.318,-67.071,119.768,-66.992,120.187,-66.966,120.289,-66.967,120.375,-66.984,119.954,-67.076,119.281,-67.199,118.922,-67.32,119.133,-67.371,120.4,-67.236,120.979,-67.136,121.488,-67.091,121.613,-67.057,122.033,-66.902,122.183,-66.859,122.633,-66.805,123.222,-66.745,123.667,-66.677,123.969,-66.608,124.196,-66.601,124.371,-66.652,124.598,-66.708,124.822,-66.695,125.095,-66.641,125.286,-66.516,125.398,-66.424,125.603,-66.393,125.866,-66.364,126.077,-66.396,126.424,-66.462,126.665,-66.498,126.874,-66.759,127.365,-66.99,127.541,-67.051,127.968,-67.028,128.431,-67.119,128.628,-67.107,128.816,-67.08,128.982,-67.098,129.237,-67.042,129.5,-66.753,129.741,-66.469,129.976,-66.345,130.121,-66.292,130.301,-66.268,130.579,-66.209,130.952,-66.191,131.232,-66.216,131.831,-66.236,132.32,-66.165,132.874,-66.178,133.148,-66.095,133.445,-66.081,133.843,-66.154,133.958,-66.204,134.179,-66.277,134.232,-66.348,134.289,-66.477,134.398,-66.48,134.77,-66.353,134.971,-66.33,135.352,-66.127,135.555,-66.18,136.009,-66.267,136.194,-66.292,136.553,-66.439,136.74,-66.408,136.889,-66.34,137.336,-66.346,137.754,-66.406,137.926,-66.457,138.14,-66.544,138.271,-66.564,138.376,-66.54,139.242,-66.574,139.613,-66.638,139.9,-66.715,140.902,-66.752,141.286,-66.832,141.517,-66.794,141.973,-66.807,142.159,-66.874,142.327,-66.948,142.688,-67.013,142.888,-67,143.169,-66.949,143.448,-66.877,143.73,-66.877,143.863,-66.939,143.911,-67.091,144.118,-67.088,144.348,-67.018,144.551,-67.036,144.621,-67.141,144.515,-67.283,144.26,-67.479,144.154,-67.644,143.942,-67.794,143.977,-67.865,144.189,-67.9,144.404,-67.794,144.879,-67.721,145.128,-67.626,145.556,-67.591,145.975,-67.624,146.276,-67.751,146.828,-67.965,146.852,-68.041,146.897,-68.12,146.878,-68.191,146.798,-68.274,147.094,-68.369,147.354,-68.384,147.569,-68.375,148.456,-68.467,148.881,-68.431,149.263,-68.431,149.717,-68.418,150.066,-68.42,150.342,-68.436,150.672,-68.403,150.936,-68.358,151.068,-68.385,151.121,-68.623,151.139,-68.764,151.289,-68.817,151.448,-68.764,151.562,-68.694,152.265,-68.726,152.546,-68.73,152.814,-68.768,153.082,-68.857,153.34,-68.818,153.496,-68.764,153.705,-68.729,153.767,-68.641,153.792,-68.493,153.785,-68.35,153.908,-68.323,154.032,-68.35,154.2,-68.418,154.576,-68.634,154.866,-68.774,154.988,-68.841,155.164,-68.895,155.52,-69.024,156.011,-69.078,156.489,-69.183,157.046,-69.176,157.481,-69.309,157.776,-69.205,157.933,-69.181,158.158,-69.209,158.433,-69.299,158.647,-69.32,159.386,-69.468,159.784,-69.522,159.931,-69.631,160.126,-69.734,160.126,-69.84,160.21,-69.975,160.652,-70.081,160.827,-70.182,161.037,-70.317,161.425,-70.827,161.625,-70.916,161.916,-70.907,162.189,-71.04,162.278,-71.022,162.287,-70.969,162.04,-70.625,162.022,-70.44,162.216,-70.334,162.675,-70.305,163.026,-70.501,163.349,-70.621,163.567,-70.642,163.998,-70.637,164.403,-70.51,164.716,-70.557,165.209,-70.571,165.854,-70.645,166.132,-70.633,166.627,-70.664,167.229,-70.771,167.569,-70.81,167.64,-70.854,167.799,-70.925,167.878,-71.013,167.966,-71.092,168.173,-71.183,168.383,-71.197,168.798,-71.275,169.664,-71.511,169.977,-71.581,170.162,-71.63,170.25,-71.569,170.277,-71.444,170.436,-71.419,170.603,-71.604,170.78,-71.745,170.859,-71.869,170.675,-71.969,170.409,-71.948,170.224,-71.948,170.03,-72.116,169.954,-72.403,170.127,-72.398,170.259,-72.371,170.286,-72.477,170.206,-72.565,170.048,-72.601,169.775,-72.534,169.44,-72.487,169.072,-72.469,168.719,-72.384,168.576,-72.379,168.428,-72.383,168.622,-72.473,168.82,-72.552,169.27,-72.621,169.829,-72.729,169.845,-72.795,169.712,-72.877,169.545,-73.05,169.033,-73.2,168.736,-73.091,168.381,-73.066,168.204,-73.13,167.853,-73.122,167.156,-73.147,166.883,-73.011,166.453,-72.936,166.467,-72.997,166.834,-73.224,167.226,-73.276,167.616,-73.337,167.709,-73.394,167.534,-73.447,167.296,-73.44,166.996,-73.544,166.429,-73.527,166.159,-73.534,166.001,-73.577,165.86,-73.593,165.971,-73.631,166.106,-73.735,165.913,-73.823,165.734,-73.867,165.549,-73.846,165.347,-73.879,165.25,-73.782,165.245,-73.571,165.129,-73.383,165.005,-73.375,164.813,-73.397,164.75,-73.559,164.888,-73.838,164.98,-73.926,164.906,-74.003,164.776,-74.029,165.037,-74.263,165.263,-74.426,165.4,-74.479,165.409,-74.559,165.303,-74.594,165.001,-74.563,164.853,-74.578,164.689,-74.568,164.411,-74.533,164.174,-74.523,163.936,-74.567,163.735,-74.564,163.557,-74.417,163.398,-74.382,163.266,-74.426,163.167,-74.602,162.961,-74.656,162.752,-74.736,162.604,-74.823,162.534,-75.167,162.41,-75.238,162.226,-75.235,162.088,-75.262,161.91,-75.234,161.68,-75.218,160.911,-75.335,161.033,-75.396,161.227,-75.386,161.904,-75.404,162.19,-75.467,162.239,-75.622,162.352,-75.687,162.578,-75.758,162.754,-75.793,162.816,-75.846,162.745,-75.952,162.648,-76.049,162.437,-76.155,162.498,-76.208,162.728,-76.225,162.825,-76.464,162.675,-76.569,162.745,-76.658,162.763,-76.746,162.61,-76.829,162.489,-76.869,162.45,-76.956,162.679,-77.007,162.85,-77.024,163.087,-77.032,163.25,-77.126,163.458,-77.269,163.608,-77.388,163.619,-77.582,163.766,-77.7,164.045,-77.775,164.036,-77.853,164.232,-77.877,164.421,-77.883,164.492,-77.954,164.43,-78.042,164.108,-78.147,163.978,-78.224,164.297,-78.236,164.628,-78.316,165.051,-78.226,165.274,-78.129,165.418,-78.042,165.524,-78.064,165.534,-78.154,165.663,-78.306,166.209,-78.452,166.51,-78.497,166.801,-78.522,167.058,-78.518,167.13,-78.606,167.049,-78.686,166.85,-78.68,166.525,-78.695,166.287,-78.628,166.117,-78.571,164.635,-78.603,164.301,-78.63,163.902,-78.717,163.503,-78.759,162.895,-78.845,162.639,-78.898,161.975,-78.694,161.757,-78.545,161.669,-78.536,161.51,-78.571,161.502,-78.677,161.813,-78.907,161.951,-78.968,161.951,-79.03,161.864,-79.061,161.737,-79.06,161.546,-79.015,161.191,-78.979,160.874,-79.05,160.483,-79.201,160.567,-79.302,160.67,-79.359,160.646,-79.427,160.209,-79.554,160.068,-79.561,159.976,-79.586,160.084,-79.633,160.323,-79.636,160.346,-79.692,160.265,-79.737,159.872,-79.79,159.896,-79.859,160.111,-79.892,160.387,-79.879,160.558,-79.93,160.559,-80.011,160.382,-80.054,160.179,-80.088,158.767,-80.293,158.56,-80.349,158.574,-80.423,159.065,-80.443,160.542,-80.425,160.637,-80.45,160.603,-80.508,160.521,-80.583,160.601,-80.637,160.823,-80.674,160.83,-80.73,160.72,-80.753,160.503,-80.779,160.26,-80.787,160.276,-80.847,160.607,-80.901,160.717,-80.907,160.728,-81.113,160.717,-81.2,160.54,-81.242,160.479,-81.27,160.47,-81.341,160.908,-81.39,161.582,-81.61,161.73,-81.61,161.996,-81.653,162.425,-81.765,162.577,-81.832,162.821,-81.866,162.854,-81.921,163.004,-81.969,163.602,-82.121,163.68,-82.187,162.427,-82.314,161.167,-82.408,161.283,-82.49,162.644,-82.482,163.012,-82.535,163.175,-82.519,163.269,-82.463,164.001,-82.397,164.747,-82.354,164.98,-82.385,165.982,-82.63,166.446,-82.722,166.742,-82.757,166.957,-82.765,167.116,-82.801,167.271,-82.879,167.233,-82.952,167.297,-82.986,167.404,-82.999,167.602,-83.047,167.828,-83.031,168.092,-82.975,168.276,-82.987,168.607,-83.065,168.482,-83.127,168.408,-83.155,168.32,-83.211,168.24,-83.23,167.973,-83.243,167.825,-83.243,167.674,-83.231,167.658,-83.272,167.843,-83.316,168.11,-83.362,169.838,-83.399,170.332,-83.479,170.817,-83.436,171.036,-83.448,171.221,-83.475,171.289,-83.555,171.537,-83.581,171.917,-83.644,172.45,-83.675,172.874,-83.673,173.397,-83.759,173.662,-83.761,173.822,-83.81,175.011,-83.839,175.187,-83.877,175.323,-83.94,175.606,-83.968,175.911,-83.973,177.581,-84.075,178.209,-84.13,178.353,-84.127,178.496,-84.136,178.944,-84.181,179.403,-84.206,179.62,-84.268,180,-84.352,180,-89.999,178.594,-89.999,177.188,-89.999,175.781,-89.999,174.375,-89.999,172.969,-89.999,171.563,-89.999,170.156,-89.999,168.75,-89.999,167.344,-89.999,165.938,-89.999,164.531,-89.999,163.125,-89.999,161.719,-89.999,160.313,-89.999,158.906,-89.999,157.5,-89.999,156.094,-89.999,154.688,-89.999,153.281,-89.999,151.875,-89.999,150.469,-89.999,149.063,-89.999,147.656,-89.999,146.25,-89.999,144.844,-89.999,143.438,-89.999,142.031,-89.999,140.625,-89.999,139.219,-89.999,137.813,-89.999,136.406,-89.999,135,-89.999,133.594,-89.999,132.188,-89.999,130.781,-89.999,129.375,-89.999,127.969,-89.999,126.562,-89.999,125.156,-89.999,123.75,-89.999,122.344,-89.999,120.938,-89.999,119.531,-89.999,118.125,-89.999,116.719,-89.999,115.313,-89.999,113.906,-89.999,112.5,-89.999,111.094,-89.999,109.688,-89.999,108.281,-89.999,106.875,-89.999,105.469,-89.999,104.063,-89.999,102.656,-89.999,101.25,-89.999,99.844,-89.999,98.438,-89.999,97.031,-89.999,95.625,-89.999,94.219,-89.999,92.813,-89.999,91.406,-89.999,90,-89.999,88.594,-89.999,87.188,-89.999,85.781,-89.999,84.375,-89.999,82.969,-89.999,81.563,-89.999,80.156,-89.999,78.75,-89.999,77.344,-89.999,75.938,-89.999,74.531,-89.999,73.125,-89.999,71.719,-89.999,70.313,-89.999,68.906,-89.999,67.5,-89.999,66.094,-89.999,64.688,-89.999,63.281,-89.999,61.875,-89.999,60.469,-89.999,59.063,-89.999,57.656,-89.999,56.25,-89.999,54.844,-89.999,53.438,-89.999,52.031,-89.999,50.625,-89.999,49.219,-89.999,47.813,-89.999,46.406,-89.999,45,-89.999,43.594,-89.999,42.188,-89.999,40.781,-89.999,39.375,-89.999,37.969,-89.999,36.563,-89.999,35.156,-89.999,33.75,-89.999,32.344,-89.999,30.938,-89.999,29.531,-89.999,28.125,-89.999,26.719,-89.999,25.313,-89.999,23.906,-89.999,22.5,-89.999,21.094,-89.999,19.688,-89.999,18.281,-89.999,16.875,-89.999,15.469,-89.999,14.063,-89.999,12.656,-89.999,11.25,-89.999,9.844,-89.999,8.438,-89.999,7.031,-89.999,5.625,-89.999,4.219,-89.999,2.813,-89.999,1.406,-89.999,0,-89.999,-1.406,-89.999,-2.812,-89.999,-4.219,-89.999,-5.625,-89.999,-7.031,-89.999,-8.438,-89.999,-9.844,-89.999,-11.25,-89.999,-12.656,-89.999,-14.062,-89.999,-15.469,-89.999,-16.875,-89.999,-18.281,-89.999,-19.687,-89.999,-21.094,-89.999,-22.5,-89.999,-23.906,-89.999,-25.313,-89.999,-26.719,-89.999,-28.125,-89.999,-29.531,-89.999,-30.937,-89.999,-32.344,-89.999,-33.75,-89.999,-35.156,-89.999,-36.562,-89.999,-37.969,-89.999,-39.375,-89.999,-40.781,-89.999,-42.187,-89.999,-43.594,-89.999,-45,-89.999,-46.406,-89.999,-47.812,-89.999,-49.219,-89.999,-50.625,-89.999,-52.031,-89.999,-53.438,-89.999,-54.844,-89.999,-56.25,-89.999,-57.656,-89.999,-59.062,-89.999,-60.469,-89.999,-61.875,-89.999,-63.281,-89.999,-64.687,-89.999,-66.094,-89.999,-67.5,-89.999,-68.906,-89.999,-70.312,-89.999,-71.719,-89.999,-73.125,-89.999,-74.531,-89.999,-75.937,-89.999,-77.344,-89.999,-78.75,-89.999,-80.156,-89.999,-81.562,-89.999,-82.969,-89.999,-84.375,-89.999,-85.781,-89.999,-87.187,-89.999,-88.594,-89.999,-90,-89.999,-91.406,-89.999,-92.812,-89.999,-94.219,-89.999,-95.625,-89.999,-97.031,-89.999,-98.437,-89.999,-99.844,-89.999,-101.25,-89.999,-102.656,-89.999,-104.062,-89.999,-105.469,-89.999,-106.875,-89.999,-108.281,-89.999,-109.687,-89.999,-111.094,-89.999,-112.5,-89.999,-113.906,-89.999,-115.312,-89.999,-116.719,-89.999,-118.125,-89.999,-119.531,-89.999,-120.937,-89.999,-122.344,-89.999,-123.75,-89.999,-125.156,-89.999,-126.562,-89.999,-127.969,-89.999,-129.375,-89.999,-130.781,-89.999,-132.187,-89.999,-133.594,-89.999,-135,-89.999,-136.406,-89.999,-137.812,-89.999,-139.219,-89.999,-140.625,-89.999,-142.031,-89.999,-143.437,-89.999,-144.844,-89.999,-146.25,-89.999,-147.656,-89.999,-149.062,-89.999,-150.469,-89.999,-151.875,-89.999,-153.281,-89.999,-154.688,-89.999,-156.094,-89.999,-157.5,-89.999,-158.906,-89.999,-160.312,-89.999,-161.719,-89.999,-163.125,-89.999,-164.531,-89.999,-165.938,-89.999,-167.344,-89.999,-168.75,-89.999,-170.156,-89.999,-171.562,-89.999,-172.969,-89.999,-174.375,-89.999,-175.781,-89.999,-177.187,-89.999,-178.594,-89.999,-180,-89.999,-180,-89.583,-180,-89.293,-180,-88.587,-180,-87.881,-180,-87.175,-180,-86.469,-180,-85.763,-180,-85.058,-180,-84.352,-178.39,-84.337,-178.069,-84.352,-177.73,-84.395,-176.986,-84.399,-176.289,-84.418,-176.107,-84.475,-175.875,-84.51,-175.381,-84.48,-174.987,-84.465,-174.663,-84.463,-171.704,-84.542,-168.668,-84.684,-168.049,-84.729,-167.492,-84.834,-166.911,-84.819,-163.464,-84.901,-162.933,-84.901,-160.821,-84.987,-157.127,-85.186,-156.81,-85.192,-156.459,-85.186,-156.643,-85.079,-156.988,-84.982,-157.454,-84.912,-157.15,-84.891,-156.49,-84.889,-156.621,-84.84,-156.986,-84.811,-158.303,-84.778,-163.569,-84.529,-163.685,-84.513,-163.759,-84.493,-163.897,-84.47,-164.114,-84.445,-164.917,-84.431,-165.135,-84.41,-165.24,-84.381,-165.185,-84.37,-165.125,-84.375,-163.899,-84.353,-163.765,-84.324,-163.758,-84.305,-163.821,-84.291,-164.032,-84.274,-164.528,-84.191,-164.685,-84.155,-164.603,-84.097,-164.503,-84.072,-164.124,-84.054,-164.011,-84.016,-164.083,-83.946,-164.951,-83.806,-165.536,-83.757,-165.922,-83.79,-166.649,-83.792,-167.553,-83.811,-167.801,-83.791,-168.053,-83.735,-168.347,-83.637,-168.497,-83.611,-168.785,-83.529,-169.168,-83.45,-171.188,-83.256,-171.539,-83.204,-174.066,-82.9,-174.172,-82.848,-174.236,-82.793,-173.071,-82.916,-172.852,-82.917,-172.593,-82.884,-172.392,-82.893,-172.124,-82.862,-171.821,-82.847,-171.031,-82.943,-169.441,-83.096,-169.016,-83.15,-168.79,-83.188,-168.604,-83.202,-168.418,-83.229,-168.191,-83.213,-168.055,-83.227,-167.724,-83.217,-166.217,-83.201,-165.619,-83.216,-164.916,-83.29,-164.644,-83.412,-164.446,-83.468,-164.058,-83.425,-163.733,-83.373,-163.111,-83.329,-162.912,-83.347,-162.574,-83.411,-162.197,-83.519,-160.595,-83.49,-159.924,-83.495,-159.444,-83.543,-157.699,-83.381,-157.428,-83.346,-157.028,-83.234,-157.356,-83.198,-157.589,-83.187,-157.679,-83.129,-157.522,-83.107,-157.018,-83.075,-156.037,-83.027,-155.459,-82.981,-155.15,-82.858,-153.822,-82.669,-153.399,-82.586,-153.01,-82.45,-153.883,-82.177,-154.717,-81.941,-154.451,-81.868,-154.188,-81.811,-154.061,-81.765,-153.957,-81.7,-154.232,-81.623,-154.485,-81.566,-154.908,-81.51,-156.493,-81.377,-157.033,-81.319,-156.815,-81.231,-156.528,-81.162,-155.921,-81.133,-152.035,-81.029,-148.123,-80.901,-148.023,-80.836,-148.543,-80.76,-148.984,-80.742,-149.147,-80.719,-149.207,-80.67,-149.214,-80.604,-149.264,-80.593,-149.429,-80.586,-150.133,-80.51,-150.282,-80.48,-150.516,-80.409,-150.575,-80.354,-150.435,-80.211,-150.221,-80.15,-149.845,-80.118,-149.578,-80.106,-148.766,-80.108,-148.448,-80.091,-148.317,-80.071,-148.34,-80.003,-148.43,-79.971,-148.433,-79.929,-148.296,-79.907,-148.129,-79.908,-148.083,-79.857,-148.177,-79.776,-148.417,-79.731,-149.051,-79.657,-150.491,-79.546,-151.048,-79.46,-151.368,-79.393,-151.636,-79.318,-151.904,-79.281,-152.091,-79.242,-152.053,-79.193,-152.138,-79.116,-152.244,-79.103,-152.701,-79.135,-153.518,-79.117,-154.518,-79.047,-155.21,-78.965,-156.115,-78.745,-156.469,-78.635,-156.208,-78.559,-155.92,-78.51,-154.716,-78.398,-154.538,-78.359,-154.293,-78.259,-154.695,-78.217,-155.037,-78.221,-155.342,-78.192,-156.569,-78.186,-157.267,-78.2,-157.848,-78.074,-158.286,-77.951,-158.407,-77.888,-158.5,-77.778,-158.351,-77.615,-158.23,-77.498,-158.246,-77.354,-158.214,-77.157,-158.003,-77.091,-157.842,-77.079,-157.465,-77.231,-157.139,-77.242,-156.668,-77.213,-156.368,-77.135,-156.211,-77.106,-155.92,-77.098,-155.359,-77.133,-155.102,-77.12,-154.815,-77.127,-153.91,-77.227,-153.713,-77.274,-153.606,-77.31,-153.573,-77.363,-153.461,-77.416,-153.077,-77.442,-151.998,-77.413,-151.719,-77.426,-150.956,-77.574,-150.306,-77.731,-150.084,-77.771,-149.718,-77.797,-149.588,-77.774,-149.474,-77.715,-149.126,-77.643,-148.34,-77.551,-148.156,-77.462,-148.26,-77.413,-148.559,-77.361,-148.744,-77.343,-148.844,-77.284,-148.839,-77.202,-148.777,-77.125,-148.572,-77.105,-148.196,-77.211,-147.73,-77.31,-147.566,-77.325,-147.442,-77.321,-147.207,-77.286,-146.928,-77.26,-146.391,-77.472,-146.074,-77.487,-145.677,-77.488,-145.601,-77.455,-145.65,-77.398,-145.714,-77.338,-145.794,-77.33,-145.808,-77.273,-145.635,-77.221,-145.516,-77.199,-145.563,-77.162,-145.753,-77.103,-145.864,-77.094,-145.967,-77.069,-145.934,-77.029,-145.806,-77.012,-145.629,-76.954,-145.686,-76.884,-145.676,-76.797,-145.75,-76.749,-146.166,-76.658,-146.777,-76.507,-147.34,-76.438,-148.601,-76.493,-149.046,-76.458,-149.34,-76.419,-149.654,-76.365,-149.285,-76.311,-148.895,-76.272,-148.78,-76.238,-148.632,-76.168,-148.459,-76.118,-148.32,-76.104,-147.86,-76.131,-146.817,-76.318,-146.597,-76.338,-145.886,-76.424,-145.687,-76.429,-145.442,-76.409,-145.642,-76.326,-145.86,-76.267,-146.383,-76.1,-146.323,-76.02,-145.988,-75.889,-145.106,-75.879,-144.721,-75.832,-144.221,-75.731,-143.574,-75.564,-143.022,-75.543,-142.33,-75.491,-142.094,-75.53,-141.506,-75.69,-141.135,-75.746,-141.009,-75.751,-140.874,-75.746,-141.223,-75.546,-140.999,-75.52,-140.709,-75.498,-140.471,-75.447,-140.294,-75.406,-139.691,-75.213,-139.149,-75.16,-137.618,-75.076,-137.09,-75.153,-136.65,-75.162,-136.55,-75.139,-136.462,-75.036,-136.228,-74.836,-136.03,-74.765,-135.362,-74.69,-134.84,-74.694,-134.465,-74.776,-134.117,-74.83,-133.796,-74.855,-133.475,-74.852,-132.992,-74.806,-132.351,-74.789,-132.049,-74.766,-131.707,-74.811,-130.857,-74.826,-130.196,-74.891,-129.791,-74.891,-129.238,-74.829,-128.941,-74.82,-127.863,-74.719,-127.02,-74.698,-126.384,-74.743,-125.353,-74.715,-124.312,-74.736,-123.889,-74.773,-121.544,-74.75,-119.677,-74.655,-119.422,-74.622,-119.022,-74.518,-118.803,-74.422,-118.656,-74.393,-118.342,-74.382,-117.806,-74.403,-117.068,-74.473,-116.433,-74.447,-115.223,-74.487,-115.105,-74.455,-114.991,-74.275,-114.791,-73.989,-114.624,-73.903,-114.346,-73.925,-113.508,-74.089,-113.489,-74.158,-113.575,-74.208,-113.714,-74.228,-113.753,-74.367,-113.641,-74.406,-113.454,-74.394,-113.333,-74.454,-113.597,-74.559,-113.783,-74.618,-113.903,-74.644,-113.985,-74.843,-114.097,-74.909,-114.11,-74.982,-113.932,-74.982,-113.753,-74.952,-113.593,-74.944,-113.092,-74.892,-112.17,-74.832,-111.868,-74.801,-111.696,-74.792,-111.584,-74.751,-111.739,-74.653,-111.789,-74.572,-111.696,-74.504,-111.722,-74.387,-111.806,-74.27,-111.63,-74.181,-111.467,-74.201,-111.18,-74.188,-111.02,-74.23,-110.77,-74.269,-110.534,-74.289,-110.307,-74.367,-110.23,-74.536,-110.3,-74.711,-110.532,-74.836,-110.968,-74.951,-111.463,-75.133,-111.359,-75.22,-111.104,-75.191,-109.99,-75.199,-109.272,-75.185,-108.822,-75.207,-108.254,-75.253,-107.805,-75.322,-107.267,-75.334,-106.932,-75.309,-106.619,-75.344,-105.399,-75.198,-104.902,-75.115,-104.618,-75.156,-104.16,-75.121,-103.901,-75.153,-103.425,-75.101,-103.121,-75.095,-102.771,-75.117,-101.708,-75.127,-101.628,-75.222,-101.304,-75.366,-101.039,-75.422,-100.706,-75.398,-100.463,-75.353,-100.083,-75.37,-99.531,-75.309,-98.98,-75.327,-98.752,-75.317,-98.646,-75.277,-98.558,-75.19,-98.727,-75.141,-99.208,-75.079,-99.652,-74.949,-99.849,-74.922,-100.164,-74.938,-100.313,-74.914,-100.473,-74.872,-100.265,-74.823,-100.013,-74.662,-100.119,-74.515,-100.238,-74.484,-100.531,-74.489,-100.882,-74.541,-101.023,-74.505,-101.252,-74.486,-101.343,-74.35,-101.587,-74.096,-101.715,-74.024,-102.105,-73.958,-102.441,-73.926,-102.766,-73.884,-102.863,-73.784,-102.8,-73.646,-102.411,-73.616,-102.037,-73.631,-101.828,-73.655,-101.587,-73.667,-101.311,-73.695,-101.13,-73.735,-100.985,-73.757,-100.718,-73.758,-99.781,-73.72,-99.656,-73.694,-99.541,-73.645,-99.343,-73.634,-99.162,-73.641,-98.896,-73.611,-99.2,-73.571,-99.528,-73.495,-100.021,-73.403,-100.436,-73.353,-101.189,-73.318,-101.574,-73.33,-101.816,-73.311,-102.675,-73.321,-102.909,-73.285,-103.076,-73.185,-103.308,-72.945,-103.375,-72.819,-103.217,-72.772,-103.11,-72.721,-102.856,-72.716,-102.485,-72.736,-102.363,-72.76,-102.272,-72.835,-102.363,-72.911,-102.482,-72.951,-102.409,-72.987,-102.029,-72.998,-101.842,-73.021,-101.681,-73.03,-101.332,-72.995,-100.821,-72.981,-100.564,-73.016,-100.259,-73.041,-99.811,-73,-98.209,-73.022,-98.012,-73.033,-97.819,-73.102,-97.651,-73.144,-97.476,-73.126,-96.956,-73.206,-96.676,-73.269,-96.394,-73.301,-96.152,-73.309,-95.881,-73.294,-95.529,-73.241,-95.237,-73.22,-95.03,-73.239,-94.586,-73.25,-94.246,-73.313,-93.985,-73.287,-93.706,-73.215,-92.828,-73.165,-92.241,-73.178,-91.169,-73.307,-90.921,-73.319,-90.431,-73.243,-90.274,-73.119,-90.295,-72.978,-90.152,-72.945,-90.035,-72.96,-89.818,-72.863,-89.522,-72.871,-89.341,-72.89,-89.229,-72.826,-89.127,-72.693,-88.78,-72.683,-88.527,-72.702,-88.194,-72.787,-88.195,-72.859,-88.332,-72.934,-88.561,-73.121,-88.419,-73.229,-88.205,-73.22,-87.936,-73.241,-87.608,-73.195,-87.401,-73.192,-87.038,-73.354,-86.791,-73.364,-86.602,-73.354,-85.981,-73.208,-85.801,-73.192,-85.582,-73.259,-85.261,-73.413,-84.981,-73.502,-84.571,-73.557,-84.214,-73.573,-83.796,-73.645,-83.565,-73.706,-83.042,-73.707,-82.815,-73.732,-82.183,-73.857,-81.606,-73.796,-81.309,-73.738,-81.163,-73.632,-81.236,-73.474,-81.262,-73.315,-81.176,-73.249,-81.024,-73.236,-80.336,-73.414,-80.38,-73.308,-80.439,-73.225,-80.614,-73.083,-80.588,-72.978,-80.442,-72.945,-80.152,-73,-79.808,-73.028,-79.522,-73.09,-78.964,-73.312,-78.786,-73.507,-78.408,-73.556,-78.144,-73.547,-77.846,-73.515,-77.444,-73.488,-77.136,-73.496,-76.85,-73.46,-76.765,-73.566,-77.033,-73.718,-77.135,-73.818,-77.049,-73.844,-76.887,-73.821,-76.755,-73.789,-76.291,-73.805,-75.916,-73.736,-75.595,-73.711,-75.293,-73.639,-75.044,-73.645,-74.855,-73.658,-74.594,-73.715,-74.345,-73.684,-74.197,-73.696,-73.996,-73.7,-72.929,-73.448,-72.687,-73.452,-72.381,-73.438,-71.994,-73.379,-71.698,-73.353,-71.453,-73.354,-71.017,-73.263,-70.323,-73.274,-69.969,-73.226,-69.282,-73.17,-68.821,-73.105,-68,-72.936,-67.667,-72.835,-67.307,-72.611,-67.08,-72.388,-66.828,-72.09,-66.952,-71.897,-67.084,-71.812,-67.196,-71.719,-67.46,-71.527,-67.53,-71.285,-67.505,-71.058,-67.598,-70.845,-67.692,-70.686,-67.888,-70.422,-68.126,-70.25,-68.403,-70.02,-68.404,-69.809,-68.47,-69.644,-68.638,-69.526,-68.708,-69.432,-68.58,-69.413,-68.462,-69.384,-68.141,-69.348,-67.372,-69.412,-67.304,-69.318,-67.11,-69.248,-66.975,-69.161,-67.021,-69.029,-67.188,-68.974,-67.391,-68.861,-67.299,-68.771,-67.134,-68.771,-67.054,-68.671,-67.117,-68.575,-67.041,-68.453,-66.894,-68.298,-66.793,-68.24,-66.978,-68.147,-67.15,-68.025,-67.107,-67.93,-67.021,-67.831,-66.915,-67.693,-66.77,-67.593,-66.677,-67.56,-66.705,-67.527,-66.923,-67.492,-67.124,-67.485,-67.487,-67.547,-67.545,-67.535,-67.565,-67.503,-67.586,-67.435,-67.55,-67.269,-67.493,-67.113,-67.44,-67.091,-67.299,-67.071,-67.16,-66.952,-67.034,-66.945,-66.955,-66.985,-66.929,-67.144,-66.886,-67.18,-66.902,-67.256,-66.836,-67.282,-66.757,-67.233,-66.61,-67.209,-66.552,-67.263,-66.499,-67.289,-66.472,-67.243,-66.491,-67.114,-66.515,-67.062,-66.533,-66.979,-66.502,-66.94,-66.465,-66.875,-66.527,-66.741,-66.504,-66.69,-66.371,-66.609,-66.307,-66.592,-66.182,-66.592,-65.954,-66.646,-65.847,-66.65,-65.766,-66.625,-65.718,-66.573,-65.678,-66.403,-65.776,-66.343,-65.775,-66.288,-65.717,-66.254,-65.617,-66.135,-65.465,-66.129,-65.316,-66.14,-65.172,-66.117,-65.222,-66.068,-65.267,-65.994,-65.105,-65.958,-64.999,-65.946,-64.722,-65.993,-64.614,-66.019,-64.514,-65.96,-64.547,-65.9,-64.653,-65.867,-64.673,-65.814,-64.647,-65.748,-64.475,-65.781,-64.435,-65.768,-64.39,-65.708,-64.417,-65.68,-64.439,-65.641,-64.213,-65.633,-64.18,-65.617,-64.132,-65.571,-64.066,-65.554,-63.862,-65.556,-63.818,-65.532,-63.798,-65.48,-63.908,-65.467,-64.051,-65.417,-64.071,-65.278,-64.038,-65.179,-63.912,-65.093,-63.76,-65.033,-63.482,-65.085,-63.264,-65.073,-63.178,-65.126,-63.059,-65.139,-63.033,-65.08,-63.086,-65.028,-63.12,-64.942,-62.775,-64.842,-62.665,-64.858,-62.528,-64.833,-62.576,-64.756,-62.503,-64.656,-62.404,-64.643,-62.338,-64.729,-62.243,-64.747,-62.14,-64.727,-61.883,-64.625,-61.756,-64.61,-61.632,-64.605,-61.5,-64.546,-61.47,-64.476,-61.396,-64.427,-61.174,-64.362,-61.082,-64.315,-60.887,-64.15,-60.922,-64.108,-60.864,-64.073,-60.277,-63.924,-59.99,-63.91,-59.51,-63.821,-59.218,-63.714,-59.036,-63.67,-58.872,-63.552,-58.674,-63.534,-58.216,-63.451,-57.868,-63.319,-57.39,-63.226,-57.168,-63.235,-57.077,-63.262,-57.021,-63.373]],[[-45.223,-78.811,-45.092,-78.814,-44.566,-78.804,-44.041,-78.807,-43.722,-78.818,-43.627,-78.846,-43.544,-78.902,-43.451,-78.99,-43.364,-79.085,-43.267,-79.163,-43.211,-79.3,-43.119,-79.35,-42.965,-79.477,-42.945,-79.579,-42.99,-79.801,-43.066,-79.891,-43.267,-79.979,-43.496,-79.969,-43.6,-79.974,-43.704,-79.99,-43.742,-80.003,-43.758,-80.021,-43.49,-80.096,-43.459,-80.123,-43.454,-80.155,-43.489,-80.178,-43.528,-80.191,-49.187,-80.643,-49.41,-80.667,-49.63,-80.712,-49.701,-80.753,-49.773,-80.784,-54.162,-80.87,-54.202,-80.864,-54.241,-80.847,-54.351,-80.76,-54.372,-80.624,-54.347,-80.569,-54.13,-80.517,-54.045,-80.487,-53.676,-80.284,-53.482,-80.189,-53.394,-80.109,-53.346,-80.114,-53.176,-80.161,-53.053,-80.175,-52.807,-80.156,-52.567,-80.099,-52.461,-80.067,-52.357,-80.078,-52.338,-80.126,-52.297,-80.141,-51.711,-79.99,-51.184,-79.82,-50.664,-79.627,-50.402,-79.512,-50.339,-79.479,-50.295,-79.43,-50.331,-79.381,-50.379,-79.338,-50.42,-79.321,-50.464,-79.313,-50.733,-79.283,-50.649,-79.233,-50.573,-79.172,-50.52,-79.104,-50.502,-79.022,-50.514,-78.98,-50.502,-78.95,-50.38,-78.923,-50.298,-78.882,-50.242,-78.833,-50.335,-78.818,-50.377,-78.78,-50.294,-78.696,-50.22,-78.605,-50.142,-78.557,-49.94,-78.462,-49.354,-78.222,-49.144,-78.094,-49.081,-78.047,-47.692,-77.84,-47.463,-77.819,-47.03,-77.791,-46.826,-77.785,-46.258,-77.805,-45.993,-77.827,-45.53,-77.881,-44.852,-77.988,-44.594,-78.035,-44.34,-78.093,-44.094,-78.167,-43.854,-78.258,-43.809,-78.287,-43.785,-78.336,-43.777,-78.385,-43.788,-78.433,-43.852,-78.53,-43.947,-78.598,-45.068,-78.661,-45.213,-78.687,-45.295,-78.74,-45.352,-78.791,-45.223,-78.811]],[[-59.734,-80.344,-59.774,-80.558,-59.771,-80.657,-59.825,-80.733,-59.926,-80.774,-60.125,-80.841,-60.268,-80.881,-60.583,-80.948,-62.023,-80.889,-62.671,-80.834,-62.94,-80.766,-62.986,-80.735,-63.068,-80.627,-63.144,-80.595,-63.715,-80.617,-64.065,-80.65,-64.126,-80.668,-64.22,-80.734,-64.268,-80.749,-65.203,-80.607,-66.184,-80.442,-66.591,-80.358,-66.734,-80.319,-66.771,-80.294,-66.681,-80.26,-66.588,-80.239,-66.483,-80.224,-66.377,-80.222,-66.296,-80.235,-66.217,-80.258,-66.168,-80.346,-66.115,-80.361,-65.98,-80.384,-62.519,-80.373,-62.232,-80.369,-61.633,-80.344,-61.313,-80.307,-61.194,-80.257,-61.485,-80.244,-61.597,-80.206,-61.694,-80.134,-61.717,-80.069,-61.684,-80.02,-61.302,-79.996,-61.246,-79.978,-61.346,-79.951,-61.343,-79.887,-61.115,-79.862,-61.026,-79.809,-60.579,-79.741,-59.873,-79.777,-59.706,-79.875,-59.752,-79.938,-59.786,-80.001,-59.788,-80.101,-59.498,-80.115,-59.408,-80.151,-59.322,-80.196,-59.427,-80.198,-59.53,-80.208,-59.612,-80.255,-59.683,-80.315,-59.734,-80.344]],[[-70.051,-69.189,-70.079,-69.311,-69.913,-69.267,-69.855,-69.277,-69.708,-69.321,-69.417,-69.583,-69.353,-69.666,-69.234,-69.909,-69.091,-70.09,-68.731,-70.408,-68.554,-70.581,-68.459,-70.683,-68.451,-70.818,-68.336,-70.856,-68.314,-70.912,-68.278,-71.097,-68.252,-71.313,-68.228,-71.725,-68.241,-71.822,-68.394,-71.975,-68.461,-72.085,-68.543,-72.158,-68.64,-72.21,-69.149,-72.427,-69.209,-72.534,-70.063,-72.626,-70.543,-72.664,-70.731,-72.623,-70.923,-72.613,-71.159,-72.627,-71.846,-72.639,-72.367,-72.67,-72.433,-72.658,-72.48,-72.617,-72.531,-72.59,-72.67,-72.596,-72.78,-72.581,-72.888,-72.547,-73.007,-72.484,-73.057,-72.448,-73.086,-72.408,-72.855,-72.304,-72.737,-72.281,-72.618,-72.275,-72.376,-72.296,-72.135,-72.331,-71.605,-72.359,-70.873,-72.366,-70.671,-72.356,-70.428,-72.323,-70.206,-72.228,-70.314,-72.191,-70.424,-72.168,-70.533,-72.163,-70.641,-72.17,-70.945,-72.229,-71.178,-72.264,-71.413,-72.284,-71.661,-72.25,-71.892,-72.153,-71.898,-72.121,-71.107,-72.047,-71.034,-72.035,-70.891,-71.987,-70.845,-71.946,-70.821,-71.907,-71.355,-71.836,-71.465,-71.838,-71.574,-71.851,-71.816,-71.822,-72.046,-71.74,-72.259,-71.641,-72.337,-71.632,-72.412,-71.662,-72.928,-71.922,-72.972,-71.924,-73.167,-71.905,-73.41,-71.853,-73.633,-71.835,-73.776,-71.849,-73.83,-71.87,-73.691,-71.929,-73.572,-71.981,-73.537,-72.022,-73.899,-72.152,-73.996,-72.17,-74.153,-72.159,-74.209,-72.142,-74.322,-72.073,-74.429,-72.056,-74.663,-72.07,-74.786,-72.064,-74.908,-72.033,-75.024,-71.988,-75.13,-71.964,-75.259,-71.914,-75.353,-71.878,-75.383,-71.828,-75.373,-71.78,-75.331,-71.752,-75.325,-71.726,-75.353,-71.68,-75.335,-71.645,-75.293,-71.615,-75.1,-71.555,-74.863,-71.543,-74.636,-71.617,-74.487,-71.642,-74.419,-71.643,-74.392,-71.638,-74.373,-71.618,-74.38,-71.579,-74.42,-71.507,-74.425,-71.457,-74.375,-71.415,-74.308,-71.4,-74.236,-71.388,-74.187,-71.383,-74.041,-71.41,-73.937,-71.438,-73.724,-71.517,-73.545,-71.573,-73.479,-71.579,-73.427,-71.559,-73.38,-71.528,-73.592,-71.448,-73.617,-71.397,-73.604,-71.351,-73.474,-71.325,-73.397,-71.321,-73.02,-71.369,-72.821,-71.384,-72.622,-71.388,-72.212,-71.335,-72.43,-71.275,-72.905,-71.223,-72.995,-71.187,-73.06,-71.127,-72.71,-71.073,-72.356,-71.075,-71.719,-71.145,-71.504,-71.112,-71.308,-71.011,-71.194,-70.985,-70.741,-70.993,-70.381,-70.946,-70.323,-70.951,-70.268,-70.965,-69.916,-71.134,-69.87,-71.126,-69.835,-71.093,-69.823,-71.034,-69.823,-70.973,-69.83,-70.914,-69.876,-70.876,-69.933,-70.88,-69.993,-70.897,-70.094,-70.883,-70.196,-70.851,-70.299,-70.836,-70.661,-70.818,-70.917,-70.786,-71.049,-70.762,-71.173,-70.713,-71.19,-70.66,-71.061,-70.537,-70.562,-70.404,-70.328,-70.361,-70.09,-70.351,-69.975,-70.36,-69.702,-70.415,-69.66,-70.412,-69.618,-70.398,-69.883,-70.305,-70.118,-70.234,-70.234,-70.18,-70.328,-70.16,-70.72,-70.139,-70.926,-70.192,-71.024,-70.201,-71.121,-70.196,-71.696,-70.068,-71.729,-70.054,-71.81,-70.005,-71.854,-69.969,-71.879,-69.909,-71.868,-69.847,-71.852,-69.807,-71.767,-69.649,-71.718,-69.524,-71.743,-69.423,-71.834,-69.367,-71.963,-69.329,-72.081,-69.267,-72.114,-69.225,-72.135,-69.177,-72.138,-69.115,-72.109,-69.06,-72.058,-69.001,-71.99,-68.971,-71.869,-68.941,-71.392,-68.874,-70.417,-68.789,-70.312,-68.832,-70.154,-68.923,-70.105,-68.959,-70.053,-69.14,-70.051,-69.189]],[[-98.091,-71.912,-98.176,-72.018,-98.168,-72.123,-97.923,-72.117,-97.816,-71.919,-97.585,-71.883,-97.473,-72,-97.582,-72.095,-97.526,-72.149,-97.46,-72.188,-97.345,-72.189,-97.242,-72.132,-97.196,-72.091,-97.155,-72.045,-97.089,-71.944,-96.869,-71.851,-96.383,-71.836,-96.125,-71.896,-96.298,-72.045,-96.715,-72.132,-96.979,-72.222,-96.89,-72.247,-96.799,-72.259,-96.718,-72.255,-96.482,-72.208,-95.906,-72.122,-95.685,-72.057,-95.609,-72.068,-95.61,-72.175,-95.531,-72.249,-95.575,-72.41,-95.826,-72.439,-96.078,-72.454,-96.014,-72.525,-96.03,-72.554,-96.052,-72.577,-96.693,-72.548,-96.804,-72.558,-96.915,-72.578,-97.028,-72.574,-97.25,-72.521,-97.366,-72.522,-97.596,-72.548,-97.828,-72.557,-98.163,-72.556,-98.408,-72.548,-98.641,-72.49,-98.882,-72.473,-99.149,-72.472,-99.434,-72.407,-99.672,-72.38,-100.014,-72.312,-100.104,-72.287,-100.195,-72.273,-100.357,-72.278,-101.602,-72.176,-101.785,-72.178,-101.903,-72.19,-102.022,-72.185,-102.265,-72.135,-102.314,-72.081,-102.288,-72.032,-102.237,-72.009,-102.128,-71.985,-100.401,-71.866,-100.219,-71.833,-100.085,-71.837,-99.985,-71.939,-99.833,-72.046,-99.784,-72.044,-99.735,-72.033,-99.563,-71.945,-99.254,-71.972,-99.082,-71.933,-98.965,-71.854,-98.615,-71.764,-98.394,-71.782,-98.189,-71.82,-98.091,-71.912]],[[-120.556,-73.756,-120.378,-73.856,-120.312,-73.922,-120.272,-73.989,-120.99,-74.157,-121.019,-74.173,-121.054,-74.26,-121.036,-74.279,-121.005,-74.293,-121.002,-74.326,-121.062,-74.337,-122.287,-74.403,-122.859,-74.343,-122.938,-74.302,-122.956,-74.24,-122.891,-74.227,-122.765,-74.219,-122.794,-74.19,-122.875,-74.141,-122.881,-74.099,-122.71,-73.994,-122.625,-73.966,-122.951,-73.867,-122.992,-73.844,-123.035,-73.838,-123.191,-73.849,-123.346,-73.843,-123.292,-73.803,-123.249,-73.739,-123.112,-73.682,-123.012,-73.673,-122.91,-73.68,-122.436,-73.682,-121.967,-73.712,-121.497,-73.733,-120.722,-73.752,-120.556,-73.756]],[[-126.33,-73.286,-126.065,-73.315,-125.976,-73.357,-125.857,-73.388,-125.736,-73.406,-125.627,-73.453,-125.561,-73.536,-125.504,-73.562,-125.326,-73.618,-125.264,-73.666,-125.276,-73.691,-125.612,-73.711,-125.723,-73.703,-125.828,-73.718,-125.86,-73.749,-125.857,-73.78,-125.799,-73.802,-125.674,-73.822,-125.552,-73.82,-125.327,-73.796,-125.224,-73.801,-125.109,-73.826,-124.993,-73.83,-124.694,-73.75,-124.617,-73.735,-124.54,-73.74,-124.129,-73.834,-124.042,-73.88,-124.101,-73.907,-124.152,-73.944,-124.129,-73.971,-123.932,-74.008,-123.851,-74.057,-123.8,-74.076,-123.811,-74.117,-123.839,-74.168,-123.837,-74.226,-123.937,-74.256,-123.982,-74.256,-124.199,-74.226,-124.873,-74.208,-125.09,-74.182,-125.421,-74.07,-125.549,-74.063,-125.683,-74.035,-125.887,-73.955,-126.244,-73.891,-126.472,-73.812,-126.466,-73.746,-126.496,-73.7,-126.538,-73.68,-126.583,-73.67,-126.711,-73.654,-126.838,-73.657,-126.902,-73.677,-127.006,-73.726,-127.122,-73.734,-127.212,-73.724,-127.232,-73.713,-127.233,-73.586,-127.332,-73.567,-127.414,-73.516,-127.429,-73.447,-127.394,-73.382,-127.268,-73.304,-127.124,-73.294,-126.978,-73.308,-126.83,-73.291,-126.597,-73.279,-126.33,-73.286]],[[-159.053,-79.807,-160.302,-79.845,-160.807,-79.812,-161.866,-79.704,-163.317,-79.505,-163.712,-79.442,-163.971,-79.389,-164.226,-79.321,-164.282,-79.246,-164.245,-79.138,-164.2,-79.051,-164.126,-78.995,-163.815,-78.929,-163.66,-78.856,-163.345,-78.78,-163.256,-78.722,-163.124,-78.719,-162.873,-78.725,-162.622,-78.742,-162.39,-78.76,-162.161,-78.793,-161.643,-78.901,-161.283,-79.007,-160.764,-79.132,-160.25,-79.271,-159.964,-79.324,-159.684,-79.402,-159.419,-79.508,-159.366,-79.545,-159.256,-79.591,-159.19,-79.637,-159.119,-79.675,-159.051,-79.695,-158.997,-79.735,-159.01,-79.78,-159.053,-79.807]],[[167.084,-77.322,167.461,-77.394,168.451,-77.386,169.276,-77.455,169.353,-77.525,169.117,-77.561,168.755,-77.653,168.519,-77.681,168.323,-77.683,167.918,-77.644,167.392,-77.649,167.279,-77.703,167.025,-77.756,166.729,-77.851,166.65,-77.774,166.533,-77.7,166.237,-77.547,166.217,-77.525,166.378,-77.494,166.458,-77.444,166.626,-77.377,166.607,-77.336,166.469,-77.289,166.413,-77.252,166.506,-77.189,166.716,-77.162,166.987,-77.187,167.107,-77.271,167.085,-77.307,167.084,-77.322]],[[-31.119,-79.798,-30.985,-79.818,-30.841,-79.771,-30.861,-79.726,-30.78,-79.647,-30.66,-79.733,-29.871,-79.823,-29.614,-79.91,-29.721,-79.93,-29.8,-79.926,-30.029,-79.936,-30.422,-80.011,-30.844,-79.938,-31.594,-79.888,-31.824,-79.85,-32,-79.732,-31.68,-79.634,-31.605,-79.645,-31.119,-79.798]],[[-33.934,-79.32,-34.05,-79.357,-36.481,-79.294,-36.601,-79.283,-36.566,-79.209,-36.238,-79.196,-36.048,-79.181,-35.79,-79.149,-35.597,-79.092,-35.535,-79.09,-34.392,-79.223,-33.995,-79.279,-33.947,-79.305,-33.934,-79.32]],[[-70.334,-79.68,-70.553,-79.683,-70.984,-79.674,-71.414,-79.64,-71.526,-79.624,-71.687,-79.568,-71.735,-79.539,-71.778,-79.501,-71.784,-79.444,-71.667,-79.246,-71.454,-79.129,-71.254,-79.06,-70.626,-78.902,-70.544,-78.884,-69.972,-78.809,-69.748,-78.769,-69.398,-78.686,-67.479,-78.362,-67.038,-78.316,-66.84,-78.35,-66.728,-78.384,-66.787,-78.422,-67.046,-78.514,-67.166,-78.57,-67.481,-78.682,-68.157,-78.871,-68.638,-79.013,-69.251,-79.21,-69.394,-79.28,-69.686,-79.443,-69.634,-79.518,-69.732,-79.618,-70.116,-79.666,-70.334,-79.68]],[[-57.846,-64.054,-57.809,-64.068,-57.774,-64.062,-57.741,-64.048,-57.71,-64.015,-57.593,-63.967,-57.48,-63.962,-57.517,-64.011,-57.249,-64.097,-57.273,-64.166,-57.222,-64.221,-57.328,-64.238,-57.414,-64.296,-57.338,-64.318,-57.295,-64.367,-57.388,-64.379,-57.581,-64.35,-57.683,-64.357,-57.671,-64.311,-57.703,-64.293,-57.823,-64.302,-57.871,-64.401,-57.91,-64.41,-57.952,-64.394,-57.921,-64.331,-57.971,-64.32,-58.022,-64.322,-58.169,-64.369,-58.214,-64.37,-58.304,-64.315,-58.02,-64.242,-58.138,-64.206,-58.162,-64.161,-58.147,-64.097,-58.25,-64.107,-58.352,-64.131,-58.398,-64.135,-58.438,-64.113,-58.425,-64.068,-58.342,-63.994,-58.275,-63.916,-58.146,-63.878,-58.07,-63.847,-57.971,-63.835,-57.926,-63.806,-57.831,-63.804,-57.779,-63.868,-57.781,-63.907,-57.827,-63.949,-57.846,-64.054]],[[-55.528,-63.174,-55.466,-63.2,-55.216,-63.199,-55.156,-63.205,-55.106,-63.249,-55.075,-63.324,-55.157,-63.353,-55.594,-63.336,-55.75,-63.297,-55.83,-63.298,-56.009,-63.342,-56.083,-63.383,-56.379,-63.437,-56.463,-63.418,-56.499,-63.358,-56.505,-63.334,-56.475,-63.318,-56.461,-63.302,-56.466,-63.283,-56.385,-63.234,-56.042,-63.157,-55.59,-63.128,-55.529,-63.157,-55.528,-63.174]],[[-57.978,-61.912,-57.849,-61.94,-57.738,-61.921,-57.677,-61.942,-57.637,-61.998,-57.64,-62.02,-57.807,-62.012,-57.963,-62.078,-58.148,-62.063,-58.172,-62.118,-58.133,-62.146,-58.183,-62.17,-58.341,-62.119,-58.467,-62.137,-58.507,-62.226,-58.562,-62.244,-58.594,-62.248,-58.644,-62.225,-58.746,-62.218,-58.755,-62.206,-58.819,-62.171,-59.004,-62.21,-58.955,-62.164,-58.71,-62.045,-58.684,-62.008,-58.399,-61.938,-58.265,-61.953,-57.978,-61.912]],[[-63.181,-64.47,-63.277,-64.573,-63.131,-64.572,-63.032,-64.535,-62.928,-64.519,-62.837,-64.572,-63.026,-64.611,-63.203,-64.68,-63.275,-64.717,-63.355,-64.734,-63.458,-64.727,-63.558,-64.734,-63.647,-64.803,-63.74,-64.834,-63.77,-64.808,-63.804,-64.792,-64.007,-64.769,-64.099,-64.733,-64.184,-64.71,-64.272,-64.698,-64.226,-64.635,-64.171,-64.582,-63.867,-64.51,-63.897,-64.487,-63.916,-64.457,-63.674,-64.421,-63.668,-64.384,-63.683,-64.343,-63.606,-64.314,-63.534,-64.273,-63.486,-64.261,-63.334,-64.266,-63.23,-64.324,-63.271,-64.381,-63.181,-64.47]],[[-62.326,-64.424,-62.396,-64.465,-62.455,-64.472,-62.508,-64.454,-62.58,-64.514,-62.727,-64.496,-62.782,-64.479,-62.747,-64.472,-62.721,-64.445,-62.643,-64.392,-62.504,-64.253,-62.48,-64.211,-62.59,-64.14,-62.611,-64.116,-62.586,-64.076,-62.545,-64.046,-62.451,-64.012,-62.329,-64.013,-62.268,-64.04,-62.269,-64.09,-62.058,-64.138,-62.094,-64.235,-62.174,-64.296,-62.186,-64.369,-62.304,-64.401,-62.326,-64.424]],[[-67.988,-67.474,-68.093,-67.539,-68.175,-67.558,-68.25,-67.54,-68.325,-67.532,-68.381,-67.555,-68.439,-67.656,-68.507,-67.707,-68.58,-67.733,-68.622,-67.723,-68.664,-67.723,-68.734,-67.746,-68.818,-67.753,-68.901,-67.744,-68.982,-67.68,-69.098,-67.603,-69.12,-67.578,-69.138,-67.515,-69.133,-67.453,-69.082,-67.403,-68.82,-67.234,-68.734,-67.157,-68.656,-67.07,-68.575,-66.993,-68.417,-66.853,-68.336,-66.802,-67.938,-66.657,-67.831,-66.624,-67.711,-66.633,-67.681,-66.709,-67.741,-66.746,-67.932,-66.845,-67.969,-66.982,-67.968,-67.032,-67.949,-67.045,-67.876,-67.062,-67.828,-67.082,-67.761,-67.123,-67.688,-67.147,-67.848,-67.219,-67.956,-67.255,-68.03,-67.3,-68.175,-67.344,-68.235,-67.372,-68.144,-67.382,-68.007,-67.418,-67.969,-67.45,-67.988,-67.474]],[[-73.707,-70.635,-73.55,-70.723,-73.695,-70.794,-74.205,-70.924,-74.505,-70.973,-74.806,-71.012,-76.176,-71.132,-76.271,-71.133,-76.364,-71.117,-76.421,-71.09,-76.512,-70.991,-76.5,-70.941,-76.378,-70.894,-76.249,-70.864,-76.035,-70.836,-75.21,-70.773,-75.127,-70.752,-75.06,-70.706,-75.038,-70.651,-75.007,-70.609,-74.954,-70.59,-74.898,-70.591,-74.79,-70.631,-74.59,-70.792,-74.527,-70.77,-74.469,-70.727,-74.456,-70.587,-74.401,-70.576,-74.225,-70.615,-74.115,-70.655,-74.113,-70.577,-74.038,-70.553,-73.958,-70.561,-73.879,-70.578,-73.707,-70.635]],[[-74.987,-69.728,-74.81,-69.752,-74.55,-69.861,-74.465,-69.917,-74.438,-69.95,-74.46,-69.972,-74.578,-69.998,-74.672,-70.132,-74.849,-70.179,-75.268,-70.149,-75.727,-70.096,-75.764,-70.085,-75.804,-70.038,-75.813,-69.984,-75.76,-69.916,-75.681,-69.882,-75.34,-69.84,-75.314,-69.817,-75.265,-69.749,-75.179,-69.735,-75.136,-69.741,-74.987,-69.728]],[[-74.354,-73.098,-74.499,-73.229,-74.522,-73.244,-74.668,-73.275,-74.615,-73.311,-74.575,-73.328,-74.551,-73.369,-74.467,-73.427,-74.366,-73.464,-74.452,-73.565,-74.575,-73.611,-75.901,-73.333,-76.003,-73.288,-76.053,-73.255,-76.09,-73.203,-76.096,-73.15,-76.062,-73.109,-76.018,-73.085,-75.898,-73.056,-75.775,-73.054,-75.506,-73.109,-75.468,-73.101,-75.417,-73.052,-75.276,-73.05,-75.244,-73.009,-75.439,-72.994,-75.6,-72.953,-75.702,-72.911,-75.731,-72.879,-75.377,-72.82,-74.474,-72.894,-74.336,-72.919,-74.276,-72.951,-74.224,-72.997,-74.354,-73.098]],[[-91.161,-73.182,-91.344,-73.207,-91.511,-73.196,-91.45,-72.968,-91.357,-72.909,-91.382,-72.868,-91.551,-72.754,-91.67,-72.624,-91.612,-72.594,-91.304,-72.547,-90.947,-72.556,-90.807,-72.611,-90.763,-72.681,-90.78,-72.732,-90.895,-72.824,-90.776,-72.854,-90.751,-72.917,-90.776,-72.993,-90.893,-73.084,-90.998,-73.137,-91.161,-73.182]],[[167.643,-78.141,167.517,-78.216,167.377,-78.249,166.936,-78.222,166.626,-78.284,166.285,-78.306,166.122,-78.275,166.051,-78.213,166.013,-78.131,166.013,-78.102,166.111,-78.09,166.567,-78.148,166.76,-78.198,166.864,-78.196,167.138,-78.13,167.364,-78.046,167.422,-78.006,167.498,-77.992,167.594,-78.022,167.64,-78.112,167.643,-78.141]],[[100.981,-65.678,100.547,-65.701,100.512,-65.675,100.351,-65.673,100.293,-65.651,100.27,-65.603,100.324,-65.521,100.409,-65.466,100.545,-65.409,100.607,-65.396,100.883,-65.378,101.079,-65.403,101.221,-65.472,101.259,-65.528,101.238,-65.565,100.981,-65.678]],[[26.857,-70.381,26.793,-70.419,26.609,-70.412,26.47,-70.448,26.358,-70.434,26.005,-70.373,25.964,-70.295,25.954,-70.261,25.983,-70.2,26.301,-70.072,26.426,-70.061,26.605,-70.078,26.686,-70.114,26.737,-70.186,26.875,-70.33,26.857,-70.381]],[[-5.894,-70.552,-6.156,-70.612,-6.181,-70.586,-6.266,-70.55,-6.438,-70.453,-6.244,-70.446,-6.068,-70.405,-5.972,-70.421,-5.95,-70.432,-5.894,-70.552]],[[-3.28,-70.534,-3.442,-70.535,-3.49,-70.508,-3.497,-70.488,-3.287,-70.344,-3.173,-70.307,-2.95,-70.28,-2.805,-70.288,-2.714,-70.32,-2.684,-70.376,-2.683,-70.462,-2.738,-70.507,-3.28,-70.534]],[[-66.174,-80.078,-66.267,-80.081,-66.319,-80.075,-66.367,-80.054,-66.41,-79.973,-66.904,-79.909,-66.962,-79.873,-66.994,-79.793,-67.077,-79.762,-67.719,-79.62,-67.771,-79.589,-67.809,-79.546,-67.688,-79.528,-67.438,-79.56,-66.979,-79.569,-66.881,-79.582,-66.785,-79.608,-66.274,-79.612,-66.014,-79.624,-65.87,-79.738,-65.579,-79.771,-65.54,-79.837,-65.504,-79.954,-65.899,-80.041,-65.989,-80.054,-66.174,-80.078]],[[-67.262,-79.453,-67.434,-79.501,-68.161,-79.479,-68.408,-79.464,-68.549,-79.437,-68.422,-79.333,-68.324,-79.298,-68.233,-79.285,-68.033,-79.227,-67.714,-79.214,-67.475,-79.223,-67.069,-79.268,-67.173,-79.312,-67.24,-79.328,-67.305,-79.394,-67.262,-79.453]],[[-55.165,-61.22,-55.297,-61.249,-55.347,-61.212,-55.369,-61.146,-55.44,-61.106,-55.387,-61.073,-54.671,-61.117,-54.71,-61.14,-55.058,-61.169,-55.165,-61.22]],[[-55.873,-63.536,-55.957,-63.58,-56.178,-63.513,-56.235,-63.469,-56.21,-63.437,-55.858,-63.407,-55.762,-63.422,-55.719,-63.492,-55.873,-63.536]],[[-57.24,-64.567,-57.326,-64.571,-57.433,-64.54,-57.448,-64.488,-57.446,-64.46,-57.366,-64.439,-57.315,-64.435,-57.023,-64.352,-56.895,-64.333,-56.952,-64.382,-56.945,-64.427,-56.991,-64.468,-57.24,-64.567]],[[-56.06,-63.079,-56.259,-63.173,-56.354,-63.169,-56.546,-63.098,-56.601,-63.062,-56.614,-63.045,-56.489,-62.982,-56.14,-63.005,-56.062,-63.013,-56.058,-63.019,-56.051,-63.055,-56.06,-63.079]],[[-57.374,-63.807,-57.36,-63.825,-57.161,-63.816,-57.104,-63.841,-57.218,-63.876,-57.248,-63.868,-57.344,-63.879,-57.616,-63.854,-57.683,-63.813,-57.439,-63.791,-57.374,-63.807]],[[-61.998,-69.722,-62.085,-69.729,-62.172,-69.637,-62.216,-69.495,-62.496,-69.288,-62.568,-69.18,-62.516,-69.155,-62.442,-69.146,-62.239,-69.176,-62.118,-69.215,-61.978,-69.3,-61.816,-69.376,-61.784,-69.442,-61.807,-69.515,-61.911,-69.533,-61.908,-69.588,-61.97,-69.691,-61.998,-69.722]],[[-60.625,-62.56,-60.576,-62.573,-60.14,-62.549,-60.003,-62.618,-59.85,-62.615,-60.221,-62.745,-60.322,-62.708,-60.354,-62.679,-60.378,-62.617,-60.62,-62.633,-60.697,-62.621,-60.796,-62.662,-60.995,-62.679,-61.063,-62.679,-61.15,-62.634,-61.152,-62.589,-60.975,-62.592,-60.838,-62.534,-60.799,-62.475,-60.732,-62.491,-60.625,-62.56]],[[-71.985,-69.698,-72.202,-69.74,-72.345,-69.707,-72.777,-69.645,-72.957,-69.529,-72.937,-69.469,-72.857,-69.433,-72.726,-69.413,-72.464,-69.452,-72.331,-69.492,-71.985,-69.698]],[[-95.027,-72.665,-95.219,-72.67,-95.273,-72.647,-95.216,-72.599,-94.753,-72.517,-94.566,-72.468,-94.538,-72.476,-94.514,-72.492,-94.434,-72.589,-94.426,-72.613,-95.027,-72.665]],[[-104.538,-73.166,-104.66,-73.212,-104.881,-73.201,-105.053,-73.126,-105.123,-73.026,-105.132,-72.992,-105.085,-72.966,-104.972,-72.941,-104.538,-73.166]],[[-119.549,-74.11,-119.758,-74.122,-119.821,-74.12,-119.887,-74.097,-119.905,-74.082,-119.797,-74.029,-119.694,-74.006,-119.662,-73.989,-119.803,-73.815,-119.669,-73.809,-119.516,-73.775,-119.216,-73.778,-118.959,-73.809,-118.91,-73.834,-118.877,-73.878,-118.99,-73.967,-119.059,-73.998,-119.449,-74.076,-119.549,-74.11]],[[-127.366,-74.623,-127.518,-74.641,-127.817,-74.575,-127.915,-74.543,-128,-74.489,-128.07,-74.478,-128.096,-74.466,-128.133,-74.327,-128.043,-74.312,-127.853,-74.332,-127.486,-74.405,-127.23,-74.425,-127.145,-74.48,-127.232,-74.578,-127.366,-74.623]],[[-161.994,-83.119,-162.305,-83.142,-163.047,-83.097,-163.242,-83.06,-163.348,-83.022,-163.552,-82.988,-163.602,-82.969,-163.602,-82.927,-163.634,-82.902,-163.704,-82.879,-163.735,-82.857,-163.796,-82.843,-162.798,-82.865,-162.411,-82.899,-162.34,-82.923,-161.635,-83.027,-161.828,-83.043,-161.994,-83.119]],[[-160.467,-81.589,-160.571,-81.598,-163.253,-81.482,-163.766,-81.445,-163.89,-81.424,-163.939,-81.404,-163.951,-81.391,-163.93,-81.352,-163.869,-81.324,-163.201,-81.281,-162.456,-81.313,-161.559,-81.397,-160.938,-81.463,-160.617,-81.522,-160.485,-81.567,-160.467,-81.589]],[[163.976,-74.833,163.845,-74.833,163.763,-74.803,163.737,-74.734,163.742,-74.712,164.002,-74.629,164.208,-74.608,164.098,-74.732,164.059,-74.753,163.976,-74.833]],[[162.968,-75.567,162.788,-75.696,162.662,-75.692,162.591,-75.669,162.72,-75.597,162.842,-75.566,162.917,-75.557,162.968,-75.567]],[[169.844,-73.605,169.709,-73.625,169.522,-73.562,169.479,-73.539,169.659,-73.418,169.645,-73.379,169.672,-73.346,169.74,-73.32,169.783,-73.324,169.887,-73.459,169.96,-73.514,169.859,-73.568,169.844,-73.605]],[[164.834,-67.54,164.746,-67.569,164.692,-67.56,164.639,-67.5,164.696,-67.408,164.675,-67.289,164.684,-67.259,164.825,-67.326,164.85,-67.364,164.907,-67.419,164.919,-67.447,164.86,-67.504,164.834,-67.54]],[[163.302,-66.821,163.284,-66.882,163.235,-66.868,163.164,-66.819,163.09,-66.701,163.156,-66.688,163.238,-66.709,163.271,-66.768,163.299,-66.798,163.302,-66.821]],[[162.611,-66.477,162.557,-66.525,162.511,-66.52,162.303,-66.4,162.326,-66.347,162.297,-66.304,162.302,-66.265,162.311,-66.251,162.563,-66.433,162.611,-66.477]],[[96.613,-66.036,96.727,-66.061,96.932,-66.058,97.006,-66.097,97.019,-66.139,97.016,-66.164,96.934,-66.201,96.395,-66.225,96.307,-66.186,96.399,-66.08,96.5,-66.046,96.613,-66.036]],[[100.265,-66.217,100.133,-66.229,100.082,-66.203,100.076,-66.188,100.174,-66.131,100.291,-66.112,100.282,-66.18,100.265,-66.217]],[[98.846,-66.47,98.752,-66.482,98.655,-66.453,98.605,-66.4,98.596,-66.383,98.749,-66.369,98.95,-66.421,98.846,-66.47]],[[103.397,-65.445,103.337,-65.469,103.176,-65.455,103.138,-65.435,103.124,-65.338,103.113,-65.312,103.054,-65.285,102.789,-65.236,102.76,-65.168,102.796,-65.136,102.893,-65.13,103.137,-65.191,103.191,-65.237,103.182,-65.308,103.186,-65.331,103.261,-65.377,103.379,-65.426,103.397,-65.445]],[[92.601,-65.808,92.471,-65.822,92.333,-65.807,92.263,-65.76,92.248,-65.74,92.301,-65.707,92.496,-65.702,92.634,-65.731,92.665,-65.76,92.67,-65.775,92.601,-65.808]],[[85.822,-66.953,85.65,-66.98,85.622,-66.965,85.617,-66.951,85.359,-66.854,85.314,-66.776,85.34,-66.723,85.553,-66.729,85.806,-66.775,85.938,-66.894,85.822,-66.953]],[[86.542,-66.767,86.427,-66.792,86.337,-66.788,86.232,-66.733,86.278,-66.697,86.383,-66.675,86.521,-66.687,86.557,-66.706,86.652,-66.718,86.542,-66.767]],[[85.329,-66.612,85.222,-66.643,85.136,-66.637,85.08,-66.604,85.069,-66.584,85.121,-66.519,85.165,-66.522,85.194,-66.556,85.329,-66.612]],[[69.918,-71.918,69.792,-72.047,69.744,-72.044,69.693,-71.968,69.737,-71.922,69.796,-71.894,69.895,-71.908,69.918,-71.918]],[[68.462,-72.3,68.409,-72.3,68.436,-72.26,68.566,-72.19,68.667,-72.103,68.729,-72.089,68.84,-72.165,68.817,-72.229,68.67,-72.276,68.462,-72.3]],[[72.002,-70.633,71.929,-70.633,71.841,-70.622,71.726,-70.549,71.659,-70.497,71.637,-70.444,71.647,-70.336,71.705,-70.284,71.797,-70.264,71.838,-70.312,71.851,-70.368,71.88,-70.406,72,-70.457,72.056,-70.501,72.073,-70.525,72.097,-70.575,72.078,-70.609,72.002,-70.633]],[[48.546,-66.784,48.378,-66.807,48.304,-66.798,48.296,-66.774,48.294,-66.75,48.301,-66.724,48.358,-66.704,48.638,-66.701,48.751,-66.72,48.782,-66.731,48.786,-66.768,48.775,-66.778,48.546,-66.784]],[[16.223,-70.008,16.159,-70.072,15.845,-69.982,15.663,-69.955,15.614,-69.939,15.571,-69.885,15.563,-69.863,15.597,-69.828,15.699,-69.773,15.91,-69.728,16.247,-69.705,16.573,-69.723,16.625,-69.75,16.315,-69.844,16.223,-70.008]],[[3.037,-70.597,2.698,-70.624,2.623,-70.593,2.585,-70.535,2.631,-70.5,3.072,-70.382,3.193,-70.393,3.231,-70.403,3.26,-70.449,3.221,-70.519,3.171,-70.554,3.037,-70.597]],[[-2.955,-71.214,-3.061,-71.237,-3.201,-71.23,-3.309,-71.201,-3.386,-71.143,-3.404,-71.12,-3.392,-71.081,-3.399,-71.062,-3.263,-71.052,-3.213,-71.076,-3.191,-71.095,-2.955,-71.214]],[[1.299,-70.255,1.212,-70.381,1.156,-70.378,1.105,-70.304,0.99,-70.224,0.953,-70.169,0.95,-70.094,1.027,-70.05,1.315,-70.023,1.412,-70.041,1.461,-70.136,1.299,-70.255]],[[-2.533,-70.768,-2.423,-70.8,-2.256,-70.796,-2.092,-70.821,-2.119,-70.855,-2.213,-70.902,-2.293,-70.998,-2.369,-71.044,-2.607,-71.141,-2.783,-71.167,-2.825,-71.113,-2.822,-71.057,-2.805,-71.015,-2.801,-70.982,-2.963,-70.94,-2.975,-70.883,-3.007,-70.851,-3.489,-70.736,-3.575,-70.703,-3.537,-70.683,-3.04,-70.674,-2.75,-70.694,-2.533,-70.768]],[[4.526,-70.479,4.365,-70.503,4.18,-70.451,4.13,-70.417,4.076,-70.325,4.07,-70.29,4.112,-70.267,4.256,-70.241,4.495,-70.251,4.586,-70.294,4.618,-70.369,4.59,-70.433,4.526,-70.479]],[[-20.607,-73.887,-20.654,-74.105,-20.641,-74.151,-20.6,-74.197,-20.423,-74.317,-20.411,-74.408,-20.417,-74.443,-20.489,-74.493,-20.737,-74.481,-20.818,-74.455,-20.846,-74.438,-20.977,-74.225,-21.051,-74.176,-21.167,-74.133,-21.61,-74.092,-22.035,-74.107,-21.93,-74.057,-21.288,-73.989,-21.126,-73.94,-21.025,-73.88,-20.979,-73.79,-20.867,-73.677,-20.69,-73.625,-20.58,-73.619,-20.521,-73.712,-20.521,-73.798,-20.607,-73.887]],[[-16.104,-72.679,-16.175,-72.703,-16.318,-72.702,-16.453,-72.652,-16.51,-72.582,-16.517,-72.531,-16.455,-72.474,-16.356,-72.459,-16.303,-72.478,-16.173,-72.6,-16.104,-72.679]],[[-12.509,-72.173,-12.588,-72.196,-12.72,-72.188,-12.888,-72.137,-12.944,-72.099,-12.963,-72.064,-12.915,-72.015,-12.875,-72.001,-12.789,-72.007,-12.637,-72.071,-12.535,-72.14,-12.509,-72.173]],[[-32.343,-79.674,-32.515,-79.683,-32.583,-79.658,-32.501,-79.592,-32.377,-79.535,-32.15,-79.53,-31.933,-79.568,-31.957,-79.604,-32.001,-79.607,-32.343,-79.674]],[[-54.071,-61.299,-54.115,-61.308,-54.184,-61.27,-54.192,-61.247,-54.122,-61.202,-54.05,-61.142,-54.024,-61.135,-54.041,-61.255,-54.071,-61.299]],[[-58.838,-62.303,-59.059,-62.348,-59.175,-62.302,-59.202,-62.283,-59.064,-62.239,-58.991,-62.249,-58.962,-62.264,-58.879,-62.268,-58.838,-62.303]],[[-61.158,-69.976,-61.309,-69.978,-61.378,-69.95,-61.404,-69.933,-61.386,-69.893,-61.327,-69.856,-61.152,-69.883,-61.108,-69.955,-61.158,-69.976]],[[-60.656,-68.768,-60.693,-68.795,-60.82,-68.778,-60.894,-68.759,-61.015,-68.71,-60.947,-68.681,-60.814,-68.688,-60.705,-68.722,-60.656,-68.768]],[[-60.741,-70.711,-60.826,-70.711,-60.896,-70.69,-60.958,-70.629,-60.976,-70.599,-60.942,-70.533,-60.884,-70.518,-60.554,-70.509,-60.452,-70.544,-60.449,-70.603,-60.488,-70.647,-60.741,-70.711]],[[-60.552,-71.053,-60.652,-71.059,-60.79,-71.041,-60.906,-71.007,-60.946,-70.967,-60.889,-70.934,-60.783,-70.914,-60.613,-70.92,-60.533,-70.962,-60.516,-71,-60.536,-71.041,-60.552,-71.053]],[[-59.389,-62.444,-59.525,-62.451,-59.619,-62.395,-59.661,-62.354,-59.479,-62.352,-59.396,-62.367,-59.353,-62.413,-59.389,-62.444]],[[-60.505,-62.967,-60.555,-62.978,-60.62,-62.969,-60.618,-62.987,-60.564,-63.009,-60.622,-63.018,-60.693,-62.996,-60.74,-62.949,-60.706,-62.906,-60.637,-62.895,-60.505,-62.967]],[[-60.653,-63.867,-60.778,-63.902,-60.852,-63.891,-60.972,-63.849,-60.81,-63.837,-60.797,-63.717,-60.715,-63.669,-60.562,-63.696,-60.656,-63.759,-60.689,-63.808,-60.655,-63.85,-60.653,-63.867]],[[-61.952,-64.077,-62.044,-64.08,-62.021,-64.027,-61.936,-63.99,-61.798,-63.967,-61.886,-64.027,-61.911,-64.054,-61.952,-64.077]],[[-63.316,-64.861,-63.474,-64.907,-63.558,-64.906,-63.459,-64.796,-63.367,-64.792,-63.219,-64.73,-63.177,-64.739,-63.257,-64.791,-63.316,-64.861]],[[-62.615,-63.069,-62.655,-63.074,-62.639,-63.032,-62.527,-62.924,-62.317,-62.874,-62.344,-62.918,-62.411,-62.972,-62.615,-63.069]],[[-65.845,-65.842,-66.064,-65.881,-66.175,-65.867,-66.181,-65.826,-66.153,-65.774,-66.05,-65.745,-66.067,-65.666,-66,-65.633,-65.968,-65.571,-65.834,-65.527,-65.637,-65.548,-65.668,-65.626,-65.67,-65.653,-65.784,-65.674,-65.814,-65.687,-65.841,-65.738,-65.836,-65.814,-65.845,-65.842]],[[-66.595,-66.201,-66.819,-66.313,-66.85,-66.305,-66.868,-66.294,-66.867,-66.275,-66.792,-66.234,-66.779,-66.111,-66.631,-66.067,-66.575,-66.082,-66.623,-66.134,-66.593,-66.179,-66.595,-66.201]],[[-67.362,-66.895,-67.409,-66.902,-67.521,-66.897,-67.593,-66.876,-67.5,-66.804,-67.511,-66.756,-67.426,-66.737,-67.332,-66.754,-67.269,-66.815,-67.257,-66.841,-67.362,-66.895]],[[-67.349,-67.766,-67.545,-67.785,-67.693,-67.763,-67.69,-67.688,-67.731,-67.679,-67.743,-67.661,-67.557,-67.604,-67.418,-67.591,-67.247,-67.599,-67.175,-67.625,-67.149,-67.65,-67.28,-67.712,-67.3,-67.737,-67.349,-67.766]],[[-71.695,-70.265,-71.648,-70.295,-71.432,-70.267,-71.355,-70.298,-71.34,-70.317,-71.438,-70.392,-71.551,-70.439,-71.685,-70.442,-71.782,-70.319,-71.795,-70.288,-71.695,-70.265]],[[-73.878,-73.357,-73.975,-73.376,-74.038,-73.366,-74.147,-73.315,-74.134,-73.277,-74.084,-73.249,-74.049,-73.22,-73.832,-73.113,-73.674,-73.1,-73.542,-73.124,-73.682,-73.225,-73.721,-73.296,-73.878,-73.357]],[[-93.796,-72.92,-93.966,-72.92,-94.078,-72.884,-94.113,-72.86,-94.047,-72.823,-94.004,-72.82,-93.8,-72.882,-93.756,-72.908,-93.796,-72.92]],[[-116.739,-74.165,-117.23,-74.193,-117.363,-74.161,-117.398,-74.122,-117.376,-74.083,-116.381,-73.866,-116.203,-73.896,-116.155,-73.91,-116.451,-74.018,-116.585,-74.056,-116.609,-74.069,-116.534,-74.083,-116.514,-74.096,-116.571,-74.126,-116.739,-74.165]],[[-132.391,-74.442,-132.546,-74.498,-132.857,-74.462,-132.832,-74.422,-132.552,-74.387,-132.362,-74.41,-132.391,-74.442]],[[-131.067,-74.584,-131.179,-74.605,-131.598,-74.554,-131.841,-74.543,-131.952,-74.514,-132.025,-74.489,-132.049,-74.464,-132.163,-74.426,-131.938,-74.349,-131.763,-74.324,-131.594,-74.33,-131.56,-74.367,-131.234,-74.414,-130.981,-74.414,-130.957,-74.456,-130.967,-74.515,-131.067,-74.584]],[[-147.588,-76.65,-147.579,-76.663,-147.73,-76.653,-147.954,-76.597,-148.001,-76.577,-147.9,-76.558,-147.77,-76.577,-147.649,-76.611,-147.588,-76.65]],[[-145.238,-75.711,-145.348,-75.716,-145.541,-75.693,-146.043,-75.612,-146.151,-75.574,-146.076,-75.533,-145.896,-75.505,-145.761,-75.514,-145.417,-75.588,-145.315,-75.641,-145.252,-75.683,-145.238,-75.711]],[[-146.607,-76.961,-146.981,-77.006,-147.079,-76.993,-147.044,-76.93,-147.101,-76.887,-147.116,-76.866,-147.087,-76.837,-146.867,-76.837,-146.244,-76.883,-146.164,-76.949,-146.607,-76.961]],[[-148.596,-77.007,-149.015,-77.019,-149.245,-76.993,-149.303,-76.916,-149.238,-76.9,-148.704,-76.936,-148.509,-76.955,-148.44,-76.977,-148.474,-76.998,-148.596,-77.007]],[[-149.333,-76.717,-148.928,-76.73,-148.663,-76.721,-148.384,-76.744,-148.321,-76.772,-148.371,-76.795,-148.67,-76.802,-148.815,-76.841,-148.984,-76.845,-149.238,-76.818,-149.469,-76.757,-149.333,-76.717]],[[-149.218,-77.336,-148.929,-77.387,-149.439,-77.371,-149.662,-77.301,-149.519,-77.275,-149.375,-77.28,-149.249,-77.315,-149.218,-77.336]],[[-150.233,-76.776,-150.655,-76.789,-150.83,-76.762,-150.874,-76.737,-150.838,-76.714,-150.177,-76.691,-150.104,-76.719,-150.085,-76.735,-150.233,-76.776]],[[-146.69,-76.246,-146.894,-76.261,-147.151,-76.197,-147.345,-76.147,-147.408,-76.105,-147.421,-76.09,-147.418,-76.073,-147.361,-76.063,-146.949,-76.098,-146.69,-76.246]],[[-146.79,-76.633,-146.908,-76.714,-147.221,-76.671,-147.355,-76.619,-147.279,-76.553,-147.135,-76.532,-146.947,-76.555,-146.878,-76.563,-146.79,-76.633]],[[-150.397,-77.369,-150.475,-77.374,-151.344,-77.296,-151.512,-77.273,-151.218,-77.226,-151.022,-77.22,-150.499,-77.335,-150.356,-77.349,-150.397,-77.369]],[[-153.93,-80.033,-154.114,-80.036,-154.349,-80.026,-154.529,-80,-154.942,-79.966,-155.045,-79.9,-155.525,-79.846,-155.751,-79.83,-155.674,-79.766,-155.162,-79.851,-154.535,-79.936,-154.025,-79.988,-153.93,-80.033]],[[-149.231,-77.121,-149.293,-77.137,-149.729,-77.129,-149.817,-77.114,-149.856,-77.099,-150.462,-77.076,-150.736,-77.004,-150.789,-76.982,-150.68,-76.948,-150.476,-76.926,-150.393,-76.899,-149.871,-76.875,-149.79,-76.889,-149.743,-76.927,-149.506,-77.002,-149.442,-77.049,-149.416,-77.079,-149.288,-77.093,-149.231,-77.121]],[[-157.987,-82.105,-158.076,-82.112,-158.154,-82.058,-158.545,-81.949,-158.773,-81.875,-158.926,-81.819,-158.989,-81.779,-158.914,-81.78,-158.347,-81.9,-158.261,-81.947,-157.835,-82.031,-157.987,-82.105]]]},{"n":"Sint Maarten","c":"SX","r":6,"ml":5,"lx":-63.07,"ly":18.04,"b":[-63.125,18.019,-63.011,18.069],"p":[[[-63.123,18.069,-63.011,18.069,-63.012,18.045,-63.023,18.019,-63.09,18.041,-63.125,18.064,-63.123,18.069]]]},{"n":"Tuvalu","c":"TV","r":6,"ml":5,"lx":179.21,"ly":-8.51,"b":[179.196,-8.535,179.217,-8.466],"p":[[[179.214,-8.524,179.201,-8.535,179.196,-8.535,179.201,-8.512,179.198,-8.489,179.199,-8.47,179.203,-8.466,179.212,-8.488,179.217,-8.515,179.214,-8.524]]]}]} \ No newline at end of file diff --git a/src/maps/data/lakes_50m.json b/src/maps/data/lakes_50m.json new file mode 100644 index 0000000..9f59147 --- /dev/null +++ b/src/maps/data/lakes_50m.json @@ -0,0 +1 @@ +{"f":[{"n":"Mälaren","z":2,"b":[16.044,59.268,17.98,59.621],"p":[[[17.98,59.329,17.876,59.271,17.571,59.268,17.475,59.292,17.371,59.295,17.305,59.272,17.175,59.356,17.066,59.373,16.914,59.446,16.742,59.431,16.61,59.454,16.144,59.448,16.044,59.478,16.252,59.493,16.473,59.519,16.574,59.612,16.647,59.559,16.752,59.543,16.841,59.588,16.978,59.551,17.063,59.569,17.372,59.496,17.391,59.584,17.534,59.539,17.687,59.542,17.672,59.595,17.76,59.621,17.786,59.598,17.809,59.553,17.773,59.414,17.829,59.379,17.964,59.359,17.98,59.329]]]},{"n":"Ladoga","z":1,"b":[29.825,59.907,32.944,61.775],"p":[[[29.837,61.226,29.86,61.239,29.865,61.251,29.855,61.271,29.891,61.295,29.949,61.289,30.029,61.254,30.079,61.243,30.094,61.259,30.089,61.271,30.09,61.299,30.13,61.313,30.174,61.314,30.193,61.327,30.196,61.348,30.183,61.361,30.179,61.373,30.201,61.389,30.21,61.418,30.217,61.431,30.237,61.446,30.252,61.459,30.252,61.473,30.235,61.493,30.239,61.515,30.285,61.517,30.466,61.487,30.481,61.49,30.486,61.515,30.513,61.525,30.551,61.535,30.606,61.541,30.617,61.56,30.593,61.579,30.594,61.606,30.758,61.733,30.852,61.775,30.888,61.765,30.896,61.754,30.906,61.742,31.026,61.714,31.057,61.689,31.071,61.645,31.098,61.62,31.161,61.619,31.172,61.63,31.238,61.643,31.258,61.641,31.286,61.615,31.308,61.605,31.328,61.617,31.334,61.627,31.365,61.627,31.486,61.551,31.533,61.497,31.576,61.437,31.604,61.423,31.62,61.427,31.617,61.442,31.62,61.461,31.637,61.463,31.698,61.415,31.762,61.384,31.871,61.347,31.899,61.324,32.36,61.181,32.527,61.118,32.563,61.079,32.581,61.045,32.581,61.014,32.593,60.998,32.633,60.987,32.641,60.969,32.765,60.886,32.808,60.826,32.812,60.756,32.844,60.712,32.903,60.694,32.937,60.67,32.944,60.643,32.907,60.586,32.841,60.497,32.816,60.482,32.782,60.488,32.722,60.521,32.642,60.537,32.6,60.533,32.596,60.511,32.618,60.476,32.664,60.427,32.677,60.392,32.656,60.372,32.595,60.355,32.59,60.33,32.614,60.286,32.612,60.245,32.584,60.209,32.5,60.177,32.362,60.148,32.26,60.154,32.196,60.195,32.057,60.227,31.843,60.248,31.699,60.236,31.599,60.17,31.591,60.156,31.597,60.144,31.606,60.142,31.606,60.134,31.548,60.072,31.541,60.035,31.565,59.998,31.567,59.967,31.547,59.94,31.51,59.92,31.455,59.907,31.401,59.911,31.349,59.933,31.274,59.938,31.176,59.926,31.106,59.928,31.064,59.943,31.043,59.951,31.063,59.963,31.145,60.006,31.143,60.032,31.106,60.056,31.095,60.084,31.115,60.135,31.109,60.146,31.019,60.193,30.957,60.264,30.849,60.443,30.832,60.458,30.817,60.459,30.803,60.447,30.776,60.448,30.774,60.492,30.759,60.525,30.724,60.562,30.663,60.593,30.534,60.63,30.524,60.655,30.504,60.704,30.504,60.732,30.525,60.739,30.533,60.766,30.528,60.812,30.502,60.843,30.456,60.86,30.441,60.874,30.436,60.903,30.409,60.928,30.293,61.005,30.125,61.087,29.857,61.188,29.825,61.216,29.837,61.226]]]},{"n":"Albert","z":1,"b":[30.387,1.021,31.475,2.401],"p":[[[31.472,2.385,31.396,2.28,31.384,2.261,31.387,2.2,31.4,1.896,31.387,1.857,31.369,1.836,31.343,1.834,31.322,1.803,31.305,1.742,31.268,1.688,31.166,1.605,31.129,1.588,31.04,1.564,31.014,1.546,30.969,1.527,30.893,1.448,30.785,1.309,30.727,1.224,30.719,1.191,30.594,1.041,30.569,1.029,30.539,1.021,30.507,1.041,30.501,1.05,30.504,1.121,30.492,1.173,30.492,1.203,30.502,1.212,30.495,1.23,30.478,1.239,30.468,1.258,30.45,1.259,30.414,1.255,30.4,1.256,30.387,1.297,30.441,1.382,30.479,1.467,30.529,1.532,30.591,1.579,30.674,1.676,30.714,1.702,30.868,1.859,30.936,1.915,30.982,1.932,31.179,2.073,31.259,2.153,31.303,2.226,31.344,2.291,31.431,2.388,31.475,2.401,31.472,2.385]]]},{"n":"Erie","z":1,"b":[-83.456,41.398,-78.872,43.106],"p":[[[-83.105,42.286,-83.076,42.275,-83.105,42.215,-83.117,42.12,-83.108,42.071,-83.061,42.05,-82.937,42.006,-82.834,41.997,-82.69,42.036,-82.645,42.037,-82.604,42.025,-82.567,42,-82.512,41.932,-82.496,41.944,-82.49,42.005,-82.462,42.058,-82.412,42.103,-82.311,42.158,-82.16,42.223,-82.044,42.257,-81.938,42.269,-81.929,42.279,-81.874,42.315,-81.866,42.308,-81.887,42.274,-81.887,42.257,-81.867,42.258,-81.841,42.295,-81.809,42.368,-81.73,42.447,-81.495,42.591,-81.404,42.63,-81.252,42.654,-81.039,42.662,-80.827,42.643,-80.617,42.596,-80.473,42.574,-80.397,42.577,-80.094,42.546,-80.062,42.554,-80.105,42.572,-80.305,42.59,-80.368,42.608,-80.446,42.604,-80.451,42.613,-80.396,42.676,-80.365,42.683,-80.326,42.707,-80.286,42.738,-80.22,42.774,-80.099,42.802,-79.923,42.822,-79.747,42.858,-79.699,42.862,-79.661,42.851,-79.594,42.854,-79.386,42.881,-79.264,42.88,-79.176,42.873,-79.122,42.858,-79.057,42.862,-78.936,42.907,-78.922,42.924,-78.947,42.952,-79.011,42.991,-79.04,43.021,-79.035,43.044,-79.043,43.064,-79.073,43.093,-79.066,43.106,-78.908,43.07,-78.895,43.058,-78.9,43.033,-78.924,42.998,-78.92,42.948,-78.886,42.886,-78.872,42.836,-78.878,42.799,-78.918,42.764,-78.994,42.731,-79.063,42.68,-79.125,42.611,-79.209,42.555,-79.315,42.512,-79.416,42.457,-79.512,42.391,-79.704,42.3,-80.076,42.146,-80.125,42.128,-80.168,42.109,-80.252,42.075,-80.334,42.041,-80.869,41.893,-81.229,41.77,-81.447,41.671,-81.637,41.548,-81.723,41.511,-81.817,41.501,-81.966,41.508,-82.121,41.493,-82.282,41.457,-82.387,41.427,-82.434,41.405,-82.48,41.398,-82.524,41.407,-82.643,41.467,-82.651,41.462,-82.673,41.454,-82.743,41.464,-82.822,41.465,-82.891,41.443,-82.96,41.44,-83.027,41.455,-82.971,41.476,-82.7,41.521,-82.703,41.529,-82.801,41.556,-82.831,41.58,-82.856,41.578,-82.875,41.55,-82.911,41.538,-82.963,41.542,-83.016,41.563,-83.069,41.601,-83.243,41.662,-83.291,41.693,-83.352,41.714,-83.443,41.725,-83.456,41.742,-83.441,41.766,-83.362,41.88,-83.316,41.923,-83.297,41.935,-83.273,41.935,-83.262,41.946,-83.265,41.971,-83.256,41.986,-83.205,42.027,-83.177,42.049,-83.188,42.083,-83.175,42.146,-83.142,42.228,-83.105,42.286],[-78.99,42.992,-78.961,42.988,-78.93,43.022,-78.914,43.042,-78.916,43.053,-78.988,43.064,-79.014,43.052,-79.019,43.024,-79.012,43.003,-78.99,42.992],[-82.638,41.747,-82.627,41.771,-82.643,41.815,-82.654,41.827,-82.674,41.806,-82.685,41.757,-82.685,41.736,-82.638,41.747]]]},{"n":"Chad","z":1,"b":[14.259,12.877,14.743,13.239],"p":[[[14.742,13.078,14.743,13.062,14.716,13.047,14.694,13.024,14.686,12.993,14.661,12.982,14.62,12.992,14.592,12.99,14.516,12.98,14.475,12.952,14.407,12.899,14.347,12.877,14.295,12.884,14.275,12.903,14.259,12.986,14.259,13.004,14.282,13.014,14.289,13.029,14.307,13.065,14.311,13.082,14.312,13.087,14.33,13.116,14.33,13.135,14.342,13.162,14.37,13.185,14.415,13.209,14.496,13.239,14.554,13.238,14.591,13.214,14.653,13.208,14.668,13.195,14.707,13.137,14.742,13.078]]]},{"n":"Malawi","z":1,"b":[33.905,-14.403,35.284,-9.495],"p":[[[35.26,-14.277,35.284,-14.365,35.273,-14.391,35.258,-14.403,35.236,-14.401,35.193,-14.353,35.13,-14.289,35.048,-14.177,35.014,-14.157,34.958,-14.148,34.947,-14.134,34.942,-14.071,34.93,-14.042,34.906,-14.021,34.881,-14.012,34.857,-14.014,34.849,-14.035,34.858,-14.074,34.85,-14.112,34.825,-14.15,34.816,-14.178,34.825,-14.197,34.819,-14.217,34.77,-14.254,34.729,-14.268,34.707,-14.262,34.701,-14.237,34.678,-14.214,34.637,-14.195,34.6,-14.159,34.567,-14.106,34.547,-14.048,34.538,-13.985,34.546,-13.938,34.571,-13.908,34.596,-13.846,34.622,-13.751,34.605,-13.669,34.547,-13.6,34.498,-13.567,34.458,-13.572,34.41,-13.53,34.321,-13.379,34.313,-13.346,34.319,-13.308,34.338,-13.265,34.344,-13.158,34.33,-12.945,34.32,-12.921,34.299,-12.857,34.275,-12.825,34.267,-12.791,34.273,-12.756,34.263,-12.73,34.236,-12.713,34.214,-12.684,34.196,-12.642,34.197,-12.481,34.18,-12.425,34.15,-12.393,34.107,-12.384,34.074,-12.353,34.037,-12.25,34.033,-12.209,34.059,-12.049,34.1,-11.976,34.164,-11.925,34.194,-11.88,34.192,-11.84,34.222,-11.788,34.318,-11.679,34.323,-11.653,34.289,-11.425,34.262,-11.312,34.23,-11.228,34.224,-11.167,34.241,-11.131,34.245,-11.099,34.234,-11.073,34.203,-10.614,34.19,-10.59,34.195,-10.567,34.218,-10.544,34.229,-10.517,34.227,-10.485,34.238,-10.461,34.26,-10.448,34.248,-10.4,34.204,-10.319,34.155,-10.256,34.057,-10.157,34.016,-10.098,33.985,-10.024,33.941,-9.874,33.919,-9.843,33.907,-9.802,33.905,-9.751,33.916,-9.712,33.954,-9.658,33.959,-9.627,33.95,-9.565,33.962,-9.532,33.996,-9.495,34.089,-9.538,34.321,-9.732,34.328,-9.757,34.476,-9.949,34.524,-10.03,34.524,-10.073,34.57,-10.241,34.58,-10.32,34.57,-10.38,34.572,-10.428,34.59,-10.496,34.584,-10.525,34.637,-10.626,34.662,-10.71,34.667,-10.792,34.652,-10.873,34.606,-10.99,34.598,-11.037,34.608,-11.08,34.638,-11.127,34.688,-11.177,34.726,-11.238,34.752,-11.309,34.774,-11.342,34.801,-11.341,34.851,-11.352,34.891,-11.394,34.937,-11.463,34.959,-11.577,34.957,-11.735,34.934,-11.871,34.891,-11.985,34.857,-12.048,34.832,-12.058,34.788,-12.094,34.726,-12.157,34.7,-12.2,34.711,-12.224,34.714,-12.279,34.711,-12.364,34.702,-12.403,34.694,-12.422,34.791,-12.643,34.808,-12.767,34.772,-12.87,34.77,-12.963,34.8,-13.044,34.81,-13.142,34.8,-13.258,34.812,-13.332,34.846,-13.366,34.862,-13.46,34.859,-13.616,34.868,-13.701,34.888,-13.717,35.03,-13.73,35.056,-13.743,35.097,-13.825,35.18,-14.064,35.178,-14.088,35.26,-14.277],[34.622,-12.067,34.642,-12.014,34.624,-11.985,34.591,-11.971,34.554,-11.982,34.542,-12.019,34.58,-12.066,34.622,-12.067],[34.662,-12.101,34.684,-12.119,34.719,-12.111,34.746,-12.088,34.756,-12.059,34.756,-12.031,34.739,-12.013,34.715,-12.003,34.68,-12.009,34.667,-12.048,34.662,-12.101]]]},{"n":"Nyanza","z":1,"b":[31.664,-2.985,34.813,0.472],"p":[[[34.718,-0.092,34.813,-0.276,34.798,-0.296,34.703,-0.325,34.577,-0.33,34.478,-0.353,34.458,-0.369,34.475,-0.464,34.456,-0.495,34.425,-0.502,34.376,-0.459,34.329,-0.459,34.275,-0.445,34.217,-0.442,34.175,-0.47,34.079,-0.579,34.06,-0.645,34.068,-0.725,34.106,-0.798,34.163,-0.879,34.144,-0.94,34.121,-0.975,34.07,-0.997,34.063,-1.034,33.932,-1.188,33.903,-1.247,33.907,-1.278,33.952,-1.321,33.942,-1.34,33.844,-1.342,33.829,-1.363,33.829,-1.39,33.816,-1.406,33.822,-1.425,33.915,-1.495,33.921,-1.514,33.889,-1.513,33.791,-1.485,33.73,-1.48,33.686,-1.488,33.668,-1.506,33.646,-1.571,33.667,-1.604,33.675,-1.638,33.602,-1.691,33.593,-1.761,33.546,-1.8,33.489,-1.822,33.402,-1.812,33.366,-1.83,33.331,-1.864,33.298,-1.922,33.45,-1.915,33.478,-1.928,33.491,-1.953,33.521,-1.98,33.524,-1.996,33.412,-1.997,33.374,-2.032,33.3,-2.023,33.233,-2.072,33.226,-2.096,33.276,-2.124,33.517,-2.162,33.585,-2.168,33.753,-2.111,33.794,-2.123,33.81,-2.157,33.809,-2.192,33.767,-2.238,33.594,-2.358,33.51,-2.43,33.432,-2.46,33.442,-2.502,33.437,-2.522,33.416,-2.531,33.21,-2.489,33.067,-2.41,32.999,-2.402,32.945,-2.421,32.908,-2.449,32.892,-2.484,32.884,-2.664,32.947,-2.818,32.963,-2.849,32.892,-2.862,32.78,-2.971,32.76,-2.985,32.773,-2.91,32.764,-2.867,32.848,-2.797,32.862,-2.757,32.814,-2.647,32.824,-2.544,32.811,-2.515,32.751,-2.514,32.703,-2.502,32.685,-2.491,32.671,-2.456,32.644,-2.439,32.548,-2.508,32.535,-2.494,32.535,-2.462,32.521,-2.454,32.44,-2.444,32.392,-2.422,32.357,-2.394,32.32,-2.304,32.283,-2.282,32.24,-2.283,32.211,-2.31,32.174,-2.366,32.114,-2.377,32.096,-2.394,32.141,-2.486,32.15,-2.525,32.13,-2.537,32.031,-2.516,31.986,-2.547,31.969,-2.608,31.979,-2.677,32.003,-2.757,31.971,-2.752,31.918,-2.674,31.898,-2.681,31.885,-2.7,31.848,-2.705,31.819,-2.798,31.801,-2.791,31.784,-2.75,31.782,-2.693,31.815,-2.621,31.77,-2.597,31.743,-2.567,31.735,-2.53,31.786,-2.465,31.781,-2.44,31.767,-2.399,31.739,-2.357,31.702,-2.328,31.675,-2.277,31.675,-2.08,31.664,-2.016,31.664,-1.932,31.763,-1.492,31.811,-1.419,31.826,-1.359,31.865,-1.092,31.861,-1.044,31.849,-1.017,31.829,-1.013,31.804,-1.022,31.783,-1.01,31.768,-0.936,31.756,-0.888,31.732,-0.837,31.723,-0.79,31.818,-0.568,31.867,-0.481,31.994,-0.321,32.001,-0.255,31.976,-0.212,31.94,-0.18,31.933,-0.158,31.95,-0.132,32.033,-0.09,32.055,-0.057,32.159,-0.04,32.209,0.009,32.258,-0.005,32.287,0.021,32.351,0.015,32.384,0.066,32.413,0.089,32.438,0.093,32.449,0.056,32.486,0.053,32.533,0.07,32.564,0.099,32.588,0.193,32.615,0.235,32.647,0.253,32.681,0.189,32.711,0.179,32.676,0.137,32.671,0.117,32.692,0.108,32.735,0.108,32.779,0.119,32.853,0.169,32.879,0.169,32.895,0.112,32.915,0.096,32.958,0.108,33.021,0.147,33.07,0.193,33.076,0.239,33.137,0.252,33.201,0.388,33.187,0.428,33.245,0.451,33.273,0.463,33.312,0.451,33.364,0.472,33.368,0.453,33.332,0.369,33.354,0.349,33.461,0.334,33.484,0.315,33.485,0.293,33.441,0.271,33.448,0.244,33.483,0.218,33.533,0.207,33.58,0.222,33.64,0.3,33.68,0.316,33.713,0.308,33.728,0.23,33.745,0.208,33.861,0.185,33.945,0.218,33.975,0.213,33.974,0.089,33.982,0.025,34.003,-0.024,34.029,-0.062,34.059,-0.081,34.115,-0.079,34.125,-0.091,34.108,-0.133,34.117,-0.163,34.226,-0.259,34.285,-0.35,34.31,-0.356,34.334,-0.337,34.353,-0.314,34.37,-0.243,34.42,-0.196,34.538,-0.144,34.718,-0.092],[32.054,-2.328,32.06,-2.265,32.045,-2.243,31.975,-2.266,31.9,-2.284,31.876,-2.264,31.84,-2.216,31.818,-2.192,31.791,-2.234,31.783,-2.321,31.843,-2.381,31.848,-2.454,31.9,-2.443,31.917,-2.372,31.966,-2.366,32.038,-2.351,32.054,-2.328],[32.298,-0.392,32.319,-0.337,32.269,-0.29,32.194,-0.332,32.179,-0.311,32.182,-0.235,32.147,-0.232,32.086,-0.231,32.065,-0.254,32.081,-0.29,32.104,-0.322,32.13,-0.337,32.208,-0.363,32.205,-0.397,32.166,-0.442,32.145,-0.487,32.132,-0.525,32.272,-0.472,32.298,-0.392],[33.175,-2.159,33.183,-2.116,33.14,-2.069,33.12,-2.011,33.097,-1.965,33.027,-1.973,32.972,-1.973,32.949,-1.902,32.884,-1.904,32.83,-1.937,32.847,-1.979,32.851,-2.061,32.899,-2.085,32.989,-2.105,33.059,-2.135,33.125,-2.158,33.175,-2.159],[33.372,0.216,33.388,0.272,33.318,0.298,33.224,0.277,33.221,0.233,33.218,0.19,33.18,0.175,33.184,0.15,33.264,0.144,33.273,0.121,33.339,0.08,33.325,0.142,33.317,0.184,33.372,0.216]]]},{"n":"Baikal","z":1,"b":[103.713,51.482,109.955,55.779],"p":[[[109.864,55.539,109.857,55.446,109.825,55.402,109.791,55.33,109.781,55.28,109.75,55.185,109.744,55.14,109.768,55.083,109.765,55.04,109.716,54.932,109.702,54.902,109.684,54.85,109.646,54.807,109.633,54.772,109.607,54.676,109.544,54.629,109.523,54.559,109.49,54.38,109.496,54.262,109.554,54.174,109.556,54.154,109.538,54.116,109.5,54.061,109.426,53.998,109.244,53.869,109.211,53.828,109.198,53.793,109.204,53.737,109.204,53.679,109.199,53.66,109.178,53.638,109.03,53.639,109.009,53.668,109.034,53.76,109.061,53.804,109.061,53.831,109.033,53.858,108.994,53.869,108.944,53.862,108.891,53.842,108.836,53.807,108.619,53.582,108.532,53.513,108.545,53.508,108.646,53.548,108.732,53.571,108.842,53.587,108.921,53.566,108.968,53.508,108.989,53.45,108.982,53.393,108.931,53.344,108.768,53.281,108.729,53.278,108.703,53.283,108.676,53.304,108.661,53.302,108.392,53.163,108.302,53.109,108.297,53.093,108.33,53.056,108.335,53.038,108.288,52.989,108.234,52.946,108.007,52.837,107.948,52.78,107.763,52.714,107.674,52.701,107.579,52.701,107.387,52.648,107.099,52.542,106.938,52.475,106.905,52.447,106.843,52.356,106.806,52.329,106.752,52.317,106.693,52.338,106.599,52.371,106.497,52.383,106.39,52.355,106.314,52.323,106.269,52.282,106.258,52.231,106.281,52.169,106.261,52.103,106.187,51.956,106.134,51.916,106.065,51.863,105.967,51.774,105.798,51.701,105.435,51.614,105.065,51.557,104.903,51.522,104.826,51.49,104.655,51.482,104.39,51.5,104.156,51.535,103.817,51.634,103.745,51.669,103.713,51.694,103.723,51.711,103.743,51.722,103.791,51.732,104.133,51.769,104.343,51.783,104.537,51.784,104.663,51.806,104.721,51.849,104.812,51.87,104.867,51.87,104.935,51.87,105.053,51.882,105.165,51.906,105.243,51.938,105.286,51.979,105.345,52.009,105.419,52.03,105.57,52.108,105.692,52.216,105.86,52.387,106.06,52.512,106.292,52.592,106.452,52.656,106.537,52.706,106.579,52.742,106.702,52.832,106.757,52.869,106.783,52.906,106.881,52.973,106.897,53.003,106.873,53.029,106.774,53.022,106.78,53.045,106.852,53.103,107.072,53.241,107.232,53.316,107.535,53.439,107.547,53.453,107.551,53.502,107.576,53.548,107.627,53.611,107.789,53.711,108.062,53.849,108.224,53.976,108.275,54.036,108.298,54.078,108.362,54.173,108.408,54.21,108.473,54.277,108.519,54.35,108.546,54.429,108.593,54.492,108.723,54.601,108.786,54.672,108.827,54.739,108.847,54.8,108.882,54.853,108.933,54.898,108.944,54.931,108.917,54.951,108.953,54.983,109.053,55.028,109.11,55.099,109.171,55.194,109.198,55.261,109.213,55.405,109.191,55.464,109.193,55.536,109.239,55.575,109.313,55.608,109.507,55.731,109.576,55.771,109.667,55.779,109.76,55.774,109.847,55.755,109.955,55.672,109.861,55.575,109.864,55.539],[107.767,53.267,107.795,53.393,107.744,53.399,107.584,53.283,107.353,53.21,107.091,53.133,106.995,53.107,106.96,53.078,106.963,53.023,107.314,53.068,107.438,53.103,107.639,53.22,107.767,53.267]]]},{"n":"Winnipeg","z":1,"b":[-99.24,50.398,-96.288,54.413],"p":[[[-96.373,50.729,-96.359,50.691,-96.294,50.624,-96.392,50.629,-96.452,50.656,-96.542,50.728,-96.566,50.695,-96.588,50.664,-96.594,50.599,-96.608,50.552,-96.576,50.468,-96.596,50.442,-96.683,50.398,-96.761,50.398,-96.866,50.418,-96.931,50.461,-96.962,50.57,-96.976,50.644,-96.983,50.683,-96.956,50.797,-96.954,50.844,-96.967,50.927,-96.938,51.06,-96.927,51.082,-96.841,51.128,-96.832,51.157,-96.774,51.221,-96.693,51.255,-96.627,51.324,-96.631,51.343,-96.708,51.364,-96.725,51.366,-96.908,51.206,-96.937,51.193,-96.979,51.204,-96.982,51.251,-96.914,51.331,-96.742,51.554,-96.736,51.593,-96.751,51.612,-96.785,51.611,-96.797,51.626,-96.817,51.676,-96.826,51.71,-96.844,51.722,-96.881,51.724,-97.065,51.665,-97.106,51.642,-97.127,51.608,-97.124,51.536,-97.141,51.502,-97.22,51.45,-97.253,51.444,-97.276,51.491,-97.289,51.593,-97.277,51.685,-97.24,51.766,-97.25,51.865,-97.307,51.981,-97.357,52.04,-97.446,52.027,-97.496,52.001,-97.55,51.925,-97.573,51.924,-97.6,51.988,-97.526,52.061,-97.495,52.108,-97.497,52.133,-97.531,52.137,-97.68,52.138,-97.743,52.135,-97.774,52.109,-97.729,52.088,-97.735,52.045,-97.792,51.914,-97.823,51.889,-97.963,51.927,-98.036,51.981,-98.045,52.052,-98.071,52.1,-98.139,52.162,-98.143,52.211,-98.188,52.28,-98.429,52.382,-98.454,52.416,-98.469,52.486,-98.553,52.556,-98.809,52.725,-98.826,52.767,-98.82,52.783,-98.896,52.856,-98.904,52.901,-98.895,52.916,-98.773,52.965,-98.501,53.018,-98.46,53.044,-98.525,53.065,-98.696,53.082,-98.796,53.076,-98.903,53.06,-98.99,53.048,-99.05,53.073,-99.186,53.175,-99.23,53.185,-99.24,53.21,-99.202,53.353,-99.085,53.48,-99.042,53.557,-98.994,53.677,-98.983,53.718,-99.001,53.754,-98.929,53.867,-98.811,53.897,-98.731,53.877,-98.613,53.843,-98.456,53.852,-98.299,53.834,-98.009,53.71,-97.963,53.695,-97.91,53.7,-97.9,53.738,-98.076,53.823,-98.204,53.874,-98.261,53.912,-98.299,53.965,-98.297,54.018,-98.254,54.069,-98.174,54.218,-98.021,54.41,-98.003,54.413,-97.932,54.375,-97.932,54.353,-97.99,54.322,-97.983,54.316,-97.844,54.317,-97.82,54.3,-97.864,54.265,-98.083,54.202,-98.172,54.079,-98.213,54.025,-98.192,53.994,-98.061,53.931,-98.046,53.94,-98.033,53.977,-98.001,54.002,-97.94,54.029,-97.811,54.042,-97.734,54.081,-97.729,54.06,-97.762,54.013,-97.972,53.98,-97.977,53.969,-97.938,53.89,-97.832,53.736,-97.82,53.695,-97.828,53.641,-97.791,53.581,-97.707,53.515,-97.652,53.4,-97.582,53.291,-97.475,53.201,-97.437,53.119,-97.352,53.026,-97.347,53.006,-97.376,52.98,-97.424,52.965,-97.398,52.879,-97.333,52.784,-97.198,52.673,-97.112,52.478,-97.071,52.413,-97.002,52.269,-97.007,52.257,-97.019,52.204,-96.986,52.133,-96.958,52.077,-96.859,51.993,-96.835,51.942,-96.854,51.892,-96.844,51.866,-96.769,51.855,-96.707,51.815,-96.689,51.791,-96.706,51.773,-96.757,51.763,-96.722,51.704,-96.601,51.596,-96.551,51.53,-96.546,51.476,-96.495,51.413,-96.344,51.309,-96.29,51.251,-96.288,51.208,-96.337,51.179,-96.349,51.153,-96.394,51.088,-96.403,51.02,-96.367,50.857,-96.357,50.76,-96.373,50.729],[-97.938,52.318,-97.887,52.358,-97.944,52.531,-97.995,52.542,-98.046,52.46,-98.043,52.398,-97.978,52.315,-97.938,52.318],[-96.632,51.148,-96.598,51.188,-96.709,51.199,-96.737,51.137,-96.782,51.092,-96.842,51.066,-96.845,51.027,-96.762,51.01,-96.68,51.041,-96.632,51.148]]]},{"n":"Great Slave","z":1,"b":[-117.668,60.838,-108.919,62.952],"p":[[[-109.022,62.807,-108.919,62.788,-108.928,62.768,-108.952,62.752,-108.993,62.739,-108.987,62.712,-109.006,62.69,-109.252,62.597,-109.292,62.603,-109.263,62.657,-109.301,62.717,-109.396,62.739,-109.691,62.777,-109.747,62.798,-109.944,62.814,-110.284,62.827,-110.469,62.828,-110.498,62.819,-110.603,62.832,-110.662,62.829,-110.701,62.81,-110.756,62.763,-110.967,62.727,-111.269,62.661,-111.294,62.634,-111.272,62.617,-110.836,62.666,-110.536,62.66,-110.514,62.67,-110.411,62.685,-109.864,62.673,-109.768,62.668,-109.735,62.65,-109.836,62.607,-109.958,62.576,-110.101,62.557,-110.187,62.554,-110.263,62.571,-110.33,62.569,-110.455,62.542,-110.554,62.538,-110.764,62.546,-110.78,62.538,-110.683,62.516,-110.647,62.482,-110.684,62.42,-110.705,62.395,-110.75,62.387,-110.95,62.403,-111.06,62.381,-111.1,62.357,-111.236,62.302,-111.294,62.281,-111.302,62.258,-111.307,62.233,-111.388,62.192,-111.587,62.129,-111.591,62.098,-111.634,62.071,-111.638,62.059,-111.509,62.065,-111.502,62.049,-111.62,61.95,-111.843,61.857,-112.09,61.736,-112.204,61.645,-112.208,61.624,-112.329,61.577,-112.676,61.463,-112.893,61.41,-112.969,61.39,-113.001,61.405,-112.99,61.426,-113.007,61.449,-113.091,61.484,-113.148,61.466,-113.359,61.445,-113.398,61.416,-113.515,61.378,-113.602,61.335,-113.677,61.301,-113.699,61.276,-113.704,61.246,-113.682,61.214,-113.691,61.189,-113.729,61.172,-113.725,61.163,-113.648,61.157,-113.633,61.145,-113.63,61.106,-113.664,61.072,-113.729,61.028,-113.832,60.997,-113.973,60.979,-114.086,60.982,-114.173,61.006,-114.338,60.988,-114.581,60.929,-114.727,60.907,-114.773,60.923,-114.913,60.91,-115.333,60.845,-115.473,60.838,-115.744,60.857,-115.877,60.848,-116.057,60.868,-116.425,60.953,-116.502,60.994,-116.528,61.029,-116.612,61.017,-116.664,61.021,-116.719,61.039,-117.089,61.051,-117.226,61.075,-117.278,61.097,-117.438,61.195,-117.505,61.24,-117.557,61.259,-117.629,61.285,-117.668,61.308,-117.627,61.307,-117.473,61.268,-117.206,61.19,-117.067,61.15,-116.98,61.169,-116.794,61.206,-116.738,61.224,-116.726,61.241,-116.797,61.298,-116.784,61.321,-116.763,61.329,-116.594,61.324,-116.511,61.309,-116.45,61.285,-116.367,61.273,-116.199,61.266,-116.121,61.228,-115.95,61.183,-115.939,61.193,-115.955,61.218,-115.988,61.235,-115.98,61.245,-115.87,61.284,-115.858,61.302,-115.905,61.327,-116.011,61.359,-116.039,61.381,-115.956,61.406,-115.918,61.423,-115.881,61.409,-115.827,61.411,-115.717,61.44,-115.711,61.45,-115.744,61.468,-115.752,61.481,-115.745,61.511,-115.708,61.568,-115.679,61.607,-115.658,61.641,-115.571,61.687,-115.566,61.723,-115.557,61.74,-115.508,61.757,-115.337,61.778,-115.215,61.773,-115.197,61.756,-115.19,61.742,-115.162,61.736,-114.898,61.767,-114.845,61.768,-114.721,61.812,-114.648,61.857,-114.636,61.869,-114.651,61.923,-114.667,61.935,-114.724,61.937,-114.762,61.956,-114.781,61.994,-114.848,62.032,-114.942,62.055,-115.033,62.099,-115.053,62.119,-115.038,62.145,-115.039,62.154,-115.045,62.174,-115.167,62.244,-115.208,62.247,-115.29,62.237,-115.307,62.254,-115.259,62.299,-115.254,62.329,-115.291,62.345,-115.309,62.371,-115.307,62.408,-115.294,62.44,-115.271,62.467,-115.289,62.482,-115.37,62.48,-115.701,62.485,-115.756,62.539,-115.792,62.565,-115.809,62.599,-115.867,62.633,-115.992,62.682,-116.038,62.726,-116.004,62.766,-115.913,62.764,-115.763,62.72,-115.738,62.696,-115.754,62.667,-115.724,62.66,-115.672,62.664,-115.289,62.607,-115.246,62.591,-115.217,62.569,-115.202,62.543,-115.165,62.519,-115.108,62.498,-114.902,62.466,-114.77,62.412,-114.714,62.376,-114.625,62.359,-114.437,62.373,-114.373,62.388,-114.351,62.408,-114.322,62.416,-114.284,62.413,-114.227,62.385,-114.225,62.373,-114.247,62.326,-114.224,62.312,-114.155,62.279,-114.136,62.277,-114.085,62.283,-114.023,62.268,-113.892,62.206,-113.861,62.176,-113.756,62.103,-113.574,62.019,-113.493,62.015,-113.309,62.005,-113.114,62.029,-113.037,62.019,-113.005,62.044,-112.923,62.073,-112.906,62.07,-112.892,62.037,-112.529,62.074,-112.302,62.134,-112.082,62.23,-111.962,62.29,-111.78,62.368,-111.7,62.42,-111.643,62.496,-111.593,62.541,-111.551,62.556,-111.512,62.595,-111.478,62.661,-111.393,62.722,-111.259,62.779,-111.081,62.835,-110.673,62.927,-110.525,62.945,-110.056,62.952,-109.983,62.944,-109.89,62.924,-109.705,62.911,-109.638,62.895,-109.348,62.867,-109.185,62.847,-109.046,62.829,-109.022,62.807],[-111.745,62.081,-111.668,62.123,-111.706,62.131,-111.828,62.113,-111.985,62.03,-112.071,61.974,-112.306,61.916,-112.524,61.875,-112.696,61.868,-112.875,61.84,-112.971,61.832,-113.011,61.809,-112.92,61.762,-112.754,61.764,-112.741,61.721,-112.73,61.65,-112.69,61.605,-112.596,61.584,-112.455,61.605,-112.331,61.65,-112.248,61.756,-112.137,61.837,-111.927,61.916,-111.787,62.002,-111.745,62.081]]]},{"n":"Ontario","z":1,"b":[-79.774,43.198,-75.776,44.495],"p":[[[-75.787,44.484,-75.776,44.459,-75.791,44.43,-75.832,44.397,-76.238,44.183,-76.352,44.106,-76.31,44.063,-76.306,44.043,-76.281,44.046,-76.198,44.083,-76.173,44.079,-76.161,44.069,-76.154,44.045,-76.192,44.003,-76.175,43.994,-76.102,44.003,-76.071,43.998,-76.099,43.967,-76.136,43.947,-76.143,43.927,-76.198,43.874,-76.223,43.864,-76.236,43.871,-76.233,43.895,-76.285,43.869,-76.288,43.849,-76.258,43.829,-76.235,43.799,-76.218,43.758,-76.21,43.67,-76.199,43.653,-76.195,43.645,-76.2,43.635,-76.205,43.614,-76.194,43.596,-76.203,43.575,-76.232,43.55,-76.29,43.537,-76.376,43.535,-76.473,43.507,-76.58,43.454,-76.653,43.405,-76.693,43.361,-76.722,43.343,-76.741,43.35,-76.885,43.314,-76.937,43.29,-76.95,43.268,-76.963,43.264,-76.976,43.277,-77.079,43.288,-77.271,43.295,-77.421,43.288,-77.529,43.267,-77.625,43.279,-77.707,43.323,-77.838,43.357,-78.175,43.395,-78.457,43.388,-78.689,43.361,-79.053,43.285,-79.213,43.242,-79.291,43.207,-79.338,43.198,-79.401,43.209,-79.528,43.21,-79.638,43.235,-79.728,43.253,-79.773,43.301,-79.774,43.32,-79.656,43.453,-79.61,43.513,-79.596,43.552,-79.569,43.578,-79.499,43.612,-79.48,43.636,-79.446,43.647,-79.396,43.645,-79.303,43.69,-79.165,43.78,-79.061,43.835,-78.989,43.855,-78.793,43.876,-78.669,43.902,-78.581,43.912,-78.528,43.905,-78.446,43.916,-78.335,43.944,-78.186,43.966,-78,43.979,-77.881,43.994,-77.829,44.011,-77.726,44.009,-77.709,44.033,-77.682,44.045,-77.583,44.038,-77.549,44.025,-77.544,44.014,-77.568,44.008,-77.554,43.99,-77.503,43.963,-77.421,43.954,-77.325,43.972,-77.259,43.964,-77.276,43.952,-77.311,43.952,-77.315,43.947,-77.289,43.923,-77.267,43.922,-77.19,43.948,-77.185,43.938,-77.219,43.9,-77.208,43.876,-77.151,43.866,-77.054,43.883,-76.934,43.93,-76.879,43.943,-76.87,43.965,-76.91,43.965,-77.013,43.957,-77.042,43.943,-77.044,43.952,-77.024,43.987,-76.93,44.038,-76.888,44.068,-76.863,44.1,-76.89,44.099,-76.969,44.066,-77.04,44.049,-77.103,44.046,-77.117,44.079,-77.083,44.148,-77.115,44.173,-77.271,44.139,-77.29,44.127,-77.29,44.118,-77.273,44.111,-77.275,44.104,-77.296,44.098,-77.332,44.104,-77.331,44.119,-77.314,44.143,-77.333,44.153,-77.381,44.146,-77.446,44.131,-77.586,44.084,-77.584,44.094,-77.547,44.124,-77.398,44.161,-77.326,44.178,-77.28,44.176,-77.251,44.189,-77.225,44.189,-77.203,44.175,-77.148,44.181,-77.059,44.205,-77.02,44.197,-77.033,44.156,-77.01,44.153,-76.951,44.186,-76.913,44.198,-76.896,44.188,-77.021,44.133,-77.055,44.106,-77.062,44.091,-77.041,44.071,-76.992,44.074,-76.787,44.182,-76.688,44.218,-76.589,44.238,-76.49,44.244,-76.408,44.262,-76.341,44.295,-76.057,44.367,-75.942,44.411,-75.831,44.49,-75.805,44.495,-75.787,44.484],[-76.688,44.138,-76.64,44.181,-76.643,44.199,-76.761,44.172,-76.799,44.155,-76.809,44.14,-76.787,44.13,-76.731,44.127,-76.688,44.138],[-76.275,44.009,-76.277,43.987,-76.262,43.99,-76.231,44.018,-76.231,44.029,-76.26,44.022,-76.275,44.009],[-76.237,44.233,-76.227,44.254,-76.279,44.247,-76.329,44.239,-76.43,44.227,-76.451,44.202,-76.483,44.179,-76.492,44.162,-76.477,44.153,-76.474,44.141,-76.485,44.128,-76.469,44.125,-76.423,44.122,-76.365,44.156,-76.353,44.176,-76.33,44.208,-76.237,44.233],[-76.081,44.295,-76.075,44.306,-76.119,44.306,-76.145,44.292,-76.15,44.281,-76.143,44.269,-76.111,44.277,-76.081,44.295]]]},{"n":"Great Bear","z":1,"b":[-125.09,64.827,-117.514,67.029],"p":[[[-117.842,66.424,-117.676,66.456,-117.542,66.483,-117.514,66.46,-117.55,66.424,-117.613,66.385,-117.632,66.366,-117.674,66.336,-117.673,66.3,-117.68,66.253,-117.728,66.226,-117.781,66.207,-117.813,66.187,-117.872,66.15,-117.923,66.139,-117.987,66.105,-117.971,66.084,-117.907,66.067,-117.924,66.044,-118.073,66.036,-118.116,66.011,-118.134,65.962,-118.199,65.905,-118.209,65.856,-118.157,65.831,-118.064,65.831,-117.874,65.847,-117.846,65.84,-117.901,65.8,-117.903,65.764,-117.943,65.723,-118.034,65.693,-118.148,65.682,-118.264,65.67,-118.379,65.664,-118.387,65.68,-118.453,65.683,-118.569,65.644,-118.715,65.694,-118.834,65.755,-118.944,65.784,-119.276,65.796,-119.504,65.793,-119.619,65.782,-119.705,65.752,-119.698,65.714,-119.892,65.641,-119.974,65.617,-119.988,65.586,-119.762,65.493,-119.541,65.408,-119.509,65.368,-119.556,65.344,-119.717,65.342,-119.885,65.325,-119.99,65.267,-120.095,65.203,-120.159,65.176,-120.466,65.073,-120.861,64.956,-120.952,64.935,-121.073,64.9,-121.133,64.861,-121.23,64.827,-121.367,64.834,-121.471,64.879,-121.48,64.933,-121.397,64.969,-121.32,64.974,-121.214,64.993,-121.089,65.004,-121.003,65.038,-120.991,65.082,-120.55,65.289,-120.406,65.411,-120.368,65.509,-120.383,65.557,-120.466,65.585,-120.663,65.605,-120.978,65.553,-121.182,65.496,-121.368,65.435,-121.548,65.364,-121.546,65.342,-121.569,65.297,-121.606,65.229,-121.612,65.175,-121.616,65.128,-121.615,65.093,-121.663,65.074,-121.727,65.061,-121.77,65.037,-121.929,64.996,-122.04,64.961,-122.558,65.023,-122.891,65.068,-123.371,65.114,-123.446,65.116,-123.472,65.152,-123.444,65.185,-123.344,65.2,-123.188,65.216,-122.853,65.278,-122.767,65.342,-122.769,65.409,-122.887,65.487,-122.786,65.531,-122.589,65.588,-122.374,65.657,-122.206,65.753,-122.164,65.817,-122.531,65.944,-122.549,65.986,-122.387,66.007,-122.067,65.977,-121.772,65.948,-121.475,65.957,-121.293,66.001,-121.249,66.054,-121.278,66.092,-121.533,66.122,-121.695,66.178,-122.11,66.234,-122.436,66.251,-122.745,66.251,-123.016,66.255,-123.02,66.221,-122.993,66.173,-123.04,66.126,-123.092,66.1,-123.098,66.073,-123.116,66.054,-123.154,66.064,-123.198,66.104,-123.396,66.135,-123.569,66.139,-123.77,66.125,-124.051,66.13,-124.261,66.157,-124.499,66.153,-124.59,66.11,-124.574,66.056,-124.59,66.016,-124.642,66.007,-124.676,66.025,-124.732,66.04,-124.785,65.995,-124.817,65.941,-124.898,65.966,-124.963,66.01,-125.02,66.057,-125.09,66.157,-125.063,66.248,-124.806,66.28,-124.59,66.274,-124.525,66.276,-124.404,66.309,-123.56,66.427,-123.177,66.517,-123.146,66.511,-123.143,66.483,-123.088,66.473,-122.987,66.495,-122.844,66.533,-122.529,66.56,-122.523,66.599,-122.411,66.599,-122.344,66.583,-121.98,66.636,-121.606,66.709,-121.528,66.719,-121.455,66.74,-121.233,66.832,-120.692,66.907,-120.593,66.93,-120.223,66.978,-120.103,67.001,-119.878,67.029,-119.785,67.004,-119.755,66.99,-119.671,66.992,-119.443,66.966,-119.211,66.933,-119.097,66.909,-119.252,66.829,-119.568,66.746,-119.762,66.696,-119.993,66.662,-120.084,66.654,-120.095,66.634,-120.13,66.6,-120.209,66.568,-120.33,66.537,-120.34,66.527,-120.314,66.493,-120.325,66.47,-120.411,66.456,-120.478,66.422,-120.445,66.395,-120.23,66.392,-120.016,66.38,-119.962,66.356,-119.768,66.327,-119.722,66.311,-119.592,66.307,-119.521,66.316,-119.489,66.338,-119.105,66.326,-118.756,66.331,-118.605,66.348,-118.337,66.399,-118.247,66.46,-118.192,66.515,-118.004,66.601,-117.838,66.639,-117.726,66.659,-117.696,66.658,-117.683,66.642,-117.699,66.621,-117.691,66.596,-117.734,66.551,-117.841,66.487,-117.864,66.461,-117.862,66.438,-117.842,66.424]]]},{"n":"Saint Clair","z":1,"b":[-83.105,42.275,-82.414,42.697],"p":[[[-83.076,42.275,-83.105,42.286,-83.063,42.323,-83.017,42.347,-82.926,42.375,-82.902,42.394,-82.883,42.442,-82.87,42.519,-82.841,42.568,-82.796,42.589,-82.784,42.613,-82.804,42.639,-82.787,42.665,-82.733,42.69,-82.687,42.697,-82.65,42.687,-82.633,42.661,-82.63,42.595,-82.639,42.574,-82.645,42.558,-82.634,42.542,-82.613,42.525,-82.481,42.5,-82.453,42.495,-82.432,42.498,-82.418,42.472,-82.414,42.411,-82.428,42.367,-82.462,42.339,-82.516,42.322,-82.59,42.315,-82.867,42.335,-82.971,42.327,-83.014,42.312,-83.047,42.297,-83.076,42.275]]]},{"n":"Vänern","z":1.7,"b":[12.313,58.372,14.091,59.39],"p":[[[14.038,59.083,14.066,59.097,14.083,59.1,14.089,59.092,14.091,59.079,14.083,59.064,14.072,59.028,14.056,59.017,14.05,59.012,14.039,59.004,14.012,59.004,13.993,58.986,13.995,58.945,13.987,58.898,13.975,58.865,13.954,58.844,13.924,58.823,13.89,58.8,13.859,58.772,13.83,58.744,13.8,58.722,13.737,58.704,13.631,58.684,13.517,58.663,13.437,58.642,13.358,58.617,13.3,58.561,13.24,58.515,13.195,58.511,13.172,58.512,13.148,58.521,13.137,58.541,13.149,58.559,13.149,58.573,13.148,58.597,13.115,58.608,13.041,58.604,12.952,58.572,12.913,58.546,12.869,58.504,12.798,58.486,12.728,58.452,12.679,58.421,12.65,58.397,12.624,58.382,12.602,58.376,12.58,58.372,12.562,58.373,12.551,58.379,12.554,58.394,12.573,58.419,12.571,58.432,12.554,58.43,12.507,58.397,12.375,58.383,12.335,58.374,12.313,58.395,12.329,58.434,12.488,58.513,12.607,58.617,12.611,58.641,12.563,58.65,12.525,58.661,12.504,58.695,12.501,58.728,12.5,58.764,12.471,58.783,12.448,58.783,12.425,58.801,12.432,58.813,12.459,58.817,12.474,58.84,12.472,58.877,12.464,58.921,12.469,58.935,12.482,58.931,12.497,58.92,12.51,58.901,12.52,58.867,12.526,58.846,12.535,58.832,12.557,58.819,12.576,58.826,12.6,58.846,12.618,58.887,12.647,58.931,12.676,58.987,12.708,59.036,12.746,59.065,12.784,59.076,12.818,59.082,12.869,59.073,12.945,59.048,13.041,59.014,13.062,58.977,13.088,58.947,13.122,58.921,13.16,58.905,13.195,58.899,13.236,58.905,13.244,58.93,13.221,58.993,13.177,59.097,13.123,59.198,13.103,59.255,13.103,59.274,13.108,59.284,13.118,59.296,13.13,59.319,13.143,59.342,13.155,59.353,13.173,59.348,13.191,59.325,13.218,59.311,13.246,59.31,13.252,59.315,13.251,59.325,13.249,59.336,13.251,59.345,13.261,59.352,13.278,59.357,13.316,59.361,13.378,59.364,13.461,59.37,13.548,59.381,13.628,59.39,13.68,59.39,13.72,59.378,13.738,59.359,13.744,59.341,13.752,59.331,13.766,59.331,13.8,59.337,13.83,59.341,13.867,59.335,13.904,59.328,13.933,59.33,13.962,59.334,13.997,59.329,14.028,59.32,14.046,59.314,14.052,59.305,14.056,59.29,14.054,59.268,14.047,59.241,14.027,59.204,13.994,59.157,13.946,59.113,13.931,59.09,13.945,59.067,13.973,59.066,14.005,59.068,14.038,59.083]]]},{"n":"Okeechobee","z":1.7,"b":[-81.081,26.691,-80.621,27.182],"p":[[[-80.629,26.887,-80.668,26.826,-80.711,26.742,-80.743,26.697,-80.784,26.691,-80.827,26.707,-80.858,26.723,-80.896,26.752,-80.932,26.81,-80.979,26.856,-81.027,26.876,-81.071,26.917,-81.081,26.956,-81.055,26.991,-80.997,27.033,-80.942,27.077,-80.87,27.147,-80.803,27.182,-80.759,27.182,-80.711,27.133,-80.69,27.11,-80.671,27.087,-80.654,27.054,-80.639,27.008,-80.621,26.949,-80.629,26.887]]]},{"n":"Nicaragua","z":1.7,"b":[-85.914,11.035,-84.769,12.109],"p":[[[-84.814,11.26,-84.792,11.206,-84.781,11.157,-84.769,11.118,-84.803,11.065,-84.842,11.049,-84.897,11.035,-84.94,11.052,-85.005,11.057,-85.11,11.081,-85.26,11.123,-85.411,11.174,-85.517,11.216,-85.584,11.25,-85.64,11.283,-85.684,11.317,-85.719,11.353,-85.754,11.406,-85.798,11.483,-85.834,11.563,-85.854,11.626,-85.864,11.664,-85.877,11.683,-85.89,11.691,-85.899,11.702,-85.903,11.721,-85.901,11.745,-85.901,11.795,-85.908,11.874,-85.914,11.953,-85.912,12.003,-85.894,12.035,-85.871,12.098,-85.835,12.109,-85.798,12.107,-85.751,12.094,-85.676,12.064,-85.588,12.016,-85.513,11.959,-85.441,11.902,-85.362,11.855,-85.259,11.783,-85.132,11.663,-85.009,11.527,-84.916,11.414,-84.857,11.329,-84.814,11.26]]]},{"n":"Tana","z":1.7,"b":[37.002,11.632,37.592,12.303],"p":[[[37.286,11.794,37.253,11.82,37.211,11.822,37.169,11.817,37.146,11.833,37.113,11.855,37.096,11.84,37.078,11.829,37.058,11.834,37.04,11.852,37.024,11.88,37.002,11.907,37.006,11.972,37.008,12.029,37.017,12.087,37.028,12.165,37.048,12.21,37.072,12.243,37.096,12.266,37.126,12.27,37.164,12.26,37.195,12.231,37.219,12.23,37.269,12.224,37.333,12.272,37.377,12.288,37.413,12.291,37.46,12.303,37.496,12.286,37.529,12.259,37.555,12.223,37.566,12.164,37.592,12.117,37.585,12.084,37.583,12.049,37.577,12.018,37.56,11.986,37.55,11.942,37.513,11.91,37.501,11.876,37.496,11.835,37.44,11.753,37.387,11.632,37.361,11.635,37.342,11.632,37.327,11.636,37.32,11.648,37.301,11.668,37.322,11.709,37.309,11.741,37.289,11.771,37.286,11.794]]]},{"n":"Titicaca","z":1.7,"b":[-70.021,-16.589,-68.586,-15.264],"p":[[[-69.366,-16.223,-69.4,-16.2,-69.436,-16.187,-69.483,-16.182,-69.487,-16.161,-69.449,-16.112,-69.439,-16.059,-69.447,-16.029,-69.489,-16.012,-69.558,-15.991,-69.627,-15.967,-69.668,-15.943,-69.691,-15.922,-69.717,-15.907,-69.748,-15.887,-69.782,-15.855,-69.812,-15.828,-69.834,-15.819,-69.846,-15.827,-69.854,-15.841,-69.852,-15.861,-69.842,-15.884,-69.833,-15.905,-69.835,-15.916,-69.845,-15.92,-69.859,-15.92,-69.876,-15.914,-69.895,-15.9,-69.923,-15.88,-69.961,-15.86,-69.994,-15.842,-70.011,-15.828,-70.016,-15.807,-70.021,-15.771,-70.017,-15.733,-70.002,-15.706,-69.979,-15.68,-69.956,-15.646,-69.921,-15.634,-69.866,-15.66,-69.818,-15.692,-69.798,-15.693,-69.818,-15.638,-69.869,-15.533,-69.929,-15.427,-69.938,-15.399,-69.914,-15.374,-69.855,-15.344,-69.844,-15.324,-69.833,-15.31,-69.799,-15.299,-69.761,-15.287,-69.742,-15.269,-69.687,-15.264,-69.625,-15.344,-69.541,-15.372,-69.449,-15.474,-69.405,-15.491,-69.361,-15.511,-69.299,-15.621,-69.27,-15.628,-69.224,-15.66,-69.19,-15.679,-69.17,-15.697,-69.147,-15.725,-69.103,-15.765,-69.039,-15.796,-69.014,-15.848,-69.014,-15.91,-68.995,-15.926,-68.904,-15.943,-68.831,-15.948,-68.796,-15.972,-68.809,-15.994,-68.826,-16.017,-68.853,-16.049,-68.889,-16.084,-68.871,-16.136,-68.829,-16.19,-68.764,-16.202,-68.694,-16.219,-68.635,-16.221,-68.593,-16.239,-68.594,-16.272,-68.586,-16.288,-68.588,-16.302,-68.612,-16.312,-68.655,-16.318,-68.693,-16.326,-68.705,-16.339,-68.712,-16.359,-68.739,-16.383,-68.787,-16.407,-68.86,-16.415,-68.906,-16.424,-68.928,-16.448,-68.897,-16.465,-68.868,-16.5,-68.858,-16.524,-68.856,-16.547,-68.862,-16.569,-68.871,-16.589,-68.904,-16.589,-68.942,-16.582,-69.006,-16.551,-69.049,-16.525,-69.076,-16.521,-69.098,-16.483,-69.052,-16.417,-69.039,-16.364,-69.043,-16.318,-69.044,-16.281,-69.005,-16.252,-68.946,-16.223,-68.905,-16.241,-68.885,-16.241,-68.891,-16.206,-68.947,-16.176,-69.015,-16.111,-69.077,-16.081,-69.103,-16.087,-69.107,-16.132,-69.121,-16.178,-69.136,-16.232,-69.178,-16.25,-69.222,-16.254,-69.269,-16.251,-69.324,-16.243,-69.366,-16.223]]]},{"n":"Cedar","z":1.7,"b":[-100.891,53.052,-99.33,54.252],"p":[[[-99.349,53.199,-99.33,53.159,-99.369,53.15,-99.389,53.131,-99.396,53.1,-99.395,53.085,-99.403,53.074,-99.416,53.065,-99.436,53.058,-99.459,53.053,-99.487,53.052,-99.518,53.057,-99.539,53.067,-99.54,53.083,-99.538,53.097,-99.552,53.105,-99.569,53.112,-99.579,53.123,-99.587,53.141,-99.605,53.163,-99.624,53.18,-99.638,53.188,-99.644,53.183,-99.645,53.17,-99.647,53.155,-99.657,53.145,-99.685,53.14,-99.73,53.14,-99.798,53.137,-99.876,53.102,-99.972,53.098,-100.048,53.117,-100.125,53.131,-100.2,53.152,-100.274,53.185,-100.346,53.231,-100.412,53.27,-100.47,53.289,-100.542,53.307,-100.646,53.347,-100.709,53.375,-100.771,53.402,-100.785,53.409,-100.842,53.426,-100.876,53.449,-100.891,53.471,-100.881,53.488,-100.855,53.501,-100.779,53.504,-100.707,53.5,-100.641,53.495,-100.604,53.498,-100.593,53.507,-100.592,53.519,-100.606,53.535,-100.632,53.554,-100.634,53.569,-100.591,53.572,-100.51,53.577,-100.39,53.583,-100.323,53.593,-100.313,53.64,-100.324,53.669,-100.352,53.712,-100.383,53.758,-100.432,53.79,-100.413,53.822,-100.33,53.865,-100.347,53.898,-100.393,53.892,-100.474,53.896,-100.498,53.905,-100.506,53.917,-100.483,53.95,-100.387,53.954,-100.331,53.958,-100.303,53.973,-100.3,53.99,-100.303,54.008,-100.306,54.031,-100.302,54.061,-100.306,54.098,-100.328,54.14,-100.351,54.177,-100.358,54.202,-100.35,54.215,-100.335,54.222,-100.31,54.221,-100.278,54.213,-100.244,54.211,-100.216,54.222,-100.191,54.24,-100.163,54.252,-100.122,54.246,-100.064,54.219,-99.994,54.192,-99.927,54.188,-99.882,54.192,-99.869,54.186,-99.928,54.154,-100.056,54.097,-100.18,54.042,-100.226,54.015,-100.209,54.008,-100.178,54.004,-100.14,53.993,-100.105,53.971,-100.094,53.941,-100.113,53.911,-100.14,53.883,-100.151,53.861,-100.141,53.846,-100.118,53.837,-100.06,53.847,-99.963,53.876,-99.845,53.927,-99.737,53.989,-99.66,54.034,-99.617,54.043,-99.604,54.027,-99.602,54.011,-99.617,53.989,-99.649,53.958,-99.69,53.918,-99.729,53.87,-99.776,53.835,-99.833,53.827,-99.883,53.831,-99.908,53.827,-99.914,53.81,-99.915,53.785,-99.926,53.76,-99.955,53.747,-99.995,53.735,-100.033,53.714,-100.076,53.694,-100.125,53.686,-100.17,53.679,-100.201,53.664,-100.215,53.646,-100.219,53.632,-100.185,53.611,-100.103,53.576,-100.019,53.533,-99.976,53.491,-99.96,53.456,-99.94,53.43,-99.929,53.407,-99.938,53.381,-99.948,53.355,-99.94,53.332,-99.925,53.311,-99.914,53.288,-99.906,53.268,-99.894,53.255,-99.872,53.248,-99.839,53.245,-99.799,53.253,-99.759,53.273,-99.693,53.299,-99.589,53.32,-99.491,53.333,-99.437,53.337,-99.421,53.329,-99.411,53.311,-99.407,53.285,-99.405,53.257,-99.375,53.223,-99.349,53.199]]]},{"n":"Onega","z":1.7,"b":[33.941,60.867,36.428,62.916],"p":[[[36.419,61.392,36.428,61.373,36.425,61.349,36.408,61.319,36.396,61.302,36.383,61.285,36.38,61.281,36.374,61.271,36.371,61.257,36.378,61.242,36.388,61.225,36.395,61.206,36.387,61.184,36.364,61.16,36.325,61.135,36.279,61.114,36.224,61.096,36.163,61.083,36.099,61.063,36.037,61.03,35.976,60.994,35.915,60.967,35.88,60.948,35.884,60.931,35.904,60.917,35.91,60.902,35.899,60.89,35.88,60.878,35.855,60.871,35.829,60.867,35.801,60.872,35.772,60.886,35.719,60.905,35.635,60.918,35.551,60.938,35.499,60.97,35.473,61.002,35.45,61.023,35.426,61.033,35.4,61.04,35.363,61.038,35.316,61.028,35.255,61.015,35.184,61.005,35.133,61.007,35.117,61.022,35.093,61.045,35.022,61.064,34.942,61.07,34.896,61.047,34.869,61.027,34.846,61.027,34.829,61.039,34.815,61.055,34.802,61.074,34.796,61.096,34.797,61.123,34.804,61.15,34.819,61.174,34.838,61.194,34.887,61.206,34.974,61.212,35.075,61.207,35.157,61.194,35.207,61.177,35.231,61.165,35.208,61.159,35.143,61.156,35.078,61.151,35.053,61.141,35.062,61.126,35.08,61.108,35.108,61.092,35.142,61.077,35.192,61.069,35.259,61.067,35.345,61.073,35.441,61.063,35.527,61.023,35.583,61.034,35.615,61.076,35.632,61.108,35.629,61.15,35.611,61.197,35.578,61.246,35.537,61.292,35.495,61.329,35.457,61.356,35.398,61.386,35.304,61.43,35.154,61.485,34.962,61.543,34.772,61.601,34.63,61.654,34.531,61.704,34.448,61.753,34.388,61.796,34.345,61.831,34.323,61.854,34.315,61.869,34.336,61.864,34.385,61.843,34.445,61.819,34.494,61.808,34.531,61.812,34.562,61.824,34.583,61.846,34.594,61.874,34.594,61.906,34.586,61.938,34.566,61.969,34.537,61.999,34.477,62.039,34.381,62.093,34.294,62.142,34.255,62.169,34.255,62.186,34.262,62.21,34.265,62.238,34.257,62.262,34.223,62.294,34.165,62.339,34.098,62.379,34.042,62.397,34.003,62.401,33.978,62.408,33.962,62.425,33.947,62.453,33.941,62.482,33.944,62.5,33.959,62.51,33.979,62.516,34.003,62.516,34.027,62.512,34.054,62.495,34.084,62.463,34.137,62.414,34.218,62.352,34.292,62.297,34.328,62.264,34.329,62.246,34.321,62.227,34.343,62.199,34.417,62.158,34.514,62.12,34.591,62.096,34.636,62.095,34.653,62.109,34.644,62.136,34.632,62.182,34.647,62.213,34.658,62.245,34.615,62.304,34.609,62.339,34.633,62.348,34.682,62.339,34.696,62.365,34.66,62.406,34.592,62.458,34.547,62.498,34.527,62.524,34.52,62.542,34.532,62.546,34.557,62.54,34.595,62.519,34.641,62.489,34.696,62.443,34.758,62.383,34.811,62.332,34.841,62.31,34.852,62.294,34.859,62.26,34.871,62.219,34.892,62.187,34.928,62.16,34.976,62.118,35.029,62.078,35.075,62.054,35.115,62.054,35.15,62.071,35.165,62.081,35.168,62.097,35.144,62.127,35.094,62.172,35.052,62.212,35.046,62.229,35.096,62.218,35.188,62.19,35.284,62.162,35.341,62.149,35.387,62.158,35.455,62.181,35.518,62.21,35.554,62.236,35.564,62.26,35.567,62.287,35.559,62.319,35.54,62.354,35.498,62.404,35.433,62.469,35.352,62.523,35.271,62.545,35.201,62.55,35.147,62.559,35.093,62.58,35.024,62.613,34.967,62.639,34.943,62.643,34.868,62.611,34.839,62.591,34.776,62.622,34.739,62.621,34.704,62.643,34.658,62.68,34.621,62.712,34.583,62.756,34.536,62.818,34.48,62.874,34.489,62.904,34.533,62.916,34.605,62.873,34.703,62.844,34.788,62.831,34.855,62.826,34.91,62.815,34.958,62.797,35.006,62.773,35.083,62.741,35.202,62.705,35.326,62.666,35.416,62.627,35.464,62.604,35.488,62.606,35.495,62.619,35.504,62.625,35.519,62.617,35.544,62.599,35.585,62.576,35.641,62.556,35.698,62.538,35.74,62.52,35.771,62.496,35.803,62.461,35.822,62.412,35.823,62.352,35.8,62.281,35.764,62.205,35.73,62.139,35.674,62.079,35.689,62.04,35.75,61.999,35.818,61.951,35.843,61.902,35.871,61.855,35.926,61.806,35.985,61.756,36.026,61.704,36.061,61.654,36.109,61.611,36.159,61.571,36.2,61.531,36.247,61.493,36.312,61.459,36.371,61.432,36.406,61.411,36.419,61.392],[35.326,61.935,35.347,61.998,35.339,62.033,35.308,62.077,35.252,62.08,35.196,61.988,35.171,61.906,35.16,61.859,35.202,61.839,35.274,61.87,35.326,61.935]]]},{"n":"Great Salt","z":1.7,"b":[-113.014,40.699,-111.951,41.667],"p":[[[-111.974,41.002,-111.951,40.968,-111.961,40.942,-112.053,40.902,-112.097,40.881,-112.19,40.769,-112.267,40.74,-112.332,40.699,-112.37,40.701,-112.394,40.757,-112.436,40.816,-112.446,40.875,-112.478,40.943,-112.493,40.938,-112.508,40.919,-112.522,40.878,-112.546,40.839,-112.58,40.849,-112.632,40.927,-112.69,40.98,-112.728,41.019,-112.751,41.059,-112.768,41.114,-112.788,41.174,-112.814,41.224,-112.846,41.261,-112.881,41.289,-112.918,41.328,-112.961,41.377,-112.985,41.427,-112.997,41.489,-112.998,41.534,-112.995,41.56,-112.997,41.576,-113.007,41.582,-113.014,41.597,-112.999,41.612,-112.966,41.614,-112.909,41.637,-112.844,41.657,-112.8,41.667,-112.77,41.666,-112.742,41.659,-112.719,41.637,-112.707,41.603,-112.734,41.563,-112.738,41.537,-112.708,41.496,-112.666,41.466,-112.604,41.431,-112.554,41.43,-112.527,41.421,-112.512,41.381,-112.489,41.309,-112.458,41.247,-112.437,41.219,-112.422,41.217,-112.406,41.217,-112.39,41.223,-112.377,41.232,-112.366,41.248,-112.381,41.324,-112.388,41.385,-112.402,41.432,-112.39,41.447,-112.374,41.457,-112.33,41.443,-112.29,41.41,-112.243,41.393,-112.171,41.397,-112.092,41.408,-112.064,41.4,-112.063,41.388,-112.076,41.347,-112.114,41.336,-112.179,41.342,-112.241,41.345,-112.27,41.332,-112.287,41.317,-112.304,41.283,-112.284,41.268,-112.243,41.222,-112.212,41.192,-112.181,41.163,-112.152,41.134,-112.083,41.065,-111.974,41.002],[-112.171,40.875,-112.156,40.956,-112.198,41.056,-112.236,41.069,-112.263,41.058,-112.252,41.02,-112.25,40.964,-112.216,40.924,-112.171,40.875]]]},{"n":"Michigan","z":1,"b":[-88.028,41.638,-84.757,46.109],"p":[[[-84.763,45.792,-84.772,45.795,-84.806,45.775,-84.866,45.769,-84.952,45.775,-84.981,45.767,-84.955,45.743,-84.977,45.707,-85.047,45.656,-85.088,45.605,-85.099,45.553,-85.088,45.507,-85.055,45.467,-85.006,45.44,-84.943,45.429,-84.93,45.413,-84.967,45.393,-85.033,45.381,-85.127,45.378,-85.223,45.356,-85.319,45.314,-85.376,45.277,-85.391,45.244,-85.394,45.196,-85.382,45.078,-85.389,45.033,-85.43,44.949,-85.485,44.84,-85.549,44.772,-85.561,44.781,-85.551,44.815,-85.497,44.924,-85.489,44.97,-85.494,44.994,-85.513,44.997,-85.534,44.976,-85.558,44.931,-85.563,44.907,-85.549,44.903,-85.559,44.873,-85.593,44.816,-85.623,44.804,-85.647,44.838,-85.645,44.882,-85.605,44.97,-85.619,44.987,-85.596,45.042,-85.585,45.082,-85.597,45.123,-85.588,45.161,-85.556,45.196,-85.566,45.207,-85.617,45.193,-85.689,45.128,-85.784,45.012,-85.858,44.962,-85.91,44.976,-85.953,44.967,-85.985,44.933,-86.02,44.916,-86.058,44.916,-86.08,44.876,-86.088,44.794,-86.126,44.748,-86.237,44.72,-86.259,44.699,-86.261,44.669,-86.243,44.63,-86.242,44.551,-86.259,44.434,-86.296,44.331,-86.353,44.242,-86.415,44.169,-86.519,44.074,-86.521,44.055,-86.47,43.98,-86.449,43.924,-86.438,43.813,-86.441,43.794,-86.541,43.657,-86.542,43.637,-86.435,43.407,-86.34,43.242,-86.329,43.224,-86.301,43.158,-86.248,43.029,-86.222,42.9,-86.22,42.773,-86.233,42.65,-86.26,42.531,-86.3,42.418,-86.352,42.309,-86.407,42.224,-86.52,42.082,-86.575,41.988,-86.632,41.911,-86.692,41.853,-86.798,41.789,-87.073,41.669,-87.159,41.646,-87.253,41.638,-87.356,41.643,-87.412,41.656,-87.425,41.679,-87.447,41.681,-87.477,41.695,-87.515,41.721,-87.557,41.766,-87.602,41.859,-87.651,42,-87.695,42.084,-87.733,42.112,-87.767,42.154,-87.797,42.211,-87.812,42.341,-87.812,42.543,-87.802,42.68,-87.781,42.753,-87.787,42.809,-87.819,42.848,-87.838,42.896,-87.846,42.953,-87.862,42.992,-87.888,43.012,-87.893,43.041,-87.879,43.078,-87.879,43.103,-87.893,43.117,-87.896,43.137,-87.889,43.165,-87.902,43.245,-87.898,43.294,-87.881,43.347,-87.824,43.443,-87.806,43.49,-87.801,43.538,-87.774,43.599,-87.7,43.724,-87.698,43.752,-87.723,43.809,-87.729,43.851,-87.724,43.907,-87.7,43.977,-87.658,44.059,-87.603,44.125,-87.535,44.174,-87.512,44.23,-87.536,44.293,-87.527,44.386,-87.487,44.509,-87.441,44.601,-87.391,44.662,-87.356,44.72,-87.337,44.776,-87.346,44.801,-87.383,44.827,-87.42,44.866,-87.48,44.874,-87.555,44.849,-87.566,44.85,-87.604,44.847,-87.677,44.79,-87.741,44.692,-87.788,44.652,-87.851,44.624,-87.898,44.593,-87.929,44.559,-87.969,44.552,-88.019,44.571,-88.028,44.618,-87.998,44.692,-87.952,44.764,-87.857,44.88,-87.854,44.908,-87.83,44.942,-87.683,44.993,-87.627,45.032,-87.608,45.083,-87.602,45.157,-87.593,45.178,-87.571,45.187,-87.506,45.249,-87.399,45.365,-87.302,45.484,-87.216,45.607,-87.154,45.677,-87.114,45.692,-87.085,45.73,-87.039,45.845,-87.005,45.897,-86.982,45.917,-86.971,45.905,-86.993,45.842,-86.993,45.79,-86.974,45.723,-86.938,45.7,-86.884,45.721,-86.837,45.765,-86.799,45.832,-86.754,45.859,-86.703,45.848,-86.661,45.852,-86.627,45.873,-86.563,45.883,-86.552,45.857,-86.559,45.806,-86.58,45.782,-86.612,45.784,-86.65,45.757,-86.691,45.701,-86.69,45.657,-86.644,45.625,-86.622,45.623,-86.622,45.652,-86.601,45.682,-86.534,45.736,-86.527,45.753,-86.4,45.801,-86.351,45.831,-86.338,45.866,-86.272,45.937,-86.209,45.959,-86.123,45.969,-86.04,45.963,-85.959,45.941,-85.914,45.941,-85.903,45.964,-85.834,45.98,-85.708,45.988,-85.615,46.02,-85.555,46.078,-85.479,46.108,-85.386,46.109,-85.26,46.088,-85.101,46.046,-84.972,45.989,-84.802,45.874,-84.757,45.863,-84.763,45.792],[-85.492,45.76,-85.539,45.775,-85.573,45.738,-85.608,45.656,-85.612,45.609,-85.586,45.598,-85.552,45.603,-85.509,45.624,-85.492,45.663,-85.501,45.75,-85.492,45.76],[-86.052,45.156,-86.06,45.147,-86.038,45.103,-86.016,45.089,-85.988,45.088,-85.976,45.102,-85.98,45.132,-85.998,45.15,-86.052,45.156],[-86.926,45.415,-86.938,45.426,-86.967,45.368,-86.947,45.346,-86.892,45.342,-86.862,45.352,-86.857,45.394,-86.862,45.409,-86.878,45.414,-86.926,45.415],[-87.049,45.303,-87.088,45.279,-87.122,45.225,-87.166,45.19,-87.221,45.174,-87.259,45.138,-87.279,45.081,-87.316,45.031,-87.396,44.954,-87.403,44.933,-87.398,44.903,-87.382,44.864,-87.355,44.839,-87.316,44.827,-87.28,44.835,-87.188,44.922,-87.179,44.941,-87.191,44.95,-87.179,44.983,-87.141,45.041,-87.114,45.068,-87.096,45.067,-87.078,45.078,-87.059,45.102,-87.062,45.125,-87.086,45.145,-87.084,45.154,-87.057,45.151,-87.047,45.172,-87.054,45.218,-87.042,45.237,-87.01,45.228,-86.992,45.241,-86.986,45.275,-87.005,45.296,-87.049,45.303]]]},{"n":"Superior","z":1,"b":[-92.107,46.436,-84.344,49.001],"p":[[[-84.432,46.522,-84.344,46.531,-84.358,46.505,-84.387,46.502,-84.424,46.491,-84.467,46.464,-84.528,46.436,-84.592,46.443,-84.635,46.482,-84.697,46.491,-84.779,46.468,-84.851,46.469,-84.914,46.494,-85.034,46.506,-85.049,46.524,-85.046,46.543,-85.027,46.563,-85.017,46.611,-85.015,46.686,-84.998,46.74,-84.968,46.771,-85.015,46.783,-85.138,46.778,-85.281,46.752,-85.442,46.707,-85.616,46.691,-85.804,46.704,-85.947,46.702,-86.105,46.679,-86.132,46.687,-86.231,46.656,-86.403,46.588,-86.531,46.524,-86.637,46.453,-86.654,46.452,-86.679,46.468,-86.733,46.49,-86.79,46.491,-86.851,46.47,-86.914,46.483,-86.977,46.53,-87.034,46.543,-87.086,46.523,-87.171,46.514,-87.358,46.524,-87.378,46.54,-87.394,46.59,-87.415,46.612,-87.451,46.627,-87.509,46.683,-87.637,46.827,-87.673,46.844,-87.778,46.888,-87.877,46.908,-88.129,46.935,-88.184,46.955,-88.216,46.952,-88.383,46.874,-88.39,46.857,-88.4,46.841,-88.427,46.814,-88.449,46.802,-88.463,46.814,-88.476,46.837,-88.474,46.886,-88.456,46.963,-88.466,47.013,-88.504,47.035,-88.521,47.063,-88.515,47.096,-88.543,47.122,-88.604,47.141,-88.633,47.167,-88.628,47.199,-88.634,47.22,-88.65,47.232,-88.716,47.207,-88.83,47.146,-88.907,47.091,-88.948,47.043,-89.009,47.016,-89.09,47.009,-89.155,46.989,-89.269,46.918,-89.355,46.882,-89.497,46.855,-89.695,46.839,-89.861,46.793,-89.993,46.718,-90.154,46.654,-90.341,46.602,-90.469,46.584,-90.536,46.599,-90.617,46.631,-90.737,46.689,-90.754,46.684,-90.745,46.672,-90.748,46.662,-90.77,46.645,-90.923,46.603,-90.944,46.619,-90.886,46.677,-90.864,46.721,-90.865,46.761,-90.84,46.81,-90.788,46.868,-90.768,46.908,-90.782,46.929,-90.84,46.962,-90.887,46.965,-90.946,46.954,-91.127,46.868,-91.145,46.874,-91.203,46.888,-91.218,46.886,-91.357,46.811,-91.499,46.766,-91.836,46.702,-91.921,46.694,-91.987,46.699,-92.073,46.736,-92.107,46.762,-92.104,46.789,-92.065,46.819,-91.645,47.05,-91.386,47.215,-90.957,47.524,-90.847,47.585,-90.597,47.688,-90.197,47.786,-90,47.841,-89.829,47.901,-89.72,47.947,-89.672,47.979,-89.634,47.994,-89.605,47.992,-89.547,48.013,-89.45,48.092,-89.438,48.099,-89.345,48.121,-89.315,48.159,-89.297,48.204,-89.252,48.28,-89.221,48.311,-89.214,48.345,-89.229,48.382,-89.225,48.418,-89.201,48.451,-89.086,48.498,-88.775,48.583,-88.764,48.572,-88.818,48.506,-88.876,48.381,-88.897,48.378,-88.916,48.363,-88.933,48.335,-88.888,48.337,-88.783,48.368,-88.734,48.384,-88.722,48.415,-88.706,48.476,-88.683,48.51,-88.654,48.517,-88.635,48.546,-88.625,48.599,-88.602,48.63,-88.565,48.64,-88.55,48.658,-88.552,48.684,-88.542,48.753,-88.535,48.786,-88.503,48.833,-88.458,48.848,-88.395,48.845,-88.352,48.821,-88.329,48.776,-88.323,48.738,-88.334,48.709,-88.394,48.656,-88.567,48.528,-88.585,48.492,-88.569,48.474,-88.559,48.447,-88.539,48.443,-88.501,48.467,-88.484,48.488,-88.489,48.507,-88.476,48.531,-88.444,48.56,-88.418,48.569,-88.4,48.557,-88.369,48.562,-88.325,48.584,-88.248,48.596,-88.233,48.597,-88.138,48.683,-88.109,48.733,-88.109,48.776,-88.147,48.821,-88.224,48.867,-88.247,48.917,-88.249,48.942,-88.264,48.974,-88.248,48.986,-88.2,48.977,-88.161,48.982,-88.128,48.998,-88.07,49.001,-88.023,48.999,-87.966,48.959,-87.761,48.92,-87.685,48.899,-87.592,48.878,-87.568,48.871,-87.552,48.848,-87.517,48.839,-87.464,48.842,-87.407,48.835,-87.311,48.799,-87.224,48.776,-87.085,48.779,-87.009,48.789,-86.888,48.772,-86.82,48.773,-86.752,48.801,-86.693,48.808,-86.643,48.795,-86.621,48.777,-86.581,48.753,-86.459,48.765,-86.432,48.758,-86.396,48.731,-86.39,48.714,-86.371,48.688,-86.325,48.637,-86.314,48.619,-86.283,48.598,-86.257,48.548,-86.237,48.471,-86.216,48.423,-86.191,48.406,-86.181,48.385,-86.159,48.304,-86.104,48.21,-86.022,48.119,-85.914,48.03,-85.778,47.97,-85.615,47.941,-85.47,47.934,-85.342,47.95,-85.174,47.959,-84.964,47.96,-84.855,47.95,-84.847,47.928,-84.867,47.896,-84.915,47.855,-84.92,47.828,-84.922,47.793,-84.961,47.746,-84.981,47.718,-84.975,47.698,-84.986,47.672,-85.014,47.639,-84.998,47.595,-84.936,47.54,-84.86,47.496,-84.771,47.462,-84.716,47.427,-84.693,47.39,-84.668,47.371,-84.641,47.367,-84.616,47.35,-84.594,47.32,-84.618,47.267,-84.688,47.191,-84.72,47.142,-84.712,47.12,-84.772,47.037,-84.775,47.004,-84.75,46.981,-84.656,46.949,-84.625,46.922,-84.6,46.918,-84.582,46.937,-84.545,46.946,-84.488,46.944,-84.435,46.931,-84.385,46.906,-84.367,46.88,-84.382,46.853,-84.408,46.844,-84.485,46.848,-84.528,46.834,-84.553,46.806,-84.561,46.73,-84.54,46.697,-84.528,46.697,-84.51,46.713,-84.488,46.744,-84.464,46.747,-84.438,46.723,-84.466,46.674,-84.548,46.6,-84.569,46.577,-84.568,46.543,-84.501,46.483,-84.475,46.489,-84.459,46.504,-84.432,46.522],[-90.756,46.965,-90.766,46.951,-90.761,46.937,-90.723,46.92,-90.708,46.927,-90.713,46.941,-90.738,46.962,-90.756,46.965],[-90.653,46.828,-90.589,46.855,-90.596,46.869,-90.619,46.87,-90.752,46.814,-90.782,46.791,-90.792,46.772,-90.768,46.77,-90.711,46.783,-90.68,46.798,-90.678,46.813,-90.653,46.828],[-90.547,46.919,-90.535,46.954,-90.543,46.963,-90.576,46.957,-90.65,46.937,-90.652,46.922,-90.58,46.914,-90.547,46.919],[-90.41,47.044,-90.41,47.064,-90.422,47.071,-90.447,47.066,-90.464,47.048,-90.472,47.018,-90.459,47.01,-90.427,47.026,-90.41,47.044],[-88.057,48.837,-88.087,48.808,-88.076,48.741,-88.057,48.717,-88.031,48.718,-87.998,48.743,-87.941,48.757,-87.859,48.758,-87.816,48.768,-87.812,48.785,-87.782,48.795,-87.77,48.811,-87.765,48.841,-87.813,48.859,-87.915,48.865,-87.996,48.857,-88.057,48.837],[-88.911,47.936,-88.625,48.042,-88.593,48.072,-88.597,48.087,-88.581,48.111,-88.485,48.167,-88.499,48.176,-88.588,48.158,-88.756,48.104,-89.147,47.953,-89.193,47.929,-89.2,47.917,-89.169,47.917,-89.169,47.902,-89.199,47.872,-89.195,47.852,-89.157,47.841,-89.082,47.851,-88.97,47.881,-88.93,47.896,-88.924,47.905,-88.96,47.909,-88.955,47.926,-88.911,47.936],[-87.79,47.411,-87.741,47.434,-87.793,47.467,-87.878,47.484,-87.994,47.484,-88.11,47.473,-88.32,47.421,-88.394,47.389,-88.562,47.272,-88.609,47.217,-88.616,47.174,-88.574,47.143,-88.434,47.122,-88.427,47.137,-88.433,47.143,-88.454,47.152,-88.417,47.191,-88.405,47.175,-88.409,47.131,-88.438,47.1,-88.468,47.077,-88.469,47.048,-88.439,47.011,-88.402,47.019,-88.358,47.073,-88.237,47.159,-88.24,47.178,-88.222,47.211,-88.016,47.321,-87.975,47.355,-87.983,47.368,-88.027,47.386,-88.021,47.392,-87.79,47.411],[-87.69,48.75,-87.622,48.757,-87.626,48.78,-87.667,48.821,-87.702,48.841,-87.731,48.84,-87.748,48.835,-87.753,48.824,-87.731,48.794,-87.73,48.778,-87.744,48.764,-87.73,48.755,-87.69,48.75],[-85.606,47.742,-85.596,47.757,-85.641,47.782,-85.765,47.807,-85.827,47.804,-85.883,47.791,-85.931,47.766,-85.951,47.747,-85.942,47.734,-85.859,47.719,-85.666,47.733,-85.606,47.742]]]},{"n":"Huron","z":1,"b":[-84.763,43.017,-79.678,46.563],"p":[[[-84.344,46.531,-84.236,46.557,-84.165,46.563,-84.118,46.539,-84.087,46.507,-84.084,46.473,-84.106,46.43,-84.109,46.393,-84.092,46.361,-84.034,46.344,-83.934,46.342,-83.882,46.333,-83.863,46.306,-83.843,46.302,-83.661,46.29,-83.593,46.278,-83.572,46.26,-83.459,46.238,-83.025,46.181,-82.895,46.188,-82.719,46.207,-82.664,46.176,-82.638,46.172,-82.598,46.177,-82.35,46.193,-82.318,46.163,-82.196,46.134,-82.116,46.132,-82.012,46.129,-81.817,46.115,-81.769,46.104,-81.766,46.071,-81.765,46.058,-81.75,46.057,-81.727,46.073,-81.689,46.079,-81.662,46.093,-81.646,46.115,-81.615,46.119,-81.545,46.09,-81.542,46.075,-81.64,46.04,-81.629,46.025,-81.624,46.017,-81.597,46.012,-81.595,45.996,-81.627,45.978,-81.597,45.967,-81.502,45.993,-81.449,45.994,-81.304,45.974,-81.237,45.974,-81.197,45.991,-81.191,46.006,-81.174,46.016,-81.162,46.012,-81.158,45.992,-81.16,45.955,-81.097,45.942,-80.968,45.952,-80.89,45.951,-80.818,45.951,-80.805,45.942,-80.787,45.919,-80.732,45.877,-80.713,45.858,-80.696,45.816,-80.656,45.79,-80.643,45.767,-80.641,45.747,-80.629,45.727,-80.593,45.682,-80.546,45.65,-80.52,45.623,-80.511,45.59,-80.486,45.577,-80.444,45.584,-80.427,45.598,-80.429,45.625,-80.414,45.62,-80.389,45.574,-80.392,45.501,-80.385,45.458,-80.35,45.399,-80.302,45.383,-80.271,45.36,-80.177,45.344,-80.168,45.348,-80.185,45.387,-80.176,45.397,-80.145,45.399,-80.092,45.393,-80.057,45.375,-80.041,45.342,-80.05,45.301,-80.111,45.259,-80.121,45.223,-80.108,45.201,-80.092,45.197,-80.075,45.211,-80.049,45.204,-80.015,45.176,-80.006,45.153,-80.023,45.136,-80.083,45.126,-80.086,45.109,-80.022,45.072,-79.972,45.022,-79.946,45.017,-79.932,45.001,-79.929,44.974,-79.902,44.957,-79.828,44.936,-79.8,44.885,-79.785,44.854,-79.772,44.819,-79.75,44.815,-79.733,44.832,-79.732,44.863,-79.696,44.875,-79.692,44.859,-79.713,44.821,-79.709,44.791,-79.678,44.769,-79.717,44.761,-79.827,44.768,-79.876,44.799,-79.891,44.809,-79.925,44.813,-79.935,44.824,-79.92,44.855,-79.93,44.863,-79.997,44.856,-80.065,44.834,-80.108,44.807,-80.108,44.775,-80.079,44.743,-80.021,44.71,-79.997,44.652,-80.008,44.572,-80.037,44.517,-80.085,44.489,-80.226,44.51,-80.46,44.578,-80.593,44.639,-80.65,44.721,-80.67,44.727,-80.79,44.7,-80.854,44.666,-80.91,44.618,-80.925,44.633,-80.901,44.712,-80.891,44.761,-80.895,44.781,-80.919,44.797,-80.963,44.809,-81.022,44.802,-81.11,44.765,-81.13,44.77,-81.128,44.782,-81.036,44.843,-81.006,44.876,-81.006,44.907,-80.993,44.934,-80.966,44.959,-80.973,44.97,-81.012,44.967,-81.032,44.957,-81.074,44.914,-81.134,44.918,-81.141,44.928,-81.125,44.953,-81.138,44.968,-81.18,44.974,-81.193,45.003,-81.207,45.009,-81.26,45.028,-81.264,45.081,-81.282,45.124,-81.313,45.156,-81.316,45.189,-81.285,45.241,-81.296,45.249,-81.4,45.256,-81.595,45.262,-81.697,45.251,-81.706,45.223,-81.683,45.203,-81.629,45.189,-81.59,45.166,-81.577,45.145,-81.541,45.125,-81.499,45.081,-81.451,45.015,-81.415,44.987,-81.377,44.984,-81.355,44.978,-81.358,44.911,-81.341,44.87,-81.3,44.832,-81.278,44.79,-81.281,44.716,-81.278,44.644,-81.298,44.597,-81.352,44.542,-81.387,44.497,-81.402,44.461,-81.431,44.436,-81.474,44.425,-81.519,44.399,-81.565,44.36,-81.595,44.311,-81.609,44.253,-81.647,44.192,-81.708,44.131,-81.732,44.06,-81.718,43.982,-81.717,43.642,-81.705,43.558,-81.713,43.474,-81.739,43.392,-81.785,43.327,-81.848,43.28,-81.911,43.246,-81.974,43.223,-82.033,43.186,-82.088,43.133,-82.181,43.085,-82.312,43.043,-82.417,43.017,-82.432,43.039,-82.461,43.081,-82.516,43.28,-82.597,43.634,-82.663,43.854,-82.716,43.939,-82.788,44.004,-82.878,44.048,-82.947,44.067,-82.995,44.061,-83.069,44.025,-83.209,43.994,-83.267,43.973,-83.299,43.945,-83.338,43.929,-83.383,43.924,-83.387,43.917,-83.353,43.906,-83.353,43.879,-83.447,43.758,-83.469,43.731,-83.526,43.721,-83.573,43.697,-83.622,43.636,-83.668,43.621,-83.735,43.627,-83.848,43.664,-83.916,43.701,-83.938,43.738,-83.94,43.782,-83.92,43.834,-83.922,43.875,-83.879,43.963,-83.821,43.993,-83.738,44.001,-83.692,44.014,-83.683,44.035,-83.657,44.05,-83.614,44.058,-83.577,44.113,-83.548,44.215,-83.512,44.271,-83.487,44.281,-83.464,44.277,-83.463,44.265,-83.366,44.33,-83.331,44.375,-83.328,44.421,-83.319,44.556,-83.308,44.644,-83.294,44.683,-83.3,44.785,-83.328,44.869,-83.436,44.961,-83.452,44.992,-83.448,45.025,-83.426,45.059,-83.378,45.067,-83.293,45.04,-83.279,45.047,-83.302,45.062,-83.316,45.093,-83.32,45.139,-83.331,45.164,-83.349,45.169,-83.396,45.221,-83.403,45.248,-83.397,45.274,-83.415,45.299,-83.479,45.339,-83.594,45.379,-83.869,45.456,-83.918,45.486,-83.986,45.504,-84.073,45.509,-84.128,45.532,-84.151,45.574,-84.189,45.611,-84.242,45.644,-84.326,45.666,-84.441,45.676,-84.563,45.712,-84.691,45.774,-84.763,45.792,-84.757,45.863,-84.727,45.861,-84.713,45.871,-84.713,45.882,-84.725,45.895,-84.717,45.944,-84.725,45.958,-84.675,46.033,-84.632,46.053,-84.581,46.046,-84.55,46.029,-84.537,46.004,-84.521,45.995,-84.49,45.994,-84.473,45.99,-84.463,45.992,-84.447,46.006,-84.341,46,-83.979,45.962,-83.929,45.968,-83.901,45.981,-83.894,46.001,-83.917,46.021,-84.012,46.061,-84.041,46.088,-84.048,46.114,-84.035,46.137,-84.055,46.159,-84.109,46.178,-84.227,46.184,-84.248,46.203,-84.238,46.222,-84.204,46.247,-84.202,46.283,-84.213,46.304,-84.229,46.366,-84.255,46.408,-84.307,46.469,-84.327,46.491,-84.358,46.505,-84.344,46.531],[-83.345,45.993,-83.392,45.988,-83.437,45.963,-83.467,45.936,-83.48,45.907,-83.453,45.884,-83.387,45.869,-83.332,45.88,-83.269,45.947,-83.271,45.961,-83.296,45.977,-83.345,45.993],[-83.529,46.024,-83.58,46.08,-83.608,46.097,-83.684,46.106,-83.699,46.096,-83.68,46.07,-83.683,46.057,-83.715,46.038,-83.775,46.011,-83.82,46.005,-83.851,46.019,-83.871,46.017,-83.875,45.991,-83.866,45.979,-83.823,45.956,-83.731,45.943,-83.588,45.941,-83.518,45.958,-83.494,45.991,-83.503,46.013,-83.529,46.024],[-83.829,46.148,-83.808,46.167,-83.795,46.214,-83.822,46.253,-83.891,46.284,-83.971,46.303,-84.061,46.31,-84.097,46.288,-84.078,46.236,-84.039,46.177,-83.979,46.109,-83.93,46.092,-83.907,46.097,-83.887,46.119,-83.876,46.148,-83.856,46.157,-83.829,46.148],[-84.169,46.324,-84.188,46.323,-84.196,46.303,-84.185,46.279,-84.135,46.24,-84.121,46.246,-84.116,46.261,-84.12,46.285,-84.137,46.306,-84.169,46.324],[-84.133,46.341,-84.133,46.362,-84.164,46.41,-84.165,46.457,-84.137,46.503,-84.142,46.531,-84.18,46.542,-84.216,46.536,-84.274,46.488,-84.235,46.445,-84.209,46.384,-84.189,46.36,-84.163,46.346,-84.133,46.341],[-84.429,45.807,-84.557,45.82,-84.563,45.804,-84.487,45.754,-84.441,45.734,-84.412,45.736,-84.39,45.747,-84.375,45.765,-84.38,45.788,-84.417,45.814,-84.429,45.807],[-82.962,45.833,-82.937,45.811,-82.736,45.797,-82.626,45.772,-82.51,45.727,-82.409,45.698,-82.323,45.684,-82.256,45.66,-82.208,45.627,-82.091,45.591,-82.076,45.57,-82.055,45.561,-82.029,45.566,-81.821,45.692,-81.76,45.698,-81.76,45.685,-81.865,45.611,-81.902,45.603,-81.922,45.604,-81.977,45.57,-81.977,45.561,-81.946,45.566,-81.914,45.557,-81.881,45.535,-81.828,45.547,-81.755,45.591,-81.686,45.657,-81.593,45.792,-81.601,45.801,-81.685,45.801,-81.699,45.815,-81.673,45.845,-81.665,45.872,-81.675,45.896,-81.698,45.894,-81.735,45.864,-81.763,45.821,-81.796,45.74,-81.804,45.743,-81.838,45.788,-81.842,45.823,-81.828,45.862,-81.841,45.885,-81.879,45.892,-81.897,45.91,-81.895,45.939,-81.909,45.962,-81.939,45.979,-81.987,45.977,-82.055,45.955,-82.106,45.919,-82.143,45.869,-82.175,45.871,-82.205,45.924,-82.236,45.946,-82.268,45.936,-82.288,45.945,-82.295,45.973,-82.332,45.982,-82.401,45.971,-82.444,45.955,-82.463,45.934,-82.481,45.93,-82.498,45.944,-82.527,45.939,-82.569,45.914,-82.58,45.896,-82.556,45.87,-82.56,45.86,-82.52,45.849,-82.515,45.833,-82.531,45.806,-82.554,45.807,-82.583,45.837,-82.608,45.848,-82.627,45.839,-82.645,45.843,-82.66,45.86,-82.691,45.863,-82.737,45.852,-82.765,45.858,-82.774,45.88,-82.793,45.898,-82.823,45.91,-82.832,45.936,-82.821,45.977,-82.845,45.99,-82.905,45.978,-82.945,45.963,-82.965,45.945,-82.996,45.943,-83.035,45.957,-83.069,45.955,-83.097,45.935,-83.125,45.938,-83.151,45.963,-83.176,45.969,-83.201,45.955,-83.216,45.933,-83.222,45.902,-83.16,45.875,-82.962,45.833],[-81.713,45.526,-81.735,45.531,-81.829,45.498,-81.837,45.484,-81.833,45.473,-81.815,45.463,-81.788,45.466,-81.727,45.5,-81.713,45.526],[-80.173,44.796,-80.174,44.809,-80.158,44.848,-80.171,44.865,-80.216,44.872,-80.236,44.865,-80.233,44.843,-80.216,44.823,-80.173,44.796]]]},{"n":"Laguna Madre","z":2,"b":[-97.833,23.915,-97.427,25.447],"p":[[[-97.427,25.354,-97.456,25.273,-97.537,25.082,-97.612,24.782,-97.687,24.476,-97.72,24.255,-97.758,23.915,-97.765,23.917,-97.775,24.082,-97.759,24.159,-97.75,24.264,-97.741,24.391,-97.768,24.436,-97.805,24.444,-97.828,24.477,-97.833,24.517,-97.824,24.544,-97.8,24.546,-97.742,24.502,-97.725,24.501,-97.689,24.663,-97.7,24.709,-97.703,24.845,-97.643,24.908,-97.655,24.937,-97.707,24.966,-97.729,24.994,-97.699,25.159,-97.7,25.201,-97.706,25.225,-97.754,25.257,-97.721,25.288,-97.68,25.346,-97.654,25.365,-97.643,25.364,-97.611,25.326,-97.576,25.312,-97.543,25.312,-97.531,25.327,-97.518,25.435,-97.508,25.447,-97.473,25.44,-97.438,25.406,-97.427,25.354]]]},{"n":"Wollaston","z":2,"b":[-103.832,57.695,-102.791,58.552],"p":[[[-102.837,58.33,-102.862,58.318,-102.911,58.323,-102.948,58.306,-102.982,58.275,-103.001,58.243,-102.983,58.22,-102.939,58.198,-102.941,58.173,-103.056,58.124,-103.102,58.134,-103.231,58.081,-103.259,58.053,-103.281,57.984,-103.281,57.897,-103.269,57.87,-103.245,57.851,-103.218,57.844,-103.211,57.828,-103.269,57.738,-103.33,57.697,-103.36,57.695,-103.347,57.729,-103.306,57.778,-103.314,57.828,-103.359,57.861,-103.399,57.868,-103.469,57.877,-103.559,57.869,-103.728,57.796,-103.794,57.777,-103.829,57.779,-103.832,57.794,-103.796,57.823,-103.656,57.919,-103.67,57.939,-103.777,57.932,-103.803,57.947,-103.793,57.964,-103.697,58.019,-103.673,58.037,-103.672,58.054,-103.682,58.078,-103.709,58.102,-103.706,58.127,-103.654,58.173,-103.619,58.234,-103.613,58.267,-103.619,58.283,-103.662,58.284,-103.627,58.348,-103.61,58.409,-103.581,58.431,-103.55,58.455,-103.495,58.462,-103.384,58.432,-103.239,58.472,-103.223,58.485,-103.221,58.525,-103.205,58.544,-103.185,58.552,-103.141,58.533,-102.952,58.533,-102.946,58.503,-102.921,58.477,-102.803,58.412,-102.791,58.381,-102.837,58.33]]]},{"n":"Athabasca","z":2,"b":[-111.301,58.505,-106.018,59.6],"p":[[[-109.233,59.127,-109.279,59.127,-109.453,59.083,-109.886,59.006,-110.074,58.949,-110.276,58.86,-110.336,58.826,-110.403,58.768,-110.44,58.716,-110.435,58.699,-110.37,58.676,-110.376,58.644,-110.411,58.631,-110.673,58.63,-110.747,58.614,-110.777,58.583,-110.787,58.546,-110.812,58.512,-110.839,58.511,-110.877,58.629,-110.903,58.631,-110.944,58.621,-110.99,58.594,-111.036,58.522,-111.075,58.505,-111.093,58.527,-111.107,58.547,-111.11,58.572,-111.098,58.616,-111.262,58.703,-111.295,58.729,-111.301,58.756,-111.28,58.788,-111.194,58.788,-111.15,58.775,-111.161,58.739,-111.125,58.735,-111.019,58.764,-110.872,58.837,-110.612,59.041,-110.483,59.11,-110.352,59.156,-110.035,59.297,-109.875,59.357,-109.835,59.389,-109.777,59.413,-109.799,59.433,-109.801,59.445,-109.705,59.504,-109.623,59.543,-109.547,59.57,-109.398,59.597,-109.26,59.6,-109.172,59.587,-109.036,59.498,-108.828,59.51,-108.795,59.504,-108.863,59.406,-108.857,59.387,-108.748,59.387,-108.538,59.435,-108.219,59.473,-107.953,59.434,-107.558,59.396,-107.239,59.348,-106.816,59.327,-106.609,59.32,-106.021,59.29,-106.018,59.271,-106.042,59.252,-106.71,59.263,-107.205,59.284,-107.452,59.304,-107.589,59.292,-107.606,59.3,-107.626,59.331,-107.708,59.34,-107.734,59.336,-107.757,59.32,-107.786,59.288,-107.819,59.229,-107.837,59.216,-107.958,59.204,-108.211,59.161,-108.75,59.12,-108.942,59.115,-109.103,59.098,-109.163,59.102,-109.233,59.127]]]},{"n":"Mistassini","z":2,"b":[-74.22,50.038,-72.69,51.397],"p":[[[-72.9,51.146,-72.988,51.104,-73.114,50.997,-73.135,50.989,-73.254,50.913,-73.371,50.845,-73.459,50.793,-73.458,50.814,-73.394,50.893,-73.262,50.977,-73.189,51.069,-73.175,51.071,-73.151,51.07,-73.132,51.09,-73.129,51.106,-73.143,51.145,-73.151,51.156,-73.173,51.158,-73.293,51.077,-73.396,50.994,-73.573,50.835,-73.599,50.825,-73.631,50.824,-73.771,50.641,-73.8,50.595,-73.817,50.499,-73.81,50.452,-73.799,50.412,-73.788,50.39,-73.807,50.388,-73.832,50.413,-73.843,50.406,-73.837,50.341,-73.819,50.301,-73.8,50.236,-73.818,50.211,-73.904,50.162,-74.008,50.12,-74.034,50.091,-74.076,50.043,-74.152,50.038,-74.22,50.058,-74.2,50.076,-74.098,50.092,-74.013,50.178,-73.879,50.218,-73.828,50.249,-73.836,50.276,-73.88,50.297,-73.896,50.327,-73.899,50.352,-73.878,50.381,-73.839,50.639,-73.887,50.59,-73.916,50.544,-73.926,50.514,-73.917,50.489,-73.919,50.474,-73.932,50.458,-74.002,50.478,-74.105,50.399,-74.132,50.403,-74.104,50.448,-73.954,50.722,-73.845,50.888,-73.856,50.898,-73.911,50.897,-73.916,50.91,-73.89,50.954,-73.905,50.963,-73.892,50.981,-73.641,51.078,-73.378,51.24,-73.202,51.32,-73.067,51.39,-72.986,51.397,-72.984,51.378,-73.067,51.317,-73.04,51.309,-72.89,51.354,-72.811,51.361,-72.792,51.353,-72.8,51.335,-72.851,51.305,-73.067,51.206,-73.092,51.178,-73.072,51.16,-73.041,51.154,-72.811,51.244,-72.723,51.254,-72.69,51.241,-72.719,51.217,-72.9,51.146]]]},{"n":"IJsselmeer","z":2,"b":[5.014,52.252,5.87,53.065],"p":[[[5.867,52.563,5.87,52.485,5.795,52.428,5.495,52.252,5.316,52.292,5.04,52.344,5.014,52.364,5.036,52.405,5.062,52.431,5.069,52.451,5.07,52.52,5.06,52.566,5.066,52.624,5.138,52.627,5.242,52.67,5.247,52.73,5.165,52.786,5.118,52.836,5.094,52.886,5.068,52.923,5.152,52.967,5.269,53.027,5.377,53.065,5.398,53.042,5.413,52.903,5.42,52.874,5.455,52.857,5.547,52.846,5.657,52.851,5.667,52.817,5.625,52.782,5.618,52.758,5.613,52.684,5.619,52.658,5.653,52.63,5.835,52.609,5.867,52.563],[5.801,52.466,5.834,52.502,5.829,52.535,5.797,52.565,5.736,52.582,5.658,52.588,5.539,52.558,5.377,52.49,5.169,52.379,5.184,52.358,5.242,52.334,5.424,52.298,5.499,52.302,5.548,52.324,5.621,52.376,5.741,52.429,5.801,52.466]]]},{"n":"Rybinsk","z":2,"b":[37.151,58.016,39.005,59.21],"p":[[[38.681,58.108,38.612,58.113,38.383,58.122,38.357,58.1,38.437,58.036,38.437,58.018,38.415,58.016,38.369,58.032,38.31,58.041,38.281,58.054,38.259,58.071,38.25,58.103,38.251,58.199,38.221,58.237,38.149,58.249,37.944,58.301,37.897,58.292,37.507,58.535,37.417,58.613,37.317,58.657,37.155,58.695,37.155,58.707,37.169,58.726,37.151,58.849,37.308,58.76,37.372,58.731,37.423,58.727,37.41,58.689,37.41,58.67,37.459,58.634,37.538,58.613,37.565,58.553,37.609,58.545,37.661,58.557,37.667,58.572,37.621,58.626,37.632,58.636,37.715,58.618,37.768,58.618,37.995,58.55,38.074,58.542,38.074,58.592,38.103,58.619,38.096,58.634,37.988,58.672,37.984,58.692,37.945,58.72,37.942,58.735,37.961,58.744,38.024,58.746,37.996,58.862,37.918,58.858,37.869,58.897,37.828,58.914,37.777,58.926,37.714,58.928,37.642,58.919,37.581,58.899,37.501,58.845,37.471,58.857,37.466,58.875,37.484,58.927,37.499,58.944,37.609,58.989,37.673,59.025,37.696,59.064,37.676,59.102,37.64,59.138,37.645,59.157,37.843,59.155,37.94,59.174,37.987,59.177,38.002,59.16,38.007,59.144,38.027,59.128,38.116,59.103,38.159,59.102,38.18,59.113,38.18,59.132,38.176,59.148,38.124,59.203,38.143,59.21,38.183,59.203,38.294,59.161,38.415,59.173,38.407,59.152,38.332,59.093,38.239,59.063,38.171,59.065,37.91,59.103,37.866,59.094,37.858,59.075,37.92,59.042,38.047,58.994,38.119,58.955,38.427,58.747,38.61,58.637,38.753,58.547,38.989,58.496,39.004,58.476,38.97,58.441,39,58.387,39.005,58.358,38.871,58.331,38.83,58.296,38.812,58.251,38.819,58.135,38.805,58.122,38.779,58.135,38.692,58.207,38.638,58.22,38.619,58.201,38.698,58.133,38.704,58.113,38.681,58.108]]]},{"n":"Issyk-Kul","z":2,"b":[76.215,42.168,78.303,42.734],"p":[[[78.303,42.588,78.292,42.564,78.158,42.55,78.014,42.487,77.871,42.428,77.852,42.402,77.854,42.367,77.716,42.24,77.595,42.2,77.515,42.182,77.137,42.168,77.023,42.173,76.921,42.19,76.578,42.291,76.432,42.32,76.329,42.33,76.263,42.328,76.227,42.389,76.215,42.435,76.226,42.461,76.453,42.489,76.747,42.565,76.965,42.612,77.19,42.647,77.337,42.67,77.535,42.651,77.684,42.702,78.064,42.727,78.249,42.734,78.151,42.672,78.119,42.642,78.136,42.608,78.303,42.588]]]},{"n":"Woods","z":2,"b":[-95.315,48.863,-94.041,49.993],"p":[[[-94.052,49.373,-94.041,49.358,-94.047,49.268,-94.065,49.238,-94.092,49.219,-94.119,49.206,-94.129,49.21,-94.115,49.231,-94.109,49.263,-94.182,49.33,-94.221,49.394,-94.278,49.407,-94.251,49.457,-94.254,49.471,-94.365,49.483,-94.474,49.472,-94.646,49.445,-94.745,49.409,-94.77,49.393,-94.79,49.353,-94.778,49.34,-94.64,49.354,-94.609,49.345,-94.577,49.298,-94.522,49.267,-94.464,49.232,-94.354,49.266,-94.303,49.253,-94.272,49.227,-94.267,49.194,-94.24,49.181,-94.099,49.167,-94.074,49.155,-94.117,49.126,-94.197,49.096,-94.265,49.114,-94.31,49.104,-94.324,49.071,-94.537,48.945,-94.713,48.863,-94.818,48.868,-94.916,48.926,-95.003,48.964,-95.037,48.959,-95.179,48.897,-95.274,48.886,-95.313,48.928,-95.315,48.97,-95.267,49.029,-95.266,49.06,-95.277,49.105,-95.261,49.137,-95.199,49.166,-95.085,49.176,-94.983,49.195,-94.936,49.24,-94.941,49.321,-95.009,49.342,-95.014,49.368,-94.941,49.4,-94.909,49.437,-94.822,49.512,-94.691,49.561,-94.645,49.603,-94.65,49.621,-94.688,49.626,-94.745,49.616,-94.796,49.599,-94.911,49.526,-94.985,49.497,-95.107,49.463,-95.139,49.472,-95.116,49.543,-95.152,49.622,-95.099,49.639,-95.008,49.635,-94.962,49.621,-94.973,49.584,-94.958,49.575,-94.926,49.576,-94.888,49.585,-94.84,49.613,-94.87,49.663,-94.823,49.69,-94.588,49.724,-94.56,49.764,-94.576,49.807,-94.678,49.953,-94.659,49.974,-94.615,49.993,-94.588,49.988,-94.539,49.931,-94.53,49.853,-94.496,49.804,-94.451,49.738,-94.346,49.724,-94.312,49.685,-94.332,49.639,-94.314,49.6,-94.362,49.576,-94.368,49.563,-94.302,49.515,-94.173,49.497,-94.134,49.448,-94.144,49.427,-94.052,49.373],[-94.702,49.191,-94.787,49.192,-94.784,49.17,-94.731,49.092,-94.657,49.09,-94.631,49.056,-94.594,49.036,-94.528,49.041,-94.506,49.118,-94.446,49.179,-94.448,49.202,-94.515,49.215,-94.621,49.191,-94.702,49.191]]]},{"n":"Nipigon","z":2,"b":[-89.092,49.389,-88.03,50.291],"p":[[[-88.03,49.961,-88.106,49.934,-88.145,49.912,-88.143,49.895,-88.108,49.868,-88.129,49.785,-88.112,49.711,-88.134,49.635,-88.166,49.58,-88.159,49.507,-88.18,49.466,-88.305,49.459,-88.368,49.434,-88.406,49.389,-88.439,49.394,-88.467,49.443,-88.494,49.463,-88.513,49.514,-88.542,49.549,-88.552,49.544,-88.547,49.488,-88.563,49.462,-88.622,49.428,-88.668,49.433,-88.706,49.442,-88.773,49.458,-88.786,49.488,-88.759,49.527,-88.71,49.587,-88.61,49.606,-88.59,49.608,-88.587,49.628,-88.607,49.647,-88.688,49.633,-88.744,49.631,-88.786,49.567,-88.823,49.54,-88.869,49.521,-88.909,49.51,-88.96,49.501,-89,49.529,-89.019,49.568,-89.016,49.583,-88.96,49.56,-88.92,49.56,-88.907,49.579,-88.906,49.618,-88.929,49.694,-88.93,49.755,-88.956,49.756,-89.008,49.714,-89.045,49.726,-89.092,49.776,-89.087,49.804,-89.069,49.824,-89.004,49.787,-88.949,49.81,-88.941,49.82,-88.95,49.855,-88.945,49.88,-88.888,49.933,-88.905,50.023,-88.893,50.057,-88.769,50.213,-88.705,50.254,-88.639,50.277,-88.551,50.239,-88.465,50.238,-88.356,50.162,-88.315,50.148,-88.288,50.15,-88.278,50.159,-88.288,50.178,-88.34,50.226,-88.415,50.265,-88.421,50.289,-88.384,50.291,-88.344,50.275,-88.232,50.213,-88.199,50.188,-88.171,50.135,-88.172,50.113,-88.204,50.111,-88.215,50.096,-88.222,50.059,-88.218,50.027,-88.235,49.991,-88.213,49.993,-88.169,50.028,-88.133,50.035,-88.084,50.001,-88.033,49.979,-88.03,49.961],[-88.554,50.022,-88.554,50.036,-88.59,50.047,-88.616,50.047,-88.63,50.044,-88.633,50.017,-88.639,49.985,-88.654,49.95,-88.688,49.927,-88.719,49.9,-88.747,49.879,-88.748,49.868,-88.716,49.85,-88.676,49.85,-88.621,49.82,-88.588,49.805,-88.582,49.816,-88.59,49.836,-88.611,49.862,-88.636,49.897,-88.636,49.916,-88.625,49.951,-88.588,49.983,-88.567,49.997,-88.554,50.022],[-88.385,49.647,-88.374,49.689,-88.393,49.7,-88.422,49.685,-88.489,49.66,-88.49,49.612,-88.399,49.617,-88.385,49.647]]]},{"n":"Reindeer","z":2,"b":[-103.298,56.277,-101.533,58.107],"p":[[[-101.724,57.91,-101.678,57.918,-101.544,57.92,-101.533,57.88,-101.543,57.816,-101.63,57.757,-101.735,57.647,-101.828,57.579,-101.961,57.517,-102.048,57.484,-102.106,57.427,-102.094,57.368,-101.888,57.305,-101.836,57.27,-101.864,57.25,-101.957,57.239,-102.032,57.191,-102.048,57.114,-101.999,57.054,-101.962,57.025,-102.041,56.965,-102.127,56.915,-102.177,56.864,-102.313,56.838,-102.34,56.81,-102.356,56.771,-102.398,56.697,-102.437,56.658,-102.439,56.627,-102.465,56.608,-102.544,56.588,-102.601,56.564,-102.885,56.473,-102.927,56.441,-102.938,56.402,-102.977,56.369,-103.044,56.38,-103.087,56.419,-103.137,56.414,-103.201,56.358,-103.232,56.309,-103.241,56.278,-103.272,56.277,-103.288,56.304,-103.271,56.387,-103.261,56.437,-103.257,56.475,-103.298,56.496,-103.293,56.543,-103.241,56.574,-103.192,56.555,-103.153,56.62,-103.129,56.764,-103.106,56.853,-103.069,56.849,-103.08,56.677,-103.051,56.569,-102.978,56.562,-102.862,56.683,-102.794,56.709,-102.732,56.716,-102.692,56.742,-102.677,56.773,-102.625,56.787,-102.57,56.83,-102.544,56.93,-102.554,56.998,-102.584,57.046,-102.676,57.083,-102.742,57.101,-102.722,57.154,-102.693,57.21,-102.721,57.245,-102.789,57.283,-102.852,57.299,-102.919,57.295,-102.933,57.364,-102.89,57.451,-102.791,57.504,-102.694,57.564,-102.674,57.597,-102.693,57.637,-102.692,57.688,-102.64,57.702,-102.523,57.73,-102.44,57.799,-102.205,57.931,-102.149,57.993,-102.151,58.038,-102.052,58.107,-101.991,58.088,-102.013,58.003,-101.971,57.963,-101.913,57.991,-101.868,58.054,-101.819,58.065,-101.803,58.02,-101.847,57.898,-101.846,57.866,-101.815,57.843,-101.733,57.873,-101.724,57.91]]]},{"n":"Oneida","z":3,"b":[-76.096,43.177,-75.733,43.263],"p":[[[-75.75,43.226,-75.733,43.206,-75.8,43.181,-75.959,43.177,-76.096,43.238,-76.096,43.254,-76.06,43.263,-75.861,43.248,-75.75,43.226]]]},{"n":"Iliamna","z":3,"b":[-155.914,59.335,-153.971,59.791],"p":[[[-153.971,59.769,-154.109,59.718,-154.23,59.687,-154.371,59.665,-154.534,59.628,-154.563,59.592,-154.555,59.548,-154.579,59.503,-154.583,59.474,-154.614,59.451,-154.685,59.443,-154.907,59.432,-155.743,59.335,-155.881,59.377,-155.914,59.456,-155.846,59.538,-155.26,59.662,-154.993,59.705,-154.328,59.791,-154.19,59.781,-154.074,59.78,-153.971,59.769]]]},{"n":"","z":3,"b":[-156.884,57.633,-155.961,58.086],"p":[[[-155.961,57.87,-156.021,57.837,-156.021,57.812,-155.996,57.793,-155.993,57.763,-156.04,57.705,-156.13,57.64,-156.159,57.633,-156.127,57.689,-156.115,57.749,-156.161,57.797,-156.358,57.836,-156.605,57.879,-156.678,57.888,-156.832,57.931,-156.884,57.999,-156.817,58.067,-156.638,58.086,-156.279,58.006,-155.976,57.909,-155.961,57.87]]]},{"n":"Tahoe","z":3,"b":[-120.139,38.945,-119.918,39.254],"p":[[[-119.943,39.025,-119.966,38.957,-120.033,38.945,-120.081,38.971,-120.113,39.06,-120.139,39.091,-120.132,39.143,-120.059,39.221,-119.986,39.242,-119.962,39.254,-119.936,39.254,-119.918,39.207,-119.943,39.025]]]},{"n":"","z":3,"b":[35.197,-14.739,35.324,-14.51],"p":[[[35.324,-14.619,35.293,-14.684,35.27,-14.733,35.249,-14.739,35.197,-14.682,35.198,-14.58,35.208,-14.531,35.24,-14.51,35.268,-14.517,35.298,-14.563,35.324,-14.619]]]},{"n":"Zaysan","z":3,"b":[83.027,47.699,84.516,49.726],"p":[[[84.516,47.93,84.427,47.857,84.394,47.816,84.357,47.732,84.296,47.699,84.242,47.706,83.994,47.843,83.766,47.951,83.625,48.013,83.445,48.049,83.259,48.095,83.083,48.178,83.027,48.212,83.039,48.25,83.182,48.249,83.275,48.261,83.435,48.27,83.498,48.305,83.533,48.375,83.461,48.432,83.392,48.5,83.387,48.554,83.417,48.597,83.423,48.743,83.471,48.858,83.589,48.962,83.747,49.039,83.872,49.063,84.036,49.111,84.121,49.159,84.1,49.238,83.972,49.329,83.849,49.417,83.724,49.44,83.66,49.443,83.608,49.486,83.539,49.532,83.479,49.538,83.426,49.573,83.365,49.626,83.364,49.644,83.391,49.652,83.608,49.606,83.74,49.631,83.89,49.72,83.964,49.726,83.961,49.703,83.818,49.634,83.797,49.597,83.757,49.564,83.729,49.535,83.823,49.484,84.288,49.238,84.361,49.161,84.329,49.125,84.149,49.086,83.864,48.994,83.777,48.975,83.609,48.91,83.514,48.813,83.487,48.718,83.517,48.617,83.613,48.437,83.774,48.41,83.919,48.397,83.907,48.374,83.682,48.353,83.596,48.303,83.581,48.267,83.634,48.242,83.74,48.198,83.881,48.152,84.051,48.083,84.167,48.056,84.334,48.023,84.468,47.969,84.516,47.93]]]},{"n":"Winnebago","z":3,"b":[-88.519,43.808,-88.321,44.196],"p":[[[-88.325,44.068,-88.334,43.938,-88.383,43.866,-88.431,43.808,-88.465,43.813,-88.492,43.939,-88.519,44.029,-88.484,44.11,-88.418,44.195,-88.358,44.196,-88.321,44.18,-88.325,44.068]]]},{"n":"Nettilling","z":3,"b":[-71.438,65.954,-69.221,67.028],"p":[[[-71.438,66.596,-71.421,66.628,-71.373,66.663,-71.331,66.726,-71.209,66.862,-71.194,66.945,-71.227,67.001,-71.174,67.028,-71.001,67.02,-70.826,66.994,-70.788,66.967,-70.737,66.923,-70.63,66.868,-70.611,66.832,-70.569,66.794,-70.463,66.753,-70.353,66.737,-70.288,66.748,-70.211,66.731,-69.945,66.688,-69.736,66.628,-69.621,66.602,-69.49,66.581,-69.466,66.547,-69.527,66.503,-69.646,66.476,-69.646,66.442,-69.553,66.432,-69.423,66.429,-69.402,66.432,-69.347,66.449,-69.253,66.451,-69.221,66.406,-69.34,66.335,-69.471,66.29,-69.511,66.268,-69.53,66.238,-69.557,66.212,-69.585,66.203,-69.636,66.219,-69.648,66.246,-69.684,66.243,-69.732,66.22,-69.742,66.205,-69.77,66.203,-69.89,66.254,-69.975,66.258,-70.185,66.22,-70.376,66.195,-70.519,66.265,-70.582,66.289,-70.619,66.282,-70.656,66.252,-70.677,66.226,-70.71,66.204,-70.851,66.156,-71.104,65.991,-71.226,65.954,-71.311,65.969,-71.321,65.987,-71.221,66.024,-71.179,66.083,-71.194,66.151,-71.145,66.215,-71.15,66.256,-71.147,66.29,-71.127,66.331,-71.163,66.372,-71.231,66.418,-71.306,66.515,-71.438,66.596]]]},{"n":"Turkana","z":3,"b":[35.847,2.405,36.697,4.654],"p":[[[36.685,2.538,36.633,2.425,36.571,2.405,36.535,2.448,36.424,2.734,36.331,2.874,36.242,2.966,36.2,2.974,36.154,3.038,36.113,3.167,36.038,3.264,35.966,3.368,35.937,3.482,35.915,3.519,35.872,3.598,35.847,3.775,35.857,3.876,35.888,4.038,35.919,4.27,35.926,4.385,35.95,4.553,35.995,4.566,36.045,4.539,36.074,4.555,36.098,4.623,36.132,4.653,36.155,4.654,36.174,4.632,36.197,4.431,36.215,4.148,36.213,4.054,36.226,3.88,36.221,3.473,36.311,3.207,36.427,3.011,36.538,2.924,36.652,2.858,36.697,2.727,36.685,2.538]]]},{"n":"Dubawnt","z":3,"b":[-102.174,62.664,-100.644,63.546],"p":[[[-101.935,63.221,-101.807,63.267,-101.749,63.337,-101.688,63.37,-101.635,63.375,-101.611,63.382,-101.583,63.397,-101.584,63.428,-101.548,63.456,-101.457,63.443,-101.396,63.418,-101.365,63.397,-101.319,63.394,-101.293,63.415,-101.332,63.467,-101.276,63.491,-101.137,63.43,-101.081,63.439,-101.061,63.46,-101.008,63.463,-100.902,63.53,-100.856,63.52,-100.799,63.54,-100.706,63.546,-100.644,63.468,-100.673,63.373,-100.77,63.319,-100.823,63.297,-100.864,63.294,-100.917,63.301,-100.983,63.303,-101.041,63.292,-101.06,63.268,-101.064,63.243,-101.113,63.171,-101.104,63.102,-101.061,63.06,-101.007,63.052,-100.884,63.007,-100.884,62.956,-100.982,62.911,-101.028,62.896,-101.059,62.858,-101.023,62.803,-101.028,62.761,-101.138,62.713,-101.29,62.666,-101.382,62.683,-101.392,62.721,-101.354,62.766,-101.322,62.828,-101.33,62.891,-101.381,62.928,-101.414,62.911,-101.434,62.845,-101.473,62.791,-101.499,62.74,-101.512,62.675,-101.529,62.664,-101.554,62.684,-101.553,62.723,-101.59,62.776,-101.673,62.797,-101.755,62.801,-101.823,62.881,-101.916,62.877,-101.989,62.887,-102.035,62.925,-102.048,63.004,-102.091,63.066,-102.097,63.076,-102.127,63.089,-102.174,63.122,-102.16,63.165,-102.079,63.194,-101.935,63.221]]]},{"n":"Baker","z":3,"b":[-96.273,63.996,-94.225,64.325],"p":[[[-94.626,64.159,-94.556,64.149,-94.456,64.14,-94.346,64.135,-94.247,64.124,-94.225,64.076,-94.312,64.012,-94.455,63.996,-94.624,64.031,-94.86,64.066,-95.299,64.079,-95.502,64.058,-95.671,64.056,-95.784,64.056,-95.839,64.084,-95.946,64.127,-96.227,64.193,-96.273,64.301,-96.203,64.325,-95.744,64.302,-95.251,64.274,-95.037,64.239,-94.917,64.225,-94.793,64.217,-94.65,64.197,-94.626,64.159]]]},{"n":"Vistula Lagoon","z":3,"b":[19.717,54.434,20.405,54.71],"p":[[[19.764,54.434,19.717,54.482,19.879,54.589,19.942,54.647,19.958,54.682,19.988,54.71,20.033,54.702,20.084,54.673,20.208,54.684,20.405,54.682,20.102,54.583,19.929,54.517,19.769,54.438,19.764,54.434]]]},{"n":"Vistula Lagoon","z":3,"b":[19.251,54.268,19.764,54.482],"p":[[[19.717,54.482,19.764,54.434,19.738,54.412,19.658,54.374,19.586,54.348,19.481,54.3,19.347,54.268,19.251,54.311,19.279,54.338,19.503,54.381,19.616,54.426,19.717,54.482]]]},{"n":"Champlain","z":3,"b":[-73.451,43.757,-73.088,45.078],"p":[[[-73.373,44.148,-73.405,44.054,-73.369,43.876,-73.356,43.781,-73.368,43.757,-73.386,43.81,-73.403,43.866,-73.43,44.005,-73.447,44.031,-73.451,44.083,-73.444,44.193,-73.354,44.281,-73.377,44.393,-73.4,44.431,-73.444,44.638,-73.4,44.775,-73.383,44.936,-73.347,45.018,-73.324,45.029,-73.308,44.956,-73.296,44.914,-73.285,44.913,-73.255,44.942,-73.204,45.022,-73.147,45.073,-73.104,45.078,-73.088,45.046,-73.111,45.009,-73.163,44.985,-73.193,44.957,-73.173,44.919,-73.176,44.874,-73.194,44.707,-73.214,44.586,-73.227,44.563,-73.263,44.542,-73.256,44.507,-73.229,44.493,-73.228,44.445,-73.236,44.433,-73.248,44.437,-73.271,44.397,-73.288,44.32,-73.326,44.234,-73.373,44.148]]]},{"n":"Salton Sea","z":3,"b":[-116.062,33.138,-115.612,33.522],"p":[[[-115.612,33.274,-115.616,33.223,-115.698,33.138,-115.789,33.147,-115.911,33.303,-116.062,33.433,-116.053,33.502,-116.01,33.522,-115.948,33.519,-115.884,33.452,-115.66,33.321,-115.612,33.274]]]},{"n":"Martre","z":3,"b":[-118.628,63.113,-117.306,63.57],"p":[[[-117.319,63.209,-117.306,63.185,-117.36,63.146,-117.554,63.113,-117.738,63.113,-117.785,63.133,-118.107,63.223,-118.137,63.25,-118.164,63.261,-118.201,63.283,-118.315,63.291,-118.467,63.31,-118.627,63.337,-118.628,63.375,-118.564,63.428,-118.344,63.513,-118.1,63.57,-118.001,63.544,-117.915,63.466,-117.764,63.404,-117.63,63.384,-117.575,63.372,-117.57,63.358,-117.47,63.305,-117.435,63.298,-117.339,63.224,-117.319,63.209]]]},{"n":"Llanquihue","z":3,"b":[-72.994,-41.294,-72.599,-40.974],"p":[[[-72.967,-41.285,-72.989,-41.228,-72.994,-41.15,-72.974,-41.087,-72.912,-41.021,-72.849,-40.974,-72.776,-40.989,-72.703,-41.026,-72.639,-41.1,-72.599,-41.184,-72.629,-41.214,-72.727,-41.2,-72.876,-41.294,-72.967,-41.285]]]},{"n":"Hongze Lake","z":3,"b":[117.809,32.885,118.861,33.645],"p":[[[118.605,33.172,118.574,33.141,118.519,33.036,118.492,32.998,118.431,32.973,118.267,32.925,118.227,32.897,118.183,32.885,118.171,32.906,118.192,32.941,118.181,32.971,118.142,32.986,118.088,32.958,118.066,32.936,118.05,32.964,118.084,33.005,118.13,33.028,118.226,32.991,118.24,33.008,118.234,33.046,118.182,33.139,118.102,33.144,117.979,33.142,117.915,33.126,117.892,33.065,117.838,33.038,117.809,33.048,117.851,33.062,117.874,33.093,117.886,33.157,117.912,33.181,117.992,33.187,118.114,33.215,118.175,33.21,118.227,33.143,118.275,33.045,118.34,32.994,118.407,33.01,118.417,33.038,118.44,33.068,118.46,33.099,118.47,33.128,118.454,33.16,118.322,33.182,118.249,33.221,118.233,33.288,118.273,33.267,118.318,33.268,118.392,33.338,118.446,33.407,118.49,33.401,118.57,33.372,118.607,33.376,118.547,33.437,118.488,33.54,118.493,33.623,118.544,33.645,118.571,33.62,118.608,33.515,118.664,33.463,118.774,33.443,118.861,33.357,118.861,33.339,118.834,33.304,118.778,33.176,118.716,33.089,118.681,33.077,118.678,33.124,118.653,33.174,118.605,33.172]]]},{"n":"Khövsgöl Nuur","z":3,"b":[100.179,50.494,100.787,51.61],"p":[[[100.787,51.397,100.77,51.293,100.749,51.222,100.73,51.138,100.685,51.019,100.688,50.983,100.507,50.809,100.472,50.739,100.445,50.659,100.473,50.625,100.473,50.603,100.444,50.568,100.37,50.538,100.193,50.494,100.179,50.531,100.194,50.594,100.228,50.654,100.248,50.743,100.296,51.109,100.284,51.25,100.356,51.352,100.402,51.392,100.388,51.441,100.38,51.466,100.491,51.556,100.525,51.597,100.559,51.61,100.752,51.447,100.787,51.397]]]},{"n":"Uvs","z":3,"b":[92.235,50.005,93.362,50.658],"p":[[[92.316,50.302,92.235,50.413,92.348,50.475,92.546,50.54,92.789,50.617,92.893,50.658,92.965,50.638,92.99,50.608,92.978,50.54,93.076,50.428,93.227,50.364,93.259,50.358,93.254,50.327,93.325,50.308,93.362,50.282,93.301,50.255,93.216,50.251,93.095,50.198,93.018,50.145,92.913,50.056,92.722,50.01,92.601,50.005,92.498,50.053,92.382,50.142,92.337,50.228,92.316,50.302]]]},{"n":"Mweru","z":3,"b":[28.324,-9.505,29.128,-8.513],"p":[[[29.128,-8.662,29.107,-8.74,29.021,-8.922,28.891,-9.092,28.769,-9.272,28.696,-9.396,28.592,-9.407,28.521,-9.404,28.504,-9.441,28.51,-9.485,28.474,-9.505,28.377,-9.503,28.333,-9.45,28.324,-9.364,28.326,-9.237,28.367,-9.101,28.643,-8.768,28.867,-8.513,28.954,-8.521,29.078,-8.6,29.128,-8.662]]]},{"n":"Khanka","z":3,"b":[132.005,44.529,132.81,45.329],"p":[[[132.81,45.052,132.743,44.943,132.614,44.711,132.46,44.613,132.366,44.529,132.276,44.569,132.238,44.606,132.241,44.63,132.169,44.674,132.069,44.743,132.084,44.851,132.109,44.969,132.015,45.074,132.017,45.127,132.005,45.214,132.071,45.287,132.23,45.328,132.402,45.329,132.579,45.296,132.725,45.23,132.808,45.149,132.81,45.052]]]},{"n":"Tonlé Sap","z":3,"b":[103.699,12.426,104.591,13.25],"p":[[[104.57,12.426,104.532,12.458,104.464,12.506,104.401,12.508,104.258,12.564,104.14,12.671,104.105,12.789,104.008,12.804,103.895,12.867,103.806,12.947,103.755,13.039,103.724,13.121,103.699,13.207,103.731,13.25,103.842,13.235,103.981,13.179,104.1,13.036,104.199,12.845,104.235,12.764,104.268,12.739,104.37,12.747,104.44,12.724,104.468,12.64,104.468,12.571,104.471,12.559,104.498,12.54,104.555,12.514,104.558,12.486,104.565,12.463,104.591,12.432,104.57,12.426]]]},{"n":"Qinghai","z":3,"b":[99.65,36.557,100.753,37.218],"p":[[[100.753,36.811,100.746,36.781,100.723,36.745,100.723,36.673,100.704,36.589,100.628,36.557,100.523,36.58,100.431,36.641,100.228,36.645,99.958,36.679,99.827,36.739,99.7,36.853,99.65,36.911,99.681,36.947,99.762,36.948,99.816,36.955,99.834,36.988,99.804,37.067,99.787,37.149,99.836,37.182,99.956,37.218,100.104,37.202,100.275,37.15,100.448,37.025,100.586,36.903,100.753,36.811]]]},{"n":"Yarroto Vtoroe","z":3,"b":[70.93,67.988,71.381,68.103],"p":[[[71.239,67.99,71.133,68,71.065,67.996,70.971,68.012,70.93,68.065,70.964,68.098,71.093,68.103,71.277,68.059,71.381,68.025,71.381,67.997,71.343,67.988,71.239,67.99]]]},{"n":"Tharthar","z":3,"b":[43.066,33.706,43.493,34.329],"p":[[[43.261,33.728,43.196,33.792,43.106,33.915,43.074,34.113,43.066,34.258,43.084,34.315,43.116,34.329,43.239,34.141,43.362,34.043,43.467,33.95,43.493,33.888,43.476,33.807,43.478,33.729,43.42,33.706,43.332,33.717,43.261,33.728]]]},{"n":"Milh","z":3,"b":[43.423,32.531,43.878,33.079],"p":[[[43.878,32.688,43.863,32.612,43.793,32.535,43.739,32.531,43.668,32.553,43.629,32.58,43.577,32.596,43.482,32.642,43.438,32.715,43.423,32.81,43.432,32.869,43.451,32.879,43.475,32.919,43.469,33.011,43.508,33.06,43.55,33.079,43.583,33.039,43.72,32.808,43.821,32.732,43.878,32.688]]]},{"n":"Dead Sea","z":3,"b":[35.402,31.325,35.585,31.766],"p":[[[35.585,31.724,35.567,31.574,35.545,31.359,35.521,31.327,35.423,31.325,35.403,31.348,35.402,31.492,35.421,31.619,35.463,31.71,35.522,31.756,35.559,31.766,35.585,31.724]]]},{"n":"Pihkva","z":3,"b":[27.497,57.86,28.169,58.215],"p":[[[27.497,58.184,27.569,58.215,27.592,58.175,27.714,58.164,27.865,58.173,27.973,58.133,28.097,58.044,28.149,57.975,28.169,57.915,28.102,57.86,27.992,57.868,27.911,57.896,27.875,57.92,27.882,57.937,27.803,57.99,27.678,58.019,27.624,58.014,27.497,58.184]]]},{"n":"Saint-Jean","z":4,"b":[-72.331,48.444,-71.774,48.755],"p":[[[-72.055,48.454,-72.133,48.469,-72.165,48.481,-72.242,48.575,-72.306,48.619,-72.331,48.669,-72.291,48.733,-72.158,48.755,-72,48.724,-71.907,48.681,-71.846,48.659,-71.776,48.61,-71.774,48.561,-71.781,48.518,-71.823,48.475,-71.924,48.444,-72.055,48.454]]]},{"n":"Keuka","z":4,"b":[-77.203,42.425,-77.058,42.648],"p":[[[-77.058,42.644,-77.107,42.521,-77.191,42.425,-77.203,42.437,-77.17,42.512,-77.148,42.568,-77.113,42.569,-77.077,42.648,-77.058,42.644]]]},{"n":"Seneca","z":4,"b":[-76.97,42.416,-76.876,42.878],"p":[[[-76.879,42.416,-76.91,42.459,-76.918,42.591,-76.954,42.73,-76.97,42.878,-76.954,42.87,-76.882,42.683,-76.883,42.537,-76.876,42.444,-76.879,42.416]]]},{"n":"Cayuga","z":4,"b":[-76.773,42.489,-76.52,42.943],"p":[[[-76.52,42.501,-76.528,42.489,-76.586,42.547,-76.667,42.603,-76.773,42.778,-76.751,42.925,-76.735,42.943,-76.701,42.758,-76.665,42.668,-76.603,42.594,-76.52,42.501]]]},{"n":"Kainji","z":4,"b":[4.4,9.865,4.685,10.644],"p":[[[4.685,10.365,4.679,10.344,4.598,10.196,4.6,10.113,4.617,10.003,4.659,9.885,4.626,9.865,4.594,9.867,4.566,9.903,4.537,10.028,4.496,10.066,4.474,10.07,4.468,10.098,4.495,10.126,4.51,10.148,4.506,10.174,4.47,10.186,4.464,10.196,4.485,10.229,4.465,10.306,4.4,10.375,4.4,10.403,4.438,10.418,4.451,10.438,4.459,10.46,4.47,10.627,4.492,10.644,4.567,10.603,4.606,10.617,4.656,10.607,4.68,10.585,4.65,10.517,4.667,10.41,4.685,10.365]]]},{"n":"Urmia","z":4,"b":[45.015,37.135,45.858,38.264],"p":[[[45.666,37.742,45.747,37.701,45.749,37.663,45.755,37.578,45.858,37.426,45.831,37.341,45.703,37.303,45.663,37.288,45.613,37.212,45.536,37.135,45.449,37.149,45.365,37.227,45.292,37.438,45.257,37.615,45.253,37.685,45.234,37.764,45.087,37.885,45.08,37.988,45.155,38.045,45.188,38.108,45.171,38.144,45.092,38.14,45.015,38.144,45.026,38.193,45.106,38.264,45.357,38.264,45.458,38.173,45.487,38.073,45.447,37.932,45.436,37.806,45.483,37.754,45.546,37.754,45.666,37.742]]]},{"n":"Cree","z":4,"b":[-107.091,57.224,-106.087,57.713],"p":[[[-106.258,57.696,-106.181,57.681,-106.11,57.655,-106.114,57.595,-106.106,57.56,-106.087,57.535,-106.097,57.507,-106.095,57.465,-106.14,57.414,-106.193,57.398,-106.254,57.374,-106.374,57.369,-106.541,57.326,-106.608,57.337,-106.698,57.327,-106.732,57.342,-106.764,57.356,-106.804,57.356,-106.884,57.321,-106.933,57.287,-106.97,57.254,-107.035,57.229,-107.079,57.224,-107.091,57.236,-107.073,57.326,-107.083,57.361,-107.06,57.386,-107.028,57.426,-107.064,57.457,-107.069,57.476,-107.018,57.477,-106.924,57.462,-106.831,57.49,-106.655,57.597,-106.422,57.649,-106.316,57.705,-106.28,57.713,-106.258,57.696]]]},{"n":"","z":4,"b":[-99.764,62.807,-99.142,63.187],"p":[[[-99.606,62.959,-99.558,63.004,-99.348,63.173,-99.298,63.187,-99.303,63.155,-99.332,63.1,-99.304,63.048,-99.234,63.025,-99.159,62.991,-99.142,62.929,-99.24,62.873,-99.386,62.817,-99.573,62.807,-99.73,62.819,-99.762,62.848,-99.764,62.873,-99.606,62.959]]]},{"n":"Kasba","z":4,"b":[-102.6,59.929,-101.849,60.649],"p":[[[-102.549,60.322,-102.485,60.314,-102.379,60.368,-102.315,60.424,-102.367,60.443,-102.437,60.5,-102.434,60.585,-102.352,60.633,-102.261,60.649,-102.215,60.615,-102.177,60.561,-102.158,60.512,-102.108,60.457,-101.998,60.375,-101.886,60.238,-101.849,60.112,-101.878,60.037,-101.885,59.983,-101.983,60.01,-101.988,59.965,-102.023,59.929,-102.085,60.019,-102.275,60.211,-102.382,60.25,-102.477,60.236,-102.588,60.246,-102.6,60.309,-102.549,60.322]]]},{"n":"Sarygamysh","z":4,"b":[57.053,41.573,57.865,42.335],"p":[[[57.865,41.773,57.818,41.7,57.685,41.636,57.527,41.594,57.43,41.579,57.346,41.573,57.247,41.653,57.174,41.735,57.116,41.731,57.062,41.725,57.053,41.773,57.094,41.968,57.183,42.185,57.273,42.303,57.392,42.335,57.55,42.261,57.627,42.15,57.613,42.105,57.632,42.066,57.682,42.01,57.7,41.913,57.845,41.838,57.865,41.773]]]},{"n":"Chany","z":4,"b":[76.734,54.614,78.049,55.057],"p":[[[76.747,54.832,76.869,54.883,77.067,54.919,77.134,54.923,77.164,54.887,77.245,54.856,77.294,54.89,77.307,54.925,77.327,54.968,77.43,55.02,77.57,55.053,77.636,55.057,77.667,55.054,77.696,55.047,77.724,55.025,77.878,55.009,78.033,54.975,78.049,54.927,77.938,54.891,77.807,54.863,77.777,54.859,77.694,54.832,77.643,54.803,77.636,54.769,77.693,54.741,77.73,54.692,77.741,54.661,77.694,54.634,77.6,54.614,77.565,54.624,77.574,54.647,77.522,54.665,77.435,54.684,77.354,54.694,77.245,54.723,77.052,54.732,77.017,54.73,76.943,54.715,76.826,54.701,76.734,54.717,76.753,54.787,76.747,54.832]]]},{"n":"Alakol","z":4,"b":[81.399,45.764,82.133,46.456],"p":[[[82.133,45.764,81.749,45.872,81.555,45.965,81.427,46.055,81.404,46.151,81.41,46.235,81.399,46.305,81.443,46.392,81.53,46.452,81.65,46.456,81.761,46.431,81.841,46.396,81.912,46.351,81.976,46.279,82.025,46.185,82.038,46.08,82.064,45.944,82.133,45.764]]]},{"n":"","z":4,"b":[-90.58,30.179,-90.425,30.348],"p":[[[-90.425,30.274,-90.467,30.198,-90.537,30.179,-90.574,30.197,-90.58,30.235,-90.557,30.284,-90.493,30.348,-90.433,30.326,-90.425,30.274]]]},{"n":"Wiyâshâkimî","z":4,"b":[-74.825,55.935,-73.985,56.336],"p":[[[-73.99,56.026,-74.07,55.975,-74.184,55.957,-74.228,55.946,-74.221,55.935,-74.249,55.938,-74.276,55.951,-74.231,55.985,-74.336,56.024,-74.564,56.067,-74.732,56.091,-74.8,56.137,-74.825,56.214,-74.819,56.273,-74.771,56.308,-74.664,56.336,-74.477,56.318,-74.312,56.262,-74.248,56.161,-74.161,56.144,-74.054,56.137,-73.985,56.097,-73.99,56.026]]]},{"n":"Nipissing","z":4,"b":[-80.447,46.161,-79.436,46.358],"p":[[[-80.447,46.282,-80.409,46.29,-80.346,46.286,-80.314,46.264,-80.253,46.249,-80.147,46.258,-80.118,46.277,-80.143,46.302,-80.125,46.324,-80.053,46.346,-79.856,46.34,-79.779,46.348,-79.653,46.358,-79.494,46.311,-79.436,46.235,-79.487,46.184,-79.573,46.166,-79.772,46.213,-79.876,46.193,-79.986,46.161,-80.006,46.166,-79.991,46.199,-80.015,46.232,-80.063,46.244,-80.293,46.225,-80.447,46.282]]]},{"n":"Hulun","z":4,"b":[116.988,48.561,118.035,49.351],"p":[[[117.229,48.585,117.114,48.561,117.089,48.579,117.105,48.629,117.096,48.705,117.014,48.786,116.988,48.841,117.059,48.907,117.225,49.003,117.389,49.139,117.529,49.27,117.636,49.342,117.69,49.351,117.707,49.322,117.688,49.274,117.686,49.22,117.716,49.18,117.749,49.174,117.776,49.161,117.812,49.125,117.899,49.11,118.007,49.105,118.035,49.071,117.964,49.035,117.875,49.033,117.824,49.059,117.772,49.059,117.733,49.03,117.716,48.986,117.516,48.846,117.328,48.694,117.31,48.636,117.229,48.585]]]},{"n":"Bangweulu","z":4,"b":[29.496,-11.416,30.019,-10.878],"p":[[[29.995,-11.091,29.937,-11.216,29.917,-11.301,29.837,-11.38,29.752,-11.416,29.669,-11.415,29.588,-11.394,29.562,-11.298,29.55,-11.248,29.511,-11.23,29.496,-11.195,29.52,-11.172,29.565,-11.157,29.741,-10.931,29.772,-10.933,29.847,-10.887,29.927,-10.878,30,-10.938,30.019,-11.014,29.995,-11.091]]]},{"n":"Inari","z":4,"b":[27.162,68.715,28.76,69.419],"p":[[[28.447,69.326,28.53,69.336,28.632,69.364,28.677,69.395,28.76,69.419,28.749,69.394,28.696,69.363,28.554,69.299,28.532,69.257,28.566,69.222,28.54,69.192,28.448,69.153,28.442,69.116,28.4,69.082,28.251,69.064,28.091,69.007,28.092,68.981,28.264,68.922,28.329,68.893,28.356,68.872,28.336,68.857,28.295,68.852,28.079,68.876,27.989,68.88,27.891,68.861,27.919,68.828,27.978,68.816,27.992,68.788,27.87,68.742,27.84,68.749,27.836,68.782,27.779,68.792,27.7,68.759,27.655,68.732,27.566,68.749,27.464,68.761,27.434,68.76,27.366,68.751,27.282,68.715,27.26,68.737,27.359,68.771,27.428,68.802,27.417,68.841,27.328,68.884,27.271,68.891,27.172,68.906,27.162,68.923,27.242,68.946,27.484,69.004,27.526,69.031,27.56,69.062,27.745,69.129,27.941,69.178,27.984,69.162,28.088,69.162,28.084,69.19,28.049,69.203,28.011,69.227,27.991,69.254,28.031,69.288,28.101,69.299,28.115,69.271,28.138,69.237,28.232,69.249,28.309,69.284,28.323,69.305,28.345,69.323,28.447,69.326]]]},{"n":"Oulujärvi","z":4,"b":[26.78,64.12,28.188,64.53],"p":[[[26.78,64.391,26.815,64.415,26.831,64.44,26.814,64.469,26.827,64.505,26.866,64.53,27.112,64.523,27.214,64.497,27.217,64.414,27.262,64.369,27.365,64.33,27.469,64.339,27.578,64.38,27.727,64.397,27.925,64.381,27.913,64.333,27.93,64.307,27.883,64.309,27.801,64.304,27.738,64.282,27.765,64.245,27.859,64.233,27.94,64.205,28.058,64.178,28.157,64.16,28.186,64.142,28.188,64.131,28.164,64.121,28.073,64.121,28.009,64.128,27.929,64.145,27.653,64.225,27.562,64.274,27.506,64.27,27.481,64.25,27.421,64.23,27.323,64.209,27.282,64.173,27.288,64.131,27.258,64.12,27.2,64.158,27.073,64.201,26.942,64.238,26.921,64.278,26.793,64.362,26.78,64.391]]]},{"n":"Peipus","z":4,"b":[26.966,58.184,27.821,59.007],"p":[[[27.569,58.215,27.497,58.184,27.473,58.217,27.425,58.303,27.324,58.402,27.237,58.452,27.157,58.648,26.966,58.806,26.974,58.884,27.107,58.954,27.298,58.995,27.524,59.007,27.683,58.98,27.748,58.912,27.814,58.571,27.821,58.485,27.798,58.447,27.748,58.43,27.583,58.306,27.561,58.257,27.569,58.215]]]},{"n":"Kivu","z":4,"b":[28.85,-2.448,29.358,-1.605],"p":[[[29.198,-1.644,29.249,-1.736,29.289,-1.86,29.358,-2.016,29.338,-2.058,29.283,-2.095,29.256,-2.13,29.24,-2.162,29.149,-2.277,29.112,-2.303,29.077,-2.345,29.047,-2.355,28.995,-2.36,28.91,-2.433,28.85,-2.448,28.857,-2.371,28.917,-2.288,28.903,-2.24,28.876,-2.228,28.872,-2.187,28.879,-2.153,28.916,-2.082,28.909,-2.008,28.988,-1.838,29.033,-1.647,29.071,-1.605,29.198,-1.644]]]},{"n":"Edward","z":4,"b":[29.327,-0.64,29.887,-0.12],"p":[[[29.419,-0.63,29.366,-0.64,29.335,-0.577,29.327,-0.53,29.36,-0.452,29.417,-0.339,29.496,-0.18,29.597,-0.134,29.644,-0.12,29.783,-0.121,29.887,-0.172,29.868,-0.303,29.642,-0.507,29.51,-0.612,29.452,-0.607,29.419,-0.63]]]},{"n":"Tai","z":4,"b":[119.939,30.936,120.602,31.532],"p":[[[120.602,31.087,120.394,30.971,120.236,30.936,120.103,30.973,119.978,31.091,119.939,31.253,119.987,31.392,120.108,31.492,120.219,31.532,120.254,31.51,120.256,31.495,120.251,31.475,120.271,31.445,120.325,31.442,120.38,31.429,120.372,31.386,120.331,31.35,120.326,31.32,120.357,31.27,120.391,31.227,120.431,31.2,120.44,31.153,120.395,31.1,120.423,31.083,120.461,31.081,120.54,31.127,120.602,31.087]]]},{"n":"","z":4,"b":[-76.547,56.127,-75.935,56.55],"p":[[[-76.547,56.164,-76.525,56.184,-76.504,56.213,-76.417,56.306,-76.397,56.345,-76.42,56.365,-76.425,56.389,-76.391,56.499,-76.366,56.55,-76.341,56.531,-76.33,56.504,-76.305,56.468,-76.245,56.423,-76.234,56.397,-76.275,56.379,-76.205,56.322,-76.116,56.277,-76.025,56.237,-75.939,56.191,-75.935,56.174,-75.953,56.145,-76.147,56.127,-76.446,56.139,-76.547,56.164]]]},{"n":"Mar Chiquita","z":4,"b":[-62.979,-30.917,-62.23,-30.521],"p":[[[-62.23,-30.776,-62.408,-30.841,-62.571,-30.863,-62.727,-30.917,-62.81,-30.847,-62.86,-30.801,-62.931,-30.751,-62.979,-30.692,-62.931,-30.649,-62.827,-30.624,-62.722,-30.584,-62.688,-30.548,-62.644,-30.521,-62.426,-30.559,-62.413,-30.543,-62.374,-30.548,-62.288,-30.589,-62.233,-30.671,-62.23,-30.776]]]},{"n":"Kremenchuk","z":4,"b":[31.888,48.994,33.194,49.57],"p":[[[33.167,49.019,33.163,48.994,33.076,49.004,33.066,49.021,33.043,49.063,32.981,49.083,32.863,49.087,32.77,49.116,32.718,49.14,32.621,49.199,32.408,49.302,32.294,49.346,32.17,49.404,31.9,49.513,31.888,49.554,31.955,49.57,32.114,49.541,32.184,49.521,32.35,49.469,32.461,49.426,32.632,49.382,32.682,49.401,32.659,49.464,32.665,49.537,32.712,49.559,32.754,49.485,32.861,49.408,33.015,49.281,33.104,49.2,33.194,49.119,33.194,49.094,33.167,49.019]]]},{"n":"La Grande 3","z":4,"b":[-76.124,53.465,-74.433,53.897],"p":[[[-76.088,53.666,-76.1,53.737,-76.036,53.737,-75.93,53.766,-75.902,53.791,-75.912,53.813,-75.85,53.855,-75.552,53.892,-75.292,53.897,-75.268,53.878,-75.241,53.843,-75.184,53.82,-74.799,53.893,-74.734,53.896,-74.725,53.857,-74.82,53.838,-74.889,53.794,-75.049,53.753,-75.194,53.721,-75.27,53.709,-75.265,53.684,-75.162,53.677,-74.943,53.672,-74.771,53.654,-74.593,53.667,-74.456,53.701,-74.433,53.685,-74.461,53.663,-74.679,53.609,-74.807,53.605,-74.935,53.578,-75.027,53.576,-74.998,53.622,-75.034,53.639,-75.12,53.623,-75.238,53.619,-75.478,53.631,-75.633,53.638,-75.658,53.62,-75.614,53.602,-75.601,53.582,-75.632,53.538,-75.716,53.497,-75.934,53.465,-76.102,53.475,-76.124,53.523,-76.064,53.569,-76.024,53.591,-75.945,53.624,-75.907,53.653,-75.945,53.686,-76.018,53.672,-76.088,53.666]]]},{"n":"Robert-Bourassa","z":4,"b":[-77.685,53.493,-76.088,54.188],"p":[[[-76.1,53.737,-76.088,53.666,-76.36,53.63,-76.385,53.596,-76.515,53.557,-76.839,53.499,-76.912,53.501,-77.187,53.493,-77.223,53.515,-77.29,53.523,-77.397,53.521,-77.488,53.56,-77.493,53.607,-77.465,53.638,-77.511,53.651,-77.621,53.659,-77.685,53.679,-77.612,53.722,-77.42,53.798,-77.341,53.811,-77.288,53.791,-77.016,53.821,-76.902,53.847,-76.766,53.91,-76.731,53.977,-76.846,54.031,-76.888,54.057,-76.868,54.072,-76.84,54.088,-76.66,54.026,-76.618,54.033,-76.602,54.055,-76.561,54.056,-76.509,54.048,-76.471,54.059,-76.472,54.082,-76.533,54.115,-76.573,54.14,-76.557,54.157,-76.454,54.178,-76.368,54.188,-76.329,54.162,-76.264,54.136,-76.202,54.139,-76.191,54.153,-76.15,54.143,-76.105,54.116,-76.14,54.108,-76.183,54.103,-76.255,54.073,-76.304,53.963,-76.349,53.946,-76.376,53.927,-76.433,53.898,-76.487,53.851,-76.596,53.788,-76.703,53.75,-76.75,53.723,-76.696,53.699,-76.554,53.712,-76.143,53.737,-76.1,53.737]]]},{"n":"","z":4,"b":[-157.044,57.443,-156.548,57.785],"p":[[[-156.548,57.766,-156.604,57.696,-156.777,57.566,-156.783,57.533,-156.768,57.507,-156.796,57.464,-156.834,57.443,-156.927,57.473,-157.019,57.512,-157.044,57.548,-157.006,57.566,-156.954,57.569,-156.893,57.58,-156.842,57.611,-156.837,57.653,-156.716,57.725,-156.683,57.755,-156.59,57.785,-156.548,57.766]]]},{"n":"Enriquillo","z":4,"b":[-71.794,18.421,-71.509,18.551],"p":[[[-71.509,18.47,-71.537,18.434,-71.607,18.421,-71.794,18.494,-71.777,18.531,-71.683,18.551,-71.584,18.514,-71.509,18.47]]]},{"n":"","z":4,"b":[-160.134,66.43,-159.663,66.544],"p":[[[-159.665,66.474,-159.663,66.433,-159.801,66.43,-159.945,66.442,-159.992,66.469,-160.134,66.52,-160.115,66.544,-159.922,66.535,-159.729,66.512,-159.665,66.474]]]},{"n":"","z":4,"b":[-115.896,60.109,-115.062,60.35],"p":[[[-115.062,60.329,-115.148,60.214,-115.157,60.195,-115.219,60.183,-115.286,60.16,-115.295,60.131,-115.294,60.109,-115.477,60.138,-115.605,60.173,-115.744,60.163,-115.879,60.163,-115.896,60.201,-115.804,60.254,-115.65,60.254,-115.531,60.262,-115.349,60.295,-115.221,60.337,-115.11,60.35,-115.062,60.329]]]},{"n":"Sakami","z":4,"b":[-76.938,52.968,-76.497,53.485],"p":[[[-76.637,53.26,-76.648,53.242,-76.665,53.221,-76.692,53.212,-76.685,53.19,-76.688,53.17,-76.784,53.147,-76.843,53.095,-76.839,53.034,-76.799,52.981,-76.855,52.968,-76.899,52.995,-76.938,53.119,-76.904,53.165,-76.881,53.216,-76.89,53.244,-76.892,53.291,-76.873,53.324,-76.892,53.345,-76.844,53.394,-76.72,53.469,-76.703,53.444,-76.703,53.419,-76.661,53.417,-76.613,53.447,-76.564,53.485,-76.515,53.468,-76.497,53.414,-76.523,53.379,-76.637,53.361,-76.672,53.34,-76.676,53.314,-76.656,53.298,-76.608,53.306,-76.589,53.289,-76.637,53.26]]]},{"n":"Argentino","z":4,"b":[-73.3,-50.62,-71.982,-49.982],"p":[[[-71.982,-50.224,-71.999,-50.244,-72.113,-50.264,-72.274,-50.302,-72.441,-50.307,-72.595,-50.293,-72.672,-50.278,-72.794,-50.291,-72.973,-50.357,-73.004,-50.419,-72.962,-50.471,-72.858,-50.47,-72.795,-50.46,-72.782,-50.483,-72.85,-50.538,-72.921,-50.612,-72.955,-50.62,-73.01,-50.515,-73.109,-50.365,-73.168,-50.332,-73.223,-50.335,-73.274,-50.346,-73.285,-50.331,-73.214,-50.27,-73.028,-50.289,-72.972,-50.261,-72.983,-50.226,-73.044,-50.199,-73.163,-50.148,-73.232,-50.165,-73.291,-50.211,-73.3,-50.185,-73.278,-50.121,-73.286,-50.07,-73.251,-50.033,-73.205,-50.001,-73.188,-49.982,-73.137,-50.031,-73.062,-50.133,-72.947,-50.18,-72.8,-50.196,-72.731,-50.186,-72.44,-50.173,-72.116,-50.132,-72.039,-50.162,-71.982,-50.224]]]},{"n":"Namtso","z":4,"b":[90.289,30.524,91.014,30.924],"p":[[[90.346,30.524,90.314,30.573,90.292,30.698,90.289,30.847,90.353,30.88,90.476,30.855,90.579,30.829,90.651,30.84,90.676,30.871,90.712,30.895,90.866,30.885,90.944,30.924,90.975,30.923,91.012,30.888,91.014,30.839,90.933,30.793,90.843,30.718,90.714,30.641,90.596,30.596,90.346,30.524]]]},{"n":"Tuz","z":4,"b":[33.158,38.523,33.674,39.127],"p":[[[33.458,38.854,33.525,38.812,33.597,38.799,33.674,38.712,33.617,38.578,33.533,38.523,33.423,38.559,33.326,38.611,33.236,38.667,33.158,38.772,33.219,38.876,33.293,38.967,33.309,39.057,33.305,39.127,33.35,39.12,33.424,39.056,33.454,38.992,33.44,38.938,33.458,38.854]]]},{"n":"Scott","z":4,"b":[-106.407,59.805,-105.704,60.228],"p":[[[-105.959,59.998,-106.019,59.994,-106.073,59.959,-106.081,59.892,-106.055,59.845,-106.104,59.809,-106.167,59.808,-106.195,59.836,-106.206,59.86,-106.237,59.862,-106.259,59.827,-106.278,59.813,-106.338,59.805,-106.375,59.831,-106.326,59.935,-106.332,60.022,-106.398,60.043,-106.407,60.064,-106.374,60.087,-106.308,60.101,-106.276,60.087,-106.263,60.061,-106.228,60.035,-106.195,60.021,-106.18,60.035,-106.153,60.097,-106.122,60.144,-106.019,60.192,-105.873,60.228,-105.826,60.214,-105.801,60.203,-105.704,60.184,-105.77,60.131,-105.914,60.117,-106.028,60.111,-106.069,60.083,-106.046,60.05,-105.939,60.059,-105.88,60.038,-105.875,60.006,-105.959,59.998]]]},{"n":"","z":4,"b":[27.34,60.938,27.849,61.078],"p":[[[27.535,60.938,27.429,60.969,27.342,61.001,27.34,61.021,27.367,61.033,27.448,60.994,27.579,60.984,27.692,61.012,27.704,61.056,27.724,61.078,27.777,61.068,27.81,61.044,27.849,60.984,27.744,60.95,27.535,60.938]]]},{"n":"Rikkavesi","z":4,"b":[29.422,62.682,29.822,63.051],"p":[[[29.472,62.869,29.521,62.833,29.562,62.834,29.583,62.888,29.537,62.976,29.494,63.029,29.522,63.051,29.595,62.981,29.747,62.876,29.822,62.795,29.812,62.732,29.742,62.697,29.666,62.682,29.623,62.74,29.541,62.774,29.458,62.833,29.422,62.883,29.44,62.894,29.472,62.869]]]},{"n":"","z":4,"b":[29.188,62.641,29.522,62.805],"p":[[[29.229,62.705,29.219,62.772,29.293,62.805,29.345,62.745,29.39,62.721,29.465,62.734,29.522,62.706,29.454,62.641,29.339,62.658,29.219,62.65,29.188,62.684,29.229,62.705]]]},{"n":"Nerpichye","z":4,"b":[159.808,69.228,160.48,69.378],"p":[[[160.48,69.25,160.433,69.228,160.256,69.238,159.965,69.282,159.808,69.347,159.844,69.378,159.998,69.357,160.17,69.327,160.325,69.296,160.48,69.25]]]},{"n":"Yarroto Pervoe","z":4,"b":[71.298,67.87,71.786,68.037],"p":[[[71.359,67.87,71.298,67.889,71.303,67.906,71.37,67.947,71.552,68.004,71.708,68.037,71.786,67.993,71.776,67.95,71.65,67.895,71.359,67.87]]]},{"n":"Ozero Neyto","z":4,"b":[70.128,70.011,70.512,70.136],"p":[[[70.489,70.092,70.512,70.055,70.371,70.015,70.231,70.011,70.128,70.076,70.174,70.136,70.368,70.117,70.489,70.092]]]},{"n":"Ozero Yambuto","z":4,"b":[70.749,69.953,71.058,70.139],"p":[[[70.851,69.969,70.749,70.033,70.754,70.067,70.79,70.096,70.837,70.136,70.941,70.139,70.939,70.1,70.943,70.054,71.058,69.988,71.026,69.953,70.851,69.969]]]},{"n":"","z":4,"b":[70.344,70.123,70.821,70.217],"p":[[[70.563,70.123,70.372,70.153,70.344,70.2,70.77,70.217,70.821,70.191,70.793,70.166,70.696,70.136,70.563,70.123]]]},{"n":"Vygozero","z":4,"b":[34.214,62.919,35.413,63.851],"p":[[[35.013,63.506,35.145,63.482,35.249,63.492,35.374,63.492,35.413,63.448,35.382,63.434,35.193,63.404,35.172,63.381,35.099,63.383,35.049,63.399,34.977,63.429,34.872,63.464,34.829,63.434,34.885,63.294,34.953,63.181,34.957,63.119,34.946,63.033,34.91,62.951,34.863,62.919,34.861,62.941,34.867,62.976,34.864,63.01,34.873,63.04,34.899,63.055,34.891,63.107,34.858,63.172,34.853,63.225,34.835,63.276,34.804,63.326,34.702,63.399,34.552,63.459,34.517,63.484,34.525,63.502,34.453,63.623,34.358,63.697,34.308,63.692,34.266,63.656,34.215,63.613,34.214,63.676,34.284,63.777,34.347,63.851,34.428,63.848,34.563,63.816,34.644,63.789,34.616,63.766,34.857,63.721,34.861,63.688,34.99,63.585,34.984,63.564,34.953,63.54,35.013,63.506]]]},{"n":"Segozero","z":4,"b":[33.257,63.189,34.066,63.487],"p":[[[33.441,63.444,33.586,63.487,33.637,63.476,33.657,63.433,33.697,63.409,33.847,63.438,33.933,63.418,33.991,63.297,34.066,63.229,34.017,63.201,33.897,63.189,33.703,63.19,33.572,63.203,33.499,63.216,33.307,63.284,33.274,63.316,33.274,63.341,33.283,63.382,33.257,63.423,33.355,63.424,33.441,63.444]]]},{"n":"Koitere","z":4,"b":[30.481,62.767,30.847,63.05],"p":[[[30.604,62.876,30.587,62.937,30.615,62.966,30.676,62.973,30.704,63.012,30.755,63.05,30.831,63.027,30.847,62.954,30.742,62.864,30.689,62.847,30.659,62.808,30.667,62.784,30.659,62.767,30.634,62.775,30.502,62.779,30.481,62.788,30.539,62.82,30.604,62.876]]]},{"n":"Pielinen","z":4,"b":[29.042,62.722,30.33,63.573],"p":[[[29.994,63.293,30.004,63.271,29.966,63.265,29.959,63.227,29.994,63.14,30.138,62.973,30.33,62.829,30.317,62.809,30.229,62.804,30.143,62.722,30.09,62.729,30.155,62.79,30.216,62.868,30.137,62.929,30.065,62.944,30.033,62.959,29.854,63.087,29.814,63.145,29.594,63.158,29.528,63.179,29.393,63.183,29.299,63.256,29.238,63.323,29.123,63.407,29.042,63.475,29.071,63.508,29.077,63.564,29.092,63.573,29.187,63.514,29.309,63.461,29.372,63.44,29.527,63.377,29.722,63.315,29.784,63.294,29.831,63.291,29.915,63.315,29.994,63.293]]]},{"n":"Pyaozero","z":4,"b":[30.651,65.905,31.335,66.27],"p":[[[31.17,66.107,31.211,66.059,31.24,66.004,31.277,65.955,31.335,65.918,31.295,65.905,31.155,65.914,30.95,65.964,30.651,66.052,30.704,66.135,30.722,66.186,30.789,66.23,30.899,66.263,31.033,66.27,31.103,66.245,31.174,66.138,31.17,66.107]]]},{"n":"Topozero","z":4,"b":[31.507,65.388,32.712,65.89],"p":[[[32.517,65.532,32.664,65.477,32.688,65.452,32.712,65.416,32.706,65.388,32.668,65.389,32.635,65.418,32.481,65.469,32.274,65.506,32.109,65.546,31.996,65.569,31.946,65.599,31.943,65.622,31.895,65.636,31.845,65.644,31.733,65.67,31.574,65.735,31.507,65.795,31.693,65.856,31.983,65.89,32.051,65.876,32.118,65.834,32.139,65.784,32.204,65.764,32.329,65.7,32.36,65.667,32.363,65.629,32.377,65.577,32.425,65.542,32.517,65.532]]]},{"n":"","z":4,"b":[-72.841,81.67,-68.968,81.94],"p":[[[-72.841,81.68,-72.618,81.705,-72.347,81.728,-69.582,81.918,-69.285,81.933,-68.968,81.94,-69.476,81.887,-70.016,81.84,-72.22,81.698,-72.714,81.67,-72.736,81.677,-72.841,81.68]]]},{"n":"","z":4,"b":[-89.522,52.854,-88.952,53.006],"p":[[[-89.522,52.883,-89.471,52.897,-89.434,52.918,-89.422,52.948,-89.283,53.006,-89.199,52.988,-89.123,52.966,-89.004,52.985,-88.952,52.988,-88.983,52.94,-89.044,52.87,-89.151,52.854,-89.362,52.866,-89.478,52.868,-89.522,52.883]]]},{"n":"Bosten","z":4,"b":[86.728,41.825,87.375,42.108],"p":[[[87.375,41.851,87.259,41.894,87.102,41.901,86.894,41.87,86.753,41.825,86.728,41.85,86.735,41.903,86.795,41.975,86.829,42.073,86.889,42.108,87.008,42.084,87.315,42.017,87.375,41.908,87.375,41.851]]]},{"n":"","z":4,"b":[30.85,61.921,31.053,62.083],"p":[[[30.868,61.974,30.868,62.009,30.853,62.049,30.85,62.083,30.926,62.083,30.971,62.065,31.053,61.97,31.02,61.929,30.939,61.921,30.885,61.941,30.868,61.974]]]},{"n":"Ilmen","z":4,"b":[30.857,58.142,31.688,58.477],"p":[[[31.531,58.23,31.446,58.206,31.394,58.142,31.156,58.171,30.876,58.214,30.857,58.239,30.955,58.284,31.019,58.32,31.08,58.332,31.17,58.359,31.252,58.425,31.298,58.477,31.374,58.429,31.43,58.417,31.511,58.389,31.518,58.373,31.637,58.388,31.653,58.369,31.646,58.351,31.647,58.308,31.688,58.202,31.631,58.207,31.531,58.23]]]},{"n":"Poyang","z":4,"b":[115.965,28.804,116.709,29.751],"p":[[[116.706,29.136,116.709,29.102,116.672,29.098,116.635,29.102,116.618,29.115,116.581,29.128,116.521,29.143,116.496,29.122,116.477,29.043,116.503,28.953,116.53,28.894,116.537,28.845,116.512,28.804,116.468,28.805,116.441,28.86,116.418,28.88,116.409,28.905,116.348,28.906,116.329,28.961,116.097,29.106,116.064,29.149,116.047,29.19,115.982,29.279,115.965,29.311,116.002,29.394,116.026,29.453,116.091,29.555,116.117,29.653,116.1,29.688,116.082,29.703,116.113,29.726,116.18,29.751,116.198,29.751,116.23,29.698,116.235,29.666,116.207,29.645,116.199,29.604,116.181,29.573,116.159,29.519,116.145,29.454,116.11,29.427,116.115,29.402,116.143,29.386,116.188,29.382,116.196,29.362,116.153,29.334,116.137,29.309,116.153,29.253,116.31,29.232,116.343,29.234,116.368,29.223,116.358,29.186,116.362,29.154,116.407,29.176,116.44,29.236,116.469,29.25,116.54,29.24,116.604,29.237,116.64,29.237,116.664,29.226,116.661,29.19,116.674,29.159,116.706,29.136]]]},{"n":"Daguan Lake","z":4,"b":[116.257,29.964,116.52,30.094],"p":[[[116.461,30.055,116.496,30.02,116.52,29.988,116.498,29.964,116.388,29.966,116.29,29.994,116.259,30.048,116.257,30.094,116.281,30.079,116.298,30.086,116.327,30.087,116.386,30.069,116.442,30.077,116.461,30.055]]]},{"n":"Longgan","z":4,"b":[115.977,29.881,116.26,30.061],"p":[[[116.195,30.038,116.242,30.032,116.253,30.01,116.26,29.976,116.254,29.935,116.211,29.908,116.131,29.898,116.044,29.881,115.977,29.952,115.985,30.016,116.073,30,116.118,30,116.129,30.026,116.15,30.061,116.195,30.038]]]},{"n":"Liangzi","z":4,"b":[114.35,30.085,114.642,30.366],"p":[[[114.35,30.196,114.371,30.211,114.395,30.236,114.406,30.268,114.444,30.285,114.496,30.289,114.501,30.311,114.474,30.333,114.458,30.364,114.501,30.366,114.539,30.345,114.603,30.3,114.623,30.258,114.642,30.133,114.623,30.096,114.581,30.137,114.542,30.188,114.509,30.194,114.474,30.158,114.445,30.085,114.408,30.106,114.365,30.165,114.35,30.196]]]},{"n":"Siling","z":4,"b":[88.566,31.596,89.323,31.918],"p":[[[89.318,31.794,89.287,31.788,89.253,31.749,89.211,31.679,89.138,31.602,89.058,31.596,88.989,31.609,88.946,31.625,88.937,31.658,88.948,31.7,88.927,31.728,88.85,31.726,88.764,31.712,88.685,31.729,88.608,31.796,88.566,31.844,88.608,31.88,88.704,31.867,88.759,31.834,88.789,31.829,88.814,31.841,88.864,31.887,88.968,31.918,89.12,31.913,89.266,31.872,89.323,31.823,89.318,31.794]]]},{"n":"Khar-Us","z":4,"b":[91.976,47.769,92.802,48.361],"p":[[[92.787,48.178,92.622,48.098,92.442,48.03,92.354,47.964,92.28,47.851,92.2,47.769,92.084,47.816,91.991,47.933,91.976,48.01,92.105,48.054,92.249,48.13,92.25,48.263,92.287,48.352,92.363,48.361,92.425,48.36,92.483,48.339,92.402,48.237,92.405,48.156,92.48,48.138,92.544,48.138,92.643,48.193,92.645,48.271,92.638,48.33,92.733,48.31,92.802,48.253,92.787,48.178]]]},{"n":"Taupo","z":4,"b":[175.749,-38.948,176.083,-38.677],"p":[[[176.066,-38.812,176.003,-38.865,175.927,-38.902,175.851,-38.931,175.795,-38.948,175.755,-38.943,175.8,-38.845,175.782,-38.797,175.749,-38.773,175.763,-38.727,175.809,-38.677,175.944,-38.677,176.057,-38.721,176.083,-38.758,176.066,-38.812]]]},{"n":"Geneva","z":4,"b":[6.133,46.205,6.874,46.529],"p":[[[6.874,46.432,6.866,46.403,6.845,46.385,6.665,46.402,6.469,46.389,6.336,46.361,6.157,46.205,6.133,46.205,6.158,46.302,6.344,46.458,6.588,46.529,6.819,46.466,6.874,46.432]]]},{"n":"Neagh","z":4,"b":[-6.585,54.499,-6.272,54.723],"p":[[[-6.488,54.723,-6.459,54.715,-6.318,54.709,-6.272,54.66,-6.282,54.604,-6.349,54.525,-6.354,54.502,-6.427,54.499,-6.562,54.515,-6.585,54.56,-6.541,54.623,-6.511,54.698,-6.488,54.723]]]},{"n":"Vättern","z":4,"b":[14.121,57.796,14.968,58.854],"p":[[[14.933,58.784,14.941,58.756,14.929,58.7,14.9,58.635,14.891,58.592,14.963,58.539,14.946,58.511,14.827,58.462,14.686,58.395,14.622,58.299,14.582,58.202,14.543,58.138,14.499,58.085,14.339,57.933,14.245,57.809,14.159,57.796,14.121,57.925,14.213,58.15,14.391,58.406,14.507,58.561,14.632,58.68,14.726,58.731,14.817,58.791,14.932,58.854,14.968,58.849,14.962,58.835,14.933,58.784]]]},{"n":"Van","z":4,"b":[42.353,38.326,43.611,38.988],"p":[[[42.989,38.346,42.923,38.39,42.912,38.426,42.882,38.477,42.856,38.484,42.781,38.459,42.683,38.457,42.586,38.482,42.538,38.49,42.353,38.5,42.365,38.549,42.434,38.61,42.458,38.643,42.458,38.692,42.526,38.745,42.723,38.782,42.845,38.794,42.935,38.794,43.079,38.809,43.163,38.846,43.159,38.869,43.126,38.882,43.088,38.881,43.075,38.916,43.152,38.948,43.285,38.964,43.436,38.988,43.56,38.978,43.611,38.947,43.531,38.898,43.383,38.836,43.292,38.813,43.21,38.716,43.186,38.603,43.287,38.548,43.318,38.489,43.235,38.407,43.148,38.337,43.088,38.326,42.989,38.346]]]},{"n":"Yellowstone","z":4,"b":[-110.555,44.319,-110.199,44.567],"p":[[[-110.199,44.335,-110.235,44.322,-110.273,44.319,-110.292,44.336,-110.328,44.356,-110.341,44.383,-110.394,44.391,-110.39,44.425,-110.426,44.447,-110.489,44.437,-110.531,44.414,-110.555,44.447,-110.529,44.471,-110.49,44.473,-110.45,44.48,-110.418,44.503,-110.381,44.556,-110.327,44.567,-110.272,44.544,-110.264,44.447,-110.202,44.391,-110.199,44.335]]]},{"n":"Teshekpuk","z":4,"b":[-154.257,70.48,-152.94,70.737],"p":[[[-152.94,70.569,-153.275,70.518,-153.656,70.48,-153.719,70.494,-153.794,70.487,-153.926,70.512,-154.019,70.543,-154.101,70.598,-154.174,70.634,-154.237,70.639,-154.257,70.661,-154.161,70.678,-154.035,70.684,-153.975,70.679,-153.938,70.698,-153.812,70.725,-153.611,70.737,-153.014,70.603,-152.94,70.569]]]},{"n":"Flathead","z":4,"b":[-114.254,47.711,-114.005,48.091],"p":[[[-114.156,47.933,-114.18,48.022,-114.199,48.04,-114.193,48.069,-114.134,48.091,-114.046,48.015,-114.024,47.934,-114.005,47.785,-114.021,47.756,-114.056,47.711,-114.118,47.718,-114.15,47.714,-114.163,47.74,-114.126,47.784,-114.148,47.812,-114.193,47.826,-114.25,47.837,-114.254,47.858,-114.194,47.891,-114.156,47.933]]]},{"n":"Mead","z":4,"b":[-114.816,35.831,-113.633,36.519],"p":[[[-113.636,35.831,-113.67,35.843,-113.726,35.922,-113.795,36.01,-113.895,36.079,-113.952,36.13,-113.989,36.165,-114.039,36.163,-114.11,36.032,-114.208,36.006,-114.294,36.03,-114.321,36.076,-114.373,36.1,-114.406,36.091,-114.451,36.079,-114.504,36.106,-114.64,36.106,-114.714,36.024,-114.741,36.014,-114.769,36.02,-114.812,36.075,-114.816,36.121,-114.653,36.154,-114.512,36.194,-114.432,36.237,-114.413,36.348,-114.387,36.483,-114.364,36.515,-114.336,36.519,-114.335,36.444,-114.359,36.335,-114.37,36.209,-114.295,36.11,-114.226,36.05,-114.159,36.077,-114.097,36.152,-114.03,36.245,-113.996,36.244,-113.924,36.176,-113.858,36.116,-113.777,36.048,-113.699,35.958,-113.633,35.859,-113.636,35.831]]]},{"n":"Claire","z":4,"b":[-112.447,58.303,-111.717,58.825],"p":[[[-111.817,58.56,-111.809,58.517,-111.831,58.484,-111.838,58.46,-111.843,58.425,-111.831,58.394,-111.789,58.331,-111.809,58.303,-111.898,58.306,-112.08,58.347,-112.189,58.413,-112.237,58.46,-112.208,58.499,-112.211,58.55,-112.255,58.576,-112.315,58.563,-112.393,58.578,-112.447,58.618,-112.431,58.683,-112.337,58.778,-112.154,58.825,-112.041,58.819,-112.037,58.787,-112.113,58.778,-112.119,58.736,-111.985,58.713,-111.902,58.726,-111.844,58.721,-111.802,58.697,-111.766,58.685,-111.731,58.686,-111.717,58.659,-111.817,58.56]]]},{"n":"Smallwood","z":4,"b":[-66.621,53.421,-63.104,55.102],"p":[[[-63.176,54.129,-63.184,54.109,-63.203,54.074,-63.104,54.004,-63.137,53.973,-63.2,53.956,-63.247,53.966,-63.305,53.99,-63.417,54.006,-63.489,54.012,-63.555,54.006,-63.568,53.982,-63.529,53.955,-63.529,53.929,-63.652,53.925,-63.703,53.918,-63.77,53.902,-63.764,53.879,-63.827,53.858,-64.009,53.862,-64.124,53.896,-64.138,53.96,-64.176,54.012,-64.495,54.079,-64.525,54.11,-64.564,54.125,-64.652,54.114,-64.688,54.081,-64.642,54.031,-64.658,53.996,-64.694,53.961,-64.751,53.934,-64.836,53.931,-64.922,53.916,-65.014,53.888,-65.123,53.84,-65.177,53.818,-65.263,53.796,-65.298,53.76,-65.295,53.724,-65.191,53.674,-65.19,53.639,-65.148,53.564,-65.164,53.54,-65.128,53.5,-64.973,53.455,-65.02,53.421,-65.234,53.455,-65.529,53.49,-65.583,53.542,-65.48,53.549,-65.388,53.559,-65.335,53.587,-65.317,53.605,-65.326,53.636,-65.364,53.675,-65.381,53.715,-65.363,53.756,-65.328,53.821,-65.338,53.879,-65.407,53.904,-65.453,53.914,-65.507,53.95,-65.517,53.997,-65.583,54.061,-65.728,54.087,-65.826,54.135,-66.112,54.347,-66.254,54.426,-66.276,54.478,-66.364,54.541,-66.44,54.562,-66.429,54.508,-66.349,54.424,-66.377,54.375,-66.406,54.338,-66.387,54.263,-66.331,54.179,-66.324,54.154,-66.382,54.164,-66.491,54.273,-66.546,54.363,-66.577,54.424,-66.608,54.488,-66.616,54.521,-66.615,54.582,-66.621,54.623,-66.535,54.628,-66.508,54.654,-66.529,54.77,-66.509,54.802,-66.555,54.883,-66.6,54.922,-66.595,54.949,-66.567,54.958,-66.574,54.991,-66.598,55.029,-66.584,55.049,-66.553,55.054,-66.541,55.073,-66.556,55.102,-66.532,55.1,-66.449,55.052,-66.345,54.979,-66.349,54.952,-66.352,54.937,-66.351,54.914,-66.38,54.904,-66.42,54.901,-66.432,54.893,-66.428,54.881,-66.399,54.853,-66.412,54.838,-66.423,54.833,-66.429,54.813,-66.36,54.715,-66.354,54.687,-66.2,54.577,-66.191,54.525,-66.191,54.476,-66.023,54.346,-65.994,54.34,-65.938,54.271,-65.909,54.285,-65.872,54.281,-65.791,54.202,-65.713,54.163,-65.682,54.176,-65.503,54.055,-65.4,54.007,-65.345,54.043,-65.192,53.993,-65.103,54.012,-65.178,54.075,-65.13,54.083,-64.864,54.053,-64.816,54.076,-64.941,54.152,-65.011,54.224,-65.072,54.34,-65.165,54.435,-65.305,54.46,-65.302,54.475,-65.279,54.523,-65.271,54.608,-65.224,54.65,-65.157,54.656,-65.135,54.638,-65.218,54.557,-65.187,54.528,-65.14,54.511,-65.103,54.473,-65.059,54.396,-64.96,54.303,-64.98,54.289,-64.97,54.236,-64.92,54.171,-64.883,54.148,-64.879,54.16,-64.855,54.167,-64.763,54.144,-64.68,54.143,-64.617,54.175,-64.583,54.186,-64.51,54.189,-64.431,54.187,-64.333,54.166,-64.219,54.178,-64.234,54.211,-64.274,54.231,-64.284,54.256,-64.325,54.273,-64.407,54.256,-64.464,54.253,-64.48,54.273,-64.425,54.332,-64.4,54.511,-64.366,54.615,-64.378,54.679,-64.416,54.737,-64.397,54.743,-64.32,54.696,-64.296,54.659,-64.329,54.566,-64.32,54.517,-64.202,54.406,-64.138,54.35,-64.029,54.285,-63.947,54.243,-63.87,54.169,-63.748,54.096,-63.584,54.078,-63.465,54.098,-63.373,54.14,-63.319,54.143,-63.228,54.137,-63.176,54.129]]]},{"n":"Taymyr","z":4,"b":[99.507,73.838,105.333,75.16],"p":[[[101.023,73.86,100.949,73.894,101.026,73.936,101.104,73.992,101.062,74.095,100.968,74.196,100.9,74.256,100.768,74.292,100.607,74.296,100.5,74.308,100.412,74.337,100.218,74.347,100.079,74.343,99.957,74.331,99.71,74.34,99.585,74.36,99.507,74.4,99.607,74.42,99.83,74.407,99.972,74.409,100.171,74.44,100.232,74.482,100.351,74.529,100.495,74.577,100.501,74.631,100.434,74.722,100.318,74.8,100.097,74.861,100.132,74.896,100.266,74.936,100.419,74.948,100.463,75.007,100.285,75.079,100.042,75.103,100.027,75.125,100.033,75.148,100.109,75.16,100.192,75.15,100.301,75.127,100.435,75.087,100.537,75.073,100.612,75.036,100.616,74.982,100.544,74.932,100.534,74.873,100.573,74.823,100.707,74.757,100.841,74.72,101.059,74.681,101.806,74.641,102.484,74.641,103.253,74.676,103.493,74.71,103.762,74.763,104.032,74.795,104.234,74.814,104.209,74.859,104.028,74.885,103.878,74.928,103.912,74.985,103.991,75.009,104.047,75.021,104.117,75.055,104.129,75.088,104.146,75.112,104.264,75.121,104.467,75.093,104.628,75.012,104.864,74.915,105.307,74.855,105.333,74.814,105.232,74.785,104.973,74.772,104.508,74.74,103.999,74.651,103.92,74.633,103.843,74.629,103.742,74.633,103.569,74.593,103.204,74.568,102.833,74.54,102.464,74.516,102.041,74.502,101.986,74.413,102.073,74.322,102.19,74.24,102.161,74.233,102.059,74.231,101.979,74.236,101.848,74.258,101.691,74.29,101.426,74.356,101.313,74.404,101.331,74.439,101.366,74.479,101.213,74.491,101.049,74.484,100.856,74.463,100.882,74.425,100.981,74.412,101.103,74.387,101.1,74.33,101.156,74.246,101.277,74.143,101.439,74.043,101.529,73.953,101.553,73.878,101.533,73.841,101.435,73.838,101.377,73.844,101.125,73.849,101.023,73.86]]]},{"n":"Izabal","z":4.7,"b":[-89.388,15.382,-88.843,15.765],"p":[[[-88.878,15.712,-88.925,15.678,-88.97,15.636,-88.997,15.573,-89.022,15.489,-89.079,15.444,-89.182,15.417,-89.238,15.41,-89.278,15.395,-89.301,15.382,-89.326,15.412,-89.366,15.46,-89.388,15.488,-89.372,15.505,-89.317,15.522,-89.247,15.549,-89.183,15.575,-89.105,15.602,-88.971,15.688,-88.862,15.764,-88.843,15.765,-88.852,15.741,-88.878,15.712]]]},{"n":"Mogotoyevo","z":4.7,"b":[148.916,71.906,149.318,72.163],"p":[[[149.318,72.037,149.311,72.008,149.251,71.969,149.187,71.921,149.113,71.906,149.01,71.925,148.942,71.979,148.916,72.034,148.93,72.081,148.961,72.129,148.987,72.153,149.021,72.163,149.079,72.162,149.155,72.151,149.224,72.127,149.283,72.082,149.318,72.037]]]},{"n":"Ayakum","z":4.7,"b":[89.082,37.485,89.645,37.633],"p":[[[89.611,37.495,89.514,37.485,89.365,37.504,89.228,37.528,89.145,37.538,89.097,37.56,89.082,37.587,89.147,37.598,89.277,37.615,89.396,37.633,89.487,37.628,89.565,37.613,89.618,37.594,89.645,37.575,89.639,37.553,89.64,37.527,89.645,37.51,89.611,37.495]]]},{"n":"Storsjön","z":4.7,"b":[13.972,62.826,14.671,63.325],"p":[[[14.084,63.259,14.054,63.296,14.026,63.319,14.063,63.325,14.22,63.305,14.394,63.262,14.446,63.264,14.51,63.265,14.612,63.205,14.671,63.139,14.569,63.162,14.504,63.165,14.455,63.153,14.427,63.127,14.422,63.09,14.444,63.028,14.469,62.952,14.474,62.887,14.461,62.836,14.444,62.827,14.427,62.826,14.39,62.874,14.345,62.925,14.341,62.953,14.368,62.956,14.39,62.953,14.413,62.954,14.43,62.98,14.398,63.028,14.357,63.087,14.208,63.152,14.114,63.178,14.057,63.194,13.981,63.246,13.972,63.263,14.005,63.259,14.061,63.236,14.083,63.242,14.084,63.259]]]},{"n":"St. Joseph","z":4.7,"b":[-91.367,50.918,-90.427,51.215],"p":[[[-91.345,50.969,-91.302,51.035,-91.274,51.069,-91.214,51.066,-91.091,51.038,-90.976,51.011,-90.911,51.037,-90.844,51.073,-90.795,51.094,-90.745,51.12,-90.663,51.176,-90.604,51.215,-90.52,51.192,-90.472,51.17,-90.431,51.14,-90.427,51.119,-90.528,51.068,-90.747,51.014,-90.937,50.954,-91.12,50.946,-91.238,50.934,-91.318,50.918,-91.367,50.93,-91.345,50.969]]]},{"n":"Rukwa","z":4.7,"b":[31.794,-8.09,32.53,-7.585],"p":[[[32.514,-8.039,32.53,-8.07,32.521,-8.09,32.363,-8.033,32.171,-7.963,32.052,-7.971,31.96,-7.986,31.921,-7.959,31.873,-7.884,31.823,-7.783,31.794,-7.701,31.8,-7.653,31.834,-7.619,31.897,-7.585,32.026,-7.633,32.254,-7.826,32.448,-7.989,32.514,-8.039]]]},{"n":"Nam Ngum Dam","z":4.7,"b":[102.506,18.45,102.894,18.759],"p":[[[102.879,18.527,102.826,18.527,102.719,18.497,102.675,18.465,102.646,18.45,102.615,18.476,102.571,18.56,102.546,18.615,102.527,18.669,102.506,18.746,102.527,18.759,102.561,18.735,102.582,18.696,102.622,18.681,102.638,18.673,102.659,18.662,102.704,18.657,102.747,18.65,102.753,18.599,102.791,18.569,102.851,18.55,102.885,18.544,102.894,18.541,102.879,18.527]]]},{"n":"Gouin","z":4.7,"b":[-75.414,48.288,-74.1,48.802],"p":[[[-74.531,48.461,-74.545,48.439,-74.575,48.425,-74.596,48.418,-74.621,48.42,-74.638,48.414,-74.651,48.4,-74.677,48.364,-74.686,48.357,-74.692,48.382,-74.706,48.42,-74.722,48.434,-74.756,48.449,-74.816,48.512,-74.867,48.56,-74.911,48.575,-74.945,48.576,-74.964,48.556,-74.969,48.519,-74.96,48.499,-74.942,48.487,-74.945,48.454,-74.954,48.38,-74.96,48.364,-74.986,48.365,-75.023,48.359,-75.059,48.347,-75.082,48.341,-75.13,48.288,-75.14,48.289,-75.13,48.33,-75.115,48.414,-75.099,48.478,-75.073,48.495,-75.042,48.514,-75.008,48.564,-75.018,48.612,-75.07,48.596,-75.095,48.577,-75.129,48.555,-75.165,48.526,-75.182,48.479,-75.181,48.438,-75.174,48.414,-75.213,48.406,-75.269,48.408,-75.3,48.424,-75.336,48.437,-75.363,48.429,-75.387,48.444,-75.414,48.438,-75.389,48.483,-75.35,48.532,-75.302,48.58,-75.282,48.597,-75.25,48.598,-75.219,48.592,-75.181,48.602,-75.146,48.636,-75.109,48.675,-75.061,48.706,-75.042,48.712,-74.97,48.694,-74.921,48.71,-74.812,48.757,-74.762,48.773,-74.723,48.785,-74.63,48.801,-74.558,48.802,-74.543,48.778,-74.522,48.756,-74.544,48.704,-74.58,48.64,-74.555,48.608,-74.507,48.594,-74.485,48.58,-74.456,48.581,-74.412,48.589,-74.397,48.579,-74.406,48.547,-74.37,48.486,-74.314,48.449,-74.263,48.439,-74.223,48.426,-74.174,48.398,-74.118,48.388,-74.1,48.372,-74.151,48.36,-74.237,48.347,-74.312,48.377,-74.395,48.44,-74.463,48.471,-74.51,48.48,-74.531,48.461]]]},{"n":"Gaoyou","z":4.7,"b":[118.882,32.686,119.407,33.201],"p":[[[119.407,32.794,119.392,32.751,119.35,32.749,119.3,32.748,119.253,32.736,119.201,32.691,119.173,32.686,119.165,32.706,119.147,32.753,119.125,32.798,119.092,32.836,119.079,32.855,119.084,32.866,119.13,32.877,119.188,32.878,119.229,32.882,119.242,32.896,119.241,32.919,119.253,32.943,119.251,32.978,119.245,33.013,119.264,33.034,119.296,33.041,119.314,33.054,119.303,33.085,119.269,33.121,119.237,33.154,119.215,33.156,119.18,33.148,119.168,33.118,119.164,33.09,119.154,33.065,119.099,33.027,119.035,33.031,118.966,33.038,118.907,33.058,118.882,33.077,118.957,33.071,119.059,33.058,119.102,33.069,119.135,33.144,119.17,33.186,119.226,33.2,119.254,33.201,119.304,33.164,119.349,33.078,119.346,33.024,119.372,33,119.397,32.916,119.407,32.794]]]},{"n":"Pyasino","z":4.7,"b":[87.349,69.478,88.161,70.097],"p":[[[88.161,69.498,88.146,69.483,88.108,69.482,88.04,69.479,87.937,69.478,87.875,69.483,87.814,69.493,87.728,69.513,87.695,69.541,87.705,69.587,87.721,69.624,87.685,69.705,87.609,69.809,87.472,69.871,87.373,69.876,87.349,69.889,87.372,69.92,87.395,69.953,87.41,70.004,87.494,70.024,87.647,70.071,87.81,70.097,87.925,70.09,87.969,70.061,87.939,70.025,87.771,69.982,87.657,69.96,87.66,69.921,87.682,69.895,87.71,69.878,87.785,69.841,87.881,69.784,87.93,69.756,87.996,69.687,88.025,69.614,88.015,69.59,88.039,69.563,88.114,69.529,88.161,69.498]]]},{"n":"","z":4.7,"b":[26.276,61.558,27.045,62.037],"p":[[[27.037,61.662,26.998,61.664,26.938,61.67,26.863,61.696,26.795,61.703,26.743,61.694,26.77,61.666,26.837,61.648,26.879,61.626,26.884,61.594,26.886,61.581,26.89,61.569,26.87,61.564,26.834,61.573,26.811,61.571,26.796,61.558,26.778,61.576,26.758,61.612,26.665,61.681,26.554,61.758,26.468,61.791,26.406,61.809,26.35,61.834,26.29,61.854,26.276,61.866,26.305,61.9,26.351,61.935,26.412,61.976,26.442,61.962,26.428,61.945,26.391,61.905,26.376,61.87,26.418,61.863,26.464,61.875,26.491,61.89,26.524,61.9,26.54,61.915,26.527,61.928,26.531,61.939,26.575,61.937,26.603,61.917,26.631,61.886,26.668,61.868,26.684,61.872,26.686,61.881,26.692,61.888,26.713,61.886,26.744,61.871,26.774,61.861,26.796,61.871,26.81,61.887,26.809,61.902,26.764,61.913,26.698,61.931,26.644,61.968,26.591,62.022,26.588,62.037,26.64,62.011,26.705,61.993,26.755,61.975,26.819,61.948,26.873,61.902,26.881,61.871,26.877,61.855,26.894,61.797,26.9,61.753,26.927,61.73,26.983,61.712,27.028,61.704,27.043,61.688,27.045,61.671,27.037,61.662]]]},{"n":"Hjälmaren","z":4.7,"b":[15.344,59.149,16.406,59.317],"p":[[[16.404,59.309,16.406,59.305,16.356,59.288,16.31,59.275,16.13,59.24,15.921,59.194,15.892,59.176,15.881,59.162,15.816,59.149,15.701,59.151,15.624,59.167,15.62,59.192,15.59,59.225,15.545,59.246,15.52,59.255,15.481,59.261,15.344,59.27,15.354,59.283,15.5,59.302,15.613,59.317,15.728,59.298,15.822,59.295,15.857,59.304,15.99,59.286,16.187,59.288,16.271,59.3,16.334,59.303,16.404,59.309]]]},{"n":"","z":4.7,"b":[26.232,62.952,26.741,63.365],"p":[[[26.63,63.114,26.592,63.089,26.603,63.035,26.648,62.982,26.65,62.961,26.629,62.952,26.538,62.981,26.462,63.015,26.468,63.024,26.469,63.039,26.462,63.065,26.454,63.095,26.427,63.123,26.353,63.168,26.272,63.216,26.235,63.248,26.232,63.264,26.257,63.26,26.304,63.226,26.36,63.187,26.429,63.164,26.491,63.17,26.565,63.196,26.586,63.234,26.511,63.276,26.449,63.302,26.42,63.329,26.423,63.354,26.459,63.365,26.524,63.341,26.623,63.321,26.697,63.322,26.717,63.314,26.685,63.304,26.649,63.293,26.692,63.278,26.733,63.257,26.741,63.227,26.714,63.191,26.663,63.164,26.644,63.138,26.63,63.114]]]},{"n":"Bienville","z":4.7,"b":[-73.604,54.903,-72.225,55.281],"p":[[[-73.182,55.098,-73.154,55.113,-73.154,55.125,-73.067,55.147,-72.923,55.18,-72.832,55.191,-72.732,55.2,-72.647,55.213,-72.589,55.216,-72.517,55.221,-72.418,55.241,-72.305,55.272,-72.225,55.281,-72.25,55.262,-72.281,55.226,-72.286,55.193,-72.29,55.166,-72.358,55.126,-72.473,55.083,-72.629,55.049,-72.706,55.01,-72.789,54.998,-72.918,54.974,-73.045,54.946,-73.11,54.947,-73.149,54.964,-73.213,54.968,-73.313,54.947,-73.413,54.921,-73.555,54.903,-73.604,54.927,-73.511,54.942,-73.428,54.97,-73.409,54.987,-73.433,55.006,-73.485,55.022,-73.514,55.034,-73.511,55.048,-73.491,55.056,-73.434,55.051,-73.365,55.048,-73.272,55.07,-73.206,55.089,-73.182,55.098]]]},{"n":"Beloe","z":4.7,"b":[37.231,60.077,38.004,60.329],"p":[[[37.232,60.164,37.231,60.186,37.286,60.233,37.4,60.289,37.542,60.319,37.737,60.329,37.898,60.301,37.981,60.239,38.004,60.161,37.999,60.103,37.968,60.086,37.823,60.079,37.715,60.077,37.469,60.088,37.397,60.098,37.28,60.131,37.232,60.164]]]},{"n":"Brokopondo","z":4.7,"b":[-55.335,4.462,-54.796,4.999],"p":[[[-54.894,4.783,-54.925,4.747,-54.941,4.725,-54.938,4.699,-54.924,4.673,-54.892,4.665,-54.865,4.655,-54.864,4.626,-54.874,4.601,-54.9,4.594,-54.932,4.581,-54.952,4.596,-54.983,4.609,-55.013,4.599,-55.048,4.57,-55.064,4.533,-55.087,4.489,-55.123,4.462,-55.149,4.471,-55.19,4.484,-55.22,4.492,-55.23,4.514,-55.26,4.525,-55.299,4.524,-55.335,4.528,-55.329,4.574,-55.303,4.609,-55.279,4.621,-55.273,4.663,-55.284,4.699,-55.235,4.779,-55.173,4.879,-55.15,4.964,-55.127,4.999,-55.093,4.999,-55.02,4.971,-54.968,4.918,-54.938,4.904,-54.885,4.908,-54.84,4.912,-54.807,4.908,-54.796,4.881,-54.814,4.836,-54.838,4.789,-54.862,4.787,-54.894,4.783]]]},{"n":"Manyara","z":4.7,"b":[35.742,-3.757,35.882,-3.416],"p":[[[35.744,-3.636,35.762,-3.553,35.792,-3.48,35.822,-3.432,35.848,-3.416,35.869,-3.424,35.882,-3.452,35.869,-3.576,35.838,-3.701,35.806,-3.737,35.775,-3.757,35.753,-3.75,35.742,-3.707,35.744,-3.636]]]},{"n":"Abaya","z":4.7,"b":[37.696,6.014,38.143,6.589],"p":[[[38.055,6.578,38.102,6.576,38.138,6.577,38.143,6.56,38.129,6.519,38.116,6.473,38.107,6.449,38.087,6.423,38.058,6.38,38.014,6.307,37.944,6.202,37.875,6.108,37.843,6.073,37.802,6.049,37.739,6.014,37.701,6.015,37.696,6.044,37.709,6.088,37.745,6.136,37.79,6.172,37.826,6.189,37.865,6.22,37.887,6.263,37.875,6.36,37.863,6.455,37.869,6.491,37.936,6.548,38.017,6.589,38.055,6.578]]]},{"n":"Akkajaure","z":4.7,"b":[17.131,67.419,18.428,67.792],"p":[[[18.184,67.478,17.838,67.586,17.632,67.612,17.575,67.62,17.475,67.644,17.339,67.654,17.246,67.674,17.22,67.687,17.154,67.735,17.131,67.792,17.303,67.753,17.435,67.714,18.116,67.561,18.203,67.533,18.396,67.462,18.428,67.419,18.184,67.478]]]},{"n":"Balaton","z":4.7,"b":[17.269,46.721,18.164,47.055],"p":[[[17.586,46.766,17.476,46.724,17.277,46.721,17.269,46.742,17.271,46.76,17.309,46.763,17.383,46.779,17.479,46.8,17.665,46.852,17.824,46.911,17.881,46.943,17.955,46.987,18.039,47.037,18.078,47.055,18.13,47.039,18.164,47.011,18.143,46.963,17.881,46.866,17.672,46.791,17.586,46.766]]]},{"n":"Canandaigua","z":4.7,"b":[-77.364,42.689,-77.245,42.895],"p":[[[-77.262,42.815,-77.309,42.768,-77.354,42.689,-77.364,42.704,-77.361,42.734,-77.334,42.785,-77.291,42.836,-77.271,42.895,-77.245,42.876,-77.262,42.815]]]},{"n":"Owasco","z":4.7,"b":[-76.549,42.769,-76.468,42.922],"p":[[[-76.479,42.77,-76.527,42.836,-76.549,42.897,-76.536,42.922,-76.469,42.781,-76.468,42.769,-76.479,42.77]]]},{"n":"Skaneateles","z":4.7,"b":[-76.428,42.787,-76.272,42.96],"p":[[[-76.272,42.787,-76.362,42.85,-76.428,42.91,-76.425,42.96,-76.372,42.887,-76.293,42.833,-76.272,42.787]]]},{"n":"","z":4.7,"b":[-164.054,60.167,-163.468,60.439],"p":[[[-163.468,60.414,-163.529,60.393,-163.557,60.362,-163.577,60.342,-163.557,60.27,-163.629,60.214,-163.77,60.167,-163.775,60.178,-163.795,60.211,-163.784,60.259,-163.775,60.286,-163.793,60.298,-163.836,60.297,-163.937,60.285,-164.042,60.275,-164.054,60.295,-164.018,60.302,-163.901,60.333,-163.85,60.351,-163.881,60.369,-163.939,60.389,-163.893,60.408,-163.866,60.43,-163.852,60.433,-163.797,60.416,-163.734,60.431,-163.721,60.439,-163.66,60.433,-163.499,60.425,-163.468,60.414]]]},{"n":"Bear","z":4.7,"b":[-111.407,41.871,-111.282,42.152],"p":[[[-111.288,42.033,-111.308,41.949,-111.327,41.889,-111.349,41.871,-111.373,41.883,-111.394,41.937,-111.407,41.988,-111.403,42.062,-111.393,42.131,-111.344,42.152,-111.313,42.148,-111.285,42.109,-111.282,42.059,-111.288,42.033]]]},{"n":"Utah","z":4.7,"b":[-111.914,40.064,-111.68,40.377],"p":[[[-111.742,40.19,-111.827,40.158,-111.848,40.095,-111.867,40.07,-111.892,40.064,-111.894,40.081,-111.89,40.117,-111.899,40.154,-111.914,40.176,-111.852,40.252,-111.86,40.323,-111.881,40.377,-111.793,40.369,-111.763,40.337,-111.733,40.285,-111.701,40.238,-111.68,40.225,-111.69,40.206,-111.742,40.19]]]},{"n":"","z":4.7,"b":[-126.414,66.754,-126.129,67.021],"p":[[[-126.414,66.938,-126.399,66.983,-126.356,67.011,-126.29,67.021,-126.218,67.01,-126.158,66.979,-126.129,66.941,-126.149,66.909,-126.183,66.883,-126.191,66.855,-126.177,66.817,-126.189,66.796,-126.236,66.775,-126.275,66.754,-126.314,66.76,-126.369,66.784,-126.408,66.857,-126.414,66.938]]]},{"n":"Leech","z":4.7,"b":[-94.621,47.062,-94.245,47.285],"p":[[[-94.245,47.191,-94.26,47.15,-94.302,47.097,-94.346,47.075,-94.395,47.067,-94.433,47.077,-94.461,47.109,-94.519,47.143,-94.566,47.12,-94.57,47.075,-94.578,47.062,-94.586,47.079,-94.605,47.123,-94.621,47.166,-94.611,47.206,-94.589,47.202,-94.502,47.221,-94.437,47.232,-94.379,47.241,-94.296,47.285,-94.281,47.27,-94.297,47.221,-94.308,47.183,-94.278,47.186,-94.245,47.191]]]},{"n":"Mille Lacs","z":4.7,"b":[-90.688,48.785,-90.288,48.974],"p":[[[-90.292,48.935,-90.288,48.916,-90.314,48.878,-90.34,48.821,-90.375,48.786,-90.44,48.785,-90.458,48.798,-90.525,48.806,-90.601,48.811,-90.634,48.833,-90.632,48.867,-90.65,48.917,-90.684,48.92,-90.688,48.944,-90.666,48.967,-90.626,48.974,-90.591,48.96,-90.561,48.926,-90.546,48.895,-90.48,48.885,-90.4,48.886,-90.361,48.893,-90.324,48.916,-90.292,48.935]]]},{"n":"Baskatong","z":4.7,"b":[-76.033,46.715,-75.688,46.997],"p":[[[-75.694,46.897,-75.688,46.872,-75.698,46.825,-75.732,46.768,-75.751,46.718,-75.795,46.715,-75.864,46.755,-75.943,46.759,-76.017,46.747,-76.033,46.759,-76.01,46.778,-75.971,46.819,-75.958,46.859,-75.955,46.875,-75.89,46.889,-75.831,46.902,-75.779,46.964,-75.737,46.997,-75.727,46.974,-75.694,46.897]]]},{"n":"Ranco","z":4.7,"b":[-72.558,-40.302,-72.263,-40.128],"p":[[[-72.265,-40.26,-72.263,-40.292,-72.273,-40.302,-72.331,-40.299,-72.457,-40.293,-72.547,-40.272,-72.558,-40.235,-72.552,-40.201,-72.529,-40.172,-72.425,-40.135,-72.33,-40.128,-72.299,-40.15,-72.273,-40.166,-72.267,-40.192,-72.27,-40.221,-72.265,-40.26]]]},{"n":"Trout","z":4.7,"b":[-121.586,60.4,-121.091,60.771],"p":[[[-121.256,60.448,-121.355,60.408,-121.408,60.4,-121.429,60.414,-121.456,60.443,-121.497,60.459,-121.586,60.438,-121.566,60.464,-121.484,60.517,-121.389,60.56,-121.316,60.588,-121.301,60.623,-121.314,60.7,-121.336,60.771,-121.276,60.762,-121.176,60.722,-121.108,60.665,-121.091,60.592,-121.149,60.513,-121.256,60.448]]]},{"n":"Viedma","z":4.7,"b":[-73.03,-49.784,-72.101,-49.44],"p":[[[-72.306,-49.761,-72.49,-49.691,-72.621,-49.641,-72.71,-49.639,-72.753,-49.657,-72.825,-49.659,-72.943,-49.668,-73.029,-49.673,-73.03,-49.662,-73.002,-49.639,-72.973,-49.616,-72.969,-49.588,-72.991,-49.552,-73.018,-49.508,-72.985,-49.481,-72.93,-49.44,-72.878,-49.44,-72.814,-49.478,-72.52,-49.552,-72.261,-49.611,-72.211,-49.626,-72.132,-49.668,-72.101,-49.709,-72.101,-49.736,-72.114,-49.759,-72.136,-49.775,-72.184,-49.784,-72.306,-49.761]]]},{"n":"Tumba","z":4.7,"b":[17.811,-0.989,18.129,-0.652],"p":[[[18.036,-0.957,18.014,-0.931,17.982,-0.892,17.941,-0.852,17.917,-0.816,17.918,-0.771,17.901,-0.736,17.843,-0.713,17.817,-0.695,17.811,-0.672,17.833,-0.669,17.901,-0.665,17.983,-0.652,18.045,-0.657,18.089,-0.68,18.114,-0.708,18.121,-0.731,18.111,-0.782,18.107,-0.799,18.117,-0.86,18.126,-0.931,18.129,-0.981,18.102,-0.989,18.077,-0.971,18.057,-0.956,18.036,-0.957]]]},{"n":"","z":4.7,"b":[30.727,63.613,31.199,63.922],"p":[[[30.768,63.834,30.795,63.879,30.797,63.922,30.833,63.918,30.873,63.903,30.882,63.887,30.896,63.868,31.023,63.836,31.167,63.815,31.199,63.786,31.191,63.775,31.17,63.771,31.14,63.783,31.1,63.79,31.08,63.78,31.088,63.754,31.121,63.733,31.138,63.709,31.134,63.698,31.114,63.681,31.122,63.654,31.144,63.629,31.129,63.613,31.068,63.64,30.999,63.668,30.949,63.683,30.912,63.708,30.919,63.738,30.916,63.767,30.883,63.79,30.749,63.805,30.727,63.825,30.768,63.834]]]},{"n":"Po","z":4.7,"b":[116.314,30.078,116.54,30.245],"p":[[[116.424,30.108,116.401,30.118,116.387,30.143,116.373,30.161,116.351,30.175,116.317,30.203,116.314,30.23,116.335,30.234,116.357,30.245,116.371,30.244,116.39,30.224,116.443,30.207,116.472,30.204,116.514,30.208,116.539,30.207,116.54,30.18,116.527,30.149,116.525,30.116,116.526,30.089,116.506,30.078,116.494,30.085,116.468,30.104,116.424,30.108]]]},{"n":"Buir","z":4.7,"b":[117.529,47.666,117.869,47.961],"p":[[[117.581,47.686,117.558,47.71,117.529,47.739,117.531,47.776,117.57,47.835,117.638,47.901,117.709,47.948,117.764,47.961,117.801,47.95,117.841,47.91,117.869,47.865,117.856,47.825,117.806,47.772,117.729,47.718,117.655,47.68,117.606,47.666,117.586,47.671,117.581,47.686]]]},{"n":"Ang Laren","z":4.7,"b":[82.837,31.445,83.341,31.652],"p":[[[83.23,31.464,83.201,31.464,83.094,31.492,82.991,31.515,82.913,31.517,82.857,31.541,82.837,31.564,82.894,31.575,82.928,31.596,82.934,31.612,82.974,31.627,83.06,31.641,83.1,31.652,83.142,31.597,83.209,31.557,83.29,31.508,83.341,31.462,83.333,31.445,83.296,31.446,83.23,31.464]]]},{"n":"Tangra Yumco","z":4.7,"b":[86.396,30.795,86.777,31.367],"p":[[[86.777,31.166,86.764,31.136,86.704,31.123,86.628,31.084,86.607,31.034,86.565,30.943,86.523,30.858,86.494,30.811,86.457,30.795,86.419,30.807,86.397,30.84,86.396,30.871,86.412,30.894,86.445,30.952,86.516,31.058,86.607,31.221,86.654,31.367,86.742,31.342,86.765,31.256,86.777,31.166]]]},{"n":"Kulundinskoe","z":4.7,"b":[79.387,52.83,79.809,53.143],"p":[[[79.77,52.967,79.744,52.932,79.731,52.9,79.736,52.873,79.75,52.86,79.734,52.842,79.691,52.83,79.603,52.837,79.517,52.858,79.451,52.899,79.4,52.944,79.387,52.974,79.387,53.007,79.399,53.041,79.427,53.076,79.472,53.102,79.594,53.143,79.638,53.137,79.695,53.118,79.738,53.092,79.767,53.048,79.791,53.012,79.809,52.996,79.77,52.967]]]},{"n":"Siletiteniz","z":4.7,"b":[72.923,53.07,73.424,53.398],"p":[[[73.387,53.07,73.325,53.113,73.197,53.205,73.088,53.269,73.014,53.271,72.958,53.25,72.933,53.26,72.923,53.281,72.957,53.309,72.981,53.344,72.987,53.368,73.031,53.384,73.109,53.395,73.204,53.398,73.321,53.382,73.405,53.361,73.424,53.337,73.421,53.255,73.419,53.154,73.408,53.108,73.403,53.088,73.407,53.072,73.387,53.07]]]},{"n":"Ubinskoye","z":4.7,"b":[79.823,55.385,80.328,55.564],"p":[[[80.328,55.54,80.308,55.494,80.208,55.452,80.1,55.413,79.987,55.39,79.898,55.385,79.847,55.397,79.823,55.423,79.823,55.452,79.881,55.479,79.984,55.516,80.086,55.55,80.186,55.564,80.269,55.561,80.305,55.563,80.328,55.54]]]},{"n":"","z":4.7,"b":[38.647,61.195,38.917,61.467],"p":[[[38.651,61.344,38.675,61.39,38.708,61.421,38.797,61.452,38.894,61.467,38.917,61.445,38.884,61.381,38.858,61.323,38.875,61.289,38.895,61.255,38.886,61.229,38.837,61.207,38.761,61.195,38.697,61.199,38.663,61.217,38.647,61.273,38.651,61.344]]]},{"n":"","z":4.7,"b":[27.886,66.034,29.089,66.338],"p":[[[28.869,66.311,28.949,66.306,29.03,66.31,29.089,66.299,29.053,66.284,29.03,66.27,29.007,66.243,28.941,66.22,28.791,66.162,28.701,66.106,28.626,66.063,28.523,66.049,28.364,66.034,28.07,66.034,28.043,66.048,28.049,66.066,28.047,66.09,28.034,66.108,28.025,66.119,27.952,66.144,27.886,66.168,27.94,66.172,28.03,66.162,28.086,66.145,28.118,66.125,28.128,66.097,28.138,66.074,28.177,66.072,28.32,66.1,28.433,66.125,28.506,66.136,28.54,66.167,28.561,66.186,28.703,66.212,28.833,66.233,28.836,66.242,28.835,66.262,28.78,66.3,28.727,66.331,28.755,66.338,28.807,66.335,28.869,66.311]]]},{"n":"Umbozero","z":4.7,"b":[34.194,67.543,34.582,67.887],"p":[[[34.194,67.871,34.216,67.882,34.28,67.887,34.335,67.858,34.363,67.805,34.449,67.734,34.551,67.645,34.582,67.593,34.534,67.552,34.409,67.543,34.318,67.545,34.317,67.572,34.313,67.6,34.315,67.655,34.319,67.701,34.301,67.724,34.254,67.75,34.237,67.771,34.248,67.793,34.218,67.836,34.194,67.871]]]},{"n":"Kovdozero","z":4.7,"b":[31.554,66.425,32.397,66.912],"p":[[[32.315,66.502,32.3,66.486,32.304,66.481,32.325,66.466,32.366,66.458,32.381,66.454,32.397,66.445,32.39,66.438,32.355,66.425,32.224,66.453,32.12,66.471,31.991,66.471,31.978,66.49,31.983,66.499,32.107,66.519,32.135,66.538,32.119,66.564,32.028,66.634,31.963,66.7,31.901,66.741,31.736,66.75,31.577,66.758,31.561,66.769,31.554,66.783,31.576,66.799,31.609,66.813,31.621,66.822,31.708,66.823,31.764,66.838,31.765,66.858,31.701,66.873,31.68,66.885,31.746,66.912,31.861,66.886,31.948,66.841,32.087,66.845,32.218,66.835,32.239,66.816,32.203,66.779,32.18,66.761,32.196,66.745,32.249,66.731,32.31,66.706,32.329,66.69,32.339,66.674,32.311,66.652,32.255,66.624,32.22,66.601,32.242,66.566,32.306,66.525,32.315,66.502]]]},{"n":"Tekapo","z":4.7,"b":[170.5,-44.003,170.656,-43.534],"p":[[[170.656,-43.658,170.653,-43.681,170.618,-43.737,170.576,-43.851,170.575,-43.935,170.51,-44.003,170.508,-43.967,170.509,-43.896,170.512,-43.853,170.53,-43.816,170.5,-43.596,170.519,-43.534,170.531,-43.579,170.543,-43.675,170.559,-43.707,170.579,-43.718,170.6,-43.712,170.632,-43.678,170.656,-43.658]]]},{"n":"Bustakh","z":4.7,"b":[141.586,72.465,142.194,72.608],"p":[[[142.194,72.562,142.153,72.549,142.057,72.518,141.96,72.484,141.829,72.466,141.69,72.465,141.639,72.476,141.637,72.5,141.623,72.521,141.607,72.541,141.586,72.57,141.622,72.579,141.794,72.593,141.965,72.608,142.127,72.595,142.194,72.562]]]},{"n":"Yamdrok","z":4.7,"b":[90.386,28.786,91.003,29.211],"p":[[[90.931,28.923,90.89,28.934,90.867,28.927,90.856,28.928,90.835,28.933,90.808,28.938,90.768,28.914,90.752,28.875,90.739,28.866,90.715,28.883,90.67,28.864,90.631,28.838,90.59,28.797,90.566,28.787,90.535,28.786,90.508,28.804,90.479,28.835,90.454,28.86,90.447,28.878,90.456,28.887,90.474,28.885,90.492,28.87,90.51,28.859,90.53,28.861,90.561,28.874,90.594,28.903,90.621,28.939,90.647,28.975,90.672,29.011,90.664,29.047,90.659,29.089,90.654,29.126,90.63,29.151,90.59,29.167,90.534,29.165,90.477,29.145,90.424,29.111,90.388,29.093,90.386,29.107,90.424,29.138,90.503,29.188,90.562,29.211,90.633,29.192,90.692,29.161,90.729,29.13,90.726,29.109,90.719,29.094,90.721,29.072,90.73,29.052,90.768,29.012,90.788,28.991,90.829,28.989,90.844,29.013,90.876,29.024,90.919,29.021,90.963,29.003,90.992,28.978,90.988,28.951,91.003,28.904,90.993,28.892,90.956,28.912,90.931,28.923]]]},{"n":"Ozero Tenis","z":4.7,"b":[71.625,56.058,72.009,56.211],"p":[[[72.003,56.202,72.009,56.177,71.989,56.143,71.947,56.109,71.869,56.106,71.817,56.095,71.778,56.087,71.75,56.064,71.718,56.058,71.676,56.067,71.662,56.115,71.625,56.178,71.642,56.191,71.721,56.197,71.82,56.189,71.862,56.192,71.88,56.206,71.941,56.211,72.003,56.202]]]},{"n":"Ozero Chernoye","z":4.7,"b":[67.296,55.713,67.679,55.869],"p":[[[67.679,55.801,67.64,55.792,67.621,55.78,67.608,55.765,67.594,55.75,67.504,55.713,67.396,55.718,67.298,55.745,67.296,55.777,67.321,55.815,67.348,55.851,67.351,55.868,67.382,55.869,67.433,55.835,67.483,55.806,67.536,55.806,67.595,55.821,67.668,55.817,67.679,55.801]]]},{"n":"","z":4.7,"b":[32.704,65.762,33.087,66.044],"p":[[[32.91,65.975,32.931,65.957,32.974,65.94,33.055,65.93,33.087,65.909,33.081,65.879,33.076,65.852,33.072,65.833,32.946,65.794,32.8,65.762,32.743,65.769,32.736,65.787,32.734,65.805,32.704,65.83,32.778,65.858,32.842,65.876,32.849,65.894,32.838,65.913,32.795,65.929,32.745,65.948,32.751,65.975,32.796,66.007,32.811,66.033,32.836,66.044,32.898,66.043,32.891,66.023,32.863,65.998,32.871,65.983,32.91,65.975]]]},{"n":"Bolshoe Morskoe","z":4.7,"b":[158.546,69.99,158.934,70.138],"p":[[[158.934,70.092,158.913,70.084,158.883,70.073,158.845,70.036,158.819,70,158.792,69.99,158.752,69.994,158.642,70.029,158.573,70.066,158.546,70.099,158.558,70.124,158.61,70.138,158.709,70.138,158.854,70.122,158.925,70.119,158.934,70.092]]]},{"n":"Chukochye","z":4.7,"b":[160.025,69.352,160.445,69.539],"p":[[[160.025,69.456,160.066,69.468,160.099,69.485,160.068,69.514,160.055,69.538,160.104,69.539,160.22,69.519,160.325,69.489,160.367,69.462,160.413,69.441,160.445,69.43,160.424,69.42,160.3,69.417,160.206,69.411,160.249,69.386,160.278,69.359,160.248,69.352,160.152,69.369,160.056,69.394,160.033,69.41,160.047,69.428,160.044,69.446,160.025,69.456]]]},{"n":"Shijiu","z":4.7,"b":[118.723,31.393,118.964,31.554],"p":[[[118.78,31.409,118.768,31.419,118.747,31.459,118.743,31.492,118.724,31.522,118.723,31.542,118.758,31.545,118.867,31.546,118.941,31.554,118.964,31.503,118.96,31.465,118.915,31.435,118.85,31.393,118.815,31.402,118.78,31.409]]]},{"n":"Hong","z":4.7,"b":[113.23,29.749,113.454,29.953],"p":[[[113.451,29.94,113.454,29.914,113.426,29.876,113.391,29.826,113.369,29.785,113.34,29.76,113.312,29.749,113.275,29.78,113.237,29.848,113.23,29.893,113.249,29.904,113.302,29.888,113.331,29.894,113.362,29.949,113.406,29.953,113.451,29.94]]]},{"n":"Võrtsjärv","z":4.7,"b":[25.928,58.143,26.143,58.396],"p":[[[26.143,58.327,26.141,58.269,26.11,58.21,26.085,58.143,26.051,58.146,26.004,58.219,25.928,58.305,25.941,58.345,25.966,58.382,26.019,58.396,26.077,58.391,26.121,58.374,26.143,58.327]]]},{"n":"Chao","z":4.7,"b":[117.297,31.445,117.941,31.709],"p":[[[117.941,31.521,117.912,31.522,117.83,31.57,117.784,31.579,117.706,31.524,117.623,31.455,117.563,31.445,117.468,31.467,117.382,31.536,117.317,31.592,117.297,31.65,117.315,31.697,117.366,31.709,117.413,31.694,117.434,31.651,117.448,31.61,117.487,31.593,117.543,31.591,117.594,31.609,117.652,31.654,117.733,31.66,117.81,31.621,117.919,31.551,117.941,31.521]]]},{"n":"","z":4.7,"b":[12.899,63.427,13.358,63.726],"p":[[[12.899,63.652,12.93,63.686,13.012,63.712,13.117,63.726,13.209,63.724,13.273,63.711,13.3,63.695,13.276,63.686,13.16,63.69,13.11,63.671,13.085,63.65,13.097,63.608,13.119,63.554,13.213,63.486,13.358,63.451,13.335,63.427,13.243,63.452,13.179,63.471,13.113,63.497,13.01,63.544,12.936,63.6,12.899,63.652]]]},{"n":"Rossignol","z":4.7,"b":[-65.199,44.149,-64.964,44.272],"p":[[[-64.966,44.252,-64.964,44.238,-65.001,44.182,-65.025,44.164,-65.099,44.149,-65.176,44.155,-65.198,44.181,-65.199,44.208,-65.189,44.235,-65.167,44.257,-65.134,44.27,-65.104,44.272,-65.095,44.247,-65.08,44.228,-65.053,44.224,-65.01,44.23,-64.966,44.252]]]},{"n":"Foshay Lake","z":4.7,"b":[-66.19,45.856,-65.92,46.099],"p":[[[-66.106,45.871,-66.15,45.856,-66.19,45.881,-66.131,45.91,-66.077,45.953,-66.048,46.01,-66.027,46.05,-66.002,46.066,-65.97,46.082,-65.958,46.091,-65.942,46.099,-65.92,46.093,-65.92,46.064,-65.93,46.034,-65.95,46.006,-65.979,45.969,-65.989,45.942,-66.041,45.904,-66.106,45.871]]]},{"n":"Goose","z":4.7,"b":[-120.489,41.785,-120.323,42.107],"p":[[[-120.324,41.987,-120.35,41.923,-120.378,41.832,-120.396,41.792,-120.443,41.785,-120.466,41.803,-120.484,41.869,-120.489,41.988,-120.475,42.065,-120.449,42.088,-120.411,42.107,-120.375,42.09,-120.344,42.066,-120.323,42.04,-120.324,41.987]]]},{"n":"Pyramid","z":4.7,"b":[-119.696,39.868,-119.432,40.209],"p":[[[-119.432,39.903,-119.433,39.886,-119.446,39.868,-119.487,39.878,-119.547,39.927,-119.636,40.028,-119.696,40.123,-119.684,40.15,-119.641,40.191,-119.577,40.209,-119.534,40.181,-119.515,40.082,-119.483,39.974,-119.454,39.933,-119.432,39.903]]]},{"n":"Sevan","z":4.7,"b":[44.972,40.149,45.659,40.614],"p":[[[44.99,40.515,44.972,40.542,44.989,40.582,44.986,40.612,45.015,40.614,45.095,40.59,45.242,40.51,45.311,40.488,45.373,40.479,45.437,40.435,45.537,40.365,45.602,40.318,45.636,40.279,45.656,40.243,45.659,40.215,45.651,40.195,45.608,40.172,45.543,40.156,45.481,40.156,45.389,40.151,45.313,40.149,45.276,40.164,45.248,40.197,45.218,40.296,45.183,40.385,45.153,40.404,45.125,40.428,45.095,40.461,45.04,40.49,44.99,40.515]]]},{"n":"","z":4.7,"b":[114.674,30.211,114.846,30.345],"p":[[[114.839,30.287,114.812,30.283,114.783,30.278,114.753,30.283,114.739,30.279,114.743,30.263,114.76,30.238,114.781,30.221,114.778,30.215,114.762,30.211,114.735,30.212,114.704,30.212,114.679,30.228,114.679,30.25,114.68,30.277,114.674,30.3,114.713,30.33,114.764,30.345,114.784,30.344,114.806,30.345,114.834,30.327,114.846,30.302,114.839,30.287]]]},{"n":"","z":4.7,"b":[114.584,30.608,114.786,30.751],"p":[[[114.601,30.662,114.622,30.656,114.64,30.66,114.649,30.674,114.646,30.715,114.674,30.724,114.703,30.735,114.719,30.751,114.746,30.738,114.781,30.701,114.786,30.663,114.756,30.65,114.696,30.613,114.622,30.608,114.584,30.65,114.586,30.663,114.601,30.662]]]},{"n":"Nansi","z":4.7,"b":[117.128,34.498,117.326,34.725],"p":[[[117.326,34.587,117.295,34.549,117.25,34.502,117.21,34.498,117.174,34.543,117.141,34.612,117.128,34.663,117.139,34.725,117.179,34.719,117.266,34.665,117.318,34.621,117.326,34.587]]]},{"n":"","z":4.7,"b":[114.482,30.339,114.756,30.512],"p":[[[114.601,30.344,114.553,30.384,114.499,30.418,114.482,30.431,114.495,30.439,114.533,30.435,114.575,30.405,114.601,30.399,114.631,30.42,114.633,30.44,114.629,30.456,114.607,30.483,114.609,30.496,114.64,30.486,114.666,30.49,114.678,30.497,114.701,30.512,114.739,30.494,114.756,30.477,114.752,30.463,114.748,30.443,114.753,30.429,114.727,30.42,114.683,30.397,114.655,30.346,114.623,30.339,114.601,30.344]]]},{"n":"Futou Lake","z":4.7,"b":[114.143,29.901,114.301,30.141],"p":[[[114.301,30.025,114.289,30.002,114.252,29.972,114.244,29.938,114.264,29.915,114.255,29.906,114.218,29.901,114.171,29.921,114.143,29.988,114.147,30.083,114.177,30.114,114.218,30.141,114.249,30.141,114.246,30.115,114.237,30.084,114.253,30.045,114.273,30.044,114.294,30.043,114.301,30.025]]]},{"n":"","z":4.7,"b":[113.941,29.862,114.158,30.027],"p":[[[114.158,29.872,114.137,29.862,114.114,29.883,114.087,29.934,114.063,29.927,114.029,29.898,113.99,29.879,113.969,29.88,113.963,29.892,113.984,29.916,114.003,29.942,113.996,29.959,113.97,29.963,113.944,29.966,113.941,29.981,114.019,30.001,114.074,30.027,114.101,30.022,114.13,29.935,114.152,29.89,114.158,29.872]]]},{"n":"Millerton","z":4.7,"b":[-119.693,37,-119.598,37.057],"p":[[[-119.603,37.057,-119.598,37.051,-119.603,37.031,-119.641,37.008,-119.693,37,-119.672,37.028,-119.623,37.046,-119.603,37.057]]]},{"n":"Bluenose","z":4.7,"b":[-119.875,68.246,-119.47,68.755],"p":[[[-119.875,68.558,-119.848,68.583,-119.806,68.619,-119.809,68.642,-119.825,68.661,-119.786,68.684,-119.744,68.719,-119.72,68.746,-119.691,68.755,-119.665,68.754,-119.663,68.744,-119.625,68.715,-119.613,68.698,-119.572,68.672,-119.526,68.658,-119.486,68.633,-119.47,68.608,-119.487,68.595,-119.552,68.594,-119.595,68.574,-119.621,68.532,-119.65,68.471,-119.642,68.44,-119.59,68.396,-119.511,68.361,-119.525,68.332,-119.646,68.289,-119.734,68.255,-119.817,68.246,-119.848,68.254,-119.844,68.321,-119.833,68.421,-119.835,68.484,-119.859,68.529,-119.875,68.558]]]},{"n":"Pipmuacan","z":4.7,"b":[-70.963,49.333,-69.783,49.887],"p":[[[-70.08,49.44,-70.137,49.491,-70.14,49.53,-70.137,49.565,-70.157,49.592,-70.195,49.617,-70.242,49.635,-70.283,49.641,-70.324,49.624,-70.351,49.59,-70.335,49.54,-70.329,49.523,-70.354,49.504,-70.406,49.519,-70.431,49.532,-70.45,49.537,-70.467,49.532,-70.487,49.499,-70.505,49.461,-70.508,49.427,-70.516,49.411,-70.539,49.439,-70.571,49.473,-70.639,49.466,-70.677,49.448,-70.701,49.427,-70.736,49.442,-70.756,49.488,-70.789,49.536,-70.816,49.543,-70.834,49.542,-70.855,49.525,-70.892,49.457,-70.94,49.408,-70.963,49.406,-70.921,49.497,-70.859,49.599,-70.834,49.625,-70.826,49.616,-70.792,49.596,-70.744,49.56,-70.716,49.525,-70.667,49.531,-70.585,49.572,-70.514,49.63,-70.471,49.672,-70.442,49.684,-70.411,49.682,-70.34,49.67,-70.305,49.701,-70.307,49.748,-70.358,49.832,-70.377,49.887,-70.339,49.884,-70.317,49.864,-70.289,49.813,-70.259,49.75,-70.236,49.699,-70.206,49.682,-70.171,49.684,-70.135,49.708,-70.1,49.69,-70.073,49.643,-70.017,49.597,-69.951,49.555,-69.899,49.545,-69.844,49.472,-69.787,49.37,-69.783,49.339,-69.825,49.333,-69.856,49.342,-69.866,49.366,-69.889,49.407,-69.904,49.451,-69.902,49.481,-69.917,49.504,-69.946,49.518,-69.982,49.517,-70.029,49.498,-70.064,49.475,-70.069,49.459,-70.08,49.44]]]},{"n":"Grand Lake","z":4.7,"b":[-58.013,48.643,-56.928,49.336],"p":[[[-57.035,49.265,-57.011,49.297,-56.99,49.332,-56.963,49.336,-56.944,49.304,-56.928,49.264,-56.97,49.227,-57.025,49.213,-57.053,49.215,-57.13,49.166,-57.24,49.056,-57.435,48.907,-57.513,48.858,-57.578,48.786,-57.76,48.672,-57.859,48.643,-57.947,48.658,-58.013,48.663,-57.965,48.685,-57.78,48.774,-57.665,48.859,-57.562,48.894,-57.371,49.011,-57.27,49.099,-57.262,49.147,-57.262,49.181,-57.221,49.189,-57.115,49.237,-57.035,49.265]]]},{"n":"","z":4.7,"b":[-126.762,67.256,-126.191,67.578],"p":[[[-126.357,67.271,-126.419,67.256,-126.495,67.263,-126.514,67.32,-126.536,67.398,-126.548,67.442,-126.584,67.471,-126.6,67.495,-126.613,67.522,-126.691,67.535,-126.762,67.56,-126.73,67.573,-126.614,67.578,-126.494,67.552,-126.398,67.488,-126.298,67.444,-126.231,67.434,-126.205,67.419,-126.198,67.403,-126.208,67.382,-126.208,67.358,-126.191,67.33,-126.206,67.304,-126.357,67.271]]]},{"n":"Colville","z":4.7,"b":[-126.241,67.059,-125.73,67.313],"p":[[[-125.751,67.313,-125.73,67.297,-125.756,67.238,-125.793,67.177,-125.828,67.124,-125.918,67.079,-126.061,67.059,-126.166,67.065,-126.222,67.085,-126.224,67.104,-126.196,67.115,-126.184,67.138,-126.191,67.169,-126.216,67.182,-126.241,67.199,-126.235,67.215,-126.033,67.254,-125.793,67.305,-125.751,67.313]]]},{"n":"Hottah","z":4.7,"b":[-118.832,64.781,-118.146,65.291],"p":[[[-118.666,64.999,-118.682,65.03,-118.681,65.061,-118.677,65.081,-118.698,65.109,-118.767,65.158,-118.832,65.192,-118.825,65.217,-118.733,65.245,-118.659,65.262,-118.552,65.279,-118.445,65.291,-118.423,65.281,-118.395,65.261,-118.365,65.245,-118.303,65.227,-118.265,65.199,-118.246,65.136,-118.279,65.09,-118.282,65.069,-118.275,65.043,-118.248,65.014,-118.237,64.986,-118.248,64.956,-118.276,64.929,-118.325,64.903,-118.319,64.873,-118.239,64.845,-118.156,64.852,-118.149,64.843,-118.146,64.795,-118.176,64.786,-118.333,64.781,-118.488,64.841,-118.578,64.884,-118.636,64.928,-118.664,64.97,-118.666,64.999]]]},{"n":"Big Trout","z":4.7,"b":[-90.357,53.679,-89.609,53.886],"p":[[[-90.357,53.766,-90.344,53.789,-90.321,53.835,-90.29,53.868,-90.252,53.87,-90.227,53.856,-90.201,53.841,-90.171,53.838,-90.145,53.854,-90.101,53.871,-90.046,53.881,-90.002,53.886,-89.928,53.863,-89.862,53.834,-89.847,53.815,-89.729,53.791,-89.609,53.772,-89.678,53.733,-89.814,53.69,-89.939,53.679,-90.034,53.685,-90.103,53.717,-90.167,53.747,-90.275,53.757,-90.357,53.766]]]},{"n":"Trout","z":4.7,"b":[-93.481,51.033,-93.072,51.302],"p":[[[-93.165,51.071,-93.182,51.055,-93.2,51.039,-93.235,51.033,-93.32,51.075,-93.429,51.154,-93.481,51.208,-93.48,51.237,-93.455,51.264,-93.406,51.292,-93.37,51.302,-93.344,51.286,-93.283,51.279,-93.215,51.299,-93.175,51.295,-93.141,51.273,-93.123,51.241,-93.101,51.207,-93.072,51.173,-93.074,51.159,-93.137,51.123,-93.165,51.071]]]},{"n":"Island","z":4.7,"b":[-94.949,53.71,-94.002,53.989],"p":[[[-94.236,53.734,-94.351,53.73,-94.446,53.72,-94.518,53.71,-94.553,53.713,-94.682,53.733,-94.855,53.786,-94.933,53.827,-94.949,53.859,-94.938,53.902,-94.933,53.933,-94.889,53.946,-94.851,53.956,-94.822,53.975,-94.754,53.989,-94.634,53.986,-94.601,53.972,-94.621,53.945,-94.686,53.915,-94.689,53.897,-94.654,53.876,-94.59,53.858,-94.477,53.864,-94.346,53.874,-94.229,53.883,-94.131,53.892,-94.048,53.882,-94.002,53.847,-94.014,53.798,-94.103,53.759,-94.18,53.742,-94.236,53.734]]]},{"n":"Naococane","z":4.7,"b":[-70.982,52.61,-70.397,53.06],"p":[[[-70.509,52.996,-70.486,52.983,-70.437,52.976,-70.397,52.974,-70.442,52.941,-70.476,52.901,-70.484,52.872,-70.464,52.863,-70.467,52.841,-70.474,52.813,-70.468,52.746,-70.444,52.677,-70.422,52.639,-70.433,52.61,-70.487,52.63,-70.522,52.682,-70.519,52.725,-70.522,52.761,-70.521,52.785,-70.538,52.8,-70.577,52.803,-70.592,52.789,-70.603,52.759,-70.64,52.736,-70.656,52.751,-70.703,52.775,-70.762,52.787,-70.78,52.809,-70.777,52.863,-70.783,52.899,-70.817,52.909,-70.847,52.878,-70.899,52.848,-70.94,52.817,-70.965,52.819,-70.981,52.841,-70.982,52.872,-70.952,52.907,-70.924,52.938,-70.899,52.94,-70.838,52.943,-70.768,52.956,-70.75,52.963,-70.741,52.981,-70.748,52.996,-70.771,53.008,-70.828,53.015,-70.831,53.028,-70.833,53.043,-70.82,53.06,-70.75,53.038,-70.691,52.998,-70.652,52.99,-70.599,52.988,-70.548,53.007,-70.527,53.007,-70.509,52.996]]]},{"n":"Ronge","z":4.7,"b":[-105.258,54.925,-104.369,55.307],"p":[[[-104.396,55.16,-104.455,55.138,-104.507,55.133,-104.591,55.154,-104.637,55.148,-104.69,55.126,-104.735,55.09,-104.79,55.034,-104.822,54.994,-104.901,54.977,-105.026,54.947,-105.128,54.925,-105.194,54.936,-105.237,54.957,-105.245,55.002,-105.247,55.048,-105.258,55.074,-105.233,55.117,-105.218,55.156,-105.224,55.179,-105.229,55.2,-105.237,55.219,-105.205,55.248,-105.138,55.283,-105.081,55.302,-105.06,55.298,-104.955,55.28,-104.833,55.306,-104.808,55.301,-104.798,55.292,-104.755,55.302,-104.714,55.296,-104.674,55.291,-104.621,55.307,-104.59,55.302,-104.574,55.281,-104.519,55.257,-104.418,55.254,-104.4,55.247,-104.369,55.182,-104.396,55.16]]]},{"n":"Mille Lacs","z":4.7,"b":[-93.795,46.136,-93.514,46.36],"p":[[[-93.534,46.249,-93.521,46.21,-93.514,46.177,-93.54,46.15,-93.6,46.136,-93.668,46.137,-93.726,46.158,-93.775,46.2,-93.795,46.255,-93.758,46.308,-93.679,46.345,-93.607,46.36,-93.569,46.357,-93.552,46.341,-93.542,46.3,-93.538,46.269,-93.534,46.249]]]},{"n":"Winnipesaukee","z":4.7,"b":[-71.443,43.532,-71.214,43.725],"p":[[[-71.293,43.635,-71.262,43.609,-71.216,43.587,-71.214,43.569,-71.232,43.54,-71.277,43.532,-71.371,43.598,-71.443,43.628,-71.436,43.656,-71.43,43.679,-71.409,43.695,-71.394,43.711,-71.399,43.725,-71.374,43.722,-71.331,43.7,-71.306,43.667,-71.293,43.635]]]},{"n":"Simcoe","z":4.7,"b":[-79.584,44.227,-79.163,44.698],"p":[[[-79.448,44.31,-79.479,44.27,-79.48,44.236,-79.499,44.227,-79.519,44.274,-79.537,44.353,-79.574,44.393,-79.584,44.405,-79.567,44.431,-79.503,44.463,-79.445,44.51,-79.413,44.562,-79.393,44.603,-79.387,44.641,-79.373,44.698,-79.354,44.69,-79.341,44.636,-79.265,44.563,-79.211,44.535,-79.177,44.491,-79.163,44.45,-79.172,44.419,-79.202,44.384,-79.342,44.343,-79.448,44.31]]]},{"n":"Gods","z":4.7,"b":[-94.768,54.412,-93.645,54.85],"p":[[[-93.923,54.835,-93.777,54.808,-93.69,54.792,-93.645,54.788,-93.647,54.779,-93.702,54.758,-93.835,54.758,-93.912,54.744,-93.934,54.709,-93.989,54.671,-94.157,54.636,-94.291,54.618,-94.34,54.607,-94.414,54.586,-94.45,54.542,-94.465,54.521,-94.495,54.502,-94.551,54.48,-94.675,54.44,-94.763,54.412,-94.767,54.427,-94.768,54.454,-94.758,54.476,-94.735,54.5,-94.65,54.541,-94.558,54.623,-94.508,54.669,-94.489,54.724,-94.476,54.786,-94.477,54.801,-94.437,54.805,-94.369,54.819,-94.272,54.842,-94.11,54.85,-94.012,54.847,-93.923,54.835]]]},{"n":"Hoh Xil Hu","z":4.7,"b":[90.95,35.528,91.316,35.653],"p":[[[91.316,35.595,91.303,35.574,91.286,35.544,91.268,35.528,91.165,35.547,91.043,35.565,90.979,35.609,90.95,35.634,90.953,35.645,90.993,35.653,91.033,35.65,91.115,35.637,91.199,35.634,91.222,35.646,91.271,35.629,91.316,35.595]]]},{"n":"Hammar","z":4.7,"b":[46.712,30.663,47.608,30.917],"p":[[[47.608,30.689,47.573,30.664,47.491,30.663,47.407,30.678,47.245,30.711,47.104,30.74,47.04,30.742,46.953,30.733,46.879,30.727,46.852,30.722,46.822,30.725,46.807,30.738,46.804,30.764,46.802,30.786,46.792,30.803,46.765,30.817,46.75,30.823,46.719,30.837,46.712,30.867,46.748,30.903,46.801,30.913,46.818,30.915,46.861,30.917,46.897,30.9,46.926,30.892,46.983,30.857,47.063,30.828,47.103,30.826,47.146,30.814,47.194,30.8,47.29,30.772,47.357,30.782,47.429,30.812,47.472,30.814,47.5,30.809,47.504,30.797,47.53,30.768,47.59,30.713,47.608,30.689]]]},{"n":"Malheur","z":4.7,"b":[-118.948,43.24,-118.612,43.425],"p":[[[-118.68,43.29,-118.721,43.291,-118.766,43.258,-118.821,43.24,-118.864,43.263,-118.903,43.303,-118.935,43.354,-118.948,43.394,-118.929,43.414,-118.791,43.425,-118.664,43.405,-118.633,43.344,-118.612,43.298,-118.623,43.282,-118.643,43.281,-118.654,43.284,-118.68,43.29]]]},{"n":"","z":4.7,"b":[-56.712,48.182,-56.466,48.363],"p":[[[-56.483,48.3,-56.586,48.232,-56.712,48.182,-56.689,48.219,-56.639,48.266,-56.604,48.3,-56.587,48.321,-56.56,48.352,-56.524,48.363,-56.494,48.346,-56.466,48.329,-56.483,48.3]]]},{"n":"","z":4.7,"b":[-95.918,55.419,-95.6,55.615],"p":[[[-95.6,55.615,-95.635,55.576,-95.683,55.52,-95.714,55.477,-95.769,55.442,-95.861,55.421,-95.918,55.419,-95.913,55.436,-95.906,55.461,-95.896,55.487,-95.817,55.526,-95.795,55.569,-95.699,55.596,-95.6,55.615]]]},{"n":"","z":4.7,"b":[-165.898,65.08,-165.376,65.205],"p":[[[-165.382,65.205,-165.376,65.193,-165.403,65.171,-165.482,65.134,-165.597,65.097,-165.708,65.08,-165.792,65.091,-165.888,65.138,-165.898,65.165,-165.852,65.182,-165.82,65.191,-165.655,65.188,-165.455,65.196,-165.382,65.205]]]},{"n":"","z":4.7,"b":[-163.936,61.055,-163.547,61.231],"p":[[[-163.798,61.144,-163.783,61.129,-163.781,61.106,-163.801,61.076,-163.816,61.055,-163.842,61.055,-163.858,61.077,-163.897,61.115,-163.936,61.145,-163.931,61.169,-163.889,61.196,-163.826,61.217,-163.76,61.219,-163.633,61.226,-163.547,61.231,-163.591,61.211,-163.628,61.198,-163.709,61.174,-163.798,61.144]]]},{"n":"","z":4.7,"b":[-162.768,60.76,-162.502,60.843],"p":[[[-162.508,60.795,-162.502,60.772,-162.527,60.763,-162.573,60.76,-162.666,60.777,-162.75,60.801,-162.768,60.818,-162.755,60.832,-162.714,60.842,-162.655,60.843,-162.573,60.826,-162.508,60.795]]]},{"n":"Tikchik","z":4.7,"b":[-159.058,59.164,-158.863,59.337],"p":[[[-158.863,59.201,-158.875,59.166,-158.913,59.164,-158.941,59.18,-158.951,59.208,-158.959,59.246,-158.998,59.292,-159.058,59.337,-158.945,59.314,-158.878,59.252,-158.865,59.221,-158.863,59.201]]]},{"n":"Tikchik","z":4.7,"b":[-159.132,59.896,-158.223,60.016],"p":[[[-158.223,59.943,-158.257,59.935,-158.381,59.949,-158.535,59.961,-158.705,59.934,-158.826,59.911,-158.971,59.896,-159.024,59.896,-159.092,59.9,-159.108,59.913,-159.12,59.934,-159.132,59.951,-159.1,59.952,-159,59.948,-158.759,59.98,-158.542,60.016,-158.486,60.012,-158.423,59.983,-158.307,59.98,-158.242,59.963,-158.223,59.943]]]},{"n":"Tikchik","z":4.7,"b":[-159.159,60.19,-158.774,60.283],"p":[[[-159.159,60.196,-159.132,60.22,-159.058,60.242,-158.919,60.273,-158.853,60.283,-158.874,60.264,-158.831,60.246,-158.778,60.23,-158.774,60.216,-158.774,60.201,-158.781,60.19,-158.913,60.201,-159.083,60.212,-159.149,60.196,-159.159,60.196]]]},{"n":"Tikchik","z":4.7,"b":[-159.133,59.425,-158.582,59.653],"p":[[[-158.708,59.564,-158.825,59.543,-158.835,59.518,-158.804,59.491,-158.663,59.465,-158.605,59.443,-158.582,59.425,-158.633,59.427,-158.757,59.438,-158.805,59.445,-158.869,59.481,-158.992,59.516,-159.046,59.547,-159.083,59.594,-159.133,59.651,-159.094,59.653,-158.945,59.586,-158.892,59.59,-158.794,59.603,-158.647,59.596,-158.708,59.564]]]},{"n":"Khyargas Nuur","z":4.7,"b":[92.832,48.991,93.799,49.322],"p":[[[93.799,49.129,93.779,49.103,93.678,49.07,93.536,49.034,93.45,49.009,93.397,48.991,93.322,49.037,93.241,49.097,93.179,49.112,93.104,49.139,92.975,49.187,92.865,49.225,92.832,49.253,92.835,49.282,92.864,49.306,92.94,49.315,93.059,49.322,93.26,49.292,93.421,49.226,93.471,49.205,93.516,49.213,93.56,49.206,93.654,49.215,93.723,49.19,93.786,49.158,93.799,49.129]]]},{"n":"","z":4.7,"b":[85.352,30.759,85.889,31.045],"p":[[[85.888,30.929,85.851,30.886,85.845,30.827,85.827,30.783,85.802,30.759,85.783,30.759,85.786,30.789,85.757,30.84,85.692,30.87,85.64,30.869,85.6,30.855,85.568,30.842,85.513,30.846,85.438,30.871,85.377,30.906,85.352,30.944,85.361,30.992,85.402,31.035,85.476,31.045,85.561,31.028,85.647,31.025,85.725,31.028,85.81,31.005,85.875,30.978,85.889,30.952,85.888,30.929]]]},{"n":"Assal","z":4.7,"b":[42.396,11.629,42.447,11.712],"p":[[[42.447,11.677,42.441,11.64,42.417,11.629,42.399,11.645,42.396,11.672,42.399,11.693,42.411,11.712,42.436,11.697,42.447,11.677]]]},{"n":"Rainy","z":4.7,"b":[-93.613,48.351,-92.435,48.934],"p":[[[-92.918,48.433,-92.995,48.451,-93.05,48.48,-93.071,48.504,-93.056,48.513,-92.982,48.491,-92.865,48.462,-92.788,48.455,-92.745,48.477,-92.704,48.505,-92.698,48.517,-92.813,48.54,-92.936,48.565,-92.95,48.576,-92.948,48.586,-93.001,48.593,-93.079,48.591,-93.109,48.581,-93.1,48.564,-93.117,48.553,-93.162,48.558,-93.196,48.579,-93.279,48.607,-93.339,48.6,-93.382,48.616,-93.37,48.638,-93.37,48.657,-93.401,48.681,-93.403,48.705,-93.378,48.71,-93.352,48.717,-93.365,48.748,-93.417,48.785,-93.496,48.813,-93.579,48.826,-93.613,48.834,-93.583,48.843,-93.491,48.839,-93.434,48.846,-93.439,48.869,-93.45,48.887,-93.463,48.903,-93.408,48.915,-93.345,48.929,-93.322,48.934,-93.294,48.885,-93.262,48.825,-93.227,48.805,-93.203,48.791,-93.201,48.767,-93.2,48.746,-93.229,48.718,-93.265,48.686,-93.261,48.674,-93.245,48.667,-93.223,48.67,-93.171,48.701,-93.124,48.728,-93.135,48.706,-93.149,48.675,-93.119,48.668,-93.015,48.683,-92.979,48.688,-92.949,48.681,-92.896,48.683,-92.793,48.695,-92.779,48.678,-92.853,48.646,-92.87,48.635,-92.868,48.625,-92.866,48.615,-92.808,48.604,-92.732,48.588,-92.685,48.591,-92.655,48.6,-92.641,48.587,-92.603,48.565,-92.585,48.534,-92.583,48.51,-92.57,48.5,-92.6,48.485,-92.567,48.468,-92.491,48.456,-92.47,48.44,-92.449,48.424,-92.435,48.391,-92.441,48.357,-92.456,48.351,-92.478,48.366,-92.52,48.399,-92.6,48.424,-92.764,48.43,-92.918,48.433]]]},{"n":"Williston","z":4.7,"b":[-124.836,55.176,-122.222,56.757],"p":[[[-122.502,56.084,-122.603,56.09,-122.7,56.077,-122.819,56.061,-122.925,56.058,-123.012,56.04,-123.139,55.974,-123.429,55.985,-123.716,56.015,-123.782,55.995,-123.79,55.942,-123.774,55.887,-123.76,55.863,-123.742,55.854,-123.715,55.848,-123.687,55.802,-123.657,55.752,-123.614,55.728,-123.536,55.678,-123.449,55.623,-123.387,55.572,-123.303,55.49,-123.232,55.425,-123.177,55.402,-123.169,55.393,-123.169,55.358,-123.173,55.33,-123.169,55.291,-123.159,55.235,-123.147,55.191,-123.144,55.176,-123.172,55.199,-123.237,55.3,-123.325,55.434,-123.415,55.534,-123.506,55.613,-123.638,55.684,-123.809,55.794,-123.883,55.813,-123.921,55.829,-123.996,55.893,-124.028,55.906,-124.04,55.913,-124.049,55.922,-124.043,55.943,-124.027,55.972,-124.033,56,-124.1,56.014,-124.21,56.014,-124.385,56.064,-124.508,56.114,-124.482,56.115,-124.354,56.081,-124.215,56.053,-124.166,56.067,-124.158,56.087,-124.238,56.146,-124.345,56.21,-124.481,56.361,-124.834,56.713,-124.836,56.757,-124.759,56.742,-124.661,56.658,-124.562,56.566,-124.459,56.471,-124.397,56.401,-124.375,56.368,-124.359,56.345,-124.312,56.31,-124.249,56.263,-124.194,56.24,-124.142,56.237,-124.073,56.255,-124.053,56.223,-124.073,56.154,-124.038,56.1,-123.97,56.061,-123.788,56.072,-123.606,56.061,-123.458,56.049,-123.238,56.029,-123.118,56.048,-123.091,56.076,-122.971,56.116,-122.832,56.13,-122.757,56.139,-122.638,56.159,-122.566,56.158,-122.423,56.128,-122.264,56.084,-122.223,56.058,-122.222,56.021,-122.293,56.023,-122.345,56.029,-122.435,56.055,-122.502,56.084]]]},{"n":"Split","z":4.7,"b":[-96.572,56.063,-95.842,56.302],"p":[[[-96.049,56.136,-96.081,56.118,-96.088,56.1,-96.073,56.085,-96.086,56.079,-96.173,56.085,-96.245,56.092,-96.358,56.094,-96.452,56.087,-96.553,56.063,-96.572,56.086,-96.504,56.15,-96.371,56.165,-96.279,56.177,-96.205,56.22,-96.054,56.273,-95.906,56.302,-95.842,56.291,-95.865,56.265,-95.933,56.246,-96.036,56.222,-96.105,56.203,-96.075,56.182,-96.044,56.156,-96.049,56.136]]]},{"n":"Fort Peck","z":4.7,"b":[-108.196,47.39,-106.217,48.027],"p":[[[-106.217,47.585,-106.251,47.55,-106.269,47.548,-106.252,47.6,-106.251,47.644,-106.285,47.697,-106.318,47.755,-106.336,47.785,-106.36,47.813,-106.384,47.848,-106.42,47.876,-106.454,47.879,-106.48,47.858,-106.521,47.828,-106.55,47.785,-106.556,47.75,-106.599,47.753,-106.683,47.739,-106.768,47.724,-106.813,47.716,-106.838,47.694,-106.85,47.649,-106.862,47.631,-106.879,47.649,-106.906,47.672,-106.944,47.667,-106.993,47.644,-107.015,47.645,-107.03,47.666,-107.183,47.685,-107.306,47.685,-107.333,47.685,-107.386,47.654,-107.429,47.635,-107.507,47.624,-107.612,47.623,-107.719,47.563,-107.846,47.468,-107.911,47.39,-107.924,47.407,-107.909,47.471,-107.916,47.509,-107.93,47.542,-107.949,47.558,-107.982,47.569,-108.044,47.58,-108.17,47.584,-108.196,47.602,-108.173,47.613,-108.045,47.614,-107.959,47.601,-107.931,47.588,-107.889,47.538,-107.841,47.526,-107.779,47.57,-107.712,47.613,-107.685,47.646,-107.688,47.671,-107.714,47.688,-107.605,47.676,-107.455,47.662,-107.422,47.68,-107.38,47.716,-107.336,47.728,-107.295,47.722,-107.231,47.724,-107.121,47.725,-106.99,47.716,-106.93,47.725,-106.938,47.746,-106.933,47.764,-106.881,47.778,-106.821,47.783,-106.769,47.788,-106.738,47.806,-106.709,47.822,-106.655,47.841,-106.596,47.86,-106.582,47.879,-106.566,47.907,-106.574,47.936,-106.59,47.958,-106.538,47.983,-106.433,48.027,-106.406,48.021,-106.366,48.015,-106.325,47.985,-106.331,47.95,-106.358,47.925,-106.345,47.879,-106.285,47.837,-106.253,47.815,-106.267,47.793,-106.253,47.769,-106.241,47.739,-106.232,47.691,-106.217,47.628,-106.217,47.585]]]},{"n":"Sakakawea","z":4.7,"b":[-103.772,47.464,-101.14,48.18],"p":[[[-102.147,47.568,-102.237,47.589,-102.333,47.586,-102.409,47.564,-102.429,47.562,-102.42,47.59,-102.361,47.625,-102.299,47.657,-102.308,47.705,-102.336,47.757,-102.425,47.764,-102.561,47.785,-102.616,47.819,-102.653,47.916,-102.622,47.956,-102.614,47.992,-102.657,48.035,-102.761,48.084,-102.901,48.12,-103.059,48.125,-103.186,48.097,-103.238,48.063,-103.339,48.03,-103.459,48.016,-103.52,48.027,-103.551,48.072,-103.583,48.097,-103.665,48.063,-103.75,48.017,-103.772,48.027,-103.759,48.074,-103.721,48.102,-103.666,48.142,-103.575,48.153,-103.524,48.108,-103.482,48.081,-103.45,48.067,-103.353,48.076,-103.211,48.145,-103.118,48.171,-103.03,48.18,-102.916,48.171,-102.79,48.145,-102.675,48.105,-102.598,48.053,-102.557,47.968,-102.579,47.911,-102.577,47.867,-102.575,47.844,-102.527,47.822,-102.462,47.814,-102.408,47.806,-102.384,47.82,-102.4,47.877,-102.415,47.938,-102.375,47.952,-102.311,47.964,-102.273,47.978,-102.258,47.978,-102.265,47.961,-102.279,47.938,-102.287,47.925,-102.294,47.912,-102.276,47.856,-102.253,47.793,-102.24,47.766,-102.187,47.748,-102.169,47.729,-102.212,47.677,-102.16,47.624,-102.093,47.604,-102.021,47.585,-101.884,47.561,-101.718,47.567,-101.625,47.58,-101.601,47.586,-101.589,47.599,-101.453,47.608,-101.276,47.628,-101.203,47.645,-101.163,47.634,-101.14,47.606,-101.184,47.585,-101.247,47.567,-101.328,47.533,-101.4,47.506,-101.59,47.515,-101.679,47.498,-101.772,47.488,-101.883,47.467,-101.917,47.464,-101.951,47.488,-102.032,47.529,-102.096,47.553,-102.147,47.568]]]},{"n":"Teslin","z":4.7,"b":[-133.245,59.604,-132.089,60.449],"p":[[[-132.095,59.607,-132.125,59.604,-132.159,59.626,-132.213,59.688,-132.316,59.835,-132.399,59.922,-132.61,60.05,-132.834,60.135,-132.958,60.201,-133.012,60.251,-133.187,60.382,-133.245,60.449,-133.138,60.427,-132.98,60.297,-132.798,60.192,-132.702,60.153,-132.589,60.123,-132.522,60.098,-132.461,60.055,-132.364,59.986,-132.323,59.951,-132.277,59.902,-132.23,59.849,-132.225,59.838,-132.214,59.816,-132.202,59.77,-132.123,59.694,-132.089,59.644,-132.095,59.607]]]},{"n":"Arrow Lakes","z":4.7,"b":[-118.156,49.336,-117.907,49.802],"p":[[[-117.907,49.342,-117.957,49.336,-118.063,49.412,-118.096,49.45,-118.139,49.521,-118.156,49.601,-118.138,49.703,-118.128,49.752,-118.092,49.802,-118.084,49.774,-118.109,49.583,-118.087,49.515,-118.048,49.457,-117.976,49.406,-117.911,49.375,-117.907,49.342]]]},{"n":"Pend Oreille","z":4.7,"b":[-116.569,47.964,-116.215,48.302],"p":[[[-116.215,48.16,-116.306,48.139,-116.41,48.107,-116.441,48.075,-116.446,48.026,-116.456,47.984,-116.489,47.97,-116.529,47.964,-116.553,47.971,-116.541,47.995,-116.522,48.044,-116.498,48.115,-116.458,48.168,-116.408,48.191,-116.374,48.208,-116.381,48.229,-116.4,48.247,-116.431,48.254,-116.469,48.263,-116.525,48.242,-116.569,48.25,-116.53,48.284,-116.499,48.301,-116.433,48.302,-116.362,48.284,-116.302,48.236,-116.235,48.183,-116.215,48.16]]]},{"n":"Chelan","z":4.7,"b":[-120.628,47.852,-120.011,48.279],"p":[[[-120.011,47.852,-120.05,47.857,-120.118,47.88,-120.172,47.914,-120.205,47.962,-120.333,48.04,-120.53,48.162,-120.625,48.257,-120.628,48.279,-120.535,48.204,-120.32,48.064,-120.163,47.958,-120.069,47.884,-120.011,47.852]]]},{"n":"Black","z":4.7,"b":[-105.868,58.919,-104.948,59.296],"p":[[[-105.394,59.128,-105.554,59.099,-105.66,59.055,-105.733,58.98,-105.785,58.922,-105.835,58.919,-105.868,58.939,-105.855,58.978,-105.806,59.031,-105.55,59.15,-105.215,59.296,-105.119,59.286,-105.012,59.263,-104.948,59.237,-104.975,59.2,-105.012,59.173,-105.216,59.146,-105.303,59.138,-105.394,59.128]]]},{"n":"Louise","z":4.7,"b":[-146.704,62.282,-146.473,62.508],"p":[[[-146.473,62.332,-146.478,62.312,-146.515,62.282,-146.564,62.293,-146.653,62.362,-146.704,62.411,-146.703,62.445,-146.69,62.477,-146.684,62.504,-146.661,62.508,-146.648,62.484,-146.634,62.46,-146.61,62.414,-146.54,62.363,-146.473,62.332]]]},{"n":"Red","z":4.7,"b":[-95.271,47.889,-94.777,48.065],"p":[[[-94.97,48.032,-94.871,48.004,-94.783,47.97,-94.777,47.936,-94.792,47.91,-94.897,47.892,-95.111,47.889,-95.248,47.907,-95.271,47.942,-95.251,47.989,-95.187,48.033,-95.086,48.065,-95.011,48.06,-94.97,48.032]]]},{"n":"Red","z":4.7,"b":[-95.031,48.064,-94.558,48.203],"p":[[[-95.02,48.125,-94.975,48.152,-94.84,48.184,-94.69,48.203,-94.612,48.199,-94.561,48.178,-94.558,48.141,-94.6,48.106,-94.663,48.083,-94.75,48.066,-94.894,48.064,-95.008,48.072,-95.031,48.094,-95.02,48.125]]]},{"n":"Yathkyed","z":4.7,"b":[-98.458,62.422,-97.309,62.979],"p":[[[-97.969,62.548,-97.995,62.566,-98.051,62.607,-98.132,62.664,-98.206,62.69,-98.227,62.67,-98.315,62.618,-98.35,62.616,-98.348,62.665,-98.356,62.708,-98.387,62.73,-98.419,62.762,-98.447,62.804,-98.458,62.829,-98.432,62.828,-98.409,62.816,-98.356,62.811,-98.263,62.812,-98.207,62.81,-98.202,62.829,-98.194,62.851,-98.145,62.855,-98.099,62.842,-98.055,62.849,-97.983,62.864,-97.874,62.867,-97.764,62.864,-97.714,62.891,-97.68,62.947,-97.63,62.979,-97.617,62.967,-97.618,62.924,-97.638,62.891,-97.662,62.864,-97.647,62.828,-97.635,62.781,-97.631,62.716,-97.613,62.663,-97.561,62.638,-97.518,62.603,-97.509,62.568,-97.475,62.534,-97.427,62.502,-97.384,62.485,-97.36,62.484,-97.358,62.496,-97.36,62.507,-97.349,62.508,-97.326,62.501,-97.309,62.485,-97.318,62.461,-97.377,62.432,-97.445,62.422,-97.528,62.453,-97.626,62.492,-97.702,62.507,-97.78,62.509,-97.842,62.5,-97.876,62.488,-97.889,62.47,-97.913,62.459,-97.965,62.474,-98.023,62.504,-98.048,62.527,-98.016,62.537,-97.969,62.548]]]},{"n":"Amadjuak","z":4.7,"b":[-72.053,64.581,-70.378,65.408],"p":[[[-70.647,64.686,-70.637,64.711,-70.694,64.739,-70.804,64.774,-70.917,64.792,-70.999,64.787,-71.052,64.768,-71.097,64.707,-71.225,64.643,-71.373,64.636,-71.446,64.626,-71.499,64.609,-71.563,64.613,-71.657,64.632,-71.797,64.681,-71.899,64.728,-71.919,64.751,-71.898,64.778,-71.928,64.806,-71.982,64.835,-72.001,64.867,-72.019,64.889,-72.036,64.9,-72.053,64.91,-72.039,64.925,-71.995,64.942,-71.956,64.948,-71.959,64.931,-71.963,64.913,-71.938,64.907,-71.851,64.918,-71.819,64.91,-71.872,64.873,-71.873,64.844,-71.818,64.824,-71.748,64.819,-71.708,64.829,-71.725,64.845,-71.75,64.861,-71.584,64.884,-71.512,64.906,-71.504,64.921,-71.534,64.947,-71.638,64.985,-71.624,65.016,-71.583,65.04,-71.555,65.051,-71.546,65.082,-71.57,65.129,-71.642,65.189,-71.691,65.242,-71.676,65.278,-71.665,65.308,-71.717,65.353,-71.765,65.397,-71.737,65.408,-71.645,65.367,-71.542,65.31,-71.483,65.289,-71.429,65.286,-71.357,65.28,-71.3,65.266,-71.283,65.251,-71.27,65.234,-71.231,65.211,-71.093,65.163,-70.937,65.124,-70.909,65.111,-70.875,65.101,-70.771,65.07,-70.695,65.024,-70.667,64.985,-70.648,64.959,-70.636,64.934,-70.633,64.908,-70.532,64.832,-70.414,64.744,-70.384,64.717,-70.378,64.707,-70.429,64.686,-70.48,64.664,-70.477,64.655,-70.473,64.64,-70.467,64.621,-70.419,64.605,-70.385,64.59,-70.416,64.581,-70.509,64.6,-70.627,64.619,-70.721,64.622,-70.729,64.639,-70.682,64.661,-70.669,64.671,-70.647,64.686]]]},{"n":"Amistad","z":4.7,"b":[-101.312,29.458,-100.918,29.648],"p":[[[-100.918,29.502,-100.92,29.495,-100.936,29.486,-100.978,29.48,-101.039,29.46,-101.1,29.458,-101.151,29.467,-101.19,29.492,-101.233,29.503,-101.272,29.498,-101.287,29.506,-101.275,29.526,-101.261,29.562,-101.269,29.578,-101.28,29.585,-101.302,29.584,-101.312,29.603,-101.31,29.622,-101.305,29.634,-101.275,29.648,-101.214,29.603,-101.181,29.579,-101.142,29.558,-101.108,29.528,-101.072,29.51,-101.035,29.526,-101.004,29.548,-100.993,29.535,-100.977,29.557,-100.951,29.61,-100.944,29.606,-100.956,29.565,-100.951,29.529,-100.933,29.507,-100.918,29.502]]]},{"n":"Cabonga","z":4.7,"b":[-76.823,47.056,-76.327,47.691],"p":[[[-76.479,47.138,-76.435,47.122,-76.394,47.122,-76.359,47.119,-76.327,47.102,-76.331,47.083,-76.363,47.068,-76.393,47.056,-76.443,47.064,-76.505,47.092,-76.535,47.109,-76.553,47.121,-76.582,47.178,-76.603,47.241,-76.625,47.269,-76.651,47.278,-76.668,47.266,-76.654,47.222,-76.639,47.185,-76.669,47.2,-76.707,47.242,-76.715,47.311,-76.741,47.355,-76.781,47.357,-76.789,47.385,-76.778,47.416,-76.767,47.449,-76.779,47.476,-76.823,47.498,-76.819,47.517,-76.748,47.558,-76.658,47.611,-76.562,47.667,-76.522,47.691,-76.516,47.673,-76.595,47.624,-76.699,47.57,-76.722,47.54,-76.715,47.524,-76.656,47.552,-76.605,47.572,-76.618,47.543,-76.633,47.51,-76.556,47.498,-76.459,47.476,-76.408,47.446,-76.422,47.4,-76.475,47.331,-76.489,47.284,-76.494,47.25,-76.502,47.222,-76.517,47.201,-76.512,47.171,-76.479,47.138]]]},{"n":"Chapala","z":4.7,"b":[-103.372,20.124,-102.7,20.329],"p":[[[-102.711,20.194,-102.7,20.182,-102.713,20.162,-102.744,20.139,-102.786,20.124,-102.852,20.139,-102.934,20.168,-103.008,20.183,-103.089,20.181,-103.17,20.179,-103.255,20.2,-103.34,20.231,-103.372,20.26,-103.272,20.281,-103.105,20.308,-103.002,20.329,-102.894,20.319,-102.77,20.198,-102.744,20.198,-102.711,20.194]]]},{"n":"Managua","z":4.7,"b":[-86.589,12.172,-86.102,12.48],"p":[[[-86.104,12.312,-86.102,12.271,-86.105,12.208,-86.175,12.185,-86.261,12.172,-86.315,12.177,-86.307,12.206,-86.297,12.242,-86.304,12.265,-86.323,12.28,-86.387,12.274,-86.47,12.274,-86.529,12.303,-86.572,12.343,-86.589,12.38,-86.589,12.405,-86.546,12.41,-86.468,12.428,-86.4,12.464,-86.339,12.48,-86.294,12.474,-86.236,12.383,-86.161,12.315,-86.119,12.329,-86.104,12.312]]]},{"n":"Itaipu","z":4.7,"b":[-54.795,-25.44,-54.183,-24.066],"p":[[[-54.183,-24.708,-54.227,-24.726,-54.292,-24.751,-54.312,-24.783,-54.308,-24.819,-54.257,-24.878,-54.221,-24.92,-54.251,-24.916,-54.314,-24.892,-54.357,-24.892,-54.375,-24.92,-54.393,-24.946,-54.387,-24.982,-54.337,-25.033,-54.318,-25.079,-54.332,-25.115,-54.348,-25.139,-54.377,-25.151,-54.377,-25.18,-54.288,-25.216,-54.201,-25.24,-54.268,-25.261,-54.34,-25.291,-54.383,-25.303,-54.433,-25.309,-54.432,-25.335,-54.402,-25.371,-54.424,-25.389,-54.491,-25.387,-54.527,-25.396,-54.554,-25.419,-54.582,-25.44,-54.611,-25.433,-54.634,-25.417,-54.634,-25.302,-54.608,-25.267,-54.547,-25.235,-54.519,-25.19,-54.527,-25.155,-54.559,-25.138,-54.58,-25.128,-54.576,-25.112,-54.624,-25.101,-54.721,-25.088,-54.754,-25.063,-54.712,-25.043,-54.61,-25.045,-54.528,-25.053,-54.51,-25.016,-54.506,-24.965,-54.526,-24.935,-54.67,-24.905,-54.795,-24.88,-54.77,-24.868,-54.7,-24.873,-54.612,-24.884,-54.531,-24.877,-54.474,-24.848,-54.466,-24.804,-54.524,-24.75,-54.49,-24.712,-54.4,-24.69,-54.386,-24.659,-54.387,-24.62,-54.419,-24.579,-54.42,-24.542,-54.388,-24.509,-54.4,-24.454,-54.382,-24.416,-54.332,-24.404,-54.312,-24.368,-54.313,-24.322,-54.348,-24.264,-54.368,-24.211,-54.357,-24.165,-54.35,-24.122,-54.323,-24.092,-54.267,-24.066,-54.283,-24.144,-54.271,-24.225,-54.252,-24.286,-54.258,-24.468,-54.259,-24.642,-54.216,-24.686,-54.183,-24.708]]]},{"n":"Gatun","z":4.7,"b":[-80.076,9.016,-79.671,9.32],"p":[[[-79.671,9.144,-79.688,9.124,-79.691,9.104,-79.8,9.073,-79.83,9.046,-79.847,9.045,-79.855,9.055,-79.834,9.08,-79.811,9.105,-79.836,9.118,-79.875,9.139,-79.9,9.154,-79.912,9.121,-79.926,9.078,-79.947,9.068,-79.953,9.043,-79.96,9.023,-79.973,9.024,-79.993,9.039,-80.012,9.06,-80.021,9.056,-80.019,9.037,-80.03,9.026,-80.058,9.021,-80.076,9.016,-80.068,9.041,-80.013,9.097,-79.953,9.192,-79.923,9.267,-79.857,9.294,-79.822,9.32,-79.809,9.316,-79.805,9.285,-79.799,9.254,-79.822,9.238,-79.844,9.222,-79.838,9.207,-79.816,9.185,-79.808,9.172,-79.776,9.148,-79.739,9.13,-79.717,9.127,-79.686,9.134,-79.671,9.144]]]},{"n":"Kariba","z":4.7,"b":[27.018,-17.963,28.988,-16.495],"p":[[[28.988,-16.611,28.968,-16.626,28.943,-16.658,28.931,-16.689,28.901,-16.682,28.867,-16.676,28.854,-16.724,28.829,-16.77,28.79,-16.79,28.762,-16.808,28.725,-16.798,28.697,-16.774,28.7,-16.747,28.69,-16.71,28.655,-16.7,28.613,-16.716,28.586,-16.728,28.555,-16.739,28.509,-16.759,28.474,-16.793,28.459,-16.834,28.461,-16.866,28.454,-16.865,28.419,-16.837,28.382,-16.795,28.341,-16.788,28.242,-16.818,28.15,-16.839,28.117,-16.861,28.101,-16.896,28.084,-16.947,28.089,-16.978,28.102,-16.993,28.096,-17.017,28.076,-17.046,28.047,-17.065,27.999,-17.051,27.959,-17.04,27.943,-17.051,27.914,-17.079,27.871,-17.118,27.841,-17.146,27.813,-17.156,27.75,-17.175,27.705,-17.207,27.683,-17.23,27.668,-17.254,27.654,-17.294,27.611,-17.381,27.576,-17.451,27.565,-17.455,27.549,-17.474,27.502,-17.508,27.455,-17.533,27.44,-17.553,27.45,-17.558,27.462,-17.559,27.453,-17.568,27.407,-17.58,27.353,-17.602,27.336,-17.618,27.287,-17.673,27.228,-17.754,27.195,-17.801,27.164,-17.844,27.116,-17.912,27.077,-17.956,27.051,-17.963,27.021,-17.958,27.018,-17.927,27.04,-17.904,27.065,-17.874,27.094,-17.848,27.096,-17.807,27.107,-17.771,27.172,-17.689,27.255,-17.61,27.292,-17.581,27.318,-17.564,27.338,-17.55,27.354,-17.529,27.371,-17.504,27.372,-17.479,27.344,-17.46,27.371,-17.389,27.431,-17.3,27.472,-17.247,27.55,-17.152,27.609,-17.073,27.641,-17.068,27.669,-17.075,27.688,-17.066,27.686,-17.042,27.704,-16.998,27.717,-16.959,27.705,-16.94,27.718,-16.928,27.753,-16.923,27.8,-16.917,27.831,-16.901,27.817,-16.869,27.789,-16.836,27.794,-16.819,27.826,-16.811,27.858,-16.793,27.877,-16.773,27.899,-16.769,27.917,-16.782,27.942,-16.797,27.975,-16.819,28.009,-16.824,28.042,-16.818,28.073,-16.79,28.106,-16.755,28.136,-16.736,28.168,-16.707,28.179,-16.679,28.175,-16.655,28.206,-16.647,28.246,-16.63,28.283,-16.601,28.298,-16.568,28.29,-16.543,28.314,-16.549,28.35,-16.56,28.373,-16.573,28.393,-16.586,28.441,-16.558,28.497,-16.54,28.53,-16.546,28.552,-16.536,28.56,-16.521,28.579,-16.506,28.623,-16.495,28.681,-16.508,28.761,-16.532,28.854,-16.572,28.899,-16.569,28.943,-16.576,28.967,-16.597,28.988,-16.611]]]},{"n":"Kyoga","z":4.7,"b":[32.537,1.223,33.487,1.731],"p":[[[33.487,1.643,33.443,1.641,33.334,1.623,33.227,1.585,33.161,1.542,33.126,1.503,33.14,1.483,33.206,1.477,33.258,1.47,33.251,1.454,33.208,1.43,33.197,1.399,33.243,1.386,33.282,1.402,33.303,1.419,33.317,1.42,33.321,1.398,33.326,1.356,33.305,1.305,33.271,1.253,33.26,1.223,33.23,1.254,33.167,1.289,33.123,1.313,33.019,1.382,32.899,1.43,32.869,1.432,32.864,1.419,32.868,1.405,32.892,1.381,32.893,1.311,32.86,1.332,32.824,1.388,32.786,1.402,32.764,1.394,32.752,1.357,32.742,1.34,32.661,1.376,32.58,1.411,32.554,1.437,32.537,1.497,32.545,1.522,32.611,1.514,32.736,1.545,32.801,1.581,32.803,1.572,32.797,1.556,32.811,1.549,32.862,1.556,32.938,1.563,32.982,1.553,32.999,1.547,33.022,1.56,33.067,1.601,33.101,1.634,33.113,1.624,33.129,1.616,33.163,1.639,33.206,1.66,33.237,1.67,33.269,1.664,33.31,1.656,33.357,1.677,33.414,1.731,33.446,1.728,33.457,1.684,33.471,1.671,33.483,1.653,33.487,1.643]]]},{"n":"Kubenskoe","z":4.7,"b":[39.053,59.48,39.798,59.869],"p":[[[39.267,59.783,39.305,59.774,39.348,59.766,39.454,59.711,39.511,59.707,39.582,59.665,39.707,59.634,39.78,59.638,39.744,59.61,39.721,59.578,39.764,59.546,39.798,59.525,39.734,59.48,39.553,59.546,39.402,59.624,39.194,59.747,39.053,59.839,39.054,59.868,39.093,59.869,39.139,59.842,39.192,59.809,39.23,59.794,39.267,59.783]]]},{"n":"Constance","z":4.7,"b":[9.113,47.506,9.731,47.782],"p":[[[9.731,47.527,9.719,47.515,9.685,47.506,9.554,47.511,9.452,47.514,9.25,47.598,9.183,47.671,9.113,47.772,9.132,47.782,9.231,47.733,9.373,47.677,9.514,47.625,9.644,47.566,9.731,47.527]]]},{"n":"","z":4.7,"b":[22.579,61.202,23.293,61.46],"p":[[[23.241,61.417,23.185,61.407,23.1,61.392,23.046,61.386,22.987,61.369,22.847,61.324,22.702,61.263,22.659,61.219,22.613,61.202,22.579,61.211,22.632,61.234,22.663,61.268,22.718,61.3,22.841,61.341,22.975,61.395,23.053,61.429,23.137,61.446,23.26,61.46,23.293,61.452,23.249,61.433,23.244,61.424,23.241,61.417]]]},{"n":"Dead Sea","z":4.7,"b":[35.377,31.096,35.522,31.232],"p":[[[35.522,31.177,35.517,31.154,35.508,31.125,35.498,31.105,35.459,31.096,35.417,31.104,35.391,31.135,35.377,31.178,35.382,31.216,35.401,31.231,35.446,31.232,35.494,31.205,35.522,31.177]]]},{"n":"Sea of Galilee","z":4.7,"b":[35.534,32.72,35.636,32.9],"p":[[[35.632,32.782,35.616,32.749,35.577,32.72,35.551,32.75,35.534,32.803,35.539,32.856,35.565,32.892,35.599,32.9,35.623,32.878,35.636,32.834,35.632,32.782]]]},{"n":"Argyle","z":4.7,"b":[128.53,-16.788,129.032,-16.076],"p":[[[129.032,-16.143,129.015,-16.161,129.001,-16.19,128.97,-16.228,128.944,-16.271,128.958,-16.302,128.951,-16.319,128.938,-16.349,128.928,-16.37,128.909,-16.394,128.895,-16.424,128.878,-16.436,128.864,-16.428,128.851,-16.422,128.827,-16.422,128.802,-16.45,128.792,-16.508,128.798,-16.554,128.794,-16.593,128.788,-16.637,128.784,-16.694,128.757,-16.778,128.734,-16.788,128.72,-16.759,128.696,-16.717,128.663,-16.681,128.626,-16.66,128.611,-16.645,128.597,-16.633,128.583,-16.615,128.587,-16.602,128.591,-16.572,128.584,-16.542,128.537,-16.536,128.53,-16.508,128.568,-16.444,128.62,-16.314,128.635,-16.25,128.659,-16.236,128.678,-16.223,128.671,-16.195,128.645,-16.159,128.658,-16.144,128.694,-16.137,128.737,-16.134,128.774,-16.087,128.805,-16.076,128.809,-16.087,128.778,-16.126,128.784,-16.149,128.791,-16.171,128.812,-16.171,128.844,-16.158,128.891,-16.139,128.946,-16.117,128.991,-16.117,129.022,-16.128,129.032,-16.143]]]},{"n":"Ngoring","z":4.7,"b":[97.547,34.788,97.887,35.088],"p":[[[97.887,34.838,97.861,34.823,97.801,34.827,97.751,34.828,97.717,34.826,97.695,34.831,97.689,34.812,97.683,34.792,97.648,34.788,97.603,34.798,97.575,34.825,97.547,34.876,97.555,34.902,97.583,34.929,97.625,34.953,97.644,34.978,97.636,35.003,97.651,35.028,97.687,35.053,97.735,35.08,97.763,35.088,97.794,35.033,97.797,34.982,97.801,34.932,97.847,34.882,97.887,34.838]]]},{"n":"Gyaring","z":4.7,"b":[97.062,34.826,97.468,35.023],"p":[[[97.468,34.976,97.46,34.961,97.413,34.916,97.36,34.865,97.288,34.835,97.23,34.826,97.084,34.907,97.072,34.931,97.062,34.979,97.067,34.997,97.085,35,97.118,35.008,97.231,35.023,97.351,35.02,97.419,34.997,97.468,34.976]]]},{"n":"Toktogul","z":4.7,"b":[72.635,41.663,73.3,41.9],"p":[[[73.291,41.759,73.266,41.755,73.202,41.745,73.107,41.757,72.972,41.755,72.839,41.74,72.679,41.663,72.654,41.667,72.635,41.687,72.658,41.701,72.689,41.728,72.709,41.767,72.688,41.822,72.658,41.874,72.685,41.9,72.817,41.898,72.94,41.878,73.011,41.855,73.086,41.828,73.24,41.801,73.3,41.776,73.291,41.759]]]},{"n":"Rakshastal","z":4.7,"b":[81.126,30.608,81.309,30.863],"p":[[[81.309,30.636,81.29,30.617,81.255,30.608,81.212,30.611,81.158,30.634,81.126,30.662,81.15,30.688,81.185,30.721,81.184,30.804,81.199,30.852,81.234,30.863,81.275,30.839,81.294,30.817,81.296,30.789,81.252,30.753,81.279,30.709,81.309,30.662,81.309,30.636]]]},{"n":"Manasarovar","z":4.7,"b":[81.373,30.591,81.584,30.778],"p":[[[81.584,30.7,81.552,30.654,81.494,30.61,81.434,30.591,81.392,30.61,81.373,30.659,81.376,30.758,81.409,30.778,81.46,30.774,81.524,30.757,81.574,30.733,81.584,30.7]]]},{"n":"Volta","z":4.7,"b":[-1.715,6.338,0.32,9.117],"p":[[[0.272,6.675,0.299,6.719,0.317,6.738,0.309,6.698,0.276,6.648,0.23,6.616,0.192,6.607,0.172,6.615,0.16,6.618,0.152,6.611,0.154,6.597,0.151,6.57,0.147,6.538,0.14,6.505,0.133,6.45,0.138,6.366,0.103,6.338,0.081,6.353,0.073,6.404,0.041,6.405,-0.011,6.379,-0.033,6.408,-0.06,6.452,-0.099,6.49,-0.175,6.533,-0.255,6.585,-0.324,6.622,-0.428,6.668,-0.55,6.717,-0.634,6.753,-0.674,6.802,-0.684,6.834,-0.66,6.836,-0.616,6.807,-0.58,6.785,-0.551,6.811,-0.518,6.794,-0.491,6.737,-0.467,6.723,-0.442,6.727,-0.419,6.731,-0.409,6.766,-0.406,6.816,-0.393,6.828,-0.368,6.773,-0.335,6.716,-0.305,6.717,-0.271,6.722,-0.228,6.734,-0.194,6.741,-0.175,6.724,-0.163,6.686,-0.144,6.648,-0.096,6.629,-0.057,6.63,-0.041,6.677,-0.026,6.72,-0.017,6.733,-0.023,6.748,-0.049,6.769,-0.067,6.789,-0.071,6.809,-0.066,6.825,-0.042,6.847,-0.011,6.853,0.012,6.845,0.03,6.85,0.077,6.87,0.112,6.897,0.104,6.91,0.059,6.914,0.042,6.946,0.087,6.974,0.12,6.971,0.123,6.983,0.099,7.004,0.088,7.023,0.093,7.037,0.101,7.053,0.098,7.08,0.086,7.121,0.085,7.147,0.066,7.151,0.045,7.164,0.028,7.176,0.01,7.187,0.007,7.209,0.015,7.211,0.035,7.2,0.063,7.195,0.083,7.194,0.099,7.193,0.118,7.203,0.142,7.222,0.153,7.241,0.16,7.252,0.174,7.252,0.184,7.266,0.184,7.287,0.172,7.304,0.156,7.309,0.135,7.313,0.102,7.343,0.068,7.393,0.05,7.432,0.051,7.449,0.047,7.46,0.046,7.478,-0.002,7.5,-0.063,7.508,-0.058,7.466,-0.066,7.444,-0.107,7.458,-0.128,7.472,-0.136,7.496,-0.157,7.51,-0.207,7.512,-0.242,7.517,-0.232,7.532,-0.208,7.546,-0.169,7.55,-0.134,7.553,-0.105,7.551,-0.071,7.549,-0.027,7.546,0.012,7.543,-0.018,7.592,-0.064,7.641,-0.11,7.645,-0.182,7.647,-0.242,7.639,-0.336,7.618,-0.432,7.594,-0.475,7.589,-0.498,7.597,-0.531,7.576,-0.603,7.554,-0.654,7.551,-0.611,7.567,-0.543,7.6,-0.483,7.625,-0.422,7.649,-0.357,7.664,-0.294,7.667,-0.262,7.684,-0.256,7.706,-0.254,7.72,-0.21,7.714,-0.161,7.721,-0.125,7.74,-0.098,7.746,-0.1,7.755,-0.134,7.767,-0.157,7.779,-0.159,7.794,-0.152,7.813,-0.152,7.841,-0.17,7.89,-0.186,7.927,-0.192,7.955,-0.224,7.977,-0.285,8.015,-0.315,8.04,-0.344,8.038,-0.351,8.002,-0.384,7.98,-0.417,7.972,-0.394,8,-0.379,8.048,-0.415,8.079,-0.446,8.091,-0.498,8.057,-0.527,8.062,-0.526,8.128,-0.543,8.154,-0.561,8.134,-0.591,8.1,-0.624,8.085,-0.643,8.087,-0.643,8.106,-0.693,8.11,-0.756,8.102,-0.761,8.118,-0.748,8.135,-0.721,8.135,-0.655,8.142,-0.604,8.159,-0.6,8.173,-0.621,8.191,-0.676,8.239,-0.724,8.28,-0.754,8.297,-0.795,8.315,-0.801,8.334,-0.784,8.366,-0.838,8.447,-0.912,8.555,-0.965,8.604,-0.999,8.63,-1.041,8.646,-1.087,8.64,-1.128,8.632,-1.141,8.595,-1.153,8.551,-1.18,8.604,-1.219,8.658,-1.254,8.668,-1.304,8.697,-1.351,8.724,-1.405,8.739,-1.443,8.731,-1.488,8.697,-1.545,8.664,-1.605,8.644,-1.664,8.629,-1.715,8.63,-1.663,8.673,-1.591,8.695,-1.519,8.731,-1.466,8.761,-1.412,8.781,-1.376,8.805,-1.355,8.792,-1.294,8.75,-1.269,8.726,-1.126,8.705,-1.062,8.722,-1.067,8.75,-1.096,8.784,-1.134,8.813,-1.145,8.874,-1.174,8.945,-1.179,8.979,-1.178,9.027,-1.174,9.074,-1.152,9.117,-1.13,9.076,-1.127,9.028,-1.125,8.981,-1.121,8.929,-1.106,8.897,-1.075,8.853,-1.045,8.843,-1.026,8.844,-0.997,8.775,-0.931,8.653,-0.873,8.586,-0.843,8.579,-0.817,8.6,-0.818,8.65,-0.823,8.707,-0.809,8.743,-0.791,8.761,-0.782,8.756,-0.795,8.721,-0.806,8.689,-0.77,8.656,-0.722,8.639,-0.705,8.643,-0.707,8.625,-0.757,8.578,-0.805,8.52,-0.797,8.497,-0.782,8.469,-0.721,8.402,-0.66,8.357,-0.627,8.329,-0.594,8.288,-0.566,8.275,-0.554,8.251,-0.527,8.232,-0.485,8.209,-0.449,8.209,-0.394,8.221,-0.355,8.243,-0.318,8.244,-0.286,8.253,-0.245,8.245,-0.202,8.213,-0.193,8.186,-0.257,8.185,-0.321,8.177,-0.33,8.158,-0.308,8.142,-0.263,8.108,-0.19,8.053,-0.128,8.011,-0.103,7.976,-0.082,7.876,-0.052,7.811,-0.024,7.793,0.002,7.793,0.019,7.789,0.055,7.792,0.07,7.821,0.058,7.863,0.056,7.886,0.059,7.92,0.073,7.969,0.079,8.003,0.062,8.041,0.07,8.081,0.099,8.093,0.129,8.086,0.154,8.078,0.17,8.086,0.165,8.115,0.152,8.161,0.123,8.21,0.105,8.242,0.137,8.254,0.169,8.289,0.198,8.338,0.217,8.363,0.161,8.381,0.092,8.403,0.081,8.429,0.093,8.461,0.091,8.5,0.085,8.548,0.106,8.583,0.135,8.598,0.15,8.633,0.16,8.7,0.184,8.742,0.198,8.738,0.185,8.69,0.168,8.625,0.152,8.592,0.128,8.574,0.121,8.518,0.113,8.457,0.103,8.434,0.115,8.416,0.178,8.399,0.242,8.379,0.261,8.355,0.28,8.341,0.294,8.334,0.284,8.327,0.242,8.322,0.214,8.314,0.217,8.296,0.19,8.251,0.184,8.206,0.201,8.166,0.231,8.125,0.268,8.106,0.267,8.094,0.24,8.088,0.198,8.079,0.161,8.06,0.146,8.021,0.121,7.972,0.108,7.939,0.124,7.913,0.146,7.894,0.168,7.863,0.221,7.883,0.258,7.905,0.251,7.876,0.252,7.863,0.248,7.849,0.237,7.834,0.248,7.803,0.278,7.762,0.279,7.699,0.281,7.617,0.282,7.575,0.259,7.581,0.24,7.578,0.228,7.556,0.229,7.536,0.242,7.511,0.25,7.491,0.285,7.515,0.32,7.542,0.313,7.506,0.302,7.47,0.297,7.453,0.295,7.416,0.287,7.382,0.276,7.353,0.277,7.339,0.277,7.33,0.292,7.303,0.294,7.278,0.276,7.258,0.272,7.234,0.279,7.218,0.281,7.201,0.279,7.182,0.276,7.172,0.271,7.138,0.289,7.106,0.305,7.099,0.3,7.071,0.281,7.047,0.273,7.001,0.263,6.948,0.24,6.896,0.237,6.768,0.254,6.673,0.272,6.675],[0.13,6.679,0.181,6.726,0.159,6.767,0.115,6.796,0.062,6.808,0.017,6.779,0.011,6.718,0.049,6.695,0.09,6.659,0.13,6.679]]]},{"n":"South Aral Sea","z":1,"b":[58.221,44.425,59.303,46.002],"p":[[[58.312,45.062,58.372,45.131,58.417,45.291,58.549,45.408,58.574,45.532,58.638,45.59,58.653,45.722,58.712,45.835,58.838,45.834,58.814,45.882,58.849,45.918,59.06,45.895,59.079,45.943,59.179,45.926,59.24,45.994,59.303,46.002,59.244,45.892,59.105,45.897,58.914,45.795,58.869,45.596,58.766,45.563,58.672,45.46,58.629,45.264,58.682,45.13,58.547,45.008,58.588,44.99,58.567,44.915,58.624,44.818,58.563,44.772,58.524,44.569,58.483,44.504,58.364,44.425,58.26,44.431,58.235,44.461,58.237,44.524,58.321,44.603,58.221,44.847,58.312,45.062]]]},{"n":"North Aral Sea","z":1,"b":[59.988,46.086,61.528,46.784],"p":[[[60.783,46.086,60.752,46.118,60.825,46.201,60.722,46.374,60.58,46.293,60.348,46.403,60.133,46.357,59.988,46.438,60.002,46.572,60.287,46.7,60.368,46.617,60.401,46.509,60.41,46.504,60.42,46.508,60.453,46.618,60.459,46.623,60.471,46.622,60.58,46.563,60.741,46.627,60.746,46.639,60.745,46.653,60.526,46.751,60.525,46.777,60.58,46.784,60.811,46.747,60.776,46.581,60.892,46.473,61.134,46.45,61.256,46.493,61.264,46.5,61.265,46.513,61.203,46.577,61.297,46.611,61.315,46.648,61.528,46.677,61.45,46.584,61.37,46.528,61.283,46.448,61.3,46.385,61.217,46.237,60.977,46.17,60.862,46.09,60.783,46.086]]]},{"n":"Barsakelmes","z":5,"b":[59.504,46.083,59.813,46.322],"p":[[[59.729,46.083,59.592,46.209,59.504,46.319,59.731,46.322,59.785,46.284,59.813,46.124,59.729,46.083]]]},{"n":"Balkhash","z":1,"b":[73.437,44.989,79.245,46.818],"p":[[[78.991,46.749,79.019,46.768,79.022,46.795,79.052,46.808,79.084,46.81,79.122,46.799,79.177,46.764,79.222,46.72,79.245,46.68,79.245,46.645,79.236,46.634,79.217,46.61,79.157,46.603,79.095,46.582,79.068,46.526,79.023,46.447,78.971,46.406,78.89,46.37,78.787,46.385,78.711,46.396,78.625,46.38,78.544,46.401,78.465,46.392,78.438,46.377,78.435,46.352,78.462,46.313,78.445,46.297,78.409,46.299,78.367,46.339,78.317,46.35,78.233,46.337,78.161,46.319,78.128,46.319,78.075,46.328,78.018,46.343,77.865,46.429,77.817,46.439,77.781,46.423,77.726,46.407,77.652,46.415,77.563,46.435,77.429,46.5,77.355,46.502,77.282,46.473,77.165,46.489,77.104,46.454,77.004,46.464,76.853,46.492,76.742,46.532,76.464,46.542,76.238,46.555,76.176,46.532,76.033,46.512,75.909,46.536,75.867,46.536,75.715,46.516,75.611,46.507,75.558,46.53,75.515,46.567,75.492,46.596,75.49,46.631,75.508,46.636,75.548,46.634,75.562,46.646,75.56,46.653,75.553,46.673,75.509,46.678,75.463,46.671,75.417,46.641,75.404,46.61,75.432,46.542,75.448,46.498,75.406,46.471,75.35,46.47,75.33,46.513,75.304,46.519,75.278,46.515,75.256,46.474,75.241,46.448,75.212,46.427,75.152,46.438,75.121,46.446,75.093,46.461,75.067,46.451,75.013,46.417,74.911,46.405,74.888,46.377,74.857,46.294,74.815,46.216,74.794,46.13,74.771,46.108,74.599,46.022,74.55,46.006,74.37,46.02,74.278,46.004,74.258,45.977,74.253,45.942,74.303,45.79,74.289,45.765,74.251,45.761,74.243,45.752,74.245,45.74,74.311,45.695,74.313,45.671,74.289,45.651,74.231,45.615,74.101,45.537,74.089,45.509,74.083,45.352,74.087,45.344,74.128,45.337,74.151,45.317,74.156,45.271,74.143,45.103,74.136,45.07,74.101,45.059,74.094,45.04,74.12,45,74.1,44.989,74.056,44.994,74.017,45.013,74.01,45.034,74.016,45.056,73.993,45.112,74.008,45.147,73.999,45.189,73.981,45.204,73.877,45.239,73.84,45.264,73.663,45.436,73.615,45.513,73.465,45.582,73.437,45.609,73.444,45.657,73.494,45.687,73.532,45.734,73.519,45.763,73.451,45.78,73.44,45.806,73.463,45.826,73.488,45.831,73.56,45.816,73.633,45.907,73.715,45.983,73.737,46.013,73.663,46.062,73.654,46.093,73.646,46.15,73.679,46.183,73.752,46.203,73.775,46.206,73.829,46.185,73.898,46.208,73.982,46.186,74.021,46.205,74.027,46.222,73.95,46.288,74.023,46.369,74.094,46.428,74.28,46.47,74.375,46.518,74.43,46.578,74.524,46.623,74.553,46.69,74.625,46.752,74.804,46.781,74.939,46.817,74.974,46.814,75.089,46.814,75.163,46.775,75.201,46.759,75.338,46.73,75.427,46.738,75.826,46.818,75.951,46.795,76.203,46.78,76.298,46.753,76.336,46.729,76.405,46.662,76.45,46.641,76.496,46.636,76.6,46.665,76.668,46.673,76.7,46.667,76.793,46.631,76.859,46.644,77.082,46.625,77.175,46.556,77.226,46.567,77.266,46.611,77.37,46.623,77.602,46.64,77.645,46.656,77.69,46.659,77.86,46.648,78.136,46.595,78.169,46.58,78.297,46.469,78.324,46.482,78.348,46.512,78.359,46.623,78.368,46.643,78.397,46.658,78.433,46.652,78.452,46.618,78.476,46.625,78.504,46.665,78.551,46.707,78.581,46.723,78.63,46.747,78.761,46.724,78.784,46.735,78.787,46.755,78.793,46.78,78.823,46.795,78.868,46.799,78.93,46.788,78.991,46.749]]]},{"n":"Tanganyika","z":1,"b":[29.058,-8.791,31.198,-3.338],"p":[[[30.806,-8.578,30.799,-8.566,30.711,-8.534,30.681,-8.514,30.672,-8.503,30.654,-8.507,30.62,-8.544,30.555,-8.58,30.534,-8.579,30.56,-8.556,30.569,-8.537,30.568,-8.514,30.543,-8.504,30.495,-8.507,30.464,-8.498,30.448,-8.449,30.461,-8.382,30.478,-8.355,30.508,-8.332,30.549,-8.313,30.573,-8.262,30.581,-8.181,30.567,-8.115,30.532,-8.064,30.499,-8.033,30.468,-8.021,30.442,-7.997,30.396,-7.928,30.367,-7.903,30.294,-7.873,30.277,-7.848,30.265,-7.768,30.221,-7.67,30.204,-7.587,30.214,-7.558,30.208,-7.474,30.147,-7.299,30.09,-7.214,30.013,-7.145,29.928,-7.095,29.836,-7.063,29.752,-6.995,29.616,-6.822,29.537,-6.754,29.511,-6.72,29.48,-6.633,29.484,-6.538,29.476,-6.515,29.454,-6.517,29.436,-6.509,29.424,-6.491,29.311,-6.191,29.289,-6.176,29.27,-6.131,29.25,-6.109,29.215,-6.088,29.193,-6.038,29.184,-5.958,29.19,-5.915,29.214,-5.892,29.33,-5.799,29.34,-5.766,29.341,-5.746,29.333,-5.662,29.342,-5.635,29.371,-5.63,29.372,-5.616,29.226,-5.371,29.157,-5.236,29.117,-5.124,29.102,-5.054,29.112,-5.026,29.114,-4.714,29.124,-4.586,29.147,-4.529,29.169,-4.504,29.203,-4.511,29.202,-4.437,29.254,-4.101,29.245,-4.065,29.228,-4.071,29.218,-4.118,29.145,-4.335,29.133,-4.343,29.063,-4.317,29.058,-4.286,29.089,-4.245,29.099,-4.206,29.088,-4.169,29.086,-4.092,29.103,-3.896,29.117,-3.852,29.117,-3.816,29.103,-3.788,29.11,-3.75,29.138,-3.703,29.147,-3.617,29.139,-3.49,29.142,-3.408,29.158,-3.37,29.175,-3.346,29.196,-3.338,29.298,-3.355,29.332,-3.39,29.331,-3.462,29.335,-3.663,29.346,-3.794,29.364,-3.856,29.434,-3.977,29.433,-4.001,29.523,-4.203,29.579,-4.3,29.629,-4.366,29.653,-4.42,29.641,-4.489,29.607,-4.789,29.614,-4.881,29.6,-4.896,29.608,-4.907,29.637,-4.913,29.684,-4.939,29.749,-4.984,29.792,-5.041,29.813,-5.109,29.816,-5.152,29.803,-5.169,29.8,-5.257,29.788,-5.263,29.774,-5.354,29.752,-5.405,29.758,-5.467,29.812,-5.528,29.865,-5.63,29.924,-5.771,29.951,-5.861,29.948,-5.899,29.916,-5.938,29.856,-5.98,29.817,-5.997,29.798,-5.99,29.775,-6.001,29.748,-6.03,29.729,-6.095,29.717,-6.196,29.722,-6.244,29.748,-6.288,29.82,-6.37,29.943,-6.479,29.95,-6.498,29.965,-6.5,30.023,-6.469,30.094,-6.457,30.137,-6.465,30.166,-6.483,30.182,-6.51,30.299,-6.616,30.356,-6.69,30.398,-6.778,30.455,-6.856,30.528,-6.923,30.563,-6.993,30.561,-7.065,30.543,-7.104,30.521,-7.107,30.513,-7.116,30.522,-7.166,30.577,-7.276,30.599,-7.376,30.589,-7.464,30.604,-7.542,30.645,-7.61,30.725,-7.701,30.825,-7.911,30.906,-8.038,30.913,-8.075,30.925,-8.098,30.957,-8.121,30.962,-8.139,30.948,-8.162,30.95,-8.19,30.964,-8.238,31.015,-8.303,31.102,-8.385,31.143,-8.451,31.138,-8.501,31.151,-8.551,31.184,-8.602,31.198,-8.652,31.189,-8.73,31.179,-8.742,31.166,-8.739,31.15,-8.72,31.133,-8.725,31.113,-8.754,31.084,-8.776,31.045,-8.791,31.022,-8.787,30.971,-8.745,30.806,-8.578]]]},{"n":"Lagoa Mirim","z":1.7,"b":[-53.624,-33.557,-52.591,-32.199],"p":[[[-52.598,-32.585,-52.617,-32.638,-52.659,-32.698,-52.694,-32.757,-52.731,-32.812,-52.773,-32.876,-52.808,-32.907,-52.847,-32.917,-52.895,-32.91,-52.939,-32.861,-52.976,-32.822,-53.015,-32.799,-53.068,-32.808,-53.131,-32.846,-53.173,-32.956,-53.228,-33.055,-53.292,-33.081,-53.326,-33.085,-53.352,-33.095,-53.377,-33.12,-53.409,-33.159,-53.437,-33.235,-53.453,-33.351,-53.437,-33.473,-53.454,-33.532,-53.491,-33.547,-53.508,-33.557,-53.527,-33.553,-53.544,-33.539,-53.557,-33.515,-53.567,-33.483,-53.573,-33.433,-53.577,-33.368,-53.586,-33.299,-53.604,-33.242,-53.62,-33.191,-53.624,-33.136,-53.6,-33.079,-53.546,-33.024,-53.473,-32.959,-53.406,-32.954,-53.354,-32.926,-53.323,-32.883,-53.313,-32.834,-53.313,-32.793,-53.302,-32.761,-53.265,-32.733,-53.217,-32.707,-53.178,-32.679,-53.139,-32.649,-53.092,-32.62,-53.03,-32.598,-52.991,-32.534,-52.959,-32.468,-52.877,-32.432,-52.81,-32.391,-52.804,-32.348,-52.792,-32.311,-52.78,-32.263,-52.745,-32.217,-52.72,-32.199,-52.701,-32.204,-52.69,-32.237,-52.69,-32.348,-52.653,-32.394,-52.625,-32.454,-52.593,-32.499,-52.591,-32.54,-52.598,-32.585]]]},{"n":"Manitoba","z":2,"b":[-99.549,50.202,-97.951,51.781],"p":[[[-97.959,50.357,-98.016,50.304,-98.072,50.274,-98.151,50.24,-98.242,50.214,-98.33,50.202,-98.402,50.204,-98.455,50.211,-98.519,50.242,-98.551,50.287,-98.549,50.421,-98.568,50.488,-98.608,50.568,-98.716,50.737,-98.815,50.864,-98.853,50.946,-98.941,51.008,-98.991,51.076,-99.006,51.114,-99.008,51.14,-98.988,51.152,-98.941,51.115,-98.881,51.037,-98.867,51.028,-98.847,51.039,-98.829,51.072,-98.836,51.1,-98.856,51.109,-98.897,51.188,-98.953,51.202,-98.979,51.223,-99.026,51.267,-99.077,51.339,-99.138,51.503,-99.163,51.543,-99.258,51.563,-99.32,51.617,-99.37,51.635,-99.426,51.627,-99.527,51.546,-99.542,51.578,-99.538,51.66,-99.549,51.699,-99.545,51.714,-99.491,51.758,-99.443,51.777,-99.39,51.781,-99.338,51.759,-99.292,51.716,-99.239,51.646,-99.217,51.636,-99.181,51.653,-99.166,51.675,-99.177,51.762,-99.166,51.777,-99.105,51.736,-99.064,51.686,-98.999,51.511,-98.956,51.433,-98.918,51.395,-98.907,51.404,-98.901,51.428,-98.911,51.513,-98.972,51.675,-98.929,51.702,-98.894,51.709,-98.865,51.707,-98.833,51.684,-98.79,51.639,-98.758,51.588,-98.752,51.552,-98.779,51.466,-98.778,51.41,-98.754,51.363,-98.705,51.326,-98.591,51.269,-98.572,51.243,-98.583,51.218,-98.633,51.165,-98.669,51.104,-98.718,51.088,-98.724,51.059,-98.754,51.016,-98.76,50.995,-98.74,50.935,-98.72,50.912,-98.5,50.86,-98.41,50.823,-98.244,50.719,-98.174,50.66,-98.071,50.552,-97.971,50.47,-97.951,50.42,-97.959,50.357]]]},{"n":"Winnipegosis","z":2,"b":[-101.102,51.59,-99.62,53.171],"p":[[[-99.674,51.613,-99.775,51.59,-99.858,51.643,-99.935,51.717,-99.95,51.763,-99.899,51.841,-99.953,51.999,-99.984,52.04,-100,52.044,-100.005,52.016,-99.984,51.812,-99.999,51.744,-100.011,51.731,-100.025,51.727,-100.049,51.756,-100.113,51.891,-100.159,52.116,-100.165,52.254,-100.156,52.306,-100.136,52.351,-100.027,52.464,-99.992,52.567,-100.045,52.649,-100.062,52.763,-100.096,52.796,-100.241,52.865,-100.268,52.872,-100.292,52.859,-100.326,52.833,-100.343,52.807,-100.326,52.794,-100.279,52.782,-100.242,52.64,-100.261,52.621,-100.275,52.623,-100.301,52.651,-100.339,52.672,-100.358,52.713,-100.418,52.773,-100.44,52.924,-100.453,52.953,-100.523,52.967,-100.564,52.996,-100.6,52.998,-100.665,52.98,-100.667,52.959,-100.621,52.886,-100.678,52.778,-100.746,52.752,-100.823,52.753,-100.839,52.767,-100.825,52.799,-100.833,52.813,-100.915,52.814,-100.941,52.845,-100.993,52.88,-100.984,52.931,-100.994,52.987,-101.007,53.008,-101.102,53.081,-101.091,53.1,-101.051,53.124,-100.964,53.147,-100.895,53.098,-100.82,53.091,-100.726,53.098,-100.644,53.115,-100.59,53.138,-100.543,53.17,-100.515,53.171,-100.335,53.107,-100.105,53.054,-99.844,52.975,-99.79,52.945,-99.757,52.913,-99.699,52.915,-99.685,52.91,-99.678,52.9,-99.705,52.812,-99.662,52.61,-99.661,52.539,-99.675,52.473,-99.713,52.407,-99.839,52.243,-99.827,52.152,-99.809,52.095,-99.749,52.028,-99.744,52.005,-99.744,51.973,-99.756,51.939,-99.798,51.876,-99.803,51.83,-99.781,51.761,-99.746,51.752,-99.666,51.854,-99.647,51.842,-99.642,51.72,-99.62,51.651,-99.624,51.629,-99.645,51.617,-99.674,51.613]]]},{"n":"Manicouagan","z":2,"b":[-69.25,50.716,-68.197,51.883],"p":[[[-68.255,51.399,-68.217,51.345,-68.204,51.306,-68.223,51.265,-68.313,51.192,-68.352,51.149,-68.447,51.12,-68.529,51.075,-68.642,50.991,-68.74,50.716,-68.744,50.776,-68.735,50.842,-68.743,50.922,-68.782,51.004,-68.868,51.094,-68.995,51.18,-69.148,51.274,-69.176,51.324,-69.161,51.391,-69.134,51.518,-69.155,51.58,-69.25,51.682,-69.232,51.7,-69.202,51.684,-69.138,51.656,-69.118,51.661,-69.099,51.709,-69.092,51.813,-69.063,51.838,-69.043,51.675,-69.01,51.633,-68.979,51.631,-68.935,51.648,-68.856,51.721,-68.808,51.683,-68.768,51.684,-68.7,51.691,-68.636,51.71,-68.596,51.756,-68.535,51.755,-68.501,51.811,-68.457,51.883,-68.437,51.874,-68.429,51.859,-68.443,51.818,-68.483,51.702,-68.461,51.672,-68.319,51.642,-68.252,51.686,-68.206,51.707,-68.197,51.692,-68.269,51.615,-68.267,51.572,-68.291,51.537,-68.286,51.507,-68.251,51.465,-68.255,51.399],[-68.538,51.393,-68.58,51.457,-68.562,51.48,-68.539,51.473,-68.473,51.402,-68.436,51.376,-68.391,51.368,-68.407,51.442,-68.396,51.503,-68.405,51.532,-68.486,51.591,-68.605,51.631,-68.721,51.632,-68.783,51.62,-68.844,51.6,-68.959,51.536,-69.054,51.472,-69.101,51.41,-69.092,51.368,-69.063,51.332,-69.046,51.302,-69.015,51.28,-68.924,51.242,-68.865,51.198,-68.758,51.165,-68.683,51.156,-68.596,51.168,-68.537,51.2,-68.468,51.214,-68.427,51.232,-68.4,51.256,-68.403,51.285,-68.492,51.349,-68.538,51.393]]]},{"n":"Syvash","z":2,"b":[33.696,45.255,35.439,46.267],"p":[[[35.399,45.255,35.276,45.302,35.089,45.358,35.062,45.379,35.043,45.402,35.04,45.429,35.08,45.499,35.052,45.565,35.024,45.604,35,45.631,34.882,45.687,34.834,45.725,34.762,45.743,34.743,45.786,34.683,45.804,34.676,45.819,34.645,45.837,34.495,45.785,34.495,45.803,34.618,45.915,34.619,45.935,34.608,45.944,34.568,45.952,34.525,45.907,34.497,45.899,34.46,45.917,34.412,45.89,34.39,45.888,34.38,45.901,34.388,45.95,34.382,45.979,34.363,46,34.271,46.027,34.227,46.021,34.19,46.03,34.182,46.016,34.187,45.977,34.187,45.956,34.164,45.944,34.14,45.963,34.133,46.01,34.121,46.026,34.072,46.059,33.998,46.072,33.978,46.052,33.96,46.04,33.924,46.039,33.885,46.06,33.831,46.143,33.815,46.142,33.771,46.133,33.708,46.182,33.696,46.2,33.704,46.215,33.728,46.228,33.778,46.234,33.848,46.231,33.922,46.213,34.03,46.157,34.078,46.142,34.107,46.144,34.103,46.182,34.112,46.193,34.13,46.19,34.147,46.191,34.164,46.203,34.158,46.248,34.159,46.267,34.175,46.266,34.236,46.214,34.27,46.191,34.298,46.181,34.312,46.18,34.329,46.19,34.544,46.161,34.555,46.138,34.548,46.115,34.492,46.113,34.469,46.101,34.429,46.022,34.443,45.999,34.479,45.983,34.523,45.988,34.588,46.021,34.64,46.153,34.667,46.173,34.716,46.165,34.754,46.131,34.755,46.113,34.718,46.09,34.713,46.053,34.74,46.035,34.789,46.024,34.816,45.959,34.84,45.843,34.909,45.793,35.032,45.638,35.182,45.472,35.3,45.364,35.426,45.295,35.439,45.275,35.399,45.255]]]},{"n":"Saimaa","z":2,"b":[26.899,61.068,29.982,63.584],"p":[[[29.518,62.123,29.492,62.089,29.47,62.08,29.492,62.046,29.552,62.003,29.615,61.983,29.638,62.005,29.685,62.004,29.757,61.949,29.775,61.906,29.777,61.889,29.733,61.874,29.727,61.848,29.694,61.834,29.66,61.826,29.61,61.802,29.531,61.778,29.477,61.742,29.426,61.734,29.29,61.649,29.266,61.615,29.115,61.6,29.098,61.578,28.941,61.626,28.73,61.671,28.629,61.642,28.608,61.617,28.76,61.61,28.79,61.59,28.722,61.581,28.68,61.558,28.679,61.524,28.693,61.484,28.646,61.476,28.582,61.473,28.568,61.441,28.464,61.399,28.406,61.37,28.345,61.349,28.36,61.331,28.477,61.332,28.641,61.352,28.663,61.368,28.714,61.352,28.82,61.297,28.867,61.247,28.788,61.204,28.61,61.162,28.359,61.093,28.292,61.08,27.988,61.08,27.947,61.068,27.919,61.08,27.892,61.114,27.882,61.157,27.966,61.188,28.082,61.179,28.172,61.153,28.197,61.163,28.174,61.188,28.072,61.233,27.862,61.252,27.678,61.232,27.625,61.214,27.519,61.23,27.406,61.259,27.432,61.27,27.435,61.296,27.368,61.326,27.303,61.329,27.27,61.324,27.265,61.379,27.301,61.433,27.274,61.468,27.298,61.484,27.296,61.557,27.276,61.653,27.329,61.693,27.327,61.632,27.339,61.597,27.359,61.531,27.459,61.523,27.476,61.539,27.472,61.55,27.471,61.599,27.488,61.618,27.499,61.6,27.515,61.577,27.593,61.581,27.665,61.607,27.678,61.636,27.758,61.656,27.773,61.685,27.705,61.731,27.623,61.764,27.605,61.781,27.626,61.801,27.816,61.728,27.866,61.696,27.918,61.657,27.921,61.627,27.946,61.601,28,61.549,28.054,61.511,28.104,61.522,28.129,61.549,28.147,61.551,28.185,61.544,28.234,61.621,28.248,61.716,28.262,61.744,28.306,61.756,28.339,61.749,28.369,61.751,28.442,61.768,28.539,61.841,28.672,61.826,28.807,61.828,28.84,61.846,28.838,61.863,28.737,61.891,28.697,61.921,28.579,61.993,28.427,62.052,28.403,62.069,28.297,62.097,28.168,62.096,28.086,62.1,28.032,62.108,27.926,62.133,27.899,62.157,27.902,62.244,27.885,62.306,27.877,62.329,27.852,62.354,27.844,62.376,27.8,62.429,27.755,62.429,27.679,62.412,27.597,62.418,27.547,62.412,27.489,62.443,27.459,62.479,27.419,62.496,27.367,62.482,27.297,62.477,27.236,62.507,27.178,62.553,27.159,62.577,27.191,62.572,27.295,62.518,27.339,62.519,27.352,62.544,27.373,62.562,27.398,62.576,27.6,62.494,27.647,62.498,27.67,62.513,27.669,62.528,27.608,62.601,27.619,62.71,27.648,62.735,27.666,62.76,27.65,62.816,27.667,62.874,27.684,62.912,27.652,62.925,27.621,62.916,27.588,62.9,27.564,62.877,27.56,62.851,27.494,62.863,27.415,62.902,27.391,62.941,27.33,63.001,27.176,63.084,27.195,63.099,27.207,63.122,27.193,63.169,27.197,63.229,27.193,63.289,27.213,63.309,27.246,63.308,27.239,63.332,27.19,63.388,27.175,63.438,27.135,63.496,27.068,63.523,27.038,63.517,27.02,63.501,26.959,63.524,26.899,63.566,26.899,63.584,27.113,63.546,27.183,63.518,27.234,63.457,27.234,63.427,27.211,63.417,27.25,63.376,27.297,63.356,27.338,63.346,27.363,63.316,27.347,63.279,27.337,63.213,27.264,63.146,27.358,63.068,27.44,63.023,27.559,63.001,27.605,63.015,27.677,62.979,27.723,62.954,27.77,62.946,27.774,62.996,27.736,63.059,27.767,63.1,27.81,63.092,27.848,63.071,27.851,63.032,27.888,63.019,28.006,63.04,28.106,63.009,28.279,63.031,28.352,63.04,28.35,63.026,28.308,63.003,28.262,62.984,28.192,62.964,28.154,62.933,28.187,62.857,28.144,62.825,28.149,62.804,28.181,62.772,28.211,62.73,28.239,62.706,28.268,62.689,28.264,62.663,28.653,62.527,28.799,62.456,28.898,62.39,28.81,62.382,28.659,62.42,28.529,62.474,28.496,62.501,28.565,62.499,28.555,62.523,28.482,62.555,28.424,62.575,28.367,62.587,28.34,62.577,28.274,62.565,28.211,62.508,28.191,62.448,28.169,62.434,28.103,62.442,28.106,62.476,28.138,62.538,28.057,62.69,27.979,62.732,27.849,62.719,27.766,62.68,27.755,62.652,27.801,62.627,27.758,62.583,27.846,62.524,27.915,62.43,28.04,62.409,28.042,62.379,27.991,62.337,27.963,62.298,27.97,62.259,28.067,62.244,28.144,62.252,28.276,62.256,28.277,62.276,28.324,62.287,28.358,62.27,28.406,62.237,28.506,62.22,28.566,62.222,28.608,62.279,28.63,62.294,28.67,62.306,28.699,62.328,28.668,62.368,28.666,62.387,28.693,62.383,28.765,62.348,28.835,62.267,28.87,62.235,28.852,62.212,28.837,62.185,28.786,62.177,28.826,62.134,28.976,62.142,29.139,62.169,29.198,62.177,29.33,62.166,29.345,62.186,29.353,62.219,29.319,62.242,29.298,62.275,29.298,62.313,29.278,62.349,29.265,62.394,29.297,62.426,29.3,62.476,29.261,62.551,29.279,62.599,29.335,62.6,29.352,62.571,29.364,62.53,29.422,62.523,29.509,62.467,29.626,62.419,29.65,62.427,29.612,62.462,29.589,62.5,29.593,62.527,29.616,62.559,29.633,62.597,29.697,62.609,29.776,62.577,29.825,62.518,29.885,62.463,29.926,62.434,29.835,62.389,29.802,62.362,29.793,62.335,29.768,62.313,29.688,62.316,29.641,62.31,29.62,62.294,29.599,62.27,29.648,62.223,29.727,62.199,29.791,62.2,29.874,62.185,29.96,62.164,29.982,62.133,29.911,62.115,29.708,62.151,29.688,62.138,29.607,62.145,29.518,62.123],[27.849,62.951,27.842,62.911,27.886,62.834,28.011,62.775,28.072,62.746,28.109,62.755,28.109,62.788,28.099,62.852,28.075,62.935,28.062,62.976,27.991,62.978,27.986,62.977,27.903,62.973,27.849,62.951],[27.637,61.273,27.677,61.31,27.659,61.355,27.629,61.383,27.556,61.397,27.509,61.409,27.418,61.437,27.36,61.422,27.328,61.4,27.368,61.369,27.475,61.315,27.588,61.266,27.637,61.273],[27.959,61.48,27.969,61.503,27.946,61.527,27.923,61.526,27.859,61.519,27.794,61.538,27.732,61.547,27.712,61.534,27.7,61.488,27.666,61.483,27.59,61.486,27.548,61.491,27.52,61.485,27.564,61.456,27.662,61.435,27.712,61.403,27.75,61.401,27.798,61.405,27.914,61.442,27.959,61.48],[28.496,61.478,28.445,61.516,28.459,61.558,28.493,61.602,28.536,61.622,28.515,61.699,28.476,61.723,28.388,61.712,28.328,61.73,28.277,61.71,28.281,61.665,28.286,61.623,28.308,61.582,28.243,61.539,28.24,61.523,28.255,61.497,28.245,61.473,28.226,61.443,28.24,61.394,28.307,61.408,28.416,61.416,28.495,61.444,28.496,61.478],[29.477,61.967,29.442,62.038,29.363,62.105,29.322,62.13,29.232,62.126,29.187,62.122,29.102,62.118,29.019,62.125,28.939,62.105,28.809,62.082,28.703,62.082,28.691,62.036,28.714,61.995,28.828,61.963,28.944,61.873,28.99,61.802,29.137,61.775,29.255,61.807,29.309,61.87,29.327,61.944,29.342,61.971,29.403,61.975,29.445,61.948,29.474,61.951,29.477,61.967]]]},{"n":"Zaysan","z":3,"b":[83.027,47.699,84.516,49.726],"p":[[[84.516,47.93,84.427,47.857,84.394,47.816,84.357,47.732,84.296,47.699,84.242,47.706,83.994,47.843,83.766,47.951,83.625,48.013,83.445,48.049,83.259,48.095,83.083,48.178,83.027,48.212,83.039,48.25,83.182,48.249,83.275,48.261,83.435,48.27,83.498,48.305,83.533,48.375,83.461,48.432,83.392,48.5,83.387,48.554,83.417,48.597,83.423,48.743,83.471,48.858,83.589,48.962,83.747,49.039,83.872,49.063,84.036,49.111,84.121,49.159,84.1,49.238,83.972,49.329,83.849,49.417,83.724,49.44,83.66,49.443,83.608,49.486,83.539,49.532,83.479,49.538,83.426,49.573,83.365,49.626,83.364,49.644,83.391,49.652,83.608,49.606,83.74,49.631,83.89,49.72,83.964,49.726,83.961,49.703,83.818,49.634,83.797,49.597,83.757,49.564,83.729,49.535,83.823,49.484,84.288,49.238,84.361,49.161,84.329,49.125,84.149,49.086,83.864,48.994,83.777,48.975,83.609,48.91,83.514,48.813,83.487,48.718,83.517,48.617,83.613,48.437,83.774,48.41,83.919,48.397,83.907,48.374,83.682,48.353,83.596,48.303,83.581,48.267,83.634,48.242,83.74,48.198,83.881,48.152,84.051,48.083,84.167,48.056,84.334,48.023,84.468,47.969,84.516,47.93]]]},{"n":"Churchill","z":3,"b":[-109.158,55.552,-108.074,56.813],"p":[[[-108.561,56.51,-108.634,56.488,-108.692,56.49,-108.715,56.521,-108.721,56.611,-108.736,56.668,-108.787,56.69,-108.859,56.709,-108.87,56.748,-108.837,56.784,-108.765,56.813,-108.703,56.792,-108.634,56.715,-108.571,56.696,-108.53,56.704,-108.495,56.693,-108.425,56.629,-108.412,56.464,-108.324,56.441,-108.218,56.435,-108.134,56.426,-108.074,56.382,-108.111,56.308,-108.189,56.271,-108.191,56.253,-108.174,56.234,-108.179,56.143,-108.122,56.063,-108.135,56.038,-108.178,56.015,-108.219,55.966,-108.247,55.915,-108.3,55.858,-108.486,55.765,-108.572,55.698,-108.605,55.648,-108.598,55.585,-108.627,55.552,-108.665,55.569,-108.687,55.628,-108.678,55.695,-108.688,55.749,-108.716,55.842,-108.759,55.903,-108.918,55.933,-109.034,55.93,-109.1,55.981,-109.158,56.068,-109.137,56.108,-109.086,56.122,-108.875,56.08,-108.671,55.979,-108.618,55.887,-108.6,55.848,-108.52,55.879,-108.467,55.936,-108.467,56.033,-108.433,56.104,-108.352,56.156,-108.273,56.191,-108.268,56.244,-108.315,56.29,-108.345,56.284,-108.376,56.285,-108.489,56.329,-108.539,56.366,-108.534,56.428,-108.53,56.479,-108.561,56.51]]]},{"n":"General Carrera/Buenos Aires","z":3,"b":[-72.904,-47.011,-71.166,-46.297],"p":[[[-71.166,-46.527,-71.207,-46.563,-71.376,-46.575,-71.628,-46.538,-71.782,-46.499,-71.998,-46.521,-72.258,-46.586,-72.554,-46.744,-72.774,-46.873,-72.815,-46.909,-72.826,-46.951,-72.86,-46.997,-72.904,-47.011,-72.87,-46.911,-72.789,-46.785,-72.707,-46.727,-72.661,-46.664,-72.695,-46.592,-72.724,-46.512,-72.692,-46.499,-72.645,-46.564,-72.585,-46.605,-72.478,-46.596,-72.237,-46.513,-71.988,-46.437,-71.872,-46.382,-71.88,-46.341,-71.872,-46.324,-71.847,-46.316,-71.755,-46.33,-71.626,-46.297,-71.605,-46.313,-71.578,-46.361,-71.442,-46.408,-71.313,-46.436,-71.221,-46.482,-71.166,-46.527]]]},{"n":"Päijänne","z":3,"b":[25.188,60.961,27.092,63.216],"p":[[[26.95,62.852,26.986,62.858,27.092,62.842,27.084,62.833,27.018,62.832,27.018,62.8,26.957,62.794,26.88,62.796,26.916,62.744,26.983,62.686,26.952,62.652,26.999,62.613,26.984,62.603,26.896,62.624,26.853,62.606,26.819,62.564,26.825,62.526,26.851,62.497,26.886,62.495,26.93,62.482,26.909,62.456,26.778,62.479,26.736,62.494,26.724,62.553,26.69,62.596,26.662,62.583,26.665,62.551,26.629,62.479,26.58,62.412,26.5,62.391,26.426,62.366,26.432,62.329,26.42,62.318,26.379,62.346,26.384,62.381,26.407,62.418,26.465,62.427,26.508,62.424,26.51,62.453,26.558,62.49,26.526,62.539,26.383,62.567,26.273,62.551,26.239,62.522,26.238,62.481,26.254,62.437,26.288,62.407,26.312,62.368,26.277,62.34,26.173,62.358,26.128,62.329,26.128,62.28,26.089,62.295,26.05,62.348,25.992,62.373,25.956,62.356,25.979,62.311,26.002,62.277,25.974,62.254,25.946,62.21,25.951,62.145,25.923,62.149,25.898,62.2,25.876,62.206,25.826,62.148,25.821,62.072,25.878,62.021,25.858,62.001,25.79,61.992,25.774,61.967,25.703,61.964,25.641,61.95,25.632,61.916,25.651,61.903,25.701,61.885,25.682,61.869,25.615,61.842,25.588,61.793,25.622,61.774,25.683,61.802,25.721,61.813,25.723,61.778,25.68,61.712,25.682,61.638,25.706,61.601,25.63,61.602,25.586,61.591,25.583,61.541,25.573,61.494,25.641,61.467,25.607,61.442,25.543,61.423,25.557,61.393,25.547,61.335,25.58,61.308,25.759,61.278,25.977,61.255,26.013,61.235,26.027,61.212,26.082,61.196,26.118,61.211,26.106,61.246,26.163,61.31,26.275,61.374,26.278,61.36,26.238,61.317,26.255,61.284,26.257,61.247,26.234,61.242,26.212,61.263,26.173,61.271,26.148,61.256,26.175,61.218,26.197,61.175,26.184,61.148,26.241,61.102,26.216,61.081,26.178,61.059,26.175,61.041,26.15,61.003,26.095,60.964,26.025,60.961,26.071,61.004,26.102,61.101,26.026,61.17,25.961,61.19,25.913,61.184,25.871,61.168,25.788,61.215,25.67,61.273,25.621,61.263,25.645,61.235,25.657,61.206,25.583,61.166,25.531,61.115,25.564,61.08,25.603,61.055,25.592,61.024,25.549,61.027,25.518,61.062,25.48,61.088,25.454,61.073,25.405,61.058,25.356,61.076,25.397,61.116,25.459,61.156,25.5,61.229,25.465,61.28,25.442,61.273,25.431,61.25,25.406,61.251,25.394,61.284,25.356,61.315,25.303,61.334,25.271,61.352,25.255,61.366,25.263,61.388,25.194,61.475,25.188,61.532,25.233,61.546,25.27,61.568,25.338,61.58,25.398,61.584,25.418,61.626,25.403,61.67,25.358,61.697,25.228,61.759,25.209,61.794,25.237,61.796,25.306,61.777,25.389,61.794,25.444,61.844,25.393,61.872,25.314,61.878,25.317,61.902,25.423,61.921,25.51,61.931,25.506,61.95,25.543,61.989,25.625,62.005,25.671,62.059,25.586,62.101,25.498,62.111,25.472,62.142,25.55,62.143,25.668,62.136,25.713,62.162,25.716,62.194,25.735,62.219,25.761,62.235,25.824,62.252,25.881,62.292,25.907,62.33,25.922,62.385,25.897,62.441,25.865,62.443,25.841,62.433,25.825,62.448,25.815,62.491,25.763,62.534,25.69,62.543,25.644,62.589,25.632,62.627,25.781,62.652,25.795,62.705,25.827,62.725,25.894,62.72,25.888,62.756,25.831,62.783,25.818,62.794,25.857,62.825,25.866,62.875,25.934,62.913,25.87,62.987,25.832,63.014,25.737,63.046,25.617,63.071,25.541,63.03,25.522,62.966,25.495,62.957,25.436,62.975,25.368,63.007,25.384,63.028,25.444,63.052,25.449,63.098,25.44,63.128,25.505,63.145,25.589,63.211,25.623,63.216,25.621,63.182,25.681,63.137,25.792,63.092,25.822,63.103,25.837,63.13,25.903,63.111,25.932,63.082,25.991,63.043,26.084,62.993,26.079,62.981,26.015,62.991,26.014,62.972,26.077,62.93,26.22,62.854,26.364,62.768,26.371,62.725,26.302,62.729,26.203,62.781,26.103,62.826,26.067,62.819,26.038,62.796,26.013,62.752,26.007,62.71,25.98,62.706,26.02,62.665,26.029,62.632,25.913,62.648,25.821,62.613,25.834,62.535,25.938,62.462,26.024,62.477,26.056,62.521,26.096,62.519,26.103,62.488,26.069,62.454,26.084,62.421,26.16,62.388,26.185,62.46,26.178,62.55,26.217,62.572,26.342,62.608,26.359,62.633,26.451,62.633,26.469,62.704,26.387,62.789,26.358,62.826,26.359,62.854,26.447,62.797,26.502,62.75,26.563,62.706,26.69,62.637,26.785,62.609,26.813,62.623,26.827,62.659,26.822,62.697,26.75,62.742,26.689,62.787,26.671,62.817,26.595,62.872,26.568,62.9,26.59,62.901,26.68,62.846,26.73,62.836,26.743,62.866,26.713,62.929,26.729,62.957,26.762,62.959,26.765,62.98,26.917,63.007,26.996,63.031,27.033,63.019,26.978,62.998,26.931,62.941,26.948,62.916,26.93,62.913,26.894,62.917,26.889,62.895,26.927,62.859,26.95,62.852]]]},{"n":"Srednee Kuyto","z":3,"b":[30.362,64.864,31.955,65.183],"p":[[[30.362,65.149,30.397,65.159,30.467,65.124,30.646,65.074,30.804,65.105,30.903,65.122,30.93,65.157,30.974,65.183,31.346,65.181,31.576,65.137,31.616,65.112,31.628,65.065,31.661,65.032,31.829,64.96,31.942,64.897,31.955,64.864,31.905,64.88,31.752,64.935,31.664,64.945,31.545,65.003,31.51,65.04,31.51,65.067,31.482,65.096,31.373,65.116,31.246,65.117,31.191,65.102,31.181,65.078,31.129,65.078,31.019,65.092,30.986,65.082,30.906,65.074,30.839,65.043,30.814,64.982,30.743,64.944,30.661,64.94,30.54,64.931,30.556,64.95,30.606,64.977,30.721,64.995,30.709,65.021,30.533,65.063,30.404,65.112,30.362,65.149]]]},{"n":"Kakhovka","z":4,"b":[33.54,46.822,35.302,47.842],"p":[[[35.302,47.492,35.269,47.45,35.213,47.45,35.049,47.428,34.961,47.406,34.808,47.463,34.592,47.52,34.419,47.487,34.19,47.42,34.177,47.389,34.183,47.346,34.173,47.325,34.097,47.316,33.904,47.159,33.718,46.941,33.7,46.899,33.687,46.861,33.559,46.822,33.54,46.831,33.576,46.883,33.63,47.034,33.655,47.05,33.667,47.042,33.705,47.061,33.761,47.119,33.777,47.163,33.835,47.212,33.933,47.282,33.963,47.331,33.965,47.359,33.947,47.451,33.936,47.541,33.984,47.556,34.067,47.558,34.148,47.542,34.211,47.536,34.24,47.575,34.266,47.59,34.316,47.567,34.38,47.558,34.436,47.569,34.487,47.593,34.731,47.578,34.997,47.588,35.088,47.676,35.086,47.824,35.106,47.842,35.13,47.84,35.242,47.647,35.302,47.492]]]},{"n":"","z":4,"b":[-105.063,21.796,-105.042,21.809],"p":[[[-105.042,21.801,-105.042,21.796,-105.063,21.809,-105.042,21.801]]]},{"n":"Kuybyshev","z":4,"b":[48.378,53.27,51.937,55.833],"p":[[[48.387,54.168,48.378,54.231,48.399,54.337,48.405,54.452,48.416,54.597,48.551,54.652,48.745,54.675,48.808,54.733,48.832,54.804,48.864,54.871,48.859,55.012,48.977,55.074,49.159,55.163,49.172,55.293,49.104,55.353,49.048,55.443,49.059,55.483,49.12,55.476,49.189,55.381,49.233,55.338,49.303,55.323,49.358,55.339,49.328,55.379,49.332,55.452,49.329,55.518,49.367,55.544,49.368,55.524,49.371,55.465,49.455,55.404,49.583,55.39,49.788,55.383,50,55.415,50.09,55.463,50.225,55.438,50.379,55.431,50.481,55.46,50.675,55.451,50.834,55.48,51.126,55.501,51.255,55.526,51.331,55.499,51.353,55.496,51.414,55.494,51.469,55.511,51.467,55.59,51.398,55.685,51.414,55.774,51.448,55.812,51.48,55.833,51.472,55.795,51.461,55.701,51.521,55.632,51.635,55.646,51.763,55.72,51.875,55.704,51.937,55.699,51.886,55.671,51.8,55.684,51.748,55.675,51.728,55.643,51.714,55.593,51.615,55.565,51.55,55.558,51.524,55.503,51.41,55.441,51.206,55.489,51.174,55.463,51.198,55.441,51.219,55.4,51.184,55.357,51.13,55.393,51.071,55.415,50.877,55.452,50.764,55.429,50.734,55.407,49.892,55.294,49.811,55.244,49.692,55.191,49.523,55.134,49.461,55.114,49.452,55.074,49.446,55.052,49.396,55.016,49.32,55.014,49.224,55.023,49.136,54.998,49.008,54.97,48.94,54.902,48.969,54.828,49.03,54.809,49.098,54.8,49.081,54.776,48.986,54.728,48.976,54.667,48.973,54.641,48.943,54.617,48.896,54.582,48.909,54.55,48.895,54.528,48.863,54.501,48.864,54.462,48.787,54.447,48.631,54.435,48.53,54.394,48.499,54.334,48.549,54.241,48.659,54.171,48.756,54.15,48.798,54.131,48.809,54.099,48.848,54.045,49.02,54.015,49.275,54.071,49.395,54.12,49.563,54.179,49.727,54.225,49.742,54.191,49.666,54.159,49.537,54.127,49.441,54.038,49.309,53.973,49.218,53.934,49.219,53.878,49.27,53.818,49.32,53.791,49.351,53.77,49.372,53.73,49.369,53.695,49.345,53.698,49.31,53.722,49.279,53.728,49.19,53.807,49.127,53.825,49.091,53.794,49.072,53.713,49.063,53.607,49.087,53.534,49.247,53.502,49.561,53.499,49.882,53.486,49.905,53.457,49.852,53.426,49.495,53.436,49.316,53.402,49.258,53.352,49.23,53.284,49.094,53.27,48.801,53.281,48.759,53.324,48.925,53.309,49.137,53.309,49.206,53.338,49.205,53.368,49.106,53.405,48.992,53.44,49.009,53.46,48.956,53.535,48.865,53.648,48.877,53.754,48.903,53.828,48.883,53.866,48.813,53.939,48.661,54.038,48.567,54.098,48.468,54.124,48.387,54.168]]]},{"n":"Mead","z":4,"b":[-114.816,35.831,-113.633,36.519],"p":[[[-113.636,35.831,-113.67,35.843,-113.726,35.922,-113.795,36.01,-113.895,36.079,-113.952,36.13,-113.989,36.165,-114.039,36.163,-114.11,36.032,-114.208,36.006,-114.294,36.03,-114.321,36.076,-114.373,36.1,-114.406,36.091,-114.451,36.079,-114.504,36.106,-114.64,36.106,-114.714,36.024,-114.741,36.014,-114.769,36.02,-114.812,36.075,-114.816,36.121,-114.653,36.154,-114.512,36.194,-114.432,36.237,-114.413,36.348,-114.387,36.483,-114.364,36.515,-114.336,36.519,-114.335,36.444,-114.359,36.335,-114.37,36.209,-114.295,36.11,-114.226,36.05,-114.159,36.077,-114.097,36.152,-114.03,36.245,-113.996,36.244,-113.924,36.176,-113.858,36.116,-113.777,36.048,-113.699,35.958,-113.633,35.859,-113.636,35.831]]]},{"n":"Powell","z":4,"b":[-111.53,36.919,-110.366,37.906],"p":[[[-110.368,37.898,-110.366,37.871,-110.406,37.81,-110.433,37.711,-110.561,37.582,-110.673,37.45,-110.697,37.393,-110.776,37.31,-110.836,37.225,-110.792,37.193,-110.65,37.208,-110.676,37.175,-110.792,37.17,-110.996,37.114,-111.112,37.076,-111.189,37.023,-111.307,37.016,-111.382,36.977,-111.455,36.919,-111.473,36.932,-111.53,37.029,-111.439,37.059,-111.386,37.055,-111.339,37.062,-111.304,37.099,-111.256,37.146,-111.211,37.119,-111.183,37.136,-111.163,37.165,-111.073,37.139,-110.963,37.151,-110.887,37.264,-110.858,37.335,-110.799,37.352,-110.736,37.431,-110.755,37.526,-110.733,37.535,-110.681,37.531,-110.596,37.592,-110.555,37.631,-110.515,37.673,-110.447,37.769,-110.435,37.871,-110.404,37.906,-110.368,37.898]]]},{"n":"Southern Indian","z":4,"b":[-99.686,56.641,-97.995,57.654],"p":[[[-98.295,57.493,-98.201,57.405,-98.098,57.392,-98.024,57.399,-97.995,57.387,-98.028,57.362,-98.109,57.317,-98.19,57.215,-98.226,57.136,-98.25,57.112,-98.392,57.094,-98.561,57.061,-98.685,56.979,-98.779,56.887,-98.855,56.841,-98.939,56.841,-99.002,56.862,-99.068,56.846,-99.218,56.736,-99.342,56.733,-99.47,56.733,-99.553,56.711,-99.544,56.683,-99.549,56.654,-99.571,56.641,-99.643,56.681,-99.686,56.73,-99.663,56.753,-99.546,56.776,-99.407,56.903,-99.378,56.96,-99.329,56.988,-99.226,56.951,-99.155,56.956,-99.124,57.005,-99.069,57.049,-99.015,57.085,-99.011,57.05,-99.037,56.975,-99.006,56.927,-98.905,56.925,-98.873,56.946,-98.88,57.004,-98.857,57.06,-98.775,57.073,-98.695,57.117,-98.661,57.18,-98.651,57.219,-98.668,57.262,-98.632,57.276,-98.542,57.275,-98.508,57.299,-98.518,57.35,-98.537,57.456,-98.593,57.529,-98.596,57.585,-98.556,57.611,-98.54,57.596,-98.544,57.566,-98.528,57.543,-98.472,57.549,-98.397,57.589,-98.293,57.638,-98.206,57.654,-98.208,57.625,-98.224,57.58,-98.271,57.53,-98.295,57.493]]]},{"n":"Sobradinho","z":4,"b":[-43.16,-11.257,-40.818,-9.004],"p":[[[-40.92,-9.257,-40.818,-9.406,-40.818,-9.438,-40.84,-9.455,-41.097,-9.5,-41.138,-9.577,-41.147,-9.664,-41.188,-9.702,-41.266,-9.71,-41.35,-9.736,-41.464,-9.794,-41.541,-9.801,-41.561,-9.789,-41.649,-9.819,-41.818,-9.82,-41.866,-9.77,-41.935,-9.787,-41.999,-9.833,-42.075,-9.888,-42.171,-10.024,-42.167,-10.081,-42.071,-10.115,-41.986,-10.152,-41.916,-10.237,-41.874,-10.3,-41.917,-10.29,-41.938,-10.253,-41.995,-10.196,-42.119,-10.151,-42.201,-10.145,-42.243,-10.114,-42.278,-10.107,-42.335,-10.098,-42.363,-10.192,-42.252,-10.462,-42.157,-10.668,-42.196,-10.746,-42.24,-10.896,-42.269,-10.931,-42.3,-10.949,-42.336,-11.001,-42.351,-10.999,-42.295,-10.84,-42.237,-10.66,-42.259,-10.548,-42.317,-10.495,-42.37,-10.427,-42.415,-10.415,-42.482,-10.467,-42.538,-10.55,-42.611,-10.675,-42.725,-10.842,-42.734,-10.875,-42.74,-11.044,-42.764,-11.142,-42.812,-11.187,-42.825,-11.251,-42.863,-11.249,-42.902,-11.257,-42.934,-11.244,-42.978,-11.174,-43.017,-11.138,-43.072,-11.109,-43.15,-11.143,-43.16,-11.115,-43.107,-10.996,-43.087,-10.94,-43.051,-10.916,-42.968,-10.832,-42.893,-10.774,-42.924,-10.713,-42.947,-10.69,-42.994,-10.664,-42.991,-10.623,-42.955,-10.594,-42.904,-10.651,-42.839,-10.741,-42.78,-10.744,-42.745,-10.706,-42.748,-10.615,-42.79,-10.561,-42.851,-10.498,-42.848,-10.462,-42.816,-10.462,-42.733,-10.517,-42.668,-10.538,-42.626,-10.509,-42.614,-10.48,-42.679,-10.398,-42.685,-10.354,-42.673,-10.288,-42.642,-10.301,-42.558,-10.352,-42.497,-10.331,-42.494,-10.267,-42.53,-10.212,-42.596,-10.093,-42.791,-9.998,-42.816,-9.962,-42.791,-9.945,-42.753,-9.956,-42.734,-9.916,-42.71,-9.954,-42.691,-9.967,-42.675,-9.955,-42.639,-9.973,-42.606,-9.999,-42.593,-10.031,-42.567,-10.071,-42.547,-10.078,-42.508,-10.069,-42.482,-10.044,-42.519,-9.983,-42.51,-9.948,-42.472,-9.959,-42.419,-9.995,-42.361,-9.985,-42.341,-9.949,-42.333,-9.906,-42.303,-9.872,-42.287,-9.834,-42.305,-9.772,-42.354,-9.708,-42.318,-9.696,-42.221,-9.696,-42.195,-9.613,-42.141,-9.607,-42.09,-9.592,-42.077,-9.546,-42.048,-9.511,-41.998,-9.517,-41.979,-9.573,-41.924,-9.618,-41.74,-9.565,-41.592,-9.591,-41.503,-9.586,-41.457,-9.596,-41.446,-9.591,-41.365,-9.622,-41.328,-9.608,-41.305,-9.591,-41.264,-9.594,-41.224,-9.511,-41.224,-9.432,-41.264,-9.362,-41.245,-9.346,-41.186,-9.366,-41.175,-9.352,-41.191,-9.309,-41.184,-9.283,-41.132,-9.291,-41.064,-9.282,-41.042,-9.242,-41.029,-9.194,-41.047,-9.149,-41.041,-9.125,-41.038,-9.058,-41.027,-9.004,-40.976,-9.018,-40.959,-9.038,-40.935,-9.057,-40.953,-9.174,-40.92,-9.257],[-42.803,-10.961,-42.849,-10.935,-42.839,-10.905,-42.804,-10.87,-42.772,-10.843,-42.783,-10.831,-42.847,-10.846,-42.891,-10.902,-42.991,-10.943,-42.996,-10.961,-42.899,-10.945,-42.879,-10.975,-42.918,-11.038,-42.931,-11.079,-42.91,-11.111,-42.879,-11.148,-42.849,-11.143,-42.807,-11.083,-42.803,-10.961]]]},{"n":"Toba","z":4.7,"b":[98.526,2.344,99.143,2.876],"p":[[[99.143,2.412,99.143,2.397,99.093,2.37,98.999,2.349,98.908,2.344,98.846,2.353,98.818,2.38,98.754,2.467,98.689,2.555,98.711,2.577,98.784,2.515,98.885,2.476,98.927,2.483,98.949,2.504,98.919,2.558,98.826,2.671,98.76,2.729,98.723,2.724,98.69,2.688,98.662,2.644,98.641,2.65,98.581,2.732,98.526,2.824,98.53,2.859,98.572,2.876,98.64,2.873,98.707,2.854,98.792,2.799,98.87,2.731,98.918,2.665,98.979,2.575,99.042,2.483,99.102,2.437,99.143,2.412]]]},{"n":"","z":4.7,"b":[25.891,61.542,26.57,61.841],"p":[[[26.57,61.623,26.567,61.616,26.545,61.609,26.473,61.612,26.346,61.655,26.24,61.718,26.183,61.73,26.121,61.716,26.077,61.704,26.072,61.679,26.085,61.657,26.103,61.648,26.123,61.657,26.144,61.669,26.167,61.653,26.183,61.629,26.172,61.6,26.158,61.565,26.148,61.546,26.114,61.542,26.059,61.555,26.007,61.59,25.958,61.619,25.9,61.647,25.891,61.675,25.931,61.691,25.955,61.717,25.966,61.743,25.953,61.767,25.936,61.786,25.944,61.796,26.032,61.772,26.126,61.767,26.176,61.798,26.219,61.841,26.237,61.841,26.246,61.822,26.265,61.785,26.278,61.751,26.279,61.733,26.295,61.716,26.329,61.714,26.354,61.715,26.406,61.717,26.438,61.735,26.466,61.735,26.507,61.718,26.543,61.688,26.563,61.648,26.57,61.623]]]},{"n":"Pukaki","z":4.7,"b":[170.131,-44.173,170.248,-43.704],"p":[[[170.248,-44.117,170.23,-44.173,170.204,-44.171,170.178,-44.17,170.178,-44.065,170.153,-43.914,170.131,-43.809,170.136,-43.766,170.152,-43.746,170.176,-43.723,170.198,-43.704,170.21,-43.714,170.2,-43.782,170.184,-43.831,170.213,-43.968,170.243,-44.075,170.248,-44.117]]]},{"n":"Ōhau","z":4.7,"b":[169.86,-44.295,169.995,-43.973],"p":[[[169.995,-44.287,169.945,-44.295,169.893,-44.277,169.875,-44.248,169.865,-44.224,169.87,-44.181,169.874,-44.087,169.86,-43.987,169.881,-43.973,169.885,-43.998,169.891,-44.039,169.912,-44.051,169.948,-44.008,169.98,-43.974,169.982,-44.017,169.925,-44.091,169.913,-44.172,169.921,-44.221,169.947,-44.255,169.962,-44.269,169.995,-44.287]]]},{"n":"Abitibi","z":4.7,"b":[-80.18,48.449,-79.226,48.935],"p":[[[-80.18,48.812,-80.153,48.835,-80.123,48.877,-80.105,48.913,-80.055,48.919,-79.952,48.935,-79.889,48.924,-79.865,48.89,-79.836,48.875,-79.836,48.853,-79.857,48.807,-79.855,48.765,-79.832,48.73,-79.77,48.717,-79.701,48.723,-79.654,48.739,-79.617,48.77,-79.576,48.797,-79.499,48.761,-79.413,48.715,-79.324,48.705,-79.259,48.69,-79.248,48.665,-79.257,48.599,-79.243,48.544,-79.233,48.503,-79.226,48.472,-79.247,48.456,-79.283,48.449,-79.313,48.452,-79.311,48.475,-79.312,48.498,-79.311,48.532,-79.295,48.578,-79.297,48.608,-79.319,48.631,-79.373,48.645,-79.439,48.648,-79.478,48.642,-79.483,48.63,-79.485,48.618,-79.511,48.613,-79.602,48.62,-79.701,48.641,-79.753,48.644,-79.804,48.632,-79.867,48.633,-79.916,48.659,-79.972,48.699,-79.981,48.717,-80,48.734,-80.017,48.748,-80.06,48.783,-80.091,48.775,-80.148,48.79,-80.18,48.812]]]},{"n":"Fort Peck","z":4.7,"b":[-108.196,47.39,-106.217,48.027],"p":[[[-106.217,47.585,-106.251,47.55,-106.269,47.548,-106.252,47.6,-106.251,47.644,-106.285,47.697,-106.318,47.755,-106.336,47.785,-106.36,47.813,-106.384,47.848,-106.42,47.876,-106.454,47.879,-106.48,47.858,-106.521,47.828,-106.55,47.785,-106.556,47.75,-106.599,47.753,-106.683,47.739,-106.768,47.724,-106.813,47.716,-106.838,47.694,-106.85,47.649,-106.862,47.631,-106.879,47.649,-106.906,47.672,-106.944,47.667,-106.993,47.644,-107.015,47.645,-107.03,47.666,-107.183,47.685,-107.306,47.685,-107.333,47.685,-107.386,47.654,-107.429,47.635,-107.507,47.624,-107.612,47.623,-107.719,47.563,-107.846,47.468,-107.911,47.39,-107.924,47.407,-107.909,47.471,-107.916,47.509,-107.93,47.542,-107.949,47.558,-107.982,47.569,-108.044,47.58,-108.17,47.584,-108.196,47.602,-108.173,47.613,-108.045,47.614,-107.959,47.601,-107.931,47.588,-107.889,47.538,-107.841,47.526,-107.779,47.57,-107.712,47.613,-107.685,47.646,-107.688,47.671,-107.714,47.688,-107.605,47.676,-107.455,47.662,-107.422,47.68,-107.38,47.716,-107.336,47.728,-107.295,47.722,-107.231,47.724,-107.121,47.725,-106.99,47.716,-106.93,47.725,-106.938,47.746,-106.933,47.764,-106.881,47.778,-106.821,47.783,-106.769,47.788,-106.738,47.806,-106.709,47.822,-106.655,47.841,-106.596,47.86,-106.582,47.879,-106.566,47.907,-106.574,47.936,-106.59,47.958,-106.538,47.983,-106.433,48.027,-106.406,48.021,-106.366,48.015,-106.325,47.985,-106.331,47.95,-106.358,47.925,-106.345,47.879,-106.285,47.837,-106.253,47.815,-106.267,47.793,-106.253,47.769,-106.241,47.739,-106.232,47.691,-106.217,47.628,-106.217,47.585]]]},{"n":"Oahe","z":4.7,"b":[-101.188,44.419,-100.255,46.714],"p":[[[-100.289,45.017,-100.341,45.006,-100.37,44.988,-100.382,44.951,-100.392,44.896,-100.427,44.847,-100.476,44.794,-100.532,44.763,-100.573,44.753,-100.625,44.767,-100.636,44.75,-100.614,44.669,-100.595,44.601,-100.559,44.584,-100.516,44.593,-100.488,44.573,-100.504,44.522,-100.477,44.499,-100.418,44.492,-100.397,44.47,-100.388,44.451,-100.394,44.434,-100.412,44.419,-100.478,44.443,-100.567,44.446,-100.61,44.47,-100.637,44.503,-100.62,44.531,-100.633,44.575,-100.671,44.627,-100.676,44.676,-100.682,44.714,-100.724,44.732,-100.774,44.747,-100.803,44.761,-100.848,44.764,-100.946,44.747,-101.072,44.732,-101.163,44.725,-101.188,44.727,-101.158,44.747,-101.071,44.764,-100.995,44.777,-100.976,44.798,-100.96,44.818,-100.908,44.82,-100.861,44.831,-100.805,44.834,-100.767,44.85,-100.778,44.882,-100.737,44.883,-100.67,44.867,-100.604,44.828,-100.559,44.811,-100.54,44.839,-100.5,44.87,-100.457,44.872,-100.441,44.897,-100.443,44.939,-100.447,44.999,-100.401,45.052,-100.347,45.102,-100.335,45.157,-100.323,45.215,-100.317,45.26,-100.345,45.305,-100.407,45.348,-100.543,45.353,-100.612,45.355,-100.528,45.377,-100.4,45.383,-100.344,45.39,-100.355,45.422,-100.378,45.447,-100.434,45.504,-100.543,45.583,-100.627,45.647,-100.637,45.662,-100.606,45.664,-100.531,45.636,-100.448,45.644,-100.4,45.687,-100.39,45.71,-100.398,45.747,-100.414,45.808,-100.478,45.89,-100.579,46.008,-100.659,46.12,-100.668,46.162,-100.641,46.223,-100.612,46.25,-100.594,46.314,-100.594,46.392,-100.606,46.489,-100.621,46.537,-100.634,46.58,-100.713,46.641,-100.767,46.673,-100.786,46.696,-100.777,46.714,-100.714,46.705,-100.667,46.669,-100.585,46.617,-100.564,46.559,-100.543,46.485,-100.527,46.386,-100.552,46.279,-100.574,46.187,-100.557,46.14,-100.559,46.096,-100.535,46.059,-100.493,45.988,-100.432,45.923,-100.377,45.895,-100.35,45.854,-100.342,45.767,-100.345,45.685,-100.354,45.645,-100.398,45.618,-100.408,45.564,-100.376,45.524,-100.307,45.469,-100.281,45.427,-100.279,45.369,-100.271,45.296,-100.255,45.242,-100.273,45.16,-100.286,45.088,-100.265,45.071,-100.262,45.044,-100.289,45.017]]]},{"n":"Teslin","z":4.7,"b":[-133.245,59.604,-132.089,60.449],"p":[[[-132.095,59.607,-132.125,59.604,-132.159,59.626,-132.213,59.688,-132.316,59.835,-132.399,59.922,-132.61,60.05,-132.834,60.135,-132.958,60.201,-133.012,60.251,-133.187,60.382,-133.245,60.449,-133.138,60.427,-132.98,60.297,-132.798,60.192,-132.702,60.153,-132.589,60.123,-132.522,60.098,-132.461,60.055,-132.364,59.986,-132.323,59.951,-132.277,59.902,-132.23,59.849,-132.225,59.838,-132.214,59.816,-132.202,59.77,-132.123,59.694,-132.089,59.644,-132.095,59.607]]]},{"n":"Arrow Lakes","z":4.7,"b":[-118.156,49.336,-117.907,49.802],"p":[[[-117.907,49.342,-117.957,49.336,-118.063,49.412,-118.096,49.45,-118.139,49.521,-118.156,49.601,-118.138,49.703,-118.128,49.752,-118.092,49.802,-118.084,49.774,-118.109,49.583,-118.087,49.515,-118.048,49.457,-117.976,49.406,-117.911,49.375,-117.907,49.342]]]},{"n":"Nasser","z":4.7,"b":[31.091,21.609,33.007,23.988],"p":[[[32.234,22.674,32.167,22.68,32.127,22.688,32.096,22.683,32.056,22.665,32.02,22.634,31.986,22.6,31.962,22.573,31.953,22.535,31.936,22.481,31.897,22.441,31.855,22.415,31.831,22.381,31.826,22.351,31.791,22.358,31.746,22.363,31.714,22.351,31.694,22.334,31.679,22.327,31.664,22.322,31.645,22.312,31.64,22.297,31.648,22.273,31.625,22.242,31.552,22.209,31.487,22.159,31.441,22.102,31.412,22.057,31.391,21.984,31.355,21.881,31.319,21.812,31.295,21.785,31.257,21.764,31.203,21.709,31.156,21.649,31.124,21.612,31.091,21.609,31.112,21.671,31.169,21.749,31.236,21.848,31.303,21.932,31.336,22.01,31.372,22.103,31.45,22.21,31.593,22.339,31.684,22.43,31.704,22.453,31.74,22.47,31.785,22.483,31.752,22.535,31.679,22.604,31.676,22.626,31.729,22.603,31.794,22.569,31.824,22.565,31.841,22.583,31.864,22.614,31.884,22.639,31.911,22.661,31.965,22.698,32.024,22.734,32.061,22.752,32.088,22.764,32.132,22.781,32.167,22.788,32.191,22.779,32.222,22.769,32.292,22.723,32.337,22.691,32.413,22.694,32.501,22.758,32.526,22.795,32.542,22.829,32.562,22.899,32.582,22.929,32.614,22.968,32.636,23.004,32.667,23.078,32.7,23.151,32.739,23.229,32.788,23.273,32.819,23.278,32.833,23.293,32.843,23.318,32.856,23.342,32.851,23.382,32.853,23.438,32.841,23.483,32.787,23.503,32.743,23.516,32.706,23.493,32.661,23.472,32.622,23.477,32.599,23.496,32.65,23.534,32.707,23.57,32.697,23.587,32.676,23.594,32.661,23.58,32.646,23.567,32.624,23.587,32.655,23.633,32.739,23.624,32.798,23.584,32.834,23.619,32.854,23.677,32.843,23.708,32.848,23.733,32.87,23.76,32.873,23.792,32.863,23.829,32.828,23.873,32.823,23.91,32.879,23.988,32.933,23.941,32.945,23.791,32.986,23.695,32.981,23.655,32.951,23.643,32.921,23.634,32.909,23.61,32.911,23.584,32.924,23.567,32.941,23.563,32.951,23.555,32.988,23.53,33.005,23.509,32.951,23.509,32.922,23.485,32.944,23.43,32.979,23.396,32.99,23.371,32.995,23.337,33.006,23.3,33.007,23.26,33.007,23.237,32.995,23.231,32.957,23.241,32.921,23.249,32.929,23.204,32.911,23.161,32.865,23.156,32.853,23.142,32.855,23.117,32.869,23.088,32.907,23.051,32.944,23.004,32.99,22.955,32.972,22.935,32.859,22.978,32.785,23.039,32.744,23.033,32.739,23.008,32.722,22.972,32.705,22.947,32.69,22.938,32.66,22.922,32.632,22.906,32.618,22.896,32.611,22.869,32.607,22.823,32.586,22.783,32.516,22.725,32.429,22.651,32.368,22.604,32.332,22.581,32.328,22.55,32.297,22.531,32.254,22.536,32.248,22.547,32.27,22.564,32.299,22.592,32.296,22.612,32.279,22.623,32.256,22.642,32.238,22.659,32.234,22.674]]]},{"n":"","z":4.7,"b":[23.773,61.031,24.702,61.721],"p":[[[24.608,61.654,24.558,61.64,24.493,61.614,24.456,61.59,24.507,61.552,24.469,61.528,24.342,61.52,24.265,61.496,24.194,61.457,24.172,61.423,24.169,61.404,24.153,61.376,24.128,61.344,24.158,61.328,24.24,61.315,24.279,61.293,24.245,61.289,24.161,61.28,24.08,61.26,24.024,61.247,24.014,61.231,24.041,61.207,24.092,61.185,24.176,61.183,24.242,61.195,24.296,61.187,24.355,61.162,24.372,61.145,24.354,61.142,24.33,61.139,24.337,61.12,24.396,61.071,24.439,61.031,24.414,61.036,24.352,61.073,24.277,61.093,24.03,61.154,23.875,61.183,23.781,61.214,23.773,61.283,23.803,61.26,23.845,61.228,23.919,61.211,23.952,61.224,23.981,61.256,24.021,61.282,24.035,61.312,24.017,61.369,24.043,61.423,24.171,61.484,24.254,61.529,24.306,61.553,24.358,61.587,24.333,61.607,24.309,61.621,24.321,61.633,24.368,61.629,24.422,61.621,24.454,61.626,24.481,61.647,24.497,61.668,24.491,61.679,24.524,61.687,24.604,61.703,24.691,61.721,24.702,61.702,24.674,61.668,24.659,61.656,24.635,61.656,24.608,61.654]]]},{"n":"Pyhäjärvi","z":4.7,"b":[23.349,61.262,24.728,62.266],"p":[[[23.511,61.419,23.507,61.391,23.528,61.369,23.568,61.348,23.646,61.331,23.707,61.317,23.712,61.304,23.717,61.287,23.725,61.27,23.717,61.262,23.675,61.276,23.572,61.296,23.485,61.331,23.44,61.371,23.38,61.382,23.349,61.39,23.365,61.403,23.416,61.418,23.509,61.451,23.639,61.491,23.709,61.511,23.677,61.531,23.638,61.556,23.627,61.589,23.599,61.623,23.57,61.641,23.603,61.647,23.632,61.652,23.627,61.665,23.603,61.688,23.621,61.712,23.667,61.728,23.667,61.745,23.68,61.777,23.692,61.81,23.703,61.825,23.725,61.836,23.708,61.865,23.751,61.877,23.838,61.87,23.861,61.879,23.859,61.894,23.865,61.911,23.951,61.944,24.032,61.974,24.025,61.987,24.016,62,24.015,62.038,24.003,62.076,23.974,62.09,23.873,62.095,23.738,62.103,23.681,62.116,23.684,62.125,23.673,62.137,23.649,62.166,23.638,62.197,23.645,62.215,23.675,62.227,23.699,62.227,23.691,62.213,23.683,62.191,23.701,62.164,23.75,62.159,23.81,62.155,23.855,62.14,23.885,62.142,23.906,62.154,23.915,62.188,23.928,62.221,23.968,62.233,23.999,62.232,23.979,62.208,23.953,62.178,23.954,62.16,23.98,62.16,24.034,62.154,24.122,62.151,24.181,62.157,24.146,62.143,24.123,62.123,24.117,62.092,24.126,62.057,24.181,62.045,24.22,62.045,24.257,62.08,24.285,62.117,24.301,62.12,24.321,62.115,24.335,62.103,24.319,62.073,24.304,62.042,24.324,62.03,24.375,62.029,24.498,62.027,24.565,62.045,24.524,62.081,24.459,62.099,24.384,62.119,24.343,62.146,24.363,62.156,24.438,62.141,24.533,62.106,24.595,62.09,24.611,62.109,24.599,62.146,24.582,62.18,24.585,62.197,24.601,62.201,24.615,62.205,24.613,62.227,24.611,62.251,24.63,62.263,24.662,62.266,24.674,62.229,24.682,62.194,24.722,62.198,24.728,62.181,24.709,62.134,24.686,62.095,24.63,62.043,24.634,61.986,24.684,61.949,24.706,61.921,24.678,61.929,24.599,61.965,24.549,61.993,24.482,62.001,24.417,61.995,24.417,61.987,24.377,61.99,24.294,61.996,24.181,62.01,24.094,62.02,24.119,61.988,24.105,61.951,24.053,61.927,24.004,61.907,23.962,61.894,23.954,61.88,23.852,61.835,23.758,61.786,23.776,61.756,23.786,61.729,23.767,61.703,23.743,61.673,23.739,61.651,23.82,61.646,23.834,61.636,23.772,61.618,23.784,61.612,23.798,61.599,23.791,61.56,23.776,61.521,23.73,61.495,23.611,61.46,23.511,61.419]]]},{"n":"Volta","z":4.7,"b":[-1.715,6.338,0.32,9.117],"p":[[[0.272,6.675,0.299,6.719,0.317,6.738,0.309,6.698,0.276,6.648,0.23,6.616,0.192,6.607,0.172,6.615,0.16,6.618,0.152,6.611,0.154,6.597,0.151,6.57,0.147,6.538,0.14,6.505,0.133,6.45,0.138,6.366,0.103,6.338,0.081,6.353,0.073,6.404,0.041,6.405,-0.011,6.379,-0.033,6.408,-0.06,6.452,-0.099,6.49,-0.175,6.533,-0.255,6.585,-0.324,6.622,-0.428,6.668,-0.55,6.717,-0.634,6.753,-0.674,6.802,-0.684,6.834,-0.66,6.836,-0.616,6.807,-0.58,6.785,-0.551,6.811,-0.518,6.794,-0.491,6.737,-0.467,6.723,-0.442,6.727,-0.419,6.731,-0.409,6.766,-0.406,6.816,-0.393,6.828,-0.368,6.773,-0.335,6.716,-0.305,6.717,-0.271,6.722,-0.228,6.734,-0.194,6.741,-0.175,6.724,-0.163,6.686,-0.144,6.648,-0.096,6.629,-0.057,6.63,-0.041,6.677,-0.026,6.72,-0.017,6.733,-0.023,6.748,-0.049,6.769,-0.067,6.789,-0.071,6.809,-0.066,6.825,-0.042,6.847,-0.011,6.853,0.012,6.845,0.03,6.85,0.077,6.87,0.112,6.897,0.104,6.91,0.059,6.914,0.042,6.946,0.087,6.974,0.12,6.971,0.123,6.983,0.099,7.004,0.088,7.023,0.093,7.037,0.101,7.053,0.098,7.08,0.086,7.121,0.085,7.147,0.066,7.151,0.045,7.164,0.028,7.176,0.01,7.187,0.007,7.209,0.015,7.211,0.035,7.2,0.063,7.195,0.083,7.194,0.099,7.193,0.118,7.203,0.142,7.222,0.153,7.241,0.16,7.252,0.174,7.252,0.184,7.266,0.184,7.287,0.172,7.304,0.156,7.309,0.135,7.313,0.102,7.343,0.068,7.393,0.05,7.432,0.051,7.449,0.047,7.46,0.046,7.478,-0.002,7.5,-0.063,7.508,-0.058,7.466,-0.066,7.444,-0.107,7.458,-0.128,7.472,-0.136,7.496,-0.157,7.51,-0.207,7.512,-0.242,7.517,-0.232,7.532,-0.208,7.546,-0.169,7.55,-0.134,7.553,-0.105,7.551,-0.071,7.549,-0.027,7.546,0.012,7.543,-0.018,7.592,-0.064,7.641,-0.11,7.645,-0.182,7.647,-0.242,7.639,-0.336,7.618,-0.432,7.594,-0.475,7.589,-0.498,7.597,-0.531,7.576,-0.603,7.554,-0.654,7.551,-0.611,7.567,-0.543,7.6,-0.483,7.625,-0.422,7.649,-0.357,7.664,-0.294,7.667,-0.262,7.684,-0.256,7.706,-0.254,7.72,-0.21,7.714,-0.161,7.721,-0.125,7.74,-0.098,7.746,-0.1,7.755,-0.134,7.767,-0.157,7.779,-0.159,7.794,-0.152,7.813,-0.152,7.841,-0.17,7.89,-0.186,7.927,-0.192,7.955,-0.224,7.977,-0.285,8.015,-0.315,8.04,-0.344,8.038,-0.351,8.002,-0.384,7.98,-0.417,7.972,-0.394,8,-0.379,8.048,-0.415,8.079,-0.446,8.091,-0.498,8.057,-0.527,8.062,-0.526,8.128,-0.543,8.154,-0.561,8.134,-0.591,8.1,-0.624,8.085,-0.643,8.087,-0.643,8.106,-0.693,8.11,-0.756,8.102,-0.761,8.118,-0.748,8.135,-0.721,8.135,-0.655,8.142,-0.604,8.159,-0.6,8.173,-0.621,8.191,-0.676,8.239,-0.724,8.28,-0.754,8.297,-0.795,8.315,-0.801,8.334,-0.784,8.366,-0.838,8.447,-0.912,8.555,-0.965,8.604,-0.999,8.63,-1.041,8.646,-1.087,8.64,-1.128,8.632,-1.141,8.595,-1.153,8.551,-1.18,8.604,-1.219,8.658,-1.254,8.668,-1.304,8.697,-1.351,8.724,-1.405,8.739,-1.443,8.731,-1.488,8.697,-1.545,8.664,-1.605,8.644,-1.664,8.629,-1.715,8.63,-1.663,8.673,-1.591,8.695,-1.519,8.731,-1.466,8.761,-1.412,8.781,-1.376,8.805,-1.355,8.792,-1.294,8.75,-1.269,8.726,-1.126,8.705,-1.062,8.722,-1.067,8.75,-1.096,8.784,-1.134,8.813,-1.145,8.874,-1.174,8.945,-1.179,8.979,-1.178,9.027,-1.174,9.074,-1.152,9.117,-1.13,9.076,-1.127,9.028,-1.125,8.981,-1.121,8.929,-1.106,8.897,-1.075,8.853,-1.045,8.843,-1.026,8.844,-0.997,8.775,-0.931,8.653,-0.873,8.586,-0.843,8.579,-0.817,8.6,-0.818,8.65,-0.823,8.707,-0.809,8.743,-0.791,8.761,-0.782,8.756,-0.795,8.721,-0.806,8.689,-0.77,8.656,-0.722,8.639,-0.705,8.643,-0.707,8.625,-0.757,8.578,-0.805,8.52,-0.797,8.497,-0.782,8.469,-0.721,8.402,-0.66,8.357,-0.627,8.329,-0.594,8.288,-0.566,8.275,-0.554,8.251,-0.527,8.232,-0.485,8.209,-0.449,8.209,-0.394,8.221,-0.355,8.243,-0.318,8.244,-0.286,8.253,-0.245,8.245,-0.202,8.213,-0.193,8.186,-0.257,8.185,-0.321,8.177,-0.33,8.158,-0.308,8.142,-0.263,8.108,-0.19,8.053,-0.128,8.011,-0.103,7.976,-0.082,7.876,-0.052,7.811,-0.024,7.793,0.002,7.793,0.019,7.789,0.055,7.792,0.07,7.821,0.058,7.863,0.056,7.886,0.059,7.92,0.073,7.969,0.079,8.003,0.062,8.041,0.07,8.081,0.099,8.093,0.129,8.086,0.154,8.078,0.17,8.086,0.165,8.115,0.152,8.161,0.123,8.21,0.105,8.242,0.137,8.254,0.169,8.289,0.198,8.338,0.217,8.363,0.161,8.381,0.092,8.403,0.081,8.429,0.093,8.461,0.091,8.5,0.085,8.548,0.106,8.583,0.135,8.598,0.15,8.633,0.16,8.7,0.184,8.742,0.198,8.738,0.185,8.69,0.168,8.625,0.152,8.592,0.128,8.574,0.121,8.518,0.113,8.457,0.103,8.434,0.115,8.416,0.178,8.399,0.242,8.379,0.261,8.355,0.28,8.341,0.294,8.334,0.284,8.327,0.242,8.322,0.214,8.314,0.217,8.296,0.19,8.251,0.184,8.206,0.201,8.166,0.231,8.125,0.268,8.106,0.267,8.094,0.24,8.088,0.198,8.079,0.161,8.06,0.146,8.021,0.121,7.972,0.108,7.939,0.124,7.913,0.146,7.894,0.168,7.863,0.221,7.883,0.258,7.905,0.251,7.876,0.252,7.863,0.248,7.849,0.237,7.834,0.248,7.803,0.278,7.762,0.279,7.699,0.281,7.617,0.282,7.575,0.259,7.581,0.24,7.578,0.228,7.556,0.229,7.536,0.242,7.511,0.25,7.491,0.285,7.515,0.32,7.542,0.313,7.506,0.302,7.47,0.297,7.453,0.295,7.416,0.287,7.382,0.276,7.353,0.277,7.339,0.277,7.33,0.292,7.303,0.294,7.278,0.276,7.258,0.272,7.234,0.279,7.218,0.281,7.201,0.279,7.182,0.276,7.172,0.271,7.138,0.289,7.106,0.305,7.099,0.3,7.071,0.281,7.047,0.273,7.001,0.263,6.948,0.24,6.896,0.237,6.768,0.254,6.673,0.272,6.675],[0.13,6.679,0.181,6.726,0.159,6.767,0.115,6.796,0.062,6.808,0.017,6.779,0.011,6.718,0.049,6.695,0.09,6.659,0.13,6.679]]]},{"n":"Bratsk","z":5,"b":[101.275,53.107,103.743,56.349],"p":[[[103.533,53.636,103.725,53.602,103.737,53.554,103.743,53.496,103.726,53.484,103.686,53.512,103.609,53.552,103.49,53.563,103.44,53.519,103.425,53.473,103.44,53.223,103.374,53.107,103.343,53.228,103.343,53.572,103.275,53.677,103.096,53.742,102.868,53.79,102.927,53.82,103.117,53.774,103.208,53.806,103.119,53.938,103.003,54.068,102.987,54.156,103.017,54.283,103.106,54.415,103.173,54.424,103.218,54.487,103.186,54.627,103.089,54.713,103.033,54.782,103.07,54.921,103.226,55.046,103.282,55.159,103.272,55.302,103.179,55.486,103.149,55.578,103.139,55.685,103.074,55.789,102.99,55.854,102.961,55.958,102.921,56.063,102.852,56.104,102.797,56.108,102.717,56.143,102.593,56.156,102.53,56.158,102.506,56.156,102.494,56.127,102.48,56.108,102.439,56.091,102.393,56.088,102.309,56.11,102.236,56.103,102.18,56.068,101.986,56.026,102,55.961,101.978,55.903,101.906,55.834,101.913,55.783,101.99,55.731,102.085,55.718,102.094,55.696,102.051,55.663,102.102,55.628,102.332,55.59,102.386,55.544,102.366,55.485,102.315,55.436,102.311,55.401,102.327,55.353,102.342,55.32,102.39,55.302,102.43,55.285,102.39,55.262,102.344,55.232,102.32,55.195,102.285,55.143,102.233,55.101,102.186,55.07,102.118,55.053,102.142,55.08,102.184,55.118,102.208,55.146,102.259,55.183,102.274,55.222,102.33,55.271,102.31,55.301,102.284,55.345,102.26,55.409,102.268,55.445,102.307,55.495,102.3,55.53,102.217,55.514,102.072,55.457,101.789,55.414,101.761,55.436,101.757,55.469,101.979,55.523,101.97,55.563,101.91,55.59,101.806,55.654,101.788,55.772,101.741,55.807,101.504,55.889,101.411,55.923,101.322,55.946,101.275,55.982,101.352,55.987,101.483,55.968,101.613,55.969,101.633,55.977,101.706,55.973,101.73,56.014,101.732,56.07,101.699,56.095,101.672,56.135,101.706,56.175,101.697,56.218,101.71,56.264,101.767,56.287,101.803,56.291,101.867,56.236,101.931,56.242,101.964,56.227,101.922,56.167,101.945,56.125,102.054,56.156,102.255,56.227,102.26,56.255,102.462,56.253,102.474,56.311,102.522,56.349,102.582,56.332,102.588,56.298,102.626,56.253,102.727,56.25,102.799,56.262,102.823,56.235,102.872,56.199,102.941,56.197,103.001,56.223,103.023,56.214,103.009,56.144,103.02,56.045,103.063,55.92,103.143,55.808,103.247,55.714,103.249,55.672,103.226,55.558,103.334,55.358,103.364,55.219,103.348,55.132,103.304,55.02,103.185,54.878,103.154,54.801,103.216,54.719,103.279,54.578,103.286,54.482,103.349,54.323,103.425,54.205,103.463,54.16,103.397,54.17,103.318,54.303,103.261,54.342,103.205,54.311,103.144,54.239,103.103,54.147,103.16,54.001,103.298,53.874,103.3,53.841,103.33,53.779,103.435,53.679,103.533,53.636]]]},{"n":"Khantayskoye","z":5,"b":[89.313,68.24,92.331,68.509],"p":[[[91.937,68.489,92.104,68.509,92.222,68.46,92.331,68.426,92.114,68.408,91.898,68.398,91.541,68.397,91.105,68.368,90.92,68.311,90.533,68.287,89.935,68.24,89.807,68.263,89.772,68.305,89.667,68.315,89.556,68.31,89.472,68.313,89.399,68.33,89.313,68.335,89.33,68.351,89.315,68.388,89.523,68.399,90.232,68.369,90.515,68.39,90.658,68.407,90.869,68.405,91.092,68.443,91.483,68.44,91.937,68.489]]]},{"n":"Keta","z":5,"b":[89.547,68.7,91.713,68.862],"p":[[[91.306,68.793,90.843,68.755,90.544,68.71,90.356,68.7,90.05,68.702,90.006,68.707,89.989,68.727,89.547,68.819,89.582,68.848,89.737,68.856,89.999,68.837,90.223,68.787,90.445,68.758,90.684,68.779,90.787,68.799,90.918,68.816,91.192,68.832,91.335,68.831,91.452,68.836,91.713,68.862,91.484,68.787,91.306,68.793]]]},{"n":"Lama","z":5,"b":[88.825,69.323,91.559,69.625],"p":[[[89.469,69.378,89.225,69.349,88.958,69.323,88.825,69.339,88.845,69.371,89.123,69.425,89.346,69.459,89.546,69.495,89.901,69.603,90.205,69.625,90.523,69.577,91.08,69.488,91.436,69.51,91.559,69.418,90.988,69.459,90.66,69.453,90.408,69.519,90.099,69.539,89.761,69.489,89.582,69.444,89.469,69.378]]]},{"n":"Uddjaure","z":5,"b":[17.346,65.852,18.009,66.201],"p":[[[17.967,65.852,17.955,65.864,17.926,65.88,17.882,65.89,17.815,65.866,17.74,65.857,17.693,65.875,17.691,65.905,17.704,65.931,17.736,65.949,17.72,65.979,17.637,66.012,17.591,66.014,17.577,66.005,17.546,66.012,17.517,66.05,17.46,66.066,17.401,66.051,17.36,66.047,17.346,66.08,17.395,66.1,17.471,66.113,17.547,66.108,17.595,66.085,17.672,66.067,17.733,66.083,17.723,66.115,17.615,66.168,17.621,66.193,17.676,66.201,17.777,66.187,17.835,66.155,17.968,66.106,18.009,66.051,17.964,66.048,17.945,65.997,17.949,65.959,17.948,65.934,17.964,65.912,18.001,65.882,17.967,65.852]]]},{"n":"Storavan","z":5,"b":[17.967,65.616,18.509,65.882],"p":[[[18.001,65.882,18.031,65.857,18.14,65.778,18.286,65.776,18.37,65.752,18.381,65.73,18.424,65.7,18.509,65.66,18.378,65.648,18.255,65.677,18.173,65.664,18.151,65.644,18.161,65.621,18.131,65.616,18.093,65.629,18.083,65.649,18.071,65.715,18.036,65.774,17.967,65.852,18.001,65.882]]]},{"n":"","z":5,"b":[13.71,63.754,15.824,64.66],"p":[[[15.755,63.758,15.642,63.808,15.576,63.784,15.542,63.76,15.536,63.798,15.52,63.85,15.303,63.932,15.263,63.961,15.39,63.926,15.417,63.939,15.419,63.959,15.376,63.997,15.316,64.031,15.319,64.11,15.24,64.157,15.125,64.245,15.037,64.285,14.328,64.388,14.192,64.447,14.093,64.519,13.961,64.563,13.71,64.636,13.802,64.66,14.096,64.557,14.195,64.481,14.356,64.42,14.493,64.417,14.664,64.383,14.851,64.347,14.94,64.344,14.991,64.336,15.013,64.334,15.048,64.338,15.111,64.341,15.21,64.322,15.261,64.262,15.322,64.163,15.419,64.075,15.394,64.051,15.395,64.019,15.471,63.967,15.578,63.868,15.745,63.83,15.824,63.754,15.755,63.758]]]},{"n":"","z":5,"b":[13.198,64.671,13.635,64.785],"p":[[[13.29,64.738,13.399,64.785,13.527,64.727,13.635,64.686,13.6,64.675,13.368,64.693,13.234,64.671,13.198,64.684,13.29,64.738]]]},{"n":"Habbaniyah","z":5,"b":[43.304,33.19,43.595,33.435],"p":[[[43.595,33.306,43.566,33.219,43.483,33.19,43.434,33.199,43.402,33.257,43.304,33.309,43.309,33.349,43.382,33.435,43.428,33.39,43.477,33.338,43.554,33.348,43.595,33.306]]]},{"n":"Skadar","z":5,"b":[19.099,42.079,19.481,42.29],"p":[[[19.293,42.265,19.364,42.29,19.392,42.228,19.451,42.157,19.481,42.108,19.467,42.079,19.394,42.088,19.216,42.158,19.099,42.278,19.175,42.277,19.293,42.265]]]},{"n":"Aylmer","z":5,"b":[-109.639,63.945,-107.932,64.413],"p":[[[-107.932,64.036,-108.084,64.045,-108.161,64.038,-108.252,64.022,-108.372,63.983,-108.459,63.945,-108.661,63.988,-108.749,64.036,-108.858,64.065,-109.114,64.076,-109.394,64.02,-109.571,64.018,-109.639,64.051,-109.363,64.066,-109.154,64.104,-109.029,64.103,-108.634,64.17,-108.598,64.195,-108.601,64.227,-108.623,64.276,-108.602,64.331,-108.541,64.388,-108.467,64.413,-108.432,64.398,-108.438,64.314,-108.419,64.267,-108.361,64.278,-108.285,64.317,-108.247,64.316,-108.261,64.277,-108.335,64.209,-108.358,64.181,-108.336,64.156,-108.263,64.142,-108.257,64.16,-108.186,64.177,-108.102,64.137,-107.945,64.094,-107.932,64.036]]]},{"n":"Clinton-Colden","z":5,"b":[-107.945,63.776,-107.088,64.131],"p":[[[-107.945,64.094,-107.837,64.089,-107.752,64.071,-107.675,64.081,-107.589,64.112,-107.464,64.128,-107.323,64.131,-107.238,64.1,-107.233,64.067,-107.24,64.037,-107.24,64.022,-107.269,63.983,-107.222,63.926,-107.104,63.924,-107.088,63.912,-107.106,63.894,-107.2,63.824,-107.274,63.776,-107.502,63.789,-107.529,63.798,-107.557,63.833,-107.632,63.877,-107.614,63.937,-107.622,63.97,-107.668,63.985,-107.789,63.981,-107.805,63.956,-107.836,63.983,-107.932,64.036,-107.945,64.094]]]},{"n":"Francis Case","z":5,"b":[-99.471,43.059,-98.56,43.991],"p":[[[-99.147,43.433,-99.04,43.358,-98.905,43.285,-98.831,43.206,-98.706,43.135,-98.571,43.102,-98.56,43.06,-98.666,43.059,-98.747,43.102,-98.9,43.159,-98.93,43.226,-99.069,43.31,-99.178,43.374,-99.315,43.468,-99.344,43.539,-99.383,43.606,-99.471,43.669,-99.441,43.752,-99.365,43.843,-99.352,43.888,-99.374,43.947,-99.364,43.991,-99.322,43.957,-99.303,43.903,-99.315,43.841,-99.37,43.757,-99.411,43.703,-99.35,43.643,-99.244,43.477,-99.147,43.433]]]},{"n":"Falcon International","z":5,"b":[-99.464,26.562,-99.113,27.057],"p":[[[-99.184,26.76,-99.15,26.751,-99.119,26.749,-99.113,26.727,-99.149,26.652,-99.15,26.592,-99.172,26.564,-99.2,26.562,-99.243,26.584,-99.255,26.67,-99.283,26.76,-99.331,26.803,-99.394,26.795,-99.386,26.807,-99.364,26.821,-99.368,26.886,-99.401,26.954,-99.464,27.025,-99.457,27.057,-99.415,27.044,-99.369,27.002,-99.277,26.921,-99.218,26.91,-99.218,26.805,-99.184,26.76]]]},{"n":"Île-à-la-Crosse","z":5,"b":[-108.044,55.211,-107.666,55.921],"p":[[[-107.789,55.211,-107.843,55.303,-107.963,55.42,-107.999,55.471,-108.044,55.508,-108.036,55.554,-107.985,55.575,-107.85,55.528,-107.806,55.553,-107.851,55.618,-107.857,55.661,-107.805,55.665,-107.769,55.698,-107.767,55.802,-107.766,55.894,-107.737,55.921,-107.666,55.744,-107.706,55.582,-107.74,55.48,-107.74,55.408,-107.792,55.367,-107.769,55.267,-107.789,55.211]]]},{"n":"Wholdaia","z":5,"b":[-104.643,60.341,-103.647,61.026],"p":[[[-103.836,60.554,-103.907,60.505,-103.935,60.477,-103.949,60.421,-104.117,60.355,-104.187,60.341,-104.152,60.425,-104.135,60.533,-104.229,60.587,-104.332,60.633,-104.395,60.685,-104.468,60.703,-104.494,60.726,-104.39,60.809,-104.343,60.871,-104.38,60.901,-104.425,60.885,-104.44,60.852,-104.505,60.818,-104.588,60.813,-104.637,60.835,-104.643,60.856,-104.53,60.86,-104.509,60.883,-104.45,60.973,-104.449,61.001,-104.419,61.022,-104.335,61.026,-104.297,61.005,-104.281,60.981,-104.213,60.938,-104.212,60.903,-104.23,60.846,-104.219,60.814,-104.141,60.826,-104.139,60.805,-104.219,60.757,-104.217,60.738,-104.124,60.752,-104.073,60.781,-104.02,60.793,-103.983,60.785,-103.851,60.824,-103.769,60.866,-103.752,60.909,-103.742,60.945,-103.716,60.952,-103.647,60.902,-103.667,60.835,-103.813,60.785,-103.97,60.707,-104.082,60.693,-104.137,60.668,-104.123,60.624,-104.042,60.602,-103.937,60.614,-103.879,60.641,-103.859,60.677,-103.81,60.71,-103.746,60.701,-103.736,60.642,-103.836,60.554]]]},{"n":"Tehek","z":5,"b":[-96.032,64.762,-95.249,65.042],"p":[[[-95.249,65.003,-95.39,64.972,-95.463,64.921,-95.465,64.859,-95.456,64.814,-95.459,64.789,-95.487,64.766,-95.55,64.762,-95.593,64.782,-95.62,64.806,-95.625,64.826,-95.636,64.846,-95.712,64.874,-95.799,64.923,-95.858,64.915,-95.87,64.871,-95.881,64.851,-95.993,64.891,-96.032,64.923,-96.003,64.974,-95.931,65.003,-95.855,65.011,-95.726,65.022,-95.632,65.039,-95.503,65.042,-95.25,65.034,-95.249,65.003]]]},{"n":"Caniapiscau","z":5,"b":[-70.141,53.946,-69.269,54.379],"p":[[[-69.351,54.083,-69.269,54.047,-69.32,54.025,-69.412,54.036,-69.502,54.019,-69.595,53.998,-69.722,53.961,-69.782,53.947,-69.834,53.946,-69.869,53.955,-69.856,53.977,-69.827,53.999,-69.741,54.024,-69.703,54.038,-69.707,54.069,-69.758,54.078,-69.847,54.101,-69.983,54.139,-70.055,54.134,-70.129,54.11,-70.141,54.129,-70.084,54.178,-70.011,54.223,-69.98,54.237,-69.926,54.252,-69.879,54.261,-69.869,54.289,-69.88,54.311,-69.927,54.32,-69.979,54.318,-70.001,54.325,-70.029,54.337,-70.071,54.337,-70.082,54.343,-70.041,54.372,-69.971,54.365,-69.901,54.379,-69.858,54.368,-69.804,54.312,-69.793,54.283,-69.682,54.268,-69.687,54.24,-69.671,54.225,-69.664,54.21,-69.704,54.172,-69.697,54.153,-69.588,54.107,-69.477,54.092,-69.433,54.111,-69.406,54.121,-69.351,54.083]]]},{"n":"Bermen","z":5,"b":[-69.243,53.428,-68.68,54.042],"p":[[[-68.68,53.798,-68.705,53.775,-68.726,53.689,-68.707,53.61,-68.716,53.583,-68.777,53.577,-68.853,53.594,-68.869,53.576,-68.869,53.523,-68.9,53.483,-68.935,53.469,-68.976,53.456,-69.055,53.428,-69.085,53.444,-69.045,53.492,-68.984,53.559,-68.947,53.609,-68.868,53.645,-68.827,53.676,-68.837,53.717,-68.866,53.737,-68.85,53.755,-68.805,53.765,-68.781,53.775,-68.767,53.796,-68.814,53.846,-68.907,53.876,-68.967,53.908,-69.008,53.94,-69.061,53.958,-69.225,53.949,-69.243,53.961,-69.231,53.979,-69.2,53.989,-69.171,53.99,-69.154,54,-69.16,54.023,-69.146,54.04,-69.116,54.023,-69.062,54.007,-69.022,54.028,-69.005,54.042,-68.979,54.011,-68.949,54,-68.903,53.985,-68.864,53.952,-68.846,53.911,-68.733,53.836,-68.685,53.812,-68.68,53.798]]]},{"n":"Caniapiscau","z":5,"b":[-70.517,54.359,-69.766,54.742],"p":[[[-70.233,54.444,-70.274,54.436,-70.312,54.408,-70.419,54.359,-70.497,54.407,-70.517,54.441,-70.46,54.432,-70.424,54.41,-70.357,54.429,-70.214,54.479,-70.095,54.473,-69.97,54.49,-69.932,54.516,-69.912,54.552,-69.912,54.617,-69.92,54.654,-69.96,54.684,-70.029,54.676,-70.008,54.722,-69.983,54.742,-69.935,54.732,-69.9,54.704,-69.86,54.622,-69.84,54.556,-69.839,54.525,-69.774,54.492,-69.766,54.466,-69.882,54.456,-69.949,54.425,-69.988,54.425,-70.015,54.438,-70.155,54.436,-70.233,54.444]]]},{"n":"Kentucky","z":5,"b":[-88.283,35.945,-87.91,37.022],"p":[[[-88.057,36.669,-88.023,36.528,-88.007,36.427,-87.956,36.303,-87.91,36.187,-87.934,36.074,-87.975,35.945,-88.041,35.949,-87.983,36.099,-87.991,36.234,-88.039,36.307,-88.066,36.314,-88.092,36.26,-88.116,36.293,-88.119,36.355,-88.086,36.447,-88.084,36.555,-88.126,36.655,-88.272,36.951,-88.283,37.022,-88.207,36.971,-88.057,36.669]]]},{"n":"Strom Thurmond","z":5,"b":[-82.589,33.641,-82.188,34.017],"p":[[[-82.411,33.954,-82.381,33.927,-82.327,33.847,-82.247,33.774,-82.188,33.694,-82.208,33.664,-82.332,33.672,-82.468,33.641,-82.575,33.643,-82.541,33.662,-82.439,33.671,-82.275,33.725,-82.308,33.776,-82.419,33.846,-82.491,33.898,-82.521,33.893,-82.555,33.879,-82.579,33.865,-82.553,33.937,-82.577,33.956,-82.589,34.017,-82.546,34.009,-82.485,33.966,-82.441,33.938,-82.411,33.954]]]},{"n":"Kinbasket","z":5,"b":[-119.128,52.06,-118.18,52.739],"p":[[[-118.402,52.172,-118.287,52.159,-118.18,52.178,-118.218,52.129,-118.322,52.103,-118.366,52.09,-118.392,52.069,-118.448,52.06,-118.499,52.069,-118.527,52.102,-118.551,52.241,-118.642,52.332,-118.751,52.404,-118.89,52.549,-119.128,52.739,-119.058,52.727,-118.931,52.618,-118.677,52.411,-118.565,52.303,-118.402,52.172]]]},{"n":"George","z":5,"b":[-81.684,29.198,-81.529,29.505],"p":[[[-81.684,29.368,-81.683,29.505,-81.64,29.414,-81.54,29.287,-81.529,29.224,-81.595,29.198,-81.64,29.258,-81.663,29.314,-81.684,29.368]]]},{"n":"Havasu","z":5,"b":[-114.405,34.242,-114.082,34.54],"p":[[[-114.359,34.438,-114.405,34.488,-114.377,34.54,-114.327,34.478,-114.221,34.421,-114.082,34.318,-114.134,34.242,-114.164,34.253,-114.189,34.318,-114.261,34.368,-114.359,34.438]]]},{"n":"Shasta","z":5,"b":[-122.426,40.721,-122.095,40.871],"p":[[[-122.232,40.871,-122.242,40.806,-122.229,40.793,-122.116,40.8,-122.095,40.783,-122.102,40.766,-122.151,40.746,-122.241,40.754,-122.364,40.724,-122.411,40.721,-122.426,40.744,-122.417,40.768,-122.395,40.829,-122.362,40.82,-122.351,40.784,-122.301,40.804,-122.259,40.861,-122.232,40.871]]]},{"n":"Point","z":5,"b":[-114.442,64.952,-112.217,65.677],"p":[[[-114.098,65.378,-114.185,65.446,-114.442,65.477,-114.405,65.561,-114.381,65.616,-114.397,65.657,-114.229,65.677,-114.173,65.638,-114.183,65.611,-114.298,65.6,-114.287,65.555,-114.222,65.522,-114.169,65.496,-114.077,65.476,-114.001,65.448,-113.937,65.444,-113.93,65.425,-113.91,65.402,-113.84,65.368,-113.621,65.416,-113.517,65.385,-113.401,65.382,-113.271,65.401,-113.113,65.385,-113.125,65.361,-113.224,65.356,-113.316,65.332,-113.312,65.318,-113.272,65.306,-113.125,65.292,-112.871,65.242,-112.678,65.203,-112.55,65.207,-112.462,65.22,-112.427,65.221,-112.294,65.22,-112.217,65.225,-112.222,65.192,-112.307,65.183,-112.346,65.128,-112.29,65.038,-112.242,64.966,-112.288,64.952,-112.318,64.96,-112.35,65.013,-112.417,65.104,-112.457,65.153,-112.528,65.16,-112.65,65.16,-112.822,65.19,-112.884,65.203,-112.939,65.213,-112.997,65.196,-113.063,65.216,-113.324,65.26,-113.507,65.272,-113.618,65.253,-113.787,65.244,-113.95,65.335,-114.055,65.361,-114.098,65.378]]]},{"n":"Upper Klamath","z":5,"b":[-122.06,42.251,-121.806,42.604],"p":[[[-121.941,42.604,-121.948,42.499,-121.921,42.475,-121.828,42.409,-121.806,42.289,-121.809,42.251,-121.842,42.256,-121.922,42.332,-121.921,42.36,-121.957,42.396,-122.042,42.428,-122.06,42.475,-122.017,42.523,-121.965,42.59,-121.941,42.604]]]},{"n":"Harney","z":5,"b":[-119.205,43.16,-119.005,43.308],"p":[[[-119.195,43.229,-119.205,43.256,-119.183,43.282,-119.133,43.308,-119.019,43.259,-119.005,43.239,-119.023,43.197,-119.081,43.16,-119.163,43.184,-119.195,43.229]]]},{"n":"Chiputneticook Lakes","z":5,"b":[-67.863,45.589,-67.449,45.759],"p":[[[-67.708,45.692,-67.6,45.637,-67.533,45.643,-67.48,45.683,-67.461,45.642,-67.449,45.607,-67.512,45.589,-67.625,45.602,-67.779,45.67,-67.863,45.74,-67.787,45.759,-67.708,45.692]]]},{"n":"Tucuruí Dam","z":5,"b":[-49.865,-5.388,-48.937,-3.835],"p":[[[-49.709,-4.422,-49.81,-4.409,-49.826,-4.391,-49.865,-4.321,-49.859,-4.298,-49.815,-4.26,-49.816,-4.194,-49.809,-4.175,-49.753,-4.115,-49.749,-4.058,-49.719,-4.028,-49.716,-3.958,-49.715,-3.855,-49.687,-3.835,-49.626,-3.848,-49.592,-3.958,-49.543,-4.125,-49.524,-4.226,-49.524,-4.257,-49.499,-4.283,-49.448,-4.36,-49.429,-4.454,-49.435,-4.49,-49.35,-4.602,-49.298,-4.679,-49.316,-4.725,-49.283,-4.8,-49.29,-4.836,-49.347,-4.881,-49.338,-4.945,-49.281,-5.165,-49.176,-5.275,-48.962,-5.276,-48.937,-5.303,-48.971,-5.333,-49.17,-5.388,-49.281,-5.301,-49.351,-5.183,-49.415,-5.044,-49.532,-4.846,-49.549,-4.74,-49.592,-4.652,-49.677,-4.617,-49.713,-4.593,-49.675,-4.546,-49.653,-4.484,-49.675,-4.437,-49.709,-4.422]]]},{"n":"Três Marias Dam","z":5,"b":[-45.516,-19.051,-44.903,-18.201],"p":[[[-44.972,-18.782,-44.942,-18.794,-44.932,-18.824,-44.903,-18.841,-44.92,-18.857,-44.967,-18.85,-45.044,-18.828,-45.118,-18.83,-45.129,-18.849,-45.121,-18.887,-45.077,-19.005,-45.089,-19.051,-45.112,-19.029,-45.156,-18.948,-45.199,-18.957,-45.217,-18.962,-45.31,-18.974,-45.397,-19.007,-45.396,-18.939,-45.386,-18.923,-45.251,-18.909,-45.209,-18.882,-45.195,-18.831,-45.214,-18.753,-45.236,-18.758,-45.247,-18.838,-45.281,-18.853,-45.34,-18.829,-45.34,-18.801,-45.29,-18.764,-45.258,-18.678,-45.255,-18.623,-45.283,-18.625,-45.359,-18.66,-45.372,-18.636,-45.349,-18.621,-45.331,-18.603,-45.298,-18.541,-45.318,-18.501,-45.367,-18.488,-45.4,-18.524,-45.429,-18.556,-45.476,-18.564,-45.516,-18.586,-45.507,-18.545,-45.479,-18.474,-45.434,-18.433,-45.355,-18.395,-45.353,-18.359,-45.328,-18.303,-45.329,-18.273,-45.379,-18.268,-45.36,-18.239,-45.287,-18.201,-45.262,-18.204,-45.23,-18.231,-45.253,-18.296,-45.285,-18.414,-45.259,-18.474,-45.212,-18.489,-45.192,-18.514,-45.185,-18.569,-45.181,-18.607,-45.148,-18.598,-45.133,-18.608,-45.136,-18.64,-45.156,-18.686,-45.152,-18.735,-45.097,-18.775,-45.036,-18.759,-45.017,-18.723,-45,-18.739,-44.989,-18.769,-44.972,-18.782]]]},{"n":"Engineer Souza Dias Dam","z":5,"b":[-51.438,-20.529,-50.582,-19.668],"p":[[[-51.416,-20.426,-51.423,-20.397,-51.413,-20.373,-51.396,-20.355,-51.438,-20.284,-51.427,-20.257,-51.381,-20.277,-51.314,-20.281,-51.272,-20.255,-51.181,-20.257,-51.156,-20.24,-51.172,-20.185,-51.15,-20.147,-51.104,-20.179,-51.078,-20.175,-51.05,-20.1,-51.049,-20.013,-51.051,-19.781,-51.03,-19.718,-50.997,-19.668,-50.97,-19.727,-50.955,-19.912,-50.949,-19.981,-50.807,-19.932,-50.668,-19.841,-50.582,-19.82,-50.669,-19.947,-50.792,-19.995,-50.844,-20.032,-50.936,-20.09,-50.985,-20.17,-50.978,-20.232,-50.934,-20.265,-50.913,-20.319,-50.983,-20.307,-51.036,-20.329,-51.09,-20.338,-51.112,-20.337,-51.148,-20.351,-51.207,-20.368,-51.245,-20.392,-51.177,-20.458,-51.065,-20.513,-51.066,-20.529,-51.197,-20.499,-51.305,-20.441,-51.363,-20.452,-51.397,-20.441,-51.416,-20.426]]]},{"n":"","z":5,"b":[-72.609,-40.704,-72.365,-40.617],"p":[[[-72.609,-40.658,-72.541,-40.617,-72.391,-40.622,-72.365,-40.639,-72.37,-40.663,-72.464,-40.704,-72.57,-40.69,-72.609,-40.658]]]},{"n":"Colhué Huapí","z":5,"b":[-68.976,-45.657,-68.566,-45.264],"p":[[[-68.676,-45.422,-68.664,-45.461,-68.641,-45.512,-68.601,-45.55,-68.574,-45.568,-68.566,-45.607,-68.599,-45.645,-68.708,-45.657,-68.848,-45.644,-68.923,-45.529,-68.966,-45.447,-68.976,-45.324,-68.97,-45.282,-68.934,-45.264,-68.875,-45.294,-68.859,-45.388,-68.836,-45.413,-68.802,-45.413,-68.766,-45.365,-68.702,-45.369,-68.676,-45.422]]]},{"n":"","z":5,"b":[8.881,47.656,9.128,47.752],"p":[[[9.128,47.671,8.881,47.656,8.948,47.686,8.981,47.702,8.977,47.73,8.999,47.752,9.094,47.716,9.128,47.671]]]},{"n":"","z":5,"b":[36.768,62.179,37.035,62.466],"p":[[[36.867,62.198,36.827,62.223,36.776,62.239,36.768,62.26,36.784,62.291,36.802,62.364,36.813,62.43,36.847,62.459,36.909,62.466,37.015,62.428,37.035,62.368,37.012,62.295,37.025,62.262,36.983,62.211,36.898,62.179,36.867,62.198]]]},{"n":"","z":5,"b":[38.844,60.394,39.188,60.777],"p":[[[38.909,60.586,38.892,60.585,38.89,60.664,38.844,60.734,38.866,60.777,38.938,60.767,39.085,60.718,39.173,60.68,39.185,60.617,39.188,60.496,39.15,60.442,39.096,60.394,39.031,60.395,39.028,60.411,39.042,60.438,39.069,60.475,39.054,60.523,39.008,60.573,38.958,60.595,38.909,60.586]]]},{"n":"","z":5,"b":[31.346,64.405,32.044,64.557],"p":[[[31.545,64.435,31.346,64.465,31.37,64.5,31.473,64.482,31.594,64.477,31.648,64.475,31.743,64.477,31.79,64.495,31.783,64.524,31.765,64.557,31.821,64.553,31.898,64.515,32.015,64.495,32.044,64.46,32.002,64.405,31.873,64.416,31.545,64.435]]]},{"n":"Imandra","z":5,"b":[31.899,67.415,32.406,67.544],"p":[[[32.406,67.457,32.342,67.442,32.246,67.415,32.125,67.434,31.899,67.512,31.944,67.544,32.071,67.543,32.216,67.539,32.342,67.487,32.406,67.457]]]},{"n":"","z":5,"b":[34.995,67.707,35.297,68.1],"p":[[[35.159,67.811,35.136,67.782,35.126,67.751,35.08,67.707,34.995,67.801,35.055,67.827,35.129,67.875,35.158,67.906,35.138,67.926,35.092,67.94,35.099,67.979,35.221,68.059,35.247,68.1,35.291,68.078,35.297,68.006,35.243,67.956,35.256,67.923,35.26,67.88,35.222,67.845,35.159,67.811]]]},{"n":"Chardara Reservoir","z":5,"b":[67.933,41.048,68.456,41.255],"p":[[[67.991,41.255,68.162,41.2,68.243,41.189,68.393,41.161,68.456,41.107,68.403,41.078,68.28,41.052,68.135,41.048,68.024,41.099,67.933,41.225,67.991,41.255]]]},{"n":"Vilyuy","z":5,"b":[109.558,61.783,112.48,63.625],"p":[[[111.12,62.62,111.166,62.672,111.194,62.707,111.158,62.74,111.165,62.792,111.154,62.808,111.139,62.848,110.839,62.912,110.648,62.982,110.431,62.989,110.433,63.046,110.494,63.099,110.484,63.136,110.373,63.139,110.23,63.155,110.109,63.172,110.039,63.208,110.094,63.222,110.2,63.217,110.291,63.218,110.374,63.242,110.393,63.292,110.389,63.35,110.274,63.4,110.162,63.439,110.002,63.429,109.848,63.382,109.75,63.405,109.648,63.424,109.587,63.455,109.746,63.466,109.899,63.456,109.946,63.48,110.038,63.558,110.056,63.594,110.096,63.625,110.136,63.621,110.146,63.6,110.113,63.573,110.127,63.536,110.247,63.485,110.431,63.459,110.482,63.46,110.563,63.422,110.61,63.388,110.568,63.336,110.474,63.25,110.5,63.233,110.546,63.198,110.668,63.199,110.691,63.217,110.744,63.239,110.795,63.219,110.843,63.19,110.918,63.178,110.939,63.159,110.914,63.146,110.886,63.125,110.728,63.09,110.719,63.063,110.772,63.036,110.865,63.019,110.944,62.997,111.015,63.046,111.086,63.094,111.044,63.269,111.05,63.293,111.042,63.311,111.076,63.349,111.103,63.415,111.143,63.424,111.17,63.353,111.162,63.308,111.17,63.295,111.144,63.219,111.16,63.176,111.211,63.138,111.273,63.119,111.334,63.116,111.473,63.13,111.558,63.165,111.584,63.218,111.536,63.288,111.596,63.283,111.652,63.283,111.69,63.289,111.798,63.266,111.9,63.219,111.977,63.119,111.985,63.097,111.83,63.003,111.829,62.96,111.924,62.913,111.998,62.9,112.06,62.914,112.12,62.917,112.155,62.97,112.115,63.017,112.108,63.054,112.12,63.13,112.078,63.235,112.047,63.315,111.908,63.37,111.816,63.383,111.91,63.412,112.079,63.409,112.125,63.432,112.171,63.424,112.139,63.381,112.135,63.315,112.148,63.249,112.234,63.126,112.44,63.075,112.48,63.02,112.4,63,112.38,62.92,112.368,62.842,112.278,62.79,112.175,62.765,112.106,62.783,112.049,62.818,111.915,62.849,111.846,62.873,111.777,62.862,111.729,62.846,111.693,62.864,111.631,62.863,111.581,62.858,111.488,62.873,111.435,62.894,111.399,62.919,111.408,62.94,111.47,62.944,111.521,62.953,111.561,62.968,111.633,62.973,111.657,63.041,111.774,63.07,111.839,63.121,111.844,63.16,111.786,63.196,111.709,63.187,111.64,63.143,111.592,63.077,111.518,63.047,111.416,63.028,111.318,63.028,111.207,63.005,111.191,62.94,111.293,62.883,111.345,62.847,111.376,62.805,111.388,62.723,111.424,62.653,111.39,62.611,111.317,62.55,111.359,62.494,111.358,62.472,111.294,62.46,111.293,62.431,111.328,62.397,111.489,62.402,111.605,62.39,111.64,62.361,111.666,62.331,111.663,62.311,111.504,62.349,111.39,62.327,111.372,62.283,111.604,62.214,111.551,62.187,111.483,62.196,111.423,62.219,111.35,62.223,111.286,62.191,111.296,62.131,111.399,62.058,111.251,61.99,111.046,61.99,110.9,62.011,110.618,62.064,110.554,62.079,110.417,62.093,110.35,62.11,110.303,62.134,110.16,62.124,110.016,62.078,109.827,61.994,109.665,61.911,109.635,61.836,109.601,61.783,109.56,61.799,109.568,61.878,109.558,61.9,109.604,61.942,109.637,62.007,109.627,62.053,109.714,62.039,109.816,62.055,109.932,62.103,110.145,62.204,110.433,62.194,110.606,62.131,110.674,62.121,110.757,62.099,110.81,62.111,110.887,62.084,110.937,62.086,110.947,62.083,110.973,62.061,111.088,62.051,111.156,62.11,111.123,62.164,111.12,62.221,111.209,62.256,111.243,62.309,111.235,62.375,111.209,62.431,111.137,62.483,111.012,62.504,110.916,62.488,110.875,62.468,110.859,62.478,110.852,62.499,110.784,62.528,110.851,62.546,110.979,62.54,111.051,62.544,111.09,62.57,111.12,62.62]]]},{"n":"","z":5,"b":[139.658,53.34,139.922,53.556],"p":[[[139.881,53.556,139.922,53.527,139.855,53.46,139.809,53.4,139.823,53.361,139.816,53.34,139.749,53.348,139.672,53.39,139.658,53.456,139.748,53.524,139.881,53.556]]]},{"n":"Krasnoye","z":5,"b":[174.207,64.433,174.804,64.702],"p":[[[174.429,64.569,174.575,64.581,174.605,64.615,174.607,64.652,174.713,64.669,174.804,64.65,174.766,64.568,174.671,64.481,174.541,64.433,174.338,64.437,174.207,64.513,174.273,64.608,174.352,64.676,174.373,64.702,174.378,64.66,174.352,64.594,174.429,64.569]]]},{"n":"Biwa","z":5,"b":[135.889,35,136.268,35.466],"p":[[[136.058,35.387,136.083,35.443,136.137,35.466,136.182,35.453,136.225,35.407,136.268,35.347,136.214,35.264,135.981,35.098,135.922,35.026,135.889,35,135.892,35.093,135.969,35.203,136.032,35.304,136.058,35.387]]]},{"n":"Aishihik","z":5,"b":[-137.522,61.227,-136.989,61.652],"p":[[[-137.148,61.503,-137.096,61.457,-137.075,61.386,-137.042,61.31,-136.992,61.273,-136.989,61.235,-137,61.227,-137.012,61.235,-137.033,61.262,-137.083,61.292,-137.168,61.45,-137.332,61.542,-137.522,61.636,-137.442,61.652,-137.331,61.612,-137.263,61.577,-137.148,61.503]]]},{"n":"","z":5,"b":[-156.409,58.515,-155.435,58.729],"p":[[[-155.453,58.637,-155.576,58.617,-155.655,58.602,-155.646,58.576,-155.496,58.548,-155.473,58.53,-155.52,58.515,-155.653,58.52,-155.748,58.555,-155.841,58.597,-156.028,58.6,-156.162,58.605,-156.323,58.629,-156.38,58.665,-156.409,58.691,-156.343,58.709,-156.198,58.712,-156.197,58.724,-156.153,58.729,-156.09,58.713,-156.037,58.7,-156.006,58.688,-155.838,58.683,-155.56,58.663,-155.435,58.66,-155.453,58.637]]]},{"n":"","z":5,"b":[-163.031,61.041,-162.713,61.109],"p":[[[-162.838,61.041,-163.031,61.083,-163.002,61.109,-162.801,61.081,-162.713,61.077,-162.838,61.041]]]},{"n":"Clark","z":5,"b":[-154.752,60.04,-153.627,60.436],"p":[[[-154.473,60.222,-154.312,60.279,-154.089,60.34,-153.689,60.431,-153.627,60.436,-153.659,60.404,-154.056,60.301,-154.26,60.23,-154.549,60.113,-154.72,60.04,-154.752,60.07,-154.642,60.143,-154.573,60.183,-154.541,60.189,-154.473,60.222]]]},{"n":"","z":5,"b":[-151.166,60.048,-150.654,60.305],"p":[[[-151.166,60.227,-151.149,60.253,-151.065,60.305,-150.894,60.272,-150.717,60.159,-150.654,60.121,-150.681,60.048,-150.716,60.052,-150.767,60.082,-150.831,60.128,-150.937,60.168,-151.021,60.206,-151.06,60.218,-151.166,60.227]]]},{"n":"","z":5,"b":[-160.131,61.34,-159.934,61.422],"p":[[[-160.002,61.35,-160.089,61.34,-160.131,61.358,-160.103,61.406,-160.038,61.422,-159.971,61.402,-159.934,61.367,-160.002,61.35]]]},{"n":"","z":5,"b":[-162.397,60.821,-162.204,60.99],"p":[[[-162.307,60.951,-162.297,60.918,-162.265,60.89,-162.204,60.853,-162.242,60.827,-162.275,60.821,-162.37,60.83,-162.396,60.899,-162.397,60.95,-162.362,60.99,-162.307,60.951]]]},{"n":"","z":5,"b":[-163.973,61.472,-163.638,61.648],"p":[[[-163.706,61.562,-163.638,61.519,-163.699,61.472,-163.729,61.523,-163.875,61.578,-163.959,61.619,-163.973,61.645,-163.931,61.648,-163.783,61.602,-163.706,61.562]]]},{"n":"","z":5,"b":[-163.619,62.574,-163.41,62.71],"p":[[[-163.459,62.671,-163.5,62.652,-163.524,62.574,-163.582,62.6,-163.619,62.675,-163.565,62.686,-163.456,62.71,-163.41,62.679,-163.459,62.671]]]},{"n":"Wheeler","z":5,"b":[-87.7,34.565,-86.899,34.851],"p":[[[-87.661,34.814,-87.542,34.851,-87.383,34.851,-87.268,34.808,-87.006,34.683,-86.935,34.61,-86.899,34.575,-86.959,34.565,-87.323,34.767,-87.512,34.792,-87.637,34.765,-87.7,34.773,-87.661,34.814]]]},{"n":"Üüreg","z":5,"b":[90.891,50.08,91.131,50.214],"p":[[[91.052,50.208,91.117,50.175,91.131,50.13,91.078,50.099,90.961,50.08,90.891,50.136,90.942,50.214,91.052,50.208]]]},{"n":"","z":5,"b":[90.006,33.325,90.401,33.702],"p":[[[90.401,33.526,90.394,33.436,90.322,33.363,90.247,33.325,90.101,33.377,90.033,33.385,90.006,33.437,90.047,33.445,90.186,33.416,90.29,33.475,90.282,33.569,90.289,33.638,90.319,33.702,90.401,33.526]]]},{"n":"","z":5,"b":[89.71,33.256,89.97,33.523],"p":[[[89.71,33.469,89.771,33.469,89.832,33.48,89.876,33.505,89.935,33.523,89.97,33.48,89.919,33.376,89.89,33.329,89.83,33.256,89.806,33.286,89.786,33.417,89.734,33.447,89.71,33.469]]]},{"n":"","z":5,"b":[75.936,31.9,76.171,32.153],"p":[[[76.169,31.9,76.082,31.909,75.975,31.972,75.938,32.061,75.936,32.139,75.983,32.153,76.095,32.073,76.171,31.962,76.169,31.9]]]},{"n":"","z":5,"b":[91.424,31.943,91.532,32.133],"p":[[[91.469,31.947,91.437,31.991,91.424,32.062,91.455,32.112,91.511,32.133,91.532,32.08,91.529,31.966,91.518,31.943,91.469,31.947]]]},{"n":"","z":5,"b":[87.415,31.847,87.596,31.969],"p":[[[87.415,31.852,87.424,31.912,87.49,31.954,87.546,31.969,87.591,31.948,87.596,31.903,87.53,31.847,87.415,31.852]]]},{"n":"Pangong Tso","z":5,"b":[78.443,33.677,78.976,33.974],"p":[[[78.443,33.939,78.49,33.974,78.658,33.816,78.758,33.747,78.961,33.741,78.976,33.723,78.914,33.697,78.734,33.677,78.553,33.851,78.443,33.939]]]},{"n":"Tengiz","z":5,"b":[68.69,50.179,69.617,50.719],"p":[[[69.288,50.688,69.371,50.719,69.516,50.693,69.617,50.676,69.613,50.615,69.5,50.591,69.39,50.599,69.335,50.604,69.275,50.588,69.109,50.46,69.047,50.426,69.036,50.397,69.057,50.364,69.237,50.354,69.259,50.311,69.256,50.254,69.2,50.254,69.148,50.241,69.074,50.201,68.938,50.179,68.778,50.228,68.69,50.266,68.719,50.33,68.759,50.438,68.779,50.513,68.861,50.571,68.993,50.564,69.091,50.564,69.227,50.616,69.288,50.688]]]},{"n":"Kushmurun","z":5,"b":[64.501,52.516,64.982,52.908],"p":[[[64.574,52.605,64.685,52.687,64.79,52.768,64.884,52.856,64.949,52.908,64.982,52.883,64.967,52.731,64.89,52.672,64.794,52.624,64.765,52.613,64.722,52.594,64.649,52.553,64.515,52.516,64.501,52.535,64.574,52.605]]]},{"n":"Femund","z":5,"b":[11.742,61.929,11.797,62.052],"p":[[[11.797,61.939,11.753,61.929,11.742,62.008,11.778,62.052,11.797,61.939]]]},{"n":"Femund","z":5,"b":[11.721,61.931,11.967,62.421],"p":[[[11.846,62.212,11.893,62.3,11.892,62.4,11.922,62.421,11.967,62.407,11.95,62.31,11.929,62.205,11.9,62.144,11.885,62.089,11.902,62.026,11.927,61.931,11.883,61.931,11.843,62.046,11.721,62.103,11.736,62.138,11.766,62.154,11.846,62.212]]]},{"n":"","z":5,"b":[-77.026,51.288,-76.554,51.416],"p":[[[-76.677,51.373,-76.554,51.364,-76.563,51.344,-76.751,51.288,-77.026,51.329,-76.983,51.343,-76.903,51.355,-76.862,51.38,-76.84,51.404,-76.819,51.416,-76.762,51.391,-76.677,51.373]]]},{"n":"","z":5,"b":[-91.647,51.071,-91.39,51.166],"p":[[[-91.418,51.12,-91.39,51.071,-91.542,51.085,-91.591,51.118,-91.647,51.154,-91.609,51.166,-91.504,51.162,-91.441,51.163,-91.417,51.143,-91.418,51.12]]]},{"n":"","z":5,"b":[-91.991,51.894,-91.895,51.982],"p":[[[-91.97,51.958,-91.95,51.982,-91.895,51.952,-91.91,51.911,-91.932,51.894,-91.968,51.924,-91.991,51.937,-91.97,51.958]]]},{"n":"","z":5,"b":[-78.827,51.034,-78.55,51.152],"p":[[[-78.616,51.077,-78.55,51.034,-78.813,51.097,-78.827,51.152,-78.794,51.146,-78.72,51.109,-78.616,51.077]]]},{"n":"","z":5,"b":[-78.813,47.57,-78.571,47.691],"p":[[[-78.628,47.597,-78.696,47.577,-78.763,47.57,-78.787,47.595,-78.798,47.626,-78.813,47.656,-78.792,47.691,-78.724,47.684,-78.612,47.681,-78.571,47.656,-78.584,47.625,-78.604,47.608,-78.628,47.597]]]},{"n":"Dozois","z":5,"b":[-77.322,47.347,-76.935,47.614],"p":[[[-77.273,47.591,-77.235,47.55,-77.177,47.547,-77.129,47.561,-77.102,47.545,-77.065,47.508,-76.963,47.502,-76.935,47.488,-76.966,47.442,-77.119,47.477,-77.185,47.456,-77.179,47.412,-77.165,47.389,-77.181,47.369,-77.259,47.347,-77.289,47.386,-77.27,47.463,-77.304,47.513,-77.322,47.573,-77.304,47.607,-77.288,47.614,-77.273,47.591]]]},{"n":"","z":5,"b":[-108.09,64.697,-107.979,64.749],"p":[[[-108.09,64.715,-108.085,64.737,-108.044,64.749,-108.016,64.745,-107.979,64.72,-107.987,64.71,-108.063,64.697,-108.09,64.715]]]},{"n":"","z":5,"b":[-108.533,64.559,-108.18,64.686],"p":[[[-108.189,64.646,-108.209,64.634,-108.24,64.625,-108.481,64.559,-108.533,64.562,-108.494,64.598,-108.329,64.654,-108.24,64.684,-108.18,64.686,-108.189,64.646]]]},{"n":"","z":5,"b":[-109.1,64.552,-108.821,64.727],"p":[[[-108.929,64.6,-108.989,64.613,-109.056,64.594,-109.09,64.587,-109.1,64.603,-109.098,64.656,-109.061,64.686,-109.071,64.711,-109.062,64.727,-108.93,64.706,-108.893,64.664,-108.852,64.606,-108.821,64.552,-108.854,64.561,-108.929,64.6]]]},{"n":"","z":5,"b":[-109.362,64.536,-109.149,64.758],"p":[[[-109.251,64.594,-109.282,64.608,-109.34,64.655,-109.362,64.702,-109.36,64.734,-109.347,64.758,-109.291,64.758,-109.24,64.74,-109.256,64.727,-109.284,64.702,-109.272,64.662,-109.149,64.58,-109.16,64.547,-109.224,64.536,-109.273,64.536,-109.235,64.565,-109.251,64.594]]]},{"n":"","z":5,"b":[-79.367,71.089,-78.671,71.178],"p":[[[-78.866,71.16,-78.792,71.146,-78.671,71.126,-78.671,71.112,-78.699,71.1,-78.896,71.089,-79.211,71.097,-79.339,71.122,-79.367,71.149,-79.326,71.17,-79.277,71.175,-79.09,71.178,-78.88,71.173,-78.866,71.16]]]},{"n":"","z":5,"b":[-104.862,69.718,-104.358,69.858],"p":[[[-104.511,69.718,-104.646,69.733,-104.862,69.856,-104.762,69.858,-104.482,69.84,-104.422,69.83,-104.358,69.815,-104.388,69.756,-104.511,69.718]]]},{"n":"","z":5,"b":[-98.552,67.192,-98.229,67.421],"p":[[[-98.382,67.384,-98.302,67.35,-98.229,67.332,-98.272,67.289,-98.421,67.192,-98.491,67.206,-98.546,67.243,-98.552,67.272,-98.523,67.351,-98.486,67.421,-98.435,67.414,-98.382,67.384]]]},{"n":"","z":5,"b":[-96.285,55.212,-95.676,55.401],"p":[[[-96.285,55.238,-96.205,55.271,-96.055,55.287,-95.853,55.361,-95.789,55.4,-95.702,55.401,-95.676,55.364,-95.733,55.308,-95.83,55.28,-96.106,55.217,-96.229,55.212,-96.285,55.238]]]},{"n":"","z":5,"b":[-102.378,54.449,-102.106,54.738],"p":[[[-102.363,54.578,-102.336,54.655,-102.287,54.715,-102.197,54.738,-102.116,54.708,-102.106,54.598,-102.128,54.494,-102.169,54.455,-102.256,54.449,-102.34,54.475,-102.378,54.516,-102.363,54.578]]]},{"n":"","z":5,"b":[-105.812,54.055,-105.553,54.509],"p":[[[-105.812,54.234,-105.728,54.414,-105.714,54.492,-105.683,54.509,-105.643,54.488,-105.568,54.459,-105.553,54.39,-105.57,54.327,-105.636,54.268,-105.673,54.222,-105.707,54.15,-105.714,54.086,-105.755,54.055,-105.805,54.078,-105.81,54.161,-105.812,54.234]]]},{"n":"","z":5,"b":[-71.125,53.407,-70.843,53.596],"p":[[[-70.932,53.519,-70.935,53.483,-70.904,53.458,-70.861,53.452,-70.843,53.427,-70.865,53.407,-70.98,53.431,-71.048,53.501,-71.125,53.565,-71.117,53.591,-71.019,53.596,-70.95,53.565,-70.932,53.519]]]},{"n":"","z":5,"b":[-61.297,52.315,-60.6,52.619],"p":[[[-60.904,52.55,-60.724,52.444,-60.708,52.409,-60.657,52.358,-60.604,52.329,-60.6,52.32,-60.648,52.315,-60.739,52.378,-60.79,52.424,-60.917,52.477,-60.968,52.516,-61.15,52.589,-61.297,52.603,-61.139,52.619,-60.904,52.55]]]},{"n":"","z":5,"b":[-112.343,64.638,-111.887,64.899],"p":[[[-111.996,64.66,-111.988,64.69,-112.151,64.78,-112.326,64.86,-112.343,64.899,-112.114,64.848,-111.887,64.701,-111.927,64.659,-111.966,64.638,-111.996,64.66]]]},{"n":"","z":5,"b":[-83.925,48.305,-83.429,48.506],"p":[[[-83.611,48.378,-83.688,48.339,-83.781,48.305,-83.832,48.315,-83.925,48.307,-83.854,48.337,-83.721,48.421,-83.64,48.427,-83.618,48.426,-83.573,48.424,-83.429,48.506,-83.468,48.434,-83.611,48.378]]]},{"n":"","z":5,"b":[-107.063,55.806,-106.88,55.927],"p":[[[-107.063,55.82,-107.059,55.855,-107.011,55.89,-106.947,55.921,-106.897,55.927,-106.88,55.908,-106.984,55.832,-106.973,55.815,-107.009,55.806,-107.063,55.82]]]},{"n":"","z":5,"b":[-106.551,55.631,-106.33,55.76],"p":[[[-106.472,55.631,-106.514,55.639,-106.524,55.657,-106.551,55.668,-106.539,55.689,-106.441,55.728,-106.344,55.76,-106.33,55.74,-106.359,55.691,-106.341,55.662,-106.386,55.648,-106.472,55.631]]]},{"n":"","z":5,"b":[-102.774,62.41,-102.551,62.762],"p":[[[-102.597,62.657,-102.58,62.605,-102.56,62.552,-102.551,62.499,-102.576,62.45,-102.67,62.418,-102.737,62.41,-102.677,62.56,-102.692,62.602,-102.769,62.669,-102.774,62.704,-102.694,62.762,-102.66,62.76,-102.597,62.657]]]},{"n":"","z":5,"b":[-103.478,61.366,-103.144,61.677],"p":[[[-103.447,61.366,-103.469,61.385,-103.478,61.431,-103.456,61.513,-103.342,61.562,-103.233,61.642,-103.21,61.677,-103.144,61.653,-103.191,61.588,-103.296,61.521,-103.392,61.395,-103.447,61.366]]]},{"n":"","z":5,"b":[-103.836,58.545,-103.6,58.702],"p":[[[-103.836,58.609,-103.798,58.669,-103.786,58.691,-103.663,58.702,-103.621,58.678,-103.6,58.661,-103.623,58.607,-103.668,58.545,-103.771,58.574,-103.819,58.583,-103.836,58.609]]]},{"n":"","z":5,"b":[-69.274,56.055,-68.795,56.65],"p":[[[-69.153,56.507,-69.123,56.332,-69.077,56.272,-68.808,56.089,-68.795,56.055,-69.121,56.248,-69.189,56.326,-69.195,56.427,-69.274,56.504,-69.219,56.563,-69.128,56.65,-69.136,56.606,-69.153,56.507]]]},{"n":"","z":5,"b":[-89.089,66.483,-88.792,66.633],"p":[[[-88.93,66.577,-89.089,66.483,-89.075,66.556,-88.96,66.607,-88.792,66.633,-88.82,66.596,-88.93,66.577]]]},{"n":"","z":5,"b":[-88.711,66.579,-88.091,66.714],"p":[[[-88.202,66.633,-88.091,66.605,-88.213,66.581,-88.284,66.634,-88.396,66.657,-88.547,66.64,-88.682,66.579,-88.71,66.597,-88.711,66.632,-88.658,66.665,-88.615,66.711,-88.567,66.714,-88.463,66.701,-88.341,66.705,-88.202,66.633]]]},{"n":"Kumpupintil","z":5.7,"b":[122.608,-23.685,122.947,-23.255],"p":[[[122.823,-23.685,122.768,-23.661,122.641,-23.538,122.608,-23.469,122.697,-23.322,122.703,-23.274,122.758,-23.264,122.879,-23.255,122.932,-23.277,122.947,-23.434,122.926,-23.591,122.876,-23.662,122.823,-23.685]]]},{"n":"Mackay","z":5.7,"b":[128.317,-22.678,129.219,-22.002],"p":[[[128.601,-22.648,128.385,-22.598,128.317,-22.491,128.341,-22.404,128.429,-22.369,128.478,-22.345,128.599,-22.309,128.794,-22.22,128.908,-22.12,128.934,-22.064,129,-22.009,129.135,-22.002,129.216,-22.035,129.201,-22.085,129.072,-22.174,129.06,-22.271,129.142,-22.335,129.219,-22.366,129.215,-22.417,129.158,-22.448,129.099,-22.498,129.026,-22.563,128.899,-22.654,128.82,-22.678,128.601,-22.648]]]},{"n":"Gairdner","z":5.7,"b":[135.418,-32.326,136.312,-31.063],"p":[[[135.896,-32.326,135.89,-32.241,135.921,-32.15,136.045,-31.966,136.052,-31.902,135.992,-31.835,135.852,-31.743,135.812,-31.668,135.788,-31.617,135.749,-31.593,135.715,-31.597,135.691,-31.606,135.637,-31.59,135.611,-31.514,135.634,-31.445,135.638,-31.402,135.624,-31.382,135.595,-31.366,135.562,-31.354,135.522,-31.315,135.49,-31.28,135.439,-31.232,135.418,-31.096,135.429,-31.063,135.493,-31.084,135.605,-31.189,135.777,-31.238,135.886,-31.224,135.955,-31.225,136.08,-31.254,136.106,-31.305,136.104,-31.353,136.11,-31.415,136.145,-31.505,136.203,-31.577,136.275,-31.845,136.312,-31.926,136.253,-32.148,136.256,-32.211,136.238,-32.24,136.185,-32.253,136.123,-32.232,136.056,-32.209,135.975,-32.23,135.916,-32.318,135.896,-32.326]]]},{"n":"Torrens","z":5.7,"b":[137.212,-31.897,138.133,-30.179],"p":[[[137.83,-31.717,137.775,-31.864,137.748,-31.897,137.71,-31.877,137.613,-31.808,137.601,-31.778,137.672,-31.732,137.703,-31.664,137.733,-31.326,137.703,-31.273,137.652,-31.241,137.557,-31.156,137.531,-31.106,137.565,-31.082,137.632,-31.063,137.659,-31.027,137.662,-30.999,137.642,-30.832,137.621,-30.765,137.626,-30.673,137.595,-30.633,137.427,-30.629,137.395,-30.573,137.383,-30.471,137.328,-30.365,137.256,-30.288,137.212,-30.223,137.227,-30.179,137.323,-30.217,137.421,-30.307,137.482,-30.383,137.602,-30.427,137.746,-30.494,137.84,-30.597,137.928,-30.717,138.002,-30.874,138.03,-30.951,138.02,-31.003,138.026,-31.06,138.133,-31.259,138.111,-31.393,137.83,-31.717]]]},{"n":"Barlee","z":6,"b":[119.129,-29.58,120.006,-28.882],"p":[[[119.798,-29.505,119.786,-29.58,119.728,-29.58,119.636,-29.55,119.662,-29.51,119.693,-29.452,119.666,-29.387,119.636,-29.338,119.665,-29.277,119.668,-29.21,119.629,-29.186,119.58,-29.188,119.548,-29.216,119.542,-29.379,119.517,-29.505,119.442,-29.554,119.382,-29.542,119.437,-29.236,119.38,-29.214,119.33,-29.235,119.204,-29.351,119.142,-29.28,119.129,-29.114,119.226,-29.091,119.314,-29.012,119.398,-28.994,119.548,-29.057,119.65,-29.027,119.715,-28.936,119.764,-28.888,119.857,-28.899,119.938,-28.882,120.006,-28.928,119.993,-28.981,119.946,-28.985,119.877,-28.943,119.718,-29.067,119.683,-29.12,119.751,-29.297,119.79,-29.373,119.823,-29.413,119.798,-29.505]]]},{"n":"Frome","z":5.7,"b":[139.488,-31.126,140.044,-30.321],"p":[[[139.938,-30.964,139.92,-31.027,139.876,-31.084,139.835,-31.126,139.8,-31.124,139.744,-31.08,139.659,-31.033,139.613,-31.011,139.519,-30.925,139.488,-30.877,139.55,-30.774,139.719,-30.566,139.896,-30.374,139.951,-30.321,140.011,-30.341,140.044,-30.423,140.026,-30.524,139.982,-30.854,139.938,-30.964]]]},{"n":"Eyre South","z":6,"b":[137.111,-29.468,137.72,-29.131],"p":[[[137.661,-29.348,137.605,-29.371,137.522,-29.395,137.407,-29.444,137.374,-29.454,137.28,-29.449,137.189,-29.468,137.111,-29.449,137.119,-29.346,137.2,-29.245,137.331,-29.222,137.589,-29.159,137.698,-29.131,137.72,-29.197,137.661,-29.348]]]},{"n":"Eyre North","z":5.6,"b":[136.785,-29.1,138.014,-27.859],"p":[[[137.914,-29.1,137.777,-29.038,137.619,-29.051,137.478,-29.012,137.407,-29.029,137.344,-29.025,137.301,-28.988,137.239,-28.892,137.146,-28.887,137.125,-28.918,137.12,-28.978,137.075,-29.014,137.018,-29.015,136.949,-28.953,136.894,-28.832,136.917,-28.756,136.922,-28.689,136.883,-28.45,136.907,-28.426,136.966,-28.394,136.99,-28.314,136.969,-28.199,136.913,-28.103,136.889,-28.064,136.83,-28.044,136.785,-28.019,136.8,-27.967,136.891,-27.911,137.033,-27.859,137.328,-27.974,137.453,-28.087,137.566,-28.116,137.672,-28.12,137.702,-28.143,137.707,-28.196,137.72,-28.28,137.774,-28.35,137.733,-28.39,137.712,-28.537,137.68,-28.562,137.676,-28.645,137.786,-28.84,137.905,-28.985,138.014,-29.051,138.01,-29.091,137.914,-29.1]]]}]} \ No newline at end of file diff --git a/src/maps/data/places_110m.json b/src/maps/data/places_110m.json new file mode 100644 index 0000000..a042065 --- /dev/null +++ b/src/maps/data/places_110m.json @@ -0,0 +1 @@ +{"f":[{"n":"Vatican City","a":"Vatican","i":"VA","p":832,"la":41.903,"lo":12.453,"k":1,"z":7,"s":8},{"n":"San Marino","a":"San Marino","i":"SM","p":29579,"la":43.936,"lo":12.442,"k":1,"z":6.1,"s":7},{"n":"Vaduz","a":"Liechtenstein","i":"LI","p":36281,"la":47.134,"lo":9.517,"k":1,"z":6.7,"s":7},{"n":"Lobamba","a":"eSwatini","i":"SZ","p":9782,"la":-26.467,"lo":31.2,"k":0,"z":6,"s":6},{"n":"Luxembourg","a":"Luxembourg","i":"LU","p":107260,"la":49.612,"lo":6.13,"k":1,"z":6,"s":6},{"n":"Palikir","a":"Federated States of Micronesia","i":"FM","p":4645,"la":6.917,"lo":158.15,"k":1,"z":6,"s":6},{"n":"Majuro","a":"Marshall Islands","i":"MH","p":25400,"la":7.103,"lo":171.38,"k":1,"z":6,"s":6},{"n":"Funafuti","a":"Tuvalu","i":"TV","p":4749,"la":-8.517,"lo":179.217,"k":1,"z":6,"s":6},{"n":"Melekeok","a":"Palau","i":"PW","p":7026,"la":7.487,"lo":134.627,"k":1,"z":6,"s":6},{"n":"Bir Lehlou","a":"Western Sahara","i":"EH","p":500,"la":26.119,"lo":-9.653,"k":0,"z":6,"s":6},{"n":"Monaco","a":"Monaco","i":"MC","p":36371,"la":43.74,"lo":7.407,"k":1,"z":5.1,"s":6},{"n":"Tarawa","a":"Kiribati","i":"KI","p":28802,"la":1.338,"lo":173.018,"k":1,"z":6,"s":6},{"n":"Moroni","a":"Comoros","i":"KM","p":128698,"la":-11.704,"lo":43.24,"k":1,"z":6,"s":6},{"n":"Andorra","a":"Andorra","i":"AD","p":53998,"la":42.511,"lo":1.527,"k":1,"z":6,"s":6},{"n":"Port-of-Spain","a":"Trinidad and Tobago","i":"TT","p":294934,"la":10.652,"lo":-61.517,"k":1,"z":5.1,"s":4},{"n":"Kigali","a":"Rwanda","i":"RW","p":860000,"la":-1.952,"lo":30.059,"k":1,"z":5.1,"s":4},{"n":"Mbabane","a":"eSwatini","i":"SZ","p":90138,"la":-26.317,"lo":31.133,"k":1,"z":5.6,"s":4},{"n":"Juba","a":"South Sudan","i":"SS","p":111975,"la":4.83,"lo":31.58,"k":0,"z":5.1,"s":4},{"n":"The Hague","a":"Netherlands","i":"NL","p":1406000,"la":52.08,"lo":4.27,"k":0,"z":5.6,"s":4},{"n":"Ljubljana","a":"Slovenia","i":"SI","p":314807,"la":46.055,"lo":14.515,"k":1,"z":5.6,"s":4},{"n":"Bratislava","a":"Slovakia","i":"SK","p":423737,"la":48.15,"lo":17.117,"k":1,"z":5.1,"s":4},{"n":"Doha","a":"Qatar","i":"QA","p":1450000,"la":25.287,"lo":51.533,"k":1,"z":5.6,"s":4},{"n":"Podgorica","a":"Montenegro","i":"ME","p":145850,"la":42.466,"lo":19.266,"k":1,"z":5.6,"s":4},{"n":"Sri Jayawardenepura Kotte","a":"Sri Lanka","i":"LK","p":115826,"la":6.9,"lo":79.95,"k":0,"z":5.6,"s":4},{"n":"Baguio","a":"Philippines","i":"PH","p":447824,"la":16.43,"lo":120.57,"k":0,"z":5.6,"s":4},{"n":"Dodoma","a":"Tanzania","i":"TZ","p":218269,"la":-6.183,"lo":35.75,"k":0,"z":5.6,"s":4},{"n":"Bern","a":"Switzerland","i":"CH","p":275329,"la":46.917,"lo":7.467,"k":1,"z":5.6,"s":4},{"n":"Laayoune","a":"Morocco","i":"MA","p":188084,"la":27.15,"lo":-13.2,"k":0,"z":5.6,"s":4},{"n":"Pristina","a":"Kosovo","i":"-99","p":465186,"la":42.667,"lo":21.166,"k":1,"z":5.6,"s":4},{"n":"Roseau","a":"Dominica","i":"DM","p":23336,"la":15.301,"lo":-61.387,"k":1,"z":5.1,"s":4},{"n":"Djibouti","a":"Djibouti","i":"DJ","p":923000,"la":11.595,"lo":43.148,"k":1,"z":5.6,"s":4},{"n":"Putrajaya","a":"Malaysia","i":"MY","p":67964,"la":2.933,"lo":101.695,"k":0,"z":5.6,"s":4},{"n":"Kyoto","a":"Japan","i":"JP","p":1805000,"la":35.032,"lo":135.748,"k":0,"z":5.1,"s":4},{"n":"Banjul","a":"The Gambia","i":"GM","p":43094,"la":13.454,"lo":-16.592,"k":1,"z":5.6,"s":4},{"n":"Skopje","a":"North Macedonia","i":"MK","p":494087,"la":42,"lo":21.433,"k":1,"z":5.6,"s":4},{"n":"Bridgetown","a":"Barbados","i":"BB","p":191152,"la":13.102,"lo":-59.617,"k":1,"z":5.6,"s":4},{"n":"Porto-Novo","a":"Benin","i":"BJ","p":300000,"la":6.483,"lo":2.617,"k":0,"z":5.6,"s":4},{"n":"Bujumbura","a":"Burundi","i":"BI","p":331700,"la":-3.376,"lo":29.36,"k":1,"z":5.6,"s":4},{"n":"Kingstown","a":"Saint Vincent and the Grenadines","i":"VC","p":49485,"la":13.156,"lo":-61.22,"k":1,"z":5.1,"s":4},{"n":"Castries","a":"Saint Lucia","i":"LC","p":37963,"la":14.008,"lo":-60.993,"k":1,"z":5.6,"s":4},{"n":"Basseterre","a":"Saint Kitts and Nevis","i":"KN","p":21887,"la":17.302,"lo":-62.717,"k":1,"z":5.6,"s":4},{"n":"Port Louis","a":"Mauritius","i":"MU","p":595491,"la":-20.167,"lo":57.5,"k":1,"z":5.6,"s":4},{"n":"Saint George's","a":"Grenada","i":"GD","p":33734,"la":12.053,"lo":-61.742,"k":1,"z":5.1,"s":4},{"n":"Manama","a":"Bahrain","i":"BH","p":563920,"la":26.236,"lo":50.583,"k":1,"z":5.6,"s":4},{"n":"Saint John's","a":"Antigua and Barbuda","i":"AG","p":35499,"la":17.118,"lo":-61.85,"k":1,"z":5.1,"s":4},{"n":"Montevideo","a":"Uruguay","i":"UY","p":1513000,"la":-34.905,"lo":-56.187,"k":1,"z":4,"s":3},{"n":"Lomé","a":"Togo","i":"TG","p":1452000,"la":6.134,"lo":1.221,"k":1,"z":5,"s":3},{"n":"Tunis","a":"Tunisia","i":"TN","p":2412500,"la":36.803,"lo":10.18,"k":1,"z":5,"s":3},{"n":"Abu Dhabi","a":"United Arab Emirates","i":"AE","p":603492,"la":24.467,"lo":54.367,"k":1,"z":5,"s":3},{"n":"Ashgabat","a":"Turkmenistan","i":"TM","p":727700,"la":37.95,"lo":58.383,"k":1,"z":5,"s":3},{"n":"Lusaka","a":"Zambia","i":"ZM","p":1328000,"la":-15.415,"lo":28.281,"k":1,"z":4,"s":3},{"n":"Harare","a":"Zimbabwe","i":"ZW","p":1572000,"la":-17.816,"lo":31.043,"k":1,"z":4,"s":3},{"n":"Dili","a":"East Timor","i":"TL","p":234331,"la":-8.559,"lo":125.579,"k":1,"z":5,"s":3},{"n":"Port Vila","a":"Vanuatu","i":"VU","p":44040,"la":-17.733,"lo":168.317,"k":1,"z":4,"s":3},{"n":"Tegucigalpa","a":"Honduras","i":"HN","p":946000,"la":14.104,"lo":-87.219,"k":1,"z":5,"s":3},{"n":"Georgetown","a":"Guyana","i":"GY","p":264350,"la":6.802,"lo":-58.167,"k":1,"z":4,"s":3},{"n":"Reykjavík","a":"Iceland","i":"IS","p":166212,"la":64.143,"lo":-21.937,"k":1,"z":3.7,"s":3},{"n":"Port-au-Prince","a":"Haiti","i":"HT","p":1998000,"la":18.543,"lo":-72.338,"k":1,"z":5,"s":3},{"n":"Kampala","a":"Uganda","i":"UG","p":1420000,"la":0.319,"lo":32.581,"k":1,"z":5,"s":3},{"n":"Paramaribo","a":"Suriname","i":"SR","p":254169,"la":5.835,"lo":-55.167,"k":1,"z":5,"s":3},{"n":"Niamey","a":"Niger","i":"NE","p":915000,"la":13.519,"lo":2.115,"k":1,"z":5,"s":3},{"n":"Dushanbe","a":"Tajikistan","i":"TJ","p":1086244,"la":38.56,"lo":68.774,"k":1,"z":5,"s":3},{"n":"Asunción","a":"Paraguay","i":"PY","p":1870000,"la":-25.291,"lo":-57.626,"k":1,"z":4,"s":3},{"n":"Managua","a":"Nicaragua","i":"NI","p":920000,"la":12.155,"lo":-86.27,"k":1,"z":5,"s":3},{"n":"Freetown","a":"Sierra Leone","i":"SL","p":827000,"la":8.472,"lo":-13.236,"k":1,"z":5,"s":3},{"n":"Islamabad","a":"Pakistan","i":"PK","p":780000,"la":33.689,"lo":73.081,"k":1,"z":5,"s":3},{"n":"Kathmandu","a":"Nepal","i":"NP","p":895000,"la":27.719,"lo":85.315,"k":1,"z":5,"s":3},{"n":"Bloemfontein","a":"South Africa","i":"ZA","p":463064,"la":-29.12,"lo":26.23,"k":1,"z":5,"s":3},{"n":"Pretoria","a":"South Africa","i":"ZA","p":1338000,"la":-25.705,"lo":28.227,"k":1,"z":5,"s":3},{"n":"Port Moresby","a":"Papua New Guinea","i":"PG","p":283733,"la":-9.465,"lo":147.193,"k":1,"z":4,"s":3},{"n":"Honiara","a":"Solomon Islands","i":"SB","p":76328,"la":-9.438,"lo":159.95,"k":1,"z":5,"s":3},{"n":"Panama City","a":"Panama","i":"PA","p":1281000,"la":8.97,"lo":-79.535,"k":1,"z":5,"s":3},{"n":"Rabat","a":"Morocco","i":"MA","p":1705000,"la":34.025,"lo":-6.836,"k":1,"z":5,"s":3},{"n":"Chișinău","a":"Moldova","i":"MD","p":688134,"la":47.005,"lo":28.858,"k":1,"z":5,"s":3},{"n":"Maputo","a":"Mozambique","i":"MZ","p":1446000,"la":-25.953,"lo":32.587,"k":1,"z":5,"s":3},{"n":"Mogadishu","a":"Somalia","i":"SO","p":1100000,"la":2.069,"lo":45.365,"k":1,"z":4,"s":3},{"n":"Muscat","a":"Oman","i":"OM","p":734697,"la":23.585,"lo":58.378,"k":1,"z":5,"s":3},{"n":"Colombo","a":"Sri Lanka","i":"LK","p":217000,"la":6.932,"lo":79.858,"k":1,"z":5,"s":3},{"n":"Ulaanbaatar","a":"Mongolia","i":"MN","p":885000,"la":47.919,"lo":106.915,"k":1,"z":4,"s":3},{"n":"Windhoek","a":"Namibia","i":"NA","p":268132,"la":-22.57,"lo":17.084,"k":1,"z":5,"s":3},{"n":"Abuja","a":"Nigeria","i":"NG","p":1576000,"la":9.055,"lo":7.49,"k":1,"z":4,"s":3},{"n":"Bissau","a":"Guinea Bissau","i":"GW","p":403339,"la":11.865,"lo":-15.598,"k":1,"z":5,"s":3},{"n":"Amman","a":"Jordan","i":"JO","p":1060000,"la":31.952,"lo":35.931,"k":1,"z":5,"s":3},{"n":"Vilnius","a":"Lithuania","i":"LT","p":542366,"la":54.683,"lo":25.317,"k":1,"z":5,"s":3},{"n":"Riga","a":"Latvia","i":"LV","p":742572,"la":56.95,"lo":24.1,"k":1,"z":4,"s":3},{"n":"Bishkek","a":"Kyrgyzstan","i":"KG","p":837000,"la":42.875,"lo":74.583,"k":1,"z":5,"s":3},{"n":"Maseru","a":"Lesotho","i":"LS","p":361324,"la":-29.317,"lo":27.483,"k":1,"z":5,"s":3},{"n":"Antananarivo","a":"Madagascar","i":"MG","p":1697000,"la":-18.915,"lo":47.515,"k":1,"z":5,"s":3},{"n":"Quito","a":"Ecuador","i":"EC","p":1701000,"la":-0.213,"lo":-78.502,"k":1,"z":4,"s":3},{"n":"San José","a":"Costa Rica","i":"CR","p":1284000,"la":9.93,"lo":-84.079,"k":1,"z":5,"s":3},{"n":"San Salvador","a":"El Salvador","i":"SV","p":1433000,"la":13.703,"lo":-89.216,"k":1,"z":5,"s":3},{"n":"Kingston","a":"Jamaica","i":"JM","p":937700,"la":17.977,"lo":-76.767,"k":1,"z":5,"s":3},{"n":"N'Djamena","a":"Chad","i":"TD","p":989000,"la":12.115,"lo":15.047,"k":1,"z":5,"s":3},{"n":"Malabo","a":"Equatorial Guinea","i":"GQ","p":155963,"la":3.75,"lo":8.783,"k":1,"z":5,"s":3},{"n":"Asmara","a":"Eritrea","i":"ER","p":620802,"la":15.333,"lo":38.933,"k":1,"z":5,"s":3},{"n":"Zagreb","a":"Croatia","i":"HR","p":722526,"la":45.8,"lo":16,"k":1,"z":5,"s":3},{"n":"Tallinn","a":"Estonia","i":"EE","p":394024,"la":59.434,"lo":24.728,"k":1,"z":5,"s":3},{"n":"Lilongwe","a":"Malawi","i":"MW","p":646750,"la":-13.983,"lo":33.783,"k":1,"z":5,"s":3},{"n":"Guatemala City","a":"Guatemala","i":"GT","p":1024000,"la":14.623,"lo":-90.529,"k":1,"z":5,"s":3},{"n":"Libreville","a":"Gabon","i":"GA","p":578156,"la":0.385,"lo":9.458,"k":1,"z":5,"s":3},{"n":"Suva","a":"Fiji","i":"FJ","p":175399,"la":-18.133,"lo":178.442,"k":1,"z":5,"s":3},{"n":"Valparaíso","a":"Chile","i":"CL","p":854000,"la":-33.048,"lo":-71.617,"k":0,"z":5,"s":3},{"n":"Nouakchott","a":"Mauritania","i":"MR","p":742144,"la":18.086,"lo":-15.975,"k":1,"z":5,"s":3},{"n":"Bamako","a":"Mali","i":"ML","p":1494000,"la":12.652,"lo":-8.002,"k":1,"z":4,"s":3},{"n":"Beirut","a":"Lebanon","i":"LB","p":1846000,"la":33.874,"lo":35.508,"k":1,"z":5,"s":3},{"n":"Tbilisi","a":"Georgia","i":"GE","p":1100000,"la":41.727,"lo":44.789,"k":1,"z":5,"s":3},{"n":"Nur-Sultan","a":"Kazakhstan","i":"KZ","p":345604,"la":51.181,"lo":71.428,"k":1,"z":4,"s":3},{"n":"Vientiane","a":"Laos","i":"LA","p":754000,"la":17.967,"lo":102.6,"k":1,"z":5,"s":3},{"n":"Brazzaville","a":"Congo (Brazzaville)","i":"CG","p":1355000,"la":-4.257,"lo":15.283,"k":1,"z":5,"s":3},{"n":"Conakry","a":"Guinea","i":"GN","p":1494000,"la":9.533,"lo":-13.682,"k":1,"z":5,"s":3},{"n":"Yamoussoukro","a":"Ivory Coast","i":"CI","p":206499,"la":6.818,"lo":-5.276,"k":1,"z":5,"s":3},{"n":"Ottawa","a":"Canada","i":"CA","p":1145000,"la":45.419,"lo":-75.702,"k":1,"z":3,"s":3},{"n":"Belgrade","a":"Serbia","i":"RS","p":1099000,"la":44.821,"lo":20.466,"k":1,"z":5,"s":3},{"n":"Bandar Seri Begawan","a":"Brunei","i":"BN","p":296500,"la":4.883,"lo":114.933,"k":1,"z":5,"s":3},{"n":"Sucre","a":"Bolivia","i":"BO","p":224838,"la":-19.041,"lo":-65.26,"k":1,"z":4.7,"s":3},{"n":"Belmopan","a":"Belize","i":"BZ","p":15220,"la":17.252,"lo":-88.767,"k":1,"z":5,"s":3},{"n":"Bangui","a":"Central African Republic","i":"CF","p":831925,"la":4.367,"lo":18.558,"k":1,"z":5,"s":3},{"n":"Yaoundé","a":"Cameroon","i":"CM","p":1611000,"la":3.869,"lo":11.515,"k":1,"z":5,"s":3},{"n":"Tirana","a":"Albania","i":"AL","p":895350,"la":41.328,"lo":19.819,"k":1,"z":5,"s":3},{"n":"Yerevan","a":"Armenia","i":"AM","p":1102000,"la":40.183,"lo":44.512,"k":1,"z":5,"s":3},{"n":"Baku","a":"Azerbaijan","i":"AZ","p":2122300,"la":40.397,"lo":49.86,"k":1,"z":5,"s":3},{"n":"Phnom Penh","a":"Cambodia","i":"KH","p":1466000,"la":11.552,"lo":104.915,"k":1,"z":5,"s":3},{"n":"La Paz","a":"Bolivia","i":"BO","p":1590000,"la":-16.496,"lo":-68.152,"k":1,"z":4,"s":3},{"n":"Cotonou","a":"Benin","i":"BJ","p":762000,"la":6.363,"lo":2.404,"k":1,"z":5,"s":3},{"n":"Sofia","a":"Bulgaria","i":"BG","p":1185000,"la":42.685,"lo":23.315,"k":1,"z":5,"s":3},{"n":"Minsk","a":"Belarus","i":"BY","p":1805000,"la":53.902,"lo":27.565,"k":1,"z":5,"s":3},{"n":"Thimphu","a":"Bhutan","i":"BT","p":98676,"la":27.473,"lo":89.639,"k":1,"z":5,"s":3},{"n":"Gaborone","a":"Botswana","i":"BW","p":208411,"la":-24.646,"lo":25.912,"k":1,"z":5,"s":3},{"n":"Canberra","a":"Australia","i":"AU","p":327700,"la":-35.283,"lo":149.129,"k":1,"z":4,"s":3},{"n":"Ouagadougou","a":"Burkina Faso","i":"BF","p":1149000,"la":12.372,"lo":-1.527,"k":1,"z":5,"s":3},{"n":"Sarajevo","a":"Bosnia and Herzegovina","i":"BA","p":696731,"la":43.85,"lo":18.383,"k":1,"z":5,"s":3},{"n":"Naypyidaw","a":"Myanmar","i":"MM","p":930000,"la":19.769,"lo":96.117,"k":1,"z":5,"s":3},{"n":"Nuku'alofa","a":"Tonga","i":"TO","p":42620,"la":-21.139,"lo":-175.221,"k":1,"z":5,"s":3},{"n":"Hargeisa","a":"Somaliland","i":"-99","p":477876,"la":9.56,"lo":44.065,"k":1,"z":5,"s":3},{"n":"Victoria","a":"Seychelles","i":"SC","p":33576,"la":-4.617,"lo":55.45,"k":1,"z":5,"s":3},{"n":"São Tomé","a":"Sao Tome and Principe","i":"ST","p":88219,"la":0.337,"lo":6.73,"k":1,"z":5,"s":3},{"n":"Apia","a":"Samoa","i":"WS","p":61916,"la":-13.836,"lo":-171.769,"k":1,"z":5,"s":3},{"n":"Valletta","a":"Malta","i":"MT","p":368250,"la":35.9,"lo":14.515,"k":1,"z":5,"s":3},{"n":"Malé","a":"Maldives","i":"MV","p":112927,"la":4.172,"lo":73.509,"k":1,"z":5,"s":3},{"n":"Jerusalem","a":"Israel","i":"IL","p":1029300,"la":31.778,"lo":35.207,"k":1,"z":3.7,"s":3},{"n":"Praia","a":"Cape Verde","i":"CV","p":113364,"la":14.917,"lo":-23.517,"k":1,"z":5,"s":3},{"n":"Nassau","a":"The Bahamas","i":"BS","p":227940,"la":25.083,"lo":-77.35,"k":1,"z":5,"s":3},{"n":"Nicosia","a":"Cyprus","i":"CY","p":224300,"la":35.167,"lo":33.367,"k":1,"z":5,"s":3},{"n":"Wellington","a":"New Zealand","i":"NZ","p":393400,"la":-41.292,"lo":174.777,"k":1,"z":4,"s":3},{"n":"Hanoi","a":"Vietnam","i":"VN","p":4378000,"la":21.035,"lo":105.848,"k":1,"z":4,"s":2},{"n":"Ankara","a":"Turkey","i":"TR","p":3716000,"la":39.929,"lo":32.862,"k":1,"z":4,"s":2},{"n":"Budapest","a":"Hungary","i":"HU","p":1679000,"la":47.502,"lo":19.081,"k":1,"z":4.7,"s":2},{"n":"Sanaa","a":"Yemen","i":"YE","p":2008000,"la":15.357,"lo":44.205,"k":1,"z":4,"s":2},{"n":"Bucharest","a":"Romania","i":"RO","p":1942000,"la":44.435,"lo":26.098,"k":1,"z":4,"s":2},{"n":"Damascus","a":"Syria","i":"SY","p":2466000,"la":33.502,"lo":36.298,"k":1,"z":4.7,"s":2},{"n":"Lisbon","a":"Portugal","i":"PT","p":2812000,"la":38.725,"lo":-9.147,"k":1,"z":4,"s":2},{"n":"Khartoum","a":"Sudan","i":"SD","p":4754000,"la":15.59,"lo":32.532,"k":1,"z":4,"s":2},{"n":"Oslo","a":"Norway","i":"NO","p":835000,"la":59.919,"lo":10.748,"k":1,"z":4,"s":2},{"n":"Warsaw","a":"Poland","i":"PL","p":1707000,"la":52.231,"lo":21.005,"k":1,"z":4,"s":2},{"n":"Pyongyang","a":"North Korea","i":"KP","p":3300000,"la":39.021,"lo":125.753,"k":1,"z":4.7,"s":2},{"n":"Dar es Salaam","a":"Tanzania","i":"TZ","p":2930000,"la":-6.798,"lo":39.266,"k":1,"z":4,"s":2},{"n":"Dublin","a":"Ireland","i":"IE","p":1059000,"la":53.347,"lo":-6.257,"k":1,"z":4,"s":2},{"n":"Monrovia","a":"Liberia","i":"LR","p":1041000,"la":6.315,"lo":-10.8,"k":1,"z":4,"s":2},{"n":"Kuala Lumpur","a":"Malaysia","i":"MY","p":1448000,"la":3.14,"lo":101.689,"k":1,"z":4,"s":2},{"n":"Havana","a":"Cuba","i":"CU","p":2174000,"la":23.134,"lo":-82.366,"k":1,"z":4,"s":2},{"n":"Prague","a":"Czechia","i":"CZ","p":1162000,"la":50.087,"lo":14.423,"k":1,"z":4.7,"s":2},{"n":"Kuwait City","a":"Kuwait","i":"KW","p":2063000,"la":29.372,"lo":47.976,"k":1,"z":4,"s":2},{"n":"Santo Domingo","a":"Dominican Republic","i":"DO","p":2154000,"la":18.471,"lo":-69.93,"k":1,"z":4,"s":2},{"n":"Accra","a":"Ghana","i":"GH","p":2121000,"la":5.552,"lo":-0.219,"k":1,"z":4,"s":2},{"n":"Tripoli","a":"Libya","i":"LY","p":2189000,"la":32.893,"lo":13.18,"k":1,"z":4,"s":2},{"n":"Tel Aviv","a":"Israel","i":"IL","p":3112000,"la":32.082,"lo":34.768,"k":0,"z":4,"s":2},{"n":"Helsinki","a":"Finland","i":"FI","p":1115000,"la":60.164,"lo":24.932,"k":1,"z":4,"s":2},{"n":"København","a":"Denmark","i":"DK","p":1085000,"la":55.681,"lo":12.562,"k":1,"z":4,"s":2},{"n":"Abidjan","a":"Ivory Coast","i":"CI","p":3802000,"la":5.323,"lo":-4.02,"k":1,"z":4,"s":2},{"n":"Brasília","a":"Brazil","i":"BR","p":3716996,"la":-15.781,"lo":-47.918,"k":1,"z":3.7,"s":2},{"n":"Brussels","a":"Belgium","i":"BE","p":1743000,"la":50.835,"lo":4.331,"k":1,"z":4.7,"s":2},{"n":"Dhaka","a":"Bangladesh","i":"BD","p":12797394,"la":23.725,"lo":90.407,"k":1,"z":4,"s":2},{"n":"Luanda","a":"Angola","i":"AO","p":5172900,"la":-8.836,"lo":13.232,"k":1,"z":4,"s":2},{"n":"Algiers","a":"Algeria","i":"DZ","p":3354000,"la":36.765,"lo":3.049,"k":1,"z":4,"s":2},{"n":"Yangon","a":"Myanmar","i":"MM","p":4088000,"la":16.785,"lo":96.165,"k":0,"z":4,"s":2},{"n":"San Francisco","a":"United States of America","i":"US","p":3450000,"la":37.784,"lo":-122.4,"k":0,"z":2.7,"s":1},{"n":"Denver","a":"United States of America","i":"US","p":2313000,"la":39.741,"lo":-104.986,"k":0,"z":3.7,"s":1},{"n":"Houston","a":"United States of America","i":"US","p":4459000,"la":29.741,"lo":-95.348,"k":0,"z":3,"s":1},{"n":"Miami","a":"United States of America","i":"US","p":5585000,"la":25.79,"lo":-80.226,"k":0,"z":2.1,"s":1},{"n":"Atlanta","a":"United States of America","i":"US","p":4506000,"la":33.739,"lo":-84.368,"k":0,"z":3,"s":1},{"n":"Chicago","a":"United States of America","i":"US","p":8990000,"la":41.848,"lo":-87.635,"k":0,"z":3,"s":1},{"n":"Caracas","a":"Venezuela","i":"VE","p":2985000,"la":10.503,"lo":-66.919,"k":1,"z":3,"s":1},{"n":"Kyiv","a":"Ukraine","i":"UA","p":2709000,"la":50.435,"lo":30.515,"k":1,"z":3.7,"s":1},{"n":"Dubai","a":"United Arab Emirates","i":"AE","p":1379000,"la":25.215,"lo":55.287,"k":0,"z":3.7,"s":1},{"n":"Tashkent","a":"Uzbekistan","i":"UZ","p":2184000,"la":41.304,"lo":69.269,"k":1,"z":3.7,"s":1},{"n":"Madrid","a":"Spain","i":"ES","p":5567000,"la":40.402,"lo":-3.685,"k":1,"z":3,"s":1},{"n":"Geneva","a":"Switzerland","i":"CH","p":1240000,"la":46.21,"lo":6.14,"k":0,"z":3.7,"s":1},{"n":"Stockholm","a":"Sweden","i":"SE","p":1264000,"la":59.324,"lo":18.066,"k":1,"z":3,"s":1},{"n":"Bangkok","a":"Thailand","i":"TH","p":6704000,"la":13.752,"lo":100.515,"k":1,"z":3,"s":1},{"n":"Lima","a":"Peru","i":"PE","p":8012000,"la":-12.046,"lo":-77.052,"k":1,"z":3,"s":1},{"n":"Dakar","a":"Senegal","i":"SN","p":2604000,"la":14.718,"lo":-17.475,"k":1,"z":3,"s":1},{"n":"Johannesburg","a":"South Africa","i":"ZA","p":3435000,"la":-26.168,"lo":28.028,"k":0,"z":3,"s":1},{"n":"Amsterdam","a":"Netherlands","i":"NL","p":1031000,"la":52.352,"lo":4.915,"k":1,"z":3.7,"s":1},{"n":"Casablanca","a":"Morocco","i":"MA","p":3181000,"la":33.602,"lo":-7.618,"k":0,"z":3.7,"s":1},{"n":"Seoul","a":"South Korea","i":"KR","p":9796000,"la":37.568,"lo":126.998,"k":1,"z":3,"s":1},{"n":"Manila","a":"Philippines","i":"PH","p":11100000,"la":14.606,"lo":120.98,"k":1,"z":2.7,"s":1},{"n":"Monterrey","a":"Mexico","i":"MX","p":3712000,"la":25.672,"lo":-100.332,"k":0,"z":3.7,"s":1},{"n":"Berlin","a":"Germany","i":"DE","p":3406000,"la":52.524,"lo":13.4,"k":1,"z":2.7,"s":1},{"n":"Ürümqi","a":"China","i":"CN","p":3575000,"la":43.807,"lo":87.573,"k":0,"z":3.7,"s":1},{"n":"Chengdu","a":"China","i":"CN","p":4123000,"la":30.672,"lo":104.068,"k":0,"z":3,"s":1},{"n":"Ōsaka","a":"Japan","i":"JP","p":11294000,"la":34.691,"lo":135.504,"k":0,"z":3,"s":1},{"n":"Kinshasa","a":"Congo (Kinshasa)","i":"CD","p":7843000,"la":-4.328,"lo":15.313,"k":1,"z":3,"s":1},{"n":"New Delhi","a":"India","i":"IN","p":317797,"la":28.6,"lo":77.2,"k":1,"z":2.1,"s":1},{"n":"Bengaluru","a":"India","i":"IN","p":6787000,"la":12.972,"lo":77.558,"k":0,"z":3.7,"s":1},{"n":"Athens","a":"Greece","i":"GR","p":3242000,"la":37.985,"lo":23.731,"k":1,"z":3.7,"s":1},{"n":"Baghdad","a":"Iraq","i":"IQ","p":5054000,"la":33.341,"lo":44.392,"k":1,"z":3.7,"s":1},{"n":"Addis Ababa","a":"Ethiopia","i":"ET","p":3100000,"la":9.035,"lo":38.698,"k":1,"z":3,"s":1},{"n":"Tehran","a":"Iran","i":"IR","p":7873000,"la":35.674,"lo":51.422,"k":1,"z":3,"s":1},{"n":"Vancouver","a":"Canada","i":"CA","p":2313328,"la":49.275,"lo":-123.124,"k":0,"z":2.1,"s":1},{"n":"Toronto","a":"Canada","i":"CA","p":5213000,"la":43.665,"lo":-79.389,"k":0,"z":2.1,"s":1},{"n":"Buenos Aires","a":"Argentina","i":"AR","p":12795000,"la":-34.611,"lo":-58.433,"k":1,"z":2.1,"s":1},{"n":"Kabul","a":"Afghanistan","i":"AF","p":3277000,"la":34.519,"lo":69.181,"k":1,"z":3.7,"s":1},{"n":"Vienna","a":"Austria","i":"AT","p":2400000,"la":48.202,"lo":16.365,"k":1,"z":3.7,"s":1},{"n":"Melbourne","a":"Australia","i":"AU","p":4170000,"la":-37.818,"lo":144.973,"k":0,"z":3,"s":1},{"n":"Taipei","a":"Taiwan","i":"TW","p":6900273,"la":25.036,"lo":121.568,"k":1,"z":3.7,"s":1},{"n":"Auckland","a":"New Zealand","i":"NZ","p":1377200,"la":-36.848,"lo":174.763,"k":0,"z":3,"s":1},{"n":"Los Angeles","a":"United States of America","i":"US","p":12500000,"la":34.049,"lo":-118.232,"k":0,"z":2,"s":0},{"n":"Washington, D.C.","a":"United States of America","i":"US","p":4338000,"la":38.901,"lo":-77.011,"k":1,"z":2.1,"s":0},{"n":"New York","a":"United States of America","i":"US","p":19040000,"la":40.722,"lo":-73.996,"k":0,"z":1.7,"s":0},{"n":"London","a":"United Kingdom","i":"GB","p":8567000,"la":51.502,"lo":-0.119,"k":1,"z":1.7,"s":0},{"n":"Istanbul","a":"Turkey","i":"TR","p":10061000,"la":41.018,"lo":28.974,"k":0,"z":3,"s":0},{"n":"Riyadh","a":"Saudi Arabia","i":"SA","p":4465000,"la":24.634,"lo":46.72,"k":1,"z":3.7,"s":0},{"n":"Cape Town","a":"South Africa","i":"ZA","p":3215000,"la":-33.918,"lo":18.433,"k":1,"z":1.7,"s":0},{"n":"Moscow","a":"Russia","i":"RU","p":10452000,"la":55.754,"lo":37.614,"k":1,"z":1.7,"s":0},{"n":"Mexico City","a":"Mexico","i":"MX","p":19028000,"la":19.444,"lo":-99.133,"k":1,"z":2.1,"s":0},{"n":"Lagos","a":"Nigeria","i":"NG","p":9466000,"la":6.445,"lo":3.39,"k":0,"z":2,"s":0},{"n":"Rome","a":"Italy","i":"IT","p":3339000,"la":41.898,"lo":12.481,"k":1,"z":2.7,"s":0},{"n":"Beijing","a":"China","i":"CN","p":11106000,"la":39.902,"lo":116.394,"k":1,"z":1.7,"s":0},{"n":"Nairobi","a":"Kenya","i":"KE","p":3010000,"la":-1.281,"lo":36.815,"k":1,"z":2,"s":0},{"n":"Jakarta","a":"Indonesia","i":"ID","p":9125000,"la":-6.172,"lo":106.827,"k":1,"z":2.1,"s":0},{"n":"Bogota","a":"Colombia","i":"CO","p":7772000,"la":4.598,"lo":-74.085,"k":1,"z":2.1,"s":0},{"n":"Cairo","a":"Egypt","i":"EG","p":11893000,"la":30.052,"lo":31.248,"k":1,"z":1.7,"s":0},{"n":"Shanghai","a":"China","i":"CN","p":14987000,"la":31.218,"lo":121.435,"k":0,"z":2,"s":0},{"n":"Tokyo","a":"Japan","i":"JP","p":35676000,"la":35.687,"lo":139.749,"k":1,"z":1.7,"s":0},{"n":"Mumbai","a":"India","i":"IN","p":18978000,"la":19.068,"lo":72.876,"k":0,"z":1.7,"s":0},{"n":"Paris","a":"France","i":"FR","p":9904000,"la":48.858,"lo":2.353,"k":1,"z":1.7,"s":0},{"n":"Santiago","a":"Chile","i":"CL","p":5720000,"la":-33.44,"lo":-70.651,"k":1,"z":2,"s":0},{"n":"Kolkata","a":"India","i":"IN","p":14787000,"la":22.57,"lo":88.369,"k":0,"z":3.7,"s":0},{"n":"Rio de Janeiro","a":"Brazil","i":"BR","p":11748000,"la":-22.907,"lo":-43.212,"k":0,"z":1.7,"s":0},{"n":"São Paulo","a":"Brazil","i":"BR","p":18845000,"la":-23.557,"lo":-46.627,"k":0,"z":3,"s":0},{"n":"Sydney","a":"Australia","i":"AU","p":4630000,"la":-33.871,"lo":151.213,"k":0,"z":1.7,"s":0},{"n":"Singapore","a":"Singapore","i":"SG","p":5183700,"la":1.295,"lo":103.854,"k":1,"z":2.1,"s":0},{"n":"Hong Kong","a":"Hong Kong S.A.R.","i":"HK","p":7206000,"la":22.307,"lo":114.183,"k":0,"z":3,"s":0}]} \ No newline at end of file diff --git a/src/maps/data/places_50m.json b/src/maps/data/places_50m.json new file mode 100644 index 0000000..ba16935 --- /dev/null +++ b/src/maps/data/places_50m.json @@ -0,0 +1 @@ +{"f":[{"n":"Bombo","a":"Uganda","i":"UG","p":75000,"la":0.583,"lo":32.533,"k":0,"z":7,"s":10},{"n":"Fort Portal","a":"Uganda","i":"UG","p":42670,"la":0.671,"lo":30.275,"k":0,"z":7,"s":10},{"n":"Potenza","a":"Italy","i":"IT","p":69060,"la":40.642,"lo":15.799,"k":0,"z":7,"s":10},{"n":"Campobasso","a":"Italy","i":"IT","p":50762,"la":41.563,"lo":14.656,"k":0,"z":7,"s":10},{"n":"Aosta","a":"Italy","i":"IT","p":34062,"la":45.737,"lo":7.315,"k":0,"z":7,"s":10},{"n":"Mariehamn","a":"Aland","i":"AX","p":10682,"la":60.097,"lo":19.949,"k":0,"z":7,"s":10},{"n":"Ramallah","a":"Palestine","i":"PS","p":24599,"la":31.903,"lo":35.206,"k":0,"z":9,"s":10},{"n":"Vatican City","a":"Vatican","i":"VA","p":832,"la":41.903,"lo":12.453,"k":1,"z":7,"s":8},{"n":"Poitier","a":"France","i":"FR","p":85960,"la":46.583,"lo":0.333,"k":0,"z":6.7,"s":8},{"n":"Clermont-Ferrand","a":"France","i":"FR","p":233050,"la":45.78,"lo":3.08,"k":0,"z":6,"s":8},{"n":"Besançon","a":"France","i":"FR","p":128426,"la":47.23,"lo":6.03,"k":0,"z":7,"s":8},{"n":"Artigas Base","a":"Antarctica","i":"AQ","p":60,"la":-62.174,"lo":-58.864,"k":0,"z":7,"s":8},{"n":"Chipata","a":"Zambia","i":"ZM","p":85963,"la":-13.63,"lo":32.64,"k":0,"z":6.7,"s":7},{"n":"Jinja","a":"Uganda","i":"UG","p":301619,"la":0.44,"lo":33.2,"k":0,"z":6.7,"s":7},{"n":"Arua","a":"Uganda","i":"UG","p":250000,"la":3.02,"lo":30.9,"k":0,"z":6.7,"s":7},{"n":"Mbale","a":"Uganda","i":"UG","p":402368,"la":1.09,"lo":34.17,"k":0,"z":6.7,"s":7},{"n":"Moroto","a":"Uganda","i":"UG","p":371,"la":2.54,"lo":34.64,"k":0,"z":6.7,"s":7},{"n":"Masaka","a":"Uganda","i":"UG","p":65373,"la":-0.33,"lo":31.73,"k":0,"z":6.7,"s":7},{"n":"Mbarara","a":"Uganda","i":"UG","p":83700,"la":-0.6,"lo":30.65,"k":0,"z":6.7,"s":7},{"n":"Otjiwarongo","a":"Namibia","i":"NA","p":24815,"la":-20.46,"lo":16.64,"k":0,"z":6.7,"s":7},{"n":"Bologna","a":"Italy","i":"IT","p":488172,"la":44.5,"lo":11.34,"k":0,"z":6.1,"s":7},{"n":"Cagliari","a":"Italy","i":"IT","p":291511,"la":39.222,"lo":9.104,"k":0,"z":6.1,"s":7},{"n":"Catanzaro","a":"Italy","i":"IT","p":95251,"la":38.9,"lo":16.6,"k":0,"z":6.7,"s":7},{"n":"Bari","a":"Italy","i":"IT","p":500577,"la":41.114,"lo":16.873,"k":0,"z":5.1,"s":7},{"n":"L'Aquila","a":"Italy","i":"IT","p":68503,"la":42.35,"lo":13.39,"k":0,"z":6.7,"s":7},{"n":"Ancona","a":"Italy","i":"IT","p":100507,"la":43.6,"lo":13.5,"k":0,"z":6.1,"s":7},{"n":"Perugia","a":"Italy","i":"IT","p":149125,"la":43.11,"lo":12.39,"k":0,"z":6.7,"s":7},{"n":"Trieste","a":"Italy","i":"IT","p":216035,"la":45.65,"lo":13.8,"k":0,"z":6.7,"s":7},{"n":"Trento","a":"Italy","i":"IT","p":107808,"la":46.08,"lo":11.12,"k":0,"z":6.1,"s":7},{"n":"Fort-de-France","a":"France","i":"MQ","p":253995,"la":14.61,"lo":-61.08,"k":0,"z":6.7,"s":7},{"n":"Gifu","a":"Japan","i":"JP","p":412895,"la":35.423,"lo":136.763,"k":0,"z":6.1,"s":7},{"n":"Saint-Laurent-du-Maroni","a":"France","i":"GF","p":24287,"la":5.498,"lo":-54.033,"k":0,"z":6.7,"s":7},{"n":"Caen","a":"France","i":"FR","p":190099,"la":49.184,"lo":-0.35,"k":0,"z":6.7,"s":7},{"n":"Nantes","a":"France","i":"FR","p":438537,"la":47.21,"lo":-1.59,"k":0,"z":5.1,"s":7},{"n":"Ajaccio","a":"France","i":"FR","p":54364,"la":41.927,"lo":8.728,"k":0,"z":6.1,"s":7},{"n":"Montpellier","a":"France","i":"FR","p":327254,"la":43.61,"lo":3.87,"k":0,"z":6.1,"s":7},{"n":"Dijon","a":"France","i":"FR","p":169946,"la":47.33,"lo":5.03,"k":0,"z":6,"s":7},{"n":"Orléans","a":"France","i":"FR","p":217301,"la":47.9,"lo":1.9,"k":0,"z":6.1,"s":7},{"n":"Rouen","a":"France","i":"FR","p":532559,"la":49.43,"lo":1.08,"k":0,"z":6,"s":7},{"n":"Reims","a":"France","i":"FR","p":196565,"la":49.25,"lo":4.03,"k":0,"z":6.1,"s":7},{"n":"Amiens","a":"France","i":"FR","p":143086,"la":49.9,"lo":2.3,"k":0,"z":6.7,"s":7},{"n":"Nancy","a":"France","i":"FR","p":268976,"la":48.684,"lo":6.2,"k":0,"z":5.1,"s":7},{"n":"Basse-terre","a":"France","i":"GP","p":307,"la":16.01,"lo":-61.705,"k":0,"z":6.7,"s":7},{"n":"Novi Sad","a":"Serbia","i":"RS","p":225457,"la":45.25,"lo":19.85,"k":0,"z":6.1,"s":7},{"n":"Banja Luka","a":"Bosnia and Herzegovina","i":"BA","p":221738,"la":44.78,"lo":17.18,"k":0,"z":6.1,"s":7},{"n":"George Town","a":"Cayman Islands","i":"KY","p":4268,"la":19.295,"lo":-81.372,"k":0,"z":6.7,"s":7},{"n":"Grand Turk","a":"Turks and Caicos Islands","i":"TC","p":5801,"la":21.466,"lo":-71.136,"k":0,"z":6.7,"s":7},{"n":"Douglas","a":"Isle of Man","i":"IM","p":35854,"la":54.149,"lo":-4.48,"k":0,"z":6.7,"s":7},{"n":"San Marino","a":"San Marino","i":"SM","p":29579,"la":43.936,"lo":12.442,"k":1,"z":6.1,"s":7},{"n":"Willemstad","a":"Curacao","i":"CW","p":146813,"la":12.112,"lo":-68.872,"k":0,"z":6.7,"s":7},{"n":"Oranjestad","a":"Aruba","i":"AW","p":68775,"la":12.522,"lo":-70.026,"k":0,"z":6.7,"s":7},{"n":"Vaduz","a":"Liechtenstein","i":"LI","p":36281,"la":47.134,"lo":9.517,"k":1,"z":6.7,"s":7},{"n":"Capitan Arturo Prat Station","a":"Antarctica","i":"AQ","p":41,"la":-62.5,"lo":-59.683,"k":0,"z":6.1,"s":7},{"n":"Marambio Base","a":"Antarctica","i":"AQ","p":150,"la":-64.233,"lo":-56.65,"k":0,"z":5,"s":7},{"n":"Zucchelli Station","a":"Antarctica","i":"AQ","p":80,"la":-74.619,"lo":164.219,"k":0,"z":6.7,"s":7},{"n":"Rothera Station","a":"Antarctica","i":"AQ","p":130,"la":-67.565,"lo":-68.124,"k":0,"z":4,"s":7},{"n":"Palmer Station","a":"Antarctica","i":"AQ","p":46,"la":-64.762,"lo":-64.047,"k":0,"z":4,"s":7},{"n":"Base Presidente Montalva","a":"Antarctica","i":"AQ","p":150,"la":-62.183,"lo":-58.908,"k":0,"z":5,"s":7},{"n":"Carlini Base","a":"Antarctica","i":"AQ","p":60,"la":-62.224,"lo":-58.65,"k":0,"z":6.1,"s":7},{"n":"King Sejong Station","a":"Antarctica","i":"AQ","p":90,"la":-62.225,"lo":-58.772,"k":0,"z":6.1,"s":7},{"n":"Great Wall Station","a":"Antarctica","i":"AQ","p":40,"la":-62.216,"lo":-58.967,"k":0,"z":5,"s":7},{"n":"Escudero Base","a":"Antarctica","i":"AQ","p":20,"la":-62.195,"lo":-58.953,"k":0,"z":6.1,"s":7},{"n":"Elephant Island","a":"Antarctica","i":"AQ","p":6,"la":-62,"lo":-58,"k":0,"z":6.1,"s":7},{"n":"Scott Base","a":"Antarctica","i":"AQ","p":85,"la":-77.847,"lo":166.749,"k":0,"z":6.7,"s":7},{"n":"McMurdo Station","a":"Antarctica","i":"AQ","p":1000,"la":-77.732,"lo":166.869,"k":0,"z":3,"s":7},{"n":"Zhongshan Station","a":"Antarctica","i":"AQ","p":60,"la":-69.434,"lo":76.339,"k":0,"z":6.1,"s":7},{"n":"Vostok Station","a":"Antarctica","i":"AQ","p":25,"la":-78.466,"lo":106.8,"k":0,"z":6.7,"s":7},{"n":"Peter I Island","a":"Antarctica","i":"AQ","p":1,"la":-68.795,"lo":-90.584,"k":0,"z":6.1,"s":7},{"n":"Mirny Station","a":"Antarctica","i":"AQ","p":169,"la":-66.576,"lo":93.006,"k":0,"z":3,"s":7},{"n":"Mawson Station","a":"Antarctica","i":"AQ","p":60,"la":-67.615,"lo":62.875,"k":0,"z":6.1,"s":7},{"n":"Davis Station","a":"Antarctica","i":"AQ","p":70,"la":-68.778,"lo":78.141,"k":0,"z":6.1,"s":7},{"n":"Concordia Research Station","a":"Antarctica","i":"AQ","p":30,"la":-74.666,"lo":124.167,"k":0,"z":6.7,"s":7},{"n":"Casey Station","a":"Antarctica","i":"AQ","p":200,"la":-66.286,"lo":110.536,"k":0,"z":3,"s":7},{"n":"Amundsen–Scott South Pole Station","a":"Antarctica","i":"AQ","p":200,"la":-90,"lo":176.994,"k":0,"z":5,"s":7},{"n":"Wasa Station","a":"Antarctica","i":"AQ","p":10,"la":-73.05,"lo":-13.417,"k":0,"z":6.1,"s":7},{"n":"Troll Station","a":"Antarctica","i":"AQ","p":44,"la":-72.016,"lo":2.533,"k":0,"z":6.1,"s":7},{"n":"Svea Station","a":"Antarctica","i":"AQ","p":10,"la":-74.583,"lo":-11.217,"k":0,"z":6.1,"s":7},{"n":"Novolazarevskaya Station","a":"Antarctica","i":"AQ","p":70,"la":-71.301,"lo":-11.85,"k":0,"z":6.1,"s":7},{"n":"Neumayer Station III","a":"Antarctica","i":"AQ","p":40,"la":-70.89,"lo":-7.838,"k":0,"z":6.1,"s":7},{"n":"Maitri Station","a":"Antarctica","i":"AQ","p":65,"la":-70.782,"lo":11.73,"k":0,"z":6.1,"s":7},{"n":"Halley Station","a":"Antarctica","i":"AQ","p":70,"la":-76.092,"lo":-26.474,"k":0,"z":6.1,"s":7},{"n":"Belgrano II Base","a":"Antarctica","i":"AQ","p":100,"la":-77.866,"lo":-34.617,"k":0,"z":6.1,"s":7},{"n":"Sobral Base","a":"Antarctica","i":"AQ","p":40,"la":-81.179,"lo":-40.502,"k":0,"z":6.1,"s":7},{"n":"Aboa Station","a":"Antarctica","i":"AQ","p":18,"la":-75.05,"lo":-13.417,"k":0,"z":6.1,"s":7},{"n":"San Martín Base","a":"Antarctica","i":"AQ","p":20,"la":-68.116,"lo":-67.1,"k":0,"z":6.1,"s":7},{"n":"Gen. O'Higgins Base","a":"Antarctica","i":"AQ","p":44,"la":-63.328,"lo":-57.89,"k":0,"z":6.1,"s":7},{"n":"Esperanza Base","a":"Antarctica","i":"AQ","p":250,"la":-63.375,"lo":-57.026,"k":0,"z":3,"s":7},{"n":"Orcadas Station","a":"Antarctica","i":"AQ","p":45,"la":-60.733,"lo":-44.733,"k":0,"z":6.1,"s":7},{"n":"Signy Research Station","a":"Antarctica","i":"AQ","p":8,"la":-60.631,"lo":-45.601,"k":0,"z":6.1,"s":7},{"n":"Dumont d'Urville Station","a":"Antarctica","i":"AQ","p":120,"la":-66.775,"lo":139.964,"k":0,"z":4,"s":7},{"n":"Showa Station","a":"Antarctica","i":"AQ","p":60,"la":-69.03,"lo":39.746,"k":0,"z":6.1,"s":7},{"n":"Gibraltar","a":"Gibraltar","i":"GI","p":187083,"la":36.128,"lo":-5.346,"k":0,"z":5.1,"s":6},{"n":"Lobamba","a":"eSwatini","i":"SZ","p":9782,"la":-26.467,"lo":31.2,"k":0,"z":6,"s":6},{"n":"Edinburgh","a":"United Kingdom","i":"GB","p":504966,"la":55.948,"lo":-3.219,"k":0,"z":5.1,"s":6},{"n":"Cardiff","a":"United Kingdom","i":"GB","p":861400,"la":51.483,"lo":-3.168,"k":0,"z":5.1,"s":6},{"n":"Longyearbyen","a":"Svalbard and Jan Mayen Islands","i":"SJ","p":1232,"la":78.221,"lo":15.621,"k":0,"z":6,"s":6},{"n":"Luxembourg","a":"Luxembourg","i":"LU","p":107260,"la":49.612,"lo":6.13,"k":1,"z":6,"s":6},{"n":"Turin","a":"Italy","i":"IT","p":1652000,"la":45.072,"lo":7.668,"k":0,"z":6,"s":6},{"n":"Nouméa","a":"New Caledonia","i":"NC","p":93060,"la":-22.263,"lo":166.444,"k":0,"z":6,"s":6},{"n":"Matsuyama","a":"Japan","i":"JP","p":533541,"la":33.846,"lo":132.766,"k":0,"z":6,"s":6},{"n":"Rennes","a":"France","i":"FR","p":209375,"la":48.1,"lo":-1.67,"k":0,"z":6,"s":6},{"n":"Toulouse","a":"France","i":"FR","p":847000,"la":43.622,"lo":1.448,"k":0,"z":6,"s":6},{"n":"Limoges","a":"France","i":"FR","p":152199,"la":45.83,"lo":1.25,"k":0,"z":6.7,"s":6},{"n":"Lille","a":"France","i":"FR","p":1044000,"la":50.652,"lo":3.078,"k":0,"z":6,"s":6},{"n":"Strasbourg","a":"France","i":"FR","p":439972,"la":48.58,"lo":7.75,"k":0,"z":6,"s":6},{"n":"Batumi","a":"Georgia","i":"GE","p":155542,"la":41.6,"lo":41.63,"k":0,"z":6,"s":6},{"n":"Funchal","a":"Portugal","i":"PT","p":204767,"la":32.65,"lo":-16.88,"k":0,"z":6,"s":6},{"n":"El Fasher","a":"Sudan","i":"SD","p":252609,"la":13.63,"lo":25.35,"k":0,"z":6,"s":6},{"n":"Keetmanshoop","a":"Namibia","i":"NA","p":18039,"la":-26.574,"lo":18.13,"k":0,"z":6,"s":6},{"n":"Swakopmund","a":"Namibia","i":"NA","p":29491,"la":-22.669,"lo":14.535,"k":0,"z":6,"s":6},{"n":"Genoa","a":"Italy","i":"IT","p":647497,"la":44.41,"lo":8.93,"k":0,"z":5.1,"s":6},{"n":"Sukhumi","a":"Georgia","i":"GE","p":81546,"la":43.02,"lo":41.02,"k":0,"z":6,"s":6},{"n":"Palikir","a":"Federated States of Micronesia","i":"FM","p":4645,"la":6.917,"lo":158.15,"k":1,"z":6,"s":6},{"n":"Majuro","a":"Marshall Islands","i":"MH","p":25400,"la":7.103,"lo":171.38,"k":1,"z":6,"s":6},{"n":"Agana","a":"Guam","i":"GU","p":122411,"la":13.47,"lo":144.75,"k":0,"z":6,"s":6},{"n":"Funafuti","a":"Tuvalu","i":"TV","p":4749,"la":-8.517,"lo":179.217,"k":1,"z":6,"s":6},{"n":"Melekeok","a":"Palau","i":"PW","p":7026,"la":7.487,"lo":134.627,"k":1,"z":6,"s":6},{"n":"Bir Lehlou","a":"Western Sahara","i":"EH","p":500,"la":26.119,"lo":-9.653,"k":0,"z":6,"s":6},{"n":"Monaco","a":"Monaco","i":"MC","p":36371,"la":43.74,"lo":7.407,"k":1,"z":5.1,"s":6},{"n":"Tarawa","a":"Kiribati","i":"KI","p":28802,"la":1.338,"lo":173.018,"k":1,"z":6,"s":6},{"n":"Moroni","a":"Comoros","i":"KM","p":128698,"la":-11.704,"lo":43.24,"k":1,"z":6,"s":6},{"n":"Macau","a":"Macau S.A.R","i":"MO","p":568700,"la":22.189,"lo":113.544,"k":0,"z":6,"s":6},{"n":"Andorra","a":"Andorra","i":"AD","p":53998,"la":42.511,"lo":1.527,"k":1,"z":6,"s":6},{"n":"San Bernardino","a":"United States of America","i":"US","p":1745000,"la":34.122,"lo":-117.302,"k":0,"z":5.6,"s":4},{"n":"Bridgeport","a":"United States of America","i":"US","p":1018000,"la":41.182,"lo":-73.202,"k":0,"z":5.6,"s":4},{"n":"Rochester","a":"United States of America","i":"US","p":755000,"la":43.172,"lo":-77.622,"k":0,"z":5.1,"s":4},{"n":"Manchester","a":"United Kingdom","i":"GB","p":2230000,"la":53.475,"lo":-2.249,"k":0,"z":5,"s":4},{"n":"Gujranwala","a":"Pakistan","i":"PK","p":1513000,"la":32.162,"lo":74.183,"k":0,"z":5.6,"s":4},{"n":"Incheon","a":"South Korea","i":"KR","p":2550000,"la":37.478,"lo":126.64,"k":0,"z":5.6,"s":4},{"n":"Benin City","a":"Nigeria","i":"NG","p":1190000,"la":6.342,"lo":5.618,"k":0,"z":5.6,"s":4},{"n":"Xiamen","a":"China","i":"CN","p":2519000,"la":24.452,"lo":118.078,"k":0,"z":5.6,"s":4},{"n":"Nanchong","a":"China","i":"CN","p":2174000,"la":30.782,"lo":106.128,"k":0,"z":5.6,"s":4},{"n":"Neijiang","a":"China","i":"CN","p":1466000,"la":29.582,"lo":105.048,"k":0,"z":5.6,"s":4},{"n":"Nanyang","a":"China","i":"CN","p":1944000,"la":33.002,"lo":112.528,"k":0,"z":5.6,"s":4},{"n":"Jinxi","a":"China","i":"CN","p":2426000,"la":40.752,"lo":120.828,"k":0,"z":5.6,"s":4},{"n":"Yantai","a":"China","i":"CN","p":2116000,"la":37.532,"lo":121.398,"k":0,"z":5.6,"s":4},{"n":"Zaozhuang","a":"China","i":"CN","p":2145000,"la":34.882,"lo":117.568,"k":0,"z":5.1,"s":4},{"n":"Suzhou","a":"China","i":"CN","p":1650000,"la":31.302,"lo":120.618,"k":0,"z":5.6,"s":4},{"n":"Xuzhou","a":"China","i":"CN","p":2091000,"la":34.282,"lo":117.178,"k":0,"z":5.6,"s":4},{"n":"Wuxi","a":"China","i":"CN","p":1749000,"la":31.582,"lo":120.298,"k":0,"z":5.6,"s":4},{"n":"Jilin","a":"China","i":"CN","p":2396000,"la":43.852,"lo":126.548,"k":0,"z":5.6,"s":4},{"n":"Chandigarh","a":"India","i":"IN","p":979000,"la":30.722,"lo":76.778,"k":0,"z":5.6,"s":4},{"n":"Jammu","a":"India","i":"IN","p":791000,"la":32.714,"lo":74.845,"k":0,"z":5.6,"s":4},{"n":"Sholapur","a":"India","i":"IN","p":1057000,"la":17.672,"lo":75.898,"k":0,"z":5.6,"s":4},{"n":"Aurangabad","a":"India","i":"IN","p":1113000,"la":19.862,"lo":75.329,"k":0,"z":5.6,"s":4},{"n":"Nasik","a":"India","i":"IN","p":1473000,"la":20.002,"lo":73.778,"k":0,"z":5.6,"s":4},{"n":"Dispur","a":"India","i":"IN","p":16140,"la":26.144,"lo":91.767,"k":0,"z":5.6,"s":4},{"n":"Jullundur","a":"India","i":"IN","p":855000,"la":31.337,"lo":75.567,"k":0,"z":5.6,"s":4},{"n":"Allahabad","a":"India","i":"IN","p":1201000,"la":25.457,"lo":81.838,"k":0,"z":5.6,"s":4},{"n":"Moradabad","a":"India","i":"IN","p":787000,"la":28.844,"lo":78.755,"k":0,"z":5.6,"s":4},{"n":"Ghaziabad","a":"India","i":"IN","p":1341000,"la":28.662,"lo":77.406,"k":0,"z":5.6,"s":4},{"n":"Agra","a":"India","i":"IN","p":1592000,"la":27.172,"lo":78.013,"k":0,"z":5.6,"s":4},{"n":"Aligarh","a":"India","i":"IN","p":805000,"la":27.894,"lo":78.06,"k":0,"z":5.6,"s":4},{"n":"Meerut","a":"India","i":"IN","p":1398000,"la":29.002,"lo":77.698,"k":0,"z":5.6,"s":4},{"n":"Dhanbad","a":"India","i":"IN","p":1246000,"la":23.802,"lo":86.418,"k":0,"z":5.6,"s":4},{"n":"Gwalior","a":"India","i":"IN","p":978000,"la":26.2,"lo":78.169,"k":0,"z":5.6,"s":4},{"n":"Vadodara","a":"India","i":"IN","p":1756000,"la":22.312,"lo":73.178,"k":0,"z":5.6,"s":4},{"n":"Rajkot","a":"India","i":"IN","p":1260000,"la":22.312,"lo":70.798,"k":0,"z":5.6,"s":4},{"n":"Durazno","a":"Uruguay","i":"UY","p":34037,"la":-33.41,"lo":-56.51,"k":0,"z":5.6,"s":4},{"n":"International Falls","a":"United States of America","i":"US","p":15240,"la":48.601,"lo":-93.411,"k":0,"z":5.6,"s":4},{"n":"St. Paul","a":"United States of America","i":"US","p":734854,"la":44.944,"lo":-93.085,"k":0,"z":5.6,"s":4},{"n":"Billings","a":"United States of America","i":"US","p":104552,"la":45.788,"lo":-108.54,"k":0,"z":5.6,"s":4},{"n":"Great Falls","a":"United States of America","i":"US","p":66558,"la":47.5,"lo":-111.3,"k":0,"z":5.6,"s":4},{"n":"Missoula","a":"United States of America","i":"US","p":72856,"la":46.872,"lo":-113.993,"k":0,"z":5.6,"s":4},{"n":"Minot","a":"United States of America","i":"US","p":39439,"la":48.232,"lo":-101.296,"k":0,"z":5.6,"s":4},{"n":"Fargo","a":"United States of America","i":"US","p":162842,"la":46.877,"lo":-96.789,"k":0,"z":5.1,"s":4},{"n":"Hilo","a":"United States of America","i":"US","p":52391,"la":19.706,"lo":-155.082,"k":0,"z":5.1,"s":4},{"n":"Olympia","a":"United States of America","i":"US","p":156984,"la":47.038,"lo":-122.899,"k":0,"z":5.6,"s":4},{"n":"Spokane","a":"United States of America","i":"US","p":347705,"la":47.67,"lo":-117.42,"k":0,"z":5.1,"s":4},{"n":"Vancouver","a":"United States of America","i":"US","p":525802,"la":45.63,"lo":-122.64,"k":0,"z":5.6,"s":4},{"n":"Flagstaff","a":"United States of America","i":"US","p":63993,"la":35.198,"lo":-111.651,"k":0,"z":5.1,"s":4},{"n":"Tucson","a":"United States of America","i":"US","p":823000,"la":32.207,"lo":-110.892,"k":0,"z":5.1,"s":4},{"n":"Santa Barbara","a":"United States of America","i":"US","p":181632,"la":34.411,"lo":-119.717,"k":0,"z":5.6,"s":4},{"n":"Fresno","a":"United States of America","i":"US","p":616353,"la":36.748,"lo":-119.773,"k":0,"z":5.1,"s":4},{"n":"Eureka","a":"United States of America","i":"US","p":42398,"la":40.793,"lo":-124.15,"k":0,"z":5,"s":4},{"n":"Colorado Springs","a":"United States of America","i":"US","p":493654,"la":38.863,"lo":-104.792,"k":0,"z":5.6,"s":4},{"n":"Reno","a":"United States of America","i":"US","p":328585,"la":39.53,"lo":-119.82,"k":0,"z":5.1,"s":4},{"n":"Elko","a":"United States of America","i":"US","p":19252,"la":40.833,"lo":-115.762,"k":0,"z":5.6,"s":4},{"n":"Albuquerque","a":"United States of America","i":"US","p":898642,"la":35.105,"lo":-106.641,"k":0,"z":5.1,"s":4},{"n":"Salem","a":"United States of America","i":"US","p":229010,"la":44.928,"lo":-123.024,"k":0,"z":5.6,"s":4},{"n":"Casper","a":"United States of America","i":"US","p":60791,"la":42.847,"lo":-106.323,"k":0,"z":5.6,"s":4},{"n":"Topeka","a":"United States of America","i":"US","p":132091,"la":39.05,"lo":-95.67,"k":0,"z":5.1,"s":4},{"n":"Kansas City","a":"United States of America","i":"US","p":1469000,"la":39.109,"lo":-94.606,"k":0,"z":5.1,"s":4},{"n":"Tulsa","a":"United States of America","i":"US","p":946962,"la":36.12,"lo":-95.93,"k":0,"z":5.6,"s":4},{"n":"Sioux Falls","a":"United States of America","i":"US","p":155724,"la":43.55,"lo":-96.73,"k":0,"z":5.6,"s":4},{"n":"Shreveport","a":"United States of America","i":"US","p":248053,"la":32.5,"lo":-93.77,"k":0,"z":5.6,"s":4},{"n":"Baton Rouge","a":"United States of America","i":"US","p":422072,"la":30.458,"lo":-91.14,"k":0,"z":5.6,"s":4},{"n":"Ft. Worth","a":"United States of America","i":"US","p":1440454,"la":32.74,"lo":-97.34,"k":0,"z":5.1,"s":4},{"n":"Corpus Christi","a":"United States of America","i":"US","p":277454,"la":27.743,"lo":-97.402,"k":0,"z":5.6,"s":4},{"n":"Austin","a":"United States of America","i":"US","p":1161000,"la":30.269,"lo":-97.745,"k":0,"z":5.1,"s":4},{"n":"Amarillo","a":"United States of America","i":"US","p":181766,"la":35.23,"lo":-101.83,"k":0,"z":5.6,"s":4},{"n":"El Paso","a":"United States of America","i":"US","p":753000,"la":31.782,"lo":-106.512,"k":0,"z":5.1,"s":4},{"n":"Laredo","a":"United States of America","i":"US","p":434768,"la":27.506,"lo":-99.507,"k":0,"z":5.6,"s":4},{"n":"Mérida","a":"Venezuela","i":"VE","p":345489,"la":8.57,"lo":-71.183,"k":0,"z":5.6,"s":4},{"n":"Burlington","a":"United States of America","i":"US","p":93807,"la":44.476,"lo":-73.212,"k":0,"z":5.6,"s":4},{"n":"Montgomery","a":"United States of America","i":"US","p":198325,"la":32.362,"lo":-86.279,"k":0,"z":5.6,"s":4},{"n":"Tallahassee","a":"United States of America","i":"US","p":221222,"la":30.45,"lo":-84.28,"k":0,"z":5.6,"s":4},{"n":"Orlando","a":"United States of America","i":"US","p":1350000,"la":28.512,"lo":-81.382,"k":0,"z":5.1,"s":4},{"n":"Jacksonville","a":"United States of America","i":"US","p":988000,"la":30.332,"lo":-81.672,"k":0,"z":5.1,"s":4},{"n":"Savannah","a":"United States of America","i":"US","p":180187,"la":32.021,"lo":-81.11,"k":0,"z":5.1,"s":4},{"n":"Columbia","a":"United States of America","i":"US","p":398093,"la":33.997,"lo":-81.031,"k":0,"z":5.6,"s":4},{"n":"Indianapolis","a":"United States of America","i":"US","p":1436000,"la":39.752,"lo":-86.172,"k":0,"z":5.1,"s":4},{"n":"Wilmington","a":"United States of America","i":"US","p":161560,"la":34.226,"lo":-77.945,"k":0,"z":5.1,"s":4},{"n":"Knoxville","a":"United States of America","i":"US","p":655400,"la":35.97,"lo":-83.92,"k":0,"z":5.6,"s":4},{"n":"Richmond","a":"United States of America","i":"US","p":912000,"la":37.552,"lo":-77.452,"k":0,"z":5,"s":4},{"n":"Charleston","a":"United States of America","i":"US","p":123799,"la":38.35,"lo":-81.633,"k":0,"z":5.1,"s":4},{"n":"Baltimore","a":"United States of America","i":"US","p":2255000,"la":39.282,"lo":-76.615,"k":0,"z":5.6,"s":4},{"n":"Syracuse","a":"United States of America","i":"US","p":662577,"la":43.05,"lo":-76.15,"k":0,"z":5.6,"s":4},{"n":"Puerto Ayacucho","a":"Venezuela","i":"VE","p":52526,"la":5.664,"lo":-67.624,"k":0,"z":5.6,"s":4},{"n":"Port-of-Spain","a":"Trinidad and Tobago","i":"TT","p":294934,"la":10.652,"lo":-61.517,"k":1,"z":5.1,"s":4},{"n":"Augusta","a":"United States of America","i":"US","p":24042,"la":44.311,"lo":-69.78,"k":0,"z":5.6,"s":4},{"n":"Sault Ste. Marie","a":"United States of America","i":"US","p":83805,"la":46.495,"lo":-84.345,"k":0,"z":5.1,"s":4},{"n":"Atakpamé","a":"Togo","i":"TG","p":80683,"la":7.53,"lo":1.12,"k":0,"z":5.6,"s":4},{"n":"Sousse","a":"Tunisia","i":"TN","p":327004,"la":35.83,"lo":10.625,"k":0,"z":5.6,"s":4},{"n":"Taizz","a":"Yemen","i":"YE","p":751000,"la":13.606,"lo":44.037,"k":0,"z":5.1,"s":4},{"n":"Sitka","a":"United States of America","i":"US","p":8931,"la":57.06,"lo":-135.328,"k":0,"z":5.1,"s":4},{"n":"Lviv","a":"Ukraine","i":"UA","p":803880,"la":49.835,"lo":24.03,"k":0,"z":5.1,"s":4},{"n":"Odessa","a":"Ukraine","i":"UA","p":991000,"la":46.492,"lo":30.708,"k":0,"z":5.1,"s":4},{"n":"Zhytomyr","a":"Ukraine","i":"UA","p":282192,"la":50.246,"lo":28.662,"k":0,"z":5.6,"s":4},{"n":"Dnipro","a":"Ukraine","i":"UA","p":1050000,"la":48.482,"lo":34.998,"k":0,"z":5.1,"s":4},{"n":"Donetsk","a":"Ukraine","i":"UA","p":988000,"la":48.002,"lo":37.828,"k":0,"z":5,"s":4},{"n":"Kharkiv","a":"Ukraine","i":"UA","p":1461000,"la":50.002,"lo":36.248,"k":0,"z":5.1,"s":4},{"n":"Türkmenbaşy","a":"Turkmenistan","i":"TM","p":68292,"la":40.023,"lo":52.97,"k":0,"z":5.6,"s":4},{"n":"Bukhara","a":"Uzbekistan","i":"UZ","p":319476,"la":39.78,"lo":64.43,"k":0,"z":5.1,"s":4},{"n":"Nukus","a":"Uzbekistan","i":"UZ","p":230006,"la":42.47,"lo":59.615,"k":0,"z":5.6,"s":4},{"n":"Türkmenabat","a":"Turkmenistan","i":"TM","p":234817,"la":39.11,"lo":63.58,"k":0,"z":5.6,"s":4},{"n":"Mary","a":"Turkmenistan","i":"TM","p":178708,"la":37.6,"lo":61.833,"k":0,"z":5.1,"s":4},{"n":"Andijan","a":"Uzbekistan","i":"UZ","p":650000,"la":40.79,"lo":72.34,"k":0,"z":5.1,"s":4},{"n":"Haiphong","a":"Vietnam","i":"VN","p":1969000,"la":20.832,"lo":106.678,"k":0,"z":5.6,"s":4},{"n":"Da Nang","a":"Vietnam","i":"VN","p":1000000,"la":16.06,"lo":108.25,"k":0,"z":5.6,"s":4},{"n":"Kabwe","a":"Zambia","i":"ZM","p":188979,"la":-14.44,"lo":28.45,"k":0,"z":5.6,"s":4},{"n":"Mufulira","a":"Zambia","i":"ZM","p":153624,"la":-12.55,"lo":28.26,"k":0,"z":5.6,"s":4},{"n":"Kitwe","a":"Zambia","i":"ZM","p":404901,"la":-12.81,"lo":28.22,"k":0,"z":5.6,"s":4},{"n":"Livingstone","a":"Zambia","i":"ZM","p":165480,"la":-17.86,"lo":25.86,"k":0,"z":5.6,"s":4},{"n":"Chitungwiza","a":"Zimbabwe","i":"ZW","p":340360,"la":-18,"lo":31.1,"k":0,"z":5.6,"s":4},{"n":"Douala","a":"Cameroon","i":"CM","p":1906000,"la":4.062,"lo":9.708,"k":0,"z":5.6,"s":4},{"n":"Birmingham","a":"United Kingdom","i":"GB","p":2285000,"la":52.477,"lo":-1.922,"k":0,"z":5,"s":4},{"n":"Belfast","a":"United Kingdom","i":"GB","p":450406,"la":54.594,"lo":-5.928,"k":0,"z":5,"s":4},{"n":"İzmir","a":"Turkey","i":"TR","p":2587000,"la":38.438,"lo":27.15,"k":0,"z":5.1,"s":4},{"n":"Bursa","a":"Turkey","i":"TR","p":1492000,"la":40.202,"lo":29.068,"k":0,"z":5.1,"s":4},{"n":"Samsun","a":"Turkey","i":"TR","p":609339,"la":41.28,"lo":36.344,"k":0,"z":5.6,"s":4},{"n":"Konya","a":"Turkey","i":"TR","p":919000,"la":37.877,"lo":32.473,"k":0,"z":5.6,"s":4},{"n":"Adana","a":"Turkey","i":"TR","p":1293000,"la":36.997,"lo":35.318,"k":0,"z":5.1,"s":4},{"n":"Gulu","a":"Uganda","i":"UG","p":146858,"la":2.78,"lo":32.28,"k":0,"z":5.6,"s":4},{"n":"Kigali","a":"Rwanda","i":"RW","p":860000,"la":-1.952,"lo":30.059,"k":1,"z":5.1,"s":4},{"n":"Cottica","a":"Suriname","i":"SR","p":29210,"la":3.85,"lo":-54.233,"k":0,"z":5.6,"s":4},{"n":"Córdoba","a":"Spain","i":"ES","p":321376,"la":37.88,"lo":-4.77,"k":0,"z":5.6,"s":4},{"n":"Niamey","a":"Niger","i":"NE","p":232555,"la":13.492,"lo":7.096,"k":0,"z":5.6,"s":4},{"n":"Tahoua","a":"Niger","i":"NE","p":115956,"la":14.9,"lo":5.26,"k":0,"z":5.6,"s":4},{"n":"Constanța","a":"Romania","i":"RO","p":303399,"la":44.203,"lo":28.61,"k":0,"z":5.6,"s":4},{"n":"Luleå","a":"Sweden","i":"SE","p":48638,"la":65.597,"lo":22.158,"k":0,"z":5.1,"s":4},{"n":"Sundsvall","a":"Sweden","i":"SE","p":73389,"la":62.4,"lo":17.317,"k":0,"z":5.1,"s":4},{"n":"Iași","a":"Romania","i":"RO","p":325914,"la":47.168,"lo":27.575,"k":0,"z":5.6,"s":4},{"n":"Surat Thani","a":"Thailand","i":"TH","p":157627,"la":9.15,"lo":99.34,"k":0,"z":5.6,"s":4},{"n":"Chiang Mai","a":"Thailand","i":"TH","p":397211,"la":18.8,"lo":98.98,"k":0,"z":5.6,"s":4},{"n":"Nakhon Ratchasima","a":"Thailand","i":"TH","p":334984,"la":15,"lo":102.1,"k":0,"z":5.6,"s":4},{"n":"Mbabane","a":"eSwatini","i":"SZ","p":90138,"la":-26.317,"lo":31.133,"k":1,"z":5.6,"s":4},{"n":"Piura","a":"Peru","i":"PE","p":396932,"la":-5.21,"lo":-80.63,"k":0,"z":5.6,"s":4},{"n":"Arequipa","a":"Peru","i":"PE","p":815000,"la":-16.418,"lo":-71.532,"k":0,"z":5.6,"s":4},{"n":"Chimbote","a":"Peru","i":"PE","p":349846,"la":-9.07,"lo":-78.57,"k":0,"z":5.6,"s":4},{"n":"Pucallpa","a":"Peru","i":"PE","p":310750,"la":-8.369,"lo":-74.535,"k":0,"z":5.1,"s":4},{"n":"Iquitos","a":"Peru","i":"PE","p":458729,"la":-3.75,"lo":-73.25,"k":0,"z":5.1,"s":4},{"n":"Huancayo","a":"Peru","i":"PE","p":412733,"la":-12.08,"lo":-75.2,"k":0,"z":5.6,"s":4},{"n":"Ciudad del Este","a":"Paraguay","i":"PY","p":320872,"la":-25.517,"lo":-54.616,"k":0,"z":5.6,"s":4},{"n":"Ponta Delgada","a":"Portugal","i":"PT","p":61526,"la":37.748,"lo":-25.667,"k":0,"z":5.6,"s":4},{"n":"Vigo","a":"Spain","i":"ES","p":378952,"la":42.22,"lo":-8.73,"k":0,"z":5.6,"s":4},{"n":"Bilbao","a":"Spain","i":"ES","p":875552,"la":43.25,"lo":-2.93,"k":0,"z":5,"s":4},{"n":"Kaolack","a":"Senegal","i":"SN","p":277812,"la":14.15,"lo":-16.1,"k":0,"z":5.6,"s":4},{"n":"Kaédi","a":"Senegal","i":"SN","p":21656,"la":16.15,"lo":-13.5,"k":0,"z":5.6,"s":4},{"n":"Geneina","a":"Sudan","i":"SD","p":162981,"la":13.45,"lo":22.44,"k":0,"z":5.6,"s":4},{"n":"Medina","a":"Saudi Arabia","i":"SA","p":1010000,"la":24.502,"lo":39.578,"k":0,"z":5.1,"s":4},{"n":"Tabuk","a":"Saudi Arabia","i":"SA","p":547957,"la":28.384,"lo":36.555,"k":0,"z":5.6,"s":4},{"n":"Juba","a":"South Sudan","i":"SS","p":111975,"la":4.83,"lo":31.58,"k":0,"z":5.1,"s":4},{"n":"Malakal","a":"South Sudan","i":"SS","p":160765,"la":9.537,"lo":31.656,"k":0,"z":5.6,"s":4},{"n":"Omdurman","a":"Sudan","i":"SD","p":2395159,"la":15.617,"lo":32.48,"k":0,"z":5.6,"s":4},{"n":"Al-Ubayyid","a":"Sudan","i":"SD","p":393311,"la":13.183,"lo":30.217,"k":0,"z":5.6,"s":4},{"n":"The Hague","a":"Netherlands","i":"NL","p":1406000,"la":52.08,"lo":4.27,"k":0,"z":5.6,"s":4},{"n":"Kristiansand","a":"Norway","i":"NO","p":63814,"la":58.146,"lo":7.975,"k":0,"z":5.6,"s":4},{"n":"Ljubljana","a":"Slovenia","i":"SI","p":314807,"la":46.055,"lo":14.515,"k":1,"z":5.6,"s":4},{"n":"Bratislava","a":"Slovakia","i":"SK","p":423737,"la":48.15,"lo":17.117,"k":1,"z":5.1,"s":4},{"n":"Hammerfest","a":"Norway","i":"NO","p":10000,"la":70.661,"lo":23.688,"k":0,"z":5.1,"s":4},{"n":"Doha","a":"Qatar","i":"QA","p":1450000,"la":25.287,"lo":51.533,"k":1,"z":5.6,"s":4},{"n":"Quetta","a":"Pakistan","i":"PK","p":768000,"la":30.222,"lo":67.023,"k":0,"z":5.6,"s":4},{"n":"Larkana","a":"Pakistan","i":"PK","p":364033,"la":27.562,"lo":68.207,"k":0,"z":5.6,"s":4},{"n":"Springbok","a":"South Africa","i":"ZA","p":10438,"la":-29.667,"lo":17.883,"k":0,"z":5.6,"s":4},{"n":"Upington","a":"South Africa","i":"ZA","p":71373,"la":-28.46,"lo":21.23,"k":0,"z":5.6,"s":4},{"n":"Worcester","a":"South Africa","i":"ZA","p":127597,"la":-33.64,"lo":19.44,"k":0,"z":5.6,"s":4},{"n":"George","a":"South Africa","i":"ZA","p":174582,"la":-33.95,"lo":22.45,"k":0,"z":5.6,"s":4},{"n":"Tete","a":"Mozambique","i":"MZ","p":129316,"la":-16.17,"lo":33.58,"k":0,"z":5.6,"s":4},{"n":"Pemba","a":"Mozambique","i":"MZ","p":110643,"la":-12.983,"lo":40.532,"k":0,"z":5.6,"s":4},{"n":"Nampula","a":"Mozambique","i":"MZ","p":388526,"la":-15.136,"lo":39.293,"k":0,"z":5.6,"s":4},{"n":"Welkom","a":"South Africa","i":"ZA","p":431944,"la":-27.97,"lo":26.73,"k":0,"z":5.1,"s":4},{"n":"Xai-Xai","a":"Mozambique","i":"MZ","p":128805,"la":-25.04,"lo":33.64,"k":0,"z":5.6,"s":4},{"n":"Goroka","a":"Papua New Guinea","i":"PG","p":39699,"la":-6.083,"lo":145.385,"k":0,"z":5.6,"s":4},{"n":"Mt. Hagen","a":"Papua New Guinea","i":"PG","p":59064,"la":-5.863,"lo":144.217,"k":0,"z":5.6,"s":4},{"n":"Rabaul","a":"Papua New Guinea","i":"PG","p":8074,"la":-4.205,"lo":152.143,"k":0,"z":5.6,"s":4},{"n":"Lae","a":"Papua New Guinea","i":"PG","p":131052,"la":-6.733,"lo":146.99,"k":0,"z":5.6,"s":4},{"n":"David","a":"Panama","i":"PA","p":110037,"la":8.433,"lo":-82.433,"k":0,"z":5.6,"s":4},{"n":"Oujda","a":"Morocco","i":"MA","p":409391,"la":34.69,"lo":-1.91,"k":0,"z":5.6,"s":4},{"n":"Safi","a":"Morocco","i":"MA","p":353476,"la":32.32,"lo":-9.24,"k":0,"z":5.6,"s":4},{"n":"Podgorica","a":"Montenegro","i":"ME","p":145850,"la":42.466,"lo":19.266,"k":1,"z":5.6,"s":4},{"n":"Quelimane","a":"Mozambique","i":"MZ","p":188964,"la":-17.88,"lo":36.89,"k":0,"z":5.6,"s":4},{"n":"East London","a":"South Africa","i":"ZA","p":338627,"la":-32.97,"lo":27.87,"k":0,"z":5.6,"s":4},{"n":"Middelburg","a":"South Africa","i":"ZA","p":18164,"la":-31.5,"lo":25.01,"k":0,"z":5.1,"s":4},{"n":"Nalchik","a":"Russia","i":"RU","p":307866,"la":43.498,"lo":43.618,"k":0,"z":5.6,"s":4},{"n":"Stavropol","a":"Russia","i":"RU","p":363064,"la":45.05,"lo":41.98,"k":0,"z":5.6,"s":4},{"n":"Ugolnye Kopi","a":"Russia","i":"RU","p":3367,"la":64.733,"lo":177.7,"k":0,"z":5.6,"s":4},{"n":"Kaliningrad","a":"Russia","i":"RU","p":434954,"la":54.7,"lo":20.497,"k":0,"z":5,"s":4},{"n":"Pskov","a":"Russia","i":"RU","p":201990,"la":57.83,"lo":28.33,"k":0,"z":5.1,"s":4},{"n":"Bryansk","a":"Russia","i":"RU","p":427236,"la":53.26,"lo":34.43,"k":0,"z":5.1,"s":4},{"n":"Smolensk","a":"Russia","i":"RU","p":320991,"la":54.783,"lo":32.047,"k":0,"z":5.1,"s":4},{"n":"Petrozavodsk","a":"Russia","i":"RU","p":265025,"la":61.781,"lo":34.361,"k":0,"z":5.1,"s":4},{"n":"Tver","a":"Russia","i":"RU","p":400212,"la":56.86,"lo":35.89,"k":0,"z":5.6,"s":4},{"n":"Vologda","a":"Russia","i":"RU","p":294889,"la":59.21,"lo":39.92,"k":0,"z":5.1,"s":4},{"n":"Yaroslavl","a":"Russia","i":"RU","p":606730,"la":57.62,"lo":39.87,"k":0,"z":5.1,"s":4},{"n":"Rostov","a":"Russia","i":"RU","p":1052000,"la":47.237,"lo":39.711,"k":0,"z":5.1,"s":4},{"n":"Sochi","a":"Russia","i":"RU","p":327608,"la":43.59,"lo":39.73,"k":0,"z":5.1,"s":4},{"n":"Krasnodar","a":"Russia","i":"RU","p":649851,"la":45.02,"lo":39,"k":0,"z":5.1,"s":4},{"n":"Penza","a":"Russia","i":"RU","p":512602,"la":53.18,"lo":45,"k":0,"z":5.1,"s":4},{"n":"Ryazan","a":"Russia","i":"RU","p":520173,"la":54.62,"lo":39.72,"k":0,"z":5.6,"s":4},{"n":"Voronezh","a":"Russia","i":"RU","p":844000,"la":51.665,"lo":39.187,"k":0,"z":5.1,"s":4},{"n":"Magnitogorsk","a":"Russia","i":"RU","p":413351,"la":53.423,"lo":58.98,"k":0,"z":5.1,"s":4},{"n":"Chelyabinsk","a":"Russia","i":"RU","p":1091000,"la":55.157,"lo":61.437,"k":0,"z":5.1,"s":4},{"n":"Vorkuta","a":"Russia","i":"RU","p":80039,"la":67.5,"lo":64.01,"k":0,"z":5.6,"s":4},{"n":"Kirov","a":"Russia","i":"RU","p":457437,"la":58.59,"lo":49.67,"k":0,"z":5.1,"s":4},{"n":"Nizhny Tagil","a":"Russia","i":"RU","p":381116,"la":57.92,"lo":59.975,"k":0,"z":5.1,"s":4},{"n":"Astrakhan","a":"Russia","i":"RU","p":502533,"la":46.349,"lo":48.055,"k":0,"z":5.1,"s":4},{"n":"Orenburg","a":"Russia","i":"RU","p":550204,"la":51.78,"lo":55.11,"k":0,"z":5.1,"s":4},{"n":"Saratov","a":"Russia","i":"RU","p":843000,"la":51.582,"lo":46.028,"k":0,"z":5.1,"s":4},{"n":"Ulyanovsk","a":"Russia","i":"RU","p":640680,"la":54.33,"lo":48.41,"k":0,"z":5.6,"s":4},{"n":"Omsk","a":"Russia","i":"RU","p":1135000,"la":54.992,"lo":73.398,"k":0,"z":3.7,"s":4},{"n":"Tyumen","a":"Russia","i":"RU","p":519119,"la":57.14,"lo":65.53,"k":0,"z":5.6,"s":4},{"n":"Novokuznetsk","a":"Russia","i":"RU","p":539616,"la":53.75,"lo":87.115,"k":0,"z":5.1,"s":4},{"n":"Kemerovo","a":"Russia","i":"RU","p":477090,"la":55.34,"lo":86.09,"k":0,"z":5.1,"s":4},{"n":"Grozny","a":"Russia","i":"RU","p":226100,"la":43.319,"lo":45.699,"k":0,"z":5,"s":4},{"n":"Kandy","a":"Sri Lanka","i":"LK","p":111701,"la":7.28,"lo":80.67,"k":0,"z":5.6,"s":4},{"n":"Sri Jayawardenepura Kotte","a":"Sri Lanka","i":"LK","p":115826,"la":6.9,"lo":79.95,"k":0,"z":5.6,"s":4},{"n":"Daejeon","a":"South Korea","i":"KR","p":1468000,"la":36.337,"lo":127.423,"k":0,"z":5.1,"s":4},{"n":"Gwangju","a":"South Korea","i":"KR","p":1440000,"la":35.173,"lo":126.908,"k":0,"z":5.6,"s":4},{"n":"Busan","a":"South Korea","i":"KR","p":3480000,"la":35.097,"lo":129.008,"k":0,"z":5.6,"s":4},{"n":"Zamboanga","a":"Philippines","i":"PH","p":773000,"la":6.922,"lo":122.078,"k":0,"z":5.6,"s":4},{"n":"Laoag","a":"Philippines","i":"PH","p":207048,"la":18.199,"lo":120.594,"k":0,"z":5.6,"s":4},{"n":"Baguio","a":"Philippines","i":"PH","p":447824,"la":16.43,"lo":120.57,"k":0,"z":5.6,"s":4},{"n":"General Santos","a":"Philippines","i":"PH","p":950530,"la":6.111,"lo":125.175,"k":0,"z":5.6,"s":4},{"n":"Ust-Ilimsk","a":"Russia","i":"RU","p":100271,"la":57.99,"lo":102.633,"k":0,"z":5.6,"s":4},{"n":"Angarsk","a":"Russia","i":"RU","p":243158,"la":52.56,"lo":103.92,"k":0,"z":5.6,"s":4},{"n":"Abakan","a":"Russia","i":"RU","p":167289,"la":53.704,"lo":91.445,"k":0,"z":5.6,"s":4},{"n":"Norilsk","a":"Russia","i":"RU","p":165873,"la":69.34,"lo":88.225,"k":0,"z":5.6,"s":4},{"n":"Khatanga","a":"Russia","i":"RU","p":3205,"la":72.041,"lo":102.465,"k":0,"z":5.6,"s":4},{"n":"Kyzyl","a":"Russia","i":"RU","p":108240,"la":51.707,"lo":94.383,"k":0,"z":5.6,"s":4},{"n":"Ulan-Ude","a":"Russia","i":"RU","p":360278,"la":51.825,"lo":107.625,"k":0,"z":5.6,"s":4},{"n":"Blagoveshchensk","a":"Russia","i":"RU","p":221296,"la":50.267,"lo":127.533,"k":0,"z":5.6,"s":4},{"n":"Bukachacha","a":"Russia","i":"RU","p":3372,"la":52.983,"lo":116.917,"k":0,"z":5.6,"s":4},{"n":"Dalnegorsk","a":"Russia","i":"RU","p":8123,"la":44.537,"lo":135.517,"k":0,"z":5.6,"s":4},{"n":"Ambarchik","a":"Russia","i":"RU","p":0,"la":69.651,"lo":162.334,"k":0,"z":5.6,"s":4},{"n":"Batagay","a":"Russia","i":"RU","p":4266,"la":67.656,"lo":134.635,"k":0,"z":5.6,"s":4},{"n":"Chokurdakh","a":"Russia","i":"RU","p":2506,"la":70.618,"lo":147.895,"k":0,"z":5.6,"s":4},{"n":"Ust-Nera","a":"Russia","i":"RU","p":9148,"la":64.567,"lo":143.2,"k":0,"z":5.6,"s":4},{"n":"Lensk","a":"Russia","i":"RU","p":25589,"la":60.725,"lo":114.947,"k":0,"z":5.6,"s":4},{"n":"Aldan","a":"Russia","i":"RU","p":24426,"la":58.603,"lo":125.389,"k":0,"z":5.6,"s":4},{"n":"Mirny","a":"Russia","i":"RU","p":40308,"la":62.54,"lo":113.961,"k":0,"z":5.1,"s":4},{"n":"Zhigansk","a":"Russia","i":"RU","p":3237,"la":66.77,"lo":123.371,"k":0,"z":5.1,"s":4},{"n":"Okhotsk","a":"Russia","i":"RU","p":5570,"la":59.383,"lo":143.217,"k":0,"z":5.1,"s":4},{"n":"Khabarovsk","a":"Russia","i":"RU","p":579000,"la":48.455,"lo":135.12,"k":0,"z":5.6,"s":4},{"n":"Okha","a":"Russia","i":"RU","p":26560,"la":53.574,"lo":142.948,"k":0,"z":5.6,"s":4},{"n":"Yuzhno Sakhalinsk","a":"Russia","i":"RU","p":176484,"la":46.965,"lo":142.74,"k":0,"z":5.1,"s":4},{"n":"Mexicali","a":"Mexico","i":"MX","p":885000,"la":32.652,"lo":-115.482,"k":0,"z":5.6,"s":4},{"n":"La Paz","a":"Mexico","i":"MX","p":189767,"la":24.14,"lo":-110.32,"k":0,"z":5.6,"s":4},{"n":"Torreón","a":"Mexico","i":"MX","p":1144000,"la":25.572,"lo":-103.422,"k":0,"z":5.6,"s":4},{"n":"Culiacán","a":"Mexico","i":"MX","p":809000,"la":24.832,"lo":-107.382,"k":0,"z":5.6,"s":4},{"n":"Nogales","a":"Mexico","i":"MX","p":178097,"la":31.305,"lo":-110.945,"k":0,"z":5.6,"s":4},{"n":"Hermosillo","a":"Mexico","i":"MX","p":595811,"la":29.099,"lo":-110.954,"k":0,"z":5.6,"s":4},{"n":"Guaymas","a":"Mexico","i":"MX","p":103449,"la":27.93,"lo":-110.89,"k":0,"z":5.6,"s":4},{"n":"San Luis Potosí","a":"Mexico","i":"MX","p":992000,"la":22.172,"lo":-101.002,"k":0,"z":5.6,"s":4},{"n":"Matamoros","a":"Mexico","i":"MX","p":538785,"la":25.88,"lo":-97.5,"k":0,"z":5.6,"s":4},{"n":"Nuevo Laredo","a":"Mexico","i":"MX","p":349550,"la":27.5,"lo":-99.55,"k":0,"z":5.6,"s":4},{"n":"Colima","a":"Mexico","i":"MX","p":223287,"la":19.23,"lo":-103.72,"k":0,"z":5.6,"s":4},{"n":"Campeche","a":"Mexico","i":"MX","p":205212,"la":19.83,"lo":-90.5,"k":0,"z":5.6,"s":4},{"n":"Oaxaca","a":"Mexico","i":"MX","p":530728,"la":17.083,"lo":-96.67,"k":0,"z":5.1,"s":4},{"n":"León","a":"Mexico","i":"MX","p":1488000,"la":21.152,"lo":-101.702,"k":0,"z":5.6,"s":4},{"n":"Maiduguri","a":"Nigeria","i":"NG","p":896000,"la":11.852,"lo":13.158,"k":0,"z":5.6,"s":4},{"n":"Port Harcourt","a":"Nigeria","i":"NG","p":1020000,"la":4.812,"lo":7.008,"k":0,"z":5.6,"s":4},{"n":"Makurdi","a":"Nigeria","i":"NG","p":292645,"la":7.73,"lo":8.53,"k":0,"z":5.6,"s":4},{"n":"Ibadan","a":"Nigeria","i":"NG","p":2628000,"la":7.382,"lo":3.928,"k":0,"z":5.1,"s":4},{"n":"Ogbomosho","a":"Nigeria","i":"NG","p":951000,"la":8.132,"lo":4.238,"k":0,"z":5.6,"s":4},{"n":"Warri","a":"Nigeria","i":"NG","p":830106,"la":5.52,"lo":5.76,"k":0,"z":5.6,"s":4},{"n":"Kaduna","a":"Nigeria","i":"NG","p":1442000,"la":10.522,"lo":7.438,"k":0,"z":5.6,"s":4},{"n":"Gdańsk","a":"Poland","i":"PL","p":740000,"la":54.36,"lo":18.64,"k":0,"z":5,"s":4},{"n":"Kraków","a":"Poland","i":"PL","p":756000,"la":50.062,"lo":19.958,"k":0,"z":5.1,"s":4},{"n":"Dalandzadgad","a":"Mongolia","i":"MN","p":15093,"la":43.584,"lo":104.44,"k":0,"z":5.6,"s":4},{"n":"Wonsan","a":"North Korea","i":"KP","p":329207,"la":39.16,"lo":127.431,"k":0,"z":5.6,"s":4},{"n":"Sinuiju","a":"North Korea","i":"KP","p":288112,"la":40.086,"lo":124.421,"k":0,"z":5.6,"s":4},{"n":"Dund-Us","a":"Mongolia","i":"MN","p":30500,"la":48.017,"lo":91.633,"k":0,"z":5.6,"s":4},{"n":"Choibalsan","a":"Mongolia","i":"MN","p":33376,"la":48.067,"lo":114.506,"k":0,"z":5.6,"s":4},{"n":"Lüderitz","a":"Namibia","i":"NA","p":15137,"la":-26.648,"lo":15.159,"k":0,"z":5.6,"s":4},{"n":"Walvis Bay","a":"Namibia","i":"NA","p":52058,"la":-22.958,"lo":14.505,"k":0,"z":5.6,"s":4},{"n":"Mwanza","a":"Tanzania","i":"TZ","p":493944,"la":-2.52,"lo":32.93,"k":0,"z":5.6,"s":4},{"n":"Morogoro","a":"Tanzania","i":"TZ","p":250902,"la":-6.82,"lo":37.66,"k":0,"z":5.6,"s":4},{"n":"Dodoma","a":"Tanzania","i":"TZ","p":218269,"la":-6.183,"lo":35.75,"k":0,"z":5.6,"s":4},{"n":"Arusha","a":"Tanzania","i":"TZ","p":341136,"la":-3.36,"lo":36.67,"k":0,"z":5.6,"s":4},{"n":"Bern","a":"Switzerland","i":"CH","p":275329,"la":46.917,"lo":7.467,"k":1,"z":5.6,"s":4},{"n":"Malmö","a":"Sweden","i":"SE","p":269349,"la":55.599,"lo":13.023,"k":0,"z":5.1,"s":4},{"n":"Laayoune","a":"Morocco","i":"MA","p":188084,"la":27.15,"lo":-13.2,"k":0,"z":5.6,"s":4},{"n":"Ternate","a":"Indonesia","i":"ID","p":187521,"la":0.793,"lo":127.363,"k":0,"z":5.6,"s":4},{"n":"Ambon","a":"Indonesia","i":"ID","p":355596,"la":-3.717,"lo":128.2,"k":0,"z":5.6,"s":4},{"n":"Raba","a":"Indonesia","i":"ID","p":106101,"la":-8.45,"lo":118.767,"k":0,"z":5.6,"s":4},{"n":"Jayapura","a":"Indonesia","i":"ID","p":169341,"la":-2.533,"lo":140.7,"k":0,"z":5.6,"s":4},{"n":"Florence","a":"Italy","i":"IT","p":1500000,"la":43.78,"lo":11.25,"k":0,"z":5.6,"s":4},{"n":"Catania","a":"Italy","i":"IT","p":674542,"la":37.5,"lo":15.08,"k":0,"z":5.6,"s":4},{"n":"Pristina","a":"Kosovo","i":"-99","p":465186,"la":42.667,"lo":21.166,"k":1,"z":5.6,"s":4},{"n":"Meru","a":"Kenya","i":"KE","p":47226,"la":0.06,"lo":37.64,"k":0,"z":5.6,"s":4},{"n":"Eldoret","a":"Kenya","i":"KE","p":353381,"la":0.52,"lo":35.27,"k":0,"z":5.6,"s":4},{"n":"Banda Aceh","a":"Indonesia","i":"ID","p":513698,"la":5.55,"lo":95.32,"k":0,"z":5.6,"s":4},{"n":"George Town","a":"Malaysia","i":"MY","p":2500000,"la":5.414,"lo":100.329,"k":0,"z":5.1,"s":4},{"n":"Zhangye","a":"China","i":"CN","p":230728,"la":38.93,"lo":100.45,"k":0,"z":5.6,"s":4},{"n":"Wuwei","a":"China","i":"CN","p":493092,"la":37.928,"lo":102.641,"k":0,"z":5.6,"s":4},{"n":"Dunhuang","a":"China","i":"CN","p":140094,"la":40.143,"lo":94.662,"k":0,"z":5,"s":4},{"n":"Tianshui","a":"China","i":"CN","p":1225000,"la":34.602,"lo":105.918,"k":0,"z":5.6,"s":4},{"n":"Dulan","a":"China","i":"CN","p":100,"la":36.167,"lo":98.267,"k":0,"z":5.6,"s":4},{"n":"Golmud","a":"China","i":"CN","p":107092,"la":36.417,"lo":94.883,"k":0,"z":5,"s":4},{"n":"Yulin","a":"China","i":"CN","p":1127000,"la":22.632,"lo":110.148,"k":0,"z":5.6,"s":4},{"n":"Bose","a":"China","i":"CN","p":175282,"la":23.9,"lo":106.613,"k":0,"z":5.6,"s":4},{"n":"Wuzhou","a":"China","i":"CN","p":442315,"la":23.48,"lo":111.32,"k":0,"z":5.6,"s":4},{"n":"Liupanshui","a":"China","i":"CN","p":1221000,"la":26.596,"lo":104.831,"k":0,"z":5.6,"s":4},{"n":"Quanzhou","a":"China","i":"CN","p":1463000,"la":24.902,"lo":118.578,"k":0,"z":5.6,"s":4},{"n":"Hefei","a":"China","i":"CN","p":2035000,"la":31.852,"lo":117.278,"k":0,"z":5.6,"s":4},{"n":"Suzhou","a":"China","i":"CN","p":1964000,"la":33.638,"lo":116.977,"k":0,"z":5.6,"s":4},{"n":"Zhanjiang","a":"China","i":"CN","p":1590000,"la":21.202,"lo":110.378,"k":0,"z":5.6,"s":4},{"n":"Shaoguan","a":"China","i":"CN","p":720266,"la":24.8,"lo":113.58,"k":0,"z":5.1,"s":4},{"n":"Balikpapan","a":"Indonesia","i":"ID","p":445905,"la":-1.25,"lo":116.83,"k":0,"z":5.6,"s":4},{"n":"Kuching","a":"Malaysia","i":"MY","p":570407,"la":1.53,"lo":110.33,"k":0,"z":5.6,"s":4},{"n":"Antsiranana","a":"Madagascar","i":"MG","p":82937,"la":-12.277,"lo":49.312,"k":0,"z":5.6,"s":4},{"n":"Fianarantsoa","a":"Madagascar","i":"MG","p":184184,"la":-21.433,"lo":47.083,"k":0,"z":5.6,"s":4},{"n":"Mahajanga","a":"Madagascar","i":"MG","p":154657,"la":-15.67,"lo":46.345,"k":0,"z":5.6,"s":4},{"n":"Toliara","a":"Madagascar","i":"MG","p":115319,"la":-23.357,"lo":43.69,"k":0,"z":5.1,"s":4},{"n":"Surakarta","a":"Indonesia","i":"ID","p":555308,"la":-7.565,"lo":110.825,"k":0,"z":5.6,"s":4},{"n":"Bandar Lampung","a":"Indonesia","i":"ID","p":865000,"la":-5.428,"lo":105.268,"k":0,"z":5.6,"s":4},{"n":"Tanjung Pandan","a":"Indonesia","i":"ID","p":62374,"la":-2.75,"lo":107.65,"k":0,"z":5.6,"s":4},{"n":"Malang","a":"Indonesia","i":"ID","p":805000,"la":-7.978,"lo":112.608,"k":0,"z":5.6,"s":4},{"n":"Kupang","a":"Indonesia","i":"ID","p":282396,"la":-10.179,"lo":123.583,"k":0,"z":5.6,"s":4},{"n":"Parepare","a":"Indonesia","i":"ID","p":87776,"la":-4.017,"lo":119.633,"k":0,"z":5.6,"s":4},{"n":"Cuenca","a":"Ecuador","i":"EC","p":286878,"la":-2.9,"lo":-79,"k":0,"z":5.6,"s":4},{"n":"Santa Cruz","a":"Ecuador","i":"EC","p":11262,"la":-0.533,"lo":-90.35,"k":0,"z":5.6,"s":4},{"n":"Limón","a":"Costa Rica","i":"CR","p":85001,"la":10,"lo":-83.033,"k":0,"z":5.6,"s":4},{"n":"Santiago de Cuba","a":"Cuba","i":"CU","p":555865,"la":20.025,"lo":-75.821,"k":0,"z":5.1,"s":4},{"n":"Santiago","a":"Dominican Republic","i":"DO","p":1550753,"la":19.5,"lo":-70.67,"k":0,"z":5.6,"s":4},{"n":"Manizales","a":"Colombia","i":"CO","p":375848,"la":5.06,"lo":-75.52,"k":0,"z":5.6,"s":4},{"n":"Pasto","a":"Colombia","i":"CO","p":382236,"la":1.214,"lo":-77.281,"k":0,"z":5.6,"s":4},{"n":"Barranquilla","a":"Colombia","i":"CO","p":1798000,"la":10.962,"lo":-74.802,"k":0,"z":5.6,"s":4},{"n":"Roseau","a":"Dominica","i":"DM","p":23336,"la":15.301,"lo":-61.387,"k":1,"z":5.1,"s":4},{"n":"Mbandaka","a":"Congo (Kinshasa)","i":"CD","p":274996,"la":0.04,"lo":18.26,"k":0,"z":5.6,"s":4},{"n":"Moundou","a":"Chad","i":"TD","p":156705,"la":8.55,"lo":16.09,"k":0,"z":5.6,"s":4},{"n":"Suez","a":"Egypt","i":"EG","p":508335,"la":30.005,"lo":32.55,"k":0,"z":5.6,"s":4},{"n":"Bur Said","a":"Egypt","i":"EG","p":623864,"la":31.26,"lo":32.29,"k":0,"z":5.6,"s":4},{"n":"El Faiyum","a":"Egypt","i":"EG","p":316772,"la":29.31,"lo":30.84,"k":0,"z":5.6,"s":4},{"n":"Aswan","a":"Egypt","i":"EG","p":313442,"la":24.088,"lo":32.899,"k":0,"z":5.6,"s":4},{"n":"Asyut","a":"Egypt","i":"EG","p":420585,"la":27.19,"lo":31.18,"k":0,"z":5.6,"s":4},{"n":"Kisangani","a":"Congo (Kinshasa)","i":"CD","p":578470,"la":0.52,"lo":25.22,"k":0,"z":5.6,"s":4},{"n":"Assab","a":"Eritrea","i":"ER","p":105496,"la":13.01,"lo":42.73,"k":0,"z":5.6,"s":4},{"n":"Djibouti","a":"Djibouti","i":"DJ","p":923000,"la":11.595,"lo":43.148,"k":1,"z":5.6,"s":4},{"n":"Dresden","a":"Germany","i":"DE","p":617515,"la":51.05,"lo":13.75,"k":0,"z":5,"s":4},{"n":"Xigaze","a":"China","i":"CN","p":80000,"la":29.25,"lo":88.883,"k":0,"z":5,"s":4},{"n":"Shache","a":"China","i":"CN","p":851374,"la":38.426,"lo":77.25,"k":0,"z":5.6,"s":4},{"n":"Yining","a":"China","i":"CN","p":542507,"la":43.9,"lo":81.35,"k":0,"z":5.1,"s":4},{"n":"Altay","a":"China","i":"CN","p":142000,"la":47.867,"lo":88.117,"k":0,"z":5.6,"s":4},{"n":"Putrajaya","a":"Malaysia","i":"MY","p":67964,"la":2.933,"lo":101.695,"k":0,"z":5.6,"s":4},{"n":"Shizuishan","a":"China","i":"CN","p":136570,"la":39.233,"lo":106.769,"k":0,"z":5.6,"s":4},{"n":"Yulin","a":"China","i":"CN","p":155960,"la":38.283,"lo":109.733,"k":0,"z":5.6,"s":4},{"n":"Ankang","a":"China","i":"CN","p":1100000,"la":32.68,"lo":109.02,"k":0,"z":5.6,"s":4},{"n":"Houma","a":"China","i":"CN","p":102400,"la":35.62,"lo":111.21,"k":0,"z":5.1,"s":4},{"n":"Yueyang","a":"China","i":"CN","p":826000,"la":29.382,"lo":113.098,"k":0,"z":5.6,"s":4},{"n":"Hengyang","a":"China","i":"CN","p":1016000,"la":26.882,"lo":112.588,"k":0,"z":5.6,"s":4},{"n":"Mianyang","a":"China","i":"CN","p":1396000,"la":31.465,"lo":104.69,"k":0,"z":5.6,"s":4},{"n":"Xichang","a":"China","i":"CN","p":379993,"la":27.88,"lo":102.3,"k":0,"z":5,"s":4},{"n":"Baoshan","a":"China","i":"CN","p":1000000,"la":25.12,"lo":99.15,"k":0,"z":5.6,"s":4},{"n":"Gejiu","a":"China","i":"CN","p":149105,"la":23.38,"lo":103.15,"k":0,"z":5.1,"s":4},{"n":"Shijiazhuang","a":"China","i":"CN","p":2417000,"la":38.052,"lo":114.478,"k":0,"z":5.6,"s":4},{"n":"Handan","a":"China","i":"CN","p":1631000,"la":36.582,"lo":114.478,"k":0,"z":5.1,"s":4},{"n":"Anshan","a":"China","i":"CN","p":1639000,"la":41.117,"lo":122.938,"k":0,"z":5.6,"s":4},{"n":"Dalian","a":"China","i":"CN","p":3167000,"la":38.925,"lo":121.628,"k":0,"z":5.1,"s":4},{"n":"Qingdao","a":"China","i":"CN","p":2866000,"la":36.092,"lo":120.328,"k":0,"z":5,"s":4},{"n":"Linyi","a":"China","i":"CN","p":2082000,"la":35.082,"lo":118.328,"k":0,"z":5.6,"s":4},{"n":"Huaiyin","a":"China","i":"CN","p":1264000,"la":33.585,"lo":119.066,"k":0,"z":5.6,"s":4},{"n":"Wenzhou","a":"China","i":"CN","p":2350000,"la":28.022,"lo":120.648,"k":0,"z":5.1,"s":4},{"n":"Ningbo","a":"China","i":"CN","p":1923000,"la":29.882,"lo":121.548,"k":0,"z":5.1,"s":4},{"n":"Fukuoka","a":"Japan","i":"JP","p":2792000,"la":33.597,"lo":130.408,"k":0,"z":5.6,"s":4},{"n":"Miyazaki","a":"Japan","i":"JP","p":324384,"la":31.918,"lo":131.418,"k":0,"z":5.6,"s":4},{"n":"Naha","a":"Japan","i":"JP","p":905238,"la":26.207,"lo":127.673,"k":0,"z":5.1,"s":4},{"n":"Kōchi","a":"Japan","i":"JP","p":335570,"la":33.562,"lo":133.538,"k":0,"z":5.6,"s":4},{"n":"Gorontalo","a":"Indonesia","i":"ID","p":365497,"la":0.55,"lo":123.07,"k":0,"z":5.6,"s":4},{"n":"Tongliao","a":"China","i":"CN","p":884000,"la":43.622,"lo":122.268,"k":0,"z":5.6,"s":4},{"n":"Hohhot","a":"China","i":"CN","p":1726000,"la":40.822,"lo":111.658,"k":0,"z":5.1,"s":4},{"n":"Chifeng","a":"China","i":"CN","p":1277000,"la":42.272,"lo":118.948,"k":0,"z":5.6,"s":4},{"n":"Ulan Hot","a":"China","i":"CN","p":241894,"la":46.08,"lo":122.08,"k":0,"z":5.6,"s":4},{"n":"Hailar","a":"China","i":"CN","p":231171,"la":49.2,"lo":119.7,"k":0,"z":5.6,"s":4},{"n":"Jiamusi","a":"China","i":"CN","p":1020000,"la":46.832,"lo":130.348,"k":0,"z":5.1,"s":4},{"n":"Bei'an","a":"China","i":"CN","p":154936,"la":48.246,"lo":126.529,"k":0,"z":5.6,"s":4},{"n":"Daqing","a":"China","i":"CN","p":1693000,"la":46.582,"lo":124.998,"k":0,"z":5.1,"s":4},{"n":"Jixi","a":"China","i":"CN","p":965000,"la":45.302,"lo":130.968,"k":0,"z":5.6,"s":4},{"n":"Nagoya","a":"Japan","i":"JP","p":3230000,"la":35.157,"lo":136.913,"k":0,"z":5.1,"s":4},{"n":"Nagano","a":"Japan","i":"JP","p":594311,"la":36.65,"lo":138.17,"k":0,"z":5.6,"s":4},{"n":"Kushiro","a":"Japan","i":"JP","p":198566,"la":42.975,"lo":144.375,"k":0,"z":5.1,"s":4},{"n":"Hakodate","a":"Japan","i":"JP","p":302984,"la":41.795,"lo":140.74,"k":0,"z":5.6,"s":4},{"n":"Kyoto","a":"Japan","i":"JP","p":1805000,"la":35.032,"lo":135.748,"k":0,"z":5.1,"s":4},{"n":"Sendai","a":"Japan","i":"JP","p":2250000,"la":38.268,"lo":140.87,"k":0,"z":5.1,"s":4},{"n":"Sakata","a":"Japan","i":"JP","p":100446,"la":38.92,"lo":139.85,"k":0,"z":5.6,"s":4},{"n":"Bandundu","a":"Congo (Kinshasa)","i":"CD","p":118211,"la":-3.31,"lo":17.38,"k":0,"z":5.6,"s":4},{"n":"Kananga","a":"Congo (Kinshasa)","i":"CD","p":765000,"la":-5.888,"lo":22.398,"k":0,"z":5.6,"s":4},{"n":"Kasongo","a":"Congo (Kinshasa)","i":"CD","p":63000,"la":-4.45,"lo":26.66,"k":0,"z":5.6,"s":4},{"n":"Mbuji-Mayi","a":"Congo (Kinshasa)","i":"CD","p":1295000,"la":-6.148,"lo":23.598,"k":0,"z":5.6,"s":4},{"n":"Kalemie","a":"Congo (Kinshasa)","i":"CD","p":206257,"la":-5.933,"lo":29.2,"k":0,"z":5.6,"s":4},{"n":"Butembo","a":"Congo (Kinshasa)","i":"CD","p":286403,"la":0.13,"lo":29.28,"k":0,"z":5.6,"s":4},{"n":"Goma","a":"Congo (Kinshasa)","i":"CD","p":144124,"la":-1.679,"lo":29.222,"k":0,"z":5.1,"s":4},{"n":"Mzuzu","a":"Malawi","i":"MW","p":110201,"la":-11.46,"lo":34.02,"k":0,"z":5.6,"s":4},{"n":"Blantyre","a":"Malawi","i":"MW","p":584877,"la":-15.79,"lo":34.99,"k":0,"z":5.6,"s":4},{"n":"Quetzaltenango","a":"Guatemala","i":"GT","p":575263,"la":14.83,"lo":-91.52,"k":0,"z":5.6,"s":4},{"n":"Banjul","a":"The Gambia","i":"GM","p":43094,"la":13.454,"lo":-16.592,"k":1,"z":5.6,"s":4},{"n":"Faridabad","a":"India","i":"IN","p":1394000,"la":28.435,"lo":77.315,"k":0,"z":5.6,"s":4},{"n":"Srinagar","a":"India","i":"IN","p":1140000,"la":34.069,"lo":74.828,"k":0,"z":5.6,"s":4},{"n":"Vijayawada","a":"India","i":"IN","p":1137000,"la":16.522,"lo":80.628,"k":0,"z":5.6,"s":4},{"n":"Thiruvananthapuram","a":"India","i":"IN","p":954000,"la":8.516,"lo":76.925,"k":0,"z":5.6,"s":4},{"n":"Kochi","a":"India","i":"IN","p":1519000,"la":10.017,"lo":76.222,"k":0,"z":5.6,"s":4},{"n":"Cuttack","a":"India","i":"IN","p":580000,"la":20.47,"lo":85.89,"k":0,"z":5.6,"s":4},{"n":"Hubballi","a":"India","i":"IN","p":890000,"la":15.362,"lo":75.123,"k":0,"z":5.6,"s":4},{"n":"Mangaluru","a":"India","i":"IN","p":776632,"la":12.9,"lo":74.85,"k":0,"z":5.6,"s":4},{"n":"Mysuru","a":"India","i":"IN","p":887000,"la":12.312,"lo":76.658,"k":0,"z":5.6,"s":4},{"n":"Kalaburagi","a":"India","i":"IN","p":483615,"la":17.35,"lo":76.82,"k":0,"z":5.6,"s":4},{"n":"Kolhapur","a":"India","i":"IN","p":750000,"la":16.7,"lo":74.22,"k":0,"z":5.1,"s":4},{"n":"Nanded","a":"India","i":"IN","p":623708,"la":19.197,"lo":77.308,"k":0,"z":5.6,"s":4},{"n":"Akola","a":"India","i":"IN","p":503502,"la":20.71,"lo":77.01,"k":0,"z":5.6,"s":4},{"n":"Guwahati","a":"India","i":"IN","p":983000,"la":26.162,"lo":91.768,"k":0,"z":5.6,"s":4},{"n":"Kayes","a":"Congo (Brazzaville)","i":"CG","p":62521,"la":-4.18,"lo":13.28,"k":0,"z":5.6,"s":4},{"n":"Franceville","a":"Gabon","i":"GA","p":42967,"la":-1.633,"lo":13.583,"k":0,"z":5.6,"s":4},{"n":"Bordeaux","a":"France","i":"FR","p":803000,"la":44.852,"lo":-0.597,"k":0,"z":5,"s":4},{"n":"Marseille","a":"France","i":"FR","p":1400000,"la":43.292,"lo":5.373,"k":0,"z":5,"s":4},{"n":"Le Havre","a":"France","i":"FR","p":242124,"la":49.505,"lo":0.105,"k":0,"z":5,"s":4},{"n":"Gao","a":"Mali","i":"ML","p":116967,"la":16.267,"lo":-0.05,"k":0,"z":5.1,"s":4},{"n":"Coihaique","a":"Chile","i":"CL","p":45787,"la":-45.57,"lo":-72.07,"k":0,"z":5.6,"s":4},{"n":"Arica","a":"Chile","i":"CL","p":185999,"la":-18.5,"lo":-70.29,"k":0,"z":5.6,"s":4},{"n":"Copiapó","a":"Chile","i":"CL","p":129280,"la":-27.372,"lo":-70.33,"k":0,"z":5.6,"s":4},{"n":"La Serena","a":"Chile","i":"CL","p":154521,"la":-29.9,"lo":-71.25,"k":0,"z":5.6,"s":4},{"n":"Los Angeles","a":"Chile","i":"CL","p":145239,"la":-37.48,"lo":-72.347,"k":0,"z":5.6,"s":4},{"n":"Narsarsuaq","a":"Greenland","i":"GL","p":145,"la":61.167,"lo":-45.417,"k":0,"z":5.1,"s":4},{"n":"Sisimiut","a":"Greenland","i":"GL","p":5227,"la":66.95,"lo":-53.667,"k":0,"z":5.1,"s":4},{"n":"Upernavik","a":"Greenland","i":"GL","p":1129,"la":72.709,"lo":-56.142,"k":0,"z":5.1,"s":4},{"n":"Qaanaaq","a":"Greenland","i":"GL","p":616,"la":77.467,"lo":-69.23,"k":0,"z":5.6,"s":4},{"n":"Nouadhibou","a":"Mauritania","i":"MR","p":86738,"la":20.9,"lo":-17.056,"k":0,"z":5.6,"s":4},{"n":"Kayes","a":"Mali","i":"ML","p":77207,"la":14.45,"lo":-11.44,"k":0,"z":5.6,"s":4},{"n":"Ayoun el Atrous","a":"Mauritania","i":"MR","p":1423,"la":16.667,"lo":-9.617,"k":0,"z":5.6,"s":4},{"n":"Ségou","a":"Mali","i":"ML","p":122952,"la":13.44,"lo":-6.26,"k":0,"z":5.6,"s":4},{"n":"Skopje","a":"North Macedonia","i":"MK","p":494087,"la":42,"lo":21.433,"k":1,"z":5.6,"s":4},{"n":"Al Jawf","a":"Libya","i":"LY","p":24132,"la":24.2,"lo":23.29,"k":0,"z":5.6,"s":4},{"n":"Tmassa","a":"Libya","i":"LY","p":500,"la":26.367,"lo":15.8,"k":0,"z":5.6,"s":4},{"n":"Misrata","a":"Libya","i":"LY","p":386120,"la":32.38,"lo":15.1,"k":0,"z":5.6,"s":4},{"n":"Zuwara","a":"Libya","i":"LY","p":180310,"la":32.934,"lo":12.079,"k":0,"z":5.6,"s":4},{"n":"Kirkuk","a":"Iraq","i":"IQ","p":601433,"la":35.472,"lo":44.392,"k":0,"z":5.1,"s":4},{"n":"Mosul","a":"Iraq","i":"IQ","p":1316000,"la":36.347,"lo":43.143,"k":0,"z":5.6,"s":4},{"n":"Najaf","a":"Iraq","i":"IQ","p":665552,"la":32,"lo":44.335,"k":0,"z":5.6,"s":4},{"n":"Bahir Dar","a":"Ethiopia","i":"ET","p":206748,"la":11.6,"lo":37.383,"k":0,"z":5.6,"s":4},{"n":"Mekele","a":"Ethiopia","i":"ET","p":95856,"la":13.5,"lo":39.47,"k":0,"z":5.6,"s":4},{"n":"Dire Dawa","a":"Ethiopia","i":"ET","p":252279,"la":9.59,"lo":41.86,"k":0,"z":5.6,"s":4},{"n":"Rovaniemi","a":"Finland","i":"FI","p":34781,"la":66.5,"lo":25.716,"k":0,"z":5.1,"s":4},{"n":"Vaasa","a":"Finland","i":"FI","p":57014,"la":63.1,"lo":21.6,"k":0,"z":5.1,"s":4},{"n":"Tampere","a":"Finland","i":"FI","p":259279,"la":61.5,"lo":23.75,"k":0,"z":5.1,"s":4},{"n":"Aqtobe","a":"Kazakhstan","i":"KZ","p":262457,"la":50.28,"lo":57.17,"k":0,"z":5.6,"s":4},{"n":"Rudniy","a":"Kazakhstan","i":"KZ","p":124000,"la":52.953,"lo":63.13,"k":0,"z":5.6,"s":4},{"n":"Qyzylorda","a":"Kazakhstan","i":"KZ","p":300000,"la":44.8,"lo":65.465,"k":0,"z":5.6,"s":4},{"n":"Atyrau","a":"Kazakhstan","i":"KZ","p":182753,"la":47.113,"lo":51.92,"k":0,"z":5,"s":4},{"n":"Ekibastuz","a":"Kazakhstan","i":"KZ","p":127868,"la":51.73,"lo":75.32,"k":0,"z":5.6,"s":4},{"n":"Pavlodar","a":"Kazakhstan","i":"KZ","p":329002,"la":52.3,"lo":76.95,"k":0,"z":5.6,"s":4},{"n":"Semey","a":"Kazakhstan","i":"KZ","p":311353,"la":50.435,"lo":80.275,"k":0,"z":5.6,"s":4},{"n":"Oskemen","a":"Kazakhstan","i":"KZ","p":319067,"la":49.99,"lo":82.615,"k":0,"z":5.6,"s":4},{"n":"Yazd","a":"Iran","i":"IR","p":477905,"la":31.92,"lo":54.37,"k":0,"z":5.6,"s":4},{"n":"Ahvaz","a":"Iran","i":"IR","p":996000,"la":31.282,"lo":48.718,"k":0,"z":5.1,"s":4},{"n":"Basra","a":"Iraq","i":"IQ","p":870000,"la":30.515,"lo":47.812,"k":0,"z":5.1,"s":4},{"n":"Bandar-e-Abbas","a":"Iran","i":"IR","p":461499,"la":27.204,"lo":56.272,"k":0,"z":5.6,"s":4},{"n":"Hamadan","a":"Iran","i":"IR","p":528256,"la":34.796,"lo":48.515,"k":0,"z":5.6,"s":4},{"n":"Tabriz","a":"Iran","i":"IR","p":1413000,"la":38.088,"lo":46.299,"k":0,"z":5.1,"s":4},{"n":"Ludhiana","a":"India","i":"IN","p":1649000,"la":30.93,"lo":75.87,"k":0,"z":5.6,"s":4},{"n":"Kota","a":"India","i":"IN","p":827000,"la":25.182,"lo":75.833,"k":0,"z":5.6,"s":4},{"n":"Jodhpur","a":"India","i":"IN","p":995000,"la":26.294,"lo":73.015,"k":0,"z":5.6,"s":4},{"n":"Shymkent","a":"Kazakhstan","i":"KZ","p":465392,"la":42.32,"lo":69.595,"k":0,"z":5.6,"s":4},{"n":"Taraz","a":"Kazakhstan","i":"KZ","p":358153,"la":42.9,"lo":71.365,"k":0,"z":5.6,"s":4},{"n":"Lucknow","a":"India","i":"IN","p":2695000,"la":26.857,"lo":80.913,"k":0,"z":5.6,"s":4},{"n":"Saharanpur","a":"India","i":"IN","p":484873,"la":29.97,"lo":77.55,"k":0,"z":5.6,"s":4},{"n":"Ranchi","a":"India","i":"IN","p":1044000,"la":23.372,"lo":85.328,"k":0,"z":5.6,"s":4},{"n":"Bhagalpur","a":"India","i":"IN","p":361548,"la":25.23,"lo":86.98,"k":0,"z":5.6,"s":4},{"n":"Raipur","a":"India","i":"IN","p":875000,"la":21.237,"lo":81.633,"k":0,"z":5.6,"s":4},{"n":"Jabalpur","a":"India","i":"IN","p":1285000,"la":23.177,"lo":79.953,"k":0,"z":5.6,"s":4},{"n":"Indore","a":"India","i":"IN","p":2026000,"la":22.717,"lo":75.863,"k":0,"z":5.6,"s":4},{"n":"Puducherry","a":"India","i":"IN","p":227411,"la":11.935,"lo":79.83,"k":0,"z":5.1,"s":4},{"n":"Salem","a":"India","i":"IN","p":873000,"la":11.672,"lo":78.178,"k":0,"z":5.6,"s":4},{"n":"Tiruchirappalli","a":"India","i":"IN","p":951000,"la":10.801,"lo":78.688,"k":0,"z":5.6,"s":4},{"n":"Pointe-Noire","a":"Congo (Brazzaville)","i":"CG","p":659084,"la":-4.77,"lo":11.88,"k":0,"z":5.6,"s":4},{"n":"Kankan","a":"Guinea","i":"GN","p":114009,"la":10.39,"lo":-9.31,"k":0,"z":5.1,"s":4},{"n":"Nzérékoré","a":"Guinea","i":"GN","p":168121,"la":7.76,"lo":-8.83,"k":0,"z":5.1,"s":4},{"n":"Bouaké","a":"Ivory Coast","i":"CI","p":567481,"la":7.69,"lo":-5.03,"k":0,"z":5.6,"s":4},{"n":"St.-Denis","a":"France","i":"RE","p":190047,"la":-20.879,"lo":55.448,"k":0,"z":5.6,"s":4},{"n":"Rio Branco","a":"Brazil","i":"BR","p":257642,"la":-9.967,"lo":-67.8,"k":0,"z":5.6,"s":4},{"n":"São Luís","a":"Brazil","i":"BR","p":1038000,"la":-2.514,"lo":-44.268,"k":0,"z":5.6,"s":4},{"n":"Porto Velho","a":"Brazil","i":"BR","p":306180,"la":-8.75,"lo":-63.9,"k":0,"z":5.1,"s":4},{"n":"Alvorada","a":"Brazil","i":"BR","p":10232,"la":-12.47,"lo":-49.082,"k":0,"z":5.6,"s":4},{"n":"Corumbá","a":"Brazil","i":"BR","p":96520,"la":-19.016,"lo":-57.65,"k":0,"z":5.6,"s":4},{"n":"Belo Horizonte","a":"Brazil","i":"BR","p":5575000,"la":-19.913,"lo":-43.917,"k":0,"z":5.1,"s":4},{"n":"Montes Claros","a":"Brazil","i":"BR","p":332379,"la":-16.72,"lo":-43.86,"k":0,"z":5.6,"s":4},{"n":"Uberlândia","a":"Brazil","i":"BR","p":563536,"la":-18.9,"lo":-48.28,"k":0,"z":5.6,"s":4},{"n":"Colíder","a":"Brazil","i":"BR","p":27139,"la":-10.817,"lo":-55.451,"k":0,"z":5.6,"s":4},{"n":"Alta Floresta","a":"Brazil","i":"BR","p":40466,"la":-9.9,"lo":-55.91,"k":0,"z":5.6,"s":4},{"n":"Cuiabá","a":"Brazil","i":"BR","p":806000,"la":-15.568,"lo":-56.087,"k":0,"z":5.1,"s":4},{"n":"Pelotas","a":"Brazil","i":"BR","p":320674,"la":-31.75,"lo":-52.33,"k":0,"z":5.6,"s":4},{"n":"Caxias do Sul","a":"Brazil","i":"BR","p":381270,"la":-29.18,"lo":-51.17,"k":0,"z":5.6,"s":4},{"n":"Ponta Grossa","a":"Brazil","i":"BR","p":292177,"la":-25.09,"lo":-50.16,"k":0,"z":5.6,"s":4},{"n":"Teresina","a":"Brazil","i":"BR","p":907000,"la":-5.093,"lo":-42.782,"k":0,"z":5.6,"s":4},{"n":"Maceió","a":"Brazil","i":"BR","p":1186000,"la":-9.618,"lo":-35.732,"k":0,"z":5.6,"s":4},{"n":"Vitória da Conquista","a":"Brazil","i":"BR","p":308204,"la":-14.85,"lo":-40.84,"k":0,"z":5.6,"s":4},{"n":"Barreiras","a":"Brazil","i":"BR","p":158292,"la":-12.14,"lo":-45,"k":0,"z":5.6,"s":4},{"n":"Vila Velha","a":"Brazil","i":"BR","p":1209091,"la":-20.338,"lo":-40.29,"k":0,"z":5.6,"s":4},{"n":"Natal","a":"Brazil","i":"BR","p":1088000,"la":-5.778,"lo":-35.242,"k":0,"z":5.6,"s":4},{"n":"Thompson","a":"Canada","i":"CA","p":13727,"la":55.75,"lo":-97.867,"k":0,"z":5.6,"s":4},{"n":"Brandon","a":"Canada","i":"CA","p":28418,"la":49.833,"lo":-99.95,"k":0,"z":5.6,"s":4},{"n":"Fort Smith","a":"Canada","i":"CA","p":518,"la":60,"lo":-111.883,"k":0,"z":5.1,"s":4},{"n":"Fort McMurray","a":"Canada","i":"CA","p":21863,"la":56.733,"lo":-111.383,"k":0,"z":5.6,"s":4},{"n":"Peace River","a":"Canada","i":"CA","p":5340,"la":56.233,"lo":-117.283,"k":0,"z":5.6,"s":4},{"n":"Fort St. John","a":"Canada","i":"CA","p":18776,"la":56.25,"lo":-120.833,"k":0,"z":5.1,"s":4},{"n":"Iqaluit","a":"Canada","i":"CA","p":6124,"la":63.75,"lo":-68.5,"k":0,"z":5.1,"s":4},{"n":"Cambridge Bay","a":"Canada","i":"CA","p":1477,"la":69.117,"lo":-105.033,"k":0,"z":5.6,"s":4},{"n":"Kugluktuk","a":"Canada","i":"CA","p":1302,"la":67.799,"lo":-115.125,"k":0,"z":5.6,"s":4},{"n":"Chesterfield Inlet","a":"Canada","i":"CA","p":374,"la":63.338,"lo":-90.7,"k":0,"z":5.6,"s":4},{"n":"Arviat","a":"Canada","i":"CA","p":1868,"la":61.109,"lo":-94.059,"k":0,"z":5.6,"s":4},{"n":"Taloyoak","a":"Canada","i":"CA","p":774,"la":69.533,"lo":-93.533,"k":0,"z":5.6,"s":4},{"n":"Igloolik","a":"Canada","i":"CA","p":1612,"la":69.257,"lo":-81.794,"k":0,"z":5.6,"s":4},{"n":"Dawson City","a":"Canada","i":"CA","p":1319,"la":64.067,"lo":-139.417,"k":0,"z":5.1,"s":4},{"n":"Timmins","a":"Canada","i":"CA","p":34974,"la":48.467,"lo":-81.333,"k":0,"z":5.6,"s":4},{"n":"North Bay","a":"Canada","i":"CA","p":50170,"la":46.3,"lo":-79.45,"k":0,"z":5.6,"s":4},{"n":"Kuujjuarapik","a":"Canada","i":"CA","p":1243,"la":55.281,"lo":-77.766,"k":0,"z":5.6,"s":4},{"n":"Kuujjuaq","a":"Canada","i":"CA","p":1273,"la":58.1,"lo":-68.4,"k":0,"z":5.6,"s":4},{"n":"Sydney","a":"Canada","i":"CA","p":37538,"la":46.066,"lo":-60.18,"k":0,"z":5.6,"s":4},{"n":"Labrador City","a":"Canada","i":"CA","p":10313,"la":52.941,"lo":-66.916,"k":0,"z":5.6,"s":4},{"n":"Ebolowa","a":"Cameroon","i":"CM","p":87875,"la":2.9,"lo":11.15,"k":0,"z":5.6,"s":4},{"n":"Bambari","a":"Central African Republic","i":"CF","p":62098,"la":5.762,"lo":20.667,"k":0,"z":5.6,"s":4},{"n":"Venice","a":"Italy","i":"IT","p":270816,"la":45.439,"lo":12.335,"k":0,"z":5.1,"s":4},{"n":"El Calafate","a":"Argentina","i":"AR","p":8000,"la":-50.333,"lo":-72.3,"k":0,"z":5.6,"s":4},{"n":"San Juan","a":"Argentina","i":"AR","p":447048,"la":-31.55,"lo":-68.52,"k":0,"z":5.6,"s":4},{"n":"Rawson","a":"Argentina","i":"AR","p":26335,"la":-43.3,"lo":-65.1,"k":0,"z":5.1,"s":4},{"n":"Neuquén","a":"Argentina","i":"AR","p":242092,"la":-38.95,"lo":-68.06,"k":0,"z":5.1,"s":4},{"n":"Trinidad","a":"Bolivia","i":"BO","p":84259,"la":-14.833,"lo":-64.9,"k":0,"z":5.6,"s":4},{"n":"Santa Rosa","a":"Argentina","i":"AR","p":111424,"la":-36.62,"lo":-64.3,"k":0,"z":5.1,"s":4},{"n":"Bariloche","a":"Argentina","i":"AR","p":95394,"la":-41.15,"lo":-71.3,"k":0,"z":5.6,"s":4},{"n":"Salta","a":"Argentina","i":"AR","p":512686,"la":-24.783,"lo":-65.417,"k":0,"z":5.6,"s":4},{"n":"Tucumán","a":"Argentina","i":"AR","p":830000,"la":-26.814,"lo":-65.219,"k":0,"z":5.6,"s":4},{"n":"Formosa","a":"Argentina","i":"AR","p":221383,"la":-26.173,"lo":-58.183,"k":0,"z":5.6,"s":4},{"n":"Santa Fe","a":"Argentina","i":"AR","p":489505,"la":-31.624,"lo":-60.69,"k":0,"z":5.6,"s":4},{"n":"Rosario","a":"Argentina","i":"AR","p":1203000,"la":-32.949,"lo":-60.668,"k":0,"z":5.1,"s":4},{"n":"Campinas","a":"Brazil","i":"BR","p":2791000,"la":-22.908,"lo":-47.056,"k":0,"z":5.6,"s":4},{"n":"Sorocaba","a":"Brazil","i":"BR","p":563281,"la":-23.49,"lo":-47.47,"k":0,"z":5.6,"s":4},{"n":"Ribeirão Preto","a":"Brazil","i":"BR","p":551267,"la":-21.17,"lo":-47.83,"k":0,"z":5.6,"s":4},{"n":"Petrolina","a":"Brazil","i":"BR","p":260985,"la":-9.38,"lo":-40.51,"k":0,"z":5.6,"s":4},{"n":"Bamenda","a":"Cameroon","i":"CM","p":445299,"la":5.96,"lo":10.15,"k":0,"z":5.6,"s":4},{"n":"Garoua","a":"Cameroon","i":"CM","p":436899,"la":9.3,"lo":13.39,"k":0,"z":5.6,"s":4},{"n":"Herat","a":"Afghanistan","i":"AF","p":481009,"la":34.33,"lo":62.17,"k":0,"z":5.1,"s":4},{"n":"Mazar-i-Sharif","a":"Afghanistan","i":"AF","p":458151,"la":36.7,"lo":67.1,"k":0,"z":5.1,"s":4},{"n":"Battambang","a":"Cambodia","i":"KH","p":154773,"la":13.1,"lo":103.2,"k":0,"z":5.6,"s":4},{"n":"Siem Reap","a":"Cambodia","i":"KH","p":109398,"la":13.367,"lo":103.85,"k":0,"z":5.6,"s":4},{"n":"Malanje","a":"Angola","i":"AO","p":125856,"la":-9.54,"lo":16.34,"k":0,"z":5.6,"s":4},{"n":"Benguela","a":"Angola","i":"AO","p":151226,"la":-12.578,"lo":13.407,"k":0,"z":5.6,"s":4},{"n":"Lubango","a":"Angola","i":"AO","p":125632,"la":-14.91,"lo":13.49,"k":0,"z":5.6,"s":4},{"n":"Moçâmedes","a":"Angola","i":"AO","p":132900,"la":-15.19,"lo":12.16,"k":0,"z":5.6,"s":4},{"n":"Tarija","a":"Bolivia","i":"BO","p":159269,"la":-21.517,"lo":-64.75,"k":0,"z":5.6,"s":4},{"n":"Bridgetown","a":"Barbados","i":"BB","p":191152,"la":13.102,"lo":-59.617,"k":1,"z":5.6,"s":4},{"n":"Annaba","a":"Algeria","i":"DZ","p":503524,"la":36.92,"lo":7.76,"k":0,"z":5.6,"s":4},{"n":"Parakou","a":"Benin","i":"BJ","p":188853,"la":9.34,"lo":2.62,"k":0,"z":5.6,"s":4},{"n":"Porto-Novo","a":"Benin","i":"BJ","p":300000,"la":6.483,"lo":2.617,"k":0,"z":5.6,"s":4},{"n":"Constantine","a":"Algeria","i":"DZ","p":605179,"la":36.36,"lo":6.6,"k":0,"z":5.6,"s":4},{"n":"Brest","a":"Belarus","i":"BY","p":300715,"la":52.1,"lo":23.7,"k":0,"z":5.1,"s":4},{"n":"Khulna","a":"Bangladesh","i":"BD","p":1553000,"la":22.842,"lo":89.558,"k":0,"z":5.6,"s":4},{"n":"Francistown","a":"Botswana","i":"BW","p":89979,"la":-21.17,"lo":27.5,"k":0,"z":5.6,"s":4},{"n":"Mahalapye","a":"Botswana","i":"BW","p":50744,"la":-23.1,"lo":26.82,"k":0,"z":5.6,"s":4},{"n":"Serowe","a":"Botswana","i":"BW","p":48573,"la":-22.39,"lo":26.71,"k":0,"z":5.6,"s":4},{"n":"Katherine","a":"Australia","i":"AU","p":10141,"la":-14.467,"lo":132.267,"k":0,"z":5.6,"s":4},{"n":"Busselton","a":"Australia","i":"AU","p":10214,"la":-33.656,"lo":115.349,"k":0,"z":5.6,"s":4},{"n":"Mandurah","a":"Australia","i":"AU","p":73356,"la":-32.523,"lo":115.747,"k":0,"z":5.6,"s":4},{"n":"Broome","a":"Australia","i":"AU","p":13218,"la":-17.962,"lo":122.231,"k":0,"z":5,"s":4},{"n":"Kalgoorlie","a":"Australia","i":"AU","p":36852,"la":-30.735,"lo":121.46,"k":0,"z":5.1,"s":4},{"n":"Albany","a":"Australia","i":"AU","p":26445,"la":-35.017,"lo":117.892,"k":0,"z":5,"s":4},{"n":"Port Hedland","a":"Australia","i":"AU","p":14288,"la":-20.31,"lo":118.606,"k":0,"z":5.1,"s":4},{"n":"Karratha","a":"Australia","i":"AU","p":16796,"la":-20.73,"lo":116.87,"k":0,"z":5.6,"s":4},{"n":"Geraldton","a":"Australia","i":"AU","p":27258,"la":-28.767,"lo":114.6,"k":0,"z":5,"s":4},{"n":"Griffith","a":"Australia","i":"AU","p":15455,"la":-34.29,"lo":146.04,"k":0,"z":5,"s":4},{"n":"Orange","a":"Australia","i":"AU","p":39329,"la":-33.28,"lo":149.1,"k":0,"z":5.6,"s":4},{"n":"Dubbo","a":"Australia","i":"AU","p":31574,"la":-32.26,"lo":148.597,"k":0,"z":5,"s":4},{"n":"Armidale","a":"Australia","i":"AU","p":22673,"la":-30.512,"lo":151.667,"k":0,"z":5.6,"s":4},{"n":"Broken Hill","a":"Australia","i":"AU","p":17395,"la":-31.95,"lo":141.433,"k":0,"z":5.1,"s":4},{"n":"Port Lincoln","a":"Australia","i":"AU","p":13630,"la":-34.733,"lo":135.867,"k":0,"z":5.6,"s":4},{"n":"Whyalla","a":"Australia","i":"AU","p":22559,"la":-33.025,"lo":137.561,"k":0,"z":5,"s":4},{"n":"Portland","a":"Australia","i":"AU","p":11808,"la":-38.34,"lo":141.59,"k":0,"z":5,"s":4},{"n":"Bendigo","a":"Australia","i":"AU","p":81657,"la":-36.76,"lo":144.28,"k":0,"z":5.6,"s":4},{"n":"Wangaratta","a":"Australia","i":"AU","p":14022,"la":-36.36,"lo":146.3,"k":0,"z":5.6,"s":4},{"n":"Windorah","a":"Australia","i":"AU","p":158,"la":-25.433,"lo":142.65,"k":0,"z":5,"s":4},{"n":"Mount Isa","a":"Australia","i":"AU","p":33200,"la":-20.724,"lo":139.49,"k":0,"z":5.1,"s":4},{"n":"Rockhampton","a":"Australia","i":"AU","p":65850,"la":-23.364,"lo":150.52,"k":0,"z":5.6,"s":4},{"n":"Cairns","a":"Australia","i":"AU","p":154225,"la":-16.888,"lo":145.763,"k":0,"z":5,"s":4},{"n":"Gold Coast","a":"Australia","i":"AU","p":527660,"la":-28.082,"lo":153.448,"k":0,"z":5,"s":4},{"n":"Devonport","a":"Australia","i":"AU","p":19317,"la":-41.193,"lo":146.331,"k":0,"z":5.6,"s":4},{"n":"Bobo Dioulasso","a":"Burkina Faso","i":"BF","p":360106,"la":11.18,"lo":-4.29,"k":0,"z":5.6,"s":4},{"n":"Rajshahi","a":"Bangladesh","i":"BD","p":810000,"la":24.377,"lo":88.603,"k":0,"z":5.6,"s":4},{"n":"Mandalay","a":"Myanmar","i":"MM","p":1300000,"la":21.972,"lo":96.083,"k":0,"z":5.6,"s":4},{"n":"Sittwe","a":"Myanmar","i":"MM","p":179032,"la":20.14,"lo":92.88,"k":0,"z":5.6,"s":4},{"n":"Bujumbura","a":"Burundi","i":"BI","p":331700,"la":-3.376,"lo":29.36,"k":1,"z":5.6,"s":4},{"n":"Pago Pago","a":"American Samoa","i":"AS","p":12576,"la":-14.277,"lo":-170.707,"k":0,"z":5.6,"s":4},{"n":"Kingstown","a":"Saint Vincent and the Grenadines","i":"VC","p":49485,"la":13.156,"lo":-61.22,"k":1,"z":5.1,"s":4},{"n":"Castries","a":"Saint Lucia","i":"LC","p":37963,"la":14.008,"lo":-60.993,"k":1,"z":5.6,"s":4},{"n":"Basseterre","a":"Saint Kitts and Nevis","i":"KN","p":21887,"la":17.302,"lo":-62.717,"k":1,"z":5.6,"s":4},{"n":"Las Palmas","a":"Spain","i":"ES","p":378495,"la":28.1,"lo":-15.43,"k":0,"z":5.1,"s":4},{"n":"Berbera","a":"Somaliland","i":"-99","p":242344,"la":10.436,"lo":45.016,"k":0,"z":5.6,"s":4},{"n":"Port Louis","a":"Mauritius","i":"MU","p":595491,"la":-20.167,"lo":57.5,"k":1,"z":5.6,"s":4},{"n":"Gaza City","a":"Palestine","i":"PS","p":505700,"la":31.53,"lo":34.445,"k":0,"z":5.6,"s":4},{"n":"Saint George's","a":"Grenada","i":"GD","p":33734,"la":12.053,"lo":-61.742,"k":1,"z":5.1,"s":4},{"n":"Papeete","a":"French Polynesia","i":"PF","p":131695,"la":-17.533,"lo":-149.567,"k":0,"z":5.6,"s":4},{"n":"Manama","a":"Bahrain","i":"BH","p":563920,"la":26.236,"lo":50.583,"k":1,"z":5.6,"s":4},{"n":"Freeport","a":"The Bahamas","i":"BS","p":25383,"la":26.533,"lo":-78.7,"k":0,"z":5.6,"s":4},{"n":"Saint John's","a":"Antigua and Barbuda","i":"AG","p":35499,"la":17.118,"lo":-61.85,"k":1,"z":5.1,"s":4},{"n":"Taichung","a":"Taiwan","i":"TW","p":2629323,"la":24.152,"lo":120.682,"k":0,"z":5.6,"s":4},{"n":"Napier","a":"New Zealand","i":"NZ","p":57800,"la":-39.49,"lo":176.926,"k":0,"z":5,"s":4},{"n":"Manukau","a":"New Zealand","i":"NZ","p":375600,"la":-37,"lo":174.885,"k":0,"z":5.6,"s":4},{"n":"Hamilton","a":"New Zealand","i":"NZ","p":148200,"la":-37.778,"lo":175.29,"k":0,"z":5,"s":4},{"n":"Blenheim","a":"New Zealand","i":"NZ","p":30300,"la":-41.514,"lo":173.955,"k":0,"z":5,"s":4},{"n":"Dunedin","a":"New Zealand","i":"NZ","p":117700,"la":-45.885,"lo":170.491,"k":0,"z":5,"s":4},{"n":"Leh","a":"India","i":"IN","p":30870,"la":34.162,"lo":77.572,"k":0,"z":5.6,"s":4},{"n":"Amaravati","a":"India","i":"IN","p":5800000,"la":16.534,"lo":80.524,"k":0,"z":5.6,"s":4},{"n":"Kozhikode","a":"India","i":"IN","p":953000,"la":11.252,"lo":75.768,"k":0,"z":5,"s":3},{"n":"Bhubaneswar","a":"India","i":"IN","p":844000,"la":20.272,"lo":85.825,"k":0,"z":5,"s":3},{"n":"Jamshedpur","a":"India","i":"IN","p":1300000,"la":22.789,"lo":86.196,"k":0,"z":5,"s":3},{"n":"Montevideo","a":"Uruguay","i":"UY","p":1513000,"la":-34.905,"lo":-56.187,"k":1,"z":4,"s":3},{"n":"Helena","a":"United States of America","i":"US","p":38725,"la":46.593,"lo":-112.035,"k":0,"z":5,"s":3},{"n":"Bismarck","a":"United States of America","i":"US","p":63871,"la":46.808,"lo":-100.783,"k":0,"z":5,"s":3},{"n":"Boise","a":"United States of America","i":"US","p":338071,"la":43.609,"lo":-116.227,"k":0,"z":5,"s":3},{"n":"San Jose","a":"United States of America","i":"US","p":1668000,"la":37.331,"lo":-121.888,"k":0,"z":5,"s":3},{"n":"Sacramento","a":"United States of America","i":"US","p":1604000,"la":38.577,"lo":-121.472,"k":0,"z":5,"s":3},{"n":"Las Vegas","a":"United States of America","i":"US","p":1823000,"la":36.165,"lo":-115.151,"k":0,"z":5,"s":3},{"n":"Santa Fe","a":"United States of America","i":"US","p":92681,"la":35.687,"lo":-105.937,"k":0,"z":5,"s":3},{"n":"Portland","a":"United States of America","i":"US","p":1875000,"la":45.522,"lo":-122.682,"k":0,"z":4,"s":3},{"n":"Salt Lake City","a":"United States of America","i":"US","p":966000,"la":40.777,"lo":-111.932,"k":0,"z":4,"s":3},{"n":"Cheyenne","a":"United States of America","i":"US","p":72927,"la":41.14,"lo":-104.82,"k":0,"z":5,"s":3},{"n":"Des Moines","a":"United States of America","i":"US","p":380655,"la":41.58,"lo":-93.62,"k":0,"z":5,"s":3},{"n":"Omaha","a":"United States of America","i":"US","p":877110,"la":41.24,"lo":-96.01,"k":0,"z":5,"s":3},{"n":"Oklahoma City","a":"United States of America","i":"US","p":788000,"la":35.472,"lo":-97.521,"k":0,"z":5,"s":3},{"n":"Pierre","a":"United States of America","i":"US","p":13879,"la":44.368,"lo":-100.351,"k":0,"z":5,"s":3},{"n":"San Antonio","a":"United States of America","i":"US","p":1473000,"la":29.42,"lo":-98.493,"k":0,"z":5,"s":3},{"n":"San Cristóbal","a":"Venezuela","i":"VE","p":438798,"la":7.77,"lo":-72.25,"k":0,"z":5,"s":3},{"n":"Valencia","a":"Venezuela","i":"VE","p":1770000,"la":10.232,"lo":-67.982,"k":0,"z":5,"s":3},{"n":"Jackson","a":"United States of America","i":"US","p":250902,"la":32.299,"lo":-90.185,"k":0,"z":5,"s":3},{"n":"Raleigh","a":"United States of America","i":"US","p":1163515,"la":35.773,"lo":-78.647,"k":0,"z":5,"s":3},{"n":"Cleveland","a":"United States of America","i":"US","p":1890000,"la":41.472,"lo":-81.697,"k":0,"z":5,"s":3},{"n":"Cincinnati","a":"United States of America","i":"US","p":1636000,"la":39.164,"lo":-84.459,"k":0,"z":5,"s":3},{"n":"Nashville","a":"United States of America","i":"US","p":877000,"la":36.172,"lo":-86.782,"k":0,"z":5,"s":3},{"n":"Memphis","a":"United States of America","i":"US","p":1081000,"la":35.145,"lo":-90.029,"k":0,"z":5,"s":3},{"n":"Norfolk","a":"United States of America","i":"US","p":1047869,"la":36.85,"lo":-76.28,"k":0,"z":5.7,"s":3},{"n":"Milwaukee","a":"United States of America","i":"US","p":1388000,"la":43.03,"lo":-87.917,"k":0,"z":5,"s":3},{"n":"Buffalo","a":"United States of America","i":"US","p":1016000,"la":42.882,"lo":-78.882,"k":0,"z":5,"s":3},{"n":"Pittsburgh","a":"United States of America","i":"US","p":1838000,"la":40.432,"lo":-80.002,"k":0,"z":5,"s":3},{"n":"Ciudad Guayana","a":"Venezuela","i":"VE","p":746535,"la":8.37,"lo":-62.62,"k":0,"z":5,"s":3},{"n":"Lomé","a":"Togo","i":"TG","p":1452000,"la":6.134,"lo":1.221,"k":1,"z":5,"s":3},{"n":"Tunis","a":"Tunisia","i":"TN","p":2412500,"la":36.803,"lo":10.18,"k":1,"z":5,"s":3},{"n":"Kodiak","a":"United States of America","i":"US","p":9461,"la":57.79,"lo":-152.407,"k":0,"z":5,"s":3},{"n":"Cold Bay","a":"United States of America","i":"US","p":200,"la":55.2,"lo":-162.715,"k":0,"z":5,"s":3},{"n":"Bethel","a":"United States of America","i":"US","p":6228,"la":60.793,"lo":-161.756,"k":0,"z":5,"s":3},{"n":"Point Hope","a":"United States of America","i":"US","p":461,"la":68.348,"lo":-166.808,"k":0,"z":5,"s":3},{"n":"Utqiaġvik","a":"United States of America","i":"US","p":4336,"la":71.291,"lo":-156.789,"k":0,"z":5,"s":3},{"n":"Nome","a":"United States of America","i":"US","p":3485,"la":64.506,"lo":-165.406,"k":0,"z":4,"s":3},{"n":"Valdez","a":"United States of America","i":"US","p":4036,"la":61.136,"lo":-146.348,"k":0,"z":5,"s":3},{"n":"Juneau","a":"United States of America","i":"US","p":30711,"la":58.303,"lo":-134.401,"k":0,"z":4,"s":3},{"n":"Fairbanks","a":"United States of America","i":"US","p":56993,"la":64.837,"lo":-147.711,"k":0,"z":4,"s":3},{"n":"Prudhoe Bay","a":"United States of America","i":"US","p":2500,"la":70.292,"lo":-148.669,"k":0,"z":3.7,"s":3},{"n":"Sevastopol","a":"Russia","i":"RU","p":379200,"la":44.604,"lo":33.524,"k":0,"z":4,"s":3},{"n":"Abu Dhabi","a":"United Arab Emirates","i":"AE","p":603492,"la":24.467,"lo":54.367,"k":1,"z":5,"s":3},{"n":"Ashgabat","a":"Turkmenistan","i":"TM","p":727700,"la":37.95,"lo":58.383,"k":1,"z":5,"s":3},{"n":"Samarkand","a":"Uzbekistan","i":"UZ","p":708000,"la":39.67,"lo":66.945,"k":0,"z":5,"s":3},{"n":"Lusaka","a":"Zambia","i":"ZM","p":1328000,"la":-15.415,"lo":28.281,"k":1,"z":4,"s":3},{"n":"Harare","a":"Zimbabwe","i":"ZW","p":1572000,"la":-17.816,"lo":31.043,"k":1,"z":4,"s":3},{"n":"Bulawayo","a":"Zimbabwe","i":"ZW","p":699385,"la":-20.17,"lo":28.58,"k":0,"z":5,"s":3},{"n":"Dili","a":"East Timor","i":"TL","p":234331,"la":-8.559,"lo":125.579,"k":1,"z":5,"s":3},{"n":"Port Vila","a":"Vanuatu","i":"VU","p":44040,"la":-17.733,"lo":168.317,"k":1,"z":4,"s":3},{"n":"Tegucigalpa","a":"Honduras","i":"HN","p":946000,"la":14.104,"lo":-87.219,"k":1,"z":5,"s":3},{"n":"Georgetown","a":"Guyana","i":"GY","p":264350,"la":6.802,"lo":-58.167,"k":1,"z":4,"s":3},{"n":"Reykjavík","a":"Iceland","i":"IS","p":166212,"la":64.143,"lo":-21.937,"k":1,"z":3.7,"s":3},{"n":"Port-au-Prince","a":"Haiti","i":"HT","p":1998000,"la":18.543,"lo":-72.338,"k":1,"z":5,"s":3},{"n":"Glasgow","a":"United Kingdom","i":"GB","p":1160000,"la":55.862,"lo":-4.239,"k":0,"z":4,"s":3},{"n":"Kampala","a":"Uganda","i":"UG","p":1420000,"la":0.319,"lo":32.581,"k":1,"z":5,"s":3},{"n":"Aden","a":"Yemen","i":"YE","p":1000000,"la":12.78,"lo":45.009,"k":0,"z":5,"s":3},{"n":"Paramaribo","a":"Suriname","i":"SR","p":254169,"la":5.835,"lo":-55.167,"k":1,"z":5,"s":3},{"n":"Seville","a":"Spain","i":"ES","p":1212045,"la":37.405,"lo":-5.98,"k":0,"z":5,"s":3},{"n":"Zinder","a":"Niger","i":"NE","p":230358,"la":13.8,"lo":8.983,"k":0,"z":5,"s":3},{"n":"Niamey","a":"Niger","i":"NE","p":915000,"la":13.519,"lo":2.115,"k":1,"z":5,"s":3},{"n":"Port Sudan","a":"Sudan","i":"SD","p":489725,"la":19.616,"lo":37.216,"k":0,"z":5,"s":3},{"n":"Dushanbe","a":"Tajikistan","i":"TJ","p":1086244,"la":38.56,"lo":68.774,"k":1,"z":5,"s":3},{"n":"Cusco","a":"Peru","i":"PE","p":361182,"la":-13.525,"lo":-71.972,"k":0,"z":4,"s":3},{"n":"Tacna","a":"Peru","i":"PE","p":280098,"la":-18,"lo":-70.25,"k":0,"z":5,"s":3},{"n":"Trujillo","a":"Peru","i":"PE","p":765171,"la":-8.12,"lo":-79.02,"k":0,"z":4,"s":3},{"n":"Ica","a":"Peru","i":"PE","p":279420,"la":-14.068,"lo":-75.725,"k":0,"z":5,"s":3},{"n":"Asunción","a":"Paraguay","i":"PY","p":1870000,"la":-25.291,"lo":-57.626,"k":1,"z":4,"s":3},{"n":"Managua","a":"Nicaragua","i":"NI","p":920000,"la":12.155,"lo":-86.27,"k":1,"z":5,"s":3},{"n":"Freetown","a":"Sierra Leone","i":"SL","p":827000,"la":8.472,"lo":-13.236,"k":1,"z":5,"s":3},{"n":"Agadez","a":"Niger","i":"NE","p":117770,"la":16.996,"lo":7.983,"k":0,"z":5,"s":3},{"n":"Nyala","a":"Sudan","i":"SD","p":392373,"la":12.06,"lo":24.89,"k":0,"z":5,"s":3},{"n":"Wau","a":"South Sudan","i":"SS","p":127384,"la":7.7,"lo":27.99,"k":0,"z":5,"s":3},{"n":"Dongola","a":"Sudan","i":"SD","p":26404,"la":19.167,"lo":30.483,"k":0,"z":5,"s":3},{"n":"Kassala","a":"Sudan","i":"SD","p":401477,"la":15.46,"lo":36.39,"k":0,"z":5,"s":3},{"n":"Tromsø","a":"Norway","i":"NO","p":52436,"la":69.635,"lo":18.992,"k":0,"z":3.7,"s":3},{"n":"Trondheim","a":"Norway","i":"NO","p":147139,"la":63.417,"lo":10.417,"k":0,"z":4,"s":3},{"n":"Bergen","a":"Norway","i":"NO","p":213585,"la":60.391,"lo":5.325,"k":0,"z":5,"s":3},{"n":"Islamabad","a":"Pakistan","i":"PK","p":780000,"la":33.689,"lo":73.081,"k":1,"z":5,"s":3},{"n":"Multan","a":"Pakistan","i":"PK","p":1522000,"la":30.202,"lo":71.453,"k":0,"z":5,"s":3},{"n":"Hyderabad","a":"Pakistan","i":"PK","p":1459000,"la":25.382,"lo":68.373,"k":0,"z":5,"s":3},{"n":"Peshawar","a":"Pakistan","i":"PK","p":1303000,"la":34.007,"lo":71.533,"k":0,"z":5,"s":3},{"n":"Kathmandu","a":"Nepal","i":"NP","p":895000,"la":27.719,"lo":85.315,"k":1,"z":5,"s":3},{"n":"Nacala","a":"Mozambique","i":"MZ","p":224795,"la":-14.519,"lo":40.715,"k":0,"z":5,"s":3},{"n":"Bloemfontein","a":"South Africa","i":"ZA","p":463064,"la":-29.12,"lo":26.23,"k":1,"z":5,"s":3},{"n":"Pretoria","a":"South Africa","i":"ZA","p":1338000,"la":-25.705,"lo":28.227,"k":1,"z":5,"s":3},{"n":"Port Moresby","a":"Papua New Guinea","i":"PG","p":283733,"la":-9.465,"lo":147.193,"k":1,"z":4,"s":3},{"n":"Honiara","a":"Solomon Islands","i":"SB","p":76328,"la":-9.438,"lo":159.95,"k":1,"z":5,"s":3},{"n":"Panama City","a":"Panama","i":"PA","p":1281000,"la":8.97,"lo":-79.535,"k":1,"z":5,"s":3},{"n":"Fez","a":"Morocco","i":"MA","p":1002000,"la":34.057,"lo":-5.002,"k":0,"z":5,"s":3},{"n":"Rabat","a":"Morocco","i":"MA","p":1705000,"la":34.025,"lo":-6.836,"k":1,"z":5,"s":3},{"n":"Marrakesh","a":"Morocco","i":"MA","p":872000,"la":31.632,"lo":-8.002,"k":0,"z":5,"s":3},{"n":"Chișinău","a":"Moldova","i":"MD","p":688134,"la":47.005,"lo":28.858,"k":1,"z":5,"s":3},{"n":"Beira","a":"Mozambique","i":"MZ","p":530604,"la":-19.82,"lo":34.87,"k":0,"z":5,"s":3},{"n":"Port Elizabeth","a":"South Africa","i":"ZA","p":1021000,"la":-33.968,"lo":25.598,"k":0,"z":5,"s":3},{"n":"Maputo","a":"Mozambique","i":"MZ","p":1446000,"la":-25.953,"lo":32.587,"k":1,"z":5,"s":3},{"n":"Tomsk","a":"Russia","i":"RU","p":535479,"la":56.495,"lo":84.975,"k":0,"z":4,"s":3},{"n":"Anadyr","a":"Russia","i":"RU","p":10332,"la":64.737,"lo":177.475,"k":0,"z":5,"s":3},{"n":"Murmansk","a":"Russia","i":"RU","p":319263,"la":68.97,"lo":33.1,"k":0,"z":3.7,"s":3},{"n":"Archangel","a":"Russia","i":"RU","p":356051,"la":64.575,"lo":40.545,"k":0,"z":4,"s":3},{"n":"Nizhny Novgorod","a":"Russia","i":"RU","p":1278000,"la":56.335,"lo":43.998,"k":0,"z":5,"s":3},{"n":"Volgograd","a":"Russia","i":"RU","p":984000,"la":48.712,"lo":44.498,"k":0,"z":4,"s":3},{"n":"Ufa","a":"Russia","i":"RU","p":1018000,"la":54.792,"lo":56.038,"k":0,"z":5,"s":3},{"n":"Yekaterinburg","a":"Russia","i":"RU","p":1313000,"la":56.852,"lo":60.598,"k":0,"z":5,"s":3},{"n":"Samara","a":"Russia","i":"RU","p":1137000,"la":53.197,"lo":50.149,"k":0,"z":3.7,"s":3},{"n":"Kazan","a":"Russia","i":"RU","p":1115000,"la":55.752,"lo":49.124,"k":0,"z":5,"s":3},{"n":"Surgut","a":"Russia","i":"RU","p":400000,"la":61.26,"lo":73.425,"k":0,"z":4,"s":3},{"n":"Barnaul","a":"Russia","i":"RU","p":599579,"la":53.355,"lo":83.745,"k":0,"z":5,"s":3},{"n":"Novosibirsk","a":"Russia","i":"RU","p":1389000,"la":55.032,"lo":82.958,"k":0,"z":5,"s":3},{"n":"Mogadishu","a":"Somalia","i":"SO","p":1100000,"la":2.069,"lo":45.365,"k":1,"z":4,"s":3},{"n":"Muscat","a":"Oman","i":"OM","p":734697,"la":23.585,"lo":58.378,"k":1,"z":5,"s":3},{"n":"Colombo","a":"Sri Lanka","i":"LK","p":217000,"la":6.932,"lo":79.858,"k":1,"z":5,"s":3},{"n":"Cebu","a":"Philippines","i":"PH","p":815000,"la":10.322,"lo":123.898,"k":0,"z":4,"s":3},{"n":"Iloilo","a":"Philippines","i":"PH","p":387681,"la":10.705,"lo":122.545,"k":0,"z":5,"s":3},{"n":"Davao","a":"Philippines","i":"PH","p":1402000,"la":7.112,"lo":125.628,"k":0,"z":5,"s":3},{"n":"Bratsk","a":"Russia","i":"RU","p":246348,"la":56.157,"lo":101.615,"k":0,"z":5,"s":3},{"n":"Irkutsk","a":"Russia","i":"RU","p":586695,"la":52.285,"lo":104.284,"k":0,"z":3.7,"s":3},{"n":"Krasnoyarsk","a":"Russia","i":"RU","p":925000,"la":56.016,"lo":92.864,"k":0,"z":5,"s":3},{"n":"Dikson","a":"Russia","i":"RU","p":1113,"la":73.507,"lo":80.545,"k":0,"z":5,"s":3},{"n":"Chita","a":"Russia","i":"RU","p":308500,"la":52.055,"lo":113.465,"k":0,"z":5,"s":3},{"n":"Vladivostok","a":"Russia","i":"RU","p":587022,"la":43.13,"lo":131.91,"k":0,"z":5,"s":3},{"n":"Nizhneyansk","a":"Russia","i":"RU","p":400,"la":71.433,"lo":136.067,"k":0,"z":5,"s":3},{"n":"Yakutsk","a":"Russia","i":"RU","p":235600,"la":62.035,"lo":129.735,"k":0,"z":4,"s":3},{"n":"Tiksi","a":"Russia","i":"RU","p":5700,"la":71.627,"lo":128.835,"k":0,"z":4,"s":3},{"n":"Magadan","a":"Russia","i":"RU","p":95282,"la":59.575,"lo":150.81,"k":0,"z":3.7,"s":3},{"n":"Tijuana","a":"Mexico","i":"MX","p":1553000,"la":32.502,"lo":-117.082,"k":0,"z":5,"s":3},{"n":"Chihuahua","a":"Mexico","i":"MX","p":793000,"la":28.647,"lo":-106.087,"k":0,"z":4,"s":3},{"n":"Mazatlán","a":"Mexico","i":"MX","p":368204,"la":23.221,"lo":-106.42,"k":0,"z":5,"s":3},{"n":"Tampico","a":"Mexico","i":"MX","p":859419,"la":22.3,"lo":-97.87,"k":0,"z":5,"s":3},{"n":"Acapulco","a":"Mexico","i":"MX","p":715584,"la":16.85,"lo":-99.916,"k":0,"z":5,"s":3},{"n":"Veracruz","a":"Mexico","i":"MX","p":578963,"la":19.177,"lo":-96.16,"k":0,"z":5,"s":3},{"n":"Tuxtla Gutiérrez","a":"Mexico","i":"MX","p":481128,"la":16.75,"lo":-93.15,"k":0,"z":5,"s":3},{"n":"Cancún","a":"Mexico","i":"MX","p":542043,"la":21.17,"lo":-86.83,"k":0,"z":4,"s":3},{"n":"Mérida","a":"Mexico","i":"MX","p":965000,"la":20.969,"lo":-89.619,"k":0,"z":5,"s":3},{"n":"Enugu","a":"Nigeria","i":"NG","p":688862,"la":6.45,"lo":7.5,"k":0,"z":5,"s":3},{"n":"Sokoto","a":"Nigeria","i":"NG","p":732178,"la":13.06,"lo":5.24,"k":0,"z":5,"s":3},{"n":"Perm","a":"Russia","i":"RU","p":997000,"la":58.002,"lo":56.248,"k":0,"z":4,"s":3},{"n":"Erdenet","a":"Mongolia","i":"MN","p":79647,"la":49.053,"lo":104.118,"k":0,"z":5,"s":3},{"n":"Ulaanbaatar","a":"Mongolia","i":"MN","p":885000,"la":47.919,"lo":106.915,"k":1,"z":4,"s":3},{"n":"Mbeya","a":"Tanzania","i":"TZ","p":291649,"la":-8.89,"lo":33.43,"k":0,"z":5,"s":3},{"n":"Windhoek","a":"Namibia","i":"NA","p":268132,"la":-22.57,"lo":17.084,"k":1,"z":5,"s":3},{"n":"Grootfontein","a":"Namibia","i":"NA","p":24099,"la":-19.567,"lo":18.117,"k":0,"z":5,"s":3},{"n":"Zanzibar","a":"Tanzania","i":"TZ","p":403658,"la":-6.16,"lo":39.2,"k":0,"z":5,"s":3},{"n":"Valencia","a":"Spain","i":"ES","p":808000,"la":39.487,"lo":-0.402,"k":0,"z":4.7,"s":3},{"n":"Palana","a":"Russia","i":"RU","p":3671,"la":59.084,"lo":159.95,"k":0,"z":5,"s":3},{"n":"Petropavlovsk-Kamchatsky","a":"Russia","i":"RU","p":187282,"la":53.062,"lo":158.623,"k":0,"z":5,"s":3},{"n":"Abuja","a":"Nigeria","i":"NG","p":1576000,"la":9.055,"lo":7.49,"k":1,"z":4,"s":3},{"n":"Padang","a":"Indonesia","i":"ID","p":855000,"la":-0.958,"lo":100.358,"k":0,"z":5,"s":3},{"n":"Bissau","a":"Guinea Bissau","i":"GW","p":403339,"la":11.865,"lo":-15.598,"k":1,"z":5,"s":3},{"n":"Palermo","a":"Italy","i":"IT","p":863000,"la":38.127,"lo":13.348,"k":0,"z":5,"s":3},{"n":"Amman","a":"Jordan","i":"JO","p":1060000,"la":31.952,"lo":35.931,"k":1,"z":5,"s":3},{"n":"Vilnius","a":"Lithuania","i":"LT","p":542366,"la":54.683,"lo":25.317,"k":1,"z":5,"s":3},{"n":"Riga","a":"Latvia","i":"LV","p":742572,"la":56.95,"lo":24.1,"k":1,"z":4,"s":3},{"n":"Bishkek","a":"Kyrgyzstan","i":"KG","p":837000,"la":42.875,"lo":74.583,"k":1,"z":5,"s":3},{"n":"Jiayuguan","a":"China","i":"CN","p":148279,"la":39.82,"lo":98.3,"k":0,"z":5,"s":3},{"n":"Xining","a":"China","i":"CN","p":1048000,"la":36.622,"lo":101.768,"k":0,"z":4.7,"s":3},{"n":"Guilin","a":"China","i":"CN","p":987000,"la":25.282,"lo":110.278,"k":0,"z":5,"s":3},{"n":"Huainan","a":"China","i":"CN","p":1451000,"la":32.632,"lo":116.978,"k":0,"z":5,"s":3},{"n":"Shantou","a":"China","i":"CN","p":1601000,"la":23.372,"lo":116.668,"k":0,"z":5,"s":3},{"n":"Tarakan","a":"Indonesia","i":"ID","p":193069,"la":3.3,"lo":117.633,"k":0,"z":5,"s":3},{"n":"Mombasa","a":"Kenya","i":"KE","p":882000,"la":-4.038,"lo":39.688,"k":0,"z":5,"s":3},{"n":"Maseru","a":"Lesotho","i":"LS","p":361324,"la":-29.317,"lo":27.483,"k":1,"z":5,"s":3},{"n":"Antananarivo","a":"Madagascar","i":"MG","p":1697000,"la":-18.915,"lo":47.515,"k":1,"z":5,"s":3},{"n":"Semarang","a":"Indonesia","i":"ID","p":1396000,"la":-6.965,"lo":110.418,"k":0,"z":5,"s":3},{"n":"Palembang","a":"Indonesia","i":"ID","p":1749000,"la":-2.978,"lo":104.748,"k":0,"z":5,"s":3},{"n":"Bandjarmasin","a":"Indonesia","i":"ID","p":603576,"la":-3.33,"lo":114.58,"k":0,"z":5,"s":3},{"n":"Makassar","a":"Indonesia","i":"ID","p":1262000,"la":-5.138,"lo":119.43,"k":0,"z":5,"s":3},{"n":"Lyon","a":"France","i":"FR","p":1423000,"la":45.772,"lo":4.828,"k":0,"z":4.7,"s":3},{"n":"Quito","a":"Ecuador","i":"EC","p":1701000,"la":-0.213,"lo":-78.502,"k":1,"z":4,"s":3},{"n":"San José","a":"Costa Rica","i":"CR","p":1284000,"la":9.93,"lo":-84.079,"k":1,"z":5,"s":3},{"n":"San Salvador","a":"El Salvador","i":"SV","p":1433000,"la":13.703,"lo":-89.216,"k":1,"z":5,"s":3},{"n":"Kingston","a":"Jamaica","i":"JM","p":937700,"la":17.977,"lo":-76.767,"k":1,"z":5,"s":3},{"n":"Cartagena","a":"Colombia","i":"CO","p":887000,"la":10.402,"lo":-75.516,"k":0,"z":5,"s":3},{"n":"Mitú","a":"Colombia","i":"CO","p":5917,"la":1.198,"lo":-70.174,"k":0,"z":5,"s":3},{"n":"Bumba","a":"Congo (Kinshasa)","i":"CD","p":160539,"la":2.19,"lo":22.46,"k":0,"z":5,"s":3},{"n":"N'Djamena","a":"Chad","i":"TD","p":989000,"la":12.115,"lo":15.047,"k":1,"z":5,"s":3},{"n":"Abéché","a":"Chad","i":"TD","p":158317,"la":13.84,"lo":20.83,"k":0,"z":5,"s":3},{"n":"Malabo","a":"Equatorial Guinea","i":"GQ","p":155963,"la":3.75,"lo":8.783,"k":1,"z":5,"s":3},{"n":"Luxor","a":"Egypt","i":"EG","p":609248,"la":25.7,"lo":32.65,"k":0,"z":5,"s":3},{"n":"Asmara","a":"Eritrea","i":"ER","p":620802,"la":15.333,"lo":38.933,"k":1,"z":5,"s":3},{"n":"Zagreb","a":"Croatia","i":"HR","p":722526,"la":45.8,"lo":16,"k":1,"z":5,"s":3},{"n":"Tallinn","a":"Estonia","i":"EE","p":394024,"la":59.434,"lo":24.728,"k":1,"z":5,"s":3},{"n":"Lhasa","a":"China","i":"CN","p":219599,"la":29.645,"lo":91.1,"k":0,"z":4,"s":3},{"n":"Hami","a":"China","i":"CN","p":580000,"la":42.827,"lo":93.515,"k":0,"z":5,"s":3},{"n":"Hotan","a":"China","i":"CN","p":408894,"la":37.1,"lo":79.927,"k":0,"z":4,"s":3},{"n":"Kashgar","a":"China","i":"CN","p":506640,"la":39.476,"lo":75.97,"k":0,"z":5,"s":3},{"n":"Yinchuan","a":"China","i":"CN","p":991000,"la":38.47,"lo":106.271,"k":0,"z":5,"s":3},{"n":"Pingxiang","a":"China","i":"CN","p":961000,"la":27.622,"lo":113.848,"k":0,"z":5,"s":3},{"n":"Nagasaki","a":"Japan","i":"JP","p":435455,"la":32.765,"lo":129.885,"k":0,"z":5,"s":3},{"n":"Qiqihar","a":"China","i":"CN","p":1641000,"la":47.347,"lo":123.988,"k":0,"z":4,"s":3},{"n":"Kikwit","a":"Congo (Kinshasa)","i":"CD","p":637736,"la":-5.03,"lo":18.85,"k":0,"z":5,"s":3},{"n":"Matadi","a":"Congo (Kinshasa)","i":"CD","p":245862,"la":-5.817,"lo":13.45,"k":0,"z":5,"s":3},{"n":"Kolwezi","a":"Congo (Kinshasa)","i":"CD","p":418000,"la":-10.717,"lo":25.472,"k":0,"z":5,"s":3},{"n":"Lubumbashi","a":"Congo (Kinshasa)","i":"CD","p":1352000,"la":-11.678,"lo":27.478,"k":0,"z":5,"s":3},{"n":"Lilongwe","a":"Malawi","i":"MW","p":646750,"la":-13.983,"lo":33.783,"k":1,"z":5,"s":3},{"n":"Guatemala City","a":"Guatemala","i":"GT","p":1024000,"la":14.623,"lo":-90.529,"k":1,"z":5,"s":3},{"n":"Cayenne","a":"France","i":"GF","p":61550,"la":4.933,"lo":-52.33,"k":0,"z":4,"s":3},{"n":"Libreville","a":"Gabon","i":"GA","p":578156,"la":0.385,"lo":9.458,"k":1,"z":5,"s":3},{"n":"Vishakhapatnam","a":"India","i":"IN","p":1529000,"la":17.683,"lo":83.22,"k":0,"z":5,"s":3},{"n":"Suva","a":"Fiji","i":"FJ","p":175399,"la":-18.133,"lo":178.442,"k":1,"z":5,"s":3},{"n":"Port-Gentil","a":"Gabon","i":"GA","p":116836,"la":-0.72,"lo":8.78,"k":0,"z":5,"s":3},{"n":"Timbuktu","a":"Mali","i":"ML","p":68872,"la":16.767,"lo":-3.017,"k":0,"z":5,"s":3},{"n":"Punta Arenas","a":"Chile","i":"CL","p":117430,"la":-53.165,"lo":-70.94,"k":0,"z":3,"s":3},{"n":"Iquique","a":"Chile","i":"CL","p":227499,"la":-20.25,"lo":-70.13,"k":0,"z":5,"s":3},{"n":"Antofagasta","a":"Chile","i":"CL","p":309832,"la":-23.65,"lo":-70.4,"k":0,"z":4,"s":3},{"n":"Valparaíso","a":"Chile","i":"CL","p":854000,"la":-33.048,"lo":-71.617,"k":0,"z":5,"s":3},{"n":"Valdivia","a":"Chile","i":"CL","p":159599,"la":-39.822,"lo":-73.233,"k":0,"z":4,"s":3},{"n":"Concepción","a":"Chile","i":"CL","p":889725,"la":-36.83,"lo":-73.05,"k":0,"z":5,"s":3},{"n":"Puerto Montt","a":"Chile","i":"CL","p":174629,"la":-41.47,"lo":-72.93,"k":0,"z":5,"s":3},{"n":"Nuuk","a":"Greenland","i":"GL","p":14798,"la":64.198,"lo":-51.733,"k":0,"z":4,"s":3},{"n":"Nouakchott","a":"Mauritania","i":"MR","p":742144,"la":18.086,"lo":-15.975,"k":1,"z":5,"s":3},{"n":"Bamako","a":"Mali","i":"ML","p":1494000,"la":12.652,"lo":-8.002,"k":1,"z":4,"s":3},{"n":"Atar","a":"Mauritania","i":"MR","p":44265,"la":20.517,"lo":-13.05,"k":0,"z":5,"s":3},{"n":"Djenné","a":"Mali","i":"ML","p":32944,"la":13.9,"lo":-4.55,"k":0,"z":5,"s":3},{"n":"Sabha","a":"Libya","i":"LY","p":100249,"la":27.033,"lo":14.433,"k":0,"z":5,"s":3},{"n":"Banghazi","a":"Libya","i":"LY","p":1180000,"la":32.119,"lo":20.065,"k":0,"z":4.7,"s":3},{"n":"Thessaloniki","a":"Greece","i":"GR","p":828000,"la":40.698,"lo":22.883,"k":0,"z":5,"s":3},{"n":"Beirut","a":"Lebanon","i":"LB","p":1846000,"la":33.874,"lo":35.508,"k":1,"z":5,"s":3},{"n":"Tbilisi","a":"Georgia","i":"GE","p":1100000,"la":41.727,"lo":44.789,"k":1,"z":5,"s":3},{"n":"Gondar","a":"Ethiopia","i":"ET","p":156230,"la":12.61,"lo":37.46,"k":0,"z":5,"s":3},{"n":"Nur-Sultan","a":"Kazakhstan","i":"KZ","p":345604,"la":51.181,"lo":71.428,"k":1,"z":4,"s":3},{"n":"Qaraghandy","a":"Kazakhstan","i":"KZ","p":451800,"la":49.885,"lo":73.115,"k":0,"z":5,"s":3},{"n":"Almaty","a":"Kazakhstan","i":"KZ","p":1209000,"la":43.327,"lo":76.913,"k":0,"z":5,"s":3},{"n":"Isfahan","a":"Iran","i":"IR","p":1628000,"la":32.702,"lo":51.698,"k":0,"z":5,"s":3},{"n":"Shiraz","a":"Iran","i":"IR","p":1240000,"la":29.632,"lo":52.568,"k":0,"z":5,"s":3},{"n":"Amritsar","a":"India","i":"IN","p":1212000,"la":31.642,"lo":74.868,"k":0,"z":5,"s":3},{"n":"Varanasi","a":"India","i":"IN","p":1352000,"la":25.332,"lo":82.998,"k":0,"z":5,"s":3},{"n":"Asansol","a":"India","i":"IN","p":1328000,"la":23.685,"lo":86.981,"k":0,"z":5,"s":3},{"n":"Bhilai","a":"India","i":"IN","p":1097000,"la":21.211,"lo":81.372,"k":0,"z":5,"s":3},{"n":"Bhopal","a":"India","i":"IN","p":1727000,"la":23.252,"lo":77.408,"k":0,"z":5,"s":3},{"n":"Madurai","a":"India","i":"IN","p":1294000,"la":9.922,"lo":78.118,"k":0,"z":5,"s":3},{"n":"Coimbatore","a":"India","i":"IN","p":1696000,"la":11.002,"lo":76.948,"k":0,"z":5,"s":3},{"n":"Vientiane","a":"Laos","i":"LA","p":754000,"la":17.967,"lo":102.6,"k":1,"z":5,"s":3},{"n":"Brazzaville","a":"Congo (Brazzaville)","i":"CG","p":1355000,"la":-4.257,"lo":15.283,"k":1,"z":5,"s":3},{"n":"Conakry","a":"Guinea","i":"GN","p":1494000,"la":9.533,"lo":-13.682,"k":1,"z":5,"s":3},{"n":"Yamoussoukro","a":"Ivory Coast","i":"CI","p":206499,"la":6.818,"lo":-5.276,"k":1,"z":5,"s":3},{"n":"Cruzeiro do Sul","a":"Brazil","i":"BR","p":56862,"la":-7.63,"lo":-72.67,"k":0,"z":5,"s":3},{"n":"Leticia","a":"Colombia","i":"CO","p":59575,"la":-4.201,"lo":-69.955,"k":0,"z":5,"s":3},{"n":"Manaus","a":"Brazil","i":"BR","p":1753000,"la":-3.098,"lo":-60.002,"k":0,"z":4,"s":3},{"n":"Caxias","a":"Brazil","i":"BR","p":134640,"la":-4.833,"lo":-43.35,"k":0,"z":5,"s":3},{"n":"Santarém","a":"Brazil","i":"BR","p":230428,"la":-2.433,"lo":-54.7,"k":0,"z":5,"s":3},{"n":"Marabá","a":"Brazil","i":"BR","p":166182,"la":-5.35,"lo":-49.116,"k":0,"z":5,"s":3},{"n":"Vilhena","a":"Brazil","i":"BR","p":63231,"la":-12.717,"lo":-60.117,"k":0,"z":5,"s":3},{"n":"Ji-Paraná","a":"Brazil","i":"BR","p":65016,"la":-10.833,"lo":-61.967,"k":0,"z":5,"s":3},{"n":"Campo Grande","a":"Brazil","i":"BR","p":778000,"la":-20.448,"lo":-54.619,"k":0,"z":5,"s":3},{"n":"Florianópolis","a":"Brazil","i":"BR","p":1023000,"la":-27.578,"lo":-48.522,"k":0,"z":5,"s":3},{"n":"Feira de Santana","a":"Brazil","i":"BR","p":481911,"la":-12.25,"lo":-38.97,"k":0,"z":5,"s":3},{"n":"Winnipeg","a":"Canada","i":"CA","p":632063,"la":49.883,"lo":-97.166,"k":0,"z":4,"s":3},{"n":"Churchill","a":"Canada","i":"CA","p":1000,"la":58.766,"lo":-94.166,"k":0,"z":4,"s":3},{"n":"Regina","a":"Canada","i":"CA","p":176183,"la":50.45,"lo":-104.617,"k":0,"z":5,"s":3},{"n":"Saskatoon","a":"Canada","i":"CA","p":198958,"la":52.17,"lo":-106.67,"k":0,"z":5,"s":3},{"n":"Calgary","a":"Canada","i":"CA","p":1110000,"la":51.085,"lo":-114.082,"k":0,"z":4,"s":3},{"n":"Prince Rupert","a":"Canada","i":"CA","p":14708,"la":54.317,"lo":-130.33,"k":0,"z":4,"s":3},{"n":"Victoria","a":"Canada","i":"CA","p":289625,"la":48.433,"lo":-123.35,"k":0,"z":5,"s":3},{"n":"Arctic Bay","a":"Canada","i":"CA","p":604,"la":73.033,"lo":-85.167,"k":0,"z":5,"s":3},{"n":"Resolute","a":"Canada","i":"CA","p":250,"la":74.683,"lo":-94.9,"k":0,"z":5,"s":3},{"n":"Naujaat","a":"Canada","i":"CA","p":1000,"la":66.529,"lo":-86.283,"k":0,"z":4,"s":3},{"n":"Yellowknife","a":"Canada","i":"CA","p":19234,"la":62.442,"lo":-114.397,"k":0,"z":4,"s":3},{"n":"Fort Good Hope","a":"Canada","i":"CA","p":597,"la":66.267,"lo":-128.633,"k":0,"z":5,"s":3},{"n":"Whitehorse","a":"Canada","i":"CA","p":23276,"la":60.717,"lo":-135.05,"k":0,"z":4,"s":3},{"n":"Boa Vista","a":"Brazil","i":"BR","p":235150,"la":2.816,"lo":-60.666,"k":0,"z":5,"s":3},{"n":"Macapá","a":"Brazil","i":"BR","p":499166,"la":0.033,"lo":-51.05,"k":0,"z":5,"s":3},{"n":"Ottawa","a":"Canada","i":"CA","p":1145000,"la":45.419,"lo":-75.702,"k":1,"z":3,"s":3},{"n":"Fort Severn","a":"Canada","i":"CA","p":125,"la":55.983,"lo":-87.65,"k":0,"z":5,"s":3},{"n":"Thunder Bay","a":"Canada","i":"CA","p":99334,"la":48.446,"lo":-89.275,"k":0,"z":5,"s":3},{"n":"Québec","a":"Canada","i":"CA","p":624177,"la":46.84,"lo":-71.246,"k":0,"z":4,"s":3},{"n":"Halifax","a":"Canada","i":"CA","p":359111,"la":44.65,"lo":-63.6,"k":0,"z":4,"s":3},{"n":"St. John's","a":"Canada","i":"CA","p":131469,"la":47.585,"lo":-52.681,"k":0,"z":4,"s":3},{"n":"Nain","a":"Canada","i":"CA","p":1151,"la":56.547,"lo":-61.686,"k":0,"z":5,"s":3},{"n":"Charlottetown","a":"Canada","i":"CA","p":42402,"la":46.249,"lo":-63.131,"k":0,"z":5,"s":3},{"n":"N'Délé","a":"Central African Republic","i":"CF","p":11764,"la":8.409,"lo":20.653,"k":0,"z":5,"s":3},{"n":"Belgrade","a":"Serbia","i":"RS","p":1099000,"la":44.821,"lo":20.466,"k":1,"z":5,"s":3},{"n":"Obo","a":"Central African Republic","i":"CF","p":12887,"la":5.4,"lo":26.5,"k":0,"z":5,"s":3},{"n":"Bandar Seri Begawan","a":"Brunei","i":"BN","p":296500,"la":4.883,"lo":114.933,"k":1,"z":5,"s":3},{"n":"Puerto Deseado","a":"Argentina","i":"AR","p":3305,"la":-47.75,"lo":-65.9,"k":0,"z":5,"s":3},{"n":"Río Gallegos","a":"Argentina","i":"AR","p":85700,"la":-51.633,"lo":-69.217,"k":0,"z":5,"s":3},{"n":"Comodoro Rivadavia","a":"Argentina","i":"AR","p":140850,"la":-45.87,"lo":-67.5,"k":0,"z":5,"s":3},{"n":"Mendoza","a":"Argentina","i":"AR","p":893000,"la":-32.881,"lo":-68.819,"k":0,"z":5,"s":3},{"n":"Sucre","a":"Bolivia","i":"BO","p":224838,"la":-19.041,"lo":-65.26,"k":1,"z":4.7,"s":3},{"n":"Riberalta","a":"Bolivia","i":"BO","p":74014,"la":-10.983,"lo":-66.1,"k":0,"z":5,"s":3},{"n":"Bahía Blanca","a":"Argentina","i":"AR","p":281536,"la":-38.74,"lo":-62.265,"k":0,"z":5,"s":3},{"n":"Mar del Plata","a":"Argentina","i":"AR","p":555897,"la":-38,"lo":-57.58,"k":0,"z":5,"s":3},{"n":"Córdoba","a":"Argentina","i":"AR","p":1452000,"la":-31.398,"lo":-64.184,"k":0,"z":4,"s":3},{"n":"Posadas","a":"Argentina","i":"AR","p":357119,"la":-27.358,"lo":-55.885,"k":0,"z":5,"s":3},{"n":"Belmopan","a":"Belize","i":"BZ","p":15220,"la":17.252,"lo":-88.767,"k":1,"z":5,"s":3},{"n":"Bangui","a":"Central African Republic","i":"CF","p":831925,"la":4.367,"lo":18.558,"k":1,"z":5,"s":3},{"n":"Maroua","a":"Cameroon","i":"CM","p":319941,"la":10.596,"lo":14.325,"k":0,"z":5,"s":3},{"n":"Yaoundé","a":"Cameroon","i":"CM","p":1611000,"la":3.869,"lo":11.515,"k":1,"z":5,"s":3},{"n":"Tirana","a":"Albania","i":"AL","p":895350,"la":41.328,"lo":19.819,"k":1,"z":5,"s":3},{"n":"Yerevan","a":"Armenia","i":"AM","p":1102000,"la":40.183,"lo":44.512,"k":1,"z":5,"s":3},{"n":"Baku","a":"Azerbaijan","i":"AZ","p":2122300,"la":40.397,"lo":49.86,"k":1,"z":5,"s":3},{"n":"Kandahar","a":"Afghanistan","i":"AF","p":715542,"la":31.61,"lo":65.695,"k":0,"z":5,"s":3},{"n":"Phnom Penh","a":"Cambodia","i":"KH","p":1466000,"la":11.552,"lo":104.915,"k":1,"z":5,"s":3},{"n":"Menongue","a":"Angola","i":"AO","p":13030,"la":-14.667,"lo":17.7,"k":0,"z":5,"s":3},{"n":"Huambo","a":"Angola","i":"AO","p":1100000,"la":-12.748,"lo":15.758,"k":0,"z":5,"s":3},{"n":"La Paz","a":"Bolivia","i":"BO","p":1590000,"la":-16.496,"lo":-68.152,"k":1,"z":4,"s":3},{"n":"Santa Cruz","a":"Bolivia","i":"BO","p":2102998,"la":-17.752,"lo":-63.228,"k":0,"z":5,"s":3},{"n":"Oran","a":"Algeria","i":"DZ","p":798000,"la":35.712,"lo":-0.622,"k":0,"z":5,"s":3},{"n":"Cotonou","a":"Benin","i":"BJ","p":762000,"la":6.363,"lo":2.404,"k":1,"z":5,"s":3},{"n":"Tamanrasset","a":"Algeria","i":"DZ","p":76000,"la":22.785,"lo":5.523,"k":0,"z":5,"s":3},{"n":"Ghardaia","a":"Algeria","i":"DZ","p":125480,"la":32.49,"lo":3.67,"k":0,"z":5,"s":3},{"n":"Sofia","a":"Bulgaria","i":"BG","p":1185000,"la":42.685,"lo":23.315,"k":1,"z":5,"s":3},{"n":"Minsk","a":"Belarus","i":"BY","p":1805000,"la":53.902,"lo":27.565,"k":1,"z":5,"s":3},{"n":"Thimphu","a":"Bhutan","i":"BT","p":98676,"la":27.473,"lo":89.639,"k":1,"z":5,"s":3},{"n":"Gaborone","a":"Botswana","i":"BW","p":208411,"la":-24.646,"lo":25.912,"k":1,"z":5,"s":3},{"n":"Darwin","a":"Australia","i":"AU","p":93080,"la":-12.425,"lo":130.85,"k":0,"z":3.7,"s":3},{"n":"Alice Springs","a":"Australia","i":"AU","p":27710,"la":-23.701,"lo":133.88,"k":0,"z":5,"s":3},{"n":"Canberra","a":"Australia","i":"AU","p":327700,"la":-35.283,"lo":149.129,"k":1,"z":4,"s":3},{"n":"Newcastle","a":"Australia","i":"AU","p":540796,"la":-32.93,"lo":151.77,"k":0,"z":5,"s":3},{"n":"Adelaide","a":"Australia","i":"AU","p":1145000,"la":-34.933,"lo":138.598,"k":0,"z":3.7,"s":3},{"n":"Townsville","a":"Australia","i":"AU","p":138954,"la":-19.25,"lo":146.77,"k":0,"z":5,"s":3},{"n":"Brisbane","a":"Australia","i":"AU","p":1860000,"la":-27.453,"lo":153.033,"k":0,"z":4,"s":3},{"n":"Hobart","a":"Australia","i":"AU","p":80870,"la":-42.85,"lo":147.295,"k":0,"z":5,"s":3},{"n":"Ouagadougou","a":"Burkina Faso","i":"BF","p":1149000,"la":12.372,"lo":-1.527,"k":1,"z":5,"s":3},{"n":"Sarajevo","a":"Bosnia and Herzegovina","i":"BA","p":696731,"la":43.85,"lo":18.383,"k":1,"z":5,"s":3},{"n":"Naypyidaw","a":"Myanmar","i":"MM","p":930000,"la":19.769,"lo":96.117,"k":1,"z":5,"s":3},{"n":"San Juan","a":"Puerto Rico","i":"PR","p":2478905,"la":18.44,"lo":-66.13,"k":0,"z":5,"s":3},{"n":"Stanley","a":"Falkland Islands","i":"FK","p":2213,"la":-51.7,"lo":-57.85,"k":0,"z":5,"s":3},{"n":"Hamilton","a":"Bermuda","i":"BM","p":52320,"la":32.294,"lo":-64.784,"k":1,"z":5,"s":3},{"n":"Nuku'alofa","a":"Tonga","i":"TO","p":42620,"la":-21.139,"lo":-175.221,"k":1,"z":5,"s":3},{"n":"Hargeisa","a":"Somaliland","i":"-99","p":477876,"la":9.56,"lo":44.065,"k":1,"z":5,"s":3},{"n":"Victoria","a":"Seychelles","i":"SC","p":33576,"la":-4.617,"lo":55.45,"k":1,"z":5,"s":3},{"n":"São Tomé","a":"Sao Tome and Principe","i":"ST","p":88219,"la":0.337,"lo":6.73,"k":1,"z":5,"s":3},{"n":"Apia","a":"Samoa","i":"WS","p":61916,"la":-13.836,"lo":-171.769,"k":1,"z":5,"s":3},{"n":"Valletta","a":"Malta","i":"MT","p":368250,"la":35.9,"lo":14.515,"k":1,"z":5,"s":3},{"n":"Malé","a":"Maldives","i":"MV","p":112927,"la":4.172,"lo":73.509,"k":1,"z":5,"s":3},{"n":"Jerusalem","a":"Israel","i":"IL","p":1029300,"la":31.778,"lo":35.207,"k":1,"z":3.7,"s":3},{"n":"Praia","a":"Cape Verde","i":"CV","p":113364,"la":14.917,"lo":-23.517,"k":1,"z":5,"s":3},{"n":"Nassau","a":"The Bahamas","i":"BS","p":227940,"la":25.083,"lo":-77.35,"k":1,"z":5,"s":3},{"n":"Nicosia","a":"Cyprus","i":"CY","p":224300,"la":35.167,"lo":33.367,"k":1,"z":5,"s":3},{"n":"Kaohsiung","a":"Taiwan","i":"TW","p":2769072,"la":22.633,"lo":120.267,"k":0,"z":5,"s":3},{"n":"Wellington","a":"New Zealand","i":"NZ","p":393400,"la":-41.292,"lo":174.777,"k":1,"z":4,"s":3},{"n":"Christchurch","a":"New Zealand","i":"NZ","p":363200,"la":-43.535,"lo":172.63,"k":0,"z":4,"s":3},{"n":"Shenzhen","a":"China","i":"CN","p":7581000,"la":22.548,"lo":114.061,"k":0,"z":4.7,"s":2},{"n":"Zibo","a":"China","i":"CN","p":3061000,"la":36.821,"lo":117.996,"k":0,"z":4.7,"s":2},{"n":"Minneapolis","a":"United States of America","i":"US","p":2616000,"la":44.982,"lo":-93.254,"k":0,"z":4,"s":2},{"n":"Honolulu","a":"United States of America","i":"US","p":786000,"la":21.303,"lo":-157.858,"k":0,"z":4,"s":2},{"n":"Seattle","a":"United States of America","i":"US","p":3074000,"la":47.6,"lo":-122.315,"k":0,"z":4,"s":2},{"n":"Phoenix","a":"United States of America","i":"US","p":3551000,"la":33.448,"lo":-112.067,"k":0,"z":4,"s":2},{"n":"San Diego","a":"United States of America","i":"US","p":2916000,"la":32.719,"lo":-117.151,"k":0,"z":4,"s":2},{"n":"St. Louis","a":"United States of America","i":"US","p":2199000,"la":38.637,"lo":-90.242,"k":0,"z":4,"s":2},{"n":"New Orleans","a":"United States of America","i":"US","p":785000,"la":29.969,"lo":-90.088,"k":0,"z":4,"s":2},{"n":"Dallas","a":"United States of America","i":"US","p":4798000,"la":32.772,"lo":-96.795,"k":0,"z":4,"s":2},{"n":"Maracaibo","a":"Venezuela","i":"VE","p":2072000,"la":10.732,"lo":-71.662,"k":0,"z":4,"s":2},{"n":"Boston","a":"United States of America","i":"US","p":4467000,"la":42.332,"lo":-71.072,"k":0,"z":4,"s":2},{"n":"Tampa","a":"United States of America","i":"US","p":2314000,"la":27.949,"lo":-82.461,"k":0,"z":5,"s":2},{"n":"Philadelphia","a":"United States of America","i":"US","p":5492000,"la":39.946,"lo":-75.18,"k":0,"z":5,"s":2},{"n":"Detroit","a":"United States of America","i":"US","p":4101000,"la":42.334,"lo":-83.051,"k":0,"z":4,"s":2},{"n":"Anchorage","a":"United States of America","i":"US","p":260283,"la":61.214,"lo":-149.887,"k":0,"z":3,"s":2},{"n":"Hanoi","a":"Vietnam","i":"VN","p":4378000,"la":21.035,"lo":105.848,"k":1,"z":4,"s":2},{"n":"Ho Chi Minh City","a":"Vietnam","i":"VN","p":5314000,"la":10.762,"lo":106.703,"k":0,"z":4,"s":2},{"n":"Ankara","a":"Turkey","i":"TR","p":3716000,"la":39.929,"lo":32.862,"k":1,"z":4,"s":2},{"n":"Budapest","a":"Hungary","i":"HU","p":1679000,"la":47.502,"lo":19.081,"k":1,"z":4.7,"s":2},{"n":"Sanaa","a":"Yemen","i":"YE","p":2008000,"la":15.357,"lo":44.205,"k":1,"z":4,"s":2},{"n":"Barcelona","a":"Spain","i":"ES","p":4920000,"la":41.385,"lo":2.181,"k":0,"z":4,"s":2},{"n":"Bucharest","a":"Romania","i":"RO","p":1942000,"la":44.435,"lo":26.098,"k":1,"z":4,"s":2},{"n":"Aleppo","a":"Syria","i":"SY","p":2738000,"la":36.232,"lo":37.168,"k":0,"z":4.7,"s":2},{"n":"Damascus","a":"Syria","i":"SY","p":2466000,"la":33.502,"lo":36.298,"k":1,"z":4.7,"s":2},{"n":"Zürich","a":"Switzerland","i":"CH","p":1108000,"la":47.382,"lo":8.548,"k":0,"z":4.7,"s":2},{"n":"Lisbon","a":"Portugal","i":"PT","p":2812000,"la":38.725,"lo":-9.147,"k":1,"z":4,"s":2},{"n":"Khartoum","a":"Sudan","i":"SD","p":4754000,"la":15.59,"lo":32.532,"k":1,"z":4,"s":2},{"n":"Jeddah","a":"Saudi Arabia","i":"SA","p":3012000,"la":21.519,"lo":39.217,"k":0,"z":4,"s":2},{"n":"Makkah","a":"Saudi Arabia","i":"SA","p":1385000,"la":21.432,"lo":39.818,"k":0,"z":3.7,"s":2},{"n":"Oslo","a":"Norway","i":"NO","p":835000,"la":59.919,"lo":10.748,"k":1,"z":4,"s":2},{"n":"Lahore","a":"Pakistan","i":"PK","p":6577000,"la":31.562,"lo":74.348,"k":0,"z":4,"s":2},{"n":"Karachi","a":"Pakistan","i":"PK","p":12130000,"la":24.872,"lo":66.988,"k":0,"z":4,"s":2},{"n":"Durban","a":"South Africa","i":"ZA","p":2729000,"la":-29.861,"lo":31.011,"k":0,"z":4,"s":2},{"n":"St. Petersburg","a":"Russia","i":"RU","p":4553000,"la":59.941,"lo":30.314,"k":0,"z":3.7,"s":2},{"n":"Guadalajara","a":"Mexico","i":"MX","p":4198000,"la":20.672,"lo":-103.332,"k":0,"z":4,"s":2},{"n":"Puebla","a":"Mexico","i":"MX","p":2195000,"la":19.033,"lo":-98.204,"k":0,"z":4,"s":2},{"n":"Kano","a":"Nigeria","i":"NG","p":3140000,"la":12.002,"lo":8.518,"k":0,"z":4.7,"s":2},{"n":"Warsaw","a":"Poland","i":"PL","p":1707000,"la":52.231,"lo":21.005,"k":1,"z":4,"s":2},{"n":"Pyongyang","a":"North Korea","i":"KP","p":3300000,"la":39.021,"lo":125.753,"k":1,"z":4.7,"s":2},{"n":"Dar es Salaam","a":"Tanzania","i":"TZ","p":2930000,"la":-6.798,"lo":39.266,"k":1,"z":4,"s":2},{"n":"Medan","a":"Indonesia","i":"ID","p":2115000,"la":3.582,"lo":98.648,"k":0,"z":4,"s":2},{"n":"Dublin","a":"Ireland","i":"IE","p":1059000,"la":53.347,"lo":-6.257,"k":1,"z":4,"s":2},{"n":"Monrovia","a":"Liberia","i":"LR","p":1041000,"la":6.315,"lo":-10.8,"k":1,"z":4,"s":2},{"n":"Naples","a":"Italy","i":"IT","p":2250000,"la":40.842,"lo":14.243,"k":0,"z":4.7,"s":2},{"n":"Milan","a":"Italy","i":"IT","p":2945000,"la":45.472,"lo":9.203,"k":0,"z":4.7,"s":2},{"n":"Kuala Lumpur","a":"Malaysia","i":"MY","p":1448000,"la":3.14,"lo":101.689,"k":1,"z":4,"s":2},{"n":"Lanzhou","a":"China","i":"CN","p":2561000,"la":36.058,"lo":103.79,"k":0,"z":4,"s":2},{"n":"Nanning","a":"China","i":"CN","p":2167000,"la":22.822,"lo":108.318,"k":0,"z":4,"s":2},{"n":"Guiyang","a":"China","i":"CN","p":3662000,"la":26.582,"lo":106.718,"k":0,"z":4.7,"s":2},{"n":"Chongqing","a":"China","i":"CN","p":6461000,"la":29.567,"lo":106.593,"k":0,"z":4,"s":2},{"n":"Fuzhou","a":"China","i":"CN","p":2606000,"la":26.082,"lo":119.298,"k":0,"z":4.7,"s":2},{"n":"Guangzhou","a":"China","i":"CN","p":8829000,"la":23.125,"lo":113.262,"k":0,"z":4,"s":2},{"n":"Dongguan","a":"China","i":"CN","p":4528000,"la":23.022,"lo":113.75,"k":0,"z":5.6,"s":2},{"n":"Bandung","a":"Indonesia","i":"ID","p":2394000,"la":-6.948,"lo":107.568,"k":0,"z":4.7,"s":2},{"n":"Surabaya","a":"Indonesia","i":"ID","p":2845000,"la":-7.247,"lo":112.749,"k":0,"z":4,"s":2},{"n":"Guayaquil","a":"Ecuador","i":"EC","p":2514000,"la":-2.218,"lo":-79.922,"k":0,"z":4.7,"s":2},{"n":"Medellín","a":"Colombia","i":"CO","p":3297000,"la":6.277,"lo":-75.577,"k":0,"z":4,"s":2},{"n":"Cali","a":"Colombia","i":"CO","p":2254000,"la":3.402,"lo":-76.502,"k":0,"z":4,"s":2},{"n":"Havana","a":"Cuba","i":"CU","p":2174000,"la":23.134,"lo":-82.366,"k":1,"z":4,"s":2},{"n":"Alexandria","a":"Egypt","i":"EG","p":4165000,"la":31.202,"lo":29.948,"k":0,"z":4,"s":2},{"n":"Frankfurt","a":"Germany","i":"DE","p":2895000,"la":50.1,"lo":8.675,"k":0,"z":4,"s":2},{"n":"Hamburg","a":"Germany","i":"DE","p":1757000,"la":53.552,"lo":9.998,"k":0,"z":5,"s":2},{"n":"Munich","a":"Germany","i":"DE","p":1275000,"la":48.132,"lo":11.573,"k":0,"z":4.7,"s":2},{"n":"Prague","a":"Czechia","i":"CZ","p":1162000,"la":50.087,"lo":14.423,"k":1,"z":4.7,"s":2},{"n":"Kuwait City","a":"Kuwait","i":"KW","p":2063000,"la":29.372,"lo":47.976,"k":1,"z":4,"s":2},{"n":"Xian","a":"China","i":"CN","p":4009000,"la":34.277,"lo":108.893,"k":0,"z":3.7,"s":2},{"n":"Taiyuan","a":"China","i":"CN","p":2913000,"la":37.877,"lo":112.543,"k":0,"z":4,"s":2},{"n":"Wuhan","a":"China","i":"CN","p":7243000,"la":30.582,"lo":114.268,"k":0,"z":4,"s":2},{"n":"Changsha","a":"China","i":"CN","p":2604000,"la":28.202,"lo":112.968,"k":0,"z":4.7,"s":2},{"n":"Kunming","a":"China","i":"CN","p":2931000,"la":25.043,"lo":102.697,"k":0,"z":4,"s":2},{"n":"Zhengzhou","a":"China","i":"CN","p":2636000,"la":34.757,"lo":113.663,"k":0,"z":4,"s":2},{"n":"Shenyeng","a":"China","i":"CN","p":4787000,"la":41.807,"lo":123.448,"k":0,"z":4,"s":2},{"n":"Jinan","a":"China","i":"CN","p":2798000,"la":36.677,"lo":116.993,"k":0,"z":4,"s":2},{"n":"Tianjin","a":"China","i":"CN","p":7180000,"la":39.083,"lo":117.197,"k":0,"z":4,"s":2},{"n":"Nanchang","a":"China","i":"CN","p":2350000,"la":28.682,"lo":115.878,"k":0,"z":4.7,"s":2},{"n":"Nanjing","a":"China","i":"CN","p":3679000,"la":32.052,"lo":118.778,"k":0,"z":4,"s":2},{"n":"Hangzhou","a":"China","i":"CN","p":3007000,"la":30.252,"lo":120.168,"k":0,"z":4,"s":2},{"n":"Hiroshima","a":"Japan","i":"JP","p":2045000,"la":34.39,"lo":132.441,"k":0,"z":4,"s":2},{"n":"Changchun","a":"China","i":"CN","p":3183000,"la":43.867,"lo":125.338,"k":0,"z":4,"s":2},{"n":"Baotou","a":"China","i":"CN","p":2036000,"la":40.654,"lo":109.82,"k":0,"z":4,"s":2},{"n":"Harbin","a":"China","i":"CN","p":3621000,"la":45.752,"lo":126.648,"k":0,"z":3.7,"s":2},{"n":"Sapporo","a":"Japan","i":"JP","p":2544000,"la":43.077,"lo":141.338,"k":0,"z":4,"s":2},{"n":"Santo Domingo","a":"Dominican Republic","i":"DO","p":2154000,"la":18.471,"lo":-69.93,"k":1,"z":4,"s":2},{"n":"Accra","a":"Ghana","i":"GH","p":2121000,"la":5.552,"lo":-0.219,"k":1,"z":4,"s":2},{"n":"Delhi","a":"India","i":"IN","p":15926000,"la":28.672,"lo":77.228,"k":0,"z":4,"s":2},{"n":"Hyderabad","a":"India","i":"IN","p":6376000,"la":17.402,"lo":78.478,"k":0,"z":4,"s":2},{"n":"Pune","a":"India","i":"IN","p":4672000,"la":18.516,"lo":73.852,"k":0,"z":4,"s":2},{"n":"Nagpur","a":"India","i":"IN","p":2454000,"la":21.172,"lo":79.088,"k":0,"z":4,"s":2},{"n":"Tripoli","a":"Libya","i":"LY","p":2189000,"la":32.893,"lo":13.18,"k":1,"z":4,"s":2},{"n":"Tel Aviv","a":"Israel","i":"IL","p":3112000,"la":32.082,"lo":34.768,"k":0,"z":4,"s":2},{"n":"Helsinki","a":"Finland","i":"FI","p":1115000,"la":60.164,"lo":24.932,"k":1,"z":4,"s":2},{"n":"Mashhad","a":"Iran","i":"IR","p":2469000,"la":36.272,"lo":59.568,"k":0,"z":4,"s":2},{"n":"Jaipur","a":"India","i":"IN","p":2917000,"la":26.923,"lo":75.808,"k":0,"z":4,"s":2},{"n":"Kanpur","a":"India","i":"IN","p":3162000,"la":26.462,"lo":80.318,"k":0,"z":4,"s":2},{"n":"Patna","a":"India","i":"IN","p":2158000,"la":25.627,"lo":85.128,"k":0,"z":4.7,"s":2},{"n":"Chennai","a":"India","i":"IN","p":7163000,"la":13.092,"lo":80.278,"k":0,"z":4,"s":2},{"n":"Ahmedabad","a":"India","i":"IN","p":5375000,"la":23.032,"lo":72.578,"k":0,"z":4.7,"s":2},{"n":"Surat","a":"India","i":"IN","p":3842000,"la":21.202,"lo":72.838,"k":0,"z":4,"s":2},{"n":"København","a":"Denmark","i":"DK","p":1085000,"la":55.681,"lo":12.562,"k":1,"z":4,"s":2},{"n":"Abidjan","a":"Ivory Coast","i":"CI","p":3802000,"la":5.323,"lo":-4.02,"k":1,"z":4,"s":2},{"n":"Belém","a":"Brazil","i":"BR","p":2167000,"la":-1.448,"lo":-48.482,"k":0,"z":4,"s":2},{"n":"Brasília","a":"Brazil","i":"BR","p":3716996,"la":-15.781,"lo":-47.918,"k":1,"z":3.7,"s":2},{"n":"Porto Alegre","a":"Brazil","i":"BR","p":3917000,"la":-30.048,"lo":-51.202,"k":0,"z":4,"s":2},{"n":"Curitiba","a":"Brazil","i":"BR","p":3084000,"la":-25.418,"lo":-49.322,"k":0,"z":4,"s":2},{"n":"Fortaleza","a":"Brazil","i":"BR","p":3602319,"la":-3.748,"lo":-38.582,"k":0,"z":4,"s":2},{"n":"Salvador","a":"Brazil","i":"BR","p":3484000,"la":-12.968,"lo":-38.482,"k":0,"z":3.7,"s":2},{"n":"Edmonton","a":"Canada","i":"CA","p":1058000,"la":53.552,"lo":-113.502,"k":0,"z":4,"s":2},{"n":"Montréal","a":"Canada","i":"CA","p":3678000,"la":45.502,"lo":-73.585,"k":0,"z":4,"s":2},{"n":"Goiânia","a":"Brazil","i":"BR","p":2022000,"la":-16.718,"lo":-49.302,"k":0,"z":4.7,"s":2},{"n":"Recife","a":"Brazil","i":"BR","p":3651000,"la":-8.061,"lo":-34.909,"k":0,"z":3.7,"s":2},{"n":"Brussels","a":"Belgium","i":"BE","p":1743000,"la":50.835,"lo":4.331,"k":1,"z":4.7,"s":2},{"n":"Dhaka","a":"Bangladesh","i":"BD","p":12797394,"la":23.725,"lo":90.407,"k":1,"z":4,"s":2},{"n":"Luanda","a":"Angola","i":"AO","p":5172900,"la":-8.836,"lo":13.232,"k":1,"z":4,"s":2},{"n":"Algiers","a":"Algeria","i":"DZ","p":3354000,"la":36.765,"lo":3.049,"k":1,"z":4,"s":2},{"n":"Chattogram","a":"Bangladesh","i":"BD","p":4529000,"la":22.332,"lo":91.798,"k":0,"z":4,"s":2},{"n":"Perth","a":"Australia","i":"AU","p":1532000,"la":-31.953,"lo":115.838,"k":0,"z":3.7,"s":2},{"n":"Yangon","a":"Myanmar","i":"MM","p":4088000,"la":16.785,"lo":96.165,"k":0,"z":4,"s":2},{"n":"San Francisco","a":"United States of America","i":"US","p":3450000,"la":37.784,"lo":-122.4,"k":0,"z":2.7,"s":1},{"n":"Denver","a":"United States of America","i":"US","p":2313000,"la":39.741,"lo":-104.986,"k":0,"z":3.7,"s":1},{"n":"Houston","a":"United States of America","i":"US","p":4459000,"la":29.741,"lo":-95.348,"k":0,"z":3,"s":1},{"n":"Miami","a":"United States of America","i":"US","p":5585000,"la":25.79,"lo":-80.226,"k":0,"z":2.1,"s":1},{"n":"Atlanta","a":"United States of America","i":"US","p":4506000,"la":33.739,"lo":-84.368,"k":0,"z":3,"s":1},{"n":"Chicago","a":"United States of America","i":"US","p":8990000,"la":41.848,"lo":-87.635,"k":0,"z":3,"s":1},{"n":"Caracas","a":"Venezuela","i":"VE","p":2985000,"la":10.503,"lo":-66.919,"k":1,"z":3,"s":1},{"n":"Kyiv","a":"Ukraine","i":"UA","p":2709000,"la":50.435,"lo":30.515,"k":1,"z":3.7,"s":1},{"n":"Dubai","a":"United Arab Emirates","i":"AE","p":1379000,"la":25.215,"lo":55.287,"k":0,"z":3.7,"s":1},{"n":"Tashkent","a":"Uzbekistan","i":"UZ","p":2184000,"la":41.304,"lo":69.269,"k":1,"z":3.7,"s":1},{"n":"Madrid","a":"Spain","i":"ES","p":5567000,"la":40.402,"lo":-3.685,"k":1,"z":3,"s":1},{"n":"Geneva","a":"Switzerland","i":"CH","p":1240000,"la":46.21,"lo":6.14,"k":0,"z":3.7,"s":1},{"n":"Stockholm","a":"Sweden","i":"SE","p":1264000,"la":59.324,"lo":18.066,"k":1,"z":3,"s":1},{"n":"Bangkok","a":"Thailand","i":"TH","p":6704000,"la":13.752,"lo":100.515,"k":1,"z":3,"s":1},{"n":"Lima","a":"Peru","i":"PE","p":8012000,"la":-12.046,"lo":-77.052,"k":1,"z":3,"s":1},{"n":"Dakar","a":"Senegal","i":"SN","p":2604000,"la":14.718,"lo":-17.475,"k":1,"z":3,"s":1},{"n":"Johannesburg","a":"South Africa","i":"ZA","p":3435000,"la":-26.168,"lo":28.028,"k":0,"z":3,"s":1},{"n":"Amsterdam","a":"Netherlands","i":"NL","p":1031000,"la":52.352,"lo":4.915,"k":1,"z":3.7,"s":1},{"n":"Casablanca","a":"Morocco","i":"MA","p":3181000,"la":33.602,"lo":-7.618,"k":0,"z":3.7,"s":1},{"n":"Seoul","a":"South Korea","i":"KR","p":9796000,"la":37.568,"lo":126.998,"k":1,"z":3,"s":1},{"n":"Manila","a":"Philippines","i":"PH","p":11100000,"la":14.606,"lo":120.98,"k":1,"z":2.7,"s":1},{"n":"Monterrey","a":"Mexico","i":"MX","p":3712000,"la":25.672,"lo":-100.332,"k":0,"z":3.7,"s":1},{"n":"Berlin","a":"Germany","i":"DE","p":3406000,"la":52.524,"lo":13.4,"k":1,"z":2.7,"s":1},{"n":"Ürümqi","a":"China","i":"CN","p":3575000,"la":43.807,"lo":87.573,"k":0,"z":3.7,"s":1},{"n":"Chengdu","a":"China","i":"CN","p":4123000,"la":30.672,"lo":104.068,"k":0,"z":3,"s":1},{"n":"Ōsaka","a":"Japan","i":"JP","p":11294000,"la":34.691,"lo":135.504,"k":0,"z":3,"s":1},{"n":"Kinshasa","a":"Congo (Kinshasa)","i":"CD","p":7843000,"la":-4.328,"lo":15.313,"k":1,"z":3,"s":1},{"n":"New Delhi","a":"India","i":"IN","p":317797,"la":28.6,"lo":77.2,"k":1,"z":2.1,"s":1},{"n":"Bengaluru","a":"India","i":"IN","p":6787000,"la":12.972,"lo":77.558,"k":0,"z":3.7,"s":1},{"n":"Athens","a":"Greece","i":"GR","p":3242000,"la":37.985,"lo":23.731,"k":1,"z":3.7,"s":1},{"n":"Baghdad","a":"Iraq","i":"IQ","p":5054000,"la":33.341,"lo":44.392,"k":1,"z":3.7,"s":1},{"n":"Addis Ababa","a":"Ethiopia","i":"ET","p":3100000,"la":9.035,"lo":38.698,"k":1,"z":3,"s":1},{"n":"Tehran","a":"Iran","i":"IR","p":7873000,"la":35.674,"lo":51.422,"k":1,"z":3,"s":1},{"n":"Vancouver","a":"Canada","i":"CA","p":2313328,"la":49.275,"lo":-123.124,"k":0,"z":2.1,"s":1},{"n":"Toronto","a":"Canada","i":"CA","p":5213000,"la":43.665,"lo":-79.389,"k":0,"z":2.1,"s":1},{"n":"Buenos Aires","a":"Argentina","i":"AR","p":12795000,"la":-34.611,"lo":-58.433,"k":1,"z":2.1,"s":1},{"n":"Kabul","a":"Afghanistan","i":"AF","p":3277000,"la":34.519,"lo":69.181,"k":1,"z":3.7,"s":1},{"n":"Vienna","a":"Austria","i":"AT","p":2400000,"la":48.202,"lo":16.365,"k":1,"z":3.7,"s":1},{"n":"Melbourne","a":"Australia","i":"AU","p":4170000,"la":-37.818,"lo":144.973,"k":0,"z":3,"s":1},{"n":"Taipei","a":"Taiwan","i":"TW","p":6900273,"la":25.036,"lo":121.568,"k":1,"z":3.7,"s":1},{"n":"Auckland","a":"New Zealand","i":"NZ","p":1377200,"la":-36.848,"lo":174.763,"k":0,"z":3,"s":1},{"n":"Los Angeles","a":"United States of America","i":"US","p":12500000,"la":34.049,"lo":-118.232,"k":0,"z":2,"s":0},{"n":"Washington, D.C.","a":"United States of America","i":"US","p":4338000,"la":38.901,"lo":-77.011,"k":1,"z":2.1,"s":0},{"n":"New York","a":"United States of America","i":"US","p":19040000,"la":40.722,"lo":-73.996,"k":0,"z":1.7,"s":0},{"n":"London","a":"United Kingdom","i":"GB","p":8567000,"la":51.502,"lo":-0.119,"k":1,"z":1.7,"s":0},{"n":"Istanbul","a":"Turkey","i":"TR","p":10061000,"la":41.018,"lo":28.974,"k":0,"z":3,"s":0},{"n":"Riyadh","a":"Saudi Arabia","i":"SA","p":4465000,"la":24.634,"lo":46.72,"k":1,"z":3.7,"s":0},{"n":"Cape Town","a":"South Africa","i":"ZA","p":3215000,"la":-33.918,"lo":18.433,"k":1,"z":1.7,"s":0},{"n":"Moscow","a":"Russia","i":"RU","p":10452000,"la":55.754,"lo":37.614,"k":1,"z":1.7,"s":0},{"n":"Mexico City","a":"Mexico","i":"MX","p":19028000,"la":19.444,"lo":-99.133,"k":1,"z":2.1,"s":0},{"n":"Lagos","a":"Nigeria","i":"NG","p":9466000,"la":6.445,"lo":3.39,"k":0,"z":2,"s":0},{"n":"Rome","a":"Italy","i":"IT","p":3339000,"la":41.898,"lo":12.481,"k":1,"z":2.7,"s":0},{"n":"Beijing","a":"China","i":"CN","p":11106000,"la":39.902,"lo":116.394,"k":1,"z":1.7,"s":0},{"n":"Nairobi","a":"Kenya","i":"KE","p":3010000,"la":-1.281,"lo":36.815,"k":1,"z":2,"s":0},{"n":"Jakarta","a":"Indonesia","i":"ID","p":9125000,"la":-6.172,"lo":106.827,"k":1,"z":2.1,"s":0},{"n":"Bogota","a":"Colombia","i":"CO","p":7772000,"la":4.598,"lo":-74.085,"k":1,"z":2.1,"s":0},{"n":"Cairo","a":"Egypt","i":"EG","p":11893000,"la":30.052,"lo":31.248,"k":1,"z":1.7,"s":0},{"n":"Shanghai","a":"China","i":"CN","p":14987000,"la":31.218,"lo":121.435,"k":0,"z":2,"s":0},{"n":"Tokyo","a":"Japan","i":"JP","p":35676000,"la":35.687,"lo":139.749,"k":1,"z":1.7,"s":0},{"n":"Mumbai","a":"India","i":"IN","p":18978000,"la":19.068,"lo":72.876,"k":0,"z":1.7,"s":0},{"n":"Paris","a":"France","i":"FR","p":9904000,"la":48.858,"lo":2.353,"k":1,"z":1.7,"s":0},{"n":"Santiago","a":"Chile","i":"CL","p":5720000,"la":-33.44,"lo":-70.651,"k":1,"z":2,"s":0},{"n":"Kolkata","a":"India","i":"IN","p":14787000,"la":22.57,"lo":88.369,"k":0,"z":3.7,"s":0},{"n":"Rio de Janeiro","a":"Brazil","i":"BR","p":11748000,"la":-22.907,"lo":-43.212,"k":0,"z":1.7,"s":0},{"n":"São Paulo","a":"Brazil","i":"BR","p":18845000,"la":-23.557,"lo":-46.627,"k":0,"z":3,"s":0},{"n":"Sydney","a":"Australia","i":"AU","p":4630000,"la":-33.871,"lo":151.213,"k":0,"z":1.7,"s":0},{"n":"Singapore","a":"Singapore","i":"SG","p":5183700,"la":1.295,"lo":103.854,"k":1,"z":2.1,"s":0},{"n":"Hong Kong","a":"Hong Kong S.A.R.","i":"HK","p":7206000,"la":22.307,"lo":114.183,"k":0,"z":3,"s":0}]} \ No newline at end of file diff --git a/src/maps/data/rivers_50m.json b/src/maps/data/rivers_50m.json new file mode 100644 index 0000000..669bdd0 --- /dev/null +++ b/src/maps/data/rivers_50m.json @@ -0,0 +1 @@ +{"f":[{"n":"Kama","z":5,"b":[49.248,55.113,51.937,55.701],"l":[[51.937,55.701,51.881,55.686,51.82,55.697,51.748,55.694,51.663,55.608,51.579,55.599,51.513,55.583,51.509,55.529,51.486,55.496,51.369,55.468,51.213,55.503,51.135,55.483,51.079,55.468,50.98,55.466,50.834,55.456,50.688,55.42,50.412,55.401,50.078,55.381,49.822,55.335,49.532,55.261,49.382,55.172,49.248,55.113]]},{"n":"Kama","z":5,"b":[51.936,55.695,56.686,60.5],"l":[[53.694,58.206,53.677,58.273,53.626,58.357,53.552,58.394,53.465,58.419,53.429,58.435,53.439,58.485,53.471,58.511,53.492,58.587,53.381,58.672,53.3,58.78,53.247,58.825,53.201,58.93,53.239,59.011,53.219,59.099,53.15,59.128,53.063,59.178,53.003,59.209,52.964,59.282,52.912,59.34,52.866,59.402,52.815,59.494,52.689,59.582,52.615,59.608,52.609,59.68,52.639,59.71,52.687,59.734,52.767,59.724,52.803,59.758,52.866,59.798,52.92,59.896,52.975,59.956,53.035,59.973,53.126,59.99,53.221,60.015,53.322,60.052,53.401,60.056,53.451,60.074,53.492,60.124,53.526,60.139,53.595,60.138,53.732,60.184,53.825,60.21,53.897,60.259,53.947,60.292,53.986,60.323,54.038,60.337,54.105,60.336,54.155,60.357,54.267,60.361,54.313,60.337,54.391,60.277,54.521,60.239,54.849,60.245,55.035,60.226,55.146,60.203,55.25,60.221,55.358,60.251,55.571,60.311,55.664,60.417,55.715,60.485,55.873,60.5,55.979,60.459,56.11,60.424,56.137,60.392,56.135,60.385,56.165,60.334,56.156,60.287,56.15,60.234,56.152,60.205,56.168,60.155,56.236,60.159,56.277,60.14,56.281,60.107,56.299,60.052,56.294,60.001,56.311,59.945,56.341,59.93,56.364,59.923,56.385,59.917,56.434,59.883,56.471,59.85,56.55,59.811,56.591,59.766,56.623,59.691,56.661,59.625,56.683,59.562,56.686,59.452,56.678,59.389,56.612,59.349,56.575,59.333,56.427,59.304,56.342,59.279,56.24,59.25,56.209,59.197,56.179,59.098,56.224,58.997,56.252,58.885,56.268,58.81,56.145,58.696,56.089,58.655,56.095,58.572,56.177,58.554,56.251,58.522,56.393,58.443,56.341,58.261,56.334,58.146,56.333,58.136,56.331,58.11,56.328,58.096,56.322,58.078,56.308,58.059,56.201,58.028,56.066,58.03,55.891,57.949,55.832,57.946,55.794,58.022,55.726,58.076,55.661,58.069,55.633,58.015,55.475,57.896,55.299,57.877,55.247,57.874,55.212,57.832,55.267,57.796,55.347,57.763,55.4,57.68,55.449,57.59,55.525,57.577,55.544,57.532,55.456,57.33,55.355,57.313,55.248,57.306,55.136,57.342,55.081,57.341,55.043,57.305,55.016,57.237,54.979,57.114,54.922,57.091,54.85,57.086,54.781,57.055,54.67,57.042,54.539,57.055,54.453,57.042,54.379,56.998,54.313,56.984,54.222,56.979,54.167,56.942,54.15,56.87,54.136,56.82,54.088,56.771,54.046,56.753,53.996,56.736,53.949,56.74,53.917,56.777,53.91,56.812,53.885,56.828,53.831,56.839,53.797,56.834,53.746,56.805,53.743,56.794,53.75,56.777,53.752,56.759,53.747,56.715,53.757,56.674,53.774,56.661,53.781,56.654,53.792,56.613,53.8,56.528,53.814,56.49,53.842,56.458,53.854,56.419,53.866,56.401,53.9,56.393,53.975,56.36,54.035,56.343,54.102,56.278,54.157,56.24,54.185,56.188,54.117,56.158,54.099,56.144,54.083,56.123,54.047,56.058,54.006,56.029,53.949,56.022,53.888,56.032,53.834,56.051,53.774,56.051,53.739,56.034,53.663,55.993,53.643,55.982,53.565,55.97,53.543,55.959,53.54,55.951,53.539,55.94,53.54,55.918,53.539,55.914,53.536,55.911,53.526,55.904,53.499,55.893,53.286,55.852,53.063,55.87,52.992,55.9,52.917,55.926,52.842,55.93,52.76,55.954,52.708,55.965,52.609,55.928,52.53,55.863,52.468,55.852,52.377,55.87,52.332,55.871,52.306,55.842,52.316,55.803,52.344,55.753,52.352,55.729,52.335,55.705,52.277,55.696,52.094,55.695,52.023,55.722,51.936,55.701]]},{"n":"Abay","z":3,"b":[37.113,11.632,37.387,11.906],"l":[[37.113,11.855,37.15,11.894,37.197,11.906,37.264,11.903,37.32,11.834,37.387,11.632]]},{"n":"Al Furat","z":3,"b":[38.066,35.863,38.561,36.342],"l":[[38.561,35.863,38.365,35.903,38.246,35.951,38.139,36.02,38.079,36.074,38.066,36.114,38.092,36.167,38.157,36.232,38.188,36.284,38.184,36.342]]},{"n":"Alabama","z":5,"b":[-86.522,32.811,-86.2,33.588],"l":[[-86.522,33.032,-86.521,33.014,-86.461,32.836,-86.445,32.811],[-86.2,33.588,-86.212,33.56,-86.33,33.444]]},{"n":"Albany","z":5,"b":[-91.97,50.998,-90.417,51.988],"l":[[-91.312,51.063,-91.075,50.998,-90.875,51.014,-90.634,51.087,-90.489,51.116,-90.417,51.093],[-91.598,51.162,-91.418,51.068],[-91.781,51.335,-91.886,51.458],[-91.915,51.762,-91.881,51.723,-91.885,51.67,-91.917,51.59,-91.923,51.534,-91.893,51.487],[-91.929,51.892,-91.97,51.988]]},{"n":"Albert Nile","z":2,"b":[31.356,2.193,31.475,2.4],"l":[[31.367,2.193,31.356,2.254,31.383,2.306,31.475,2.4]]},{"n":"Amur","z":3,"b":[136.77,49.759,140.114,51.764],"l":[[137.07,50.454,137.101,50.298,137.057,50.216,136.946,50.155,136.872,50.098,136.834,50.045,136.771,49.915,136.77,49.85,136.81,49.759],[140.114,51.764,139.95,51.64,139.902,51.597,139.858,51.579]]},{"n":"Angara","z":2.1,"b":[101.777,51.816,106.497,57.943],"l":[[106.497,52.383,106.438,52.415,106.352,52.404,106.221,52.36,106.069,52.264,105.896,52.117,105.725,51.998,105.556,51.908,105.361,51.847,105.141,51.816,104.99,51.819,104.866,51.872],[103.374,53.107,103.403,53.282,103.404,53.425,103.384,53.594,103.279,53.797,103.087,54.032,103.058,54.218,103.19,54.355,103.251,54.481,103.24,54.598,103.192,54.696,103.107,54.774,103.133,54.892,103.268,55.05,103.329,55.18,103.316,55.282,103.278,55.379,103.215,55.47,103.186,55.575,103.189,55.692,103.151,55.777,103.071,55.828,103.024,55.895,103.009,55.98,102.965,56.057,102.892,56.127,102.751,56.173,102.544,56.195,102.304,56.168,102.031,56.092,101.863,56.072,101.8,56.107,101.777,56.166,101.803,56.291],[102.674,57.943,102.674,57.855,102.651,57.789,102.606,57.717,102.534,57.638,102.434,57.551,102.371,57.446,102.345,57.321,102.338,57.195,102.358,57.004]]},{"n":"Atbara","z":5,"b":[35.895,14.62,35.924,14.935],"l":[[35.924,14.935,35.9,14.863,35.895,14.62]]},{"n":"Athabasca","z":4.7,"b":[-111.28,58.505,-111.016,58.788],"l":[[-111.016,58.691,-111.075,58.505],[-111.28,58.788,-111.208,58.72,-111.142,58.696,-111.016,58.691]]},{"n":"Back","z":5,"b":[-108.533,64.562,-96.995,66.151],"l":[[-108.533,64.562,-108.18,64.686],[-108.089,64.715,-107.979,64.72],[-98.656,65.928,-98.677,66.003,-98.716,66.023,-98.782,66.014,-98.874,66.032,-98.992,66.076,-99.127,66.062,-99.279,65.989,-99.467,65.932,-99.692,65.892,-99.852,65.886,-99.948,65.912,-100.115,65.909,-100.353,65.876,-100.582,65.871,-100.8,65.895,-100.973,65.898,-101.099,65.879,-101.314,65.889,-101.77,65.947],[-97.267,65.974,-96.995,66.151]]},{"n":"Biya","z":5,"b":[87.392,51.364,87.756,51.762],"l":[[87.756,51.364,87.686,51.636,87.595,51.735,87.392,51.762]]},{"n":"Chattahoochee","z":5,"b":[-85.125,30.721,-83.815,34.364],"l":[[-84.966,30.923,-84.887,30.759,-84.884,30.721],[-85.067,31.635,-85.125,31.777,-85.124,31.881,-85.064,32.051],[-83.815,34.364,-84.076,34.167]]},{"n":"Churchill","z":5,"b":[-64.713,53.125,-64.678,53.382],"l":[[-64.682,53.125,-64.713,53.275,-64.712,53.339,-64.678,53.382]]},{"n":"Colorado","z":4.7,"b":[-114.741,34.253,-105.821,40.185],"l":[[-114.377,34.54,-114.371,34.488,-114.308,34.433,-114.159,34.355,-114.125,34.317,-114.125,34.314,-114.126,34.287,-114.164,34.253],[-114.591,35.353,-114.649,35.476,-114.662,35.545,-114.645,35.631],[-114.741,36.014,-114.716,36.085,-114.669,36.122,-114.593,36.148,-114.505,36.156,-114.406,36.148,-114.332,36.116,-114.282,36.06,-114.232,36.032,-114.183,36.03,-114.119,36.077,-114.062,36.175,-114.005,36.195,-113.949,36.16,-113.76,36.007,-113.636,35.831],[-110.368,37.898,-110.399,37.883,-110.421,37.832,-110.43,37.772,-110.47,37.702,-110.54,37.626,-110.654,37.521,-110.692,37.483,-110.698,37.452,-110.722,37.406,-110.781,37.336,-110.916,37.157,-111.019,37.127,-111.301,37.054,-111.431,37.003,-111.473,36.932],[-105.821,40.185,-105.861,40.165]]},{"n":"Columbia","z":3,"b":[-118.527,48.232,-117.517,52.102],"l":[[-118.072,48.664,-118.174,48.512,-118.198,48.404,-118.168,48.232],[-118.092,49.802,-118.13,49.664,-118.128,49.574,-118.097,49.484,-118.014,49.404,-117.907,49.342],[-117.906,50.682,-117.917,50.465,-117.892,50.339,-117.837,50.231,-117.827,50.159,-117.88,50.103],[-117.517,51.659,-117.873,51.87],[-118.527,52.102,-118.366,52.09]]},{"n":"Coppermine","z":5,"b":[-114.366,64.638,-111.956,65.64],"l":[[-112.343,64.899,-112.05,64.776,-111.956,64.711,-111.966,64.638],[-114.173,65.639,-114.305,65.64,-114.355,65.609,-114.366,65.544,-114.29,65.496,-114.127,65.465,-114.034,65.425,-114.011,65.378,-113.787,65.336,-113.36,65.298,-113.025,65.252,-112.781,65.199,-112.6,65.177,-112.483,65.186,-112.389,65.138,-112.317,65.032,-112.29,64.974,-112.318,64.96]]},{"n":"Dalälven","z":5,"b":[13.212,60.122,17.416,61.663],"l":[[16.434,60.156,16.323,60.122],[17.416,60.51,17.389,60.461,17.246,60.402,16.978,60.315,16.832,60.252,16.807,60.211,16.721,60.184,16.499,60.163],[14.551,60.998,14.632,60.922,14.729,60.881,14.868,60.848,14.946,60.806,14.973,60.728],[13.717,61.414,13.672,61.468,13.591,61.502,13.459,61.533,13.348,61.577,13.212,61.663]]},{"n":"Dniester","z":5,"b":[30.259,46.101,30.478,46.32],"l":[[30.478,46.101,30.311,46.233,30.259,46.288,30.269,46.32]]},{"n":"Dnieper","z":4,"b":[30.478,46.831,35.149,51.217],"l":[[33.54,46.831,33.606,46.869,33.662,46.957,33.739,47.05,33.859,47.149,33.976,47.307,33.998,47.411,34.155,47.49,34.448,47.542,34.712,47.553,34.946,47.521,35.088,47.528,35.138,47.573,35.149,47.657,35.106,47.842],[33.194,49.094,32.884,49.225,32.603,49.302,31.886,49.555],[31.44,50,31.48,49.933,31.486,49.871,31.467,49.754],[30.508,51.217,30.478,50.904,30.479,50.749,30.515,50.597]]},{"n":"Don","z":5,"b":[42.11,47.602,43.985,49.507],"l":[[42.11,47.602,42.303,47.661,42.484,47.728,42.663,47.794,42.832,47.838,42.881,47.885,42.927,47.961,43.042,48.186,43.132,48.294,43.237,48.389,43.459,48.539,43.528,48.614,43.522,48.653,43.481,48.699,43.465,48.737,43.474,48.769,43.522,48.821,43.606,48.892,43.69,48.929,43.773,48.931,43.844,48.949,43.896,48.968,43.923,49.01,43.938,49.072,43.985,49.136,43.956,49.186,43.905,49.204,43.785,49.203,43.749,49.207,43.708,49.25,43.7,49.328,43.659,49.402,43.602,49.486,43.549,49.507]]},{"n":"Blue Nile","z":3,"b":[34.38,11.296,34.778,11.783],"l":[[34.38,11.783,34.409,11.639,34.509,11.517,34.778,11.296]]},{"n":"Ertis","z":2.1,"b":[83.364,47.732,84.357,49.644],"l":[[83.364,49.644,83.603,49.52,83.731,49.471,83.826,49.454,83.959,49.379,84.132,49.245,84.204,49.162,84.176,49.129,84.012,49.07,83.714,48.984,83.533,48.889,83.468,48.784,83.45,48.668,83.479,48.543,83.524,48.455,83.587,48.404,83.589,48.359,83.531,48.318,83.507,48.273,83.516,48.222,83.642,48.137,83.887,48.018,84.072,47.902,84.198,47.791,84.285,47.734,84.357,47.732]]},{"n":"Firat","z":3,"b":[38.74,38.813,38.813,39.078],"l":[[38.768,38.813,38.813,38.875,38.74,39.078]]},{"n":"Gan","z":5,"b":[115.982,29.279,116.198,29.751],"l":[[115.982,29.279,116.034,29.319,116.088,29.437,116.198,29.751]]},{"n":"Grande","z":4.7,"b":[-50.994,-21.465,-44.387,-19.82],"l":[[-44.387,-21.465,-44.616,-21.318],[-45.205,-21.093,-45.252,-21.086,-45.276,-21.067,-45.336,-21.035,-45.443,-21.011,-45.577,-20.918,-45.737,-20.754,-45.847,-20.686,-45.906,-20.714,-45.951,-20.714,-45.981,-20.686,-46.078,-20.671,-46.326,-20.668],[-47.073,-20.284,-46.741,-20.48,-46.619,-20.564,-46.587,-20.623],[-50.994,-20.102,-50.582,-19.82],[-49.385,-19.98,-50.365,-19.858]]},{"n":"Han","z":5,"b":[111,32.568,111.478,32.741],"l":[[111.478,32.568,111,32.741]]},{"n":"Huang","z":3,"b":[97.072,34.835,97.763,35.088],"l":[[97.288,34.835,97.248,34.889,97.194,34.913,97.072,34.931],[97.763,35.088,97.714,34.962,97.659,34.909,97.546,34.876]]},{"n":"Indus","z":3,"b":[72.707,34.086,72.848,34.623],"l":[[72.782,34.623,72.835,34.371,72.848,34.258,72.838,34.198,72.802,34.148,72.707,34.086]]},{"n":"Jordan","z":5,"b":[35.423,31.325,35.599,32.9],"l":[[35.423,31.325,35.465,31.562,35.499,31.672,35.559,31.766],[35.599,32.9,35.577,32.72]]},{"n":"Kemijoki","z":5,"b":[27.359,66.467,27.519,66.765],"l":[[27.385,66.765,27.519,66.668,27.513,66.621,27.414,66.592,27.363,66.55,27.359,66.467]]},{"n":"Klamath","z":4.7,"b":[-121.921,42.251,-121.809,42.475],"l":[[-121.921,42.475,-121.916,42.419,-121.809,42.251]]},{"n":"Krishna","z":5,"b":[78.997,16.23,79.313,16.579],"l":[[78.997,16.23,79.157,16.235,79.211,16.301,79.213,16.429,79.238,16.515,79.313,16.579]]},{"n":"La Grande Rivière","z":4,"b":[-77.42,53.427,-70.843,53.798],"l":[[-70.843,53.427,-70.948,53.439,-70.991,53.468,-71.007,53.518,-71.041,53.555,-71.117,53.591],[-72.034,53.598,-71.926,53.572,-71.871,53.584,-71.817,53.646],[-74.433,53.685,-74.697,53.631,-74.852,53.628,-74.965,53.648,-75.239,53.656,-75.617,53.674,-75.802,53.664,-75.895,53.71,-76.136,53.699,-76.664,53.668,-77.04,53.677,-77.196,53.699,-77.311,53.732,-77.42,53.798]]},{"n":"Mackenzie","z":2.1,"b":[-117.668,61.048,-113.704,61.339],"l":[[-117.668,61.308,-117.221,61.15,-116.826,61.082,-116.335,61.051,-115.996,61.048,-115.81,61.073,-115.502,61.157,-115.072,61.299,-114.569,61.339,-113.704,61.246]]},{"n":"Madison","z":2,"b":[-111.309,44.757,-111.1,44.868],"l":[[-111.1,44.757,-111.118,44.762,-111.189,44.771,-111.242,44.8,-111.301,44.863,-111.309,44.868]]},{"n":"Mahäna Nadï","z":5,"b":[83.3,21.52,83.858,21.736],"l":[[83.858,21.52,83.713,21.673,83.636,21.728,83.578,21.736,83.528,21.717,83.488,21.669,83.425,21.658,83.3,21.695]]},{"n":"Mississippi","z":4.7,"b":[-94.637,47.406,-94.094,47.464],"l":[[-94.326,47.406,-94.202,47.457,-94.144,47.464,-94.094,47.433],[-94.507,47.456,-94.637,47.44]]},{"n":"Missouri","z":2,"b":[-111.697,43.06,-98.56,48.151],"l":[[-99.365,43.991,-99.328,43.894,-99.353,43.818,-99.43,43.729,-99.442,43.674,-99.39,43.653,-99.347,43.599,-99.313,43.515,-99.196,43.415,-98.997,43.299,-98.892,43.225,-98.882,43.192,-98.798,43.146,-98.56,43.06],[-111.516,46.362,-111.54,46.469,-111.585,46.544,-111.697,46.662],[-100.777,46.713,-100.625,46.608,-100.569,46.513,-100.556,46.393,-100.571,46.287,-100.614,46.196,-100.557,46.041,-100.4,45.822,-100.369,45.705,-100.369,45.674,-100.402,45.643,-100.433,45.613,-100.438,45.561,-100.433,45.535,-100.406,45.505,-100.342,45.448,-100.297,45.316,-100.302,45.134,-100.321,45.053,-100.348,45.037,-100.375,45.033,-100.402,44.993,-100.41,44.918,-100.444,44.854,-100.505,44.8,-100.583,44.783,-100.68,44.802,-100.691,44.795,-100.697,44.776,-100.659,44.735,-100.635,44.68,-100.633,44.62,-100.609,44.578,-100.561,44.555,-100.556,44.53,-100.562,44.504,-100.536,44.476,-100.418,44.45,-100.394,44.434],[-101.4,47.506,-101.428,47.552,-101.549,47.556,-101.772,47.533,-101.976,47.547,-102.161,47.598,-102.269,47.669,-102.299,47.76,-102.345,47.799,-102.409,47.786,-102.491,47.794,-102.59,47.824,-102.62,47.881,-102.58,47.965,-102.614,48.038,-102.724,48.1,-102.873,48.137,-103.06,48.151,-103.179,48.136,-103.23,48.093,-103.308,48.059,-103.414,48.036,-103.496,48.054,-103.555,48.114,-103.631,48.115,-103.772,48.027],[-106.406,48.02,-106.519,47.886,-106.6,47.822,-106.879,47.707,-107.04,47.694,-107.262,47.706,-107.385,47.694,-107.41,47.659,-107.473,47.645,-107.575,47.653,-107.689,47.608,-107.813,47.512,-107.888,47.502,-107.94,47.581,-108.02,47.598,-108.196,47.602]]},{"n":"Black Volta","z":4.7,"b":[-1.713,8.631,-0.976,8.762],"l":[[-1.713,8.631,-1.57,8.682,-1.502,8.714,-1.437,8.756,-1.376,8.762,-1.3,8.714,-1.177,8.672,-0.976,8.662]]},{"n":"Murray","z":4,"b":[146.028,-36.108,147.272,-36.015],"l":[[147.043,-36.108,147.1,-36.05,147.157,-36.036,147.272,-36.049],[146.028,-36.015,146.216,-36.056]]},{"n":"White Volta","z":5,"b":[-1.155,8.661,-0.974,9.117],"l":[[-0.974,8.661,-1.111,8.856,-1.155,8.97,-1.152,9.117]]},{"n":"Naryn","z":4.7,"b":[72.654,41.667,73.3,41.79],"l":[[73.3,41.776,72.932,41.79,72.771,41.762,72.654,41.667]]},{"n":"Nelson","z":5,"b":[-98.015,50.979,-94.779,56.394],"l":[[-96.598,50.979,-96.595,51.308,-96.631,51.482,-96.703,51.611,-96.952,51.86,-97.377,52.23,-97.643,52.532,-97.75,52.766,-97.831,53.042,-97.914,53.519,-97.869,53.714,-97.906,53.776,-97.981,53.837,-98.015,53.886,-98.009,53.925,-98.005,53.966,-97.987,53.988,-97.914,54.003,-97.798,54.026,-97.731,54.082],[-97.901,54.705,-97.791,54.571],[-97.946,54.948,-97.254,55.236],[-96.553,56.063,-96.504,56.107,-96.421,56.129,-96.285,56.141,-96.196,56.171,-96.152,56.219,-96.058,56.255,-95.841,56.291],[-95.431,56.339,-94.779,56.394]]},{"n":"Neva","z":4.7,"b":[31.043,59.951,31.414,60.235],"l":[[31.043,59.951,31.181,59.983,31.274,60.054,31.414,60.235]]},{"n":"Niger","z":3,"b":[-4.304,9.865,4.657,15.507],"l":[[4.626,9.865,4.529,10.239,4.537,10.425,4.657,10.609],[-3.98,15.281,-4.166,15.365,-4.247,15.421,-4.304,15.507]]},{"n":"Nile","z":2,"b":[31.091,21.609,32.914,23.988],"l":[[32.879,23.988,32.909,23.808,32.903,23.696,32.872,23.591,32.878,23.481,32.903,23.415,32.914,23.332,32.855,23.208,32.68,23.009,32.587,22.876,32.576,22.806,32.514,22.74,32.418,22.672,32.326,22.653,32.232,22.718,32.137,22.733,32.042,22.699,31.961,22.63,31.894,22.526,31.735,22.395,31.544,22.252,31.4,22.093,31.362,21.98,31.28,21.845,31.091,21.609]]},{"n":"Ob","z":4,"b":[81.327,53.886,83.025,54.845],"l":[[83.025,54.845,82.312,54.491,82.004,54.358,81.862,54.33,81.741,54.269,81.639,54.176,81.522,54.104,81.391,54.052,81.327,53.991,81.334,53.886]]},{"n":"Orange","z":5,"b":[24.731,-30.664,25.972,-29.979],"l":[[25.972,-30.545,25.902,-30.548,25.841,-30.581,25.764,-30.648,25.67,-30.664,25.506,-30.612],[24.731,-29.979,24.978,-30.263]]},{"n":"Ord","z":5,"b":[128.708,-16.788,128.756,-16.133],"l":[[128.737,-16.133,128.708,-16.434,128.714,-16.575,128.747,-16.638,128.756,-16.699,128.734,-16.788]]},{"n":"Ottawa","z":5,"b":[-79.559,46.827,-79.166,47.577],"l":[[-79.166,46.827,-79.365,47.022,-79.435,47.134,-79.442,47.228,-79.479,47.319,-79.547,47.407,-79.559,47.483,-79.496,47.577]]},{"n":"Panama Canal","z":5,"b":[-79.923,9.104,-79.691,9.267],"l":[[-79.691,9.104,-79.756,9.112,-79.814,9.153,-79.923,9.267]]},{"n":"Paranaíba","z":4,"b":[-50.499,-19.02,-47.447,-18.171],"l":[[-50.49,-19.02,-50.499,-18.953,-50.458,-18.877,-50.368,-18.769,-50.247,-18.69,-50.094,-18.641,-49.96,-18.616,-49.789,-18.615],[-48.227,-18.34,-48.952,-18.326,-49.108,-18.388],[-47.447,-18.171,-47.786,-18.379,-47.929,-18.445,-48.017,-18.434]]},{"n":"Paraná","z":4,"b":[-54.611,-25.433,-50.994,-19.668],"l":[[-54.611,-25.433,-54.473,-25.22,-54.436,-25.121,-54.454,-25.065,-54.413,-24.867,-54.313,-24.528,-54.281,-24.306,-54.317,-24.201,-54.318,-24.128,-54.267,-24.066],[-51.936,-21.423,-51.955,-21.469,-51.99,-21.494,-52.048,-21.511,-52.073,-21.554,-52.065,-21.623,-52.123,-21.718,-52.246,-21.84,-52.339,-21.957,-52.403,-22.069,-52.566,-22.207,-52.961,-22.455],[-50.997,-19.668,-50.994,-20.102,-51.064,-20.223,-51.165,-20.308,-51.397,-20.442]]},{"n":"Peace","z":2.1,"b":[-124.836,56,-113.714,58.71],"l":[[-122.222,56.021,-122.482,56.112,-122.691,56.124,-122.934,56.086,-123.088,56.046,-123.152,56.005,-123.323,56,-123.601,56.03,-123.797,56.033,-123.912,56.011,-124.083,56.085,-124.309,56.256,-124.454,56.389,-124.517,56.483,-124.621,56.587,-124.836,56.757],[-113.823,58.71,-113.714,58.669]]},{"n":"Pit","z":5,"b":[-122.369,40.763,-122.095,40.784],"l":[[-122.095,40.784,-122.369,40.763]]},{"n":"R. des Outaouais","z":5,"b":[-78.813,47.478,-76.935,47.83],"l":[[-77.457,47.556,-77.471,47.566,-77.484,47.595,-77.53,47.596,-77.592,47.576,-77.613,47.582,-77.615,47.597,-77.544,47.671],[-77.322,47.573,-77.301,47.562,-77.265,47.527,-77.216,47.507,-77.158,47.531,-77.114,47.526,-77.086,47.492,-77.037,47.478,-76.935,47.488],[-78.724,47.684,-78.744,47.672,-78.813,47.656],[-77.527,47.744,-77.528,47.766,-77.544,47.82,-77.567,47.83]]},{"n":"Rainy","z":5,"b":[-93.382,48.616,-91.319,48.912],"l":[[-93.382,48.616,-92.779,48.678],[-91.319,48.828,-91.44,48.903,-91.518,48.912,-91.63,48.865]]},{"n":"Rhein","z":4,"b":[8.881,47.511,9.554,47.671],"l":[[9.128,47.671,8.881,47.656],[9.183,47.671,9.554,47.511]]},{"n":"Rhône","z":5,"b":[6.133,46.205,6.866,46.437],"l":[[6.133,46.205,6.235,46.333,6.322,46.394,6.429,46.431,6.578,46.437,6.866,46.403]]},{"n":"Rio Grande","z":4,"b":[-107.293,26.564,-99.172,33.356],"l":[[-99.457,27.057,-99.444,27.037,-99.302,26.885,-99.23,26.762,-99.172,26.564],[-101.039,29.46,-101.304,29.634],[-107.293,32.91,-107.281,33.114],[-107.181,33.173,-107.17,33.356]]},{"n":"Rupert","z":5,"b":[-76.819,51.338,-74.818,51.446],"l":[[-76.819,51.416,-76.781,51.354,-76.73,51.338,-76.553,51.364],[-75.204,51.446,-75.086,51.442,-75,51.419,-74.948,51.374,-74.895,51.367,-74.818,51.412]]},{"n":"Río Grande de Santiago","z":5,"b":[-105.104,20.198,-102.77,21.822],"l":[[-102.78,20.31,-102.77,20.198],[-103.709,20.938,-103.594,20.971],[-105.104,21.822,-105.06,21.807,-105.034,21.779]]},{"n":"Sacramento","z":4.7,"b":[-122.411,40.721,-122.369,40.763],"l":[[-122.369,40.763,-122.411,40.721]]},{"n":"San Joaquin","z":5,"b":[-119.698,37,-119.601,37.062],"l":[[-119.698,37,-119.601,37.062]]},{"n":"San Juan","z":4.7,"b":[-85.871,11.118,-84.77,12.098],"l":[[-84.77,11.118,-84.978,11.207,-85.253,11.453,-85.871,12.098]]},{"n":"Saskatchewan","z":4,"b":[-103.891,53.159,-97.914,53.982],"l":[[-99.23,53.185,-98.416,53.354,-98.082,53.436,-97.914,53.519],[-100.323,53.593,-100.163,53.503,-100.066,53.405,-99.976,53.268,-99.852,53.212,-99.693,53.237,-99.543,53.227,-99.33,53.159],[-103.421,53.652,-103.58,53.629,-103.697,53.586,-103.891,53.481],[-102.437,53.982,-102.381,53.966]]},{"n":"Sassandra","z":5,"b":[-7.057,6.264,-6.989,7.041],"l":[[-7.043,7.041,-7.057,6.911,-7.039,6.825,-6.993,6.739,-6.989,6.586,-7.026,6.365,-7.008,6.264]]},{"n":"Savannah","z":5,"b":[-82.589,33.664,-82.208,34.018],"l":[[-82.589,34.018,-82.352,33.838,-82.257,33.749,-82.208,33.664]]},{"n":"Semliki","z":5,"b":[29.452,-0.607,31.367,2.193],"l":[[29.452,-0.607,29.597,-0.134],[31.367,2.193,31.159,1.922,30.943,1.683,30.478,1.239]]},{"n":"Shire","z":3,"b":[35.257,-14.758,35.278,-14.504],"l":[[35.257,-14.504,35.278,-14.758]]},{"n":"Shishhid Gol","z":4,"b":[99.31,51.349,99.427,51.41],"l":[[99.427,51.349,99.31,51.41]]},{"n":"Snake","z":4,"b":[-113.472,42.654,-111.048,43.338],"l":[[-113.316,42.654,-113.472,42.675],[-112.864,42.813,-112.64,43.023],[-111.048,43.207,-111.093,43.234,-111.186,43.338]]},{"n":"South Saskatchewan","z":5,"b":[-108.006,50.636,-106.69,51.262],"l":[[-106.853,51.262,-106.69,51.13,-106.691,51.072,-106.803,51.045,-106.901,50.963,-106.986,50.824,-107.159,50.723,-107.42,50.66,-107.665,50.636,-108.006,50.658]]},{"n":"Suez Canal","z":5,"b":[32.36,30.181,32.561,30.424],"l":[[32.561,30.181,32.36,30.424]]},{"n":"Sukhona","z":4,"b":[39.053,59.525,39.799,59.868],"l":[[39.053,59.868,39.5,59.616,39.686,59.53,39.799,59.525]]},{"n":"Sutlej","z":5,"b":[76.435,30.792,81.295,31.422],"l":[[81.295,30.792,81.199,30.852],[76.765,31.409,76.744,31.334,76.715,31.298,76.671,31.278,76.596,31.306,76.435,31.422]]},{"n":"Svir’","z":4.7,"b":[31.414,60.235,32.821,60.655],"l":[[31.414,60.235,31.595,60.408,31.798,60.52,32.086,60.629,32.356,60.655,32.611,60.598,32.711,60.544,32.821,60.483]]},{"n":"Syr Darya","z":4.7,"b":[61.562,40.273,70.355,46.11],"l":[[70.355,40.435,70.319,40.371,70.186,40.331,69.827,40.273],[68.456,41.107,68.173,41.13,68.05,41.165,68.003,41.212,67.991,41.255],[61.76,46.021,61.562,46.11]]},{"n":"São Francisco","z":4,"b":[-45.336,-19.051,-40.818,-9.406],"l":[[-45.089,-19.051,-45.176,-18.801,-45.202,-18.591,-45.249,-18.501,-45.324,-18.459,-45.336,-18.38,-45.262,-18.203],[-43.15,-11.143,-43.057,-10.988,-42.945,-10.888,-42.781,-10.791,-42.633,-10.638,-42.5,-10.427,-42.439,-10.273,-42.45,-10.175,-42.404,-10.09,-42.301,-10.019,-42.209,-9.911,-42.128,-9.768,-41.91,-9.696,-41.554,-9.694,-41.335,-9.676,-41.253,-9.643,-41.175,-9.568,-41.102,-9.453,-41.003,-9.406,-40.818,-9.438]]},{"n":"Tajo","z":4.7,"b":[-5.996,39.797,-2.577,40.724],"l":[[-5.996,39.852,-5.851,39.807,-5.656,39.797,-5.215,39.812],[-2.734,40.528,-2.577,40.724]]},{"n":"Tennessee","z":4.7,"b":[-88.283,34.575,-86.899,37.022],"l":[[-86.899,34.575,-87.242,34.754,-87.4,34.819,-87.486,34.828,-87.572,34.818,-87.7,34.773],[-87.975,35.945,-88.004,35.962,-87.992,36.011,-87.949,36.1,-87.959,36.216,-88.023,36.359,-88.056,36.484,-88.057,36.59,-88.114,36.738,-88.283,37.022]]},{"n":"Teslin","z":3,"b":[-133.245,59.607,-132.095,60.449],"l":[[-132.095,59.607,-132.353,59.921,-132.545,60.069,-132.759,60.156,-132.96,60.262,-133.245,60.449]]},{"n":"Thelon","z":4.7,"b":[-104.834,63.071,-94.225,64.761],"l":[[-104.834,63.071,-104.729,63.128,-104.697,63.178,-104.706,63.27],[-96.273,64.301,-95.448,64.158,-95.049,64.158,-94.811,64.126,-94.602,64.062,-94.429,64.041,-94.225,64.076],[-101.811,64.411,-101.536,64.493],[-97.103,64.761,-97.163,64.715,-97.292,64.716,-97.511,64.75,-97.773,64.741,-98.08,64.687,-98.264,64.666,-98.326,64.679,-98.382,64.652,-98.433,64.585,-98.493,64.555,-98.563,64.562,-98.687,64.539,-98.864,64.487,-99.003,64.491,-99.104,64.552,-99.293,64.583,-99.571,64.584,-99.789,64.603,-99.949,64.639,-100.088,64.633,-100.208,64.586,-100.4,64.573,-100.795,64.604]]},{"n":"Tocantins","z":4,"b":[-49.687,-5.335,-48.937,-3.835],"l":[[-49.687,-3.835,-49.639,-4.211,-49.546,-4.546,-49.392,-4.966,-49.28,-5.218,-49.21,-5.303,-49.115,-5.335,-48.937,-5.303]]},{"n":"Tugaloo","z":5,"b":[-83.166,34.366,-82.819,34.599],"l":[[-83.166,34.599,-83.053,34.511,-82.976,34.476,-82.897,34.466,-82.848,34.437,-82.819,34.366]]},{"n":"Ural","z":4,"b":[58.631,51.695,58.835,52.197],"l":[[58.835,52.197,58.782,52.071,58.767,51.976,58.773,51.87,58.74,51.786,58.631,51.695]]},{"n":"Vaal","z":4,"b":[25.622,-27.699,28.497,-26.867],"l":[[26.091,-27.603,26.06,-27.69,26.037,-27.699,26.012,-27.659,25.955,-27.624,25.865,-27.594,25.771,-27.601,25.622,-27.668],[28.497,-26.991,28.446,-26.973,28.124,-26.867]]},{"n":"Verkhniy Yenisey","z":4,"b":[90.939,53.735,92.408,55.503],"l":[[91.511,53.735,91.454,53.83,91.466,53.904,91.528,53.987,91.556,54.046,91.551,54.079,91.484,54.167,91.358,54.311,91.286,54.415,91.269,54.479,91.179,54.55,91.016,54.628,90.939,54.722,90.947,54.832,90.988,54.926,91.062,55.004,91.351,55.087,91.855,55.176,92.182,55.291,92.408,55.503]]},{"n":"Victoria Nile","z":2,"b":[31.768,-0.936,33.259,1.666],"l":[[33.187,0.428,33.259,0.379,33.232,0.294,33.129,0.159,33.018,-0.052,32.897,-0.338,32.744,-0.545,32.56,-0.675,32.292,-0.789,31.768,-0.936],[32.545,1.522,32.614,1.454,32.685,1.427,32.78,1.422,32.835,1.405,32.893,1.311],[32.241,1.666,32.475,1.552]]},{"n":"Vilyuy","z":5,"b":[109.587,62.857,112.48,63.48],"l":[[109.587,63.455,109.814,63.426,109.939,63.435,110.038,63.473,110.142,63.48,110.251,63.457,110.345,63.423,110.423,63.378,110.449,63.313,110.421,63.227,110.474,63.177,110.61,63.163,110.644,63.128,110.577,63.073,110.665,63.008,110.909,62.935,111.057,62.935,111.107,63.01,111.234,63.057,111.436,63.076,111.576,63.126,111.656,63.208,111.741,63.239,111.831,63.218,111.886,63.176,111.907,63.112,111.869,63.059,111.772,63.019,111.777,62.965,111.885,62.896,111.995,62.86,112.107,62.857,112.208,62.906,112.296,63.007,112.375,63.048,112.48,63.021]]},{"n":"Vistula","z":4.7,"b":[18.81,49.925,18.937,49.932],"l":[[18.81,49.925,18.937,49.932]]},{"n":"Volga","z":3,"b":[36.375,50.222,49.905,58.288],"l":[[45.664,50.222,45.831,50.357,45.836,50.457,45.733,50.567,45.68,50.647,45.675,50.696,45.744,50.742,45.885,50.782,45.981,50.824,46.03,50.866,46.044,50.925,46.023,51,45.968,51.066,45.879,51.124,45.836,51.188,45.837,51.26,45.87,51.322,45.935,51.375,45.971,51.423,45.979,51.465,46.033,51.506,46.131,51.547,46.218,51.614,46.294,51.708,46.365,51.746,46.477,51.773,46.566,51.752,46.68,51.744,46.775,51.748],[49.059,55.483,49.285,55.234,49.248,55.113,49.023,55.036,48.9,54.91,48.878,54.736,48.782,54.617,48.611,54.554,48.507,54.489,48.47,54.422,48.453,54.344,48.458,54.257,48.585,54.135,48.833,53.978,48.964,53.827,48.978,53.681,49.006,53.579,49.048,53.522,49.104,53.483,49.209,53.461,49.567,53.467,49.905,53.457],[43.432,56.675,43.344,56.733,43.286,56.814,43.231,56.937,43.175,57.024,43.119,57.075,43.089,57.119,43.082,57.156,43.104,57.213,43.178,57.329,42.874,57.41,42.724,57.431,42.576,57.415],[37.151,56.753,36.882,56.783,36.736,56.75,36.624,56.664,36.52,56.643,36.375,56.706],[37.761,57.291,37.944,57.312,38.079,57.382,38.301,57.572],[41.343,57.493,41.206,57.517,41.099,57.588,40.976,57.714,40.889,57.784,40.838,57.799,40.706,57.79,40.494,57.755,40.293,57.695,40.007,57.567],[38.437,58.018,38.326,58.083,38.319,58.15,38.408,58.288,38.704,58.113]]},{"n":"Volta","z":4.7,"b":[-0.976,6.338,0.231,8.662],"l":[[-0.976,8.662,-0.726,8.362,-0.526,8.198,-0.295,8.068,-0.153,7.945,-0.101,7.828,-0.054,7.759,-0.011,7.738,0.039,7.648,0.098,7.49,0.155,7.394,0.209,7.36,0.231,7.245,0.218,7.05,0.195,6.922,0.16,6.863,0.161,6.802,0.197,6.74,0.194,6.688,0.134,6.635,0.108,6.551,0.103,6.338]]},{"n":"Vorma","z":5,"b":[11.157,59.389,11.203,59.911],"l":[[11.17,59.389,11.201,59.451,11.203,59.498],[11.2,59.67,11.194,59.72,11.159,59.831,11.157,59.911]]},{"n":"Vuoksi","z":5,"b":[29.596,60.236,31.412,60.717],"l":[[30.478,60.574,30.298,60.654,30.117,60.676,29.874,60.669,29.714,60.678,29.596,60.717],[30.534,60.63,30.658,60.649,30.793,60.635,30.979,60.593,31.157,60.489,31.412,60.236]]},{"n":"Waikato","z":5,"b":[175.802,-38.939,176.059,-38.694],"l":[[175.802,-38.939,175.83,-38.917,175.965,-38.821,176.033,-38.758,176.059,-38.694]]},{"n":"Winnipeg","z":5,"b":[-96.598,48.863,-94.508,50.979],"l":[[-94.615,49.993,-94.508,49.707,-94.549,49.574,-94.703,49.496,-94.802,49.425,-94.847,49.361,-94.86,49.259,-94.843,49.119,-94.803,49.003,-94.713,48.863],[-96.026,50.283,-95.989,50.405],[-96.598,50.979,-96.467,50.774,-96.392,50.685,-96.294,50.624]]},{"n":"Yuan","z":5,"b":[111.993,28.838,113.071,29.389],"l":[[112.905,29.031,112.801,28.927,112.693,28.874,112.545,28.838,112.351,28.861,111.993,28.985],[112.994,29.147,113.071,29.389]]},{"n":"Zambezi","z":3,"b":[27.021,-17.958,32.708,-15.565],"l":[[27.021,-17.958,27.131,-17.849,27.271,-17.643,27.438,-17.512,27.637,-17.262,27.757,-17.06,27.932,-16.896,28.164,-16.77,28.4,-16.663,28.761,-16.532],[32.708,-15.579,32.497,-15.565,32.346,-15.589,32.184,-15.647,31.924,-15.667,31.566,-15.65,31.329,-15.662,31.213,-15.704,31.065,-15.704,30.796,-15.642]]},{"n":"","z":5,"b":[-111.016,58.544,-103.665,59.361],"l":[[-103.785,58.691,-103.665,58.544],[-111.016,58.691,-110.802,58.744,-110.637,58.82,-110.448,58.937,-110.257,59.031,-110.064,59.103,-109.775,59.183,-109.389,59.272,-109.105,59.305,-108.92,59.282,-108.518,59.298,-107.898,59.351,-107.511,59.361,-107.358,59.329,-106.966,59.303,-106.018,59.271],[-105.55,59.15,-105.249,59.194,-105.115,59.2,-105.012,59.173]]},{"n":"","z":4.7,"b":[-103.447,61.366,-95.447,64.158],"l":[[-103.22,61.681,-103.192,61.643,-103.224,61.609,-103.306,61.567,-103.372,61.5,-103.447,61.366],[-102.919,62.319,-102.909,62.281,-102.862,62.26,-102.777,62.241,-102.727,62.215,-102.714,62.182,-102.783,62.13,-102.936,62.061,-103.049,61.979,-103.158,61.836],[-100.552,62.355,-100.074,62.227,-99.841,62.213,-99.617,62.3],[-101.163,62.367,-100.735,62.356],[-101.831,62.52,-101.847,62.451,-101.826,62.39,-101.775,62.314,-101.654,62.295,-101.366,62.35],[-98.35,62.616,-98.24,62.717,-98.082,62.755,-97.841,62.764,-97.697,62.822,-97.63,62.979],[-101.529,62.664,-101.537,62.791,-101.581,62.862,-101.66,62.919,-101.783,62.941,-102.035,62.925],[-102.66,62.76,-102.68,62.694,-102.665,62.642,-102.622,62.583,-102.634,62.517,-102.735,62.411],[-97.459,63.157,-97.287,63.514],[-95.878,63.656,-96.301,63.613,-96.569,63.608,-96.952,63.637],[-95.497,64.051,-95.447,64.158]]},{"n":"Abay","z":3,"b":[36.981,11.036,37.18,11.855],"l":[[37.179,11.036,37.022,11.192,36.981,11.287,37.015,11.416,37.05,11.58,37.097,11.656,37.167,11.7,37.18,11.755,37.113,11.855]]},{"n":"Blue Nile","z":3,"b":[35.353,9.884,38.514,11.634],"l":[[37.387,11.634,37.393,11.571,37.45,11.534,37.56,11.504,37.68,11.421,37.812,11.288,38.019,11.186,38.302,11.116,38.462,11.037,38.5,10.949,38.514,10.772,38.503,10.505,38.478,10.314,38.439,10.199,38.376,10.136,38.289,10.123,38.11,10.048,37.839,9.911,37.66,9.884,37.574,9.966,37.49,10.021,37.409,10.049,37.354,10.052,37.328,10.032,37.308,10.04,37.296,10.076,37.263,10.105,37.212,10.125,37.189,10.164,37.194,10.224,37.159,10.266,37.046,10.303,36.677,10.36,36.541,10.404,36.514,10.453,36.483,10.469,36.45,10.454,36.395,10.375,36.32,10.232,36.207,10.155,36.055,10.143,35.957,10.111,35.912,10.058,35.849,10.036,35.768,10.044,35.726,10.03,35.724,10.009,35.72,9.998,35.706,9.996,35.628,10.017,35.369,10.14,35.353,10.155]]},{"n":"Al Furat","z":3,"b":[38.026,34.428,40.993,36.836],"l":[[38.56,35.863,38.916,35.913,39.227,35.881,39.613,35.784,39.833,35.683,39.889,35.579,40.004,35.473,40.179,35.363,40.263,35.298,40.257,35.277,40.294,35.253,40.377,35.225,40.424,35.167,40.438,35.079,40.475,35.016,40.535,34.978,40.584,34.918,40.62,34.837,40.711,34.752,40.855,34.663,40.928,34.581,40.93,34.506,40.946,34.458,40.993,34.428],[38.026,36.836,38.056,36.741,38.103,36.694,38.174,36.663,38.231,36.618,38.272,36.558,38.266,36.482,38.184,36.342]]},{"n":"Alabama","z":5,"b":[-88.016,30.694,-85.183,34.259],"l":[[-86.446,32.812,-86.268,32.622,-86.243,32.527,-86.31,32.462,-86.328,32.42,-86.339,32.409,-86.374,32.418,-86.423,32.408,-86.482,32.368,-86.543,32.363,-86.605,32.394,-86.652,32.398,-86.684,32.373,-86.72,32.369,-86.759,32.384,-86.786,32.374,-86.795,32.354,-86.81,32.353,-86.824,32.362,-86.84,32.41,-86.855,32.42,-86.871,32.42,-86.911,32.381,-86.95,32.373,-86.989,32.398,-87.025,32.404,-87.057,32.391,-87.058,32.371,-87.03,32.342,-87.05,32.317,-87.119,32.297,-87.139,32.26,-87.11,32.207,-87.133,32.176,-87.208,32.167,-87.246,32.138,-87.25,32.089,-87.285,32.086,-87.354,32.127,-87.409,32.11,-87.453,32.036,-87.452,31.98,-87.407,31.942,-87.418,31.916,-87.484,31.901,-87.494,31.868,-87.447,31.816,-87.456,31.765,-87.521,31.715,-87.543,31.66,-87.522,31.601,-87.535,31.559,-87.583,31.535,-87.602,31.499,-87.591,31.451,-87.63,31.413,-87.718,31.383,-87.791,31.316,-87.848,31.214,-87.893,31.159,-87.921,31.152,-87.941,31.14,-87.945,31.118,-87.951,30.919,-87.971,30.829,-88.005,30.8,-88.016,30.76,-88.011,30.694],[-86.33,33.444,-86.366,33.418,-86.376,33.382,-86.373,33.327,-86.401,33.276,-86.462,33.228,-86.484,33.189,-86.468,33.158,-86.476,33.115,-86.517,33.061,-86.522,33.032],[-85.183,34.259,-85.501,34.209,-85.657,34.205,-85.703,34.203,-85.727,34.192,-85.733,34.174,-85.707,34.154,-85.705,34.134,-85.734,34.112,-85.771,34.107,-85.813,34.113,-85.838,34.1,-85.845,34.076,-85.863,34.058,-85.881,34.058,-85.898,34.05,-85.901,34.032,-85.922,34.01,-85.95,34.01,-85.965,33.992,-85.967,33.967,-85.984,33.944,-86.033,33.928,-86.055,33.885,-86.046,33.811,-86.082,33.753,-86.161,33.712,-86.191,33.667,-86.194,33.615,-86.2,33.588]]},{"n":"Albany","z":5,"b":[-92.299,51.063,-81.828,52.224],"l":[[-91.418,51.069,-91.312,51.063],[-90.416,51.092,-89.862,51.198,-89.593,51.278,-89.423,51.368,-89.293,51.419,-89.205,51.43,-89.141,51.461,-89.104,51.51,-89.056,51.515,-88.998,51.474,-88.912,51.483,-88.799,51.541,-88.689,51.57,-88.583,51.57,-88.51,51.55,-88.469,51.511,-88.329,51.493,-88.088,51.494,-87.853,51.434,-87.624,51.314,-87.448,51.26,-87.324,51.27,-87.198,51.309,-87.067,51.378,-86.941,51.412,-86.82,51.413,-86.702,51.442,-86.529,51.528,-86.439,51.582,-86.4,51.635,-86.384,51.705,-86.352,51.75,-86.306,51.77,-86.247,51.775,-86.177,51.766,-86.095,51.731,-86.001,51.669,-85.841,51.646,-85.616,51.663,-85.4,51.605,-85.194,51.471,-85.078,51.378,-85.053,51.325,-84.906,51.251,-84.638,51.158,-84.455,51.14,-84.358,51.198,-84.078,51.296,-83.615,51.433,-83.314,51.572,-83.176,51.713,-83.035,51.821,-82.893,51.896,-82.758,51.936,-82.631,51.943,-82.505,51.985,-82.381,52.063,-82.253,52.111,-82.055,52.138,-81.883,52.194,-81.828,52.224],[-91.782,51.336,-91.724,51.277,-91.684,51.258,-91.659,51.244,-91.646,51.215,-91.641,51.19,-91.598,51.161],[-91.893,51.487,-91.886,51.458],[-91.929,51.892,-91.915,51.762],[-92.299,51.974,-92.237,51.948,-92.155,51.951,-91.97,51.988]]},{"n":"Albert Nile","z":2,"b":[31.382,2.401,32.016,3.614],"l":[[32.016,3.614,31.935,3.528,31.885,3.508,31.839,3.526,31.785,3.523,31.724,3.499,31.624,3.363,31.485,3.117,31.404,2.951,31.382,2.864,31.388,2.804,31.421,2.77,31.434,2.719,31.427,2.651,31.45,2.588,31.503,2.53,31.515,2.476,31.475,2.401]]},{"n":"Aldan","z":4,"b":[123.063,56.528,135.834,63.49],"l":[[123.566,56.528,123.205,56.561,123.101,56.558,123.066,56.58,123.063,56.625,123.086,56.68,123.134,56.746,123.17,56.837,123.193,56.953,123.294,57.091,123.474,57.248,123.555,57.341,123.537,57.369,123.552,57.407,123.598,57.454,123.59,57.485,123.528,57.5,123.505,57.539,123.521,57.603,123.546,57.651,123.58,57.682,123.63,57.685,123.697,57.658,123.757,57.669,123.81,57.718,123.804,57.789,123.739,57.882,123.719,57.977,123.746,58.074,123.824,58.154,123.955,58.214,123.985,58.271,123.989,58.291,124.021,58.315,124.101,58.328,124.229,58.332,124.298,58.366,124.31,58.43,124.348,58.503,124.445,58.628,124.578,58.72,124.654,58.749,124.716,58.744,124.759,58.761,124.782,58.8,124.845,58.824,124.945,58.832,125.032,58.86,125.104,58.908,125.144,58.95,125.152,58.988,125.205,59.016,125.304,59.035,125.339,59.065,125.31,59.107,125.309,59.142,125.336,59.17,125.338,59.206,125.315,59.249,125.314,59.286,125.335,59.316,125.406,59.314,125.527,59.281,125.636,59.226,125.735,59.15,125.877,59.092,126.064,59.053,126.193,59.007,126.265,58.955,126.471,58.896,126.809,58.832,127.008,58.78,127.068,58.742,127.14,58.747,127.221,58.795,127.32,58.811,127.436,58.795,127.521,58.805,127.577,58.843,127.657,58.85,127.761,58.825,127.815,58.792,127.818,58.75,127.855,58.716,127.928,58.689,128.004,58.712,128.082,58.784,128.157,58.818,128.229,58.813,128.287,58.792,128.329,58.754,128.377,58.766,128.432,58.826,128.478,58.817,128.515,58.739,128.569,58.694,128.64,58.684,128.688,58.648,128.712,58.588,128.816,58.545,128.999,58.518,129.17,58.518,129.326,58.545,129.489,58.538,129.658,58.5,129.837,58.524,130.026,58.612,130.234,58.645,130.461,58.622,130.586,58.628,130.608,58.662,130.609,58.714,130.588,58.785,130.602,58.835,130.65,58.865,130.782,58.9,130.998,58.938,131.141,58.937,131.21,58.899,131.266,58.904,131.307,58.952,131.362,58.965,131.431,58.945,131.526,58.947,131.648,58.97,131.768,59.01,131.887,59.067,131.948,59.116,131.95,59.157,132.003,59.172,132.106,59.161,132.209,59.177,132.366,59.242,132.638,59.372,132.806,59.412,132.96,59.406,133.058,59.459,133.101,59.573,133.179,59.635,133.291,59.646,133.53,59.79,133.896,60.069,134.198,60.267,134.554,60.444,134.705,60.46,134.839,60.44,135.007,60.389,135.104,60.392,135.13,60.446,135.089,60.506,134.982,60.57,134.927,60.629,134.924,60.685,135.019,60.754,135.211,60.836,135.362,60.946,135.472,61.084,135.506,61.187,135.462,61.256,135.462,61.313,135.504,61.357,135.478,61.394,135.383,61.424,135.383,61.47,135.476,61.532,135.526,61.58,135.533,61.614,135.485,61.632,135.383,61.633,135.293,61.662,135.215,61.718,135.264,61.761,135.438,61.791,135.53,61.843,135.539,61.917,135.599,61.982,135.709,62.036,135.738,62.094,135.685,62.156,135.715,62.212,135.826,62.261,135.834,62.315,135.739,62.376,135.651,62.473,135.572,62.608,135.489,62.677,135.404,62.679,135.325,62.661,135.252,62.622,135.11,62.617,134.898,62.647,134.638,62.718,134.332,62.83,134.184,62.911,134.194,62.962,134.146,62.989,134.039,62.992,133.972,63.007,133.946,63.035,133.946,63.074,133.803,63.132,133.441,63.091,133.224,63.092,133.151,63.135,133.146,63.177,133.209,63.219,133.236,63.274,133.23,63.345,132.974,63.356,132.469,63.31,132.018,63.318,131.619,63.382,131.343,63.386,131.19,63.331,131.007,63.313,130.689,63.341,130.431,63.339,130.105,63.377,129.384,63.49]]},{"n":"Allegheny","z":3,"b":[-79.991,40.448,-77.86,42.144],"l":[[-77.86,41.832,-77.999,41.781,-78.083,41.771,-78.197,41.79,-78.33,41.878,-78.367,41.942,-78.351,42.013,-78.42,42.059,-78.573,42.081,-78.674,42.11,-78.723,42.144,-78.797,42.128,-78.895,42.061,-78.955,41.985,-78.977,41.899,-79.059,41.849,-79.201,41.834,-79.308,41.789,-79.381,41.714,-79.417,41.65,-79.416,41.571,-79.452,41.506,-79.513,41.48,-79.61,41.472,-79.699,41.447,-79.78,41.405,-79.81,41.374,-79.79,41.352,-79.798,41.328,-79.834,41.3,-79.816,41.264,-79.746,41.221,-79.682,41.155,-79.624,41.066,-79.604,41.012,-79.62,40.992,-79.587,40.966,-79.523,40.939,-79.515,40.906,-79.543,40.824,-79.698,40.637,-79.83,40.519,-79.946,40.456,-79.991,40.448]]},{"n":"Amazonas","z":2,"b":[-73.489,-4.445,-52.664,-1.552],"l":[[-52.664,-1.552,-52.874,-1.611,-53,-1.668,-53.145,-1.764,-53.358,-1.838,-53.639,-1.892,-53.843,-1.958,-53.971,-2.038,-54.045,-2.141,-54.064,-2.267,-54.187,-2.358,-54.468,-2.44,-54.525,-2.44,-54.564,-2.441,-54.639,-2.419,-54.676,-2.359,-54.675,-2.253,-54.694,-2.184,-54.734,-2.152,-54.794,-2.12,-54.873,-2.089,-54.957,-2.104,-55.046,-2.167,-55.15,-2.18,-55.269,-2.145,-55.375,-2.083,-55.458,-1.987,-55.515,-1.949,-55.555,-1.924,-55.606,-1.918,-55.669,-1.927,-55.736,-1.964,-55.84,-1.99,-55.981,-2.006,-56.129,-2.08,-56.284,-2.212,-56.401,-2.336,-56.48,-2.454,-56.549,-2.527,-56.607,-2.556,-56.676,-2.575,-56.756,-2.582,-56.847,-2.567,-56.951,-2.528,-57.05,-2.523,-57.143,-2.551,-57.244,-2.526,-57.353,-2.448,-57.44,-2.405,-57.506,-2.396,-57.605,-2.481,-57.74,-2.662,-57.883,-2.784,-58.035,-2.848,-58.135,-2.959,-58.184,-3.117,-58.235,-3.2,-58.268,-3.214,-58.316,-3.212,-58.38,-3.202,-58.453,-3.182,-58.549,-3.205,-58.642,-3.304,-58.689,-3.342,-58.805,-3.347,-58.89,-3.323,-58.982,-3.271,-59.091,-3.239,-59.217,-3.228,-59.381,-3.184,-59.584,-3.109,-59.735,-3.078,-59.837,-3.092,-59.897,-3.128,-59.906,-3.171,-59.925,-3.206,-59.957,-3.237,-60.105,-3.306,-60.203,-3.325,-60.279,-3.302,-60.411,-3.3,-60.598,-3.319,-60.747,-3.398,-60.858,-3.538,-61.036,-3.61,-61.28,-3.614,-61.468,-3.676,-61.598,-3.796,-61.717,-3.883,-61.824,-3.936,-61.893,-3.947,-62.011,-3.896,-62.277,-3.762,-62.367,-3.718,-62.393,-3.716,-62.423,-3.731,-62.449,-3.802,-62.543,-3.851,-62.705,-3.878,-62.863,-3.938,-63.018,-4.032,-63.097,-4.078,-63.152,-4.072,-63.177,-4.035,-63.216,-3.974,-63.283,-3.923,-63.45,-3.871,-63.573,-3.854,-63.654,-3.872,-63.747,-3.868,-63.854,-3.843,-64.035,-3.765,-64.291,-3.632,-64.458,-3.524,-64.576,-3.398,-64.79,-3.222,-64.905,-3.128,-65.07,-2.971,-65.18,-2.835,-65.288,-2.747,-65.395,-2.707,-65.459,-2.663,-65.48,-2.615,-65.558,-2.594,-65.761,-2.603,-65.901,-2.493,-65.977,-2.466,-66.038,-2.485,-66.096,-2.477,-66.153,-2.442,-66.21,-2.46,-66.269,-2.532,-66.334,-2.537,-66.405,-2.475,-66.471,-2.439,-66.531,-2.429,-66.573,-2.478,-66.596,-2.588,-66.641,-2.659,-66.706,-2.692,-66.801,-2.715,-66.925,-2.727,-67.077,-2.695,-67.258,-2.619,-67.384,-2.623,-67.455,-2.707,-67.572,-2.779,-67.736,-2.838,-67.846,-2.928,-67.932,-3.108,-67.927,-3.223,-67.946,-3.283,-67.988,-3.326,-68.095,-3.334,-68.266,-3.307,-68.596,-3.345,-69.084,-3.449,-69.352,-3.553,-69.399,-3.655,-69.416,-3.764,-69.404,-3.879,-69.425,-3.974,-69.478,-4.049,-69.481,-4.111,-69.434,-4.161,-69.459,-4.223,-69.556,-4.299,-69.685,-4.335,-69.847,-4.331,-69.948,-4.285,-69.966,-4.236,-70.017,-4.162,-70.095,-4.092,-70.168,-4.05,-70.198,-3.995,-70.24,-3.883,-70.298,-3.844,-70.339,-3.814,-70.379,-3.819,-70.421,-3.85,-70.486,-3.869,-70.53,-3.866,-70.557,-3.87,-70.9,-3.96,-71.058,-3.985,-71.138,-3.963,-71.193,-3.903,-71.283,-3.869,-71.408,-3.862,-71.521,-3.826,-71.623,-3.761,-71.689,-3.65,-71.718,-3.492,-71.752,-3.395,-71.789,-3.359,-71.914,-3.384,-72.126,-3.468,-72.293,-3.503,-72.476,-3.481,-72.747,-3.457,-72.891,-3.467,-72.997,-3.504,-73.094,-3.562,-73.18,-3.643,-73.211,-3.783,-73.189,-3.984,-73.192,-4.099,-73.221,-4.13,-73.268,-4.136,-73.334,-4.117,-73.366,-4.133,-73.364,-4.183,-73.385,-4.231,-73.431,-4.277,-73.463,-4.336,-73.489,-4.445]]},{"n":"Amu Darya","z":4.7,"b":[58.316,36.95,68.212,44.426],"l":[[68.212,37.022,68.068,36.95,67.958,36.972,67.834,37.064,67.766,37.14,67.753,37.2,67.7,37.227,67.607,37.223,67.547,37.236,67.517,37.267,67.442,37.258,67.32,37.21,67.195,37.235,67.069,37.335,66.828,37.371,66.472,37.345,66.108,37.415,65.737,37.582,65.48,37.68,65.337,37.712,65.22,37.797,65.131,37.938,64.907,38.088,64.549,38.247,64.272,38.432,64.076,38.642,63.953,38.8,63.904,38.904,63.724,39.049,63.413,39.232,63.198,39.33,63.081,39.344,63.014,39.4,62.998,39.5,62.897,39.608,62.71,39.723,62.573,39.845,62.442,40.036,62.375,40.332,62.298,40.468,62.188,40.541,62.095,40.683,62.018,40.894,61.954,41.031,61.903,41.094,61.8,41.163,61.645,41.24,61.497,41.276,61.357,41.272,61.263,41.3,61.215,41.36,61.031,41.487,60.71,41.68,60.527,41.825,60.482,41.919,60.42,41.978,60.343,42,60.291,42.04,60.266,42.1,60.203,42.164,60.102,42.232,59.95,42.291,59.745,42.341,59.587,42.416,59.475,42.516,59.387,42.574,59.322,42.591,59.3,42.632,59.319,42.697,59.298,42.744,59.236,42.772,59.21,42.808,59.222,42.852,59.204,42.904,59.156,42.966,59.139,43.026,59.153,43.084,59.141,43.152,59.103,43.23,59.093,43.288,59.097,43.294],[59.099,43.292,59.097,43.294],[59.097,43.294,59.112,43.326,59.098,43.379,59.052,43.446,59.036,43.498,59.05,43.533,59.106,43.561,59.204,43.581,59.255,43.633,59.26,43.716,59.211,43.805,59.107,43.901,59.082,43.971,59.134,44.016,59.255,44.066,59.541,44.148],[59.097,43.294,59.005,43.423,58.993,43.526,59.032,43.643,59.019,43.732,58.952,43.792,58.831,43.839,58.655,43.872,58.57,43.948,58.575,44.067,58.512,44.202,58.316,44.426]]},{"n":"Amur","z":3,"b":[127.492,47.681,140.84,53.22],"l":[[136.81,49.759,136.843,49.729,136.844,49.678,136.825,49.595,136.721,49.52,136.53,49.452,136.394,49.367,136.311,49.265,136.271,49.171,136.275,49.086,136.162,48.987,135.818,48.817,135.745,48.764,135.669,48.755,135.565,48.771,135.445,48.768,135.307,48.746,135.185,48.705,135.077,48.645,135.026,48.571,135.032,48.482,134.965,48.421,134.823,48.388,134.709,48.383,134.623,48.406,134.487,48.403,134.206,48.36,133.842,48.274,133.672,48.208,133.573,48.133,133.468,48.097,133.301,48.102,133.144,48.106,133.02,48.064,132.877,47.979,132.773,47.94,132.707,47.947,132.637,47.89,132.562,47.769,132.476,47.715,132.38,47.729,132.15,47.718,131.785,47.681,131.557,47.682,131.464,47.723,131.319,47.728,131.122,47.698,131.003,47.691,130.962,47.709,130.933,47.76,130.915,47.843,130.849,47.929,130.733,48.019,130.712,48.128,130.787,48.255,130.804,48.341,130.763,48.388,130.747,48.43,130.659,48.483,130.597,48.575,130.552,48.603,130.566,48.68,130.617,48.773,130.553,48.861,130.355,48.866,130.196,48.892,130.037,48.972,129.793,49.199,129.671,49.278,129.591,49.287,129.534,49.323,129.498,49.389,129.441,49.389,129.385,49.389,129.35,49.362,129.31,49.354,129.248,49.379,129.185,49.381,129.12,49.362,129.065,49.375,129.02,49.419,128.938,49.449,128.819,49.464,128.77,49.495,128.791,49.542,128.769,49.577,128.704,49.6,128.527,49.594,128.237,49.559,128,49.569,127.814,49.622,127.711,49.672,127.69,49.717,127.637,49.76,127.551,49.802,127.502,49.873,127.492,49.975,127.512,50.072,127.59,50.209],[139.858,51.579,139.806,51.563,139.705,51.558,139.602,51.544,139.495,51.505,139.449,51.449,139.378,51.405,139.283,51.374,139.159,51.363,139.008,51.375,138.918,51.35,138.89,51.291,138.825,51.248,138.675,51.207,138.068,50.856,137.708,50.714,137.391,50.665,137.187,50.607,137.095,50.54,137.054,50.493,137.07,50.454],[140.114,51.764,140.147,51.786,140.206,51.814,140.218,51.875,140.182,51.964,140.215,52.046,140.317,52.121,140.385,52.226,140.42,52.362,140.405,52.468,140.338,52.545,140.262,52.592,140.178,52.61,140.125,52.657,140.104,52.734,139.996,52.822,139.801,52.922,139.757,53.032,139.863,53.153,140.003,53.215,140.176,53.22,140.343,53.188,140.504,53.119,140.614,53.091,140.707,53.111,140.783,53.105,140.84,53.088]]},{"n":"Anadyr’","z":5,"b":[168.574,64.684,174.549,67.099],"l":[[170.966,67.099,171.345,66.974,171.448,66.826,171.508,66.766,171.559,66.745,171.377,66.686,170.962,66.587,170.659,66.486,170.468,66.383,170.248,66.304,169.998,66.249,169.857,66.206,169.824,66.175,169.81,66.071,169.815,65.893,169.766,65.791,169.612,65.755,169.123,65.647,168.935,65.582,168.883,65.526,168.786,65.475,168.643,65.428,168.574,65.37,168.581,65.302,168.654,65.245,168.795,65.201,168.814,65.14,168.711,65.065,168.646,64.995,168.618,64.931,168.704,64.866,168.902,64.8,169.146,64.781,169.434,64.808,169.755,64.795,170.107,64.742,170.4,64.72,170.632,64.729,170.733,64.777,170.76,64.823,170.776,64.859,170.788,64.912,170.832,64.928,170.891,64.926,170.977,64.959,171.09,65.026,171.125,65.076,171.079,65.107,171.067,65.162,171.089,65.24,171.261,65.281,171.583,65.284,171.793,65.252,171.892,65.186,172.022,65.152,172.185,65.148,172.294,65.169,172.352,65.214,172.498,65.266,172.732,65.325,172.889,65.392,172.968,65.467,173.053,65.516,173.145,65.54,173.234,65.544,173.322,65.529,173.37,65.502,173.38,65.464,173.456,65.415,173.6,65.354,173.676,65.31,173.685,65.282,173.668,65.242,173.606,65.164,173.55,65.124,173.543,65.09,173.567,65.048,173.648,65.021,173.787,65.007,173.91,64.951,174.017,64.855,174.164,64.778,174.442,64.692,174.549,64.684]]},{"n":"Angara","z":2.1,"b":[92.998,51.87,104.867,58.973],"l":[[104.867,51.87,104.437,52.165,104.29,52.289,104.281,52.339,104.205,52.399,104.059,52.47,103.973,52.54,103.945,52.61,103.877,52.674,103.77,52.733,103.71,52.799,103.698,52.873,103.617,52.934,103.469,52.982,103.39,53.032,103.374,53.107],[101.803,56.291,101.817,56.561,101.77,56.687,101.668,56.76,101.561,56.801,101.449,56.808,101.391,56.848,101.387,56.919,101.461,56.97,101.612,57.002,101.778,56.988,101.959,56.929,102.113,56.898,102.239,56.894,102.316,56.92,102.358,57.004],[102.674,57.943,102.795,58.122,102.837,58.245,102.84,58.373,102.78,58.524,102.659,58.698,102.48,58.83,102.462,58.836],[102.489,58.829,102.462,58.836],[102.462,58.836,102.101,58.933],[102.462,58.836,102.243,58.919,102.101,58.933],[102.101,58.933,101.842,58.956,101.276,58.941,100.98,58.899,100.954,58.83,100.817,58.758,100.568,58.681,100.437,58.627,100.424,58.598,100.434,58.555,100.465,58.5,100.441,58.439,100.361,58.371,100.251,58.346,100.11,58.364,99.986,58.412,99.881,58.489,99.754,58.536,99.603,58.553,99.472,58.594,99.361,58.66,99.241,58.701,99.113,58.717,98.962,58.701,98.788,58.652,98.691,58.6,98.67,58.547,98.628,58.506,98.565,58.476,98.322,58.446,97.899,58.413,97.572,58.408,97.339,58.432,97.171,58.43,97.066,58.404,96.984,58.393,96.923,58.399,96.85,58.432,96.765,58.491,96.514,58.527,96.098,58.541,95.777,58.522,95.55,58.472,95.418,58.425,95.383,58.381,95.235,58.307,94.975,58.203,94.599,58.14,94.108,58.12,93.73,58.129,93.467,58.168,93.279,58.161,93.166,58.11,93.08,58.086,92.998,58.1],[102.101,58.933,101.948,58.973]]},{"n":"Araguaia","z":4,"b":[-53.211,-17.855,-48.159,-5.303],"l":[[-53.115,-17.855,-53.211,-17.409,-53.202,-17.302,-53.158,-17.209,-53.078,-17.095,-53.033,-16.995,-53.024,-16.91,-52.943,-16.814,-52.789,-16.705,-52.68,-16.576,-52.615,-16.427,-52.545,-16.329,-52.469,-16.282,-52.434,-16.218,-52.441,-16.136,-52.417,-16.082,-52.362,-16.058,-52.308,-16.009,-52.257,-15.936,-52.141,-15.872,-51.962,-15.819,-51.847,-15.747,-51.799,-15.656,-51.779,-15.593,-51.788,-15.557,-51.773,-15.523,-51.732,-15.49,-51.696,-15.403,-51.665,-15.261,-51.59,-15.142,-51.471,-15.044,-51.372,-15.024,-51.286,-15.003,-51.246,-15.004,-51.189,-14.977,-51.114,-14.838,-50.978,-14.463,-50.941,-14.201,-50.909,-14.107,-50.868,-14.095,-50.852,-13.995,-50.861,-13.807,-50.8,-13.612,-50.667,-13.41,-50.598,-13.249,-50.595,-13.131,-50.566,-13.015,-50.484,-12.844,-50.566,-12.82,-50.614,-12.764,-50.655,-12.667,-50.66,-12.558,-50.629,-12.437,-50.632,-12.289,-50.668,-12.112,-50.675,-11.99,-50.653,-11.922,-50.661,-11.854,-50.717,-11.749,-50.666,-11.652,-50.671,-11.574,-50.717,-11.483,-50.708,-11.346,-50.645,-11.163,-50.61,-11.001,-50.6,-10.79,-50.599,-10.69,-50.551,-10.567,-50.455,-10.388,-50.403,-10.258,-50.395,-10.177,-50.337,-10.034,-50.175,-9.73,-50.084,-9.422,-49.979,-9.22,-49.83,-9.022,-49.708,-8.895,-49.613,-8.839,-49.502,-8.704,-49.374,-8.489,-49.283,-8.301,-49.227,-8.14,-49.19,-7.995,-49.172,-7.867,-49.22,-7.752,-49.334,-7.649,-49.34,-7.51,-49.24,-7.335,-49.196,-7.167,-49.21,-7.006,-49.08,-6.856,-48.805,-6.718,-48.653,-6.596,-48.622,-6.489,-48.58,-6.414,-48.527,-6.372,-48.471,-6.353,-48.41,-6.358,-48.391,-6.309,-48.414,-6.207,-48.39,-6.142,-48.319,-6.112,-48.297,-6.067,-48.321,-6.007,-48.307,-5.966,-48.254,-5.945,-48.245,-5.889,-48.28,-5.796,-48.259,-5.724,-48.181,-5.672,-48.159,-5.619,-48.193,-5.565,-48.231,-5.533,-48.275,-5.523,-48.317,-5.486,-48.357,-5.424,-48.422,-5.398,-48.511,-5.408,-48.598,-5.399,-48.723,-5.355,-48.937,-5.303]]},{"n":"Ergun","z":3,"b":[117.873,49.513,121.405,53.317],"l":[[121.405,53.317,120.985,53.285,120.704,53.172,120.421,52.968,120.218,52.84,120.095,52.787,120.044,52.718,120.068,52.633,120.173,52.602,120.36,52.627,120.521,52.615,120.656,52.567,120.699,52.494,120.65,52.396,120.665,52.3,120.744,52.205,120.75,52.097,120.682,51.973,120.511,51.849,120.237,51.723,120.067,51.601,119.967,51.422,119.813,51.267,119.757,51.179,119.746,51.108,119.685,51.03,119.573,50.947,119.512,50.863,119.502,50.779,119.446,50.703,119.344,50.634,119.281,50.561,119.256,50.484,119.217,50.433,119.164,50.406,119.192,50.38,119.302,50.354,119.346,50.279,119.326,50.155,119.26,50.066,119.147,50.013,118.98,49.979,118.756,49.963,118.452,49.845,117.873,49.513]]},{"n":"Arkansas","z":4,"b":[-106.381,33.797,-91.077,39.222],"l":[[-91.077,33.797,-91.122,33.824,-91.15,33.864,-91.167,33.922,-91.217,33.955,-91.299,33.962,-91.323,33.974,-91.33,33.991,-91.343,34.004,-91.387,34.005,-91.419,34.026,-91.439,34.066,-91.488,34.091,-91.564,34.1,-91.682,34.153,-91.844,34.249,-91.921,34.271,-91.939,34.267,-91.973,34.275,-92.017,34.316,-92.109,34.448,-92.155,34.566,-92.155,34.673,-92.225,34.756,-92.365,34.818,-92.447,34.877,-92.469,34.934,-92.506,34.972,-92.558,34.993,-92.577,35.035,-92.562,35.096,-92.595,35.119,-92.713,35.095,-92.831,35.161,-92.93,35.184,-93.05,35.185,-93.193,35.244,-93.358,35.363,-93.521,35.415,-93.682,35.399,-93.797,35.421,-93.864,35.479,-93.958,35.494,-94.078,35.467,-94.139,35.434,-94.141,35.396,-94.162,35.373,-94.202,35.367,-94.268,35.385,-94.358,35.43,-94.43,35.415,-94.484,35.342,-94.529,35.314,-94.567,35.331,-94.602,35.325,-94.635,35.297,-94.666,35.3,-94.694,35.333,-94.792,35.375,-94.959,35.425,-95.074,35.488,-95.138,35.565,-95.19,35.603,-95.229,35.605,-95.245,35.637,-95.236,35.699,-95.251,35.747,-95.29,35.78,-95.388,35.795,-95.545,35.79,-95.63,35.816,-95.643,35.875,-95.721,35.925,-95.864,35.967,-95.958,36.029,-96.004,36.11,-96.084,36.152,-96.199,36.153,-96.26,36.174,-96.266,36.213,-96.316,36.256,-96.409,36.303,-96.477,36.322,-96.521,36.312,-96.542,36.332,-96.54,36.382,-96.578,36.415,-96.656,36.431,-96.711,36.473,-96.742,36.539,-96.767,36.542,-96.806,36.532,-96.865,36.494,-96.921,36.469,-96.96,36.472,-96.986,36.486,-96.999,36.513,-96.979,36.542,-96.944,36.567,-96.94,36.579,-96.957,36.591,-97,36.595,-97.041,36.619,-97.053,36.664,-96.993,36.706,-96.862,36.745,-96.836,36.766,-96.838,36.782,-96.877,36.797,-96.913,36.866,-96.932,36.981,-96.986,37.058,-97.074,37.098,-97.135,37.189,-97.167,37.332,-97.237,37.499,-97.344,37.69,-97.695,37.925,-98.289,38.205,-98.633,38.346,-98.728,38.349,-98.854,38.308,-99.011,38.223,-99.218,38.055,-99.474,37.805,-99.698,37.697,-99.987,37.748,-100.776,37.935,-101.127,37.971,-101.304,37.918,-101.627,37.95,-102.095,38.066,-102.559,38.118,-103.017,38.106,-103.32,38.081,-103.467,38.042,-103.594,38.048,-103.699,38.099,-103.833,38.136,-104.075,38.174,-104.237,38.238,-104.411,38.26,-104.649,38.261,-104.826,38.298,-104.94,38.371,-105.093,38.425,-105.283,38.459,-105.47,38.451,-105.654,38.4,-105.832,38.425,-106.005,38.527,-106.089,38.611,-106.087,38.677,-106.135,38.802,-106.286,39.078,-106.381,39.222]]},{"n":"Atbara","z":5,"b":[33.952,14.935,35.958,17.699],"l":[[35.583,16,35.352,16.313,35.273,16.445,35.268,16.503,35.19,16.632,35.036,16.834,34.955,16.963,34.945,17.02,34.767,17.138,34.421,17.319,34.228,17.453,34.188,17.541,34.122,17.606,33.952,17.699],[35.586,16.004,35.613,15.716,35.714,15.469,35.9,15.169,35.95,15.045,35.958,14.999,35.924,14.935]]},{"n":"Athabasca","z":4.7,"b":[-118.084,52.171,-111.07,59.005],"l":[[-117.575,52.171,-117.543,52.209,-117.552,52.271,-117.645,52.456,-117.674,52.537,-117.727,52.578,-117.814,52.606,-117.864,52.647,-117.878,52.701,-117.926,52.757,-118.007,52.815,-118.045,52.872,-118.039,52.929,-118.052,52.977,-118.084,53.018,-118.036,53.113,-117.907,53.263,-117.679,53.415,-117.354,53.57,-117.184,53.676,-117.17,53.735,-117.173,53.775,-117.193,53.796,-117.172,53.839,-117.108,53.907,-116.924,54.01,-116.617,54.151,-116.414,54.228,-116.315,54.243,-116.133,54.224,-115.868,54.172,-115.615,54.149,-115.373,54.155,-115.176,54.2,-115.021,54.285,-114.859,54.331,-114.689,54.34,-114.536,54.39,-114.399,54.481,-114.332,54.565,-114.336,54.642,-114.265,54.804,-114.049,55.174],[-114.042,55.168,-113.76,55.228,-113.599,55.172,-113.466,55.018,-113.413,54.901,-113.44,54.822,-113.41,54.771,-113.324,54.748,-113.259,54.773,-113.215,54.846,-113.077,54.909,-112.846,54.961,-112.733,55,-112.739,55.028,-112.772,55.05,-112.834,55.066,-112.85,55.116,-112.82,55.198,-112.774,55.256,-112.712,55.291,-112.708,55.385,-112.761,55.537,-112.74,55.648,-112.645,55.716,-112.614,55.789,-112.648,55.868,-112.658,55.947,-112.645,56.025,-112.61,56.098,-112.555,56.165,-112.58,56.256,-112.683,56.371,-112.65,56.464,-112.482,56.536,-112.257,56.573,-111.974,56.574,-111.728,56.615,-111.518,56.694,-111.423,56.798,-111.442,56.926,-111.488,57.023,-111.559,57.09,-111.612,57.169,-111.647,57.261,-111.657,57.337,-111.644,57.397,-111.57,57.513,-111.435,57.684,-111.381,57.808,-111.407,57.885,-111.399,57.951,-111.357,58.004,-111.348,58.048,-111.372,58.081,-111.377,58.121,-111.365,58.167,-111.391,58.207,-111.455,58.241,-111.497,58.294,-111.518,58.365,-111.514,58.412,-111.487,58.434,-111.372,58.448,-111.17,58.454,-111.07,58.468,-111.075,58.505],[-111.28,58.788,-111.2,58.892,-111.233,58.946,-111.411,59.005]]},{"n":"Ayeyarwady","z":3,"b":[94.74,16.183,97.519,25.701],"l":[[97.519,25.701,97.508,25.69,97.515,25.668,97.506,25.59,97.449,25.417,97.394,25.337,97.34,25.35,97.292,25.31,97.249,25.216,97.25,25.152,97.293,25.12,97.29,25.1,97.239,25.091,97.216,25.064,97.22,25.019,97.196,24.981,97.117,24.933,97.055,24.835,97.055,24.755,97.095,24.659,97.117,24.552,97.12,24.432,97.142,24.347,97.184,24.296,97.198,24.252,97.183,24.214,97.114,24.178,96.991,24.145,96.892,24.169,96.819,24.25,96.737,24.307,96.647,24.341,96.561,24.344,96.479,24.317,96.402,24.251,96.331,24.148,96.291,24.059,96.28,23.946,96.085,23.653,96.006,23.359,95.979,22.966,95.949,22.754,95.917,22.723,95.923,22.663,95.968,22.574,95.984,22.495,95.97,22.427,95.976,22.377,96,22.347,96.02,22.23,96.034,22.026,96.009,21.907,95.946,21.873,95.879,21.875,95.811,21.913,95.71,21.928,95.578,21.921,95.481,21.871,95.418,21.779,95.373,21.666,95.344,21.531,95.315,21.458,95.269,21.438,95.173,21.345,95.061,21.287,94.9,21.232,94.82,21.146,94.82,21.03,94.794,20.933,94.74,20.854,94.761,20.702,94.857,20.477,94.907,20.305,94.912,20.184,94.956,20.071,95.081,19.91,95.14,19.845,95.169,19.71,95.188,19.485,95.177,19.277,95.136,19.087,95.14,18.945,95.189,18.852,95.18,18.771,95.113,18.702,95.076,18.641,95.067,18.588,95.143,18.479,95.303,18.314,95.4,18.181,95.433,18.083,95.452,17.956,95.455,17.856,95.456,17.803,95.472,17.744,95.485,17.698,95.539,17.642,95.561,17.586,95.552,17.529,95.58,17.48,95.645,17.439,95.655,17.39,95.611,17.332,95.603,17.254,95.632,17.158,95.625,17.086,95.582,17.039,95.574,16.977,95.6,16.902,95.543,16.838,95.481,16.814,95.404,16.784,95.331,16.721,95.322,16.647,95.236,16.52,94.992,16.251,94.952,16.21,94.893,16.183]]},{"n":"Back","z":5,"b":[-108.828,64.562,-95.797,66.617],"l":[[-108.828,64.562,-108.533,64.562],[-108.18,64.686,-108.09,64.715],[-107.979,64.72,-107.849,64.755,-107.775,64.803,-107.712,64.877,-107.605,64.953,-107.454,65.032,-107.374,65.108,-107.365,65.18,-107.377,65.244,-107.408,65.298,-107.362,65.366,-107.239,65.447,-107.103,65.478,-106.954,65.458,-106.802,65.399,-106.648,65.301,-106.462,65.249,-106.244,65.244,-106.127,65.229,-106.111,65.205,-105.897,65.164,-105.275,65.077,-105.005,65.071,-104.648,65.112,-104.112,65.197,-103.709,65.283,-103.436,65.369,-103.277,65.445,-103.231,65.509,-103.159,65.556,-103.061,65.584,-102.812,65.601,-102.413,65.605,-102.178,65.63,-102.107,65.676,-102.096,65.724,-102.144,65.772,-102.068,65.834,-101.77,65.947],[-98.656,65.927,-98.6,65.884,-98.541,65.887,-98.461,65.92,-98.356,65.929,-98.225,65.914,-98.145,65.926,-98.116,65.966,-98.06,65.999,-97.979,66.024,-97.858,66.013,-97.698,65.964,-97.53,65.949,-97.267,65.974],[-96.995,66.152,-96.654,66.092,-96.475,66.093,-96.344,66.137,-96.192,66.206,-96.018,66.302,-95.896,66.389,-95.826,66.466,-95.799,66.527,-95.797,66.617]]},{"n":"Bafing","z":4,"b":[-12.088,10.522,-10.215,13.778],"l":[[-12.088,10.59,-12.006,10.529,-11.93,10.522,-11.835,10.549,-11.775,10.619,-11.752,10.731,-11.728,10.792,-11.691,10.809,-11.621,10.812,-11.602,10.864,-11.611,10.965,-11.58,11.073,-11.509,11.188,-11.443,11.251,-11.385,11.263,-11.303,11.317,-11.198,11.413,-11.128,11.498,-11.092,11.574,-11.059,11.6,-11.029,11.575,-10.965,11.574,-10.869,11.595,-10.744,11.734,-10.59,11.99,-10.466,12.139,-10.373,12.18,-10.341,12.231,-10.37,12.292,-10.345,12.395,-10.267,12.54,-10.238,12.636,-10.26,12.684,-10.252,12.725,-10.215,12.759,-10.223,12.797,-10.274,12.838,-10.294,12.883,-10.282,12.932,-10.292,12.957,-10.323,12.96,-10.352,13.021,-10.379,13.138,-10.43,13.212,-10.505,13.241,-10.609,13.256,-10.742,13.255,-10.804,13.297,-10.796,13.382,-10.772,13.427,-10.738,13.438,-10.734,13.454,-10.753,13.503,-10.833,13.64,-10.867,13.735,-10.873,13.751,-10.892,13.778]]},{"n":"Bahr el Zeraf","z":5,"b":[30.536,7.767,31.139,9.426],"l":[[30.536,7.767,30.621,8.031,30.646,8.212,30.641,8.4,30.669,8.516,30.731,8.56,30.784,8.641,30.829,8.758,30.84,8.873,30.819,8.986,30.891,9.138,31.139,9.426]]},{"n":"Mountain Nile","z":2,"b":[31.148,3.614,32.016,6.83],"l":[[32.016,3.614,31.711,3.944,31.588,4.101,31.544,4.194,31.517,4.326,31.507,4.496,31.525,4.606,31.57,4.656,31.635,4.817,31.719,5.087,31.77,5.3,31.788,5.455,31.789,5.589,31.774,5.702,31.743,5.802,31.673,5.93,31.46,6.357,31.329,6.582,31.148,6.83]]},{"n":"White Nile","z":2,"b":[30.2,6.83,31.148,9.531],"l":[[31.148,6.83,30.922,6.983,30.77,7.153,30.617,7.39,30.547,7.521,30.56,7.545,30.549,7.582,30.512,7.632,30.508,7.693,30.536,7.765,30.508,7.88,30.426,8.037,30.366,8.124,30.329,8.141,30.323,8.227,30.348,8.383,30.339,8.485,30.296,8.534,30.284,8.59,30.301,8.654,30.289,8.704,30.248,8.742,30.218,8.818,30.2,8.933,30.233,9.043,30.316,9.147,30.35,9.237,30.333,9.311,30.37,9.395,30.46,9.489,30.618,9.531,30.846,9.52,31.001,9.494,31.125,9.433]]},{"n":"Bamingui","z":5,"b":[18.886,7.538,19.733,8.836],"l":[[19.733,7.538,19.627,7.56,19.592,7.601,19.592,7.666,19.524,7.725,19.389,7.778,19.331,7.886,19.349,8.049,19.285,8.247,19.064,8.599,19.109,8.656,19.064,8.715,18.886,8.836]]},{"n":"Barito","z":5,"b":[114.067,-3.377,114.93,0.565],"l":[[114.838,0.565,114.871,0.458,114.853,0.416,114.792,0.403,114.736,0.355,114.659,0.229,114.659,0.183,114.637,0.159,114.592,0.142,114.522,0.145,114.425,0.167,114.349,0.174,114.295,0.166,114.259,0.143,114.239,0.106,114.219,0.094,114.192,0.041,114.221,-0.004,114.276,-0.045,114.249,-0.11,114.139,-0.198,114.079,-0.287,114.067,-0.377,114.14,-0.503,114.297,-0.664,114.427,-0.737,114.533,-0.72,114.586,-0.688,114.587,-0.641,114.639,-0.618,114.741,-0.619,114.824,-0.662,114.887,-0.747,114.923,-0.825,114.93,-0.894,114.904,-0.945,114.846,-0.975,114.836,-1.025,114.874,-1.094,114.864,-1.163,114.806,-1.231,114.807,-1.28,114.865,-1.31,114.886,-1.386,114.861,-1.566,114.796,-1.771,114.791,-1.843,114.824,-1.852,114.853,-1.891,114.877,-1.962,114.876,-2.037,114.848,-2.116,114.853,-2.222,114.889,-2.353,114.865,-2.505,114.779,-2.678,114.752,-2.803,114.782,-2.881,114.787,-2.946,114.766,-2.998,114.716,-3.052,114.636,-3.109,114.584,-3.19,114.526,-3.377]]},{"n":"Barwon","z":3,"b":[147.415,-30.125,150.47,-28.558],"l":[[150.47,-28.65,150.341,-28.564,150.15,-28.558,149.853,-28.604,149.677,-28.612,149.595,-28.565,149.512,-28.579,149.467,-28.608,149.433,-28.657,149.335,-28.723,149.174,-28.807,149.081,-28.875,149.058,-28.928,148.993,-28.986,148.887,-29.05,148.796,-29.188,148.72,-29.4,148.641,-29.528,148.558,-29.571,148.429,-29.688,148.254,-29.879,147.978,-30.013,147.415,-30.125]]},{"n":"Benue","z":4,"b":[6.776,7.788,11.924,9.497],"l":[[11.924,9.491,11.783,9.497,11.574,9.409,11.249,9.23,11.038,9.061,10.941,8.901,10.858,8.795,10.788,8.745,10.636,8.707,10.401,8.684,10.218,8.59,10.086,8.427,9.977,8.327,9.847,8.272,9.53,8.121,9.395,8.039,9.337,7.976,9.141,7.892,8.807,7.788,8.437,7.799,7.829,7.99,7.422,8.019,7.156,7.968,6.776,7.793]]},{"n":"Biya","z":5,"b":[85.056,51.762,87.392,52.633],"l":[[87.392,51.762,87.177,51.849,87.107,51.976,87.112,52.171,87.07,52.293,86.982,52.341,86.929,52.387,86.911,52.429,86.868,52.454,86.798,52.46,86.688,52.507,86.537,52.595,86.431,52.633,86.372,52.619,86.334,52.586,86.318,52.534,86.247,52.54,86.121,52.603,86.019,52.617,85.942,52.58,85.855,52.574,85.758,52.599,85.604,52.6,85.392,52.578,85.228,52.534,85.056,52.434]]},{"n":"Borcea","z":2.1,"b":[27.267,44.138,27.935,44.744],"l":[[27.267,44.138,27.769,44.363,27.93,44.486,27.935,44.592,27.922,44.67,27.877,44.744]]},{"n":"Braco Menor","z":4,"b":[-50.484,-12.846,-49.889,-9.73],"l":[[-50.484,-12.846,-50.392,-12.617,-50.296,-12.49,-50.164,-12.388,-50.105,-12.313,-50.12,-12.266,-50.114,-12.209,-50.06,-12.093,-50.036,-12.065,-49.999,-12.058,-49.948,-12.071,-49.914,-12.034,-49.897,-11.945,-49.904,-11.873,-49.935,-11.816,-49.931,-11.693,-49.892,-11.504,-49.889,-11.399,-49.92,-11.376,-49.933,-11.316,-49.926,-11.217,-49.939,-11.111,-49.974,-10.998,-49.978,-10.772,-49.954,-10.435,-49.963,-10.191,-50.006,-10.04,-50.048,-9.96,-50.09,-9.952,-50.126,-9.923,-50.158,-9.873,-50.174,-9.819,-50.175,-9.73]]},{"n":"Brahmaputra","z":2.1,"b":[89.613,23.459,95.444,28.003],"l":[[89.739,24.33,89.714,24.135,89.708,23.962,89.744,23.859,89.868,23.72,89.994,23.621,90.074,23.573,90.267,23.459],[89.739,24.33,89.809,24.172,89.923,24.082,90.116,23.971,90.214,23.869,90.218,23.776,90.244,23.735,90.291,23.745,90.355,23.709,90.476,23.583,90.562,23.537],[95.396,28.003,95.444,27.884,95.369,27.806,95.189,27.729,95.116,27.698,95.104,27.683,95.109,27.678,95.122,27.676,95.137,27.666,95.139,27.644,95.084,27.615,94.972,27.586,94.878,27.543,94.801,27.487,94.725,27.413,94.652,27.322,94.63,27.278,94.597,27.234,94.567,27.189,94.559,27.131,94.518,27.072,94.443,27.014,94.335,26.952,94.193,26.885,94.12,26.867,94.023,26.867,93.752,26.732,93.403,26.739,93.245,26.718,93.161,26.671,93.009,26.636,92.787,26.612,92.656,26.58,92.615,26.538,92.544,26.521,92.442,26.529,92.312,26.495,92.155,26.418,92.071,26.361,92.062,26.323,91.966,26.27,91.644,26.164,91.547,26.148,91.436,26.151,91.25,26.183,91.152,26.205,91.052,26.192,90.917,26.151,90.76,26.155,90.58,26.204,90.356,26.174,90.088,26.065,89.89,25.901,89.762,25.682,89.699,25.49,89.701,25.325,89.68,25.168,89.621,25.026,89.613,24.948,89.62,24.895,89.729,24.649,89.75,24.411,89.739,24.33]]},{"n":"Branco","z":5,"b":[-63.78,-1.389,-60.525,3.717],"l":[[-63.78,2.846,-63.703,3.001,-63.699,3.075,-63.741,3.12,-63.761,3.163,-63.757,3.227,-63.779,3.464,-63.743,3.597,-63.658,3.705,-63.541,3.717,-63.392,3.634,-63.289,3.615,-63.231,3.66,-63.189,3.635,-63.161,3.539,-63.111,3.459,-63.04,3.393,-62.865,3.304,-62.586,3.194,-62.383,3.142,-62.256,3.148,-62.165,3.177,-62.11,3.227,-62.047,3.251,-61.975,3.247,-61.901,3.269,-61.825,3.316,-61.72,3.333,-61.585,3.321,-61.5,3.324,-61.446,3.355,-61.353,3.359,-61.299,3.382,-61.253,3.425,-61.095,3.452,-60.825,3.465,-60.666,3.455,-60.616,3.421,-60.569,3.318,-60.525,3.146,-60.576,2.947,-60.722,2.722,-60.801,2.54,-60.813,2.404,-60.871,2.284,-60.974,2.182,-61.03,2.113,-61.039,2.079,-61.023,2.03,-60.983,1.967,-61.026,1.864,-61.154,1.722,-61.256,1.471,-61.334,1.114,-61.46,0.815,-61.634,0.575,-61.755,0.25,-61.825,-0.161,-61.847,-0.419,-61.812,-0.576,-61.868,-0.792,-61.878,-0.956,-61.861,-1.141,-61.863,-1.27,-61.881,-1.389]]},{"n":"Bratul Chillia","z":2.1,"b":[28.761,45.18,29.706,45.42],"l":[[29.706,45.26,29.652,45.314,29.568,45.371,29.404,45.42,29.224,45.403,29.027,45.321,28.894,45.29,28.824,45.311,28.782,45.31,28.767,45.286,28.77,45.267,28.791,45.252,28.788,45.241,28.761,45.234,28.781,45.218,28.883,45.18]]},{"n":"Bratul Sfintu Gheorghe","z":2.1,"b":[28.883,44.915,29.605,45.18],"l":[[28.883,45.18,29.075,45.1,29.147,45.084,29.175,45.089,29.205,45.08,29.276,45.007,29.348,44.97,29.42,44.97,29.495,44.95,29.605,44.915]]},{"n":"Bratul Sulina","z":2.1,"b":[28.883,45.152,29.679,45.18],"l":[[28.883,45.18,29.679,45.152]]},{"n":"Brazos","z":5,"b":[-100.501,28.898,-95.378,33.584],"l":[[-100.501,32.905,-100.441,32.957,-100.319,32.994,-100.115,33.034,-99.998,33.091,-99.968,33.165,-99.964,33.248,-99.987,33.34,-99.891,33.435,-99.679,33.532,-99.494,33.582,-99.337,33.584,-99.247,33.556,-99.225,33.497,-99.184,33.468,-99.126,33.466,-99.081,33.438,-99.05,33.383,-99.022,33.361,-98.996,33.373,-98.973,33.355,-98.953,33.307,-98.882,33.246,-98.7,33.136,-98.712,33.082,-98.693,33.039,-98.647,32.99,-98.608,32.987,-98.577,33.032,-98.519,33.033,-98.433,32.992,-98.406,32.952,-98.437,32.915,-98.435,32.879,-98.399,32.846,-98.34,32.837,-98.258,32.853,-98.201,32.836,-98.169,32.784,-98.176,32.752,-98.194,32.739,-98.186,32.719,-98.156,32.704,-97.973,32.649,-97.85,32.574,-97.788,32.479,-97.716,32.416,-97.636,32.384,-97.623,32.358,-97.676,32.337,-97.696,32.303,-97.682,32.258,-97.632,32.213,-97.546,32.168,-97.498,32.157,-97.474,32.168,-97.464,32.163,-97.462,32.152,-97.466,32.09,-97.417,32.003,-97.315,31.892,-97.263,31.809,-97.261,31.752,-97.193,31.663,-97.06,31.541,-96.971,31.421,-96.904,31.244,-96.654,30.791,-96.512,30.61,-96.394,30.551,-96.319,30.485,-96.288,30.412,-96.242,30.376,-96.181,30.38,-96.131,30.362,-96.093,30.325,-96.099,30.258,-96.149,30.164,-96.153,30.092,-96.11,30.042,-96.078,29.931,-96.058,29.758,-96.03,29.666,-95.979,29.647,-95.905,29.632,-95.758,29.601,-95.656,29.559,-95.599,29.505,-95.572,29.418,-95.573,29.298,-95.589,29.206,-95.619,29.141,-95.57,29.074,-95.44,29.004,-95.378,28.947,-95.388,28.898]]},{"n":"Busira","z":5,"b":[18.951,-0.294,20.754,-0.105],"l":[[20.754,-0.282,20.612,-0.294,20.503,-0.28,20.38,-0.242,20.297,-0.234,20.254,-0.254,20.221,-0.227,20.196,-0.154,20.156,-0.13,20.101,-0.155,20.028,-0.147,19.937,-0.105,19.864,-0.105,19.809,-0.148,19.713,-0.15,19.577,-0.112,19.411,-0.114,19.214,-0.155,19.099,-0.199,19.064,-0.247,19.023,-0.258,18.951,-0.219]]},{"n":"Bykovskaya Protoka","z":2,"b":[126.657,72.294,127.726,72.524],"l":[[126.69,72.294,126.657,72.423,126.674,72.485,126.729,72.524,126.984,72.515,127.666,72.43,127.726,72.413]]},{"n":"Bénoué","z":4,"b":[11.924,7.777,13.999,9.506],"l":[[13.524,7.777,13.674,7.855,13.717,7.922,13.705,8.006,13.736,8.071,13.85,8.138,13.96,8.24,13.998,8.312,13.999,8.388,13.977,8.453,13.93,8.506,13.927,8.597,13.966,8.726,13.925,8.833,13.801,8.917,13.681,9.054,13.565,9.244,13.475,9.314,13.412,9.264,13.319,9.226,13.197,9.199,13.084,9.223,12.927,9.335,12.821,9.375,12.717,9.351,12.581,9.277,12.456,9.307,12.342,9.443,12.195,9.506,11.924,9.491]]},{"n":"Bío-Bío","z":4.7,"b":[-73.133,-38.77,-71.127,-36.795],"l":[[-71.241,-38.77,-71.127,-38.691,-71.127,-38.602,-71.241,-38.413,-71.304,-38.353,-71.329,-38.28,-71.336,-38.172,-71.401,-38.081,-71.526,-38.007,-71.62,-37.92,-71.684,-37.82,-71.754,-37.754,-71.83,-37.721,-71.918,-37.714,-72.02,-37.732,-72.09,-37.719,-72.148,-37.652,-72.256,-37.593,-72.355,-37.568,-72.482,-37.557,-72.576,-37.531,-72.636,-37.489,-72.691,-37.406,-72.741,-37.281,-72.807,-37.205,-72.89,-37.178,-72.941,-37.115,-72.958,-37.016,-72.999,-36.927,-73.062,-36.846,-73.112,-36.803,-73.133,-36.795]]},{"n":"Caquetá","z":4.7,"b":[-76.536,-1.53,-69.462,1.33],"l":[[-76.533,1.33,-76.536,1.139,-76.508,1.057,-76.422,1.002,-76.263,0.98,-76.145,0.988,-76.015,1.019,-75.92,0.996,-75.859,0.918,-75.69,0.844,-75.413,0.772,-75.252,0.676,-75.209,0.556,-75.139,0.49,-75.044,0.476,-74.988,0.42,-74.971,0.321,-74.893,0.241,-74.753,0.181,-74.678,0.1,-74.669,-0.003,-74.638,-0.07,-74.585,-0.101,-74.527,-0.108,-74.462,-0.091,-74.404,-0.093,-74.353,-0.113,-74.306,-0.149,-74.262,-0.202,-74.166,-0.263,-74.016,-0.334,-73.885,-0.374,-73.771,-0.383,-73.693,-0.406,-73.629,-0.463,-73.517,-0.515,-73.438,-0.528,-73.356,-0.518,-73.288,-0.536,-73.233,-0.582,-73.154,-0.585,-73.05,-0.546,-72.969,-0.545,-72.913,-0.583,-72.851,-0.591,-72.784,-0.57,-72.704,-0.591,-72.611,-0.654,-72.529,-0.653,-72.458,-0.589,-72.398,-0.575,-72.349,-0.611,-72.286,-0.607,-72.209,-0.563,-72.139,-0.593,-72.076,-0.697,-71.99,-0.765,-71.883,-0.798,-71.803,-0.847,-71.75,-0.914,-71.677,-0.947,-71.584,-0.945,-71.543,-0.978,-71.557,-1.044,-71.55,-1.087,-71.523,-1.107,-71.477,-1.111,-71.379,-1.093,-71.227,-1.061,-71.13,-1.095,-71.036,-1.184,-70.945,-1.24,-70.858,-1.263,-70.778,-1.305,-70.706,-1.367,-70.623,-1.41,-70.529,-1.435,-70.44,-1.438,-70.356,-1.419,-70.256,-1.441,-70.141,-1.503,-70.067,-1.53,-70.033,-1.52,-69.975,-1.435,-69.893,-1.275,-69.801,-1.215,-69.7,-1.255,-69.6,-1.256,-69.524,-1.226,-69.462,-1.231]]},{"n":"Yangtze","z":2,"b":[103.851,28.621,113.128,31.061],"l":[[103.851,28.668,103.942,28.627,104.094,28.621,104.335,28.636,104.492,28.679,104.564,28.749,104.62,28.779,104.65,28.795,104.753,28.817,104.833,28.816,104.893,28.79,104.939,28.793,104.985,28.79,105.012,28.778,105.043,28.746,105.091,28.724,105.128,28.732,105.153,28.769,105.209,28.786,105.296,28.785,105.351,28.805,105.373,28.848,105.424,28.877,105.503,28.892,105.547,28.915,105.577,28.922,105.619,28.906,105.673,28.898,105.741,28.918,105.785,28.906,105.803,28.862,105.829,28.839,105.861,28.837,105.882,28.852,105.89,28.884,105.876,28.915,105.838,28.943,105.834,28.98,105.865,29.025,105.946,29.058,106.076,29.08,106.146,29.125,106.157,29.195,106.253,29.274,106.434,29.363,106.518,29.426,106.505,29.462,106.523,29.516,106.572,29.589,106.618,29.612,106.66,29.586,106.721,29.578,106.802,29.588,106.894,29.65,106.998,29.763,107.084,29.787,107.15,29.721,107.237,29.699,107.345,29.718,107.419,29.771,107.458,29.856,107.53,29.89,107.634,29.873,107.842,30.043,108.308,30.58,108.391,30.692,108.418,30.755,108.417,30.808,108.443,30.853,108.496,30.892,108.757,30.936,109.46,31.01,109.545,31.034,109.598,31.031,109.646,31.008,109.742,31.016,109.884,31.054,109.984,31.061,110.043,31.037,110.143,31.032,110.285,31.045,110.474,31.02,110.828,30.922,110.971,30.852,111.045,30.835,111.098,30.847,111.159,30.829,111.228,30.781,111.309,30.673,111.399,30.507,111.465,30.418,111.507,30.407,111.52,30.384,111.507,30.351,111.512,30.318,111.538,30.287,111.571,30.287,111.612,30.311,111.736,30.389,111.818,30.426,111.86,30.421,111.911,30.387,111.969,30.323,112.058,30.293,112.179,30.295,112.247,30.278,112.263,30.242,112.252,30.191,112.215,30.125,112.252,30.068,112.363,30.018,112.406,29.927,112.381,29.793,112.41,29.738,112.492,29.762,112.544,29.792,112.569,29.784,112.593,29.766,112.604,29.752,112.629,29.752,112.632,29.765,112.657,29.788,112.707,29.799,112.739,29.786,112.732,29.772,112.745,29.754,112.782,29.744,112.819,29.737,112.885,29.76,112.942,29.74,112.955,29.691,112.94,29.669,112.917,29.6,112.924,29.53,112.951,29.508,112.989,29.505,113.017,29.494,113.031,29.481,113.053,29.469,113.097,29.456,113.128,29.463]]},{"n":"Chari","z":5,"b":[14.176,8.816,18.886,12.98],"l":[[18.886,8.836,18.842,8.816,18.76,8.844,18.625,8.914,18.476,9.041,18.231,9.319,17.791,9.722,17.623,9.913,17.582,10.026,17.521,10.129,17.372,10.296,17.316,10.35,17.248,10.388,17.17,10.41,17.104,10.456,17.051,10.525,17.004,10.564,16.963,10.573,16.876,10.548,16.742,10.489,16.611,10.481,16.483,10.522,16.34,10.598,16.18,10.708,16.017,10.773,15.849,10.793,15.711,10.834,15.603,10.895,15.521,10.957,15.466,11.019,15.433,11.105,15.423,11.214,15.386,11.344,15.322,11.496,15.301,11.638,15.322,11.773,15.283,11.895,15.184,12.007,15.075,12.085],[15.075,12.085,14.95,12.043,14.843,12.073,14.692,12.144,14.584,12.219,14.519,12.298,14.415,12.344,14.273,12.357,14.197,12.384,14.176,12.471],[15.075,12.085,14.957,12.13,14.881,12.269,14.847,12.502,14.761,12.656,14.623,12.73,14.545,12.82,14.516,12.98]]},{"n":"Chattahoochee","z":5,"b":[-85.185,29.745,-83.636,34.716],"l":[[-84.886,30.721,-84.893,30.668,-84.961,30.574,-85.08,30.272,-85.117,30.17,-85.113,30.08,-85.043,29.995,-85.016,29.909,-85.033,29.822,-85.024,29.772,-84.969,29.745],[-85.067,31.635,-85.055,31.573,-85.093,31.296,-85.067,31.118,-84.966,30.923],[-84.076,34.167,-84.105,34.091,-84.153,34.042,-84.23,33.995,-84.292,33.98,-84.339,33.998,-84.43,33.926,-84.566,33.765,-84.729,33.622,-84.921,33.496,-85.037,33.396,-85.077,33.323,-85.089,33.252,-85.072,33.184,-85.096,33.107,-85.161,33.023,-85.185,32.937,-85.157,32.804,-85.015,32.526,-84.974,32.409,-84.986,32.361,-84.969,32.32,-84.922,32.286,-84.93,32.247,-84.993,32.202,-85.035,32.147,-85.065,32.051],[-83.746,34.716,-83.691,34.7,-83.661,34.659,-83.636,34.587,-83.672,34.504,-83.817,34.364]]},{"n":"Chattooga","z":5,"b":[-83.356,34.708,-83.121,35.076],"l":[[-83.121,35.076,-83.121,35.001,-83.17,34.933,-83.317,34.806,-83.356,34.708]]},{"n":"Chenab","z":5,"b":[70.612,29.023,77.436,33.371],"l":[[72.139,31.183,72.139,31.116,72.103,31.048,71.995,30.909,71.771,30.614,71.679,30.583,71.577,30.508,71.435,30.38,71.316,30.206,71.221,29.986,71.173,29.829,71.172,29.736,71.131,29.649,71.049,29.571,71.017,29.505,71.036,29.452,71.035,29.392,71.014,29.326,70.987,29.275,70.955,29.238,70.912,29.221,70.857,29.223,70.797,29.178,70.732,29.084,70.684,29.037,70.612,29.023],[77.436,32.293,77.134,32.457,76.94,32.593,76.767,32.671,76.552,32.736,76.437,32.842,76.423,32.989,76.338,33.122,76.182,33.243,76.02,33.323,75.854,33.363,75.759,33.371,75.737,33.347,75.746,33.305,75.788,33.246,75.802,33.202,75.788,33.171,75.677,33.152,75.468,33.144,75.308,33.174,75.198,33.241,75.122,33.246,75.08,33.191,74.999,33.17,74.879,33.186,74.809,33.123,74.789,32.982,74.583,32.789,74.188,32.542,73.844,32.371,73.548,32.275,73.335,32.146,73.139,31.902,72.974,31.758,72.854,31.699,72.722,31.677,72.582,31.622,72.435,31.533,72.334,31.457,72.279,31.395,72.247,31.328,72.238,31.256,72.21,31.211,72.139,31.183]]},{"n":"Chixoy","z":4.7,"b":[-91.303,15.017,-90.417,16.391],"l":[[-91.278,15.017,-91.264,15.157,-91.303,15.237,-91.296,15.27,-91.257,15.285,-91.047,15.308,-90.881,15.288,-90.677,15.236,-90.555,15.225,-90.492,15.286,-90.483,15.316,-90.534,15.347,-90.647,15.378,-90.676,15.427,-90.624,15.493,-90.611,15.558,-90.64,15.622,-90.686,15.66,-90.749,15.673,-90.79,15.697,-90.809,15.733,-90.789,15.817,-90.732,15.951,-90.637,16.019,-90.506,16.023,-90.447,16.071,-90.46,16.162,-90.45,16.261,-90.417,16.351,-90.417,16.391]]},{"n":"Chubut","z":5,"b":[-71.22,-43.846,-65.047,-42.617],"l":[[-71.22,-43.582,-71.092,-43.608,-71.013,-43.562,-70.906,-43.396],[-70.899,-43.393,-70.811,-43.096,-70.744,-42.982,-70.669,-42.953,-70.626,-42.904,-70.615,-42.835,-70.579,-42.766,-70.518,-42.698,-70.461,-42.651,-70.408,-42.625,-70.244,-42.617,-69.97,-42.628,-69.784,-42.643,-69.686,-42.664,-69.508,-42.821,-69.252,-43.114,-69.134,-43.305,-69.155,-43.391,-69.153,-43.447,-69.117,-43.486,-68.976,-43.582,-68.901,-43.668,-68.844,-43.774,-68.765,-43.818,-68.665,-43.798,-68.547,-43.806,-68.41,-43.842,-68.102,-43.824,-67.358,-43.714,-67.308,-43.714,-67.264,-43.742,-67.225,-43.797,-67.16,-43.832,-67.068,-43.846,-66.879,-43.838,-66.523,-43.8,-66.493,-43.795,-66.458,-43.768,-66.457,-43.745,-66.465,-43.706,-66.444,-43.677,-66.356,-43.619,-66.277,-43.577,-66.107,-43.521,-65.982,-43.459,-65.902,-43.392,-65.793,-43.353,-65.656,-43.344,-65.551,-43.322,-65.479,-43.288,-65.344,-43.286,-65.047,-43.333]]},{"n":"Chulyshman","z":5,"b":[87.735,50.505,89.656,51.364],"l":[[89.656,50.505,89.403,50.598,89.194,50.622,88.946,50.608,88.76,50.656,88.636,50.764,88.512,50.831,88.387,50.857,88.22,50.929,88.01,51.048,87.861,51.151,87.772,51.238,87.735,51.302,87.756,51.364]]},{"n":"Churchill","z":5,"b":[-64.682,52.886,-60.337,53.546],"l":[[-64.622,52.891,-64.682,53.125],[-64.678,53.382,-64.577,53.49,-64.449,53.532,-64.26,53.546,-64.067,53.538,-63.867,53.508,-63.638,53.425,-63.378,53.291,-63.034,53.21,-62.606,53.181,-62.352,53.145,-62.272,53.103,-62.214,53.042,-62.179,52.963,-62.036,52.911,-61.785,52.886,-61.65,52.906,-61.631,52.968,-61.578,52.996,-61.448,52.986,-60.981,53.198,-60.724,53.268,-60.558,53.25,-60.337,53.277]]},{"n":"Coco","z":5,"b":[-86.844,13.42,-83.158,15.008],"l":[[-86.844,13.42,-86.639,13.513,-86.492,13.61,-86.411,13.636,-86.346,13.622,-86.282,13.572,-86.217,13.487,-86.148,13.461,-86.075,13.493,-86.004,13.505,-85.935,13.496,-85.856,13.581,-85.769,13.759,-85.728,13.876,-85.731,13.932,-85.682,13.983,-85.58,14.028,-85.477,14.109,-85.374,14.224,-85.284,14.292,-85.208,14.312,-85.179,14.343,-85.198,14.386,-85.192,14.447,-85.161,14.525,-85.117,14.571,-85.059,14.583,-85.037,14.608,-85.049,14.645,-85.037,14.686,-84.985,14.752,-84.86,14.81,-84.789,14.79,-84.73,14.713,-84.646,14.661,-84.538,14.633,-84.454,14.644,-84.394,14.692,-84.34,14.706,-84.292,14.687,-84.267,14.698,-84.264,14.739,-84.239,14.748,-84.192,14.726,-84.151,14.72,-84.114,14.731,-84.1,14.751,-84.093,14.771,-84.066,14.786,-83.972,14.771,-83.867,14.794,-83.751,14.856,-83.674,14.884,-83.635,14.876,-83.59,14.908,-83.537,14.977,-83.415,15.008,-83.158,14.993]]},{"n":"Colorado","z":4.7,"b":[-115.199,-39.88,-62.254,40.404],"l":[[-64.97,-38.804,-64.596,-38.841,-64.39,-38.878,-64.228,-38.925,-63.989,-39.038,-63.674,-39.217,-63.456,-39.316,-63.336,-39.335,-63.214,-39.38,-63.091,-39.451,-62.918,-39.499,-62.697,-39.524,-62.517,-39.608,-62.309,-39.822,-62.279,-39.852,-62.254,-39.88],[-70.302,-35.288,-70.223,-35.591,-70.166,-35.722,-70.072,-35.813,-69.865,-35.86,-69.762,-35.964,-69.694,-36.14,-69.671,-36.26,-69.692,-36.324,-69.693,-36.407,-69.676,-36.508,-69.694,-36.596,-69.748,-36.67,-69.785,-36.78,-69.806,-36.925,-69.797,-37.02,-69.758,-37.064,-69.699,-37.106,-69.621,-37.146,-69.463,-37.174,-69.226,-37.19,-69.076,-37.24,-69.016,-37.323,-68.87,-37.383,-68.641,-37.422,-68.501,-37.465,-68.452,-37.514,-68.273,-37.558,-67.963,-37.598,-67.783,-37.659,-67.735,-37.74,-67.751,-37.82,-67.83,-37.901,-67.866,-37.969,-67.858,-38.022,-67.824,-38.056,-67.765,-38.069,-67.69,-38.122,-67.599,-38.215,-67.46,-38.252,-67.271,-38.232,-67.15,-38.269,-67.097,-38.362,-66.993,-38.436,-66.763,-38.516,-66.653,-38.564,-66.603,-38.61,-66.576,-38.671,-66.52,-38.71,-66.435,-38.726,-66.068,-38.74,-65.893,-38.761,-65.757,-38.799,-65.658,-38.807,-65.595,-38.786,-65.514,-38.791,-65.416,-38.823,-65.27,-38.83,-64.978,-38.804],[-114.721,32.724,-114.788,32.565,-114.839,32.504,-114.897,32.49,-114.959,32.42,-115.025,32.294,-115.093,32.213,-115.164,32.176,-115.199,32.124,-115.196,32.058,-115.187,32.044,-115.173,32.045,-115.153,32.036,-115.126,31.988,-115.093,31.965,-115.035,31.966,-115.009,31.946,-114.981,31.918,-114.934,31.901],[-114.164,34.253,-114.329,34.142,-114.418,34.051,-114.509,33.904,-114.495,33.785,-114.499,33.686,-114.522,33.608,-114.556,33.536,-114.603,33.47,-114.651,33.431,-114.702,33.418,-114.719,33.331,-114.704,33.17,-114.648,33.072,-114.551,33.037,-114.491,32.984,-114.469,32.912,-114.478,32.842,-114.516,32.773,-114.582,32.735,-114.721,32.724],[-114.591,35.353,-114.611,34.998,-114.611,34.907,-114.557,34.795,-114.398,34.59,-114.379,34.541],[-114.741,36.014,-114.732,35.984,-114.687,35.917,-114.684,35.814,-114.677,35.73,-114.65,35.684,-114.645,35.631],[-112.876,36.28,-113.206,36.124,-113.296,36.048,-113.325,35.995,-113.323,35.835,-113.356,35.764,-113.422,35.762,-113.579,35.801,-113.636,35.831],[-111.473,36.932,-111.591,36.85,-111.683,36.748,-111.788,36.599,-111.852,36.488,-111.874,36.414,-111.865,36.32,-111.826,36.206,-111.834,36.126,-111.891,36.081,-111.995,36.073,-112.147,36.102,-112.256,36.151,-112.322,36.221,-112.377,36.246,-112.42,36.226,-112.459,36.229,-112.495,36.254,-112.499,36.288,-112.471,36.332,-112.467,36.366,-112.489,36.389,-112.533,36.401,-112.598,36.402,-112.693,36.372,-112.881,36.28],[-105.861,40.165,-105.985,40.127,-106.126,40.093,-106.285,40.091,-106.401,40.062,-106.482,40.019,-106.595,39.923,-106.638,39.894,-106.676,39.89,-106.726,39.92,-106.793,39.924,-106.877,39.902,-106.969,39.828,-107.071,39.701,-107.132,39.644,-107.235,39.602,-107.541,39.579,-107.734,39.579,-107.847,39.557,-107.944,39.512,-108.106,39.41,-108.242,39.315,-108.314,39.178,-108.434,39.099,-108.558,39.09,-108.801,39.184,-108.867,39.201,-108.952,39.198,-109.051,39.141,-109.196,39.033,-109.265,38.935,-109.306,38.812,-109.363,38.734,-109.434,38.702,-109.499,38.651,-109.556,38.581,-109.596,38.554,-109.614,38.561,-109.635,38.546,-109.648,38.485,-109.67,38.453,-109.7,38.449,-109.723,38.414,-109.739,38.35,-109.785,38.279,-109.897,38.165,-110.131,37.962,-110.248,37.895,-110.368,37.898],[-105.846,40.404,-105.816,40.215,-105.821,40.185]]},{"n":"Columbia","z":3,"b":[-123.221,45.591,-115.865,52.102],"l":[[-122.15,45.594,-122.203,45.591,-122.561,45.625,-122.65,45.627,-122.726,45.674,-122.746,45.75,-122.843,45.978,-122.942,46.115,-123.042,46.16,-123.121,46.179,-123.221,46.154],[-118.168,48.232,-118.232,48.086,-118.284,48.035,-118.346,48.032,-118.368,47.996,-118.349,47.927,-118.351,47.879,-118.375,47.849,-118.433,47.857,-118.525,47.903,-118.665,47.927,-118.854,47.93,-118.983,47.982,-119.052,48.082,-119.228,48.101,-119.511,48.04,-119.681,48.03,-119.74,48.073,-119.801,48.085,-119.864,48.066,-119.887,48.029,-119.868,47.975,-119.87,47.929,-119.894,47.891,-119.969,47.813,-120.04,47.783,-120.14,47.763,-120.214,47.693,-120.262,47.575,-120.273,47.487,-120.248,47.429,-120.194,47.382,-120.068,47.329,-120.068,47.275,-120.049,47.245,-120.01,47.219,-119.997,47.176,-120.011,47.117,-119.992,46.985,-119.939,46.78,-119.838,46.674,-119.687,46.667,-119.577,46.681,-119.506,46.716,-119.417,46.679,-119.311,46.569,-119.256,46.456,-119.253,46.341,-119.201,46.269,-119.108,46.237,-119.085,46.233,-119.057,46.22,-119.036,46.206,-118.968,46.132,-118.948,46.11,-118.932,46.095,-118.925,46.08,-118.931,46.063,-118.959,46.038,-118.99,46.019,-119.05,45.981,-119.177,45.945,-119.429,45.934,-119.934,45.838,-120.156,45.762,-120.351,45.725,-120.716,45.69,-121.045,45.639,-121.184,45.644,-121.243,45.687,-121.402,45.712,-121.663,45.718,-121.882,45.689,-122.085,45.603,-122.15,45.594],[-117.907,49.342,-117.721,49.331,-117.676,49.275,-117.711,49.172,-117.701,49.098,-117.648,49.055,-117.682,49.015,-117.717,48.975,-117.908,48.858,-118.021,48.766,-118.074,48.663],[-117.88,50.103,-117.917,50.092,-117.925,50.064,-117.915,50.016,-117.95,49.971,-118.029,49.929,-118.074,49.881,-118.092,49.802],[-115.865,50.287,-116.081,50.591,-116.224,50.758,-116.365,50.89,-116.545,51.01,-116.762,51.12,-116.931,51.244,-117.052,51.383,-117.183,51.471,-117.324,51.507,-117.425,51.558,-117.517,51.659],[-117.873,51.87,-118.366,52.09],[-118.527,52.102,-118.581,52.052,-118.595,51.999,-118.589,51.925,-118.611,51.858,-118.661,51.796,-118.681,51.742,-118.672,51.696,-118.599,51.59,-118.463,51.426,-118.359,51.323,-118.289,51.281,-118.237,51.214,-118.205,51.12,-118.195,51.049,-118.208,51,-118.138,50.903,-117.906,50.683]]},{"n":"Congo","z":2.1,"b":[13.068,-8.272,27.049,2.178],"l":[[26.441,-8.272,26.654,-8.223,26.742,-8.193,26.776,-8.165,26.794,-8.124,26.794,-8.069,26.838,-8.011,26.928,-7.949,26.973,-7.9,26.973,-7.864,26.929,-7.804,26.839,-7.72,26.865,-7.529,27.009,-7.229,27.049,-6.991,26.984,-6.815,26.961,-6.664,26.98,-6.537,26.97,-6.422,26.931,-6.319,26.906,-6.173,26.895,-5.982,26.9,-5.818,26.93,-5.61],[24.292,0.782,24.178,0.924,24.005,1.022,23.734,1.123,23.461,1.342,23.184,1.679,22.968,1.873,22.735,1.95,22.557,2.101,22.396,2.161,22.194,2.178,21.969,2.178,21.721,2.158,21.499,2.113,21.304,2.041,20.944,1.995,20.418,1.972,19.902,1.864,19.143,1.574,18.714,1.223,18.526,1.052,18.436,0.944,18.343,0.692,18.199,0.1,18.184,0.063,18.109,-0.124,17.9,-0.391,17.753,-0.549,17.543,-0.775,17.279,-1,17.108,-1.064,16.975,-1.14,16.88,-1.226,16.849,-1.272,16.78,-1.376,16.622,-1.699,16.541,-1.84,16.434,-1.961,16.274,-2.108,16.215,-2.178,16.192,-2.279,16.202,-2.465,16.217,-3.03,16.191,-3.194,16.147,-3.464,15.99,-3.766,15.872,-3.934,15.755,-3.986,15.6,-4.031,15.526,-4.088,15.481,-4.172,15.395,-4.245,15.267,-4.308,15.106,-4.461,14.912,-4.706,14.779,-4.846,14.708,-4.882,14.634,-4.885,14.558,-4.856,14.494,-4.852,14.418,-4.883,14.234,-4.912,14.133,-4.945,14.054,-4.991,14.005,-5.052,13.985,-5.129,13.938,-5.205,13.863,-5.278,13.832,-5.301,13.809,-5.303,13.777,-5.279,13.752,-5.279,13.735,-5.303,13.692,-5.31,13.623,-5.3,13.574,-5.316,13.543,-5.359,13.55,-5.414,13.595,-5.482,13.591,-5.513,13.565,-5.531,13.553,-5.576,13.564,-5.655,13.512,-5.743,13.399,-5.841,13.303,-5.882,13.184,-5.856,13.068,-5.865]]},{"n":"Copper","z":5,"b":[-145.419,60.669,-143.658,62.699],"l":[[-143.658,62.474,-143.675,62.557,-143.713,62.605,-143.776,62.646,-143.907,62.674,-144.203,62.699,-144.458,62.663,-144.733,62.568,-145.114,62.403,-145.336,62.286,-145.399,62.219,-145.419,62.157,-145.384,62.073,-145.166,61.916,-144.936,61.81,-144.621,61.701,-144.455,61.617,-144.427,61.526,-144.486,61.453,-144.58,61.407,-144.729,61.366,-144.828,61.309,-144.877,61.238,-144.873,61.151,-144.815,61.049,-144.716,60.951,-144.574,60.859,-144.534,60.833,-144.523,60.815,-144.532,60.788,-144.616,60.714,-144.638,60.69,-144.691,60.669]]},{"n":"Coppermine","z":5,"b":[-116.326,64.552,-111.212,67.819],"l":[[-111.212,64.583,-111.351,64.552,-111.456,64.562,-111.572,64.604,-111.715,64.628,-111.966,64.638],[-112.318,64.96,-112.343,64.899],[-114.173,65.638,-114.168,65.684,-114.231,65.728,-114.36,65.787,-114.43,65.864,-114.439,65.958,-114.403,66.035,-114.321,66.096,-114.374,66.233,-114.56,66.444,-114.751,66.586,-114.948,66.658,-115.265,66.728,-115.921,66.831,-116.157,66.81,-116.266,66.832,-116.326,66.89,-116.293,66.972,-116.166,67.08,-116.134,67.156,-116.196,67.201,-116.074,67.224,-115.768,67.226,-115.6,67.236,-115.571,67.253,-115.595,67.276,-115.67,67.303,-115.713,67.341,-115.724,67.389,-115.694,67.44,-115.625,67.495,-115.593,67.536,-115.598,67.563,-115.553,67.615,-115.458,67.691,-115.335,67.747,-115.133,67.819]]},{"n":"Cubango","z":5,"b":[16.145,-18.06,21.503,-12.634],"l":[[16.145,-12.654,16.192,-12.634,16.234,-12.66,16.288,-12.725,16.328,-12.821,16.365,-13.01,16.431,-13.436,16.414,-13.711,16.335,-13.977,16.288,-14.196,16.272,-14.368,16.294,-14.493,16.354,-14.573,16.446,-14.641,16.568,-14.699,16.646,-14.821,16.679,-15.005,16.75,-15.166,16.859,-15.305,16.944,-15.395,17.005,-15.436,17.12,-15.474,17.288,-15.508,17.379,-15.555,17.393,-15.615,17.462,-15.714,17.586,-15.853,17.664,-16.019,17.696,-16.212,17.767,-16.418,17.876,-16.637,18.095,-16.953,18.588,-17.57,18.718,-17.703,18.826,-17.766,18.955,-17.804,19.076,-17.818,19.189,-17.808,19.377,-17.825,19.639,-17.869,19.912,-17.881,20.194,-17.864,20.393,-17.887,20.508,-17.953,20.625,-17.997,20.746,-18.02,20.908,-18.006,21.113,-17.956,21.288,-17.963,21.503,-18.06]]},{"n":"Culuene","z":4.7,"b":[-54.609,-14.984,-52.877,-11.444],"l":[[-54.609,-14.984,-54.357,-14.897,-54.251,-14.837,-54.207,-14.775,-54.136,-14.758,-54.04,-14.786,-53.967,-14.79,-53.894,-14.761,-53.659,-14.592,-53.512,-14.346,-53.374,-13.968,-53.269,-13.736,-53.197,-13.651,-53.16,-13.583,-53.159,-13.532,-53.08,-13.344,-52.922,-13.019,-52.877,-12.807,-52.945,-12.71,-52.97,-12.644,-52.953,-12.611,-53.004,-12.517,-53.125,-12.361,-53.213,-12.268,-53.27,-12.235,-53.329,-12.161,-53.391,-12.047,-53.459,-11.966,-53.532,-11.918,-53.579,-11.846,-53.599,-11.75,-53.567,-11.637,-53.444,-11.444]]},{"n":"Dadu","z":5,"b":[100.07,29.236,103.719,33.006],"l":[[100.07,33.006,100.165,32.896,100.24,32.839,100.371,32.779,100.509,32.627,100.616,32.546,100.739,32.486,100.801,32.427,100.802,32.37,100.842,32.326,100.921,32.295,100.97,32.212,100.999,32.009,101.214,31.837,101.422,31.8,101.694,31.841,101.879,31.823,101.977,31.748,102.039,31.646,102.064,31.518,102.055,31.432,102.011,31.389,102.004,31.35,102.034,31.316,102.01,31.258,101.932,31.176,101.887,31.105,101.876,30.985,101.882,30.925,101.928,30.846,102.013,30.748,102.058,30.655,102.065,30.568,102.102,30.446,102.169,30.291,102.195,30.175,102.179,30.097,102.185,30.032,102.214,29.979,102.213,29.84,102.182,29.614,102.206,29.439,102.285,29.316,102.397,29.278,102.616,29.35,102.76,29.313,102.818,29.279,102.839,29.237,102.877,29.237,102.931,29.281,102.979,29.288,103.022,29.258,103.061,29.258,103.096,29.288,103.151,29.289,103.225,29.26,103.278,29.261,103.309,29.293,103.359,29.311,103.427,29.316,103.506,29.292,103.594,29.237,103.644,29.236,103.655,29.289,103.639,29.321,103.596,29.333,103.569,29.36,103.559,29.4,103.595,29.455,103.642,29.495,103.677,29.525,103.719,29.537]]},{"n":"Daly","z":5,"b":[130.26,-14.706,131.844,-13.302],"l":[[131.844,-14.706,131.75,-14.669,131.706,-14.601,131.681,-14.491,131.566,-14.345,131.257,-14.073,131.2,-13.981,131.186,-13.927,131.195,-13.879,131.174,-13.849,131.123,-13.836,131.037,-13.855,130.916,-13.904,130.827,-13.914,130.771,-13.883,130.721,-13.819,130.678,-13.721,130.637,-13.671,130.598,-13.669,130.546,-13.618,130.482,-13.516,130.409,-13.456,130.327,-13.438,130.305,-13.401,130.324,-13.353,130.325,-13.33,130.293,-13.309,130.26,-13.302]]},{"n":"Dalälven","z":5,"b":[12.095,60.122,17.457,62.157],"l":[[16.433,60.156,16.499,60.163],[17.417,60.51,17.457,60.642],[14.973,60.728,15.047,60.711,15.057,60.681,15.029,60.632,15.045,60.591,15.107,60.557,15.206,60.54,15.343,60.541,15.478,60.505,15.611,60.432,15.754,60.401,15.904,60.411,16.006,60.39,16.058,60.336,16.073,60.28,16.052,60.22,16.112,60.173,16.323,60.122],[13.718,61.413,13.724,61.334,13.74,61.3,13.768,61.284,13.949,61.268,14.034,61.237,14.094,61.183,14.231,61.116,14.552,60.997],[12.095,62.15,12.212,62.157,12.291,62.12,12.41,61.999,13.212,61.663]]},{"n":"Damietta Branch","z":2,"b":[31.078,30.151,31.839,31.526],"l":[[31.163,30.151,31.078,30.325,31.107,30.419,31.21,30.508,31.25,30.623,31.226,30.766,31.224,30.869,31.245,30.932,31.31,31.019,31.42,31.128,31.509,31.186,31.579,31.192,31.671,31.278,31.839,31.526]]},{"n":"Danube","z":2.1,"b":[17.206,43.671,28.761,48.061],"l":[[17.206,48.061,17.481,47.888,17.635,47.81,17.762,47.77,18.146,47.763,18.754,47.788,18.795,47.809,19,47.819,19.071,47.807,19.103,47.779,19.123,47.731,19.124,47.709,19.073,47.552,19.06,47.481,19.03,47.413,18.959,47.365,18.907,47.26,18.884,47.151,18.928,47.083,18.935,47.033,18.94,47,18.936,46.885,18.945,46.804,18.983,46.74,18.966,46.685,18.894,46.638,18.874,46.586,18.905,46.528,18.919,46.423,18.916,46.271,18.859,46.151,18.748,46.063,18.727,46.014,18.759,45.935,18.839,45.836,18.894,45.767,18.947,45.656,18.918,45.601,18.954,45.558,19.055,45.527,19.064,45.515,19.033,45.502,19.008,45.466,19.005,45.4,19.093,45.337,19.273,45.278,19.475,45.243,19.699,45.233,19.828,45.241,19.863,45.269,19.907,45.259,19.959,45.213,20.045,45.186,20.163,45.179,20.253,45.15,20.282,45.126,20.315,45.099,20.339,45.038,20.326,44.969,20.353,44.911,20.421,44.864,20.5,44.843,20.591,44.847,20.636,44.828,20.634,44.786,20.666,44.741,20.731,44.693,20.913,44.708,21.36,44.827,21.523,44.79,21.597,44.755,21.636,44.71,21.74,44.681,21.909,44.666,22.027,44.62,22.093,44.542,22.201,44.561,22.351,44.676,22.498,44.706,22.642,44.651,22.721,44.606,22.734,44.57,22.701,44.556,22.62,44.562,22.554,44.54,22.502,44.49,22.495,44.435,22.531,44.378,22.582,44.338,22.648,44.316,22.683,44.286,22.688,44.248,22.775,44.195,22.945,44.127,23.028,44.078,23.024,44.047,22.985,44.017,22.911,43.987,22.868,43.948,22.856,43.899,22.868,43.865,22.919,43.834,23.225,43.874,23.535,43.854,23.951,43.787,24.227,43.763,24.431,43.794,25.16,43.686,25.497,43.671,25.686,43.712,25.819,43.767,25.933,43.871,26.216,44.007,26.489,44.084,26.848,44.146,27.087,44.167,27.222,44.143,27.267,44.138,27.338,44.133,27.412,44.138,27.594,44.179,27.86,44.263,28.022,44.343,28.08,44.419,28.083,44.476,28.032,44.513,28.013,44.558,28.025,44.611,27.992,44.664,27.907,44.719,27.877,44.744,27.85,44.768,27.83,44.796,27.831,44.83,27.863,44.865,27.895,44.968,27.926,45.139,27.965,45.269,28.011,45.358,28.079,45.414,28.212,45.45,28.318,45.347,28.451,45.292,28.761,45.234]]},{"n":"Darling","z":3,"b":[141.918,-34.117,147.415,-29.928],"l":[[147.415,-30.125,147.262,-30.122,147.201,-30.107,147.182,-30.078,147.048,-30.027,146.799,-29.953,146.656,-29.928,146.616,-29.95,146.46,-29.962,146.188,-29.963,146.043,-29.983,146.026,-30.023,145.909,-30.103,145.692,-30.224,145.565,-30.316,145.528,-30.378,145.467,-30.409,145.338,-30.409,145.272,-30.464,145.219,-30.481,145.157,-30.479,145.11,-30.51,145.075,-30.575,145.008,-30.626,144.906,-30.662,144.813,-30.733,144.728,-30.838,144.622,-30.9,144.494,-30.921,144.411,-30.965,144.372,-31.033,144.27,-31.102,144.107,-31.173,144.022,-31.25,144.017,-31.333,143.904,-31.403,143.683,-31.458,143.548,-31.514,143.5,-31.57,143.454,-31.59,143.411,-31.575,143.373,-31.609,143.341,-31.692,143.298,-31.737,143.245,-31.745,143.197,-31.779,143.156,-31.839,143.104,-31.881,143.04,-31.903,143.005,-31.956,142.999,-32.039,142.978,-32.09,142.926,-32.116,142.538,-32.333,142.404,-32.505,142.388,-32.804,142.397,-32.951,142.375,-33.048,142.326,-33.143,142.368,-33.239,142.501,-33.335,142.57,-33.4,142.575,-33.435,142.535,-33.512,142.449,-33.632,142.312,-33.723,142.124,-33.787,142.002,-33.867,141.944,-33.964,141.918,-34.038,141.926,-34.117]]},{"n":"Zapadnaya Dvina","z":4.7,"b":[24.054,55.069,32.318,57.066],"l":[[32.078,56.908,32.062,56.877,32.097,56.861,32.177,56.85,32.242,56.821,32.318,56.75,32.273,56.609,32.233,56.552,32.184,56.531,32.135,56.478,32.087,56.395,32.073,56.315,32.092,56.24,32.068,56.194,32.001,56.177,31.974,56.144,31.986,56.095,31.875,56.069,31.641,56.066,31.479,56.039,31.39,55.989,31.379,55.889,31.449,55.738,31.415,55.655,31.278,55.639,31.171,55.601,31.056,55.509,30.643,55.436,30.409,55.369,30.214,55.285,30.167,55.235,30.148,55.195,30.087,55.183,29.925,55.204,29.815,55.205,29.757,55.187,29.726,55.161,29.722,55.129,29.687,55.116,29.62,55.122,29.555,55.107,29.491,55.069,29.452,55.071,29.438,55.112,29.406,55.132,29.355,55.133,29.316,55.155,29.288,55.2,29.202,55.258,29.057,55.329,29.001,55.38,28.993,55.409,28.919,55.455,28.724,55.524,28.504,55.566,28.26,55.582,28.087,55.62,27.986,55.681,27.928,55.74,27.914,55.796,27.881,55.817,27.855,55.819,27.764,55.808,27.604,55.798,27.459,55.803,27.349,55.831,27.015,55.899,26.809,55.907,26.621,55.88,26.489,55.899,26.414,55.965,26.364,56.039,26.34,56.121,26.294,56.178,26.227,56.21,26.184,56.264,26.166,56.342,26.064,56.418,25.879,56.493,25.779,56.553,25.762,56.598,25.71,56.609,25.623,56.587,25.534,56.59,25.444,56.617,25.358,56.619,25.276,56.597,25.174,56.594,25.053,56.612,24.891,56.668,24.689,56.764,24.505,56.827,24.341,56.858,24.22,56.894,24.144,56.936,24.091,56.985,24.054,57.066]]},{"n":"Desaguadero","z":5,"b":[-67.362,-38.804,-64.97,-32.397],"l":[[-67.362,-32.397,-67.235,-32.67,-67.201,-32.8,-67.218,-32.877,-67.208,-33.044,-67.171,-33.301,-67.051,-33.588,-66.848,-33.905,-66.75,-34.094,-66.756,-34.154,-66.775,-34.196,-66.806,-34.218,-66.819,-34.268,-66.815,-34.346,-66.794,-34.442,-66.757,-34.558,-66.697,-34.675,-66.614,-34.794,-66.555,-34.921,-66.522,-35.056,-66.534,-35.346,-66.592,-35.79,-66.679,-36.078,-66.793,-36.209,-66.833,-36.395,-66.797,-36.635,-66.795,-36.8,-66.828,-36.889,-66.828,-36.99,-66.794,-37.103,-66.796,-37.173,-66.832,-37.201,-66.784,-37.251,-66.651,-37.325,-66.542,-37.407,-66.458,-37.498,-66.371,-37.544,-66.279,-37.547,-66.211,-37.606,-66.168,-37.722,-66.067,-37.799,-65.91,-37.838,-65.822,-37.915,-65.802,-38.029,-65.818,-38.097,-65.87,-38.117,-65.912,-38.161,-65.946,-38.228,-65.912,-38.282,-65.81,-38.324,-65.743,-38.37,-65.712,-38.422,-65.63,-38.489,-65.494,-38.571,-65.313,-38.66,-64.97,-38.804]]},{"n":"Dicle","z":4,"b":[39.643,37.062,42.377,38.416],"l":[[39.643,38.416,40.018,38.348,40.024,38.306,40.068,38.269,40.15,38.223,40.178,38.179,40.153,38.136,40.171,38.106,40.235,38.091,40.262,38.023,40.252,37.903,40.427,37.832,40.969,37.8,41.155,37.757,41.358,37.744,41.64,37.746,41.8,37.709,41.84,37.632,41.854,37.556,41.841,37.481,41.882,37.441,41.979,37.435,42.094,37.395,42.226,37.322,42.313,37.23,42.377,37.062]]},{"n":"Digul","z":5,"b":[139.177,-7.252,140.742,-4.856],"l":[[140.683,-4.856,140.731,-4.915,140.742,-4.967,140.734,-5.03,140.697,-5.109,140.597,-5.253,140.319,-5.536,140.223,-5.673,140.217,-5.758,140.239,-5.828,140.289,-5.883,140.309,-5.961,140.295,-6.116,140.264,-6.153,140.266,-6.216,140.289,-6.319,140.314,-6.375,140.338,-6.382,140.346,-6.427,140.337,-6.51,140.349,-6.573,140.382,-6.616,140.372,-6.675,140.321,-6.75,140.265,-6.802,140.205,-6.831,140.154,-6.879,140.111,-6.947,140.09,-7.009,140.092,-7.066,140.062,-7.114,139.999,-7.153,139.918,-7.146,139.85,-7.105,139.798,-7.105,139.74,-7.138,139.707,-7.204,139.652,-7.222,139.626,-7.193,139.589,-7.2,139.558,-7.231,139.521,-7.252,139.489,-7.244,139.468,-7.215,139.458,-7.164,139.421,-7.16,139.388,-7.185,139.355,-7.204,139.237,-7.177,139.177,-7.19]]},{"n":"Dihang","z":2.1,"b":[94.356,28.003,95.418,29.876],"l":[[94.356,29.31,94.526,29.439,94.641,29.484,94.759,29.489,94.844,29.521,94.897,29.58,94.919,29.636,94.909,29.689,94.959,29.728,95.069,29.752,95.125,29.793,95.128,29.851,95.171,29.876,95.255,29.867,95.294,29.85,95.29,29.824,95.312,29.801,95.36,29.781,95.395,29.691,95.418,29.531,95.385,29.41,95.298,29.329,95.191,29.258,95.064,29.198,94.944,29.09,94.83,28.933,94.809,28.846,94.88,28.829,94.961,28.751,95.052,28.61,95.097,28.506,95.094,28.439,95.068,28.376,95.018,28.316,95.003,28.258,95.022,28.202,95.088,28.172,95.199,28.169,95.291,28.127,95.396,28.003]]},{"n":"Dnepre","z":4,"b":[33.391,54.917,33.891,55.874],"l":[[33.735,55.874,33.818,55.827,33.826,55.802,33.788,55.784,33.803,55.764,33.872,55.742,33.891,55.713,33.845,55.657,33.768,55.57,33.702,55.546,33.622,55.556,33.585,55.498,33.59,55.372,33.57,55.287,33.524,55.243,33.473,55.145,33.391,54.917]]},{"n":"Dniester","z":5,"b":[22.847,46.09,30.493,49.565],"l":[[30.478,46.101,30.493,46.09],[22.847,49.267,23.082,49.461,23.388,49.554,23.607,49.565,23.841,49.524,24.019,49.481,24.139,49.436,24.227,49.372,24.281,49.29,24.36,49.245,24.466,49.237,24.594,49.182,24.745,49.08,24.851,49.028,24.912,49.026,24.987,48.998,25.122,48.918,25.155,48.878,25.185,48.882,25.211,48.885,25.23,48.864,25.251,48.842,25.302,48.842,25.339,48.832,25.398,48.845,25.521,48.809,25.668,48.689,25.731,48.659,25.781,48.66,25.827,48.649,25.894,48.608,25.958,48.602,26.005,48.614,26.037,48.617,26.072,48.572,26.093,48.563,26.13,48.572,26.169,48.547,26.253,48.529,26.398,48.535,26.498,48.517,26.555,48.474,26.591,48.466,26.607,48.493,26.639,48.504,26.668,48.517,26.694,48.539,26.72,48.55,26.759,48.562,26.784,48.565,26.804,48.579,26.835,48.583,26.884,48.558,26.997,48.56,27.272,48.601,27.433,48.584,27.48,48.51,27.574,48.465,27.714,48.449,27.82,48.416,27.891,48.365,27.963,48.334,28.038,48.321,28.08,48.296,28.088,48.257,28.159,48.238,28.291,48.239,28.347,48.213,28.327,48.161,28.341,48.144,28.388,48.162,28.423,48.147,28.442,48.109,28.463,48.091,28.496,48.096,28.539,48.104,28.574,48.069,28.572,48.028,28.604,48.009,28.653,48.017,28.766,47.987,28.934,47.898,29.01,47.796,28.994,47.68,29.017,47.583,29.079,47.504,29.103,47.45,29.089,47.421,29.103,47.392,29.12,47.369,29.116,47.341,29.104,47.31,29.154,47.235,29.145,47.181,29.132,47.153,29.137,47.136,29.172,47.136,29.215,47.143,29.265,47.146,29.314,47.116,29.359,47.066,29.36,47.029,29.317,47.005,29.3,46.984,29.311,46.967,29.424,46.955,29.472,46.912,29.499,46.829,29.537,46.801,29.589,46.827,29.635,46.813,29.677,46.756,29.69,46.715,29.674,46.689,29.693,46.665,29.748,46.641,29.763,46.62,29.77,46.601,29.805,46.581,29.898,46.553,30.131,46.423,30.22,46.426,30.254,46.401,30.269,46.32]]},{"n":"Dnieper","z":4,"b":[30.034,46.616,35.194,55.001],"l":[[32.578,46.616,32.645,46.65,32.934,46.717,33.067,46.759,33.237,46.777,33.357,46.786,33.451,46.809,33.54,46.831],[35.105,47.84,35.13,47.972,35.118,48.038,35.077,48.081,35.087,48.108,35.149,48.12,35.185,48.15,35.194,48.199,35.17,48.28,35.113,48.394,34.959,48.477,34.819,48.492,34.708,48.53,34.41,48.64,34.225,48.736,34.065,48.808,33.717,48.942,33.508,49.008,33.363,49.065,33.194,49.094],[31.888,49.554,31.691,49.636,31.462,49.757],[31.44,50,31.244,49.979,31.071,50.023,30.856,50.126,30.707,50.222,30.625,50.312,30.567,50.417,30.515,50.597],[33.391,54.917,33.109,54.986,32.978,55.001,32.903,54.986,32.823,54.958,32.738,54.916,32.687,54.871,32.67,54.822,32.611,54.763,32.512,54.694,32.382,54.695,32.221,54.765,32.006,54.777,31.598,54.707,30.974,54.655,30.718,54.613,30.622,54.564,30.552,54.544,30.509,54.553,30.463,54.537,30.413,54.495,30.36,54.425,30.304,54.326,30.318,54.185,30.405,54.001,30.407,53.899,30.324,53.879,30.275,53.802,30.261,53.666,30.284,53.523,30.371,53.297,30.279,53.25,30.26,53.206,30.284,53.148,30.251,53.12,30.162,53.124,30.091,53.101,30.038,53.051,30.034,52.954,30.081,52.809,30.143,52.71,30.222,52.658,30.258,52.606,30.252,52.555,30.279,52.507,30.342,52.462,30.365,52.43,30.351,52.409,30.397,52.38,30.503,52.342,30.546,52.306,30.525,52.272,30.584,52.197,30.722,52.08,30.796,51.996,30.805,51.944,30.773,51.894,30.699,51.844,30.625,51.766,30.551,51.658,30.547,51.545,30.612,51.427,30.611,51.33,30.508,51.217]]},{"n":"Don","z":5,"b":[38.306,47.106,43.549,54.064],"l":[[42.11,47.602,42.015,47.56,41.853,47.564,41.755,47.59,41.593,47.599,41.407,47.599,41.295,47.564,41.184,47.547,41.073,47.549,41.009,47.567,40.993,47.601,40.97,47.61,40.926,47.586,40.588,47.491,40.412,47.401,40.283,47.284,40.112,47.222,39.9,47.216,39.689,47.187,39.378,47.111,39.293,47.106],[43.549,49.507,43.379,49.487,43.316,49.456,43.302,49.408,43.252,49.365,43.165,49.329,43.095,49.324,43.042,49.351,43.016,49.38,43.017,49.412,42.957,49.442,42.835,49.472,42.797,49.516,42.77,49.573,42.731,49.582,42.647,49.567,42.446,49.579,42.297,49.599,42.006,49.61,41.833,49.597,41.713,49.616,41.467,49.613,41.38,49.588,41.314,49.587,41.27,49.609,41.261,49.63,41.288,49.648,41.279,49.683,41.235,49.734,41.14,49.77,40.997,49.791,40.921,49.827,40.914,49.879,40.89,49.922,40.849,49.956,40.778,49.964,40.677,49.945,40.644,49.953,40.621,49.977,40.596,50.027,40.502,50.12,40.432,50.139,40.386,50.083,40.322,50.065,40.198,50.096,40.04,50.103,40.001,50.133,40.03,50.186,40.027,50.238,39.99,50.289,40.002,50.346,40.064,50.408,40.036,50.475,39.92,50.547,39.907,50.617,39.879,50.65,39.794,50.692,39.757,50.733,39.768,50.775,39.794,50.797,39.814,50.833,39.789,50.874,39.775,50.919,39.778,50.952,39.745,50.962,39.677,50.955,39.513,50.96,39.266,50.987,39.143,51.024,39.144,51.071,39.182,51.111,39.256,51.142,39.262,51.173,39.199,51.203,39.177,51.238,39.16,51.294,39.131,51.364,39.052,51.43,39.052,51.477,39.059,51.507,39.037,51.54,39.018,51.568,39.012,51.597,39.038,51.626,39.046,51.657,39.035,51.69,39.06,51.757,39.122,51.859,39.14,51.934,39.133,52.012],[39.133,52.019,39.152,52.052,39.164,52.084,39.154,52.125,39.096,52.179,38.99,52.248,38.958,52.289,38.958,52.327,38.905,52.438,38.931,52.576,38.933,52.665,38.911,52.703,38.964,52.794,39.093,52.938,39.155,53.118,39.152,53.333,39.112,53.449,39.056,53.519,39.021,53.561,38.998,53.576,38.871,53.618,38.641,53.687,38.531,53.754,38.541,53.819,38.529,53.861,38.478,53.889,38.306,54.064]]},{"n":"Danube","z":2.1,"b":[8.22,47.931,17.206,49.027],"l":[[8.22,48.047,8.49,47.931,8.696,47.936,8.836,47.976,8.979,48.053,9.132,48.086,9.295,48.078,9.421,48.116,9.509,48.202,9.589,48.244,9.661,48.241,9.784,48.289,9.959,48.388,10.127,48.453,10.289,48.483,10.385,48.516,10.415,48.552,10.524,48.607,10.712,48.679,11.025,48.735,11.684,48.794,11.766,48.871,11.85,48.906,11.964,48.925,12.024,48.958,12.033,49.027,12.242,49.018,12.361,48.992,12.507,48.924],[17.206,48.061,17.165,48.117,17.111,48.146,17.029,48.167,16.878,48.172,16.659,48.161,16.486,48.213,16.36,48.327,16.209,48.38,16.034,48.369,15.861,48.379,15.693,48.411,15.573,48.424,15.502,48.417,15.431,48.371,15.36,48.286,15.265,48.229,15.147,48.198,15.036,48.198,14.932,48.227,14.829,48.227,14.726,48.197,14.583,48.219,14.4,48.292,14.243,48.328,14.114,48.329,14.037,48.352,14.012,48.399,13.872,48.462,13.487,48.582,13.316,48.602,13.2,48.636,13.081,48.691,13.005,48.748,12.972,48.806,12.909,48.836,12.816,48.836,12.704,48.86,12.507,48.931]]},{"n":"Double Mountain Fork Brazos","z":5,"b":[-101.667,32.905,-100.501,33.54],"l":[[-101.667,33.54,-101.426,33.4,-101.323,33.266,-101.253,33.201,-101.18,33.16,-101.107,33.14,-100.995,33.14,-100.832,33.103,-100.709,33.044,-100.501,32.905]]},{"n":"Drava","z":5,"b":[13.822,45.558,18.954,47.402],"l":[[13.822,47.09,14.367,47.147,14.507,47.209,14.62,47.224,14.752,47.214,14.861,47.245,14.946,47.319,15.062,47.371,15.145,47.4,15.208,47.402,15.303,47.394,15.346,47.347,15.352,47.316,15.323,47.3,15.316,47.253,15.329,47.174,15.361,47.121,15.413,47.095,15.476,46.997,15.56,46.831,15.582,46.767,15.63,46.736,15.701,46.723,16.042,46.663,16.211,46.57,16.321,46.535,16.428,46.524,16.57,46.485,16.748,46.416,16.871,46.339,16.94,46.254,17.033,46.187,17.15,46.14,17.242,46.077,17.311,45.996,17.406,45.951,17.529,45.941,17.607,45.914,17.64,45.868,17.706,45.827,17.807,45.79,17.964,45.77,18.283,45.765,18.551,45.627,18.712,45.574,18.954,45.558]]},{"n":"Duero","z":5,"b":[-8.675,41.062,-2.357,41.928],"l":[[-2.837,41.928,-2.542,41.85,-2.444,41.797,-2.445,41.743,-2.424,41.688,-2.357,41.605,-2.498,41.521,-2.617,41.505,-2.76,41.528,-2.866,41.529,-2.935,41.508,-3.038,41.524,-3.176,41.578,-3.411,41.633,-3.743,41.688,-3.941,41.693,-4.002,41.649,-4.146,41.631,-4.372,41.64,-4.682,41.592,-5.075,41.489,-5.307,41.464,-5.378,41.516,-5.518,41.527,-5.726,41.498,-5.838,41.492,-5.888,41.474,-5.971,41.49,-6.092,41.543,-6.178,41.55,-6.244,41.516,-6.289,41.455,-6.403,41.375,-6.566,41.304,-6.69,41.214,-6.776,41.108,-6.883,41.062,-7.011,41.079,-7.085,41.113,-7.103,41.166,-7.244,41.189,-7.651,41.181,-8.235,41.092,-8.425,41.084,-8.555,41.128,-8.675,41.154]]},{"n":"Ebro","z":4.7,"b":[-4.189,40.722,0.891,43.011],"l":[[-4.189,43.011,-4.102,43.001,-4.055,42.953,-4.017,42.861,-3.963,42.825,-3.89,42.844,-3.822,42.842,-3.757,42.817,-3.709,42.833,-3.678,42.888,-3.627,42.898,-3.522,42.845,-3.451,42.807,-3.337,42.792,-3.155,42.787,-3.011,42.755,-2.905,42.695,-2.843,42.644,-2.823,42.602,-2.756,42.56,-2.641,42.518,-2.563,42.507,-2.522,42.527,-2.395,42.514,-2.183,42.466,-1.994,42.391,-1.748,42.236,-1.405,41.978,-1.11,41.802,-0.748,41.622,-0.526,41.479,-0.444,41.374,-0.301,41.309,-0.097,41.284,0.04,41.306,0.111,41.375,0.167,41.411,0.208,41.413,0.263,41.399,0.331,41.368,0.362,41.328,0.357,41.28,0.364,41.256,0.395,41.254,0.525,41.249,0.59,41.22,0.632,41.165,0.652,41.119,0.652,41.083,0.614,41.038,0.538,40.982,0.517,40.904,0.551,40.802,0.651,40.745,0.891,40.722]]},{"n":"White Nile","z":2,"b":[31.125,9.354,32.796,15.635],"l":[[31.125,9.433,31.346,9.366,31.453,9.354,31.518,9.376,31.581,9.445,31.64,9.562,31.746,9.657,31.899,9.731,32.036,9.834,32.158,9.968,32.235,10.072,32.267,10.148,32.27,10.246,32.244,10.369,32.201,10.435,32.139,10.446,32.108,10.471,32.106,10.51,32.24,10.656,32.511,10.908,32.645,11.103,32.642,11.24,32.673,11.414,32.737,11.624,32.764,11.795,32.749,11.995,32.73,12.19,32.743,12.333,32.783,12.489,32.791,12.652,32.765,12.822,32.767,12.939,32.796,13.004,32.752,13.117,32.635,13.277,32.586,13.424,32.604,13.559,32.587,13.638,32.535,13.662,32.5,13.704,32.481,13.764,32.439,13.825,32.373,13.886,32.295,14.032,32.205,14.264,32.172,14.465,32.196,14.637,32.262,14.783,32.37,14.902,32.44,15.13,32.49,15.635]]},{"n":"Blue Nile","z":3,"b":[32.49,10.155,35.353,15.635],"l":[[35.353,10.155,35.343,10.187,35.293,10.411,35.242,10.517,35.147,10.573,35.182,10.974,35.161,11.14,35.097,11.202,34.993,11.249,34.778,11.296],[34.38,11.783,34.324,12.048,34.317,12.194,34.341,12.308,34.301,12.403,34.199,12.478,34.157,12.534,34.176,12.572,34.175,12.637,34.154,12.729,34.117,12.804,34.065,12.863,34.04,12.872,34,12.902,33.981,12.958,33.961,13.144,33.93,13.258,33.89,13.3,33.828,13.325,33.744,13.334,33.692,13.381,33.672,13.466,33.639,13.53,33.593,13.573,33.582,13.619,33.607,13.669,33.602,13.708,33.578,13.734,33.581,13.756,33.597,13.772,33.599,13.793,33.58,13.819,33.574,13.839,33.581,13.856,33.596,13.867,33.604,13.887,33.591,13.937,33.609,14.011,33.657,14.109,33.566,14.344,33.337,14.716,33.211,14.956,33.189,15.064,33.134,15.16,33.046,15.244,32.949,15.304,32.842,15.339,32.735,15.423,32.628,15.553,32.554,15.622,32.49,15.635]]},{"n":"Elbe","z":4.7,"b":[9.784,50.031,15.889,53.555],"l":[[15.583,50.693,15.615,50.614,15.653,50.576,15.706,50.553,15.734,50.522,15.736,50.484,15.782,50.44,15.864,50.394,15.884,50.372,15.889,50.328,15.845,50.272,15.819,50.197,15.817,50.107,15.677,50.052,15.4,50.031,15.236,50.035,15.159,50.075,15.088,50.152,14.969,50.179,14.779,50.181,14.631,50.222,14.473,50.343,14.388,50.438,14.332,50.46,14.277,50.442,14.241,50.454,14.222,50.498,14.178,50.52,14.109,50.52,14.062,50.535,14.039,50.567,14.04,50.589,14.063,50.602,14.067,50.619,14.07,50.641,14.084,50.661,14.16,50.686,14.203,50.75,14.212,50.851,14.136,50.918,13.977,50.95,13.874,50.991,13.828,51.041,13.744,51.079,13.622,51.104,13.522,51.141,13.443,51.19,13.398,51.235,13.386,51.278,13.345,51.312,13.275,51.339,13.226,51.382,13.184,51.474],[13.191,51.474,12.934,51.623,12.842,51.711,12.829,51.79,12.742,51.843,12.58,51.871,12.382,51.881,12.147,51.874,11.995,51.909,11.925,51.986,11.843,52.031,11.749,52.046,11.691,52.072,11.668,52.109,11.668,52.155,11.692,52.21,11.757,52.273,11.917,52.378,11.978,52.438,12.003,52.562,12.014,52.77,11.921,52.918,11.724,53.006,11.555,53.054,11.414,53.062,11.297,53.093,11.203,53.147,11.139,53.166,11.106,53.151,10.994,53.2,10.804,53.312,10.682,53.369,10.601,53.372,10.413,53.417,10.305,53.426,10.215,53.413,10.082,53.44],[10.082,53.44,9.819,53.54,9.784,53.555,9.9,53.552,9.988,53.538,10.048,53.516,10.082,53.44]]},{"n":"Ertis","z":2.1,"b":[74.994,47.727,85.267,53.986],"l":[[85.267,47.994,84.999,47.928,84.88,47.88,84.822,47.828,84.729,47.783,84.6,47.744,84.491,47.727,84.357,47.732],[83.364,49.644,83.128,49.682,82.991,49.748,82.872,49.856,82.696,49.947,82.462,50.021,82.321,50.101,82.273,50.185,82.15,50.219,81.953,50.202,81.775,50.228,81.614,50.295,81.482,50.325,81.379,50.319,81.248,50.338,81.089,50.382,80.963,50.392,80.87,50.367,80.691,50.36,80.426,50.373,80.242,50.41,80.139,50.473,80.019,50.496,79.882,50.479,79.702,50.517,79.479,50.608,79.313,50.662,79.203,50.678,79.116,50.667,79.05,50.63,78.9,50.651,78.545,50.767,78.348,50.858,78.245,50.923,78.171,50.993,78.106,51.094,78.05,51.225,77.96,51.335,77.837,51.424,77.691,51.499,77.521,51.56,77.441,51.651,77.451,51.771,77.334,51.886,77.089,51.997,76.96,52.108,76.948,52.219,76.9,52.303,76.814,52.36,76.78,52.411,76.796,52.455,76.794,52.502,76.775,52.552,76.734,52.589,76.673,52.612,76.654,52.64,76.678,52.672,76.679,52.704,76.659,52.736,76.55,52.816,76.514,52.827,76.494,52.83,76.479,52.85,76.485,52.885,76.461,52.899,76.314,52.905,76.26,52.93,76.242,52.98,76.144,53.015,75.967,53.034,75.855,53.078,75.806,53.146,75.727,53.202,75.619,53.246,75.568,53.282,75.567,53.304,75.545,53.322,75.521,53.333,75.503,53.344,75.506,53.361,75.503,53.397,75.457,53.455,75.357,53.52,75.203,53.59,75.101,53.654,75.05,53.71,75.04,53.789,75.07,53.891,75.062,53.953,74.994,53.986]]},{"n":"Irtysh","z":2.1,"b":[85.267,46.99,90.325,48.019],"l":[[90.325,47.65,90.284,47.491,90.253,47.43,90.201,47.408,90.132,47.401,90.064,47.356,89.973,47.269,89.872,47.213,89.763,47.187,89.655,47.126,89.546,47.03,89.406,46.99,89.233,47.005,89.032,47.061,88.804,47.159,88.515,47.233,88.166,47.284,87.847,47.348,87.559,47.427,87.387,47.495,87.331,47.555,87.266,47.58,87.19,47.572,87.1,47.591,86.944,47.66,86.607,47.832,86.433,47.878,86.31,47.853,86.154,47.876,85.966,47.945,85.792,47.98,85.634,47.982,85.544,47.994,85.521,48.016,85.449,48.019,85.267,47.994]]},{"n":"Euphrates","z":3,"b":[40.993,30.902,47.468,34.509],"l":[[45.463,31.305,45.541,31.229,45.685,31.184,45.922,31.146,46.121,31.086,46.363,30.965,46.483,30.903,46.58,30.902,46.692,30.942,46.886,30.959,47.16,30.953,47.331,30.967,47.399,31,47.442,31.004,47.468,30.968],[40.993,34.428,41.119,34.388,41.177,34.393,41.208,34.43,41.276,34.443,41.38,34.43,41.43,34.436,41.448,34.44,41.471,34.449,41.502,34.436,41.591,34.448,41.717,34.497,41.827,34.509,41.921,34.484,41.99,34.446,42.055,34.369,42.122,34.302,42.17,34.288,42.22,34.306,42.248,34.297,42.263,34.29,42.286,34.292,42.303,34.334,42.33,34.346,42.366,34.329,42.366,34.302,42.331,34.264,42.332,34.231,42.369,34.205,42.383,34.165,42.375,34.114,42.417,34.059,42.447,34.041,42.51,34.001,42.551,33.944,42.541,33.888,42.563,33.87,42.617,33.891,42.673,33.89,42.73,33.865,42.748,33.82,42.724,33.752,42.767,33.671,42.877,33.577,42.965,33.521,43.032,33.503,43.079,33.505,43.106,33.526,43.155,33.515,43.228,33.471,43.288,33.455,43.334,33.466,43.454,33.442,43.536,33.418,43.65,33.384,43.776,33.311,43.832,33.226,43.872,33.199,43.905,33.199,43.948,33.195,44.029,33.16,44.122,33.053,44.254,32.828,44.263,32.793,44.264,32.727,44.237,32.624,44.255,32.496,44.321,32.349,44.355,32.235,44.356,32.157,44.387,32.072,44.448,31.981,44.477,31.878,44.475,31.762,44.548,31.648,44.77,31.479,45.042,31.441,45.158,31.403,45.209,31.352,45.293,31.321,45.467,31.305]]},{"n":"Ferenc Csatorna","z":5,"b":[18.839,45.836,18.92,46.267],"l":[[18.92,46.267,18.909,45.95,18.905,45.932,18.894,45.866,18.839,45.836]]},{"n":"Finlay","z":5,"b":[-127.235,56.757,-124.836,57.674],"l":[[-127.235,56.815,-126.926,56.916,-126.915,57.082,-126.822,57.19,-126.645,57.296,-126.551,57.415,-126.542,57.546,-126.518,57.627,-126.478,57.659,-126.403,57.674,-126.294,57.674,-126.167,57.622,-126.022,57.516,-125.845,57.439,-125.634,57.391,-125.393,57.261,-125.12,57.049,-124.946,56.897,-124.836,56.757]]},{"n":"Firat","z":3,"b":[37.848,36.836,41.455,40.18],"l":[[38.768,38.813,38.555,38.768,38.45,38.714,38.383,38.635,38.355,38.581,38.367,38.551,38.452,38.508,38.61,38.45,38.719,38.426,38.78,38.435,38.847,38.413,38.921,38.358,38.997,38.334,39.075,38.34,39.115,38.306,39.115,38.23,39.149,38.19,39.218,38.183,39.248,38.154,39.239,38.102,39.171,38.034,39.042,37.949,38.978,37.887,38.978,37.849,38.956,37.82,38.949,37.797,38.924,37.745,38.841,37.713,38.829,37.69,38.838,37.666,38.813,37.645,38.701,37.631,38.608,37.593,38.533,37.532,38.439,37.495,38.328,37.483,38.25,37.461,38.207,37.429,38.137,37.421,38.039,37.437,37.966,37.425,37.92,37.384,37.855,37.292,37.848,37.217,37.862,37.116,37.897,37.061,37.951,37.054,37.991,36.996,38.026,36.836],[41.455,40.18,41.328,40.016,40.985,39.944,40.77,39.928,40.569,39.945,40.396,39.942,40.251,39.919,40.169,39.895,40.15,39.869,40.17,39.824,40.23,39.76,40.24,39.694,40.203,39.626,40.163,39.584,40.122,39.568,39.939,39.595,39.613,39.666,39.415,39.683,39.343,39.648,39.196,39.619,38.976,39.594,38.74,39.536,38.488,39.445,38.426,39.342,38.555,39.229,38.643,39.17,38.689,39.167,38.718,39.144,38.74,39.078]]},{"n":"Fitzroy","z":5,"b":[123.563,-18.492,127.152,-17.383],"l":[[127.152,-17.383,126.998,-17.454,126.903,-17.478,126.775,-17.478,126.4,-17.458,126.237,-17.48,126.162,-17.538,126.124,-17.582,126.124,-17.613,126.098,-17.633,126.045,-17.641,125.985,-17.707,125.918,-17.83,125.792,-17.988,125.511,-18.277,125.139,-18.452,124.923,-18.492,124.736,-18.456,124.581,-18.353,124.456,-18.183,124.351,-18.09,124.267,-18.075,124.178,-18.085,124.087,-18.119,124,-18.116,123.918,-18.075,123.817,-17.967,123.698,-17.793,123.622,-17.701,123.588,-17.691,123.57,-17.661,123.568,-17.588,123.563,-17.521]]},{"n":"Fraser","z":4.7,"b":[-123.08,49.131,-118.319,54.245],"l":[[-118.319,52.522,-118.572,52.762,-118.601,52.825,-118.721,52.891,-118.941,52.983,-119.175,53.023,-119.425,53.009,-119.669,53.056,-119.906,53.161,-120.244,53.346,-120.681,53.609,-121.024,53.778,-121.396,53.889,-121.584,53.93,-121.649,53.963,-121.654,54.002,-121.738,54.061,-121.903,54.141,-122.089,54.201,-122.295,54.24,-122.435,54.245,-122.509,54.215,-122.551,54.174,-122.56,54.121,-122.611,54.046,-122.704,53.947,-122.744,53.816,-122.733,53.652,-122.708,53.534,-122.67,53.461,-122.709,53.399,-122.827,53.348,-122.804,53.266,-122.64,53.153,-122.53,53.042,-122.475,52.932,-122.457,52.798,-122.475,52.642,-122.436,52.508,-122.34,52.395,-122.285,52.259,-122.27,52.097,-122.272,51.99,-122.29,51.937,-122.321,51.902,-122.365,51.884,-122.392,51.846,-122.408,51.758,-122.268,51.519,-122.225,51.407,-122.232,51.345,-122.143,51.221,-121.955,51.038,-121.858,50.901,-121.85,50.812,-121.866,50.766,-121.906,50.763,-121.924,50.74,-121.92,50.698,-121.86,50.615,-121.744,50.491,-121.628,50.302,-121.51,50.047,-121.449,49.791,-121.445,49.532,-121.49,49.387,-121.582,49.354,-121.652,49.307,-121.698,49.246,-121.874,49.19,-122.179,49.139,-122.445,49.141,-122.673,49.197,-122.85,49.207,-123.041,49.151,-123.08,49.131]]},{"n":"Gan","z":5,"b":[114.412,24.707,116.016,29.279],"l":[[114.412,24.707,114.48,24.708,114.53,24.735,114.614,24.816,114.73,24.882,114.762,24.953,114.748,25.051,114.783,25.113,114.865,25.137,114.899,25.196,114.885,25.29,114.93,25.43,115.033,25.614,115.09,25.746,115.101,25.826,115.068,25.864,114.992,25.86,114.94,25.882,114.913,25.93,114.909,25.992,114.938,26.105,114.904,26.125,114.908,26.153,114.939,26.194,114.946,26.237,114.931,26.283,114.862,26.369,114.738,26.497,114.717,26.614,114.797,26.722,114.879,26.789,114.961,26.813,114.995,26.849,114.98,26.897,114.988,26.942,115.021,26.985,115.021,27.032,114.99,27.083,115.009,27.138,115.076,27.198,115.098,27.284,115.075,27.396,115.13,27.512,115.264,27.634,115.369,27.785,115.445,27.966,115.577,28.11,115.765,28.218,115.848,28.317,115.818,28.452,115.816,28.585,115.869,28.716,115.975,28.888,116.016,29.05,115.982,29.279]]},{"n":"Ganges","z":3,"b":[78.022,21.721,90.435,30.888],"l":[[89.483,22.276,89.425,22.496,89.372,22.581,89.3,22.619],[89.3,22.619,89.261,22.574,89.255,22.445,89.286,22.308,89.354,22.16,89.379,22.058,89.352,21.972,89.351,21.886,89.336,21.788,89.354,21.721],[88.44,22.986,88.385,22.868,88.347,22.735,88.344,22.629,88.29,22.549,88.185,22.496,88.118,22.41,88.087,22.218],[88.44,22.986,88.518,22.975,88.598,22.975,88.682,22.954,88.756,22.915,88.82,22.858,88.843,22.803,88.823,22.748,88.845,22.7,88.91,22.659,88.971,22.511,89.05,22.275,89.056,22.186,89.051,22.093],[90.074,23.573,90.096,23.34,90.127,23.252,90.181,23.206,90.228,23.178,90.244,23.138,90.265,23.116,90.299,23.088,90.302,23.055,90.315,23.016,90.362,22.961,90.361,22.876,90.394,22.775,90.435,22.752],[89.109,23.992,89.219,23.808,89.302,23.733,89.381,23.694,89.447,23.64,89.5,23.572,89.52,23.525,89.509,23.499,89.527,23.483,89.575,23.477,89.598,23.443,89.598,23.383,89.615,23.337,89.632,23.301,89.638,23.252,89.66,23.223,89.709,23.192,89.719,23.133,89.765,23.02,89.926,22.84,90.008,22.685,90.014,22.563,89.985,22.466],[89.109,23.992,89.2,23.933,89.281,23.931,89.38,23.901,89.497,23.844,89.604,23.823,89.749,23.848],[88.397,24.389,88.51,24.261,88.534,24.208,88.527,24.166,88.554,24.129,88.616,24.098,88.643,24.059,88.636,24.012,88.69,23.968,88.805,23.928,88.864,23.856,88.868,23.752,88.837,23.666,88.772,23.598,88.764,23.531,88.811,23.463,88.853,23.441,88.885,23.423,88.906,23.39,88.926,23.363,88.989,23.333,89.002,23.263,89.028,23.186,89.089,23.113,89.113,23.04,89.1,22.97,89.113,22.893,89.154,22.809,89.208,22.749,89.268,22.708,89.276,22.703,89.271,22.679,89.3,22.619],[88.397,24.389,88.499,24.347,88.642,24.326,88.724,24.275,88.742,24.193,88.77,24.144,88.806,24.127,88.858,24.131,88.926,24.155,89.01,24.109,89.109,23.992],[87.92,24.825,87.94,24.666,88.019,24.543,88.175,24.389,88.238,24.234,88.209,24.079,88.205,23.945,88.227,23.831,88.215,23.74,88.183,23.7,88.214,23.637,88.272,23.587,88.304,23.543,88.345,23.53,88.361,23.461,88.352,23.333,88.375,23.255,88.43,23.227,88.467,23.186,88.486,23.134,88.459,23.027,88.44,22.986],[87.92,24.825,87.946,24.753,87.999,24.661,88.079,24.55,88.145,24.486,88.225,24.461,88.287,24.48,88.338,24.454,88.397,24.389],[79.841,30.888,79.82,30.837,79.829,30.787,79.859,30.722,79.865,30.679,79.834,30.648,79.745,30.543,79.667,30.522,79.571,30.55,79.47,30.516,79.313,30.372,79.232,30.301,79.159,30.285,79.067,30.3,78.939,30.292,78.777,30.26,78.669,30.201,78.614,30.116,78.52,30.09,78.387,30.124,78.281,30.093,78.203,29.998,78.17,29.889,78.183,29.768,78.139,29.673,78.04,29.606,78.022,29.492,78.086,29.332,78.106,29.181,78.084,29.04,78.098,28.882,78.176,28.621,78.242,28.522,78.271,28.443,78.284,28.352,78.337,28.253,78.43,28.146,78.603,28.026,78.855,27.893,79.025,27.825,79.112,27.82,79.167,27.786,79.192,27.723,79.255,27.68,79.358,27.658,79.446,27.62,79.519,27.568,79.591,27.466,79.662,27.316,79.733,27.217,79.838,27.145,79.922,27.126,79.998,27.025,80.094,26.837,80.238,26.648,80.43,26.459,80.544,26.317,80.58,26.222,80.652,26.138,80.76,26.066,80.869,26.038,80.978,26.053,81.123,25.989,81.303,25.844,81.395,25.746,81.398,25.692,81.458,25.646,81.573,25.606,81.649,25.564,81.686,25.518,81.756,25.499,81.857,25.507,81.907,25.487,81.903,25.413,82.042,25.312,82.12,25.285,82.183,25.296,82.215,25.272,82.217,25.259,82.226,25.243,82.239,25.241,82.254,25.246,82.274,25.262,82.3,25.26,82.318,25.23,82.345,25.227,82.38,25.249,82.436,25.235,82.513,25.184,82.552,25.184,82.563,25.203,82.585,25.218,82.62,25.22,82.786,25.144,82.879,25.142,82.965,25.188,83.017,25.239,83.033,25.295,83.074,25.33,83.139,25.344,83.159,25.377,83.135,25.427,83.139,25.472,83.17,25.51,83.242,25.519,83.353,25.498,83.44,25.466,83.489,25.444,83.513,25.462,83.513,25.497,83.518,25.534,83.6,25.584,83.67,25.607,83.713,25.59,83.752,25.542,83.803,25.519,83.866,25.522,83.959,25.577,84.082,25.683,84.171,25.722,84.226,25.694,84.284,25.701,84.343,25.744,84.428,25.747,84.54,25.711,84.634,25.708,84.711,25.738,84.827,25.747,84.981,25.734,85.119,25.683,85.239,25.592,85.338,25.544,85.412,25.535,85.456,25.538,85.501,25.545,85.555,25.544,85.618,25.494,85.697,25.474,85.786,25.495,85.88,25.462,85.979,25.377,86.098,25.324,86.237,25.303,86.371,25.341,86.5,25.437,86.571,25.448,86.584,25.373,86.617,25.315,86.668,25.276,86.743,25.28,86.841,25.327,86.965,25.32,87.115,25.26,87.219,25.267,87.275,25.34,87.35,25.383,87.444,25.394,87.518,25.366,87.572,25.3,87.644,25.257,87.734,25.237,87.779,25.193,87.78,25.124,87.824,25.064,87.911,25.013,87.942,24.956,87.919,24.893,87.92,24.825]]},{"n":"Ariège","z":5,"b":[1.454,42.627,1.825,43.415],"l":[[1.809,42.627,1.825,42.693,1.791,42.732,1.675,42.783,1.616,42.856,1.594,42.97,1.587,43.149,1.54,43.298,1.454,43.415]]},{"n":"Genala","z":5,"b":[38.724,4.324,41.966,6.605],"l":[[38.774,6.605,38.724,6.541,38.76,6.453,38.92,6.253,38.991,6.126,39.11,6.054,39.357,5.981,39.388,5.969,39.407,5.952,39.478,5.795,39.559,5.724,39.654,5.709,39.701,5.658,39.699,5.572,39.736,5.526,39.81,5.521,39.88,5.488,39.946,5.426,40.031,5.385,40.135,5.365,40.273,5.37,40.443,5.4,40.627,5.487,40.824,5.629,40.954,5.702,41.016,5.708,41.073,5.693,41.124,5.66,41.196,5.526,41.288,5.291,41.394,5.09,41.515,4.922,41.579,4.801,41.587,4.728,41.685,4.599,41.966,4.324]]},{"n":"Gharraf Canal","z":4,"b":[45.809,30.964,46.405,32.502],"l":[[45.809,32.502,45.901,32.431,45.928,32.377,45.923,32.316,45.943,32.264,45.989,32.219,45.998,32.133,45.97,32.005,45.986,31.922,46.046,31.883,46.089,31.773,46.116,31.592,46.154,31.457,46.203,31.367,46.276,31.282,46.371,31.202,46.405,31.112,46.366,30.964]]},{"n":"Gila","z":5,"b":[-114.582,32.657,-108.114,33.577],"l":[[-111.249,33.104,-111.455,33.04,-111.648,33.054,-111.899,33.126,-112.122,33.222,-112.32,33.342,-112.511,33.372,-112.697,33.311,-112.767,33.238,-112.72,33.154,-112.704,33.087,-112.717,33.036,-112.809,33.03,-112.98,33.067,-113.084,33.067,-113.12,33.03,-113.197,33.001,-113.317,32.979,-113.525,32.906,-113.824,32.782,-114.005,32.725,-114.068,32.735,-114.13,32.725,-114.19,32.696,-114.261,32.709,-114.344,32.763,-114.428,32.776,-114.582,32.735],[-108.204,33.577,-108.114,33.413,-108.246,33.176,-108.348,33.091,-108.464,33.079,-108.555,32.992,-108.62,32.828,-108.71,32.718,-108.825,32.659,-108.944,32.657,-109.067,32.711,-109.156,32.786,-109.209,32.881,-109.264,32.94,-109.321,32.962,-109.397,32.943,-109.492,32.884,-109.591,32.853,-109.695,32.85,-109.846,32.932,-110.042,33.099,-110.226,33.186,-110.398,33.193,-110.554,33.15,-110.694,33.056,-110.814,33.038,-110.916,33.095,-111.039,33.121,-111.256,33.112]]},{"n":"Glåma","z":5,"b":[10.624,60.525,12.028,62.687],"l":[[11.484,62.687,11.188,62.496,11.085,62.443,10.984,62.391,10.773,62.309,10.655,62.245,10.632,62.201,10.624,62.157,10.634,62.112,10.699,62.062,10.821,62.006,10.888,61.95,10.901,61.893,10.883,61.835,10.835,61.776,10.863,61.7,10.968,61.607,11.033,61.526,11.072,61.423,11.247,61.317,11.318,61.233,11.344,61.134,11.402,61.029,11.493,60.919,11.579,60.849,11.661,60.818,11.737,60.763,11.806,60.685,11.881,60.633,11.962,60.609,12.009,60.579,12.028,60.525]]},{"n":"Godävari","z":4.7,"b":[73.652,16.334,82.339,20.036],"l":[[81.739,16.942,81.833,16.669,81.856,16.553,81.839,16.503,81.8,16.465,81.736,16.441,81.706,16.4,81.712,16.334],[81.739,16.942,81.888,16.777,82.034,16.713,82.339,16.707],[73.652,20.013,73.889,19.99,73.99,19.999,74.035,20.031,74.084,20.036,74.163,20.003,74.422,19.918,74.589,19.835,74.749,19.726,74.917,19.652,75.093,19.613,75.197,19.579,75.247,19.533,75.473,19.425,75.591,19.38,75.678,19.362,75.769,19.361,75.846,19.376,75.905,19.363,75.917,19.326,75.941,19.314,75.987,19.333,76.011,19.326,76.027,19.31,76.092,19.291,76.319,19.257,76.347,19.241,76.353,19.202,76.337,19.14,76.347,19.124,76.368,19.12,76.396,19.111,76.427,19.077,76.473,19.073,76.535,19.099,76.582,19.086,76.614,19.035,76.657,19.002,76.71,18.986,76.748,18.996,76.77,19.034,76.856,19.07,77.005,19.105,77.093,19.108,77.12,19.081,77.196,19.089,77.319,19.134,77.405,19.12,77.453,19.048,77.579,18.966,77.784,18.873,77.968,18.87,78.133,18.958,78.235,18.984,78.275,18.948,78.319,18.946,78.367,18.976,78.429,18.98,78.504,18.956,78.575,18.966,78.643,19.012,78.701,19.027,78.751,19.014,78.835,19.026,78.953,19.063,79.05,19.033,79.125,18.937,79.201,18.871,79.317,18.816,79.401,18.843,79.496,18.809,79.631,18.723,79.727,18.69,79.785,18.711,79.822,18.762,79.874,18.802,79.925,18.807,80.029,18.744,80.142,18.702,80.244,18.709,80.319,18.686,80.366,18.632,80.39,18.573,80.389,18.509,80.408,18.463,80.447,18.434,80.473,18.388,80.485,18.324,80.541,18.261,80.688,18.166,80.84,17.996,80.892,17.865,80.895,17.716,80.986,17.622,81.165,17.582,81.267,17.543,81.291,17.506,81.352,17.482,81.45,17.472,81.528,17.448,81.587,17.41,81.647,17.279,81.739,16.942]]},{"n":"Grande","z":3,"b":[-66.323,-22.155,-44.058,-15.86],"l":[[-44.45,-22.155,-44.304,-22.047,-44.208,-21.948,-44.112,-21.822,-44.062,-21.734,-44.058,-21.684,-44.068,-21.658,-44.093,-21.657,-44.127,-21.613,-44.171,-21.524,-44.241,-21.476,-44.387,-21.465],[-44.616,-21.318,-44.686,-21.236,-44.739,-21.206,-44.799,-21.201,-44.857,-21.176,-44.915,-21.13,-44.971,-21.125,-45.026,-21.161,-45.076,-21.175,-45.122,-21.169,-45.164,-21.14,-45.205,-21.093],[-46.326,-20.668,-46.587,-20.623],[-47.073,-20.284,-47.329,-20.118,-47.428,-20.036,-47.46,-20.001,-47.518,-19.994,-47.594,-20.019,-47.656,-20.023,-47.696,-19.991,-47.75,-19.977,-47.819,-19.981,-47.86,-20.012,-47.862,-20.056,-47.909,-20.082,-47.991,-20.083,-48.051,-20.12,-48.109,-20.117,-48.178,-20.083,-48.253,-20.093,-48.361,-20.139,-48.716,-20.144,-48.812,-20.159,-48.858,-20.233,-48.887,-20.37,-48.918,-20.429,-48.952,-20.41,-48.978,-20.338,-48.995,-20.214,-49.04,-20.187,-49.113,-20.259,-49.166,-20.295,-49.2,-20.294,-49.239,-20.246,-49.284,-20.149,-49.29,-20.072,-49.259,-20.017,-49.278,-19.987,-49.385,-19.98],[-50.364,-19.859,-50.434,-19.8,-50.489,-19.791,-50.584,-19.821],[-63.757,-19.028,-63.619,-19.071,-63.565,-19.049,-63.548,-18.977,-63.497,-18.928,-63.411,-18.904,-63.31,-18.847,-63.195,-18.756,-63.074,-18.615,-62.949,-18.425,-62.858,-18.197,-62.773,-17.799,-62.786,-17.598,-62.775,-17.465,-62.743,-17.334,-62.738,-17.243,-62.762,-17.192,-62.823,-17.151,-62.922,-17.121,-63,-17.031,-63.059,-16.88,-63.259,-16.732,-63.601,-16.585,-63.804,-16.449,-63.869,-16.322,-63.965,-16.237,-64.092,-16.192,-64.162,-16.148,-64.176,-16.104,-64.303,-16.027,-64.664,-15.86],[-66.323,-17.577,-66.019,-17.862,-65.96,-17.952,-65.916,-17.978,-65.868,-17.972,-65.793,-18.009,-65.642,-18.129,-65.39,-18.371,-65.287,-18.493,-65.25,-18.575,-65.19,-18.626,-65.108,-18.645,-65.051,-18.63,-65.018,-18.581,-64.956,-18.538,-64.863,-18.503,-64.756,-18.53,-64.635,-18.621,-64.529,-18.651,-64.437,-18.618,-64.342,-18.686,-64.242,-18.855,-64.16,-18.954,-64.097,-18.985,-64.05,-19.042,-64.021,-19.126,-63.985,-19.165,-63.94,-19.158,-63.906,-19.122,-63.884,-19.058,-63.842,-19.027,-63.751,-19.028]]},{"n":"Great Pee Dee","z":5,"b":[-80.481,33.405,-79.106,36.232],"l":[[-80.464,36.232,-80.456,36.206,-80.481,36.093,-80.459,36.011,-80.392,35.943,-80.365,35.893,-80.378,35.86,-80.406,35.843,-80.447,35.841,-80.465,35.817,-80.46,35.773,-80.423,35.742,-80.354,35.724,-80.27,35.652,-80.122,35.466,-80.071,35.401,-80.055,35.312,-80.058,35.176,-80.027,35.109,-79.963,35.113,-79.915,35.096,-79.884,35.06,-79.884,34.976,-79.915,34.846,-79.891,34.738,-79.812,34.652,-79.781,34.58,-79.8,34.523,-79.791,34.505,-79.754,34.503,-79.722,34.465,-79.676,34.359,-79.634,34.3,-79.596,34.288,-79.551,34.195,-79.471,33.934,-79.334,33.786,-79.246,33.715,-79.162,33.673,-79.115,33.629,-79.106,33.583,-79.123,33.529,-79.19,33.437,-79.226,33.405]]},{"n":"Guaporé","z":3,"b":[-65.447,-11.913,-65.031,-10.392],"l":[[-65.031,-11.913,-65.037,-11.829,-65.09,-11.741,-65.115,-11.735,-65.143,-11.752,-65.163,-11.765,-65.186,-11.75,-65.19,-11.71,-65.175,-11.647,-65.206,-11.581,-65.282,-11.511,-65.322,-11.439,-65.325,-11.365,-65.342,-11.315,-65.373,-11.29,-65.39,-11.246,-65.394,-11.184,-65.372,-11.11,-65.324,-11.025,-65.334,-10.893,-65.402,-10.715,-65.44,-10.586,-65.447,-10.507,-65.437,-10.449,-65.395,-10.392]]},{"n":"Göta älv","z":5,"b":[11.729,57.764,12.335,58.374],"l":[[12.335,58.374,12.171,58.24,12.112,58.14,12.105,58.031,12.029,57.928,11.81,57.781,11.729,57.764]]},{"n":"Hailar","z":3,"b":[117.873,49.192,122.287,49.849],"l":[[117.873,49.513,117.912,49.429,117.969,49.374,118.057,49.319,118.175,49.332,118.323,49.412,118.44,49.437,118.524,49.405,118.583,49.347,118.617,49.262,118.747,49.212,119.086,49.192,119.232,49.199,119.337,49.226,119.45,49.275,119.71,49.275,120.118,49.225,120.518,49.256,120.911,49.368,121.216,49.473,121.433,49.572,121.626,49.635,121.878,49.678,122.287,49.849]]},{"n":"Han","z":5,"b":[106.398,30.407,114.288,33.234],"l":[[111.478,32.568,111.602,32.468,111.642,32.407,111.639,32.354,111.685,32.265,111.78,32.141,111.919,32.065,112.101,32.036,112.189,31.966,112.181,31.853,112.225,31.771,112.322,31.719,112.39,31.657,112.432,31.586,112.44,31.496,112.417,31.388,112.434,31.308,112.521,31.23,112.556,31.219,112.572,31.183,112.58,31.118,112.569,31.096,112.539,31.077,112.523,31.039,112.556,30.939,112.599,30.875,112.634,30.848,112.632,30.815,112.622,30.787,112.605,30.713,112.627,30.67,112.689,30.658,112.713,30.627,112.707,30.601,112.716,30.575,112.754,30.572,112.797,30.564,112.849,30.519,112.886,30.515,112.973,30.531,113.073,30.475,113.21,30.43,113.381,30.407,113.512,30.443,113.602,30.538,113.672,30.597,113.769,30.626,113.801,30.622,113.838,30.63,113.86,30.644,113.9,30.637,113.926,30.644,113.939,30.66,113.964,30.657,114.009,30.615,114.096,30.586,114.288,30.572],[106.398,33.068,106.676,33.142,106.832,33.134,106.931,33.102,107.026,33.042,107.189,33.063,107.42,33.165,107.596,33.2,107.718,33.171,107.807,33.178,107.861,33.222,107.905,33.234,107.939,33.216,107.956,33.175,107.956,33.112,108.036,33.062,108.195,33.024,108.266,32.964,108.247,32.881,108.288,32.811,108.388,32.753,108.411,32.734,108.411,32.709,108.425,32.665,108.512,32.55,108.651,32.506,108.718,32.528,108.759,32.599,108.782,32.614,108.798,32.617,108.819,32.609,108.836,32.603,108.855,32.608,108.922,32.636,109.063,32.735,109.175,32.775,109.257,32.757,109.325,32.778,109.377,32.838,109.487,32.886,109.716,32.934,109.739,32.937,109.767,32.934,110.14,32.825,110.305,32.807,110.367,32.841,110.475,32.843,110.628,32.814,110.715,32.815,110.736,32.846,110.808,32.848,110.931,32.821,110.967,32.788,110.969,32.771,111,32.741]]},{"n":"Amur","z":3,"b":[121.405,50.209,127.59,53.556],"l":[[127.59,50.209,127.395,50.299,127.337,50.35,127.351,50.394,127.341,50.428,127.306,50.454,127.308,50.494,127.347,50.55,127.347,50.621,127.307,50.708,127.198,50.829,127.02,50.986,126.925,51.1,126.912,51.172,126.888,51.23,126.854,51.261,126.834,51.315,126.848,51.374,126.827,51.412,126.802,51.448,126.805,51.506,126.775,51.545,126.709,51.566,126.689,51.61,126.701,51.703,126.654,51.781,126.511,51.926,126.468,52.031,126.456,52.126,126.395,52.173,126.391,52.214,126.384,52.286,126.346,52.306,126.324,52.332,126.342,52.362,126.313,52.4,126.238,52.445,126.203,52.484,126.194,52.519,126.157,52.547,126.046,52.573,126.016,52.61,126.023,52.643,126.047,52.673,126.06,52.692,126.056,52.716,126.048,52.739,126.004,52.768,125.942,52.801,125.872,52.872,125.783,52.891,125.728,52.891,125.681,52.931,125.695,52.956,125.692,53.004,125.649,53.042,125.596,53.057,125.546,53.048,125.422,53.084,125.226,53.166,125.075,53.204,124.971,53.197,124.907,53.173,124.882,53.13,124.812,53.134,124.64,53.211,124.466,53.23,124.369,53.271,124.291,53.341,124.22,53.37,124.154,53.359,123.995,53.406,123.741,53.511,123.608,53.547,123.56,53.527,123.535,53.526,123.489,53.529,123.424,53.531,123.31,53.556,123.154,53.545,122.958,53.498,122.745,53.469,122.516,53.457,122.38,53.463,122.338,53.485,122.089,53.451,121.405,53.317]]},{"n":"Helmand","z":5,"b":[61.604,30.192,68.489,34.572],"l":[[61.604,31.204,61.754,31.014,61.817,30.802,61.842,30.506,62.031,30.31,62.383,30.216,62.685,30.192,62.937,30.238,63.194,30.342,63.457,30.503,63.663,30.57,63.812,30.543,63.908,30.538,63.949,30.557,64.002,30.617,64.065,30.719,64.12,30.872,64.166,31.075,64.236,31.239,64.328,31.366,64.371,31.488,64.366,31.578,64.391,31.657,64.455,31.728,64.64,31.847,64.801,31.999,64.932,32.181,65.064,32.289,65.195,32.322,65.332,32.486,65.475,32.782,65.62,32.964,65.842,33.065,65.937,33.121,66.017,33.202,66.114,33.326,66.193,33.393,66.255,33.402,66.309,33.441,66.356,33.509,66.452,33.585,66.599,33.668,66.728,33.716,66.84,33.731,66.959,33.792,67.086,33.898,67.172,33.952,67.216,33.953,67.233,33.989,67.222,34.057,67.229,34.109,67.266,34.157,67.498,34.306,67.734,34.395,68.212,34.513,68.307,34.493,68.376,34.507,68.489,34.572]]},{"n":"Holston","z":4.7,"b":[-83.974,35.87,-81.42,37.009],"l":[[-81.42,37.009,-81.695,36.932,-81.875,36.853,-82.136,36.762,-82.589,36.61,-82.665,36.522,-82.936,36.401,-83.026,36.37,-83.193,36.324,-83.387,36.256,-83.454,36.2,-83.747,36.076,-83.9,35.978,-83.974,35.915,-83.969,35.87]]},{"n":"Red","z":5,"b":[100.183,20.289,106.518,25.443],"l":[[106.518,20.289,106.387,20.348,106.329,20.356,106.288,20.34,106.27,20.347,106.276,20.377,106.264,20.398,106.234,20.408,106.196,20.466,106.149,20.572,106.109,20.623,106.076,20.619,106.023,20.665,105.951,20.761,105.909,20.861,105.897,20.964,105.836,21.051,105.725,21.122,105.626,21.162,105.538,21.17,105.477,21.203,105.426,21.292,105.325,21.273,105.275,21.301,105.241,21.37,105.216,21.389,105.197,21.358,105.118,21.43,104.977,21.604,104.692,21.875,104.052,22.424,103.479,22.902,103.161,23.121,102.906,23.24,102.614,23.353,102.119,23.514,101.892,23.699,101.745,23.857,101.601,24.049,101.507,24.208,101.463,24.335,101.41,24.434,101.349,24.505,101.292,24.54,101.238,24.538,101.118,24.655,100.931,24.891,100.812,25.003,100.76,24.993,100.642,25.021,100.459,25.088,100.351,25.15,100.303,25.235,100.183,25.443]]},{"n":"Hongshui","z":4,"b":[106.944,23.662,109.529,25.225],"l":[[106.944,25.225,106.963,25.223,106.974,25.2,106.982,25.134,107.019,25.061,107.063,25.029,107.156,25.009,107.277,24.875,107.315,24.796,107.312,24.729,107.362,24.603,107.467,24.417,107.542,24.318,107.586,24.308,107.608,24.281,107.608,24.236,107.578,24.189,107.516,24.139,107.494,24.087,107.512,24.031,107.547,23.986,107.601,23.954,107.643,23.945,107.674,23.961,107.733,23.905,107.821,23.776,107.902,23.722,107.975,23.744,108.039,23.782,108.094,23.836,108.131,23.857,108.152,23.843,108.223,23.889,108.343,23.995,108.453,24.035,108.552,24.007,108.631,23.96,108.691,23.891,108.758,23.85,108.833,23.836,108.865,23.806,108.855,23.758,108.873,23.71,108.919,23.662,108.999,23.663,109.112,23.712,109.192,23.723,109.238,23.695,109.281,23.688,109.322,23.7,109.369,23.746,109.423,23.825,109.469,23.847,109.529,23.796]]},{"n":"Huang","z":3,"b":[96.164,33.356,114.831,40.875],"l":[[97.288,34.835,97.547,34.876],[97.763,35.088,97.934,35.082,98.028,35.034,98.101,34.94,98.197,34.874,98.318,34.835,98.382,34.798,98.396,34.747,98.4,34.674,98.384,34.631,98.348,34.593,98.376,34.531,98.469,34.445,98.563,34.39,98.661,34.367,98.713,34.38,98.72,34.43,98.752,34.461,98.809,34.473,98.892,34.462,99.001,34.428,99.058,34.301,99.061,34.081,99.123,33.915,99.245,33.803,99.324,33.748,99.359,33.751,99.381,33.771,99.391,33.809,99.439,33.812,99.526,33.781,99.62,33.775,99.721,33.791,99.868,33.744,100.062,33.631,100.191,33.604,100.256,33.662,100.278,33.709,100.33,33.757,100.434,33.799,100.587,33.883,100.689,33.926,100.74,33.928,100.825,33.878,100.942,33.777,101.009,33.764,101.026,33.839,101.123,33.827,101.301,33.727,101.481,33.678,101.663,33.679,101.809,33.611,101.919,33.473,102.005,33.388,102.066,33.356,102.193,33.371,102.385,33.434,102.482,33.484,102.482,33.521,102.45,33.557,102.387,33.592,102.353,33.638,102.33,33.725,102.316,33.745,102.285,33.8,102.206,33.897,102.103,33.959,101.976,33.986,101.838,34.047,101.687,34.143,101.517,34.219,101.328,34.273,101.21,34.35,101.163,34.45,101.087,34.513,100.982,34.539,100.912,34.593,100.878,34.677,100.815,34.708,100.721,34.686,100.63,34.708,100.54,34.773,100.455,34.813,100.376,34.827,100.307,34.936,100.22,35.241,100.259,35.282,100.236,35.338,100.166,35.424,100.172,35.543,100.255,35.695,100.334,35.774,100.409,35.777,100.546,35.869,100.744,36.05,100.898,36.142,101.009,36.145,101.071,36.135,101.085,36.112,101.176,36.089,101.343,36.065,101.495,36.086,101.63,36.151,101.763,36.152,101.893,36.086,101.993,35.997,102.064,35.886,102.185,35.845,102.356,35.873,102.61,35.86,102.948,35.806,103.177,35.812,103.299,35.878,103.317,35.931,103.232,35.97,103.256,36.04,103.388,36.139,103.56,36.158,103.771,36.096,103.916,36.068,103.995,36.075,104.031,36.104,104.023,36.156,104.072,36.229,104.179,36.322,104.352,36.428,104.591,36.547,104.713,36.628,104.72,36.671,104.69,36.728,104.625,36.798,104.528,36.85,104.4,36.884,104.327,36.946,104.308,37.035,104.314,37.093,104.353,37.174,104.446,37.197,104.52,37.241,104.576,37.307,104.692,37.359,104.957,37.415,104.998,37.461,105.152,37.483,105.433,37.495,105.649,37.522,105.8,37.565,105.905,37.661,105.964,37.81,106.037,37.916,106.125,37.977,106.196,38.064,106.251,38.177,106.338,38.282,106.457,38.378,106.531,38.498,106.559,38.64,106.64,38.78,106.774,38.918,106.851,39.017,106.871,39.075,106.849,39.183,106.783,39.34,106.759,39.483,106.776,39.611,106.768,39.76,106.718,40.016,107.043,40.286,107.149,40.396,107.144,40.434,107.174,40.5,107.239,40.592,107.38,40.671,107.598,40.738,107.72,40.793,107.745,40.837,107.862,40.865,108.07,40.875,108.19,40.863,108.224,40.83,108.29,40.815,108.389,40.816,108.476,40.778,108.553,40.7,108.651,40.632,108.77,40.573,108.884,40.547,108.992,40.554,109.134,40.535,109.309,40.49,109.45,40.489,109.555,40.53,109.635,40.532,109.687,40.494,109.836,40.491,110.08,40.524,110.303,40.484,110.505,40.373,110.675,40.302,110.811,40.271,110.919,40.266,110.996,40.286,111.125,40.235,111.307,40.11,111.407,40.013,111.426,39.942,111.416,39.867,111.378,39.789,111.385,39.718,111.437,39.653,111.433,39.576,111.373,39.488,111.284,39.43,111.212,39.414,111.168,39.4,111.156,39.37,111.155,39.347,111.192,39.297,111.199,39.24,111.145,39.125,111.081,39.044,111.009,38.997,110.98,38.956,110.996,38.92,110.97,38.832,110.904,38.692,110.878,38.6,110.891,38.557,110.798,38.443,110.599,38.258,110.503,38.113,110.511,38.008,110.585,37.881,110.725,37.732,110.782,37.614,110.758,37.525,110.719,37.465,110.664,37.434,110.644,37.384,110.658,37.316,110.61,37.217,110.5,37.087,110.427,37.021,110.391,37.019,110.384,37.005,110.407,36.98,110.408,36.919,110.387,36.823,110.393,36.763,110.425,36.739,110.428,36.717,110.403,36.698,110.417,36.648,110.471,36.566,110.486,36.429,110.46,36.237,110.49,36.006,110.576,35.733,110.579,35.534,110.459,35.343,110.366,35.234,110.316,35.135,110.278,35.008,110.262,34.868,110.27,34.637,110.465,34.597,110.619,34.604,110.795,34.645,110.89,34.683,110.97,34.724,111.027,34.746,111.101,34.795,111.243,34.83,111.453,34.852,111.589,34.899,111.651,34.972,111.736,35.03,111.844,35.074,112.118,35.022,112.559,34.872,112.857,34.809,113.012,34.831,113.156,34.875,113.288,34.94,113.399,34.974,113.489,34.978,113.617,34.954,113.784,34.902,113.9,34.895,113.965,34.931,114.156,34.936,114.473,34.91,114.681,34.928,114.798,34.978,114.831,35.02],[96.164,35.13,96.748,35.053,97.072,34.931]]},{"n":"Yellow","z":3,"b":[114.838,35.02,119.036,37.824],"l":[[114.838,35.02,114.862,35.061,114.866,35.145,114.905,35.229,115.014,35.33,115.136,35.406,115.272,35.459,115.34,35.503,115.341,35.538,115.382,35.597,115.463,35.681,115.596,35.758,115.781,35.83,115.871,35.88,115.866,35.91,115.883,35.939,115.924,35.966,115.966,35.976,116.036,36.071,116.078,36.11,116.116,36.143,116.227,36.201,116.451,36.339,116.581,36.44,116.593,36.49,116.644,36.561,116.735,36.656,116.862,36.726,117.024,36.773,117.151,36.862,117.24,36.994,117.352,37.082,117.485,37.127,117.571,37.175,117.608,37.228,117.693,37.256,117.825,37.26,117.97,37.29,118.128,37.347,118.238,37.431,118.298,37.541,118.385,37.604,118.498,37.619,118.645,37.678,118.826,37.78,118.946,37.824,119.036,37.809]]},{"n":"Hudson","z":5,"b":[-74.178,41.25,-73.617,44.1],"l":[[-74.074,44.1,-74.064,44.016,-74.08,43.982,-74.119,43.972,-74.152,43.942,-74.176,43.894,-74.178,43.86,-74.146,43.832,-73.971,43.735,-73.888,43.661,-73.839,43.576,-73.83,43.506,-73.862,43.451,-73.867,43.383,-73.846,43.302,-73.781,43.274,-73.671,43.297,-73.617,43.232,-73.622,43,-73.728,42.714,-73.767,42.544,-73.773,42.394,-73.818,42.265,-73.901,42.159,-73.944,42.02,-73.948,41.765,-73.984,41.574,-73.984,41.46,-73.96,41.331,-73.969,41.281,-73.97,41.25]]},{"n":"Ideriyn","z":2.1,"b":[97.413,47.999,100.7,49.263],"l":[[98.007,47.999,97.617,48.075,97.452,48.141,97.413,48.202,97.444,48.279,97.454,48.357,97.443,48.435,97.529,48.541,97.711,48.673,98.05,48.736,98.547,48.73,98.923,48.697,99.179,48.638,99.588,48.725,100.152,48.958,100.473,49.076,100.551,49.077,100.613,49.098,100.659,49.139,100.686,49.185,100.7,49.263]]},{"n":"Indaiá","z":4,"b":[-46.499,-20.352,-45.089,-19.051],"l":[[-46.499,-20.321,-46.322,-20.352,-46.097,-20.348,-46.002,-20.337,-45.961,-20.318,-45.947,-20.296,-45.961,-20.273,-45.943,-20.231,-45.896,-20.173,-45.819,-20.14,-45.714,-20.133,-45.64,-20.068,-45.598,-19.945,-45.554,-19.87,-45.506,-19.841,-45.492,-19.8,-45.513,-19.746,-45.496,-19.688,-45.443,-19.626,-45.395,-19.524,-45.352,-19.383,-45.281,-19.284,-45.182,-19.227,-45.121,-19.161,-45.089,-19.051]]},{"n":"Indigirka","z":4.7,"b":[141.885,62.507,150.098,71.227],"l":[[144.713,62.766,144.441,63.009,144.316,63.091,144.249,63.092,143.974,63.178,143.492,63.351,143.055,63.47,142.663,63.536,142.446,63.599,142.402,63.658,142.3,63.729,142.058,63.851,141.915,63.954,141.885,64.037,141.921,64.136,142.012,64.215,142.159,64.276,142.366,64.293,142.634,64.269,142.789,64.303,142.831,64.396,142.84,64.459,142.862,64.501,142.917,64.532,143.151,64.579,143.267,64.633,143.265,64.694,143.295,64.748,143.356,64.793,143.338,64.871,143.243,64.983,143.161,65.055,143.092,65.088,143.085,65.134,143.14,65.195,143.157,65.24,143.161,65.277,143.169,65.312,143.218,65.344,143.23,65.371,143.208,65.389,143.134,65.404,142.814,65.424,142.637,65.461,142.603,65.515,142.618,65.592,142.68,65.692,142.811,65.789,143.011,65.884,143.147,66.017,143.219,66.189,143.223,66.309,143.158,66.377,143.106,66.466,143.068,66.574,142.987,66.65,142.865,66.693,142.754,66.813,142.654,67.01,142.615,67.16,142.635,67.265,142.599,67.314,142.574,67.341,142.578,67.363,142.618,67.395,142.636,67.425,142.655,67.452,142.845,67.503,143.337,67.6,143.647,67.637,143.776,67.611,144.015,67.604,144.362,67.614,144.574,67.626,144.65,67.639,144.695,67.661,144.708,67.692,144.671,67.714,144.582,67.726,144.527,67.75,144.507,67.785,144.594,67.814,144.885,67.847,145.025,67.871,145.059,67.908,145.067,67.954,145.093,67.98,145.167,67.985,145.238,67.995,145.282,68.023,145.279,68.047,145.278,68.082,145.313,68.111,145.346,68.142,145.358,68.179,145.423,68.2,145.539,68.192,145.644,68.218,145.653,68.267,145.715,68.293,145.809,68.288,145.856,68.304,145.869,68.351,145.913,68.383,145.901,68.431,145.969,68.498,146.071,68.547,146.206,68.578,146.358,68.578,146.528,68.547,146.646,68.545,146.711,68.571,146.809,68.581,146.94,68.573,147.022,68.624,147.056,68.735,147.144,68.801,147.286,68.821,147.346,68.855,147.325,68.903,147.327,68.93,147.354,68.939,147.422,68.934,147.483,68.942,147.492,68.977,147.463,69.003,147.444,69.072,147.488,69.147,147.471,69.221,147.392,69.295,147.434,69.352,147.597,69.392,147.638,69.44,147.598,69.508,147.575,69.557,147.575,69.586,147.568,69.599,147.538,69.619,147.445,69.642,147.429,69.656,147.423,69.668,147.435,69.677,147.463,69.695,147.474,69.73,147.451,69.773,147.466,69.847,147.519,69.953,147.519,70.004,147.496,70.023,147.453,70.039,147.39,70.067,147.355,70.111,147.352,70.144,147.385,70.167,147.393,70.195,147.365,70.225,147.379,70.272,147.489,70.336,147.538,70.38,147.525,70.402,147.538,70.425,147.576,70.449,147.657,70.481,147.783,70.522,147.853,70.56,147.867,70.595,148.08,70.635,148.492,70.679,148.709,70.73,148.732,70.787,148.941,70.837,149.333,70.881,149.561,70.925,149.622,70.97,149.646,71.002,149.662,71.03,149.691,71.046,149.848,71.083,149.913,71.111,149.893,71.131,149.892,71.152,149.91,71.174,149.984,71.209,150.098,71.227],[144.712,62.768,145.009,62.507]]},{"n":"Indragi","z":5,"b":[100.55,-0.675,103.405,-0.331],"l":[[100.55,-0.566,100.66,-0.572,100.724,-0.602,100.779,-0.657,100.84,-0.675,100.937,-0.647,101.007,-0.659,101.043,-0.638,101.07,-0.59,101.115,-0.586,101.179,-0.624,101.259,-0.636,101.356,-0.622,101.424,-0.627,101.483,-0.632,101.534,-0.611,101.631,-0.51,101.77,-0.486,101.953,-0.539,102.097,-0.535,102.256,-0.442,102.372,-0.356,102.521,-0.378,102.742,-0.481,102.843,-0.506,102.937,-0.479,103.105,-0.374,103.229,-0.331,103.346,-0.36,103.405,-0.362]]},{"n":"Indus","z":3,"b":[67.454,23.848,79.735,35.836],"l":[[67.86,23.903,67.925,24.001],[67.646,23.92,67.711,23.957,67.714,24.001,67.713,24.066],[67.925,24.001,68,24.007,68.017,23.991,68.025,23.934,68.042,23.892,68.037,23.848],[67.925,24.001,67.924,24.056,67.801,24.059,67.76,24.053,67.713,24.066],[67.713,24.066,67.661,24.105],[79.735,32.444,79.43,32.745],[79.437,32.738,79.341,32.881,79.22,32.996,79.044,33.133,78.907,33.189,78.809,33.165,78.64,33.208,78.245,33.398,78.183,33.445,78.152,33.493,78.15,33.542,78.077,33.62,77.931,33.727,77.792,33.857,77.66,34.011,77.53,34.106,77.339,34.16,76.967,34.288,76.782,34.386,76.661,34.498,76.529,34.583,76.388,34.639,76.285,34.708,76.222,34.789,76.196,34.861,76.209,34.924,76.142,35.008,75.995,35.113,75.902,35.21,75.863,35.297,75.791,35.335,75.688,35.321,75.568,35.357,75.431,35.443,75.353,35.511,75.333,35.561,75.264,35.59,75.145,35.598,75.001,35.632,74.83,35.694,74.74,35.753,74.73,35.812,74.701,35.836,74.654,35.826,74.631,35.748,74.632,35.601,74.575,35.5,74.461,35.445,74.342,35.415,74.22,35.408,74.058,35.436,73.857,35.497,73.645,35.531,73.423,35.538,73.302,35.471,73.28,35.33,73.175,35.204,72.986,35.093,72.885,35.004,72.872,34.939,72.895,34.886,72.955,34.845,72.945,34.809,72.864,34.777,72.814,34.726,72.782,34.623],[72.707,34.086,72.652,34.032,72.592,34.015,72.509,34.019,72.411,34,72.297,33.959,72.239,33.895,72.236,33.809,72.197,33.772,72.124,33.784,72.071,33.779,72.038,33.757,71.986,33.665,71.914,33.504,71.839,33.406,71.762,33.372,71.722,33.274,71.72,33.111,71.651,32.994,71.516,32.923,71.425,32.838,71.355,32.689,71.344,32.484,71.273,32.311,71.07,31.999,70.861,31.569,70.801,31.3,70.829,30.92,70.77,30.811,70.775,30.716,70.824,30.6,70.83,30.485,70.793,30.372,70.792,30.212,70.827,30.004,70.792,29.825,70.688,29.676,70.657,29.55,70.699,29.45,70.702,29.353,70.666,29.261,70.651,29.176,70.657,29.098,70.612,29.023,70.515,28.952,70.443,28.916,70.397,28.915,70.287,28.835,70.115,28.677,69.962,28.576,69.829,28.531,69.715,28.444,69.621,28.315,69.535,28.246,69.458,28.237,69.409,28.212,69.389,28.172,69.356,28.157,69.311,28.165,69.249,28.141,69.171,28.085,69.107,28.058,69.059,28.061,69.026,28.04,69.008,27.995,68.98,27.975,68.942,27.981,68.929,27.955,68.943,27.897,68.928,27.853,68.883,27.823,68.884,27.792,68.932,27.761,68.95,27.737,68.938,27.719,68.826,27.678,68.762,27.698,68.705,27.759,68.669,27.771,68.654,27.731,68.573,27.688,68.426,27.641,68.337,27.603,68.307,27.572,68.3,27.511,68.318,27.42,68.297,27.371,68.239,27.366,68.194,27.342,68.162,27.3,68.16,27.263,68.188,27.231,68.175,27.208,68.121,27.193,68.099,27.163,68.109,27.117,68.063,27.062,67.963,26.998,67.911,26.91,67.905,26.742,67.891,26.487,67.9,26.369,67.926,26.302,67.982,26.243,68.068,26.192,68.117,26.14,68.13,26.088,68.202,25.999,68.332,25.873,68.379,25.795,68.341,25.765,68.348,25.728,68.4,25.684,68.398,25.621,68.342,25.539,68.323,25.444,68.34,25.334,68.337,25.262,68.313,25.226,68.319,25.192,68.353,25.158,68.352,25.139,68.314,25.132,68.286,25.089,68.269,25.009,68.245,24.979,68.214,24.997,68.183,24.973,68.155,24.907,68.116,24.873,68.068,24.87,68.025,24.76,67.986,24.544,67.936,24.43,67.874,24.418,67.85,24.396,67.863,24.363,67.849,24.344,67.806,24.339,67.796,24.318,67.818,24.282,67.787,24.239,67.702,24.191,67.659,24.146,67.659,24.104,67.627,24.078,67.532,24.061,67.454,24.04]]},{"n":"Irrawaddy Delta","z":2.1,"b":[94.656,15.839,96.189,17.908],"l":[[94.703,16.512,94.728,16.673,94.744,16.783,94.742,16.908,94.795,17.035,94.903,17.164,94.954,17.257,94.948,17.315,94.977,17.352,95.042,17.371,95.12,17.472,95.21,17.656,95.304,17.766,95.455,17.856,95.482,17.88,95.52,17.908,95.575,17.904,95.633,17.81,95.694,17.625,95.783,17.438,95.899,17.251,95.956,17.13,95.952,17.074,96.042,16.969,96.09,16.847,96.189,16.768],[95.481,16.814,95.323,16.328,95.237,16.133,95.133,16.025,95.099,15.958,95.078,15.839],[95.6,16.902,95.649,16.768,95.719,16.709,95.829,16.653,95.872,16.593,95.847,16.529,95.845,16.478,95.867,16.44,95.91,16.412,95.994,16.39,96.043,16.34],[95.472,17.744,95.406,17.718,95.356,17.666,95.296,17.554,95.291,17.502,95.323,17.468,95.34,17.431,95.335,17.379,95.317,17.348,95.276,17.323,95.263,17.302,95.253,17.224,95.248,17.178,95.213,17.144,95.186,17.078,95.166,16.979,95.175,16.903,95.213,16.852,95.222,16.796,95.176,16.674,95.145,16.61,95.107,16.574,95.061,16.566,95.014,16.503,94.968,16.388,94.907,16.288,94.794,16.162,94.748,16.08,94.656,15.989]]},{"n":"Irtysh","z":2.1,"b":[68.215,53.986,74.994,61.124],"l":[[74.994,53.986,74.918,54.003,74.904,54.028,74.924,54.066,74.872,54.128,74.745,54.214,74.66,54.291,74.617,54.36,74.558,54.4,74.484,54.41,74.4,54.449,74.305,54.517,74.125,54.586,73.728,54.694],[73.722,54.694,73.539,54.805,73.434,54.845,73.347,54.85,73.318,54.888,73.346,54.96,73.308,55.009,73.203,55.036,73.157,55.097,73.17,55.193,73.128,55.242,73.029,55.245,72.993,55.266,73.02,55.305,73.163,55.389,73.424,55.518,73.662,55.618,73.878,55.689,74.081,55.729,74.272,55.739,74.469,55.833,74.672,56.011,74.709,56.058,74.709,56.092,74.676,56.126,74.67,56.148,74.69,56.162,74.699,56.203,74.681,56.264,74.698,56.286,74.729,56.289,74.737,56.317,74.721,56.337,74.74,56.371,74.749,56.396,74.735,56.419,74.706,56.427,74.678,56.478,74.68,56.563,74.656,56.607,74.606,56.609,74.59,56.627,74.609,56.663,74.595,56.693,74.577,56.717,74.585,56.734,74.597,56.757,74.552,56.845,74.511,56.893,74.456,56.923,74.387,56.936,74.366,56.95,74.334,56.988,74.181,57.017,74.117,57.053,74.142,57.096,74.119,57.133,74.049,57.163,73.965,57.174,73.869,57.166,73.77,57.179,73.669,57.213,73.635,57.239,73.633,57.258,73.599,57.272,73.504,57.265,73.441,57.278,73.436,57.306,73.371,57.327,73.247,57.342,73.18,57.362,73.171,57.388,73.113,57.408,73.007,57.422,72.949,57.441,72.903,57.458,72.867,57.447,72.78,57.442,72.625,57.472,72.529,57.502,72.492,57.52,72.45,57.534,72.413,57.529,72.389,57.536,72.334,57.565,72.231,57.579,71.951,57.581,71.855,57.608,71.82,57.63,71.751,57.643,71.606,57.635,71.527,57.645,71.515,57.674,71.473,57.699,71.4,57.719,71.312,57.721,71.209,57.704,71.108,57.71,71.007,57.74,70.964,57.764,70.92,57.765,70.873,57.761,70.785,57.777,70.683,57.822,70.615,57.821,70.597,57.823,70.569,57.851,70.526,57.859,70.468,57.847,70.425,57.861,70.398,57.902,70.396,57.928,70.373,57.957,70.294,57.977,70.225,57.977,70.176,57.954,70.137,57.959,70.107,57.99,70.053,58.009,69.974,58.015,69.877,58.001,69.763,57.967,69.649,57.96,69.536,57.98,69.48,58,69.427,58.017,69.316,57.995,69.239,58,69.197,58.032,69.14,58.032,69.07,58,69,58,68.931,58.032,68.871,58.037,68.818,58.014,68.73,58.037,68.605,58.108,68.464,58.149,68.226,58.168],[68.861,61.124,69.011,61.019,69.161,60.976,69.362,60.959,69.516,60.885,69.623,60.755,69.724,60.683,69.802,60.668,69.83,60.65,69.836,60.621,69.816,60.604,69.782,60.567,69.769,60.512,69.801,60.463,69.804,60.419,69.778,60.38,69.785,60.354,69.825,60.342,69.854,60.311,69.874,60.259,69.862,60.239,69.821,60.237,69.77,60.207,69.729,60.17,69.721,60.126,69.738,60.095,69.781,60.078,69.854,60.058,69.888,60.005,69.866,59.943,69.865,59.901,69.888,59.866,69.871,59.835,69.82,59.839,69.777,59.817,69.757,59.766,69.699,59.744,69.601,59.75,69.498,59.727,69.334,59.65,69.284,59.577,69.223,59.555,69.133,59.561,69.075,59.536,69.047,59.481,68.997,59.452,68.925,59.451,68.911,59.43,68.929,59.402,68.905,59.354,68.831,59.292,68.799,59.24,68.81,59.198,68.843,59.173,68.882,59.161,68.882,59.138,68.862,59.12,68.854,59.084,68.873,59.057,68.856,59.021,68.812,59.013,68.801,59.002,68.821,58.967,68.812,58.935,68.762,58.926,68.749,58.9,68.75,58.877,68.737,58.859,68.697,58.853,68.689,58.827,68.705,58.788,68.671,58.764,68.627,58.77,68.569,58.753,68.556,58.679,68.564,58.607,68.566,58.562,68.549,58.521,68.469,58.512,68.399,58.5,68.355,58.47,68.331,58.431,68.328,58.383,68.283,58.355,68.24,58.342,68.219,58.328,68.215,58.29,68.233,58.246,68.222,58.166]]},{"n":"Ishikari","z":5,"b":[141.358,43.13,143.066,43.895],"l":[[143.066,43.7,142.885,43.81,142.722,43.878,142.632,43.895,142.56,43.883,142.481,43.849,142.396,43.792,142.254,43.738,141.943,43.648,141.909,43.623,141.884,43.564,141.861,43.428,141.71,43.329,141.644,43.254,141.613,43.169,141.567,43.13,141.508,43.135,141.448,43.163,141.358,43.238]]},{"n":"Ismailiya Canal","z":5,"b":[31.238,30.121,32.303,30.593],"l":[[31.238,30.121,31.282,30.129,31.324,30.18,31.378,30.278,31.483,30.379,31.64,30.484,31.865,30.551,32.303,30.593]]},{"n":"Japurá","z":4.7,"b":[-69.462,-3.222,-64.774,-1.231],"l":[[-69.462,-1.231,-69.452,-1.299,-69.447,-1.448,-69.339,-1.605,-69.128,-1.754,-68.914,-1.818,-68.699,-1.797,-68.458,-1.748,-68.191,-1.672,-67.964,-1.639,-67.776,-1.651,-67.629,-1.685,-67.525,-1.742,-67.361,-1.798,-67.138,-1.854,-66.805,-1.823,-66.363,-1.706,-65.981,-1.716,-65.658,-1.853,-65.445,-1.905,-65.344,-1.869,-65.268,-1.904,-65.193,-2.063,-65.227,-2.138,-65.18,-2.266,-65.066,-2.472,-65.009,-2.606,-65.011,-2.667,-64.979,-2.709,-64.914,-2.733,-64.889,-2.777,-64.903,-2.842,-64.875,-2.927,-64.802,-3.031,-64.774,-3.109,-64.79,-3.222]]},{"n":"Jequitinhonha","z":5,"b":[-43.665,-18.294,-38.885,-15.821],"l":[[-43.409,-18.294,-43.532,-18.104,-43.559,-18.012,-43.515,-17.923,-43.476,-17.839,-43.494,-17.818,-43.558,-17.832,-43.611,-17.786,-43.655,-17.68,-43.665,-17.608,-43.642,-17.57,-43.57,-17.505,-43.488,-17.496,-43.365,-17.507,-43.299,-17.476,-43.233,-17.418,-43.213,-17.339,-43.181,-17.304,-43.118,-17.279,-43.048,-17.212,-42.972,-17.105,-42.905,-17.046,-42.846,-17.035,-42.777,-16.96,-42.698,-16.819,-42.555,-16.716,-42.347,-16.651,-42.178,-16.651,-42.046,-16.717,-41.919,-16.703,-41.796,-16.61,-41.687,-16.562,-41.548,-16.56,-41.332,-16.524,-41.236,-16.486,-41.187,-16.435,-41.124,-16.416,-41.044,-16.43,-40.908,-16.355,-40.715,-16.19,-40.52,-16.116,-40.324,-16.134,-40.17,-16.125,-40.059,-16.092,-39.99,-16.056,-39.963,-16.019,-39.84,-15.986,-39.513,-15.942,-39.402,-16.025,-39.323,-16.04,-39.237,-16.015,-39.164,-15.955,-39.104,-15.86,-39.021,-15.821,-38.949,-15.825,-38.885,-15.842]]},{"n":"Yangtze","z":2,"b":[98.542,25.981,103.859,31.69],"l":[[99.399,28.12,99.431,27.842,99.494,27.642,99.6,27.426,99.727,27.246,99.874,27.101,99.954,26.994,99.967,26.922,99.996,26.904,100.041,26.94,100.063,27.012,100.063,27.12,100.109,27.221,100.199,27.314,100.232,27.399,100.206,27.474,100.219,27.554,100.27,27.64,100.296,27.721,100.298,27.795,100.33,27.834,100.39,27.837,100.427,27.754,100.442,27.584,100.472,27.484,100.519,27.454,100.545,27.393,100.55,27.301,100.523,27.238,100.463,27.202,100.45,27.169,100.481,27.139,100.486,27.05,100.463,26.901,100.437,26.817,100.408,26.797,100.411,26.691,100.444,26.499,100.449,26.378,100.425,26.326,100.428,26.279,100.459,26.236,100.582,26.203,100.798,26.178,100.961,26.211,101.071,26.301,101.2,26.357,101.349,26.381,101.447,26.442,101.495,26.541,101.59,26.593,101.734,26.599,101.841,26.554,101.912,26.46,101.94,26.363,101.925,26.261,101.899,26.193,101.863,26.157,101.847,26.098,101.852,26.016,101.912,25.981,102.027,25.994,102.126,26.057,102.21,26.171,102.331,26.266,102.489,26.341,102.583,26.37,102.612,26.353,102.622,26.324,102.614,26.281,102.635,26.252,102.686,26.237,102.753,26.263,102.836,26.331,102.905,26.365,102.961,26.365,102.99,26.395,102.994,26.455,103.011,26.498,103.042,26.523,103.018,26.633,102.94,26.828,102.897,27.013,102.89,27.189,102.903,27.313,102.938,27.385,102.969,27.412,102.998,27.395,103.046,27.398,103.147,27.434,103.2,27.534,103.237,27.572,103.276,27.583,103.293,27.601,103.287,27.627,103.326,27.67,103.41,27.73,103.479,27.814,103.535,27.922,103.532,27.992,103.471,28.026,103.444,28.065,103.451,28.111,103.484,28.161,103.589,28.25,103.626,28.264,103.663,28.257,103.702,28.229,103.762,28.243,103.841,28.298,103.859,28.377,103.816,28.48,103.809,28.565,103.851,28.668],[98.542,31.69,98.799,31.438,98.855,31.323,98.797,31.261,98.738,31.252,98.679,31.295,98.638,31.28,98.615,31.206,98.653,31.125,98.753,31.036,98.799,30.963,98.791,30.908,98.831,30.842,98.917,30.766,98.947,30.711,98.919,30.68,98.943,30.477,99.018,30.102,99.039,29.849,99.004,29.717,99.014,29.551,99.096,29.25,99.146,28.641,99.221,28.36,99.399,28.126]]},{"n":"Jordan","z":5,"b":[35.401,31.23,35.864,33.596],"l":[[35.424,31.325,35.401,31.23],[35.577,32.72,35.551,32.396,35.572,32.238,35.535,32.103,35.532,31.985,35.559,31.766],[35.864,33.596,35.696,33.431,35.619,33.244,35.595,33.111,35.599,32.9]]},{"n":"Jubba","z":5,"b":[41.966,-0.251,42.751,4.324],"l":[[41.966,4.324,42.029,4.291,42.056,4.255,42.067,4.206,42.121,4.173,42.219,4.155,42.342,4.078,42.49,3.943,42.542,3.795,42.499,3.635,42.497,3.522,42.536,3.456,42.533,3.398,42.488,3.35,42.473,3.28,42.487,3.188,42.516,3.13,42.56,3.107,42.575,3.07,42.562,3.02,42.502,2.942,42.393,2.838,42.346,2.749,42.36,2.677,42.345,2.614,42.302,2.559,42.295,2.503,42.322,2.444,42.316,2.388,42.274,2.333,42.229,2.319,42.179,2.348,42.145,2.345,42.126,2.312,42.144,2.28,42.2,2.252,42.205,2.221,42.157,2.188,42.177,2.135,42.265,2.061,42.297,1.993,42.275,1.929,42.306,1.835,42.389,1.711,42.426,1.617,42.416,1.553,42.424,1.513,42.425,1.462,42.462,1.375,42.561,1.219,42.605,1.07,42.595,0.929,42.626,0.808,42.698,0.705,42.738,0.581,42.751,0.362,42.73,0.135,42.685,0.015,42.608,-0.072,42.585,-0.148,42.634,-0.251]]},{"n":"Juruena","z":4.7,"b":[-59.077,-14.532,-58.622,-10.975],"l":[[-59.07,-14.532,-59.036,-14.373,-59.038,-14.249,-59.077,-14.038,-59.041,-13.979,-59.026,-13.821,-59.019,-13.544,-58.997,-13.367,-58.96,-13.289,-58.933,-12.998,-58.915,-12.494,-58.863,-12.181,-58.778,-12.058,-58.731,-11.92,-58.724,-11.769,-58.699,-11.657,-58.654,-11.586,-58.628,-11.503,-58.622,-11.407,-58.633,-11.364,-58.669,-11.33,-58.68,-11.234,-58.659,-10.975]]},{"n":"Kadéï","z":5,"b":[14.243,1.654,16.263,5.865],"l":[[14.617,5.865,14.617,5.496,14.493,5.287,14.463,5.182,14.485,5.111,14.437,5.03,14.319,4.941,14.254,4.797,14.243,4.598,14.297,4.43,14.417,4.292,14.498,4.23,14.539,4.229,14.57,4.211,14.574,4.189,14.623,4.172,14.68,4.205,14.754,4.228,14.827,4.23,14.873,4.203,14.894,4.147,14.957,4.118,15.064,4.116,15.147,4.091,15.236,4.019,15.368,4.055,15.451,4.046,15.531,4.005,15.578,3.931,15.593,3.825,15.651,3.756,15.755,3.724,15.881,3.65,16.032,3.535,16.108,3.4,16.11,3.246,16.158,3.103,16.252,2.971,16.263,2.924,16.245,2.884,16.174,2.824,16.118,2.716,16.095,2.599,16.107,2.473,16.136,2.364,16.183,2.27,16.177,2.205,16.116,2.168,16.08,2.107,16.07,2.022,16.088,1.918,16.135,1.796,16.136,1.715,16.098,1.654]]},{"n":"Kafue","z":5,"b":[25.947,-15.953,28.973,-11.626],"l":[[27.157,-11.626,27.143,-11.749,27.183,-11.852,27.317,-12.039,27.366,-12.154,27.445,-12.255,27.568,-12.379,27.655,-12.429,27.706,-12.405,27.821,-12.42,28.002,-12.474,28.136,-12.585,28.223,-12.755,28.24,-12.943,28.185,-13.152,28.125,-13.284,28.06,-13.339,27.964,-13.368,27.837,-13.372,27.76,-13.408,27.733,-13.476,27.688,-13.516,27.626,-13.526,27.603,-13.573,27.616,-13.659,27.561,-13.769,27.436,-13.903,27.386,-14.012,27.412,-14.095,27.41,-14.165,27.38,-14.22,27.317,-14.279,27.222,-14.343,27.12,-14.383,26.959,-14.409,26.656,-14.44,26.5,-14.517,26.39,-14.648,26.292,-14.691,26.206,-14.644,26.151,-14.647,26.127,-14.701,26.131,-14.736,26.163,-14.754,26.179,-14.789,26.178,-14.841,26.112,-14.881,25.983,-14.907,25.947,-14.947,26.006,-14.999,26.015,-15.054,25.977,-15.111,25.967,-15.288,25.987,-15.585,26.048,-15.755,26.15,-15.797,26.285,-15.776,26.453,-15.692,26.568,-15.664,26.63,-15.69,26.748,-15.667,26.921,-15.595,27.08,-15.606,27.227,-15.698,27.373,-15.721,27.517,-15.673,27.573,-15.673,27.625,-15.685,27.643,-15.683,27.653,-15.665,27.675,-15.648,27.71,-15.646,27.894,-15.66,28.037,-15.704,28.17,-15.78,28.275,-15.81,28.352,-15.793,28.482,-15.826,28.663,-15.91,28.802,-15.953,28.973,-15.95]]},{"n":"Kagera","z":2,"b":[29.593,-3.365,31.768,-0.936],"l":[[29.593,-2.984,29.827,-3.047,29.901,-3.091,29.895,-3.137,29.909,-3.16,29.943,-3.16,29.976,-3.184,30.021,-3.256,29.99,-3.315,29.99,-3.345,30.01,-3.365,30.105,-3.315,30.274,-3.196,30.391,-3.088,30.455,-2.991,30.47,-2.92,30.433,-2.875,30.424,-2.824,30.441,-2.769,30.53,-2.679,30.691,-2.554,30.774,-2.463,30.779,-2.404,30.798,-2.363,30.829,-2.339,30.855,-2.265,30.877,-2.143,30.865,-2.044,30.819,-1.967,30.807,-1.851,30.828,-1.694,30.813,-1.563,30.762,-1.459,30.711,-1.397,30.632,-1.367,30.508,-1.208,30.47,-1.131,30.477,-1.083,30.52,-1.063,30.599,-1.07,30.673,-1.051,30.742,-1.008,30.809,-0.995,30.874,-1.013,30.942,-1.063,31.012,-1.143,31.125,-1.2,31.28,-1.234,31.409,-1.239,31.512,-1.213,31.573,-1.157,31.594,-1.072,31.645,-1.006,31.768,-0.936]]},{"n":"Kamchatka","z":5,"b":[157.632,54.081,162.461,56.398],"l":[[157.632,54.29,157.694,54.131,157.748,54.081,157.817,54.086,157.884,54.115,157.982,54.194,158.124,54.364,158.205,54.434,158.271,54.461,158.314,54.505,158.333,54.565,158.456,54.642,158.684,54.737,158.857,54.866,158.975,55.031,159.101,55.145,159.233,55.209,159.31,55.261,159.329,55.301,159.329,55.328,159.309,55.341,159.315,55.352,159.336,55.378,159.343,55.391,159.36,55.41,159.405,55.433,159.488,55.451,159.608,55.462,159.665,55.483,159.659,55.511,159.677,55.542,159.722,55.575,159.74,55.642,159.733,55.741,159.697,55.815,159.632,55.863,159.633,55.875,159.641,55.887,159.652,55.905,159.647,55.941,159.693,55.986,159.79,56.042,159.868,56.116,159.926,56.208,160.022,56.271,160.156,56.304,160.247,56.346,160.295,56.398,160.684,56.386,161.778,56.271,161.906,56.235,162.044,56.224,162.176,56.261,162.373,56.264,162.461,56.235]]},{"n":"Kapuas","z":5,"b":[109.15,-0.352,113.465,1.201],"l":[[113.465,1.201,113.233,0.965,112.954,0.855,112.794,0.823,112.66,0.834,112.536,0.802,112.421,0.728,112.252,0.672,112.028,0.636,111.95,0.599,111.945,0.546,111.917,0.503,111.84,0.462,111.821,0.43,111.827,0.408,111.802,0.375,111.736,0.33,111.681,0.321,111.645,0.311,111.632,0.272,111.618,0.242,111.58,0.242,111.556,0.221,111.542,0.158,111.501,0.104,111.465,0.098,111.404,0.106,111.367,0.089,111.329,0.098,111.304,0.102,111.282,0.082,111.263,0.088,111.232,0.106,111.206,0.089,111.156,0.079,111.077,0.092,111.024,0.082,110.997,0.048,110.933,0.023,110.833,0.008,110.772,0.029,110.732,0.046,110.696,0.033,110.653,0.043,110.613,0.06,110.58,0.06,110.568,0.034,110.544,0.02,110.517,0.023,110.485,-0.002,110.465,-0.038,110.418,-0.066,110.335,-0.081,110.177,-0.056,110.098,-0.07,110.007,-0.095,109.947,-0.185,109.81,-0.269,109.596,-0.346,109.522,-0.352,109.453,-0.337,109.403,-0.304,109.336,-0.267,109.219,-0.231,109.15,-0.186]]},{"n":"Kasai","z":3,"b":[16.191,-11.249,22.307,-3.052],"l":[[22.237,-11.249,22.217,-11.013,22.178,-10.892,22.203,-10.829,22.28,-10.784,22.307,-10.691,22.283,-10.552,22.282,-10.453,22.302,-10.397,22.275,-10.259,22.198,-10.041,22.089,-9.863,21.949,-9.726,21.857,-9.594,21.813,-9.469,21.829,-9.168,21.905,-8.693,21.896,-8.341,21.801,-8.112,21.78,-7.865,21.834,-7.602,21.842,-7.421,21.785,-7.275,21.692,-7.109,21.547,-6.964,21.206,-6.692,20.939,-6.535,20.783,-6.366,20.648,-6.134,20.58,-5.993,20.579,-5.943,20.638,-5.876,20.755,-5.793,20.835,-5.712,20.898,-5.596,21.038,-5.339,21.075,-5.102,21.055,-4.8,20.966,-4.611,20.808,-4.537,20.688,-4.455,20.606,-4.365,20.522,-4.309,20.464,-4.295,20.403,-4.3,20.375,-4.319,20.355,-4.362,20.323,-4.39,20.285,-4.39,20.145,-4.315,19.904,-4.162,19.742,-4.081,19.66,-4.072,19.41,-3.959,18.991,-3.743,18.74,-3.626,18.616,-3.601,18.51,-3.599,18.414,-3.551,18.292,-3.457,18.13,-3.412,17.928,-3.415,17.796,-3.391,17.735,-3.34,17.674,-3.313,17.613,-3.31,17.427,-3.234,17.355,-3.2,17.06,-3.059,16.978,-3.052,16.885,-3.073,16.745,-3.086,16.519,-3.07,16.364,-3.073,16.281,-3.097,16.231,-3.139,16.191,-3.194]]},{"n":"Katherine","z":5,"b":[131.844,-14.706,133.227,-13.298],"l":[[133.134,-13.298,133.2,-13.421,133.224,-13.507,133.227,-13.595,133.21,-13.664,133.173,-13.712,133.133,-13.736,133.069,-13.733,132.966,-13.705,132.9,-13.73,132.835,-13.801,132.781,-13.914,132.741,-14.069,132.695,-14.172,132.645,-14.223,132.568,-14.266,132.466,-14.302,132.302,-14.414,132.076,-14.602,131.933,-14.698,131.844,-14.706]]},{"n":"Kayan","z":5,"b":[114.762,1.76,117.569,2.929],"l":[[114.762,1.76,114.876,1.885,114.956,1.933,115.085,1.949,115.261,2.011,115.36,2.031,115.44,2.028,115.49,2.01,115.51,1.976,115.552,1.969,115.617,1.988,115.694,2.061,115.781,2.188,115.845,2.204,115.886,2.11,115.959,2.066,116.063,2.073,116.141,2.146,116.194,2.286,116.326,2.466,116.536,2.685,116.656,2.798,116.686,2.805,116.719,2.782,116.756,2.73,116.812,2.736,116.887,2.801,116.982,2.81,117.099,2.765,117.168,2.755,117.191,2.779,117.229,2.787,117.284,2.776,117.342,2.809,117.402,2.885,117.46,2.922,117.512,2.929,117.569,2.929]]},{"n":"Kem","z":5,"b":[31.57,63.766,34.803,65.111],"l":[[31.57,63.869,31.714,63.819,31.834,63.766,31.944,63.767,32.236,63.853,32.294,63.903,32.357,63.984,32.378,64.037,32.357,64.062,32.36,64.109,32.388,64.177,32.339,64.249,32.215,64.325,32.185,64.377,32.25,64.404,32.302,64.445,32.34,64.499,32.303,64.57,32.193,64.656,32.144,64.723,32.149,64.748],[32.044,64.46,32.2,64.384],[32.149,64.748,32.154,64.771,32.259,64.82,32.459,64.872,32.589,64.93,32.65,64.995,32.742,65.016,32.865,64.994,32.952,65.016,33.001,65.081,33.066,65.111,33.186,65.104,34.005,64.975,34.386,64.944,34.709,64.98,34.803,64.986],[31.955,64.864,32.149,64.748]]},{"n":"Kemijoki","z":5,"b":[24.628,65.859,28.955,68.105],"l":[[27.359,66.467,27.324,66.406,27.271,66.373,27.19,66.349,26.901,66.356,26.403,66.395,26.066,66.441,25.889,66.493,25.709,66.494,25.525,66.444,25.4,66.363,25.303,66.193,24.946,66.118,24.798,66.048,24.711,65.912,24.628,65.859],[28.781,68.105,28.93,68.012,28.955,67.961,28.878,67.901,28.851,67.813,28.803,67.751,28.724,67.686,28.553,67.59,28.289,67.462,28.187,67.369,28.245,67.309,28.227,67.287,28.132,67.304,28.085,67.293,28.086,67.255,28.063,67.231,27.992,67.214,27.608,67.158,27.5,67.106,27.54,67.039,27.516,66.945,27.385,66.765]]},{"n":"Khatanga","z":5,"b":[102.078,71.952,105.144,72.777],"l":[[105.144,72.777,104.86,72.75,104.616,72.671,104.429,72.646,104.2,72.576,104.17,72.52,103.89,72.462,103.36,72.4,103.078,72.349,103.044,72.307,102.886,72.214,102.602,72.071,102.364,71.987,102.078,71.952]]},{"n":"Kheta","z":5,"b":[92.22,69.844,102.078,71.952],"l":[[92.22,69.987,92.861,69.948,93.228,69.926,93.62,69.888,93.733,69.853,94.006,69.844,94.439,69.862,94.819,69.933,95.145,70.056,95.405,70.131,95.598,70.155,95.744,70.211,95.843,70.298,95.876,70.356,95.844,70.385,95.834,70.439,95.846,70.517,95.737,70.557,95.384,70.565,95.325,70.631,95.158,70.67,94.726,70.652,94.388,70.677,94.145,70.747,94.139,70.831,94.371,70.93,94.867,70.945,96.007,70.846,97.365,70.813,97.835,70.825,97.869,70.87,97.947,70.908,98.069,70.939,98.141,70.983,98.164,71.04,98.18,71.12,98.305,71.191,98.704,71.158,98.97,71.151,99.102,71.171,99.265,71.248,99.513,71.433],[102.078,71.952,101.584,71.816,101.081,71.752,100.405,71.713,99.91,71.659,99.597,71.591,99.54,71.515,99.515,71.435]]},{"n":"Kibali","z":3,"b":[28.586,2.333,30.439,3.598],"l":[[30.371,2.333,30.439,2.663,30.363,2.825,30.278,2.878,30.159,2.876,30.008,2.924,29.825,3.024,29.68,3.081,29.571,3.094,29.509,3.15,29.495,3.249,29.44,3.3,29.347,3.303,29.245,3.381,29.135,3.535,29.033,3.598,28.938,3.568,28.814,3.564,28.586,3.596]]},{"n":"Klamath","z":4.7,"b":[-124.093,41.225,-121.785,42.251],"l":[[-123.78,41.277,-123.837,41.302,-123.884,41.362,-123.938,41.466,-123.991,41.523,-124.068,41.539,-124.093,41.54],[-121.809,42.251,-121.785,42.215,-121.8,42.181,-121.845,42.137,-121.901,42.126,-121.966,42.149,-122.052,42.119,-122.157,42.036,-122.257,41.991,-122.353,41.986,-122.447,41.951,-122.54,41.887,-122.637,41.867,-122.738,41.889,-122.812,41.892,-122.882,41.866,-122.999,41.826,-123.1,41.83,-123.223,41.866,-123.342,41.811,-123.458,41.665,-123.511,41.532,-123.503,41.411,-123.535,41.315,-123.609,41.244,-123.679,41.225,-123.78,41.277]]},{"n":"Kokemäenjoki","z":5,"b":[21.598,61.211,22.579,61.578],"l":[[22.579,61.211,22.399,61.293,22.341,61.302,22.345,61.265,22.301,61.256,22.21,61.276,22.053,61.344,21.716,61.519,21.655,61.545,21.598,61.578]]},{"n":"Kolyma","z":4,"b":[145.216,61.454,160.856,68.789],"l":[[145.216,61.856,145.606,61.899,145.801,61.877,145.943,61.839,146.096,61.78,146.23,61.697,146.344,61.59,146.486,61.509,146.657,61.455,146.812,61.454,146.95,61.508,147.052,61.522,147.119,61.497,147.225,61.52,147.371,61.59,147.433,61.708,147.412,61.873,147.355,61.985,147.26,62.045,147.255,62.14,147.342,62.271,147.464,62.331,147.624,62.32,147.718,62.299,147.747,62.266,147.816,62.243,147.924,62.229,148.025,62.18,148.12,62.095,148.244,62.075,148.397,62.119,148.524,62.107,148.625,62.037,148.663,61.98,148.622,61.913,148.727,61.785,148.747,61.729,148.718,61.704,148.722,61.68,148.758,61.659,148.859,61.666,149.027,61.7,149.2,61.699,149.379,61.66,149.501,61.674,149.566,61.741,149.601,61.802,149.606,61.859,149.656,61.897,149.752,61.917,149.81,61.943,149.83,61.976,149.915,62.02,150.064,62.075,150.19,62.092,150.292,62.072,150.429,62.088,150.601,62.141,150.714,62.197,150.766,62.257,150.8,62.341,150.817,62.449,150.804,62.533,150.763,62.593,150.777,62.639,150.845,62.672,151.056,62.665,151.41,62.619,151.618,62.559,151.682,62.486,151.742,62.454,151.798,62.462,151.865,62.499,151.942,62.565,151.992,62.556,152.015,62.471,152.084,62.427,152.201,62.425,152.271,62.435,152.293,62.458,152.292,62.488,152.27,62.525,152.326,62.608,152.462,62.736,152.505,62.823,152.456,62.87,152.461,62.929,152.521,63,152.543,63.063,152.53,63.118,152.543,63.162,152.584,63.194,152.598,63.244,152.583,63.312,152.65,63.361,152.798,63.394,152.908,63.456,152.982,63.549,153.158,63.67,153.437,63.819,153.664,63.902,153.841,63.917,154.057,63.96,154.315,64.033,154.468,64.088,154.517,64.127,154.542,64.168,154.542,64.213,154.498,64.224,154.41,64.203,154.372,64.227,154.383,64.295,154.317,64.354,154.175,64.406,154.08,64.512,154.031,64.671,153.835,64.734,153.493,64.701,153.222,64.716,153.022,64.779,152.818,64.813,152.611,64.817,152.487,64.839,152.447,64.879,152.327,64.925,152.127,64.98,151.964,65.042,151.837,65.113,151.768,65.164,151.758,65.195,151.703,65.22,151.606,65.237,151.549,65.265,151.534,65.303,151.497,65.331,151.439,65.349,151.343,65.415,151.21,65.532,151.145,65.603,151.15,65.628,151.059,65.69,150.873,65.788,150.768,65.887,150.744,65.986,150.755,66.04,150.777,66.061,150.785,66.088,150.773,66.129,150.798,66.149,150.944,66.139,151.037,66.162,151.08,66.223,151.105,66.235,151.148,66.235,151.254,66.211,151.374,66.216,151.509,66.25,151.596,66.256,151.634,66.232,151.654,66.241,151.654,66.285,151.692,66.332,151.77,66.382,151.798,66.421,151.778,66.447,151.921,66.503,152.4,66.647,152.439,66.673,152.434,66.702,152.385,66.732,152.392,66.751,152.455,66.758,152.538,66.806,152.64,66.896,152.768,66.964,152.922,67.012,153.007,67.061,153.025,67.109,153.18,67.151,153.473,67.186,153.625,67.214,153.635,67.234,153.6,67.278,153.52,67.346,153.535,67.407,153.644,67.463,154.088,67.534,154.865,67.62,155.354,67.646,155.555,67.614,155.804,67.61,156.101,67.636,156.217,67.657,156.247,67.671,156.25,67.692,156.282,67.717,156.348,67.736,156.354,67.753,156.336,67.775,156.2,67.819,156.132,67.874,156.132,67.918,156.146,67.944,156.256,67.961,156.502,67.937,156.667,67.939,156.793,67.982,157.432,68.093,157.654,68.175,157.671,68.265,157.772,68.36,157.956,68.46,158.019,68.545,157.963,68.616,158.015,68.656,158.175,68.666,158.318,68.705,158.446,68.771,158.549,68.789,158.667,68.741,159.059,68.774,159.157,68.783,159.19,68.768,159.192,68.735,159.179,68.694,159.215,68.67,159.395,68.632,160.127,68.54,160.559,68.512,160.679,68.542,160.856,68.538]]},{"n":"Konqi","z":5,"b":[86.372,40.633,89.387,41.315],"l":[[89.387,40.633,88.92,40.672,88.715,40.69,88.422,40.744,88.286,40.815,88.127,40.86,87.943,40.877,87.787,40.91,87.659,40.958,87.529,40.974,87.397,40.957,87.258,40.971,87.114,41.016,87.025,41.069,86.991,41.13,86.899,41.205,86.749,41.294,86.598,41.315,86.372,41.243]]},{"n":"Koukourou","z":5,"b":[19.733,7.026,20.935,7.538],"l":[[20.935,7.343,20.829,7.26,20.767,7.234,20.713,7.239,20.673,7.212,20.647,7.154,20.597,7.105,20.487,7.046,20.383,7.026,20.304,7.034,20.213,7.061,20.106,7.117,19.982,7.203,19.874,7.319,19.733,7.538]]},{"n":"Koyukuk","z":5,"b":[-157.769,64.85,-150.726,68.043],"l":[[-150.821,68.043,-150.892,67.854,-150.988,67.747,-151.027,67.678,-151.041,67.611,-150.991,67.548,-150.878,67.49,-150.789,67.426,-150.726,67.357,-150.726,67.279,-150.791,67.193,-151.032,67.092,-151.451,66.975,-151.619,66.918,-151.676,66.857,-151.709,66.739,-151.779,66.658,-151.885,66.615,-152.032,66.58,-152.219,66.555,-152.396,66.552,-152.562,66.571,-152.734,66.551,-152.914,66.492,-153.074,66.46,-153.214,66.456,-153.296,66.438,-153.33,66.39,-153.416,66.411,-153.6,66.382,-153.909,66.308,-154.134,66.195,-154.276,66.043,-154.454,65.923,-154.668,65.834,-154.847,65.794,-154.991,65.802,-155.055,65.833,-155.088,65.852,-155.222,65.883,-155.272,65.91,-155.31,65.93,-155.363,65.921,-155.39,65.936,-155.393,65.962,-155.434,65.994,-155.521,65.998,-155.653,65.975,-155.769,65.98,-155.869,66.013,-156.01,66.022,-156.192,66.008,-156.219,65.987,-156.225,65.963,-156.25,65.947,-156.328,65.93,-156.348,65.911,-156.334,65.886,-156.349,65.833,-156.481,65.755,-156.503,65.726,-156.488,65.696,-156.475,65.674,-156.49,65.654,-156.471,65.63,-156.426,65.621,-156.391,65.617,-156.369,65.577,-156.346,65.559,-156.336,65.534,-156.348,65.516,-156.382,65.504,-156.388,65.487,-156.391,65.467,-156.426,65.455,-156.498,65.457,-156.57,65.458,-156.638,65.469,-156.629,65.491,-156.673,65.517,-156.77,65.549,-156.858,65.564,-156.935,65.564,-157.028,65.539,-157.137,65.49,-157.291,65.475,-157.492,65.492,-157.609,65.491,-157.643,65.47,-157.684,65.415,-157.676,65.396,-157.643,65.389,-157.624,65.364,-157.621,65.323,-157.642,65.299,-157.627,65.26,-157.652,65.216,-157.662,65.164,-157.647,65.084,-157.595,65.055,-157.506,65.078,-157.432,65.075,-157.375,65.044,-157.397,65.027,-157.499,65.026,-157.545,65.003,-157.535,64.961,-157.582,64.921,-157.74,64.867,-157.769,64.85]]},{"n":"Krishna","z":5,"b":[73.796,15.782,80.892,17.992],"l":[[79.313,16.579,79.657,16.69,79.818,16.703,79.912,16.655,79.993,16.678,80.063,16.773,80.121,16.768,80.169,16.664,80.262,16.607,80.401,16.597,80.524,16.553,80.63,16.475,80.717,16.393,80.784,16.307,80.834,16.201,80.883,16.014,80.892,15.971,80.892,15.866,80.878,15.82,80.865,15.782],[73.796,17.992,73.898,17.931,73.962,17.854,74.054,17.686,74.118,17.633,74.127,17.579,74.103,17.507,74.121,17.423,74.18,17.329,74.204,17.255,74.193,17.203,74.232,17.157,74.322,17.119,74.369,17.083,74.376,17.049,74.396,17.034,74.429,17.038,74.457,17.016,74.492,16.942,74.537,16.858,74.587,16.808,74.659,16.766,74.672,16.733,74.628,16.711,74.623,16.667,74.66,16.602,74.754,16.596,74.907,16.649,75.013,16.634,75.075,16.552,75.126,16.54,75.167,16.598,75.231,16.621,75.317,16.608,75.383,16.58,75.429,16.536,75.449,16.492,75.443,16.448,75.49,16.43,75.591,16.441,75.677,16.422,75.749,16.373,75.838,16.338,75.943,16.317,76.016,16.279,76.059,16.225,76.124,16.188,76.213,16.167,76.405,16.227,76.699,16.368,76.882,16.47,76.954,16.534,77.057,16.528,77.259,16.415,77.616,16.349,77.845,16.232,78.066,16.044,78.193,15.956,78.227,15.97,78.244,15.992,78.244,16.023,78.283,16.05,78.361,16.072,78.498,16.071,78.693,16.047,78.805,16.064,78.833,16.122,78.884,16.171,78.997,16.23]]},{"n":"Kuskokwim","z":5,"b":[-161.947,60.685,-154.721,63.079],"l":[[-154.721,63.079,-154.844,63.02,-154.926,63.006,-155.01,63.018,-155.125,63.005,-155.271,62.965,-155.368,62.962,-155.416,62.996,-155.492,63,-155.595,62.972,-155.619,62.949,-155.564,62.931,-155.605,62.886,-155.741,62.815,-155.817,62.766,-155.831,62.739,-155.823,62.72,-155.794,62.71,-155.775,62.678,-155.767,62.624,-155.776,62.586,-155.814,62.554,-156.025,62.466,-156.142,62.38,-156.236,62.268,-156.256,62.163,-156.203,62.065,-156.203,62.007,-156.256,61.988,-156.298,61.951,-156.327,61.895,-156.519,61.817,-156.873,61.715,-157.244,61.725,-157.632,61.848,-157.885,61.892,-158.004,61.856,-158.081,61.85,-158.118,61.873,-158.182,61.821,-158.273,61.693,-158.393,61.61,-158.544,61.573,-158.71,61.566,-158.892,61.59,-159.187,61.594,-159.801,61.573,-160.215,61.543,-160.415,61.504,-160.538,61.445,-160.604,61.388,-160.615,61.334,-160.652,61.306,-160.716,61.302,-160.755,61.28,-160.77,61.239,-160.846,61.187,-160.984,61.123,-161.068,61.058,-161.1,60.992,-161.175,60.938,-161.292,60.897,-161.386,60.885,-161.457,60.903,-161.487,60.893,-161.476,60.854,-161.483,60.827,-161.508,60.811,-161.555,60.816,-161.624,60.844,-161.661,60.838,-161.666,60.799,-161.694,60.78,-161.745,60.781,-161.762,60.768,-161.746,60.74,-161.774,60.721,-161.881,60.704,-161.947,60.685]]},{"n":"Kwango","z":5,"b":[16.508,-11.256,18.797,-3.2],"l":[[18.797,-11.256,18.707,-10.879,18.632,-10.731,18.622,-10.657,18.652,-10.609,18.61,-10.526,18.497,-10.41,18.48,-10.363,18.487,-10.339,18.496,-10.317,18.481,-10.281,18.495,-10.232,18.536,-10.17,18.588,-10.117,18.65,-10.072,18.677,-9.997,18.67,-9.891,18.648,-9.805,18.611,-9.74,18.546,-9.681,18.455,-9.63,18.361,-9.522,18.267,-9.357,18.189,-9.254,18.128,-9.211,18.091,-9.204,18.056,-9.191,18.043,-9.152,18.05,-9.115,18.042,-9.011,18.014,-8.875,17.938,-8.715,17.816,-8.531,17.708,-8.414,17.613,-8.363,17.559,-8.272,17.536,-8.076,17.245,-7.623,17.155,-7.461,17.122,-7.419,17.064,-7.363,16.985,-7.257,16.952,-7.157,16.966,-7.062,16.919,-6.934,16.813,-6.773,16.743,-6.618,16.709,-6.472,16.701,-6.346,16.718,-6.241,16.697,-6.164,16.64,-6.115,16.602,-6.04,16.577,-5.89,16.515,-5.391,16.508,-5.158,16.536,-5.023,16.536,-4.841,16.508,-4.611,16.508,-4.46,16.535,-4.386,16.536,-4.315,16.513,-4.245,16.587,-4.18,16.758,-4.119,16.87,-4.062,16.921,-4.011,16.993,-3.991,17.086,-4.003,17.183,-3.92,17.286,-3.742,17.322,-3.61,17.291,-3.525,17.298,-3.454,17.365,-3.369,17.376,-3.295,17.355,-3.2]]},{"n":"Kyzyl-Khem","z":4,"b":[95.78,51.295,98,51.519],"l":[[98,51.514,97.879,51.519,97.703,51.48,97.298,51.36,96.524,51.346,96.235,51.356,96.174,51.383,96.099,51.383,95.965,51.342,95.78,51.295]]},{"n":"La Grande Rivière","z":4,"b":[-78.946,53.406,-70.362,53.916],"l":[[-70.48,53.406,-70.42,53.408,-70.387,53.435,-70.362,53.487,-70.473,53.492,-70.845,53.428],[-71.117,53.591,-71.277,53.664,-71.452,53.678,-71.816,53.646],[-72.036,53.598,-72.094,53.602,-72.102,53.616,-72.078,53.64,-72.082,53.655,-72.113,53.659,-72.28,53.621,-72.35,53.63,-72.389,53.675,-72.503,53.691,-72.692,53.679,-72.727,53.704,-72.852,53.765,-73.143,53.866,-73.328,53.916,-73.407,53.914,-73.536,53.877,-73.714,53.805,-73.962,53.748,-74.439,53.684],[-77.42,53.798,-77.626,53.82,-77.826,53.806,-78.088,53.764,-78.39,53.762,-78.901,53.817,-78.924,53.823,-78.946,53.832]]},{"n":"Mekong","z":2.1,"b":[96.941,17.821,102.233,31.966],"l":[[96.941,31.966,97.055,31.692,97.121,31.577,97.175,31.528,97.207,31.463,97.215,31.381,97.204,31.325,97.173,31.296,97.168,31.233,97.188,31.134,97.243,31.05,97.332,30.979,97.377,30.89,97.378,30.785,97.495,30.618,97.846,30.278,97.963,30.162,98.037,30.046,98.109,29.89,98.161,29.81,98.194,29.806,98.242,29.763,98.307,29.68,98.363,29.568,98.411,29.427,98.484,29.307,98.582,29.209,98.619,29.126,98.595,29.058,98.593,29.009,98.624,28.963,98.67,28.772,98.727,28.632,98.809,28.478,98.863,28.304,98.889,28.109,98.938,27.959,99.012,27.856,99.036,27.768,99.01,27.695,99.005,27.623,99.028,27.516,99.088,27.377,99.104,27.308,99.094,27.265,99.11,27.215,99.151,27.16,99.175,27.071,99.182,26.948,99.165,26.801,99.124,26.632,99.114,26.531,99.135,26.498,99.141,26.38,99.134,26.176,99.139,26.021,99.157,25.918,99.213,25.792,99.31,25.644,99.341,25.521,99.308,25.423,99.341,25.315,99.44,25.198,99.525,25.129,99.599,25.109,99.7,25.017,99.828,24.853,99.939,24.762,100.031,24.743,100.087,24.718,100.106,24.687,100.14,24.69,100.187,24.727,100.251,24.747,100.332,24.75,100.418,24.682,100.511,24.545,100.548,24.388,100.529,24.211,100.485,24.108,100.416,24.078,100.388,24.026,100.401,23.953,100.356,23.854,100.252,23.73,100.185,23.62,100.139,23.473,100.067,23.278,100.106,23.149,100.233,23.023,100.313,22.887,100.347,22.744,100.415,22.636,100.517,22.563,100.567,22.502,100.565,22.451,100.588,22.398,100.636,22.343,100.647,22.306,100.622,22.288,100.632,22.268,100.677,22.247,100.705,22.205,100.715,22.141,100.77,22.047,100.869,21.923,100.984,21.845,101.115,21.813,101.166,21.789,101.134,21.774,101.128,21.705,101.147,21.582,101.08,21.469,100.928,21.366,100.82,21.314,100.757,21.313,100.703,21.251,100.659,21.13,100.614,21.059,100.567,21.038,100.536,20.992,100.522,20.922,100.549,20.884,100.618,20.879,100.623,20.86,100.565,20.825,100.493,20.813,100.407,20.823,100.326,20.796,100.249,20.73,100.184,20.589,100.13,20.372,100.115,20.258,100.14,20.245,100.174,20.273,100.218,20.34,100.266,20.377,100.318,20.386,100.373,20.34,100.432,20.241,100.492,20.184,100.553,20.17,100.589,20.073,100.597,19.893,100.653,19.825,100.755,19.87,100.857,19.878,100.959,19.849,101.037,19.848,101.091,19.875,101.153,19.872,101.223,19.838,101.333,19.822,101.484,19.823,101.699,19.89,101.979,20.022,102.137,20.087,102.173,20.083,102.205,20.058,102.233,20.011,102.141,19.903,101.93,19.734,101.823,19.604,101.822,19.513,101.842,19.451,101.885,19.419,101.886,19.372,101.845,19.31,101.819,19.181,101.808,18.984,101.816,18.855,101.841,18.794,101.826,18.698,101.77,18.568,101.658,18.422,101.488,18.262,101.408,18.119,101.419,17.992,101.459,17.902,101.564,17.821]]},{"n":"Lek","z":4,"b":[4.085,51.896,5.331,51.994],"l":[[5.331,51.963,5.161,51.994,4.999,51.977,4.787,51.925,4.596,51.898,4.426,51.896,4.28,51.919,4.085,51.994]]},{"n":"Lena","z":2,"b":[105.07,53.845,130.051,72.294],"l":[[120.648,60.403,120.343,60.396,120.184,60.35,120.069,60.262,119.874,60.229,119.599,60.252,119.355,60.191,119.142,60.046,118.959,59.966,118.805,59.949,118.678,59.917,118.575,59.87,118.51,59.824,118.482,59.778,118.42,59.745,118.323,59.725,117.998,59.805,117.447,59.986,117.129,60.134,117.046,60.249,116.964,60.331,116.884,60.381,116.669,60.439,116.321,60.505,116.031,60.598,115.798,60.717,115.503,60.775,115.146,60.773,114.893,60.745,114.743,60.692,114.549,60.567,114.311,60.369,114.137,60.26,114.029,60.241,113.823,60.125,113.519,59.912,113.24,59.779,112.983,59.728,112.836,59.674,112.798,59.618,112.745,59.578,112.643,59.545,112.543,59.363,112.394,59.274,112.161,59.215,112.01,59.197,111.942,59.218,111.846,59.187,111.723,59.104,111.549,59.019,111.323,58.93,111.202,58.847,111.186,58.769,111.117,58.739,110.993,58.758,110.949,58.746,110.929,58.71,110.805,58.659,110.56,58.662,110.282,58.645,109.97,58.606,109.798,58.539,109.764,58.445,109.694,58.359,109.587,58.279,109.498,58.231,109.427,58.215,109.341,58.225,109.239,58.261,109.154,58.275,109.085,58.264,108.94,58.21,108.721,58.113,108.606,58.044,108.594,58.001,108.553,57.972,108.482,57.956,108.409,57.907,108.332,57.825,108.229,57.785,108.1,57.787,108.034,57.759,108.029,57.702,107.957,57.659,107.904,57.635,107.894,57.61,107.918,57.57,107.872,57.523,107.697,57.438,107.619,57.408,107.593,57.386,107.584,57.357,107.565,57.343,107.519,57.346,107.487,57.329,107.471,57.292,107.415,57.271,107.318,57.264,107.251,57.276,107.213,57.308,107.149,57.326,107.059,57.328,106.989,57.31,106.937,57.27,106.918,57.232,106.93,57.196,106.912,57.146,106.865,57.083,106.747,57.043,106.558,57.026,106.425,57.028,106.348,57.051,106.256,57.011,106.15,56.909,105.986,56.833,105.764,56.783,105.672,56.742,105.712,56.711,105.831,56.696,106.031,56.698,106.173,56.671,106.256,56.617,106.292,56.56,106.281,56.501,106.25,56.451,106.201,56.411,106.058,56.366,105.822,56.316,105.695,56.266,105.676,56.217,105.718,56.152,105.821,56.07,105.852,55.976,105.811,55.869,105.731,55.755,105.613,55.633,105.52,55.49,105.45,55.326,105.331,55.197,105.163,55.103,105.076,55.008,105.07,54.913,105.108,54.85,105.19,54.82,105.234,54.754,105.241,54.649,105.224,54.55,105.185,54.456,105.221,54.384,105.332,54.334,105.457,54.238,105.665,54.022,105.806,54.006,105.873,53.973,105.913,53.916,105.974,53.878,106.056,53.858,106.305,53.857,106.927,53.884,106.996,53.85,107.053,53.845,107.119,53.858,107.159,53.894,107.171,53.952,107.234,53.999,107.347,54.036,107.456,54.105,107.562,54.205,107.657,54.265,107.783,54.294,108.021,54.246,108.107,54.2,108.118,54.138,108.088,54.082,107.98,54.004],[126.69,72.294,126.897,72.05,127.02,71.946,127.127,71.899,127.168,71.845,127.142,71.784,127.162,71.683,127.229,71.542,127.258,71.412,127.25,71.294,127.317,71.184,127.459,71.082,127.507,70.999,127.46,70.934,127.472,70.879,127.414,70.742,127.091,70.606,126.789,70.541,126.509,70.545,126.231,70.527,125.954,70.487,125.801,70.449,125.77,70.414,125.833,70.367,125.99,70.309,126.019,70.235,125.919,70.145,125.773,70.069,125.485,69.974,125.201,69.904,125.079,69.826,125.023,69.716,124.902,69.58,124.722,69.4,124.622,69.336,124.251,69.181,124.094,69.097,124.026,69.03,123.985,68.926,123.971,68.785,123.927,68.634,123.853,68.474,123.774,68.349,123.689,68.259,123.535,68.173,123.31,68.092,123.15,67.929,123.057,67.685,123.095,67.455,123.263,67.237,123.398,66.997,123.5,66.737,123.588,66.581,123.697,66.503,123.738,66.461,123.876,66.198,124.028,66.022,124.211,65.873,124.285,65.733,124.251,65.602,124.268,65.504,124.337,65.441,124.374,65.379,124.377,65.316,124.408,65.252,124.466,65.184,124.593,65.098,124.79,64.992,124.954,64.931,125.085,64.916,125.22,64.879,125.359,64.818,125.454,64.747,125.503,64.665,125.744,64.575,126.175,64.478,126.423,64.403,126.506,64.378,126.853,64.224,127.292,63.965,127.586,63.843,127.88,63.772,128.078,63.697,128.178,63.618,128.291,63.561,128.417,63.527,128.641,63.516,128.963,63.528,129.188,63.523,129.38,63.49,129.586,63.305,129.637,63.199,129.603,63.11,129.622,63.034,129.695,62.971,129.764,62.858,129.828,62.696,129.909,62.569,130.009,62.479,130.051,62.396,130.037,62.322,129.997,62.27,129.934,62.24,129.892,62.16,129.873,62.031,129.788,61.893,129.638,61.748,129.476,61.631,129.216,61.499,128.751,61.323,128.481,61.266,128.249,61.269,127.893,61.228,127.413,61.142,127.047,61.103,126.796,61.112,126.554,61.101,126.322,61.071,126.097,61.014,125.881,60.931,125.578,60.857,124.995,60.758,124.893,60.71,124.73,60.688,124.473,60.675,124.279,60.646,124.147,60.599,123.552,60.576,122.297,60.575,121.653,60.598,121.425,60.643,121.162,60.622,120.864,60.537,120.7,60.473,120.654,60.407]]},{"n":"Liao","z":5,"b":[122.14,40.688,123.831,43.483],"l":[[123.554,43.483,123.614,43.399,123.604,43.29,123.545,43.128,123.534,43.019,123.571,42.963,123.583,42.877,123.568,42.76,123.611,42.66,123.712,42.576,123.774,42.504,123.796,42.442,123.831,42.406,123.771,42.327,123.513,42.203,123.35,42.149,123.283,42.166,123.177,42.145,123.033,42.085,122.893,41.957,122.758,41.762,122.677,41.579,122.636,41.325,122.457,41.214,122.401,41.133,122.44,41.055,122.409,41.003,122.326,40.963,122.294,40.898,122.236,40.852,122.152,40.823,122.147,40.78,122.14,40.688]]},{"n":"Liard","z":5,"b":[-131.373,59.311,-121.221,61.864],"l":[[-131.373,61.204,-131.32,61.072,-131.246,61.043,-131.132,61.075,-131.044,61.082,-130.951,61.054,-130.619,60.862,-130.374,60.765,-130.105,60.698,-129.845,60.582,-129.593,60.417,-129.374,60.309,-129.185,60.26,-129.028,60.186,-128.904,60.087,-128.791,60.035,-128.691,60.029,-128.61,60.003,-128.547,59.956,-128.397,59.909,-128.161,59.863,-127.991,59.883,-127.888,59.971,-127.772,60.005,-127.644,59.986,-127.54,59.952,-127.462,59.902,-127.446,59.843,-127.494,59.773,-127.463,59.738,-127.354,59.738,-127.259,59.712,-127.138,59.636,-126.987,59.66,-126.835,59.65,-126.63,59.614,-126.404,59.55,-126.156,59.457,-125.979,59.425,-125.872,59.455,-125.81,59.456,-125.672,59.384,-125.447,59.322,-125.232,59.311,-125.028,59.349,-124.88,59.413,-124.789,59.503,-124.72,59.553,-124.682,59.616,-124.646,59.665,-124.554,59.729,-124.414,59.724,-124.225,59.651,-124.16,59.619,-124.146,59.561,-124.081,59.558,-124.048,59.594,-123.931,59.71,-123.86,59.853,-123.835,60.025,-123.713,60.165,-123.494,60.274,-123.42,60.366,-123.49,60.443,-123.534,60.523,-123.553,60.605,-123.552,60.666,-123.53,60.704,-123.468,60.735,-123.437,60.801,-123.395,60.893,-123.346,60.947,-123.326,60.974,-123.277,61.022,-123.182,61.078,-123.136,61.097,-123.003,61.101,-122.886,61.135,-122.753,61.218,-122.405,61.307,-121.842,61.403,-121.521,61.482,-121.442,61.544,-121.414,61.596,-121.434,61.64,-121.384,61.685,-121.263,61.731,-121.221,61.777,-121.28,61.864]]},{"n":"Limpopo","z":5,"b":[26.962,-25.189,33.631,-22.146],"l":[[26.962,-23.737,27.086,-23.578,27.146,-23.524,27.186,-23.523,27.241,-23.49,27.313,-23.424,27.399,-23.384,27.499,-23.368,27.563,-23.325,27.593,-23.253,27.644,-23.218,27.717,-23.22,27.758,-23.197,27.769,-23.149,27.813,-23.108,27.891,-23.074,27.931,-23.034,27.935,-22.987,28.028,-22.874,28.21,-22.694,28.382,-22.593,28.543,-22.573,28.696,-22.535,28.84,-22.481,28.946,-22.395,29.014,-22.278,29.13,-22.213,29.377,-22.193,29.663,-22.146,29.902,-22.184,30.19,-22.291,30.46,-22.329,30.712,-22.298,30.916,-22.291,31.073,-22.308,31.197,-22.345,31.288,-22.402,31.393,-22.437,31.512,-22.451,31.63,-22.506,31.749,-22.6,31.877,-22.74,32.015,-22.924,32.119,-23.036,32.188,-23.076,32.231,-23.133,32.247,-23.205,32.302,-23.302,32.396,-23.424,32.488,-23.636,32.577,-23.937,32.673,-24.14,32.825,-24.297,32.871,-24.404,32.906,-24.44,32.943,-24.441,33.01,-24.485,33.108,-24.572,33.183,-24.614,33.234,-24.609,33.264,-24.624,33.273,-24.657,33.329,-24.699,33.431,-24.751,33.485,-24.772,33.51,-24.771,33.526,-24.757,33.537,-24.745,33.552,-24.739,33.568,-24.751,33.597,-24.823,33.631,-24.949,33.614,-25.041,33.545,-25.098,33.515,-25.142,33.53,-25.189]]},{"n":"Lom","z":5,"b":[13.126,4.933,14.705,6.698],"l":[[14.665,6.698,14.705,6.661,14.687,6.585,14.596,6.393,14.471,6.302,14.431,6.222,14.432,6.124,14.417,6.042,14.386,5.977,14.314,5.926,14.201,5.89,14.13,5.814,14.1,5.699,14.067,5.64,14.029,5.637,13.991,5.607,13.951,5.549,13.91,5.522,13.866,5.528,13.848,5.499,13.858,5.435,13.801,5.372,13.678,5.311,13.595,5.299,13.528,5.358,13.466,5.381,13.423,5.362,13.378,5.307,13.364,5.234,13.381,5.141,13.359,5.056,13.299,4.978,13.233,4.938,13.126,4.933]]},{"n":"Nizhnyaya Tunguska","z":4.7,"b":[87.961,57.8,108.603,65.969],"l":[[105.689,58.068,105.809,58,105.877,57.982,105.962,57.999,106.199,58.028,106.319,58.011,106.402,57.958,106.534,57.908,106.89,57.826,107.057,57.804,107.173,57.805,107.238,57.828,107.317,57.827,107.409,57.8,107.504,57.816,107.6,57.874,107.687,57.902,107.762,57.899,107.892,57.961,108.075,58.089,108.195,58.152,108.251,58.149,108.342,58.211,108.467,58.34,108.506,58.435,108.461,58.497,108.458,58.547,108.498,58.585,108.484,58.644,108.416,58.721,108.406,58.781,108.452,58.822,108.466,58.903,108.448,59.025,108.392,59.126,108.252,59.247,108.134,59.395,108.074,59.446,108.033,59.449,108.023,59.485,108.045,59.553,108.018,59.61,107.941,59.657,107.963,59.726,108.084,59.818,108.099,59.893,108.091,59.936,108.104,60.002,108.094,60.051,108.062,60.085,108.062,60.123,108.038,60.205,107.897,60.246,107.819,60.292,107.803,60.343,107.811,60.391,107.842,60.437,107.841,60.493,107.872,60.524,107.895,60.554,107.893,60.593,107.866,60.624,107.867,60.679,107.891,60.721,107.956,60.84,107.98,60.914,107.965,60.942,107.93,60.956,107.875,60.955,107.854,60.995,107.866,61.074,107.909,61.117,107.982,61.126,108.007,61.145,107.985,61.175,107.959,61.231,107.967,61.269,108.029,61.327,108.036,61.379,107.988,61.423,107.978,61.467,108.006,61.511,108.008,61.552,108.053,61.597,108.085,61.635,108.072,61.674,108.04,61.7,108.013,61.729,108,61.752,108.014,61.815,108.034,61.879,108.048,61.929,108.089,61.971,108.223,62.034,108.245,62.085,108.221,62.135,108.225,62.191,108.279,62.256,108.29,62.333,108.256,62.421,108.225,62.468,108.196,62.471,108.179,62.451,108.173,62.406,108.14,62.373,108.08,62.35,108.021,62.355,107.962,62.388,107.922,62.442,107.95,62.527,108.062,62.583,108.407,62.641,108.587,62.681,108.603,62.704,108.595,62.729,108.563,62.755,108.445,62.763,108.239,62.753,108.17,62.779,108.237,62.841,108.366,62.878,108.555,62.887,108.592,62.919,108.552,62.948,108.471,62.955,108.369,62.961,108.317,62.974,108.188,62.973,108.119,62.99,108.112,63.026,108.144,63.064,108.216,63.102,108.218,63.143,108.151,63.188,108.078,63.203,108,63.189,107.949,63.148,107.927,63.078,107.887,63.054,107.829,63.078,107.805,63.111,107.815,63.153,107.772,63.166,107.676,63.15,107.611,63.159,107.576,63.192,107.525,63.21,107.455,63.214,107.422,63.236,107.372,63.272,107.278,63.304,106.829,63.334,106.809,63.35,106.839,63.38,106.918,63.424,106.925,63.471,106.859,63.519,106.741,63.538,106.569,63.526,106.441,63.495,106.357,63.444,106.266,63.437,106.172,63.445,106.099,63.42,106.07,63.375,106.026,63.367,105.818,63.392,105.646,63.381,105.51,63.333,105.385,63.327,105.273,63.362,105.183,63.408,105.117,63.464,104.775,63.517,104.157,63.568,103.807,63.625,103.725,63.687,103.705,63.734,103.637,63.785,103.487,63.82,103.271,63.899,103.03,64.032,102.831,64.103,102.674,64.111,102.539,64.098,102.373,64.047,101.994,64.094,101.784,64.14,101.617,64.203,101.432,64.202,101.228,64.137,101.023,64.13,100.818,64.181,100.671,64.2,100.581,64.187,100.487,64.211,100.389,64.272,100.278,64.294,100.153,64.277,100.037,64.232,99.929,64.158,99.443,64.132,98.578,64.155,98.124,64.154,98.083,64.13,98.097,64.101,98.168,64.066,98.189,64.03,98.159,63.993,98.073,63.96,97.932,63.933,97.906,63.889,97.998,63.828,98.03,63.785,98.003,63.761,97.78,63.775,97.361,63.825,96.993,63.837,96.675,63.809,96.476,63.818,96.397,63.865,96.271,63.892,96.1,63.901,95.887,63.945,95.632,64.025,95.429,64.059,95.278,64.046,95.078,64.079,94.829,64.16,94.519,64.191,94.147,64.172,93.714,64.232,93.22,64.373,92.831,64.438,92.548,64.427,92.382,64.404,92.334,64.368,92.252,64.366,92.08,64.412],[87.961,65.776,88.132,65.83,88.228,65.844,88.307,65.835,88.386,65.855,88.466,65.904,88.616,65.942,88.836,65.969,89.211,65.904,89.74,65.748,90.018,65.656,90.045,65.627,90.035,65.582,89.989,65.521,89.974,65.399,89.992,65.216,90.046,65.116,90.137,65.099,90.212,65.066,90.274,65.017,90.304,64.956,90.302,64.883,90.337,64.831,90.406,64.8,90.514,64.782,90.66,64.776,90.824,64.718,91.007,64.608,91.345,64.518,92.081,64.411]]},{"n":"Lualaba","z":2.1,"b":[25.413,-11.743,26.441,-8.272],"l":[[26.249,-11.743,26.166,-11.672,26.119,-11.658,26.08,-11.678,26.03,-11.68,25.938,-11.653,25.886,-11.623,25.84,-11.547,25.782,-11.417,25.715,-11.331,25.639,-11.29,25.602,-11.247,25.604,-11.2,25.59,-11.169,25.56,-11.153,25.552,-11.114,25.566,-11.054,25.611,-11.005,25.684,-10.969,25.726,-10.878,25.735,-10.731,25.696,-10.607,25.61,-10.507,25.541,-10.467,25.49,-10.486,25.449,-10.485,25.417,-10.462,25.414,-10.407,25.441,-10.319,25.44,-10.23,25.413,-10.137,25.441,-10.036,25.525,-9.926,25.547,-9.811,25.506,-9.691,25.489,-9.587,25.502,-9.455,25.561,-9.266,25.611,-9.229,25.67,-9.278,25.729,-9.276,25.786,-9.221,25.843,-9.197,25.901,-9.204,25.966,-9.137,26.037,-8.996,26.058,-8.881,26.031,-8.792,26.086,-8.705,26.223,-8.621,26.284,-8.544,26.268,-8.474,26.282,-8.42,26.326,-8.383,26.346,-8.347,26.341,-8.313,26.364,-8.29,26.441,-8.272]]},{"n":"Congo","z":2.1,"b":[24.292,-5.61,27.014,0.782],"l":[[26.93,-5.61,27.003,-5.378,27.014,-5.286,26.988,-5.256,26.959,-5.123,26.927,-4.885,26.879,-4.731,26.813,-4.662,26.652,-4.55,26.395,-4.397,26.199,-4.25,26.065,-4.11,26.012,-3.958,26.04,-3.796,26.013,-3.649,25.93,-3.519,25.904,-3.363,25.935,-3.18,25.949,-3.023,25.946,-2.89,25.927,-2.804,25.876,-2.745],[25.876,-2.752,25.788,-2.457,25.785,-2.113,25.837,-1.623,25.818,-1.286,25.682,-1.012,25.534,-0.816,25.465,-0.682,25.423,-0.545,25.443,-0.386,25.525,-0.204,25.554,-0.036,25.529,0.121,25.489,0.235,25.409,0.346,25.255,0.459,25.136,0.507,24.934,0.539,24.791,0.577,24.69,0.628,24.584,0.705,24.472,0.753,24.292,0.782]]},{"n":"Mackenzie","z":2.1,"b":[-135.638,61.248,-117.668,69.324],"l":[[-117.668,61.308,-117.706,61.367,-117.86,61.415,-118.143,61.47,-118.323,61.455,-118.402,61.369,-118.7,61.3,-119.22,61.248,-119.773,61.304,-120.36,61.468,-120.673,61.567,-120.714,61.603,-120.718,61.648,-120.686,61.704,-120.684,61.752,-120.713,61.792,-120.868,61.821,-121.148,61.839,-121.28,61.864,-121.363,61.88,-121.589,61.974,-122.235,62.118,-122.603,62.167,-122.906,62.17,-123.139,62.204,-123.388,62.31,-123.402,62.326,-123.356,62.405,-123.25,62.548,-123.204,62.709,-123.218,62.889,-123.254,63.024,-123.311,63.113,-123.419,63.199,-123.577,63.28,-123.683,63.379,-123.736,63.493,-123.83,63.63,-123.966,63.79,-124.084,63.897,-124.183,63.952,-124.266,63.978,-124.333,63.977,-124.39,63.999,-124.438,64.046,-124.451,64.104,-124.429,64.174,-124.448,64.232,-124.506,64.278,-124.584,64.311,-124.682,64.333,-124.789,64.405,-124.906,64.526,-124.947,64.615,-124.912,64.671,-124.943,64.707,-125.039,64.724,-125.109,64.764,-125.153,64.826,-125.375,64.875,-125.619,64.895,-125.775,64.908,-126.027,64.956,-126.182,65.049,-126.459,65.177,-126.646,65.238,-126.929,65.291,-127.061,65.295,-127.337,65.375,-128.03,65.608,-128.401,65.62,-128.588,65.643,-128.715,65.681,-128.75,65.739,-128.817,65.791,-128.97,65.867,-129.065,65.95,-129.102,66.04,-129.093,66.109,-129.038,66.155,-128.923,66.202,-128.748,66.251,-128.666,66.292,-128.678,66.327,-128.745,66.358,-128.867,66.385,-129.089,66.48,-129.411,66.644,-129.699,66.75,-129.955,66.797,-130.118,66.86,-130.189,66.941,-130.236,67.046,-130.26,67.174,-130.306,67.258,-130.375,67.296,-130.496,67.325,-130.705,67.34,-130.759,67.354,-130.808,67.374,-130.882,67.42,-130.956,67.461,-131.16,67.489,-131.437,67.473,-131.907,67.403,-132.569,67.278,-133.02,67.245,-133.26,67.304,-133.433,67.371,-133.539,67.444,-133.64,67.479,-133.737,67.477,-133.887,67.526,-134.091,67.625,-134.199,67.718,-134.212,67.803,-134.277,67.889,-134.394,67.976,-134.448,68.073,-134.44,68.177,-134.409,68.231,-134.366,68.292,-134.33,68.314,-134.272,68.318,-134.199,68.345,-134.125,68.392,-134.099,68.429,-134.075,68.517,-134.103,68.576,-134.244,68.706],[-134.244,68.706,-134.391,68.706,-134.539,68.77,-134.836,68.964],[-134.244,68.706,-134.545,68.919,-134.626,69.017,-134.586,69.071,-134.503,69.111,-134.313,69.154,-134.245,69.192,-134.174,69.253],[-134.836,68.964,-135.034,68.908,-135.131,68.903,-135.195,68.93,-135.322,68.936,-135.606,68.911,-135.638,68.892],[-134.836,68.964,-135.037,69.062,-135.076,69.11,-135.02,69.139,-135.039,69.181,-135.183,69.263,-135.255,69.324]]},{"n":"Madeira","z":3,"b":[-65.422,-10.392,-58.689,-3.342],"l":[[-65.395,-10.392,-65.313,-10.253,-65.299,-10.147,-65.325,-10.027,-65.328,-9.936,-65.309,-9.873,-65.338,-9.79,-65.414,-9.689,-65.422,-9.611,-65.361,-9.556,-65.233,-9.546,-65.038,-9.581,-64.914,-9.583,-64.863,-9.554,-64.833,-9.499,-64.827,-9.416,-64.802,-9.372,-64.759,-9.367,-64.701,-9.32,-64.627,-9.231,-64.534,-9.185,-64.423,-9.181,-64.341,-9.138,-64.287,-9.056,-64.216,-9.002,-64.127,-8.976,-64.08,-8.937,-64.075,-8.884,-64.035,-8.832,-63.96,-8.783,-63.919,-8.729,-63.913,-8.672,-63.849,-8.619,-63.728,-8.572,-63.636,-8.556,-63.571,-8.571,-63.547,-8.555,-63.562,-8.507,-63.545,-8.466,-63.495,-8.432,-63.438,-8.358,-63.343,-8.186,-63.245,-8.215,-63.131,-8.177,-62.888,-8.034,-62.867,-7.945,-62.9,-7.803,-62.98,-7.577,-62.98,-7.351,-62.898,-7.123,-62.824,-7.004,-62.757,-6.994,-62.688,-6.934,-62.615,-6.823,-62.529,-6.762,-62.429,-6.752,-62.367,-6.723,-62.344,-6.675,-62.344,-6.634,-62.367,-6.598,-62.348,-6.538,-62.288,-6.454,-62.25,-6.367,-62.234,-6.278,-62.184,-6.23,-62.126,-6.222,-62.092,-6.218,-62.058,-6.22,-61.905,-6.236,-61.844,-6.21,-61.827,-6.147,-61.78,-6.086,-61.706,-6.026,-61.668,-5.973,-61.668,-5.927,-61.648,-5.901,-61.609,-5.896,-61.599,-5.881,-61.62,-5.856,-61.549,-5.838,-61.385,-5.827,-61.289,-5.781,-61.258,-5.699,-61.117,-5.615,-60.865,-5.529,-60.734,-5.448,-60.725,-5.37,-60.647,-5.293,-60.529,-5.23,-60.459,-5.199,-60.425,-5.176,-60.388,-5.147,-60.362,-5.103,-60.26,-4.977,-60.171,-4.913,-60.105,-4.917,-60.037,-4.897,-59.966,-4.851,-59.931,-4.791,-59.931,-4.714,-59.914,-4.671,-59.883,-4.595,-59.786,-4.432,-59.699,-4.367,-59.648,-4.364,-59.603,-4.37,-59.574,-4.366,-59.548,-4.357,-59.521,-4.342,-59.489,-4.318,-59.427,-4.25,-59.362,-4.156,-59.328,-4.045,-59.253,-3.952,-59.138,-3.875,-59.083,-3.798,-59.09,-3.72,-58.997,-3.596,-58.689,-3.342]]},{"n":"Madison","z":2,"b":[-111.719,44.653,-110.744,46.36],"l":[[-110.744,44.727,-110.859,44.668,-110.933,44.653,-111.004,44.662,-111.054,44.689,-111.091,44.747,-111.1,44.757],[-111.309,44.868,-111.329,44.867,-111.406,44.843,-111.477,44.852,-111.553,44.892,-111.626,44.988,-111.694,45.141,-111.719,45.263,-111.701,45.354,-111.646,45.458,-111.553,45.573,-111.493,45.722,-111.464,45.904,-111.43,46.025,-111.39,46.085,-111.406,46.177,-111.516,46.36]]},{"n":"Madre de Dios","z":5,"b":[-72.165,-12.661,-65.395,-10.392],"l":[[-72.165,-12.182,-72.094,-12.063,-72.093,-11.96,-72.139,-11.835,-72.117,-11.742,-72.028,-11.683,-71.931,-11.659,-71.776,-11.675,-71.604,-11.688,-71.533,-11.716,-71.503,-11.765,-71.399,-11.848,-71.221,-11.965,-71.086,-12.08,-70.997,-12.192,-70.91,-12.259,-70.825,-12.278,-70.754,-12.33,-70.695,-12.412,-70.59,-12.489,-70.438,-12.561,-70.319,-12.598,-70.232,-12.6,-70.18,-12.589,-70.155,-12.565,-70.115,-12.568,-70.026,-12.586,-69.768,-12.655,-69.578,-12.661,-69.455,-12.603,-69.391,-12.559,-69.344,-12.554,-69.309,-12.568,-69.268,-12.582,-69.235,-12.575,-69.215,-12.552,-69.187,-12.548,-69.156,-12.573,-69.107,-12.574,-69.063,-12.543,-69.008,-12.534,-68.949,-12.556,-68.859,-12.548,-68.678,-12.495,-68.606,-12.429,-68.556,-12.409,-68.503,-12.414,-68.455,-12.373,-68.412,-12.286,-68.354,-12.22,-68.282,-12.176,-68.239,-12.12,-68.223,-12.053,-68.16,-12.005,-68.051,-11.974,-67.976,-11.897,-67.936,-11.773,-67.881,-11.704,-67.811,-11.688,-67.749,-11.635,-67.695,-11.546,-67.641,-11.494,-67.588,-11.481,-67.519,-11.498,-67.436,-11.545,-67.36,-11.551,-67.293,-11.514,-67.239,-11.463,-67.198,-11.397,-67.127,-11.36,-66.976,-11.344,-66.96,-11.294,-66.916,-11.276,-66.838,-11.274,-66.803,-11.254,-66.786,-11.222,-66.749,-11.23,-66.673,-11.205,-66.567,-11.132,-66.48,-11.095,-66.413,-11.095,-66.37,-11.062,-66.343,-11.026,-66.315,-11.018,-66.276,-11.007,-66.214,-10.95,-66.158,-10.947,-66.118,-10.957,-66.079,-10.937,-66.065,-10.893,-66.061,-10.861,-66.092,-10.848,-66.084,-10.828,-66.036,-10.802,-65.983,-10.795,-65.924,-10.806,-65.902,-10.78,-65.897,-10.725,-65.874,-10.696,-65.82,-10.697,-65.78,-10.667,-65.768,-10.598,-65.74,-10.566,-65.696,-10.57,-65.604,-10.527,-65.395,-10.392]]},{"n":"Magdalena","z":4,"b":[-76.507,1.896,-73.765,11.11],"l":[[-76.507,1.943,-76.281,1.896,-76.179,1.898,-76.097,1.948,-75.791,2.118,-75.69,2.198,-75.692,2.246,-75.662,2.297,-75.599,2.352,-75.572,2.401,-75.583,2.446,-75.518,2.57,-75.376,2.774,-75.292,2.981,-75.266,3.19,-75.159,3.461,-74.971,3.793,-74.858,4.038,-74.82,4.194,-74.824,4.274,-74.869,4.278,-74.861,4.422,-74.771,4.848,-74.756,5.189,-74.723,5.367,-74.668,5.498,-74.646,5.615,-74.657,5.72,-74.641,5.829,-74.596,5.944,-74.576,6.055,-74.579,6.162,-74.531,6.265,-74.433,6.363,-74.385,6.465,-74.386,6.573,-74.317,6.667,-74.178,6.749,-74.053,6.848,-73.943,6.962,-73.9,7.078,-73.925,7.195,-73.907,7.385,-73.846,7.648,-73.83,7.846,-73.858,7.979,-73.852,8.071,-73.79,8.15],[-73.796,8.15,-73.765,8.329,-73.784,8.535,-73.843,8.827,-73.942,8.992,-74.083,9.03,-74.223,9.104,-74.361,9.213,-74.456,9.261,-74.507,9.249,-74.559,9.278,-74.611,9.347,-74.663,9.391,-74.742,9.418,-74.786,9.441,-74.795,9.494,-74.786,9.586,-74.806,9.691,-74.854,9.81,-74.86,9.909,-74.823,9.986,-74.84,10.06,-74.912,10.13,-74.892,10.275,-74.781,10.495,-74.727,10.684,-74.728,10.841,-74.761,10.972,-74.845,11.11]]},{"n":"Mahäna Nadï","z":5,"b":[80.625,20.053,86.245,21.863],"l":[[80.625,20.599,80.63,20.715,80.665,20.765,80.763,20.798,80.889,20.97,81.006,21.046,81.156,21.083,81.271,21.22,81.35,21.456,81.428,21.588,81.543,21.632,81.641,21.59,81.681,21.622,81.694,21.713,81.745,21.764,81.834,21.776,81.92,21.809,82.003,21.863,82.114,21.848,82.255,21.762,82.376,21.728,82.479,21.745,82.723,21.739,83.3,21.695],[83.857,21.52,83.949,21.454,83.943,21.337,83.83,21.055,83.832,20.962,83.866,20.907,83.932,20.86,84.046,20.846,84.21,20.867,84.31,20.847,84.346,20.787,84.444,20.73,84.603,20.674,84.708,20.615,84.757,20.552,84.922,20.48,85.344,20.357,85.665,20.443,85.832,20.445,85.952,20.393,86.048,20.311,86.159,20.143,86.177,20.094,86.245,20.053]]},{"n":"Malyy Yenisey","z":4,"b":[94.465,51.295,95.78,51.734],"l":[[95.78,51.295,95.558,51.476,95.23,51.585,94.465,51.734]]},{"n":"Mamberamo","z":5,"b":[137.815,-3.202,138.702,-1.484],"l":[[138.702,-3.191,138.668,-3.202,138.623,-3.198,138.598,-3.158,138.592,-3.027,138.557,-2.949,138.494,-2.925,138.453,-2.854,138.434,-2.736,138.407,-2.654,138.356,-2.587,138.221,-2.419,138.123,-2.342,138.018,-2.3,137.923,-2.223,137.838,-2.109,137.815,-2.027,137.852,-1.977,137.858,-1.909,137.834,-1.822,137.838,-1.769,137.852,-1.735,137.844,-1.699,137.849,-1.649,137.911,-1.484]]},{"n":"Mamoré","z":3,"b":[-65.304,-15.86,-64.664,-11.913],"l":[[-64.664,-15.86,-64.72,-15.799,-64.769,-15.676,-64.828,-15.469,-64.89,-15.323,-64.954,-15.237,-64.998,-15.157,-65.021,-15.083,-65.02,-15.025,-64.996,-14.981,-65.017,-14.892,-65.082,-14.756,-65.087,-14.717,-65.066,-14.697,-65.025,-14.615,-64.978,-14.446,-64.976,-14.327,-65.019,-14.257,-65.038,-14.199,-65.034,-14.153,-65.062,-14.086,-65.121,-13.997,-65.167,-13.952,-65.199,-13.949,-65.211,-13.917,-65.201,-13.855,-65.232,-13.781,-65.304,-13.694,-65.302,-13.533,-65.226,-13.299,-65.191,-13.204,-65.183,-13.144,-65.137,-13.112,-65.118,-13.053,-65.099,-13.022,-65.056,-12.998,-65.029,-12.937,-65.017,-12.839,-65.053,-12.694,-65.137,-12.501,-65.169,-12.39,-65.15,-12.361,-65.151,-12.335,-65.171,-12.311,-65.169,-12.292,-65.142,-12.268,-65.145,-12.235,-65.173,-12.165,-65.155,-12.126,-65.122,-12.111,-65.114,-12.081,-65.126,-12.027,-65.114,-11.986,-65.061,-11.955,-65.031,-11.913]]},{"n":"Mananantanana","z":5,"b":[45.653,-22.011,47.167,-21.275],"l":[[47.167,-22.011,47.164,-21.973,47.101,-21.939,46.976,-21.896,46.905,-21.852,46.879,-21.786,46.73,-21.736,46.675,-21.653,46.665,-21.543,46.637,-21.513,46.596,-21.473,46.574,-21.464,46.493,-21.38,46.345,-21.302,46.186,-21.275,46.016,-21.301,45.898,-21.348,45.829,-21.416,45.76,-21.444,45.653,-21.428]]},{"n":"Mangoky","z":5,"b":[43.542,-21.89,45.653,-21.316],"l":[[45.653,-21.428,45.514,-21.44,45.425,-21.501,45.293,-21.672,44.806,-21.724,44.565,-21.77,44.409,-21.828,44.302,-21.845,44.244,-21.818,44.166,-21.829,44.066,-21.876,43.986,-21.89,43.914,-21.872,43.895,-21.861,43.874,-21.838,43.785,-21.637,43.697,-21.501,43.542,-21.316]]},{"n":"Damqogkanbab","z":2.1,"b":[82.4,29.126,87.66,30.411],"l":[[82.4,30.411,82.723,30.365,82.825,30.371,82.886,30.336,82.939,30.261,83.164,30.126,83.761,29.833,83.925,29.761,83.983,29.711,83.987,29.659,84.014,29.617,84.063,29.586,84.189,29.555,84.392,29.525,84.501,29.452,84.517,29.335,84.634,29.254,84.854,29.209,85.014,29.221,85.115,29.291,85.226,29.313,85.409,29.274,85.592,29.204,85.867,29.195,86.295,29.225,86.734,29.209,87.184,29.148,87.472,29.126,87.66,29.152]]},{"n":"Marañón","z":4.7,"b":[-78.716,-9.913,-73.489,-4.433],"l":[[-76.617,-9.913,-76.711,-9.64,-76.765,-9.473,-76.766,-9.365,-76.733,-9.26,-76.835,-9.117,-77.073,-8.937,-77.224,-8.769,-77.29,-8.612,-77.346,-8.522,-77.392,-8.496,-77.437,-8.424,-77.483,-8.304,-77.534,-8.215,-77.592,-8.158,-77.621,-8.057,-77.622,-7.91,-77.719,-7.628,-77.909,-7.21,-78.01,-6.936,-78.02,-6.805,-78.062,-6.67,-78.138,-6.529,-78.219,-6.432,-78.306,-6.378,-78.374,-6.301,-78.45,-6.15,-78.568,-6.089,-78.643,-6.017,-78.716,-5.915,-78.703,-5.762,-78.603,-5.561,-78.528,-5.446,-78.479,-5.417,-78.456,-5.355,-78.459,-5.259,-78.434,-5.198,-78.382,-5.171,-78.366,-5.114,-78.387,-5.027,-78.382,-4.964,-78.35,-4.924,-78.335,-4.871,-78.336,-4.804,-78.305,-4.762,-78.242,-4.744,-78.207,-4.708,-78.202,-4.653,-78.163,-4.632,-78.091,-4.646,-77.958,-4.598,-77.763,-4.488,-77.623,-4.433,-77.538,-4.434,-77.473,-4.45,-77.43,-4.481,-77.408,-4.526,-77.41,-4.584,-77.387,-4.617,-77.342,-4.623,-77.333,-4.642,-77.344,-4.651,-77.338,-4.683,-77.307,-4.729,-77.282,-4.732,-77.263,-4.69,-77.169,-4.708,-77.002,-4.784,-76.832,-4.822,-76.659,-4.823,-76.566,-4.84,-76.552,-4.873,-76.511,-4.901,-76.442,-4.926,-76.352,-4.935,-76.24,-4.928,-76.173,-4.944,-76.137,-4.999,-75.841,-5.039,-75.723,-5.043,-75.686,-5.025,-75.654,-5.031,-75.629,-5.062,-75.559,-5.006,-75.445,-4.865,-75.259,-4.78,-75.002,-4.751,-74.847,-4.691,-74.796,-4.6,-74.722,-4.529,-74.625,-4.478,-74.549,-4.485,-74.492,-4.549,-74.413,-4.606,-74.312,-4.654,-74.247,-4.638,-74.218,-4.556,-74.177,-4.502,-74.125,-4.477,-74.053,-4.474,-73.962,-4.491,-73.888,-4.531,-73.829,-4.592,-73.724,-4.579,-73.489,-4.445]]},{"n":"Mekong","z":2.1,"b":[101.564,10.001,106.136,18.423],"l":[[104.951,11.591,104.951,11.508,104.976,11.472,105.026,11.453,105.041,11.428,105.023,11.395,105.044,11.212,105.105,10.879,105.311,10.535,105.831,10.001],[104.951,11.591,105.181,11.446,105.266,11.356,105.282,11.274,105.266,11.18,105.218,11.075,105.199,10.978,105.21,10.89,105.249,10.825,105.315,10.785,105.358,10.723,105.377,10.641,105.49,10.529,105.697,10.39,105.879,10.299,105.931,10.284,106.136,10.222],[101.564,17.821,101.687,17.889,101.744,17.953,101.775,18.033,101.819,18.065,101.875,18.046,101.947,18.081,102.035,18.17,102.101,18.211,102.148,18.204,102.232,18.149,102.352,18.046,102.459,17.985,102.553,17.965,102.598,17.927,102.596,17.87,102.617,17.833,102.661,17.818,102.68,17.824,102.675,17.852,102.718,17.892,102.807,17.946,102.899,17.977,102.991,17.986,103.051,18.029,103.091,18.138],[103.09,18.137,103.091,18.138],[103.091,18.138,103.092,18.141],[103.091,18.138,103.149,18.222,103.2,18.259,103.263,18.278,103.28,18.305,103.249,18.339,103.252,18.373,103.288,18.408,103.367,18.423,103.488,18.418,103.63,18.383,103.792,18.316,103.899,18.295,103.95,18.319,104.049,18.217,104.196,17.988,104.323,17.816,104.428,17.699,104.539,17.609,104.656,17.547,104.74,17.462,104.816,17.3,104.759,17.077,104.744,16.884,104.751,16.648,104.819,16.466,104.95,16.34,105.026,16.238,105.047,16.16,105.149,16.094,105.331,16.038,105.406,15.987,105.376,15.942,105.373,15.89,105.399,15.83,105.462,15.78,105.562,15.741,105.622,15.7,105.641,15.657,105.639,15.586,105.616,15.488,105.574,15.413,105.513,15.361,105.52,15.331,105.595,15.324,105.666,15.273,105.782,15.121,105.868,15.033,105.907,14.944,105.926,14.826,105.915,14.713,105.873,14.606,105.86,14.495,105.873,14.38,105.864,14.267,105.832,14.158,105.855,14.07,105.934,14.005,105.981,13.948,105.996,13.9,105.996,13.852,105.982,13.803,105.991,13.752,106.021,13.698,106.005,13.609,105.94,13.486,105.918,13.405,105.949,13.35,106.023,13.196,106.025,13.061,105.981,12.893,105.978,12.683,106.018,12.428,105.985,12.289,105.88,12.266,105.763,12.264,105.636,12.284,105.543,12.204,105.486,12.025,105.389,11.936,105.252,11.939,105.168,11.92,105.137,11.88,105.09,11.855,105.025,11.845,105,11.822,105.013,11.786,105.002,11.747,104.965,11.706,104.948,11.662,104.951,11.591]]},{"n":"Mendoza","z":5,"b":[-70.136,-32.355,-69.418,-31.283],"l":[[-70.136,-32.238,-70.043,-32.334,-69.97,-32.355,-69.889,-32.332,-69.833,-32.281,-69.803,-32.202,-69.796,-32.144,-69.81,-32.106,-69.787,-32.033,-69.698,-31.871,-69.589,-31.782,-69.521,-31.693,-69.458,-31.574,-69.427,-31.455,-69.429,-31.304,-69.418,-31.283]]},{"n":"Meta","z":5,"b":[-73.917,3.958,-67.423,6.29],"l":[[-73.872,4.67,-73.909,4.608,-73.917,4.541,-73.908,4.447,-73.886,4.365,-73.831,4.258,-73.741,4.193,-73.719,4.142,-73.733,4.083,-73.571,4.022,-73.233,3.96,-73.034,3.958,-72.976,4.014,-72.927,4.1,-72.885,4.215,-72.836,4.282,-72.748,4.313,-72.479,4.332,-72.373,4.356,-72.34,4.392,-72.28,4.42,-72.193,4.44,-72.116,4.438,-72.047,4.412,-71.962,4.443,-71.86,4.533,-71.661,4.637,-71.364,4.756,-71.174,4.844,-71.09,4.901,-71.027,4.977,-70.982,5.071,-70.894,5.167,-70.762,5.265,-70.687,5.336,-70.671,5.382,-70.52,5.465,-70.234,5.585,-70.03,5.743,-69.907,5.939,-69.768,6.043,-69.611,6.056,-69.507,6.077,-69.432,6.122,-69.357,6.148,-69.311,6.138,-69.268,6.1,-69.195,6.115,-69.09,6.184,-68.937,6.198,-68.736,6.157,-68.472,6.157,-68.143,6.198,-67.939,6.242,-67.859,6.29,-67.727,6.285,-67.423,6.202]]},{"n":"Min","z":5,"b":[103.719,28.779,104.62,29.537],"l":[[103.719,29.537,103.767,29.517,103.832,29.386,103.866,29.316,103.857,29.29,103.926,29.224,104.145,29.062,104.251,28.996,104.291,28.948,104.302,28.896,104.38,28.849,104.62,28.779]]},{"n":"Mississippi","z":2,"b":[-95.026,28.981,-89.016,47.482],"l":[[-89.376,28.981,-89.257,29.155,-89.016,29.203],[-89.261,29.155,-89.289,29.178],[-89.289,29.178,-89.181,29.336],[-89.289,29.178,-89.433,29.294,-89.519,29.347,-89.616,29.387,-89.666,29.416,-89.67,29.444,-89.714,29.486,-89.758,29.506,-89.869,29.608,-89.973,29.689,-90.007,29.754,-90.012,29.811,-89.987,29.86,-89.958,29.876,-89.932,29.878,-89.923,29.888,-89.923,29.903,-89.933,29.914,-90.041,29.932,-90.25,29.943,-90.39,29.976,-90.464,30.032,-90.582,30.048,-90.744,30.023,-90.849,30.04,-90.878,30.075,-90.897,30.098,-90.944,30.127,-90.99,30.126,-91.021,30.147,-91.038,30.181,-91.074,30.204,-91.11,30.209,-91.133,30.227,-91.13,30.248,-91.148,30.268,-91.179,30.285,-91.191,30.307,-91.187,30.328,-91.195,30.352,-91.226,30.368,-91.235,30.402,-91.218,30.479,-91.226,30.505,-91.258,30.534,-91.257,30.565,-91.276,30.615,-91.338,30.706,-91.388,30.744,-91.423,30.753,-91.529,30.756,-91.557,30.78,-91.547,30.815,-91.523,30.854,-91.526,30.871,-91.546,30.908,-91.618,30.938,-91.646,30.952,-91.66,30.972,-91.657,30.996,-91.653,31.03,-91.611,31.065,-91.596,31.11,-91.631,31.218,-91.618,31.277,-91.559,31.288,-91.529,31.345,-91.529,31.449,-91.506,31.519,-91.459,31.555,-91.415,31.64,-91.373,31.774,-91.286,31.884,-91.157,31.971,-91.093,32.046,-91.089,32.104,-91.075,32.131,-91.048,32.146,-91.005,32.186,-90.948,32.24,-90.938,32.277,-90.94,32.303,-90.945,32.331,-90.99,32.381,-91.059,32.424,-91.104,32.49,-91.101,32.541,-91.116,32.586,-91.15,32.625,-91.142,32.661,-91.094,32.694,-91.092,32.726,-91.136,32.756,-91.136,32.809,-91.093,32.885,-91.096,32.938,-91.144,32.967,-91.153,33.041,-91.107,33.218,-91.117,33.241,-91.163,33.294,-91.168,33.359,-91.141,33.439,-91.154,33.5,-91.193,33.523,-91.203,33.547,-91.215,33.583,-91.173,33.617,-91.17,33.647,-91.207,33.676,-91.184,33.716,-91.1,33.767,-91.077,33.797,-91.053,33.828,-91.038,33.901,-91.05,33.945,-91.07,33.974,-91.06,33.989,-91.043,34.003,-90.943,34.03,-90.915,34.049,-90.909,34.072,-90.94,34.129,-90.91,34.189,-90.817,34.252,-90.768,34.308,-90.762,34.356,-90.718,34.392,-90.635,34.414,-90.584,34.504,-90.564,34.661,-90.539,34.728,-90.497,34.738,-90.475,34.76,-90.468,34.805,-90.416,34.852,-90.32,34.899,-90.282,34.945,-90.293,34.97,-90.294,35.004,-90.256,35.046,-90.142,35.114,-90.093,35.204,-90.108,35.314,-90.066,35.414,-89.967,35.503,-89.927,35.592,-89.943,35.679,-89.895,35.751,-89.78,35.806,-89.764,35.828,-89.768,35.845,-89.77,35.864,-89.769,35.872,-89.764,35.889,-89.738,35.899,-89.704,35.907,-89.689,35.921,-89.689,35.944,-89.709,35.983,-89.698,36.03,-89.642,36.105,-89.634,36.168,-89.675,36.221,-89.654,36.248,-89.628,36.255,-89.595,36.259,-89.584,36.272,-89.584,36.288,-89.612,36.322,-89.604,36.352,-89.555,36.373,-89.541,36.427,-89.56,36.513,-89.555,36.541,-89.55,36.553,-89.536,36.555,-89.52,36.555,-89.497,36.529,-89.485,36.498,-89.471,36.489,-89.452,36.498,-89.397,36.588,-89.351,36.611,-89.331,36.616,-89.28,36.6,-89.256,36.594,-89.203,36.626,-89.156,36.716,-89.13,36.811,-89.122,36.912,-89.154,36.992,-89.225,37.053,-89.272,37.08,-89.281,37.078,-89.292,37.071,-89.296,37.052,-89.293,37.038,-89.286,37.023,-89.29,37.013,-89.301,37.012,-89.352,37.036,-89.414,37.102,-89.487,37.213,-89.503,37.301,-89.462,37.366,-89.462,37.434,-89.503,37.506,-89.523,37.576,-89.523,37.645,-89.623,37.747,-89.823,37.884,-89.97,37.969,-90.064,38.001,-90.174,38.069,-90.299,38.174,-90.366,38.254,-90.374,38.31,-90.317,38.458,-90.151,38.781,-90.132,38.819,-90.127,38.833,-90.134,38.847,-90.15,38.86,-90.22,38.898,-90.354,38.93,-90.478,38.948,-90.577,38.91,-90.664,38.987,-90.737,39.182,-90.919,39.387,-91.21,39.603,-91.379,39.749,-91.426,39.827,-91.467,39.943,-91.502,40.098,-91.5,40.226,-91.442,40.379,-91.391,40.397,-91.381,40.448,-91.394,40.537,-91.339,40.604,-91.215,40.65,-91.102,40.75,-91,40.905,-90.954,41.016,-90.964,41.083,-91.001,41.146,-91.067,41.206,-91.082,41.28,-91.048,41.369,-90.939,41.426,-90.754,41.45,-90.587,41.495,-90.437,41.56,-90.348,41.632,-90.319,41.71,-90.276,41.77,-90.218,41.813,-90.177,41.884,-90.152,41.982,-90.149,42.054,-90.168,42.1,-90.233,42.154,-90.347,42.215,-90.41,42.272,-90.425,42.326,-90.485,42.383,-90.59,42.445,-90.657,42.521,-90.688,42.61,-90.789,42.677,-90.96,42.72,-91.07,42.789,-91.118,42.882,-91.148,42.986,-91.161,43.102,-91.148,43.2,-91.107,43.278,-91.117,43.331,-91.178,43.359,-91.219,43.395,-91.238,43.44,-91.278,43.797,-91.32,43.936,-91.383,43.991,-91.515,44.054,-91.716,44.126,-91.841,44.194,-91.912,44.289,-92.055,44.4,-92.149,44.445,-92.24,44.462,-92.313,44.498,-92.367,44.552,-92.442,44.587,-92.537,44.601,-92.644,44.645,-92.82,44.758,-92.963,44.79,-93.03,44.838,-93.07,44.912,-93.119,44.937,-93.175,44.913,-93.232,44.963,-93.291,45.087,-93.349,45.167,-93.408,45.202,-93.936,45.389,-94.063,45.439,-94.133,45.496,-94.189,45.577,-94.212,45.632,-94.202,45.662,-94.238,45.714,-94.32,45.789,-94.371,45.853,-94.389,45.906,-94.386,45.972,-94.363,46.051,-94.366,46.125,-94.394,46.194,-94.333,46.284,-94.183,46.396,-94.091,46.482,-94.057,46.543,-93.959,46.569,-93.798,46.559,-93.692,46.586,-93.641,46.651,-93.572,46.698,-93.486,46.725,-93.404,46.807,-93.325,46.944,-93.309,47.023,-93.357,47.044,-93.402,47.09,-93.445,47.16,-93.509,47.211,-93.593,47.243,-93.676,47.249,-93.759,47.228,-93.798,47.242,-93.795,47.29,-93.829,47.312,-93.901,47.306,-93.976,47.336,-94.094,47.433],[-95.026,47.156,-94.968,47.318,-94.877,47.451,-94.794,47.482,-94.637,47.44],[-94.508,47.457,-94.326,47.406]]},{"n":"Missouri","z":2,"b":[-111.989,38.579,-90.133,48.15],"l":[[-98.56,43.06,-98.143,42.852,-97.965,42.806,-97.881,42.85,-97.698,42.875,-97.417,42.881,-97.239,42.861,-97.164,42.813,-97.02,42.76,-96.805,42.701,-96.676,42.638,-96.633,42.569,-96.558,42.525,-96.451,42.506,-96.384,42.418,-96.356,42.26,-96.295,42.133,-96.201,42.036,-96.154,41.965,-96.154,41.92,-96.131,41.872,-96.087,41.821,-96.069,41.734,-96.078,41.611,-96.059,41.546,-96.011,41.538,-95.991,41.52,-95.997,41.494,-95.982,41.474,-95.944,41.461,-95.933,41.425,-95.95,41.367,-95.936,41.331,-95.892,41.316,-95.884,41.288,-95.911,41.245,-95.907,41.212,-95.871,41.188,-95.847,41.08,-95.834,40.889,-95.838,40.778,-95.859,40.745,-95.826,40.676,-95.74,40.571,-95.685,40.466,-95.662,40.363,-95.607,40.295,-95.521,40.265,-95.464,40.198,-95.434,40.096,-95.362,40.01,-95.116,39.872,-95.056,39.871,-94.984,39.891,-94.93,39.861,-94.893,39.782,-94.899,39.741,-94.946,39.739,-95.005,39.692,-95.075,39.597,-95.058,39.509,-94.953,39.426,-94.882,39.343,-94.847,39.259,-94.778,39.194,-94.622,39.124,-94.504,39.124,-94.421,39.151,-94.334,39.204,-94.239,39.207,-94.136,39.159,-94.028,39.154,-93.915,39.19,-93.773,39.211,-93.604,39.218,-93.513,39.237,-93.501,39.27,-93.469,39.271,-93.418,39.24,-93.343,39.27,-93.243,39.361,-93.108,39.359,-92.939,39.265,-92.877,39.192,-92.922,39.141,-92.933,39.087,-92.909,39.03,-92.816,38.993,-92.654,38.976,-92.503,38.89,-92.293,38.659,-92.124,38.586,-91.978,38.598,-91.801,38.67,-91.619,38.706,-91.433,38.707,-91.238,38.667,-91.034,38.585,-90.872,38.579,-90.752,38.648,-90.661,38.681,-90.598,38.681,-90.513,38.732,-90.404,38.835,-90.316,38.86,-90.256,38.845,-90.133,38.819],[-100.394,44.436,-100.372,44.415,-100.345,44.374,-100.225,44.329,-100.019,44.286,-99.922,44.245,-99.934,44.205,-99.921,44.171,-99.883,44.142,-99.804,44.122,-99.685,44.113,-99.647,44.132,-99.691,44.179,-99.684,44.205,-99.626,44.208,-99.587,44.178,-99.568,44.115,-99.51,44.06,-99.364,43.991],[-111.699,46.663,-111.758,46.671,-111.814,46.711,-111.885,46.786,-111.913,46.837,-111.898,46.863,-111.919,46.916,-111.976,46.997,-111.989,47.057,-111.959,47.096,-111.616,47.342,-111.454,47.415,-111.348,47.412,-111.293,47.439,-111.29,47.498,-111.237,47.541,-111.137,47.567,-111.037,47.632,-110.938,47.736,-110.856,47.784,-110.792,47.777,-110.617,47.843,-110.331,47.984,-110.167,48.043,-110.124,48.021,-110.089,47.968,-110.062,47.885,-109.995,47.816,-109.886,47.762,-109.609,47.755,-109.164,47.794,-108.935,47.795,-108.922,47.758,-108.874,47.718,-108.788,47.677,-108.607,47.643,-108.191,47.602],[-101.4,47.506,-101.423,47.484,-101.407,47.431,-101.362,47.339,-101.248,47.282,-101.066,47.26,-100.979,47.231,-100.987,47.196,-100.967,47.148,-100.917,47.087,-100.902,47.034,-100.921,46.99,-100.892,46.904,-100.777,46.714],[-106.406,48.021,-106.367,48.058,-106.221,48.058,-105.956,48.033,-105.729,48.042,-105.542,48.086,-105.34,48.1,-105.125,48.083,-104.978,48.095,-104.899,48.136,-104.75,48.15,-104.529,48.137,-104.277,48.093,-103.996,48.02,-103.835,47.994,-103.772,48.027]]},{"n":"Mitchell","z":5,"b":[141.581,-16.484,143.469,-15.195],"l":[[143.469,-16.484,143.138,-16.38,142.75,-16.104,142.565,-16.001,142.452,-15.981,142.365,-15.947,142.271,-15.874,142.043,-15.473,141.877,-15.306,141.751,-15.245,141.581,-15.195]]},{"n":"Black Volta","z":4.7,"b":[-5.263,8.16,-1.713,12.738],"l":[[-5.263,11.139,-5.185,11.124,-5.126,11.154,-5.059,11.224,-4.99,11.264,-4.879,11.282,-4.715,11.406,-4.639,11.432,-4.596,11.402,-4.539,11.413,-4.469,11.467,-4.382,11.507,-4.277,11.533,-4.124,11.697,-3.922,11.998,-3.82,12.176,-3.815,12.232,-3.784,12.296,-3.725,12.366,-3.666,12.401,-3.607,12.402,-3.569,12.477,-3.55,12.628,-3.511,12.715,-3.45,12.738,-3.406,12.72,-3.379,12.663,-3.336,12.645,-3.278,12.666,-3.249,12.64,-3.248,12.567,-3.208,12.5,-3.126,12.44,-3.023,12.397,-2.899,12.372,-2.849,12.339,-2.875,12.298,-2.864,12.235,-2.816,12.151,-2.824,12.098,-2.886,12.075,-2.896,12.019,-2.853,11.927,-2.854,11.861,-2.901,11.819,-2.883,11.779,-2.802,11.743,-2.79,11.606,-2.849,11.369,-2.858,11.242,-2.817,11.226,-2.804,11.165,-2.83,11.008,-2.907,10.728,-2.915,10.592,-2.878,10.508,-2.837,10.455,-2.791,10.432,-2.787,10.402,-2.823,10.363,-2.82,10.323,-2.777,10.282,-2.767,10.238,-2.788,10.193,-2.783,10.083,-2.751,9.91,-2.75,9.797,-2.781,9.746,-2.766,9.658,-2.706,9.534,-2.686,9.432,-2.706,9.351,-2.702,9.302,-2.674,9.283,-2.689,9.219,-2.747,9.11,-2.747,9.045,-2.69,9.025,-2.649,8.957,-2.625,8.84,-2.557,8.731,-2.446,8.631,-2.364,8.514,-2.311,8.38,-2.247,8.274,-2.17,8.196,-2.087,8.16,-2,8.166,-1.935,8.192,-1.892,8.237,-1.845,8.255,-1.793,8.244,-1.768,8.26,-1.77,8.304,-1.797,8.328,-1.851,8.333,-1.887,8.357,-1.904,8.398,-1.899,8.47,-1.871,8.573,-1.821,8.625,-1.713,8.631]]},{"n":"Murray","z":3,"b":[139.289,-36.967,148.197,-33.999],"l":[[148.005,-36.967,148.162,-36.73,148.197,-36.625,148.147,-36.546,148.034,-36.181,147.975,-36.046,147.931,-36.019,147.762,-35.956,147.674,-35.954,147.596,-35.982,147.538,-35.984,147.502,-35.959,147.458,-35.954,147.407,-35.971,147.371,-36.004,147.35,-36.052,147.323,-36.07,147.272,-36.049],[147.045,-36.108,146.896,-36.1,146.761,-36.067,146.59,-36.005,146.47,-35.994,146.403,-36.033,146.331,-36.054,146.216,-36.056],[146.028,-36.015,145.815,-35.977,145.692,-35.931,145.588,-35.863,145.432,-35.835,145.224,-35.846,145.083,-35.862,145.008,-35.884,144.962,-35.923,144.943,-35.978,144.944,-36.029,144.965,-36.075,144.916,-36.107,144.799,-36.126,144.668,-36.088,144.524,-35.994,144.427,-35.905,144.378,-35.82,144.29,-35.73,144.163,-35.634,143.971,-35.528,143.714,-35.411,143.585,-35.327,143.582,-35.275,143.54,-35.234,143.459,-35.203,143.404,-35.168,143.377,-35.127,143.363,-35.032,143.361,-34.882,143.325,-34.788,143.221,-34.733,142.965,-34.693,142.857,-34.655,142.812,-34.607,142.774,-34.588,142.742,-34.598,142.705,-34.649,142.663,-34.741,142.619,-34.783,142.571,-34.775,142.507,-34.712,142.426,-34.593,142.388,-34.485,142.394,-34.387,142.373,-34.333,142.328,-34.322,142.287,-34.286,142.252,-34.223,142.158,-34.173,141.926,-34.117,141.761,-34.112,141.666,-34.137,141.586,-34.191,141.47,-34.183,141.318,-34.112,141.192,-34.073,141.094,-34.066,141.026,-34.043,140.997,-34.016,140.948,-33.999,140.914,-34.011,140.806,-34.062,140.763,-34.096,140.784,-34.113,140.787,-34.145,140.773,-34.189,140.715,-34.246,140.613,-34.315,140.565,-34.375,140.571,-34.426,140.533,-34.433,140.452,-34.394,140.392,-34.324,140.352,-34.223,140.248,-34.175,140.08,-34.181,139.979,-34.176,139.944,-34.16,139.933,-34.136,139.93,-34.103,139.886,-34.074,139.801,-34.057,139.753,-34.051,139.721,-34.077,139.657,-34.148,139.607,-34.383,139.603,-34.544,139.645,-34.631,139.635,-34.695,139.576,-34.734,139.57,-34.787,139.617,-34.852,139.592,-34.878,139.495,-34.864,139.4,-34.884,139.307,-34.939,139.289,-34.981,139.347,-35.011,139.355,-35.048,139.313,-35.093,139.299,-35.133,139.31,-35.168,139.351,-35.207,139.423,-35.25,139.435,-35.297,139.374,-35.365,139.303,-35.399]]},{"n":"N. Fork Kuskokwim","z":5,"b":[-154.721,63.079,-151.674,64.326],"l":[[-151.674,64.301,-152.207,64.326,-152.399,64.235,-152.499,64.161,-152.568,64.072,-152.597,64.008,-152.585,63.97,-152.791,63.884,-153.215,63.751,-153.455,63.649,-153.513,63.577,-153.605,63.515,-153.733,63.464,-153.85,63.403,-153.959,63.332,-154.009,63.286,-154.001,63.265,-154.178,63.211,-154.721,63.079]]},{"n":"White Volta","z":5,"b":[-2.135,9.117,-0.336,14.01],"l":[[-2.083,14.01,-2.055,13.681,-2.108,13.566,-2.104,13.473,-2.063,13.365,-2.069,13.253,-2.123,13.138,-2.135,13.068,-2.105,13.042,-1.989,13.038,-1.787,13.055,-1.628,13.032,-1.511,12.966,-1.411,12.946,-1.327,12.97,-1.262,12.962,-1.216,12.921,-1.183,12.836,-1.163,12.708,-1.141,12.651,-1.115,12.667,-1.096,12.658,-1.083,12.624,-1.092,12.333,-1.045,12.153,-0.94,11.975,-0.858,11.867,-0.798,11.829,-0.771,11.754,-0.778,11.64,-0.724,11.557,-0.609,11.505,-0.534,11.454,-0.499,11.405,-0.492,11.37,-0.512,11.35,-0.483,11.291,-0.406,11.194,-0.373,11.081,-0.384,10.954,-0.374,10.867,-0.344,10.82,-0.339,10.784,-0.359,10.76,-0.361,10.72,-0.336,10.638,-0.608,10.546,-0.7,10.546,-0.703,10.608,-0.724,10.64,-0.765,10.642,-0.817,10.604,-0.88,10.526,-0.937,10.499,-0.988,10.522,-1.027,10.522,-1.054,10.497,-1.067,10.445,-1.067,10.367,-1.05,10.303,-1.018,10.253,-1.024,10.206,-1.068,10.161,-1.07,10.117,-1.031,10.074,-1,9.961,-0.977,9.777,-1.021,9.696,-1.13,9.715,-1.237,9.686,-1.342,9.608,-1.385,9.523,-1.366,9.431,-1.328,9.359,-1.27,9.307,-1.242,9.254,-1.246,9.199,-1.223,9.158,-1.152,9.117]]},{"n":"Nanpan","z":4,"b":[103.053,23.802,106.944,25.879],"l":[[103.922,25.879,103.855,25.801,103.836,25.725,103.846,25.574,103.883,25.341,103.867,25.219,103.81,25.129,103.713,25.048,103.488,24.939,103.448,24.935,103.416,24.955,103.392,25,103.326,25.013,103.219,24.994,103.165,24.944,103.165,24.863,103.132,24.774,103.064,24.677,103.053,24.609,103.098,24.57,103.105,24.52,103.074,24.458,103.08,24.321,103.146,24.004,103.169,23.87,103.195,23.818,103.232,23.802,103.262,23.81,103.288,23.84,103.376,23.863,103.527,23.88,103.61,23.916,103.626,23.97,103.619,24.02,103.59,24.065,103.591,24.11,103.623,24.153,103.674,24.185,103.746,24.204,103.825,24.27,103.912,24.385,103.987,24.442,104.052,24.441,104.151,24.472,104.353,24.562,104.537,24.741,104.681,24.659,104.8,24.673,104.941,24.756,105.056,24.85,105.144,24.956,105.208,24.991,105.247,24.956,105.308,24.937,105.39,24.935,105.45,24.905,105.489,24.847,105.583,24.79,105.731,24.735,105.833,24.712,105.888,24.723,105.942,24.707,105.994,24.664,106.065,24.689,106.154,24.783,106.188,24.862,106.166,24.928,106.231,24.979,106.381,25.017,106.509,25.07,106.614,25.139,106.742,25.189,106.874,25.215,106.944,25.225]]},{"n":"Narmada","z":5,"b":[73.112,21.75,81.574,23.129],"l":[[81.574,22.754,81.28,22.877,81.096,22.956,81.027,22.969,81.01,22.943,80.973,22.936,80.917,22.947,80.877,22.979,80.854,23.03,80.832,23.04,80.812,23.01,80.732,22.985,80.59,22.964,80.509,22.925,80.487,22.87,80.484,22.788,80.498,22.678,80.486,22.611,80.448,22.586,80.411,22.584,80.373,22.602,80.333,22.658,80.29,22.75,80.217,22.79,80.06,22.775,79.963,22.852,79.933,22.934,79.938,23.047,79.834,23.112,79.622,23.129,79.419,23.122,79.224,23.091,79.116,23.061,79.096,23.03,78.997,23.024,78.82,23.04,78.672,23.038,78.553,23.016,78.442,22.97,78.338,22.899,78.257,22.868,78.2,22.878,78.158,22.859,78.133,22.812,78.075,22.794,77.984,22.803,77.919,22.825,77.88,22.857,77.79,22.825,77.65,22.729,77.457,22.639,77.086,22.514,76.884,22.458,76.803,22.407,76.776,22.342,76.714,22.289,76.615,22.248,76.53,22.254,76.458,22.309,76.306,22.299,76.071,22.225,75.608,22.146,74.567,22.021,74.466,22.035,74.387,22.012,74.295,21.957,74.202,21.932,74.108,21.938,73.985,21.914,73.832,21.862,73.703,21.872,73.596,21.944,73.518,21.981,73.469,21.985,73.417,21.961,73.363,21.911,73.311,21.897,73.258,21.919,73.235,21.908,73.242,21.866,73.218,21.844,73.163,21.844,73.135,21.821,73.137,21.783,73.112,21.75]]},{"n":"Naryn","z":4.7,"b":[70.355,40.435,78.019,41.88],"l":[[72.654,41.667,72.349,41.497,72.222,41.376,72.171,41.246,72.053,41.117,71.87,40.988,71.597,40.907,71.234,40.872,71.101,40.842,71.062,40.822,71.026,40.809,70.965,40.796,70.863,40.756,70.698,40.661,70.355,40.435],[77.737,41.88,77.978,41.869,78.019,41.836,77.901,41.75,77.763,41.624,77.673,41.567,77.586,41.536,77.278,41.518,76.75,41.51,76.388,41.492,76.092,41.449,75.882,41.459,75.747,41.442,75.617,41.4,75.478,41.397,75.33,41.431,75.066,41.446,74.686,41.443,74.436,41.424,74.317,41.39,74.18,41.4,74.025,41.456,73.946,41.519,73.942,41.591,73.922,41.649,73.886,41.696,73.797,41.74,73.655,41.783,73.513,41.797,73.298,41.776]]},{"n":"Nederrijn","z":4,"b":[5.331,51.872,6.039,51.981],"l":[[6.039,51.872,5.922,51.961,5.747,51.981,5.331,51.963]]},{"n":"Negro","z":3,"b":[-70.336,-41.047,-59.897,2.833],"l":[[-68.014,-38.975,-67.757,-39.052,-67.598,-39.074,-67.453,-39.069,-67.314,-39.085,-67.18,-39.123,-66.987,-39.129,-66.735,-39.104,-66.531,-39.1,-66.373,-39.117,-66.224,-39.154,-66.084,-39.212,-65.931,-39.248,-65.765,-39.261,-65.629,-39.331,-65.521,-39.457,-65.434,-39.593,-65.366,-39.74,-65.234,-39.83,-65.037,-39.862,-64.899,-39.91,-64.82,-39.975,-64.728,-40.025,-64.622,-40.062,-64.503,-40.078,-64.371,-40.074,-64.172,-40.166,-63.905,-40.354,-63.738,-40.455,-63.67,-40.469,-63.574,-40.534,-63.388,-40.709,-63.174,-40.743,-63.028,-40.826,-62.86,-40.948,-62.798,-41.047],[-70.336,2.058,-69.967,2.11,-69.683,2.159,-69.526,2.204,-69.401,2.262,-69.252,2.271,-69.079,2.23,-68.932,2.211,-68.81,2.215,-68.701,2.264,-68.603,2.36,-68.497,2.379,-68.385,2.321,-68.277,2.372,-68.175,2.534,-68.113,2.606,-68.093,2.588,-68.068,2.608,-68.038,2.665,-67.955,2.73,-67.859,2.794,-67.766,2.833,-67.667,2.8,-67.619,2.794,-67.597,2.769,-67.568,2.69,-67.535,2.677,-67.486,2.644,-67.392,2.56,-67.312,2.472,-67.253,2.429,-67.211,2.39,-67.198,2.333,-67.215,2.275,-67.165,2.143,-67.131,2.101,-67.114,2.051,-67.131,2,-67.09,1.94,-66.988,1.68,-66.982,1.601,-66.958,1.564,-66.931,1.458,-66.884,1.358,-66.896,1.29,-66.876,1.223,-66.86,1.022,-66.877,0.938,-66.921,0.903,-67.008,0.887,-67.137,0.889,-67.231,0.768,-67.32,0.403,-67.277,0.156,-67.201,0.008,-67.077,-0.122,-66.984,-0.18,-66.922,-0.164,-66.772,-0.216,-66.535,-0.335,-66.373,-0.377,-66.285,-0.343,-66.172,-0.324,-66.034,-0.322,-65.942,-0.303,-65.896,-0.268,-65.84,-0.264,-65.772,-0.291,-65.681,-0.301,-65.566,-0.292,-65.447,-0.316,-65.261,-0.401,-65.005,-0.457,-64.798,-0.449,-64.43,-0.367,-64.152,-0.314,-63.972,-0.304,-63.796,-0.32,-63.633,-0.382,-63.481,-0.492,-63.31,-0.585,-63.025,-0.702,-62.879,-0.874,-62.73,-0.985,-62.432,-1.146,-61.933,-1.377,-61.881,-1.389,-61.799,-1.389,-61.717,-1.401,-61.62,-1.449,-61.538,-1.53,-61.471,-1.642,-61.437,-1.736,-61.435,-1.814,-61.355,-1.926,-61.197,-2.074,-61.142,-2.15,-61.092,-2.218,-61.04,-2.36,-60.903,-2.553,-60.571,-2.922,-60.451,-3.028,-60.36,-3.065,-60.26,-3.069,-60.158,-3.096,-60.052,-3.145,-59.976,-3.158,-59.897,-3.128]]},{"n":"Nelson","z":5,"b":[-98.012,54.082,-92.802,56.928],"l":[[-97.731,54.082,-97.595,54.241,-97.569,54.319,-97.615,54.373,-97.622,54.429,-97.591,54.487,-97.629,54.53,-97.791,54.571],[-97.901,54.705,-97.924,54.774,-97.957,54.807,-98.008,54.826,-98.012,54.863,-97.946,54.948],[-97.254,55.236,-97.074,55.249,-96.954,55.302,-96.833,55.4,-96.768,55.483,-96.76,55.551,-96.704,55.671,-96.602,55.843,-96.552,55.962,-96.553,56.063],[-95.842,56.291,-95.431,56.339],[-94.779,56.394,-94.344,56.421,-93.996,56.549,-93.591,56.788,-93.262,56.909,-92.882,56.915,-92.802,56.928]]},{"n":"Neuquén","z":4,"b":[-70.803,-38.975,-68.014,-36.529],"l":[[-70.803,-36.529,-70.7,-36.909,-70.648,-37.077,-70.648,-37.149,-70.681,-37.182,-70.698,-37.227,-70.699,-37.282,-70.675,-37.325,-70.626,-37.355,-70.51,-37.375,-70.325,-37.387,-70.199,-37.42,-70.13,-37.474,-70.077,-37.539,-70.04,-37.615,-69.991,-37.658,-69.931,-37.67,-69.886,-37.767,-69.857,-37.947,-69.809,-38.078,-69.74,-38.159,-69.7,-38.245,-69.687,-38.337,-69.604,-38.435,-69.452,-38.538,-69.325,-38.592,-69.224,-38.597,-69.102,-38.543,-68.961,-38.43,-68.849,-38.371,-68.769,-38.366,-68.603,-38.447,-68.351,-38.614,-68.173,-38.766,-68.014,-38.975]]},{"n":"Neva","z":4.7,"b":[30.157,59.811,31.043,59.951],"l":[[31.043,59.951,31.014,59.933,30.982,59.882,30.906,59.843,30.795,59.811,30.652,59.835,30.477,59.917,30.347,59.947,30.22,59.916,30.157,59.904]]},{"n":"Niagara","z":3,"b":[-79.066,43.106,-79.059,43.278],"l":[[-79.059,43.278,-79.066,43.106]]},{"n":"Niger","z":3,"b":[-10.816,4.291,6.776,17.057],"l":[[6.266,4.991,6.224,4.919,6.131,4.844,6.078,4.71,6.055,4.42,6.077,4.291],[6.266,4.991,6.271,4.432],[6.455,5.363,6.374,5.346,6.302,5.316,6.24,5.274,6.21,5.235,6.21,5.201,6.17,5.164,6.09,5.124,5.992,5.13,5.876,5.182,5.738,5.191,5.476,5.154],[6.455,5.363,6.394,5.301,6.349,5.256,6.33,5.221,6.333,5.196,6.358,5.18,6.354,5.153,6.321,5.114,6.292,5.095,6.266,5.095,6.257,5.061,6.266,4.991],[6.557,5.542,6.606,5.386,6.587,5.301,6.522,5.235,6.474,5.156,6.443,5.065,6.461,5.003,6.526,4.97,6.573,4.908,6.599,4.816,6.643,4.729,6.709,4.607,6.715,4.501,6.71,4.452,6.71,4.425,6.715,4.342],[-10.732,9.09,-10.665,9.154,-10.622,9.235,-10.58,9.355,-10.583,9.453,-10.656,9.566,-10.642,9.75,-10.684,9.915,-10.779,10.121,-10.816,10.248,-10.796,10.294,-10.747,10.322,-10.668,10.331,-10.634,10.359,-10.644,10.403,-10.613,10.426,-10.541,10.426,-10.481,10.452,-10.433,10.503,-10.363,10.524,-10.271,10.513,-10.218,10.491,-10.199,10.478,-10.174,10.472,-10.151,10.482,-10.053,10.569,-9.97,10.608,-9.901,10.602,-9.866,10.625,-9.848,10.642,-9.801,10.662,-9.658,10.678,-9.535,10.781,-9.433,10.969,-9.343,11.065,-9.268,11.067,-9.199,11.097,-9.136,11.154,-9.117,11.239,-9.14,11.352,-9.089,11.419,-8.962,11.438,-8.852,11.505,-8.759,11.62,-8.651,11.719,-8.527,11.801,-8.433,11.905,-8.367,12.031,-8.281,12.144,-8.175,12.243,-8.098,12.375,-8.051,12.539,-7.964,12.627,-7.837,12.636,-7.666,12.747,-7.453,12.958,-7.279,13.104,-7.078,13.224,-6.949,13.325,-6.788,13.362,-6.551,13.368,-6.367,13.409,-6.237,13.484,-6.146,13.572,-6.094,13.672,-6.021,13.71,-5.902,13.719,-5.785,13.745,-5.595,13.887,-5.435,13.962,-5.305,13.969,-5.214,14.008,-5.164,14.079,-4.996,14.155,-4.712,14.236,-4.549,14.266,-4.487,14.274,-4.414,14.313,-4.201,14.551,-4.117,14.772,-4.042,14.885,-3.948,14.964,-3.921,15.073,-3.979,15.281,-3.954,15.29,-3.914,15.307,-3.852,15.354,-3.82,15.395,-3.818,15.432,-3.853,15.456,-3.927,15.467,-3.923,15.504,-3.796,15.603,-3.787,15.62,-3.861,15.642,-3.849,15.66,-3.812,15.674,-3.748,15.684,-3.714,15.732,-3.708,15.818,-3.682,15.872,-3.635,15.894,-3.598,15.939,-3.57,16.005,-3.503,16.047],[4.626,9.865,4.644,9.755,4.631,9.669,4.593,9.571,4.586,9.473,4.612,9.374,4.654,9.312,4.713,9.287,4.75,9.243,4.764,9.18,4.83,9.161,4.947,9.185,5.057,9.15,5.159,9.055,5.292,8.989,5.456,8.949,5.604,8.883,5.802,8.742,6.02,8.754,6.159,8.742,6.291,8.71,6.385,8.628,6.438,8.497,6.521,8.377,6.633,8.268,6.716,8.144,6.771,8.006,6.776,7.793,6.71,7.362,6.664,6.973,6.66,6.743,6.691,6.44,6.741,6.202,6.707,5.978,6.556,5.541,6.495,5.464,6.48,5.422,6.49,5.388,6.455,5.363],[-4.304,15.507,-4.338,15.621,-4.317,15.683,-4.252,15.732,-4.208,15.793,-4.185,15.865,-4.138,15.892,-4.066,15.873,-3.961,15.925,-3.824,16.047,-3.689,16.113,-3.556,16.123,-3.467,16.161],[-3.503,16.047,-3.467,16.161],[-3.467,16.161,-3.424,16.226,-3.379,16.261,-3.333,16.266,-3.31,16.292,-3.31,16.337,-3.263,16.386,-3.222,16.408],[-3.222,16.408,-3.189,16.3,-3.187,16.253,-3.206,16.225,-3.234,16.207,-3.271,16.199,-3.302,16.163,-3.329,16.101,-3.396,16.062,-3.503,16.047],[-3.222,16.408,-3.167,16.438,-3.106,16.511,-3.08,16.606,-3.028,16.65,-2.948,16.643,-2.743,16.69,-2.411,16.792,-2.197,16.837,-2.101,16.826,-2.043,16.835,-2.023,16.864,-1.897,16.915,-1.664,16.988,-1.448,17.035,-1.25,17.057,-1.118,17.048,-1.052,17.009,-0.965,16.996,-0.857,17.008,-0.77,16.993,-0.705,16.95,-0.601,16.938,-0.457,16.955,-0.346,16.873,-0.269,16.691,-0.203,16.577,-0.148,16.533,-0.089,16.426,0.003,16.17,0.144,15.983,0.259,15.872,0.397,15.775,0.471,15.679,0.481,15.585,0.548,15.51,0.672,15.456,0.739,15.41,0.749,15.371,0.727,15.283,0.675,15.144,0.657,15.046,0.675,14.988,0.722,14.948,0.799,14.926,0.855,14.861,0.891,14.751,0.965,14.63,1.076,14.498,1.208,14.381,1.362,14.28,1.463,14.146,1.512,13.98,1.581,13.847,1.671,13.747,1.812,13.651,2.004,13.558,2.125,13.476,2.174,13.402,2.236,13.343,2.31,13.299,2.355,13.176,2.37,12.976,2.415,12.818,2.49,12.704,2.521,12.627,2.508,12.59,2.515,12.552,2.54,12.529,2.571,12.53,2.597,12.552,2.615,12.552,2.626,12.531,2.653,12.522,2.692,12.53,2.718,12.519,2.736,12.468,2.768,12.432,2.815,12.419,2.844,12.399,3.15,12.118,3.267,11.992,3.299,11.927,3.36,11.88,3.45,11.852,3.532,11.787,3.595,11.696,3.695,11.62,3.851,11.57,4.022,11.484,4.133,11.391,4.241,11.261,4.368,11.17,4.513,11.115,4.62,11.045,4.688,10.957,4.705,10.837,4.656,10.607]]},{"n":"Nile","z":2,"b":[30.317,15.635,33.969,30.121],"l":[[32.49,15.635,32.535,16.04,32.629,16.256,32.785,16.418,32.929,16.508,33.061,16.526,33.165,16.566,33.241,16.628,33.359,16.686,33.519,16.738,33.635,16.825,33.706,16.944,33.732,17.029,33.712,17.079,33.715,17.151,33.74,17.246,33.81,17.379,33.925,17.55,33.952,17.699,33.969,17.792,33.942,18.107,33.9,18.278,33.845,18.305,33.784,18.364,33.718,18.455,33.682,18.555,33.675,18.662,33.645,18.724,33.59,18.741,33.567,18.847,33.574,19.043,33.539,19.173,33.463,19.237,33.413,19.308,33.38,19.423,33.138,19.503,33.018,19.511,32.938,19.474,32.838,19.383,32.717,19.238,32.62,19.161,32.546,19.152,32.468,19.083,32.385,18.954,32.306,18.89,32.23,18.891,32.153,18.865,32.076,18.812,32.039,18.743,32.044,18.661,32.005,18.606,31.921,18.58,31.8,18.452,31.642,18.223,31.474,18.077,31.296,18.015,31.146,18.002,31.023,18.036,30.911,18.087,30.809,18.154,30.744,18.257,30.717,18.397,30.674,18.499,30.616,18.562,30.562,18.66,30.511,18.794,30.486,18.988,30.488,19.243,30.458,19.471,30.396,19.672,30.35,19.786,30.348,19.822,30.37,19.857,30.5,19.914,30.575,19.967,30.594,20.016,30.6,20.098,30.591,20.214,30.571,20.291,30.539,20.329,30.49,20.351,30.422,20.358,30.366,20.417,30.321,20.527,30.317,20.616,30.352,20.683,30.363,20.735,30.37,20.774,30.397,20.805,30.51,20.84,30.576,20.9,30.595,20.985,30.632,21.044,30.687,21.076,30.709,21.121,30.699,21.177,30.763,21.249,30.903,21.337,30.97,21.399,30.965,21.436,30.994,21.493,31.091,21.609],[32.879,23.988,32.913,24.328,32.913,24.454,32.889,24.478,32.892,24.54,32.92,24.639,32.921,24.77,32.895,24.933,32.871,25.037,32.848,25.082,32.767,25.149,32.629,25.239,32.54,25.35,32.502,25.483,32.493,25.565,32.523,25.612,32.704,25.754,32.764,25.879,32.763,26.034,32.734,26.129,32.678,26.163,32.553,26.143,32.362,26.067,32.229,26.077,32.154,26.171,32.094,26.189,32.041,26.19,32.015,26.2,31.997,26.247,31.966,26.297,31.871,26.375,31.82,26.445,31.812,26.509,31.782,26.542,31.73,26.543,31.706,26.564,31.712,26.605,31.695,26.632,31.655,26.644,31.553,26.762,31.369,27.01,31.341,27.057,31.304,27.099,31.278,27.123,31.245,27.144,30.969,27.356,30.868,27.5,30.889,27.619,30.884,27.768,30.877,27.902,30.863,27.985,30.837,28.038,30.807,28.085,30.774,28.12,30.744,28.165,30.74,28.23,30.762,28.309,30.811,28.401,30.836,28.492,30.837,28.582,30.858,28.682,30.899,28.791,30.995,28.943,31.146,29.136,31.219,29.282,31.215,29.38,31.236,29.489,31.281,29.609,31.287,29.783,31.255,29.999,31.254,30.08,31.238,30.121]]},{"n":"Nmai","z":3,"b":[97.519,25.62,98.373,28.425],"l":[[97.983,28.425,98.192,28.25,98.297,28.132,98.335,28.008,98.342,27.84,98.302,27.73,98.215,27.679,98.154,27.674,98.118,27.715,98.08,27.726,98.04,27.707,98.042,27.629,98.087,27.494,98.172,27.329,98.296,27.133,98.362,26.974,98.373,26.85,98.354,26.701,98.281,26.443,98.158,26.03,98.096,25.867,98.055,25.815,97.976,25.74,97.86,25.641,97.775,25.62,97.72,25.678,97.641,25.706,97.558,25.703,97.519,25.701]]},{"n":"North Saskatchewan","z":4,"b":[-117.132,51.993,-105.065,54.041],"l":[[-117.132,52.014,-117.011,52.077,-116.937,52.072,-116.838,51.993,-116.534,52.022,-116.438,52.079,-116.448,52.176,-116.39,52.276,-116.262,52.379,-116.053,52.437,-115.764,52.453,-115.576,52.44,-115.488,52.399,-115.398,52.387,-115.308,52.404,-115.211,52.393,-115.108,52.355,-115.029,52.345,-114.975,52.363,-114.984,52.463,-115.057,52.644,-115.111,52.74,-115.146,52.752,-115.164,52.785,-115.166,52.838,-115.184,52.875,-115.218,52.895,-115.227,52.94,-115.211,53.01,-115.159,53.05,-115.071,53.061,-115.014,53.083,-114.974,53.131,-114.841,53.276,-114.729,53.321,-114.596,53.314,-114.483,53.338,-114.388,53.392,-114.262,53.396,-114.104,53.348,-113.947,53.333,-113.789,53.35,-113.691,53.389,-113.651,53.451,-113.569,53.5,-113.445,53.536,-113.306,53.628,-113.153,53.777,-113.024,53.876,-112.919,53.926,-112.856,53.975,-112.834,54.023,-112.732,54.032,-112.55,54.003,-112.41,54.006,-112.313,54.041,-112.193,54.022,-112.05,53.948,-111.92,53.908,-111.802,53.902,-111.737,53.87,-111.726,53.812,-111.578,53.774,-111.292,53.755,-111.094,53.772,-110.986,53.825,-110.858,53.851,-110.712,53.849,-110.563,53.801,-110.411,53.707,-110.302,53.668,-110.235,53.686,-110.123,53.661,-109.967,53.593,-109.826,53.564,-109.702,53.573,-109.613,53.549,-109.558,53.492,-109.535,53.452,-109.543,53.428,-109.482,53.409,-109.352,53.393,-109.236,53.36,-109.135,53.308,-109.022,53.216,-108.897,53.082,-108.71,52.971,-108.459,52.882,-108.343,52.819,-108.362,52.781,-108.361,52.755,-108.328,52.734],[-108.334,52.734,-108.103,52.611,-107.985,52.567,-107.904,52.56,-107.747,52.502,-107.513,52.394,-107.357,52.338,-107.281,52.334,-107.18,52.343,-107.057,52.366,-106.962,52.441,-106.895,52.568,-106.691,52.764,-106.352,53.029,-106.15,53.171,-106.085,53.191,-106.034,53.185,-105.996,53.154,-105.88,53.163,-105.684,53.211,-105.456,53.236,-105.065,53.24]]},{"n":"Nu","z":4,"b":[96.596,20.951,99.089,30.448],"l":[[96.596,30.448,96.675,30.443,96.707,30.406,96.717,30.333,96.854,30.245,97.116,30.142,97.29,30.032,97.375,29.918,97.424,29.817,97.436,29.729,97.477,29.635,97.546,29.536,97.66,29.447,97.819,29.367,97.949,29.226,98.052,29.023,98.163,28.87,98.343,28.715,98.367,28.577,98.391,28.522,98.423,28.502,98.451,28.402,98.474,28.22,98.536,28.088,98.635,28.003,98.682,27.913,98.676,27.817,98.709,27.722,98.816,27.581,98.869,27.341,98.889,27.059,98.895,26.656,98.885,26.362,98.857,26.177,98.856,25.931,98.882,25.625,98.882,25.372,98.856,25.173,98.871,24.991,98.927,24.828,98.98,24.723,99.03,24.677,99.036,24.594,98.998,24.475,99.016,24.383,99.089,24.317,99.058,24.248,98.922,24.175,98.764,24.116,98.583,24.07,98.473,24.026,98.432,23.984,98.42,23.933,98.437,23.872,98.496,23.783,98.598,23.667,98.638,23.563,98.617,23.472,98.635,23.417,98.692,23.398,98.721,23.357,98.723,23.294,98.654,23.248,98.514,23.22,98.474,23.173,98.534,23.109,98.548,23.068,98.516,23.051,98.504,23.024,98.513,22.988,98.554,22.953,98.628,22.918,98.629,22.871,98.558,22.812,98.553,22.706,98.614,22.555,98.626,22.432,98.588,22.336,98.587,22.251,98.625,22.177,98.676,22.115,98.742,22.065,98.718,21.995,98.605,21.905,98.574,21.79,98.624,21.651,98.629,21.564,98.589,21.531,98.595,21.483,98.647,21.421,98.681,21.337,98.696,21.231,98.726,21.145,98.773,21.077,98.724,21.02,98.508,20.951]]},{"n":"Salween","z":4,"b":[91.518,30.437,96.596,31.943],"l":[[91.518,31.943,91.546,31.815,91.6,31.747,91.691,31.696,91.755,31.607,91.794,31.478,91.863,31.418,91.963,31.425,92.032,31.396,92.071,31.33,92.134,31.299,92.221,31.304,92.282,31.347,92.319,31.429,92.428,31.486,92.703,31.533,92.869,31.563,92.958,31.56,93.027,31.534,93.156,31.529,93.347,31.544,93.524,31.536,93.687,31.504,93.819,31.501,93.922,31.525,94.106,31.523,94.371,31.495,94.528,31.468,94.575,31.443,94.576,31.387,94.529,31.301,94.531,31.251,94.583,31.237,94.629,31.196,94.669,31.129,94.71,31.091,94.752,31.08,94.821,31.102,94.919,31.158,95.039,31.186,95.181,31.188,95.301,31.171,95.399,31.136,95.486,31.074,95.563,30.985,95.667,30.957,95.799,30.99,95.962,30.978,96.157,30.921,96.283,30.873,96.34,30.835,96.411,30.721,96.497,30.529,96.554,30.437,96.596,30.448]]},{"n":"Ob","z":4,"b":[70.354,52.332,85.056,61.304],"l":[[85.056,52.434,84.589,52.422,84.347,52.393,84.175,52.342,84.003,52.332,83.831,52.362,83.728,52.414,83.694,52.489,83.683,52.592,83.699,52.789,83.658,52.961,83.658,53.04,83.685,53.083,83.817,53.109,83.86,53.175,83.849,53.199,83.838,53.353,83.712,53.419,83.493,53.429,83.332,53.457,83.228,53.503,83.128,53.505,83.03,53.464,82.897,53.437,82.728,53.425,82.581,53.446,82.454,53.499,82.388,53.545,82.38,53.585,82.33,53.631,82.237,53.684,82.114,53.718,81.96,53.734,81.867,53.758,81.836,53.789,81.727,53.8,81.542,53.788,81.42,53.808,81.334,53.886],[70.354,61.301,70.784,61.304,70.979,61.298,71.081,61.283,71.154,61.251,71.199,61.202,71.257,61.195,71.329,61.23,71.447,61.253,71.695,61.269,72.044,61.255,72.223,61.264,72.347,61.293,72.466,61.288,72.581,61.251,72.678,61.243,72.757,61.263,72.907,61.263,73.237,61.235,73.566,61.196,73.71,61.196,73.779,61.221,73.877,61.214,74.002,61.177,74.118,61.175,74.223,61.207,74.314,61.201,74.385,61.168,74.429,61.168,74.449,61.189,74.515,61.213,74.645,61.215,74.75,61.202,74.868,61.16,75.333,61.124,75.535,61.082,75.627,61.024,75.714,61.01,75.794,61.04,75.887,61.05,75.994,61.04,76.135,61,76.308,60.93,76.435,60.905,76.517,60.925,76.626,60.896,76.764,60.82,76.96,60.745,77.215,60.674,77.399,60.649,77.512,60.669,77.602,60.662,77.672,60.627,77.714,60.586,77.729,60.538,77.807,60.483,77.949,60.422,78.111,60.396,78.293,60.405,78.341,60.371,78.345,60.332,78.389,60.304,78.437,60.292,78.481,60.244,78.563,60.196,78.683,60.15,78.785,60.133,78.836,60.141,78.871,60.116,78.876,60.084,78.909,60.065,78.962,60.055,78.979,60.019,78.959,59.981,78.952,59.933,78.979,59.884,79.039,59.841,79.13,59.805,79.166,59.766,79.147,59.724,79.148,59.689,79.183,59.647,79.283,59.558,79.388,59.517,79.531,59.495,79.623,59.46,79.665,59.412,79.779,59.402,79.967,59.429,80.093,59.424,80.158,59.385,80.172,59.349,80.133,59.317,80.152,59.289,80.228,59.265,80.295,59.273,80.354,59.312,80.411,59.33,80.467,59.328,80.502,59.299,80.516,59.243,80.616,59.18,80.802,59.108,80.973,59.091,81.128,59.13,81.256,59.118,81.358,59.056,81.411,59.004,81.415,58.963,81.444,58.945,81.478,58.937,81.496,58.914,81.505,58.886,81.535,58.848,81.568,58.831,81.573,58.797,81.542,58.745,81.554,58.718,81.648,58.701,81.704,58.707,81.735,58.723,81.775,58.736,81.858,58.702,81.902,58.66,81.908,58.61,81.965,58.585,82.073,58.584,82.172,58.549,82.262,58.48,82.339,58.439,82.403,58.427,82.434,58.404,82.435,58.369,82.463,58.359,82.519,58.376,82.575,58.363,82.631,58.322,82.734,58.305,82.884,58.313,82.928,58.283,82.934,58.24,82.961,58.193,83.031,58.15,83.097,58.128,83.16,58.127,83.224,58.057,83.289,57.92,83.351,57.826,83.41,57.776,83.55,57.742,83.7,57.73,83.777,57.699,83.816,57.641,83.833,57.58,83.907,57.506,83.943,57.441,83.94,57.384,83.958,57.339,83.997,57.306,84.105,57.277,84.28,57.253,84.344,57.21,84.351,57.163,84.376,57.137,84.406,57.109,84.414,57.067,84.407,57.011,84.436,56.911,84.401,56.792,84.308,56.69,84.144,56.565,84.064,56.458,84.066,56.369,84.054,56.314,84.027,56.293,84.017,56.264,84.024,56.227,83.994,56.18,83.925,56.121,83.887,56.038,83.879,55.929,83.819,55.833,83.708,55.748,83.65,55.68,83.645,55.629,83.536,55.561,83.323,55.475,83.156,55.436,83.033,55.444,82.944,55.433,82.888,55.404,82.869,55.34,82.887,55.243,82.877,55.173,82.837,55.129,82.879,55.065,83.002,54.981,83.053,54.915,83.025,54.845]]},{"n":"Ob","z":4,"b":[68.857,61.124,70.354,61.301],"l":[[68.857,61.124,69.01,61.198,69.104,61.213,69.189,61.196,69.29,61.215,69.408,61.272,69.578,61.284,69.8,61.251,70.021,61.252,70.354,61.301]]},{"n":"Ob","z":2.1,"b":[65.14,61.089,69.013,66.814],"l":[[68.857,61.124,68.786,61.089,68.589,61.095,68.465,61.125,68.348,61.181,68.309,61.256,68.348,61.351,68.353,61.416,68.325,61.451,68.134,61.519,67.779,61.62,67.586,61.712,67.553,61.794,67.455,61.919,67.292,62.087,67.043,62.2,66.707,62.258,66.511,62.318,66.455,62.38,66.39,62.42,66.281,62.447,65.988,62.482,65.68,62.622,65.258,62.879,65.163,62.996,65.14,63.072,65.149,63.131,65.211,63.206,65.335,63.296,65.472,63.343,65.527,63.395,65.524,63.456,65.557,63.496,65.701,63.532,65.767,63.584,65.757,63.652,65.732,63.707,65.709,63.734,65.721,63.769,65.788,63.792,65.805,63.809,65.796,63.843,65.781,63.878,65.803,63.899,65.805,63.95,65.789,64.031,65.806,64.076,65.858,64.085,65.885,64.101,65.889,64.126,65.93,64.136,66.008,64.131,66.031,64.154,65.998,64.206,65.953,64.242,65.895,64.262,65.885,64.289,65.923,64.325,65.92,64.37,65.876,64.426,65.8,64.482,65.69,64.537,65.685,64.587,65.706,64.615,65.688,64.659,65.554,64.709,65.501,64.756,65.529,64.799,65.533,64.842,65.512,64.885,65.543,64.93,65.565,64.982,65.524,65.016,65.408,65.022,65.301,65.053,65.257,65.099,65.277,65.176,65.362,65.283,65.455,65.362,65.557,65.411,65.606,65.468,65.605,65.532,65.625,65.604,65.667,65.685,65.736,65.764,65.831,65.842,65.877,65.933,65.871,66.035,65.83,66.094,65.821,66.137,65.839,66.157,65.883,66.196,65.957,66.22,66.097,66.278,66.3,66.411,66.451,66.571,66.663,66.627,66.936,66.578,67.153,66.583,67.315,66.641],[67.315,66.641,67.573,66.629,67.927,66.547,68.197,66.538,68.385,66.6,68.497,66.656,68.533,66.707,68.655,66.752],[67.315,66.641,67.56,66.759,67.692,66.783,67.869,66.773,68.007,66.784,68.105,66.814,68.261,66.807,68.655,66.752],[68.655,66.752,68.907,66.799,69.013,66.788]]},{"n":"Oder","z":4,"b":[14.129,49.637,18.317,53.639],"l":[[17.618,49.739,17.847,49.654,17.946,49.637,18.016,49.672,18.142,49.73,18.225,49.795,18.307,49.886,18.317,49.972,18.253,50.055,18.231,50.137,18.252,50.22,18.185,50.321,18.032,50.441,17.958,50.532,17.964,50.593,17.938,50.656,17.88,50.722,17.762,50.78,17.45,50.882,17.361,50.928,17.3,50.975,17.269,51.023,17.194,51.068,17.015,51.131,16.869,51.229,16.738,51.253,16.573,51.236,16.475,51.245,16.442,51.279,16.44,51.315,16.467,51.354,16.474,51.421,16.462,51.518,16.408,51.595,16.31,51.652,16.223,51.677,16.146,51.669,16.02,51.691,15.847,51.743,15.769,51.811,15.787,51.894,15.758,51.961,15.682,52.011,15.424,52.046,14.761,52.077,14.705,52.11,14.692,52.15,14.705,52.207,14.68,52.25,14.616,52.278,14.574,52.314,14.555,52.36,14.57,52.431,14.619,52.528,14.514,52.646,14.254,52.783,14.129,52.878,14.139,52.933,14.194,52.982,14.293,53.027,14.369,53.106,14.446,53.275,14.581,53.445,14.618,53.526,14.61,53.584,14.584,53.639]]},{"n":"Ohio","z":3,"b":[-89.165,36.992,-79.991,40.686],"l":[[-85.186,38.694,-85.211,38.704,-85.247,38.727,-85.307,38.737,-85.405,38.73,-85.445,38.683,-85.425,38.597,-85.459,38.53,-85.546,38.484,-85.609,38.423,-85.646,38.347,-85.706,38.301,-85.788,38.285,-85.855,38.215,-85.906,38.091,-85.961,38.014,-86.021,37.984,-86.103,37.992,-86.208,38.037,-86.268,38.083,-86.284,38.13,-86.315,38.164,-86.343,38.179,-86.361,38.179,-86.363,38.172,-86.355,38.159,-86.365,38.145,-86.43,38.14,-86.457,38.12,-86.448,38.086,-86.463,38.062,-86.5,38.047,-86.52,38.015,-86.522,37.964,-86.549,37.917,-86.599,37.875,-86.673,37.889,-86.77,37.96,-86.869,37.976,-86.969,37.938,-87.044,37.886,-87.094,37.821,-87.141,37.805,-87.209,37.855,-87.372,37.923,-87.457,37.938,-87.518,37.922,-87.561,37.929,-87.576,37.948,-87.587,37.956,-87.599,37.946,-87.601,37.887,-87.615,37.849,-87.643,37.833,-87.665,37.846,-87.679,37.886,-87.714,37.9,-87.77,37.885,-87.825,37.891,-87.874,37.916,-87.892,37.913,-87.906,37.902,-87.902,37.855,-87.912,37.818,-87.938,37.794,-87.975,37.788,-88.022,37.8,-88.042,37.789,-88.073,37.771,-88.129,37.7,-88.14,37.621,-88.108,37.534,-88.191,37.465,-88.389,37.411,-88.495,37.364,-88.507,37.324,-88.493,37.262,-88.453,37.178,-88.446,37.12,-88.472,37.088,-88.614,37.11,-88.873,37.186,-89.046,37.183,-89.135,37.102,-89.165,37.042,-89.154,36.992],[-79.991,40.448,-80.149,40.522,-80.223,40.584,-80.265,40.661,-80.344,40.686,-80.519,40.647,-80.638,40.603,-80.659,40.563,-80.623,40.512,-80.604,40.445,-80.602,40.364,-80.668,40.185,-80.804,39.909,-80.872,39.736,-80.874,39.664,-80.963,39.571,-81.139,39.456,-81.263,39.387,-81.333,39.363,-81.392,39.365,-81.44,39.393,-81.493,39.374,-81.552,39.308,-81.606,39.274,-81.657,39.272,-81.701,39.229,-81.737,39.147,-81.771,39.099,-81.803,39.086,-81.805,39.044,-81.778,38.973,-81.78,38.947,-81.795,38.946,-81.809,38.95,-81.844,38.934,-81.882,38.894,-81.897,38.894,-81.907,38.9,-81.919,38.952,-81.95,38.989,-82.001,39.011,-82.072,38.963,-82.163,38.845,-82.204,38.768,-82.188,38.715,-82.18,38.638,-82.202,38.606,-82.252,38.594,-82.291,38.552,-82.318,38.48,-82.387,38.434,-82.498,38.415,-82.621,38.451,-82.758,38.541,-82.845,38.628,-82.88,38.71,-83,38.714,-83.203,38.639,-83.357,38.627,-83.461,38.68,-83.538,38.7,-83.616,38.683,-83.69,38.65,-83.752,38.669,-83.828,38.731,-83.948,38.774,-84.113,38.798,-84.227,38.865,-84.29,38.975,-84.358,39.05,-84.43,39.091,-84.509,39.103,-84.595,39.086,-84.661,39.094,-84.707,39.129,-84.766,39.125,-84.837,39.083,-84.861,39.04,-84.835,38.996,-84.837,38.961,-84.864,38.933,-84.854,38.913,-84.807,38.9,-84.79,38.868,-84.801,38.819,-84.901,38.77,-85.094,38.714,-85.136,38.697,-85.17,38.69,-85.186,38.694]]},{"n":"Okavango","z":5,"b":[21.503,-21.265,24.739,-18.06],"l":[[24.739,-21.265,24.69,-21.185,24.597,-21.117,24.444,-21.036,24.367,-20.944,24.367,-20.84,24.4,-20.705,24.467,-20.54,24.495,-20.44,24.484,-20.406,24.453,-20.391,24.403,-20.393,24.323,-20.345,24.212,-20.248,24.098,-20.228,23.981,-20.287,23.881,-20.282,23.799,-20.212,23.698,-20.16,23.577,-20.126,23.46,-20.119,23.404,-20.118,23.37,-20.103,23.368,-20.08,23.392,-20.048,23.545,-19.886,23.595,-19.735,23.579,-19.683],[23.354,-19.556,23.435,-19.622,23.579,-19.683],[23.528,-19.522,23.579,-19.683],[23.237,-19.338,23.354,-19.556],[23.237,-19.338,23.292,-19.387,23.37,-19.405,23.438,-19.441,23.526,-19.521],[22.527,-18.942,22.542,-19.072,22.635,-19.2,22.771,-19.368,22.892,-19.473,22.998,-19.517,23.124,-19.545,23.354,-19.556],[22.527,-18.942,22.58,-19.04,22.663,-19.082,22.774,-19.068,22.894,-19.078,23.023,-19.111,23.129,-19.19,23.212,-19.316,23.237,-19.338],[21.503,-18.06,21.637,-18.097,21.738,-18.185,21.85,-18.339,21.937,-18.417,22,-18.422,22.088,-18.518,22.2,-18.705,22.317,-18.822,22.439,-18.87,22.527,-18.942]]},{"n":"Olenekskaya Protoka","z":2,"b":[122.752,72.294,126.69,72.907],"l":[[126.69,72.294,126.475,72.353,126.336,72.35,126.203,72.308,126.019,72.304,125.786,72.34,125.424,72.436,124.932,72.591,124.614,72.665,124.469,72.657,124.221,72.691,123.871,72.767,123.607,72.793,123.43,72.768,123.189,72.773,122.752,72.907]]},{"n":"Olenëk","z":5,"b":[104.957,67.124,123.743,72.979],"l":[[104.957,67.568,105.339,67.514,105.468,67.57,105.562,67.584,105.665,67.573,105.693,67.536,105.707,67.512,105.751,67.486,105.814,67.477,105.895,67.483,105.962,67.505,106.013,67.542,106.026,67.57,106.113,67.594,106.252,67.557,106.283,67.526,106.248,67.486,106.217,67.44,106.285,67.375,106.402,67.351,106.567,67.368,106.872,67.34,107.315,67.269,107.618,67.244,107.78,67.266,108.068,67.239,108.482,67.164,108.851,67.124,109.292,67.13,109.337,67.141,109.371,67.167,109.448,67.175,109.576,67.159,109.773,67.166,110.038,67.197,110.128,67.235,110.145,67.261,110.168,67.278,110.231,67.28,110.27,67.286,110.286,67.296,110.277,67.31,110.261,67.331,110.257,67.354,110.319,67.391,110.447,67.426,110.639,67.46,110.808,67.508,110.954,67.569,110.956,67.602,110.941,67.618,110.924,67.637,110.935,67.664,110.931,67.697,110.893,67.747,110.908,67.786,111.054,67.848,111.195,67.894,111.331,67.922,111.447,67.924,111.543,67.898,111.586,67.866,111.599,67.846,111.641,67.825,111.776,67.858,111.964,67.863,112.204,67.839,112.336,67.838,112.359,67.861,112.35,67.884,112.31,67.908,112.285,67.947,112.274,68.002,112.191,68.048,112.037,68.084,111.98,68.121,112.021,68.161,112.108,68.199,112.205,68.211,112.237,68.229,112.234,68.26,112.192,68.281,112.192,68.305,112.239,68.344,112.254,68.377,112.239,68.407,112.194,68.441,112.186,68.495,112.241,68.547,112.323,68.553,112.434,68.514,112.532,68.5,112.617,68.511,112.843,68.487,113.209,68.427,113.542,68.432,113.839,68.502,114.048,68.518,114.166,68.481,114.339,68.469,114.565,68.483,114.693,68.481,114.721,68.464,114.694,68.442,114.612,68.415,114.642,68.39,114.783,68.368,115.064,68.405,115.483,68.501,115.705,68.543,115.727,68.529,115.73,68.51,115.713,68.485,115.639,68.45,115.51,68.404,115.582,68.347,115.855,68.279,115.947,68.238,115.858,68.223,115.811,68.194,115.804,68.151,115.872,68.124,116.016,68.114,116.07,68.075,116.102,68.007,116.177,67.895,116.203,67.813,116.179,67.761,116.258,67.74,116.44,67.751,116.527,67.781,116.555,67.827,116.638,67.862,116.887,67.877,116.994,67.897,116.958,67.924,116.965,67.958,117.012,68.001,117.122,68.031,117.292,68.049,117.462,68.094,117.633,68.169,117.594,68.234,117.345,68.291,117.224,68.338,117.231,68.374,117.203,68.404,117.141,68.426,117.098,68.459,117.075,68.504,117.139,68.549,117.292,68.596,117.434,68.573,117.568,68.479,117.741,68.469,117.955,68.545,118.07,68.615,118.087,68.68,118.052,68.714,117.965,68.716,117.959,68.733,118.035,68.763,118.193,68.761,118.434,68.724,118.573,68.731,118.609,68.783,118.689,68.823,118.811,68.851,118.822,68.886,118.72,68.929,118.637,68.947,118.573,68.941,118.525,68.919,118.493,68.882,118.293,68.902,117.961,68.962,117.912,69.015,117.939,69.052,118.228,69.092,118.357,69.12,118.371,69.159,118.33,69.248,118.34,69.284,118.358,69.323,118.389,69.346,118.375,69.382,118.296,69.415,118.262,69.456,118.272,69.503,118.227,69.551,118.177,69.575,118.187,69.629,118.299,69.647,118.741,69.696,119.026,69.752,119.156,69.814,119.248,69.876,119.303,69.938,119.321,70.007,119.303,70.082,119.195,70.164,118.996,70.254,118.909,70.314,118.934,70.342,118.986,70.343,119.067,70.314,119.208,70.312,119.409,70.337,119.558,70.387,119.653,70.462,119.751,70.511,119.852,70.534,120.15,70.533,120.896,70.497,121.727,70.512,122.124,70.534,122.364,70.57,122.511,70.606,122.549,70.64,122.524,70.672,122.382,70.695,122.307,70.734,122.297,70.79,122.322,70.841,122.381,70.887,122.386,70.923,122.338,70.949,122.319,70.971,122.33,70.986,122.373,71.005,122.45,71.026,122.48,71.061,122.495,71.09,122.501,71.141,122.545,71.193,122.625,71.225,122.648,71.26,122.615,71.298,122.739,71.379,123.02,71.503,123.274,71.587,123.499,71.632,123.612,71.668,123.613,71.696,123.657,71.721,123.743,71.744,123.738,71.79,123.643,71.859,123.631,71.935,123.702,72.02,123.424,72.146,122.799,72.316,122.232,72.502,121.722,72.705,121.355,72.799,121.133,72.782,120.967,72.799,120.857,72.85,120.745,72.878,120.573,72.889,120.199,72.894,119.885,72.933,119.75,72.979]]},{"n":"Omo","z":5,"b":[35.912,4.539,37.6,9.151],"l":[[37.032,9.151,37.031,9.052,37.067,8.997,37.14,8.953,37.184,8.907,37.204,8.838,37.157,8.741,37.149,8.692,37.166,8.658,37.41,8.364,37.518,8.261,37.574,8.228,37.6,8.112,37.59,7.877,37.527,7.649,37.411,7.426,37.377,7.286,37.427,7.23,37.475,7.202,37.521,7.202,37.524,7.184,37.483,7.147,37.452,7.072,37.43,6.958,37.396,6.893,37.35,6.877,37.3,6.821,37.247,6.724,37.165,6.659,37.054,6.626,36.983,6.633,36.954,6.681,36.878,6.706,36.754,6.71,36.579,6.635,36.355,6.481,36.209,6.415,36.143,6.435,36.085,6.419,36.036,6.367,36.004,6.306,35.983,6.198,35.955,6.172,35.96,6.119,35.989,6.028,35.978,5.84,35.927,5.553,35.912,5.391,35.933,5.353,35.984,5.344,36.066,5.365,36.113,5.41,36.128,5.481,36.155,5.501,36.196,5.47,36.202,5.418,36.175,5.344,36.171,5.285,36.192,5.24,36.167,5.208,36.095,5.187,36.066,5.165,36.08,5.141,36.072,5.114,36.043,5.084,36.048,5.043,36.087,4.99,36.074,4.914,36.009,4.812,35.994,4.706,36.045,4.539]]},{"n":"Orange","z":4,"b":[16.448,-30.679,29.057,-28.031],"l":[[25.506,-30.612,25.344,-30.56,25.212,-30.473,24.978,-30.263],[24.731,-29.979,24.489,-29.818,24.392,-29.736,24.358,-29.678,24.296,-29.643,24.207,-29.632,24.103,-29.586,23.984,-29.506,23.923,-29.441,23.92,-29.393,23.882,-29.347,23.808,-29.304,23.736,-29.23,23.628,-29.074,23.577,-29.166,23.539,-29.205,23.498,-29.222,23.452,-29.219,23.412,-29.203,23.376,-29.211,23.348,-29.245,23.28,-29.288,23.163,-29.332,23.1,-29.37,23.088,-29.39,23.056,-29.412,23.014,-29.431,23.002,-29.486,22.981,-29.512,22.924,-29.522,22.892,-29.529,22.87,-29.537,22.852,-29.558,22.863,-29.624,22.85,-29.661,22.812,-29.669,22.741,-29.613,22.638,-29.493,22.507,-29.403,22.35,-29.343,22.266,-29.268,22.257,-29.177,22.209,-29.061,22.122,-28.918,22.056,-28.854,22.012,-28.867,21.963,-28.858,21.908,-28.825,21.835,-28.721,21.745,-28.544,21.662,-28.44,21.586,-28.408,21.486,-28.4,21.362,-28.417,21.236,-28.494,21.107,-28.629,20.97,-28.714,20.827,-28.748,20.704,-28.76,20.602,-28.747,20.485,-28.692,20.352,-28.593,20.21,-28.514,20.057,-28.453,19.878,-28.449,19.671,-28.504,19.54,-28.575,19.483,-28.662,19.407,-28.714,19.313,-28.733,19.271,-28.778,19.282,-28.848,19.246,-28.902,19.162,-28.939,19.026,-28.928,18.839,-28.869,18.6,-28.855,18.311,-28.886,18.103,-28.872,17.976,-28.811,17.842,-28.777,17.699,-28.768,17.617,-28.743,17.448,-28.698,17.416,-28.621,17.396,-28.563,17.348,-28.501,17.343,-28.452,17.38,-28.414,17.386,-28.353,17.359,-28.269,17.312,-28.229,17.246,-28.231,17.205,-28.199,17.188,-28.132,17.149,-28.082,17.056,-28.031,16.933,-28.07,16.875,-28.128,16.841,-28.219,16.81,-28.265,16.795,-28.341,16.787,-28.395,16.756,-28.452,16.723,-28.476,16.689,-28.465,16.626,-28.488,16.487,-28.573,16.448,-28.618],[29.057,-29.082,29.017,-29.218,28.958,-29.277,28.868,-29.304,28.809,-29.359,28.766,-29.481,28.721,-29.55,28.725,-29.586,28.762,-29.612,28.769,-29.643,28.744,-29.678,28.746,-29.732,28.773,-29.802,28.753,-29.851,28.686,-29.877,28.666,-29.91,28.693,-29.948,28.686,-29.984,28.645,-30.018,28.567,-30.038,28.454,-30.045,28.374,-30.032,28.325,-30.001,28.209,-30.006,28.025,-30.049,27.925,-30.101,27.906,-30.164,27.876,-30.213,27.835,-30.25,27.792,-30.264,27.744,-30.257,27.713,-30.282,27.698,-30.339,27.657,-30.374,27.591,-30.386,27.51,-30.371,27.431,-30.339,27.409,-30.325,27.39,-30.326,27.388,-30.335,27.389,-30.353,27.377,-30.372,27.345,-30.386,27.338,-30.402,27.337,-30.416,27.334,-30.427,27.31,-30.431,27.295,-30.445,27.283,-30.476,27.226,-30.499,27.082,-30.522,27.041,-30.541,27.028,-30.573,26.988,-30.609,26.897,-30.654,26.796,-30.678,26.684,-30.679,26.581,-30.65,26.488,-30.59,26.401,-30.565,26.32,-30.574,26.247,-30.556,26.18,-30.509,26.103,-30.5,25.972,-30.545]]},{"n":"Ord","z":5,"b":[127.578,-17.706,128.891,-15.298],"l":[[127.578,-17.623,127.929,-17.463,128.157,-17.523,128.258,-17.58,128.309,-17.654,128.356,-17.696,128.398,-17.706,128.502,-17.653,128.668,-17.537,128.789,-17.432,128.867,-17.335,128.891,-17.259,128.862,-17.204,128.856,-17.151,128.873,-17.102,128.837,-17.048,128.746,-16.99,128.697,-16.931,128.689,-16.871,128.698,-16.828,128.734,-16.788],[128.737,-16.134,128.703,-16.084,128.701,-16.032,128.744,-15.929,128.747,-15.802,128.712,-15.651,128.612,-15.563,128.447,-15.538,128.346,-15.495,128.289,-15.402,128.28,-15.35,128.255,-15.298]]},{"n":"Orinoco","z":3,"b":[-67.855,2.153,-61.099,9.788],"l":[[-63.436,2.374,-63.641,2.312,-63.732,2.258,-63.779,2.191,-63.849,2.156,-63.991,2.153,-64.279,2.265,-64.408,2.292,-64.475,2.271,-64.68,2.326,-65.022,2.456,-65.196,2.587,-65.202,2.718,-65.246,2.818,-65.328,2.886,-65.393,2.966,-65.442,3.059,-65.49,3.117,-65.537,3.142,-65.613,3.152,-65.772,3.142,-65.942,3.162,-66.011,3.13,-66.036,3.052,-66.129,3.024,-66.29,3.045,-66.413,3.091,-66.5,3.161,-66.556,3.237,-66.581,3.318,-66.645,3.42,-66.801,3.606,-66.946,3.706,-67.002,3.792,-67.015,3.896,-67.116,3.948,-67.302,3.946,-67.48,3.981,-67.732,4.087,-67.783,4.198,-67.799,4.284,-67.795,4.381,-67.814,4.455,-67.855,4.507,-67.855,4.665,-67.814,4.931,-67.804,5.133,-67.825,5.27,-67.788,5.375,-67.695,5.448,-67.642,5.559,-67.631,5.709,-67.575,5.833,-67.474,5.93,-67.439,6.026,-67.472,6.12,-67.423,6.202,-67.4,6.24,-67.225,6.386,-67.119,6.557,-67.062,6.851,-67.071,7.033,-66.959,7.151,-66.73,7.266,-66.563,7.396,-66.461,7.542,-66.297,7.629,-66.072,7.657,-65.912,7.714,-65.769,7.841,-65.641,7.892,-65.518,7.901,-65.358,7.883,-65.221,7.857,-65.105,7.823,-65.004,7.771,-64.928,7.71,-64.904,7.681,-64.887,7.668,-64.873,7.666,-64.854,7.668,-64.838,7.67,-64.787,7.685,-64.742,7.709,-64.59,7.76,-64.51,7.824,-64.538,7.892,-64.545,7.943,-64.531,7.978,-64.481,7.999,-64.395,8.006,-64.331,8.028,-64.288,8.065,-64.222,8.057,-64.155,8.005,-64.114,7.981,-64.089,7.975,-64.05,7.971,-63.993,7.974,-63.924,8.013,-63.849,8.092,-63.727,8.136,-63.558,8.145,-63.399,8.191,-63.249,8.273,-63.101,8.304,-62.953,8.284,-62.791,8.334,-62.615,8.454,-62.429,8.553,-62.235,8.633,-62.152,8.642],[-62.152,8.642,-62.092,8.648,-62.002,8.6,-61.88,8.582,-61.65,8.603,-61.619,8.597,-61.395,8.774,-61.099,8.941],[-62.063,9.036,-62.077,8.975,-62.065,8.928,-62.025,8.896,-62.039,8.828,-62.137,8.673,-62.152,8.642],[-61.736,9.631,-61.743,9.603,-61.849,9.289,-61.924,9.152,-62.044,9.055,-62.063,9.036],[-62.3,9.788,-62.309,9.748,-62.326,9.721,-62.414,9.562,-62.388,9.421,-62.277,9.245,-62.18,9.134,-62.098,9.089,-62.063,9.036]]},{"n":"Ottawa","z":5,"b":[-79.166,45.42,-74.315,46.827],"l":[[-76.903,45.892,-76.824,45.88,-76.754,45.798,-76.667,45.643,-76.553,45.542,-76.413,45.496,-76.27,45.484,-76.123,45.505,-76.001,45.484,-75.904,45.42,-75.626,45.451,-75.167,45.575,-74.864,45.642,-74.717,45.651,-74.577,45.632,-74.378,45.561,-74.315,45.531],[-77.371,46.063,-77.272,45.954,-77.155,45.912,-76.902,45.892],[-77.683,46.187,-77.373,46.064],[-77.683,46.187,-77.682,46.187],[-77.684,46.187,-77.683,46.187],[-79.166,46.827,-79.015,46.607,-78.903,46.486,-78.781,46.393,-78.46,46.307,-77.683,46.187]]},{"n":"Ouham","z":5,"b":[15.323,6.264,18.285,9.316],"l":[[15.323,6.462,15.398,6.342,15.496,6.325,15.714,6.393,15.79,6.461,15.846,6.486,15.908,6.49,15.96,6.437,16.001,6.328,16.075,6.27,16.182,6.264,16.276,6.302,16.358,6.383,16.404,6.448,16.414,6.497,16.474,6.56,16.584,6.637,16.663,6.671,16.709,6.662,16.774,6.609,16.857,6.514,16.933,6.472,17.001,6.486,17.082,6.475,17.174,6.441,17.281,6.448,17.401,6.497,17.509,6.52,17.605,6.515,17.709,6.558,17.821,6.65,17.9,6.772,17.948,6.926,18.002,7.023,18.062,7.062,18.127,7.139,18.197,7.254,18.249,7.313,18.285,7.314,18.275,7.362,18.22,7.457,18.145,7.49,18.051,7.46,17.984,7.469,17.945,7.517,17.877,7.548,17.782,7.561,17.749,7.611,17.778,7.698,17.768,7.826,17.718,7.995,17.699,8.127,17.709,8.224,17.77,8.369,17.882,8.56,17.945,8.744,17.96,8.92,17.992,9.011,18.04,9.018,18.098,9.056,18.164,9.126,18.207,9.2,18.235,9.316]]},{"n":"Pamir","z":4.7,"b":[72.657,37.029,73.605,37.472],"l":[[73.605,37.446,73.481,37.472,73.383,37.462,73.211,37.409,72.896,37.268,72.757,37.173,72.657,37.029]]},{"n":"Panama Canal","z":5,"b":[-79.927,8.924,-79.552,9.361],"l":[[-79.691,9.104,-79.566,8.968,-79.552,8.924],[-79.923,9.267,-79.927,9.304,-79.915,9.361]]},{"n":"Panj","z":4.7,"b":[68.212,36.684,72.657,38.456],"l":[[72.657,37.029,72.359,36.983,72.154,36.901,71.942,36.766,71.802,36.694,71.734,36.684,71.666,36.697,71.598,36.733,71.531,36.845,71.433,37.128,71.505,37.603,71.546,37.796,71.58,37.864,71.582,37.91,71.552,37.933,71.488,37.932,71.39,37.906,71.32,37.902,71.279,37.918,71.283,38.008,71.333,38.17,71.256,38.307,71.052,38.418,70.879,38.456,70.736,38.423,70.616,38.334,70.519,38.192,70.418,38.075,70.313,37.985,70.239,37.941,70.215,37.924,70.199,37.886,70.255,37.765,70.251,37.664,70.189,37.582,70.12,37.543,70.045,37.547,69.985,37.566,69.941,37.6,69.821,37.61,69.626,37.594,69.492,37.553,69.42,37.487,69.399,37.399,69.43,37.291,69.415,37.208,69.354,37.15,69.304,37.117,69.265,37.108,69.18,37.158,69.05,37.267,68.96,37.325,68.912,37.334,68.885,37.328,68.855,37.317,68.838,37.303,68.824,37.271,68.782,37.258,68.723,37.268,68.669,37.258,68.637,37.224,68.547,37.183,68.387,37.137,68.3,37.088,68.285,37.036,68.261,37.013,68.212,37.022]]},{"n":"Paraiba","z":5,"b":[-46.021,-23.396,-41.022,-21.582],"l":[[-45.049,-23.049,-45.505,-23.309,-45.606,-23.351,-45.709,-23.361,-45.85,-23.354,-45.934,-23.365,-45.961,-23.393,-45.99,-23.396,-46.021,-23.375,-46.004,-23.309,-45.941,-23.197,-45.861,-23.127,-45.765,-23.096,-45.666,-23.038,-45.565,-22.952,-45.446,-22.893,-45.31,-22.86,-45.167,-22.773,-45.016,-22.631,-44.863,-22.547,-44.706,-22.521,-44.651,-22.524,-44.593,-22.538,-44.522,-22.517,-44.362,-22.435,-44.256,-22.426,-44.205,-22.49,-44.151,-22.508,-44.094,-22.48,-44.04,-22.477,-43.989,-22.498,-43.852,-22.438,-43.629,-22.299,-43.425,-22.196,-43.144,-22.097],[-43.138,-22.097,-42.28,-21.714,-41.991,-21.6,-41.855,-21.582,-41.775,-21.619,-41.684,-21.631,-41.581,-21.618,-41.467,-21.646,-41.341,-21.714,-41.213,-21.714,-41.022,-21.611]]},{"n":"Paranaíba","z":4,"b":[-49.789,-19.171,-46.314,-18.083],"l":[[-46.314,-19.171,-46.561,-18.852,-46.517,-18.793,-46.514,-18.765,-46.538,-18.748,-46.54,-18.71,-46.519,-18.652,-46.535,-18.564,-46.586,-18.448,-46.697,-18.355,-46.867,-18.284,-46.992,-18.258,-47.071,-18.277,-47.152,-18.228,-47.235,-18.112,-47.319,-18.083,-47.447,-18.171],[-48.018,-18.434,-48.227,-18.34],[-49.108,-18.388,-49.179,-18.405,-49.247,-18.468,-49.337,-18.585,-49.417,-18.603,-49.488,-18.522,-49.553,-18.51,-49.612,-18.567,-49.678,-18.6,-49.789,-18.615]]},{"n":"Paraná","z":2.1,"b":[-60.757,-34.038,-50.49,-14.319],"l":[[-59.182,-33.806,-59.082,-33.822,-58.934,-33.884,-58.737,-33.994,-58.591,-34.038,-58.429,-33.991],[-60.39,-33.173,-60.282,-33.259,-59.854,-33.534,-59.548,-33.684,-59.362,-33.709,-59.287,-33.742,-59.241,-33.779,-59.182,-33.806],[-60.39,-33.173,-60.111,-33.249,-59.947,-33.268,-59.823,-33.25,-59.664,-33.315,-59.472,-33.463,-59.328,-33.604,-59.182,-33.806],[-60.391,-33.173,-60.39,-33.173],[-58.605,-27.314,-58.816,-27.438,-58.878,-27.512,-58.85,-27.611,-58.824,-27.718,-58.831,-27.826,-58.862,-27.971,-58.93,-28.076,-59.035,-28.142,-59.087,-28.288,-59.087,-28.514,-59.122,-28.735,-59.192,-28.948,-59.194,-28.981,-59.205,-29.032,-59.227,-29.058,-59.256,-29.083,-59.304,-29.104,-59.44,-29.17,-59.529,-29.25,-59.565,-29.335,-59.605,-29.495,-59.65,-29.73,-59.654,-29.898,-59.615,-29.999,-59.614,-30.11,-59.651,-30.233,-59.656,-30.336,-59.629,-30.421,-59.625,-30.526,-59.637,-30.604,-59.638,-30.669,-59.654,-30.717,-59.675,-30.76,-59.738,-30.83,-60.064,-31.27,-60.302,-31.554,-60.448,-31.666,-60.581,-31.699,-60.666,-31.768,-60.702,-31.871,-60.706,-31.959,-60.676,-32.033,-60.678,-32.136,-60.726,-32.335,-60.757,-32.517,-60.744,-32.645,-60.698,-32.78,-60.631,-32.916,-60.541,-33.053,-60.39,-33.173],[-54.611,-25.433,-54.632,-26.006,-54.678,-26.309,-54.755,-26.533,-54.826,-26.652,-54.889,-26.667,-54.934,-26.703,-54.962,-26.759,-55.014,-26.807,-55.089,-26.845,-55.13,-26.886,-55.136,-26.931,-55.208,-26.96,-55.346,-26.973,-55.427,-27.009,-55.451,-27.068,-55.497,-27.115,-55.565,-27.15,-55.597,-27.208,-55.594,-27.288,-55.633,-27.357,-55.715,-27.415,-55.79,-27.416,-55.859,-27.362,-55.951,-27.326,-56.067,-27.308,-56.164,-27.321,-56.242,-27.367,-56.311,-27.439,-56.37,-27.537,-56.437,-27.554,-56.511,-27.488,-56.603,-27.468,-56.716,-27.494,-56.805,-27.485,-56.872,-27.441,-56.974,-27.436,-57.112,-27.47,-57.391,-27.43,-57.812,-27.317,-58.168,-27.273,-58.605,-27.314],[-53.786,-23.333,-53.908,-23.407,-53.996,-23.57,-54.05,-23.823,-54.125,-23.978,-54.267,-24.066,-54.067,-24.004,-53.99,-23.838,-53.916,-23.577,-53.858,-23.423,-53.786,-23.333],[-52.961,-22.455,-53.116,-22.64,-53.272,-22.752,-53.481,-22.852,-53.62,-23.001,-53.69,-23.198,-53.786,-23.333],[-51.397,-20.441,-51.534,-20.558,-51.593,-20.666,-51.619,-20.807,-51.692,-20.944,-51.811,-21.077,-51.866,-21.183,-51.857,-21.26,-51.873,-21.33,-51.936,-21.423],[-50.49,-19.02,-50.532,-19.087,-50.578,-19.112,-50.642,-19.118,-50.713,-19.161,-50.792,-19.238,-50.844,-19.311,-50.869,-19.377,-50.871,-19.424,-50.864,-19.438,-50.881,-19.459,-50.924,-19.464,-50.964,-19.511,-50.997,-19.668],[-55.075,-14.319,-55.432,-14.357,-55.8,-14.549,-56.006,-14.635,-56.173,-14.68,-56.316,-14.778,-56.436,-14.928,-56.497,-15.037,-56.497,-15.104,-56.444,-15.171,-56.337,-15.239,-56.275,-15.328,-56.258,-15.437,-56.205,-15.531,-56.117,-15.609,-56.089,-15.68,-56.121,-15.746,-56.133,-15.801,-56.122,-15.845,-56.07,-15.896,-55.977,-15.952,-55.917,-16.009,-55.89,-16.069,-55.905,-16.13,-55.963,-16.195,-55.977,-16.254,-55.947,-16.309,-55.965,-16.362,-56.031,-16.413,-56.134,-16.453,-56.273,-16.483,-56.382,-16.565,-56.461,-16.699,-56.529,-16.785,-56.586,-16.822,-56.62,-16.872,-56.633,-16.936,-56.627,-16.993,-56.59,-17.067,-56.79,-17.386,-56.888,-17.518,-56.98,-17.595,-57.08,-17.734,-57.198,-17.812,-57.37,-17.876,-57.432,-18.045,-57.386,-18.321,-57.407,-18.497,-57.496,-18.573,-57.549,-18.664,-57.567,-18.769,-57.607,-18.838,-57.668,-18.873,-57.688,-18.919,-57.668,-18.978,-57.588,-19.019,-57.448,-19.042,-57.365,-19.07,-57.342,-19.103,-57.304,-19.233,-57.3,-19.297,-57.309,-19.351,-57.345,-19.414,-57.408,-19.485,-57.427,-19.529,-57.403,-19.546,-57.413,-19.587,-57.458,-19.652,-57.522,-19.697,-57.606,-19.721,-57.649,-19.752,-57.649,-19.789,-57.711,-19.861,-57.835,-19.97,-57.962,-20.059,-58.094,-20.13,-58.149,-20.177,-58.129,-20.202,-58.129,-20.226,-58.148,-20.25,-58.115,-20.304,-58.032,-20.39,-57.988,-20.498,-57.984,-20.628,-57.966,-20.687,-57.934,-20.675,-57.904,-20.685,-57.875,-20.715,-57.881,-20.746,-57.921,-20.778,-57.921,-20.803,-57.881,-20.822,-57.874,-20.85,-57.902,-20.887,-57.891,-20.918,-57.843,-20.942,-57.822,-21.009,-57.831,-21.119,-57.852,-21.199,-57.887,-21.25,-57.893,-21.289,-57.868,-21.317,-57.88,-21.375,-57.927,-21.464,-57.937,-21.553,-57.91,-21.643,-57.918,-21.789,-57.981,-22.09,-57.97,-22.215,-57.931,-22.29,-57.862,-22.356,-57.814,-22.455,-57.788,-22.589,-57.817,-22.671,-57.899,-22.701,-57.903,-22.751,-57.829,-22.822,-57.792,-22.898,-57.79,-22.98,-57.713,-23.084,-57.561,-23.21,-57.476,-23.348,-57.459,-23.499,-57.407,-23.613,-57.32,-23.692,-57.293,-23.779,-57.327,-23.875,-57.299,-23.972,-57.208,-24.069,-57.172,-24.132,-57.19,-24.162,-57.17,-24.223,-57.112,-24.318,-57.08,-24.403,-57.073,-24.478,-57.084,-24.547,-57.13,-24.64,-57.124,-24.724,-57.154,-24.787,-57.217,-24.857,-57.237,-24.915,-57.212,-24.961,-57.221,-24.984,-57.264,-24.983,-57.32,-25.015,-57.388,-25.08,-57.448,-25.112,-57.499,-25.11,-57.561,-25.154,-57.635,-25.245,-57.644,-25.328,-57.587,-25.405,-57.563,-25.474,-57.572,-25.534,-57.626,-25.599,-57.725,-25.667,-57.755,-25.697,-57.757,-25.726,-57.782,-25.784,-57.865,-25.907,-57.886,-25.964,-57.891,-26.007,-57.943,-26.053,-58.082,-26.139,-58.111,-26.18,-58.118,-26.225,-58.136,-26.251,-58.155,-26.263,-58.181,-26.307,-58.203,-26.381,-58.205,-26.477,-58.188,-26.593,-58.191,-26.63,-58.222,-26.65,-58.239,-26.677,-58.246,-26.731,-58.272,-26.771,-58.318,-26.796,-58.335,-26.825,-58.323,-26.858,-58.356,-26.89,-58.436,-26.922,-58.485,-26.968,-58.503,-27.03,-58.548,-27.084,-58.619,-27.132,-58.642,-27.196,-58.605,-27.314]]},{"n":"Peace","z":2.1,"b":[-122.222,55.934,-111.411,59.152],"l":[[-122.222,56.021,-122.17,55.972,-122.118,55.962,-122.048,55.974,-121.951,56.017,-121.826,56.091,-121.631,56.165,-121.365,56.24,-121.039,56.238,-120.654,56.16,-120.389,56.121,-120.244,56.122,-119.735,56.18,-119.492,56.231,-119.298,56.294,-119.145,56.316,-119.032,56.296,-118.978,56.267,-118.984,56.23,-118.963,56.188,-118.917,56.14,-118.907,56.1,-118.917,56.07,-118.869,56.017,-118.716,55.956,-118.449,55.934,-118.068,55.949,-117.739,56.023,-117.462,56.153,-117.273,56.288,-117.171,56.427,-117.128,56.536,-117.143,56.616,-117.177,56.669,-117.228,56.705,-117.23,56.733,-117.253,56.755,-117.291,56.787,-117.319,56.843,-117.295,56.925,-117.219,57.033,-117.209,57.083,-117.213,57.117,-117.195,57.137,-117.17,57.148,-117.121,57.18,-117.097,57.229,-117.106,57.268,-117.133,57.309,-117.127,57.357,-117.083,57.417,-117.091,57.477,-117.105,57.507,-117.095,57.53,-117.058,57.558,-117.054,57.6,-117.076,57.622,-117.094,57.664,-117.088,57.692,-117.093,57.719,-117.106,57.752,-117.098,57.774,-117.107,57.795,-117.1,57.823,-117.078,57.845,-117.083,57.886,-117.133,57.921,-117.148,57.971,-117.116,58.024,-117.053,58.067,-116.956,58.099,-116.832,58.112,-116.68,58.107,-116.592,58.115,-116.569,58.137,-116.573,58.162,-116.604,58.189,-116.612,58.216,-116.598,58.242,-116.616,58.261,-116.642,58.278,-116.644,58.292,-116.6,58.312,-116.544,58.31,-116.495,58.306,-116.496,58.278,-116.482,58.263,-116.455,58.259,-116.384,58.293,-116.271,58.363,-116.144,58.416,-116.003,58.45,-115.767,58.439,-115.435,58.382,-115.169,58.359,-114.968,58.37,-114.694,58.444,-114.347,58.58,-114.086,58.664,-113.823,58.71],[-113.714,58.669,-113.271,58.779,-113.127,58.832,-113.134,58.865,-113.101,58.889,-113.026,58.903,-112.944,58.958,-112.853,59.054,-112.769,59.118,-112.692,59.152,-112.528,59.143,-112.277,59.092,-112.102,59.078,-112.001,59.102,-111.944,59.099,-111.931,59.07,-111.962,59.035,-112.037,58.994,-112.059,58.963,-112.03,58.941,-111.966,58.94,-111.868,58.961,-111.76,58.953,-111.642,58.918,-111.539,58.927,-111.411,59.005]]},{"n":"Pechora","z":4,"b":[52.126,61.699,59.287,68.27],"l":[[59.287,62.058,59.175,62.022,59.076,62.022,58.89,62.057,58.777,61.996,58.639,62.008,58.438,62.073,58.234,62.067,58.027,61.988,57.909,61.913,57.879,61.84,57.828,61.806,57.756,61.812,57.617,61.783,57.412,61.72,57.229,61.699,57.009,61.728,56.962,61.763,56.882,61.797,56.857,61.803,56.842,61.816,56.851,61.846,56.836,61.903,56.796,61.984,56.729,62.049,56.666,62.082,56.641,62.1,56.631,62.13,56.63,62.16,56.644,62.178,56.7,62.234,56.726,62.344,56.706,62.489,56.625,62.595,56.483,62.661,56.36,62.701,56.257,62.715,56.213,62.753,56.227,62.817,56.201,62.884,56.103,62.99,55.936,63.16,55.941,63.191,55.956,63.209,55.993,63.22,56.056,63.196,56.174,63.222,56.292,63.29,56.37,63.305,56.419,63.313,56.441,63.35,56.445,63.383,56.47,63.416,56.519,63.427,56.541,63.442,56.53,63.464,56.478,63.502,56.467,63.539,56.504,63.566,56.556,63.589,56.639,63.661,56.712,63.763,56.944,63.839,57.336,63.89,57.47,63.912,57.507,63.942,57.511,63.986,57.525,64.017,57.584,64.052,57.603,64.09,57.577,64.123,57.581,64.16,57.559,64.197,57.565,64.231,57.579,64.258,57.624,64.29,57.638,64.325,57.673,64.34,57.716,64.373,57.73,64.397,57.773,64.411,57.783,64.443,57.787,64.474,57.793,64.496,57.805,64.535,57.807,64.579,57.828,64.651,57.879,64.74,57.844,64.806,57.722,64.847,57.608,64.908,57.503,64.988,57.4,65.029,57.301,65.034,57.256,65.06,57.265,65.108,57.203,65.137,57.068,65.147,57.016,65.241,57.047,65.421,56.967,65.568,56.777,65.683,56.744,65.789,56.777,65.847,56.834,65.912,56.837,65.971,56.806,66.003,56.736,66.033,56.621,66.122,56.491,66.257,56.283,66.325,55.998,66.326,55.763,66.302,55.412,66.2,55.264,66.142,55.171,66.065,55.133,65.968,55.093,65.91,55.029,65.878,54.836,65.752,54.711,65.702,54.588,65.686,54.456,65.625,54.315,65.516,54.103,65.458,53.822,65.449,53.588,65.417,53.403,65.361,53.194,65.333,52.959,65.332,52.67,65.357,52.327,65.408,52.153,65.466,52.15,65.532,52.179,65.629,52.239,65.759,52.349,65.869,52.507,65.96,52.567,66.02,52.528,66.049,52.507,66.111,52.506,66.206,52.487,66.287,52.451,66.354,52.454,66.404,52.496,66.438,52.504,66.474,52.478,66.514,52.471,66.556,52.484,66.599,52.451,66.651,52.373,66.71,52.354,66.831,52.392,67.014,52.381,67.115,52.287,67.145,52.169,67.26,52.132,67.318,52.136,67.358,52.17,67.396,52.234,67.43,52.231,67.456,52.16,67.473,52.126,67.507,52.128,67.557,52.247,67.614,52.483,67.678,52.627,67.699],[52.627,67.699,52.743,67.755,53.107,67.864],[52.627,67.699,52.68,67.678,52.813,67.673,53.025,67.684,53.184,67.734,53.291,67.824,53.514,67.911,53.852,67.995,54.067,68.081,54.207,68.209,54.233,68.266],[53.107,67.864,53.215,67.933,53.085,67.965,53.027,68.011,53.043,68.069,53.093,68.118,53.179,68.157,53.227,68.196,53.242,68.242,53.262,68.27],[53.914,68.231,53.749,68.175,53.672,68.125,53.621,68.057,53.495,67.987,53.192,67.88,53.107,67.864]]},{"n":"Pend Oreille","z":5,"b":[-117.682,48.178,-116.569,49.027],"l":[[-117.682,49.015,-117.414,49.027,-117.337,49.007,-117.324,48.961,-117.339,48.905,-117.383,48.84,-117.391,48.753,-117.363,48.645,-117.325,48.569,-117.278,48.527,-117.249,48.447,-117.239,48.33,-117.122,48.241,-116.901,48.182,-116.761,48.178,-116.705,48.23,-116.65,48.254,-116.569,48.25]]},{"n":"Pilcomayo","z":5,"b":[-66.444,-25.328,-57.644,-19.12],"l":[[-66.444,-19.37,-66.387,-19.308,-66.373,-19.268,-66.384,-19.23,-66.359,-19.196,-66.297,-19.165,-66.207,-19.206,-66.089,-19.318,-65.98,-19.366,-65.827,-19.342,-65.778,-19.25,-65.702,-19.186,-65.582,-19.12,-65.465,-19.143,-65.35,-19.254,-65.252,-19.328,-65.171,-19.362,-65.078,-19.377,-64.973,-19.373,-64.84,-19.545,-64.679,-19.894,-64.59,-20.157,-64.572,-20.336,-64.499,-20.441,-64.371,-20.472,-64.274,-20.586,-64.209,-20.785,-64.132,-20.899,-64.044,-20.929,-63.975,-21.002,-63.922,-21.117,-63.843,-21.176,-63.736,-21.178,-63.552,-21.242,-63.293,-21.367,-63.122,-21.466,-63.041,-21.539,-62.977,-21.633,-62.908,-21.806,-62.815,-22.05,-62.745,-22.16,-62.665,-22.218,-62.626,-22.262,-62.626,-22.29,-62.542,-22.35,-62.372,-22.439,-62.214,-22.612,-62.067,-22.869,-61.928,-23.059,-61.799,-23.182,-61.68,-23.269,-61.571,-23.319,-61.513,-23.36,-61.506,-23.392,-61.404,-23.457,-61.208,-23.557,-61.085,-23.656,-61.033,-23.756,-60.84,-23.858,-60.505,-23.964,-60.262,-24.014,-60.11,-24.009,-59.892,-24.094,-59.609,-24.267,-59.435,-24.387,-59.373,-24.454,-59.187,-24.562,-58.724,-24.787,-58.52,-24.843,-58.423,-24.894,-58.365,-24.959,-58.309,-24.979,-58.253,-24.954,-58.136,-24.977,-57.96,-25.049,-57.822,-25.136,-57.644,-25.328]]},{"n":"Pit","z":5,"b":[-122.095,40.784,-120.429,41.709],"l":[[-120.429,41.709,-120.457,41.558,-120.514,41.498,-120.679,41.451,-120.971,41.397,-121.115,41.289,-121.112,41.128,-121.13,41.023,-121.171,40.974,-121.223,40.977,-121.284,41.032,-121.347,41.042,-121.411,41.007,-121.496,41,-121.6,41.02,-121.689,41.015,-121.763,40.984,-121.829,40.986,-121.888,41.02,-121.932,41.02,-121.963,40.986,-121.97,40.947,-121.952,40.901,-121.981,40.854,-122.095,40.784]]},{"n":"Po","z":5,"b":[7.11,44.665,12.523,45.22],"l":[[7.11,44.706,7.326,44.665,7.424,44.687,7.502,44.795,7.619,44.849,7.665,44.925,7.677,45.041,7.731,45.129,7.824,45.19,7.976,45.22,8.185,45.219,8.36,45.199,8.5,45.161,8.594,45.113,8.644,45.054,8.808,45.054,9.224,45.141,9.462,45.1,9.505,45.093,9.55,45.116,9.607,45.129,9.652,45.114,9.699,45.086,9.733,45.083,9.754,45.104,9.782,45.103,9.816,45.08,9.846,45.085,9.87,45.118,9.909,45.133,9.96,45.13,10.008,45.106,10.05,45.06,10.154,45.026,10.317,45.003,10.43,44.972,10.491,44.935,10.557,44.928,10.628,44.953,10.666,44.986,10.671,45.028,10.74,45.057,10.871,45.072,11.02,45.075,11.188,45.065,11.312,45.029,11.392,44.968,11.479,44.927,11.574,44.906,11.684,44.918,11.869,44.985,12.085,45.02,12.199,45.013,12.283,44.976,12.377,44.961,12.523,44.968]]},{"n":"Porcupine","z":5,"b":[-145.453,65.65,-137.491,67.654],"l":[[-139.811,65.65,-139.728,65.748,-139.697,65.811,-139.687,65.903,-139.698,65.933,-139.671,65.959,-139.609,65.989,-139.488,66.002,-139.306,65.998,-139.166,66.016,-139.066,66.056,-138.923,66.177,-138.736,66.38,-138.554,66.511,-138.379,66.57,-138.249,66.642,-138.163,66.727,-138.068,66.768,-137.963,66.767,-137.886,66.787,-137.839,66.83,-137.821,66.862,-137.826,66.886,-137.801,66.902,-137.682,66.928,-137.603,66.986,-137.558,67.001,-137.521,66.992,-137.499,67.003,-137.491,67.032,-137.526,67.054,-137.604,67.07,-137.612,67.102,-137.551,67.152,-137.561,67.178,-137.639,67.18,-137.664,67.2,-137.701,67.237,-137.788,67.297,-137.816,67.339,-137.783,67.361,-137.773,67.395,-137.786,67.442,-137.857,67.469,-137.988,67.475,-138.103,67.505,-138.202,67.558,-138.335,67.581,-138.599,67.58,-138.629,67.594,-138.637,67.612,-138.622,67.634,-138.633,67.648,-138.67,67.654,-139.287,67.548,-139.668,67.575,-139.826,67.574,-139.887,67.554,-139.912,67.535,-139.9,67.517,-139.995,67.539,-140.109,67.538,-140.27,67.514,-140.352,67.492,-140.356,67.471,-140.511,67.454,-140.817,67.443,-141.103,67.409,-141.501,67.328,-141.617,67.29,-141.665,67.252,-141.683,67.201,-141.709,67.171,-141.742,67.162,-142.055,67.164,-142.111,67.168,-142.132,67.178,-142.137,67.197,-142.148,67.211,-142.181,67.22,-142.32,67.168,-142.565,67.054,-142.861,66.998,-143.209,67,-143.457,66.988,-143.606,66.961,-143.668,66.936,-143.644,66.912,-143.643,66.894,-143.664,66.884,-143.781,66.902,-143.851,66.885,-143.921,66.835,-144.041,66.797,-144.212,66.773,-144.32,66.773,-144.363,66.796,-144.39,66.796,-144.403,66.771,-144.465,66.759,-144.575,66.758,-144.686,66.729,-144.797,66.672,-144.911,66.649,-145.027,66.66,-145.197,66.645,-145.453,66.587]]},{"n":"Potomac","z":5,"b":[-78.325,38.889,-77.03,39.686],"l":[[-78.325,39.639,-78.181,39.686,-78.098,39.678,-78.027,39.631,-77.956,39.609,-77.883,39.611,-77.855,39.6,-77.872,39.576,-77.853,39.548,-77.798,39.517,-77.78,39.488,-77.797,39.461,-77.792,39.442,-77.762,39.429,-77.745,39.4,-77.739,39.354,-77.669,39.31,-77.534,39.266,-77.479,39.221,-77.503,39.175,-77.506,39.143,-77.481,39.113,-77.301,39.053,-77.19,38.969,-77.102,38.936,-77.053,38.915,-77.03,38.889]]},{"n":"Purari","z":5,"b":[143.492,-7.841,145.302,-5.938],"l":[[143.492,-5.938,143.639,-6.294,143.638,-6.387,143.682,-6.457,143.769,-6.536,143.878,-6.589,144.01,-6.617,144.124,-6.597,144.221,-6.528,144.301,-6.491,144.363,-6.485,144.494,-6.57,144.757,-6.801,144.781,-6.83,144.797,-6.859,144.802,-6.897,144.831,-6.914,144.884,-6.905,144.926,-6.91,144.959,-6.929,144.97,-6.952,144.966,-6.974,144.984,-6.992,145.042,-6.988,145.083,-7,145.106,-7.026,145.127,-7.03,145.153,-7.019,145.191,-7.027,145.265,-7.078,145.302,-7.166,145.301,-7.291,145.257,-7.361,145.17,-7.374,145.118,-7.411,145.101,-7.473,145.127,-7.538,145.195,-7.608,145.215,-7.687,145.186,-7.787,145.194,-7.841]]},{"n":"Purús","z":5,"b":[-72.426,-10.907,-61.406,-3.676],"l":[[-72.426,-10.845,-72.349,-10.907,-72.221,-10.901,-71.912,-10.824,-71.801,-10.718,-71.689,-10.662,-71.539,-10.621,-71.38,-10.543,-71.214,-10.429,-71.136,-10.356,-71.147,-10.323,-71.126,-10.292,-71.074,-10.263,-71.037,-10.18,-71.013,-10.043,-70.929,-9.912,-70.786,-9.788,-70.708,-9.696,-70.696,-9.635,-70.659,-9.595,-70.596,-9.575,-70.55,-9.514,-70.52,-9.412,-70.455,-9.33,-70.353,-9.266,-70.298,-9.211,-70.291,-9.166,-70.225,-9.123,-70.1,-9.083,-69.994,-9.077,-69.907,-9.104,-69.847,-9.1,-69.815,-9.065,-69.764,-9.047,-69.658,-9.046,-69.616,-9.059,-69.593,-9.045,-69.57,-9.013,-69.541,-9,-69.492,-9.004,-69.419,-8.964,-69.323,-8.874,-69.188,-8.798,-69.014,-8.738,-68.923,-8.731,-68.889,-8.748,-68.867,-8.77,-68.823,-8.782,-68.772,-8.771,-68.711,-8.795,-68.634,-8.883,-68.593,-8.953,-68.588,-9.006,-68.563,-9.028,-68.518,-9.018,-68.488,-9.029,-68.472,-9.06,-68.407,-9.065,-68.292,-9.046,-68.218,-9.046,-68.183,-9.066,-68.105,-9.044,-67.924,-8.947,-67.518,-8.777,-67.378,-8.682,-67.368,-8.603,-67.379,-8.574,-67.398,-8.561,-67.405,-8.535,-67.393,-8.487,-67.4,-8.459,-67.412,-8.443,-67.41,-8.425,-67.355,-8.38,-67.347,-8.343,-67.364,-8.316,-67.358,-8.295,-67.342,-8.254,-67.264,-8.149,-67.233,-8.069,-67.249,-8.014,-67.201,-7.939,-67.09,-7.843,-67.048,-7.798,-67.045,-7.775,-67.025,-7.76,-66.999,-7.76,-66.978,-7.742,-66.968,-7.701,-66.923,-7.672,-66.843,-7.653,-66.789,-7.658,-66.761,-7.685,-66.719,-7.673,-66.662,-7.621,-66.605,-7.606,-66.547,-7.626,-66.511,-7.617,-66.499,-7.577,-66.453,-7.541,-66.374,-7.509,-66.335,-7.523,-66.329,-7.562,-66.298,-7.578,-66.24,-7.567,-66.195,-7.587,-66.165,-7.638,-66.122,-7.658,-66.069,-7.657,-66.044,-7.684,-66.019,-7.696,-65.957,-7.675,-65.911,-7.68,-65.886,-7.702,-65.856,-7.701,-65.835,-7.683,-65.789,-7.684,-65.741,-7.698,-65.673,-7.691,-65.648,-7.662,-65.62,-7.646,-65.578,-7.647,-65.543,-7.632,-65.522,-7.599,-65.487,-7.592,-65.45,-7.596,-65.429,-7.582,-65.431,-7.561,-65.41,-7.549,-65.385,-7.552,-65.373,-7.535,-65.38,-7.514,-65.367,-7.493,-65.302,-7.45,-65.23,-7.411,-65.193,-7.408,-65.167,-7.426,-65.152,-7.4,-65.147,-7.327,-65.121,-7.311,-65.098,-7.307,-65.035,-7.325,-64.99,-7.312,-64.975,-7.268,-64.957,-7.257,-64.933,-7.25,-64.871,-7.276,-64.79,-7.252,-64.69,-7.178,-64.645,-7.131,-64.656,-7.11,-64.646,-7.096,-64.616,-7.087,-64.612,-7.075,-64.609,-7.063,-64.584,-7.049,-64.585,-7.02,-64.636,-6.929,-64.635,-6.872,-64.58,-6.848,-64.565,-6.825,-64.589,-6.803,-64.595,-6.77,-64.586,-6.725,-64.552,-6.702,-64.495,-6.7,-64.462,-6.679,-64.454,-6.638,-64.415,-6.6,-64.345,-6.567,-64.298,-6.474,-64.261,-6.246,-64.304,-6.025,-64.357,-5.934,-64.433,-5.901,-64.454,-5.867,-64.418,-5.833,-64.397,-5.787,-64.392,-5.728,-64.368,-5.695,-64.326,-5.689,-64.311,-5.665,-64.323,-5.622,-64.292,-5.598,-64.218,-5.592,-64.181,-5.603,-64.182,-5.631,-64.163,-5.647,-64.124,-5.652,-64.095,-5.638,-64.075,-5.605,-64.019,-5.589,-63.927,-5.59,-63.857,-5.624,-63.811,-5.69,-63.744,-5.716,-63.657,-5.702,-63.63,-5.721,-63.635,-5.757,-63.624,-5.788,-63.59,-5.811,-63.542,-5.8,-63.48,-5.755,-63.427,-5.742,-63.383,-5.762,-63.304,-5.733,-63.191,-5.655,-63.158,-5.662,-63.144,-5.683,-63.122,-5.716,-63.072,-5.728,-63.052,-5.707,-63.063,-5.622,-63.047,-5.573,-63.002,-5.56,-62.995,-5.531,-62.98,-5.473,-62.957,-5.46,-62.936,-5.437,-62.901,-5.437,-62.856,-5.43,-62.83,-5.392,-62.829,-5.276,-62.839,-5.208,-62.861,-5.187,-62.843,-5.153,-62.785,-5.106,-62.763,-5.061,-62.778,-5.02,-62.748,-4.968,-62.7,-4.944,-62.658,-4.951,-62.617,-4.937,-62.601,-4.913,-62.589,-4.875,-62.551,-4.857,-62.51,-4.862,-62.473,-4.837,-62.462,-4.808,-62.476,-4.776,-62.453,-4.744,-62.393,-4.713,-62.313,-4.704,-62.214,-4.716,-62.115,-4.664,-62.079,-4.631,-62.033,-4.59,-62.016,-4.538,-62.04,-4.487,-62.023,-4.444,-61.992,-4.429,-61.921,-4.415,-61.875,-4.38,-61.854,-4.324,-61.79,-4.28,-61.681,-4.248,-61.564,-4.146,-61.441,-3.973,-61.406,-3.832,-61.468,-3.676]]},{"n":"Pánuco","z":5,"b":[-98.59,21.965,-97.782,22.279],"l":[[-98.59,21.981,-98.471,21.965,-98.425,21.98,-98.414,22.019,-98.376,22.041,-98.311,22.046,-98.258,22.066,-98.217,22.102,-98.178,22.099,-98.139,22.056,-98.097,22.047,-98.051,22.07,-98.047,22.107,-98.051,22.123,-98.015,22.178,-97.966,22.209,-97.871,22.219,-97.782,22.279]]},{"n":"Quan","z":4,"b":[109.529,23.405,110.089,23.796],"l":[[109.529,23.796,109.543,23.729,109.575,23.7,109.629,23.688,109.651,23.665,109.641,23.629,109.663,23.575,109.717,23.502,109.796,23.454,109.9,23.43,109.963,23.431,109.986,23.455,110.021,23.452,110.089,23.405]]},{"n":"R. des Outaouais","z":5,"b":[-79.496,47.488,-76.819,47.938],"l":[[-76.819,47.517,-76.891,47.539,-76.92,47.532,-76.935,47.488],[-77.322,47.573,-77.361,47.578,-77.431,47.557,-77.457,47.556],[-78.813,47.656,-78.987,47.554,-79.068,47.537,-79.113,47.572,-79.225,47.586,-79.496,47.577],[-77.544,47.671,-77.527,47.744],[-77.567,47.83,-77.621,47.818,-77.729,47.733,-77.834,47.728,-77.933,47.79,-78.004,47.784,-78.047,47.709,-78.127,47.688,-78.246,47.722,-78.342,47.764,-78.414,47.815,-78.449,47.861,-78.445,47.903,-78.485,47.929,-78.57,47.938,-78.641,47.925,-78.698,47.888,-78.726,47.823,-78.716,47.709,-78.724,47.684]]},{"n":"Rainy","z":5,"b":[-94.713,48.525,-90.503,49.329],"l":[[-93.377,48.617,-93.464,48.561,-93.564,48.537,-93.708,48.525,-93.804,48.549,-93.852,48.607,-94.055,48.659,-94.414,48.704,-94.621,48.743,-94.675,48.774,-94.705,48.809,-94.713,48.863],[-91.629,48.865,-91.659,48.792,-91.757,48.762,-91.935,48.751,-92.06,48.76,-92.132,48.787,-92.321,48.77,-92.779,48.678],[-90.503,49.329,-90.613,49.219,-90.59,49.169,-90.601,49.13,-90.637,49.086,-90.756,49.054,-90.957,49.032,-91.123,48.973,-91.319,48.828]]},{"n":"Rhein","z":4,"b":[8.751,46.676,9.626,47.671],"l":[[8.751,46.676,9.046,46.758,9.416,46.835],[9.409,46.835,9.517,46.907,9.534,46.963,9.496,47.028,9.479,47.098,9.484,47.173,9.528,47.271,9.609,47.392,9.626,47.467,9.554,47.511],[9.128,47.671,9.183,47.671]]},{"n":"Rhein","z":4,"b":[7.529,47.564,8.881,48.965],"l":[[8.881,47.656,8.7,47.686,8.62,47.662,8.58,47.595,8.482,47.576,8.328,47.607,8.198,47.607,8.094,47.576,7.927,47.564,7.698,47.57,7.565,47.607,7.529,47.674,7.539,47.774,7.593,47.906,7.608,48.003,7.584,48.064,7.617,48.157,7.706,48.28,7.765,48.41,7.795,48.547,7.838,48.636,7.923,48.699,8.23,48.965]]},{"n":"Rhine","z":4,"b":[4.985,48.965,8.498,51.888],"l":[[8.23,48.965,8.417,49.214,8.486,49.33,8.498,49.397,8.489,49.436,8.458,49.447,8.422,49.505,8.381,49.611,8.389,49.69,8.431,49.728,8.437,49.767,8.401,49.815,8.357,49.911,8.305,49.981,8.243,50.023,8.129,50.026,7.962,49.991,7.803,50.042,7.654,50.181,7.607,50.222,7.611,50.266,7.629,50.324,7.608,50.367,7.557,50.4,7.444,50.433,7.344,50.486,7.257,50.559,7.204,50.622,7.185,50.673,7.125,50.734,7.025,50.805,6.995,50.853,6.995,50.904,6.973,50.938,6.968,50.998,6.938,51.04,6.881,51.064,6.851,51.097,6.847,51.139,6.816,51.169,6.758,51.187,6.76,51.227,6.741,51.268,6.69,51.303,6.698,51.364,6.707,51.422,6.698,51.458,6.693,51.487,6.687,51.524,6.624,51.559,6.593,51.592,6.596,51.637,6.357,51.775,6.212,51.837,6.08,51.869,6.039,51.872,5.975,51.877,5.898,51.86,5.78,51.863,5.622,51.888,5.492,51.878,5.389,51.833,5.25,51.813,4.985,51.824]]},{"n":"Rhône","z":5,"b":[4.371,43.401,8.25,46.482],"l":[[4.617,43.71,4.514,43.702,4.467,43.683,4.44,43.65,4.436,43.627,4.444,43.613,4.434,43.591,4.379,43.56,4.371,43.528,4.41,43.447],[6.133,46.205,5.902,46.123,5.814,46.013,5.793,45.849,5.747,45.737,5.676,45.676,5.573,45.704,5.437,45.821,5.35,45.847,5.285,45.812,5.191,45.799,5.058,45.817,4.952,45.808,4.871,45.771,4.824,45.713,4.809,45.633,4.819,45.582,4.853,45.561,4.841,45.513,4.784,45.439,4.79,45.289,4.858,45.063,4.864,44.911,4.806,44.834,4.781,44.761,4.786,44.693,4.772,44.637,4.721,44.573,4.674,44.35,4.675,44.228,4.711,44.133,4.759,44.071,4.82,44.04,4.843,44.012,4.821,43.973,4.69,43.906,4.638,43.828,4.619,43.715,4.631,43.639,4.695,43.579,4.724,43.554,4.735,43.508,4.743,43.449,4.787,43.401],[8.25,46.482,8.068,46.374,7.955,46.335,7.799,46.325,7.653,46.327,7.471,46.277,7.205,46.178,7.089,46.152,7.02,46.197,6.866,46.403]]},{"n":"Rio Grande","z":4,"b":[-107.427,25.871,-97.146,37.809],"l":[[-99.172,26.564,-99.108,26.447,-99.015,26.399,-98.873,26.381,-98.765,26.34,-98.691,26.276,-98.598,26.238,-98.486,26.225,-98.378,26.182,-98.275,26.111,-98.083,26.064,-97.801,26.042,-97.587,25.984,-97.44,25.891,-97.376,25.872,-97.358,25.871,-97.35,25.885,-97.339,25.911,-97.282,25.942,-97.146,25.961],[-101.039,29.46,-101.016,29.401,-100.924,29.315,-100.755,29.183,-100.659,29.069,-100.636,28.973,-100.55,28.821,-100.399,28.614,-100.332,28.503,-100.348,28.486,-100.336,28.428,-100.296,28.328,-100.221,28.243,-100.112,28.173,-100.001,28.048,-99.89,27.867,-99.754,27.73,-99.595,27.636,-99.505,27.548,-99.484,27.467,-99.486,27.398,-99.51,27.34,-99.5,27.286,-99.455,27.234,-99.44,27.17,-99.458,27.082,-99.457,27.057],[-106.566,31.82,-106.436,31.764,-106.347,31.679,-106.256,31.545,-106.148,31.451,-106.024,31.398,-105.813,31.241,-105.514,30.981,-105.276,30.807,-105.098,30.721,-104.979,30.646,-104.918,30.583,-104.836,30.448,-104.681,30.134,-104.681,29.991,-104.622,29.854,-104.504,29.678,-104.401,29.574,-104.312,29.542,-104.216,29.48,-104.111,29.386,-103.99,29.323,-103.853,29.291,-103.664,29.207,-103.423,29.071,-103.258,29.001,-103.168,28.998,-103.09,29.042,-103.023,29.132,-102.957,29.19,-102.892,29.216,-102.866,29.258,-102.878,29.315,-102.834,29.444,-102.734,29.644,-102.615,29.752,-102.476,29.769,-102.386,29.807,-102.343,29.865,-102.269,29.871,-102.163,29.825,-101.991,29.796,-101.752,29.782,-101.612,29.787,-101.569,29.809,-101.546,29.808,-101.545,29.784,-101.509,29.773,-101.44,29.777,-101.38,29.743,-101.304,29.634],[-107.293,32.91,-107.24,32.743,-107.172,32.674,-107.07,32.649,-106.975,32.584,-106.889,32.479,-106.846,32.399,-106.847,32.346,-106.777,32.195,-106.566,31.82],[-107.181,33.173,-107.28,33.116],[-107.427,37.776,-107.067,37.744,-106.924,37.809,-106.814,37.792,-106.688,37.716,-106.542,37.678,-106.374,37.678,-106.196,37.647,-106.007,37.584,-105.879,37.525,-105.811,37.469,-105.763,37.406,-105.735,37.335,-105.732,37.24,-105.752,37.119,-105.743,36.98,-105.703,36.823,-105.695,36.651,-105.719,36.464,-105.761,36.339,-105.85,36.244,-106.006,36.133,-106.073,36.048,-106.101,35.951,-106.166,35.855,-106.268,35.76,-106.343,35.648,-106.392,35.519,-106.487,35.376,-106.628,35.218,-106.692,35.089,-106.682,34.989,-106.716,34.781,-106.796,34.465,-106.85,34.295,-106.878,34.269,-106.886,34.147,-106.874,33.928,-106.928,33.749,-107.048,33.61,-107.123,33.494,-107.17,33.356]]},{"n":"Rosetta Branch","z":2,"b":[30.395,30.121,31.238,31.458],"l":[[31.238,30.121,31.212,30.139,31.163,30.151,31.053,30.192,30.972,30.245,30.935,30.306,30.896,30.335,30.855,30.354,30.858,30.384,30.848,30.471,30.786,30.666,30.77,30.78,30.785,30.811,30.772,30.844,30.762,30.866,30.763,30.944,30.747,31.011,30.715,31.065,30.656,31.118,30.569,31.17,30.523,31.229,30.52,31.295,30.488,31.357,30.395,31.458]]},{"n":"Ruki","z":5,"b":[18.184,-0.219,18.951,0.082],"l":[[18.951,-0.219,18.804,-0.121,18.662,-0.069,18.475,-0.031,18.36,0.012,18.318,0.062,18.272,0.082,18.184,0.063]]},{"n":"Rupert","z":5,"b":[-78.731,51.07,-73.641,51.546],"l":[[-74.818,51.412,-74.59,51.48,-74.491,51.49,-74.445,51.467,-74.389,51.462,-74.337,51.46,-74.297,51.435,-74.256,51.405,-74.169,51.376,-74.073,51.371,-73.966,51.387,-73.891,51.349,-73.847,51.257,-73.837,51.175,-73.861,51.103,-73.815,51.07,-73.641,51.078],[-75.204,51.446,-75.229,51.485,-75.226,51.508,-75.242,51.527,-75.327,51.54,-75.481,51.546,-75.822,51.5,-76.553,51.364],[-78.731,51.497,-78.69,51.487,-78.457,51.397,-78.157,51.362,-77.713,51.354,-77.304,51.378,-76.931,51.435,-76.821,51.415]]},{"n":"Ruvuma","z":5,"b":[35.199,-11.716,40.464,-10.464],"l":[[35.599,-10.807,35.358,-10.712,35.26,-10.696,35.224,-10.726,35.204,-10.776,35.199,-10.879,35.368,-11.406,35.451,-11.59,35.504,-11.605,35.564,-11.602,35.631,-11.582,35.705,-11.532,35.785,-11.453,35.911,-11.455,36.082,-11.537,36.175,-11.609,36.191,-11.671,36.306,-11.706,36.519,-11.716,36.674,-11.684,36.771,-11.61,36.873,-11.571,36.979,-11.567,37.059,-11.592,37.114,-11.647,37.218,-11.687,37.373,-11.71,37.542,-11.675,37.725,-11.581,37.829,-11.482,37.855,-11.379,37.885,-11.317,37.92,-11.295,38.017,-11.282,38.177,-11.279,38.315,-11.311,38.492,-11.413,38.795,-11.229,38.988,-11.167,39.171,-11.167,39.322,-11.123,39.439,-11.035,39.563,-10.979,39.694,-10.955,39.817,-10.912,39.989,-10.821,40.464,-10.464]]},{"n":"Río Grande de Santiago","z":5,"b":[-105.457,20.31,-102.78,21.822],"l":[[-102.78,20.31,-102.79,20.368,-102.866,20.398,-103.07,20.443,-103.121,20.478,-103.153,20.525,-103.147,20.587,-103.19,20.663,-103.293,20.735,-103.341,20.795,-103.332,20.841,-103.351,20.91,-103.398,21.002,-103.44,21.051,-103.478,21.059,-103.521,21.04,-103.592,20.972],[-103.709,20.938,-103.842,20.989,-103.885,21.027,-103.882,21.068,-103.93,21.118,-104.079,21.205],[-104.079,21.206,-104.18,21.2,-104.253,21.22,-104.33,21.263,-104.364,21.298,-104.442,21.433,-104.526,21.502,-104.634,21.708,-104.664,21.765,-104.724,21.817,-104.779,21.822,-104.892,21.767,-105,21.729,-105.021,21.739,-105.033,21.777],[-105.104,21.822,-105.176,21.804,-105.207,21.795,-105.246,21.756,-105.339,21.705,-105.411,21.691,-105.457,21.672]]},{"n":"S. Branch Potomac","z":5,"b":[-79.571,38.415,-78.325,39.639],"l":[[-79.571,38.415,-79.385,38.634,-79.253,38.901,-79.163,38.999,-79.071,39.017,-78.956,39.124,-78.818,39.32,-78.705,39.447,-78.575,39.533,-78.496,39.533,-78.46,39.556,-78.443,39.601,-78.407,39.628,-78.325,39.639]]},{"n":"Sacramento","z":4.7,"b":[-122.432,38.08,-121.504,40.721],"l":[[-121.581,38.659,-121.514,38.618,-121.504,38.58,-121.528,38.532,-121.531,38.475,-121.514,38.41,-121.519,38.366,-121.546,38.342,-121.544,38.311,-121.512,38.273,-121.544,38.228,-121.572,38.195,-121.689,38.148,-121.708,38.128,-121.725,38.101,-121.749,38.08],[-122.411,40.721,-122.432,40.648,-122.374,40.574,-122.244,40.474,-122.182,40.401,-122.188,40.354,-122.177,40.327,-122.15,40.319,-122.153,40.282,-122.187,40.216,-122.184,40.175,-122.144,40.159,-122.104,40.067,-122.064,39.9,-122.017,39.794,-121.964,39.75,-121.956,39.632,-121.993,39.44,-122.005,39.317,-121.992,39.262,-121.941,39.186,-121.853,39.091,-121.805,39.002,-121.797,38.919,-121.762,38.851,-121.701,38.796,-121.649,38.769,-121.606,38.772,-121.584,38.744,-121.581,38.659]]},{"n":"Saint John","z":5,"b":[-70.264,45.335,-66.053,47.354],"l":[[-70.264,46.183,-70.208,46.315,-70.161,46.374,-70.075,46.42,-70.014,46.494,-69.96,46.514,-69.893,46.504,-69.862,46.518,-69.867,46.558,-69.729,46.7,-69.449,46.943,-69.261,47.08,-69.167,47.113,-69.096,47.122,-69.05,47.108,-68.961,47.135,-68.829,47.203,-68.669,47.253,-68.48,47.286,-68.377,47.316,-68.358,47.345,-68.311,47.354,-68.235,47.346,-68.097,47.275,-67.794,47.074,-67.711,46.984,-67.698,46.868,-67.725,46.696,-67.702,46.584,-67.628,46.532,-67.599,46.492,-67.615,46.462,-67.598,46.407,-67.546,46.327,-67.528,46.227,-67.545,46.109,-67.497,46.03,-67.386,45.993,-67.292,45.977,-67.214,45.985,-67.123,45.964,-67.02,45.914,-66.935,45.912,-66.867,45.959,-66.796,45.982,-66.684,45.985,-66.524,45.899,-66.394,45.858,-66.24,45.834,-66.131,45.757,-66.065,45.628,-66.053,45.569,-66.081,45.52,-66.179,45.435,-66.218,45.379,-66.218,45.355,-66.183,45.335]]},{"n":"Salado","z":5,"b":[-65.865,-31.959,-60.608,-24.208],"l":[[-65.865,-24.208,-65.82,-24.417,-65.855,-24.53,-65.831,-24.642,-65.76,-24.791,-65.649,-24.925,-65.498,-25.046,-65.424,-25.136,-65.427,-25.194,-65.382,-25.235,-65.29,-25.258,-65.205,-25.228,-65.126,-25.144,-65.049,-25.119,-64.974,-25.151,-64.923,-25.202,-64.896,-25.271,-64.83,-25.321,-64.722,-25.351,-64.601,-25.329,-64.465,-25.257,-64.368,-25.185,-64.309,-25.114,-64.253,-25.086,-64.2,-25.102,-64.117,-25.227,-64.006,-25.463,-63.951,-25.613,-63.954,-25.677,-63.897,-25.795,-63.781,-25.966,-63.738,-26.092,-63.769,-26.172,-63.754,-26.306,-63.692,-26.493,-63.676,-26.658,-63.707,-26.801,-63.712,-26.937,-63.691,-27.066,-63.646,-27.206,-63.576,-27.356,-63.546,-27.477,-63.559,-27.617,-63.572,-27.663,-63.529,-27.772,-63.435,-27.958,-63.321,-28.103,-63.188,-28.209,-63.116,-28.296,-63.105,-28.365,-63.058,-28.417,-62.976,-28.452,-62.883,-28.521,-62.78,-28.624,-62.695,-28.779,-62.627,-28.987,-62.504,-29.155,-62.326,-29.283,-62.166,-29.358,-62.023,-29.382,-61.896,-29.363,-61.785,-29.302,-61.703,-29.299,-61.649,-29.355,-61.508,-29.422,-61.281,-29.5,-61.064,-29.597,-60.859,-29.714,-60.742,-29.799,-60.714,-29.852,-60.706,-29.96,-60.717,-30.123,-60.705,-30.223,-60.67,-30.26,-60.658,-30.345,-60.669,-30.477,-60.655,-30.576,-60.618,-30.642,-60.608,-30.695,-60.625,-30.733,-60.679,-30.764,-60.771,-30.787,-60.821,-30.831,-60.835,-30.926,-60.807,-31.07,-60.828,-31.172,-60.889,-31.28,-60.889,-31.341,-60.828,-31.357,-60.787,-31.447,-60.766,-31.61,-60.777,-31.733,-60.819,-31.816,-60.811,-31.882,-60.706,-31.959]]},{"n":"Salween","z":4,"b":[97.332,16.552,98.679,20.951],"l":[[98.508,20.951,98.502,20.828,98.538,20.712,98.613,20.561,98.66,20.414,98.679,20.272,98.552,20.149,98.141,19.99,97.943,19.961,97.865,19.929,97.841,19.884,97.81,19.858,97.771,19.85,97.696,19.718,97.531,19.331,97.457,18.852,97.44,18.831,97.406,18.828,97.353,18.843,97.332,18.778,97.341,18.634,97.381,18.494,97.451,18.36,97.524,18.296,97.599,18.303,97.632,18.29,97.622,18.258,97.652,18.174,97.72,18.037,97.74,17.935,97.699,17.834,97.755,17.571,97.749,17.444,97.7,17.364,97.659,17.251,97.626,17.107,97.609,16.947,97.606,16.772,97.622,16.652,97.668,16.552]]},{"n":"San Joaquin","z":5,"b":[-121.525,36.805,-118.737,38.056],"l":[[-119.698,37,-119.78,36.897,-119.94,36.843,-120.205,36.805,-120.393,36.866,-120.504,37.024,-120.605,37.118,-120.695,37.148,-120.856,37.285,-121.204,37.652],[-118.737,37.096,-118.808,37.203,-119.108,37.412,-119.153,37.441,-119.188,37.456,-119.223,37.438,-119.246,37.405,-119.323,37.253,-119.404,37.167,-119.488,37.149,-119.548,37.12,-119.601,37.062],[-121.204,37.651,-121.255,37.703,-121.282,37.78,-121.303,37.9,-121.347,37.971,-121.445,38.004,-121.468,38.017,-121.496,38.042,-121.525,38.056]]},{"n":"San Juan","z":4.7,"b":[-86.105,-32.389,-67.362,12.208],"l":[[-69.418,-31.283,-69.397,-31.277,-69.185,-31.269,-69.078,-31.295,-69.029,-31.353,-69.004,-31.415,-69.002,-31.481,-68.944,-31.519,-68.828,-31.529,-68.693,-31.515,-68.539,-31.477,-68.451,-31.472,-68.418,-31.512,-68.367,-31.573,-68.357,-31.654,-68.37,-31.775,-68.349,-31.883,-68.294,-31.979,-68.28,-32.044,-68.27,-32.072,-68.226,-32.084,-68.089,-32.074,-67.974,-32.112,-67.879,-32.197,-67.744,-32.25,-67.567,-32.27,-67.449,-32.308,-67.362,-32.389],[-84.769,11.118,-84.488,11.044,-84.348,10.98,-84.256,10.901,-84.205,10.841,-84.197,10.802,-84.168,10.78,-84.096,10.776,-83.919,10.735,-83.811,10.743,-83.713,10.786,-83.659,10.837,-83.641,10.915],[-86.105,12.208,-85.871,12.098]]},{"n":"Sanaga","z":5,"b":[9.766,3.624,13.126,4.993],"l":[[13.126,4.933,12.994,4.984,12.901,4.993,12.712,4.96,12.631,4.936,12.548,4.877,12.463,4.782,12.385,4.733,12.28,4.725,12.131,4.575,12.058,4.544,12.012,4.583,11.907,4.542,11.742,4.423,11.596,4.399,11.468,4.469,11.296,4.412,11.079,4.227,10.859,4.108,10.636,4.053,10.502,4.047,10.459,4.088,10.392,4.045,10.304,3.919,10.235,3.859,10.186,3.865,10.116,3.813,10.024,3.703,9.938,3.643,9.82,3.628,9.766,3.624]]},{"n":"Sangha","z":5,"b":[16.09,-1.272,17.237,1.654],"l":[[16.098,1.654,16.09,1.617,16.15,1.487,16.212,1.426,16.281,1.419,16.357,1.366,16.438,1.268,16.501,1.221,16.554,1.184,16.601,1.058,16.664,0.716,16.73,0.524,16.797,0.482,16.86,0.475,16.905,0.456,16.923,0.431,16.964,0.41,16.991,0.408,17.036,0.394,17.074,0.362,17.096,0.316,17.103,0.256,17.131,0.221,17.18,0.211,17.19,0.169,17.161,0.095,17.156,0,17.177,-0.115,17.174,-0.214,17.148,-0.298,17.146,-0.346,17.169,-0.36,17.173,-0.391,17.159,-0.44,17.17,-0.471,17.207,-0.485,17.229,-0.556,17.237,-0.682,17.228,-0.744,17.181,-0.773,17.166,-0.842,17.103,-0.92,16.99,-1.009,16.913,-1.102,16.849,-1.272]]},{"n":"Santa Cruz","z":4.7,"b":[-71.982,-50.24,-68.98,-50.003],"l":[[-71.982,-50.224,-71.362,-50.209,-71.112,-50.215,-71.024,-50.236,-70.937,-50.232,-70.852,-50.202,-70.671,-50.204,-70.395,-50.24,-70.239,-50.24,-70.204,-50.205,-70.154,-50.188,-70.06,-50.187,-69.83,-50.187,-69.722,-50.158,-69.659,-50.101,-69.516,-50.086,-69.294,-50.115,-69.141,-50.101,-69.013,-50.017,-68.98,-50.003]]},{"n":"Saskatchewan","z":4,"b":[-105.065,53.157,-99.23,54.071],"l":[[-99.33,53.159,-99.23,53.185],[-105.065,53.24,-104.901,53.164,-104.819,53.157,-104.765,53.193,-104.571,53.233,-104.238,53.276,-104.027,53.344,-103.894,53.482],[-103.421,53.653,-103.129,53.752,-103.045,53.809,-103.072,53.855,-103.05,53.922,-102.978,54.009,-102.9,54.059,-102.813,54.071,-102.686,54.053,-102.434,53.982],[-102.381,53.966,-102.188,53.904,-102.04,53.884,-101.872,53.885,-101.732,53.904,-101.62,53.942,-101.548,53.981,-101.516,54.021,-101.46,53.994,-101.381,53.903,-101.265,53.862,-101.112,53.874,-101.043,53.859,-101.056,53.817,-101.026,53.761,-100.953,53.69,-100.782,53.66,-100.514,53.67,-100.366,53.654,-100.323,53.591]]},{"n":"Sassandra","z":5,"b":[-7.255,4.953,-6.062,9.714],"l":[[-7.008,6.264,-6.953,6.185,-6.776,6.054,-6.701,5.973,-6.731,5.945,-6.733,5.902,-6.708,5.845,-6.676,5.81,-6.637,5.797,-6.592,5.736,-6.543,5.628,-6.481,5.538,-6.373,5.43,-6.274,5.32,-6.232,5.226,-6.215,5.111,-6.171,5.03,-6.062,4.953],[-7.236,8.945,-7.201,8.828,-7.199,8.747,-7.22,8.665,-7.201,8.549,-7.142,8.398,-7.103,8.172,-7.072,7.719,-7.064,7.643,-7.042,7.611,-7.003,7.596,-6.989,7.553,-7,7.481,-7.026,7.426,-7.066,7.386,-7.075,7.285,-7.042,7.042],[-7.167,9.714,-7.214,9.605,-7.23,9.511,-7.23,9.336,-7.204,9.193,-7.212,9.126,-7.244,9.088,-7.255,9.04,-7.236,8.952]]},{"n":"Savannah","z":5,"b":[-82.819,32.03,-80.872,34.366],"l":[[-82.208,33.664,-82.006,33.523,-81.936,33.447,-81.933,33.39,-81.909,33.349,-81.864,33.325,-81.821,33.274,-81.78,33.195,-81.699,33.127,-81.578,33.069,-81.49,32.936,-81.434,32.728,-81.377,32.607,-81.29,32.557,-81.171,32.38,-81.133,32.275,-81.135,32.182,-81.075,32.11,-80.872,32.03],[-82.819,34.366,-82.589,34.017]]},{"n":"Seine","z":4,"b":[0.439,47.578,4.708,49.494],"l":[[4.708,47.578,4.584,47.74,4.554,47.812,4.574,47.85,4.565,47.891,4.506,47.958,4.408,48.082,4.322,48.145,4.214,48.19,4.096,48.284,3.969,48.427,3.858,48.517,3.763,48.551,3.561,48.526,3.252,48.44,3.038,48.393,2.922,48.385,2.819,48.409,2.687,48.492,2.573,48.533,2.499,48.582,2.428,48.65,2.405,48.699,2.43,48.728,2.43,48.766,2.404,48.811,2.352,48.834,2.296,48.852,2.27,48.875,2.25,48.908,2.217,48.912,2.173,48.904,2.144,48.928,2.138,48.948,2.111,48.958,2.062,48.954,2.008,48.961,1.976,48.996,1.908,49.006,1.803,48.99,1.718,48.992,1.675,49.017,1.668,49.047,1.635,49.06,1.585,49.052,1.497,49.084,1.397,49.16,1.354,49.211,1.314,49.219,1.279,49.212,1.244,49.221,1.238,49.244,1.228,49.282,1.189,49.307,1.092,49.312,1.056,49.328,1.043,49.355,1.023,49.381,0.972,49.378,0.93,49.39,0.897,49.424,0.853,49.415,0.828,49.426,0.785,49.471,0.719,49.48,0.652,49.46,0.585,49.453,0.493,49.494,0.439,49.473]]},{"n":"Selenge (Selenga)","z":2.1,"b":[100.7,49.263,107.554,52.383],"l":[[100.7,49.263,101.061,49.346,101.164,49.41,101.256,49.439,101.37,49.455,101.467,49.445,101.548,49.407,101.696,49.391,101.911,49.397,102.061,49.385,102.148,49.356,102.321,49.355,102.581,49.382,102.738,49.385,102.791,49.363,102.832,49.363,102.86,49.385,102.976,49.377,103.182,49.338,103.444,49.361,103.765,49.446,104.012,49.484,104.187,49.475,104.342,49.513,104.477,49.596,104.763,49.692,105.198,49.803,105.6,49.958,106.155,50.254,106.083,50.375,106.115,50.446,106.226,50.508,106.271,50.559,106.248,50.6,106.255,50.644,106.293,50.69,106.301,50.751,106.277,50.826,106.277,50.885,106.3,50.929,106.394,50.974,106.556,51.021,106.679,51.091,106.763,51.184,106.851,51.252,106.992,51.317,107.063,51.424,107.148,51.478,107.269,51.514,107.391,51.609,107.512,51.763,107.554,51.853,107.516,51.88,107.502,51.932,107.513,52.009,107.5,52.074,107.462,52.125,107.35,52.145,107.162,52.132,107,52.104,106.862,52.059,106.753,52.055,106.672,52.092,106.615,52.145],[106.615,52.145,106.583,52.214,106.614,52.266,106.752,52.316],[106.615,52.145,106.499,52.234,106.477,52.29,106.497,52.383]]},{"n":"Semliki","z":5,"b":[29.367,-1.22,30.478,1.239],"l":[[29.838,-1.2,29.782,-1.141,29.697,-1.149,29.564,-1.204,29.476,-1.22,29.433,-1.199,29.397,-1.158,29.367,-1.098,29.377,-0.952,29.452,-0.607],[29.597,-0.134,29.638,0.113,29.614,0.234,29.537,0.31,29.534,0.386,29.604,0.464,29.661,0.559,29.705,0.671,29.834,0.773,30.047,0.864,30.183,0.973,30.24,1.103,30.321,1.185,30.478,1.239]]},{"n":"Setit","z":5,"b":[35.865,13.875,37.817,14.62],"l":[[37.817,13.875,37.7,13.91,37.629,13.97,37.535,14.117,37.354,14.372,37.257,14.454,37.185,14.446,37.133,14.406,37.099,14.334,37.064,14.289,37.025,14.272,36.941,14.281,36.812,14.315,36.679,14.308,36.542,14.258,36.427,14.25,36.334,14.282,36.22,14.279,36.085,14.242,35.977,14.258,35.895,14.326,35.865,14.425,35.895,14.62]]},{"n":"Shatt al Arab","z":3,"b":[47.468,29.962,48.547,30.968],"l":[[47.468,30.968,47.582,30.831,47.647,30.775,47.702,30.754,47.736,30.704,47.749,30.625,47.785,30.561,47.845,30.512,47.938,30.478,48.066,30.458,48.148,30.417,48.182,30.355,48.226,30.321,48.279,30.316,48.331,30.285,48.383,30.23,48.401,30.188,48.388,30.16,48.399,30.11,48.435,30.038,48.479,30.004,48.547,29.962]]},{"n":"Shiquan","z":3,"b":[79.735,31.4,81.64,32.553],"l":[[81.64,31.45,81.549,31.404,81.5,31.4,81.446,31.432,81.293,31.535,81.225,31.602,81.191,31.667,81.187,31.743,81.213,31.828,81.268,31.9,81.352,31.957,81.366,32.002,81.31,32.033,81.282,32.08,81.283,32.142,81.252,32.224,81.188,32.325,81.029,32.42,80.775,32.509,80.535,32.553,80.309,32.552,80.08,32.525,79.735,32.444]]},{"n":"Shire","z":3,"b":[34.743,-17.705,35.293,-14.401],"l":[[35.278,-14.758,35.293,-14.921,35.251,-15.057,35.107,-15.303,34.96,-15.372,34.874,-15.445,34.802,-15.544,34.758,-15.624,34.743,-15.685,34.751,-15.784,34.795,-15.991,34.889,-16.13,34.925,-16.225,34.934,-16.323,34.975,-16.392,35.049,-16.434,35.134,-16.516,35.23,-16.639,35.281,-16.808,35.291,-17.129,35.291,-17.705],[35.257,-14.504,35.236,-14.401]]},{"n":"Shishhid Gol","z":4,"b":[98,50.552,99.491,51.514],"l":[[98.809,50.627,98.985,50.557,99.063,50.552,99.121,50.61,99.207,50.751,99.322,50.809,99.403,50.898,99.478,51.048,99.491,51.149,99.444,51.2,99.466,51.329,99.424,51.349],[99.31,51.41,99.004,51.473,98.764,51.474,98.487,51.437,98.262,51.442,98,51.514]]},{"n":"Skeena","z":5,"b":[-129.626,54.228,-127.384,57.079],"l":[[-128.57,57.079,-128.284,56.907,-127.851,56.717,-127.621,56.574,-127.448,56.417,-127.384,56.322,-127.428,56.289,-127.543,56.278,-127.728,56.291,-127.858,56.274,-127.931,56.228,-127.967,56.169,-127.965,56.096,-127.939,56.03,-127.89,55.972,-127.889,55.904,-127.936,55.829,-127.891,55.779,-127.753,55.756,-127.703,55.721,-127.739,55.675,-127.72,55.594,-127.645,55.478,-127.627,55.405,-127.665,55.374,-127.685,55.33,-127.685,55.274,-127.717,55.218,-127.781,55.162,-127.903,55.126,-128.085,55.11,-128.225,55.073,-128.323,55.018,-128.374,54.967,-128.377,54.923,-128.351,54.87,-128.294,54.809,-128.317,54.725,-128.47,54.567,-129.484,54.242,-129.542,54.228,-129.578,54.228,-129.626,54.23]]},{"n":"Slave","z":2.1,"b":[-113.704,59.005,-111.384,61.253],"l":[[-111.411,59.005,-111.384,59.153,-111.408,59.213,-111.474,59.234,-111.473,59.262,-111.404,59.296,-111.394,59.346,-111.442,59.411,-111.453,59.49,-111.425,59.582,-111.45,59.683,-111.569,59.848,-111.631,59.922,-111.7,59.966,-111.799,60.005,-111.923,60.026,-112.07,60.029,-112.206,60.097,-112.33,60.232,-112.465,60.317,-112.523,60.332,-112.56,60.346,-112.551,60.362,-112.51,60.369,-112.456,60.381,-112.449,60.41,-112.461,60.438,-112.495,60.466,-112.497,60.49,-112.511,60.52,-112.547,60.543,-112.734,60.591,-112.838,60.643,-112.857,60.701,-112.898,60.739,-112.933,60.743,-112.957,60.754,-112.969,60.786,-112.978,60.801,-113.001,60.802,-113.061,60.777,-113.13,60.769,-113.186,60.784,-113.226,60.832,-113.247,60.911,-113.219,60.974,-113.142,61.02,-113.134,61.054,-113.151,61.069,-113.181,61.087,-113.192,61.12,-113.16,61.158,-113.164,61.172,-113.177,61.182,-113.211,61.188,-113.223,61.207,-113.214,61.226,-113.215,61.24,-113.252,61.249,-113.319,61.253,-113.704,61.246]]},{"n":"Snake","z":4,"b":[-119.036,42.522,-110.631,46.715],"l":[[-113.474,42.675,-113.624,42.578,-113.785,42.538,-114.008,42.522,-114.284,42.554,-114.614,42.635,-114.813,42.707,-114.882,42.771,-114.913,42.825,-114.908,42.871,-114.961,42.9,-115.072,42.913,-115.144,42.941,-115.176,42.985,-115.257,42.989,-115.386,42.953,-115.569,42.956,-115.806,42.999,-115.916,42.995,-115.974,42.978,-116.079,43.019,-116.257,43.129,-116.359,43.212,-116.386,43.269,-116.439,43.304,-116.516,43.316,-116.606,43.364,-116.707,43.448,-116.767,43.514,-116.785,43.562,-116.865,43.617,-117.006,43.679,-117.064,43.734,-117.024,43.806,-116.958,43.964,-116.944,44.038,-116.961,44.079,-116.95,44.119,-116.911,44.157,-116.912,44.199,-116.953,44.245,-117.029,44.276,-117.14,44.29,-117.197,44.344,-117.202,44.436,-117.119,44.578,-116.949,44.77,-116.856,44.906,-116.84,44.985,-116.737,45.169,-116.549,45.457,-116.477,45.641,-116.521,45.722,-116.617,45.799,-116.765,45.873,-116.869,45.958,-116.93,46.056,-116.948,46.124,-116.922,46.164,-116.943,46.232,-117.01,46.33,-117.039,46.397,-117.029,46.433,-117.07,46.449,-117.162,46.445,-117.214,46.468,-117.224,46.517,-117.284,46.582,-117.393,46.664,-117.503,46.709,-117.615,46.715,-117.702,46.698,-117.764,46.655,-117.875,46.618,-118.035,46.586,-118.205,46.577,-118.386,46.59,-118.546,46.527,-118.688,46.387,-118.827,46.291,-119.036,46.206],[-112.864,42.812,-113.022,42.686,-113.135,42.646,-113.316,42.654],[-111.188,43.339,-111.348,43.448,-111.409,43.51,-111.425,43.561,-111.586,43.635,-111.894,43.73,-112.043,43.708,-112.033,43.568,-112.072,43.446,-112.159,43.343,-112.312,43.225,-112.639,43.024],[-110.631,43.837,-110.727,43.62,-110.78,43.535,-110.822,43.512,-110.837,43.478,-110.825,43.435,-110.792,43.402,-110.738,43.378,-110.725,43.336,-110.754,43.274,-110.837,43.226,-110.983,43.19,-111.048,43.207]]},{"n":"Sungari","z":5,"b":[124.675,45.389,132.562,47.769],"l":[[124.675,45.45,124.814,45.446,124.875,45.466,124.902,45.511,124.947,45.523,125.01,45.503,125.052,45.466,125.072,45.413,125.156,45.389,125.302,45.395,125.394,45.42,125.432,45.463,125.549,45.488,125.745,45.495,125.888,45.55,125.979,45.652,126.107,45.702,126.273,45.698,126.415,45.711,126.534,45.741,126.63,45.797,126.704,45.876,126.802,45.943,126.923,45.997,127.083,45.998,127.284,45.945,127.416,45.922,127.479,45.93,127.527,45.961,127.559,46.014,127.64,46.011,127.772,45.953,127.91,45.922,128.056,45.918,128.187,45.932,128.303,45.963,128.419,45.959,128.533,45.921,128.648,45.92,128.762,45.955,128.86,45.948,128.941,45.899,129.036,45.899,129.145,45.947,129.247,46.042,129.342,46.183,129.432,46.277,129.515,46.321,129.577,46.382,129.617,46.458,129.702,46.525,129.831,46.582,129.915,46.649,129.953,46.724,130.003,46.764,130.065,46.767,130.104,46.784,130.12,46.816,130.179,46.833,130.332,46.835],[132.562,47.769,132.414,47.567,132.3,47.48,132.142,47.4,132.044,47.334,132.007,47.28,131.705,47.237,131.137,47.205,130.81,47.151,130.722,47.074,130.645,47.027,130.578,47.01,130.531,46.97,130.503,46.908,130.449,46.867,130.33,46.837]]},{"n":"Soroksari Duna","z":5,"b":[18.935,47.033,19.098,47.481],"l":[[19.06,47.481,19.091,47.439,19.098,47.418,19.094,47.394,19.052,47.354,18.974,47.275,18.949,47.199,18.973,47.145,18.976,47.101,18.935,47.033]]},{"n":"South Saskatchewan","z":5,"b":[-116.422,49.904,-105.065,53.24],"l":[[-106.853,51.263,-107.015,51.388,-107.067,51.464,-107.062,51.533,-106.993,51.608,-106.859,51.687,-106.776,51.748,-106.744,51.792,-106.738,51.853,-106.758,51.932,-106.755,51.976,-106.727,51.986,-106.713,52.012,-106.713,52.055,-106.574,52.198,-106.294,52.441,-106.148,52.616,-106.135,52.722,-106.103,52.808,-106.054,52.873,-105.903,52.932,-105.651,52.983,-105.497,53.046,-105.442,53.122,-105.373,53.153,-105.289,53.139,-105.201,53.159,-105.065,53.24],[-116.422,51.667,-116.357,51.651,-116.319,51.612,-116.271,51.51,-115.86,51.273,-115.658,51.194,-115.53,51.194,-115.414,51.159,-115.312,51.09,-115.1,51.103,-114.781,51.198,-114.48,51.196,-114.197,51.094,-114.041,50.999,-114.01,50.909,-113.94,50.85,-113.832,50.825,-113.581,50.799,-113.447,50.813,-113.31,50.848,-113.19,50.838,-113.088,50.783,-113.003,50.767,-112.933,50.788,-112.809,50.789,-112.632,50.77,-112.524,50.729,-112.485,50.665,-112.444,50.631,-112.4,50.626,-112.357,50.574,-112.316,50.475,-112.263,50.422,-112.197,50.415,-112.168,50.393,-112.173,50.355,-112.151,50.303,-112.101,50.238,-112.031,50.205,-111.941,50.204,-111.825,50.176,-111.684,50.121,-111.609,50.065,-111.6,50.01,-111.624,49.968,-111.643,49.937,-111.607,49.904,-111.523,49.909,-111.422,49.934,-111.342,50.001,-111.264,50.041,-111.188,50.055,-110.775,50.046,-110.66,50.059,-110.681,50.095,-110.664,50.12,-110.609,50.134,-110.58,50.155,-110.577,50.185,-110.594,50.211,-110.632,50.235,-110.632,50.299,-110.594,50.404,-110.529,50.448,-110.436,50.433,-110.391,50.463,-110.393,50.537,-110.344,50.59,-110.245,50.62,-110.158,50.661,-110.084,50.714,-110.054,50.754,-110.068,50.781,-110.054,50.81,-110.013,50.838,-109.998,50.863,-110.011,50.884,-109.977,50.909,-109.897,50.938,-109.634,50.97,-109.188,51.004,-108.925,51.002,-108.846,50.965,-108.73,50.939,-108.576,50.925,-108.441,50.858,-108.326,50.737,-108.203,50.672,-108.006,50.658]]},{"n":"Sprague","z":4.7,"b":[-121.921,42.4,-120.882,42.583],"l":[[-120.882,42.4,-121.048,42.441,-121.437,42.482,-121.604,42.525,-121.679,42.583,-121.776,42.579,-121.889,42.512,-121.921,42.475]]},{"n":"St. Clair","z":3,"b":[-82.645,42.558,-82.417,43.017],"l":[[-82.417,43.017,-82.488,42.74,-82.545,42.625,-82.645,42.558]]},{"n":"St. Lawrence","z":2.1,"b":[-75.792,44.497,-74.709,45.004],"l":[[-74.709,45.004,-74.762,44.999,-74.857,45.004,-74.996,44.97,-75.179,44.899,-75.401,44.772,-75.792,44.497]]},{"n":"Stikine","z":4.7,"b":[-132.358,56.626,-127.596,58.148],"l":[[-128.13,57.292,-127.939,57.37,-127.833,57.394,-127.705,57.389,-127.601,57.432,-127.596,57.49,-127.656,57.576,-127.664,57.639,-127.62,57.678,-127.723,57.706,-127.974,57.722,-128.128,57.762,-128.186,57.826,-128.286,57.89,-128.428,57.956,-128.615,57.976,-128.846,57.952,-129.04,57.949,-129.196,57.966,-129.332,57.96,-129.449,57.931,-129.733,57.975,-130.185,58.093,-130.475,58.148,-130.605,58.14,-130.76,58.105,-130.939,58.041,-131.055,57.988,-131.108,57.945,-131.19,57.909,-131.302,57.882,-131.418,57.821,-131.598,57.679,-131.728,57.553,-131.767,57.467,-131.757,57.379,-131.775,57.333,-131.821,57.329,-131.841,57.306,-131.833,57.263,-131.794,57.214,-131.725,57.159,-131.71,57.116,-131.751,57.086,-131.775,56.978,-131.781,56.792,-131.801,56.753,-131.834,56.715,-131.881,56.703,-132.075,56.711,-132.206,56.696,-132.311,56.639,-132.358,56.626]]},{"n":"Suez Canal","z":5,"b":[32.303,29.974,32.566,31.256],"l":[[32.561,30.182,32.566,29.974],[32.36,30.424,32.303,30.593,32.334,30.689,32.337,30.859,32.324,31.256]]},{"n":"Sukhona","z":4,"b":[38.656,59.302,46.34,60.76],"l":[[39.798,59.525,40.099,59.476,40.215,59.447,40.248,59.421,40.262,59.38,40.26,59.323,40.35,59.302,40.532,59.318,40.684,59.352,40.808,59.404,40.917,59.418,41.011,59.396,41.155,59.391,41.349,59.403,41.558,59.43,41.784,59.473,41.9,59.512,41.906,59.546,41.954,59.584,42.044,59.628,42.169,59.657,42.328,59.67,42.462,59.7,42.57,59.746,42.653,59.816,42.711,59.91,42.853,59.982,43.191,60.057,43.731,60.19,44.032,60.292,44.276,60.407,44.492,60.474,44.679,60.493,44.835,60.526,44.962,60.573,45.156,60.6,45.418,60.605,45.606,60.632,45.72,60.68,45.831,60.697,45.939,60.684,46.051,60.699,46.166,60.742,46.252,60.76,46.34,60.749],[39.053,59.868,38.928,59.939,38.656,59.93]]},{"n":"Susitna","z":5,"b":[-150.57,61.307,-146.684,62.859],"l":[[-146.684,62.505,-146.829,62.576,-146.879,62.615,-146.884,62.646,-146.959,62.676,-147.103,62.705,-147.228,62.702,-147.335,62.668,-147.493,62.689,-147.705,62.765,-147.89,62.812,-148.125,62.838,-148.51,62.849,-148.68,62.839,-148.763,62.811,-148.867,62.813,-148.993,62.844,-149.188,62.859,-149.453,62.855,-149.694,62.803,-149.912,62.702,-150.044,62.619,-150.113,62.519,-150.159,62.434,-150.162,62.355,-150.136,62.255,-150.138,62.193,-150.168,62.169,-150.161,62.14,-150.114,62.106,-150.094,62.049,-150.099,61.968,-150.172,61.843,-150.313,61.676,-150.425,61.577,-150.509,61.547,-150.559,61.488,-150.57,61.364,-150.567,61.307]]},{"n":"Sutlej","z":5,"b":[71.014,29.326,81.376,31.837],"l":[[81.376,30.758,81.294,30.792],[81.199,30.852,80.874,31.054,80.731,31.121,80.628,31.121,80.534,31.066,80.474,31.053,80.418,31.064,80.296,31.125,80.107,31.235,79.98,31.336,79.917,31.426,79.795,31.481,79.616,31.5,79.456,31.55,79.315,31.632,79.171,31.67,79.023,31.664,78.922,31.697,78.869,31.769,78.811,31.816,78.749,31.837,78.652,31.804,78.521,31.718,78.452,31.657,78.445,31.619,78.405,31.6,78.33,31.6,78.284,31.581,78.269,31.541,78.153,31.532,77.937,31.553,77.757,31.512,77.611,31.407,77.495,31.356,77.411,31.358,77.353,31.338,77.323,31.297,77.27,31.267,77.196,31.25,77.09,31.273,76.952,31.337,76.883,31.38,76.884,31.403,76.854,31.413,76.765,31.409],[76.435,31.422,76.384,31.407,76.37,31.387,76.375,31.355,76.535,31.144,76.567,31.053,76.526,31.003,76.36,30.989,76.069,31.011,75.791,31.013,75.527,30.994,75.305,31.035,75.124,31.134,74.984,31.177,74.886,31.165,74.728,31.092,74.51,30.96,74.38,30.893,74.339,30.894,74.216,30.769,74.009,30.52,73.806,30.363,73.505,30.268,73.382,30.229,73.325,30.194,73.295,30.151,73.268,30.139,73.245,30.16,73.158,30.124,73.008,30.03,72.858,29.96,72.71,29.915,72.631,29.876,72.623,29.843,72.554,29.829,72.426,29.835,72.309,29.794,72.202,29.704,72.153,29.64,72.162,29.599,72.121,29.557,72.03,29.513,71.905,29.487,71.747,29.478,71.65,29.452,71.615,29.409,71.565,29.382,71.5,29.372,71.418,29.382,71.32,29.413,71.209,29.413,71.014,29.326]]},{"n":"Severnaya Dvina","z":4,"b":[40.408,60.749,46.617,64.755],"l":[[46.34,60.749,46.456,60.843,46.512,60.91,46.547,60.982,46.565,61.044,46.565,61.096,46.58,61.13,46.61,61.145,46.617,61.186,46.591,61.287,46.255,61.472,45.945,61.595,45.551,61.717,45.291,61.84,45.167,61.964,45.091,62.061,45.063,62.13,44.975,62.206,44.826,62.29,44.562,62.347,44.184,62.377,43.907,62.442,43.732,62.543,43.404,62.67,42.924,62.824,42.657,62.934,42.605,63.002,42.487,63.045,42.212,63.073,42.077,63.141,42.041,63.195,42.057,63.258,42.016,63.329,41.916,63.408,41.872,63.483,41.885,63.553,41.806,63.605,41.634,63.639,41.515,63.681,41.448,63.732,41.47,63.785,41.58,63.84,41.633,63.88,41.628,63.904,41.658,63.944,41.724,63.999,41.752,64.052,41.742,64.102,41.777,64.137,41.857,64.157,41.884,64.192,41.856,64.244,41.654,64.295,41.277,64.345,40.949,64.416,40.53,64.555,40.45,64.63,40.408,64.755]]},{"n":"Svir’","z":4.7,"b":[32.816,60.477,34.846,61.027],"l":[[32.816,60.482,32.863,60.477,32.996,60.511,33.14,60.576,33.29,60.68,33.438,60.738,33.583,60.752,33.713,60.811,33.828,60.915,33.918,60.964,34.017,60.958,34.086,60.926,34.293,60.943,34.846,61.027]]},{"n":"Syr Darya","z":4.7,"b":[60.878,40.225,69.827,46.137],"l":[[69.827,40.273,69.684,40.302,69.563,40.286,69.414,40.234,69.299,40.225,69.218,40.259,69.152,40.338,69.101,40.461,69.087,40.539,69.094,40.577,69.082,40.599,69.02,40.658,68.817,40.797,68.702,40.898,68.675,40.962,68.61,41.022,68.456,41.107],[67.991,41.255,68.008,41.285,68.069,41.336,68.087,41.463,68.051,41.658,68.067,41.834,68.134,41.991,68.186,42.081,68.201,42.099,68.207,42.132,68.192,42.15,68.188,42.182,68.255,42.237,68.285,42.302,68.277,42.377,68.253,42.443,68.211,42.501,68.208,42.554,68.243,42.603,68.255,42.663,68.245,42.735,68.229,42.769,68.224,42.794,68.2,42.825,68.135,42.866,68.119,42.908,68.129,42.947,68.089,42.991,67.929,43.031,67.838,43.121,67.818,43.26,67.789,43.341,67.751,43.365,67.72,43.358,67.693,43.331,67.66,43.343,67.646,43.389,67.605,43.418,67.548,43.421,67.514,43.459,67.504,43.534,67.444,43.631,67.333,43.751,67.259,43.811,67.222,43.812,67.195,43.839,67.176,43.894,67.107,43.949,66.988,44.003,66.902,44.009,66.85,43.967,66.771,43.959,66.665,43.985,66.603,43.989,66.561,44.009,66.533,44.102,66.494,44.152,66.442,44.158,66.418,44.181,66.422,44.218,66.382,44.268,66.299,44.33,66.263,44.371,66.188,44.425,66.019,44.457,65.975,44.509,65.957,44.54,65.916,44.584,65.833,44.64,65.794,44.687,65.791,44.723,65.738,44.748,65.616,44.756,65.493,44.816,65.37,44.927,65.23,44.993,65.072,45.014,64.964,45.012,64.904,44.989,64.784,45.002,64.602,45.052,64.466,45.113,64.375,45.187,64.334,45.234,64.318,45.27,64.261,45.281,64.221,45.322,64.198,45.394,64.112,45.446,63.964,45.48,63.883,45.511,63.869,45.539,63.877,45.567,63.907,45.595,63.876,45.632,63.784,45.68,63.752,45.713,63.7,45.718,63.336,45.635,63.124,45.633,63.005,45.7,62.9,45.719,62.809,45.691,62.733,45.691,62.673,45.72,62.634,45.759,62.616,45.809,62.565,45.813,62.483,45.772,62.376,45.758,62.246,45.772,62.127,45.751,62.019,45.696,61.897,45.705,61.76,45.776,61.703,45.833,61.725,45.875,61.763,45.903,61.817,45.918,61.823,45.949,61.76,46.021],[61.562,46.11,61.264,46.051,61.093,46.058,60.878,46.137]]},{"n":"São Francisco","z":4,"b":[-45.262,-18.204,-36.412,-8.559],"l":[[-45.262,-18.204,-45.242,-18.126,-45.221,-18.103,-45.19,-18.109,-45.16,-18.036,-45.13,-17.886,-45.086,-17.797,-45.028,-17.77,-45.008,-17.717,-45.027,-17.637,-45.007,-17.514,-44.95,-17.347,-44.888,-17.219,-44.823,-17.13,-44.87,-16.94,-45.028,-16.649,-45.089,-16.483,-45.052,-16.444,-45.038,-16.316,-45.049,-15.991,-44.922,-15.959,-44.861,-15.917,-44.823,-15.854,-44.744,-15.795,-44.563,-15.711,-44.271,-15.458,-44.154,-15.311,-44.116,-15.185,-44.054,-15.057,-43.971,-14.926,-43.906,-14.752,-43.861,-14.534,-43.8,-14.37,-43.724,-14.258,-43.654,-14.062,-43.591,-13.781,-43.532,-13.613,-43.479,-13.556,-43.454,-13.408,-43.457,-13.17,-43.443,-13.008,-43.411,-12.923,-43.342,-12.82,-43.238,-12.698,-43.209,-12.559,-43.256,-12.403,-43.265,-12.291,-43.235,-12.222,-43.254,-12.13,-43.323,-12.013,-43.348,-11.931,-43.329,-11.885,-43.336,-11.805,-43.37,-11.691,-43.362,-11.614,-43.314,-11.574,-43.255,-11.451,-43.15,-11.143],[-40.818,-9.438,-40.671,-9.474,-40.552,-9.457,-40.413,-9.397,-40.32,-9.293,-40.273,-9.145,-40.212,-9.102,-40.148,-9.095,-40.078,-9.088,-40.005,-9.058,-39.949,-9.022,-39.909,-8.98,-39.888,-8.924,-39.885,-8.853,-39.835,-8.809,-39.738,-8.791,-39.688,-8.749,-39.683,-8.683,-39.601,-8.62,-39.442,-8.559,-39.327,-8.566,-39.257,-8.64,-39.093,-8.718,-38.835,-8.8,-38.705,-8.88,-38.668,-8.896,-38.619,-8.896,-38.584,-8.866,-38.544,-8.831,-38.505,-8.835,-38.484,-8.878,-38.482,-8.96,-38.433,-9.004,-38.336,-9.01,-38.294,-9.043,-38.308,-9.102,-38.283,-9.204,-38.221,-9.348,-37.938,-9.538,-37.183,-9.893,-37.019,-9.957,-36.956,-10.014,-36.94,-10.084,-36.88,-10.154,-36.777,-10.224,-36.699,-10.257,-36.647,-10.255,-36.607,-10.293,-36.58,-10.371,-36.538,-10.411,-36.483,-10.411,-36.443,-10.432,-36.412,-10.49]]},{"n":"Sénégal","z":4,"b":[-16.535,13.778,-10.892,16.679],"l":[[-10.892,13.778,-10.914,13.789,-10.952,13.801,-11.011,13.856,-11.062,13.972,-11.129,14.045,-11.213,14.074,-11.246,14.105,-11.227,14.138,-11.23,14.192,-11.253,14.267,-11.3,14.343,-11.371,14.418,-11.477,14.49,-11.618,14.557,-11.74,14.598,-11.843,14.612,-11.936,14.647,-12.061,14.729,-12.303,14.817,-12.409,14.889,-12.46,14.975,-12.544,15.039,-12.66,15.082,-12.735,15.131,-12.77,15.187,-12.813,15.224,-12.859,15.243,-12.863,15.262,-12.852,15.29,-12.863,15.34,-12.931,15.453,-12.994,15.505,-13.049,15.497,-13.079,15.51,-13.098,15.535,-13.105,15.572,-13.142,15.603,-13.206,15.617,-13.258,15.7,-13.297,15.854,-13.348,15.973,-13.41,16.059,-13.454,16.091,-13.487,16.097,-13.498,16.11,-13.507,16.135,-13.556,16.144,-13.624,16.118,-13.685,16.127,-13.715,16.169,-13.757,16.173,-13.81,16.138,-13.868,16.148,-13.933,16.203,-13.968,16.257,-13.975,16.311,-14.086,16.419,-14.3,16.58,-14.534,16.656,-14.787,16.646,-14.929,16.654,-14.96,16.679,-14.991,16.677,-15.022,16.647,-15.055,16.641,-15.091,16.657,-15.113,16.645,-15.121,16.604,-15.211,16.583,-15.38,16.582,-15.517,16.557,-15.621,16.507,-15.768,16.485,-15.959,16.492,-16.074,16.51,-16.113,16.54,-16.168,16.547,-16.239,16.531,-16.302,16.451,-16.358,16.307,-16.404,16.225,-16.441,16.205,-16.48,16.097,-16.502,15.917,-16.535,15.838]]},{"n":"Tajo","z":4.7,"b":[-7.118,39.682,-1.906,40.811],"l":[[-5.996,39.852,-6.326,39.764,-6.533,39.746,-6.726,39.77,-6.896,39.757,-7.118,39.682],[-2.733,40.528,-2.961,40.275,-3.1,40.156,-3.226,40.087,-3.381,40.048,-3.565,40.038,-3.739,39.995,-3.901,39.92,-4.059,39.881,-4.213,39.879,-4.318,39.839,-4.428,39.867,-4.628,39.948,-4.757,39.982,-4.816,39.968,-4.889,39.921,-4.978,39.84,-5.071,39.803,-5.217,39.812],[-1.906,40.601,-2.06,40.745,-2.144,40.797,-2.242,40.811,-2.577,40.724]]},{"n":"Tanana","z":5,"b":[-152.048,62.027,-141.395,65.165],"l":[[-142.022,62.027,-142.071,62.066,-142.087,62.109,-142.085,62.171,-142.051,62.217,-141.951,62.264,-141.817,62.306,-141.743,62.355,-141.685,62.424,-141.602,62.48,-141.494,62.523,-141.425,62.564,-141.395,62.604,-141.413,62.673,-141.479,62.771,-141.552,62.829,-141.633,62.847,-141.656,62.872,-141.622,62.905,-141.738,62.983,-142.004,63.105,-142.192,63.165,-142.302,63.165,-142.42,63.186,-142.544,63.227,-142.632,63.279,-142.686,63.34,-142.744,63.368,-142.807,63.362,-142.879,63.378,-142.961,63.417,-143.038,63.43,-143.149,63.411,-143.617,63.404,-143.789,63.42,-143.822,63.457,-143.827,63.499,-143.803,63.544,-143.827,63.593,-143.899,63.645,-144.101,63.688,-144.433,63.722,-144.678,63.791,-144.835,63.894,-144.895,63.958,-144.887,64,-144.915,64.034,-145.032,64.068,-145.158,64.084,-145.294,64.083,-145.439,64.108,-145.594,64.16,-145.726,64.18,-145.834,64.168,-146.029,64.192,-146.309,64.254,-146.505,64.278,-146.672,64.256,-146.934,64.383,-147.021,64.456,-147.021,64.516,-147.08,64.582,-147.197,64.653,-147.322,64.711,-147.456,64.754,-147.626,64.782,-147.832,64.796,-147.993,64.783,-148.163,64.726,-148.566,64.62,-148.801,64.583,-149.116,64.582,-149.138,64.594,-149.14,64.647,-149.123,64.741,-149.131,64.783,-149.156,64.804,-149.156,64.824,-149.129,64.853,-149.139,64.878,-149.187,64.901,-149.307,64.911,-149.499,64.909,-149.676,64.877,-149.839,64.814,-150.082,64.832,-150.404,64.93,-150.674,64.977,-150.89,64.973,-151.09,64.947,-151.273,64.9,-151.415,64.905,-151.517,64.962,-151.553,64.995,-151.544,65.025,-151.565,65.034,-151.623,65.016,-151.675,65.029,-151.697,65.085,-151.793,65.127,-152.048,65.165]]},{"n":"Tapajós","z":4.7,"b":[-58.659,-10.975,-54.639,-2.385],"l":[[-58.659,-10.975,-58.591,-10.664,-58.513,-10.521,-58.404,-10.442,-58.316,-10.277,-58.248,-10.024,-58.228,-9.802,-58.257,-9.609,-58.352,-9.475,-58.512,-9.4,-58.609,-9.303,-58.644,-9.182,-58.64,-9.069,-58.599,-8.963,-58.515,-8.858,-58.387,-8.754,-58.358,-8.691,-58.428,-8.668,-58.427,-8.523,-58.353,-8.256,-58.329,-8.051,-58.354,-7.906,-58.33,-7.78,-58.259,-7.673,-58.224,-7.575,-58.224,-7.485,-58.202,-7.415,-58.136,-7.34,-58.146,-7.263,-58.228,-7.142,-58.386,-6.949,-58.455,-6.794,-58.437,-6.676,-58.375,-6.57,-58.269,-6.478,-58.144,-6.413,-58.002,-6.378,-57.872,-6.318,-57.754,-6.235,-57.681,-6.127,-57.652,-5.996,-57.492,-5.82,-57.2,-5.598,-57.042,-5.435,-57.019,-5.329,-56.97,-5.247,-56.894,-5.188,-56.821,-5.059,-56.75,-4.862,-56.686,-4.745,-56.63,-4.708,-56.56,-4.7,-56.475,-4.722,-56.397,-4.695,-56.326,-4.62,-56.283,-4.533,-56.268,-4.435,-56.155,-4.333,-56.008,-4.26,-55.945,-4.229,-55.786,-4.128,-55.678,-4.03,-55.567,-3.892,-55.394,-3.627,-55.233,-3.279,-55.145,-2.982,-55.077,-2.62,-54.992,-2.423,-54.89,-2.392,-54.798,-2.385,-54.639,-2.419]]},{"n":"Tarim","z":5,"b":[80.888,40.467,86.753,41.826],"l":[[80.888,40.467,81.026,40.519,81.129,40.535,81.242,40.533,81.343,40.574,81.429,40.659,81.546,40.707,81.693,40.719,81.81,40.753,81.896,40.808,82.121,40.886,82.486,40.987,82.749,41.004,82.912,40.939,83.082,40.928,83.26,40.971,83.455,40.998,83.666,41.009,83.793,41.001,83.835,40.975,83.896,40.998,83.975,41.07,84.091,41.131,84.246,41.182,84.477,41.195,84.784,41.169,84.984,41.129,85.077,41.077,85.218,41.028,85.407,40.984,85.617,40.993,85.848,41.054,86.038,41.071,86.185,41.042,86.335,41.037,86.486,41.058,86.505,41.1,86.392,41.163,86.345,41.206,86.372,41.243,86.382,41.278,86.368,41.285,86.334,41.275,86.266,41.297,86.163,41.352,86.061,41.382,85.959,41.388,85.877,41.426,85.814,41.496,85.786,41.571,85.795,41.651,85.829,41.707,85.888,41.738,85.973,41.753,86.085,41.752,86.165,41.772,86.212,41.815,86.305,41.819,86.443,41.786,86.573,41.783,86.753,41.826]]},{"n":"Taritatu","z":5,"b":[138.702,-3.711,140.295,-3.191],"l":[[140.295,-3.711,139.942,-3.629,139.789,-3.552,139.684,-3.528,139.575,-3.531,139.522,-3.493,139.487,-3.471,139.416,-3.481,139.384,-3.473,139.335,-3.439,139.21,-3.443,139.122,-3.403,139.07,-3.318,138.958,-3.254,138.755,-3.204,138.702,-3.191]]},{"n":"Taz","z":5,"b":[78.59,62.637,85.617,67.578],"l":[[84.22,62.637,84.445,62.811,84.582,62.886,84.833,62.966,85.245,63.077,85.506,63.189,85.617,63.303,85.609,63.357,85.482,63.352,85.389,63.391,85.329,63.475,85.184,63.537,84.955,63.576,84.806,63.62,84.738,63.667,84.636,63.696,84.5,63.707,84.323,63.682,84.105,63.623,83.915,63.588,83.753,63.58,83.706,63.595,83.774,63.634,83.765,63.681,83.681,63.736,83.602,63.758,83.529,63.746,83.496,63.751,83.502,63.771,83.459,63.807,83.323,63.884,83.198,63.924,83.109,63.926,83.015,63.902,82.985,63.875,83.018,63.844,83,63.81,82.931,63.775,82.898,63.766,82.902,63.785,82.862,63.784,82.778,63.763,82.686,63.763,82.585,63.784,82.523,63.784,82.502,63.763,82.459,63.767,82.397,63.797,82.38,63.82,82.409,63.835,82.349,63.866,82.2,63.912,82.077,63.967,81.98,64.03,81.885,64.114,81.791,64.22,81.754,64.288,81.773,64.317,81.75,64.35,81.683,64.387,81.669,64.41,81.706,64.42,81.713,64.437,81.687,64.462,81.439,64.543,81.388,64.571,81.441,64.581,81.488,64.606,81.528,64.646,81.537,64.674,81.516,64.691,81.522,64.699,81.553,64.699,81.535,64.732,81.47,64.796,81.468,64.846,81.53,64.881,81.528,64.909,81.463,64.929,81.43,64.979,81.43,65.057,81.528,65.112,81.724,65.144,81.89,65.192,82.025,65.258,82.101,65.309,82.116,65.346,82.088,65.372,82.017,65.388,81.99,65.42,82.005,65.47,82.042,65.488,82.099,65.475,82.12,65.478,82.104,65.497,82.123,65.524,82.176,65.56,82.399,65.651,82.422,65.703,82.337,65.756,82.303,65.815,82.32,65.88,82.279,65.941,82.179,65.999,82.126,66.04,82.119,66.064,82.148,66.109,82.214,66.176,82.212,66.213,82.142,66.219,82.096,66.245,82.073,66.293,82.121,66.336,82.241,66.373,82.272,66.406,82.214,66.433,82.216,66.466,82.277,66.506,82.294,66.538,82.268,66.562,82.262,66.602,82.276,66.656,82.156,66.756,81.9,66.9,81.527,67.011,81.037,67.087,80.725,67.12,80.592,67.11,80.535,67.091,80.552,67.063,80.49,67.073,80.348,67.118,80.252,67.135,80.176,67.117,79.635,67.213,79.451,67.259,79.444,67.286,79.409,67.296,79.347,67.29,79.131,67.36,78.59,67.578]]},{"n":"Tagus","z":4.7,"b":[-8.792,39.078,-7.118,39.682],"l":[[-7.118,39.682,-7.454,39.681,-7.653,39.634,-7.828,39.541,-7.982,39.492,-8.185,39.485,-8.376,39.468,-8.521,39.371,-8.661,39.249,-8.742,39.115,-8.792,39.078]]},{"n":"Tennessee","z":4.7,"b":[-88.472,34.403,-83.969,37.088],"l":[[-87.7,34.773,-87.814,34.763,-87.965,34.844,-88.084,34.933,-88.191,35.012,-88.306,35.126,-88.311,35.185,-88.282,35.239,-88.218,35.287,-88.188,35.329,-88.19,35.366,-88.139,35.391,-88.034,35.404,-87.994,35.418,-88.018,35.434,-88.015,35.464,-87.986,35.511,-87.987,35.579,-88.02,35.67,-88.01,35.767,-87.958,35.868,-87.943,35.925,-87.975,35.945],[-83.969,35.87,-84.229,35.767,-84.344,35.736,-84.4,35.743,-84.457,35.775,-84.542,35.863,-84.598,35.803,-84.63,35.789,-84.656,35.803,-84.701,35.749,-84.766,35.629,-84.82,35.566,-84.862,35.559,-84.89,35.528,-84.902,35.472,-84.924,35.45,-84.957,35.461,-85.013,35.391,-85.092,35.242,-85.182,35.132,-85.285,35.062,-85.347,35.05,-85.367,35.095,-85.392,35.096,-85.424,35.055,-85.46,35.05,-85.501,35.081,-85.559,35.08,-85.635,35.049,-85.68,35.01,-85.694,34.963,-85.839,34.803,-86.115,34.528,-86.294,34.403,-86.378,34.427,-86.462,34.478,-86.546,34.557,-86.666,34.591,-86.899,34.575],[-88.283,37.022,-88.472,37.088]]},{"n":"Teslin","z":3,"b":[-134.913,59.072,-131.208,61.57],"l":[[-131.208,59.087,-131.393,59.072,-131.513,59.084,-131.689,59.135,-131.792,59.141,-131.861,59.178,-131.93,59.248,-131.997,59.364,-132.095,59.607],[-133.245,60.449,-134.288,61.089,-134.647,61.352,-134.667,61.45,-134.737,61.517,-134.913,61.57]]},{"n":"Thames","z":5,"b":[-2.022,51.392,0.425,51.763],"l":[[-2.022,51.686,-1.939,51.648,-1.824,51.652,-1.562,51.7,-1.434,51.705,-1.369,51.725,-1.324,51.763,-1.282,51.759,-1.242,51.713,-1.239,51.682,-1.22,51.65,-1.17,51.628,-1.124,51.587,-1.11,51.525,-1.069,51.485,-0.999,51.465,-0.944,51.479,-0.903,51.527,-0.841,51.554,-0.76,51.56,-0.715,51.549,-0.706,51.521,-0.677,51.504,-0.604,51.494,-0.503,51.416,-0.433,51.392,-0.36,51.398,-0.32,51.419,-0.314,51.455,-0.245,51.48,-0.049,51.501,0.385,51.453,0.425,51.466]]},{"n":"Thelon","z":4.7,"b":[-105.942,62.356,-93.656,64.761],"l":[[-105.942,62.356,-105.664,62.41,-105.305,62.439,-104.772,62.461,-104.586,62.486,-104.544,62.5,-104.55,62.517,-104.598,62.551,-104.676,62.577,-104.734,62.589,-104.757,62.605,-104.735,62.622,-104.743,62.659,-104.763,62.682,-104.772,62.709,-104.744,62.729,-104.761,62.762,-104.819,62.808,-104.801,62.864,-104.769,62.906,-104.767,62.937,-104.817,62.948,-104.846,62.983,-104.834,63.071],[-104.706,63.27,-104.681,63.352,-104.612,63.44,-104.489,63.562,-104.452,63.63,-104.501,63.644,-104.468,63.691,-104.354,63.772,-104.214,63.836,-104.05,63.884,-103.901,63.974,-103.766,64.108,-103.696,64.193,-103.692,64.227,-103.532,64.269,-103.217,64.317,-102.966,64.328,-102.779,64.302,-102.676,64.263,-102.657,64.211,-102.626,64.178,-102.585,64.165,-102.409,64.184,-102.098,64.235,-101.91,64.298,-101.811,64.411],[-94.225,64.076,-93.766,63.994,-93.656,63.973],[-101.536,64.493,-100.795,64.604],[-97.103,64.761,-96.728,64.697,-96.58,64.655,-96.536,64.614,-96.504,64.554,-96.484,64.474,-96.423,64.401,-96.273,64.301]]},{"n":"Tigris","z":4,"b":[42.377,30.999,47.441,37.062],"l":[[42.377,37.062,42.434,36.99,42.466,36.967,42.489,36.96,42.509,36.941,42.513,36.876,42.559,36.832,42.713,36.785,42.759,36.755,42.774,36.733,42.761,36.719,42.858,36.708,42.895,36.691,42.901,36.662,42.886,36.646,42.85,36.644,42.811,36.609,42.771,36.54,42.844,36.478,43.031,36.425,43.169,36.334,43.258,36.205,43.317,36.076,43.346,35.946,43.343,35.858,43.308,35.812,43.297,35.765,43.312,35.718,43.299,35.682,43.261,35.657,43.248,35.586,43.262,35.47,43.348,35.316,43.505,35.126,43.568,35.006,43.538,34.957,43.584,34.832,43.708,34.63,43.785,34.453,43.815,34.3,43.854,34.194,43.904,34.135,43.96,34.098,44.055,34.075,44.232,34.05,44.329,34.019,44.403,33.974,44.427,33.909,44.402,33.825,44.398,33.769,44.417,33.739,44.4,33.697,44.348,33.641,44.318,33.586,44.31,33.531,44.324,33.451,44.361,33.347,44.397,33.293,44.431,33.291,44.48,33.235,44.544,33.127,44.675,33.024,44.871,32.925,44.981,32.887,45.027,32.891,45.041,32.879,45.041,32.856,45.044,32.832,45.08,32.803,45.14,32.785,45.225,32.778,45.26,32.752,45.259,32.714,45.298,32.663,45.384,32.604,45.522,32.556,45.809,32.502,46.03,32.619,46.137,32.632,46.201,32.588,46.344,32.542,46.563,32.494,46.692,32.443,46.73,32.386,46.735,32.321,46.706,32.248,46.699,32.191,46.713,32.149,46.755,32.112,46.827,32.079,46.888,32.014,46.94,31.917,47.008,31.868,47.093,31.865,47.139,31.826,47.146,31.749,47.221,31.627,47.363,31.459,47.434,31.286,47.441,30.999]]},{"n":"Tisa","z":5,"b":[20.061,45.126,20.308,46.261],"l":[[20.179,46.261,20.087,46.166,20.066,46.138,20.064,46.117,20.079,46.076,20.074,46.049,20.061,46.029,20.08,46.001,20.104,45.969,20.124,45.922,20.122,45.89,20.098,45.873,20.105,45.834,20.14,45.778,20.136,45.742,20.11,45.724,20.085,45.689,20.07,45.632,20.105,45.563,20.188,45.483,20.223,45.431,20.209,45.407,20.231,45.349,20.289,45.256,20.308,45.192,20.282,45.126]]},{"n":"Tisza","z":5,"b":[20.066,46.261,24.338,48.415],"l":[[21.346,48.026,21.182,48.006,21.103,47.944,21.057,47.835,20.871,47.69,20.546,47.51,20.4,47.382,20.434,47.306,20.426,47.27,20.377,47.272,20.306,47.242,20.214,47.178,20.175,47.119,20.189,47.066,20.218,47.038,20.192,47.01,20.155,46.98,20.144,46.939,20.125,46.912,20.066,46.874,20.08,46.811,20.166,46.724,20.192,46.653,20.157,46.598,20.163,46.5,20.207,46.36,20.217,46.283,20.179,46.261],[24.256,48.322,24.338,48.294,24.333,48.234,24.234,48.083,24.17,47.955,24.002,47.936,23.709,47.983,23.477,48.049,23.307,48.136,23.146,48.155,22.996,48.106,22.885,48.091,22.815,48.111,22.702,48.107,22.545,48.078,22.42,48.095,22.325,48.159,22.276,48.244,22.273,48.35,22.254,48.407,22.219,48.415,22.133,48.374,21.997,48.284,21.869,48.222,21.75,48.186,21.637,48.17,21.53,48.175,21.465,48.146,21.443,48.082,21.409,48.045,21.339,48.026]]},{"n":"Tobol","z":4.7,"b":[60.91,51.442,68.218,58.168],"l":[[60.91,51.669,61.015,51.492,61.363,51.442,61.516,51.454,61.589,51.513,61.606,51.636,61.565,51.822,61.555,51.949,61.575,52.016,61.56,52.086,61.509,52.158,61.547,52.253,61.674,52.37,61.735,52.442,61.777,52.466,61.835,52.483,62.047,52.487,62.298,52.529,62.589,52.609,62.767,52.696,62.834,52.79,63.024,52.891,63.337,52.999,63.587,53.146,63.864,53.423,63.876,53.567,63.846,53.671,63.778,53.782,63.752,53.887,63.767,53.984,63.798,54.041,63.844,54.056,63.858,54.079,63.859,54.118,63.877,54.153,63.964,54.203,64.007,54.252,64.063,54.303,64.199,54.347,64.59,54.402,64.809,54.48,64.858,54.582,64.916,54.652,64.983,54.689,65.001,54.718,64.971,54.739,64.974,54.773,65.009,54.821,65.008,54.863,64.972,54.899,64.958,54.936,64.966,54.973,65.044,55.064,65.193,55.21,65.255,55.312,65.23,55.37,65.264,55.411,65.357,55.437,65.448,55.501,65.58,55.654,65.611,55.788,65.641,55.841,65.682,55.86,65.695,55.899,65.68,55.96,65.758,56.027,65.928,56.102,65.973,56.152,65.965,56.191,65.997,56.251,66.185,56.308,66.256,56.362,66.26,56.384,66.278,56.425,66.337,56.481,66.349,56.531,66.311,56.574,66.329,56.632,66.402,56.704,66.436,56.766,66.433,56.818,66.465,56.851,66.533,56.862,66.562,56.884,66.552,56.917,66.577,56.961,66.637,57.018,66.731,57.071,66.934,57.158,66.96,57.186,66.963,57.211,66.944,57.235,66.953,57.258,66.993,57.28,67.004,57.297,67.043,57.32,67.076,57.339,67.1,57.375,67.08,57.432,67.097,57.464,67.149,57.471,67.16,57.5,67.129,57.55,67.091,57.58,67.099,57.612,67.115,57.635,67.148,57.678,67.153,57.735,67.208,57.786,67.313,57.831,67.534,57.864,67.871,57.886,68.053,57.91,68.082,57.938,68.083,57.97,68.058,58.006,68.089,58.06,68.218,58.168]]},{"n":"Tocantins","z":4,"b":[-49.717,-15.926,-47.397,-2.652],"l":[[-49.179,-15.926,-49.258,-15.768,-49.35,-15.725,-49.437,-15.634,-49.55,-15.479,-49.592,-15.348,-49.564,-15.242,-49.514,-15.165,-49.444,-15.118,-49.356,-15.011,-49.252,-14.845,-49.155,-14.746,-49.064,-14.715,-49.009,-14.629,-48.992,-14.486,-48.994,-14.387,-49.014,-14.33,-49.008,-14.282,-48.973,-14.243,-48.796,-14.125,-48.713,-14.111,-48.653,-14.148,-48.598,-14.154,-48.546,-14.129,-48.384,-13.996,-48.342,-13.922,-48.348,-13.855,-48.325,-13.828,-48.273,-13.84,-48.216,-13.82,-48.155,-13.767,-48.116,-13.691,-48.1,-13.591,-48.123,-13.516,-48.185,-13.467,-48.198,-13.329,-48.164,-13.103,-48.181,-12.897,-48.284,-12.619,-48.238,-12.512,-48.235,-12.466,-48.259,-12.445,-48.266,-12.413,-48.256,-12.37,-48.307,-12.309,-48.42,-12.23,-48.524,-12.029,-48.646,-11.636,-48.648,-11.536,-48.598,-11.467,-48.516,-11.384,-48.489,-11.257,-48.541,-11.11,-48.526,-10.979,-48.443,-10.863,-48.417,-10.732,-48.448,-10.586,-48.454,-10.43,-48.437,-10.262,-48.405,-10.14,-48.357,-10.065,-48.345,-10.001,-48.367,-9.951,-48.365,-9.908,-48.339,-9.874,-48.346,-9.799,-48.387,-9.685,-48.398,-9.553,-48.379,-9.402,-48.307,-9.19,-48.182,-8.917,-48.127,-8.722,-48.141,-8.607,-48.134,-8.522,-48.106,-8.467,-48.113,-8.369,-48.154,-8.229,-48.162,-8.142,-48.137,-8.108,-48.087,-8.085,-48.011,-8.072,-47.963,-8.003,-47.941,-7.877,-47.862,-7.779,-47.724,-7.709,-47.647,-7.627,-47.63,-7.532,-47.589,-7.459,-47.522,-7.407,-47.495,-7.356,-47.507,-7.306,-47.546,-7.288,-47.612,-7.302,-47.669,-7.28,-47.717,-7.222,-47.716,-7.185,-47.665,-7.167,-47.606,-7.115,-47.537,-7.027,-47.47,-6.796,-47.406,-6.421,-47.397,-6.072,-47.445,-5.75,-47.486,-5.561,-47.52,-5.506,-47.612,-5.455,-47.763,-5.407,-47.854,-5.351,-47.885,-5.286,-47.953,-5.256,-48.059,-5.263,-48.169,-5.241,-48.282,-5.189,-48.378,-5.17,-48.458,-5.181,-48.524,-5.22,-48.574,-5.288,-48.631,-5.33,-48.723,-5.355],[-49.687,-3.835,-49.649,-3.361,-49.66,-3.091,-49.708,-2.867,-49.717,-2.736,-49.67,-2.676,-49.635,-2.652]]},{"n":"Tom’","z":5,"b":[84.436,53.435,89.771,56.911],"l":[[89.771,53.646,89.395,53.435,89.176,53.441,89.08,53.483,89.033,53.564,88.975,53.621,88.907,53.654,88.744,53.677,88.487,53.689,88.316,53.684,88.232,53.661,88.115,53.678,87.967,53.733,87.788,53.777,87.579,53.808,87.401,53.81,87.254,53.781,87.179,53.798,87.175,53.861,87.249,53.937,87.402,54.028,87.491,54.105,87.517,54.171,87.512,54.232,87.477,54.288,87.484,54.344,87.536,54.399,87.54,54.475,87.497,54.571,87.4,54.635,87.173,54.681,87.087,54.716,87.069,54.75,87.051,54.83,87.057,54.856,87.103,54.896,87.121,54.945,87.112,55.002,87.04,55.026,86.842,55.067,86.801,55.103,86.715,55.14,86.594,55.143,86.436,55.109,86.371,55.164,86.338,55.236,86.262,55.332,86.166,55.385,86.048,55.395,85.967,55.434,85.924,55.503,85.833,55.571,85.696,55.639,85.537,55.67,85.358,55.663,85.194,55.68,85.045,55.719,84.967,55.766,84.959,55.822,84.977,55.866,84.989,55.917,84.984,55.953,84.958,55.995,84.947,56.049,84.95,56.141,84.916,56.199,84.906,56.235,84.911,56.295,84.898,56.343,84.9,56.378,84.943,56.402,84.947,56.448,84.912,56.516,84.856,56.567,84.782,56.6,84.717,56.663,84.662,56.755,84.597,56.807,84.523,56.819,84.474,56.846,84.436,56.911]]},{"n":"Tongtian","z":2,"b":[92.993,31.69,98.542,34.741],"l":[[92.993,34.1,93.095,34.195,93.262,34.222,93.529,34.215,93.707,34.239,93.838,34.321,94.01,34.501,94.117,34.567,94.215,34.578,94.338,34.629,94.485,34.719,94.661,34.741,94.865,34.694,95.018,34.624,95.119,34.532,95.212,34.486,95.296,34.486,95.398,34.442,95.518,34.355,95.575,34.293,95.57,34.257,95.596,34.229,95.652,34.21,95.757,34.111,95.91,33.933,96.023,33.824,96.095,33.784,96.153,33.77,96.196,33.781,96.222,33.769,96.231,33.735,96.276,33.725,96.359,33.739,96.418,33.715,96.454,33.654,96.529,33.604,96.644,33.566,96.702,33.516,96.704,33.454,96.723,33.406,96.757,33.373,96.781,33.364,96.794,33.381,96.808,33.371,96.822,33.333,96.873,33.32,96.962,33.334,97.011,33.31,97.02,33.246,97.085,33.144,97.207,33.002,97.291,32.923,97.337,32.906,97.378,32.848,97.413,32.749,97.624,32.614,98.012,32.442,98.209,32.33,98.216,32.278,98.278,32.185,98.395,32.051,98.446,31.941,98.432,31.856,98.454,31.782,98.542,31.69]]},{"n":"Trofimovskaya Protoka","z":4.7,"b":[126.444,72.353,128.258,73.267],"l":[[126.475,72.353,126.444,72.508,126.519,72.594,126.712,72.685,126.811,72.762,126.814,72.827,126.901,72.898,127.072,72.975,127.318,73.032,127.641,73.069,127.911,73.132,128.258,73.267]]},{"n":"Tshuapa","z":5,"b":[20.754,-3.177,24.617,-0.191],"l":[[24.594,-3.177,24.617,-2.995,24.583,-2.902,24.499,-2.839,24.44,-2.769,24.388,-2.656,24.369,-2.477,24.271,-2.309,24.088,-2.092,23.971,-1.828,23.92,-1.519,23.848,-1.402,23.769,-1.361,23.708,-1.331,23.669,-1.273,23.488,-1.211,23.166,-1.146,22.96,-1.064,22.871,-0.965,22.771,-0.905,22.661,-0.885,22.557,-0.851,22.458,-0.802,22.397,-0.754,22.375,-0.706,22.273,-0.651,22.001,-0.559,21.785,-0.486,21.683,-0.438,21.624,-0.389,21.575,-0.371,21.535,-0.384,21.475,-0.367,21.393,-0.317,21.303,-0.295,21.204,-0.3,21.127,-0.285,21.072,-0.249,21.029,-0.261,21.009,-0.249,20.978,-0.205,20.936,-0.191,20.877,-0.197,20.821,-0.222,20.796,-0.269,20.754,-0.282]]},{"n":"Tugaloo","z":5,"b":[-83.356,34.6,-83.167,34.708],"l":[[-83.356,34.708,-83.167,34.6]]},{"n":"Tumatskaya Protoka","z":4.7,"b":[126.298,72.353,126.698,73.335],"l":[[126.475,72.353,126.375,72.435,126.309,72.486,126.298,72.522,126.298,72.585,126.31,72.675,126.333,72.735,126.367,72.766,126.372,72.819,126.349,72.895,126.416,72.954,126.575,72.995,126.669,73.036,126.698,73.078,126.676,73.132,126.565,73.233,126.553,73.335]]},{"n":"Tuotuo","z":2,"b":[90.795,34.091,92.993,34.321],"l":[[90.795,34.3,91.152,34.321,91.647,34.281,91.853,34.235,91.936,34.169,92.037,34.165,92.157,34.222,92.309,34.24,92.493,34.22,92.62,34.18,92.688,34.118,92.79,34.091,92.993,34.1]]},{"n":"Ubangi","z":3,"b":[17.724,-0.549,18.622,3.679],"l":[[18.597,3.679,18.622,3.304,18.491,2.924,18.212,2.415,18.072,2.013,18.073,1.719,18.058,1.535,18.012,1.422,17.902,1.118,17.886,0.857,17.925,0.537,17.888,0.234,17.773,-0.052,17.724,-0.278,17.753,-0.549]]},{"n":"Ucayali","z":2,"b":[-75.28,-15.336,-71.457,-4.445],"l":[[-71.669,-15.336,-71.658,-15.217,-71.637,-15.173,-71.604,-15.164,-71.597,-15.115,-71.617,-15.027,-71.596,-14.936,-71.501,-14.792,-71.457,-14.693,-71.457,-14.568,-71.489,-14.384,-71.519,-14.264,-71.549,-14.207,-71.6,-14.163,-71.672,-14.13,-71.738,-14.04,-71.799,-13.892,-71.914,-13.81,-72.081,-13.793,-72.232,-13.738,-72.364,-13.644,-72.549,-13.547,-72.786,-13.446,-72.952,-13.411,-73.049,-13.442,-73.128,-13.449,-73.221,-13.422,-73.478,-13.175,-73.562,-13.061,-73.558,-12.997,-73.584,-12.897,-73.64,-12.76,-73.716,-12.645,-73.811,-12.551,-73.916,-12.392,-74.031,-12.168,-74.055,-12.007,-73.989,-11.909,-74.031,-11.718,-74.183,-11.433,-74.258,-11.256,-74.257,-11.188,-74.204,-11.154,-74.101,-11.155,-73.983,-11.188,-73.851,-11.252,-73.763,-11.28,-73.719,-11.272,-73.719,-11.195,-73.763,-11.048,-73.774,-10.912,-73.753,-10.787,-73.804,-10.684,-73.927,-10.603,-74.003,-10.509,-74.03,-10.403,-74.033,-10.31,-74.013,-10.233,-74.017,-10.205,-74.038,-10.188,-74.051,-10.173,-74.041,-10.128,-74.017,-10.098,-74.004,-10.074,-74.01,-10.059,-74.029,-10.051,-74.058,-10.015,-74.071,-9.961,-74.057,-9.933,-74.015,-9.933,-73.984,-9.908,-73.963,-9.859,-74.001,-9.833,-74.098,-9.829,-74.151,-9.767,-74.16,-9.648,-74.18,-9.559,-74.212,-9.501,-74.247,-9.487,-74.268,-9.489,-74.281,-9.475,-74.291,-9.453,-74.315,-9.415,-74.379,-9.367,-74.405,-9.293,-74.392,-9.192,-74.412,-9.139,-74.447,-9.115,-74.459,-9.089,-74.454,-9.062,-74.458,-9.041,-74.475,-9.029,-74.539,-8.849,-74.508,-8.732,-74.405,-8.62,-74.331,-8.57,-74.286,-8.583,-74.266,-8.553,-74.269,-8.479,-74.298,-8.457,-74.352,-8.486,-74.4,-8.486,-74.441,-8.455,-74.473,-8.447,-74.496,-8.446,-74.506,-8.419,-74.507,-8.321,-74.532,-8.26,-74.582,-8.237,-74.599,-8.207,-74.584,-8.17,-74.594,-8.153,-74.628,-8.157,-74.639,-8.131,-74.625,-8.076,-74.634,-8.03,-74.665,-7.992,-74.706,-7.977,-74.724,-7.975,-74.743,-7.962,-74.728,-7.943,-74.737,-7.917,-74.816,-7.883,-74.86,-7.885,-74.869,-7.924,-74.899,-7.922,-74.95,-7.879,-74.965,-7.84,-74.946,-7.806,-74.962,-7.751,-75.013,-7.676,-75.013,-7.652,-74.99,-7.647,-74.979,-7.623,-75.015,-7.558,-75.016,-7.497,-74.983,-7.438,-75.019,-7.365,-75.123,-7.279,-75.196,-7.24,-75.238,-7.25,-75.255,-7.237,-75.247,-7.203,-75.269,-7.183,-75.28,-7.159,-75.265,-7.129,-75.151,-7.05,-75.136,-7.012,-75.142,-6.988,-75.166,-6.96,-75.15,-6.902,-75.091,-6.827,-75.084,-6.748,-75.13,-6.665,-75.128,-6.61,-75.106,-6.587,-75.125,-6.536,-75.163,-6.496,-75.178,-6.454,-75.152,-6.419,-75.151,-6.345,-75.187,-6.236,-75.18,-6.157,-75.129,-6.106,-75.1,-6.106,-75.081,-6.112,-75.062,-6.101,-75.052,-6.076,-75.018,-6.063,-74.962,-6.075,-74.888,-6.051,-74.8,-5.98,-74.754,-5.985,-74.722,-5.987,-74.697,-5.975,-74.694,-5.942,-74.647,-5.863,-74.555,-5.758,-74.484,-5.727,-74.434,-5.769,-74.4,-5.774,-74.381,-5.742,-74.358,-5.738,-74.335,-5.749,-74.307,-5.739,-74.285,-5.669,-74.244,-5.626,-74.192,-5.603,-74.177,-5.579,-74.186,-5.556,-74.183,-5.533,-74.165,-5.513,-74.167,-5.484,-74.175,-5.47,-74.173,-5.455,-74.159,-5.435,-74.156,-5.413,-74.162,-5.394,-74.148,-5.373,-74.107,-5.345,-74.089,-5.305,-74.099,-5.216,-74.086,-5.125,-74.049,-5.032,-74.027,-5.021,-74,-5.022,-73.963,-5.063,-73.9,-5.058,-73.818,-4.994,-73.774,-4.938,-73.765,-4.895,-73.74,-4.882,-73.714,-4.887,-73.704,-4.863,-73.724,-4.827,-73.702,-4.771,-73.635,-4.711,-73.594,-4.701,-73.563,-4.696,-73.556,-4.676,-73.56,-4.644,-73.547,-4.606,-73.49,-4.553,-73.489,-4.521,-73.506,-4.51,-73.512,-4.489,-73.489,-4.445]]},{"n":"Uele","z":3,"b":[18.567,3.402,28.586,5.127],"l":[[28.586,3.596,28.43,3.647,28.348,3.65,28.29,3.622,28.239,3.637,28.194,3.695,28.141,3.729,28.08,3.74,27.978,3.72,27.835,3.67,27.696,3.644,27.56,3.643,27.431,3.696,27.309,3.803,27.228,3.851,27.185,3.84,27.17,3.789,27.18,3.698,27.153,3.617,27.089,3.544,27.04,3.517,27.007,3.536,26.954,3.535,26.879,3.515,26.833,3.584,26.816,3.741,26.785,3.864,26.74,3.952,26.697,4.002,26.656,4.012,26.561,3.954,26.335,3.765,26.235,3.68,26.157,3.669,26.068,3.703,25.988,3.715,25.916,3.705,25.859,3.657,25.816,3.569,25.762,3.543,25.696,3.577,25.619,3.576,25.53,3.539,25.428,3.524,25.313,3.531,25.226,3.506,25.167,3.45,25.102,3.415,25.031,3.402,24.979,3.417,24.949,3.462,24.839,3.482,24.649,3.477,24.461,3.533,24.276,3.651,24.081,3.709,23.877,3.708,23.733,3.745,23.651,3.818,23.545,3.845,23.415,3.824,23.3,3.836,23.199,3.88,23.049,3.885,22.85,3.851,22.668,3.909,22.422,4.135,21.908,4.254,21.687,4.281,21.538,4.245,21.35,4.311,21.268,4.323,21.23,4.302,21.126,4.332,20.956,4.413,20.793,4.447,20.647,4.436,20.558,4.463,20.486,4.542,20.394,4.686,20.226,4.83,20.002,4.945,19.862,5.031,19.807,5.089,19.686,5.121,19.501,5.127,19.323,5.071,19.069,4.891,18.832,4.523,18.7,4.383,18.594,4.346,18.567,4.258,18.62,4.117,18.634,3.954,18.597,3.679]]},{"n":"Ural","z":4,"b":[51.11,46.944,59.426,54.52],"l":[[58.631,51.695,58.628,51.619,58.659,51.561,58.723,51.496,58.751,51.425,58.744,51.347,58.713,51.287,58.657,51.247,58.527,51.201,58.325,51.149,58.209,51.106,58.181,51.072,58.045,51.069,57.801,51.096,57.536,51.165,57.248,51.275,56.906,51.353,56.51,51.399,56.224,51.472,56.049,51.573,55.877,51.648,55.709,51.696,55.456,51.735,55.119,51.766,54.65,51.726,54.047,51.613,53.667,51.56,53.511,51.566,53.406,51.551,53.35,51.514,53.227,51.485,53.038,51.464,52.903,51.467,52.82,51.495,52.728,51.498,52.626,51.478,52.554,51.442,52.512,51.391,52.428,51.383,52.301,51.418,52.225,51.421,52.199,51.392,52.128,51.382,52.012,51.391,51.941,51.378,51.916,51.343,51.769,51.286,51.499,51.205,51.395,51.148,51.364,51.075,51.316,50.998,51.221,50.904,51.183,50.826,51.201,50.765,51.189,50.708,51.148,50.657,51.136,50.595,51.154,50.523,51.15,50.478,51.121,50.46,51.116,50.436,51.133,50.406,51.131,50.365,51.11,50.312,51.156,50.253,51.269,50.187,51.329,50.122,51.341,50.027,51.32,49.955,51.347,49.913,51.416,49.875,51.471,49.802,51.512,49.692,51.558,49.631,51.594,49.614,51.608,49.585,51.636,49.546,51.707,49.488,51.739,49.449,51.744,49.42,51.76,49.404,51.828,49.374,51.848,49.352,51.847,49.336,51.859,49.323,51.884,49.314,51.916,49.275,51.948,49.206,51.936,49.138,51.881,49.071,51.85,49.001,51.841,48.927,51.854,48.895,51.88,48.869,51.876,48.815,51.804,48.623,51.776,48.586,51.73,48.56,51.667,48.547,51.625,48.515,51.606,48.469,51.603,48.443,51.61,48.424,51.635,48.405,51.633,48.386,51.602,48.37,51.589,48.318,51.592,48.229,51.584,48.181,51.569,48.167,51.582,48.119,51.64,48.016,51.654,47.882,51.623,47.716,51.674,47.579,51.806,47.47,51.879,47.397,51.895,47.36,51.892,47.331,51.872,47.31,51.877,47.293,51.908,47.28,51.927,47.25,51.934,47.204,51.884,47.121,51.724,46.944],[59.426,54.52,59.307,54.403,59.194,54.325,59.162,54.278,59.172,54.236,59.15,54.175,59.095,54.093,59.102,54.012,59.173,53.932,59.19,53.863,59.156,53.805,59.169,53.746,59.229,53.686,59.254,53.64,59.243,53.61,59.182,53.573,59.072,53.531,59.021,53.464,59.029,53.374,59.067,53.307,59.136,53.265,59.13,53.217,59.049,53.165,58.974,53.071,58.905,52.936,58.904,52.811,58.971,52.697,58.996,52.62,58.986,52.6,58.99,52.569,59.01,52.546,59.058,52.527,59.083,52.513,59.08,52.47,59.062,52.42,58.834,52.196]]},{"n":"Uruguay","z":5,"b":[-58.201,-32.472,-49.737,-27.081],"l":[[-49.737,-28.511,-49.839,-28.465,-49.938,-28.448,-50.069,-28.444,-50.141,-28.453,-50.155,-28.474,-50.233,-28.469,-50.446,-28.422,-50.578,-28.386,-50.736,-28.236,-50.966,-27.959,-51.117,-27.808,-51.19,-27.781,-51.287,-27.716,-51.406,-27.613,-51.488,-27.564,-51.53,-27.569,-51.582,-27.548,-51.645,-27.5,-51.702,-27.488,-51.754,-27.512,-51.8,-27.512,-51.873,-27.509,-51.913,-27.473,-51.984,-27.377,-52.06,-27.314,-52.139,-27.283,-52.192,-27.281,-52.229,-27.293,-52.265,-27.277,-52.304,-27.283,-52.348,-27.276,-52.415,-27.236,-52.511,-27.231,-52.636,-27.259,-52.742,-27.246,-52.826,-27.19,-52.9,-27.177,-52.953,-27.179,-52.994,-27.163,-53.031,-27.137,-53.071,-27.142,-53.104,-27.154,-53.134,-27.14,-53.184,-27.15,-53.227,-27.174,-53.263,-27.163,-53.285,-27.129,-53.305,-27.098,-53.348,-27.081,-53.398,-27.082,-53.457,-27.103,-53.49,-27.133,-53.498,-27.172,-53.54,-27.198,-53.616,-27.21,-53.66,-27.2,-53.672,-27.169,-53.718,-27.158,-53.8,-27.165,-53.852,-27.156,-53.935,-27.161,-54.04,-27.244,-54.114,-27.275,-54.156,-27.254,-54.205,-27.29,-54.26,-27.382,-54.327,-27.423,-54.448,-27.446,-54.484,-27.457,-54.555,-27.454,-54.615,-27.477,-54.666,-27.527,-54.72,-27.545,-54.777,-27.533,-54.829,-27.551,-54.876,-27.599,-54.903,-27.652,-54.91,-27.709,-54.956,-27.747,-55.04,-27.768,-55.069,-27.796,-55.064,-27.836,-55.102,-27.867,-55.244,-27.899,-55.347,-27.956,-55.41,-28.038,-55.477,-28.089,-55.582,-28.121,-55.726,-28.204,-55.746,-28.255,-55.692,-28.303,-55.672,-28.345,-55.687,-28.382,-55.732,-28.387,-55.806,-28.36,-55.859,-28.354,-55.891,-28.37,-55.905,-28.4,-55.904,-28.443,-55.93,-28.473,-55.985,-28.489,-56.02,-28.525,-56.034,-28.581,-56.103,-28.652,-56.226,-28.737,-56.322,-28.852,-56.393,-28.997,-56.476,-29.092,-56.571,-29.138,-56.636,-29.203,-56.672,-29.287,-56.772,-29.418,-56.939,-29.595,-57.089,-29.716,-57.225,-29.782,-57.301,-29.857,-57.318,-29.939,-57.405,-30.034,-57.564,-30.14,-57.646,-30.227,-57.651,-30.295,-57.713,-30.384,-57.831,-30.495,-57.873,-30.591,-57.819,-30.712,-57.811,-30.859,-57.834,-30.917,-57.886,-30.937,-57.898,-30.975,-57.87,-31.031,-57.868,-31.104,-57.893,-31.195,-57.948,-31.299,-58.033,-31.417,-58.054,-31.495,-58.01,-31.534,-57.988,-31.576,-57.989,-31.621,-58.007,-31.685,-58.042,-31.769,-58.096,-31.832,-58.168,-31.873,-58.189,-31.924,-58.16,-31.987,-58.156,-32.052,-58.177,-32.119,-58.165,-32.185,-58.12,-32.249,-58.123,-32.322,-58.201,-32.472]]},{"n":"Usumacinta","z":4.7,"b":[-92.71,16.391,-90.417,18.665],"l":[[-90.417,16.391,-90.471,16.44,-90.576,16.468,-90.634,16.511,-90.634,16.565,-90.66,16.631,-90.711,16.708,-90.816,16.787,-90.976,16.868,-91.112,16.976,-91.224,17.112,-91.319,17.2,-91.392,17.236,-91.417,17.264,-91.417,17.287,-91.4,17.314,-91.414,17.35,-91.466,17.384,-91.496,17.42,-91.504,17.458,-91.488,17.481,-91.449,17.488,-91.45,17.515,-91.49,17.563,-91.473,17.589,-91.398,17.594,-91.366,17.627,-91.378,17.686,-91.4,17.711,-91.433,17.701,-91.446,17.715,-91.44,17.751,-91.456,17.779,-91.494,17.798,-91.524,17.794,-91.546,17.766,-91.567,17.773,-91.587,17.813,-91.613,17.818,-91.645,17.787,-91.652,17.764,-91.64,17.75,-91.641,17.736,-91.69,17.732,-91.732,17.762,-91.79,17.844,-91.85,17.878,-91.912,17.862,-91.993,17.887,-92.093,17.954,-92.137,18.013,-92.127,18.062,-92.144,18.122,-92.189,18.191,-92.212,18.204],[-92.212,18.204,-92.296,18.255,-92.464,18.316,-92.572,18.335,-92.621,18.312,-92.652,18.333,-92.67,18.431,-92.675,18.481,-92.679,18.543,-92.688,18.578,-92.71,18.612],[-92.212,18.204,-92.263,18.361,-92.31,18.426,-92.371,18.467,-92.422,18.531,-92.485,18.665]]},{"n":"Vaal","z":4,"b":[23.628,-29.073,30.21,-26.383],"l":[[25.622,-27.668,25.366,-27.758,25.223,-27.855,25.11,-27.991,24.99,-28.07,24.864,-28.093,24.774,-28.149,24.719,-28.238,24.694,-28.336,24.699,-28.443,24.676,-28.496,24.631,-28.503,24.605,-28.53,24.601,-28.562,24.582,-28.581,24.555,-28.564,24.516,-28.554,24.468,-28.572,24.424,-28.556,24.402,-28.509,24.359,-28.463,24.297,-28.432,24.216,-28.473,24.115,-28.587,24.04,-28.723,23.992,-28.882,23.883,-28.989,23.628,-29.073],[28.124,-26.867,28,-26.717,27.909,-26.694,27.81,-26.75,27.716,-26.768,27.627,-26.748,27.557,-26.772,27.506,-26.84,27.437,-26.871,27.351,-26.865,27.276,-26.887,27.212,-26.936,27.118,-26.937,26.994,-26.889,26.943,-26.89,26.921,-26.933,26.887,-26.948,26.825,-26.935,26.698,-26.99,26.507,-27.115,26.45,-27.177,26.45,-27.217,26.467,-27.235,26.477,-27.279,26.459,-27.328,26.466,-27.362,26.432,-27.398,26.333,-27.407,26.228,-27.46,26.091,-27.603],[30.135,-26.383,30.21,-26.479,30.188,-26.56,30.049,-26.709,29.839,-26.811,29.697,-26.845,29.552,-26.846,29.473,-26.867,29.461,-26.909,29.415,-26.924,29.336,-26.911,29.269,-26.933,29.215,-26.99,29.156,-27.025,29.104,-27.042,29.062,-27.044,28.992,-27.007,28.933,-27.006,28.859,-27.028,28.78,-27.018,28.694,-26.977,28.665,-26.976,28.656,-26.993,28.627,-27.019,28.565,-27.045,28.531,-27.043,28.524,-27.014,28.496,-26.99]]},{"n":"Verde","z":5,"b":[-100.344,21.664,-98.59,22.117],"l":[[-100.344,22.117,-100.171,22.027,-99.993,21.945,-99.902,21.864,-99.84,21.756,-99.782,21.703,-99.73,21.705,-99.69,21.692,-99.665,21.664,-99.621,21.673,-99.559,21.719,-99.46,21.719,-99.324,21.672,-99.191,21.699,-99.06,21.799,-98.952,21.851,-98.868,21.855,-98.807,21.89,-98.769,21.957,-98.728,21.988,-98.662,21.983,-98.59,21.981]]},{"n":"Verkhniy Yenisey","z":4,"b":[91.367,51.566,94.465,58.1],"l":[[94.465,51.734,94.07,51.604,93.646,51.566,92.769,51.581,92.35,51.701,92.152,51.796,92.035,51.905,92.068,52.012,92.251,52.117,92.377,52.214,92.444,52.303,92.438,52.378,92.358,52.437,92.314,52.5,92.307,52.565,92.172,52.597,91.91,52.594,91.769,52.616,91.748,52.662,91.672,52.677,91.541,52.662,91.439,52.674,91.367,52.714,91.378,52.797,91.472,52.924,91.501,53.012,91.465,53.06,91.454,53.102,91.468,53.136,91.538,53.16,91.664,53.173,91.788,53.23,91.909,53.329,91.955,53.398,91.924,53.437,91.847,53.479,91.725,53.522,91.634,53.57,91.575,53.621,91.559,53.66,91.586,53.687,91.578,53.709,91.511,53.735],[92.998,58.1,92.989,58.049,93.028,58.029,93.113,58.024,93.176,57.98,93.22,57.897,93.305,57.637,93.305,57.516,93.262,57.419,93.293,57.332,93.398,57.255,93.453,57.183,93.457,57.114,93.438,57.063,93.396,57.028,93.383,56.993,93.457,56.861,93.509,56.823,93.58,56.802,93.657,56.726,93.741,56.596,93.735,56.48,93.64,56.38,93.55,56.317,93.465,56.292,93.418,56.262,93.408,56.227,93.345,56.195,93.228,56.167,93.162,56.131,93.148,56.085,92.935,56.035,92.523,55.979,92.3,55.923,92.266,55.865,92.301,55.813,92.406,55.768,92.47,55.716,92.493,55.658,92.48,55.598,92.408,55.503]]},{"n":"Victoria Nile","z":2,"b":[31.366,0.428,33.187,2.333],"l":[[32.893,1.311,33.011,0.809,33.085,0.589,33.187,0.428],[32.475,1.552,32.545,1.522],[31.366,2.193,31.387,2.2,31.751,2.324,31.948,2.333,32.087,2.265,32.187,2.236,32.247,2.245,32.295,2.199,32.333,2.1,32.349,2.005,32.344,1.914,32.282,1.83,32.164,1.754,32.104,1.699,32.104,1.665,32.138,1.652,32.241,1.666]]},{"n":"Vilyuy","z":5,"b":[103.516,61.939,126.423,66.008],"l":[[126.423,64.403,126.421,64.285,126.31,64.18,126.094,64.057,125.808,63.964,125.454,63.901,125.115,63.894,124.793,63.944,124.621,63.986,124.599,64.021,124.345,64.051,123.616,64.091,123.225,63.944,123.035,63.907,122.917,63.931,122.607,63.925,122.106,63.891,121.795,63.852,121.675,63.808,121.503,63.789,121.28,63.795,121.141,63.777,121.085,63.736,121.023,63.726,120.955,63.747,120.869,63.729,120.766,63.674,120.658,63.648,120.545,63.652,120.482,63.611,120.469,63.524,120.329,63.509,120.063,63.564,119.674,63.566,119.163,63.516,118.891,63.481,118.858,63.461,118.859,63.43,118.894,63.388,118.833,63.349,118.678,63.314,118.408,63.285,118.024,63.264,117.769,63.221,117.642,63.156,117.544,63.049,117.476,62.902,117.457,62.813,117.487,62.784,117.481,62.754,117.439,62.722,117.419,62.661,117.421,62.573,117.456,62.516,117.524,62.491,117.549,62.445,117.53,62.379,117.573,62.306,117.679,62.228,117.709,62.161,117.665,62.106,117.619,62.088,117.57,62.106,117.522,62.09,117.473,62.039,117.38,61.992,117.244,61.95,117.044,61.939,116.782,61.961,116.55,62,116.35,62.056,116.216,62.104,116.148,62.143,116.126,62.19,116.151,62.245,116.054,62.301,115.835,62.358,115.764,62.421,115.735,62.467,115.735,62.505,115.763,62.559,115.717,62.584,115.596,62.579,115.494,62.592,115.41,62.621,115.352,62.656,115.32,62.698,115.323,62.738,115.362,62.776,115.338,62.802,115.251,62.815,115.229,62.851,115.273,62.907,115.281,62.952,115.255,62.984,115.198,63.015,115.11,63.045,115.029,63.043,114.955,63.009,114.865,63.001,114.759,63.02,114.683,63.046,114.638,63.079,114.631,63.114,114.664,63.149,114.671,63.173,114.654,63.185,114.344,63.145,114.207,63.148,114.133,63.186,114.059,63.195,113.984,63.175,113.937,63.179,113.917,63.206,113.855,63.228,113.752,63.246,113.638,63.215,113.515,63.136,113.497,63.097,113.455,63.061,113.378,63.057,113.355,63.04,113.363,62.996,113.375,62.96,113.389,62.934,113.432,62.905,113.431,62.873,113.385,62.839,113.339,62.829,113.292,62.842,113.254,62.88,113.225,62.941,113.157,62.978,113.051,62.99,112.992,63.013,112.98,63.047,112.926,63.056,112.83,63.039,112.783,63.009,112.766,62.981,112.709,62.96,112.48,63.02],[103.516,65.994,103.677,66.008,103.769,65.993,103.885,65.935,104.057,65.803,104.268,65.731,104.576,65.674,104.811,65.647,104.973,65.65,105.152,65.683,105.348,65.744,105.495,65.765,105.593,65.744,105.711,65.685,105.85,65.587,105.918,65.515,105.916,65.47,105.932,65.437,105.966,65.419,106.021,65.422,106.097,65.447,106.216,65.448,106.378,65.424,106.54,65.435,106.703,65.48,106.782,65.517,106.821,65.555,106.935,65.575,107.415,65.618,107.704,65.592,107.814,65.568,107.84,65.538,107.845,65.507,107.829,65.475,107.841,65.458,107.858,65.446,107.856,65.427,107.88,65.385,107.957,65.317,108.064,65.296,108.203,65.323,108.349,65.314,108.594,65.236,108.628,65.213,108.638,65.194,108.623,65.179,108.667,65.151,108.771,65.11,108.883,65.105,109.004,65.139,109.106,65.125,109.19,65.065,109.299,65.019,109.432,64.987,109.487,64.939,109.463,64.875,109.418,64.829,109.351,64.8,109.317,64.763,109.315,64.721,109.392,64.675,109.547,64.626,109.555,64.581,109.416,64.541,109.367,64.484,109.407,64.411,109.408,64.36,109.368,64.331,109.296,64.321,109.19,64.329,109.114,64.29,109.069,64.203,109.125,64.165,109.283,64.176,109.421,64.167,109.54,64.137,109.546,64.103,109.438,64.066,109.405,64.026,109.447,63.984,109.463,63.95,109.451,63.924,109.477,63.895,109.542,63.863,109.545,63.818,109.486,63.761,109.386,63.707,109.284,63.685,109.245,63.655,109.271,63.626,109.348,63.569,109.411,63.528,109.587,63.455]]},{"n":"Vistula","z":4.7,"b":[18.2,49.644,21.961,54.349],"l":[[18.879,49.644,18.788,49.817,18.771,49.888,18.81,49.925],[18.937,49.932,19.068,49.952,19.141,49.987,19.2,50.042,19.348,50.046,19.586,49.999,19.852,50.006,20.291,50.096,20.522,50.137,20.645,50.188,20.738,50.261,20.883,50.307,21.082,50.326,21.289,50.386,21.505,50.485,21.67,50.586,21.784,50.689,21.824,50.846,21.79,51.057,21.784,51.188,21.805,51.239,21.856,51.288,21.935,51.335,21.961,51.386,21.933,51.442,21.897,51.473,21.854,51.478,21.83,51.501,21.824,51.542,21.732,51.591,21.553,51.647,21.452,51.705,21.43,51.763,21.371,51.82,21.276,51.877,21.241,51.918,21.265,51.944,21.259,51.993,21.221,52.067,21.126,52.17,20.974,52.302,20.842,52.384,20.731,52.416,20.524,52.419,20.222,52.392,19.964,52.421,19.64,52.549,19.202,52.644,18.959,52.76,18.766,52.928,18.538,53.038,18.274,53.089,18.2,53.174,18.315,53.293,18.463,53.383,18.645,53.444,18.739,53.519,18.745,53.608,18.777,53.674,18.835,53.716,18.871,53.777,18.884,53.858,18.873,53.947,18.839,54.043,18.853,54.129,18.916,54.205,18.949,54.272,18.976,54.349]]},{"n":"Volga","z":3,"b":[32.598,45.657,50.148,58.113],"l":[[47.842,46.735,48.114,46.683,48.22,46.605,48.25,46.484,48.314,46.401,48.411,46.356,48.501,46.272,48.684,46.086],[45.664,50.222,45.49,50.122,45.414,50.056,45.379,49.991,45.361,49.916,45.36,49.832,45.305,49.737,45.198,49.632,45.107,49.513,45.048,49.408,45.011,49.351,44.996,49.313,44.99,49.294,44.987,49.259,44.976,49.195,44.842,49.013,44.588,48.727,44.544,48.665,44.528,48.598,44.596,48.545,44.865,48.466,45.043,48.434,45.129,48.451,45.228,48.447,45.337,48.42,45.411,48.424,45.449,48.461,45.559,48.451,45.733,48.399,45.798,48.383,45.831,48.369,45.865,48.353,45.909,48.322,46.078,48.218,46.155,48.11,46.147,47.994,46.193,47.918,46.295,47.881,46.35,47.843,46.359,47.802,46.409,47.773,46.498,47.756,46.616,47.673,46.764,47.525,46.875,47.444,46.948,47.431,47.009,47.403,47.057,47.359,47.082,47.316,47.085,47.252,47.086,47.214,47.212,47.144,47.463,47.029,47.585,46.942,47.577,46.884,47.612,46.855,47.69,46.855,47.8,46.773,47.942,46.609,48.002,46.506,47.981,46.464,47.98,46.418,47.998,46.37,47.916,46.25,47.735,46.057,47.641,45.929,47.636,45.866,47.61,45.834,47.578,45.813,47.561,45.79,47.563,45.761,47.587,45.737,47.631,45.694,47.65,45.657],[49.905,53.457,50.042,53.455,50.108,53.405,50.148,53.306,50.129,53.236,50.048,53.193,49.957,53.185,49.856,53.213,49.717,53.203,49.543,53.156,49.428,53.152,49.372,53.193,49.269,53.219,49.119,53.23,48.919,53.209,48.667,53.155,48.524,53.105,48.491,53.057,48.503,52.995,48.561,52.919,48.505,52.816,48.334,52.688,48.177,52.506,48.035,52.271,47.901,52.125,47.775,52.066,47.644,52.043,47.507,52.055,47.273,51.981,47.114,51.919,46.971,51.83,46.843,51.773,46.775,51.748],[43.432,56.675,43.657,56.496,43.837,56.404,44.047,56.337,44.167,56.274,44.198,56.214,44.258,56.18,44.348,56.172,44.451,56.14,44.566,56.086,44.776,56.082,45.082,56.131,45.281,56.137,45.373,56.102,45.443,56.104,45.489,56.143,45.56,56.16,45.655,56.153,45.737,56.166,45.806,56.201,45.872,56.199,45.936,56.161,46.025,56.172,46.138,56.232,46.294,56.287,46.492,56.336,46.68,56.309,46.857,56.205,47.166,56.151,47.829,56.145,47.886,56.122,47.927,56.073,47.972,55.992,48.067,55.925,48.212,55.872,48.46,55.825,48.809,55.784,49.003,55.693,49.059,55.483],[37.148,56.752,37.392,56.862,37.477,56.935,37.486,57.008,37.558,57.106,37.761,57.291],[32.598,57.25,32.681,57.079,32.675,57.009,32.692,56.972,32.729,56.945,32.851,56.932,33.057,56.933,33.282,56.899,33.528,56.829,33.675,56.765,33.722,56.707,33.816,56.641,33.956,56.565,33.98,56.509,33.886,56.473,33.844,56.445,33.857,56.423,34.05,56.357,34.424,56.245,34.668,56.209,34.784,56.247,34.862,56.29,34.903,56.337,34.909,56.369,34.88,56.385,34.891,56.416,34.94,56.459,34.956,56.503,34.937,56.546,35.001,56.578,35.223,56.613,35.334,56.703,35.422,56.742,35.523,56.76,35.607,56.791,35.672,56.835,35.773,56.861,35.908,56.867,36.066,56.846,36.245,56.799,36.344,56.758,36.375,56.706],[41.344,57.493,41.715,57.448,41.889,57.451,41.989,57.489,42.173,57.485,42.576,57.415],[40.007,57.567,39.853,57.669,39.798,57.725,39.79,57.77,39.697,57.831,39.517,57.909,39.246,57.99,38.704,58.113],[38.301,57.572,38.446,57.761,38.48,57.872,38.437,58.018]]},{"n":"Volta","z":4.7,"b":[0.099,5.76,0.672,6.338],"l":[[0.103,6.338,0.099,6.188,0.157,6.114,0.275,6.065,0.38,6.042,0.47,6.046,0.544,6.021,0.6,5.968,0.639,5.897,0.672,5.76]]},{"n":"Vorma","z":5,"b":[10.945,59.171,12.055,60.525],"l":[[11.17,59.389,11.141,59.3,11.083,59.249,10.945,59.171],[11.2,59.67,11.179,59.632,11.127,59.604,11.127,59.562,11.184,59.513,11.203,59.498],[12.028,60.525,12.055,60.492,12.048,60.411,12.017,60.27,11.925,60.212,11.774,60.236,11.641,60.228,11.527,60.186,11.385,60.102,11.192,59.956,11.157,59.911]]},{"n":"Vuoksi","z":5,"b":[28.788,60.574,30.534,61.204],"l":[[30.478,60.574,30.504,60.587,30.511,60.616,30.534,60.63],[28.788,61.204,28.822,61.129,29.095,60.952,29.288,60.849,29.596,60.717]]},{"n":"Vychegda","z":4.7,"b":[46.591,61.287,55.398,62.51],"l":[[53.691,61.65,53.653,61.686,53.651,61.713,53.673,61.744,53.635,61.775,53.539,61.807,53.438,61.809,53.333,61.782,53.25,61.79,53.19,61.833,53.034,61.865,52.658,61.897,52.583,61.931,52.452,61.939,52.241,61.933,52.076,61.915,51.956,61.883,51.894,61.854,51.889,61.828,51.779,61.823,51.564,61.84,51.307,61.815,51.007,61.748,50.877,61.719,50.803,61.751,50.78,61.836,50.766,61.875,50.762,61.966,50.7,62.005,50.546,62.017,50.313,62.001,50.242,62.041,50.247,62.077,50.298,62.13,50.343,62.167,50.331,62.208,50.259,62.221,50.081,62.218,49.866,62.174,49.611,62.088,49.379,62.074,49.168,62.131,48.902,62.098,48.581,61.977,48.4,61.879,48.359,61.804,48.235,61.709,48.028,61.594,47.802,61.495,47.557,61.411,47.222,61.347,46.591,61.287],[55.398,62.362,55.183,62.438,55.015,62.438,54.908,62.462,54.805,62.51,54.727,62.506,54.676,62.45,54.568,62.422,54.402,62.422,54.297,62.397,54.252,62.347,54.223,62.298,54.211,62.252,54.236,62.165,54.298,62.036,54.394,61.944,54.524,61.889,54.662,61.858,54.809,61.848,54.891,61.797,54.908,61.703,54.905,61.648,54.882,61.631,54.126,61.532,53.829,61.539,53.684,61.65]]},{"n":"Waal","z":4,"b":[3.979,51.719,4.985,51.848],"l":[[4.985,51.824,4.725,51.757,4.581,51.719,4.418,51.74,4.074,51.841,3.979,51.848]]},{"n":"Wabash","z":5,"b":[-88.095,37.789,-84.639,40.872],"l":[[-84.755,40.359,-84.647,40.356,-84.639,40.371,-84.724,40.422,-84.757,40.44,-84.767,40.483,-84.759,40.525,-84.794,40.567,-84.886,40.588,-85.007,40.65,-85.156,40.752,-85.317,40.826,-85.49,40.872,-85.677,40.863,-85.877,40.801,-86.113,40.763,-86.383,40.75,-86.618,40.66,-86.818,40.494,-86.955,40.412,-87.029,40.415,-87.117,40.39,-87.218,40.337,-87.404,40.214,-87.445,40.127,-87.438,40.018,-87.419,39.941,-87.388,39.895,-87.378,39.808,-87.388,39.682,-87.413,39.568,-87.452,39.467,-87.507,39.392,-87.579,39.344,-87.611,39.291,-87.603,39.234,-87.615,39.193,-87.647,39.169,-87.632,39.107,-87.568,39.007,-87.53,38.909,-87.518,38.812,-87.529,38.742,-87.584,38.679,-87.636,38.543,-87.681,38.488,-87.735,38.47,-87.784,38.419,-87.827,38.336,-87.873,38.295,-87.921,38.298,-87.957,38.282,-87.98,38.249,-87.977,38.214,-87.949,38.178,-87.958,38.144,-88.006,38.113,-88.005,38.098,-87.99,38.089,-87.996,38.074,-88.026,38.06,-88.045,38.013,-88.043,37.937,-88.059,37.898,-88.091,37.897,-88.095,37.876,-88.042,37.789]]},{"n":"Waikato","z":5,"b":[174.768,-39.072,176.326,-37.287],"l":[[175.744,-39.072,175.807,-39.014,175.817,-38.983,175.803,-38.967,175.802,-38.939],[176.059,-38.694,176.125,-38.628,176.183,-38.603,176.256,-38.595,176.304,-38.567,176.326,-38.519,176.303,-38.498,176.234,-38.506,176.167,-38.482,176.101,-38.428,176.003,-38.405,175.873,-38.415,175.778,-38.394,175.72,-38.341,175.679,-38.274,175.656,-38.192,175.654,-38.107,175.673,-38.019,175.655,-37.963,175.573,-37.928,175.451,-37.892,175.353,-37.828,175.239,-37.725,175.178,-37.625,175.169,-37.527,175.138,-37.43,175.083,-37.334,175,-37.287,174.833,-37.292,174.768,-37.339]]},{"n":"Waitaki","z":5,"b":[170.223,-44.911,170.999,-44.003],"l":[[170.51,-44.003,170.387,-44.216,170.332,-44.288,170.279,-44.32,170.257,-44.361,170.243,-44.388,170.223,-44.49,170.283,-44.591,170.491,-44.739,170.558,-44.795,170.655,-44.833,170.882,-44.891,170.999,-44.911]]},{"n":"Weir","z":3,"b":[150.47,-29.139,151.863,-28.639],"l":[[151.863,-28.739,151.75,-28.77,151.673,-28.824,151.554,-28.955,151.528,-29.035,151.478,-29.087,151.393,-29.139,151.335,-29.104,151.303,-28.982,151.231,-28.898,151.118,-28.851,151.058,-28.805,151.05,-28.758,150.982,-28.71,150.856,-28.66,150.712,-28.639,150.47,-28.65]]},{"n":"Willamette","z":5,"b":[-123.22,43.422,-122.095,45.674],"l":[[-122.095,43.422,-122.219,43.485,-122.371,43.515,-122.424,43.574,-122.427,43.672,-122.458,43.746,-122.517,43.795,-122.684,43.888,-122.96,44.024,-123.119,44.15,-123.184,44.323,-123.22,44.513,-123.189,44.599,-123.103,44.644,-123.077,44.68,-123.112,44.707,-123.116,44.747,-123.09,44.802,-123.098,44.836,-123.14,44.849,-123.124,44.887,-123.05,44.947,-123.024,44.999,-123.046,45.042,-123.036,45.081,-122.993,45.117,-122.987,45.157,-123.018,45.202,-123.016,45.238,-122.981,45.267,-122.902,45.288,-122.718,45.306,-122.621,45.368,-122.603,45.427,-122.634,45.502,-122.677,45.56,-122.732,45.603,-122.745,45.649,-122.726,45.674]]},{"n":"Winnipeg","z":5,"b":[-96.295,49.993,-94.615,50.625],"l":[[-94.615,49.993,-94.651,50.024,-94.648,50.062,-94.639,50.101,-94.668,50.135,-94.745,50.143,-94.809,50.15,-94.931,50.131,-94.999,50.141,-95.013,50.18,-95.073,50.22,-95.179,50.261,-95.207,50.34,-95.276,50.366,-95.398,50.369,-95.506,50.323,-95.6,50.228,-95.726,50.17,-95.883,50.148,-95.987,50.148,-96.036,50.168,-96.052,50.205,-96.026,50.283],[-95.989,50.404,-96.005,50.451,-96.081,50.506,-96.295,50.625]]},{"n":"Xar Moron","z":5,"b":[117.172,42.592,120.759,43.423],"l":[[117.209,42.592,117.172,42.613,117.179,42.645,117.216,42.696,117.23,42.748,117.214,42.827,117.189,42.886,117.194,42.935,117.222,42.994,117.347,43.063,117.57,43.142,117.746,43.185,117.874,43.192,118.016,43.22,118.172,43.271,118.471,43.276,118.911,43.237,119.22,43.229,119.484,43.262,120.759,43.423]]},{"n":"Xi","z":4,"b":[111.301,22.178,113.552,23.481],"l":[[113.035,22.795,113.095,22.73,113.166,22.576,113.214,22.483,113.264,22.363,113.416,22.178],[113.035,22.795,113.299,22.698,113.395,22.651,113.444,22.599,113.492,22.591,113.552,22.587],[112.98,23.041,113.445,23.055],[112.805,23.148,112.819,23.082,112.891,22.918,112.963,22.822,113.035,22.795],[112.805,23.148,112.912,23.113,112.974,23.052,113.024,22.954,113.09,22.907,113.213,22.912,113.338,22.889],[111.301,23.481,111.433,23.449,111.499,23.375,111.542,23.249,111.707,23.161,112.137,23.09,112.271,23.142,112.353,23.132,112.427,23.076,112.498,23.078,112.563,23.136,112.643,23.165,112.736,23.164,112.805,23.148]]},{"n":"Xiliao","z":5,"b":[120.759,43.423,123.554,43.815],"l":[[120.759,43.423,121.059,43.509,121.349,43.517,121.729,43.474,122.03,43.521,122.251,43.656,122.449,43.748,122.624,43.795,122.769,43.815,122.886,43.808,123.017,43.765,123.162,43.687,123.269,43.647,123.338,43.643,123.405,43.628,123.468,43.6,123.513,43.561,123.554,43.483]]},{"n":"Xingu","z":4.7,"b":[-53.444,-11.444,-51.641,-1.64],"l":[[-53.444,-11.444,-53.305,-11.298,-53.273,-11.175,-53.302,-11.025,-53.266,-10.917,-53.166,-10.852,-53.101,-10.706,-53.072,-10.48,-53.032,-10.343,-52.982,-10.296,-52.972,-10.25,-53.001,-10.206,-53.011,-10.155,-53.002,-10.095,-52.945,-10.005,-52.839,-9.884,-52.755,-9.808,-52.69,-9.775,-52.636,-9.715,-52.59,-9.628,-52.48,-9.596,-52.304,-9.62,-52.205,-9.585,-52.182,-9.491,-52.133,-9.4,-52.058,-9.311,-52.036,-9.237,-52.067,-9.176,-52.083,-9.085,-52.085,-8.965,-52.132,-8.878,-52.222,-8.824,-52.292,-8.746,-52.342,-8.643,-52.413,-8.581,-52.505,-8.558,-52.572,-8.522,-52.612,-8.473,-52.637,-8.4,-52.645,-8.303,-52.688,-8.167,-52.765,-7.993,-52.742,-7.726,-52.618,-7.365,-52.513,-7.131,-52.427,-7.022,-52.31,-6.954,-52.164,-6.925,-52.064,-6.863,-52.011,-6.767,-52.011,-6.677,-52.063,-6.594,-52.132,-6.548,-52.216,-6.539,-52.301,-6.483,-52.387,-6.381,-52.435,-6.29,-52.45,-6.172,-52.512,-6.089,-52.534,-6.009,-52.536,-5.902,-52.58,-5.754,-52.667,-5.565,-52.75,-5.445,-52.829,-5.395,-52.882,-5.298,-52.908,-5.156,-52.856,-4.933,-52.775,-4.745,-52.725,-4.629,-52.679,-4.441,-52.719,-4.37,-52.714,-4.301,-52.665,-4.233,-52.634,-4.114,-52.613,-3.861],[-52.609,-3.855,-52.345,-3.595,-52.233,-3.438,-52.184,-3.299,-52.109,-3.275,-52.009,-3.367,-51.952,-3.441,-51.939,-3.499,-51.905,-3.543,-51.849,-3.574,-51.787,-3.56,-51.719,-3.502,-51.693,-3.445,-51.709,-3.388,-51.691,-3.313,-51.641,-3.218,-51.657,-3.15,-51.738,-3.108,-51.836,-3.021,-51.952,-2.887,-52.003,-2.765,-51.989,-2.655,-52.039,-2.489,-52.092,-2.386,-52.153,-2.267,-52.221,-2.041,-52.251,-1.746,-52.231,-1.683,-52.197,-1.64]]},{"n":"Xun","z":4,"b":[110.089,23.397,111.301,23.555],"l":[[110.089,23.408,110.201,23.545,110.334,23.555,110.622,23.45,110.745,23.466,110.814,23.447,110.868,23.397,110.997,23.399,111.301,23.481]]},{"n":"Yadkin","z":5,"b":[-81.576,36.047,-80.464,36.259],"l":[[-81.576,36.074,-81.522,36.047,-81.465,36.055,-81.349,36.106,-80.899,36.225,-80.674,36.259,-80.49,36.242,-80.464,36.232]]},{"n":"Yamuna","z":5,"b":[77.074,25.289,81.903,30.915],"l":[[78.34,30.915,78.072,30.738,77.987,30.568,77.942,30.518,77.91,30.532,77.815,30.511,77.655,30.456,77.574,30.391,77.57,30.278,77.528,30.203,77.443,30.127,77.301,30.027,77.187,29.871,77.101,29.661,77.074,29.538,77.105,29.502,77.107,29.469,77.094,29.45,77.093,29.427,77.121,29.401,77.145,29.34,77.137,29.191,77.156,29.066,77.199,28.965,77.221,28.858,77.222,28.745,77.249,28.647,77.327,28.52,77.455,28.425,77.489,28.374,77.474,28.335,77.484,28.282,77.521,28.215,77.521,28.147,77.486,28.08,77.496,27.985,77.552,27.86,77.607,27.797,77.662,27.797,77.681,27.749,77.664,27.653,77.668,27.601,77.693,27.592,77.7,27.57,77.69,27.533,77.711,27.467,77.765,27.371,77.821,27.305,77.88,27.269,77.92,27.263,77.942,27.286,77.966,27.29,77.994,27.274,78.012,27.245,78.02,27.202,78.068,27.183,78.156,27.188,78.203,27.165,78.21,27.113,78.249,27.096,78.292,27.106,78.309,27.095,78.313,27.079,78.327,27.062,78.383,27.058,78.437,27.024,78.488,26.961,78.508,26.953,78.522,26.96,78.544,26.958,78.609,26.91,78.663,26.894,78.707,26.911,78.779,26.88,78.879,26.801,78.948,26.762,78.986,26.762,79.024,26.721,79.064,26.639,79.136,26.58,79.24,26.542,79.263,26.511,79.262,26.485,79.251,26.472,79.266,26.446,79.28,26.433,79.311,26.428,79.402,26.436,79.473,26.416,79.494,26.372,79.518,26.359,79.536,26.355,79.545,26.335,79.538,26.306,79.592,26.244,79.712,26.156,79.804,26.108,79.868,26.099,79.912,26.112,79.932,26.125,79.947,26.125,79.956,26.095,79.989,26.072,80.05,26.071,80.08,26.053,80.08,26.017,80.113,25.979,80.211,25.92,80.442,25.889,80.524,25.87,80.535,25.852,80.523,25.835,80.489,25.819,80.524,25.781,80.63,25.723,80.717,25.702,80.784,25.72,80.844,25.717,80.897,25.696,80.93,25.668,80.944,25.632,80.969,25.617,81.005,25.623,81.022,25.605,81.019,25.563,81.059,25.531,81.142,25.507,81.181,25.466,81.176,25.407,81.222,25.367,81.318,25.345,81.377,25.319,81.399,25.289,81.472,25.295,81.598,25.337,81.694,25.351,81.762,25.337,81.804,25.352,81.82,25.395,81.847,25.416,81.903,25.415]]},{"n":"Yangtze","z":2,"b":[113.128,29.463,120.074,32.235],"l":[[113.128,29.463,113.166,29.484,113.21,29.538,113.53,29.832,113.696,29.949,113.76,29.939,113.86,29.972,113.995,30.05,114.011,30.124,113.909,30.196,113.885,30.246,113.94,30.276,113.99,30.279,114.032,30.253,114.116,30.328,114.288,30.572,114.303,30.593,114.396,30.664,114.472,30.661,114.558,30.603,114.662,30.582,114.784,30.598,114.841,30.565,114.833,30.482,114.885,30.423,114.997,30.387,115.058,30.336,115.067,30.269,115.102,30.229,115.16,30.216,115.252,30.128,115.376,29.966,115.504,29.879,115.638,29.867,115.751,29.828,115.843,29.764,115.983,29.746,116.171,29.775,116.212,29.785],[116.198,29.751,116.212,29.785],[116.212,29.785,116.327,29.815,116.451,29.868,116.544,29.931,116.606,30.006,116.683,30.061,116.776,30.097,116.847,30.168,116.896,30.272,116.913,30.349,116.9,30.399,116.972,30.446,117.13,30.491,117.212,30.552,117.217,30.629,117.247,30.674,117.327,30.695,117.605,30.776,117.725,30.886,117.78,31.052,117.844,31.124,117.916,31.104,117.968,31.13,117.999,31.202,118.089,31.255,118.24,31.29,118.312,31.341,118.305,31.406,118.333,31.51,118.395,31.653,118.515,31.841,118.692,32.074,118.961,32.203,119.324,32.227,119.53,32.229,119.643,32.235,119.771,32.207,119.867,32.1,119.941,32.04,120.074,31.96]]},{"n":"Yarkant","z":5,"b":[75.941,35.521,80.888,40.467],"l":[[77.525,35.521,77.638,35.582,77.683,35.631,77.708,35.715,77.585,35.857,77.597,35.971,77.704,36.105,77.701,36.196,77.588,36.243,77.511,36.308,77.471,36.389,77.358,36.437,77.172,36.452,77.017,36.44,76.891,36.402,76.783,36.388,76.69,36.396,76.52,36.43,76.464,36.468,76.435,36.524,76.341,36.586,76.182,36.655,76.058,36.756,75.969,36.889,75.947,37.011,75.992,37.122,75.988,37.212,75.941,37.272,75.952,37.293,75.97,37.303,76.006,37.296,76.056,37.286,76.142,37.311,76.214,37.382,76.229,37.523,76.188,37.732,76.216,37.875,76.314,37.95,76.402,37.989,76.479,37.993,76.523,37.978,76.533,37.947,76.597,37.919,76.716,37.895,76.927,37.997,77.23,38.225,77.368,38.4,77.343,38.522,77.39,38.655,77.512,38.799,77.568,38.905,77.56,38.973,77.579,39.04,77.625,39.106,77.67,39.141,77.713,39.146,77.802,39.204,77.937,39.316,78.121,39.425,78.354,39.531,78.607,39.597,78.882,39.622,79.101,39.66,79.265,39.713,79.597,39.862,80.097,40.108,80.377,40.274,80.437,40.361,80.572,40.42,80.888,40.467]]},{"n":"Brahmaputra","z":2.1,"b":[87.66,29.013,94.356,29.381],"l":[[87.66,29.152,87.747,29.288,87.823,29.349,87.917,29.38,88.015,29.381,88.118,29.352,88.182,29.347,88.208,29.368,88.257,29.366,88.327,29.343,88.43,29.336,88.631,29.351,89.404,29.366,89.699,29.359,89.774,29.335,89.862,29.334,89.963,29.356,90.082,29.36,90.218,29.344,90.321,29.315,90.392,29.274,90.506,29.278,90.662,29.329,90.746,29.34,90.759,29.31,90.786,29.308,90.828,29.332,90.897,29.337,91.038,29.317,92.076,29.281,92.465,29.231,92.551,29.155,92.695,29.101,92.895,29.069,93.023,29.073,93.078,29.11,93.117,29.107,93.138,29.061,93.183,29.029,93.25,29.013,93.331,29.05,93.427,29.14,93.568,29.178,93.755,29.163,93.932,29.171,94.097,29.202,94.224,29.24,94.356,29.31]]},{"n":"Yenisey","z":4,"b":[83.11,58.1,92.998,70.11],"l":[[83.11,70.11,83.307,70.049,83.464,69.976,83.551,69.891,83.791,69.79,84.183,69.67,84.487,69.647,84.702,69.719,84.906,69.766,85.098,69.788,85.413,69.721,85.85,69.567,85.953,69.49,85.963,69.382,85.992,69.294,86.025,69.187,86.051,69.119,86.012,69.031,85.955,68.933,85.992,68.841,86.193,68.74,86.248,68.7,86.287,68.666,86.33,68.599,86.353,68.529,86.345,68.456,86.401,68.381,86.524,68.302,86.536,68.214,86.437,68.114,86.436,68.016,86.48,67.928,86.451,67.84,86.347,67.748,86.286,67.66,86.288,67.573,86.353,67.488,86.447,67.437,86.568,67.42,86.618,67.359,86.598,67.255,86.618,67.184,86.679,67.146,86.698,67.093,86.677,67.025,86.688,66.942,86.73,66.845,86.63,66.747,86.388,66.647,86.286,66.585,86.326,66.558,86.477,66.564,86.741,66.601,87.032,66.557,87.35,66.431,87.458,66.387,87.505,66.348,87.514,66.32,87.479,66.289,87.454,66.273,87.316,66.225,87.275,66.19,87.312,66.163,87.474,66.135,87.762,66.105,87.914,66.062,87.93,66.004,87.896,65.962,87.81,65.936,87.765,65.91,87.763,65.884,87.772,65.854,87.793,65.818,87.85,65.791,87.944,65.773,88.003,65.74,88.029,65.694,87.98,65.457,87.848,64.999,87.822,64.878,87.795,64.817,87.768,64.787,87.7,64.737,87.631,64.679,87.564,64.595,87.54,64.516,87.544,64.442,87.577,64.373,87.579,64.271,87.55,64.137,87.558,64.039,87.602,63.975,87.551,63.886,87.405,63.77,87.347,63.678,87.377,63.607,87.36,63.528,87.293,63.439,87.266,63.382,87.277,63.357,87.442,63.299,87.76,63.207,88.024,63.093,88.34,62.888,88.434,62.854,88.48,62.803,88.51,62.724,88.595,62.635,88.736,62.535,88.857,62.472,88.961,62.443,89.014,62.388,89.016,62.307,89.083,62.179,89.212,62.004,89.325,61.894,89.47,61.827,89.706,61.751,89.786,61.688,89.789,61.613,89.843,61.586,89.95,61.606,90.052,61.601,90.15,61.573,90.174,61.537,90.124,61.494,90.001,61.45,89.806,61.405,89.685,61.337,89.637,61.244,89.659,61.11,89.751,60.935,89.847,60.815,89.947,60.751,90.006,60.657,90.025,60.535,90.056,60.459,90.12,60.417,90.166,60.381,90.172,60.307,90.309,60.238,90.495,60.115,90.698,59.927,90.783,59.782,90.804,59.745,90.858,59.707,90.884,59.657,90.882,59.597,90.99,59.498,91.207,59.361,91.325,59.267,91.345,59.217,91.431,59.163,91.582,59.106,91.678,59.051,91.719,58.997,91.733,58.953,91.718,58.919,91.769,58.901,91.885,58.898,91.977,58.876,92.045,58.835,92.046,58.795,91.982,58.757,91.982,58.727,92.048,58.707,92.084,58.649,92.091,58.555,92.139,58.498,92.228,58.48,92.275,58.451,92.282,58.412,92.32,58.38,92.39,58.357,92.428,58.326,92.435,58.287,92.473,58.245,92.539,58.199,92.679,58.157,92.998,58.1]]},{"n":"Yuan","z":5,"b":[107.501,26.164,113.128,29.463],"l":[[107.505,26.338,107.501,26.239,107.519,26.191,107.558,26.164,107.61,26.167,107.708,26.218,107.78,26.358,107.8,26.432,107.793,26.485,107.826,26.53,107.9,26.564,107.959,26.617,108.005,26.688,108.072,26.753,108.159,26.812,108.251,26.839,108.348,26.835,108.396,26.817,108.396,26.785,108.453,26.725,108.568,26.636,108.645,26.618,108.683,26.67,108.718,26.696,108.748,26.695,108.762,26.679,108.76,26.649,108.795,26.614,108.869,26.575,108.961,26.573,109.073,26.608,109.194,26.679,109.39,26.838,109.438,26.894,109.444,26.954,109.424,27.038,109.441,27.08,109.494,27.079,109.556,27.107,109.628,27.165,109.667,27.177,109.675,27.144,109.692,27.128,109.719,27.131,109.74,27.152,109.755,27.19,109.823,27.189,109.943,27.147,110.051,27.19,110.217,27.423,110.264,27.504,110.284,27.566,110.277,27.611,110.299,27.653,110.351,27.694,110.367,27.729,110.347,27.758,110.348,27.791,110.371,27.828,110.347,27.863,110.275,27.896,110.251,27.927,110.275,27.955,110.252,27.981,110.182,28.005,110.135,28.036,110.113,28.076,110.128,28.128,110.179,28.193,110.195,28.242,110.175,28.275,110.182,28.289,110.216,28.286,110.272,28.32,110.351,28.393,110.419,28.486,110.477,28.602,110.633,28.69,110.887,28.751,111.04,28.803,111.094,28.845,111.149,28.838,111.207,28.781,111.279,28.762,111.405,28.791,111.522,28.986,111.559,29.005,111.593,28.99,111.626,28.996,111.655,29.013,111.679,29.013,111.705,28.992,111.744,28.988,111.796,29.01,111.865,29.012,111.993,28.985],[112.905,29.031,112.994,29.147],[113.069,29.388,113.128,29.463]]},{"n":"Yukon","z":3,"b":[-165.244,61.269,-134.87,66.611],"l":[[-134.916,61.568,-134.87,61.628,-134.885,61.685,-134.946,61.759,-134.964,61.816,-134.941,61.856,-134.94,61.889,-134.961,61.912,-135.121,61.957,-135.419,62.024,-135.629,62.049,-135.75,62.032,-135.928,62.046,-136.307,62.121,-136.362,62.145,-136.378,62.197,-136.354,62.275,-136.583,62.428,-137.356,62.781,-137.452,62.809,-137.733,62.82,-138.398,62.801,-139.236,62.97,-139.418,63.011,-139.532,63.071,-139.564,63.161,-139.545,63.234,-139.472,63.288,-139.506,63.353,-139.646,63.427,-139.73,63.513,-139.76,63.609,-139.759,63.674,-139.728,63.707,-139.723,63.75,-139.744,63.805,-139.743,63.865,-139.721,63.932,-139.653,63.983,-139.541,64.017,-139.482,64.057,-139.476,64.102,-139.511,64.158,-139.587,64.227,-139.849,64.297,-140.296,64.367,-140.453,64.405,-140.477,64.426,-140.47,64.453,-140.465,64.498,-140.479,64.526,-140.545,64.518,-140.589,64.528,-140.61,64.558,-140.649,64.572,-140.705,64.57,-140.73,64.585,-140.724,64.616,-140.746,64.627,-140.796,64.62,-140.882,64.641,-140.933,64.674,-141.006,64.689,-141.065,64.726,-141.059,64.749,-141.093,64.775,-141.164,64.802,-141.176,64.833,-141.128,64.867,-141.129,64.888,-141.179,64.895,-141.198,64.913,-141.187,64.943,-141.23,64.954,-141.327,64.945,-141.373,64.966,-141.369,65.041,-141.404,65.086,-141.533,65.129,-141.768,65.183,-141.924,65.241,-142.002,65.3,-142.147,65.335,-142.359,65.344,-142.502,65.363,-142.576,65.392,-142.655,65.393,-142.738,65.366,-142.929,65.355,-143.227,65.359,-143.447,65.383,-143.589,65.427,-143.673,65.472,-143.698,65.518,-143.781,65.554,-143.922,65.579,-144.023,65.613,-144.085,65.655,-144.087,65.722,-144.03,65.812,-144.017,65.876,-144.048,65.914,-144.251,66.027,-144.624,66.215,-144.832,66.335,-144.874,66.386,-145.003,66.453,-145.219,66.537,-145.453,66.587,-145.828,66.611],[-163.259,62.016,-163.241,61.94,-163.242,61.886,-163.289,61.847,-163.388,61.829,-163.539,61.832,-163.644,61.816,-163.705,61.783,-163.774,61.777,-163.836,61.797,-163.845,61.835,-163.86,61.851,-163.898,61.853,-163.959,61.839,-164.002,61.851,-164.026,61.891,-164.087,61.879,-164.183,61.816,-164.264,61.789,-164.331,61.798,-164.429,61.782,-164.56,61.742,-164.646,61.744,-164.746,61.762,-164.831,61.739,-164.879,61.698,-164.869,61.665,-164.766,61.629,-164.743,61.591,-164.778,61.551,-164.888,61.507,-165.007,61.48,-165.091,61.441,-165.188,61.392,-165.206,61.33,-165.201,61.286,-165.244,61.269],[-163.259,62.016,-163.434,62.061,-163.731,62.092,-163.904,62.134,-163.953,62.184,-163.948,62.248,-163.889,62.325,-163.859,62.392,-163.861,62.448,-163.93,62.532,-164.067,62.643,-164.222,62.71,-164.482,62.745,-164.522,62.737,-164.589,62.709],[-145.828,66.611,-146.664,66.508,-147.015,66.445,-147.159,66.386,-147.323,66.352,-147.509,66.344,-147.596,66.321,-147.585,66.284,-147.746,66.264,-148.079,66.261,-148.364,66.225,-148.601,66.155,-148.729,66.104,-148.764,66.067,-148.79,66.062,-148.812,66.085,-148.873,66.09,-148.971,66.077,-149.014,66.056,-149,66.029,-149.054,66.007,-149.175,65.991,-149.251,65.957,-149.284,65.905,-149.438,65.889,-149.714,65.91,-149.92,65.89,-150.058,65.83,-150.048,65.798,-149.89,65.795,-149.827,65.755,-149.858,65.679,-149.93,65.65,-150.041,65.669,-150.125,65.641,-150.183,65.565,-150.324,65.52,-150.549,65.505,-150.792,65.446,-151.054,65.343,-151.401,65.26,-152.048,65.165,-152.349,65.124,-152.534,65.134,-152.699,65.182,-152.857,65.201,-153.007,65.191,-153.126,65.166,-153.215,65.124,-153.331,65.112,-153.476,65.129,-153.74,65.084,-154.122,64.976,-154.402,64.927,-154.581,64.936,-154.848,64.902,-155.134,64.867,-155.381,64.79,-155.575,64.756,-155.754,64.764,-155.998,64.721,-156.202,64.713,-156.425,64.68,-156.733,64.664,-156.76,64.696,-156.809,64.718,-156.917,64.73,-156.993,64.755,-157.036,64.795,-157.271,64.826,-157.396,64.82,-157.51,64.844,-157.769,64.85,-157.842,64.869,-157.913,64.858,-158.003,64.801,-158.169,64.681,-158.286,64.643,-158.501,64.464,-158.678,64.343,-158.691,64.311,-158.659,64.223,-158.674,64.138,-158.734,64.058,-158.826,63.994,-159.01,63.925,-159.149,63.884,-159.203,63.848,-159.217,63.802,-159.256,63.767,-159.318,63.743,-159.386,63.679,-159.458,63.575,-159.479,63.494,-159.448,63.437,-159.502,63.372,-159.642,63.301,-159.725,63.214,-159.753,63.111,-159.826,63.035,-159.945,62.987,-160.019,62.916,-160.049,62.822,-160.09,62.756,-160.144,62.717,-160.17,62.66,-160.17,62.586,-160.133,62.523,-160.058,62.471,-160.006,62.415,-159.975,62.371,-159.963,62.321,-159.956,62.28,-159.913,62.253,-159.805,62.236,-159.744,62.214,-159.73,62.188,-159.742,62.154,-159.782,62.114,-159.852,62.086,-159.952,62.071,-160.032,62.033,-160.093,61.973,-160.336,61.941,-160.762,61.936,-161.067,61.907,-161.253,61.853,-161.339,61.805,-161.326,61.764,-161.346,61.72,-161.401,61.673,-161.528,61.637,-161.754,61.617,-161.826,61.601,-161.92,61.576,-161.956,61.597,-161.963,61.656,-162.023,61.745,-162.135,61.863,-162.249,61.923,-162.367,61.924,-162.44,61.948,-162.47,61.994,-162.527,62.006,-162.613,61.984,-162.668,61.957,-162.691,61.923,-162.74,61.918,-162.814,61.942,-162.888,61.94,-162.962,61.913,-163.043,61.908,-163.132,61.923,-163.204,61.959,-163.259,62.016],[-145.828,66.611,-145.833,66.611],[-145.825,66.611,-145.828,66.611]]},{"n":"Za","z":2.1,"b":[94.084,31.966,96.941,33.205],"l":[[94.084,33.156,94.448,33.163,94.507,33.204,94.61,33.205,94.777,33.181,94.916,33.14,95.026,33.083,95.089,33.025,95.106,32.966,95.225,32.915,95.447,32.87,95.581,32.815,95.627,32.748,95.728,32.672,95.882,32.587,96.011,32.552,96.113,32.567,96.183,32.562,96.239,32.526,96.381,32.433,96.455,32.343,96.507,32.225,96.585,32.155,96.69,32.133,96.77,32.085,96.824,32.012,96.874,31.973,96.941,31.966]]},{"n":"Zambezi","z":3,"b":[22.544,-18.842,36.498,-11.101],"l":[[36.039,-18.337,36.105,-18.424,36.14,-18.536,36.145,-18.62,36.12,-18.677,36.129,-18.72,36.15,-18.75,36.153,-18.795,36.125,-18.842],[36.039,-18.337,36.498,-18.576],[28.761,-16.532,28.833,-16.424,28.857,-16.306,28.857,-16.142,28.876,-16.036,28.913,-15.988,28.973,-15.95,29.051,-15.901,29.288,-15.777,29.487,-15.697,29.73,-15.645,30.796,-15.642],[32.707,-15.58,32.925,-15.586,33.028,-15.634,33.089,-15.725,33.203,-15.835,33.372,-15.963,33.564,-16.147,33.885,-16.508,33.984,-16.599,34.205,-16.697,34.582,-16.834,34.795,-16.923,34.844,-16.964,34.897,-17.059,34.981,-17.28,35.096,-17.521,35.187,-17.636,35.291,-17.705,35.389,-17.8,35.48,-17.92,35.602,-18.013,35.755,-18.08,35.846,-18.146,35.877,-18.211,35.941,-18.275,36.039,-18.337],[24.266,-11.379,24.327,-11.311,24.334,-11.239,24.308,-11.141,24.226,-11.101,24.022,-11.126,23.75,-11.332,23.599,-11.423,23.477,-11.467,23.387,-11.521,23.33,-11.586,23.173,-11.686,22.915,-11.823,22.76,-11.952,22.707,-12.074,22.65,-12.166,22.588,-12.227,22.553,-12.346,22.544,-12.522,22.581,-12.69,22.662,-12.849,22.698,-12.965,22.687,-13.038,22.753,-13.132,22.964,-13.305,23.023,-13.355,23.074,-13.458,23.137,-13.63,23.186,-13.726,23.221,-13.747,23.228,-13.785,23.207,-13.84,23.207,-13.909,23.228,-13.993,23.225,-14.066,23.197,-14.131,23.195,-14.216,23.218,-14.324,23.189,-14.443,23.107,-14.573,23.034,-14.784,22.969,-15.077,22.982,-15.278,23.073,-15.387,23.125,-15.482,23.136,-15.564,23.127,-15.625,23.099,-15.666,23.12,-15.737,23.189,-15.837,23.246,-16.013,23.292,-16.264,23.492,-16.549,23.846,-16.868,24.077,-17.14,24.238,-17.478,24.733,-17.518,24.932,-17.543,25.002,-17.569,25.092,-17.634,25.259,-17.794,25.452,-17.845,25.557,-17.85,25.64,-17.824,25.742,-17.858,25.863,-17.952,25.996,-17.97,26.14,-17.912,26.333,-17.929,26.578,-18.023,26.78,-18.042,27.021,-17.958]]},{"n":"","z":5,"b":[170.001,-44.361,170.257,-44.286],"l":[[170.001,-44.286,170.11,-44.291,170.257,-44.361]]},{"n":"","z":5,"b":[170.204,-44.32,170.279,-44.171],"l":[[170.204,-44.171,170.279,-44.32]]},{"n":"","z":5,"b":[-106.018,58.455,-103.55,59.271],"l":[[-103.55,58.455,-103.665,58.545],[-103.786,58.691,-103.959,58.772,-103.999,58.809,-103.962,58.829,-103.95,58.868,-103.963,58.927,-104.144,58.979,-104.495,59.023,-104.792,59.037,-104.92,59.08,-104.951,59.142,-105.012,59.173],[-105.55,59.15,-105.572,59.221,-105.689,59.251,-106.018,59.271]]},{"n":"","z":5,"b":[-138.515,59.166,-137,61.227],"l":[[-137,61.227,-137.061,60.991,-137.069,60.952,-137.09,60.91,-137.177,60.874,-137.348,60.824,-137.511,60.797,-137.664,60.794,-137.768,60.757,-137.822,60.687,-137.845,60.607,-137.836,60.517,-137.878,60.412,-137.972,60.293,-137.997,60.231,-137.967,60.178,-137.863,60.067,-137.851,60.027,-137.863,59.984,-137.966,59.927,-137.991,59.881,-137.936,59.846,-137.929,59.811,-137.969,59.776,-137.972,59.738,-137.92,59.678,-137.83,59.587,-137.817,59.544,-137.824,59.51,-137.968,59.461,-137.987,59.452,-138.021,59.439,-138.061,59.437,-138.184,59.401,-138.233,59.372,-138.26,59.32,-138.25,59.291,-138.229,59.275,-138.209,59.266,-138.198,59.252,-138.185,59.224,-138.182,59.202,-138.189,59.19,-138.202,59.18,-138.221,59.174,-138.239,59.176,-138.438,59.183,-138.482,59.177,-138.515,59.166]]},{"n":"","z":5,"b":[-125.619,64.895,-123.472,65.152],"l":[[-123.472,65.152,-123.737,65.062,-124.205,65.026,-124.966,65.014,-125.41,64.982,-125.619,64.895]]},{"n":"","z":5,"b":[168.633,-46.566,168.957,-45.327],"l":[[168.775,-45.327,168.724,-45.461,168.683,-45.516,168.633,-45.537,168.637,-45.57,168.692,-45.614,168.712,-45.669,168.694,-45.736,168.701,-45.789,168.747,-45.847,168.922,-46.028,168.957,-46.107,168.929,-46.209,168.799,-46.448,168.767,-46.566]]},{"n":"","z":4.7,"b":[-73.177,46.044,-72.991,46.177],"l":[[-72.991,46.177,-73.079,46.16,-73.146,46.124,-73.177,46.071,-73.164,46.044]]},{"n":"","z":4.7,"b":[-103.759,60.952,-95.492,64.05],"l":[[-103.447,61.366,-103.468,61.283,-103.55,61.212,-103.699,61.126,-103.759,61.05,-103.716,60.952],[-103.158,61.836,-103.236,61.773,-103.251,61.734,-103.22,61.681],[-99.618,62.301,-98.574,62.252,-98.209,62.252,-98.175,62.285,-98.206,62.38,-98.35,62.616],[-101.366,62.35,-101.318,62.345],[-100.735,62.356,-100.552,62.355],[-102.737,62.41,-102.919,62.319],[-101.831,62.52,-101.529,62.664],[-102.035,62.925,-102.118,62.915,-102.181,62.884,-102.25,62.828,-102.379,62.79,-102.66,62.76],[-97.63,62.979,-97.61,63.047,-97.567,63.092,-97.459,63.157],[-97.288,63.514,-97.235,63.573,-97.182,63.593,-97.111,63.593,-97.084,63.604,-97.071,63.635,-97.014,63.64,-96.952,63.637],[-95.878,63.656,-95.811,63.669,-95.799,63.694,-95.809,63.719,-95.799,63.746,-95.723,63.766,-95.677,63.798,-95.677,63.846,-95.631,63.898,-95.537,63.956,-95.492,64.001,-95.497,64.05]]},{"n":"","z":4.7,"b":[105.931,9.949,106.658,10.33],"l":[[106.464,10.298,106.385,10.33,106.138,10.306],[106.138,10.306,105.931,10.284],[106.138,10.306,106.184,10.253,106.222,10.245,106.292,10.262,106.33,10.247,106.36,10.211,106.408,10.117,106.468,10.05,106.519,10.015,106.579,9.979,106.658,9.949]]},{"n":"","z":4.7,"b":[-101.318,62.345,-101.163,62.367],"l":[[-101.163,62.367,-101.318,62.345]]},{"n":"Garonne","z":5,"b":[-0.567,43.415,1.454,45.001],"l":[[1.454,43.415,1.453,43.417,1.409,43.514,1.409,43.588,1.361,43.689,1.22,43.881,1.082,44.031,0.861,44.126,0.509,44.215,0.323,44.295,0.304,44.366,0.255,44.422,0.137,44.484,0.003,44.561,-0.102,44.587,-0.224,44.588,-0.343,44.636,-0.534,44.796,-0.562,44.83,-0.567,44.86,-0.542,44.898,-0.536,44.952,-0.548,45.001]]},{"n":"Loire","z":4.7,"b":[-1.743,44.775,4.256,47.894],"l":[[3.083,46.985,3.022,47.149,2.916,47.287,2.893,47.363,2.917,47.423,2.883,47.499,2.789,47.59,2.614,47.691,2.36,47.804,2.145,47.871,1.971,47.894,1.845,47.893,1.769,47.866,1.608,47.764,1.364,47.587,1.158,47.474,0.99,47.425,0.815,47.396,0.635,47.386,0.428,47.341,0.207,47.244,0.075,47.218,-0.038,47.249,-0.216,47.364,-0.418,47.402,-0.731,47.371,-1.056,47.375,-1.249,47.336,-1.417,47.258,-1.559,47.216,-1.633,47.202,-1.743,47.216],[3.083,46.985,3.168,46.977,3.249,46.952,3.329,46.846,3.49,46.829,3.625,46.74,3.724,46.639,3.749,46.558,3.86,46.507,3.984,46.46,3.99,46.379,4.039,46.238,4.101,46.166,4.101,46.063,4.033,45.977,4.052,45.925,4.157,45.852,4.213,45.745,4.219,45.628,4.256,45.477,4.2,45.368,4.132,45.307,4.083,45.238,3.953,45.199,3.91,45.116,3.922,45.037,3.904,44.972,3.928,44.871,4.07,44.81,4.169,44.775,4.2,44.823]]}]} \ No newline at end of file diff --git a/src/maps/data/states_50m.json b/src/maps/data/states_50m.json new file mode 100644 index 0000000..64dcd18 --- /dev/null +++ b/src/maps/data/states_50m.json @@ -0,0 +1 @@ +{"f":[{"z":3,"b":[-53.935,-29.356,-49.699,-27.081],"l":[-53.935,-27.161,-53.852,-27.156,-53.8,-27.165,-53.719,-27.158,-53.671,-27.169,-53.66,-27.201,-53.616,-27.21,-53.54,-27.198,-53.498,-27.172,-53.49,-27.133,-53.457,-27.103,-53.398,-27.082,-53.347,-27.081,-53.305,-27.098,-53.263,-27.163,-53.227,-27.174,-53.184,-27.149,-53.134,-27.14,-53.104,-27.154,-53.071,-27.142,-53.03,-27.138,-52.994,-27.163,-52.953,-27.179,-52.9,-27.177,-52.826,-27.189,-52.742,-27.246,-52.636,-27.259,-52.511,-27.23,-52.415,-27.236,-52.348,-27.276,-52.304,-27.283,-52.265,-27.277,-52.229,-27.293,-52.192,-27.281,-52.139,-27.283,-52.06,-27.313,-51.984,-27.377,-51.913,-27.473,-51.873,-27.509,-51.754,-27.512,-51.702,-27.488,-51.645,-27.5,-51.582,-27.547,-51.53,-27.569,-51.488,-27.564,-51.406,-27.613,-51.287,-27.716,-51.19,-27.781,-51.117,-27.808,-50.966,-27.959,-50.736,-28.236,-50.578,-28.386,-50.447,-28.422,-50.233,-28.469,-50.155,-28.474,-50.141,-28.453,-50.069,-28.444,-49.939,-28.448,-49.839,-28.465,-49.737,-28.511,-49.708,-28.53,-49.699,-28.597,-49.797,-28.624,-49.846,-28.695,-49.954,-28.794,-49.945,-28.95,-50.007,-29.102,-50.07,-29.093,-50.164,-29.195,-50.168,-29.285,-50.052,-29.356,-50.034,-29.32,-50.079,-29.244,-49.954,-29.195,-49.869,-29.218,-49.815,-29.271,-49.713,-29.325]},{"z":3,"b":[-38.221,-9.356,-35.15,-8.834],"l":[-38.221,-9.348,-38.155,-9.267,-38.092,-9.186,-37.967,-9.142,-37.815,-8.977,-37.806,-8.896,-37.748,-8.861,-37.681,-8.977,-37.628,-8.981,-37.521,-8.945,-37.471,-9.003,-37.391,-9.044,-37.181,-9.24,-36.998,-9.307,-36.94,-9.356,-36.905,-9.289,-36.833,-9.262,-36.579,-9.294,-36.44,-9.209,-36.346,-9.2,-36.266,-9.142,-36.239,-9.088,-36.123,-9.017,-36.119,-8.968,-36.016,-8.896,-35.971,-8.905,-35.882,-8.874,-35.802,-8.869,-35.721,-8.919,-35.61,-8.865,-35.471,-8.834,-35.391,-8.883,-35.302,-8.887,-35.15,-8.914]},{"z":3,"b":[-40.538,-7.856,-38.699,-7.347],"l":[-38.699,-7.619,-38.82,-7.664,-38.864,-7.704,-38.967,-7.847,-38.998,-7.82,-39.074,-7.856,-39.11,-7.753,-39.244,-7.682,-39.306,-7.624,-39.351,-7.548,-39.534,-7.477,-39.655,-7.374,-39.846,-7.347,-39.954,-7.361,-40.065,-7.405,-40.172,-7.419,-40.262,-7.392,-40.538,-7.392]},{"z":3,"b":[-38.699,-8.285,-34.81,-7.271],"l":[-34.81,-7.567,-34.98,-7.512,-35.065,-7.41,-35.271,-7.383,-35.373,-7.459,-35.498,-7.454,-35.556,-7.655,-35.681,-7.713,-35.713,-7.709,-35.837,-7.744,-35.887,-7.732,-35.931,-7.838,-35.967,-7.816,-36.074,-7.825,-36.123,-7.78,-36.168,-7.825,-36.217,-7.78,-36.266,-7.825,-36.338,-7.811,-36.422,-7.825,-36.449,-7.91,-36.57,-7.923,-36.659,-8.012,-36.628,-8.084,-36.784,-8.222,-36.833,-8.227,-36.963,-8.285,-37.061,-8.231,-37.128,-8.164,-37.15,-7.977,-37.213,-7.959,-37.342,-7.999,-37.226,-7.816,-37.168,-7.762,-37.213,-7.646,-37.199,-7.575,-37.016,-7.508,-37.016,-7.401,-37.15,-7.347,-37.253,-7.271,-37.346,-7.298,-37.382,-7.352,-37.436,-7.347,-37.552,-7.477,-37.708,-7.548,-37.793,-7.637,-37.896,-7.686,-37.976,-7.771,-38.057,-7.758,-38.088,-7.82,-38.15,-7.771,-38.168,-7.807,-38.226,-7.811,-38.257,-7.852,-38.302,-7.829,-38.304,-7.782,-38.355,-7.7,-38.418,-7.749,-38.449,-7.736,-38.525,-7.767,-38.588,-7.74,-38.592,-7.691,-38.646,-7.677,-38.699,-7.619]},{"z":3,"b":[-41.369,-8.713,-40.53,-7.392],"l":[-40.538,-7.392,-40.646,-7.401,-40.69,-7.427,-40.69,-7.512,-40.619,-7.637,-40.659,-7.762,-40.547,-7.834,-40.53,-7.905,-40.543,-8.03,-40.592,-8.124,-40.748,-8.244,-40.833,-8.365,-40.887,-8.352,-40.922,-8.432,-41.016,-8.419,-41.083,-8.526,-41.159,-8.548,-41.213,-8.633,-41.369,-8.713]},{"z":3,"b":[-46.271,-14.869,-45.913,-12.798],"l":[-46.025,-14.869,-45.931,-14.749,-45.913,-14.691,-45.963,-14.535,-45.963,-14.468,-45.922,-14.352,-46.012,-14.298,-46.038,-14.253,-46.128,-14.182,-46.172,-14.075,-46.222,-13.999,-46.262,-13.869,-46.271,-13.655,-46.186,-13.405,-46.097,-13.352,-46.083,-13.253,-46.177,-13.218,-46.181,-13.16,-46.15,-13.026,-46.074,-12.977,-46.088,-12.914,-46.235,-12.798]},{"z":3,"b":[-48.28,-16.044,-47.302,-15.49],"l":[-47.302,-16.039,-47.855,-16.044,-48.181,-16.039,-48.253,-16.03,-48.235,-15.959,-48.28,-15.838,-48.204,-15.736,-48.239,-15.691,-48.181,-15.49,-47.766,-15.49,-47.422,-15.499,-47.413,-15.535,-47.306,-15.588,-47.306,-15.709,-47.351,-15.834,-47.36,-15.986,-47.302,-16.039]},{"z":3,"b":[-53.056,-19.511,-50.964,-18.017],"l":[-50.964,-19.511,-51.034,-19.369,-51.137,-19.285,-51.311,-19.253,-51.405,-19.169,-51.543,-19.137,-51.641,-19.128,-51.842,-19.048,-51.913,-18.99,-52.061,-18.945,-52.097,-18.896,-52.271,-18.811,-52.342,-18.816,-52.498,-18.704,-52.614,-18.722,-52.762,-18.709,-52.869,-18.682,-52.891,-18.637,-52.847,-18.535,-52.788,-18.463,-52.784,-18.392,-52.909,-18.347,-52.989,-18.387,-53.034,-18.356,-53.056,-18.294,-53.043,-18.097,-53.052,-18.017]},{"z":3,"b":[-50.965,-19.513,-46.025,-14.713],"l":[-46.025,-14.869,-46.079,-14.923,-46.19,-14.936,-46.297,-14.91,-46.315,-14.847,-46.387,-14.771,-46.516,-14.713,-46.583,-14.794,-46.556,-14.869,-46.579,-14.914,-46.547,-15.039,-46.637,-15.07,-46.735,-15.017,-46.833,-15.012,-46.891,-15.048,-46.931,-15.227,-46.891,-15.24,-46.855,-15.325,-46.936,-15.432,-46.931,-15.544,-46.869,-15.593,-46.833,-15.843,-46.864,-15.883,-47.079,-15.936,-47.128,-15.923,-47.23,-16.035,-47.302,-16.039,-47.338,-16.146,-47.324,-16.227,-47.351,-16.303,-47.431,-16.41,-47.454,-16.494,-47.405,-16.57,-47.266,-16.66,-47.159,-16.919,-47.15,-16.977,-47.23,-17.026,-47.213,-17.075,-47.311,-17.142,-47.422,-17.271,-47.454,-17.347,-47.498,-17.329,-47.53,-17.454,-47.458,-17.526,-47.409,-17.499,-47.288,-17.548,-47.271,-17.669,-47.329,-17.744,-47.355,-17.825,-47.347,-17.878,-47.28,-18.061,-47.319,-18.083,-47.447,-18.171,-47.786,-18.379,-47.929,-18.445,-48.018,-18.434,-48.227,-18.34,-48.954,-18.326,-49.108,-18.388,-49.179,-18.405,-49.247,-18.468,-49.248,-18.469,-49.248,-18.47,-49.249,-18.471,-49.25,-18.472,-49.251,-18.473,-49.252,-18.475,-49.254,-18.476,-49.255,-18.478,-49.257,-18.481,-49.258,-18.483,-49.26,-18.485,-49.262,-18.488,-49.264,-18.491,-49.267,-18.494,-49.269,-18.497,-49.271,-18.5,-49.274,-18.503,-49.276,-18.506,-49.279,-18.509,-49.281,-18.513,-49.284,-18.516,-49.287,-18.52,-49.289,-18.523,-49.292,-18.527,-49.295,-18.53,-49.297,-18.534,-49.3,-18.537,-49.303,-18.541,-49.305,-18.544,-49.308,-18.547,-49.31,-18.55,-49.313,-18.554,-49.315,-18.557,-49.317,-18.56,-49.32,-18.563,-49.322,-18.565,-49.324,-18.568,-49.326,-18.57,-49.327,-18.573,-49.329,-18.575,-49.33,-18.577,-49.332,-18.579,-49.333,-18.58,-49.334,-18.582,-49.335,-18.583,-49.336,-18.584,-49.337,-18.585,-49.417,-18.603,-49.488,-18.522,-49.553,-18.51,-49.612,-18.567,-49.678,-18.6,-49.789,-18.615,-49.96,-18.616,-50.094,-18.641,-50.247,-18.69,-50.368,-18.769,-50.458,-18.877,-50.499,-18.953,-50.49,-19.02,-50.532,-19.087,-50.578,-19.112,-50.642,-19.118,-50.713,-19.161,-50.792,-19.238,-50.844,-19.311,-50.869,-19.377,-50.871,-19.424,-50.864,-19.438,-50.881,-19.459,-50.924,-19.464,-50.964,-19.511,-50.965,-19.513]},{"z":3,"b":[-44.864,-23.294,-44.168,-22.405],"l":[-44.815,-23.294,-44.864,-23.204,-44.806,-23.137,-44.797,-22.99,-44.726,-22.936,-44.588,-22.878,-44.472,-22.852,-44.36,-22.856,-44.275,-22.82,-44.248,-22.749,-44.168,-22.7,-44.217,-22.593,-44.538,-22.619,-44.637,-22.602,-44.65,-22.561,-44.771,-22.423,-44.833,-22.405]},{"z":3,"b":[-46.57,-12.798,-45.757,-10.329],"l":[-46.235,-12.798,-46.235,-12.713,-46.159,-12.602,-46.159,-12.503,-46.315,-12.423,-46.347,-12.343,-46.338,-12.128,-46.324,-12.093,-46.248,-12.048,-46.213,-11.999,-46.079,-11.927,-46.101,-11.865,-46.159,-11.834,-46.248,-11.843,-46.275,-11.767,-46.244,-11.727,-46.105,-11.664,-46.083,-11.602,-46.19,-11.544,-46.427,-11.499,-46.489,-11.414,-46.552,-11.378,-46.57,-11.316,-46.53,-11.236,-46.463,-11.177,-46.369,-10.968,-46.248,-10.914,-46.235,-10.883,-46.302,-10.758,-46.088,-10.584,-46.038,-10.57,-45.86,-10.468,-45.757,-10.329]},{"z":3,"b":[-45.945,-10.329,-45.757,-10.316],"l":[-45.757,-10.329,-45.945,-10.316]},{"z":3,"b":[-50.485,-13.383,-46.235,-12.388],"l":[-50.485,-12.843,-50.392,-12.617,-50.296,-12.49,-50.164,-12.388,-50.137,-12.477,-50.204,-12.57,-50.199,-12.655,-50.275,-12.789,-50.271,-12.892,-50.239,-12.932,-50.052,-13.021,-49.972,-13.026,-49.913,-13.07,-49.686,-13.177,-49.521,-13.2,-49.382,-13.249,-49.36,-13.133,-49.297,-13.003,-49.137,-12.736,-49.016,-12.655,-48.998,-12.749,-48.927,-12.785,-48.824,-12.874,-48.797,-12.928,-48.802,-12.999,-48.766,-13.097,-48.762,-13.209,-48.775,-13.352,-48.739,-13.383,-48.686,-13.338,-48.655,-13.177,-48.588,-13.155,-48.48,-13.195,-48.369,-13.186,-48.226,-13.093,-48.164,-13.103,-48.175,-13.181,-48.074,-13.195,-48.003,-13.249,-47.936,-13.244,-47.855,-13.294,-47.681,-13.356,-47.659,-13.32,-47.722,-13.151,-47.641,-13.115,-47.556,-13.244,-47.44,-13.249,-47.293,-13.2,-47.092,-13.097,-46.972,-13.07,-46.829,-13.07,-46.753,-13.035,-46.646,-12.923,-46.592,-12.892,-46.436,-12.847,-46.302,-12.834,-46.235,-12.798]},{"z":3,"b":[-38.244,-11.557,-37.388,-9.512],"l":[-37.388,-11.497,-37.574,-11.539,-37.632,-11.521,-37.659,-11.557,-37.797,-11.526,-37.851,-11.441,-37.967,-11.401,-38.007,-11.356,-37.98,-11.24,-38.061,-11.169,-38.11,-11.03,-38.177,-10.981,-38.235,-10.896,-38.244,-10.825,-38.19,-10.718,-38.11,-10.709,-38.012,-10.753,-37.971,-10.753,-37.815,-10.677,-37.784,-10.628,-37.824,-10.548,-37.838,-10.423,-37.784,-10.307,-37.78,-10.079,-37.9,-9.95,-37.9,-9.914,-37.963,-9.874,-38.03,-9.727,-37.994,-9.646,-38.043,-9.606,-37.976,-9.512]},{"z":3,"b":[-53.663,-26.7,-48.581,-25.972],"l":[-48.581,-25.985,-48.65,-25.972,-48.918,-25.977,-48.945,-26.008,-49.11,-25.994,-49.213,-26.03,-49.297,-26.106,-49.454,-26.169,-49.489,-26.222,-49.597,-26.222,-49.722,-26.16,-49.753,-26.124,-49.882,-26.039,-49.949,-26.012,-50.19,-26.053,-50.315,-26.053,-50.369,-26.086,-50.458,-26.026,-50.543,-26.026,-50.646,-26.07,-50.735,-26.231,-50.771,-26.222,-50.9,-26.28,-50.945,-26.244,-51.074,-26.236,-51.248,-26.347,-51.288,-26.423,-51.28,-26.499,-51.239,-26.606,-51.284,-26.651,-51.422,-26.7,-51.503,-26.602,-51.873,-26.602,-52.007,-26.584,-52.199,-26.45,-52.458,-26.432,-52.543,-26.401,-52.641,-26.401,-52.672,-26.378,-52.815,-26.338,-52.913,-26.365,-52.994,-26.352,-53.123,-26.369,-53.28,-26.262,-53.355,-26.24,-53.458,-26.289,-53.516,-26.289,-53.663,-26.258]},{"z":3,"b":[-63.394,-1.449,-58.869,2.224],"l":[-63.394,2.224,-63.306,2.166,-63.15,2.18,-63.123,2.113,-63.003,2.019,-62.847,2.019,-62.708,1.948,-62.69,1.912,-62.73,1.831,-62.713,1.738,-62.789,1.604,-62.722,1.497,-62.623,1.416,-62.614,1.363,-62.516,1.059,-62.512,0.961,-62.445,0.805,-62.534,0.733,-62.539,0.675,-62.485,0.541,-62.489,0.488,-62.53,0.43,-62.534,0.314,-62.57,0.247,-62.565,0.175,-62.525,0.095,-62.579,0.019,-62.57,-0.048,-62.512,-0.124,-62.476,-0.222,-62.405,-0.271,-62.373,-0.343,-62.369,-0.468,-62.32,-0.521,-62.311,-0.642,-62.378,-0.718,-62.476,-0.695,-62.507,-0.771,-62.418,-0.829,-62.315,-0.945,-62.244,-0.981,-62.204,-1.044,-62.141,-1.066,-62.03,-1.146,-61.936,-1.244,-61.878,-1.369,-61.881,-1.389,-61.799,-1.389,-61.717,-1.401,-61.62,-1.449,-61.619,-1.276,-61.574,-1.142,-61.561,-1.021,-61.579,-0.945,-61.534,-0.727,-61.458,-0.637,-61.244,-0.548,-61.222,-0.499,-61.105,-0.494,-61.061,-0.53,-60.931,-0.557,-60.905,-0.611,-60.815,-0.686,-60.735,-0.852,-60.641,-0.861,-60.516,-0.829,-60.467,-0.74,-60.338,-0.704,-60.306,-0.673,-60.315,-0.619,-60.391,-0.521,-60.387,-0.463,-60.315,-0.307,-60.311,-0.236,-60.253,-0.151,-60.217,-0.044,-60.168,0.006,-60.128,0.131,-60.025,0.224,-59.771,0.229,-58.869,0.224]},{"z":3,"b":[-58.974,0.224,-58.869,1.386],"l":[-58.974,1.386,-58.869,0.224]},{"z":3,"b":[-73.797,-9.838,-66.828,-7.117],"l":[-73.797,-7.117,-72.664,-7.593,-70.373,-8.155,-69.806,-8.454,-66.829,-9.838,-66.828,-9.838]},{"z":3,"b":[-61.61,-13.663,-59.824,-8.767],"l":[-61.61,-8.767,-61.507,-8.847,-61.485,-8.914,-61.53,-8.999,-61.556,-9.128,-61.53,-9.227,-61.592,-9.24,-61.61,-9.32,-61.547,-9.41,-61.552,-9.463,-61.48,-9.637,-61.521,-9.704,-61.565,-9.727,-61.521,-9.861,-61.534,-9.994,-61.57,-10.061,-61.565,-10.262,-61.472,-10.436,-61.503,-10.686,-61.472,-10.758,-61.521,-10.789,-61.507,-10.878,-61.521,-10.954,-61.472,-10.999,-60.44,-11.003,-60.44,-11.039,-60.382,-11.102,-60.28,-11.079,-60.177,-11.119,-60.07,-11.115,-60.003,-11.146,-59.909,-11.383,-60.025,-11.535,-60.101,-11.602,-60.101,-11.744,-60.061,-11.905,-59.985,-11.919,-59.931,-12.052,-59.887,-12.128,-59.891,-12.244,-59.824,-12.387,-59.909,-12.619,-60.003,-12.731,-60.043,-12.874,-60.083,-12.928,-60.195,-12.972,-60.271,-13.07,-60.275,-13.137,-60.351,-13.271,-60.382,-13.419,-60.659,-13.602,-60.725,-13.663]},{"z":3,"b":[-38.605,-6.396,-37.158,-4.799],"l":[-38.525,-6.383,-38.605,-6.396,-38.579,-6.262,-38.512,-6.182,-38.494,-6.128,-38.413,-6.061,-38.369,-6.093,-38.302,-6.075,-38.128,-5.878,-38.079,-5.762,-38.079,-5.686,-38.047,-5.615,-37.913,-5.463,-37.721,-5.061,-37.596,-4.959,-37.543,-4.932,-37.158,-4.799]},{"z":3,"b":[-38.525,-6.977,-34.92,-6.026],"l":[-38.525,-6.383,-38.436,-6.414,-38.284,-6.503,-38.239,-6.486,-38.132,-6.521,-38.056,-6.445,-38.003,-6.432,-37.842,-6.343,-37.793,-6.294,-37.757,-6.177,-37.641,-6.128,-37.476,-6.084,-37.422,-6.097,-37.262,-6.026,-37.177,-6.052,-37.177,-6.115,-37.396,-6.392,-37.431,-6.517,-37.507,-6.552,-37.516,-6.682,-37.338,-6.7,-37.271,-6.74,-37.217,-6.825,-37.155,-6.785,-37.043,-6.753,-36.989,-6.709,-36.945,-6.749,-36.793,-6.767,-36.744,-6.825,-36.771,-6.932,-36.713,-6.977,-36.659,-6.932,-36.574,-6.927,-36.503,-6.785,-36.53,-6.736,-36.534,-6.637,-36.445,-6.624,-36.516,-6.481,-36.494,-6.378,-36.382,-6.302,-36.293,-6.307,-36.253,-6.414,-35.927,-6.468,-35.721,-6.445,-35.668,-6.427,-35.467,-6.463,-35.306,-6.53,-35.257,-6.508,-35.047,-6.535,-34.92,-6.481]},{"z":3,"b":[-41.864,-21.276,-40.967,-20.776],"l":[-41.864,-20.776,-41.744,-20.82,-41.713,-20.977,-41.73,-21.044,-41.713,-21.111,-41.44,-21.2,-41.391,-21.186,-41.266,-21.231,-41.07,-21.213,-40.967,-21.276]},{"z":3,"b":[-45.757,-10.896,-41.369,-8.713],"l":[-45.757,-10.329,-45.605,-10.334,-45.516,-10.414,-45.472,-10.486,-45.44,-10.619,-45.315,-10.78,-45.043,-10.896,-44.931,-10.861,-44.851,-10.878,-44.784,-10.856,-44.753,-10.785,-44.646,-10.736,-44.619,-10.686,-44.538,-10.628,-44.418,-10.588,-44.311,-10.602,-44.217,-10.628,-44.119,-10.588,-43.994,-10.454,-43.913,-10.432,-43.802,-10.204,-43.762,-10.169,-43.753,-10.106,-43.704,-10.035,-43.713,-9.95,-43.681,-9.802,-43.726,-9.749,-43.784,-9.566,-43.829,-9.499,-43.811,-9.427,-43.695,-9.445,-43.659,-9.365,-43.605,-9.338,-43.538,-9.361,-43.449,-9.302,-43.415,-9.336,-43.378,-9.414,-43.186,-9.419,-43.128,-9.374,-43.043,-9.396,-43.03,-9.436,-42.937,-9.474,-42.931,-9.517,-42.851,-9.548,-42.753,-9.521,-42.619,-9.566,-42.574,-9.486,-42.489,-9.499,-42.431,-9.41,-42.311,-9.307,-42.15,-9.294,-42.043,-9.209,-41.918,-9.213,-41.851,-9.253,-41.797,-9.173,-41.735,-9.133,-41.735,-8.981,-41.61,-8.963,-41.556,-8.972,-41.498,-8.936,-41.467,-8.865,-41.4,-8.785,-41.369,-8.713]},{"z":3,"b":[-41.449,-7.392,-40.418,-2.869],"l":[-40.538,-7.392,-40.534,-7.325,-40.583,-7.195,-40.512,-7.003,-40.44,-6.905,-40.418,-6.811,-40.48,-6.736,-40.614,-6.709,-40.708,-6.66,-40.722,-6.57,-40.771,-6.494,-40.797,-6.378,-40.806,-6.253,-40.891,-6.012,-40.891,-5.905,-40.922,-5.682,-40.918,-5.602,-40.949,-5.419,-41.025,-5.365,-41.07,-5.307,-41.061,-5.169,-41.114,-5.066,-41.123,-5.008,-41.186,-4.914,-41.181,-4.811,-41.226,-4.727,-41.244,-4.615,-41.235,-4.539,-41.141,-4.427,-41.074,-4.325,-41.119,-4.177,-41.181,-4.124,-41.23,-4.044,-41.284,-3.811,-41.355,-3.709,-41.382,-3.593,-41.449,-3.436,-41.427,-3.32,-41.378,-3.271,-41.257,-3.079,-41.273,-2.869]},{"z":3,"b":[-38.73,-7.619,-38.525,-6.383],"l":[-38.699,-7.619,-38.637,-7.535,-38.632,-7.459,-38.583,-7.427,-38.534,-7.294,-38.547,-7.236,-38.601,-7.222,-38.677,-7.16,-38.686,-7.03,-38.73,-7.003,-38.726,-6.892,-38.65,-6.838,-38.619,-6.758,-38.65,-6.691,-38.579,-6.481,-38.525,-6.383]},{"z":3,"b":[-54.793,-1.177,-51.888,2.511],"l":[-54.682,2.511,-54.735,2.416,-54.717,2.265,-54.788,2.131,-54.753,2.081,-54.793,2.01,-54.762,1.97,-54.766,1.898,-54.735,1.773,-54.579,1.782,-54.498,1.747,-54.364,1.76,-54.204,1.657,-54.172,1.657,-54.083,1.541,-54.079,1.506,-54.007,1.523,-53.922,1.461,-53.891,1.407,-53.757,1.394,-53.739,1.434,-53.659,1.421,-53.65,1.363,-53.556,1.367,-53.538,1.242,-53.44,1.26,-53.409,1.184,-53.467,1.153,-53.418,0.943,-53.266,0.782,-53.132,0.751,-53.114,0.72,-53.141,0.537,-53.128,0.385,-53.043,0.247,-53.007,0.135,-53.016,0.055,-52.976,-0.017,-52.913,-0.191,-52.829,-0.182,-52.699,-0.302,-52.628,-0.396,-52.632,-0.557,-52.605,-0.611,-52.525,-0.646,-52.507,-0.731,-52.516,-0.874,-52.4,-0.869,-52.369,-0.923,-52.391,-0.954,-52.36,-1.07,-52.239,-1.146,-52.132,-1.151,-52.052,-1.177,-51.985,-1.142,-51.888,-1.16]},{"z":3,"b":[-58.869,-7.338,-56.391,0.224],"l":[-58.869,0.224,-58.851,-0.084,-58.864,-0.173,-58.855,-0.352,-58.739,-0.432,-58.748,-0.624,-58.704,-0.695,-58.628,-0.767,-58.57,-0.762,-58.44,-0.861,-58.391,-1.048,-58.315,-1.119,-58.253,-1.128,-58.159,-1.235,-58.083,-1.298,-57.98,-1.347,-57.9,-1.423,-57.829,-1.445,-57.655,-1.588,-57.565,-1.624,-57.449,-1.691,-57.338,-1.731,-57.275,-1.713,-57.248,-1.758,-57.168,-1.771,-57.088,-1.811,-57.007,-1.936,-56.824,-2.035,-56.744,-2.052,-56.748,-2.177,-56.637,-2.222,-56.498,-2.16,-56.485,-2.249,-56.401,-2.336,-56.391,-2.392,-56.757,-3.186,-58.293,-6.499,-58.375,-6.57,-58.437,-6.676,-58.455,-6.794,-58.386,-6.949,-58.228,-7.142,-58.146,-7.263,-58.137,-7.338]},{"z":3,"b":[-58.137,-9.843,-50.235,-7.338],"l":[-50.235,-9.843,-51.297,-9.789,-55.092,-9.566,-56.463,-9.468,-56.489,-9.468,-56.677,-9.378,-56.771,-9.396,-56.838,-9.271,-56.927,-9.244,-57.047,-9.231,-57.074,-9.186,-57.083,-9.079,-57.101,-9.052,-57.297,-8.959,-57.48,-8.794,-57.579,-8.758,-57.641,-8.615,-57.65,-8.499,-57.681,-8.436,-57.641,-8.231,-57.78,-8.048,-57.896,-7.691,-57.949,-7.619,-57.98,-7.53,-58.061,-7.41,-58.137,-7.338]},{"z":3,"b":[-41.882,-20.776,-40.217,-17.892],"l":[-41.864,-20.776,-41.882,-20.758,-41.811,-20.646,-41.851,-20.624,-41.797,-20.535,-41.797,-20.428,-41.855,-20.369,-41.717,-20.209,-41.405,-20.213,-41.364,-20.186,-41.306,-19.954,-41.186,-19.865,-41.159,-19.66,-41.03,-19.548,-41.047,-19.49,-40.958,-19.472,-40.927,-19.294,-40.945,-19.146,-41.052,-19.035,-41.025,-18.981,-41.105,-18.887,-41.163,-18.905,-41.235,-18.852,-41.195,-18.807,-41.092,-18.829,-40.931,-18.807,-40.927,-18.695,-41.03,-18.637,-41.003,-18.454,-41.025,-18.405,-41.128,-18.334,-41.141,-18.298,-41.061,-18.177,-41.016,-18.173,-40.913,-18.102,-40.838,-18.151,-40.766,-18.137,-40.775,-18.093,-40.909,-17.972,-40.788,-17.972,-40.726,-17.945,-40.668,-17.954,-40.534,-17.901,-40.476,-17.928,-40.427,-17.892,-40.217,-17.972]},{"z":3,"b":[-40.217,-17.977,-40.208,-17.972],"l":[-40.217,-17.972,-40.208,-17.977]},{"z":3,"b":[-61.61,-8.767,-58.137,-7.338],"l":[-61.61,-8.767,-58.547,-8.749,-58.472,-8.691,-58.427,-8.523,-58.397,-8.416,-58.354,-8.276,-58.329,-8.051,-58.354,-7.906,-58.33,-7.78,-58.259,-7.673,-58.224,-7.575,-58.224,-7.485,-58.202,-7.415,-58.137,-7.338]},{"z":3,"b":[-53.065,-18.021,-53.052,-18.017],"l":[-53.052,-18.017,-53.065,-18.021]},{"z":3,"b":[-40.208,-18.325,-39.672,-17.977],"l":[-40.208,-17.977,-40.186,-18.008,-39.672,-18.325]},{"z":3,"b":[-50.717,-12.843,-50.237,-9.846],"l":[-50.485,-12.843,-50.566,-12.82,-50.614,-12.763,-50.655,-12.667,-50.66,-12.558,-50.629,-12.437,-50.632,-12.289,-50.668,-12.112,-50.675,-11.99,-50.653,-11.922,-50.661,-11.854,-50.717,-11.749,-50.666,-11.652,-50.671,-11.574,-50.717,-11.483,-50.708,-11.346,-50.645,-11.163,-50.61,-11.001,-50.599,-10.69,-50.551,-10.567,-50.455,-10.388,-50.403,-10.258,-50.395,-10.177,-50.337,-10.034,-50.237,-9.846]},{"z":3,"b":[-48.73,-5.356,-46.006,-1.146],"l":[-48.722,-5.356,-48.73,-5.338,-48.253,-4.954,-47.802,-4.597,-47.655,-4.606,-47.583,-4.548,-47.489,-4.423,-47.458,-4.338,-47.382,-4.276,-47.333,-4.164,-47.311,-4.066,-47.288,-4.079,-47.065,-3.843,-47.025,-3.597,-47.03,-3.57,-46.967,-3.526,-46.945,-3.396,-46.86,-3.329,-46.824,-3.329,-46.744,-3.191,-46.668,-3.093,-46.637,-2.986,-46.65,-2.914,-46.574,-2.847,-46.668,-2.74,-46.601,-2.664,-46.507,-2.615,-46.485,-2.548,-46.427,-2.512,-46.436,-2.41,-46.409,-2.387,-46.413,-2.24,-46.378,-2.253,-46.28,-2.142,-46.213,-1.927,-46.217,-1.807,-46.302,-1.802,-46.315,-1.731,-46.244,-1.722,-46.199,-1.677,-46.181,-1.575,-46.199,-1.486,-46.123,-1.347,-46.159,-1.316,-46.141,-1.24,-46.006,-1.146]},{"z":3,"b":[-45.98,-10.316,-41.815,-2.679],"l":[-45.945,-10.316,-45.936,-10.213,-45.954,-10.182,-45.9,-10.084,-45.9,-10.026,-45.86,-9.999,-45.86,-9.834,-45.838,-9.535,-45.797,-9.463,-45.829,-9.374,-45.896,-9.329,-45.905,-9.177,-45.936,-9.119,-45.94,-9.012,-45.98,-8.927,-45.927,-8.789,-45.82,-8.7,-45.753,-8.561,-45.735,-8.432,-45.659,-8.311,-45.655,-8.253,-45.579,-8.151,-45.543,-7.869,-45.472,-7.673,-45.297,-7.557,-45.025,-7.494,-44.909,-7.445,-44.815,-7.365,-44.713,-7.396,-44.579,-7.249,-44.449,-7.146,-44.306,-7.111,-44.257,-7.008,-44.168,-6.923,-44.163,-6.887,-44.092,-6.807,-44.061,-6.829,-44.038,-6.767,-43.954,-6.762,-43.815,-6.709,-43.677,-6.7,-43.561,-6.753,-43.463,-6.843,-43.302,-6.802,-43.199,-6.753,-43.132,-6.78,-42.98,-6.744,-42.918,-6.669,-42.913,-6.615,-42.855,-6.481,-42.869,-6.436,-42.851,-6.253,-42.963,-6.186,-43.003,-6.124,-43.074,-6.057,-43.11,-5.771,-43.083,-5.713,-43.101,-5.624,-43.047,-5.597,-42.913,-5.392,-42.833,-5.311,-42.806,-5.16,-42.833,-5.097,-42.855,-4.936,-42.905,-4.829,-42.954,-4.776,-42.931,-4.731,-42.949,-4.66,-42.913,-4.637,-42.864,-4.499,-42.896,-4.405,-42.954,-4.392,-42.989,-4.24,-42.891,-4.142,-42.851,-4.039,-42.735,-3.923,-42.663,-3.789,-42.677,-3.7,-42.628,-3.615,-42.574,-3.57,-42.503,-3.45,-42.454,-3.477,-42.364,-3.45,-42.217,-3.432,-42.101,-3.302,-42.105,-3.262,-42.003,-3.231,-41.922,-3.111,-41.842,-3.035,-41.815,-2.936,-41.869,-2.852,-41.831,-2.679]},{"z":3,"b":[-41.369,-9.472,-38.216,-8.559],"l":[-41.369,-8.713,-41.324,-8.736,-41.159,-8.709,-41.105,-8.718,-41.101,-8.776,-41.004,-8.781,-40.98,-8.816,-40.891,-8.856,-40.891,-9.035,-40.855,-9.093,-40.851,-9.151,-40.704,-9.218,-40.69,-9.343,-40.744,-9.423,-40.686,-9.472,-40.565,-9.46,-40.413,-9.397,-40.32,-9.293,-40.273,-9.145,-40.212,-9.102,-40.078,-9.088,-40.005,-9.058,-39.949,-9.022,-39.909,-8.98,-39.888,-8.924,-39.885,-8.853,-39.835,-8.809,-39.738,-8.791,-39.688,-8.749,-39.683,-8.683,-39.601,-8.62,-39.442,-8.559,-39.333,-8.566,-39.257,-8.64,-39.098,-8.715,-38.835,-8.8,-38.705,-8.88,-38.668,-8.896,-38.619,-8.896,-38.577,-8.861,-38.544,-8.831,-38.505,-8.835,-38.484,-8.878,-38.482,-8.96,-38.433,-9.004,-38.336,-9.01,-38.294,-9.043,-38.308,-9.102,-38.283,-9.204,-38.221,-9.348,-38.216,-9.351]},{"z":3,"b":[-46.074,-17.977,-39.864,-14.24],"l":[-40.208,-17.977,-40.23,-17.919,-40.186,-17.816,-40.208,-17.767,-40.284,-17.718,-40.382,-17.624,-40.413,-17.557,-40.48,-17.557,-40.498,-17.419,-40.61,-17.392,-40.605,-17.311,-40.565,-17.262,-40.574,-17.133,-40.525,-16.932,-40.471,-16.869,-40.297,-16.878,-40.244,-16.834,-40.284,-16.744,-40.275,-16.615,-40.253,-16.566,-40.137,-16.544,-40.132,-16.503,-40.056,-16.396,-39.94,-16.311,-39.864,-16.111,-39.936,-15.999,-40.003,-15.994,-40.132,-15.892,-40.168,-15.896,-40.235,-15.82,-40.329,-15.825,-40.476,-15.776,-40.534,-15.798,-40.574,-15.758,-40.668,-15.718,-40.762,-15.74,-40.829,-15.682,-40.891,-15.695,-40.945,-15.673,-41.03,-15.736,-41.159,-15.78,-41.333,-15.718,-41.36,-15.494,-41.802,-15.111,-41.94,-15.173,-42.088,-15.182,-42.177,-15.106,-42.262,-15.106,-42.431,-15.035,-42.579,-14.932,-42.632,-14.941,-42.891,-14.749,-42.954,-14.677,-43.23,-14.637,-43.387,-14.7,-43.449,-14.78,-43.494,-14.789,-43.708,-14.736,-43.824,-14.695,-43.864,-14.66,-43.879,-14.625,-43.879,-14.624,-43.879,-14.623,-43.879,-14.622,-43.879,-14.621,-43.878,-14.62,-43.878,-14.618,-43.878,-14.617,-43.878,-14.615,-43.877,-14.613,-43.877,-14.611,-43.876,-14.609,-43.876,-14.607,-43.875,-14.605,-43.875,-14.603,-43.874,-14.6,-43.874,-14.598,-43.873,-14.595,-43.873,-14.593,-43.872,-14.59,-43.872,-14.588,-43.871,-14.585,-43.871,-14.582,-43.87,-14.579,-43.87,-14.577,-43.869,-14.574,-43.868,-14.571,-43.868,-14.569,-43.867,-14.566,-43.867,-14.564,-43.866,-14.561,-43.866,-14.559,-43.865,-14.556,-43.865,-14.554,-43.864,-14.552,-43.864,-14.549,-43.864,-14.547,-43.863,-14.546,-43.863,-14.544,-43.862,-14.542,-43.862,-14.541,-43.862,-14.539,-43.862,-14.538,-43.861,-14.537,-43.861,-14.536,-43.861,-14.535,-43.861,-14.534,-43.8,-14.37,-43.781,-14.343,-43.838,-14.316,-44.159,-14.271,-44.213,-14.24,-44.32,-14.244,-44.36,-14.271,-44.561,-14.347,-44.838,-14.517,-44.882,-14.597,-45.101,-14.718,-45.226,-14.74,-45.32,-14.856,-45.463,-14.941,-45.552,-14.936,-45.655,-15.044,-45.713,-15.124,-45.766,-15.146,-45.927,-15.128,-45.967,-15.186,-46.074,-15.249,-45.976,-14.986,-46.025,-14.869]},{"z":3,"b":[-50.994,-22.892,-44.833,-19.791],"l":[-44.833,-22.405,-44.922,-22.45,-45.056,-22.468,-45.244,-22.561,-45.271,-22.606,-45.409,-22.646,-45.471,-22.612,-45.53,-22.651,-45.583,-22.615,-45.63,-22.623,-45.672,-22.619,-45.802,-22.7,-45.744,-22.727,-45.744,-22.798,-45.793,-22.852,-45.86,-22.861,-45.909,-22.82,-46.021,-22.874,-46.146,-22.892,-46.155,-22.847,-46.288,-22.883,-46.36,-22.843,-46.369,-22.749,-46.472,-22.682,-46.396,-22.615,-46.454,-22.517,-46.543,-22.472,-46.547,-22.441,-46.641,-22.41,-46.699,-22.32,-46.663,-22.204,-46.601,-22.133,-46.659,-22.053,-46.619,-21.986,-46.659,-21.905,-46.668,-21.811,-46.628,-21.771,-46.605,-21.682,-46.561,-21.673,-46.494,-21.526,-46.507,-21.459,-46.605,-21.428,-46.655,-21.374,-46.686,-21.396,-46.815,-21.361,-46.896,-21.405,-47.003,-21.401,-46.994,-21.343,-47.056,-21.195,-47.123,-21.133,-47.146,-20.981,-47.222,-20.914,-47.213,-20.807,-47.11,-20.633,-47.155,-20.526,-47.253,-20.477,-47.293,-20.423,-47.266,-20.285,-47.235,-20.204,-47.244,-20.174,-47.329,-20.118,-47.428,-20.036,-47.46,-20.001,-47.518,-19.994,-47.594,-20.019,-47.656,-20.023,-47.696,-19.991,-47.75,-19.977,-47.819,-19.981,-47.86,-20.012,-47.862,-20.056,-47.909,-20.082,-47.991,-20.083,-48.051,-20.12,-48.109,-20.117,-48.178,-20.083,-48.253,-20.093,-48.364,-20.138,-48.716,-20.144,-48.812,-20.159,-48.858,-20.233,-48.887,-20.37,-48.918,-20.429,-48.952,-20.41,-48.978,-20.338,-48.995,-20.214,-49.04,-20.187,-49.113,-20.259,-49.166,-20.295,-49.2,-20.294,-49.239,-20.246,-49.284,-20.149,-49.288,-20.074,-49.259,-20.017,-49.278,-19.987,-49.377,-19.978,-50.364,-19.859,-50.434,-19.8,-50.489,-19.791,-50.584,-19.821,-50.994,-20.102]},{"z":3,"b":[-53.119,-22.642,-50.994,-20.102],"l":[-50.994,-20.102,-51.064,-20.223,-51.161,-20.307,-51.397,-20.442,-51.534,-20.558,-51.593,-20.666,-51.619,-20.807,-51.692,-20.944,-51.811,-21.077,-51.866,-21.183,-51.857,-21.26,-51.873,-21.33,-51.936,-21.423,-51.955,-21.469,-51.99,-21.494,-52.048,-21.511,-52.073,-21.554,-52.065,-21.623,-52.123,-21.718,-52.246,-21.84,-52.339,-21.957,-52.403,-22.069,-52.566,-22.207,-52.961,-22.455,-53.116,-22.64,-53.119,-22.642]},{"z":3,"b":[-53.116,-22.794,-51.003,-22.521],"l":[-51.003,-22.794,-51.181,-22.74,-51.355,-22.651,-51.547,-22.686,-51.641,-22.651,-51.695,-22.664,-51.744,-22.619,-51.873,-22.611,-51.985,-22.548,-52.065,-22.521,-52.141,-22.544,-52.163,-22.602,-52.217,-22.642,-52.262,-22.602,-52.315,-22.619,-52.445,-22.602,-52.521,-22.615,-52.614,-22.57,-52.695,-22.602,-52.949,-22.57,-53.045,-22.636,-53.116,-22.64]},{"z":3,"b":[-51.003,-25.297,-48.082,-22.794],"l":[-48.082,-25.297,-48.092,-25.236,-48.186,-25.191,-48.23,-25.012,-48.266,-25.035,-48.413,-24.959,-48.512,-25.084,-48.565,-25.057,-48.543,-24.811,-48.489,-24.744,-48.614,-24.682,-48.78,-24.695,-48.829,-24.664,-49.025,-24.669,-49.092,-24.686,-49.217,-24.691,-49.297,-24.664,-49.311,-24.53,-49.239,-24.419,-49.226,-24.338,-49.275,-24.316,-49.338,-24.213,-49.333,-24.142,-49.409,-24.084,-49.601,-23.865,-49.57,-23.834,-49.552,-23.713,-49.605,-23.642,-49.655,-23.508,-49.614,-23.41,-49.637,-23.258,-49.73,-23.106,-49.909,-23.035,-49.913,-22.986,-49.958,-22.963,-49.972,-22.914,-50.123,-22.941,-50.293,-22.954,-50.369,-22.914,-50.431,-22.945,-50.628,-22.923,-50.726,-22.95,-50.784,-22.945,-50.882,-22.82,-51.003,-22.794]},{"z":3,"b":[-57.753,-18.03,-53.065,-17.186],"l":[-53.065,-18.021,-53.146,-18.03,-53.306,-17.999,-53.494,-18.012,-53.605,-17.994,-53.726,-18.008,-53.869,-17.945,-53.976,-17.932,-53.989,-17.901,-53.86,-17.691,-53.753,-17.642,-53.681,-17.258,-53.744,-17.253,-53.82,-17.307,-53.949,-17.459,-54.038,-17.512,-54.083,-17.588,-54.146,-17.628,-54.235,-17.637,-54.32,-17.673,-54.391,-17.673,-54.422,-17.579,-54.525,-17.512,-54.699,-17.517,-54.78,-17.557,-54.811,-17.602,-54.9,-17.651,-55.074,-17.682,-55.181,-17.686,-55.338,-17.584,-55.534,-17.503,-55.61,-17.392,-55.788,-17.352,-55.833,-17.311,-55.985,-17.267,-56.025,-17.213,-56.101,-17.186,-56.248,-17.218,-56.329,-17.285,-56.436,-17.32,-56.489,-17.302,-56.668,-17.32,-56.745,-17.317,-56.79,-17.386,-56.888,-17.518,-56.98,-17.595,-57.08,-17.734,-57.198,-17.812,-57.37,-17.876,-57.419,-17.882,-57.472,-17.901,-57.494,-17.869,-57.601,-17.816,-57.632,-17.74,-57.753,-17.735]},{"z":2,"b":[138.002,-25.999,138.002,-16.557],"l":[138.002,-16.557,138.002,-25.999]},{"z":2,"b":[129.002,-25.999,129.002,-14.871],"l":[129.002,-25.999,129.002,-14.871]},{"z":2,"b":[129.002,-25.999,138.002,-25.999],"l":[138.002,-25.999,129.002,-25.999]},{"z":2,"b":[138.002,-28.999,141.002,-25.999],"l":[141.002,-28.999,141.002,-25.999,138.002,-25.999]},{"z":2,"b":[129.002,-31.693,129.002,-25.999],"l":[129.002,-31.693,129.002,-25.999]},{"z":2,"b":[140.966,-38.072,140.997,-34.005],"l":[140.997,-34.016,140.968,-34.005,140.966,-38.072]},{"z":2,"b":[141.002,-29.139,153.516,-28.144],"l":[141.002,-28.999,148.974,-28.999,148.993,-28.986,148.994,-28.986,148.994,-28.985,148.995,-28.984,148.996,-28.984,148.997,-28.983,148.998,-28.982,148.999,-28.981,149,-28.98,149.001,-28.979,149.003,-28.978,149.004,-28.976,149.006,-28.975,149.007,-28.973,149.009,-28.972,149.011,-28.97,149.012,-28.969,149.014,-28.967,149.016,-28.966,149.018,-28.964,149.02,-28.962,149.022,-28.96,149.024,-28.959,149.026,-28.957,149.028,-28.955,149.029,-28.953,149.031,-28.952,149.033,-28.95,149.035,-28.948,149.037,-28.947,149.039,-28.945,149.041,-28.943,149.042,-28.942,149.044,-28.94,149.046,-28.939,149.047,-28.937,149.049,-28.936,149.05,-28.935,149.051,-28.934,149.052,-28.933,149.054,-28.932,149.055,-28.931,149.055,-28.93,149.056,-28.929,149.057,-28.929,149.057,-28.928,149.058,-28.928,149.081,-28.875,149.174,-28.807,149.335,-28.723,149.433,-28.657,149.467,-28.608,149.512,-28.579,149.596,-28.566,149.677,-28.612,149.853,-28.604,150.15,-28.558,150.341,-28.564,150.47,-28.65,150.712,-28.639,150.856,-28.66,150.983,-28.71,151.05,-28.758,151.058,-28.805,151.118,-28.851,151.231,-28.898,151.303,-28.982,151.335,-29.104,151.393,-29.139,151.478,-29.087,151.528,-29.035,151.554,-28.955,151.73,-28.887,151.761,-28.945,151.824,-28.972,151.855,-28.919,151.935,-28.923,152.024,-28.874,152.069,-28.704,152.007,-28.637,151.966,-28.535,152.163,-28.436,152.216,-28.436,152.292,-28.387,152.413,-28.343,152.493,-28.262,152.57,-28.323,152.609,-28.294,152.752,-28.356,152.846,-28.325,153.118,-28.352,153.194,-28.258,153.382,-28.236,153.516,-28.144]},{"z":2,"b":[148.77,-35.914,149.404,-35.146],"l":[148.77,-35.494,148.774,-35.637,148.792,-35.709,148.85,-35.749,148.89,-35.745,148.899,-35.82,148.949,-35.887,149.051,-35.914,149.105,-35.829,149.087,-35.611,149.127,-35.584,149.149,-35.508,149.14,-35.441,149.185,-35.383,149.279,-35.347,149.368,-35.356,149.404,-35.329,149.257,-35.271,149.194,-35.218,149.19,-35.178,149.105,-35.146,148.828,-35.311,148.77,-35.494]},{"z":2,"b":[141.002,-34.043,141.026,-28.999],"l":[141.002,-28.999,141.026,-34.043]},{"z":2,"b":[140.997,-37.501,149.944,-34.016],"l":[149.944,-37.501,148.216,-36.803,148.133,-36.777,148.162,-36.73,148.197,-36.625,148.147,-36.546,148.034,-36.181,147.975,-36.046,147.931,-36.019,147.762,-35.956,147.674,-35.954,147.591,-35.982,147.538,-35.984,147.502,-35.959,147.458,-35.954,147.407,-35.971,147.407,-35.972,147.406,-35.972,147.405,-35.973,147.404,-35.974,147.404,-35.975,147.403,-35.975,147.402,-35.976,147.401,-35.977,147.4,-35.977,147.4,-35.978,147.399,-35.979,147.398,-35.98,147.397,-35.981,147.396,-35.982,147.395,-35.983,147.394,-35.984,147.393,-35.985,147.392,-35.986,147.39,-35.987,147.389,-35.987,147.388,-35.988,147.387,-35.989,147.386,-35.99,147.385,-35.991,147.384,-35.992,147.383,-35.993,147.382,-35.994,147.381,-35.995,147.38,-35.996,147.379,-35.997,147.378,-35.998,147.377,-35.998,147.377,-35.999,147.376,-36,147.375,-36,147.375,-36.001,147.374,-36.002,147.373,-36.002,147.373,-36.003,147.372,-36.003,147.372,-36.004,147.371,-36.004,147.35,-36.052,147.323,-36.069,147.272,-36.049,147.157,-36.036,147.1,-36.05,147.047,-36.106,146.896,-36.1,146.746,-36.061,146.59,-36.005,146.47,-35.994,146.403,-36.033,146.325,-36.054,146.216,-36.057,146.038,-36.017,145.815,-35.977,145.692,-35.931,145.588,-35.863,145.432,-35.835,145.431,-35.835,145.43,-35.835,145.429,-35.835,145.426,-35.835,145.424,-35.835,145.421,-35.835,145.418,-35.835,145.415,-35.836,145.411,-35.836,145.407,-35.836,145.402,-35.836,145.397,-35.837,145.393,-35.837,145.387,-35.837,145.382,-35.837,145.377,-35.838,145.371,-35.838,145.365,-35.838,145.359,-35.839,145.353,-35.839,145.347,-35.839,145.341,-35.84,145.335,-35.84,145.328,-35.84,145.322,-35.841,145.316,-35.841,145.31,-35.841,145.304,-35.842,145.298,-35.842,145.292,-35.842,145.286,-35.842,145.28,-35.843,145.275,-35.843,145.269,-35.843,145.264,-35.844,145.259,-35.844,145.255,-35.844,145.25,-35.844,145.246,-35.845,145.242,-35.845,145.239,-35.845,145.236,-35.845,145.233,-35.845,145.23,-35.845,145.228,-35.846,145.227,-35.846,145.225,-35.846,145.224,-35.846,145.223,-35.846,145.222,-35.846,145.22,-35.846,145.219,-35.846,145.217,-35.847,145.215,-35.847,145.212,-35.847,145.21,-35.847,145.207,-35.848,145.204,-35.848,145.201,-35.848,145.197,-35.849,145.194,-35.849,145.19,-35.85,145.187,-35.85,145.183,-35.851,145.179,-35.851,145.175,-35.851,145.171,-35.852,145.166,-35.852,145.162,-35.853,145.158,-35.853,145.154,-35.854,145.15,-35.854,145.145,-35.855,145.141,-35.855,145.137,-35.856,145.133,-35.856,145.129,-35.857,145.125,-35.857,145.121,-35.858,145.117,-35.858,145.114,-35.859,145.11,-35.859,145.107,-35.859,145.104,-35.86,145.101,-35.86,145.098,-35.86,145.095,-35.861,145.093,-35.861,145.091,-35.861,145.089,-35.861,145.087,-35.862,145.086,-35.862,145.085,-35.862,145.084,-35.862,145.083,-35.862,145.008,-35.884,144.962,-35.923,144.943,-35.978,144.944,-36.029,144.965,-36.075,144.916,-36.107,144.799,-36.126,144.668,-36.088,144.524,-35.994,144.427,-35.905,144.378,-35.82,144.29,-35.73,144.163,-35.634,143.971,-35.528,143.714,-35.411,143.585,-35.327,143.584,-35.279,143.54,-35.234,143.459,-35.203,143.404,-35.168,143.377,-35.127,143.363,-35.032,143.361,-34.882,143.325,-34.788,143.221,-34.733,142.965,-34.691,142.857,-34.655,142.812,-34.607,142.774,-34.588,142.742,-34.598,142.705,-34.649,142.663,-34.741,142.619,-34.783,142.571,-34.775,142.507,-34.712,142.426,-34.593,142.388,-34.485,142.394,-34.387,142.373,-34.333,142.328,-34.322,142.287,-34.286,142.252,-34.223,142.158,-34.173,141.926,-34.117,141.761,-34.112,141.666,-34.137,141.586,-34.191,141.47,-34.183,141.318,-34.112,141.192,-34.073,141.094,-34.066,141.026,-34.043,140.997,-34.016]},{"z":2,"b":[150.612,-35.19,150.705,-35.12],"l":[150.705,-35.12,150.636,-35.146,150.612,-35.19]},{"z":3,"b":[-66.829,-9.925,-66.628,-9.838],"l":[-66.829,-9.838,-66.628,-9.925]},{"z":2,"b":[-84.325,34.988,-81.659,36.611],"l":[-84.325,34.988,-84.287,35.201,-84.261,35.25,-84.232,35.261,-84.129,35.261,-84.062,35.286,-84.004,35.414,-83.871,35.515,-83.812,35.54,-83.723,35.56,-83.511,35.584,-83.284,35.682,-83.12,35.775,-82.99,35.8,-82.949,35.835,-82.92,35.879,-82.902,35.939,-82.888,35.952,-82.813,35.947,-82.756,36,-82.7,36.03,-82.645,36.047,-82.611,36.047,-82.598,36.032,-82.589,35.987,-82.577,35.972,-82.553,35.971,-82.521,35.981,-82.478,36.01,-82.368,36.1,-82.295,36.134,-82.182,36.154,-82.101,36.107,-82.051,36.123,-81.908,36.306,-81.851,36.34,-81.808,36.353,-81.734,36.355,-81.733,36.406,-81.708,36.482,-81.704,36.534,-81.659,36.611]},{"z":2,"b":[-75.192,39.983,-74.699,41.357],"l":[-74.699,41.357,-74.778,41.325,-74.812,41.302,-74.841,41.269,-74.912,41.155,-75.032,41.052,-75.123,40.999,-75.123,40.972,-75.075,40.884,-75.076,40.856,-75.112,40.802,-75.175,40.776,-75.186,40.744,-75.192,40.69,-75.189,40.596,-75.171,40.577,-75.097,40.543,-75.034,40.42,-74.976,40.406,-74.735,40.155,-74.909,40.08,-75.024,40.017,-75.074,39.983]},{"z":2,"b":[-72.553,42.73,-71.51,45.008],"l":[-71.518,45.008,-71.533,44.988,-71.51,44.908,-71.621,44.772,-71.618,44.728,-71.568,44.608,-71.571,44.579,-71.588,44.565,-71.609,44.514,-71.638,44.482,-71.683,44.45,-71.826,44.374,-71.868,44.355,-72.002,44.329,-72.031,44.301,-72.062,44.116,-72.106,44.013,-72.115,43.965,-72.174,43.884,-72.222,43.791,-72.297,43.715,-72.338,43.622,-72.362,43.587,-72.385,43.529,-72.407,43.332,-72.438,43.225,-72.474,43.039,-72.496,42.992,-72.519,42.967,-72.55,42.887,-72.553,42.856,-72.54,42.83,-72.484,42.766,-72.467,42.73]},{"z":2,"b":[-82.6,38.42,-82.589,38.432],"l":[-82.589,38.42,-82.6,38.432]},{"z":2,"b":[-82.613,38.42,-82.589,38.448],"l":[-82.613,38.448,-82.589,38.42]},{"z":2,"b":[-82.62,37.54,-81.965,38.42],"l":[-81.965,37.54,-82.103,37.571,-82.28,37.687,-82.394,37.828,-82.43,37.894,-82.472,37.938,-82.482,37.984,-82.589,38.1,-82.618,38.15,-82.62,38.182,-82.585,38.243,-82.578,38.272,-82.589,38.42]},{"z":2,"b":[-83.668,36.605,-81.965,37.54],"l":[-81.965,37.54,-82.343,37.297,-82.517,37.209,-82.682,37.138,-82.695,37.126,-82.732,37.048,-82.832,36.979,-82.895,36.906,-83.044,36.846,-83.15,36.762,-83.417,36.681,-83.566,36.653,-83.668,36.605]},{"z":2,"b":[-89.838,30.193,-89.521,31.001],"l":[-89.735,31.001,-89.765,30.886,-89.835,30.731,-89.838,30.69,-89.82,30.64,-89.82,30.611,-89.794,30.569,-89.737,30.527,-89.686,30.463,-89.641,30.349,-89.629,30.296,-89.592,30.223,-89.572,30.209,-89.545,30.201,-89.521,30.193]},{"z":2,"b":[-97.226,45.943,-96.556,48.994],"l":[-96.556,45.943,-96.57,46.019,-96.561,46.138,-96.592,46.228,-96.599,46.272,-96.617,46.327,-96.731,46.473,-96.758,46.59,-96.787,46.648,-96.796,46.797,-96.793,46.839,-96.77,46.92,-96.818,46.974,-96.825,47.019,-96.848,47.373,-96.863,47.424,-96.859,47.588,-96.946,47.762,-96.988,47.82,-97.009,47.892,-97.007,47.926,-97.079,48.043,-97.126,48.173,-97.122,48.286,-97.137,48.371,-97.131,48.437,-97.144,48.537,-97.129,48.593,-97.128,48.681,-97.226,48.994]},{"z":2,"b":[-94.484,33.554,-94.047,33.648],"l":[-94.047,33.554,-94.099,33.577,-94.192,33.589,-94.238,33.581,-94.295,33.587,-94.333,33.565,-94.358,33.561,-94.378,33.566,-94.39,33.586,-94.432,33.6,-94.484,33.648]},{"z":2,"b":[-75.785,39.722,-75.421,39.843],"l":[-75.421,39.815,-75.51,39.843,-75.635,39.839,-75.677,39.827,-75.709,39.803,-75.785,39.722]},{"z":2,"b":[-81.087,35.066,-81.045,35.156],"l":[-81.087,35.156,-81.057,35.146,-81.047,35.131,-81.051,35.089,-81.045,35.066]},{"z":2,"b":[-82.778,35.084,-82.356,35.211],"l":[-82.778,35.087,-82.744,35.084,-82.651,35.128,-82.464,35.178,-82.393,35.211,-82.356,35.202]},{"z":2,"b":[-120.001,48.993,-114.051,60.001],"l":[-120,60.001,-120,59.616,-120,59.231,-120,58.847,-120,58.462,-120,58.077,-120,57.692,-120,57.307,-120.001,56.922,-120.001,56.538,-120.001,56.153,-120.001,55.768,-120.001,55.383,-120.001,54.998,-120.001,54.613,-120.001,54.229,-120.001,53.844,-119.973,53.797,-119.917,53.77,-119.904,53.723,-119.885,53.708,-119.812,53.699,-119.735,53.634,-119.743,53.615,-119.887,53.615,-119.918,53.612,-119.921,53.602,-119.896,53.567,-119.894,53.516,-119.855,53.509,-119.82,53.491,-119.723,53.4,-119.68,53.371,-119.648,53.363,-119.592,53.372,-119.435,53.362,-119.4,53.353,-119.309,53.242,-119.261,53.209,-119.046,53.144,-119.022,53.139,-119.016,53.204,-118.993,53.236,-118.914,53.212,-118.804,53.153,-118.774,53.122,-118.766,53.065,-118.731,53.058,-118.678,53.026,-118.668,52.972,-118.631,52.909,-118.605,52.895,-118.502,52.888,-118.477,52.879,-118.428,52.836,-118.413,52.784,-118.358,52.742,-118.33,52.709,-118.319,52.674,-118.35,52.623,-118.288,52.562,-118.266,52.518,-118.225,52.483,-118.247,52.441,-118.243,52.409,-118.221,52.386,-118.191,52.384,-118.06,52.413,-118.044,52.427,-118.029,52.466,-118.002,52.486,-117.883,52.43,-117.758,52.395,-117.732,52.383,-117.735,52.353,-117.822,52.272,-117.788,52.233,-117.751,52.208,-117.587,52.142,-117.385,52.144,-117.348,52.159,-117.32,52.156,-117.245,52.047,-117.077,51.926,-117.037,51.886,-116.939,51.736,-116.913,51.714,-116.83,51.715,-116.815,51.722,-116.808,51.753,-116.791,51.771,-116.758,51.789,-116.718,51.8,-116.674,51.791,-116.634,51.758,-116.567,51.651,-116.502,51.615,-116.458,51.574,-116.411,51.544,-116.377,51.497,-116.31,51.448,-116.279,51.348,-116.25,51.323,-116.176,51.297,-116.135,51.266,-116.054,51.237,-116.042,51.23,-116.035,51.218,-116.014,51.151,-115.998,51.13,-115.94,51.102,-115.794,51.07,-115.619,50.962,-115.596,50.936,-115.588,50.909,-115.64,50.869,-115.634,50.855,-115.433,50.756,-115.399,50.731,-115.386,50.726,-115.376,50.727,-115.36,50.729,-115.347,50.729,-115.335,50.72,-115.311,50.697,-115.298,50.673,-115.308,50.656,-115.311,50.642,-115.305,50.635,-115.267,50.594,-115.222,50.563,-115.206,50.554,-115.189,50.557,-115.128,50.585,-115.091,50.585,-115.048,50.577,-114.993,50.545,-114.789,50.359,-114.716,50.129,-114.668,50.061,-114.652,50.02,-114.651,49.985,-114.678,49.955,-114.685,49.934,-114.638,49.797,-114.643,49.73,-114.686,49.643,-114.737,49.604,-114.734,49.587,-114.717,49.569,-114.681,49.556,-114.602,49.551,-114.577,49.544,-114.576,49.519,-114.59,49.452,-114.588,49.42,-114.565,49.394,-114.48,49.333,-114.426,49.274,-114.386,49.252,-114.378,49.232,-114.355,49.211,-114.19,49.168,-114.164,49.147,-114.133,49.094,-114.057,49.038,-114.051,49.015,-114.063,48.993]},{"z":2,"b":[-136.445,60.001,-123.819,68.895],"l":[-123.819,60.001,-123.842,60.02,-123.879,60.04,-123.994,60.04,-124.011,60.046,-124.013,60.06,-123.992,60.099,-123.997,60.127,-124.103,60.225,-124.185,60.321,-124.205,60.353,-124.225,60.453,-124.249,60.473,-124.403,60.494,-124.469,60.567,-124.599,60.662,-124.611,60.682,-124.611,60.701,-124.505,60.788,-124.506,60.822,-124.583,60.953,-124.616,60.96,-124.807,60.961,-124.837,60.948,-124.86,60.924,-124.88,60.86,-125.152,60.844,-125.23,60.826,-125.322,60.792,-125.379,60.792,-125.707,60.84,-125.864,60.89,-125.899,60.89,-125.931,60.883,-125.957,60.865,-126.003,60.809,-126.064,60.816,-126.099,60.829,-126.115,60.863,-126.162,60.866,-126.223,60.856,-126.235,60.844,-126.235,60.809,-126.255,60.794,-126.297,60.785,-126.35,60.781,-126.52,60.8,-126.665,60.762,-126.767,60.775,-126.832,60.764,-126.859,60.769,-126.89,60.796,-126.888,60.833,-126.912,60.86,-126.918,60.884,-126.909,60.95,-126.944,61.052,-126.964,61.063,-127.055,61.051,-127.093,61.071,-127.096,61.081,-127.082,61.094,-127.027,61.126,-127.015,61.198,-127.021,61.237,-127.073,61.37,-127.116,61.404,-127.167,61.465,-127.229,61.494,-127.299,61.512,-127.468,61.511,-127.609,61.533,-127.656,61.552,-127.713,61.588,-127.807,61.619,-127.994,61.711,-128.019,61.78,-128.047,61.813,-128.094,61.841,-128.206,61.865,-128.232,61.884,-128.258,61.928,-128.366,61.991,-128.4,62.034,-128.561,62.117,-128.616,62.122,-128.679,62.112,-128.776,62.069,-128.814,62.064,-128.841,62.073,-128.88,62.111,-128.907,62.119,-129.014,62.132,-129.136,62.126,-129.209,62.136,-129.255,62.153,-129.269,62.166,-129.262,62.184,-129.239,62.202,-129.233,62.22,-129.244,62.236,-129.293,62.294,-129.291,62.322,-129.239,62.373,-129.249,62.389,-129.294,62.411,-129.296,62.425,-129.287,62.441,-129.2,62.47,-129.189,62.49,-129.211,62.513,-129.449,62.58,-129.49,62.599,-129.495,62.619,-129.525,62.674,-129.544,62.688,-129.62,62.712,-129.636,62.728,-129.642,62.756,-129.658,62.776,-129.719,62.832,-129.729,62.865,-129.714,62.895,-129.714,62.931,-129.617,63.037,-129.612,63.059,-129.63,63.068,-129.734,63.07,-129.827,63.092,-129.87,63.157,-129.906,63.187,-129.979,63.216,-130.06,63.261,-130.134,63.276,-130.14,63.293,-130.102,63.319,-129.948,63.376,-129.905,63.406,-129.845,63.486,-129.855,63.522,-129.888,63.555,-129.946,63.586,-129.976,63.616,-130.063,63.636,-130.082,63.65,-130.098,63.679,-130.111,63.695,-130.142,63.698,-130.244,63.678,-130.285,63.686,-130.303,63.704,-130.298,63.721,-130.26,63.737,-130.145,63.762,-130.121,63.773,-130.118,63.793,-130.14,63.808,-130.283,63.823,-130.364,63.85,-130.513,63.92,-130.661,63.951,-130.758,63.993,-130.792,64.031,-130.847,64.049,-130.872,64.081,-130.93,64.125,-130.941,64.141,-130.913,64.168,-130.913,64.193,-130.931,64.219,-130.982,64.259,-131.012,64.273,-131.026,64.298,-131.022,64.324,-131.107,64.401,-131.265,64.447,-131.359,64.458,-131.399,64.451,-131.45,64.416,-131.513,64.393,-131.586,64.38,-131.764,64.381,-131.797,64.388,-131.803,64.407,-131.79,64.426,-131.73,64.489,-131.716,64.515,-131.729,64.534,-131.752,64.545,-131.893,64.577,-132.052,64.684,-132.106,64.702,-132.321,64.763,-132.402,64.777,-132.554,64.78,-132.588,64.794,-132.6,64.809,-132.601,64.826,-132.584,64.842,-132.505,64.884,-132.497,64.913,-132.497,64.957,-132.481,64.97,-132.381,65.022,-132.354,65.039,-132.353,65.057,-132.381,65.075,-132.516,65.094,-132.538,65.109,-132.54,65.17,-132.579,65.185,-132.61,65.181,-132.708,65.167,-132.752,65.173,-132.768,65.185,-132.773,65.203,-132.762,65.225,-132.597,65.28,-132.566,65.3,-132.542,65.341,-132.346,65.443,-132.268,65.538,-132.195,65.598,-132.196,65.628,-132.295,65.716,-132.378,65.767,-132.489,65.82,-132.533,65.834,-132.555,65.857,-132.539,65.886,-132.341,65.955,-132.343,65.979,-132.371,65.994,-132.483,65.989,-132.577,66.024,-132.614,66.029,-132.714,65.998,-132.766,65.971,-132.821,65.928,-132.903,65.914,-132.948,65.917,-132.968,65.934,-132.94,65.976,-132.916,66.002,-132.918,66.018,-132.938,66.029,-133.041,66.028,-133.229,66.006,-133.412,65.956,-133.5,65.955,-133.589,65.963,-133.622,65.975,-133.624,66.01,-133.645,66.049,-133.676,66.074,-133.677,66.096,-133.66,66.112,-133.594,66.15,-133.569,66.177,-133.567,66.208,-133.571,66.249,-133.583,66.285,-133.612,66.295,-133.651,66.3,-133.76,66.304,-133.79,66.304,-133.8,66.326,-133.79,66.348,-133.76,66.416,-133.739,66.44,-133.706,66.444,-133.674,66.446,-133.654,66.453,-133.652,66.472,-133.658,66.509,-133.649,66.536,-133.635,66.566,-133.652,66.586,-133.726,66.631,-133.764,66.651,-133.769,66.669,-133.771,66.684,-133.799,66.711,-133.806,66.733,-133.798,66.752,-133.783,66.785,-133.794,66.819,-133.848,66.862,-133.924,66.903,-134.031,66.944,-134.053,66.973,-134.056,67.004,-134.118,67.004,-134.405,67.004,-134.692,67.005,-134.979,67.005,-135.265,67.005,-135.552,67.005,-135.839,67.005,-136.126,67.005,-136.169,67.015,-136.195,67.036,-136.223,67.088,-136.224,67.135,-136.221,67.163,-136.218,67.181,-136.15,67.226,-136.108,67.288,-136.105,67.304,-136.129,67.354,-136.141,67.375,-136.159,67.394,-136.178,67.423,-136.178,67.45,-136.169,67.514,-136.208,67.577,-136.233,67.6,-136.265,67.616,-136.401,67.666,-136.434,67.703,-136.445,67.747,-136.445,68.038,-136.445,68.329,-136.445,68.62,-136.444,68.895]},{"z":2,"b":[-69.054,47.295,-68.379,47.589],"l":[-68.379,47.589,-68.394,47.563,-68.43,47.528,-68.496,47.48,-68.628,47.421,-69.054,47.295]},{"z":2,"b":[-67.613,47.855,-66.704,48.022],"l":[-67.613,47.999,-67.609,47.969,-67.579,47.94,-67.365,47.855,-67.307,47.887,-67.187,47.894,-67.058,47.919,-66.952,47.899,-66.925,47.964,-66.843,47.998,-66.704,48.022]},{"z":2,"b":[-124.228,42.001,-120.001,42.001],"l":[-120.001,42.001,-120.264,42.001,-120.528,42.001,-120.792,42.001,-121.055,42.001,-121.319,42.001,-121.583,42.001,-121.846,42.001,-122.11,42.001,-122.374,42.001,-122.637,42.001,-122.901,42.001,-123.165,42.001,-123.428,42.001,-123.692,42.001,-123.956,42.001,-124.228,42.001]},{"z":2,"b":[-116.049,44.397,-111.051,48.993],"l":[-111.051,44.499,-111.117,44.525,-111.161,44.556,-111.198,44.591,-111.265,44.688,-111.304,44.723,-111.337,44.745,-111.373,44.746,-111.416,44.734,-111.448,44.711,-111.459,44.671,-111.491,44.624,-111.489,44.585,-111.475,44.565,-111.479,44.554,-111.5,44.545,-111.659,44.56,-111.797,44.54,-111.892,44.555,-112.074,44.548,-112.231,44.561,-112.285,44.554,-112.32,44.533,-112.364,44.48,-112.375,44.47,-112.393,44.468,-112.648,44.497,-112.716,44.498,-112.787,44.463,-112.81,44.438,-112.816,44.406,-112.831,44.397,-112.854,44.398,-112.91,44.419,-112.97,44.454,-113,44.48,-113.006,44.539,-113.054,44.605,-113.063,44.665,-113.093,44.715,-113.141,44.765,-113.187,44.799,-113.234,44.81,-113.32,44.803,-113.339,44.808,-113.432,44.874,-113.461,44.904,-113.469,44.93,-113.438,44.994,-113.447,45.029,-113.471,45.073,-113.504,45.109,-113.557,45.14,-113.607,45.209,-113.697,45.305,-113.722,45.379,-113.756,45.441,-113.766,45.503,-113.8,45.54,-113.797,45.588,-113.802,45.601,-113.813,45.607,-113.873,45.634,-113.943,45.682,-113.978,45.692,-113.992,45.693,-114.001,45.681,-114.006,45.658,-114.117,45.579,-114.21,45.542,-114.256,45.506,-114.298,45.488,-114.319,45.486,-114.351,45.497,-114.448,45.551,-114.529,45.576,-114.537,45.587,-114.533,45.633,-114.496,45.686,-114.534,45.782,-114.523,45.815,-114.493,45.839,-114.405,45.874,-114.397,45.891,-114.416,45.982,-114.465,46.016,-114.478,46.036,-114.464,46.11,-114.485,46.158,-114.447,46.199,-114.444,46.271,-114.413,46.344,-114.38,46.453,-114.387,46.5,-114.346,46.551,-114.332,46.601,-114.33,46.638,-114.337,46.659,-114.351,46.668,-114.381,46.668,-114.519,46.648,-114.563,46.65,-114.602,46.66,-114.624,46.678,-114.631,46.719,-114.662,46.746,-114.678,46.75,-114.74,46.734,-114.754,46.735,-114.761,46.742,-114.786,46.777,-114.887,46.845,-114.936,46.923,-115.034,46.991,-115.127,47.085,-115.307,47.231,-115.349,47.261,-115.486,47.312,-115.593,47.385,-115.702,47.438,-115.713,47.447,-115.714,47.452,-115.708,47.457,-115.677,47.471,-115.67,47.498,-115.711,47.553,-115.691,47.607,-115.718,47.681,-115.734,47.706,-115.811,47.776,-115.862,47.842,-115.941,47.904,-116.049,48.005,-116.049,48.252,-116.049,48.499,-116.048,48.747,-116.048,48.993]},{"z":2,"b":[-111.051,42.001,-111.05,44.499],"l":[-111.051,44.499,-111.051,44.343,-111.051,44.187,-111.051,44.031,-111.051,43.874,-111.051,43.718,-111.051,43.562,-111.051,43.406,-111.051,43.25,-111.051,43.094,-111.051,42.938,-111.051,42.782,-111.051,42.625,-111.051,42.469,-111.05,42.313,-111.05,42.157,-111.05,42.001]},{"z":2,"b":[-111.051,44.499,-104.037,45.001],"l":[-111.051,44.499,-111.051,44.624,-111.051,44.75,-111.051,44.876,-111.051,45.001,-110.832,45.001,-110.613,45.001,-110.393,45.001,-110.174,45.001,-109.955,45.001,-109.736,45.001,-109.517,45.001,-109.297,45.001,-109.078,45.001,-108.859,45.001,-108.64,45.001,-108.421,45.001,-108.202,45.001,-107.982,45.001,-107.763,45.001,-107.544,45.001,-107.325,45.001,-107.106,45.001,-106.887,45.001,-106.667,45.001,-106.448,45.001,-106.229,45.001,-106.01,45.001,-105.791,45.001,-105.571,45.001,-105.352,45.001,-105.133,45.001,-104.914,45.001,-104.695,45.001,-104.476,45.001,-104.256,45.001,-104.037,45.001]},{"z":2,"b":[-120.001,42.001,-117.017,42.001],"l":[-117.017,42.001,-117.39,42.001,-117.763,42.001,-118.136,42.001,-118.509,42.001,-118.882,42.001,-119.255,42.001,-119.628,42.001,-120.001,42.001]},{"z":2,"b":[-117.017,42.001,-114.043,42.001],"l":[-117.017,42.001,-116.645,42.001,-116.273,42.001,-115.901,42.001,-115.53,42.001,-115.158,42.001,-114.786,42.001,-114.414,42.001,-114.043,42.001]},{"z":2,"b":[-114.043,42.001,-111.05,42.001],"l":[-111.05,42.001,-111.237,42.001,-111.424,42.001,-111.611,42.001,-111.798,42.001,-111.985,42.001,-112.172,42.001,-112.359,42.001,-112.546,42.001,-112.733,42.001,-112.92,42.001,-113.107,42.001,-113.295,42.001,-113.482,42.001,-113.669,42.001,-113.856,42.001,-114.043,42.001]},{"z":2,"b":[-111.05,41.001,-109.046,42.001],"l":[-111.05,42.001,-111.05,41.876,-111.05,41.751,-111.05,41.626,-111.05,41.501,-111.05,41.376,-111.05,41.251,-111.05,41.126,-111.05,41.001,-110.8,41.001,-110.549,41.001,-110.299,41.001,-110.048,41.001,-109.798,41.001,-109.547,41.001,-109.297,41.001,-109.046,41.001]},{"z":2,"b":[-114.043,37.004,-114.04,42.001],"l":[-114.043,42.001,-114.043,41.845,-114.042,41.689,-114.042,41.533,-114.042,41.376,-114.042,41.22,-114.042,41.064,-114.042,40.908,-114.042,40.752,-114.042,40.596,-114.042,40.44,-114.042,40.283,-114.042,40.127,-114.042,39.971,-114.042,39.815,-114.042,39.659,-114.041,39.503,-114.041,39.346,-114.041,39.19,-114.041,39.034,-114.041,38.878,-114.041,38.722,-114.041,38.566,-114.041,38.41,-114.041,38.253,-114.041,38.097,-114.041,37.941,-114.041,37.785,-114.041,37.629,-114.04,37.473,-114.04,37.316,-114.04,37.16,-114.04,37.004]},{"z":2,"b":[-114.04,37.001,-109.047,37.004],"l":[-114.04,37.004,-113.728,37.004,-113.416,37.004,-113.104,37.004,-112.792,37.003,-112.48,37.003,-112.168,37.003,-111.856,37.003,-111.543,37.002,-111.231,37.002,-110.919,37.002,-110.607,37.002,-110.295,37.002,-109.983,37.001,-109.671,37.001,-109.359,37.001,-109.047,37.001]},{"z":2,"b":[-109.048,31.328,-109.047,37.001],"l":[-109.047,37.001,-109.047,36.646,-109.047,36.292,-109.047,35.937,-109.047,35.583,-109.047,35.228,-109.047,34.874,-109.047,34.519,-109.047,34.165,-109.047,33.81,-109.047,33.456,-109.047,33.101,-109.047,32.747,-109.047,32.392,-109.047,32.038,-109.047,31.683,-109.048,31.328]},{"z":2,"b":[-109.046,41.001,-104.022,41.001],"l":[-109.046,41.001,-108.732,41.001,-108.418,41.001,-108.104,41.001,-107.79,41.001,-107.476,41.001,-107.162,41.001,-106.848,41.001,-106.534,41.001,-106.22,41.001,-105.906,41.001,-105.592,41.001,-105.278,41.001,-104.964,41.001,-104.65,41.001,-104.336,41.001,-104.022,41.001]},{"z":2,"b":[-109.047,37.001,-109.046,41.001],"l":[-109.046,41.001,-109.046,40.751,-109.046,40.501,-109.046,40.251,-109.046,40.001,-109.046,39.751,-109.046,39.501,-109.047,39.251,-109.047,39.001,-109.047,38.751,-109.047,38.501,-109.047,38.251,-109.047,38.001,-109.047,37.751,-109.047,37.501,-109.047,37.251,-109.047,37.001]},{"z":2,"b":[-109.047,37.001,-103.001,37.001],"l":[-103.001,37.001,-103.379,37.001,-103.757,37.001,-104.134,37.001,-104.512,37.001,-104.89,37.001,-105.268,37.001,-105.646,37.001,-106.024,37.001,-106.402,37.001,-106.779,37.001,-107.157,37.001,-107.535,37.001,-107.913,37.001,-108.291,37.001,-108.669,37.001,-109.047,37.001]},{"z":2,"b":[-104.034,45.943,-104.011,48.993],"l":[-104.011,45.943,-104.012,46.134,-104.014,46.324,-104.015,46.515,-104.017,46.706,-104.018,46.896,-104.019,47.087,-104.021,47.278,-104.022,47.469,-104.024,47.659,-104.025,47.85,-104.026,48.041,-104.028,48.231,-104.029,48.422,-104.031,48.613,-104.032,48.803,-104.034,48.993]},{"z":2,"b":[-104.037,45.001,-104.005,45.943],"l":[-104.037,45.001,-104.029,45.001,-104.021,45.001,-104.013,45.001,-104.005,45.001,-104.005,45.004,-104.005,45.007,-104.005,45.01,-104.005,45.013,-104.006,45.246,-104.008,45.478,-104.01,45.711,-104.011,45.943]},{"z":2,"b":[-104.037,43.001,-104.029,45.001],"l":[-104.029,43.001,-104.03,43.251,-104.031,43.501,-104.032,43.751,-104.033,44.001,-104.034,44.251,-104.035,44.501,-104.036,44.751,-104.037,45.001]},{"z":2,"b":[-104.011,45.943,-96.556,45.943],"l":[-96.556,45.943,-96.789,45.943,-97.022,45.943,-97.255,45.943,-97.488,45.943,-97.721,45.943,-97.954,45.943,-98.187,45.943,-98.42,45.943,-98.653,45.943,-98.886,45.943,-99.119,45.943,-99.352,45.943,-99.585,45.943,-99.818,45.943,-100.051,45.943,-100.284,45.943,-100.517,45.943,-100.75,45.943,-100.983,45.943,-101.216,45.943,-101.449,45.943,-101.682,45.943,-101.914,45.943,-102.147,45.943,-102.38,45.943,-102.613,45.943,-102.846,45.943,-103.079,45.943,-103.312,45.943,-103.545,45.943,-103.778,45.943,-104.011,45.943]},{"z":2,"b":[-104.029,41.001,-104.022,43.001],"l":[-104.022,41.001,-104.023,41.251,-104.023,41.501,-104.024,41.751,-104.025,42.001,-104.026,42.251,-104.027,42.501,-104.028,42.751,-104.029,43.001]},{"z":2,"b":[-104.022,40.001,-102.024,41.001],"l":[-102.024,40.001,-102.025,40.126,-102.025,40.251,-102.025,40.376,-102.025,40.501,-102.025,40.626,-102.025,40.751,-102.025,40.876,-102.025,41.001,-102.275,41.001,-102.524,41.001,-102.774,41.001,-103.023,41.001,-103.273,41.001,-103.522,41.001,-103.772,41.001,-104.022,41.001]},{"z":2,"b":[-102.024,37.001,-102.013,40.001],"l":[-102.013,37.001,-102.014,37.188,-102.014,37.376,-102.015,37.563,-102.016,37.751,-102.016,37.938,-102.017,38.126,-102.018,38.313,-102.019,38.501,-102.019,38.688,-102.02,38.876,-102.021,39.064,-102.022,39.251,-102.022,39.439,-102.023,39.626,-102.024,39.814,-102.024,40.001]},{"z":2,"b":[-102.024,40.001,-95.347,40.001],"l":[-95.347,40.001,-95.527,40.001,-95.737,40.001,-95.946,40.001,-96.156,40.001,-96.365,40.001,-96.575,40.001,-96.784,40.001,-96.994,40.001,-97.204,40.001,-97.413,40.001,-97.623,40.001,-97.832,40.001,-98.042,40.001,-98.252,40.001,-98.461,40.001,-98.671,40.001,-98.88,40.001,-99.09,40.001,-99.3,40.001,-99.509,40.001,-99.719,40.001,-99.928,40.001,-100.138,40.001,-100.348,40.001,-100.557,40.001,-100.767,40.001,-100.976,40.001,-101.186,40.001,-101.396,40.001,-101.605,40.001,-101.815,40.001,-102.024,40.001]},{"z":2,"b":[-103.001,36.501,-103,37.001],"l":[-103,36.501,-103,36.626,-103.001,36.751,-103.001,36.876,-103.001,37.001]},{"z":2,"b":[-103.001,37.001,-102.013,37.001],"l":[-102.013,37.001,-102.26,37.001,-102.507,37.001,-102.754,37.001,-103.001,37.001]},{"z":2,"b":[-103,33.648,-94.484,36.501],"l":[-94.484,33.648,-94.709,33.699,-94.766,33.732,-94.869,33.768,-94.962,33.843,-95.146,33.937,-95.216,33.959,-95.238,33.956,-95.276,33.911,-95.374,33.874,-95.427,33.873,-95.523,33.887,-95.542,33.898,-95.556,33.926,-95.594,33.945,-95.623,33.93,-95.763,33.891,-95.784,33.865,-95.863,33.863,-95.942,33.883,-96.139,33.822,-96.207,33.774,-96.279,33.758,-96.315,33.711,-96.337,33.718,-96.411,33.775,-96.487,33.792,-96.592,33.849,-96.595,33.899,-96.606,33.906,-96.625,33.911,-96.649,33.906,-96.694,33.864,-96.72,33.853,-96.746,33.854,-96.848,33.883,-96.89,33.935,-96.911,33.95,-96.934,33.953,-96.978,33.938,-97.002,33.882,-97.022,33.867,-97.064,33.857,-97.061,33.829,-97.099,33.75,-97.119,33.735,-97.145,33.738,-97.172,33.757,-97.192,33.785,-97.201,33.808,-97.186,33.87,-97.195,33.891,-97.215,33.897,-97.265,33.868,-97.308,33.869,-97.364,33.84,-97.398,33.831,-97.43,33.839,-97.482,33.886,-97.571,33.916,-97.606,33.972,-97.622,33.988,-97.641,33.992,-97.66,33.99,-97.692,33.972,-97.795,33.899,-97.842,33.876,-97.883,33.871,-97.922,33.878,-97.946,33.897,-97.951,33.971,-97.974,33.994,-98.069,34.04,-98.09,34.084,-98.099,34.145,-98.214,34.135,-98.356,34.142,-98.42,34.088,-98.454,34.078,-98.494,34.089,-98.608,34.161,-98.73,34.147,-98.799,34.155,-98.969,34.209,-99.102,34.213,-99.158,34.223,-99.187,34.235,-99.2,34.318,-99.253,34.388,-99.27,34.407,-99.36,34.456,-99.375,34.458,-99.385,34.447,-99.405,34.392,-99.432,34.389,-99.536,34.405,-99.632,34.388,-99.686,34.416,-99.867,34.543,-99.941,34.576,-99.999,34.587,-100,34.706,-100,34.826,-100,34.946,-100,35.065,-100,35.185,-100.001,35.305,-100.001,35.424,-100.001,35.544,-100.001,35.663,-100.001,35.783,-100.001,35.903,-100.002,36.022,-100.002,36.142,-100.002,36.262,-100.002,36.381,-100.002,36.501,-100.19,36.501,-100.377,36.501,-100.565,36.501,-100.752,36.501,-100.939,36.501,-101.127,36.501,-101.314,36.501,-101.501,36.501,-101.689,36.501,-101.876,36.501,-102.063,36.501,-102.251,36.501,-102.438,36.501,-102.626,36.501,-102.813,36.501,-103,36.501]},{"z":2,"b":[-102.013,37.001,-94.618,37.001],"l":[-94.618,37.001,-94.849,37.001,-95.08,37.001,-95.311,37.001,-95.542,37.001,-95.774,37.001,-96.005,37.001,-96.236,37.001,-96.467,37.001,-96.698,37.001,-96.929,37.001,-97.16,37.001,-97.391,37.001,-97.622,37.001,-97.853,37.001,-98.084,37.001,-98.315,37.001,-98.547,37.001,-98.778,37.001,-99.009,37.001,-99.24,37.001,-99.471,37.001,-99.702,37.001,-99.933,37.001,-100.164,37.001,-100.395,37.001,-100.626,37.001,-100.857,37.001,-101.088,37.001,-101.32,37.001,-101.551,37.001,-101.782,37.001,-102.013,37.001]},{"z":2,"b":[-96.627,42.511,-96.454,43.501],"l":[-96.454,43.501,-96.599,43.498,-96.581,43.435,-96.542,43.373,-96.535,43.338,-96.543,43.309,-96.57,43.283,-96.563,43.243,-96.555,43.232,-96.49,43.206,-96.468,43.17,-96.461,43.13,-96.463,43.099,-96.502,43.043,-96.513,42.969,-96.563,42.859,-96.616,42.791,-96.627,42.757,-96.622,42.73,-96.539,42.659,-96.534,42.63,-96.506,42.594,-96.481,42.511]},{"z":2,"b":[-94.625,37.001,-94.618,39.086],"l":[-94.625,39.086,-94.624,38.956,-94.624,38.825,-94.623,38.695,-94.623,38.565,-94.623,38.434,-94.622,38.304,-94.622,38.174,-94.621,38.043,-94.621,37.913,-94.62,37.783,-94.62,37.652,-94.62,37.522,-94.619,37.392,-94.619,37.261,-94.618,37.131,-94.618,37.001]},{"z":2,"b":[-94.625,39.086,-94.622,39.124],"l":[-94.622,39.124,-94.625,39.086]},{"z":2,"b":[-94.618,36.501,-94.618,37.001],"l":[-94.618,37.001,-94.618,36.876,-94.618,36.751,-94.618,36.626,-94.618,36.501]},{"z":2,"b":[-95.765,40.379,-91.442,40.625],"l":[-95.765,40.602,-95.515,40.603,-95.263,40.604,-95.011,40.606,-94.759,40.607,-94.506,40.609,-94.254,40.61,-94.002,40.612,-93.75,40.613,-93.497,40.615,-93.245,40.616,-92.993,40.617,-92.741,40.619,-92.488,40.62,-92.236,40.622,-91.984,40.623,-91.732,40.625,-91.682,40.552,-91.627,40.531,-91.617,40.507,-91.545,40.469,-91.53,40.432,-91.442,40.379]},{"z":2,"b":[-94.047,33.012,-94.041,33.554],"l":[-94.041,33.012,-94.043,33.148,-94.044,33.283,-94.046,33.419,-94.047,33.554]},{"z":2,"b":[-94.618,33.648,-94.439,36.501],"l":[-94.618,36.501,-94.596,36.362,-94.574,36.222,-94.551,36.083,-94.529,35.944,-94.506,35.805,-94.484,35.666,-94.462,35.526,-94.439,35.387,-94.442,35.279,-94.445,35.17,-94.448,35.061,-94.45,34.953,-94.453,34.844,-94.456,34.735,-94.459,34.627,-94.462,34.518,-94.464,34.409,-94.467,34.3,-94.47,34.192,-94.473,34.083,-94.476,33.974,-94.479,33.866,-94.481,33.757,-94.484,33.648]},{"z":2,"b":[-94.618,35.993,-89.705,36.501],"l":[-94.618,36.501,-94.34,36.501,-94.061,36.501,-93.783,36.501,-93.504,36.501,-93.226,36.501,-92.947,36.501,-92.669,36.501,-92.39,36.501,-92.112,36.501,-91.833,36.501,-91.555,36.501,-91.276,36.501,-90.998,36.501,-90.719,36.501,-90.441,36.501,-90.162,36.501,-90.118,36.423,-90.074,36.372,-90.067,36.334,-90.076,36.297,-90.149,36.216,-90.216,36.178,-90.253,36.138,-90.303,36.099,-90.381,35.993,-90.217,35.995,-90.052,35.997,-89.888,35.999,-89.705,36.002]},{"z":2,"b":[-80.521,40.647,-80.519,42.324],"l":[-80.519,40.647,-80.52,40.753,-80.52,40.858,-80.52,40.962,-80.52,41.067,-80.52,41.172,-80.52,41.277,-80.52,41.382,-80.52,41.487,-80.52,41.591,-80.52,41.696,-80.52,41.801,-80.52,41.906,-80.52,42.011,-80.52,42.116,-80.52,42.22,-80.521,42.324]},{"z":2,"b":[-80.52,39.723,-79.477,40.647],"l":[-79.477,39.723,-79.608,39.723,-79.738,39.723,-79.868,39.723,-79.998,39.723,-80.129,39.723,-80.259,39.723,-80.389,39.723,-80.519,39.723,-80.52,39.838,-80.52,39.954,-80.52,40.069,-80.52,40.185,-80.52,40.301,-80.52,40.416,-80.52,40.532,-80.519,40.647]},{"z":2,"b":[-94.043,29.977,-93.531,33.012],"l":[-93.794,29.977,-93.727,30.077,-93.711,30.113,-93.721,30.283,-93.75,30.345,-93.747,30.38,-93.715,30.473,-93.72,30.558,-93.665,30.661,-93.625,30.714,-93.579,30.824,-93.569,30.895,-93.549,30.948,-93.563,31.005,-93.531,31.046,-93.551,31.104,-93.558,31.18,-93.597,31.21,-93.666,31.323,-93.664,31.372,-93.736,31.478,-93.738,31.514,-93.807,31.569,-93.821,31.604,-93.827,31.75,-93.846,31.793,-93.905,31.877,-94.043,31.999,-94.043,32.126,-94.043,32.252,-94.043,32.379,-94.042,32.506,-94.042,32.632,-94.042,32.759,-94.042,32.885,-94.041,33.012]},{"z":2,"b":[-94.041,33.012,-91.15,33.016],"l":[-94.041,33.012,-93.862,33.012,-93.682,33.012,-93.502,33.013,-93.323,33.013,-93.143,33.013,-92.963,33.013,-92.784,33.013,-92.604,33.014,-92.424,33.014,-92.245,33.014,-92.065,33.014,-91.885,33.015,-91.706,33.015,-91.526,33.015,-91.346,33.015,-91.15,33.016]},{"z":2,"b":[-90.294,34.999,-88.173,35.001],"l":[-90.294,35.001,-90.039,35.001,-89.773,35.001,-89.507,35,-89.242,35,-88.976,35,-88.71,34.999,-88.445,34.999,-88.173,34.999]},{"z":2,"b":[-87.607,30.378,-85.007,31.002],"l":[-85.007,31.002,-85.33,31.001,-85.653,31.001,-85.977,31.001,-86.3,31.001,-86.624,31.001,-86.947,31.001,-87.27,31.001,-87.594,31.001,-87.607,30.929,-87.601,30.861,-87.566,30.796,-87.422,30.672,-87.408,30.641,-87.434,30.546,-87.429,30.478,-87.445,30.443,-87.48,30.412,-87.49,30.378]},{"z":2,"b":[-83.097,35.001,-83.096,35.002],"l":[-83.097,35.002,-83.096,35.001]},{"z":2,"b":[-85.624,34.988,-84.325,35.001],"l":[-84.325,34.988,-84.649,34.991,-84.974,34.994,-85.299,34.998,-85.624,35.001]},{"z":2,"b":[-84.325,34.988,-83.115,35.001],"l":[-83.115,35.001,-83.417,34.998,-83.72,34.995,-84.022,34.991,-84.325,34.988]},{"z":2,"b":[-83.115,35.001,-82.778,35.087],"l":[-82.778,35.087,-82.937,35.044,-83.097,35.002,-83.115,35.001]},{"z":2,"b":[-81.045,33.877,-78.564,35.114],"l":[-81.045,35.066,-80.934,35.114,-80.882,35.058,-80.793,34.963,-80.789,34.907,-80.784,34.829,-80.643,34.825,-80.501,34.821,-80.36,34.817,-80.219,34.813,-80.078,34.809,-79.936,34.805,-79.795,34.801,-79.654,34.797,-79.519,34.683,-79.383,34.569,-79.248,34.454,-79.113,34.34,-78.978,34.226,-78.843,34.112,-78.708,33.998,-78.564,33.877]},{"z":2,"b":[-82.356,35.156,-81.087,35.202],"l":[-82.356,35.202,-82.197,35.196,-82.039,35.19,-81.88,35.184,-81.722,35.179,-81.563,35.173,-81.405,35.167,-81.246,35.161,-81.087,35.156]},{"z":2,"b":[-71.801,41.707,-71.234,42.017],"l":[-71.801,42.012,-71.726,42.013,-71.623,42.014,-71.524,42.015,-71.463,42.016,-71.387,42.017,-71.384,41.972,-71.379,41.902,-71.338,41.891,-71.339,41.835,-71.341,41.798,-71.305,41.775,-71.268,41.751,-71.234,41.707]},{"z":2,"b":[-73.481,42.008,-71.801,42.056],"l":[-73.481,42.056,-73.396,42.053,-73.312,42.05,-73.228,42.048,-73.144,42.045,-73.06,42.042,-72.976,42.039,-72.892,42.037,-72.808,42.034,-72.806,42.008,-72.763,42.011,-72.758,42.034,-72.638,42.033,-72.519,42.031,-72.399,42.03,-72.28,42.028,-72.16,42.027,-72.041,42.026,-71.921,42.024,-71.802,42.023,-71.801,42.012]},{"z":2,"b":[-71.842,41.336,-71.796,42.012],"l":[-71.801,42.012,-71.8,41.958,-71.8,41.883,-71.799,41.829,-71.798,41.753,-71.797,41.668,-71.797,41.589,-71.796,41.52,-71.799,41.479,-71.805,41.417,-71.83,41.393,-71.842,41.336]},{"z":2,"b":[-81.971,37.208,-77.727,39.452],"l":[-81.965,37.54,-81.958,37.508,-81.971,37.473,-81.939,37.433,-81.913,37.369,-81.876,37.33,-81.829,37.299,-81.752,37.271,-81.661,37.222,-81.589,37.208,-81.565,37.21,-81.486,37.257,-81.419,37.282,-81.35,37.334,-81.227,37.263,-81.178,37.263,-81.057,37.294,-80.944,37.302,-80.915,37.311,-80.873,37.341,-80.872,37.399,-80.862,37.415,-80.843,37.422,-80.821,37.423,-80.77,37.402,-80.72,37.401,-80.507,37.469,-80.491,37.464,-80.465,37.433,-80.371,37.473,-80.316,37.51,-80.292,37.566,-80.246,37.621,-80.255,37.645,-80.285,37.677,-80.246,37.751,-80.188,37.824,-79.972,38.046,-79.903,38.181,-79.813,38.267,-79.783,38.336,-79.715,38.404,-79.669,38.544,-79.64,38.572,-79.598,38.577,-79.555,38.559,-79.525,38.533,-79.505,38.491,-79.396,38.454,-79.304,38.45,-79.24,38.477,-79.203,38.526,-79.153,38.618,-79.104,38.673,-79.073,38.742,-79.041,38.79,-78.992,38.82,-78.869,38.818,-78.805,38.86,-78.757,38.908,-78.727,38.924,-78.693,38.922,-78.581,38.999,-78.435,39.161,-78.404,39.263,-78.366,39.336,-78.351,39.411,-78.322,39.452,-78.193,39.37,-78.118,39.323,-77.949,39.216,-77.836,39.145,-77.792,39.227,-77.727,39.346]},{"z":2,"b":[-79.477,39.722,-75.785,39.723],"l":[-79.477,39.723,-79.247,39.723,-79.016,39.723,-78.785,39.723,-78.554,39.723,-78.324,39.723,-78.093,39.723,-77.862,39.723,-77.631,39.723,-77.4,39.723,-77.169,39.723,-76.939,39.723,-76.708,39.722,-76.477,39.722,-76.246,39.722,-76.016,39.722,-75.785,39.722]},{"z":2,"b":[-79.488,39.211,-79.477,39.723],"l":[-79.477,39.723,-79.48,39.595,-79.483,39.467,-79.485,39.339,-79.488,39.211]},{"z":2,"b":[-75.785,38.455,-75.038,39.722],"l":[-75.785,39.722,-75.775,39.564,-75.765,39.406,-75.756,39.247,-75.746,39.089,-75.736,38.93,-75.726,38.772,-75.717,38.613,-75.707,38.455,-75.705,38.455,-75.702,38.455,-75.7,38.455,-75.698,38.455,-75.696,38.455,-75.693,38.455,-75.691,38.455,-75.689,38.455,-75.588,38.455,-75.366,38.455,-75.145,38.455,-75.038,38.456]},{"z":2,"b":[-75.659,37.954,-75.224,38.041],"l":[-75.659,37.954,-75.62,37.999,-75.469,38.015,-75.376,38.025,-75.252,38.037,-75.224,38.041]},{"z":2,"b":[-77.122,38.807,-76.931,39.012],"l":[-77.02,38.807,-76.931,38.888,-76.989,38.952,-77.042,39.012,-77.122,38.944]},{"z":2,"b":[-79.763,41.357,-74.699,42.539],"l":[-74.699,41.357,-74.72,41.395,-74.757,41.424,-74.935,41.474,-74.981,41.508,-75.023,41.553,-75.051,41.608,-75.066,41.667,-75.067,41.713,-75.054,41.765,-75.095,41.799,-75.096,41.832,-75.122,41.853,-75.239,41.892,-75.274,41.947,-75.351,41.998,-75.489,41.998,-75.627,41.999,-75.765,41.999,-75.903,41.999,-76.041,41.999,-76.179,41.999,-76.316,41.999,-76.454,41.999,-76.592,41.999,-76.73,41.999,-76.868,41.999,-77.006,41.999,-77.144,41.999,-77.281,42,-77.419,42,-77.557,42,-77.695,42,-77.833,42,-77.971,42,-78.109,42,-78.246,42,-78.384,42,-78.522,42,-78.66,42,-78.798,42,-78.936,42,-79.074,42.001,-79.212,42.001,-79.349,42.001,-79.487,42.001,-79.625,42.001,-79.763,42.001,-79.763,42.136,-79.763,42.27,-79.763,42.405,-79.762,42.539]},{"z":2,"b":[-73.507,42.056,-73.253,42.752],"l":[-73.253,42.752,-73.285,42.668,-73.317,42.584,-73.349,42.5,-73.38,42.416,-73.412,42.332,-73.444,42.248,-73.476,42.164,-73.507,42.08,-73.481,42.056]},{"z":2,"b":[-73.723,40.992,-73.481,42.056],"l":[-73.481,42.056,-73.489,41.961,-73.497,41.866,-73.505,41.771,-73.513,41.676,-73.521,41.581,-73.529,41.486,-73.537,41.391,-73.545,41.296,-73.514,41.257,-73.484,41.219,-73.522,41.201,-73.58,41.173,-73.642,41.143,-73.723,41.105,-73.682,41.055,-73.63,40.992]},{"z":2,"b":[-74.699,40.992,-73.91,41.357],"l":[-74.699,41.357,-74.6,41.312,-74.501,41.266,-74.402,41.22,-74.303,41.175,-74.204,41.129,-74.105,41.083,-74.006,41.037,-73.91,40.992]},{"z":2,"b":[-73.253,42.73,-72.467,42.752],"l":[-72.467,42.73,-72.565,42.733,-72.663,42.736,-72.762,42.739,-72.86,42.741,-72.958,42.744,-73.057,42.747,-73.155,42.749,-73.253,42.752]},{"z":2,"b":[-73.426,42.752,-73.247,45.005],"l":[-73.253,42.752,-73.28,42.813,-73.267,42.865,-73.265,42.935,-73.262,43.027,-73.259,43.128,-73.255,43.265,-73.253,43.357,-73.25,43.44,-73.247,43.553,-73.306,43.616,-73.33,43.627,-73.349,43.62,-73.391,43.579,-73.405,43.579,-73.416,43.59,-73.413,43.628,-73.376,43.762,-73.375,43.804,-73.383,43.876,-73.425,44.038,-73.426,44.075,-73.41,44.131,-73.381,44.187,-73.332,44.245,-73.321,44.269,-73.325,44.368,-73.31,44.46,-73.372,44.597,-73.349,44.775,-73.366,44.86,-73.345,44.939,-73.352,45.005]},{"z":2,"b":[-72.467,42.702,-70.806,42.882],"l":[-70.806,42.877,-70.923,42.882,-70.974,42.872,-71.076,42.825,-71.139,42.808,-71.242,42.73,-71.33,42.702,-71.472,42.706,-71.614,42.709,-71.756,42.713,-71.898,42.716,-72.04,42.72,-72.183,42.723,-72.325,42.727,-72.467,42.73]},{"z":2,"b":[-71.085,43.07,-70.733,45.294],"l":[-70.733,43.07,-70.813,43.164,-70.829,43.239,-70.92,43.328,-70.956,43.389,-70.968,43.458,-70.962,43.532,-70.97,43.643,-70.978,43.754,-70.985,43.865,-70.993,43.976,-71.001,44.086,-71.008,44.197,-71.016,44.308,-71.024,44.419,-71.031,44.53,-71.039,44.641,-71.047,44.751,-71.054,44.862,-71.062,44.973,-71.07,45.084,-71.077,45.195,-71.085,45.294]},{"z":2,"b":[-96.851,43.501,-96.454,45.943],"l":[-96.454,43.501,-96.454,43.726,-96.454,43.95,-96.454,44.175,-96.454,44.399,-96.454,44.624,-96.454,44.848,-96.454,45.073,-96.454,45.297,-96.53,45.372,-96.696,45.432,-96.738,45.468,-96.815,45.557,-96.845,45.596,-96.851,45.626,-96.814,45.659,-96.663,45.756,-96.615,45.8,-96.591,45.843,-96.556,45.943]},{"z":2,"b":[-96.454,43.501,-91.245,43.502],"l":[-91.245,43.502,-91.575,43.502,-91.901,43.502,-92.226,43.502,-92.551,43.502,-92.876,43.502,-93.201,43.502,-93.527,43.502,-93.852,43.502,-94.177,43.502,-94.502,43.501,-94.828,43.501,-95.153,43.501,-95.478,43.501,-95.803,43.501,-96.128,43.501,-96.454,43.501]},{"z":2,"b":[-84.796,41.701,-83.122,41.95],"l":[-84.796,41.701,-84.463,41.711,-84.13,41.72,-83.797,41.73,-83.464,41.739,-83.304,41.838,-83.122,41.95]},{"z":2,"b":[-89.958,47.287,-89.498,47.998],"l":[-89.958,47.287,-89.843,47.464,-89.728,47.642,-89.614,47.819,-89.498,47.998]},{"z":2,"b":[-87.221,41.761,-87.039,42.493],"l":[-87.221,41.761,-87.176,41.944,-87.13,42.127,-87.085,42.31,-87.039,42.493]},{"z":2,"b":[-90.651,42.493,-87.039,42.513],"l":[-90.651,42.513,-90.191,42.51,-89.741,42.508,-89.291,42.505,-88.84,42.503,-88.39,42.5,-87.94,42.498,-87.489,42.495,-87.039,42.493]},{"z":2,"b":[-84.822,39.092,-84.796,41.701],"l":[-84.822,39.092,-84.82,39.424,-84.817,39.749,-84.813,40.075,-84.81,40.4,-84.806,40.725,-84.803,41.051,-84.8,41.376,-84.796,41.701]},{"z":2,"b":[-87.221,41.701,-84.796,41.761],"l":[-87.221,41.761,-86.918,41.761,-86.615,41.76,-86.312,41.76,-86.009,41.76,-85.706,41.76,-85.403,41.76,-85.1,41.76,-84.796,41.76,-84.796,41.701]},{"z":2,"b":[-90.413,42.493,-86.265,47.287],"l":[-87.039,42.493,-87.079,42.782,-87.114,43.031,-87.155,43.327,-87.145,43.571,-87.142,43.658,-87.083,43.89,-87.032,44.091,-86.919,44.362,-86.822,44.591,-86.715,44.846,-86.536,45.041,-86.401,45.133,-86.265,45.227,-86.482,45.323,-86.774,45.452,-86.942,45.452,-87.115,45.452,-87.244,45.29,-87.42,45.184,-87.46,45.068,-87.654,45.122,-87.694,45.161,-87.719,45.201,-87.721,45.243,-87.663,45.345,-87.658,45.362,-87.664,45.371,-87.696,45.383,-87.769,45.364,-87.842,45.361,-87.865,45.368,-87.875,45.385,-87.874,45.413,-87.859,45.445,-87.815,45.506,-87.815,45.563,-87.795,45.596,-87.814,45.657,-87.809,45.686,-87.84,45.718,-87.923,45.76,-88.116,45.816,-88.096,45.878,-88.104,45.905,-88.152,45.945,-88.355,45.992,-88.493,46.014,-88.63,46.014,-88.727,46.031,-88.837,46.037,-88.908,46.065,-88.978,46.093,-89.049,46.121,-89.119,46.149,-89.244,46.173,-89.369,46.197,-89.494,46.221,-89.619,46.245,-89.744,46.269,-89.869,46.294,-89.994,46.318,-90.119,46.342,-90.147,46.384,-90.174,46.426,-90.202,46.468,-90.23,46.51,-90.288,46.527,-90.318,46.537,-90.341,46.558,-90.392,46.549,-90.413,46.585,-90.356,46.673,-90.299,46.761,-90.242,46.848,-90.185,46.936,-90.128,47.024,-90.071,47.112,-90.015,47.199,-89.958,47.287]},{"z":2,"b":[-91.656,31.001,-89.735,31.002],"l":[-91.656,31.002,-91.417,31.001,-91.177,31.001,-90.937,31.001,-90.696,31.001,-90.456,31.001,-90.216,31.001,-89.976,31.001,-89.735,31.001]},{"z":2,"b":[-89.557,36.501,-89.487,36.503],"l":[-89.487,36.503,-89.557,36.501]},{"z":2,"b":[-120.001,34.991,-114.611,42.001],"l":[-114.611,34.991,-114.882,35.203,-115.136,35.401,-115.39,35.599,-115.644,35.797,-115.898,35.995,-116.152,36.194,-116.406,36.392,-116.66,36.59,-116.843,36.728,-117.026,36.866,-117.21,37.003,-117.393,37.141,-117.576,37.279,-117.76,37.417,-117.943,37.555,-118.126,37.692,-118.36,37.856,-118.595,38.019,-118.829,38.183,-119.063,38.346,-119.297,38.51,-119.532,38.673,-119.766,38.837,-120,39,-120,39.188,-120,39.375,-120,39.563,-120,39.75,-120,39.938,-120,40.125,-120,40.313,-120,40.5,-120.001,40.688,-120.001,40.875,-120.001,41.063,-120.001,41.251,-120.001,41.438,-120.001,41.626,-120.001,41.813,-120.001,42.001]},{"z":2,"b":[-68.379,47.589,-67.613,47.999],"l":[-67.613,47.999,-67.739,47.999,-67.913,47.999,-68.023,47.999,-68.114,47.999,-68.114,47.929,-68.252,47.929,-68.378,47.929,-68.378,47.838,-68.379,47.765,-68.379,47.692,-68.379,47.589]},{"z":2,"b":[-139.057,60.001,-123.819,60.002],"l":[-123.819,60.001,-124.771,60.001,-125.723,60.001,-126.674,60.001,-127.626,60.001,-128.578,60.001,-129.53,60.001,-130.481,60.001,-131.433,60.001,-132.385,60.001,-133.337,60.001,-134.288,60.001,-135.24,60.001,-136.192,60.001,-137.144,60.001,-138.095,60.001,-139.057,60.002]},{"z":2,"b":[-123.819,60.001,-120,60.001],"l":[-120,60.001,-120.477,60.001,-120.955,60.001,-121.432,60.001,-121.91,60.001,-122.387,60.001,-122.864,60.001,-123.342,60.001,-123.819,60.001]},{"z":2,"b":[-64.315,45.836,-64.031,46.013],"l":[-64.031,46.013,-64.051,45.978,-64.16,45.965,-64.216,45.901,-64.273,45.85,-64.315,45.836]},{"z":2,"b":[-67.761,51.443,-57.1,60.306],"l":[-64.614,60.306,-64.816,60.267,-64.847,60.251,-64.847,60.238,-64.789,60.196,-64.645,60.161,-64.622,60.141,-64.65,60.11,-64.711,60.091,-64.782,60.064,-64.88,60.05,-64.887,60.035,-64.868,60.009,-64.834,59.986,-64.682,59.943,-64.665,59.933,-64.661,59.922,-64.666,59.906,-64.762,59.864,-64.791,59.816,-64.818,59.54,-64.776,59.488,-64.715,59.466,-64.445,59.528,-64.385,59.515,-64.379,59.475,-64.426,59.456,-64.503,59.399,-64.519,59.356,-64.519,59.226,-64.505,59.166,-64.478,59.116,-64.442,59.091,-64.332,59.044,-64.324,59.02,-64.358,59.002,-64.394,58.998,-64.77,59.049,-64.802,59.04,-64.833,59.02,-64.851,58.984,-64.848,58.949,-64.809,58.929,-64.618,58.911,-64.294,58.866,-64.236,58.786,-64.192,58.769,-64.155,58.771,-63.991,58.811,-63.727,58.862,-63.666,58.854,-63.592,58.83,-63.535,58.799,-63.504,58.771,-63.497,58.753,-63.506,58.743,-63.553,58.732,-63.961,58.684,-64.031,58.666,-64.071,58.642,-64.085,58.621,-64.086,58.599,-64.062,58.555,-64.039,58.545,-63.882,58.555,-63.841,58.505,-63.837,58.483,-63.848,58.46,-64.023,58.399,-64.098,58.359,-64.371,58.169,-64.395,58.144,-64.4,58.121,-64.387,58.099,-64.227,58.018,-64.134,57.875,-64.079,57.813,-64.045,57.785,-63.986,57.803,-63.961,57.802,-63.921,57.783,-63.872,57.731,-63.842,57.713,-63.793,57.706,-63.716,57.696,-63.618,57.726,-63.601,57.723,-63.598,57.692,-63.615,57.669,-63.744,57.581,-63.759,57.553,-63.762,57.513,-63.742,57.396,-63.8,57.319,-63.793,57.279,-63.76,57.241,-63.758,57.228,-63.792,57.13,-63.836,57.083,-63.876,57.023,-63.903,56.889,-63.95,56.863,-64.004,56.821,-64.03,56.787,-64.101,56.738,-64.116,56.707,-64.112,56.691,-63.955,56.527,-63.926,56.475,-63.955,56.439,-64.053,56.43,-64.136,56.395,-64.119,56.334,-64.121,56.299,-64.115,56.289,-64.084,56.275,-63.913,56.229,-63.899,56.212,-63.992,56.145,-64.004,56.097,-63.968,56.095,-63.886,56.088,-63.836,56.063,-63.578,56.026,-63.558,55.992,-63.586,55.976,-63.661,55.948,-63.698,55.923,-63.708,55.883,-63.701,55.861,-63.524,55.706,-63.415,55.596,-63.363,55.565,-63.305,55.542,-63.154,55.514,-63.151,55.484,-63.159,55.44,-63.151,55.404,-63.112,55.34,-63.161,55.307,-63.429,55.21,-63.511,55.168,-63.534,55.14,-63.551,55.086,-63.48,54.987,-63.476,54.962,-63.491,54.918,-63.57,54.776,-63.658,54.644,-63.699,54.615,-63.751,54.612,-63.91,54.634,-64.341,54.726,-64.815,54.734,-65.122,54.719,-65.676,54.745,-65.742,54.769,-65.797,54.805,-65.868,54.898,-65.884,54.907,-66.083,54.942,-66.157,54.968,-66.24,55.008,-66.618,55.256,-66.733,55.319,-66.774,55.328,-66.8,55.311,-66.797,55.294,-66.725,55.213,-66.719,55.193,-66.747,55.129,-66.693,54.998,-66.71,54.948,-66.657,54.847,-66.651,54.799,-66.724,54.748,-66.867,54.781,-67.167,54.937,-67.302,55.064,-67.339,55.075,-67.408,55.072,-67.426,55.059,-67.437,55.036,-67.416,54.984,-67.372,54.917,-67.29,54.826,-67.205,54.699,-67.199,54.682,-67.147,54.653,-67.15,54.617,-67.236,54.586,-67.286,54.519,-67.315,54.517,-67.537,54.573,-67.579,54.565,-67.601,54.553,-67.62,54.507,-67.61,54.452,-67.582,54.392,-67.515,54.282,-67.508,54.253,-67.511,54.236,-67.681,54.141,-67.731,54.1,-67.761,54.061,-67.757,54.034,-67.531,53.839,-67.471,53.796,-67.456,53.73,-67.477,53.635,-67.474,53.613,-67.457,53.601,-67.165,53.543,-67.128,53.503,-67.076,53.417,-67.026,53.393,-66.98,53.384,-66.944,53.355,-66.938,53.329,-66.965,53.183,-66.97,53.073,-67.028,52.98,-67.054,52.92,-67.07,52.833,-67.068,52.755,-67.029,52.72,-66.951,52.698,-66.897,52.694,-66.856,52.698,-66.815,52.687,-66.688,52.727,-66.671,52.749,-66.631,52.91,-66.615,52.932,-66.521,52.958,-66.49,53.005,-66.468,53.019,-66.428,53.011,-66.39,52.992,-66.328,52.934,-66.3,52.897,-66.31,52.861,-66.341,52.84,-66.357,52.806,-66.366,52.741,-66.417,52.688,-66.426,52.661,-66.394,52.418,-66.402,52.398,-66.463,52.341,-66.462,52.295,-66.439,52.229,-66.407,52.184,-66.379,52.165,-66.345,52.155,-66.327,52.189,-66.296,52.295,-66.28,52.301,-66.149,52.23,-66.097,52.21,-66.036,52.109,-65.979,52.081,-65.883,52.054,-65.827,52.052,-65.806,52.064,-65.781,52.104,-65.691,52.094,-65.631,52.073,-65.589,52.073,-65.552,52.094,-65.474,52.196,-65.452,52.203,-65.398,52.203,-65.268,52.185,-65.137,52.155,-65.036,52.113,-64.932,52.055,-64.765,51.905,-64.658,51.849,-64.627,51.825,-64.639,51.791,-64.693,51.763,-64.702,51.745,-64.697,51.72,-64.598,51.6,-64.506,51.609,-64.291,51.729,-64.289,51.769,-64.326,51.811,-64.335,51.842,-64.321,51.93,-64.271,52.083,-64.26,52.1,-64.16,52.148,-64.131,52.187,-64.103,52.378,-64.183,52.554,-64.187,52.626,-64.178,52.815,-64.139,52.854,-64.008,52.878,-63.973,52.895,-63.917,53.066,-63.905,53.077,-63.799,53.104,-63.723,53.109,-63.672,53.101,-63.606,53.035,-63.555,52.964,-63.549,52.94,-63.567,52.826,-63.563,52.797,-63.55,52.774,-63.41,52.689,-63.423,52.67,-63.523,52.649,-63.811,52.611,-63.899,52.589,-63.955,52.566,-64.018,52.525,-64.059,52.485,-64.066,52.466,-64.062,52.458,-63.951,52.429,-63.943,52.418,-63.935,52.381,-63.884,52.345,-63.766,52.298,-63.684,52.138,-63.668,52.094,-63.664,52.06,-63.704,52.048,-63.786,52.051,-63.792,52.044,-63.783,52.02,-63.757,52.011,-63.607,52.001,-63.2,52.001,-62.793,52.001,-62.387,52.001,-61.98,52.001,-61.574,52.001,-61.167,52.001,-60.76,52.001,-60.354,52.001,-59.947,52.001,-59.541,52.001,-59.134,52.001,-58.727,52.001,-58.321,52.001,-57.914,52.001,-57.508,52.001,-57.101,52.001,-57.101,51.711,-57.1,51.443]},{"z":2,"b":[-120.682,60.001,-101.987,78.687],"l":[-102.002,60.001,-102.002,60.242,-102.002,60.482,-102.002,60.723,-102.003,60.963,-102.003,61.204,-102.003,61.444,-102.003,61.685,-102.004,61.925,-102.004,62.166,-102.004,62.406,-102.004,62.647,-102.005,62.887,-102.005,63.128,-102.005,63.368,-102.005,63.609,-102.006,63.849,-101.987,64.08,-101.999,64.228,-102.435,64.263,-102.871,64.297,-103.307,64.332,-103.743,64.366,-104.178,64.401,-104.614,64.435,-105.05,64.469,-105.486,64.504,-105.922,64.538,-106.358,64.573,-106.794,64.607,-107.229,64.642,-107.665,64.676,-108.101,64.71,-108.537,64.745,-108.973,64.779,-109.215,64.813,-109.516,64.903,-109.805,65.051,-110.093,65.2,-110.381,65.349,-110.669,65.497,-111.128,65.498,-111.587,65.5,-112.045,65.501,-112.504,65.502,-112.759,65.58,-113.015,65.659,-113.27,65.737,-113.526,65.815,-113.781,65.893,-114.037,65.971,-114.292,66.049,-114.547,66.127,-114.803,66.206,-115.058,66.284,-115.314,66.362,-115.569,66.44,-115.825,66.518,-116.08,66.596,-116.336,66.674,-116.591,66.753,-116.846,66.831,-117.102,66.909,-117.357,66.987,-117.613,67.065,-117.868,67.143,-118.124,67.221,-118.379,67.3,-118.634,67.378,-118.89,67.456,-119.145,67.534,-119.401,67.612,-119.656,67.69,-119.912,67.768,-120.167,67.847,-120.422,67.925,-120.678,68.003,-120.678,68.209,-120.679,68.414,-120.679,68.62,-120.679,68.826,-120.68,69.031,-120.68,69.237,-120.682,69.567,-120.681,69.649,-120.203,69.649,-119.725,69.649,-119.246,69.649,-118.768,69.649,-118.29,69.649,-117.812,69.649,-117.334,69.649,-116.856,69.65,-116.65,69.65,-116.694,69.695,-116.821,69.76,-116.948,69.825,-117.075,69.889,-117.055,70.002,-116.596,70.002,-116.068,70.002,-115.539,70.002,-115.011,70.002,-114.483,70.002,-113.955,70.001,-113.427,70.001,-112.881,70.001,-112.878,69.912,-112.875,69.833,-112.762,69.834,-112.65,69.834,-112.5,69.912,-112.5,70.001,-112.188,70.001,-111.875,70.001,-111.563,70.001,-111.25,70.001,-110.938,70.001,-110.625,70.001,-110.313,70.001,-110,70.001,-110,70.544,-110,71.087,-110.001,71.63,-110.001,72.173,-110.001,72.716,-110.002,72.982,-110.001,73.259,-110.002,73.802,-110.002,74.345,-110.003,74.851,-110.002,74.888,-110.002,75.43,-110.003,75.973,-110.003,76.244,-110.003,76.48,-110.003,76.516,-110.003,77.059,-110.003,77.602,-110.004,77.929,-110.004,78.09,-110.004,78.145,-110.004,78.322,-110.004,78.687]},{"z":2,"b":[-110,60.001,-102.002,60.001],"l":[-102.002,60.001,-102.502,60.001,-103.001,60.001,-103.501,60.001,-104.001,60.001,-104.501,60.001,-105.001,60.001,-105.501,60.001,-106.001,60.001,-106.501,60.001,-107.001,60.001,-107.501,60.001,-108,60.001,-108.5,60.001,-109,60.001,-109.5,60.001,-110,60.001]},{"z":2,"b":[-120,60.001,-110,60.001],"l":[-120,60.001,-119.375,60.001,-118.75,60.001,-118.125,60.001,-117.5,60.001,-116.875,60.001,-116.25,60.001,-115.625,60.001,-115,60.001,-114.375,60.001,-113.75,60.001,-113.125,60.001,-112.5,60.001,-111.875,60.001,-111.25,60.001,-110.625,60.001,-110,60.001]},{"z":2,"b":[-110,48.993,-109.999,60.001],"l":[-110,60.001,-110,59.657,-110,59.313,-110,58.969,-110,58.625,-110,58.281,-110,57.937,-110,57.593,-110,57.249,-110,56.905,-110,56.561,-110,56.217,-110,55.873,-110,55.529,-110,55.185,-110,54.842,-110,54.498,-110,54.154,-110,53.81,-110,53.466,-110,53.122,-110,52.778,-110,52.434,-110,52.09,-110,51.746,-110,51.402,-110,51.058,-110,50.714,-109.999,50.37,-109.999,50.026,-109.999,49.682,-109.999,49.338,-109.999,48.993]},{"z":2,"b":[-95.16,49.37,-88.948,56.851],"l":[-88.948,56.851,-89.205,56.695,-89.464,56.535,-89.722,56.376,-89.981,56.216,-90.33,55.991,-90.68,55.767,-91.029,55.542,-91.378,55.317,-91.684,55.108,-91.989,54.899,-92.295,54.69,-92.6,54.481,-92.855,54.298,-93.11,54.114,-93.365,53.931,-93.62,53.748,-94.004,53.517,-94.387,53.285,-94.77,53.054,-95.154,52.822,-95.155,52.391,-95.156,51.96,-95.157,51.528,-95.158,51.097,-95.159,50.665,-95.159,50.234,-95.16,49.802,-95.155,49.37]},{"z":2,"b":[-102.002,60.001,-94.772,60.002],"l":[-102.002,60.001,-101.552,60.001,-101.102,60.001,-100.652,60.001,-100.202,60.001,-99.752,60.001,-99.302,60.001,-98.852,60.001,-98.402,60.001,-97.952,60.001,-97.502,60.001,-97.052,60.001,-96.603,60.001,-96.153,60.001,-95.703,60.001,-95.253,60.001,-94.772,60.002]},{"z":2,"b":[-102.002,48.993,-101.367,60.001],"l":[-102.002,60.001,-102.002,59.74,-102.002,59.478,-102.002,59.217,-102.002,58.955,-102.001,58.694,-102.001,58.432,-102.001,58.171,-102.001,57.909,-102.001,57.648,-102.001,57.386,-102.001,57.125,-102.001,56.863,-102.001,56.602,-102.001,56.34,-102.001,56.079,-102.001,55.818,-101.961,55.391,-101.921,54.965,-101.882,54.538,-101.842,54.112,-101.802,53.685,-101.762,53.259,-101.723,52.832,-101.683,52.406,-101.643,51.979,-101.603,51.553,-101.563,51.126,-101.524,50.7,-101.484,50.273,-101.444,49.847,-101.404,49.421,-101.367,48.993]},{"z":2,"b":[-123.221,45.591,-116.896,46.179],"l":[-116.896,46.002,-117.144,46.002,-117.41,46.002,-117.675,46.002,-117.941,46.002,-118.206,46.001,-118.472,46.001,-118.737,46.001,-119.002,46.001,-119.02,46,-119.05,45.981,-119.177,45.945,-119.429,45.934,-119.934,45.838,-120.156,45.762,-120.351,45.725,-120.716,45.69,-121.045,45.639,-121.184,45.644,-121.243,45.687,-121.402,45.712,-121.663,45.718,-121.882,45.689,-122.085,45.603,-122.15,45.594,-122.203,45.591,-122.561,45.625,-122.65,45.627,-122.726,45.674,-122.746,45.75,-122.843,45.978,-122.942,46.115,-123.042,46.16,-123.121,46.179,-123.221,46.154]},{"z":2,"b":[-117.039,46.002,-116.896,48.993],"l":[-116.896,46.002,-116.93,46.056,-116.948,46.124,-116.922,46.164,-116.943,46.232,-117.01,46.33,-117.039,46.397,-117.032,46.42,-117.025,46.429,-117.026,46.589,-117.027,46.75,-117.028,46.91,-117.028,47.07,-117.029,47.231,-117.03,47.391,-117.031,47.551,-117.032,47.712,-117.032,47.872,-117.033,48.032,-117.034,48.192,-117.035,48.353,-117.036,48.513,-117.037,48.673,-117.037,48.834,-117.039,48.993]},{"z":2,"b":[-117.202,42.001,-116.477,46.002],"l":[-117.017,42.001,-117.017,42.224,-117.018,42.447,-117.018,42.67,-117.018,42.893,-117.019,43.117,-117.019,43.34,-117.02,43.563,-117.02,43.808,-117.02,43.813,-116.958,43.964,-116.944,44.038,-116.961,44.079,-116.95,44.119,-116.911,44.157,-116.912,44.199,-116.953,44.245,-117.029,44.276,-117.14,44.29,-117.197,44.344,-117.202,44.436,-117.119,44.578,-116.949,44.77,-116.856,44.906,-116.84,44.985,-116.737,45.169,-116.549,45.457,-116.477,45.641,-116.521,45.722,-116.617,45.799,-116.765,45.873,-116.869,45.958,-116.896,46.002]},{"z":2,"b":[-114.725,32.715,-114.125,34.991],"l":[-114.611,34.991,-114.611,34.907,-114.557,34.795,-114.398,34.59,-114.379,34.541,-114.377,34.54,-114.371,34.488,-114.308,34.433,-114.159,34.355,-114.125,34.317,-114.125,34.314,-114.126,34.287,-114.164,34.253,-114.329,34.142,-114.418,34.051,-114.509,33.904,-114.495,33.785,-114.499,33.686,-114.522,33.608,-114.556,33.536,-114.603,33.47,-114.651,33.431,-114.702,33.418,-114.719,33.331,-114.704,33.17,-114.648,33.072,-114.551,33.037,-114.491,32.984,-114.469,32.912,-114.478,32.842,-114.516,32.773,-114.582,32.735,-114.721,32.724,-114.725,32.715]},{"z":2,"b":[-114.741,34.991,-114.04,37.004],"l":[-114.611,34.991,-114.611,34.998,-114.591,35.353,-114.649,35.476,-114.662,35.545,-114.645,35.631,-114.65,35.684,-114.677,35.73,-114.684,35.814,-114.687,35.917,-114.732,35.984,-114.741,36.014,-114.716,36.085,-114.669,36.122,-114.593,36.148,-114.505,36.156,-114.406,36.148,-114.332,36.116,-114.282,36.06,-114.232,36.032,-114.183,36.03,-114.119,36.077,-114.062,36.175,-114.043,36.182,-114.041,36.612,-114.04,37.004]},{"z":2,"b":[-106.668,31.768,-103,36.501],"l":[-103,36.501,-103.042,36.501,-103.044,36.219,-103.045,35.938,-103.047,35.657,-103.048,35.376,-103.049,35.095,-103.051,34.814,-103.052,34.533,-103.054,34.252,-103.055,33.971,-103.057,33.689,-103.058,33.408,-103.06,33.127,-103.061,32.846,-103.063,32.565,-103.064,32.284,-103.066,32.003,-103.29,32.003,-103.515,32.003,-103.74,32.002,-103.965,32.002,-104.19,32.002,-104.414,32.002,-104.639,32.002,-104.864,32.002,-105.089,32.002,-105.313,32.002,-105.538,32.002,-105.763,32.002,-105.988,32.002,-106.213,32.001,-106.437,32.001,-106.662,32.001,-106.668,32.001,-106.566,31.82,-106.445,31.768]},{"z":2,"b":[-95.347,39.124,-94.622,40.001],"l":[-95.347,40.001,-95.116,39.872,-95.056,39.871,-94.984,39.891,-94.93,39.861,-94.893,39.782,-94.899,39.741,-94.946,39.739,-95.005,39.692,-95.075,39.597,-95.058,39.509,-94.953,39.426,-94.882,39.343,-94.847,39.259,-94.778,39.194,-94.622,39.124]},{"z":2,"b":[-95.765,40.001,-95.347,40.602],"l":[-95.765,40.602,-95.74,40.571,-95.685,40.466,-95.662,40.363,-95.607,40.295,-95.521,40.265,-95.464,40.198,-95.434,40.096,-95.362,40.01,-95.347,40.001]},{"z":2,"b":[-96.481,40.602,-95.765,42.511],"l":[-96.481,42.511,-96.451,42.506,-96.384,42.418,-96.356,42.26,-96.295,42.133,-96.201,42.036,-96.154,41.965,-96.154,41.92,-96.131,41.872,-96.087,41.821,-96.069,41.734,-96.078,41.611,-96.059,41.546,-96.011,41.538,-95.991,41.52,-95.997,41.494,-95.982,41.474,-95.944,41.461,-95.933,41.425,-95.95,41.367,-95.936,41.331,-95.892,41.316,-95.884,41.288,-95.911,41.245,-95.907,41.212,-95.871,41.188,-95.847,41.08,-95.834,40.889,-95.838,40.778,-95.859,40.745,-95.826,40.676,-95.765,40.602]},{"z":2,"b":[-104.029,42.511,-96.481,43.001],"l":[-96.481,42.511,-96.558,42.525,-96.633,42.569,-96.676,42.638,-96.805,42.701,-97.02,42.76,-97.164,42.813,-97.239,42.861,-97.417,42.881,-97.698,42.875,-97.881,42.85,-97.965,42.806,-98.143,42.852,-98.442,43.001,-98.837,43.001,-99.183,43.001,-99.529,43.001,-99.875,43.001,-100.222,43.001,-100.568,43.001,-100.914,43.001,-101.26,43.001,-101.606,43.001,-101.952,43.001,-102.298,43.001,-102.644,43.001,-102.991,43.001,-103.337,43.001,-103.683,43.001,-104.029,43.001]},{"z":2,"b":[-91.656,31.002,-90.938,33.016],"l":[-91.15,33.016,-91.144,32.967,-91.096,32.938,-91.093,32.885,-91.136,32.809,-91.136,32.756,-91.092,32.726,-91.094,32.694,-91.142,32.661,-91.15,32.625,-91.116,32.586,-91.101,32.541,-91.104,32.49,-91.059,32.424,-90.99,32.381,-90.945,32.331,-90.94,32.303,-90.938,32.277,-90.948,32.24,-91.005,32.186,-91.048,32.146,-91.075,32.131,-91.089,32.104,-91.093,32.046,-91.157,31.971,-91.286,31.884,-91.373,31.774,-91.415,31.64,-91.459,31.555,-91.506,31.519,-91.529,31.449,-91.529,31.345,-91.559,31.288,-91.618,31.277,-91.631,31.218,-91.596,31.11,-91.611,31.065,-91.653,31.03,-91.656,31.002]},{"z":2,"b":[-91.215,33.016,-90.282,35.001],"l":[-90.294,35.001,-90.293,34.97,-90.282,34.945,-90.32,34.899,-90.416,34.852,-90.468,34.805,-90.475,34.76,-90.497,34.738,-90.539,34.728,-90.564,34.661,-90.584,34.504,-90.635,34.414,-90.718,34.392,-90.762,34.356,-90.768,34.308,-90.817,34.252,-90.91,34.189,-90.94,34.129,-90.909,34.072,-90.915,34.049,-90.943,34.03,-91.043,34.003,-91.06,33.989,-91.07,33.974,-91.05,33.945,-91.038,33.901,-91.053,33.828,-91.077,33.797,-91.1,33.767,-91.184,33.716,-91.207,33.676,-91.17,33.647,-91.173,33.617,-91.215,33.583,-91.203,33.547,-91.193,33.523,-91.154,33.5,-91.141,33.439,-91.168,33.359,-91.163,33.294,-91.117,33.241,-91.107,33.218,-91.153,33.041,-91.15,33.016]},{"z":2,"b":[-90.294,35.001,-89.689,36.002],"l":[-89.705,36.002,-89.709,35.983,-89.689,35.944,-89.689,35.921,-89.704,35.907,-89.738,35.899,-89.764,35.889,-89.769,35.872,-89.77,35.864,-89.768,35.845,-89.764,35.828,-89.78,35.806,-89.895,35.751,-89.944,35.679,-89.927,35.592,-89.967,35.503,-90.066,35.414,-90.108,35.314,-90.093,35.204,-90.142,35.114,-90.256,35.046,-90.294,35.004,-90.294,35.001]},{"z":2,"b":[-89.705,36.002,-89.541,36.501],"l":[-89.557,36.501,-89.541,36.427,-89.555,36.373,-89.604,36.352,-89.612,36.322,-89.584,36.288,-89.584,36.272,-89.595,36.259,-89.628,36.255,-89.654,36.248,-89.675,36.221,-89.634,36.168,-89.642,36.105,-89.698,36.03,-89.705,36.002]},{"z":2,"b":[-89.56,36.501,-89.487,36.555],"l":[-89.487,36.503,-89.497,36.529,-89.52,36.555,-89.536,36.555,-89.55,36.553,-89.555,36.541,-89.56,36.513,-89.557,36.501]},{"z":2,"b":[-89.487,36.489,-89.45,36.503],"l":[-89.45,36.5,-89.452,36.498,-89.471,36.489,-89.485,36.498,-89.487,36.503]},{"z":2,"b":[-89.45,36.5,-89.122,36.992],"l":[-89.154,36.992,-89.122,36.912,-89.13,36.811,-89.156,36.716,-89.203,36.626,-89.256,36.594,-89.28,36.6,-89.331,36.616,-89.351,36.611,-89.397,36.588,-89.45,36.5]},{"z":2,"b":[-91.502,36.992,-89.154,40.379],"l":[-91.442,40.379,-91.5,40.226,-91.502,40.098,-91.467,39.943,-91.426,39.827,-91.379,39.749,-91.21,39.603,-90.919,39.387,-90.737,39.182,-90.664,38.987,-90.577,38.91,-90.478,38.948,-90.354,38.93,-90.22,38.898,-90.15,38.86,-90.134,38.847,-90.127,38.833,-90.132,38.819,-90.151,38.781,-90.317,38.458,-90.374,38.31,-90.366,38.254,-90.299,38.174,-90.174,38.069,-90.064,38.001,-89.97,37.969,-89.823,37.884,-89.623,37.747,-89.523,37.645,-89.523,37.576,-89.503,37.506,-89.462,37.434,-89.462,37.366,-89.503,37.301,-89.487,37.213,-89.414,37.102,-89.352,37.036,-89.301,37.012,-89.29,37.013,-89.286,37.023,-89.293,37.038,-89.296,37.052,-89.292,37.071,-89.281,37.078,-89.272,37.08,-89.225,37.053,-89.154,36.992]},{"z":2,"b":[-91.442,40.379,-90.149,42.513],"l":[-90.651,42.513,-90.59,42.445,-90.485,42.383,-90.425,42.326,-90.41,42.272,-90.347,42.215,-90.233,42.154,-90.168,42.1,-90.149,42.054,-90.152,41.982,-90.177,41.884,-90.218,41.813,-90.276,41.77,-90.319,41.71,-90.348,41.632,-90.437,41.56,-90.587,41.495,-90.754,41.45,-90.939,41.426,-91.048,41.369,-91.082,41.28,-91.067,41.206,-91.001,41.146,-90.964,41.083,-90.954,41.016,-91,40.905,-91.102,40.75,-91.215,40.65,-91.339,40.604,-91.394,40.537,-91.381,40.448,-91.391,40.397,-91.442,40.379]},{"z":2,"b":[-91.245,42.513,-90.651,43.502],"l":[-91.245,43.502,-91.238,43.44,-91.219,43.395,-91.178,43.359,-91.117,43.331,-91.107,43.278,-91.148,43.2,-91.161,43.102,-91.148,42.986,-91.118,42.882,-91.07,42.789,-90.96,42.72,-90.789,42.677,-90.688,42.61,-90.657,42.521,-90.651,42.513]},{"z":2,"b":[-92.897,43.502,-89.958,47.309],"l":[-91.245,43.502,-91.278,43.797,-91.32,43.936,-91.383,43.991,-91.515,44.054,-91.716,44.126,-91.841,44.194,-91.912,44.289,-92.055,44.4,-92.149,44.445,-92.24,44.462,-92.313,44.498,-92.367,44.552,-92.442,44.587,-92.537,44.601,-92.644,44.645,-92.77,44.726,-92.773,44.732,-92.799,44.79,-92.785,44.823,-92.763,44.934,-92.765,44.969,-92.793,45.071,-92.759,45.111,-92.766,45.236,-92.754,45.278,-92.686,45.381,-92.683,45.433,-92.707,45.494,-92.757,45.543,-92.892,45.595,-92.897,45.659,-92.875,45.706,-92.786,45.796,-92.738,45.875,-92.694,45.909,-92.425,46.028,-92.367,46.036,-92.325,46.07,-92.293,46.084,-92.292,46.228,-92.292,46.373,-92.292,46.517,-92.292,46.661,-92.224,46.671,-92.195,46.707,-92.125,46.763,-92.093,46.762,-91.965,46.73,-91.841,46.779,-91.717,46.827,-91.593,46.876,-91.468,46.925,-91.265,47.021,-91.061,47.117,-90.858,47.213,-90.654,47.309,-90.48,47.304,-90.306,47.298,-90.132,47.293,-89.958,47.287]},{"z":2,"b":[-89.165,36.992,-88.042,37.789],"l":[-88.042,37.789,-88.073,37.771,-88.129,37.7,-88.14,37.621,-88.108,37.534,-88.191,37.465,-88.389,37.411,-88.495,37.364,-88.507,37.324,-88.493,37.262,-88.453,37.178,-88.446,37.12,-88.472,37.088,-88.614,37.11,-88.873,37.186,-89.046,37.183,-89.135,37.102,-89.165,37.042,-89.154,36.992]},{"z":2,"b":[-88.042,37.788,-84.79,39.092],"l":[-84.822,39.092,-84.837,39.083,-84.861,39.04,-84.835,38.996,-84.837,38.961,-84.864,38.933,-84.854,38.913,-84.807,38.9,-84.79,38.868,-84.801,38.819,-84.901,38.77,-85.094,38.714,-85.136,38.697,-85.17,38.69,-85.186,38.694,-85.211,38.704,-85.247,38.727,-85.307,38.737,-85.405,38.73,-85.445,38.683,-85.425,38.597,-85.459,38.53,-85.546,38.484,-85.609,38.423,-85.646,38.347,-85.706,38.301,-85.788,38.285,-85.855,38.215,-85.906,38.091,-85.961,38.014,-86.021,37.984,-86.103,37.992,-86.208,38.037,-86.268,38.083,-86.284,38.13,-86.315,38.164,-86.343,38.179,-86.361,38.179,-86.363,38.172,-86.355,38.159,-86.365,38.145,-86.43,38.14,-86.457,38.12,-86.448,38.086,-86.463,38.062,-86.5,38.048,-86.52,38.015,-86.522,37.964,-86.549,37.917,-86.599,37.875,-86.673,37.889,-86.77,37.96,-86.869,37.976,-86.969,37.938,-87.044,37.886,-87.094,37.821,-87.141,37.805,-87.209,37.855,-87.372,37.923,-87.457,37.938,-87.518,37.922,-87.561,37.929,-87.576,37.948,-87.587,37.956,-87.599,37.946,-87.601,37.887,-87.615,37.849,-87.643,37.833,-87.665,37.846,-87.679,37.886,-87.714,37.9,-87.77,37.885,-87.825,37.891,-87.874,37.916,-87.892,37.913,-87.906,37.902,-87.902,37.855,-87.912,37.818,-87.938,37.794,-87.975,37.788,-88.022,37.8,-88.042,37.789]},{"z":2,"b":[-84.822,38.448,-82.613,39.129],"l":[-82.613,38.448,-82.621,38.451,-82.758,38.541,-82.845,38.628,-82.88,38.71,-83,38.714,-83.203,38.639,-83.357,38.627,-83.461,38.68,-83.538,38.7,-83.616,38.683,-83.69,38.65,-83.752,38.669,-83.828,38.731,-83.948,38.774,-84.113,38.798,-84.227,38.865,-84.29,38.975,-84.358,39.05,-84.43,39.091,-84.509,39.103,-84.595,39.086,-84.661,39.094,-84.707,39.129,-84.766,39.125,-84.822,39.092]},{"z":2,"b":[-82.613,38.415,-80.519,40.647],"l":[-80.519,40.647,-80.638,40.603,-80.659,40.563,-80.623,40.512,-80.604,40.445,-80.602,40.364,-80.668,40.185,-80.804,39.909,-80.872,39.736,-80.874,39.664,-80.963,39.571,-81.139,39.456,-81.263,39.387,-81.333,39.363,-81.392,39.365,-81.44,39.393,-81.493,39.374,-81.552,39.308,-81.606,39.274,-81.657,39.272,-81.701,39.229,-81.737,39.147,-81.771,39.099,-81.803,39.086,-81.805,39.044,-81.778,38.973,-81.78,38.947,-81.795,38.946,-81.809,38.95,-81.844,38.934,-81.882,38.894,-81.897,38.894,-81.907,38.9,-81.919,38.952,-81.95,38.989,-82.001,39.011,-82.072,38.963,-82.163,38.845,-82.204,38.768,-82.188,38.715,-82.18,38.638,-82.202,38.606,-82.252,38.594,-82.291,38.552,-82.318,38.48,-82.387,38.434,-82.498,38.415,-82.613,38.448]},{"z":2,"b":[-88.095,37.789,-87.221,41.761],"l":[-88.042,37.789,-88.095,37.876,-88.091,37.897,-88.059,37.898,-88.042,37.937,-88.045,38.013,-88.026,38.06,-87.996,38.074,-87.99,38.089,-88.005,38.098,-88.006,38.113,-87.958,38.144,-87.949,38.178,-87.977,38.214,-87.98,38.249,-87.957,38.282,-87.921,38.298,-87.873,38.295,-87.827,38.336,-87.784,38.419,-87.735,38.47,-87.681,38.488,-87.636,38.543,-87.584,38.679,-87.529,38.742,-87.518,38.812,-87.53,38.909,-87.568,39.007,-87.632,39.107,-87.647,39.169,-87.615,39.193,-87.603,39.234,-87.611,39.291,-87.579,39.344,-87.533,39.375,-87.533,39.379,-87.531,39.662,-87.53,39.962,-87.528,40.262,-87.527,40.562,-87.525,40.861,-87.524,41.161,-87.523,41.461,-87.521,41.761,-87.371,41.761,-87.221,41.761]},{"z":2,"b":[-88.486,30.371,-88.085,34.999],"l":[-88.173,34.999,-88.085,34.933,-88.088,34.909,-88.089,34.893,-88.096,34.846,-88.12,34.661,-88.144,34.477,-88.169,34.292,-88.193,34.107,-88.218,33.923,-88.242,33.738,-88.267,33.553,-88.291,33.368,-88.315,33.184,-88.34,32.999,-88.364,32.814,-88.389,32.629,-88.413,32.445,-88.437,32.26,-88.462,32.075,-88.486,31.891,-88.476,31.703,-88.465,31.515,-88.455,31.327,-88.444,31.14,-88.434,30.952,-88.423,30.764,-88.413,30.577,-88.4,30.371]},{"z":2,"b":[-88.203,34.999,-85.624,35.025],"l":[-85.624,35.001,-85.784,35.002,-85.944,35.004,-86.105,35.005,-86.265,35.007,-86.426,35.008,-86.586,35.01,-86.747,35.011,-86.907,35.013,-87.067,35.014,-87.228,35.016,-87.388,35.017,-87.549,35.019,-87.709,35.02,-87.87,35.022,-88.03,35.023,-88.19,35.025,-88.203,35.024,-88.191,35.012,-88.173,34.999]},{"z":2,"b":[-89.45,36.5,-83.668,36.693],"l":[-83.668,36.605,-83.675,36.603,-83.683,36.6,-83.691,36.598,-83.699,36.596,-83.707,36.594,-83.715,36.591,-83.723,36.589,-83.73,36.587,-84.078,36.597,-84.425,36.606,-84.772,36.616,-85.119,36.626,-85.466,36.636,-85.813,36.645,-86.16,36.655,-86.508,36.665,-86.84,36.66,-87.173,36.654,-87.506,36.649,-87.839,36.644,-87.869,36.675,-87.956,36.682,-88.091,36.693,-88.096,36.693,-88.057,36.59,-88.056,36.501,-88.059,36.5,-88.216,36.5,-88.388,36.5,-88.561,36.5,-88.734,36.5,-88.906,36.5,-89.079,36.5,-89.252,36.5,-89.45,36.5]},{"z":2,"b":[-83.668,36.598,-81.659,36.619],"l":[-81.659,36.611,-81.801,36.615,-81.905,36.619,-81.953,36.598,-82.162,36.599,-82.329,36.6,-82.612,36.601,-82.956,36.602,-83.441,36.604,-83.668,36.605]},{"z":2,"b":[-81.659,36.551,-75.857,36.611],"l":[-75.857,36.551,-75.889,36.551,-75.966,36.551,-76.215,36.553,-76.577,36.556,-76.938,36.559,-77.3,36.561,-77.662,36.564,-78.024,36.567,-78.386,36.57,-78.747,36.573,-79.109,36.575,-79.471,36.578,-79.833,36.581,-80.194,36.584,-80.556,36.587,-80.918,36.589,-81.28,36.592,-81.642,36.595,-81.659,36.611]},{"z":2,"b":[-85.007,30.371,-81.504,31.002],"l":[-85.007,31.002,-84.966,30.923,-84.887,30.759,-84.884,30.721,-84.79,30.715,-84.631,30.706,-84.472,30.696,-84.312,30.687,-84.153,30.678,-83.993,30.668,-83.834,30.659,-83.675,30.65,-83.515,30.64,-83.356,30.631,-83.196,30.622,-83.037,30.612,-82.877,30.603,-82.718,30.594,-82.559,30.584,-82.399,30.575,-82.24,30.566,-82.239,30.533,-82.213,30.481,-82.206,30.422,-82.194,30.393,-82.163,30.376,-82.112,30.371,-82.062,30.405,-82.03,30.478,-82.022,30.56,-82.041,30.653,-82.038,30.732,-82.013,30.778,-81.983,30.789,-81.957,30.814,-81.936,30.82,-81.88,30.806,-81.673,30.74,-81.504,30.731]},{"z":2,"b":[-85.624,31.002,-84.922,35.001],"l":[-85.007,31.002,-85.067,31.118,-85.093,31.296,-85.055,31.573,-85.067,31.635,-85.125,31.777,-85.124,31.881,-85.064,32.051,-85.065,32.051,-85.035,32.147,-84.993,32.202,-84.93,32.247,-84.922,32.286,-84.968,32.32,-84.986,32.361,-84.974,32.409,-85.015,32.526,-85.157,32.804,-85.176,32.898,-85.201,32.956,-85.227,33.084,-85.254,33.212,-85.28,33.34,-85.307,33.467,-85.333,33.595,-85.359,33.723,-85.386,33.851,-85.412,33.979,-85.439,34.106,-85.465,34.234,-85.492,34.362,-85.518,34.49,-85.544,34.617,-85.571,34.745,-85.597,34.873,-85.624,35.001]},{"z":2,"b":[-83.356,32.03,-80.872,35.001],"l":[-83.115,35.001,-83.121,35.001,-83.17,34.933,-83.317,34.806,-83.356,34.708,-83.167,34.6,-83.166,34.599,-83.053,34.511,-82.976,34.476,-82.897,34.466,-82.848,34.437,-82.819,34.366,-82.589,34.017,-82.589,34.018,-82.352,33.838,-82.257,33.749,-82.208,33.664,-82.006,33.523,-81.936,33.447,-81.933,33.39,-81.909,33.349,-81.864,33.325,-81.821,33.274,-81.78,33.195,-81.699,33.127,-81.578,33.069,-81.49,32.936,-81.434,32.728,-81.377,32.607,-81.29,32.557,-81.172,32.38,-81.133,32.275,-81.135,32.182,-81.075,32.11,-80.872,32.03]},{"z":2,"b":[-77.122,38.807,-77.02,38.944],"l":[-77.122,38.944,-77.102,38.936,-77.053,38.915,-77.03,38.889,-77.02,38.807]},{"z":2,"b":[-77.727,38.944,-77.122,39.346],"l":[-77.727,39.346,-77.669,39.31,-77.534,39.266,-77.479,39.221,-77.503,39.175,-77.506,39.143,-77.481,39.113,-77.301,39.053,-77.19,38.969,-77.122,38.944]},{"z":2,"b":[-79.488,39.211,-77.727,39.686],"l":[-79.488,39.211,-79.36,39.285,-79.294,39.312,-79.075,39.476,-79.047,39.477,-78.971,39.454,-78.815,39.57,-78.796,39.613,-78.778,39.627,-78.758,39.625,-78.746,39.614,-78.753,39.59,-78.742,39.578,-78.678,39.55,-78.587,39.534,-78.575,39.533,-78.496,39.533,-78.46,39.556,-78.443,39.601,-78.407,39.628,-78.325,39.639,-78.181,39.686,-78.098,39.678,-78.027,39.631,-77.956,39.609,-77.883,39.611,-77.855,39.6,-77.872,39.576,-77.853,39.548,-77.798,39.517,-77.78,39.488,-77.797,39.461,-77.792,39.442,-77.762,39.429,-77.745,39.4,-77.739,39.354,-77.727,39.346]},{"z":2,"b":[-79.559,45.214,-74.34,51.545],"l":[-79.521,51.545,-79.52,51.226,-79.52,50.966,-79.52,50.919,-79.519,50.742,-79.519,50.5,-79.518,50.258,-79.518,50.017,-79.517,49.775,-79.517,49.533,-79.516,49.291,-79.516,49.049,-79.515,48.807,-79.515,48.565,-79.514,48.323,-79.514,48.082,-79.513,47.84,-79.513,47.556,-79.513,47.551,-79.559,47.483,-79.547,47.407,-79.479,47.319,-79.442,47.228,-79.435,47.134,-79.365,47.022,-79.166,46.827,-79.015,46.607,-78.903,46.486,-78.781,46.393,-78.46,46.307,-77.683,46.187,-77.373,46.064,-77.371,46.063,-77.293,45.977,-77.282,45.962,-77.267,45.929,-77.233,45.904,-77.101,45.833,-77.031,45.807,-76.977,45.798,-76.941,45.801,-76.926,45.82,-76.913,45.876,-76.89,45.89,-76.872,45.887,-76.824,45.88,-76.754,45.798,-76.667,45.643,-76.553,45.542,-76.413,45.496,-76.27,45.484,-76.123,45.505,-76.001,45.484,-75.904,45.42,-75.626,45.451,-75.167,45.575,-74.864,45.642,-74.717,45.651,-74.577,45.632,-74.397,45.568,-74.415,45.509,-74.453,45.394,-74.478,45.318,-74.413,45.269,-74.34,45.214]},{"z":4.7,"b":[47.949,52.78,50.256,54.516],"l":[50.077,54.516,50.119,54.437,50.206,54.418,50.256,54.305,50.197,54.038,49.979,53.966,49.957,53.907,49.827,53.887,49.624,53.815,49.414,53.868,49.299,53.84,49.029,53.857,48.874,53.716,48.491,53.771,48.456,53.666,48.331,53.624,48.376,53.471,48.057,53.476,47.949,53.359,48.035,53.315,48.144,53.162,48.149,53.059,48.222,52.999,48.39,52.97,48.522,52.978,48.593,52.919,48.54,52.83,48.437,52.78]},{"z":4.7,"b":[45.792,52.616,46.983,54.005],"l":[46.831,52.616,46.852,52.726,46.983,52.771,46.935,52.91,46.913,53.079,46.954,53.191,46.906,53.335,46.795,53.408,46.494,53.511,46.468,53.654,46.193,53.85,46.212,53.909,45.957,54.005,45.792,54.004]},{"z":4.7,"b":[50.072,56.1,51.436,56.67],"l":[50.072,56.623,50.35,56.67,50.497,56.511,50.586,56.367,50.706,56.33,50.851,56.39,50.908,56.296,50.837,56.248,51.073,56.131,51.224,56.1,51.436,56.144]},{"z":4.7,"b":[51.399,55.858,53.632,56.538],"l":[51.436,56.144,51.501,56.082,51.399,55.994,51.442,55.925,51.633,55.952,51.829,55.918,51.938,55.974,52.064,55.896,52.168,55.899,52.242,55.965,52.195,56.077,52.439,56.033,52.473,56.074,52.655,56.018,52.794,56.095,52.839,56.163,52.711,56.23,52.562,56.259,52.733,56.378,52.908,56.416,52.934,56.538,53.08,56.527,52.945,56.366,52.958,56.252,53.273,56.085,53.336,56.157,53.26,56.254,53.427,56.277,53.551,56.241,53.564,56.181,53.405,56.023,53.276,55.935,53.289,55.858,53.632,55.907]},{"z":4.7,"b":[47.242,54.678,48.412,55.829],"l":[47.242,54.678,47.37,54.753,47.379,54.832,47.487,54.858,47.729,54.831,47.803,54.945,47.996,54.968,48.075,55.108,48.017,55.276,47.913,55.316,47.763,55.214,47.703,55.328,47.834,55.342,47.974,55.5,48.101,55.545,48.025,55.63,48.205,55.694,48.412,55.829]},{"z":4.7,"b":[48.401,55.829,50.18,56.623],"l":[48.412,55.829,48.401,55.888,48.694,55.965,48.69,56.068,48.835,56.072,49.016,56.233,49.037,56.358,49.276,56.353,49.558,56.441,49.578,56.505,49.765,56.536,49.885,56.442,50.025,56.46,50.18,56.549,50.072,56.623]},{"z":4.7,"b":[50.077,54.33,52.539,54.642],"l":[52.539,54.377,52.482,54.467,52.223,54.445,52.099,54.388,51.933,54.436,51.936,54.528,51.668,54.567,51.513,54.639,51.391,54.601,51.296,54.642,51.169,54.637,51.093,54.554,51.004,54.539,50.948,54.348,50.697,54.427,50.519,54.33,50.384,54.486,50.077,54.516]},{"z":4.7,"b":[47.242,54.51,50.077,54.884],"l":[50.077,54.516,49.975,54.51,49.746,54.574,49.616,54.562,49.476,54.775,49.291,54.884,49.118,54.795,49.014,54.793,48.828,54.648,48.641,54.66,48.47,54.634,48.29,54.738,48.086,54.766,47.85,54.704,47.754,54.731,47.637,54.565,47.366,54.527,47.351,54.593,47.242,54.678]},{"z":4.7,"b":[42.094,52.403,43.237,53.812],"l":[42.134,53.812,42.094,53.54,42.312,53.44,42.354,53.368,42.748,53.022,42.905,52.97,43.095,52.822,43.237,52.661,43.042,52.561,43.011,52.482,43.08,52.403]},{"z":4.7,"b":[84.41,56.044,88.624,56.833],"l":[88.624,56.833,88.552,56.787,88.419,56.829,88.144,56.719,87.779,56.548,87.62,56.64,87.397,56.616,87.167,56.676,87.13,56.54,86.913,56.556,86.707,56.632,86.532,56.569,86.394,56.549,86.313,56.627,86.217,56.624,86.112,56.497,85.784,56.406,85.791,56.361,85.606,56.324,85.688,56.239,85.379,56.213,85.254,56.245,85.121,56.216,85.009,56.133,84.893,56.17,84.41,56.044]},{"z":4.7,"b":[38.895,53.342,40.16,53.588],"l":[38.895,53.565,39.041,53.55,39.162,53.395,39.415,53.414,39.524,53.462,39.512,53.525,39.74,53.588,39.907,53.499,39.918,53.416,40.16,53.342]},{"z":4.7,"b":[40.16,53.342,42.134,53.823],"l":[40.16,53.342,40.317,53.356,40.502,53.461,40.738,53.56,40.84,53.487,41.021,53.49,41.174,53.434,41.354,53.556,41.349,53.741,41.4,53.783,41.748,53.823,42.134,53.812]},{"z":4.7,"b":[37.727,52.017,38.038,53.009],"l":[37.747,52.017,37.778,52.161,37.948,52.246,38.038,52.345,37.911,52.473,37.783,52.508,37.727,52.597,37.813,52.648,37.777,52.719,37.871,52.772,37.79,52.845,37.839,52.891,37.841,53.009]},{"z":4.7,"b":[37.841,52.974,38.895,53.565],"l":[37.841,53.009,37.912,53.073,38.108,53.059,38.307,52.974,38.473,53.041,38.595,53.049,38.633,53.123,38.595,53.24,38.477,53.31,38.762,53.382,38.697,53.5,38.895,53.565]},{"z":4.7,"b":[75.159,58.65,84.26,61.047],"l":[84.26,60.856,83.997,60.826,83.616,60.982,83.504,61.047,83.144,61.028,82.407,60.723,82.362,60.601,82.139,60.532,81.83,60.645,81.498,60.615,81.141,60.637,81.017,60.757,80.723,60.797,80.414,60.761,80.165,60.664,79.853,60.692,79.503,60.687,79.36,60.664,79.285,60.726,79.314,60.792,79.169,60.837,78.806,60.78,78.675,60.828,78.474,60.777,78.164,60.803,77.941,60.749,77.702,60.825,77.408,60.811,77.136,60.856,77.068,60.726,76.97,60.649,77.054,60.578,77.013,60.527,76.78,60.452,76.749,60.347,76.848,60.266,76.699,60.117,76.758,60.057,76.741,59.758,76.656,59.693,76.645,59.585,76.44,59.545,76.172,59.543,76.007,59.428,75.895,59.426,75.843,59.323,75.628,59.238,75.687,59.04,75.642,58.982,75.373,58.786,75.159,58.65]},{"z":4.7,"b":[39.953,51.954,40.697,53.342],"l":[40.16,53.342,40.107,53.276,40.141,53.072,40.07,52.925,40.08,52.804,39.979,52.769,39.953,52.654,40.067,52.561,40.203,52.512,40.331,52.403,40.477,52.374,40.697,52.27,40.616,52.129,40.632,52.066,40.507,52.022,40.504,51.954]},{"z":4.7,"b":[46.341,58.075,47.602,59.611],"l":[47.114,59.611,47.056,59.379,47.252,59.355,47.239,59.22,47.571,59.06,47.602,58.907,47.452,58.921,47.282,58.893,47.313,58.807,47.26,58.756,47.12,58.775,46.947,58.607,46.687,58.567,46.554,58.488,46.541,58.397,46.424,58.347,46.407,58.209,46.341,58.075]},{"z":4.7,"b":[43.727,57.431,46.341,58.08],"l":[46.341,58.075,45.817,58.047,45.706,57.974,45.557,57.939,45.462,58.035,45.294,58.041,45.155,58.08,44.863,58.075,44.793,57.955,44.865,57.88,44.728,57.704,44.612,57.707,44.483,57.646,44.303,57.517,43.995,57.44,43.846,57.431,43.727,57.466]},{"z":4.7,"b":[35.931,53.618,37.259,54.836],"l":[37.259,54.836,37.208,54.688,37.089,54.549,36.805,54.61,36.848,54.472,36.965,54.396,36.891,54.283,36.714,54.317,36.461,54.288,36.354,54.223,36.237,54.229,36.201,54.098,36.111,53.961,35.989,53.916,35.931,53.786,36.01,53.618]},{"z":4.7,"b":[40.926,58.579,47.114,59.633],"l":[40.926,58.579,41.218,58.74,41.146,58.785,41.245,58.902,41.522,59.037,41.687,59.08,42.012,59.118,42.09,59.23,41.959,59.278,42.058,59.427,42.188,59.391,42.197,59.303,42.632,59.308,42.671,59.228,42.8,59.17,43.026,59.208,43.151,59.261,43.294,59.218,43.371,59.28,43.498,59.294,43.582,59.188,43.742,59.26,43.96,59.205,44.103,59.23,44.207,59.172,44.391,59.153,44.991,59.122,45.091,59.184,45.366,59.198,45.918,59.184,46.004,59.282,46.182,59.345,46.356,59.347,46.406,59.633,46.757,59.607,46.875,59.632,47.114,59.611]},{"z":4.7,"b":[35.37,55.194,36.904,55.319],"l":[35.37,55.239,35.465,55.22,35.579,55.29,35.755,55.287,35.856,55.238,36.238,55.194,36.423,55.319,36.586,55.309,36.904,55.229]},{"z":4.7,"b":[37.12,54.836,37.259,54.991],"l":[37.12,54.991,37.259,54.836]},{"z":4.7,"b":[36.904,54.991,37.12,55.229],"l":[36.904,55.229,37.084,55.096,37.12,54.991]},{"z":4.7,"b":[42.467,54.476,46.056,55.177],"l":[42.467,54.871,42.636,54.787,43.098,54.799,43.155,54.903,43.369,54.95,43.61,54.901,43.689,54.831,43.949,54.807,43.939,54.742,44.091,54.734,44.107,54.646,44.376,54.59,44.54,54.53,44.597,54.58,44.74,54.516,44.917,54.476,45.114,54.494,45.029,54.568,45.091,54.659,45.041,54.714,45.124,54.796,45.209,54.757,45.266,54.873,45.424,54.882,45.439,55.033,45.605,55.164,45.71,55.177,45.74,55.077,45.815,55,45.954,54.997,46.056,55.042]},{"z":4.7,"b":[46.056,54.772,46.441,55.042],"l":[46.056,55.042,46.415,54.895,46.441,54.772]},{"z":4.7,"b":[36.01,53.009,37.841,53.618],"l":[36.01,53.618,36.098,53.556,36.22,53.57,36.398,53.429,36.535,53.391,36.667,53.416,36.807,53.297,37.088,53.28,37.197,53.251,37.327,53.303,37.568,53.289,37.561,53.169,37.787,53.089,37.841,53.009]},{"z":4.7,"b":[42.206,53.847,42.699,54.871],"l":[42.308,53.847,42.437,54.012,42.319,54.099,42.535,54.083,42.596,54.177,42.463,54.254,42.273,54.171,42.206,54.231,42.381,54.264,42.527,54.367,42.625,54.373,42.557,54.492,42.699,54.582,42.524,54.723,42.419,54.736,42.391,54.807,42.467,54.871]},{"z":4.7,"b":[88.365,52.168,89.26,55.446],"l":[88.609,55.446,88.396,55.279,88.55,55.083,88.547,55.023,88.66,54.903,88.748,54.89,88.753,54.805,88.613,54.697,88.603,54.522,88.519,54.416,88.561,54.288,88.798,54.407,88.955,54.369,89.004,54.29,89.129,54.35,89.219,54.243,89.187,54.147,89.047,54.117,88.981,54.065,89.073,53.953,89.26,53.835,89.181,53.781,89.018,53.742,89.011,53.679,88.834,53.585,88.895,53.387,89.051,53.249,89.049,53.13,88.895,52.987,89.118,52.92,89.236,52.81,89.043,52.678,89.115,52.626,88.945,52.488,88.676,52.352,88.688,52.257,88.365,52.168]},{"z":4.7,"b":[35.104,53.272,36.01,53.644],"l":[35.104,53.311,35.205,53.272,35.356,53.284,35.422,53.404,35.637,53.543,35.766,53.519,35.911,53.644,36.01,53.618]},{"z":4.7,"b":[42.134,53.812,42.308,53.847],"l":[42.134,53.812,42.308,53.847]},{"z":4.7,"b":[42.308,53.664,45.792,54.017],"l":[42.308,53.847,42.513,53.737,42.631,53.808,42.94,53.801,43.17,53.839,43.106,53.909,43.182,54.001,43.31,54.017,43.425,53.989,43.528,54.015,43.752,53.963,43.933,53.862,43.877,53.81,44.099,53.736,44.206,53.76,44.314,53.701,44.428,53.723,44.534,53.664,44.729,53.7,44.615,53.831,44.617,53.892,44.883,53.969,45.176,53.979,45.339,53.885,45.646,53.93,45.792,54.004]},{"z":4.7,"b":[45.792,54.004,46.734,54.772],"l":[45.792,54.004,45.879,54.031,46.018,54.189,46.379,54.231,46.488,54.284,46.719,54.33,46.734,54.384,46.543,54.406,46.482,54.499,46.533,54.553,46.516,54.653,46.444,54.688,46.441,54.772]},{"z":4.7,"b":[46.441,54.632,47.242,54.772],"l":[46.441,54.772,46.869,54.632,47.063,54.69,47.242,54.678]},{"z":4.7,"b":[42.588,56.42,43.727,57.466],"l":[43.727,57.466,43.689,57.279,43.539,57.262,43.514,57.118,43.67,57.043,43.501,56.906,43.339,56.861,42.978,56.842,42.83,56.802,42.787,56.669,42.902,56.646,42.845,56.552,42.882,56.472,42.648,56.481,42.588,56.42]},{"z":4.7,"b":[46.341,56.966,47.762,58.075],"l":[46.341,58.075,46.43,58.02,47.354,58.026,47.452,57.987,47.703,57.958,47.762,57.876,47.572,57.81,47.521,57.586,47.471,57.529,47.249,57.567,47.054,57.498,46.75,57.517,46.695,57.32,46.79,57.281,46.742,57.205,46.763,57.03,46.715,56.966]},{"z":4.7,"b":[38.675,54.245,40.206,55.309],"l":[38.675,54.245,38.737,54.345,38.909,54.398,38.798,54.451,38.82,54.602,39.216,54.632,39.328,54.748,39.398,54.876,39.488,54.941,39.685,55.021,39.813,54.997,39.888,55.081,39.827,55.183,40.001,55.2,40.206,55.309]},{"z":4.7,"b":[41.783,54.871,42.467,55.119],"l":[41.783,55.119,41.886,55.059,42.167,54.949,42.44,55.013,42.467,54.871]},{"z":4.7,"b":[38.33,55.309,40.206,56.697],"l":[40.206,55.309,40.137,55.434,40.141,55.517,40.037,55.546,40.085,55.613,39.9,55.734,39.823,55.833,39.555,55.776,39.358,55.761,39.31,55.831,39.123,55.83,39.016,55.925,38.839,55.901,38.729,55.953,38.606,55.959,38.562,56.083,38.403,56.264,38.412,56.369,38.33,56.562,38.374,56.697]},{"z":4.7,"b":[38.672,53.565,38.909,54.245],"l":[38.895,53.565,38.909,53.753,38.837,53.76,38.795,53.872,38.841,53.919,38.714,54.001,38.694,54.075,38.764,54.135,38.672,54.177,38.675,54.245]},{"z":4.7,"b":[41.783,55.119,42.906,56.42],"l":[42.588,56.42,42.761,56.306,42.755,56.22,42.906,56.152,42.686,56.122,42.694,56.003,42.565,55.969,42.486,55.852,42.38,55.777,42.281,55.77,42.056,55.613,42.068,55.481,42.117,55.422,41.933,55.33,41.859,55.241,41.87,55.158,41.783,55.119]},{"z":4.7,"b":[40.206,55.109,41.783,55.34],"l":[41.783,55.119,41.658,55.109,41.497,55.157,41.412,55.131,41.21,55.221,41.047,55.183,40.965,55.26,40.729,55.204,40.612,55.207,40.517,55.328,40.345,55.34,40.206,55.309]},{"z":4.7,"b":[37.626,58.49,40.926,58.954],"l":[37.626,58.49,37.717,58.622,37.941,58.641,38.048,58.736,38.478,58.706,38.55,58.777,38.916,58.744,39,58.846,38.94,58.944,39.119,58.954,39.5,58.889,39.62,58.895,39.969,58.759,40.161,58.559,40.293,58.544,40.565,58.591,40.926,58.579]},{"z":4.7,"b":[39.417,56.368,42.588,56.822],"l":[39.417,56.772,39.679,56.822,39.859,56.78,39.879,56.646,39.943,56.53,40.156,56.434,40.491,56.584,40.715,56.598,40.934,56.502,41.053,56.475,41.934,56.517,41.958,56.391,42.066,56.368,42.289,56.471,42.469,56.421,42.588,56.42]},{"z":4.7,"b":[38.257,56.697,38.374,56.77],"l":[38.374,56.697,38.257,56.77]},{"z":4.7,"b":[37.35,56.77,38.306,58.49],"l":[38.257,56.77,38.154,56.832,38.199,56.894,38.25,57.129,38.143,57.197,38.306,57.276,38.27,57.371,38.148,57.337,37.984,57.359,37.794,57.443,37.81,57.608,37.391,57.828,37.367,57.977,37.484,57.981,37.549,58.053,37.673,58.083,37.562,58.165,37.35,58.239,37.449,58.408,37.644,58.429,37.626,58.49]},{"z":4.7,"b":[39.387,56.772,40.445,57.293],"l":[40.445,57.276,40.327,57.293,40.193,57.213,40.064,57.219,39.842,57.177,39.844,57.129,39.52,57.027,39.527,56.956,39.387,56.835,39.417,56.772]},{"z":4.7,"b":[38.374,56.538,39.417,56.772],"l":[39.417,56.772,39.31,56.684,39.337,56.609,39.272,56.538,39.121,56.594,39,56.543,38.847,56.598,38.629,56.581,38.374,56.697]},{"z":4.7,"b":[40.445,57.276,43.727,57.75],"l":[43.727,57.466,43.542,57.567,43.312,57.517,43.264,57.424,42.973,57.388,42.853,57.394,42.851,57.667,42.726,57.702,42.65,57.635,42.404,57.633,42.336,57.579,42.186,57.56,42.026,57.589,41.886,57.686,41.63,57.75,41.567,57.704,41.604,57.524,41.544,57.459,41.434,57.472,41.27,57.426,41.17,57.435,41.073,57.348,40.571,57.316,40.445,57.276]},{"z":4.7,"b":[40.385,57.276,41.164,58.579],"l":[40.445,57.276,40.448,57.441,40.385,57.498,40.499,57.58,40.663,57.581,40.623,57.655,40.626,57.94,40.744,58.052,40.885,58.126,40.889,58.198,41.164,58.361,41.065,58.411,41.034,58.522,40.926,58.579]},{"z":4.7,"b":[53.785,56.359,54.352,58.44],"l":[53.785,58.44,53.886,58.319,53.81,58.228,54.087,57.994,54.156,57.706,54.096,57.607,54.168,57.559,54.188,57.463,54.158,57.314,54.314,57.291,54.295,57.084,54.341,57.012,54.099,56.966,54.088,56.771,53.869,56.713,53.959,56.632,54.168,56.534,54.245,56.432,54.352,56.359]},{"z":4.7,"b":[51.774,60.242,59.349,61.682],"l":[59.349,61.682,59.114,61.615,59.023,61.552,58.692,61.503,57.204,61.511,57.086,61.488,56.702,61.525,56.545,61.435,56.391,61.42,56.257,61.206,55.841,61.25,55.69,61.086,55.316,61.123,55.214,61.019,55.062,61.009,54.98,60.864,53.853,60.98,53.791,60.848,53.354,60.893,53.349,61.037,52.876,61.094,52.79,60.949,52.442,60.98,52.355,60.844,51.89,60.876,51.774,60.606,52.134,60.547,52.138,60.473,52.324,60.432,52.298,60.242]},{"z":4.7,"b":[52.298,58.44,53.922,60.242],"l":[52.298,60.242,52.454,60.201,53.358,60.166,53.457,60.205,53.608,60.143,53.707,60.001,53.658,59.824,53.592,59.709,53.418,59.678,53.354,59.501,53.206,59.364,53.335,59.185,53.434,59.144,53.877,59.093,53.767,59.022,53.726,58.917,53.821,58.821,53.922,58.783,53.75,58.697,53.705,58.595,53.785,58.44]},{"z":4.7,"b":[51.12,56.144,53.785,58.537],"l":[53.785,58.44,53.498,58.45,53.34,58.389,53.165,58.456,53.13,58.537,52.921,58.534,52.87,58.411,52.329,58.45,52.167,58.49,51.971,58.464,51.796,58.361,51.791,58.281,51.673,58.166,51.766,58.141,51.793,58.043,51.885,57.986,51.852,57.842,51.924,57.817,51.862,57.647,51.748,57.546,51.64,57.544,51.601,57.457,51.397,57.478,51.181,57.447,51.12,57.265,51.193,57.203,51.227,57.067,51.359,56.927,51.54,56.883,51.4,56.767,51.379,56.68,51.206,56.67,51.145,56.515,51.273,56.44,51.32,56.329,51.42,56.284,51.436,56.144]},{"z":4.7,"b":[46.715,56.623,50.101,57.291],"l":[46.715,56.966,46.823,56.93,46.951,56.945,47.161,56.901,47.174,56.814,47.509,56.893,47.77,57.06,47.91,56.983,48.202,57.028,48.293,57.163,48.451,57.128,48.569,57.172,48.822,57.164,48.961,57.089,49.039,57.222,49.035,57.288,49.213,57.291,49.15,57.15,49.285,57.039,49.411,57.025,49.529,57.073,49.7,57.073,49.742,56.9,50.017,56.864,50.101,56.792,50.072,56.623]},{"z":4.7,"b":[48.419,59.208,52.298,60.854],"l":[48.419,60.854,48.538,60.564,48.519,60.476,48.755,60.361,48.497,60.218,48.456,60.115,48.493,59.989,48.508,59.715,48.562,59.699,49.073,59.66,49.039,59.5,49.151,59.506,49.363,59.416,49.525,59.397,49.527,59.235,49.664,59.208,49.795,59.233,49.834,59.638,50.04,59.682,49.999,59.777,50.212,59.754,50.386,59.84,50.83,59.871,51.075,60.061,51.307,60.028,51.421,59.943,51.521,59.946,51.802,60.115,51.9,60.244,52.061,60.317,52.298,60.242]},{"z":4.7,"b":[35.174,55.948,38.257,56.944],"l":[38.257,56.77,38.015,56.774,37.974,56.854,37.656,56.944,37.553,56.906,37.499,56.785,37.231,56.773,37.157,56.683,37.135,56.56,37.055,56.518,36.833,56.583,36.537,56.475,36.173,56.408,36.096,56.347,35.983,56.387,35.783,56.392,35.645,56.474,35.458,56.448,35.548,56.371,35.484,56.242,35.379,56.174,35.317,56.054,35.21,56.019,35.174,55.948]},{"z":4.7,"b":[36.904,54.991,37.843,55.904],"l":[37.12,54.991,37.216,55.084,37.425,55.184,37.463,55.379,37.566,55.358,37.659,55.569,37.813,55.649,37.843,55.763,37.801,55.838,37.611,55.904,37.431,55.878,37.348,55.744,37.414,55.66,37.211,55.602,37.175,55.448,36.914,55.426,37.014,55.301,36.904,55.229]},{"z":4.7,"b":[34.73,58.525,36.173,59.103],"l":[36.173,58.525,36.126,58.695,35.836,58.745,35.751,58.827,35.398,58.844,35.266,58.765,35.125,58.815,35.131,58.937,34.831,58.991,34.879,59.043,34.73,59.103]},{"z":4.7,"b":[31,56.931,36.173,58.525],"l":[36.173,58.525,35.941,58.455,35.935,58.423,35.552,58.448,35.394,58.431,35.279,58.359,35.269,58.271,35.035,58.2,34.786,58.242,34.684,58.197,34.651,58.081,34.488,58.146,34.142,58.208,33.869,58.144,33.719,58.158,33.545,58.047,33.686,58.003,33.701,57.927,33.581,57.858,33.537,57.72,33.325,57.655,33.309,57.566,33.216,57.454,33.046,57.437,32.807,57.46,32.668,57.346,32.272,57.195,32.133,57.184,31.967,57.117,31.722,57.106,31.678,57.013,31.546,56.94,31.307,56.969,31.118,56.931,31,56.965]},{"z":4.7,"b":[46.349,59.611,47.114,60.854],"l":[47.043,60.854,47.084,60.819,46.959,60.66,47.049,60.575,46.862,60.507,46.807,60.318,46.75,60.261,46.441,60.271,46.349,60.214,46.404,60.107,46.608,60.067,47.036,60.1,46.923,59.844,46.935,59.789,47.113,59.753,47.114,59.611]},{"z":4.7,"b":[36.173,58.49,37.626,58.866],"l":[37.626,58.49,37.188,58.771,37.122,58.866,36.864,58.843,36.816,58.758,36.669,58.685,36.521,58.558,36.324,58.5,36.173,58.525]},{"z":4.7,"b":[37.259,54.245,38.675,54.836],"l":[37.259,54.836,37.461,54.822,37.564,54.737,37.758,54.736,37.75,54.82,37.939,54.832,38.039,54.714,38.219,54.66,38.462,54.64,38.49,54.58,38.39,54.469,38.514,54.364,38.521,54.289,38.675,54.245]},{"z":4.7,"b":[46.214,55.829,48.412,56.335],"l":[46.214,56.05,46.402,56.119,46.658,56.12,47.056,56.288,47.277,56.335,47.385,56.33,47.437,56.188,47.529,56.154,47.829,56.145,47.905,56.114,48.024,55.937,48.285,55.846,48.412,55.829]},{"z":4.7,"b":[45.651,56.05,46.715,56.966],"l":[46.715,56.966,46.601,56.92,46.336,56.952,46.313,56.907,45.959,56.872,45.859,56.629,45.651,56.591,45.663,56.461,45.789,56.425,45.949,56.433,45.946,56.325,45.874,56.259,46.068,56.182,46.092,56.056,46.214,56.05]},{"z":4.7,"b":[45.92,55.042,46.384,56.05],"l":[46.214,56.05,46.099,55.941,46.094,55.808,45.92,55.688,46.048,55.617,46.14,55.524,46.244,55.524,46.384,55.463,46.254,55.385,46.229,55.273,46.098,55.204,46.056,55.042]},{"z":4.7,"b":[78.483,62.178,86.028,72.395],"l":[78.483,72.395,78.697,72.18,79.419,72.043,79.695,72.058,80.218,71.942,80.339,71.89,80.036,71.831,80.119,71.675,79.868,71.602,79.383,71.615,79.333,71.481,79.143,71.422,79.245,71.317,79.565,71.277,79.954,71.134,80.403,71.091,80.55,71.044,80.552,70.9,80.68,70.805,80.608,70.71,80.753,70.635,80.569,70.461,80.177,70.43,80.095,70.353,79.63,70.221,79.505,70.115,79.208,70.053,79.171,69.978,78.913,69.861,79.116,69.732,79.585,69.588,79.644,69.491,79.854,69.355,80.26,69.356,80.734,69.308,80.967,69.209,81.324,69.286,81.611,69.286,81.626,69.41,81.783,69.429,81.929,69.349,81.772,69.233,81.893,69.183,82.257,69.157,82.497,69.111,82.333,69.062,82.481,68.995,82.546,68.84,83.015,68.684,82.867,68.628,82.536,68.611,82.71,68.414,82.681,68.283,82.55,68.179,82.395,68.177,82.354,67.944,82.059,67.948,81.731,67.908,81.882,67.799,82.1,67.724,82.11,67.609,82.387,67.524,82.256,67.409,82.353,67.328,82.117,67.244,82.796,66.977,83.042,66.891,83.162,66.818,83.293,66.663,83.078,66.579,83.091,66.459,83.262,66.389,83.525,66.173,83.381,66.14,83.296,66.058,83.521,65.908,83.518,65.822,84.031,65.793,84.168,65.703,84.319,65.684,84.291,65.613,84.43,65.551,84.55,65.433,84.586,65.311,84.523,65.213,84.388,65.175,84.313,65.098,84.387,64.994,84.279,64.94,84.47,64.89,84.82,64.931,84.968,64.891,85.184,64.751,85.61,64.828,85.859,64.756,85.812,64.656,85.916,64.588,85.845,64.501,85.951,64.329,86.028,64.28,85.934,64.123,85.948,64.053,85.534,63.937,85.373,63.808,85.333,63.701,85.097,63.534,85.276,63.522,85.378,63.467,85.41,63.353,85.641,63.374,85.469,63.149,85.584,63.054,85.518,62.933,85.324,62.869,85.225,62.801,85.09,62.638,84.958,62.593,84.861,62.451,84.746,62.41,84.436,62.178]},{"z":4.7,"b":[62.018,62.178,84.436,65.717],"l":[84.436,62.178,84.289,62.276,84.237,62.377,84.096,62.375,83.798,62.56,83.533,62.525,83.311,62.457,83.034,62.53,83.01,62.595,82.741,62.68,82.733,62.757,82.288,62.775,82.073,62.8,81.84,62.692,81.6,62.773,81.349,62.821,81.195,62.95,81.075,63.117,80.584,63.083,80.574,63.019,80.073,62.826,79.915,62.792,79.82,62.604,78.793,62.615,78.379,62.568,77.967,62.478,77.889,62.557,77.327,62.71,77.132,62.813,77.026,62.975,76.623,63.038,76.289,62.95,76.24,63.019,75.803,63.114,75.7,63.091,75.364,63.108,74.915,63.032,74.507,63.043,74.241,63.172,73.368,63.207,73.284,63.315,73.104,63.436,72.907,63.393,72.655,63.28,72.161,63.299,71.964,63.197,71.805,63.219,71.608,63.185,71.569,63.551,71.388,63.675,70.914,63.692,70.813,63.822,70.638,63.903,70.57,64.015,70.755,64.1,70.769,64.194,70.672,64.237,70.473,64.252,70.307,64.344,70,64.316,69.825,64.341,69.693,64.4,69.39,64.389,69.244,64.466,69.015,64.472,68.859,64.436,68.99,64.33,68.811,64.221,68.425,64.296,68.179,64.176,67.936,64.118,67.809,64.032,67.679,64.079,67.281,64.081,67.171,64.163,66.819,64.226,66.796,64.321,66.892,64.383,66.936,64.487,66.682,64.557,66.434,64.515,66.146,64.586,65.883,64.569,65.4,64.571,65.176,64.446,64.995,64.466,64.761,64.427,64.622,64.439,64.444,64.356,64.016,64.357,63.896,64.287,63.611,64.268,63.417,64.324,63.503,64.371,63.467,64.451,63.215,64.526,62.699,64.492,62.532,64.56,62.6,64.645,62.803,64.726,62.799,64.847,62.679,64.878,62.801,65.051,62.817,65.292,62.517,65.339,62.374,65.449,62.411,65.515,62.278,65.623,62.018,65.717]},{"z":4.7,"b":[163.137,61.815,174.468,64.931],"l":[174.454,61.815,174.468,61.979,174.344,62.05,174.117,62.074,174.112,62.189,174.015,62.269,174.093,62.34,174.08,62.447,173.872,62.441,173.798,62.518,173.504,62.534,173.027,62.375,172.853,62.331,172.705,62.402,172.417,62.432,172.319,62.404,172.055,62.467,171.47,62.312,171.218,62.369,170.901,62.279,170.382,62.311,170.262,62.285,170.159,62.342,170.086,62.492,169.986,62.545,170.01,62.677,169.842,62.655,169.657,62.709,169.514,62.814,169.127,62.973,168.939,62.944,168.51,63.054,168.544,63.126,168.796,63.303,169.099,63.44,169.205,63.516,169.509,63.584,169.582,63.643,169.549,63.811,169.364,63.88,169.344,63.95,169.079,63.959,168.874,64.021,168.778,64.185,168.638,64.211,168.499,64.317,168.198,64.339,168.026,64.286,167.815,64.351,167.714,64.341,167.498,64.481,167.414,64.482,167.237,64.624,167,64.55,166.807,64.533,166.406,64.601,166.199,64.609,166.035,64.559,165.743,64.679,165.684,64.754,165.481,64.743,165.329,64.775,165.263,64.851,165.02,64.837,165.004,64.69,164.882,64.686,164.798,64.772,164.216,64.925,163.873,64.865,163.52,64.931,163.345,64.818,163.137,64.77]},{"z":4.7,"b":[157.692,66.359,162.847,69.677],"l":[158.529,66.359,158.368,66.459,158.47,66.488,158.637,66.619,158.716,66.731,158.905,66.803,158.703,67.055,158.299,67.148,157.958,67.258,157.793,67.358,157.876,67.413,157.692,67.545,157.806,67.705,158.329,67.754,158.27,67.823,158.093,67.84,158.277,68.076,158.633,68.142,158.934,68.144,159.111,68.191,159.693,68.239,159.85,68.277,160.997,68.301,161.226,68.384,161.406,68.412,161.711,68.373,162.178,68.355,162.418,68.309,162.535,68.363,162.581,68.501,162.723,68.605,162.606,68.714,162.594,68.796,162.755,68.802,162.847,68.869,162.554,68.991,162.524,69.118,162.774,69.203,162.521,69.325,162.315,69.547,162.432,69.677]},{"z":4.6,"b":[75.222,11.96,75.313,12.076],"l":[75.313,11.96,75.262,12.076,75.222,12.022]},{"z":4.6,"b":[79.705,10.81,79.847,10.986],"l":[79.847,10.81,79.767,10.889,79.705,10.91,79.737,10.986,79.847,10.98]},{"z":4.6,"b":[79.686,11.833,79.85,12],"l":[79.811,11.833,79.727,11.842,79.686,12,79.791,11.917,79.85,11.955]},{"z":4.6,"b":[74.893,11.667,76.416,12.756],"l":[76.416,11.667,76.389,11.744,76.317,11.747,76.177,11.857,76.115,11.857,76.056,11.964,75.961,11.929,75.846,11.951,75.776,12.01,75.761,12.069,75.629,12.102,75.537,12.169,75.426,12.292,75.389,12.295,75.312,12.469,75.102,12.679,74.893,12.756]},{"z":4.6,"b":[76.241,8.285,77.399,11.667],"l":[76.416,11.667,76.339,11.591,76.241,11.604,76.256,11.478,76.409,11.437,76.495,11.385,76.534,11.313,76.466,11.205,76.641,11.209,76.719,11.139,76.69,10.933,76.82,10.876,76.89,10.78,76.876,10.678,76.821,10.633,76.809,10.437,76.842,10.311,76.928,10.236,76.986,10.231,77.065,10.296,77.191,10.351,77.277,10.21,77.224,10.058,77.26,9.962,77.226,9.859,77.235,9.765,77.188,9.613,77.263,9.557,77.341,9.58,77.399,9.5,77.312,9.301,77.255,9.122,77.165,9.001,77.203,8.894,77.249,8.859,77.235,8.784,77.179,8.695,77.204,8.612,77.279,8.526,77.166,8.337,77.11,8.285]},{"z":4.6,"b":[77.257,15.953,77.621,18.308],"l":[77.551,18.308,77.6,18.115,77.563,18.071,77.621,17.987,77.62,17.926,77.5,17.812,77.525,17.746,77.464,17.713,77.412,17.631,77.493,17.578,77.583,17.579,77.599,17.487,77.374,17.223,77.465,17.043,77.433,16.935,77.411,16.721,77.432,16.594,77.371,16.524,77.257,16.449,77.294,16.407,77.533,16.361,77.491,16.262,77.484,16.002,77.446,15.953]},{"z":4.6,"b":[76.416,11.626,78.23,12.873],"l":[78.23,12.76,78.056,12.846,78.002,12.807,77.895,12.873,77.801,12.857,77.765,12.719,77.609,12.673,77.569,12.56,77.619,12.467,77.561,12.34,77.472,12.274,77.492,12.205,77.668,12.204,77.769,12.12,77.671,11.971,77.49,11.926,77.434,11.812,77.354,11.784,77.259,11.817,77.122,11.793,77.083,11.757,76.997,11.826,76.908,11.8,76.821,11.626,76.751,11.642,76.605,11.631,76.532,11.709,76.465,11.715,76.416,11.667]},{"z":4.6,"b":[73.68,15.614,74.101,15.772],"l":[74.101,15.64,73.99,15.614,73.943,15.632,73.908,15.721,73.813,15.772,73.68,15.713]},{"z":4.6,"b":[74.091,14.888,74.304,15.64],"l":[74.101,15.64,74.227,15.63,74.258,15.519,74.237,15.472,74.304,15.289,74.241,15.238,74.293,15.188,74.26,15.098,74.241,14.956,74.091,14.888]},{"z":4.6,"b":[77.503,18.308,80.27,19.866],"l":[80.27,18.714,80.091,18.697,80.002,18.786,79.96,18.867,79.934,19.053,79.862,19.081,79.944,19.169,79.931,19.212,79.965,19.402,79.931,19.483,79.865,19.509,79.796,19.594,79.735,19.603,79.539,19.534,79.42,19.536,79.266,19.59,79.198,19.462,79.059,19.547,79.024,19.542,78.873,19.65,78.848,19.725,78.78,19.777,78.712,19.774,78.534,19.825,78.478,19.799,78.383,19.866,78.322,19.863,78.384,19.685,78.333,19.643,78.303,19.483,78.24,19.46,78.174,19.375,78.168,19.264,78.064,19.264,77.992,19.323,77.92,19.332,77.855,19.228,77.834,19.121,77.752,19.046,77.912,18.835,77.839,18.81,77.781,18.704,77.703,18.669,77.68,18.57,77.592,18.548,77.503,18.382,77.551,18.308]},{"z":4.6,"b":[74.101,15.64,77.551,18.46],"l":[77.551,18.308,77.461,18.282,77.347,18.352,77.302,18.46,77.233,18.411,77.231,18.334,77.118,18.254,77.088,18.197,76.95,18.18,76.893,18.144,76.885,17.974,76.839,17.884,76.741,17.898,76.756,17.826,76.676,17.707,76.593,17.771,76.513,17.773,76.391,17.637,76.322,17.618,76.344,17.352,76.21,17.389,76.095,17.378,75.936,17.326,75.87,17.416,75.784,17.375,75.613,17.472,75.553,17.366,75.622,17.221,75.605,17.158,75.647,17.091,75.617,16.975,75.554,17.015,75.453,16.964,75.371,16.984,75.256,16.959,75.243,16.902,75.167,16.868,75.043,16.953,74.96,16.951,74.922,16.889,74.845,16.852,74.867,16.776,74.685,16.708,74.666,16.606,74.557,16.552,74.525,16.624,74.43,16.644,74.353,16.544,74.292,16.528,74.366,16.293,74.452,16.254,74.47,16.11,74.416,16.096,74.429,15.994,74.354,15.873,74.331,15.762,74.224,15.726,74.186,15.766,74.101,15.64]},{"z":4.6,"b":[74.129,21.075,80.679,22.023],"l":[74.129,21.952,74.205,21.925,74.284,21.945,74.408,22.023,74.486,21.946,74.512,21.727,74.708,21.611,74.894,21.612,75.048,21.554,75.13,21.438,75.312,21.381,75.535,21.372,75.814,21.383,75.878,21.4,76.083,21.368,76.143,21.193,76.109,21.153,76.187,21.086,76.287,21.075,76.437,21.108,76.498,21.18,76.614,21.202,76.654,21.251,76.633,21.341,76.73,21.413,76.786,21.502,76.794,21.577,76.917,21.618,77.029,21.694,77.116,21.724,77.219,21.701,77.333,21.774,77.461,21.77,77.552,21.697,77.555,21.542,77.457,21.549,77.433,21.499,77.482,21.424,77.578,21.384,77.713,21.38,77.788,21.423,77.871,21.398,78.029,21.438,78.193,21.551,78.413,21.606,78.428,21.508,78.514,21.527,78.696,21.475,78.904,21.509,78.922,21.572,79.216,21.655,79.24,21.713,79.413,21.69,79.533,21.619,79.552,21.55,79.65,21.582,79.798,21.588,79.84,21.534,80.005,21.548,80.187,21.639,80.31,21.58,80.418,21.447,80.626,21.326,80.679,21.351]},{"z":4.6,"b":[80.27,18.714,80.894,21.351],"l":[80.679,21.351,80.626,21.243,80.485,21.168,80.435,20.999,80.481,20.932,80.552,20.919,80.573,20.698,80.503,20.633,80.622,20.601,80.622,20.326,80.424,20.234,80.426,20.146,80.531,20.135,80.557,20.048,80.531,19.921,80.425,19.893,80.538,19.811,80.566,19.74,80.676,19.677,80.713,19.593,80.831,19.547,80.894,19.484,80.832,19.345,80.717,19.263,80.617,19.319,80.576,19.392,80.397,19.234,80.367,19.117,80.272,18.979,80.344,18.851,80.275,18.777,80.27,18.714]},{"z":4.7,"b":[41.713,44.976,45.712,46.24],"l":[45.706,44.976,45.712,45.014,45.586,45.158,45.353,45.243,44.706,45.506,44.421,45.527,44.207,45.61,44.148,45.653,44.097,45.774,43.878,45.945,43.742,45.979,43.568,45.981,43.393,46.016,43.237,46.112,42.991,46.18,42.905,46.24,42.835,46.156,42.671,46.088,42.533,45.998,42.317,45.981,42.34,46.107,42.177,46.099,42.15,45.949,41.713,45.996]},{"z":4.6,"b":[83.337,22.539,84.352,24.14],"l":[83.97,22.539,83.996,22.593,84.191,22.701,84.228,22.783,84.352,22.886,84.292,22.997,84.115,22.999,84.089,23.085,84.029,23.146,84.039,23.309,84.015,23.383,83.947,23.37,83.952,23.475,84.015,23.554,83.991,23.632,83.906,23.565,83.781,23.586,83.721,23.676,83.735,23.738,83.697,23.819,83.557,23.936,83.522,24.066,83.442,24.122,83.337,24.14]},{"z":4.7,"b":[84.26,56.833,89.391,60.856],"l":[88.624,56.833,88.721,57.055,88.53,57.096,88.648,57.212,88.739,57.233,88.861,57.432,89.077,57.515,89.167,57.616,89.38,57.637,89.354,57.796,89.391,57.878,89.328,57.95,88.851,57.961,88.687,58.037,88.156,58.107,88.014,58.268,87.918,58.5,87.932,58.526,88.383,58.909,88.797,59.01,88.826,59.035,88.62,59.199,88.586,59.299,87.936,59.266,87.878,59.289,87.554,59.641,87.492,59.674,87.214,59.685,87.084,59.883,86.625,59.95,85.968,59.956,85.496,59.892,84.692,59.905,84.531,59.975,84.611,60.059,84.632,60.206,84.781,60.356,84.708,60.459,84.355,60.791,84.26,60.856]},{"z":4.7,"b":[84.26,60.856,85.956,62.178],"l":[84.26,60.856,84.648,61.001,85.671,61.289,85.644,61.388,85.771,61.443,85.95,61.465,85.956,61.55,85.854,61.595,85.722,61.575,85.48,61.628,85.386,61.698,85.273,61.683,84.919,61.797,84.681,61.815,84.52,61.945,84.551,62.001,84.436,62.178]},{"z":4.7,"b":[88.609,52.183,91.846,55.446],"l":[90.477,52.183,90.7,52.458,91.038,52.66,91.242,52.747,91.396,52.868,91.408,52.947,91.351,53.077,91.66,53.158,91.841,53.288,91.846,53.352,91.744,53.447,91.47,53.514,91.472,53.567,91.333,53.758,91.327,53.817,91.465,53.966,91.464,54.057,91.21,54.338,91.159,54.488,90.855,54.627,90.915,54.791,90.84,54.839,90.715,54.831,90.685,54.905,90.458,54.871,90.436,55.016,90.262,55.05,90.129,55.105,89.904,55.086,89.658,55.003,89.55,55.046,89.201,54.99,89.147,55.076,88.966,55.185,88.95,55.289,88.76,55.344,88.609,55.446]},{"z":4.7,"b":[88.517,55.446,89.443,56.833],"l":[88.609,55.446,88.682,55.516,89.062,55.693,89.17,55.704,89.363,55.773,89.443,55.876,89.33,55.902,89.199,56.135,89.056,56.21,89.041,56.305,88.913,56.31,88.861,56.366,88.734,56.376,88.592,56.444,88.625,56.517,88.537,56.545,88.517,56.627,88.661,56.636,88.721,56.732,88.624,56.833]},{"z":4.7,"b":[37.773,60.65,47.043,61.522],"l":[47.043,60.854,46.753,60.907,46.737,60.986,46.527,61.051,46.285,60.968,45.878,61.016,45.767,61.055,45.792,61.154,45.723,61.179,45.332,61.189,45.229,60.904,44.455,60.947,44.206,60.972,44.158,61.082,43.776,61.021,43.715,60.885,43.536,60.853,43.511,60.767,43.15,60.806,43.049,60.74,42.823,60.855,42.65,60.863,42.395,60.769,42.024,60.836,41.801,60.899,41.519,60.899,41.394,60.858,41.412,60.791,41.34,60.676,41.059,60.65,40.819,60.711,40.757,60.774,40.534,60.725,39.923,60.652,39.764,60.737,39.648,60.746,39.338,60.684,39.068,60.714,38.948,60.803,38.789,60.713,38.633,60.723,38.574,60.799,38.43,60.805,38.418,60.874,38.213,60.924,38.166,61.133,37.963,61.16,37.857,61.285,37.773,61.522]},{"z":4.7,"b":[95.653,53.639,108.32,64.224],"l":[108.027,64.224,108.224,64.203,108.32,64.039,108.263,63.996,107.68,63.989,107.55,63.857,107.372,63.866,107.202,63.957,107.053,63.892,106.764,63.996,106.68,63.931,106.793,63.842,106.629,63.697,106.691,63.636,106.517,63.483,106.725,63.375,106.661,63.297,106.438,63.32,106.368,63.272,106.403,63.113,106.126,63.048,106.225,62.894,106.435,62.847,106.525,62.741,106.495,62.692,106.726,62.588,106.714,62.488,106.479,62.329,106.397,62.314,106.443,62.185,106.375,62.103,106.404,62.031,106.218,62.003,105.902,61.835,105.941,61.685,105.823,61.631,105.718,61.65,105.55,61.566,105.367,61.53,105.253,61.42,104.957,61.398,104.904,61.368,105.005,61.198,104.837,61.176,104.635,61.036,104.571,60.955,104.629,60.89,104.46,60.839,104.594,60.735,104.523,60.645,104.676,60.4,104.889,60.302,105.221,60.276,105.321,60.295,105.476,60.254,105.424,60.123,105.485,60.022,105.377,59.822,105.186,59.858,105.035,59.836,105.005,59.766,104.799,59.691,104.883,59.525,104.986,59.461,105.225,59.429,105.196,59.303,105.288,59.264,105.353,59.108,104.948,58.959,104.759,58.82,104.872,58.719,104.787,58.666,104.458,58.602,104.374,58.713,104.22,58.758,103.949,58.774,103.89,58.903,103.693,58.923,103.375,59.034,103.246,59.18,103.239,59.284,103.083,59.264,102.999,59.315,102.759,59.199,102.588,59.203,102.455,59.153,102.407,59.067,102.439,58.944,102.537,58.897,102.565,58.797,102.52,58.728,102.311,58.642,102.162,58.516,101.774,58.453,101.678,58.407,101.531,58.225,101.298,58.219,101.208,58.142,101.137,57.985,100.971,57.87,100.846,57.845,100.885,57.77,100.801,57.673,100.955,57.555,100.911,57.452,100.716,57.413,100.638,57.477,100.419,57.542,100.275,57.805,100.129,57.867,100.079,58.045,100.017,58.081,99.794,58.063,99.703,58.086,99.498,57.929,99.411,57.796,99.08,57.77,98.962,57.723,98.768,57.791,98.546,57.791,97.987,57.822,97.934,57.811,97.355,57.061,97.489,56.964,97.469,56.827,97.547,56.793,97.765,56.788,97.762,56.559,97.868,56.555,97.882,56.427,97.841,56.393,97.591,56.387,97.565,56.19,97.44,56.147,97.352,56.051,97.125,56.102,96.986,56.018,96.96,55.861,96.76,55.746,96.753,55.651,96.82,55.558,96.918,55.33,96.72,55.265,96.685,55.085,96.586,55.019,96.712,54.927,96.673,54.806,96.565,54.697,96.552,54.58,96.495,54.511,96.351,54.562,96.221,54.543,96.039,54.558,95.981,54.452,95.897,54.389,95.717,54.388,95.653,54.282,95.678,54.234,95.919,54.156,96.079,54.063,96.126,53.972,96.406,53.783,96.505,53.684,96.684,53.639]},{"z":4.7,"b":[45.442,60.854,50.467,66.119],"l":[49.006,66.119,48.989,65.269,49.021,65.238,49.567,65.292,49.663,65.255,50.063,64.89,50.16,64.847,50.467,64.792,50.348,64.538,50.273,64.513,49.627,64.592,49.45,64.463,48.95,64.491,48.84,64.357,48.518,64.356,48.408,64.224,48.196,64.239,48.116,64.348,46.611,64.327,46.323,64.265,45.825,64.284,45.442,64.188,45.593,64.064,45.884,63.964,46.11,63.946,46.436,63.888,46.819,63.747,46.97,63.64,47.026,63.531,46.937,63.352,47.091,63.246,47.366,63.167,47.555,63.148,47.516,63.037,47.4,62.955,47.083,62.84,47.238,62.636,47.235,62.351,47.386,62.32,47.413,62.193,47.621,62.185,47.65,62.297,47.751,62.322,48.291,62.322,48.237,62.473,48.366,62.729,48.629,62.706,48.714,62.816,48.875,62.829,49.609,62.77,49.369,62.149,49.213,62.091,49.052,61.691,49.097,61.658,49.417,61.618,49.461,61.586,49.302,61.176,49.098,61.175,48.889,61.141,48.509,61.048,48.446,60.984,48.419,60.854]},{"z":4.7,"b":[47.043,60.854,48.419,61.059],"l":[48.419,60.854,47.984,60.896,47.965,60.999,47.568,61.059,47.245,61.056,47.104,60.956,47.043,60.854]},{"z":4.7,"b":[108.027,58.223,119.132,64.282],"l":[119.108,58.223,119.07,58.341,119.132,58.472,118.98,58.543,118.821,58.574,118.893,58.816,118.795,58.935,118.84,59.016,118.707,59.064,118.689,59.191,118.84,59.3,118.734,59.42,118.407,59.512,118.336,59.596,118.058,59.583,117.906,59.444,117.779,59.528,117.598,59.472,117.352,59.497,117.089,59.588,117.19,59.646,117.228,59.798,117.065,59.908,117.296,60.019,117.077,60.029,116.954,60.169,116.722,60.266,116.582,60.36,116.075,60.407,115.999,60.457,115.842,60.468,115.686,60.526,115.395,60.487,115.112,60.395,115.007,60.269,114.836,60.19,114.697,60.219,114.54,60.102,114.545,59.985,114.309,59.879,114.161,59.748,114.03,59.681,113.849,59.682,113.779,59.609,113.592,59.565,113.605,59.499,113.435,59.402,113.465,59.266,113.264,59.157,113.152,59.164,112.728,59.083,112.554,59.033,112.612,58.954,112.451,58.924,112.465,59.168,112.631,59.31,112.435,59.326,112.218,59.5,111.933,59.272,111.776,59.276,111.625,59.207,111.47,59.267,111.316,59.257,111.164,59.189,110.713,59.259,110.58,59.191,110.589,59.131,110.471,59.034,110.287,58.985,110.119,58.98,109.636,59.066,109.499,59.294,109.268,59.316,109.269,59.449,109.517,59.63,109.504,59.764,109.65,59.871,109.764,60.027,109.694,60.129,109.741,60.233,109.917,60.451,110.116,60.587,110.057,60.674,110.268,60.702,110.24,60.831,110.458,60.988,110.517,61.091,110.481,61.159,110.169,61.164,110.094,61.263,109.792,61.325,109.846,61.424,109.851,61.557,109.62,61.718,109.598,61.87,109.819,62.008,110,62.164,109.996,62.278,109.918,62.406,109.557,62.431,109.306,62.484,109.512,62.777,109.614,62.874,109.465,62.95,109.453,63.149,109.215,63.442,109.053,63.549,108.857,63.537,108.547,63.599,108.16,63.563,108.112,63.617,108.265,63.673,108.279,63.796,108.687,63.795,108.766,63.86,108.735,63.977,108.495,64.116,108.533,64.228,108.468,64.282,108.027,64.224]},{"z":4.7,"b":[105.54,64.182,112.579,73.713],"l":[108.027,64.224,107.856,64.182,107.57,64.299,107.368,64.251,106.877,64.414,106.395,64.443,106.204,64.403,106.132,64.481,105.803,64.53,105.708,64.653,105.873,64.68,105.832,64.76,105.946,64.884,106.123,64.905,105.973,65.012,106.282,65.137,106.454,65.233,106.451,65.313,106.635,65.393,106.836,65.378,106.94,65.511,106.713,65.549,106.631,65.635,106.44,65.662,106.478,65.845,106.471,66.007,106.33,66.154,106.171,66.159,106.103,66.378,106.322,66.479,106.249,66.581,106.247,66.702,106.047,66.789,106.044,66.901,105.687,66.939,105.54,67.023,105.894,67.044,106.055,67.167,106.317,67.209,106.492,67.284,106.685,67.254,106.802,67.348,106.833,67.673,106.875,67.84,106.896,68.143,106.885,68.865,106.153,69.391,106.427,69.564,106.604,69.517,106.839,69.518,107.124,69.588,108.007,69.705,108.149,69.825,108.291,69.857,108.871,69.84,108.941,69.789,109.237,69.776,109.36,69.838,109.509,70.033,109.358,70.069,109.277,70.207,109.535,70.264,109.602,70.378,109.995,70.417,110.117,70.478,110.103,70.632,110.489,70.726,110.539,70.801,110.903,70.808,111.335,70.855,111.522,70.939,111.953,70.982,112.027,71.042,112.277,71.013,112.579,71.114,112.513,71.268,111.997,71.414,111.996,72.133,111.817,72.132,111.643,72.274,111.085,72.37,111.281,72.478,111.21,72.545,110.881,72.58,110.702,72.65,110.611,72.781,110.627,72.895,110.837,72.945,110.881,73.023,110.755,73.175,110.571,73.253,110.523,73.404,110.818,73.559,111.073,73.583,111.111,73.641,110.827,73.713]},{"z":4.7,"b":[110.801,73.759,111.303,73.883],"l":[110.801,73.759,111.303,73.883]},{"z":4.7,"b":[49.006,66.119,65.48,68.43],"l":[65.48,68.43,64.745,68.404,64.523,68.344,64.53,68.2,64.325,68.134,63.829,68.017,63.944,67.884,63.69,67.762,63.445,67.771,63.161,67.691,62.871,67.58,62.821,67.388,61.851,67.126,61.695,67.03,61.432,67,60.082,66.99,59.918,67.002,58.79,67,53.772,66.999,52.246,67.089,51.836,66.995,51.811,66.954,51.495,66.907,51.51,66.783,49.006,66.119]},{"z":4.7,"b":[44.214,65.808,49.006,66.408],"l":[49.006,66.119,48.156,66.13,47.753,65.941,47.764,65.861,47.661,65.808,47.407,65.836,47.11,65.821,46.347,66.028,45.832,66.088,45.741,66.068,44.578,66.307,44.476,66.37,44.214,66.408]},{"z":4.7,"b":[62.018,65.717,66.206,68.43],"l":[62.018,65.717,62.504,65.831,62.839,65.869,62.807,65.937,62.87,66.079,63.066,66.232,63.241,66.313,63.306,66.4,63.809,66.547,63.969,66.646,64.316,66.668,64.567,66.794,64.999,66.865,65.116,66.908,65.09,67.054,65.19,67.145,65.422,67.215,65.708,67.335,65.937,67.393,66.11,67.489,66.026,67.576,65.836,67.555,65.831,67.643,66.206,67.693,66.022,67.794,66.083,67.934,65.437,67.92,65.29,68.012,65.33,68.085,65.273,68.216,65.48,68.43]},{"z":4.7,"b":[64.545,68.43,65.654,69.271],"l":[65.48,68.43,65.654,68.556,65.517,68.588,65.326,68.73,65.315,68.803,65.064,68.814,64.904,68.871,64.554,68.871,64.545,69.032,64.99,69.205,65.038,69.271]},{"z":4.6,"b":[107.238,37.068,107.658,37.853],"l":[107.658,37.853,107.607,37.766,107.509,37.764,107.436,37.663,107.317,37.59,107.322,37.528,107.238,37.301,107.325,37.164,107.285,37.068]},{"z":4.6,"b":[110.237,34.616,111.225,39.385],"l":[111.11,39.385,111.225,39.311,111.118,39.067,110.995,39.004,110.98,38.817,110.872,38.621,110.899,38.545,110.86,38.468,110.774,38.426,110.671,38.317,110.58,38.287,110.511,38.202,110.515,37.955,110.589,37.919,110.653,37.813,110.753,37.746,110.789,37.561,110.746,37.48,110.63,37.397,110.687,37.357,110.665,37.282,110.46,37.051,110.389,37.022,110.398,36.781,110.483,36.593,110.496,36.484,110.459,36.349,110.473,36.264,110.447,36.142,110.511,35.887,110.56,35.849,110.576,35.713,110.62,35.597,110.425,35.294,110.335,35.198,110.26,34.945,110.237,34.801,110.252,34.663,110.336,34.616]},{"z":4.6,"b":[109.207,29.066,113.902,30.096],"l":[109.207,29.104,109.269,29.242,109.338,29.29,109.432,29.534,109.472,29.568,109.7,29.656,109.783,29.735,110.104,29.785,110.225,29.737,110.362,29.65,110.447,29.727,110.526,29.725,110.591,29.771,110.501,29.863,110.463,30.014,110.543,30.055,110.716,30.03,110.742,30.096,110.944,30.064,111.246,30.032,111.415,29.926,111.585,29.904,111.769,29.92,111.924,29.856,112.126,29.707,112.17,29.649,112.363,29.572,112.444,29.641,112.648,29.627,112.786,29.737,112.886,29.792,112.946,29.709,112.908,29.601,112.928,29.495,113.008,29.518,113.146,29.472,113.186,29.532,113.416,29.765,113.52,29.842,113.536,29.686,113.635,29.685,113.673,29.545,113.729,29.437,113.588,29.294,113.641,29.222,113.655,29.124,113.711,29.073,113.773,29.125,113.902,29.066]},{"z":4.6,"b":[113.562,25.448,114.227,29.066],"l":[113.902,29.066,113.905,28.956,114.012,28.913,114.143,28.789,114.113,28.616,114.057,28.564,114.177,28.507,114.227,28.409,114.184,28.293,114.092,28.258,114.028,28.184,114.014,28.106,113.916,28.021,113.737,27.982,113.706,27.938,113.701,27.819,113.611,27.699,113.562,27.591,113.6,27.368,113.667,27.33,113.783,27.371,113.851,27.346,113.824,27.229,113.754,27.142,113.892,26.945,113.832,26.827,113.865,26.656,114.085,26.577,114.068,26.454,114.015,26.385,113.999,26.283,113.939,26.209,114.063,26.178,114.162,26.221,114.214,26.17,114.041,26.069,114.018,25.935,113.907,25.75,113.918,25.66,113.982,25.582,113.928,25.448]},{"z":4.6,"b":[106.382,28.2,109.258,29.246],"l":[109.258,28.506,109.155,28.436,109.095,28.205,108.943,28.2,108.728,28.242,108.784,28.426,108.73,28.475,108.583,28.374,108.582,28.601,108.33,28.689,108.367,28.777,108.341,28.92,108.286,29.077,108.055,29.065,107.875,28.984,107.79,29.04,107.783,29.137,107.557,29.187,107.496,29.246,107.43,29.183,107.424,29.077,107.5,28.993,107.437,28.877,107.297,28.808,107.161,28.881,107.031,28.888,106.973,28.805,106.889,28.801,106.835,28.731,106.829,28.609,106.762,28.591,106.75,28.502,106.562,28.511,106.626,28.637,106.562,28.767,106.489,28.541,106.382,28.572]},{"z":4.6,"b":[108.791,26.039,109.507,28.506],"l":[109.258,28.506,109.264,28.322,109.354,28.265,109.299,28.095,109.363,28.03,109.31,27.97,109.328,27.808,109.413,27.739,109.447,27.65,109.418,27.56,109.318,27.501,109.277,27.439,109.206,27.445,109.015,27.262,108.9,27.189,108.897,27.147,108.791,27.083,108.874,27.018,108.95,27.027,109.008,27.094,109.095,27.134,109.156,27.08,109.231,27.152,109.438,27.131,109.499,27.062,109.507,26.976,109.444,26.901,109.505,26.825,109.397,26.755,109.294,26.727,109.377,26.646,109.403,26.537,109.315,26.426,109.297,26.313,109.399,26.27,109.428,26.099,109.457,26.039]},{"z":4.6,"b":[108.381,29.104,110.175,31.738],"l":[109.594,31.738,109.659,31.718,109.724,31.636,109.733,31.561,109.929,31.519,110.108,31.398,110.147,31.331,110.175,31.189,110.108,31.122,110.138,31,110.128,30.906,110.066,30.828,109.997,30.826,109.871,30.889,109.749,30.835,109.64,30.718,109.394,30.584,109.248,30.597,109.179,30.567,108.965,30.607,108.868,30.551,108.71,30.523,108.67,30.574,108.573,30.498,108.433,30.487,108.381,30.4,108.532,30.315,108.53,30.113,108.55,30.009,108.538,29.885,108.4,29.831,108.447,29.736,108.513,29.722,108.613,29.858,108.658,29.817,108.696,29.698,108.872,29.639,108.872,29.502,108.921,29.349,109.014,29.307,109.077,29.214,109.207,29.104]},{"z":4.6,"b":[109.207,28.506,109.285,29.104],"l":[109.207,29.104,109.275,29.049,109.246,28.931,109.247,28.786,109.281,28.729,109.213,28.629,109.285,28.573,109.258,28.506]},{"z":4.6,"b":[105.499,32.158,108.518,32.907],"l":[105.499,32.907,105.568,32.73,105.633,32.707,105.724,32.76,105.813,32.762,105.875,32.845,106.082,32.874,106.057,32.714,106.115,32.649,106.229,32.592,106.364,32.639,106.604,32.676,106.713,32.738,107.065,32.708,107.096,32.669,107.07,32.523,107.202,32.453,107.284,32.453,107.371,32.52,107.448,32.522,107.46,32.409,107.659,32.389,107.773,32.311,107.844,32.228,107.96,32.158,108.052,32.227,108.237,32.263,108.479,32.25,108.518,32.198]},{"z":4.6,"b":[108.518,31.705,109.594,32.198],"l":[108.518,32.198,108.789,32.045,109.056,31.941,109.189,31.84,109.253,31.744,109.336,31.705,109.594,31.738]},{"z":4.6,"b":[105.297,27.678,106.382,28.754],"l":[106.382,28.572,106.339,28.48,106.269,28.557,106.189,28.585,105.984,28.754,105.892,28.609,105.683,28.583,105.621,28.482,105.656,28.441,105.667,28.318,105.847,28.269,105.906,28.151,106.009,28.14,106.109,28.175,106.199,28.124,106.215,28.056,106.299,28,106.345,27.835,106.248,27.775,106.104,27.788,106.039,27.757,105.876,27.745,105.647,27.678,105.524,27.775,105.361,27.76,105.297,27.722]},{"z":4.6,"b":[97.361,32.403,101.778,34.005],"l":[97.716,32.544,97.664,32.56,97.431,32.7,97.384,32.779,97.361,32.897,97.469,32.983,97.504,33.046,97.476,33.12,97.501,33.194,97.616,33.328,97.724,33.406,97.539,33.454,97.477,33.562,97.391,33.611,97.384,33.87,97.61,33.931,97.696,34.005,97.745,33.865,97.819,33.864,98.035,33.96,98.212,33.939,98.302,33.846,98.458,33.84,98.646,33.676,98.633,33.608,98.736,33.49,98.773,33.303,98.852,33.175,99.101,33.072,99.225,32.999,99.289,32.887,99.509,32.835,99.699,32.745,99.729,32.758,99.769,32.922,99.841,32.957,100.047,32.935,100.137,32.848,100.159,32.782,100.119,32.671,100.223,32.637,100.255,32.726,100.399,32.758,100.498,32.669,100.561,32.563,100.473,32.485,100.531,32.403,100.603,32.451,100.643,32.594,100.697,32.686,100.902,32.63,101.049,32.676,101.171,32.688,101.237,32.808,101.232,32.858,101.131,32.938,101.164,33.128,101.22,33.171,101.445,33.236,101.644,33.128,101.727,33.268,101.665,33.32,101.691,33.42,101.778,33.53]},{"z":4.6,"b":[80.679,21.351,82.801,23.999],"l":[82.801,23.999,82.768,23.929,82.658,23.885,82.651,23.845,82.532,23.775,82.077,23.801,81.937,23.841,81.778,23.802,81.682,23.885,81.622,23.834,81.686,23.69,81.591,23.561,81.644,23.482,81.745,23.543,81.907,23.505,81.991,23.401,82.098,23.372,82.19,23.289,82.129,23.096,82.071,23.1,81.946,23.043,81.94,22.946,81.789,22.853,81.766,22.664,81.67,22.609,81.642,22.551,81.54,22.52,81.418,22.437,81.373,22.493,81.235,22.444,81.178,22.478,81.123,22.401,81.119,22.295,81.024,22.194,80.987,22.108,80.919,22.097,80.835,21.951,80.838,21.846,80.805,21.774,80.731,21.714,80.721,21.473,80.679,21.351]},{"z":4.6,"b":[105.291,28.572,108.518,32.198],"l":[108.518,32.198,108.389,32.184,108.419,32.1,108.345,32.074,108.268,31.973,108.287,31.922,108.501,31.791,108.516,31.708,108.407,31.566,108.323,31.499,108.223,31.468,108.194,31.331,108.033,31.222,108.007,31.039,107.932,30.949,107.93,30.873,107.85,30.81,107.769,30.827,107.736,30.883,107.621,30.837,107.508,30.822,107.457,30.761,107.478,30.61,107.372,30.5,107.229,30.238,107.033,30.058,106.77,30.037,106.717,30.059,106.618,30.269,106.532,30.338,106.449,30.324,106.385,30.264,106.239,30.22,106.125,30.342,105.999,30.385,105.792,30.423,105.709,30.274,105.557,30.148,105.739,30.033,105.71,29.868,105.558,29.804,105.551,29.748,105.412,29.703,105.291,29.585,105.315,29.47,105.405,29.43,105.422,29.334,105.506,29.291,105.645,29.27,105.701,29.224,105.745,28.995,105.795,28.937,105.878,28.92,106.008,28.973,106.26,28.869,106.257,28.794,106.298,28.672,106.382,28.572]},{"z":4.6,"b":[76.837,28.433,77.327,28.882],"l":[77.327,28.52,77.27,28.51,77.194,28.433,77.101,28.476,77.071,28.529,76.873,28.519,76.837,28.587,76.947,28.696,76.937,28.815,77.068,28.882,77.201,28.877]},{"z":4.6,"b":[76.815,30.681,76.827,30.789],"l":[76.815,30.789,76.827,30.681]},{"z":4.7,"b":[59.485,58.614,65.935,61.993],"l":[59.485,61.993,59.652,61.936,59.779,61.952,59.984,61.903,59.988,61.747,60.057,61.721,60.431,61.747,60.759,61.704,61.165,61.683,61.9,61.434,61.933,61.404,62.42,61.305,62.62,61.211,62.86,61.001,62.76,60.859,62.881,60.64,63.091,60.46,63.22,60.301,63.197,60.242,63.438,60.159,63.589,60.155,63.694,60.001,63.796,59.792,63.825,59.477,63.98,59.406,64.121,59.391,64.324,59.411,65.002,59.338,65.185,59.223,65.382,58.919,65.669,58.663,65.935,58.614]},{"z":4.7,"b":[59.232,61.993,62.018,65.717],"l":[59.485,61.993,59.404,62.108,59.507,62.307,59.565,62.335,59.644,62.51,59.527,62.541,59.394,62.731,59.483,62.892,59.278,62.97,59.232,63.089,59.33,63.274,59.292,63.342,59.494,63.611,59.521,63.791,59.584,63.938,59.761,63.996,59.838,64.08,59.588,64.233,59.633,64.344,59.589,64.467,59.484,64.497,59.704,64.668,59.648,64.772,59.75,64.859,59.889,64.901,60.147,65.065,60.305,65.071,60.677,64.912,60.831,65.051,61.003,65.06,61.107,65.161,61.241,65.192,61.263,65.307,61.511,65.5,61.882,65.702,62.018,65.717]},{"z":4.7,"b":[57.219,56.329,59.468,61.682],"l":[57.408,56.329,57.317,56.373,57.373,56.552,57.358,56.681,57.219,56.853,57.323,56.92,57.56,56.888,57.825,56.974,57.869,57.058,58.03,57.099,58.039,57.237,57.938,57.332,57.995,57.477,58.268,57.67,58.436,57.676,58.458,57.588,58.58,57.594,58.83,57.726,58.864,57.827,58.756,57.848,58.605,57.996,58.678,58.107,58.868,58.205,58.971,58.2,59.203,58.305,59.281,58.413,59.456,58.493,59.389,58.59,59.388,58.703,59.186,58.711,59.083,58.769,59.066,58.884,59.181,58.953,59.091,59.022,59.175,59.147,59.003,59.231,58.67,59.293,58.508,59.44,58.311,59.468,58.403,59.552,58.451,59.705,58.582,59.716,58.675,59.859,58.805,59.863,58.849,59.926,58.989,59.928,58.962,60.005,59.188,60.278,59.169,60.374,59.239,60.48,59.385,60.574,59.468,60.808,59.458,60.953,59.373,60.97,59.358,61.143,59.261,61.222,59.317,61.377,59.406,61.418,59.428,61.507,59.349,61.682]},{"z":4.7,"b":[59.345,61.682,59.485,61.993],"l":[59.349,61.682,59.389,61.744,59.345,61.857,59.485,61.993]},{"z":4.7,"b":[53.632,55.907,54.352,56.359],"l":[53.632,55.907,53.777,55.97,53.838,56.039,54.047,56.058,54.117,56.158,54.346,56.253,54.352,56.359]},{"z":4.7,"b":[54.352,56.108,57.408,56.515],"l":[54.352,56.359,54.43,56.343,54.532,56.515,54.918,56.392,54.968,56.32,55.448,56.341,55.548,56.4,55.876,56.437,56.073,56.289,56.275,56.305,56.402,56.387,56.655,56.22,56.73,56.224,56.864,56.108,57.213,56.182,57.274,56.305,57.408,56.329]},{"z":4.7,"b":[57.408,56.063,59.026,56.329],"l":[57.408,56.329,57.411,56.235,57.537,56.1,57.802,56.151,57.907,56.115,58.079,56.141,58.308,56.105,58.543,56.163,58.747,56.063,58.944,56.066,59.026,56.169]},{"z":4.7,"b":[53.141,54.046,54.177,55.907],"l":[53.47,54.046,53.348,54.382,53.426,54.499,53.414,54.552,53.515,54.616,53.607,54.728,53.579,54.822,53.422,54.982,53.269,55.012,53.141,55.099,53.147,55.147,53.327,55.166,53.418,55.222,53.591,55.212,53.723,55.338,53.891,55.381,53.997,55.544,54.175,55.624,54.177,55.707,53.976,55.853,53.759,55.866,53.632,55.907]},{"z":4.6,"b":[110.975,31.395,115.374,33.266],"l":[115.374,31.418,115.286,31.395,115.219,31.447,115.213,31.546,115.129,31.595,115.077,31.516,114.999,31.478,114.767,31.49,114.638,31.568,114.548,31.573,114.556,31.738,114.495,31.759,114.362,31.734,114.267,31.809,114.144,31.848,114.085,31.784,113.972,31.747,113.96,31.832,113.862,31.863,113.794,31.973,113.752,32.138,113.782,32.194,113.783,32.359,113.729,32.419,113.616,32.386,113.528,32.297,113.436,32.289,113.183,32.413,113.066,32.417,112.751,32.35,112.534,32.377,112.286,32.351,112.145,32.395,112.053,32.455,111.779,32.52,111.711,32.6,111.629,32.626,111.532,32.611,111.507,32.682,111.363,32.825,111.21,32.93,111.216,32.976,111.109,33.148,111.003,33.208,110.975,33.266]},{"z":4.6,"b":[115.374,29.789,116.147,31.418],"l":[115.374,31.418,115.55,31.183,115.669,31.217,115.768,31.144,115.877,31.129,115.946,31.034,116.036,30.989,115.859,30.861,115.862,30.785,115.775,30.755,115.768,30.688,115.833,30.594,115.947,30.488,115.877,30.39,115.912,30.316,116.04,30.221,116.081,30.047,116.078,29.969,116.126,29.905,116.147,29.789]},{"z":4.6,"b":[116.147,29.408,118.175,30.051],"l":[116.147,29.789,116.236,29.781,116.39,29.876,116.493,29.885,116.65,30.051,116.791,30.022,116.887,29.92,116.798,29.756,116.675,29.708,116.648,29.627,116.749,29.545,116.888,29.561,116.955,29.654,117.079,29.71,117.107,29.786,117.067,29.841,117.125,29.911,117.226,29.907,117.274,29.829,117.406,29.831,117.402,29.773,117.527,29.622,117.649,29.606,117.708,29.556,117.899,29.549,117.986,29.572,118.113,29.519,118.175,29.408]},{"z":4.6,"b":[114.88,31.418,116.641,34.567],"l":[116.151,34.567,116.157,34.447,116.401,34.275,116.552,34.287,116.575,34.192,116.543,34.121,116.641,33.953,116.445,33.845,116.421,33.791,116.182,33.718,116.08,33.778,115.971,33.905,115.961,34,115.841,34.007,115.733,34.066,115.654,34.048,115.586,33.98,115.561,33.898,115.63,33.839,115.574,33.75,115.635,33.592,115.443,33.548,115.346,33.453,115.316,33.374,115.345,33.251,115.251,33.121,115.143,33.084,114.969,33.124,114.887,33.083,114.88,32.993,114.925,32.958,115.185,32.859,115.199,32.591,115.367,32.56,115.457,32.507,115.565,32.402,115.647,32.402,115.694,32.492,115.79,32.471,115.888,32.394,115.915,32.25,115.928,32.025,115.901,31.794,115.663,31.784,115.532,31.736,115.39,31.512,115.374,31.418]},{"z":4.6,"b":[110.336,34.584,113.721,36.356],"l":[113.721,36.356,113.673,36.221,113.687,36.056,113.628,35.953,113.585,35.687,113.61,35.646,113.491,35.525,113.328,35.463,113.13,35.352,113.02,35.329,112.903,35.241,112.808,35.242,112.682,35.311,112.526,35.22,112.335,35.219,112.058,35.234,112.061,35.108,111.829,35.083,111.622,34.947,111.558,34.863,111.394,34.823,111.331,34.841,111.026,34.747,110.884,34.666,110.766,34.661,110.709,34.621,110.531,34.584,110.336,34.616]},{"z":4.6,"b":[113.902,29.066,116.147,29.864],"l":[113.902,29.066,114.063,29.203,114.247,29.249,114.243,29.309,114.308,29.355,114.47,29.318,114.709,29.39,114.818,29.394,114.895,29.52,114.973,29.56,115.272,29.615,115.417,29.712,115.486,29.864,115.705,29.861,115.856,29.739,115.999,29.744,116.147,29.789]},{"z":4.6,"b":[110.328,33.266,111.016,34.616],"l":[110.336,34.616,110.328,34.48,110.374,34.404,110.479,34.326,110.434,34.261,110.607,34.17,110.594,34.065,110.642,33.98,110.594,33.88,110.749,33.803,110.792,33.705,110.968,33.599,111.011,33.5,111.016,33.374,110.975,33.266]},{"z":4.6,"b":[109.487,31.738,110.975,33.266],"l":[110.975,33.266,110.9,33.205,110.823,33.199,110.711,33.108,110.604,33.154,110.547,33.245,110.432,33.173,110.234,33.156,110.094,33.213,109.96,33.214,109.875,33.243,109.66,33.264,109.516,33.237,109.491,33.145,109.757,33.08,109.781,33.045,109.74,32.93,109.836,32.896,109.899,32.914,110.096,32.841,110.155,32.73,110.168,32.622,110.074,32.617,110.001,32.552,109.88,32.592,109.635,32.591,109.579,32.541,109.561,32.405,109.487,32.336,109.548,32.234,109.619,32.189,109.666,32.043,109.61,31.927,109.594,31.738]},{"z":4.6,"b":[108.547,-7.665,108.895,-6.814],"l":[108.799,-7.665,108.734,-7.605,108.742,-7.537,108.705,-7.391,108.547,-7.279,108.595,-7.142,108.695,-7.14,108.789,-7.061,108.801,-6.935,108.895,-6.814]},{"z":4.6,"b":[106.401,-7.007,106.763,-6.301],"l":[106.401,-7.007,106.451,-6.836,106.506,-6.783,106.427,-6.697,106.405,-6.524,106.434,-6.439,106.421,-6.37,106.56,-6.355,106.631,-6.384,106.739,-6.386,106.763,-6.301]},{"z":4.6,"b":[121.068,-3.02,121.598,-2.873],"l":[121.598,-2.973,121.475,-3.02,121.278,-2.879,121.164,-2.873,121.068,-2.918]},{"z":4.6,"b":[110.043,-8.201,110.844,-7.569],"l":[110.844,-8.201,110.799,-8.122,110.787,-7.874,110.753,-7.833,110.579,-7.821,110.507,-7.783,110.441,-7.569,110.301,-7.669,110.137,-7.677,110.117,-7.762,110.043,-7.889]},{"z":4.6,"b":[119.412,-3.513,119.873,-1.955],"l":[119.467,-3.513,119.453,-3.281,119.412,-3.215,119.639,-3.121,119.543,-2.907,119.56,-2.746,119.661,-2.758,119.786,-2.695,119.739,-2.565,119.707,-2.354,119.624,-2.294,119.646,-2.176,119.753,-2.082,119.825,-2.046,119.873,-1.955]},{"z":4.6,"b":[119.873,-2.973,121.787,-1.86],"l":[119.873,-1.955,120.048,-1.861,120.157,-1.86,120.292,-1.9,120.448,-1.866,120.553,-2.025,120.625,-2.104,120.804,-2.239,121.35,-2.348,121.449,-2.388,121.638,-2.558,121.742,-2.61,121.787,-2.67,121.772,-2.801,121.598,-2.973]},{"z":4.6,"b":[77.201,28.52,77.338,28.877],"l":[77.327,28.52,77.338,28.626,77.307,28.721,77.227,28.784,77.201,28.877]},{"z":4.6,"b":[110.944,-8.226,111.673,-6.734],"l":[110.944,-8.226,110.965,-8.117,111.099,-8.009,111.228,-7.938,111.308,-7.838,111.265,-7.732,111.191,-7.707,111.217,-7.613,111.185,-7.529,111.197,-7.309,111.22,-7.251,111.444,-7.311,111.46,-7.274,111.608,-7.154,111.598,-6.961,111.629,-6.8,111.673,-6.734]},{"z":4.6,"b":[78.169,23.999,82.808,26.851],"l":[82.801,23.999,82.679,24.167,82.733,24.214,82.767,24.386,82.734,24.42,82.734,24.552,82.808,24.566,82.772,24.668,82.692,24.72,82.624,24.692,82.427,24.719,82.424,24.648,82.303,24.638,82.319,24.702,82.033,24.859,81.881,24.922,81.817,25.036,81.585,25.08,81.537,25.2,81.361,25.157,81.243,25.17,81.239,25.098,81.185,24.973,81.098,24.909,80.999,24.966,80.893,24.984,80.779,24.971,80.859,25.188,80.806,25.212,80.718,25.082,80.647,25.07,80.58,25.148,80.515,25.041,80.458,25.015,80.288,25.016,80.265,25.064,80.422,25.181,80.395,25.247,80.326,25.282,80.284,25.414,80.191,25.413,80.125,25.351,80.015,25.331,79.852,25.248,79.863,25.153,79.661,25.148,79.542,25.177,79.468,25.104,79.339,25.147,79.425,25.257,79.295,25.259,79.269,25.164,79.109,25.195,79.024,25.175,78.983,25.237,78.885,25.266,78.955,25.386,78.919,25.505,78.853,25.434,78.749,25.426,78.728,25.346,78.597,25.414,78.516,25.266,78.61,25.136,78.64,24.964,78.732,24.815,78.715,24.685,78.75,24.63,78.879,24.613,78.955,24.393,78.826,24.23,78.747,24.279,78.656,24.295,78.489,24.411,78.386,24.301,78.33,24.342,78.35,24.408,78.278,24.453,78.229,24.533,78.265,24.583,78.261,24.693,78.169,24.865,78.337,25.023,78.366,25.186,78.262,25.412,78.33,25.465,78.371,25.553,78.558,25.576,78.715,25.626,78.754,25.674,78.74,25.79,78.808,25.842,78.954,26.11,78.963,26.278,79.061,26.355,79.028,26.498,78.974,26.572,78.936,26.694,78.842,26.712,78.693,26.798,78.564,26.758,78.371,26.851,78.2,26.824]},{"z":4.6,"b":[77.281,27.809,77.529,28.52],"l":[77.281,27.809,77.424,27.891,77.48,27.898,77.526,27.974,77.468,28.047,77.529,28.253,77.467,28.315,77.497,28.393,77.327,28.52]},{"z":4.6,"b":[77.065,28.877,77.585,30.383],"l":[77.201,28.877,77.196,28.998,77.141,29.13,77.131,29.281,77.15,29.403,77.065,29.57,77.088,29.659,77.18,29.804,77.23,29.977,77.285,30.038,77.57,30.278,77.585,30.383]},{"z":4.6,"b":[83.337,24.14,83.523,24.53],"l":[83.523,24.53,83.402,24.498,83.454,24.362,83.337,24.14]},{"z":4.6,"b":[82.801,23.911,83.337,24.14],"l":[83.337,24.14,83.206,23.944,83.089,23.911,82.964,23.914,82.801,23.999]},{"z":4.6,"b":[83.523,24.307,87.784,25.324],"l":[87.784,25.214,87.726,25.252,87.599,25.267,87.523,25.324,87.407,25.181,87.292,25.174,87.266,25.072,87.186,25.062,87.117,24.971,87.119,24.851,87.078,24.745,87.05,24.597,86.925,24.614,86.89,24.532,86.755,24.588,86.702,24.548,86.586,24.561,86.494,24.48,86.439,24.34,86.276,24.435,86.29,24.552,86.155,24.556,86.098,24.69,86.033,24.712,85.912,24.698,85.831,24.771,85.746,24.77,85.647,24.649,85.662,24.569,85.578,24.558,85.526,24.502,85.391,24.509,85.281,24.487,84.961,24.348,84.889,24.373,84.798,24.497,84.588,24.394,84.519,24.308,84.472,24.307,84.312,24.451,84.305,24.531,84.096,24.504,84.044,24.624,83.918,24.544,83.788,24.537,83.696,24.506,83.523,24.53]},{"z":4.6,"b":[83.323,24.53,84.606,27.378],"l":[83.523,24.53,83.531,24.704,83.509,24.762,83.36,24.863,83.36,25.009,83.323,25.068,83.334,25.173,83.384,25.236,83.775,25.445,83.904,25.529,84.143,25.736,84.213,25.728,84.239,25.648,84.324,25.659,84.315,25.742,84.373,25.765,84.487,25.701,84.601,25.747,84.606,25.8,84.532,25.862,84.42,25.89,84.374,25.94,84.261,25.931,84.15,26.066,84.065,26.104,83.993,26.241,84.187,26.293,84.178,26.404,84.026,26.469,83.955,26.454,83.92,26.533,84.054,26.565,84.099,26.65,84.383,26.642,84.306,26.744,84.246,26.757,84.241,26.874,84.149,26.876,84.071,26.924,84.023,27.06,83.938,27.164,83.942,27.222,83.829,27.378]},{"z":4.6,"b":[76.768,30.383,77.585,30.908],"l":[77.585,30.383,77.303,30.46,77.148,30.539,77.15,30.664,77.115,30.722,77.002,30.777,76.906,30.899,76.768,30.908]},{"z":4.7,"b":[33.571,44.398,33.852,44.852],"l":[33.73,44.398,33.852,44.432,33.806,44.527,33.713,44.582,33.709,44.666,33.611,44.721,33.675,44.792,33.571,44.852]},{"z":4.6,"b":[103.592,-4.927,103.765,-4.805],"l":[103.592,-4.927,103.765,-4.805]},{"z":4.6,"b":[101.027,-2.48,101.336,-2.297],"l":[101.027,-2.48,101.171,-2.367,101.336,-2.297]},{"z":4.6,"b":[103.765,-4.947,105.828,-3.756],"l":[105.828,-4.158,105.709,-4.15,105.513,-3.868,105.31,-3.756,105.266,-3.886,105.129,-3.956,105.111,-4.06,105.009,-4.169,104.84,-4.221,104.674,-4.297,104.527,-4.32,104.397,-4.405,104.309,-4.44,104.293,-4.495,104.309,-4.628,104.265,-4.708,104.337,-4.814,104.298,-4.892,104.21,-4.893,104.081,-4.947,103.889,-4.934,103.855,-4.854,103.765,-4.805]},{"z":4.6,"b":[102.067,-4.805,103.765,-2.782],"l":[103.765,-4.805,103.723,-4.757,103.694,-4.639,103.622,-4.505,103.616,-4.418,103.391,-4.335,103.186,-4.243,103.142,-4.047,103.107,-4.02,102.981,-4.035,102.908,-3.991,102.845,-4,102.768,-3.961,102.63,-3.814,102.702,-3.791,102.897,-3.631,102.993,-3.584,103.017,-3.485,102.975,-3.396,102.87,-3.376,102.766,-3.279,102.631,-3.373,102.563,-3.344,102.462,-3.25,102.489,-3.151,102.431,-3.088,102.241,-3.059,102.224,-2.954,102.067,-2.782]},{"z":4.6,"b":[101.336,-2.782,102.067,-2.236],"l":[102.067,-2.782,101.963,-2.735,101.9,-2.74,101.737,-2.636,101.564,-2.45,101.517,-2.305,101.436,-2.236,101.336,-2.297]},{"z":4.6,"b":[101.114,-2.297,101.841,-1.038],"l":[101.336,-2.297,101.26,-2.081,101.142,-1.92,101.114,-1.705,101.226,-1.728,101.476,-1.702,101.519,-1.64,101.675,-1.5,101.678,-1.289,101.794,-1.239,101.841,-1.166,101.783,-1.132,101.77,-1.038]},{"z":4.6,"b":[123.075,0.306,123.5,0.941],"l":[123.075,0.941,123.205,0.837,123.247,0.733,123.325,0.664,123.439,0.611,123.481,0.557,123.5,0.43,123.457,0.306]},{"z":4.6,"b":[102.067,-2.782,104.532,-1.694],"l":[104.532,-1.734,104.428,-1.694,104.376,-1.714,104.322,-1.804,104.126,-1.831,103.958,-1.791,103.886,-1.795,103.535,-1.968,103.524,-2.225,103.46,-2.238,103.463,-2.352,103.419,-2.394,103.304,-2.285,103.272,-2.209,103.216,-2.186,103.17,-2.286,103.205,-2.361,103.123,-2.398,102.862,-2.354,102.864,-2.462,102.792,-2.568,102.619,-2.672,102.596,-2.71,102.487,-2.73,102.398,-2.71,102.26,-2.64,102.233,-2.684,102.067,-2.782]},{"z":4.6,"b":[100.163,-1.038,101.77,0.758],"l":[101.77,-1.038,101.674,-0.967,101.525,-0.932,101.434,-0.847,101.331,-0.787,101.236,-0.703,101.04,-0.474,101,-0.401,100.856,-0.322,100.79,-0.206,100.749,-0.064,100.846,-0.03,100.847,0.186,100.51,0.444,100.369,0.411,100.265,0.45,100.163,0.648,100.207,0.711,100.173,0.758]},{"z":4.6,"b":[100.071,0.758,100.441,2.555],"l":[100.173,0.758,100.21,0.866,100.21,1.054,100.225,1.146,100.148,1.225,100.13,1.3,100.071,1.359,100.105,1.415,100.35,1.525,100.441,1.599,100.435,1.667,100.376,1.814,100.304,1.881,100.319,2.046,100.313,2.177,100.279,2.355,100.314,2.555]},{"z":4.6,"b":[121.16,0.483,122.197,1.028],"l":[122.197,1.028,122.046,0.944,121.993,0.935,121.84,0.97,121.508,0.847,121.378,0.861,121.303,0.826,121.235,0.725,121.16,0.686,121.257,0.587,121.334,0.576,121.333,0.483]},{"z":4.6,"b":[101.77,-1.085,103.525,-0.739],"l":[103.525,-0.739,103.1,-0.772,102.917,-0.763,102.804,-0.793,102.682,-0.904,102.541,-1.085,102.452,-1.068,102.356,-1.01,102.261,-0.88,102.189,-0.856,102.013,-0.88,101.935,-0.927,101.882,-1,101.77,-1.038]},{"z":4.6,"b":[121.598,-3.246,122.349,-2.973],"l":[121.598,-2.973,121.781,-2.986,122.058,-3.093,122.207,-3.111,122.349,-3.246]},{"z":4.6,"b":[119.494,-1.955,119.873,-0.834],"l":[119.873,-1.955,119.823,-1.872,119.846,-1.792,119.822,-1.729,119.725,-1.613,119.693,-1.461,119.642,-1.394,119.494,-1.329,119.527,-1.224,119.602,-1.151,119.572,-1.052,119.566,-0.834]},{"z":4.6,"b":[106.763,-6.382,106.989,-6.027],"l":[106.763,-6.301,106.924,-6.382,106.985,-6.235,106.989,-6.027]},{"z":4.6,"b":[106.684,-6.301,106.763,-6.059],"l":[106.763,-6.301,106.702,-6.21,106.684,-6.102,106.731,-6.059]},{"z":4.6,"b":[120.883,31.012,121.341,31.491],"l":[120.909,31.012,120.883,31.14,121.035,31.144,121.059,31.248,121.126,31.277,121.136,31.374,121.187,31.458,121.341,31.491]},{"z":4.6,"b":[116.377,31.133,119.63,34.64],"l":[119.63,31.133,119.363,31.192,119.317,31.267,119.163,31.287,119.075,31.239,118.816,31.226,118.756,31.281,118.751,31.358,118.827,31.398,118.881,31.522,118.86,31.627,118.681,31.647,118.684,31.701,118.477,31.79,118.481,31.859,118.352,31.947,118.383,32.054,118.499,32.138,118.514,32.2,118.65,32.238,118.684,32.339,118.668,32.453,118.612,32.478,118.552,32.574,118.752,32.613,118.891,32.596,119.081,32.446,119.199,32.594,119.19,32.711,119.11,32.834,119.009,32.91,118.993,32.962,118.857,32.972,118.798,32.865,118.748,32.839,118.72,32.732,118.399,32.731,118.3,32.777,118.239,32.924,118.248,33.013,118.218,33.18,118.081,33.15,117.996,33.168,117.932,33.235,118.122,33.611,118.177,33.693,118.124,33.766,117.926,33.734,117.752,33.724,117.746,33.882,117.688,33.888,117.651,33.982,117.572,33.99,117.503,34.05,117.392,34.031,117.321,34.079,117.159,34.091,117.034,34.156,116.97,34.275,116.949,34.389,116.802,34.4,116.77,34.452,116.625,34.485,116.428,34.64,116.377,34.64]},{"z":4.6,"b":[119.63,30.764,120.909,31.176],"l":[119.63,31.133,119.74,31.176,119.932,31.15,119.943,31.087,120.129,30.943,120.213,30.921,120.362,30.939,120.447,30.89,120.491,30.764,120.578,30.846,120.709,30.891,120.692,30.977,120.909,31.012]},{"z":4.6,"b":[120.909,30.691,121.275,31.012],"l":[120.909,31.012,120.969,31.009,120.981,30.832,121.104,30.834,121.256,30.744,121.275,30.691]},{"z":4.6,"b":[118.001,28.289,118.476,29.408],"l":[118.446,28.289,118.476,28.332,118.458,28.517,118.416,28.604,118.426,28.695,118.372,28.799,118.303,28.836,118.229,28.955,118.064,29.051,118.001,29.144,118.052,29.275,118.132,29.298,118.175,29.408]},{"z":4.6,"b":[118.175,29.408,119.63,31.133],"l":[118.175,29.408,118.373,29.453,118.47,29.524,118.557,29.625,118.648,29.658,118.724,29.73,118.741,29.828,118.82,29.88,118.894,29.983,118.853,30.159,118.909,30.222,118.881,30.324,118.935,30.352,119.058,30.312,119.211,30.314,119.363,30.404,119.321,30.518,119.242,30.551,119.351,30.675,119.443,30.639,119.501,30.769,119.575,30.847,119.63,31.133]},{"z":4.6,"b":[115.843,24.917,118.446,28.289],"l":[115.888,24.917,115.888,25,115.843,25.195,115.908,25.238,115.979,25.351,116,25.497,116.045,25.575,116.028,25.636,116.124,25.764,116.135,25.866,116.18,25.907,116.323,25.955,116.397,26.061,116.419,26.153,116.399,26.288,116.502,26.407,116.594,26.395,116.619,26.492,116.542,26.563,116.505,26.689,116.545,26.868,116.67,26.987,116.923,27.021,117.04,27.11,117.154,27.276,117.101,27.347,117.125,27.43,117.112,27.568,117.225,27.723,117.287,27.773,117.284,27.857,117.544,27.967,117.605,27.866,117.746,27.813,117.823,27.937,117.946,27.973,118.061,27.979,118.131,28.041,118.368,28.1,118.349,28.22,118.446,28.289]},{"z":4.6,"b":[118.446,27.206,120.419,28.326],"l":[118.446,28.289,118.499,28.262,118.735,28.326,118.797,28.213,118.766,28.178,118.748,27.973,118.82,27.91,118.889,27.722,118.923,27.55,119.005,27.48,119.186,27.42,119.266,27.424,119.39,27.513,119.467,27.525,119.512,27.637,119.616,27.666,119.687,27.511,119.693,27.411,119.77,27.316,120.064,27.344,120.148,27.4,120.249,27.41,120.344,27.38,120.406,27.297,120.419,27.206]},{"z":4.6,"b":[81.759,17.909,84.778,19.161],"l":[81.759,17.909,81.924,18,82.042,18.043,82.132,18.039,82.24,17.993,82.33,18.044,82.353,18.155,82.31,18.195,82.361,18.292,82.368,18.414,82.449,18.522,82.503,18.511,82.511,18.422,82.597,18.325,82.587,18.244,82.781,18.358,82.78,18.417,82.878,18.409,82.904,18.355,83.039,18.39,83.037,18.469,83.084,18.525,83.028,18.641,83.127,18.758,83.202,18.742,83.394,18.869,83.327,18.93,83.341,19.005,83.424,18.964,83.49,19.052,83.573,19.07,83.613,19.13,83.706,19.001,83.802,19,83.854,18.86,83.902,18.806,84.071,18.818,84.153,18.788,84.318,18.809,84.463,18.946,84.525,19.033,84.641,19.101,84.695,19.161,84.778,19.116]},{"z":4.6,"b":[76.719,12.76,78.568,15.976],"l":[78.23,12.76,78.286,12.843,78.416,12.948,78.516,13.089,78.568,13.275,78.381,13.323,78.35,13.376,78.375,13.58,78.184,13.602,78.068,13.699,78.097,13.809,78.033,13.885,77.921,13.83,77.879,13.917,77.735,13.824,77.593,13.767,77.499,13.704,77.443,13.702,77.384,13.847,77.292,13.848,77.152,13.904,77.103,13.85,77.133,13.785,77.032,13.746,76.959,13.782,77.01,13.905,76.936,14.011,76.943,14.061,76.873,14.137,76.974,14.156,76.995,14.066,77.112,14.005,77.289,14.011,77.332,13.908,77.388,13.963,77.328,14.052,77.369,14.163,77.468,14.192,77.461,14.29,77.384,14.262,77.278,14.296,77.257,14.334,77.111,14.33,77.045,14.232,76.908,14.258,76.862,14.347,76.932,14.458,76.83,14.479,76.753,14.566,76.762,14.764,76.8,14.795,76.822,14.921,76.719,14.99,76.764,15.094,76.948,15.022,77.08,15.037,77.128,15.132,77.105,15.308,77.026,15.351,77,15.614,77.085,15.667,77.025,15.781,77.034,15.874,77.144,15.961,77.226,15.976,77.446,15.953]},{"z":4.6,"b":[77.446,15.833,81.776,17.909],"l":[77.446,15.953,77.641,15.901,77.85,15.894,77.888,15.919,78.087,15.833,78.235,15.95,78.256,16.006,78.4,16.083,78.5,16.094,78.734,16.03,78.839,16.067,78.839,16.125,79.022,16.247,79.161,16.219,79.215,16.244,79.233,16.331,79.213,16.493,79.266,16.57,79.406,16.587,79.504,16.642,79.721,16.689,79.752,16.724,79.882,16.703,79.958,16.63,80.043,16.739,80.073,16.823,80.002,16.871,80.078,16.973,80.22,17.033,80.281,16.997,80.351,16.862,80.506,16.792,80.586,16.934,80.474,16.937,80.384,16.976,80.502,17.108,80.591,17.14,80.67,17.08,80.83,17.046,80.893,17.156,80.99,17.218,81.144,17.25,81.214,17.324,81.408,17.371,81.47,17.42,81.535,17.638,81.629,17.763,81.776,17.833,81.759,17.909]},{"z":4.6,"b":[114.311,-3.434,115.68,-1.361],"l":[115.678,-1.426,115.68,-1.361,115.427,-1.434,115.404,-1.474,115.338,-1.765,115.358,-1.904,115.308,-1.992,115.33,-2.037,115.131,-2.208,114.905,-2.263,114.879,-2.326,114.908,-2.419,114.822,-2.562,114.785,-2.689,114.675,-2.759,114.631,-2.822,114.504,-2.881,114.471,-3.004,114.311,-3.434]},{"z":4.6,"b":[78.23,12.633,80.26,13.468],"l":[80.26,13.468,80.098,13.464,79.962,13.417,79.957,13.366,79.816,13.295,79.785,13.224,79.697,13.236,79.668,13.291,79.572,13.284,79.544,13.324,79.45,13.331,79.42,13.22,79.315,13.132,79.251,13.148,79.162,13.045,79.078,13.036,78.944,13.093,78.736,13.061,78.645,12.992,78.583,12.778,78.419,12.633,78.353,12.642,78.23,12.76]},{"z":4.6,"b":[80.278,13.513,80.288,13.527],"l":[80.288,13.527,80.278,13.513]},{"z":4.6,"b":[99.151,0.252,100.173,0.868],"l":[99.151,0.252,99.342,0.474,99.584,0.563,99.685,0.501,99.814,0.468,99.908,0.504,99.915,0.569,99.818,0.709,99.794,0.78,99.71,0.849,99.826,0.868,100.173,0.758]},{"z":4.6,"b":[97.784,2.189,98.268,4.295],"l":[98.101,2.189,98.128,2.327,98.063,2.444,98.025,2.576,98.069,2.677,98.057,2.812,97.923,2.909,97.896,3.083,97.936,3.141,97.864,3.257,97.99,3.33,97.907,3.433,97.914,3.484,97.844,3.58,97.784,3.745,97.893,3.848,97.89,3.915,98.001,3.98,98.063,4.24,98.188,4.295,98.268,4.288]},{"z":4.7,"b":[162.265,61.658,163.434,64.77],"l":[163.137,64.77,163.246,64.727,163.167,64.559,163.345,64.487,163.434,64.373,163.256,64.321,163.167,64.19,162.942,64.2,162.794,64.148,162.841,64.017,162.929,63.95,162.888,63.877,162.726,63.837,162.755,63.738,162.979,63.621,162.957,63.491,162.884,63.436,162.695,63.393,162.744,63.182,162.38,63.119,162.269,62.935,162.548,62.845,162.673,62.71,162.819,62.697,162.709,62.601,162.768,62.503,162.736,62.297,162.664,62.262,162.404,62.253,162.265,62.129,162.512,62.103,162.48,61.963,162.311,61.934,162.468,61.82,162.431,61.764,162.51,61.658]},{"z":4.6,"b":[72.947,20.04,73.166,20.127],"l":[73.166,20.115,73.161,20.047,72.997,20.04,72.947,20.127]},{"z":4.7,"b":[144.719,62.047,158.529,66.359],"l":[144.745,62.047,144.719,62.082,144.903,62.16,144.993,62.244,145.177,62.286,145.175,62.428,145.124,62.49,145.48,62.548,145.326,62.754,145.366,62.91,145.252,63.18,145.364,63.331,145.445,63.515,145.633,63.66,145.572,63.836,146.109,63.942,146.291,64.061,146.377,64.197,146.499,64.209,146.996,64.147,147.23,64.056,147.307,64.132,147.61,64.17,147.603,64.047,147.955,63.944,148.115,64.051,148.254,64.255,148.001,64.363,148.023,64.431,148.161,64.458,148.291,64.549,148.436,64.524,148.631,64.419,149.27,64.446,149.402,64.437,149.59,64.525,149.816,64.567,150.068,64.502,150.024,64.37,150.224,64.213,150.633,64.171,150.631,64.331,150.966,64.332,151.123,64.371,151.301,64.361,151.48,64.436,152.016,64.504,152.136,64.44,152.169,64.355,152.414,64.407,152.331,64.51,152.748,64.686,152.502,64.945,152.645,65.022,152.62,65.169,152.688,65.233,153.032,65.275,153.521,65.408,153.401,65.504,153.531,65.706,153.454,65.788,153.505,65.842,153.698,65.886,154.041,65.869,154.25,65.83,154.374,65.921,154.359,66.033,154.22,66.069,154.395,66.205,154.548,66.238,154.802,66.185,154.885,66.134,155.17,66.209,155.32,66.142,155.752,66.177,155.958,66.096,156.36,66.067,156.488,66.146,156.659,66.193,156.762,66.105,156.973,66.033,156.927,65.965,157.124,65.915,157.373,65.926,157.382,66,157.718,66.112,158.124,66.156,158.485,66.118,158.393,66.255,158.529,66.359]},{"z":4.7,"b":[158.529,64.649,163.137,66.359],"l":[158.529,66.359,158.776,66.284,159.166,66.234,159.203,66.164,159.052,66.048,158.934,65.903,158.922,65.743,159.377,65.658,159.834,65.529,159.997,65.523,160.17,65.451,160.174,65.376,160.418,65.251,160.537,65.162,160.688,65.149,161.01,65.17,161.281,65.141,161.417,65.069,161.645,65.013,161.784,64.838,162.293,64.742,162.608,64.734,162.736,64.661,162.997,64.649,163.117,64.693,163.137,64.77]},{"z":4.6,"b":[115.427,39.442,117.429,41.031],"l":[117.357,40.257,117.251,40.338,117.238,40.45,117.184,40.497,117.243,40.57,117.412,40.597,117.429,40.669,117.303,40.662,117.199,40.69,117.058,40.677,116.91,40.749,116.676,40.94,116.653,41.031,116.438,40.9,116.459,40.792,116.291,40.74,116.207,40.75,116.13,40.658,115.995,40.596,115.874,40.61,115.728,40.54,115.945,40.294,115.82,40.152,115.578,40.096,115.427,39.963,115.496,39.924,115.511,39.838,115.437,39.749,115.463,39.643,115.618,39.604,115.756,39.509,115.945,39.575,116.001,39.558,116.21,39.572,116.25,39.506,116.387,39.442,116.428,39.521,116.604,39.611,116.793,39.602]},{"z":4.6,"b":[116.793,39.602,116.876,39.686],"l":[116.793,39.602,116.876,39.686]},{"z":4.6,"b":[117.239,40.093,117.372,40.257],"l":[117.239,40.093,117.311,40.11,117.372,40.201,117.357,40.257]},{"z":4.6,"b":[113.454,36.356,114.541,40.743],"l":[113.721,36.356,113.631,36.438,113.535,36.483,113.547,36.549,113.459,36.636,113.454,36.737,113.608,36.769,113.679,36.872,113.776,36.891,113.754,37.115,113.858,37.209,113.887,37.296,114.053,37.501,114.096,37.6,114.1,37.698,114.026,37.751,113.987,37.839,113.891,37.949,113.791,38.153,113.558,38.243,113.518,38.378,113.534,38.537,113.609,38.644,113.669,38.66,113.825,38.81,113.76,38.909,113.853,39.072,114.013,39.119,114.089,39.074,114.216,39.069,114.394,39.169,114.42,39.3,114.541,39.531,114.402,39.658,114.383,39.861,114.233,39.876,114.086,39.917,113.936,40.016,113.996,40.132,114.085,40.185,114.45,40.294,114.434,40.361,114.286,40.379,114.258,40.55,114.152,40.674,114.154,40.743]},{"z":4.6,"b":[117.357,39.219,118.01,40.257],"l":[118.01,39.219,118.008,39.343,117.897,39.399,117.893,39.565,117.631,39.576,117.623,39.666,117.533,39.75,117.501,39.989,117.766,39.964,117.75,40.063,117.451,40.227,117.357,40.257]},{"z":4.6,"b":[115.276,36.158,117.837,38.271],"l":[115.46,36.158,115.449,36.249,115.343,36.352,115.276,36.474,115.342,36.6,115.478,36.754,115.626,36.794,115.773,36.946,115.852,37.057,115.907,37.206,115.975,37.322,116.115,37.368,116.256,37.387,116.22,37.462,116.35,37.621,116.411,37.485,116.609,37.639,116.744,37.799,116.84,37.834,117.421,37.847,117.569,38.049,117.687,38.073,117.78,38.164,117.837,38.271]},{"z":4.6,"b":[113.721,36.057,115.46,36.359],"l":[115.46,36.158,115.35,36.082,115.245,36.161,115.106,36.185,115.034,36.101,114.931,36.057,114.893,36.12,114.707,36.153,114.548,36.139,114.343,36.245,114.156,36.247,114.063,36.277,113.989,36.359,113.808,36.339,113.721,36.356]},{"z":4.6,"b":[116.151,34.567,116.377,34.64],"l":[116.377,34.64,116.267,34.577,116.151,34.567]},{"z":4.6,"b":[114.834,34.567,116.151,36.158],"l":[116.151,34.567,116.044,34.6,115.816,34.571,115.707,34.6,115.646,34.569,115.542,34.578,115.436,34.676,115.429,34.78,115.318,34.843,115.216,34.836,115.175,34.944,114.998,35.008,114.834,35.016,114.864,35.189,114.942,35.219,115.068,35.38,115.227,35.416,115.345,35.496,115.395,35.616,115.504,35.723,115.655,35.746,115.761,35.849,115.874,35.866,115.863,35.925,115.966,35.986,116.034,35.966,116.077,36.103,115.927,36.022,115.829,36.031,115.739,35.969,115.53,35.911,115.468,35.913,115.416,35.824,115.318,35.792,115.334,35.931,115.424,35.995,115.46,36.158]},{"z":4.6,"b":[81.394,17.807,83.996,22.539],"l":[81.394,17.807,81.444,17.88,81.543,18.262,81.644,18.292,81.741,18.358,81.792,18.438,81.913,18.545,81.904,18.622,81.969,18.686,82.139,18.747,82.169,18.879,82.224,18.922,82.225,19.015,82.145,19.244,82.169,19.387,82.029,19.518,82.029,19.776,81.824,19.913,81.868,20.047,81.94,20.105,82.046,20.018,82.096,20.051,82.228,19.959,82.29,19.836,82.381,19.886,82.682,19.823,82.695,20.001,82.611,19.988,82.416,20.067,82.434,20.159,82.422,20.428,82.34,20.556,82.367,20.657,82.342,20.861,82.454,20.847,82.562,20.954,82.668,21.155,82.965,21.179,83.078,21.115,83.198,21.173,83.279,21.359,83.361,21.337,83.381,21.4,83.328,21.499,83.352,21.577,83.444,21.719,83.473,21.807,83.557,21.835,83.571,21.923,83.515,21.964,83.546,22.102,83.605,22.196,83.733,22.262,83.841,22.349,83.946,22.371,83.996,22.441,83.97,22.539]},{"z":4.6,"b":[83.97,21.986,86.687,22.557],"l":[83.97,22.539,84.067,22.5,84.148,22.387,84.311,22.324,84.421,22.331,84.53,22.423,84.734,22.427,84.801,22.45,84.908,22.422,85.066,22.503,85.1,22.394,85.088,22.28,84.985,22.113,85.109,22.108,85.232,22.035,85.324,22.127,85.425,22.164,85.551,22.108,85.679,22.076,85.792,22.129,85.785,22.012,85.834,21.986,85.916,22.022,85.974,22.105,85.965,22.245,85.987,22.394,85.939,22.501,86.04,22.557,86.102,22.5,86.268,22.422,86.366,22.33,86.487,22.337,86.687,22.218]},{"z":4.6,"b":[116.377,34.399,119.296,35.106],"l":[116.377,34.64,116.42,34.834,116.47,34.898,116.632,34.938,116.828,34.958,116.941,34.88,116.993,34.774,117.061,34.709,117.182,34.45,117.235,34.461,117.274,34.564,117.337,34.589,117.469,34.475,117.591,34.472,117.653,34.526,117.773,34.527,117.79,34.651,117.915,34.677,118.05,34.651,118.165,34.399,118.391,34.444,118.423,34.578,118.477,34.689,118.596,34.707,118.681,34.684,118.759,34.749,118.777,34.834,118.842,34.935,118.867,35.036,119.076,35.05,119.176,35.106,119.296,35.071]},{"z":4.6,"b":[81.394,17.797,81.759,17.909],"l":[81.759,17.909,81.578,17.797,81.394,17.807]},{"z":4.6,"b":[25.013,-28.068,27.583,-26.738],"l":[25.013,-28.068,25.053,-28.058,25.129,-27.929,25.225,-27.878,25.241,-27.823,25.421,-27.71,25.581,-27.655,25.64,-27.673,25.799,-27.581,25.874,-27.621,26,-27.639,26.229,-27.489,26.25,-27.426,26.45,-27.353,26.486,-27.254,26.411,-27.177,26.49,-27.139,26.645,-26.999,26.914,-26.89,27.044,-26.929,27.178,-26.924,27.308,-26.862,27.408,-26.894,27.48,-26.874,27.519,-26.781,27.583,-26.738]},{"z":4.6,"b":[27.583,-26.894,28.256,-26.671],"l":[27.583,-26.738,27.644,-26.764,27.882,-26.724,27.97,-26.671,28.044,-26.818,28.256,-26.894]},{"z":4.6,"b":[29.016,-31.078,30.196,-29.979],"l":[30.196,-31.078,30.126,-30.934,30.064,-30.859,29.974,-30.832,29.794,-30.709,29.602,-30.688,29.476,-30.62,29.32,-30.655,29.22,-30.618,29.163,-30.555,29.2,-30.436,29.296,-30.374,29.298,-30.318,29.175,-30.286,29.102,-30.174,29.116,-30.078,29.016,-29.979]},{"z":4.6,"b":[25.467,-30.681,27.378,-30.312],"l":[27.378,-30.312,27.298,-30.488,27.132,-30.502,27.01,-30.533,26.922,-30.655,26.781,-30.643,26.703,-30.675,26.535,-30.651,26.442,-30.56,26.311,-30.582,26.205,-30.525,26.06,-30.514,25.725,-30.681,25.634,-30.657,25.611,-30.606,25.467,-30.613]},{"z":4.6,"b":[28.859,-28.773,29.771,-27.452],"l":[28.859,-28.773,28.973,-28.575,29.189,-28.535,29.235,-28.44,29.407,-28.357,29.488,-28.268,29.589,-28.25,29.675,-28.121,29.624,-28.048,29.722,-27.811,29.683,-27.668,29.766,-27.605,29.716,-27.518,29.771,-27.452]},{"z":4.6,"b":[29.771,-27.452,31.27,-27.236],"l":[29.771,-27.452,29.929,-27.364,30.133,-27.379,30.281,-27.302,30.423,-27.275,30.444,-27.309,30.575,-27.311,30.662,-27.277,30.833,-27.334,30.902,-27.311,30.98,-27.355,31.151,-27.324,31.27,-27.236]},{"z":4.6,"b":[92.995,24.033,93.312,24.399],"l":[93.312,24.033,93.249,24.057,93.106,24.045,92.995,24.095,93.018,24.399]},{"z":4.6,"b":[92.246,23.683,92.325,24.238],"l":[92.246,23.683,92.271,23.829,92.319,23.851,92.325,24.188,92.298,24.238]},{"z":4.6,"b":[118.855,39.986,119.85,41.273],"l":[119.85,39.986,119.738,40.1,119.743,40.205,119.635,40.209,119.58,40.373,119.6,40.477,119.564,40.54,119.294,40.531,119.19,40.62,119.191,40.7,119.068,40.651,118.855,40.821,118.965,41.075,119.048,41.094,119.164,41.176,119.231,41.273]},{"z":4.6,"b":[93.449,25.206,94.655,25.684],"l":[94.655,25.445,94.553,25.49,94.565,25.684,94.325,25.512,94.209,25.495,94.027,25.554,93.803,25.544,93.809,25.446,93.679,25.328,93.605,25.206,93.449,25.302]},{"z":4.6,"b":[123.696,40.859,125.766,43.472],"l":[125.704,40.859,125.576,40.913,125.692,41.004,125.766,41.139,125.738,41.245,125.651,41.286,125.47,41.575,125.45,41.68,125.306,41.68,125.328,41.747,125.286,41.826,125.295,41.957,125.458,42.105,125.481,42.151,125.284,42.234,125.269,42.309,125.179,42.333,125.209,42.406,125.102,42.494,125.096,42.577,125.019,42.647,124.942,42.804,124.86,42.888,124.888,43.087,124.766,43.106,124.673,42.999,124.52,42.872,124.376,42.986,124.419,43.082,124.296,43.157,124.282,43.226,124.006,43.31,123.807,43.454,123.749,43.472,123.696,43.353]},{"z":4.7,"b":[144.745,59.365,147.159,62.058],"l":[144.745,62.047,144.968,62.057,145.156,61.968,145.272,61.992,145.514,61.964,145.743,62.058,145.891,62.025,146.219,61.903,146.361,61.82,146.505,61.662,146.583,61.636,146.613,61.443,146.768,61.403,146.683,61.313,146.747,61.223,146.747,61.115,146.678,61.081,146.701,60.959,146.546,60.958,146.464,60.709,146.32,60.659,146.069,60.739,145.62,60.557,145.51,60.464,145.63,60.392,145.571,60.335,145.793,60.198,146.1,60.224,146.374,60.271,146.452,60.142,146.68,60.095,146.657,59.982,146.787,59.901,146.989,59.948,147.141,59.849,147.159,59.647,147.075,59.411,146.887,59.365]},{"z":4.7,"b":[133.972,46.637,139.012,48.445],"l":[139.012,47.396,138.856,47.466,138.755,47.438,138.629,47.451,138.595,47.548,138.618,47.629,138.517,47.669,138.61,47.726,138.707,47.735,138.826,47.861,138.581,48.031,138.722,48.136,138.71,48.183,138.558,48.181,138.48,48.303,138.489,48.402,138.306,48.445,138.179,48.379,138.145,48.316,137.995,48.297,137.843,48.209,137.579,48.228,137.449,48.202,137.315,48.084,137.417,47.978,137.597,47.91,137.601,47.784,137.787,47.697,137.896,47.724,138.048,47.668,138.087,47.517,138.006,47.455,137.755,47.329,137.598,47.3,137.452,47.198,137.328,47.214,137.094,47.297,137.013,47.236,136.934,47.057,136.974,47.012,136.835,46.944,136.728,46.841,136.577,46.777,136.451,46.81,136.384,46.769,136.264,46.765,136.062,46.85,135.942,46.79,135.85,46.811,135.746,46.949,135.526,46.953,135.42,46.911,135.363,46.95,135.433,47.019,135.205,47.153,135.078,47.193,135.015,47.128,134.851,47.154,134.653,47.092,134.549,46.967,134.443,46.942,134.412,46.866,134.465,46.786,134.412,46.678,134.199,46.644,133.972,46.637]},{"z":4.7,"b":[131.396,48.371,134.987,49.486],"l":[131.396,49.244,131.463,49.206,131.612,49.288,131.767,49.31,131.966,49.305,132.068,49.466,132.12,49.486,132.398,49.393,132.382,49.295,132.639,49.29,132.732,49.315,132.815,49.235,133.033,49.219,133.201,49.141,133.309,48.826,133.77,48.759,133.824,48.683,133.935,48.64,134.105,48.633,134.269,48.716,134.392,48.743,134.482,48.659,134.746,48.569,134.862,48.625,134.987,48.595,134.977,48.507,134.884,48.424,134.769,48.374,134.598,48.416,134.323,48.371]},{"z":4.7,"b":[130.862,55.679,144.745,62.573],"l":[130.923,55.679,130.932,55.749,130.862,55.889,130.924,55.942,130.964,56.129,131.093,56.206,131.088,56.345,131.184,56.468,131.394,56.479,131.502,56.55,131.67,56.55,131.676,56.648,131.797,56.717,131.751,56.843,131.445,57.036,131.474,57.079,131.179,57.137,131.183,57.235,131.509,57.251,131.599,57.296,131.565,57.365,131.617,57.453,131.729,57.5,131.693,57.561,131.975,57.586,132.035,57.691,131.888,57.809,131.757,57.853,131.806,57.906,131.61,57.995,131.477,58.168,131.705,58.174,132.039,58.02,132.017,58.19,132.196,58.248,132.207,58.35,132.129,58.489,132.372,58.524,132.564,58.64,132.599,58.891,132.808,58.88,132.871,58.987,132.862,59.064,133.126,59.204,133.384,59.24,133.543,59.294,133.625,59.232,133.767,59.223,134.047,59.256,134.219,59.209,134.555,59.181,134.725,59.211,134.752,59.153,135.209,59.122,135.26,59.22,135.422,59.387,135.619,59.495,135.791,59.534,136.053,59.438,136.759,59.355,136.836,59.398,137.182,59.446,137.349,59.549,137.456,59.551,137.458,59.647,137.616,59.74,137.833,59.761,138.044,59.741,138.174,59.68,138.253,59.713,138.174,59.837,138.263,59.938,138.2,60.231,138.315,60.318,138.327,60.406,138.243,60.493,138.404,60.611,138.429,60.702,138.284,60.782,138.186,60.895,138.205,60.952,138.346,61,138.383,61.1,138.703,61.193,138.651,61.282,138.719,61.337,138.941,61.321,139.12,61.449,139.247,61.433,139.435,61.491,139.548,61.482,139.667,61.641,139.919,61.802,139.988,61.972,140.114,61.971,140.256,62.038,140.3,62.119,140.294,62.274,140.404,62.422,140.635,62.425,140.705,62.573,140.881,62.499,141.055,62.482,141.067,62.417,141.302,62.431,141.545,62.179,141.838,62.041,142.008,62.027,142.187,62.081,142.29,61.963,142.438,61.87,142.653,61.962,142.826,61.949,142.928,61.901,143.085,62.018,143.126,62.101,143.362,62.024,143.62,62.016,143.571,61.948,143.954,61.957,143.927,61.766,144.002,61.727,144.245,61.76,144.54,61.856,144.586,61.978,144.745,62.047]},{"z":4.7,"b":[70.43,55.285,75.105,58.581],"l":[75.105,58.581,74.671,58.354,74.614,58.294,74.484,58.269,74.206,58.134,73.087,58.146,72.861,58.011,72.476,58.028,72.149,58.026,71.989,58.123,71.269,58.076,71.221,58.166,71.245,58.341,71.302,58.385,71.12,58.453,71.084,58.518,70.865,58.535,70.436,57.934,70.43,57.785,70.565,57.705,70.58,57.584,70.675,57.511,70.436,57.409,70.491,57.304,70.634,57.265,70.729,57.194,70.8,57.319,71.08,57.344,71.144,57.252,71.371,57.064,71.652,56.923,71.673,56.838,71.557,56.732,71.4,56.725,71.357,56.671,71.169,56.676,71.029,56.527,70.855,56.456,70.927,56.347,70.77,56.296,70.85,56.236,70.828,56.16,70.909,56.112,70.847,55.963,70.586,55.838,70.549,55.676,70.747,55.59,70.676,55.505,70.544,55.487,70.471,55.418,70.469,55.285]},{"z":4.7,"b":[61.98,54.163,63.329,56.134],"l":[63.329,54.163,63.25,54.247,63.128,54.306,63.13,54.448,63.238,54.478,63.295,54.564,63.192,54.686,62.903,54.694,62.711,54.626,62.424,54.723,62.379,54.672,62.147,54.644,62.025,54.717,62.123,54.815,61.98,54.934,61.989,55.035,62.244,55.079,62.165,55.242,62.049,55.311,62.234,55.341,62.304,55.403,62.579,55.517,62.516,55.564,62.517,55.738,62.343,55.905,62.305,55.979,62.132,56.065,62.072,56.134]},{"z":4.7,"b":[65.935,58.589,75.159,59.989],"l":[65.935,58.614,66.252,58.614,66.38,58.589,66.741,58.67,66.786,58.72,66.774,58.93,66.951,58.973,67.071,58.925,67.264,59.02,67.564,59.093,67.742,59.212,68.195,59.242,68.358,59.283,68.543,59.551,68.844,59.621,69.295,59.783,69.078,59.893,69.133,59.945,69.306,59.989,69.501,59.989,69.616,59.905,70.155,59.9,70.248,59.84,70.399,59.817,70.798,59.863,71.017,59.823,71.193,59.905,71.296,59.799,71.777,59.648,71.882,59.573,72,59.566,72.355,59.34,72.437,59.303,72.454,59.215,72.657,59.107,72.863,59.157,72.976,59.099,72.967,59,73.095,58.946,73.032,58.863,73.444,58.818,73.621,58.854,73.756,58.809,74,58.8,74.1,58.739,74.743,58.748,74.899,58.696,74.981,58.718,75.159,58.65]},{"z":4.7,"b":[75.105,58.581,75.159,58.65],"l":[75.159,58.65,75.105,58.581]},{"z":4.7,"b":[64.967,55.33,68.772,56.768],"l":[68.772,55.33,68.591,55.424,68.487,55.401,68.317,55.555,68.174,55.596,68.189,55.7,67.912,55.665,67.825,55.721,67.591,55.711,67.447,55.735,67.299,55.897,66.989,55.989,66.802,55.967,66.761,56.045,66.463,55.95,66.298,56.058,66.186,56.068,66.028,55.99,65.861,56.065,65.708,56.195,65.667,56.274,65.117,56.325,65.088,56.448,64.967,56.501,65.016,56.649,64.995,56.768]},{"z":4.7,"b":[64.839,56.768,66.197,58.614],"l":[64.995,56.768,65.14,56.77,65.227,56.86,65.101,56.892,64.994,57.042,64.992,57.221,64.918,57.296,64.842,57.446,64.937,57.549,64.839,57.641,65.428,57.834,65.54,57.78,65.702,57.901,66.197,58.033,66.002,58.297,65.935,58.614]},{"z":4.6,"b":[109.76,21.56,112.047,24.736],"l":[111.997,24.736,111.932,24.698,111.933,24.601,112.006,24.553,111.991,24.466,112.047,24.381,111.976,24.277,111.869,24.232,111.899,23.991,111.808,23.909,111.793,23.849,111.658,23.842,111.614,23.76,111.601,23.658,111.471,23.613,111.469,23.558,111.391,23.431,111.363,23.273,111.37,23.139,111.411,23.053,111.351,22.908,111.277,22.819,111.181,22.749,111.066,22.741,111.024,22.639,110.755,22.58,110.697,22.369,110.775,22.284,110.671,22.268,110.642,22.183,110.467,22.151,110.414,22.198,110.334,22.19,110.355,22.101,110.356,21.889,110.216,21.875,110.142,21.897,109.967,21.867,109.917,21.687,109.76,21.56]},{"z":4.6,"b":[134.255,-4.255,135.265,-2.838],"l":[134.873,-4.255,135.265,-3.76,134.258,-3.399,134.255,-3.308,134.332,-3.19,134.451,-3.078,134.445,-3.024,134.67,-2.838]},{"z":4.6,"b":[104.54,24.631,109.457,26.039],"l":[104.54,24.742,104.729,24.631,104.852,24.706,104.877,24.755,105.012,24.797,105.1,24.954,105.206,25.004,105.289,24.933,105.381,24.957,105.444,24.924,105.509,24.817,105.675,24.79,105.788,24.716,105.916,24.729,106.001,24.648,106.176,24.782,106.199,24.877,106.156,24.96,106.307,24.981,106.587,25.104,106.667,25.174,106.886,25.197,106.909,25.248,106.996,25.255,107.015,25.353,106.975,25.44,107.022,25.498,107.244,25.559,107.398,25.387,107.424,25.303,107.499,25.214,107.642,25.268,107.776,25.153,107.862,25.146,107.967,25.209,108.068,25.213,108.129,25.269,108.132,25.386,108.293,25.539,108.405,25.515,108.568,25.416,108.695,25.602,108.768,25.599,108.774,25.522,108.941,25.549,109.024,25.595,109.044,25.709,108.953,25.691,108.953,25.776,109.085,25.805,109.149,25.751,109.3,25.743,109.399,25.999,109.457,26.039]},{"z":4.6,"b":[109.457,24.643,111.997,26.382],"l":[109.457,26.039,109.603,26.053,109.672,25.985,109.677,25.889,109.772,25.908,109.781,26.024,109.838,26.045,109.912,26.181,110.062,26.15,110.034,26.036,110.138,26.052,110.247,25.978,110.342,26.113,110.485,26.172,110.537,26.225,110.577,26.351,110.689,26.332,110.724,26.274,110.879,26.275,110.935,26.382,111.123,26.306,111.252,26.28,111.257,26.21,111.202,25.93,111.256,25.869,111.408,25.908,111.411,25.803,111.324,25.73,111.305,25.639,111.316,25.503,111.254,25.399,111.164,25.365,111.114,25.239,110.993,25.151,110.955,25.025,111.006,24.935,111.079,24.944,111.126,25.044,111.274,25.148,111.427,25.106,111.462,25.019,111.43,24.943,111.474,24.793,111.428,24.681,111.517,24.643,111.616,24.707,111.667,24.78,111.797,24.76,111.926,24.775,111.997,24.736]},{"z":4.6,"b":[26.397,-25.317,28.294,-24.614],"l":[28.294,-25.317,28.231,-25.226,28.128,-25.2,28.124,-25.11,28.222,-25.078,28.098,-25.009,27.966,-24.985,27.906,-25.003,27.668,-24.971,27.587,-24.997,27.546,-25.093,27.488,-25.056,27.367,-25.059,27.289,-25.007,27.183,-24.991,27.055,-24.85,27.023,-24.724,26.944,-24.79,26.768,-24.866,26.626,-24.827,26.455,-24.812,26.41,-24.741,26.397,-24.614]},{"z":4.6,"b":[27.191,-26.738,28.294,-25.317],"l":[28.294,-25.317,28.235,-25.353,28.104,-25.372,28.004,-25.361,27.949,-25.41,28.033,-25.472,27.964,-25.593,27.943,-25.807,27.876,-25.853,27.728,-25.878,27.641,-25.804,27.459,-25.885,27.445,-26.007,27.395,-26.123,27.29,-26.146,27.324,-26.287,27.191,-26.382,27.23,-26.504,27.243,-26.64,27.335,-26.616,27.569,-26.65,27.583,-26.738]},{"z":4.6,"b":[28.355,-25.411,31.856,-23.963],"l":[31.856,-23.963,31.78,-23.965,31.712,-24.05,31.446,-24.023,31.413,-24.054,31.248,-24.101,31.203,-24.035,31.111,-24.097,31.051,-24.19,31.059,-24.321,31.195,-24.311,31.163,-24.379,31.26,-24.508,31.221,-24.557,30.975,-24.582,30.765,-24.465,30.734,-24.582,30.673,-24.618,30.72,-24.704,30.659,-24.819,30.492,-24.948,30.421,-24.851,30.35,-24.877,30.184,-24.855,30.185,-25.022,30.124,-25.123,30.035,-25.072,30.007,-25.15,30.004,-25.332,29.886,-25.344,29.77,-25.395,29.671,-25.37,29.528,-25.411,29.353,-25.404,29.268,-25.375,29.211,-25.408,29.166,-25.304,29.1,-25.318,29.038,-25.269,29.043,-25.142,28.969,-25.072,28.898,-24.956,28.732,-24.943,28.612,-25.002,28.629,-25.052,28.459,-25.117,28.355,-25.189,28.392,-25.224,28.606,-25.158]},{"z":4.6,"b":[28.294,-25.322,28.663,-25.158],"l":[28.606,-25.158,28.663,-25.247,28.527,-25.27,28.433,-25.322,28.294,-25.317]},{"z":4.6,"b":[28.256,-27.502,29.771,-26.894],"l":[28.256,-26.894,28.386,-26.933,28.426,-26.983,28.641,-27.01,28.652,-26.956,28.823,-27.039,28.959,-26.995,29.057,-27.03,29.126,-27.154,29.178,-27.138,29.442,-27.253,29.485,-27.342,29.615,-27.502,29.771,-27.452]},{"z":4.6,"b":[22.745,-33.98,24.182,-31.79],"l":[23.636,-33.98,23.687,-33.872,23.544,-33.796,23.367,-33.781,23.437,-33.712,23.618,-33.644,23.563,-33.496,23.437,-33.433,23.262,-33.399,23.029,-33.39,22.75,-33.402,22.745,-33.328,22.916,-33.096,22.916,-33.014,23.001,-32.889,23.195,-32.808,23.327,-32.808,23.37,-32.768,23.278,-32.668,23.237,-32.505,23.243,-32.425,23.325,-32.355,23.602,-32.325,23.674,-32.239,23.757,-32.196,23.885,-32.228,24.029,-32.175,24.09,-32.125,24.122,-32.003,24.182,-31.952,24.13,-31.858,24.147,-31.79]},{"z":4.7,"b":[42.489,51.608,43.08,52.403],"l":[42.489,51.608,42.49,51.693,42.643,51.804,42.694,51.966,42.806,52.012,42.751,52.078,42.752,52.174,42.996,52.365,43.08,52.403]},{"z":4.7,"b":[43.08,52.316,46.831,52.695],"l":[43.08,52.403,43.193,52.462,43.353,52.393,43.532,52.426,43.747,52.433,44.044,52.35,44.347,52.316,44.422,52.416,44.539,52.432,44.5,52.521,44.873,52.434,44.91,52.378,45.072,52.377,45.14,52.421,45.264,52.392,45.431,52.4,45.528,52.481,45.644,52.519,45.717,52.471,45.981,52.407,45.971,52.486,46.147,52.591,46.276,52.619,46.289,52.672,46.516,52.695,46.713,52.628,46.831,52.616]},{"z":4.7,"b":[53.47,51.575,58.857,54.046],"l":[53.47,54.046,53.598,53.883,53.656,53.752,53.878,53.658,54.108,53.417,54.247,53.375,54.302,53.41,54.52,53.36,54.559,53.27,54.664,53.236,54.802,53.271,54.927,53.22,54.955,53.11,55.162,52.823,55.264,52.862,55.407,52.837,55.412,52.658,55.545,52.542,55.546,52.385,55.778,52.344,55.889,52.399,55.919,52.499,56.078,52.604,56.365,52.523,56.445,52.42,56.451,52.309,56.194,52.189,56.217,52.132,56.495,52.162,56.627,52.118,56.685,52.045,56.608,51.829,56.748,51.773,56.715,51.721,56.78,51.592,56.89,51.625,56.876,51.699,56.999,51.728,57.036,51.66,57.207,51.575,57.374,51.639,57.432,51.726,57.621,51.759,57.653,51.863,57.784,51.826,57.882,51.846,57.981,51.786,58.092,51.795,58.236,51.738,58.494,51.762,58.576,51.793,58.639,52.016,58.694,52.049,58.606,52.181,58.691,52.265,58.857,52.263,58.828,52.433]},{"z":4.7,"b":[42.873,50.368,47.447,51.231],"l":[47.447,50.368,47.277,50.372,47.184,50.512,47.008,50.48,46.878,50.524,46.83,50.633,46.708,50.691,46.583,50.701,46.483,50.617,46.461,50.547,46.295,50.567,46.255,50.529,46.065,50.544,46.068,50.652,45.89,50.759,45.811,50.774,45.678,50.729,45.668,50.594,45.495,50.613,45.241,50.586,45.191,50.697,45.293,50.781,45.308,50.877,45.215,50.942,45.202,51.04,45.042,51.126,44.66,51.173,44.21,51.194,44.182,51.117,44.004,51.087,43.864,51.113,43.682,51.113,43.642,51.066,43.33,51.018,43.142,51.122,42.873,51.231]},{"z":4.7,"b":[50.796,51.729,52.539,54.377],"l":[50.796,51.729,51.415,52.103,51.453,52.219,51.413,52.35,51.563,52.447,51.471,52.569,51.545,52.681,51.732,52.668,51.772,52.783,51.715,52.84,51.862,52.926,52.061,53,52.069,53.154,52.128,53.174,52.143,53.297,52.193,53.375,52.079,53.501,52.253,53.654,52.413,53.927,52.378,53.966,52.476,54.077,52.339,54.174,52.271,54.327,52.506,54.322,52.539,54.377]},{"z":4.7,"b":[52.539,53.974,53.47,54.377],"l":[52.539,54.377,52.771,54.347,53.001,54.271,52.951,54.196,53.141,54.076,53.258,54.071,53.384,53.974,53.47,54.046]},{"z":4.7,"b":[59.026,56.134,62.072,56.376],"l":[59.026,56.169,59.135,56.206,59.122,56.317,59.194,56.352,59.349,56.318,59.701,56.306,59.789,56.233,59.921,56.265,60.24,56.209,60.315,56.238,60.585,56.214,60.751,56.237,60.909,56.188,61.15,56.217,61.084,56.326,61.15,56.376,61.267,56.331,61.385,56.367,61.537,56.297,61.642,56.319,61.816,56.239,61.864,56.181,62.072,56.134]},{"z":4.7,"b":[85.079,52.612,87.175,54.487],"l":[85.079,54.469,85.16,54.487,85.435,54.212,85.577,54.232,85.811,54.126,85.944,54.04,86.117,54,86.232,53.887,86.292,53.876,86.406,53.751,86.455,53.614,86.569,53.538,86.723,53.484,86.934,53.453,86.983,53.407,86.705,53.196,86.943,53.08,87.077,53.075,87.026,52.921,86.944,52.862,87.074,52.717,87.175,52.706,87.173,52.612]},{"z":4.7,"b":[58.828,51.975,60.149,52.495],"l":[60.064,51.975,59.914,52.1,59.975,52.182,60.14,52.267,60.149,52.421,59.946,52.428,59.781,52.492,59.509,52.435,59.398,52.495,59.249,52.487,59.175,52.277,58.828,52.433]},{"z":4.7,"b":[57.137,52.433,59.975,56.169],"l":[58.828,52.433,58.777,52.525,58.812,52.587,58.724,52.645,58.778,52.704,58.749,52.835,58.9,53.066,58.891,53.188,58.829,53.2,58.865,53.32,58.868,53.502,58.821,53.596,58.907,53.666,58.873,53.758,58.906,53.862,59.014,53.958,59.162,53.993,59.263,54.14,59.344,54.185,59.544,54.196,59.694,54.129,59.801,54.242,59.667,54.34,59.746,54.447,59.679,54.498,59.86,54.603,59.975,54.801,59.903,54.867,59.806,54.846,59.614,54.867,59.404,54.703,59.228,54.611,59.061,54.632,58.825,54.572,58.614,54.47,58.364,54.547,58.206,54.515,57.971,54.393,57.743,54.501,57.742,54.583,57.594,54.629,57.497,54.701,57.386,54.72,57.165,54.819,57.243,54.957,57.137,55.092,57.234,55.15,57.264,55.262,57.518,55.334,57.675,55.3,58.03,55.275,58.144,55.209,57.965,55.159,58.089,55.02,58.147,55.117,58.288,55.171,58.364,55.084,58.62,54.95,58.816,55.025,58.674,55.188,58.797,55.203,58.872,55.323,58.981,55.303,59.159,55.332,59.201,55.45,59.565,55.5,59.447,55.632,59.255,55.614,59.296,55.78,59.165,55.79,59.228,55.911,59.178,55.99,59.269,56.048,59.188,56.127,59.026,56.169]},{"z":4.7,"b":[84.41,54.469,85.079,56.044],"l":[84.41,56.044,84.529,56.042,84.567,55.961,84.506,55.868,84.606,55.841,84.566,55.709,84.709,55.662,84.771,55.54,84.731,55.391,84.875,55.389,84.823,55.246,84.916,55.165,84.883,54.985,85.076,54.868,84.955,54.744,85.043,54.662,84.948,54.604,85.079,54.469]},{"z":4.7,"b":[77.785,53.327,85.079,54.469],"l":[85.079,54.469,84.693,54.337,84.532,54.17,84.266,54.216,84.003,54.144,83.555,54.123,83.492,54.086,83.499,53.991,83.385,53.949,83.295,54.083,83.183,54.062,82.733,53.787,82.486,53.469,82.256,53.591,82.214,53.663,82.104,53.662,81.968,53.719,81.846,53.71,81.756,53.778,81.732,53.854,81.838,53.895,81.855,53.969,81.582,54.017,81.474,54.072,81.247,54.101,81.112,54.152,81.104,54.213,80.994,54.265,80.925,54.396,80.802,54.33,80.655,54.204,80.525,54.17,80.297,54.07,80.128,54.075,80.044,54.001,79.866,53.932,79.862,53.858,79.721,53.841,79.572,53.733,79.442,53.693,79.304,53.716,79.245,53.68,79.028,53.661,78.886,53.682,78.603,53.616,78.627,53.529,78.377,53.488,78.26,53.495,78.204,53.612,77.925,53.565,77.93,53.44,77.785,53.327]},{"z":4.7,"b":[75.097,54.113,76.309,57.25],"l":[76.109,57.25,76.196,56.969,76.249,56.913,76.284,56.726,76.009,56.548,76.108,56.328,76.309,56.224,76.305,56.183,76.006,56.179,75.939,56.129,75.776,56.144,75.696,56.068,75.566,56.054,75.318,55.896,75.342,55.764,75.134,55.699,75.227,55.652,75.178,55.569,75.311,55.5,75.191,55.462,75.097,55.382,75.268,55.25,75.255,55.137,75.394,55.037,75.363,54.908,75.53,54.891,75.64,54.812,75.691,54.713,75.766,54.706,75.83,54.602,75.697,54.544,75.683,54.482,75.691,54.113]},{"z":4.7,"b":[76.109,55.692,84.41,57.25],"l":[76.109,57.25,76.802,57.21,76.919,57.188,78.444,57.161,78.448,57.122,79.59,56.929,80.249,56.455,80.31,56.439,81.122,56.533,81.215,56.508,81.426,56.373,81.557,56.258,81.944,56.339,82.146,56.335,82.342,56.356,82.427,56.394,82.828,56.441,82.856,56.533,82.996,56.508,83.096,56.549,83.252,56.456,83.154,56.382,83.137,56.276,83.214,56.212,83.128,56.132,83.353,55.969,83.407,55.893,83.293,55.851,83.223,55.737,83.316,55.692,83.403,55.735,83.568,55.693,83.783,55.882,83.91,55.913,83.951,56.005,84.197,56.055,84.329,55.988,84.41,56.044]},{"z":4.7,"b":[62.072,56.134,64.995,56.841],"l":[62.072,56.134,62.083,56.291,62.253,56.363,62.333,56.469,62.542,56.578,62.59,56.524,63.001,56.598,63.098,56.569,63.359,56.563,63.448,56.61,63.766,56.508,64.173,56.468,64.237,56.519,64.219,56.601,64.357,56.653,64.337,56.753,64.484,56.76,64.542,56.808,64.826,56.841,64.995,56.768]},{"z":4.7,"b":[75.08,57.25,76.109,58.581],"l":[75.105,58.581,75.303,58.487,75.304,58.451,75.08,58.354,75.222,58.222,75.093,58.11,75.543,57.941,75.581,57.675,75.727,57.599,75.859,57.399,75.992,57.385,76.109,57.25]},{"z":4.6,"b":[113.928,24.5,115.888,25.448],"l":[113.928,25.448,114.012,25.435,114.026,25.268,114.115,25.308,114.279,25.292,114.384,25.335,114.429,25.386,114.569,25.407,114.58,25.36,114.707,25.29,114.683,25.163,114.729,25.119,114.548,25.054,114.414,24.98,114.384,24.885,114.302,24.759,114.192,24.695,114.289,24.596,114.425,24.5,114.498,24.556,114.642,24.585,114.712,24.555,114.74,24.625,114.863,24.588,114.945,24.67,115.047,24.707,115.139,24.684,115.24,24.746,115.421,24.785,115.541,24.685,115.599,24.609,115.706,24.546,115.799,24.573,115.749,24.748,115.787,24.861,115.888,24.917]},{"z":4.6,"b":[115.888,23.616,117.175,24.917],"l":[115.888,24.917,116.036,24.894,116.115,24.846,116.372,24.841,116.53,24.615,116.604,24.656,116.745,24.671,116.749,24.551,116.882,24.394,116.968,24.185,116.925,24.078,117.037,23.738,117.175,23.616]},{"z":4.6,"b":[74.323,23.036,78.2,26.824],"l":[78.2,26.824,78.101,26.795,78.079,26.683,78.013,26.696,77.903,26.664,77.831,26.576,77.715,26.506,77.617,26.483,77.431,26.374,77.323,26.352,77.21,26.242,77.125,26.238,77.082,26.185,76.908,26.099,76.727,25.916,76.652,25.923,76.546,25.848,76.478,25.72,76.551,25.46,76.652,25.379,76.78,25.332,76.913,25.309,77.081,25.353,77.202,25.356,77.255,25.434,77.359,25.407,77.363,25.262,77.399,25.212,77.341,25.114,77.187,25.128,76.999,25.088,76.861,25.034,76.883,24.897,76.781,24.837,76.848,24.76,76.967,24.737,77.037,24.67,77.035,24.538,76.943,24.5,76.881,24.562,76.806,24.556,76.815,24.36,76.892,24.241,76.87,24.182,76.767,24.16,76.573,24.273,76.543,24.215,76.456,24.265,76.23,24.278,76.133,24.329,76.117,24.134,75.999,24.051,75.998,23.984,75.899,23.918,75.776,23.883,75.7,23.793,75.597,23.818,75.578,23.876,75.49,23.94,75.539,24.041,75.622,23.995,75.73,24.001,75.805,24.119,75.748,24.19,75.795,24.252,75.734,24.424,75.792,24.494,75.875,24.514,75.869,24.599,75.759,24.777,75.649,24.738,75.448,24.703,75.204,24.725,75.172,24.763,75.318,24.955,75.235,25.04,75.133,25.04,75.084,24.889,74.981,24.863,74.876,24.919,74.815,24.917,74.838,24.797,74.974,24.786,74.965,24.694,74.87,24.666,74.807,24.736,74.712,24.524,74.847,24.436,74.766,24.359,74.759,24.257,74.87,24.236,74.893,24.134,74.968,24.033,74.908,23.897,74.907,23.791,74.939,23.687,74.823,23.5,74.648,23.431,74.57,23.368,74.536,23.251,74.658,23.226,74.716,23.179,74.612,23.113,74.476,23.062,74.323,23.036]},{"z":4.6,"b":[111.997,24.736,113.928,25.506],"l":[111.997,24.736,112.066,24.797,112.145,24.794,112.165,24.912,112.121,24.949,112.183,25.153,112.214,25.2,112.458,25.164,112.663,25.113,112.714,25,112.808,24.943,112.989,24.964,112.959,25.056,112.969,25.181,112.889,25.241,112.917,25.325,113.09,25.414,113.217,25.506,113.422,25.384,113.523,25.386,113.582,25.326,113.714,25.361,113.828,25.357,113.928,25.448]},{"z":4.6,"b":[74.466,27.679,77.281,29.947],"l":[74.517,29.947,74.547,29.861,74.466,29.799,74.506,29.737,74.603,29.726,74.575,29.59,74.602,29.47,74.537,29.441,74.613,29.333,74.759,29.364,74.821,29.403,74.907,29.378,74.97,29.284,75.107,29.238,75.25,29.256,75.328,29.296,75.392,29.253,75.393,29.106,75.452,29.024,75.515,28.996,75.486,28.927,75.594,28.643,75.699,28.521,75.887,28.404,76.026,28.262,76.032,28.188,75.962,28.139,75.993,28.051,75.932,27.918,76,27.843,76.215,27.842,76.18,27.91,76.163,28.058,76.267,28.077,76.368,28.156,76.453,28.163,76.553,27.971,76.658,28.017,76.647,28.086,76.857,28.229,76.941,28.123,76.921,28.034,76.941,27.852,76.901,27.734,76.973,27.679,77.041,27.807,77.281,27.809]},{"z":4.6,"b":[77.281,26.824,78.223,27.809],"l":[77.281,27.809,77.346,27.542,77.46,27.385,77.606,27.324,77.657,27.203,77.591,27.131,77.513,27.109,77.628,26.973,77.461,26.904,77.511,26.833,77.705,26.93,77.977,26.904,78.107,26.947,78.223,26.937,78.2,26.824]},{"z":4.6,"b":[76.704,30.677,76.827,30.789],"l":[76.815,30.789,76.704,30.781,76.776,30.677,76.827,30.681]},{"z":4.6,"b":[73.898,29.947,74.517,30.222],"l":[74.517,29.947,73.916,29.98,73.898,30.035,73.97,30.137,73.933,30.222]},{"z":4.6,"b":[75.602,30.908,76.768,32.517],"l":[75.847,32.517,75.912,32.431,75.693,32.272,75.623,32.259,75.639,32.147,75.602,32.069,75.707,32.05,75.88,31.972,75.964,31.849,75.923,31.806,76.071,31.564,76.158,31.339,76.275,31.31,76.374,31.419,76.434,31.318,76.561,31.277,76.645,30.999,76.768,30.908]},{"z":4.6,"b":[76.768,30.789,76.833,30.908],"l":[76.768,30.908,76.833,30.842,76.815,30.789]},{"z":4.6,"b":[74.517,29.562,76.905,30.681],"l":[76.827,30.681,76.887,30.634,76.905,30.384,76.753,30.431,76.714,30.337,76.606,30.274,76.625,30.143,76.531,30.084,76.43,30.14,76.267,30.114,76.178,29.955,76.226,29.869,75.957,29.739,75.887,29.749,75.786,29.831,75.718,29.778,75.604,29.765,75.446,29.806,75.312,29.656,75.295,29.575,75.235,29.562,75.172,29.688,75.238,29.749,75.188,29.835,75.105,29.821,75.073,29.891,74.996,29.873,74.903,29.955,74.823,29.985,74.711,29.968,74.651,29.914,74.517,29.947]},{"z":4.7,"b":[43.634,47.477,45.301,48.255],"l":[45.301,48.098,45.137,48.112,44.963,48.209,44.933,48.135,44.766,48.059,44.662,48.089,44.547,48.063,44.54,48.197,44.306,48.255,44.314,48.094,44.448,48.05,44.467,47.99,44.389,47.868,44.083,47.909,44.032,47.816,43.951,47.756,43.822,47.75,43.705,47.683,43.634,47.57,43.732,47.477]},{"z":4.6,"b":[28.256,-26.894,28.976,-25.115],"l":[28.256,-26.894,28.304,-26.829,28.408,-26.759,28.402,-26.659,28.506,-26.613,28.529,-26.565,28.644,-26.508,28.767,-26.516,28.796,-26.427,28.864,-26.401,28.807,-26.308,28.718,-26.338,28.532,-26.221,28.501,-26.08,28.581,-26.059,28.634,-25.973,28.778,-26.012,28.87,-26.005,28.881,-25.896,28.941,-25.87,28.976,-25.674,28.92,-25.552,28.804,-25.542,28.859,-25.632,28.764,-25.706,28.67,-25.664,28.628,-25.527,28.629,-25.333,28.783,-25.228,28.752,-25.115,28.606,-25.158]},{"z":4.7,"b":[45.301,45.546,47.528,48.098],"l":[47.528,45.546,47.47,45.635,47.354,45.684,47.296,45.646,46.922,45.578,46.906,45.637,47.04,45.966,47.17,46.078,47.089,46.118,46.846,46.076,46.856,46.244,46.504,46.274,46.531,46.338,46.664,46.429,46.94,46.432,46.93,46.556,47.022,46.635,47.15,46.668,47.213,46.742,47.126,46.851,46.988,46.863,46.912,46.98,46.801,47.004,46.726,47.089,46.511,47.406,46.526,47.435,46.788,47.557,46.685,47.628,46.449,47.478,46.297,47.437,46.116,47.502,45.843,47.719,45.707,47.908,45.741,48,45.677,48.032,45.547,47.997,45.405,48.092,45.301,48.098]},{"z":4.7,"b":[44.987,48.098,46.644,48.869],"l":[45.301,48.098,45.15,48.238,44.987,48.268,45.042,48.395,45.172,48.439,45.375,48.464,45.301,48.558,45.609,48.593,45.76,48.666,45.948,48.836,46.057,48.869,46.298,48.84,46.644,48.659]},{"z":4.7,"b":[46.831,52.57,48.437,52.81],"l":[46.831,52.616,46.917,52.637,47.062,52.57,47.435,52.57,47.531,52.611,47.731,52.587,47.777,52.68,47.917,52.675,47.998,52.754,48.252,52.81,48.437,52.78]},{"z":4.7,"b":[48.351,51.729,50.794,52.78],"l":[48.437,52.78,48.351,52.669,48.524,52.682,48.7,52.639,48.73,52.568,48.936,52.475,49.159,52.486,49.344,52.469,49.478,52.331,49.628,52.366,49.812,52.193,49.977,52.184,50.221,52.13,50.236,52.035,50.517,51.991,50.732,51.914,50.716,51.841,50.794,51.729]},{"z":4.6,"b":[93.018,24.399,93.449,25.302],"l":[93.449,25.302,93.394,25.235,93.189,24.811,93.124,24.81,93.087,24.707,93.046,24.445,93.018,24.399]},{"z":4.6,"b":[92.298,24.133,93.018,24.471],"l":[93.018,24.399,92.829,24.377,92.746,24.471,92.701,24.362,92.631,24.295,92.624,24.239,92.489,24.133,92.416,24.239,92.298,24.238]},{"z":4.7,"b":[29.418,59.61,30.756,60.266],"l":[29.697,59.95,29.68,59.869,29.85,59.828,30.113,59.663,30.259,59.61,30.502,59.631,30.556,59.686,30.748,59.726,30.756,59.777,30.512,59.862,30.527,59.97,30.374,60.036,30.39,60.123,30.179,60.148,29.633,60.266,29.418,60.169]},{"z":4.6,"b":[93.336,25.302,95.176,27.033],"l":[95.176,27.033,95.031,26.933,94.917,26.951,94.799,26.814,94.69,26.741,94.487,26.677,94.358,26.498,94.207,26.484,94.177,26.372,94.008,26.184,93.973,26.04,93.989,25.946,93.904,25.857,93.752,25.959,93.709,25.87,93.581,25.763,93.524,25.689,93.336,25.538,93.459,25.381,93.449,25.302]},{"z":4.7,"b":[34.73,59.103,35.686,61.188],"l":[35.604,61.188,35.686,61.121,35.489,60.959,35.44,60.862,35.281,60.85,35.154,60.787,35.259,60.585,35.231,60.341,35.147,60.269,35.157,60.067,35.238,59.985,35.404,59.974,35.382,59.875,35.424,59.701,35.497,59.647,35.496,59.554,35.324,59.507,35.449,59.428,35.301,59.301,35.135,59.254,34.901,59.238,34.73,59.103]},{"z":4.7,"b":[29.975,58.445,34.73,59.422],"l":[34.73,59.103,34.568,59.108,34.247,59.198,34.1,59.145,33.855,59.198,33.716,59.302,33.52,59.373,33.368,59.38,33.246,59.326,32.955,59.422,32.732,59.267,32.706,59.142,32.584,59.181,32.291,59.207,32.124,59.394,31.882,59.375,31.827,59.314,31.563,59.368,31.452,59.092,31.258,58.981,31.16,59.073,30.981,59.06,30.962,58.975,30.728,58.829,30.687,58.746,30.413,58.711,30.28,58.754,30.101,58.73,30.088,58.64,30.167,58.533,29.975,58.445]},{"z":4.7,"b":[35.604,61.188,37.773,61.594],"l":[37.773,61.522,37.278,61.501,36.996,61.594,36.873,61.545,36.578,61.529,36.467,61.407,35.604,61.188]},{"z":4.7,"b":[29.277,60.646,35.604,61.32],"l":[35.604,61.188,35.392,61.162,35.258,61.254,34.822,61.278,34.741,61.32,34.544,61.27,34.457,61.168,34.216,61.242,33.535,61.221,33.452,61.134,33.585,61.071,33.696,61.122,33.867,61.071,33.752,60.944,33.406,60.989,33.423,60.936,33.253,60.899,33.078,60.738,32.91,60.724,32.703,60.646,29.87,61.176,29.576,61.171,29.491,61.239,29.277,61.305]},{"z":4.7,"b":[29.362,66.356,33.05,66.684],"l":[33.05,66.576,32.594,66.582,32.531,66.489,32.363,66.467,32.329,66.359,31.902,66.356,31.785,66.503,31.652,66.537,31.555,66.655,31.073,66.684,29.362,66.627]},{"z":4.7,"b":[33.046,66.574,33.184,66.576],"l":[33.184,66.574,33.046,66.576]},{"z":4.7,"b":[35.882,61.522,37.927,63.971],"l":[36.523,63.971,36.555,63.764,36.375,63.645,35.945,63.62,35.882,63.573,36.058,63.46,36.22,63.416,36.262,63.236,36.32,63.161,36.259,63.032,36.501,62.826,36.76,62.718,37.081,62.656,37.352,62.684,37.505,62.645,37.588,62.473,37.573,62.361,37.43,62.237,37.719,62.203,37.808,61.998,37.927,61.968,37.857,61.698,37.762,61.648,37.773,61.522]},{"z":4.6,"b":[103.596,24.742,105.297,27.722],"l":[105.297,27.722,105.197,27.399,105.083,27.413,104.871,27.31,104.783,27.332,104.594,27.314,104.444,27.347,104.344,27.441,104.174,27.272,103.981,27.4,103.911,27.39,103.859,27.315,103.695,27.152,103.596,27.076,103.701,27.052,103.778,26.956,103.778,26.88,103.717,26.799,103.775,26.732,103.758,26.629,103.829,26.547,104.02,26.514,104.153,26.651,104.218,26.627,104.359,26.648,104.43,26.713,104.466,26.613,104.559,26.585,104.627,26.498,104.676,26.379,104.545,26.27,104.505,26.147,104.499,26.031,104.393,25.948,104.421,25.857,104.306,25.665,104.422,25.579,104.433,25.507,104.554,25.485,104.566,25.381,104.669,25.297,104.72,25.211,104.689,25.101,104.707,25.017,104.546,24.813,104.54,24.742]},{"z":4.6,"b":[104.495,23.152,106.194,24.742],"l":[104.54,24.742,104.495,24.698,104.502,24.578,104.599,24.403,104.667,24.34,104.729,24.364,104.753,24.468,104.961,24.416,105.061,24.431,105.176,24.31,105.17,24.167,105.253,24.086,105.32,24.118,105.447,24.038,105.532,24.126,105.608,24.14,105.641,24.082,105.788,24.021,105.872,24.025,105.971,24.088,106.157,23.974,106.194,23.869,106.134,23.797,106.152,23.75,106.128,23.541,105.983,23.469,105.848,23.519,105.702,23.359,105.629,23.347,105.619,23.285,105.512,23.152]},{"z":4.7,"b":[45.706,44.752,46.888,45.001],"l":[45.706,44.976,45.854,45.001,46.434,44.937,46.603,44.885,46.888,44.752]},{"z":4.6,"b":[99.099,26.035,105.297,29.224],"l":[99.109,29.224,99.121,28.965,99.099,28.856,99.118,28.709,99.169,28.602,99.163,28.438,99.275,28.277,99.382,28.185,99.416,28.255,99.389,28.334,99.393,28.543,99.503,28.579,99.534,28.681,99.629,28.815,99.705,28.845,99.732,28.745,99.828,28.673,99.846,28.604,99.934,28.569,100.001,28.456,100.143,28.342,100.162,28.236,100.047,28.187,100.059,28.066,100.202,27.899,100.296,27.853,100.349,27.747,100.41,27.829,100.533,27.816,100.625,27.897,100.661,27.875,100.872,27.562,100.867,27.495,100.941,27.435,100.992,27.331,101.004,27.198,101.173,27.191,101.141,27.045,101.342,26.871,101.371,26.775,101.503,26.766,101.495,26.706,101.412,26.57,101.544,26.495,101.644,26.349,101.58,26.307,101.595,26.209,101.677,26.228,101.764,26.176,101.776,26.107,101.85,26.035,101.922,26.099,102.113,26.084,102.252,26.229,102.358,26.263,102.401,26.312,102.568,26.379,102.626,26.345,102.61,26.26,102.72,26.238,102.878,26.365,102.951,26.345,103.003,26.4,102.996,26.485,103.058,26.557,102.975,26.831,102.901,26.925,102.878,27.032,102.914,27.112,102.889,27.291,102.977,27.389,103.121,27.412,103.218,27.567,103.415,27.743,103.509,27.865,103.514,27.971,103.435,28.068,103.459,28.135,103.572,28.243,103.666,28.263,103.709,28.216,103.88,28.326,103.795,28.531,103.837,28.605,104.057,28.63,104.414,28.64,104.44,28.605,104.25,28.531,104.268,28.334,104.377,28.282,104.445,28.1,104.334,28.047,104.375,27.962,104.482,27.891,104.605,27.846,104.702,27.883,104.897,27.901,104.958,27.957,104.966,28.048,105.056,28.088,105.172,28.062,105.203,28.002,105.282,28.001,105.228,27.856,105.297,27.722]},{"z":4.7,"b":[45.509,42.474,46.586,44.015],"l":[45.509,44.015,45.71,43.955,45.803,43.978,46.045,43.934,46.016,43.876,46.22,43.863,46.257,43.948,46.349,43.998,46.462,43.962,46.452,43.887,46.586,43.872,46.585,43.772,46.483,43.672,46.439,43.541,46.338,43.458,46.36,43.361,46.423,43.315,46.403,43.114,46.534,42.961,46.425,42.877,46.336,42.904,46.123,42.843,46.17,42.73,46.095,42.715,45.948,42.575,45.821,42.487,45.727,42.474]},{"z":4.7,"b":[45.124,44.015,45.706,44.976],"l":[45.509,44.015,45.506,44.19,45.124,44.202,45.131,44.268,45.404,44.318,45.444,44.402,45.398,44.444,45.237,44.46,45.271,44.591,45.435,44.617,45.614,44.766,45.621,44.899,45.706,44.976]},{"z":4.7,"b":[33.501,53.311,35.104,54.026],"l":[33.501,54.016,33.682,54.026,33.78,53.982,33.86,53.879,34.031,53.862,34.199,53.892,34.385,53.719,34.49,53.666,34.488,53.544,34.582,53.428,34.673,53.39,35.017,53.381,35.104,53.311]},{"z":4.7,"b":[34.805,52.353,35.314,53.311],"l":[35.104,53.311,35.122,53.186,35.182,53.102,35.314,53.066,35.157,52.969,34.967,52.97,34.805,52.868,34.869,52.784,34.948,52.783,35.092,52.698,35.011,52.614,34.99,52.511,34.833,52.424,34.897,52.353]},{"z":4.7,"b":[33.501,54.016,35.37,55.239],"l":[35.37,55.239,35.284,55.112,35.144,55.022,35.002,54.887,34.866,54.884,34.921,54.772,34.817,54.709,34.5,54.632,34.416,54.451,34.326,54.43,34.108,54.55,33.927,54.572,33.729,54.562,33.643,54.485,33.617,54.381,33.684,54.321,33.562,54.156,33.501,54.016]},{"z":4.7,"b":[32.706,53.42,33.501,54.016],"l":[33.501,54.016,33.475,53.972,33.287,53.954,33.289,53.822,32.899,53.569,32.908,53.493,32.706,53.42]},{"z":4.7,"b":[31.467,55.632,35.174,56.075],"l":[31.467,55.729,31.586,55.747,31.755,55.714,31.894,55.77,32.094,55.674,32.722,55.69,32.939,55.632,33.188,55.653,33.225,55.698,33.372,55.703,33.505,55.755,33.67,55.865,33.66,55.946,33.838,55.949,33.917,56.073,34.201,56.075,34.631,55.918,35.031,55.984,35.174,55.948]},{"z":4.7,"b":[35.174,55.239,35.397,55.948],"l":[35.174,55.948,35.354,55.779,35.316,55.727,35.397,55.617,35.303,55.571,35.301,55.299,35.37,55.239]},{"z":4.6,"b":[77.585,28.737,80.073,30.395],"l":[80.073,28.821,80.001,28.737,79.844,28.805,79.841,28.855,79.743,28.885,79.699,28.85,79.526,28.878,79.444,28.85,79.371,28.97,79.296,28.972,79.171,29.051,79.056,29.152,78.95,29.127,78.868,29.215,78.722,29.284,78.907,29.388,78.643,29.538,78.479,29.722,78.381,29.762,78.228,29.947,78.155,29.925,78.189,29.707,78.107,29.655,77.983,29.719,77.831,29.66,77.784,29.742,77.747,29.878,77.746,29.977,77.835,30.131,77.929,30.244,77.751,30.326,77.662,30.395,77.585,30.383]},{"z":4.6,"b":[77.585,30.383,78.946,31.29],"l":[77.585,30.383,77.813,30.521,77.706,30.791,77.789,30.931,77.806,31.051,77.941,31.173,78.04,31.172,78.284,31.29,78.369,31.29,78.501,31.22,78.582,31.231,78.793,31.207,78.87,31.122,78.946,31.138]},{"z":4.6,"b":[87.768,25.214,88.297,26.539],"l":[87.784,25.214,87.838,25.278,87.768,25.436,87.956,25.534,88.043,25.547,88.046,25.669,87.911,25.765,87.805,25.874,87.831,26.011,87.979,26.146,88.08,26.193,88.18,26.286,88.297,26.336,88.282,26.428,88.195,26.501,88.087,26.539]},{"z":4.6,"b":[110.899,-2.909,113.776,0.559],"l":[113.776,0.559,113.647,0.41,113.623,0.311,113.532,0.261,113.416,0.275,113.325,0.245,113.453,0.066,113.479,-0.048,113.429,-0.19,113.326,-0.252,113.249,-0.361,113.301,-0.423,113.256,-0.551,113.161,-0.47,113.084,-0.518,113.004,-0.519,112.755,-0.636,112.578,-0.704,112.497,-0.702,112.38,-0.755,112.119,-0.735,112.059,-0.796,111.969,-0.823,111.651,-1.015,111.587,-1.102,111.577,-1.187,111.416,-1.269,111.327,-1.373,111.242,-1.421,111.134,-1.53,111.012,-1.526,110.92,-1.613,111.017,-1.66,111.025,-1.867,110.995,-1.985,111.012,-2.127,111.138,-2.405,111.138,-2.733,111.067,-2.765,110.899,-2.909]},{"z":4.6,"b":[86.687,21.614,87.477,22.218],"l":[87.477,21.614,87.409,21.786,87.275,21.814,87.232,21.848,87.191,21.956,87.113,21.971,87.059,21.907,86.994,21.972,86.987,22.067,86.834,22.122,86.687,22.218]},{"z":4.6,"b":[85.829,22.218,87.895,25.214],"l":[86.687,22.218,86.78,22.287,86.79,22.416,86.718,22.469,86.749,22.545,86.611,22.604,86.587,22.681,86.399,22.785,86.415,22.943,86.45,23.004,86.142,23.028,86.02,23.153,85.888,23.163,85.83,23.261,85.895,23.416,85.829,23.483,85.996,23.512,86.056,23.603,86.135,23.559,86.121,23.501,86.214,23.449,86.336,23.508,86.348,23.568,86.459,23.639,86.646,23.697,86.8,23.726,86.81,23.818,86.877,23.913,87.08,23.815,87.166,23.81,87.241,23.851,87.302,24.057,87.338,24.002,87.489,24.047,87.487,24.124,87.572,24.118,87.662,24.186,87.631,24.269,87.721,24.318,87.8,24.501,87.791,24.562,87.877,24.589,87.895,24.67,87.853,24.758,87.877,24.939,87.844,25.042,87.781,25.09,87.784,25.214]},{"z":4.6,"b":[89.658,26.187,89.888,26.722],"l":[89.658,26.187,89.727,26.206,89.782,26.309,89.829,26.328,89.888,26.533,89.859,26.722]},{"z":4.6,"b":[88.001,27.085,88.756,27.248],"l":[88.756,27.149,88.664,27.173,88.518,27.176,88.448,27.114,88.357,27.085,88.233,27.137,88.118,27.138,88.001,27.248]},{"z":4.6,"b":[92.139,24.238,92.298,24.545],"l":[92.298,24.238,92.22,24.264,92.258,24.396,92.222,24.5,92.139,24.545]},{"z":4.6,"b":[89.809,25.005,92.806,26.089],"l":[92.385,25.005,92.565,25.152,92.806,25.228,92.79,25.349,92.588,25.499,92.593,25.587,92.382,25.764,92.167,25.69,92.165,25.809,92.215,25.919,92.184,25.993,92.261,26.055,92.164,26.088,91.922,26.038,91.825,26.089,91.752,26.048,91.718,25.968,91.582,26.027,91.475,25.896,91.361,25.843,91.251,25.738,91.208,25.736,91.179,25.87,91.097,25.84,90.975,25.944,90.753,25.962,90.644,25.931,90.618,25.97,90.485,26.012,90.136,25.96,89.974,25.802,89.942,25.691,90.002,25.587,89.897,25.541,89.875,25.46,89.809,25.441]},{"z":4.7,"b":[98.634,52.105,99.25,52.954],"l":[98.94,52.105,98.932,52.192,98.809,52.196,98.799,52.274,98.646,52.309,98.634,52.413,98.806,52.514,98.805,52.611,98.937,52.673,98.938,52.798,98.856,52.847,98.929,52.918,99.22,52.865,99.25,52.954]},{"z":4.7,"b":[99.25,51.132,116.17,57.24],"l":[99.25,52.954,99.366,53.054,99.379,53.113,99.536,53.105,99.69,53.132,99.805,53.19,99.878,53.299,100.08,53.3,100.26,53.375,100.401,53.234,100.363,53.137,100.606,52.944,100.827,52.856,100.852,52.811,101.031,52.769,101.32,52.641,101.436,52.631,101.476,52.571,101.688,52.461,101.743,52.402,101.91,52.317,101.963,52.222,102.046,52.212,102.188,52.276,102.32,52.231,102.395,52.259,102.542,52.228,102.574,52.149,102.673,52.098,102.777,51.985,102.945,51.907,103.065,51.883,103.222,51.788,103.181,51.621,103.335,51.434,103.576,51.479,103.69,51.46,103.819,51.388,103.814,51.132,104.098,51.218,104.212,51.281,104.494,51.309,104.609,51.268,104.67,51.399,104.629,51.443,104.65,51.622,104.899,51.669,105.123,51.731,105.461,51.863,105.752,52.031,105.992,52.221,106.198,52.44,108.041,53.193,108.061,53.205,108.423,53.783,109.007,54.425,109.022,54.494,108.671,54.547,108.64,54.601,108.716,54.785,108.69,55.062,108.782,55.17,108.773,55.237,108.651,55.313,108.622,55.456,108.771,55.623,108.854,55.866,108.96,55.941,109.18,55.913,109.105,56.022,108.991,56.04,108.929,56.148,108.799,56.257,108.571,56.294,108.557,56.346,108.719,56.499,108.699,56.583,108.93,56.588,109.143,56.676,109.314,56.672,109.41,56.735,109.665,56.694,109.78,56.711,109.887,56.891,109.989,56.969,110.248,56.925,110.399,56.921,110.532,56.847,110.662,56.833,110.753,56.874,110.883,56.834,110.998,56.858,111.134,56.815,111.363,56.895,111.337,56.95,111.462,57.102,111.62,57.095,111.628,57.016,111.854,56.913,111.916,56.945,112.11,56.944,112.235,56.878,112.444,56.863,112.662,56.96,112.762,56.753,112.886,56.697,113.356,56.626,113.547,56.672,113.803,56.602,113.87,56.533,114.01,56.545,114.127,56.676,114.289,56.692,114.498,56.671,114.795,56.71,114.851,56.837,115.05,56.991,115.086,57.119,115.45,57.097,115.698,57.137,115.852,57.24,116.156,57.229,116.17,57.141,116.048,57.082,115.805,57.031,115.711,56.964]},{"z":4.7,"b":[41.656,45.996,44.334,47.575],"l":[43.732,47.477,43.799,47.371,43.802,47.283,43.939,47.269,43.953,47.365,44.11,47.414,44.117,47.533,44.228,47.575,44.334,47.475,44.328,47.111,44.262,46.9,43.93,46.723,43.817,46.589,43.928,46.521,43.918,46.416,43.786,46.407,43.646,46.216,43.559,46.169,43.356,46.172,43.08,46.36,42.912,46.435,42.787,46.389,42.597,46.411,42.409,46.525,42.181,46.578,42.034,46.543,42.1,46.407,42.202,46.314,41.995,46.301,41.915,46.185,41.84,46.153,41.726,46.214,41.656,46.105,41.713,45.996]},{"z":4.7,"b":[41.162,45.947,41.713,45.996],"l":[41.713,45.996,41.658,45.976,41.42,45.995,41.375,45.947,41.162,45.954]},{"z":4.7,"b":[40.081,49.576,41.362,50.226],"l":[41.362,50.226,41.288,50.179,41.341,50.075,41.179,50.016,41.056,49.916,41.029,49.794,40.885,49.69,40.612,49.648,40.411,49.597,40.17,49.624,40.081,49.576]},{"z":4.7,"b":[41.362,47.45,43.732,50.226],"l":[41.362,50.226,41.492,50.084,41.595,50.045,41.617,49.962,41.876,49.9,42.115,49.75,42.178,49.668,42.072,49.502,42.053,49.402,42.092,49.289,42.037,49.267,42.095,49.141,42.344,49.152,42.411,49.047,42.644,48.977,42.74,48.786,42.651,48.705,42.701,48.624,42.634,48.552,42.442,48.505,42.277,48.498,42.165,48.464,42.074,48.392,42.147,48.287,42.051,48.23,42.096,48.154,42.03,48.029,42.283,48.016,42.343,48.045,42.492,48.014,42.728,47.854,42.785,47.731,42.955,47.51,43.054,47.573,43.263,47.45,43.482,47.484,43.732,47.477]},{"z":4.7,"b":[42.489,51.231,42.873,51.608],"l":[42.489,51.608,42.734,51.479,42.861,51.381,42.873,51.231]},{"z":4.7,"b":[41.168,50.226,42.873,51.239],"l":[42.873,51.231,42.715,51.239,42.573,51.169,42.255,51.132,42.067,51.16,41.977,51.203,41.855,51.189,41.737,51.026,41.444,50.917,41.37,50.831,41.168,50.781,41.314,50.725,41.4,50.625,41.526,50.599,41.424,50.482,41.509,50.392,41.362,50.226]},{"z":4.7,"b":[38.299,51.92,40.504,52.098],"l":[38.299,51.961,38.529,52.024,38.698,52.098,38.762,52.052,38.88,52.093,38.994,52.001,39.182,52.098,39.447,52.082,39.497,51.973,39.834,51.92,39.913,51.98,40.088,51.985,40.295,51.956,40.504,51.954]},{"z":4.7,"b":[40.504,51.6,42.489,51.954],"l":[40.504,51.954,40.735,51.94,40.938,51.89,40.892,51.743,41.066,51.71,41.268,51.735,41.311,51.794,41.556,51.767,41.952,51.656,42.248,51.694,42.274,51.6,42.403,51.633,42.489,51.608]},{"z":4.7,"b":[35.313,50.906,38.292,51.42],"l":[38.292,51.292,38.027,51.366,38.046,51.42,37.829,51.42,37.667,51.366,37.539,51.394,37.379,51.385,37.249,51.331,37.144,51.343,37.054,51.247,36.93,51.181,36.856,51.186,36.618,51.08,36.314,51.103,35.976,51.158,36,51.058,35.877,51.012,35.817,50.945,35.546,50.906,35.512,50.947,35.313,50.959]},{"z":4.7,"b":[38.292,49.856,39.234,51.292],"l":[38.292,51.292,38.446,51.257,38.501,51.189,38.396,51.157,38.397,51.077,38.516,51.062,38.618,50.998,38.738,51.074,38.846,51.036,38.874,50.911,38.737,50.901,38.724,50.79,38.966,50.647,39.072,50.636,39.098,50.506,38.99,50.433,39.085,50.349,39.099,50.173,39.234,50.056,39.175,49.856]},{"z":4.7,"b":[37.747,51.879,38.299,52.017],"l":[37.747,52.017,37.821,51.912,37.997,51.879,38.101,51.927,38.192,51.912,38.299,51.961]},{"z":4.7,"b":[38.207,51.292,38.41,51.961],"l":[38.299,51.961,38.41,51.92,38.41,51.688,38.22,51.708,38.207,51.629,38.276,51.526,38.323,51.341,38.292,51.292]},{"z":4.7,"b":[34.372,51.799,34.917,52.353],"l":[34.897,52.353,34.917,52.322,34.805,52.185,34.715,52.216,34.672,52.065,34.531,52.07,34.407,52.013,34.372,51.799]},{"z":4.7,"b":[34.897,51.936,37.747,52.414],"l":[34.897,52.353,35.124,52.335,35.241,52.39,35.396,52.414,35.572,52.387,35.637,52.332,35.893,52.274,36.001,52.313,36.146,52.319,36.357,52.366,36.552,52.215,36.729,52.176,36.82,52.12,37.158,52.072,37.354,51.936,37.481,52.009,37.652,51.971,37.747,52.017]},{"z":4.7,"b":[130.41,49.244,134.956,55.704],"l":[130.923,55.679,131.087,55.618,131.301,55.618,131.599,55.652,131.804,55.614,131.962,55.658,132.126,55.645,132.24,55.704,132.538,55.676,132.663,55.561,132.644,55.491,132.908,55.359,132.733,55.349,132.581,55.196,132.355,55.191,132.367,55.056,132.087,55.05,131.971,54.998,131.994,54.909,131.612,54.781,131.532,54.722,131.399,54.719,131.199,54.613,131.22,54.527,131.114,54.323,130.944,54.323,130.774,54.288,130.507,54.12,130.484,53.993,130.41,53.948,130.447,53.882,130.635,53.887,130.84,53.828,130.921,53.768,131.097,53.813,131.421,53.751,131.444,53.627,131.498,53.555,131.483,53.476,131.532,53.319,131.465,53.213,131.8,53.241,131.894,53.128,131.991,53.134,132.112,53.223,132.398,53.244,132.594,53.23,132.893,53.235,132.951,53.289,133.153,53.291,133.21,53.421,133.573,53.546,133.709,53.477,134.011,53.433,134.215,53.472,134.223,53.515,134.451,53.538,134.555,53.621,134.748,53.539,134.761,53.475,134.924,53.417,134.897,53.315,134.956,53.254,134.859,53.184,134.812,53.066,134.659,52.939,134.631,52.801,134.706,52.766,134.782,52.647,134.659,52.575,134.663,52.471,134.606,52.418,134.224,52.489,134.056,52.5,134.003,52.539,133.659,52.572,133.369,52.683,133.28,52.664,133.238,52.567,133.246,52.456,133.42,52.251,133.322,52.198,132.907,52.159,132.59,52.082,132.525,51.963,132.404,51.963,132.423,51.86,132.279,51.793,132.216,51.817,131.959,51.737,131.864,51.755,131.728,51.681,131.431,51.688,131.395,51.607,131.484,51.476,131.488,51.345,131.29,51.364,131.169,51.249,131.065,51.246,130.919,51.024,130.816,51.02,130.772,50.942,130.841,50.881,130.688,50.746,130.645,50.657,130.954,50.65,131.011,50.568,130.866,50.457,130.989,50.382,131.134,50.419,131.158,50.345,131.29,50.366,131.325,50.269,131.442,50.186,131.295,49.99,131.445,49.964,131.499,49.901,131.469,49.73,131.384,49.659,131.5,49.611,131.494,49.411,131.396,49.244]},{"z":4.7,"b":[130.554,48.86,131.396,49.25],"l":[131.396,49.244,131.182,49.25,131.007,49.051,130.983,48.972,130.829,48.983,130.554,48.86]},{"z":4.7,"b":[119.898,55.466,130.923,57.032],"l":[119.898,56.889,120.031,56.899,120.303,57.004,120.656,57.032,121.019,57.031,121.053,56.99,121.407,56.976,121.511,56.922,121.602,56.813,121.608,56.741,122.16,56.738,122.283,56.651,122.467,56.602,122.589,56.49,122.781,56.498,122.919,56.551,123.132,56.457,123.453,56.406,123.915,56.377,124.06,56.231,124.206,56.165,124.243,56.081,124.36,56.057,124.448,55.905,124.635,55.902,124.958,55.826,125.248,55.88,125.437,55.837,125.546,55.736,125.758,55.796,126.039,55.728,126.047,55.589,126.234,55.591,126.434,55.555,126.551,55.633,126.843,55.606,126.954,55.687,127.188,55.691,127.33,55.656,127.487,55.555,127.573,55.58,127.675,55.683,127.794,55.629,128.29,55.648,128.502,55.516,128.663,55.521,128.772,55.469,128.975,55.466,129.089,55.533,129.047,55.654,129.444,55.7,129.653,55.745,129.892,55.734,129.995,55.693,130.742,55.719,130.923,55.679]},{"z":4.7,"b":[123.162,43.436,131.253,46.272],"l":[131.253,43.469,131.149,43.436,130.995,43.508,130.917,43.45,130.589,43.628,130.414,43.669,130.333,43.928,130.28,43.964,130.104,43.845,130.033,43.856,130,43.984,129.913,44.022,129.802,43.965,129.752,43.874,129.535,43.87,129.249,43.786,129.216,43.738,129.229,43.608,129.015,43.54,128.883,43.556,128.764,43.709,128.747,43.802,128.632,43.912,128.529,44.08,128.446,44.154,128.451,44.316,128.419,44.45,128.359,44.502,128.207,44.44,128.155,44.353,128.043,44.341,128.052,44.126,127.85,44.056,127.744,44.086,127.715,44.161,127.61,44.22,127.618,44.272,127.493,44.411,127.559,44.574,127.375,44.649,127.233,44.616,127.147,44.655,127.045,44.598,127.032,44.731,126.978,44.826,127.077,44.934,126.953,45.134,126.789,45.149,126.559,45.247,126.421,45.228,126.15,45.141,125.841,45.235,125.705,45.36,125.699,45.511,125.521,45.471,125.432,45.477,125.307,45.417,125.104,45.395,125.028,45.498,124.872,45.449,124.612,45.448,124.563,45.415,124.396,45.451,124.332,45.547,124.224,45.636,124.141,45.632,124.018,45.782,124.062,45.872,123.979,45.978,124.034,46.018,123.919,46.272,123.356,46.235,123.258,46.265,123.162,46.22]},{"z":4.7,"b":[119.67,53.43,122.127,56.889],"l":[119.898,56.889,119.67,56.771,119.787,56.7,119.809,56.612,119.96,56.493,120.257,56.459,120.276,56.386,120.533,56.285,120.527,56.232,120.344,56.22,120.062,56.138,120.055,56.078,120.26,55.916,120.466,55.872,120.532,55.914,120.986,56.02,121.217,56.013,121.276,55.867,121.347,55.813,121.307,55.742,121.363,55.507,121.439,55.481,121.634,55.535,121.693,55.6,121.883,55.606,121.984,55.476,121.939,55.214,121.95,55.007,121.667,54.821,121.659,54.728,121.826,54.784,121.922,54.733,121.895,54.646,121.936,54.566,122.127,54.468,122.115,54.407,121.902,54.369,121.866,54.419,121.731,54.426,121.655,54.355,121.753,54.272,121.754,54.152,121.641,54.113,121.643,54.012,121.783,53.973,121.919,53.831,121.947,53.605,122.018,53.521,122.025,53.43]},{"z":4.7,"b":[115.711,56.726,119.108,58.398],"l":[115.711,56.964,115.86,56.932,115.957,56.865,116.122,56.813,116.362,56.847,116.629,56.749,116.815,56.726,117.012,56.806,117.141,56.809,117.18,56.866,117.571,56.851,117.576,56.935,117.673,56.978,117.677,57.066,117.601,57.15,117.723,57.187,117.794,57.287,117.597,57.321,117.427,57.264,117.357,57.337,117.321,57.457,117.257,57.509,117.282,57.617,117.14,57.668,117.173,57.736,117.128,57.842,117.313,57.847,117.368,57.934,117.375,58.061,117.438,58.152,117.588,58.184,117.471,58.304,117.684,58.398,118.142,58.386,118.317,58.353,118.405,58.262,118.63,58.175,118.784,58.213,119.108,58.223]},{"z":4.7,"b":[119.047,56.889,119.898,58.223],"l":[119.108,58.223,119.154,58.146,119.135,58.007,119.047,57.728,119.165,57.669,119.141,57.539,119.41,57.615,119.487,57.591,119.41,57.497,119.442,57.371,119.546,57.34,119.683,57.157,119.622,57.101,119.645,57.002,119.772,56.983,119.898,56.889]},{"z":4.7,"b":[90.477,51.781,96.684,53.639],"l":[90.477,52.183,90.582,52.112,90.744,52.063,90.839,51.912,91.101,51.943,91.18,51.89,91.543,51.901,91.616,51.869,91.894,51.853,91.987,51.794,92.43,51.802,92.505,51.781,92.673,51.84,92.844,51.832,92.901,51.895,93.075,51.945,93.36,52.129,93.678,52.224,93.768,52.371,93.774,52.446,93.989,52.585,94.139,52.761,94.141,52.832,94.29,52.9,94.512,52.911,94.604,53.055,94.604,53.176,94.69,53.224,94.594,53.316,94.74,53.383,94.976,53.405,95.041,53.376,95.26,53.456,95.381,53.372,95.622,53.449,95.768,53.52,96.048,53.454,96.186,53.515,96.241,53.613,96.418,53.543,96.642,53.548,96.684,53.639]},{"z":4.7,"b":[96.684,52.954,99.25,53.729],"l":[96.684,53.639,96.743,53.711,96.875,53.729,97.135,53.613,97.328,53.596,97.448,53.461,97.586,53.438,97.699,53.379,97.972,53.352,98.031,53.253,98.28,53.226,98.303,53.104,98.615,53.108,98.698,53.155,99.056,53.104,98.989,53.034,99.25,52.954]},{"z":4.7,"b":[107.745,49.967,116.927,56.964],"l":[115.711,56.964,115.589,56.902,115.571,56.732,115.434,56.69,115.49,56.62,115.688,56.595,115.673,56.54,115.801,56.466,115.684,56.425,115.688,56.327,115.774,56.206,115.765,56.086,115.889,55.965,115.844,55.857,115.845,55.705,115.892,55.641,115.895,55.525,116.072,55.415,116.178,55.391,116.376,55.409,116.458,55.34,116.476,55.253,116.716,55.145,116.774,55.148,116.893,54.977,116.927,54.826,116.884,54.738,116.774,54.69,116.777,54.627,116.673,54.548,116.458,54.517,116.19,54.538,115.934,54.504,115.629,54.442,115.526,54.404,115.491,54.329,115.269,54.257,115.164,54.162,115.023,54.098,114.966,54.023,114.86,53.981,114.588,53.927,114.501,53.845,114.378,53.844,114.326,53.79,113.983,53.744,113.927,53.681,113.793,53.63,113.878,53.562,113.842,53.382,113.936,53.358,113.978,53.24,114.201,53.187,114.132,53.1,114.325,53.068,114.233,52.961,114.235,52.822,114.088,52.695,113.965,52.633,113.744,52.574,113.573,52.453,113.236,52.469,112.95,52.343,112.68,52.331,112.545,52.347,112.405,52.208,112.185,52.288,112.062,52.277,111.796,52.09,111.724,52.006,111.586,51.922,111.454,51.878,111.125,51.818,110.845,51.66,110.862,51.595,110.677,51.504,110.423,51.523,110.389,51.577,110.049,51.616,109.629,51.449,109.247,51.366,109.047,51.5,108.942,51.44,108.757,51.449,108.649,51.501,108.532,51.423,108.385,51.237,108.555,51.157,108.484,51.057,108.353,51.055,108.308,51.011,108.25,50.844,108.067,50.753,108.187,50.658,108.472,50.59,108.554,50.465,108.315,50.403,108.213,50.411,108.132,50.318,107.99,50.335,107.926,50.215,107.77,50.147,107.796,50.026,107.745,49.967]},{"z":4.7,"b":[88.736,51.553,90.477,52.192],"l":[88.736,51.559,89.033,51.6,89.233,51.645,89.223,51.56,89.421,51.605,89.581,51.553,89.73,51.582,89.943,51.75,90.105,51.768,90.028,51.984,90.119,52.044,90.124,52.129,90.359,52.192,90.477,52.183]},{"z":4.6,"b":[80.323,35.246,89.678,36.449],"l":[89.678,36.082,89.224,36.264,88.958,36.287,88.828,36.374,88.712,36.368,88.522,36.414,88.459,36.449,88,36.428,87.925,36.383,87.571,36.334,87.346,36.374,87.194,36.304,87.001,36.274,86.787,36.26,86.714,36.231,86.459,36.219,86.4,36.17,86.212,36.116,86.087,35.876,85.93,35.762,85.717,35.759,85.62,35.675,85.389,35.757,85.26,35.73,85.15,35.75,84.924,35.689,84.778,35.6,84.563,35.559,84.215,35.389,84.133,35.378,83.984,35.416,83.871,35.375,83.565,35.361,83.453,35.394,83.246,35.421,83.099,35.413,82.972,35.49,82.978,35.601,82.936,35.674,82.785,35.685,82.71,35.65,82.428,35.69,82.319,35.634,82.289,35.545,82.086,35.448,82.002,35.319,81.711,35.246,81.545,35.252,81.462,35.31,81.357,35.332,81.21,35.317,81.061,35.389,80.979,35.293,80.858,35.347,80.766,35.343,80.656,35.384,80.486,35.404,80.323,35.507]},{"z":4.6,"b":[89.435,31.548,97.716,36.082],"l":[89.678,36.082,89.515,36.053,89.435,35.988,89.485,35.88,89.581,35.84,89.789,35.827,89.789,35.776,89.715,35.642,89.69,35.509,89.73,35.444,89.484,35.342,89.455,35.213,89.572,35.073,89.584,34.941,89.788,34.926,89.825,34.846,89.724,34.74,89.791,34.554,89.796,34.408,89.84,34.383,89.787,34.199,89.637,34.093,89.742,33.91,89.877,33.823,89.975,33.631,90.141,33.578,90.203,33.5,90.206,33.402,90.323,33.289,90.436,33.282,90.66,33.156,90.804,33.141,90.95,33.239,91.128,33.254,91.189,33.337,91.362,33.337,91.376,33.271,91.543,33.08,91.72,32.983,91.813,32.968,91.977,32.849,92.086,32.886,92.196,32.869,92.221,32.745,92.466,32.769,92.65,32.74,92.727,32.761,92.85,32.729,93.006,32.735,93.031,32.664,93.219,32.66,93.345,32.578,93.443,32.563,93.51,32.516,93.597,32.565,93.734,32.572,93.877,32.495,94.111,32.478,94.178,32.522,94.317,32.541,94.582,32.672,94.722,32.593,94.755,32.535,94.852,32.5,94.939,32.428,94.951,32.337,95.062,32.261,95.259,32.244,95.382,32.17,95.425,32.095,95.354,31.954,95.407,31.811,95.497,31.742,95.593,31.762,95.725,31.746,95.774,31.698,95.849,31.714,96.094,31.701,96.167,31.548,96.215,31.607,96.213,31.735,96.148,31.78,96.183,31.871,96.278,31.909,96.483,31.753,96.664,31.721,96.737,31.68,96.795,31.715,96.753,31.839,96.786,31.91,96.754,31.974,96.915,31.99,97.018,32.023,97.124,32.009,97.255,32.076,97.254,32.204,97.36,32.26,97.393,32.385,97.326,32.424,97.361,32.5,97.562,32.484,97.631,32.444,97.716,32.544]},{"z":4.6,"b":[92.772,39.153,96.366,42.723],"l":[96.366,42.723,96.091,42.587,96.044,42.5,96.018,42.139,96.036,41.995,95.864,41.855,95.772,41.83,95.492,41.856,95.058,41.789,94.581,41.585,94.011,41.104,93.76,40.823,93.669,40.683,93.577,40.588,93.286,40.469,93.037,40.49,92.926,40.423,92.92,40.118,92.777,39.906,92.772,39.811,92.948,39.452,92.936,39.153]},{"z":4.6,"b":[89.678,36.01,92.936,39.153],"l":[92.936,39.153,92.333,39.05,92.123,38.941,91.881,38.875,91.629,38.826,91.492,38.817,91.294,38.745,91.012,38.699,90.654,38.674,90.636,38.624,90.478,38.533,90.312,38.466,90.151,38.433,90.193,38.326,90.439,37.996,90.41,37.847,90.436,37.779,90.834,37.608,90.957,37.519,91.074,37.488,91.152,37.323,91.235,37.195,91.316,37.119,91.288,37.014,91.191,37.001,91.055,36.945,90.891,36.939,90.808,36.911,90.712,36.822,90.688,36.698,90.71,36.636,90.805,36.559,91.007,36.503,91.061,36.314,91.136,36.141,91.111,36.074,90.915,36.013,90.829,36.01,90.623,36.111,90.292,36.116,90.166,36.129,90.014,36.253,89.972,36.105,89.867,36.066,89.678,36.082]},{"z":4.6,"b":[75.348,33.197,76.74,34.573],"l":[76.74,33.197,76.647,33.352,76.488,33.502,76.246,33.574,76.236,33.729,76.166,33.873,76.024,33.982,75.88,33.964,75.742,34.066,75.663,34.199,75.585,34.21,75.469,34.28,75.375,34.371,75.348,34.573]},{"z":4.6,"b":[76.74,32.504,78.381,33.197],"l":[78.381,32.528,78.27,32.504,78.304,32.596,78.375,32.649,78.367,32.765,78.254,32.698,78.115,32.668,78.009,32.61,77.939,32.695,77.929,32.762,77.858,32.873,77.721,33.013,77.539,32.918,77.349,32.867,77.136,32.927,76.956,33.025,76.74,33.197]},{"z":4.6,"b":[113.776,0.559,114.211,1.463],"l":[113.776,0.559,113.918,0.736,113.948,0.862,113.863,0.902,113.864,0.976,114.018,1.079,114.192,1.214,114.211,1.463]},{"z":4.6,"b":[115.607,-2.363,116.573,-1.426],"l":[116.573,-2.363,116.39,-2.314,116.043,-2.288,115.994,-2.271,115.874,-2.351,115.814,-2.184,115.829,-2.047,115.749,-2.001,115.741,-1.784,115.689,-1.636,115.607,-1.467,115.678,-1.426]},{"z":4.6,"b":[113.776,-1.426,115.805,0.735],"l":[115.678,-1.426,115.72,-1.426,115.802,-1.294,115.805,-1.085,115.74,-1.104,115.629,-0.978,115.518,-0.938,115.46,-0.886,115.415,-0.774,115.381,-0.582,115.33,-0.511,115.274,-0.311,115.277,-0.22,115.347,0.002,115.275,0.031,115.187,0.005,115.086,-0.118,115.003,-0.178,114.967,-0.098,115.018,0.058,114.953,0.284,114.968,0.364,115.076,0.533,115.057,0.637,114.976,0.735,114.735,0.673,114.663,0.621,114.378,0.589,114.184,0.65,114.036,0.647,113.97,0.604,113.776,0.559]},{"z":4.6,"b":[73.166,20.115,74.292,21.952],"l":[74.129,21.952,74.061,21.94,73.918,21.867,73.809,21.836,73.829,21.651,73.752,21.611,73.821,21.508,73.996,21.555,74.192,21.546,74.292,21.514,74.109,21.452,74.053,21.466,73.96,21.394,73.919,21.292,73.8,21.281,73.777,21.217,73.695,21.123,73.868,20.975,73.903,20.884,73.909,20.776,73.811,20.677,73.792,20.606,73.7,20.562,73.476,20.679,73.404,20.67,73.459,20.538,73.361,20.369,73.394,20.27,73.364,20.178,73.272,20.193,73.243,20.139,73.166,20.115]},{"z":4.6,"b":[72.708,20.073,72.947,20.127],"l":[72.947,20.127,72.708,20.073]},{"z":4.6,"b":[72.897,20.115,73.166,20.33],"l":[73.166,20.115,73.042,20.158,73.047,20.221,73.112,20.255,73.05,20.33,72.897,20.222,72.947,20.127]},{"z":4.6,"b":[72.818,20.379,72.875,20.468],"l":[72.818,20.379,72.875,20.404,72.847,20.468]},{"z":4.6,"b":[70.967,20.729,71.006,20.747],"l":[71.006,20.737,70.983,20.747,70.967,20.729]},{"z":4.6,"b":[71.057,23.036,74.323,24.708],"l":[71.057,24.693,71.251,24.637,71.378,24.647,71.546,24.688,71.652,24.658,71.751,24.678,71.948,24.663,72.051,24.708,72.099,24.657,72.262,24.608,72.275,24.568,72.501,24.496,72.556,24.507,72.668,24.461,72.747,24.381,72.897,24.361,72.959,24.39,73,24.493,73.095,24.504,73.137,24.336,73.079,24.204,73.246,24.016,73.354,24.105,73.424,23.941,73.364,23.889,73.362,23.784,73.511,23.684,73.621,23.583,73.619,23.452,73.711,23.415,73.812,23.435,73.904,23.333,73.96,23.359,74.106,23.248,74.109,23.191,74.233,23.156,74.323,23.036]},{"z":4.6,"b":[74.026,21.952,74.458,23.036],"l":[74.323,23.036,74.365,22.875,74.427,22.872,74.458,22.804,74.376,22.651,74.273,22.61,74.161,22.517,74.063,22.521,74.026,22.48,74.113,22.411,74.222,22.409,74.224,22.349,74.056,22.294,74.071,22.198,74.142,22.063,74.129,21.952]},{"z":4.7,"b":[121.174,46.22,126.056,53.317],"l":[121.413,53.317,121.607,53.241,121.8,53.024,121.692,52.908,121.503,52.775,121.174,52.595,121.491,52.445,121.692,52.389,121.831,52.269,122.018,52.283,122.174,52.48,122.339,52.454,122.492,52.295,122.731,52.249,122.775,52.2,122.638,52.095,122.754,51.847,122.714,51.702,122.836,51.568,122.839,51.511,122.956,51.386,123.088,51.312,123.272,51.267,123.438,51.273,123.57,51.204,123.789,51.233,123.866,51.313,124.004,51.302,124.162,51.328,124.351,51.279,124.502,51.361,124.666,51.335,124.821,51.366,125.061,51.534,125.128,51.616,125.333,51.583,125.487,51.507,125.577,51.389,125.719,51.254,125.843,51.206,125.871,51.145,125.984,51.104,126.056,50.98,126.001,50.902,125.824,50.767,125.809,50.562,125.531,50.393,125.418,50.195,125.264,50.106,125.295,50.014,125.188,49.942,125.235,49.848,125.22,49.668,125.131,49.637,125.236,49.554,125.267,49.438,125.223,49.22,125.158,49.151,124.864,49.175,124.822,49.061,124.622,48.742,124.616,48.668,124.538,48.572,124.521,48.369,124.562,48.236,124.52,48.129,124.426,48.174,124.327,48.332,124.301,48.492,124.234,48.531,124.078,48.448,123.727,48.191,123.578,48.054,123.293,47.946,123.207,47.814,123.095,47.755,122.797,47.644,122.565,47.531,122.495,47.41,122.405,47.332,122.574,47.131,122.797,47.034,122.996,46.84,123.187,46.762,123.33,46.833,123.38,46.898,123.489,46.817,123.609,46.775,123.536,46.692,123.397,46.598,123.292,46.586,123.094,46.622,123.02,46.612,122.989,46.407,123.162,46.22]},{"z":4.6,"b":[24.147,-31.79,25.522,-30.613],"l":[25.467,-30.613,25.483,-30.756,25.522,-30.802,25.507,-30.934,25.446,-31.095,25.35,-31.228,25.215,-31.202,24.968,-31.284,24.765,-31.389,24.559,-31.402,24.513,-31.553,24.5,-31.705,24.335,-31.719,24.154,-31.758,24.147,-31.79]},{"z":4.6,"b":[17.767,-32.937,24.147,-30.471],"l":[24.147,-31.79,23.963,-31.727,23.863,-31.8,23.809,-31.728,23.699,-31.667,23.401,-31.681,23.293,-31.751,23.181,-31.89,23.07,-31.977,23.027,-31.886,22.943,-31.856,22.756,-31.834,22.606,-31.777,22.477,-31.653,22.291,-31.57,22.144,-31.854,22.089,-31.893,22.101,-32.057,21.971,-32.196,21.853,-32.243,21.716,-32.212,21.553,-32.241,21.423,-32.349,21.305,-32.377,21.192,-32.459,21.118,-32.607,20.865,-32.677,20.823,-32.716,20.784,-32.862,20.705,-32.918,20.546,-32.902,20.421,-32.937,20.209,-32.764,20.123,-32.596,20.082,-32.412,20.213,-32.284,20.139,-32.19,19.925,-32.357,19.801,-32.373,19.704,-32.438,19.61,-32.458,19.534,-32.638,19.473,-32.583,19.488,-32.466,19.425,-32.31,19.493,-32.232,19.471,-32.078,19.319,-32.022,19.211,-31.914,19.029,-31.893,19.072,-31.748,19.067,-31.577,19.017,-31.495,18.997,-31.29,18.937,-31.197,18.905,-31.01,18.928,-30.942,18.939,-30.724,18.843,-30.695,18.735,-30.577,18.561,-30.471,18.464,-30.526,18.411,-30.5,18.336,-30.654,18.247,-30.783,18.157,-30.822,18.024,-30.775,17.892,-30.934,17.767,-31.156]},{"z":4.6,"b":[22.621,-28.1,25.027,-25.987],"l":[22.621,-26.112,22.705,-26.13,22.702,-26.41,22.815,-26.448,22.851,-26.398,22.915,-26.155,22.986,-26.161,23.057,-26.1,23.099,-25.987,23.374,-26.016,23.351,-26.08,23.229,-26.059,23.222,-26.12,23.303,-26.144,23.222,-26.268,23.284,-26.343,23.271,-26.479,23.458,-26.529,23.581,-26.615,23.494,-26.737,23.846,-26.887,23.927,-26.866,23.988,-26.912,24.024,-27.016,23.962,-27.1,23.894,-27.258,24.085,-27.359,24.036,-27.475,24.043,-27.585,24.116,-27.628,24.112,-27.743,24.408,-27.684,24.445,-27.783,24.363,-27.899,24.447,-27.977,24.438,-28.038,24.52,-28.1,24.611,-28,24.621,-27.904,24.691,-27.826,24.671,-27.672,24.796,-27.611,24.815,-27.687,24.972,-27.685,25.027,-27.708,24.842,-27.906,24.932,-28.097,25.013,-28.068]},{"z":4.6,"b":[24.341,-30.613,25.467,-28.068],"l":[25.013,-28.068,25.01,-28.115,24.916,-28.317,24.845,-28.519,24.874,-28.65,24.655,-29.069,24.341,-29.649,24.409,-29.764,24.658,-29.896,24.67,-29.95,24.784,-29.988,24.907,-30.201,25.029,-30.283,25.043,-30.33,25.185,-30.456,25.467,-30.613]},{"z":4.6,"b":[95.176,26.655,95.246,27.033],"l":[95.246,26.655,95.235,26.791,95.202,26.867,95.225,26.974,95.176,27.033]},{"z":4.6,"b":[94.959,27.033,96.007,27.993],"l":[95.176,27.033,95.261,27.049,95.424,27.141,95.493,27.25,95.546,27.274,95.644,27.23,95.8,27.286,95.904,27.276,96.007,27.365,95.978,27.45,95.88,27.463,95.887,27.557,95.804,27.619,95.776,27.717,95.901,27.865,95.964,27.993,95.717,27.99,95.544,27.902,95.408,27.871,95.313,27.896,94.959,27.773]},{"z":4.6,"b":[75.805,32.517,76.74,33.231],"l":[76.74,33.197,76.626,33.189,76.559,33.231,76.397,33.198,76.294,33.148,76.216,33.051,76.101,33.013,75.972,32.911,75.826,32.938,75.805,32.899,75.909,32.773,75.923,32.617,75.847,32.517]},{"z":4.6,"b":[75.298,32.301,75.847,32.517],"l":[75.847,32.517,75.743,32.464,75.683,32.4,75.612,32.395,75.502,32.301,75.39,32.335,75.298,32.332]},{"z":4.6,"b":[97.716,29.224,99.109,32.544],"l":[97.716,32.544,98.014,32.465,98.081,32.405,98.206,32.356,98.22,32.252,98.31,32.129,98.453,31.984,98.421,31.878,98.445,31.804,98.559,31.677,98.592,31.599,98.885,31.354,98.768,31.23,98.683,31.316,98.611,31.256,98.62,31.188,98.803,30.991,98.775,30.902,98.954,30.748,98.905,30.664,98.933,30.491,98.961,30.46,98.987,30.144,99.034,30.055,99.056,29.915,99.005,29.82,98.987,29.651,99.042,29.562,99.063,29.308,99.109,29.224]},{"z":4.6,"b":[98.099,28.14,99.109,29.224],"l":[99.109,29.224,98.99,29.202,99.006,29.073,98.932,28.986,98.909,28.912,98.946,28.842,98.873,28.812,98.82,28.886,98.785,29,98.665,28.974,98.639,28.879,98.678,28.757,98.589,28.67,98.64,28.424,98.576,28.318,98.49,28.248,98.41,28.251,98.3,28.355,98.23,28.203,98.099,28.14]},{"z":4.7,"b":[88.736,49.903,89.869,51.559],"l":[88.736,51.559,88.955,51.438,88.946,51.228,89.032,51.184,89.052,51.065,89.384,50.858,89.674,50.579,89.78,50.567,89.869,50.485,89.844,50.43,89.508,50.362,89.39,50.319,89.336,50.21,89.501,50.207,89.553,50.052,89.644,49.903]},{"z":4.7,"b":[27.67,58.44,29.975,59.014],"l":[27.67,58.982,27.873,59.014,28.131,58.99,28.272,58.856,28.552,58.865,28.757,58.827,28.946,58.82,28.975,58.78,29.232,58.706,29.449,58.595,29.539,58.48,29.786,58.44,29.975,58.445]},{"z":4.7,"b":[87.173,52.168,88.418,52.612],"l":[87.173,52.612,87.334,52.598,87.37,52.518,87.579,52.487,87.636,52.45,87.924,52.548,88.201,52.416,88.413,52.455,88.418,52.351,88.363,52.299,88.365,52.168]},{"z":4.7,"b":[87.839,51.316,88.736,52.168],"l":[88.365,52.168,88.121,52.098,87.839,51.811,88.086,51.714,87.967,51.598,87.862,51.549,88.106,51.432,88.132,51.386,88.364,51.32,88.508,51.316,88.665,51.424,88.659,51.535,88.736,51.559]},{"z":4.6,"b":[92.936,33.512,103.04,39.322],"l":[101.778,33.53,101.613,33.512,101.577,33.631,101.496,33.705,101.4,33.645,101.16,33.664,101.166,33.756,101.129,33.851,101.009,33.881,100.948,33.93,100.792,34.155,100.815,34.295,100.935,34.386,101.036,34.339,101.256,34.3,101.324,34.26,101.612,34.191,101.647,34.136,101.78,34.068,101.863,34.141,102.012,34.178,102.077,34.267,102.212,34.352,102.15,34.495,102.013,34.536,101.932,34.591,101.914,34.661,101.793,34.632,101.755,34.704,101.923,34.787,101.913,34.845,102.009,34.942,102.267,35.056,102.376,35.204,102.318,35.275,102.289,35.407,102.426,35.435,102.535,35.536,102.702,35.524,102.721,35.619,102.68,35.757,102.7,35.839,102.838,35.861,102.976,35.833,103,35.991,102.933,36.086,103.04,36.216,102.894,36.309,102.819,36.326,102.77,36.498,102.698,36.609,102.611,36.657,102.615,36.729,102.675,36.802,102.46,36.956,102.496,37.089,102.551,37.164,102.427,37.278,102.246,37.381,102.142,37.407,102.033,37.479,102.067,37.548,101.996,37.615,101.937,37.731,101.766,37.525,101.647,37.612,101.293,37.799,101.05,37.964,100.636,38.105,100.554,38.252,100.497,38.289,100.358,38.307,100.141,38.486,100.083,38.395,100.156,38.303,100.079,38.278,99.8,38.366,99.557,38.515,99.496,38.601,99.099,38.89,99.095,38.958,98.962,38.982,98.902,39.036,98.782,39.068,98.602,38.948,98.445,38.957,98.324,39.023,98.263,38.932,98.069,38.802,98.021,38.836,97.698,38.981,97.675,39.005,97.363,39.15,97.137,39.203,97.026,39.196,96.963,39.103,96.991,38.997,96.971,38.864,96.981,38.744,96.942,38.639,96.971,38.567,96.945,38.358,96.845,38.356,96.51,38.47,96.503,38.549,96.315,38.627,96.055,38.679,96.008,38.752,95.892,38.784,95.788,38.862,95.722,38.875,95.566,39.015,95.461,39.047,95.305,39.183,95.057,39.147,94.912,39.198,94.725,39.237,94.631,39.295,94.479,39.318,94.117,39.322,93.909,39.286,93.722,39.307,93.502,39.272,93.405,39.217,93.276,39.189,93.14,39.199,93.036,39.152,92.936,39.153]},{"z":4.7,"b":[84.014,50.629,87.173,52.668],"l":[84.067,50.629,84.183,50.661,84.542,50.691,84.6,50.858,84.528,50.881,84.471,50.973,84.252,51.002,84.018,51.064,84.014,51.138,84.172,51.144,84.27,51.275,84.394,51.266,84.705,51.395,84.937,51.43,85.008,51.489,85.239,51.471,85.313,51.569,85.428,51.56,85.537,51.7,85.67,51.754,85.74,51.861,85.877,51.919,85.834,51.971,85.94,52.075,86.15,52.07,86.265,52.012,86.44,52.023,86.505,52.12,86.754,52.109,86.814,52.145,86.806,52.292,86.618,52.372,86.841,52.648,86.952,52.635,87.117,52.668,87.173,52.612]},{"z":4.6,"b":[105.499,32.885,108.704,37.068],"l":[107.285,37.068,107.3,36.907,107.501,36.891,107.549,36.84,107.695,36.825,107.887,36.755,108.042,36.598,108.356,36.546,108.441,36.461,108.607,36.432,108.704,36.359,108.65,36.226,108.7,36.126,108.677,36.005,108.493,35.881,108.523,35.776,108.51,35.701,108.604,35.548,108.617,35.393,108.493,35.272,108.277,35.263,108.155,35.291,107.979,35.223,107.923,35.267,107.742,35.318,107.67,35.228,107.83,34.977,107.71,34.951,107.57,34.965,107.497,34.926,107.321,34.942,107.211,34.892,107.052,35.038,106.915,35.089,106.565,35.08,106.491,35.03,106.501,34.926,106.55,34.863,106.492,34.741,106.318,34.583,106.368,34.52,106.498,34.52,106.614,34.459,106.674,34.385,106.656,34.254,106.554,34.281,106.587,34.137,106.514,34.107,106.43,33.942,106.48,33.868,106.455,33.803,106.482,33.701,106.559,33.599,106.496,33.544,106.393,33.619,106.171,33.562,106.073,33.618,105.993,33.611,105.952,33.553,105.832,33.498,105.785,33.406,105.718,33.389,105.747,33.294,105.913,33.234,105.911,33.032,105.886,32.978,105.639,32.885,105.499,32.907]},{"z":4.6,"b":[101.778,32.608,105.499,34.296],"l":[105.499,32.907,105.431,32.911,105.386,32.823,105.435,32.774,105.27,32.642,105.147,32.608,105.032,32.639,104.898,32.612,104.841,32.64,104.645,32.658,104.558,32.688,104.406,32.809,104.289,32.848,104.321,32.953,104.378,32.959,104.406,33.063,104.328,33.14,104.281,33.272,104.293,33.364,104.175,33.49,104.156,33.624,104.093,33.669,103.915,33.683,103.745,33.676,103.659,33.711,103.523,33.714,103.559,33.807,103.351,33.756,103.272,33.765,103.184,33.822,103.193,33.884,103.142,33.962,103.152,34.109,103.12,34.169,103.001,34.214,102.931,34.296,102.782,34.274,102.598,34.165,102.618,34.084,102.441,34.06,102.372,33.976,102.319,33.987,102.171,33.942,102.343,33.725,102.356,33.609,102.482,33.54,102.482,33.465,102.279,33.378,102.131,33.285,102.099,33.222,102.004,33.219,101.864,33.123,101.832,33.269,101.923,33.406,101.884,33.546,101.778,33.53]},{"z":4.6,"b":[97.191,37.39,104.359,42.787],"l":[104.359,37.401,104.262,37.39,104.109,37.462,103.864,37.626,103.679,37.775,103.438,37.842,103.387,37.999,103.388,38.102,103.522,38.14,103.471,38.436,103.794,38.591,103.98,38.759,104.043,38.874,104.172,38.96,104.225,39.091,104.06,39.308,104.024,39.441,103.749,39.425,103.464,39.357,103.284,39.293,102.965,39.119,102.831,39.13,102.593,39.178,102.393,39.237,101.825,39.064,102.023,38.893,102.02,38.857,101.831,38.69,101.58,38.688,101.414,38.749,101.32,38.758,101.328,38.826,101.248,38.87,101.18,39.021,101.049,39.001,100.889,39.105,100.852,39.167,100.833,39.38,100.79,39.408,100.543,39.409,100.455,39.496,100.307,39.55,100.287,39.627,100.189,39.695,100.043,39.728,99.686,39.876,99.431,39.879,99.533,40.001,99.627,40.069,99.899,40.205,100.014,40.402,100.14,40.518,100.204,40.617,100.176,40.735,100.075,40.859,100.011,40.896,99.665,40.901,99.496,40.843,99.157,40.836,98.826,40.727,98.646,40.568,98.515,40.534,98.235,40.542,98.327,40.856,98.294,40.917,98.204,40.951,97.951,41.12,97.708,41.349,97.648,41.456,97.68,41.502,97.847,41.619,97.191,42.787]},{"z":4.6,"b":[104.359,35.261,107.285,37.44],"l":[104.359,37.401,104.465,37.44,104.69,37.412,104.722,37.339,104.621,37.249,104.709,37.215,104.765,37.25,104.855,37.218,104.922,37.097,105.18,36.972,105.322,36.781,105.22,36.693,105.269,36.55,105.393,36.384,105.446,36.255,105.437,36.107,105.351,36.058,105.318,35.933,105.432,35.756,105.687,35.66,105.829,35.494,105.936,35.525,106.065,35.488,106.112,35.425,106.211,35.395,106.313,35.274,106.378,35.261,106.465,35.332,106.495,35.553,106.429,35.699,106.47,35.727,106.767,35.707,106.913,35.789,106.843,35.882,106.915,35.907,106.945,36.077,106.814,36.212,106.745,36.207,106.604,36.278,106.497,36.268,106.495,36.437,106.44,36.515,106.494,36.56,106.502,36.707,106.602,36.726,106.649,36.834,106.575,36.931,106.634,36.998,106.644,37.182,106.791,37.19,106.933,37.107,107.188,37.117,107.285,37.068]},{"z":4.6,"b":[107.658,37.633,111.137,39.565],"l":[111.11,39.385,111.041,39.423,111.137,39.565,111.013,39.565,110.846,39.465,110.685,39.264,110.603,39.267,110.494,39.374,110.338,39.318,110.225,39.424,110.109,39.428,110.177,39.285,109.963,39.181,109.821,39.054,109.674,39.011,109.635,38.906,109.538,38.785,109.424,38.769,109.313,38.62,109.16,38.542,108.986,38.335,108.953,38.199,109.028,38.111,109.001,37.988,108.954,37.926,108.863,37.988,108.791,37.942,108.764,37.682,108.496,37.669,108.333,37.634,108.039,37.633,107.991,37.726,107.885,37.802,107.658,37.853]},{"z":4.6,"b":[104.359,37.401,107.658,39.361],"l":[107.658,37.853,107.405,37.922,107.324,38.066,107.163,38.139,107.061,38.115,106.829,38.15,106.47,38.291,106.645,38.459,106.704,38.637,106.932,38.921,106.978,39.036,106.912,39.069,106.808,39.199,106.789,39.361,106.614,39.358,106.497,39.286,106.347,39.294,106.276,39.269,106.283,39.157,106.113,39.121,106.06,38.979,105.897,38.736,105.841,38.571,105.828,38.227,105.756,38.136,105.825,38.005,105.788,37.805,105.659,37.74,105.534,37.725,105.354,37.753,105.085,37.661,104.944,37.546,104.647,37.507,104.522,37.53,104.425,37.499,104.359,37.401]},{"z":4.6,"b":[113.881,40.743,119.231,42.591],"l":[119.231,41.273,119.153,41.312,118.728,41.347,118.478,41.355,118.37,41.314,118.314,41.57,118.244,41.592,118.159,41.719,118.278,41.753,118.322,41.864,118.268,42.074,118.134,42.022,118.086,42.182,117.989,42.222,118.04,42.289,118.024,42.383,117.8,42.569,117.743,42.591,117.594,42.552,117.45,42.549,117.412,42.458,117.244,42.482,117.056,42.463,116.887,42.395,116.827,42.308,116.879,42.22,116.858,42.02,116.6,41.924,116.466,41.948,116.378,42.01,116.207,41.884,116.13,41.873,116.038,41.801,115.937,41.927,115.859,41.927,115.528,41.772,115.34,41.721,115.347,41.624,115.226,41.586,115.097,41.616,114.889,41.61,114.876,41.812,114.931,41.839,114.92,41.937,114.805,42.178,114.752,42.14,114.596,42.141,114.481,42.075,114.42,41.956,114.256,41.86,114.214,41.767,114.197,41.594,114.027,41.531,113.928,41.418,113.909,41.294,113.985,41.241,113.964,41.153,113.881,41.099,113.908,41.028,114.019,40.924,114.06,40.814,114.154,40.743]},{"z":4.6,"b":[111.11,39.385,114.154,40.743],"l":[114.154,40.743,114.027,40.635,114.076,40.55,114.044,40.488,113.808,40.434,113.677,40.429,113.518,40.344,113.334,40.319,113.23,40.417,113.086,40.404,112.961,40.356,112.844,40.226,112.734,40.173,112.596,40.238,112.399,40.288,112.279,40.229,112.165,40.059,112.099,40.005,111.921,39.689,111.761,39.598,111.675,39.636,111.49,39.652,111.425,39.625,111.421,39.522,111.343,39.444,111.199,39.427,111.11,39.385]},{"z":4.6,"b":[121.666,43.353,123.696,46.22],"l":[123.162,46.22,123.069,46.109,122.799,46.089,122.801,45.913,122.773,45.788,122.647,45.725,122.498,45.825,122.399,45.949,122.24,45.813,121.996,45.98,121.774,46.01,121.815,45.914,121.666,45.716,121.785,45.676,121.934,45.69,121.964,45.568,122.027,45.485,122.136,45.449,122.234,45.287,122.192,45.201,122.011,45.118,122.038,44.934,122.023,44.847,122.046,44.695,122.088,44.62,122.282,44.429,122.254,44.246,122.377,44.22,122.715,44.336,122.955,44.447,123.045,44.513,123.124,44.494,123.117,44.406,123.168,44.367,123.258,44.204,123.359,44.151,123.322,44.042,123.522,43.697,123.49,43.591,123.361,43.54,123.378,43.453,123.606,43.365,123.696,43.353]},{"z":4.6,"b":[119.231,41.273,123.696,43.353],"l":[123.696,43.353,123.612,43.081,123.354,43.012,123.182,42.944,123.131,42.826,122.893,42.745,122.728,42.741,122.476,42.85,122.367,42.799,122.426,42.722,122.4,42.658,122.299,42.636,122.045,42.721,121.918,42.647,121.878,42.54,121.724,42.458,121.646,42.454,121.544,42.535,121.369,42.5,121.034,42.26,120.913,42.297,120.735,42.228,120.628,42.16,120.509,42.149,120.469,42.038,120.34,41.966,120.141,41.789,120.108,41.707,120.026,41.734,120.03,41.861,119.96,41.972,119.892,41.999,119.808,42.12,119.821,42.209,119.629,42.251,119.563,42.372,119.336,42.289,119.245,42.192,119.366,42.1,119.322,41.965,119.331,41.868,119.299,41.777,119.316,41.658,119.39,41.473,119.362,41.375,119.231,41.273]},{"z":4.7,"b":[44.839,42.691,45.172,43.584],"l":[44.839,43.584,45.047,43.494,45.121,43.204,45.169,43.127,45.137,43.075,45.161,42.946,45.137,42.835,45.172,42.792,45.076,42.691]},{"z":4.7,"b":[44.839,43.661,45.509,44.015],"l":[44.839,43.661,45,43.705,45.08,43.788,45.083,43.953,45.19,43.952,45.237,43.88,45.432,43.879,45.459,44,45.509,44.015]},{"z":4.7,"b":[44.839,43.584,44.839,43.661],"l":[44.839,43.661,44.839,43.584]},{"z":4.7,"b":[44.499,42.74,44.937,43.62],"l":[44.839,43.584,44.768,43.62,44.616,43.61,44.522,43.564,44.499,43.392,44.572,43.392,44.575,43.24,44.713,43.218,44.818,43.163,44.915,43.071,44.937,42.92,44.852,42.836,44.628,42.883,44.639,42.74]},{"z":4.7,"b":[43.386,42.887,44.433,43.819],"l":[44.285,43.819,44.328,43.664,44.433,43.654,44.38,43.49,44.181,43.357,44.108,43.431,44.036,43.365,44.084,43.274,43.898,43.254,43.848,43.31,43.735,43.11,43.566,42.939,43.437,42.95,43.386,42.887]},{"z":4.7,"b":[44.285,43.661,44.839,43.83],"l":[44.285,43.819,44.399,43.795,44.694,43.83,44.803,43.802,44.839,43.661]},{"z":4.7,"b":[42.363,43.235,42.683,43.807],"l":[42.683,43.807,42.671,43.696,42.46,43.615,42.479,43.5,42.405,43.395,42.404,43.284,42.363,43.235]},{"z":4.7,"b":[42.683,43.807,44.285,43.994],"l":[42.683,43.807,42.852,43.817,42.975,43.891,43.111,43.896,43.175,43.948,43.266,43.91,43.3,43.824,43.502,43.867,43.729,43.839,43.803,43.941,43.906,43.994,44.259,43.925,44.285,43.819]},{"z":4.7,"b":[38.728,43.751,40.625,45.193],"l":[39.752,45.127,39.642,45.193,39.54,45.156,39.324,45.001,39.042,44.966,38.921,45.017,38.728,45.012,38.76,44.939,39.064,44.81,39.553,44.807,39.632,44.858,39.489,44.967,39.495,45.031,39.639,45.034,39.75,44.935,39.884,45.007,40.02,44.724,39.915,44.699,39.915,44.583,39.874,44.473,39.793,44.357,39.908,44.325,39.971,44.256,40.078,44.244,39.988,44.078,39.905,44.077,39.8,44.19,39.718,44.118,39.754,44.061,39.73,43.918,39.931,43.898,40.279,43.751,40.336,43.769,40.455,43.981,40.383,44.033,40.366,44.198,40.437,44.284,40.408,44.343,40.406,44.499,40.327,44.564,40.394,44.725,40.554,44.72,40.625,44.767,40.528,44.915,40.416,45,40.174,45.11,39.964,45.075,39.752,45.127]},{"z":4.7,"b":[41.729,43.807,42.683,44.495],"l":[41.729,44.459,41.905,44.495,41.945,44.403,42.038,44.364,42.23,44.344,42.346,44.306,42.499,44.299,42.555,44.245,42.303,44.129,42.306,44.058,42.378,43.971,42.497,44.03,42.66,43.917,42.683,43.807]},{"z":4.7,"b":[40.841,44.459,41.729,45.954],"l":[41.162,45.954,41.261,45.872,41.274,45.754,41.172,45.702,40.87,45.696,40.841,45.539,40.925,45.43,41.008,45.385,41.038,45.218,41.26,45.228,41.36,45.212,41.347,45.122,41.469,44.987,41.642,44.981,41.651,44.897,41.538,44.736,41.417,44.708,41.567,44.546,41.702,44.501,41.729,44.459]},{"z":4.7,"b":[40.69,43.523,41.729,44.459],"l":[41.729,44.459,41.602,44.367,41.704,44.308,41.711,44.243,41.633,44.122,41.538,44.085,41.449,43.994,41.282,44.012,41.256,44.081,40.985,44.116,40.951,43.989,40.782,43.927,40.724,43.81,40.723,43.631,40.69,43.523]},{"z":4.7,"b":[30.907,55.578,31.467,55.769],"l":[31.467,55.729,31.187,55.737,31.047,55.769,30.907,55.578]},{"z":4.7,"b":[38.666,45.954,41.162,46.88],"l":[38.666,46.88,38.69,46.821,38.883,46.808,38.849,46.713,38.884,46.635,39.099,46.601,39.18,46.688,39.444,46.809,39.577,46.778,39.817,46.785,40.023,46.765,40.132,46.722,40.045,46.616,40.059,46.542,40.214,46.525,40.284,46.403,40.234,46.358,40.339,46.28,40.735,46.276,40.757,46.233,40.954,46.198,41,46.127,41.126,46.068,41.162,45.954]},{"z":4.7,"b":[29.658,56.953,31,58.445],"l":[29.975,58.445,29.658,58.244,29.679,58.077,29.82,58.083,29.95,57.944,30.168,57.912,30.53,57.887,30.52,57.742,30.472,57.682,30.609,57.617,30.493,57.488,30.516,57.318,30.773,57.258,30.647,57.191,30.645,56.982,30.8,56.953,31,56.965]},{"z":4.7,"b":[30.817,55.729,31.498,56.965],"l":[31,56.965,30.896,56.897,30.89,56.808,30.817,56.72,30.913,56.656,31.08,56.623,31.017,56.519,31.215,56.435,31.231,56.346,31.492,56.301,31.474,56.119,31.498,56.029,31.345,55.964,31.413,55.89,31.467,55.729]},{"z":4.6,"b":[80.27,17.776,81.394,18.714],"l":[81.394,17.807,81.245,17.804,81.148,17.837,81.045,17.776,80.986,17.84,80.979,17.964,80.938,18.086,80.852,18.196,80.772,18.233,80.696,18.412,80.583,18.541,80.503,18.6,80.409,18.589,80.32,18.642,80.27,18.714]},{"z":3,"b":[-50.237,-9.846,-48.159,-5.364],"l":[-50.237,-9.846,-50.235,-9.843,-50.175,-9.73,-50.084,-9.422,-49.979,-9.22,-49.83,-9.022,-49.708,-8.895,-49.613,-8.839,-49.503,-8.709,-49.379,-8.498,-49.283,-8.301,-49.227,-8.14,-49.19,-7.995,-49.172,-7.867,-49.22,-7.752,-49.334,-7.649,-49.34,-7.51,-49.24,-7.335,-49.195,-7.169,-49.21,-7.006,-49.08,-6.856,-48.805,-6.718,-48.653,-6.596,-48.622,-6.489,-48.58,-6.414,-48.527,-6.372,-48.471,-6.353,-48.41,-6.358,-48.391,-6.309,-48.414,-6.207,-48.39,-6.142,-48.319,-6.112,-48.297,-6.067,-48.321,-6.007,-48.307,-5.966,-48.254,-5.945,-48.245,-5.889,-48.28,-5.796,-48.259,-5.724,-48.181,-5.672,-48.159,-5.619,-48.193,-5.565,-48.231,-5.533,-48.275,-5.522,-48.317,-5.486,-48.357,-5.424,-48.422,-5.398,-48.511,-5.408,-48.598,-5.399,-48.699,-5.364]},{"z":3,"b":[-48.723,-10.316,-45.945,-5.17],"l":[-48.699,-5.364,-48.723,-5.355,-48.631,-5.33,-48.574,-5.288,-48.524,-5.22,-48.458,-5.181,-48.378,-5.17,-48.282,-5.189,-48.169,-5.241,-48.059,-5.263,-47.953,-5.256,-47.885,-5.286,-47.854,-5.351,-47.763,-5.407,-47.612,-5.455,-47.52,-5.506,-47.486,-5.561,-47.445,-5.75,-47.397,-6.072,-47.406,-6.421,-47.47,-6.796,-47.537,-7.027,-47.606,-7.115,-47.665,-7.167,-47.716,-7.185,-47.717,-7.222,-47.669,-7.28,-47.612,-7.302,-47.546,-7.288,-47.507,-7.306,-47.495,-7.356,-47.498,-7.45,-47.458,-7.535,-47.413,-7.53,-47.347,-7.66,-47.303,-7.663,-47.28,-7.731,-47.15,-7.856,-47.079,-7.977,-47.016,-8.044,-46.985,-8.039,-46.869,-7.959,-46.744,-7.923,-46.574,-7.905,-46.489,-7.963,-46.467,-8.07,-46.503,-8.173,-46.507,-8.276,-46.543,-8.32,-46.503,-8.396,-46.659,-8.396,-46.793,-8.436,-46.838,-8.486,-46.882,-8.584,-46.922,-8.744,-46.905,-8.829,-47.065,-8.977,-47.083,-9.035,-47.038,-9.066,-46.94,-9.07,-46.847,-9.169,-46.829,-9.307,-46.753,-9.41,-46.561,-9.499,-46.534,-9.557,-46.601,-9.651,-46.65,-9.664,-46.668,-9.744,-46.507,-9.856,-46.463,-9.95,-46.472,-10.003,-46.347,-10.169,-46.284,-10.186,-46.19,-10.177,-46.088,-10.209,-46.034,-10.271,-45.945,-10.316]},{"z":3,"b":[-50.998,-20.102,-50.965,-19.513],"l":[-50.965,-19.513,-50.998,-19.664,-50.994,-20.102]},{"z":3,"b":[-54.243,-24.047,-53.119,-22.642],"l":[-53.119,-22.642,-53.272,-22.752,-53.481,-22.852,-53.62,-23.001,-53.677,-23.174,-53.771,-23.322,-53.908,-23.407,-53.996,-23.57,-54.05,-23.823,-54.125,-23.978,-54.243,-24.047]},{"z":3,"b":[-44.833,-22.405,-41.864,-20.776],"l":[-44.833,-22.405,-44.73,-22.361,-44.663,-22.369,-44.601,-22.316,-44.534,-22.303,-44.445,-22.253,-44.293,-22.24,-44.217,-22.249,-44.092,-22.173,-43.896,-22.106,-43.815,-22.066,-43.565,-22.066,-43.463,-22.057,-43.342,-22.003,-43.132,-22.026,-43.063,-22.063,-42.28,-21.713,-42.284,-21.642,-42.369,-21.633,-42.364,-21.593,-42.302,-21.486,-42.297,-21.405,-42.222,-21.338,-42.181,-21.164,-42.097,-21.003,-42.141,-20.963,-42.097,-20.923,-41.998,-20.932,-41.963,-20.91,-41.927,-20.829,-41.864,-20.776]},{"z":3,"b":[-38.216,-9.513,-37.975,-9.351],"l":[-38.216,-9.351,-37.976,-9.512,-37.975,-9.513]},{"z":3,"b":[-37.975,-10.492,-36.411,-9.513],"l":[-37.975,-9.513,-37.938,-9.538,-37.426,-9.778,-37.196,-9.886,-37.019,-9.957,-36.956,-10.014,-36.94,-10.084,-36.88,-10.154,-36.777,-10.224,-36.699,-10.257,-36.648,-10.254,-36.607,-10.293,-36.58,-10.371,-36.538,-10.411,-36.483,-10.411,-36.482,-10.411,-36.481,-10.411,-36.48,-10.412,-36.479,-10.412,-36.478,-10.413,-36.477,-10.414,-36.476,-10.414,-36.475,-10.415,-36.474,-10.415,-36.473,-10.416,-36.472,-10.416,-36.471,-10.417,-36.47,-10.417,-36.469,-10.418,-36.467,-10.419,-36.466,-10.419,-36.465,-10.42,-36.464,-10.421,-36.463,-10.421,-36.462,-10.422,-36.46,-10.423,-36.459,-10.423,-36.458,-10.424,-36.457,-10.424,-36.456,-10.425,-36.455,-10.426,-36.454,-10.426,-36.453,-10.427,-36.452,-10.427,-36.451,-10.428,-36.45,-10.428,-36.449,-10.429,-36.448,-10.429,-36.447,-10.43,-36.446,-10.43,-36.446,-10.431,-36.445,-10.431,-36.444,-10.431,-36.444,-10.432,-36.443,-10.432,-36.411,-10.492]},{"z":3,"b":[-66.828,-9.838,-61.61,-7.999],"l":[-66.828,-9.838,-66.744,-9.749,-66.695,-9.749,-66.597,-9.664,-66.503,-9.633,-66.405,-9.526,-66.396,-9.405,-66.105,-9.419,-65.998,-9.401,-65.931,-9.414,-65.766,-9.566,-65.677,-9.535,-65.632,-9.45,-65.525,-9.414,-65.445,-9.445,-65.418,-9.392,-65.222,-9.258,-65.199,-9.267,-65.172,-9.374,-65.092,-9.436,-65.039,-9.401,-64.905,-9.213,-64.927,-9.119,-64.887,-9.061,-64.78,-8.986,-64.69,-9.021,-64.597,-9.026,-64.476,-8.95,-64.418,-8.972,-64.382,-8.941,-64.217,-8.95,-64.119,-8.936,-64.128,-8.686,-64.074,-8.713,-63.998,-8.682,-63.922,-8.535,-63.994,-8.441,-63.918,-8.334,-63.753,-8.285,-63.726,-8.191,-63.592,-8.164,-63.588,-8.079,-63.539,-7.999,-62.905,-8.008,-62.829,-8.017,-62.722,-8.061,-62.677,-8.115,-62.637,-8.222,-62.556,-8.289,-62.539,-8.361,-62.454,-8.347,-62.373,-8.383,-62.306,-8.57,-62.172,-8.611,-62.132,-8.767,-61.998,-8.811,-61.918,-8.874,-61.873,-8.856,-61.838,-8.744,-61.762,-8.727,-61.717,-8.686,-61.61,-8.722,-61.61,-8.767]},{"z":3,"b":[-53.211,-18.017,-50.485,-12.843],"l":[-53.052,-18.017,-53.115,-17.855,-53.211,-17.409,-53.203,-17.304,-53.158,-17.209,-53.078,-17.095,-53.033,-16.995,-53.024,-16.91,-52.943,-16.814,-52.789,-16.705,-52.68,-16.576,-52.614,-16.428,-52.681,-16.361,-52.668,-16.289,-52.534,-16.227,-52.538,-16.169,-52.417,-16.082,-52.362,-16.058,-52.308,-16.009,-52.257,-15.936,-52.142,-15.873,-51.962,-15.819,-51.847,-15.747,-51.799,-15.656,-51.779,-15.593,-51.788,-15.557,-51.773,-15.523,-51.732,-15.49,-51.696,-15.403,-51.665,-15.261,-51.59,-15.142,-51.471,-15.044,-51.286,-15.003,-51.246,-15.004,-51.189,-14.977,-51.114,-14.838,-50.978,-14.463,-50.941,-14.201,-50.909,-14.107,-50.868,-14.095,-50.852,-13.995,-50.861,-13.806,-50.8,-13.612,-50.667,-13.41,-50.598,-13.249,-50.595,-13.131,-50.566,-13.015,-50.485,-12.843]},{"z":4.6,"b":[78.954,34.326,80.327,35.523],"l":[80.327,35.503,80.276,35.523,80.249,35.479,80.258,35.408,80.207,35.313,80.102,35.173,80.017,35.096,80.039,35.047,80.036,34.98,79.99,34.963,79.906,34.946,79.879,34.902,79.906,34.855,79.948,34.768,79.96,34.658,79.897,34.58,79.825,34.466,79.768,34.424,79.671,34.439,79.56,34.474,79.458,34.471,79.343,34.431,79.202,34.441,79.049,34.389,78.954,34.326]},{"z":4.6,"b":[92.073,26.916,94.959,27.773],"l":[92.073,26.916,92.204,26.916,92.468,26.966,92.621,27.038,92.717,27.052,92.91,27.031,92.966,26.974,93.094,26.959,93.415,26.995,93.503,26.974,93.684,26.982,93.84,27.109,93.897,27.243,94.127,27.453,94.208,27.538,94.233,27.629,94.289,27.622,94.414,27.583,94.667,27.652,94.959,27.773]},{"z":4.6,"b":[116.744,39.686,117.239,40.093],"l":[116.876,39.686,116.922,39.774,116.781,39.89,116.744,39.959,116.829,40.037,116.988,40.034,117.239,40.093]},{"z":4.6,"b":[116.681,38.559,117.558,39.602],"l":[116.793,39.602,116.791,39.493,116.853,39.341,116.826,39.217,116.892,39.103,116.712,39.012,116.681,38.916,116.711,38.821,116.881,38.699,117.021,38.708,117.095,38.612,117.189,38.606,117.271,38.559,117.558,38.625]},{"z":4.6,"b":[116.876,39.618,117.239,40.093],"l":[117.239,40.093,117.148,39.948,117.203,39.762,117.153,39.618,116.876,39.686]}]} \ No newline at end of file diff --git a/src/maps/map_data.js b/src/maps/map_data.js new file mode 100644 index 0000000..d646834 --- /dev/null +++ b/src/maps/map_data.js @@ -0,0 +1,80 @@ +const fs = require("fs"); +const path = require("path"); + +const DATA_DIR = path.join(__dirname, "data"); +const loaded = new Map(); + +const load = (name) => { + if (loaded.has(name)) return loaded.get(name); + let data = { f: [] }; + try { data = JSON.parse(fs.readFileSync(path.join(DATA_DIR, `${name}.json`), "utf8")); } catch (_) {} + loaded.set(name, data); + return data; +}; + +const countries = (detail) => load(detail === "high" ? "countries_50m" : "countries_110m").f; +const coast = () => load("coast_50m").f; +const lakes = () => load("lakes_50m").f; +const rivers = () => load("rivers_50m").f; +const states = () => load("states_50m").f; +const places = (detail) => load(detail === "high" ? "places_50m" : "places_110m").f; + +const fold = (s) => String(s || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim(); + +let placeIndex = null; +const buildPlaceIndex = () => { + if (placeIndex) return placeIndex; + const list = []; + for (const p of places("high")) { + list.push({ kind: "place", name: p.n, country: p.a, iso: p.i, pop: p.p || 0, capital: !!p.k, lat: p.la, lng: p.lo, key: fold(p.n) }); + } + for (const c of countries("low")) { + list.push({ kind: "country", name: c.n, country: "", iso: c.c, pop: Number.MAX_SAFE_INTEGER, capital: false, lat: c.ly, lng: c.lx, bbox: c.b, key: fold(c.n) }); + } + for (const c of countries("high")) { + if (list.some((e) => e.kind === "country" && e.key === fold(c.n))) continue; + list.push({ kind: "country", name: c.n, country: "", iso: c.c, pop: Number.MAX_SAFE_INTEGER, capital: false, lat: c.ly, lng: c.lx, bbox: c.b, key: fold(c.n) }); + } + placeIndex = list; + return list; +}; + +const searchPlaces = (query, limit = 5) => { + const q = fold(query); + if (!q) return []; + const index = buildPlaceIndex(); + const scored = []; + for (const e of index) { + let score = 0; + if (e.key === q) score = 3; + else if (e.key.startsWith(q)) score = 2; + else if (e.key.includes(q)) score = 1; + else if (e.iso && fold(e.iso) === q) score = 2; + if (!score) continue; + scored.push({ e, score }); + } + scored.sort((a, b) => b.score - a.score || b.e.pop - a.e.pop || a.e.key.localeCompare(b.e.key)); + return scored.slice(0, limit).map(({ e }) => ({ ...e, key: undefined })); +}; + +const toRad = (d) => d * Math.PI / 180; +const distanceKm = (lat1, lng1, lat2, lng2) => { + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +}; + +const nearestPlace = (lat, lng, maxKm = 300) => { + if (typeof lat !== "number" || typeof lng !== "number" || !isFinite(lat) || !isFinite(lng)) return null; + let best = null; + for (const p of places("high")) { + if (Math.abs(p.la - lat) > 4) continue; + const d = distanceKm(lat, lng, p.la, p.lo); + if (d > maxKm) continue; + if (!best || d < best.km) best = { name: p.n, country: p.a, iso: p.i, km: Math.round(d), lat: p.la, lng: p.lo }; + } + return best; +}; + +module.exports = { load, countries, coast, lakes, rivers, states, places, searchPlaces, nearestPlace, distanceKm, fold }; diff --git a/src/maps/map_renderer.js b/src/maps/map_renderer.js index 0a63299..427799a 100644 --- a/src/maps/map_renderer.js +++ b/src/maps/map_renderer.js @@ -1,223 +1,608 @@ -const { execFileSync } = require("child_process"); -const path = require("path"); -const fs = require("fs"); -const crypto = require("crypto"); +const data = require("./map_data"); -const BASE_MAP = path.join(__dirname, "..", "client", "assets", "images", "worldmap-z2.png"); -const CACHE_DIR = path.join(__dirname, "cache"); -const TILES_DIR = path.join(__dirname, "tiles"); const MAP_W = 1024; -const MAP_H = 1024; +const MAP_H = 640; +const TILE = 256; +const MIN_ZOOM = 2; +const MAX_ZOOM = 9; +const TILE_MAX_ZOOM = 6; +const TILE_PATH = "/maptiles"; +const TILE_ENV = process.env.OASIS_MAP_TILES || ""; +const CRISP_ZOOM = 9; +const MAX_LAT = 85.0511; +const GRID_COLS = 24; +const GRID_ROWS = 15; +const CLUSTER_PX = 28; +const EDGE = 6; +const POPUP_W = 320; +const POPUP_H = 400; +const POPUP_GAP = 10; -const latLngToPx = (lat, lng) => { - const latRad = lat * Math.PI / 180; - const x = Math.round((lng + 180) / 360 * MAP_W); - const y = Math.round((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * MAP_H); - return { x: Math.max(12, Math.min(MAP_W - 12, x)), y: Math.max(12, Math.min(MAP_H - 12, y)) }; -}; +const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); +const num = (v) => { const n = typeof v === "number" ? v : parseFloat(v); return isFinite(n) ? n : null; }; +const r1 = (v) => Math.round(v * 10) / 10; +const r4 = (v) => Math.round(v * 10000) / 10000; +const esc = (s) => String(s == null ? "" : s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); -const pxToLatLng = (px, py) => { - const lng = px / MAP_W * 360 - 180; - const n = Math.PI - 2 * Math.PI * py / MAP_H; - const lat = 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); - return { lat: Math.round(lat * 100) / 100, lng: Math.round(lng * 100) / 100 }; -}; - -const getMaxTileZoom = () => { - try { - const dirs = fs.readdirSync(TILES_DIR).filter(d => /^\d+$/.test(d) && fs.existsSync(path.join(TILES_DIR, d, '0_0.png'))); - return dirs.length ? Math.max(...dirs.map(Number)) : 0; - } catch (_) { return 0; } -}; - -const getViewportBounds = (centerLat, centerLng, zoom) => { - const effectiveZ = Math.min(zoom, getMaxTileZoom()); - const n = Math.pow(2, effectiveZ); - const tileSize = 256; - const worldPx = n * tileSize; - const scale = Math.pow(2, zoom - effectiveZ); - const vw = MAP_W / scale; - const vh = MAP_H / scale; - - const latRad = centerLat * Math.PI / 180; - const cx = (centerLng + 180) / 360 * worldPx; - const cy = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * worldPx; - - const x0 = cx - vw / 2; - const y0 = cy - vh / 2; - const x1 = cx + vw / 2; - const y1 = cy + vh / 2; - - const pxToLng = (px) => px / worldPx * 360 - 180; - const pxToLat = (py) => { const nn = Math.PI - 2 * Math.PI * py / worldPx; return 180 / Math.PI * Math.atan(0.5 * (Math.exp(nn) - Math.exp(-nn))); }; +const worldPx = (zoom) => TILE * Math.pow(2, zoom); +const project = (lat, lng, zoom) => { + const w = worldPx(zoom); + const la = clamp(lat, -MAX_LAT, MAX_LAT) * Math.PI / 180; return { - latMin: Math.max(-85, pxToLat(Math.min(y1, worldPx - 1))), - latMax: Math.min(85, pxToLat(Math.max(y0, 0))), - lngMin: Math.max(-180, pxToLng(Math.max(x0, 0))), - lngMax: Math.min(180, pxToLng(Math.min(x1, worldPx - 1))) + x: (lng + 180) / 360 * w, + y: (1 - Math.log(Math.tan(Math.PI / 4 + la / 2)) / Math.PI) / 2 * w }; }; -const renderMapWithPins = (markers, mainIdx) => { - const pins = (Array.isArray(markers) ? markers : []) - .filter((m) => m && typeof m.lat === "number" && typeof m.lng === "number") - .map((m, i) => ({ ...latLngToPx(m.lat, m.lng), main: i === (mainIdx || 0) })); +const unproject = (x, y, zoom) => { + const w = worldPx(zoom); + const n = Math.PI - 2 * Math.PI * y / w; + return { lat: 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))), lng: x / w * 360 - 180 }; +}; - const hash = crypto.createHash("md5") - .update(pins.map((p) => `${p.x},${p.y},${p.main}`).join(";")) - .digest("hex") - .slice(0, 12); +const latLngToPx = (lat, lng) => { + const p = project(lat, lng, MIN_ZOOM); + return { x: Math.round(p.x), y: Math.round(p.y) }; +}; - const outFile = path.join(CACHE_DIR, `map_${hash}.png`); +const pxToLatLng = (px, py) => { + const p = unproject(px, py, MIN_ZOOM); + return { lat: Math.round(p.lat * 100) / 100, lng: Math.round(p.lng * 100) / 100 }; +}; - if (fs.existsSync(outFile)) return `map_${hash}.png`; +const normalizeZoom = (z) => clamp(Math.round(num(z) == null ? MIN_ZOOM : num(z)), MIN_ZOOM, MAX_ZOOM); - const script = ` -from PIL import Image, ImageDraw -import sys, json +const makeView = (centerLat, centerLng, zoom) => { + const z = normalizeZoom(zoom); + const w = worldPx(z); + const c = project(num(centerLat) == null ? 0 : num(centerLat), num(centerLng) == null ? 0 : num(centerLng), z); + const cx = w <= MAP_W ? w / 2 : clamp(c.x, MAP_W / 2, w - MAP_W / 2); + const cy = w <= MAP_H ? w / 2 : clamp(c.y, MAP_H / 2, w - MAP_H / 2); + const center = unproject(cx, cy, z); + return { zoom: z, lat: r4(center.lat), lng: r4(center.lng), x0: cx - MAP_W / 2, y0: cy - MAP_H / 2, world: w }; +}; -pins = json.loads(sys.argv[1]) -im = Image.open(sys.argv[2]).copy() -draw = ImageDraw.Draw(im) +const toScreen = (view, lat, lng) => { + const p = project(lat, lng, view.zoom); + return { x: p.x - view.x0, y: p.y - view.y0 }; +}; -for p in pins: - x, y, main = p['x'], p['y'], p.get('main', False) - sw = 3 if main else 2 - sh = 18 if main else 13 - clr = '#e74c3c' if main else '#3498db' - dark = '#c0392b' if main else '#2980b9' - draw.polygon([(x, y + 2), (x - sw, y - sh + sw * 2), (x + sw, y - sh + sw * 2)], fill=clr) - draw.ellipse([x - sw - 1, y - sh - sw, x + sw + 1, y - sh + sw], fill=dark, outline='white', width=1) +const fromScreen = (view, x, y) => { + const p = unproject(view.x0 + x, view.y0 + y, view.zoom); + return { lat: r4(clamp(p.lat, -MAX_LAT, MAX_LAT)), lng: r4(clamp(p.lng, -180, 180)) }; +}; -im.save(sys.argv[3], optimize=True) -`; +const getViewportBounds = (centerLat, centerLng, zoom) => { + const view = makeView(centerLat, centerLng, zoom); + const nw = fromScreen(view, 0, 0); + const se = fromScreen(view, MAP_W, MAP_H); + return { latMin: se.lat, latMax: nw.lat, lngMin: nw.lng, lngMax: se.lng }; +}; - try { - fs.mkdirSync(CACHE_DIR, { recursive: true }); - execFileSync("python3", [ - "-c", script, - JSON.stringify(pins), - BASE_MAP, - outFile - ], { timeout: 10000 }); - } catch (e) { - return null; +const fitView = (points, opts = {}) => { + const pts = (Array.isArray(points) ? points : []).filter((p) => p && num(p.lat) != null && num(p.lng) != null); + const single = normalizeZoom(opts.singleZoom || 8); + if (!pts.length) return { zoom: MIN_ZOOM, lat: 10, lng: 0 }; + if (pts.length === 1) return { zoom: single, lat: num(pts[0].lat), lng: num(pts[0].lng) }; + let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; + for (const p of pts) { + const q = project(num(p.lat), num(p.lng), 0); + x0 = Math.min(x0, q.x); x1 = Math.max(x1, q.x); y0 = Math.min(y0, q.y); y1 = Math.max(y1, q.y); } + const pad = opts.pad == null ? 70 : opts.pad; + const dx = Math.max(x1 - x0, 1e-6); + const dy = Math.max(y1 - y0, 1e-6); + const z = Math.floor(Math.log2(Math.min((MAP_W - 2 * pad) / dx, (MAP_H - 2 * pad) / dy))); + const c = unproject((x0 + x1) / 2, (y0 + y1) / 2, 0); + return { zoom: clamp(z, MIN_ZOOM, Math.min(MAX_ZOOM, opts.maxZoom || MAX_ZOOM)), lat: r4(c.lat), lng: r4(c.lng) }; +}; - return `map_${hash}.png`; +const parseView = (raw) => { + const s = Array.isArray(raw) ? raw[raw.length - 1] : raw; + const m = /^\s*(\d{1,2})\/(-?\d+(?:\.\d+)?)\/(-?\d+(?:\.\d+)?)\s*$/.exec(String(s || "")); + if (!m) return null; + return { zoom: normalizeZoom(m[1]), lat: clamp(parseFloat(m[2]), -MAX_LAT, MAX_LAT), lng: clamp(parseFloat(m[3]), -180, 180) }; +}; + +const viewParam = (v) => `${normalizeZoom(v.zoom)}/${r4(v.lat)}/${r4(v.lng)}`; + +const parsePick = (raw) => { + const s = Array.isArray(raw) ? raw[raw.length - 1] : raw; + const m = /^\s*(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\s*$/.exec(String(s || "")); + if (!m) return null; + const lat = parseFloat(m[1]), lng = parseFloat(m[2]); + if (Math.abs(lat) > 90 || Math.abs(lng) > 180) return null; + return { lat: r4(lat), lng: r4(lng) }; +}; + +const resolveView = (params = {}, pins = [], opts = {}) => { + const explicit = parseView(params.view); + if (explicit) return makeView(explicit.lat, explicit.lng, explicit.zoom); + const clat = num(Array.isArray(params.clat) ? params.clat[0] : params.clat); + const clng = num(Array.isArray(params.clng) ? params.clng[0] : params.clng); + const zoomParam = num(Array.isArray(params.zoom) ? params.zoom[0] : params.zoom); + if (clat != null && clng != null) return makeView(clat, clng, zoomParam == null ? (opts.singleZoom || 8) : zoomParam); + const fit = fitView(pins, opts); + if (zoomParam != null && zoomParam >= MIN_ZOOM) return makeView(opts.focusLat != null ? opts.focusLat : fit.lat, opts.focusLng != null ? opts.focusLng : fit.lng, zoomParam); + return makeView(fit.lat, fit.lng, fit.zoom); +}; + +const metersPerPx = (view) => 156543.03392 * Math.cos(view.lat * Math.PI / 180) / Math.pow(2, view.zoom); + +const bboxHit = (b, vb) => b[0] <= vb[2] && b[2] >= vb[0] && b[1] <= vb[3] && b[3] >= vb[1]; + +const clipPolygon = (pts, x0, y0, x1, y1) => { + let out = pts; + const passes = [ + (p) => p.x >= x0, (p, q) => ({ x: x0, y: p.y + (q.y - p.y) * (x0 - p.x) / (q.x - p.x) }), + (p) => p.x <= x1, (p, q) => ({ x: x1, y: p.y + (q.y - p.y) * (x1 - p.x) / (q.x - p.x) }), + (p) => p.y >= y0, (p, q) => ({ x: p.x + (q.x - p.x) * (y0 - p.y) / (q.y - p.y), y: y0 }), + (p) => p.y <= y1, (p, q) => ({ x: p.x + (q.x - p.x) * (y1 - p.y) / (q.y - p.y), y: y1 }) + ]; + for (let i = 0; i < passes.length && out.length; i += 2) { + const inside = passes[i], cross = passes[i + 1]; + const res = []; + let prev = out[out.length - 1]; + for (const cur of out) { + const ci = inside(cur), pi = inside(prev); + if (ci) { + if (!pi) res.push(cross(prev, cur)); + res.push(cur); + } else if (pi) { + res.push(cross(prev, cur)); + } + prev = cur; + } + out = res; + } + return out; +}; + +const clipSegment = (a, b, x0, y0, x1, y1) => { + let t0 = 0, t1 = 1; + const dx = b.x - a.x, dy = b.y - a.y; + const checks = [[-dx, a.x - x0], [dx, x1 - a.x], [-dy, a.y - y0], [dy, y1 - a.y]]; + for (const [p, q] of checks) { + if (p === 0) { if (q < 0) return null; continue; } + const r = q / p; + if (p < 0) { if (r > t1) return null; if (r > t0) t0 = r; } + else { if (r < t0) return null; if (r < t1) t1 = r; } + } + return [{ x: a.x + t0 * dx, y: a.y + t0 * dy }, { x: a.x + t1 * dx, y: a.y + t1 * dy }]; +}; + +const simplify = (pts, tol) => { + if (pts.length <= 2) return pts; + const keep = new Uint8Array(pts.length); + keep[0] = 1; keep[pts.length - 1] = 1; + const stack = [[0, pts.length - 1]]; + const tol2 = tol * tol; + while (stack.length) { + const [a, b] = stack.pop(); + if (b - a < 2) continue; + const ax = pts[a].x, ay = pts[a].y, bx = pts[b].x, by = pts[b].y; + const dx = bx - ax, dy = by - ay; + const len2 = dx * dx + dy * dy; + let best = -1, bestD = 0; + for (let i = a + 1; i < b; i++) { + let d; + if (len2 === 0) { d = (pts[i].x - ax) ** 2 + (pts[i].y - ay) ** 2; } + else { + const t = clamp(((pts[i].x - ax) * dx + (pts[i].y - ay) * dy) / len2, 0, 1); + d = (pts[i].x - ax - t * dx) ** 2 + (pts[i].y - ay - t * dy) ** 2; + } + if (d > bestD) { bestD = d; best = i; } + } + if (best >= 0 && bestD > tol2) { keep[best] = 1; stack.push([a, best], [best, b]); } + } + const out = []; + for (let i = 0; i < pts.length; i++) if (keep[i]) out.push(pts[i]); + return out; +}; + +const projectFlat = (flat, view, tol) => { + const out = []; + let lx = NaN, ly = NaN; + for (let i = 0; i < flat.length; i += 2) { + const p = toScreen(view, flat[i + 1], flat[i]); + if (Math.abs(p.x - lx) + Math.abs(p.y - ly) < tol) continue; + out.push(p); + lx = p.x; ly = p.y; + } + return simplify(out, tol); +}; + +const pathOf = (pts, close) => { + if (pts.length < 2) return ""; + let d = `M${r1(pts[0].x)} ${r1(pts[0].y)}`; + for (let i = 1; i < pts.length; i++) d += `L${r1(pts[i].x)} ${r1(pts[i].y)}`; + return close ? d + "Z" : d; +}; + +const polygonPath = (rings, view, tol) => { + let d = ""; + for (const ring of rings) { + const pts = projectFlat(ring, view, tol); + if (pts.length < 3) continue; + const clipped = clipPolygon(pts, -EDGE, -EDGE, MAP_W + EDGE, MAP_H + EDGE); + if (clipped.length >= 3) d += pathOf(clipped, true); + } + return d; +}; + +const linePath = (flat, view, tol) => { + const pts = projectFlat(flat, view, tol); + let d = ""; + let run = []; + const flush = () => { if (run.length >= 2) d += pathOf(run, false); run = []; }; + for (let i = 1; i < pts.length; i++) { + const seg = clipSegment(pts[i - 1], pts[i], -EDGE, -EDGE, MAP_W + EDGE, MAP_H + EDGE); + if (!seg) { flush(); continue; } + if (!run.length || Math.abs(run[run.length - 1].x - seg[0].x) + Math.abs(run[run.length - 1].y - seg[0].y) > 0.05) { flush(); run.push(seg[0]); } + run.push(seg[1]); + } + flush(); + return d; +}; + +const viewBoxLatLng = (view) => { + const nw = fromScreen(view, -EDGE, -EDGE); + const se = fromScreen(view, MAP_W + EDGE, MAP_H + EDGE); + return [nw.lng, se.lat, se.lng, nw.lat]; +}; + +const baseCache = new Map(); +const cacheGet = (key) => baseCache.get(key); +const cacheSet = (key, val) => { + if (baseCache.size >= 64) baseCache.delete(baseCache.keys().next().value); + baseCache.set(key, val); +}; + +const tileZoom = (zoom, thumb) => clamp(Math.floor(zoom) - (thumb ? 2 : 0), 0, TILE_MAX_ZOOM); + +const tileCoverage = (view, opts = {}) => { + const tz = tileZoom(view.zoom, !!opts.thumb); + const size = TILE * Math.pow(2, view.zoom - tz); + const n = Math.pow(2, tz); + const tx0 = Math.max(0, Math.floor(view.x0 / size)), tx1 = Math.min(n - 1, Math.ceil((view.x0 + MAP_W) / size) - 1); + const ty0 = Math.max(0, Math.floor(view.y0 / size)), ty1 = Math.min(n - 1, Math.ceil((view.y0 + MAP_H) / size) - 1); + const out = []; + for (let ty = ty0; ty <= ty1; ty++) { + for (let tx = tx0; tx <= tx1; tx++) out.push({ z: tz, x: tx, y: ty, sx: tx * size - view.x0, sy: ty * size - view.y0, size }); + } + return out; +}; + +const tileHref = (base, t) => `${base}/${t.z}/${t.x}_${t.y}.jpg`; + +const tileBaseOf = (opts) => opts.tileBase || TILE_ENV || `${opts.publicBase || ""}${TILE_PATH}`; + +const renderRaster = (view, opts) => { + const tiles = tileCoverage(view, { thumb: !!opts.thumb }); + if (!tiles.length) return ""; + const base = tileBaseOf(opts); + const clipId = `${opts.id || "map"}-clip`; + let out = ``; + for (const t of tiles) { + out += ``; + } + return out + ""; +}; + +const graticuleStep = (zoom) => zoom <= 2 ? 30 : zoom === 3 ? 15 : zoom === 4 ? 10 : 0; + +const renderGraticule = (view, vb) => { + const step = graticuleStep(view.zoom); + if (!step) return ""; + let d = ""; + for (let lng = Math.ceil(vb[0] / step) * step; lng <= vb[2]; lng += step) { + const x = r1(toScreen(view, 0, lng).x); + d += `M${x} 0L${x} ${MAP_H}`; + } + for (let lat = Math.ceil(vb[1] / step) * step; lat <= vb[3]; lat += step) { + if (Math.abs(lat) >= 85) continue; + const y = r1(toScreen(view, lat, 0).y); + d += `M0 ${y}L${MAP_W} ${y}`; + } + return d ? `` : ""; +}; + +const renderBase = (view, detail) => { + const key = `${view.zoom}|${Math.round(view.x0)}|${Math.round(view.y0)}|${detail}`; + const hit = cacheGet(key); + if (hit) return hit; + const z = view.zoom; + const tol = detail === "low" ? 4 : z <= 3 ? 1.1 : 0.7; + const vb = viewBoxLatLng(view); + const high = z >= 5; + let out = detail === "low" ? "" : renderGraticule(view, vb); + let borders = ""; + for (const c of data.countries(high ? "high" : "low")) { + if (!bboxHit(c.b, vb)) continue; + for (const poly of c.p) { + const d = polygonPath(poly, view, tol); + if (d) borders += ``; + } + } + out += `${borders}`; + if (high && detail !== "low") { + let st = ""; + for (const s of data.states()) { + if (s.z > z || !bboxHit(s.b, vb)) continue; + const d = linePath(s.l, view, tol); + if (d) st += ``; + } + if (st) out += `${st}`; + } + if (z >= CRISP_ZOOM && detail !== "low") { + let co = ""; + for (const c of data.coast()) { + if (!bboxHit(c.b, vb)) continue; + const d = linePath(c.l, view, tol); + if (d) co += ``; + } + for (const l of data.lakes()) { + if (l.z > z || !bboxHit(l.b, vb)) continue; + for (const poly of l.p) { + const d = polygonPath(poly, view, tol); + if (d) co += ``; + } + } + if (co) out += `${co}`; + } + cacheSet(key, out); + return out; +}; + +const textWidth = (s, size) => String(s || "").length * size * 0.58; + +const collides = (boxes, b) => boxes.some((o) => b.x0 < o.x1 && b.x1 > o.x0 && b.y0 < o.y1 && b.y1 > o.y0); + +const renderLabels = (view, boxes) => { + const z = view.zoom; + let out = ""; + if (z >= 3 && z <= 7) { + const list = data.countries(z >= 5 ? "high" : "low").filter((c) => c.ml <= z && c.n).sort((a, b) => a.r - b.r); + for (const c of list) { + const p = toScreen(view, c.ly, c.lx); + if (p.x < 20 || p.x > MAP_W - 20 || p.y < 10 || p.y > MAP_H - 10) continue; + const w = textWidth(c.n, 14); + const b = { x0: p.x - w / 2 - 4, x1: p.x + w / 2 + 4, y0: p.y - 10, y1: p.y + 6 }; + if (collides(boxes, b)) continue; + boxes.push(b); + out += `${esc(c.n)}`; + } + } + if (z >= 2) { + const list = data.places(z >= 5 ? "high" : "low").filter((p) => p.z <= z).sort((a, b) => (b.k - a.k) || (b.p - a.p)); + for (const pl of list) { + const p = toScreen(view, pl.la, pl.lo); + if (p.x < 4 || p.x > MAP_W - 4 || p.y < 4 || p.y > MAP_H - 4) continue; + const w = textWidth(pl.n, 12); + const dot = { x0: p.x - 5, x1: p.x + 5, y0: p.y - 5, y1: p.y + 5 }; + if (collides(boxes, dot)) continue; + const lb = { x0: p.x + 5, x1: p.x + 9 + w, y0: p.y - 8, y1: p.y + 8 }; + const withLabel = !collides(boxes, lb) && lb.x1 <= MAP_W; + boxes.push(dot); + if (withLabel) boxes.push(lb); + const cls = pl.k ? "map-place-dot map-place-capital" : "map-place-dot"; + out += ``; + if (withLabel) out += `${esc(pl.n)}`; + } + } + return out ? `${out}` : ""; +}; + +const niceScale = (view) => { + const mpp = metersPerPx(view); + const target = 150 * mpp; + const pow = Math.pow(10, Math.floor(Math.log10(target))); + const nice = [1, 2, 5, 10].map((n) => n * pow).filter((n) => n <= target).pop() || pow; + const px = nice / mpp; + const label = nice >= 1000 ? `${Math.round(nice / 1000)} km` : `${Math.round(nice)} m`; + return { px, label }; +}; + +const renderScale = (view) => { + const { px, label } = niceScale(view); + const x = 14, y = MAP_H - 16; + return `${esc(label)} · z${view.zoom}`; +}; + +const renderAttribution = () => `Natural Earth`; + +const truncate = (s, n) => { + const line = String(s || "").split(/\r?\n/)[0].trim(); + return line.length > n ? line.slice(0, n - 1) + "…" : line; +}; + +const pinPath = (s) => `M0 0C${-7 * s} ${-9 * s} ${-11 * s} ${-14 * s} ${-11 * s} ${-21 * s}A${11 * s} ${11 * s} 0 1 1 ${11 * s} ${-21 * s}C${11 * s} ${-14 * s} ${7 * s} ${-9 * s} 0 0Z`; + +const clusterPins = (placed, view) => { + if (view.zoom >= MAX_ZOOM) return placed.map((p) => ({ ...p, members: [p] })); + const clusters = []; + for (const p of placed) { + const near = clusters.find((c) => Math.abs(c.x - p.x) <= CLUSTER_PX && Math.abs(c.y - p.y) <= CLUSTER_PX); + if (near) { + near.members.push(p); + near.x = near.members.reduce((s, m) => s + m.x, 0) / near.members.length; + near.y = near.members.reduce((s, m) => s + m.y, 0) / near.members.length; + } else { + clusters.push({ x: p.x, y: p.y, members: [p] }); + } + } + return clusters; +}; + +const renderPins = (view, pins, opts, boxes, hit = {}) => { + const placed = []; + pins.forEach((m, i) => { + const lat = num(m.lat), lng = num(m.lng); + if (lat == null || lng == null) return; + const p = toScreen(view, lat, lng); + if (p.x < -20 || p.x > MAP_W + 20 || p.y < -10 || p.y > MAP_H + 50) return; + placed.push({ ...m, lat, lng, x: p.x, y: p.y, idx: i }); + }); + if (!placed.length) return ""; + const clusters = clusterPins(placed.filter((p) => !p.pick), view); + for (const p of placed.filter((p) => p.pick)) clusters.push({ x: p.x, y: p.y, members: [p], pick: true }); + clusters.sort((a, b) => a.y - b.y); + const quiet = !!opts.popup; + let out = ""; + for (const c of clusters) { + const x = r1(c.x), y = r1(c.y); + if (c.members.length > 1) { + const n = c.members.length; + const center = fromScreen(view, c.x, c.y); + if (c.members.some((m) => m.focus)) hit.focus = { x: c.x, y: c.y, top: 20 }; + let gap = Infinity; + for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) gap = Math.min(gap, Math.max(Math.abs(c.members[i].x - c.members[j].x), Math.abs(c.members[i].y - c.members[j].y))); + const splitZoom = gap > 0 ? view.zoom + Math.ceil(Math.log2((CLUSTER_PX + 1) / gap)) : MAX_ZOOM; + const href = opts.viewHref ? opts.viewHref({ zoom: clamp(Math.max(splitZoom, view.zoom + 1), MIN_ZOOM, MAX_ZOOM), lat: center.lat, lng: center.lng }) : null; + const title = opts.clusterTitle ? `${n} ${opts.clusterTitle}` : String(n); + const glyph = `${esc(title)}${n}`; + out += href ? `${glyph}` : glyph; + boxes.push({ x0: c.x - 20, x1: c.x + 20, y0: c.y - 20, y1: c.y + 20 }); + continue; + } + const m = c.members[0]; + if (m.pick) { + out += ``; + continue; + } + const s = m.main ? 1.25 : 1; + const cls = ["map-pin", m.main ? "map-pin-main" : "", m.focus ? "map-pin-focus" : ""].filter(Boolean).join(" "); + let g = ``; + if (m.title) g += `${esc(m.title)}`; + g += ``; + const silent = quiet && m.focus; + if (silent) hit.focus = { x: c.x, y: c.y, top: 32 * s }; + const label = silent ? "" : truncate(m.label, 28); + if (label) { + const w = textWidth(label, 12) + 12; + const ly = -21 * s - 11 * s - 16; + g += `${esc(label)}`; + boxes.push({ x0: c.x - w / 2, x1: c.x + w / 2, y0: c.y + ly - 12, y1: c.y }); + } else { + boxes.push({ x0: c.x - 12, x1: c.x + 12, y0: c.y - 34, y1: c.y }); + } + if (m.image && !silent) { + const size = 38; + g += ``; + boxes.push({ x0: c.x + 13 * s, x1: c.x + 13 * s + size + 4, y0: c.y - 21 * s - size / 2 - 2, y1: c.y - 21 * s + size / 2 + 2 }); + } + g += ""; + out += m.href ? `${g}` : g; + } + return `${out}`; +}; + +const popupPlacement = (at) => { + const roomAbove = at.y - at.top - POPUP_GAP; + const roomBelow = MAP_H - at.y - POPUP_GAP; + const below = roomAbove < POPUP_H && roomBelow > roomAbove; + const h = Math.min(POPUP_H, Math.max(below ? roomBelow : roomAbove, 60)); + const x = clamp(at.x - POPUP_W / 2, 4, MAP_W - POPUP_W - 4); + const y = below ? at.y + POPUP_GAP : at.y - at.top - POPUP_GAP - h; + return { x, y, w: POPUP_W, h, below, tipX: clamp(at.x, x + 14, x + POPUP_W - 14) }; +}; + +const renderPopup = (at, html) => { + if (!at || !html) return ""; + const p = popupPlacement(at); + const pointer = p.below + ? `M${r1(p.tipX - 8)} ${r1(p.y)}L${r1(p.tipX)} ${r1(p.y - 8)}L${r1(p.tipX + 8)} ${r1(p.y)}Z` + : `M${r1(p.tipX - 8)} ${r1(p.y + p.h)}L${r1(p.tipX)} ${r1(p.y + p.h + 8)}L${r1(p.tipX + 8)} ${r1(p.y + p.h)}Z`; + return `` + + `` + + `
${html}
`; +}; + +const renderGrid = (view, opts) => { + const pick = opts.pick; + const legacy = opts.clickUrl; + if (!pick && !legacy) return ""; + const cells = []; + for (let gy = 0; gy < GRID_ROWS; gy++) { + for (let gx = 0; gx < GRID_COLS; gx++) { + const c = fromScreen(view, (gx + 0.5) / GRID_COLS * MAP_W, (gy + 0.5) / GRID_ROWS * MAP_H); + const val = `${c.lat},${c.lng}`; + if (pick) { + cells.push(``); + } else { + const latParam = opts.latParam || "lat", lngParam = opts.lngParam || "lng"; + cells.push(``); + } + } + } + return `
${cells.join("")}
`; +}; + +const renderControls = (view, opts) => { + const nav = opts.nav; + if (!nav) return ""; + const t = opts.text || {}; + const el = (target, label, cls, title) => { + const value = target ? viewParam(target) : ""; + if (nav.form) { + return ``; + } + return `${label}`; + }; + const pan = (dx, dy) => { + const c = fromScreen(view, MAP_W / 2 + dx * MAP_W * 0.4, MAP_H / 2 + dy * MAP_H * 0.4); + return { zoom: view.zoom, lat: c.lat, lng: c.lng }; + }; + const zoomIn = view.zoom < MAX_ZOOM ? el({ zoom: view.zoom + 1, lat: view.lat, lng: view.lng }, "+", "map-ctrl-in", t.zoomIn || "+") : `+`; + const zoomOut = view.zoom > MIN_ZOOM ? el({ zoom: view.zoom - 1, lat: view.lat, lng: view.lng }, "−", "map-ctrl-out", t.zoomOut || "−") : `−`; + const fit = el(null, "⌖", "map-ctrl-fit", t.fit || "⌖"); + return `
${zoomIn}${zoomOut}
` + + `
` + + `${el(pan(0, -1), "↑", "map-ctrl-up", t.pan || "↑")}` + + `${el(pan(-1, 0), "←", "map-ctrl-left", t.pan || "←")}${fit}${el(pan(1, 0), "→", "map-ctrl-right", t.pan || "→")}` + + `${el(pan(0, 1), "↓", "map-ctrl-down", t.pan || "↓")}` + + `
`; +}; + +const renderMapHtml = (view, pins, opts = {}) => { + const detail = opts.thumb ? "low" : "normal"; + const boxes = []; + const pinList = Array.isArray(pins) ? pins : []; + const grid = renderGrid(view, opts); + const hit = {}; + const pinsSvg = renderPins(view, pinList, opts, boxes, hit) + renderPopup(hit.focus, opts.popup); + const labels = opts.thumb ? "" : renderLabels(view, boxes); + const extras = opts.thumb ? "" : renderScale(view) + renderAttribution(); + const base = `` + renderRaster(view, opts) + renderBase(view, detail); + const svgOpen = ``; + let html = `
`; + if (grid || opts.nav) { + html += svgOpen + base + labels + extras + ""; + html += grid; + html += `${pinsSvg}`; + } else { + html += svgOpen + base + labels + pinsSvg + extras + ""; + } + html += renderControls(view, opts); + html += "
"; + return html; +}; + +const renderMapWithPins = (markers, mainIdx) => { + const pins = (Array.isArray(markers) ? markers : []).map((m, i) => ({ ...m, main: i === (mainIdx || 0) })); + const fit = fitView(pins); + return renderMapHtml(makeView(fit.lat, fit.lng, fit.zoom), pins, {}); }; const renderZoomedMapWithPins = (centerLat, centerLng, zoom, markers, mainIdx) => { - const maxZ = getMaxTileZoom(); - if (!maxZ || zoom <= 2) return renderMapWithPins(markers, mainIdx); - - const effectiveZ = Math.min(zoom, maxZ); - const scale = Math.pow(2, zoom - effectiveZ); - const n = Math.pow(2, effectiveZ); - const tileSize = 256; - const worldPx = n * tileSize; - - const latRad = centerLat * Math.PI / 180; - const cx = (centerLng + 180) / 360 * worldPx; - const cy = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * worldPx; - - const vw = MAP_W / scale; - const vh = MAP_H / scale; - const x0 = cx - vw / 2; - const y0 = cy - vh / 2; - - const pinData = (Array.isArray(markers) ? markers : []) - .filter((m) => m && typeof m.lat === "number" && typeof m.lng === "number") - .map((m, i) => { - const latR = m.lat * Math.PI / 180; - const wx = (m.lng + 180) / 360 * worldPx; - const wy = (1 - Math.log(Math.tan(latR) + 1 / Math.cos(latR)) / Math.PI) / 2 * worldPx; - return { px: (wx - x0) * scale, py: (wy - y0) * scale, main: i === (mainIdx || 0) }; - }); - - const hashInput = `z${zoom}_${Math.round(centerLat * 100)}_${Math.round(centerLng * 100)}_` + pinData.map(p => `${Math.round(p.px)},${Math.round(p.py)},${p.main}`).join(";"); - const hash = crypto.createHash("md5").update(hashInput).digest("hex").slice(0, 12); - const outFile = path.join(CACHE_DIR, `map_${hash}.png`); - - if (fs.existsSync(outFile)) return `map_${hash}.png`; - - const txMin = Math.max(0, Math.floor(x0 / tileSize)); - const txMax = Math.min(n - 1, Math.floor((x0 + vw) / tileSize)); - const tyMin = Math.max(0, Math.floor(y0 / tileSize)); - const tyMax = Math.min(n - 1, Math.floor((y0 + vh) / tileSize)); - - const tiles = []; - for (let tx = txMin; tx <= txMax; tx++) { - for (let ty = tyMin; ty <= tyMax; ty++) { - const tp = path.join(TILES_DIR, String(effectiveZ), `${tx}_${ty}.png`); - tiles.push({ tx, ty, tp, exists: fs.existsSync(tp) }); - } - } - - const script = ` -from PIL import Image, ImageDraw -import sys, json, os - -args = json.loads(sys.argv[1]) -out_file = sys.argv[2] - -tile_size = 256 -tx_min = args['txMin'] -ty_min = args['tyMin'] -tx_max = args['txMax'] -ty_max = args['tyMax'] -x0 = args['x0'] -y0 = args['y0'] -vw = args['vw'] -vh = args['vh'] -scale = args['scale'] -pins = args['pins'] -tiles = args['tiles'] - -canvas_w = (tx_max - tx_min + 1) * tile_size -canvas_h = (ty_max - ty_min + 1) * tile_size -canvas = Image.new('RGB', (canvas_w, canvas_h), (170, 211, 223)) - -for t in tiles: - if t['exists']: - try: - tile = Image.open(t['tp']).convert('RGB') - canvas.paste(tile, ((t['tx'] - tx_min) * tile_size, (t['ty'] - ty_min) * tile_size)) - except: - pass - -crop_x = x0 - tx_min * tile_size -crop_y = y0 - ty_min * tile_size -cropped = canvas.crop((int(crop_x), int(crop_y), int(crop_x + vw), int(crop_y + vh))) -result = cropped.resize((1024, 1024), Image.LANCZOS) - -draw = ImageDraw.Draw(result) -for p in pins: - px, py, main = p['px'], p['py'], p.get('main', False) - if -20 <= px <= 1044 and -20 <= py <= 1044: - sw = 3 if main else 2 - sh = 18 if main else 13 - clr = '#e74c3c' if main else '#3498db' - dark = '#c0392b' if main else '#2980b9' - draw.polygon([(px, py + 2), (px - sw, py - sh + sw * 2), (px + sw, py - sh + sw * 2)], fill=clr) - draw.ellipse([px - sw - 1, py - sh - sw, px + sw + 1, py - sh + sw], fill=dark, outline='white', width=1) - -result.save(out_file, optimize=True) -`; - - try { - fs.mkdirSync(CACHE_DIR, { recursive: true }); - execFileSync("python3", [ - "-c", script, - JSON.stringify({ txMin, tyMin, txMax, tyMax, x0, y0, vw, vh, scale, pins: pinData, tiles }), - outFile - ], { timeout: 15000 }); - } catch (e) { - return renderMapWithPins(markers, mainIdx); - } - - return `map_${hash}.png`; + const pins = (Array.isArray(markers) ? markers : []).map((m, i) => ({ ...m, main: i === (mainIdx || 0) })); + return renderMapHtml(makeView(centerLat, centerLng, zoom), pins, {}); }; -module.exports = { renderMapWithPins, renderZoomedMapWithPins, getViewportBounds, getMaxTileZoom, latLngToPx, pxToLatLng, MAP_W, MAP_H }; +module.exports = { + MAP_W, MAP_H, MIN_ZOOM, MAX_ZOOM, TILE_MAX_ZOOM, TILE_PATH, + project, unproject, latLngToPx, pxToLatLng, makeView, toScreen, fromScreen, fitView, resolveView, + parseView, viewParam, parsePick, getViewportBounds, metersPerPx, clusterPins, niceScale, tileCoverage, popupPlacement, + renderMapHtml, renderMapWithPins, renderZoomedMapWithPins +}; diff --git a/src/models/activity_model.js b/src/models/activity_model.js index 6b51862..b4770b6 100644 --- a/src/models/activity_model.js +++ b/src/models/activity_model.js @@ -1,8 +1,10 @@ const pull = require('../server/node_modules/pull-stream'); +const longText = require('../backend/long_text'); const ssbRef = require('../server/node_modules/ssb-ref'); const { getConfig } = require('../configs/config-manager.js'); const { buildVoteTally } = require('../backend/vote_tally'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const safeFeedId = (v) => { @@ -57,6 +59,8 @@ function inferType(c = {}) { } const HIDDEN_ENVELOPE_TYPES = new Set([ + 'textChunk', + 'aiExchange', 'tribe-keys-distrib', 'tribe-keys', 'tribe-invite-msg', @@ -67,7 +71,6 @@ const HIDDEN_ENVELOPE_TYPES = new Set([ 'larpJoinHouse', 'larpLeaveLarp', 'larpTestAttempt', - 'larpHousePost', 'larpHouseInvite', 'larpHouseInviteRedeem', 'larpHouseTribeAnchor', @@ -75,19 +78,29 @@ const HIDDEN_ENVELOPE_TYPES = new Set([ 'larpAutoInvite', 'karmaScore', 'pubAvailability', + 'pam', 'calendarReminderSent', 'taskReminderSent', + 'ubiClaim', 'ubiClaimResult', + 'ubiRefuse', 'ubiAllocation', + 'bankClaim', 'courts-key' ]); -module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { +module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, larpModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }; let _feedCache = null; let _feedCacheInflight = null; + const logTip = async () => { + try { + const ssbClient = await openSsb(); + return await require('./typed_log').logTip(ssbClient); + } catch (_) { return null; } + }; const FEED_CACHE_MS = 15 * 1000; const buildAccessibleTribeIds = async () => { @@ -95,14 +108,14 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { if (!tribesModel) return set; try { const list = await tribesModel.listAll(); - for (const t of list) { - if (!t || !t.id) continue; + await Promise.all(list.map(async (t) => { + if (!t || !t.id) return; set.add(t.id); try { const chain = await tribesModel.getChainIds(t.id); for (const cid of chain) set.add(cid); } catch (_) {} - } + })); } catch (_) {} return set; }; @@ -143,6 +156,92 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { return t.length > max ? t.slice(0, max - 1) + '…' : t; }; + const CHAT_THREAD_LIMIT = 6; + const buildChatThreads = async (ssbClient, idToAction, rootOf) => { + const replacedChatIds = new Set(); + for (const a of idToAction.values()) { + if (a.type !== 'chat') continue; + const rep = a.content && a.content.replaces; + if (rep) replacedChatIds.add(rep); + } + const stateByRoot = new Map(); + for (const a of idToAction.values()) { + if (a.type !== 'chat') continue; + const root = rootOf(a.id); + const isTip = !replacedChatIds.has(a.id); + const prev = stateByRoot.get(root); + if (prev && !((isTip && !prev.isTip) || (isTip === prev.isTip && (a.ts || 0) >= prev.ts))) continue; + const cc = a.content || {}; + stateByRoot.set(root, { ts: a.ts || 0, isTip, owner: cc.author || a.author || null, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || ''), members: Array.isArray(cc.members) ? cc.members.length : 0 }); + } + const msgsByRoot = new Map(); + for (const a of idToAction.values()) { + const isPoll = a.type === 'poll'; + if (a.type !== 'chatMessage' && !isPoll) continue; + const c = a.content || {}; + if (c.tribeId || c.encryptedText || c.encryptedQuestion) continue; + const root = c.chatId; + if (!root) continue; + if (isPoll) { + if (!(typeof c.question === 'string' && c.question.trim())) continue; + } else if (!(typeof c.text === 'string' && c.text) && !c.image) continue; + if (!msgsByRoot.has(root)) msgsByRoot.set(root, []); + msgsByRoot.get(root).push(isPoll ? { ...a, content: { chatId: root, text: c.question.trim(), poll: true } } : a); + } + const missingRoots = [...msgsByRoot.keys()].filter(root => !stateByRoot.has(root)); + const rootMsgs = new Map(await Promise.all(missingRoots.map(async root => [root, await getMsg(ssbClient, root)]))); + for (const root of missingRoots) { + const val = rootMsgs.get(root); + const cc = val && val.content; + if (cc && typeof cc === 'object' && cc.type === 'chat') { + stateByRoot.set(root, { ts: 0, owner: cc.author || val.author || null, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || ''), members: Array.isArray(cc.members) ? cc.members.length : 0 }); + } else { + stateByRoot.set(root, null); + } + } + const items = []; + for (const [root, msgs] of msgsByRoot.entries()) { + const info = stateByRoot.get(root); + if (!info || info.tribeId || info.status !== 'OPEN') continue; + const asc = msgs.slice().sort((x, y) => (x.ts || 0) - (y.ts || 0)); + const last = asc[asc.length - 1]; + items.push({ + id: `chatthread:${root}`, + type: 'chatThread', + author: last.author, + ts: last.ts || 0, + content: { + type: 'chatThread', + chatRoot: root, + chatAuthor: info.owner || null, + title: info.title || '', + description: info.description || '', + members: Math.max(info.members || 0, new Set(asc.map(m => m.author)).size), + messageCount: asc.length, + replies: (() => { + const byId = new Map(asc.map(m => [m.id, m])); + return asc.slice(-CHAT_THREAD_LIMIT).map(m => { + const quotedId = (m.content && typeof m.content.replyTo === 'string') ? m.content.replyTo : null; + const quoted = quotedId ? byId.get(quotedId) : null; + return { + id: m.id, + author: m.author, + ts: m.ts || 0, + text: (m.content && m.content.text) || '', + poll: !!(m.content && m.content.poll), + image: (m.content && m.content.image) || null, + mimeType: (m.content && m.content.mimeType) || '', + replyTo: quotedId, + reply: quoted ? { id: quoted.id, author: quoted.author, text: String((quoted.content && quoted.content.text) || '').slice(0, 140) } : null + }; + }); + })() + } + }); + } + return items; + }; + return { invalidateCache() { _feedCache = null; @@ -156,16 +255,18 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { if (entry && now - entry.ts < FEED_CACHE_MS) return entry.value; if (!_feedCacheInflight) _feedCacheInflight = new Map(); if (_feedCacheInflight.has(cacheKey)) return _feedCacheInflight.get(cacheKey); + const tipNow = await logTip(); + if (entry && tipNow && entry.tip === tipNow) { entry.ts = now; return entry.value; } const promise = (async () => { const ssbClient = await openSsb(); const userId = ssbClient.id; - const results = await new Promise((resolve, reject) => { + const results = (await new Promise((resolve, reject) => { pull( - ssbClient.createLogStream({ reverse: true, limit: logLimit }), + ssbClient.createLogStream({}), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) ); - }); + })).reverse(); if (typeof ssbClient.createUserStream === 'function') { const ownResults = await new Promise((resolve) => { @@ -181,12 +282,42 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } const tombstoned = buildValidatedTombstoneSet(results); + const chunkLookup = longText.lookupIn(longText.indexChunks(results)); const parentOf = new Map(); const idToAction = new Map(); const rawById = new Map(); const voteTally = buildVoteTally(results); + const pollTally = new Map(); + for (const m of results) { + const c = m && m.value && m.value.content; + if (!c || c.type !== 'pollVote' || typeof c.target !== 'string') continue; + if (!Array.isArray(c.choices)) continue; + const entry = pollTally.get(c.target) || { counts: {}, voters: new Map() }; + entry.voters.set(m.value.author, { ts: m.value.timestamp || 0, choices: c.choices }); + pollTally.set(c.target, entry); + } + for (const entry of pollTally.values()) { + entry.counts = {}; + for (const v of entry.voters.values()) { + for (const choice of v.choices) entry.counts[choice] = (entry.counts[choice] || 0) + 1; + } + } const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); + let larpWall = null; + if (larpModel && results.some(m => m && m.value && m.value.content && m.value.content.type === 'larpHousePost')) { + try { + const s = await openSsb(); + larpWall = { + mine: await larpModel.getUserHouse(s.id).catch(() => null), + governing: typeof larpModel.getGoverningHouseKey === 'function' ? larpModel.getGoverningHouseKey() : null, + members: await larpModel.listAllMemberships().catch(() => new Map()) + }; + } catch (_) { larpWall = null; } + } + const larpPostVisible = (author, house) => !!larpWall && !!house + && (larpWall.members.get(author) || 'academia') === house + && (house === 'academia' || house === larpWall.governing || house === larpWall.mine); for (const msg of results) { const k = msg.key; @@ -194,7 +325,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { let c = v?.content; if (!c) continue; if (typeof c === 'string' && c.endsWith('.box')) continue; + if (c.private === true) continue; if (c.type && HIDDEN_ENVELOPE_TYPES.has(c.type)) continue; + if (c.type === 'larpHousePost' && !larpPostVisible(v.author, String(c.house || '').toLowerCase())) continue; if (typeof c.type === 'string' && /e2ee/i.test(c.type)) continue; if (c.type === 'contact') continue; if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { @@ -224,6 +357,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } catch (_) {} } if (!isContentSane(c)) continue; + if ((c.type === 'post' || c.type === 'forum') && longText.hasChunks(c)) c = longText.resolveField(c, 'text', v?.author, chunkLookup); + if (c.type === 'about' && !c.name && !c.description && !c.image) continue; const ts = v?.timestamp || Number(c?.timestamp || 0) || (c?.updatedAt ? Date.parse(c.updatedAt) : 0) || 0; idToAction.set(k, { id: k, author: v?.author, ts, type: inferType(c), content: c }); rawById.set(k, msg); @@ -232,17 +367,23 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { const forgedParentOf = new Map(); const forgedFeedContent = new Map(); + const vg = versionGroups(Array.from(idToAction.values()).map(a => versionEntry({ key: a.id, value: { ...(rawById.get(a.id) || {}).value, content: a.content } }))); for (const [child, parent] of Array.from(parentOf.entries())) { const ca = idToAction.get(child); - const pa = idToAction.get(parent); - if (!pa) { parentOf.delete(child); continue; } - if (!ca || String(ca.author) !== String(pa.author)) { + if (vg.isOrphan(child)) { + parentOf.delete(child); + if (vg.isForeignCopy(child)) { idToAction.delete(child); rawById.delete(child); } + continue; + } + if (!ca || vg.isForeign(child)) { forgedParentOf.set(child, parent); if (ca && ca.type === 'feed') forgedFeedContent.set(child, ca.content || {}); parentOf.delete(child); idToAction.delete(child); rawById.delete(child); + continue; } + if (vg.isForeign(parent)) parentOf.set(child, vg.rootOf(child)); } const replacedIds = new Set(parentOf.values()); @@ -256,7 +397,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { const target = String(c.target || ''); const author = v.author; if (!target || !author) continue; - const key = `${target}${author}`; + const key = `${target}\u0000${author}`; const prev = aiHelpfulSeen.get(key); const curTs = v.timestamp || 0; if (!prev || curTs > prev.ts) aiHelpfulSeen.set(key, { target, ts: curTs, helpful: c.helpful !== false }); @@ -298,6 +439,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } const fetchedTargetCache = new Map(); + const missingTargets = [...new Set([...idToAction.values()].filter(a => a.type === 'spread').map(a => (a.content || {}).vote?.link || '').filter(link => link && !rawById.get(link)))]; + for (const [link, got] of await Promise.all(missingTargets.map(async link => [link, await getMsg(ssbClient, link)]))) { + fetchedTargetCache.set(link, got ? { key: link, value: got } : null); + } for (const a of idToAction.values()) { if (a.type !== 'spread') continue; @@ -457,7 +602,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { const v = msg && msg.value; const c = v && v.content; if (!c || typeof c !== 'object') continue; - const isOpinion = c.type === 'feedOpinion' && typeof c.target === 'string'; + const isOpinion = typeof c.type === 'string' && /Opinion$/.test(c.type) && c.type !== 'pollOpinion' && typeof c.target === 'string'; const isVoteAction = c.type === 'feed-action' && c.action === 'vote' && (typeof c.root === 'string' || typeof c.target === 'string'); if (!isOpinion && !isVoteAction) continue; const target = isOpinion ? c.target : String(c.root || c.target); @@ -487,21 +632,19 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } const latest = []; + const docUrls = [...new Set([...idToAction.values()].filter(a => a.type === 'document').map(a => (a.content || {}).url))]; + const docBlobOk = new Map(await Promise.all(docUrls.map(async url => [url, await hasBlob(ssbClient, url)]))); for (const a of idToAction.values()) { if (tombstoned.has(a.id)) continue; if (a.type === 'tribe' && parentOf.has(a.id)) continue; const c = a.content || {}; - if (c.root && tombstoned.has(c.root)) continue; + if (c.root && tombstoned.has(c.root) && !c.replaces) continue; if (a.type === 'vote' && tombstoned.has(c.vote?.link)) continue; if (a.type === 'spread' && (c.spreadTargetId || c.vote?.link) && tombstoned.has(c.spreadTargetId || c.vote?.link)) continue; if (c.key && tombstoned.has(c.key)) continue; if (c.branch && tombstoned.has(c.branch)) continue; if (c.target && tombstoned.has(c.target)) continue; - if (a.type === 'document') { - const url = c.url; - const ok = await hasBlob(ssbClient, url); - if (!ok) continue; - } + if (a.type === 'document' && !docBlobOk.get(c.url)) continue; if (a.type === 'forum' && c.root) { const rootId = typeof c.root === 'string' ? c.root : (c.root?.key || c.root?.id || ''); const rootAction = idToAction.get(rootId); @@ -511,7 +654,25 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { const actionRoot = rootOf(a.id); const extra = a.type === 'aiExchange' ? { helpfulVotes: aiHelpfulCounts.get(a.id) || 0 } : {}; const voteAgg = a.type === 'votes' ? voteTally.get(a.id) : null; + if (a.type === 'poll') { + const tally = pollTally.get(a.id) || pollTally.get(actionRoot); + a.pollCounts = tally ? tally.counts : {}; + a.pollVoters = tally ? tally.voters.size : 0; + } let content = voteAgg ? { ...c, ...voteAgg } : a.content; + if (a.type !== 'feed' && a.type !== 'votes' && (feedOpinionsByRoot.get(actionRoot) || []).length) { + const base = a.content || {}; + const opinions = { ...(base.opinions || {}) }; + const voters = Array.isArray(base.opinions_inhabitants) ? base.opinions_inhabitants.slice() : []; + const voterSet = new Set(voters); + for (const op of feedOpinionsByRoot.get(actionRoot)) { + if (!op.author || voterSet.has(op.author)) continue; + voterSet.add(op.author); + voters.push(op.author); + if (op.category) opinions[op.category] = (Number(opinions[op.category]) || 0) + 1; + } + content = { ...content, opinions, opinions_inhabitants: voters }; + } if (a.type === 'feed') { const base = a.content || {}; const opinions = { ...(base.opinions || {}) }; @@ -541,6 +702,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { let deduped = latest.filter(a => !a.tipId || a.tipId === a.id || (a.type === 'tribe' && !parentOf.has(a.id))); const mediaTypes = new Set(['image','video','audio','document','bookmark','map']); + const textTypes = new Set(['feed', 'post']); + const REPEAT_WINDOW_MS = 10 * 60 * 1000; const perAuthorUnique = new Set(); const byKey = new Map(); const norm = s => String(s || '').trim().toLowerCase(); @@ -580,6 +743,19 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } else if (prevHasImage === newHasImage && effTs > prev.__effTs) { byKey.set(key, { ...a, __effTs: effTs, __hasImage: newHasImage }); } + } else if (textTypes.has(a.type)) { + const t = norm(c.text); + const key = t ? `${a.type}:${a.author}:${t}` : null; + const prev = key ? byKey.get(key) : null; + if (!key) { + byKey.set(`id:${a.id}`, { ...a, __effTs: effTs }); + } else if (!prev) { + byKey.set(key, { ...a, __effTs: effTs }); + } else if (Math.abs(effTs - prev.__effTs) <= REPEAT_WINDOW_MS) { + if (effTs > prev.__effTs) byKey.set(key, { ...a, __effTs: effTs }); + } else { + byKey.set(`id:${a.id}`, { ...a, __effTs: effTs }); + } } else if (a.type === 'tribe') { const t = norm(c.title); if (t) { @@ -598,11 +774,37 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { deduped = Array.from(byKey.values()).map(x => { delete x.__effTs; delete x.__hasImage; return x }); + if (industryModel && deduped.some(a => a.type === 'industryBuild')) { + const builds = await industryModel.listAllBuilds().catch(() => []); + const buildStatusById = new Map(); + for (const b of (builds || [])) buildStatusById.set(b.id || b.key, String(b.status || 'PROPOSED').toUpperCase()); + deduped = deduped.filter(a => { + if (a.type !== 'industryBuild') return true; + const st = buildStatusById.get(a.id); + if (st === undefined || st === 'PROPOSED' || st === 'REJECTED') return false; + a.content = { ...a.content, buildStatus: st }; + return true; + }); + } + + if (industryModel && deduped.some(a => a.type === 'industryBlueprint')) { + const bps = await industryModel.listAllBlueprints().catch(() => []); + const bpById = new Map((bps || []).map(b => [b.id || b.key, b])); + for (const a of deduped) { + if (a.type !== 'industryBlueprint') continue; + const b = bpById.get(a.id); + if (b && b.estTotal != null) a.content = { ...a.content, estTotal: b.estTotal }; + } + } + const tribeInternalTypes = new Set(['tribe-content', 'tribeParliamentCandidature', 'tribeParliamentTerm', 'tribeParliamentProposal', 'tribeParliamentRule', 'tribeParliamentLaw', 'tribeParliamentRevocation']); - const hiddenTypes = new Set(['padEntry', 'chatMessage', 'calendarDate', 'calendarNote', 'calendarReminderSent', 'taskReminderSent', 'feed-action', 'pubBalance', 'pubAvailability', 'log', 'gameScore']); + const hiddenTypes = new Set(['wallet', 'bankWallet', 'padEntry', 'roomMember', 'chatMessage', 'calendarDate', 'calendarNote', 'calendarReminderSent', 'taskReminderSent', 'feed-action', 'pubBalance', 'pubAvailability', 'log', 'logPublic', 'gameScore', 'pollVote', 'pollClose', 'pollOpinion', 'curriculum', 'schoolLesson', 'schoolEnroll', 'schoolCertificate', 'schoolProgress', 'schoolExam', 'schoolExamResult', 'school-invite', 'schoolExamQuestion', 'schoolLessonMedia']); + const chatThreadItems = await buildChatThreads(ssbClient, idToAction, rootOf, deduped); + deduped = deduped.concat(chatThreadItems); const isAllowedTribeActivity = (a) => { if (tribeInternalTypes.has(a.type)) return false; const c = a.content || {}; + if (a.type === 'poll' && c.chatId) return false; if (c.tribeId) return false; if (a.type === 'tribe') { const isInitial = !c.replaces; @@ -613,25 +815,31 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } return true; }; - const itemVisible = (a) => { - if (hiddenTypes.has(a.type)) return false; + const itemVisible = (a, opts = {}) => { + if (hiddenTypes.has(a.type) && !(opts.allowHidden && opts.allowHidden.has(a.type))) return false; const c = a.content || {}; if (c.encryptedPayload) return false; if (a.type === 'pad' && c.status !== 'OPEN') return false; + if (a.type === 'room' && (c.status !== 'OPEN' || c.encrypted === true)) return false; if (a.type === 'chat' && c.status !== 'OPEN') return false; if (a.type === 'calendar' && c.status !== 'OPEN') return false; if (a.type === 'event' && String(c.isPublic || '').toLowerCase() === 'private' && c.organizer !== userId && !(Array.isArray(c.attendees) && c.attendees.includes(userId))) return false; if (a.type === 'task' && String(c.isPublic || '').toUpperCase() === 'PRIVATE' && a.author !== userId && !(Array.isArray(c.assignees) && c.assignees.includes(userId))) return false; if (a.type === 'forum' && c.isPrivate === true && a.author !== userId) return false; if (a.type === 'job' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId && !(Array.isArray(c.subscribers) && c.subscribers.includes(userId))) return false; + if (a.type === 'schoolCourse' && c.encryptedPayload) return false; + if (a.type === 'schoolCourse' && String(c.visibility || '').toUpperCase() === 'INVITE' && a.author !== userId && !(Array.isArray(c.invited) && c.invited.includes(userId))) return false; + if (a.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false; if (a.type === 'market' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.seller !== userId) return false; + if (a.type === 'market' && c.shopProductId) return false; if (a.type === 'shop' && String(c.visibility || '').toUpperCase() === 'CLOSED' && a.author !== userId) return false; if (a.type === 'curriculum' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false; if (a.type === 'shopProduct' && c.shopVisibility && String(c.shopVisibility).toUpperCase() === 'CLOSED' && a.author !== userId) return false; + if (a.type === 'transfer' && String(c.status || '').toUpperCase() === 'UNCONFIRMED' && a.author !== userId && c.from !== userId && c.to !== userId) return false; return true; }; - const isVisible = (a) => { - if (!itemVisible(a)) return false; + const isVisible = (a, opts) => { + if (!itemVisible(a, opts)) return false; if (a.type === 'post' || a.type === 'opinion') { const c = a.content || {}; const ref = (typeof c.root === 'string' && c.root) @@ -650,7 +858,14 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { if (filter === 'mine') out = deduped.filter(a => a.author === userId && isAllowedTribeActivity(a) && isVisible(a)); else if (filter === 'recent') { const cutoff = Date.now() - 24 * 60 * 60 * 1000; out = deduped.filter(a => (a.ts || 0) >= cutoff && isAllowedTribeActivity(a) && isVisible(a)) } else if (filter === 'all') out = deduped.filter(a => isAllowedTribeActivity(a) && isVisible(a)); - else if (filter === 'banking') out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim' || a.type === 'ubiClaim'); + else if (filter === 'top') { + const visible = deduped.filter(a => isAllowedTribeActivity(a) && isVisible(a)); + const perAuthor = new Map(); + for (const a of visible) perAuthor.set(a.author, (perAuthor.get(a.author) || 0) + 1); + out = visible + .map(a => ({ ...a, authorActions: perAuthor.get(a.author) || 0 })) + .sort((x, y) => (y.authorActions - x.authorActions) || ((y.ts || 0) - (x.ts || 0))); + } else if (filter === 'tribe') out = deduped.filter(a => a.type === 'tribe' || String(a.type || '').startsWith('tribe')); else if (filter === 'spread') out = deduped.filter(a => a.type === 'spread'); else if (filter === 'parliament') @@ -664,18 +879,30 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { }); else if (filter === 'task') out = deduped.filter(a => a.type === 'task' || a.type === 'taskAssignment'); + else if (filter === 'votes') + out = deduped.filter(a => (a.type === 'votes' || a.type === 'poll') && isAllowedTribeActivity(a) && isVisible(a)); + else if (filter === 'industry') + out = deduped.filter(a => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(a.type) && isVisible(a)); else if (filter === 'pad') out = deduped.filter(a => a.type === 'pad' && (a.content || {}).status === 'OPEN'); - else if (filter === 'chat') out = deduped.filter(a => a.type === 'chat' && (a.content || {}).status === 'OPEN'); + else if (filter === 'room') out = deduped.filter(a => a.type === 'room' && (a.content || {}).status === 'OPEN' && (a.content || {}).encrypted !== true); + else if (filter === 'wiki') out = deduped.filter(a => a.type === 'wikiPage' && isVisible(a)); + else if (filter === 'emergency') out = deduped.filter(a => (a.type === 'emergency' || a.type === 'emergencyUpdate') && isVisible(a)); + else if (filter === 'campaign') out = deduped.filter(a => (a.type === 'campaign' || a.type === 'campaignUpdate') && isVisible(a)); + else if (filter === 'mailing') out = deduped.filter(a => a.type === 'mailingList' && isVisible(a)); + else if (filter === 'logistics') out = deduped.filter(a => a.type === 'logisticsRoute' && isVisible(a)); + else if (filter === 'podcast') out = deduped.filter(a => (a.type === 'podcast' || a.type === 'podcastEpisode') && isVisible(a)); + else if (filter === 'chat') out = deduped.filter(a => (a.type === 'chat' || a.type === 'chatThread') && isAllowedTribeActivity(a) && isVisible(a)); else if (filter === 'calendar') out = deduped.filter(a => a.type === 'calendar' && (a.content || {}).status === 'OPEN'); + else if (filter === 'transfer') out = deduped.filter(a => a.type === 'transfer' && isVisible(a)); else out = deduped.filter(a => a.type === filter); - out.sort((a, b) => (b.ts || 0) - (a.ts || 0)); + if (filter !== 'top') out.sort((a, b) => (b.ts || 0) - (a.ts || 0)); return out; })(); _feedCacheInflight.set(cacheKey, promise); try { const value = await promise; - _feedCache.set(cacheKey, { value, ts: Date.now() }); + _feedCache.set(cacheKey, { value, ts: Date.now(), tip: tipNow }); return value; } finally { _feedCacheInflight.delete(cacheKey); diff --git a/src/models/agenda_model.js b/src/models/agenda_model.js index 863b8a5..e1b3f67 100644 --- a/src/models/agenda_model.js +++ b/src/models/agenda_model.js @@ -3,9 +3,11 @@ const path = require('path'); const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); -const agendaConfigPath = path.join(__dirname, '../configs/agenda-config.json'); +const agendaConfigPath = require('../configs/state-manager').statePath('agenda-config.json'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; function readAgendaConfig() { @@ -19,137 +21,117 @@ function writeAgendaConfig(cfg) { fs.writeFileSync(agendaConfigPath, JSON.stringify(cfg, null, 2)); } -module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel }) => { +module.exports = ({ cooler, campaignsModel = null, logisticsModel = null, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel, industryModel, housingModel, schoolModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; const STATUS_ORDER = ['FOR SALE', 'OPEN', 'RESERVED', 'CLOSED', 'SOLD']; const sIdx = s => STATUS_ORDER.indexOf(String(s || '').toUpperCase()); - const fetchItems = (targetType) => - new Promise((resolve, reject) => { - openSsb().then((ssbClient) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => { - if (err) return reject(err); + const fetchItems = async (targetType) => { + const ssbClient = await openSsb(); + const msgs = await readTyped(ssbClient, [targetType, 'tombstone'], { limit: logLimit }); - const tomb = buildValidatedTombstoneSet(msgs); - const nodes = new Map(); - const parent = new Map(); - const child = new Map(); + const tomb = buildValidatedTombstoneSet(msgs); + const nodes = new Map(); + const versions = []; - for (const m of msgs) { - const k = m.key; - const v = m.value; - const c = v?.content; - if (!c) continue; - if (c.type === 'tombstone') continue; - if (c.type !== targetType) continue; - if (c.encryptedPayload) continue; - nodes.set(k, { key: k, ts: v.timestamp || 0, author: v.author, content: c }); - if (c.replaces) parent.set(k, c.replaces); - } + for (const m of msgs) { + const k = m.key; + const v = m.value; + const c = v?.content; + if (!c) continue; + if (c.type === 'tombstone') continue; + if (c.type !== targetType) continue; + if (c.encryptedPayload) continue; + nodes.set(k, { key: k, ts: v.timestamp || 0, author: v.author, content: c }); + versions.push(versionEntry(m)); + } - for (const [k, p] of Array.from(parent.entries())) { - const cn = nodes.get(k); - const pn = nodes.get(p); - if (!pn) { parent.delete(k); continue; } - if (!cn || String(cn.author) !== String(pn.author)) { parent.delete(k); nodes.delete(k); } - } - for (const [k, p] of parent.entries()) child.set(p, k); + const vg = versionGroups(versions); + const groups = new Map(); + for (const r of vg.roots) groups.set(r, new Set(vg.groups.get(r).filter(id => !vg.isForeign(id)))); - const rootOf = (id) => { let cur = id; while (parent.has(cur) && nodes.has(parent.get(cur))) cur = parent.get(cur); return cur; }; + const statusOrder = ['FOR SALE', 'OPEN', 'RESERVED', 'CLOSED', 'SOLD']; + const sIdx = s => statusOrder.indexOf(String(s || '').toUpperCase()); - const groups = new Map(); - for (const id of nodes.keys()) { - const r = rootOf(id); - if (!groups.has(r)) groups.set(r, new Set()); - groups.get(r).add(id); - } + const out = []; - const statusOrder = ['FOR SALE', 'OPEN', 'RESERVED', 'CLOSED', 'SOLD']; - const sIdx = s => statusOrder.indexOf(String(s || '').toUpperCase()); + for (const [root, ids] of groups.entries()) { + const items = Array.from(ids).map(id => nodes.get(id)).filter(n => n && !tomb.has(n.key)); + if (!items.length) continue; - const out = []; + const tip = nodes.get(vg.tipOf(root)); + if (tomb.has(tip.key)) continue; - for (const [root, ids] of groups.entries()) { - const items = Array.from(ids).map(id => nodes.get(id)).filter(n => n && !tomb.has(n.key)); - if (!items.length) continue; + if (targetType === 'market') { + let chosen = items[0]; + for (const n of items) { + const a = sIdx(n.content.status); + const b = sIdx(chosen.content.status); + if (a > b || (a === b && n.ts > chosen.ts)) chosen = n; + } + const c = chosen.content; + let status = c.status; + if (c.deadline) { + const dl = moment(c.deadline); + if (dl.isValid() && dl.isBefore(moment()) && String(status).toUpperCase() !== 'SOLD') status = 'DISCARDED'; + } + if (status === 'FOR SALE' && (c.stock || 0) === 0) continue; - let tipId = Array.from(ids).find(id => !child.has(id)); - let tip = tipId ? nodes.get(tipId) : items.reduce((a, b) => a.ts > b.ts ? a : b); + out.push({ + ...c, + status, + author: chosen.author, + id: chosen.key, + tipId: chosen.key, + createdAt: c.createdAt || chosen.ts + }); + continue; + } - if (targetType === 'market') { - let chosen = items[0]; - for (const n of items) { - const a = sIdx(n.content.status); - const b = sIdx(chosen.content.status); - if (a > b || (a === b && n.ts > chosen.ts)) chosen = n; - } - const c = chosen.content; - let status = c.status; - if (c.deadline) { - const dl = moment(c.deadline); - if (dl.isValid() && dl.isBefore(moment()) && String(status).toUpperCase() !== 'SOLD') status = 'DISCARDED'; - } - if (status === 'FOR SALE' && (c.stock || 0) === 0) continue; + if (targetType === 'job') { + const latest = items.sort((a, b) => b.ts - a.ts)[0]; + const withSubsNode = items + .filter(n => Array.isArray(n.content.subscribers)) + .sort((a, b) => b.ts - a.ts)[0]; + const subscribers = withSubsNode ? withSubsNode.content.subscribers : []; + const latestWithStatus = items + .filter(n => typeof n.content.status !== 'undefined') + .sort((a, b) => b.ts - a.ts)[0]; + const resolvedStatus = latestWithStatus + ? latestWithStatus.content.status + : latest.content.status; - out.push({ - ...c, - status, - author: chosen.author, - id: chosen.key, - tipId: chosen.key, - createdAt: c.createdAt || chosen.ts - }); - continue; - } + const c = { ...latest.content, status: resolvedStatus, subscribers }; - if (targetType === 'job') { - const latest = items.sort((a, b) => b.ts - a.ts)[0]; - const withSubsNode = items - .filter(n => Array.isArray(n.content.subscribers)) - .sort((a, b) => b.ts - a.ts)[0]; - const subscribers = withSubsNode ? withSubsNode.content.subscribers : []; - const latestWithStatus = items - .filter(n => typeof n.content.status !== 'undefined') - .sort((a, b) => b.ts - a.ts)[0]; - const resolvedStatus = latestWithStatus - ? latestWithStatus.content.status - : latest.content.status; + out.push({ + ...c, + author: latest.author, + id: latest.key, + tipId: latest.key, + createdAt: c.createdAt || latest.ts + }); + continue; + } - const c = { ...latest.content, status: resolvedStatus, subscribers }; + out.push({ + ...tip.content, + author: tip.author, + id: tip.key, + tipId: tip.key, + createdAt: tip.content.createdAt || tip.ts + }); + } - out.push({ - ...c, - author: latest.author, - id: latest.key, - tipId: latest.key, - createdAt: c.createdAt || latest.ts - }); - continue; - } - - out.push({ - ...tip.content, - author: tip.author, - id: tip.key, - tipId: tip.key, - createdAt: tip.content.createdAt || tip.ts - }); - } - - resolve(out); - }) - ); - }).catch(reject); - }); + return out; + }; return { async listAgenda(filter = 'all') { const agendaConfig = readAgendaConfig(); const discardedItems = agendaConfig.discardedItems || []; + const removedItems = agendaConfig.removedItems || []; const ssbClient = await openSsb(); const userId = ssbClient.id; @@ -184,7 +166,27 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel ? projectsModel.listProjects('all').then(normalize).catch(() => []) : fetchItems('project'); - const [tasksAll, eventsAll, transfersAll, tribesAll, marketAll, reportsAll, jobsAll, projectsAll, calendarsAll] = await Promise.all([ + const industryViaModel = industryModel && typeof industryModel.listMyBuilds === 'function' + ? industryModel.listMyBuilds().then(normalize).catch(() => []) + : []; + + const housingViaModel = housingModel && typeof housingModel.listHousing === 'function' + ? housingModel.listHousing('ALL', userId).then(normalize).catch(() => []) + : []; + + const schoolViaModel = schoolModel && typeof schoolModel.listCourses === 'function' + ? schoolModel.listCourses('ALL', userId, {}).then(normalize).catch(() => []) + : []; + + const campaignsViaModel = campaignsModel && typeof campaignsModel.listAll === 'function' + ? campaignsModel.listAll({ filter: 'all' }).catch(() => []) + : []; + + const logisticsViaModel = logisticsModel && typeof logisticsModel.listAll === 'function' + ? logisticsModel.listAll({ filter: 'all' }).catch(() => []) + : []; + + const [tasksAll, eventsAll, transfersAll, tribesAll, marketAll, reportsAll, jobsAll, projectsAll, calendarsAll, industryAll, housingAll, schoolAll, campaignsAll, logisticsAll] = await Promise.all([ tasksViaModel, eventsViaModel, fetchItems('transfer'), @@ -193,7 +195,12 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel fetchItems('report'), jobsViaModel, projectsViaModel, - calendarsViaModel + calendarsViaModel, + industryViaModel, + housingViaModel, + schoolViaModel, + campaignsViaModel, + logisticsViaModel ]); const tasks = tasksAll.filter(c => Array.isArray(c.assignees) && c.assignees.includes(userId)).map(t => ({ ...t, type: 'task' })); @@ -205,22 +212,36 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel ).map(m => ({ ...m, type: 'market' })); const reports = reportsAll.filter(c => c.author === userId || (Array.isArray(c.confirmations) && c.confirmations.includes(userId))).map(r => ({ ...r, type: 'report' })); const jobs = jobsAll.filter(c => c.author === userId || (Array.isArray(c.subscribers) && c.subscribers.includes(userId))).map(j => ({ ...j, type: 'job', title: j.title })); - const projects = projectsAll.map(p => ({ ...p, type: 'project' })); + const projects = projectsAll.filter(p => p.author === userId || (Array.isArray(p.followers) && p.followers.includes(userId)) || (Array.isArray(p.backers) && p.backers.some(b => b && b.userId === userId)) || (Array.isArray(p.bounties) && p.bounties.some(b => b && b.claimedBy === userId))).map(p => ({ ...p, type: 'project' })); + const industryBuilds = industryAll.map(b => ({ ...b, type: 'industry', title: b.title })); + const schoolCourses = schoolAll + .filter(c => c.author === userId || (Array.isArray(c.students) && c.students.includes(userId)) || (Array.isArray(c.pending) && c.pending.some(pn => pn.author === userId))) + .map(c => ({ ...c, type: 'schoolCourse', title: c.title, date: c.startDate || c.createdAt, teaching: c.author === userId })); + const campaigns = (Array.isArray(campaignsAll) ? campaignsAll : []) + .filter(c => c && c.deadline && (c.author === userId || (Array.isArray(c.signers) && c.signers.includes(userId)))) + .map(c => ({ ...c, type: 'campaign', date: c.deadline, status: c.closed ? 'CLOSED' : 'OPEN' })); + const routes = (Array.isArray(logisticsAll) ? logisticsAll : []) + .filter(r => r && r.date && (r.author === userId || !!r.myBooking)) + .map(r => ({ ...r, type: 'logisticsRoute', status: r.status === 'CLOSED' ? 'CLOSED' : 'OPEN' })); + const housingPlaces = housingAll + .filter(h => h.author === userId || (Array.isArray(h.requests) && h.requests.includes(userId))) + .map(h => ({ ...h, type: 'housing', date: h.availableFrom || h.createdAt, requested: Array.isArray(h.requests) && h.requests.includes(userId) })); const myCalendars = calendarsAll .filter(c => c.author === userId || (Array.isArray(c.participants) && c.participants.includes(userId))); - const calendars = myCalendars.map(c => ({ ...c, type: 'calendar' })); + const calendars = myCalendars.map(c => ({ ...c, id: c.rootId || c.id || c.key, type: 'calendar' })); const calendarDates = []; if (calendarsModel && typeof calendarsModel.getDatesForCalendar === 'function') { - for (const cal of myCalendars) { + const datesByCal = await Promise.all(myCalendars.map(cal => calendarsModel.getDatesForCalendar(cal.rootId || cal.id || cal.key).catch(() => []))); + for (const [ci, cal] of myCalendars.entries()) { try { - const dates = await calendarsModel.getDatesForCalendar(cal.id || cal.key); + const dates = datesByCal[ci]; for (const d of (dates || [])) { calendarDates.push({ type: 'calendarDate', id: d.key || d.id, title: d.label || cal.title || 'Calendar date', calendarTitle: cal.title || '', - calendarId: cal.id || cal.key, + calendarId: cal.rootId || cal.id || cal.key, date: d.date || d.createdAt, label: d.label || '', author: cal.author, @@ -241,9 +262,14 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel ...reports, ...jobs, ...projects, + ...industryBuilds, + ...housingPlaces, + ...schoolCourses, + ...campaigns, + ...routes, ...calendars, ...calendarDates - ]; + ].filter(i => !removedItems.includes(i.id)); let filtered; if (filter === 'discarded') { @@ -260,6 +286,11 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel else if (filter === 'closed') filtered = filtered.filter(i => String(i.status).toUpperCase() === 'CLOSED'); else if (filter === 'jobs') filtered = filtered.filter(i => i.type === 'job'); else if (filter === 'projects') filtered = filtered.filter(i => i.type === 'project'); + else if (filter === 'industry') filtered = filtered.filter(i => i.type === 'industry'); + else if (filter === 'housing') filtered = filtered.filter(i => i.type === 'housing'); + else if (filter === 'school') filtered = filtered.filter(i => i.type === 'schoolCourse'); + else if (filter === 'campaigns') filtered = filtered.filter(i => i.type === 'campaign'); + else if (filter === 'logistics') filtered = filtered.filter(i => i.type === 'logisticsRoute'); else if (filter === 'calendars') filtered = filtered.filter(i => i.type === 'calendar' || i.type === 'calendarDate'); else if (filter === 'today') { const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0); @@ -313,6 +344,11 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel reports: mainItems.filter(i => i.type === 'report').length, jobs: mainItems.filter(i => i.type === 'job').length, projects: mainItems.filter(i => i.type === 'project').length, + industry: mainItems.filter(i => i.type === 'industry').length, + housing: mainItems.filter(i => i.type === 'housing').length, + school: mainItems.filter(i => i.type === 'schoolCourse').length, + campaigns: mainItems.filter(i => i.type === 'campaign').length, + logistics: mainItems.filter(i => i.type === 'logisticsRoute').length, calendars: mainItems.filter(i => i.type === 'calendar' || i.type === 'calendarDate').length, today: mainItems.filter(i => { const d = itemTs(i); return d >= startOfDay.getTime() && d <= endOfDay.getTime(); }).length, upcoming: mainItems.filter(i => itemTs(i) > now).length, @@ -334,6 +370,14 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel const agendaConfig = readAgendaConfig(); agendaConfig.discardedItems = agendaConfig.discardedItems.filter(id => id !== itemId); writeAgendaConfig(agendaConfig); + }, + + async removeItem(itemId) { + const agendaConfig = readAgendaConfig(); + agendaConfig.discardedItems = (agendaConfig.discardedItems || []).filter(id => id !== itemId); + agendaConfig.removedItems = agendaConfig.removedItems || []; + if (!agendaConfig.removedItems.includes(itemId)) agendaConfig.removedItems.push(itemId); + writeAgendaConfig(agendaConfig); } }; }; diff --git a/src/models/audios_model.js b/src/models/audios_model.js index 13f66d6..cf65107 100644 --- a/src/models/audios_model.js +++ b/src/models/audios_model.js @@ -1,6 +1,7 @@ const pull = require("../server/node_modules/pull-stream"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); @@ -31,10 +32,9 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))); - }); + const AUDIO_TYPES = ["audio", "audioOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, AUDIO_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -44,9 +44,6 @@ module.exports = ({ cooler }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -62,41 +59,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), 'utf8'); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -113,9 +85,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildAudio = (node, rootId, viewerId, agg) => { @@ -132,9 +104,10 @@ module.exports = ({ cooler }) => { createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, tags: tagsArr, - author: c.author, + author: node.author, title: c.title || "", description: c.description || "", + license: c.license || "", mapUrl: c.mapUrl || "", opinions, opinions_inhabitants: voters, @@ -163,16 +136,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Audio not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Audio not found"); return root; }, - async createAudio(blobMarkdown, tagsRaw, title, description, mapUrl) { + async createAudio(blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -189,7 +158,8 @@ module.exports = ({ cooler }) => { description: description || "", mapUrl: mapUrl || "", opinions: {}, - opinions_inhabitants: [] + opinions_inhabitants: [], + ...(license ? { license } : {}) }; return new Promise((resolve, reject) => { @@ -251,7 +221,7 @@ module.exports = ({ cooler }) => { }); }, - async updateAudioById(id, blobMarkdown, tagsRaw, title, description, mapUrl) { + async updateAudioById(id, blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -275,7 +245,8 @@ module.exports = ({ cooler }) => { description: description !== undefined ? description || "" : oldMsg.content.description || "", mapUrl: mapUrl !== undefined ? mapUrl || "" : oldMsg.content.mapUrl || "", createdAt: oldMsg.content.createdAt, - updatedAt: now + updatedAt: now, + ...(license !== undefined ? { license } : {}) }; const tombstone = { type: "tombstone", target: tipId, deletedAt: now, author: userId }; @@ -358,11 +329,11 @@ module.exports = ({ cooler }) => { const ssbClient = await openSsb(); const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('audios', messages, buildIndex); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Audio not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Audio not found"); return buildAudio(agg.contentNode, rootId, viewer, agg); }, diff --git a/src/models/backup_model.js b/src/models/backup_model.js new file mode 100644 index 0000000..175e028 --- /dev/null +++ b/src/models/backup_model.js @@ -0,0 +1,779 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const zlib = require('zlib'); +const crypto = require('crypto'); +const { pipeline } = require('stream'); +const pull = require('../server/node_modules/pull-stream'); +const toPull = require('../server/node_modules/stream-to-pull-stream'); + +const KEY_MAGIC = Buffer.from('OASIS1'); +const BACKUP_MAGIC = Buffer.from('OASISBK1'); +const SNAPSHOT_MAGIC = Buffer.from('OASISSN1'); +const SNAPSHOT_MAX_BYTES = 2 * 1024 * 1024 * 1024; +const SALT_LEN = 16; +const IV_LEN = 12; +const TAG_LEN = 16; +const REC_META = 1; +const REC_MSG = 2; +const REC_BLOB = 3; +const REC_FILE = 4; +const REC_MAX_LEN = 256 * 1024 * 1024; + +const stateRoot = () => { + try { return path.join(require('../configs/state-manager').ssbDir(), 'oasis'); } catch (_) { return null; } +}; + +const NOT_BACKED_UP = new Set(['oasis-config.json', 'oasis-server-config.json', 'rooms']); +const listStateFiles = (dir, base = dir) => { + const out = []; + let entries = []; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return out; } + for (const entry of entries) { + if (dir === base && NOT_BACKED_UP.has(entry.name)) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) out.push(...listStateFiles(full, base)); + else if (entry.isFile() && !entry.name.endsWith('.migrated') && !/^snapshot(-recent)?\.oasissn/.test(entry.name)) out.push({ rel: path.relative(base, full), full }); + } + return out; +}; +const BLOB_RE = /&[A-Za-z0-9+/=]{44}\.sha256/g; +const DEFAULT_REMINDER_DAYS = 30; +const REMINDER_OPTIONS = [7, 30, 90, 0]; + +const MODULE_GROUPS = [ + { key: 'feed', test: (t) => /^(post|feed|about|contact|pub|vote|subscription)$/.test(t) }, + { key: 'media', test: (t) => /^(audio|video|image|document|bookmark|torrent|file)/i.test(t) }, + { key: 'blogs', test: (t) => /^blog/i.test(t) }, + { key: 'forum', test: (t) => /^forum/i.test(t) }, + { key: 'chats', test: (t) => /^chat/i.test(t) }, + { key: 'pads', test: (t) => /^pad/i.test(t) }, + { key: 'rooms', test: (t) => /^room/i.test(t) }, + { key: 'wiki', test: (t) => /^wiki/i.test(t) }, + { key: 'maps', test: (t) => /^map/i.test(t) }, + { key: 'tribes', test: (t) => /^tribe/i.test(t) }, + { key: 'school', test: (t) => /^school/i.test(t) }, + { key: 'parliament', test: (t) => /^parliament/i.test(t) }, + { key: 'courts', test: (t) => /^courts/i.test(t) }, + { key: 'emergencies', test: (t) => /^emergency/i.test(t) }, + { key: 'votes', test: (t) => /^(votes?|poll)/i.test(t) }, + { key: 'events', test: (t) => /^event/i.test(t) }, + { key: 'calendars', test: (t) => /^calendar/i.test(t) }, + { key: 'tasks', test: (t) => /^task/i.test(t) }, + { key: 'reports', test: (t) => /^report/i.test(t) }, + { key: 'mailing', test: (t) => /^mailing/i.test(t) }, + { key: 'campaigns', test: (t) => /^campaign/i.test(t) }, + { key: 'market', test: (t) => /^market/i.test(t) }, + { key: 'shops', test: (t) => /^shop/i.test(t) }, + { key: 'jobs', test: (t) => /^(job|curriculum)/i.test(t) }, + { key: 'housing', test: (t) => /^housing/i.test(t) }, + { key: 'projects', test: (t) => /^project/i.test(t) }, + { key: 'industry', test: (t) => /^industry/i.test(t) }, + { key: 'banking', test: (t) => /^(bank|ubi|wallet)/i.test(t) }, + { key: 'transfers', test: (t) => /^transfer/i.test(t) }, + { key: 'logistics', test: (t) => /^logistics/i.test(t) }, + { key: 'podcasts', test: (t) => /^podcast/i.test(t) }, + { key: 'games', test: (t) => /^(game|larp|pixelia|melody)/i.test(t) }, + { key: 'logs', test: (t) => /^log/i.test(t) } +]; +const MODULE_KEYS = [...MODULE_GROUPS.map(g => g.key), 'inbox', 'other']; + +const moduleOf = (content) => { + if (typeof content === 'string') return 'inbox'; + const t = content && typeof content === 'object' ? String(content.type || '') : ''; + if (!t) return 'other'; + const g = MODULE_GROUPS.find(x => x.test(t)); + return g ? g.key : 'other'; +}; + +const normalizePassword = (password) => { + if (password && typeof password === 'object' && password.password) password = password.password; + return String(password || ''); +}; +const deriveKey = (password, salt) => crypto.scryptSync(password, salt, 32); + +const encryptBuffer = (plaintext, password) => { + const salt = crypto.randomBytes(SALT_LEN); + const iv = crypto.randomBytes(IV_LEN); + const cipher = crypto.createCipheriv('aes-256-gcm', deriveKey(password, salt), iv); + const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); + return Buffer.concat([KEY_MAGIC, salt, iv, cipher.getAuthTag(), ciphertext]); +}; + +const decryptBuffer = (data, password) => { + if (!Buffer.isBuffer(data) || data.length < KEY_MAGIC.length + SALT_LEN + IV_LEN + TAG_LEN) throw new Error('Unrecognized or corrupt backup file.'); + if (!data.slice(0, KEY_MAGIC.length).equals(KEY_MAGIC)) throw new Error('Unrecognized or corrupt backup file.'); + let o = KEY_MAGIC.length; + const salt = data.slice(o, o += SALT_LEN); + const iv = data.slice(o, o += IV_LEN); + const tag = data.slice(o, o += TAG_LEN); + const decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(password, salt), iv); + decipher.setAuthTag(tag); + return Buffer.concat([decipher.update(data.slice(o)), decipher.final()]); +}; + +const ssbDirOf = () => require('../server/ssb_config').path; +const secretPathOf = () => path.join(ssbDirOf(), 'secret'); + +const stateFile = () => require('../configs/state-manager').statePath('oasis-backup.json'); +const readState = () => { + try { return JSON.parse(fs.readFileSync(stateFile(), 'utf8')) || {}; } catch (_) { return {}; } +}; +const writeState = (next) => { + try { + const p = stateFile(); + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, JSON.stringify(next, null, 2), { mode: 0o600 }); + } catch (_) {} + return next; +}; + + +const reminderState = () => { + const b = readState(); + const days = b.reminderDays === undefined ? DEFAULT_REMINDER_DAYS : Number(b.reminderDays); + const lastAt = b.lastAt || null; + const lastTs = lastAt ? Date.parse(lastAt) || 0 : 0; + const overdue = days > 0 && (!lastTs || Date.now() - lastTs > days * 86400000); + return { lastAt, reminderDays: days, overdue, daysSince: lastTs ? Math.floor((Date.now() - lastTs) / 86400000) : null }; +}; +const setReminderDays = (days) => { + const n = Number(days); + return writeState({ ...readState(), reminderDays: REMINDER_OPTIONS.includes(n) ? n : DEFAULT_REMINDER_DAYS }); +}; +const markBackup = () => writeState({ ...readState(), lastAt: new Date().toISOString() }); + +const blobRefsOf = (value) => { + let json; + try { json = JSON.stringify(value); } catch (_) { return []; } + return Array.from(new Set(json.match(BLOB_RE) || [])); +}; + +const record = (type, payload) => { + const head = Buffer.alloc(5); + head.writeUInt8(type, 0); + head.writeUInt32BE(payload.length, 1); + return Buffer.concat([head, payload]); +}; + +const writeChunk = (stream, chunk) => new Promise((resolve, reject) => { + const ok = stream.write(chunk, (err) => err ? reject(err) : null); + if (ok) resolve(); + else stream.once('drain', resolve); +}); + +module.exports = ({ cooler }) => { + let ssb; + let restoreJob = null; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const readLog = async (ssbClient) => new Promise((resolve, reject) => { + pull(ssbClient.createLogStream({ reverse: false }), pull.collect((err, arr) => err ? reject(err) : resolve(arr || []))); + }); + + const listBlobs = async (ssbClient) => { + if (!ssbClient.blobs || typeof ssbClient.blobs.ls !== 'function') return new Map(); + return new Promise((resolve) => { + pull(ssbClient.blobs.ls({ long: true }), pull.collect((err, arr) => { + if (err) return resolve(new Map()); + const out = new Map(); + for (const b of arr || []) { + if (!b) continue; + if (typeof b === 'string') out.set(b, 0); + else if (b.id) out.set(b.id, Number(b.size) || 0); + } + resolve(out); + })); + }); + }; + + const readBlob = (ssbClient, id) => new Promise((resolve) => { + pull(ssbClient.blobs.get(id), pull.collect((e, chunks) => { + if (e || !chunks || !chunks.length) return resolve(null); + resolve(Buffer.concat(chunks.map(c => Buffer.isBuffer(c) ? c : Buffer.from(c)))); + })); + }); + + const normalizeOptions = (opts = {}) => { + const scope = String(opts.scope || 'all').toLowerCase() === 'mine' ? 'mine' : 'all'; + const withBlobs = opts.blobs === undefined ? true : !(opts.blobs === false || String(opts.blobs) === '0' || String(opts.blobs) === 'false' || String(opts.blobs) === 'off'); + const rawMods = Array.isArray(opts.modules) ? opts.modules : (opts.modules ? String(opts.modules).split(',') : []); + const modules = rawMods.map(m => String(m || '').trim().toLowerCase()).filter(m => MODULE_KEYS.includes(m)); + const sinceTs = opts.since ? (Date.parse(opts.since) || 0) : 0; + return { scope, withBlobs, modules, sinceTs }; + }; + + const selectMessages = async (ssbClient, options) => { + const me = ssbClient.id; + const all = await readLog(ssbClient); + const picked = []; + const blobIds = new Set(); + for (const m of all) { + if (!m || !m.value) continue; + if (options.scope === 'mine' && m.value.author !== me) continue; + if (options.sinceTs && Number(m.value.timestamp || m.timestamp || 0) < options.sinceTs) continue; + if (options.modules.length && !options.modules.includes(moduleOf(m.value.content))) continue; + picked.push(m); + if (options.withBlobs) for (const ref of blobRefsOf(m.value.content)) blobIds.add(ref); + } + if (options.withBlobs && options.scope !== 'mine' && !options.sinceTs && !options.modules.length) { + const sizes = await listBlobs(ssbClient); + for (const id of sizes.keys()) blobIds.add(id); + } + return { picked, blobIds }; + }; + + return { + MODULE_KEYS, + REMINDER_OPTIONS, + moduleOf, + encryptBuffer, + decryptBuffer, + + async exportKeys(password) { + const pw = normalizePassword(password); + if (pw.length < 32) throw new Error('Password too short'); + const secretPath = secretPathOf(); + if (!fs.existsSync(secretPath)) throw new Error(".ssb/secret file doesn't exist"); + const original = fs.readFileSync(secretPath); + const encrypted = encryptBuffer(original, pw); + if (!decryptBuffer(encrypted, pw).equals(original)) throw new Error('Backup verification failed; nothing was exported.'); + return { filename: 'oasis.enc', data: encrypted }; + }, + + async importKeys({ filePath, password }) { + const pw = normalizePassword(password); + if (!fs.existsSync(filePath)) throw new Error('Encrypted file not found.'); + let decrypted; + try { decrypted = decryptBuffer(fs.readFileSync(filePath), pw); } catch (_) { throw new Error('Wrong password or corrupt backup file.'); } + let keys = null; + try { keys = JSON.parse(decrypted.toString('utf8').split('\n').filter(l => !l.trim().startsWith('#')).join('\n')); } catch (_) { keys = null; } + const ssbKeys = require('../server/node_modules/ssb-keys'); + const probe = Buffer.from('oasis-keys-check'); + const valid = (() => { + try { + if (!keys || keys.curve !== 'ed25519' || typeof keys.public !== 'string' || typeof keys.private !== 'string' || keys.id !== '@' + keys.public) return false; + return ssbKeys.verify(keys, ssbKeys.sign(keys, probe), probe) === true; + } catch (_) { return false; } + })(); + if (!valid) throw new Error('The file does not contain a valid Oasis identity.'); + const ssbDir = ssbDirOf(); + fs.mkdirSync(ssbDir, { recursive: true }); + const secretPath = path.join(ssbDir, 'secret'); + if (fs.existsSync(secretPath)) { + const bakPath = path.join(ssbDir, 'secret.bak-' + new Date().toISOString().replace(/[:.]/g, '-')); + fs.copyFileSync(secretPath, bakPath); + try { fs.chmodSync(bakPath, 0o600); } catch (_) {} + } + const tmpPath = path.join(ssbDir, 'secret.tmp-' + process.pid); + fs.writeFileSync(tmpPath, decrypted, { mode: 0o600 }); + fs.renameSync(tmpPath, secretPath); + try { fs.unlinkSync(filePath); } catch (_) {} + let id = ''; + try { id = JSON.parse(decrypted.toString('utf8').split('\n').filter(l => !l.trim().startsWith('#')).join('\n')).id || ''; } catch (_) {} + return { path: secretPath, id }; + }, + + recoveryKit() { + const secretPath = secretPathOf(); + if (!fs.existsSync(secretPath)) throw new Error(".ssb/secret file doesn't exist"); + const raw = fs.readFileSync(secretPath, 'utf8'); + const clean = raw.split('\n').filter(l => !l.trim().startsWith('#')).join('\n').trim(); + let id = ''; + try { id = JSON.parse(clean).id || ''; } catch (_) {} + return { secret: clean, id, createdAt: new Date().toISOString() }; + }, + + async estimate(opts = {}) { + const ssbClient = await openSsb(); + const options = normalizeOptions(opts); + const { picked, blobIds } = await selectMessages(ssbClient, options); + let bytes = 0; + for (const m of picked) bytes += Buffer.byteLength(JSON.stringify({ key: m.key, value: m.value, timestamp: m.timestamp }), 'utf8') + 5; + let blobBytes = 0; + let blobsFound = 0; + if (options.withBlobs && blobIds.size) { + const sizes = await listBlobs(ssbClient); + for (const id of blobIds) { + if (!sizes.has(id)) continue; + blobsFound += 1; + blobBytes += sizes.get(id) || 0; + } + } + return { options, messages: picked.length, blobs: blobsFound, blobsReferenced: blobIds.size, messageBytes: bytes, blobBytes, bytes: bytes + blobBytes }; + }, + + async createBackup(opts = {}, password, outPath) { + const pw = normalizePassword(password); + if (pw.length < 32) throw new Error('Password too short'); + const ssbClient = await openSsb(); + const options = normalizeOptions(opts); + const { picked, blobIds } = await selectMessages(ssbClient, options); + const salt = crypto.randomBytes(SALT_LEN); + const iv = crypto.randomBytes(IV_LEN); + const cipher = crypto.createCipheriv('aes-256-gcm', deriveKey(pw, salt), iv); + const gzip = zlib.createGzip(); + const out = fs.createWriteStream(outPath); + const done = new Promise((resolve, reject) => pipeline(gzip, cipher, out, (err) => err ? reject(err) : resolve())); + out.write(Buffer.concat([BACKUP_MAGIC, salt, iv])); + const meta = { version: 1, createdAt: new Date().toISOString(), author: ssbClient.id, options, messages: picked.length }; + await writeChunk(gzip, record(REC_META, Buffer.from(JSON.stringify(meta), 'utf8'))); + for (const m of picked) { + await writeChunk(gzip, record(REC_MSG, Buffer.from(JSON.stringify({ key: m.key, value: m.value, timestamp: m.timestamp }), 'utf8'))); + } + let files = 0; + const root = stateRoot(); + if (root) { + for (const f of listStateFiles(root)) { + let data = null; + try { data = fs.readFileSync(f.full); } catch (_) { continue; } + const head = Buffer.from(JSON.stringify({ rel: f.rel.split(path.sep).join('/'), size: data.length }), 'utf8'); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32BE(head.length, 0); + await writeChunk(gzip, record(REC_FILE, Buffer.concat([lenBuf, head, data]))); + files += 1; + } + } + let blobs = 0; + if (options.withBlobs) { + for (const id of blobIds) { + const data = await readBlob(ssbClient, id); + if (!data) continue; + const head = Buffer.from(JSON.stringify({ id, size: data.length }), 'utf8'); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32BE(head.length, 0); + await writeChunk(gzip, record(REC_BLOB, Buffer.concat([lenBuf, head, data]))); + blobs += 1; + } + } + gzip.end(); + await done; + fs.appendFileSync(outPath, cipher.getAuthTag()); + markBackup(); + return { path: outPath, messages: picked.length, blobs, bytes: fs.statSync(outPath).size, filename: `oasis-backup-${new Date().toISOString().slice(0, 10)}.oasisbk` }; + }, + + async createSnapshot(outPath, { sinceMs = 0, now = Date.now() } = {}) { + const ssbClient = await openSsb(); + const all = await readLog(ssbClient); + const byAuthor = new Map(); + for (const m of all) { + if (!m || !m.value || !m.value.author || m.value.content === undefined || m.value.content === null) continue; + const list = byAuthor.get(m.value.author) || []; + list.push(m); + byAuthor.set(m.value.author, list); + } + const latest = (list) => list.reduce((t, m) => Math.max(t, Number(m.timestamp) || 0, Number(m.value.timestamp) || 0), 0); + const picked = [...byAuthor.values()] + .filter(list => !(sinceMs > 0) || latest(list) >= now - sinceMs) + .sort((a, b) => latest(b) - latest(a)) + .flatMap(list => list.sort((a, b) => (a.value.sequence || 0) - (b.value.sequence || 0))); + const gzip = zlib.createGzip(); + const out = fs.createWriteStream(outPath); + const done = new Promise((resolve, reject) => pipeline(gzip, out, (err) => err ? reject(err) : resolve())); + out.write(SNAPSHOT_MAGIC); + const meta = { version: 1, kind: 'snapshot', createdAt: new Date().toISOString(), author: ssbClient.id, messages: picked.length, feeds: new Set(picked.map(m => m.value.author)).size, boxed: picked.filter(m => typeof m.value.content === 'string').length, sinceMs: sinceMs > 0 ? sinceMs : null }; + await writeChunk(gzip, record(REC_META, Buffer.from(JSON.stringify(meta), 'utf8'))); + for (const m of picked) { + await writeChunk(gzip, record(REC_MSG, Buffer.from(JSON.stringify({ key: m.key, value: m.value, timestamp: m.timestamp }), 'utf8'))); + } + gzip.end(); + await done; + return { path: outPath, messages: picked.length, feeds: meta.feeds, bytes: fs.statSync(outPath).size, createdAt: meta.createdAt }; + }, + + isSnapshotFile(filePath) { + try { + const fd = fs.openSync(filePath, 'r'); + const head = Buffer.alloc(SNAPSHOT_MAGIC.length); + fs.readSync(fd, head, 0, head.length, 0); + fs.closeSync(fd); + return head.equals(SNAPSHOT_MAGIC); + } catch (_) { return false; } + }, + + async fetchSnapshot({ address, connect = null, tier = 'full' }, outPath) { + const ssbClient = await openSsb(); + const doConnect = connect || ((addr, cb) => ssbClient.conn.connect(addr, cb)); + const discard = () => { try { fs.unlinkSync(outPath); } catch (_) {} return null; }; + try { + const rpc = await new Promise((resolve, reject) => doConnect(address, (err, r) => err ? reject(err) : resolve(r))); + if (!rpc || !rpc.snapshot || typeof rpc.snapshot.get !== 'function') return null; + const info = await new Promise((resolve) => rpc.snapshot.info({ tier }, (err, i) => resolve(err ? null : i))); + if (!info || !(info.bytes > 0) || info.bytes > SNAPSHOT_MAX_BYTES) return null; + const out = fs.createWriteStream(outPath); + let written = 0; + await new Promise((resolve, reject) => { + pull( + rpc.snapshot.get({ tier }), + pull.map(c => Buffer.isBuffer(c) ? c : Buffer.from(c)), + pull.through(c => { written += c.length; }), + toPull.sink(out, (err) => err ? reject(err) : resolve()) + ); + }); + if (written > SNAPSHOT_MAX_BYTES || !this.isSnapshotFile(outPath)) return discard(); + return { address, tier, path: outPath, bytes: written, createdAt: info.createdAt }; + } catch (_) { return discard(); } + }, + + startBootstrap({ address, connect = null, tiers = ['recent', 'full'] }) { + if (restoreJob && restoreJob.running) return restoreJob; + const empty = () => ({ messages: 0, skipped: 0, failed: 0, forked: 0, blobs: 0, blobsSkipped: 0, percent: 0 }); + const job = { running: true, source: 'snapshot', phase: null, tier: null, tiers: [], address, startedAt: new Date().toISOString(), finishedAt: null, progress: empty(), result: null, error: null }; + restoreJob = job; + const total = { ...empty(), files: 0, forks: [], errors: [], meta: null }; + const run = async () => { + for (const tier of tiers) { + const tmp = path.join(os.tmpdir(), `oasis-snapshot-${tier}-${Date.now()}-${Math.random().toString(36).slice(2)}.oasissn`); + job.tier = tier; + job.phase = 'download'; + try { + const got = await this.fetchSnapshot({ address, connect, tier }, tmp); + if (!got) continue; + job.phase = 'restore'; + const res = await this.restoreBackup({ filePath: tmp, password: '', stateFiles: false, onProgress: (p) => { job.progress = { ...total, ...p, messages: total.messages + p.messages, skipped: total.skipped + p.skipped, failed: total.failed + p.failed, forked: total.forked + p.forked }; } }); + for (const k of ['messages', 'skipped', 'failed', 'forked', 'blobs', 'blobsSkipped', 'files']) total[k] += Number(res[k]) || 0; + total.forks.push(...(res.forks || [])); + total.errors.push(...(res.errors || [])); + total.meta = res.meta || total.meta; + job.tiers.push({ tier, messages: res.messages, skipped: res.skipped, bytes: got.bytes }); + } finally { try { fs.unlinkSync(tmp); } catch (_) {} } + } + if (!job.tiers.length) throw new Error('snapshot not available'); + total.percent = 100; + return total; + }; + job.promise = run() + .then((res) => { job.result = res; job.progress = { ...job.progress, ...res }; return res; }) + .catch((err) => { job.error = err && err.message ? err.message : String(err); return null; }) + .finally(() => { job.running = false; job.phase = null; job.finishedAt = new Date().toISOString(); }); + return job; + }, + + async readBackup(filePath, password, onRecord) { + const size = fs.statSync(filePath).size; + const snapshot = this.isSnapshotFile(filePath); + const pw = snapshot ? '' : normalizePassword(password); + const headLen = snapshot ? SNAPSHOT_MAGIC.length : BACKUP_MAGIC.length + SALT_LEN + IV_LEN; + const tailLen = snapshot ? 0 : TAG_LEN; + if (size < headLen + tailLen) throw new Error('Unrecognized or corrupt backup file.'); + const fd = fs.openSync(filePath, 'r'); + const head = Buffer.alloc(headLen); + fs.readSync(fd, head, 0, headLen, 0); + const tag = Buffer.alloc(TAG_LEN); + if (!snapshot) fs.readSync(fd, tag, 0, TAG_LEN, size - TAG_LEN); + fs.closeSync(fd); + if (!snapshot && !head.slice(0, BACKUP_MAGIC.length).equals(BACKUP_MAGIC)) throw new Error('Unrecognized or corrupt backup file.'); + let decipher = null; + if (!snapshot) { + const salt = head.slice(BACKUP_MAGIC.length, BACKUP_MAGIC.length + SALT_LEN); + const iv = head.slice(BACKUP_MAGIC.length + SALT_LEN, headLen); + decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(pw, salt), iv); + decipher.setAuthTag(tag); + } + const gunzip = zlib.createGunzip(); + const total = size - headLen - tailLen; + const source = fs.createReadStream(filePath, { start: headLen, end: size - tailLen - 1 }); + let streamError = null; + if (decipher) pipeline(source, decipher, gunzip, (err) => { if (err) streamError = err; }); + else pipeline(source, gunzip, (err) => { if (err) streamError = err; }); + const chunks = []; + let pendingLen = 0; + let meta = null; + const compact = () => { + if (chunks.length < 2) return; + const all = Buffer.concat(chunks, pendingLen); + chunks.length = 0; + chunks.push(all); + }; + const nextRecord = () => { + if (pendingLen < 5) return null; + if (chunks[0].length < 5) compact(); + const type = chunks[0].readUInt8(0); + const len = chunks[0].readUInt32BE(1); + if (len > REC_MAX_LEN) throw new Error('Backup record too large.'); + if (pendingLen < 5 + len) return null; + compact(); + const all = chunks[0]; + chunks.length = 0; + pendingLen = all.length - 5 - len; + if (pendingLen) chunks.push(all.slice(5 + len)); + return { type, payload: all.slice(5, 5 + len) }; + }; + const handle = async (type, payload) => { + const read = Math.min(total, source.bytesRead || 0); + if (type === REC_META) { meta = JSON.parse(payload.toString('utf8')); await onRecord({ type: 'meta', meta, read, total }); } + else if (type === REC_MSG) await onRecord({ type: 'msg', msg: JSON.parse(payload.toString('utf8')), read, total }); + else if (type === REC_BLOB) { + const hl = payload.readUInt32BE(0); + const header = JSON.parse(payload.slice(4, 4 + hl).toString('utf8')); + await onRecord({ type: 'blob', id: header.id, data: payload.slice(4 + hl), read, total }); + } + else if (type === REC_FILE) { + const hl = payload.readUInt32BE(0); + const header = JSON.parse(payload.slice(4, 4 + hl).toString('utf8')); + await onRecord({ type: 'file', rel: header.rel, data: payload.slice(4 + hl), read, total }); + } + }; + try { + for await (const chunk of gunzip) { + chunks.push(chunk); + pendingLen += chunk.length; + let rec; + while ((rec = nextRecord())) await handle(rec.type, rec.payload); + } + } catch (e) { + source.destroy(); + if (streamError || /unsupported state|auth|bad decrypt|incorrect header|invalid/i.test(String(e && e.message))) throw new Error('Wrong password or corrupt backup file.'); + throw e; + } + if (streamError) throw new Error('Wrong password or corrupt backup file.'); + return meta; + }, + + async restoreBackup({ filePath, password, onProgress, stateFiles = true }) { + const ssbClient = await openSsb(); + let validate = null; + try { validate = require('../server/node_modules/ssb-validate'); } catch (_) {} + const seqKey = (v) => `${v.author}::${v.sequence}`; + const existing = new Map(); + for (const m of await readLog(ssbClient)) if (m && m.value) existing.set(seqKey(m.value), m.key); + const stats = { messages: 0, skipped: 0, failed: 0, forked: 0, blobs: 0, blobsSkipped: 0, files: 0, percent: 0, forks: [], errors: [], meta: null }; + const forkAuthors = new Map(); + const errorCounts = new Map(); + const noteError = (err) => { + const reason = String(err && err.message ? err.message : err || 'unknown error').replace(/, on feed:@[^\s]+/g, '').replace(/in state:\{.*?\}/g, '').trim(); + errorCounts.set(reason, (errorCounts.get(reason) || 0) + 1); + }; + const addMsg = (value) => new Promise((resolve) => { + if (typeof ssbClient.add !== 'function') return resolve(new Error('add is not available')); + ssbClient.add(value, (err) => resolve(err || null)); + }); + const addBlob = (data) => new Promise((resolve) => { + pull(pull.values([data]), ssbClient.blobs.add((err, ref) => resolve(err ? null : ref))); + }); + const getByKey = (key) => new Promise((resolve) => { + if (!key || typeof ssbClient.get !== 'function') return resolve(false); + try { ssbClient.get(key, (err, value) => resolve(!err && !!value)); } catch (_) { resolve(false); } + }); + const storedByKey = async (msg) => { + if (await getByKey(msg.key)) return true; + let computed = null; + try { computed = validate ? validate.id(msg.value) : null; } catch (_) { computed = null; } + return computed && computed !== msg.key ? getByKey(computed) : false; + }; + const supportsLs = !!(ssbClient.blobs && typeof ssbClient.blobs.ls === 'function'); + const present = supportsLs ? await listBlobs(ssbClient) : new Map(); + const hasBlob = (id) => supportsLs ? Promise.resolve(present.has(id)) : new Promise((resolve) => ssbClient.blobs.has(id, (err, has) => resolve(!err && !!has))); + const deferred = []; + const report = () => { if (typeof onProgress === 'function') onProgress({ ...stats }); }; + const sameMessage = (storedKey, msg) => { + if (storedKey === msg.key) return true; + try { return storedKey === validate.id(msg.value); } catch (_) { return false; } + }; + const tryAdd = async (msg) => { + const k = seqKey(msg.value); + if (existing.has(k)) { + if (sameMessage(existing.get(k), msg)) { stats.skipped += 1; return true; } + stats.forked += 1; + forkAuthors.set(msg.value.author, (forkAuthors.get(msg.value.author) || 0) + 1); + return true; + } + const err = await addMsg(msg.value); + if (err) { + if (!(await storedByKey(msg))) return err; + stats.skipped += 1; + existing.set(k, msg.key); + return true; + } + stats.messages += 1; + existing.set(k, msg.key); + return true; + }; + const meta = await this.readBackup(filePath, password, async (rec) => { + if (rec.total) stats.percent = Math.min(100, (rec.read / rec.total) * 100); + if (rec.type === 'meta') { stats.meta = rec.meta; return; } + if (rec.type === 'msg') { + if (!rec.msg || !rec.msg.value || !rec.msg.value.author) { stats.failed += 1; noteError('malformed message'); report(); return; } + const res = await tryAdd(rec.msg); + if (res !== true) deferred.push({ msg: rec.msg, err: res }); + report(); + return; + } + if (rec.type === 'file') { + if (!stateFiles) { stats.skipped += 1; report(); return; } + const root = stateRoot(); + const rel = typeof rec.rel === 'string' ? rec.rel.replace(/\\/g, '/') : ''; + const parts = rel.split('/').filter(Boolean); + const safe = !!root && parts.length > 0 && !NOT_BACKED_UP.has(parts[0]) && !path.isAbsolute(rel) && parts.every(p => p !== '.' && p !== '..' && !/[\0]/.test(p)); + const rootAbs = root ? path.resolve(root) : ''; + const target = safe ? path.resolve(rootAbs, ...parts) : ''; + if (!safe || !target.startsWith(rootAbs + path.sep)) { stats.failed += 1; noteError('state file rejected'); report(); return; } + try { + fs.mkdirSync(path.dirname(target), { recursive: true }); + if (fs.existsSync(target)) { try { fs.copyFileSync(target, `${target}.before-restore`); } catch (_) {} } + fs.writeFileSync(target, rec.data); + stats.files += 1; + } catch (_) { stats.failed += 1; noteError('state file could not be written'); } + report(); + return; + } + if (rec.type === 'blob') { + if (await hasBlob(rec.id)) { stats.blobsSkipped += 1; report(); return; } + const ref = await addBlob(rec.data); + if (ref) { stats.blobs += 1; present.set(ref, rec.data.length); } else { stats.failed += 1; noteError('blob could not be stored'); } + report(); + } + }); + deferred.sort((a, b) => a.msg.value.author === b.msg.value.author ? (a.msg.value.sequence || 0) - (b.msg.value.sequence || 0) : String(a.msg.value.author).localeCompare(String(b.msg.value.author))); + let pending = deferred; + while (pending.length) { + const next = []; + for (const item of pending) { + const res = await tryAdd(item.msg); + if (res !== true) next.push({ msg: item.msg, err: res }); + } + report(); + if (next.length === pending.length) { pending = next; break; } + pending = next; + } + for (const item of pending) { stats.failed += 1; noteError(item.err); } + stats.percent = 100; + stats.forks = Array.from(forkAuthors.entries()).map(([author, count]) => ({ author, count, mine: author === ssbClient.id })); + stats.errors = Array.from(errorCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([reason, count]) => ({ reason, count })); + try { fs.unlinkSync(filePath); } catch (_) {} + report(); + return { ...stats, meta: stats.meta || meta }; + }, + + startRestore({ filePath, password, source = 'file' }) { + if (restoreJob && restoreJob.running) return restoreJob; + const job = { running: true, source, startedAt: new Date().toISOString(), finishedAt: null, progress: { messages: 0, skipped: 0, failed: 0, forked: 0, blobs: 0, blobsSkipped: 0, percent: 0 }, result: null, error: null }; + restoreJob = job; + job.promise = this.restoreBackup({ filePath, password, onProgress: (p) => { job.progress = p; } }) + .then((res) => { job.result = res; return res; }) + .catch((err) => { job.error = err && err.message ? err.message : String(err); try { fs.unlinkSync(filePath); } catch (_) {} return null; }) + .finally(() => { job.running = false; job.finishedAt = new Date().toISOString(); }); + return job; + }, + + restoreStatus() { + if (!restoreJob) return null; + const { promise, ...rest } = restoreJob; + return rest; + }, + + reminderState() { return reminderState(); }, + + setReminderDays(days) { return setReminderDays(days); }, + + markBackup() { return markBackup(); }, + + async rebuildIndexes() { + const started = Date.now(); + const ssbClient = await openSsb(); + const indexDir = path.join(require('../configs/state-manager').ssbDir(), 'db2', 'indexes'); + const measure = (dir) => { + let files = 0, bytes = 0; + const walk = (d) => { + let entries = []; + try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch (_) { return; } + for (const entry of entries) { + const full = path.join(d, entry.name); + if (entry.isDirectory()) { walk(full); continue; } + try { bytes += fs.statSync(full).size; files += 1; } catch (_) {} + } + }; + walk(dir); + return { files, bytes }; + }; + const before = measure(indexDir); + let error = null; + try { + await new Promise((resolve, reject) => { + try { ssbClient.rebuild((err) => err ? reject(err) : resolve()); } catch (e) { reject(e); } + }); + } catch (e) { error = e && e.message ? e.message : String(e); } + const after = measure(indexDir); + let totalMessages = 0; + try { totalMessages = (await readLog(ssbClient)).length; } catch (_) {} + return { + checkedAt: new Date().toISOString(), + tookMs: Date.now() - started, + ok: !error, + error, + totalMessages, + indexes: { path: indexDir, files: after.files, bytes: after.bytes, deltaBytes: after.bytes - before.bytes } + }; + }, + async verify({ author = null } = {}) { + const started = Date.now(); + const ssbClient = await openSsb(); + const me = author || ssbClient.id; + const all = await readLog(ssbClient); + let validate = null; + try { validate = require('../server/node_modules/ssb-validate'); } catch (_) {} + const own = all.filter(m => m && m.value && m.value.author === me).sort((a, b) => (a.value.sequence || 0) - (b.value.sequence || 0)); + const feed = { messages: own.length, lastSequence: own.length ? own[own.length - 1].value.sequence : 0, gaps: [], brokenLinks: [], badHashes: [], badSignatures: [], duplicates: [] }; + let expected = 1; + let prevKey = null; + const seen = new Set(); + for (const m of own) { + const seq = Number(m.value.sequence) || 0; + if (seen.has(seq)) feed.duplicates.push(seq); + seen.add(seq); + if (seq !== expected) { feed.gaps.push({ expected, found: seq }); expected = seq; } + if (prevKey && m.value.previous !== prevKey) feed.brokenLinks.push(seq); + if (validate) { + try { if (validate.id(m.value) !== m.key) feed.badHashes.push(seq); } catch (_) { feed.badHashes.push(seq); } + try { if (validate.checkInvalidOOO(m.value, null)) feed.badSignatures.push(seq); } catch (_) { feed.badSignatures.push(seq); } + } + prevKey = m.key; + expected = seq + 1; + } + feed.ok = !feed.gaps.length && !feed.brokenLinks.length && !feed.badHashes.length && !feed.badSignatures.length && !feed.duplicates.length; + const bySeq = new Map(); + for (const m of all) { + if (!m || !m.value || !m.value.author) continue; + const k = `${m.value.author}::${m.value.sequence}`; + if (!bySeq.has(k)) bySeq.set(k, new Set()); + bySeq.get(k).add(m.key); + } + const forkMap = new Map(); + for (const [k, keys] of bySeq) { + if (keys.size < 2) continue; + const [feedId, seq] = k.split('::'); + if (!forkMap.has(feedId)) forkMap.set(feedId, []); + forkMap.get(feedId).push({ sequence: Number(seq), keys: Array.from(keys) }); + } + const forks = Array.from(forkMap.entries()).map(([feedId, points]) => ({ author: feedId, mine: feedId === me, points: points.sort((a, b) => a.sequence - b.sequence) })); + const referenced = new Set(); + const referencedOwn = new Set(); + for (const m of all) { + if (!m || !m.value) continue; + for (const ref of blobRefsOf(m.value.content)) { + referenced.add(ref); + if (m.value.author === me) referencedOwn.add(ref); + } + } + const present = await listBlobs(ssbClient); + const orphan = []; + let orphanBytes = 0; + for (const [id, size] of present) if (!referenced.has(id)) { orphan.push(id); orphanBytes += size || 0; } + const missing = []; + const missingOwn = []; + for (const id of referenced) { + if (!present.size || present.has(id)) continue; + missing.push(id); + if (referencedOwn.has(id)) missingOwn.push(id); + } + return { + author: me, checkedAt: new Date().toISOString(), tookMs: Date.now() - started, totalMessages: all.length, + feed, forks, forkCount: forks.length, + blobs: { present: present.size, referenced: referenced.size, orphan: orphan.length, orphanBytes, missing: missing.length, missingOwn: missingOwn.length, orphanIds: orphan.slice(0, 50), missingIds: missing.slice(0, 50), missingOwnIds: missingOwn.slice(0, 50), listed: present.size > 0 || referenced.size === 0 } + }; + } + }; +}; + +Object.assign(module.exports, { reminderState, setReminderDays, markBackup, REMINDER_OPTIONS, MODULE_KEYS }); diff --git a/src/models/banking_model.js b/src/models/banking_model.js index 4837168..f5092e2 100644 --- a/src/models/banking_model.js +++ b/src/models/banking_model.js @@ -2,7 +2,8 @@ const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const pull = require("../server/node_modules/pull-stream"); -const { getConfig } = require("../configs/config-manager.js"); +const { readTyped, CONTENT_TYPES, requestScope } = require("./typed_log"); +const { getConfig, defaultInvite } = require("../configs/config-manager.js"); const { config } = require("../server/SSB_server.js"); const sharedState = require("../configs/shared-state.js"); @@ -19,12 +20,17 @@ const DEFAULT_RULES = { graceDays: 30 }; -const STORAGE_DIR = process.env.OASIS_BANKING_DIR || path.join(__dirname, "..", "configs"); -const EPOCHS_PATH = path.join(STORAGE_DIR, "banking-epochs.json"); -const TRANSFERS_PATH = path.join(STORAGE_DIR, "banking-allocations.json"); -const ADDR_PATH = path.join(STORAGE_DIR, "wallet-addresses.json"); -const ECO_HISTORY_PATH = path.join(STORAGE_DIR, "banking-eco-history.json"); -const ECO_HISTORY_MAX = 500; +const { statePath, stateDir } = require("../configs/state-manager"); +const stateFile = (name) => process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, name) : statePath(name); +const STORAGE_DIR = process.env.OASIS_BANKING_DIR || stateDir("banking-epochs.json"); +const EPOCHS_PATH = stateFile("banking-epochs.json"); +const TRANSFERS_PATH = stateFile("banking-allocations.json"); +const ADDR_PATH = stateFile("wallet-addresses.json"); +const BOOK_PATH = stateFile("banking-address-book.json"); +const ECO_HISTORY_PATH = stateFile("banking-eco-history.json"); +const UBI_PAID_PATH = stateFile("banking-ubi-paid.json"); +const REBALANCE_PENDING_PATH = stateFile("banking-rebalance-pending.json"); +const FUNDS_HISTORY_PATH = stateFile("banking-funds-history.json"); const ECO_HISTORY_MIN_GAP_MS = 5 * 60 * 1000; const ECOIN_PER_GRAM_CO2 = 0.1; @@ -42,6 +48,8 @@ function ensureStoreFiles() { if (!fs.existsSync(TRANSFERS_PATH)) fs.writeFileSync(TRANSFERS_PATH, "[]"); if (!fs.existsSync(ADDR_PATH)) fs.writeFileSync(ADDR_PATH, "{}"); if (!fs.existsSync(ECO_HISTORY_PATH)) fs.writeFileSync(ECO_HISTORY_PATH, "[]"); + if (!fs.existsSync(UBI_PAID_PATH)) fs.writeFileSync(UBI_PAID_PATH, "{}"); + if (!fs.existsSync(FUNDS_HISTORY_PATH)) fs.writeFileSync(FUNDS_HISTORY_PATH, "[]"); } function readEcoHistory() { @@ -49,6 +57,59 @@ function readEcoHistory() { try { return JSON.parse(fs.readFileSync(ECO_HISTORY_PATH, "utf8")) || []; } catch (_) { return []; } } +const ECO_HISTORY_TIERS = [ + { olderThanMs: 2 * ONE_DAY_MS, keepEveryMs: 60 * 60 * 1000 }, + { olderThanMs: 60 * ONE_DAY_MS, keepEveryMs: ONE_DAY_MS } +]; +const ECO_HISTORY_HARD_MAX = 20000; + +function readFundsHistory() { + try { return JSON.parse(fs.readFileSync(FUNDS_HISTORY_PATH, "utf8")) || []; } catch (_) { return []; } +} + +function appendFundsHistory(sample) { + ensureStoreFiles(); + let arr = readFundsHistory(); + if (!Array.isArray(arr)) arr = []; + const last = arr[arr.length - 1]; + if (last && Number(sample.ts) - Number(last.ts) < ECO_HISTORY_MIN_GAP_MS) return arr; + arr.push(sample); + arr = compactEcoHistory(arr); + writeJson(FUNDS_HISTORY_PATH, arr); + return arr; +} + +function compactEcoHistory(arr) { + const now = Date.now(); + const out = []; + let lastKeptTs = -Infinity; + for (const s of arr) { + const ts = Number(s.ts) || 0; + const age = now - ts; + const tier = ECO_HISTORY_TIERS.slice().reverse().find(t => age > t.olderThanMs); + if (!tier || ts - lastKeptTs >= tier.keepEveryMs) { out.push(s); lastKeptTs = ts; } + } + return out.length > ECO_HISTORY_HARD_MAX ? out.slice(out.length - ECO_HISTORY_HARD_MAX) : out; +} + +const RANGE_MS = { today: ONE_DAY_MS, week: 7 * ONE_DAY_MS, month: 30 * ONE_DAY_MS, year: 365 * ONE_DAY_MS, "5y": 5 * 365 * ONE_DAY_MS, all: Infinity }; +const normalizeRange = (r) => (Object.prototype.hasOwnProperty.call(RANGE_MS, String(r || "")) ? String(r) : "today"); +const filterByRange = (arr, range, key = "ts") => { + const span = RANGE_MS[normalizeRange(range)]; + if (!Number.isFinite(span)) return arr; + const from = Date.now() - span; + return arr.filter(x => Number(x[key]) >= from); +}; +const RANGE_ORDER = ["today", "week", "month", "year", "5y", "all"]; +const widenRange = (arr, range, key = "ts") => { + const start = Math.max(0, RANGE_ORDER.indexOf(normalizeRange(range))); + for (let i = start; i < RANGE_ORDER.length; i++) { + const r = RANGE_ORDER[i]; + if (filterByRange(arr, r, key).length >= 2) return r; + } + return normalizeRange(range); +}; + function appendEcoHistory(sample) { ensureStoreFiles(); let arr = readEcoHistory(); @@ -56,7 +117,7 @@ function appendEcoHistory(sample) { const last = arr[arr.length - 1]; if (last && Number(sample.ts) - Number(last.ts) < ECO_HISTORY_MIN_GAP_MS) return arr; arr.push(sample); - if (arr.length > ECO_HISTORY_MAX) arr = arr.slice(arr.length - ECO_HISTORY_MAX); + arr = compactEcoHistory(arr); try { fs.writeFileSync(ECO_HISTORY_PATH, JSON.stringify(arr)); } catch (_) {} return arr; } @@ -133,7 +194,52 @@ function writeJson(p, v) { fs.writeFileSync(p, JSON.stringify(v, null, 2)); } -async function rpcCall(method, params, kind = "user") { +const ubiPaidKey = (epochId, claimantId) => `${epochId}:${claimantId}`; + +function readUbiPaidLedger() { + ensureStoreFiles(); + const raw = readJson(UBI_PAID_PATH, {}); + return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}; +} + +function reserveUbiPayment(key) { + const ledger = readUbiPaidLedger(); + if (ledger[key]) return false; + ledger[key] = { startedAt: new Date().toISOString() }; + writeJson(UBI_PAID_PATH, ledger); + return true; +} + +function settleUbiPayment(key, patch) { + const ledger = readUbiPaidLedger(); + ledger[key] = { ...(ledger[key] || {}), ...patch }; + writeJson(UBI_PAID_PATH, ledger); +} + +function releaseUbiPayment(key) { + const ledger = readUbiPaidLedger(); + delete ledger[key]; + writeJson(UBI_PAID_PATH, ledger); +} + +function ubiAddressTaken(epochId, address, claimantId) { + const ledger = readUbiPaidLedger(); + const own = ubiPaidKey(epochId, claimantId); + return Object.keys(ledger).some(k => k !== own && k.startsWith(`${epochId}:`) && ledger[k] && ledger[k].address === address); +} + +const rpcFailureLog = new Map(); +function logRpcFailure(method, kind, reason) { + const key = `${kind}:${method}:${reason}`; + const now = Date.now(); + if (now - (rpcFailureLog.get(key) || 0) < 5 * 60 * 1000) return; + rpcFailureLog.set(key, now); + if (kind === "pub") console.warn(`[ECOin RPC] ${kind} ${method} failed: ${reason}`); +} + +const RPC_TIMEOUT_MS = { sendtoaddress: 120000, listtransactions: 30000, gettransaction: 15000 }; +const ENGINE_RPC_TIMEOUT_MS = 20000; +async function rpcCall(method, params, kind = "user", timeoutMs = 0) { const cfg = getWalletCfg(kind); if (!cfg?.url) { return null; @@ -145,7 +251,7 @@ async function rpcCall(method, params, kind = "user") { headers.authorization = "Basic " + Buffer.from(`${cfg.user}:${cfg.pass}`).toString("base64"); } const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), 1500); + const timer = setTimeout(() => controller.abort(), timeoutMs || RPC_TIMEOUT_MS[method] || 1500); try { const res = await fetch(cfg.url, { method: "POST", @@ -159,23 +265,78 @@ async function rpcCall(method, params, kind = "user") { signal: controller.signal, }); if (!res.ok) { + logRpcFailure(method, kind, `HTTP ${res.status}`); return null; } const data = await res.json(); if (data.error) { + logRpcFailure(method, kind, data.error.message || JSON.stringify(data.error)); return null; } return data.result; } catch (err) { + logRpcFailure(method, kind, err && err.name === "AbortError" ? "timeout" : (err && err.message) || String(err)); return null; } finally { clearTimeout(timer); } } -async function safeGetBalance(kind = "user") { +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function listWalletSends(kind = "pub") { + for (const account of ["*", ""]) { + const list = await rpcCall("listtransactions", [account, 1000, 0], kind); + if (Array.isArray(list)) return list.filter(t => t && t.category === "send"); + } + return null; +} + +const sameAmount = (a, b) => Math.abs(Math.abs(Number(a)) - Math.abs(Number(b))) <= 0.000001; + +function sendMatches(t, { address, amount, comment, sinceMs, amountIfNoComment }) { + if (!t || t.address !== address || typeof t.txid !== "string") return false; + const at = Number(t.time || t.timereceived || 0) * 1000; + if (sinceMs && at && at < sinceMs - 5 * 60 * 1000) return false; + if (amount !== undefined && amount !== null && !sameAmount(t.amount, amount)) return false; + if (comment) { + if (typeof t.comment === "string") { if (t.comment !== comment) return false; } + else if (amountIfNoComment !== undefined && amountIfNoComment !== null && !sameAmount(t.amount, amountIfNoComment)) return false; + } + return true; +} + +async function findWalletSend(match, kind = "pub", sends = null) { + const list = sends || await listWalletSends(kind); + if (!list) return undefined; + const hit = list.find(t => sendMatches(t, match) && !(match.exclude && match.exclude.has(t.txid))); + return hit ? hit.txid : null; +} + +async function sendVerified(address, amount, comment, kind = "pub") { + const sinceMs = Date.now(); + const txid = await rpcCall("sendtoaddress", [address, amount, comment], kind); + if (typeof txid === "string" && txid) return { txid }; + await sleep(3000); + const found = await findWalletSend({ address, amount, comment, sinceMs }, kind); + if (typeof found === "string") return { txid: found, recovered: true }; + return { txid: null, unknown: found === undefined }; +} + +async function tryGetBalance(kind = "user", timeoutMs = 0) { try { - const r = await rpcCall("getbalance", [], kind); + const r = await rpcCall("getbalance", [], kind, timeoutMs); + if (r === null || r === undefined) return null; + const n = Number(r); + return Number.isFinite(n) ? n : null; + } catch { + return null; + } +} + +async function safeGetBalance(kind = "user", timeoutMs = 0) { + try { + const r = await rpcCall("getbalance", [], kind, timeoutMs); return Number(r) || 0; } catch { return 0; @@ -193,6 +354,35 @@ function writeAddrMap(m) { writeJson(ADDR_PATH, m || {}); } +function readAddressBook() { + const raw = readJson(BOOK_PATH, []); + return Array.isArray(raw) ? raw : []; +} + +function listAddressBook() { + return readAddressBook().map(e => ({ entryId: e.id, id: e.userId || null, address: e.address, label: e.label || "", source: "book", createdAt: e.createdAt })); +} + +function addAddressBookEntry({ label, address, userId }) { + if (!isValidEcoinAddress(address)) return { status: "invalid" }; + const book = readAddressBook(); + const cleanLabel = String(label || "").trim().slice(0, 80); + const cleanUser = /^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(String(userId || "")) ? String(userId) : null; + const dup = book.find(e => e.address === address && (e.label || "") === cleanLabel && (e.userId || null) === cleanUser); + if (dup) return { status: "exists" }; + book.push({ id: crypto.randomBytes(6).toString("hex"), label: cleanLabel, address, userId: cleanUser, createdAt: new Date().toISOString() }); + writeJson(BOOK_PATH, book); + return { status: "added" }; +} + +function removeAddressBookEntry(entryId) { + const book = readAddressBook(); + const next = book.filter(e => e.id !== String(entryId || "")); + if (next.length === book.length) return { status: "not_found" }; + writeJson(BOOK_PATH, next); + return { status: "deleted" }; +} + function getLogLimit() { return getConfig().ssbLogStream?.limit || 1000; } @@ -201,6 +391,11 @@ function isValidEcoinAddress(addr) { return typeof addr === "string" && /^E[1-9A-HJ-NP-Za-km-z]{32,34}$/.test(addr); } +function hasWalletCredentials(cfg = null) { + const w = (cfg || getConfig() || {}).wallet || {}; + return !!(String(w.url || "").trim() && String(w.user || "").trim() && String(w.pass || "").trim()); +} + function getWalletCfg(kind) { const cfg = getConfig() || {}; if (kind === "pub") { @@ -211,14 +406,32 @@ function getWalletCfg(kind) { } function isPubNode() { - const pubId = (getConfig() || {}).walletPub?.pubId || ""; - const myId = config?.keys?.id || ""; - return !!pubId && !!myId && pubId === myId; + const cfg = getConfig() || {}; + return config?.pub === true && !!(cfg.wallet && cfg.wallet.url) && !!config?.keys?.id; } -function getConfiguredPubId() { - return (getConfig() || {}).walletPub?.pubId || ""; +function getDefaultPubId() { + const m = String((defaultInvite() || {}).code || "").match(/(@[A-Za-z0-9+/]+={0,2}\.ed25519)/); + return m ? m[1] : ""; } +const PUB_DISCOVERY_TTL_MS = 60 * 1000; +const PUB_ANNOUNCE_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000; +const PUB_ANNOUNCE_REFRESH_MS = 12 * 60 * 60 * 1000; +const CLAIMANT_MIN_AGE_MS = 30 * 24 * 60 * 60 * 1000; +const REBALANCE_MIN_CLAIMANTS = 3; +const REBALANCE_PAYOUT_RATIO = 0.5; +const REBALANCE_TOLERANCE = 0.05; +const REBALANCE_SETTLE_MS = 6 * 60 * 60 * 1000; +const REBALANCE_CONCEPT = "OASIS UBI Rebalance"; +const UBI_PAYMENT_CONCEPT = "UBI - "; +const UBI_PAYMENT_PREFIXES = ["UBI - ", "OASIS UBI Payment"]; +const UBI_SEND_COMMENT = "OASIS UBI Payment"; +const UBI_TAKEOVER_AFTER_MS = 3 * 24 * 60 * 60 * 1000; +const UBI_TAKEOVER_MARGIN_MS = 12 * 60 * 60 * 1000; +const UBI_MAX_ATTEMPTS = 6; +const UBI_RETRY_AFTER_MS = 60 * 60 * 1000; +const isUbiPayoutConcept = (c) => UBI_PAYMENT_PREFIXES.some(p => String(c || "").startsWith(p)); +const TXID_RE = /\b([0-9a-f]{64})\b/i; function resolveUserId(maybeId) { const s = String(maybeId || "").trim(); @@ -226,7 +439,7 @@ function resolveUserId(maybeId) { return config?.keys?.id || ""; } -let FEED_SRC = "none"; + module.exports = ({ services } = {}) => { const transfersRepo = { @@ -260,38 +473,56 @@ module.exports = ({ services } = {}) => { return ssbInstance; } + const karma = require("./karma_model")({ services, ranges: { normalizeRange, widenRange, filterByRange } }); + const { listAllActions, fetchUserActions, scoreFromActions, getCarbonGramsForUser, getUserEngagementScore, getLastKarmaScore, listKarmaHistory } = karma; + + const BANKING_TYPES = ["wallet", "karmaScore", "ubiClaim", "ubiClaimResult", "ubiAllocation", "ubiRefuse", "pubAvailability", "tombstone"]; + async function scanLogStream() { const ssb = await openSsb(); if (!ssb) return []; - return new Promise((resolve, reject) => - pull( - ssb.createLogStream({ limit: getLogLimit(), reverse: true }), - pull.collect((err, arr) => err ? reject(err) : resolve(arr)) - ) - ); + return readTyped(ssb, BANKING_TYPES, { limit: getLogLimit(), withWindow: true }); + } + + const claimsInFlight = new Set(); + + const isWalletMessage = (c) => !!c && typeof c.address === "string" && isValidEcoinAddress(c.address) && + ((c.type === "wallet" && c.coin === "ECO") || c.type === "bankWallet"); + const isWalletUnset = (c) => !!c && typeof c.address === "string" && c.address === "" && + ((c.type === "wallet" && c.coin === "ECO") || c.type === "bankWallet"); + + async function scanWalletMessages() { + const ssb = await openSsb(); + if (!ssb) return []; + const msgs = await readTyped(ssb, ["wallet", "bankWallet"], { limit: Math.max(getLogLimit(), 20000) }); + return msgs.slice().sort((a, b) => (((b.value && b.value.timestamp) || 0) - ((a.value && a.value.timestamp) || 0)) || (((b.value && b.value.sequence) || 0) - ((a.value && a.value.sequence) || 0))); } async function getWalletFromSSB(userId) { - const msgs = await scanLogStream(); + const msgs = await scanWalletMessages(); for (const m of msgs) { const v = m.value || {}; - const c = v.content || {}; - if (v.author === userId && c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") { - return c.address; - } + if (v.author !== userId) continue; + if (isWalletMessage(v.content)) return v.content.address; + if (isWalletUnset(v.content)) return null; } return null; } + async function hasPublishedAddress(userId) { + return !!(await getWalletFromSSB(userId)); + } + async function scanAllWalletsSSB() { const latest = {}; - const msgs = await scanLogStream(); + const decided = new Set(); + const msgs = await scanWalletMessages(); for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; - if (c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") { - if (!latest[v.author]) latest[v.author] = c.address; - } + if (!v.author || decided.has(v.author)) continue; + if (isWalletMessage(c)) { latest[v.author] = c.address; decided.add(v.author); } + else if (isWalletUnset(c)) decided.add(v.author); } return latest; } @@ -347,6 +578,7 @@ module.exports = ({ services } = {}) => { if (ssbAll[userId]) { m[userId] = "__removed__"; writeAddrMap(m); + if (idsEqual(userId, config.keys.id)) { try { await publishSelfAddress(""); } catch (_) {} } return { status: "deleted" }; } if (hadLocal) { @@ -379,235 +611,6 @@ module.exports = ({ services } = {}) => { return strip(A) === strip(B); } - function inferType(c = {}) { - if (c.vote) return "vote"; - if (c.votes) return "votes"; - if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet"; - if (c.type === "ubiClaimResult" && c.txid && c.epochId) return "ubiClaimResult"; - if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim"; - if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market"; - if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project"; - if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe"; - if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event"; - if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task"; - if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report"; - if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job"; - if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio"; - if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video"; - if (typeof c.url !== "undefined" && c.title && c.key) return "document"; - if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed"; - if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post"; - if (typeof c.contact !== "undefined") return "contact"; - if (typeof c.about !== "undefined") return "about"; - if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer"; - return ""; - } - - function normalizeType(a) { - const t = a.type || a.content?.type || inferType(a.content) || ""; - return String(t).toLowerCase(); - } - - function priorityBump(p) { - const s = String(p || "").toUpperCase(); - if (s === "HIGH") return 3; - if (s === "MEDIUM") return 1; - return 0; - } - - function severityBump(s) { - const x = String(s || "").toUpperCase(); - if (x === "CRITICAL") return 6; - if (x === "HIGH") return 4; - if (x === "MEDIUM") return 2; - return 0; - } - - function scoreMarket(c) { - const st = String(c.status || "").toUpperCase(); - let s = 5; - if (st === "SOLD") s += 8; - else if (st === "ACTIVE") s += 3; - const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0; - s += Math.min(10, bids); - return s; - } - - function scoreProject(c) { - const st = String(c.status || "ACTIVE").toUpperCase(); - const prog = Number(c.progress || 0); - let s = 8 + Math.min(10, prog / 10); - if (st === "FUNDED") s += 10; - return s; - } - - function calculateOpinionScore(content) { - const cats = content?.opinions || {}; - let s = 0; - for (const k in cats) { - if (!Object.prototype.hasOwnProperty.call(cats, k)) continue; - if (k === "interesting" || k === "inspiring") s += 5; - else if (k === "boring" || k === "spam" || k === "propaganda") s -= 3; - else s += 1; - } - return s; - } - - async function listAllActions() { - if (services?.feed?.listAll) { - const arr = await services.feed.listAll(); - FEED_SRC = "services.feed.listAll"; - return normalizeFeedArray(arr); - } - if (services?.activity?.list) { - const arr = await services.activity.list(); - FEED_SRC = "services.activity.list"; - return normalizeFeedArray(arr); - } - if (typeof global.listFeed === "function") { - const arr = await global.listFeed("all"); - FEED_SRC = "global.listFeed('all')"; - return normalizeFeedArray(arr); - } - const ssb = await openSsb(); - if (!ssb || !ssb.createLogStream) { - FEED_SRC = "none"; - return []; - } - const msgs = await scanLogStream(); - FEED_SRC = "ssb.createLogStream"; - return msgs.map(m => { - const v = m.value || {}; - const c = v.content || {}; - return { - id: v.key || m.key, - author: v.author, - type: (c.type || "").toLowerCase(), - value: v, - content: c - }; - }); - } - - function normalizeFeedArray(arr) { - if (!Array.isArray(arr)) return []; - return arr.map(x => { - const value = x.value || {}; - const content = x.content || value.content || {}; - const author = x.author || value.author || content.author || null; - const type = (content.type || "").toLowerCase(); - return { id: x.id || value.key || x.key, author, type, value, content }; - }); - } - -async function publishKarmaScore(userId, karmaScore) { - const ssb = await openSsb(); - if (!ssb || !ssb.publish) return false; - const timestamp = new Date().toISOString(); - const content = { type: "karmaScore", karmaScore, userId, timestamp }; - return new Promise((resolve, reject) => { - ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg)); - }); -} - -async function fetchUserActions(userId) { - const me = resolveUserId(userId); - const actions = await listAllActions(); - const authored = actions.filter(a => - (a.author && a.author === me) || (a.value?.author && a.value.author === me) - ); - if (authored.length) return authored; - return actions.filter(a => { - const c = a.content || {}; - const fields = [c.author, c.organizer, c.seller, c.about, c.contact]; - return fields.some(f => f && f === me); - }); -} - -function scoreFromActions(actions) { - let score = 0; - const nowMs = Date.now(); - for (const action of actions) { - const t = normalizeType(action); - const c = action.content || {}; - const rawType = String(c.type || "").toLowerCase(); - const ts = action.value?.timestamp; - const ageDays = ts ? (nowMs - ts) / 86400000 : Infinity; - const decay = ageDays <= 30 ? 1.0 : ageDays <= 90 ? 0.5 : 0.25; - if (t === "post") score += 10 * decay; - else if (t === "comment") score += 5 * decay; - else if (t === "like") score += 2 * decay; - else if (t === "image") score += 8 * decay; - else if (t === "video") score += 12 * decay; - else if (t === "audio") score += 8 * decay; - else if (t === "document") score += 6 * decay; - else if (t === "bookmark") score += 2 * decay; - else if (t === "feed") score += 6 * decay; - else if (t === "forum") score += (c.root ? 5 : 10) * decay; - else if (t === "vote") score += (3 + calculateOpinionScore(c)) * decay; - else if (t === "votes") score += Math.min(10, Number(c.totalVotes || 0)) * decay; - else if (t === "market") score += scoreMarket(c) * decay; - else if (t === "project") score += scoreProject(c) * decay; - else if (t === "tribe") score += (6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0)) * decay; - else if (t === "event") score += (4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0)) * decay; - else if (t === "task") score += (3 + priorityBump(c.priority)) * decay; - else if (t === "report") score += (4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity)) * decay; - else if (t === "curriculum") score += 5 * decay; - else if (t === "aiexchange") score += (Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0) * decay; - else if (t === "job") score += (4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0)) * decay; - else if (t === "bankclaim") score += Math.min(20, Math.log(1 + Math.max(0, Number(c.amount) || 0)) * 5) * decay; - else if (t === "bankwallet") score += 2 * decay; - else if (t === "transfer") score += 1 * decay; - else if (t === "about") score += (1 + ((c.visibilityPrefs && c.visibilityPrefs.fediverseHandle) ? 8 : 0)) * decay; - else if (t === "contact") score += 1 * decay; - else if (t === "pub") score += 1 * decay; - else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") score += 12 * decay; - else if (t === "parliamentterm" || rawType === "parliamentterm") score += 25 * decay; - else if (t === "parliamentproposal" || rawType === "parliamentproposal") score += 8 * decay; - else if (t === "parliamentlaw" || rawType === "parliamentlaw") score += 16 * decay; - else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") score += 10 * decay; - else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") score += 4 * decay; - else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") score += 3 * decay; - else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") score += 4 * decay; - else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") score += 10 * decay; - else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") score += 8 * decay; - else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") score += 6 * decay; - else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") score += 3 * decay; - else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") score += 1 * decay; - else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") score += 6 * decay; - else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") score += 2 * decay; - else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") score += 3 * decay; - else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") score += 5 * decay; - else if (t === "larpjoinhouse") score += 15 * decay; - else if (t === "larphousepost") score += 6 * decay; - else if (t === "larptestattempt") score += 3 * decay; - else if (t === "torrent") score += 6 * decay; - else if (t === "shop" || t === "shopproduct") score += 6 * decay; - else if (t === "shop-purchase") score += 2 * decay; - else if (t === "pad" || t === "padentry") score += 3 * decay; - else if (t === "calendar" || t === "calendarnote" || t === "calendardate") score += 3 * decay; - else if (t === "chat") score += 1 * decay; - else if (t === "gamescore") score += 2 * decay; - else if (t === "pixelia") score += 2 * decay; - } - return Math.max(0, Math.round(score)); -} - -async function getCarbonGramsForUser(userId) { - const ssb = await openSsb(); - if (!ssb || !userId) return 0; - return new Promise((resolve) => { - let bytes = 0; - pull( - ssb.createUserStream({ id: userId }), - pull.drain( - (m) => { try { bytes += Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) {} }, - () => resolve((bytes / (1024 * 1024)) * 0.095) - ) - ); - }); -} - async function getBytesForUser(userId) { const ssb = await openSsb(); if (!ssb || !userId) return 0; @@ -624,7 +627,7 @@ async function getBytesForUser(userId) { } let _ecoTaxStatsCache = null; -const ECO_TAX_STATS_TTL_MS = 60 * 1000; +const ECO_TAX_STATS_TTL_MS = 10 * 60 * 1000; async function calculateEcoTaxStatsInternal() { const ssb = await (async () => { @@ -702,12 +705,14 @@ async function calculateEcoTaxStatsInternal() { }); } +let _ecoTaxStatsPending = null; async function calculateEcoTaxStats() { const now = Date.now(); if (_ecoTaxStatsCache && (now - _ecoTaxStatsCache.ts) < ECO_TAX_STATS_TTL_MS) { return _ecoTaxStatsCache.value; } - const value = await calculateEcoTaxStatsInternal().catch(() => null); + if (!_ecoTaxStatsPending) _ecoTaxStatsPending = calculateEcoTaxStatsInternal().catch(() => null).finally(() => { _ecoTaxStatsPending = null; }); + const value = await _ecoTaxStatsPending; if (value) _ecoTaxStatsCache = { ts: now, value }; return value || { totalBlocks: 0, totalBytes: 0, totalGramsCO2: 0, @@ -736,7 +741,7 @@ async function getUserFirstBlockTs(userId) { pull( ssb.createUserStream({ id: userId, limit: 1 }), pull.drain( - (m) => { if (m && m.value && m.value.timestamp && !first) first = Number(m.value.timestamp); }, + (m) => { if (m && m.value && !first) first = Number(m.timestamp) || 0; }, () => resolve(first || 0) ) ); @@ -754,82 +759,68 @@ async function getUserArchTax(userId) { return archTaxFromDays(ageDays); } -async function getUserEngagementScore(userId) { - const ssb = await openSsb(); +let _industryModel = null; +function industryModel() { + if (_industryModel) return _industryModel; + if (!services || !services.cooler) return null; + try { _industryModel = require("./industry_model")({ cooler: services.cooler }); } catch (_) { _industryModel = null; } + return _industryModel; +} + +async function getIndustryBalance(userId) { const uid = resolveUserId(userId); - const actions = await fetchUserActions(uid); - const rawKarma = scoreFromActions(actions); - const carbonGrams = await getCarbonGramsForUser(uid).catch(() => 0); - const karmaScore = Math.max(0, Math.round(rawKarma - carbonGrams)); - - const prev = await getLastKarmaScore(uid); - const lastPublishedTimestamp = await getLastPublishedTimestamp(uid); - - const isSelf = idsEqual(uid, ssb.id); - const hasSSB = !!(ssb && ssb.publish); - - const changed = (prev === null) || (karmaScore !== prev); - const nowMs = Date.now(); - const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0; - const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000; - - if (isSelf && hasSSB && changed && cooldownOk) { - await publishKarmaScore(uid, karmaScore); + const industry = industryModel(); + if (!industry) return { received: 0, sent: 0, net: 0, networkTotal: 0 }; + const builds = await industry.listAllBuilds().catch(() => []); + let production = 0; + let earned = 0; + for (const b of (builds || [])) { + const est = parseFloat(b && b.estTotal); + if (Number.isFinite(est) && est > 0) production += est; + const mine = parseFloat(b && b.points && b.points[uid]); + if (Number.isFinite(mine) && mine > 0) earned += mine; } - return karmaScore; + return { + received: Number(earned.toFixed(6)), + sent: 0, + net: Number(earned.toFixed(6)), + networkTotal: Number(production.toFixed(6)) + }; } -async function getLastKarmaScore(userId) { +async function getSchoolBalance(userId) { + const uid = resolveUserId(userId); const ssb = await openSsb(); - if (!ssb) return null; - return new Promise((resolve) => { - const source = ssb.messagesByType - ? ssb.messagesByType({ type: "karmaScore", reverse: true }) - : ssb.createLogStream && ssb.createLogStream({ reverse: true }); - if (!source) return resolve(null); - pull( - source, - pull.filter(msg => { - const v = msg.value || msg; - const c = v.content || {}; - return c && c.type === "karmaScore" && c.userId === userId; - }), - pull.take(1), - pull.collect((err, arr) => { - if (err || !arr || !arr.length) return resolve(null); - const v = arr[0].value || arr[0]; - const c = v.content || {}; - resolve(Number(c.karmaScore) || 0); - }) - ); - }); -} - -async function getLastPublishedTimestamp(userId) { - const ssb = await openSsb(); - if (!ssb) return new Date(0).toISOString(); - const fallback = new Date(0).toISOString(); - return new Promise((resolve) => { - const source = ssb.messagesByType - ? ssb.messagesByType({ type: "karmaScore", reverse: true }) - : ssb.createLogStream && ssb.createLogStream({ reverse: true }); - if (!source) return resolve(fallback); - pull( - source, - pull.filter(msg => { - const v = msg.value || msg; - const c = v.content || {}; - return c && c.type === "karmaScore" && c.userId === userId; - }), - pull.take(1), - pull.collect((err, arr) => { - if (err || !arr || !arr.length) return resolve(fallback); - const v = arr[0].value || arr[0]; - const c = v.content || {}; - resolve(c.timestamp || fallback); - }) - ); + const collect = (type) => new Promise((resolve) => { + if (!ssb.messagesByType) return resolve([]); + pull(ssb.messagesByType({ type }), pull.collect((err, msgs) => resolve(err ? [] : msgs))); }); + const [transfers, confirms] = await Promise.all([collect("transfer"), collect("transferConfirm")]); + const confirmsByTarget = new Map(); + for (const m of confirms) { + const c = m.value && m.value.content; + if (!c || !c.target) continue; + if (!confirmsByTarget.has(c.target)) confirmsByTarget.set(c.target, new Set()); + if (m.value.author) confirmsByTarget.get(c.target).add(m.value.author); + } + const monthKey = new Date().toISOString().slice(0, 7); + let received = 0; + let lifetime = 0; + for (const m of transfers) { + const c = m.value && m.value.content; + if (!c || c.to !== uid) continue; + const tags = Array.isArray(c.tags) ? c.tags.map(t => String(t).toUpperCase()) : []; + if (!tags.includes("SCHOOL")) continue; + const signatures = new Set(Array.isArray(c.confirmedBy) ? c.confirmedBy : []); + if (c.from) signatures.add(c.from); + for (const a of (confirmsByTarget.get(m.key) || [])) signatures.add(a); + if (signatures.size < 2) continue; + const amount = parseFloat(c.amount); + if (!Number.isFinite(amount) || amount <= 0) continue; + lifetime += amount; + if (String(c.createdAt || "").slice(0, 7) === monthKey) received += amount; + } + return { received: Number(received.toFixed(6)), lifetime: Number(lifetime.toFixed(6)), net: Number(received.toFixed(6)) }; } function computePoolVars(pubBal, rules) { @@ -841,10 +832,11 @@ async function getLastPublishedTimestamp(userId) { } async function computeEpoch({ epochId, userId, rules = DEFAULT_RULES }) { - const pubBal = await safeGetBalance("pub"); + const pubBal = await safeGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); const pv = computePoolVars(pubBal, rules); const addresses = await listAddressesMerged(); - const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address)); + const pubIds = await knownPubIds(); + const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address) && a.id !== config.keys.id && !pubIds.has(a.id)); const capUser = rules.caps?.cap_user_epoch ?? DEFAULT_RULES.caps.cap_user_epoch; const wMin = rules.caps?.w_min ?? DEFAULT_RULES.caps.w_min; const wMax = rules.caps?.w_max ?? DEFAULT_RULES.caps.w_max; @@ -899,7 +891,10 @@ async function getLastPublishedTimestamp(userId) { from: config.keys.id, to: a.user, amount: a.amount, - concept: `UBI ${eid}`, + ecoTax: a.ecoTax, + archTax: a.archTax, + totalTax: a.totalTax, + concept: `${UBI_PAYMENT_CONCEPT}${eid}`, status: "UNCLAIMED", createdAt: new Date().toISOString(), deadline: new Date(Date.now() + ((rules.graceDays ?? DEFAULT_RULES.graceDays) * 86400000)).toISOString(), @@ -924,25 +919,78 @@ async function getLastPublishedTimestamp(userId) { if (claimerId && allocation.to !== claimerId) throw new Error("This allocation is not for you."); const addr = await getUserAddress(allocation.to); if (!addr || !isValidEcoinAddress(addr)) throw new Error("No valid ECOin address registered."); - const txid = await rpcCall("sendtoaddress", [addr, allocation.amount, `UBI ${allocation.concept || "claim"}`], "pub"); - if (!txid) throw new Error("RPC sendtoaddress failed. Check PUB wallet configuration."); + const epochId = String((allocation.tags || []).find(t => String(t).startsWith("epoch:")) || "").replace(/^epoch:/, "") || epochIdNow(); + const paidKey = ubiPaidKey(epochId, allocation.to); + const pubBal = await safeGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); + if (computePoolVars(pubBal, DEFAULT_RULES).available < Number(allocation.amount)) throw new Error("The PUB wallet is at its reserve."); + if (ubiAddressTaken(epochId, addr, allocation.to)) throw new Error("This ECOin address was already paid this epoch."); + if (!reserveUbiPayment(paidKey)) throw new Error("Invalid allocation or already claimed."); + settleUbiPayment(paidKey, { attempts: 1, lastAttemptAt: new Date().toISOString(), amount: allocation.amount, address: addr }); + const sent = await sendVerified(addr, allocation.amount, UBI_SEND_COMMENT, "pub"); + const txid = sent.txid; + if (!txid) { + if (sent.unknown) settleUbiPayment(paidKey, { error: "the wallet did not answer" }); + else releaseUbiPayment(paidKey); + throw new Error("RPC sendtoaddress failed. Check PUB wallet configuration."); + } + settleUbiPayment(paidKey, { txid, paidAt: new Date().toISOString() }); await transfersRepo.markClosed(transferId, txid); + console.log(`[UBI] paid ${allocation.amount} ECO to ${String(allocation.to).slice(0, 12)}… (${epochId}) tx ${txid}`); + try { await publishUbiClaimResult(transferId, epochId, txid, allocation.to, allocation.amount, addr); } catch (_) {} + try { await publishUbiTransfer({ to: allocation.to, amount: allocation.amount, epochId, txid }); } catch (_) {} return { txid }; } + async function publishUbiTransfer({ to, amount, epochId, txid }) { + const ssb = await openSsb(); + if (!ssb || !ssb.publish) return; + const now = new Date().toISOString(); + const content = { + type: "transfer", + from: config.keys.id, + to, + concept: `${UBI_PAYMENT_CONCEPT}${epochId}`, + amount: Number(amount).toFixed(6), + category: "ECONOMIC", + createdAt: now, + updatedAt: now, + deadline: null, + confirmedBy: [config.keys.id, to], + status: "CLOSED", + tags: ["UBI", `epoch:${epochId}`], + opinions: {}, + opinions_inhabitants: [], + txid + }; + const published = await new Promise((resolve, reject) => ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg))); + if (services && typeof services.notifyUbiPaid === "function") { + try { await services.notifyUbiPaid({ to, amount: Number(amount).toFixed(6), epochId, txid, transferKey: published && published.key ? published.key : null }); } catch (_) {} + } + return published; + } + async function claimUBI(userId) { const uid = resolveUserId(userId); const epochId = epochIdNow(); - const pubId = getConfiguredPubId(); - if (!pubId) throw new Error("no_pub_configured"); - const alreadyClaimed = await hasClaimedThisMonth(uid); - if (alreadyClaimed) throw new Error("already_claimed"); - const ssb = await openSsb(); - if (!ssb || !ssb.publish) throw new Error("ssb_unavailable"); - const now = new Date().toISOString(); - const claimContent = { type: "ubiClaim", pubId, epochId, claimedAt: now }; - await new Promise((resolve, reject) => ssb.publish(claimContent, (err, res) => err ? reject(err) : resolve(res))); - return { status: "claimed_pending", epochId }; + const lockKey = ubiPaidKey(epochId, uid); + if (isPubNode()) throw new Error("pub"); + if (claimsInFlight.has(lockKey)) throw new Error("already_claimed"); + claimsInFlight.add(lockKey); + try { + const pubId = await resolvePubId(); + if (!pubId) throw new Error("no_pub_configured"); + const alreadyClaimed = await hasClaimedThisMonth(uid); + if (alreadyClaimed) throw new Error("already_claimed"); + if (await hasRefusedThisMonth(uid)) throw new Error("already_refused"); + const ssb = await openSsb(); + if (!ssb || !ssb.publish) throw new Error("ssb_unavailable"); + const now = new Date().toISOString(); + const claimContent = { type: "ubiClaim", pubId, epochId, claimedAt: now }; + await new Promise((resolve, reject) => ssb.publish(claimContent, (err, res) => err ? reject(err) : resolve(res))); + return { status: "claimed_pending", epochId }; + } finally { + claimsInFlight.delete(lockKey); + } } async function updateAllocationStatus(allocationId, status, txid) { @@ -960,13 +1008,107 @@ async function getLastPublishedTimestamp(userId) { } } - async function hasClaimedThisMonth(userId) { + async function hasRefusedThisMonth(userId) { const epochId = epochIdNow(); const msgs = await scanLogStream(); + return msgs.some(m => m.value && m.value.author === userId && m.value.content && m.value.content.type === "ubiRefuse" && m.value.content.epochId === epochId); + } + + async function refuseUBI(userId) { + const uid = resolveUserId(userId); + const epochId = epochIdNow(); + if (await hasClaimedThisMonth(uid)) throw new Error("already_claimed"); + if (await hasRefusedThisMonth(uid)) throw new Error("already_refused"); + const pubId = await resolvePubId(); + const ssb = await openSsb(); + if (!ssb || !ssb.publish) throw new Error("ssb_unavailable"); + const content = { type: "ubiRefuse", pubId, epochId, refusedAt: new Date().toISOString() }; + await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); + return { status: "refused", epochId }; + } + + async function claimAvailability(userId) { + const uid = resolveUserId(userId); + if (isPubNode()) return { available: false, reason: "pub" }; + const pub = await discoverUbiPub(); + if (!pub.pubId || !pub.available) return { available: false, reason: "no_pub" }; + const address = await getUserAddress(uid); + const cfg = getWalletCfg("user") || {}; + if (!address || !isValidEcoinAddress(address) || !hasWalletCredentials()) return { available: false, reason: "no_wallet" }; + if (await hasClaimedThisMonth(uid)) return { available: false, reason: "claimed" }; + if (await hasRefusedThisMonth(uid)) return { available: false, reason: "refused" }; + return { available: true, epochId: epochIdNow(), pubId: pub.pubId }; + } + + function pubIdsFrom(msgs) { + const ids = new Set(); + const defaultPubId = getDefaultPubId(); + if (defaultPubId) ids.add(defaultPubId); + try { if (isPubNode() && config.keys && config.keys.id) ids.add(config.keys.id); } catch (_) {} + for (const m of msgs || []) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "pubAvailability" && c.coin === "ECO" && v.author) ids.add(v.author); + } + return ids; + } + + async function trustedPubIds() { + const ids = pubIdsFrom([]); + const me = config.keys && config.keys.id; + const ssb = await openSsb(); + if (!ssb || !me) return ids; + const msgs = await readTyped(ssb, ["pub"], { limit: Math.max(getLogLimit(), 20000) }).catch(() => []); for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; - if (c.type === "ubiClaimResult" && c.userId === userId && c.epochId === epochId) return true; + const key = c.address && typeof c.address.key === "string" ? c.address.key : ""; + if (c.type !== "pub" || v.author !== me || !/^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(key) || ids.has(key)) continue; + if (await isFollowing(ssb, me, key)) ids.add(key); + } + if (!isPubNode()) return ids; + const announcers = await scanUbiAnnouncements().catch(() => new Map()); + const contacts = await readTyped(ssb, ["contact"], { limit: Math.max(getLogLimit(), 20000) }).catch(() => []); + const latest = new Map(); + for (const m of contacts) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type !== "contact" || v.author !== me || !announcers.has(c.contact)) continue; + const prev = latest.get(c.contact); + if (!prev || (Number(v.sequence) || 0) > prev.seq) latest.set(c.contact, { seq: Number(v.sequence) || 0, c }); + } + for (const [id, { c }] of latest) if (c.following === true && c.pub !== true && c.blocking !== true) ids.add(id); + return ids; + } + + function claimAddressees(msgs) { + const addressed = new Set(); + for (const m of msgs || []) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "ubiClaim" && v.author && c.pubId) addressed.add(`${c.epochId}:${v.author}:${c.pubId}`); + } + return addressed; + } + + const resultHonoured = (v, c, trusted, addressed) => !!v.author && (trusted.has(v.author) || addressed.has(`${c.epochId}:${c.userId}:${v.author}`)); + + async function knownPubIds() { + const ids = pubIdsFrom([]); + try { for (const id of (await scanUbiAnnouncements()).keys()) ids.add(id); } catch (_) {} + try { for (const id of await trustedPubIds()) ids.add(id); } catch (_) {} + return ids; + } + + async function hasClaimedThisMonth(userId) { + const epochId = epochIdNow(); + const msgs = await scanLogStream(); + const trusted = await trustedPubIds(); + const addressed = claimAddressees(msgs); + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "ubiClaimResult" && c.userId === userId && c.epochId === epochId && resultHonoured(v, c, trusted, addressed)) return true; if (c.type === "ubiClaim" && v.author === userId && c.epochId === epochId) return true; } return false; @@ -974,13 +1116,15 @@ async function getLastPublishedTimestamp(userId) { async function getUbiClaimHistory(userId) { const msgs = await scanLogStream(); + const trusted = await trustedPubIds(); + const addressed = claimAddressees(msgs); let lastClaimedDate = null; let totalClaimed = 0; let claimCount = 0; for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; - if (c.type === "ubiClaimResult" && c.userId === userId) { + if (c.type === "ubiClaimResult" && c.userId === userId && resultHonoured(v, c, trusted, addressed)) { totalClaimed += Number(c.amount) || 0; claimCount += 1; const d = c.processedAt || null; @@ -991,57 +1135,368 @@ async function getLastPublishedTimestamp(userId) { } async function getUbiAllocationsFromSSB() { - const pubId = getConfiguredPubId(); + const pubId = isPubNode() ? config.keys.id : await resolvePubId(); if (!pubId) return []; const msgs = await scanLogStream(); + const pubIds = pubIdsFrom(msgs); + const trusted = await trustedPubIds(); + const addressed = claimAddressees(msgs); + const paid = new Map(); + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "ubiClaimResult" && c.txid && c.epochId && c.userId && resultHonoured(v, c, trusted, addressed)) paid.set(ubiPaidKey(c.epochId, c.userId), c.txid); + } const out = []; + const seen = new Map(); for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; if (v.author === pubId && c && c.type === "ubiAllocation") { - out.push({ + if (!c.to || c.to === pubId || pubIds.has(c.to)) continue; + const dupKey = ubiPaidKey(c.epochId, c.to); + const prev = seen.get(dupKey); + if (prev && String(prev.createdAt || "") >= String(c.createdAt || "")) continue; + const txid = paid.get(dupKey) || null; + if (prev) out.splice(out.indexOf(prev), 1); + const entry = { id: c.allocationId, from: pubId, to: c.to, amount: c.amount, concept: c.concept, epochId: c.epochId, - status: c.status || "UNCLAIMED", + status: txid ? "CLOSED" : (c.status || "UNCLAIMED"), + txid, createdAt: c.createdAt || new Date().toISOString() - }); + }; + seen.set(dupKey, entry); + out.push(entry); } } return out; } - async function publishPubAvailability() { - if (!isPubNode()) return; - const balance = await safeGetBalance("pub"); - const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user ?? 1); - const available = Number(balance) >= floor; - const ssb = await openSsb(); - if (!ssb || !ssb.publish) return; - const content = { type: "pubAvailability", available, coin: "ECO", timestamp: Date.now() }; - await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); - return available; - } - - async function getPubAvailabilityFromSSB() { - const pubId = getConfiguredPubId(); - if (!pubId) return false; - const msgs = await scanLogStream(); - let latest = null; + let lastAnnounce = null; + async function loadLastAnnounce(ssb) { + if (lastAnnounce) return lastAnnounce; + const msgs = await readTyped(ssb, ["pubAvailability"], { limit: Math.max(getLogLimit(), 5000) }); + let best = { available: null, timestamp: 0 }; for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; - if (v.author === pubId && c && c.type === "pubAvailability" && c.coin === "ECO") { - if (!latest || (Number(c.timestamp) || 0) > (Number(latest.timestamp) || 0)) latest = c; - } + if (v.author !== config.keys.id || !c || c.type !== "pubAvailability") continue; + const ts = Number(c.timestamp) || 0; + if (ts > best.timestamp) best = { available: !!c.available, timestamp: ts, balance: Number(c.balance) || 0 }; } - return !!(latest && latest.available); + lastAnnounce = best; + return lastAnnounce; } - async function listBanking(filter = "overview", userId) { + async function scanUbiAnnouncements() { + const ssb = await openSsb(); + if (!ssb) return new Map(); + const msgs = await readTyped(ssb, ["pubAvailability"], { limit: Math.max(getLogLimit(), 5000) }); + const latestByPub = new Map(); + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (!c || c.type !== "pubAvailability" || c.coin !== "ECO") continue; + const ts = Number(c.timestamp) || Number(v.timestamp) || 0; + const prev = latestByPub.get(v.author); + if (!prev || ts > prev.timestamp) latestByPub.set(v.author, { pubId: v.author, available: !!c.available, timestamp: ts, balance: Number(c.balance) || 0, balanceKnown: c.balance !== undefined && c.balance !== null && Number.isFinite(Number(c.balance)), pool: Number(c.pool) || 0, address: isValidEcoinAddress(c.address) ? String(c.address) : null }); + } + return latestByPub; + } + + async function networkPubIds() { + const trusted = await trustedPubIds(); + const ids = new Set(trusted); + const ssb = await openSsb(); + if (!ssb) return ids; + const contacts = await readTyped(ssb, ["contact"], { limit: Math.max(getLogLimit(), 20000) }).catch(() => []); + const latest = new Map(); + for (const m of contacts) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type !== "contact" || !trusted.has(v.author) || typeof c.contact !== "string") continue; + const k = `${v.author} ${c.contact}`; + const seq = Number(v.sequence) || 0; + const prev = latest.get(k); + if (!prev || seq > prev.seq) latest.set(k, { seq, contact: c.contact, federated: c.following === true && c.pub !== true && c.blocking !== true }); + } + for (const { contact, federated } of latest.values()) if (federated) ids.add(contact); + return ids; + } + + async function listNetworkUbiPubs() { + const latestByPub = await scanUbiAnnouncements(); + const known = await networkPubIds(); + const out = []; + for (const cand of latestByPub.values()) { + if (!known.has(cand.pubId) || Date.now() - cand.timestamp > PUB_ANNOUNCE_MAX_AGE_MS || !isValidEcoinAddress(cand.address) || !cand.balanceKnown) continue; + out.push(cand); + } + return out; + } + + async function listUbiPubsDetailed() { + const pubs = await listNetworkUbiPubs(); + if (!pubs.length) return []; + const ssb = await openSsb(); + const { transfers } = ssb ? await readUbiLedger(ssb) : { transfers: [] }; + const defaultPubId = getDefaultPubId(); + const connected = (await discoverUbiPub()).pubId; + const me = isPubNode() && config.keys ? config.keys.id : null; + return pubs.map(p => { + const payouts = transfers.filter(t => t.author === p.pubId && isUbiPayoutConcept(t.concept)); + const lastPayoutAt = payouts.reduce((acc, t) => Math.max(acc, Date.parse(t.createdAt || "") || 0), 0); + const paidOut = payouts.reduce((acc, t) => acc + (Number(t.amount) || 0), 0); + const address = p.address || null; + return { ...p, address, lastPayoutAt, paidOut: Number(paidOut.toFixed(6)), payouts: payouts.length, isDefault: p.pubId === defaultPubId, isConnected: p.pubId === connected, isThisNode: !!me && p.pubId === me }; + }).sort((a, b) => (Number(b.isThisNode || b.isConnected) - Number(a.isThisNode || a.isConnected)) || (b.balance - a.balance)); + } + + async function listUbiRebalanceSeries(range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { points: [], hasAnyData: false, range: normalizeRange(range), bucket: "month" }; + const { transfers } = await readUbiLedger(ssb); + const known = await networkPubIds(); + const events = transfers + .filter(t => String(t.concept || "").startsWith(REBALANCE_CONCEPT) && known.has(t.author)) + .map(t => ({ ts: Date.parse(t.createdAt || "") || 0, rebalanced: Number(t.amount) || 0 })) + .filter(e => e.ts > 0) + .sort((a, b) => a.ts - b.ts); + return bucketEvents(events, range, explicit, ["rebalanced"]); + } + + async function listUbiCharts(range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { monthly: [], pool: [] }; + const { transfers } = await readUbiLedger(ssb); + const byMonth = new Map(); + for (const t of transfers) { + if (!isUbiPayoutConcept(t.concept)) continue; + const month = (String(t.concept).match(/(\d{4}-\d{2})/) || [])[1] || String(t.createdAt || "").slice(0, 7); + if (!month) continue; + const cur = byMonth.get(month) || { month, amount: 0, count: 0 }; + cur.amount += Number(t.amount) || 0; + cur.count += 1; + byMonth.set(month, cur); + } + const monthly = Array.from(byMonth.values()).sort((a, b) => a.month.localeCompare(b.month)).map(m => ({ ...m, amount: Number(m.amount.toFixed(6)) })); + const msgs = await readTyped(ssb, ["pubAvailability"], { limit: Math.max(getLogLimit(), 5000) }); + const trusted = await trustedPubIds(); + const events = msgs + .filter(m => m.value && m.value.content && m.value.content.type === "pubAvailability" && m.value.content.coin === "ECO" && trusted.has(m.value.author) && Number.isFinite(Number(m.value.content.balance))) + .map(m => ({ ts: Number(m.value.content.timestamp) || Number(m.value.timestamp) || 0, pubId: m.value.author, balance: Number(m.value.content.balance) })) + .filter(x => x.ts > 0) + .sort((a, b) => a.ts - b.ts); + const latestByPub = new Map(); + const pool = []; + for (const e of events) { + latestByPub.set(e.pubId, e.balance); + let total = 0; + for (const v of latestByPub.values()) total += v; + const last = pool[pool.length - 1]; + if (last && last.ts === e.ts) last.balance = Number(total.toFixed(6)); + else pool.push({ ts: e.ts, balance: Number(total.toFixed(6)) }); + } + const fullPool = pool.some(p => p.balance > 0) ? pool : []; + const monthTs = (m) => Date.parse(`${m}-01T00:00:00Z`) || 0; + const monthlyTs = monthly.map(m => ({ ...m, ts: monthTs(m.month) })); + const poolRange = explicit ? normalizeRange(range) : widenRange(fullPool, range); + const monthlyRange = explicit ? normalizeRange(range) : widenRange(monthlyTs, range); + return { + monthly: filterByRange(monthlyTs, monthlyRange).slice(-120), + pool: filterByRange(fullPool, poolRange).slice(-120), + hasMonthly: monthlyTs.length >= 2, + hasPool: fullPool.length >= 2, + poolRange, + monthlyRange, + pubCount: latestByPub.size + }; + } + + function bucketEvents(events, range, explicit, fields) { + const effective = explicit ? normalizeRange(range) : widenRange(events, range); + const inRange = filterByRange(events, effective); + const bucketMs = effective === "today" ? 60 * 60 * 1000 : (effective === "week" || effective === "month") ? ONE_DAY_MS : 0; + const bucketOf = (ts) => { + if (bucketMs) return Math.floor(ts / bucketMs) * bucketMs; + const d = new Date(ts); + return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1); + }; + const buckets = new Map(); + for (const e of inRange) { + const b = bucketOf(e.ts); + const cur = buckets.get(b) || fields.reduce((acc, f) => ({ ...acc, [f]: 0 }), { ts: b }); + for (const f of fields) cur[f] += Number(e[f]) || 0; + buckets.set(b, cur); + } + let points = Array.from(buckets.values()).sort((a, b) => a.ts - b.ts) + .map(p => fields.reduce((acc, f) => ({ ...acc, [f]: Number(Number(p[f]).toFixed(6)) }), { ts: p.ts })) + .slice(-240); + if (points.length === 1) { + const span = RANGE_MS[effective]; + const baseTs = Number.isFinite(span) ? Date.now() - span : points[0].ts - (bucketMs || 30 * ONE_DAY_MS); + points = [fields.reduce((acc, f) => ({ ...acc, [f]: 0 }), { ts: Math.min(baseTs, points[0].ts - 1) }), ...points]; + } + return { points, range: effective, bucket: bucketMs === 60 * 60 * 1000 ? "hour" : bucketMs ? "day" : "month", hasAnyData: events.length >= 1 }; + } + + async function listUbiPaymentsSeries(range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { points: [], hasAnyData: false, range: normalizeRange(range), bucket: "month" }; + const { transfers } = await readUbiLedger(ssb); + const events = transfers + .filter(t => isUbiPayoutConcept(t.concept)) + .map(t => ({ ts: Date.parse(t.createdAt || "") || 0, distributed: Number(t.amount) || 0 })) + .filter(e => e.ts > 0) + .sort((a, b) => a.ts - b.ts); + return bucketEvents(events, range, explicit, ["distributed"]); + } + + async function listWealthSeries(range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { points: [], totals: { distributed: 0, taxes: 0 } }; + const { transfers } = await readUbiLedger(ssb); + const allocs = await readTyped(ssb, ["ubiAllocation"], { limit: Math.max(getLogLimit(), 20000) }); + const events = []; + for (const t of transfers) { + if (!isUbiPayoutConcept(t.concept)) continue; + const ts = Date.parse(t.createdAt || "") || 0; + if (ts) events.push({ ts, distributed: Number(t.amount) || 0, taxes: 0 }); + } + for (const m of allocs) { + const c = m.value && m.value.content; + if (!c || c.type !== "ubiAllocation") continue; + const ts = Date.parse(c.createdAt || "") || Number(m.value.timestamp) || 0; + if (ts) events.push({ ts, distributed: 0, taxes: Number(c.totalTax) || 0 }); + } + events.sort((a, b) => a.ts - b.ts); + const totals = events.reduce((acc, e) => ({ distributed: acc.distributed + e.distributed, taxes: acc.taxes + e.taxes }), { distributed: 0, taxes: 0 }); + totals.distributed = Number(totals.distributed.toFixed(6)); + totals.taxes = Number(totals.taxes.toFixed(6)); + return { ...bucketEvents(events, range, explicit, ["distributed", "taxes"]), totals }; + } + + async function listNetworkEpochs() { + const ssb = await openSsb(); + if (!ssb) return []; + const byEpoch = new Map(); + const bump = (id, amount, to) => { + if (!id) return; + const cur = byEpoch.get(id) || { id, pool: 0, recipients: new Set() }; + cur.pool += Number(amount) || 0; + if (to) cur.recipients.add(to); + byEpoch.set(id, cur); + }; + const allocs = await getUbiAllocationsFromSSB().catch(() => []); + for (const a of allocs) bump(String(a.epochId || ""), a.amount, a.to); + if (!byEpoch.size) { + const { transfers } = await readUbiLedger(ssb); + for (const t of transfers) { + if (!isUbiPayoutConcept(t.concept)) continue; + const tag = (t.tags || []).find(x => /^epoch:\d{4}-\d{2}$/i.test(String(x))); + const id = tag ? String(tag).slice(6) : ((String(t.concept || "").match(/(\d{4}-\d{2})/) || [])[1] || ""); + bump(id, t.amount, t.to); + } + } + return Array.from(byEpoch.values()) + .map(e => ({ id: e.id, pool: Number(e.pool.toFixed(6)), recipients: e.recipients.size })) + .sort((a, b) => String(b.id).localeCompare(String(a.id))); + } + + async function sampleUserFunds() { + if (!hasWalletCredentials()) return false; + const hist = readFundsHistory(); + const last = Array.isArray(hist) && hist.length ? hist[hist.length - 1] : null; + if (last && Date.now() - Number(last.ts) < ECO_HISTORY_MIN_GAP_MS) return false; + try { + const balance = Number(await rpcCall("getbalance", [], "user")); + if (!Number.isFinite(balance)) return false; + appendFundsHistory({ ts: Date.now(), balance }); + return true; + } catch (_) { return false; } + } + + function listFundsHistory(range = "all", explicit = true) { + const all = readFundsHistory() + .map(s => ({ ts: Number(s.ts) || 0, balance: Number(s.balance) || 0 })) + .filter(x => x.ts > 0) + .sort((a, b) => a.ts - b.ts); + const effective = explicit ? normalizeRange(range) : widenRange(all, range); + return { points: filterByRange(all, effective).slice(-240), hasAny: all.length >= 2, range: effective }; + } + + async function listUbiPubs() { + const latestByPub = await scanUbiAnnouncements(); + const trusted = await trustedPubIds(); + const out = []; + for (const cand of latestByPub.values()) { + if (!trusted.has(cand.pubId) || Date.now() - cand.timestamp > PUB_ANNOUNCE_MAX_AGE_MS) continue; + out.push(cand); + } + return out.sort((a, b) => b.timestamp - a.timestamp); + } + + async function publishPubAvailability() { + if (!isPubNode()) return; + const raw = await tryGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); + const balance = raw === null ? 0 : raw; + const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user ?? 1); + const pv = computePoolVars(balance, DEFAULT_RULES); + const available = raw !== null && pv.available >= floor; + const ssb = await openSsb(); + if (!ssb || !ssb.publish) return; + const last = await loadLastAnnounce(ssb); + const stale = Date.now() - last.timestamp > PUB_ANNOUNCE_REFRESH_MS; + const announcedBalance = Number(pv.available.toFixed(6)); + const balanceMoved = raw !== null && Math.abs(announcedBalance - (Number(last.balance) || 0)) > Math.max(1, (Number(last.balance) || 0) * 0.2); + if (last.available === available && !stale && !balanceMoved) return available; + const address = await getAnyWalletAddress().catch(() => null); + const content = { type: "pubAvailability", available, coin: "ECO", ...(raw === null ? {} : { balance: announcedBalance, pool: Number(pv.pool.toFixed(6)) }), address: isValidEcoinAddress(address) ? address : undefined, timestamp: Date.now() }; + await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); + lastAnnounce = { available, timestamp: content.timestamp, balance: raw === null ? 0 : announcedBalance }; + console.log(`[UBI] announced ${available ? "available" : "unavailable"} (PUB wallet ${Number(balance).toFixed(6)} ECO)`); + return available; + } + + let discoveredPub = { pubId: "", available: false, timestamp: 0, checkedAt: 0 }; + async function discoverUbiPub() { + if (Date.now() - discoveredPub.checkedAt < PUB_DISCOVERY_TTL_MS) return discoveredPub; + const latestByPub = await scanUbiAnnouncements(); + const trusted = await trustedPubIds(); + const defaultPubId = getDefaultPubId(); + const fresh = (cand) => cand && Date.now() - cand.timestamp <= PUB_ANNOUNCE_MAX_AGE_MS; + const defaultCand = latestByPub.get(defaultPubId) || null; + let best = null; + if (fresh(defaultCand) && defaultCand.available) best = defaultCand; + else { + for (const cand of latestByPub.values()) { + if (!trusted.has(cand.pubId) || !fresh(cand) || !cand.available) continue; + if (!best || cand.timestamp > best.timestamp) best = cand; + } + } + if (!best && defaultPubId) best = { pubId: defaultPubId, available: false, timestamp: (defaultCand && defaultCand.timestamp) || 0 }; + discoveredPub = { pubId: (best && best.pubId) || "", available: !!(best && best.available), timestamp: (best && best.timestamp) || 0, balance: (best && best.balance) || 0, pool: (best && best.pool) || 0, checkedAt: Date.now() }; + return discoveredPub; + } + + async function resolvePubId() { + return (await discoverUbiPub()).pubId; + } + + async function getPubAvailabilityFromSSB() { + const pubId = await resolvePubId(); + if (!pubId) return false; + return (await discoverUbiPub()).available; + } + + async function listBanking(filter = "overview", userId, opts = {}) { + const explicitRange = !!opts.range; + const range = normalizeRange(opts.range); const uid = resolveUserId(userId); const epochId = epochIdNow(); let pubBalance = 0; @@ -1050,8 +1505,7 @@ async function getLastPublishedTimestamp(userId) { if (isPubNode()) { pubBalance = await safeGetBalance("pub"); const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user ?? 1); - ubiAvailable = Number(pubBalance) >= floor; - try { await publishPubAvailability(); } catch (_) {} + ubiAvailable = computePoolVars(Number(pubBalance) || 0, DEFAULT_RULES).available >= floor; const all = await transfersRepo.listByTag("UBI"); allocations = all.map(t => ({ id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status, @@ -1061,37 +1515,82 @@ async function getLastPublishedTimestamp(userId) { ubiAvailable = await getPubAvailabilityFromSSB(); allocations = await getUbiAllocationsFromSSB(); } - const userBalance = await safeGetBalance("user"); - const epochs = await epochsRepo.list(); + let userBalance = 0; + if (hasWalletCredentials()) { + try { + userBalance = Number(await rpcCall("getbalance", [], "user")) || 0; + appendFundsHistory({ ts: Date.now(), balance: userBalance }); + } catch (_) { userBalance = 0; } + } + const industryBal = await getIndustryBalance(uid).catch(() => ({ received: 0, sent: 0, net: 0, networkTotal: 0 })); + const schoolBal = await getSchoolBalance(uid).catch(() => ({ received: 0, lifetime: 0, net: 0 })); + let epochs = await epochsRepo.list(); + if (!epochs.length) epochs = await listNetworkEpochs().catch(() => []); let computed = null; - try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {} + if (filter === "overview" || isPubNode()) { + try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {} + } const pv = computePoolVars(pubBalance, DEFAULT_RULES); const actions = await fetchUserActions(uid); const rawScore = scoreFromActions(actions); const carbonGramsForScore = await getCarbonGramsForUser(uid).catch(() => 0); const engagementScore = Math.max(0, Math.round(rawScore - carbonGramsForScore)); - const poolForEpoch = computed?.epoch?.pool || pv.pool || 0; + let poolForEpoch = computed?.epoch?.pool || pv.pool || 0; + if (!isPubNode()) { + const info = await discoverUbiPub(); + poolForEpoch = Number(info.pool) || computePoolVars((Number(info.balance) || 0) + (DEFAULT_RULES.reserveMin || 0), DEFAULT_RULES).pool || 0; + } const futureUBI = Number(((engagementScore / 100) * poolForEpoch).toFixed(6)); const addresses = await listAddressesMerged(); const alreadyClaimed = await hasClaimedThisMonth(uid); - const pubId = getConfiguredPubId(); + const pubId = isPubNode() ? config.keys.id : await resolvePubId(); + const pubLastSeen = isPubNode() ? (lastAnnounce ? lastAnnounce.timestamp : 0) : (await discoverUbiPub()).timestamp; const userAddress = await getUserAddress(uid); const userWalletCfg = getWalletCfg("user") || {}; - const hasValidWallet = !!(userAddress && isValidEcoinAddress(userAddress) && userWalletCfg.url); + const hasValidWallet = !!(userAddress && isValidEcoinAddress(userAddress) && hasWalletCredentials()); + const addressPublished = await hasPublishedAddress(uid).catch(() => false); const summary = { userBalance, + industryBalance: industryBal.net, + schoolBalance: schoolBal.net, + schoolLifetime: schoolBal.lifetime, + industryNetworkTotal: industryBal.networkTotal, + industryReceived: industryBal.received, + industrySent: industryBal.sent, epochId, pool: poolForEpoch, weightsSum: computed?.epoch?.weightsSum || 0, userEngagementScore: engagementScore, futureUBI, alreadyClaimed, + alreadyRefused: await hasRefusedThisMonth(uid).catch(() => false), pubId, + wealthTotals: (await listWealthSeries("all").catch(() => ({ totals: { distributed: 0, taxes: 0 } }))).totals, + pubLastSeen, + pubBalance: isPubNode() ? pubBalance : null, + addressPublished, hasValidWallet, ubiAvailability: ubiAvailable ? "OK" : "NO_FUNDS" }; const exchange = await calculateEcoinValue(); - const exchangeHistory = readEcoHistory(); + if (filter === 'exchange') { + const pubs = await listUbiPubs().catch(() => []); + exchange.pubsSupply = Number(pubs.reduce((acc, p) => acc + (Number(p.balance) || 0), 0).toFixed(6)); + const lastSample = readEcoHistory().filter(x => Number(x.currentSupply) > 0).pop(); + const generated = Number(exchange.currentSupply) > 0 ? Number(exchange.currentSupply) : (lastSample ? Number(lastSample.currentSupply) : 0); + exchange.existingSupply = generated > 0 ? Number(generated.toFixed(6)) : null; + exchange.holdingSupply = generated > 0 ? Number(Math.max(0, generated - exchange.pubsSupply).toFixed(6)) : null; + } + const fullHistory = readEcoHistory(); + const valueRange = explicitRange ? range : widenRange(fullHistory, range); + let exchangeHistory = filterByRange(fullHistory, valueRange); + if (exchangeHistory.length === 1 && fullHistory.length > 1) { + const firstTs = Number(exchangeHistory[0].ts) || 0; + const before = fullHistory.filter(x => Number(x.ts) < firstTs).pop(); + const span = RANGE_MS[normalizeRange(valueRange)]; + if (before) exchangeHistory = [{ ...before, ts: Number.isFinite(span) ? Math.max(Number(before.ts), Date.now() - span) : Number(before.ts) }, ...exchangeHistory]; + } + const valueHasAnyData = Array.isArray(fullHistory) && fullHistory.length >= 2; let taxStats = null; let userEcoinTax = 0; if (filter === 'taxes' || filter === 'exchange' || filter === 'overview') { @@ -1104,8 +1603,15 @@ async function getLastPublishedTimestamp(userId) { try { userArchTax = await getUserArchTax(uid); } catch (_) { userArchTax = 0; } } const userTotalTax = (userEcoinTax || 0) + (userArchTax || 0); + const ubiPubs = filter === 'ubi' ? await listUbiPubsDetailed().catch(() => []) : []; + const ubiCharts = filter === 'ubi' ? await listUbiCharts(range, explicitRange).catch(() => null) : null; + const karmaHistory = filter === 'overview' ? await listKarmaHistory(uid, range, explicitRange).catch(() => ({ points: [], hasAny: false, range })) : { points: [], hasAny: false, range }; + const fundsHistory = filter === 'overview' ? listFundsHistory(range, explicitRange) : { points: [], hasAny: false, range }; + const wealth = filter === 'exchange' || filter === 'overview' ? await listWealthSeries(range, explicitRange).catch(() => ({ points: [], totals: { distributed: 0, taxes: 0 } })) : { points: [], totals: { distributed: 0, taxes: 0 } }; + const ubiPayments = filter === 'ubi' || filter === 'overview' ? await listUbiPaymentsSeries(range, explicitRange).catch(() => ({ points: [], hasAnyData: false })) : { points: [], hasAnyData: false }; + const ubiRebalances = filter === 'ubi' ? await listUbiRebalanceSeries(range, explicitRange).catch(() => ({ points: [], hasAnyData: false })) : { points: [], hasAnyData: false }; return { - summary, allocations, epochs, rules: DEFAULT_RULES, taxRules, addresses, exchange, exchangeHistory, taxStats, + summary, allocations, epochs, rules: DEFAULT_RULES, taxRules, addresses, exchange, exchangeHistory, taxStats, ubiPubs, ubiCharts, karmaHistory, fundsHistory, wealth, ubiPayments, ubiRebalances, range, valueRange, valueHasAnyData, userEcoinTax: Number((userEcoinTax || 0).toFixed(6)), userArchTax: Number((userArchTax || 0).toFixed(6)), userTotalTax: Number(userTotalTax.toFixed(6)) @@ -1121,17 +1627,27 @@ async function getLastPublishedTimestamp(userId) { async function getEpochById(id) { const existing = await epochsRepo.get(id); if (existing) return existing; - const all = await transfersRepo.listAll(); - const filtered = all.filter(t => (t.tags || []).includes(`epoch:${id}`)); - const pool = filtered.reduce((s, t) => s + Number(t.amount || 0), 0); - return { id, pool, weightsSum: 0, rules: DEFAULT_RULES, hash: "-" }; + const allocations = await listEpochAllocations(id).catch(() => []); + if (!allocations.length) return null; + const pool = allocations.reduce((s, t) => s + Number(t.amount || 0), 0); + return { id, pool: Number(pool.toFixed(6)), recipients: allocations.length, rules: DEFAULT_RULES }; } async function listEpochAllocations(id) { const all = await transfersRepo.listAll(); - return all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({ + const local = all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({ id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status, createdAt: t.createdAt || new Date().toISOString(), txid: t.txid })); + if (local.length) return local; + const published = await getUbiAllocationsFromSSB().catch(() => []); + const fromNetwork = published.filter(a => String(a.epochId || "") === String(id)); + if (fromNetwork.length) return fromNetwork; + const ssb = await openSsb(); + if (!ssb) return []; + const { transfers } = await readUbiLedger(ssb); + return transfers + .filter(t => isUbiPayoutConcept(t.concept) && (String(t.concept || "").includes(id) || (t.tags || []).includes(`epoch:${id}`))) + .map(t => ({ id: t.key, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status || "UNCONFIRMED", createdAt: t.createdAt || new Date().toISOString(), txid: t.txid })); } let genesisTimeCache = null; @@ -1156,6 +1672,63 @@ async function getLastPublishedTimestamp(userId) { } catch (_) { return 0; } } + async function getRecentBlockSeconds(blocks, window = 720) { + if (!blocks || blocks <= window + 1) return 0; + try { + const hCur = await rpcCall("getblockhash", [blocks]); + const hOld = await rpcCall("getblockhash", [blocks - window]); + if (!hCur || !hOld) return 0; + const bCur = await rpcCall("getblock", [hCur]); + const bOld = await rpcCall("getblock", [hOld]); + const elapsed = (bCur?.time || 0) - (bOld?.time || 0); + return elapsed > 0 ? elapsed / window : 0; + } catch (_) { return 0; } + } + + function observedIssuancePerHour(currentSupply) { + const hist = readEcoHistory(); + if (!Array.isArray(hist) || hist.length < 2 || !(currentSupply > 0)) return null; + const now = Date.now(); + const windowStart = now - 30 * ONE_DAY_MS; + const base = hist.find(s => Number(s.ts) >= windowStart && Number(s.currentSupply) > 0) || hist.find(s => Number(s.currentSupply) > 0); + if (!base) return null; + const spanMs = now - Number(base.ts); + if (spanMs < 6 * 60 * 60 * 1000) return null; + const minted = currentSupply - Number(base.currentSupply); + if (!Number.isFinite(minted) || minted < 0) return null; + return minted / (spanMs / (3600 * 1000)); + } + + function observedInflationFromHistory(currentSupply) { + const hist = readEcoHistory(); + if (!Array.isArray(hist) || hist.length < 2 || !(currentSupply > 0)) return null; + const now = Date.now(); + const minSpanMs = 6 * 60 * 60 * 1000; + const oldEnough = (s) => Number(s.currentSupply) > 0 && now - Number(s.ts) >= minSpanMs; + const base = hist.find(s => Number(s.ts) >= now - 7 * ONE_DAY_MS && oldEnough(s)) + || hist.find(s => Number(s.ts) >= now - 30 * ONE_DAY_MS && oldEnough(s)) + || hist.find(oldEnough); + if (!base) return null; + const spanMs = now - Number(base.ts); + const growth = (currentSupply - Number(base.currentSupply)) / Number(base.currentSupply); + return growth * (365 * ONE_DAY_MS / spanMs) * 100; + } + + async function ubiPerHourPerInhabitant() { + const pool = isPubNode() + ? computePoolVars(await safeGetBalance("pub"), DEFAULT_RULES).pool + : (Number((await discoverUbiPub()).pool) || 0); + const pubIds = await knownPubIds(); + const addresses = await listAddressesMerged().catch(() => []); + const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address) && !pubIds.has(a.id) && !(isPubNode() && a.id === config.keys.id)).length; + const cap = DEFAULT_RULES.caps.cap_user_epoch; + const floor = DEFAULT_RULES.caps.floor_user ?? 1; + const perInhabitant = pool > 0 && eligible > 0 ? Math.max(floor, Math.min(cap, pool / eligible)) : 0; + const now = new Date(); + const hoursInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate() * 24; + return perInhabitant / hoursInMonth; + } + async function calculateEcoinValue() { const totalSupply = 25500000; let circulatingSupply = 0; @@ -1170,12 +1743,18 @@ async function getLastPublishedTimestamp(userId) { const mining = await rpcCall("getmininginfo", []); blockValueEco = (mining?.blockvalue || 0) / 1e8; } catch (_) {} - const avgSec = await getAvgBlockSeconds(blocks); - const ecoValuePerHour = avgSec > 0 ? (3600 / avgSec) * blockValueEco : 0; + const recentSec = await getRecentBlockSeconds(blocks); + const avgSec = recentSec > 0 ? recentSec : await getAvgBlockSeconds(blocks); + const nominalPerHour = avgSec > 0 ? (3600 / avgSec) * blockValueEco : 0; + const observedPerHour = isSynced ? observedIssuancePerHour(circulatingSupply) : null; + const issuancePerHour = observedPerHour !== null ? observedPerHour : nominalPerHour; + const ecoValuePerHour = await ubiPerHourPerInhabitant().catch(() => 0); const maturity = totalSupply > 0 ? circulatingSupply / totalSupply : 0; const ecoTimeMs = maturity * 3600 * 1000; - const annualIssuance = ecoValuePerHour * 24 * 365; - const inflationFactor = circulatingSupply > 0 ? (annualIssuance / circulatingSupply) * 100 : 0; + const annualIssuance = issuancePerHour * 24 * 365; + const inflationIssuance = circulatingSupply > 0 ? (annualIssuance / circulatingSupply) * 100 : 0; + const inflationObserved = isSynced ? observedInflationFromHistory(circulatingSupply) : null; + const inflationFactor = inflationObserved !== null && Number.isFinite(inflationObserved) ? inflationObserved : inflationIssuance; const inflationMonthly = inflationFactor / 12; const result = { ecoValue: Number(ecoValuePerHour.toFixed(6)), @@ -1183,6 +1762,8 @@ async function getLastPublishedTimestamp(userId) { totalSupply, inflationFactor: Number(inflationFactor.toFixed(2)), inflationMonthly: Number(inflationMonthly.toFixed(2)), + inflationIssuance: Number(inflationIssuance.toFixed(2)), + inflationObserved: inflationObserved === null ? null : Number(inflationObserved.toFixed(2)), currentSupply: circulatingSupply, isSynced }; @@ -1190,6 +1771,7 @@ async function getLastPublishedTimestamp(userId) { appendEcoHistory({ ts: Date.now(), ecoValue: result.ecoValue, + ecoTimeMs: result.ecoTimeMs, currentSupply: result.currentSupply, inflationFactor: result.inflationFactor, blockValueEco: Number(blockValueEco.toFixed(8)), @@ -1200,15 +1782,25 @@ async function getLastPublishedTimestamp(userId) { return result; } + const perRequest = (key, fn) => { + const store = requestScope.getStore(); + if (!store) return fn(); + const memo = store.bankingMemo || (store.bankingMemo = new Map()); + if (!memo.has(key)) memo.set(key, fn()); + return memo.get(key); + }; + async function getBankingData(userId) { - const ecoValue = await calculateEcoinValue(); + const ecoValue = await perRequest("ecoinValue", () => calculateEcoinValue()); const karmaScore = await getUserEngagementScore(userId); + const received = await karma.getKarmaBreakdown(userId); + const karmaReceived = { positive: received.positive, negative: received.negative }; let estimatedUBI = 0; try { - const pubBal = isPubNode() ? await safeGetBalance("pub") : 0; + const pubBal = isPubNode() ? await perRequest("pubBalance", () => safeGetBalance("pub")) : 0; const pv = computePoolVars(pubBal, DEFAULT_RULES); const pool = pv.pool || 0; - const addresses = await listAddressesMerged(); + const addresses = await perRequest("addresses", () => listAddressesMerged()); const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address)); const wMin = DEFAULT_RULES.caps?.w_min ?? 0.2; const wMax = DEFAULT_RULES.caps?.w_max ?? 6; @@ -1233,6 +1825,7 @@ async function getLastPublishedTimestamp(userId) { return { ecoValue, karmaScore, + karmaReceived, estimatedUBI, estimatedUBIBeforeTax: Number(estimatedUBIBeforeTax.toFixed(6)), userEcoinTax: Number(userEcoinTax.toFixed(6)), @@ -1264,6 +1857,9 @@ async function getLastPublishedTimestamp(userId) { allocationId: allocation.id, to: allocation.to, amount: allocation.amount, + ecoTax: Number(allocation.ecoTax) || 0, + archTax: Number(allocation.archTax) || 0, + totalTax: Number(allocation.totalTax) || 0, concept: allocation.concept, epochId: epochTag ? epochTag.slice(6) : "", status: "UNCLAIMED", @@ -1279,87 +1875,367 @@ async function getLastPublishedTimestamp(userId) { return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); } - async function publishUbiClaimResult(allocationId, epochId, txid, userId, amount) { + async function publishUbiClaimResult(allocationId, epochId, txid, userId, amount, address) { const ssb = await openSsb(); if (!ssb) return; - const content = { type: "ubiClaimResult", allocationId, epochId, txid, userId, amount, processedAt: new Date().toISOString() }; + const content = { type: "ubiClaimResult", allocationId, epochId, txid, userId, amount, ...(address ? { address: String(address) } : {}), processedAt: new Date().toISOString() }; return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); } + async function isEligibleClaimant(userId) { + if ((isPubNode() && userId === config.keys.id) || (await knownPubIds()).has(userId)) return { ok: false, reason: "a PUB cannot claim the UBI" }; + const addr = await getUserAddress(userId); + if (!addr || !isValidEcoinAddress(addr)) return { ok: false, reason: "no published ECOin address" }; + const firstTs = await getUserFirstBlockTs(userId).catch(() => 0); + if (!firstTs || Date.now() - firstTs < CLAIMANT_MIN_AGE_MS) return { ok: false, reason: "feed younger than 30 days" }; + const score = await getUserEngagementScore(userId).catch(() => 0); + if (!(Number(score) > 0)) return { ok: false, reason: "no activity" }; + return { ok: true, address: addr, score: Number(score) }; + } + + function ubiAmountFor({ pool, userW, totalW, score, ecoTax, archTax }) { + const floorUbi = Math.max(1, DEFAULT_RULES.caps.floor_user ?? 1); + const capUser = DEFAULT_RULES.caps.cap_user_epoch; + const gross = Math.max(floorUbi, Math.min(pool * userW / totalW, capUser)); + const surplus = Math.max(0, gross - floorUbi); + const tax = (Number(ecoTax) || 0) + (Number(archTax) || 0); + return Number((floorUbi + Math.max(0, surplus - tax)).toFixed(6)); + } + + const previousEpochId = () => { + const d = new Date(); + d.setUTCDate(1); + d.setUTCMonth(d.getUTCMonth() - 1); + return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`; + }; + + const epochStartMs = (eid) => { + const m = /^(\d{4})-(\d{2})$/.exec(String(eid || "")); + return m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, 1) : 0; + }; + + function claimPaidByMe(claim, me, defaultPubId, nowMs) { + const addressed = claim.pubId || defaultPubId; + const age = nowMs - (Number(claim._ts) || nowMs); + if (addressed === me) return me === defaultPubId || age < UBI_TAKEOVER_AFTER_MS; + if (!defaultPubId || me !== defaultPubId) return false; + const eid = claim.epochId || ""; + return age >= UBI_TAKEOVER_AFTER_MS + UBI_TAKEOVER_MARGIN_MS && (eid === epochIdNow() || eid === previousEpochId()); + } + async function processPendingClaims() { if (!isPubNode()) return; const ssb = await openSsb(); if (!ssb) return; const claims = []; const results = []; + const pubIds = await knownPubIds(); + const trusted = await trustedPubIds(); + const addressed = new Set(); + const seenClaims = new Set(); await new Promise((resolve, reject) => { pull(ssb.messagesByType({ type: "ubiClaim", reverse: false }), pull.drain(msg => { if (msg.value?.content?.type === "ubiClaim") { - claims.push({ ...msg.value.content, _author: msg.value.author }); + if (msg.value.content.pubId) addressed.add(`${msg.value.content.epochId}:${msg.value.author}:${msg.value.content.pubId}`); + const key = ubiPaidKey(msg.value.content.epochId || "", msg.value.author); + if (seenClaims.has(key)) return; + seenClaims.add(key); + claims.push({ ...msg.value.content, _author: msg.value.author, _ts: Number(msg.value.timestamp) || Date.parse(msg.value.content.claimedAt || "") || 0 }); } }, err => err ? reject(err) : resolve())); }); await new Promise((resolve, reject) => { pull(ssb.messagesByType({ type: "ubiClaimResult", reverse: false }), - pull.drain(msg => { if (msg.value?.content?.type === "ubiClaimResult") results.push(msg.value.content); }, + pull.drain(msg => { if (msg.value?.content?.type === "ubiClaimResult" && resultHonoured(msg.value, msg.value.content, trusted, addressed)) results.push(msg.value.content); }, err => err ? reject(err) : resolve())); }); const processedEpochUser = new Set(results.map(r => `${r.epochId}:${r.userId}`)); + const paidAddressBy = new Map(); + for (const r of results) if (r.address && r.epochId && r.userId) paidAddressBy.set(`${r.epochId}:${r.address}`, r.userId); + const refused = new Set(); + await new Promise((resolve, reject) => { + pull(ssb.messagesByType({ type: "ubiRefuse", reverse: false }), + pull.drain(msg => { const c = msg.value?.content; if (c && c.type === "ubiRefuse") refused.add(`${c.epochId}:${msg.value.author}`); }, + err => err ? reject(err) : resolve())); + }); const epochId = epochIdNow(); + const me = config.keys.id; + const defaultPubId = getDefaultPubId(); + const nowMs = Date.now(); + const attributed = new Set(results.map(r => r.txid).filter(Boolean)); + for (const e of Object.values(readUbiPaidLedger())) if (e && e.txid) attributed.add(e.txid); + let sends = null; + const walletSends = async () => { if (!sends) sends = await listWalletSends("pub"); return sends; }; + + const settlePaid = async ({ claim, claimantId, claimEpoch, paidKey, txid, amount, address, recovered }) => { + settleUbiPayment(paidKey, { txid, amount, address, paidAt: new Date().toISOString(), error: undefined }); + attributed.add(txid); + processedEpochUser.add(paidKey); + if (sends) sends.push({ category: "send", address, amount: -Number(amount), txid, comment: UBI_SEND_COMMENT, time: Math.floor(Date.now() / 1000) }); + try { + const open = (await transfersRepo.listByTag(`epoch:${claimEpoch}`)).find(a => a.to === claimantId && (a.status === "UNCLAIMED" || a.status === "UNCONFIRMED")); + if (open) await transfersRepo.markClosed(open.id, txid); + } catch (_) {} + console.log(`[UBI] ${recovered ? "found payment of" : "paid"} ${amount} ECO to ${claimantId.slice(0, 12)}… (${claimEpoch}) tx ${txid}`); + const allocationId = claim.allocationId || `claim:${claimEpoch}:${claimantId}`; + await publishUbiClaimResult(allocationId, claimEpoch, txid, claimantId, amount, address); + await publishBankClaim({ amount, epochId: claimEpoch, allocationId, txid }); + await publishUbiTransfer({ to: claimantId, amount, epochId: claimEpoch, txid }); + }; + for (const claim of claims) { const claimantId = claim._author; - if (!claimantId) continue; + if (!claimantId || claimantId === me || pubIds.has(claimantId)) continue; const claimEpoch = claim.epochId || epochId; - if (processedEpochUser.has(`${claimEpoch}:${claimantId}`)) continue; + if (!/^\d{4}-\d{2}$/.test(String(claimEpoch)) || (claimEpoch !== epochIdNow() && claimEpoch !== previousEpochId())) continue; + const paidKey = ubiPaidKey(claimEpoch, claimantId); + if (processedEpochUser.has(paidKey)) continue; + if (!claimPaidByMe(claim, me, defaultPubId, nowMs)) continue; + const ledgerEntry = readUbiPaidLedger()[paidKey]; + if (ledgerEntry && ledgerEntry.txid) continue; + if (refused.has(`${claimEpoch}:${claimantId}`)) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: refused by the inhabitant`); continue; } try { - const addr = await getUserAddress(claimantId); - if (!addr || !isValidEcoinAddress(addr)) continue; - const pubBal = await safeGetBalance("pub"); - if (pubBal <= 0) continue; + let attempts = 0; + if (ledgerEntry) { + attempts = Number(ledgerEntry.attempts) || 1; + const lastAt = Date.parse(ledgerEntry.lastAttemptAt || ledgerEntry.startedAt || "") || 0; + if (attempts >= UBI_MAX_ATTEMPTS) continue; + if (claimEpoch !== epochIdNow() && claimEpoch !== previousEpochId()) continue; + if (nowMs - lastAt < UBI_RETRY_AFTER_MS) continue; + const prevAddress = ledgerEntry.address || await getUserAddress(claimantId).catch(() => null); + if (prevAddress && ledgerEntry.amount) { + const list = await walletSends(); + if (!list) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… waiting: the wallet does not answer, the previous attempt cannot be checked`); continue; } + const found = await findWalletSend({ address: prevAddress, amount: ledgerEntry.amount, sinceMs: lastAt || epochStartMs(claimEpoch), exclude: attributed }, "pub", list); + if (found) { await settlePaid({ claim, claimantId, claimEpoch, paidKey, txid: found, amount: ledgerEntry.amount, address: prevAddress, recovered: true }); continue; } + } + } + const eligibility = await isEligibleClaimant(claimantId); + if (!eligibility.ok) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: ${eligibility.reason}`); continue; } + const addr = eligibility.address; + const pubBal = await safeGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); + if (pubBal <= 0) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: PUB wallet balance is 0`); continue; } const pv = computePoolVars(pubBal, DEFAULT_RULES); const addresses = await listAddressesMerged(); - const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address)); - const karmaScore = await getUserEngagementScore(claimantId); + const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address) && a.id !== me && !pubIds.has(a.id)); + const karmaScore = eligibility.score; const wMin = DEFAULT_RULES.caps.w_min; const wMax = DEFAULT_RULES.caps.w_max; - const capUser = DEFAULT_RULES.caps.cap_user_epoch; const userW = clamp(1 + karmaScore / 100, wMin, wMax); const totalW = eligible.reduce((acc) => acc + clamp(1, wMin, wMax), 0) || 1; - const amount = Number(Math.max(1, Math.min(pv.pool * userW / totalW, capUser)).toFixed(6)); - const txid = await rpcCall("sendtoaddress", [addr, amount, `UBI ${claimEpoch}`], "pub"); - if (!txid) continue; - await publishUbiClaimResult(claim.allocationId || `claim:${claimEpoch}:${claimantId}`, claimEpoch, txid, claimantId, amount); - await publishBankClaim({ amount, epochId: claimEpoch, allocationId: claim.allocationId || `claim:${claimEpoch}:${claimantId}`, txid }); - const now = new Date().toISOString(); - await new Promise((resolve, reject) => ssb.publish({ - type: "transfer", - from: config.keys.id, - to: claimantId, - concept: `UBI ${claimEpoch} ${claimantId}`, - amount: String(amount), - createdAt: now, - updatedAt: now, - deadline: null, - confirmedBy: [config.keys.id], - status: "UNCONFIRMED", - tags: ["UBI"], - opinions: {}, - opinions_inhabitants: [], - txid - }, (err, msg) => err ? reject(err) : resolve(msg))); + const ecoTax = await getUserEcoinTax(claimantId).catch(() => 0); + const archTax = await getUserArchTax(claimantId).catch(() => 0); + const amount = ubiAmountFor({ pool: pv.pool, userW, totalW, score: karmaScore, ecoTax, archTax }); + const list = await walletSends(); + if (!list) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… waiting: the wallet does not answer`); continue; } + const earlier = await findWalletSend({ address: addr, comment: UBI_SEND_COMMENT, amountIfNoComment: amount, sinceMs: epochStartMs(claimEpoch), exclude: attributed }, "pub", list); + if (earlier) { await settlePaid({ claim, claimantId, claimEpoch, paidKey, txid: earlier, amount, address: addr, recovered: true }); continue; } + if (pv.available < amount) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: the PUB wallet is down to its reserve`); continue; } + if (ubiAddressTaken(claimEpoch, addr, claimantId) || (paidAddressBy.has(`${claimEpoch}:${addr}`) && paidAddressBy.get(`${claimEpoch}:${addr}`) !== claimantId)) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: this ECOin address was already paid this epoch`); continue; } + if (ledgerEntry) settleUbiPayment(paidKey, { attempts: attempts + 1, lastAttemptAt: new Date().toISOString(), amount, address: addr, error: undefined }); + else { + if (!reserveUbiPayment(paidKey)) continue; + settleUbiPayment(paidKey, { attempts: 1, lastAttemptAt: new Date().toISOString(), amount, address: addr }); + } + processedEpochUser.add(paidKey); + const sent = await sendVerified(addr, amount, UBI_SEND_COMMENT, "pub"); + if (!sent.txid) { + settleUbiPayment(paidKey, { error: sent.unknown ? "the wallet did not answer" : "sendtoaddress failed" }); + console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… not paid yet: ${sent.unknown ? "the wallet did not answer, it will be checked again" : "sendtoaddress failed, it will be retried"}`); + continue; + } + await settlePaid({ claim, claimantId, claimEpoch, paidKey, txid: sent.txid, amount, address: addr, recovered: sent.recovered }); + } catch (err) { console.warn(`[UBI] claim by ${String(claimantId).slice(0, 12)}… failed after payment: ${(err && err.message) || err}`); } + } + } + + async function readUbiLedger(ssb) { + const msgs = await readTyped(ssb, ["transfer", "transferConfirm", "ubiClaim"], { limit: Math.max(getLogLimit(), 20000) }); + const transfers = []; + const confirms = new Map(); + const claims = []; + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (!c || typeof c !== "object") continue; + if (c.type === "transfer" && Array.isArray(c.tags) && c.tags.some(t => String(t).toUpperCase() === "UBI")) transfers.push({ key: m.key, author: v.author, ...c }); + else if (c.type === "transferConfirm" && c.target) { if (!confirms.has(c.target)) confirms.set(c.target, new Set()); confirms.get(c.target).add(v.author); } + else if (c.type === "ubiClaim") claims.push({ key: m.key, author: v.author, pubId: c.pubId, epochId: c.epochId }); + } + return { transfers, confirms, claims }; + } + + function isFollowing(ssb, source, dest) { + return new Promise((resolve) => { + if (!ssb || !ssb.friends || typeof ssb.friends.isFollowing !== "function") return resolve(false); + ssb.friends.isFollowing({ source, dest }, (err, val) => resolve(!err && !!val)); + }); + } + + async function rebalanceUbiPools() { + if (!isPubNode()) return []; + const ssb = await openSsb(); + if (!ssb || !ssb.publish) return []; + const me = config.keys.id; + const epochId = epochIdNow(); + const pubs = (await listUbiPubs()).filter(p => p.pubId !== me && p.balanceKnown); + const { transfers, confirms, claims } = await readUbiLedger(ssb); + const confirmedBy = (t) => confirms.get(t.key) || new Set(); + const sum = (list) => list.reduce((acc, t) => acc + (Number(t.amount) || 0), 0); + const createdOf = (t) => Date.parse(t.createdAt || "") || 0; + const isRebalance = (t) => String(t.concept || "").startsWith(REBALANCE_CONCEPT); + const epochOf = (t) => { + const tag = (Array.isArray(t.tags) ? t.tags : []).find(x => String(x).startsWith("epoch:")); + return tag ? String(tag).slice(6) : ((String(t.concept || "").match(/(\d{4}-\d{2})\s*$/) || [])[1] || ""); + }; + const eligibleCache = new Map(); + const eligible = async (id) => { + if (!eligibleCache.has(id)) eligibleCache.set(id, !!(await isEligibleClaimant(id)).ok); + return eligibleCache.get(id); + }; + const servedBy = async (pubId) => { + const epochs = new Set([previousEpochId(), epochId]); + const people = new Set(); + for (const c of claims) { + if (!epochs.has(c.epochId) || people.has(c.author) || !(c.pubId === pubId || transfers.some(t => t.author === pubId && t.to === c.author && isUbiPayoutConcept(t.concept) && epochOf(t) === c.epochId))) continue; + if (await eligible(c.author)) people.add(c.author); + } + return people; + }; + const now = Date.now(); + const balanceOf = (p) => { + const announcedAt = Number(p.timestamp) || 0; + const inflows = transfers.filter(t => isRebalance(t) && t.to === p.pubId && createdOf(t) > announcedAt - REBALANCE_SETTLE_MS); + const outflows = transfers.filter(t => isRebalance(t) && t.author === p.pubId && createdOf(t) > announcedAt); + const payouts = transfers.filter(t => t.author === p.pubId && isUbiPayoutConcept(t.concept) && createdOf(t) > announcedAt); + return Math.max(0, (Number(p.balance) || 0) + sum(inflows) - sum(outflows) - sum(payouts)); + }; + const sent = []; + const recorded = new Set(transfers.map(t => t.txid).filter(Boolean)); + const pendingAll = () => { const raw = readJson(REBALANCE_PENDING_PATH, {}); return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}; }; + const setPending = (key, value) => { const all = pendingAll(); if (value) all[key] = value; else delete all[key]; writeJson(REBALANCE_PENDING_PATH, all); }; + let left = 0; + const record = async (pub, amount, txid, served, ep = epochId, at = null) => { + const when = at || new Date().toISOString(); + const content = { type: "transfer", from: me, to: pub.pubId, concept: `${REBALANCE_CONCEPT} · ${ep}`, amount: Number(amount).toFixed(6), category: "ECONOMIC", createdAt: when, updatedAt: when, deadline: null, confirmedBy: [me], status: "UNCONFIRMED", tags: ["UBI", "REBALANCE", `epoch:${ep}`], opinions: {}, opinions_inhabitants: [], txid }; + await new Promise((resolve, reject) => ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg))); + recorded.add(txid); + transfers.push({ ...content, author: me }); + left -= Number(amount); + console.log(`[UBI] rebalanced ${amount} ECO to PUB ${pub.pubId.slice(0, 12)}…${served ? ` (${served} claimants)` : ""} tx ${txid}`); + sent.push({ pubId: pub.pubId, amount: Number(amount), txid }); + }; + for (const [key, pending] of Object.entries(pendingAll())) { + const cut = key.indexOf(":"); + const ep = key.slice(0, cut); + const pubId = key.slice(cut + 1); + const list = await listWalletSends("pub"); + if (!list) { console.warn(`[UBI] rebalance to ${pubId.slice(0, 12)}… waiting: the wallet does not answer, the previous attempt cannot be checked`); continue; } + const found = await findWalletSend({ address: pending.address, amount: pending.amount, comment: REBALANCE_CONCEPT, sinceMs: Date.parse(pending.startedAt || "") || 0, exclude: recorded }, "pub", list); + if (found) await record(pubs.find(p => p.pubId === pubId) || { pubId }, pending.amount, found, 0, ep, pending.startedAt || null); + setPending(key, null); + } + if (!pubs.length) return sent; + const myBalance = await tryGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); + if (myBalance === null) return sent; + const mine = computePoolVars(myBalance, DEFAULT_RULES).available; + const balances = new Map([[me, mine], ...pubs.map(p => [p.pubId, balanceOf(p)])]); + const average = Array.from(balances.values()).reduce((a, b) => a + b, 0) / balances.size; + const tolerance = Math.max(1, average * REBALANCE_TOLERANCE); + const myExcess = mine - average; + if (myExcess < tolerance) return sent; + const totalExcess = Array.from(balances.values()).reduce((acc, b) => acc + Math.max(0, b - average), 0); + left = myExcess; + const poorer = pubs.map(p => ({ pub: p, gap: average - balances.get(p.pubId) })).filter(x => x.gap >= tolerance).sort((a, b) => b.gap - a.gap); + for (const { pub, gap } of poorer) { + if (left < 1) break; + const tag = pub.pubId.slice(0, 12); + try { + const pendingKey = `${epochId}:${pub.pubId}`; + if (Object.keys(pendingAll()).some(k => k.endsWith(`:${pub.pubId}`))) { console.warn(`[UBI] rebalance to ${tag}… waiting: an earlier transfer is still being checked`); continue; } + if (!(await isFollowing(ssb, me, pub.pubId))) { console.warn(`[UBI] rebalance to ${tag}… skipped: this PUB does not follow it`); continue; } + const address = pub.address || await getUserAddress(pub.pubId).catch(() => null); + if (!address || !isValidEcoinAddress(address)) { console.warn(`[UBI] rebalance to ${tag}… skipped: no ECOin address`); continue; } + const served = await servedBy(pub.pubId); + if (served.size < REBALANCE_MIN_CLAIMANTS) { console.warn(`[UBI] rebalance to ${tag}… skipped: ${served.size} eligible claimant(s), ${REBALANCE_MIN_CLAIMANTS} required`); continue; } + const receivedBefore = sum(transfers.filter(t => t.author === me && t.to === pub.pubId && isRebalance(t))); + const paidOut = sum(transfers.filter(t => t.author === pub.pubId && isUbiPayoutConcept(t.concept) && confirmedBy(t).has(t.to))); + if (receivedBefore > 0 && paidOut < receivedBefore * REBALANCE_PAYOUT_RATIO) { console.warn(`[UBI] rebalance to ${tag}… skipped: paid out ${paidOut.toFixed(6)} of ${receivedBefore.toFixed(6)} received`); continue; } + const amount = Number(Math.min(left, gap * myExcess / totalExcess).toFixed(6)); + if (!(amount >= 1)) { console.warn(`[UBI] rebalance to ${tag}… skipped: nothing needed`); continue; } + setPending(pendingKey, { address, amount, startedAt: new Date().toISOString() }); + const result = await sendVerified(address, amount, REBALANCE_CONCEPT, "pub"); + if (!result.txid) { + if (!result.unknown) setPending(pendingKey, null); + console.warn(`[UBI] rebalance to ${tag}… not sent: ${result.unknown ? "the wallet did not answer, it will be checked next time" : "sendtoaddress failed"}`); + continue; + } + await record(pub, amount, result.txid, served.size); + setPending(pendingKey, null); + } catch (err) { console.warn(`[UBI] rebalance to ${tag}… failed: ${(err && err.message) || err}`); } + } + return sent; + } + + async function confirmIncomingTransfers() { + if (!services || !services.transfers || typeof services.transfers.confirmTransferById !== "function" || typeof services.transfers.listAll !== "function") return []; + const kind = isPubNode() ? "pub" : "user"; + const cfg = getWalletCfg(kind) || {}; + if (!cfg.url) return []; + const ssb = await openSsb(); + if (!ssb) return []; + const me = config.keys.id; + const msgs = await readTyped(ssb, ["transfer", "transferConfirm"], { limit: Math.max(getLogLimit(), 20000) }); + const txidOf = (t) => (String(t.txid || "").match(TXID_RE) ? String(t.txid) : (String(t.concept || "").match(TXID_RE) || [])[1] || "").toLowerCase(); + const signed = new Map(); + const confirmedByMe = new Set(); + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "transferConfirm" && c.target && v.author === me) confirmedByMe.add(c.target); + else if (c.type === "transfer" && c.to === me && c.from === v.author) signed.set(m.key, c); + } + const used = new Set(); + for (const k of confirmedByMe) { const txid = signed.has(k) ? txidOf(signed.get(k)) : ""; if (txid) used.add(txid); } + const claimed = new Map(); + const incoming = []; + for (const t of (await services.transfers.listAll("all")) || []) { + if (t.to !== me || t.from === me || t.from !== t.author) continue; + const txid = txidOf(t); + if (!txid) continue; + if (t.status === "CLOSED" || (Array.isArray(t.confirmedBy) && t.confirmedBy.includes(me))) { used.add(txid); continue; } + if (t.status !== "UNCONFIRMED") continue; + claimed.set(txid, (claimed.get(txid) || 0) + 1); + incoming.push({ ...t, txid }); + } + const done = []; + for (const t of incoming) { + if (claimed.get(t.txid) !== 1 || used.has(t.txid)) continue; + const amount = Number(t.amount); + if (!Number.isFinite(amount) || amount <= 0) continue; + const tx = await rpcCall("gettransaction", [t.txid], kind); + if (!tx || !(Number(tx.confirmations) >= 1) || !Array.isArray(tx.details)) continue; + const address = signed.has(t.id) && typeof signed.get(t.id).address === "string" ? signed.get(t.id).address : ""; + if (!tx.details.some(d => d && d.category === "receive" && Number(d.amount) >= amount - 0.000001 && (!address || d.address === address))) continue; + try { + await services.transfers.confirmTransferById(t.id); + used.add(t.txid); + console.log(`[WALLET] confirmed incoming ${amount.toFixed(6)} ECO from ${String(t.from).slice(0, 12)}… (${t.concept}) tx ${t.txid}`); + done.push(t.id); } catch (_) {} } + return done; } return { DEFAULT_RULES, isPubNode, - getConfiguredPubId, computeEpoch, executeEpoch, getUserEngagementScore, + getKarmaReceived: async (userId) => { const r = karma.receivedBreakdownFor(String(userId || config.keys.id), await listAllActions()); return { positive: r.positive, negative: r.negative }; }, publishBankClaim, publishUbiAllocation, publishUbiClaim, @@ -1367,6 +2243,8 @@ async function getLastPublishedTimestamp(userId) { publishPubAvailability, getPubAvailabilityFromSSB, hasClaimedThisMonth, + hasWalletCredentials, + sampleUserFunds, getUbiClaimHistory, claimUBI, processPendingClaims, @@ -1378,10 +2256,34 @@ async function getLastPublishedTimestamp(userId) { listEpochAllocations, addAddress, removeAddress, + listAddressBook, + addAddressBookEntry, + removeAddressBookEntry, ensureSelfAddressPublished, getUserAddress, setUserAddress, listAddressesMerged, + scanAllWalletsSSB, + hasPublishedAddress, + discoverUbiPub, + resolvePubId, + listUbiPubs, + listUbiPubsDetailed, + listUbiCharts, + listKarmaHistory, + listWealthSeries, + listUbiPaymentsSeries, + listNetworkEpochs, + normalizeRange, + filterByRange, + compactEcoHistory, + isEligibleClaimant, + ubiAmountFor, + rebalanceUbiPools, + confirmIncomingTransfers, + refuseUBI, + hasRefusedThisMonth, + claimAvailability, calculateEcoinValue, calculateEcoTaxStats, getUserEcoinTax, diff --git a/src/models/blobcache_model.js b/src/models/blobcache_model.js new file mode 100644 index 0000000..0bfb001 --- /dev/null +++ b/src/models/blobcache_model.js @@ -0,0 +1,161 @@ +const fs = require('fs'); +const pull = require('../server/node_modules/pull-stream'); + +const BLOB_RE = /&[A-Za-z0-9+/=]{44}\.sha256/g; +const DEFAULT_MAX_MB = 0; +const PROTECT_MS = 24 * 60 * 60 * 1000; +const FLUSH_MS = 60 * 1000; + +const pinsPath = () => { + try { return require('../configs/state-manager').statePath('blob-pins.json'); } catch (_) { return null; } +}; + +const accessPath = () => { + try { return require('../configs/state-manager').statePath('blob-access.json'); } catch (_) { return null; } +}; + +module.exports = ({ cooler } = {}) => { + const openSsb = async () => cooler.open(); + let access = null; + let dirty = false; + let flushTimer = null; + + const loadAccess = () => { + if (access) return access; + access = {}; + const p = accessPath(); + if (p) { try { access = JSON.parse(fs.readFileSync(p, 'utf8')) || {}; } catch (_) { access = {}; } } + return access; + }; + + const flush = () => { + if (!dirty) return; + const p = accessPath(); + if (!p) return; + try { fs.writeFileSync(p, JSON.stringify(loadAccess())); dirty = false; } catch (_) {} + }; + + const normalizeId = (raw) => { + let id = String(raw || '').trim(); + try { id = decodeURIComponent(id); } catch (_) {} + if (!id) return null; + if (!id.startsWith('&')) id = `&${id}`; + return /^&[A-Za-z0-9+/=]{44}\.sha256$/.test(id) ? id : null; + }; + + const touch = (raw) => { + const id = normalizeId(raw); + if (!id) return; + loadAccess()[id] = Date.now(); + dirty = true; + if (!flushTimer) { + flushTimer = setTimeout(() => { flushTimer = null; flush(); }, FLUSH_MS); + if (typeof flushTimer.unref === 'function') flushTimer.unref(); + } + }; + + const listBlobs = async (ssb) => new Promise((resolve) => { + if (!ssb.blobs || typeof ssb.blobs.ls !== 'function') return resolve([]); + pull(ssb.blobs.ls({ long: true }), pull.collect((err, arr) => { + if (err) return resolve([]); + resolve((arr || []).filter(b => b && b.id).map(b => ({ id: b.id, size: Number(b.size) || 0, ts: Number(b.ts) || 0 }))); + })); + }); + + const loadPins = () => { + const p = pinsPath(); + if (!p) return []; + try { const arr = JSON.parse(fs.readFileSync(p, 'utf8')); return Array.isArray(arr) ? arr : []; } catch (_) { return []; } + }; + + const pin = (ids) => { + const p = pinsPath(); + if (!p) return; + const set = new Set(loadPins()); + for (const raw of (Array.isArray(ids) ? ids : [ids])) { const id = normalizeId(raw); if (id) set.add(id); } + try { fs.writeFileSync(p, JSON.stringify(Array.from(set))); } catch (_) {} + }; + + const getContent = (ssb, key) => new Promise((resolve) => { + try { ssb.get(key, (err, v) => resolve(!err && v && v.content && typeof v.content === 'object' ? v.content : null)); } catch (_) { resolve(null); } + }); + + const ownBlobIds = async (ssb) => { + const own = new Set(); + const msgs = await new Promise((resolve) => { + pull(ssb.createUserStream({ id: ssb.id, reverse: false }), pull.collect((err, arr) => resolve(err ? [] : (arr || [])))); + }); + const spreads = new Map(); + const tombstoned = new Set(); + for (const m of msgs) { + const content = m && m.value && m.value.content; + if (!content || typeof content !== 'object') continue; + if (content.type === 'spread' && typeof content.link === 'string') spreads.set(m.key, content.link); + else if (content.type === 'tombstone' && typeof content.target === 'string') tombstoned.add(content.target); + for (const id of (JSON.stringify(content).match(BLOB_RE) || [])) own.add(id); + } + if (ssb.private && typeof ssb.private.read === 'function') { + const boxed = await new Promise((resolve) => { + pull(ssb.private.read({ reverse: false }), pull.collect((err, arr) => resolve(err ? [] : (arr || [])))); + }); + for (const m of boxed) { + const content = m && m.value && m.value.content; + if (content && typeof content === 'object') for (const id of (JSON.stringify(content).match(BLOB_RE) || [])) own.add(id); + } + } + for (const id of loadPins()) own.add(id); + for (const [key, link] of spreads) { + if (tombstoned.has(key)) continue; + const seeded = await getContent(ssb, link); + if (!seeded || (seeded.type !== 'torrent' && seeded.type !== 'file')) continue; + for (const id of (JSON.stringify(seeded).match(BLOB_RE) || [])) own.add(id); + } + return own; + }; + + const usage = async () => { + const ssb = await openSsb(); + const blobs = await listBlobs(ssb); + return { count: blobs.length, bytes: blobs.reduce((n, b) => n + b.size, 0) }; + }; + + const collect = async ({ maxBytes, now = Date.now(), protectMs = PROTECT_MS } = {}) => { + const limit = Number(maxBytes); + const ssb = await openSsb(); + const blobs = await listBlobs(ssb); + const total = blobs.reduce((n, b) => n + b.size, 0); + const result = { before: total, after: total, deleted: 0, freed: 0, kept: blobs.length }; + if (!(limit > 0) || total <= limit) return result; + const own = await ownBlobIds(ssb); + const seen = loadAccess(); + const candidates = blobs + .filter(b => !own.has(b.id)) + .map(b => ({ ...b, last: Math.max(Number(seen[b.id]) || 0, b.ts) })) + .filter(b => now - b.last > protectMs) + .sort((a, b) => a.last - b.last); + let current = total; + for (const b of candidates) { + if (current <= limit) break; + const removed = await new Promise((resolve) => ssb.blobs.rm(b.id, (err) => resolve(!err))); + if (!removed) continue; + current -= b.size; + result.deleted += 1; + result.freed += b.size; + delete seen[b.id]; + dirty = true; + } + result.after = current; + result.kept = blobs.length - result.deleted; + flush(); + return result; + }; + + const maxBytesFor = (cfg, { isPublic = false } = {}) => { + const mb = Number(cfg && cfg.blobCache && (isPublic ? cfg.blobCache.pubMaxMB : cfg.blobCache.maxMB)); + if (!Number.isFinite(mb) || mb <= 0) return 0; + return Math.round(mb * 1024 * 1024); + }; + + return { touch, usage, collect, ownBlobIds, maxBytesFor, flush, pin, DEFAULT_MAX_MB }; +}; +module.exports.DEFAULT_MAX_MB = DEFAULT_MAX_MB; diff --git a/src/models/blockchain_model.js b/src/models/blockchain_model.js index 39fe868..b305269 100644 --- a/src/models/blockchain_model.js +++ b/src/models/blockchain_model.js @@ -2,6 +2,8 @@ const pull = require('../server/node_modules/pull-stream'); const config = require('../server/ssb_config'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readContentWindow, requestScope, logTip } = require('./typed_log'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, tribeCrypto, tribesModel }) => { @@ -11,6 +13,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const nameByFeedId = new Map(); const HIDDEN_ENVELOPE_TYPES = new Set([ 'tribe-keys-distrib', @@ -19,7 +22,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { ]); const isHiddenBoxedContent = (rawContent) => - typeof rawContent === 'string' && rawContent.endsWith('.box'); + typeof rawContent === 'string' && /\.box\d*$/.test(rawContent); const buildAccessibleTribeIds = async () => { const set = new Set(); @@ -41,16 +44,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const hasBlob = async (ssbClient, url) => new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has))); - const isClosedSold = s => - String(s || '').toUpperCase() === 'SOLD' || String(s || '').toUpperCase() === 'CLOSED'; - - const projectRank = (status) => { - const S = String(status || '').toUpperCase(); - if (S === 'COMPLETED') return 3; - if (S === 'ACTIVE') return 2; - if (S === 'PAUSED') return 1; - if (S === 'CANCELLED') return 0; - return -1; + const isReplacedBlock = (vg, tombstoned, block) => { + const c = block.content; + const rootDeleted = !c.replaces && c?.type === 'forum' && c.root && tombstoned.has(c.root); + return vg.tipOf(block.id) !== block.id || vg.isForeignCopy(block.id) || tombstoned.has(block.id) || !!rootDeleted; }; const safeDecode = (s) => { @@ -127,115 +124,83 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return nameByFeedId; }; + let windowCache = null; + const readWindow = async (ssbClient) => { + const tip = await logTip(ssbClient).catch(() => null); + if (tip && windowCache && windowCache.tip === tip) return windowCache.msgs; + const msgs = await readContentWindow(ssbClient, logLimit); + if (tip) windowCache = { tip, msgs }; + return msgs; + }; + + async function buildBlocks(showLogs, me) { + const ssbClient = await openSsb(); + + const results = await readWindow(ssbClient); + + const tombstoned = buildValidatedTombstoneSet(results); + const idToBlock = new Map(); + const versions = []; + + const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; + const accessibleTribeIds = await buildAccessibleTribeIds(); + for (const msg of results) { + const k = msg.key; + let c = msg.value?.content; + const author = msg.value?.author; + if (isHiddenBoxedContent(c) || msg.value?.private === true) continue; + if (showLogs && typeof c === 'string' && author === me) { + try { + const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 }); + c = dec?.value?.content; + } catch { c = null; } + } + if (!c?.type) continue; + if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue; + if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { + const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null; + if (!r || !r.body) continue; + const inner = r.body; + const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg'); + c = { type: innerType, _decrypted: true, _rootId: r.rootId, ...inner }; + } else if (c.tribeId && !accessibleTribeIds.has(c.tribeId)) { + continue; + } + + if (c.type === 'about') { + const aboutId = String(c.about || author || '').trim(); + const nm = typeof c.name === 'string' ? c.name.trim() : ''; + if (aboutId && nm && !nameByFeedId.has(aboutId)) nameByFeedId.set(aboutId, nm); + } + + if (c.type === 'tombstone' && c.target) { + idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); + continue; + } + versions.push(versionEntry({ key: k, value: { ...msg.value, content: c } })); + idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); + } + + const vg = versionGroups(versions); + + const blockData = Array.from(idToBlock.values()).map(block => ({ + ...block, + isTombstoned: tombstoned.has(block.id), + isReplaced: isReplacedBlock(vg, tombstoned, block) + })); + + return blockData; + } + return { async listBlockchain(filter = 'all', userId, search = {}) { - const ssbClient = await openSsb(); - - const results = await new Promise((resolve, reject) => - pull( - ssbClient.createLogStream({ reverse: true, limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ); - - const tombstoned = buildValidatedTombstoneSet(results); - const idToBlock = new Map(); - const referencedAsReplaces = new Set(); - - const nameByFeedId = new Map(); - const showLogs = (filter === 'logs' || filter === 'LOGS'); const me = userId || config.keys.id; - const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; - const accessibleTribeIds = await buildAccessibleTribeIds(); - for (const msg of results) { - const k = msg.key; - let c = msg.value?.content; - const author = msg.value?.author; - if (isHiddenBoxedContent(c)) continue; - if (showLogs && typeof c === 'string' && author === me) { - try { - const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 }); - c = dec?.value?.content; - } catch { c = null; } - } - if (!c?.type) continue; - if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue; - if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { - const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null; - if (!r || !r.body) continue; - const inner = r.body; - const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg'); - c = { type: innerType, _decrypted: true, _rootId: r.rootId, ...inner }; - } else if (c.tribeId && !accessibleTribeIds.has(c.tribeId)) { - continue; - } - - if (c.type === 'about') { - const aboutId = String(c.about || author || '').trim(); - const nm = typeof c.name === 'string' ? c.name.trim() : ''; - if (aboutId && nm && !nameByFeedId.has(aboutId)) nameByFeedId.set(aboutId, nm); - } - - if (c.type === 'tombstone' && c.target) { - idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); - continue; - } - if (c.replaces) referencedAsReplaces.add(c.replaces); - idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); - } - - const tipBlocks = []; - for (const [id, block] of idToBlock.entries()) { - if (!referencedAsReplaces.has(id) && block.content.replaces) tipBlocks.push(block); - } - for (const [id, block] of idToBlock.entries()) { - if (!block.content.replaces && !referencedAsReplaces.has(id)) tipBlocks.push(block); - } - - const groups = {}; - for (const block of tipBlocks) { - const ancestor = block.content.replaces || block.id; - if (!groups[ancestor]) groups[ancestor] = []; - groups[ancestor].push(block); - } - - const liveTipIds = new Set(); - for (const groupBlocks of Object.values(groups)) { - let best = groupBlocks[0]; - for (const block of groupBlocks) { - if (block.type === 'market') { - if (isClosedSold(block.content.status) && !isClosedSold(best.content.status)) { - best = block; - } else if ((block.content.status === best.content.status) && block.ts > best.ts) { - best = block; - } - } else if (block.type === 'project') { - const br = projectRank(best.content.status); - const cr = projectRank(block.content.status); - if (cr > br || (cr === br && block.ts > best.ts)) best = block; - } else if (block.type === 'job' || block.type === 'forum') { - if (block.ts > best.ts) best = block; - } else { - if (block.ts > best.ts) best = block; - } - } - liveTipIds.add(best.id); - } - - const blockData = Array.from(idToBlock.values()).map(block => { - const c = block.content; - const rootDeleted = c?.type === 'forum' && c.root && tombstoned.has(c.root); - return { - ...block, - isTombstoned: tombstoned.has(block.id), - isReplaced: c.replaces - ? (!liveTipIds.has(block.id) || tombstoned.has(block.id)) - : referencedAsReplaces.has(block.id) || tombstoned.has(block.id) || rootDeleted - }; - }); - - let filtered = blockData; + const store = requestScope.getStore(); + const memoKey = `blockchainBase:${showLogs}:${me}`; + if (store && !store[memoKey]) store[memoKey] = buildBlocks(showLogs, me); + const blockData = await (store ? store[memoKey] : buildBlocks(showLogs, me)); + let filtered = blockData.slice(); if (filter === 'RECENT' || filter === 'recent') { const now = Date.now(); @@ -267,7 +232,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const needsNameSearch = !!authorQ && !authorQ.toLowerCase().includes('.ed25519'); if (needsNameSearch) { - aboutIndex = await buildNameIndexFromAbout(ssbClient, 10000); + aboutIndex = await buildNameIndexFromAbout(await openSsb(), 10000); for (const [fid, nm] of aboutIndex.entries()) { if (!nameByFeedId.has(fid)) nameByFeedId.set(fid, nm); } @@ -293,17 +258,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { async getBlockById(id, userId) { const ssbClient = await openSsb(); - const results = await new Promise((resolve, reject) => - pull( - ssbClient.createLogStream({ reverse: true, limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ); + const results = await readWindow(ssbClient); const me = userId || config.keys.id; const tombstoned = buildValidatedTombstoneSet(results); const idToBlock = new Map(); - const referencedAsReplaces = new Set(); + const versions = []; const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); @@ -311,7 +271,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const k = msg.key; let c = msg.value?.content; const author = msg.value?.author; - if (isHiddenBoxedContent(c)) continue; + if (isHiddenBoxedContent(c) || msg.value?.private === true) continue; if (typeof c === 'string' && author === me) { try { const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 }); @@ -333,48 +293,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); continue; } - if (c.replaces) referencedAsReplaces.add(c.replaces); + versions.push(versionEntry({ key: k, value: { ...msg.value, content: c } })); idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); } - const tipBlocks = []; - for (const [bid, block] of idToBlock.entries()) { - if (!referencedAsReplaces.has(bid) && block.content.replaces) tipBlocks.push(block); - } - for (const [bid, block] of idToBlock.entries()) { - if (!block.content.replaces && !referencedAsReplaces.has(bid)) tipBlocks.push(block); - } - - const groups = {}; - for (const block of tipBlocks) { - const ancestor = block.content.replaces || block.id; - if (!groups[ancestor]) groups[ancestor] = []; - groups[ancestor].push(block); - } - - const liveTipIds = new Set(); - for (const groupBlocks of Object.values(groups)) { - let best = groupBlocks[0]; - for (const block of groupBlocks) { - if (block.type === 'market') { - if (isClosedSold(block.content.status) && !isClosedSold(best.content.status)) { - best = block; - } else if ((block.content.status === best.content.status) && block.ts > best.ts) { - best = block; - } - } else if (block.type === 'project') { - const br = projectRank(best.content.status); - const cr = projectRank(block.content.status); - if (cr > br || (cr === br && block.ts > best.ts)) best = block; - } else if (block.type === 'job' || block.type === 'forum') { - if (block.ts > best.ts) best = block; - } else { - if (block.ts > best.ts) best = block; - } - } - liveTipIds.add(best.id); - } - const block = idToBlock.get(id); if (!block) return null; @@ -383,12 +305,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { if (!valid) return null; } - const c = block.content; - const rootDeleted = c?.type === 'forum' && c.root && tombstoned.has(c.root); const isTombstoned = tombstoned.has(block.id); - const isReplaced = c.replaces - ? (!liveTipIds.has(block.id) || tombstoned.has(block.id)) - : referencedAsReplaces.has(block.id) || tombstoned.has(block.id) || rootDeleted; + const isReplaced = isReplacedBlock(versionGroups(versions), tombstoned, block); return { ...block, isTombstoned, isReplaced }; } diff --git a/src/models/blog_model.js b/src/models/blog_model.js new file mode 100644 index 0000000..c0fa690 --- /dev/null +++ b/src/models/blog_model.js @@ -0,0 +1,183 @@ +const pull = require('../server/node_modules/pull-stream'); +const { getConfig } = require('../configs/config-manager.js'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, memoIndex } = require('./typed_log'); +const longText = require('../backend/long_text'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const OPINION_TYPE = 'blogOpinion'; + +module.exports = ({ cooler, isPublic = false }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const BLOG_TYPES = ['post', OPINION_TYPE, 'tombstone', longText.CHUNK_TYPE]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, BLOG_TYPES, { limit: logLimit }); + + const isRootPost = (c) => + !!c && c.type === 'post' && typeof c.text === 'string' && + !c.root && !c.fork && !c.branch && !c.about && !c.target; + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const chunks = longText.indexChunks(messages); + const posts = new Map(); + const commentsByRoot = new Map(); + const opinionsByRoot = new Map(); + + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object') continue; + if (tomb.has(m.key)) continue; + + if (c.type === 'post' && typeof c.text === 'string') { + if (isRootPost(c)) { + posts.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + } else if (typeof c.root === 'string') { + commentsByRoot.set(c.root, (commentsByRoot.get(c.root) || 0) + 1); + } + continue; + } + + if (c.type === OPINION_TYPE && typeof c.target === 'string') { + const entry = opinionsByRoot.get(c.target) || { counts: {}, voters: [] }; + if (!entry.voters.includes(v.author)) { + entry.voters.push(v.author); + entry.counts[c.category] = (entry.counts[c.category] || 0) + 1; + } + opinionsByRoot.set(c.target, entry); + } + } + + return { tomb, posts, commentsByRoot, opinionsByRoot, chunks }; + }; + + const buildBlog = (node, idx) => { + const c = node.c || {}; + const op = idx.opinionsByRoot.get(node.key) || { counts: {}, voters: [] }; + return { + id: node.key, + key: node.key, + author: node.author, + subject: typeof c.contentWarning === 'string' ? c.contentWarning : '', + text: longText.joinText(c.text || '', c.chunks, longText.lookupIn(idx.chunks), node.author), + mentions: Array.isArray(c.mentions) ? c.mentions : [], + allowComments: c.allowComments !== false, + tags: Array.isArray(c.tags) ? c.tags.map(t => String(t || '').trim().replace(/^#+/, '')).filter(Boolean) : [], + createdAt: new Date(node.ts).toISOString(), + commentCount: idx.commentsByRoot.get(node.key) || 0, + opinions: op.counts, + opinions_inhabitants: op.voters + }; + }; + + const collect = async () => { + const ssbClient = await openSsb(); + const messages = await getAllMessages(ssbClient); + const idx = memoIndex('blog', messages, buildIndex); + const list = []; + for (const node of idx.posts.values()) list.push(buildBlog(node, idx)); + return { list, idx, viewerId: ssbClient.id }; + }; + + const opinionScore = (blog) => + Object.values(blog.opinions || {}).reduce((n, v) => n + (Number(v) || 0), 0); + + return { + type: 'post', + + async listAll(filter = 'ALL', opts = {}) { + const { list, viewerId } = await collect(); + const f = String(filter || 'ALL').toUpperCase(); + const favorites = Array.isArray(opts.favorites) ? new Set(opts.favorites.map(String)) : null; + let out = list.slice(); + + if (f === 'MINE') out = out.filter(b => String(b.author) === String(viewerId)); + else if (f === 'FAVORITES') out = favorites ? out.filter(b => favorites.has(String(b.id))) : []; + else if (f === 'RECENT') out = out.filter(b => (Date.parse(b.createdAt || '') || 0) >= Date.now() - 86400000); + + const q = String(opts.q || '').trim().toLowerCase(); + if (q) out = out.filter(b => + b.text.toLowerCase().includes(q) || b.subject.toLowerCase().includes(q) || + b.tags.some(t => t.toLowerCase().includes(q.replace(/^#/, '')))); + + if (f === 'TOP') { + out.sort((a, b) => (opinionScore(b) + b.commentCount) - (opinionScore(a) + a.commentCount) || + new Date(b.createdAt) - new Date(a.createdAt)); + } else { + out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + } + + return out; + }, + + async getBlogById(id) { + const { list } = await collect(); + const blog = list.find(b => b.id === id); + if (!blog) throw new Error('Blog not found'); + return blog; + }, + + async createBlog({ text, subject = '', mentions = [], allowComments = true, tags = [] } = {}) { + if (isPublic) throw new Error('Not available in public mode'); + const body = String(text || '').trim(); + if (!body) throw new Error('Blog text is required'); + const ssbClient = await openSsb(); + const publish = (c) => new Promise((res, rej) => ssbClient.publish(c, (err, msg) => err ? rej(err) : res(msg))); + const content = await longText.chunkContent({ + type: 'post', + text: body, + allowComments: allowComments !== false, + ...(Array.isArray(tags) && tags.length ? { tags: Array.from(new Set(tags.map(t => String(t || '').trim().replace(/^#+/, '')).filter(Boolean))) } : {}), + ...(Array.isArray(mentions) && mentions.length ? { mentions } : {}), + ...(String(subject || '').trim() ? { contentWarning: String(subject).trim() } : {}) + }, 'text', { publish }); + return publish(content); + }, + + async deleteBlogById(id) { + if (isPublic) throw new Error('Not available in public mode'); + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const msg = await new Promise((res, rej) => ssbClient.get(id, (err, m) => (err || !m || !m.content) ? rej(new Error('Blog not found')) : res(m))); + if (!isRootPost(msg.content)) throw new Error('Blog not found'); + if (msg.author !== userId) throw new Error('Not the author'); + const content = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId }; + return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); + }, + + async createOpinion(id, category) { + const categories = require('../backend/opinion_categories'); + if (!categories.includes(category)) throw new Error('Invalid opinion category'); + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const blog = await this.getBlogById(id); + if (blog.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); + const content = { type: OPINION_TYPE, target: blog.id, category, createdAt: new Date().toISOString() }; + return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); + }, + + async resolveRootId(id) { + const ssbClient = await openSsb(); + const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m))); + const c = msg && (msg.content || (msg.value && msg.value.content)); + if (c && c.type === 'post' && typeof c.root === 'string') return c.root; + return id; + }, + + async blogHrefFor(id) { + const ssbClient = await openSsb(); + const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m))); + const c = msg && (msg.content || (msg.value && msg.value.content)); + if (!c || c.type !== 'post' || typeof c.text !== 'string') return null; + if (c.private === true || Array.isArray(c.recps)) return null; + if (typeof c.root === 'string') { + return `/blogs/${encodeURIComponent(c.root)}#${encodeURIComponent(id)}`; + } + if (c.fork || c.branch || c.about || c.target) return null; + return `/blogs/${encodeURIComponent(id)}`; + } + }; +}; diff --git a/src/models/bookmarking_model.js b/src/models/bookmarking_model.js index 144fb96..df6c3ac 100644 --- a/src/models/bookmarking_model.js +++ b/src/models/bookmarking_model.js @@ -3,7 +3,8 @@ const moment = require("../server/node_modules/moment"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -36,13 +37,9 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const BOOKMARK_TYPES = ["bookmark", "bookmarkOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, BOOKMARK_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -52,9 +49,6 @@ module.exports = ({ cooler }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -70,48 +64,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, t] of Array.from(parent.entries())) { - const cn = nodes.get(k); - const pn = nodes.get(t); - if (!pn) { parent.delete(k); if (child.get(t) === k) child.delete(t); continue; } - if (!cn || String(cn.author) !== String(pn.author)) { parent.delete(k); if (child.get(t) === k) child.delete(t); nodes.delete(k); } - } - - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -128,9 +90,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildBookmark = (node, rootId, viewerId, agg) => { @@ -145,7 +107,6 @@ module.exports = ({ cooler }) => { updatedAt: c.updatedAt || null, lastVisit: c.lastVisit || null, tags: safeArr(c.tags), - category: c.category || "", description: c.description || "", opinions, opinions_inhabitants: voters, @@ -173,16 +134,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Bookmark not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Bookmark not found"); return root; }, - async createBookmark(url, tagsRaw, description, category, lastVisit) { + async createBookmark(url, tagsRaw, description, lastVisit) { const ssbClient = await openSsb(); const now = new Date().toISOString(); @@ -195,7 +152,6 @@ module.exports = ({ cooler }) => { url: u, tags: normalizeTags(tagsRaw), description: description || "", - category: category || "", createdAt: now, updatedAt: now, lastVisit: coerceLastVisit(lastVisit), @@ -231,7 +187,6 @@ module.exports = ({ cooler }) => { url, tags: updatedData.tags !== undefined ? normalizeTags(updatedData.tags) : safeArr(oldMsg.content.tags), description: updatedData.description !== undefined ? updatedData.description || "" : oldMsg.content.description || "", - category: updatedData.category !== undefined ? updatedData.category || "" : oldMsg.content.category || "", lastVisit: coerceLastVisit(updatedData.lastVisit || oldMsg.content.lastVisit), createdAt: oldMsg.content.createdAt, updatedAt: now @@ -296,11 +251,10 @@ module.exports = ({ cooler }) => { if (q) { list = list.filter((b) => { const url = String(b.url || "").toLowerCase(); - const cat = String(b.category || "").toLowerCase(); const desc = String(b.description || "").toLowerCase(); const tags = safeArr(b.tags).join(" ").toLowerCase(); const author = String(b.author || "").toLowerCase(); - return url.includes(q) || cat.includes(q) || desc.includes(q) || tags.includes(q) || author.includes(q); + return url.includes(q) || desc.includes(q) || tags.includes(q) || author.includes(q); }); } @@ -322,10 +276,10 @@ module.exports = ({ cooler }) => { const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('bookmarking', messages, buildIndex); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Bookmark not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Bookmark not found"); return buildBookmark(agg.contentNode, rootId, viewer, agg); }, diff --git a/src/models/calendars_model.js b/src/models/calendars_model.js index 5eb49ce..e21f286 100644 --- a/src/models/calendars_model.js +++ b/src/models/calendars_model.js @@ -2,6 +2,7 @@ const pull = require("../server/node_modules/pull-stream") const crypto = require("crypto") const { getConfig } = require("../configs/config-manager.js") const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { readTyped, memoIndex } = require('./typed_log') const { collabContent, openInviteOf } = require('../backend/collab_content') const calCollab = collabContent({ membersField: 'participants', undecField: 'encrypted', contentFields: ['title', 'deadline', 'status'], listFields: ['tags', 'invites'] }) const logLimit = getConfig().ssbLogStream?.limit || 1000 @@ -14,25 +15,40 @@ const normalizeTags = (raw) => { return String(raw).split(",").map(t => t.trim()).filter(Boolean) } const hasAnyInterval = (w, m, y) => !!(w || m || y) -const expandRecurrence = (firstDate, deadline, weekly, monthly, yearly) => { - const start = new Date(firstDate) - const out = [start] - if (!deadline || !hasAnyInterval(weekly, monthly, yearly)) return out - const end = new Date(deadline).getTime() - const seen = new Set([start.getTime()]) - const walk = (mutate) => { - const n = new Date(start) - mutate(n) - while (n.getTime() <= end) { - const t = n.getTime() - if (!seen.has(t)) { seen.add(t); out.push(new Date(n)) } - mutate(n) - } - } - if (weekly) walk((d) => d.setDate(d.getDate() + 7)) - if (monthly) walk((d) => d.setMonth(d.getMonth() + 1)) - if (yearly) walk((d) => d.setFullYear(d.getFullYear() + 1)) - return out.sort((a, b) => a.getTime() - b.getTime()) +const { expandRecurrence, truthy } = require('./recurrence') +const MAX_OCCURRENCES_PER_DATE = 520 +const MAX_DATES_PER_VIEW = 5000 + +const ts = (v) => { + const t = new Date(v).getTime() + return Number.isFinite(t) ? t : null +} + +const boundedOccurrences = (date, until, weekly, monthly, yearly) => { + const start = ts(date) + const end = ts(until) + if (start === null || end === null) return expandRecurrence(date, until, weekly, monthly, yearly).slice(0, MAX_OCCURRENCES_PER_DATE) + const limit = new Date(start) + if (truthy(weekly)) limit.setUTCDate(limit.getUTCDate() + 7 * MAX_OCCURRENCES_PER_DATE) + else if (truthy(monthly)) limit.setUTCMonth(limit.getUTCMonth() + MAX_OCCURRENCES_PER_DATE) + else limit.setUTCFullYear(limit.getUTCFullYear() + MAX_OCCURRENCES_PER_DATE) + const stop = Number.isFinite(limit.getTime()) ? Math.min(end, limit.getTime()) : end + return expandRecurrence(date, new Date(stop).toISOString(), weekly, monthly, yearly).slice(0, MAX_OCCURRENCES_PER_DATE) +} + +const assertCalendarDates = ({ deadline, date, until }) => { + const now = Date.now() + const dl = deadline ? ts(deadline) : null + const dt = date ? ts(date) : null + const un = until ? ts(until) : null + if (deadline && dl === null) throw new Error("Deadline is not a valid date") + if (date && dt === null) throw new Error("Date is not a valid date") + if (until && un === null) throw new Error("The recurrence end is not a valid date") + if (dl !== null && dl <= now) throw new Error("Deadline must be in the future") + if (dt !== null && dt <= now) throw new Error("Date must be in the future") + if (dt !== null && dl !== null && dt > dl) throw new Error("Date cannot be later than the deadline") + if (un !== null && dt !== null && un < dt) throw new Error("The recurrence cannot end before it starts") + if (un !== null && dl !== null && un > dl) throw new Error("The recurrence cannot end after the deadline") } module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) => { @@ -47,6 +63,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) return (tribeCrypto && tribeCrypto.getKeys(rid)) || [] } const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0) + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } const rotateCalendarKey = async (rootId, remainingMembers) => { if (!ownCrypto || !tribeCrypto || !rootId) return @@ -54,11 +71,10 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (!existing) return const newKey = ownCrypto.generateTribeKey() const newGen = ownCrypto.addNewKey(rootId, newKey) - if (!Array.isArray(remainingMembers) || !remainingMembers.length) return const ssbClient = await openSsb() const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - for (const m of remainingMembers) { + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { @@ -75,9 +91,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const ssbKeys = require("../server/node_modules/ssb-keys") const config = require("../server/ssb_config") const msgs = await readAll(ssbClient) + const authorOf = new Map() + for (const m of msgs) if (m && m.key && m.value) authorOf.set(m.key, m.value.author) for (const m of msgs) { const c = m.value && m.value.content if (!c || c.type !== "tribe-keys") continue + if (!c.tribeId || !authorOf.has(c.tribeId) || (m.value.author !== authorOf.get(c.tribeId) && m.value.author !== ssbClient.id)) continue const memberKeys = c.memberKeys if (!memberKeys || typeof memberKeys !== "object") continue const boxed = memberKeys[ssbClient.id] @@ -91,12 +110,45 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } catch (_) {} } - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ) + const CALENDAR_TYPES = ["calendar", "calendarDate", "calendarNote", "calendarParticipant", "calendarReminderSent", "tribe-keys", "tombstone"] + + const readAll = async (ssbClient) => unwrapForIndex(await readTyped(ssbClient, CALENDAR_TYPES, { limit: logLimit, withWindow: true })) + + const ensureMemberKeys = async (ssbClient, messages, cals) => { + if (!tribeCrypto || !ownCrypto) return + const leavesByRoot = new Map() + for (const m of messages) { + const c = m.value && m.value.content + if (!c || c.type !== "calendarParticipant" || c.on !== false || !c.target) continue + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []) + leavesByRoot.get(c.target).push({ member: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }) + } + const ssbKeys = require("../server/node_modules/ssb-keys") + for (const cal of (Array.isArray(cals) ? cals : [])) { + if (!cal || cal.tribeId || cal.encrypted || cal.author !== ssbClient.id) continue + const rootId = cal.rootId + if (!rootId || !lookupKey(rootId)) continue + const members = Array.isArray(cal.participants) ? cal.participants : [] + const plan = ownCrypto.keyPlan({ rootId, ownerId: cal.author, gen: lookupGen(rootId), members, messages, leaves: leavesByRoot.get(rootId) || [] }) + if (plan.rotate) { + await rotateCalendarKey(rootId, members) + continue + } + if (!plan.missing.length) continue + const key = lookupKey(rootId) + const memberKeys = {} + for (const m of plan.missing) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + } + if (!Object.keys(memberKeys).length) continue + await new Promise((resolve) => { + ssbClient.publish({ type: "tribe-keys", tribeId: rootId, generation: lookupGen(rootId) || 1, memberKeys }, () => resolve()) + }) + } + } const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null + const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, CALENDAR_TYPES) : msgs const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {} @@ -108,6 +160,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) return tribeCrypto.encryptContent(content, [key], true) } + const decryptScoped = async (content, rootId) => { + if (!content) return content + if (content.tribeId || (tribeCrypto && tribeCrypto.isTribeMsg(content))) return await decryptIfTribe(content) + return decryptCalendarRoot(content, rootId) + } + const decryptCalendarRoot = (content, rootId) => { if (!content || !content.encryptedPayload) return content if (!tribeCrypto) return content @@ -178,9 +236,9 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const c = v.content if (!c) continue if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue } - if (c.type === "calendarParticipant" && c.target) { participantMsgs.push({ target: c.target, author: v.author, on: c.on !== false, ts: v.timestamp || m.timestamp || 0 }); continue } + if (c.type === "calendarParticipant" && c.target) { participantMsgs.push({ target: c.target, author: v.author, on: c.on !== false, keyProof: typeof c.keyProof === "string" ? c.keyProof : "", code: typeof c.code === "string" ? c.code : "", ts: v.timestamp || m.timestamp || 0 }); continue } if (c.type === "calendar") { - nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) authorByKey.set(k, v.author) if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) } } @@ -200,30 +258,31 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const rootOf = (id) => { let cur = id; while (parent.has(cur)) cur = parent.get(cur); return cur } const tipOf = (id) => { let cur = id; while (child.has(cur)) cur = child.get(cur); return cur } - const contentTipOf = (root) => { - const rn = nodes.get(root) - if (!rn) return root - let cur = root - let best = root - const seen = new Set() - while (child.has(cur) && !seen.has(cur)) { - seen.add(cur) - const next = child.get(cur) - const n = nodes.get(next) - if (!n) break - if (n.author === rn.author && !tomb.has(next)) best = next - cur = next - } - return best + const ownerTip = new Map() + for (const [k, n] of nodes.entries()) { + const r = rootOf(k) + const rn = nodes.get(r) + if (!rn || n.author !== rn.author || tomb.has(k)) continue + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k) } + const contentTipOf = (root) => ownerTip.get(root) || root const roots = new Set() for (const id of nodes.keys()) roots.add(rootOf(id)) + const joinAllowed = (root, pm) => { + const rn = nodes.get(root) + if (!rn) return false + if (pm.author === rn.author || rn.c.tribeId) return true + if (!rn.c.encryptedPayload && String(rn.c.status || "OPEN").toUpperCase() === "OPEN") return true + return !!pm.keyProof && lookupKeys(root).some(k => keyProofFor(k, pm.author) === pm.keyProof) + } const participantsByRoot = new Map() for (const pm of participantMsgs) { if (!nodes.has(pm.target)) continue const r = rootOf(pm.target) + if (pm.on && !joinAllowed(r, pm)) continue if (!participantsByRoot.has(r)) participantsByRoot.set(r, new Map()) const m2 = participantsByRoot.get(r) const prev = m2.get(pm.author) @@ -259,17 +318,30 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: c.status || "OPEN", deadline: undec ? "" : (c.deadline || ""), tags: Array.isArray(c.tags) ? c.tags : [], - author: c.author || node.author, + mapUrl: typeof c.mapUrl === "string" ? c.mapUrl : "", + author: node.author || c.author, participants: Array.isArray(participants) ? participants : (Array.isArray(c.participants) ? c.participants : []), invites: Array.isArray(c.invites) ? c.invites : [], createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, tribeId: c.tribeId || null, - encrypted: !!undec + encrypted: !!undec, + contentEncrypted: !!(c.encryptedPayload || c._decrypted === true) } } + const calendarMembers = async (cal) => { + const set = new Set([cal.author, ...(Array.isArray(cal.participants) ? cal.participants : [])].filter(Boolean)) + if (cal.tribeId && tribesModel) { + try { + const t = await tribesModel.getTribeById(cal.tribeId) + for (const m of (t && Array.isArray(t.members) ? t.members : [])) set.add(m) + } catch (_) {} + } + return set + } + const isClosed = (calendar) => { if (calendar.status === "CLOSED") return true if (!calendar.deadline) return false @@ -305,17 +377,11 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const live = new Set() const tomb = buildValidatedTombstoneSet(messages) - for (const m of messages) { - const c = m.value && m.value.content - if (!c) continue - if (c.type === "calendar") live.add(m.key) - } const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (!live.has(rid) || tomb.has(rid)) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } @@ -327,11 +393,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) - if (idx.tomb.has(tip)) throw new Error("Not found") - let root = tip - while (idx.parent.has(root)) root = idx.parent.get(root) + const root = idx.rootOf(id) + if (idx.tomb.has(idx.contentTipOf(root))) throw new Error("Not found") return root }, @@ -339,20 +402,23 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) + const tip = idx.contentTipOf(idx.rootOf(id)) if (idx.tomb.has(tip)) throw new Error("Not found") return tip }, - async createCalendar({ title, status, deadline, tags, firstDate, firstDateLabel, firstNote, intervalWeekly, intervalMonthly, intervalYearly, tribeId }) { + async createCalendar({ title, status, deadline, tags, firstDate, firstDateLabel, firstNote, intervalWeekly, intervalMonthly, intervalYearly, intervalDeadline, mapUrl, tribeId }) { const ssbClient = await openSsb() const userId = ssbClient.id const now = new Date().toISOString() const validStatus = ["OPEN", "CLOSED"].includes(String(status).toUpperCase()) ? String(status).toUpperCase() : "OPEN" - if (deadline && new Date(deadline).getTime() <= Date.now()) throw new Error("Deadline must be in the future") - if (!firstDate || new Date(firstDate).getTime() <= Date.now()) throw new Error("First date must be in the future") + if (!firstDate) throw new Error("First date must be in the future") + assertCalendarDates({ + deadline, + date: firstDate, + until: hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) ? intervalDeadline : "" + }) let plainContent = { type: "calendar", @@ -360,6 +426,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: validStatus, deadline: deadline || "", tags: normalizeTags(tags), + mapUrl: safeText(mapUrl), author: userId, participants: [userId], invites: [], @@ -406,6 +473,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: validStatus, deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], + mapUrl: dec.mapUrl || "", author: userId, participants: [userId], invites: [{ code: pubCode, ek, salt: inviteSalt, gen: 1, public: true }], @@ -430,7 +498,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) ...(intervalWeekly ? { intervalWeekly: true } : {}), ...(intervalMonthly ? { intervalMonthly: true } : {}), ...(intervalYearly ? { intervalYearly: true } : {}), - ...(deadline && hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) ? { intervalDeadline: deadline } : {}), + ...(hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) && (intervalDeadline || deadline) ? { intervalDeadline: intervalDeadline || deadline } : {}), ...(tribeId ? { tribeId } : {}) } if (tribeId) dateContent = await encryptIfTribe(dateContent) @@ -468,26 +536,32 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) ssbClient.get(tipId, (err, it) => err ? reject(err) : resolve(it)) }) if (!item || !item.content) throw new Error("Calendar not found") - const oldDec = item.content.tribeId - ? await decryptIfTribe(item.content) - : decryptCalendarRoot(item.content, rootId) + const oldDec = await decryptScoped(item.content, rootId) assertReadable(oldDec, "Calendar") - if ((oldDec.author || item.content.author) !== userId) throw new Error("Not the author") + if (item.author !== userId) throw new Error("Not the author") + const nextDeadline = data.deadline !== undefined ? data.deadline : (oldDec.deadline || "") + if (nextDeadline !== (oldDec.deadline || "")) assertCalendarDates({ deadline: nextDeadline }) let updated = { type: "calendar", title: data.title !== undefined ? safeText(data.title) : (oldDec.title || ""), status: data.status !== undefined ? (["OPEN","CLOSED"].includes(String(data.status).toUpperCase()) ? String(data.status).toUpperCase() : oldDec.status) : (oldDec.status || "OPEN"), - deadline: data.deadline !== undefined ? data.deadline : (oldDec.deadline || ""), + deadline: nextDeadline, tags: data.tags !== undefined ? normalizeTags(data.tags) : (Array.isArray(oldDec.tags) ? oldDec.tags : []), + mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : (oldDec.mapUrl || ""), author: oldDec.author || userId, participants: oldDec.participants || [userId], invites: Array.isArray(oldDec.invites) ? oldDec.invites : [], - ...(item.content.tribeId ? { tribeId: item.content.tribeId } : {}), + ...(oldDec.tribeId ? { tribeId: oldDec.tribeId } : {}), createdAt: oldDec.createdAt, updatedAt: new Date().toISOString(), replaces: tipId } - if (item.content.tribeId) updated = await encryptIfTribe(updated) + const closing = !oldDec.tribeId && String(oldDec.status || "OPEN").toUpperCase() === "OPEN" && updated.status === "CLOSED" + if (closing) { + updated.invites = updated.invites.filter(inv => !(inv && typeof inv === "object" && inv.public === true)) + try { await rotateCalendarKey(rootId, (updated.participants || []).filter(p => p !== userId)) } catch (_) {} + } + if (updated.tribeId) updated = await encryptIfTribe(updated) else updated = encryptStandalone(updated, rootId) const result = await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -501,10 +575,10 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const userId = ssbClient.id const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) if (!item || !item.content) throw new Error("Calendar not found") - const dec = await decryptIfTribe(item.content) + const rootId = await this.resolveRootId(id) + const dec = await decryptScoped(item.content, rootId) assertReadable(dec, "Calendar") - const contentAuthor = (dec && dec.author) || (typeof item.content === 'object' && item.content.author) - if (contentAuthor !== userId) throw new Error("Not the author") + if (item.author !== userId) throw new Error("Not the author") const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } return new Promise((resolve, reject) => ssbClient.publish(tombstone, e => e ? reject(e) : resolve())) }, @@ -535,9 +609,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const participants = Array.isArray(cal.participants) ? cal.participants : [] if (!participants.includes(userId)) return const content = { type: "calendarParticipant", target: rootId, on: false, createdAt: new Date().toISOString() } - const result = await new Promise((resolve, reject) => ssbClient.publish(content, (e, res) => e ? reject(e) : resolve(res))) - try { await rotateCalendarKey(rootId, participants.filter(p => p !== userId)) } catch (_) {} - return result + return new Promise((resolve, reject) => ssbClient.publish(content, (e, res) => e ? reject(e) : resolve(res))) }, async findCalendarByLinkText(linkSubstring) { @@ -559,7 +631,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) async getCalendarById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = memoIndex('calendars', messages, buildIndex) await decryptIndexNodes(idx) let root = id while (idx.parent.has(root)) root = idx.parent.get(root) @@ -577,6 +649,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const ssbClient = await openSsb() const uid = viewerId || ssbClient.id let list = calCollab.visibleThenCollapsed(await collectCalendars(), uid) + try { await ensureMemberKeys(ssbClient, await readAll(ssbClient), list) } catch (_) {} if (filter === "mine") list = list.filter(c => c.author === uid) else if (filter === "recent") { const now = Date.now() @@ -594,10 +667,19 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const cal = await this.getCalendarById(rootId) if (!cal) throw new Error("Calendar not found") if (cal.status === "CLOSED" && userId !== cal.author) throw new Error("Only the author can add dates to a CLOSED calendar") - if (!date || new Date(date).getTime() <= Date.now()) throw new Error("Date must be in the future") - + if (!date) throw new Error("Date must be in the future") const hasInterval = hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) + assertCalendarDates({ + deadline: cal.deadline || "", + date, + until: hasInterval ? intervalDeadline : "" + }) + const ruleDeadline = hasInterval ? (intervalDeadline || cal.deadline || "") : "" + if (!(await calendarMembers(cal)).has(userId)) { + if (cal.tribeId || cal.status !== "OPEN") throw new Error("Only participants can add dates") + await this.joinCalendar(rootId) + } let dateContent = { type: "calendarDate", calendarId: rootId, @@ -623,6 +705,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const rootId = await this.resolveRootId(calendarId) const cal = await this.getCalendarById(rootId) const calDeadline = cal && cal.deadline ? cal.deadline : "" + const pubKey = cal ? tryDecryptPublicInviteKey(cal.invites) : null + const members = cal ? await calendarMembers(cal) : new Set() const ssbClient = await openSsb() const messages = await readAll(ssbClient) const authorByKey = new Map() @@ -642,6 +726,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const c = v.content if (!c || c.type !== "calendarDate") continue if (c.calendarId !== rootId) continue + if (!members.has(v.author)) continue let dec = c if (c.encryptedPayload && tribeCrypto) { if (c.tribeId && tribesModel) { @@ -649,25 +734,24 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) dec = r && !r._undecryptable ? r : c if (r && r._undecryptable) continue } else { - const keys = lookupKeys(c.calendarId) - if (keys && keys.length) { - const r = tribeCrypto.decryptContent(c, keys.map(k => [k])) - dec = r && !r._undecryptable ? r : c - if (r && r._undecryptable) continue - } + const keys = [...(lookupKeys(c.calendarId) || []), ...(pubKey ? [pubKey] : [])] + if (!keys.length) continue + const r = tribeCrypto.decryptContent(c, keys.map(k => [k])) + if (!r || r._undecryptable) continue + dec = r } } const baseEntry = { key: m.key, calendarId: dec.calendarId || c.calendarId, label: dec.label || "", - author: dec.author || v.author, + author: v.author || dec.author, createdAt: dec.createdAt || new Date(v.timestamp || 0).toISOString() } const hasInterval = !!(dec.intervalWeekly || dec.intervalMonthly || dec.intervalYearly) const ruleDeadline = dec.intervalDeadline || calDeadline if (hasInterval && ruleDeadline) { - const occurrences = expandRecurrence(dec.date, ruleDeadline, dec.intervalWeekly, dec.intervalMonthly, dec.intervalYearly) + const occurrences = boundedOccurrences(dec.date, ruleDeadline, dec.intervalWeekly, dec.intervalMonthly, dec.intervalYearly) for (const occ of occurrences) { dates.push({ ...baseEntry, date: occ.toISOString() }) } @@ -675,8 +759,10 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) dates.push({ ...baseEntry, date: dec.date }) } } - dates.sort((a, b) => new Date(a.date) - new Date(b.date)) - return dates + const owner = cal ? cal.author : null + const kept = [...dates.filter(d => d.author === owner), ...dates.filter(d => d.author !== owner)].slice(0, MAX_DATES_PER_VIEW) + kept.sort((a, b) => new Date(a.date) - new Date(b.date)) + return kept }, async deleteDate(dateId, calendarId) { @@ -754,8 +840,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const userId = ssbClient.id const item = await new Promise((resolve, reject) => ssbClient.get(noteId, (e, it) => e ? reject(e) : resolve(it))) if (!item || !item.content) throw new Error("Note not found") - const dec = await decryptIfTribe(item.content) - if ((dec.author || item.content.author) !== userId) throw new Error("Not the author") + if (item.author !== userId) throw new Error("Not the author") return new Promise((resolve, reject) => { ssbClient.publish({ type: "tombstone", target: noteId, deletedAt: new Date().toISOString(), author: userId }, (e, msg) => e ? reject(e) : resolve(msg)) }) @@ -776,15 +861,23 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } } const notes = [] + let pubKey for (const m of messages) { const v = m.value || {} const c = v.content if (!c || c.type !== "calendarNote") continue if (tombstoned.has(m.key)) continue - if (c.calendarId !== rootId || c.dateId !== dateId) continue + if (c.calendarId !== rootId || (dateId != null && c.dateId !== dateId)) continue let dec = c - if (c.encryptedPayload && tribeCrypto && tribesModel) { - const r = await tribeCrypto.decryptFromTribe(c, tribesModel) + if (c.encryptedPayload) { + let r = await decryptScoped(c, rootId) + if ((!r || r._undecryptable) && !c.tribeId && tribeCrypto) { + if (pubKey === undefined) { + const cal = await this.getCalendarById(rootId).catch(() => null) + pubKey = cal ? tryDecryptPublicInviteKey(cal.invites) : null + } + if (pubKey) r = tribeCrypto.decryptContent(c, [[pubKey]]) + } if (r && !r._undecryptable) dec = r else continue } @@ -793,7 +886,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) calendarId: dec.calendarId || c.calendarId, dateId: dec.dateId || c.dateId, text: dec.text || "", - author: dec.author || v.author, + author: v.author || dec.author, createdAt: dec.createdAt || new Date(v.timestamp || 0).toISOString() }) } @@ -826,33 +919,36 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } } - const calendarDeadlines = new Map() + const calendarInfo = new Map() const dueByCalendar = new Map() - for (const m of messages) { - if (tombstoned.has(m.key)) continue + const dated = messages.filter(m => !tombstoned.has(m.key) && ((m.value || {}).content || {}).type === "calendarDate") + const decoded = await Promise.all(dated.map(async (m) => { + const c = m.value.content + if (!(c.encryptedPayload && tribeCrypto && tribesModel)) return c + const r = await tribeCrypto.decryptFromTribe(c, tribesModel) + return !r || r._undecryptable ? null : r + })) + for (let i = 0; i < dated.length; i++) { + const m = dated[i] const v = m.value || {} const c = v.content - if (!c || c.type !== "calendarDate") continue - let dec = c - if (c.encryptedPayload && tribeCrypto && tribesModel) { - const r = await tribeCrypto.decryptFromTribe(c, tribesModel) - if (!r || r._undecryptable) continue - dec = r - } - if (!dec.date) continue + const dec = decoded[i] + if (!dec || !dec.date) continue const calId = c.calendarId - let calDeadline = calendarDeadlines.get(calId) - if (calDeadline === undefined) { + let info = calendarInfo.get(calId) + if (info === undefined) { try { const cc = await this.getCalendarById(calId) - calDeadline = (cc && cc.deadline) || "" - } catch (_) { calDeadline = "" } - calendarDeadlines.set(calId, calDeadline) + info = cc ? { deadline: cc.deadline || "", members: await calendarMembers(cc) } : null + } catch (_) { info = null } + calendarInfo.set(calId, info) } + if (!info || !info.members.has(v.author)) continue + const calDeadline = info.deadline const hasInterval = !!(dec.intervalWeekly || dec.intervalMonthly || dec.intervalYearly) const ruleDeadline = dec.intervalDeadline || calDeadline const occurrences = (hasInterval && ruleDeadline) - ? expandRecurrence(dec.date, ruleDeadline, dec.intervalWeekly, dec.intervalMonthly, dec.intervalYearly) + ? boundedOccurrences(dec.date, ruleDeadline, dec.intervalWeekly, dec.intervalMonthly, dec.intervalYearly) : [new Date(dec.date)] for (const occ of occurrences) { if (occ.getTime() > now) continue @@ -916,18 +1012,19 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (cal.author !== userId) throw new Error("Only the author can generate invites") const code = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex") let invite = code - const pubFlag = opts.public ? { public: true } : {} - if (tribeCrypto && !cal.tribeId) { + const calKey = !cal.tribeId ? lookupKey(cal.rootId) : null + if (tribeCrypto && calKey) { const inviteSalt = tribeCrypto.generateInviteSalt() - const ekChain = tribeCrypto.encryptChainForInvite([cal.rootId], code, inviteSalt) - if (ekChain) invite = { code, ekChain, salt: inviteSalt, gen: lookupGen(cal.rootId), ...pubFlag } + const ek = tribeCrypto.encryptForInvite(calKey, code, inviteSalt) + invite = opts.public + ? { code, ek, salt: inviteSalt, gen: lookupGen(cal.rootId), public: true } + : { ch: tribeCrypto.hashInviteCode(code, inviteSalt), ek, salt: inviteSalt, gen: lookupGen(cal.rootId) } } if (opts.public && typeof invite !== "object") invite = { code, public: true } const tipId = await this.resolveCurrentId(calendarId) const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) - const dec = item.content.tribeId - ? await decryptIfTribe(item.content) - : decryptCalendarRoot(item.content, cal.rootId) + const dec = await decryptScoped(item.content, cal.rootId) + assertReadable(dec, "Calendar") const invites = [...(Array.isArray(dec.invites) ? dec.invites : []), invite] let updated = { type: "calendar", @@ -935,15 +1032,16 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], + mapUrl: dec.mapUrl || "", author: dec.author, participants: Array.isArray(dec.participants) ? dec.participants : [userId], invites, - ...(item.content.tribeId ? { tribeId: item.content.tribeId } : {}), + ...(dec.tribeId ? { tribeId: dec.tribeId } : {}), createdAt: dec.createdAt, updatedAt: new Date().toISOString(), replaces: tipId } - if (item.content.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfTribe(updated) else updated = encryptStandalone(updated, cal.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -971,9 +1069,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (cal.author !== userId) throw new Error("Only the author can remove invites") const tipId = await this.resolveCurrentId(calendarId) const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) - const dec = item.content.tribeId - ? await decryptIfTribe(item.content) - : decryptCalendarRoot(item.content, cal.rootId) + const dec = await decryptScoped(item.content, cal.rootId) + assertReadable(dec, "Calendar") const invites = (Array.isArray(dec.invites) ? dec.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)) let updated = { type: "calendar", @@ -981,19 +1078,21 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], + mapUrl: dec.mapUrl || "", author: dec.author, participants: Array.isArray(dec.participants) ? dec.participants : [userId], invites, - ...(item.content.tribeId ? { tribeId: item.content.tribeId } : {}), + ...(dec.tribeId ? { tribeId: dec.tribeId } : {}), createdAt: dec.createdAt, updatedAt: new Date().toISOString(), replaces: tipId } - if (item.content.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfTribe(updated) else updated = encryptStandalone(updated, cal.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } await new Promise((resolve, reject) => ssbClient.publish(tombstone, e => e ? reject(e) : resolve())) + if (!dec.tribeId) await rotateCalendarKey(cal.rootId, Array.isArray(cal.participants) ? cal.participants : []) }, async joinByInvite(code) { @@ -1006,16 +1105,26 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const invs = Array.isArray(cal.invites) ? cal.invites : [] for (const inv of invs) { if (typeof inv === "string" && inv === code) { matched = cal; matchedInvite = inv; break } - if (typeof inv === "object" && inv.code === code) { matched = cal; matchedInvite = inv; break } + if (typeof inv === "object" && inv && (inv.code === code || (inv.ch && tribeCrypto && inv.ch === tribeCrypto.hashInviteCode(code, inv.salt)))) { matched = cal; matchedInvite = inv; break } } if (matched) break } if (!matched) throw new Error("Invalid or expired invite code") if (matched.participants.includes(userId)) throw new Error("Already a participant") + if (typeof matchedInvite === "object" && matchedInvite.ch && tribeCrypto && matchedInvite.ek) { + let probeKey = null + try { probeKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) } catch (_) {} + const messages = await readAll(await openSsb()) + const spent = messages.some(m => { + const c = m.value && m.value.content + return c && c.type === "calendarParticipant" && c.on !== false && c.target === matched.rootId && c.code === matchedInvite.ch && m.value.author !== userId && (!probeKey || keyProofFor(probeKey, m.value.author) === c.keyProof) + }) + if (spent) throw new Error("Invite already used") + } let calKey = null if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt) + const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) if (Array.isArray(chain) && chain.length) { for (const entry of chain) { if (Array.isArray(entry.keys) && entry.keys.length) { @@ -1033,9 +1142,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } const tipId = await this.resolveCurrentId(matched.rootId) const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) - const dec = item.content.tribeId - ? await decryptIfTribe(item.content) - : decryptCalendarRoot(item.content, matched.rootId) + const dec = await decryptScoped(item.content, matched.rootId) + assertReadable(dec, "Calendar") const isPublicInvite = typeof matchedInvite === "object" && matchedInvite.public === true const invites = isPublicInvite ? (Array.isArray(dec.invites) ? dec.invites : []) @@ -1049,15 +1157,16 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], + mapUrl: dec.mapUrl || "", author: dec.author, participants: [...(Array.isArray(dec.participants) ? dec.participants : []), userId], invites, - ...(item.content.tribeId ? { tribeId: item.content.tribeId } : {}), + ...(dec.tribeId ? { tribeId: dec.tribeId } : {}), createdAt: dec.createdAt, updatedAt: new Date().toISOString(), replaces: tipId } - if (item.content.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfTribe(updated) else updated = encryptStandalone(updated, matched.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -1077,7 +1186,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } } catch (_) {} } - await new Promise((resolve, reject) => ssbClient.publish({ type: "calendarParticipant", target: matched.rootId, on: true, createdAt: new Date().toISOString() }, (e, res) => e ? reject(e) : resolve(res))) + await new Promise((resolve, reject) => ssbClient.publish({ type: "calendarParticipant", target: matched.rootId, on: true, ...(calKey ? { keyProof: keyProofFor(calKey, userId) } : {}), ...(matchedInvite && typeof matchedInvite === "object" && matchedInvite.ch ? { code: matchedInvite.ch } : {}), createdAt: new Date().toISOString() }, (e, res) => e ? reject(e) : resolve(res))) return matched.rootId } } diff --git a/src/models/campaigns_model.js b/src/models/campaigns_model.js new file mode 100644 index 0000000..add3e31 --- /dev/null +++ b/src/models/campaigns_model.js @@ -0,0 +1,312 @@ +const { readTyped, memoIndex } = require('./typed_log'); +const categories = require('../backend/opinion_categories'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const CAMPAIGN_TYPE = 'campaign'; +const SIGNATURE_TYPE = 'campaignSignature'; +const UPDATE_TYPE = 'campaignUpdate'; +const OPINION_TYPE = 'campaignOpinion'; +const PROPOSAL_TYPE = 'parliamentProposal'; +const TYPES = [CAMPAIGN_TYPE, SIGNATURE_TYPE, UPDATE_TYPE, OPINION_TYPE, PROPOSAL_TYPE, 'tombstone']; +const CATEGORIES = ['ENVIRONMENT', 'RIGHTS', 'HEALTH', 'EDUCATION', 'INFRASTRUCTURE', 'CULTURE', 'ECONOMY', 'OTHER']; +const STATUSES = ['OPEN', 'ACHIEVED', 'CLOSED']; +const RECENT_MS = 24 * 60 * 60 * 1000; + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normU = (v) => String(v || '').trim().toUpperCase(); +const normalizeCategory = (v) => (CATEGORIES.includes(normU(v)) ? normU(v) : 'OTHER'); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const toIso = (v) => { const t = Date.parse(v || ''); return Number.isFinite(t) ? new Date(t).toISOString() : ''; }; +const MEDIA_RE = /!?\[(image|video):[^\]]*\]\((&[^)]+)\)/; +const mediaOf = (text) => { const m = safeText(text).match(MEDIA_RE); return m ? { kind: m[1], blobId: m[2] } : null; }; +const goalOf = (v) => { const n = Math.floor(Number(v)); return Number.isFinite(n) && n > 0 ? n : 0; }; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const parentOf = new Map(); + const signatures = []; + const updates = []; + const opinions = []; + const proposalByCampaign = new Map(); + for (const m of messages) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === CAMPAIGN_TYPE) { + nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } else if (c.type === SIGNATURE_TYPE && typeof c.target === 'string') { + signatures.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0, text: safeText(c.text), createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString() }); + } else if (c.type === PROPOSAL_TYPE) { + const legacy = typeof c.campaignId === 'string' && c.campaignId ? null : String(c.description || '').match(/signatures: \/campaigns\/(%[^\s]+\.sha256)\s*$/); + const ref = typeof c.campaignId === 'string' && c.campaignId ? c.campaignId : (legacy ? legacy[1] : ''); + if (ref && !proposalByCampaign.has(ref)) proposalByCampaign.set(ref, m.key); + } else if (c.type === OPINION_TYPE && typeof c.target === 'string') { + opinions.push({ target: c.target, author: v.author, category: String(c.category || ''), ts: v.timestamp || 0 }); + } else if (c.type === UPDATE_TYPE && typeof c.target === 'string') { + updates.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0, text: safeText(c.text), createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString(), replaces: typeof c.replaces === 'string' ? c.replaces : null }); + } + } + const updateParent = new Map(updates.filter(u => u.replaces).map(u => [u.key, u.replaces])); + const updateRootOf = (key) => { + const seen = new Set(); + let cur = key; + while (updateParent.has(cur) && !seen.has(cur)) { seen.add(cur); cur = updateParent.get(cur); } + return cur; + }; + const updateVersions = new Map(); + for (const u of updates) { + const uroot = updateRootOf(u.key); + if (tomb.has(uroot)) continue; + if (!updateVersions.has(uroot)) updateVersions.set(uroot, []); + updateVersions.get(uroot).push(u); + } + const updateItems = []; + for (const [uroot, versions] of updateVersions) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + updateItems.push({ id: uroot, key: uroot, tipId: tip.key, target: first.target, author: first.author, ts: first.ts, text: tip.text, createdAt: first.createdAt, updatedAt: tip.createdAt, edited: tip.key !== uroot }); + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const sigByRoot = new Map(); + for (const s of signatures) { + if (!nodes.has(s.target)) continue; + const r = rootOf(s.target); + if (!sigByRoot.has(r)) sigByRoot.set(r, new Map()); + const prev = sigByRoot.get(r).get(s.author); + if (!prev || s.ts < prev.ts) sigByRoot.get(r).set(s.author, s); + } + const opinionsByRoot = new Map(); + for (const op of opinions) { + if (!nodes.has(op.target)) continue; + const r = rootOf(op.target); + if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); + opinionsByRoot.get(r).push(op); + } + const campaigns = new Map(); + const now = Date.now(); + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + const c = tip.c; + const sigs = Array.from((sigByRoot.get(root) || new Map()).values()).sort((a, b) => a.ts - b.ts); + const goal = goalOf(c.goal); + const deadline = toIso(c.deadline); + const deadlineTs = Date.parse(deadline) || 0; + const expired = !!deadlineTs && deadlineTs < now; + const achieved = goal > 0 && sigs.length >= goal; + const manual = normU(c.status) === 'CLOSED'; + const status = manual ? 'CLOSED' : (achieved ? 'ACHIEVED' : (expired ? 'CLOSED' : 'OPEN')); + const thread = updateItems.filter(u => nodes.has(u.target) && rootOf(u.target) === root && u.author === first.author).sort((a, b) => a.ts - b.ts); + const ops = {}; + const voters = []; + for (const op of (opinionsByRoot.get(root) || []).sort((a, b) => a.ts - b.ts)) { + if (voters.includes(op.author) || !categories.includes(op.category)) continue; + voters.push(op.author); + ops[op.category] = (ops[op.category] || 0) + 1; + } + campaigns.set(root, { + id: root, rootId: root, tipId: tip.key, + title: safeText(c.title), text: safeText(c.text), media: mediaOf(c.text), category: normalizeCategory(c.category), + goal, deadline, deadlineTs, expired, tags: normalizeTags(c.tags), mapUrl: safeText(c.mapUrl), + status, achieved, closed: status === 'CLOSED', proposalId: safeText(c.proposalId) || proposalByCampaign.get(root) || '', + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, + signatures: sigs, signatureCount: sigs.length, signers: sigs.map(s => s.author), + progress: goal > 0 ? Math.min(100, Math.round((sigs.length / goal) * 100)) : 0, + updates: thread, + opinions: ops, opinions_inhabitants: voters, opinionCount: voters.length, + lastActivityTs: Math.max(tip.ts, ...sigs.map(s => s.ts), ...thread.map(u => u.ts)) + }); + } + return { campaigns, rootOf, nodes, updatesById: new Map(updateItems.map(u => [u.id, u])), updateRootOf }; + }; + + const load = async () => { + const ssbClient = await openSsb(); + const idx = memoIndex('campaigns', await readTyped(ssbClient, TYPES, { limit: logLimit }), buildIndex); + return { ssbClient, idx }; + }; + + const find = (idx, id) => { + const key = String(id || ''); + if (idx.campaigns.has(key)) return idx.campaigns.get(key); + if (idx.nodes.has(key)) return idx.campaigns.get(idx.rootOf(key)) || null; + return null; + }; + + const decorate = (cp, me) => ({ + ...cp, + isOwner: cp.author === me, + signed: cp.signers.includes(me), + canSign: !cp.closed && !cp.signers.includes(me), + canElevate: cp.author === me && cp.achieved && !cp.proposalId + }); + + const versionOf = (cp, me, data = {}) => ({ + type: CAMPAIGN_TYPE, + title: safeText(data.title) || cp.title, + text: data.text !== undefined ? safeText(data.text) : cp.text, + category: data.category !== undefined ? normalizeCategory(data.category) : cp.category, + goal: data.goal !== undefined && goalOf(data.goal) ? goalOf(data.goal) : cp.goal, + deadline: data.deadline !== undefined ? toIso(data.deadline) : cp.deadline, + tags: data.tags !== undefined ? normalizeTags(data.tags) : cp.tags, + mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : cp.mapUrl, + status: data.status !== undefined ? (normU(data.status) === 'CLOSED' ? 'CLOSED' : 'OPEN') : (cp.status === 'CLOSED' && !cp.expired ? 'CLOSED' : 'OPEN'), + proposalId: data.proposalId !== undefined ? safeText(data.proposalId) : cp.proposalId, + author: me, + createdAt: cp.createdAt, + updatedAt: new Date().toISOString(), + replaces: cp.tipId + }); + + return { + CATEGORIES, + STATUSES, + + async createCampaign({ title, text, category, goal, deadline, tags, mapUrl }) { + const ssbClient = await openSsb(); + const t = safeText(title); + if (!t) throw new Error('Title is required'); + const g = goalOf(goal); + if (!g) throw new Error('A signature goal is required'); + const now = new Date().toISOString(); + return publish(ssbClient, { + type: CAMPAIGN_TYPE, title: t, text: safeText(text), category: normalizeCategory(category), goal: g, + deadline: toIso(deadline), tags: normalizeTags(tags), mapUrl: safeText(mapUrl), status: 'OPEN', + author: ssbClient.id, createdAt: now, updatedAt: now + }); + }, + + async updateCampaign(id, data = {}) { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.author !== ssbClient.id) throw new Error('Only the promoter can update this campaign'); + const res = await publish(ssbClient, versionOf(cp, ssbClient.id, data)); + return { key: res.key, rootId: cp.id }; + }, + + async closeCampaign(id) { return this.updateCampaign(id, { status: 'CLOSED' }); }, + async reopenCampaign(id) { return this.updateCampaign(id, { status: 'OPEN' }); }, + + async deleteCampaign(id) { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.author !== ssbClient.id) throw new Error('Only the promoter can delete this campaign'); + await publish(ssbClient, { type: 'tombstone', target: cp.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: cp.id }; + }, + + async sign(id, text = '') { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.closed) throw new Error('This campaign is closed'); + if (cp.signers.includes(ssbClient.id)) throw new Error('You already signed this campaign'); + return publish(ssbClient, { type: SIGNATURE_TYPE, target: cp.id, text: safeText(text).slice(0, 500), createdAt: new Date().toISOString() }); + }, + + async addUpdate(id, text) { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.author !== ssbClient.id) throw new Error('Only the promoter can post updates'); + const body = safeText(text); + if (!body) throw new Error('Text is required'); + return publish(ssbClient, { type: UPDATE_TYPE, target: cp.id, text: body, createdAt: new Date().toISOString() }); + }, + + async editUpdate(updateId, text) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) !== String(ssbClient.id)) throw new Error('Only the promoter can edit an update'); + const body = safeText(text); + if (!body) throw new Error('Text is required'); + const res = await publish(ssbClient, { type: UPDATE_TYPE, target: update.target, text: body, createdAt: new Date().toISOString(), replaces: update.tipId }); + return { key: res.key, rootId: update.id, campaignId: update.target }; + }, + + async deleteUpdate(updateId) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) !== String(ssbClient.id)) throw new Error('Only the promoter can delete an update'); + await publish(ssbClient, { type: 'tombstone', target: update.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: update.id, campaignId: update.target }; + }, + + async createOpinion(id, category) { + if (!categories.includes(category)) throw new Error('Invalid voting category'); + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.opinions_inhabitants.includes(ssbClient.id)) throw new Error('Already voted'); + return publish(ssbClient, { type: OPINION_TYPE, target: cp.id, category, createdAt: new Date().toISOString() }); + }, + + async recordProposal(id, proposalId) { + return this.updateCampaign(id, { proposalId: safeText(proposalId) }); + }, + + async getCampaignById(id, viewerId = null) { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + return cp ? decorate(cp, viewerId || ssbClient.id) : null; + }, + + async listAll({ filter = 'all', q = '', viewerId = null } = {}) { + const { ssbClient, idx } = await load(); + const me = viewerId || ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + let out = Array.from(idx.campaigns.values()).map(cp => decorate(cp, me)); + if (f === 'mine') out = out.filter(cp => cp.isOwner); + else if (f === 'signed') out = out.filter(cp => cp.signed); + else if (f === 'recent') out = out.filter(cp => cp.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'top') out = out.filter(cp => cp.signatureCount > 0); + else if (STATUSES.includes(f.toUpperCase())) out = out.filter(cp => cp.status === f.toUpperCase()); + else if (CATEGORIES.includes(f.toUpperCase())) out = out.filter(cp => cp.category === f.toUpperCase()); + if (needle) out = out.filter(cp => [cp.title, cp.text, cp.category, ...cp.tags].some(v => String(v || '').toLowerCase().includes(needle))); + if (f === 'top') out.sort((a, b) => b.signatureCount - a.signatureCount || b.lastActivityTs - a.lastActivityTs); + else out.sort((a, b) => b.lastActivityTs - a.lastActivityTs); + return out; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const cp = find(idx, id); + return cp ? cp.id : String(id || ''); + } + }; +}; diff --git a/src/models/chats_model.js b/src/models/chats_model.js index f4abc89..5f60594 100644 --- a/src/models/chats_model.js +++ b/src/models/chats_model.js @@ -2,6 +2,7 @@ const pull = require("../server/node_modules/pull-stream") const crypto = require("crypto") const { getConfig } = require("../configs/config-manager.js") const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { readTyped, memoIndex } = require("./typed_log") const { collabContent, openInviteOf } = require('../backend/collab_content') const chatCollab = collabContent({ membersField: 'members', undecField: 'undecryptable', contentFields: ['title', 'description', 'image', 'category', 'status'], listFields: ['tags', 'invites'] }) const logLimit = getConfig().ssbLogStream?.limit || 1000 @@ -16,13 +17,37 @@ const normalizeTags = (raw) => { const INVITE_CODE_BYTES = 16 const VALID_STATUS = ["OPEN", "INVITE-ONLY", "CLOSED"] +const REACTION_EMOJIS = ["up", "heart", "laugh", "down"] + +const DEFAULT_MESSAGES_PER_HOUR = 60 module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + const MESSAGES_PER_HOUR = (() => { + try { + const raw = getConfig()?.chats?.messagesPerHour + const n = parseInt(raw, 10) + return Number.isFinite(n) && n > 0 ? n : DEFAULT_MESSAGES_PER_HOUR + } catch (_) { + return DEFAULT_MESSAGES_PER_HOUR + } + })() + const ownCrypto = chatCrypto || tribeCrypto const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null + const hashCode = (code, salt) => tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null + const inviteMatches = (inv, token) => { + if (!token) return false + if (typeof inv === "string") return inv === token + if (!inv || typeof inv !== "object") return false + if (typeof inv.code === "string" && inv.code === token) return true + if (typeof inv.ch === "string" && (inv.ch === token || hashCode(token, inv.salt) === inv.ch)) return true + return false + } + const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "") + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } const lookupKeys = (rid) => { const a = (ownCrypto && ownCrypto.getKeys(rid)) || [] if (a.length) return a @@ -36,11 +61,10 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (!existing) return const newKey = ownCrypto.generateTribeKey() const newGen = ownCrypto.addNewKey(rootId, newKey) - if (!Array.isArray(remainingMembers) || !remainingMembers.length) return const ssbClient = await openSsb() const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - for (const m of remainingMembers) { + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { @@ -57,9 +81,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const ssbKeys = require("../server/node_modules/ssb-keys") const config = require("../server/ssb_config") const msgs = await readAll(ssbClient) + const idx = buildIndex(msgs) + const trusted = (root, author) => { + const n = idx.nodes.get(root) + return !!author && !!n && (author === n.author || author === ssbClient.id) + } for (const m of msgs) { const c = m.value && m.value.content if (!c || c.type !== "tribe-keys") continue + if (!trusted(c.tribeId, m.value.author)) continue const memberKeys = c.memberKeys if (!memberKeys || typeof memberKeys !== "object") continue const boxed = memberKeys[ssbClient.id] @@ -86,10 +116,9 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { return ks.length ? ks[0] : null } - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ) + const CHAT_TYPES = ["chat", "chatMessage", "chatMember", "chatReaction", "chatPin", "tribe-keys", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, CHAT_TYPES, { limit: logLimit }) const buildIndex = (messages) => { const tomb = new Set() @@ -100,6 +129,8 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const authorByKey = new Map() const tombRequests = [] const memberMsgs = [] + const reactMsgs = [] + const pinMsgs = [] for (const m of messages) { const k = m.key @@ -108,11 +139,19 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (!c) continue if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue } if (c.type === "chatMember" && c.target && c.member) { - memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, code: typeof c.code === "string" ? c.code : "", author: v.author, ts: v.timestamp || m.timestamp || 0 }) + memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, code: typeof c.code === "string" ? c.code : "", keyProof: typeof c.keyProof === "string" ? c.keyProof : "", author: v.author, ts: v.timestamp || m.timestamp || 0 }) + continue + } + if (c.type === "chatReaction" && c.target && REACTION_EMOJIS.includes(c.emoji)) { + reactMsgs.push({ target: c.target, emoji: c.emoji, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0 }) + continue + } + if (c.type === "chatPin" && c.target) { + pinMsgs.push({ target: c.target, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0 }) continue } if (c.type === "chat") { - nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) authorByKey.set(k, v.author) if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) } } else if (c.type === "chatMessage") { @@ -121,24 +160,28 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } } - const strictParent = new Map() - const strictChild = new Map() - for (const [c1, p1] of parent.entries()) { - const childNode = nodes.get(c1) - const parentNode = nodes.get(p1) - if (childNode && parentNode && childNode.author === parentNode.author) { - strictParent.set(c1, p1) - strictChild.set(p1, c1) - } - } - for (const t of tombRequests) { const targetAuthor = authorByKey.get(t.target) if (targetAuthor && t.author === targetAuthor) tomb.add(t.target) } - const rootOf = (id) => { let cur = id; while (strictParent.has(cur)) cur = strictParent.get(cur); return cur } - const tipOf = (id) => { let cur = id; while (strictChild.has(cur)) cur = strictChild.get(cur); return cur } + const rootMemo = new Map() + const rootOf = (id) => { + if (rootMemo.has(id)) return rootMemo.get(id) + let cur = id, g = 0 + while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur) + rootMemo.set(id, cur) + return cur + } + const ownerTip = new Map() + for (const [k, n] of nodes) { + const r = rootOf(k) + const rn = nodes.get(r) + if (!rn || n.author !== rn.author) continue + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k) + } + const tipOf = (id) => { const r = rootOf(id); return ownerTip.get(r) || r } const roots = new Set() for (const id of nodes.keys()) roots.add(rootOf(id)) @@ -146,25 +189,72 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { for (const r of roots) tipByRoot.set(r, tipOf(r)) const memberByRoot = new Map() + const invitesOf = (root) => { + const out = [] + for (const id of [root, tipOf(root)]) { const n = nodes.get(id); if (n && Array.isArray(n.c.invites)) out.push(...n.c.invites) } + return out + } + const sealedRoot = (root) => { const oc = (nodes.get(root) || {}).c || {}; return !!(oc.encryptedPayload || oc.encrypted === true) } + const selfJoinAllowed = (root, mm) => { + if (!sealedRoot(root)) return true + const invites = invitesOf(root) + if (invites.some(inv => inv && typeof inv === "object" && inv.public === true)) return true + if (mm.keyProof && lookupKeys(root).some(k => keyProofFor(k, mm.member) === mm.keyProof)) return true + const tribeChat = !!((nodes.get(root) || {}).c || {}).tribeId + return !!mm.code && invites.some(inv => tribeChat ? inviteMatches(inv, mm.code) : (typeof inv === "string" ? inv === mm.code : !!(inv && typeof inv === "object" && typeof inv.code === "string" && inv.code === mm.code))) + } const consumedByRoot = new Map() + const codeClaimsByRoot = new Map() for (const mm of memberMsgs) { if (!nodes.has(mm.target)) continue const r = rootOf(mm.target) - if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) - const perMember = memberByRoot.get(r) - const prev = perMember.get(mm.member) - if (!prev || mm.ts >= prev.ts) perMember.set(mm.member, { on: mm.on, ts: mm.ts }) + const ownerAuthor = nodes.get(r) && nodes.get(r).author + const self = mm.member === mm.author + if (!self && mm.author !== ownerAuthor) continue + if (mm.on && mm.code) { + if (!codeClaimsByRoot.has(r)) codeClaimsByRoot.set(r, []) + codeClaimsByRoot.get(r).push({ code: mm.code, member: mm.member, keyProof: mm.keyProof, byOwner: mm.author === ownerAuthor }) + } + if (self && mm.author !== ownerAuthor && mm.on && !selfJoinAllowed(r, mm)) continue if (mm.on && mm.code) { if (!consumedByRoot.has(r)) consumedByRoot.set(r, new Set()) consumedByRoot.get(r).add(mm.code) } + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const perMember = memberByRoot.get(r) + const prev = perMember.get(mm.member) + if (!prev || mm.ts >= prev.ts) perMember.set(mm.member, { on: mm.on, ts: mm.ts }) } const isCodeConsumed = (rootId, code) => !!(code && consumedByRoot.has(rootId) && consumedByRoot.get(rootId).has(code)) + const codeClaims = (rootId, codes) => (codeClaimsByRoot.get(rootId) || []).filter(c => codes.includes(c.code)) const rawRootOf = (id) => { let cur = id, g = 0; while (parent.has(cur) && g++ < 100000) cur = parent.get(cur); return cur } - return { tomb, nodes, parent: strictParent, child: strictChild, rawParent: parent, rawChild: child, rawRootOf, rootOf, tipOf, tipByRoot, msgNodes, memberByRoot, consumedByRoot, isCodeConsumed } + for (const [f, fNode] of nodes) { + const r = rootOf(f) + if (!nodes.has(r) || fNode.author === nodes.get(r).author) continue + const opened = decryptChatContent(fNode.c, r) + if (opened.undecryptable || !Array.isArray(opened.c && opened.c.members)) continue + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const perMember = memberByRoot.get(r) + const prev = perMember.get(fNode.author) + if (!prev || fNode.ts >= prev.ts) perMember.set(fNode.author, { on: opened.c.members.includes(fNode.author), ts: fNode.ts }) + } + + const reactionByKey = new Map() + for (const r of reactMsgs) { + const rk = `${r.target}|${r.author}|${r.emoji}` + const prev = reactionByKey.get(rk) + if (!prev || r.ts >= prev.ts) reactionByKey.set(rk, r) + } + const pinByTarget = new Map() + for (const pm of pinMsgs) { + const prev = pinByTarget.get(pm.target) + if (!prev || pm.ts >= prev.ts) pinByTarget.set(pm.target, pm) + } + + return { tomb, nodes, rawRootOf, rootOf, tipOf, tipByRoot, msgNodes, memberByRoot, consumedByRoot, isCodeConsumed, codeClaims, reactionByKey, pinByTarget } } const resolveKeyChainSets = (chatRootId) => { @@ -236,13 +326,13 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const { c, undecryptable } = decryptChatContent(rawC, rootId) let ownerContent = c - let ownerAuthor = c.author || node.author + let ownerAuthor = node.author if (idx) { const ownerNode = idx.nodes.get(rootId) if (ownerNode) { const decOwner = decryptChatContent(ownerNode.c || {}, rootId) ownerContent = decOwner.c - ownerAuthor = decOwner.c.author || ownerNode.author + ownerAuthor = ownerNode.author } } const memberToggles = idx && idx.memberByRoot ? idx.memberByRoot.get(rootId) : null @@ -263,7 +353,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { tags: safeArr(c.tags), members, invites, - author: c.author || node.author, + author: node.author, createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, encrypted: !!c.encrypted, @@ -277,11 +367,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (c.type !== "chatMessage") return null let text = c.text || "" + let image = c.image || null + let mimeType = typeof c.mimeType === "string" ? c.mimeType : null if (tribeCrypto && c.encryptedText) { const candidateKeys = [...tribeKeys, ...lookupKeys(chatRootId)] for (const keyHex of candidateKeys) { try { text = tribeCrypto.decryptWithKey(c.encryptedText, keyHex) + if (c.encryptedImage) image = tribeCrypto.decryptWithKey(c.encryptedImage, keyHex) + if (c.encryptedMime) mimeType = tribeCrypto.decryptWithKey(c.encryptedMime, keyHex) break } catch (_) {} } @@ -291,8 +385,10 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { key: node.key, chatId: c.chatId || "", text, - image: c.image || null, - author: c.author || node.author, + image, + mimeType, + replyTo: typeof c.replyTo === "string" ? c.replyTo : null, + author: node.author, createdAt: c.createdAt || new Date(node.ts).toISOString() } } @@ -304,28 +400,27 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { }) const ensureMemberKeys = async (ssbClient, messages, chats) => { - if (!tribeCrypto) return - const distributed = new Map() + if (!tribeCrypto || !ownCrypto) return + const leavesByRoot = new Map() for (const m of messages) { const c = m.value && m.value.content - if (!c || c.type !== "tribe-keys" || !c.tribeId) continue - const mk = c.memberKeys - if (!mk || typeof mk !== "object") continue - if (!distributed.has(c.tribeId)) distributed.set(c.tribeId, new Set()) - for (const id of Object.keys(mk)) distributed.get(c.tribeId).add(id) + if (!c || c.type !== "chatMember" || c.on !== false || !c.target || !c.member) continue + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []) + leavesByRoot.get(c.target).push({ member: c.member, author: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }) } const ssbKeys = require("../server/node_modules/ssb-keys") for (const chat of safeArr(chats)) { - if (!chat || chat.undecryptable) continue + if (!chat || chat.undecryptable || chat.author !== ssbClient.id) continue const rootId = chat.rootId - if (!rootId) continue + if (!rootId || !lookupKey(rootId)) continue + const members = safeArr(chat.members) + const leaves = (leavesByRoot.get(rootId) || []).filter(l => l.author === l.member || l.author === chat.author) + const plan = ownCrypto.keyPlan({ rootId, ownerId: chat.author, gen: lookupGen(rootId), members, messages, leaves }) + if (plan.rotate) { await rotateChatKey(rootId, members); continue } + if (!plan.missing.length) continue const key = lookupKey(rootId) - if (!key) continue - const have = distributed.get(rootId) || new Set() - const missing = safeArr(chat.members).filter(m => m && m !== ssbClient.id && !have.has(m)) - if (!missing.length) continue const memberKeys = {} - for (const m of missing) { + for (const m of plan.missing) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue @@ -339,9 +434,22 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { new Promise((resolve, reject) => { const content = { type: "chatMember", target: rootId, member, on: !!on, createdAt: new Date().toISOString() } if (typeof code === "string" && code) content.code = code + const proofKey = on && member === ssbClient.id ? lookupKey(rootId) : null + if (proofKey) content.keyProof = keyProofFor(proofKey, member) ssbClient.publish(content, (e, res) => e ? reject(e) : resolve(res)) }) + const decryptTribeChats = async (idx) => { + if (!tribeCrypto || !tribesModel || typeof tribeCrypto.decryptFromTribe !== "function") return idx + for (const [k, n] of idx.nodes) { + const c = n.c || {} + if (!c.encryptedPayload || !c.tribeId) continue + const dec = await tribeCrypto.decryptFromTribe(c, tribesModel).catch(() => null) + if (dec && !dec._undecryptable) idx.nodes.set(k, { ...n, c: dec }) + } + return idx + } + const collectChats = (idx) => { const out = [] for (const [rootId, tipId] of idx.tipByRoot.entries()) { @@ -357,15 +465,21 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { return { type: "chat", + async encryptionKeyFor(chatRootId, tribeId = null) { + if (!tribeCrypto) return null + if (tribeId) { + const k = await getTribeFirstKeyFor(tribeId) + if (k) return k + } + return lookupKey(chatRootId) || null + }, + async resolveRootId(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) - if (idx.tomb.has(tip)) throw new Error("Not found") - let root = tip - while (idx.parent.has(root)) root = idx.parent.get(root) + const root = idx.rootOf(id) + if (idx.tomb.has(idx.tipOf(root))) throw new Error("Not found") return root }, @@ -373,13 +487,12 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) + const tip = idx.tipOf(idx.rootOf(id)) if (idx.tomb.has(tip)) throw new Error("Not found") return tip }, - async createChat(title, description, image, category, status, tagsRaw, tribeId) { + async createChat(title, description, image, category, status, tagsRaw, tribeId, extra = {}) { const ssbClient = await openSsb() const userId = ssbClient.id const blobId = image ? String(image).trim() || null : null @@ -400,7 +513,8 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { author: userId, createdAt: now, updatedAt: now, - ...(tribeId ? { tribeId } : {}) + ...(tribeId ? { tribeId } : {}), + ...(extra && typeof extra === "object" && typeof extra.courseId === "string" && extra.courseId ? { courseId: extra.courseId } : {}) } if (!tribeCrypto) { @@ -456,13 +570,11 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { ssbClient.get(tipId, (err, item) => err || !item?.content ? reject(new Error("Chat not found")) : resolve(item)) }) const c = item.content - const rawAuthor = c.author || (c.encryptedPayload ? null : undefined) - if (rawAuthor && rawAuthor !== userId) throw new Error("Not the author") + if (item.author !== userId) throw new Error("Not the author") const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let rootId = tipId - while (idx.parent.has(rootId)) rootId = idx.parent.get(rootId) + const rootId = idx.rootOf(tipId) const node = { key: tipId, c, author: item.author, ts: item.timestamp || 0 } const chat = buildChat(node, rootId, idx) if (!chat) throw new Error("Invalid chat") @@ -522,7 +634,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { return new Promise((resolve, reject) => { ssbClient.get(tipId, (err, item) => { if (err || !item?.content) return reject(new Error("Chat not found")) - if (item.content.author && item.content.author !== userId) return reject(new Error("Not the author")) + if (item.author !== userId) return reject(new Error("Not the author")) ssbClient.publish({ type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }, (e) => e ? reject(e) : resolve()) }) }) @@ -533,11 +645,9 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const userId = ssbClient.id const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) + const root = idx.rootOf(id) + const tip = idx.tipOf(root) if (idx.tomb.has(tip)) throw new Error("Not found") - let root = tip - while (idx.parent.has(root)) root = idx.parent.get(root) const node = idx.nodes.get(tip) if (!node) throw new Error("Not found") @@ -570,18 +680,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { async getChatById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = await decryptTribeChats(memoIndex('chats', messages, buildIndex)) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) + const root = idx.rootOf(id) + const tip = idx.tipOf(root) if (idx.tomb.has(tip)) return null const node = idx.nodes.get(tip) if (!node || node.c.type !== "chat") return null - let root = tip - while (idx.parent.has(root)) root = idx.parent.get(root) - const chat = buildChat(node, root, idx) if (!chat) return null return chatCollab.fold(chat, collectChats(idx)) @@ -591,13 +698,25 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const ssbClient = await openSsb() const uid = viewerId || ssbClient.id const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = await decryptTribeChats(buildIndex(messages)) const now = Date.now() let list = chatCollab.visibleThenCollapsed(collectChats(idx), uid) + const lastMsgAt = new Map() + const lastMineAt = new Map() + for (const node of idx.msgNodes.values()) { + const cid = node.c && node.c.chatId + if (!cid) continue + const r = idx.rawRootOf(cid) || cid + const t = node.ts || 0 + if (t > (lastMsgAt.get(r) || 0)) lastMsgAt.set(r, t) + if (node.author === uid && t > (lastMineAt.get(r) || 0)) lastMineAt.set(r, t) + } + list = list.map(c => ({ ...c, lastMsgAt: lastMsgAt.get(c.rootId) || 0, lastMineAt: lastMineAt.get(c.rootId) || 0 })) + if (filter === "mine") list = list.filter(c => c.author === uid) - else if (filter === "recent") list = list.filter(c => new Date(c.createdAt).getTime() >= now - 86400000) + else if (filter === "recent") list = list.filter(c => Math.max(new Date(c.createdAt).getTime() || 0, Number(c.lastMsgAt || 0)) >= now - 86400000) else if (filter === "open") list = list.filter(c => c.status === "OPEN" || c.status === "INVITE-ONLY") else if (filter === "closed") list = list.filter(c => c.status === "CLOSED") @@ -631,13 +750,16 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (tribeCrypto) { const inviteSalt = tribeCrypto.generateInviteSalt() const ekChain = tribeCrypto.encryptChainForInvite([chat.rootId], code, inviteSalt) + const ident = opts.public ? { code, ...pubFlag } : { ch: hashCode(code, inviteSalt) } if (ekChain) { - invite = { code, ekChain, salt: inviteSalt, gen: lookupGen(chat.rootId), ...pubFlag } + invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(chat.rootId) } } else { const chatKey = lookupKey(chat.rootId) if (chatKey) { const ek = tribeCrypto.encryptForInvite(chatKey, code, inviteSalt) - invite = { code, ek, salt: inviteSalt, gen: lookupGen(chat.rootId), ...pubFlag } + invite = { ...ident, ek, salt: inviteSalt, gen: lookupGen(chat.rootId) } + } else { + invite = { ...ident, salt: inviteSalt } } } } @@ -668,6 +790,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (chat.author !== userId) throw new Error("Only the author can remove invites") const invites = (Array.isArray(chat.invites) ? chat.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)) await this.updateChatById(chatId, { invites, members: chat.members, status: chat.status, title: chat.title, description: chat.description, image: chat.image, category: chat.category, tags: chat.tags }) + if (!chat.tribeId) await rotateChatKey(chat.rootId, chat.members) }, async joinByInvite(code) { @@ -687,12 +810,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (!chat || !chat.invites.length) continue for (const inv of chat.invites) { - if (typeof inv === "string" && inv === code) { - matchedChat = chat; matchedInvite = inv; break - } - if (typeof inv === "object" && inv.code === code) { - matchedChat = chat; matchedInvite = inv; break - } + if (inviteMatches(inv, code)) { matchedChat = chat; matchedInvite = inv; break } } if (matchedChat) break } @@ -701,29 +819,40 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (matchedChat.members.includes(userId)) throw new Error("Already a participant") const isPublic = typeof matchedInvite === "object" && matchedInvite.public === true - if (!isPublic && idx.isCodeConsumed(matchedChat.rootId, code)) throw new Error("Invite already used") + const token = inviteToken(matchedInvite) || code let chatKey = null + let chain = null if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt) - if (Array.isArray(chain) && chain.length) { - for (const entry of chain) { - if (Array.isArray(entry.keys) && entry.keys.length) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length) - } else if (entry.key) { - tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1) - } - } - chatKey = chain[0].key - } + chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) + if (Array.isArray(chain) && chain.length) chatKey = chain[0].key + else chain = null } else if (matchedInvite.ek) { chatKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) - ownCrypto.setKey(matchedChat.rootId, chatKey, matchedInvite.gen || 1) } } - await publishMemberToggle(ssbClient, matchedChat.rootId, userId, true, isPublic ? undefined : code) + if (!isPublic) { + if (idx.isCodeConsumed(matchedChat.rootId, token) || idx.isCodeConsumed(matchedChat.rootId, code)) throw new Error("Invite already used") + const claims = idx.codeClaims(matchedChat.rootId, [token, code]).filter(c => c.member !== userId) + const spent = chatKey ? claims.some(c => c.byOwner || keyProofFor(chatKey, c.member) === c.keyProof) : claims.length > 0 + if (spent) throw new Error("Invite already used") + } + + if (chain) { + for (const entry of chain) { + if (Array.isArray(entry.keys) && entry.keys.length) { + tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length) + } else if (entry.key) { + tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1) + } + } + } else if (chatKey) { + ownCrypto.setKey(matchedChat.rootId, chatKey, matchedInvite.gen || 1) + } + + await publishMemberToggle(ssbClient, matchedChat.rootId, userId, true, isPublic ? undefined : token) if (tribeCrypto && chatKey) { try { @@ -767,9 +896,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const chat = await this.getChatById(chatId) if (!chat) throw new Error("Chat not found") if (chat.author === userId) throw new Error("Author cannot leave their own chat") - const members = chat.members.filter(m => m !== userId) await publishMemberToggle(ssbClient, chat.rootId, userId, false) - try { await rotateChatKey(chat.rootId, members) } catch (_) {} }, async ingestKeys() { await ingestOwnTribeKeys() }, @@ -779,17 +906,11 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const live = new Set() - for (const m of messages) { - const c = m.value && m.value.content - if (!c) continue - if (c.type === "chat") live.add(m.key) - } const tomb = buildValidatedTombstoneSet(messages) const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (!live.has(rid) || tomb.has(rid)) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } @@ -797,7 +918,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } catch (_) { return 0 } }, - async sendMessage(chatId, text, image = null) { + async sendMessage(chatId, text, image = null, replyTo = null, mimeType = null) { const ssbClient = await openSsb() const userId = ssbClient.id const chat = await this.getChatById(chatId) @@ -814,7 +935,12 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const c = m.value?.content return c?.type === "chatMessage" && c?.chatId === chat.rootId && m.value?.author === userId && (m.value?.timestamp || 0) >= oneHourAgo }).length - if (recentCount >= 3) throw new Error("Rate limit: max 3 messages per hour") + if (recentCount >= MESSAGES_PER_HOUR) { + const err = new Error(`Rate limit: max ${MESSAGES_PER_HOUR} messages per hour`) + err.code = "CHAT_RATE_LIMIT" + err.retryAfterMinutes = 60 + throw err + } const now = new Date().toISOString() let content = { @@ -824,6 +950,14 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { createdAt: now } if (image) content.image = image + if (image && mimeType) content.mimeType = String(mimeType) + if (replyTo) { + const idxAll = buildIndex(messages) + const target = idxAll.msgNodes.get(replyTo) + if (target && (target.c.chatId === chat.rootId || idxAll.rawRootOf(target.c.chatId) === idxAll.rawRootOf(chat.rootId))) { + content.replyTo = replyTo + } + } const chatIsEncrypted = !!(chat.tribeId) || !!lookupKey(chat.rootId) if (chatIsEncrypted && tribeCrypto) { @@ -832,6 +966,14 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (!encKey) encKey = lookupKey(chat.rootId) if (!encKey) throw new Error(`Missing chat key for ${chat.rootId} — cannot send message`) content.encryptedText = tribeCrypto.encryptWithKey(safeText(text), encKey) + if (content.image) { + content.encryptedImage = tribeCrypto.encryptWithKey(String(content.image), encKey) + delete content.image + if (content.mimeType) { + content.encryptedMime = tribeCrypto.encryptWithKey(String(content.mimeType), encKey) + delete content.mimeType + } + } if (chat.tribeId) content.tribeId = chat.tribeId } else { content.text = safeText(text) @@ -862,10 +1004,74 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (msg) result.push(msg) } + const viewerId = ssbClient.id + let chatAuthor = null + if (chatNode) chatAuthor = chatNode.author + const reactsByTarget = new Map() + for (const r of idx.reactionByKey.values()) { + if (!r.on) continue + if (!reactsByTarget.has(r.target)) reactsByTarget.set(r.target, []) + reactsByTarget.get(r.target).push(r) + } + const byKey = new Map(result.map(m => [m.key, m])) + for (const m of result) { + const counts = {} + const mine = {} + for (const e of REACTION_EMOJIS) { counts[e] = 0; mine[e] = false } + for (const r of reactsByTarget.get(m.key) || []) { + counts[r.emoji] += 1 + if (r.author === viewerId) mine[r.emoji] = true + } + m.reactions = { counts, mine } + const pin = idx.pinByTarget.get(m.key) + m.pinned = !!(pin && pin.on && chatAuthor && pin.author === chatAuthor) + const quoted = m.replyTo ? byKey.get(m.replyTo) : null + m.reply = quoted ? { author: quoted.author, text: String(quoted.text || "").slice(0, 120) } : null + } + result.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) return result }, + async toggleReaction(chatId, targetKey, emoji) { + if (!REACTION_EMOJIS.includes(emoji)) throw new Error("Invalid reaction") + const ssbClient = await openSsb() + const userId = ssbClient.id + const chat = await this.getChatById(chatId) + if (!chat) throw new Error("Chat not found") + if (chat.status === "CLOSED") throw new Error("Chat is closed") + if (!chat.members.includes(userId)) { + if (chat.status === "OPEN") await this.joinChat(chatId) + else throw new Error("Not a participant") + } + const messages = await readAll(ssbClient) + const idx = buildIndex(messages) + const target = idx.msgNodes.get(targetKey) + if (!target || (target.c.chatId !== chat.rootId && idx.rawRootOf(target.c.chatId) !== idx.rawRootOf(chat.rootId))) throw new Error("Message not found") + const prev = idx.reactionByKey.get(`${targetKey}|${userId}|${emoji}`) + const on = !(prev && prev.on) + return new Promise((resolve, reject) => { + ssbClient.publish({ type: "chatReaction", chatId: chat.rootId, target: targetKey, emoji, on, author: userId, createdAt: new Date().toISOString() }, (e, res) => e ? reject(e) : resolve(res)) + }) + }, + + async togglePin(chatId, targetKey) { + const ssbClient = await openSsb() + const userId = ssbClient.id + const chat = await this.getChatById(chatId) + if (!chat) throw new Error("Chat not found") + if (chat.author !== userId) throw new Error("Only the chat author can pin") + const messages = await readAll(ssbClient) + const idx = buildIndex(messages) + const target = idx.msgNodes.get(targetKey) + if (!target || (target.c.chatId !== chat.rootId && idx.rawRootOf(target.c.chatId) !== idx.rawRootOf(chat.rootId))) throw new Error("Message not found") + const prev = idx.pinByTarget.get(targetKey) + const on = !(prev && prev.on && prev.author === userId) + return new Promise((resolve, reject) => { + ssbClient.publish({ type: "chatPin", chatId: chat.rootId, target: targetKey, on, author: userId, createdAt: new Date().toISOString() }, (e, res) => e ? reject(e) : resolve(res)) + }) + }, + async getParticipants(chatRootId) { const chat = await this.getChatById(chatRootId) if (!chat) return [] diff --git a/src/models/content_visibility.js b/src/models/content_visibility.js new file mode 100644 index 0000000..6afd0b7 --- /dev/null +++ b/src/models/content_visibility.js @@ -0,0 +1,30 @@ +const up = (v) => String(v == null ? '' : v).toUpperCase(); +const arr = (v) => (Array.isArray(v) ? v : []); +const isPrivateFlag = (v) => { + if (v === false) return true; + const s = String(v == null ? '' : v).toLowerCase(); + return s === 'private' || s === 'false' || s === '0'; +}; + +const isContentVisibleTo = (type, c, viewer, author = null) => { + if (!c || typeof c !== 'object') return false; + if (c.encrypted === true || c.encryptedPayload || c.encryptedQuestion || c.encryptedText) return false; + const owner = String(author || c.author || '') === String(viewer); + switch (type) { + case 'task': return !isPrivateFlag(c.isPublic) || owner || arr(c.assignees).includes(viewer); + case 'event': return !isPrivateFlag(c.isPublic) || owner || arr(c.attendees).includes(viewer); + case 'forum': return !(c.isPrivate === true || c.isPrivate === 'true' || c.isPrivate === 'on') || owner; + case 'job': return up(c.visibility) !== 'HIDDEN' || owner || arr(c.subscribers).includes(viewer); + case 'housing': + case 'market': return up(c.visibility) !== 'HIDDEN' || owner; + case 'shop': return up(c.visibility) !== 'CLOSED' || owner; + case 'schoolCourse': return up(c.visibility) !== 'INVITE' || owner || arr(c.students).includes(viewer) || arr(c.invited).includes(viewer); + case 'tribe': return c.isAnonymous === false || owner || arr(c.members).includes(viewer); + case 'poll': return !c.chatId && !c.tribeId; + case 'room': return !c.tribeId && c.encrypted !== true; + case 'curriculum': return up(c.visibility) !== 'HIDDEN' || owner; + default: return !c.tribeId; + } +}; + +module.exports = { isContentVisibleTo }; diff --git a/src/models/courts_model.js b/src/models/courts_model.js index 13b8fde..91f946c 100644 --- a/src/models/courts_model.js +++ b/src/models/courts_model.js @@ -2,6 +2,9 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); +const { buildVoteResults } = require('../backend/vote_tally'); +const { versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const CASE_ANSWER_DAYS = 7; @@ -16,6 +19,13 @@ const EVIDENCE_FIELDS = ['text', 'link', 'imageUrl']; const ANSWER_FIELDS = ['stance', 'text']; const VERDICT_FIELDS = ['result', 'orders']; const SETTLEMENT_FIELDS = ['terms']; +const COURTS_TYPES = [ + 'courtsCase', 'courtsMediators', 'courtsJudge', 'courtsVoteLink', 'courtsVisibility', + 'courtsAnswer', 'courtsEvidence', 'courtsVerdict', 'courtsSettlementProposal', + 'courtsSettlementAccepted', 'courtsSupport', 'courtsNomination', 'courtsNominationVote', + 'courts-key', 'tombstone' +]; +const VOTE_LOG_TYPES = ['votes', 'votesVote', 'tombstone']; module.exports = ({ cooler, services = {}, tribeCrypto }) => { let ssb; @@ -73,12 +83,7 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => { async function readLog() { const ssbClient = await openSsb(); - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, arr) => (err ? reject(err) : resolve(arr))) - ); - }); + return readTyped(ssbClient, COURTS_TYPES, { limit: logLimit }); } async function listByType(type) { @@ -86,13 +91,19 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => { const tomb = buildValidatedTombstoneSet(msgs); const rep = new Map(); const map = new Map(); + const authorOf = new Map(); + for (const m of msgs) { + const c = m.value?.content || m.content; + if (c && c.type === type) authorOf.set(m.key || m.id, m.value?.author); + } for (const m of msgs) { const k = m.key || m.id; const c = m.value?.content || m.content; if (!c) continue; if (c.type === type) { + if (c.replaces && authorOf.has(c.replaces) && authorOf.get(c.replaces) !== m.value?.author) continue; if (c.replaces) rep.set(c.replaces, k); - map.set(k, { id: k, ...c }); + map.set(k, { ...c, id: k }); } } for (const oldId of rep.keys()) map.delete(oldId); @@ -252,7 +263,7 @@ if (c.type === type) { myPublicPreference = c.publicPrefRespondent; } const rootId = c.rootCaseId || c.id; - const tally = c.voteId ? await voteTally(c.voteId) : null; + const tally = c.voteId ? await voteTally(c.voteId, [c.accuser, c.respondentId]) : null; rows.push({ ...c, respondent: c.respondentId || c.respondent, @@ -284,27 +295,24 @@ if (c.type === type) { for (const m of msgs) { const c = m.value?.content || m.content; if (!c || c.type !== 'courtsCase') continue; - nodes.set(m.key || m.id, { key: m.key || m.id, author: m.value?.author, c }); + nodes.set(m.key || m.id, { key: m.key || m.id, author: m.value?.author, seq: m.value?.sequence || 0, ts: m.value?.timestamp || m.timestamp || 0, c }); } - const strictNext = new Map(), strictPrev = new Map(); - for (const [k, n] of nodes) { - const t = n.c.replaces; - if (t) { const o = nodes.get(t); if (o && o.author === n.author) { strictNext.set(t, k); strictPrev.set(k, t); } } - } - return { nodes, strictNext, strictPrev }; + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === 'string' ? n.c.replaces : null, claimedAuthor: typeof n.c.accuser === 'string' ? n.c.accuser : null }))); + return { nodes, vg }; } function strictCaseBase(msgs, id, idxIn) { const idx = idxIn || collectCaseNodes(msgs); - const { nodes, strictNext, strictPrev } = idx; + const { nodes, vg } = idx; const tomb = buildValidatedTombstoneSet(msgs); let startKey = nodes.has(id) ? id : null; if (!startKey) { for (const [k, n] of nodes) { if (String(n.c.rootCaseId || '') === id) { startKey = k; break; } } } if (!startKey) return null; - let root = startKey, g = 0; while (strictPrev.has(root) && g++ < 100000) root = strictPrev.get(root); - let tip = root; g = 0; while (strictNext.has(tip) && g++ < 100000) tip = strictNext.get(tip); + const root = vg.rootOf(startKey); + if (vg.isForeignCopy(root)) return null; + const tip = vg.tipOf(root); if (tomb.has(tip) || tomb.has(root)) return null; const node = nodes.get(tip); if (!node) return null; @@ -329,6 +337,7 @@ if (c.type === type) { let medATs = 0, medRTs = 0, judgeId = '', judgeTs = 0, voteId = String(base.voteId || ''), voteTs = 0; let prefA = base.publicPrefAccuser, prefATs = 0, prefR = base.publicPrefRespondent, prefRTs = 0; let answered = false, settled = false, verdict = null, verdictTs = 0, verdictAt = null, settledAt = null; + const verdictClaims = []; for (const m of msgs) { const c = m.value?.content || m.content; if (!c) continue; @@ -351,20 +360,20 @@ if (c.type === type) { answered = true; } else if (c.type === 'courtsSettlementAccepted' && matchC(c) && isParty(author)) { settled = true; settledAt = c.createdAt || new Date(ts).toISOString(); - } else if (c.type === 'courtsVerdict' && matchC(c) && ts >= verdictTs) { - verdict = author; verdictTs = ts; verdictAt = c.createdAt || new Date(ts).toISOString(); + } else if (c.type === 'courtsVerdict' && matchC(c)) { + verdictClaims.push({ author, ts, at: c.createdAt || new Date(ts).toISOString() }); } } - let verdictValid = false; - if (verdict) { - if (method === 'JUDGE') verdictValid = (verdict === judgeId); - else if (method === 'MEDIATION') verdictValid = (medA.includes(verdict) || medR.includes(verdict)); - else if (method === 'DICTATOR') verdictValid = (verdict === dictatorId); + const legit = await verdictAuthors({ method, judgeId, medA, medR, dictatorId, openedAt: base.openedAt || base.createdAt, accuser, respondentId }); + for (const v of verdictClaims) { + if (!legit.has(v.author) || v.ts < verdictTs) continue; + verdict = v.author; verdictTs = v.ts; verdictAt = v.at; } + const verdictValid = !!verdict; let voteDecided = false; if ((method === 'POPULAR' || method === 'KARMATOCRACY') && voteId) { - try { const t = await voteTally(voteId); if (t && t.closed && t.total > 0) voteDecided = true; } catch (_) {} + try { const t = await voteTally(voteId, [accuser, respondentId]); if (t && t.closed && t.total > 0 && (t.yes >= t.needed || t.no >= t.needed)) voteDecided = true; } catch (_) {} } base.mediatorsAccuser = medA; @@ -380,6 +389,33 @@ if (c.type === type) { return base; } + async function isRulingDictator(feedId, at) { + if (!feedId || !services.parliament || !services.parliament.listTerms) return false; + const atMs = new Date(at).getTime(); + if (!Number.isFinite(atMs)) return false; + try { + const terms = await services.parliament.listTerms('all'); + return (terms || []).some(t => + String(t.method || '').toUpperCase() === 'DICTATORSHIP' && + t.powerType === 'inhabitant' && + String(t.powerId || '') === String(feedId) && + new Date(t.startAt).getTime() <= atMs && + atMs < new Date(t.endAt).getTime() + ); + } catch (_) { return false; } + } + + async function verdictAuthors({ method, judgeId, medA, medR, dictatorId, openedAt, accuser, respondentId }) { + const out = new Set(); + if (method === 'JUDGE' && judgeId) out.add(String(judgeId)); + else if (method === 'MEDIATION') for (const x of [...ensureArray(medA), ...ensureArray(medR)]) out.add(String(x)); + else if (method === 'DICTATOR' && dictatorId && await isRulingDictator(dictatorId, openedAt)) out.add(String(dictatorId)); + out.delete(String(accuser || '')); + out.delete(String(respondentId || '')); + out.delete(''); + return out; + } + async function getCaseById(caseId) { const id = String(caseId || '').trim(); if (!id) return null; @@ -649,17 +685,22 @@ if (c.type === type) { return map; } - async function voteTally(voteId) { - if (!voteId || !services.votes || !services.votes.getVoteById) return null; + async function voteTally(voteId, parties) { + if (!voteId) return null; try { - const v = await services.votes.getVoteById(voteId); - const vm = v && v.votes ? v.votes : {}; + const ssbClient = await openSsb(); + const msgs = await readTyped(ssbClient, VOTE_LOG_TYPES, { limit: logLimit }); + const r = buildVoteResults(msgs).get(voteId); + if (!r) return null; + const tomb = buildValidatedTombstoneSet(msgs); + if (tomb.has(r.rootId) || tomb.has(voteId)) return null; + if (Array.isArray(parties) && !parties.map(x => String(x || '')).includes(String(r.creator || ''))) return null; + const vm = r.votes || {}; const yes = Number(vm.YES ?? vm.Yes ?? vm.yes ?? 0); const no = Number(vm.NO ?? vm.No ?? vm.no ?? 0); - const sum = Object.values(vm).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const deadline = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); + const total = Number(r.totalVotes || 0); + const deadline = r.deadline || null; + const closed = !!(deadline && moment(deadline).isBefore(moment())); const needed = Math.floor(total / 2) + 1; return { yes, no, total, needed, closed, deadline }; } catch { return null; } @@ -868,8 +909,24 @@ if (c.type === type) { .filter(matchCase) .map(tryDecrypt) .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0)); + const legit = await verdictAuthors({ + method: String(base.method || '').toUpperCase(), + judgeId, + medA: ma, + medR: mr, + dictatorId, + openedAt: base.openedAt || base.createdAt, + accuser: accuserId, + respondentId + }); + const legitVerdictKeys = new Set(); + for (const m of await readLog()) { + const c = m.value?.content; + if (c && c.type === 'courtsVerdict' && legit.has(String(m.value?.author || ''))) legitVerdictKeys.add(m.key || m.id); + } const verdicts = verdictsAll .filter(matchCase) + .filter(v => legitVerdictKeys.has(v.id)) .map(tryDecrypt) .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0)); const verdict = verdicts.length ? verdicts[verdicts.length - 1] : null; @@ -884,7 +941,7 @@ if (c.type === type) { const hasVerdict = !!verdict; const supportsMap = await countSupportsByCase(); const supportCount = supportsMap.get(String(caseRootId)) || supportsMap.get(String(id)) || 0; - const tally = base.voteId ? await voteTally(base.voteId) : null; + const tally = base.voteId ? await voteTally(base.voteId, [accuserId, respondentId]) : null; return { ...base, id, @@ -919,12 +976,7 @@ if (c.type === type) { const ssbKeys = require('../server/node_modules/ssb-keys'); const ssbConfig = require('../server/ssb_config'); const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, arr) => (err ? rej(err) : res(arr))) - ); - }); + const msgs = await readLog(); for (const m of msgs) { const c = m.value?.content; if (!c || c.type !== 'courts-key') continue; diff --git a/src/models/crypto.js b/src/models/crypto.js index 10d7085..65b3dec 100644 --- a/src/models/crypto.js +++ b/src/models/crypto.js @@ -1,7 +1,6 @@ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); -const keylock = require('./keylock'); const SENSITIVE_FIELDS = [ 'title', 'description', 'location', 'price', 'salary', 'options', 'votes', @@ -13,13 +12,17 @@ const SENSITIVE_FIELDS = [ const ENVELOPE_PRESERVE = new Set([ 'type', 'tribeId', 'contentType', 'replaces', 'target', 'author', 'createdAt', 'updatedAt', 'encryptedPayload', - 'mapId', 'calendarId', 'dateId', 'padId', 'roomId', 'parentId', + 'mapId', 'calendarId', 'dateId', 'padId', 'roomId', 'parentId', 'courseId', 'members', 'invites', 'participants', '_decrypted', '_undecryptable' ]); const INVITE_SALT_LEGACY = 'SolarNET.HuB'; -const INVITE_SCRYPT = { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 }; +const INVITE_SCRYPT = { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }; +const INVITE_SCRYPT_LEGACY = { N: 131072, r: 8, p: 1, maxmem: 512 * 1024 * 1024 }; +const INVITE_CACHE_MAX = 512; +const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']); +const INVITE_FAIL_TTL_MS = 10 * 60 * 1000; const FP_INFO = Buffer.from('v1-fp', 'utf8'); const ENVELOPE_TYPE = 'tribe-msg'; @@ -28,7 +31,7 @@ const KEY_DISTRIB_TYPE = 'tribe-keys-distrib'; const KEY_DISTRIB_BATCH = 7; module.exports = (configPath, namespace = 'tribes') => { - const keysDir = path.join(configPath, 'keys'); + const keysDir = require('../configs/state-manager').keysDir(configPath); try { fs.mkdirSync(keysDir, { recursive: true, mode: 0o700 }); } catch (_) {} const keyringPath = path.join(keysDir, `${namespace}-keys.json`); @@ -46,14 +49,7 @@ module.exports = (configPath, namespace = 'tribes') => { const loadKeyring = () => { try { - const raw = fs.readFileSync(keyringPath); - if (keylock.isWrapped(raw)) { - if (!keylock.getKEK()) throw new Error('keyring cifrado pero keylock bloqueado'); - keyring = JSON.parse(keylock.unwrap(raw).toString('utf8')); - } else { - keyring = JSON.parse(raw.toString('utf8')); - if (keylock.getKEK()) { try { saveKeyring(); } catch (_) {} } - } + keyring = JSON.parse(fs.readFileSync(keyringPath, 'utf8')); try { fs.chmodSync(keyringPath, 0o600); } catch (_) {} } catch (e) { if (e.code !== 'ENOENT') throw e; @@ -63,16 +59,15 @@ module.exports = (configPath, namespace = 'tribes') => { }; const saveKeyring = () => { - const json = JSON.stringify(keyring, null, 2); - const kek = keylock.getKEK(); - const out = kek ? keylock.wrap(Buffer.from(json, 'utf8'), kek) : Buffer.from(json, 'utf8'); const tmp = keyringPath + '.tmp.' + process.pid + '.' + Date.now(); - fs.writeFileSync(tmp, out, { mode: 0o600 }); + fs.writeFileSync(tmp, JSON.stringify(keyring, null, 2), { encoding: 'utf8', mode: 0o600 }); fs.renameSync(tmp, keyringPath); try { fs.chmodSync(keyringPath, 0o600); } catch (_) {} }; const generateTribeKey = () => crypto.randomBytes(32).toString('hex'); + const HEX_KEY = /^[0-9a-f]{64}$/i; + const validKey = (k) => typeof k === 'string' && HEX_KEY.test(k); const getKey = (rid) => { const e = keyring[rid]; @@ -92,15 +87,17 @@ module.exports = (configPath, namespace = 'tribes') => { const getAllRootIds = () => Object.keys(keyring); const setKey = (rid, kHex, gen) => { + if (!validKey(kHex)) return; keyring[rid] = { keys: [kHex], gen: gen || 1 }; saveKeyring(); }; const setKeys = (rid, ks, topGen) => { - if (!Array.isArray(ks) || !ks.length) return; + if (!Array.isArray(ks)) return; const seen = new Set(); const dedup = []; - for (const k of ks) if (k && !seen.has(k)) { seen.add(k); dedup.push(k); } + for (const k of ks) if (validKey(k) && !seen.has(k)) { seen.add(k); dedup.push(k); } + if (!dedup.length) return; keyring[rid] = { keys: dedup, gen: topGen || dedup.length }; saveKeyring(); }; @@ -108,8 +105,9 @@ module.exports = (configPath, namespace = 'tribes') => { const mergeKeys = (rid, incoming, topGen) => { const e = keyring[rid] || { keys: [], gen: 0 }; const seen = new Set(e.keys); - const merged = [...e.keys]; - for (const k of incoming) if (k && !seen.has(k)) { seen.add(k); merged.push(k); } + const fresh = []; + for (const k of (Array.isArray(incoming) ? incoming : [])) if (validKey(k) && !seen.has(k)) { seen.add(k); fresh.push(k); } + const merged = [...fresh, ...e.keys]; keyring[rid] = { keys: merged, gen: Math.max(e.gen || 0, topGen || merged.length) }; saveKeyring(); return keyring[rid].gen; @@ -117,6 +115,7 @@ module.exports = (configPath, namespace = 'tribes') => { const addNewKey = (rid, kHex) => { const e = keyring[rid] || { keys: [], gen: 0 }; + if (!validKey(kHex)) return e.gen; if (e.keys.includes(kHex)) return e.gen; e.keys.unshift(kHex); e.gen = (e.gen || 0) + 1; @@ -125,6 +124,27 @@ module.exports = (configPath, namespace = 'tribes') => { return e.gen; }; + const keyPlan = ({ rootId, ownerId, gen, members, messages, leaves }) => { + const stamp = (m) => Number(m.timestamp || (m.value && m.value.timestamp) || 0); + const g = Number(gen) || 0; + let genStart = g > 1 ? null : 0; + const lastGot = new Map(); + for (const m of (Array.isArray(messages) ? messages : [])) { + const c = m && m.value && m.value.content; + if (!c || c.type !== 'tribe-keys' || c.tribeId !== rootId || m.value.author !== ownerId) continue; + if (!c.memberKeys || typeof c.memberKeys !== 'object') continue; + const t = stamp(m); + for (const id of Object.keys(c.memberKeys)) lastGot.set(id, Math.max(lastGot.get(id) || 0, t)); + if (g > 1 && Number(c.generation) === g) genStart = genStart === null ? t : Math.min(genStart, t); + } + if (genStart === null) genStart = 0; + const current = new Set((Array.isArray(members) ? members : []).filter(Boolean)); + const gone = (Array.isArray(leaves) ? leaves : []).filter((l) => l && l.member && l.member !== ownerId); + const rotate = g > 0 && gone.some((l) => !current.has(l.member) && l.ts > genStart); + const missing = rotate ? [] : [...current].filter((id) => id !== ownerId && (!lastGot.has(id) || gone.some((l) => l.member === id && l.ts > lastGot.get(id)))); + return { rotate, missing }; + }; + const dropKey = (rid) => { if (keyring[rid]) { delete keyring[rid]; @@ -132,8 +152,15 @@ module.exports = (configPath, namespace = 'tribes') => { } }; - const fingerprint = (kHex) => - crypto.createHmac('sha256', Buffer.from(kHex, 'hex')).update(FP_INFO).digest('hex').slice(0, 32); + const fingerprints = new Map(); + const fingerprint = (kHex) => { + let fp = fingerprints.get(kHex); + if (!fp) { + fp = crypto.createHmac('sha256', Buffer.from(kHex, 'hex')).update(FP_INFO).digest('hex').slice(0, 32); + fingerprints.set(kHex, fp); + } + return fp; + }; const buildFingerprintIndex = () => { const m = new Map(); @@ -211,9 +238,9 @@ module.exports = (configPath, namespace = 'tribes') => { const generateInviteSalt = () => crypto.randomBytes(16).toString('hex'); - const deriveInviteKey = (code, salt) => { + const deriveInviteKey = (code, salt, params = INVITE_SCRYPT) => { const s = (salt === undefined || salt === null || salt === '') ? INVITE_SALT_LEGACY : salt; - return crypto.scryptSync(code, s, 32, INVITE_SCRYPT); + return crypto.scryptSync(code, s, 32, params); }; const hashInviteCode = (code, salt) => { @@ -229,22 +256,76 @@ module.exports = (configPath, namespace = 'tribes') => { return encryptWithKey(tribeKeyHex, derived.toString('hex'), inviteAad(inviteCode, salt)); }; - const decryptFromInvite = (encryptedKey, inviteCode, salt) => { - const derived = deriveInviteKey(inviteCode, salt); - return decryptWithKey(encryptedKey, derived.toString('hex'), inviteAad(inviteCode, salt)); + const derivedCache = new Map(); + const failedCache = new Map(); + const remember = (map, k, v, max) => { + map.delete(k); + map.set(k, v); + while (map.size > max) map.delete(map.keys().next().value); }; + const derivedKeyName = (code, salt, params) => `${params.N}|${salt == null ? '' : salt}|${code}`; + const openWithDerived = (payload, code, salt, params, aad) => { + const name = derivedKeyName(code, salt, params); + const cached = derivedCache.get(name); + const derived = cached || deriveInviteKey(code, salt, params); + const out = decryptWithKey(payload, derived.toString('hex'), aad); + if (out && !cached) remember(derivedCache, name, derived, INVITE_CACHE_MAX); + return out; + }; + const failureName = (payload, code, salt) => crypto.createHash('sha256').update(`${String(payload)}|${code}|${salt == null ? '' : salt}`).digest('hex'); + const recentlyFailed = (name) => { + const at = failedCache.get(name); + return !!at && Date.now() - at < INVITE_FAIL_TTL_MS; + }; + + const decryptFromInvite = (encryptedKey, inviteCode, salt) => { + const failure = failureName(encryptedKey, inviteCode, salt); + if (recentlyFailed(failure)) throw new Error('Unsupported state or unable to authenticate data'); + const aad = inviteAad(inviteCode, salt); + try { + const out = openWithDerived(encryptedKey, inviteCode, salt, INVITE_SCRYPT, aad); + if (out) return out; + } catch (_) {} + for (let attempt = 0; attempt < 3; attempt++) { + try { + const out = openWithDerived(encryptedKey, inviteCode, salt, INVITE_SCRYPT_LEGACY, aad); + if (out) return out; + } catch (_) {} + } + remember(failedCache, failure, Date.now(), INVITE_CACHE_MAX); + throw new Error('Unsupported state or unable to authenticate data'); + }; + + const INVITE_ENCRYPT_ATTEMPTS = 3; const encryptChainForInvite = (ancestryRootIds, code, salt) => { const chain = ancestryRootIds.map(rid => ({ rootId: rid, keys: getKeys(rid), gen: getGen(rid) })); if (chain.some(e => !Array.isArray(e.keys) || !e.keys.length)) return null; - const k = deriveInviteKey(code, salt); - return encryptWithKey(JSON.stringify(chain), k.toString('hex'), inviteAad(code, salt)); + const plain = JSON.stringify(chain); + const aad = inviteAad(code, salt); + for (let attempt = 0; attempt < INVITE_ENCRYPT_ATTEMPTS; attempt++) { + const k = deriveInviteKey(code, salt); + const payload = encryptWithKey(plain, k.toString('hex'), aad); + if (!payload) continue; + const readBack = decryptChainFromInvite(payload, code, salt); + if (readBack && JSON.stringify(readBack.map(e => ({ rootId: e.rootId, keys: e.keys, gen: e.gen }))) + === JSON.stringify(chain.map(e => ({ rootId: e.rootId, keys: e.keys, gen: e.gen })))) { + return payload; + } + } + throw new Error('Could not produce a verifiable invite on this machine'); }; - const decryptChainFromInvite = (encryptedPayload, code, salt) => { - const k = deriveInviteKey(code, salt); + const decryptChainOnce = (encryptedPayload, code, salt) => { + let json = null + try { + json = openWithDerived(encryptedPayload, code, salt, INVITE_SCRYPT, inviteAad(code, salt)); + } catch (_) { + try { + json = openWithDerived(encryptedPayload, code, salt, INVITE_SCRYPT_LEGACY, inviteAad(code, salt)); + } catch (_) { return null; } + } try { - const json = decryptWithKey(encryptedPayload, k.toString('hex'), inviteAad(code, salt)); const parsed = JSON.parse(json); if (Array.isArray(parsed) && parsed.every(e => e && e.rootId && Array.isArray(e.keys) && e.keys.length)) { return parsed.map(e => ({ @@ -258,6 +339,18 @@ module.exports = (configPath, namespace = 'tribes') => { return null; }; + const decryptChainFromInvite = (encryptedPayload, code, salt, attempts = 1) => { + const failure = failureName(encryptedPayload, code, salt); + if (recentlyFailed(failure)) return null; + const tries = Math.max(1, Number(attempts) || 1); + for (let i = 0; i < tries; i++) { + const chain = decryptChainOnce(encryptedPayload, code, salt); + if (chain) return chain; + } + remember(failedCache, failure, Date.now(), INVITE_CACHE_MAX); + return null; + }; + const inviteMatchesCode = (inv, code) => { if (!inv || typeof inv !== 'object' || !inv.codeHash) return false; return inv.codeHash === hashInviteCode(code, inv.salt); @@ -354,7 +447,9 @@ module.exports = (configPath, namespace = 'tribes') => { const payload = JSON.parse(plaintext); const result = { ...content }; delete result.encryptedPayload; - Object.assign(result, payload); + if (payload && typeof payload === 'object' && !Array.isArray(payload)) { + for (const [k, v] of Object.entries(payload)) if (!UNSAFE_KEYS.has(k) && !(k in envelope)) result[k] = v; + } return result; } catch (_) {} } @@ -523,7 +618,7 @@ module.exports = (configPath, namespace = 'tribes') => { ENVELOPE_TYPE, ENVELOPE_VERSION, KEY_DISTRIB_TYPE, KEY_DISTRIB_BATCH, loadKeyring, saveKeyring, generateTribeKey, getKey, getKeys, getGen, getAllRootIds, - setKey, setKeys, mergeKeys, addNewKey, dropKey, + setKey, setKeys, mergeKeys, addNewKey, dropKey, keyPlan, fingerprint, buildFingerprintIndex, isTribeMsg, wrapMsg, unwrapMsg, encryptWithKey, decryptWithKey, diff --git a/src/models/cv_model.js b/src/models/cv_model.js index 8e66e54..173ac0b 100644 --- a/src/models/cv_model.js +++ b/src/models/cv_model.js @@ -1,8 +1,11 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const CV_TYPES = ['curriculum', 'tombstone']; + const extractBlobId = str => { if (!str || typeof str !== 'string') return null; const match = str.match(/\(([^)]+\.sha256)\)/); @@ -24,7 +27,7 @@ module.exports = ({ cooler }) => { return { type: 'curriculum', - async createCV(data, photoBlobId) { + async createCV(data, photoBlobId, pdfBlobId = null) { const ssbClient = await openSsb(); const userId = ssbClient.id; const content = { @@ -33,6 +36,7 @@ module.exports = ({ cooler }) => { name: data.name, description: data.description, photo: extractBlobId(photoBlobId) || null, + pdf: extractBlobId(pdfBlobId) || null, contact: userId, personalSkills: parseCSV(data.personalSkills), personalExperiences: data.personalExperiences || '', @@ -47,6 +51,8 @@ module.exports = ({ cooler }) => { status: data.status || 'LOOKING FOR WORK', preferences: data.preferences || 'REMOTE WORKING', visibility: String(data.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC', + aiManaged: data.aiManaged === undefined ? true : (data.aiManaged === true || data.aiManaged === '1' || data.aiManaged === 'true'), + matchThreshold: Math.min(100, Math.max(0, parseInt(data.matchThreshold, 10) || 80)), createdAt: new Date().toISOString() }; return new Promise((resolve, reject) => { @@ -54,7 +60,7 @@ module.exports = ({ cooler }) => { }); }, - async updateCV(id, data, photoBlobId) { + async updateCV(id, data, photoBlobId, pdfBlobId = null) { const ssbClient = await openSsb(); const userId = ssbClient.id; @@ -85,7 +91,8 @@ module.exports = ({ cooler }) => { author: userId, name: data.name, description: data.description, - photo: extractBlobId(photoBlobId) || null, + photo: extractBlobId(photoBlobId) || old.content.photo || null, + pdf: extractBlobId(pdfBlobId) || old.content.pdf || null, contact: userId, personalSkills: parseCSV(data.personalSkills), personalExperiences: data.personalExperiences || '', @@ -102,6 +109,12 @@ module.exports = ({ cooler }) => { visibility: data.visibility !== undefined ? (String(data.visibility).toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC') : (old.content.visibility || 'PUBLIC'), + aiManaged: data.aiManaged !== undefined + ? (data.aiManaged === true || data.aiManaged === '1' || data.aiManaged === 'true') + : (old.content.aiManaged !== false), + matchThreshold: data.matchThreshold !== undefined + ? Math.min(100, Math.max(0, parseInt(data.matchThreshold, 10) || 80)) + : (Number(old.content.matchThreshold) || 80), createdAt: old.content.createdAt, updatedAt: new Date().toISOString() }; @@ -143,34 +156,27 @@ module.exports = ({ cooler }) => { const userId = ssbClient.id; const authorId = targetUserId || userId; - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => { - if (err) return reject(err); + const msgs = await readTyped(ssbClient, CV_TYPES, { limit: logLimit }); - const tombstoned = buildValidatedTombstoneSet(msgs); + const tombstoned = buildValidatedTombstoneSet(msgs); - const cvMsgs = msgs - .filter(m => - m.value?.content?.type === 'curriculum' && - m.value.author === authorId && - !tombstoned.has(m.key) - ) - .sort((a, b) => b.value.timestamp - a.value.timestamp); + const cvMsgs = msgs + .filter(m => + m.value?.content?.type === 'curriculum' && + m.value.author === authorId && + !tombstoned.has(m.key) + ) + .sort((a, b) => b.value.timestamp - a.value.timestamp); - if (!cvMsgs.length) { - return resolve(null); - } + if (!cvMsgs.length) { + return null; + } - const latest = cvMsgs[0]; - const c = latest.value.content; - const visibility = String(c.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; - if (visibility === 'HIDDEN' && authorId !== userId) return resolve(null); - resolve({ id: latest.key, ...c, visibility }); - }) - ); - }); + const latest = cvMsgs[0]; + const c = latest.value.content; + const visibility = String(c.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; + if (visibility === 'HIDDEN' && authorId !== userId) return null; + return { id: latest.key, ...c, visibility }; } }; }; diff --git a/src/models/data_model.js b/src/models/data_model.js new file mode 100644 index 0000000..f51ca0b --- /dev/null +++ b/src/models/data_model.js @@ -0,0 +1,496 @@ +const pull = require('../server/node_modules/pull-stream'); +const { getConfig } = require('../configs/config-manager.js'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); +const { isContentVisibleTo } = require('./content_visibility'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); + +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const norm = (v) => String(v == null ? '' : v).trim().toLowerCase(); + +const KINDS = { + inhabitants: { type: 'curriculum', href: (id, c) => `/author/${encodeURIComponent(c.author)}` }, + jobs: { type: 'job', href: (id) => `/jobs/${encodeURIComponent(id)}` }, + projects: { type: 'project', href: (id) => `/projects/${encodeURIComponent(id)}` }, + events: { type: 'event', href: (id) => `/events/${encodeURIComponent(id)}` }, + tribes: { type: 'tribe', href: (id) => `/tribe/${encodeURIComponent(id)}` }, + market: { type: 'market', href: (id) => `/market/${encodeURIComponent(id)}` }, + housing: { type: 'housing', href: (id) => `/housing/${encodeURIComponent(id)}` }, + industry: { type: 'industry', href: (id) => `/industry/${encodeURIComponent(id)}` }, + tasks: { type: 'task', href: (id) => `/tasks/${encodeURIComponent(id)}` }, + reports: { type: 'report', href: (id) => `/reports/${encodeURIComponent(id)}` }, + votes: { type: 'poll', href: (id) => `/polls/${encodeURIComponent(id)}` }, + audios: { type: 'audio', href: (id) => `/audios/${encodeURIComponent(id)}` }, + videos: { type: 'video', href: (id) => `/videos/${encodeURIComponent(id)}` }, + images: { type: 'image', href: (id) => `/images/${encodeURIComponent(id)}` }, + documents: { type: 'document', href: (id) => `/documents/${encodeURIComponent(id)}` }, + bookmarks: { type: 'bookmark', href: (id) => `/bookmarks/${encodeURIComponent(id)}` }, + torrents: { type: 'torrent', href: (id) => `/torrents/${encodeURIComponent(id)}` }, + files: { type: 'file', href: (id) => `/files/${encodeURIComponent(id)}` }, + chats: { type: 'chat', href: (id) => `/chats/${encodeURIComponent(id)}` }, + pads: { type: 'pad', href: (id) => `/pads/${encodeURIComponent(id)}` }, + rooms: { type: 'room', href: (id) => `/rooms/${encodeURIComponent(id)}` }, + maps: { type: 'map', href: (id) => `/maps/${encodeURIComponent(id)}` }, + calendars: { type: 'calendar', href: (id) => `/calendars/${encodeURIComponent(id)}` }, + forum: { type: 'forum', href: (id) => `/forum/${encodeURIComponent(id)}` }, + school: { type: 'schoolCourse', href: (id) => `/school/course/${encodeURIComponent(id)}` }, + wiki: { type: 'wikiPage', href: (id) => `/wiki/${encodeURIComponent(id)}` }, + emergencies: { type: 'emergency', href: (id) => `/emergencies/${encodeURIComponent(id)}` }, + mailing: { type: 'mailingList', href: (id) => `/mailing/${encodeURIComponent(id)}` }, + logistics: { type: 'logisticsRoute', href: (id) => `/logistics/${encodeURIComponent(id)}` }, + podcasts: { type: 'podcast', href: (id) => `/podcasts/${encodeURIComponent(id)}` }, + campaigns: { type: 'campaign', href: (id) => `/campaigns/${encodeURIComponent(id)}` }, + blogs: { type: 'post', href: (id) => `/blogs/${encodeURIComponent(id)}` } +}; + +const isRootBlogPost = (c) => + !!c && c.type === 'post' && typeof c.text === 'string' && + !c.root && !c.fork && !c.branch && !c.about && !c.target && + c.private !== true && !Array.isArray(c.recps); + +const KIND_BY_TYPE = Object.fromEntries(Object.entries(KINDS).map(([k, v]) => [v.type, k])); + +const cvSkills = (c) => [ + ...(c.personalSkills || []), + ...(c.oasisSkills || []), + ...(c.educationalSkills || []), + ...(c.professionalSkills || []) +]; + +const PLACEHOLDERS = new Set(['unknown', 'n/a', 'na', 'none', '-', 'other']); + +const TITLE_STOPWORDS = new Set([ + 'the', 'this', 'that', 'and', 'for', 'with', 'from', 'into', 'about', 'new', 'all', 'not', 'are', 'was', 'you', 'your', 'our', + 'los', 'las', 'del', 'una', 'unos', 'unas', 'este', 'esta', 'esto', 'que', 'con', 'para', 'por', 'sin', 'sobre', 'bajo', 'mas', 'más' +]); + +const titleTerms = (c) => String(c.title || c.name || c.question || c.concept || '') + .toLowerCase() + .split(/[^\w\u0080-\uFFFF]+/) + .filter(w => w.length >= 3 && !TITLE_STOPWORDS.has(w) && !/^\d+$/.test(w)); + +const termsOf = (kind, c) => { + const out = []; + if (kind === 'inhabitants') { + out.push(...cvSkills(c)); + if (c.languages) out.push(...String(c.languages).split(/[,;]/)); + } else if (kind === 'jobs') { + out.push(...(c.tasks || []), ...(c.tags || []), c.job_type, c.location); + } else if (kind === 'industry') { + out.push(...(c.tags || []), c.sector, ...(c.skills || [])); + } else { + out.push(...(c.tags || [])); + if (c.category) out.push(c.category); + } + return Array.from(new Set(out.map(norm).filter(t => t && !PLACEHOLDERS.has(t)))); +}; + +const DESC_TERMS_MAX = 40; +const descTerms = (c) => String(c.description || c.text || c.body || '') + .toLowerCase() + .split(/[^\w\u0080-\uFFFF]+/) + .filter(w => w.length >= 4 && !TITLE_STOPWORDS.has(w) && !/^\d+$/.test(w)) + .slice(0, DESC_TERMS_MAX); + +const FAV_KIND = Object.fromEntries(Object.keys(KINDS).map(k => [k, k === 'votes' ? 'polls' : k])); + +const ALIKE_MIN_AFFINITY = 0.3; +const RATED_MIN_OPINIONS = 3; +const REASON_ORDER = ['related', 'content', 'mutual', 'following', 'supportsYou', 'tribe', 'alike', 'cv', 'pinned', 'rated', 'near']; + +const opinionsTotalOf = (c) => { + const ops = c && typeof c.opinions === 'object' && c.opinions ? c.opinions : {}; + let total = 0; + for (const v of Object.values(ops)) total += Number(v) || 0; + return total; +}; + +const excerpt = (s) => String(s || '').replace(/!?\[[^\]]*\]\([^)]*\)/g, ' ').replace(/[#*_>`~|]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 90); +const titleOf = (kind, c, author) => { + if (kind === 'inhabitants') return c.name || ''; + return c.title || c.name || c.question || c.concept || c.subject || c.contentWarning || excerpt(typeof c.text === 'string' ? c.text : '') || excerpt(typeof c.description === 'string' ? c.description : '') || ''; +}; + +const jaccard = (a, b) => { + if (!a.size || !b.size) return { score: 0, common: [] }; + const common = [...b].filter(x => a.has(x)); + const union = a.size + b.size - common.length; + return { score: union > 0 ? common.length / union : 0, common }; +}; + +module.exports = ({ cooler, favoriteIdsFor = null }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const DATA_TYPES = [...Object.values(KINDS).map(v => v.type), 'tombstone', 'contact', 'about']; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, DATA_TYPES, { limit: logLimit }); + + const buildGraph = async () => { + const ssbClient = await openSsb(); + const viewerId = ssbClient.id; + const messages = await getAllMessages(ssbClient); + const tomb = buildValidatedTombstoneSet(messages); + + const candidates = new Map(); + const versions = []; + const follows = new Map(); + const followers = new Map(); + const aboutTerms = new Set(); + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object' || !c.type) continue; + if (c.type === 'contact') { + if (typeof c.contact !== 'string' || typeof c.following !== 'boolean') continue; + if (v.author === viewerId) follows.set(c.contact, c.following); + else if (c.contact === viewerId) followers.set(v.author, c.following); + continue; + } + if (c.type === 'about') { + if (v.author === viewerId && c.about === viewerId && typeof c.description === 'string') for (const t of descTerms(c)) aboutTerms.add(norm(t)); + continue; + } + if (c.tribeId && c.type !== 'tribe') continue; + if (c.type === 'post' && !isRootBlogPost(c)) continue; + const kind = KIND_BY_TYPE[c.type]; + if (!kind) continue; + versions.push(versionEntry(m)); + if (tomb.has(m.key)) continue; + if (!isContentVisibleTo(c.type, c, viewerId, v.author)) continue; + candidates.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, kind, c }); + } + + const vg = versionGroups(versions); + const latestByRoot = new Map(); + for (const root of vg.roots) { + const tip = candidates.get(vg.tipOf(root)); + if (!tip) continue; + if (tip.kind !== 'tribes') { latestByRoot.set(root, { ...tip, key: root }); continue; } + const members = new Set(Array.isArray(tip.c.members) ? tip.c.members : []); + const lastByAuthor = new Map(); + for (const k of vg.groups.get(root)) { + const n = candidates.get(k); + if (!n || n.author === tip.author || n.ts < tip.ts) continue; + const prev = lastByAuthor.get(n.author); + if (!prev || n.ts >= prev.ts) lastByAuthor.set(n.author, n); + } + for (const [a, n] of lastByAuthor) { if (Array.isArray(n.c.members) && n.c.members.includes(a)) members.add(a); else members.delete(a); } + latestByRoot.set(root, { ...tip, key: root, c: { ...tip.c, members: [...members] } }); + } + + const nodes = []; + const byAuthorCv = new Map(); + for (const node of latestByRoot.values()) { + const coreTerms = termsOf(node.kind, node.c); + const extra = node.kind === 'inhabitants' ? [] : [...titleTerms(node.c), ...descTerms(node.c)].map(norm).filter(t => t && !PLACEHOLDERS.has(t)); + const terms = Array.from(new Set([...coreTerms, ...extra])); + if (!terms.length) continue; + const entry = { + id: node.key, + kind: node.kind, + ctype: KINDS[node.kind] ? KINDS[node.kind].type : '', + author: node.author, + title: titleOf(node.kind, node.c, node.author), + terms, + termSet: new Set(terms), + coreTermSet: new Set(coreTerms), + ts: node.ts, + createdAt: node.c.createdAt || new Date(node.ts).toISOString(), + href: KINDS[node.kind].href(node.key, node.c), + members: node.kind === 'tribes' && Array.isArray(node.c.members) ? node.c.members : null, + location: norm(node.c.location || ''), + opinionsTotal: opinionsTotalOf(node.c) + }; + if (node.kind === 'inhabitants') { + const prev = byAuthorCv.get(entry.author); + if (prev && prev.ts >= entry.ts) continue; + byAuthorCv.set(entry.author, entry); + continue; + } + nodes.push(entry); + } + for (const cv of byAuthorCv.values()) nodes.push(cv); + + const following = new Set([...follows.entries()].filter(([, on]) => on).map(([id]) => id)); + const supporters = new Set([...followers.entries()].filter(([, on]) => on).map(([id]) => id)); + return { viewerId, nodes, cvByAuthor: byAuthorCv, following, supporters, aboutTerms }; + }; + + const favoriteIds = async () => { + if (typeof favoriteIdsFor !== 'function') return new Set(); + const out = new Set(); + await Promise.all(Object.values(FAV_KIND).map(async (k) => { + try { for (const id of await favoriteIdsFor(k)) out.add(String(id)); } catch (_) {} + })); + return out; + }; + + const diversify = (items) => { + const leaders = new Map(); + for (const it of items) if (!leaders.has(it.kind)) leaders.set(it.kind, it); + const lead = new Set(leaders.values()); + return [...leaders.values()].sort((x, y) => y.score - x.score || y.ts - x.ts) + .concat(items.filter(it => !lead.has(it))); + }; + + const MAX_ENTITIES = 400; + const MAX_PAIRS = 300; + + const strip = (n) => ({ + id: n.id, kind: n.kind, ctype: n.ctype, author: n.author, title: n.title, + href: n.href, createdAt: n.createdAt, ts: n.ts + }); + + const buildProfile = async (graph) => { + const { viewerId, nodes, cvByAuthor, aboutTerms } = graph; + const use = nodes.slice(0, MAX_ENTITIES); + const favs = await favoriteIds(); + + const myTermSet = new Set(); + const cvTerms = new Set(); + const contentTerms = new Set(); + const mineCv = cvByAuthor.get(viewerId); + if (mineCv) for (const t of mineCv.terms) { myTermSet.add(t); cvTerms.add(t); } + for (const t of aboutTerms) if (t && !PLACEHOLDERS.has(t)) myTermSet.add(t); + const tribeMates = new Set(); + const pinnedTerms = new Set(); + let myLocation = mineCv ? mineCv.location : ''; + for (const n of use) { + if (String(n.author) === String(viewerId)) { + for (const t of n.terms) { myTermSet.add(t); contentTerms.add(t); } + if (!myLocation && n.location) myLocation = n.location; + } + if (favs.has(String(n.id))) for (const t of n.terms) { myTermSet.add(t); pinnedTerms.add(t); } + if (n.members && (String(n.author) === String(viewerId) || n.members.includes(viewerId))) for (const m of n.members) tribeMates.add(m); + } + tribeMates.delete(viewerId); + + const df = new Map(); + for (const n of use) for (const t of n.termSet) df.set(t, (df.get(t) || 0) + 1); + const total = use.length || 1; + const weightOf = (t) => Math.log(1 + total / (df.get(t) || 1)); + return { use, myTermSet, cvTerms, contentTerms, pinnedTerms, tribeMates, myLocation, df, weightOf, total }; + }; + + const affinitiesFor = (viewerId, { use, myTermSet, pinnedTerms, tribeMates, weightOf }) => { + const termsByAuthor = new Map(); + for (const n of use) { + if (String(n.author) === String(viewerId)) continue; + if (!termsByAuthor.has(n.author)) termsByAuthor.set(n.author, new Set()); + const set = termsByAuthor.get(n.author); + for (const t of n.termSet) set.add(t); + } + let myW = 0; + for (const t of myTermSet) myW += weightOf(t); + const byAuthor = new Map(); + for (const [author, terms] of termsByAuthor) { + const common = [...terms].filter(t => myTermSet.has(t)); + let commonW = 0; + for (const t of common) commonW += weightOf(t); + let theirW = 0; + for (const t of terms) theirW += weightOf(t); + const floor = Math.min(myW, theirW); + const score = floor > 0 ? Math.min(1, commonW / floor) : 0; + common.sort((x, y) => weightOf(y) - weightOf(x) || x.localeCompare(y)); + const reasons = []; + if (tribeMates.has(String(author))) reasons.push('tribe'); + if (common.some(t => pinnedTerms.has(t))) reasons.push('pinned'); + byAuthor.set(author, { score, common, reasons }); + } + for (const m of tribeMates) if (!byAuthor.has(m)) byAuthor.set(m, { score: 0, common: [], reasons: ['tribe'] }); + return byAuthor; + }; + + return { + KINDS: Object.keys(KINDS), + + async authorAffinities() { + const graph = await buildGraph(); + const profile = await buildProfile(graph); + return { viewerId: graph.viewerId, byAuthor: affinitiesFor(graph.viewerId, profile), hasProfile: profile.myTermSet.size > 0 }; + }, + + async listMatches(filter = 'ALL', opts = {}) { + const graph = await buildGraph(); + const { viewerId, following, supporters } = graph; + const profile = await buildProfile(graph); + const { use, myTermSet, cvTerms, contentTerms, pinnedTerms, tribeMates, myLocation, df, weightOf, total } = profile; + const affinity = affinitiesFor(viewerId, profile); + const f = String(filter || 'ALL').toUpperCase(); + + const expanded = new Set(); + for (const n of use) { + if (String(n.author) === String(viewerId)) continue; + if (![...n.termSet].some(t => myTermSet.has(t))) continue; + for (const t of n.termSet) if (!myTermSet.has(t)) expanded.add(t); + } + + let out = []; + for (const n of use) { + if (!myTermSet.size) break; + if (String(n.author) === String(viewerId)) continue; + const common = [...n.termSet].filter(t => myTermSet.has(t)); + let commonW = 0; + for (const t of common) commonW += weightOf(t); + let indirectW = 0; + for (const t of n.termSet) if (!myTermSet.has(t) && expanded.has(t)) indirectW += weightOf(t); + let itemW = 0; + for (const t of n.termSet) itemW += weightOf(t); + const direct = itemW > 0 ? Math.min(1, commonW / itemW) : 0; + const indirect = itemW > 0 ? Math.min(1, indirectW / itemW) : 0; + let dfSum = 0; + for (const t of n.termSet) dfSum += (df.get(t) || 1) / total; + const connectivity = n.termSet.size ? dfSum / n.termSet.size : 0; + const base = 0.8 * direct + 0.15 * indirect + 0.05 * connectivity; + const ageDays = Math.max(0, (Date.now() - (n.ts || 0)) / 86400000); + const recency = 0.8 + 0.2 * Math.exp(-ageDays / 60); + const social = (following.has(String(n.author)) ? 0.1 : 0) + (tribeMates.has(String(n.author)) ? 0.05 : 0); + const score = Math.min(1, base * recency + social); + common.sort((x, y) => weightOf(y) - weightOf(x) || x.localeCompare(y)); + const author = String(n.author); + const flags = new Set(); + if (!common.length && indirect > 0) flags.add('related'); + if (common.some(t => contentTerms.has(t))) flags.add('content'); + if (following.has(author) && supporters.has(author)) flags.add('mutual'); + else if (following.has(author)) flags.add('following'); + else if (supporters.has(author)) flags.add('supportsYou'); + if (tribeMates.has(author)) flags.add('tribe'); + const aff = affinity.get(n.author); + if (aff && aff.score >= ALIKE_MIN_AFFINITY) flags.add('alike'); + if (common.some(t => cvTerms.has(t))) flags.add('cv'); + if (common.some(t => pinnedTerms.has(t))) flags.add('pinned'); + if (n.opinionsTotal >= RATED_MIN_OPINIONS) flags.add('rated'); + if (myLocation && n.location && n.location === myLocation) flags.add('near'); + const reasons = REASON_ORDER.filter(r => flags.has(r)); + out.push({ ...strip(n), score, common, connections: common.length, reasons }); + } + + if (opts.hidden && (opts.hidden.authors || opts.hidden.ids)) { + const ha = opts.hidden.authors instanceof Set ? opts.hidden.authors : new Set(); + const hi = opts.hidden.ids instanceof Set ? opts.hidden.ids : new Set(); + out = out.filter(s => !ha.has(String(s.author)) && !hi.has(String(s.id))); + } + const anyMatches = out.length > 0; + const kindsAvail = {}; + for (const n of use) { + if (String(n.author) !== String(viewerId)) kindsAvail[n.kind] = true; + } + + const kindsMatched = {}; + for (const s of out) kindsMatched[s.kind] = true; + + if (f === 'RECENT') { + const cutoff = Date.now() - 24 * 60 * 60 * 1000; + out = out.filter(s => s.ts >= cutoff); + } else if (f !== 'ALL' && f !== 'TOP') { + const kind = f.toLowerCase(); + if (KINDS[kind]) out = out.filter(s => s.kind === kind); + } + + const q = norm(opts.q); + if (q) out = out.filter(s => norm(s.title).includes(q) || s.common.some(t => t.includes(q))); + + const present = new Set(); + for (const s of out) for (const r of s.reasons) present.add(r); + const reasonsAvail = REASON_ORDER.filter(r => present.has(r)); + const reason = String(opts.reason || '').trim(); + if (reason && REASON_ORDER.includes(reason)) out = out.filter(s => s.reasons.includes(reason)); + + if (f === 'RECENT') out.sort((x, y) => y.ts - x.ts || y.score - x.score); + else out.sort((x, y) => y.score - x.score || y.ts - x.ts); + if (f === 'ALL' || f === 'TOP') out = diversify(out); + + return { + matches: out.slice(0, MAX_PAIRS), + total: out.length, + hasProfile: myTermSet.size > 0, + myTerms: [...myTermSet], + kindsAvail, + kindsMatched, + anyMatches, + reasonsAvail, + reason: reasonsAvail.includes(reason) ? reason : '' + }; + }, + + async jobMatchesFor(viewerId, { minScore = 0.8 } = {}) { + const { nodes, cvByAuthor } = await buildGraph(); + const mine = cvByAuthor.get(viewerId); + if (!mine) return []; + return nodes + .filter(n => n.kind === 'jobs' && String(n.author) !== String(viewerId)) + .map(n => { + const { score, common } = jaccard(mine.termSet, n.coreTermSet || n.termSet); + return { id: n.id, title: n.title, author: n.author, href: n.href, score, common }; + }) + .filter(m => m.score >= minScore) + .sort((a, b) => b.score - a.score); + }, + + async cohesion() { + const { cvByAuthor, nodes } = await buildGraph(); + const use = nodes.slice(0, MAX_ENTITIES); + let comparisons = 0; + let sum = 0; + let connectedPairs = 0; + const linkedEntities = new Set(); + for (let i = 0; i < use.length; i++) { + for (let j = i + 1; j < use.length; j++) { + const a = use[i], b = use[j]; + if (a.kind === 'inhabitants' && b.kind === 'inhabitants' && a.author === b.author) continue; + const { score } = jaccard(a.termSet, b.termSet); + comparisons += 1; + sum += score; + if (score > 0) { connectedPairs += 1; linkedEntities.add(a.id); linkedEntities.add(b.id); } + } + } + const coefficient = comparisons > 0 ? sum / comparisons : 0; + + const people = [...cvByAuthor.values()]; + const linked = new Set(); + let cvComparisons = 0; + let cvSum = 0; + for (let i = 0; i < people.length; i++) { + for (let j = i + 1; j < people.length; j++) { + const { score } = jaccard(people[i].termSet, people[j].termSet); + cvComparisons += 1; + cvSum += score; + if (score > 0) { linked.add(people[i].author); linked.add(people[j].author); } + } + } + const cvCoefficient = cvComparisons > 0 ? cvSum / cvComparisons : 0; + + const skillSet = new Set(); + for (const p of people) for (const t of p.terms) skillSet.add(t); + + const termCount = new Map(); + for (const n of nodes) for (const t of n.terms) termCount.set(t, (termCount.get(t) || 0) + 1); + const topTerms = [...termCount.entries()] + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .slice(0, 12) + .map(([term, count]) => ({ term, count })); + + const perKind = {}; + for (const n of nodes) perKind[n.kind] = (perKind[n.kind] || 0) + 1; + + return { + coefficient, + percent: Math.round(coefficient * 1000) / 10, + comparisons, + pairs: connectedPairs, + entities: use.length, + distinctTerms: termCount.size, + topTerms, + perKind, + people: people.length, + skills: skillSet.size, + connected: linkedEntities.size, + isolated: Math.max(0, use.length - linkedEntities.size), + cvCoefficient, + cvPercent: Math.round(cvCoefficient * 1000) / 10 + }; + } + }; +}; diff --git a/src/models/dev_model.js b/src/models/dev_model.js new file mode 100644 index 0000000..8b18406 --- /dev/null +++ b/src/models/dev_model.js @@ -0,0 +1,340 @@ +const fs = require('fs'); +const path = require('path'); +const { getConfig } = require('../configs/config-manager.js'); + +const ROOT = path.resolve(__dirname, '..', '..'); + +const ALLOWED_ROOTS = ['src', 'docs', 'scripts', 'test']; +const ALLOWED_ROOT_FILES = ['README.md', 'LICENSE', 'install.sh', 'oasis.sh']; +const DENIED_SEGMENTS = new Set(['node_modules', '.git', '.ssb', 'packages', 'configs', 'embeddings', 'tiles', 'cache', 'results']); +const DENIED_NAMES = new Set(['secret', '.env', '.npmrc', 'package-lock.json']); +const DENIED_EXTENSIONS = new Set(['.gguf', '.jks', '.keystore', '.enc', '.pem', '.key', '.p12', '.crt']); +const VIEWABLE_EXTENSIONS = new Set(['.js', '.mjs', '.json', '.css', '.md', '.sh', '.html', '.txt', '.yml', '.yaml']); + +const STATS_TTL = 60 * 1000; +let statsCache = null; +let statsCacheAt = 0; + +const MAX_VIEW_BYTES = 2 * 1024 * 1024; +const PAGE_LINES = 500; +const SEARCH_MAX = 200; +const SEARCH_SNIPPET = 200; + +const extOf = (name) => { + const i = String(name || '').lastIndexOf('.'); + return i > 0 ? String(name).slice(i).toLowerCase() : ''; +}; + +const isDeniedEntry = (name) => { + const n = String(name || ''); + if (!n || n === '.' || n === '..') return true; + if (n.toLowerCase().startsWith('.git')) return true; + if (DENIED_SEGMENTS.has(n)) return true; + if (DENIED_NAMES.has(n)) return true; + if (DENIED_EXTENSIONS.has(extOf(n))) return true; + return false; +}; + +const PLAIN_TEXT_NAME = /^[A-Z][A-Z0-9_-]*$/; + +const isViewable = (name) => (VIEWABLE_EXTENSIONS.has(extOf(name)) || PLAIN_TEXT_NAME.test(String(name || ''))) && !isDeniedEntry(name); + +function realRoot() { + try { return fs.realpathSync(ROOT); } catch (_) { return ROOT; } +} + +function splitPath(rel) { + const raw = String(rel || ''); + for (let i = 0; i < raw.length; i++) { if (raw.charCodeAt(i) < 32) throw new Error('Invalid path'); } + return raw.replace(/^[/\\]+/, '').split(/[/\\]+/).filter(s => s && s !== '.'); +} + +function resolveSafe(rel) { + const segments = splitPath(rel); + if (segments.some(s => s === '..')) throw new Error('Invalid path'); + if (segments.some(isDeniedEntry)) throw new Error('Path not available'); + if (segments.length) { + const head = segments[0]; + const isRootFile = segments.length === 1 && ALLOWED_ROOT_FILES.includes(head); + if (!ALLOWED_ROOTS.includes(head) && !isRootFile) throw new Error('Path not available'); + } + const target = path.resolve(ROOT, segments.join(path.sep)); + let real; + try { real = fs.realpathSync(target); } catch (_) { throw new Error('Not found'); } + const base = realRoot(); + if (real !== base && !real.startsWith(base + path.sep)) throw new Error('Path not available'); + return { abs: real, rel: segments.join('/'), segments }; +} + +function parentsOf(segments) { + const out = []; + let acc = ''; + for (const s of segments) { + acc = acc ? `${acc}/${s}` : s; + out.push({ name: s, path: acc }); + } + return out; +} + +function rootListing() { + const dirs = []; + const files = []; + for (const name of ALLOWED_ROOTS) { + const abs = path.join(ROOT, name); + if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) dirs.push({ name, path: name }); + } + for (const name of ALLOWED_ROOT_FILES) { + const abs = path.join(ROOT, name); + if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue; + files.push({ name, path: name, size: fs.statSync(abs).size, ext: extOf(name), viewable: isViewable(name) }); + } + return { path: '', parents: [], dirs, files, hidden: 0 }; +} + +function walkFiles(relDir, onFile) { + let entries; + try { entries = fs.readdirSync(path.resolve(ROOT, relDir), { withFileTypes: true }); } catch (_) { return true; } + for (const e of entries) { + if (isDeniedEntry(e.name)) continue; + const rel = relDir ? `${relDir}/${e.name}` : e.name; + if (e.isSymbolicLink()) continue; + if (e.isDirectory()) { if (walkFiles(rel, onFile) === false) return false; continue; } + if (!e.isFile()) continue; + if (onFile(rel, e.name) === false) return false; + } + return true; +} + +const MODULE_ALIASES = { + ai: { model: 'src/AI/ai_service.mjs', view: 'src/views/AI_view.js' }, + aiNav: { model: 'src/AI/routes_index.js', view: 'src/views/AI_view.js', paths: ['/ai/ask'] }, + docs: { paths: ['/documents'] }, + blogs: { model: 'src/models/blog_model.js', view: 'src/views/blog_view.js', paths: ['/blogs'] }, + polls: { model: 'src/models/polls_model.js', view: 'src/views/polls_view.js', paths: ['/polls'] }, + data: { model: 'src/models/data_model.js', view: 'src/views/data_view.js', paths: ['/data'] }, + invites: { model: 'src/models/main_models.js', view: 'src/views/invites_view.js' }, + graphos: { model: 'src/models/main_models.js', view: 'src/views/graphos_view.js' } +}; + +const GROUPS = { + translations: ['src/client/assets/translations'], + styles: ['src/client/assets/styles', 'src/client/assets/themes'], + tests: ['test'] +}; + +module.exports = { + ROOT, + PAGE_LINES, + GROUPS, + + listGroup(key) { + const roots = GROUPS[String(key || '')]; + if (!roots) throw new Error('Not found'); + const dirs = []; + const files = []; + for (const rel of roots) { + let listing; + try { listing = module.exports.listTree(rel); } catch (_) { continue; } + dirs.push(...listing.dirs); + files.push(...listing.files); + } + dirs.sort((a, b) => a.name.localeCompare(b.name)); + files.sort((a, b) => a.name.localeCompare(b.name)); + return { group: String(key), path: '', parents: [], dirs, files, hidden: 0 }; + }, + + listTree(relPath) { + const segments = splitPath(relPath); + if (!segments.length) return rootListing(); + const target = resolveSafe(relPath); + const stat = fs.statSync(target.abs); + if (!stat.isDirectory()) throw new Error('Not a directory'); + const entries = fs.readdirSync(target.abs, { withFileTypes: true }); + const dirs = []; + const files = []; + let hidden = 0; + for (const e of entries) { + if (isDeniedEntry(e.name) || e.isSymbolicLink()) { hidden++; continue; } + const rel = `${target.rel}/${e.name}`; + if (e.isDirectory()) { dirs.push({ name: e.name, path: rel }); continue; } + if (!e.isFile()) { hidden++; continue; } + let size = 0; + try { size = fs.statSync(path.join(target.abs, e.name)).size; } catch (_) { size = 0; } + files.push({ name: e.name, path: rel, size, ext: extOf(e.name), viewable: isViewable(e.name) }); + } + dirs.sort((a, b) => a.name.localeCompare(b.name)); + files.sort((a, b) => a.name.localeCompare(b.name)); + return { path: target.rel, parents: parentsOf(target.segments), dirs, files, hidden }; + }, + + readFile(relPath, opts = {}) { + const target = resolveSafe(relPath); + const stat = fs.statSync(target.abs); + if (!stat.isFile()) throw new Error('Not a file'); + if (!isViewable(path.basename(target.abs))) throw new Error('This file type cannot be displayed'); + if (stat.size > MAX_VIEW_BYTES) throw new Error('This file is too large to be displayed'); + const all = fs.readFileSync(target.abs, 'utf8').split('\n'); + const totalLines = all.length; + const requested = parseInt(opts.from, 10); + const from = Math.min(Math.max(Number.isFinite(requested) ? requested : 1, 1), Math.max(1, totalLines)); + const to = Math.min(from + PAGE_LINES - 1, totalLines); + const lines = []; + for (let n = from; n <= to; n++) lines.push({ n, text: all[n - 1] }); + return { + path: target.rel, + parents: parentsOf(target.segments), + name: path.basename(target.abs), + size: stat.size, + totalLines, + from, + to, + pageLines: PAGE_LINES, + hasPrev: from > 1, + hasNext: to < totalLines, + lines + }; + }, + + rawFile(relPath) { + const target = resolveSafe(relPath); + const stat = fs.statSync(target.abs); + if (!stat.isFile()) throw new Error('Not a file'); + if (!isViewable(path.basename(target.abs))) throw new Error('This file type cannot be displayed'); + if (stat.size > MAX_VIEW_BYTES) throw new Error('This file is too large to be displayed'); + return { name: path.basename(target.abs), content: fs.readFileSync(target.abs, 'utf8') }; + }, + + searchCode(query, opts = {}) { + const q = String(query || '').trim(); + if (q.length < 2) return { query: q, results: [], truncated: false, scanned: 0 }; + const wanted = String(opts.ext || '').trim().toLowerCase(); + const max = Math.min(Math.max(parseInt(opts.max, 10) || SEARCH_MAX, 1), SEARCH_MAX); + const needle = q.toLowerCase(); + const results = []; + let scanned = 0; + let truncated = false; + for (const root of ALLOWED_ROOTS) { + if (truncated) break; + walkFiles(root, (rel, name) => { + if (!isViewable(name)) return true; + if (wanted && extOf(name) !== wanted) return true; + let stat; + try { stat = fs.statSync(path.resolve(ROOT, rel)); } catch (_) { return true; } + if (stat.size > MAX_VIEW_BYTES) return true; + scanned++; + let content; + try { content = fs.readFileSync(path.resolve(ROOT, rel), 'utf8'); } catch (_) { return true; } + if (content.toLowerCase().indexOf(needle) < 0) return true; + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (lines[i].toLowerCase().indexOf(needle) < 0) continue; + results.push({ path: rel, line: i + 1, text: lines[i].trim().slice(0, SEARCH_SNIPPET) }); + if (results.length >= max) { truncated = true; return false; } + } + return true; + }); + } + return { query: q, results, truncated, scanned }; + }, + + moduleMap() { + const cfg = getConfig(); + const mods = Object.keys((cfg && cfg.modules) || {}).map(k => k.replace(/Mod$/, '')).sort(); + let backend = ''; + try { backend = fs.readFileSync(path.join(ROOT, 'src', 'backend', 'backend.js'), 'utf8'); } catch (_) { backend = ''; } + const modelFiles = fs.existsSync(path.join(ROOT, 'src', 'models')) ? fs.readdirSync(path.join(ROOT, 'src', 'models')) : []; + const viewFiles = fs.existsSync(path.join(ROOT, 'src', 'views')) ? fs.readdirSync(path.join(ROOT, 'src', 'views')) : []; + const stem = (s) => String(s || '').toLowerCase().replace(/s$/, ''); + const pick = (files, name, suffixes) => { + for (const suf of suffixes) { + const exact = files.find(f => f.toLowerCase() === `${name.toLowerCase()}${suf}`); + if (exact) return exact; + } + const singular = name.replace(/s$/, ''); + for (const suf of suffixes) { + const short = files.find(f => f.toLowerCase() === `${singular.toLowerCase()}${suf}`); + if (short) return short; + } + const target = stem(name); + return files.find(f => { + const suf = suffixes.find(x => f.toLowerCase().endsWith(x)); + if (!suf) return false; + const base = stem(f.slice(0, f.length - suf.length)); + return base.startsWith(target) || target.startsWith(base); + }) || null; + }; + const testsIndex = new Map(); + const modsDir = path.join(ROOT, 'test', 'mods'); + const indexTests = (dir, prefix) => { + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; } + for (const e of entries) { + if (!e.isDirectory()) continue; + const rel = `${prefix}/${e.name}`; + if (!testsIndex.has(e.name)) testsIndex.set(e.name, rel); + indexTests(path.join(dir, e.name), rel); + } + }; + indexTests(modsDir, 'test/mods'); + return mods.map(name => { + const alias = MODULE_ALIASES[name] || {}; + const routePaths = Array.isArray(alias.paths) && alias.paths.length ? alias.paths : [`/${name}`]; + let routes = 0; + for (const routePath of routePaths) { + const escaped = routePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const re = new RegExp(`\\.(get|post)\\(\\s*["'\`]${escaped}(?=["'/?])`, 'g'); + routes += backend ? (backend.match(re) || []).length : 0; + } + const model = alias.model !== undefined ? alias.model : (pick(modelFiles, name, ['_model.js', '_models.js']) ? `src/models/${pick(modelFiles, name, ['_model.js', '_models.js'])}` : null); + const view = alias.view !== undefined ? alias.view : (pick(viewFiles, name, ['_view.js', '_views.js']) ? `src/views/${pick(viewFiles, name, ['_view.js', '_views.js'])}` : null); + return { + name, + enabled: (cfg.modules || {})[`${name}Mod`] !== 'off', + model, + view, + routes, + tests: testsIndex.get(name) || testsIndex.get(name.replace(/s$/, '')) || (() => { + const target = stem(name); + for (const [dir, rel] of testsIndex) { + const base = stem(dir); + if (base.startsWith(target) || target.startsWith(base)) return rel; + } + return null; + })() + }; + }); + }, + + projectStats() { + const now = Date.now(); + if (statsCache && now - statsCacheAt < STATS_TTL) return statsCache; + let files = 0; + let lines = 0; + let bytes = 0; + for (const root of ALLOWED_ROOTS) { + walkFiles(root, (rel, name) => { + if (!isViewable(name)) return true; + try { + const abs = path.resolve(ROOT, rel); + const stat = fs.statSync(abs); + if (stat.size > MAX_VIEW_BYTES) return true; + files++; + bytes += stat.size; + lines += fs.readFileSync(abs, 'utf8').split('\n').length; + } catch (_) {} + return true; + }); + } + const cfg = getConfig(); + const modules = Object.keys((cfg && cfg.modules) || {}).length; + const translations = path.join(ROOT, 'src', 'client', 'assets', 'translations'); + const languages = fs.existsSync(translations) ? fs.readdirSync(translations).filter(f => /^oasis_[a-z]{2}\.js$/.test(f)).length : 0; + const testsDir = path.join(ROOT, 'test', 'mods'); + const testMods = fs.existsSync(testsDir) ? fs.readdirSync(testsDir).length : 0; + let version = ''; + try { version = String(JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'server', 'package.json'), 'utf8')).version || ''); } catch (_) { version = ''; } + statsCache = { files, lines, bytes, modules, languages, testMods, version, node: process.version }; + statsCacheAt = now; + return statsCache; + } +}; diff --git a/src/models/documents_model.js b/src/models/documents_model.js index ada7c64..b044b9d 100644 --- a/src/models/documents_model.js +++ b/src/models/documents_model.js @@ -2,8 +2,9 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); -const mediaFavorites = require("../backend/media-favorites"); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require("./typed_log"); +const contentFavorites = require("../backend/content_favorites"); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -34,20 +35,13 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const DOCUMENT_TYPES = ["document", "documentOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, DOCUMENT_TYPES, { limit: logLimit }); const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -63,50 +57,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, t] of Array.from(parent.entries())) { - const orig = nodes.get(t); - const node = nodes.get(k); - if (orig && node && orig.author === node.author) continue; - parent.delete(k); - if (child.get(t) === k) child.delete(t); - if (orig) nodes.delete(k); - } - - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -123,9 +83,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const pickDoc = (node, rootId, agg) => { @@ -140,6 +100,7 @@ module.exports = ({ cooler }) => { author: node.author, title: c.title || "", description: c.description || "", + license: c.license || "", opinions: agg ? agg.opinions : (c.opinions || {}), opinions_inhabitants: agg ? agg.voters : safeArr(c.opinions_inhabitants), sizeBytes: node.sizeBytes || 0 @@ -154,7 +115,7 @@ module.exports = ({ cooler }) => { const favoritesSetForDocuments = async () => { try { - return await mediaFavorites.getFavoriteSet("documents"); + return await contentFavorites.getFavoriteSet("documents"); } catch { return new Set(); } @@ -178,16 +139,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Document not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Document not found"); return root; }, - async createDocument(blobMarkdown, tagsRaw, title, description) { + async createDocument(blobMarkdown, tagsRaw, title, description, license) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); if (!blobId) throw new Error("Missing document blob"); @@ -203,7 +160,8 @@ module.exports = ({ cooler }) => { title: title || "", description: description || "", opinions: {}, - opinions_inhabitants: [] + opinions_inhabitants: [], + ...(license ? { license } : {}) }; return new Promise((resolve, reject) => { @@ -211,7 +169,7 @@ module.exports = ({ cooler }) => { }); }, - async updateDocumentById(id, blobMarkdown, tagsRaw, title, description) { + async updateDocumentById(id, blobMarkdown, tagsRaw, title, description, license) { const ssbClient = await openSsb(); const tipId = await this.resolveCurrentId(id); @@ -238,7 +196,8 @@ module.exports = ({ cooler }) => { tags, title: title !== undefined ? (title || "") : oldMsg.content.title || "", description: description !== undefined ? (description || "") : oldMsg.content.description || "", - updatedAt + updatedAt, + ...(license !== undefined ? { license } : {}) }; const tombstone = { type: "tombstone", target: tipId, deletedAt: updatedAt, author: userId }; @@ -327,12 +286,12 @@ module.exports = ({ cooler }) => { async getDocumentById(id) { const ssbClient = await openSsb(); const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('documents', messages, buildIndex); const favorites = await favoritesSetForDocuments(); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Document not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Document not found"); const c = agg.contentNode.c || {}; return { @@ -345,6 +304,7 @@ module.exports = ({ cooler }) => { author: agg.contentNode.author, title: c.title || "", description: c.description || "", + license: c.license || "", opinions: agg.opinions, opinions_inhabitants: agg.voters, isFavorite: favorites.has(rootId || agg.contentTip) diff --git a/src/models/emergencies_model.js b/src/models/emergencies_model.js new file mode 100644 index 0000000..91929b3 --- /dev/null +++ b/src/models/emergencies_model.js @@ -0,0 +1,297 @@ +const { readTyped, memoIndex } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const EMERGENCY_TYPES = ['emergency', 'emergencyConfirm', 'emergencyUpdate', 'tombstone']; +const CATEGORIES = ['WEATHER', 'INFRASTRUCTURE', 'HEALTH', 'SECURITY', 'LOST', 'NEIGHBORHOOD']; +const SEVERITY_THRESHOLDS = { LOW: 2, MEDIUM: 5, HIGH: 10 }; +const EXPIRES_OPTIONS = { '1d': 1, '3d': 3, '7d': 7, '30d': 30 }; +const MEDIA_RE = /!?\[(image|video):[^\]]*\]\((&[^)]+)\)/; +const RECENT_MS = 24 * 60 * 60 * 1000; + +const normU = (v) => String(v == null ? '' : v).trim().toUpperCase(); +const safeText = (v) => String(v == null ? '' : v).trim(); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const normalizeCategory = (v) => (CATEGORIES.includes(normU(v)) ? normU(v) : 'NEIGHBORHOOD'); +const severityOf = (confirmations) => { + const n = Number(confirmations) || 0; + if (n >= SEVERITY_THRESHOLDS.HIGH) return 'HIGH'; + if (n >= SEVERITY_THRESHOLDS.MEDIUM) return 'MEDIUM'; + if (n >= SEVERITY_THRESHOLDS.LOW) return 'LOW'; + return 'UNVERIFIED'; +}; +const expiresAtFrom = (expiresIn, from = Date.now()) => { + const days = EXPIRES_OPTIONS[String(expiresIn || '').toLowerCase()]; + return days ? new Date(from + days * 24 * 60 * 60 * 1000).toISOString() : null; +}; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const readAll = async (ssbClient) => readTyped(ssbClient, EMERGENCY_TYPES, { limit: logLimit }); + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const parentOf = new Map(); + const confirms = []; + const updates = []; + for (const m of messages) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === 'emergency') { + nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } else if (c.type === 'emergencyConfirm' && typeof c.target === 'string') { + confirms.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0 }); + } else if (c.type === 'emergencyUpdate' && typeof c.target === 'string') { + updates.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0, text: safeText(c.text), createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString(), replaces: typeof c.replaces === 'string' ? c.replaces : null }); + } + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const updateParent = new Map(updates.filter(u => u.replaces).map(u => [u.key, u.replaces])); + const updateRootOf = (key) => { + const seen = new Set(); + let cur = key; + while (updateParent.has(cur) && !seen.has(cur)) { seen.add(cur); cur = updateParent.get(cur); } + return cur; + }; + const updateByKey = new Map(updates.map(u => [u.key, u])); + const updateVersions = new Map(); + for (const u of updates) { + const uroot = updateRootOf(u.key); + if (tomb.has(uroot)) continue; + if (!updateVersions.has(uroot)) updateVersions.set(uroot, []); + updateVersions.get(uroot).push(u); + } + const updateItems = []; + for (const [uroot, versions] of updateVersions) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + const confirmers = Array.from(new Set(confirms.filter(cf => updateByKey.has(cf.target) && updateRootOf(cf.target) === uroot && cf.author !== first.author).map(cf => cf.author))); + updateItems.push({ + id: uroot, key: uroot, tipId: tip.key, target: first.target, author: first.author, + ts: first.ts, editedTs: tip.ts, text: tip.text, createdAt: first.createdAt, updatedAt: tip.createdAt, edited: tip.key !== uroot, + confirmations: confirmers, confirmationCount: confirmers.length, + lastActivityTs: Math.max(tip.ts, ...confirms.filter(cf => updateByKey.has(cf.target) && updateRootOf(cf.target) === uroot).map(cf => cf.ts)) + }); + } + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const emergencies = new Map(); + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + const c = tip.c; + const thread = updateItems.filter(u => nodes.has(u.target) && rootOf(u.target) === root && u.author === first.author).sort((a, b) => a.ts - b.ts); + const confirmers = Array.from(new Set([ + ...confirms.filter(cf => nodes.has(cf.target) && rootOf(cf.target) === root && cf.author !== first.author).map(cf => cf.author), + ...thread.flatMap(u => u.confirmations) + ])); + const expiresAt = c.expiresAt || null; + const expired = !!(expiresAt && Date.parse(expiresAt) < Date.now()); + const status = normU(c.status) === 'RESOLVED' ? 'RESOLVED' : (expired ? 'EXPIRED' : 'ACTIVE'); + emergencies.set(root, { + id: root, rootId: root, tipId: tip.key, + title: safeText(c.title), text: safeText(c.text), + media: (() => { const m = safeText(c.text).match(MEDIA_RE); return m ? { kind: m[1], blobId: m[2] } : null; })(), + category: normalizeCategory(c.category), mapUrl: safeText(c.mapUrl), tags: normalizeTags(c.tags), + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, lastActivityTs: Math.max(tip.ts, ...thread.map(u => u.lastActivityTs), ...confirms.filter(cf => nodes.has(cf.target) && rootOf(cf.target) === root).map(cf => cf.ts)), + expiresAt, expired, status, resolved: status === 'RESOLVED', + confirmations: confirmers, confirmationCount: confirmers.length, severity: severityOf(confirmers.length), + updates: thread + }); + } + const updatesById = new Map(updateItems.map(u => [u.id, u])); + return { emergencies, rootOf, nodes, updatesById, updateRootOf }; + }; + + const load = async () => { + const ssbClient = await openSsb(); + const idx = memoIndex('emergencies', await readAll(ssbClient), buildIndex); + return { ssbClient, idx }; + }; + + const find = (idx, id) => { + const key = String(id || ''); + if (idx.emergencies.has(key)) return idx.emergencies.get(key); + if (idx.nodes.has(key)) return idx.emergencies.get(idx.rootOf(key)) || null; + return null; + }; + + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + + return { + CATEGORIES, + SEVERITY_THRESHOLDS, + EXPIRES_OPTIONS, + severityOf, + + async createEmergency({ title, text, category, mapUrl, tags, expiresIn }) { + const ssbClient = await openSsb(); + const cleanTitle = safeText(title); + if (!cleanTitle) throw new Error('Title required'); + const now = new Date().toISOString(); + const content = { + type: 'emergency', + title: cleanTitle, + text: safeText(text), + category: normalizeCategory(category), + mapUrl: safeText(mapUrl), + tags: normalizeTags(tags), + status: 'ACTIVE', + expiresAt: expiresAtFrom(expiresIn), + author: ssbClient.id, + createdAt: now, + updatedAt: now + }; + return publish(ssbClient, content); + }, + + async updateEmergency(id, data = {}) { + const { ssbClient, idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Emergency not found'); + if (String(emergency.author) !== String(ssbClient.id)) throw new Error('Only the author can update'); + const editsContent = ['title', 'text', 'category', 'mapUrl', 'tags', 'expiresIn'].some(k => data[k] !== undefined); + if (editsContent && (emergency.confirmationCount || 0) > 0) throw new Error('Cannot edit an emergency after it has been confirmed'); + const content = { + type: 'emergency', + title: data.title !== undefined ? (safeText(data.title) || emergency.title) : emergency.title, + text: data.text !== undefined ? safeText(data.text) : emergency.text, + category: data.category !== undefined ? normalizeCategory(data.category) : emergency.category, + mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : emergency.mapUrl, + tags: data.tags !== undefined ? normalizeTags(data.tags) : emergency.tags, + status: data.status !== undefined ? (normU(data.status) === 'RESOLVED' ? 'RESOLVED' : 'ACTIVE') : (emergency.resolved ? 'RESOLVED' : 'ACTIVE'), + expiresAt: data.expiresIn !== undefined && data.expiresIn !== 'keep' ? expiresAtFrom(data.expiresIn) : emergency.expiresAt, + author: ssbClient.id, + createdAt: emergency.createdAt, + updatedAt: new Date().toISOString(), + replaces: emergency.tipId + }; + const res = await publish(ssbClient, content); + return { key: res.key, rootId: emergency.id }; + }, + + async resolveEmergency(id) { + return this.updateEmergency(id, { status: 'RESOLVED' }); + }, + + async confirmEmergency(id) { + const { ssbClient, idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Emergency not found'); + if (String(emergency.author) === String(ssbClient.id)) throw new Error('Cannot confirm own emergency'); + if (emergency.confirmations.includes(ssbClient.id)) throw new Error('Already confirmed'); + return publish(ssbClient, { type: 'emergencyConfirm', target: emergency.id, createdAt: new Date().toISOString() }); + }, + + async confirmUpdate(updateId) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) === String(ssbClient.id)) throw new Error('Cannot confirm own update'); + if (update.confirmations.includes(ssbClient.id)) throw new Error('Already confirmed'); + return publish(ssbClient, { type: 'emergencyConfirm', target: update.id, createdAt: new Date().toISOString() }); + }, + + async editUpdate(updateId, text) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) !== String(ssbClient.id)) throw new Error('Only the author can edit an update'); + if (update.confirmationCount > 0) throw new Error('Cannot edit an update after it has been confirmed'); + const clean = safeText(text); + if (!clean) throw new Error('Text required'); + const res = await publish(ssbClient, { type: 'emergencyUpdate', target: update.target, text: clean, createdAt: new Date().toISOString(), replaces: update.tipId }); + return { key: res.key, rootId: update.id, emergencyId: update.target }; + }, + + async deleteUpdate(updateId) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) !== String(ssbClient.id)) throw new Error('Only the author can delete an update'); + await publish(ssbClient, { type: 'tombstone', target: update.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: update.id, emergencyId: update.target }; + }, + + async addUpdate(id, text) { + const { ssbClient, idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Emergency not found'); + if (String(emergency.author) !== String(ssbClient.id)) throw new Error('Only the author can post updates'); + const clean = safeText(text); + if (!clean) throw new Error('Text required'); + return publish(ssbClient, { type: 'emergencyUpdate', target: emergency.id, text: clean, createdAt: new Date().toISOString() }); + }, + + async deleteEmergency(id) { + const { ssbClient, idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Emergency not found'); + if (String(emergency.author) !== String(ssbClient.id)) throw new Error('Only the author can delete'); + return publish(ssbClient, { type: 'tombstone', target: emergency.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + }, + + async getEmergencyById(id) { + const { idx } = await load(); + return find(idx, id); + }, + + async listAll({ filter = 'all', q = '', viewerId } = {}) { + const { ssbClient, idx } = await load(); + const me = viewerId || ssbClient.id; + let list = Array.from(idx.emergencies.values()); + const f = normU(filter || 'all'); + if (f === 'MINE') list = list.filter(a => String(a.author) === String(me)); + else if (f === 'RECENT') list = list.filter(a => a.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'ACTIVE') list = list.filter(a => a.status === 'ACTIVE'); + else if (f === 'RESOLVED') list = list.filter(a => a.status === 'RESOLVED'); + else if (f === 'EXPIRED') list = list.filter(a => a.status === 'EXPIRED'); + else if (['UNVERIFIED', 'LOW', 'MEDIUM', 'HIGH'].includes(f)) list = list.filter(a => a.severity === f); + else if (CATEGORIES.includes(f)) list = list.filter(a => a.category === f); + const needle = safeText(q).toLowerCase(); + if (needle) list = list.filter(a => a.title.toLowerCase().includes(needle) || a.text.toLowerCase().includes(needle) || a.tags.some(t => t.toLowerCase().includes(needle))); + return list.sort((a, b) => b.lastActivityTs - a.lastActivityTs); + }, + + async featured() { + const { idx } = await load(); + const rank = { HIGH: 3, MEDIUM: 2, LOW: 1, UNVERIFIED: 0 }; + const candidates = Array.from(idx.emergencies.values()).filter(a => a.status === 'ACTIVE' && a.severity === 'HIGH'); + candidates.sort((a, b) => (rank[b.severity] - rank[a.severity]) || (b.confirmationCount - a.confirmationCount) || (b.lastActivityTs - a.lastActivityTs)); + const top = candidates[0]; + return top ? { id: top.id, title: top.title, severity: top.severity, category: top.category, confirmationCount: top.confirmationCount, href: `/emergencies/${encodeURIComponent(top.id)}` } : null; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Not found'); + return emergency.id; + } + }; +}; diff --git a/src/models/events_model.js b/src/models/events_model.js index fb6513d..fb95fb2 100644 --- a/src/models/events_model.js +++ b/src/models/events_model.js @@ -1,11 +1,21 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); +const { normalizeImages, normalizeVideo } = require('./media_gallery'); +const { truthy, hasAnyInterval, nextOccurrence, upcomingOccurrences } = require('./recurrence'); const crypto = require('crypto'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { getConfig } = require('../configs/config-manager.js'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const EVENT_TYPES = [ + 'event', 'eventOpinion', 'eventAttend', + 'event-invite', 'event-invite-tombstone', + 'event-open-invite', 'event-open-invite-tombstone', + 'tribe-keys', 'tombstone' +]; + module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -14,10 +24,53 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ownCrypto = eventCrypto || tribeCrypto; const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null; + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0); + const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ); + readTyped(ssbClient, EVENT_TYPES, { limit: logLimit }); + + const rotateEventKey = async (rid, attendees) => { + if (!ownCrypto || !tribeCrypto || !rid || !lookupKey(rid)) return; + const ssbClient = await openSsb(); + const ssbKeys = require("../server/node_modules/ssb-keys"); + const newKey = ownCrypto.generateTribeKey(); + const newGen = ownCrypto.addNewKey(rid, newKey); + const memberKeys = {}; + for (const m of new Set([ssbClient.id, ...(Array.isArray(attendees) ? attendees : [])])) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} + } + await new Promise((resolve) => { + ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: newGen, memberKeys }, () => resolve()); + }); + }; + + const ensureAttendeeKeys = async (ssbClient, messages, events) => { + if (!tribeCrypto || !ownCrypto) return; + const leavesByRoot = new Map(); + for (const m of messages) { + const c = m.value && m.value.content; + if (!c || c.type !== 'eventAttend' || c.on !== false || typeof c.target !== 'string') continue; + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []); + leavesByRoot.get(c.target).push({ member: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }); + } + const ssbKeys = require("../server/node_modules/ssb-keys"); + for (const ev of events) { + if (!ev || !ev.rid || ev.organizer !== ssbClient.id || !lookupKey(ev.rid)) continue; + const members = Array.isArray(ev.attendees) ? ev.attendees : []; + const plan = ownCrypto.keyPlan({ rootId: ev.rid, ownerId: ev.organizer, gen: lookupGen(ev.rid), members, messages, leaves: leavesByRoot.get(ev.rid) || [] }); + if (plan.rotate) { await rotateEventKey(ev.rid, members); continue; } + if (!plan.missing.length) continue; + const key = lookupKey(ev.rid); + const memberKeys = {}; + for (const m of plan.missing) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys); } catch (_) {} + } + if (!Object.keys(memberKeys).length) continue; + await new Promise((resolve) => { + ssbClient.publish({ type: "tribe-keys", tribeId: ev.rid, generation: lookupGen(ev.rid) || 1, memberKeys }, () => resolve()); + }); + } + }; const ingestOwnTribeKeys = async () => { if (!ownCrypto) return; @@ -26,9 +79,11 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ssbKeys = require("../server/node_modules/ssb-keys"); const cfg = require("../server/ssb_config"); const msgs = await readAll(ssbClient); + const idx = buildEventIndex(msgs); for (const m of msgs) { const c = m.value && m.value.content; if (!c || c.type !== "tribe-keys") continue; + if (!c.tribeId || (m.value.author !== idx.eventAuthor.get(c.tribeId) && m.value.author !== ssbClient.id)) continue; const memberKeys = c.memberKeys; if (!memberKeys || typeof memberKeys !== "object") continue; const boxed = memberKeys[ssbClient.id]; @@ -72,8 +127,22 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { return m.toISOString(); }; + const recurrenceOf = (c) => ({ + weekly: truthy(c.intervalWeekly), + monthly: truthy(c.intervalMonthly), + yearly: truthy(c.intervalYearly), + until: c.recurrenceUntil || '' + }); + + const effectiveDate = (c) => { + const r = recurrenceOf(c); + if (!hasAnyInterval(r.weekly, r.monthly, r.yearly) || !r.until) return c.date; + const next = nextOccurrence(c.date, r.until, r.weekly, r.monthly, r.yearly); + return next ? next.toISOString() : c.date; + }; + const deriveStatus = (c) => { - const dateM = moment(c.date); + const dateM = moment(effectiveDate(c)); let status = String(c.status || 'OPEN').toUpperCase(); if (dateM.isValid() && dateM.isBefore(moment())) status = 'CLOSED'; if (status !== 'OPEN' && status !== 'CLOSED') status = 'OPEN'; @@ -82,45 +151,18 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const buildEventIndex = (messages) => { const eventAuthor = new Map(); - const eventReplaces = new Map(); + const entries = []; for (const m of messages) { const c = m.value && m.value.content; if (!c || c.type !== 'event') continue; eventAuthor.set(m.key, m.value.author); - if (c.replaces) eventReplaces.set(m.key, c.replaces); + entries.push({ key: m.key, author: m.value.author, seq: m.value.sequence || 0, ts: m.value.timestamp || m.timestamp || 0, replaces: typeof c.replaces === 'string' ? c.replaces : null, claimedAuthor: typeof c.organizer === 'string' ? c.organizer : null }); } - for (const [key, target] of Array.from(eventReplaces.entries())) { - if (!eventAuthor.has(target)) { eventReplaces.delete(key); continue; } - if (eventAuthor.get(target) !== eventAuthor.get(key)) { eventReplaces.delete(key); eventAuthor.delete(key); } - } - const strictNext = new Map(); - for (const [key, target] of eventReplaces) { - if (eventAuthor.get(target) === eventAuthor.get(key)) strictNext.set(target, key); - } - const rootOf = (id) => { - let cur = id; - const seen = new Set(); - while (eventReplaces.has(cur) && eventAuthor.has(eventReplaces.get(cur))) { - if (seen.has(cur)) break; - seen.add(cur); - cur = eventReplaces.get(cur); - } - return cur; - }; - const contentTipOf = (root) => { - let cur = root; - const seen = new Set(); - while (strictNext.has(cur)) { - if (seen.has(cur)) break; - seen.add(cur); - cur = strictNext.get(cur); - } - return cur; - }; - return { eventAuthor, eventReplaces, strictNext, rootOf, contentTipOf }; + const vg = versionGroups(entries); + return { eventAuthor, vg, rootOf: vg.rootOf, contentTipOf: vg.tipOf }; }; - const collectCollab = (messages) => { + const collectCollab = (messages, rootOf = (id) => id) => { const opinions = new Map(); const attend = new Map(); for (const m of messages) { @@ -128,13 +170,14 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { if (!c || typeof c.target !== 'string') continue; const author = m.value.author; const ts = (m.value && m.value.timestamp) || 0; + const target = rootOf(c.target); if (c.type === 'eventOpinion' && typeof c.category === 'string') { - if (!opinions.has(c.target)) opinions.set(c.target, new Map()); - const byAuthor = opinions.get(c.target); + if (!opinions.has(target)) opinions.set(target, new Map()); + const byAuthor = opinions.get(target); if (!byAuthor.has(author)) byAuthor.set(author, c.category); } else if (c.type === 'eventAttend') { - if (!attend.has(c.target)) attend.set(c.target, new Map()); - const byAuthor = attend.get(c.target); + if (!attend.has(target)) attend.set(target, new Map()); + const byAuthor = attend.get(target); const prev = byAuthor.get(author); if (!prev || ts >= prev.ts) byAuthor.set(author, { on: c.on !== false, ts }); } @@ -171,7 +214,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { return idx.rootOf(id); }, - async createEvent(title, description, date, location, price = 0, url = "", attendees = [], tagsRaw = [], isPublic, mapUrl = "", clearnetPublic = false) { + async createEvent(title, description, date, location, price = 0, url = "", attendees = [], tagsRaw = [], isPublic, mapUrl = "", clearnetPublic = false, media = {}, recurrence = {}) { const ssbClient = await openSsb(); const userId = await me(); @@ -203,6 +246,12 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { isPublic: visibility, mapUrl: String(mapUrl || "").trim(), clearnetPublic: clearnetPublic === true || clearnetPublic === 'true' || clearnetPublic === 'on', + images: normalizeImages(media && media.images), + video: normalizeVideo(media && media.video), + intervalWeekly: truthy(recurrence && recurrence.weekly), + intervalMonthly: truthy(recurrence && recurrence.monthly), + intervalYearly: truthy(recurrence && recurrence.yearly), + recurrenceUntil: String((recurrence && recurrence.until) || '').trim(), opinions: {}, opinions_inhabitants: [] }; @@ -263,23 +312,28 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ssbClient = await openSsb(); const rid = await this.resolveRootId(eventId).catch(() => eventId); const messages = await readAll(ssbClient); + const authorOf = new Map(); + for (const m of messages) if (m && m.key && m.value) authorOf.set(m.key, m.value.author); + const eventAuthor = authorOf.get(rid); + if (!eventAuthor) return null; const markerTomb = new Set(); const invTomb = new Set(); for (const m of messages) { const c = m.value && m.value.content; - if (!c) continue; - if (c.type === 'event-open-invite-tombstone' && typeof c.target === 'string') markerTomb.add(c.target); - if (c.type === 'event-invite-tombstone' && typeof c.target === 'string') invTomb.add(c.target); + if (!c || typeof c.target !== 'string' || authorOf.get(c.target) !== m.value.author) continue; + if (c.type === 'event-open-invite-tombstone') markerTomb.add(c.target); + if (c.type === 'event-invite-tombstone') invTomb.add(c.target); } let best = null; for (const m of messages) { const c = m.value && m.value.content; if (!c || c.type !== 'event-open-invite' || c.v !== 1) continue; if (c.target !== rid || typeof c.code !== 'string') continue; + if (m.value.author !== eventAuthor) continue; if (markerTomb.has(m.key)) continue; if (c.inviteKey && invTomb.has(c.inviteKey)) continue; const ts = (m.value && m.value.timestamp) || 0; - if (!best || ts > best.ts) best = { code: c.code, by: c.by || m.value.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }; + if (!best || ts > best.ts) best = { code: c.code, by: m.value.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }; } return best ? { code: best.code, by: best.by, markerKey: best.markerKey, inviteKey: best.inviteKey } : null; }, @@ -324,6 +378,16 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { if (rec.by !== userId && organizer !== userId) throw new Error("Not allowed to remove this invitation"); await new Promise((resolve, reject) => ssbClient.publish({ type: 'event-open-invite-tombstone', target: rec.markerKey, ts: new Date().toISOString() }, (err) => err ? reject(err) : resolve())); if (rec.inviteKey) await new Promise((resolve, reject) => ssbClient.publish({ type: 'event-invite-tombstone', target: rec.inviteKey, ts: new Date().toISOString() }, (err) => err ? reject(err) : resolve())); + if (organizer === userId) { + const rid = await this.resolveRootId(eventId); + const messages = await readAll(ssbClient); + const idx = buildEventIndex(messages); + const tip = idx.contentTipOf(rid); + const raw = messages.find(x => x.key === tip); + const rc = raw && raw.value && raw.value.content; + const dec = rc && rc.encryptedPayload ? decryptEventContent(rc, rid) : rc; + if (dec && !dec._undecryptable) await rotateEventKey(rid, aggregateCollab(dec, rid, collectCollab(messages, idx.rootOf)).attendees); + } }, async joinByInvite(code) { @@ -368,14 +432,13 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const status = deriveStatus(c); if (status === 'CLOSED') throw new Error("Cannot attend a closed event"); - const collab = collectCollab(messages); + const collab = collectCollab(messages, idx.rootOf); const agg = aggregateCollab(c, rid, collab); const isAttending = agg.attendees.includes(userId); const isLeaving = isAttending; const on = !isAttending; const isPrivate = normalizePrivacy(c.isPublic) === 'private'; - const isOrganizer = c.organizer === userId; const result = await new Promise((resolve, reject) => { ssbClient.publish({ type: 'eventAttend', target: rid, on, createdAt: new Date().toISOString() }, (err2, res2) => err2 ? reject(err2) : resolve(res2)); @@ -397,36 +460,20 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { } catch (_) {} } - if (isPrivate && isLeaving && !isOrganizer && ownCrypto && tribeCrypto) { - try { - const remaining = agg.attendees.filter(a => a !== userId); - const newKey = ownCrypto.generateTribeKey(); - const newGen = ownCrypto.addNewKey(rid, newKey); - const ssbKeys = require("../server/node_modules/ssb-keys"); - const memberKeys = {}; - for (const m of remaining) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} - } - if (Object.keys(memberKeys).length) { - await new Promise((resolve) => { - ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: newGen, memberKeys }, () => resolve()); - }); - } - } catch (_) {} - } - return result; }, async deleteEventById(eventId) { const ssbClient = await openSsb(); const userId = await me(); - const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, ev) => err || !ev || !ev.content ? rej(new Error("Error retrieving event")) : res(ev))); - const rid = await this.resolveRootId(eventId); + const idx = buildEventIndex(await readAll(ssbClient)); + const rid = idx.rootOf(eventId); + const tipId = idx.contentTipOf(rid); + const ev = await new Promise((res, rej) => ssbClient.get(tipId, (err, ev) => err || !ev || !ev.content ? rej(new Error("Error retrieving event")) : res(ev))); const c = ev.content && ev.content.encryptedPayload ? decryptEventContent(ev.content, rid) : ev.content; if (c && c._undecryptable) throw new Error("Event is encrypted and cannot be decrypted"); - if (c.organizer !== userId) throw new Error("Only the organizer can delete this event"); - const tombstone = { type: 'tombstone', target: eventId, deletedAt: new Date().toISOString(), author: userId }; + if (c.organizer !== userId || ev.author !== userId) throw new Error("Only the organizer can delete this event"); + const tombstone = { type: 'tombstone', target: tipId, deletedAt: new Date().toISOString(), author: userId }; return new Promise((resolve, reject) => { ssbClient.publish(tombstone, (err, res) => err ? reject(err) : resolve(res)); }); @@ -435,15 +482,19 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { async getEventById(eventId) { const ssbClient = await openSsb(); const messages = await readAll(ssbClient); - const idx = buildEventIndex(messages); + const idx = memoIndex('events', messages, buildEventIndex); const rid = idx.rootOf(eventId); + if (idx.vg.isForeignCopy(rid)) throw new Error("Error retrieving event"); const contentTip = idx.contentTipOf(rid); + const tombstoned = buildValidatedTombstoneSet(messages); + if (tombstoned.has(rid) || tombstoned.has(contentTip)) throw new Error("Error retrieving event"); const msg = await new Promise((res, rej) => ssbClient.get(contentTip, (err, msg) => err || !msg || !msg.content ? rej(new Error("Error retrieving event")) : res(msg))); const c = msg.content && msg.content.encryptedPayload ? decryptEventContent(msg.content, rid) : msg.content; if (c && c._undecryptable) throw new Error("Event is encrypted and cannot be decrypted with available keys"); + if (c && c.organizer && c.organizer !== msg.author) throw new Error("Error retrieving event"); const status = deriveStatus(c); - const agg = aggregateCollab(c, rid, collectCollab(messages)); + const agg = aggregateCollab(c, rid, collectCollab(messages, idx.rootOf)); return { id: contentTip, @@ -461,6 +512,15 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { status, isPublic: normalizePrivacy(c.isPublic), mapUrl: c.mapUrl || "", + images: normalizeImages(c.images), + video: normalizeVideo(c.video), + intervalWeekly: truthy(c.intervalWeekly), + intervalMonthly: truthy(c.intervalMonthly), + intervalYearly: truthy(c.intervalYearly), + recurrenceUntil: c.recurrenceUntil || '', + recurring: hasAnyInterval(truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)) && !!c.recurrenceUntil, + nextDate: effectiveDate(c), + occurrences: upcomingOccurrences(c.date, c.recurrenceUntil, truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)).map(d => d.toISOString()), clearnetPublic: !!c.clearnetPublic, encrypted: normalizePrivacy(c.isPublic) === 'private', opinions: agg.opinions, @@ -471,11 +531,13 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { async updateEventById(eventId, updatedData) { const ssbClient = await openSsb(); const userId = await me(); - const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, ev) => err || !ev || !ev.content ? rej(new Error("Error retrieving event")) : res(ev))); - const rid = await this.resolveRootId(eventId); + const idx = buildEventIndex(await readAll(ssbClient)); + const rid = idx.rootOf(eventId); + const tipId = idx.contentTipOf(rid); + const ev = await new Promise((res, rej) => ssbClient.get(tipId, (err, ev) => err || !ev || !ev.content ? rej(new Error("Error retrieving event")) : res(ev))); const c = ev.content && ev.content.encryptedPayload ? decryptEventContent(ev.content, rid) : ev.content; if (c && c._undecryptable) throw new Error("Event is encrypted and cannot be decrypted"); - if (c.organizer !== userId) throw new Error("Only the organizer can update this event"); + if (c.organizer !== userId || ev.author !== userId) throw new Error("Only the organizer can update this event"); const status = deriveStatus(c); if (status === 'CLOSED') throw new Error("Cannot edit a closed event"); @@ -503,8 +565,14 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { isPublic: updatedData.isPublic !== undefined ? normalizePrivacy(updatedData.isPublic) : normalizePrivacy(c.isPublic), clearnetPublic: updatedData.clearnetPublic !== undefined ? (updatedData.clearnetPublic === true || updatedData.clearnetPublic === 'true' || updatedData.clearnetPublic === 'on') : !!c.clearnetPublic, attendees: uniq(Array.isArray(c.attendees) ? c.attendees : []), + images: normalizeImages(updatedData.images !== undefined ? updatedData.images : c.images), + video: updatedData.video !== undefined ? normalizeVideo(updatedData.video) : normalizeVideo(c.video), + intervalWeekly: updatedData.intervalWeekly !== undefined ? truthy(updatedData.intervalWeekly) : truthy(c.intervalWeekly), + intervalMonthly: updatedData.intervalMonthly !== undefined ? truthy(updatedData.intervalMonthly) : truthy(c.intervalMonthly), + intervalYearly: updatedData.intervalYearly !== undefined ? truthy(updatedData.intervalYearly) : truthy(c.intervalYearly), + recurrenceUntil: updatedData.recurrenceUntil !== undefined ? String(updatedData.recurrenceUntil || '').trim() : (c.recurrenceUntil || ''), updatedAt: new Date().toISOString(), - replaces: eventId + replaces: tipId }; const wasPrivate = normalizePrivacy(c.isPublic) === 'private'; @@ -554,7 +622,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ev = await new Promise((res, rej) => ssbClient.get(contentTip, (err, m) => (err || !m || !m.content) ? rej(new Error('Event not found')) : res(m))); const c = ev.content && ev.content.encryptedPayload ? decryptEventContent(ev.content, rid) : ev.content; if (c && c._undecryptable) throw new Error('Event is encrypted and cannot be decrypted'); - const agg = aggregateCollab(c, rid, collectCollab(messages)); + const agg = aggregateCollab(c, rid, collectCollab(messages, idx.rootOf)); if (agg.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); return new Promise((res, rej) => ssbClient.publish({ type: 'eventOpinion', target: rid, category, createdAt: new Date().toISOString() }, (err, result) => err ? rej(err) : res(result))); }, @@ -562,71 +630,67 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { async listAll(author = null, filter = 'all') { const ssbClient = await openSsb(); const userId = await me(); - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, results) => { - if (err) return reject(new Error("Error listing events: " + err.message)); - const tombstoned = buildValidatedTombstoneSet(results); - const idx = buildEventIndex(results); - const collab = collectCollab(results); + const results = await readAll(ssbClient); + const tombstoned = buildValidatedTombstoneSet(results); + const idx = buildEventIndex(results); + const collab = collectCollab(results, idx.rootOf); - const roots = new Set(); - for (const r of results) { - const rawC = r.value && r.value.content; - if (!rawC || rawC.type !== 'event') continue; - if (!idx.eventAuthor.has(r.key)) continue; - roots.add(idx.rootOf(r.key)); - } + const byKey = new Map(results.map(x => [x.key, x])); + const byRoot = new Map(); + for (const rid of idx.vg.roots) { + if (tombstoned.has(rid)) continue; + const contentTip = idx.contentTipOf(rid); + if (tombstoned.has(contentTip)) continue; + const r = byKey.get(contentTip); + if (!r) continue; + const rawC = r.value && r.value.content; + if (!rawC) continue; + const c = rawC.encryptedPayload ? decryptEventContent(rawC, rid) : rawC; + if (!c || c._undecryptable) continue; + if (author && c.organizer !== author) continue; - const byRoot = new Map(); - for (const rid of roots) { - if (tombstoned.has(rid)) continue; - const contentTip = idx.contentTipOf(rid); - if (tombstoned.has(contentTip)) continue; - const r = results.find(x => x.key === contentTip); - if (!r) continue; - const rawC = r.value && r.value.content; - if (!rawC) continue; - const c = rawC.encryptedPayload ? decryptEventContent(rawC, rid) : rawC; - if (!c || c._undecryptable) continue; - if (author && c.organizer !== author) continue; + const status = deriveStatus(c); + const agg = aggregateCollab(c, rid, collab); - const status = deriveStatus(c); - const agg = aggregateCollab(c, rid, collab); + byRoot.set(rid, { + id: contentTip, + title: c.title || '', + description: c.description || '', + date: c.date || '', + location: c.location || '', + price: c.price || 0, + url: c.url || '', + attendees: agg.attendees, + tags: Array.isArray(c.tags) ? c.tags.filter(Boolean) : [], + createdAt: c.createdAt || new Date().toISOString(), + organizer: c.organizer || '', + status, + isPublic: normalizePrivacy(c.isPublic), + mapUrl: c.mapUrl || "", + images: normalizeImages(c.images), + video: normalizeVideo(c.video), + intervalWeekly: truthy(c.intervalWeekly), + intervalMonthly: truthy(c.intervalMonthly), + intervalYearly: truthy(c.intervalYearly), + recurrenceUntil: c.recurrenceUntil || '', + recurring: hasAnyInterval(truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)) && !!c.recurrenceUntil, + nextDate: effectiveDate(c), + encrypted: normalizePrivacy(c.isPublic) === 'private', + opinions: agg.opinions, + opinions_inhabitants: agg.opinions_inhabitants + }); + } - byRoot.set(rid, { - id: contentTip, - title: c.title || '', - description: c.description || '', - date: c.date || '', - location: c.location || '', - price: c.price || 0, - url: c.url || '', - attendees: agg.attendees, - tags: Array.isArray(c.tags) ? c.tags.filter(Boolean) : [], - createdAt: c.createdAt || new Date().toISOString(), - organizer: c.organizer || '', - status, - isPublic: normalizePrivacy(c.isPublic), - mapUrl: c.mapUrl || "", - encrypted: normalizePrivacy(c.isPublic) === 'private', - opinions: agg.opinions, - opinions_inhabitants: agg.opinions_inhabitants - }); - } + try { await ensureAttendeeKeys(ssbClient, results, [...byRoot].filter(([rid, e]) => e.encrypted && idx.eventAuthor.get(rid) === userId).map(([rid, e]) => ({ rid, organizer: userId, attendees: e.attendees }))); } catch (_) {} - let out = Array.from(byRoot.values()); - out = dedupeBy(out, e => e.title ? [norm(e.organizer), norm(e.title), norm(e.date)].join('|') : null); + let out = Array.from(byRoot.values()); + out = dedupeBy(out, e => e.title ? [norm(e.organizer), norm(e.title), norm(e.date)].join('|') : null); - if (filter === 'mine') out = out.filter(e => e.organizer === userId); - if (filter === 'open') out = out.filter(e => String(e.status).toUpperCase() === 'OPEN'); - if (filter === 'closed') out = out.filter(e => String(e.status).toUpperCase() === 'CLOSED'); + if (filter === 'mine') out = out.filter(e => e.organizer === userId); + if (filter === 'open') out = out.filter(e => String(e.status).toUpperCase() === 'OPEN'); + if (filter === 'closed') out = out.filter(e => String(e.status).toUpperCase() === 'CLOSED'); - resolve(out); - }) - ); - }); + return out; } }; }; diff --git a/src/models/exportmode_model.js b/src/models/exportmode_model.js index cd535d5..d3d31ba 100644 --- a/src/models/exportmode_model.js +++ b/src/models/exportmode_model.js @@ -1,15 +1,13 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); -const archiver = require('../server/node_modules/archiver'); module.exports = { exportSSB: async (outputPath) => { try { - const homeDir = os.homedir(); - const ssbPath = path.join(homeDir, '.ssb'); - const output = fs.createWriteStream(outputPath); - const archive = archiver('zip', { + const ssbPath = require('../configs/state-manager').ssbDir(); + const output = fs.createWriteStream(outputPath, { mode: 0o600 }); + const archive = require('../server/node_modules/archiver')('zip', { zlib: { level: 9 } }); archive.pipe(output); @@ -22,7 +20,8 @@ module.exports = { const filePath = path.join(dirPath, file); const stat = fs.statSync(filePath); - if (file === 'secret') { + const rel = path.relative(ssbPath, filePath); + if (file === 'secret' || file.startsWith('secret.') || file === 'oasis-config.json' || rel === path.join('oasis', 'keys') || rel.startsWith(path.join('oasis', 'keys') + path.sep)) { return; } diff --git a/src/models/favorites_model.js b/src/models/favorites_model.js index d0fd3f5..43d8334 100644 --- a/src/models/favorites_model.js +++ b/src/models/favorites_model.js @@ -1,4 +1,4 @@ -const mediaFavorites = require("../backend/media-favorites"); +const contentFavorites = require("../backend/content_favorites"); const safeArr = (v) => (Array.isArray(v) ? v : []); const safeText = (v) => String(v || "").trim(); @@ -15,7 +15,7 @@ const toTs = (d) => { return Number.isFinite(t) ? t : 0; }; -module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel, mapsModel, padsModel, chatsModel, calendarsModel, torrentsModel, marketModel, shopsModel }) => { +module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel, mapsModel, padsModel, roomsModel, chatsModel, calendarsModel, torrentsModel, filesModel, marketModel, shopsModel, eventsModel, tasksModel, reportsModel, votesModel, jobsModel, housingModel, projectsModel, transfersModel, forumModel, blogsModel, pollsModel, schoolModel, wikiModel, emergenciesModel, mailingModel, logisticsModel, podcastsModel, campaignsModel }) => { const kindConfig = { audios: { base: "/audios/", @@ -45,6 +45,34 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi base: "/pads/", getById: getFn(padsModel, ["getPadById", "getById"]) }, + rooms: { + base: "/rooms/", + getById: getFn(roomsModel, ["getRoomById"]) + }, + wiki: { + base: "/wiki/", + getById: getFn(wikiModel, ["getPage"]) + }, + emergencies: { + base: "/emergencies/", + getById: getFn(emergenciesModel, ["getEmergencyById"]) + }, + mailing: { + base: "/mailing/", + getById: getFn(mailingModel, ["getListById"]) + }, + logistics: { + base: "/logistics/", + getById: getFn(logisticsModel, ["getRouteById"]) + }, + podcasts: { + base: "/podcasts/", + getById: getFn(podcastsModel, ["getAnyById"]) + }, + campaigns: { + base: "/campaigns/", + getById: getFn(campaignsModel, ["getCampaignById"]) + }, chats: { base: "/chats/", getById: getFn(chatsModel, ["getChatById", "getById"]) @@ -57,6 +85,10 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi base: "/torrents/", getById: getFn(torrentsModel, ["getTorrentById", "getById"]) }, + files: { + base: "/files/", + getById: getFn(filesModel, ["getFileById", "getById"]) + }, market: { base: "/market/", getById: getFn(marketModel, ["getItemById", "getById"]) @@ -64,10 +96,62 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi shopProducts: { base: "/shops/product/", getById: getFn(shopsModel, ["getProductById"]) + }, + events: { + base: "/events/", + getById: getFn(eventsModel, ["getEventById", "getById"]) + }, + tasks: { + base: "/tasks/", + getById: getFn(tasksModel, ["getTaskById", "getById"]) + }, + reports: { + base: "/reports/", + getById: getFn(reportsModel, ["getReportById", "getById"]) + }, + school: { + base: "/school/course/", + getById: getFn(schoolModel, ["getCourseById", "getById"]) + }, + votes: { + base: "/votes/", + getById: getFn(votesModel, ["getVoteById", "getById"]) + }, + jobs: { + base: "/jobs/", + getById: getFn(jobsModel, ["getJobById", "getById"]) + }, + housing: { + base: "/housing/", + getById: getFn(housingModel, ["getHousingById", "getById"]) + }, + projects: { + base: "/projects/", + getById: getFn(projectsModel, ["getProjectById", "getById"]) + }, + transfers: { + base: "/transfers/", + getById: getFn(transfersModel, ["getTransferById", "getById"]) + }, + forum: { + base: "/forum/", + getById: getFn(forumModel, ["getForumById", "getById"]) + }, + blogs: { + base: "/blogs/", + getById: getFn(blogsModel, ["getBlogById", "getById"]) + }, + polls: { + base: "/polls/", + getById: getFn(pollsModel, ["getPollById", "getById"]) + }, + shops: { + base: "/shops/", + getById: getFn(shopsModel, ["getShopById", "getById"]) } }; - const kindOrder = ["audios", "bookmarks", "calendars", "chats", "documents", "images", "maps", "pads", "torrents", "videos", "market", "shopProducts"]; + const kindOrder = ["audios", "blogs", "bookmarks", "calendars", "campaigns", "chats", "documents", "events", "files", "forum", "housing", "images", "jobs", "logistics", "mailing", "maps", "market", "pads", "podcasts", "polls", "projects", "reports", "rooms", "school", "shopProducts", "shops", "emergencies", "tasks", "torrents", "transfers", "videos", "votes", "wiki"]; const hydrateKind = async (kind, ids) => { const cfg = kindConfig[kind]; @@ -79,9 +163,11 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi if (!id) return null; try { const obj = await cfg.getById(id); + if (!obj || typeof obj !== "object") return null; const viewId = safeText(obj?.key || obj?.id || id); return { + content: obj, kind, favId: id, viewHref: `${cfg.base}${encodeURIComponent(viewId)}`, @@ -104,7 +190,7 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi }; const loadAll = async () => { - const sets = await Promise.all(kindOrder.map((k) => mediaFavorites.getFavoriteSet(k))); + const sets = await Promise.all(kindOrder.map((k) => contentFavorites.getFavoriteSet(k))); const idsByKind = {}; kindOrder.forEach((k, i) => { idsByKind[k] = Array.from(sets[i] || []); @@ -118,26 +204,15 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi const flat = kindOrder.flatMap((k) => byKind[k]); - const counts = { - audios: byKind.audios.length, - bookmarks: byKind.bookmarks.length, - calendars: byKind.calendars.length, - chats: byKind.chats.length, - documents: byKind.documents.length, - images: byKind.images.length, - maps: byKind.maps.length, - pads: byKind.pads.length, - torrents: byKind.torrents.length, - videos: byKind.videos.length, - market: byKind.market.length, - shopProducts: byKind.shopProducts.length, - all: flat.length - }; + const counts = { all: flat.length }; + for (const k of kindOrder) counts[k] = (byKind[k] || []).length; const recentFlat = flat - .slice() + .filter((x) => (toTs(x.updatedAt) || toTs(x.createdAt) || 0) >= Date.now() - 86400000) .sort((a, b) => (toTs(b.updatedAt) || toTs(b.createdAt)) - (toTs(a.updatedAt) || toTs(a.createdAt))); + counts.recent = recentFlat.length; + return { byKind, flat, recentFlat, counts }; }; @@ -167,11 +242,13 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi return { items: grouped, counts }; }, + kinds: kindOrder.slice(), + async removeFavorite(kind, id) { const k = safeText(kind); const favId = safeText(id); if (!k || !favId) return; - await mediaFavorites.removeFavorite(k, favId); + await contentFavorites.removeFavorite(k, favId); } }; }; diff --git a/src/models/fediverse_model.js b/src/models/fediverse_model.js index a8e1e0d..a61bee1 100644 --- a/src/models/fediverse_model.js +++ b/src/models/fediverse_model.js @@ -1,8 +1,9 @@ const fs = require('fs'); const path = require('path'); +const { Readable } = require('stream'); const { getConfig, saveConfig } = require('../configs/config-manager.js'); -const ACCOUNTS_PATH = path.join(__dirname, '..', 'configs', 'fediverse-accounts.json'); +const ACCOUNTS_PATH = require('../configs/state-manager').statePath('fediverse-accounts.json'); const FETCH_TIMEOUT_MS = 8000; const TIMELINE_CACHE_MS = 60 * 1000; const TIMELINE_LIMIT = 40; @@ -18,9 +19,10 @@ const spoofHeaders = (accept) => ({ }); const isPrivateHost = (hostname) => { - const h = String(hostname || '').toLowerCase(); + let h = String(hostname || '').toLowerCase().trim().replace(/^\[|\]$/g, ''); if (!h) return true; - if (h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.local')) return true; + if (h.startsWith('::ffff:')) h = h.slice(7); + if (h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.local') || h === '0.0.0.0' || h === '::') return true; if (h === '::1' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true; const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); if (m) { @@ -34,6 +36,89 @@ const isPrivateHost = (hostname) => { return false; }; + +const TG_TIMEOUT_MS = 20000; +const TG_MEDIA_MAX_BYTES = 25 * 1024 * 1024; +const PROXY_MEDIA_MAX_BYTES = TG_MEDIA_MAX_BYTES; +const networkPaused = () => { + if (process.env.OASIS_NETWORK_PAUSED === '1') return true; + try { return require('../configs/config-manager.js').getConfig().networkPaused === true; } catch (_) { return true; } +}; +const netFetch = (url, opts) => networkPaused() ? Promise.reject(new Error('Network is paused')) : fetch(url, opts); +const publicOnlyLookup = (hostname, options, callback) => { + require('dns').lookup(hostname, { ...(options || {}), all: true }, (err, addresses) => { + if (err) return callback(err); + const list = Array.isArray(addresses) ? addresses : []; + if (!list.length || list.some(a => isPrivateHost(a.address))) return callback(Object.assign(new Error('Refusing a private address'), { code: 'EPRIVATE' })); + if (options && options.all) return callback(null, list); + callback(null, list[0].address, list[0].family); + }); +}; +let publicClient; +const publicFetch = (url, opts) => { + if (networkPaused()) return Promise.reject(new Error('Network is paused')); + if (publicClient === undefined) { + try { + const { Agent, fetch: clientFetch } = require('../server/node_modules/undici'); + publicClient = { fetch: clientFetch, dispatcher: new Agent({ connect: { lookup: publicOnlyLookup } }) }; + } catch (_) { publicClient = null; } + } + return publicClient ? publicClient.fetch(url, { ...opts, dispatcher: publicClient.dispatcher }) : fetch(url, opts); +}; +const readCapped = async (res, max) => { + if (Number(res.headers.get('content-length') || 0) > max) return null; + if (!res.body || typeof res.body.getReader !== 'function') { + const ab = await res.arrayBuffer(); + return ab.byteLength > max ? null : Buffer.from(ab); + } + const reader = res.body.getReader(); + const parts = []; + let total = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + total += value.length; + if (total > max) { try { await reader.cancel(); } catch (_) {} return null; } + parts.push(Buffer.from(value)); + } + return Buffer.concat(parts); +}; +const TG_MEDIA_CACHE_MAX = 60; +const TG_LOGIN_TTL_MS = 10 * 60 * 1000; +let gram = null; +const loadGram = () => { + if (gram) return gram; + const { TelegramClient, Api } = require('../server/node_modules/telegram'); + const { StringSession } = require('../server/node_modules/telegram/sessions'); + const { Logger, LogLevel } = require('../server/node_modules/telegram/extensions/Logger'); + gram = { TelegramClient, Api, StringSession, Logger, LogLevel }; + return gram; +}; +const withTimeout = (promise, ms = TG_TIMEOUT_MS) => new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('telegramErrTimeout')), ms); + Promise.resolve(promise).then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); }); +}); +const tgDisplayName = (u) => { + if (!u) return ''; + if (u.title) return String(u.title); + const full = [u.firstName, u.lastName].filter(Boolean).join(' ').trim(); + return full || (u.username ? `@${u.username}` : ''); +}; +const tgErrCode = (err) => { + if (err && err.message === 'peerErrPaused') return 'peerErrPaused'; + const m = String((err && (err.errorMessage || err.message)) || '').toUpperCase(); + if (m.includes('PHONE_CODE_INVALID') || m.includes('PHONE_CODE_EMPTY')) return 'telegramErrCode'; + if (m.includes('PHONE_CODE_EXPIRED')) return 'telegramErrCodeExpired'; + if (m.includes('PASSWORD_HASH_INVALID')) return 'telegramErrPassword'; + if (m.includes('PHONE_NUMBER_INVALID') || m.includes('PHONE_NUMBER_BANNED')) return 'telegramErrPhone'; + if (m.includes('API_ID_INVALID') || m.includes('API_ID_PUBLISHED_FLOOD')) return 'telegramErrApi'; + if (m.includes('FLOOD')) return 'telegramErrFlood'; + if (m.includes('TELEGRAMERRTIMEOUT')) return 'telegramErrTimeout'; + if (m.includes('AUTH_KEY') || m.includes('SESSION_REVOKED') || m.includes('USER_DEACTIVATED') || m.includes('AUTH_USER_CANCEL')) return 'telegramErrAuth'; + return 'telegramErrConnect'; +}; +const tgHardError = (code) => code === 'telegramErrPhone' || code === 'telegramErrApi' || code === 'telegramErrFlood' || code === 'telegramErrAuth' || code === 'telegramErrConnect' || code === 'telegramErrTimeout'; + module.exports = ({ isPublic } = {}) => { const cache = new Map(); let mediaHosts = new Set(); @@ -82,7 +167,7 @@ module.exports = ({ isPublic } = {}) => { const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); try { const headers = Object.assign(spoofHeaders('application/json'), { Authorization: `Bearer ${token}` }, opts.headers || {}); - const res = await fetch(`${instance}${pathName}`, { ...opts, headers, signal: controller.signal }); + const res = await netFetch(`${instance}${pathName}`, { ...opts, headers, signal: controller.signal }); return res; } finally { clearTimeout(timer); @@ -287,8 +372,15 @@ module.exports = ({ isPublic } = {}) => { if (!file || !file.filepath) return null; let buf; try { buf = fs.readFileSync(file.filepath); } catch (_) { throw new Error('fediverseErrMedia'); } + return api.uploadMediaBuffer({ buffer: buf, mimetype: file.mimetype, filename: file.originalFilename }); + }, + + async uploadMediaBuffer({ buffer, mimetype, filename } = {}) { + const m = getMastodon(); + if (!m) throw new Error('fediverseErrAuth'); + if (!buffer || !buffer.length) return null; const fd = new FormData(); - fd.append('file', new Blob([buf], { type: file.mimetype || 'application/octet-stream' }), file.originalFilename || 'upload'); + fd.append('file', new Blob([buffer], { type: mimetype || 'application/octet-stream' }), filename || 'upload'); let res; try { res = await apiFetch(m.instance, m.token, '/api/v2/media', { method: 'POST', body: fd }); @@ -361,6 +453,656 @@ module.exports = ({ isPublic } = {}) => { favourite(id) { return this.action(id, 'favourite'); }, unfavourite(id) { return this.action(id, 'unfavourite'); }, + telegram: (() => { + const getTg = () => readStore().telegram || null; + let client = null; + let pending = null; + const entities = new Map(); + const messages = new Map(); + const mediaCache = new Map(); + const avatarCache = new Map(); + const dialogCache = { ts: 0, value: null }; + + const killClient = async (c) => { + if (!c) return; + try { await withTimeout(c.destroy(), 8000); } catch (_) { try { await c.disconnect(); } catch (__) {} } + }; + const newClient = (sessionStr, apiId, apiHash) => { + const { TelegramClient, StringSession, Logger, LogLevel } = loadGram(); + return new TelegramClient(new StringSession(sessionStr || ''), Number(apiId), String(apiHash), { + connectionRetries: 3, + baseLogger: new Logger(LogLevel.NONE) + }); + }; + const getClient = async () => { + const t = getTg(); + if (!t || !t.session) throw new Error('telegramErrAuth'); + if (networkPaused()) { + const c = client; + client = null; + await killClient(c); + throw new Error('peerErrPaused'); + } + if (client && client.connected) return client; + if (!client) client = newClient(t.session, t.apiId, t.apiHash); + await withTimeout(client.connect()); + return client; + }; + const rememberDialogs = (list) => { + for (const d of list) if (d && d.entity) entities.set(String(d.id), d.entity); + }; + const resolveEntity = async (c, id) => { + const key = String(id); + if (entities.has(key)) return entities.get(key); + try { rememberDialogs(await withTimeout(c.getDialogs({ limit: TIMELINE_LIMIT }))); } catch (_) {} + if (entities.has(key)) return entities.get(key); + const ent = await withTimeout(c.getEntity(Number(key))); + entities.set(key, ent); + return ent; + }; + const peerKind = (d) => (d.isUser ? 'user' : d.isChannel ? 'channel' : 'group'); + const kindOfEntity = (ent) => { + const { Api } = loadGram(); + if (ent instanceof Api.User) return 'user'; + if (ent instanceof Api.Channel) return ent.megagroup ? 'group' : 'channel'; + return 'group'; + }; + const mediaInfo = (msg) => { + const { Api } = loadGram(); + const m = msg && msg.media; + if (!m) return null; + if (m instanceof Api.MessageMediaPhoto) return { kind: 'photo', mime: 'image/jpeg', size: 0, name: '' }; + if (m instanceof Api.MessageMediaDocument && m.document && m.document.mimeType !== undefined) { + const mime = String(m.document.mimeType || 'application/octet-stream'); + const size = Number(m.document.size || 0); + const kind = mime.startsWith('image/') ? 'photo' : mime.startsWith('video/') ? 'video' : mime.startsWith('audio/') ? 'audio' : 'file'; + let name = ''; + for (const attr of (m.document.attributes || [])) if (attr && attr.fileName) name = String(attr.fileName); + return { kind, mime, size, name }; + } + return { kind: 'other', mime: '', size: 0, name: '' }; + }; + const toIso = (unix) => (unix ? new Date(Number(unix) * 1000).toISOString() : ''); + const mapMessage = (msg, chatId, senderNames) => { + const info = mediaInfo(msg); + const sid = msg.senderId ? String(msg.senderId) : ''; + return { + id: Number(msg.id), + chatId: String(chatId), + text: String(msg.message || ''), + date: toIso(msg.date), + out: msg.out === true, + senderId: sid, + senderName: senderNames.get(sid) || '', + media: info && info.kind !== 'other' && info.size <= TG_MEDIA_MAX_BYTES + ? { ...info, url: `/fediverse/telegram/media/${encodeURIComponent(String(chatId))}/${encodeURIComponent(String(msg.id))}` } + : null + }; + }; + const trimCache = (map) => { + while (map.size > TG_MEDIA_CACHE_MAX) map.delete(map.keys().next().value); + }; + const dropPending = async () => { + if (pending && pending.client && pending.client !== client) await killClient(pending.client); + pending = null; + }; + const waitStep = async (p, prevStep, ms = 25000) => { + const started = Date.now(); + while (Date.now() - started < ms) { + if (p.done || p.error || p.step !== prevStep || p.awaiting) return; + await sleep(200); + } + }; + + return { + hasAccount() { return !!(getTg() && getTg().session); }, + getAccount() { + const t = getTg(); + if (!t || !t.session) return null; + return { + network: 'telegram', + username: t.username || '', + displayName: t.displayName || t.username || t.phone || '', + phone: t.phone || '', + avatar: '/fediverse/telegram/avatar/me' + }; + }, + async getAccountStats() { + if (!this.hasAccount()) return null; + const data = await this.getDialogs(); + if (!data || data.error) return null; + return { + chats: data.dialogs.length, + unread: data.dialogs.reduce((sum, d) => sum + (Number(d.unread) || 0), 0) + }; + }, + loginState() { + if (!pending) return null; + if (Date.now() - pending.startedAt > TG_LOGIN_TTL_MS && !pending.done) { dropPending(); return null; } + return { step: pending.step, error: pending.error || '', done: pending.done === true }; + }, + async beginLogin({ apiId, apiHash, phone }) { + if (isPublic) throw new Error('fediverseErrPublic'); + const id = Number(String(apiId || '').trim()); + const hash = String(apiHash || '').trim(); + const ph = String(phone || '').trim(); + if (!id || !hash || !ph) throw new Error('telegramErrMissing'); + await dropPending(); + if (networkPaused()) throw new Error('peerErrPaused'); + const c = newClient('', id, hash); + try { await withTimeout(c.connect()); } catch (err) { throw new Error(tgErrCode(err)); } + const p = { client: c, apiId: id, apiHash: hash, phone: ph, step: 'sending', error: '', done: false, awaiting: false, startedAt: Date.now(), resolveCode: null, resolvePassword: null }; + pending = p; + p.promise = c.start({ + phoneNumber: async () => ph, + phoneCode: async () => new Promise((resolve) => { p.step = 'code'; p.awaiting = true; p.resolveCode = resolve; }), + password: async () => new Promise((resolve) => { p.step = 'password'; p.awaiting = true; p.resolvePassword = resolve; }), + onError: (err) => { + const code = tgErrCode(err); + p.error = code; + return tgHardError(code); + } + }).then(async () => { + const me = await withTimeout(c.getMe()); + const store = readStore(); + store.telegram = { + apiId: id, + apiHash: hash, + phone: ph, + session: c.session.save(), + userId: String(me.id || ''), + username: me.username || '', + displayName: tgDisplayName(me) + }; + writeStore(store); + if (client && client !== c) await killClient(client); + client = c; + entities.clear(); dialogCache.value = null; + p.done = true; p.step = 'done'; p.error = ''; + pending = null; + setModuleFlag('on'); + }).catch(async (err) => { + if (!p.error) p.error = tgErrCode(err); + p.done = true; p.step = 'error'; + await killClient(c); + }); + await waitStep(p, 'sending', 12000); + return this.loginState(); + }, + async submitCode(code) { + const p = pending; + if (!p || p.done || typeof p.resolveCode !== 'function') throw new Error('telegramErrNoLogin'); + const value = String(code || '').replace(/\s+/g, ''); + if (!value) throw new Error('telegramErrCode'); + const resolve = p.resolveCode; + p.resolveCode = null; p.awaiting = false; p.error = ''; + resolve(value); + await waitStep(p, 'code'); + return this.loginState(); + }, + async submitPassword(password) { + const p = pending; + if (!p || p.done || typeof p.resolvePassword !== 'function') throw new Error('telegramErrNoLogin'); + const value = String(password || ''); + if (!value) throw new Error('telegramErrPassword'); + const resolve = p.resolvePassword; + p.resolvePassword = null; p.awaiting = false; p.error = ''; + resolve(value); + await waitStep(p, 'password'); + return this.loginState(); + }, + async cancelLogin() { await dropPending(); }, + async goOffline() { + await dropPending(); + const c = client; + client = null; + await killClient(c); + }, + async disconnect() { + const { Api } = loadGram(); + try { + const c = await getClient(); + await withTimeout(c.invoke(new Api.auth.LogOut()), 8000); + } catch (_) {} + await killClient(client); + client = null; + const store = readStore(); + delete store.telegram; + writeStore(store); + entities.clear(); messages.clear(); mediaCache.clear(); avatarCache.clear(); dialogCache.value = null; + return true; + }, + invalidateCache() { dialogCache.value = null; }, + async getDialogs(force = false) { + if (!this.hasAccount()) return { connected: false, dialogs: [] }; + const now = Date.now(); + if (!force && dialogCache.value && now - dialogCache.ts < TIMELINE_CACHE_MS) return dialogCache.value; + let c; + try { c = await getClient(); } catch (err) { return { connected: true, account: this.getAccount(), dialogs: [], error: tgErrCode(err) }; } + let list; + try { list = await withTimeout(c.getDialogs({ limit: TIMELINE_LIMIT })); } catch (err) { return { connected: true, account: this.getAccount(), dialogs: [], error: tgErrCode(err) }; } + rememberDialogs(list); + const dialogs = list.map((d) => { + const id = String(d.id); + const last = d.message || null; + return { + id, + title: String(d.title || d.name || id), + kind: peerKind(d), + unread: Number(d.unreadCount || 0), + date: toIso(d.date || (last && last.date)), + last: last ? { text: String(last.message || ''), out: last.out === true, hasMedia: !!last.media } : null, + avatar: `/fediverse/telegram/avatar/${encodeURIComponent(id)}` + }; + }); + const value = { connected: true, account: this.getAccount(), dialogs }; + dialogCache.ts = now; dialogCache.value = value; + return value; + }, + async getChat(id) { + const c = await getClient(); + const entity = await resolveEntity(c, id); + const list = await withTimeout(c.getMessages(entity, { limit: TIMELINE_LIMIT })); + const senderNames = new Map(); + for (const msg of list) { + const sid = msg.senderId ? String(msg.senderId) : ''; + if (!sid || senderNames.has(sid)) continue; + try { senderNames.set(sid, tgDisplayName(await withTimeout(msg.getSender(), 8000))); } catch (_) { senderNames.set(sid, ''); } + } + for (const msg of list) messages.set(`${id}:${msg.id}`, msg); + trimCache(messages); + try { await withTimeout(c.markAsRead(entity), 8000); } catch (_) {} + dialogCache.value = null; + return { + id: String(id), + title: tgDisplayName(entity) || String(id), + kind: kindOfEntity(entity), + messages: list.slice().reverse().map((msg) => mapMessage(msg, id, senderNames)) + }; + }, + async sendMessage(id, { text, file } = {}) { + const c = await getClient(); + const entity = await resolveEntity(c, id); + const msg = String(text || '').trim(); + if (file && file.filepath) { + await withTimeout(c.sendFile(entity, { file: file.filepath, caption: msg, forceDocument: false }), 90000); + } else { + if (!msg) throw new Error('fediverseErrEmpty'); + await withTimeout(c.sendMessage(entity, { message: msg })); + } + dialogCache.value = null; + return true; + }, + async getMedia(chatId, msgId) { + const key = `${chatId}:${msgId}`; + if (mediaCache.has(key)) return mediaCache.get(key); + const c = await getClient(); + let msg = messages.get(key) || null; + if (!msg) { + const entity = await resolveEntity(c, chatId); + const arr = await withTimeout(c.getMessages(entity, { ids: [Number(msgId)] })); + msg = arr && arr[0] ? arr[0] : null; + } + if (!msg || !msg.media) return null; + const info = mediaInfo(msg); + if (!info || info.kind === 'other' || info.size > TG_MEDIA_MAX_BYTES) return null; + const buf = await withTimeout(c.downloadMedia(msg, {}), 90000); + if (!buf || !buf.length) return null; + const out = { contentType: info.mime || 'application/octet-stream', buffer: Buffer.from(buf), name: info.name }; + mediaCache.set(key, out); + trimCache(mediaCache); + return out; + }, + async getAvatar(id) { + const key = String(id); + if (avatarCache.has(key)) return avatarCache.get(key); + const c = await getClient(); + const entity = key === 'me' ? 'me' : await resolveEntity(c, key); + let buf = null; + try { buf = await withTimeout(c.downloadProfilePhoto(entity, { isBig: false }), 15000); } catch (_) { buf = null; } + const out = buf && buf.length ? { contentType: 'image/jpeg', buffer: Buffer.from(buf) } : null; + avatarCache.set(key, out); + trimCache(avatarCache); + return out; + } + }; + })(), + + + peertube: (() => { + const getPt = () => readStore().peertube || null; + const feedCache = new Map(); + const PT_UPLOAD_TIMEOUT_MS = 15 * 60 * 1000; + const PT_PRIVACY = { public: 1, unlisted: 2, private: 3 }; + + const fetchTimed = async (url, opts = {}, timeoutMs = FETCH_TIMEOUT_MS) => { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { return await netFetch(url, { ...opts, signal: controller.signal }); } finally { clearTimeout(timer); } + }; + const oauthClient = async (instance) => { + let res; + try { res = await fetchTimed(`${instance}/api/v1/oauth-clients/local`, { headers: spoofHeaders('application/json') }); } catch (_) { throw new Error('peertubeErrConnect'); } + if (!res.ok) throw new Error('peertubeErrConnect'); + const data = await res.json().catch(() => null); + if (!data || !data.client_id || !data.client_secret) throw new Error('peertubeErrConnect'); + return data; + }; + const tokenRequest = async (instance, params) => { + let res; + try { + res = await fetchTimed(`${instance}/api/v1/users/token`, { + method: 'POST', + headers: { ...spoofHeaders('application/json'), 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(params).toString() + }); + } catch (_) { throw new Error('peertubeErrConnect'); } + if (res.status === 400 || res.status === 401 || res.status === 403) throw new Error('peertubeErrAuth'); + if (!res.ok) throw new Error('peertubeErrConnect'); + const data = await res.json().catch(() => null); + if (!data || !data.access_token) throw new Error('peertubeErrAuth'); + return data; + }; + const saveTokens = (pt, data) => { + const store = readStore(); + if (!store.peertube) return null; + store.peertube = { ...store.peertube, token: data.access_token, refreshToken: data.refresh_token || pt.refreshToken || '', expiresAt: Date.now() + (Number(data.expires_in) || 0) * 1000 }; + writeStore(store); + return store.peertube; + }; + const refreshAccess = async (pt) => { + if (!pt || !pt.refreshToken) return null; + try { + const data = await tokenRequest(pt.instance, { client_id: pt.clientId, client_secret: pt.clientSecret, grant_type: 'refresh_token', refresh_token: pt.refreshToken }); + return saveTokens(pt, data); + } catch (_) { return null; } + }; + const ptFetch = async (pathName, opts = {}, retry = true) => { + let pt = getPt(); + if (!pt) throw new Error('peertubeErrAuth'); + if (pt.expiresAt && Date.now() > pt.expiresAt - 60000 && retry) { const fresh = await refreshAccess(pt); if (fresh) pt = fresh; } + const headers = Object.assign(spoofHeaders('application/json'), { Authorization: `Bearer ${pt.token}` }, opts.headers || {}); + let res; + try { res = await fetchTimed(`${pt.instance}${pathName}`, { method: opts.method || 'GET', body: opts.body, headers }, opts.timeoutMs || FETCH_TIMEOUT_MS); } catch (_) { throw new Error('peertubeErrConnect'); } + if (res.status === 401 && retry) { + const fresh = await refreshAccess(pt); + if (fresh) return ptFetch(pathName, opts, false); + } + return res; + }; + const absUrl = (pt, p) => { + if (typeof p !== 'string' || !p) return ''; + if (/^https?:\/\//i.test(p)) return p; + return `${pt.instance}${p.startsWith('/') ? '' : '/'}${p}`; + }; + const rememberHost = (url) => { try { mediaHosts.add(new URL(url).host); } catch (_) {} }; + const avatarOf = (pt, obj) => { + const list = Array.isArray(obj && obj.avatars) ? obj.avatars.slice().sort((a, b) => (Number(b.width) || 0) - (Number(a.width) || 0)) : []; + const p = list.length ? list[0].path : (obj && obj.avatar && obj.avatar.path) || ''; + return p ? proxify(absUrl(pt, p)) : ''; + }; + const channelOf = (pt, ch) => { + if (!ch || typeof ch !== 'object') return { name: '', displayName: '', host: '', handle: '', avatar: '', url: '' }; + const host = ch.host || (() => { try { return new URL(pt.instance).host; } catch (_) { return ''; } })(); + return { name: ch.name || '', displayName: ch.displayName || ch.name || '', host, handle: ch.name ? `${ch.name}@${host}` : '', avatar: avatarOf(pt, ch), url: ch.url || (ch.name ? `${pt.instance}/c/${ch.name}` : '') }; + }; + const mapVideo = (pt, v) => { + if (!v || typeof v !== 'object') return null; + const id = String(v.uuid || v.shortUUID || v.id || ''); + return { + id, + numericId: v.id, + name: typeof v.name === 'string' ? v.name : '', + description: typeof v.description === 'string' ? v.description : '', + thumbnail: proxify(absUrl(pt, v.thumbnailPath || v.previewPath || '')), + duration: Number(v.duration) || 0, + views: Number(v.views) || 0, + likes: Number(v.likes) || 0, + dislikes: Number(v.dislikes) || 0, + publishedAt: v.publishedAt || v.createdAt || '', + channel: channelOf(pt, v.channel), + url: typeof v.url === 'string' && v.url ? v.url : `${pt.instance}/w/${id}`, + privacy: v.privacy && v.privacy.label ? String(v.privacy.label) : '', + privacyId: v.privacy && v.privacy.id ? Number(v.privacy.id) : 0, + state: v.state && v.state.label ? String(v.state.label) : '', + isLive: v.isLive === true, + nsfw: v.nsfw === true + }; + }; + const pickFile = (v) => { + const candidates = []; + for (const f of (Array.isArray(v.files) ? v.files : [])) if (f && f.fileUrl) candidates.push(f); + for (const pl of (Array.isArray(v.streamingPlaylists) ? v.streamingPlaylists : [])) for (const f of (Array.isArray(pl.files) ? pl.files : [])) if (f && f.fileUrl) candidates.push(f); + if (!candidates.length) return null; + const resOf = (f) => Number(f.resolution && f.resolution.id) || 0; + const under = candidates.filter(f => resOf(f) > 0 && resOf(f) <= 720).sort((a, b) => resOf(b) - resOf(a)); + const pick = under[0] || candidates.slice().sort((a, b) => resOf(a) - resOf(b))[0]; + return { url: pick.fileUrl, resolution: resOf(pick), size: Number(pick.size) || 0 }; + }; + const mapComment = (pt, c) => c && typeof c === 'object' ? { + id: String(c.id || ''), + html: typeof c.text === 'string' ? c.text : '', + createdAt: c.createdAt || '', + totalReplies: Number(c.totalReplies) || 0, + account: c.account ? { name: c.account.name || '', displayName: c.account.displayName || c.account.name || '', host: c.account.host || '', avatar: avatarOf(pt, c.account) } : { name: '', displayName: '', host: '', avatar: '' } + } : null; + const listOf = async (pathName) => { + const res = await ptFetch(pathName); + if (res.status === 401 || res.status === 403) return { error: 'peertubeErrAuth' }; + if (!res.ok) return { error: 'peertubeErrFetch' }; + const data = await res.json().catch(() => null); + return { total: Number(data && data.total) || 0, data: Array.isArray(data && data.data) ? data.data : [] }; + }; + const invalidatePt = () => feedCache.clear(); + + return { + hasAccount() { return !!(getPt() && getPt().token); }, + getAccount() { + const pt = getPt(); + if (!pt || !pt.token) return null; + let host = ''; + try { host = new URL(pt.instance).host; } catch (_) {} + return { + network: 'peertube', + instance: pt.instance, + host, + username: pt.username || '', + displayName: pt.displayName || pt.username || '', + handle: pt.username ? `${pt.username}@${host}` : host, + avatar: pt.avatar ? proxify(pt.avatar) : '', + channel: pt.channelName || '', + channelDisplayName: pt.channelDisplayName || pt.channelName || '', + channelUrl: pt.channelName ? `${pt.instance}/c/${pt.channelName}` : pt.instance, + profileUrl: pt.username ? `${pt.instance}/a/${pt.username}` : pt.instance + }; + }, + async connect({ instance, username, password }) { + if (isPublic) throw new Error('fediverseErrPublic'); + const origin = (() => { try { return normalizeInstance(instance); } catch (_) { throw new Error('peertubeErrInstance'); } })(); + const user = String(username || '').trim(); + const pass = String(password || ''); + if (!user || !pass) throw new Error('peertubeErrMissing'); + const client = await oauthClient(origin); + const tokens = await tokenRequest(origin, { client_id: client.client_id, client_secret: client.client_secret, grant_type: 'password', response_type: 'code', username: user, password: pass }); + let me; + try { + const res = await fetchTimed(`${origin}/api/v1/users/me`, { headers: Object.assign(spoofHeaders('application/json'), { Authorization: `Bearer ${tokens.access_token}` }) }); + if (!res.ok) throw new Error('peertubeErrAuth'); + me = await res.json(); + } catch (_) { throw new Error('peertubeErrConnect'); } + const channel = Array.isArray(me.videoChannels) && me.videoChannels.length ? me.videoChannels[0] : null; + const acc = me.account || {}; + const avatars = Array.isArray(acc.avatars) ? acc.avatars.slice().sort((a, b) => (Number(b.width) || 0) - (Number(a.width) || 0)) : []; + const avatarPath = avatars.length ? avatars[0].path : (acc.avatar && acc.avatar.path) || ''; + const store = readStore(); + store.peertube = { + instance: origin, + clientId: client.client_id, + clientSecret: client.client_secret, + token: tokens.access_token, + refreshToken: tokens.refresh_token || '', + expiresAt: Date.now() + (Number(tokens.expires_in) || 0) * 1000, + userId: String(me.id || ''), + username: me.username || user, + displayName: acc.displayName || me.username || user, + avatar: avatarPath ? `${origin}${avatarPath.startsWith('/') ? '' : '/'}${avatarPath}` : '', + accountName: acc.name || me.username || user, + channelId: channel ? String(channel.id) : '', + channelName: channel ? channel.name || '' : '', + channelDisplayName: channel ? channel.displayName || channel.name || '' : '' + }; + writeStore(store); + invalidatePt(); + try { mediaHosts.add(new URL(origin).host); } catch (_) {} + setModuleFlag('on'); + return this.getAccount(); + }, + async disconnect() { + const pt = getPt(); + if (pt && pt.token) { + try { await fetchTimed(`${pt.instance}/api/v1/users/revoke-token`, { method: 'POST', headers: Object.assign(spoofHeaders('application/json'), { Authorization: `Bearer ${pt.token}` }) }); } catch (_) {} + } + const store = readStore(); + delete store.peertube; + writeStore(store); + invalidatePt(); + return true; + }, + invalidateCache() { invalidatePt(); }, + async getAccountStats() { + const pt = getPt(); + if (!pt) return null; + let me = null; + try { const res = await ptFetch('/api/v1/users/me'); if (res.ok) me = await res.json().catch(() => null); } catch (_) {} + if (!me) return null; + const channel = Array.isArray(me.videoChannels) && me.videoChannels.length ? me.videoChannels[0] : null; + let videos = 0, subscriptions = 0; + try { const v = await listOf('/api/v1/users/me/videos?start=0&count=1'); if (!v.error) videos = v.total; } catch (_) {} + try { const sub = await listOf('/api/v1/users/me/subscriptions?start=0&count=1'); if (!sub.error) subscriptions = sub.total; } catch (_) {} + return { + videos, + subscriptions, + followers: channel ? Number(channel.followersCount) || 0 : 0, + quota: Number(me.videoQuota) || 0, + quotaUsed: Number(me.videoQuotaUsed) || 0 + }; + }, + async getFeed(kind = 'subscriptions', force = false) { + const pt = getPt(); + if (!pt) return { connected: false, videos: [] }; + try { mediaHosts.add(new URL(pt.instance).host); } catch (_) {} + const key = kind === 'mine' ? 'mine' : 'subscriptions'; + const now = Date.now(); + const hit = feedCache.get(key); + if (!force && hit && now - hit.ts < TIMELINE_CACHE_MS) return hit.value; + let result; + try { + result = await listOf(key === 'mine' ? `/api/v1/users/me/videos?start=0&count=${TIMELINE_LIMIT}&sort=-publishedAt` : `/api/v1/users/me/subscriptions/videos?start=0&count=${TIMELINE_LIMIT}&sort=-publishedAt`); + } catch (err) { + return { connected: true, account: this.getAccount(), videos: [], error: (err && err.message) || 'peertubeErrFetch' }; + } + if (result.error) return { connected: true, account: this.getAccount(), videos: [], error: result.error }; + const value = { connected: true, account: this.getAccount(), videos: result.data.map(v => mapVideo(pt, v)).filter(Boolean), total: result.total, kind: key }; + feedCache.set(key, { ts: now, value }); + return value; + }, + async getVideo(id) { + const pt = getPt(); + if (!pt) return null; + const vid = encodeURIComponent(String(id || '')); + let res; + try { res = await ptFetch(`/api/v1/videos/${vid}`); } catch (_) { return { error: 'peertubeErrFetch' }; } + if (!res.ok) return { error: 'peertubeErrFetch' }; + const raw = await res.json().catch(() => null); + const video = mapVideo(pt, raw); + if (!video) return { error: 'peertubeErrFetch' }; + const file = pickFile(raw || {}); + if (file) { rememberHost(file.url); video.stream = `/fediverse/peertube/stream?u=${encodeURIComponent(file.url)}`; video.resolution = file.resolution; video.fileSize = file.size; } + video.embedUrl = raw && raw.embedPath ? absUrl(pt, raw.embedPath) : ''; + video.tags = Array.isArray(raw && raw.tags) ? raw.tags.map(String) : []; + let comments = []; + try { + const c = await listOf(`/api/v1/videos/${vid}/comment-threads?start=0&count=${TIMELINE_LIMIT}&sort=-createdAt`); + if (!c.error) comments = c.data.map(x => mapComment(pt, x)).filter(Boolean); + } catch (_) {} + let myRating = 'none'; + try { const r = await ptFetch(`/api/v1/users/me/videos/${vid}/rating`); if (r.ok) { const d = await r.json().catch(() => null); if (d && d.rating) myRating = String(d.rating); } } catch (_) {} + return { video, comments, myRating }; + }, + async rate(id, rating) { + const allowed = { like: 1, dislike: 1, none: 1 }; + if (!allowed[rating]) throw new Error('peertubeErrFetch'); + let res; + try { res = await ptFetch(`/api/v1/videos/${encodeURIComponent(String(id || ''))}/rate`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rating }) }); } catch (_) { throw new Error('peertubeErrConnect'); } + if (res.status === 401 || res.status === 403) throw new Error('peertubeErrAuth'); + if (!res.ok && res.status !== 204) throw new Error('peertubeErrFetch'); + invalidatePt(); + return true; + }, + async comment(id, text) { + const body = String(text || '').trim(); + if (!body) throw new Error('fediverseErrEmpty'); + let res; + try { res = await ptFetch(`/api/v1/videos/${encodeURIComponent(String(id || ''))}/comment-threads`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: body }) }); } catch (_) { throw new Error('peertubeErrConnect'); } + if (res.status === 401 || res.status === 403) throw new Error('peertubeErrAuth'); + if (!res.ok) throw new Error('peertubeErrComment'); + return true; + }, + async upload({ file, name, description, privacy } = {}) { + const pt = getPt(); + if (!pt) throw new Error('peertubeErrAuth'); + if (!file || !file.filepath || !(Number(file.size) > 0)) throw new Error('peertubeErrNoFile'); + const title = String(name || '').trim(); + if (!title) throw new Error('peertubeErrTitle'); + let buf; + try { buf = fs.readFileSync(file.filepath); } catch (_) { throw new Error('peertubeErrUpload'); } + const fd = new FormData(); + fd.append('videofile', new Blob([buf], { type: file.mimetype || 'video/mp4' }), file.originalFilename || 'video.mp4'); + if (pt.channelId) fd.append('channelId', String(pt.channelId)); + fd.append('name', title.slice(0, 120)); + fd.append('privacy', String(PT_PRIVACY[String(privacy || 'public')] || 1)); + const desc = String(description || '').trim(); + if (desc) fd.append('description', desc.slice(0, 10000)); + let res; + try { res = await ptFetch('/api/v1/videos/upload', { method: 'POST', body: fd, timeoutMs: PT_UPLOAD_TIMEOUT_MS }); } catch (_) { throw new Error('peertubeErrUpload'); } + if (res.status === 401 || res.status === 403) throw new Error('peertubeErrAuth'); + if (!res.ok) throw new Error('peertubeErrUpload'); + const data = await res.json().catch(() => ({})); + invalidatePt(); + const v = data && data.video ? data.video : {}; + return { id: String(v.uuid || v.shortUUID || v.id || '') }; + }, + async openStream(remoteUrl, range) { + let u; + try { u = new URL(String(remoteUrl || '')); } catch (_) { return null; } + if (u.protocol !== 'https:' || isPrivateHost(u.hostname) || !api.isHostAllowed(u.host)) return null; + const headers = spoofHeaders('*/*'); + if (range) headers.Range = String(range); + let res = null; + let current = u; + for (let hop = 0; hop < 5; hop++) { + try { res = await publicFetch(current.href, { headers, redirect: 'manual' }); } catch (_) { return null; } + if (![301, 302, 303, 307, 308].includes(res.status)) break; + let next; + try { next = new URL(res.headers.get('location') || '', current); } catch (_) { return null; } + if (next.protocol !== 'https:' || isPrivateHost(next.hostname)) return null; + current = next; + res = null; + } + if (!res || (!res.ok && res.status !== 206)) return null; + const type = res.headers.get('content-type') || 'application/octet-stream'; + if (!/^(video|audio|application\/octet-stream)/i.test(type)) return null; + return { + status: res.status, + type, + length: res.headers.get('content-length') || '', + contentRange: res.headers.get('content-range') || '', + body: res.body ? Readable.fromWeb(res.body) : null + }; + } + }; + })(), + isHostAllowed(host) { if (mediaHosts.has(host)) return true; const m = getMastodon(); @@ -368,6 +1110,10 @@ module.exports = ({ isPublic } = {}) => { try { if (new URL(m.instance).host === host) return true; } catch (_) {} try { if (m.avatar && new URL(m.avatar).host === host) return true; } catch (_) {} } + const pt = readStore().peertube; + if (pt && pt.instance) { + try { if (new URL(pt.instance).host === host) return true; } catch (_) {} + } return false; }, @@ -382,7 +1128,7 @@ module.exports = ({ isPublic } = {}) => { try { let current = u, res = null; for (let hop = 0; hop < 5; hop++) { - res = await fetch(current.href, { signal: controller.signal, redirect: 'manual', headers: spoofHeaders('image/avif,image/webp,*/*') }); + res = await publicFetch(current.href, { signal: controller.signal, redirect: 'manual', headers: spoofHeaders('image/avif,image/webp,*/*') }); if (res.status < 300 || res.status >= 400) break; const loc = res.headers.get('location'); if (!loc) return null; @@ -395,8 +1141,8 @@ module.exports = ({ isPublic } = {}) => { if (!res || !res.ok) return null; const ct = res.headers.get('content-type') || 'application/octet-stream'; if (!/^(image|video|audio)\//i.test(ct)) return null; - const ab = await res.arrayBuffer(); - return { contentType: ct, buffer: Buffer.from(ab) }; + const buffer = await readCapped(res, PROXY_MEDIA_MAX_BYTES); + return buffer ? { contentType: ct, buffer } : null; } catch (_) { return null; } finally { @@ -407,3 +1153,5 @@ module.exports = ({ isPublic } = {}) => { return api; }; + +module.exports.isPrivateHost = isPrivateHost; diff --git a/src/models/feed_model.js b/src/models/feed_model.js index 80f894b..e37d5c9 100644 --- a/src/models/feed_model.js +++ b/src/models/feed_model.js @@ -2,7 +2,8 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, mergeDuplicatesBy, norm } = require('../backend/dedupe'); +const { readTyped } = require('./typed_log'); +const { dedupeBy, mergeDuplicatesBy, norm, versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const FEED_TEXT_MIN = Number(getConfig().feed?.minLength ?? 1); @@ -15,10 +16,10 @@ module.exports = ({ cooler }) => { return ssb; }; - const cleanText = (t) => (typeof t === "string" ? t.trim() : ""); + const cleanText = (t) => (typeof t === "string" ? t.replace(/\r\n?/g, "\n").trim() : ""); const isValidFeedText = (t) => { - const s = cleanText(t); + const s = cleanText(t).replace(/!?\[[^\]\n]*\]\(\s*&[^)\s]+\.sha256\s*\)/g, "").trim(); return s.length >= FEED_TEXT_MIN && s.length <= FEED_TEXT_MAX; }; @@ -27,10 +28,9 @@ module.exports = ({ cooler }) => { ssbClient.get(id, (err, val) => (err ? reject(err) : resolve({ key: id, value: val }))); }); - const getAllMessages = (ssbClient) => - new Promise((resolve, reject) => { - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))); - }); + const FEED_TYPES = ["feed", "feedOpinion", "feed-action", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, FEED_TYPES, { limit: logLimit }); const extractTags = (text) => { const list = (String(text || "").match(/#[A-Za-z0-9_]{1,32}/g) || []).map((t) => t.slice(1).toLowerCase()); @@ -40,11 +40,8 @@ module.exports = ({ cooler }) => { const buildIndex = async (ssbClient) => { const messages = await getAllMessages(ssbClient); - const forward = new Map(); - const replacedIds = new Set(); const tombstoned = buildValidatedTombstoneSet(messages); const feedsById = new Map(); - const authorById = new Map(); const actions = []; const opinionMsgs = []; @@ -59,7 +56,6 @@ module.exports = ({ cooler }) => { } if (c.type === "feed") { feedsById.set(k, msg); - authorById.set(k, msg?.value?.author); continue; } if (c.type === "feed-action") { @@ -68,24 +64,8 @@ module.exports = ({ cooler }) => { } } - for (const [k, msg] of feedsById) { - const t = msg?.value?.content?.replaces; - if (!t) continue; - if (authorById.get(t) === authorById.get(k)) { - forward.set(t, k); - replacedIds.add(t); - } - } - - const resolve = (id) => { - let cur = id; - const seen = new Set(); - while (forward.has(cur) && !seen.has(cur)) { - seen.add(cur); - cur = forward.get(cur); - } - return cur; - }; + const vg = versionGroups(Array.from(feedsById.values()).map(versionEntry)); + const resolve = (id) => vg.tipOf(id); const actionsByRoot = new Map(); for (const a of actions) { @@ -105,7 +85,7 @@ module.exports = ({ cooler }) => { opinionsByTip.get(tip).push(op); } - return { resolve, tombstoned, feedsById, replacedIds, actionsByRoot, opinionsByTip }; + return { resolve, vg, tombstoned, feedsById, actionsByRoot, opinionsByTip }; }; const resolveCurrentId = async (id) => { @@ -114,7 +94,7 @@ module.exports = ({ cooler }) => { return idx.resolve(id); }; - const createFeed = async (text, mentions) => { + const createFeed = async (text, mentions, media = null) => { const ssbClient = await openSsb(); const userId = ssbClient.id; @@ -129,7 +109,7 @@ module.exports = ({ cooler }) => { const content = { type: "feed", - text: cleaned, + text: typeof media === "string" && media.trim() ? `${cleaned}\n${media.trim()}` : cleaned, author: userId, createdAt: new Date().toISOString(), tags: extractTags(cleaned), @@ -146,6 +126,7 @@ module.exports = ({ cooler }) => { const userId = ssbClient.id; const idx = await buildIndex(ssbClient); + if (idx.vg.isForeignCopy(idx.vg.rootOf(contentId))) throw new Error("Invalid feed"); const tipId = idx.resolve(contentId); let msg; @@ -216,6 +197,7 @@ module.exports = ({ cooler }) => { const userId = ssbClient.id; const idx = await buildIndex(ssbClient); + if (idx.vg.isForeignCopy(idx.vg.rootOf(contentId))) throw new Error("Invalid feed"); const tipId = idx.resolve(contentId); const msg = idx.feedsById.get(tipId); @@ -249,30 +231,22 @@ module.exports = ({ cooler }) => { return !!c && c.type === "feed" && isValidFeedText(c.text); }; - let tips = Array.from(idx.feedsById.values()).filter( + let tips = Array.from(idx.vg.roots).map((r) => idx.feedsById.get(idx.resolve(r))).filter( (m) => - !idx.replacedIds.has(m.key) && + !!m && !idx.tombstoned.has(m.key) && isValidFeedMsg(m) ); const textEditedEver = (m) => { - const seen = new Set(); - let cur = m; - let lastText = cur?.value?.content?.text; - while (cur?.value?.content?.replaces) { - const prevId = cur.value.content.replaces; - if (!prevId || seen.has(prevId)) break; - seen.add(prevId); - const prev = idx.feedsById.get(prevId); - if (!prev) break; - if (prev?.value?.author !== cur?.value?.author) break; - const prevText = prev?.value?.content?.text; - if (typeof lastText === "string" && typeof prevText === "string" && lastText !== prevText) return true; - cur = prev; - lastText = prevText; + const root = idx.vg.rootOf(m.key); + const owner = idx.feedsById.get(root)?.value?.author; + const texts = new Set(); + for (const k of (idx.vg.groups.get(root) || [])) { + const v = idx.feedsById.get(k)?.value; + if (v && v.author === owner && typeof v.content?.text === "string") texts.add(v.content.text); } - return false; + return texts.size > 1; }; const materialize = (feedMsg) => { @@ -300,7 +274,7 @@ module.exports = ({ cooler }) => { } if (ac.action === "comment") { - commentCount++; + if (!idx.tombstoned.has(a.key)) commentCount++; continue; } } @@ -367,7 +341,7 @@ module.exports = ({ cooler }) => { if (filter === "MINE") { feeds = feeds.filter((m) => (m.value?.author || m.value?.content?.author) === userId); - } else if (filter === "TODAY") { + } else if (filter === "RECENT" || filter === "TODAY") { feeds = feeds.filter((m) => now - getTs(m) < 86400000); } @@ -388,7 +362,7 @@ module.exports = ({ cooler }) => { const ssbClient = await openSsb(); const idx = await buildIndex(ssbClient); const currentId = idx.resolve(feedId); - if (idx.tombstoned.has(currentId)) return null; + if (idx.tombstoned.has(currentId) || idx.vg.isForeignCopy(idx.vg.rootOf(currentId))) return null; const msg = idx.feedsById.get(currentId); if (!msg) return null; const actions = idx.actionsByRoot.get(currentId) || []; @@ -402,7 +376,7 @@ module.exports = ({ cooler }) => { refeeds++; if (ac.author || a?.value?.author) refeedsInhabitants.push(ac.author || a.value.author); } - if (ac.type === "feed-action" && ac.action === "comment") { + if (ac.type === "feed-action" && ac.action === "comment" && !idx.tombstoned.has(a.key)) { commentCount++; } } @@ -417,7 +391,7 @@ module.exports = ({ cooler }) => { const currentId = idx.resolve(feedId); const actions = idx.actionsByRoot.get(currentId) || []; return actions - .filter(a => a?.value?.content?.type === "feed-action" && a?.value?.content?.action === "comment") + .filter(a => a?.value?.content?.type === "feed-action" && a?.value?.content?.action === "comment" && !idx.tombstoned.has(a.key)) .sort((a, b) => (a?.value?.timestamp || 0) - (b?.value?.timestamp || 0)); }; @@ -430,6 +404,15 @@ module.exports = ({ cooler }) => { }); }; - return { createFeed, createRefeed, addOpinion, listFeeds, resolveCurrentId, getFeedById, getComments, addComment }; + const deleteFeedById = async (id) => { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const currentId = (await resolveCurrentId(id)) || id; + const item = await new Promise((res, rej) => ssbClient.get(currentId, (e, m) => (e || !m || !m.content) ? rej(new Error("Feed not found")) : res(m))); + if (item.author !== userId && item.content?.author !== userId) throw new Error("Not the author"); + return new Promise((res, rej) => ssbClient.publish({ type: "tombstone", target: currentId, deletedAt: new Date().toISOString(), author: userId }, (e, m) => e ? rej(e) : res(m))); + }; + + return { createFeed, createRefeed, addOpinion, listFeeds, resolveCurrentId, getFeedById, getComments, addComment, deleteFeedById }; }; diff --git a/src/models/files_model.js b/src/models/files_model.js new file mode 100644 index 0000000..94acd6a --- /dev/null +++ b/src/models/files_model.js @@ -0,0 +1,387 @@ +const pull = require("../server/node_modules/pull-stream"); +const { getConfig } = require("../configs/config-manager.js"); +const categories = require("../backend/opinion_categories"); +const { readTyped, memoIndex } = require("./typed_log"); + +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const safeArr = (v) => (Array.isArray(v) ? v : []); + +const normalizeTags = (raw) => { + if (raw === undefined || raw === null) return undefined; + if (Array.isArray(raw)) return raw.map((t) => String(t || "").trim()).filter(Boolean); + return String(raw).split(",").map((t) => t.trim()).filter(Boolean); +}; + +const fileExtra = (extra) => { + if (!extra || typeof extra !== "object") return {}; + const out = {}; + if (extra.mime) out.mime = String(extra.mime); + if (extra.fileName) out.fileName = String(extra.fileName); + if (typeof extra.torrentKey === "string" && extra.torrentKey.startsWith("%")) out.torrentKey = extra.torrentKey; + if (typeof extra.torrentUrl === "string" && extra.torrentUrl.startsWith("&")) out.torrentUrl = extra.torrentUrl; + if (extra.cipher && typeof extra.cipher === "object" && extra.cipher.key && extra.cipher.manifestBlobId) out.cipher = { v: Number(extra.cipher.v) || 1, key: String(extra.cipher.key), manifestBlobId: String(extra.cipher.manifestBlobId), chunkCount: Number(extra.cipher.chunkCount) || 0 }; + return out; +}; + +const parseBlobId = (blobMarkdown) => { + const s = String(blobMarkdown || ""); + const match = s.match(/\((&[^)]+\.sha256)\)/); + if (match) return match[1]; + const fallback = s.match(/\(([^)]+)\)/g); + return fallback ? fallback[fallback.length - 1].slice(1, -1) : s || null; +}; + +const voteSum = (opinions = {}) => + Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0); + +module.exports = ({ cooler, tribeCrypto, tribesModel }) => { + let ssb; + + const openSsb = async () => { + if (!ssb) ssb = await cooler.open(); + return ssb; + }; + + const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null; + const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c; + const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c; + const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {}; + const decryptIndexNodes = tribeHelpers ? tribeHelpers.decryptIndexNodes : async () => {}; + const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'file') : msgs; + const tombFor = async (target, tribeId, author) => tribeHelpers ? tribeHelpers.encryptTombstone(target, tribeId, author) : { type: 'tombstone', target, deletedAt: new Date().toISOString(), author }; + + const FILE_TYPES = ["file", "fileOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, FILE_TYPES, { limit: logLimit, withWindow: true }); + + const getMsg = async (ssbClient, key) => + new Promise((resolve) => { + ssbClient.get(key, (err, msg) => (err ? resolve(null) : resolve(msg))); + }); + + const buildIndex = (messages) => { + const tomb = new Set(); + const nodes = new Map(); + const authorByKey = new Map(); + const tombRequests = []; + const opinionMsgs = []; + + for (const m of messages) { + const k = m.key; + const v = m.value || {}; + const c = v.content; + if (!c) continue; + + if (c.type === "tombstone" && c.target) { + tombRequests.push({ target: c.target, author: v.author }); + continue; + } + + if (c.type === "fileOpinion") { opinionMsgs.push({ target: c.target, author: v.author, category: c.category }); continue; } + + if (c.type !== "file") continue; + + const ts = v.timestamp || m.timestamp || 0; + let sizeBytes = 0; + try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); + authorByKey.set(k, v.author); + } + + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; + + for (const t of tombRequests) { + const targetAuthor = authorByKey.get(t.target); + if (targetAuthor && t.author === targetAuthor) tomb.add(t.target); + } + + const opinionsByRoot = new Map(); + for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } + + const tipByRoot = new Map(); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); + + const resolveGroup = (root) => { + const contentTip = contentTipOf(root); + const contentNode = nodes.get(contentTip) || nodes.get(root); + const lc = contentNode ? contentNode.c : {}; + const opinions = { ...(lc.opinions || {}) }; + const voters = safeArr(lc.opinions_inhabitants).slice(); + const voterSet = new Set(voters); + for (const op of (opinionsByRoot.get(root) || [])) { + if (voterSet.has(op.author)) continue; + voterSet.add(op.author); voters.push(op.author); + opinions[op.category] = (opinions[op.category] || 0) + 1; + } + return { contentTip, contentNode, opinions, voters }; + }; + + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; + }; + + const buildFile = (node, rootId, viewerId, agg) => { + const c = node.c || {}; + const undec = c.encryptedPayload && c._decrypted === false; + const opinions = agg ? agg.opinions : (c.opinions || {}); + const voters = agg ? agg.voters : safeArr(c.opinions_inhabitants); + return { + key: node.key, + rootId, + url: undec ? "" : c.url, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + tags: safeArr(c.tags), + author: node.author, + title: undec ? "" : (c.title || ""), + description: undec ? "" : (c.description || ""), + size: c.size || 0, + mime: undec ? "" : (c.mime || ""), + fileName: undec ? "" : (c.fileName || ""), + torrentKey: undec ? "" : (typeof c.torrentKey === "string" && c.torrentKey.startsWith("%") ? c.torrentKey : ""), + torrentUrl: undec ? "" : (typeof c.torrentUrl === "string" && c.torrentUrl.startsWith("&") ? c.torrentUrl : ""), + cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null), + opinions, + opinions_inhabitants: voters, + hasVoted: viewerId ? voters.includes(viewerId) : false, + tribeId: c.tribeId || null, + encrypted: !!undec, + sizeBytes: node.sizeBytes || 0 + }; + }; + + + return { + type: "file", + + async resolveCurrentId(id) { + const ssbClient = await openSsb(); + const messages = await getAllMessages(ssbClient); + const idx = buildIndex(unwrapForIndex(messages)); + + const tip = idx.tipByRoot.get(idx.rootOf(id)) || id; + if (idx.tomb.has(tip)) throw new Error("File not found"); + return tip; + }, + + async resolveRootId(id) { + const ssbClient = await openSsb(); + const messages = await getAllMessages(ssbClient); + const idx = buildIndex(unwrapForIndex(messages)); + + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("File not found"); + return root; + }, + + async createFile(blobMarkdown, tagsRaw, title, description, size, tribeId, extra = null) { + const ssbClient = await openSsb(); + const blobId = parseBlobId(blobMarkdown); + const tags = normalizeTags(tagsRaw) || []; + const now = new Date().toISOString(); + + let content = { + type: "file", + url: blobId, + createdAt: now, + updatedAt: null, + author: ssbClient.id, + tags, + title: title || "", + description: description || "", + size: Number(size) || 0, + opinions: {}, + opinions_inhabitants: [], + ...(tribeId ? { tribeId } : {}), + ...fileExtra(extra) + }; + + content = await encryptIfTribe(content); + + return new Promise((resolve, reject) => { + ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res))); + }); + }, + + async createLink(url, tagsRaw, title, description, size, extra = null) { + const ssbClient = await openSsb(); + const link = String(url || '').trim(); + if (!/^https?:\/\//i.test(link)) throw new Error('Invalid link'); + const content = { + type: "file", + url: link, + createdAt: new Date().toISOString(), + updatedAt: null, + author: ssbClient.id, + tags: normalizeTags(tagsRaw) || [], + title: title || "", + description: description || "", + size: Number(size) || 0, + opinions: {}, + opinions_inhabitants: [], + ...fileExtra(extra) + }; + return new Promise((resolve, reject) => { + ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res))); + }); + }, + + async updateFileById(id, blobMarkdown, tagsRaw, title, description, extra = null) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const tipId = await this.resolveCurrentId(id); + const oldMsg = await getMsg(ssbClient, tipId); + + if (!oldMsg) throw new Error("File not found"); + const oldDec = await decryptIfTribe(oldMsg.content); + if (!oldDec || oldDec.type !== "file") throw new Error("File not found"); + assertReadable(oldDec, "File"); + if (oldMsg.author !== userId) throw new Error("Not the author"); + const aggFile = await this.getFileById(id, userId); + if (aggFile && Object.keys(aggFile.opinions || {}).some(k => (aggFile.opinions[k] || 0) > 0)) throw new Error("Cannot edit file after it has received opinions."); + + const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldDec.tags); + const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null; + const now = new Date().toISOString(); + + let updated = { + type: "file", + replaces: tipId, + url: blobId || oldDec.url, + tags, + title: title !== undefined ? title || "" : oldDec.title || "", + description: description !== undefined ? description || "" : oldDec.description || "", + size: extra && Number(extra.size) > 0 ? Number(extra.size) : (oldDec.size || 0), + ...(extra ? fileExtra(extra) : fileExtra(oldDec)), + opinions: oldDec.opinions || {}, + opinions_inhabitants: oldDec.opinions_inhabitants || [], + author: oldDec.author || userId, + ...((oldDec.tribeId || oldMsg.content.tribeId) ? { tribeId: oldDec.tribeId || oldMsg.content.tribeId } : {}), + createdAt: oldDec.createdAt, + updatedAt: now + }; + + updated = await encryptIfTribe(updated); + + const result = await new Promise((resolve, reject) => { + ssbClient.publish(updated, (err, res) => (err ? reject(err) : resolve(res))); + }); + const tombstone = await tombFor(tipId, oldDec.tribeId || oldMsg.content.tribeId, userId); + await new Promise((res, rej) => ssbClient.publish(tombstone, (e) => (e ? rej(e) : res()))); + return result; + }, + + async deleteFileById(id) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const tipId = await this.resolveCurrentId(id); + const msg = await getMsg(ssbClient, tipId); + + if (!msg) throw new Error("File not found"); + const dec = await decryptIfTribe(msg.content); + if (!dec || dec.type !== "file") throw new Error("File not found"); + if (msg.author !== userId) throw new Error("Not the author"); + + const tombstone = await tombFor(tipId, dec.tribeId || msg.content.tribeId, userId); + + return new Promise((resolve, reject) => { + ssbClient.publish(tombstone, (err, res) => (err ? reject(err) : resolve(res))); + }); + }, + + async listAll(filterOrOpts = "all", maybeOpts = {}) { + const ssbClient = await openSsb(); + + const opts = typeof filterOrOpts === "object" ? filterOrOpts : maybeOpts || {}; + const filter = (typeof filterOrOpts === "string" ? filterOrOpts : opts.filter || "all") || "all"; + const q = String(opts.q || "").trim().toLowerCase(); + const sort = String(opts.sort || "recent").trim(); + const viewerId = opts.viewerId || ssbClient.id; + + const messages = await getAllMessages(ssbClient); + const idx = buildIndex(unwrapForIndex(messages)); + await decryptIndexNodes(idx); + + const items = []; + for (const [rootId, tipId] of idx.tipByRoot.entries()) { + if (idx.tomb.has(tipId)) continue; + const node = idx.nodes.get(tipId); + if (!node) continue; + items.push(buildFile(node, rootId, viewerId, idx.resolveGroup(rootId))); + } + + let list = dedupeBy(items, x => x.url ? [norm(x.author), norm(x.url)].join('|') : null); + const now = Date.now(); + + if (filter === "mine") list = list.filter((a) => String(a.author) === String(viewerId)); + else if (filter === "recent") list = list.filter((a) => new Date(a.createdAt).getTime() >= now - 86400000); + else if (filter === "top") { + list = list.slice().sort((a, b) => voteSum(b.opinions) - voteSum(a.opinions) || new Date(b.createdAt) - new Date(a.createdAt)); + } + + if (q) { + list = list.filter((a) => { + const title = String(a.title || "").toLowerCase(); + const desc = String(a.description || "").toLowerCase(); + const tags = safeArr(a.tags).join(" ").toLowerCase(); + const author = String(a.author || "").toLowerCase(); + return title.includes(q) || desc.includes(q) || tags.includes(q) || author.includes(q); + }); + } + + if (sort === "top") { + list = list.slice().sort((a, b) => voteSum(b.opinions) - voteSum(a.opinions) || new Date(b.createdAt) - new Date(a.createdAt)); + } else if (sort === "oldest") { + list = list.slice().sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); + } else { + list = list.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + } + + return list; + }, + + async getFileById(id, viewerId = null) { + const ssbClient = await openSsb(); + const viewer = viewerId || ssbClient.id; + const messages = await getAllMessages(ssbClient); + const idx = await memoIndex('files', messages, async (m) => { const built = buildIndex(unwrapForIndex(m)); await decryptIndexNodes(built); return built; }); + + const root = idx.rootOf(id); + if (idx.nodes.has(root) && !idx.tipByRoot.has(root)) throw new Error("File not found"); + const agg = idx.resolveGroup(root); + if (agg.contentNode && !idx.tomb.has(agg.contentTip)) return buildFile(agg.contentNode, root, viewer, agg); + + const tip = idx.tipByRoot.get(root) || id; + if (idx.tomb.has(tip)) throw new Error("File not found"); + + const msg = await getMsg(ssbClient, tip); + if (!msg) throw new Error("File not found"); + let c = msg.content; + if (tribeCrypto && (c?.encryptedPayload || tribeCrypto.isTribeMsg(c)) && tribesModel) { + const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false }; + } + if (!c || c.type !== "file") throw new Error("File not found"); + return buildFile({ key: tip, ts: msg.timestamp || 0, c, author: msg.author }, root, viewer, agg); + }, + + async createOpinion(id, category) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + + if (!categories.includes(category)) throw new Error("Invalid voting category"); + + const file = await this.getFileById(id, userId); + if (!file) throw new Error("File not found"); + if (safeArr(file.opinions_inhabitants).includes(userId)) throw new Error("Already voted"); + + const content = { type: "fileOpinion", target: file.rootId, category, createdAt: new Date().toISOString() }; + return new Promise((resolve, reject) => { + ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res))); + }); + } + }; +}; diff --git a/src/models/fileshare_model.js b/src/models/fileshare_model.js new file mode 100644 index 0000000..c3da79b --- /dev/null +++ b/src/models/fileshare_model.js @@ -0,0 +1,234 @@ +const pull = require('../server/node_modules/pull-stream'); +const fs = require('fs'); +const crypto = require('crypto'); +const { Readable } = require('stream'); +const fsc = require('../backend/fileshare_crypto'); + +const MAX_CHUNK_SIZE = 5 * 1024 * 1024; +const MAX_PREFETCH_CHUNKS = 64; +const BLOB_REF = /^&[A-Za-z0-9+/]{43}=\.sha256$/; +const DEFAULT_MAX_SHARE_BYTES = 1024 * 1024 * 1024; +const maxShareBytes = () => { + try { return Number(require('../configs/config-manager.js').getConfig().fileShare.maxSize) || DEFAULT_MAX_SHARE_BYTES; } catch (_) { return DEFAULT_MAX_SHARE_BYTES; } +}; +const checkManifest = (manifest) => { + const size = Number(manifest && manifest.size); + const chunkSize = Number(manifest && manifest.chunkSize); + const chunks = manifest && manifest.chunks; + if (!Number.isInteger(size) || size < 0 || size > maxShareBytes()) throw new Error('fileshare manifest invalid'); + if (!Number.isInteger(chunkSize) || chunkSize <= 0 || chunkSize > MAX_CHUNK_SIZE) throw new Error('fileshare manifest invalid'); + if (!Array.isArray(chunks) || chunks.length !== Math.max(1, Math.ceil(size / chunkSize)) || chunks.some(ref => !BLOB_REF.test(String(ref)))) throw new Error('fileshare manifest invalid'); + if (manifest.plainSha256 !== null && manifest.plainSha256 !== undefined && !/^[0-9a-f]{64}$/.test(String(manifest.plainSha256))) throw new Error('fileshare manifest invalid'); + return manifest; +}; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { + if (!ssb) ssb = await cooler.open(); + return ssb; + }; + + const addBlob = (client, buffer) => + new Promise((resolve, reject) => { + pull(pull.values([buffer]), client.blobs.add((err, ref) => err ? reject(err) : resolve(ref))); + }); + + const getBlob = (client, ref) => + new Promise((resolve, reject) => { + pull(client.blobs.get(ref), pull.collect((err, chunks) => err ? reject(err) : resolve(Buffer.concat(chunks)))); + }); + + const hasBlob = (client, ref) => + new Promise((resolve) => { + if (!client.blobs || typeof client.blobs.has !== 'function') return resolve(false); + client.blobs.has(ref, (err, has) => resolve(!err && !!has)); + }); + + const wantBlob = (client, ref, timeoutMs) => + new Promise((resolve) => { + if (typeof client.blobs.want !== 'function') return resolve(true); + let done = false; + const finish = (ok) => { if (done) return; done = true; if (timer) clearTimeout(timer); resolve(ok); }; + const timer = timeoutMs ? setTimeout(() => finish(false), timeoutMs) : null; + client.blobs.want(ref, (err) => finish(!err)); + }); + + const rmBlob = (client, ref) => + new Promise((resolve) => { + if (typeof client.blobs.rm !== 'function') return resolve(false); + client.blobs.rm(ref, (err) => resolve(!err)); + }); + + const createShareFromBuffer = async ({ buffer, filename, mime, chunkSize } = {}) => { + if (!Buffer.isBuffer(buffer)) throw new Error('buffer required'); + const client = await openSsb(); + const key = fsc.generateFileKey(); + const size = fsc.DEFAULT_CHUNK_SIZE; + const useChunk = Math.min(Number(chunkSize) || fsc.DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE); + + const chunkHashes = []; + for (const part of fsc.splitBuffer(buffer, useChunk)) { + const payload = fsc.encryptChunk(part, key); + chunkHashes.push(await addBlob(client, payload)); + } + + const manifest = fsc.buildManifest({ + filename, mime, size: buffer.length, chunkSize: useChunk, + chunkHashes, plainSha256: fsc.sha256Hex(buffer) + }); + const manifestBlobId = await addBlob(client, fsc.encryptManifest(manifest, key)); + + return { + type: 'fileShare', + v: 1, + key: fsc.keyToHex(key), + manifestBlobId, + filename: manifest.filename, + mime: manifest.mime, + size: manifest.size, + chunkCount: chunkHashes.length + }; + }; + + const createShareFromFile = async ({ filepath, filename, mime, chunkSize } = {}) => { + if (!filepath) throw new Error('filepath required'); + const client = await openSsb(); + const key = fsc.generateFileKey(); + const useChunk = Math.min(Number(chunkSize) || fsc.DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE); + const chunkHashes = []; + const hashAll = crypto.createHash('sha256'); + let total = 0; + let carry = Buffer.alloc(0); + const flush = async (part) => { + hashAll.update(part); + total += part.length; + chunkHashes.push(await addBlob(client, fsc.encryptChunk(part, key))); + }; + for await (const data of fs.createReadStream(filepath, { highWaterMark: useChunk })) { + carry = carry.length ? Buffer.concat([carry, data]) : data; + while (carry.length >= useChunk) { + await flush(carry.subarray(0, useChunk)); + carry = carry.subarray(useChunk); + } + } + if (carry.length || !chunkHashes.length) await flush(carry); + + const manifest = fsc.buildManifest({ + filename, mime, size: total, chunkSize: useChunk, + chunkHashes, plainSha256: hashAll.digest('hex') + }); + const manifestBlobId = await addBlob(client, fsc.encryptManifest(manifest, key)); + + return { + type: 'fileShare', v: 1, + key: fsc.keyToHex(key), manifestBlobId, + filename: manifest.filename, mime: manifest.mime, size: manifest.size, + chunkCount: chunkHashes.length + }; + }; + + const openManifest = async (pointer) => { + const client = await openSsb(); + const key = fsc.keyFromHex(pointer.key); + await wantBlob(client, pointer.manifestBlobId); + const payload = await getBlob(client, pointer.manifestBlobId); + return checkManifest(fsc.decryptManifest(payload, key)); + }; + + const CHUNK_WANT_TIMEOUT = 30000; + const PREFLIGHT_TIMEOUT = 12000; + + const readShareStream = (pointer) => { + const key = fsc.keyFromHex(pointer.key); + async function* gen() { + const client = await openSsb(); + if (!(await wantBlob(client, pointer.manifestBlobId, CHUNK_WANT_TIMEOUT))) throw new Error('fileshare blob unavailable'); + const manifest = checkManifest(fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key)); + const hash = crypto.createHash('sha256'); + let total = 0; + for (const ref of manifest.chunks) { + if (!(await wantBlob(client, ref, CHUNK_WANT_TIMEOUT))) throw new Error('fileshare blob unavailable'); + const plain = fsc.decryptChunk(await getBlob(client, ref), key); + total += plain.length; + if (total > manifest.size) throw new Error('fileshare size mismatch'); + hash.update(plain); + yield plain; + } + if (total !== manifest.size) throw new Error('fileshare size mismatch'); + if (manifest.plainSha256 && hash.digest('hex') !== manifest.plainSha256) throw new Error('fileshare content mismatch'); + } + return Readable.from(gen()); + }; + + const ensureAvailable = async (pointer, timeoutMs = PREFLIGHT_TIMEOUT) => { + try { + const client = await openSsb(); + const key = fsc.keyFromHex(pointer.key); + if (!(await wantBlob(client, pointer.manifestBlobId, timeoutMs))) return false; + const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); + if (manifest.chunks.length && !(await wantBlob(client, manifest.chunks[0], timeoutMs))) return false; + return true; + } catch (_) { + return false; + } + }; + + const reassembleToBuffer = async (pointer) => { + const parts = []; + for await (const chunk of readShareStream(pointer)) parts.push(chunk); + return Buffer.concat(parts); + }; + + const isAvailable = async (pointer) => { + const client = await openSsb(); + if (!(await hasBlob(client, pointer.manifestBlobId))) return false; + try { + const key = fsc.keyFromHex(pointer.key); + const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); + for (const ref of manifest.chunks) if (!(await hasBlob(client, ref))) return false; + return true; + } catch (_) { + return false; + } + }; + + const prefetch = async (pointer) => { + try { + const client = await openSsb(); + if (!(await hasBlob(client, pointer.manifestBlobId))) { wantBlob(client, pointer.manifestBlobId, 0); return; } + const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), fsc.keyFromHex(pointer.key)); + if (!Array.isArray(manifest.chunks) || manifest.chunks.length > MAX_PREFETCH_CHUNKS || manifest.chunks.some(ref => !BLOB_REF.test(String(ref)))) return; + for (const ref of manifest.chunks) if (!(await hasBlob(client, ref))) wantBlob(client, ref, 0); + } catch (_) {} + }; + + const removeLocalBlobs = async (pointer) => { + const client = await openSsb(); + let removed = 0; + try { + const key = fsc.keyFromHex(pointer.key); + if (await hasBlob(client, pointer.manifestBlobId)) { + const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); + for (const ref of manifest.chunks) if (await rmBlob(client, ref)) removed++; + } + } catch (_) {} + if (await rmBlob(client, pointer.manifestBlobId)) removed++; + return removed; + }; + + const pruneExpired = async (sentFileShares, ttlMs, now) => { + const cutoff = (Number.isFinite(now) ? now : Date.now()) - (Number(ttlMs) || 0); + let pruned = 0; + for (const item of (Array.isArray(sentFileShares) ? sentFileShares : [])) { + if (!item || !item.pointer) continue; + const sentAt = new Date(item.sentAt || 0).getTime(); + if (!(sentAt <= cutoff)) continue; + const n = await removeLocalBlobs(item.pointer); + if (n > 0) pruned++; + } + return pruned; + }; + + return { createShareFromBuffer, createShareFromFile, openManifest, readShareStream, reassembleToBuffer, isAvailable, ensureAvailable, prefetch, removeLocalBlobs, pruneExpired }; +}; diff --git a/src/models/fork_workflows.js b/src/models/fork_workflows.js new file mode 100644 index 0000000..4bc97ec --- /dev/null +++ b/src/models/fork_workflows.js @@ -0,0 +1,39 @@ +"use strict"; +// Workflows propios de la rama, registrados sin editar workflows_model.js. +// +// workflows_model.js es del dev (lo toca en 8 de cada 39 releases) y ya trae un +// workflow "activists" con unos 50 modulos. En vez de redefinirlo, la rama anade +// los suyos empujandolos a los arrays que ese fichero exporta; getWorkflow, +// currentWorkflow y el asistente de bienvenida los leen de ahi. +// Ver docs/FRONTERA_UPSTREAM.md. + +const wf = require('./workflows_model'); + +const FORK_WORKFLOWS = [ + { + // una acampada: hablar (chats, telefono y salas de voz del dev), escribir juntos, + // compartir lo que pasa y decidir (tribus = comisiones, parlamento y votaciones) + key: 'acampada', + theme: 'Acampada', + homePage: 'activity', + modules: ['chats', 'phone', 'rooms', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup', + 'tribes', 'parliament', 'votes', 'events'] + } +]; + +let done = false; +const register = () => { + if (done) return; + done = true; + for (const w of FORK_WORKFLOWS) { + if (wf.WORKFLOWS.some(x => x.key === w.key)) continue; + wf.WORKFLOWS.push(w); + wf.WORKFLOW_KEYS.push(w.key); + // en el asistente, detras de activistas (en el movil mobile_ui.workflowPick pone + // Activismo y Acampada las primeras) + const at = wf.WIZARD_WORKFLOWS.indexOf('activists'); + wf.WIZARD_WORKFLOWS.splice(at === -1 ? wf.WIZARD_WORKFLOWS.length : at + 1, 0, w.key); + } +}; + +module.exports = { FORK_WORKFLOWS, register }; diff --git a/src/models/forum_model.js b/src/models/forum_model.js index df27060..3869e60 100644 --- a/src/models/forum_model.js +++ b/src/models/forum_model.js @@ -2,6 +2,8 @@ const pull = require('../server/node_modules/pull-stream'); const crypto = require('crypto'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, requestScope, memoIndex } = require('./typed_log'); +const longText = require('../backend/long_text'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { @@ -33,12 +35,13 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { const ssbClient = await openSsb(); const ssbKeys = require('../server/node_modules/ssb-keys'); const cfg = require('../server/ssb_config'); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? rej(e) : res(m))) - ); + const msgs = await readForumLog(); + const authorOf = new Map(); + for (const m of msgs) if (m && m.key && m.value) authorOf.set(m.key, m.value.author); for (const m of msgs) { const c = m.value && m.value.content; if (!c || c.type !== 'tribe-keys') continue; + if (!c.tribeId || authorOf.get(c.tribeId) !== m.value.author) continue; const memberKeys = c.memberKeys; if (!memberKeys || typeof memberKeys !== 'object') continue; const boxed = memberKeys[ssbClient.id]; @@ -60,41 +63,63 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { return ssb; }; + const FORUM_TYPES = ['forum', 'forum-invite', 'forum-invite-tombstone', 'forum-open-invite', 'forum-open-invite-tombstone', 'tribe-keys', 'vote', 'tombstone', longText.CHUNK_TYPE]; + + const chunkIndexOf = (msgs) => longText.indexChunks( + msgs.filter(m => m.value && m.value.content && m.value.content.type === longText.CHUNK_TYPE), + (c) => { + if (!c || !c.encryptedPayload) return c; + const dec = decryptForumContent(c, null); + return dec && !dec._undecryptable ? dec : null; + } + ); + const fullText = (idx, c, author) => longText.joinText(c && c.text, c && c.chunks, longText.lookupIn(idx), author); + const readForumLog = async () => { const ssbClient = await openSsb(); - return new Promise((res, rej) => pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? rej(e) : res(m || [])))); + return readTyped(ssbClient, FORUM_TYPES, { limit: logLimit, withWindow: true }); }; - const scanForumOpenInvite = async (forumId) => { - const messages = await readForumLog(); + const authorsByKey = (messages) => { + const out = new Map(); + for (const m of messages) if (m && m.key && m.value) out.set(m.key, m.value.author); + return out; + }; + + const inviteTombstones = (messages, authorOf) => { const markerTomb = new Set(); const invTomb = new Set(); for (const m of messages) { const c = m.value && m.value.content; - if (!c) continue; - if (c.type === 'forum-open-invite-tombstone' && typeof c.target === 'string') markerTomb.add(c.target); - if (c.type === 'forum-invite-tombstone' && typeof c.target === 'string') invTomb.add(c.target); + if (!c || typeof c.target !== 'string' || authorOf.get(c.target) !== m.value.author) continue; + if (c.type === 'forum-open-invite-tombstone') markerTomb.add(c.target); + if (c.type === 'forum-invite-tombstone') invTomb.add(c.target); } + return { markerTomb, invTomb }; + }; + + const scanForumOpenInvite = async (forumId) => { + const messages = await readForumLog(); + const authorOf = authorsByKey(messages); + const forumAuthor = authorOf.get(forumId); + if (!forumAuthor) return null; + const { markerTomb, invTomb } = inviteTombstones(messages, authorOf); let best = null; for (const m of messages) { const c = m.value && m.value.content; if (!c || c.type !== 'forum-open-invite' || c.v !== 1) continue; if (c.target !== forumId || typeof c.code !== 'string') continue; + if (m.value.author !== forumAuthor) continue; if (markerTomb.has(m.key)) continue; if (c.inviteKey && invTomb.has(c.inviteKey)) continue; const ts = (m.value && m.value.timestamp) || 0; - if (!best || ts > best.ts) best = { code: c.code, by: c.by || m.value.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }; + if (!best || ts > best.ts) best = { code: c.code, by: m.value.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }; } return best; }; async function collectTombstones(ssbClient) { - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(buildValidatedTombstoneSet(msgs))) - ); - }); + return deletedOf(await readForumLog()); } async function findActiveVote(ssbClient, targetId, voter) { @@ -108,16 +133,38 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { }); } - async function aggregateVotes(ssbClient, targetId) { - const tombstoned = await collectTombstones(ssbClient); + const deletedOf = (msgs) => memoIndex('forum:deleted', msgs, buildValidatedTombstoneSet); + + function aggregateVotes(ssbClient, targetId, known = null) { + const store = requestScope.getStore(); + if (!store || store.method !== 'GET') return countVotes(ssbClient, targetId, known); + if (!store.forumVotes) store.forumVotes = new Map(); + let pending = store.forumVotes.get(targetId); + if (!pending) { pending = countVotes(ssbClient, targetId, known); store.forumVotes.set(targetId, pending); } + return pending; + } + + async function countVotes(ssbClient, targetId, known = null) { + const tombstoned = known || await collectTombstones(ssbClient); return new Promise((resolve, reject) => { - let positives = 0, negatives = 0; + const latest = new Map(); pull( ssbClient.links({ source: null, dest: targetId, rel: 'vote', values: true, keys: true }), pull.filter(link => link.value.content?.vote && !tombstoned.has(link.key)), pull.drain( - link => link.value.content.vote.value > 0 ? positives++ : negatives++, - err => err ? reject(err) : resolve({ positives, negatives }) + link => { + const author = link.value.author; + const ts = Number(link.value.timestamp) || 0; + const seq = Number(link.value.sequence) || 0; + const prev = latest.get(author); + if (!prev || ts > prev.ts || (ts === prev.ts && seq > prev.seq)) latest.set(author, { value: Number(link.value.content.vote.value) || 0, ts, seq }); + }, + err => { + if (err) return reject(err); + let positives = 0, negatives = 0; + for (const v of latest.values()) { if (v.value > 0) positives++; else if (v.value < 0) negatives++; } + resolve({ positives, negatives }); + } ) ); }); @@ -141,16 +188,89 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { } async function getMessageById(id) { - const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, data) => err ? rej(err) : res(data))) - ); + const msgs = await readForumLog(); const msg = msgs.find(m => m.key === id && m.value.content?.type === 'forum'); if (!msg) throw new Error('Message not found'); - return { key: msg.key, ...msg.value.content, timestamp: msg.value.timestamp }; + return { key: msg.key, ...msg.value.content, author: msg.value.author, timestamp: msg.value.timestamp }; } + const buildForums = async (ssbClient) => { + const msgs = await readForumLog(); + const deleted = deletedOf(msgs); + const chunkIdx = chunkIndexOf(msgs); + const decode = (m) => { + const c = m.value && m.value.content; + if (!c) return null; + if (c.encryptedPayload) { + const dec = decryptForumContent(c, m.value.content.root || m.key); + return (dec && !dec._undecryptable) ? dec : null; + } + return c; + }; + const forums = msgs + .map(m => ({ m, c: decode(m) })) + .filter(({ m, c }) => c && c.type === 'forum' && !c.root && !deleted.has(m.key)) + .map(({ m, c }) => ({ ...c, key: m.key, author: m.value.author, text: fullText(chunkIdx, c, m.value.author) })); + const forumsWithVotes = await Promise.all( + forums.map(async f => { + const { positives, negatives } = await aggregateVotes(ssbClient, f.key, deleted); + return { ...f, positiveVotes: positives, negativeVotes: negatives }; + }) + ); + const repliesByRoot = {}; + msgs.forEach(m => { + const cRaw = m.value && m.value.content; + if (!cRaw) return; + const root = cRaw.encryptedPayload ? null : cRaw.root; + if (!root) { + if (!cRaw.encryptedPayload) return; + const decReply = decryptForumContent(cRaw, null); + if (!decReply || decReply._undecryptable || decReply.type !== 'forum' || !decReply.root) return; + if (deleted.has(m.key)) return; + repliesByRoot[decReply.root] = repliesByRoot[decReply.root] || []; + repliesByRoot[decReply.root].push({ key: m.key, text: fullText(chunkIdx, decReply, m.value.author), author: m.value.author, timestamp: m.value.timestamp }); + return; + } + if (cRaw.type === 'forum' && root && !deleted.has(m.key)) { + repliesByRoot[root] = repliesByRoot[root] || []; + repliesByRoot[root].push({ key: m.key, text: fullText(chunkIdx, cRaw, m.value.author), author: m.value.author, timestamp: m.value.timestamp }); + } + }); + const final = await Promise.all( + forumsWithVotes.map(async f => { + const replies = repliesByRoot[f.key] || []; + await Promise.all(replies.map(async (r) => { + const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key, deleted); + r.positiveVotes = rp; + r.negativeVotes = rn; + r.score = rp - rn; + })); + const replyPos = replies.reduce((sum, r) => sum + (r.positiveVotes || 0), 0); + const replyNeg = replies.reduce((sum, r) => sum + (r.negativeVotes || 0), 0); + const positiveVotes = f.positiveVotes + replyPos; + const negativeVotes = f.negativeVotes + replyNeg; + const score = positiveVotes - negativeVotes; + const participants = new Set(replies.map(r => r.author).concat(f.author)); + const messagesCount = replies.length + 1; + const lastMessage = + replies.length + ? replies.reduce((a, b) => (new Date(a.timestamp) > new Date(b.timestamp) ? a : b)) + : null; + return { + ...f, + positiveVotes, + negativeVotes, + score, + participants: Array.from(participants), + messagesCount, + lastMessage, + messages: replies + }; + }) + ); + return final; + }; + return { ingestKeys: async () => { await ingestOwnTribeKeys(); }, @@ -168,15 +288,14 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { votes_inhabitants: [], isPrivate: isPrivateFlag }; - let content = plainContent; let forumKey = null; - if (isPrivateFlag && ownCrypto && tribeCrypto) { - forumKey = ownCrypto.generateTribeKey(); - content = tribeCrypto.encryptContent(plainContent, [forumKey], true); - } - const result = await new Promise((resolve, reject) => - ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res)) + if (isPrivateFlag && ownCrypto && tribeCrypto) forumKey = ownCrypto.generateTribeKey(); + const seal = (c) => forumKey ? tribeCrypto.encryptContent(c, [forumKey], true) : c; + const publish = (c) => new Promise((resolve, reject) => + ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res)) ); + const content = await longText.chunkContent(plainContent, 'text', { maxBytes: forumKey ? longText.CONTENT_BYTES.sealed : longText.CONTENT_BYTES.plain, publish }); + const result = await publish(content); if (forumKey) { ownCrypto.setKey(result.key, forumKey, 1); try { @@ -208,14 +327,17 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { votes_inhabitants: [] }; if (parentId) content.branch = parentId; + let key = null; if (isPrivate && ownCrypto && tribeCrypto) { - const key = lookupKey(forumId); + key = lookupKey(forumId); if (!key) throw new Error('Missing forum key — cannot reply to encrypted forum'); - content = tribeCrypto.encryptContent(content, [key], true); } - return new Promise((resolve, reject) => - ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res)) + const seal = (c) => key ? tribeCrypto.encryptContent(c, [key], true) : c; + const publish = (c) => new Promise((resolve, reject) => + ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res)) ); + content = await longText.chunkContent(content, 'text', { maxBytes: key ? longText.CONTENT_BYTES.sealed : longText.CONTENT_BYTES.plain, publish }); + return publish(content); }, generateInvite: async (forumId) => { @@ -226,7 +348,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { const rawC = rawRoot.content; const dec = rawC && rawC.encryptedPayload ? decryptForumContent(rawC, forumId) : rawC; if (dec && dec._undecryptable) throw new Error('Forum is encrypted and cannot be decrypted'); - if (dec.author !== userId) throw new Error('Only the author can generate invites'); + if (rawRoot.author !== userId) throw new Error('Only the author can generate invites'); if (dec.isPrivate !== true) throw new Error('Only private forums use invitation codes'); const key = lookupKey(forumId); if (!key) throw new Error('Missing forum key'); @@ -252,7 +374,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { const rawC = rawRoot.content; const dec = rawC && rawC.encryptedPayload ? decryptForumContent(rawC, forumId) : rawC; if (dec && dec._undecryptable) throw new Error('Forum is encrypted and cannot be decrypted'); - if (dec.author !== userId) throw new Error('Only the author can generate invites'); + if (rawRoot.author !== userId) throw new Error('Only the author can generate invites'); if (dec.isPrivate !== true) throw new Error('Only private forums use invitation codes'); if (await scanForumOpenInvite(forumId)) throw new Error('An open invitation already exists'); const key = lookupKey(forumId); @@ -276,9 +398,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { let author = null; try { const rawRoot = await new Promise((res, rej) => ssbClient.get(forumId, (e, m) => e ? rej(e) : res(m))); - const rawC = rawRoot && rawRoot.content; - const dec = rawC && rawC.encryptedPayload ? decryptForumContent(rawC, forumId) : rawC; - author = dec && dec.author; + author = rawRoot && rawRoot.author; } catch (_) {} if (rec.by !== userId && author !== userId) throw new Error('Not allowed to remove this invitation'); await new Promise((resolve, reject) => ssbClient.publish({ type: 'forum-open-invite-tombstone', target: rec.markerKey, ts: new Date().toISOString() }, (err) => err ? reject(err) : resolve())); @@ -288,19 +408,15 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { joinByInvite: async (code) => { if (!ownCrypto || !tribeCrypto) throw new Error('Forum crypto unavailable'); const ssbClient = await openSsb(); - const messages = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? rej(e) : res(m))) - ); - const invTomb = new Set(); - for (const m of messages) { - const c = m.value && m.value.content; - if (c && c.type === 'forum-invite-tombstone' && typeof c.target === 'string') invTomb.add(c.target); - } + const messages = await readForumLog(); + const authorOf = authorsByKey(messages); + const { invTomb } = inviteTombstones(messages, authorOf); let matched = null; for (const m of messages) { const c = m.value && m.value.content; if (!c || c.type !== 'forum-invite') continue; if (invTomb.has(m.key)) continue; + if (!authorOf.get(c.target) || authorOf.get(c.target) !== m.value.author) continue; try { const hash = tribeCrypto.hashInviteCode(code, c.salt); if (hash === c.codeHash) { matched = c; break; } @@ -356,81 +472,10 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { listAll: async filter => { const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, data) => err ? rej(err) : res(data))) - ); - const deleted = buildValidatedTombstoneSet(msgs); - const decode = (m) => { - const c = m.value && m.value.content; - if (!c) return null; - if (c.encryptedPayload) { - const dec = decryptForumContent(c, m.value.content.root || m.key); - return (dec && !dec._undecryptable) ? dec : null; - } - return c; - }; - const forums = msgs - .map(m => ({ m, c: decode(m) })) - .filter(({ m, c }) => c && c.type === 'forum' && !c.root && !deleted.has(m.key)) - .map(({ m, c }) => ({ ...c, key: m.key })); - const forumsWithVotes = await Promise.all( - forums.map(async f => { - const { positives, negatives } = await aggregateVotes(ssbClient, f.key); - return { ...f, positiveVotes: positives, negativeVotes: negatives }; - }) - ); - const repliesByRoot = {}; - msgs.forEach(m => { - const cRaw = m.value && m.value.content; - if (!cRaw) return; - const root = cRaw.encryptedPayload ? null : cRaw.root; - if (!root) { - if (!cRaw.encryptedPayload) return; - const decReply = decryptForumContent(cRaw, null); - if (!decReply || decReply._undecryptable || decReply.type !== 'forum' || !decReply.root) return; - if (deleted.has(m.key)) return; - repliesByRoot[decReply.root] = repliesByRoot[decReply.root] || []; - repliesByRoot[decReply.root].push({ key: m.key, text: decReply.text, author: decReply.author, timestamp: m.value.timestamp }); - return; - } - if (cRaw.type === 'forum' && root && !deleted.has(m.key)) { - repliesByRoot[root] = repliesByRoot[root] || []; - repliesByRoot[root].push({ key: m.key, text: cRaw.text, author: cRaw.author, timestamp: m.value.timestamp }); - } - }); - const final = await Promise.all( - forumsWithVotes.map(async f => { - const replies = repliesByRoot[f.key] || []; - for (let r of replies) { - const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key); - r.positiveVotes = rp; - r.negativeVotes = rn; - r.score = rp - rn; - } - const replyPos = replies.reduce((sum, r) => sum + (r.positiveVotes || 0), 0); - const replyNeg = replies.reduce((sum, r) => sum + (r.negativeVotes || 0), 0); - const positiveVotes = f.positiveVotes + replyPos; - const negativeVotes = f.negativeVotes + replyNeg; - const score = positiveVotes - negativeVotes; - const participants = new Set(replies.map(r => r.author).concat(f.author)); - const messagesCount = replies.length + 1; - const lastMessage = - replies.length - ? replies.reduce((a, b) => (new Date(a.timestamp) > new Date(b.timestamp) ? a : b)) - : null; - return { - ...f, - positiveVotes, - negativeVotes, - score, - participants: Array.from(participants), - messagesCount, - lastMessage, - messages: replies - }; - }) - ); + const userId = ssbClient.id; + const store = requestScope.getStore(); + if (store && !store.forumAll) store.forumAll = buildForums(ssbClient); + const final = (await (store ? store.forumAll : buildForums(ssbClient))).slice(); const filtered = filter === 'mine' ? final.filter(f => f.author === userId) @@ -442,17 +487,14 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { getForumById: async id => { const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, data) => err ? rej(err) : res(data))) - ); - const deleted = buildValidatedTombstoneSet(msgs); + const msgs = await readForumLog(); + const deleted = deletedOf(msgs); const original = msgs.find(m => m.key === id && !deleted.has(m.key)); if (!original || original.value.content?.type !== 'forum') throw new Error('Forum not found'); const rawBase = original.value.content; const base = rawBase.encryptedPayload ? decryptForumContent(rawBase, id) : rawBase; if (base && base._undecryptable) throw new Error('Forum is encrypted and cannot be decrypted with available keys'); - const { positives, negatives } = await aggregateVotes(ssbClient, id); + const { positives, negatives } = await aggregateVotes(ssbClient, id, deleted); const replyAuthors = []; for (const m of msgs) { if (deleted.has(m.key)) continue; @@ -464,11 +506,13 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { if (!dec || dec._undecryptable) continue; rc = dec; } - if (rc.type === 'forum' && rc.root === id) replyAuthors.push(rc.author); + if (rc.type === 'forum' && rc.root === id) replyAuthors.push(m.value.author); } - const participants = Array.from(new Set(replyAuthors.concat(base.author).filter(Boolean))); + const participants = Array.from(new Set(replyAuthors.concat(original.value.author).filter(Boolean))); return { ...base, + author: original.value.author, + text: fullText(chunkIndexOf(msgs), base, original.value.author), key: id, positiveVotes: positives, negativeVotes: negatives, @@ -479,11 +523,9 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { getMessagesByForumId: async forumId => { const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, data) => err ? rej(err) : res(data))) - ); - const deleted = buildValidatedTombstoneSet(msgs); + const msgs = await readForumLog(); + const deleted = deletedOf(msgs); + const chunkIdx = chunkIndexOf(msgs); const decodeReply = (m) => { const c = m.value && m.value.content; if (!c || c.type !== 'forum') return null; @@ -500,18 +542,20 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { .filter(r => r && !deleted.has(r.m.key)) .map(({ c, m }) => ({ key: m.key, - text: c.text, - author: c.author, + text: fullText(chunkIdx, c, m.value.author), + author: m.value.author, timestamp: m.value.timestamp, parent: c.branch || null })); - for (let r of replies) { - const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key); - r.positiveVotes = rp; - r.negativeVotes = rn; - r.score = rp - rn; - } - const { positives: p, negatives: n } = await aggregateVotes(ssbClient, forumId); + const [, { positives: p, negatives: n }] = await Promise.all([ + Promise.all(replies.map(async (r) => { + const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key, deleted); + r.positiveVotes = rp; + r.negativeVotes = rn; + r.score = rp - rn; + })), + aggregateVotes(ssbClient, forumId, deleted) + ]); const replyPos = replies.reduce((sum, r) => sum + (r.positiveVotes || 0), 0); const replyNeg = replies.reduce((sum, r) => sum + (r.negativeVotes || 0), 0); const positiveVotes = p + replyPos; diff --git a/src/models/games_model.js b/src/models/games_model.js index c148226..65b2eac 100644 --- a/src/models/games_model.js +++ b/src/models/games_model.js @@ -1,11 +1,14 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); +const { readTyped } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 5000; +const GAME_TYPES = ['gameScore']; + const VALID_GAMES = new Set([ 'cocoland', 'ecoinflow', 'neoninfiltrator', 'spaceinvaders', 'arkanoid', 'pingpong', 'asteroids', 'tiktaktoe', 'flipflop', - '8ball', 'artillery', 'labyrinth', 'cocoman', 'tetris' + '8ball', 'artillery', 'labyrinth', 'cocoman', 'tetris', 'minesweeper' ]); module.exports = ({ cooler }) => { @@ -16,12 +19,7 @@ module.exports = ({ cooler }) => { }; async function readAll(ssbClient) { - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, results) => (err ? reject(err) : resolve(results))) - ); - }); + return readTyped(ssbClient, GAME_TYPES, { limit: logLimit }); } return { diff --git a/src/models/housing_model.js b/src/models/housing_model.js new file mode 100644 index 0000000..1f06356 --- /dev/null +++ b/src/models/housing_model.js @@ -0,0 +1,527 @@ +const pull = require("../server/node_modules/pull-stream") +const moment = require("../server/node_modules/moment") +const categories = require("../backend/opinion_categories") +const { getConfig } = require("../configs/config-manager.js") +const { dedupeByPreferring, versionGroups } = require('../backend/dedupe') +const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { readTyped, memoIndex } = require('./typed_log') +const logLimit = getConfig().ssbLogStream?.limit || 1000 + +const HOUSING_TYPES = ["sale", "rent", "couchsurfing"] +const PROPERTY_TYPES = ["apartment", "house", "room", "land", "other"] + +const norm = (s) => String(s || "").trim().toLowerCase() +const safeArr = (v) => (Array.isArray(v) ? v : []) + +const toNum = (v) => { + const n = parseFloat(String(v ?? "").replace(",", ".")) + return Number.isFinite(n) ? n : NaN +} + +const toInt = (v, fallback = 0) => { + const n = parseInt(String(v ?? ""), 10) + return Number.isFinite(n) ? n : fallback +} + +const nonNeg = (v) => { + const n = toNum(v) + return Number.isFinite(n) && n >= 0 ? n : 0 +} + +const safeDate = (v) => { + const s = String(v || "").trim() + if (!s) return "" + const d = new Date(s) + return Number.isFinite(d.getTime()) ? s : "" +} + +const normalizeTags = (raw) => { + if (raw === undefined || raw === null) return [] + if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean) + return String(raw).split(",").map(t => t.trim()).filter(Boolean) +} + +const safeType = (v) => { + const t = norm(v) + return HOUSING_TYPES.includes(t) ? t : "" +} + +const safeProperty = (v) => { + const t = norm(v) + return PROPERTY_TYPES.includes(t) ? t : "other" +} + +const { MAX_IMAGES, normalizeImages, normalizeVideo } = require('./media_gallery') + +const matchSearch = (item, q) => { + const qq = norm(q) + if (!qq) return true + const hay = [ + item.title, + item.description, + item.place, + item.rules, + item.housing_type, + item.property_type, + safeArr(item.tags).join(" ") + ].map(x => norm(x)).join(" ") + return hay.includes(qq) +} + +module.exports = ({ cooler, tribeCrypto }) => { + let ssb + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + + const isEncrypted = (c) => !!(c && c.encryptedPayload) + + const keysForRoot = (rootId) => { + if (!tribeCrypto || !rootId) return [] + const ks = (tribeCrypto.getKeys && tribeCrypto.getKeys(rootId)) || [] + if (ks.length) return ks + const k = tribeCrypto.getKey ? tribeCrypto.getKey(rootId) : null + return k ? [k] : [] + } + + const decryptHousing = (c, rootId) => { + if (!isEncrypted(c)) return c + if (!tribeCrypto) return { ...c, _undecryptable: true } + const keys = keysForRoot(rootId) + if (!keys.length) return { ...c, _undecryptable: true } + return tribeCrypto.decryptContent(c, keys.map(k => [k])) + } + + const publishHousing = (ssbClient, content, rootId) => new Promise((resolve, reject) => { + const hidden = String(content.visibility || "PUBLIC").toUpperCase() === "HIDDEN" + if (!hidden || !tribeCrypto) { + ssbClient.publish(content, (err, msg) => { + if (err) return reject(err) + if (msg && msg.key && tribeCrypto && !rootId) { + try { tribeCrypto.setKey(msg.key, tribeCrypto.generateTribeKey(), 1) } catch (_) {} + } + resolve(msg) + }) + return + } + let key = rootId ? (keysForRoot(rootId)[0] || null) : null + if (!key) key = tribeCrypto.generateTribeKey() + let envelope + try { envelope = tribeCrypto.encryptContent(content, [key], true) } catch (err) { return reject(err) } + ssbClient.publish(envelope, (err, msg) => { + if (err) return reject(err) + try { tribeCrypto.setKey(rootId || (msg && msg.key), key, 1) } catch (_) {} + resolve(msg) + }) + }) + + const HOUSING_MSG_TYPES = ["housing", "housingOpinion", "housingRequest", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, HOUSING_MSG_TYPES, { limit: logLimit, withWindow: true, withPrivate: true }) + + const buildIndex = (messages, ssbClient) => { + const tomb = buildValidatedTombstoneSet(messages) + const nodes = new Map() + const requestMsgs = [] + const opinionMsgs = [] + + for (const m of messages) { + const key = m.key + const v = m.value || {} + const c = v.content + if (!c || typeof c !== "object") continue + + if (c.type === "housing") { + nodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) + continue + } + + if (c.type === "housingOpinion" && c.target) { + opinionMsgs.push({ target: c.target, author: v.author, category: c.category }) + continue + } + + if (c.type === "housingRequest" && c.housingId) { + const author = v.author + if (!author) continue + requestMsgs.push({ ts: v.timestamp || m.timestamp || 0, value: !!c.value, author, housingId: c.housingId }) + continue + } + } + + if (ssbClient) { + for (const m of messages) { + if (typeof m.value?.content !== 'string') continue + try { + const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const c = dec?.value?.content + if (!c || c.type !== 'housingRequest' || !c.housingId) continue + const author = dec.value.author + if (!author) continue + requestMsgs.push({ ts: dec.value.timestamp || m.timestamp || 0, value: !!c.value, author, housingId: c.housingId }) + } catch {} + } + } + + const vg = versionGroups(Array.from(nodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))) + const rootOf = vg.rootOf + const tipOf = vg.tipOf + + const tipByRoot = new Map() + for (const r of vg.roots) tipByRoot.set(r, tipOf(r)) + + const requestLatest = new Map() + for (const req of requestMsgs) { + const housingId = rootOf(req.housingId) + const k = `${housingId}::${req.author}` + const prev = requestLatest.get(k) + if (!prev || req.ts >= prev.ts) requestLatest.set(k, { ...req, housingId }) + } + + const requestsByRoot = new Map() + const everRequestedByRoot = new Map() + for (const { housingId, author, value } of requestLatest.values()) { + if (!requestsByRoot.has(housingId)) requestsByRoot.set(housingId, new Set()) + if (!everRequestedByRoot.has(housingId)) everRequestedByRoot.set(housingId, new Set()) + everRequestedByRoot.get(housingId).add(author) + const set = requestsByRoot.get(housingId) + if (value) set.add(author) + else set.delete(author) + } + + const opinionsByRoot = new Map() + for (const op of opinionMsgs) { + if (!nodes.has(op.target)) continue + const r = rootOf(op.target) + if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []) + opinionsByRoot.get(r).push(op) + } + + const aggregateFor = (rootId, content, ownerId) => { + const opinions = { ...((content && content.opinions) || {}) } + const voters = safeArr(content && content.opinions_inhabitants).slice() + const voterSet = new Set(voters) + for (const op of (opinionsByRoot.get(rootId) || [])) { + if (!op.author || op.author === ownerId) continue + if (voterSet.has(op.author)) continue + if (!categories.includes(op.category)) continue + voterSet.add(op.author); voters.push(op.author) + opinions[op.category] = (opinions[op.category] || 0) + 1 + } + return { opinions, opinions_inhabitants: voters } + } + + return { tomb, nodes, vg, rootOf, tipOf, tipByRoot, requestsByRoot, everRequestedByRoot, aggregateFor } + } + + const buildObject = (node, rootId, idx, viewer) => { + const raw = node.c || {} + const c = decryptHousing(raw, rootId) + if (c._undecryptable) return null + const author = node.author || c.author + const requests = Array.from(idx.requestsByRoot.get(rootId) || []) + const visibleRequests = (author === viewer) ? requests : (requests.includes(viewer) ? [viewer] : []) + const agg = idx.aggregateFor(rootId, c, author) + return { + id: node.key, + rootId, + housing_type: safeType(c.housing_type), + property_type: safeProperty(c.property_type), + title: String(c.title || ""), + description: String(c.description || ""), + rules: String(c.rules || ""), + place: String(c.place || ""), + mapUrl: String(c.mapUrl || ""), + price: Number.isFinite(toNum(c.price)) ? toNum(c.price).toFixed(6) : "0.000000", + rooms: toInt(c.rooms, 0), + size: nonNeg(c.size), + capacity: toInt(c.capacity, 0), + availableFrom: safeDate(c.availableFrom), + availableTo: safeDate(c.availableTo), + images: normalizeImages(c.images && c.images.length ? c.images : c.image), + image: normalizeImages(c.images && c.images.length ? c.images : c.image)[0] || null, + video: normalizeVideo(c.video), + author, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + status: String(c.status || "OPEN").toUpperCase() === "CLOSED" ? "CLOSED" : "OPEN", + tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags), + requests: visibleRequests, + requestCount: requests.length, + opinions: agg.opinions, + opinions_inhabitants: agg.opinions_inhabitants, + requestedByViewer: requests.includes(viewer), + everRequestedByViewer: (idx.everRequestedByRoot.get(rootId) || new Set()).has(viewer), + ratedByViewer: agg.opinions_inhabitants.includes(viewer), + visibility: String(c.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" + } + } + + const buildContent = (data, opts = {}) => { + const housing_type = safeType(data.housing_type) + if (!housing_type) throw new Error("Invalid housing type") + + const title = String(data.title || "").trim() + if (!title) throw new Error("Invalid title") + const description = String(data.description || "").trim() + if (!description) throw new Error("Invalid description") + + const availableFrom = safeDate(data.availableFrom) + const availableTo = safeDate(data.availableTo) + if (!availableFrom) throw new Error("The start date is required") + if (opts.enforceFutureStart) { + const [y, m, d] = String(availableFrom).split("-").map(Number) + const startsAt = new Date(y, (m || 1) - 1, d || 1) + const today = new Date() + today.setHours(0, 0, 0, 0) + if (startsAt < today) throw new Error("The start date cannot be earlier than today") + } + if (availableTo && new Date(availableTo) < new Date(availableFrom)) { + throw new Error("The end date cannot be earlier than the start date") + } + + const images = normalizeImages(data.images !== undefined ? data.images : data.image) + const video = normalizeVideo(data.video) + + return { + type: "housing", + housing_type, + property_type: safeProperty(data.property_type), + title, + description, + rules: String(data.rules || "").trim(), + place: String(data.place || "").trim(), + mapUrl: String(data.mapUrl || "").trim(), + price: housing_type === "couchsurfing" ? "0.000000" : nonNeg(data.price).toFixed(6), + rooms: Math.max(0, toInt(data.rooms, 0)), + size: nonNeg(data.size), + capacity: Math.max(0, toInt(data.capacity, 0)), + availableFrom, + availableTo, + images, + image: images[0] || null, + video, + tags: normalizeTags(data.tags), + visibility: String(data.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" + } + } + + return { + type: "housing", + HOUSING_TYPES, + PROPERTY_TYPES, + MAX_IMAGES, + + async createHousing(data) { + const ssbClient = await openSsb() + const content = { + ...buildContent(data, { enforceFutureStart: true }), + author: ssbClient.id, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: "OPEN", + opinions: {}, + opinions_inhabitants: [] + } + return publishHousing(ssbClient, content, null) + }, + + async resolveCurrentId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tip = idx.tipOf(id) + if (idx.tomb.has(tip) || idx.vg.isForeignCopy(idx.rootOf(tip))) throw new Error("Housing not found") + return tip + }, + + async resolveRootId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tip = idx.tipOf(id) + if (idx.tomb.has(tip) || idx.vg.isForeignCopy(idx.rootOf(tip))) throw new Error("Housing not found") + return idx.rootOf(tip) + }, + + async updateHousing(id, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tipId = idx.tipOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(idx.rootOf(tipId))) throw new Error("Housing not found") + const node = idx.nodes.get(tipId) + if (!node || !node.c) throw new Error("Housing not found") + if ((node.author || node.c.author) !== ssbClient.id) throw new Error("Unauthorized") + + const rootId = idx.rootOf(tipId) + const current = decryptHousing(node.c, rootId) + if (current._undecryptable) throw new Error("Cannot decrypt this listing") + const startChanged = data.availableFrom !== undefined && safeDate(data.availableFrom) !== safeDate(current.availableFrom) + const merged = buildContent({ + housing_type: data.housing_type === undefined ? current.housing_type : data.housing_type, + property_type: data.property_type === undefined ? current.property_type : data.property_type, + title: data.title === undefined ? current.title : data.title, + description: data.description === undefined ? current.description : data.description, + rules: data.rules === undefined ? current.rules : data.rules, + place: data.place === undefined ? current.place : data.place, + mapUrl: data.mapUrl === undefined ? current.mapUrl : data.mapUrl, + price: data.price === undefined ? current.price : data.price, + rooms: data.rooms === undefined ? current.rooms : data.rooms, + size: data.size === undefined ? current.size : data.size, + capacity: data.capacity === undefined ? current.capacity : data.capacity, + availableFrom: data.availableFrom === undefined ? current.availableFrom : data.availableFrom, + availableTo: data.availableTo === undefined ? current.availableTo : data.availableTo, + images: data.images === undefined + ? (current.images && current.images.length ? current.images : current.image) + : data.images, + video: data.video === undefined ? current.video : data.video, + tags: data.tags === undefined ? current.tags : data.tags, + visibility: data.visibility === undefined ? current.visibility : data.visibility + }, { enforceFutureStart: startChanged }) + + let status = String(current.status || "OPEN").toUpperCase() + if (data.status !== undefined) { + const s = String(data.status || "").toUpperCase() + if (!["OPEN", "CLOSED"].includes(s)) throw new Error("Invalid status") + status = s + } + + const next = { + ...merged, + status, + author: current.author, + createdAt: current.createdAt, + updatedAt: new Date().toISOString(), + opinions: current.opinions || {}, + opinions_inhabitants: safeArr(current.opinions_inhabitants), + replaces: tipId + } + + const tomb = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id } + await new Promise((res, rej) => ssbClient.publish(tomb, (e) => e ? rej(e) : res())) + return publishHousing(ssbClient, next, rootId) + }, + + async updateHousingStatus(id, status) { + return this.updateHousing(id, { status: String(status || "").toUpperCase() }) + }, + + async deleteHousing(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tipId = idx.tipOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(idx.rootOf(tipId))) throw new Error("Housing not found") + const node = idx.nodes.get(tipId) + if (!node) throw new Error("Housing not found") + if ((node.author || node.c.author) !== ssbClient.id) throw new Error("Unauthorized") + const tomb = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id } + return new Promise((res, rej) => ssbClient.publish(tomb, (e, r) => e ? rej(e) : res(r))) + }, + + async requestHousing(id) { + const ssbClient = await openSsb() + const me = ssbClient.id + const item = await this.getHousingById(id) + if (item.author === me) throw new Error("Cannot request your own listing") + if (item.status !== "OPEN") throw new Error("This listing is closed") + if (safeArr(item.requests).includes(me)) return { alreadyRequested: true } + const msg = { type: "housingRequest", housingId: item.rootId, value: true, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, item.author], (e, m) => e ? rej(e) : res(m))) + }, + + async cancelRequest(id) { + const ssbClient = await openSsb() + const me = ssbClient.id + const item = await this.getHousingById(id) + if (item.author === me) throw new Error("Cannot cancel a request on your own listing") + if (!safeArr(item.requests).includes(me)) return { notRequested: true } + const msg = { type: "housingRequest", housingId: item.rootId, value: false, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, item.author], (e, m) => e ? rej(e) : res(m))) + }, + + async createOpinion(id, category) { + if (!categories.includes(category)) throw new Error("Invalid category") + const ssbClient = await openSsb() + const me = ssbClient.id + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tipId = idx.tipOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(idx.rootOf(tipId))) throw new Error("Housing not found") + const node = idx.nodes.get(tipId) + if (!node) throw new Error("Housing not found") + const rootId = idx.rootOf(tipId) + const owner = node.author || node.c.author + if (owner === me) throw new Error("You cannot rate your own listing") + + const agg = idx.aggregateFor(rootId, decryptHousing(node.c, rootId), owner) + if (agg.opinions_inhabitants.includes(me)) throw new Error("Already voted") + + const everRequested = idx.everRequestedByRoot.get(rootId) || new Set() + if (!everRequested.has(me)) throw new Error("You can rate only a place you have requested") + + const content = { type: "housingOpinion", target: rootId, category, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async listHousing(filter = "ALL", viewerId = null, query = {}) { + const ssbClient = await openSsb() + const viewer = viewerId || ssbClient.id + const idx = buildIndex(await readAll(ssbClient), ssbClient) + + const items = [] + for (const [rootId, tipId] of idx.tipByRoot.entries()) { + if (idx.tomb.has(tipId)) continue + const node = idx.nodes.get(tipId) + if (!node) continue + const item = buildObject(node, rootId, idx, viewer) + if (!item) continue + if (item.visibility === "HIDDEN" && item.author !== viewer) continue + items.push(item) + } + + const F = String(filter || "ALL").toUpperCase() + let list = dedupeByPreferring( + items, + (h) => (h.author && h.createdAt) ? [norm(h.author), norm(h.createdAt), norm(h.title)].join("|") : null, + (h) => h.requestCount + ) + + if (F === "MINE") list = list.filter(h => h.author === viewer) + else if (F === "REQUESTED") list = list.filter(h => safeArr(h.requests).includes(viewer)) + else if (HOUSING_TYPES.includes(norm(F))) list = list.filter(h => h.housing_type === norm(F)) + else if (F === "OPEN") list = list.filter(h => h.status === "OPEN") + else if (F === "CLOSED") list = list.filter(h => h.status === "CLOSED") + else if (F === "RECENT") list = list.filter(h => moment(h.createdAt).isAfter(moment().subtract(24, "hours"))) + + const search = String(query.search || query.q || "").trim() + if (search) list = list.filter(h => matchSearch(h, search)) + + const minP = toNum(query.minPrice) + const maxP = toNum(query.maxPrice) + if (Number.isFinite(minP)) list = list.filter(h => toNum(h.price) >= minP) + if (Number.isFinite(maxP)) list = list.filter(h => toNum(h.price) <= maxP) + + const place = String(query.place || "").trim() + if (place) list = list.filter(h => norm(h.place).includes(norm(place))) + + const ratingOf = (h) => safeArr(h.opinions_inhabitants).length + const sort = String(query.sort || "").trim() + if (F === "TOP" || sort === "rating") list.sort((a, b) => ratingOf(b) - ratingOf(a)) + else if (sort === "price") list.sort((a, b) => toNum(a.price) - toNum(b.price)) + else if (sort === "requests") list.sort((a, b) => b.requestCount - a.requestCount) + else list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + + return list + }, + + async getHousingById(id, viewerId = null) { + const ssbClient = await openSsb() + const viewer = viewerId || ssbClient.id + const idx = memoIndex('housing', await readAll(ssbClient), (m) => buildIndex(m, ssbClient)) + const tipId = idx.tipOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(idx.rootOf(tipId))) throw new Error("Housing not found") + const node = idx.nodes.get(tipId) + if (!node) throw new Error("Housing not found") + const rootId = idx.rootOf(tipId) + const item = buildObject(node, rootId, idx, viewer) + if (!item) throw new Error("Housing not found") + if (item.visibility === "HIDDEN" && item.author !== viewer) throw new Error("Housing not found") + return item + } + } +} diff --git a/src/models/images_model.js b/src/models/images_model.js index 50c90e3..e9a38e1 100644 --- a/src/models/images_model.js +++ b/src/models/images_model.js @@ -2,7 +2,8 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -31,13 +32,9 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const IMAGE_TYPES = ["image", "imageOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, IMAGE_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -47,9 +44,6 @@ module.exports = ({ cooler }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -65,48 +59,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, t] of Array.from(parent.entries())) { - const cn = nodes.get(k); - const pn = nodes.get(t); - if (!pn) { parent.delete(k); if (child.get(t) === k) child.delete(t); continue; } - if (!cn || String(cn.author) !== String(pn.author)) { parent.delete(k); if (child.get(t) === k) child.delete(t); nodes.delete(k); } - } - - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -123,9 +85,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildImage = (node, rootId, viewerId, agg) => { @@ -141,8 +103,8 @@ module.exports = ({ cooler }) => { author: node.author, title: c.title || "", description: c.description || "", + license: c.license || "", mapUrl: c.mapUrl || "", - meme: !!c.meme, opinions: agg ? agg.opinions : (c.opinions || {}), opinions_inhabitants: voters, hasVoted: viewerId ? voters.includes(viewerId) : false, @@ -168,16 +130,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Image not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Image not found"); return root; }, - async createImage(blobMarkdown, tagsRaw, title, description, memeBool, mapUrl) { + async createImage(blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -193,9 +151,9 @@ module.exports = ({ cooler }) => { title: title || "", description: description || "", mapUrl: mapUrl || "", - meme: !!memeBool, opinions: {}, - opinions_inhabitants: [] + opinions_inhabitants: [], + ...(license ? { license } : {}) }; return new Promise((resolve, reject) => { @@ -203,7 +161,7 @@ module.exports = ({ cooler }) => { }); }, - async updateImageById(id, blobMarkdown, tagsRaw, title, description, memeBool, mapUrl) { + async updateImageById(id, blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -226,9 +184,9 @@ module.exports = ({ cooler }) => { title: title !== undefined ? title || "" : oldMsg.content.title || "", description: description !== undefined ? description || "" : oldMsg.content.description || "", mapUrl: mapUrl !== undefined ? mapUrl || "" : oldMsg.content.mapUrl || "", - meme: typeof memeBool === "boolean" ? memeBool : !!oldMsg.content.meme, createdAt: oldMsg.content.createdAt, - updatedAt: now + updatedAt: now, + ...(license !== undefined ? { license } : {}) }; const tombstone = { type: "tombstone", target: tipId, deletedAt: now, author: userId }; @@ -280,7 +238,6 @@ module.exports = ({ cooler }) => { if (filter === "mine") list = list.filter((im) => String(im.author) === String(viewerId)); else if (filter === "recent") list = list.filter((im) => new Date(im.createdAt).getTime() >= now - 86400000); - else if (filter === "meme") list = list.filter((im) => im.meme === true); else if (filter === "top") { list = list .slice() @@ -315,10 +272,10 @@ module.exports = ({ cooler }) => { const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('images', messages, buildIndex); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Image not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Image not found"); return buildImage(agg.contentNode, rootId, viewer, agg); }, diff --git a/src/models/industry_model.js b/src/models/industry_model.js new file mode 100644 index 0000000..5593924 --- /dev/null +++ b/src/models/industry_model.js @@ -0,0 +1,992 @@ +const pull = require("../server/node_modules/pull-stream") +const { getConfig } = require("../configs/config-manager.js") +const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { readTyped } = require('./typed_log') +const logLimit = (getConfig().ssbLogStream && getConfig().ssbLogStream.limit) || 1000 + +module.exports = ({ cooler }) => { + let ssb + const openSsb = async () => { + if (!ssb) ssb = await cooler.open() + return ssb + } + + const TYPE = "industry" + const SECTORS = ["software", "hardware", "agriculture", "textile", "energy", "food", "construction", "media", "services", "other"] + const POLICIES = ["open", "vote", "invite"] + + const clampPercent = (n) => { + const x = parseInt(n, 10) + if (!Number.isFinite(x)) return 0 + return Math.max(0, Math.min(100, x)) + } + const clampMajority = (n) => { + const x = parseFloat(n) + if (!Number.isFinite(x)) return 0.5 + return Math.max(0.5, Math.min(1, x)) + } + const clampQuorum = (n) => { + const x = parseInt(n, 10) + if (!Number.isFinite(x) || x < 1) return 1 + return Math.min(100000, x) + } + const safeSector = (s) => { + const v = String(s || "").toLowerCase().trim() + return SECTORS.includes(v) ? v : "other" + } + const safePolicy = (p) => { + const v = String(p || "").toLowerCase().trim() + return POLICIES.includes(v) ? v : "vote" + } + const nonNegNum = (v) => { + const x = parseFloat(v) + return Number.isFinite(x) && x >= 0 ? x : 0 + } + + const INDUSTRY_TYPES = [TYPE, "industryMember", "industryInvite", "industryApply", "industryVote", "industryOpinion", "industryBlueprint", "industryBuild", "industryBuildStatus", "industryBuildDates", "industryBuildUpdate", "industryContribution", "industryAllocation", "tombstone"] + + async function getAllMsgs(ssbClient) { + return readTyped(ssbClient, INDUSTRY_TYPES, { limit: logLimit }) + } + + function chainMaps(nodeMap) { + const next = new Map(), prev = new Map(), strictNext = new Map() + for (const [key, n] of nodeMap) { + const t = n.c.replaces + if (!t) continue + next.set(t, key); prev.set(key, t) + const orig = nodeMap.get(t) + if (orig && orig.author === n.author) strictNext.set(t, key) + } + return { next, prev, strictNext } + } + + function buildIndex(messages) { + const tomb = buildValidatedTombstoneSet(messages) + const nodes = new Map() + const blueprints = new Map() + const builds = new Map() + const collab = { member: [], invite: [], apply: [], vote: [], opinion: [], buildStatus: [], buildDates: [], contribution: [], allocation: [] } + for (const m of messages) { + const c = m && m.value && m.value.content + if (!c) continue + if (c.type === "tombstone") continue + const ts = (m.value && m.value.timestamp) || 0 + const author = m.value && m.value.author + if (c.type === "industryMember") { collab.member.push({ target: c.target, author, action: c.action === "leave" ? "leave" : "join", ts }); continue } + if (c.type === "industryInvite") { collab.invite.push({ target: c.target, author, invitee: c.invitee, ts }); continue } + if (c.type === "industryApply") { collab.apply.push({ target: c.target, author, ts }); continue } + if (c.type === "industryVote") { collab.vote.push({ target: c.target, author, subject: String(c.subject || ""), ref: String(c.ref || ""), choice: c.choice === "no" ? "no" : "yes", ts }); continue } + if (c.type === "industryOpinion") { collab.opinion.push({ target: c.target, author, category: c.category, ts }); continue } + if (c.type === "industryBlueprint") { blueprints.set(m.key, { key: m.key, ts, c, author }); continue } + if (c.type === "industryBuild") { builds.set(m.key, { key: m.key, ts, c, author }); continue } + if (c.type === "industryBuildStatus") { collab.buildStatus.push({ target: c.target, author, status: String(c.status || "").toUpperCase(), ts }); continue } + if (c.type === "industryBuildDates" || c.type === "industryBuildUpdate") { collab.buildDates.push({ target: c.target, author, title: String(c.title || ""), notes: String(c.notes || ""), startDate: String(c.startDate || ""), endDate: String(c.endDate || ""), ts }); continue } + if (c.type === "industryContribution") { collab.contribution.push({ target: c.target, author, kind: c.kind, hours: nonNegNum(c.hours), item: String(c.item || ""), value: nonNegNum(c.value), eco: nonNegNum(c.eco), transferId: c.transferId || null, note: String(c.note || ""), ts }); continue } + if (c.type === "industryAllocation") { collab.allocation.push({ target: c.target, author, shares: (c.shares && typeof c.shares === "object") ? c.shares : {}, amounts: (c.amounts && typeof c.amounts === "object") ? c.amounts : {}, pot: nonNegNum(c.pot), outputValue: nonNegNum(c.outputValue), disposition: c.disposition || null, ts }); continue } + if (c.type !== TYPE) continue + nodes.set(m.key, { key: m.key, ts, c, author }) + } + const fc = chainMaps(nodes) + const bp = chainMaps(blueprints) + return { tomb, nodes, blueprints, builds, collab, naiveNext: fc.next, naivePrev: fc.prev, strictNext: fc.strictNext, bpNext: bp.next, bpPrev: bp.prev, bpStrictNext: bp.strictNext } + } + + const rootOfIdx = (idx, key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x } + const rootOfBlueprint = (idx, key) => { let x = key, g = 0; while (idx.bpPrev.has(x) && idx.blueprints.has(idx.bpPrev.get(x)) && g++ < 100000) x = idx.bpPrev.get(x); return x } + + function passesThreshold(yesCount, memberCount, quorum, majority) { + const size = Math.max(1, memberCount) + const need = Math.max(Math.min(clampQuorum(quorum), size), Math.ceil(size * clampMajority(majority))) + return yesCount >= need + } + + function actionVoteState(idx, groupRoot, members, subject, ref) { + const pref = new Map() + for (const v of idx.collab.vote.filter(x => x.target === groupRoot && x.subject === subject && x.ref === ref).sort((a, b) => a.ts - b.ts)) { + if (!members.has(v.author)) continue + pref.set(v.author, v.choice) + } + const voters = [...pref.entries()].filter(([, c]) => c === "yes").map(([m]) => m) + return { yes: voters.length, voters } + } + + function voteNeedOf(g) { + const size = Math.max(1, new Set(g.facility.members).size) + return Math.max(Math.min(clampQuorum(g.facility.quorum), size), Math.ceil(size * clampMajority(g.facility.majority))) + } + + function actionApproved(idx, g, subject, ref) { + const members = new Set(g.facility.members) + if (members.size <= 1) return true + const st = actionVoteState(idx, g.root, members, subject, ref) + return passesThreshold(st.yes, members.size, g.facility.quorum, g.facility.majority) + } + + function blueprintGovernance(idx, g, bpRoot) { + const members = new Set(g.facility.members) + const up = actionVoteState(idx, g.root, members, "bpUpdate", bpRoot) + const del = actionVoteState(idx, g.root, members, "bpDelete", bpRoot) + return { + root: bpRoot, + facilityMembers: members.size, + voteNeed: voteNeedOf(g), + updateYes: up.yes, + updateVoters: up.voters, + deleteYes: del.yes, + deleteVoters: del.voters, + updateApproved: actionApproved(idx, g, "bpUpdate", bpRoot), + deleteApproved: actionApproved(idx, g, "bpDelete", bpRoot) + } + } + + function buildGovernance(idx, g, buildId) { + const members = new Set(g.facility.members) + const up = actionVoteState(idx, g.root, members, "buildUpdate", buildId) + const del = actionVoteState(idx, g.root, members, "buildDelete", buildId) + return { + facilityMembers: members.size, + voteNeed: voteNeedOf(g), + updateYes: up.yes, + updateVoters: up.voters, + deleteYes: del.yes, + deleteVoters: del.voters, + updateApproved: actionApproved(idx, g, "buildUpdate", buildId), + deleteApproved: actionApproved(idx, g, "buildDelete", buildId) + } + } + + function resolveGroup(idx, root) { + const { nodes, collab, naiveNext, strictNext } = idx + const followStrict = (key) => { let x = key, g = 0; while (strictNext.has(x) && g++ < 100000) x = strictNext.get(x); return x } + const rootNode = nodes.get(root) + if (!rootNode) return null + const steward = rootNode.author + let tip = followStrict(root) + const tn = nodes.get(tip) + if (!tn || tn.author !== steward) tip = root + const best = nodes.get(tip) + if (!best) return null + + const groupKeys = [] + { let x = root, g = 0; groupKeys.push(x); while (naiveNext.has(x) && g++ < 100000) { x = naiveNext.get(x); groupKeys.push(x) } } + + const facility = { ...best.c, id: tip } + const policy = safePolicy(facility.membershipPolicy) + const quorum = clampQuorum(facility.quorum) + const majority = clampMajority(facility.majority) + + const KIND_ORDER = { invite: 0, apply: 1, member: 2, vote: 3 } + const events = [] + for (const e of collab.member.filter(x => x.target === root)) events.push({ kind: "member", ...e }) + for (const e of collab.invite.filter(x => x.target === root)) events.push({ kind: "invite", ...e }) + for (const e of collab.apply.filter(x => x.target === root)) events.push({ kind: "apply", ...e }) + for (const e of collab.vote.filter(x => x.target === root)) events.push({ kind: "vote", ...e }) + events.sort((a, b) => (a.ts - b.ts) || (KIND_ORDER[a.kind] - KIND_ORDER[b.kind])) + + const members = new Set([steward]) + const invites = new Set() + const applicants = new Set() + const admitVotes = new Map() + const dissolveVoters = new Set() + let dissolved = false + + const admitKey = (uid) => "admit:" + uid + for (const ev of events) { + if (ev.kind === "invite") { + if (members.has(ev.author) && ev.invitee) invites.add(ev.invitee) + continue + } + if (ev.kind === "apply") { + if (!members.has(ev.author)) applicants.add(ev.author) + continue + } + if (ev.kind === "member") { + if (ev.action === "leave") { if (ev.author !== steward) members.delete(ev.author); continue } + if (members.has(ev.author)) continue + if (policy === "open") { members.add(ev.author); applicants.delete(ev.author) } + else if (policy === "invite" && invites.has(ev.author)) { members.add(ev.author); applicants.delete(ev.author) } + continue + } + if (ev.kind === "vote") { + if (!members.has(ev.author)) continue + if (ev.subject === "admit" && ev.ref) { + const k = admitKey(ev.ref) + if (!admitVotes.has(k)) admitVotes.set(k, new Map()) + admitVotes.get(k).set(ev.author, ev.choice) + const yes = [...admitVotes.get(k).values()].filter(v => v === "yes").length + if (!members.has(ev.ref) && passesThreshold(yes, members.size, quorum, majority)) { + members.add(ev.ref); applicants.delete(ev.ref) + } + } else if (ev.subject === "dissolve") { + if (ev.choice === "yes") dissolveVoters.add(ev.author); else dissolveVoters.delete(ev.author) + if (passesThreshold(dissolveVoters.size, members.size, quorum, majority)) dissolved = true + } + continue + } + } + + const pendingApplicants = [...applicants].map(uid => { + const k = admitKey(uid) + const vm = admitVotes.get(k) || new Map() + let yes = 0, no = 0 + for (const v of vm.values()) { if (v === "yes") yes++; else no++ } + return { id: uid, yes, no, voters: [...vm.keys()] } + }) + + const opinions = {} + const opinionSet = new Set() + for (const k of groupKeys) { + const n = nodes.get(k); if (!n || n.author !== steward) continue + const c = n.c + if (c.opinions && typeof c.opinions === "object") { for (const kk of Object.keys(c.opinions)) opinions[kk] = Math.max(opinions[kk] || 0, Number(c.opinions[kk]) || 0) } + for (const v of (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])) opinionSet.add(v) + } + for (const op of collab.opinion.filter(x => x.target === root)) { if (!opinionSet.has(op.author)) { opinionSet.add(op.author); opinions[op.category] = (opinions[op.category] || 0) + 1 } } + + const pausePref = new Map() + for (const ev of events) { + if (ev.kind !== "vote" || ev.subject !== "pause") continue + if (!members.has(ev.author)) continue + pausePref.set(ev.author, ev.choice) + } + const pauseVoters = [...pausePref.entries()].filter(([, c]) => c === "yes").map(([m]) => m) + const paused = passesThreshold(pauseVoters.length, members.size, quorum, majority) + + facility.steward = steward + facility.root = root + facility.membershipPolicy = policy + facility.quorum = quorum + facility.majority = majority + facility.members = [...members] + facility.memberCount = members.size + facility.pendingApplicants = pendingApplicants + facility.invites = [...invites] + facility.opinions = opinions + facility.opinions_inhabitants = [...opinionSet] + facility.pauseVoters = pauseVoters + facility.pauseYes = pauseVoters.length + facility.status = dissolved ? "DISSOLVED" : (paused ? "PAUSED" : "ACTIVE") + + return { tip, root, best, facility, tombstoned: idx.tomb.has(tip) } + } + + const BUILD_STATUSES = ["PROPOSED", "REJECTED", "APPROVED", "STOCKING", "IN_PRODUCTION", "COMPLETED", "FAILED"] + const BUILD_TRANSITIONS = { APPROVED: ["STOCKING", "FAILED"], STOCKING: ["IN_PRODUCTION", "FAILED"], IN_PRODUCTION: ["COMPLETED", "FAILED"], FAILED: ["APPROVED", "STOCKING", "IN_PRODUCTION"], COMPLETED: ["IN_PRODUCTION"] } + const CONTRIBUTABLE = new Set(["APPROVED", "STOCKING", "IN_PRODUCTION"]) + + const normalizeMaterials = (raw, text) => { + if (Array.isArray(raw)) return raw.map(m => ({ item: String((m && m.item) || "").trim(), qty: nonNegNum(m && m.qty), price: nonNegNum(m && m.price) })).filter(m => m.item) + const t = String(text || "") + if (!t.trim()) return [] + return t.split("\n").map(line => { const parts = line.split(/[:,]/).map(x => String(x || "").trim()); return { item: parts[0] || "", qty: nonNegNum(parts[1]), price: nonNegNum(parts[2]) } }).filter(m => m.item) + } + + const safeMaterials = (v) => (Array.isArray(v) ? v : []) + const estimateBlueprint = (bp, laborRate) => { + const materialsCost = safeMaterials(bp.materials).reduce((s, m) => s + nonNegNum(m.qty) * nonNegNum(m.price), 0) + const laborCost = nonNegNum(bp.laborHours) * nonNegNum(laborRate) + return { + estMaterialsCost: Number(materialsCost.toFixed(6)), + estLaborCost: Number(laborCost.toFixed(6)), + estTotal: Number((materialsCost + laborCost).toFixed(6)) + } + } + const estimateForBlueprintRef = (idx, bpRef, laborRate) => { + if (!bpRef) return null + const b = resolveBlueprint(idx, rootOfBlueprint(idx, bpRef)) + if (!b || b.tombstoned) return null + return { blueprintName: b.blueprint.name, blueprintImage: b.blueprint.image || null, blueprintKind: b.blueprint.outKind || null, blueprintLicense: b.blueprint.license || "copyleft", ...estimateBlueprint(b.blueprint, laborRate) } + } + const normalizeSkills = (raw) => { + if (Array.isArray(raw)) return raw.map(x => String(x || "").trim()).filter(Boolean) + return String(raw || "").split(",").map(x => x.trim()).filter(Boolean) + } + + function computeShares(contributions, laborRate) { + const rate = nonNegNum(laborRate) + const points = new Map() + for (const c of contributions) { + let p = 0 + if (c.kind === "labor") p = (c.hours || 0) * rate + else if (c.kind === "material") p = (c.value || 0) + else if (c.kind === "eco") p = (c.eco || 0) + if (p <= 0) continue + points.set(c.author, (points.get(c.author) || 0) + p) + } + let total = 0 + for (const v of points.values()) total += v + const shares = new Map() + if (total > 0) for (const [k, v] of points) shares.set(k, v / total) + return { points, total, shares } + } + + function resolveBlueprint(idx, root) { + const { blueprints, bpStrictNext } = idx + const followStrict = (key) => { let x = key, g = 0; while (bpStrictNext.has(x) && g++ < 100000) x = bpStrictNext.get(x); return x } + const rootNode = blueprints.get(root) + if (!rootNode) return null + const author = rootNode.author + let tip = followStrict(root) + const tn = blueprints.get(tip) + if (!tn || tn.author !== author) tip = root + const best = blueprints.get(tip) + if (!best) return null + return { tip, root, blueprint: { ...best.c, id: tip, author }, tombstoned: idx.tomb.has(tip) } + } + + function resolveBuild(idx, root, facility) { + const node = idx.builds.get(root) + if (!node) return null + const build = { ...node.c, id: root, proposer: node.author } + const steward = facility.steward + const members = new Set(facility.members) + const approveVotes = new Map() + for (const v of idx.collab.vote.filter(x => x.subject === "build" && x.ref === root).sort((a, b) => a.ts - b.ts)) { + if (!members.has(v.author)) continue + approveVotes.set(v.author, v.choice) + } + const approveYes = [...approveVotes.values()].filter(x => x === "yes").length + const approveNo = [...approveVotes.values()].filter(x => x === "no").length + let status = "PROPOSED" + if (passesThreshold(approveYes, members.size, facility.quorum, facility.majority)) status = "APPROVED" + else if (passesThreshold(approveNo, members.size, facility.quorum, facility.majority)) status = "REJECTED" + for (const ev of idx.collab.buildStatus.filter(x => x.target === root).sort((a, b) => a.ts - b.ts)) { + const allowed = BUILD_TRANSITIONS[status] || [] + if (!allowed.includes(ev.status)) continue + const isSteward = ev.author === steward + const isProposer = ev.author === build.proposer + const stewardOnly = ev.status === "COMPLETED" || ev.status === "FAILED" + if (stewardOnly && !isSteward) continue + if (!isSteward && !isProposer) continue + status = ev.status + } + for (const ev of idx.collab.buildDates.filter(x => x.target === root).sort((a, b) => a.ts - b.ts)) { + if (ev.author !== steward && ev.author !== build.proposer) continue + if (ev.title) build.title = ev.title + if (ev.notes) build.notes = ev.notes + if (ev.startDate) build.startDate = ev.startDate + if (ev.endDate) build.endDate = ev.endDate + } + build.status = status + const contributions = idx.collab.contribution.filter(x => x.target === root && members.has(x.author)).sort((a, b) => a.ts - b.ts) + build.contributions = contributions.map(c => ({ author: c.author, kind: c.kind, hours: c.hours, item: c.item, value: c.value, eco: c.eco, note: c.note, at: new Date(c.ts || 0).toISOString() })) + const cs = computeShares(contributions, facility.laborRate) + build.points = Object.fromEntries(cs.points) + build.totalPoints = cs.total + build.shares = Object.fromEntries(cs.shares) + build.treasury = contributions.reduce((s, c) => s + (c.eco || 0), 0) + const allocs = idx.collab.allocation.filter(x => x.target === root && x.author === steward).sort((a, b) => a.ts - b.ts) + const latest = allocs[allocs.length - 1] || null + build.distributed = !!latest + build.allocation = latest ? { shares: latest.shares, amounts: latest.amounts, pot: latest.pot, outputValue: latest.outputValue, disposition: latest.disposition, at: new Date(latest.ts || 0).toISOString() } : null + build.tombstoned = idx.tomb.has(root) + return build + } + + async function getById(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + return g.facility + } + + async function resolveTipId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + return g.tip + } + + const CONTENT_COLLAB_FIELDS = ["id", "root", "steward", "members", "memberCount", "pendingApplicants", "invites", "opinions", "opinions_inhabitants", "pauseVoters", "pauseYes"] + function stripCollab(content) { + const out = { ...content } + for (const f of CONTENT_COLLAB_FIELDS) delete out[f] + out.opinions = {} + out.opinions_inhabitants = [] + return out + } + + async function publishReplace(ssbClient, currentId, content) { + const tomb = { type: "tombstone", target: currentId, deletedAt: new Date().toISOString(), author: ssbClient.id } + const updated = { ...stripCollab(content), type: TYPE, replaces: currentId, updatedAt: new Date().toISOString() } + await new Promise((res, rej) => ssbClient.publish(tomb, (e) => (e ? rej(e) : res()))) + return new Promise((res, rej) => ssbClient.publish(updated, (e, m) => (e ? rej(e) : res(m)))) + } + + const publish = (ssbClient, content) => new Promise((res, rej) => ssbClient.publish(content, (e, m) => (e ? rej(e) : res(m)))) + + return { + type: TYPE, + SECTORS, + POLICIES, + + async createFacility(data) { + const ssbClient = await openSsb() + const content = { + type: TYPE, + name: String(data.name || "").trim(), + sector: safeSector(data.sector), + description: String(data.description || "").trim(), + image: data.image || null, + mapUrl: String(data.mapUrl || "").trim(), + membershipPolicy: safePolicy(data.membershipPolicy), + quorum: clampQuorum(data.quorum), + majority: clampMajority(data.majority), + laborRate: nonNegNum(data.laborRate), + license: String(data.license || "copyleft").trim() || "copyleft", + tags: normalizeSkills(data.tags), + status: "ACTIVE", + author: ssbClient.id, + createdAt: new Date().toISOString(), + updatedAt: null, + opinions: {}, + opinions_inhabitants: [] + } + if (!content.name) throw new Error("Facility name is required") + return publish(ssbClient, content) + }, + + async updateFacility(id, patch) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.steward !== ssbClient.id) throw new Error("Unauthorized") + const current = g.facility + const nextStatus = patch.status === undefined ? current.status : String(patch.status || "").toUpperCase() + if (nextStatus === "DISSOLVED" && current.status !== "DISSOLVED") throw new Error("Dissolution requires a governance vote") + const updated = { + ...current, + ...patch, + name: patch.name === undefined ? current.name : String(patch.name || "").trim(), + sector: patch.sector === undefined ? current.sector : safeSector(patch.sector), + description: patch.description === undefined ? current.description : String(patch.description || "").trim(), + image: patch.image === undefined ? current.image : (patch.image || null), + mapUrl: patch.mapUrl === undefined ? current.mapUrl : String(patch.mapUrl || "").trim(), + membershipPolicy: patch.membershipPolicy === undefined ? current.membershipPolicy : safePolicy(patch.membershipPolicy), + quorum: patch.quorum === undefined ? current.quorum : clampQuorum(patch.quorum), + majority: patch.majority === undefined ? current.majority : clampMajority(patch.majority), + laborRate: patch.laborRate === undefined ? current.laborRate : nonNegNum(patch.laborRate), + tags: patch.tags === undefined ? current.tags : normalizeSkills(patch.tags), + status: nextStatus + } + return publishReplace(ssbClient, current.id, updated) + }, + + async deleteFacility(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.steward !== ssbClient.id) throw new Error("Unauthorized") + if (g.facility.memberCount > 1) throw new Error("A facility with members cannot be deleted; it must be dissolved by vote") + const tomb = { type: "tombstone", target: g.tip, deletedAt: new Date().toISOString(), author: ssbClient.id } + return publish(ssbClient, tomb) + }, + + async joinFacility(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.status === "DISSOLVED") throw new Error("Facility is dissolved") + const uid = ssbClient.id + if (g.facility.members.includes(uid)) throw new Error("Already a member") + const policy = g.facility.membershipPolicy + if (policy === "vote") return publish(ssbClient, { type: "industryApply", target: g.root, createdAt: new Date().toISOString() }) + if (policy === "invite" && !g.facility.invites.includes(uid)) throw new Error("You need an invitation to join") + return publish(ssbClient, { type: "industryMember", target: g.root, action: "join", createdAt: new Date().toISOString() }) + }, + + async leaveFacility(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + const uid = ssbClient.id + if (uid === g.facility.steward) throw new Error("The steward cannot leave; dissolve the facility instead") + if (!g.facility.members.includes(uid)) throw new Error("Not a member") + return publish(ssbClient, { type: "industryMember", target: g.root, action: "leave", createdAt: new Date().toISOString() }) + }, + + async inviteToFacility(id, invitee) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can invite") + if (!/^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(String(invitee || ""))) throw new Error("Invalid invitee id") + return publish(ssbClient, { type: "industryInvite", target: g.root, invitee: String(invitee), createdAt: new Date().toISOString() }) + }, + + async voteGovernance(id, subject, ref, choice) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can vote") + const subj = String(subject || "") + const SUBJECTS = ["admit", "dissolve", "pause", "bpUpdate", "bpDelete", "buildUpdate", "buildDelete"] + if (!SUBJECTS.includes(subj)) throw new Error("Invalid governance subject") + if (subj !== "admit" && subj !== "dissolve" && subj !== "pause" && !String(ref || "").trim()) throw new Error("Governance vote requires a target") + return publish(ssbClient, { type: "industryVote", target: g.root, subject: subj, ref: String(ref || ""), choice: choice === "no" ? "no" : "yes", createdAt: new Date().toISOString() }) + }, + + async createOpinion(id, category) { + const categories = require('../backend/opinion_categories') + if (!categories.includes(category)) throw new Error('Invalid opinion category') + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error('Facility not found') + if ((g.facility.opinions_inhabitants || []).includes(ssbClient.id)) throw new Error('Already opined') + return publish(ssbClient, { type: 'industryOpinion', target: g.root, category, createdAt: new Date().toISOString() }) + }, + + async getFacilityById(id) { + return getById(id) + }, + + async listFacilities(filter = "ALL") { + const ssbClient = await openSsb() + const uid = ssbClient.id + const idx = buildIndex(await getAllMsgs(ssbClient)) + const roots = new Set() + for (const key of idx.nodes.keys()) roots.add(rootOfIdx(idx, key)) + let list = [] + for (const root of roots) { + const g = resolveGroup(idx, root) + if (!g || g.tombstoned) continue + list.push(g.facility) + } + const f = String(filter || "ALL").toUpperCase() + if (f === "MINE") list = list.filter(x => x.steward === uid) + else if (f === "RECENT") list = list.filter(x => (Date.parse(x.createdAt || "") || 0) >= Date.now() - 86400000) + else if (f === "MEMBER") list = list.filter(x => Array.isArray(x.members) && x.members.includes(uid)) + else if (f === "ACTIVE") list = list.filter(x => x.status === "ACTIVE") + else if (f === "PAUSED") list = list.filter(x => x.status === "PAUSED") + else if (f === "DISSOLVED") list = list.filter(x => x.status === "DISSOLVED") + else if (SECTORS.includes(f.toLowerCase())) list = list.filter(x => x.sector === f.toLowerCase()) + list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return list + }, + + async facilitiesOf(feedId) { + const ssbClient = await openSsb() + const uid = feedId || ssbClient.id + const idx = buildIndex(await getAllMsgs(ssbClient)) + const roots = new Set() + for (const key of idx.nodes.keys()) roots.add(rootOfIdx(idx, key)) + const list = [] + for (const root of roots) { + const g = resolveGroup(idx, root) + if (!g || g.tombstoned) continue + if (Array.isArray(g.facility.members) && g.facility.members.includes(uid)) list.push(g.facility) + } + list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return list + }, + + async listMyBuilds() { + const ssbClient = await openSsb() + const uid = ssbClient.id + const idx = buildIndex(await getAllMsgs(ssbClient)) + const facRoots = new Set() + for (const key of idx.nodes.keys()) facRoots.add(rootOfIdx(idx, key)) + const myFacilities = new Map() + for (const root of facRoots) { + const g = resolveGroup(idx, root) + if (!g || g.tombstoned) continue + if (Array.isArray(g.facility.members) && g.facility.members.includes(uid)) myFacilities.set(g.root, g.facility) + } + const out = [] + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + const fac = myFacilities.get(node.c.facility) + if (!fac) continue + const build = resolveBuild(idx, key, fac) + build.facilityId = fac.root + build.facilityName = fac.name + out.push(build) + } + out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return out + }, + + async createBlueprint(facilityId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can add blueprints") + const content = { + type: "industryBlueprint", + facility: g.root, + name: String(data.name || "").trim(), + description: String(data.description || "").trim(), + image: data.image || null, + sector: safeSector(data.sector || g.facility.sector), + materials: normalizeMaterials(data.materials, data.materialsText), + laborHours: nonNegNum(data.laborHours), + skills: normalizeSkills(data.skills), + outItem: String(data.outItem || "").trim(), + outQty: nonNegNum(data.outQty), + outUnit: String(data.outUnit || "unit").trim() || "unit", + outKind: String(data.outKind || "physical").toLowerCase() === "digital" ? "digital" : "physical", + license: String(data.license || "copyleft").trim() || "copyleft", + createdAt: new Date().toISOString() + } + if (!content.name) throw new Error("Blueprint name is required") + return publish(ssbClient, content) + }, + + async updateBlueprint(id, patch) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) + if (!b || b.tombstoned) throw new Error("Blueprint not found") + if (b.blueprint.author !== ssbClient.id) throw new Error("Unauthorized") + const gb = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) + if (!gb || gb.tombstoned) throw new Error("Facility not found") + if (!gb.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + if (!actionApproved(idx, gb, "bpUpdate", b.root)) throw new Error("Updating this blueprint requires a governance vote") + const cur = b.blueprint + const updated = { + type: "industryBlueprint", + facility: cur.facility, + name: patch.name === undefined ? cur.name : String(patch.name || "").trim(), + description: patch.description === undefined ? (cur.description || "") : String(patch.description || "").trim(), + image: patch.image === undefined ? (cur.image || null) : (patch.image || null), + sector: patch.sector === undefined ? cur.sector : safeSector(patch.sector), + materials: patch.materials === undefined && patch.materialsText === undefined ? cur.materials : normalizeMaterials(patch.materials, patch.materialsText), + laborHours: patch.laborHours === undefined ? cur.laborHours : nonNegNum(patch.laborHours), + skills: patch.skills === undefined ? cur.skills : normalizeSkills(patch.skills), + outItem: patch.outItem === undefined ? cur.outItem : String(patch.outItem || "").trim(), + outQty: patch.outQty === undefined ? cur.outQty : nonNegNum(patch.outQty), + outUnit: patch.outUnit === undefined ? cur.outUnit : String(patch.outUnit || "unit").trim() || "unit", + outKind: patch.outKind === undefined ? cur.outKind : (String(patch.outKind).toLowerCase() === "digital" ? "digital" : "physical"), + license: patch.license === undefined ? cur.license : String(patch.license || "copyleft").trim() || "copyleft", + replaces: b.tip, + createdAt: cur.createdAt, + updatedAt: new Date().toISOString() + } + const tomb = { type: "tombstone", target: b.tip, deletedAt: new Date().toISOString(), author: ssbClient.id } + await publish(ssbClient, tomb) + return publish(ssbClient, updated) + }, + + async deleteBlueprint(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) + if (!b || b.tombstoned) throw new Error("Blueprint not found") + const g = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + if (b.blueprint.author !== ssbClient.id) throw new Error("Only the author can delete a blueprint") + if (!actionApproved(idx, g, "bpDelete", b.root)) throw new Error("Deleting this blueprint requires a governance vote") + const chain = [] + { let x = b.root, guard = 0; chain.push(x); while (idx.bpNext.has(x) && guard++ < 100000) { x = idx.bpNext.get(x); chain.push(x) } } + const chainSet = new Set(chain) + let usedBy = 0 + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + if (chainSet.has(node.c.blueprint)) usedBy++ + } + if (usedBy > 0) throw new Error(`This blueprint is used by ${usedBy} build${usedBy > 1 ? "s" : ""}; delete them first`) + let last = null + for (const key of chain) { + const node = idx.blueprints.get(key) + if (!node || node.author !== ssbClient.id) continue + if (idx.tomb.has(key)) continue + last = await publish(ssbClient, { type: "tombstone", target: key, deletedAt: new Date().toISOString(), author: ssbClient.id }) + } + return last + }, + + async getBlueprint(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) + if (!b || b.tombstoned) throw new Error("Blueprint not found") + const g = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + const builds = [] + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + if (node.c.blueprint !== b.tip && node.c.blueprint !== b.root) continue + const rb = resolveBuild(idx, key, g.facility) + if (rb) builds.push({ id: key, title: rb.title, status: rb.status, startDate: rb.startDate, endDate: rb.endDate }) + } + builds.sort((x, y) => new Date(y.startDate || 0) - new Date(x.startDate || 0)) + return { + ...b.blueprint, + facilityId: g.root, + facilityName: g.facility.name, + members: g.facility.members, + builds, + ...estimateBlueprint(b.blueprint, g.facility.laborRate), + ...blueprintGovernance(idx, g, b.root) + } + }, + + async listBlueprints(facilityId) { + const ssbClient = await openSsb() + const msgs = await getAllMsgs(ssbClient) + const idx = buildIndex(msgs) + const root = rootOfIdx(idx, facilityId) + const g = resolveGroup(idx, root) + const laborRate = g && !g.tombstoned ? g.facility.laborRate : 0 + const roots = new Set() + for (const key of idx.blueprints.keys()) roots.add(rootOfBlueprint(idx, key)) + const list = [] + for (const r of roots) { + const b = resolveBlueprint(idx, r) + if (!b || b.tombstoned) continue + if (b.blueprint.facility !== root) continue + list.push({ ...b.blueprint, ...estimateBlueprint(b.blueprint, laborRate), ...(g && !g.tombstoned ? blueprintGovernance(idx, g, b.root) : {}) }) + } + list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return list + }, + + async createBuild(facilityId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can propose builds") + let blueprintId = String(data.blueprintId || "").trim() || null + if (!blueprintId) throw new Error("A build needs a blueprint") + { + const b = resolveBlueprint(idx, rootOfBlueprint(idx, blueprintId)) + if (!b || b.tombstoned || b.blueprint.facility !== g.root) throw new Error("Blueprint not found in this facility") + blueprintId = b.tip + } + const content = { type: "industryBuild", facility: g.root, blueprint: blueprintId, title: String(data.title || "").trim(), notes: String(data.notes || "").trim(), image: data.image || null, startDate: String(data.startDate || "").trim() || null, endDate: String(data.endDate || "").trim() || null, createdAt: new Date().toISOString() } + if (!content.title) throw new Error("Build title is required") + const today = new Date().toISOString().slice(0, 10) + if (!content.startDate) throw new Error("Build start date is required") + if (!content.endDate) throw new Error("Build end date is required") + if (content.startDate < today) throw new Error("Start date cannot be in the past") + if (content.startDate && content.endDate && new Date(content.endDate) < new Date(content.startDate)) throw new Error("End date must be after start date") + return publish(ssbClient, content) + }, + + async voteBuild(buildId, choice) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can vote") + return publish(ssbClient, { type: "industryVote", target: g.root, subject: "build", ref: buildId, choice: choice === "no" ? "no" : "yes", createdAt: new Date().toISOString() }) + }, + + async setBuildStatus(buildId, status) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + const build = resolveBuild(idx, buildId, g.facility) + const next = String(status || "").toUpperCase() + if (!BUILD_STATUSES.includes(next)) throw new Error("Invalid status") + const allowed = BUILD_TRANSITIONS[build.status] || [] + if (!allowed.includes(next)) throw new Error(`Cannot move build from ${build.status} to ${next}`) + if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + const isSteward = g.facility.steward === ssbClient.id + const isProposer = build.proposer === ssbClient.id + const isReopen = build.status === "COMPLETED" || build.status === "FAILED" + if (build.status === "COMPLETED" && build.distributed) throw new Error("A distributed build cannot be reopened") + if ((next === "COMPLETED" || next === "FAILED" || isReopen) && !isSteward) throw new Error("Only the steward can complete, fail or reopen a build") + if (!isSteward && !isProposer) throw new Error("Only the steward or the proposer can advance the build") + return publish(ssbClient, { type: "industryBuildStatus", target: buildId, status: next, createdAt: new Date().toISOString() }) + }, + + async contribute(buildId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can contribute") + if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") + const build = resolveBuild(idx, buildId, g.facility) + if (!CONTRIBUTABLE.has(build.status)) throw new Error("This build is not accepting contributions") + const kind = ["labor", "material", "eco"].includes(String(data.kind)) ? String(data.kind) : "labor" + const content = { type: "industryContribution", target: buildId, kind, hours: nonNegNum(data.hours), item: String(data.item || "").trim(), value: nonNegNum(data.value), eco: nonNegNum(data.eco), transferId: data.transferId || null, note: String(data.note || "").trim(), createdAt: new Date().toISOString() } + if (kind === "labor") { + if (content.hours <= 0) throw new Error("Labor contributions require hours above zero") + content.item = ""; content.value = 0; content.eco = 0 + } else if (kind === "material") { + if (!content.item) throw new Error("Material contributions require an item") + if (content.value <= 0) throw new Error("Material contributions require a value above zero") + content.hours = 0; content.eco = 0 + } else { + if (content.eco <= 0) throw new Error("ECO contributions require an amount above zero") + content.hours = 0; content.item = ""; content.value = 0 + } + return publish(ssbClient, content) + }, + + async getBuild(buildId) { + const ssbClient = await openSsb() + const msgs = await getAllMsgs(ssbClient) + const idx = buildIndex(msgs) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g) throw new Error("Facility not found") + const build = resolveBuild(idx, buildId, g.facility) + build.facilityId = g.root + build.facilityName = g.facility.name + build.steward = g.facility.steward + build.members = g.facility.members + const est = estimateForBlueprintRef(idx, build.blueprint, g.facility.laborRate) + if (est) Object.assign(build, est) + Object.assign(build, buildGovernance(idx, g, buildId)) + return build + }, + + async updateBuild(buildId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + const build = resolveBuild(idx, buildId, g.facility) + const isSteward = g.facility.steward === ssbClient.id + if (build.proposer !== ssbClient.id && !isSteward) throw new Error("Only the steward or the proposer can update a build") + if (build.distributed) throw new Error("A distributed build cannot be updated") + if (!actionApproved(idx, g, "buildUpdate", buildId)) throw new Error("Updating this build requires a governance vote") + const startDate = String(data.startDate || "").trim() + const endDate = String(data.endDate || "").trim() + if (!startDate) throw new Error("Build start date is required") + if (!endDate) throw new Error("Build end date is required") + if (new Date(endDate) < new Date(startDate)) throw new Error("End date must be after start date") + const today = new Date().toISOString().slice(0, 10) + if (endDate < today) throw new Error("End date cannot be in the past") + const title = data.title === undefined ? String(build.title || "") : String(data.title || "").trim() + if (!title) throw new Error("Build title is required") + const notes = data.notes === undefined ? String(build.notes || "") : String(data.notes || "").trim() + return publish(ssbClient, { type: "industryBuildUpdate", target: buildId, title, notes, startDate, endDate, createdAt: new Date().toISOString() }) + }, + + async deleteBuild(buildId) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + if (bnode.author !== ssbClient.id) throw new Error("Only the proposer can delete a build") + if (!actionApproved(idx, g, "buildDelete", buildId)) throw new Error("Deleting this build requires a governance vote") + return publish(ssbClient, { type: "tombstone", target: buildId, deletedAt: new Date().toISOString(), author: ssbClient.id }) + }, + + async listBuilds(facilityId, filter = "ALL") { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) + if (!g || g.tombstoned) throw new Error("Facility not found") + let list = [] + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + if (node.c.facility !== g.root) continue + const b = resolveBuild(idx, key, g.facility) + if (!b) continue + const est = estimateForBlueprintRef(idx, b.blueprint, g.facility.laborRate) + list.push({ ...b, ...(est || {}), ...buildGovernance(idx, g, key) }) + } + const f = String(filter || "ALL").toUpperCase() + if (BUILD_STATUSES.includes(f)) list = list.filter(x => x.status === f) + list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return list + }, + + async listAllBlueprints() { + const ssbClient = await openSsb() + const msgs = await getAllMsgs(ssbClient) + const idx = buildIndex(msgs) + const roots = new Set() + for (const key of idx.blueprints.keys()) roots.add(rootOfBlueprint(idx, key)) + const facCache = new Map() + const list = [] + for (const root of roots) { + const b = resolveBlueprint(idx, root) + if (!b || b.tombstoned) continue + const facRoot = rootOfIdx(idx, b.blueprint.facility) + let g = facCache.get(facRoot) + if (g === undefined) { g = resolveGroup(idx, facRoot); facCache.set(facRoot, g) } + if (!g || g.tombstoned) continue + list.push({ ...b.blueprint, facilityId: g.root, facilityName: g.facility.name, members: g.facility.members, ...estimateBlueprint(b.blueprint, g.facility.laborRate), ...blueprintGovernance(idx, g, b.root) }) + } + list.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)) + return list + }, + + async listAllBuilds() { + const ssbClient = await openSsb() + const msgs = await getAllMsgs(ssbClient) + const idx = buildIndex(msgs) + const facCache = new Map() + const list = [] + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + const facRoot = rootOfIdx(idx, node.c.facility) + let g = facCache.get(facRoot) + if (g === undefined) { g = resolveGroup(idx, facRoot); facCache.set(facRoot, g) } + if (!g || g.tombstoned) continue + const b = resolveBuild(idx, key, g.facility) + if (!b) continue + const est = estimateForBlueprintRef(idx, b.blueprint, g.facility.laborRate) + list.push({ ...b, facilityId: g.root, facilityName: g.facility.name, members: g.facility.members, ...(est || {}), ...buildGovernance(idx, g, key) }) + } + list.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)) + return list + }, + + computeDistributionPlan(build, outputValue) { + const pot = nonNegNum(build.treasury) + nonNegNum(outputValue) + const distributable = pot + const allocations = [] + const amounts = {} + for (const [uid, share] of Object.entries(build.shares || {})) { + const amount = distributable * share + amounts[uid] = amount + if (amount > 0) allocations.push({ to: uid, amount, share }) + } + return { pot, distributable, allocations, amounts } + }, + + async distributeBuild(buildId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.steward !== ssbClient.id) throw new Error("Only the steward can distribute") + const build = resolveBuild(idx, buildId, g.facility) + if (build.status !== "COMPLETED") throw new Error("Only a completed build can be distributed") + if (build.distributed) throw new Error("This build has already been distributed") + if (build.totalPoints <= 0) throw new Error("There are no contributions to distribute") + const plan = this.computeDistributionPlan(build, data && data.outputValue) + const content = { type: "industryAllocation", target: buildId, shares: build.shares, amounts: plan.amounts, pot: plan.pot, outputValue: nonNegNum(data && data.outputValue), disposition: (data && data.disposition) || null, createdAt: new Date().toISOString() } + await publish(ssbClient, content) + return { plan, facility: g.facility, build } + } + } +} diff --git a/src/models/inhabitants_model.js b/src/models/inhabitants_model.js index 2b3c0dd..a594a75 100644 --- a/src/models/inhabitants_model.js +++ b/src/models/inhabitants_model.js @@ -1,11 +1,7 @@ const pull = require('../server/node_modules/pull-stream'); -const ssbClientGUI = require("../client/gui"); -const coolerInstance = ssbClientGUI({ offline: require('../server/ssb_config').offline }); +const { readTyped, CONTENT_TYPES, requestScope, authorActivity } = require('./typed_log'); +const { isContentVisibleTo } = require('./content_visibility'); const models = require("../models/main_models"); -const { about, friend } = models({ - cooler: coolerInstance, - isPublic: require('../server/ssb_config').public, -}); const { getConfig } = require('../configs/config-manager.js'); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -15,36 +11,48 @@ function toImageUrl(imgId, size=256){ return `/image/${size}/${encodeURIComponent(imgId)}`; } -module.exports = ({ cooler }) => { +const MIN_SUGGESTION_AFFINITY = 0.1; +const MAX_SUGGESTED = 12; + +module.exports = ({ cooler, tribesModel = null, dataModel = null }) => { + const { about, friend } = models({ cooler, isPublic: require('../server/ssb_config').public }); let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; - async function getLastKarmaScore(feedId) { + const bytesPerAuthor = (ssbClient) => readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }).then((msgs) => { + const acc = {}; + for (const m of msgs) { + const author = m && m.value && m.value.author; + if (!author) continue; + try { acc[author] = (acc[author] || 0) + Buffer.byteLength(JSON.stringify(m.value), 'utf8'); } catch (_) {} + } + return acc; + }).catch(() => ({})); + + async function latestKarmaByAuthor() { const ssbClient = await openSsb(); return new Promise(resolve => { + const out = new Map(); const src = ssbClient.messagesByType ? ssbClient.messagesByType({ type: "karmaScore", reverse: true }) : ssbClient.createLogStream && ssbClient.createLogStream({ reverse: true }); - if (!src) return resolve(0); + if (!src) return resolve(out); pull( src, - pull.filter(msg => { + pull.drain(msg => { const v = msg.value || msg; const c = v.content || {}; - return v.author === feedId && c.type === "karmaScore" && typeof c.karmaScore !== "undefined"; - }), - pull.take(1), - pull.collect((err, arr) => { - if (err || !arr || !arr.length) return resolve(0); - const v = arr[0].value || arr[0]; - resolve(v.content.karmaScore || 0); - }) + if (c.type !== "karmaScore" || typeof c.karmaScore === "undefined" || out.has(v.author)) return; + out.set(v.author, c.karmaScore || 0); + }, () => resolve(out)) ); }); } async function getLastActivityTimestamp(feedId) { const ssbClient = await openSsb(); + const activity = await authorActivity(ssbClient); + if (activity) return activity.get(feedId) || null; const norm = (t) => (t && t < 1e12 ? t * 1000 : t || 0); return new Promise((resolve) => { pull( @@ -82,21 +90,34 @@ module.exports = ({ cooler }) => { } }; - async function listAllBase(ssbClient) { - const authorsMsgs = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true }), - pull.filter(msg => !!msg.value?.author && msg.value?.content?.type !== 'tombstone'), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); - const uniqueFeedIds = Array.from(new Set(authorsMsgs.map(r => r.value.author).filter(Boolean))); + const publicMode = require('../server/ssb_config').public; + const imageUrlFrom = (raw, size = 256) => { + const img = raw == null || raw.link == null ? `&${"0".repeat(43)}=.sha256` : (typeof raw.link === 'string' ? raw.link : raw); + const id = typeof img === 'string' ? img : (img && (img.link || img.url)); + return toImageUrl(id, size); + }; + + async function listAllBase(ssbClient, onlyIds = null) { + const store = requestScope.getStore(); + const memoKey = onlyIds ? null : 'inhabitantsBase'; + if (memoKey && store && store[memoKey]) return store[memoKey]; + const pending = buildBase(ssbClient, onlyIds); + if (memoKey && store) store[memoKey] = pending; + return pending; + } + + async function buildBase(ssbClient, onlyIds) { + const only = onlyIds ? new Set(onlyIds) : null; + const authorsMsgs = (await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true })).filter(msg => !!msg.value?.author && msg.value?.content?.type !== 'tombstone').reverse(); + const uniqueFeedIds = Array.from(new Set(authorsMsgs.map(r => r.value.author).filter(Boolean))).filter(id => !only || only.has(id)); + const profiles = publicMode ? null : await about.profiles().catch(() => null); const users = await Promise.all( uniqueFeedIds.map(async (feedId) => { - const rawName = await about.name(feedId); + const p = profiles ? (profiles.get(feedId) || {}) : null; + const rawName = p ? (p.name || feedId.slice(1, 9)) : await about.name(feedId); const name = rawName || feedId.slice(0, 10); - const description = await about.description(feedId); - const photo = await fetchUserImageUrl(feedId, 256); + const description = p ? (p.description || "") : await about.description(feedId); + const photo = p ? imageUrlFrom(p.image, 256) : await fetchUserImageUrl(feedId, 256); const lastActivityTs = await getLastActivityTimestamp(feedId); const { bucket, range } = bucketLastActivity(lastActivityTs); return { id: feedId, name, description, photo, lastActivityTs, lastActivityBucket: bucket, lastActivityRange: range }; @@ -115,8 +136,14 @@ module.exports = ({ cooler }) => { } return { + async countInhabitants() { + const ssbClient = await openSsb(); + const msgs = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); + return new Set(msgs.filter(msg => !!msg.value?.author && msg.value?.content?.type !== 'tombstone').map(msg => msg.value.author)).size; + }, + async listInhabitants(options = {}) { - const { filter = 'all', search = '', location = '', language = '', skills = '', includeInactive = false } = options; + const { filter = 'all', search = '', location = '', language = '', skills = '', includeInactive = false, ids = null } = options; const ssbClient = await openSsb(); const userId = ssbClient.id; @@ -130,9 +157,9 @@ module.exports = ({ cooler }) => { return filterInactive(users); } - if (filter === 'all' || filter === 'TOP KARMA' || filter === 'TOP ACTIVITY' || filter === 'TOP ECO') { - let users = await listAllBase(ssbClient); - if (filter !== 'TOP ACTIVITY') { + if (filter === 'all' || filter === 'TOP KARMA' || filter === 'TOP ACTIVITY' || filter === 'TOP INACTIVITY' || filter === 'TOP ECO') { + let users = (await listAllBase(ssbClient, ids)).slice(); + if (filter !== 'TOP ACTIVITY' && filter !== 'TOP INACTIVITY') { users = filterInactive(users); } if (search) { @@ -143,23 +170,12 @@ module.exports = ({ cooler }) => { (u.id || '').toLowerCase().includes(q) ); } - const bytesByAuthor = await new Promise((res) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => { - if (err || !Array.isArray(msgs)) return res({}); - const acc = {}; - for (const m of msgs) { - const author = m && m.value && m.value.author; - if (!author) continue; - try { acc[author] = (acc[author] || 0) + Buffer.byteLength(JSON.stringify(m.value), 'utf8'); } catch (_) {} - } - res(acc); - }) - ); - }); + const store = requestScope.getStore(); + if (store && !store.inhabitantBytes) store.inhabitantBytes = bytesPerAuthor(ssbClient); + const bytesByAuthor = await (store ? store.inhabitantBytes : bytesPerAuthor(ssbClient)); + const karmaOf = await latestKarmaByAuthor(); const withMetrics = await Promise.all(users.map(async u => { - const karmaScore = await getLastKarmaScore(u.id); + const karmaScore = karmaOf.get(u.id) || 0; const bytes = (bytesByAuthor && bytesByAuthor[u.id]) || 0; const carbonGrams = (bytes / (1024 * 1024)) * 0.095; if (filter === 'TOP ECO') { @@ -170,6 +186,7 @@ module.exports = ({ cooler }) => { })); if (filter === 'TOP KARMA') return withMetrics.sort((a, b) => (b.karmaScore || 0) - (a.karmaScore || 0)); if (filter === 'TOP ACTIVITY') return withMetrics.sort((a, b) => (b.lastActivityTs || 0) - (a.lastActivityTs || 0)); + if (filter === 'TOP INACTIVITY') return withMetrics.sort((a, b) => (a.lastActivityTs || 0) - (b.lastActivityTs || 0)); if (filter === 'TOP ECO') return withMetrics.sort((a, b) => (b.ecoScore || 0) - (a.ecoScore || 0)); return withMetrics; } @@ -198,80 +215,54 @@ module.exports = ({ cooler }) => { if (filter === 'SUGGESTED') { const base = await listAllBase(ssbClient); const active = filterInactive(base); - const cvRecords = await new Promise((res) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true }), - pull.filter(msg => msg && msg.value && msg.value.content && msg.value.content.type === 'curriculum'), - pull.collect((err, msgs) => err ? res([]) : res(msgs)) - ); - }); - const cvByAuthor = new Map(); - for (const r of cvRecords) { - const c = r.value && r.value.content; - if (c && c.author && !cvByAuthor.has(c.author)) cvByAuthor.set(c.author, c); - } - const extractSkills = (cv) => cv ? [ - ...(cv.personalSkills || []), - ...(cv.oasisSkills || []), - ...(cv.educationalSkills || []), - ...(cv.professionalSkills || []) - ].map(s => String(s || '').toLowerCase()).filter(Boolean) : []; - const mecv = await this.getCVByUserId().catch(() => null); - const mySkills = extractSkills(mecv); + const affinities = dataModel ? await dataModel.authorAffinities().catch(() => null) : null; + const byAuthor = affinities ? affinities.byAuthor : new Map(); + const karmaOf = await latestKarmaByAuthor(); const rels = await Promise.all( active.map(async u => { if (u.id === userId) return null; const rel = await friend.getRelationship(u.id).catch(() => ({})); const n = normalizeRel(rel); if (n.iFollow || n.blocking || n.blockedBy) return null; - const karmaScore = await getLastKarmaScore(u.id); - const theirSkills = extractSkills(cvByAuthor.get(u.id)); - const commonSkills = mySkills.length && theirSkills.length - ? Array.from(new Set(mySkills.filter(s => theirSkills.includes(s)))) - : []; - const followsMeBonus = n.followsMe ? 20 : 0; - const karmaBonus = Math.min(20, Math.log10(1 + Math.max(0, karmaScore)) * 5); - const skillBonus = commonSkills.length * 4; - const activityBonus = u.lastActivityBucket === 'green' ? 5 : (u.lastActivityBucket === 'orange' ? 2 : 0); - const suggestionScore = followsMeBonus + karmaBonus + skillBonus + activityBonus; - return { user: u, rel: n, karmaScore, commonSkills, suggestionScore }; + const aff = byAuthor.get(u.id) || { score: 0, common: [], reasons: [] }; + const tribeMate = aff.reasons.includes('tribe'); + if (aff.score < MIN_SUGGESTION_AFFINITY && !n.followsMe && !tribeMate) return null; + const social = (n.followsMe ? 0.15 : 0) + (tribeMate ? 0.1 : 0); + const activityBonus = u.lastActivityBucket === 'green' ? 0.05 : (u.lastActivityBucket === 'orange' ? 0.02 : 0); + const suggestionScore = Math.min(1, aff.score + social + activityBonus); + const karmaScore = karmaOf.get(u.id) || 0; + return { user: u, rel: n, karmaScore, commonSkills: aff.common, reasons: aff.reasons, suggestionScore }; }) ); - const candidates = rels.filter(Boolean).filter(x => x.suggestionScore > 0); + const candidates = rels.filter(Boolean); const enriched = candidates.map(x => ({ ...x.user, karmaScore: x.karmaScore, followsYou: x.rel.followsMe, commonSkills: x.commonSkills, + reasons: x.reasons, mutualCount: x.rel.followsMe ? 1 : 0, - suggestionScore: x.suggestionScore + suggestionScore: x.suggestionScore, + affinity: x.suggestionScore })); const unique = Array.from(new Map(enriched.map(u => [u.id, u])).values()); return unique.sort((a, b) => (b.suggestionScore || 0) - (a.suggestionScore || 0) || (b.karmaScore || 0) - (a.karmaScore || 0) || (b.lastActivityTs || 0) - (a.lastActivityTs || 0) - ); + ).slice(0, MAX_SUGGESTED); } - if (filter === 'CVs' || filter === 'MATCHSKILLS') { - const records = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true}), - pull.filter(msg => - msg.value.content?.type === 'curriculum' && - msg.value.content?.type !== 'tombstone' - ), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + if (filter === 'CVs') { + const records = (await readTyped(ssbClient, ['curriculum'], { limit: logLimit })).filter(msg => msg.value.content?.type === 'curriculum').reverse(); let cvs = records.map(r => r.value.content); cvs = Array.from(new Map(cvs.map(u => [u.author, u])).values()); if (filter === 'CVs') { + const profiles = publicMode ? null : await about.profiles().catch(() => null); let out = await Promise.all(cvs.map(async c => { - const photo = await fetchUserImageUrl(c.author, 256); + const photo = profiles ? imageUrlFrom((profiles.get(c.author) || {}).image, 256) : await fetchUserImageUrl(c.author, 256); const lastActivityTs = await getLastActivityTimestamp(c.author); const { bucket, range } = bucketLastActivity(lastActivityTs); const base = this._normalizeCurriculum(c, photo); @@ -295,59 +286,19 @@ module.exports = ({ cooler }) => { return out; } - if (filter === 'MATCHSKILLS') { - let base = await Promise.all(cvs.map(async c => { - const photo = await fetchUserImageUrl(c.author, 256); - const lastActivityTs = await getLastActivityTimestamp(c.author); - const { bucket, range } = bucketLastActivity(lastActivityTs); - const norm = this._normalizeCurriculum(c, photo); - const karmaScore = await getLastKarmaScore(c.author).catch(() => 0); - return { ...norm, lastActivityTs, lastActivityBucket: bucket, lastActivityRange: range, karmaScore }; - })); - base = filterInactive(base); - const mecv = await this.getCVByUserId(); - const userSkills = Array.from(new Set( - (mecv - ? [ - ...(mecv.personalSkills || []), - ...(mecv.oasisSkills || []), - ...(mecv.educationalSkills || []), - ...(mecv.professionalSkills || []) - ] - : []).map(s => String(s || '').toLowerCase()).filter(Boolean) - )); - if (!userSkills.length) return []; - const userSet = new Set(userSkills); - const matches = base.map(c => { - if (c.id === userId) return null; - const theirSkillsRaw = (c.skills || []).map(s => String(s || '').toLowerCase()).filter(Boolean); - const theirSet = new Set(theirSkillsRaw); - const common = Array.from(theirSet).filter(s => userSet.has(s)); - if (!common.length) return null; - const unionSize = userSet.size + theirSet.size - common.length; - const matchScore = unionSize > 0 ? common.length / unionSize : 0; - const matchCoverage = userSet.size > 0 ? common.length / userSet.size : 0; - return { ...c, commonSkills: common, matchScore, matchCoverage }; - }).filter(Boolean); - return matches.sort((a, b) => - (b.matchScore - a.matchScore) || - (b.commonSkills.length - a.commonSkills.length) || - ((b.karmaScore || 0) - (a.karmaScore || 0)) || - ((b.lastActivityTs || 0) - (a.lastActivityTs || 0)) - ); - } } return []; }, _normalizeCurriculum(c, photoUrl) { - const photo = photoUrl || toImageUrl(c.photo, 256); + const photo = c.photo ? toImageUrl(c.photo, 256) : (photoUrl || toImageUrl(null, 256)); return { id: c.author, name: c.name, description: c.description, photo, + pdf: c.pdf || null, skills: [ ...(c.personalSkills || []), ...(c.oasisSkills || []), @@ -358,10 +309,20 @@ module.exports = ({ cooler }) => { languages: typeof c.languages === 'string' ? c.languages.split(',').map(x => x.trim()) : Array.isArray(c.languages) ? c.languages : [], + status: c.status, + preferences: c.preferences, createdAt: c.createdAt }; }, + async listSupporters(feedId) { + const ssbClient = await openSsb(); + const graph = await new Promise((resolve) => ssbClient.friends.graph((err, g) => resolve(err ? {} : (g || {})))); + return Object.entries(graph) + .filter(([source, edges]) => source !== feedId && edges && (edges[feedId] === true || Number(edges[feedId]) > 0)) + .map(([source]) => source); + }, + async getLatestAboutById(id) { const ssbClient = await openSsb(); const records = await new Promise((res, rej) => { @@ -407,20 +368,8 @@ module.exports = ({ cooler }) => { const isOwner = viewer === target; const arr = (v) => Array.isArray(v) ? v : []; const up = (v) => String(v || '').toUpperCase(); - const COUNTED = new Set(['post','event','task','forum','tribe','market','job','project','shop','image','video','audio','document','bookmark','transfer','map']); - const accessible = (type, c) => { - if (c.encryptedPayload) return false; - switch (type) { - case 'task': return up(c.isPublic) !== 'PRIVATE' || isOwner || arr(c.assignees).includes(viewer); - case 'event': return String(c.isPublic || '').toLowerCase() !== 'private' || isOwner || arr(c.attendees).includes(viewer); - case 'forum': return c.isPrivate !== true || isOwner; - case 'job': return up(c.visibility) !== 'HIDDEN' || isOwner || arr(c.subscribers).includes(viewer); - case 'market': return up(c.visibility) !== 'HIDDEN' || isOwner; - case 'shop': return up(c.visibility) !== 'CLOSED' || isOwner; - case 'tribe': { const st = up(c.status); return !(st === 'PRIVATE' || st === 'INVITE-ONLY') || isOwner || arr(c.members).includes(viewer); } - default: return true; - } - }; + const COUNTED = new Set(['post','event','task','forum','market','job','housing','project','industry','shop','image','video','audio','document','bookmark','transfer','map']); + const accessible = (type, c) => isOwner || isContentVisibleTo(type, c, viewer, target); const counts = {}; await new Promise((resolve) => { pull( @@ -436,6 +385,13 @@ module.exports = ({ cooler }) => { }, () => resolve()) ); }); + if (tribesModel) { + try { + const visible = await tribesModel.listTribesForViewer(viewer); + const n = visible.filter(t => String(t.author) === String(target)).length; + if (n > 0) counts.tribe = n; + } catch (_) {} + } return counts; }, @@ -472,16 +428,7 @@ module.exports = ({ cooler }) => { addAll(tokenize(job.requirements)); if (keywords.size === 0) return []; - const records = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true }), - pull.filter(msg => - msg.value?.content?.type === 'curriculum' && - msg.value?.content?.type !== 'tombstone' - ), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const records = (await readTyped(ssbClient, ['curriculum'], { limit: logLimit })).filter(msg => msg.value?.content?.type === 'curriculum').reverse(); let cvs = records.map(r => r.value.content); cvs = Array.from(new Map(cvs.map(u => [u.author, u])).values()); cvs = cvs.filter(c => String(c.visibility || 'PUBLIC').toUpperCase() !== 'HIDDEN'); diff --git a/src/models/jobs_model.js b/src/models/jobs_model.js index 3cf3ff6..d396483 100644 --- a/src/models/jobs_model.js +++ b/src/models/jobs_model.js @@ -2,9 +2,12 @@ const pull = require("../server/node_modules/pull-stream") const moment = require("../server/node_modules/moment") const { getConfig } = require("../configs/config-manager.js") const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeByPreferring } = require('../backend/dedupe') +const { dedupeByPreferring, versionGroups } = require('../backend/dedupe') +const { readTyped, memoIndex } = require('./typed_log') const logLimit = getConfig().ssbLogStream?.limit || 1000 +const JOB_TYPES = ["job", "job_sub", "tombstone"] + const norm = (s) => String(s || "").trim().toLowerCase() const toNum = (v) => { const n = parseFloat(String(v ?? "").replace(",", ".")) @@ -40,20 +43,12 @@ module.exports = ({ cooler, tribeCrypto }) => { const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ) + readTyped(ssbClient, JOB_TYPES, { limit: logLimit, withWindow: true, withPrivate: true }) const buildIndex = (messages, ssbClient) => { - const tomb = new Set() + const tomb = buildValidatedTombstoneSet(messages) const jobNodes = new Map() - const parent = new Map() - const child = new Map() - const naiveReplaces = new Map() - const jobSubLatest = new Map() + const subMsgs = [] for (const m of messages) { const key = m.key @@ -61,65 +56,19 @@ module.exports = ({ cooler, tribeCrypto }) => { const c = v.content if (!c) continue - if (c.type === "tombstone" && c.target) { - tomb.add(c.target) - continue - } - if (c.type === "job") { - jobNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) - if (c.replaces) naiveReplaces.set(key, c.replaces) + jobNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) continue } if (c.type === "job_sub" && c.jobId) { const author = v.author if (!author) continue - const ts = v.timestamp || m.timestamp || 0 - const jobId = c.jobId - const k = `${jobId}::${author}` - const prev = jobSubLatest.get(k) - if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId }) + subMsgs.push({ ts: v.timestamp || m.timestamp || 0, value: !!c.value, author, jobId: c.jobId }) continue } } - for (const [key, replacesId] of naiveReplaces.entries()) { - const node = jobNodes.get(key) - if (!node) continue - const orig = jobNodes.get(replacesId) - if (!orig) continue - if (String(orig.author) !== String(node.author)) { jobNodes.delete(key); continue } - parent.set(key, replacesId) - child.set(replacesId, key) - } - - const rootOf = (id) => { - let cur = id - while (parent.has(cur)) cur = parent.get(cur) - return cur - } - - const roots = new Set() - for (const id of jobNodes.keys()) roots.add(rootOf(id)) - - const tipOf = (id) => { - let cur = id - while (child.has(cur)) cur = child.get(cur) - return cur - } - - const tipByRoot = new Map() - for (const r of roots) tipByRoot.set(r, tipOf(r)) - - const subsByJob = new Map() - for (const { jobId, author, value } of jobSubLatest.values()) { - if (!subsByJob.has(jobId)) subsByJob.set(jobId, new Set()) - const set = subsByJob.get(jobId) - if (value) set.add(author) - else set.delete(author) - } - if (ssbClient) { for (const m of messages) { if (typeof m.value?.content !== 'string') continue @@ -130,16 +79,30 @@ module.exports = ({ cooler, tribeCrypto }) => { if (c.type !== 'job_sub' || !c.jobId) continue const author = dec.value.author if (!author) continue - const ts = dec.value.timestamp || m.timestamp || 0 - const jobId = c.jobId - const k = `${jobId}::${author}` - const prev = jobSubLatest.get(k) - if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId }) + subMsgs.push({ ts: dec.value.timestamp || m.timestamp || 0, value: !!c.value, author, jobId: c.jobId }) } catch {} } } - return { tomb, jobNodes, parent, child, rootOf, tipOf, tipByRoot, subsByJob } + const vg = versionGroups(Array.from(jobNodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))) + + const jobSubLatest = new Map() + for (const sub of subMsgs) { + const jobId = vg.rootOf(sub.jobId) + const k = `${jobId}::${sub.author}` + const prev = jobSubLatest.get(k) + if (!prev || sub.ts >= prev.ts) jobSubLatest.set(k, { ...sub, jobId }) + } + + const subsByJob = new Map() + for (const { jobId, author, value } of jobSubLatest.values()) { + if (!subsByJob.has(jobId)) subsByJob.set(jobId, new Set()) + const set = subsByJob.get(jobId) + if (value) set.add(author) + else set.delete(author) + } + + return { tomb, jobNodes, vg, subsByJob } } const buildJobObject = (node, rootId, subscribers) => { @@ -177,6 +140,7 @@ module.exports = ({ cooler, tribeCrypto }) => { status: c.status || "OPEN", tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags), subscribers: Array.isArray(visibleSubs) ? visibleSubs : [], + industry: c.industry || "", mapUrl: c.mapUrl || "", visibility: String(c.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC", clearnetPublic: !!c.clearnetPublic @@ -235,6 +199,7 @@ module.exports = ({ cooler, tribeCrypto }) => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), status: "OPEN", + industry: String(jobData.industry || "").trim(), mapUrl: String(jobData.mapUrl || "").trim(), visibility: String(jobData.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC", clearnetPublic: jobData.clearnetPublic === true || jobData.clearnetPublic === 'true' || jobData.clearnetPublic === 'on' @@ -253,26 +218,20 @@ module.exports = ({ cooler, tribeCrypto }) => { async resolveCurrentId(jobId) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const { tomb, child } = buildIndex(messages, ssbClient) + const { tomb, vg } = buildIndex(messages, ssbClient) - let cur = jobId - while (child.has(cur)) cur = child.get(cur) - if (tomb.has(cur)) throw new Error("Job not found") + const cur = vg.tipOf(jobId) + if (tomb.has(cur) || vg.isForeignCopy(vg.rootOf(cur))) throw new Error("Job not found") return cur }, async resolveRootId(jobId) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const { tomb, parent, child } = buildIndex(messages, ssbClient) + const { tomb, vg } = buildIndex(messages, ssbClient) - let tip = jobId - while (child.has(tip)) tip = child.get(tip) - if (tomb.has(tip)) throw new Error("Job not found") - - let root = tip - while (parent.has(root)) root = parent.get(root) - return root + if (tomb.has(vg.tipOf(jobId)) || vg.isForeignCopy(vg.rootOf(jobId))) throw new Error("Job not found") + return vg.rootOf(jobId) }, async updateJob(id, jobData) { @@ -466,7 +425,8 @@ module.exports = ({ cooler, tribeCrypto }) => { const idx = buildIndex(messages, ssbClient) const jobs = [] - for (const [rootId, tipId] of idx.tipByRoot.entries()) { + for (const rootId of idx.vg.roots) { + const tipId = idx.vg.tipOf(rootId) if (idx.tomb.has(tipId)) continue const node = idx.jobNodes.get(tipId) if (!node) continue @@ -521,14 +481,11 @@ module.exports = ({ cooler, tribeCrypto }) => { const viewer = viewerId || ssbClient.id const messages = await readAll(ssbClient) - const idx = buildIndex(messages, ssbClient) + const idx = memoIndex('jobs', messages, (m) => buildIndex(m, ssbClient)) - let tipId = id - while (idx.child.has(tipId)) tipId = idx.child.get(tipId) - if (idx.tomb.has(tipId)) throw new Error("Job not found") - - let rootId = tipId - while (idx.parent.has(rootId)) rootId = idx.parent.get(rootId) + const tipId = idx.vg.tipOf(id) + const rootId = idx.vg.rootOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(rootId)) throw new Error("Job not found") const gate = (job) => { if (job.visibility === "HIDDEN" && job.author !== viewer) throw new Error("Job not found") @@ -538,7 +495,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const node = idx.jobNodes.get(tipId) if (!node) { const msg = await new Promise((r, j) => ssbClient.get(tipId, (e, m) => e ? j(e) : r(m))) - if (!msg || !msg.content) throw new Error("Job not found") + if (!msg || !msg.content || msg.content.type !== "job" || (msg.content.author && msg.content.author !== msg.author)) throw new Error("Job not found") const tmpNode = { key: tipId, ts: msg.timestamp || 0, c: msg.content, author: msg.author } const subsSet = idx.subsByJob.get(rootId) || new Set() const subs = Array.from(subsSet) diff --git a/src/models/karma_model.js b/src/models/karma_model.js new file mode 100644 index 0000000..c500986 --- /dev/null +++ b/src/models/karma_model.js @@ -0,0 +1,591 @@ +const pull = require("../server/node_modules/pull-stream"); +const { readTyped, CONTENT_TYPES } = require("./typed_log"); +const { getConfig } = require("../configs/config-manager.js"); +const { config } = require("../server/SSB_server.js"); + +function getLogLimit() { + return getConfig().ssbLogStream?.limit || 1000; +} + +const OWN_SCORE_CAP = 200; +const RECEIVED_AUTHOR_MIN_AGE_MS = 30 * 24 * 60 * 60 * 1000; +const opinionCategories = require("../backend/opinion_categories"); +const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive); +const MODERATION_OPINIONS = new Set(opinionCategories.karma.moderation); +const NEUTRAL_OPINIONS = new Set(opinionCategories.karma.neutral); +const NEGATIVE_MIN_VOTERS = 3; +const KARMA_PERIOD_MS = 30 * 24 * 60 * 60 * 1000; +const NEGATIVE_PATTERN_MIN_VOTERS = 5; +const NEGATIVE_PATTERN_POINTS = 2; +const POSITIVE_PER_VOTER_CAP = 10; +const TRUST_MIN_FOLLOWERS = 2; +const REACTIONS_PER_MESSAGE_CAP = 10; +const PLAYS_PER_EPISODE_CAP = 20; +const ACTIONS_CACHE_TTL_MS = 60 * 1000; + +function resolveUserId(maybeId) { + const s = String(maybeId || "").trim(); + if (s) return s; + return config?.keys?.id || ""; +} + +function idsEqual(a, b) { + if (!a || !b) return false; + const A = String(a).trim(); + const B = String(b).trim(); + if (A === B) return true; + const strip = s => s.replace(/^@/, "").replace(/\.ed25519$/, ""); + return strip(A) === strip(B); +} + + function inferType(c = {}) { + if (c.vote) return "vote"; + if (c.votes) return "votes"; + if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet"; + if (c.type === "ubiClaimResult" && c.txid && c.epochId) return "ubiClaimResult"; + if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim"; + if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market"; + if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project"; + if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe"; + if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event"; + if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task"; + if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report"; + if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job"; + if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio"; + if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video"; + if (typeof c.url !== "undefined" && c.title && c.key) return "document"; + if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed"; + if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post"; + if (typeof c.contact !== "undefined") return "contact"; + if (typeof c.about !== "undefined") return "about"; + if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer"; + return ""; + } + + function normalizeType(a) { + const t = a.type || a.content?.type || inferType(a.content) || ""; + return String(t).toLowerCase(); + } + + function priorityBump(p) { + const s = String(p || "").toUpperCase(); + if (s === "HIGH") return 3; + if (s === "MEDIUM") return 1; + return 0; + } + + function severityBump(s) { + const x = String(s || "").toUpperCase(); + if (x === "CRITICAL") return 6; + if (x === "HIGH") return 4; + if (x === "MEDIUM") return 2; + return 0; + } + + function scoreMarket(c) { + const st = String(c.status || "").toUpperCase(); + let s = 5; + if (st === "SOLD") s += 8; + else if (st === "ACTIVE") s += 3; + const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0; + s += Math.min(10, bids); + return s; + } + + function scoreProject(c) { + const st = String(c.status || "ACTIVE").toUpperCase(); + const prog = Number(c.progress || 0); + let s = 8 + Math.min(10, prog / 10); + if (st === "FUNDED") s += 10; + return s; + } + + function calculateOpinionScore(content) { + const cats = content?.opinions || {}; + let s = 0; + for (const k in cats) { + if (!Object.prototype.hasOwnProperty.call(cats, k)) continue; + if (NEUTRAL_OPINIONS.has(k)) continue; + if (k === "interesting" || k === "inspiring") s += 5; + else if (MODERATION_OPINIONS.has(k)) s -= 5; + else if (CONSTRUCTIVE_OPINIONS.has(k)) s -= 3; + else s += 1; + } + return s; + } + +function basePointsFor(action) { + const t = normalizeType(action); + const c = action.content || {}; + const rawType = String(c.type || "").toLowerCase(); + if (t === "post") return 10; + else if (t === "comment") return 5; + else if (t === "like") return 2; + else if (t === "image") return 8; + else if (t === "video") return 12; + else if (t === "audio") return 8; + else if (t === "document") return 6; + else if (t === "bookmark") return 2; + else if (t === "feed") return 6; + else if (t === "forum") return (c.root ? 5 : 10); + else if (t === "vote") return (3 + calculateOpinionScore(c)); + else if (t === "votes") return Math.min(10, Number(c.totalVotes || 0)); + else if (t === "market") return scoreMarket(c); + else if (t === "project") return scoreProject(c); + else if (t === "tribe") return (6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0)); + else if (t === "event") return (4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0)); + else if (t === "task") return (3 + priorityBump(c.priority)); + else if (t === "report") return (4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity)); + else if (t === "curriculum") return 5; + else if (t === "wikipage") return (c.replaces ? 2 : 6); + else if (t === "emergency") return (c.replaces ? 1 : 5); + else if (t === "emergencyconfirm") return 1; + else if (t === "emergencyupdate") return 2; + else if (t === "mailinglist") return (c.replaces ? 1 : 3); + else if (t === "logisticsroute") return (c.replaces ? 1 : 4); + else if (t === "logisticsrating") return 1; + else if (t === "podcast") return (c.replaces ? 1 : 5); + else if (t === "podcastepisode") return (c.replaces ? 1 : 4); + else if (t === "podcastopinion" || t === "podcastplay" || t === "campaignopinion" || t === "logisticsopinion") return 1; + else if (t === "campaign") return (c.replaces ? 1 : 5); + else if (t === "campaignsignature") return 1; + else if (t === "campaignupdate") return 2; + else if (t === "aiexchange") return (Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0); + else if (t === "job") return (4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0)); + else if (t === "transfer") return 1; + else if (t === "about") return (1 + ((c.visibilityPrefs && c.visibilityPrefs.fediverseHandle) ? 8 : 0)); + else if (t === "contact") return 1; + else if (t === "pub") return 1; + else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") return 12; + else if (t === "parliamentterm" || rawType === "parliamentterm") return 25; + else if (t === "parliamentproposal" || rawType === "parliamentproposal") return 8; + else if (t === "parliamentlaw" || rawType === "parliamentlaw") return 16; + else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") return 10; + else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") return 4; + else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") return 3; + else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") return 4; + else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") return 10; + else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") return 8; + else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") return 6; + else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") return 3; + else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") return 1; + else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") return 6; + else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") return 2; + else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") return 3; + else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") return 5; + else if (t === "larpjoinhouse") return 15; + else if (t === "larphousepost") return 6; + else if (t === "larptestattempt") return 3; + else if (t === "torrent") return 6; + else if (t === "file") return 6; + else if (t === "shop" || t === "shopproduct") return 6; + else if (t === "shop-purchase") return 2; + else if (t === "pad" || t === "padentry") return 3; + else if (t === "room") return 3; + else if (t === "roommember") return 1; + else if (t === "calendar" || t === "calendarnote" || t === "calendardate") return 3; + else if (t === "chat") return 1; + else if (t === "gamescore") return 2; + else if (t === "pixelia") return 2; + else if (rawType === "industry") return 8; + else if (rawType === "industryblueprint") return 6; + else if (rawType === "industrybuild") return 8; + else if (rawType === "industrycontribution") return 4; + else if (rawType === "industryallocation") return 10; + else if (rawType === "industryvote") return 2; + else if (rawType === "industrymember") return 2; + else if (rawType === "industryopinion") return 1; + else if (rawType === "schoolcourse") return 10; + else if (rawType === "schoollesson") return 6; + else if (rawType === "schoolenroll") return 3; + else if (rawType === "schoolcertificate") return 12; + else if (rawType === "schoolexam") return 6; + else if (rawType === "schoolprogress") return 1; + else if (rawType === "schoolopinion") return 2; + return 0; +} + +function decayFor(ts, nowMs) { + const ageDays = ts ? (nowMs - ts) / 86400000 : Infinity; + return ageDays <= 30 ? 1.0 : ageDays <= 90 ? 0.5 : 0.25; +} + +function scoreFromActions(actions) { + let score = 0; + const nowMs = Date.now(); + const perTypeDay = new Map(); + for (const action of actions) { + const pts = basePointsFor(action); + if (!pts) continue; + const ts = action.value?.timestamp; + const dayKey = `${normalizeType(action)}:${ts ? new Date(ts).toISOString().slice(0, 10) : "?"}`; + const n = (perTypeDay.get(dayKey) || 0) + 1; + perTypeDay.set(dayKey, n); + score += pts * decayFor(ts, nowMs) / n; + } + return Math.max(0, Math.round(Math.min(OWN_SCORE_CAP, score))); +} + +function receivedBreakdownFor(userId, actions) { + const nowMs = Date.now(); + const mine = new Set(); + const firstTs = new Map(); + const follows = new Map(); + for (const a of actions) { + if (!a.author) continue; + const ts = Number(a.receivedAt) || 0; + if (ts && (!firstTs.has(a.author) || ts < firstTs.get(a.author))) firstTs.set(a.author, ts); + if (a.author === userId && a.id) mine.add(a.id); + const c = a.content || {}; + if (c.type === "contact" && typeof c.contact === "string" && c.contact !== a.author) { + const k = `${a.author}|${c.contact}`; + const at = Number(a.value?.timestamp) || ts; + const prev = follows.get(k); + if (!prev || at >= prev.at) follows.set(k, { on: c.following === true && c.blocking !== true, at }); + } + } + const followersOf = new Map(); + for (const [k, f] of follows) { + if (!f.on) continue; + const [from, to] = k.split("|"); + if (!followersOf.has(to)) followersOf.set(to, new Set()); + followersOf.get(to).add(from); + } + const known = (id) => firstTs.has(id) && nowMs - firstTs.get(id) >= RECEIVED_AUTHOR_MIN_AGE_MS; + const trusted = (voter, sameSide) => { + let n = 0; + for (const f of followersOf.get(voter) || []) { + if (f === userId || sameSide.has(f) || !known(f)) continue; + if (++n >= TRUST_MIN_FOLLOWERS) return true; + } + return false; + }; + const seen = new Set(); + const perTarget = new Map(); + const negatives = new Map(); + const votes = new Map(); + const reactions = new Map(); + const credits = []; + let positive = 0; + let negative = 0; + const add = (author, kind, target, pts, ts, capPerTarget = Infinity) => { + const k = `${kind}:${author}:${target}`; + if (seen.has(k)) return; + const n = perTarget.get(`${kind}:${target}`) || 0; + if (n >= capPerTarget) return; + seen.add(k); + perTarget.set(`${kind}:${target}`, n + 1); + credits.push({ voter: author, pts, ts: Number(ts) || 0 }); + }; + const addNegative = (author, target, pts, ts) => { + const k = `neg:${author}:${target}`; + if (seen.has(k)) return; + seen.add(k); + if (!negatives.has(target)) negatives.set(target, []); + negatives.get(target).push({ voter: author, pts, ts: Number(ts) || 0 }); + }; + for (const a of actions) { + const b = a.author; + if (!b || b === userId) continue; + if (!firstTs.has(b) || nowMs - firstTs.get(b) < RECEIVED_AUTHOR_MIN_AGE_MS) continue; + const c = a.content || {}; + const type = String(c.type || ""); + const ts = a.value?.timestamp; + if (/Opinion$/.test(type) && type !== "pollOpinion" && mine.has(c.target)) { + const cat = String(c.category || ""); + if (NEUTRAL_OPINIONS.has(cat)) continue; + if (MODERATION_OPINIONS.has(cat)) addNegative(b, c.target, -5, ts); + else if (CONSTRUCTIVE_OPINIONS.has(cat)) addNegative(b, c.target, -3, ts); + else add(b, "opinion", c.target, 3, ts); + } + else if (type === "vote" && c.vote && mine.has(c.vote.link)) { + const k = `${b}:${c.vote.link}`; + const prev = votes.get(k); + if (!prev || (Number(ts) || 0) >= prev.ts) votes.set(k, { author: b, target: c.vote.link, value: Number(c.vote.value) || 0, ts: Number(ts) || 0 }); + } + else if (type === "feed-action" && c.action === "vote" && mine.has(c.root || c.target)) add(b, "opinion", c.root || c.target, 3, ts); + else if (type === "feed-action" && c.action === "comment" && mine.has(c.root)) add(b, "comment", c.root, 2, ts, 10); + else if (type === "feed-action" && c.action === "refeed" && mine.has(c.root)) add(b, "refeed", c.root, 2, ts, 10); + else if (type === "post" && c.root && mine.has(c.root)) add(b, "comment", c.root, 2, ts, 10); + else if (type === "contact" && c.following === true && c.contact === userId) add(b, "follow", userId, 2, ts); + else if (type === "chatReaction" && mine.has(c.target)) { + const k = `${b}:${c.target}:${c.emoji}`; + const prev = reactions.get(k); + if (!prev || (Number(ts) || 0) >= prev.ts) reactions.set(k, { author: b, target: c.target, emoji: c.emoji, on: c.on !== false, ts: Number(ts) || 0 }); + } + else if (type === "podcastPlay" && mine.has(c.target)) add(b, "play", c.target, 1, ts, PLAYS_PER_EPISODE_CAP); + else if (type === "marketPurchase" && mine.has(c.target)) add(b, "purchase", c.target, 5, ts); + else if (type === "shop-purchase" && c.seller === userId) add(b, "purchase", c.productId || a.id, 5, ts); + else if (type === "schoolEnroll" && c.value === true && mine.has(c.courseId)) add(b, "enroll", c.courseId, 5, ts); + else if (type === "transferConfirm" && mine.has(c.target)) add(b, "confirm", c.target, 3, ts); + } + for (const r of reactions.values()) { + if (!r.on) continue; + if (r.emoji === "down") addNegative(r.author, r.target, -3, r.ts); + else add(r.author, "reaction", r.target, 1, r.ts, REACTIONS_PER_MESSAGE_CAP); + } + for (const v of votes.values()) { + if (v.value > 0) add(v.author, "opinion", v.target, 3, v.ts); + else if (v.value < 0) addNegative(v.author, v.target, -3, v.ts); + } + const vouched = new Map(); + const isTrusted = (voter, side, cache) => { + if (!cache.has(voter)) cache.set(voter, trusted(voter, side)); + return cache.get(voter); + }; + const usedByVoter = new Map(); + for (const c of credits.sort((x, y) => x.ts - y.ts)) { + if (!isTrusted(c.voter, new Set(), vouched)) continue; + const k = `${c.voter}:${Math.floor(c.ts / KARMA_PERIOD_MS)}`; + const used = usedByVoter.get(k) || 0; + const take = Math.min(c.pts, POSITIVE_PER_VOTER_CAP - used); + if (take <= 0) continue; + usedByVoter.set(k, used + take); + positive += take * decayFor(c.ts, nowMs); + } + const blaming = new Set(); + for (const list of negatives.values()) for (const n of list) blaming.add(n.voter); + const vouchedAgainst = new Map(); + const pattern = new Map(); + for (const all of negatives.values()) { + const list = all.filter(n => isTrusted(n.voter, blaming, vouchedAgainst)); + if (list.length >= NEGATIVE_MIN_VOTERS) { + for (const n of list) negative += Math.abs(n.pts) * decayFor(n.ts, nowMs); + continue; + } + for (const n of list) { + if (!n.ts || nowMs - n.ts > KARMA_PERIOD_MS) continue; + if ((pattern.get(n.voter) || 0) < n.ts) pattern.set(n.voter, n.ts); + } + } + if (pattern.size >= NEGATIVE_PATTERN_MIN_VOTERS) { + for (const ts of pattern.values()) negative += NEGATIVE_PATTERN_POINTS * decayFor(ts, nowMs); + } + positive = Math.round(positive); + negative = Math.round(negative); + return { score: positive - negative, positive, negative }; +} + +function receivedScoreFor(userId, actions) { + return receivedBreakdownFor(userId, actions).score; +} + +module.exports = ({ services, ranges } = {}) => { + const normalizeRange = ranges && ranges.normalizeRange ? ranges.normalizeRange : (r) => String(r || "all"); + const widenRange = ranges && ranges.widenRange ? ranges.widenRange : (arr, r) => normalizeRange(r); + const filterByRange = ranges && ranges.filterByRange ? ranges.filterByRange : (arr) => arr; + + let ssbInstance; + async function openSsb() { + if (ssbInstance) return ssbInstance; + if (services?.cooler?.open) ssbInstance = await services.cooler.open(); + else if (global.ssb) ssbInstance = global.ssb; + else { + try { + const srv = require("../server/SSB_server.js"); + ssbInstance = srv?.ssb || srv?.server || srv?.default || null; + } catch (_) { + ssbInstance = null; + } + } + return ssbInstance; + } + + let actionsCache = null; + async function listAllActions() { + const ssb = await openSsb(); + if (!ssb) { + return []; + } + const ownSeq = await ownFeedSeq(ssb); + if (actionsCache && actionsCache.ownSeq === ownSeq && Date.now() - actionsCache.ts < ACTIONS_CACHE_TTL_MS) return actionsCache.value; + const msgs = await readTyped(ssb, CONTENT_TYPES, { limit: Math.max(getLogLimit(), 20000) }); + const value = msgs.map(m => { + const v = m.value || {}; + const c = v.content || {}; + return { + id: v.key || m.key, + author: v.author, + type: (c.type || "").toLowerCase(), + value: v, + content: c, + receivedAt: Number(m.timestamp) || 0 + }; + }); + actionsCache = { ts: Date.now(), ownSeq, value }; + return value; + } + +async function ownFeedSeq(ssb) { + return new Promise((res) => { + try { + pull( + ssb.createUserStream({ id: ssb.id, reverse: true, limit: 1 }), + pull.collect((err, msgs) => res(err || !msgs || !msgs.length ? 0 : (msgs[0].value && msgs[0].value.sequence) || 0)) + ); + } catch (_) { res(0); } + }); +} + +async function publishKarmaScore(userId, karmaScore) { + const ssb = await openSsb(); + if (!ssb || !ssb.publish) return false; + if (!(await ownFeedSeq(ssb))) return false; + const timestamp = new Date().toISOString(); + const content = { type: "karmaScore", karmaScore, userId, timestamp }; + return new Promise((resolve, reject) => { + ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg)); + }); +} + +async function fetchUserActions(userId) { + const me = resolveUserId(userId); + const actions = await listAllActions(); + const authored = actions.filter(a => + (a.author && a.author === me) || (a.value?.author && a.value.author === me) + ); + if (authored.length) return authored; + return actions.filter(a => { + const c = a.content || {}; + const fields = [c.author, c.organizer, c.seller, c.about, c.contact]; + return fields.some(f => f && f === me); + }); +} + +async function getCarbonGramsForUser(userId) { + const ssb = await openSsb(); + if (!ssb || !userId) return 0; + return new Promise((resolve) => { + let bytes = 0; + pull( + ssb.createUserStream({ id: userId }), + pull.drain( + (m) => { try { bytes += Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) {} }, + () => resolve((bytes / (1024 * 1024)) * 0.095) + ) + ); + }); +} + +async function getUserEngagementScore(userId) { + const ssb = await openSsb(); + const uid = resolveUserId(userId); + const actions = await fetchUserActions(uid); + const all = await listAllActions(); + const rawKarma = scoreFromActions(actions) + receivedScoreFor(uid, all); + const carbonGrams = await getCarbonGramsForUser(uid).catch(() => 0); + const karmaScore = Math.max(0, Math.round(rawKarma - carbonGrams)); + + const prev = await getLastKarmaScore(uid); + const lastPublishedTimestamp = await getLastPublishedTimestamp(uid); + + const isSelf = idsEqual(uid, ssb.id); + const hasSSB = !!(ssb && ssb.publish); + + const changed = (prev === null) || (karmaScore !== prev); + const nowMs = Date.now(); + const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0; + const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000; + + if (isSelf && hasSSB && changed && cooldownOk) { + await publishKarmaScore(uid, karmaScore); + } + return karmaScore; +} + +async function getLastKarmaScore(userId) { + const ssb = await openSsb(); + if (!ssb) return null; + return new Promise((resolve) => { + const source = ssb.messagesByType + ? ssb.messagesByType({ type: "karmaScore", reverse: true }) + : ssb.createLogStream && ssb.createLogStream({ reverse: true }); + if (!source) return resolve(null); + pull( + source, + pull.filter(msg => { + const v = msg.value || msg; + const c = v.content || {}; + return c && c.type === "karmaScore" && c.userId === userId; + }), + pull.take(1), + pull.collect((err, arr) => { + if (err || !arr || !arr.length) return resolve(null); + const v = arr[0].value || arr[0]; + const c = v.content || {}; + resolve(Number(c.karmaScore) || 0); + }) + ); + }); +} + +async function getLastPublishedTimestamp(userId) { + const ssb = await openSsb(); + if (!ssb) return new Date(0).toISOString(); + const fallback = new Date(0).toISOString(); + return new Promise((resolve) => { + const source = ssb.messagesByType + ? ssb.messagesByType({ type: "karmaScore", reverse: true }) + : ssb.createLogStream && ssb.createLogStream({ reverse: true }); + if (!source) return resolve(fallback); + pull( + source, + pull.filter(msg => { + const v = msg.value || msg; + const c = v.content || {}; + return c && c.type === "karmaScore" && c.userId === userId; + }), + pull.take(1), + pull.collect((err, arr) => { + if (err || !arr || !arr.length) return resolve(fallback); + const v = arr[0].value || arr[0]; + const c = v.content || {}; + resolve(c.timestamp || fallback); + }) + ); + }); +} + + async function getKarmaBreakdown(userId) { + const uid = resolveUserId(userId); + const received = receivedBreakdownFor(uid, await listAllActions()); + return { karmaScore: await getUserEngagementScore(uid), positive: received.positive, negative: received.negative }; + } + + async function listKarmaHistory(userId, range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { points: [], hasAny: false, range: normalizeRange(range) }; + const uid = resolveUserId(userId); + const msgs = await readTyped(ssb, ["karmaScore"], { limit: Math.max(getLogLimit(), 5000) }); + const all = msgs + .filter(m => m.value && m.value.author === uid && m.value.content && m.value.content.type === "karmaScore") + .map(m => ({ ts: Date.parse(m.value.content.timestamp || "") || Number(m.value.timestamp) || 0, score: Number(m.value.content.karmaScore) || 0 })) + .filter(x => x.ts > 0) + .sort((a, b) => a.ts - b.ts); + const effective = explicit ? normalizeRange(range) : widenRange(all, range); + return { points: filterByRange(all, effective).slice(-120), hasAny: all.length >= 2, range: effective }; + } + + return { + listAllActions, + fetchUserActions, + scoreFromActions, + receivedBreakdownFor, + receivedScoreFor, + getCarbonGramsForUser, + getUserEngagementScore, + getKarmaBreakdown, + getLastKarmaScore, + getLastPublishedTimestamp, + publishKarmaScore, + listKarmaHistory, + basePointsFor, + decayFor, + NEGATIVE_MIN_VOTERS, + NEGATIVE_PATTERN_MIN_VOTERS, + NEGATIVE_PATTERN_POINTS, + POSITIVE_PER_VOTER_CAP, + KARMA_PERIOD_MS, + TRUST_MIN_FOLLOWERS + }; +}; diff --git a/src/models/keylock.js b/src/models/keylock.js deleted file mode 100644 index a50d7e8..0000000 --- a/src/models/keylock.js +++ /dev/null @@ -1,129 +0,0 @@ -const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); -const ssbKeys = require('../server/node_modules/ssb-keys'); - -const MAGIC = Buffer.from('OAES1\n', 'utf8'); -const KEYLEN = 32; -const DEFAULT_SCRYPT = { N: 16384, r: 8, p: 1 }; -const VERIFIER_PLAINTEXT = Buffer.from('oasis-keylock-v1', 'utf8'); - -let KEK = null; // SOLO en memoria - -const maxmemFor = (s) => Math.max(64 * 1024 * 1024, 256 * s.N * s.r); - -const deriveKEK = (password, salt, scrypt = DEFAULT_SCRYPT) => - crypto.scryptSync(Buffer.from(String(password), 'utf8'), salt, KEYLEN, - { N: scrypt.N, r: scrypt.r, p: scrypt.p, maxmem: maxmemFor(scrypt) }); - -const isWrapped = (buf) => - Buffer.isBuffer(buf) && buf.length >= MAGIC.length && buf.subarray(0, MAGIC.length).equals(MAGIC); - -const wrap = (buf, kek = KEK) => { - if (!kek) throw new Error('keylock: locked (no KEK)'); - const iv = crypto.randomBytes(12); - const c = crypto.createCipheriv('aes-256-gcm', kek, iv); - const ct = Buffer.concat([c.update(buf), c.final()]); - const tag = c.getAuthTag(); - return Buffer.concat([MAGIC, iv, tag, ct]); -}; - -const unwrap = (blob, kek = KEK) => { - if (!kek) throw new Error('keylock: locked (no KEK)'); - if (!isWrapped(blob)) throw new Error('keylock: not a wrapped blob'); - let o = MAGIC.length; - const iv = blob.subarray(o, o += 12); - const tag = blob.subarray(o, o += 16); - const ct = blob.subarray(o); - const d = crypto.createDecipheriv('aes-256-gcm', kek, iv); - d.setAuthTag(tag); - return Buffer.concat([d.update(ct), d.final()]); -}; - -const lockPath = (configPath) => path.join(configPath, 'keylock.json'); - -const isConfigured = (configPath) => { - try { return fs.existsSync(lockPath(configPath)); } catch (_) { return false; } -}; - -const setPassword = (configPath, password) => { - const salt = crypto.randomBytes(16); - const scrypt = DEFAULT_SCRYPT; - const kek = deriveKEK(password, salt, scrypt); - const verifier = wrap(VERIFIER_PLAINTEXT, kek); - const data = { - v: 1, kdf: 'scrypt', N: scrypt.N, r: scrypt.r, p: scrypt.p, - salt: salt.toString('hex'), verifier: verifier.toString('base64'), - }; - fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); - const tmp = lockPath(configPath) + '.tmp'; - fs.writeFileSync(tmp, JSON.stringify(data), { mode: 0o600 }); - fs.renameSync(tmp, lockPath(configPath)); - KEK = kek; - return kek; -}; - -const verifyPassword = (configPath, password) => { - let data; - try { data = JSON.parse(fs.readFileSync(lockPath(configPath), 'utf8')); } catch (_) { return null; } - const salt = Buffer.from(data.salt, 'hex'); - const scrypt = { N: data.N || DEFAULT_SCRYPT.N, r: data.r || DEFAULT_SCRYPT.r, p: data.p || DEFAULT_SCRYPT.p }; - const kek = deriveKEK(password, salt, scrypt); - try { - if (unwrap(Buffer.from(data.verifier, 'base64'), kek).equals(VERIFIER_PLAINTEXT)) { - KEK = kek; - return kek; - } - } catch (_) {} - return null; -}; - -const getKEK = () => KEK; -const setKEK = (k) => { KEK = k; }; -const isUnlocked = () => KEK != null; -const lock = () => { try { if (KEK) KEK.fill(0); } catch (_) {} KEK = null; }; - -const secretEncPath = (configPath) => path.join(configPath, 'secret.enc'); -const legacySecretPath = (configPath) => path.join(configPath, 'secret'); - -const shred = (p) => { - try { fs.writeFileSync(p, crypto.randomBytes(Math.max(1, fs.statSync(p).size))); } catch (_) {} - try { fs.unlinkSync(p); } catch (_) {} -}; - -const writeSecret = (configPath, keys) => { - const blob = wrap(Buffer.from(JSON.stringify(keys), 'utf8')); - fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); - const tmp = secretEncPath(configPath) + '.tmp'; - fs.writeFileSync(tmp, blob, { mode: 0o600 }); - fs.renameSync(tmp, secretEncPath(configPath)); -}; - -const loadOrCreateSecret = (configPath) => { - if (!KEK) throw new Error('keylock: locked'); - fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); - const encP = secretEncPath(configPath); - if (fs.existsSync(encP)) { - return JSON.parse(unwrap(fs.readFileSync(encP)).toString('utf8')); - } - const legP = legacySecretPath(configPath); - if (fs.existsSync(legP)) { - const keys = ssbKeys.loadSync(legP); - writeSecret(configPath, keys); - shred(legP); - return keys; - } - const keys = ssbKeys.generate('ed25519'); // primer arranque - writeSecret(configPath, keys); - return keys; -}; - -const hasIdentity = (configPath) => - fs.existsSync(secretEncPath(configPath)) || fs.existsSync(legacySecretPath(configPath)); - -module.exports = { - MAGIC, deriveKEK, wrap, unwrap, isWrapped, - isConfigured, setPassword, verifyPassword, - getKEK, setKEK, isUnlocked, lock, - loadOrCreateSecret, writeSecret, hasIdentity, -}; diff --git a/src/models/larp_model.js b/src/models/larp_model.js index e495d69..3c72b0e 100644 --- a/src/models/larp_model.js +++ b/src/models/larp_model.js @@ -1,4 +1,5 @@ const pull = require('../server/node_modules/pull-stream'); +const { readTyped, requestScope } = require('./typed_log'); const fs = require('fs'); const path = require('path'); @@ -10,6 +11,7 @@ const HOUSE_KEYS = ['academia','solaris','arrakis','terraverde','unsystem','dogm const VALID_KEY = (k) => HOUSE_KEYS.includes(String(k || '').toLowerCase()); const TEST_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000; +const ACADEMIA_GRACE_MS = 10 * 60 * 1000; const PROFILE_QUESTIONS = [ { @@ -165,9 +167,20 @@ function getGoverningHouseKey(now = new Date()) { return HOUSE_KEYS[now.getMonth() % HOUSE_KEYS.length]; } +function getGoverningPeriodId(now = new Date()) { + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${getGoverningHouseKey(now)}:${now.getFullYear()}-${month}`; +} + module.exports = ({ cooler, tribesModel, tribeCrypto }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const startedAt = Date.now(); + let academiaRetry = null; + const followsSomeone = async (client, me) => { + const contacts = await readTyped(client, ['contact']).catch(() => []); + return contacts.some((m) => m && m.value && m.value.author === me && m.value.content && m.value.content.following === true); + }; const houseTribeTag = (houseKey) => { const h = HOUSES[houseKey]; @@ -187,7 +200,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return tags.includes(tag) && members.includes(me); }); if (!candidates.length) return null; - candidates.sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()); + candidates.sort((a, b) => (new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()) || String(a.id || '').localeCompare(String(b.id || ''))); return candidates[0]; } @@ -201,78 +214,59 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return tags.includes(tag); }); if (!candidates.length) return null; - candidates.sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()); + candidates.sort((a, b) => (new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()) || String(a.id || '').localeCompare(String(b.id || ''))); return candidates[0]; } + const ANCHOR_TYPES = ['larpHouseTribeAnchor', 'larpHouseTribeAnchorTombstone', 'tombstone']; + const authorOfMsg = (client, key) => new Promise((resolve) => client.get(key, (err, v) => resolve(err || !v ? null : v.author || null))); + async function findEarliestHouseAnchor(houseKey) { if (!VALID_KEY(houseKey)) return null; const client = await openSsb(); - return new Promise((resolve) => { - const anchors = []; - const anchorTombstones = []; - const tribeTombstones = new Map(); - const msgAuthorByKey = new Map(); - pull( - client.createLogStream(), - pull.drain((m) => { - if (!m || !m.value) return; - const author = m.value.author; - if (m.key && author) msgAuthorByKey.set(m.key, author); - const c = m.value.content; - if (!c || typeof c !== 'object') return; - if (c.type === 'larpHouseTribeAnchor') { - if (c.house !== houseKey) return; - if (typeof c.tribeRootId !== 'string') return; - const tribeTs = Number(Date.parse(c.tribeCreatedAt || '')) || m.value.timestamp || 0; - anchors.push({ tribeRootId: c.tribeRootId, anchorAuthor: author, tribeTs }); - } else if (c.type === 'larpHouseTribeAnchorTombstone') { - if (c.house !== houseKey) return; - if (typeof c.tribeRootId !== 'string') return; - anchorTombstones.push({ tribeRootId: c.tribeRootId, tombstoneAuthor: author }); - } else if (c.type === 'tombstone' && typeof c.target === 'string') { - tribeTombstones.set(c.target, author); - } - }, () => { - const killedAnchorPairs = new Set(); - for (const t of anchorTombstones) { - killedAnchorPairs.add(`${t.tribeRootId}|${t.tombstoneAuthor}`); - } - const deadTribes = new Set(); - for (const [target, tombAuthor] of tribeTombstones) { - const tribeAuthor = msgAuthorByKey.get(target); - if (tribeAuthor && tribeAuthor === tombAuthor) deadTribes.add(target); - } - const live = anchors.filter(a => - !killedAnchorPairs.has(`${a.tribeRootId}|${a.anchorAuthor}`) && - !deadTribes.has(a.tribeRootId) - ); - if (!live.length) return resolve(null); - live.sort((a, b) => a.tribeTs - b.tribeTs); - const first = live[0]; - resolve({ tribeRootId: first.tribeRootId, author: first.anchorAuthor, tribeTs: first.tribeTs }); - }) - ); - }); + const msgs = await readTyped(client, ANCHOR_TYPES); + const anchors = []; + const killedAnchorPairs = new Set(); + const tombAuthors = new Map(); + for (const m of msgs) { + const c = m && m.value && m.value.content; + if (!c || typeof c !== 'object') continue; + const author = m.value.author; + if (c.type === 'larpHouseTribeAnchor') { + if (c.house !== houseKey || typeof c.tribeRootId !== 'string') continue; + const tribeTs = Number(Date.parse(c.tribeCreatedAt || '')) || m.value.timestamp || 0; + anchors.push({ tribeRootId: c.tribeRootId, anchorAuthor: author, tribeTs }); + } else if (c.type === 'larpHouseTribeAnchorTombstone') { + if (c.house !== houseKey || typeof c.tribeRootId !== 'string') continue; + killedAnchorPairs.add(`${c.tribeRootId}|${author}`); + } else if (c.type === 'tombstone' && typeof c.target === 'string') { + if (!tombAuthors.has(c.target)) tombAuthors.set(c.target, new Set()); + tombAuthors.get(c.target).add(author); + } + } + const candidates = anchors.filter(a => !killedAnchorPairs.has(`${a.tribeRootId}|${a.anchorAuthor}`)); + const deadTribes = new Set(); + await Promise.all([...new Set(candidates.map(a => a.tribeRootId).filter(t => tombAuthors.has(t)))].map(async (t) => { + const tribeAuthor = await authorOfMsg(client, t); + if (tribeAuthor && tombAuthors.get(t).has(tribeAuthor)) deadTribes.add(t); + })); + const live = candidates.filter(a => !deadTribes.has(a.tribeRootId)); + if (!live.length) return null; + live.sort((a, b) => a.tribeTs - b.tribeTs); + const first = live[0]; + return { tribeRootId: first.tribeRootId, author: first.anchorAuthor, tribeTs: first.tribeTs }; } async function findHouseAnchorByTribe(houseKey, tribeRootId) { if (!VALID_KEY(houseKey) || !tribeRootId) return null; const client = await openSsb(); - return new Promise((resolve) => { - let hit = null; - pull( - client.createLogStream(), - pull.drain((m) => { - if (hit) return; - const c = m && m.value && m.value.content; - if (!c || c.type !== 'larpHouseTribeAnchor') return; - if (c.house !== houseKey) return; - if (c.tribeRootId !== tribeRootId) return; - hit = { author: m.value.author, ts: m.value.timestamp || 0 }; - }, () => resolve(hit)) - ); - }); + const msgs = await readTyped(client, ANCHOR_TYPES); + for (const m of msgs) { + const c = m && m.value && m.value.content; + if (!c || c.type !== 'larpHouseTribeAnchor' || c.house !== houseKey || c.tribeRootId !== tribeRootId) continue; + return { author: m.value.author, ts: m.value.timestamp || 0 }; + } + return null; } async function publishHouseTribeAnchor(houseKey, tribeRootId, tribeCreatedAt) { @@ -327,7 +321,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { const canonical = await findAnyHouseTribe(houseKey).catch(() => null); const canonRoot = canonical ? await tribesModel.getRootId(canonical.id).catch(() => canonical.id) : null; const myTribes = await listMyHouseTribes(houseKey); - myTribes.sort((a, b) => a.createdAtTs - b.createdAtTs); + myTribes.sort((a, b) => (a.createdAtTs - b.createdAtTs) || String(a.rootId || '').localeCompare(String(b.rootId || ''))); const keepRoot = canonRoot || (myTribes[0] && myTribes[0].rootId); for (const t of myTribes) { if (t.rootId === keepRoot) continue; @@ -340,6 +334,13 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { await redeemPendingAutoInvites().catch(() => {}); return await findMyHouseTribe(houseKey) || canonical; } + if (Date.now() - startedAt < ACADEMIA_GRACE_MS && await followsSomeone(client, me)) { + if (!academiaRetry) { + academiaRetry = setTimeout(() => { academiaRetry = null; enqueue(runCatchup); }, ACADEMIA_GRACE_MS - (Date.now() - startedAt)); + if (academiaRetry.unref) academiaRetry.unref(); + } + return null; + } const house = HOUSES[houseKey] || {}; const tag = houseTribeTag(houseKey); try { @@ -455,6 +456,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { joinedAt: new Date().toISOString() }, (err, msg) => err ? reject(err) : resolve(msg)); }); + forgetMemberships(); if (previousHouse && previousHouse !== houseKey) { await leaveMyHouseTribe(previousHouse).catch(() => {}); } @@ -466,53 +468,45 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { async function getUserHouse(feedId) { const client = await openSsb(); const target = feedId || client.id; - return new Promise((resolve) => { - let latest = null; - let latestTs = 0; - pull( - client.createUserStream({ id: target, reverse: true }), - pull.drain((m) => { - const c = m && m.value && m.value.content; - if (!c) return; - const ts = m.value.timestamp || 0; - if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) { - if (ts > latestTs) { latestTs = ts; latest = c.house; } - } else if (c.type === 'larpLeaveLarp') { - if (ts > latestTs) { latestTs = ts; latest = null; } - } - }, () => resolve(latest)) - ); - }); + return (await listAllMemberships()).get(target) || null; } - async function listAllMemberships() { + async function readMemberships() { const client = await openSsb(); - return new Promise((resolve) => { - const byAuthor = new Map(); - pull( - client.createLogStream({ reverse: true }), - pull.drain((m) => { - const author = m && m.value && m.value.author; - if (!author) return; - const c = m.value.content; - if (!c) return; - const ts = m.value.timestamp || 0; - if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) { - const prev = byAuthor.get(author); - if (!prev || ts > prev.ts) byAuthor.set(author, { house: c.house, ts }); - } else if (c.type === 'larpLeaveLarp') { - const prev = byAuthor.get(author); - if (!prev || ts > prev.ts) byAuthor.set(author, { house: null, ts }); - } - }, () => { - const result = new Map(); - for (const [a, v] of byAuthor.entries()) { - if (v.house) result.set(a, v.house); - } - resolve(result); - }) - ); - }); + const msgs = await readTyped(client, ['larpJoinHouse', 'larpLeaveLarp'], { limit: 0 }); + const byAuthor = new Map(); + for (const m of msgs) { + const author = m && m.value && m.value.author; + if (!author) continue; + const c = m.value.content; + if (!c) continue; + const ts = m.value.timestamp || 0; + const seq = Number(m.value.sequence) || 0; + const prev = byAuthor.get(author); + const newer = !prev || ts > prev.ts || (ts === prev.ts && seq > prev.seq); + if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) { + if (newer) byAuthor.set(author, { house: c.house, ts, seq }); + } else if (c.type === 'larpLeaveLarp') { + if (newer) byAuthor.set(author, { house: null, ts, seq }); + } + } + const result = new Map(); + for (const [a, v] of byAuthor.entries()) { + if (v.house) result.set(a, v.house); + } + return result; + } + + const forgetMemberships = () => { + const store = requestScope.getStore(); + if (store) delete store.larpMemberships; + }; + + async function listAllMemberships() { + const store = requestScope.getStore(); + if (!store) return readMemberships(); + if (!store.larpMemberships) store.larpMemberships = readMemberships(); + return store.larpMemberships; } async function publishLeaveLarp() { @@ -525,6 +519,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { leftAt: new Date().toISOString() }, (err, msg) => err ? reject(err) : resolve(msg)); }); + forgetMemberships(); if (previousHouse) { await leaveMyHouseTribe(previousHouse).catch(() => {}); } @@ -568,18 +563,28 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { }); } + function wallIsPublic(houseKey, { isGoverning = false } = {}) { + return houseKey === 'academia' || isGoverning === true; + } + async function listHousePosts(houseKey, { viewerHouse = null, isGoverning = false } = {}) { if (!VALID_KEY(houseKey)) return []; const viewerIsMember = viewerHouse === houseKey; - if (!viewerIsMember && !isGoverning) return []; + if (!viewerIsMember && !wallIsPublic(houseKey, { isGoverning })) return []; const client = await openSsb(); const memberships = await listAllMemberships(); return new Promise((resolve) => { const posts = []; + const tombs = new Map(); pull( client.createLogStream({ reverse: true }), pull.drain((m) => { const c = m && m.value && m.value.content; + if (c && c.type === 'tombstone' && typeof c.target === 'string') { + if (!tombs.has(c.target)) tombs.set(c.target, new Set()); + tombs.get(c.target).add(m.value.author); + return; + } if (!c || c.type !== 'larpHousePost') return; if (c.house !== houseKey) return; const author = m.value.author; @@ -593,8 +598,9 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { ts: m.value.timestamp || 0 }); }, () => { - posts.sort((a, b) => b.ts - a.ts); - resolve(posts); + const shown = posts.filter(post => !(tombs.get(post.id) && tombs.get(post.id).has(post.author))); + shown.sort((a, b) => b.ts - a.ts); + resolve(shown); }) ); }); @@ -761,14 +767,20 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { const ssbKeys = require('../server/node_modules/ssb-keys'); const config = require('../server/ssb_config'); const codes = []; + const sealedOnly = client.private && typeof client.private.read === 'function'; await new Promise((resolve) => { pull( - client.createLogStream({ reverse: true, limit: 2000 }), + sealedOnly ? client.private.read({ reverse: true }) : client.createLogStream({ reverse: true, limit: 2000 }), pull.drain((m) => { const c = m && m.value && m.value.content; - if (typeof c !== 'string' || !c.endsWith('.box')) return; let decoded; - try { decoded = ssbKeys.unbox(c, config.keys); } catch (_) { return; } + if (typeof c === 'string') { + if (!c.endsWith('.box')) return; + try { decoded = ssbKeys.unbox(c, config.keys); } catch (_) { return; } + } else { + if (!m.value.private) return; + decoded = c; + } if (!decoded) return; if (typeof decoded === 'string') { try { decoded = JSON.parse(decoded); } catch (_) { return; } @@ -823,6 +835,13 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { await issueAutoInvitesForMyHouse().catch(() => {}); return; } + if (typeof c === 'object' && c.type === 'tribe' && Array.isArray(c.tags) && c.tags.includes(houseTribeTag('academia'))) { + if (m.value.author === me) return; + const myHouse = await getUserHouse(me).catch(() => null); + if (myHouse !== 'academia') return; + await ensureHouseTribe('academia').catch(() => {}); + return; + } if (typeof c === 'object' && c.type === 'larpHouseTribeAnchor' && VALID_KEY(c.house)) { if (m.value.author === me) return; const myHouse = await getUserHouse(me).catch(() => null); @@ -912,14 +931,17 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return { HOUSES, HOUSE_KEYS, + wallIsPublic, TEST_COOLDOWN_MS, TEST_QUESTIONS_COUNT, PROFILE_QUESTIONS, computeCycle, getGoverningHouseKey, + getGoverningPeriodId, publishJoin, publishLeaveLarp, getUserHouse, + listAllMemberships, listHousesWithCounts, getMembersOfHouse, publishHousePost, diff --git a/src/models/legacy_model.js b/src/models/legacy_model.js deleted file mode 100644 index 3499811..0000000 --- a/src/models/legacy_model.js +++ /dev/null @@ -1,99 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); -const os = require('os'); - -const MAGIC = Buffer.from('OASIS1'); -const SALT_LEN = 16; -const IV_LEN = 12; -const TAG_LEN = 16; - -function normalizePassword(password) { - if (password && typeof password === 'object' && password.password) password = password.password; - return String(password || ''); -} - -function deriveKey(password, salt) { - return crypto.scryptSync(password, salt, 32); -} - -function encryptBuffer(plaintext, password) { - const salt = crypto.randomBytes(SALT_LEN); - const iv = crypto.randomBytes(IV_LEN); - const key = deriveKey(password, salt); - const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); - const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); - const tag = cipher.getAuthTag(); - return Buffer.concat([MAGIC, salt, iv, tag, ciphertext]); -} - -function decryptBuffer(data, password) { - if (!Buffer.isBuffer(data) || data.length < MAGIC.length + SALT_LEN + IV_LEN + TAG_LEN) { - throw new Error('Unrecognized or corrupt backup file.'); - } - if (!data.slice(0, MAGIC.length).equals(MAGIC)) { - throw new Error('Unrecognized or corrupt backup file.'); - } - let o = MAGIC.length; - const salt = data.slice(o, o += SALT_LEN); - const iv = data.slice(o, o += IV_LEN); - const tag = data.slice(o, o += TAG_LEN); - const ciphertext = data.slice(o); - const key = deriveKey(password, salt); - const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); - decipher.setAuthTag(tag); - return Buffer.concat([decipher.update(ciphertext), decipher.final()]); -} - -module.exports = { - exportData: async (password) => { - try { - const pw = normalizePassword(password); - const homeDir = os.homedir(); - const secretFilePath = path.join(homeDir, '.ssb', 'secret'); - if (!fs.existsSync(secretFilePath)) { - throw new Error(".ssb/secret file doesn't exist"); - } - const original = fs.readFileSync(secretFilePath); - const encrypted = encryptBuffer(original, pw); - const roundtrip = decryptBuffer(encrypted, pw); - if (!roundtrip.equals(original)) { - throw new Error('Backup verification failed; nothing was written.'); - } - const encryptedFilePath = path.join(homeDir, 'oasis.enc'); - fs.writeFileSync(encryptedFilePath, encrypted, { mode: 0o600 }); - return encryptedFilePath; - } catch (error) { - throw new Error("Error exporting data: " + error.message); - } - }, - importData: async ({ filePath, password }) => { - try { - const pw = normalizePassword(password); - if (!fs.existsSync(filePath)) { - throw new Error('Encrypted file not found.'); - } - const data = fs.readFileSync(filePath); - let decrypted; - try { - decrypted = decryptBuffer(data, pw); - } catch (_) { - throw new Error('Wrong password or corrupt backup file.'); - } - const homeDir = os.homedir(); - const ssbDir = path.join(homeDir, '.ssb'); - fs.mkdirSync(ssbDir, { recursive: true }); - const secretPath = path.join(ssbDir, 'secret'); - if (fs.existsSync(secretPath)) { - fs.copyFileSync(secretPath, path.join(ssbDir, 'secret.bak-' + new Date().toISOString().replace(/[:.]/g, '-'))); - } - const tmpPath = path.join(ssbDir, 'secret.tmp-' + process.pid); - fs.writeFileSync(tmpPath, decrypted, { mode: 0o600 }); - fs.renameSync(tmpPath, secretPath); - try { fs.unlinkSync(filePath); } catch (_) {} - return secretPath; - } catch (error) { - throw new Error("Error importing data: " + error.message); - } - } -}; diff --git a/src/models/logistics_model.js b/src/models/logistics_model.js new file mode 100644 index 0000000..cec6bea --- /dev/null +++ b/src/models/logistics_model.js @@ -0,0 +1,390 @@ +const { readTyped, memoIndex } = require('./typed_log'); +const categories = require('../backend/opinion_categories'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const ROUTE_TYPE = 'logisticsRoute'; +const RATING_TYPE = 'logisticsRating'; +const BOOKING_TYPE = 'logistics-booking'; +const BOOKING_STATUS_TYPE = 'logistics-booking-status'; +const OPINION_TYPE = 'logisticsOpinion'; +const PUBLIC_TYPES = [ROUTE_TYPE, RATING_TYPE, OPINION_TYPE, 'tombstone']; +const KINDS = ['TRIP', 'SHIPMENT']; +const MODES = ['OFFER', 'REQUEST']; +const PRICE_TYPES = ['FREE', 'ECO', 'TIME']; +const RECURRENCES = ['NONE', 'DAILY', 'WEEKLY', 'MONTHLY']; +const STATUSES = ['OPEN', 'CLOSED']; +const BOOKING_STATUSES = ['PENDING', 'CONFIRMED', 'REJECTED', 'CANCELLED', 'DELIVERED']; +const OWNER_BOOKING_STATUSES = ['CONFIRMED', 'REJECTED', 'DELIVERED']; +const RECENT_MS = 24 * 60 * 60 * 1000; +const MAX_ZONES = 8; + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normU = (v) => String(v || '').trim().toUpperCase(); +const pick = (list, v, fallback) => (list.includes(normU(v)) ? normU(v) : fallback); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const toIso = (v) => { const t = Date.parse(v || ''); return Number.isFinite(t) ? new Date(t).toISOString() : ''; }; +const num = (v, min = 0) => { const n = Number(v); return Number.isFinite(n) && n >= min ? n : 0; }; +const MEDIA_RE = /!?\[(image|video):[^\]]*\]\((&[^)]+)\)/; +const mediaOf = (text) => { const m = safeText(text).match(MEDIA_RE); return m ? { kind: m[1], blobId: m[2] } : null; }; +const zoneKey = (s) => safeText(s).toLowerCase(); + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + const publishPrivate = (ssbClient, content, recps) => new Promise((resolve, reject) => ssbClient.private.publish(content, recps, (err, res) => err ? reject(err) : resolve(res))); + + const readPublic = async (ssbClient) => readTyped(ssbClient, PUBLIC_TYPES, { limit: logLimit }); + + const readPrivate = async (ssbClient) => { + const msgs = await readTyped(ssbClient, [BOOKING_TYPE, BOOKING_STATUS_TYPE], { limit: logLimit, withPrivate: true }); + const bookings = []; + const statuses = []; + for (const m of msgs) { + const v = (m && m.value) || {}; + if (v.private !== true) continue; + const c = v.content; + if (!c || typeof c !== 'object') continue; + const ts = v.timestamp || m.timestamp || 0; + if (c.type === BOOKING_TYPE && c.routeId) bookings.push({ id: m.key, author: v.author, ts, c }); + else if (c.type === BOOKING_STATUS_TYPE && c.bookingId) statuses.push({ key: m.key, author: v.author, ts, c }); + } + return { bookings, statuses }; + }; + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const parentOf = new Map(); + const ratings = []; + const opinions = []; + for (const m of messages) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === ROUTE_TYPE) { + nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } else if (c.type === OPINION_TYPE && typeof c.target === 'string') { + opinions.push({ target: c.target, author: v.author, category: String(c.category || ''), ts: v.timestamp || 0 }); + } else if (c.type === RATING_TYPE && typeof c.target === 'string') { + ratings.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0, score: Math.min(5, Math.max(1, num(c.score, 1) || 1)), text: safeText(c.text), createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString() }); + } + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const routes = new Map(); + const now = Date.now(); + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + const c = tip.c; + const dateIso = toIso(c.date); + const dateTs = Date.parse(dateIso) || 0; + const recurrence = pick(RECURRENCES, c.recurrence, 'NONE'); + const past = !!dateTs && dateTs < now && recurrence === 'NONE'; + const status = pick(STATUSES, c.status, 'OPEN') === 'CLOSED' ? 'CLOSED' : (past ? 'PAST' : 'OPEN'); + const byAuthor = new Map(); + for (const r of ratings.filter(r => nodes.has(r.target) && rootOf(r.target) === root)) { + const prev = byAuthor.get(r.author); + if (!prev || r.ts >= prev.ts) byAuthor.set(r.author, r); + } + const routeRatings = Array.from(byAuthor.values()).sort((a, b) => b.ts - a.ts); + const ops = {}; + const voters = []; + for (const op of opinions.filter(o => nodes.has(o.target) && rootOf(o.target) === root).sort((a, b) => a.ts - b.ts)) { + if (voters.includes(op.author) || !categories.includes(op.category)) continue; + voters.push(op.author); + ops[op.category] = (ops[op.category] || 0) + 1; + } + const ratingAvg = routeRatings.length ? Math.round((routeRatings.reduce((s, r) => s + r.score, 0) / routeRatings.length) * 10) / 10 : 0; + routes.set(root, { + id: root, rootId: root, tipId: tip.key, + kind: pick(KINDS, c.kind, 'TRIP'), mode: pick(MODES, c.mode, 'OFFER'), + title: safeText(c.title), description: safeText(c.description), + origin: safeText(c.origin), destination: safeText(c.destination), mapUrl: safeText(c.mapUrl), + date: dateIso, dateTs, recurrence, + seats: num(c.seats), seatsTaken: num(c.seatsTaken), size: safeText(c.size), weight: safeText(c.weight), media: mediaOf(c.description), + priceType: pick(PRICE_TYPES, c.priceType, 'FREE'), price: num(c.price), + orderRef: safeText(c.orderRef), tags: normalizeTags(c.tags), + status, closed: status !== 'OPEN', + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, + ratings: routeRatings, ratingCount: routeRatings.length, ratingAvg, + opinions: ops, opinions_inhabitants: voters, opinionCount: voters.length + }); + } + return { routes, rootOf, nodes }; + }; + + const bookingsFor = (priv, routeId, me) => { + const statusById = new Map(); + for (const s of priv.statuses) { + const prev = statusById.get(s.c.bookingId); + if (!prev || s.ts >= prev.ts) statusById.set(s.c.bookingId, { status: pick(BOOKING_STATUSES, s.c.status, 'PENDING'), ts: s.ts, by: s.author, receipt: safeText(s.c.receipt) }); + } + return priv.bookings + .filter(b => String(b.c.routeId) === String(routeId)) + .map(b => { + const st = statusById.get(b.id) || null; + return { + id: b.id, routeId: String(b.c.routeId), booker: b.author, owner: safeText(b.c.owner), + seats: num(b.c.seats), notes: safeText(b.c.notes), orderRef: safeText(b.c.orderRef), + createdAt: b.c.createdAt || new Date(b.ts).toISOString(), ts: b.ts, + status: st ? st.status : 'PENDING', statusAt: st ? new Date(st.ts).toISOString() : null, receipt: st ? st.receipt : '', + mine: b.author === me + }; + }) + .sort((a, b) => b.ts - a.ts); + }; + + const load = async () => { + const ssbClient = await openSsb(); + const [pub, priv] = await Promise.all([readPublic(ssbClient), readPrivate(ssbClient)]); + return { ssbClient, idx: memoIndex('logistics', pub, buildIndex), priv }; + }; + + const find = (idx, id) => { + const key = String(id || ''); + if (idx.routes.has(key)) return idx.routes.get(key); + if (idx.nodes.has(key)) return idx.routes.get(idx.rootOf(key)) || null; + return null; + }; + + const decorate = (route, priv, me) => { + const bookings = bookingsFor(priv, route.id, me); + const confirmed = bookings.filter(b => b.status === 'CONFIRMED' || b.status === 'DELIVERED'); + const taken = Math.max(route.seatsTaken || 0, confirmed.reduce((s, b) => s + (b.seats || 1), 0)); + const myBooking = bookings.find(b => b.mine && b.status !== 'CANCELLED' && b.status !== 'REJECTED') || null; + const isOwner = route.author === me; + const participated = isOwner || confirmed.some(b => b.mine); + const myRating = route.ratings.find(r => r.author === me) || null; + return { + ...route, + bookings: isOwner ? bookings : bookings.filter(b => b.mine), + bookingCount: bookings.filter(b => b.status !== 'CANCELLED' && b.status !== 'REJECTED').length, + confirmedCount: confirmed.length, seatsTaken: taken, + seatsLeft: route.kind === 'TRIP' && route.seats ? Math.max(0, route.seats - taken) : null, + myBooking, isOwner, participated, + canBook: !isOwner && !route.closed && !myBooking, + canRate: participated && route.closed && !myRating, + myRating, + lastActivityTs: Math.max(route.ts, ...bookings.map(b => b.ts), ...route.ratings.map(r => r.ts)) + }; + }; + + const routeContent = (userId, data, base = null) => ({ + type: ROUTE_TYPE, + kind: pick(KINDS, data.kind, base ? base.kind : 'TRIP'), + mode: pick(MODES, data.mode, base ? base.mode : 'OFFER'), + title: safeText(data.title) || (base ? base.title : ''), + description: data.description !== undefined ? safeText(data.description) : (base ? base.description : ''), + origin: data.origin !== undefined ? safeText(data.origin) : (base ? base.origin : ''), + destination: data.destination !== undefined ? safeText(data.destination) : (base ? base.destination : ''), + mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : (base ? base.mapUrl : ''), + date: data.date !== undefined ? toIso(data.date) : (base ? base.date : ''), + recurrence: pick(RECURRENCES, data.recurrence, base ? base.recurrence : 'NONE'), + seats: data.seats !== undefined ? num(data.seats) : (base ? base.seats : 0), + size: data.size !== undefined ? safeText(data.size) : (base ? base.size : ''), + weight: data.weight !== undefined ? safeText(data.weight) : (base ? base.weight : ''), + priceType: pick(PRICE_TYPES, data.priceType, base ? base.priceType : 'FREE'), + price: data.price !== undefined ? num(data.price) : (base ? base.price : 0), + orderRef: data.orderRef !== undefined ? safeText(data.orderRef) : (base ? base.orderRef : ''), + tags: data.tags !== undefined ? normalizeTags(data.tags) : (base ? base.tags : []), + status: data.status !== undefined ? pick(STATUSES, data.status, 'OPEN') : (base ? (base.status === 'CLOSED' ? 'CLOSED' : 'OPEN') : 'OPEN'), + seatsTaken: data.seatsTaken !== undefined ? num(data.seatsTaken) : (base ? base.seatsTaken : 0), + author: userId, + createdAt: base ? base.createdAt : new Date().toISOString(), + updatedAt: new Date().toISOString() + }); + + return { + KINDS, MODES, PRICE_TYPES, RECURRENCES, STATUSES, BOOKING_STATUSES, + + async createRoute(data = {}) { + const ssbClient = await openSsb(); + const content = routeContent(ssbClient.id, data); + if (!content.title) throw new Error('Title is required'); + if (!content.origin || !content.destination) throw new Error('Origin and destination are required'); + if (!content.date) throw new Error('A valid date is required'); + if (!(content.price > 0)) { content.price = 0; content.priceType = 'FREE'; } else if (content.priceType === 'FREE') content.priceType = 'ECO'; + return publish(ssbClient, content); + }, + + async updateRoute(id, data = {}) { + const { ssbClient, idx } = await load(); + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + if (route.author !== ssbClient.id) throw new Error('Only the author can update this route'); + const content = { ...routeContent(ssbClient.id, data, route), replaces: route.tipId }; + if (!(content.price > 0)) { content.price = 0; content.priceType = 'FREE'; } else if (content.priceType === 'FREE') content.priceType = 'ECO'; + const res = await publish(ssbClient, content); + return { key: res.key, rootId: route.id }; + }, + + async createOpinion(id, category) { + if (!categories.includes(category)) throw new Error('Invalid voting category'); + const { ssbClient, idx } = await load(); + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + if (route.opinions_inhabitants.includes(ssbClient.id)) throw new Error('Already voted'); + return publish(ssbClient, { type: OPINION_TYPE, target: route.id, category, createdAt: new Date().toISOString() }); + }, + + async closeRoute(id) { return this.updateRoute(id, { status: 'CLOSED' }); }, + async reopenRoute(id) { return this.updateRoute(id, { status: 'OPEN' }); }, + + async deleteRoute(id) { + const { ssbClient, idx } = await load(); + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + if (route.author !== ssbClient.id) throw new Error('Only the author can delete this route'); + await publish(ssbClient, { type: 'tombstone', target: route.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: route.id }; + }, + + async book(id, { seats = 1, notes = '', orderRef = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + const view = decorate(route, priv, me); + if (view.isOwner) throw new Error('You cannot book your own route'); + if (view.closed) throw new Error('This route is closed'); + if (view.myBooking) throw new Error('You already booked this route'); + const n = route.kind === 'TRIP' ? Math.max(1, Math.floor(num(seats, 1) || 1)) : 1; + if (view.seatsLeft !== null && n > view.seatsLeft) throw new Error('Not enough seats left'); + const content = { type: BOOKING_TYPE, routeId: route.id, owner: route.author, seats: n, notes: safeText(notes), orderRef: safeText(orderRef), createdAt: new Date().toISOString() }; + const res = await publishPrivate(ssbClient, content, [me, route.author]); + return { ...res, routeId: route.id, owner: route.author, booker: me }; + }, + + async setBookingStatus(bookingId, status, { receipt = '' } = {}) { + const { ssbClient, priv } = await load(); + const me = ssbClient.id; + const st = pick(BOOKING_STATUSES, status, ''); + if (!st || st === 'PENDING') throw new Error('Invalid status'); + const b = priv.bookings.find(x => x.id === bookingId); + if (!b) throw new Error('Booking not found'); + const owner = safeText(b.c.owner); + if (OWNER_BOOKING_STATUSES.includes(st)) { if (me !== owner) throw new Error('Only the route owner can set this status'); } + else if (me !== b.author) throw new Error('Only the booker can cancel'); + const content = { type: BOOKING_STATUS_TYPE, bookingId, routeId: String(b.c.routeId), status: st, ...(st === 'DELIVERED' ? { receipt: safeText(receipt) || `Delivered ${new Date().toISOString()}` } : {}), updatedAt: new Date().toISOString() }; + const res = await publishPrivate(ssbClient, content, Array.from(new Set([me, b.author, owner].filter(Boolean)))); + if (me === owner) { + try { + const fresh = await load(); + const route = find(fresh.idx, b.c.routeId); + if (route && route.kind === 'TRIP' && route.seats) { + const taken = bookingsFor(fresh.priv, route.id, me).filter(x => x.status === 'CONFIRMED' || x.status === 'DELIVERED').reduce((sum, x) => sum + (x.seats || 1), 0); + if (taken !== route.seatsTaken) await publish(ssbClient, { ...routeContent(me, { seatsTaken: taken }, route), replaces: route.tipId }); + } + } catch (_) {} + } + return { ...res, routeId: String(b.c.routeId), owner, booker: b.author, status: st }; + }, + + async rate(id, { score, text = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + const view = decorate(route, priv, me); + if (!view.participated) throw new Error('Only participants can rate a route'); + if (!view.closed) throw new Error('Routes are rated once they are closed'); + if (view.myRating) throw new Error('You already rated this route'); + const s = Math.min(5, Math.max(1, Math.round(num(score, 1) || 1))); + return publish(ssbClient, { type: RATING_TYPE, target: route.id, score: s, text: safeText(text), createdAt: new Date().toISOString() }); + }, + + async getRouteById(id) { + const { ssbClient, idx, priv } = await load(); + const route = find(idx, id); + return route ? decorate(route, priv, ssbClient.id) : null; + }, + + async myBookings() { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const out = []; + for (const b of priv.bookings) { + if (b.author !== me) continue; + const route = find(idx, b.c.routeId); + if (!route) continue; + const view = decorate(route, priv, me); + const mine = view.bookings.find(x => x.id === b.id); + if (mine) out.push({ ...mine, route: view }); + } + return out.sort((a, b) => b.ts - a.ts); + }, + + zonesOf(list) { + const counts = new Map(); + for (const r of list || []) { + for (const z of [r.origin, r.destination]) { + const k = zoneKey(z); + if (!k) continue; + const prev = counts.get(k) || { label: safeText(z), count: 0 }; + prev.count += 1; + counts.set(k, prev); + } + } + return Array.from(counts.values()).sort((a, b) => b.count - a.count || a.label.localeCompare(b.label)).slice(0, MAX_ZONES); + }, + + async listAll({ filter = 'all', q = '', zone = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + const z = zoneKey(zone); + let out = Array.from(idx.routes.values()).map(r => decorate(r, priv, me)); + if (f === 'mine') out = out.filter(r => r.isOwner); + else if (f === 'booked') out = out.filter(r => !!r.myBooking); + else if (f === 'recent') out = out.filter(r => r.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'trips') out = out.filter(r => r.kind === 'TRIP'); + else if (f === 'shipments') out = out.filter(r => r.kind === 'SHIPMENT'); + else if (f === 'offers') out = out.filter(r => r.mode === 'OFFER'); + else if (f === 'requests') out = out.filter(r => r.mode === 'REQUEST'); + else if (f === 'upcoming') out = out.filter(r => r.status === 'OPEN'); + else if (f === 'past') out = out.filter(r => r.status === 'PAST'); + else if (f === 'closed') out = out.filter(r => r.status === 'CLOSED'); + else if (f === 'history') out = out.filter(r => r.closed && r.participated); + else if (f === 'free') out = out.filter(r => r.priceType === 'FREE'); + else if (f === 'eco') out = out.filter(r => r.priceType === 'ECO'); + else if (f === 'time') out = out.filter(r => r.priceType === 'TIME'); + else if (f === 'top') out = out.filter(r => r.ratingCount > 0); + if (z) out = out.filter(r => zoneKey(r.origin) === z || zoneKey(r.destination) === z); + if (needle) out = out.filter(r => [r.title, r.description, r.origin, r.destination, r.kind, r.mode, ...r.tags].some(v => String(v || '').toLowerCase().includes(needle))); + if (f === 'top') out.sort((a, b) => b.ratingAvg - a.ratingAvg || b.ratingCount - a.ratingCount); + else out.sort((a, b) => (a.closed === b.closed ? 0 : (a.closed ? 1 : -1)) || (a.dateTs && b.dateTs ? a.dateTs - b.dateTs : b.lastActivityTs - a.lastActivityTs)); + return out; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const route = find(idx, id); + return route ? route.id : String(id || ''); + } + }; +}; diff --git a/src/models/logs_model.js b/src/models/logs_model.js index 88bade7..741b44d 100644 --- a/src/models/logs_model.js +++ b/src/models/logs_model.js @@ -1,11 +1,12 @@ const pull = require('../server/node_modules/pull-stream'); -const util = require('../server/node_modules/util'); -const axios = require('../server/node_modules/axios'); +const { requestScope } = require('./typed_log'); +const util = require('util'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const logLimit = getConfig().ssbLogStream?.limit || 1000; + const DAY_MS = 24 * 60 * 60 * 1000; const WEEK_MS = 7 * DAY_MS; const MONTH_MS = 30 * DAY_MS; @@ -21,9 +22,9 @@ const FILTER_WINDOWS = { const ACTION_TYPES = new Set([ 'post', 'about', 'contact', 'feed', 'bookmark', 'image', 'audio', 'video', - 'document', 'torrent', 'event', 'task', 'taskAssignment', - 'votes', 'vote', 'report', 'tribe', 'chat', 'chatMessage', 'pad', 'padEntry', - 'forum', 'market', 'job', 'project', 'pixelia', 'map', 'mapMarker', + 'document', 'torrent', 'file', 'event', 'task', 'taskAssign', + 'votes', 'vote', 'report', 'tribe', 'chat', 'chatMessage', 'pad', 'padEntry', 'room', 'roomMember', + 'forum', 'market', 'job', 'project', 'industry', 'industryBuild', 'pixelia', 'map', 'mapMarker', 'shop', 'shopProduct', 'curriculum', 'gameScore', 'calendar', 'calendarDate', 'calendarNote', 'transfer', 'bankClaim', 'ubiClaim', @@ -32,7 +33,29 @@ const ACTION_TYPES = new Set([ 'courtsCase', 'courtsEvidence', 'courtsAnswer', 'courtsVerdict', 'courtsNomination', 'courtsNominationVote', 'courtsSettlementProposal', 'courtsSettlementAccepted', - 'tribeParliamentCandidature', 'tribeParliamentRule' + 'tribeParliamentCandidature', 'tribeParliamentRule', + 'wikiPage', 'emergency', 'emergencyConfirm', 'emergencyUpdate', 'mailingList', + 'logisticsRoute', 'logisticsRating', 'logisticsOpinion', + 'podcast', 'podcastEpisode', 'podcastOpinion', 'podcastPlay', + 'campaign', 'campaignSignature', 'campaignUpdate', 'campaignOpinion', + 'audioOpinion', 'blogOpinion', 'bookmarkOpinion', 'documentOpinion', 'eventOpinion', 'feedOpinion', + 'fileOpinion', 'imageOpinion', 'videoOpinion', 'torrentOpinion', 'housingOpinion', 'industryOpinion', + 'marketOpinion', 'projectOpinion', 'reportOpinion', 'schoolOpinion', 'shopOpinion', 'taskOpinion', + 'transferOpinion', 'votesOpinion', 'pollOpinion', + 'calendarParticipant', 'chatMember', 'chatPin', 'chatReaction', 'padMember', 'mapMember', + 'courtsJudge', 'courtsMediators', 'courtsSupport', 'courtsVisibility', 'courtsVoteLink', + 'eventAttend', 'feed-action', 'spread', 'subscription', 'job_sub', + 'housing', 'housingRequest', + 'industryAllocation', 'industryApply', 'industryBlueprint', 'industryBuildStatus', 'industryBuildUpdate', + 'industryContribution', 'industryMember', 'industryVote', + 'larpHousePost', 'larpJoinHouse', 'larpLeaveLarp', 'larpTestAttempt', + 'marketBid', 'marketPurchase', 'shopPurchase', + 'parliamentCandidatureVote', 'tribeParliamentTerm', + 'poll', 'pollVote', 'pollClose', 'votesVote', + 'projectClaim', 'projectFollow', 'projectPledge', 'projectPledgeConfirm', + 'reportConfirm', 'transferConfirm', 'ubiRefuse', + 'schoolCourse', 'schoolEnroll', 'schoolLesson', 'schoolLessonMedia', 'schoolExam', 'schoolExamQuestion', + 'schoolExamResult', 'schoolProgress', 'schoolCertificate', 'schoolCommentHide' ]); const ACTION_PHRASES = { @@ -46,9 +69,10 @@ const ACTION_PHRASES = { video: 'uploaded a video', document: 'uploaded a document', torrent: 'shared a torrent', + file: 'shared a file', event: 'created an event', task: 'created a task', - taskAssignment: 'updated a task assignment', + taskAssign: 'updated a task assignment', votes: 'participated in a vote', vote: 'cast a vote', report: 'submitted a report', @@ -57,10 +81,14 @@ const ACTION_PHRASES = { chatMessage: 'sent a chat message', pad: 'worked on a collaborative pad', padEntry: 'edited a pad entry', + room: 'opened a room', + roomMember: 'joined a room', market: 'posted in the market', forum: 'posted in the forum', job: 'posted a job opportunity', project: 'advanced a project', + industry: 'founded a network-owned facility', + industryBuild: 'proposed a production build', pixelia: 'placed a pixel in pixelia', map: 'contributed to a map', mapMarker: 'placed a marker on a map', @@ -88,7 +116,100 @@ const ACTION_PHRASES = { courtsSettlementProposal: 'proposed a courts settlement', courtsSettlementAccepted: 'accepted a courts settlement', tribeParliamentCandidature: 'stood for a tribe parliament', - tribeParliamentRule: 'contributed a tribe parliament rule' + tribeParliamentRule: 'contributed a tribe parliament rule', + wikiPage: 'edited a wiki page', + emergency: 'reported an emergency', + emergencyConfirm: 'confirmed an emergency', + emergencyUpdate: 'posted an emergency update', + mailingList: 'created a mailing list', + logisticsRoute: 'published a logistics route', + logisticsRating: 'rated a logistics route', + logisticsOpinion: 'gave an opinion on a route', + podcast: 'created a podcast', + podcastEpisode: 'published a podcast episode', + podcastOpinion: 'gave an opinion on a podcast', + podcastPlay: 'listened to a podcast episode', + campaign: 'started a campaign', + campaignSignature: 'signed a campaign', + campaignUpdate: 'posted a campaign update', + campaignOpinion: 'gave an opinion on a campaign', + audioOpinion: 'gave an opinion on an audio', + blogOpinion: 'gave an opinion on a blog post', + bookmarkOpinion: 'gave an opinion on a bookmark', + documentOpinion: 'gave an opinion on a document', + eventOpinion: 'gave an opinion on an event', + feedOpinion: 'gave an opinion on a feed', + fileOpinion: 'gave an opinion on a file', + imageOpinion: 'gave an opinion on an image', + videoOpinion: 'gave an opinion on a video', + torrentOpinion: 'gave an opinion on a torrent', + housingOpinion: 'gave an opinion on a housing listing', + industryOpinion: 'gave an opinion on a facility', + marketOpinion: 'gave an opinion on a market item', + projectOpinion: 'gave an opinion on a project', + reportOpinion: 'gave an opinion on a report', + schoolOpinion: 'gave an opinion on a course', + shopOpinion: 'gave an opinion on a shop', + taskOpinion: 'gave an opinion on a task', + transferOpinion: 'gave an opinion on a transfer', + votesOpinion: 'gave an opinion on a votation', + pollOpinion: 'gave an opinion on a poll', + calendarParticipant: 'joined a calendar', + chatMember: 'joined a chat', + chatPin: 'pinned a chat message', + chatReaction: 'reacted to a chat message', + padMember: 'joined a pad', + mapMember: 'joined a map', + courtsJudge: 'chose a judge for a courts case', + courtsMediators: 'proposed mediators for a courts case', + courtsSupport: 'supported a courts case', + courtsVisibility: 'changed the visibility of a courts case', + courtsVoteLink: 'linked a vote to a courts case', + eventAttend: 'joined an event', + 'feed-action': 'refeeded a feed', + spread: 'spread some content', + subscription: 'subscribed to some content', + job_sub: 'subscribed to a job', + housing: 'published a housing listing', + housingRequest: 'requested a housing', + industryAllocation: 'shared out the output of a production build', + industryApply: 'applied to join a facility', + industryBlueprint: 'published a blueprint', + industryBuildStatus: 'updated the status of a production build', + industryBuildUpdate: 'posted a production build update', + industryContribution: 'contributed to a production build', + industryMember: 'joined a facility', + industryVote: 'voted in a facility', + larpHousePost: 'posted in a L.A.R.P. house', + larpJoinHouse: 'joined a L.A.R.P. house', + larpLeaveLarp: 'left L.A.R.P.', + larpTestAttempt: 'took a L.A.R.P. test', + marketBid: 'placed a bid in the market', + marketPurchase: 'bought in the market', + shopPurchase: 'bought in a shop', + parliamentCandidatureVote: 'voted for a parliamentary candidature', + tribeParliamentTerm: 'took part in a tribe parliament term', + poll: 'created a poll', + pollVote: 'voted in a poll', + pollClose: 'closed a poll', + votesVote: 'voted in a votation', + projectClaim: 'claimed a project bounty', + projectFollow: 'followed a project', + projectPledge: 'pledged to a project', + projectPledgeConfirm: 'confirmed a project pledge', + reportConfirm: 'confirmed a report', + transferConfirm: 'confirmed a transfer', + ubiRefuse: 'declined the UBI', + schoolCourse: 'created a course', + schoolEnroll: 'enrolled in a course', + schoolLesson: 'published a lesson', + schoolLessonMedia: 'added media to a lesson', + schoolExam: 'created an exam', + schoolExamQuestion: 'added an exam question', + schoolExamResult: 'completed an exam', + schoolProgress: 'progressed in a course', + schoolCertificate: 'issued a course certificate', + schoolCommentHide: 'hid a comment in a course' }; const compact = (s, n = 200) => String(s || '').replace(/\s+/g, ' ').trim().slice(0, n); @@ -108,7 +229,7 @@ module.exports = ({ cooler }) => { const ssbClient = await openSsb(); const msgs = await new Promise((resolve, reject) => pull( - ssbClient.createLogStream({ reverse: true, limit: logLimit }), + ssbClient.createUserStream({ id: ssbClient.id, reverse: true, limit: logLimit }), pull.collect((err, arr) => err ? reject(err) : resolve(arr)) ) ); @@ -129,10 +250,11 @@ module.exports = ({ cooler }) => { async function callAI(prompt) { if (!prompt) return ''; + try { if ((require('../configs/config-manager.js').getConfig().modules || {}).aiMod !== 'on') return ''; } catch (_) { return ''; } const tryOnce = async () => { try { - const res = await axios.post('http://localhost:4001/ai', { input: prompt, raw: true }, { timeout: 90000 }); - return String(res?.data?.answer || '').trim(); + const answer = await require('../AI/ai_client').ask({ system: 'You write one short first-person diary sentence. No IDs, hashes, quotes, lists or markdown.', input: prompt, maxTokens: 80 }); + return String(answer || '').trim(); } catch { return ''; } }; let out = await tryOnce(); @@ -269,9 +391,10 @@ module.exports = ({ cooler }) => { async function readAllLogMessages() { const ssbClient = await openSsb(); + const sealedOnly = ssbClient.private && typeof ssbClient.private.read === 'function'; const raw = await new Promise((resolve, reject) => pull( - ssbClient.createLogStream({ reverse: false, limit: logLimit }), + sealedOnly ? ssbClient.private.read({ reverse: true, limit: logLimit }) : ssbClient.createLogStream({ reverse: true, limit: logLimit }), pull.collect((err, arr) => err ? reject(err) : resolve(arr)) ) ); @@ -285,13 +408,16 @@ module.exports = ({ cooler }) => { const tsIn = m.timestamp || valueIn?.timestamp || Date.now(); let dec; try { - dec = ssbClient.private.unbox({ key: keyIn, value: valueIn, timestamp: tsIn }); + dec = typeof valueIn.content === 'string' ? ssbClient.private.unbox({ key: keyIn, value: valueIn, timestamp: tsIn }) : (valueIn.private === true ? m : null); } catch { continue; } const v = dec?.value; const c = v?.content; if (!c) continue; if (v.author !== userId) continue; - if (c.type === 'tombstone') continue; + if (c.type === 'tombstone') { + if (typeof c.target === 'string' && c.target) tombstoned.add(c.target); + continue; + } if (c.type !== 'log') continue; if (c.replaces) replaced.set(c.replaces, dec.key || keyIn); items.push({ @@ -306,13 +432,24 @@ module.exports = ({ cooler }) => { ref: c.ref || null }); } + const parentOf = new Map(); + for (const i of items) if (i.replaces) parentOf.set(i.key, i.replaces); + const rootOf = (key) => { + let cur = key; + const seen = new Set(); + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; const survivors = items.filter(i => !tombstoned.has(i.key) && !replaced.has(i.key)); + for (const i of survivors) i.rootId = rootOf(i.key); survivors.sort((a, b) => b.ts - a.ts); return survivors; } async function listLogs(filter = 'today') { - const items = await readAllLogMessages(); + const store = requestScope.getStore(); + if (store && !store.logsAll) store.logsAll = readAllLogMessages(); + const items = await (store ? store.logsAll : readAllLogMessages()); const win = FILTER_WINDOWS[filter]; if (win === null || win === undefined) return items; const cutoff = Date.now() - win; @@ -327,11 +464,14 @@ module.exports = ({ cooler }) => { async function updateLog(id, { text, label, mode }) { const current = await getLogById(id); if (!current) return { status: 'not_found' }; - await republishLog({ - replaces: current.key, + const next = { text: text !== undefined ? text : current.text, label: label !== undefined ? label : current.label, - mode: mode || current.mode, + mode: mode || current.mode + }; + await republishLog({ + replaces: current.key, + ...next, createdAt: current.createdAt }); return { status: 'ok' }; @@ -357,6 +497,7 @@ module.exports = ({ cooler }) => { getLogById, listLogs, countLogs, - isAImodOn + isAImodOn, + FILTER_WINDOWS }; }; diff --git a/src/models/mailing_model.js b/src/models/mailing_model.js new file mode 100644 index 0000000..eee9543 --- /dev/null +++ b/src/models/mailing_model.js @@ -0,0 +1,437 @@ +const pull = require('../server/node_modules/pull-stream'); +const crypto = require('crypto'); +const { readTyped } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const longText = require('../backend/long_text'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const LIST_TYPE = 'mailingList'; +const LEAVE_TYPE = 'mailingLeave'; +const LIST_TYPES = ['OPEN', 'CLOSED']; +const STATUSES = ['ACTIVE', 'ARCHIVED']; +const BATCH = 6; +const RECENT_MS = 24 * 60 * 60 * 1000; + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normU = (v) => String(v || '').trim().toUpperCase(); +const normalizeListType = (v) => (LIST_TYPES.includes(normU(v)) ? normU(v) : 'OPEN'); +const normalizeStatus = (v) => (STATUSES.includes(normU(v)) ? normU(v) : 'ACTIVE'); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const isFeedId = (v) => typeof v === 'string' && /^@[A-Za-z0-9+/=]+\.ed25519$/.test(v.trim()); +const normalizeMembers = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(/[\s,]+/); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(isFeedId))); +}; +const uniq = (list) => Array.from(new Set((list || []).filter(Boolean))); +const canonicalSubject = (s) => String(s || '').replace(/^\s*(RE:\s*)+/i, '').trim(); + +module.exports = ({ cooler, subscriptionsModel = null }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + const publishPrivate = (ssbClient, content, recps) => new Promise((resolve, reject) => ssbClient.private.publish(content, recps, (err, res) => err ? reject(err) : resolve(res))); + + const publishPrivateBatched = async (ssbClient, content, recipients) => { + const me = ssbClient.id; + const others = uniq(recipients).filter(id => id !== me); + const out = []; + if (!others.length) { out.push(await publishPrivate(ssbClient, content, [me])); return out; } + for (let i = 0; i < others.length; i += BATCH) { + out.push(await publishPrivate(ssbClient, content, uniq([me, ...others.slice(i, i + BATCH)]))); + } + return out; + }; + + const privateState = new WeakMap(); + const readPrivate = async (ssbClient) => { + const me = ssbClient.id; + let st = privateState.get(ssbClient); + if (!st || st.me !== me) { + st = { me, lastTs: 0, seen: new Set(), lists: [], leaves: [], posts: [], chunks: new Map(), tombClaims: new Map(), authorByKey: new Map() }; + privateState.set(ssbClient, st); + } + const raw = await new Promise((resolve, reject) => { + pull(ssbClient.createLogStream(st.lastTs > 0 ? { reverse: false, gte: st.lastTs } : { reverse: false }), pull.collect((err, arr) => err ? reject(err) : resolve(arr))); + }); + for (const m of raw) { + if (!m || !m.value) continue; + const recv = Number(m.timestamp) || 0; + if (recv > st.lastTs) st.lastTs = recv; + const content = m.value.content; + if (typeof content !== 'string') continue; + if (m.key) { + if (st.seen.has(m.key)) continue; + st.seen.add(m.key); + } + let dec; + try { dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.timestamp || m.value.timestamp }); } catch (_) { continue; } + const v = (dec && dec.value) || {}; + const c = v.content; + const k = (dec && dec.key) || m.key; + if (!c || typeof c !== 'object' || !k) continue; + const ts = v.timestamp || m.timestamp || 0; + if (c.type === 'tombstone' && c.target) { + const set = st.tombClaims.get(c.target) || new Set(); + set.add(v.author); + st.tombClaims.set(c.target, set); + continue; + } + if (c.type === longText.CHUNK_TYPE) { + const ch = longText.chunkOf(v.author, c); + if (ch) st.chunks.set(k, ch); + continue; + } + st.authorByKey.set(k, v.author); + if (c.type === LIST_TYPE) st.lists.push({ key: k, author: v.author, ts, seq: v.sequence || 0, arrival: st.lists.length, c }); + else if (c.type === LEAVE_TYPE && c.list) st.leaves.push({ key: k, author: v.author, ts, list: String(c.list) }); + else if (c.type === 'post' && c.list) { + const to = Array.isArray(c.to) ? c.to : []; + if (v.author === me || to.includes(me)) st.posts.push({ key: k, author: v.author, ts, c }); + } + } + const tombed = new Set(); + for (const [target, authors] of st.tombClaims.entries()) { + const orig = st.authorByKey.get(target); + for (const a of authors) if (a === orig || a === me) { tombed.add(target); break; } + } + return { lists: st.lists.slice(), leaves: st.leaves.slice(), posts: st.posts.filter(p => !tombed.has(p.key)), tombed, chunks: st.chunks }; + }; + + const buildIndex = (publicMsgs, priv) => { + const tomb = buildValidatedTombstoneSet(publicMsgs); + const nodes = new Map(); + const parentOf = new Map(); + for (const m of publicMsgs) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object' || c.type !== LIST_TYPE) continue; + nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, arrival: Number(m.timestamp) || 0, c, closed: false }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const versionsById = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsById.has(root)) versionsById.set(root, []); + versionsById.get(root).push(node); + } + const privateById = new Map(); + for (const n of priv.lists) { + const id = n.c.listId ? String(n.c.listId) : null; + if (!id || nodes.has(id)) continue; + if (!privateById.has(id)) privateById.set(id, []); + privateById.get(id).push({ ...n, closed: true }); + } + for (const [id, versions] of privateById) versionsById.set(id, versions); + const ownerOf = (id, versions) => { + if (nodes.has(id)) return nodes.get(id).author; + const keys = new Set(versions.map(n => n.key)); + const heads = versions.filter(n => !(typeof n.c.replaces === 'string' && keys.has(n.c.replaces)) && (typeof n.c.author !== 'string' || n.c.author === n.author)); + heads.sort((a, b) => a.arrival - b.arrival); + return heads.length ? heads[0].author : null; + }; + const lists = new Map(); + for (const [id, versions] of versionsById) { + const owner = ownerOf(id, versions); + const own = versions.filter(v => v.author === owner).sort((a, b) => (a.seq - b.seq) || (a.ts - b.ts)); + if (!own.length || own.some(v => v.closed && normU(v.c.status) === 'DELETED')) continue; + const first = own[0]; + const tip = own[own.length - 1]; + const c = tip.c; + const listType = first.closed ? 'CLOSED' : normalizeListType(c.listType); + const updatedAt = c.updatedAt || new Date(tip.ts).toISOString(); + let members = first.closed ? uniq([owner, ...normalizeMembers(c.members)]) : []; + if (first.closed) { + const left = new Set(priv.leaves.filter(l => l.list === id && l.ts >= tip.ts).map(l => l.author)); + members = members.filter(m => m === owner || !left.has(m)); + } + lists.set(id, { + id, rootId: id, tipId: tip.key, + title: safeText(c.title), description: safeText(c.description), + listType, closed: listType === 'CLOSED', status: normalizeStatus(c.status), tags: normalizeTags(c.tags), + members, author: owner, + createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt, ts: tip.ts + }); + } + return { lists, rootOf, nodes }; + }; + + const historyFor = (priv, id, me) => { + const seen = new Map(); + for (const p of priv.posts) { + if (String(p.c.list) !== String(id)) continue; + const mid = p.c.mid ? String(p.c.mid) : p.key; + if (seen.has(mid)) continue; + seen.set(mid, p); + } + const out = Array.from(seen.values()).map(p => ({ + key: p.key, mid: p.c.mid ? String(p.c.mid) : p.key, list: String(p.c.list), + author: p.author, subject: safeText(p.c.subject), text: safeText(longText.joinText(p.c.text, p.c.chunks, longText.lookupIn(priv.chunks), p.author)), + thread: p.c.thread ? String(p.c.thread) : (p.c.mid ? String(p.c.mid) : p.key), + sentAt: p.c.sentAt || new Date(p.ts).toISOString(), ts: Date.parse(p.c.sentAt || '') || p.ts + })); + out.sort((a, b) => a.ts - b.ts); + return out; + }; + + const load = async () => { + const ssbClient = await openSsb(); + const [publicMsgs, priv] = await Promise.all([readTyped(ssbClient, [LIST_TYPE, 'tombstone'], { limit: logLimit }), readPrivate(ssbClient)]); + const idx = buildIndex(publicMsgs, priv); + return { ssbClient, idx, priv }; + }; + + const find = (idx, id) => { + const key = String(id || ''); + if (idx.lists.has(key)) return idx.lists.get(key); + if (idx.nodes.has(key)) return idx.lists.get(idx.rootOf(key)) || null; + return null; + }; + + const subscribersMap = async (ids) => { + if (!subscriptionsModel || !ids.length) return new Map(); + if (typeof subscriptionsModel.subscribersMap === 'function') { + try { return await subscriptionsModel.subscribersMap(ids); } catch (_) { return new Map(); } + } + const out = new Map(); + for (const id of ids) { + try { out.set(id, await subscriptionsModel.listSubscribers(id)); } catch (_) { out.set(id, []); } + } + return out; + }; + + const decorate = async (ssbClient, list, priv, subs) => { + const me = ssbClient.id; + const history = historyFor(priv, list.id, me); + const subscribers = list.closed ? [] : ((subs && subs.get(list.id)) || []); + const participants = list.closed ? list.members : uniq([list.author, ...subscribers]); + const isMember = participants.includes(me); + const lastTs = history.length ? history[history.length - 1].ts : list.ts; + return { + ...list, + subscribers, participants, participantCount: participants.length, + isMember, isOwner: list.author === me, canWrite: isMember && list.status === 'ACTIVE', + messageCount: history.length, threadCount: new Set(history.map(h => h.thread)).size, + lastActivityTs: lastTs, history + }; + }; + + const visibleTo = (list, me) => !list.closed || list.members.includes(me); + + const recipientsFor = async (ssbClient, list) => { + const me = ssbClient.id; + const subs = list.closed ? [] : ((await subscribersMap([list.id])).get(list.id) || []); + const all = list.closed ? list.members : uniq([list.author, ...subs]); + return { all, others: all.filter(id => id !== me) }; + }; + + const publishListVersion = async (ssbClient, content, list) => { + if (!list || !list.closed) return publish(ssbClient, content); + const res = await publishPrivateBatched(ssbClient, content, list.members); + return res[0]; + }; + + return { + LIST_TYPE, + LIST_TYPES, + STATUSES, + + async createList({ title, description, listType, members, tags }) { + const ssbClient = await openSsb(); + const me = ssbClient.id; + const now = new Date().toISOString(); + const t = safeText(title); + if (!t) throw new Error('Title is required'); + const type = normalizeListType(listType); + const content = { + type: LIST_TYPE, + title: t, + description: safeText(description), + listType: type, + status: 'ACTIVE', + tags: normalizeTags(tags), + author: me, + createdAt: now, + updatedAt: now + }; + if (type === 'CLOSED') { + const listId = crypto.randomBytes(16).toString('hex'); + const mem = uniq([me, ...normalizeMembers(members)]); + const res = await publishPrivateBatched(ssbClient, { ...content, listId, members: mem, private: true }, mem); + return { key: listId, rootId: listId, msgKey: res[0].key }; + } + const res = await publish(ssbClient, content); + return { key: res.key, rootId: res.key, msgKey: res.key }; + }, + + async updateList(id, data = {}) { + const { ssbClient, idx } = await load(); + const me = ssbClient.id; + const list = find(idx, id); + if (!list) throw new Error('Mailing list not found'); + if (list.author !== me) throw new Error('Only the author can update this mailing list'); + const now = new Date().toISOString(); + const nextMembers = list.closed && data.members !== undefined ? uniq([me, ...normalizeMembers(data.members)]) : list.members; + const content = { + type: LIST_TYPE, + title: data.title !== undefined && safeText(data.title) ? safeText(data.title) : list.title, + description: data.description !== undefined ? safeText(data.description) : list.description, + listType: list.listType, + status: data.status !== undefined ? normalizeStatus(data.status) : list.status, + tags: data.tags !== undefined ? normalizeTags(data.tags) : list.tags, + author: me, + createdAt: list.createdAt, + updatedAt: now, + replaces: list.tipId + }; + if (list.closed) { + const res = await publishPrivateBatched(ssbClient, { ...content, listId: list.id, members: nextMembers, private: true }, uniq([...list.members, ...nextMembers])); + return { key: res[0].key, rootId: list.id }; + } + const res = await publish(ssbClient, content); + return { key: res.key, rootId: list.id }; + }, + + async setStatus(id, status) { + return this.updateList(id, { status: normalizeStatus(status) }); + }, + + async deleteList(id) { + const { ssbClient, idx } = await load(); + const me = ssbClient.id; + const list = find(idx, id); + if (!list) throw new Error('Mailing list not found'); + if (list.author !== me) throw new Error('Only the author can delete this mailing list'); + if (list.closed) { + await publishPrivateBatched(ssbClient, { type: LIST_TYPE, listId: list.id, title: list.title, status: 'DELETED', author: me, createdAt: list.createdAt, updatedAt: new Date().toISOString(), replaces: list.tipId, private: true }, list.members); + return { key: list.id }; + } + await publish(ssbClient, { type: 'tombstone', target: list.id, deletedAt: new Date().toISOString(), author: me }); + return { key: list.id }; + }, + + async leaveList(id) { + const { ssbClient, idx } = await load(); + const me = ssbClient.id; + const list = find(idx, id); + if (!list) throw new Error('Mailing list not found'); + if (!list.closed) { + if (!subscriptionsModel) throw new Error('Subscriptions unavailable'); + return subscriptionsModel.setSubscription(list.id, 'mailing', false); + } + if (list.author === me) throw new Error('The author cannot leave the mailing list'); + if (!list.members.includes(me)) throw new Error('Not a member'); + return publishPrivateBatched(ssbClient, { type: LEAVE_TYPE, list: list.id, private: true, createdAt: new Date().toISOString() }, [list.author]); + }, + + async sendMessage(id, { subject, text, thread = '', replyTo = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const list = find(idx, id); + if (!list || !visibleTo(list, me)) throw new Error('Mailing list not found'); + if (list.status !== 'ACTIVE') throw new Error('This mailing list is archived'); + let { all, others } = await recipientsFor(ssbClient, list); + if (!all.includes(me) && !list.closed && subscriptionsModel) { + await subscriptionsModel.setSubscription(list.id, 'mailing', true); + ({ all, others } = await recipientsFor(ssbClient, list)); + if (!all.includes(me)) { all = uniq([...all, me]); others = all.filter(id => id !== me); } + } + if (!all.includes(me)) throw new Error('Only participants can write to this mailing list'); + const body = safeText(text); + if (!body) throw new Error('Text is required'); + const mid = crypto.randomBytes(12).toString('hex'); + let threadId = safeText(thread); + let subj = safeText(subject); + if (!threadId && replyTo) { + const parent = historyFor(priv, list.id, me).find(h => h.key === replyTo || h.mid === replyTo); + if (parent) { threadId = parent.thread; if (!subj) subj = `RE: ${canonicalSubject(parent.subject)}`; } + } + const content = { + type: 'post', + from: me, + to: [], + subject: subj || list.title, + text: body, + sentAt: new Date().toISOString(), + private: true, + list: list.id, + mid, + thread: threadId || mid + }; + const sendBatch = async (recps) => { + const sealed = await longText.chunkContent({ ...content, to: recps }, 'text', { maxBytes: longText.CONTENT_BYTES.boxed, extra: { private: true }, publish: (c) => publishPrivate(ssbClient, c, recps) }); + return publishPrivate(ssbClient, sealed, recps); + }; + const out = []; + if (!others.length) out.push(await sendBatch([me])); + for (let i = 0; i < others.length; i += BATCH) { + out.push(await sendBatch(uniq([me, ...others.slice(i, i + BATCH)]))); + } + return { key: out[0].key, mid, thread: content.thread, batches: out.length }; + }, + + async getListById(id) { + const { ssbClient, idx, priv } = await load(); + const list = find(idx, id); + if (!list || !visibleTo(list, ssbClient.id)) return null; + return decorate(ssbClient, list, priv, list.closed ? null : await subscribersMap([list.id])); + }, + + async listAll({ filter = 'all', q = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + const visible = Array.from(idx.lists.values()).filter(l => visibleTo(l, me)); + const subs = await subscribersMap(visible.filter(l => !l.closed).map(l => l.id)); + let out = []; + for (const list of visible) out.push(await decorate(ssbClient, list, priv, subs)); + if (f === 'mine') out = out.filter(l => l.author === me); + else if (f === 'subscribed') out = out.filter(l => l.isMember && l.author !== me); + else if (f === 'recent') out = out.filter(l => l.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'open') out = out.filter(l => !l.closed); + else if (f === 'closed') out = out.filter(l => l.closed); + else if (f === 'active') out = out.filter(l => l.status === 'ACTIVE'); + else if (f === 'archived') out = out.filter(l => l.status === 'ARCHIVED'); + if (needle) out = out.filter(l => [l.title, l.description, ...l.tags].some(v => String(v || '').toLowerCase().includes(needle))); + out.sort((a, b) => b.lastActivityTs - a.lastActivityTs); + return out; + }, + + async composerEntries() { + const all = await this.listAll({ filter: 'all' }); + return all.filter(l => l.isMember && l.status === 'ACTIVE').map(l => ({ target: l.id, scope: 'mailing', title: l.title, owner: l.author, count: l.participantCount })); + }, + + async titlesFor(ids) { + const out = new Map(); + const wanted = new Set((ids || []).map(String)); + if (!wanted.size) return out; + const { ssbClient, idx } = await load(); + for (const list of idx.lists.values()) { + if (wanted.has(list.id) && visibleTo(list, ssbClient.id)) out.set(list.id, { title: list.title, status: list.status, closed: list.closed }); + } + return out; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const list = find(idx, id); + return list ? list.id : String(id || ''); + } + }; +}; diff --git a/src/models/main_models.js b/src/models/main_models.js index d53de86..477692c 100644 --- a/src/models/main_models.js +++ b/src/models/main_models.js @@ -1,6 +1,8 @@ "use strict"; const { buildValidatedTombstoneSet } = require("./tombstone_validator"); +const { readTyped, requestScope, authorActivity } = require("./typed_log"); +const longText = require("../backend/long_text"); const debug = require("../server/node_modules/debug")("oasis"); const { isRoot, isReply: isComment } = require("../server/node_modules/ssb-thread-schema"); const lodash = require("../server/node_modules/lodash"); @@ -12,13 +14,16 @@ const pullSort = require("../server/node_modules/pull-sort"); const path = require('path'); const fs = require('fs/promises'); -const os = require('os'); const ssbRef = require("../server/node_modules/ssb-ref"); const nameCache = require('../backend/nameCache'); +const sharedState = require('../configs/shared-state'); const { getConfig } = require('../configs/config-manager.js'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const LIFETIME_CONCURRENCY = 32; +const LIFETIME_TTL_MS = 60000; +const LIFETIME_CACHE_MAX = 20000; const isEncrypted = (message) => typeof message.value.content === "string"; const isNotEncrypted = (message) => isEncrypted(message) === false; @@ -65,9 +70,10 @@ const publicOnlyFilter = pull.filter(isNotPrivate); const configure = (...customOptions) => Object.assign({}, defaultOptions, ...customOptions); -// PEERS -const ebtDir = path.join(os.homedir(), '.ssb', 'ebt'); -const unfollowedPath = path.join(os.homedir(), '.ssb', 'gossip_unfollowed.json'); +const ssbConfig = require('../server/ssb_config'); +const peerHealth = require('./peer_health'); +const ebtDir = path.join(ssbConfig.path, 'ebt'); +const unfollowedPath = require('../configs/state-manager').statePath('gossip_unfollowed.json'); async function loadPeersFromEbt() { let result = []; @@ -83,13 +89,15 @@ async function loadPeersFromEbt() { try { const data = await fs.readFile(filePath, 'utf8'); const users = JSON.parse(data); + const touchedAt = (await fs.stat(filePath)).mtimeMs; const userList = Object.keys(users).map(u => ({ id: u, link: `/author/${encodeURIComponent(u)}` })); result.push({ pub: `@${core}.ed25519`, - users: userList + users: userList, + touchedAt }); } catch {} } @@ -119,7 +127,7 @@ const canonicalizePubId = (s) => { }; const parseRemote = (remote) => { - let m = /^net:([^:]+):\d+~shs:([^=]+)=/.exec(remote); + let m = /^(?:net|onion):([^:]+):\d+~shs:([^=]+)=/.exec(remote); if (m) return { host: m[1], pubId: canonicalizePubId(m[2]) }; m = /^wss?:\/\/([^:/]+)(?::\d+)?.*~shs:([^=]+)=/.exec(remote); if (m) return { host: m[1], pubId: canonicalizePubId(m[2]) }; @@ -160,10 +168,9 @@ function toLegacyInvite(s) { return `${m[1]}:${m[2]}:@${key}~${m[4]}`; } -// CORE MODEL module.exports = ({ cooler, isPublic }) => { const models = {}; - const getAbout = async ({ key, feedId }) => { + const readAboutOf = async (feedId) => { const ssb = await cooler.open(); const source = ssb.backlinks.read({ reverse: true, @@ -182,23 +189,31 @@ module.exports = ({ cooler, isPublic }) => { return new Promise((resolve, reject) => pull( source, - pull.find( - (message) => message.value.content[key] !== undefined, - (err, message) => { - if (err) { - reject(err); - } else { - if (message === null) { - resolve(null); - } else { - resolve(message.value.content[key]); - } - } + pull.collect((err, messages) => { + if (err) return reject(err); + const out = {}; + for (const message of messages || []) { + const content = (message && message.value && message.value.content) || {}; + for (const k of Object.keys(content)) if (out[k] === undefined && content[k] !== undefined) out[k] = content[k]; } - ) + resolve(out); + }) ) ); }; + const getAbout = async ({ key, feedId }) => { + const store = requestScope.getStore(); + let pending; + if (store) { + const memo = store.aboutOf || (store.aboutOf = new Map()); + if (!memo.has(feedId)) memo.set(feedId, readAboutOf(feedId)); + pending = memo.get(feedId); + } else { + pending = readAboutOf(feedId); + } + const all = await pending; + return all[key] === undefined ? null : all[key]; + }; const feeds_to_name = {}; let all_the_names = {}; let dirty = false; @@ -207,57 +222,48 @@ module.exports = ({ cooler, isPublic }) => { if (!dirty) return; if (running) return; running = true; - - all_the_names = {}; + dirty = false; const allFeeds = Object.keys(feeds_to_name); + sharedState.setSyncedPeerCount(allFeeds.length); console.log(`- Synced-peers: [ ${allFeeds.length} ]`); console.time("- Sync-time"); - const lookups = []; - for (const feed of allFeeds) { - const e = feeds_to_name[feed]; - let pair = { feed, name: e.name }; - lookups.push(enhanceFeedInfo(pair)); - } - Promise.all(lookups) - .then(() => { - dirty = false; + buildNameTable(allFeeds) + .then((table) => { + all_the_names = table; running = false; console.timeEnd("- Sync-time"); }) .catch((err) => { + dirty = true; running = false; console.warn("- Lookup Sync failed: ", err); }); }; - const enhanceFeedInfo = ({ feed, name }) => { - return new Promise((resolve, reject) => { - getAbout({ feedId: feed, key: "image" }) - .then((img) => { - if ( - img !== null && - typeof img !== "string" && - typeof img === "object" && - typeof img.link === "string" - ) { - img = img.link; - } else if (img === null) { - img = nullImage; - } - - models.friend - .getRelationship(feed) - .then((rel) => { - let feeds_named = all_the_names[name] || []; - feeds_named.push({ feed, name, rel, img }); - all_the_names[name.toLowerCase()] = feeds_named; - resolve(); - }) - .catch(reject); - }) - .catch(reject); - }); + const buildNameTable = async (allFeeds) => { + const ssb = await cooler.open(); + const [profiles, graph] = await Promise.all([ + models.about.profiles(), + new Promise((resolve) => ssb.friends.graph((err, g) => resolve(err || !g ? {} : g))) + ]); + const me = ssb.id; + const mine = graph[me] || {}; + const table = {}; + for (const feed of allFeeds) { + const e = feeds_to_name[feed]; + if (!e || typeof e.name !== "string") continue; + const p = profiles.get(feed) || {}; + const img = p.image && typeof p.image === "object" && typeof p.image.link === "string" + ? p.image.link + : (typeof p.image === "string" ? p.image : nullImage); + const rel = feed === me + ? { me: true, following: false, blocking: false, followsMe: false } + : { me: false, following: mine[feed] >= 0, blocking: Math.round(mine[feed]) === -1, followsMe: !!(graph[feed] && graph[feed][me] >= 0) }; + const k = e.name.toLowerCase(); + (table[k] || (table[k] = [])).push({ feed, name: e.name, rel, img }); + } + return table; }; async function enrichEntries(entries) { @@ -269,7 +275,8 @@ module.exports = ({ cooler, isPublic }) => { const { host, pubId } = parseRemote(remote); const effectiveKey = pubId || (data && data.key ? canonicalizePubId(data.key) : null); const name = host || (effectiveKey ? await models.about.name(effectiveKey).catch(() => (effectiveKey || '').slice(0, 10)) : remote); - const users = effectiveKey && ebtMap.has(effectiveKey) ? ebtMap.get(effectiveKey) : []; + const known = !!(effectiveKey && ebtMap.has(effectiveKey)); + const users = known ? ebtMap.get(effectiveKey) : []; const usersWithNames = await Promise.all( users.map(async (user) => { const userName = await models.about.name(user.id).catch(() => user.id); @@ -282,28 +289,29 @@ module.exports = ({ cooler, isPublic }) => { ...data, key: effectiveKey || remote, name, - users: usersWithNames + users: known ? usersWithNames : null } ]; }) ); }; -// ABOUT MODEL models.about = { publicWebHosting: async (feedId) => { const result = await getAbout({ key: "publicWebHosting", feedId, }); - return result === true; + if (result === true) return true; + const prefs = await getAbout({ key: "visibilityPrefs", feedId }).catch(() => null); + return !!(prefs && typeof prefs === 'object' && prefs.clearnet === true); }, deviceSource: async (feedId) => { const result = await getAbout({ key: "deviceSource", feedId }); return typeof result === 'string' && result.trim() ? result : null; }, - visibilityPrefs: async (feedId) => { - const result = await getAbout({ key: "visibilityPrefs", feedId }); + visibilityPrefs: async (feedId) => models.about.visibilityFrom(await getAbout({ key: "visibilityPrefs", feedId })), + visibilityFrom: (result) => { if (!result || typeof result !== 'object') return null; return { activity: result.activity === true, @@ -314,6 +322,9 @@ models.about = { ecoTax: result.ecoTax !== false, larpSign: result.larpSign === true, gpg: result.gpg !== false, + phone: result.phone === 'mutuals' || result.phone === 'off' ? result.phone : 'whole', + phoneDnd: result.phoneDnd === true, + clearnetSince: typeof result.clearnetSince === 'string' ? result.clearnetSince : '', clearnet: result.clearnet === true, fediverse: result.fediverse === true, fediverseHandle: typeof result.fediverseHandle === 'string' ? result.fediverseHandle : '', @@ -327,7 +338,13 @@ models.about = { clearnetImages: result.clearnetImages === true, clearnetDocuments: result.clearnetDocuments === true, clearnetTorrents: result.clearnetTorrents === true, + clearnetFiles: result.clearnetFiles === true, clearnetBookmarks: result.clearnetBookmarks === true, + clearnetPodcasts: result.clearnetPodcasts === true, + clearnetSchool: result.clearnetSchool === true, + clearnetMarket: result.clearnetMarket === true, + clearnetFeed: result.clearnetFeed === true, + clearnetWiki: result.clearnetWiki === true, profileShops: result.profileShops === true, profileJobs: result.profileJobs === true, profileEvents: result.profileEvents === true, @@ -338,9 +355,41 @@ models.about = { profileImages: result.profileImages === true, profileDocuments: result.profileDocuments === true, profileTorrents: result.profileTorrents === true, - profileBookmarks: result.profileBookmarks === true + profileFiles: result.profileFiles === true, + profileBookmarks: result.profileBookmarks === true, + profilePodcasts: result.profilePodcasts === true, + profileSchool: result.profileSchool === true, + profileMarket: result.profileMarket === true, + profileFeed: result.profileFeed === true, + profileWiki: result.profileWiki === true }; }, + profiles: async () => { + const store = requestScope.getStore(); + if (store && store.aboutProfiles) return store.aboutProfiles; + const pending = (async () => { + const ssb = await cooler.open(); + const out = new Map(); + await new Promise((resolve) => { + pull( + ssb.messagesByType({ type: "about", reverse: true }), + pull.drain((m) => { + const v = (m && m.value) || {}; + const c = v.content || {}; + if (!v.author || c.about !== v.author) return; + const cur = out.get(v.author) || {}; + for (const k of ["name", "description", "image", "visibilityPrefs", "gpgFingerprint", "deviceSource"]) if (cur[k] === undefined && c[k] !== undefined) cur[k] = c[k]; + out.set(v.author, cur); + }, () => resolve()) + ); + }); + const now = Date.now(); + for (const [id, p] of out) if (p.name) nameCache.set(id, p.name, now); + return out; + })(); + if (store) store.aboutProfiles = pending; + return pending; + }, name: async (feedId) => { if (isPublic && (await models.about.publicWebHosting(feedId)) === false) { return "Redacted"; @@ -479,16 +528,14 @@ models.about = { }, }; -// BLOBS MODEL function blobIdToHexPath(blobId) { - const homeDir = os.homedir(); const m = /^&([A-Za-z0-9+/=]+)\.sha256$/.exec(blobId); if (!m) throw new Error('Invalid blobId: ' + blobId); const b64 = m[1]; const buf = Buffer.from(b64, 'base64'); const hex = buf.toString('hex'); const prefix = hex.slice(0, 2); - return path.join(homeDir, '.ssb', 'blobs', 'sha256', prefix, hex); + return path.join(ssbConfig.path, 'blobs', 'sha256', prefix, hex); } async function checkLocalBlob(blobId) { @@ -496,21 +543,57 @@ async function checkLocalBlob(blobId) { try { const buf = await fs.readFile(filePath); if (buf && buf.length) return buf; - } catch (_) { /* not found */ } + } catch (_) {} return null; } models.blob = { + getLocal: async ({ blobId }) => { + const local = await checkLocalBlob(blobId); + if (local) return local; + const ssb = await cooler.open(); + const has = await new Promise((resolve) => ssb.blobs.has(blobId, (err, v) => resolve(!err && !!v))); + if (!has) return null; + return new Promise((resolve) => { + pull(ssb.blobs.get(blobId), pull.collect((err, bufs) => resolve(err || !bufs || !bufs.length ? null : Buffer.concat(bufs)))); + }); + }, + getCached: async ({ blobId }) => { + const local = await checkLocalBlob(blobId); + if (local) return local; + const ssb = await cooler.open(); + const has = await new Promise((resolve) => ssb.blobs.has(blobId, (err, v) => resolve(!err && !!v))); + if (!has) { + try { ssb.blobs.want(blobId, () => {}); } catch (_) {} + return null; + } + return new Promise((resolve) => { + pull( + ssb.blobs.get(blobId), + pull.collect(async (err, bufs) => { + if (err || !bufs || !bufs.length) return resolve(null); + const buffer = Buffer.concat(bufs); + try { + const filePath = blobIdToHexPath(blobId); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, buffer); + } catch (e) {} + resolve(buffer); + }) + ); + }); + }, getResolved: async ({ blobId, timeout = 30000 }) => { let buf = await checkLocalBlob(blobId); if (buf) return buf; const ssb = await cooler.open(); - await new Promise((resolve, reject) => { - ssb.blobs.want(blobId, (err) => { - if (err) reject(err); - else resolve(); - }); + const arrived = await new Promise((resolve) => { + const wait = setTimeout(() => resolve(false), timeout); + try { + ssb.blobs.want(blobId, (err) => { clearTimeout(wait); resolve(!err); }); + } catch (_) { clearTimeout(wait); resolve(false); } }); + if (!arrived) return null; return new Promise((resolve, reject) => { let timer = setTimeout(() => resolve(null), timeout); pull( @@ -523,7 +606,7 @@ models.blob = { const filePath = blobIdToHexPath(blobId); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, buffer); - } catch (e) { /* ignore */ } + } catch (e) {} resolve(buffer); }) ); @@ -540,8 +623,13 @@ models.blob = { } }; -// FRIENDS MODEL models.friend = { + blockedIds: async () => { + const ssb = await cooler.open(); + const graph = await new Promise((resolve) => ssb.friends.graph((err, g) => resolve(err || !g ? {} : g))); + const mine = graph[ssb.id] || {}; + return new Set(Object.keys(mine).filter(feed => Math.round(mine[feed]) === -1)); + }, setRelationship: async ({ feedId, following, blocking }) => { if (following && blocking) { throw new Error("Cannot follow and block at the same time"); @@ -630,7 +718,6 @@ models.friend = { }, }; -// META MODEL models.meta = { myFeedId: async () => { const ssb = await cooler.open(); @@ -729,7 +816,7 @@ models.meta = { let addr = p.address; if (!addr && p.host && p.port) { const core = String(p.key).replace(/^@/, "").replace(/\.ed25519$/, ""); - addr = `net:${p.host}:${p.port}~shs:${core}`; + addr = `${/\.onion$/i.test(String(p.host)) ? 'onion' : 'net'}:${p.host}:${p.port}~shs:${core}`; } if (!addr) continue; seen.add(key); @@ -764,7 +851,7 @@ models.meta = { discovered: async () => { const ssb = await cooler.open(); const snapshot = await ssb.conn.dbPeers(); - const gossipPath = path.join(os.homedir(), '.ssb', 'gossip.json'); + const gossipPath = path.join(ssbConfig.path, 'gossip.json'); let gossipMap = new Map(); try { const gossipData = JSON.parse(await fs.readFile(gossipPath, 'utf8')); @@ -805,7 +892,7 @@ models.meta = { if (dbKeys.has(ck)) continue; let host = data.host, port = data.port; if ((!host || !port) && addr) { - const m = String(addr).match(/^net:([^:]+):(\d+)/); + const m = String(addr).match(/^(?:net|onion):([^:]+):(\d+)/); if (m) { host = host || m[1]; port = port || Number(m[2]); } } mergedSnapshot.push([addr, { key: data.key, host, port, source: data.type || 'staged', verified: data.verified }]); @@ -824,12 +911,19 @@ models.meta = { .map(([, d]) => d && d.key ? canonicalizePubId(d.key) : null) .filter(Boolean) ); - const discoveredPeers = allDbPeers.filter(([, d]) => !onlineKeys.has(canonicalizePubId(d.key))); + const isDeadPeer = (d) => { + const ck = canonicalizePubId(d.key); + const legacy = gossipMap.get(ck) || {}; + return peerHealth.isDead({ key: ck, failures: Math.max(Number(d.failure) || 0, Number(legacy.failure) || 0) }); + }; + const forgotten = await loadUnfollowedSet(); + const discoveredPeers = allDbPeers.filter(([, d]) => !onlineKeys.has(canonicalizePubId(d.key)) && !isDeadPeer(d) && !forgotten.has(canonicalizePubId(d.key))); const discoveredIds = new Set(allDbPeers.map(([, d]) => canonicalizePubId(d.key))); const ebtList = await loadPeersFromEbt(); const ebtMap = new Map(ebtList.map(e => [e.pub, e.users])); const unknownPeers = []; - for (const { pub } of ebtList) { + for (const { pub, touchedAt } of ebtList) { + if (Date.now() - (Number(touchedAt) || 0) >= peerHealth.DEAD_AFTER_MS || forgotten.has(pub)) continue; if (!discoveredIds.has(pub) && !onlineKeys.has(pub)) { const name = await models.about.name(pub).catch(() => pub); unknownPeers.push([pub, { key: pub, name, users: ebtMap.get(pub) || [] }]); @@ -837,68 +931,6 @@ models.meta = { } return { discoveredPeers, unknownPeers }; }, - connStop: async () => { - const ssb = await cooler.open(); - try { - const result = await ssb.conn.stop(); - return result; - } catch (e) { - const expectedName = "TypeError"; - const expectedMessage = "Cannot read property 'close' of null"; - if (e.name === expectedName && e.message === expectedMessage) { - debug("ssbConn is already stopped -- caught error"); - } else { - throw new Error(e); - } - } - }, - connStart: async () => { - const ssb = await cooler.open(); - const result = await ssb.conn.start(); - - return result; - }, - connRestart: async () => { - await models.meta.connStop(); - await models.meta.connStart(); - }, - sync: async () => { - const ssb = await cooler.open(); - - const progress = await ssb.progress(); - let previousTarget = progress.indexes.target; - - let keepGoing = true; - const timeoutInterval = setTimeout(() => { - keepGoing = false; - }, 5 * 60 * 1000); - - await ssb.conn.start(); - - const diff = async () => - new Promise((resolve) => { - setTimeout(async () => { - const currentProgress = await ssb.progress(); - const currentTarget = currentProgress.indexes.target; - const difference = currentTarget - previousTarget; - previousTarget = currentTarget; - debug(`Difference: ${difference} bytes`); - resolve(difference); - }, 5000); - }); - - debug("Starting sync, waiting for new messages..."); - while (keepGoing && (await diff()) === 0) { - debug("Received no new messages."); - } - debug("Finished waiting for first new message."); - while (keepGoing && (await diff()) > 0) { - debug(`Still receiving new messages...`); - } - debug("Finished waiting for last new message."); - clearInterval(timeoutInterval); - await ssb.conn.stop(); - }, acceptInvite: async (invite) => { const ssb = await cooler.open(); const code = toLegacyInvite(String(invite || '')); @@ -934,7 +966,7 @@ models.meta = { return conditions.every((x) => x === true); }; - const maxMessages = 30; // change it to control post overloading + const maxMessages = 30; const getMessages = async ({ myFeedId, @@ -943,23 +975,8 @@ models.meta = { query, filter = null, }) => { - const source = ssb.createLogStream({ reverse: true, limit: logLimit }); - - return new Promise((resolve, reject) => { - pull( - source, - pull.filter((msg) => { - return msg.value.content.type === "post"; - }), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(collectedMessages); - } - }) - ); - }); + const collected = await readTyped(ssb, ["post"], { limit: logLimit }); + return collected.filter((msg) => msg.value && msg.value.content && msg.value.content.type === "post").reverse(); }; const socialFilter = async ({ @@ -1185,7 +1202,6 @@ models.meta = { return messages.length ? messages[0] : undefined; }; -// POST MODEL const post = { firstBy: async (feedId) => { return getLimitPost(feedId, false); @@ -1269,25 +1285,6 @@ const post = { return { messages, myFeedId }; }, - fromHashtag: async (hashtag, customOptions = {}) => { - const ssb = await cooler.open(); - const myFeedId = ssb.id; - const query = [ - { - $filter: { - dest: `#${hashtag}`, - }, - }, - ]; - const messages = await getMessages({ - myFeedId, - customOptions, - ssb, - query, - }); - - return messages; - }, topicComments: async (rootId, customOptions = {}) => { const ssb = await cooler.open(); const myFeedId = ssb.id; @@ -1322,7 +1319,16 @@ const post = { return msg; }) ); - return fullMessages; + const topic = fullMessages.filter((m) => m && m.value && m.value.content && m.value.content.type === "post" && m.value.content.root === rootId); + const tombstones = topic.length ? await readTyped(ssb, ["tombstone"], { limit: 0 }).catch(() => []) : []; + const gone = buildValidatedTombstoneSet([...topic.filter(m => m && m.key && m.value), ...tombstones]); + const chunkIdx = longText.indexChunks(await readTyped(ssb, [longText.CHUNK_TYPE], { limit: logLimit })); + const lookup = longText.lookupIn(chunkIdx); + return topic + .filter(m => !(m && gone.has(m.key))) + .map(m => m && m.value && longText.hasChunks(m.value.content) + ? { ...m, value: { ...m.value, content: longText.resolveField(m.value.content, "text", m.value.author, lookup) } } + : m); }, likes: async ({ feed }, customOptions = {}) => { const ssb = await cooler.open(); @@ -1400,356 +1406,6 @@ const post = { ); }); - return messages; - }, - latest: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $lte: Date.now() }, - content: { - type: { $in: ["post", "blog"] }, - }, - }, - }, - }, - ], - }) - ); - const followingFilter = await socialFilter({ following: true }); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - followingFilter, - publicOnlyFilter, - pull.take(maxMessages), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - latestExtended: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $lte: Date.now() }, - content: { - type: { $in: ["post", "blog"] }, - }, - }, - }, - }, - ], - }) - ); - - const extendedFilter = await socialFilter({ - following: false, - me: false, - }); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - publicOnlyFilter, - extendedFilter, - pull.take(maxMessages), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - latestTopics: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $lte: Date.now() }, - content: { - type: { $in: ["post", "blog"] }, - }, - }, - }, - }, - ], - }) - ); - - const extendedFilter = await socialFilter({ - following: true, - }); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - publicOnlyFilter, - pull.filter(hasNoRoot), - extendedFilter, - pull.take(maxMessages), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - latestSummaries: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const options = configure({ - type: "post", - private: false, - }); - - const source = ssb.messagesByType(options); - - const extendedFilter = await socialFilter({ - following: true, - }); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - pull.filter((message) => isNotPrivate(message) && hasNoRoot(message)), - extendedFilter, - pull.take(maxMessages), - pullParallelMap(async (message, cb) => { - const thread = await post.fromThread(message.key); - lodash.set( - message, - "value.meta.thread", - await transform(ssb, thread, myFeedId) - ); - cb(null, message); - }), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - latestThreads: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $lte: Date.now() }, - content: { - type: { $in: ["post", "blog"] }, - }, - }, - }, - }, - ], - }) - ); - const basicSocialFilter = await socialFilter(); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - basicSocialFilter, - pull.filter((message) => isNotPrivate(message) && hasNoRoot(message)), - pull.take(maxMessages), - pullParallelMap(async (message, cb) => { - const thread = await post.fromThread(message.key); - lodash.set( - message, - "value.meta.thread", - await transform(ssb, thread, myFeedId) - ); - cb(null, message); - }), - pull.filter((message) => message.value.meta.thread.length > 1), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - - popular: async ({ period }) => { - const ssb = await cooler.open(); - - const periodDict = { - day: 1, - week: 7, - month: 30.42, - year: 365, - }; - - if (period in periodDict === false) { - throw new Error("invalid period"); - } - - const myFeedId = ssb.id; - - const now = new Date(); - const earliest = Number(now) - 1000 * 60 * 60 * 24 * periodDict[period]; - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $gte: earliest }, - content: { - type: "vote", - }, - }, - }, - }, - ], - }) - ); - const basicSocialFilter = await socialFilter(); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - publicOnlyFilter, - pull.filter((msg) => { - return ( - isNotEncrypted(msg) && - typeof msg.value.content.vote === "object" && - typeof msg.value.content.vote.link === "string" && - typeof msg.value.content.vote.value === "number" - ); - }), - pull.reduce( - (acc, cur) => { - const author = cur.value.author; - const target = cur.value.content.vote.link; - const value = cur.value.content.vote.value; - - if (acc[author] == null) { - acc[author] = {}; - } - acc[author][target] = Math.max(-1, Math.min(1, value)); - - return acc; - }, - {}, - (err, obj) => { - if (err) { - return reject(err); - } - const adjustedObj = Object.entries(obj).reduce( - (acc, [author, values]) => { - if (author === myFeedId) { - return acc; - } - const entries = Object.entries(values); - const total = 1 + Math.log(entries.length); - - entries.forEach(([link, value]) => { - if (acc[link] == null) { - acc[link] = 0; - } - acc[link] += value / total; - }); - return acc; - }, - [] - ); - - const arr = Object.entries(adjustedObj); - const length = arr.length; - - pull( - pull.values(arr), - pullSort(([, aVal], [, bVal]) => bVal - aVal), - pull.take(Math.min(length, maxMessages)), - pull.map(([key]) => key), - pullParallelMap(async (key, cb) => { - try { - const msg = await post.get(key); - cb(null, msg); - } catch (e) { - cb(null, null); - } - }), - pull.filter( - (message) => - message && - isNotPrivate(message) && - (message.value.content.type === "post" || - message.value.content.type === "blog") - ), - basicSocialFilter, - pull.collect((collectErr, collectedMessages) => { - if (collectErr) { - reject(collectErr); - } else { - resolve(collectedMessages); - } - }) - ); - } - ) - ); - }); - return messages; }, fromThread: async (msgId, customOptions) => { @@ -1884,19 +1540,21 @@ const post = { }, publish: async (options) => { const ssb = await cooler.open(); - const body = { type: "post", ...options }; - return new Promise((resolve, reject) => { - ssb.publish(body, (err, msg) => { + const publish = (content) => new Promise((resolve, reject) => { + ssb.publish(content, (err, msg) => { if (err) reject(err); else resolve(msg); }); }); + const body = { type: "post", ...options }; + const content = typeof body.text === "string" && !body.recps ? await longText.chunkContent(body, "text", { publish }) : body; + return publish(content); }, publishFediverseHandle: async (handle) => { const ssb = await cooler.open(); const current = (await models.about.visibilityPrefs(ssb.id).catch(() => null)) || {}; const h = String(handle || ''); - const prefs = { ...current, fediverseHandle: h }; + const prefs = { ...current, fediverseHandle: current.fediverse === true ? h : '' }; if (h === '') prefs.fediverse = false; return new Promise((resolve, reject) => { ssb.publish({ type: "about", about: ssb.id, visibilityPrefs: prefs }, (err, msg) => err ? reject(err) : resolve(msg)); @@ -1915,6 +1573,9 @@ const post = { ecoTax: r.ecoTax !== false, larpSign: r.larpSign === true, gpg: r.gpg !== false, + phone: r.phone === 'mutuals' || r.phone === 'off' ? r.phone : 'whole', + phoneDnd: !!(getConfig().phone || {}).dnd, + ...(typeof r.clearnetSince === 'string' && r.clearnetSince ? { clearnetSince: r.clearnetSince } : {}), clearnet: r.clearnet === true, fediverse: r.fediverse === true, fediverseHandle: typeof r.fediverseHandle === 'string' ? r.fediverseHandle : '', @@ -1928,7 +1589,13 @@ const post = { clearnetImages: r.clearnetImages === true, clearnetDocuments: r.clearnetDocuments === true, clearnetTorrents: r.clearnetTorrents === true, + clearnetFiles: r.clearnetFiles === true, clearnetBookmarks: r.clearnetBookmarks === true, + clearnetPodcasts: r.clearnetPodcasts === true, + clearnetSchool: r.clearnetSchool === true, + clearnetMarket: r.clearnetMarket === true, + clearnetFeed: r.clearnetFeed === true, + clearnetWiki: r.clearnetWiki === true, profileShops: r.profileShops === true, profileJobs: r.profileJobs === true, profileEvents: r.profileEvents === true, @@ -1939,7 +1606,13 @@ const post = { profileImages: r.profileImages === true, profileDocuments: r.profileDocuments === true, profileTorrents: r.profileTorrents === true, - profileBookmarks: r.profileBookmarks === true + profileFiles: r.profileFiles === true, + profileBookmarks: r.profileBookmarks === true, + profilePodcasts: r.profilePodcasts === true, + profileSchool: r.profileSchool === true, + profileMarket: r.profileMarket === true, + profileFeed: r.profileFeed === true, + profileWiki: r.profileWiki === true }; }; const prefs = visibilityPrefs ? normalizePrefs(visibilityPrefs) : undefined; @@ -1960,9 +1633,9 @@ const post = { if (resolvedBlobId !== undefined) baseFields.gpgBlobId = String(resolvedBlobId || ""); if (image && image.length > 0) { const megabyte = Math.pow(2, 20); - const maxSize = 50 * megabyte; + const maxSize = 75 * megabyte; if (image.length > maxSize) { - throw new Error("File is too big, maximum size is 50 megabytes"); + throw new Error("File is too big, maximum size is 75 megabytes"); } return new Promise((resolve, reject) => { pull( @@ -2115,32 +1788,34 @@ const post = { inbox: async () => { const ssb = await cooler.open(); const myFeedId = ssb.id; - const rawMessages = await new Promise((resolve, reject) => { - pull( - ssb.createLogStream({ reverse: true, limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); - const decryptedMessages = rawMessages.map(msg => { - try { - return ssb.private.unbox(msg); - } catch { - return null; + let decryptedMessages = []; + try { + if (ssb.private && typeof ssb.private.read === 'function') { + decryptedMessages = await new Promise((resolve, reject) => pull(ssb.private.read({ reverse: true, limit: logLimit * 10 }), pull.collect((err, arr) => err ? reject(err) : resolve(arr || [])))); } - }).filter(Boolean); + } catch (_) { decryptedMessages = []; } + if (!decryptedMessages.length) { + const rawMessages = await readTyped(ssb, [], { limit: logLimit, withWindow: true }); + decryptedMessages = rawMessages.map(msg => { + try { + return ssb.private.unbox(msg); + } catch { + return null; + } + }).filter(Boolean); + } const tombstoneTargets = buildValidatedTombstoneSet(decryptedMessages); return decryptedMessages.filter(msg => { if (tombstoneTargets.has(msg.key)) return false; const content = msg.value?.content; const author = msg.value?.author; return content?.type === 'post' && content?.private === true && (author === myFeedId || content.to?.includes(myFeedId)); - }); + }).reverse(); } }; models.post = post; -// SPREAD MODEL models.vote = { publish: async ({ messageKey, value, recps }) => { const ssb = await cooler.open(); @@ -2187,9 +1862,11 @@ models.lifetime = (() => { if (days < FRESH_ORANGE_DAYS) return { bucket: 'orange', range: '2w–6m' }; return { bucket: 'red', range: '≥6m' }; }; - const lastAuthorTs = async (feedId) => { + const lastAuthorTsRaw = async (feedId) => { if (!feedId) return null; const ssbClient = await cooler.open(); + const activity = await authorActivity(ssbClient); + if (activity) return activity.get(feedId) || null; return new Promise((resolve) => { try { pull( @@ -2205,7 +1882,7 @@ models.lifetime = (() => { } catch (_) { resolve(null); } }); }; - const lastBacklinkTs = async (msgKey) => { + const lastBacklinkTsRaw = async (msgKey) => { if (!msgKey) return null; const ssbClient = await cooler.open(); return new Promise((resolve) => { @@ -2221,7 +1898,23 @@ models.lifetime = (() => { } catch (_) { resolve(null); } }); }; + const recent = new Map(); + const remembered = (bucket, key, fn) => { + if (!key) return fn(key); + let memo = recent.get(bucket); + if (!memo) recent.set(bucket, memo = new Map()); + const hit = memo.get(key); + if (hit && Date.now() - hit.at < LIFETIME_TTL_MS) return hit.value; + const value = fn(key); + memo.delete(key); + memo.set(key, { at: Date.now(), value }); + while (memo.size > LIFETIME_CACHE_MAX) memo.delete(memo.keys().next().value); + return value; + }; + const lastAuthorTs = (feedId) => remembered('author', feedId, lastAuthorTsRaw); + const lastBacklinkTs = (msgKey) => remembered('backlink', msgKey, lastBacklinkTsRaw); return { + forget: () => recent.clear(), bucket: bucketOf, lastAuthorTs, lastBacklinkTs, @@ -2234,30 +1927,40 @@ models.lifetime = (() => { const candidates = [authorTs, interactionTs, createdTs].filter(x => typeof x === 'number' && x > 0); const lastTs = candidates.length ? Math.max(...candidates) : null; const { bucket, range } = bucketOf(lastTs); - return { bucket, range, lastTs, authorTs, interactionTs, createdTs }; + const own = [interactionTs, createdTs].filter(x => typeof x === 'number' && x > 0); + const content = bucketOf(own.length ? Math.max(...own) : null); + return { bucket, range, contentBucket: content.bucket, contentRange: content.range, lastTs, authorTs, interactionTs, createdTs }; }, async enrichAndFilter(items, opts = {}) { const { includeDead = false, getKey = (x) => x.id || x.key, getAuthor = (x) => x.author, getCreatedAt = (x) => x.createdAt } = opts; - const authorCache = new Map(); + const list = Array.from(items || []); + const authors = [...new Set(list.map(getAuthor).filter(Boolean))]; + const mapLimit = async (items, fn) => { + const out = new Array(items.length); + let next = 0; + await Promise.all(Array.from({ length: Math.min(LIFETIME_CONCURRENCY, items.length) }, async () => { + while (next < items.length) { const k = next++; out[k] = await fn(items[k]); } + })); + return out; + }; + const authorTsList = await mapLimit(authors, (a) => lastAuthorTs(a)); + const authorCache = new Map(authors.map((a, i) => [a, authorTsList[i]])); + const interactionList = await mapLimit(list, (item) => { const key = getKey(item); return key ? lastBacklinkTs(key) : null; }); const out = []; - for (const item of items) { + for (let i = 0; i < list.length; i++) { + const item = list[i]; const author = getAuthor(item); - let authorTs; - if (author && authorCache.has(author)) { - authorTs = authorCache.get(author); - } else { - authorTs = author ? await lastAuthorTs(author) : null; - if (author) authorCache.set(author, authorTs); - } - const key = getKey(item); - const interactionTs = key ? await lastBacklinkTs(key) : null; + const authorTs = author ? authorCache.get(author) : null; + const interactionTs = interactionList[i]; const createdAt = getCreatedAt(item); const createdTs = createdAt ? new Date(createdAt).getTime() : null; const candidates = [authorTs, interactionTs, createdTs].filter(x => typeof x === 'number' && x > 0); const lastTs = candidates.length ? Math.max(...candidates) : null; const { bucket, range } = bucketOf(lastTs); if (!includeDead && bucket === 'red') continue; - out.push({ ...item, lifetime: { bucket, range, lastTs, authorTs, interactionTs, createdTs } }); + const own = [interactionTs, createdTs].filter(x => typeof x === 'number' && x > 0); + const content = bucketOf(own.length ? Math.max(...own) : null); + out.push({ ...item, lifetime: { bucket, range, contentBucket: content.bucket, contentRange: content.range, lastTs, authorTs, interactionTs, createdTs } }); } return out; } diff --git a/src/models/maps_model.js b/src/models/maps_model.js index b1bd77c..0cae474 100644 --- a/src/models/maps_model.js +++ b/src/models/maps_model.js @@ -3,6 +3,7 @@ const crypto = require("crypto"); const { getConfig } = require("../configs/config-manager.js"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { collabContent, openInviteOf } = require('../backend/collab_content'); +const { readTyped, memoIndex } = require("./typed_log"); const mapCollab = collabContent({ membersField: 'members', undecField: 'encrypted', contentFields: ['title', 'description', 'image'], listFields: ['tags', 'invites', 'markers'] }); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -34,6 +35,16 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { return (tribeCrypto && tribeCrypto.getKeys(rid)) || []; }; const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0); + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex"); } catch (_) { return null; } }; + const hashCode = (code, salt) => (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null); + const inviteMatches = (inv, code) => { + if (!code) return false; + if (typeof inv === "string") return inv === code; + if (!inv || typeof inv !== "object") return false; + if (typeof inv.code === "string") return inv.code === code; + return typeof inv.ch === "string" && hashCode(code, inv.salt) === inv.ch; + }; + const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || ""); const rotateMapKey = async (rootId, remainingMembers) => { if (!ownCrypto || !tribeCrypto || !rootId) return; @@ -41,11 +52,10 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (!existing) return; const newKey = ownCrypto.generateTribeKey(); const newGen = ownCrypto.addNewKey(rootId, newKey); - if (!Array.isArray(remainingMembers) || !remainingMembers.length) return; const ssbClient = await openSsb(); const ssbKeys = require("../server/node_modules/ssb-keys"); const memberKeys = {}; - for (const m of remainingMembers) { + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} } if (Object.keys(memberKeys).length) { @@ -61,10 +71,13 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const ssbClient = await openSsb(); const ssbKeys = require("../server/node_modules/ssb-keys"); const config = require("../server/ssb_config"); - const msgs = await new Promise((resolve, reject) => pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? reject(e) : resolve(m)))); + const msgs = await getAllMessages(ssbClient); + const idx = buildIndex(unwrapForIndex(msgs)); + const trusted = (root, author) => { const n = idx.nodes.get(root); return !!author && !!n && (author === n.author || author === ssbClient.id); }; for (const m of msgs) { const c = m.value && m.value.content; if (!c || c.type !== "tribe-keys") continue; + if (!trusted(c.tribeId, m.value.author)) continue; const memberKeys = c.memberKeys; if (!memberKeys || typeof memberKeys !== "object") continue; const boxed = memberKeys[ssbClient.id]; @@ -148,13 +161,9 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const MAP_TYPES = ["map", "mapMarker", "mapMember", "tribe-msg", "tribe-keys", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, MAP_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve) => { @@ -185,7 +194,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } if (c.type === "mapMember" && c.target) { - memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "" }); + memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "", keyProof: typeof c.keyProof === "string" ? c.keyProof : "" }); continue; } @@ -207,7 +216,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (c.type !== "map") continue; const ts = v.timestamp || m.timestamp || 0; - nodes.set(k, { key: k, ts, c, author: v.author }); + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, author: v.author }); authorByKey.set(k, v.author); if (c.replaces) { @@ -235,21 +244,15 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { return cur; }; - const contentTipOf = (root) => { - const rn = nodes.get(root); - if (!rn) return root; - let cur = root, best = root, g = 0; - const seen = new Set(); - while (child.has(cur) && !seen.has(cur) && g++ < 100000) { - seen.add(cur); - const next = child.get(cur); - const n = nodes.get(next); - if (!n) break; - if (n.author === rn.author && !tomb.has(next)) best = next; - cur = next; - } - return best; - }; + const ownerTip = new Map(); + for (const [k, n] of nodes) { + const r = rootOf(k); + const rn = nodes.get(r); + if (!rn || n.author !== rn.author) continue; + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null; + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k); + } + const contentTipOf = (root) => ownerTip.get(root) || root; for (const t of tombRequests) { const targetAuthor = authorByKey.get(t.target); @@ -268,8 +271,21 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const forward = new Map(); for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); + const invitesOf = (root) => { + const out = []; + for (const id of [root, contentTipOf(root)]) { const n = nodes.get(id); if (n && Array.isArray(n.c.invites)) out.push(...n.c.invites); } + return out; + }; + const selfJoinAllowed = (root, mm) => { + const oc = (nodes.get(root) || {}).c || {}; + if (!oc.encryptedPayload || oc.tribeId) return true; + if (invitesOf(root).some(inv => inv && typeof inv === "object" && inv.public === true)) return true; + return !!mm.keyProof && lookupKeys(root).some(k => keyProofFor(k, mm.member) === mm.keyProof); + }; + const memberByRoot = new Map(); const consumedByRoot = new Map(); + const codeClaimsByRoot = new Map(); for (const mm of memberMsgs) { if (!nodes.has(mm.target)) continue; const r = rootOf(mm.target); @@ -279,6 +295,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const byOwner = mm.author === ownerAuthor; if (!self && !byOwner) continue; if (!mm.member) continue; + if (mm.on && mm.code) { + if (!codeClaimsByRoot.has(r)) codeClaimsByRoot.set(r, []); + codeClaimsByRoot.get(r).push({ code: mm.code, member: mm.member, keyProof: mm.keyProof, byOwner }); + } + if (self && !byOwner && mm.on && !selfJoinAllowed(r, mm)) continue; if (mm.on && mm.code) { if (!consumedByRoot.has(r)) consumedByRoot.set(r, new Set()); consumedByRoot.get(r).add(mm.code); @@ -289,12 +310,24 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (!prev || mm.ts >= prev.ts) m2.set(mm.member, { on: mm.on, ts: mm.ts }); } + const codeClaims = (root, codes) => (codeClaimsByRoot.get(root) || []).filter(c => codes.includes(c.code)); + const isCodeConsumed = (root, code) => { if (!code) return false; const set = consumedByRoot.get(root); return !!(set && set.has(code)); }; + for (const [k, n] of nodes) { + const r = rootOf(k); + const rn = nodes.get(r); + if (!rn || n.author === rn.author || !Array.isArray(n.c.members) || !selfJoinAllowed(r, { member: n.author })) continue; + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()); + const m2 = memberByRoot.get(r); + const prev = m2.get(n.author); + if (!prev || n.ts >= prev.ts) m2.set(n.author, { on: n.c.members.includes(n.author), ts: n.ts }); + } + const resolveMembers = (root) => { const ownerNode = nodes.get(root); const oc = ownerNode ? ownerNode.c : {}; @@ -305,29 +338,31 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { return [...set]; }; - const ensureMemberKeys = async (ssbClient, messages, items) => { - if (!tribeCrypto) return; - const distributed = new Map(); + return { tomb, nodes, parent, child, strictChild, rootOf, tipOf, contentTipOf, tipByRoot, contentTipByRoot, forward, markers, rawMarkers, resolveMembers, isCodeConsumed, codeClaims }; + }; + + const ensureMemberKeys = async (ssbClient, messages, items) => { + if (!tribeCrypto || !ownCrypto) return; + const leavesByRoot = new Map(); for (const m of messages) { const c = m.value && m.value.content; - if (!c || c.type !== "tribe-keys" || !c.tribeId) continue; - const mk = c.memberKeys; - if (!mk || typeof mk !== "object") continue; - if (!distributed.has(c.tribeId)) distributed.set(c.tribeId, new Set()); - for (const id of Object.keys(mk)) distributed.get(c.tribeId).add(id); + if (!c || c.type !== "mapMember" || c.on !== false || !c.target || !c.member) continue; + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []); + leavesByRoot.get(c.target).push({ member: c.member, author: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }); } const ssbKeys = require("../server/node_modules/ssb-keys"); for (const item of (Array.isArray(items) ? items : [])) { - if (!item || item.encrypted) continue; + if (!item || item.encrypted || item.author !== ssbClient.id) continue; const rootId = item.rootId; - if (!rootId) continue; + if (!rootId || !lookupKey(rootId)) continue; + const members = Array.isArray(item.members) ? item.members : []; + const leaves = (leavesByRoot.get(rootId) || []).filter(l => l.author === l.member || l.author === item.author); + const plan = ownCrypto.keyPlan({ rootId, ownerId: item.author, gen: lookupGen(rootId), members, messages, leaves }); + if (plan.rotate) { await rotateMapKey(rootId, members); continue; } + if (!plan.missing.length) continue; const key = lookupKey(rootId); - if (!key) continue; - const have = distributed.get(rootId) || new Set(); - const missing = (Array.isArray(item.members) ? item.members : []).filter(m => m && m !== ssbClient.id && !have.has(m)); - if (!missing.length) continue; const memberKeys = {}; - for (const m of missing) { + for (const m of plan.missing) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue; @@ -337,13 +372,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } } - return { tomb, nodes, parent, child, strictChild, rootOf, tipOf, contentTipOf, tipByRoot, contentTipByRoot, forward, markers, rawMarkers, resolveMembers, isCodeConsumed }; - }; - const expandMarkers = async (idx) => { for (const [mapId, raws] of idx.rawMarkers.entries()) { const list = []; for (const r of raws) { + if (idx.tomb.has(r.key)) continue; let c = r.c; if (c.encryptedPayload && tribeCrypto && tribesModel) { const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); @@ -364,6 +397,15 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } }; + const markersFor = (idx, tipId, rootId) => { + const seen = new Set(); + return safeArr(idx.markers.get(tipId)).concat(safeArr(idx.markers.get(rootId))).filter((mk) => { + if (seen.has(mk.key)) return false; + seen.add(mk.key); + return true; + }); + }; + const buildMap = (node, rootId, viewerId, markerList = [], members = null) => { const c = node.c || {}; @@ -384,6 +426,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { invites: Array.isArray(c.invites) ? c.invites : [], tribeId: c.tribeId || null, encrypted: !!undec, + contentEncrypted: !!(c.encryptedPayload || c._decrypted === true), createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, markers: markerList.filter((mk) => !mk.tombstoned) @@ -396,7 +439,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (idx.tomb.has(tipId)) continue; const node = idx.nodes.get(tipId); if (!node) continue; - const markerList = safeArr(idx.markers.get(tipId)).concat(safeArr(idx.markers.get(rootId))); + const markerList = markersFor(idx, tipId, rootId); items.push(buildMap(node, rootId, viewerId, markerList, idx.resolveMembers(rootId))); } return items; @@ -411,18 +454,12 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (!ownCrypto || typeof ownCrypto.getAllRootIds !== "function") return 0; try { const ssbClient = await openSsb(); - const messages = await new Promise((resolve, reject) => pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? reject(e) : resolve(m)))); - const live = new Set(); + const messages = await getAllMessages(ssbClient); const tomb = buildValidatedTombstoneSet(messages); - for (const m of messages) { - const c = m.value && m.value.content; - if (!c) continue; - if (c.type === "map") live.add(m.key); - } const all = ownCrypto.getAllRootIds(); let removed = 0; for (const rid of all) { - if (!live.has(rid) || tomb.has(rid)) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1; } catch (_) {} } } @@ -437,11 +474,9 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (!map) throw new Error("Map not found"); if (map.author === userId) throw new Error("Author cannot leave their own map"); if (!Array.isArray(map.members) || !map.members.includes(userId)) return; - const members = map.members.filter(m => m !== userId); const rootId = await this.resolveRootId(map.rootId || map.key); const content = { type: "mapMember", target: rootId, member: userId, on: false, createdAt: new Date().toISOString() }; const result = await new Promise((resolve, reject) => ssbClient.publish(content, (e, r) => e ? reject(e) : resolve(r))); - try { await rotateMapKey(rootId, members); } catch (_) {} return result; }, @@ -593,7 +628,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (effectiveTribeId) { updated = await encryptIfTribe(updated); - } else if (mType !== "SINGLE") { + } else if (mType !== "OPEN") { updated = encryptStandalone(updated, rootId); } @@ -629,9 +664,10 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(unwrapForIndex(messages)); + await decryptIndexNodes(idx); - let tipId = mapId; - while (idx.forward.has(tipId)) tipId = idx.forward.get(tipId); + const rootId = idx.rootOf(mapId); + const tipId = idx.contentTipOf(rootId); if (idx.tomb.has(tipId)) throw new Error("Map not found"); const node = idx.nodes.get(tipId); @@ -640,12 +676,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const mapDecrypted = await decryptIfTribe(node.c); assertReadable(mapDecrypted, "Map"); const mapType = mapDecrypted.mapType || node.c.mapType || "SINGLE"; - const mapAuthor = mapDecrypted.author || node.c.author; + const mapAuthor = node.author; if (mapType === "SINGLE") throw new Error("Map does not allow markers"); if (mapType === "CLOSED" && mapAuthor !== userId) throw new Error("Only the map creator can add markers"); const now = new Date().toISOString(); - const rootId = idx.rootOf(tipId); let content = { type: "mapMarker", mapId: rootId, @@ -670,6 +705,24 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { }); }, + async deleteMarker(markerId) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const msg = await getMsg(ssbClient, markerId); + if (!msg || !msg.content) throw new Error("Marker not found"); + let c = msg.content; + if (c.encryptedPayload && tribeCrypto && tribesModel) { + const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + if (dec && !dec._undecryptable) c = dec; + } + if (c.type !== "mapMarker" && !c.encryptedPayload) throw new Error("Marker not found"); + if (msg.author !== userId) throw new Error("Not the author"); + const tombstone = await tombFor(markerId, c.tribeId, userId); + return new Promise((resolve, reject) => { + ssbClient.publish(tombstone, (err, res) => (err ? reject(err) : resolve(res))); + }); + }, + async listAll(filterOrOpts = "all", maybeOpts = {}) { const ssbClient = await openSsb(); @@ -709,9 +762,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(unwrapForIndex(messages)); - await decryptIndexNodes(idx); - await expandMarkers(idx); + const idx = await memoIndex('maps', messages, async (m) => { const built = buildIndex(unwrapForIndex(m)); await decryptIndexNodes(built); await expandMarkers(built); return built; }); let root = id; while (idx.parent.has(root)) root = idx.parent.get(root); @@ -727,11 +778,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false }; } - const markerList = safeArr(idx.markers.get(tip)).concat(safeArr(idx.markers.get(root))); + const markerList = markersFor(idx, tip, root); return buildMap({ key: tip, ts: msg.timestamp || 0, c, author: msg.author }, root, viewer, markerList, idx.resolveMembers(root)); } - const markerList = safeArr(idx.markers.get(tip)).concat(safeArr(idx.markers.get(root))); + const markerList = markersFor(idx, tip, root); const map = buildMap(node, root, viewer, markerList, idx.resolveMembers(root)); return mapCollab.fold(map, collectMaps(idx, viewer)); }, @@ -746,8 +797,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { let invite = code; if (tribeCrypto && !map.tribeId) { const inviteSalt = tribeCrypto.generateInviteSalt(); + const ident = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt) }; const ekChain = tribeCrypto.encryptChainForInvite([map.rootId || map.key], code, inviteSalt); - if (ekChain) invite = { code, ekChain, salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1, ...(opts.public ? { public: true } : {}) }; + const mapKey = ekChain ? null : lookupKey(map.rootId || map.key); + if (ekChain) invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 }; + else if (mapKey) invite = { ...ident, ek: tribeCrypto.encryptForInvite(mapKey, code, inviteSalt), salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 }; } if (opts.public && typeof invite !== "object") invite = { code, public: true }; const tipId = await this.resolveCurrentId(mapId); @@ -840,6 +894,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { await new Promise((resolve, reject) => ssbClient.publish(updated, (e, r) => e ? reject(e) : resolve(r))); const tomb1 = await tombFor(tipId, effectiveTribeId, userId); await new Promise((resolve, reject) => ssbClient.publish(tomb1, e => e ? reject(e) : resolve())); + if (!effectiveTribeId) await rotateMapKey(rootId, Array.isArray(map.members) ? map.members : []); }, async joinByInvite(code) { @@ -854,8 +909,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { for (const m of maps) { const invs = Array.isArray(m.invites) ? m.invites : []; for (const inv of invs) { - if (typeof inv === "string" && inv === code) { matched = m; matchedInvite = inv; break; } - if (typeof inv === "object" && inv.code === code) { matched = m; matchedInvite = inv; break; } + if (inviteMatches(inv, code)) { matched = m; matchedInvite = inv; break; } } if (matched) break; } @@ -863,30 +917,38 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (Array.isArray(matched.members) && matched.members.includes(userId)) throw new Error("Already a member"); const isPublic = (typeof matchedInvite === "object" && matchedInvite.public === true); const rootId = await this.resolveRootId(matched.rootId || matched.key); - if (!isPublic) { - const idx = buildIndex(unwrapForIndex(await getAllMessages(ssbClient))); - if (idx.isCodeConsumed(rootId, code)) throw new Error("Invite already used"); - } + const token = inviteToken(matchedInvite) || code; let mapKey = null; + let chain = null; if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt); - if (Array.isArray(chain) && chain.length) { - for (const entry of chain) { - if (Array.isArray(entry.keys) && entry.keys.length) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); - } else if (entry.key) { - tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1); - } - } - mapKey = chain[0].key; - } + chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3); + if (Array.isArray(chain) && chain.length) mapKey = chain[0].key || (Array.isArray(chain[0].keys) ? chain[0].keys[0] : null); + else chain = null; } else if (matchedInvite.ek) { mapKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt); - ownCrypto.setKey(matched.rootId || matched.key, mapKey, matchedInvite.gen || 1); } } - await new Promise((resolve, reject) => ssbClient.publish({ type: "mapMember", target: rootId, member: userId, on: true, createdAt: new Date().toISOString(), ...(isPublic ? {} : { code }) }, (e, r) => e ? reject(e) : resolve(r))); + if (!isPublic) { + const idx = buildIndex(unwrapForIndex(await getAllMessages(ssbClient))); + if (idx.isCodeConsumed(rootId, token) || idx.isCodeConsumed(rootId, code)) throw new Error("Invite already used"); + const claims = idx.codeClaims(rootId, [token, code]).filter(c => c.member !== userId); + const spent = mapKey ? claims.some(c => c.byOwner || keyProofFor(mapKey, c.member) === c.keyProof) : claims.length > 0; + if (spent) throw new Error("Invite already used"); + } + if (chain) { + for (const entry of chain) { + if (Array.isArray(entry.keys) && entry.keys.length) { + tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + } else if (entry.key) { + tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1); + } + } + } else if (mapKey) { + ownCrypto.setKey(matched.rootId || matched.key, mapKey, matchedInvite.gen || 1); + } + if (!mapKey && matched.contentEncrypted && !matched.tribeId) throw new Error("Invalid or expired invite code"); + await new Promise((resolve, reject) => ssbClient.publish({ type: "mapMember", target: rootId, member: userId, on: true, createdAt: new Date().toISOString(), ...(isPublic ? {} : { code: token }), ...(mapKey ? { keyProof: keyProofFor(mapKey, userId) } : {}) }, (e, r) => e ? reject(e) : resolve(r))); if (tribeCrypto && mapKey) { try { const ssbKeys = require("../server/node_modules/ssb-keys"); diff --git a/src/models/market_model.js b/src/models/market_model.js index f4143a7..9bbf051 100644 --- a/src/models/market_model.js +++ b/src/models/market_model.js @@ -1,8 +1,10 @@ const pull = require("../server/node_modules/pull-stream") const moment = require("../server/node_modules/moment") const { getConfig } = require("../configs/config-manager.js") +const categories = require("../backend/opinion_categories") const { buildValidatedTombstoneSet } = require('./tombstone_validator') -const { dedupeByPreferring, norm } = require('../backend/dedupe') +const { readTyped, memoIndex } = require('./typed_log') +const { dedupeByPreferring, norm, versionGroups } = require('../backend/dedupe') const logLimit = getConfig().ssbLogStream?.limit || 1000 const N = (s) => String(s || "").toUpperCase().replace(/\s+/g, "_") @@ -62,62 +64,60 @@ module.exports = ({ cooler, tribeCrypto }) => { return ssb } - const readAll = async (ssbClient) => { - return new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))) - ) - } + const MARKET_TYPES = ["market", "marketBid", "marketPurchase", "marketOpinion", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, MARKET_TYPES, { limit: logLimit }) const buildMarketIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages) const nodes = new Map() const bids = [] const purchases = [] + const opinionMsgs = [] for (const m of messages) { const c = m.value && m.value.content if (!c) continue if (c.type === "tombstone") continue if (c.type === "marketBid") { bids.push({ target: c.target, author: m.value.author, amount: c.amount, time: c.time, ts: (m.value && m.value.timestamp) || 0 }); continue } if (c.type === "marketPurchase") { purchases.push({ target: c.target, author: m.value.author, ts: (m.value && m.value.timestamp) || 0 }); continue } + if (c.type === "marketOpinion" && c.target) { opinionMsgs.push({ target: c.target, author: m.value.author, category: c.category }); continue } if (c.type !== "market") continue - nodes.set(m.key, { key: m.key, ts: (m.value && m.value.timestamp) || m.timestamp || 0, c, author: m.value.author }) + nodes.set(m.key, { key: m.key, ts: (m.value && m.value.timestamp) || m.timestamp || 0, seq: m.value.sequence || 0, c, author: m.value.author }) } - const naivePrev = new Map(), strictNext = new Map() - for (const [key, n] of nodes) { - const t = n.c.replaces - if (!t) continue - naivePrev.set(key, t) - } - for (const [child, parent] of Array.from(naivePrev.entries())) { - const cn = nodes.get(child) - const pn = nodes.get(parent) - if (!pn) { naivePrev.delete(child); continue } - if (!cn || cn.author !== pn.author) { naivePrev.delete(child); nodes.delete(child); continue } - strictNext.set(parent, child) - } - return { tomb, nodes, bids, purchases, naivePrev, strictNext } + const vg = versionGroups(Array.from(nodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.seller === "string" ? n.c.seller : null }))) + return { tomb, nodes, bids, purchases, opinionMsgs, vg } } const resolveGroups = (idx) => { - const { tomb, nodes, bids, purchases, naivePrev, strictNext } = idx - const rootOf = (key) => { let x = key, g = 0; while (naivePrev.has(x) && nodes.has(naivePrev.get(x)) && g++ < 100000) x = naivePrev.get(x); return x } - const followStrict = (key) => { let x = key, g = 0; while (strictNext.has(x) && g++ < 100000) x = strictNext.get(x); return x } + const { tomb, nodes, bids, purchases, opinionMsgs, vg } = idx + const rootOf = vg.rootOf - const groups = new Map() - for (const key of nodes.keys()) { const r = rootOf(key); if (!groups.has(r)) groups.set(r, []); groups.get(r).push(key) } const bidsByRoot = new Map() for (const bd of bids) { if (!nodes.has(bd.target)) continue; const r = rootOf(bd.target); if (!bidsByRoot.has(r)) bidsByRoot.set(r, []); bidsByRoot.get(r).push(bd) } const soldByRoot = new Map() - for (const pu of purchases) { if (!nodes.has(pu.target)) continue; const r = rootOf(pu.target); soldByRoot.set(r, (soldByRoot.get(r) || 0) + 1) } + const buyersByRoot = new Map() + for (const pu of purchases) { + if (!nodes.has(pu.target)) continue + const r = rootOf(pu.target) + soldByRoot.set(r, (soldByRoot.get(r) || 0) + 1) + if (!buyersByRoot.has(r)) buyersByRoot.set(r, new Set()) + buyersByRoot.get(r).add(pu.author) + } + const opinionsByRoot = new Map() + for (const op of (opinionMsgs || [])) { + if (!nodes.has(op.target)) continue + const r = rootOf(op.target) + if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []) + opinionsByRoot.get(r).push(op) + } const out = new Map() - for (const [root, keys] of groups) { + for (const root of vg.roots) { + const keys = vg.groups.get(root) || [root] const rootNode = nodes.get(root) const sellerId = rootNode ? rootNode.author : null const sellerKeys = keys.filter(k => { const n = nodes.get(k); return n && n.author === sellerId }) - let tip = followStrict(root) - const tipNode0 = nodes.get(tip) - if (!tipNode0 || tipNode0.author !== sellerId) tip = root + const tip = vg.tipOf(root) if (tomb.has(tip)) continue let best = nodes.get(tip) if (!best) continue @@ -131,7 +131,16 @@ module.exports = ({ cooler, tribeCrypto }) => { const addLine = (line) => { const b = parseBidEntry(line); if (!b) return; const kk = `${b.bidder}|${b.amount}|${b.time}`; if (pollSet.has(kk)) return; pollSet.add(kk); poll.push(line) } for (const k of (sellerKeys.length ? sellerKeys : keys)) { const n = nodes.get(k); if (n && Array.isArray(n.c.auctions_poll)) for (const line of n.c.auctions_poll) addLine(line) } for (const bd of (bidsByRoot.get(root) || [])) { const amt = Number(bd.amount); addLine(`${bd.author}|${Number.isFinite(amt) ? amt.toFixed(6) : bd.amount}|${bd.time}`) } - out.set(root, { tip, rootId: root, best, statusN: bestS, poll, soldCount: soldByRoot.get(root) || 0 }) + const opinions = {} + const voters = [] + for (const op of (opinionsByRoot.get(root) || [])) { + if (!op.author || op.author === sellerId) continue + if (voters.includes(op.author)) continue + if (!categories.includes(op.category)) continue + voters.push(op.author) + opinions[op.category] = (opinions[op.category] || 0) + 1 + } + out.set(root, { tip, rootId: root, best, statusN: bestS, poll, soldCount: soldByRoot.get(root) || 0, buyers: Array.from(buyersByRoot.get(root) || []), opinions, voters }) } return out } @@ -180,6 +189,7 @@ module.exports = ({ cooler, tribeCrypto }) => { shopProductId: shopOpts.shopProductId || "", shopId: shopOpts.shopId || "", shopTitle: shopOpts.shopTitle || "", + industry: shopOpts.industry || "", visibility: String(visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" } @@ -198,10 +208,9 @@ module.exports = ({ cooler, tribeCrypto }) => { async resolveCurrentId(itemId) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const { tomb, strictNext } = buildMarketIndex(messages) - let cur = itemId, g = 0 - while (strictNext.has(cur) && g++ < 100000) cur = strictNext.get(cur) - if (tomb.has(cur)) throw new Error("Item not found") + const { tomb, vg } = buildMarketIndex(messages) + const cur = vg.tipOf(itemId) + if (tomb.has(cur) || vg.isForeignCopy(vg.rootOf(cur))) throw new Error("Item not found") return cur }, @@ -227,9 +236,13 @@ module.exports = ({ cooler, tribeCrypto }) => { normalized.price = p.toFixed(6) } + const currentItem = await new Promise((resolve) => ssbClient.get(tipId, (e, m) => resolve(e ? null : (m && m.content) || null))) + if (normalized.deadline !== undefined && normalized.deadline !== null && normalized.deadline !== "") { const dl = moment(normalized.deadline, moment.ISO_8601, true) if (!dl.isValid()) throw new Error("Invalid deadline") + const changed = !currentItem || !currentItem.deadline || !moment(currentItem.deadline).isSame(dl) + if (changed && dl.isBefore(moment(), "minute")) throw new Error("The deadline cannot be in the past") normalized.deadline = dl.toISOString() } @@ -297,7 +310,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const items = [] const now = moment() - for (const { tip, rootId, best, statusN, poll, soldCount } of resolveGroups(buildMarketIndex(messages)).values()) { + for (const { tip, rootId, best, statusN, poll, soldCount, buyers, opinions, voters } of resolveGroups(buildMarketIndex(messages)).values()) { const leaf = tip const c = best.c let status = D(statusN) @@ -344,7 +357,12 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: c.mapUrl || "", shopProductId: c.shopProductId || "", shopId: c.shopId || "", - shopTitle: c.shopTitle || "" + shopTitle: c.shopTitle || "", + industry: c.industry || "", + opinions: opinions || {}, + opinions_inhabitants: voters || [], + purchasedByViewer: Array.isArray(buyers) && buyers.includes(userId), + ratedByViewer: Array.isArray(voters) && voters.includes(userId) }) } @@ -395,11 +413,10 @@ module.exports = ({ cooler, tribeCrypto }) => { const userId = ssbClient.id const messages = await readAll(ssbClient) - const idx = buildMarketIndex(messages) - const rootOf = (key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x } - const grp = resolveGroups(idx).get(rootOf(itemId)) + const idx = memoIndex('market', messages, buildMarketIndex) + const grp = resolveGroups(idx).get(idx.vg.rootOf(itemId)) if (!grp) return null - const { tip, rootId, best, statusN, poll, soldCount } = grp + const { tip, rootId, best, statusN, poll, soldCount, buyers, opinions, voters } = grp const c = best.c let status = D(statusN) @@ -445,7 +462,12 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: c.mapUrl || "", shopProductId: c.shopProductId || "", shopId: c.shopId || "", - shopTitle: c.shopTitle || "" + shopTitle: c.shopTitle || "", + industry: c.industry || "", + opinions: opinions || {}, + opinions_inhabitants: voters || [], + purchasedByViewer: Array.isArray(buyers) && buyers.includes(userId), + ratedByViewer: Array.isArray(voters) && voters.includes(userId) } }, @@ -454,6 +476,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const myId = ssbClient.id const now = moment() const list = Array.isArray(items) ? items : [] + let changed = 0 for (const item of list) { if (!item || !item.deadline) continue @@ -476,8 +499,10 @@ module.exports = ({ cooler, tribeCrypto }) => { try { await this.updateItemById(item.id, { status }) + changed++ } catch (_) {} } + return changed }, async getItemByShopProductId(shopProductId) { @@ -486,6 +511,21 @@ module.exports = ({ cooler, tribeCrypto }) => { return items.find((i) => i.shopProductId === shopProductId) || null }, + async createOpinion(itemId, category) { + if (!categories.includes(category)) throw new Error("Invalid category") + const ssbClient = await openSsb() + const userId = ssbClient.id + const messages = await readAll(ssbClient) + const idx = buildMarketIndex(messages) + const group = resolveGroups(idx).get(idx.vg.rootOf(itemId)) + if (!group) throw new Error("Item not found") + if (group.best.author === userId) throw new Error("You cannot rate your own item") + if ((group.voters || []).includes(userId)) throw new Error("Already voted") + if (!(group.buyers || []).includes(userId)) throw new Error("You can rate only an item you have bought") + const content = { type: "marketOpinion", target: group.rootId, category, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + async setItemAsSold(itemId) { const tipId = await this.resolveCurrentId(itemId) const ssbClient = await openSsb() @@ -536,8 +576,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildMarketIndex(messages) - const rootOf = (key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x } - const grp = resolveGroups(idx).get(rootOf(itemId)) + const grp = resolveGroups(idx).get(idx.vg.rootOf(itemId)) if (!grp) throw new Error("Item not found") const c = grp.best.c diff --git a/src/models/media_gallery.js b/src/models/media_gallery.js new file mode 100644 index 0000000..3d8c7b4 --- /dev/null +++ b/src/models/media_gallery.js @@ -0,0 +1,42 @@ +const MAX_IMAGES = 8 + +const MEDIA_MARKDOWN = /\[(image|video|audio):[^\]]*\]\(/ + +const blobOf = (value) => { + let blobId = value || null + if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] + return blobId || null +} + +const keepMarkdown = (value, id) => + MEDIA_MARKDOWN.test(value) || value.startsWith("![") ? value : id + +const normalizeImages = (raw, max = MAX_IMAGES) => { + const list = Array.isArray(raw) ? raw : (raw ? [raw] : []) + const out = [] + const seen = new Set() + for (const entry of list) { + const value = String(entry || "").trim() + const id = blobOf(value) + if (!id || seen.has(id)) continue + seen.add(id) + out.push(keepMarkdown(value, id)) + if (out.length >= max) break + } + return out +} + +const normalizeVideo = (raw) => { + const value = String(Array.isArray(raw) ? raw[0] || "" : raw || "").trim() + const id = blobOf(value) + if (!id) return "" + return keepMarkdown(value, id) +} + +const mergeGallery = (current, uploaded, removeIndex = -1, max = MAX_IMAGES) => { + let list = normalizeImages(current, max) + if (removeIndex >= 0) list = list.filter((_, i) => i !== removeIndex) + return normalizeImages([...list, ...normalizeImages(uploaded, max)], max) +} + +module.exports = { MAX_IMAGES, blobOf, normalizeImages, normalizeVideo, mergeGallery } diff --git a/src/models/melody_model.js b/src/models/melody_model.js index 072b763..57ebd20 100644 --- a/src/models/melody_model.js +++ b/src/models/melody_model.js @@ -22,12 +22,14 @@ const TYPE_TO_DEGREE = { bookmark: 4, feed: 5, pad: 6, + room: 6, chat: 7, audio: 8, video: 9, image: 10, document: 11, torrent: 0, + file: 0, map: 2, pixelia: 3, gameScore: 5, @@ -39,7 +41,35 @@ const TYPE_TO_DEGREE = { courts: 0, market: 1, aiExchange: 6, - tombstone: 4 + tombstone: 4, + wikiPage: 2, + emergency: 11, + emergencyConfirm: 11, + emergencyUpdate: 11, + mailingList: 5, + logisticsRoute: 3, + logisticsRating: 3, + podcast: 8, + podcastEpisode: 8, + podcastPlay: 8, + campaign: 9, + campaignSignature: 9, + campaignUpdate: 9 +}; + +const FAMILY_DEGREE = [ + ['tribeParliament', 11], ['parliament', 11], ['courts', 0], ['school', 1], ['housing', 4], ['industry', 6], + ['poll', 2], ['blog', 0], ['larp', 9], ['chat', 7], ['pad', 6], ['room', 6], ['wiki', 2], ['emergency', 11], + ['mailing', 5], ['logistics', 3], ['podcast', 8], ['campaign', 9], ['project', 6], ['market', 1], ['shop', 1], + ['job', 3], ['event', 8], ['task', 10], ['report', 10], ['votes', 7], ['calendar', 8], ['map', 2], + ['transfer', 11], ['bank', 11], ['ubi', 11], ['feed', 5], ['tribe', 9], ['forum', 7], ['bookmark', 4], + ['audio', 8], ['video', 9], ['image', 10], ['document', 11], ['torrent', 0], ['file', 0] +]; + +const degreeOf = (type) => { + if (TYPE_TO_DEGREE[type] != null) return TYPE_TO_DEGREE[type]; + const family = FAMILY_DEGREE.find(([prefix]) => type.startsWith(prefix)); + return family ? family[1] : (Math.abs(hashStr(type)) % 12); }; const NOTE_FREQS = (() => { @@ -59,7 +89,7 @@ function blockToNote(msg) { const c = msg && msg.value && msg.value.content; if (!c || typeof c !== 'object') return null; const type = String(c.type || '').trim() || 'unknown'; - const degree = TYPE_TO_DEGREE[type] != null ? TYPE_TO_DEGREE[type] : (Math.abs(hashStr(type)) % 12); + const degree = degreeOf(type); const size = Buffer.byteLength(JSON.stringify(msg.value), 'utf8'); const octIdx = size < 256 ? 0 : (size < 1024 ? 1 : 2); const octave = OCTAVES[octIdx]; diff --git a/src/models/mentions_model.js b/src/models/mentions_model.js new file mode 100644 index 0000000..85a9160 --- /dev/null +++ b/src/models/mentions_model.js @@ -0,0 +1,213 @@ +const fs = require('fs'); +const pull = require('../server/node_modules/pull-stream'); +const { readTyped, CONTENT_TYPES } = require('./typed_log'); + +const SEEN_FILE = 'oasis-mentions-seen'; + +const seenPath = () => { + try { return require('../configs/state-manager').statePath(SEEN_FILE); } catch (_) { return null; } +}; + +const MAX_SEEN_KEYS = 500; + +const readSeen = () => { + const p = seenPath(); + if (!p) return { ts: 0, keys: [] }; + let raw = ''; + try { raw = fs.readFileSync(p, 'utf8').trim(); } catch (_) { return { ts: 0, keys: [] }; } + if (!raw) return { ts: 0, keys: [] }; + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + return { ts: Number(parsed.ts) || 0, keys: Array.isArray(parsed.keys) ? parsed.keys : [] }; + } + } catch (_) {} + return { ts: Number(raw) || 0, keys: [] }; +}; + +const writeSeen = (keys, ts) => { + const p = seenPath(); + if (!p) return false; + const trimmed = (Array.isArray(keys) ? keys : []).filter(Boolean).slice(0, MAX_SEEN_KEYS); + try { fs.writeFileSync(p, JSON.stringify({ ts: Number(ts) || Date.now(), keys: trimmed })); return true; } catch (_) { return false; } +}; +const { getConfig } = require('../configs/config-manager.js'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const TEXT_FIELDS = ['text', 'description', 'title', 'concept', 'question', 'rules', 'message', 'body', 'subject']; + +const SKIP_TYPES = new Set([ + 'about', 'contact', 'vote', 'pub', 'tombstone', 'karmaScore', + 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-key-distrib' +]); + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); + + const mentionsList = (content, feedId) => { + const m = content && content.mentions; + if (!m) return false; + const values = Array.isArray(m) ? m : (typeof m === 'object' ? Object.values(m).flat() : []); + return values.some(v => v && (v.link === feedId || v === feedId)); + }; + + const escapeRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const nameRes = (names) => names.map(n => new RegExp(`(^|[^\\w\\u0080-\\uFFFF\\[\\]/@])@${escapeRe(n)}(?=$|[^\\w\\u0080-\\uFFFF/+=])`, 'i')); + const myNamesOf = (messages, feedId) => { + let latest = null; + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || c.type !== 'about' || String(v.author) !== String(feedId) || c.about !== feedId || typeof c.name !== 'string' || !c.name.trim()) continue; + const ts = Number(v.timestamp || m.timestamp || 0); + if (!latest || ts >= latest.ts) latest = { ts, name: c.name.trim() }; + } + return latest ? [latest.name] : []; + }; + const mentionsText = (content, feedId, byName = []) => { + const bare = feedId.startsWith('@') ? feedId.slice(1) : feedId; + for (const field of TEXT_FIELDS) { + const v = content[field]; + if (typeof v === 'string' && (v.includes(feedId) || v.includes(bare) || byName.some(re => re.test(v)))) return true; + } + if (Array.isArray(content.tags)) { + if (content.tags.some(t => typeof t === 'string' && (t.includes(feedId) || t.includes(bare)))) return true; + } + return false; + }; + + const snippet = (content) => { + for (const field of TEXT_FIELDS) { + const v = content[field]; + if (typeof v === 'string' && v.trim()) return v.trim(); + } + return ''; + }; + + const scanCache = { fingerprint: '', fresh: null }; + + const titleOf = (content) => { + for (const field of ['title', 'concept', 'question', 'subject']) { + const v = content[field]; + if (typeof v === 'string' && v.trim()) return v.trim(); + } + return ''; + }; + + return { + async listMentions(filter = 'ALL', opts = {}) { + const ssbClient = await openSsb(); + const myFeedId = ssbClient.id; + const messages = await getAllMessages(ssbClient); + const last = messages.length ? messages[messages.length - 1] : null; + const fingerprint = `${myFeedId}|${messages.length}|${last && last.key}`; + const fresh = (scanCache.fingerprint === fingerprint && scanCache.fresh) + ? scanCache.fresh + : (() => { + const tomb = buildValidatedTombstoneSet(messages); + const vg = versionGroups(messages.filter(m => { const c = m && m.value && m.value.content; return c && typeof c === 'object' && c.type && !SKIP_TYPES.has(c.type); }).map(versionEntry)); + + const out = []; + const seen = new Set(); + const byName = nameRes(myNamesOf(messages, myFeedId)); + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object' || !c.type) continue; + if (tomb.has(m.key)) continue; + if (SKIP_TYPES.has(c.type)) continue; + if (c.encryptedPayload || c.encryptedQuestion) continue; + if (Array.isArray(c.recps) || Array.isArray(v.content && v.content.recps)) continue; + if (c.tribeId && c.type !== 'tribe') continue; + if (String(v.author) === String(myFeedId)) continue; + if (vg.tipOf(m.key) !== m.key || !vg.roots.has(vg.rootOf(m.key))) continue; + if (!mentionsList(c, myFeedId) && !mentionsText(c, myFeedId, byName)) continue; + if (seen.has(m.key)) continue; + seen.add(m.key); + out.push({ + id: m.key, + key: m.key, + author: v.author, + type: c.type, + title: titleOf(c), + text: snippet(c), + content: c, + createdAt: new Date(v.timestamp || m.timestamp || 0).toISOString(), + ts: v.timestamp || m.timestamp || 0 + }); + } + + scanCache.fingerprint = fingerprint; + scanCache.fresh = out; + return out; + })(); + const q = String(opts.q || '').trim().toLowerCase(); + let list = q + ? fresh.filter(x => x.text.toLowerCase().includes(q) || x.title.toLowerCase().includes(q) || x.type.toLowerCase().includes(q)) + : fresh; + + const f = String(filter || 'ALL').toUpperCase(); + if (f !== 'ALL' && f !== 'RECENT') list = list.filter(x => x.type === filter); + + list.sort((a, b) => b.ts - a.ts); + return list; + }, + + seenAt: () => readSeen().ts, + + readKeys: () => new Set(readSeen().keys.map(String)), + + markRead(key) { + if (!key) return false; + const seen = readSeen(); + const keys = [String(key)].concat(seen.keys.filter(k => String(k) !== String(key))); + return writeSeen(keys, seen.ts || Date.now()); + }, + + markReadMany(list) { + const seen = readSeen(); + const add = (Array.isArray(list) ? list : []).filter(Boolean).map(String); + const rest = seen.keys.filter(k => !add.includes(String(k))); + return writeSeen(add.concat(rest), seen.ts || Date.now()); + }, + + markUnread(key) { + if (!key) return false; + const seen = readSeen(); + return writeSeen(seen.keys.filter(k => String(k) !== String(key)), seen.ts); + }, + + markSeen(list) { + const items = Array.isArray(list) ? list : []; + const keys = items.map(x => x && x.key).filter(Boolean); + const newest = items.reduce((max, x) => Math.max(max, Number(x && x.ts) || 0), 0); + return writeSeen(keys, newest || Date.now()); + }, + + unseenOf(list) { + const seen = readSeen(); + const known = new Set(seen.keys); + const items = Array.isArray(list) ? list : []; + if (known.size) return items.filter(x => x && !known.has(x.key)); + if (seen.ts) return items.filter(x => x && (Number(x.ts) || 0) > seen.ts); + return items; + }, + + async countUnseen() { + return this.unseenOf(await this.listMentions('ALL')).length; + }, + + async countTypes(list) { + const counts = {}; + for (const item of (Array.isArray(list) ? list : [])) { + counts[item.type] = (counts[item.type] || 0) + 1; + } + return counts; + } + }; +}; diff --git a/src/models/nextcloud_model.js b/src/models/nextcloud_model.js new file mode 100644 index 0000000..38a7b97 --- /dev/null +++ b/src/models/nextcloud_model.js @@ -0,0 +1,151 @@ +const fs = require('fs'); +const path = require('path'); +const { statePath } = require('../configs/state-manager'); + +const ACCOUNT_PATH = statePath('nextcloud-account.json'); +const CHUNK_BYTES = 10 * 1024 * 1024; +const REQUEST_TIMEOUT_MS = 15000; +const CHUNK_TIMEOUT_MS = 10 * 60 * 1000; +const DEFAULT_FOLDER = 'Oasis'; + +const readAccount = () => { + try { + const a = JSON.parse(fs.readFileSync(ACCOUNT_PATH, 'utf8')); + return a && a.server && a.user && a.password ? a : null; + } catch (_) { return null; } +}; +const writeAccount = (a) => { + fs.mkdirSync(path.dirname(ACCOUNT_PATH), { recursive: true }); + fs.writeFileSync(ACCOUNT_PATH, JSON.stringify(a), { mode: 0o600 }); +}; +const normalizeServer = (v) => { + let s = String(v || '').trim(); + if (!s) return ''; + if (!/^https?:\/\//i.test(s)) s = `https://${s}`; + try { + const u = new URL(s); + if (!/^https?:$/.test(u.protocol)) return ''; + return `${u.origin}${u.pathname.replace(/\/+$/, '')}`; + } catch (_) { return ''; } +}; +const cleanFolder = (v) => String(v || '').trim().replace(/^\/+|\/+$/g, '').split('/').map(s => s.trim()).filter(Boolean).join('/') || DEFAULT_FOLDER; +const encPath = (p) => p.split('/').map(encodeURIComponent).join('/'); +const safeName = (v) => String(v || '').replace(/[\\/:*?"<>|\r\n]+/g, ' ').trim().slice(0, 120) || 'file'; +const pad = (n) => String(n).padStart(15, '0'); +const readRange = async (file, start, end) => { + const fh = await fs.promises.open(file, 'r'); + try { + const buf = Buffer.alloc(end - start); + let got = 0; + while (got < buf.length) { + const { bytesRead } = await fh.read(buf, got, buf.length - got, start + got); + if (!bytesRead) break; + got += bytesRead; + } + return got === buf.length ? buf : buf.subarray(0, got); + } finally { await fh.close(); } +}; + +module.exports = ({ isPublic = false } = {}) => { + const auth = (a) => 'Basic ' + Buffer.from(`${a.user}:${a.password}`).toString('base64'); + const call = async (a, method, url, { headers = {}, body, timeout = REQUEST_TIMEOUT_MS } = {}) => { + const ctl = new AbortController(); + const t = setTimeout(() => ctl.abort(), timeout); + try { + return await fetch(url, { method, headers: { Authorization: auth(a), 'OCS-APIRequest': 'true', ...headers }, body, signal: ctl.signal, redirect: 'manual' }); + } finally { clearTimeout(t); } + }; + const davFiles = (a) => `${a.server}/remote.php/dav/files/${encodeURIComponent(a.user)}`; + const davUploads = (a) => `${a.server}/remote.php/dav/uploads/${encodeURIComponent(a.user)}`; + const targetUrl = (a, name) => `${davFiles(a)}/${encPath(a.folder)}/${encodeURIComponent(name)}`; + const whoami = async (a) => { + let res; + try { res = await call(a, 'GET', `${a.server}/ocs/v1.php/cloud/user?format=json`); } catch (_) { throw new Error('nextcloudErrConnect'); } + if (!res.ok) throw new Error('nextcloudErrConnect'); + let data = null; + try { data = await res.json(); } catch (_) {} + const d = data && data.ocs && data.ocs.data; + if (!d || !d.id) throw new Error('nextcloudErrConnect'); + return { id: String(d.id), displayName: String(d['display-name'] || d.displayname || d.id) }; + }; + const mkcol = async (a, url) => { + const res = await call(a, 'MKCOL', url); + if (!(res.ok || res.status === 405)) throw new Error('nextcloudErrUpload'); + }; + const ensureFolder = async (a) => { + let cur = davFiles(a); + for (const seg of a.folder.split('/')) { cur += `/${encodeURIComponent(seg)}`; await mkcol(a, cur); } + }; + const exists = async (a, url) => { + const res = await call(a, 'PROPFIND', url, { headers: { Depth: '0' } }); + return res.status === 207 || res.status === 200; + }; + const freeName = async (a, name) => { + if (!(await exists(a, targetUrl(a, name)))) return name; + const dot = name.lastIndexOf('.'); + const stamp = Date.now().toString(36); + return dot > 0 ? `${name.slice(0, dot)}-${stamp}${name.slice(dot)}` : `${name}-${stamp}`; + }; + const putWhole = async (a, filePath, size, name) => { + const res = await call(a, 'PUT', targetUrl(a, name), { body: await readRange(filePath, 0, size), headers: { 'Content-Type': 'application/octet-stream' }, timeout: CHUNK_TIMEOUT_MS }); + if (!res.ok) throw new Error('nextcloudErrUpload'); + }; + const putChunked = async (a, filePath, size, name) => { + const base = `${davUploads(a)}/oasis-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + await mkcol(a, base); + for (let start = 0; start < size; start += CHUNK_BYTES) { + const end = Math.min(size, start + CHUNK_BYTES); + const res = await call(a, 'PUT', `${base}/${pad(start)}-${pad(end - 1)}`, { body: await readRange(filePath, start, end), headers: { 'Content-Type': 'application/octet-stream' }, timeout: CHUNK_TIMEOUT_MS }); + if (!res.ok) throw new Error('nextcloudErrUpload'); + } + const res = await call(a, 'MOVE', `${base}/.file`, { headers: { Destination: targetUrl(a, name), 'OC-Total-Length': String(size) }, timeout: CHUNK_TIMEOUT_MS }); + if (!res.ok) throw new Error('nextcloudErrUpload'); + }; + const share = async (a, name) => { + const body = new URLSearchParams({ path: `/${a.folder}/${name}`, shareType: '3' }); + const res = await call(a, 'POST', `${a.server}/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }); + if (!res.ok) throw new Error('nextcloudErrShare'); + let data = null; + try { data = await res.json(); } catch (_) {} + const url = data && data.ocs && data.ocs.data && data.ocs.data.url; + if (!url || !/^https?:\/\//i.test(String(url))) throw new Error('nextcloudErrShare'); + return String(url); + }; + + return { + getAccount() { + const a = readAccount(); + return a ? { server: a.server, user: a.user, folder: a.folder, displayName: a.displayName || a.user } : null; + }, + + async connect({ server, user, password, folder } = {}) { + if (isPublic) throw new Error('nextcloudErrConnect'); + const a = { server: normalizeServer(server), user: String(user || '').trim(), password: String(password || ''), folder: cleanFolder(folder) }; + if (!a.server || !a.user || !a.password) throw new Error('nextcloudErrConnect'); + const me = await whoami(a); + a.displayName = me.displayName; + await ensureFolder(a); + writeAccount(a); + return this.getAccount(); + }, + + disconnect() { + try { fs.unlinkSync(ACCOUNT_PATH); } catch (_) {} + }, + + async upload({ filePath, size, name } = {}) { + if (isPublic) throw new Error('nextcloudErrUpload'); + const a = readAccount(); + if (!a) throw new Error('nextcloudErrConnect'); + const bytes = Number(size) || 0; + if (!filePath || !(bytes > 0)) throw new Error('nextcloudErrUpload'); + const final = await freeName(a, safeName(name)); + if (bytes <= CHUNK_BYTES) await putWhole(a, filePath, bytes, final); + else await putChunked(a, filePath, bytes, final); + const url = await share(a, final); + return { url, name: final, size: bytes }; + } + }; +}; + +module.exports.CHUNK_BYTES = CHUNK_BYTES; diff --git a/src/models/onboarding_model.js b/src/models/onboarding_model.js new file mode 100644 index 0000000..eac7b74 --- /dev/null +++ b/src/models/onboarding_model.js @@ -0,0 +1,218 @@ +const fs = require('fs'); +const path = require('path'); +const pull = require('../server/node_modules/pull-stream'); + +const STEPS = ['profile', 'ux', 'workflow', 'theme', 'community', 'greeting']; +const MARKED_STEPS = ['profile', 'ux', 'workflow', 'theme', 'community']; +const FLAG = 'oasis-first-contact'; +const PENDING = 'welcome=pending'; +const DISMISSED = 'welcome=dismissed'; +const DONE = 'welcome=done'; + +const ssbPathOf = (given) => { + if (given) return given; + try { return require('../server/ssb_config').path; } catch (_) { return null; } +}; + +const flagPath = (dir) => { + try { + const state = require('../configs/state-manager'); + if (!dir || path.resolve(dir) === state.ssbDir()) return state.statePath(FLAG); + } catch (_) {} + return path.join(dir, FLAG); +}; + +function readFlag(dir) { + if (!dir) return { exists: false, id: '', markers: new Set() }; + let raw = ''; + try { raw = fs.readFileSync(flagPath(dir), 'utf8'); } catch (_) { return { exists: false, id: '', markers: new Set() }; } + const lines = raw.split('\n').map(l => l.trim()).filter(Boolean); + const isMarker = (l) => /^(welcome|step)=/.test(l); + const markers = new Set(lines.filter(isMarker)); + return { exists: true, id: lines.find(l => !isMarker(l)) || '', markers }; +} + +function appendMarker(dir, marker, createIfMissing) { + if (!dir) return false; + const flag = readFlag(dir); + if (flag.markers.has(marker)) return true; + if (!flag.exists && !createIfMissing) return false; + try { + fs.mkdirSync(dir, { recursive: true }); + fs.appendFileSync(flagPath(dir), `${marker}\n`); + return true; + } catch (_) { + return false; + } +} + +const isPending = (dir) => { + if (!dir) return false; + const flag = readFlag(dir); + if (!flag.exists) return true; + if (flag.markers.has(DISMISSED) || flag.markers.has(DONE)) return false; + return flag.markers.has(PENDING); +}; + +const bannerVisible = (ssbPath) => isPending(ssbPathOf(ssbPath)); + +const DEFAULT_LANGUAGE = 'en'; + +const browserLanguage = (header, supported) => { + const list = Array.isArray(supported) ? supported : []; + const wanted = String(header || '').slice(0, 1024).split(',').map((part, i) => { + const [tag, ...params] = part.trim().split(';'); + const qParam = params.map(p => p.trim()).find(p => /^q=/i.test(p)); + const q = qParam ? Number(qParam.slice(2)) : 1; + return { tag: String(tag || '').trim().toLowerCase().replace(/_/g, '-'), q: Number.isFinite(q) ? q : 0, i }; + }).filter(w => w.tag && w.tag !== '*' && w.q > 0).sort((a, b) => b.q - a.q || a.i - b.i); + for (const w of wanted) { + if (list.includes(w.tag)) return w.tag; + const base = w.tag.split('-')[0]; + if (list.includes(base)) return base; + } + return ''; +}; + +const firstRunLanguage = ({ fresh, isPublic, configured, cookie, header, supported } = {}) => { + const list = Array.isArray(supported) ? supported : []; + if (!fresh || isPublic) return { settled: true, language: '' }; + if (list.includes(configured) && configured !== DEFAULT_LANGUAGE) return { settled: true, language: '' }; + const picked = (list.includes(cookie) ? cookie : '') || browserLanguage(header, list); + if (!picked && !String(header || '').trim()) return { settled: false, language: '' }; + return { settled: true, language: picked }; +}; + +module.exports = ({ cooler, ssbPath } = {}) => { + const dir = () => ssbPathOf(ssbPath); + + const ownMessages = async () => { + const ssb = await cooler.open(); + const mine = ssb.id; + const all = await new Promise((resolve, reject) => + pull(ssb.createUserStream({ id: mine, reverse: true, limit: 2000 }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || []))) + ); + return { mine, messages: all.filter(m => m && m.value && m.value.author === mine) }; + }; + + const hasProfile = (mine, messages) => messages.some(m => { + const c = m.value.content; + if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) return false; + return String(c.name || '').trim().length > 0 || String(c.image || '').trim().length > 0; + }); + + const hasGreeted = (messages) => messages.some(m => { + const c = m.value.content; + return c && typeof c === 'object' && c.type === 'feed' && String(c.text || '').trim().length > 0; + }); + + const hasFederated = (mine, messages) => { + try { + const gossip = JSON.parse(fs.readFileSync(path.join(dir(), 'gossip.json'), 'utf8')); + if (Array.isArray(gossip) && gossip.length > 0) return true; + } catch (_) {} + return messages.some(m => { + const c = m.value.content; + return c && typeof c === 'object' && c.type === 'contact' && c.following === true && c.contact && c.contact !== mine; + }); + }; + + return { + STEPS, + + flagPath() { + return flagPath(dir()); + }, + + firstContactSeen(feedId) { + const flag = readFlag(dir()); + return flag.exists && (!feedId || flag.id === String(feedId)); + }, + + begin(feedId) { + const target = dir(); + if (!target) return false; + const flag = readFlag(target); + const closed = flag.markers.has(DISMISSED) || flag.markers.has(DONE); + const markers = closed ? [...flag.markers] : [...new Set([PENDING, ...flag.markers])]; + try { + fs.mkdirSync(target, { recursive: true }); + fs.writeFileSync(flagPath(target), `${String(feedId || '')}\n${new Date().toISOString()}\n${markers.join('\n')}\n`); + return true; + } catch (_) { + return false; + } + }, + + adopt(feedId) { + const target = dir(); + if (!target || !feedId) return false; + try { + fs.mkdirSync(target, { recursive: true }); + fs.writeFileSync(flagPath(target), `${String(feedId)}\n${new Date().toISOString()}\n${DONE}\n`); + return true; + } catch (_) { + return false; + } + }, + + markStep(step) { + if (!MARKED_STEPS.includes(step)) return false; + if (!isPending(dir())) return true; + return appendMarker(dir(), `step=${step}`); + }, + + dismiss() { + return appendMarker(dir(), DISMISSED, true); + }, + + isVisible() { + return isPending(dir()); + }, + + async status(available) { + const target = dir(); + const flag = readFlag(target); + const usable = STEPS.filter(s => !available || available[s] !== false); + let mine = ''; + let messages = []; + try { ({ mine, messages } = await ownMessages()); } catch (_) { mine = ''; messages = []; } + const steps = { + profile: hasProfile(mine, messages) || flag.markers.has('step=profile'), + ux: flag.markers.has('step=ux'), + workflow: flag.markers.has('step=workflow'), + theme: flag.markers.has('step=theme'), + community: hasFederated(mine, messages) || flag.markers.has('step=community'), + greeting: hasGreeted(messages) + }; + const profile = { id: mine, name: '', description: '', image: '' }; + for (const m of messages) { + const c = m.value.content; + if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) continue; + if (!profile.name && String(c.name || '').trim()) profile.name = String(c.name).trim(); + if (!profile.description && String(c.description || '').trim()) profile.description = String(c.description).trim(); + if (!profile.image && String(c.image || '').trim()) profile.image = String(c.image).trim(); + if (profile.name && profile.description && profile.image) break; + } + const done = usable.filter(s => steps[s]).length; + const complete = done === usable.length; + if (complete && isPending(target)) appendMarker(target, DONE); + return { + id: flag.id, + pending: isPending(target), + dismissed: flag.markers.has(DISMISSED), + steps, + profile, + usable, + done, + total: usable.length, + complete + }; + } + }; +}; + +module.exports.bannerVisible = bannerVisible; +module.exports.STEPS = STEPS; +module.exports.browserLanguage = browserLanguage; +module.exports.firstRunLanguage = firstRunLanguage; diff --git a/src/models/opinions_model.js b/src/models/opinions_model.js index 4ffb636..90e7236 100644 --- a/src/models/opinions_model.js +++ b/src/models/opinions_model.js @@ -2,7 +2,15 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const categories = require('../backend/opinion_categories'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES, requestScope } = require('./typed_log'); +const longText = require('../backend/long_text'); +const isRootBlogPost = (c) => + !!c && c.type === 'post' && typeof c.text === 'string' && + !c.root && !c.fork && !c.branch && !c.about && !c.target && + c.private !== true && !Array.isArray(c.recps); + const { buildVoteTally } = require('../backend/vote_tally'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler }) => { @@ -22,25 +30,18 @@ module.exports = ({ cooler }) => { const validTypes = [ 'bookmark', 'votes', 'transfer', - 'feed', 'image', 'audio', 'video', 'document', 'torrent' + 'feed', 'post', 'image', 'audio', 'video', 'document', 'torrent', 'file', + 'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse', + 'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute' ]; const getPreview = c => { if (c.type === 'bookmark' && c.bookmark) return `🔖 ${c.bookmark}`; - return c.text || c.description || c.title || ''; + if (c.type === 'post') return c.contentWarning || c.text || ''; + return c.text || c.description || c.title || c.name || ''; }; - const OPINION_MSG_TYPE = { - bookmark: 'bookmarkOpinion', - votes: 'votesOpinion', - transfer: 'transferOpinion', - feed: 'feedOpinion', - image: 'imageOpinion', - audio: 'audioOpinion', - video: 'videoOpinion', - document: 'documentOpinion', - torrent: 'torrentOpinion' - }; + const OPINION_MSG_TYPE = categories.messageTypes; const createVote = async (contentId, category) => { const ssbClient = await openSsb(); @@ -60,17 +61,10 @@ module.exports = ({ cooler }) => { ); }; - const listOpinions = async (filter = 'ALL', category = '') => { + const computeBase = async () => { const ssbClient = await openSsb(); - const userId = ssbClient.id; - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); const tombstoned = buildValidatedTombstoneSet(messages); - const replaces = new Map(); const byId = new Map(); const opinionMsgs = []; @@ -78,16 +72,12 @@ module.exports = ({ cooler }) => { const key = msg.key; const c = msg.value?.content; if (!c) continue; - if (c.type === 'tombstone') { - if (tombstoned.has(c.target)) byId.delete(c.target); - continue; - } + if (c.type === 'tombstone') continue; if (typeof c.type === 'string' && c.type.endsWith('Opinion') && c.target) { opinionMsgs.push({ target: c.target, author: msg.value.author, category: c.category }); continue; } - if (c.opinions && !tombstoned.has(key) && !['task', 'event', 'report'].includes(c.type)) { - if (c.replaces) replaces.set(c.replaces, key); + if (c.opinions && !['task', 'event', 'report'].includes(c.type)) { byId.set(key, { key, value: { @@ -97,38 +87,31 @@ module.exports = ({ cooler }) => { } }); } - if (c.type === 'feed' && !tombstoned.has(key) && !byId.has(key)) { - if (c.replaces) replaces.set(c.replaces, key); + if (c.type === 'feed' && !byId.has(key)) { + byId.set(key, { key, value: { ...msg.value, content: c, preview: getPreview(c) } }); + } + if (isRootBlogPost(c) && !byId.has(key)) { byId.set(key, { key, value: { ...msg.value, content: c, preview: getPreview(c) } }); } } - for (const [oldId, newId] of Array.from(replaces.entries())) { - const oldM = byId.get(oldId); - const newM = byId.get(newId); - if (!oldM) { replaces.delete(oldId); continue; } - if (!newM || String(newM.value.author) !== String(oldM.value.author)) { - replaces.delete(oldId); - byId.delete(newId); + const chunkLookup = longText.lookupIn(longText.indexChunks(messages)); + for (const [key, m] of Array.from(byId.entries())) { + const c = m.value?.content; + if (c?.type === 'post' && longText.hasChunks(c)) { + const content = longText.resolveField(c, 'text', m.value.author, chunkLookup); + byId.set(key, { key, value: { ...m.value, content, preview: getPreview(content) } }); } } - for (const replacedId of replaces.keys()) { - byId.delete(replacedId); + const vg = versionGroups(Array.from(byId.values()).map(versionEntry)); + for (const key of Array.from(byId.keys())) { + if (!vg.roots.has(vg.rootOf(key)) || vg.tipOf(key) !== key || tombstoned.has(key)) byId.delete(key); } - const tipToChain = new Map(); - for (const [oldId, newId] of replaces.entries()) { - let tip = newId; let g = 0; - while (replaces.has(tip) && g++ < 100000) tip = replaces.get(tip); - if (!tipToChain.has(tip)) tipToChain.set(tip, new Set([tip])); - tipToChain.get(tip).add(oldId); tipToChain.get(tip).add(newId); - } - const idToTip = new Map(); - for (const [tip, chain] of tipToChain.entries()) for (const id of chain) idToTip.set(id, tip); const opinionByTip = new Map(); for (const op of opinionMsgs) { - const tip = idToTip.get(op.target) || op.target; + const tip = vg.tipOf(op.target); if (!opinionByTip.has(tip)) opinionByTip.set(tip, { opinions: {}, voters: new Set() }); const agg = opinionByTip.get(tip); if (agg.voters.has(op.author)) continue; @@ -147,6 +130,12 @@ module.exports = ({ cooler }) => { } let filtered = Array.from(byId.values()).filter(m => validTypes.includes(m.value?.content?.type)); + filtered = filtered.filter(m => { + const c = m.value?.content; + if (c?.type !== 'schoolCourse') return true; + if (c.encryptedPayload) return false; + return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE'; + }); const blobTypes = ['document', 'image', 'audio', 'video']; const blobCheckCache = new Map(); @@ -226,6 +215,25 @@ module.exports = ({ cooler }) => { } filtered = Array.from(bySig.values()); + return filtered.filter(m => { + const ops = m.value?.content?.opinions || {}; + return Object.values(ops).reduce((acc, x) => acc + (Number(x) || 0), 0) > 0; + }); + }; + + const baseItems = () => { + const store = requestScope.getStore(); + if (store && store.opinionsBase) return store.opinionsBase; + const pending = computeBase(); + if (store) store.opinionsBase = pending; + return pending; + }; + + const listOpinions = async (filter = 'ALL', category = '') => { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + let filtered = (await baseItems()).slice(); + if (filter === 'MINE') { filtered = filtered.filter(m => m.value.author === userId); } else if (filter === 'RECENT') { diff --git a/src/models/pads_model.js b/src/models/pads_model.js index 6390e46..3a9c310 100644 --- a/src/models/pads_model.js +++ b/src/models/pads_model.js @@ -3,6 +3,7 @@ const crypto = require("crypto") const fs = require("fs") const { buildValidatedTombstoneSet } = require('./tombstone_validator') const { collabContent, openInviteOf } = require('../backend/collab_content') +const { readTyped, memoIndex } = require('./typed_log') const padCollab = collabContent({ membersField: 'members', undecField: 'undecryptable', contentFields: ['title', 'deadline', 'status'], listFields: ['tags', 'invites'] }) const path = require("path") const { getConfig } = require("../configs/config-manager.js") @@ -59,6 +60,18 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (ownCrypto) return lookupKey(rootId) try { return JSON.parse(fs.readFileSync(getLegacyKeyringPath(), "utf8"))[rootId] || null } catch (_) { return null } } + const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null) + const inviteMatches = (inv, token) => { + if (!token) return false + if (typeof inv === "string") return inv === token + if (!inv || typeof inv !== "object") return false + if (typeof inv.code === "string" && inv.code === token) return true + if (typeof inv.ch === "string" && (inv.ch === token || hashCode(token, inv.salt) === inv.ch)) return true + return false + } + const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "") + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } + const padKeys = (rootId) => (ownCrypto && typeof ownCrypto.getKeys === "function" ? ownCrypto.getKeys(rootId) : []).concat(tribeCrypto && typeof tribeCrypto.getKeys === "function" ? tribeCrypto.getKeys(rootId) : []) const setPadKey = (rootId, keyHex) => { migrateLegacyKeyring() if (ownCrypto) { ownCrypto.setKey(rootId, keyHex, 1); return } @@ -117,29 +130,37 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) let tagsRaw = "" try { deadline = c.deadline ? tryDecryptField(c.deadline, k) : "" } catch (_) {} try { tagsRaw = c.tags ? tryDecryptField(c.tags, k) : "" } catch (_) {} - const ensureMemberKeys = async (ssbClient, messages, items) => { - if (!tribeCrypto) return - const distributed = new Map() + return { title: safeText(title), deadline, tags: normalizeTags(tagsRaw) } + } catch (_) {} + } + return null + } + + const ensureMemberKeys = async (ssbClient, messages, items) => { + if (!tribeCrypto || !ownCrypto) return + const leavesByRoot = new Map() for (const m of messages) { const c = m.value && m.value.content - if (!c || c.type !== "tribe-keys" || !c.tribeId) continue - const mk = c.memberKeys - if (!mk || typeof mk !== "object") continue - if (!distributed.has(c.tribeId)) distributed.set(c.tribeId, new Set()) - for (const id of Object.keys(mk)) distributed.get(c.tribeId).add(id) + if (!c || c.type !== "padMember" || c.on !== false || !c.target || !c.member) continue + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []) + leavesByRoot.get(c.target).push({ member: c.member, author: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }) } const ssbKeys = require("../server/node_modules/ssb-keys") for (const item of (Array.isArray(items) ? items : [])) { - if (!item || item.undecryptable) continue + if (!item || item.undecryptable || item.author !== ssbClient.id) continue const rootId = item.rootId - if (!rootId) continue + if (!rootId || !lookupKey(rootId)) continue + const members = Array.isArray(item.members) ? item.members : [] + const leaves = (leavesByRoot.get(rootId) || []).filter(l => l.author === l.member || l.author === item.author) + const plan = ownCrypto.keyPlan({ rootId, ownerId: item.author, gen: lookupGen(rootId), members, messages, leaves }) + if (plan.rotate) { + await rotatePadKey(rootId, members) + continue + } + if (!plan.missing.length) continue const key = lookupKey(rootId) - if (!key) continue - const have = distributed.get(rootId) || new Set() - const missing = (Array.isArray(item.members) ? item.members : []).filter(m => m && m !== ssbClient.id && !have.has(m)) - if (!missing.length) continue const memberKeys = {} - for (const m of missing) { + for (const m of plan.missing) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue @@ -149,12 +170,6 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } } - return { title: safeText(title), deadline, tags: normalizeTags(tagsRaw) } - } catch (_) {} - } - return null - } - const encryptForInvite = (padKeyHex, code, saltHex) => { const salt = saltHex ? Buffer.from(saltHex, "hex") : Buffer.from(INVITE_SALT) const derived = crypto.scryptSync(code, salt, 32) @@ -189,11 +204,10 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (!existing) return const newKey = crypto.randomBytes(32).toString("hex") const newGen = ownCrypto.addNewKey(rootId, newKey) - if (!Array.isArray(remainingMembers) || !remainingMembers.length) return const ssbClient = await openSsb() const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - for (const m of remainingMembers) { + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { @@ -210,9 +224,12 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const ssbKeys = require("../server/node_modules/ssb-keys") const config = require("../server/ssb_config") const msgs = await readAll(ssbClient) + const idx = buildIndex(msgs) + const trusted = (root, author) => { const n = idx.nodes.get(root); return !!author && !!n && (author === n.author || author === ssbClient.id) } for (const m of msgs) { const c = m.value && m.value.content if (!c || c.type !== "tribe-keys") continue + if (!trusted(c.tribeId, m.value.author)) continue const memberKeys = c.memberKeys if (!memberKeys || typeof memberKeys !== "object") continue const boxed = memberKeys[ssbClient.id] @@ -226,18 +243,15 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } catch (_) {} } - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ) + const PAD_TYPES = ["pad", "padMember", "padEntry", "tribe-keys", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, PAD_TYPES, { limit: logLimit }) const buildIndex = (messages) => { const tomb = new Set() const nodes = new Map() const parent = new Map() const child = new Map() - const strictParent = new Map() - const strictChild = new Map() const authorByKey = new Map() const tombRequests = [] const memberMsgs = [] @@ -248,19 +262,14 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const c = v.content if (!c) continue if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue } - if (c.type === "padMember" && c.target) { memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "" }); continue } + if (c.type === "padMember" && c.target) { memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "", keyProof: typeof c.keyProof === "string" ? c.keyProof : "" }); continue } if (c.type === "pad") { - nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) authorByKey.set(k, v.author) if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) } } } - for (const [k, node] of nodes) { - const t = node.c.replaces - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) { strictParent.set(k, t); strictChild.set(t, k) } } - } - for (const t of tombRequests) { const targetAuthor = authorByKey.get(t.target) if (targetAuthor && t.author === targetAuthor) tomb.add(t.target) @@ -268,53 +277,86 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const rootOf = (id) => { let cur = id, g = 0; while (parent.has(cur) && g++ < 100000) cur = parent.get(cur); return cur } const tipOf = (id) => { let cur = id, g = 0; while (child.has(cur) && g++ < 100000) cur = child.get(cur); return cur } - const strictRootOf = (id) => { let cur = id, g = 0; while (strictParent.has(cur) && g++ < 100000) cur = strictParent.get(cur); return cur } - const contentTipOf = (root) => { - const rn = nodes.get(root) - if (!rn) return root - let cur = root, best = root, g = 0 - const seen = new Set() - while (child.has(cur) && !seen.has(cur) && g++ < 100000) { - seen.add(cur) - const next = child.get(cur) - const n = nodes.get(next) - if (!n) break - if (n.author === rn.author && !tomb.has(next)) best = next - cur = next - } - return best + const rootMemo = new Map() + const groupRootOf = (id) => { + if (rootMemo.has(id)) return rootMemo.get(id) + let cur = id, g = 0 + while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur) + rootMemo.set(id, cur) + return cur } + const ownerTip = new Map() + for (const [k, n] of nodes) { + const r = groupRootOf(k) + const rn = nodes.get(r) + if (!rn || n.author !== rn.author) continue + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k) + } + const contentTipOf = (root) => ownerTip.get(root) || root const roots = new Set() - for (const id of nodes.keys()) roots.add(strictRootOf(id)) + for (const id of nodes.keys()) roots.add(groupRootOf(id)) + + const invitesOf = (root) => { + const out = [] + for (const id of [root, contentTipOf(root)]) { const n = nodes.get(id); if (n && Array.isArray(n.c.invites)) out.push(...n.c.invites) } + return out + } + const selfJoinAllowed = (root, mm) => { + const oc = (nodes.get(root) || {}).c || {} + if (oc.encrypted !== true || oc.tribeId) return true + const invites = invitesOf(root) + if (invites.some(inv => inv && typeof inv === "object" && inv.public === true)) return true + if (mm.keyProof && padKeys(root).some(k => keyProofFor(k, mm.member) === mm.keyProof)) return true + return !!mm.code && invites.some(inv => typeof inv === "string" ? inv === mm.code : !!(inv && typeof inv === "object" && typeof inv.code === "string" && inv.code === mm.code)) + } const memberByRoot = new Map() const consumedByRoot = new Map() + const codeClaimsByRoot = new Map() for (const mm of memberMsgs) { if (!nodes.has(mm.target)) continue - const r = strictRootOf(mm.target) + const r = groupRootOf(mm.target) const ownerAuthor = nodes.get(r) && nodes.get(r).author if (!ownerAuthor) continue const self = mm.member === mm.author const byOwner = mm.author === ownerAuthor if (!self && !byOwner) continue if (!mm.member) continue - if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) - const m2 = memberByRoot.get(r) - const p = m2.get(mm.member) - if (!p || mm.ts >= p.ts) m2.set(mm.member, { on: mm.on, ts: mm.ts }) + if (mm.on && mm.code) { + if (!codeClaimsByRoot.has(r)) codeClaimsByRoot.set(r, []) + codeClaimsByRoot.get(r).push({ code: mm.code, member: mm.member, keyProof: mm.keyProof, byOwner }) + } + if (self && !byOwner && mm.on && !selfJoinAllowed(r, mm)) continue if (mm.on && mm.code) { if (!consumedByRoot.has(r)) consumedByRoot.set(r, new Set()) consumedByRoot.get(r).add(mm.code) } + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const m2 = memberByRoot.get(r) + const p = m2.get(mm.member) + if (!p || mm.ts >= p.ts) m2.set(mm.member, { on: mm.on, ts: mm.ts }) } + const codeClaims = (root, codes) => (codeClaimsByRoot.get(root) || []).filter(c => codes.includes(c.code)) + const isCodeConsumed = (root, code) => { if (!code) return false const set = consumedByRoot.get(root) return !!(set && set.has(code)) } + for (const [k, n] of nodes) { + const r = groupRootOf(k) + const rn = nodes.get(r) + if (!rn || n.author === rn.author || !Array.isArray(n.c.members) || !selfJoinAllowed(r, { member: n.author })) continue + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const m2 = memberByRoot.get(r) + const p = m2.get(n.author) + if (!p || n.ts >= p.ts) m2.set(n.author, { on: n.c.members.includes(n.author), ts: n.ts }) + } + const resolveMembers = (root) => { const ownerNode = nodes.get(root) const oc = ownerNode ? ownerNode.c : {} @@ -326,7 +368,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const tipByRoot = new Map() for (const r of roots) tipByRoot.set(r, contentTipOf(r)) - return { tomb, nodes, parent, child, rootOf, tipOf, strictRootOf, contentTipOf, tipByRoot, resolveMembers, isCodeConsumed } + return { tomb, nodes, parent, child, rootOf, tipOf, groupRootOf, contentTipOf, tipByRoot, resolveMembers, isCodeConsumed, codeClaims } } const decryptPadFields = (c, rootId, tribeKeys) => { @@ -358,7 +400,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) status: c.status || "OPEN", deadline, tags, - author: c.author || node.author, + author: node.author, members: Array.isArray(members) ? members : (Array.isArray(c.members) ? c.members : []), invites: Array.isArray(c.invites) ? c.invites : [], createdAt: c.createdAt || new Date(node.ts).toISOString(), @@ -423,7 +465,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - const root = idx.strictRootOf(id) + const root = idx.groupRootOf(id) const tip = idx.contentTipOf(root) if (idx.tomb.has(tip)) throw new Error("Not found") return root @@ -433,7 +475,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - const root = idx.strictRootOf(id) + const root = idx.groupRootOf(id) const tip = idx.contentTipOf(root) if (idx.tomb.has(tip)) throw new Error("Not found") return tip @@ -518,7 +560,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) return new Promise(async (resolve, reject) => { ssbClient.get(tipId, async (err, item) => { if (err || !item?.content) return reject(new Error("Pad not found")) - if (item.content.author !== userId) return reject(new Error("Not the author")) + if (item.author !== userId) return reject(new Error("Not the author")) const c = item.content const isEncrypted = c.encrypted === true let keyHex = null @@ -564,7 +606,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) return new Promise(async (resolve, reject) => { ssbClient.get(tipId, async (err, item) => { if (err || !item?.content) return reject(new Error("Pad not found")) - if (item.content.author !== userId) return reject(new Error("Not the author")) + if (item.author !== userId) return reject(new Error("Not the author")) const c = item.content let keyHex = null let usesTribeKey = false @@ -599,13 +641,11 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (!pad) throw new Error("Pad not found") if (pad.author === userId) throw new Error("Author cannot leave their own pad") if (!Array.isArray(pad.members) || !pad.members.includes(userId)) return - const members = pad.members.filter(m => m !== userId) const rootId = await this.resolveRootId(padId) await new Promise((resolve, reject) => { const content = { type: "padMember", target: rootId, member: userId, on: false, createdAt: new Date().toISOString() } ssbClient.publish(content, (e) => e ? reject(e) : resolve()) }) - try { await rotatePadKey(rootId, members) } catch (_) {} }, async ingestKeys() { await ingestOwnTribeKeys() }, @@ -615,17 +655,11 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const live = new Set() const tomb = buildValidatedTombstoneSet(messages) - for (const m of messages) { - const c = m.value && m.value.content - if (!c) continue - if (c.type === "pad") live.add(m.key) - } const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (!live.has(rid) || tomb.has(rid)) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } @@ -646,7 +680,8 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const key = tryDecryptPublicInviteKey(c.invites) if (key) setPadKey(rootId, key) } - const content = { type: "padMember", target: rootId, member: feedId, on: true, createdAt: new Date().toISOString(), ...(typeof consumedCode === "string" && consumedCode ? { code: consumedCode } : {}) } + const proofKey = feedId === ssbClient.id && c.encrypted === true && !c.tribeId ? getPadKey(rootId) : null + const content = { type: "padMember", target: rootId, member: feedId, on: true, createdAt: new Date().toISOString(), ...(typeof consumedCode === "string" && consumedCode ? { code: consumedCode } : {}), ...(proofKey ? { keyProof: keyProofFor(proofKey, feedId) } : {}) } ssbClient.publish(content, (e, res) => e ? reject(e) : resolve(res)) }) }) @@ -659,7 +694,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) return new Promise((resolve, reject) => { ssbClient.get(tipId, (err, item) => { if (err || !item?.content) return reject(new Error("Pad not found")) - if (item.content.author !== userId) return reject(new Error("Not the author")) + if (item.author !== userId) return reject(new Error("Not the author")) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } ssbClient.publish(tombstone, (e) => e ? reject(e) : resolve()) }) @@ -669,8 +704,8 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) async getPadById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) - const root = idx.strictRootOf(id) + const idx = memoIndex('pads', messages, buildIndex) + const root = idx.groupRootOf(id) const tip = idx.contentTipOf(root) if (idx.tomb.has(tip)) return null const node = idx.nodes.get(tip) @@ -709,12 +744,13 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const code = crypto.randomBytes(INVITE_BYTES).toString("hex") let invite = code const pubFlag = opts.public ? { public: true } : {} + const inviteSalt = generateInviteSalt() if (keyHex) { - const inviteSalt = generateInviteSalt() const ek = encryptForInvite(keyHex, code, inviteSalt) - invite = { code, ek, salt: inviteSalt, ...pubFlag } + invite = opts.public ? { code, ek, salt: inviteSalt, ...pubFlag } : { ch: hashCode(code, inviteSalt), ek, salt: inviteSalt } + } else { + invite = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt), salt: inviteSalt } } - if (opts.public && typeof invite !== "object") invite = { code, public: true } const invites = [...pad.invites, invite] await this.updatePadById(padId, { invites }) return code @@ -740,18 +776,18 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (pad.author !== userId) throw new Error("Only the author can remove invites") const invites = (Array.isArray(pad.invites) ? pad.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)) await this.updatePadById(padId, { invites }) + if (!pad.tribeId) await rotatePadKey(await this.resolveRootId(padId), pad.members) }, async joinByInvite(code) { const ssbClient = await openSsb() const userId = ssbClient.id - const pads = await this.listAll() + const pads = await collectPads(buildIndex(await readAll(ssbClient))) let matchedPad = null let matchedInvite = null for (const p of pads) { for (const inv of p.invites) { - if (typeof inv === "string" && inv === code) { matchedPad = p; matchedInvite = inv; break } - if (typeof inv === "object" && inv.code === code) { matchedPad = p; matchedInvite = inv; break } + if (inviteMatches(inv, code)) { matchedPad = p; matchedInvite = inv; break } } if (matchedPad) break } @@ -759,17 +795,19 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (matchedPad.members.includes(userId)) throw new Error("Already a member") const isPublic = typeof matchedInvite === "object" && matchedInvite.public === true let resolvedRootId = await this.resolveRootId(matchedPad.rootId) + const token = inviteToken(matchedInvite) || code + let padKey = null + if (typeof matchedInvite === "object" && matchedInvite.ek) padKey = decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) if (!isPublic) { const messages = await readAll(ssbClient) const idx = buildIndex(messages) - if (idx.isCodeConsumed(resolvedRootId, code)) throw new Error("Invite already used") + if (idx.isCodeConsumed(resolvedRootId, token) || idx.isCodeConsumed(resolvedRootId, code)) throw new Error("Invite already used") + const claims = idx.codeClaims(resolvedRootId, [token, code]).filter(c => c.member !== userId) + const spent = padKey ? claims.some(c => c.byOwner || keyProofFor(padKey, c.member) === c.keyProof) : claims.length > 0 + if (spent) throw new Error("Invite already used") } - let padKey = null - if (typeof matchedInvite === "object" && matchedInvite.ek) { - padKey = decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) - setPadKey(resolvedRootId, padKey) - } - await this.addMemberToPad(matchedPad.rootId, userId, isPublic ? "" : code) + if (padKey) setPadKey(resolvedRootId, padKey) + await this.addMemberToPad(matchedPad.rootId, userId, isPublic ? "" : token) if (tribeCrypto && padKey && resolvedRootId) { try { const ssbKeys = require("../server/node_modules/ssb-keys") @@ -860,7 +898,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } entries.push({ key: m.key, - author: c.author || v.author, + author: v.author, text, createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString() }) diff --git a/src/models/panicmode_model.js b/src/models/panicmode_model.js index 95933fc..d6edcdc 100644 --- a/src/models/panicmode_model.js +++ b/src/models/panicmode_model.js @@ -1,12 +1,10 @@ -const os = require('os'); const fs = require('fs'); const path = require('path'); module.exports = { removeSSB: async () => { try { - const homeDir = os.homedir(); - const ssbPath = path.join(homeDir, '.ssb'); + const ssbPath = require('../server/ssb_config').path; await fs.promises.rm(ssbPath, { recursive: true, force: true }); } catch (error) { throw new Error("Error deleting data: " + error.message); diff --git a/src/models/parliament_model.js b/src/models/parliament_model.js index 83dc837..ec18860 100644 --- a/src/models/parliament_model.js +++ b/src/models/parliament_model.js @@ -2,8 +2,21 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); +const { buildVoteResults } = require('../backend/vote_tally'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const PARLIAMENT_TYPES = [ + 'parliamentCandidature', 'parliamentCandidatureVote', 'parliamentTerm', + 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw', + 'tribe', 'about', 'tombstone' +]; +const VOTE_LOG_TYPES = ['votes', 'votesVote', 'tombstone']; +const PROPOSER_TYPES = new Set(['parliamentCandidature', 'parliamentProposal', 'parliamentRevocation']); +const TRIBE_PARLIAMENT_TYPES = [ + 'tribeParliamentTerm', 'tribeParliamentCandidature', 'tribeParliamentRule', + 'parliamentCandidature', 'tombstone' +]; const TERM_DAYS = 60; const PROPOSAL_DAYS = 7; const REVOCATION_DAYS = 15; @@ -12,6 +25,138 @@ const QUORUM_RATIO = 0.25; const VOTE_METHODS = new Set(['DEMOCRACY', 'ANARCHY', 'MAJORITY', 'MINORITY']); const METHODS = ['DEMOCRACY', 'MAJORITY', 'MINORITY', 'DICTATORSHIP', 'KARMATOCRACY']; const FEED_ID_RE = /^@.+\.ed25519$/; +const TERM_EPOCH = Date.UTC(2020, 0, 1); +const TERM_SPAN_MS = TERM_DAYS * 86400000; + +const termWindowFor = (at = Date.now()) => { + const ms = at instanceof Date ? at.getTime() : Number(at); + const base = Number.isFinite(ms) ? ms : Date.now(); + const index = Math.floor((base - TERM_EPOCH) / TERM_SPAN_MS); + const startMs = TERM_EPOCH + index * TERM_SPAN_MS; + return { + cycle: index, + startAt: new Date(startMs).toISOString(), + endAt: new Date(startMs + TERM_SPAN_MS).toISOString() + }; +}; + +const HEMICYCLE_MAX_SEATS = 400; + +const hemicycleQuorum = (n) => Math.max(PROPOSAL_QUORUM, Math.ceil(Number(n || 0) * QUORUM_RATIO)); + +const passThreshold = (method, n) => { + const m = String(method || '').toUpperCase(); + const total = Number(n || 0); + if (m === 'DEMOCRACY' || m === 'ANARCHY') return Math.floor(total / 2) + 1; + if (m === 'MAJORITY') return Math.ceil(total * 0.8); + if (m === 'MINORITY') return Math.ceil(total * 0.2); + return null; +}; + +const buildHemicycle = ({ census = [], candidatures = [], powerType = 'none', powerId = null, leaderId = null, method = 'ANARCHY', powerMembers = [], houses = {} } = {}) => { + const people = []; + const seen = new Set(); + for (const p of census) { + const id = typeof p === 'string' ? p : (p && p.id); + if (!id || seen.has(id)) continue; + seen.add(id); + people.push({ id, name: (p && typeof p === 'object' && p.name) ? String(p.name) : '' }); + } + const n = people.length; + const m = String(method || 'ANARCHY').toUpperCase(); + const power = new Set((Array.isArray(powerMembers) ? powerMembers : []).map(String)); + const leader = leaderId || (powerType === 'inhabitant' ? powerId : null); + + const cands = (Array.isArray(candidatures) ? candidatures : []).map(c => ({ + id: String(c.id || ''), + label: String(c.label || c.targetTitle || c.targetId || c.candidateId || ''), + href: c.href || null, + targetType: c.targetType || (c.candidateId ? 'inhabitant' : 'inhabitant'), + targetId: String(c.targetId || c.candidateId || ''), + method: String(c.method || '').toUpperCase(), + voters: Array.from(new Set((Array.isArray(c.voters) ? c.voters : []).map(String))).filter(v => v !== String(c.targetId || c.candidateId || '')) + })); + const isGovernment = (c) => powerType !== 'none' && powerId && c.targetId === String(powerId); + const government = cands.find(isGovernment) || null; + const opposition = cands.filter(c => c !== government && c.voters.some(v => seen.has(v))); + opposition.sort((a, b) => b.voters.length - a.voters.length || a.label.localeCompare(b.label)); + + const blocOf = new Map(); + if (leader && seen.has(String(leader)) && powerType !== 'none') blocOf.set(String(leader), 'gov'); + if (powerType === 'tribe') for (const id of power) if (seen.has(id)) blocOf.set(id, 'gov'); + if (government) for (const v of government.voters) if (seen.has(v) && !blocOf.has(v)) blocOf.set(v, 'gov'); + opposition.forEach((c, i) => { for (const v of c.voters) if (seen.has(v) && !blocOf.has(v)) blocOf.set(v, `opp${i}`); }); + + const seats = people.map(p => ({ + id: p.id, + name: p.name, + bloc: blocOf.get(p.id) || 'none', + leader: !!leader && p.id === String(leader), + inPower: power.has(p.id), + house: houses && houses[p.id] ? String(houses[p.id]) : null + })); + const order = { gov: 0 }; + opposition.forEach((c, i) => { order[`opp${i}`] = i + 1; }); + order.none = opposition.length + 1; + seats.sort((a, b) => (order[a.bloc] - order[b.bloc]) || (b.leader - a.leader) || a.id.localeCompare(b.id)); + + const count = (key) => seats.filter(s => s.bloc === key).length; + const blocs = []; + if (government) blocs.push({ key: 'gov', kind: 'government', label: government.label, href: government.href, targetId: government.targetId, method: government.method, count: count('gov') }); + opposition.forEach((c, i) => blocs.push({ key: `opp${i}`, kind: 'opposition', label: c.label, href: c.href, targetId: c.targetId, method: c.method, count: count(`opp${i}`) })); + blocs.push({ key: 'none', kind: 'abstention', label: '', href: null, count: count('none') }); + + const houseCounts = {}; + for (const s of seats) if (s.house) houseCounts[s.house] = (houseCounts[s.house] || 0) + 1; + + return { + population: n, + method: m, + powerType, + powerId: powerId || null, + leaderId: leader || null, + seats, + blocs: blocs.filter(b => b.count > 0 || b.kind === 'government'), + houses: houseCounts, + thresholds: { quorum: hemicycleQuorum(n), pass: passThreshold(m, n) }, + seatUnit: n > HEMICYCLE_MAX_SEATS ? Math.ceil(n / HEMICYCLE_MAX_SEATS) : 1 + }; +}; + +const seatPositions = (n, { width = 720, height = 380 } = {}) => { + if (n <= 0) return []; + const cx = width / 2; + const cy = height - 20; + const outer = Math.min(width / 2 - 16, height - 60); + let rows = 1; + const rowGap = () => outer / (rows + 1.5); + const capacity = (r) => { + let total = 0; + for (let i = 0; i < r; i++) { const radius = outer - i * rowGap(); total += Math.max(1, Math.floor((Math.PI * radius) / (rowGap() * 0.95))); } + return total; + }; + while (capacity(rows) < n && rows < 30) rows += 1; + const gap = rowGap(); + const radii = []; + for (let i = 0; i < rows; i++) radii.push(outer - i * gap); + const per = radii.map(r => Math.max(1, Math.floor((Math.PI * r) / (gap * 0.95)))); + const totalCap = per.reduce((a, b) => a + b, 0); + const alloc = per.map(c => Math.floor((c / totalCap) * n)); + let left = n - alloc.reduce((a, b) => a + b, 0); + for (let i = 0; left > 0; i = (i + 1) % rows) { if (alloc[i] < per[i]) { alloc[i] += 1; left -= 1; } } + const pts = []; + radii.forEach((r, row) => { + const k = alloc[row]; + for (let j = 0; j < k; j++) { + const angle = k === 1 ? Math.PI / 2 : Math.PI - (Math.PI * (j + 0.5)) / k; + pts.push({ angle, x: cx + r * Math.cos(angle), y: cy - r * Math.sin(angle), row }); + } + }); + pts.sort((a, b) => b.angle - a.angle || a.row - b.row); + const radius = Math.max(3, Math.min(11, gap * 0.4)); + return pts.map(p => ({ x: Number(p.x.toFixed(1)), y: Number(p.y.toFixed(1)), angle: p.angle, r: Number(radius.toFixed(1)) })); +}; + module.exports = ({ cooler, services = {} }) => { let ssb; @@ -19,6 +164,7 @@ module.exports = ({ cooler, services = {} }) => { const CACHE_MS = 250; let logCache = { at: 0, arr: null }; + let voteCache = { at: 0, results: null, tomb: null }; let electionInFlight = null; let sweepInFlight = null; @@ -53,6 +199,7 @@ module.exports = ({ cooler, services = {} }) => { ssbClient.publish(content, (e, r) => (e ? reject(e) : resolve(r))) ); logCache = { at: 0, arr: null }; + voteCache = { at: 0, results: null, tomb: null }; return res; } @@ -60,49 +207,75 @@ module.exports = ({ cooler, services = {} }) => { const now = Date.now(); if (logCache.arr && now - logCache.at < CACHE_MS) return logCache.arr; const ssbClient = await openSsb(); - const arr = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, out) => (err ? rej(err) : res(out || []))) - ); - }); + const arr = await readTyped(ssbClient, PARLIAMENT_TYPES, { limit: logLimit }); logCache = { at: now, arr }; return arr; } - function listByTypeFromMsgs(msgs, type) { + function chainIndex(msgs, type) { const tomb = buildValidatedTombstoneSet(msgs); - const rep = new Map(); - const children = new Map(); - const map = new Map(); - + const nodes = new Map(); for (const m of msgs || []) { - const k = m.key; const v = m.value || {}; const c = v.content; - if (!c) continue; -if (c.type === type) { - if (c.replaces) { - const oldId = c.replaces; - const ts = normMs(v.timestamp || m.timestamp || Date.now()); - const prev = rep.get(oldId); - if (!prev || ts > prev.ts) rep.set(oldId, { id: k, ts }); - if (!children.has(oldId)) children.set(oldId, new Set()); - children.get(oldId).add(k); - } - map.set(k, { ...c, id: k }); - } + if (!c || c.type !== type) continue; + nodes.set(m.key, { key: m.key, author: v.author, ts: normMs(v.timestamp || m.timestamp || Date.now()), c }); } + const consistent = (n) => { + if (PROPOSER_TYPES.has(type)) return n.c.proposer === n.author; + if (type === 'parliamentTerm') return !n.c.createdBy || n.c.createdBy === n.author; + return true; + }; + const valid = new Map(); + const settle = (key) => { + const path = []; + const seen = new Set(); + let cur = key; + while (!valid.has(cur)) { + const n = nodes.get(cur); + if (!n || !consistent(n)) { valid.set(cur, false); break; } + const parent = n.c.replaces ? nodes.get(n.c.replaces) : null; + if (!parent) { valid.set(cur, true); break; } + if (parent.author !== n.author || seen.has(cur)) { valid.set(cur, false); break; } + seen.add(cur); + path.push(cur); + cur = parent.key; + } + for (let i = path.length - 1; i >= 0; i--) { + if (!valid.has(path[i])) valid.set(path[i], valid.get(nodes.get(path[i]).c.replaces) === true); + } + return valid.get(key) === true; + }; + const next = new Map(); + for (const n of nodes.values()) { + if (!n.c.replaces || !nodes.has(n.c.replaces) || !settle(n.key)) continue; + const prev = next.get(n.c.replaces); + if (!prev || n.ts > nodes.get(prev).ts) next.set(n.c.replaces, n.key); + } + const tipOf = (k) => { let x = k, g = 0; while (next.has(x) && g++ < 100000) x = next.get(x); return x; }; + const rootOf = (k) => { let x = k, g = 0; while (nodes.has(x) && nodes.has(nodes.get(x).c.replaces) && g++ < 100000) x = nodes.get(x).c.replaces; return x; }; + const items = []; + for (const n of nodes.values()) { + if ((n.c.replaces && nodes.has(n.c.replaces)) || !settle(n.key)) continue; + const tip = tipOf(n.key); + if (tomb.has(tip)) continue; + items.push({ ...nodes.get(tip).c, id: tip }); + } + const resolve = (k) => (nodes.has(k) && settle(k) ? tipOf(rootOf(k)) : null); + const authorOf = (k) => (nodes.has(k) ? nodes.get(k).author : null); + return { items, resolve, authorOf }; + } - for (const oldId of rep.keys()) map.delete(oldId); - for (const [oldId, kids] of children.entries()) { - const winner = rep.get(oldId)?.id || null; - for (const kid of kids) { - if (kid !== winner) map.delete(kid); - } - } - for (const tId of tomb) map.delete(tId); - return [...map.values()]; + function listByTypeFromMsgs(msgs, type) { + const items = chainIndex(msgs, type).items; + return type === 'parliamentProposal' ? items.map(withCampaignRef) : items; + } + + function withCampaignRef(p) { + if (!p || p.campaignId) return p; + const m = String(p.description || '').match(/\n*\s*(\d+) signatures: \/campaigns\/(%[^\s]+\.sha256)\s*$/); + if (!m) return p; + return { ...p, campaignId: m[2], signatures: Number(m[1]) || 0, description: String(p.description || '').slice(0, m.index).trim() }; } async function listByType(type) { @@ -198,11 +371,14 @@ if (c.type === type) { const s = String(candidateInput || '').trim(); if (!s) return null; const tribes = await listTribesAny(); - const t = tribes.find(tr => + let t = tribes.find(tr => tr.id === s || (tr.title && tr.title.toLowerCase() === s.toLowerCase()) || (tr.name && tr.name.toLowerCase() === s.toLowerCase()) ); + if (!t && s.startsWith('%') && services.tribes && services.tribes.getTribeById) { + try { t = await services.tribes.getTribeById(s); } catch (_) { t = null; } + } if (t) { return { type: 'tribe', id: t.id, title: t.title || t.name || t.id, members: ensureArray(t.members) }; } @@ -228,8 +404,9 @@ if (c.type === type) { let n = 0; try { if (services.inhabitants?.listInhabitants) { - const list = await services.inhabitants.listInhabitants({ filter: 'all', includeInactive: true }); - n = Array.isArray(list) ? list.length : 0; + n = typeof services.inhabitants.countInhabitants === 'function' + ? Number(await services.inhabitants.countInhabitants()) || 0 + : ((await services.inhabitants.listInhabitants({ filter: 'all', includeInactive: true })) || []).length; } } catch {} _inhCache = { at: now, n }; @@ -251,7 +428,13 @@ if (c.type === type) { async function listCandidaturesOpenRaw() { const all = await listByType('parliamentCandidature'); - const filtered = all.filter(c => (c.status || 'OPEN') === 'OPEN'); + const window = termWindowFor(Date.now()); + const from = new Date(window.startAt).getTime(); + const filtered = all.filter(c => { + if ((c.status || 'OPEN') !== 'OPEN') return false; + const created = new Date(c.createdAt).getTime(); + return Number.isFinite(created) ? created >= from : true; + }); return filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } @@ -320,9 +503,186 @@ if (c.type === type) { return enriched; } + function supportFor(msgs) { + const cands = chainIndex(msgs, 'parliamentCandidature'); + const byId = new Map(cands.items.map(c => [c.id, c])); + const votes = []; + for (const m of msgs || []) { + const v = m.value || {}; + const c = v.content; + if (!c || c.type !== 'parliamentCandidatureVote' || typeof c.target !== 'string' || !v.author) continue; + const id = cands.resolve(c.target); + const cand = id ? byId.get(id) : null; + if (!cand) continue; + if (cand.targetType === 'inhabitant' && String(cand.targetId) === String(v.author)) continue; + votes.push({ cand, voter: v.author, ts: normMs(v.timestamp || m.timestamp || 0), seq: Number(v.sequence) || 0 }); + } + votes.sort((a, b) => (a.ts - b.ts) || (a.seq - b.seq)); + return (term) => { + const from = new Date(term.startAt).getTime(); + const to = new Date(term.endAt).getTime(); + const counted = new Map(); + for (const x of votes) { + if (counted.has(x.voter)) continue; + const created = new Date(x.cand.createdAt).getTime(); + if (!Number.isFinite(created) || created < from || created >= to) continue; + counted.set(x.voter, x.cand); + } + const perCand = new Map(); + for (const cand of counted.values()) perCand.set(cand, (perCand.get(cand) || 0) + 1); + let best = 0; + for (const [cand, n] of perCand) { + if (cand.targetType !== term.powerType || String(cand.targetId) !== String(term.powerId)) continue; + if (String(cand.method || '').toUpperCase() !== String(term.method || '').toUpperCase()) continue; + if (n > best) best = n; + } + return { votes: best, total: counted.size }; + }; + } + + function termShapeOk(t) { + const s = new Date(t && t.startAt).getTime(); + const e = new Date(t && t.endAt).getTime(); + if (!Number.isFinite(s) || !Number.isFinite(e) || e <= s || s > Date.now()) return false; + const window = termWindowFor(s); + if (new Date(window.startAt).getTime() === s && new Date(window.endAt).getTime() === e) return true; + return e <= Date.now() && Math.abs(e - s - TERM_SPAN_MS) <= 86400000; + } + + function validTermsFrom(msgs) { + const supportOf = supportFor(msgs); + const out = []; + for (const t of chainIndex(msgs, 'parliamentTerm').items) { + const method = String(t.method || '').toUpperCase(); + if (!METHODS.includes(method) || !termShapeOk(t)) continue; + const inhabitant = t.powerType === 'inhabitant' && FEED_ID_RE.test(String(t.powerId || '')); + const tribe = t.powerType === 'tribe' && !!t.powerId; + if (!inhabitant && !tribe) continue; + const support = supportOf({ ...t, method }); + if (support.votes < PROPOSAL_QUORUM) continue; + out.push({ ...t, method, winnerVotes: support.votes, totalVotes: support.total }); + } + return collapseOverlappingTerms(out, (t) => t.winnerVotes); + } + + async function listValidTerms() { + return validTermsFrom(await readLog()); + } + + async function readVoteResults() { + const now = Date.now(); + if (voteCache.results && now - voteCache.at < CACHE_MS) return voteCache; + const ssbClient = await openSsb(); + const arr = await readTyped(ssbClient, VOTE_LOG_TYPES, { limit: logLimit }); + voteCache = { at: now, results: buildVoteResults(arr), tomb: buildValidatedTombstoneSet(arr) }; + return voteCache; + } + + async function voteResult(voteId, creator) { + if (!voteId) return null; + const { results, tomb } = await readVoteResults(); + const r = results.get(voteId); + if (!r || tomb.has(r.rootId) || tomb.has(voteId)) return null; + if (creator && r.creator !== creator) return null; + const vm = r.votes || {}; + const due = r.deadline ? new Date(r.deadline).getTime() : NaN; + return { + YES: Number(vm.YES ?? vm.Yes ?? vm.yes ?? 0), + NO: Number(vm.NO ?? vm.No ?? vm.no ?? 0), + ABSTENTION: Number(vm.ABSTENTION ?? vm.Abstention ?? vm.abstention ?? 0), + total: Number(r.totalVotes || 0), + deadline: r.deadline || null, + closed: Number.isFinite(due) && due <= Date.now() + }; + } + + function termForId(termId, terms) { + const id = String(termId || ''); + const found = terms.find(t => t.id === id || t.startAt === id); + if (found) return found; + const m = id.match(/^anarchy:(-?\d+)$/); + return m ? virtualAnarchyTerm(termWindowFor(TERM_EPOCH + Number(m[1]) * TERM_SPAN_MS)) : null; + } + + async function outcomeContext() { + const msgs = await readLog(); + const terms = validTermsFrom(msgs); + const proposals = chainIndex(msgs, 'parliamentProposal'); + const revocations = chainIndex(msgs, 'parliamentRevocation'); + const voteOwner = new Map(); + const byAge = [...proposals.items, ...revocations.items].sort((a, b) => + ((new Date(a.createdAt).getTime() || 0) - (new Date(b.createdAt).getTime() || 0)) || String(a.id).localeCompare(String(b.id)) + ); + for (const x of byAge) if (x.voteId && !voteOwner.has(x.voteId)) voteOwner.set(x.voteId, x.id); + return { msgs, terms, proposals, revocations, voteOwner, members: new Map(), karma: new Map() }; + } + + async function mayPropose(term, feedId, ctx) { + if (!term || !feedId) return false; + if (term.virtual) return true; + if (term.powerType === 'inhabitant') return String(term.powerId) === String(feedId); + if (term.powerType === 'tribe') { + if (!ctx.members.has(term.powerId)) { + let members = []; + if (services.tribes) { + try { const tribe = await services.tribes.getTribeById(term.powerId); members = ensureArray(tribe && tribe.members); } catch {} + } + ctx.members.set(term.powerId, members); + } + return ctx.members.get(term.powerId).includes(feedId); + } + return false; + } + + async function karmaOf(feedId, ctx) { + if (!ctx.karma.has(feedId)) ctx.karma.set(feedId, await getInhabitantKarma(feedId)); + return ctx.karma.get(feedId); + } + + async function wonKarmatocracy(p, term, ctx) { + const spanOf = (x) => [new Date(x.createdAt).getTime() || 0, new Date(x.deadline).getTime()]; + const [ps, pe] = spanOf(p); + const rivals = []; + for (const q of ctx.proposals.items) { + if (q.termId !== p.termId || String(q.method || '').toUpperCase() !== 'KARMATOCRACY') continue; + const [qs, qe] = spanOf(q); + if (!Number.isFinite(qe) || qe > Date.now() || qs > pe || ps > qe) continue; + if (!(await mayPropose(term, q.proposer, ctx))) continue; + rivals.push({ id: q.id, proposer: q.proposer, karma: await karmaOf(q.proposer, ctx), createdAtMs: qs }); + } + rivals.sort((a, b) => (b.karma - a.karma) || (a.createdAtMs - b.createdAtMs) || String(a.proposer).localeCompare(String(b.proposer)) || String(a.id).localeCompare(String(b.id))); + return rivals.length > 0 && rivals[0].id === p.id; + } + + async function deriveOutcome(p, ctx, kind = 'proposal') { + if (!p || !p.proposer) return null; + const term = termForId(p.termId, ctx.terms); + if (!term) return null; + const method = String(p.method || '').toUpperCase(); + if (method !== String(term.method || '').toUpperCase()) return null; + if (!(await mayPropose(term, p.proposer, ctx))) return null; + const status = String(p.status || 'OPEN').toUpperCase(); + if (status === 'REJECTED' || status === 'DISCARDED') return status; + if (VOTE_METHODS.has(method)) { + if (!p.voteId || ctx.voteOwner.get(p.voteId) !== p.id) return null; + const v = await voteResult(p.voteId, p.proposer); + if (!v) return null; + if (!v.closed) return 'OPEN'; + return (await passesThreshold(method, v.total, v.YES)) ? 'APPROVED' : 'REJECTED'; + } + const due = new Date(p.deadline).getTime(); + const expired = Number.isFinite(due) && due <= Date.now(); + if (method === 'DICTATORSHIP') return (expired || status === 'APPROVED' || status === 'ENACTED') ? 'APPROVED' : 'OPEN'; + if (method === 'KARMATOCRACY') { + if (!expired) return 'OPEN'; + if (kind === 'revocation') return 'APPROVED'; + return (await wonKarmatocracy(p, term, ctx)) ? 'APPROVED' : 'REJECTED'; + } + return null; + } + async function listTermsBase(filter = 'all') { - const all = await listByType('parliamentTerm'); - const collapsed = collapseOverlappingTerms(all); + const collapsed = await listValidTerms(); const latestStart = collapsed.reduce((mx, t) => Math.max(mx, new Date(t.startAt).getTime() || 0), 0); let arr = collapsed.map(t => { const superseded = (new Date(t.startAt).getTime() || 0) < latestStart; @@ -334,8 +694,7 @@ if (c.type === type) { } async function getLatestTermAny() { - const terms = await listByType('parliamentTerm'); - const collapsed = collapseOverlappingTerms(terms); + const collapsed = await listValidTerms(); return collapsed[0] || null; } @@ -345,6 +704,28 @@ if (c.type === type) { return isExpiredTerm(t) ? null : t; } + function virtualAnarchyTerm(window = termWindowFor(Date.now())) { + return { + id: `anarchy:${window.cycle}`, + virtual: true, + type: 'parliamentTerm', + cycle: window.cycle, + method: 'ANARCHY', + powerType: 'none', + powerId: null, + powerTitle: 'ANARCHY', + winnerTribeId: null, + winnerInhabitantId: null, + winnerVotes: 0, + totalVotes: 0, + population: 0, + startAt: window.startAt, + endAt: window.endAt, + createdBy: null, + createdAt: window.startAt + }; + } + function currentCycleStart(term) { return term ? term.startAt : moment().subtract(TERM_DAYS, 'days').toISOString(); } @@ -352,6 +733,7 @@ if (c.type === type) { async function archiveAllCandidatures() { const all = await listCandidaturesOpenRaw(); for (const c of all) { + if (String(c.proposer || '') !== String(userId)) continue; const updated = { ...stripId(c), replaces: c.id, status: 'DISCARDED', updatedAt: nowISO() }; await publishMsg(updated); } @@ -409,53 +791,28 @@ if (c.type === type) { } return x.termId === termId || (termStart && x.termId === termStart); }; - const proposals = await listByType('parliamentProposal'); - const mine = termId ? proposals.filter(matchesTerm) : []; - const proposed = mine.length; + const ctx = await outcomeContext(); + const mine = termId ? ctx.proposals.items.filter(matchesTerm) : []; + let proposed = 0; let approved = 0; let declined = 0; let discarded = 0; for (const p of mine) { - const baseStatus = String(p.status || 'OPEN').toUpperCase(); - let finalStatus = baseStatus; - let isDiscarded = false; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const dl = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (dl && moment(dl).isBefore(moment())); - const reached = await passesThreshold(p.method, total, yes); - if (!closed) { - if (dl && moment(dl).isBefore(moment()) && !reached) isDiscarded = true; - else finalStatus = 'OPEN'; - } else { - finalStatus = reached ? 'APPROVED' : 'REJECTED'; - } - } catch {} - } else { - if (baseStatus === 'OPEN' && p.deadline && moment(p.deadline).isBefore(moment())) isDiscarded = true; - } - if (isDiscarded) { - discarded++; - continue; - } - if (finalStatus === 'ENACTED' || finalStatus === 'APPROVED') { - approved++; - continue; - } - if (finalStatus === 'REJECTED') { - declined++; - continue; + const outcome = await deriveOutcome(p, ctx); + if (!outcome) continue; + proposed++; + if (outcome === 'APPROVED') approved++; + else if (outcome === 'REJECTED') declined++; + else if (outcome === 'DISCARDED') discarded++; + } + let revocated = 0; + if (termId) { + for (const r of ctx.revocations.items) { + if (!matchesTerm(r)) continue; + const rTerm = termForId(r.termId, ctx.terms); + if (rTerm && !rTerm.virtual && isExpiredTerm(rTerm) && (await deriveOutcome(r, ctx, 'revocation')) === 'APPROVED') revocated++; } } - const revs = await listByType('parliamentRevocation'); - const revocated = termId - ? revs.filter(r => r.status === 'ENACTED' && matchesTerm(r)).length - : 0; return { proposed, approved, declined, discarded, revocated }; } @@ -474,7 +831,12 @@ if (c.type === type) { const pol = await summarizePoliciesForTerm({ ...term }); const eff = pol.proposed > 0 ? Math.round((pol.approved / pol.proposed) * 100) : 0; const cands = await listByType('parliamentCandidature'); - const presented = cands.length; + const cycleStartMs = term.startAt ? new Date(term.startAt).getTime() : 0; + const cycleEndMs = term.endAt ? new Date(term.endAt).getTime() : Number.MAX_SAFE_INTEGER; + const presented = cands.filter(c => { + const t = c.createdAt ? new Date(c.createdAt).getTime() : 0; + return t >= cycleStartMs && t <= cycleEndMs; + }).length; return { method, powerType: term.powerType, @@ -504,84 +866,6 @@ if (c.type === type) { return nProp + nLaw; } - async function closeExpiredKarmatocracy(term) { - const termId = term.id || term.startAt; - const all = await listByType('parliamentProposal'); - const pending = all.filter(p => - p.termId === termId && - String(p.method).toUpperCase() === 'KARMATOCRACY' && - (p.status || 'OPEN') !== 'ENACTED' && - p.deadline && moment().isAfter(parseISO(p.deadline)) - ); - if (!pending.length) return; - const withKarma = await Promise.all(pending.map(async p => ({ - ...p, - karma: await getInhabitantKarma(p.proposer), - createdAtMs: new Date(p.createdAt).getTime() || 0 - }))); - withKarma.sort((a, b) => { - if (b.karma !== a.karma) return b.karma - a.karma; - if (a.createdAtMs !== b.createdAtMs) return a.createdAtMs - b.createdAtMs; - return String(a.proposer).localeCompare(String(b.proposer)); - }); - const winner = withKarma[0]; - const losers = withKarma.slice(1); - const approve = { ...stripId(winner), replaces: winner.id, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(approve); - for (const lo of losers) { - const rej = { ...stripId(lo), replaces: lo.id, status: 'REJECTED', updatedAt: nowISO() }; - await publishMsg(rej); - } - } - - async function closeExpiredDictatorship(term) { - const termId = term.id || term.startAt; - const all = await listByType('parliamentProposal'); - const pending = all.filter(p => - p.termId === termId && - String(p.method).toUpperCase() === 'DICTATORSHIP' && - (p.status || 'OPEN') !== 'ENACTED' && - p.deadline && moment().isAfter(parseISO(p.deadline)) - ); - if (!pending.length) return; - for (const p of pending) { - const updated = { ...stripId(p), replaces: p.id, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - } - } - - async function closeExpiredRevocationKarmatocracy(term) { - const termId = term.id || term.startAt; - const all = await listByType('parliamentRevocation'); - const pending = all.filter(p => - p.termId === termId && - String(p.method).toUpperCase() === 'KARMATOCRACY' && - (p.status || 'OPEN') !== 'ENACTED' && - p.deadline && moment().isAfter(parseISO(p.deadline)) - ); - if (!pending.length) return; - for (const p of pending) { - const updated = { ...stripId(p), replaces: p.id, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - } - } - - async function closeExpiredRevocationDictatorship(term) { - const termId = term.id || term.startAt; - const all = await listByType('parliamentRevocation'); - const pending = all.filter(p => - p.termId === termId && - String(p.method).toUpperCase() === 'DICTATORSHIP' && - (p.status || 'OPEN') !== 'ENACTED' && - p.deadline && moment().isAfter(parseISO(p.deadline)) - ); - if (!pending.length) return; - for (const p of pending) { - const updated = { ...stripId(p), replaces: p.id, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - } - } - async function createRevocation({ lawId, title, reasons }) { const term = await getCurrentTermBase(); if (!term) throw new Error('No active government'); @@ -589,7 +873,7 @@ if (c.type === type) { if (!allowed) throw new Error('You are not in the goverment, yet.'); const lawIdStr = String(lawId || '').trim(); if (!lawIdStr) throw new Error('Law required'); - const laws = await listByType('parliamentLaw'); + const laws = await listLaws(); const law = laws.find(l => l.id === lawIdStr); if (!law) throw new Error('Law not found'); const method = String(term.method || 'DEMOCRACY').toUpperCase(); @@ -630,36 +914,28 @@ if (c.type === type) { return await publishMsg(rev); } - async function closeRevocation(revId) { - const ssbClient = await openSsb(); - const msg = await new Promise((resolve, reject) => - ssbClient.get(revId, (e, m) => (e || !m) ? reject(new Error('Revocation not found')) : resolve(m)) - ); - if (msg.content?.type !== 'parliamentRevocation') throw new Error('Revocation not found'); - const p = msg.content; + async function settleOwn(kind, id, manual) { + const ctx = await outcomeContext(); + const index = kind === 'revocation' ? ctx.revocations : ctx.proposals; + const tipId = index.resolve(String(id || '')); + const p = tipId ? index.items.find(x => x.id === tipId) : null; + if (!p) throw new Error(kind === 'revocation' ? 'Revocation not found' : 'Proposal not found'); const currentStatus = String(p.status || 'OPEN').toUpperCase(); if (currentStatus === 'ENACTED' || currentStatus === 'REJECTED' || currentStatus === 'DISCARDED') return p; - const method = String(p.method || '').toUpperCase(); - if (method === 'DICTATORSHIP') { - if (currentStatus === 'APPROVED') return p; - const updated = { ...p, replaces: revId, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - return updated; - } - if (method === 'KARMATOCRACY') return p; - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const ok = await passesThreshold(method, total, yes); - const desiredStatus = ok ? 'APPROVED' : 'REJECTED'; - if (currentStatus === desiredStatus) return p; - const updated = { ...p, replaces: revId, status: desiredStatus, updatedAt: nowISO() }; + if (String(p.proposer) !== String(userId)) return p; + let outcome = await deriveOutcome(p, ctx, kind); + if (manual && outcome === 'OPEN' && String(p.method || '').toUpperCase() === 'DICTATORSHIP') outcome = 'APPROVED'; + if (outcome !== 'APPROVED' && outcome !== 'REJECTED') return p; + if (currentStatus === outcome) return p; + const updated = { ...stripId(p), replaces: p.id, status: outcome, updatedAt: nowISO() }; await publishMsg(updated); return updated; } + async function closeRevocation(revId) { + return await settleOwn('revocation', revId, true); + } + async function proposeCandidature({ candidateId, method }) { const m = String(method || '').toUpperCase(); if (!METHODS.includes(m)) throw new Error('Invalid method'); @@ -706,12 +982,15 @@ if (c.type === type) { return await publishMsg(content); } - async function createProposal({ title, description }) { - let term = await getCurrentTermBase(); - if (!term) { - await resolveElection(); - term = await getCurrentTermBase(); + async function createProposal({ title, description, campaignId = '', signatures = 0, goal = 0 }) { + const campaignRef = String(campaignId || '').trim(); + if (campaignRef) { + const existing = await listByType('parliamentProposal'); + if (existing.some(p => String(p.campaignId || '') === campaignRef)) throw new Error('This campaign was already raised to Parliament'); } + const extra = campaignRef ? { campaignId: campaignRef, signatures: Math.max(0, Math.floor(Number(signatures) || 0)), goal: Math.max(0, Math.floor(Number(goal) || 0)) } : {}; + let term = await getCurrentTermBase(); + if (!term) term = await resolveElection(); if (!term) throw new Error('No active government'); const allowed = await canPropose(); if (!allowed) throw new Error('You are not in the goverment, yet.'); @@ -722,42 +1001,16 @@ if (c.type === type) { const method = String(term.method || 'DEMOCRACY').toUpperCase(); const deadline = moment().add(PROPOSAL_DAYS, 'days').toISOString(); if (method === 'DICTATORSHIP' || method === 'KARMATOCRACY') { - const proposal = { type: 'parliamentProposal', title, description: description || '', method, termId: term.id || term.startAt, proposer: userId, status: 'OPEN', deadline, createdAt: nowISO() }; + const proposal = { type: 'parliamentProposal', title, description: description || '', method, termId: term.id || term.startAt, proposer: userId, status: 'OPEN', deadline, createdAt: nowISO(), ...extra }; return await publishMsg(proposal); } const voteMsg = await services.votes.createVote(title, deadline, ['YES', 'NO', 'ABSTENTION'], [`gov:${term.id || term.startAt}`, `govMethod:${method}`, 'proposal']); - const proposal = { type: 'parliamentProposal', title, description: description || '', method, voteId: voteMsg.key || voteMsg.id, termId: term.id || term.startAt, proposer: userId, status: 'OPEN', createdAt: nowISO() }; + const proposal = { type: 'parliamentProposal', title, description: description || '', method, voteId: voteMsg.key || voteMsg.id, termId: term.id || term.startAt, proposer: userId, status: 'OPEN', createdAt: nowISO(), ...extra }; return await publishMsg(proposal); } async function closeProposal(proposalId) { - const ssbClient = await openSsb(); - const msg = await new Promise((resolve, reject) => - ssbClient.get(proposalId, (e, m) => (e || !m) ? reject(new Error('Proposal not found')) : resolve(m)) - ); - if (msg.content?.type !== 'parliamentProposal') throw new Error('Proposal not found'); - const p = msg.content; - const currentStatus = String(p.status || 'OPEN').toUpperCase(); - if (currentStatus === 'ENACTED' || currentStatus === 'REJECTED' || currentStatus === 'DISCARDED') return p; - const method = String(p.method || '').toUpperCase(); - if (method === 'DICTATORSHIP') { - if (currentStatus === 'APPROVED') return p; - const updated = { ...p, replaces: proposalId, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - return updated; - } - if (method === 'KARMATOCRACY') return p; - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const ok = await passesThreshold(method, total, yes); - const desiredStatus = ok ? 'APPROVED' : 'REJECTED'; - if (currentStatus === desiredStatus) return p; - const updated = { ...p, replaces: proposalId, status: desiredStatus, updatedAt: nowISO() }; - await publishMsg(updated); - return updated; + return await settleOwn('proposal', proposalId, true); } async function sweepProposals() { @@ -765,48 +1018,15 @@ if (c.type === type) { sweepInFlight = (async () => { const term = await getCurrentTermBase(); if (!term) return; - await closeExpiredKarmatocracy(term); - await closeExpiredDictatorship(term); - - const allProps = await listByType('parliamentProposal'); - const voteProps = allProps.filter(p => { - const m = String(p.method || '').toUpperCase(); - return (m === 'DEMOCRACY' || m === 'ANARCHY' || m === 'MAJORITY' || m === 'MINORITY') && p.voteId; - }); - - for (const p of voteProps) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const deadline = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (closed) { try { await closeProposal(p.id); } catch {} ; continue; } - } catch {} + const termId = term.id || term.startAt; + const ctx = await outcomeContext(); + for (const p of ctx.proposals.items) { + if (p.termId !== termId || String(p.proposer) !== String(userId)) continue; + try { await settleOwn('proposal', p.id, false); } catch {} } - - await closeExpiredRevocationKarmatocracy(term); - await closeExpiredRevocationDictatorship(term); - - const revs = await listByType('parliamentRevocation'); - const voteRevs = revs.filter(p => { - const m = String(p.method || '').toUpperCase(); - return (m === 'DEMOCRACY' || m === 'ANARCHY' || m === 'MAJORITY' || m === 'MINORITY') && p.voteId; - }); - - for (const p of voteRevs) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const deadline = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (closed) { try { await closeRevocation(p.id); } catch {} ; continue; } - } catch {} + for (const r of ctx.revocations.items) { + if (r.termId !== termId || String(r.proposer) !== String(userId)) continue; + try { await settleOwn('revocation', r.id, false); } catch {} } })().finally(() => { sweepInFlight = null; }); @@ -827,7 +1047,65 @@ if (c.type === type) { return await listTermsBase(filter); } + async function getHemicycle(termInput = null) { + const term = termInput || await getCurrentTerm().catch(() => null); + const window = term && term.startAt ? { startAt: term.startAt, endAt: term.endAt } : termWindowFor(Date.now()); + const from = new Date(window.startAt).getTime(); + const to = window.endAt ? new Date(window.endAt).getTime() : Number.MAX_SAFE_INTEGER; + const msgs = await readLog(); + const rootOf = new Map(); + const cands = new Map(); + for (const m of msgs || []) { + const c = m.value && m.value.content; + if (!c || c.type !== 'parliamentCandidature') continue; + const root = c.replaces ? (rootOf.get(c.replaces) || c.replaces) : m.key; + rootOf.set(m.key, root); + if (!cands.has(root)) { + cands.set(root, { id: root, targetType: c.targetType, targetId: c.targetId, targetTitle: c.targetTitle, method: c.method, createdAt: c.createdAt || new Date(normMs(m.value.timestamp || m.timestamp)).toISOString(), voters: new Set() }); + } + } + for (const m of msgs || []) { + const c = m.value && m.value.content; + if (!c || c.type !== 'parliamentCandidatureVote' || !c.target) continue; + const root = rootOf.get(c.target) || c.target; + const cand = cands.get(root); + if (cand) cand.voters.add(m.value.author); + } + const inWindow = [...cands.values()].filter(c => { const t = new Date(c.createdAt).getTime(); return Number.isFinite(t) ? (t >= from && t <= to) : false; }); + let census = []; + try { + if (services.inhabitants?.listInhabitants) census = await services.inhabitants.listInhabitants({ filter: 'all', includeInactive: true }); + } catch {} + let powerMembers = []; + if (term && term.powerType === 'tribe' && term.powerId && services.tribes?.getTribeById) { + try { const t = await services.tribes.getTribeById(term.powerId); powerMembers = Array.isArray(t && t.members) ? t.members : []; } catch {} + } + const candidatures = inWindow.map(c => ({ + id: c.id, + label: c.targetTitle || c.targetId, + href: c.targetType === 'tribe' ? `/tribe/${encodeURIComponent(c.targetId)}` : `/author/${encodeURIComponent(c.targetId)}`, + targetType: c.targetType, + targetId: c.targetId, + method: c.method, + voters: [...c.voters] + })); + return buildHemicycle({ + census: (census || []).map(u => ({ id: u.id, name: u.name || '' })), + candidatures, + powerType: term ? term.powerType : 'none', + powerId: term ? term.powerId : null, + leaderId: term && term.powerType === 'inhabitant' ? term.powerId : null, + method: term ? term.method : 'ANARCHY', + powerMembers + }); + } + async function getCurrentTerm() { + const published = await getCurrentTermBase(); + return published || virtualAnarchyTerm(); + } + + async function getPublishedTerm() { return await getCurrentTermBase(); } @@ -865,207 +1143,104 @@ if (c.type === type) { return rows; } - async function listProposalsCurrent() { - const all = await listByType('parliamentProposal'); - const rows = all.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + async function listCurrentOf(kind) { + const ctx = await outcomeContext(); + const index = kind === 'revocation' ? ctx.revocations : ctx.proposals; + const items = kind === 'revocation' ? index.items : index.items.map(withCampaignRef); + const rows = items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + const needed = await proposalQuorum(); const out = []; for (const p of rows) { - let deadline = p.deadline || null; - let yes = 0; - let total = 0; - const baseStatus = String(p.status || 'OPEN').toUpperCase(); - let derivedStatus = baseStatus; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - total = Number(v.totalVotes ?? v.total ?? sum); - yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - deadline = deadline || v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - const reached = await passesThreshold(p.method, total, yes); - if (closed) derivedStatus = reached ? 'APPROVED' : 'REJECTED'; - else derivedStatus = 'OPEN'; - } catch { - derivedStatus = baseStatus; - } - } else { - if (baseStatus === 'OPEN' && p.deadline && moment(p.deadline).isBefore(moment())) derivedStatus = 'DISCARDED'; - } - if (derivedStatus === 'ENACTED' || derivedStatus === 'REJECTED' || derivedStatus === 'DISCARDED') continue; - const needed = await proposalQuorum(); + if (String(p.status || 'OPEN').toUpperCase() === 'ENACTED') continue; + const derivedStatus = await deriveOutcome(p, ctx, kind); + if (!derivedStatus || derivedStatus === 'REJECTED' || derivedStatus === 'DISCARDED') continue; + const v = p.voteId ? await voteResult(p.voteId, p.proposer) : null; + const yes = v ? v.YES : 0; + const total = v ? v.total : 0; + const deadline = p.deadline || (v && v.deadline) || null; const onTrack = await passesThreshold(p.method, total, yes); out.push({ ...p, deadline, yes, total, needed, onTrack, derivedStatus }); } return out; } + async function listFutureOf(kind) { + const term = await getCurrentTermBase(); + if (!term) return []; + const termId = term.id || term.startAt; + const ctx = await outcomeContext(); + const index = kind === 'revocation' ? ctx.revocations : ctx.proposals; + const items = kind === 'revocation' ? index.items : index.items.map(withCampaignRef); + const rows = items + .filter(p => p.termId === termId && String(p.status || 'OPEN').toUpperCase() !== 'ENACTED') + .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + const needed = await proposalQuorum(); + const out = []; + for (const p of rows) { + if ((await deriveOutcome(p, ctx, kind)) !== 'APPROVED') continue; + const v = p.voteId ? await voteResult(p.voteId, p.proposer) : null; + const deadline = p.deadline || (v && v.deadline) || null; + out.push({ ...p, deadline, yes: v ? v.YES : 0, total: v ? v.total : 0, needed }); + } + return out; + } + + async function listProposalsCurrent() { + return await listCurrentOf('proposal'); + } + async function deriveProposalStatus(method, voteId) { const m = String(method || '').toUpperCase(); - if (!voteId || !(m === 'DEMOCRACY' || m === 'ANARCHY' || m === 'MAJORITY' || m === 'MINORITY')) return null; + if (!voteId || !VOTE_METHODS.has(m)) return null; try { - const v = await services.votes.getVoteById(voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const deadline = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (!closed) return 'OPEN'; - return (await passesThreshold(m, total, yes)) ? 'APPROVED' : 'REJECTED'; + const v = await voteResult(voteId); + if (!v) return null; + if (!v.closed) return 'OPEN'; + return (await passesThreshold(m, v.total, v.YES)) ? 'APPROVED' : 'REJECTED'; } catch { return null; } } async function listFutureLawsCurrent() { - const term = await getCurrentTermBase(); - if (!term) return []; - const termId = term.id || term.startAt; - const all = await listByType('parliamentProposal'); - const rows = all - .filter(p => p.termId === termId) - .filter(p => p.status === 'APPROVED') - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - const out = []; - for (const p of rows) { - let yes = 0; - let total = 0; - let deadline = p.deadline || null; - let voteClosed = true; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - total = Number(v.totalVotes ?? v.total ?? sum); - yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - deadline = deadline || v.deadline || v.endAt || v.expiresAt || null; - voteClosed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (!voteClosed) continue; - if (VOTE_METHODS.has(String(p.method || '').toUpperCase()) && !(await passesThreshold(p.method, total, yes))) continue; - } catch {} - } - const needed = await proposalQuorum(); - out.push({ ...p, deadline, yes, total, needed }); - } - return out; + return await listFutureOf('proposal'); } async function listRevocationsCurrent() { - const all = await listByType('parliamentRevocation'); - const rows = all.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - const out = []; - for (const p of rows) { - let deadline = p.deadline || null; - let yes = 0; - let total = 0; - const baseStatus = String(p.status || 'OPEN').toUpperCase(); - let derivedStatus = baseStatus; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - total = Number(v.totalVotes ?? v.total ?? sum); - yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - deadline = deadline || v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - const reached = await passesThreshold(p.method, total, yes); - if (closed) derivedStatus = reached ? 'APPROVED' : 'REJECTED'; - else derivedStatus = 'OPEN'; - } catch { - derivedStatus = baseStatus; - } - } else { - if (baseStatus === 'OPEN' && p.deadline && moment(p.deadline).isBefore(moment())) derivedStatus = 'DISCARDED'; - } - if (derivedStatus === 'ENACTED' || derivedStatus === 'REJECTED' || derivedStatus === 'DISCARDED') continue; - const needed = await proposalQuorum(); - const onTrack = await passesThreshold(p.method, total, yes); - out.push({ ...p, deadline, yes, total, needed, onTrack, derivedStatus }); - } - return out; + return await listCurrentOf('revocation'); } async function listFutureRevocationsCurrent() { - const term = await getCurrentTermBase(); - if (!term) return []; - const termId = term.id || term.startAt; - const all = await listByType('parliamentRevocation'); - const rows = all - .filter(p => p.termId === termId) - .filter(p => p.status === 'APPROVED') - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - const out = []; - for (const p of rows) { - let yes = 0; - let total = 0; - let deadline = p.deadline || null; - let voteClosed = true; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - total = Number(v.totalVotes ?? v.total ?? sum); - yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - deadline = deadline || v.deadline || v.endAt || v.expiresAt || null; - voteClosed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (!voteClosed) continue; - if (VOTE_METHODS.has(String(p.method || '').toUpperCase()) && !(await passesThreshold(p.method, total, yes))) continue; - } catch {} - } - const needed = await proposalQuorum(); - out.push({ ...p, deadline, yes, total, needed }); - } - return out; + return await listFutureOf('revocation'); } async function countRevocationsEnacted() { - const all = await listByType('parliamentRevocation'); - return all.filter(r => r.status === 'ENACTED').length; + const ctx = await outcomeContext(); + let n = 0; + for (const r of ctx.revocations.items) { + const term = termForId(r.termId, ctx.terms); + if (term && !term.virtual && isExpiredTerm(term) && (await deriveOutcome(r, ctx, 'revocation')) === 'APPROVED') n++; + } + return n; } - async function voteSnapshot(voteId) { - if (!voteId || !services.votes?.getVoteById) return null; - try { - const v = await services.votes.getVoteById(voteId); - const vm = v?.votes || {}; - const sum = Object.values(vm).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - return { - YES: Number(vm.YES ?? vm.Yes ?? vm.yes ?? 0), - NO: Number(vm.NO ?? vm.No ?? vm.no ?? 0), - ABSTENTION: Number(vm.ABSTENTION ?? vm.Abstention ?? vm.abstention ?? 0), - total - }; - } catch { - return null; - } - } + const votesOf = (v) => (v ? { YES: v.YES, NO: v.NO, ABSTENTION: v.ABSTENTION, total: v.total } : null); async function enactApprovedChanges(expiringTerm) { if (!expiringTerm) return; const termId = expiringTerm.id || expiringTerm.startAt; + const ctx = await outcomeContext(); - const proposals = await listByType('parliamentProposal'); - const revocations = await listByType('parliamentRevocation'); - - const approvedProps = proposals.filter(p => p.termId === termId && p.status === 'APPROVED'); - for (const p of approvedProps) { - const snap = await voteSnapshot(p.voteId); - const votesFinal = - (p.votes && Object.keys(p.votes).length ? p.votes : null) || - snap || - { YES: 1, NO: 0, ABSTENTION: 0, total: 1 }; - - const totalFinal = Number(votesFinal.total ?? Object.entries(votesFinal).reduce((s, [k, n]) => s + (k === 'total' ? 0 : Number(n || 0)), 0)); - const yesFinal = Number(votesFinal.YES ?? votesFinal.Yes ?? votesFinal.yes ?? 0); - if (VOTE_METHODS.has(String(p.method || '').toUpperCase()) && !(await passesThreshold(p.method, totalFinal, yesFinal))) { + for (const p of ctx.proposals.items.map(withCampaignRef)) { + if (p.termId !== termId || String(p.proposer) !== String(userId)) continue; + const status = String(p.status || 'OPEN').toUpperCase(); + if (status === 'ENACTED' || status === 'REJECTED' || status === 'DISCARDED') continue; + const outcome = await deriveOutcome(p, ctx); + if (outcome === 'REJECTED') { const rej = { ...stripId(p), replaces: p.id, status: 'REJECTED', updatedAt: nowISO() }; await publishMsg(rej); continue; } + if (outcome !== 'APPROVED') continue; + const snap = p.voteId ? await voteResult(p.voteId, p.proposer) : null; const law = { type: 'parliamentLaw', @@ -1075,7 +1250,7 @@ if (c.type === type) { proposer: p.proposer, termId: p.termId, voteId: p.voteId || null, - votes: votesFinal, + votes: votesOf(snap) || { YES: 1, NO: 0, ABSTENTION: 0, total: 1 }, proposedAt: p.createdAt, proposalId: p.id, enactedAt: nowISO() @@ -1086,17 +1261,24 @@ if (c.type === type) { await publishMsg(updated); } - const approvedRevs = revocations.filter(r => r.termId === termId && r.status === 'APPROVED'); - for (const r of approvedRevs) { - const tomb = { type: 'tombstone', target: r.lawId, deletedAt: nowISO(), author: userId }; - await publishMsg(tomb); + for (const r of ctx.revocations.items) { + if (r.termId !== termId || String(r.proposer) !== String(userId)) continue; + const status = String(r.status || 'OPEN').toUpperCase(); + if (status === 'ENACTED' || status === 'REJECTED' || status === 'DISCARDED') continue; + if ((await deriveOutcome(r, ctx, 'revocation')) !== 'APPROVED') continue; + const lawMsg = (ctx.msgs || []).find(m => m.key === r.lawId); + const lawValue = lawMsg && lawMsg.value; + if (lawValue && lawValue.author === userId && lawValue.content && lawValue.content.type === 'parliamentLaw') { + const tomb = { type: 'tombstone', target: r.lawId, deletedAt: nowISO(), author: userId }; + await publishMsg(tomb); + } - const snap = await voteSnapshot(r.voteId); + const snap = r.voteId ? await voteResult(r.voteId, r.proposer) : null; const updated = { ...stripId(r), replaces: r.id, status: 'ENACTED', - votes: (r.votes && Object.keys(r.votes).length ? r.votes : null) || snap || undefined, + votes: votesOf(snap) || undefined, updatedAt: nowISO() }; await publishMsg(updated); @@ -1112,7 +1294,10 @@ if (c.type === type) { try { await enactApprovedChanges(latestAny); } catch (e) { console.error('enactApprovedChanges failed:', e); } } - const open = await listCandidaturesOpen(); + const open = (await listCandidaturesOpen()).filter(c => + METHODS.includes(String(c.method || '').toUpperCase()) && + ((c.targetType === 'inhabitant' && FEED_ID_RE.test(String(c.targetId || ''))) || (c.targetType === 'tribe' && !!c.targetId)) + ); let chosen = null; let totalVotes = 0; let winnerVotes = 0; @@ -1130,44 +1315,21 @@ if (c.type === type) { } } - const startAt = now.toISOString(); - const endAt = moment(startAt).add(TERM_DAYS, 'days').toISOString(); + const window = termWindowFor(now.valueOf()); + const startAt = window.startAt; + const endAt = window.endAt; + const population = await inhabitantsCount(); + if (chosen && supportFor(await readLog())({ ...window, powerType: chosen.targetType, powerId: chosen.targetId, method: chosen.method }).votes < PROPOSAL_QUORUM) chosen = null; if (!chosen) { - const termAnarchy = { - type: 'parliamentTerm', - method: 'ANARCHY', - powerType: 'none', - powerId: null, - powerTitle: 'ANARCHY', - winnerTribeId: null, - winnerInhabitantId: null, - winnerVotes: 0, - totalVotes: 0, - startAt, - endAt, - createdBy: userId, - createdAt: nowISO() - }; - - const resAnarchy = await publishMsg(termAnarchy); - try { - await sleep(250); - const canonical = await getCurrentTermBase(); - const myId = resAnarchy.key || resAnarchy.id; - if (canonical && canonical.id && myId && canonical.id !== myId) { - const tomb = { type: 'tombstone', target: myId, deletedAt: nowISO(), author: userId }; - await publishMsg(tomb); - await archiveAllCandidatures(); - return canonical; - } - } catch {} await archiveAllCandidatures(); - return resAnarchy; + return virtualAnarchyTerm(window); } const term = { type: 'parliamentTerm', + cycle: window.cycle, + population, method: chosen.method, powerType: chosen.targetType, powerId: chosen.targetId, @@ -1205,13 +1367,81 @@ if (c.type === type) { } async function getGovernmentCard() { - const term = await getCurrentTermBase(); + const term = (await getCurrentTermBase()) || virtualAnarchyTerm(); if (!term) return null; return await computeGovernmentCard({ ...term, id: term.id || term.startAt }); } + async function getLatestTerm() { + return await getLatestTermAny(); + } + + async function getLatestGovernmentCard() { + const term = await getLatestTermAny(); + if (!term) return null; + const card = await computeGovernmentCard({ ...term, id: term.id || term.startAt }); + const current = termWindowFor(Date.now()); + const termEnd = term.endAt ? new Date(term.endAt).getTime() : NaN; + const rolled = Number.isFinite(termEnd) && termEnd <= Date.now(); + if (rolled) { + return { + ...card, + id: term.id || term.startAt, + cycle: current.cycle, + since: current.startAt, + end: current.endAt, + method: 'ANARCHY', + powerType: 'none', + powerId: null, + powerTitle: 'ANARCHY', + winnerVotes: 0, + totalVotes: 0, + votesReceived: 0, + presented: 0, + proposed: 0, + approved: 0, + declined: 0, + discarded: 0, + revocated: 0, + efficiency: 0, + expired: false, + rolled: true + }; + } + return { ...card, id: term.id || term.startAt, since: term.startAt, end: term.endAt, expired: false, rolled: false }; + } + async function listLaws() { - const items = await listByType('parliamentLaw'); + const ctx = await outcomeContext(); + const proposalsById = new Map(ctx.proposals.items.map(p => [p.id, p])); + const laws = chainIndex(ctx.msgs, 'parliamentLaw'); + const chosen = new Map(); + const groupOfLaw = new Map(); + for (const l of laws.items) { + const pid = ctx.proposals.resolve(String(l.proposalId || '')); + const p = pid ? proposalsById.get(pid) : null; + if (!p || String(p.proposer) !== String(l.proposer) || p.termId !== l.termId) continue; + const voted = withCampaignRef(p); + if (l.question !== voted.title || String(l.description || '') !== String(voted.description || '') || String(l.method || '') !== String(voted.method || '')) continue; + const term = termForId(p.termId, ctx.terms); + if (!term || term.virtual || !isExpiredTerm(term)) continue; + if ((await deriveOutcome(p, ctx)) !== 'APPROVED') continue; + groupOfLaw.set(l.id, pid); + const own = laws.authorOf(l.id) === p.proposer; + const prev = chosen.get(pid); + const better = !prev + || (own && !prev.own) + || (own === prev.own && (new Date(l.enactedAt).getTime() || 0) < (new Date(prev.law.enactedAt).getTime() || 0)); + if (better) chosen.set(pid, { law: l, own }); + } + for (const r of ctx.revocations.items) { + const pid = groupOfLaw.get(String(r.lawId || '')); + if (!pid || !chosen.has(pid)) continue; + const term = termForId(r.termId, ctx.terms); + if (!term || term.virtual || !isExpiredTerm(term)) continue; + if ((await deriveOutcome(r, ctx, 'revocation')) === 'APPROVED') chosen.delete(pid); + } + const items = [...chosen.values()].map(x => x.law); return items.sort((a, b) => new Date(b.enactedAt) - new Date(a.enactedAt)); } @@ -1243,12 +1473,19 @@ if (c.type === type) { const tribeReadLog = async () => { const client = await openSsb(); - return new Promise((resolve, reject) => { - pull( - client.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || [])) - ); - }); + return readTyped(client, TRIBE_PARLIAMENT_TYPES, { limit: logLimit }); + }; + + const tribeMembersOf = async (ids) => { + const out = new Set(); + for (const id of ids) { + try { + const t = services.tribes && services.tribes.getTribeById ? await services.tribes.getTribeById(id) : null; + if (t && t.author) out.add(t.author); + for (const m of (t && Array.isArray(t.members) ? t.members : [])) out.add(m); + } catch (_) {} + } + return out; }; const tribeListByType = async (type, tribeId) => { @@ -1258,24 +1495,91 @@ if (c.type === type) { ? await services.tribes.getChainIds(tribeId) : [tribeId]; } catch (_) { chainIds = [tribeId]; } - const tribeIdSet = new Set(Array.isArray(chainIds) && chainIds.length ? chainIds : [tribeId]); + const ids = Array.isArray(chainIds) && chainIds.length ? chainIds : [tribeId]; + const tribeIdSet = new Set(ids); + const members = await tribeMembersOf(ids); const msgs = await tribeReadLog(); const tomb = buildValidatedTombstoneSet(msgs); - const replaced = new Set(); - const items = new Map(); + const roots = new Map(); + const rewrites = []; for (const m of msgs) { const c = m.value?.content; if (!c) continue; - if (c.type === 'tombstone') continue; if (c.type !== type) continue; if (!tribeIdSet.has(c.tribeId)) continue; - if (c.replaces) replaced.add(c.replaces); - items.set(m.key, { ...c, id: m.key, _ts: m.value?.timestamp || 0 }); + const author = m.value?.author; + if (!members.has(author)) continue; + const item = { ...c, id: m.key, author, _ts: m.value?.timestamp || 0 }; + if (c.replaces) { rewrites.push(item); continue; } + if (type === 'tribeParliamentCandidature' && c.proposer !== author) continue; + if (type === 'tribeParliamentTerm') { + if (c.createdBy && c.createdBy !== author) continue; + const method = String(c.method || '').toUpperCase(); + if (!METHODS.includes(method) && method !== 'ANARCHY') continue; + if (c.leaderId && !members.has(c.leaderId)) continue; + } + roots.set(m.key, item); } - return [...items.values()].filter(it => !tomb.has(it.id) && !replaced.has(it.id)); + if (type === 'tribeParliamentCandidature') { + const byId = new Map(rewrites.map(r => [r.id, r])); + const rootOfRewrite = (id) => { let cur = id, g = 0; while (byId.has(cur) && g++ < 1000) cur = byId.get(cur).replaces; return cur; }; + const ballotsOf = new Map(); + for (const r of rewrites) { + const root = roots.get(rootOfRewrite(r.id)); + if (!root || r.author === root.candidateId) continue; + if (!ballotsOf.has(root.id)) ballotsOf.set(root.id, []); + ballotsOf.get(root.id).push({ author: r.author, ts: r._ts }); + } + return [...roots.values()].map(it => ({ ...it, _ballots: ballotsOf.get(it.id) || [], _closed: tomb.has(it.id) })); + } + const replacedBySameAuthor = new Map(); + for (const r of rewrites) { const orig = roots.get(r.replaces); if (orig && orig.author === r.author) replacedBySameAuthor.set(r.replaces, r); } + const out = []; + for (const it of roots.values()) { + let cur = it, g = 0; + while (replacedBySameAuthor.has(cur.id) && g++ < 1000) cur = replacedBySameAuthor.get(cur.id); + if (!tomb.has(cur.id) && !tomb.has(it.id)) out.push(cur); + } + return out; + }; + + const tallyCandidatures = (cands, fromTs, toTs) => { + const createdOf = (c) => Date.parse(c.createdAt) || c._ts || 0; + const inCycle = cands.filter(c => createdOf(c) >= fromTs && createdOf(c) < toTs); + const ballots = []; + for (const c of inCycle) for (const b of (c._ballots || [])) if (b.ts >= fromTs && b.ts < toTs) ballots.push({ ...b, root: c.id }); + ballots.sort((a, b) => a.ts - b.ts); + const seen = new Set(); + const votersOf = new Map(); + for (const b of ballots) { + if (seen.has(b.author)) continue; + seen.add(b.author); + if (!votersOf.has(b.root)) votersOf.set(b.root, []); + votersOf.get(b.root).push(b.author); + } + return inCycle.map(c => { const { _ballots, _closed, ...rest } = c; const voters = votersOf.get(c.id) || []; return { ...rest, voters, votes: voters.length, ...(_closed ? { _closed } : {}) }; }); + }; + + const tribeValidTerms = async (tribeId) => { + const terms = (await tribeListByType('tribeParliamentTerm', tribeId)).slice().sort((a, b) => (Date.parse(a.startAt) || 0) - (Date.parse(b.startAt) || 0)); + if (!terms.some(t => t.leaderId)) return terms; + const cands = await tribeListByType('tribeParliamentCandidature', tribeId); + const quorum = await tribeElectionQuorum(tribeId); + const out = []; + for (const t of terms) { + const start = Date.parse(t.startAt) || 0; + if (t.leaderId) { + const prevStart = out.length ? (Date.parse(out[out.length - 1].startAt) || 0) : 0; + const tally = tallyCandidatures(cands, prevStart, start); + const backing = tally.filter(c => c.candidateId === t.leaderId).reduce((n, c) => Math.max(n, c.votes), 0); + if (backing < quorum) continue; + } + out.push(t); + } + return out; }; const tribeGetCurrentTerm = async (tribeId) => { - const terms = await tribeListByType('tribeParliamentTerm', tribeId); + const terms = await tribeValidTerms(tribeId); if (terms.length === 0) return null; const now = moment(); const active = terms.find(t => moment(t.startAt).isSameOrBefore(now) && moment(t.endAt).isAfter(now)); @@ -1323,11 +1627,7 @@ if (c.type === type) { const latest = await tribeGetCurrentTerm(tribeId); if (latest && !isExpiredTerm(latest)) return latest; const opens = (await tribeListCandidatures(tribeId)) - .filter(c => (c.status || 'OPEN') === 'OPEN') - .map(c => { - const voters = ensureArray(c.voters).filter(v => String(v) !== String(c.candidateId)); - return { ...c, voters, votes: voters.length }; - }); + .filter(c => (c.status || 'OPEN') === 'OPEN'); let chosen = null, totalVotes = 0, winnerVotes = 0; if (opens.length) { opens.sort((a, b) => Number(b.votes || 0) - Number(a.votes || 0) || new Date(a.createdAt) - new Date(b.createdAt)); @@ -1372,7 +1672,11 @@ if (c.type === type) { return await tribeResolveElection(tribeId); } - const tribeListCandidatures = (tribeId) => tribeListByType('tribeParliamentCandidature', tribeId); + const tribeListCandidatures = async (tribeId) => { + const cands = await tribeListByType('tribeParliamentCandidature', tribeId); + const term = await tribeGetCurrentTerm(tribeId); + return tallyCandidatures(cands, term ? (Date.parse(term.startAt) || 0) : 0, Infinity).filter(c => !c._closed); + }; const tribeListRules = (tribeId) => tribeListByType('tribeParliamentRule', tribeId); const tribePublishCandidature = async ({ tribeId, candidateId, method }) => { @@ -1464,6 +1768,10 @@ if (c.type === type) { listCandidatures, listTerms, getCurrentTerm, + getPublishedTerm, + getLatestTerm, + getLatestGovernmentCard, + getHemicycle, listLeaders, sweepProposals, getActorMeta, @@ -1492,7 +1800,22 @@ if (c.type === type) { function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } -function collapseOverlappingTerms(terms = []) { +function compareTermsForWindow(a, b, supportOf = () => 0) { + const support = (t) => Number(supportOf(t)) || 0; + const anarchy = (t) => (String(t && t.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0); + + const supportA = support(a); + const supportB = support(b); + if (supportA !== supportB) return supportB - supportA; + + const anarchyA = anarchy(a); + const anarchyB = anarchy(b); + if (anarchyA !== anarchyB) return anarchyA - anarchyB; + + return String(a && a.id || '').localeCompare(String(b && b.id || '')); +} + +function collapseOverlappingTerms(terms = [], supportOf) { if (!terms.length) return []; const sorted = [...terms].sort((a, b) => new Date(a.startAt) - new Date(b.startAt)); const groups = []; @@ -1512,20 +1835,15 @@ function collapseOverlappingTerms(terms = []) { if (!placed) groups.push({ items: [t], minStart: tStart, maxEnd: tEnd }); } const winners = groups.map(g => { - g.items.sort((a, b) => { - const aAn = String(a.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0; - const bAn = String(b.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0; - if (aAn !== bAn) return aAn - bAn; - const aC = new Date(a.createdAt || a.startAt).getTime(); - const bC = new Date(b.createdAt || b.startAt).getTime(); - if (aC !== bC) return aC - bC; - const aS = new Date(a.startAt).getTime(); - const bS = new Date(b.startAt).getTime(); - if (aS !== bS) return aS - bS; - return String(a.id || '').localeCompare(String(b.id || '')); - }); + g.items.sort((a, b) => compareTermsForWindow(a, b, supportOf)); return g.items[0]; }); return winners.sort((a, b) => new Date(b.startAt) - new Date(a.startAt)); } +module.exports.termWindowFor = termWindowFor; +module.exports.compareTermsForWindow = compareTermsForWindow; +module.exports.collapseOverlappingTerms = collapseOverlappingTerms; +module.exports.TERM_DAYS = TERM_DAYS; + +module.exports.hemicycle = { build: buildHemicycle, seatPositions, passThreshold }; diff --git a/src/models/peer_health.js b/src/models/peer_health.js new file mode 100644 index 0000000..e7a75b3 --- /dev/null +++ b/src/models/peer_health.js @@ -0,0 +1,105 @@ +const fs = require('fs'); + +const FAILURES_MIN = 3; +const DEAD_AFTER_MS = 7 * 24 * 60 * 60 * 1000; +const RETRY_CAP_MS = 3 * 60 * 60 * 1000; +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/; + +const healthPath = () => require('../configs/state-manager').statePath('peer-health.json'); +let cache = null; +let cachePath = null; + +const canonicalKey = (key) => { + let k = String(key || '').trim().replace(/-/g, '+').replace(/_/g, '/'); + if (!k) return null; + if (!k.startsWith('@')) k = '@' + k; + if (!k.endsWith('.ed25519')) k += '.ed25519'; + if (!/=\.ed25519$/.test(k)) k = k.replace(/\.ed25519$/, '=.ed25519'); + return FEED_ID.test(k) ? k : null; +}; + +const load = () => { + const p = healthPath(); + if (!cache || cachePath !== p) { + cachePath = p; + try { cache = JSON.parse(fs.readFileSync(p, 'utf8')) || {}; } catch (_) { cache = {}; } + if (typeof cache !== 'object' || Array.isArray(cache)) cache = {}; + } + return cache; +}; + +const save = () => { try { fs.writeFileSync(cachePath, JSON.stringify(cache)); } catch (_) {} }; + +const observe = ({ connected = [], failing = [], now = Date.now() } = {}) => { + const h = load(); + let changed = false; + const up = new Set(connected.map(canonicalKey).filter(Boolean)); + for (const id of up) { + if (h[id]) { delete h[id]; changed = true; } + } + for (const k of failing) { + const id = canonicalKey(k); + if (!id || up.has(id) || h[id]) continue; + h[id] = { failingSince: now }; + changed = true; + } + if (changed) save(); + return changed; +}; + +const isDead = ({ key, failures = 0, now = Date.now() } = {}) => { + if (!(Number(failures) >= FAILURES_MIN)) return false; + const e = load()[canonicalKey(key)] || {}; + const observed = Number(e.failingSince) ? now - Number(e.failingSince) : 0; + return Math.max(observed, Number(failures) * RETRY_CAP_MS) >= DEAD_AFTER_MS; +}; + +const failingSince = (key) => Number((load()[canonicalKey(key)] || {}).failingSince) || 0; + +const addressOf = (item) => { + const m = Array.isArray(item) ? String(item[0] || '').match(/^(?:net|onion):([^:~]+):(\d+)/) : null; + if (m) return `${m[1]}:${m[2]}`; + const data = Array.isArray(item) ? item[1] : item; + return data && data.host ? `${data.host}:${Number(data.port) || 8008}` : null; +}; +const failuresByKey = (...sources) => { + const byKey = new Map(); + for (const list of sources) { + for (const item of list || []) { + const data = Array.isArray(item) ? item[1] : item; + const id = data && canonicalKey(data.key); + if (!id) continue; + const f = Number(data.failure) || 0; + const e = byKey.get(id) || { any: 0, addrs: new Map() }; + const where = addressOf(item); + if (where) e.addrs.set(where, Math.max(e.addrs.get(where) || 0, f)); + else e.any = Math.max(e.any, f); + byKey.set(id, e); + } + } + const out = new Map(); + for (const [id, e] of byKey) out.set(id, e.addrs.size ? Math.min(...[...e.addrs.values()].map(f => Math.max(f, e.any))) : e.any); + return out; +}; + +const classifyNetError = (err, address = '') => { + const msg = `${(err && err.code) || ''} ${(err && err.message) || err || ''}`.toLowerCase(); + const onion = /\.onion/i.test(String(address || '')); + if (msg.includes('network paused')) return 'paused'; + if (onion && /only know|could not connect to/.test(msg)) return 'tor'; + if (/127\.0\.0\.1:9[01]50/.test(msg) || (onion && /econnrefused|socks/.test(msg) && !/ttl|hostunreachable|host unreachable|general/.test(msg))) return 'tor'; + if (/shs\.client: (error when expecting server to accept challenge|server responded with invalid challenge)|application cap/.test(msg)) return 'keys'; + if (/shs\.client: (server hung up when we sent hello|the server's response accepting)/.test(msg)) return 'identity'; + if (/econnrefused/.test(msg)) return 'refused'; + if (/enotfound|eai_again|getaddrinfo/.test(msg)) return 'notfound'; + if (/enetunreach|ehostunreach|ehostdown|enetdown|hostunreachable|host unreachable|networkunreachable/.test(msg)) return 'unreachable'; + if (/timed? ?out|etimedout|ttl/.test(msg)) return 'timeout'; + if (/econnreset|epipe|hung up|aborted|closed/.test(msg)) return 'dropped'; + return 'other'; +}; +const NET_REASON_KEYS = { + paused: 'peerErrPaused', tor: 'peerErrTor', keys: 'peerErrKeys', identity: 'peerErrIdentity', refused: 'peerErrRefused', + notfound: 'peerErrNotFound', unreachable: 'peerErrUnreachable', timeout: 'peerErrTimeout', dropped: 'peerErrDropped', other: 'peerErrOther', silent: 'peerErrSilent' +}; + +module.exports = { canonicalKey, observe, isDead, failingSince, failuresByKey, classifyNetError, NET_REASON_KEYS, DEAD_AFTER_MS }; diff --git a/src/models/phone_model.js b/src/models/phone_model.js new file mode 100644 index 0000000..4dabb6b --- /dev/null +++ b/src/models/phone_model.js @@ -0,0 +1,325 @@ +const fs = require('fs'); +const pull = require('../server/node_modules/pull-stream'); +const sharedState = require('../configs/shared-state'); +const { notify: desktopNotify, i18nNow, displayName } = require('../backend/desktopNotify'); +const { statePath } = require('../configs/state-manager'); + +const HISTORY_MAX = 200; +const HEARD_MAX = 1000; +const LATEST_MAX = 5; +const PUBS_TTL_MS = 5 * 60 * 1000; +const RECOUNT_DELAY_MS = 500; +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/; + +const readJson = (p, fallback) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return fallback; } }; +const writeJson = (p, data) => { try { fs.writeFileSync(p, JSON.stringify(data, null, 2)); } catch (_) {} }; +const asP = (fn, ...args) => new Promise((resolve, reject) => fn(...args, (err, v) => err ? reject(err) : resolve(v))); +const rpcValue = (fn, ...args) => new Promise((resolve) => { + try { + const r = fn(...args, (err, v) => resolve(err ? null : v)); + if (r && typeof r.then === 'function') r.then(resolve, () => resolve(null)); + else if (r !== undefined) resolve(r); + } catch (_) { resolve(null); } +}); + +module.exports = ({ cooler, pmModel, nameOf, isPublic, encryptFile, isAvailable, prefetch }) => { + const historyFile = statePath('phone-history.json'); + const seenFile = statePath('phone-seen.json'); + let ssb = null; + let subscribed = false; + let pubsAt = 0; + let pubsTimer = null; + + const open = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const phone = async () => { const s = await open(); return s && s.phone ? s.phone : null; }; + const history = () => { const h = readJson(historyFile, []); return Array.isArray(h) ? h : []; }; + const seen = () => { + const s = readJson(seenFile, {}); + return { missedSeenAt: Number(s.missedSeenAt) || 0, heard: Array.isArray(s.heard) ? s.heard : [] }; + }; + + const refreshCount = async () => { + let n = 0; + const st = sharedState.getPhoneCall ? sharedState.getPhoneCall() : null; + if (st && st.dir === 'in' && st.phase === 'incoming') n += 1; + const sn = seen(); + n += history().filter(h => h && h.outcome === 'missed' && h.endedAt > sn.missedSeenAt).length; + try { + const me = (await open()).id; + const heard = new Set(sn.heard); + n += (await pmModel.listPams()).filter(m => m.value.author !== me && !heard.has(m.key)).length; + } catch (_) {} + sharedState.setPhoneCount(n); + return n; + }; + + const notify = async (peer, callId) => { + const i18n = i18nNow(); + let name = ''; + try { name = await nameOf(peer); } catch (_) {} + desktopNotify(i18n.phoneTitle, `${displayName(peer, name)} ${i18n.phoneIsCalling}`, callId ? `call:${callId}` : null, { + timeoutMs: require('../server/phone_module').RING_MS, + actions: [{ key: 'answer', label: i18n.phoneAnswer }, { key: 'reject', label: i18n.phoneReject }], + onAction: async (chosen) => { + try { + const ph = await phone(); + const st = ph ? await rpcValue(ph.state) : null; + if (!st || st.id !== callId || st.phase !== 'incoming') return; + await act(chosen === 'answer' ? 'accept' : 'reject'); + } catch (_) {} + } + }); + }; + + const ROOM_EVENT_TEXT = { join: 'roomEventJoined', leave: 'roomEventLeft', recStart: 'roomEventRecStart', recStop: 'roomEventRecStop', mute: 'roomEventMuted', unmute: 'roomEventUnmuted', hand: 'roomEventHand' }; + const roomNotices = { rid: null, since: 0, seen: new Set() }; + const announceRoom = async (room) => { + if (!room || !room.rid) { roomNotices.rid = null; roomNotices.seen = new Set(); return; } + if (roomNotices.rid !== room.rid) { roomNotices.rid = room.rid; roomNotices.since = Number(room.joinedAt) || Date.now(); roomNotices.seen = new Set(); } + if (!room.notify) { roomNotices.since = Date.now() + 1; return; } + const events = Array.isArray(room.events) ? room.events : []; + const keyOf = (e) => `${e.ts}:${e.t}:${e.id}`; + const fresh = events.filter(e => e && ROOM_EVENT_TEXT[e.t] && !roomNotices.seen.has(keyOf(e)) && Number(e.ts) >= roomNotices.since); + roomNotices.seen = new Set(events.filter(Boolean).map(keyOf)); + if (!fresh.length) return; + let me = null; + try { me = (await open()).id; } catch (_) {} + const i18n = i18nNow(); + for (const e of fresh) { + if (e.id === me) continue; + let name = ''; + try { name = await nameOf(e.id); } catch (_) {} + desktopNotify(i18n.roomsTitle, String(i18n[ROOM_EVENT_TEXT[e.t]] || '{name}').replace('{name}', displayName(e.id, name))); + } + }; + + const subscribe = async () => { + if (subscribed) return; + const ph = await phone(); + if (!ph || subscribed) return; + subscribed = true; + pull(ph.events(), pull.drain((ev) => { + if (!ev) return; + if (ev.type === 'state') { + sharedState.setPhoneCall(ev.state || null); + refreshCount(); + } else if (ev.type === 'room') { + sharedState.setPhoneRoom(ev.room || null); + announceRoom(ev.room).catch(() => {}); + } else if (ev.type === 'recordLimit') { + finishPam().catch(() => {}); + } else if (ev.type === 'incoming') { + notify(ev.peer, ev.id); + } else if (ev.type === 'ended' && ev.call) { + const h = history(); + h.unshift(ev.call); + writeJson(historyFile, h.slice(0, HISTORY_MAX)); + refreshCount(); + } + }, () => { + subscribed = false; + const retry = setTimeout(() => { subscribe().catch(() => {}); }, 5000); + if (retry.unref) retry.unref(); + })); + try { sharedState.setPhoneCall((await rpcValue(ph.state)) || null); } catch (_) {} + try { if (typeof ph.roomState === 'function') sharedState.setPhoneRoom((await rpcValue(ph.roomState)) || null); } catch (_) {} + }; + + let arrivalsWatched = false; + let recountTimer = null; + const scheduleRecount = () => { + if (recountTimer) return; + recountTimer = setTimeout(() => { recountTimer = null; refreshCount().catch(() => {}); }, RECOUNT_DELAY_MS); + if (recountTimer.unref) recountTimer.unref(); + }; + const watchArrivals = async () => { + if (arrivalsWatched) return; + const s = await open(); + if (!s || !s.db || typeof s.db.onMsgAdded !== 'function') return; + arrivalsWatched = true; + s.db.onMsgAdded((ev) => { + const v = ev && ev.kvt && ev.kvt.value; + if (!v || v.author === s.id || typeof v.content !== 'string') return; + scheduleRecount(); + }, false); + }; + + const act = async (method, ...args) => { + await subscribe(); + const ph = await phone(); + if (!ph) throw new Error('unavailable'); + return asP(ph[method], ...args); + }; + + const finishPam = async () => { + const ph = await phone(); + if (!ph) return false; + const out = await asP(ph.recordStop); + if (!out || !out.file) return false; + try { + const enc = await encryptFile(out.file); + await pmModel.sendPam(out.peer, enc.cipher, Number(out.durationMs) / 1000); + return true; + } finally { + fs.unlink(out.file, () => {}); + } + }; + + const findPam = async (key) => (await pmModel.listPams()).find(m => m.key === key) || null; + + const byType = (s, type) => new Promise((resolve) => { + if (!s || typeof s.messagesByType !== 'function') return resolve([]); + try { pull(s.messagesByType({ type }), pull.collect((err, msgs) => resolve(err ? [] : msgs))); } catch (_) { resolve([]); } + }); + const defaultPubId = () => { + const m = String((require('../configs/config-manager').defaultInvite() || {}).code || '').match(/(@[A-Za-z0-9+/]{43}=\.ed25519)/); + return m ? m[1] : ''; + }; + let announcedPubs = new Set(); + const refreshPubs = async (force = false) => { + const ids = new Set(); + const add = (id) => { if (FEED_ID.test(String(id || ''))) ids.add(String(id)); }; + try { + const s = await open(); + if (force || Date.now() - pubsAt >= PUBS_TTL_MS) { + pubsAt = Date.now(); + const found = new Set(); + for (const m of await byType(s, 'pub')) { const v = m && m.value; const k = v && v.content && v.content.address && v.content.address.key; if (FEED_ID.test(String(k || '')) && (v.author === k || v.author === s.id)) found.add(String(k)); } + for (const m of await byType(s, 'pubAvailability')) { const k = m && m.value && m.value.author; if (FEED_ID.test(String(k || ''))) found.add(String(k)); } + announcedPubs = found; + } + add(defaultPubId()); + for (const id of announcedPubs) add(id); + try { const ph = s.phone; if (ph && typeof ph.pubs === 'function') for (const id of (await rpcValue(ph.pubs)) || []) add(id); } catch (_) {} + ids.delete(s.id); + } catch (_) { add(defaultPubId()); } + sharedState.setPubIds(ids); + return ids; + }; + const isPub = (id) => sharedState.isPubId(id); + + return { + async start() { + await subscribe(); + await watchArrivals().catch(() => {}); + await refreshCount(); + await refreshPubs(true); + if (!pubsTimer) { pubsTimer = setInterval(() => { refreshPubs(true).catch(() => {}); }, PUBS_TTL_MS); if (pubsTimer.unref) pubsTimer.unref(); } + }, + refreshCount, + refreshPubs, + isPub, + async available() { + if (isPublic) return false; + const ph = await phone(); + return !!(ph && await rpcValue(ph.available)); + }, + async state() { + await subscribe(); + const ph = await phone(); + return ph ? rpcValue(ph.state) : null; + }, + async call(to, opts = {}) { + await refreshPubs(); + const list = Array.isArray(to) ? to : [to]; + if (list.some(isPub)) throw new Error('pub'); + return act('call', list.length === 1 ? list[0] : list, { legacy: Array.isArray(opts.legacy) ? opts.legacy : [] }); + }, + accept: () => act('accept'), + reject: () => act('reject'), + hangup: () => act('end'), + mute: (flag) => act('mute', !!flag), + camera: (flag) => act('camera', !!flag), + async videoFrames(peer) { + const ph = await phone(); + return ph && typeof ph.videoFrames === 'function' ? ph.videoFrames({ peer: String(peer || '') }) : null; + }, + silence: (id, flag) => act('silence', { id, on: !!flag }), + dismiss: () => act('dismiss'), + recordCancel: () => act('recordCancel'), + async sendPam() { await subscribe(); return finishPam(); }, + history, + async markAllHeard(keys) { + const s = seen(); + const set = new Set(s.heard); + for (const k of keys || []) if (typeof k === 'string' && k) set.add(k); + s.heard = [...set].slice(-HEARD_MAX); + writeJson(seenFile, s); + refreshCount(); + }, + deleteHistory(ids) { + const drop = new Set((ids || []).map(String)); + if (!drop.size) return; + writeJson(historyFile, history().filter(h => !drop.has(String(h && h.id)))); + }, + latestCalls(items) { + const me = ssb && ssb.id; + const out = []; + const seenPeers = new Set(); + for (const h of items || history()) { + for (const peer of (Array.isArray(h.peers) && h.peers.length ? h.peers : [h.peer])) { + if (!peer || peer === me || seenPeers.has(peer) || isPub(peer)) continue; + seenPeers.add(peer); + out.push({ id: peer, dir: h.dir, outcome: h.outcome, startedAt: h.startedAt }); + if (out.length >= LATEST_MAX) return out; + } + } + return out; + }, + async pams() { + const me = (await open()).id; + const heard = new Set(seen().heard); + const validShare = (s) => !!s && typeof s === 'object' && typeof s.key === 'string' && typeof s.manifestBlobId === 'string'; + const list = (await pmModel.listPams()) + .filter(m => m.value.author !== me && validShare(m.value.content && m.value.content.share)) + .map(m => ({ key: m.key, from: m.value.author, sentAt: m.value.content.sentAt || new Date(m.timestamp || 0).toISOString(), durationSec: Number(m.value.content.durationSec) || 0, heard: heard.has(m.key), share: m.value.content.share, ready: true })) + .sort((a, b) => String(b.sentAt).localeCompare(String(a.sentAt))); + if (typeof isAvailable === 'function') { + for (const item of list) { + item.ready = await isAvailable(item.share).catch(() => false); + if (!item.ready && typeof prefetch === 'function') prefetch(item.share).catch(() => {}); + } + } + return list.map(({ share, ...rest }) => rest); + }, + async pamCipher(key) { + const m = await findPam(key); + const share = m && m.value && m.value.content && m.value.content.share; + if (!share || typeof share.key !== 'string' || typeof share.manifestBlobId !== 'string') return null; + return share; + }, + markHeard(key) { + const s = seen(); + if (s.heard.includes(key)) return false; + s.heard.push(key); + s.heard = s.heard.slice(-HEARD_MAX); + writeJson(seenFile, s); + refreshCount(); + return true; + }, + async deletePam(key) { + const m = await findPam(key); + if (!m) return null; + return pmModel.deleteMessageById(key); + }, + async knownFeeds() { + const s = await open(); + await refreshPubs(); + const hops = await new Promise((resolve) => { + try { s.friends.hops({ start: s.id, max: 2 }, (err, h) => resolve(err ? {} : (h || {}))); } catch (_) { resolve({}); } + }); + const out = new Map(); + for (const [id, hop] of Object.entries(hops)) { + if (id === s.id || isPub(id) || !(Number(hop) >= 0)) continue; + out.set(id, Number(hop)); + } + return out; + }, + markMissedSeen() { + const s = seen(); + s.missedSeenAt = Date.now(); + writeJson(seenFile, s); + refreshCount(); + } + }; +}; diff --git a/src/models/phone_number.js b/src/models/phone_number.js new file mode 100644 index 0000000..c15a46e --- /dev/null +++ b/src/models/phone_number.js @@ -0,0 +1,28 @@ +const crypto = require('crypto'); + +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/; +const MSG_ID = /^%[A-Za-z0-9+/]{43}=?\.sha256$/; + +const digitsOf = (id) => { + const n = crypto.createHash('sha256').update(String(id)).digest().readUInt32BE(0) % 1000000; + const s = String(n).padStart(6, '0'); + return `${s.slice(0, 3)}-${s.slice(3)}`; +}; +const phoneNumberOf = (id) => (FEED_ID.test(String(id || '')) ? digitsOf(id) : null); +const roomNumberOf = (id) => (MSG_ID.test(String(id || '')) ? digitsOf(id) : null); + +const normalizeNumber = (value) => { + const digits = String(value || '').replace(/[^0-9A-Za-z]/g, ''); + return /^\d{6}$/.test(digits) ? `${digits.slice(0, 3)}-${digits.slice(3)}` : null; +}; + +const resolveNumber = (value, known) => { + const number = normalizeNumber(value); + if (!number) return []; + return [...(known || new Map())] + .filter(([id]) => phoneNumberOf(id) === number) + .map(([id, hop]) => ({ id, hop: Number(hop) })) + .sort((a, b) => a.hop - b.hop); +}; + +module.exports = { phoneNumberOf, roomNumberOf, normalizeNumber, resolveNumber }; diff --git a/src/models/pixelia_model.js b/src/models/pixelia_model.js index 1b14854..04c91ff 100644 --- a/src/models/pixelia_model.js +++ b/src/models/pixelia_model.js @@ -1,8 +1,72 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const PIXELIA_TYPES = ['pixelia', 'tombstone']; +const COLOR_RE = /^#[0-9a-fA-F]{6}$/; + +const pixelCoordinate = (c) => { + if (!Number.isInteger(c.x) || !Number.isInteger(c.y) || c.x < 0 || c.x > 49 || c.y < 0 || c.y > 199) return null; + const key = `${c.x + 1}:${c.y + 1}`; + if (c.coordinateKey != null && c.coordinateKey !== key) return null; + return key; +}; + +const resolvePixels = (messages) => { + const tombstoned = buildValidatedTombstoneSet(messages); + const byKey = new Map(); + + for (const m of messages) { + const c = m.value?.content; + if (!c || c.type !== 'pixelia' || !m.key) continue; + const coordinateKey = pixelCoordinate(c); + if (!coordinateKey || typeof c.color !== 'string' || !COLOR_RE.test(c.color)) continue; + byKey.set(m.key, { m, coordinateKey }); + } + + const replaced = new Set(); + const parentOf = (k) => { + const { m, coordinateKey } = byKey.get(k); + const prev = byKey.get(m.value.content.replaces); + return prev && prev.coordinateKey === coordinateKey ? prev.m.key : null; + }; + for (const k of byKey.keys()) { + const parent = parentOf(k); + if (parent) replaced.add(parent); + } + + const depthOf = new Map(); + const depth = (key) => { + const chain = []; + const inChain = new Set(); + let k = key; + while (k && !depthOf.has(k) && !inChain.has(k)) { + chain.push(k); + inChain.add(k); + k = parentOf(k); + } + let d = k && depthOf.has(k) ? depthOf.get(k) + 1 : 0; + for (let i = chain.length - 1; i >= 0; i--) depthOf.set(chain[i], d++); + return depthOf.get(key); + }; + + const latest = new Map(); + for (const [k, { m, coordinateKey }] of byKey) { + if (replaced.has(k) || tombstoned.has(k)) continue; + const cur = latest.get(coordinateKey); + if (!cur) { latest.set(coordinateKey, m); continue; } + const d = depth(k); + const curD = depth(cur.key); + const ts = m.value.timestamp || 0; + const curTs = cur.value.timestamp || 0; + if (d > curD || (d === curD && (ts > curTs || (ts === curTs && k > cur.key)))) latest.set(coordinateKey, m); + } + + return latest; +}; + module.exports = ({ cooler }) => { let ssb; @@ -13,52 +77,22 @@ module.exports = ({ cooler }) => { const getPixelByCoordinate = async (coordinateKey) => { const ssbClient = await openSsb(); - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); - - const tombstoned = buildValidatedTombstoneSet(messages); - - const replaces = new Map(); - const byId = new Map(); - - for (const m of messages) { - const c = m.value?.content; - const k = m.key; - if (!c || c.type !== 'pixelia' || c.coordinateKey !== coordinateKey) continue; - if (tombstoned.has(k)) continue; - if (c.replaces) replaces.set(c.replaces, k); - byId.set(k, m); - } - - for (const [replacedId, replacingId] of replaces.entries()) { - const orig = byId.get(replacedId); - if (!orig) continue; - const rep = byId.get(replacingId); - if (!rep || rep.value.author !== orig.value.author) { - byId.delete(replacingId); - continue; - } - byId.delete(replacedId); - } - - return [...byId.values()][0] || null; + const messages = await readTyped(ssbClient, PIXELIA_TYPES, { limit: logLimit }); + return resolvePixels(messages).get(coordinateKey) || null; }; const paintPixel = async (x, y, color) => { - if (x < 1 || x > 50 || y < 1 || y > 200) { + if (!Number.isInteger(x) || !Number.isInteger(y) || x < 1 || x > 50 || y < 1 || y > 200) { throw new Error('Coordinates out of bounds. Please use x (1-50) and y (1-200)'); } + if (typeof color !== 'string' || !COLOR_RE.test(color)) throw new Error('Invalid color'); const ssbClient = await openSsb(); const userId = ssbClient.id; const coordinateKey = `${x}:${y}`; const existingPixel = await getPixelByCoordinate(coordinateKey); - if (existingPixel) { + if (existingPixel && existingPixel.value.author === userId) { const tombstone = { type: 'tombstone', target: existingPixel.key, @@ -69,7 +103,7 @@ module.exports = ({ cooler }) => { ); } - const contributors = existingPixel?.value?.content?.contributors_inhabitants || []; + const contributors = Array.isArray(existingPixel?.value?.content?.contributors_inhabitants) ? existingPixel.value.content.contributors_inhabitants : []; const contributors_inhabitants = contributors.includes(userId) ? contributors : [...contributors, userId]; @@ -93,46 +127,14 @@ module.exports = ({ cooler }) => { const listPixels = async () => { const ssbClient = await openSsb(); - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const messages = await readTyped(ssbClient, PIXELIA_TYPES, { limit: logLimit }); - const tombstoned = buildValidatedTombstoneSet(messages); - const replaces = new Map(); - const byKey = new Map(); - - for (const m of messages) { - const c = m.value?.content; - const k = m.key; - if (!c) continue; - if (c.type === 'tombstone') continue; - if (c.type === 'pixelia') { - if (tombstoned.has(k)) continue; - if (c.replaces) replaces.set(c.replaces, k); - byKey.set(k, m); - } - } - - for (const [replacedId, replacingId] of replaces.entries()) { - const orig = byKey.get(replacedId); - if (!orig) continue; - const rep = byKey.get(replacingId); - if (!rep || rep.value.author !== orig.value.author) { - byKey.delete(replacingId); - continue; - } - byKey.delete(replacedId); - } - - return Array.from(byKey.values()).map(m => ({ + return Array.from(resolvePixels(messages).values()).map(m => ({ x: m.value.content.x + 1, y: m.value.content.y + 1, color: m.value.content.color, author: m.value.author, - contributors_inhabitants: m.value.content.contributors_inhabitants || [], + contributors_inhabitants: [...new Set([...(Array.isArray(m.value.content.contributors_inhabitants) ? m.value.content.contributors_inhabitants : []).filter(id => typeof id === 'string'), m.value.author])], timestamp: m.value.timestamp })); }; diff --git a/src/models/pm_model.js b/src/models/pm_model.js index 03be1d3..3bbd6c1 100644 --- a/src/models/pm_model.js +++ b/src/models/pm_model.js @@ -1,5 +1,89 @@ const pull = require('../server/node_modules/pull-stream'); -const util = require('../server/node_modules/util'); +const util = require('util'); +const longText = require('../backend/long_text'); + +const fs = require('fs'); + +const INBOX_READ_FILE = 'oasis-inbox-read'; +const INBOX_ARCHIVE_FILE = 'oasis-inbox-archived'; +const flagPath = (name) => { + try { return require('../configs/state-manager').statePath(name); } catch (_) { return null; } +}; +const readFlagSet = (name) => { + const file = flagPath(name); + if (!file) return new Set(); + try { const arr = JSON.parse(fs.readFileSync(file, 'utf8')); return new Set(Array.isArray(arr) ? arr.map(String) : []); } catch (_) { return new Set(); } +}; +const writeFlagSet = (name, set) => { + const file = flagPath(name); + if (!file) return false; + try { fs.writeFileSync(file, JSON.stringify(Array.from(set))); return true; } catch (_) { return false; } +}; +const readKeys = () => readFlagSet(INBOX_READ_FILE); +const markRead = (key) => { if (!key) return false; const set = readKeys(); set.add(String(key)); return writeFlagSet(INBOX_READ_FILE, set); }; +const markUnread = (key) => { if (!key) return false; const set = readKeys(); set.delete(String(key)); return writeFlagSet(INBOX_READ_FILE, set); }; +const markReadMany = (keys) => { const set = readKeys(); for (const k of (keys || [])) { if (k) set.add(String(k)); } return writeFlagSet(INBOX_READ_FILE, set); }; +const archivedKeys = () => readFlagSet(INBOX_ARCHIVE_FILE); +const archive = (key) => { if (!key) return false; const set = archivedKeys(); set.add(String(key)); return writeFlagSet(INBOX_ARCHIVE_FILE, set); }; +const unarchive = (key) => { if (!key) return false; const set = archivedKeys(); set.delete(String(key)); return writeFlagSet(INBOX_ARCHIVE_FILE, set); }; + +const INBOX_BOTS = { + blogs: ['BLOG_NEW'], + jobs: ['JOB_MATCH', 'JOB_SUBSCRIBED', 'JOB_UNSUBSCRIBED'], + projects: ['PROJECT_FOLLOWED', 'PROJECT_UNFOLLOWED', 'PROJECT_PLEDGE'], + market: ['MARKET_SOLD'], + shops: ['SHOP_SOLD'], + political: ['LARP_RULING', 'PARLIAMENT_GOV', 'TRIBE_GOV'], + banking: ['BANKING_UBI_PAID', 'BANKING_UBI_AVAILABLE', 'BANKING_CONFIRM_PENDING', 'WALLET_PAYMENT'], + school: ['SCHOOL_ENROLLED', 'SCHOOL_INVITED', 'SCHOOL_ADMITTED', 'SCHOOL_CERTIFICATE', 'SCHOOL_PASSED', 'SCHOOL_LESSON_NEW'], + industry: ['INDUSTRY_ADMITTED', 'INDUSTRY_APPLICATION', 'INDUSTRY_INVITED', 'INDUSTRY_DISSOLVED', 'INDUSTRY_BUILD_APPROVED', 'INDUSTRY_DISTRIBUTED'], + housing: ['HOUSING_REQUESTED', 'HOUSING_CANCELLED', 'HOUSING_UNAVAILABLE'], + wiki: ['WIKI_EDITED', 'WIKI_RESTORED'], + emergencies: ['EMERGENCY_UPDATED', 'EMERGENCY_RESOLVED'], + podcasts: ['PODCAST_EPISODE'], + campaigns: ['CAMPAIGN_UPDATED', 'CAMPAIGN_ACHIEVED', 'CAMPAIGN_RAISED'], + logistics: ['LOGISTICS_UPDATED', 'LOGISTICS_CLOSED', 'LOGISTICS_BOOKED', 'LOGISTICS_CANCELLED', 'LOGISTICS_CONFIRMED', 'LOGISTICS_REJECTED', 'LOGISTICS_DELIVERED'] +}; +const BOT_BY_SUBJECT = new Map(); +for (const [bot, subjects] of Object.entries(INBOX_BOTS)) for (const s of subjects) BOT_BY_SUBJECT.set(s, bot); +const isReminderSubject = (s) => /^(Task Reminder:|Calendar Reminder:)/i.test(String(s || '')); +const botOf = (content) => { + const c = content || {}; + if (isReminderSubject(c.subject)) return 'reminders'; + if (c.meta && c.meta.type === 'project-pledge') return 'projects'; + return BOT_BY_SUBJECT.get(String(c.subject || '').toUpperCase()) || null; +}; +const mutedBots = (cfg) => { + const c = cfg || {}; + return new Set(Array.isArray(c.inboxMutedBots) ? c.inboxMutedBots.map(String) : []); +}; + +const privateCaches = new WeakMap(); +const privateCacheFor = (ssb, userId) => { + let byUser = privateCaches.get(ssb); + if (!byUser) { byUser = new Map(); privateCaches.set(ssb, byUser); } + let c = byUser.get(userId); + if (!c) { + c = { lastTs: 0, seen: new Set(), posts: new Map(), pams: new Map(), chunks: new Map(), tombClaims: new Map(), authorByKey: new Map(), recpsByKey: new Map(), version: 0 }; + byUser.set(userId, c); + } + return c; +}; + +const tombedKeys = (cache, userId) => { + const tombed = new Set(); + for (const [target, tombAuthors] of cache.tombClaims.entries()) { + const origAuthor = cache.authorByKey.get(target); + const origRecps = cache.recpsByKey.get(target) || []; + for (const tombAuthor of tombAuthors) { + if (tombAuthor === origAuthor || tombAuthor === userId || origRecps.includes(tombAuthor)) { + tombed.add(target); + break; + } + } + } + return tombed; +}; module.exports = ({ cooler }) => { let ssb; @@ -26,12 +110,35 @@ module.exports = ({ cooler }) => { } return { + readKeys, + markRead, + markUnread, + markReadMany, + archivedKeys, + archive, + unarchive, + botOf, + mutedBots, + INBOX_BOTS, type: 'post', - async sendMessage(recipients = [], subject = '', text = '', crypter = false) { + async sendToMany(recipients = [], subject = '', text = '', crypter = false, ref = '') { + const others = uniqueRecps(recipients).filter(id => id !== userId); + if (!others.length) return []; + const out = []; + for (let i = 0; i < others.length; i += 6) { + const batch = others.slice(i, i + 6); + const r = await this.sendMessage(batch, subject, text, crypter, ref).catch(() => null); + if (r) out.push(r); + } + return out; + }, + + async sendMessage(recipients = [], subject = '', text = '', crypter = false, ref = '') { const ssbClient = await openSsb(); const recps = uniqueRecps([userId, ...recipients]); - const content = { + const publishAsync = util.promisify(ssbClient.private.publish); + const content = await longText.chunkContent({ type: 'post', from: userId, to: recps, @@ -39,6 +146,38 @@ module.exports = ({ cooler }) => { text, sentAt: new Date().toISOString(), private: true, + ...(ref ? { ref: String(ref) } : {}), + ...(crypter ? { crypter: true } : {}) + }, 'text', { maxBytes: longText.CONTENT_BYTES.boxed, extra: { private: true }, publish: (c) => publishAsync(c, recps) }); + return publishAsync(content, recps); + }, + + async sentRefs() { + const out = new Set(); + let messages = []; + try { messages = await this.listAllPrivate({ includeDeleted: true }); } catch (_) { return out; } + for (const m of (messages || [])) { + const c = m && m.value && m.value.content; + if (!c || !c.ref) continue; + if (m.value.author !== userId) continue; + out.add(`${String(c.subject || '')}|${String(c.ref)}`); + } + return out; + }, + + async sendFileShare(recipients = [], subject = '', fileShare = null, crypter = false) { + const ssbClient = await openSsb(); + const recps = uniqueRecps([userId, ...recipients]); + if (!fileShare || typeof fileShare !== 'object') throw new Error('Invalid file share'); + const content = { + type: 'post', + from: userId, + to: recps, + subject, + text: '', + sentAt: new Date().toISOString(), + private: true, + fileShare, ...(crypter ? { crypter: true } : {}) }; const publishAsync = util.promisify(ssbClient.private.publish); @@ -83,22 +222,23 @@ module.exports = ({ cooler }) => { return publishAsync(tombstone, tombstoneRecps); }, - async listAllPrivate() { + async listAllPrivate(opts = {}) { + const includeDeleted = !!(opts && opts.includeDeleted); const ssbClient = await openSsb(); + const cache = privateCacheFor(ssbClient, userId); const raw = await new Promise((resolve, reject) => { pull( - ssbClient.createLogStream({ reverse: false }), + ssbClient.createLogStream(cache.lastTs > 0 ? { reverse: false, gte: cache.lastTs } : { reverse: false }), pull.collect((err, arr) => err ? reject(err) : resolve(arr)) ); }); - const posts = []; - const tombed = new Set(); - const tombClaims = new Map(); - const authorByKey = new Map(); - const recpsByKey = new Map(); for (const m of raw) { if (!m || !m.value) continue; const keyIn = m.key || m.value?.key || m.value?.hash || ''; + if (!keyIn || cache.seen.has(keyIn)) continue; + cache.seen.add(keyIn); + const recvTs = Number(m.timestamp) || 0; + if (recvTs > cache.lastTs) cache.lastTs = recvTs; const valueIn = m.value || m; const tsIn = m.timestamp || m.value?.timestamp || Date.now(); let dec; @@ -112,18 +252,23 @@ module.exports = ({ cooler }) => { const k = dec?.key || keyIn; if (!c || c.private !== true || !k) continue; if (c.type === 'tombstone' && c.target) { - const set = tombClaims.get(c.target) || new Set(); + const set = cache.tombClaims.get(c.target) || new Set(); set.add(v.author); - tombClaims.set(c.target, set); + cache.tombClaims.set(c.target, set); continue; } - authorByKey.set(k, v.author); - if (c.type === 'post') { + if (c.type === longText.CHUNK_TYPE) { + const ch = longText.chunkOf(v.author, c); + if (ch) cache.chunks.set(k, ch); + continue; + } + cache.authorByKey.set(k, v.author); + if (c.type === 'post' || c.type === 'pam') { const to = Array.isArray(c.to) ? c.to : []; - recpsByKey.set(k, to); + cache.recpsByKey.set(k, to); const author = v.author; if (author === userId || to.includes(userId)) { - posts.push({ + (c.type === 'pam' ? cache.pams : cache.posts).set(k, { key: k, value: { author, content: c }, timestamp: v.timestamp || tsIn @@ -131,17 +276,48 @@ module.exports = ({ cooler }) => { } } } - for (const [target, tombAuthors] of tombClaims.entries()) { - const origAuthor = authorByKey.get(target); - const origRecps = recpsByKey.get(target) || []; - for (const tombAuthor of tombAuthors) { - if (tombAuthor === origAuthor || tombAuthor === userId || origRecps.includes(tombAuthor)) { - tombed.add(target); - break; - } - } - } - return posts.filter(m => m && m.key && !tombed.has(m.key)); + cache.version += raw.length ? 1 : 0; + const tombed = tombedKeys(cache, userId); + const lookup = longText.lookupIn(cache.chunks); + return Array.from(cache.posts.values()) + .filter(m => m && m.key && (includeDeleted || !tombed.has(m.key))) + .map(m => longText.hasChunks(m.value.content) + ? { ...m, value: { ...m.value, content: longText.resolveField(m.value.content, 'text', m.value.author, lookup) } } + : m); + }, + + async listPams() { + await this.listAllPrivate(); + const cache = privateCacheFor(ssb, userId); + const tombed = tombedKeys(cache, userId); + return Array.from(cache.pams.values()).filter(m => m && m.key && !tombed.has(m.key)); + }, + + async sendPam(recipient, share, durationSec) { + const ssbClient = await openSsb(); + if (!share || typeof share !== 'object') throw new Error('Invalid audio'); + const recps = uniqueRecps([userId, recipient]); + const content = { + type: 'pam', + from: userId, + to: recps, + share, + durationSec: Math.max(0, Math.round(Number(durationSec) || 0)), + sentAt: new Date().toISOString(), + private: true + }; + const publishAsync = util.promisify(ssbClient.private.publish); + return publishAsync(content, recps); + }, + + privateCacheVersion() { + return ssb ? privateCacheFor(ssb, userId).version : 0; } }; }; + +module.exports.botOf = botOf; +module.exports.mutedBots = mutedBots; +module.exports.INBOX_BOTS = INBOX_BOTS; +module.exports.readKeys = readKeys; +module.exports.archivedKeys = archivedKeys; diff --git a/src/models/podcasts_model.js b/src/models/podcasts_model.js new file mode 100644 index 0000000..5d0d6a7 --- /dev/null +++ b/src/models/podcasts_model.js @@ -0,0 +1,329 @@ +const { readTyped, memoIndex } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const categories = require('../backend/opinion_categories'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const CHANNEL_TYPE = 'podcast'; +const EPISODE_TYPE = 'podcastEpisode'; +const OPINION_TYPE = 'podcastOpinion'; +const PLAY_TYPE = 'podcastPlay'; +const TYPES = [CHANNEL_TYPE, EPISODE_TYPE, OPINION_TYPE, PLAY_TYPE, 'tombstone']; +const CATEGORIES = ['NEWS', 'MUSIC', 'TALK', 'EDUCATION', 'TECH', 'CULTURE', 'COMMUNITY', 'OASIS', 'OTHER']; +const RECENT_MS = 24 * 60 * 60 * 1000; +const MEDIA_RE = /!?\[(image|audio|video):([^\]]*)\]\((&[^)]+)\)/; + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normU = (v) => String(v || '').trim().toUpperCase(); +const normalizeCategory = (v) => (CATEGORIES.includes(normU(v)) ? normU(v) : 'OTHER'); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const num = (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0; }; + +const parseMedia = (raw) => { + if (!raw) return null; + if (typeof raw === 'object' && raw.blobId) return { kind: String(raw.kind || 'audio'), name: safeText(raw.name), blobId: String(raw.blobId) }; + const m = String(raw).match(MEDIA_RE); + if (!m) return null; + return { kind: m[1], name: safeText(m[2]) || 'file', blobId: m[3] }; +}; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const parentOf = new Map(); + const opinions = []; + const plays = []; + for (const m of messages) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === CHANNEL_TYPE || c.type === EPISODE_TYPE) { + nodes.set(m.key, { key: m.key, type: c.type, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } else if (c.type === OPINION_TYPE && typeof c.target === 'string') { + opinions.push({ target: c.target, author: v.author, category: String(c.category || ''), ts: v.timestamp || 0 }); + } else if (c.type === PLAY_TYPE && typeof c.target === 'string') { + plays.push({ target: c.target, author: v.author, ts: v.timestamp || 0 }); + } + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const tips = new Map(); + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author && v.type === first.type); + tips.set(root, { root, first, tip: own[own.length - 1] }); + } + const episodes = new Map(); + const opinionsByRoot = new Map(); + for (const op of opinions) { + if (!nodes.has(op.target)) continue; + const r = rootOf(op.target); + if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); + opinionsByRoot.get(r).push(op); + } + const playsByRoot = new Map(); + for (const pl of plays) { + if (!nodes.has(pl.target)) continue; + const r = rootOf(pl.target); + if (!playsByRoot.has(r)) playsByRoot.set(r, new Set()); + playsByRoot.get(r).add(pl.author); + } + for (const { root, first, tip } of tips.values()) { + if (first.type !== EPISODE_TYPE) continue; + const c = tip.c; + const channelId = safeText(c.channel); + if (!channelId) continue; + const ops = {}; + const voters = []; + for (const op of (opinionsByRoot.get(root) || [])) { + if (voters.includes(op.author) || !categories.includes(op.category)) continue; + voters.push(op.author); + ops[op.category] = (ops[op.category] || 0) + 1; + } + const listeners = Array.from(playsByRoot.get(root) || []); + episodes.set(root, { + id: root, rootId: root, tipId: tip.key, channelId, + number: num(c.number), title: safeText(c.title), description: safeText(c.description), + media: parseMedia(c.media), tags: normalizeTags(c.tags), + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, publishedTs: Date.parse(first.c.createdAt || '') || first.ts, + opinions: ops, opinions_inhabitants: voters, opinionCount: voters.length, + listeners, playCount: listeners.length + }); + } + const channels = new Map(); + for (const { root, first, tip } of tips.values()) { + if (first.type !== CHANNEL_TYPE) continue; + const c = tip.c; + const eps = Array.from(episodes.values()).filter(e => e.channelId === root && e.author === first.author).sort((a, b) => a.publishedTs - b.publishedTs || a.number - b.number); + eps.forEach((e, i) => { if (!e.number) e.number = i + 1; }); + const playCount = eps.reduce((s, e) => s + e.playCount, 0); + const ops = {}; + const voters = []; + for (const op of (opinionsByRoot.get(root) || [])) { + if (voters.includes(op.author) || !categories.includes(op.category)) continue; + voters.push(op.author); + ops[op.category] = (ops[op.category] || 0) + 1; + } + const opinionCount = voters.length + eps.reduce((s, e) => s + e.opinionCount, 0); + channels.set(root, { + id: root, rootId: root, tipId: tip.key, + title: safeText(c.title), description: safeText(c.description), category: normalizeCategory(c.category), + cover: parseMedia(c.cover), tags: normalizeTags(c.tags), + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, + episodes: eps, episodeCount: eps.length, playCount, opinionCount, + opinions: ops, opinions_inhabitants: voters, + lastEpisodeTs: eps.length ? eps[eps.length - 1].publishedTs : 0, + lastActivityTs: Math.max(tip.ts, ...eps.map(e => e.ts)) + }); + } + return { channels, episodes, rootOf, nodes }; + }; + + const load = async () => { + const ssbClient = await openSsb(); + const idx = memoIndex('podcasts', await readTyped(ssbClient, TYPES, { limit: logLimit }), buildIndex); + return { ssbClient, idx }; + }; + + const findChannel = (idx, id) => { + const key = String(id || ''); + if (idx.channels.has(key)) return idx.channels.get(key); + if (idx.nodes.has(key)) return idx.channels.get(idx.rootOf(key)) || null; + return null; + }; + const findEpisode = (idx, id) => { + const key = String(id || ''); + if (idx.episodes.has(key)) return idx.episodes.get(key); + if (idx.nodes.has(key)) return idx.episodes.get(idx.rootOf(key)) || null; + return null; + }; + + return { + CATEGORIES, + parseMedia, + + async createChannel({ title, description, category, cover, tags }) { + const ssbClient = await openSsb(); + const t = safeText(title); + if (!t) throw new Error('Title is required'); + const now = new Date().toISOString(); + const media = parseMedia(cover); + return publish(ssbClient, { + type: CHANNEL_TYPE, title: t, description: safeText(description), category: normalizeCategory(category), + ...(media ? { cover: media } : {}), tags: normalizeTags(tags), author: ssbClient.id, createdAt: now, updatedAt: now + }); + }, + + async updateChannel(id, data = {}) { + const { ssbClient, idx } = await load(); + const ch = findChannel(idx, id); + if (!ch) throw new Error('Podcast not found'); + if (ch.author !== ssbClient.id) throw new Error('Only the author can update this podcast'); + const media = data.cover !== undefined && data.cover ? parseMedia(data.cover) : ch.cover; + const res = await publish(ssbClient, { + type: CHANNEL_TYPE, + title: safeText(data.title) || ch.title, + description: data.description !== undefined ? safeText(data.description) : ch.description, + category: data.category !== undefined ? normalizeCategory(data.category) : ch.category, + ...(media ? { cover: media } : {}), + tags: data.tags !== undefined ? normalizeTags(data.tags) : ch.tags, + author: ssbClient.id, createdAt: ch.createdAt, updatedAt: new Date().toISOString(), replaces: ch.tipId + }); + return { key: res.key, rootId: ch.id }; + }, + + async deleteChannel(id) { + const { ssbClient, idx } = await load(); + const ch = findChannel(idx, id); + if (!ch) throw new Error('Podcast not found'); + if (ch.author !== ssbClient.id) throw new Error('Only the author can delete this podcast'); + await publish(ssbClient, { type: 'tombstone', target: ch.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: ch.id }; + }, + + async addEpisode(channelId, { title, description, media, number, tags }) { + const { ssbClient, idx } = await load(); + const ch = findChannel(idx, channelId); + if (!ch) throw new Error('Podcast not found'); + if (ch.author !== ssbClient.id) throw new Error('Only the author can publish episodes'); + const m = parseMedia(media); + if (!m || (m.kind !== 'audio' && m.kind !== 'video')) throw new Error('An audio or video file is required'); + const t = safeText(title); + if (!t) throw new Error('Title is required'); + const now = new Date().toISOString(); + const res = await publish(ssbClient, { + type: EPISODE_TYPE, channel: ch.id, number: num(number) || ch.episodeCount + 1, + title: t, description: safeText(description), media: m, tags: normalizeTags(tags), + author: ssbClient.id, createdAt: now, updatedAt: now + }); + return { key: res.key, rootId: res.key, channelId: ch.id }; + }, + + async updateEpisode(id, data = {}) { + const { ssbClient, idx } = await load(); + const ep = findEpisode(idx, id); + if (!ep) throw new Error('Episode not found'); + if (ep.author !== ssbClient.id) throw new Error('Only the author can update this episode'); + const m = data.media ? parseMedia(data.media) : ep.media; + const res = await publish(ssbClient, { + type: EPISODE_TYPE, channel: ep.channelId, number: data.number !== undefined && num(data.number) ? num(data.number) : ep.number, + title: safeText(data.title) || ep.title, description: data.description !== undefined ? safeText(data.description) : ep.description, + media: m, tags: data.tags !== undefined ? normalizeTags(data.tags) : ep.tags, + author: ssbClient.id, createdAt: ep.createdAt, updatedAt: new Date().toISOString(), replaces: ep.tipId + }); + return { key: res.key, rootId: ep.id, channelId: ep.channelId }; + }, + + async deleteEpisode(id) { + const { ssbClient, idx } = await load(); + const ep = findEpisode(idx, id); + if (!ep) throw new Error('Episode not found'); + if (ep.author !== ssbClient.id) throw new Error('Only the author can delete this episode'); + await publish(ssbClient, { type: 'tombstone', target: ep.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: ep.id, channelId: ep.channelId }; + }, + + async markPlayed(id) { + const { ssbClient, idx } = await load(); + const ep = findEpisode(idx, id); + if (!ep) throw new Error('Episode not found'); + if (ep.listeners.includes(ssbClient.id)) return null; + return publish(ssbClient, { type: PLAY_TYPE, target: ep.id, createdAt: new Date().toISOString() }); + }, + + async createOpinion(id, category) { + if (!categories.includes(category)) throw new Error('Invalid voting category'); + const { ssbClient, idx } = await load(); + const target = findEpisode(idx, id) || findChannel(idx, id); + if (!target) throw new Error('Episode not found'); + if (target.opinions_inhabitants.includes(ssbClient.id)) throw new Error('Already voted'); + return publish(ssbClient, { type: OPINION_TYPE, target: target.id, category, createdAt: new Date().toISOString() }); + }, + + async getChannelById(id) { + const { idx } = await load(); + return findChannel(idx, id); + }, + + async getAnyById(id) { + const { idx } = await load(); + return findChannel(idx, id) || findEpisode(idx, id); + }, + + async getEpisodeById(id) { + const { idx } = await load(); + const ep = findEpisode(idx, id); + if (!ep) return null; + return { ...ep, channel: idx.channels.get(ep.channelId) || null }; + }, + + async listAll({ filter = 'all', q = '', category = '', viewerId = null } = {}) { + const { ssbClient, idx } = await load(); + const me = viewerId || ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + const cat = normU(category); + let out = Array.from(idx.channels.values()); + if (f === 'mine') out = out.filter(c => c.author === me); + else if (f === 'recent') out = out.filter(c => c.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'viewers') out = out.filter(c => c.playCount > 0); + else if (f === 'top') out = out.filter(c => c.opinionCount > 0 || (c.spreadCount || 0) > 0); + else if (CATEGORIES.includes(f.toUpperCase())) out = out.filter(c => c.category === f.toUpperCase()); + if (cat && CATEGORIES.includes(cat)) out = out.filter(c => c.category === cat); + if (needle) out = out.filter(c => [c.title, c.description, c.category, ...c.tags, ...c.episodes.map(e => e.title)].some(v => String(v || '').toLowerCase().includes(needle))); + if (f === 'viewers') out.sort((a, b) => b.playCount - a.playCount || b.lastActivityTs - a.lastActivityTs); + else if (f === 'top') out.sort((a, b) => b.opinionCount - a.opinionCount || b.playCount - a.playCount); + else out.sort((a, b) => b.lastActivityTs - a.lastActivityTs); + return out; + }, + + async listEpisodes({ filter = 'all', q = '' } = {}) { + const { ssbClient, idx } = await load(); + const me = ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + let out = Array.from(idx.episodes.values()).filter(e => idx.channels.has(e.channelId)).map(e => ({ ...e, channel: idx.channels.get(e.channelId) })); + if (f === 'mine') out = out.filter(e => e.author === me); + else if (f === 'recent') out = out.filter(e => e.ts >= Date.now() - RECENT_MS); + else if (f === 'viewers') out = out.filter(e => e.playCount > 0); + else if (f === 'top') out = out.filter(e => e.opinionCount > 0); + if (needle) out = out.filter(e => [e.title, e.description, ...e.tags, e.channel.title].some(v => String(v || '').toLowerCase().includes(needle))); + if (f === 'viewers') out.sort((a, b) => b.playCount - a.playCount); + else if (f === 'top') out.sort((a, b) => b.opinionCount - a.opinionCount); + else out.sort((a, b) => b.publishedTs - a.publishedTs); + return out; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const ch = findChannel(idx, id); + if (ch) return ch.id; + const ep = findEpisode(idx, id); + return ep ? ep.id : String(id || ''); + } + }; +}; diff --git a/src/models/polls_model.js b/src/models/polls_model.js new file mode 100644 index 0000000..c61a5c2 --- /dev/null +++ b/src/models/polls_model.js @@ -0,0 +1,425 @@ +const pull = require('../server/node_modules/pull-stream'); +const { getConfig } = require('../configs/config-manager.js'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, memoIndex } = require('./typed_log'); +const { versionGroups } = require('../backend/dedupe'); + +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const POLL_TYPE = 'poll'; +const VOTE_TYPE = 'pollVote'; +const OPINION_TYPE = 'pollOpinion'; + +const { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH } = require('./polls_model_limits'); + +const clean = (v) => String(v == null ? '' : v).trim(); + +const normalizeOptions = (raw) => { + const list = Array.isArray(raw) ? raw : String(raw || '').split('\n'); + const out = []; + const seen = new Set(); + for (const entry of list) { + const value = clean(entry).slice(0, MAX_OPTION_LENGTH); + if (!value) continue; + const key = value.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + out.push(value); + if (out.length >= MAX_OPTIONS) break; + } + return out; +}; + +const normalizeTags = (raw) => { + const list = Array.isArray(raw) ? raw : String(raw || '').split(','); + return list.map(t => clean(t)).filter(Boolean).slice(0, 20); +}; + +module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = null }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const POLLS_TYPES = [POLL_TYPE, VOTE_TYPE, OPINION_TYPE, 'pollClose', 'post', 'tombstone']; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, POLLS_TYPES, { limit: logLimit }); + + const decryptField = (value, keys) => { + if (!tribeCrypto || typeof value !== 'string' || !value) return null; + for (const k of keys) { + try { + const out = tribeCrypto.decryptWithKey(value, k); + if (typeof out === 'string' && out) return out; + } catch (_) {} + } + return null; + }; + + const scopeKey = async (scope) => { + if (!scope || !chatsModel) return null; + if (!scope.chatId && !scope.tribeId) return null; + try { return await chatsModel.encryptionKeyFor(scope.chatId || null, scope.tribeId || null); } catch (_) { return null; } + }; + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const votesByRoot = new Map(); + const opinionsByRoot = new Map(); + const closedRoots = new Set(); + const commentsByRoot = new Map(); + const others = []; + + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === POLL_TYPE) nodes.set(m.key, { key: m.key, author: v.author, seq: v.sequence || 0, ts: v.timestamp || m.timestamp || 0, c }); + else others.push(m); + } + + const vg = versionGroups(Array.from(nodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === 'string' ? n.c.replaces : null, claimedAuthor: typeof n.c.author === 'string' ? n.c.author : null }))); + const rootOf = vg.rootOf; + const tipOf = vg.tipOf; + + for (const m of others) { + const v = m.value; + const c = v.content; + if (c.type === VOTE_TYPE && typeof c.target === 'string') { + const target = rootOf(c.target); + const entry = votesByRoot.get(target) || new Map(); + const list = entry.get(v.author) || []; + list.push({ + ts: v.timestamp || m.timestamp || 0, + choices: Array.isArray(c.choices) ? c.choices : [], + encryptedChoices: typeof c.encryptedChoices === 'string' ? c.encryptedChoices : null + }); + entry.set(v.author, list); + votesByRoot.set(target, entry); + continue; + } + if (c.type === OPINION_TYPE && typeof c.target === 'string') { + const target = rootOf(c.target); + const entry = opinionsByRoot.get(target) || { counts: {}, voters: [] }; + if (!entry.voters.includes(v.author)) { + entry.voters.push(v.author); + entry.counts[c.category] = (entry.counts[c.category] || 0) + 1; + } + opinionsByRoot.set(target, entry); + continue; + } + if (c.type === 'pollClose' && typeof c.target === 'string') { + closedRoots.add(`${v.author}|${rootOf(c.target)}`); + continue; + } + if (c.type === 'post' && typeof c.root === 'string') { + const target = rootOf(c.root); + commentsByRoot.set(target, (commentsByRoot.get(target) || 0) + 1); + } + } + + return { tomb, nodes, vg, votesByRoot, opinionsByRoot, closedRoots, commentsByRoot, rootOf, tipOf }; + }; + + const isExpired = (c) => { + if (!c.deadline) return false; + const t = Date.parse(c.deadline); + return Number.isFinite(t) && t <= Date.now(); + }; + + const buildPoll = (idx, rootId, viewerId, keys = []) => { + const tipId = idx.tipOf(rootId); + const node = idx.nodes.get(tipId); + if (!node) return null; + const c = node.c || {}; + let question = clean(c.question); + let rawOptions = c.options; + let undecryptable = false; + if (c.encryptedQuestion) { + const q = decryptField(c.encryptedQuestion, keys); + const o = decryptField(c.encryptedOptions, keys); + if (q === null) undecryptable = true; + question = q || ''; + try { rawOptions = o ? JSON.parse(o) : []; } catch (_) { rawOptions = []; } + } + const options = normalizeOptions(rawOptions); + const opensAt = (idx.nodes.get(rootId) || {}).ts || 0; + const closesAt = c.deadline ? Date.parse(c.deadline) : NaN; + const voteMap = new Map(); + for (const [author, list] of (idx.votesByRoot.get(rootId) || new Map()).entries()) { + const inWindow = list.filter(b => b.ts >= opensAt && (!Number.isFinite(closesAt) || b.ts <= closesAt)); + if (inWindow.length) voteMap.set(author, inWindow.reduce((a, b) => (b.ts >= a.ts ? b : a))); + } + + const counts = {}; + for (const o of options) counts[o] = 0; + let totalVoters = 0; + const voters = []; + let myChoices = []; + for (const [author, entry] of voteMap.entries()) { + let choices = Array.isArray(entry.choices) ? entry.choices : []; + if (entry.encryptedChoices) { + const plain = decryptField(entry.encryptedChoices, keys); + try { choices = plain ? JSON.parse(plain) : []; } catch (_) { choices = []; } + } + const picked = (Array.isArray(choices) ? choices : []).filter(o => options.includes(o)); + if (!picked.length) continue; + totalVoters += 1; + voters.push(author); + for (const o of picked) counts[o] += 1; + if (viewerId && author === viewerId) myChoices = picked; + } + + const closed = idx.closedRoots.has(`${node.author}|${rootId}`) || isExpired(c); + const op = idx.opinionsByRoot.get(rootId) || { counts: {}, voters: [] }; + + return { + id: rootId, + rootId, + tipId, + author: node.author, + question, + options, + encrypted: !!c.encryptedQuestion, + undecryptable, + chatId: c.chatId || null, + anonymous: c.anonymous === true, + multiple: c.multiple === true, + deadline: c.deadline || '', + tags: normalizeTags(c.tags), + tribeId: c.tribeId || null, + houseKey: c.houseKey || null, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + status: closed ? 'CLOSED' : 'OPEN', + counts, + totalVoters, + voters: c.anonymous === true ? [] : voters, + votersHidden: c.anonymous === true, + myChoices, + hasVoted: myChoices.length > 0, + commentCount: idx.commentsByRoot.get(rootId) || 0, + opinions: op.counts, + opinions_inhabitants: op.voters + }; + }; + + const collect = async (viewerId) => { + const ssbClient = await openSsb(); + const messages = await getAllMessages(ssbClient); + const idx = memoIndex('polls', messages, buildIndex); + + const keyCache = new Map(); + const keysFor = async (c) => { + if (!c || (!c.chatId && !c.tribeId)) return []; + const cacheKey = `${c.chatId || ''}|${c.tribeId || ''}`; + if (keyCache.has(cacheKey)) return keyCache.get(cacheKey); + const k = await scopeKey({ chatId: c.chatId || null, tribeId: c.tribeId || null }); + const keys = k ? [k] : []; + keyCache.set(cacheKey, keys); + return keys; + }; + + const list = []; + for (const rootId of idx.vg.roots) { + const tipId = idx.tipOf(rootId); + if (idx.tomb.has(tipId) || idx.tomb.has(rootId)) continue; + const node = idx.nodes.get(tipId); + const keys = await keysFor(node && node.c); + const poll = buildPoll(idx, rootId, viewerId || ssbClient.id, keys); + if (!poll) continue; + if (poll.undecryptable) { list.push(poll); continue; } + if (!poll.question || poll.options.length < MIN_OPTIONS) continue; + list.push(poll); + } + return { list, idx, viewerId: viewerId || ssbClient.id }; + }; + + return { + POLL_TYPE, + VOTE_TYPE, + MAX_OPTIONS, + MIN_OPTIONS, + + async listAll(filter = 'ALL', opts = {}) { + const { list, viewerId } = await collect(); + const f = String(filter || 'ALL').toUpperCase(); + const scopeChat = opts.chatId || null; + const scopeTribe = opts.tribeId || null; + const scopeHouse = opts.houseKey || null; + let out = list.filter(p => + scopeChat ? p.chatId === scopeChat + : scopeTribe ? (p.tribeId === scopeTribe && !p.chatId) + : scopeHouse ? p.houseKey === scopeHouse + : (!p.tribeId && !p.chatId && !p.houseKey)); + + if (f === 'MINE') out = out.filter(p => String(p.author) === String(viewerId)); + else if (f === 'OPEN') out = out.filter(p => p.status === 'OPEN'); + else if (f === 'CLOSED') out = out.filter(p => p.status === 'CLOSED'); + else if (f === 'VOTED') out = out.filter(p => p.hasVoted); + else if (f === 'RECENT') out = out.filter(p => (Date.parse(p.createdAt || '') || 0) >= Date.now() - 86400000); + else if (f === 'FAVORITES') { + const fav = new Set((opts.favorites || []).map(String)); + out = out.filter(p => fav.has(String(p.id))); + } + + const q = clean(opts.q).toLowerCase(); + if (q) out = out.filter(p => + p.question.toLowerCase().includes(q) || + p.options.some(o => o.toLowerCase().includes(q)) || + p.tags.some(t => t.toLowerCase().includes(q))); + + if (f === 'TOP') out.sort((a, b) => b.totalVoters - a.totalVoters || new Date(b.createdAt) - new Date(a.createdAt)); + else out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + + return out; + }, + + async getPollById(id, viewerId = null) { + const { list, idx } = await collect(viewerId); + const rootId = idx.rootOf(id); + const poll = list.find(p => p.id === rootId || p.id === id || p.tipId === id); + if (!poll) throw new Error('Poll not found'); + return poll; + }, + + async resolveRootId(id) { + const { idx } = await collect(); + return idx.rootOf(id); + }, + + async createPoll(data = {}) { + if (isPublic) throw new Error('Not available in public mode'); + const question = clean(data.question); + if (!question) throw new Error('Question is required'); + const options = normalizeOptions(data.options); + if (options.length < MIN_OPTIONS) throw new Error(`A poll needs at least ${MIN_OPTIONS} options`); + const deadline = clean(data.deadline); + if (deadline) { + const t = Date.parse(deadline); + if (!Number.isFinite(t)) throw new Error('Invalid deadline'); + if (t <= Date.now()) throw new Error('Deadline must be in the future'); + } + const ssbClient = await openSsb(); + const content = { + type: POLL_TYPE, + anonymous: data.anonymous === true || data.anonymous === 'true' || data.anonymous === '1', + multiple: data.multiple === true || data.multiple === 'true' || data.multiple === '1', + deadline, + tags: normalizeTags(data.tags), + createdAt: new Date().toISOString(), + ...(data.tribeId ? { tribeId: String(data.tribeId) } : {}), + ...(data.chatId ? { chatId: String(data.chatId) } : {}), + ...(data.houseKey ? { houseKey: String(data.houseKey) } : {}) + }; + const key = await scopeKey({ chatId: data.chatId, tribeId: data.tribeId }); + if (key && tribeCrypto) { + content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key); + content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key); + } else { + content.question = question; + content.options = options; + } + return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + }, + + async updatePoll(id, data = {}) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.author !== userId) throw new Error('Not the author'); + if (poll.status === 'CLOSED') throw new Error('Cannot edit a closed poll'); + if (poll.totalVoters > 0) throw new Error('Cannot edit a poll that already has votes'); + + const options = data.options !== undefined ? normalizeOptions(data.options) : poll.options; + if (options.length < MIN_OPTIONS) throw new Error(`A poll needs at least ${MIN_OPTIONS} options`); + const deadline = data.deadline !== undefined ? clean(data.deadline) : poll.deadline; + if (deadline) { + const t = Date.parse(deadline); + if (!Number.isFinite(t)) throw new Error('Invalid deadline'); + } + const question = data.question !== undefined ? clean(data.question) : poll.question; + const content = { + type: POLL_TYPE, + replaces: poll.tipId, + anonymous: data.anonymous !== undefined + ? (data.anonymous === true || data.anonymous === 'true' || data.anonymous === '1') + : poll.anonymous, + multiple: data.multiple !== undefined + ? (data.multiple === true || data.multiple === 'true' || data.multiple === '1') + : poll.multiple, + deadline, + tags: data.tags !== undefined ? normalizeTags(data.tags) : poll.tags, + createdAt: poll.createdAt, + updatedAt: new Date().toISOString(), + ...(poll.tribeId ? { tribeId: poll.tribeId } : {}), + ...(poll.chatId ? { chatId: poll.chatId } : {}), + ...(poll.houseKey ? { houseKey: poll.houseKey } : {}) + }; + const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId }); + if (key && tribeCrypto) { + content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key); + content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key); + } else { + content.question = question; + content.options = options; + } + return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + }, + + async vote(id, choicesRaw) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.status === 'CLOSED') throw new Error('This poll is closed'); + + const wanted = (Array.isArray(choicesRaw) ? choicesRaw : [choicesRaw]) + .map(c => clean(c)) + .filter(c => poll.options.includes(c)); + const unique = [...new Set(wanted)]; + if (!unique.length) throw new Error('Pick at least one option'); + if (!poll.multiple && unique.length > 1) throw new Error('This poll only accepts one option'); + + const content = { + type: VOTE_TYPE, + target: poll.rootId, + createdAt: new Date().toISOString(), + ...(poll.chatId ? { chatId: poll.chatId } : {}) + }; + const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId }); + if (key && tribeCrypto) content.encryptedChoices = tribeCrypto.encryptWithKey(JSON.stringify(unique), key); + else content.choices = unique; + return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + }, + + async closePoll(id) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.author !== userId) throw new Error('Not the author'); + if (poll.status === 'CLOSED') return { status: 'already_closed' }; + const content = { type: 'pollClose', target: poll.rootId, createdAt: new Date().toISOString() }; + await new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + return { status: 'ok' }; + }, + + async deletePoll(id) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.author !== userId) throw new Error('Not the author'); + const content = { type: 'tombstone', target: poll.tipId, deletedAt: new Date().toISOString(), author: userId }; + return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + }, + + async createOpinion(id, category) { + const categories = require('../backend/opinion_categories'); + if (!categories.includes(category)) throw new Error('Invalid opinion category'); + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); + const content = { type: OPINION_TYPE, target: poll.rootId, category, createdAt: new Date().toISOString() }; + return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); + } + }; +}; diff --git a/src/models/polls_model_limits.js b/src/models/polls_model_limits.js new file mode 100644 index 0000000..ba1e774 --- /dev/null +++ b/src/models/polls_model_limits.js @@ -0,0 +1,5 @@ +const MAX_OPTIONS = 10; +const MIN_OPTIONS = 2; +const MAX_OPTION_LENGTH = 120; + +module.exports = { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH }; diff --git a/src/models/projects_model.js b/src/models/projects_model.js index 05ecf90..76d3b3a 100644 --- a/src/models/projects_model.js +++ b/src/models/projects_model.js @@ -3,6 +3,7 @@ const moment = require("../server/node_modules/moment") const { getConfig } = require("../configs/config-manager.js") const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeBy, norm } = require('../backend/dedupe') +const { readTyped } = require("./typed_log") const logLimit = (getConfig().ssbLogStream && getConfig().ssbLogStream.limit) || 1000 module.exports = ({ cooler }) => { @@ -20,13 +21,10 @@ module.exports = ({ cooler }) => { return Math.max(0, Math.min(100, x)) } + const PROJECT_TYPES = ["project", "projectOpinion", "projectFollow", "projectPledge", "projectClaim", "projectPledgeConfirm", "tombstone"] + async function getAllMsgs(ssbClient) { - return new Promise((r, j) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((e, m) => (e ? j(e) : r(m))) - ) - }) + return readTyped(ssbClient, PROJECT_TYPES, { limit: logLimit }) } function extractBlobId(possibleMarkdownImage) { @@ -239,6 +237,7 @@ module.exports = ({ cooler }) => { if (goal < 0) goal = 0 const deadlineISO = data.deadline ? new Date(data.deadline).toISOString() : null + if (deadlineISO && new Date(deadlineISO).getTime() < Date.now()) throw new Error("The deadline cannot be in the past") const content = { type: TYPE, @@ -323,7 +322,11 @@ module.exports = ({ cooler }) => { } let deadline = patch.deadline === undefined ? current.deadline : patch.deadline - if (deadline != null && deadline !== "") deadline = new Date(deadline).toISOString() + if (deadline != null && deadline !== "") { + deadline = new Date(deadline).toISOString() + const changed = !current.deadline || new Date(current.deadline).getTime() !== new Date(deadline).getTime() + if (changed && new Date(deadline).getTime() < Date.now()) throw new Error("The deadline cannot be in the past") + } else if (deadline === "") deadline = null const updated = { @@ -546,7 +549,7 @@ module.exports = ({ cooler }) => { return publishReplace(ssbClient, project.id, patch) }, - async listProjects(filter) { + async listProjects(filter, opts = {}) { const ssbClient = await openSsb() const currentUserId = ssbClient.id const idx = buildProjectIndex(await getAllMsgs(ssbClient)) @@ -580,6 +583,15 @@ module.exports = ({ cooler }) => { else if (F === "RECENT") list = list.filter((p) => p && moment(p.createdAt).isAfter(moment().subtract(24, "hours"))) else if (F === "FOLLOWING") list = list.filter((p) => Array.isArray(p.followers) && p.followers.includes(currentUserId)) + const q = norm(opts.q) + if (q) { + list = list.filter((p) => + norm(p.title).includes(q) || + norm(p.description).includes(q) || + (Array.isArray(p.tags) && p.tags.some(t => norm(t).includes(q))) + ) + } + if (F === "TOP") { list.sort((a, b) => (parseFloat(b.pledged || 0) / (parseFloat(b.goal || 1))) - (parseFloat(a.pledged || 0) / (parseFloat(a.goal || 1)))) } else { diff --git a/src/models/pub_invites.js b/src/models/pub_invites.js new file mode 100644 index 0000000..8cc408a --- /dev/null +++ b/src/models/pub_invites.js @@ -0,0 +1,93 @@ +const ssbKeys = require('../server/node_modules/ssb-keys'); +const { readTyped } = require('./typed_log'); + +const ANNOUNCE_TYPE = 'pub-invites'; +const MAX_CODES = 40; +const FEED_ID = /@[A-Za-z0-9+/]{43}=\.ed25519/; +const SHS_KEY = /~shs:([A-Za-z0-9+/]{43}=)/; + +const LEGACY_ADDRESS = /^([^:~\s]+):(\d+):@/; +const MODERN_ADDRESS = /^(?:net|onion):([^:~\s]+):(\d+)~/; +const parseInvite = (code) => { + const s = String(code || '').trim(); + const legacy = s.match(FEED_ID); + const modern = s.match(SHS_KEY); + const pub = legacy ? legacy[0] : modern ? `@${modern[1]}.ed25519` : null; + const address = s.match(LEGACY_ADDRESS) || s.match(MODERN_ADDRESS); + const host = address ? address[1] : null; + const port = address ? Number(address[2]) : null; + let id = null; + try { + const seed = Buffer.from(s.split('~').pop().replace(/^invite:/, ''), 'base64'); + if (seed.length === 32) id = ssbKeys.generate('ed25519', seed).id; + } catch (_) {} + return { pub, id, host, port }; +}; + +const count = (n) => Math.max(0, Math.floor(Number(n) || 0)); + +const summarize = (rows) => { + const list = (Array.isArray(rows) ? rows : []) + .filter((r) => r && typeof r.id === 'string' && count(r.total) > 0) + .map((r) => ({ id: r.id, total: count(r.total), used: Math.min(count(r.used), count(r.total)) })); + const codes = list.filter((r) => r.total > 1) + .sort((a, b) => (b.total - b.used) - (a.total - a.used)) + .slice(0, MAX_CODES) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); + return { + total: list.reduce((n, r) => n + r.total, 0), + used: list.reduce((n, r) => n + r.used, 0), + codes + }; +}; + +const latestByAuthor = (msgs) => { + const out = new Map(); + for (const m of msgs || []) { + const v = m && m.value; + const c = v && v.content; + if (!c || c.type !== ANNOUNCE_TYPE) continue; + const prev = out.get(v.author); + if (!prev || (Number(v.sequence) || 0) > (Number(prev.value.sequence) || 0)) out.set(v.author, m); + } + return out; +}; + +const listUses = (ssb) => new Promise((resolve) => { + if (!ssb || !ssb.invite || typeof ssb.invite.list !== 'function') return resolve(null); + try { ssb.invite.list((err, rows) => resolve(err ? null : rows)); } catch (_) { resolve(null); } +}); + +const announce = async (ssb) => { + const rows = await listUses(ssb); + if (!rows) return null; + const next = summarize(rows); + const last = latestByAuthor(await readTyped(ssb, [ANNOUNCE_TYPE])).get(ssb.id); + const c = last && last.value.content; + const prev = c ? { total: c.total, used: c.used, codes: c.codes } : null; + if (prev ? JSON.stringify(prev) === JSON.stringify(next) : !next.total) return null; + return new Promise((resolve, reject) => ssb.publish({ type: ANNOUNCE_TYPE, ...next, updatedAt: new Date().toISOString() }, (err, msg) => (err ? reject(err) : resolve(msg)))); +}; + +const statusFor = (entries, msgs) => { + const latest = latestByAuthor(msgs); + return (Array.isArray(entries) ? entries : []).map((e) => { + const { pub, id } = parseInvite(e && e.code); + const m = pub ? latest.get(pub) : null; + if (!m) return { ...e, invites: null, exhausted: false }; + const c = m.value.content; + const total = count(c.total); + const used = Math.min(count(c.used), total); + const own = id && Array.isArray(c.codes) ? c.codes.find((x) => x && x.id === id) : null; + const exhausted = (total > 0 && used >= total) || (!!own && count(own.used) >= count(own.total)); + return { ...e, invites: total > 0 ? { used, total } : null, exhausted }; + }); +}; + +const readStatus = async (ssb, entries) => { + let msgs = []; + try { msgs = await readTyped(ssb, [ANNOUNCE_TYPE]); } catch (_) { msgs = []; } + return statusFor(entries, msgs); +}; + +module.exports = { ANNOUNCE_TYPE, parseInvite, summarize, announce, readStatus }; diff --git a/src/models/recurrence.js b/src/models/recurrence.js new file mode 100644 index 0000000..0c696aa --- /dev/null +++ b/src/models/recurrence.js @@ -0,0 +1,40 @@ +const truthy = (v) => v === true || v === 1 || v === '1' || v === 'on' || v === 'true'; + +const hasAnyInterval = (weekly, monthly, yearly) => truthy(weekly) || truthy(monthly) || truthy(yearly); + +const expandRecurrence = (firstDate, until, weekly, monthly, yearly) => { + const start = new Date(firstDate); + if (isNaN(start.getTime())) return []; + const out = [start]; + if (!until || !hasAnyInterval(weekly, monthly, yearly)) return out; + const end = new Date(until).getTime(); + if (!Number.isFinite(end)) return out; + const seen = new Set([start.getTime()]); + const walk = (mutate) => { + const n = new Date(start); + mutate(n); + let guard = 0; + while (n.getTime() <= end && guard++ < 5000) { + const t = n.getTime(); + if (!seen.has(t)) { seen.add(t); out.push(new Date(n)); } + mutate(n); + } + }; + if (truthy(weekly)) walk((d) => d.setUTCDate(d.getUTCDate() + 7)); + if (truthy(monthly)) walk((d) => d.setUTCMonth(d.getUTCMonth() + 1)); + if (truthy(yearly)) walk((d) => d.setUTCFullYear(d.getUTCFullYear() + 1)); + return out.sort((a, b) => a.getTime() - b.getTime()); +}; + +const nextOccurrence = (firstDate, until, weekly, monthly, yearly, from = Date.now()) => { + const list = expandRecurrence(firstDate, until, weekly, monthly, yearly); + for (const d of list) if (d.getTime() >= from) return d; + return list.length ? list[list.length - 1] : null; +}; + +const upcomingOccurrences = (firstDate, until, weekly, monthly, yearly, limit = 12, from = Date.now()) => + expandRecurrence(firstDate, until, weekly, monthly, yearly) + .filter(d => d.getTime() >= from) + .slice(0, limit); + +module.exports = { truthy, hasAnyInterval, expandRecurrence, nextOccurrence, upcomingOccurrences }; diff --git a/src/models/reports_model.js b/src/models/reports_model.js index 8bb1b29..39631d9 100644 --- a/src/models/reports_model.js +++ b/src/models/reports_model.js @@ -1,7 +1,9 @@ const pull = require('../server/node_modules/pull-stream'); +const { normalizeImages, normalizeVideo } = require('./media_gallery'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const normU = (v) => String(v || '').trim().toUpperCase(); @@ -39,7 +41,7 @@ const normalizeTemplate = (category, tpl) => { } if (cat === 'CONTENT') { - return pick(['contentLocation', 'whyInappropriate', 'requestedAction', 'evidenceLinks']); + return pick(['contentLocation', 'whyInappropriate', 'evidenceLinks']); } return {}; @@ -49,20 +51,13 @@ module.exports = ({ cooler }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ); - }); + const REPORT_TYPES = ['report', 'reportConfirm', 'reportOpinion', 'tombstone']; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, REPORT_TYPES, { limit: logLimit }); const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const confirms = []; const opinionMsgs = []; @@ -78,46 +73,24 @@ module.exports = ({ cooler }) => { if (c.type !== 'report') continue; const ts = v.timestamp || m.timestamp || 0; - nodes.set(k, { key: k, ts, c, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, author: v.author }); } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === 'string' ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const confirmsByRoot = new Map(), opinionsByRoot = new Map(); for (const cf of confirms) { if (!nodes.has(cf.target)) continue; const r = rootOf(cf.target); if (!confirmsByRoot.has(r)) confirmsByRoot.set(r, []); confirmsByRoot.get(r).push(cf); } for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); - if (!contentNode) return null; + if (!contentNode || !tipByRoot.has(root)) return null; const rootAuthor = nodes.get(root) ? nodes.get(root).author : null; const lc = contentNode.c || {}; @@ -125,8 +98,7 @@ module.exports = ({ cooler }) => { const voterSet = new Set(ensureArray(lc.opinions_inhabitants)); const confirmSet = new Set(ensureArray(lc.confirmations)); - const groupKeys = []; { let x = root, g = 0; groupKeys.push(x); while (child.has(x) && g++ < 100000) { x = child.get(x); groupKeys.push(x); } } - for (const k of groupKeys) { + for (const k of (vg.groups.get(root) || [root])) { const n = nodes.get(k); if (!n) continue; if (n.author !== rootAuthor) continue; const c = n.c || {}; for (const cb of ensureArray(c.confirmations)) confirmSet.add(cb); for (const vv of ensureArray(c.opinions_inhabitants)) voterSet.add(vv); @@ -150,7 +122,7 @@ module.exports = ({ cooler }) => { }; }; - return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildReport = (node, agg) => { @@ -164,6 +136,8 @@ module.exports = ({ cooler }) => { ...c, category: cat, status: normalizeStatus(c.status || 'OPEN'), + images: normalizeImages(c.images && c.images.length ? c.images : c.image), + video: normalizeVideo(c.video), severity: normalizeSeverity(c.severity) || 'low', confirmations, tags: ensureArray(c.tags), @@ -174,14 +148,13 @@ module.exports = ({ cooler }) => { }; return { - async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}) { + async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}, media = {}) { const ssb = await openSsb(); const userId = ssb.id; - let blobId = null; - if (image) { - blobId = String(image).trim() || null; - } + const images = normalizeImages((media && media.images && media.images.length) ? media.images : image); + const blobId = images[0] || null; + const clip = normalizeVideo(media && media.video); const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) @@ -196,6 +169,8 @@ module.exports = ({ cooler }) => { createdAt: new Date().toISOString(), author: userId, image: blobId, + images, + video: clip, tags, confirmations: [], severity: normalizeSeverity(severity) || 'low', @@ -227,10 +202,15 @@ module.exports = ({ cooler }) => { ? String(updatedContent.tags || '').split(',').map(t => t.trim()).filter(Boolean) : ensureArray(report.content.tags); - let blobId = report.content.image || null; - if (updatedContent.image) { - blobId = String(updatedContent.image).trim() || null; - } + const nextImages = normalizeImages( + Object.prototype.hasOwnProperty.call(updatedContent, 'images') + ? updatedContent.images + : (report.content.images && report.content.images.length ? report.content.images : (updatedContent.image || report.content.image)) + ); + const blobId = nextImages[0] || null; + const nextVideo = Object.prototype.hasOwnProperty.call(updatedContent, 'video') + ? normalizeVideo(updatedContent.video) + : normalizeVideo(report.content.video); const nextStatus = Object.prototype.hasOwnProperty.call(updatedContent, 'status') ? normalizeStatus(updatedContent.status) @@ -258,6 +238,8 @@ module.exports = ({ cooler }) => { type: 'report', replaces: tipId, image: blobId, + images: nextImages, + video: nextVideo, tags, confirmations, opinions: agg.opinions, @@ -293,7 +275,7 @@ module.exports = ({ cooler }) => { const ssb = await openSsb(); const messages = await getAllMessages(ssb); - const idx = buildIndex(messages); + const idx = memoIndex('reports', messages, buildIndex); const root = idx.rootOf(id); const agg = idx.resolveGroup(root); if (!agg || agg.tombstoned) throw new Error('Report not found'); diff --git a/src/models/rooms_model.js b/src/models/rooms_model.js new file mode 100644 index 0000000..2de9adb --- /dev/null +++ b/src/models/rooms_model.js @@ -0,0 +1,860 @@ +const crypto = require("crypto") +const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { collabContent, openInviteOf } = require('../backend/collab_content') +const { readTyped, memoIndex } = require('./typed_log') +const { roomNumberOf, normalizeNumber } = require('./phone_number') +const { getConfig } = require("../configs/config-manager.js") +const roomCollab = collabContent({ membersField: 'members', undecField: 'undecryptable', contentFields: ['title', 'description', 'image', 'status'], listFields: ['tags', 'invites'] }) +const logLimit = getConfig().ssbLogStream?.limit || 1000 + +const ROOM_MAX = 50 +const ROOM_TYPES = ["room", "roomMember", "tribe-keys", "tombstone"] +const STATUSES = ["OPEN", "INVITE-ONLY"] +const INVITE_SALT = "SolarNET.HuB-rooms" +const INVITE_BYTES = 16 +const RECENT_MS = 86400000 +const RID = /^[0-9a-f]{32}$/ +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/ +const BLOB_ID = /^&[A-Za-z0-9+/]{43}=\.sha256$/ + +const safeText = (v) => String(v || "").trim() +const normalizeTags = (raw) => { + if (!raw) return [] + if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean) + return String(raw).split(",").map(t => t.trim()).filter(Boolean) +} +const normalizeStatus = (v) => (STATUSES.includes(String(v || "").toUpperCase()) ? String(v).toUpperCase() : "OPEN") +const SEALED_REF = /^\/blob\/sealed\/%26[A-Za-z0-9%]+\.sha256\?k=[0-9a-f]{64}&t=[^\s"<>]*$/ +const blobRef = (v) => (BLOB_ID.test(safeText(v)) || SEALED_REF.test(safeText(v)) ? safeText(v) : "") +const asP = (fn, ...args) => new Promise((resolve, reject) => fn(...args, (err, v) => err ? reject(err) : resolve(v))) +const rpcValue = (fn, ...args) => new Promise((resolve) => { + try { + const r = fn(...args, (err, v) => resolve(err ? null : v)) + if (r && typeof r.then === "function") r.then(resolve, () => resolve(null)) + else if (r !== undefined) resolve(r) + } catch (_) { resolve(null) } +}) + +module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { + let ssb + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + const phone = async () => { const s = await openSsb(); return s && s.phone ? s.phone : null } + + const ownCrypto = roomCrypto || tribeCrypto + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null + const lookupKeys = (rid) => { + const a = (ownCrypto && ownCrypto.getKeys(rid)) || [] + if (a.length) return a + return (tribeCrypto && tribeCrypto.getKeys(rid)) || [] + } + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0) + const setRoomKey = (rootId, keyHex) => { if (ownCrypto) ownCrypto.setKey(rootId, keyHex, 1) } + const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null) + const inviteMatches = (inv, token) => { + if (!token) return false + if (typeof inv === "string") return inv === token + if (!inv || typeof inv !== "object") return false + if (typeof inv.code === "string" && inv.code === token) return true + if (typeof inv.ch === "string" && (inv.ch === token || hashCode(token, inv.salt) === inv.ch)) return true + return false + } + const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "") + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } + const deriveRid = (keyHex, roomId) => crypto.createHmac("sha256", Buffer.from(String(keyHex), "hex")).update("oasis-room-rid|" + roomId).digest("hex").slice(0, 32) + const currentKeyFor = (rootId, tribeId, tribeKeys) => (tribeId && Array.isArray(tribeKeys) && tribeKeys.length ? tribeKeys[0] : lookupKeys(rootId)[0]) || null + + const encryptField = (text, keyHex) => { + const key = Buffer.from(keyHex, "hex") + const iv = crypto.randomBytes(12) + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv) + const enc = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]) + const authTag = cipher.getAuthTag() + return iv.toString("hex") + authTag.toString("hex") + enc.toString("hex") + } + + const tryDecryptField = (encrypted, keyHex) => { + const key = Buffer.from(keyHex, "hex") + const iv = Buffer.from(encrypted.slice(0, 24), "hex") + const authTag = Buffer.from(encrypted.slice(24, 56), "hex") + const ciphertext = Buffer.from(encrypted.slice(56), "hex") + const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv) + decipher.setAuthTag(authTag) + return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8") + } + + const decryptField = (encrypted, keyHex) => { + try { return tryDecryptField(encrypted, keyHex) } catch (_) { return "" } + } + + const getTribeKeysFor = async (tribeId) => { + if (!tribeCrypto || !tribesModel || !tribeId) return [] + try { + const rootId = await tribesModel.getRootId(tribeId) + return tribeCrypto.getKeys(rootId) || [] + } catch (_) { return [] } + } + + const fieldsWith = (c, keyHex, strict) => { + const dec = strict ? tryDecryptField : decryptField + const title = c.title ? dec(c.title, keyHex) : "" + const opt = (v) => { try { return v ? dec(v, keyHex) : "" } catch (_) { return "" } } + return { + title: safeText(title), + description: safeText(opt(c.description)), + image: blobRef(opt(c.image)), + tags: normalizeTags(opt(c.tags)), + rid: RID.test(opt(c.rid)) ? opt(c.rid) : "" + } + } + + const decryptWithKeys = (c, keys) => { + if (!c.title || !keys.length) return null + for (const k of keys) { + try { return fieldsWith(c, k, true) } catch (_) {} + } + return null + } + + const encryptForInvite = (roomKeyHex, code, saltHex) => { + const salt = saltHex ? Buffer.from(saltHex, "hex") : Buffer.from(INVITE_SALT) + const derived = crypto.scryptSync(code, salt, 32) + return encryptField(roomKeyHex, derived.toString("hex")) + } + + const decryptFromInvite = (encryptedKey, code, saltHex) => { + const salt = saltHex ? Buffer.from(saltHex, "hex") : Buffer.from(INVITE_SALT) + const derived = crypto.scryptSync(code, salt, 32) + return decryptField(encryptedKey, derived.toString("hex")) + } + + const tryDecryptPublicInviteKey = (invites) => { + if (!Array.isArray(invites)) return null + for (const inv of invites) { + if (!inv || typeof inv !== "object" || inv.public !== true) continue + if (typeof inv.code !== "string" || typeof inv.ek !== "string") continue + try { + const key = decryptFromInvite(inv.ek, inv.code, inv.salt) + if (key) return key + } catch (_) {} + } + return null + } + + const generateInviteSalt = () => crypto.randomBytes(16).toString("hex") + + const rotateRoomKey = async (rootId, remainingMembers) => { + if (!ownCrypto || !tribeCrypto || !rootId) return + if (!lookupKey(rootId)) return + const newKey = crypto.randomBytes(32).toString("hex") + const newGen = ownCrypto.addNewKey(rootId, newKey) + const ssbClient = await openSsb() + const ssbKeys = require("../server/node_modules/ssb-keys") + const memberKeys = {} + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} + } + if (Object.keys(memberKeys).length) { + await new Promise((resolve) => { + ssbClient.publish({ type: "tribe-keys", tribeId: rootId, generation: newGen, memberKeys }, () => resolve()) + }) + } + } + + const readAll = async (ssbClient) => readTyped(ssbClient, ROOM_TYPES, { limit: logLimit }) + + const ensureMemberKeys = async (ssbClient, messages, rooms) => { + if (!tribeCrypto || !ownCrypto) return + const leavesByRoot = new Map() + for (const m of messages) { + const c = m.value && m.value.content + if (!c || c.type !== "roomMember" || c.on !== false || !c.target || !c.member) continue + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []) + leavesByRoot.get(c.target).push({ member: c.member, author: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }) + } + const ssbKeys = require("../server/node_modules/ssb-keys") + for (const room of (Array.isArray(rooms) ? rooms : [])) { + if (!room || room.tribeId || room.author !== ssbClient.id) continue + const rootId = room.rootId + if (!rootId || !lookupKey(rootId)) continue + const members = Array.isArray(room.members) ? room.members : [] + const leaves = (leavesByRoot.get(rootId) || []).filter(l => l.author === l.member || l.author === room.author) + const plan = ownCrypto.keyPlan({ rootId, ownerId: room.author, gen: lookupGen(rootId), members, messages, leaves }) + if (plan.rotate) { + await rotateRoomKey(rootId, members) + continue + } + if (!plan.missing.length) continue + const key = lookupKey(rootId) + const memberKeys = {} + for (const m of plan.missing) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + } + if (!Object.keys(memberKeys).length) continue + await asP(ssbClient.publish, { type: "tribe-keys", tribeId: rootId, generation: lookupGen(rootId) || 1, memberKeys }) + } + } + + + const ingestOwnTribeKeys = async () => { + if (!ownCrypto) return + try { + const ssbClient = await openSsb() + const ssbKeys = require("../server/node_modules/ssb-keys") + const config = require("../server/ssb_config") + const rooms = new Set() + const msgs = await readAll(ssbClient) + for (const m of msgs) { + const c = m.value && m.value.content + if (c && c.type === "room") rooms.add(m.key) + } + const idx = buildIndex(msgs) + const trusted = (root, author) => { const owner = idx.nodes.get(root) && idx.nodes.get(root).author; return !!author && !!owner && (author === owner || author === ssbClient.id) } + for (const m of msgs) { + const c = m.value && m.value.content + if (!c || c.type !== "tribe-keys" || !rooms.has(c.tribeId)) continue + if (!trusted(c.tribeId, m.value.author)) continue + const boxed = c.memberKeys && typeof c.memberKeys === "object" ? c.memberKeys[ssbClient.id] : null + if (!boxed) continue + try { + const unboxed = ssbKeys.unbox(boxed, config.keys) + const key = typeof unboxed === "string" ? unboxed : (unboxed && unboxed.toString ? unboxed.toString() : null) + if (key) ownCrypto.addNewKey(c.tribeId, key) + } catch (_) {} + } + } catch (_) {} + } + + const buildIndex = (messages) => { + const tomb = new Set() + const nodes = new Map() + const parent = new Map() + const child = new Map() + const authorByKey = new Map() + const tombRequests = [] + const memberMsgs = [] + + for (const m of messages) { + const k = m.key + const v = m.value || {} + const c = v.content + if (!c) continue + if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue } + if (c.type === "roomMember" && c.target) { memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "", keyProof: typeof c.keyProof === "string" ? c.keyProof : "" }); continue } + if (c.type === "room") { + nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) + authorByKey.set(k, v.author) + if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) } + } + } + + for (const t of tombRequests) { + const targetAuthor = authorByKey.get(t.target) + if (targetAuthor && t.author === targetAuthor) tomb.add(t.target) + } + + const rootOf = (id) => { let cur = id, g = 0; while (parent.has(cur) && g++ < 100000) cur = parent.get(cur); return cur } + const rootMemo = new Map() + const groupRootOf = (id) => { + if (rootMemo.has(id)) return rootMemo.get(id) + let cur = id, g = 0 + while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur) + rootMemo.set(id, cur) + return cur + } + const ownerTip = new Map() + for (const [k, n] of nodes) { + const r = groupRootOf(k) + const rn = nodes.get(r) + if (!rn || n.author !== rn.author) continue + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k) + } + const contentTipOf = (root) => ownerTip.get(root) || root + + const roots = new Set() + for (const id of nodes.keys()) roots.add(groupRootOf(id)) + + const invitesOf = (root) => { + const out = [] + for (const id of [root, contentTipOf(root)]) { const n = nodes.get(id); if (n && Array.isArray(n.c.invites)) out.push(...n.c.invites) } + return out + } + const selfJoinAllowed = (root, mm) => { + const oc = (nodes.get(root) || {}).c || {} + if (oc.encrypted !== true || oc.tribeId) return true + const invites = invitesOf(root) + if (invites.some(inv => inv && typeof inv === "object" && inv.public === true)) return true + if (mm.keyProof && lookupKeys(root).some(k => keyProofFor(k, mm.member) === mm.keyProof)) return true + return !!mm.code && invites.some(inv => inviteMatches(inv, mm.code)) + } + + const memberByRoot = new Map() + const consumedByRoot = new Map() + for (const mm of memberMsgs) { + if (!nodes.has(mm.target)) continue + const r = groupRootOf(mm.target) + const ownerAuthor = nodes.get(r) && nodes.get(r).author + if (!ownerAuthor || !mm.member) continue + const self = mm.member === mm.author + if (!self && mm.author !== ownerAuthor) continue + if (self && mm.author !== ownerAuthor && mm.on && !selfJoinAllowed(r, mm)) continue + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const m2 = memberByRoot.get(r) + const p = m2.get(mm.member) + if (!p || mm.ts >= p.ts) m2.set(mm.member, { on: mm.on, ts: mm.ts }) + if (mm.on && mm.code) { + if (!consumedByRoot.has(r)) consumedByRoot.set(r, new Set()) + consumedByRoot.get(r).add(mm.code) + } + } + + const isCodeConsumed = (root, code) => { + if (!code) return false + const set = consumedByRoot.get(root) + return !!(set && set.has(code)) + } + + for (const [k, n] of nodes) { + const r = groupRootOf(k) + const rn = nodes.get(r) + if (!rn || n.author === rn.author || !Array.isArray(n.c.members) || !selfJoinAllowed(r, { member: n.author })) continue + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const m2 = memberByRoot.get(r) + const p = m2.get(n.author) + if (!p || n.ts >= p.ts) m2.set(n.author, { on: n.c.members.includes(n.author), ts: n.ts }) + } + + const resolveMembers = (root) => { + const ownerNode = nodes.get(root) + const oc = ownerNode ? ownerNode.c : {} + const set = new Set(Array.isArray(oc.members) ? oc.members.filter(x => typeof x === "string" && x) : []) + for (const [mem, st] of (memberByRoot.get(root) || new Map())) { if (st.on) set.add(mem); else set.delete(mem) } + return [...set] + } + + const tipByRoot = new Map() + for (const r of roots) tipByRoot.set(r, contentTipOf(r)) + + return { tomb, nodes, parent, child, rootOf, groupRootOf, contentTipOf, tipByRoot, resolveMembers, isCodeConsumed } + } + + const decryptRoomFields = (c, rootId, tribeKeys) => { + if (c.encrypted !== true) { + return { title: safeText(c.title), description: safeText(c.description), image: blobRef(c.image), tags: normalizeTags(c.tags), rid: RID.test(String(c.rid || "")) ? String(c.rid) : "", _undec: false } + } + if (c.tribeId && Array.isArray(tribeKeys) && tribeKeys.length) { + const viaTribe = decryptWithKeys(c, tribeKeys) + if (viaTribe) return { ...viaTribe, _undec: false } + } + for (const k of [...lookupKeys(rootId), tryDecryptPublicInviteKey(c.invites)].filter(Boolean)) { + const out = decryptWithKeys(c, [k]) + if (out) return { ...out, _undec: false } + } + return { title: "", description: "", image: "", tags: [], rid: "", _undec: true } + } + + const roomTypeOf = (c) => c.status === "INVITE-ONLY" || (c.status === "CLOSED" && c.encrypted === true && !c.tribeId) ? "INVITE-ONLY" : "OPEN" + + const buildRoom = (node, rootId, tribeKeys, members) => { + const c = node.c || {} + if (c.type !== "room") return null + const f = decryptRoomFields(c, rootId, tribeKeys) + const author = c.author || node.author + const roomId = RID.test(String(c.roomId || "")) ? String(c.roomId) : "" + const liveKey = roomId && c.encrypted === true && !f._undec ? currentKeyFor(rootId, c.tribeId, tribeKeys) : null + return { + key: node.key, + rootId, + roomId, + title: f.title, + description: f.description, + image: f.image, + tags: f.tags, + status: ["OPEN", "INVITE-ONLY", "CLOSED"].includes(c.status) ? c.status : "OPEN", + type: roomTypeOf(c), + author, + members: Array.isArray(members) ? members : (Array.isArray(c.members) ? c.members : []), + invites: Array.isArray(c.invites) ? c.invites : [], + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + tribeId: c.tribeId || null, + encrypted: c.encrypted === true, + undecryptable: !!f._undec, + rid: roomId ? (liveKey ? deriveRid(liveKey, roomId) : "") : f.rid, + token: typeof c.token === "string" ? c.token : "", + hub: FEED_ID.test(String(c.hub || "")) ? c.hub : author, + hubAddress: typeof c.hubAddress === "string" ? c.hubAddress : "", + media: c.media === "video" ? "video" : "audio", + maxParticipants: ROOM_MAX, + number: roomNumberOf(rootId), + line: c.line === "SWITCHBOARD" ? "SWITCHBOARD" : "DND" + } + } + + const collectRooms = async (idx) => { + const tribeKeyCache = new Map() + const items = [] + for (const [rootId, tipId] of idx.tipByRoot.entries()) { + if (idx.tomb.has(tipId)) continue + const node = idx.nodes.get(tipId) + if (!node || node.c.type !== "room") continue + let tKeys = [] + if (node.c.tribeId) { + if (!tribeKeyCache.has(node.c.tribeId)) tribeKeyCache.set(node.c.tribeId, await getTribeKeysFor(node.c.tribeId)) + tKeys = tribeKeyCache.get(node.c.tribeId) + } + const room = buildRoom(node, rootId, tKeys, idx.resolveMembers(rootId)) + if (!room) continue + room.isClosed = room.status === "CLOSED" + items.push(room) + } + return items + } + + const pickHub = async () => { + const ph = await phone() + const me = (await openSsb()).id + const h = ph && typeof ph.roomHubFor === "function" ? await rpcValue(ph.roomHubFor) : null + if (h && FEED_ID.test(String(h.key || ""))) return { hub: h.key, hubAddress: String(h.address || "") } + return { hub: me, hubAddress: "" } + } + + const tokenFor = async (rid) => { + const ph = await phone() + const t = ph && typeof ph.roomToken === "function" ? await rpcValue(ph.roomToken, rid) : null + return typeof t === "string" ? t : "" + } + + const republish = async (id, mutate) => { + const self = api + const tipId = await self.resolveCurrentId(id) + const ssbClient = await openSsb() + const userId = ssbClient.id + const rootId = await self.resolveRootId(id) + const item = await asP(ssbClient.get, tipId).catch(() => null) + if (!item || !item.content) throw new Error("Room not found") + if (item.content.author !== userId) throw new Error("Not the author") + const c = item.content + let keyHex = null + let usesTribeKey = false + if (c.encrypted === true) { + if (c.tribeId) { + const tKeys = await getTribeKeysFor(c.tribeId) + if (tKeys.length) { keyHex = tKeys[0]; usesTribeKey = true } + } + if (!keyHex) keyHex = lookupKey(rootId) + if (!keyHex) throw new Error("Missing room key — cannot update room") + } + const enc = (text) => (c.encrypted === true ? encryptField(String(text), keyHex) : String(text)) + const updated = { ...mutate(c, enc), updatedAt: new Date().toISOString(), replaces: tipId } + await asP(ssbClient.publish, { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }) + const res = await asP(ssbClient.publish, updated) + if (keyHex && !usesTribeKey) setRoomKey(res.key, keyHex) + return res + } + + const api = { + type: "room", + ROOM_MAX, + + async decryptContent(content, rootId) { + const tKeys = content && content.tribeId ? await getTribeKeysFor(content.tribeId) : [] + return decryptRoomFields(content, rootId, tKeys) + }, + + decryptContentPublicSync(content) { + if (!content) return null + if (content.encrypted !== true) return { title: safeText(content.title), description: safeText(content.description), tags: normalizeTags(content.tags) } + if (content.tribeId) return null + const keyHex = tryDecryptPublicInviteKey(content.invites) + if (!keyHex) return null + const f = decryptWithKeys(content, [keyHex]) + return f ? { title: f.title, description: f.description, tags: f.tags } : null + }, + + async resolveRootId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient)) + const root = idx.groupRootOf(id) + if (idx.tomb.has(idx.contentTipOf(root))) throw new Error("Not found") + return root + }, + + async resolveCurrentId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient)) + const tip = idx.contentTipOf(idx.groupRootOf(id)) + if (idx.tomb.has(tip)) throw new Error("Not found") + return tip + }, + + async createRoom({ title, description, image, status, tags, tribeId } = {}) { + const ssbClient = await openSsb() + const userId = ssbClient.id + const now = new Date().toISOString() + const type = tribeId ? "INVITE-ONLY" : normalizeStatus(status) + const tagsArr = normalizeTags(tags) + const rid = crypto.randomBytes(16).toString("hex") + const roomId = crypto.randomBytes(16).toString("hex") + const sealed = !!tribeId || type !== "OPEN" + const live = { token: await tokenFor(sealed ? roomId : rid), ...(await pickHub()), media: "audio" } + const base = { type: "room", status: tribeId ? "OPEN" : type, author: userId, members: [userId], invites: [], createdAt: now, updatedAt: now, ...live } + + if (type === "OPEN" && !tribeId) { + return asP(ssbClient.publish, { ...base, title: safeText(title), description: safeText(description), image: blobRef(image), tags: tagsArr, rid, encrypted: false }) + } + + let keyHex = null + let usesTribeKey = false + if (tribeId) { + const tKeys = await getTribeKeysFor(tribeId) + if (tKeys.length) { keyHex = tKeys[0]; usesTribeKey = true } + } + if (!keyHex) keyHex = crypto.randomBytes(32).toString("hex") + const enc = (text) => encryptField(String(text), keyHex) + const msg = await asP(ssbClient.publish, { + ...base, + title: enc(safeText(title)), + description: enc(safeText(description)), + image: enc(blobRef(image)), + tags: enc(tagsArr.join(",")), + roomId, + encrypted: true, + ...(tribeId ? { tribeId } : {}) + }) + if (!usesTribeKey) { + setRoomKey(msg.key, keyHex) + if (tribeCrypto) { + try { + const ssbKeys = require("../server/node_modules/ssb-keys") + await asP(ssbClient.publish, { type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: tribeCrypto.boxKeyForMember(keyHex, userId, ssbKeys) } }) + } catch (_) {} + } + } + return msg + }, + + async updateRoomById(id, data = {}) { + const hub = data.refreshHub ? await pickHub() : null + return republish(id, (c, enc) => ({ + ...c, + title: data.title !== undefined ? enc(safeText(data.title)) : c.title, + description: data.description !== undefined ? enc(safeText(data.description)) : c.description, + image: data.image !== undefined ? enc(blobRef(data.image)) : c.image, + tags: data.tags !== undefined ? (c.encrypted === true ? enc(normalizeTags(data.tags).join(",")) : normalizeTags(data.tags)) : c.tags, + invites: data.invites !== undefined ? data.invites : c.invites, + ...(hub || {}) + })) + }, + + async rehomeHub(id) { + const ssbClient = await openSsb() + const picked = await pickHub() + if (!picked || picked.hub === ssbClient.id) return null + return republish(id, (c) => ({ ...c, ...picked })) + }, + + async closeRoomById(id) { + return republish(id, (c) => ({ ...c, status: "CLOSED" })) + }, + + async reopenRoomById(id) { + return republish(id, (c) => ({ ...c, status: roomTypeOf(c) })) + }, + + async setRoomLine(id, line) { + return republish(id, (c) => ({ ...c, line: line === "SWITCHBOARD" ? "SWITCHBOARD" : "DND" })) + }, + + async deleteRoomById(id) { + const tipId = await this.resolveCurrentId(id) + const ssbClient = await openSsb() + const item = await asP(ssbClient.get, tipId).catch(() => null) + if (!item || !item.content) throw new Error("Room not found") + if (item.content.author !== ssbClient.id) throw new Error("Not the author") + await asP(ssbClient.publish, { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id }) + }, + + async addMemberToRoom(roomId, feedId, consumedCode) { + const tipId = await this.resolveCurrentId(roomId) + const ssbClient = await openSsb() + const rootId = await this.resolveRootId(roomId) + const item = await asP(ssbClient.get, tipId).catch(() => null) + if (!item || !item.content) throw new Error("Room not found") + const c = item.content + if (c.encrypted === true && !c.tribeId && !lookupKey(rootId)) { + const key = tryDecryptPublicInviteKey(c.invites) + if (key) setRoomKey(rootId, key) + } + const proofKey = feedId === ssbClient.id && c.encrypted === true && !c.tribeId ? lookupKey(rootId) : null + return asP(ssbClient.publish, { type: "roomMember", target: rootId, member: feedId, on: true, createdAt: new Date().toISOString(), ...(typeof consumedCode === "string" && consumedCode ? { code: consumedCode } : {}), ...(proofKey ? { keyProof: keyProofFor(proofKey, feedId) } : {}) }) + }, + + async leaveRoomMembership(roomId) { + const ssbClient = await openSsb() + const userId = ssbClient.id + const room = await this.getRoomById(roomId) + if (!room) throw new Error("Room not found") + if (room.author === userId) throw new Error("Author cannot leave their own room") + if (!room.members.includes(userId)) return + await asP(ssbClient.publish, { type: "roomMember", target: room.rootId, member: userId, on: false, createdAt: new Date().toISOString() }) + }, + + async ingestKeys() { await ingestOwnTribeKeys() }, + + async pruneOrphanKeys() { + if (!ownCrypto || typeof ownCrypto.getAllRootIds !== "function") return 0 + try { + const ssbClient = await openSsb() + const tomb = buildValidatedTombstoneSet(await readAll(ssbClient)) + let removed = 0 + for (const rid of ownCrypto.getAllRootIds()) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } + } + return removed + } catch (_) { return 0 } + }, + + async getRoomById(id) { + const ssbClient = await openSsb() + const idx = memoIndex('rooms', await readAll(ssbClient), buildIndex) + const root = idx.groupRootOf(id) + const tip = idx.contentTipOf(root) + if (idx.tomb.has(tip)) return null + const node = idx.nodes.get(tip) + if (!node || node.c.type !== "room") return null + const tKeys = node.c.tribeId ? await getTribeKeysFor(node.c.tribeId) : [] + const room = buildRoom(node, root, tKeys, idx.resolveMembers(root)) + if (!room) return null + room.isClosed = room.status === "CLOSED" + return roomCollab.fold(room, await collectRooms(idx)) + }, + + async listAll({ filter = "all", viewerId } = {}) { + const ssbClient = await openSsb() + const uid = viewerId || ssbClient.id + const messages = await readAll(ssbClient) + const idx = buildIndex(messages) + let list = roomCollab.visibleThenCollapsed(await collectRooms(idx), uid) + try { await ensureMemberKeys(ssbClient, messages, list) } catch (_) {} + if (filter === "mine") list = list.filter(r => r.author === uid) + else if (filter === "recent") list = list.filter(r => new Date(r.createdAt).getTime() >= Date.now() - RECENT_MS) + else if (filter === "open") list = list.filter(r => r.type === "OPEN" && !r.isClosed) + else if (filter === "invite") list = list.filter(r => r.type === "INVITE-ONLY" && !r.isClosed) + else if (filter === "closed") list = list.filter(r => r.isClosed) + return list.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + }, + + async generateInvite(roomId, opts = {}) { + const ssbClient = await openSsb() + const room = await this.getRoomById(roomId) + if (!room) throw new Error("Room not found") + if (room.author !== ssbClient.id) throw new Error("Only the author can generate invites") + const rootId = await this.resolveRootId(roomId) + const keyHex = lookupKey(rootId) + const code = crypto.randomBytes(INVITE_BYTES).toString("hex") + let invite = code + const pubFlag = opts.public ? { public: true } : {} + const inviteSalt = generateInviteSalt() + if (keyHex) { + invite = opts.public + ? { code, ek: encryptForInvite(keyHex, code, inviteSalt), salt: inviteSalt, ...pubFlag } + : { ch: hashCode(code, inviteSalt), ek: encryptForInvite(keyHex, code, inviteSalt), salt: inviteSalt } + } else { + invite = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt), salt: inviteSalt } + } + await this.updateRoomById(roomId, { invites: [...room.invites, invite] }) + return code + }, + + async getOpenInvite(roomId) { + return openInviteOf(await this.getRoomById(roomId).catch(() => null)) + }, + + async generateOpenInvite(roomId) { + const room = await this.getRoomById(roomId) + if (!room) throw new Error("Room not found") + if ((Array.isArray(room.invites) ? room.invites : []).find(inv => typeof inv === "object" && inv.public === true)) throw new Error("An open invitation already exists") + return this.generateInvite(roomId, { public: true }) + }, + + async removeOpenInvite(roomId) { + const ssbClient = await openSsb() + const room = await this.getRoomById(roomId) + if (!room) throw new Error("Room not found") + if (room.author !== ssbClient.id) throw new Error("Only the author can remove invites") + await this.updateRoomById(roomId, { invites: (Array.isArray(room.invites) ? room.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)) }) + await rotateRoomKey(await this.resolveRootId(roomId), [...new Set([room.author, ...(Array.isArray(room.members) ? room.members : [])])]) + }, + + async joinByInvite(code) { + const ssbClient = await openSsb() + const userId = ssbClient.id + const rooms = await collectRooms(buildIndex(await readAll(ssbClient))) + let matchedRoom = null + let matchedInvite = null + for (const r of rooms) { + for (const inv of r.invites) { + if (inviteMatches(inv, code)) { matchedRoom = r; matchedInvite = inv; break } + } + if (matchedRoom) break + } + if (!matchedRoom) throw new Error("Invalid or expired invite code") + if (matchedRoom.members.includes(userId)) throw new Error("Already a member") + const isPublic = typeof matchedInvite === "object" && matchedInvite.public === true + const resolvedRootId = await this.resolveRootId(matchedRoom.rootId) + const token = inviteToken(matchedInvite) || code + if (!isPublic) { + const idx = buildIndex(await readAll(ssbClient)) + if (idx.isCodeConsumed(resolvedRootId, token) || idx.isCodeConsumed(resolvedRootId, code)) throw new Error("Invite already used") + } + let roomKey = null + if (typeof matchedInvite === "object" && matchedInvite.ek) { + roomKey = decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) + if (roomKey) setRoomKey(resolvedRootId, roomKey) + } + await this.addMemberToRoom(matchedRoom.rootId, userId, isPublic ? "" : token) + if (tribeCrypto && roomKey) { + try { + const ssbKeys = require("../server/node_modules/ssb-keys") + const memberKeys = {} + try { memberKeys[userId] = tribeCrypto.boxKeyForMember(roomKey, userId, ssbKeys) } catch (_) {} + if (matchedRoom.author && matchedRoom.author !== userId) { + try { memberKeys[matchedRoom.author] = tribeCrypto.boxKeyForMember(roomKey, matchedRoom.author, ssbKeys) } catch (_) {} + } + if (Object.keys(memberKeys).length) await asP(ssbClient.publish, { type: "tribe-keys", tribeId: resolvedRootId, generation: 1, memberKeys }) + } catch (_) {} + } + return matchedRoom.rootId + }, + + async liveParams(room) { + if (!room || !RID.test(String(room.rid || "")) || !room.token) return null + let secrets = null + if (room.tribeId) secrets = (await getTribeKeysFor(room.tribeId)).slice(0, 1) + else if (room.type === "INVITE-ONLY") secrets = lookupKeys(room.rootId).slice(0, 1) + return { + rid: room.rid, owner: room.author, token: room.token, hub: room.hub, address: room.hubAddress, + ref: room.rootId, title: room.title, secrets: Array.isArray(secrets) && secrets.length ? secrets : null, + ...(room.roomId ? { roomId: room.roomId } : {}) + } + }, + + async occupancy(room) { + const ph = await phone() + if (!ph || typeof ph.roomCount !== "function" || !room || !RID.test(String(room.rid || ""))) return null + try { return await asP(ph.roomCount, { rid: room.rid, hub: room.hub }) } catch (_) { return null } + }, + + async occupancies(rooms) { + const out = new Map() + await Promise.all((rooms || []).map(async (r) => { const o = await this.occupancy(r); if (o) out.set(r.rootId, o) })) + return out + }, + + async join(room) { + const ph = await phone() + if (!ph || typeof ph.roomJoin !== "function") throw new Error("unavailable") + const params = await this.liveParams(room) + if (!params) throw new Error("invalid") + return asP(ph.roomJoin, params) + }, + + async leave() { + const ph = await phone() + if (ph && typeof ph.roomLeave === "function") await asP(ph.roomLeave).catch(() => null) + }, + + async mute(flag) { + const ph = await phone() + if (!ph || typeof ph.roomMute !== "function") return null + return asP(ph.roomMute, !!flag) + }, + + async hand(flag) { + const ph = await phone() + if (!ph || typeof ph.roomHand !== "function") return null + return asP(ph.roomHand, !!flag) + }, + + async silence(id, flag) { + const ph = await phone() + if (!ph || typeof ph.silence !== "function") return null + return asP(ph.silence, { id, on: !!flag }) + }, + + async camera(flag) { + const ph = await phone() + if (!ph || typeof ph.camera !== "function") return null + return asP(ph.camera, !!flag) + }, + + async stream(opts) { + const ph = await phone() + if (!ph || typeof ph.stream !== "function") return null + return asP(ph.stream, opts || {}) + }, + + async viewerJoin(room) { + const ph = await phone() + if (!ph || typeof ph.viewerJoin !== "function") return null + const params = await this.liveParams(room) + if (!params || params.secrets) return null + return asP(ph.viewerJoin, params) + }, + + async viewerLeave(rid) { + const ph = await phone() + if (!ph || typeof ph.viewerLeave !== "function") return null + return asP(ph.viewerLeave, { rid: String(rid || "") }) + }, + + async viewerRooms() { + const ph = await phone() + if (!ph || typeof ph.viewerRooms !== "function") return [] + const list = await rpcValue(ph.viewerRooms) + return Array.isArray(list) ? list : [] + }, + + async publicFeed(rid, kind) { + const ph = await phone() + return ph && typeof ph.publicFeed === "function" ? ph.publicFeed({ rid: String(rid || ""), kind: kind === "sound" ? "sound" : "grid" }) : null + }, + + async recStart() { + const ph = await phone() + if (!ph || typeof ph.roomRecStart !== "function") return null + return asP(ph.roomRecStart) + }, + + async recStop() { + const ph = await phone() + if (!ph || typeof ph.roomRecStop !== "function") return null + return asP(ph.roomRecStop) + }, + + async notify(flag) { + const ph = await phone() + if (!ph || typeof ph.roomNotify !== "function") return null + return asP(ph.roomNotify, !!flag) + }, + + async clearEvents() { + const ph = await phone() + if (!ph || typeof ph.roomClearEvents !== "function") return null + return asP(ph.roomClearEvents) + }, + + async liveState() { + const ph = await phone() + return ph && typeof ph.roomState === "function" ? rpcValue(ph.roomState) : null + }, + + async findByNumber(value, viewerId) { + const number = normalizeNumber(value) + if (!number) return [] + return (await this.listAll({ filter: "all", viewerId })).filter(r => r.number === number && !r.isClosed) + } + } + return api +} diff --git a/src/models/school_model.js b/src/models/school_model.js new file mode 100644 index 0000000..7e63678 --- /dev/null +++ b/src/models/school_model.js @@ -0,0 +1,1423 @@ +const pull = require("../server/node_modules/pull-stream") +const moment = require("../server/node_modules/moment") +const { getConfig } = require("../configs/config-manager.js") +const { readTyped, memoIndex } = require("./typed_log") +const { buildValidatedTombstoneSet } = require("./tombstone_validator") +const { versionGroups } = require("../backend/dedupe") +const opinionCategories = require("../backend/opinion_categories") +const logLimit = getConfig().ssbLogStream?.limit || 1000 + +const norm = (s) => String(s || "").trim().toLowerCase() +const toNum = (v) => { + const n = parseFloat(String(v ?? "").replace(",", ".")) + return Number.isFinite(n) ? n : NaN +} + +const normalizeTags = (raw) => { + if (raw === undefined || raw === null) return [] + if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean) + return String(raw).split(",").map(t => t.trim()).filter(Boolean) +} + +const normalizeDate = (raw) => { + const t = Date.parse(String(raw || "")) + return Number.isFinite(t) ? new Date(t).toISOString() : null +} + +const normalizeIds = (raw) => { + if (raw === undefined || raw === null) return [] + const arr = Array.isArray(raw) ? raw : String(raw).split(/[\s,]+/) + return Array.from(new Set(arr.map(t => String(t || "").trim()).filter(t => t.startsWith("@")))) +} + +const matchSearch = (course, q) => { + const qq = norm(q) + if (!qq) return true + const hay = [ + course.title, + course.description, + Array.isArray(course.tags) ? course.tags.join(" ") : "" + ].map(x => norm(x)).join(" ") + return hay.includes(qq) +} + +module.exports = ({ cooler, transfersModel, schoolCrypto, chatsModel }) => { + const ssbKeys = require("../server/node_modules/ssb-keys") + const nodeCrypto = require("crypto") + let ssb + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + + const SCHOOL_TYPES = [ + "schoolCourse", "schoolLesson", "schoolLessonMedia", "schoolExam", "schoolExamQuestion", + "schoolExamResult", "schoolProgress", "schoolOpinion", "schoolCertificate", "schoolCommentHide", + "schoolEnroll", "school-invite", "tombstone", "tribe-keys", "transfer", "transferConfirm", "chatMember", "chat" + ] + + const readAll = async (ssbClient) => readTyped(ssbClient, SCHOOL_TYPES, { limit: logLimit, withWindow: true, withPrivate: true }) + + const indexOf = (messages, ssbClient) => memoIndex('school', messages, (m) => buildIndex(m, ssbClient)) + + const buildIndex = (messages, ssbClient) => { + const tomb = buildValidatedTombstoneSet(messages) + const courseNodes = new Map() + const lessonNodes = new Map() + const certNodes = new Map() + const lessonReplaces = new Map() + const enrollLatest = new Map() + const transferAgg = new Map() + const transferNodes = new Map() + const transferConfirms = [] + const tribeKeyMsgs = [] + const opinionMsgs = [] + const examNodes = new Map() + const questionNodes = new Map() + const progressLatest = new Map() + const examResultLatest = new Map() + const chatByCourse = new Map() + const chatMsgs = [] + + for (const m of messages) { + const key = m.key + const v = m.value || {} + const c = v.content + if (!c) continue + + if (c.type === "chat") { + if (typeof c.courseId === "string" && c.courseId) chatMsgs.push({ courseId: c.courseId, key, author: v.author }) + continue + } + + if (c.type === "tombstone") continue + + if (c.type === "schoolCourse") { + courseNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) + continue + } + + if (c.type === "schoolLesson" && c.courseId) { + lessonNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + if (c.replaces) lessonReplaces.set(key, c.replaces) + continue + } + + if (c.type === "schoolCertificate" && c.courseId) { + certNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + continue + } + + if (c.type === "schoolEnroll" && c.courseId) { + const author = v.author + if (!author) continue + const ts = v.timestamp || m.timestamp || 0 + const k = `${c.courseId}::${author}` + const prev = enrollLatest.get(k) + if (!prev || ts >= prev.ts) enrollLatest.set(k, { ts, value: !!c.value, author, courseId: c.courseId, transferId: c.transferId || null, keyProof: c.keyProof || null }) + continue + } + + if (c.type === "transfer") { + if (!v.author || c.from !== v.author) continue + transferNodes.set(key, { author: v.author, to: c.to, amount: toNum(c.amount), concept: String(c.concept || ""), discarded: String(c.status || "").toUpperCase() === "DISCARDED", replaces: c.replaces || null }) + continue + } + + if (c.type === "transferConfirm" && c.target) { + if (v.author) transferConfirms.push({ target: c.target, author: v.author }) + continue + } + + if (c.type === "tribe-keys" && c.tribeId && c.memberKeys && typeof c.memberKeys === "object") { + tribeKeyMsgs.push({ tribeId: c.tribeId, memberKeys: c.memberKeys, author: v.author }) + continue + } + + if (c.type === "schoolOpinion" && c.target) { + opinionMsgs.push({ target: c.target, author: v.author, category: c.category }) + continue + } + + if (c.type === "schoolLessonMedia" && c.courseId && c.target) { + examNodes.set("media:" + key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author, isMedia: true }) + continue + } + + if (c.type === "schoolExamQuestion" && c.courseId && c.target) { + questionNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + continue + } + + if (c.type === "schoolExam" && c.courseId) { + examNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + continue + } + + if (c.type === "schoolProgress" && c.courseId && c.lessonId) { + const ts = v.timestamp || m.timestamp || 0 + const k = `${c.courseId}::${c.lessonId}::${v.author}` + const prev = progressLatest.get(k) + if (!prev || ts >= prev.ts) progressLatest.set(k, { ts, value: c.value !== false, author: v.author, courseId: c.courseId, lessonId: c.lessonId }) + continue + } + } + + if (ssbClient && ssbClient.private && typeof ssbClient.private.unbox === "function") { + for (const m of messages) { + const sealed = typeof m.value?.content === "string" + if (!sealed && m.value?.private !== true) continue + try { + const dec = sealed ? ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) : m + const c = dec?.value?.content + const author = dec?.value?.author + if (!c || !author) continue + const ts = dec.value.timestamp || m.timestamp || 0 + if (c.type === "schoolEnroll" && c.courseId) { + const k = `${c.courseId}::${author}` + const prev = enrollLatest.get(k) + if (!prev || ts >= prev.ts) enrollLatest.set(k, { ts, value: !!c.value, author, courseId: c.courseId, transferId: c.transferId || null, keyProof: c.keyProof || null }) + continue + } + if (c.type === "schoolProgress" && c.courseId && c.lessonId) { + const k = `${c.courseId}::${c.lessonId}::${author}` + const prev = progressLatest.get(k) + if (!prev || ts >= prev.ts) progressLatest.set(k, { ts, value: c.value !== false, author, courseId: c.courseId, lessonId: c.lessonId }) + continue + } + if (c.type === "schoolExamResult" && c.courseId && c.examId) { + const k = `${c.examId}::${author}` + const prev = examResultLatest.get(k) + if (!prev || ts >= prev.ts) examResultLatest.set(k, { ts, author, courseId: c.courseId, examId: c.examId, score: c.score, total: c.total }) + continue + } + } catch {} + } + } + + const transferChild = new Map() + const transferForked = new Set() + for (const [key, t] of transferNodes.entries()) { + const orig = t.replaces ? transferNodes.get(t.replaces) : null + if (!orig || orig.author !== t.author) continue + if (transferChild.has(t.replaces)) transferForked.add(t.replaces) + transferChild.set(t.replaces, key) + } + const confirmersOf = new Map() + for (const cf of transferConfirms) { + if (!confirmersOf.has(cf.target)) confirmersOf.set(cf.target, new Set()) + confirmersOf.get(cf.target).add(cf.author) + } + for (const [key, t] of transferNodes.entries()) { + const orig = t.replaces ? transferNodes.get(t.replaces) : null + if (orig && orig.author === t.author) continue + const chain = [key] + while (transferChild.has(chain[chain.length - 1])) chain.push(transferChild.get(chain[chain.length - 1])) + const tipKey = chain[chain.length - 1] + const tip = transferNodes.get(tipKey) + const signers = new Set() + for (const ck of chain) for (const signer of (confirmersOf.get(ck) || [])) if (signer === tip.to) signers.add(signer) + const bill = { author: tip.author, to: tip.to, amount: tip.amount, concept: tip.concept, void: tip.discarded || tomb.has(tipKey) || chain.some(ck => transferForked.has(ck)), signers } + for (const ck of chain) transferAgg.set(ck, bill) + } + + const lessonParent = new Map() + for (const [key, target] of lessonReplaces.entries()) { + const node = lessonNodes.get(key) + const orig = lessonNodes.get(target) + if (node && orig && String(orig.author) === String(node.author)) lessonParent.set(key, target) + } + const lessonChildOf = new Map() + for (const [k, t] of lessonParent.entries()) lessonChildOf.set(t, k) + const lessonRootOf = (id) => { + let cur = id + while (lessonParent.has(cur)) cur = lessonParent.get(cur) + return cur + } + + const vg = versionGroups(Array.from(courseNodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))) + const rootOf = vg.rootOf + const tipOf = vg.tipOf + + const tipByRoot = new Map() + for (const r of vg.roots) tipByRoot.set(r, tipOf(r)) + + const courseAuthorOf = (id) => { const n = courseNodes.get(rootOf(id)); return n ? n.author : null } + for (const [key, node] of lessonNodes.entries()) if (courseAuthorOf(node.c.courseId) !== node.author) lessonNodes.delete(key) + for (const [key, node] of certNodes.entries()) if (courseAuthorOf(node.c.courseId) !== node.author) certNodes.delete(key) + for (const [key, node] of examNodes.entries()) if (courseAuthorOf(node.c.courseId) !== node.author) examNodes.delete(key) + for (const [key, node] of questionNodes.entries()) if (courseAuthorOf(node.c.courseId) !== node.author) questionNodes.delete(key) + for (const ch of chatMsgs) { + if (chatByCourse.has(ch.courseId) || courseAuthorOf(ch.courseId) !== ch.author) continue + chatByCourse.set(ch.courseId, ch.key) + } + + const enrollByCourse = new Map() + for (const { courseId, author, value, transferId, keyProof } of enrollLatest.values()) { + const root = rootOf(courseId) + if (!enrollByCourse.has(root)) enrollByCourse.set(root, new Map()) + const enrollments = enrollByCourse.get(root) + if (value) enrollments.set(author, { transferId, keyProof: keyProof || null }) + else enrollments.delete(author) + } + + const opinionsByCourse = new Map() + for (const op of opinionMsgs) { + const root = rootOf(op.target) + if (!courseNodes.has(root)) continue + if (!opinionsByCourse.has(root)) opinionsByCourse.set(root, { byAuthor: new Map() }) + const agg = opinionsByCourse.get(root) + if (!op.author || agg.byAuthor.has(op.author)) continue + agg.byAuthor.set(op.author, op.category) + } + + const progressByCourse = new Map() + for (const pr of progressLatest.values()) { + if (!pr.value) continue + const root = rootOf(pr.courseId) + if (!courseNodes.has(root)) continue + if (!progressByCourse.has(root)) progressByCourse.set(root, new Map()) + const perStudent = progressByCourse.get(root) + if (!perStudent.has(pr.author)) perStudent.set(pr.author, new Set()) + perStudent.get(pr.author).add(lessonRootOf(pr.lessonId)) + } + + const grantsByCourse = new Map() + const myId = ssbClient ? ssbClient.id : null + for (const tk of tribeKeyMsgs) { + const root = rootOf(tk.tribeId) + if (!courseNodes.has(root)) continue + if (courseNodes.get(root).author !== tk.author) continue + if (!grantsByCourse.has(root)) grantsByCourse.set(root, new Set()) + for (const member of Object.keys(tk.memberKeys)) grantsByCourse.get(root).add(member) + if (schoolCrypto && myId && tk.memberKeys[myId]) { + try { + const cfg = require("../server/ssb_config") + const unboxed = ssbKeys.unbox(tk.memberKeys[myId], cfg.keys) + if (typeof unboxed === "string" && unboxed) schoolCrypto.addNewKey(root, unboxed) + } catch {} + } + } + + return { tomb, courseNodes, lessonNodes, certNodes, examNodes, vg, rootOf, tipOf, tipByRoot, enrollByCourse, transferAgg, grantsByCourse, opinionsByCourse, progressByCourse, examResultLatest, lessonRootOf, lessonChildOf, questionNodes, chatByCourse } + } + + const keyProofFor = (keyHex, studentId) => { + try { return nodeCrypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(studentId), "utf8").digest("hex") } catch (_) { return null } + } + + const buildCourseObject = (node, rootId, enrollments, transferAgg, grants, opinionAgg, chatByCourse = new Map()) => { + let c = node.c || {} + const ringKeys = schoolCrypto ? (schoolCrypto.getKeys(rootId) || []) : [] + if (c.encryptedPayload) { + const dec = ringKeys.length && schoolCrypto ? schoolCrypto.decryptContent(c, ringKeys.map(k => [k])) : { ...c, _undecryptable: true } + if (dec._undecryptable) return { undecryptable: true, id: node.key, rootId, author: node.author } + c = dec + } + let blobId = c.image || null + if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] + + const priceN = toNum(c.price) + const paid = Number.isFinite(priceN) && priceN > 0 + const students = [] + const pending = [] + const grantedViaInvite = [] + for (const [student, info] of (enrollments || new Map())) { + if (info.keyProof && ringKeys.some(k => keyProofFor(k, student) === info.keyProof)) grantedViaInvite.push(student) + if (paid) { + const bill = info.transferId ? (transferAgg && transferAgg.get(info.transferId)) : null + const settled = !!bill && !bill.void && bill.author === student && bill.to === node.author && bill.amount >= priceN && bill.concept.includes(rootId) && bill.signers.has(node.author) + if (settled) students.push(student) + else pending.push({ author: student, transferId: info.transferId || null }) + } else { + students.push(student) + } + } + const studentOpinions = { opinions: {}, voters: [] } + for (const [author, category] of ((opinionAgg && opinionAgg.byAuthor) || new Map())) { + if (!students.includes(author)) continue + studentOpinions.voters.push(author) + if (category) studentOpinions.opinions[category] = (studentOpinions.opinions[category] || 0) + 1 + } + return { + id: node.key, + rootId, + title: c.title, + description: c.description, + tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags), + image: blobId, + price: Number.isFinite(priceN) && priceN > 0 ? priceN.toFixed(6) : "0.000000", + visibility: String(c.visibility || "PUBLIC").toUpperCase() === "INVITE" ? "INVITE" : "PUBLIC", + status: String(c.status || "ONGOING").toUpperCase() === "CLOSED" ? "CLOSED" : "ONGOING", + invited: normalizeIds(c.invited).filter(id => !students.includes(id)), + startDate: c.startDate || null, + chatId: c.chatId || chatByCourse.get(rootId) || null, + inviteCode: c.inviteCode || null, + author: node.author, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + students, + pending, + granted: Array.from(new Set([...(grants ? Array.from(grants) : []), ...grantedViaInvite])), + opinions: studentOpinions.opinions, + opinions_inhabitants: studentOpinions.voters + } + } + + const isProtected = (course) => Number(course.price) > 0 || course.visibility === "INVITE" + + const addToChat = async (ssbClient, course, members) => { + if (!course.chatId || !members.length) return + for (const member of members) { + try { + await new Promise((res) => ssbClient.publish({ type: "chatMember", target: course.chatId, member, on: true, createdAt: new Date().toISOString() }, () => res())) + } catch {} + } + if (chatsModel && typeof chatsModel.listAll === "function") { + try { await chatsModel.listAll({ filter: "all", viewerId: ssbClient.id }) } catch {} + } + } + + const canView = (course, viewer) => + course.visibility === "PUBLIC" || + course.author === viewer || + course.invited.includes(viewer) || + course.students.includes(viewer) || + course.pending.some(p => p.author === viewer) + + const ensureCourseKey = async (ssbClient, rootId) => { + if (!schoolCrypto) return null + let key = schoolCrypto.getKey(rootId) + if (key) return key + key = schoolCrypto.generateTribeKey() + schoolCrypto.setKey(rootId, key, 1) + await publishKeyGrant(ssbClient, rootId, key, [ssbClient.id]) + return key + } + + const publishKeyGrant = (ssbClient, rootId, key, members) => { + const memberKeys = {} + for (const member of members) { + try { memberKeys[member] = schoolCrypto.boxKeyForMember(key, member, ssbKeys) } catch {} + } + if (!Object.keys(memberKeys).length) return Promise.resolve(null) + return new Promise((res) => ssbClient.publish({ type: "tribe-keys", tribeId: rootId, generation: 1, memberKeys }, () => res(null))) + } + + return { + type: "schoolCourse", + + async createCourse(data) { + const ssbClient = await openSsb() + + const title = String(data.title || "").trim() + const description = String(data.description || "").trim() + if (!title) throw new Error("Invalid title") + if (!description) throw new Error("Invalid description") + + let blobId = data.image || null + if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] + + const priceN = toNum(data.price) + const protectedCourse = (Number.isFinite(priceN) && priceN > 0) || String(data.visibility || "PUBLIC").toUpperCase() === "INVITE" + const content = { + type: "schoolCourse", + title, + description, + tags: normalizeTags(data.tags), + image: blobId, + price: Number.isFinite(priceN) && priceN > 0 ? priceN.toFixed(6) : "0.000000", + visibility: String(data.visibility || "PUBLIC").toUpperCase() === "INVITE" ? "INVITE" : "PUBLIC", + status: "ONGOING", + invited: normalizeIds(data.invited), + startDate: (() => { + const d = normalizeDate(data.startDate) + if (d && d.slice(0, 10) < new Date().toISOString().slice(0, 10)) throw new Error("Start date cannot be in the past") + return d + })(), + chatId: null, + author: ssbClient.id, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + } + + let toPublish = content + let courseKey = null + if (content.visibility === "INVITE" && schoolCrypto) { + courseKey = schoolCrypto.generateTribeKey() + toPublish = schoolCrypto.encryptContent(content, [courseKey], true) + } + const published = await new Promise((res, rej) => ssbClient.publish(toPublish, (e, m) => e ? rej(e) : res(m))) + if (published && published.key && chatsModel) { + try { + await chatsModel.createChat(`Course: ${title}`, description, blobId, "school", protectedCourse ? "INVITE-ONLY" : "OPEN", normalizeTags(data.tags), null, { courseId: published.key }) + } catch {} + } + if (published && published.key) { + if (courseKey) { + schoolCrypto.setKey(published.key, courseKey, 1) + await publishKeyGrant(ssbClient, published.key, courseKey, [ssbClient.id]) + } else if (Number(content.price) > 0) { + await ensureCourseKey(ssbClient, published.key) + } + } + return published + }, + + async resolveCurrentId(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const { tomb, vg } = indexOf(messages, ssbClient) + + const cur = vg.tipOf(courseId) + if (tomb.has(cur) || vg.isForeignCopy(vg.rootOf(cur))) throw new Error("Course not found") + return cur + }, + + async resolveRootId(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const { tomb, vg } = indexOf(messages, ssbClient) + + if (tomb.has(vg.tipOf(courseId)) || vg.isForeignCopy(vg.rootOf(courseId))) throw new Error("Course not found") + return vg.rootOf(courseId) + }, + + async updateCourse(id, data) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const tipId = await this.resolveCurrentId(id) + const node = idx.courseNodes.get(tipId) + if (!node || !node.c) throw new Error("Course not found") + + let existingContent = node.c + if (existingContent.encryptedPayload) { + const rootForKeys = idx.rootOf(tipId) + const keys = schoolCrypto ? (schoolCrypto.getKeys(rootForKeys) || []) : [] + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(existingContent, keys.map(k => [k])) : { ...existingContent, _undecryptable: true } + if (dec._undecryptable) throw new Error("Course not found") + delete dec._decrypted + existingContent = dec + } + const author = existingContent.author + if (author !== ssbClient.id) throw new Error("Unauthorized") + + const patch = {} + + if (data.title !== undefined) { + const t = String(data.title || "").trim() + if (!t) throw new Error("Invalid title") + patch.title = t + } + + if (data.description !== undefined) { + const d = String(data.description || "").trim() + if (!d) throw new Error("Invalid description") + patch.description = d + } + + if (data.tags !== undefined) patch.tags = normalizeTags(data.tags) + + if (data.image !== undefined) { + let blobId = data.image + if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] + patch.image = blobId || null + } + + if (data.price !== undefined) { + const p = toNum(data.price) + if (Number.isFinite(p) && p < 0) throw new Error("Invalid price") + patch.price = Number.isFinite(p) && p > 0 ? p.toFixed(6) : "0.000000" + } + + if (data.visibility !== undefined) { + patch.visibility = String(data.visibility || "PUBLIC").toUpperCase() === "INVITE" ? "INVITE" : "PUBLIC" + } + + if (data.status !== undefined) { + const s = String(data.status || "").toUpperCase() + if (!["ONGOING", "CLOSED"].includes(s)) throw new Error("Invalid status") + patch.status = s + } + + if (data.invited !== undefined) patch.invited = normalizeIds(data.invited) + + if (data.inviteCode !== undefined) patch.inviteCode = String(data.inviteCode || "") || null + + if (data.startDate !== undefined) patch.startDate = normalizeDate(data.startDate) + + const next = { + ...existingContent, + ...patch, + author, + createdAt: existingContent.createdAt, + updatedAt: new Date().toISOString(), + replaces: tipId, + type: "schoolCourse" + } + + const tombMsg = { + type: "tombstone", + target: tipId, + deletedAt: new Date().toISOString(), + author: ssbClient.id + } + + await new Promise((res, rej) => ssbClient.publish(tombMsg, (e) => e ? rej(e) : res())) + let toPublish = next + if (String(next.visibility || "").toUpperCase() === "INVITE" && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, idx.rootOf(tipId)) + if (key) toPublish = schoolCrypto.encryptContent(next, [key], true) + } + const published = await new Promise((res, rej) => ssbClient.publish(toPublish, (e, m) => e ? rej(e) : res(m))) + const linkedChatId = existingContent.chatId || idx.chatByCourse.get(idx.rootOf(tipId)) || null + if (patch.image !== undefined && linkedChatId && chatsModel && typeof chatsModel.updateChatById === "function") { + try { await chatsModel.updateChatById(linkedChatId, { image: patch.image }) } catch {} + } + return published + }, + + async updateCourseStatus(id, status) { + return this.updateCourse(id, { status: String(status || "").toUpperCase() }) + }, + + async deleteCourse(id) { + const ssbClient = await openSsb() + const tipId = await this.resolveCurrentId(id) + const course = await this.getCourseById(tipId) + if (!course || course.author !== ssbClient.id) throw new Error("Unauthorized") + + const tombMsg = { + type: "tombstone", + target: tipId, + deletedAt: new Date().toISOString(), + author: ssbClient.id + } + + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async inviteStudent(id, studentId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(id) + const ids = normalizeIds(studentId) + if (!ids.length) throw new Error("Invalid student id") + const updated = await this.updateCourse(course.id, { invited: course.invited.concat(ids) }) + if (isProtected(course) && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, course.rootId) + if (key) await publishKeyGrant(ssbClient, course.rootId, key, ids) + } + return updated + }, + + async uninviteStudent(id, studentId) { + const course = await this.getCourseById(id) + const drop = new Set(normalizeIds(studentId)) + return this.updateCourse(course.id, { invited: course.invited.filter(s => !drop.has(s)) }) + }, + + async enroll(id) { + const ssbClient = await openSsb() + const me = ssbClient.id + + const course = await this.getCourseById(id) + if (course.author === me) throw new Error("Cannot enroll in your own course") + if (course.status !== "ONGOING") throw new Error("Course is closed") + if (course.students.includes(me)) return { alreadyEnrolled: true } + if (course.pending.some(p => p.author === me)) return { alreadyPending: true } + if (course.visibility === "INVITE" && !course.invited.includes(me)) throw new Error("Invitation required") + + const rootId = course.rootId || (await this.resolveRootId(id)) + + let transferId = null + if (Number(course.price) > 0) { + if (!transfersModel) throw new Error("Transfers module unavailable") + const deadline = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() + const bill = await transfersModel.createTransfer(course.author, `SCHOOL ${rootId}`, course.price, deadline, ["SCHOOL"], "ECONOMIC") + transferId = bill && bill.key ? bill.key : null + } + + const msg = { + type: "schoolEnroll", + courseId: rootId, + value: true, + transferId, + createdAt: new Date().toISOString() + } + + if (isProtected(course)) { + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, course.author], (e, m) => e ? rej(e) : res(m))) + } + const published = await new Promise((res, rej) => ssbClient.publish(msg, (e, m) => e ? rej(e) : res(m))) + if (course.chatId && chatsModel && typeof chatsModel.joinChat === "function") { + try { await chatsModel.joinChat(course.chatId) } catch {} + } + return published + }, + + async unenroll(id) { + const ssbClient = await openSsb() + const me = ssbClient.id + + const course = await this.getCourseById(id) + if (!course.students.includes(me) && !course.pending.some(pn => pn.author === me)) return { notEnrolled: true } + + const rootId = course.rootId || (await this.resolveRootId(id)) + + const msg = { + type: "schoolEnroll", + courseId: rootId, + value: false, + createdAt: new Date().toISOString() + } + + if (isProtected(course)) { + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, course.author], (e, m) => e ? rej(e) : res(m))) + } + return new Promise((res, rej) => ssbClient.publish(msg, (e, m) => e ? rej(e) : res(m))) + }, + + async addLesson(courseId, data) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + + const title = String(data.title || "").trim() + const text = String(data.text || "").trim() + if (!title) throw new Error("Invalid title") + if (!text) throw new Error("Invalid text") + + const rootId = course.rootId || (await this.resolveRootId(courseId)) + + const orderN = toNum(data.order) + let content = { + type: "schoolLesson", + courseId: rootId, + title, + text, + unit: String(data.unit || "").trim() || null, + order: Number.isFinite(orderN) ? orderN : null, + sessionDate: (() => { + const d = normalizeDate(data.sessionDate) + if (d && d.slice(0, 10) < new Date().toISOString().slice(0, 10)) throw new Error("Session date cannot be in the past") + return d + })(), + author: ssbClient.id, + createdAt: new Date().toISOString() + } + + if (isProtected(course) && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, rootId) + if (key) content = schoolCrypto.encryptContent(content, [key], true) + } + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async createOpinion(courseId, category) { + if (!opinionCategories.includes(category)) throw new Error("Invalid voting category") + const ssbClient = await openSsb() + const me = ssbClient.id + const course = await this.getCourseById(courseId) + if (course.author === me) throw new Error("Cannot rate your own course") + if (!course.students.includes(me)) throw new Error("Only students can rate a course") + if (course.opinions_inhabitants.includes(me)) throw new Error("Already voted") + + const content = { type: "schoolOpinion", target: course.rootId, category, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async markLesson(courseId, lessonId, value = true) { + const ssbClient = await openSsb() + const me = ssbClient.id + const course = await this.getCourseById(courseId) + if (!course.students.includes(me)) throw new Error("Only students can track progress") + + const msg = { + type: "schoolProgress", + courseId: course.rootId, + lessonId, + value: value !== false, + createdAt: new Date().toISOString() + } + + if (isProtected(course)) { + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, course.author], (e, m) => e ? rej(e) : res(m))) + } + return new Promise((res, rej) => ssbClient.publish(msg, (e, m) => e ? rej(e) : res(m))) + }, + + async progressForCourse(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const rootId = idx.rootOf(courseId) + const perStudent = idx.progressByCourse.get(rootId) || new Map() + const out = {} + for (const [student, lessonSet] of perStudent) out[student] = lessonSet.size + return out + }, + + async createExam(courseId, title, opts = {}) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + if (!isProtected(course)) throw new Error("Exams are only for paid or invite courses") + if (!schoolCrypto) throw new Error("School crypto unavailable") + + const examTitle = String(title || "").trim() + if (!examTitle) throw new Error("Invalid title") + + const rootId = course.rootId + const key = await ensureCourseKey(ssbClient, rootId) + if (!key) throw new Error("Course key unavailable") + + let content = { + type: "schoolExam", + courseId: rootId, + title: examTitle, + lessonId: opts.lessonId ? String(opts.lessonId) : null, + author: ssbClient.id, + createdAt: new Date().toISOString() + } + content = schoolCrypto.encryptContent(content, [key], true) + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async addExamQuestion(courseId, examId, data) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + if (!schoolCrypto) throw new Error("School crypto unavailable") + + const q = String(data.q || "").trim() + const options = [data.o1, data.o2, data.o3, data.o4].map(o => String(o || "").trim()) + const correct = parseInt(String(data.correct), 10) + if (!q) throw new Error("Invalid question") + if (options.some(o => !o)) throw new Error("Four options are required") + if (!Number.isFinite(correct) || correct < 0 || correct > 3) throw new Error("Invalid correct option") + + const key = await ensureCourseKey(ssbClient, course.rootId) + if (!key) throw new Error("Course key unavailable") + + let content = { + type: "schoolExamQuestion", + courseId: course.rootId, + target: examId, + q, + options, + correct, + author: ssbClient.id, + createdAt: new Date().toISOString() + } + content = schoolCrypto.encryptContent(content, [key], true) + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async deleteExamQuestion(questionId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const node = idx.questionNodes.get(questionId) + if (!node || idx.tomb.has(questionId)) throw new Error("Question not found") + if (node.author !== ssbClient.id) throw new Error("Unauthorized") + const tombMsg = { type: "tombstone", target: questionId, deletedAt: new Date().toISOString(), author: ssbClient.id } + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async deleteExam(examId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const node = idx.examNodes.get(examId) + if (!node || node.isMedia || idx.tomb.has(examId)) throw new Error("Exam not found") + if (node.author !== ssbClient.id) throw new Error("Unauthorized") + const tombMsg = { type: "tombstone", target: examId, deletedAt: new Date().toISOString(), author: ssbClient.id } + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async listExams(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const rootId = idx.rootOf(courseId) + const keys = schoolCrypto ? (schoolCrypto.getKeys(rootId) || []) : [] + + const exams = [] + for (const node of idx.examNodes.values()) { + if (node.isMedia) continue + if (idx.tomb.has(node.key)) continue + if (idx.rootOf(node.c.courseId) !== rootId) continue + let c = node.c + let locked = false + if (c.encryptedPayload) { + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(c, keys.map(k => [k])) : { ...c, _undecryptable: true } + if (dec._undecryptable) locked = true + else c = dec + } + const myResult = idx.examResultLatest.get(`${node.key}::${ssbClient.id}`) || null + const results = [] + for (const r of idx.examResultLatest.values()) { + if (r.examId === node.key) results.push({ author: r.author, score: r.score, total: r.total }) + } + const questions = [] + if (!locked) { + for (const qn of idx.questionNodes.values()) { + if (idx.tomb.has(qn.key)) continue + if (qn.c.target !== node.key) continue + let qc = qn.c + if (qc.encryptedPayload) { + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(qc, keys.map(k => [k])) : { ...qc, _undecryptable: true } + if (dec._undecryptable) continue + qc = dec + } + questions.push({ id: qn.key, q: qc.q, options: qc.options, correct: qc.correct, createdAt: qc.createdAt || new Date(qn.ts).toISOString() }) + } + questions.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + } + const passMark = locked ? null : 10 + exams.push({ + id: node.key, + courseId: rootId, + locked, + title: locked ? null : c.title, + lessonId: locked ? null : (c.lessonId ? idx.lessonRootOf(c.lessonId) : null), + passMark, + questions, + author: node.author, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + myResult: myResult ? { score: myResult.score, total: myResult.total, at: myResult.ts, passed: passMark != null && Number(myResult.score) >= passMark } : null, + results: results.map(r => ({ ...r, passed: passMark != null && Number(r.score) >= passMark })) + }) + } + return exams.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + }, + + async takeExam(courseId, examId, answers) { + const ssbClient = await openSsb() + const me = ssbClient.id + const course = await this.getCourseById(courseId) + if (course.author === me) throw new Error("Cannot take your own exam") + if (!course.students.includes(me)) throw new Error("Only students can take exams") + + const exams = await this.listExams(courseId) + const exam = exams.find(x => x.id === examId) + if (!exam || exam.locked) throw new Error("Exam not found") + if (exam.myResult && exam.myResult.passed) throw new Error("Exam already passed") + if (exam.myResult && Date.now() - Number(exam.myResult.at || 0) < 24 * 60 * 60 * 1000) throw new Error("Retry available after 24 hours") + + if (!exam.questions.length) throw new Error("Exam has no questions") + + let correctCount = 0 + exam.questions.forEach((question, i) => { + const given = parseInt(String(Array.isArray(answers) ? answers[i] : (answers || {})[`q${i}`]), 10) + if (given === question.correct) correctCount += 1 + }) + const score = Math.round((correctCount / exam.questions.length) * 100) / 10 + + const msg = { + type: "schoolExamResult", + courseId: course.rootId, + examId, + score, + total: 10, + createdAt: new Date().toISOString() + } + + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, course.author], (e, m) => e ? rej(e) : res(m))) + }, + + async addLessonMaterial(courseId, lessonId, mediaMarkdown, caption = "") { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + const media = String(mediaMarkdown || "").trim() + if (!media) throw new Error("Invalid material") + + let content = { + type: "schoolLessonMedia", + courseId: course.rootId, + target: lessonId, + media, + caption: String(caption || "").trim() || null, + author: ssbClient.id, + createdAt: new Date().toISOString() + } + + if (isProtected(course) && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, course.rootId) + if (key) content = schoolCrypto.encryptContent(content, [key], true) + } + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async deleteLessonMaterial(materialId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const node = idx.examNodes.get("media:" + materialId) + if (!node || idx.tomb.has(materialId)) throw new Error("Material not found") + if (node.author !== ssbClient.id) throw new Error("Unauthorized") + const tombMsg = { type: "tombstone", target: materialId, deletedAt: new Date().toISOString(), author: ssbClient.id } + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async listLessonMaterials(courseId, lessonId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const rootId = idx.rootOf(courseId) + const keys = schoolCrypto ? (schoolCrypto.getKeys(rootId) || []) : [] + + const materials = [] + for (const node of idx.examNodes.values()) { + if (!node.isMedia) continue + if (idx.tomb.has(node.key)) continue + if (idx.rootOf(node.c.courseId) !== rootId) continue + if (idx.lessonRootOf(node.c.target) !== idx.lessonRootOf(lessonId)) continue + let c = node.c + let locked = false + if (c.encryptedPayload) { + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(c, keys.map(k => [k])) : { ...c, _undecryptable: true } + if (dec._undecryptable) locked = true + else c = dec + } + materials.push({ + id: node.key, + lessonId, + locked, + media: locked ? null : (c.media || ""), + caption: locked ? null : (c.caption || null), + author: node.author, + createdAt: c.createdAt || new Date(node.ts).toISOString() + }) + } + return materials.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + }, + + async generateInvite(courseId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + if (course.visibility !== "INVITE") throw new Error("Only invite courses use invitation codes") + if (!schoolCrypto) throw new Error("School crypto unavailable") + + if (course.inviteCode) { + const messages = await readAll(ssbClient) + const alive = messages.some(m => { + const c = m.value && m.value.content + if (!c || c.type !== "school-invite" || c.target !== course.rootId || m.value.author !== course.author) return false + try { return schoolCrypto.hashInviteCode(course.inviteCode, c.salt) === c.codeHash } catch { return false } + }) + if (alive) return { code: course.inviteCode, courseId: course.rootId } + const key = await ensureCourseKey(ssbClient, course.rootId) + if (!key) throw new Error("Course key unavailable") + const salt = schoolCrypto.generateInviteSalt() + const ek = schoolCrypto.encryptForInvite(key, course.inviteCode, salt) + await new Promise((res, rej) => ssbClient.publish({ + type: "school-invite", target: course.rootId, ek, salt, + codeHash: schoolCrypto.hashInviteCode(course.inviteCode, salt) + }, (e) => e ? rej(e) : res())) + return { code: course.inviteCode, courseId: course.rootId } + } + + const key = await ensureCourseKey(ssbClient, course.rootId) + if (!key) throw new Error("Course key unavailable") + + const code = nodeCrypto.randomBytes(16).toString("hex") + const salt = schoolCrypto.generateInviteSalt() + const ek = schoolCrypto.encryptForInvite(key, code, salt) + await new Promise((res, rej) => ssbClient.publish({ + type: "school-invite", target: course.rootId, ek, salt, + codeHash: schoolCrypto.hashInviteCode(code, salt) + }, (e) => e ? rej(e) : res())) + try { await this.updateCourse(courseId, { inviteCode: code }) } catch (_) {} + return { code, courseId: course.rootId } + }, + + async joinByInvite(rawCode) { + if (!schoolCrypto) throw new Error("School crypto unavailable") + const ssbClient = await openSsb() + const me = ssbClient.id + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const candidates = Array.from(new Set([String(rawCode || "").trim(), String(rawCode || "").trim().toLowerCase()])).filter(Boolean) + let matched = null + let code = null + for (const m of messages) { + const c = m.value && m.value.content + if (!c || c.type !== "school-invite") continue + const owner = idx.courseNodes.get(idx.rootOf(c.target)) + if (!owner || owner.author !== m.value.author) continue + for (const cand of candidates) { + try { + if (schoolCrypto.hashInviteCode(cand, c.salt) === c.codeHash) { matched = c; code = cand; break } + } catch {} + } + if (matched) break + } + if (!matched) throw new Error("Invalid or expired invite code") + + const courseKey = schoolCrypto.decryptFromInvite(matched.ek, code, matched.salt) + if (!courseKey) throw new Error("Could not decrypt invite") + schoolCrypto.addNewKey(matched.target, courseKey) + + const tipId = idx.tipOf(matched.target) + const node = idx.courseNodes.get(tipId) + if (!node) throw new Error("Course not found") + const teacherId = node.author + let cc = node.c + if (cc.encryptedPayload) { + const dec = schoolCrypto.decryptContent(cc, [[courseKey]]) + if (dec && !dec._undecryptable) cc = dec + } + const priceN = toNum(cc.price) + + if (teacherId !== me) { + const enrollments = idx.enrollByCourse.get(matched.target) || new Map() + if (!enrollments.has(me)) { + let transferId = null + if (Number.isFinite(priceN) && priceN > 0 && transfersModel) { + const deadline = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() + const bill = await transfersModel.createTransfer(teacherId, `SCHOOL ${matched.target}`, priceN.toFixed(6), deadline, ["SCHOOL"], "ECONOMIC") + transferId = bill && bill.key ? bill.key : null + } + const msg = { type: "schoolEnroll", courseId: matched.target, value: true, transferId, keyProof: keyProofFor(courseKey, me), createdAt: new Date().toISOString() } + await new Promise((res, rej) => ssbClient.private.publish(msg, [me, teacherId], (e, m) => e ? rej(e) : res(m))) + } + const enrollChatId = cc.chatId || idx.chatByCourse.get(matched.target) || null + if (enrollChatId) { + try { + await new Promise((res) => ssbClient.publish({ type: "chatMember", target: enrollChatId, member: me, on: true, createdAt: new Date().toISOString() }, () => res())) + } catch {} + } + } + return { ok: true, courseId: matched.target } + }, + + async grantAccess(courseId, studentId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + if (!schoolCrypto) throw new Error("School crypto unavailable") + + const ids = normalizeIds(studentId) + if (!ids.length) throw new Error("Invalid student id") + for (const student of ids) { + const known = course.students.includes(student) || course.invited.includes(student) || course.pending.some(p => p.author === student) + if (!known) throw new Error("Not a student of this course") + } + + const key = await ensureCourseKey(ssbClient, course.rootId) + const grant = await publishKeyGrant(ssbClient, course.rootId, key, ids) + await addToChat(ssbClient, course, ids) + return grant + }, + + async updateLesson(courseId, lessonId, data) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + + const lessons = await this.listLessons(course.rootId) + const current = lessons.find(l => l.id === lessonId) + if (!current || current.locked) throw new Error("Lesson not found") + + const title = data.title !== undefined ? String(data.title || "").trim() : current.title + const text = data.text !== undefined ? String(data.text || "").trim() : current.text + if (!title || !text) throw new Error("Invalid lesson") + + const orderN = toNum(data.order) + let content = { + type: "schoolLesson", + courseId: course.rootId, + title, + text, + unit: data.unit !== undefined ? (String(data.unit || "").trim() || null) : current.unit, + order: data.order !== undefined ? (Number.isFinite(orderN) ? orderN : null) : current.order, + sessionDate: data.sessionDate !== undefined ? normalizeDate(data.sessionDate) : current.sessionDate, + author: ssbClient.id, + createdAt: current.createdAt, + updatedAt: new Date().toISOString(), + replaces: lessonId + } + + if (isProtected(course) && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, course.rootId) + if (key) content = schoolCrypto.encryptContent(content, [key], true) + } + + const tombMsg = { type: "tombstone", target: lessonId, deletedAt: new Date().toISOString(), author: ssbClient.id } + await new Promise((res, rej) => ssbClient.publish(tombMsg, (e) => e ? rej(e) : res())) + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async deleteLesson(lessonId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const node = idx.lessonNodes.get(lessonId) + if (!node || idx.tomb.has(lessonId)) throw new Error("Lesson not found") + if (node.author !== ssbClient.id) throw new Error("Unauthorized") + + const tombMsg = { + type: "tombstone", + target: lessonId, + deletedAt: new Date().toISOString(), + author: ssbClient.id + } + + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async listLessons(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const rootId = idx.rootOf(courseId) + const keys = schoolCrypto ? (schoolCrypto.getKeys(rootId) || []) : [] + const lessons = [] + for (const node of idx.lessonNodes.values()) { + if (idx.tomb.has(node.key)) continue + if (idx.lessonChildOf.has(node.key)) continue + if (idx.rootOf(node.c.courseId) !== rootId) continue + let c = node.c + let locked = false + if (c.encryptedPayload) { + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(c, keys.map(k => [k])) : { ...c, _undecryptable: true } + if (dec._undecryptable) locked = true + else c = dec + } + lessons.push({ + id: node.key, + courseId: rootId, + locked, + title: locked ? null : c.title, + text: locked ? null : c.text, + unit: locked ? null : (c.unit || null), + order: locked ? null : (Number.isFinite(toNum(c.order)) ? toNum(c.order) : null), + sessionDate: locked ? null : (c.sessionDate || null), + author: node.author, + createdAt: c.createdAt || new Date(node.ts).toISOString() + }) + } + const myProgress = (idx.progressByCourse.get(rootId) || new Map()).get(ssbClient.id) || new Set() + for (const lesson of lessons) lesson.completed = myProgress.has(idx.lessonRootOf(lesson.id)) + return lessons.sort((a, b) => + ((a.order ?? Infinity) - (b.order ?? Infinity)) || (new Date(a.createdAt) - new Date(b.createdAt)) + ) + }, + + async hasPassedCourse(courseId, studentId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + const student = normalizeIds(studentId)[0] || ssbClient.id + const lessons = await this.listLessons(course.rootId) + if (!lessons.length) return false + const exams = await this.listExams(course.rootId) + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const progressSet = (idx.progressByCourse.get(course.rootId) || new Map()).get(student) || new Set() + for (const lesson of lessons) { + const lessonRoot = idx.lessonRootOf(lesson.id) + const lessonExams = exams.filter(x => !x.locked && x.questions.length && x.lessonId && x.lessonId === lessonRoot) + if (lessonExams.length) { + for (const x of lessonExams) { + const r = idx.examResultLatest.get(`${x.id}::${student}`) + if (!r || x.passMark == null || Number(r.score) < x.passMark) return false + } + } else if (!progressSet.has(lessonRoot)) { + return false + } + } + return true + }, + + async issueCertificate(courseId, studentId, text = "") { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + + const student = normalizeIds(studentId)[0] + if (!student) throw new Error("Invalid student id") + if (!course.students.includes(student)) throw new Error("Student is not enrolled") + + const lessons = await this.listLessons(course.rootId) + if (lessons.length) { + const exams = await this.listExams(course.rootId) + const messages2 = await readAll(ssbClient) + const idx2 = indexOf(messages2, ssbClient) + const progressSet = (idx2.progressByCourse.get(course.rootId) || new Map()).get(student) || new Set() + for (const lesson of lessons) { + const lessonRoot = idx2.lessonRootOf(lesson.id) + const lessonExams = exams.filter(x => !x.locked && x.lessonId && x.lessonId === lessonRoot) + if (lessonExams.length) { + for (const x of lessonExams) { + const r = idx2.examResultLatest.get(`${x.id}::${student}`) + if (!r || x.passMark == null || Number(r.score) < x.passMark) throw new Error("Student has not passed all lessons") + } + } else if (!progressSet.has(lessonRoot)) { + throw new Error("Student has not completed all lessons") + } + } + } + + const rootId = course.rootId || (await this.resolveRootId(courseId)) + const existing = await this.listCertificates(rootId) + if (existing.some(cert => cert.student === student)) return { alreadyIssued: true } + + const content = { + type: "schoolCertificate", + courseId: rootId, + courseTitle: course.title, + student, + text: String(text || "").trim(), + author: ssbClient.id, + createdAt: new Date().toISOString() + } + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async listCertificates(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const rootId = idx.rootOf(courseId) + const certs = [] + for (const node of idx.certNodes.values()) { + if (idx.tomb.has(node.key)) continue + if (idx.rootOf(node.c.courseId) !== rootId) continue + certs.push({ + id: node.key, + courseId: rootId, + courseTitle: node.c.courseTitle, + student: node.c.student, + text: node.c.text || "", + author: node.author, + createdAt: node.c.createdAt || new Date(node.ts).toISOString() + }) + } + return certs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + }, + + async listCertificatesForStudent(studentId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const certs = [] + for (const node of idx.certNodes.values()) { + if (idx.tomb.has(node.key)) continue + if (node.c.student !== studentId) continue + certs.push({ + id: node.key, + courseId: idx.rootOf(node.c.courseId), + courseTitle: node.c.courseTitle, + student: node.c.student, + text: node.c.text || "", + author: node.author, + createdAt: node.c.createdAt || new Date(node.ts).toISOString() + }) + } + return certs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + }, + + async lessonRootOf(lessonId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + return idx.lessonRootOf(lessonId) || lessonId + }, + + async listHiddenComments(courseId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + const messages = await readAll(ssbClient) + const hidden = new Set() + for (const m of messages) { + const c = m.value?.content + if (!c || c.type !== "schoolCommentHide" || !c.target) continue + if (m.value?.author !== course.author) continue + if (c.courseId !== course.rootId) continue + hidden.add(c.target) + } + return hidden + }, + + async hideComment(courseId, commentId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Only the teacher can hide comments") + const content = { type: "schoolCommentHide", courseId: course.rootId, target: commentId, author: ssbClient.id, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async listCourses(filter = "ALL", viewerId = null, query = {}) { + const ssbClient = await openSsb() + const viewer = viewerId || ssbClient.id + + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const courses = [] + for (const [rootId, tipId] of idx.tipByRoot.entries()) { + if (idx.tomb.has(tipId)) continue + const node = idx.courseNodes.get(tipId) + if (!node) continue + const course = buildCourseObject(node, rootId, idx.enrollByCourse.get(rootId), idx.transferAgg, idx.grantsByCourse.get(rootId), idx.opinionsByCourse.get(rootId), idx.chatByCourse) + if (course.undecryptable) continue + if (!canView(course, viewer)) continue + courses.push(course) + } + + const F = String(filter || "ALL").toUpperCase() + let list = courses + + if (F === "MINE") list = list.filter(c => c.author === viewer) + else if (F === "APPLIED") list = list.filter(c => c.students.includes(viewer) || c.pending.some(p => p.author === viewer)) + else if (F === "OPEN") list = list.filter(c => !(Number(c.price) > 0)) + else if (F === "ONGOING") list = list.filter(c => c.status === "ONGOING") + else if (F === "CLOSED") list = list.filter(c => c.status === "CLOSED") + else if (F === "RECENT") list = list.filter(c => moment(c.createdAt).isAfter(moment().subtract(24, "hours"))) + else if (F === "TOP") list = [...list].sort((a, b) => b.students.length - a.students.length) + + const search = String(query.q || query.search || "").trim() + if (search) list = list.filter(c => matchSearch(c, search)) + + const sort = String(query.sort || "").trim() + const byRecent = () => list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + if (sort === "students") list.sort((a, b) => b.students.length - a.students.length) + else if (sort === "title") list.sort((a, b) => String(a.title).localeCompare(String(b.title))) + else if (F !== "TOP") byRecent() + + return list + }, + + async getCourseById(id, viewerId = null) { + const ssbClient = await openSsb() + const viewer = viewerId || ssbClient.id + + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const tipId = idx.tipOf(id) + const rootId = idx.rootOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(rootId)) throw new Error("Course not found") + + const node = idx.courseNodes.get(tipId) + if (!node) throw new Error("Course not found") + + const course = buildCourseObject(node, rootId, idx.enrollByCourse.get(rootId), idx.transferAgg, idx.grantsByCourse.get(rootId), idx.opinionsByCourse.get(rootId), idx.chatByCourse) + if (course.undecryptable) throw new Error("Course not found") + if (!canView(course, viewer)) throw new Error("Course not found") + return course + } + } +} diff --git a/src/models/search_model.js b/src/models/search_model.js index 650b736..1849c42 100644 --- a/src/models/search_model.js +++ b/src/models/search_model.js @@ -2,6 +2,9 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES } = require('./typed_log'); +const longText = require('../backend/long_text'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { @@ -39,8 +42,8 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { const searchableTypes = [ 'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed', 'votes', 'report', 'task', 'event', 'bookmark', 'document', - 'image', 'audio', 'video', 'torrent', 'market', 'bankWallet', 'bankClaim', - 'project', 'job', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad' + 'image', 'audio', 'video', 'torrent', 'file', 'market', 'bankWallet', 'bankClaim', + 'project', 'job', 'housing', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad', 'room', 'poll', 'schoolCourse', 'wikiPage', 'emergency', 'mailingList', 'logisticsRoute', 'podcast', 'podcastEpisode', 'campaign' ]; const getRelevantFields = (type, content) => { @@ -60,12 +63,13 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { case 'audio': return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])]; case 'image': - return [content?.url, content?.title, content?.description, ...(content?.tags || []), content?.meme]; + return [content?.url, content?.title, content?.description, ...(content?.tags || [])]; case 'video': return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])]; case 'document': return [content?.url, content?.title, content?.description, ...(content?.tags || []), content?.key]; case 'torrent': + case 'file': return [content?.title, content?.description, ...(content?.tags || []), content?.url]; case 'market': return [content?.item_type, content?.title, content?.description, content?.price, ...(content?.tags || []), content?.status, content?.item_status, content?.deadline, content?.includesShipping, content?.seller, content?.image, content?.auctions_poll, content?.stock]; @@ -83,10 +87,22 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return [content?.address]; case 'bankClaim': return [content?.amount, content?.epochId, content?.allocationId, content?.txid]; + case 'schoolCourse': + return [content?.title, content?.description, ...(content?.tags || []), content?.price, content?.status, content?.visibility]; case 'project': return [content?.title, content?.status, content?.progress, content?.goal, content?.pledged, content?.deadline, (content?.followers || []).length, (content?.backers || []).length, (content?.milestones || []).length, content?.bounty, content?.bountyAmount, content?.bounty_currency, content?.activity?.kind, content?.activityActor]; + case 'poll': + return content?.encryptedQuestion + ? [] + : [content?.question, ...(content?.options || []), ...(content?.tags || [])]; + case 'housing': + return [content?.title, content?.housing_type, content?.property_type, content?.place, content?.price, content?.rooms, content?.size, content?.capacity, content?.status]; case 'job': return [content?.title, content?.job_type, ...(content?.tasks || []), content?.location, content?.vacants, content?.salary, content?.status, (content?.subscribers || []).length]; + case 'industry': + return [content?.name, content?.sector, content?.description, content?.membershipPolicy, content?.status, content?.license, ...(content?.tags || [])]; + case 'industryBlueprint': + return [content?.name, content?.description, content?.sector, content?.outItem, content?.outKind, content?.license, ...(content?.skills || []), ...((content?.materials || []).map(m => m && m.item))]; case 'forum': return [content?.root, content?.category, content?.title, content?.text, content?.key]; case 'vote': @@ -105,6 +121,22 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return [content?.title, content?.description, content?.category, ...(content?.tags || []), content?.status, content?.author]; case 'pad': return [content?.title, content?.status, content?.deadline, ...(content?.tags || []), content?.author]; + case 'room': + return content?.encrypted === true ? [] : [content?.title, content?.description, content?.status, ...(content?.tags || []), content?.author]; + case 'wikiPage': + return [content?.title, content?.body, content?.slug, ...(content?.tags || []), ...(content?.aliases || []), content?.author]; + case 'emergency': + return [content?.title, content?.text, content?.category, content?.status, ...(content?.tags || []), content?.author]; + case 'mailingList': + return [content?.title, content?.description, content?.listType, content?.status, ...(content?.tags || []), content?.author]; + case 'logisticsRoute': + return [content?.title, content?.description, content?.origin, content?.destination, content?.kind, content?.mode, ...(content?.tags || []), content?.author]; + case 'podcast': + return [content?.title, content?.description, content?.category, ...(content?.tags || []), content?.author]; + case 'podcastEpisode': + return [content?.title, content?.description, ...(content?.tags || []), content?.author]; + case 'campaign': + return [content?.title, content?.text, content?.category, content?.status, ...(content?.tags || []), content?.author]; case 'gameScore': return [content?.game, content?.player]; default: @@ -134,6 +166,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { if (t === 'audio') return `audio:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`; if (t === 'video') return `video:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`; if (t === 'torrent') return `torrent:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`; + if (t === 'file') return `file:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`; if (t === 'bookmark') return `bookmark:${author}|${c.url || norm(c.description) || msg.key}`; if (t === 'tribe') { @@ -218,6 +251,9 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { ].join('|'); } + if (t === 'schoolCourse') { + return ['schoolCourse', author, norm(c.title), norm(c.createdAt)].join('|'); + } if (t === 'project') { return [ 'project', @@ -228,6 +264,17 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { ].join('|'); } + if (t === 'housing') { + return [ + 'housing', + author, + norm(c.title), + norm(c.place), + norm(c.price), + norm(c.housing_type) + ].join('|'); + } + if (t === 'job') { return [ 'job', @@ -239,6 +286,14 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { ].join('|'); } + if (t === 'industry') { + return ['industry', author, norm(c.name), norm(c.sector)].join('|'); + } + + if (t === 'industryBlueprint') { + return ['industryBlueprint', author, norm(c.facility), norm(c.name)].join('|'); + } + if (t === 'forum') { return `forum:${c.key || c.root || `${author}|${norm(c.title)}` || msg.key}`; } @@ -259,6 +314,30 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return ['pad', author, norm(c.title), norm(c.deadline)].join('|'); } + if (t === 'room') { + return ['room', author, norm(c.rid || c.title), norm(c.createdAt)].join('|'); + } + + if (t === 'wikiPage') { + return ['wikiPage', norm(c.tribeId), norm(c.slug)].join('|'); + } + + if (t === 'emergency') { + return ['emergency', author, norm(c.title), norm(c.createdAt)].join('|'); + } + + if (t === 'mailingList') { + return ['mailingList', author, norm(c.createdAt)].join('|'); + } + + if (t === 'logisticsRoute') { + return ['logisticsRoute', author, norm(c.createdAt)].join('|'); + } + + if (t === 'podcast' || t === 'podcastEpisode' || t === 'campaign') { + return [t, author, norm(c.createdAt)].join('|'); + } + return `${t}:${msg.key}`; }; @@ -274,20 +353,14 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return Array.from(map.values()); }; - const search = async ({ query, types = [], resultsPerPage = "10" }) => { + const search = async ({ query, types = [] }) => { const ssbClient = await openSsb(); const viewerId = ssbClient.id; const queryLower = String(query || '').toLowerCase(); - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); const tombstoned = buildValidatedTombstoneSet(messages); - const replacesMap = new Map(); const latestByKey = new Map(); for (const msg of messages) { @@ -295,23 +368,12 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { const c = msg?.value?.content; const t = c?.type; if (!t || !searchableTypes.includes(t)) continue; - if (tombstoned.has(k)) continue; - if (c.replaces) replacesMap.set(c.replaces, k); latestByKey.set(k, msg); } - for (const [oldId, newId] of Array.from(replacesMap.entries())) { - const orig = latestByKey.get(oldId); - const repl = latestByKey.get(newId); - if (!orig) { replacesMap.delete(oldId); continue; } - if (!repl || String(repl?.value?.author) !== String(orig?.value?.author)) { - replacesMap.delete(oldId); - latestByKey.delete(newId); - } - } - - for (const oldId of replacesMap.keys()) { - latestByKey.delete(oldId); + const vg = versionGroups(Array.from(latestByKey.values()).map(versionEntry)); + for (const k of Array.from(latestByKey.keys())) { + if (!vg.roots.has(vg.rootOf(k)) || vg.tipOf(k) !== k || tombstoned.has(k)) latestByKey.delete(k); } const viewerTribeIds = new Set(); @@ -347,6 +409,14 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { } } + const chunkLookup = longText.lookupIn(longText.indexChunks(messages)); + for (const [k, msg] of Array.from(latestByKey.entries())) { + const c = msg?.value?.content; + if ((c?.type === 'post' || c?.type === 'forum') && longText.hasChunks(c)) { + latestByKey.set(k, { ...msg, value: { ...msg.value, content: longText.resolveField(c, 'text', msg.value.author, chunkLookup) } }); + } + } + if (padsModel) { for (const msg of latestByKey.values()) { const c = msg?.value?.content; @@ -393,11 +463,6 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return acc; }, {}); - if (resultsPerPage !== "all") { - const limit = parseInt(resultsPerPage, 10); - for (const key in grouped) grouped[key] = grouped[key].slice(0, limit); - } - return grouped; }; diff --git a/src/models/shops_model.js b/src/models/shops_model.js index 5dedbd2..54f79ae 100644 --- a/src/models/shops_model.js +++ b/src/models/shops_model.js @@ -3,6 +3,8 @@ const { getConfig } = require("../configs/config-manager.js") const categories = require("../backend/opinion_categories") const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeByPreferring, norm } = require('../backend/dedupe') +const { readTyped, memoIndex } = require('./typed_log') +const FileType = require('../server/node_modules/file-type') const logLimit = getConfig().ssbLogStream?.limit || 1000 const safeArr = (v) => (Array.isArray(v) ? v : []) @@ -18,31 +20,90 @@ module.exports = ({ cooler, tribeCrypto }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ) + const SHOP_TYPES = ["shop", "shopProduct", "shopOpinion", "shopPurchase", "shop-invite", "shop-invite-tombstone", "shop-open-invite", "shop-open-invite-tombstone", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, SHOP_TYPES, { limit: logLimit, withWindow: true, withPrivate: true }) + const mediaKinds = new Map() + const blobMediaKind = async (ssbClient, blobId) => { + if (mediaKinds.has(blobId)) return mediaKinds.get(blobId) + let kind + try { + const head = await new Promise((resolve, reject) => pull(ssbClient.blobs.get(blobId), pull.take(1), pull.collect((err, chunks) => err ? reject(err) : resolve(Buffer.concat(chunks))))) + const ft = await FileType.fromBuffer(head) + kind = ft && ft.mime ? ft.mime.split('/')[0] : 'other' + } catch (_) { return null } + mediaKinds.set(blobId, kind) + return kind + } + const withMediaKind = async (ssbClient, items) => { + for (const p of items) { + if (!p || !p.image || !String(p.image).startsWith('&')) continue + const kind = await blobMediaKind(ssbClient, p.image) + if (kind === 'video') p.image = `[video:](${p.image})` + else if (kind === 'audio') p.image = `[audio:](${p.image})` + } + return items + } + const openSealed = (ssbClient, m) => { + if (typeof m.value?.content === "string") return ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + return m.value?.private === true ? m : null + } const SELLER_STATUSES = ["ACCEPTED", "REJECTED", "PAID", "SHIPPED"] const BUYER_STATUSES = ["RECEIVED"] const ORDER_STATUSES = [...SELLER_STATUSES, ...BUYER_STATUSES] + const ORDER_FLOW = { PENDING: ["ACCEPTED", "REJECTED"], ACCEPTED: ["PAID", "REJECTED"], PAID: ["SHIPPED"], SHIPPED: ["RECEIVED"] } const buildOrderStatusMap = (ssbClient, messages) => { const map = new Map() for (const m of messages) { - if (typeof m.value?.content !== "string") continue try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const dec = openSealed(ssbClient, m) const dc = dec?.value?.content if (!dc || dc.type !== "shop-purchase-status" || !dc.orderId) continue - const ts = dec.value.timestamp || m.timestamp || 0 - const prev = map.get(dc.orderId) - if (!prev || ts >= prev.ts) map.set(dc.orderId, { status: String(dc.status || "").toUpperCase(), ts }) + if (!map.has(dc.orderId)) map.set(dc.orderId, []) + map.get(dc.orderId).push({ status: String(dc.status || "").toUpperCase(), author: dec.value.author, ts: dec.value.timestamp || m.timestamp || 0, price: dc.price !== undefined ? String(dc.price) : undefined, title: dc.title !== undefined ? String(dc.title) : undefined }) } catch (_) {} } return map } + const orderStatusOf = (updates, buyer, seller) => { + const signed = (updates || []).filter(u => (SELLER_STATUSES.includes(u.status) && u.author === seller) || (BUYER_STATUSES.includes(u.status) && u.author === buyer)).sort((a, b) => a.ts - b.ts) + let status = "PENDING", g = 0 + while (g++ < 100) { + const next = signed.find(u => (ORDER_FLOW[status] || []).includes(u.status)) + if (!next) break + status = next.status + } + return status + } + + const acceptedTermsOf = (updates, seller) => { + const accepted = (updates || []).filter(u => u.status === "ACCEPTED" && u.author === seller && u.price !== undefined).sort((a, b) => a.ts - b.ts)[0] + return accepted ? { price: accepted.price, ...(accepted.title ? { title: accepted.title } : {}) } : null + } + const withOrderStatus = (statusMap) => (o) => { + const updates = statusMap.get(o.id) + return { ...o, ...(acceptedTermsOf(updates, o.seller) || {}), status: orderStatusOf(updates, o.buyer, o.seller) } + } + + const resolveOrder = (idx, dc) => { + const productRoot = idx.rootOf(String(dc.productId || "")) + const productNode = idx.nodes.get(productRoot) + if (!productNode || productNode.c.type !== "shopProduct") return null + const shopRoot = idx.rootOf(productShopId(productNode.c)) + const shopNode = idx.nodes.get(shopRoot) + if (!shopNode || shopNode.c.type !== "shop" || shopNode.author !== productNode.author) return null + let ref = productRoot + let g = 0 + while (idx.strictChild.has(ref) && g++ < 100000) ref = idx.strictChild.get(ref) + const raw = idx.nodes.get(ref).c + const c = isEncrypted(raw) ? decryptShopContent(raw, shopRoot) : raw + const readable = !c._undecryptable + return { productId: productRoot, shopId: shopRoot, seller: shopNode.author, title: readable ? String(c.title || "") : "", price: readable ? (c.price || "") : "" } + } + const decryptBuyers = (val, key) => { if (Array.isArray(val)) return val if (typeof val === 'string' && tribeCrypto && key) { @@ -77,7 +138,7 @@ module.exports = ({ cooler, tribeCrypto }) => { } const buildIndex = (messages) => { - const tomb = new Set() + const tomb = buildValidatedTombstoneSet(messages) const nodes = new Map() const parent = new Map() const child = new Map() @@ -90,7 +151,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const v = m.value || {} const c = v.content if (!c) continue - if (c.type === "tombstone" && c.target) { tomb.add(c.target); continue } + if (c.type === "tombstone") continue if (c.type === "shopOpinion" && c.target) { opinionMsgs.push({ target: c.target, author: v.author, category: c.category }); continue } if (c.type === "shopPurchase" && c.target) { purchaseMsgs.push({ target: c.target, author: v.author }); continue } if (c.type === "shop" || c.type === "shopProduct") { @@ -162,7 +223,7 @@ module.exports = ({ cooler, tribeCrypto }) => { tags: safeArr(c.tags), visibility: c.visibility || (encrypted ? "CLOSED" : "OPEN"), clearnetPublic: !!c.clearnetPublic, - author: raw.author || c.author || node.author, + author: node.author, createdAt: c.createdAt || raw.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || raw.updatedAt || null, opinions: agg.opinions, @@ -182,7 +243,6 @@ module.exports = ({ cooler, tribeCrypto }) => { const undecryptable = !!c._undecryptable const agg = idx ? idx.aggregateFor(rootId, c) : { opinions: c.opinions || {}, opinions_inhabitants: safeArr(c.opinions_inhabitants) } const sellerStock = Number(c.stock) || 0 - const purchaseCount = idx ? idx.purchaseCountFor(rootId) : 0 const sellerBuyers = encrypted ? safeArr(c.buyers) : decryptBuyers(c.buyers, tribeCrypto ? tribeCrypto.getKey(rootId) : null) const buyers = Array.from(new Set(sellerBuyers.concat(idx ? idx.purchaseAuthorsFor(rootId) : []))) return { @@ -193,9 +253,9 @@ module.exports = ({ cooler, tribeCrypto }) => { description: c.description || "", image: c.image || null, price: c.price || "0.000000", - stock: Math.max(0, sellerStock - purchaseCount), + stock: Math.max(0, sellerStock), featured: !!c.featured, - author: raw.author || c.author || node.author, + author: node.author, createdAt: c.createdAt || raw.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || raw.updatedAt || null, opinions: agg.opinions, @@ -346,7 +406,7 @@ module.exports = ({ cooler, tribeCrypto }) => { async getShopById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = memoIndex('shops', messages, buildIndex) const tip = idx.strictTipOf(id) if (idx.tomb.has(tip)) return null @@ -521,7 +581,7 @@ module.exports = ({ cooler, tribeCrypto }) => { async getProductById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = memoIndex('shops', messages, buildIndex) const tip = idx.strictTipOf(id) if (idx.tomb.has(tip)) return null @@ -531,7 +591,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const root = idx.rootOf(tip) - return buildProduct(node, root, idx) + return (await withMediaKind(ssbClient, [buildProduct(node, root, idx)]))[0] }, async listProducts(shopRootId) { @@ -549,7 +609,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (prod && !prod.undecryptable) items.push(prod) } - return items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return withMediaKind(ssbClient, items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))) }, async listFeaturedProducts(shopRootId) { @@ -568,7 +628,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (prod && !prod.undecryptable) items.push(prod) } - return items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).slice(0, 4) + return withMediaKind(ssbClient, items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).slice(0, 4)) }, async listAllProducts({ filter = "all", sort = "recent" } = {}) { @@ -585,8 +645,10 @@ module.exports = ({ cooler, tribeCrypto }) => { if (prod && !prod.undecryptable) items.push(prod) } - if (filter === "top") return items.slice().sort((a, b) => voteSum(b.opinions) - voteSum(a.opinions) || new Date(b.createdAt) - new Date(a.createdAt)) - return items.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + const sorted = filter === "top" + ? items.slice().sort((a, b) => voteSum(b.opinions) - voteSum(a.opinions) || new Date(b.createdAt) - new Date(a.createdAt)) + : items.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return withMediaKind(ssbClient, sorted) }, async buyProduct(productId) { @@ -608,8 +670,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const shopRoot = productShopId(raw) const c = encrypted ? decryptShopContent(raw, shopRoot) : raw if (c._undecryptable) throw new Error("Cannot access product") - const stock = Math.max(0, (Number(c.stock) || 0) - idx.purchaseCountFor(rootId)) - if (stock <= 0) throw new Error("Out of stock") + if (!((Number(c.stock) || 0) > 0)) throw new Error("Out of stock") const content = { type: "shopPurchase", target: rootId, createdAt: new Date().toISOString() } return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) @@ -626,11 +687,14 @@ module.exports = ({ cooler, tribeCrypto }) => { const rootId = idx.rootOf(tipId) const node = idx.nodes.get(tipId) - if (!node) throw new Error("Product not found") + if (!node || node.c.type !== "shopProduct") throw new Error("Product not found") const raw = node.c - const shopOwner = raw.author - if (shopOwner === userId) throw new Error("Cannot buy your own product") const shopRoot = productShopId(raw) + const shopNode = idx.nodes.get(idx.rootOf(shopRoot)) + const productRootNode = idx.nodes.get(rootId) + if (!shopNode || shopNode.c.type !== "shop" || !productRootNode || productRootNode.author !== shopNode.author || node.author !== shopNode.author) throw new Error("Product not found") + const shopOwner = shopNode.author + if (shopOwner === userId) throw new Error("Cannot buy your own product") const c = isEncrypted(raw) ? decryptShopContent(raw, shopRoot) : raw const content = { @@ -655,31 +719,24 @@ module.exports = ({ cooler, tribeCrypto }) => { const ssbClient = await openSsb() const me = ssbClient.id const messages = await readAll(ssbClient) + const idx = buildIndex(messages) const out = [] for (const m of messages) { - if (typeof m.value?.content !== "string") continue try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const dec = openSealed(ssbClient, m) if (!dec?.value?.content) continue const dc = dec.value.content if (dc.type !== "shop-purchase") continue if (dec.value.author !== me) continue - out.push({ id: m.key, ...dc, buyer: dec.value.author, ts: dec.value.timestamp || m.timestamp || 0 }) + const ts = dec.value.timestamp || m.timestamp || 0 + const resolved = resolveOrder(idx, dc) + if (!resolved) continue + out.push({ id: m.key, ...dc, ...resolved, buyer: dec.value.author, ts }) } catch (_) {} } const statusMap = buildOrderStatusMap(ssbClient, messages) - const sellerCache = new Map() - for (const o of out) { - if (!o.seller && o.shopId) { - if (!sellerCache.has(o.shopId)) { - const shop = await this.getShopById(o.shopId).catch(() => null) - sellerCache.set(o.shopId, (shop && shop.author) || null) - } - o.seller = sellerCache.get(o.shopId) || null - } - } return out - .map(o => ({ ...o, status: (statusMap.get(o.id) || {}).status || "PENDING" })) + .map(withOrderStatus(statusMap)) .sort((a, b) => b.ts - a.ts) }, @@ -689,23 +746,26 @@ module.exports = ({ cooler, tribeCrypto }) => { const shop = await this.getShopById(shopRootId).catch(() => null) if (!shop) throw new Error("Shop not found") if (shop.author !== me) throw new Error("Not the shop owner") + const rootId = shop.rootId || shopRootId const messages = await readAll(ssbClient) + const idx = buildIndex(messages) const out = [] for (const m of messages) { - if (typeof m.value?.content !== "string") continue try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const dec = openSealed(ssbClient, m) if (!dec?.value?.content) continue const dc = dec.value.content if (dc.type !== "shop-purchase") continue - if (dc.shopId !== shopRootId) continue - out.push({ id: m.key, ...dc, buyer: dec.value.author, ts: dec.value.timestamp || m.timestamp || 0 }) + const ts = dec.value.timestamp || m.timestamp || 0 + const resolved = resolveOrder(idx, dc) + if (!resolved || resolved.shopId !== rootId || resolved.seller !== me) continue + out.push({ id: m.key, ...dc, ...resolved, buyer: dec.value.author, ts }) } catch (_) {} } const statusMap = buildOrderStatusMap(ssbClient, messages) return out - .map(o => ({ ...o, status: (statusMap.get(o.id) || {}).status || "PENDING" })) + .map(withOrderStatus(statusMap)) .sort((a, b) => b.ts - a.ts) }, @@ -715,27 +775,38 @@ module.exports = ({ cooler, tribeCrypto }) => { const st = String(status || "").toUpperCase() if (!ORDER_STATUSES.includes(st)) throw new Error("Invalid status") const messages = await readAll(ssbClient) + const idx = buildIndex(messages) let order = null for (const m of messages) { if (m.key !== orderId) continue - if (typeof m.value?.content !== "string") break try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const dec = openSealed(ssbClient, m) const dc = dec?.value?.content - if (dc && dc.type === "shop-purchase") order = { ...dc, buyer: dec.value.author } + const resolved = dc && dc.type === "shop-purchase" ? resolveOrder(idx, dc) : null + if (resolved) order = { ...dc, ...resolved, buyer: dec.value.author } } catch (_) {} break } if (!order) throw new Error("Order not found") - const seller = order.seller || (await this.getShopById(order.shopId).catch(() => null) || {}).author || null + const seller = order.seller if (BUYER_STATUSES.includes(st)) { if (me !== order.buyer) throw new Error("Only the buyer can set this status") } else { if (me !== seller) throw new Error("Only the seller can set this status") } + const current = orderStatusOf(buildOrderStatusMap(ssbClient, messages).get(orderId), order.buyer, seller) + if (!(ORDER_FLOW[current] || []).includes(st)) throw new Error("Invalid status transition") const recps = Array.from(new Set([order.buyer, seller].filter(Boolean))) - const content = { type: "shop-purchase-status", orderId, shopId: order.shopId, status: st, updatedAt: new Date().toISOString() } - return new Promise((res, rej) => ssbClient.private.publish(content, recps, (e, m) => e ? rej(e) : res(m))) + const content = { type: "shop-purchase-status", orderId, shopId: order.shopId, status: st, ...(st === "ACCEPTED" ? { price: String(order.price || ""), title: String(order.title || "") } : {}), updatedAt: new Date().toISOString() } + const published = await new Promise((res, rej) => ssbClient.private.publish(content, recps, (e, m) => e ? rej(e) : res(m))) + const delta = st === "ACCEPTED" ? -1 : (st === "REJECTED" && current === "ACCEPTED" ? 1 : 0) + if (delta) { + try { + const product = await this.getProductById(order.productId) + await this.updateProductById(order.productId, { stock: String(Math.max(0, (Number(product.stock) || 0) + delta)) }) + } catch (_) {} + } + return published }, async countPendingOrders(shopRootId) { diff --git a/src/models/stats_model.js b/src/models/stats_model.js index bc14f72..fed5fa6 100644 --- a/src/models/stats_model.js +++ b/src/models/stats_model.js @@ -1,16 +1,23 @@ const pull = require('../server/node_modules/pull-stream'); -const os = require('os'); const fs = require('fs'); const path = require('path'); +const ssbDirOf = () => require('../server/ssb_config').path; const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES } = require('./typed_log'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); +const opinionCategories = require('../backend/opinion_categories'); +const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive); +const MODERATION_OPINIONS = new Set(opinionCategories.karma.moderation); +const NEUTRAL_OPINIONS = new Set(opinionCategories.karma.neutral); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const DISK_USAGE_TTL_MS = 10 * 60 * 1000; -const STORAGE_DIR = path.join(__dirname, "..", "configs"); -const ADDR_FILE = path.join(STORAGE_DIR, "wallet_addresses.json"); +const addrFile = () => process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, "wallet-addresses.json") : require('../configs/state-manager').statePath("wallet-addresses.json"); function readAddrMap() { try { + const ADDR_FILE = addrFile(); if (!fs.existsSync(ADDR_FILE)) return {}; const raw = fs.readFileSync(ADDR_FILE, 'utf8'); const obj = JSON.parse(raw || '{}'); @@ -22,7 +29,7 @@ function readAddrMap() { const listPubsFromEbt = () => { try { - const ebtDir = path.join(os.homedir(), '.ssb', 'ebt'); + const ebtDir = path.join(ssbDirOf(), 'ebt'); const files = fs.readdirSync(ebtDir); return files.filter(f => f.endsWith('.ed25519')); } catch { @@ -58,26 +65,52 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { }; const types = [ - 'bookmark','event','task','votes','report','feed','project', - 'image','torrent','audio','video','document','transfer','post','tribe', + 'bookmark','event','task','votes','report','feed','project','industry','industryBlueprint', + 'image','torrent','file','audio','video','document','transfer','post','tribe', 'market','forum','job','aiExchange','map','shop','shopProduct','chat','chatMessage', - 'pad','padEntry','gameScore','calendar','calendarDate','calendarNote','log', + 'pad','padEntry','room','roomMember','wikiPage','emergency','emergencyConfirm','emergencyUpdate','mailingList','logisticsRoute','logisticsRating','podcast','podcastEpisode','podcastPlay','campaign','campaignSignature','campaignUpdate','gameScore','calendar','calendarDate','calendarNote','log', + 'schoolCourse','schoolLesson','schoolEnroll','schoolCertificate', 'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw', 'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote' ]; - const getFolderSize = (folderPath) => { - let files; - try { files = fs.readdirSync(folderPath); } catch (_) { return 0; } - let totalSize = 0; - for (const file of files) { - const filePath = `${folderPath}/${file}`; + const walkBytes = async (dir) => { + let names; + try { names = await fs.promises.readdir(dir); } catch (_) { return 0; } + let total = 0; + for (const name of names) { + const filePath = path.join(dir, name); try { - const st = fs.statSync(filePath); - totalSize += st.isDirectory() ? getFolderSize(filePath) : st.size; + const st = await fs.promises.stat(filePath); + total += st.isDirectory() ? await walkBytes(filePath) : st.size; } catch (_) {} } - return totalSize; + return total; + }; + const diskUsage = new Map(); + const ssbDiskUsage = (root) => { + const hit = diskUsage.get(root); + if (hit && (hit.pending || Date.now() - hit.at < DISK_USAGE_TTL_MS)) return hit.pending || Promise.resolve(hit.value); + const pending = (async () => { + let names = []; + try { names = await fs.promises.readdir(root); } catch (_) {} + const usage = { total: 0, log: 0, blobs: 0 }; + for (const name of names) { + const filePath = path.join(root, name); + let bytes = 0; + try { + const st = await fs.promises.stat(filePath); + bytes = st.isDirectory() ? await walkBytes(filePath) : st.size; + } catch (_) {} + usage.total += bytes; + if (name === 'db2') usage.log = bytes; + if (name === 'blobs') usage.blobs = bytes; + } + return usage; + })(); + diskUsage.set(root, { pending, at: 0, value: null }); + pending.then((value) => diskUsage.set(root, { pending: null, at: Date.now(), value }), () => diskUsage.delete(root)); + return pending; }; const formatSize = (sizeInBytes) => { @@ -194,8 +227,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { let s = 0; for (const k in cats) { if (!Object.prototype.hasOwnProperty.call(cats, k)) continue; + if (NEUTRAL_OPINIONS.has(k)) continue; if (k === 'interesting' || k === 'inspiring') s += 5; - else if (k === 'boring' || k === 'spam' || k === 'propaganda') s -= 3; + else if (MODERATION_OPINIONS.has(k)) s -= 5; + else if (CONSTRUCTIVE_OPINIONS.has(k)) s -= 3; else s += 1; } return s; @@ -274,13 +309,56 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { else if (t === 'larphousepost') score += 6; else if (t === 'larptestattempt') score += 3; else if (t === 'torrent') score += 6; + else if (t === 'file') score += 6; else if (t === 'shop' || t === 'shopproduct') score += 6; + else if (t === 'schoolcourse' || rawType === 'schoolcourse') score += 10; + else if (t === 'schoollesson' || rawType === 'schoollesson') score += 6; + else if (t === 'schoolenroll' || rawType === 'schoolenroll') score += 3; + else if (t === 'schoolcertificate' || rawType === 'schoolcertificate') score += 12; + else if (t === 'schoolexam' || rawType === 'schoolexam') score += 6; + else if (t === 'schoolprogress' || rawType === 'schoolprogress') score += 1; + else if (t === 'schoolopinion' || rawType === 'schoolopinion') score += 2; else if (t === 'shop-purchase') score += 2; else if (t === 'pad' || t === 'padentry') score += 3; + else if (t === 'room') score += 3; + else if (t === 'roommember') score += 1; + else if (t === 'wikipage') score += 3; + else if (t === 'emergency') score += 4; + else if (t === 'emergencyconfirm' || t === 'emergencyupdate') score += 1; + else if (t === 'mailinglist') score += 3; + else if (t === 'logisticsroute') score += 3; + else if (t === 'logisticsrating') score += 1; + else if (t === 'podcast') score += 4; + else if (t === 'podcastepisode') score += 3; + else if (t === 'podcastopinion' || t === 'podcastplay') score += 1; + else if (t === 'campaign') score += 4; + else if (t === 'campaignsignature') score += 1; + else if (t === 'campaignupdate') score += 2; else if (t === 'calendar' || t === 'calendarnote' || t === 'calendardate') score += 3; else if (t === 'chat') score += 1; else if (t === 'gamescore') score += 2; else if (t === 'pixelia') score += 2; + else if (rawType === 'industry') score += 8; + else if (rawType === 'industryblueprint') score += 6; + else if (rawType === 'industrybuild') score += 8; + else if (rawType === 'industrycontribution') score += 4; + else if (rawType === 'industryallocation') score += 10; + else if (rawType === 'industryvote') score += 2; + else if (rawType === 'industrymember') score += 2; + else if (rawType === 'industryopinion') score += 1; + else if (rawType === 'wikipage') score += c.replaces ? 2 : 6; + else if (rawType === 'emergency') score += c.replaces ? 1 : 5; + else if (rawType === 'emergencyconfirm') score += 1; + else if (rawType === 'emergencyupdate') score += 2; + else if (rawType === 'mailinglist') score += c.replaces ? 1 : 3; + else if (rawType === 'logisticsroute') score += c.replaces ? 1 : 4; + else if (rawType === 'logisticsrating' || rawType === 'logisticsopinion') score += 1; + else if (rawType === 'podcast') score += c.replaces ? 1 : 5; + else if (rawType === 'podcastepisode') score += c.replaces ? 1 : 4; + else if (rawType === 'podcastopinion' || rawType === 'podcastplay') score += 1; + else if (rawType === 'campaign') score += c.replaces ? 1 : 5; + else if (rawType === 'campaignsignature' || rawType === 'campaignopinion') score += 1; + else if (rawType === 'campaignupdate') score += 2; } return Math.max(0, Math.round(score)); }; @@ -289,12 +367,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const ssbClient = await openSsb(); const userId = ssbClient.id; - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const messages = (await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true })).reverse(); const allMsgs = messages.filter(m => { const c = m.value && m.value.content; @@ -308,15 +381,15 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const scopedMsgs = filter === 'MINE' ? allMsgs.filter(m => m.value.author === userId) : allMsgs; const byType = {}; - const parentOf = {}; + const versionsOf = {}; for (const t of types) { byType[t] = new Map(); - parentOf[t] = new Map(); + versionsOf[t] = []; } const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); - for (const m of scopedMsgs) { + for (const m of allMsgs) { const k = m.key; let c = m.value.content; if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { @@ -336,27 +409,20 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { let theType = c.type; if (!types.includes(theType)) continue; byType[theType].set(k, { key: k, ts: m.value.timestamp, content: c, author: m.value.author }); - if (c.replaces) parentOf[theType].set(k, c.replaces); + versionsOf[theType].push(versionEntry({ key: k, value: { ...m.value, content: c } })); } - const findRoot = (t, id) => { - let cur = id; - const pMap = parentOf[t]; - while (pMap.has(cur)) cur = pMap.get(cur); - return cur; - }; + const vgOf = {}; + for (const t of types) vgOf[t] = versionGroups(versionsOf[t]); + const findRoot = (t, id) => vgOf[t].rootOf(id); const tipOf = {}; for (const t of types) { tipOf[t] = new Map(); - const pMap = parentOf[t]; - const fwd = new Map(); - for (const [child, parent] of pMap.entries()) fwd.set(parent, child); const allMap = byType[t]; - const roots = new Set(Array.from(allMap.keys()).map(id => findRoot(t, id))); - for (const root of roots) { - let tip = root; - while (fwd.has(tip)) tip = fwd.get(tip); + for (const root of vgOf[t].roots) { + if (filter === 'MINE' && allMap.get(root).author !== userId) continue; + const tip = vgOf[t].tipOf(root); if (tombTargets.has(tip)) continue; const node = allMap.get(tip) || allMap.get(root); if (node) tipOf[t].set(root, node); @@ -387,6 +453,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { .filter(n => n.content?.isAnonymous !== false && Array.isArray(n.content?.members) && n.content.members.includes(userId)) .map(n => ({ id: findRoot('tribe', n.key), name: n.content.name || n.content.title || n.key })); + const opinionTargets = new Set(); + for (const m of allMsgs) { + const c = m && m.value && m.value.content; + if (c && typeof c.type === 'string' && c.type.endsWith('Opinion') && typeof c.target === 'string') opinionTargets.add(c.target); + } const content = {}; const opinions = {}; for (const t of types) { @@ -398,7 +469,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { if (t === 'forum') vals = vals.filter(c => !(c.root && tombTargets.has(c.root))); } content[t] = vals.length || 0; - opinions[t] = vals.filter(e => Array.isArray(e.opinions_inhabitants) && e.opinions_inhabitants.length > 0).length || 0; + const embedded = vals.filter(e => Array.isArray(e.opinions_inhabitants) && e.opinions_inhabitants.length > 0).length || 0; + const external = t === 'tribe' ? 0 : Array.from(tipOf[t].keys()).filter(root => vgOf[t].groups.get(root).some(id => opinionTargets.has(id))).length; + opinions[t] = Math.max(embedded, external); } if (filter === 'MINE') { @@ -420,12 +493,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const inhabitants = new Set(allMsgs.map(m => m.value.author)).size; - const secretStat = fs.statSync(`${os.homedir()}/.ssb/secret`); + const secretStat = fs.statSync(path.join(ssbDirOf(), 'secret')); const createdAt = secretStat.birthtime.toLocaleString(); - const folderSize = getFolderSize(`${os.homedir()}/.ssb`); - const flumeSize = getFolderSize(`${os.homedir()}/.ssb/flume`); - const blobsSize = getFolderSize(`${os.homedir()}/.ssb/blobs`); + const { total: folderSize, log: logSize, blobs: blobsSize } = await ssbDiskUsage(ssbDirOf()); const allTs = scopedMsgs.map(m => m.value.timestamp || 0).filter(Boolean); const lastTs = allTs.length ? Math.max(...allTs) : 0; @@ -564,7 +635,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { userTombstoneCount: scopedMsgs.filter(m => m.value.content.type === 'tombstone' && m.value.author === userId).length, networkTombstoneCount: validatedTombstoneCount, folderSize: formatSize(folderSize), - statsBlockchainSize: formatSize(flumeSize), + statsBlockchainSize: formatSize(logSize), statsBlobsSize: formatSize(blobsSize), pubsCount, activity: { diff --git a/src/models/subscriptions_model.js b/src/models/subscriptions_model.js new file mode 100644 index 0000000..bc94f6a --- /dev/null +++ b/src/models/subscriptions_model.js @@ -0,0 +1,104 @@ +const { getConfig } = require('../configs/config-manager.js'); +const { readTyped } = require('./typed_log'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const latestByTargetAuthor = (messages) => { + const latest = new Map(); + for (const m of messages) { + const c = m?.value?.content; + if (!c || c.type !== 'subscription' || typeof c.target !== 'string') continue; + const author = m.value.author; + const ts = m.value.timestamp || 0; + const k = `${c.target}::${author}`; + const prev = latest.get(k); + if (!prev || ts >= prev.ts) latest.set(k, { ts, target: c.target, scope: String(c.scope || ''), author, on: c.on !== false }); + } + return latest; + }; + + return { + async setSubscription(target, scope, on) { + const ssbClient = await openSsb(); + if (typeof target !== 'string' || !target.trim()) throw new Error('Invalid target'); + const content = { + type: 'subscription', + target: target.trim(), + scope: String(scope || '').trim().slice(0, 32), + on: on !== false, + updatedAt: new Date().toISOString() + }; + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => (e ? rej(e) : res(m)))); + }, + + async myState(target) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const mine = latest.get(`${target}::${ssbClient.id}`); + if (!mine) return null; + return mine.on ? 'on' : 'off'; + }, + + async isSubscribed(target) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const mine = latest.get(`${target}::${ssbClient.id}`); + return !!(mine && mine.on); + }, + + async listSubscribers(target) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const out = []; + for (const entry of latest.values()) { + if (entry.target === target && entry.on) out.push(entry.author); + } + return Array.from(new Set(out)); + }, + + async mySubscriptions() { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const out = []; + for (const entry of latest.values()) { + if (entry.author === ssbClient.id && entry.on) out.push({ target: entry.target, scope: entry.scope }); + } + return out; + }, + + async subscribersMap(targets) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const out = new Map(); + for (const t of targets || []) out.set(t, []); + for (const entry of latest.values()) { + if (!out.has(entry.target) || !entry.on) continue; + out.get(entry.target).push(entry.author); + } + return out; + }, + + async subscriberCounts(targets) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const counts = new Map(); + for (const t of targets || []) counts.set(t, 0); + const mine = new Set(); + for (const entry of latest.values()) { + if (!counts.has(entry.target) || !entry.on) continue; + counts.set(entry.target, counts.get(entry.target) + 1); + if (entry.author === ssbClient.id) mine.add(entry.target); + } + return { counts, mine }; + } + }; +}; diff --git a/src/models/tags_model.js b/src/models/tags_model.js index f074453..f88a6ae 100644 --- a/src/models/tags_model.js +++ b/src/models/tags_model.js @@ -1,6 +1,8 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES } = require('./typed_log'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, padsModel, tribesModel }) => { @@ -13,7 +15,10 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { const norm = (v) => String(v == null ? '' : v).trim().toLowerCase(); const normalizeTag = (tag) => String(tag == null ? '' : tag).trim().replace(/^#/, ''); - const tagKey = (tag) => normalizeTag(tag).toLowerCase(); + const tagKey = (tag) => { + const k = normalizeTag(tag).toLowerCase(); + return k.includes(':') ? '' : k; + }; const getDedupeKey = (msg) => { const c = msg?.value?.content || {}; @@ -78,6 +83,10 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { return ['job', author, norm(c.title), norm(c.location), norm(c.salary), norm(c.job_type)].join('|'); } + if (t === 'industry') { + return ['industry', author, norm(c.name), norm(c.sector)].join('|'); + } + if (t === 'forum') { return `forum:${c.key || c.root || `${author}|${norm(c.title)}` || msg.key}`; } @@ -98,19 +107,15 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { }; return { - async listTags(filter = 'all') { + async listTags(filter = 'all', search = '', opts = {}) { const ssbClient = await openSsb(); + const hiddenAuthors = opts.hiddenAuthors instanceof Set ? opts.hiddenAuthors : null; + const hiddenIds = opts.hiddenIds instanceof Set ? opts.hiddenIds : null; - const messages = await new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ); - }); + const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); const tombstoned = buildValidatedTombstoneSet(messages); - const replacesMap = new Map(); const latestByKey = new Map(); for (const msg of messages) { @@ -118,24 +123,15 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { const c = msg?.value?.content; const t = c?.type; if (!k || !c || !t) continue; - if (tombstoned.has(k)) continue; if (t === 'tombstone') continue; - if (c.replaces) replacesMap.set(c.replaces, k); latestByKey.set(k, msg); } - for (const [oldId, newId] of Array.from(replacesMap.entries())) { - const oldMsg = latestByKey.get(oldId); - if (!oldMsg) { replacesMap.delete(oldId); continue; } - const newMsg = latestByKey.get(newId); - if (!newMsg || String(newMsg?.value?.author) !== String(oldMsg?.value?.author)) { - replacesMap.delete(oldId); - latestByKey.delete(newId); - } + const vg = versionGroups(Array.from(latestByKey.values()).map(versionEntry)); + for (const k of Array.from(latestByKey.keys())) { + if (!vg.roots.has(vg.rootOf(k)) || vg.tipOf(k) !== k || tombstoned.has(k)) latestByKey.delete(k); } - for (const oldId of replacesMap.keys()) latestByKey.delete(oldId); - const viewerId = ssbClient.id; const viewerVisibleTribeIds = new Set(); if (tribesModel) { @@ -146,6 +142,8 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { let filtered = Array.from(latestByKey.values()).filter(msg => { const c = msg?.value?.content; if (!c || c.type === 'tombstone') return false; + if (hiddenAuthors && hiddenAuthors.has(String(msg?.value?.author || ''))) return false; + if (hiddenIds && hiddenIds.has(msg.key)) return false; if (tombstoned.has(msg.key)) return false; if (c.encryptedPayload) return false; if (!Array.isArray(c.tags) || !c.tags.filter(Boolean).length) return false; @@ -154,21 +152,35 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { if (c.type === 'task' && String(c.isPublic).toUpperCase() === 'PRIVATE') return false; if ((c.type === 'chat' || c.type === 'pad' || c.type === 'map' || c.type === 'calendar') && c.status === 'INVITE-ONLY' && c.author !== viewerId && !(Array.isArray(c.members) && c.members.includes(viewerId))) return false; if (c.type === 'shop' && c.visibility === 'CLOSED') return false; + if (c.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.author !== viewerId) return false; + if (c.type === 'poll' && c.chatId) return false; return true; }); filtered = dedupeKeepLatest(filtered); const counts = new Map(); + const DAY = 24 * 60 * 60 * 1000; + + const bump = (k, display, { mine, ts }) => { + const prev = counts.get(k); + if (!prev) counts.set(k, { name: display, count: 1, mine: mine ? 1 : 0, lastTs: ts || 0 }); + else counts.set(k, { + name: prev.name || display, + count: prev.count + 1, + mine: prev.mine + (mine ? 1 : 0), + lastTs: Math.max(prev.lastTs || 0, ts || 0) + }); + }; for (const record of filtered) { const tagsArr = record?.value?.content?.tags || []; const uniqueTags = new Set(tagsArr.map(tagKey).filter(Boolean)); + const mine = String(record?.value?.author || '') === String(viewerId); + const ts = Number(record?.value?.timestamp || record?.timestamp || 0); for (const k of uniqueTags) { const display = normalizeTag(tagsArr.find(t => tagKey(t) === k) || k) || k; - const prev = counts.get(k); - if (!prev) counts.set(k, { name: display, count: 1 }); - else counts.set(k, { name: prev.name || display, count: prev.count + 1 }); + bump(k, display, { mine, ts }); } } @@ -177,21 +189,29 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { const viewerId = ssbClient2.id; const pads = await padsModel.listAll({ filter: 'all', viewerId }).catch(() => []); for (const pad of pads) { + if (hiddenAuthors && hiddenAuthors.has(String(pad.author || ''))) continue; + if (hiddenIds && (hiddenIds.has(pad.id) || hiddenIds.has(pad.key) || hiddenIds.has(pad.rootId))) continue; if (pad.status === 'INVITE-ONLY' && pad.author !== viewerId && !(Array.isArray(pad.members) && pad.members.includes(viewerId))) continue; if (!Array.isArray(pad.tags)) continue; const uniquePadTags = new Set(pad.tags.map(tagKey).filter(Boolean)); for (const k of uniquePadTags) { const display = normalizeTag(pad.tags.find(t => tagKey(t) === k) || k) || k; - const prev = counts.get(k); - if (!prev) counts.set(k, { name: display, count: 1 }); - else counts.set(k, { name: prev.name || display, count: prev.count + 1 }); + bump(k, display, { mine: String(pad.author || '') === String(viewerId), ts: Number(pad.createdAt ? new Date(pad.createdAt).getTime() : 0) }); } } } let tags = Array.from(counts.values()); - if (filter === 'top') { + const q = String(search || '').trim().toLowerCase().replace(/^#/, ''); + if (q) tags = tags.filter(t => String(t.name || '').toLowerCase().includes(q)); + + if (filter === 'mine') { + tags = tags.filter(t => (t.mine || 0) > 0).sort((a, b) => b.mine - a.mine || a.name.localeCompare(b.name)); + } else if (filter === 'recent') { + const since = Date.now() - DAY; + tags = tags.filter(t => (t.lastTs || 0) >= since).sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0)); + } else if (filter === 'top') { tags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); } else if (filter === 'cloud') { const max = Math.max(...tags.map(t => t.count), 1); diff --git a/src/models/tasks_model.js b/src/models/tasks_model.js index a750a3b..c810f37 100644 --- a/src/models/tasks_model.js +++ b/src/models/tasks_model.js @@ -1,8 +1,10 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); +const { normalizeImages, normalizeVideo } = require('./media_gallery'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, pmModel }) => { @@ -22,13 +24,9 @@ module.exports = ({ cooler, pmModel }) => { return fallback; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const TASK_TYPES = ['task', 'taskOpinion', 'taskAssign', 'taskReminderSent', 'tombstone']; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, TASK_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -38,9 +36,6 @@ module.exports = ({ cooler, pmModel }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; const assignMsgs = []; @@ -56,34 +51,12 @@ module.exports = ({ cooler, pmModel }) => { if (c.type !== 'task') continue; const ts = v.timestamp || m.timestamp || 0; - nodes.set(k, { key: k, ts, c, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, author: v.author }); } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === 'string' ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } @@ -116,9 +89,9 @@ module.exports = ({ cooler, pmModel }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildTask = (node, rootId, agg, now) => { @@ -131,6 +104,8 @@ module.exports = ({ cooler, pmModel }) => { id: node.key, rootId, ...c, + images: normalizeImages(c.images), + video: normalizeVideo(c.video), assignees, opinions, opinions_inhabitants: voters, @@ -138,8 +113,13 @@ module.exports = ({ cooler, pmModel }) => { }; }; + const currentTipId = async (ssb, id) => { + const idx = buildIndex(await getAllMessages(ssb)); + return idx.tipByRoot.get(idx.rootOf(id)) || id; + }; + return { - async createTask(title, description, startTime, endTime, priority, location = '', tagsRaw = [], isPublic) { + async createTask(title, description, startTime, endTime, priority, location = '', tagsRaw = [], isPublic, media = {}) { const ssb = await openSsb(); const userId = ssb.id; @@ -166,6 +146,8 @@ module.exports = ({ cooler, pmModel }) => { location, tags, isPublic: visibility, + images: normalizeImages(media && media.images), + video: normalizeVideo(media && media.video), assignees: [userId], createdAt: new Date().toISOString(), status: 'OPEN', @@ -180,9 +162,10 @@ module.exports = ({ cooler, pmModel }) => { async deleteTaskById(taskId) { const ssb = await openSsb(); const userId = ssb.id; - const task = await new Promise((res, rej) => ssb.get(taskId, (err, task) => err ? rej(new Error('Task not found')) : res(task))); + const tipId = await currentTipId(ssb, taskId); + const task = await new Promise((res, rej) => ssb.get(tipId, (err, task) => err ? rej(new Error('Task not found')) : res(task))); if (task.content.author !== userId) throw new Error('Not the author'); - const tombstone = { type: 'tombstone', target: taskId, deletedAt: new Date().toISOString(), author: userId }; + const tombstone = { type: 'tombstone', target: tipId, deletedAt: new Date().toISOString(), author: userId }; return new Promise((res, rej) => ssb.publish(tombstone, (err, result) => err ? rej(err) : res(result))); }, @@ -193,8 +176,9 @@ module.exports = ({ cooler, pmModel }) => { const keys = Object.keys(updatedData || {}).filter(k => updatedData[k] !== undefined); if (keys.length === 1 && keys[0] === 'assignees') return this.toggleAssignee(taskId); + const tipId = await currentTipId(ssb, taskId); const old = await new Promise((res, rej) => - ssb.get(taskId, (err, msg) => err || !msg ? rej(new Error('Task not found')) : res(msg)) + ssb.get(tipId, (err, msg) => err || !msg ? rej(new Error('Task not found')) : res(msg)) ); const c = old.content; @@ -253,9 +237,11 @@ module.exports = ({ cooler, pmModel }) => { tags: newTags, isPublic: newVisibility, status: newStatus, + images: normalizeImages(updatedData.images !== undefined ? updatedData.images : c.images), + video: updatedData.video !== undefined ? normalizeVideo(updatedData.video) : normalizeVideo(c.video), assignees: nextAssignees, updatedAt: new Date().toISOString(), - replaces: taskId + replaces: tipId }; return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result))); @@ -271,10 +257,10 @@ module.exports = ({ cooler, pmModel }) => { const ssb = await openSsb(); const now = moment(); const messages = await getAllMessages(ssb); - const idx = buildIndex(messages); + const idx = memoIndex('tasks', messages, buildIndex); const rootId = idx.rootOf(taskId); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error('Task not found'); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error('Task not found'); return buildTask(agg.contentNode, rootId, agg, now); }, diff --git a/src/models/tombstone_validator.js b/src/models/tombstone_validator.js index a3a5b4d..e90cdb8 100644 --- a/src/models/tombstone_validator.js +++ b/src/models/tombstone_validator.js @@ -2,26 +2,27 @@ function buildValidatedTombstoneSet(messages) { if (!Array.isArray(messages)) return new Set(); const tombClaims = new Map(); const targetAuthors = new Map(); + const targetContentAuthors = new Map(); const targetRoots = new Map(); + const claims = []; for (const m of messages) { if (!m || !m.value) continue; const v = m.value; const c = v.content; if (!c) continue; if (typeof c === 'object' && c.type === 'tombstone' && typeof c.target === 'string') { - const ts = v.timestamp || 0; - const prev = tombClaims.get(c.target); - if (!prev || ts > prev.ts) tombClaims.set(c.target, { author: v.author, ts, rootId: c._rootId || null }); + claims.push({ target: c.target, author: v.author, rootId: c._rootId || null }); continue; } if (m.key) { targetAuthors.set(m.key, v.author); + if (typeof c === 'object' && c.type === 'feed' && typeof c.author === 'string') targetContentAuthors.set(m.key, c.author); if (typeof c === 'object' && c._rootId) targetRoots.set(m.key, c._rootId); } } const out = new Set(); - for (const [target, { author, rootId }] of tombClaims.entries()) { - if (targetAuthors.get(target) !== author) continue; + for (const { target, author, rootId } of claims) { + if (targetAuthors.get(target) !== author && targetContentAuthors.get(target) !== author) continue; if (rootId) { const targetRoot = targetRoots.get(target); if (targetRoot !== rootId) continue; diff --git a/src/models/torrent_downloads_model.js b/src/models/torrent_downloads_model.js new file mode 100644 index 0000000..7da185f --- /dev/null +++ b/src/models/torrent_downloads_model.js @@ -0,0 +1,169 @@ +const pull = require('../server/node_modules/pull-stream'); +const Abortable = require('../server/node_modules/pull-abortable'); + +const JOB_TTL_MS = 6 * 60 * 60 * 1000; +const PROBE_TIMEOUT_MS = 8000; +const RECHECK_MS = 15000; + +module.exports = ({ cooler }) => { + const jobs = new Map(); + let recheckTimer = null; + + const openSsb = () => cooler.open(); + const withTimeout = (p, ms) => Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms))]); + + const peerRpcs = (ssb) => { + const out = []; + const live = ssb && ssb.peers && typeof ssb.peers === 'object' ? ssb.peers : {}; + for (const id of Object.keys(live)) { + if (id === ssb.id) continue; + for (const rpc of (Array.isArray(live[id]) ? live[id] : [])) { + if (rpc && rpc.blobs && typeof rpc.blobs.get === 'function' && !rpc.closed) out.push({ id, rpc }); + } + } + return out; + }; + + const hasLocal = (ssb, blobId) => new Promise((resolve) => { + try { ssb.blobs.has(blobId, (err, has) => resolve(!err && !!has)); } catch (_) { resolve(false); } + }); + + const remoteHas = (peer, blobId) => withTimeout(new Promise((resolve) => { + try { peer.rpc.blobs.has(blobId, (err, has) => resolve(!err && !!has)); } catch (_) { resolve(false); } + }), PROBE_TIMEOUT_MS).catch(() => false); + + const findSeeds = async (ssb, blobId) => { + const peers = peerRpcs(ssb); + const flags = await Promise.all(peers.map((p) => remoteHas(p, blobId))); + return peers.filter((_, i) => flags[i]); + }; + + const fetchFrom = (ssb, seed, job) => new Promise((resolve, reject) => { + const abortable = Abortable(); + job.abort = () => abortable.abort(new Error('cancelled')); + let received = 0; + const cap = job.size > 0 ? job.size : 75 * 1024 * 1024; + const opts = { key: job.blobId, max: cap }; + pull( + seed.rpc.blobs.get(opts), + abortable, + pull.through((buf) => { received += buf.length; job.received = received; if (received > cap) abortable.abort(new Error('too large')); }), + ssb.blobs.add(job.blobId, (err) => { job.abort = null; err ? reject(err) : resolve(received); }) + ); + }); + + const finish = (job, state, error) => { + job.state = state; + job.error = error ? String(error.message || error) : ''; + job.finishedAt = Date.now(); + job.abort = null; + if (state === 'done' && job.size > 0) job.received = job.size; + }; + + const ensureRecheck = () => { + if (recheckTimer) return; + recheckTimer = setInterval(async () => { + const pending = Array.from(jobs.values()).filter((j) => j.state === 'nosource'); + if (!pending.length) { clearInterval(recheckTimer); recheckTimer = null; return; } + let ssb; + try { ssb = await openSsb(); } catch (_) { return; } + for (const job of pending) { + if (await hasLocal(ssb, job.blobId)) { finish(job, 'done'); continue; } + const seeds = await findSeeds(ssb, job.blobId); + if (seeds.length) run(job); + } + }, RECHECK_MS); + if (recheckTimer.unref) recheckTimer.unref(); + }; + + const run = async (job) => { + if (job.state === 'downloading' || job.state === 'probing') return; + let ssb; + try { ssb = await openSsb(); } catch (err) { finish(job, 'failed', err); return; } + if (await hasLocal(ssb, job.blobId)) { finish(job, 'done'); return; } + job.state = 'probing'; + job.error = ''; + const seeds = await findSeeds(ssb, job.blobId); + job.seeds = seeds.map((s) => s.id); + if (!seeds.length) { + job.state = 'nosource'; + try { ssb.blobs.want(job.blobId, () => {}); } catch (_) {} + ensureRecheck(); + return; + } + job.state = 'downloading'; + job.startedAt = job.startedAt || Date.now(); + let lastErr = null; + for (const seed of seeds) { + if (job.cancelled) { finish(job, 'cancelled'); return; } + job.peer = seed.id; + job.received = 0; + try { + await fetchFrom(ssb, seed, job); + finish(job, 'done'); + return; + } catch (err) { + lastErr = err; + if (job.cancelled) { finish(job, 'cancelled'); return; } + } + } + finish(job, 'failed', lastErr); + }; + + const sweep = () => { + const now = Date.now(); + for (const [key, job] of jobs) if (job.finishedAt && now - job.finishedAt > JOB_TTL_MS) jobs.delete(key); + }; + + const snapshot = (job) => ({ + blobId: job.blobId, + name: job.name, + size: job.size, + received: Math.min(job.received || 0, job.size || job.received || 0), + state: job.state, + seeds: Array.isArray(job.seeds) ? job.seeds.length : 0, + peer: job.peer || '', + error: job.error || '', + torrentKey: job.torrentKey || '', + startedAt: job.startedAt || 0, + finishedAt: job.finishedAt || 0, + active: job.state === 'queued' || job.state === 'probing' || job.state === 'downloading' || job.state === 'nosource' + }); + + return { + start({ blobId, size, name, torrentKey } = {}) { + const id = String(blobId || ''); + if (!id.startsWith('&')) throw new Error('torrentFromContentMissing'); + sweep(); + let job = jobs.get(id); + if (job && (job.state === 'downloading' || job.state === 'probing' || job.state === 'done')) return snapshot(job); + job = { blobId: id, size: Number(size) || 0, name: String(name || ''), torrentKey: torrentKey || '', received: 0, state: 'queued', seeds: [], error: '', cancelled: false, startedAt: 0, finishedAt: 0, abort: null, peer: '' }; + jobs.set(id, job); + run(job); + return snapshot(job); + }, + cancel(blobId) { + const job = jobs.get(String(blobId || '')); + if (!job) return false; + job.cancelled = true; + if (typeof job.abort === 'function') { try { job.abort(); } catch (_) {} } + else if (job.state !== 'done') finish(job, 'cancelled'); + return true; + }, + remove(blobId) { return jobs.delete(String(blobId || '')); }, + get(blobId) { + const job = jobs.get(String(blobId || '')); + return job ? snapshot(job) : null; + }, + list() { + sweep(); + return Array.from(jobs.values()).map(snapshot).sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0)); + }, + async hasLocal(blobId) { + try { return await hasLocal(await openSsb(), String(blobId || '')); } catch (_) { return false; } + }, + async seedsFor(blobId) { + try { return (await findSeeds(await openSsb(), String(blobId || ''))).map((s) => s.id); } catch (_) { return []; } + } + }; +}; diff --git a/src/models/torrents_model.js b/src/models/torrents_model.js index 9dc74cb..485e05d 100644 --- a/src/models/torrents_model.js +++ b/src/models/torrents_model.js @@ -1,8 +1,9 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); +const { readTyped, memoIndex } = require("./typed_log"); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const safeArr = (v) => (Array.isArray(v) ? v : []); @@ -13,6 +14,10 @@ const normalizeTags = (raw) => { return String(raw).split(",").map((t) => t.trim()).filter(Boolean); }; +const cipherField = (cipher) => (cipher && typeof cipher === "object" && cipher.key && cipher.manifestBlobId) + ? { cipher: { v: Number(cipher.v) || 1, key: String(cipher.key), manifestBlobId: String(cipher.manifestBlobId), chunkCount: Number(cipher.chunkCount) || 0 } } + : {}; + const parseBlobId = (blobMarkdown) => { const s = String(blobMarkdown || ""); const match = s.match(/\((&[^)]+\.sha256)\)/); @@ -40,10 +45,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'torrent') : msgs; const tombFor = async (target, tribeId, author) => tribeHelpers ? tribeHelpers.encryptTombstone(target, tribeId, author) : { type: 'tombstone', target, deletedAt: new Date().toISOString(), author }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))); - }); + const TORRENT_TYPES = ["torrent", "torrentOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, TORRENT_TYPES, { limit: logLimit, withWindow: true }); const getMsg = async (ssbClient, key) => new Promise((resolve) => { @@ -53,9 +57,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const buildIndex = (messages) => { const tomb = new Set(); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const authorByKey = new Map(); const tombRequests = []; const opinionMsgs = []; @@ -78,49 +79,24 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); authorByKey.set(k, v.author); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; for (const t of tombRequests) { const targetAuthor = authorByKey.get(t.target); if (targetAuthor && t.author === targetAuthor) tomb.add(t.target); } - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); - const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); - - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); const resolveGroup = (root) => { const contentTip = contentTipOf(root); @@ -137,7 +113,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return { contentTip, contentNode, opinions, voters }; }; - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildTorrent = (node, rootId, viewerId, agg) => { @@ -156,6 +132,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { title: undec ? "" : (c.title || ""), description: undec ? "" : (c.description || ""), size: c.size || 0, + source: undec ? "" : (typeof c.source === "string" && c.source.startsWith("&") ? c.source : ""), + sourceName: undec ? "" : (c.sourceName || ""), + cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null), + sourceMime: undec ? "" : (c.sourceMime || ""), opinions, opinions_inhabitants: voters, hasVoted: viewerId ? voters.includes(viewerId) : false, @@ -184,16 +164,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(unwrapForIndex(messages)); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Torrent not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Torrent not found"); return root; }, - async createTorrent(blobMarkdown, tagsRaw, title, description, size, tribeId) { + async createTorrent(blobMarkdown, tagsRaw, title, description, size, tribeId, source = null) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -211,7 +187,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { size: Number(size) || 0, opinions: {}, opinions_inhabitants: [], - ...(tribeId ? { tribeId } : {}) + ...(tribeId ? { tribeId } : {}), + ...(source && typeof source.source === "string" && source.source.startsWith("&") + ? { source: source.source, sourceName: String(source.sourceName || ""), sourceMime: String(source.sourceMime || "") } + : {}), + ...cipherField(source && source.cipher) }; content = await encryptIfTribe(content); @@ -221,7 +201,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { }); }, - async updateTorrentById(id, blobMarkdown, tagsRaw, title, description) { + async updateTorrentById(id, blobMarkdown, tagsRaw, title, description, extra = null) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -231,7 +211,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const oldDec = await decryptIfTribe(oldMsg.content); if (!oldDec || oldDec.type !== "torrent") throw new Error("Torrent not found"); assertReadable(oldDec, "Torrent"); - if ((oldDec.author || oldMsg.content.author) !== userId) throw new Error("Not the author"); + if (oldMsg.author !== userId) throw new Error("Not the author"); const aggTorrent = await this.getTorrentById(id, userId); if (aggTorrent && Object.keys(aggTorrent.opinions || {}).some(k => (aggTorrent.opinions[k] || 0) > 0)) throw new Error("Cannot edit torrent after it has received opinions."); @@ -243,6 +223,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { type: "torrent", replaces: tipId, url: blobId || oldDec.url, + ...(blobId ? cipherField(extra && extra.cipher) : cipherField(oldDec.cipher)), tags, title: title !== undefined ? title || "" : oldDec.title || "", description: description !== undefined ? description || "" : oldDec.description || "", @@ -250,7 +231,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { opinions: oldDec.opinions || {}, opinions_inhabitants: oldDec.opinions_inhabitants || [], author: oldDec.author || userId, - ...(oldMsg.content.tribeId ? { tribeId: oldMsg.content.tribeId } : {}), + ...((oldDec.tribeId || oldMsg.content.tribeId) ? { tribeId: oldDec.tribeId || oldMsg.content.tribeId } : {}), createdAt: oldDec.createdAt, updatedAt: now }; @@ -274,7 +255,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { if (!msg) throw new Error("Torrent not found"); const dec = await decryptIfTribe(msg.content); if (!dec || dec.type !== "torrent") throw new Error("Torrent not found"); - if ((dec.author || msg.content.author) !== userId) throw new Error("Not the author"); + if (msg.author !== userId) throw new Error("Not the author"); const tombstone = await tombFor(tipId, dec.tribeId || msg.content.tribeId, userId); @@ -338,10 +319,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const ssbClient = await openSsb(); const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(unwrapForIndex(messages)); - await decryptIndexNodes(idx); + const idx = await memoIndex('torrents', messages, async (m) => { const built = buildIndex(unwrapForIndex(m)); await decryptIndexNodes(built); return built; }); const root = idx.rootOf(id); + if (idx.nodes.has(root) && !idx.tipByRoot.has(root)) throw new Error("Torrent not found"); const agg = idx.resolveGroup(root); if (agg.contentNode && !idx.tomb.has(agg.contentTip)) return buildTorrent(agg.contentNode, root, viewer, agg); diff --git a/src/models/transfers_model.js b/src/models/transfers_model.js index 8e012e2..a4c2014 100644 --- a/src/models/transfers_model.js +++ b/src/models/transfers_model.js @@ -1,11 +1,14 @@ const pull = require("../server/node_modules/pull-stream") const moment = require("../server/node_modules/moment") -const { getConfig } = require("../configs/config-manager.js") +const { getConfig, defaultInvite } = require("../configs/config-manager.js") const categories = require("../backend/opinion_categories") const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeByPreferring, norm } = require('../backend/dedupe') +const { readTyped, memoIndex } = require('./typed_log') const logLimit = getConfig().ssbLogStream?.limit || 1000 +const TRANSFER_TYPES = ["transfer", "transferConfirm", "transferOpinion", "ubiClaim", "tombstone"] + const isValidId = (to) => /^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(String(to || "")) const parseNum = (v) => { @@ -19,6 +22,11 @@ const normalizeTags = (raw) => { return String(raw).split(",").map(t => t.trim()).filter(Boolean) } +const defaultPubId = () => { + const m = String((defaultInvite() || {}).code || "").match(/(@[A-Za-z0-9+/]+={0,2}\.ed25519)/) + return m ? m[1] : "" +} + const CATEGORIES = ["ECONOMIC", "TIME", "TRUST"] const normalizeCategory = (raw) => { const c = String(raw || "ECONOMIC").trim().toUpperCase() @@ -28,14 +36,16 @@ const normalizeCategory = (raw) => { module.exports = ({ cooler }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + const ubiEpochOf = (c) => { + const tags = Array.isArray(c.tags) ? c.tags.map(t => String(t)) : [] + const fromTag = tags.find(t => /^epoch:\d{4}-\d{2}$/i.test(t)) + if (fromTag) return fromTag.slice(6) + const m = String(c.concept || "").match(/(\d{4}-\d{2})/) + return m ? m[1] : "" + } const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - }) + readTyped(ssbClient, TRANSFER_TYPES, { limit: logLimit }) const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -43,7 +53,7 @@ module.exports = ({ cooler }) => { }) const buildIndex = (messages) => { - const tomb = new Set() + const tomb = buildValidatedTombstoneSet(messages) const nodes = new Map() const parent = new Map() const child = new Map() @@ -51,7 +61,7 @@ module.exports = ({ cooler }) => { const confirms = [] const opinionMsgs = [] const ubiByPub = new Map() - const ubiByUser = new Map() + const claimedPubs = new Map() const ubiClaimNodes = [] for (const m of messages) { @@ -60,43 +70,43 @@ module.exports = ({ cooler }) => { const c = v.content if (!c) continue - if (c.type === "tombstone" && c.target) { - tomb.add(c.target) - continue - } - if (c.type === "transferConfirm") { confirms.push({ target: c.target, author: v.author, ts: v.timestamp || 0 }); continue } + if (c.type === "tombstone") continue + if (c.type === "transferConfirm") { confirms.push({ target: c.target, tip: typeof c.tip === "string" ? c.tip : null, author: v.author, ts: v.timestamp || 0 }); continue } if (c.type === "transferOpinion") { opinionMsgs.push({ target: c.target, author: v.author, category: c.category, ts: v.timestamp || 0 }); continue } if (c.type === "transfer") { + if (c.from !== v.author) continue nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) if (c.replaces) { parent.set(k, c.replaces) child.set(c.replaces, k) } const tags = Array.isArray(c.tags) ? c.tags.map(t => String(t).toUpperCase()) : [] - if (tags.includes("UBI") && c.to && c.concept) { - const key = `${c.to}::${c.concept}` - if (v.author === c.from) ubiByPub.set(key, k) - else ubiByUser.set(key, k) + if (tags.includes("UBI") && !tags.includes("REBALANCE") && c.to && c.concept) { + const key = `${c.to}::${ubiEpochOf(c) || c.concept}` + if (!ubiByPub.has(key)) ubiByPub.set(key, new Set()) + ubiByPub.get(key).add(v.author) } } if (c.type === "ubiClaim") { ubiClaimNodes.push({ k, v, c, ts: v.timestamp || m.timestamp || 0 }) + if (c.pubId) { + if (!claimedPubs.has(v.author)) claimedPubs.set(v.author, new Set()) + claimedPubs.get(v.author).add(c.pubId) + } } } - for (const [key, userMsgKey] of ubiByUser.entries()) { - if (ubiByPub.has(key)) tomb.add(userMsgKey) - } - + const defaultPub = defaultPubId() for (const { k, v, c, ts } of ubiClaimNodes) { if (tomb.has(k)) continue const claimantId = v.author const epochId = c.epochId || "" - const concept = `UBI ${epochId} ${claimantId}`.trim() - const key = `${claimantId}::${concept}` - if (ubiByPub.has(key) || ubiByUser.has(key)) continue + const concept = `UBI - ${epochId}`.trim() + const key = `${claimantId}::${epochId || concept}` + const payers = ubiByPub.get(key) + if (payers && ((c.pubId && payers.has(c.pubId)) || (defaultPub && payers.has(defaultPub)))) continue const synthetic = { type: "transfer", from: c.pubId || "", @@ -106,7 +116,7 @@ module.exports = ({ cooler }) => { createdAt: c.claimedAt || new Date(ts).toISOString(), updatedAt: c.claimedAt || new Date(ts).toISOString(), deadline: null, - confirmedBy: [c.pubId || ""].filter(Boolean), + confirmedBy: [], status: "UNCONFIRMED", tags: ["UBI", "PENDING"], opinions: {}, @@ -153,42 +163,43 @@ module.exports = ({ cooler }) => { const resolveGroup = (root) => { const contentTip = contentTipOf(root) - const best = nodes.get(contentTip) || nodes.get(root) - if (!best) return null + const latest = nodes.get(contentTip) || nodes.get(root) + if (!latest) return null const rootAuthor = nodes.get(root) ? nodes.get(root).author : null - const groupKeys = []; { let x = root, g = 0; groupKeys.push(x); while (child.has(x) && g++ < 100000) { x = child.get(x); groupKeys.push(x) } } + const rootConfirms = confirmsByRoot.get(root) || [] + const seal = rootConfirms.filter(cf => { const n = cf.tip ? nodes.get(cf.tip) : null; return n && n.author === rootAuthor && rootOf(cf.tip) === root && cf.author === n.c.to }).sort((a, b) => a.ts - b.ts)[0] + const best = seal ? nodes.get(seal.tip) : latest const confirmedBy = new Set() const opinions = {}; const opinionSet = new Set() - for (const k of groupKeys) { - const n = nodes.get(k); if (!n) continue; if (n.author !== rootAuthor) continue; const c = n.c - for (const cb of (Array.isArray(c.confirmedBy) ? c.confirmedBy : [])) confirmedBy.add(cb) - for (const v of (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])) opinionSet.add(v) - if (c.opinions && typeof c.opinions === "object") for (const kk of Object.keys(c.opinions)) opinions[kk] = Math.max(opinions[kk] || 0, Number(c.opinions[kk]) || 0) - } - if (best.c.from) confirmedBy.add(best.c.from) - for (const cf of (confirmsByRoot.get(root) || [])) confirmedBy.add(cf.author) + if (best.c.from && best.c.from === best.author) confirmedBy.add(best.c.from) + for (const cf of rootConfirms) { if (cf.author === best.c.to) confirmedBy.add(cf.author) } for (const op of (opinionsByRoot.get(root) || [])) { if (!opinionSet.has(op.author)) { opinionSet.add(op.author); opinions[op.category] = (opinions[op.category] || 0) + 1 } } - return { contentTip, best, confirmedBy: [...confirmedBy], opinions, opinions_inhabitants: [...opinionSet], tombstoned: tomb.has(contentTip) } + const ubiPayerTrusted = !!best.author && (best.author === defaultPub || !!(claimedPubs.get(best.c.to) && claimedPubs.get(best.c.to).has(best.author))) + return { contentTip, best, confirmedBy: [...confirmedBy], opinions, opinions_inhabitants: [...opinionSet], tombstoned: tomb.has(contentTip) || tomb.has(best.key), ubiPayerTrusted } } return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup } } - const deriveStatus = (t) => { - const status = String(t.status || "").toUpperCase() + const isSettledUbi = (t) => { + const tags = Array.isArray(t.tags) ? t.tags.map(x => String(x).toUpperCase()) : [] + return tags.includes("UBI") && !tags.includes("PENDING") && /^[0-9a-f]{64}$/i.test(String(t.txid || "")) + } + + const deriveStatus = (t, ubiPayerTrusted = false) => { const required = t.from === t.to ? 1 : 2 const confirmedCount = Array.isArray(t.confirmedBy) ? t.confirmedBy.length : 0 - if (status === "DISCARDED") return "DISCARDED" + if (ubiPayerTrusted && isSettledUbi(t)) return "CLOSED" if (confirmedCount >= required) return "CLOSED" const dl = t.deadline ? moment(t.deadline) : null if (dl && dl.isValid() && dl.isBefore(moment())) return "DISCARDED" - return status === "CLOSED" ? "CLOSED" : "UNCONFIRMED" + return "UNCONFIRMED" } const buildTransfer = (node, agg) => { const c = node.c || {} - const confirmedBy = agg ? agg.confirmedBy : (Array.isArray(c.confirmedBy) ? c.confirmedBy : []) + const confirmedBy = agg ? agg.confirmedBy : (c.from && c.from === node.author ? [c.from] : []) const opinions = agg ? agg.opinions : (c.opinions || {}) const opinions_inhabitants = agg ? agg.opinions_inhabitants : (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : []) return { @@ -203,8 +214,9 @@ module.exports = ({ cooler }) => { updatedAt: c.updatedAt || null, deadline: c.deadline, confirmedBy, - status: deriveStatus({ ...c, confirmedBy }), + status: deriveStatus({ ...c, confirmedBy }, !!(agg && agg.ubiPayerTrusted)), tags: Array.isArray(c.tags) ? c.tags : [], + txid: typeof c.txid === "string" ? c.txid : null, opinions, opinions_inhabitants } @@ -269,6 +281,7 @@ module.exports = ({ cooler }) => { const old = await getMsg(ssbClient, tipId) if (!old?.content || old.content.type !== "transfer") throw new Error("Transfer not found") + if (old.author !== old.content.from) throw new Error("Not the author") const current = old.content const idxU = buildIndex(await getAllMessages(ssbClient)) @@ -331,6 +344,7 @@ module.exports = ({ cooler }) => { if (!msg?.content) throw new Error("Not found") if (msg.content.type !== "transfer") throw new Error("Not found") + if (msg.author !== msg.content.from) throw new Error("Not found") const idx = buildIndex(await getAllMessages(ssbClient)) const root = idx.rootOf(tipId) @@ -345,7 +359,7 @@ module.exports = ({ cooler }) => { if ((Array.isArray(t.confirmedBy) ? t.confirmedBy : []).includes(userId)) throw new Error("Already confirmed") - const content = { type: "transferConfirm", target: root, createdAt: new Date().toISOString() } + const content = { type: "transferConfirm", target: root, tip: tipId, createdAt: new Date().toISOString() } return new Promise((resolve, reject) => { ssbClient.publish(content, (e2, result) => e2 ? reject(e2) : resolve(result)) }) @@ -358,6 +372,7 @@ module.exports = ({ cooler }) => { const msg = await getMsg(ssbClient, tipId) if (!msg?.content || msg.content.type !== "transfer") throw new Error("Not found") + if (msg.author !== msg.content.from) throw new Error("Not the author") const idxD = buildIndex(await getAllMessages(ssbClient)) const gD = idxD.resolveGroup(idxD.rootOf(tipId)) @@ -396,7 +411,7 @@ module.exports = ({ cooler }) => { async getTransferById(id) { const ssbClient = await openSsb() const messages = await getAllMessages(ssbClient) - const idx = buildIndex(messages) + const idx = memoIndex('transfers', messages, buildIndex) const root = idx.rootOf(id) const g = idx.resolveGroup(root) @@ -405,6 +420,7 @@ module.exports = ({ cooler }) => { const msg = await getMsg(ssbClient, id) if (!msg?.content || msg.content.type !== "transfer") throw new Error("Not found") + if (msg.author !== msg.content.from) throw new Error("Not found") const tmpNode = { key: id, ts: msg.timestamp || 0, c: msg.content, author: msg.author } return buildTransfer(tmpNode) diff --git a/src/models/trending_model.js b/src/models/trending_model.js index 0528d61..e25dd9d 100644 --- a/src/models/trending_model.js +++ b/src/models/trending_model.js @@ -3,7 +3,15 @@ const { getConfig } = require('../configs/config-manager.js'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const opinionCategories = require('../backend/opinion_categories'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES, requestScope } = require('./typed_log'); +const longText = require('../backend/long_text'); +const isRootBlogPost = (c) => + !!c && c.type === 'post' && typeof c.text === 'string' && + !c.root && !c.fork && !c.branch && !c.about && !c.target && + c.private !== true && !Array.isArray(c.recps); + const { buildVoteTally } = require('../backend/vote_tally'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); module.exports = ({ cooler }) => { let ssb; @@ -19,61 +27,80 @@ module.exports = ({ cooler }) => { }; const types = [ - 'bookmark', 'votes', 'feed', - 'image', 'audio', 'video', 'document', 'torrent', 'transfer' + 'bookmark', 'votes', 'poll', 'feed', 'post', + 'image', 'audio', 'video', 'document', 'torrent', 'file', 'transfer', + 'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse', + 'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute' ]; const categories = opinionCategories; - const listTrending = async (filter = 'ALL') => { + const computeBase = async () => { const ssbClient = await openSsb(); - const userId = ssbClient.id; - - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, xs) => err ? rej(err) : res(xs)) - ); - }); + const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); const tombstoned = buildValidatedTombstoneSet(messages); - const replaces = new Map(); const itemsById = new Map(); + const opinionMsgs = []; for (const m of messages) { const k = m.key; const c = m.value?.content; if (!c) continue; - if (c.type === 'tombstone' && c.target) { - tombstoned.add(c.target); - itemsById.delete(c.target); + if (c.type === 'tombstone') continue; + + if (typeof c.type === 'string' && c.type.endsWith('Opinion') && c.target) { + opinionMsgs.push({ target: c.target, author: m.value?.author, category: c.category }); continue; } - if (c.opinions && !tombstoned.has(k) && !['task', 'event', 'report'].includes(c.type)) { - if (c.replaces) replaces.set(c.replaces, k); - itemsById.set(k, m); - } + if (c.opinions && !['task', 'event', 'report'].includes(c.type)) itemsById.set(k, m); - if (c.type === 'feed' && !tombstoned.has(k) && !itemsById.has(k)) { - if (c.replaces) replaces.set(c.replaces, k); - itemsById.set(k, m); + if (c.type === 'feed' && !itemsById.has(k)) itemsById.set(k, m); + if (isRootBlogPost(c) && !itemsById.has(k)) itemsById.set(k, m); + } + + const chunkLookup = longText.lookupIn(longText.indexChunks(messages)); + for (const [k, m] of Array.from(itemsById.entries())) { + const c = m.value?.content; + if (c?.type === 'post' && longText.hasChunks(c)) { + itemsById.set(k, { ...m, value: { ...m.value, content: longText.resolveField(c, 'text', m.value.author, chunkLookup) } }); } } - for (const [replacedId, newId] of replaces.entries()) { - const oldMsg = itemsById.get(replacedId); - const newMsg = itemsById.get(newId); - if (!oldMsg) continue; - if (!newMsg || String(newMsg.value?.author) !== String(oldMsg.value?.author)) { - itemsById.delete(newId); - continue; - } - itemsById.delete(replacedId); + const vg = versionGroups(Array.from(itemsById.values()).map(versionEntry)); + for (const k of Array.from(itemsById.keys())) { + if (!vg.roots.has(vg.rootOf(k)) || vg.tipOf(k) !== k || tombstoned.has(k)) itemsById.delete(k); + } + + const opinionByTip = new Map(); + for (const op of opinionMsgs) { + const tip = vg.tipOf(op.target); + if (!opinionByTip.has(tip)) opinionByTip.set(tip, { opinions: {}, voters: new Set() }); + const agg = opinionByTip.get(tip); + if (agg.voters.has(op.author)) continue; + agg.voters.add(op.author); + if (op.category) agg.opinions[op.category] = (agg.opinions[op.category] || 0) + 1; + } + for (const [k, m] of Array.from(itemsById.entries())) { + const agg = opinionByTip.get(k); + if (!agg) continue; + const c = m.value?.content || {}; + const mergedVoters = new Set([...(Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])]); + const mergedOpinions = { ...(c.opinions || {}) }; + for (const [cat, n] of Object.entries(agg.opinions)) mergedOpinions[cat] = (mergedOpinions[cat] || 0) + n; + for (const v of agg.voters) mergedVoters.add(v); + itemsById.set(k, { ...m, value: { ...m.value, content: { ...c, opinions: mergedOpinions, opinions_inhabitants: [...mergedVoters] } } }); } let rawItems = Array.from(itemsById.values()).filter(m => types.includes(m.value?.content?.type)); + rawItems = rawItems.filter(m => { + const c = m.value?.content; + if (c?.type !== 'schoolCourse') return true; + if (c.encryptedPayload) return false; + return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE'; + }); const blobTypes = ['document', 'image', 'audio', 'video']; let items = await Promise.all( @@ -101,7 +128,8 @@ module.exports = ({ cooler }) => { case 'feed': return `feed::${(c.text || '').replace(/\s+/g, ' ').trim()}`; case 'votes': - return `votes::${(c.question || '').replace(/\s+/g, ' ').trim()}`; + case 'poll': + return `${c.type}::${(c.question || '').replace(/\s+/g, ' ').trim()}`; case 'transfer': return `transfer::${(c.concept || '')}|${c.amount || ''}|${c.from || ''}|${c.to || ''}|${c.deadline || ''}`; default: @@ -135,7 +163,21 @@ module.exports = ({ cooler }) => { bySig.set(sig, m); } } - items = Array.from(bySig.values()); + return Array.from(bySig.values()); + }; + + const baseItems = () => { + const store = requestScope.getStore(); + if (store && store.trendingBase) return store.trendingBase; + const pending = computeBase(); + if (store) store.trendingBase = pending; + return pending; + }; + + const listTrending = async (filter = 'ALL') => { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + let items = (await baseItems()).slice(); if (filter === 'MINE') { items = items.filter(m => m.value.author === userId); @@ -148,9 +190,7 @@ module.exports = ({ cooler }) => { items = items.filter(m => m.value.content.type === filter); } - if (filter !== 'ALL' && !types.includes(filter)) { - items = items.filter(m => (m.value.content.opinions_inhabitants || []).length > 0); - } + items = items.filter(m => (m.value.content.opinions_inhabitants || []).length > 0); if (filter === 'TOP') { items.sort((a, b) => { @@ -193,31 +233,12 @@ module.exports = ({ cooler }) => { const inhabitants = Array.isArray(msg.content.opinions_inhabitants) ? msg.content.opinions_inhabitants : []; if (inhabitants.includes(userId)) throw new Error('Already voted'); + const otype = categories.messageTypes[type]; + if (!otype) throw new Error('Voting not allowed on this content type'); - const tombstone = { - type: 'tombstone', - target: contentId, - deletedAt: new Date().toISOString(), - author: userId - }; - - const updated = { - ...msg.content, - opinions: { - ...(msg.content.opinions || {}), - [category]: ((msg.content.opinions && msg.content.opinions[category]) || 0) + 1 - }, - opinions_inhabitants: inhabitants.concat(userId), - updatedAt: new Date().toISOString(), - replaces: contentId - }; - - await new Promise((res, rej) => { - ssbClient.publish(tombstone, err => err ? rej(err) : res()); - }); - + const content = { type: otype, target: contentId, category, createdAt: new Date().toISOString() }; return new Promise((res, rej) => { - ssbClient.publish(updated, (err, result) => err ? rej(err) : res(result)); + ssbClient.publish(content, (err, result) => err ? rej(err) : res(result)); }); }; diff --git a/src/models/tribe_crypto.js b/src/models/tribe_crypto.js deleted file mode 100644 index f5576b7..0000000 --- a/src/models/tribe_crypto.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = (configPath) => require('./crypto')(configPath, 'tribes'); diff --git a/src/models/tribes_content_model.js b/src/models/tribes_content_model.js index 83fe943..048f3e9 100644 --- a/src/models/tribes_content_model.js +++ b/src/models/tribes_content_model.js @@ -2,11 +2,30 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const tribeLogLimit = Math.max(logLimit, 100000); +const { readTyped } = require('./typed_log'); +const longText = require('../backend/long_text'); +const { versionGroups } = require('../backend/dedupe'); +const TRIBE_LOG_TYPES = ['tribe', 'tribe-msg', 'tribe-keys-distrib', 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-open-invite-tombstone', 'tombstone', 'contact']; -const VALID_CONTENT_TYPES = ['event', 'task', 'report', 'votation', 'forum', 'forum-reply', 'market', 'job', 'project', 'media', 'feed', 'pixelia']; +const VALID_CONTENT_TYPES = ['event', 'task', 'report', 'votation', 'forum', 'forum-reply', 'market', 'job', 'project', 'media', 'feed']; const categories = require('../backend/opinion_categories'); const VALID_STATUSES = ['OPEN', 'CLOSED', 'IN-PROGRESS']; const VALID_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']; +const EXPOSURE_TYPE = 'tribeExposure'; +const REACH_LEVELS = ['tribe', 'oasis', 'clearnet']; +const REACH_RANK = { tribe: 0, oasis: 1, clearnet: 2 }; +const SNAPSHOT_FIELDS = ['contentType', 'title', 'description', 'status', 'date', 'location', 'price', 'salary', 'priority', 'options', 'category', 'tags', 'image', 'mediaType', 'license', 'url', 'deadline', 'goal', 'dates', 'lat', 'lng', 'markers', 'createdAt', 'updatedAt']; +const snapshotOf = (item, replies = []) => { + const out = {}; + for (const f of SNAPSHOT_FIELDS) { + const v = item[f]; + if (v === undefined || v === null || v === '' || (Array.isArray(v) && !v.length)) continue; + out[f] = v; + } + out.author = item.author; + if (item.contentType === 'forum') out.replies = (replies || []).map(r => ({ author: r.author, description: r.description || '', createdAt: r.createdAt || null })); + return out; +}; module.exports = ({ cooler, tribeCrypto, tribesModel }) => { let ssb; @@ -14,12 +33,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const readLog = async () => { const client = await openSsb(); - return new Promise((resolve, reject) => - pull( - client.createLogStream({ limit: tribeLogLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ); + return readTyped(client, TRIBE_LOG_TYPES, { limit: tribeLogLimit }); }; const fingerprintsForRoot = (rootId) => { @@ -38,6 +52,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { ); }; + const publishContent = async (rootId, body) => { + const publishChunk = (c) => wrapAndPublishContent(rootId, { k: longText.CHUNK_TYPE, rootId, text: c.text, index: c.index, total: c.total, group: c.group }); + const sealed = await longText.chunkContent(body, 'description', { maxBytes: longText.CONTENT_BYTES.sealed, extra: { rootId }, publish: publishChunk }); + return wrapAndPublishContent(rootId, sealed); + }; + const decodeContentMsgs = async (msgs, opts) => { const fpIdx = tribeCrypto.buildFingerprintIndex(); const targetRootId = opts && opts.rootId ? opts.rootId : null; @@ -45,6 +65,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const allowedFps = targetRootId ? fingerprintsForRoot(targetRootId) : null; const content = new Map(); + const chunks = new Map(); const tombRequests = []; const collabMsgs = []; @@ -62,36 +83,30 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { tombRequests.push({ target: b.target, author: m.value.author }); continue; } + if (b.k === longText.CHUNK_TYPE) { + const ch = longText.chunkOf(m.value.author, b); + if (ch) chunks.set(m.key, ch); + continue; + } if (b.k === 'tc-collab') { - collabMsgs.push({ sub: b.sub, target: b.target, author: m.value.author, ts: m.value.timestamp || 0, option: b.option, on: b.on, category: b.category }); + collabMsgs.push({ sub: b.sub, target: b.target, author: m.value.author, ts: m.value.timestamp || 0, seq: m.value.sequence || 0, option: b.option, on: b.on, category: b.category }); continue; } if (b.k !== 'tribe-content') continue; - content.set(m.key, { author: m.value.author, body: b, ts: m.value.timestamp }); + content.set(m.key, { author: m.value.author, body: b, ts: m.value.timestamp, seq: m.value.sequence || 0 }); } - const naiveNext = new Map(); - const strictNext = new Map(); - for (const [key, node] of content) { - const t = node.body.replaces; - if (!t) continue; - naiveNext.set(t, key); - const orig = content.get(t); - if (orig && orig.author === node.author) strictNext.set(t, key); + if (targetRootId && tribesModel && typeof tribesModel.departures === 'function') { + const gone = await tribesModel.departures(targetRootId).catch(() => new Map()); + const afterLeaving = (author, ts, seq) => { + const d = gone.get(author); + return !!d && ((d.seq !== undefined && seq > d.seq) || (d.ts !== undefined && (Number(ts) || 0) > d.ts)); + }; + for (const [key, node] of [...content]) if (afterLeaving(node.author, node.ts, node.seq)) content.delete(key); + for (let i = collabMsgs.length - 1; i >= 0; i--) if (afterLeaving(collabMsgs[i].author, collabMsgs[i].ts, collabMsgs[i].seq)) collabMsgs.splice(i, 1); } - for (const [t, key] of Array.from(naiveNext.entries())) { - const orig = content.get(t); - if (!orig) { naiveNext.delete(t); continue; } - const node = content.get(key); - if (!node || node.author !== orig.author) { naiveNext.delete(t); content.delete(key); } - } - const naivePrev = new Map(); - for (const [t, key] of naiveNext) naivePrev.set(key, t); - const followTo = (start, nextMap) => { let x = start, g = 0; while (nextMap.has(x) && g++ < 100000) x = nextMap.get(x); return x; }; - const rootOf = (key) => { let x = key, g = 0; while (naivePrev.has(x) && content.has(naivePrev.get(x)) && g++ < 100000) x = naivePrev.get(x); return x; }; - const roots = new Set(); - for (const [key, node] of content) { const t = node.body.replaces; if (!t || !content.has(t)) roots.add(key); } + const vg = versionGroups([...content].map(([key, node]) => ({ key, author: node.author, seq: node.seq, ts: node.ts, replaces: typeof node.body.replaces === 'string' ? node.body.replaces : null, claimedAuthor: typeof node.body.author === 'string' ? node.body.author : null }))); const tombstoned = new Set(); for (const t of tombRequests) { const orig = content.get(t.target); if (orig && t.author === orig.author) tombstoned.add(t.target); } @@ -100,7 +115,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const getCB = (root) => { if (!collabByRoot.has(root)) collabByRoot.set(root, { vote: new Map(), attend: new Map(), assign: new Map(), refeed: new Set(), opinion: new Map() }); return collabByRoot.get(root); }; for (const cm of collabMsgs) { if (!content.has(cm.target)) continue; - const a = getCB(rootOf(cm.target)); + const a = getCB(vg.rootOf(cm.target)); if (cm.sub === 'vote' && Number.isInteger(cm.option)) { if (!a.vote.has(cm.author)) a.vote.set(cm.author, cm.option); } else if (cm.sub === 'attend') { const p = a.attend.get(cm.author); if (!p || cm.ts >= p.ts) a.attend.set(cm.author, { on: cm.on !== false, ts: cm.ts }); } else if (cm.sub === 'assign') { const p = a.assign.get(cm.author); if (!p || cm.ts >= p.ts) a.assign.set(cm.author, { on: cm.on !== false, ts: cm.ts }); } @@ -112,15 +127,15 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const tipOf = new Map(); const EMPTY = { vote: new Map(), attend: new Map(), assign: new Map(), refeed: new Set(), opinion: new Map() }; - for (const root of roots) { - const contentTip = followTo(root, strictNext); + for (const root of vg.roots) { + const contentTip = vg.tipOf(root); const tipNode = content.get(contentTip); if (!tipNode) continue; const body = tipNode.body; const lb = tipNode.body; const a = collabByRoot.get(root) || EMPTY; - const item = { id: contentTip, ...body, author: tipNode.author, _ts: tipNode.ts }; + const item = { id: contentTip, ...body, description: longText.joinText(body.description, body.chunks, longText.lookupIn(chunks), tipNode.author), author: tipNode.author, _ts: tipNode.ts, originId: root }; const voteAuthors = new Set(); const votes = {}; const legacyVotes = lb.votes && typeof lb.votes === 'object' ? lb.votes : {}; @@ -145,8 +160,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { for (const [au, cat] of a.opinion) if (!opInh.has(au)) { opInh.add(au); opinions[cat] = (opinions[cat] || 0) + 1; } item.opinions_inhabitants = [...opInh]; item.opinions = opinions; - let cur = root, g = 0; tipOf.set(root, contentTip); - while (naiveNext.has(cur) && g++ < 100000) { cur = naiveNext.get(cur); tipOf.set(cur, contentTip); } + for (const k of (vg.groups.get(root) || [root])) tipOf.set(k, contentTip); if (tombstoned.has(contentTip)) continue; if (targetRootId && body.rootId !== targetRootId) continue; @@ -157,7 +171,23 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return { items, tipOf, tombstoned }; }; + const chainIsPublic = async (tribeId) => { + let cur; + try { cur = await tribesModel.getTribeById(tribeId); } catch (_) { return false; } + const seen = new Set(); + while (cur) { + if (cur.isAnonymous) return false; + if (!cur.parentTribeId) return true; + if (seen.has(cur.parentTribeId)) return false; + seen.add(cur.parentTribeId); + try { cur = await tribesModel.getTribeById(cur.parentTribeId); } catch (_) { return false; } + } + return false; + }; + return { + chainIsPublic, + async create(tribeId, contentType, data) { if (!VALID_CONTENT_TYPES.includes(contentType)) throw new Error('Invalid content type'); if (data.status && !VALID_STATUSES.includes(data.status)) throw new Error('Invalid status. Must be OPEN, CLOSED, or IN-PROGRESS'); @@ -187,6 +217,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { tags: data.tags || [], image: data.image || null, mediaType: data.mediaType || null, + ...(data.license ? { license: data.license } : {}), url: data.url || null, attendees: data.attendees || [], deadline: data.deadline || null, @@ -200,7 +231,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { createdAt: now, updatedAt: now }; - return wrapAndPublishContent(rootId, body); + return publishContent(rootId, body); }, async update(contentId, data, existing) { @@ -215,7 +246,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { k: 'tribe-content', rootId, contentType: existing.contentType, - replaces: contentId, + replaces: existing.id || contentId, title: data.title !== undefined ? data.title : existing.title, description: data.description !== undefined ? data.description : existing.description, status: data.status !== undefined ? data.status : existing.status, @@ -232,6 +263,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { tags: data.tags !== undefined ? data.tags : existing.tags, image: data.image !== undefined ? data.image : existing.image, mediaType: data.mediaType !== undefined ? data.mediaType : existing.mediaType, + ...((data.license !== undefined ? data.license : existing.license) ? { license: data.license !== undefined ? data.license : existing.license } : {}), url: data.url !== undefined ? data.url : existing.url, attendees: data.attendees !== undefined ? data.attendees : existing.attendees, deadline: data.deadline !== undefined ? data.deadline : existing.deadline, @@ -245,7 +277,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { createdAt: existing.createdAt, updatedAt: now }; - return wrapAndPublishContent(rootId, body); + return publishContent(rootId, body); }, async deleteById(contentId) { @@ -256,7 +288,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const body = { k: 'tombstone', rootId, - target: contentId, + target: existing.id || contentId, author: client.id, deletedAt: new Date().toISOString() }; @@ -347,6 +379,103 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return wrapAndPublishContent(item.rootId, { k: 'tc-collab', sub: 'opinion', rootId: item.rootId, target: item.id, category }); }, + reachLevels: REACH_LEVELS, + + async publishExposure(item, level, replies = []) { + if (!REACH_LEVELS.includes(level)) throw new Error('Invalid level'); + if (!item || !item.rootId) throw new Error('Content not found'); + const client = await openSsb(); + const publish = (c) => new Promise((resolve, reject) => client.publish(c, (err, r) => err ? reject(err) : resolve(r))); + const content = { + type: EXPOSURE_TYPE, + tribeId: item.rootId, + origin: item.originId || item.id, + contentType: item.contentType, + level, + item: null, + at: new Date().toISOString() + }; + if (level !== 'tribe') { + const { replies: snapReplies, ...snapshot } = snapshotOf(item, replies); + const budget = longText.CONTENT_BYTES.plain - longText.contentBytes(content); + const sealed = await longText.chunkContent(snapshot, 'description', { maxBytes: budget, publish }); + if (Array.isArray(snapReplies)) { + sealed.replies = snapReplies.slice(); + while (sealed.replies.length && longText.contentBytes({ ...content, item: sealed }) > longText.CONTENT_BYTES.plain) sealed.replies.pop(); + } + content.item = sealed; + } + return publish(content); + }, + + async exposureState({ ignorePrivacy = false } = {}) { + const client = await openSsb(); + const msgs = await readTyped(client, [EXPOSURE_TYPE, longText.CHUNK_TYPE], { limit: tribeLogLimit }); + const lookup = longText.lookupIn(longText.indexChunks(msgs)); + const byOrigin = new Map(); + for (const m of msgs) { + const c = m && m.value && m.value.content; + if (!c || c.type !== EXPOSURE_TYPE || typeof c.origin !== 'string' || typeof c.tribeId !== 'string' || !REACH_LEVELS.includes(c.level)) continue; + if (!byOrigin.has(c.origin)) byOrigin.set(c.origin, []); + byOrigin.get(c.origin).push({ by: m.value.author, ts: Number(m.value.timestamp) || 0, c }); + } + const memberCache = new Map(); + const isMember = async (author, tribeId) => { + const k = `${tribeId}|${author}`; + if (!memberCache.has(k)) memberCache.set(k, await tribesModel.isTribeMember(author, tribeId).catch(() => false)); + return memberCache.get(k); + }; + const everCache = new Map(); + const wasMember = async (author, tribeId) => { + const k = `${tribeId}|${author}`; + if (!everCache.has(k)) everCache.set(k, await tribesModel.wasEverMember(author, tribeId).catch(() => false)); + return everCache.get(k); + }; + const privacyCache = new Map(); + const isPrivate = async (tribeId) => { + if (!privacyCache.has(tribeId)) privacyCache.set(tribeId, !(await chainIsPublic(tribeId))); + return privacyCache.get(tribeId); + }; + const state = new Map(); + for (const [origin, list] of byOrigin) { + list.sort((a, b) => (b.ts - a.ts) || (REACH_RANK[a.c.level] - REACH_RANK[b.c.level])); + let chosen = null; + for (const e of list) { + if (await isMember(e.by, e.c.tribeId)) { chosen = e; break; } + if (e.c.level === 'tribe' && await wasMember(e.by, e.c.tribeId)) { chosen = e; break; } + } + if (!chosen) continue; + const priv = await isPrivate(chosen.c.tribeId); + const level = !ignorePrivacy && priv ? 'tribe' : chosen.c.level; + state.set(origin, { origin, tribeId: chosen.c.tribeId, contentType: chosen.c.contentType, level, item: level === 'tribe' || !chosen.c.item ? null : longText.resolveField(chosen.c.item, 'description', chosen.by, lookup), by: chosen.by, ts: chosen.ts }); + } + return state; + }, + + async levelOf(item, state = null) { + const st = state || await this.exposureState(); + const e = st.get(item && (item.originId || item.id)); + return e ? e.level : 'tribe'; + }, + + async listExposed(tribeId, contentType = null, minLevel = 'oasis', state = null) { + const rootId = await tribesModel.getRootId(tribeId).catch(() => tribeId); + const st = state || await this.exposureState(); + const out = []; + for (const e of st.values()) { + if (e.tribeId !== rootId || !e.item || REACH_RANK[e.level] < REACH_RANK[minLevel]) continue; + const base = { ...e.item, id: e.origin, originId: e.origin, rootId, reach: e.level, exposedBy: e.by, _ts: e.ts, attendees: [], assignees: [], votes: {}, opinions: {}, opinions_inhabitants: [], refeeds: 0, refeeds_inhabitants: [] }; + if (contentType === 'forum-reply') { + if (e.item.contentType !== 'forum') continue; + (e.item.replies || []).forEach((r, i) => out.push({ id: `${e.origin}:${i}`, originId: `${e.origin}:${i}`, rootId, contentType: 'forum-reply', parentId: e.origin, description: r.description, author: r.author, createdAt: r.createdAt, reach: e.level, attendees: [], assignees: [], votes: {}, opinions: {}, opinions_inhabitants: [], refeeds_inhabitants: [] })); + continue; + } + if (contentType && e.item.contentType !== contentType) continue; + out.push(base); + } + return out.sort((a, b) => (Date.parse(b.updatedAt || b.createdAt) || b._ts || 0) - (Date.parse(a.updatedAt || a.createdAt) || a._ts || 0)); + }, + async getThread(forumId) { const msgs = await readLog(); const { items } = await decodeContentMsgs(msgs, {}); diff --git a/src/models/tribes_model.js b/src/models/tribes_model.js index 7d2a799..8aa345b 100644 --- a/src/models/tribes_model.js +++ b/src/models/tribes_model.js @@ -4,6 +4,8 @@ const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const tribeLogLimit = Math.max(logLimit, 100000); +const { readTyped } = require('./typed_log'); +const TRIBE_LOG_TYPES = ['tribe', 'tribe-msg', 'tribe-keys-distrib', 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-open-invite', 'tribe-open-invite-tombstone', 'tombstone', 'contact']; const INVITE_CODE_BYTES = 16; const VALID_INVITE_MODES = ['strict', 'open']; @@ -25,13 +27,13 @@ module.exports = ({ cooler, tribeCrypto }) => { return true; }; - const validMembershipDelta = (prev, next, author) => { + const validMembershipDelta = (prev, next, author, selfJoinOk = () => true) => { const a = Array.isArray(prev) ? prev : []; const b = Array.isArray(next) ? next : []; const added = b.filter(m => !a.includes(m)); const removed = a.filter(m => !b.includes(m)); if (added.length === 0 && removed.length === 0) return true; - if (added.length === 1 && removed.length === 0 && added[0] === author) return true; + if (added.length === 1 && removed.length === 0 && added[0] === author) return selfJoinOk() === true; if (removed.length === 1 && added.length === 0 && removed[0] === author) return true; return false; }; @@ -49,19 +51,29 @@ module.exports = ({ cooler, tribeCrypto }) => { const x = a[f]; const y = b[f]; if (Array.isArray(x) || Array.isArray(y)) { if (!arraysEqual(x, y)) return false; continue; } - if (x !== y && !(x == null && y == null)) return false; + if (x !== y && !((x == null || x === '') && (y == null || y === ''))) return false; } return true; }; const streamLog = async () => { const client = await openSsb(); - return new Promise((resolve, reject) => { - pull( - client.createLogStream({ limit: tribeLogLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ); - }); + return readTyped(client, TRIBE_LOG_TYPES, { limit: tribeLogLimit }); + }; + const readKeyDistribs = async () => { + const client = await openSsb(); + const ssbKeys = require('../server/node_modules/ssb-keys'); + const config = require('../server/ssb_config'); + const msgs = await readTyped(client, TRIBE_LOG_TYPES, { limit: tribeLogLimit, withPrivate: true }); + const out = []; + for (const m of msgs) { + const v = m && m.value; + if (!v) continue; + const c = v.content; + const dec = (c && typeof c === 'object') ? (v.private === true && tribeCrypto.isKeyDistribContent(c) ? c : null) : tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys); + if (dec) out.push({ m, dec }); + } + return { msgs, distribs: out }; }; const buildTribeIndex = async () => { @@ -72,17 +84,31 @@ module.exports = ({ cooler, tribeCrypto }) => { const tribeMsgs = new Map(); const tombstones = new Map(); + const inviteAuthorByCh = new Map(); + const inviteAuthorByKey = new Map(); + const inviteTombstonesByAuthor = new Map(); + const sealedInvites = []; for (const m of msgs) { const c = m.value && m.value.content; if (!c) continue; const author = m.value.author; const ts = m.value.timestamp; + if (c.type === 'tribe-invite-msg' && typeof c.ch === 'string') { inviteAuthorByCh.set(c.ch, author); inviteAuthorByKey.set(m.key, author); continue; } + if (c.type === 'tribe-invite-tombstone' && typeof c.target === 'string') { + if (!inviteTombstonesByAuthor.has(author)) inviteTombstonesByAuthor.set(author, new Set()); + inviteTombstonesByAuthor.get(author).add(c.target); + continue; + } let body = null; if (tribeCrypto.isTribeMsg(c)) { const r = tribeCrypto.unwrapMsg(c, fpIdx); if (!r || !r.body || !r.body.k) continue; + if (r.body.k === 'open-invite' || r.body.k === 'open-invite-tombstone') { + sealedInvites.push({ key: m.key, author, body: r.body, keyRoot: r.rootId, ts }); + continue; + } body = r.body; } else if (c.type === 'tribe' && typeof c === 'object') { body = { @@ -104,7 +130,9 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: c.mapUrl, createdAt: c.createdAt, updatedAt: c.updatedAt, - author: c.author + author: c.author, + joinedWith: typeof c.joinedWith === 'string' ? c.joinedWith : undefined, + removedSeqs: c.removedSeqs && typeof c.removedSeqs === 'object' ? c.removedSeqs : undefined }; } else if (c.type === 'tombstone' && c.target) { tombstones.set(c.target, { author, ts }); @@ -125,6 +153,7 @@ module.exports = ({ cooler, tribeCrypto }) => { op: body.op || (body.rootId ? 'update' : 'create'), content: body, author, + seq: (m.value && m.value.sequence) || 0, _ts: ts }); } @@ -142,49 +171,58 @@ module.exports = ({ cooler, tribeCrypto }) => { } } - let progress = true; - while (progress) { - progress = false; - const candidatesByReplaces = new Map(); - for (const [k, entry] of tribeMsgs.entries()) { + const byReplaces = new Map(); + for (const [k, entry] of tribeMsgs.entries()) { + if (!entry.replaces) continue; + if (!byReplaces.has(entry.replaces)) byReplaces.set(entry.replaces, []); + byReplaces.get(entry.replaces).push([k, entry]); + } + const queue = [...tribes.keys()]; + for (let qi = 0; qi < queue.length; qi++) { + const replaces = queue[qi]; + if (child.has(replaces)) continue; + const parentEntry = tribes.get(replaces); + const root = rootByTip.get(replaces); + const rootEntry = tribes.get(root); + const rootAuthor = rootEntry && rootEntry.author; + const prevMembers = Array.isArray(parentEntry.content.members) ? parentEntry.content.members : []; + const candidates = []; + for (const [k, entry] of (byReplaces.get(replaces) || [])) { if (tribes.has(k)) continue; - const replaces = entry.replaces; - if (!replaces) continue; - const parentEntry = tribes.get(replaces); - if (!parentEntry) continue; - if (child.has(replaces)) continue; - const root = rootByTip.get(replaces); - const rootEntry = tribes.get(root); - const rootAuthor = rootEntry && rootEntry.author; const isRootAuthor = entry.author === rootAuthor; - const prevMembers = Array.isArray(parentEntry.content.members) ? parentEntry.content.members : []; if (!isRootAuthor) { if (!prevMembers.includes(entry.author) && !(entry.content.members || []).includes(entry.author)) continue; - if (!validMembershipDelta(prevMembers, entry.content.members, entry.author)) continue; + const mode = String(parentEntry.content.inviteMode || (rootEntry && rootEntry.content.inviteMode) || 'strict'); + const inviterOk = (inviter) => !!inviter && (inviter === rootAuthor || (mode === 'open' && prevMembers.includes(inviter))); + const selfJoinOk = () => { + const proof = entry.content.joinedWith; + if (proof && inviterOk(inviteAuthorByCh.get(proof))) return true; + const mine = inviteTombstonesByAuthor.get(entry.author); + if (mine) for (const target of mine) if (inviterOk(inviteAuthorByKey.get(target))) return true; + return false; + }; + if (!validMembershipDelta(prevMembers, entry.content.members, entry.author, selfJoinOk)) continue; if (!validInvitesDelta(parentEntry.content.invites, entry.content.invites, entry.author, rootAuthor)) continue; if (!structuralFieldsEqual(parentEntry.content, entry.content)) continue; } - if (!candidatesByReplaces.has(replaces)) candidatesByReplaces.set(replaces, []); - candidatesByReplaces.get(replaces).push({ k, entry, isRootAuthor, root }); + candidates.push({ k, entry, isRootAuthor, root }); } - for (const [replaces, candidates] of candidatesByReplaces.entries()) { - if (child.has(replaces)) continue; - let winner = candidates[0]; - for (let i = 1; i < candidates.length; i++) { - const c = candidates[i]; - if (c.isRootAuthor && !winner.isRootAuthor) { winner = c; continue; } - if (winner.isRootAuthor && !c.isRootAuthor) continue; - const wt = winner.entry._ts || 0; - const ct = c.entry._ts || 0; - if (ct < wt) winner = c; - else if (ct === wt && c.k < winner.k) winner = c; - } - parent.set(winner.k, replaces); - child.set(replaces, winner.k); - tribes.set(winner.k, winner.entry); - rootByTip.set(winner.k, winner.root); - progress = true; + if (!candidates.length) continue; + let winner = candidates[0]; + for (let i = 1; i < candidates.length; i++) { + const c = candidates[i]; + if (c.isRootAuthor && !winner.isRootAuthor) { winner = c; continue; } + if (winner.isRootAuthor && !c.isRootAuthor) continue; + const wt = winner.entry._ts || 0; + const ct = c.entry._ts || 0; + if (ct < wt) winner = c; + else if (ct === wt && c.k < winner.k) winner = c; } + parent.set(winner.k, replaces); + child.set(replaces, winner.k); + tribes.set(winner.k, winner.entry); + rootByTip.set(winner.k, winner.root); + queue.push(winner.k); } const tombstoned = new Set(); @@ -197,37 +235,34 @@ module.exports = ({ cooler, tribeCrypto }) => { } const rootOf = (id) => rootByTip.get(id) || id; - const tipOf = (id) => { let cur = id; while (child.has(cur)) cur = child.get(cur); return cur; }; const tipByRoot = new Map(); for (const k of tribes.keys()) { const root = rootOf(k); - const tip = tipOf(root); + if (tipByRoot.has(root)) continue; + let tip = root; + while (child.has(tip)) tip = child.get(tip); tipByRoot.set(root, tip); } const effectivelyTombstoned = new Set(tombstoned); - let cascade = true; - const seen = new Set(); - while (cascade) { - cascade = false; - for (const k of tribes.keys()) { - if (effectivelyTombstoned.has(k)) continue; - const root = rootOf(k); - if (effectivelyTombstoned.has(root)) { effectivelyTombstoned.add(k); cascade = true; continue; } + const deadMemo = new Map(); + const isDead = (k) => { + if (tombstoned.has(k)) return true; + if (deadMemo.has(k)) return deadMemo.get(k); + deadMemo.set(k, false); + const root = rootOf(k); + let dead = tombstoned.has(tipByRoot.get(root)) || (root !== k && isDead(root)); + if (!dead) { const e = tribes.get(k); const pid = e && e.content && e.content.parentTribeId; - if (!pid) continue; - if (seen.has(`${k}:${pid}`)) continue; - seen.add(`${k}:${pid}`); - const parentRoot = rootOf(pid); - if (effectivelyTombstoned.has(parentRoot) || effectivelyTombstoned.has(pid)) { - effectivelyTombstoned.add(k); - cascade = true; - } + if (pid) dead = isDead(rootOf(pid)) || isDead(pid); } - } + deadMemo.set(k, dead); + return dead; + }; + for (const k of tribes.keys()) if (isDead(k)) effectivelyTombstoned.add(k); - tribeIndex = { tribes, tombstoned, effectivelyTombstoned, parent, child, tipByRoot, rootByTip }; + tribeIndex = { tribes, tombstoned, effectivelyTombstoned, parent, child, tipByRoot, rootByTip, sealedInvites }; tribeIndexTs = Date.now(); return tribeIndex; }; @@ -253,8 +288,9 @@ module.exports = ({ cooler, tribeCrypto }) => { }; })(); - const normalizeTribe = (entry) => { + const normalizeTribe = (entry, rootEntry) => { const c = entry.content; + const root = rootEntry || entry; return { id: entry.id, title: c.title || '', @@ -269,9 +305,9 @@ module.exports = ({ cooler, tribeCrypto }) => { status: c.status || 'OPEN', parentTribeId: c.parentTribeId || null, mapUrl: c.mapUrl || '', - createdAt: c.createdAt, + createdAt: root.content.createdAt, updatedAt: c.updatedAt, - author: c.author || entry.author, + author: root.author, _ts: entry._ts }; }; @@ -290,28 +326,95 @@ module.exports = ({ cooler, tribeCrypto }) => { const scanOpenInviteMarkers = async () => { const msgs = await streamLog(); - const tombstoned = new Set(); - for (const m of msgs) { - const c = m.value && m.value.content; - if (!c) continue; - if (c.type === 'tribe-open-invite-tombstone' && typeof c.target === 'string') tombstoned.add(c.target); - if (c.type === 'tribe-invite-tombstone' && typeof c.target === 'string') tombstoned.add('inv:' + c.target); - } - const byRoot = new Map(); + const idx = await buildTribeIndex(); + const tombBy = new Map(); + const markTomb = (target, author) => { + if (!tombBy.has(target)) tombBy.set(target, new Set()); + tombBy.get(target).add(author); + }; + const markers = []; for (const m of msgs) { const v = m.value; const c = v && v.content; - if (!c || c.type !== 'tribe-open-invite' || c.v !== 1) continue; + if (!c) continue; + if (c.type === 'tribe-open-invite-tombstone' && typeof c.target === 'string') markTomb(c.target, v.author); + else if (c.type === 'tribe-invite-tombstone' && typeof c.target === 'string') markTomb('inv:' + c.target, v.author); + else if (c.type === 'tribe-open-invite' && c.v === 1) markers.push({ key: m.key, author: v.author, body: c, sealed: false, ts: v.timestamp || 0 }); + } + for (const e of idx.sealedInvites || []) { + if (e.body.k === 'open-invite-tombstone' && typeof e.body.target === 'string') markTomb(e.body.target, e.author); + else if (e.body.k === 'open-invite' && e.body.rootId === e.keyRoot) markers.push({ key: e.key, author: e.author, body: e.body, sealed: true, ts: e.ts || 0 }); + } + const killed = (target, allowed) => { + const by = tombBy.get(target); + return !!by && allowed.some((a) => a && by.has(a)); + }; + const byRoot = new Map(); + for (const mk of markers) { + const c = mk.body; if (typeof c.rootId !== 'string' || typeof c.code !== 'string') continue; - if (tombstoned.has(m.key)) continue; - if (c.inviteKey && tombstoned.has('inv:' + c.inviteKey)) continue; - const ts = (v && v.timestamp) || 0; + const rootEntry = idx.tribes.get(c.rootId); + if (!rootEntry || !mk.author) continue; + const tip = idx.tribes.get(idx.tipByRoot.get(c.rootId) || c.rootId) || rootEntry; + if (tip.content.isAnonymous !== false && !mk.sealed) continue; + const members = Array.isArray(tip.content.members) ? tip.content.members : []; + if (mk.author !== rootEntry.author && !members.includes(mk.author)) continue; + const allowed = [mk.author, rootEntry.author]; + if (killed(mk.key, allowed)) continue; + if (c.inviteKey && killed('inv:' + c.inviteKey, allowed)) continue; const prev = byRoot.get(c.rootId); - if (!prev || ts > prev.ts) byRoot.set(c.rootId, { code: c.code, by: c.by || v.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }); + if (!prev || mk.ts > prev.ts) byRoot.set(c.rootId, { code: c.code, by: c.by || mk.author, markerKey: mk.key, inviteKey: c.inviteKey || null, sealed: mk.sealed, ts: mk.ts }); } return byRoot; }; + const findInvite = async (code) => { + const msgs = await streamLog(); + const inviteTombstonedBy = new Map(); + const clearOpenInvites = new Set(); + for (const m of msgs) { + const v = m.value; + const c = v && v.content; + if (!c) continue; + if (c.type === 'tribe-open-invite' && typeof c.inviteKey === 'string') { clearOpenInvites.add(c.inviteKey); continue; } + if (c.type !== 'tribe-invite-tombstone' || typeof c.target !== 'string') continue; + if (!inviteTombstonedBy.has(c.target)) inviteTombstonedBy.set(c.target, new Set()); + inviteTombstonedBy.get(c.target).add(v.author); + } + let matched = null; + for (const m of msgs) { + const v = m.value; + const c = v && v.content; + if (!c || c.type !== 'tribe-invite-msg' || c.v !== 1) continue; + if (typeof c.ch !== 'string' || typeof c.s !== 'string' || typeof c.ek !== 'string') continue; + const multi = c.multi === 1 || c.multi === true; + if (!multi && inviteTombstonedBy.has(m.key)) continue; + if (tribeCrypto.hashInviteCode(code, c.s) !== c.ch) continue; + const chain = tribeCrypto.decryptChainFromInvite(c.ek, code, c.s, 3); + if (Array.isArray(chain) && chain.length) { + matched = { msgKey: m.key, codeHash: c.ch, chain, multi, inviteAuthor: v.author }; + break; + } + } + if (!matched) return null; + const fresh = matched.chain.filter(entry => entry && Array.isArray(entry.keys) && entry.keys.length && !tribeCrypto.getKeys(entry.rootId).length); + for (const entry of fresh) tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + tribeIndex = null; + const undo = () => { for (const entry of fresh) tribeCrypto.dropKey(entry.rootId); tribeIndex = null; }; + const rootId = matched.chain[0].rootId; + const idx = await buildTribeIndex().catch(() => null); + const entry = idx && idx.tribes.get(idx.tipByRoot.get(rootId) || rootId); + const tribe = entry && !idx.effectivelyTombstoned.has(rootId) ? normalizeTribe(entry, idx.tribes.get(rootId)) : null; + if (!tribe) { undo(); return null; } + if (matched.multi) { + const by = inviteTombstonedBy.get(matched.msgKey); + const revoked = !!by && (by.has(matched.inviteAuthor) || by.has(tribe.author)); + const exposed = tribe.isAnonymous && clearOpenInvites.has(matched.msgKey); + if (revoked || exposed) { undo(); return null; } + } + return { matched, rootId, tribe, fresh, undo }; + }; + return { type: 'tribe', @@ -359,6 +462,27 @@ module.exports = ({ cooler, tribeCrypto }) => { return result; }, + async departures(tribeId) { + const idx = await buildTribeIndex(); + const root = idx.rootByTip.get(tribeId) || tribeId; + const out = new Map(); + const rootAuthor = idx.tribes.get(root) && idx.tribes.get(root).author; + let cur = root; + let prev = null; + let g = 0; + while (cur && g++ < 100000) { + const e = idx.tribes.get(cur); + if (!e) break; + const members = Array.isArray(e.content.members) ? e.content.members : []; + const noted = rootAuthor && e.author === rootAuthor && e.content.removedSeqs ? e.content.removedSeqs : {}; + if (prev) for (const m of prev) if (!members.includes(m)) out.set(m, e.author === m ? { seq: e.seq || 0 } : (Number.isFinite(Number(noted[m])) && noted[m] !== null ? { seq: Number(noted[m]) } : { ts: e._ts || 0 })); + for (const m of members) out.delete(m); + prev = members; + cur = idx.child.get(cur); + } + return out; + }, + async getRootId(tribeId) { const idx = await buildTribeIndex(); let cur = tribeId; @@ -400,7 +524,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (idx.tombstoned.has(latestId) || idx.effectivelyTombstoned.has(latestId)) throw new Error('Tribe not found'); const entry = idx.tribes.get(latestId); if (!entry) throw new Error('Tribe not found'); - return normalizeTribe(entry); + return normalizeTribe(entry, idx.tribes.get(idx.rootByTip.get(latestId) || latestId)); }, async listAll() { @@ -411,7 +535,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (idx.effectivelyTombstoned.has(root) || idx.effectivelyTombstoned.has(tip)) continue; const entry = idx.tribes.get(tip); if (!entry) continue; - items.push(normalizeTribe(entry)); + items.push(normalizeTribe(entry, idx.tribes.get(root))); } return items; }, @@ -452,6 +576,22 @@ module.exports = ({ cooler, tribeCrypto }) => { return out; }, + async wasEverMember(userId, tribeId) { + if (!userId || !tribeId) return false; + const idx = await buildTribeIndex(); + let root = tribeId; + while (idx.parent.has(root)) root = idx.parent.get(root); + for (const entry of idx.tribes.values()) { + let r = entry.id; + while (idx.parent.has(r)) r = idx.parent.get(r); + if (r !== root) continue; + if (entry.author === userId) return true; + const members = entry.content && Array.isArray(entry.content.members) ? entry.content.members : []; + if (members.includes(userId)) return true; + } + return false; + }, + async isTribeMember(userId, tribeId) { if (!userId || !tribeId) return false; try { @@ -512,7 +652,9 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: updatedContent.mapUrl !== undefined ? updatedContent.mapUrl : tribe.mapUrl, createdAt: tribe.createdAt, updatedAt: now, - author: tribe.author + author: tribe.author, + ...(typeof updatedContent.joinedWith === 'string' && updatedContent.joinedWith ? { joinedWith: updatedContent.joinedWith } : {}), + ...(updatedContent.removedSeqs && typeof updatedContent.removedSeqs === 'object' ? { removedSeqs: updatedContent.removedSeqs } : {}) }; if (fields.isAnonymous) { return wrapAndPublish(rootId, { k: 'tribe', op: 'update', rootId, replaces: tipId, ...fields }); @@ -533,7 +675,15 @@ module.exports = ({ cooler, tribeCrypto }) => { async updateTribeMembers(tribeId, members) { const tribe = await this.getTribeById(tribeId); const old = tribe.members || []; - await this.updateTribeById(tribeId, { members }); + const leaving = old.filter(m => !members.includes(m)); + const removedSeqs = {}; + if (leaving.length) { + const client = await openSsb(); + for (const m of leaving) { + removedSeqs[m] = await new Promise((resolve) => pull(client.createUserStream({ id: m, reverse: true, limit: 1 }), pull.collect((err, list) => resolve(!err && list && list[0] && list[0].value ? Number(list[0].value.sequence) || 0 : 0)))); + } + } + await this.updateTribeById(tribeId, { members, ...(leaving.length ? { removedSeqs } : {}) }); const removed = old.filter(m => !members.includes(m)); const added = members.filter(m => !old.includes(m)); if (removed.length > 0) { @@ -589,7 +739,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const root = await this.getRootId(tribeId).catch(() => tribeId); const map = await scanOpenInviteMarkers(); const rec = map.get(root); - return rec ? { code: rec.code, by: rec.by, markerKey: rec.markerKey, inviteKey: rec.inviteKey } : null; + return rec ? { code: rec.code, by: rec.by, markerKey: rec.markerKey, inviteKey: rec.inviteKey, sealed: rec.sealed } : null; }, async enrichOpenInvites(tribes) { @@ -627,9 +777,14 @@ module.exports = ({ cooler, tribeCrypto }) => { const invitePub = await new Promise((resolve, reject) => client.publish({ type: 'tribe-invite-msg', v: 1, multi: 1, ch: codeHash, s: salt, ek: ekChain }, (err, r) => err ? reject(err) : resolve(r)) ); - await new Promise((resolve, reject) => - client.publish({ type: 'tribe-open-invite', v: 1, rootId: targetRoot, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (err, r) => err ? reject(err) : resolve(r)) - ); + const marker = { v: 1, rootId: targetRoot, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }; + if (tribe.isAnonymous) { + await wrapAndPublish(targetRoot, { k: 'open-invite', ...marker }); + } else { + await new Promise((resolve, reject) => + client.publish({ type: 'tribe-open-invite', ...marker }, (err, r) => err ? reject(err) : resolve(r)) + ); + } tribeIndex = null; return code; }, @@ -643,56 +798,61 @@ module.exports = ({ cooler, tribeCrypto }) => { if (rec.by !== userId && !(tribe && tribe.author === userId)) { throw new Error('Not allowed to remove this invitation'); } - await new Promise((resolve, reject) => - client.publish({ type: 'tribe-open-invite-tombstone', v: 1, target: rec.markerKey, ts: new Date().toISOString() }, (err, r) => err ? reject(err) : resolve(r)) - ); + const ts = new Date().toISOString(); + if (rec.sealed) { + await wrapAndPublish(await this.getRootId(tribeId), { k: 'open-invite-tombstone', target: rec.markerKey, ts }); + } else { + await new Promise((resolve, reject) => + client.publish({ type: 'tribe-open-invite-tombstone', v: 1, target: rec.markerKey, ts }, (err, r) => err ? reject(err) : resolve(r)) + ); + } if (rec.inviteKey) await this.publishInviteTombstone(rec.inviteKey).catch(() => {}); tribeIndex = null; }, + async previewInvite(rawCode) { + const found = await findInvite(String(rawCode || '').trim()); + if (!found) return null; + found.undo(); + return { rootId: found.rootId, title: found.tribe.title, tags: found.tribe.tags }; + }, + async joinByInvite(rawCode) { const code = String(rawCode || '').trim(); if (!code) throw new Error('Invalid or expired invite code'); const client = await openSsb(); const userId = client.id; - const msgs = await streamLog(); - const inviteTombstoned = new Set(); - for (const m of msgs) { - const v = m.value; - const c = v && v.content; - if (!c || c.type !== 'tribe-invite-tombstone' || typeof c.target !== 'string') continue; - inviteTombstoned.add(c.target); - } - let matched = null; - for (const m of msgs) { - const v = m.value; - const c = v && v.content; - if (!c || c.type !== 'tribe-invite-msg' || c.v !== 1) continue; - if (typeof c.ch !== 'string' || typeof c.s !== 'string' || typeof c.ek !== 'string') continue; - if (inviteTombstoned.has(m.key)) continue; - if (tribeCrypto.hashInviteCode(code, c.s) !== c.ch) continue; - const chain = tribeCrypto.decryptChainFromInvite(c.ek, code, c.s); - if (Array.isArray(chain) && chain.length) { - matched = { msgKey: m.key, codeHash: c.ch, chain, multi: c.multi === 1 || c.multi === true }; - break; - } - } - if (!matched) throw new Error('Invalid or expired invite code'); - for (const entry of matched.chain) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); - } - tribeIndex = null; - const rootId = matched.chain[0].rootId; + const found = await findInvite(code); + if (!found) throw new Error('Invalid or expired invite code'); + const { matched, rootId, fresh, undo } = found; let tribe; try { tribe = await this.getTribeById(rootId); } catch (_) { tribe = null; } - if (!tribe) throw new Error('Tribe not found after key import'); - if (tribe.members.includes(userId)) throw new Error('Already a member of this tribe'); + if (!tribe) { undo(); throw new Error('Tribe not found'); } + if (tribe.members.includes(userId)) { undo(); throw new Error('Already a member of this tribe'); } + const lineage = new Set([rootId]); + let cursor = tribe; + for (let hops = 0; cursor && cursor.parentTribeId && hops < 8; hops++) { + let parentTribe = null; + try { parentTribe = await this.getTribeById(cursor.parentTribeId); } catch (_) { parentTribe = null; } + if (!parentTribe) break; + const parentCircle = [parentTribe.author, ...(Array.isArray(parentTribe.members) ? parentTribe.members : [])]; + if (!parentCircle.includes(cursor.author)) break; + lineage.add(parentTribe.id); + lineage.add(cursor.parentTribeId); + cursor = parentTribe; + } + for (const entry of matched.chain) { + if (!entry || !Array.isArray(entry.keys) || !entry.keys.length) continue; + if (fresh.includes(entry)) { if (!lineage.has(entry.rootId)) tribeCrypto.dropKey(entry.rootId); continue; } + if (lineage.has(entry.rootId)) tribeCrypto.mergeKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + } + tribeIndex = null; const members = [...tribe.members, userId]; if (matched.multi) { - await this.updateTribeById(tribe.id, { members }); + await this.updateTribeById(tribe.id, { members, joinedWith: matched.codeHash }); } else { const invites = (tribe.invites || []).filter(inv => inv.codeHash !== matched.codeHash); - await this.updateTribeById(tribe.id, { members, invites }); + await this.updateTribeById(tribe.id, { members, invites, joinedWith: matched.codeHash }); await this.publishInviteTombstone(matched.msgKey).catch(() => {}); } await this.ensureFollowTribeMembers(tribe.id).catch(() => {}); @@ -728,9 +888,6 @@ module.exports = ({ cooler, tribeCrypto }) => { return; } await this.updateTribeById(tribeId, { members }); - if (members.length > 0) { - await this.rotateTribeKey(tribeId, members).catch(() => {}); - } }, async distributeTribeKey(tribeId, toMembers) { @@ -741,13 +898,16 @@ module.exports = ({ cooler, tribeCrypto }) => { const gen = tribeCrypto.getGen(rootId); if (!keys.length) return; const payload = tribeCrypto.buildKeyDistribPayload(rootId, keys, gen); - const batch = tribeCrypto.KEY_DISTRIB_BATCH; - for (let i = 0; i < toMembers.length; i += batch) { - const recps = toMembers.slice(i, i + batch); + const others = [...new Set(toMembers)].filter(m => m && m !== client.id); + const batch = tribeCrypto.KEY_DISTRIB_BATCH - 1; + let i = 0; + do { + const recps = [client.id, ...others.slice(i, i + batch)]; await new Promise((resolve, reject) => client.publish({ ...payload, recps }, (err) => err ? reject(err) : resolve()) ); - } + i += batch; + } while (i < others.length); }, async rotateTribeKey(tribeId, remainingMembers) { @@ -770,38 +930,41 @@ module.exports = ({ cooler, tribeCrypto }) => { const rootId = await this.getRootId(tribeId); if (!tribeCrypto.getKey(rootId)) return; const others = (tribe.members || []).filter(m => m !== userId); - if (!others.length) return; - const ssbKeys = require('../server/node_modules/ssb-keys'); - const config = require('../server/ssb_config'); - const msgs = await streamLog(); + const { distribs } = await readKeyDistribs(); const distributed = new Set(); - for (const m of msgs) { - if (m.value && m.value.author !== userId) continue; - const c = m.value && m.value.content; - const dec = tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys); - if (!dec || dec.rootId !== rootId) continue; + const currentGen = tribeCrypto.getGen(rootId); + const holdersOfCurrent = new Set(); + for (const { m, dec } of distribs) { + if (m.value.author !== userId) continue; + if (dec.rootId !== rootId) continue; const recps = Array.isArray(dec.recps) ? dec.recps : []; for (const r of recps) distributed.add(r); + if (Number(dec.gen) === Number(currentGen)) for (const r of recps) holdersOfCurrent.add(r); } + const departed = [...holdersOfCurrent].filter(r => r !== userId && !(tribe.members || []).includes(r)); + if (departed.length > 0) { await this.rotateTribeKey(tribeId, others).catch(() => {}); return; } + if (!others.length) return; const missing = others.filter(m => !distributed.has(m)); if (missing.length > 0) await this.distributeTribeKey(tribeId, missing).catch(() => {}); }, async processIncomingKeys() { - const client = await openSsb(); - const ssbKeys = require('../server/node_modules/ssb-keys'); - const config = require('../server/ssb_config'); - const msgs = await streamLog(); + const { msgs, distribs } = await readKeyDistribs(); const byTribe = new Map(); - for (const m of msgs) { - const c = m.value && m.value.content; - const dec = tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys); - if (!dec) continue; + const authorOf = new Map(); + for (const m of msgs) if (m && m.key && m.value) authorOf.set(m.key, m.value.author); + for (const { m, dec } of distribs) { const list = byTribe.get(dec.rootId) || []; - list.push({ generation: dec.gen || dec.keys.length, keys: dec.keys, ts: m.value.timestamp }); + list.push({ generation: dec.gen || dec.keys.length, keys: dec.keys, ts: m.value.timestamp, author: m.value.author }); byTribe.set(dec.rootId, list); } - for (const [rootId, entries] of byTribe.entries()) { + for (const [rootId, all] of byTribe.entries()) { + const owner = authorOf.get(rootId); + if (!owner) continue; + let members = []; + try { const t = await this.getTribeById(rootId); members = Array.isArray(t && t.members) ? t.members : []; } catch (_) {} + const allowed = new Set([owner, ...members]); + const entries = all.filter(e => allowed.has(e.author)); entries.sort((a, b) => b.generation - a.generation); const top = entries[0]; if (top && Array.isArray(top.keys) && top.keys.length) { @@ -929,7 +1092,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const all = tribeCrypto.getAllRootIds(); let removed = 0; for (const rid of all) { - if (!idx.tribes.has(rid) || idx.effectivelyTombstoned.has(rid)) { + if (idx.effectivelyTombstoned.has(rid)) { try { tribeCrypto.dropKey(rid); removed++; } catch (_) {} } } diff --git a/src/models/typed_log.js b/src/models/typed_log.js new file mode 100644 index 0000000..e7d4d60 --- /dev/null +++ b/src/models/typed_log.js @@ -0,0 +1,285 @@ +const pull = require('../server/node_modules/pull-stream'); +const { AsyncLocalStorage } = require('async_hooks'); + +const requestScope = new AsyncLocalStorage(); +const noteCapped = (limit, size) => { + if (!limit || !(size >= limit)) return; + const store = requestScope.getStore(); + if (store) { store.capped = true; store.limit = limit; } +}; + +const TAIL_PROBE = 50; +const LOG_TAIL_PROBE = 20; + +const collectStream = (stream) => + new Promise((resolve, reject) => { + pull(stream, pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))); + }); + +const isDeletion = (m) => { + const c = m && m.value && m.value.content; + return !!c && typeof c === 'object' && c.type === 'tombstone'; +}; + +const readContentWindow = (ssb, limit) => { + if (!limit) return collectStream(ssb.createLogStream({ reverse: true })); + let kept = 0; + return collectStream(pull( + ssb.createLogStream({ reverse: true }), + pull.take((m) => { if (!isDeletion(m)) kept++; return kept <= limit; }) + )); +}; + +const caches = new WeakMap(); + +const cacheFor = (ssb) => { + let c = caches.get(ssb); + if (!c) { + c = { types: new Map(), window: null, results: new Map() }; + caches.set(ssb, c); + } + return c; +}; + +const insert = (entry, msgs) => { + for (let i = msgs.length - 1; i >= 0; i--) { + const m = msgs[i]; + if (m && m.key && !entry.byKey.has(m.key)) entry.byKey.set(m.key, m); + } +}; + +const UNCAPPED_TYPES = new Set(['tombstone']); +const syncType = async (ssb, cache, type, requestedLimit) => { + const limit = UNCAPPED_TYPES.has(type) ? 0 : requestedLimit; + let entry = cache.types.get(type); + if (!entry) { + entry = { byKey: new Map(), warming: null, warm: false }; + cache.types.set(type, entry); + entry.warming = collectStream(ssb.messagesByType(limit ? { type, reverse: true, limit } : { type, reverse: true })) + .then((msgs) => { insert(entry, msgs); entry.warm = true; entry.capped = !!(limit && msgs.length >= limit); }) + .catch(() => { cache.types.delete(type); }); + await entry.warming; + return entry; + } + await entry.warming; + if (!entry.warm) return entry; + const tail = await collectStream(ssb.messagesByType({ type, reverse: true, limit: TAIL_PROBE })); + const fresh = tail.filter((m) => m && m.key && !entry.byKey.has(m.key)); + if (fresh.length === tail.length && tail.length === TAIL_PROBE) { + const all = await collectStream(ssb.messagesByType(limit ? { type, reverse: true, limit } : { type, reverse: true })); + insert(entry, all); + entry.capped = !!(limit && all.length >= limit); + } else { + insert(entry, fresh); + } + return entry; +}; + +const syncWindow = async (ssb, cache, limit) => { + let entry = cache.window; + if (!entry) { + entry = { byKey: new Map(), warming: null, warm: false }; + cache.window = entry; + entry.warming = readContentWindow(ssb, limit) + .then((msgs) => { insert(entry, msgs); entry.warm = true; }) + .catch(() => { cache.window = null; }); + await entry.warming; + return entry; + } + await entry.warming; + if (!entry.warm) return entry; + const tail = await collectStream(ssb.createLogStream({ reverse: true, limit: TAIL_PROBE })); + const fresh = tail.filter((m) => m && m.key && !entry.byKey.has(m.key)); + if (fresh.length === tail.length && tail.length === TAIL_PROBE) { + const all = await readContentWindow(ssb, limit); + insert(entry, all); + } else { + insert(entry, fresh); + } + return entry; +}; + +const syncPrivate = async (ssb, cache) => { + if (!ssb.private || typeof ssb.private.read !== 'function') return null; + let entry = cache.private; + if (!entry) { + entry = { byKey: new Map(), warming: null, warm: false }; + cache.private = entry; + entry.warming = collectStream(ssb.private.read({ reverse: true })) + .then((msgs) => { insert(entry, msgs); entry.warm = true; }) + .catch(() => { cache.private = null; }); + await entry.warming; + return entry; + } + await entry.warming; + if (!entry.warm) return entry; + const tail = await collectStream(ssb.private.read({ reverse: true, limit: TAIL_PROBE })); + const fresh = tail.filter((m) => m && m.key && !entry.byKey.has(m.key)); + if (fresh.length === tail.length && tail.length === TAIL_PROBE) { + insert(entry, await collectStream(ssb.private.read({ reverse: true }))); + } else { + insert(entry, fresh); + } + return entry; +}; + +const logTailOf = async (ssb, n) => { + if (ssb.activity && typeof ssb.activity.tail === 'function') { + const store = requestScope.getStore(); + const fresh = !(store && store.method === 'GET'); + const fast = await new Promise((resolve) => { + try { ssb.activity.tail(n, { fresh }, (err, arr) => resolve(err ? null : arr)); } catch (_) { resolve(null); } + }); + if (Array.isArray(fast)) return fast; + } + return collectStream(ssb.createLogStream({ reverse: true, limit: n })); +}; + +const readTyped = async (ssbClient, types, opts = {}) => { + const limit = opts.limit; + if (typeof ssbClient.messagesByType !== 'function') { + const all = await collectStream(ssbClient.createLogStream(limit ? { limit } : {})); + noteCapped(limit, all.length); + return all; + } + const cache = cacheFor(ssbClient); + const wanted = new Set(types); + + const logTail = (await logTailOf(ssbClient, LOG_TAIL_PROBE)).reverse(); + const tipKey = logTail.length ? logTail[logTail.length - 1].key : null; + const inTail = new Set(logTail.map((m) => m && m.key)); + const upToDate = (e) => !!e && e.warm && e.tip === tipKey; + const caughtUp = (e) => !!e && e.warm && !!e.tip && (e.tip === tipKey || inTail.has(e.tip)); + const allWarm = types.every((type) => upToDate(cache.types.get(type))) + && (!opts.withWindow || upToDate(cache.window)); + + const entries = await Promise.all(types.map((type) => { + const e = cache.types.get(type); + return caughtUp(e) ? e : syncType(ssbClient, cache, type, limit); + })); + const windowEntry = opts.withWindow + ? (caughtUp(cache.window) ? cache.window : await syncWindow(ssbClient, cache, limit)) + : null; + const withPrivate = opts.withPrivate === true; + const resultKey = `${types.join(',')}|${limit || 0}|${opts.withWindow ? 1 : 0}|${withPrivate ? 1 : 0}`; + if (allWarm && (!withPrivate || !wanted.size || upToDate(cache.private))) { + const hit = cache.results.get(resultKey); + if (hit && hit.tip === tipKey) { + if (hit.capped) noteCapped(limit, limit); + return hit.list.slice(); + } + } + const privateEntry = withPrivate && wanted.size + ? (!upToDate(cache.private) ? await syncPrivate(ssbClient, cache) : cache.private) + : null; + for (const e of [...entries, windowEntry, privateEntry]) if (e && e.warm) e.tip = tipKey; + + for (const m of logTail) { + if (!m || !m.key || !m.value) continue; + const t = m.value.content && m.value.content.type; + if (typeof t === 'string' && wanted.has(t)) { + const entry = cache.types.get(t); + if (entry && entry.warm && !entry.byKey.has(m.key)) entry.byKey.set(m.key, m); + } + if (windowEntry && windowEntry.warm && !windowEntry.byKey.has(m.key)) windowEntry.byKey.set(m.key, m); + } + + const union = new Map(); + let capped = false; + for (const entry of entries) { + if (!entry) continue; + if (entry.capped) { capped = true; noteCapped(limit, limit); } + for (const [k, m] of entry.byKey) if (!union.has(k)) union.set(k, m); + } + if (windowEntry) { + for (const [k, m] of windowEntry.byKey) if (!union.has(k)) union.set(k, m); + } + if (privateEntry && privateEntry.warm) { + for (const [k, m] of privateEntry.byKey) { + const t = m && m.value && m.value.content && m.value.content.type; + if (typeof t === 'string' && wanted.has(t)) union.set(k, m); + } + } + const sorted = Array.from(union.values()).sort((a, b) => { + const at = (a.value && a.value.timestamp) || 0; + const bt = (b.value && b.value.timestamp) || 0; + if (at !== bt) return at - bt; + const ar = a.timestamp || 0; + const br = b.timestamp || 0; + if (ar !== br) return ar - br; + if (a.value && b.value && a.value.author === b.value.author) return (a.value.sequence || 0) - (b.value.sequence || 0); + return 0; + }); + const complete = entries.every((e) => e && e.warm) + && (!windowEntry === !opts.withWindow) && (!windowEntry || windowEntry.warm) + && (!(withPrivate && wanted.size) || (privateEntry && privateEntry.warm)); + if (tipKey && complete) cache.results.set(resultKey, { tip: tipKey, capped, list: sorted }); + return sorted.slice(); +}; + +const NON_MESSAGE_LITERALS = new Set([ + 'hidden', 'submit', 'text', 'file', 'number', 'checkbox', 'radio', 'password', 'date', 'datetime-local', 'time', 'url', 'email', 'search', 'button', 'reset', + 'png', 'jpeg', 'jpg', 'gif', 'svg', 'webp', 'pdf', 'zip', 'json', 'html', 'error', 'meta', 'msg', 'blob', 'peer', 'inhabitant', 'chatThread', 'taskAssignment' +]); +const CORE_TYPES = ['post', 'about', 'contact', 'vote', 'pub', 'tombstone', 'file', 'textChunk']; + +const discoverContentTypes = () => { + const fs = require('fs'); + const path = require('path'); + const found = new Set(CORE_TYPES); + const sources = []; + try { + for (const f of fs.readdirSync(__dirname)) if (f.endsWith('.js') && f !== 'typed_log.js') sources.push(path.join(__dirname, f)); + sources.push(path.join(__dirname, '..', 'backend', 'backend.js')); + } catch (_) {} + for (const file of sources) { + let src = ''; + try { src = fs.readFileSync(file, 'utf8'); } catch (_) { continue; } + for (const m of src.matchAll(/\btype:\s*['"]([A-Za-z][\w-]*)['"]/g)) { + if (!NON_MESSAGE_LITERALS.has(m[1])) found.add(m[1]); + } + for (const m of src.matchAll(/\b(?:const|let)\s+(?:[A-Z][A-Z0-9_]*_)?TYPE\s*=\s*['"]([A-Za-z][\w-]*)['"]/g)) { + if (!NON_MESSAGE_LITERALS.has(m[1])) found.add(m[1]); + } + } + return Array.from(found).sort(); +}; + +const CONTENT_TYPES = discoverContentTypes(); + +const logTip = async (ssb) => { + const tail = await logTailOf(ssb, 1); + return tail[0] && tail[0].key ? tail[0].key : null; +}; + +const activityCache = new WeakMap(); +const ACTIVITY_TTL_MS = 5000; +const authorActivity = (ssb) => { + if (!ssb || !ssb.activity || typeof ssb.activity.latest !== 'function') return Promise.resolve(null); + const hit = activityCache.get(ssb); + if (hit && Date.now() - hit.at < ACTIVITY_TTL_MS) return hit.pending; + const store = requestScope.getStore(); + const fresh = !(store && store.method === 'GET'); + const pending = new Promise((resolve) => { + try { ssb.activity.latest(null, { fresh }, (err, all) => resolve(err || !all ? null : new Map(Object.entries(all)))); } catch (_) { resolve(null); } + }); + activityCache.set(ssb, { at: Date.now(), pending }); + return pending; +}; + +const memoIndex = (name, messages, build) => { + const store = requestScope.getStore(); + if (!store) return build(messages); + const list = Array.isArray(messages) ? messages : []; + const first = list[0]; + const last = list[list.length - 1]; + const sig = `${list.length}|${first && first.key}|${last && last.key}`; + if (!store.indexMemo) store.indexMemo = new Map(); + const hit = store.indexMemo.get(name); + if (hit && hit.sig === sig) return hit.idx; + const idx = build(messages); + store.indexMemo.set(name, { sig, idx }); + return idx; +}; + +module.exports = { readTyped, collectStream, readContentWindow, CONTENT_TYPES, discoverContentTypes, requestScope, memoIndex, logTip, authorActivity }; diff --git a/src/models/videos_model.js b/src/models/videos_model.js index 24b8a99..d9bc7a3 100644 --- a/src/models/videos_model.js +++ b/src/models/videos_model.js @@ -1,7 +1,8 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); const categories = require("../backend/opinion_categories"); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -31,13 +32,9 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const VIDEO_TYPES = ["video", "videoOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, VIDEO_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -47,9 +44,6 @@ module.exports = ({ cooler }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -65,41 +59,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -116,9 +85,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildVideo = (node, rootId, viewerId, agg) => { @@ -132,9 +101,10 @@ module.exports = ({ cooler }) => { createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, tags: safeArr(c.tags), - author: c.author, + author: node.author, title: c.title || "", description: c.description || "", + license: c.license || "", mapUrl: c.mapUrl || "", opinions, opinions_inhabitants: voters, @@ -161,16 +131,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Video not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Video not found"); return root; }, - async createVideo(blobMarkdown, tagsRaw, title, description, mapUrl) { + async createVideo(blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -187,7 +153,8 @@ module.exports = ({ cooler }) => { description: description || "", mapUrl: mapUrl || "", opinions: {}, - opinions_inhabitants: [] + opinions_inhabitants: [], + ...(license ? { license } : {}) }; return new Promise((resolve, reject) => { @@ -195,7 +162,7 @@ module.exports = ({ cooler }) => { }); }, - async updateVideoById(id, blobMarkdown, tagsRaw, title, description, mapUrl) { + async updateVideoById(id, blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -219,7 +186,8 @@ module.exports = ({ cooler }) => { description: description !== undefined ? description || "" : oldMsg.content.description || "", mapUrl: mapUrl !== undefined ? mapUrl || "" : oldMsg.content.mapUrl || "", createdAt: oldMsg.content.createdAt, - updatedAt: now + updatedAt: now, + ...(license !== undefined ? { license } : {}) }; const tombstone = { type: "tombstone", target: tipId, deletedAt: now, author: userId }; @@ -304,10 +272,10 @@ module.exports = ({ cooler }) => { const ssbClient = await openSsb(); const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('videos', messages, buildIndex); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Video not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Video not found"); return buildVideo(agg.contentNode, rootId, viewer, agg); }, diff --git a/src/models/viewer_filters.js b/src/models/viewer_filters.js index cef023e..d4dc5b7 100644 --- a/src/models/viewer_filters.js +++ b/src/models/viewer_filters.js @@ -3,7 +3,7 @@ const path = require('path'); const { getConfig } = require('../configs/config-manager.js'); const COOLDOWN_MS = 5 * 60 * 1000; -const statePath = path.join(__dirname, '../configs/follow_state.json'); +const statePath = require('../configs/state-manager').statePath('follow_state.json'); const readJson = (p, fallback) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { return fallback; } @@ -21,9 +21,175 @@ const loadState = () => { }; const saveState = (s) => writeJson(statePath, s); +const hiddenPath = require('../configs/state-manager').statePath('viewer_hidden.json'); +let hiddenState = null; +const loadHidden = () => { + if (!hiddenState) { + const raw = readJson(hiddenPath, {}); + const muted = Array.isArray(raw && raw.muted) ? raw.muted : []; + const hidden = Array.isArray(raw && raw.hidden) ? raw.hidden : []; + hiddenState = { + muted: muted.map(x => (typeof x === 'string' ? { id: x, at: 0 } : x)).filter(x => x && typeof x.id === 'string'), + hidden: hidden.filter(x => x && typeof x.id === 'string') + }; + } + return hiddenState; +}; +const saveHidden = () => writeJson(hiddenPath, loadHidden()); + +const mutedList = () => loadHidden().muted.slice(); +const mutedIds = () => new Set(loadHidden().muted.map(x => x.id)); +const isMuted = (id) => !!id && loadHidden().muted.some(x => x.id === id); +const mute = (id) => { + const s = loadHidden(); + if (!id || s.muted.some(x => x.id === id)) return false; + s.muted.push({ id, at: Date.now() }); + saveHidden(); + return true; +}; +const unmute = (id) => { + const s = loadHidden(); + const before = s.muted.length; + s.muted = s.muted.filter(x => x.id !== id); + if (s.muted.length !== before) saveHidden(); + return s.muted.length !== before; +}; + +const hiddenList = () => loadHidden().hidden.slice(); +const hiddenIds = () => new Set(loadHidden().hidden.map(x => x.id)); +const isHidden = (id) => !!id && loadHidden().hidden.some(x => x.id === id); +const hideContent = ({ id, href = '', label = '' } = {}) => { + const s = loadHidden(); + if (!id || s.hidden.some(x => x.id === id)) return false; + s.hidden.push({ id, href: String(href || '').slice(0, 300), label: String(label || '').slice(0, 120), at: Date.now() }); + saveHidden(); + return true; +}; +const unhideContent = (id) => { + const s = loadHidden(); + const before = s.hidden.length; + s.hidden = s.hidden.filter(x => x.id !== id); + if (s.hidden.length !== before) saveHidden(); + return s.hidden.length !== before; +}; + +const idsOfItem = (item) => { + if (!item || typeof item !== 'object') return []; + return [item.id, item.key, item.rootId, item.favId].filter(v => typeof v === 'string'); +}; +const isItemHidden = (item, authorOf, { authors, ids } = {}) => { + if (!item) return false; + const a = typeof authorOf === 'function' ? authorOf(item) : null; + if (a && authors && authors.has(String(a))) return true; + if (ids && ids.size && idsOfItem(item).some(id => ids.has(id))) return true; + return false; +}; + +const lanPeersPath = require('../configs/state-manager').statePath('lan-peers.json'); +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/; +const LAN_SEEN_STEP_MS = 60 * 1000; +let lanSeen = null; + +const canonicalFeedId = (key) => { + let k = String(key || '').trim(); + if (!k) return null; + if (!k.startsWith('@')) k = '@' + k; + if (!k.endsWith('.ed25519')) k += '.ed25519'; + return FEED_ID.test(k) ? k : null; +}; + +const isPrivateHost = (host) => { + const h = String(host || '').replace(/^\[|\]$/g, '').replace(/^::ffff:/i, '').toLowerCase(); + if (!h) return false; + if (/^(10|127)\./.test(h) || /^192\.168\./.test(h) || /^169\.254\./.test(h)) return true; + const m = h.match(/^172\.(\d+)\./); + if (m && Number(m[1]) >= 16 && Number(m[1]) <= 31) return true; + return h === '::1' || /^f[cd][0-9a-f]{0,2}:/.test(h) || /^fe[89ab][0-9a-f]?:/.test(h); +}; + +const hostOfAddress = (address) => { + const net = String(address || '').split('~')[0]; + if (!net.startsWith('net:')) return ''; + const hp = net.slice(4); + const i = hp.lastIndexOf(':'); + return i > 0 ? hp.slice(0, i) : hp; +}; + +const lanKeysFromConn = (entries) => { + const out = []; + for (const e of entries || []) { + if (!Array.isArray(e) || !e[1]) continue; + const [address, data] = e; + const key = canonicalFeedId(data.key); + if (!key) continue; + if (data.type === 'lan' || data.inferredType === 'lan' || isPrivateHost(hostOfAddress(address))) out.push(key); + } + return out; +}; + +const lanPeersSeen = () => { + if (!lanSeen) { + const list = readJson(lanPeersPath, []); + lanSeen = new Map(); + for (const item of (Array.isArray(list) ? list : [])) { + const id = canonicalFeedId(typeof item === 'string' ? item : item && item.key); + if (id) lanSeen.set(id, Number(item && item.seenAt) || 0); + } + } + return lanSeen; +}; + +const lanPeers = () => new Set(lanPeersSeen().keys()); + +const rememberLanPeers = (keys, now = Date.now()) => { + const seen = lanPeersSeen(); + let changed = false; + for (const k of keys || []) { + const id = canonicalFeedId(k); + if (!id) continue; + const prev = seen.get(id); + if (prev === undefined || now - prev >= LAN_SEEN_STEP_MS) { seen.set(id, now); changed = true; } + } + if (changed) writeJson(lanPeersPath, [...seen].map(([key, seenAt]) => ({ key, seenAt }))); + return changed; +}; + +const forgetLanPeers = (keys) => { + const seen = lanPeersSeen(); + let changed = false; + for (const k of keys || []) { + const id = canonicalFeedId(k); + if (id && seen.delete(id)) changed = true; + } + if (changed) writeJson(lanPeersPath, [...seen].map(([key, seenAt]) => ({ key, seenAt }))); + return changed; +}; + +const MUTUAL_CONCURRENCY = 16; +const filterByWish = async (items, { wish, viewer, authorOf: author, isOwn, isMutual, lan } = {}) => { + if (!Array.isArray(items)) return items; + const own = (it) => !!(isOwn && isOwn(it, viewer)); + if (wish === 'local') return items.filter(own); + if (wish === 'only-lan') { + const near = lan || new Set(); + return items.filter(it => { const a = author(it); return !a || a === viewer || own(it) || near.has(a); }); + } + if (wish === 'mutuals' && typeof isMutual === 'function') { + const authors = [...new Set(items.map(author).filter(a => a && a !== viewer))]; + const verdict = new Map(); + let next = 0; + await Promise.all(Array.from({ length: Math.min(MUTUAL_CONCURRENCY, authors.length) }, async () => { + while (next < authors.length) { const a = authors[next++]; verdict.set(a, !!(await isMutual(a))); } + })); + return items.filter(it => { const a = author(it); return !a || a === viewer || verdict.get(a); }); + } + return items; +}; + const wishMutualsOnly = () => getConfig().wish === 'mutuals'; const pmMutualsOnly = () => getConfig().pmVisibility === 'mutuals'; -const isFrictionActive = () => wishMutualsOnly() || pmMutualsOnly(); +const pmFollowingOnly = () => getConfig().pmVisibility === 'following'; +const isFrictionActive = () => wishMutualsOnly() || pmMutualsOnly() || pmFollowingOnly(); const listPending = () => loadState().pending; const enqueuePending = (followerId, extra = {}) => { @@ -102,8 +268,13 @@ const applyMutualSupportFilter = async (items, viewerId, friendModel) => { }; const canSendPmTo = async (viewerId, recipientId, friendModel) => { - if (!pmMutualsOnly()) return { allowed: true }; if (viewerId === recipientId) return { allowed: true }; + if (pmFollowingOnly()) { + let rel = null; + try { rel = friendModel && typeof friendModel.getRelationship === 'function' ? await friendModel.getRelationship(recipientId) : null; } catch (_) { rel = null; } + return rel && rel.following ? { allowed: true } : { allowed: false, reason: 'not-following' }; + } + if (!pmMutualsOnly()) return { allowed: true }; const isMutual = makeMutualCache(friendModel); if (await isMutual(recipientId)) return { allowed: true }; return { allowed: false, reason: 'non-mutual' }; @@ -113,6 +284,7 @@ module.exports = { COOLDOWN_MS, wishMutualsOnly, pmMutualsOnly, + pmFollowingOnly, isFrictionActive, listPending, enqueuePending, @@ -127,4 +299,22 @@ module.exports = { applyMutualSupportFilter, canSendPmTo, authorOf, + isPrivateHost, + lanKeysFromConn, + lanPeers, + lanPeersSeen, + rememberLanPeers, + forgetLanPeers, + filterByWish, + mutedList, + mutedIds, + isMuted, + mute, + unmute, + hiddenList, + hiddenIds, + isHidden, + hideContent, + unhideContent, + isItemHidden, }; diff --git a/src/models/votes_model.js b/src/models/votes_model.js index 7802606..96deddb 100644 --- a/src/models/votes_model.js +++ b/src/models/votes_model.js @@ -5,9 +5,18 @@ const { getConfig } = require('../configs/config-manager.js'); const { dedupeBy, norm } = require('../backend/dedupe'); const { buildVoteTally } = require('../backend/vote_tally'); const categories = require('../backend/opinion_categories'); +const { readTyped, memoIndex } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const MIN_VOTE_DAYS = 7; +const INSTITUTIONAL_VOTE_TAG = /^(gov|govMethod|courtsCase|courtsMethod):/; +const isVoteLocked = (v) => !!v && ( + (parseInt(v.totalVotes || 0, 10) || 0) > 0 + || Object.values(v.opinions || {}).some(n => (Number(n) || 0) > 0) + || (Array.isArray(v.tags) && v.tags.some(t => INSTITUTIONAL_VOTE_TAG.test(String(t)))) +); +const lockedError = () => Object.assign(new Error('Vote locked'), { code: 'VOTE_LOCKED' }); + module.exports = ({ cooler }) => { let ssb; const openSsb = async () => { @@ -17,13 +26,10 @@ module.exports = ({ cooler }) => { const TYPE = 'votes'; + const VOTES_TYPES = [TYPE, 'votesVote', 'votesOpinion', 'tombstone']; + async function getAllMessages(ssbClient) { - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, results) => (err ? reject(err) : resolve(results))) - ); - }); + return readTyped(ssbClient, VOTES_TYPES, { limit: logLimit }); } function buildIndex(messages) { @@ -170,12 +176,18 @@ module.exports = ({ cooler }) => { } return { + MIN_VOTE_DAYS, + async createVote(question, deadline, options = ['YES', 'NO', 'ABSTENTION', 'CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED'], tagsRaw = []) { const ssbClient = await openSsb(); const userId = ssbClient.id; const parsedDeadline = moment(deadline, moment.ISO_8601, true); if (!parsedDeadline.isValid()) throw new Error('Invalid deadline'); - if (parsedDeadline.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) throw new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); + if (parsedDeadline.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) { + const err = new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); + err.code = 'VOTE_DEADLINE_MIN'; + throw err; + } const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) @@ -214,6 +226,7 @@ module.exports = ({ cooler }) => { ssbClient.get(tipId, (err, msg) => (err || !msg ? rej(new Error('Vote not found')) : res(msg))) ); if (!vote.content || vote.content.createdBy !== userId) throw new Error('Not the author'); + if (isVoteLocked(vote.content) || isVoteLocked(await this.getVoteById(id).catch(() => null))) throw lockedError(); const tombstone = { type: 'tombstone', target: tipId, @@ -240,13 +253,17 @@ module.exports = ({ cooler }) => { if (c.createdBy !== userId) throw new Error('Not the author'); const agg = await this.getVoteById(id); const aggTotalVotes = agg ? (parseInt(agg.totalVotes || 0, 10) || 0) : 0; - if (agg && Object.keys(agg.opinions || {}).some(k => (agg.opinions[k] || 0) > 0)) throw new Error('Cannot edit vote after it has received opinions.'); + if (isVoteLocked(c) || isVoteLocked(agg)) throw lockedError(); let newDeadline = c.deadline; if (deadline != null && deadline !== '') { const parsed = moment(deadline, moment.ISO_8601, true); if (!parsed.isValid()) throw new Error('Invalid deadline'); - if (parsed.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) throw new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); + if (parsed.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) { + const err = new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); + err.code = 'VOTE_DEADLINE_MIN'; + throw err; + } newDeadline = parsed.toISOString(); } @@ -314,7 +331,7 @@ module.exports = ({ cooler }) => { async getVoteById(id) { const ssbClient = await openSsb(); const messages = await getAllMessages(ssbClient); - const index = buildIndex(messages); + const index = memoIndex('votes', messages, buildIndex); const activeList = computeActiveVotes(index); const byId = new Map(activeList.map(v => [v.id, v])); @@ -329,19 +346,7 @@ module.exports = ({ cooler }) => { return candidate; } - const msg = await new Promise((res, rej) => - ssbClient.get(id, (err, vote) => (err || !vote ? rej(new Error('Vote not found')) : res(vote))) - ); - - const content = msg.content || {}; - const status = statusFromContent(content, moment()); - - return { - id, - latestId: id, - ...content, - status - }; + throw new Error('Vote not found'); }, async listAll(filter = 'all') { @@ -353,6 +358,8 @@ module.exports = ({ cooler }) => { if (filter === 'mine') { list = list.filter(v => v.createdBy === userId); + } else if (filter === 'recent') { + list = list.filter(v => (Date.parse(v.createdAt || '') || 0) >= Date.now() - 86400000); } else if (filter === 'open') { list = list.filter(v => v.status === 'OPEN'); } else if (filter === 'closed') { @@ -379,4 +386,4 @@ module.exports = ({ cooler }) => { } }; }; - +module.exports.isVoteLocked = isVoteLocked; diff --git a/src/models/wallet_model.js b/src/models/wallet_model.js index cafa6cf..91ad69f 100644 --- a/src/models/wallet_model.js +++ b/src/models/wallet_model.js @@ -1,16 +1,19 @@ -const { - RequestManager, - HTTPTransport, - Client -} = require("../server/node_modules/@open-rpc/client-js"); +const RPC_TIMEOUT_MS = 30000; async function makeClient(url, user, pass) { - const headers = {}; + const headers = { 'Content-Type': 'application/json' }; if (user !== undefined || pass !== undefined) { headers['Authorization'] = 'Basic ' + Buffer.from(`${user || ''}:${pass || ''}`).toString('base64'); } - const transport = new HTTPTransport(url, { headers }); - return new Client(new RequestManager([transport])); + let nextId = 0; + return { + request: async ({ method, params = [] }) => { + const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '1.0', id: ++nextId, method, params }), signal: AbortSignal.timeout(RPC_TIMEOUT_MS) }); + const body = await res.json(); + if (!body || body.error) throw new Error((body && body.error && body.error.message) || `RPC ${res.status}`); + return body.result; + } + }; } module.exports = { @@ -50,8 +53,8 @@ module.exports = { const errors = []; const addrInfo = await module.exports.execute(url, user, pass, "validateaddress", [address]); const addressValid = !!addrInfo?.isvalid; - const amountValid = Number(amount) > 0; - const feeValid = Number(fee) >= 0; + const amountValid = Number.isFinite(Number(amount)) && Number(amount) > 0; + const feeValid = Number.isFinite(Number(fee)) && Number(fee) >= 0; if (!addressValid) errors.push("invalid_dest"); if (!amountValid) errors.push("invalid_amount"); if (!feeValid) errors.push("invalid_fee"); diff --git a/src/models/wiki_model.js b/src/models/wiki_model.js new file mode 100644 index 0000000..ee8ffe6 --- /dev/null +++ b/src/models/wiki_model.js @@ -0,0 +1,485 @@ +const pull = require('../server/node_modules/pull-stream'); +const { readTyped, memoIndex } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const WIKILINK_RE = /\[\[([^\]|\n]{1,120})(?:\|([^\]\n]{1,120}))?\]\]/g; + +const slugify = (value) => String(value == null ? '' : value) + .normalize('NFD') + .replace(/[̀-ͯ]/g, '') + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 80); + +const linkTarget = (raw) => { + const s = String(raw || '').trim(); + const isUrl = /^[a-z]+:\/\//i.test(s); + const wikiPath = (isUrl || s.startsWith('/')) ? s.match(/\/wiki\/([^?#]+)/) : null; + if (wikiPath) { try { return decodeURIComponent(wikiPath[1]).trim(); } catch (_) { return wikiPath[1].trim(); } } + if (isUrl) { + const seg = s.replace(/[?#].*$/, '').split('/').filter(Boolean).pop() || ''; + try { return decodeURIComponent(seg).trim(); } catch (_) { return seg.trim(); } + } + const namespaced = s.match(/^[a-z]+:(?=\S)(.+)$/i); + return namespaced ? namespaced[1].trim() : s; +}; + +const extractWikiLinks = (text) => { + const out = []; + const seen = new Set(); + for (const m of String(text || '').matchAll(WIKILINK_RE)) { + const slug = slugify(linkTarget(m[1])); + if (!slug || seen.has(slug)) continue; + seen.add(slug); + out.push(slug); + } + return out; +}; + +const WIKI_TYPE = 'wikiPage'; +const ENVELOPE_TYPE = 'tribe-msg'; +const EDIT_POLICIES = ['open', 'author', 'tribe']; +const RECENT_MS = 24 * 60 * 60 * 1000; +const MAX_CHANGES = 60; +const MAX_TITLE = 100; +const IMAGE_RE = /!\[image:[^\]\[\n]{0,160}\]\((&[^)\s]{1,120}\.sha256)\)/; +const BODY_INLINE_MAX = 6000; +const bodyCache = new Map(); +const addBlob = (ssbClient, buf) => new Promise((resolve, reject) => pull(pull.values([buf]), ssbClient.blobs.add((err, ref) => err ? reject(err) : resolve(ref)))); +const getBlob = (ssbClient, ref) => new Promise((resolve) => { + try { + pull(ssbClient.blobs.get(ref), pull.collect((err, chunks) => resolve(err || !chunks || !chunks.length ? null : Buffer.concat(chunks.map(c => Buffer.isBuffer(c) ? c : Buffer.from(c)))))); + } catch (_) { resolve(null); } +}); + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normalizeList = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const normalizePolicy = (v) => { + const s = String(v || '').toLowerCase(); + if (s === 'closed') return 'author'; + return EDIT_POLICIES.includes(s) ? s : 'open'; +}; + +module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const tribeRootId = async (tribeId) => { + if (!tribesModel || !tribeId) return tribeId; + try { return await tribesModel.getRootId(tribeId); } catch (_) { return tribeId; } + }; + + const tribeKeyFor = async (tribeId) => { + if (!tribeCrypto || !tribeId) return null; + const rootId = await tribeRootId(tribeId); + return tribeCrypto.getKey(rootId) || null; + }; + + const storeBody = async (ssbClient, body, tribeId) => { + const text = safeText(body); + if (Buffer.byteLength(text, 'utf8') <= BODY_INLINE_MAX) return { body: text }; + let payload = text; + if (tribeId) { + const key = await tribeKeyFor(tribeId); + if (!key) throw new Error('Missing tribe key — cannot publish encrypted wiki page'); + payload = JSON.stringify(tribeCrypto.encryptContent({ body: text }, [key], true)); + } + const ref = await addBlob(ssbClient, Buffer.from(payload, 'utf8')); + bodyCache.set(ref, text); + return { body: '', bodyBlob: ref }; + }; + + const resolveBody = async (ssbClient, c) => { + if (!c || typeof c.bodyBlob !== 'string' || !c.bodyBlob) return c; + if (bodyCache.has(c.bodyBlob)) return { ...c, body: bodyCache.get(c.bodyBlob) }; + const buf = await getBlob(ssbClient, c.bodyBlob); + if (!buf) return { ...c, body: safeText(c.body) }; + let text = buf.toString('utf8'); + if (c.tribeId) { + try { + const key = await tribeKeyFor(c.tribeId); + const r = tribeCrypto.decryptContent(JSON.parse(text), key ? [[key]] : []); + text = r && !r._undecryptable && typeof r.body === 'string' ? r.body : ''; + } catch (_) { text = ''; } + } + bodyCache.set(c.bodyBlob, text); + return { ...c, body: text }; + }; + + const readEntries = async (ssbClient) => { + const msgs = await readTyped(ssbClient, [WIKI_TYPE, 'tombstone', ENVELOPE_TYPE], { limit: logLimit }); + const fpIdx = tribeCrypto && typeof tribeCrypto.buildFingerprintIndex === 'function' ? tribeCrypto.buildFingerprintIndex() : null; + const out = []; + for (const m of msgs) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === WIKI_TYPE || c.type === 'tombstone') { out.push(m); continue; } + if (!fpIdx || !tribeCrypto.isTribeMsg(c)) continue; + const r = tribeCrypto.unwrapMsg(c, fpIdx); + if (!r || !r.body || typeof r.body !== 'object') continue; + const body = r.body; + if (body.k === WIKI_TYPE) { + const flat = { ...body, type: WIKI_TYPE }; + delete flat.k; + out.push({ ...m, value: { ...v, content: flat } }); + } else if (body.k === 'tombstone' && typeof body.target === 'string') { + out.push({ ...m, value: { ...v, content: { type: 'tombstone', target: body.target, deletedAt: body.deletedAt, author: body.author || v.author } } }); + } + } + for (let i = 0; i < out.length; i++) { + const c = out[i].value && out[i].value.content; + if (c && c.type === WIKI_TYPE && c.bodyBlob) out[i] = { ...out[i], value: { ...out[i].value, content: await resolveBody(ssbClient, c) } }; + } + return out; + }; + + const tribeMembersFor = async (entries) => { + const out = new Map(); + if (!tribesModel) return out; + for (const m of entries) { + const c = m.value.content; + if (c.type !== WIKI_TYPE || !c.tribeId || out.has(String(c.tribeId))) continue; + let members = []; + try { const t = await tribesModel.getTribeById(c.tribeId); members = Array.isArray(t && t.members) ? t.members : []; } catch (_) {} + out.set(String(c.tribeId), members); + } + return out; + }; + + const buildIndex = (entries, membersOf = new Map()) => { + const tomb = buildValidatedTombstoneSet(entries); + const nodes = new Map(); + for (const m of entries) { + const c = m.value.content; + if (c.type !== WIKI_TYPE) continue; + nodes.set(m.key, { key: m.key, author: m.value.author, ts: m.value.timestamp || m.timestamp || 0, c }); + } + const mayEdit = (state, author) => { + if (author === state.owner) return true; + if (state.policy === 'author') return false; + if (state.tribeId) return (membersOf.get(state.tribeId) || []).includes(author); + return state.policy !== 'tribe'; + }; + const states = new Map(); + const stateOf = (key) => { + const path = []; + const seen = new Set(); + let cur = key; + while (!states.has(cur)) { + const node = nodes.get(cur); + if (!node) { states.set(cur, null); break; } + if (typeof node.c.replaces !== 'string') { + states.set(cur, { root: cur, owner: node.author, tribeId: String(node.c.tribeId || ''), policy: normalizePolicy(node.c.editPolicy), license: safeText(node.c.license) }); + break; + } + if (seen.has(cur)) { states.set(cur, null); break; } + seen.add(cur); + path.push(cur); + cur = node.c.replaces; + } + for (let i = path.length - 1; i >= 0; i--) { + const k = path[i]; + if (states.has(k)) continue; + const node = nodes.get(k); + const parent = states.get(node.c.replaces); + if (!parent || String(node.c.tribeId || '') !== parent.tribeId || !mayEdit(parent, node.author)) { states.set(k, null); continue; } + states.set(k, node.author === parent.owner ? { ...parent, policy: normalizePolicy(node.c.editPolicy), license: safeText(node.c.license) } : parent); + } + return states.get(key); + }; + const rootOf = (key) => { const s = stateOf(key); return s ? s.root : key; }; + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + if (!stateOf(node.key)) continue; + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const pages = []; + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const tip = versions[versions.length - 1]; + const state = stateOf(tip.key); + const c = tip.c; + const body = safeText(c.body); + pages.push({ + id: root, + rootId: root, + tipId: tip.key, + slug: slugify(c.title) || safeText(c.slug), + title: safeText(c.title).slice(0, MAX_TITLE), + body, + image: (body.match(IMAGE_RE) || [])[1] || null, + tags: normalizeList(c.tags), + aliases: [...new Set([...normalizeList(c.aliases).map(slugify), safeText(c.slug)].filter(a => a && a !== slugify(c.title)))], + editPolicy: state.policy, + license: state.license, + tribeId: c.tribeId || null, + encrypted: !!c.tribeId, + author: state.owner, + lastAuthor: tip.author, + createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), + ts: tip.ts, + versionCount: versions.length, + links: extractWikiLinks(body), + versions: versions.map(vn => ({ + key: vn.key, + author: vn.author, + ts: vn.ts, + createdAt: vn.c.updatedAt || vn.c.createdAt || new Date(vn.ts).toISOString(), + title: safeText(vn.c.title), + body: safeText(vn.c.body), + tags: normalizeList(vn.c.tags), + summary: safeText(vn.c.summary) + })) + }); + } + pages.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0); + return { pages, rootOf, nodes }; + }; + + const sameScope = (page, tribeId) => String(page.tribeId || '') === String(tribeId || ''); + + const scopePages = async (pages, tribeId) => { + if (tribeId === 'any') return pages; + if (!tribeId) return pages.filter(p => !p.tribeId); + const chain = tribesModel && typeof tribesModel.getChainIds === 'function' + ? await tribesModel.getChainIds(tribeId).catch(() => [tribeId]) + : [tribeId]; + const set = new Set(chain.map(String)); + return pages.filter(p => p.tribeId && set.has(String(p.tribeId))); + }; + + const decorate = (pages, scoped) => { + const bySlug = new Map(); + for (const p of scoped) { + const keys = [p.slug, ...p.aliases]; + for (const s of keys) if (s && !bySlug.has(s)) bySlug.set(s, p); + } + const inbound = new Map(); + for (const p of scoped) { + for (const s of p.links) { + const target = bySlug.get(s); + if (!target || target.id === p.id) continue; + if (!inbound.has(target.id)) inbound.set(target.id, new Set()); + inbound.get(target.id).add(p.id); + } + } + return scoped.map(p => ({ + ...p, + backlinkCount: (inbound.get(p.id) || new Set()).size, + missingLinks: p.links.filter(s => !bySlug.has(s)), + isLinked: (inbound.get(p.id) || new Set()).size > 0 + })); + }; + + const findInScope = (scoped, idOrSlug) => { + const needle = String(idOrSlug || '').trim(); + if (!needle) return null; + if (needle.startsWith('%')) return scoped.find(p => p.id === needle || p.tipId === needle || p.versions.some(v => v.key === needle)) || null; + const slug = slugify(needle); + return scoped.find(p => p.slug === slug) || scoped.find(p => p.aliases.includes(slug)) || null; + }; + + const publishContent = async (ssbClient, content) => { + let msg = content; + if (content.tribeId) { + const key = await tribeKeyFor(content.tribeId); + if (!key) throw new Error('Missing tribe key — cannot publish encrypted wiki page'); + msg = tribeCrypto.wrapMsg({ k: WIKI_TYPE, ...content }, key); + } + return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res))); + }; + + const load = async () => { + const ssbClient = await openSsb(); + const entries = await readEntries(ssbClient); + const members = await tribeMembersFor(entries); + const idx = memoIndex('wiki', entries, (e) => buildIndex(e, members)); + return { ssbClient, idx }; + }; + + const canEdit = async (page, viewer) => { + if (!page) return false; + const owner = String(page.author) === String(viewer); + if (page.editPolicy === 'author') return owner; + if (page.tribeId) { + if (!tribesModel) return owner; + try { + const t = await tribesModel.getTribeById(page.tribeId); + return owner || (Array.isArray(t.members) && t.members.includes(viewer)); + } catch (_) { return owner; } + } + if (page.editPolicy === 'tribe') return owner; + return true; + }; + + return { + WIKI_TYPE, + EDIT_POLICIES, + slugify, + + async createPage({ title, body, tags, aliases, editPolicy, license, tribeId = null }) { + const { ssbClient, idx } = await load(); + const cleanTitle = safeText(title).slice(0, MAX_TITLE); + if (!cleanTitle) throw new Error('Title required'); + const slug = slugify(cleanTitle); + if (!slug) throw new Error('Title required'); + const scoped = await scopePages(idx.pages, tribeId); + const existing = scoped.find(p => p.slug === slug); + if (existing) return { key: existing.id, existing: true }; + const now = new Date().toISOString(); + const content = { + type: WIKI_TYPE, + title: cleanTitle, + slug, + ...(await storeBody(ssbClient, body, tribeId)), + tags: normalizeList(tags), + aliases: normalizeList(aliases).map(slugify).filter(Boolean), + editPolicy: normalizePolicy(editPolicy), + ...(license ? { license } : {}), + author: ssbClient.id, + createdAt: now, + updatedAt: now, + ...(tribeId ? { tribeId } : {}) + }; + const res = await publishContent(ssbClient, content); + return { key: res.key, existing: false }; + }, + + async updatePage(id, data = {}) { + const { ssbClient, idx } = await load(); + const page = findInScope(idx.pages, id); + if (!page) throw new Error('Page not found'); + if (!(await canEdit(page, ssbClient.id))) throw new Error('Not allowed to edit'); + const now = new Date().toISOString(); + const nextTitle = data.title !== undefined ? safeText(data.title).slice(0, MAX_TITLE) : page.title; + const nextLicense = data.license !== undefined && String(page.author) === String(ssbClient.id) ? safeText(data.license) : page.license; + const content = { + type: WIKI_TYPE, + title: nextTitle || page.title, + slug: page.slug, + ...(await storeBody(ssbClient, data.body !== undefined ? data.body : page.body, page.tribeId || null)), + tags: data.tags !== undefined ? normalizeList(data.tags) : page.tags, + aliases: data.aliases !== undefined ? normalizeList(data.aliases).map(slugify).filter(Boolean) : page.aliases, + editPolicy: data.editPolicy !== undefined ? normalizePolicy(data.editPolicy) : page.editPolicy, + ...(nextLicense ? { license: nextLicense } : {}), + summary: safeText(data.summary).slice(0, 200), + author: ssbClient.id, + createdAt: page.createdAt, + updatedAt: now, + replaces: page.tipId, + ...(page.tribeId ? { tribeId: page.tribeId } : {}) + }; + const res = await publishContent(ssbClient, content); + return { key: res.key, rootId: page.id, author: page.author }; + }, + + async restoreVersion(id, versionKey) { + const { idx } = await load(); + const page = findInScope(idx.pages, id); + if (!page) throw new Error('Page not found'); + const version = page.versions.find(v => v.key === versionKey); + if (!version) throw new Error('Version not found'); + return this.updatePage(page.id, { title: version.title, body: version.body, tags: version.tags, summary: `restore:${versionKey}` }); + }, + + async deletePage(id) { + const { ssbClient, idx } = await load(); + const page = findInScope(idx.pages, id); + if (!page) throw new Error('Page not found'); + if (String(page.author) !== String(ssbClient.id)) throw new Error('Only the creator can delete'); + const deletedAt = new Date().toISOString(); + const tomb = { type: 'tombstone', target: page.id, deletedAt, author: ssbClient.id }; + let msg = tomb; + if (page.tribeId) { + const key = await tribeKeyFor(page.tribeId); + if (!key) throw new Error('Missing tribe key — cannot delete encrypted wiki page'); + msg = tribeCrypto.wrapMsg({ k: 'tombstone', target: page.id, deletedAt, author: ssbClient.id }, key); + } + return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res))); + }, + + async getPage(idOrSlug, { tribeId = null } = {}) { + const { ssbClient, idx } = await load(); + let scoped = await scopePages(idx.pages, tribeId); + let page = findInScope(scoped, idOrSlug); + if (!page && String(idOrSlug || '').startsWith('%')) { + page = findInScope(idx.pages, idOrSlug); + if (page) scoped = await scopePages(idx.pages, page.tribeId || null); + } + if (!page) return null; + const decorated = decorate(idx.pages, scoped); + const full = decorated.find(p => p.id === page.id) || page; + const bySlug = new Map(); + for (const p of scoped) for (const s of [p.slug, ...p.aliases]) if (s && !bySlug.has(s)) bySlug.set(s, p); + const backlinks = decorated.filter(p => p.id !== full.id && p.links.some(s => bySlug.get(s) && bySlug.get(s).id === full.id)); + const outbound = []; + const seen = new Set([full.id]); + for (const slug of full.links) { + const target = bySlug.get(slug); + if (!target || seen.has(target.id)) continue; + seen.add(target.id); + outbound.push(decorated.find(p => p.id === target.id) || target); + } + const linkedPages = [...backlinks]; + const inList = new Set(backlinks.map(p => p.id)); + for (const p of outbound) if (!inList.has(p.id)) linkedPages.push(p); + return { ...full, backlinks, outbound, linkedPages, canEdit: await canEdit(full, ssbClient.id), isOwner: String(full.author) === String(ssbClient.id) }; + }, + + async listPages({ tribeId = null, filter = 'all', q = '', viewerId } = {}) { + const { ssbClient, idx } = await load(); + const me = viewerId || ssbClient.id; + const scoped = decorate(idx.pages, await scopePages(idx.pages, tribeId)); + let list = scoped; + const f = String(filter || 'all').toLowerCase(); + if (f === 'mine') list = list.filter(p => String(p.author) === String(me) || p.versions.some(v => String(v.author) === String(me))); + else if (f === 'recent') list = list.filter(p => Date.now() - p.ts <= RECENT_MS); + else if (f === 'linked') list = list.filter(p => p.isLinked); + const needle = safeText(q).toLowerCase(); + if (needle) list = list.filter(p => p.title.toLowerCase().includes(needle) || p.body.toLowerCase().includes(needle) || p.tags.some(t => t.toLowerCase().includes(needle))); + return list.slice().sort((a, b) => b.ts - a.ts); + }, + + async recentChanges({ tribeId = null } = {}) { + const { idx } = await load(); + const scoped = await scopePages(idx.pages, tribeId); + const changes = []; + for (const p of scoped) { + p.versions.forEach((v, i) => changes.push({ + pageId: p.id, slug: p.slug, title: v.title || p.title, tribeId: p.tribeId || null, + versionKey: v.key, author: v.author, ts: v.ts, createdAt: v.createdAt, summary: v.summary, index: i + 1, isCreation: i === 0 + })); + } + return changes.sort((a, b) => b.ts - a.ts || b.index - a.index).slice(0, MAX_CHANGES); + }, + + async resolveRootId(id) { + const { idx } = await load(); + const page = findInScope(idx.pages, id); + if (!page) throw new Error('Not found'); + return page.id; + }, + + async canEditPage(id, viewer) { + const { ssbClient, idx } = await load(); + const page = findInScope(idx.pages, id); + return canEdit(page, viewer || ssbClient.id); + } + }; +}; + +Object.assign(module.exports, { WIKILINK_RE, slugify, linkTarget, extractWikiLinks }); diff --git a/src/models/workflows_model.js b/src/models/workflows_model.js new file mode 100644 index 0000000..81ba66f --- /dev/null +++ b/src/models/workflows_model.js @@ -0,0 +1,156 @@ +const ALL_MODULES = [ + 'agenda', 'ai', 'aiNav', 'emergencies', 'audios', 'backup', 'banking', 'blogs', 'bookmarks', 'calendars', 'campaigns', 'chats', 'cipher', + 'courts', 'dev', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'files', 'forum', 'games', 'graphos', + 'housing', 'images', 'industry', 'invites', 'jobs', 'larp', 'logistics', 'logs', 'mailing', 'maps', 'market', + 'melody', 'opinions', 'pads', 'wiki', 'parliament', 'phone', 'pixelia', 'podcasts', 'polls', 'projects', 'reports', 'rooms', 'school', 'shops', + 'tags', 'tasks', 'torrents', 'transfers', 'trending', 'tribes', 'videos', 'votes', 'wallet' +]; + +const NETWORK = ['feed', 'blogs', 'tags', 'trending', 'opinions', 'pads', 'wiki', 'forum', 'maps', 'chats', 'rooms']; +const MEDIA = ['audios', 'bookmarks', 'docs', 'files', 'images', 'torrents', 'videos', 'podcasts']; +const OFFICE = ['agenda', 'calendars', 'campaigns', 'events', 'tasks', 'reports', 'mailing', 'phone', 'favorites']; +const GOVERNANCE = ['tribes', 'larp', 'votes', 'polls', 'school', 'parliament', 'courts', 'emergencies', 'logs']; +const ECONOMY = ['banking', 'wallet', 'transfers', 'market', 'logistics', 'housing', 'jobs', 'shops', 'industry', 'projects']; + +const PRESET_BASE = ['backup', 'invites', 'favorites', 'tags', 'trending', 'cipher']; +const PRESETS = { + minimal: ['feed', 'forum', 'games', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'blogs', 'polls', 'opinions', 'cipher', 'backup'], + social: ['agenda', 'emergencies', 'audios', 'bookmarks', 'calendars', 'campaigns', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'backup', 'logs', 'mailing', 'phone', 'maps', 'blogs', 'polls', 'opinions', 'pads', 'wiki', 'parliament', 'pixelia', 'podcasts', 'melody', 'projects', 'reports', 'rooms', 'school', 'tags', 'tasks', 'trending', 'tribes', 'videos', 'votes'], + economy: ['agenda', 'emergencies', 'audios', 'bookmarks', 'calendars', 'campaigns', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'backup', 'logs', 'mailing', 'phone', 'maps', 'blogs', 'polls', 'opinions', 'pads', 'wiki', 'parliament', 'pixelia', 'podcasts', 'melody', 'projects', 'reports', 'rooms', 'tags', 'tasks', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'housing', 'jobs', 'shops', 'industry', 'school', 'logistics'], + office: [...new Set([...OFFICE, 'votes', 'polls', 'projects', 'docs', 'pads', 'wiki', 'tribes', 'chats', 'rooms', 'feed', ...PRESET_BASE])], + library: [...new Set([...MEDIA, 'feed', ...PRESET_BASE])], + mobile: null, + full: null +}; + +const MOBILE_MODULES = [ + 'agenda', 'favorites', 'wallet', 'tribes', 'larp', 'votes', 'polls', 'events', 'calendars', 'tasks', + 'reports', 'banking', 'market', 'housing', 'jobs', 'shops', 'school', 'courts', 'transfers', 'cipher', 'invites', + 'games', 'audios', 'bookmarks', 'docs', 'files', 'images', 'torrents', 'emergencies', 'mailing', 'logistics', 'podcasts', 'campaigns', ...NETWORK.filter(m => m !== 'rooms') +]; + +const WORKFLOWS = [ + { + key: 'default', + theme: 'Dark-SNH', + homePage: 'activity', + modules: ALL_MODULES.filter(m => m !== 'aiNav') + }, + { + key: 'jobs', + theme: 'Clear-SNH', + homePage: 'activity', + modules: [...OFFICE, 'jobs', 'projects', 'industry', 'market', 'shops', 'banking', 'wallet', 'transfers', + 'forum', 'chats', 'rooms', 'pads', 'wiki', 'feed', 'tags', 'trending', 'docs', 'invites'] + }, + { + key: 'social', + theme: 'Clear-SNH', + homePage: 'feed', + modules: [...NETWORK, ...MEDIA, 'events', 'calendars', 'agenda', 'favorites', 'phone', 'games', 'pixelia', + 'melody', 'tribes', 'fediverse', 'invites', 'polls', 'school', 'emergencies'] + }, + { + key: 'business', + theme: 'Dark-SNH', + homePage: 'activity', + modules: [...ECONOMY, ...OFFICE, 'forum', 'chats', 'rooms', 'pads', 'wiki', 'emergencies', 'docs', 'feed', 'tags', 'trending', 'invites', 'cipher'] + }, + { + key: 'night', + theme: 'Matrix-SNH', + homePage: 'feed', + modules: ['feed', 'blogs', 'opinions', 'chats', 'rooms', 'forum', 'tags', 'trending', 'audios', 'videos', + 'images', 'bookmarks', 'melody', 'games', 'pixelia', 'favorites', 'school'] + }, + { + key: 'kids', + theme: 'Clear-SNH', + homePage: 'activity', + modules: ['school', 'games', 'pixelia', 'melody', 'images', 'audios', 'videos', 'docs', 'bookmarks', + 'agenda', 'calendars', 'events', 'tasks', 'favorites', 'feed', 'blogs', 'chats', 'pads', 'maps', + 'tags', 'trending', 'tribes', 'larp', 'polls', 'opinions', 'invites'] + }, + { + key: 'activists', + theme: 'Purple-SNH', + homePage: 'activity', + modules: [...GOVERNANCE, ...OFFICE, + 'forum', 'chats', 'rooms', 'feed', 'blogs', 'opinions', 'pads', 'wiki', 'docs', 'images', 'videos', 'audios', 'podcasts', + 'files', 'torrents', 'maps', 'bookmarks', 'graphos', 'melody', 'cipher', 'fediverse', 'projects', 'logistics', + 'invites', 'tags', 'trending', 'backup'] + }, + { + key: 'mobile', + theme: 'Acampada', + homePage: 'activity', + modules: MOBILE_MODULES + } +]; + +const WORKFLOW_KEYS = WORKFLOWS.map(w => w.key); + +const isMobile = () => process.env.OASIS_MOBILE === '1'; +// en el movil el workflow no toca el skin: se queda el que haya elegido cada cual +// (Acampada u OasisMobile); si no hay ninguno de movil, Acampada +const MOBILE_THEMES = ['Acampada', 'OasisMobile']; +const mobileSkin = (cfg) => { + const cur = cfg && cfg.themes && cfg.themes.current; + return MOBILE_THEMES.includes(cur) ? cur : 'Acampada'; +}; +const themeOf = (workflow, cfg) => (isMobile() ? mobileSkin(cfg) : workflow.theme); +const sameTheme = (theme, workflow) => (isMobile() ? MOBILE_THEMES.includes(theme) : theme === workflow.theme); + +const getWorkflow = (key) => WORKFLOWS.find(w => w.key === String(key || '')) || null; + +const modulesOf = (workflow) => { + const set = new Set(workflow.modules.filter(m => ALL_MODULES.includes(m))); + return ALL_MODULES.filter(m => set.has(m)); +}; + +const currentWorkflow = (config) => { + const modules = (config && config.modules) || {}; + const theme = (config && config.themes && config.themes.current) || ''; + return WORKFLOW_KEYS.find(key => { + const w = getWorkflow(key); + if (!sameTheme(theme, w)) return false; + const on = new Set(modulesOf(w)); + return ALL_MODULES.every(m => (modules[`${m}Mod`] === 'on') === on.has(m)); + }) || null; +}; + +const applyWorkflow = (cfg, workflow) => { + const enabled = new Set(modulesOf(workflow)); + cfg.modules = cfg.modules || {}; + ALL_MODULES.forEach(mod => { cfg.modules[`${mod}Mod`] = enabled.has(mod) ? 'on' : 'off'; }); + if (workflow.theme) cfg.themes = { ...(cfg.themes || {}), current: themeOf(workflow, cfg) }; + if (workflow.homePage) cfg.homePage = workflow.homePage; + return cfg; +}; + +const FULL_WORKFLOW = { key: 'full', modules: ALL_MODULES }; + +const WIZARD_WORKFLOWS = ['full', 'social', 'activists', 'kids', 'night', 'jobs', 'business', 'mobile']; + +const wizardWorkflow = (key) => { + const k = String(key || ''); + if (!WIZARD_WORKFLOWS.includes(k)) return null; + return k === 'full' ? FULL_WORKFLOW : getWorkflow(k); +}; + +PRESETS.mobile = MOBILE_MODULES; +PRESETS.full = ALL_MODULES; + +module.exports = { + ALL_MODULES, + MOBILE_MODULES, + PRESETS, + WORKFLOWS, + WORKFLOW_KEYS, + getWorkflow, + modulesOf, + currentWorkflow, + applyWorkflow, + WIZARD_WORKFLOWS, + wizardWorkflow +}; diff --git a/src/server/SSB_server.js b/src/server/SSB_server.js index cd69637..ea12deb 100644 --- a/src/server/SSB_server.js +++ b/src/server/SSB_server.js @@ -6,7 +6,7 @@ const fs = require('fs'); const path = require('path'); const SecretStack = require('secret-stack'); const caps = require('ssb-caps'); -const SSB = require('ssb-db'); +const db2Defaults = require('ssb-db2/defaults'); const config = require('./ssb_config'); const { printMetadata } = require('./ssb_metadata'); @@ -27,6 +27,7 @@ const { printMetadata } = require('./ssb_metadata'); return `${m[1].replace(/^::ffff:/, '')}:${m[2]}`; }; const logRejection = (peer) => { + if (!(process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true')) return; const ts = new Date().toISOString().replace('T', ' ').slice(0, 19); realErr.call(console, `[${ts}] REJECTED ${peer} (wrong SHS cap)`); }; @@ -51,35 +52,41 @@ const { printMetadata } = require('./ssb_metadata'); }; })(); -require('ssb-plugins').loadUserPlugins(SecretStack({ caps }), config); +try { + if (require('../configs/config-manager.js').getConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1') config.conn = { ...(config.conn || {}), autostart: false }; +} catch (_) {} const Server = SecretStack({ caps }) - .use(SSB) + .use(require('ssb-db2/core')) + .use(require('ssb-classic')) + .use(require('ssb-box')) + .use(require('ssb-db2/compat/publish')) + .use(require('ssb-db2/compat/post')) + .use(require('ssb-db2/compat/db')) + .use(require('ssb-db2/compat/log-stream')) + .use(require('ssb-db2/compat/history-stream')) + .use(require('ssb-db2/compat/ebt')) + .use(require('ssb-db2/compat/feedstate')) + .use(require('./db2_legacy')) .use(require('ssb-master')) - .use(require('ssb-gossip')) .use(require('ssb-ebt')) .use(require('ssb-friends')) .use(require('ssb-blobs')) - .use(require('ssb-meme')) - .use(require('ssb-plugins')) .use(require('ssb-conn')) - .use(require('ssb-box')) - .use(require('ssb-search')) - .use(require('ssb-private')) - .use(require('ssb-friend-pub')) .use(config.pub ? require('ssb-invite') : require('ssb-invite-client')) .use(require('ssb-logging')) .use(require('ssb-replication-scheduler')) - .use(require('ssb-partial-replication')) - .use(require('ssb-about')) .use(require('ssb-onion')) .use(require('ssb-unix-socket')) .use(require('ssb-no-auth')) - .use(require('ssb-backlinks')) - .use(require('ssb-links')) - .use(require('ssb-tangle')) - .use(require('ssb-query')); + .use(require('./snapshot_plugin')) + .use((() => { const pm = require('./phone_module'); try { if (process.env.OASIS_MOBILE === '1') { pm.setAudioFactory(require('./phone_audio_android')); pm.setVideoFactory(require('./phone_video_android')); pm.setStreamFactory(require('./phone_stream_android')); } } catch (_) {} return pm; })()) + .use(require('./network_pause')); +if (!config.pub) { + Server.use(require('ssb-lan')); + Server.use(require('./lanRouter')); +} if (config.autofollow && typeof config.autofollow === 'object' && !Array.isArray(config.autofollow)) { if (config.autofollow.enabled === false) { @@ -104,20 +111,135 @@ const isLockError = (err) => { return /Resource temporarily unavailable/i.test(msg) && /\.ssb\/.*LOCK/i.test(msg); }; +const isCorruptStoreError = (err) => { + const msg = String((err && err.message) || ''); + const stack = String((err && err.stack) || ''); + return /JSON|Unexpected token|Unexpected end|bipf|Invalid|corrupt/i.test(msg) + && /flumelog-offset|aligned-block-file|flumeview|flumedb|async-append-only-log|jitdb|bipf|ssb-db2/i.test(stack); +}; + +const migration = { running: false, percent: 0, done: false, error: null }; +let migrationPromise = null; +const MIGRATED_MARKER = 'OASIS: this log was migrated to ssb-db2'; +const oldLogIsMarker = (file) => { + try { + const st = fs.statSync(file); + return st.size < 4096 && fs.readFileSync(file, 'utf8').startsWith(MIGRATED_MARKER); + } catch (_) { return false; } +}; +const writeMigratedMarker = () => { + try { + fs.mkdirSync(db2Defaults.flumePath(config.path), { recursive: true }); + fs.writeFileSync(db2Defaults.oldLogPath(config.path), `${MIGRATED_MARKER} (${config.path}/db2/log.bipf) on ${new Date().toISOString()}.\nThis file is a guard: an Oasis older than the ssb-db2 move cannot read it and refuses to start, instead of starting with an empty log and forking your feed. Do not delete it unless you are moving back to that version on purpose, and never run both versions on this folder.\n`); + } catch (_) {} +}; +const migrateFlume = () => { + if (migrationPromise) return migrationPromise; + migrationPromise = new Promise((resolve, reject) => { + const oldLog = db2Defaults.oldLogPath(config.path); + if (!fs.existsSync(oldLog) || oldLogIsMarker(oldLog)) { migration.done = true; return resolve(false); } + if (isDebug()) console.log('- Database: migrating the local log to ssb-db2'); + migration.running = true; + let mig; + try { + mig = SecretStack({ caps }) + .use(require('ssb-db2/migrate')) + .call(null, { ...config, connections: { incoming: {}, outgoing: {} }, db2: { ...(config.db2 || {}), automigrate: false, dangerouslyKillFlumeWhenMigrated: true } }); + } catch (err) { migration.running = false; migration.error = err; return reject(err); } + const pull = require('pull-stream'); + let shown = -1; + pull(mig.db2migrate.progress(), pull.drain((p) => { + const pct = Math.max(0, Math.min(100, Math.floor((Number(p) || 0) * 100))); + migration.percent = pct; + if (pct !== shown && isDebug()) { shown = pct; console.log(` migrating ${pct}%`); } + })); + const finish = () => { + const waitGone = (tries) => { + if (!fs.existsSync(db2Defaults.oldLogPath(config.path))) { + mig.close(() => { + writeMigratedMarker(); + migration.running = false; migration.done = true; migration.percent = 100; + if (isDebug()) console.log(' migrated 100%'); + resolve(true); + }); + return; + } + if (tries <= 0) { + try { fs.rmSync(db2Defaults.flumePath(config.path), { recursive: true, force: true }); } catch (_) {} + mig.close(() => { + if (!fs.existsSync(db2Defaults.oldLogPath(config.path))) writeMigratedMarker(); + else if (isDebug()) console.log(' the old log could not be removed; it is left in place'); + migration.running = false; migration.done = true; migration.percent = 100; + resolve(true); + }); + return; + } + setTimeout(() => waitGone(tries - 1), 200); + }; + waitGone(300); + }; + try { mig.db2migrate.start(); } catch (err) { migration.running = false; migration.error = err; return reject(err); } + mig.db2migrate.synchronized((synced) => { + if (synced !== true) return; + finish(); + return false; + }); + }); + return migrationPromise; +}; + +const isDebug = () => process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; + +const isPortInUseError = (err) => String((err && err.code) || '') === 'EADDRINUSE'; + const handleFatal = (err) => { + if (isPortInUseError(err)) { + console.log(''); + console.log('Another Oasis instance is already running on this device. Close the other instance (or kill the process) and try again.'); + if (isDebug()) console.log(`Detail: ${String((err && err.message) || err)}`); + console.log(''); + process.exit(1); + } if (isLockError(err)) { console.log(''); console.log('Another Oasis instance is already running on this device. Close the other instance (or kill the process) and try again.'); + if (isDebug()) { + console.log(`Detail: ${String((err && err.message) || err)}`); + console.log(String((err && err.stack) || '').split('\n').slice(1, 4).join('\n')); + } console.log(''); process.exit(1); } + if (isCorruptStoreError(err)) { + console.log(''); + console.log('Oasis could not read its local database (a record in ~/.ssb/db2 looks corrupted).'); + console.log('This usually happens after an unclean shutdown or a disk error.'); + console.log(''); + console.log('What you can try, in order:'); + console.log(' 1. Simply start Oasis again — transient read errors often clear on the next boot.'); + console.log(' 2. Check your disk health (e.g. run fsck on the partition holding ~/.ssb).'); + console.log(' 3. Rebuild the indexes from Settings once Oasis starts, or remove ~/.ssb/db2/indexes'); + console.log(' and ~/.ssb/db2/jit (NOT log.bipf, NOT secret) so they are regenerated from the log.'); + console.log(''); + if (isDebug()) console.log(`Technical detail: ${String((err && err.message) || err)}`); + process.exit(1); + } + if (server) { + console.error('[oasis] unexpected error, still running:', (err && err.stack) || err); + return; + } throw err; }; process.on('uncaughtException', handleFatal); -if (argv[0] === 'start') { +const tightenStateDir = () => { + try { if (config.path && fs.existsSync(config.path)) fs.chmodSync(config.path, 0o700); } catch (_) {} +}; + +const startServerOnly = () => { try { + tightenStateDir(); server = Server(config); } catch (err) { handleFatal(err); @@ -133,7 +255,7 @@ if (argv[0] === 'start') { manifest.config = 'sync'; server.config = cb => { - console.log(JSON.stringify(config, null, 2)); + console.log(JSON.stringify(config, (k, v) => (k === 'keys' || k === 'private' || k === 'secret') ? undefined : v, 2)); cb(); }; @@ -182,10 +304,51 @@ if (argv[0] === 'start') { } catch (_) {} }, 1000); + const ownFeedSeq = () => new Promise((res) => { + try { + const pull = require('pull-stream'); + pull( + server.createUserStream({ id: server.id, reverse: true, limit: 1 }), + pull.collect((err, msgs) => res(err || !msgs || !msgs.length ? 0 : (msgs[0].value && msgs[0].value.sequence) || 0)) + ); + } catch (_) { res(0); } + }); + + setTimeout(async () => { + try { + if (!(await ownFeedSeq())) return; + const pull = require('pull-stream'); + const version = String(require('./package.json').version || ''); + if (!version) return; + pull( + server.createUserStream({ id: server.id, reverse: true }), + pull.filter(m => m && m.value && m.value.content && m.value.content.type === 'oasisVersion'), + pull.take(1), + pull.collect((err, msgs) => { + const mine = !err && msgs && msgs.length ? msgs[0].value.content.version : null; + if (mine !== version) { + try { server.publish({ type: 'oasisVersion', version, updatedAt: new Date().toISOString() }, () => {}); } catch (_) {} + } + }) + ); + } catch (_) {} + }, 7000); + +}; + +if (argv[0] === 'start') { + migrateFlume().then(startServerOnly, handleFatal); } module.exports = { config, - server: server || Server(config), - open: async () => server || Server(config) + get server() { + return server; + }, + migrationStatus: () => ({ ...migration }), + open: async () => { + await migrateFlume(); + if (!server) { tightenStateDir(); server = Server(config); } + return server; + } }; diff --git a/src/server/db2_legacy.js b/src/server/db2_legacy.js new file mode 100644 index 0000000..8550072 --- /dev/null +++ b/src/server/db2_legacy.js @@ -0,0 +1,651 @@ +const pull = require('pull-stream'); +const cat = require('pull-cat'); +const defer = require('pull-defer'); +const pl = require('pull-level'); +const bipf = require('bipf'); +const ssbKeys = require('ssb-keys'); +const ssbMsgs = require('ssb-msgs'); +const { heads } = require('ssb-sort'); +const Plugin = require('ssb-db2/indexes/plugin'); +const { where, and, or, type, author, descending, sortByArrival, toPullStream, toCallback, startFrom, paginate, seqs, isDecrypted, batch, lt, lte } = require('ssb-db2/operators'); + +const BIPF_CONTENT = bipf.allocAndEncode('content'); +const BIPF_AUTHOR = bipf.allocAndEncode('author'); +const BIPF_TIMESTAMP = bipf.allocAndEncode('timestamp'); +const BIPF_TYPE = bipf.allocAndEncode('type'); +const LINK_RE = /^[@%&]/; +const SEARCH_LIMIT = 100; +const ARRIVAL_SLACK_MS = 10 * 60 * 1000; +const ACTIVITY_DRAIN_MS = 3000; +const RECENT_KEEP = 64; +const FIRST_PAGE = 1000; +const LAST_PAGE = 64000; +const OPS = ['$gt', '$gte', '$lt', '$lte', '$in', '$is', '$prefix', '$ne', '$truthy', '$eq']; + +class LinksIndex extends Plugin { + constructor(log, dir) { + super(log, dir, 'oasisLinks', 1, 'json', 'json'); + } + + processRecord(record, seq, pValue) { + const buf = record.value; + const pContent = bipf.seekKey2(buf, pValue, BIPF_CONTENT, 0); + if (pContent < 0) return; + const content = bipf.decode(buf, pContent); + if (!content || typeof content !== 'object') return; + const pAuthor = bipf.seekKey2(buf, pValue, BIPF_AUTHOR, 0); + const source = pAuthor < 0 ? null : bipf.decode(buf, pAuthor); + const seen = new Set(); + ssbMsgs.indexLinks(content, (link, rel) => { + const dest = link && link.link; + if (typeof dest !== 'string' || !LINK_RE.test(dest)) return; + const mark = `${dest}\u0000${rel}`; + if (seen.has(mark)) return; + seen.add(mark); + this.batch.push({ type: 'put', key: [dest, seq, rel], value: { s: seq, a: source, r: rel } }); + }); + } + + indexesContent() { + return true; + } + + getLinks(dest, cb) { + pull( + pl.read(this.level, { gte: [dest, ''], lte: [dest, undefined], keyEncoding: this.keyEncoding, valueEncoding: this.valueEncoding, keys: false }), + pull.collect((err, arr) => err ? cb(err) : cb(null, (arr || []).filter(Boolean))) + ); + } +} + +class ActivityIndex extends Plugin { + constructor(log, dir) { + super(log, dir, 'oasisActivity', 2, undefined, 'json'); + this.latest = new Map(); + this.dirty = new Set(); + this.reached = -1; + this.waiting = []; + this.recent = []; + const onRecord = this.onRecord; + this.onRecord = (record, isLive, pValue) => { + onRecord.call(this, record, isLive, pValue); + const last = this.recent.length ? this.recent[this.recent.length - 1] : -1; + if (record.value && record.offset > last) { + this.recent.push(record.offset); + if (this.recent.length > RECENT_KEEP) this.recent.shift(); + } + this.reach(record.offset); + }; + this.offset((offset) => this.reach(offset)); + } + + reach(offset) { + if (!(offset > this.reached)) return; + this.reached = offset; + if (!this.waiting.length) return; + const due = this.waiting.filter((w) => w.offset <= offset); + this.waiting = this.waiting.filter((w) => w.offset > offset); + for (const w of due) w.done(); + } + + whenReached(offset, ms, done) { + if (this.reached >= offset) return done(true); + let settled = false; + const finish = (ok) => { if (settled) return; settled = true; clearTimeout(timer); done(ok); }; + const timer = setTimeout(() => { this.waiting = this.waiting.filter((w) => w.done !== hit); finish(false); }, ms); + const hit = () => finish(true); + this.waiting.push({ offset, done: hit }); + } + + onLoaded(cb) { + pull( + pl.read(this.level, { gt: '\x00', valueEncoding: this.valueEncoding }), + pull.drain(({ key, value }) => { this.latest.set(key, value); }, (err) => cb(err && err !== true ? err : undefined)) + ); + } + + processRecord(record, seq, pValue) { + const buf = record.value; + const pAuthor = bipf.seekKey2(buf, pValue, BIPF_AUTHOR, 0); + if (pAuthor < 0) return; + const author = bipf.decode(buf, pAuthor); + if (typeof author !== 'string') return; + const pContent = bipf.seekKey2(buf, pValue, BIPF_CONTENT, 0); + if (pContent >= 0 && bipf.getEncodedType(buf, pContent) === bipf.types.object) { + const pType = bipf.seekKey2(buf, pContent, BIPF_TYPE, 0); + if (pType >= 0 && bipf.decode(buf, pType) === 'tombstone') return; + } + const pDeclared = bipf.seekKey2(buf, pValue, BIPF_TIMESTAMP, 0); + const pArrival = bipf.seekKey2(buf, 0, BIPF_TIMESTAMP, 0); + let ts = Number(pDeclared >= 0 ? bipf.decode(buf, pDeclared) : 0) || Number(pArrival >= 0 ? bipf.decode(buf, pArrival) : 0) || 0; + if (ts > 0 && ts < 1e12) ts *= 1000; + if (!(ts > (this.latest.get(author) || 0))) return; + this.latest.set(author, ts); + this.dirty.add(author); + } + + onFlush(cb) { + for (const author of this.dirty) this.batch.push({ type: 'put', key: author, value: this.latest.get(author) }); + this.dirty.clear(); + cb(); + } + + indexesContent() { + return false; + } + + reset() { + this.latest.clear(); + this.dirty.clear(); + this.recent = []; + } +} + +const linksOf = (content) => { + const out = []; + if (!content || typeof content !== 'object') return out; + ssbMsgs.indexLinks(content, (link, rel) => { + const dest = link && link.link; + if (typeof dest === 'string' && LINK_RE.test(dest)) out.push({ dest, rel }); + }); + return out; +}; + +const plainOf = (kvt, keys, opts) => { + if (!kvt || !kvt.value) return null; + const base = { key: kvt.key, value: kvt.value, timestamp: kvt.timestamp }; + const c = kvt.value.content; + if (typeof c !== 'string') return opts.decryptedOnly ? null : base; + if (!opts.private && !opts.decryptedOnly) return base; + let plain = null; + try { plain = ssbKeys.unbox(c, keys.private); } catch (_) { plain = null; } + if (!plain) return opts.decryptedOnly ? null : base; + return legacy({ key: kvt.key, value: { ...kvt.value, content: plain }, timestamp: kvt.timestamp, meta: { private: true, originalContent: c } }, { private: opts.private }); +}; + +const liveTap = (sbot, keys, opts, match) => { + const items = []; + let waiting = null; + let ended = null; + const remove = sbot.db.onMsgAdded((ev) => { + if (ended) return false; + const m = plainOf(ev && ev.kvt, keys, opts); + if (!m || (match && !match(m))) return; + items.push(m); + if (waiting) { const w = waiting; waiting = null; w(null, items.shift()); } + }, false); + return (abort, cb) => { + if (abort) { + if (!ended) { ended = abort; items.length = 0; remove(); } + return cb(abort); + } + if (items.length) return cb(null, items.shift()); + if (ended) return cb(ended); + waiting = cb; + }; +}; + +const withSync = (old$, live$, opts = {}) => { + const seen = new Set(); + const parts = []; + if (opts.old !== false) parts.push(pull(old$, pull.through((m) => { if (m && m.key) seen.add(m.key); }))); + if (opts.sync !== false) parts.push(pull.values([{ sync: true }])); + parts.push(pull(live$, pull.filter((m) => !(m && m.key && seen.has(m.key))))); + return cat(parts); +}; + +const legacy = (msg, opts = {}) => { + if (!msg || !msg.value || msg.sync) return msg; + const priv = msg.meta && msg.meta.private === true; + if (!priv) return { key: msg.key, value: msg.value, timestamp: msg.timestamp }; + if (!opts.private) return { key: msg.key, value: { ...msg.value, content: msg.meta.originalContent }, timestamp: msg.timestamp }; + return { + key: msg.key, + value: { ...msg.value, private: true, meta: { ...(msg.value.meta || {}), private: true, original: { content: msg.meta.originalContent } } }, + timestamp: msg.timestamp + }; +}; + +const isOpObject = (f) => f && typeof f === 'object' && !Array.isArray(f) && Object.keys(f).some(k => OPS.includes(k)); + +const matchValue = (v, f) => { + if (isOpObject(f)) { + for (const op of Object.keys(f)) { + const arg = f[op]; + if (op === '$gt' && !(v > arg)) return false; + if (op === '$gte' && !(v >= arg)) return false; + if (op === '$lt' && !(v < arg)) return false; + if (op === '$lte' && !(v <= arg)) return false; + if (op === '$in' && !(Array.isArray(arg) && arg.includes(v))) return false; + if (op === '$is' && typeof v !== arg) return false; + if (op === '$prefix' && !(typeof v === 'string' && v.startsWith(arg))) return false; + if (op === '$ne' && v === arg) return false; + if (op === '$truthy' && !v) return false; + if (op === '$eq' && v !== arg) return false; + } + return true; + } + if (f && typeof f === 'object' && !Array.isArray(f)) { + if (!v || typeof v !== 'object') return false; + return Object.keys(f).every(k => matchValue(v[k], f[k])); + } + if (Array.isArray(f)) return JSON.stringify(v) === JSON.stringify(f); + return v === f; +}; + +const formatter = (opts = {}) => { + if (opts.keys === false && opts.values !== false) return (m) => (m && m.sync) ? m : m.value; + if (opts.values === false) return (m) => (m && m.sync) ? m : m.key; + return (m) => m; +}; + +const matchFilter = (msg, filter) => { + if (!filter) return true; + return Object.keys(filter).every(k => k === 'dest' ? true : matchValue(msg[k], filter[k])); +}; + +const opsFor = (filter) => { + const ops = []; + const value = filter && filter.value; + const content = value && value.content; + const t = content && content.type; + if (typeof t === 'string') ops.push(type(t)); + else if (t && Array.isArray(t.$in) && t.$in.length) ops.push(or(...t.$in.map(x => type(x)))); + if (value && typeof value.author === 'string') ops.push(author(value.author)); + if (!ops.length) return null; + return ops.length === 1 ? ops[0] : and(...ops); +}; + +const filterOf = (filter) => filter ? (m) => matchFilter(m, filter) : null; + +module.exports = [ + { + manifest: { + createUserStream: 'source', + messagesByType: 'source', + links: 'source', + createLogStream: 'source', + get: 'async', + whoami: 'sync', + status: 'sync', + progress: 'sync', + rebuild: 'async' + }, + init(sbot, config) { + sbot.db.registerIndex(LinksIndex); + const linksIndex = () => sbot.db.getIndex('oasisLinks'); + + const pages = (ops, extra) => { + let seq = 0; + let size = FIRST_PAGE; + let total = Infinity; + let ended = false; + const next = (end, cb) => { + if (end) return cb(end); + if (ended || seq >= total) return cb(true); + sbot.db.query(...[ops ? where(ops) : null, ...extra, startFrom(seq), paginate(size), toCallback((err, answer) => { + if (err) return cb(err); + if (!answer || !answer.total || !answer.results || !answer.results.length) { ended = true; return cb(true); } + total = answer.total; + seq = answer.nextSeq; + size = Math.min(size * 2, LAST_PAGE); + cb(null, answer.results); + })].filter(Boolean)); + }; + return pull(next, pull.map((page) => pull.values(page)), pull.flatten()); + }; + + const query = (ops, extra, size) => size + ? sbot.db.query(...[ops ? where(ops) : null, ...extra, batch(size), toPullStream()].filter(Boolean)) + : pages(ops, extra); + + const stream = (ops, opts = {}) => { + const shape = () => pull.map(m => legacy(m, { private: opts.private })); + const boxed = () => opts.skipPrivate ? pull.filter(m => m.sync || typeof (m.value && m.value.content) !== 'string') : pull.through(); + const pass = () => opts.filter ? pull.filter(opts.filter) : pull.through(); + const limit = typeof opts.limit === 'number' && opts.limit >= 0 ? opts.limit : -1; + const order = [opts.arrival ? sortByArrival() : null, opts.reverse ? descending() : null].filter(Boolean); + const fmt = formatter(opts); + const size = limit >= 0 ? Math.max(1, Math.min(limit, FIRST_PAGE)) : undefined; + if (!opts.live) return pull(query(ops, order, size), shape(), boxed(), pass(), limit >= 0 ? pull.take(limit) : pull.through(), pull.map(fmt)); + const match = (m) => (!opts.skipPrivate || typeof (m.value && m.value.content) !== 'string') && (!opts.match || opts.match(m)) && (!opts.filter || opts.filter(m)); + const live$ = liveTap(sbot, config.keys, { private: opts.private, decryptedOnly: opts.decryptedOnly }, match); + const old$ = pull(query(ops, order, size), shape(), boxed(), pass(), limit >= 0 ? pull.take(limit) : pull.through()); + return pull(withSync(old$, live$, opts), pull.map(fmt)); + }; + + const bySeqs = (list, opts = {}) => { + const unique = [...new Set(list)]; + if (!unique.length) return pull.empty(); + return pull(query(seqs(unique), []), pull.map(m => legacy(m, { private: opts.private }))); + }; + + const linked = (dest, opts, cb) => { + sbot.db.onDrain('oasisLinks', () => { + const idx = linksIndex(); + if (!idx) return cb(null, []); + idx.getLinks(dest, (err, entries) => { + if (err) return cb(err); + const wanted = entries.filter(e => (!opts.rel || e.r === opts.rel) && (!opts.source || e.a === opts.source)); + pull(bySeqs(wanted.map(e => e.s), opts), pull.collect(cb)); + }); + }); + }; + + const liveLinked = (dest, opts) => liveTap(sbot, config.keys, { private: opts.private }, (m) => + linksOf(m.value && m.value.content).some(l => l.dest === dest && (!opts.rel || l.rel === opts.rel)) && (!opts.source || m.value.author === opts.source)); + + const sortLegacy = (arr, byTs, reverse) => { + const claimed = (m) => Number(m.value && m.value.timestamp) || 0; + const arrival = (m) => Number(m.timestamp) || 0; + const seq = (m) => Number(m.value && m.value.sequence) || 0; + arr.sort((a, b) => (byTs ? (claimed(a) - claimed(b)) : 0) || (arrival(a) - arrival(b)) || (seq(a) - seq(b))); + if (reverse) arr.reverse(); + return arr; + }; + + let drained = false; + try { sbot.db.onDrain('base', () => { drained = true; }); } catch (_) { drained = true; } + const status = () => { + const st = (sbot.db.getStatus && sbot.db.getStatus().value) || { log: 0, jit: {}, indexes: {}, progress: 1 }; + return { + ready: drained, + sync: { since: Number(st.log) || 0, plugins: { ...(st.indexes || {}) } }, + jit: { ...(st.jit || {}) }, + progress: typeof st.progress === 'number' ? st.progress : 1, + db: 'ssb-db2' + }; + }; + + const progress = () => { + const st = status(); + const target = st.sync.since; + return { indexes: { start: 0, current: Math.min(target, Math.round(st.progress * target)), target } }; + }; + + const rebuild = (cb) => { + if (typeof cb === 'function') return sbot.db.reset(cb); + return new Promise((resolve, reject) => sbot.db.reset((err) => err ? reject(err) : resolve())); + }; + + const createUserStream = (opts = {}) => { + const id = typeof opts === 'string' ? opts : opts.id; + if (!id) return pull.error(new Error('createUserStream: missing id')); + const seqOf = (m) => Number(m.value && m.value.sequence) || 0; + const filter = (opts.gt !== undefined || opts.gte !== undefined || opts.lt !== undefined || opts.lte !== undefined) + ? (m) => (opts.gt === undefined || seqOf(m) > opts.gt) && (opts.gte === undefined || seqOf(m) >= opts.gte) && (opts.lt === undefined || seqOf(m) < opts.lt) && (opts.lte === undefined || seqOf(m) <= opts.lte) + : null; + return stream(author(id), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter, match: (m) => m.value && m.value.author === id, private: opts.private === true, arrival: true, keys: opts.keys, values: opts.values }); + }; + + const messagesByType = (opts = {}) => { + const t = typeof opts === 'string' ? opts : opts.type; + if (!t) return pull.error(new Error('messagesByType: missing type')); + const o = typeof opts === 'string' ? {} : opts; + return stream(type(t), { reverse: o.reverse === true, limit: o.limit, live: o.live === true, old: o.old, sync: o.sync, skipPrivate: true, match: (m) => !!(m.value && m.value.content && m.value.content.type === t), arrival: true, keys: o.keys, values: o.values }); + }; + + const links = (opts = {}) => { + const src = defer.source(); + const toLink = (m) => opts.values ? m : { source: m.value.author, dest: opts.dest, rel: opts.rel, key: m.key }; + if (opts.dest) { + const live$ = opts.live ? liveLinked(opts.dest, { rel: opts.rel, source: opts.source }) : null; + linked(opts.dest, { rel: opts.rel, source: opts.source }, (err, arr) => { + if (err) { if (live$) live$(err, () => {}); return src.abort(err); } + const out = sortLegacy(arr, false, opts.reverse === true); + if (!live$) return src.resolve(pull(pull.values(out), pull.map(toLink))); + src.resolve(pull(withSync(pull.values(out), live$, opts), pull.map((m) => m.sync ? m : toLink(m)))); + }); + return src; + } + const ops = opts.source ? author(opts.source) : null; + src.resolve(pull( + stream(ops, { reverse: opts.reverse === true, arrival: true }), + pull.map(m => linksOf(m.value && m.value.content).filter(l => !opts.rel || l.rel === opts.rel).map(l => opts.values ? m : { source: m.value.author, dest: l.dest, rel: l.rel, key: m.key })), + pull.flatten() + )); + return src; + }; + + const createLogStream = (opts = {}) => { + const bound = (k) => opts[k] !== undefined && opts[k] !== null && Number.isFinite(Number(opts[k])); + const upper = [bound('lt') ? lt(Number(opts.lt), 'timestamp') : null, bound('lte') ? lte(Number(opts.lte), 'timestamp') : null].filter(Boolean); + const upperOps = upper.length ? (upper.length === 1 ? upper[0] : and(...upper)) : null; + const hasRange = ['gt', 'gte', 'lt', 'lte'].some(k => opts[k] !== undefined && opts[k] !== null); + const ts = (m) => Number(m && m.timestamp) || 0; + const inRange = (m) => !m || m.sync || typeof m !== 'object' || ( + (opts.gt === undefined || opts.gt === null || ts(m) > opts.gt) && + (opts.gte === undefined || opts.gte === null || ts(m) >= opts.gte) && + (opts.lt === undefined || opts.lt === null || ts(m) < opts.lt) && + (opts.lte === undefined || opts.lte === null || ts(m) <= opts.lte) + ); + const lower = Math.max(bound('gt') ? Number(opts.gt) : -Infinity, bound('gte') ? Number(opts.gte) : -Infinity); + if (lower > -Infinity && opts.live !== true) { + const floor = lower - ARRIVAL_SLACK_MS; + const src = defer.source(); + pull( + stream(upperOps, { reverse: true, arrival: true }), + pull.take((m) => !m || typeof m !== 'object' || ts(m) >= floor), + pull.filter(inRange), + pull.collect((err, arr) => { + if (err) return src.resolve(pull.error(err)); + const ordered = opts.reverse === true ? arr : arr.reverse(); + const limited = opts.limit > 0 ? ordered.slice(0, opts.limit) : ordered; + src.resolve(pull.values(limited)); + }) + ); + return src; + } + const shaped = stream(upperOps, { reverse: opts.reverse === true, limit: hasRange ? undefined : opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, arrival: true, keys: opts.keys, values: opts.values }); + if (!hasRange) return shaped; + const filtered = pull(shaped, pull.filter(inRange)); + return opts.limit > 0 ? pull(filtered, pull.take(opts.limit)) : filtered; + }; + + const get = (idOrObject, cb) => { + const isObj = !!(idOrObject && typeof idOrObject === 'object'); + const id = isObj ? idOrObject.id : idOrObject; + sbot.db.getMsg(id, (err, msg) => { + if (err) return cb(err); + const shaped = legacy(msg, { private: isObj && idOrObject.private === true }); + cb(null, isObj && idOrObject.meta ? shaped : shaped.value); + }); + }; + + const whoami = () => ({ id: sbot.id }); + + sbot._oasisDb2 = { stream, linked, liveLinked, bySeqs, sortLegacy, legacy, matchFilter, opsFor, filterOf, linksOf }; + return { createUserStream, messagesByType, links, createLogStream, get, whoami, status, progress, rebuild }; + } + }, + { + name: 'private', + version: '1.0.0', + manifest: { publish: 'async', unbox: 'sync', read: 'source' }, + init(sbot, config) { + return { + publish(content, recps, cb) { + const list = (Array.isArray(recps) ? recps : [recps]).map(r => typeof r === 'string' ? r : (r && r.link)).filter(Boolean); + if (!list.length) return cb(new Error('private.publish: no recipients')); + sbot.db.create({ content, recps: list, encryptionFormat: 'box' }, (err, kvt) => err ? cb(err) : cb(null, legacy(kvt))); + }, + unbox(msgOrData) { + if (typeof msgOrData === 'string') return ssbKeys.unbox(msgOrData, config.keys.private); + if (!msgOrData || !msgOrData.value || typeof msgOrData.value.content !== 'string') return undefined; + let plain = null; + try { plain = ssbKeys.unbox(msgOrData.value.content, config.keys.private); } catch (_) { plain = null; } + if (!plain) return undefined; + const v = msgOrData.value; + return { + key: msgOrData.key, + value: { previous: v.previous, author: v.author, sequence: v.sequence, timestamp: v.timestamp, hash: v.hash, content: plain, private: true }, + timestamp: msgOrData.timestamp + }; + }, + read(opts = {}) { + return sbot._oasisDb2.stream(isDecrypted('box'), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, private: true, decryptedOnly: true, arrival: true, keys: opts.keys, values: opts.values }); + } + }; + } + }, + { + name: 'activity', + version: '1.0.0', + manifest: { latest: 'async', tail: 'async' }, + init(sbot) { + sbot.db.registerIndex(ActivityIndex); + return { + latest(ids, opts, cb) { + if (typeof ids === 'function') { cb = ids; ids = null; opts = {}; } + if (typeof opts === 'function') { cb = opts; opts = {}; } + const idx = sbot.db.getIndex('oasisActivity'); + if (!idx) return cb(null, null); + let settled = false; + const settle = (ok) => { + if (settled) return; + settled = true; + clearTimeout(timer); + if (!ok && !(idx.offset.value >= 0)) return cb(null, null); + const out = {}; + if (Array.isArray(ids)) { for (const id of ids) if (idx.latest.has(id)) out[id] = idx.latest.get(id); } + else for (const [id, ts] of idx.latest) out[id] = ts; + cb(null, out); + }; + const timer = setTimeout(() => settle(false), ACTIVITY_DRAIN_MS); + const log = sbot.db.getLog(); + const read = () => { + const since = log.since.value; + if (!Number.isFinite(since)) return settle(idx.offset.value >= 0); + idx.whenReached(since, ACTIVITY_DRAIN_MS, settle); + }; + if (opts && opts.fresh === false) read(); + else log.onDrain(read); + }, + tail(limit, opts, cb) { + if (typeof limit === 'function') { cb = limit; limit = 1; opts = {}; } + if (typeof opts === 'function') { cb = opts; opts = {}; } + const want = Math.max(1, Math.min(Number(limit) || 1, RECENT_KEEP)); + const idx = sbot.db.getIndex('oasisActivity'); + if (!idx) return cb(null, null); + const log = sbot.db.getLog(); + const read = () => { + const since = log.since.value; + if (!Number.isFinite(since)) return cb(null, []); + idx.whenReached(since, ACTIVITY_DRAIN_MS, (ok) => { + const offsets = idx.recent; + if (!ok || !offsets.length || offsets[offsets.length - 1] !== since) return cb(null, null); + if (offsets.length < want && offsets[0] !== 0) return cb(null, null); + const picked = offsets.slice(-want).reverse(); + const out = new Array(picked.length); + let left = picked.length; + let failed = false; + picked.forEach((offset, i) => log.get(offset, (err, buf) => { + if (failed) return; + let m = null; + try { m = !err && buf ? bipf.decode(buf, 0) : null; } catch (_) { m = null; } + if (!m || !m.key || !m.value) { failed = true; return cb(null, null); } + out[i] = legacy(m); + if (--left === 0) cb(null, out); + })); + }); + }; + if (opts && opts.fresh === false) read(); + else log.onDrain(read); + } + }; + } + }, + { + name: 'backlinks', + version: '1.0.0', + manifest: { read: 'source' }, + init(sbot) { + return { + read(opts = {}) { + const { stream, linked, liveLinked, sortLegacy, matchFilter, opsFor } = sbot._oasisDb2; + const filter = (opts.query && opts.query[0] && opts.query[0].$filter) || {}; + const priv = opts.private === true; + const pass = (m) => matchFilter(m, filter); + if (!filter.dest) { + return stream(opsFor(filter), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter: pass, private: priv }); + } + const src = defer.source(); + const limit = typeof opts.limit === 'number' && opts.limit >= 0 ? opts.limit : -1; + const fmt = formatter(opts); + const live$ = opts.live ? pull(liveLinked(filter.dest, { private: priv }), pull.filter(pass)) : null; + const finish = (arr) => { + let out = sortLegacy(arr.filter(pass), opts.index === 'DTA', opts.reverse === true); + if (limit >= 0) out = out.slice(0, limit); + if (!live$) return src.resolve(pull(pull.values(out), pull.map(fmt))); + src.resolve(pull(withSync(pull.values(out), live$, opts), pull.map(fmt))); + }; + if (opts.old === false && opts.live) return finish([]), src; + linked(filter.dest, { private: priv }, (err, arr) => { + if (!err) return finish(arr); + if (live$) live$(err, () => {}); + src.abort(err); + }); + return src; + } + }; + } + }, + { + name: 'query', + version: '1.0.0', + manifest: { read: 'source' }, + init(sbot) { + return { + read(opts = {}) { + const { stream, opsFor, filterOf } = sbot._oasisDb2; + const filter = (opts.query && opts.query[0] && opts.query[0].$filter) || null; + return stream(opsFor(filter), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter: filterOf(filter), skipPrivate: true, keys: opts.keys, values: opts.values }); + } + }; + } + }, + { + name: 'search', + version: '1.0.0', + manifest: { query: 'source' }, + init(sbot) { + const textOf = (c) => ['text', 'title', 'description', 'body', 'name', 'summary'].map(k => c && typeof c[k] === 'string' ? c[k] : '').join('\n').toLowerCase(); + return { + query(opts = {}) { + const needle = String(opts.query || '').trim().toLowerCase(); + if (!needle) return pull.empty(); + const terms = needle.split(/\s+/).filter(Boolean); + const limit = typeof opts.limit === 'number' && opts.limit > 0 ? opts.limit : SEARCH_LIMIT; + return sbot._oasisDb2.stream(null, { + reverse: opts.reverse !== false, + limit, + skipPrivate: true, + arrival: true, + filter: (m) => { const c = m.value && m.value.content; if (!c || typeof c !== 'object') return false; const hay = textOf(c); return terms.every(t => hay.includes(t)); } + }); + } + }; + } + }, + { + name: 'tangle', + version: '1.0.0', + manifest: { branch: 'async' }, + init(sbot) { + return { + branch(rootKey, cb) { + if (typeof rootKey !== 'string' || !rootKey.startsWith('%')) return cb(new Error('tangle.branch requires a message key')); + sbot._oasisDb2.linked(rootKey, {}, (err, arr) => { + if (err) return cb(err); + const thread = arr.filter(m => m.value && m.value.content && typeof m.value.content === 'object' && m.value.content.root === rootKey); + cb(null, heads([{ key: rootKey }, ...thread])); + }); + } + }; + } + } +]; + +module.exports.LinksIndex = LinksIndex; +module.exports.legacy = legacy; +module.exports.matchFilter = matchFilter; diff --git a/src/server/lanRouter.js b/src/server/lanRouter.js new file mode 100644 index 0000000..a4c6079 --- /dev/null +++ b/src/server/lanRouter.js @@ -0,0 +1,97 @@ +const pull = require('./node_modules/pull-stream'); +const Ref = require('./node_modules/ssb-ref'); +const fs = require('fs'); +const path = require('path'); + +const staged = new Set(); + +function readOasisConfig() { + try { + const dir = process.env.OASIS_STATE_DIR || process.env.ssb_path || path.join(require('os').homedir(), '.ssb'); + const p = [path.join(dir, 'oasis', 'oasis-config.json'), path.join(dir, 'oasis-config.json')].find(f => fs.existsSync(f)); + return JSON.parse(fs.readFileSync(p || path.join(__dirname, '..', 'configs', 'oasis-config.json'), 'utf8')) || {}; + } catch (_) { return {}; } +} + +function stagePeer(ssb, address, key, eagerReplicate) { + if (!address || !key || key === ssb.id) return; + if (staged.has(address)) return; + staged.add(address); + let routed = false; + try { + if (ssb.conn && typeof ssb.conn.stage === 'function') { + ssb.conn.stage(address, { type: 'lan', key }); + routed = true; + } + } catch (_) {} + try { require('../models/viewer_filters').rememberLanPeers([key]); } catch (_) {} + if (!routed) { + try { + if (ssb.gossip && typeof ssb.gossip.add === 'function') { + ssb.gossip.add(address, 'local'); + } + } catch (_) {} + } + if (eagerReplicate) { + try { + if (ssb.ebt && typeof ssb.ebt.request === 'function') { + ssb.ebt.request(key, true); + } + } catch (_) {} + try { + if (ssb.replicate && typeof ssb.replicate.request === 'function') { + ssb.replicate.request(key, true); + } + } catch (_) {} + } +} + +function handleDiscovery(ssb, d, opts) { + if (!d || !d.address) return; + if (!d.verified && !opts.acceptUnverified) return; + let key = null; + try { key = Ref.getKeyFromAddress(d.address); } catch (_) {} + if (key) stagePeer(ssb, d.address, key, opts.eagerReplicate); +} + +function guardLan(ssb, isEnabled) { + const lan = ssb && ssb.lan; + if (!lan || typeof lan.start !== 'function' || lan.oasisGuarded) return false; + const start = lan.start.bind(lan); + lan.start = (...args) => { + if (!isEnabled()) return undefined; + try { return start(...args); } catch (_) { return undefined; } + }; + lan.oasisGuarded = true; + if (!isEnabled() && typeof lan.stop === 'function') { try { lan.stop(); } catch (_) {} } + return true; +} + +function startRouter(ssb, opts, isEnabled) { + if (!ssb.lan || typeof ssb.lan.discoveredPeers !== 'function') return; + if (isEnabled()) { try { ssb.lan.start(); } catch (_) {} } + else if (typeof ssb.lan.stop === 'function') { try { ssb.lan.stop(); } catch (_) {} } + pull( + ssb.lan.discoveredPeers(), + pull.drain(d => handleDiscovery(ssb, d, opts), () => {}) + ); +} + +module.exports = { + name: 'lanRouter', + version: '1.1.0', + manifest: {}, + init(ssb, config) { + const lanCfg = (config && config.lan) || {}; + const opts = { + acceptUnverified: lanCfg.acceptUnverified === true, + eagerReplicate: lanCfg.eagerReplicate === true + }; + const isEnabled = () => { const c = readOasisConfig(); return c.lanBroadcasting !== false && c.networkPaused !== true; }; + guardLan(ssb, isEnabled); + setImmediate(() => startRouter(ssb, opts, isEnabled)); + return {}; + } +}; + +module.exports.guardLan = guardLan; diff --git a/src/server/network_pause.js b/src/server/network_pause.js new file mode 100644 index 0000000..9b92677 --- /dev/null +++ b/src/server/network_pause.js @@ -0,0 +1,210 @@ +const fs = require('fs'); +const path = require('path'); + +const readOasisConfig = () => { + try { + const dir = process.env.OASIS_STATE_DIR || process.env.ssb_path || path.join(require('os').homedir(), '.ssb'); + const p = [path.join(dir, 'oasis', 'oasis-config.json'), path.join(dir, 'oasis-config.json')].find(f => fs.existsSync(f)); + return JSON.parse(fs.readFileSync(p || path.join(__dirname, '..', 'configs', 'oasis-config.json'), 'utf8')) || {}; + } catch (_) { return {}; } +}; + +module.exports = { + name: 'networkPause', + version: '1.0.0', + manifest: { pause: 'sync', resume: 'sync', paused: 'sync', lastErrors: 'sync' }, + init(server, config) { + server.oasisNetworkPaused = readOasisConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1'; + const refused = () => new Error('network paused'); + + if (server.auth && typeof server.auth.hook === 'function') { + server.auth.hook(function (fn, args) { + const cb = args[args.length - 1]; + if (server.oasisNetworkPaused && args[0] !== server.id && typeof cb === 'function') return cb(refused()); + return fn.apply(this, args); + }); + } + if (server.connect && typeof server.connect.hook === 'function') { + server.connect.hook(function (fn, args) { + const cb = args[args.length - 1]; + if (server.oasisNetworkPaused && typeof cb === 'function') return cb(refused()); + return fn.apply(this, args); + }); + } + + const closeAll = () => { + try { for (const [addr] of server.conn.query().peersInConnection()) server.conn.disconnect(addr, () => {}); } catch (_) {} + for (const id of Object.keys(server.peers || {})) { + if (id === server.id) continue; + for (const rpc of (server.peers[id] || []).slice()) { try { rpc.close(true, () => {}); } catch (_) {} } + } + }; + + const lastErrors = new Map(); + const keyOf = (ev) => { + const { canonicalKey } = require('../models/peer_health'); + const m = String((ev && ev.address) || '').match(/~shs:([A-Za-z0-9+/=_-]{43,44})/); + return canonicalKey((ev && ev.key) || (m ? m[1] : '')); + }; + const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0); + const ownVersion = (() => { try { return String(require('./package.json').version || ''); } catch (_) { return ''; } })(); + const upToDate = (v) => { + const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(ownVersion); + return a1 > a2 || (a1 === a2 && b1 >= b2); + }; + const rank = { versions: new Map(), graph: {} }; + const refreshRank = () => { + const pull = require('pull-stream'); + const latest = new Map(); + const done = () => { + rank.versions = new Map([...latest].map(([k, m]) => [k, m.version])); + try { server.friends.graph((err, g) => { if (!err && g) rank.graph = g; }); } catch (_) {} + }; + try { + if (typeof server.messagesByType !== 'function') return done(); + pull(server.messagesByType({ type: 'oasisVersion' }), pull.drain((m) => { + const author = m && m.value && m.value.author; + const c = m && m.value && m.value.content; + if (!author || !c || typeof c.version !== 'string') return; + const ts = Number(m.value.timestamp) || 0; + const prev = latest.get(author); + if (!prev || prev.ts <= ts) latest.set(author, { version: c.version, ts }); + }, done)); + } catch (_) { done(); } + }; + const peerRank = (peer) => { + const key = peer && peer[1] && peer[1].key; + if (!key) return 0; + const follows = rank.graph[key] || {}; + const mine = rank.graph[server.id] || {}; + let total = 0, shared = 0; + for (const [k, v] of Object.entries(follows)) { + if (!(v >= 0) || k === key) continue; + total++; + if (mine[k] >= 0) shared++; + } + const v = rank.versions.get(key); + const tier = v ? (upToDate(v) ? 2 : 0) : (total > 0 ? 0 : 1); + return tier * 1e9 + Math.min(shared, 99999) * 1e4 + Math.min(total, 9999); + }; + server.oasisPeerRank = peerRank; + setImmediate(refreshRank); + const rankTimer = setInterval(refreshRank, 10 * 60 * 1000); + if (rankTimer.unref) rankTimer.unref(); + const fallbackAt = new Map(); + const tryOtherAddress = (key, failed, reason) => { + if (reason === 'paused' || server.oasisNetworkPaused) return; + if (Array.isArray(server.peers && server.peers[key]) && server.peers[key].length) return; + const now = Date.now(); + if (now - (fallbackAt.get(key) || 0) < 10 * 60 * 1000) return; + const { canonicalKey } = require('../models/peer_health'); + let entries = []; + try { entries = server.conn.dbPeers(); } catch (_) { return; } + const failedOnion = /^onion:/.test(String(failed || '')); + const alt = entries.find(([addr, data]) => addr && addr !== failed && data && canonicalKey(data.key) === key && /^onion:/.test(addr) !== failedOnion); + if (!alt) return; + fallbackAt.set(key, now); + try { server.conn.connect(alt[0], { ...(alt[1] || {}) }, () => {}); } catch (_) {} + }; + let listening = false; + const listen = () => { + if (listening) return; + try { + const { classifyNetError } = require('../models/peer_health'); + const hub = server.conn && typeof server.conn.hub === 'function' ? server.conn.hub() : null; + const stream = hub && typeof hub.listen === 'function' ? hub.listen() : null; + if (!stream) return; + listening = true; + require('pull-stream')(stream, require('pull-stream').drain((ev) => { + const key = ev && keyOf(ev); + if (!key) return; + if (ev.type === 'connected') lastErrors.delete(key); + else if (ev.type === 'connecting-failed') { + const reason = classifyNetError(ev.details, ev.address); + lastErrors.set(key, { reason, at: Date.now() }); + if (lastErrors.size > 500) lastErrors.delete(lastErrors.keys().next().value); + tryOtherAddress(key, ev.address, reason); + } + }, () => { listening = false; })); + } catch (_) { listening = false; } + }; + const listenTimer = setInterval(() => { listen(); if (listening) clearInterval(listenTimer); }, 1000); + if (listenTimer.unref) listenTimer.unref(); + + const readList = (file) => { try { const a = JSON.parse(fs.readFileSync(file, 'utf8')); return Array.isArray(a) ? a : []; } catch (_) { return []; } }; + const syncPeerBook = () => { + if (!server.conn || typeof server.conn.dbPeers !== 'function' || !config || !config.path) return; + const { canonicalKey } = require('../models/peer_health'); + let forgottenFile = null; + try { forgottenFile = require('../configs/state-manager').statePath('gossip_unfollowed.json'); } catch (_) {} + const forgotten = new Set((forgottenFile ? readList(forgottenFile) : []).map(e => e && canonicalKey(e.key)).filter(Boolean)); + let entries = []; + try { entries = server.conn.dbPeers(); } catch (_) { return; } + for (const [addr, data] of entries) { + const m = String(addr).match(/^(.*~shs:)([A-Za-z0-9+/]{43})$/); + if (!m) continue; + const padded = `${m[1]}${m[2]}=`; + try { + if (!server.conn.db().has(padded)) server.conn.remember(padded, { ...(data || {}) }); + else if (data && data.autoconnect === true && typeof server.conn.db().update === 'function') server.conn.db().update(padded, { autoconnect: true }); + server.conn.forget(addr); + } catch (_) {} + } + try { entries = server.conn.dbPeers(); } catch (_) { return; } + for (const [addr, data] of entries) { + const key = data && canonicalKey(data.key); + if (key && forgotten.has(key)) { try { server.conn.forget(addr); } catch (_) {} } + } + const chosen = new Map(); + for (const p of readList(path.join(config.path, 'gossip.json'))) { + const key = p && canonicalKey(p.key); + const host = p ? String(p.host || '').trim() : ''; + if (!key || key === server.id || forgotten.has(key) || !host) continue; + const onion = /\.onion$/i.test(host); + const prev = chosen.get(key); + if (!prev || (prev.onion && !onion)) chosen.set(key, { host, port: Number(p.port) || 8008, onion }); + } + for (const [key, p] of chosen) { + const addr = `${p.onion ? 'onion' : 'net'}:${p.host}:${p.port}~shs:${key.slice(1, -8)}`; + try { if (!server.conn.db().has(addr)) server.conn.remember(addr, { key, type: 'pub', autoconnect: true }); } catch (_) {} + } + const seeds = [].concat(config.seeds || [], (config.connections && config.connections.seeds) || []); + for (const seed of seeds) { + const addr = String(seed || '').trim(); + const m = addr.match(/~shs:([A-Za-z0-9+/]{43}=?)$/); + const key = m && canonicalKey(m[1]); + if (!key || key === server.id || forgotten.has(key)) continue; + try { if (!server.conn.db().has(addr)) server.conn.remember(addr, { key, type: 'pub', autoconnect: true }); } catch (_) {} + } + if (config.pub && typeof server.conn.db().update === 'function') { + try { entries = server.conn.dbPeers(); } catch (_) { return; } + for (const [addr, data] of entries) { + const key = data && canonicalKey(data.key); + if (!key || key === server.id || forgotten.has(key) || data.type !== 'pub' || data.autoconnect !== false) continue; + try { server.conn.db().update(addr, { autoconnect: true }); } catch (_) {} + } + } + }; + for (const ms of [5000, 30000]) { const t = setTimeout(syncPeerBook, ms); if (t.unref) t.unref(); } + const bookTimer = setInterval(syncPeerBook, 10 * 60 * 1000); + if (bookTimer.unref) bookTimer.unref(); + + return { + lastErrors() { return Object.fromEntries(lastErrors); }, + pause() { + server.oasisNetworkPaused = true; + try { server.conn.stop(); } catch (_) {} + try { if (server.lan && typeof server.lan.stop === 'function') server.lan.stop(); } catch (_) {} + closeAll(); + return true; + }, + resume() { + server.oasisNetworkPaused = false; + try { server.conn.start(); } catch (_) {} + try { if (server.lan && typeof server.lan.start === 'function') server.lan.start(); } catch (_) {} + return false; + }, + paused() { return !!server.oasisNetworkPaused; } + }; + } +}; diff --git a/src/server/package-lock.json b/src/server/package-lock.json index f8ea18b..734be1b 100644 --- a/src/server/package-lock.json +++ b/src/server/package-lock.json @@ -1,32 +1,27 @@ { "name": "@krakenslab/oasis", - "version": "0.8.7", + "version": "1.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@krakenslab/oasis", - "version": "0.7.8", + "version": "1.2.3", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { "@koa/router": "^13.1.0", - "@open-rpc/client-js": "^1.8.1", - "@xenova/transformers": "^2.17.2", - "abstract-level": "^2.0.1", "archiver": "^7.0.1", "axios": "^1.10.0", - "cors": "^2.8.5", + "bencode": "^2.0.3", + "bipf": "^1.9.0", + "chloride": "^2.4.1", "debug": "^4.3.1", "dompurify": "^3.3.1", "env-paths": "^2.2.1", - "epidemic-broadcast-trees": "^9.0.4", - "express": "^5.1.0", "file-type": "^16.5.4", "highlight.js": "11.0.0", "hyperaxe": "^2.0.1", - "ip": "https://registry.npmjs.org/neoip/-/neoip-3.0.0.tgz", - "is-svg": "^4.4.0", "jsdom": "^28.0.0", "koa": "^2.7.0", "koa-body": "^6.0.1", @@ -36,90 +31,58 @@ "minimist": "^1.2.8", "module-alias": "^2.2.3", "moment": "^2.30.1", - "multiblob": "^1.13.0", - "multiserver": "^3.3.1", - "multiserver-address": "^1.0.1", - "muxrpc": "^8.0.0", - "muxrpc-validation": "^3.0.2", - "muxrpcli": "^3.1.2", - "node-llama-cpp": "^3.10.0", - "non-private-ip": "^2.2.0", "open": "^8.4.2", "openpgp": "^6.3.0", - "packet-stream": "^2.0.6", - "packet-stream-codec": "^1.2.0", - "pdfjs-dist": "^5.2.133", + "opusscript": "^0.1.1", "pretty-ms": "^7.0.1", "pull-abortable": "^4.1.1", "pull-cat": "~1.1.5", - "pull-file": "^1.0.0", - "pull-many": "~1.0.6", + "pull-defer": "^0.2.3", + "pull-level": "^2.0.4", "pull-paramap": "^1.2.2", "pull-pushable": "^2.2.0", "pull-sort": "^1.0.2", "pull-stream": "^3.7.0", - "punycode.js": "^2.3.1", "qrcode": "^1.5.4", "secret-stack": "^6.3.1", - "ssb-about": "^2.0.1", "ssb-autofollow": "^1.1.0", - "ssb-backlinks": "^2.1.1", "ssb-blobs": "^2.0.1", "ssb-box": "^1.0.1", "ssb-caps": "^1.0.1", + "ssb-classic": "^1.1.0", "ssb-client": "^4.9.0", "ssb-config": "^3.4.4", "ssb-conn": "6.0.3", - "ssb-conn-db": "^1.0.5", - "ssb-conn-hub": "^1.2.0", - "ssb-conn-staging": "^1.0.0", - "ssb-db": "^20.4.1", - "ssb-device-address": "^1.1.6", + "ssb-db2": "^6.3.3", "ssb-ebt": "^9.1.2", - "ssb-friend-pub": "^1.0.7", "ssb-friends": "^5.0.0", - "ssb-gossip": "^1.1.1", "ssb-invite": "^3.0.3", "ssb-invite-client": "^1.3.3", "ssb-keys": "^8.0.0", "ssb-lan": "^1.0.0", - "ssb-legacy-conn": "^1.0.17", - "ssb-links": "^3.0.10", "ssb-logging": "^1.0.0", - "ssb-markdown": "^3.6.0", "ssb-master": "^1.0.3", - "ssb-meme": "^1.1.0", "ssb-mentions": "^0.5.2", "ssb-msgs": "^5.2.0", "ssb-no-auth": "^1.0.0", "ssb-onion": "^1.0.0", - "ssb-partial-replication": "^3.0.1", - "ssb-plugins": "^1.0.2", - "ssb-private": "^1.1.0", - "ssb-query": "^2.4.5", "ssb-ref": "^2.16.0", "ssb-replication-scheduler": "^3.0.0", - "ssb-search": "^1.3.0", - "ssb-server": "file:packages/ssb-server", - "ssb-tangle": "^1.0.1", + "ssb-sort": "^1.1.3", "ssb-thread-schema": "^1.1.1", - "ssb-threads": "^10.0.4", "ssb-unix-socket": "^1.0.0", - "ssb-ws": "^6.2.3", - "util": "^0.12.5", + "ssb-validate": "^4.1.4", + "stream-to-pull-stream": "^1.7.3", + "telegram": "^2.26.22", "yargs": "^17.7.2" }, "bin": { "oasis": "npm run start" }, - "devDependencies": { - "nodemon": "^3.1.0" - }, "engines": { "node": "^10.0.0 || >=12.0.0" }, "optionalDependencies": { - "fsevents": "^2.3.2", "sharp": "^0.33.5" } }, @@ -131,6 +94,8 @@ }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "license": "MIT", "engines": { "node": ">=10" @@ -189,177 +154,11 @@ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "license": "MIT" }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } + "node_modules/@cryptography/aes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@cryptography/aes/-/aes-0.1.1.tgz", + "integrity": "sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg==", + "license": "GPL-3.0-or-later" }, "node_modules/@csstools/color-helpers": { "version": "6.0.1", @@ -506,36 +305,71 @@ }, "node_modules/@hapi/bourne": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", "license": "BSD-3-Clause" }, - "node_modules/@huggingface/jinja": { - "version": "0.5.5", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { + "node_modules/@img/sharp-libvips-linux-arm64": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ - "x64" + "arm64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], + "libc": [ + "glibc" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "libc": [ + "musl" + ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], "license": "LGPL-3.0-or-later", - "optional": true, "os": [ "linux" ], @@ -543,13 +377,39 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, "node_modules/@img/sharp-linux-x64": { "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], "license": "Apache-2.0", - "optional": true, "os": [ "linux" ], @@ -563,13 +423,39 @@ "@img/sharp-libvips-linux-x64": "1.0.4" } }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "libc": [ + "musl" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, "node_modules/@img/sharp-linuxmusl-x64": { "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], "license": "Apache-2.0", - "optional": true, "os": [ "linux" ], @@ -585,6 +471,8 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -600,6 +488,8 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -610,6 +500,8 @@ }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -621,59 +513,10 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@koa/router": { "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", + "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", "license": "MIT", "dependencies": { "http-errors": "^2.0.0", @@ -684,91 +527,10 @@ "node": ">= 18" } }, - "node_modules/@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1" - } - }, - "node_modules/@kwsites/promise-deferred": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", - "license": "MIT" - }, - "node_modules/@ljharb/resumer": { - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "@ljharb/through": "^2.3.9" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@ljharb/through": { - "version": "2.3.14", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@napi-rs/canvas": { - "version": "0.1.70", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.70", - "@napi-rs/canvas-darwin-arm64": "0.1.70", - "@napi-rs/canvas-darwin-x64": "0.1.70", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.70", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.70", - "@napi-rs/canvas-linux-arm64-musl": "0.1.70", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.70", - "@napi-rs/canvas-linux-x64-gnu": "0.1.70", - "@napi-rs/canvas-linux-x64-musl": "0.1.70", - "@napi-rs/canvas-win32-x64-msvc": "0.1.70" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.70", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.70", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@noble/hashes": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -777,521 +539,10 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@node-llama-cpp/linux-arm64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-arm64/-/linux-arm64-3.16.2.tgz", - "integrity": "sha512-CxzgPsS84wL3W5sZRgxP3c9iJKEW+USrak1SmX6EAJxW/v9QGzehvT6W/aR1FyfidiIyQtOp3ga0Gg/9xfJPGw==", - "cpu": [ - "arm64", - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-armv7l": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-armv7l/-/linux-armv7l-3.16.2.tgz", - "integrity": "sha512-9G6W/MkQ/DLwGmpcj143NQ50QJg5gQZfzVf5RYx77VczBqhgwkgYHILekYrOs4xanOeqeJ8jnOnQQSp1YaJZUg==", - "cpu": [ - "arm", - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-x64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64/-/linux-x64-3.16.2.tgz", - "integrity": "sha512-OXYf8rVfoDyvN+YrfKk8F9An9a5GOxVIM8OcR1U911tc0oRNf8yfJrQ8KrM75R26gwq0Y6YZwVTP0vRCInwWOw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-x64-cuda": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda/-/linux-x64-cuda-3.16.2.tgz", - "integrity": "sha512-LTBQFqjin7tyrLNJz0XWTB5QAHDsZV71/qiiRRjXdBKSZHVVaPLfdgxypGu7ggPeBNsv+MckRXdlH5C7yMtE4A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-x64-cuda-ext": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda-ext/-/linux-x64-cuda-ext-3.16.2.tgz", - "integrity": "sha512-47d9myCJauZyzAlN7IK1eIt/4CcBMslF+yHy4q+yJotD/RV/S6qRpK2kGn+ybtdVjkPGNCoPkHKcyla9iIVjbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-x64-vulkan": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-vulkan/-/linux-x64-vulkan-3.16.2.tgz", - "integrity": "sha512-HDLAw4ZhwJuhKuF6n4x520yZXAQZahUOXtCGvPubjfpmIOElKrfDvCVlRsthAP0JwcwINzIQlVys3boMIXfBgw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/mac-arm64-metal": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-arm64-metal/-/mac-arm64-metal-3.16.2.tgz", - "integrity": "sha512-nEZ74qB0lUohF88yR741YUrUqz/qD+FJFzUTHj0FwxAynSZCjvwtzEDtavRlh3qd3yLD/0ChNn00/RQ54ISImw==", - "cpu": [ - "arm64", - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/mac-x64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-x64/-/mac-x64-3.16.2.tgz", - "integrity": "sha512-BjA+DgeDt+kRxVMV6kChb9XVXm7U5b90jUif7Z/s6ZXtOOnV6exrTM2W09kbSqAiNhZmctcVY83h2dwNTZ/yIw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-arm64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-arm64/-/win-arm64-3.16.2.tgz", - "integrity": "sha512-XHNFQzUjYODtkZjIn4NbQVrBtGB9RI9TpisiALryqfrIqagQmjBh6dmxZWlt5uduKAfT7M2/2vrABGR490FACA==", - "cpu": [ - "arm64", - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-x64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64/-/win-x64-3.16.2.tgz", - "integrity": "sha512-etrivzbyLNVhZlUosFW8JSL0OSiuKQf9qcI3dNdehD907sHquQbBJrG7lXcdL6IecvXySp3oAwCkM87VJ0b3Fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-x64-cuda": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda/-/win-x64-cuda-3.16.2.tgz", - "integrity": "sha512-jStDELHrU3rKQMOk5Hs5bWEazyjE2hzHwpNf6SblOpaGkajM/HJtxEZoL0mLHJx5qeXs4oOVkr7AzuLy0WPpNA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-x64-cuda-ext": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda-ext/-/win-x64-cuda-ext-3.16.2.tgz", - "integrity": "sha512-sdv4Kzn9bOQWNBRvw6B/zcn8dQRfZhjIHv5AfDBIOfRlSCgjebFpBeYUoU4wZPpjr3ISwcqO5MEWsw+AbUdV3Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-x64-vulkan": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-vulkan/-/win-x64-vulkan-3.16.2.tgz", - "integrity": "sha512-9xuHFCOhCQjZgQSFrk79EuSKn9nGWt/SAq/3wujQSQLtgp8jGdtZgwcmuDUoemInf10en2dcOmEt7t8dQdC3XA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@octokit/app": { - "version": "16.1.2", - "license": "MIT", - "dependencies": { - "@octokit/auth-app": "^8.1.2", - "@octokit/auth-unauthenticated": "^7.0.3", - "@octokit/core": "^7.0.6", - "@octokit/oauth-app": "^8.0.3", - "@octokit/plugin-paginate-rest": "^14.0.0", - "@octokit/types": "^16.0.0", - "@octokit/webhooks": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-app": { - "version": "8.2.0", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.3", - "@octokit/auth-oauth-user": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "toad-cache": "^3.7.0", - "universal-github-app-jwt": "^2.2.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-app": { - "version": "9.0.3", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.3", - "@octokit/auth-oauth-user": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-device": { - "version": "8.0.3", - "license": "MIT", - "dependencies": { - "@octokit/oauth-methods": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-user": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.3", - "@octokit/oauth-methods": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-unauthenticated": { - "version": "7.0.3", - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/core": { - "version": "7.0.6", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.3", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/endpoint": { - "version": "11.0.2", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/graphql": { - "version": "9.0.3", - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app": { - "version": "8.0.3", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.2", - "@octokit/auth-oauth-user": "^6.0.1", - "@octokit/auth-unauthenticated": "^7.0.2", - "@octokit/core": "^7.0.5", - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/oauth-methods": "^6.0.1", - "@types/aws-lambda": "^8.10.83", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-methods": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "27.0.0", - "license": "MIT" - }, - "node_modules/@octokit/openapi-webhooks-types": { - "version": "12.1.0", - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-graphql": { - "version": "6.0.0", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "14.0.0", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "17.0.0", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-retry": { - "version": "8.0.3", - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=7" - } - }, - "node_modules/@octokit/plugin-throttling": { - "version": "11.0.3", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": "^7.0.0" - } - }, - "node_modules/@octokit/request": { - "version": "10.0.7", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^11.0.2", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "fast-content-type-parse": "^3.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error": { - "version": "7.1.0", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/types": { - "version": "16.0.0", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^27.0.0" - } - }, - "node_modules/@octokit/webhooks": { - "version": "14.2.0", - "license": "MIT", - "dependencies": { - "@octokit/openapi-webhooks-types": "12.1.0", - "@octokit/request-error": "^7.0.0", - "@octokit/webhooks-methods": "^6.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/webhooks-methods": { - "version": "6.0.0", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@open-rpc/client-js": { - "version": "1.8.1", - "license": "Apache-2.0", - "dependencies": { - "isomorphic-fetch": "^3.0.0", - "isomorphic-ws": "^5.0.0", - "strict-event-emitter-types": "^2.0.0", - "ws": "^7.0.0" - } - }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", "license": "MIT", "dependencies": { "@noble/hashes": "^1.1.5" @@ -1299,130 +550,17 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", - "optional": true, "engines": { "node": ">=14" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" - }, - "node_modules/@reflink/reflink": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink/-/reflink-0.1.19.tgz", - "integrity": "sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@reflink/reflink-darwin-arm64": "0.1.19", - "@reflink/reflink-darwin-x64": "0.1.19", - "@reflink/reflink-linux-arm64-gnu": "0.1.19", - "@reflink/reflink-linux-arm64-musl": "0.1.19", - "@reflink/reflink-linux-x64-gnu": "0.1.19", - "@reflink/reflink-linux-x64-musl": "0.1.19", - "@reflink/reflink-win32-arm64-msvc": "0.1.19", - "@reflink/reflink-win32-x64-msvc": "0.1.19" - } - }, - "node_modules/@reflink/reflink-linux-x64-gnu": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-gnu/-/reflink-linux-x64-gnu-0.1.19.tgz", - "integrity": "sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-linux-x64-musl": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-musl/-/reflink-linux-x64-musl-0.1.19.tgz", - "integrity": "sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@sammacbeth/random-access-idb-mutable-file": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@sammacbeth/random-access-idb-mutable-file/-/random-access-idb-mutable-file-0.1.1.tgz", + "integrity": "sha512-jHnpuu2qtFgwCmhgrpCCk3/hU3XqXTqhidh4XmcTijkVsGwh1c2T0+r2hkHs1PRfsxeimx8qDAotphpRoYB2eg==", "license": "MIT", "dependencies": { "buffer": "5.1.0", @@ -1431,6 +569,8 @@ }, "node_modules/@sammacbeth/random-access-idb-mutable-file/node_modules/buffer": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", + "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", "license": "MIT", "dependencies": { "base64-js": "^1.0.2", @@ -1439,28 +579,17 @@ }, "node_modules/@sammacbeth/random-access-idb-mutable-file/node_modules/random-access-storage": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-1.3.0.tgz", + "integrity": "sha512-pdS9Mcb9TB7oICypPRALlheaSuszuAKmLVEPKJMuYor7R/zDuHh5ALuQoS+ox31XRwQUL+tDwWH2GPdyspwelA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3" } }, - "node_modules/@simple-git/args-pathspec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", - "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", - "license": "MIT" - }, - "node_modules/@simple-git/argv-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", - "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", - "license": "MIT", - "dependencies": { - "@simple-git/args-pathspec": "^1.0.3" - } - }, "node_modules/@tangle/graph": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tangle/graph/-/graph-3.2.0.tgz", + "integrity": "sha512-I92Prubc9XVRmMZkshMr2u+REuwqhDQYcFtZuHmY8yP6TVpeDmWRUZY5AKCWYdeA58igTPUbPCpvLT9iRXiRBA==", "license": "LGPL-3.0-only", "dependencies": { "lodash.clone": "^4.5.0", @@ -1469,6 +598,8 @@ }, "node_modules/@tangle/overwrite-fields": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tangle/overwrite-fields/-/overwrite-fields-2.0.3.tgz", + "integrity": "sha512-0VKHs1czriZOpVgc1aZve3lwpwgw+1+/P/wis2hH3FbACbfIYByIU1EgykVKeu/HbE3nRWLip83llFbr6Rjq4A==", "license": "LGPL-3.0-or-later", "dependencies": { "is-my-json-valid": "^2.20.5", @@ -1477,6 +608,8 @@ }, "node_modules/@tangle/reduce": { "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@tangle/reduce/-/reduce-5.0.5.tgz", + "integrity": "sha512-+GRGgvzt57fZjyGCamEc3uyh8onHlHYxuFF4HhRY4WDk9aky+WUBa8YSeFKANnGpBrptdzQuo3+VFIYucbpS3w==", "license": "LGPL-3.0-only", "dependencies": { "@tangle/graph": "^3.2.0" @@ -1484,42 +617,33 @@ }, "node_modules/@tangle/strategy": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@tangle/strategy/-/strategy-4.1.2.tgz", + "integrity": "sha512-P5qGsPuAMqZx/zJA20nwpqD3A5aGDnmjEbp221jRTClI+NSU/spcqkryOP7iH8UCRWiuzjWHA9To5ymw/xOsCA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-json-valid": "^2.20.5", "lodash.isequal": "^4.5.0" } }, - "node_modules/@tinyhttp/content-disposition": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.4.tgz", - "integrity": "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==", - "license": "MIT", - "engines": { - "node": ">=12.17.0" - }, - "funding": { - "type": "individual", - "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" - } - }, "node_modules/@tokenizer/token": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, "node_modules/@types/accepts": { "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/aws-lambda": { - "version": "8.10.160", - "license": "MIT" - }, "node_modules/@types/body-parser": { "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1528,6 +652,8 @@ }, "node_modules/@types/co-body": { "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", + "integrity": "sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -1536,6 +662,8 @@ }, "node_modules/@types/connect": { "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -1543,10 +671,14 @@ }, "node_modules/@types/content-disposition": { "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", "license": "MIT" }, "node_modules/@types/cookies": { "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==", "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1557,6 +689,8 @@ }, "node_modules/@types/express": { "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -1566,6 +700,8 @@ }, "node_modules/@types/express-serve-static-core": { "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -1576,6 +712,8 @@ }, "node_modules/@types/formidable": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz", + "integrity": "sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -1583,18 +721,26 @@ }, "node_modules/@types/http-assert": { "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", "license": "MIT" }, "node_modules/@types/http-errors": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT" }, "node_modules/@types/keygrip": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", "license": "MIT" }, "node_modules/@types/koa": { "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", "license": "MIT", "dependencies": { "@types/accepts": "*", @@ -1609,23 +755,23 @@ }, "node_modules/@types/koa-compose": { "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", "license": "MIT", "dependencies": { "@types/koa": "*" } }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT" - }, "node_modules/@types/mime": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, "node_modules/@types/node": { "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1633,14 +779,20 @@ }, "node_modules/@types/qs": { "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -1649,6 +801,8 @@ }, "node_modules/@types/serve-static": { "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -1660,63 +814,12 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@xenova/transformers": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz", - "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==", - "license": "Apache-2.0", - "dependencies": { - "@huggingface/jinja": "^0.2.2", - "onnxruntime-web": "1.14.0", - "sharp": "^0.32.0" - }, - "optionalDependencies": { - "onnxruntime-node": "1.14.0" - } - }, - "node_modules/@xenova/transformers/node_modules/@huggingface/jinja": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz", - "integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@xenova/transformers/node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "license": "MIT" }, - "node_modules/@xenova/transformers/node_modules/sharp": { - "version": "0.32.6", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", - "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/abort-controller": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -1725,23 +828,10 @@ "node": ">=6.5" } }, - "node_modules/abstract-level": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "is-buffer": "^2.0.5", - "level-supports": "^6.0.0", - "level-transcoder": "^1.0.1", - "maybe-combine-errors": "^1.0.0", - "module-error": "^1.0.1" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/abstract-leveldown": { "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -1756,6 +846,8 @@ }, "node_modules/abstract-leveldown/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -1778,6 +870,8 @@ }, "node_modules/abstract-leveldown/node_modules/level-supports": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", "license": "MIT", "dependencies": { "xtend": "^4.0.2" @@ -1788,6 +882,8 @@ }, "node_modules/accepts": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -1808,6 +904,8 @@ }, "node_modules/aligned-block-file": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/aligned-block-file/-/aligned-block-file-1.2.2.tgz", + "integrity": "sha512-2Sy0hWhifVb8ycNFJgicL8fDPL2Ct1r62XOVxXnykn36z22MPZwnQlCmB2viQlY/lwfuO67GaQjUZ0rJgdVP7Q==", "license": "MIT", "dependencies": { "hashlru": "^2.1.0", @@ -1820,6 +918,8 @@ }, "node_modules/aligned-block-file/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -1828,20 +928,10 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/ansi-escapes": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -1849,6 +939,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1860,34 +952,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "license": "ISC", - "dependencies": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" - } - }, - "node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/append-batch": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/append-batch/-/append-batch-0.0.2.tgz", + "integrity": "sha512-v1lD0bKqM4MTtGzapIx6qCGcwbVJyKaTmbzIzA6kpnVwTY1E+VNMeYoOvAywl+hXEEF4CcqkrH/4iFMbzM8XXw==", "license": "MIT" }, "node_modules/archiver": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "license": "MIT", "dependencies": { "archiver-utils": "^5.0.2", @@ -1904,6 +978,8 @@ }, "node_modules/archiver-utils": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "license": "MIT", "dependencies": { "glob": "^10.0.0", @@ -1918,38 +994,10 @@ "node": ">= 14" } }, - "node_modules/arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA==", - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -1962,48 +1010,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -2021,34 +1031,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/asap": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/async": { "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, "node_modules/async-append-only-log": { "version": "4.3.10", + "resolved": "https://registry.npmjs.org/async-append-only-log/-/async-append-only-log-4.3.10.tgz", + "integrity": "sha512-OYibcLkDBG9J8FFX4Nyf8RJOhO1EJTfCfb3rWou+qjcmuuJwBu7EuVVDpRtzJ+mPGVmwlXVPKtaw3X1lfHhxaQ==", "license": "LGPL-3.0", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -2065,71 +1063,40 @@ "push-stream-to-pull-stream": "^1.0.3" } }, - "node_modules/async-each": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", - "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/async-mutex": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", + "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", "license": "MIT", "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/async-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" + "tslib": "^2.3.1" } }, "node_modules/async-single": { "version": "1.0.5", - "license": "MIT" - }, - "node_modules/async-write": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/async-write/-/async-write-2.1.0.tgz", - "integrity": "sha512-eKEYj1+jDgQ15jazjmelcPGXO77pa5epzlyXyEQcwwLtnACtBTCrxaW+96uXF2j4rzIikmUtVlzXwejx6iIhWw==", + "resolved": "https://registry.npmjs.org/async-single/-/async-single-1.0.5.tgz", + "integrity": "sha512-I0SMZyvxwnr00advlzQbcfztaSEBv3Dw7Vg5i7pJVrXfjqHJSP1wljuOFKanBec5WKd+V4CIF4F9nhsukBikHA==", "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/atomic-file": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomic-file/-/atomic-file-2.1.1.tgz", + "integrity": "sha512-Eh6pW+fRC2/1RxPq3hO8+PkZKv+wujzKky2MP/n69eC8yMkbNFfuEb/riZHqf13M7gr6Hvglpk/kISgBSBb6bQ==", "license": "MIT", "dependencies": { "flumecodec": "0.0.1", @@ -2139,6 +1106,8 @@ }, "node_modules/atomic-file-rw": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/atomic-file-rw/-/atomic-file-rw-0.3.0.tgz", + "integrity": "sha512-XMFpe/ub9Mwdlyq3DUdOc/g2sdutDMdYw0XNFSvNGQpYdmxqVmLbeklu5KhemUYyrAj1kIrwoHOx6IxA3a7w3A==", "license": "MIT", "dependencies": { "idb-kv-store": "^4.5.0", @@ -2147,6 +1116,8 @@ }, "node_modules/atomic-file/node_modules/flumecodec": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", + "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -2154,19 +1125,14 @@ }, "node_modules/atomic-file/node_modules/level-codec": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, - "node_modules/attach-ware": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/attach-ware/-/attach-ware-1.1.1.tgz", - "integrity": "sha512-OpavlXWZkyE7m28fpCWF/RmxCukC1edukJp9IKjEpZs/O11H3896DkLpK7lMiL8ZDx2yxo9FrZQaeHkyJGcIuQ==", - "license": "MIT", - "dependencies": { - "unherit": "^1.0.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -2191,20 +1157,14 @@ }, "node_modules/b4a": { "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "license": "Apache-2.0" }, - "node_modules/bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/bare-events": { @@ -2221,129 +1181,10 @@ } } }, - "node_modules/bare-fs": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", - "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", - "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", - "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", - "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "license": "MIT", - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -2362,6 +1203,8 @@ }, "node_modules/base64-url": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-2.3.3.tgz", + "integrity": "sha512-dLMhIsK7OplcDauDH/tZLvK7JmUZK3A7KiQpjNzsBrM6Etw7hzNI1tLEywqJk9NnwkgWuFKSlx/IUO7vF6Mo8Q==", "license": "ISC", "engines": { "node": ">=6" @@ -2369,14 +1212,14 @@ }, "node_modules/bash-color": { "version": "0.0.4", + "resolved": "https://registry.npmjs.org/bash-color/-/bash-color-0.0.4.tgz", + "integrity": "sha512-ZNB4525U7BxT6v9C8LEtywyCgB4Pjnm7/bh+ru/Z9Ecxvg3fDjaJ6z305z9a61orQdbB1zqYHh5JbUqx4s4K0g==", "license": "MIT" }, - "node_modules/before-after-hook": { - "version": "4.0.0", - "license": "Apache-2.0" - }, "node_modules/bencode": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.3.tgz", + "integrity": "sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==", "license": "MIT" }, "node_modules/bidi-js": { @@ -2388,52 +1231,572 @@ "require-from-string": "^2.0.2" } }, - "node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "license": "MIT", + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", "engines": { - "node": ">=0.10.0" + "node": ">=0.6" } }, - "node_modules/binary-search": { - "version": "1.3.6", - "license": "CC0-1.0" - }, "node_modules/binary-search-bounds": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", "license": "MIT" }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bipf": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/bipf/-/bipf-1.9.0.tgz", + "integrity": "sha512-B/d8IADy5Y4v/CTMRWxLD8ONd2qRkF+2DbZLeIUql7PukfAiBhlGlw5qJcIU03l21qMEyvbi4PdntatH+j40vA==", "license": "MIT", "dependencies": { "fast-varint": "^1.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", + "node_modules/blake2b": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/blake2b/-/blake2b-2.1.4.tgz", + "integrity": "sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==", + "license": "ISC", "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "blake2b-wasm": "^2.4.0", + "nanoassert": "^2.0.0" } }, - "node_modules/bl/node_modules/buffer": { + "node_modules/blake2b-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz", + "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==", + "license": "MIT", + "dependencies": { + "b4a": "^1.0.1", + "nanoassert": "^2.0.0" + } + }, + "node_modules/blake2s": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/blake2s/-/blake2s-1.1.0.tgz", + "integrity": "sha512-lvCxvg+up7AmujO8vijTi4GsbIOuusWa+b/nN5+VAanFjzbauq0er5VzgjTC6pevhEO8SYTzY784zS2KQauO0A==", + "license": "Public Domain" + }, + "node_modules/blake3": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/blake3/-/blake3-2.1.7.tgz", + "integrity": "sha512-5d+TdKJvju96IyEaGJ0eO6CHbckWi+NBrCezGYM/WsnI3R03aLL2TWfsuZSh1rs0fTv/L3ps/r0vykjYurcIwA==", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-split": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.0.tgz", + "integrity": "sha512-CNXO3AXAS1H/kOGQkPjucm1161/XoF3aVkMfujqwk85XN/D/MkQMvoB81lXyX/2rerZS+hPAYYRR3mAW05awjQ==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-2.0.2.tgz", + "integrity": "sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/butt64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/butt64/-/butt64-2.0.1.tgz", + "integrity": "sha512-rTxg4F4IH+/u6BN2/SJjJnQsOQwXeV7Hsm9ataUsxagoqXZ2xbt+vDVDYTbS1GEDpUE/38LEuG9Mk9pgoLUzpw==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "license": "MIT", + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chacha20-universal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chacha20-universal/-/chacha20-universal-1.0.4.tgz", + "integrity": "sha512-/IOxdWWNa7nRabfe7+oF+jVkGjlr2xUL4J8l/OvzZhj+c9RpMqoo3Dq+5nU1j/BflRV4BKnaQ4+4oH1yBpQG1Q==", + "license": "ISC", + "dependencies": { + "nanoassert": "^2.0.0" + } + }, + "node_modules/charwise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/charwise/-/charwise-3.0.1.tgz", + "integrity": "sha512-RcdumNsM6fJZ5HHbYunqj2bpurVRGsXour3OR+SlLEHFhG6ALm54i6Osnh+OvO7kEoSBzwExpblYFH8zKQiEPw==", + "license": "MIT" + }, + "node_modules/chloride": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chloride/-/chloride-2.4.1.tgz", + "integrity": "sha512-ZiID87W2o2llvuF4C7Fvt9GJisazSdMsSkjAq4WaMed9zn77nlkcy08ZfrPtOGAXyaxTDj0VjnuyD97EdJLz3g==", + "license": "MIT", + "dependencies": { + "sodium-browserify": "^1.2.7", + "sodium-browserify-tweetnacl": "^0.2.5", + "sodium-chloride": "^1.1.2" + }, + "optionalDependencies": { + "sodium-native": "^3.0.0" + } + }, + "node_modules/chloride-test": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/chloride-test/-/chloride-test-1.2.4.tgz", + "integrity": "sha512-9vhoi1qXSBPn6//ZxIgSe3M2QhKHzIPZQzmrZgmPADsqW0Jxpe3db1e7aGSRUMXbxAQ04SfypdT8dGaSvIvKDw==", + "license": "MIT", + "dependencies": { + "json-buffer": "^2.0.11" + } + }, + "node_modules/chloride-test/node_modules/json-buffer": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-2.0.11.tgz", + "integrity": "sha512-Wu4/hxSZX7Krzjor+sZHWaRau6Be4WQHlrkl3v8cmxRBBewF2TotlgHUedKQJyFiUyFxnK/ZlRYnR9UNVZ7pkg==", + "license": "MIT" + }, + "node_modules/clarify-error": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clarify-error/-/clarify-error-1.0.0.tgz", + "integrity": "sha512-f96oT3/Cdwz1eB+7RaH/XRR42lwGqVPnDl9NAm9ugT+BwAFoUS/pVnkgXUo/5UaUmwMMs6/GNFP8A8gCgmgvog==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/class-list": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/class-list/-/class-list-0.1.1.tgz", + "integrity": "sha512-zqR0uW+VsLtyQhixBhkdQ+z6B8+Y8HTh28kdSVjJ4zTTKM7Xz2asAQSya9VI6m/34F6N6Ktm0mrchKB+E5a8Xw==", + "dependencies": { + "indexof": "0.0.1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/co-body": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", + "license": "MIT", + "dependencies": { + "@hapi/bourne": "^3.0.0", + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/cont": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cont/-/cont-1.0.3.tgz", + "integrity": "sha512-lZzUsnNnzBW4EN+Mll8J54cdHn6JzKyHpa9ftxxsPwkWfhdYRIHph9dFdmi0ibfqhnvR103tQ61eAF57FF87IA==", + "license": "MIT", + "dependencies": { + "continuable": "~1.2.0", + "continuable-para": "~1.2.0", + "continuable-series": "~1.2.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/continuable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/continuable/-/continuable-1.2.0.tgz", + "integrity": "sha512-DMksZyrS34yEX+DTGyt9h8/6ONHRBH+PpQ5ev3YdCy5J41r0nMNv+psAujoeoT1mJf+fzKhzacR9o1M1ys2tBQ==" + }, + "node_modules/continuable-hash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/continuable-hash/-/continuable-hash-0.1.4.tgz", + "integrity": "sha512-Gu5kdDzwmCymZ2tROViH0VYSd0iXDDaOjn8iG51FyZreS6OuqEJAE+a3sJ6/Ky6roKOHYHDYD8Ttxj+a1tyYrg==", + "dependencies": { + "continuable": "~1.1.6" + } + }, + "node_modules/continuable-hash/node_modules/continuable": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/continuable/-/continuable-1.1.8.tgz", + "integrity": "sha512-DrVON8s+uwIH+OJa/HdbLE7ViLKpDeF0+flOhLf7hKtffIvyI2l38i7jq6G5i2GFqodpkz7RJPq60VkmwxfVnA==" + }, + "node_modules/continuable-list": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/continuable-list/-/continuable-list-0.1.6.tgz", + "integrity": "sha512-dqfYFtC/YRR4fUuZyJ8A+VKX/oV8E+atSCCcOvkuHsB6MFkiyI9LrDlUXCnHYowuuDu5vYqcAiRU2hiGXtcb7w==", + "dependencies": { + "continuable": "~1.1.6" + } + }, + "node_modules/continuable-list/node_modules/continuable": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/continuable/-/continuable-1.1.8.tgz", + "integrity": "sha512-DrVON8s+uwIH+OJa/HdbLE7ViLKpDeF0+flOhLf7hKtffIvyI2l38i7jq6G5i2GFqodpkz7RJPq60VkmwxfVnA==" + }, + "node_modules/continuable-para": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/continuable-para/-/continuable-para-1.2.0.tgz", + "integrity": "sha512-XjPo974RGEJqnvFVT9PWxG97d/NCg58mwmWDbKXqTZBZuUnMIjkx0vde+xerkUuymk8DR+YUAcllK8J5/O4Leg==", + "license": "MIT", + "dependencies": { + "continuable-hash": "~0.1.4", + "continuable-list": "~0.1.5" + } + }, + "node_modules/continuable-series": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/continuable-series/-/continuable-series-1.2.0.tgz", + "integrity": "sha512-xMAlREs3m/IM0VCzuh0TRBXpjd6tl6zi3cMgiwcywIPFcCmNlcFNA6XCtA2cEx6Kq7EE6c1JVDVl3WGepp6a5g==", + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cpu-percentage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cpu-percentage/-/cpu-percentage-1.0.3.tgz", + "integrity": "sha512-LP7N2+hS7optwpkMisZoDfswnLeCzyETNOogkjw7v2YteQSPDd0vEv3nSbU8FVu58fXCkVAWtXaeSXEedT1a5g==", + "license": "MIT", + "engines": { + "node": ">=6.1" + } + }, + "node_modules/crc": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.6.0.tgz", + "integrity": "sha512-K9CVP4+ugmpRvZidsumVVL1swX7t0cqYrLhT+5rAp7/S3TjiEtpovD8TISzy+3moanm6v6/cbXOzE6JbzB+siw==", + "license": "MIT", + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc/node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", @@ -2457,1108 +1820,10 @@ "ieee754": "^1.1.13" } }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/blake2b": { - "version": "2.1.4", - "license": "ISC", - "dependencies": { - "blake2b-wasm": "^2.4.0", - "nanoassert": "^2.0.0" - } - }, - "node_modules/blake2b-wasm": { - "version": "2.4.0", - "license": "MIT", - "dependencies": { - "b4a": "^1.0.1", - "nanoassert": "^2.0.0" - } - }, - "node_modules/blake2s": { - "version": "1.1.0", - "license": "Public Domain" - }, - "node_modules/blake3": { - "version": "2.1.7", - "hasInstallScript": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.2", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.2", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/media-typer": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/mime-types": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/type-is": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/bottleneck": { - "version": "2.19.5", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", - "license": "MIT", - "dependencies": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/broadcast-stream": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/broadcast-stream/-/broadcast-stream-0.2.2.tgz", - "integrity": "sha512-d6iHc4P5YC2RvXv6qZxH3aoue88BiWhzCPki+i1Dj2igeZyNtUoMInloINRfnyrB7Fm+2f1q1bmQYhXQeKWwkQ==", - "license": "MIT", - "bin": { - "broadcast-stream": "bin.js" - } - }, - "node_modules/browser-split": { - "version": "0.0.0", - "license": "MIT" - }, - "node_modules/buffer": { - "version": "6.0.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/buffer-xor": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.1" - } - }, - "node_modules/butt64": { - "version": "2.0.1", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "license": "MIT", - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cache-base/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cache-content-type": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cat-names": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cat-names/-/cat-names-2.0.0.tgz", - "integrity": "sha512-yjcTHwA7tcro823P5yIfmTvXT8p5YxJd4eASWHAHvqaekuJoGu1qjWoldgfV2mH4tgLp+FNAf13uiDEo/O7LGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "meow": "^5.0.0", - "unique-random-array": "^1.0.0" - }, - "bin": { - "cat-names": "cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ccount": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", - "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chacha20-universal": { - "version": "1.0.4", - "license": "ISC", - "dependencies": { - "nanoassert": "^2.0.0" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/charwise": { - "version": "3.0.1", - "license": "MIT" - }, - "node_modules/chloride": { - "version": "2.4.1", - "license": "MIT", - "dependencies": { - "sodium-browserify": "^1.2.7", - "sodium-browserify-tweetnacl": "^0.2.5", - "sodium-chloride": "^1.1.2" - }, - "optionalDependencies": { - "sodium-native": "^3.0.0" - } - }, - "node_modules/chloride-test": { - "version": "1.2.4", - "license": "MIT", - "dependencies": { - "json-buffer": "^2.0.11" - } - }, - "node_modules/chloride-test/node_modules/json-buffer": { - "version": "2.0.11", - "license": "MIT" - }, - "node_modules/chmodrp": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg==", - "license": "MIT", - "dependencies": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - }, - "optionalDependencies": { - "fsevents": "^1.0.0" - } - }, - "node_modules/chokidar/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/clarify-error": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/class-list": { - "version": "0.1.1", - "dependencies": { - "indexof": "0.0.1" - } - }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/class-utils/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cmake-js": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz", - "integrity": "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "fs-extra": "^11.3.3", - "node-api-headers": "^1.8.0", - "rc": "1.2.8", - "semver": "^7.7.3", - "tar": "^7.5.6", - "url-join": "^4.0.1", - "which": "^6.0.0", - "yargs": "^17.7.2" - }, - "bin": { - "cmake-js": "bin/cmake-js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/cmake-js/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/cmake-js/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/co": { - "version": "4.6.0", - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/co-body": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "@hapi/bourne": "^3.0.0", - "inflation": "^2.0.0", - "qs": "^6.5.2", - "raw-body": "^2.3.3", - "type-is": "^1.6.16" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "license": "MIT", - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/compare-at-paths": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "libnested": "^1.3.2", - "tape": "^4.9.1", - "typewiselite": "^1.0.0" - } - }, - "node_modules/compatibility": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/compatibility/-/compatibility-1.0.1.tgz", - "integrity": "sha512-0OzQu7V1TlJvKiEGC4b4TEXypGBHzcRX8ZSJ9svyMIKnE73AFrBAmVyPIabrFKhKc0L0iDLw6voyB3vA3hEXHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "npm-install-package": "^2.1.0", - "semver": "^6.1.0" - }, - "bin": { - "compatibility": "bin.js" - } - }, - "node_modules/compatibility/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/compress-commons": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/cont": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "continuable": "~1.2.0", - "continuable-para": "~1.2.0", - "continuable-series": "~1.2.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/continuable": { - "version": "1.2.0" - }, - "node_modules/continuable-hash": { - "version": "0.1.4", - "dependencies": { - "continuable": "~1.1.6" - } - }, - "node_modules/continuable-hash/node_modules/continuable": { - "version": "1.1.8" - }, - "node_modules/continuable-list": { - "version": "0.1.6", - "dependencies": { - "continuable": "~1.1.6" - } - }, - "node_modules/continuable-list/node_modules/continuable": { - "version": "1.1.8" - }, - "node_modules/continuable-para": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "continuable-hash": "~0.1.4", - "continuable-list": "~0.1.5" - } - }, - "node_modules/continuable-series": { - "version": "1.2.0", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cookies": { - "version": "0.9.1", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cpu-percentage": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">=6.1" - } - }, - "node_modules/crc": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/crc-32": { - "version": "1.2.2", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc/node_modules/buffer": { - "version": "5.7.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/crc32-stream": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "license": "MIT", "dependencies": { "crc-32": "^1.2.0", @@ -3570,6 +1835,8 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3617,17 +1884,17 @@ "node": "20 || >=22" } }, - "node_modules/currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", - "dev": true, - "license": "MIT", + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", "dependencies": { - "array-find-index": "^1.0.1" + "es5-ext": "^0.10.64", + "type": "^2.7.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.12" } }, "node_modules/data-urls": { @@ -3680,6 +1947,8 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -3695,6 +1964,8 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -3710,6 +1981,8 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -3725,6 +1998,8 @@ }, "node_modules/debug": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3740,33 +2015,8 @@ }, "node_modules/decamelize": { "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3778,36 +2028,16 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-equal": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", "license": "MIT" }, "node_modules/deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -3815,6 +2045,8 @@ }, "node_modules/deferred-leveldown": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", "license": "MIT", "dependencies": { "abstract-leveldown": "~6.2.1", @@ -3826,6 +2058,8 @@ }, "node_modules/define-data-property": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -3841,6 +2075,8 @@ }, "node_modules/define-lazy-prop": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "license": "MIT", "engines": { "node": ">=8" @@ -3848,6 +2084,8 @@ }, "node_modules/define-properties": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -3861,37 +2099,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-property/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/defined": { - "version": "1.0.1", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3899,10 +2110,14 @@ }, "node_modules/delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, "node_modules/depd": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -3910,23 +2125,18 @@ }, "node_modules/destroy": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detab": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/detab/-/detab-1.0.2.tgz", - "integrity": "sha512-J7vnPWu/Rq5Hi8xPPnwsv55CLkJmzZZCJOs0sAc6QcC7uf1X2iOrAXyGB6bXJBl5s4anllkMBKkHz13/BenuUQ==", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.5.2" - } - }, "node_modules/detect-libc": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -3934,6 +2144,8 @@ }, "node_modules/dezalgo": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "license": "ISC", "dependencies": { "asap": "^2.0.0", @@ -3942,6 +2154,8 @@ }, "node_modules/dgram-broadcast": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dgram-broadcast/-/dgram-broadcast-1.0.0.tgz", + "integrity": "sha512-7IPzhuQZVnU2iGo+Eq+dRNpYvlCkT7/5yD6BHx5P6MtT/cDxajR7Qh4+ve2WamsXaXVRLvj16ah8YIF7qcXOLw==", "license": "MIT", "bin": { "dgram-broadcast": "bin.js" @@ -3952,260 +2166,64 @@ }, "node_modules/dijkstrajs": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, "node_modules/discontinuous-range": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", "license": "MIT" }, - "node_modules/dog-names": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/dog-names/-/dog-names-1.0.2.tgz", - "integrity": "sha512-3+2tu1P9L+J3od+yTWQpfXkcUlukfylLYpsSgAMFaUvxk8Ah7kStNdUDcg9D4hpH1D8b+uMGHv5LKlB4uaH4ng==", - "dev": true, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "license": "MIT", "dependencies": { - "meow": "^3.0.0", - "unique-random-array": "^1.0.0" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" }, - "bin": { - "dog-names": "cli.js" - }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dog-names/node_modules/camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "repeating": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/parse-json": { + "node_modules/dom-serializer/node_modules/entities": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/dog-names/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/pify": { + "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" }, - "node_modules/dog-names/node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", - "dev": true, - "license": "MIT", + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "domelementtype": "^2.2.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" + "node": ">= 4" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-utf8": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-stdin": "^4.0.1" - }, - "bin": { - "strip-indent": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/dompurify": { @@ -4217,18 +2235,24 @@ "@types/trusted-types": "^2.0.7" } }, - "node_modules/dotignore": { - "version": "0.1.2", - "license": "MIT", + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "dependencies": { - "minimatch": "^3.0.4" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" }, - "bin": { - "ignored": "bin/ignored" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4241,6 +2265,8 @@ }, "node_modules/dynamic-dijkstra": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dynamic-dijkstra/-/dynamic-dijkstra-1.0.2.tgz", + "integrity": "sha512-1N+eCCrepIeK1+qtWrMEO1CV68Hn+TLbiR9c70VB3xnut3DmUxT+3T7sRHhb0mpK2F/74IfP+loQDriU2W9lkA==", "license": "MIT", "dependencies": { "heap": "^0.2.6", @@ -4249,10 +2275,14 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, "node_modules/ed2curve": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.1.4.tgz", + "integrity": "sha512-hDZWhCHZ1wu4P2g2RVsM2MjDmmJzhvcsXr5qHUSBJZXvuhJSunhbVsWoBXdIe0/yTa3RV4UaWpOmFmrVsKr0wA==", "license": "Public domain", "dependencies": { "tweetnacl": "0.x.x" @@ -4260,50 +2290,29 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "license": "MIT" - }, - "node_modules/elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/emoji-named-characters": { - "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, - "node_modules/emoji-server": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "emoji-named-characters": "~1.0.2" - } - }, "node_modules/encodeurl": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/encoding-down": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", "license": "MIT", "dependencies": { "abstract-leveldown": "^6.2.1", @@ -4315,15 +2324,6 @@ "node": ">=6" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -4338,20 +2338,17 @@ }, "node_modules/env-paths": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/env-var": { - "version": "7.5.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/envelope-js": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/envelope-js/-/envelope-js-1.3.2.tgz", + "integrity": "sha512-U3MQnyczN9QvwgXWlxDzAiEbNjwGD1aG+l1fNdwSZijOYboiu1aLHT8vIPSITShLiD4+n9p+EEwR79rp0vsKdw==", "license": "MIT", "dependencies": { "buffer-xor": "^2.0.2", @@ -4363,6 +2360,8 @@ }, "node_modules/envelope-spec": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/envelope-spec/-/envelope-spec-1.1.1.tgz", + "integrity": "sha512-7eIn5kw7eIt2H2YEPE5oWT8ATx9AYkVFKQi4GG+3qRxgcBKmPWt71OOI3DE4Do/hryZohh9eamoxGhSgZGQpIw==", "license": "LGPL-3.0-only", "dependencies": { "ssb-bfe": "^1.0.1" @@ -4370,6 +2369,8 @@ }, "node_modules/envelope-spec/node_modules/ssb-bfe": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-bfe/-/ssb-bfe-1.1.0.tgz", + "integrity": "sha512-viY/ZzQENnO+gvahYQ35tf3OPV6ChQ+CxnyPpiloDZu7bzQNL0E24wjmoGefF+ftaCK/meeWKP3Hxm7pG50cFA==", "license": "LGPL-3.0", "dependencies": { "is-canonical-base64": "^1.1.1", @@ -4378,6 +2379,8 @@ }, "node_modules/epidemic-broadcast-trees": { "version": "9.0.4", + "resolved": "https://registry.npmjs.org/epidemic-broadcast-trees/-/epidemic-broadcast-trees-9.0.4.tgz", + "integrity": "sha512-f1Y2ayalcLQNn2I6umep29UDkTo+8nF4D//KulmwjDGP4ZPOO+ch8/LuxpXaMoeV6RO5arvvaEL53yzkKGSHTg==", "license": "MIT", "dependencies": { "push-stream": "^11.0.0" @@ -4385,6 +2388,8 @@ }, "node_modules/errno": { "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "license": "MIT", "dependencies": { "prr": "~1.0.1" @@ -4393,18 +2398,10 @@ "errno": "cli.js" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-abstract": { "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -4471,6 +2468,8 @@ }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4478,6 +2477,8 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4485,6 +2486,8 @@ }, "node_modules/es-get-iterator": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -4503,6 +2506,8 @@ }, "node_modules/es-map": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/es-map/-/es-map-1.0.6.tgz", + "integrity": "sha512-JWn54TeYJe93WHhduTh9MUqEkjhbCjU8vqq1q9FyCrOle/utr1i8PmzK2vXnP6BeLsBLvb1zV4fSkpVnT4LW1A==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -4526,6 +2531,8 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4536,6 +2543,8 @@ }, "node_modules/es-set": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-set/-/es-set-1.1.2.tgz", + "integrity": "sha512-tGLC1yArXhXwZTm3D6xbu1JL4S3cUtRRggbFWHhqTxpPc975fmcLL1VB0QOnAY8q5TBdul7YTjfEOT2VpUGcCg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -4564,6 +2573,8 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4577,6 +2588,8 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7", @@ -4590,8 +2603,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -4599,39 +2654,48 @@ }, "node_modules/escape-html": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, "engines": { - "node": ">=0.8.0" + "node": ">=0.10" } }, - "node_modules/etag": { - "version": "1.8.1", + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" } }, "node_modules/event-target-shim": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, "node_modules/events": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", "engines": { "node": ">=0.8.x" @@ -4646,272 +2710,55 @@ "bare-events": "^2.7.0" } }, - "node_modules/exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA==", - "license": "MIT", - "dependencies": { - "is-posix-bracket": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/explain-error": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/explain-error/-/explain-error-1.0.4.tgz", + "integrity": "sha512-/wSgNMxFusiYRy1rd19LT2SQlIXDppHpumpWo06wxjflD1OYxDLbl6rMVw+U3bxD5Nuhex4TKqv9Aem4D0lVzQ==", "license": "MIT" }, - "node_modules/express": { - "version": "5.1.0", - "license": "MIT", + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "type": "^2.7.2" } }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/fresh": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/media-typer": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend-shallow/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend.js": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/extend.js/-/extend.js-0.0.2.tgz", - "integrity": "sha512-kSK5oO9X2i9qUptwhkilKqBfLG322xXY2ZO6/dlPY/ozt0fc+Ac9Qo6hZE/RiRTau5XUvVv2y6z1G6lNZ8f1WA==" - }, - "node_modules/extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-fifo": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, "node_modules/fast-varint": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-varint/-/fast-varint-1.0.1.tgz", + "integrity": "sha512-Vi9Db6dVe0GbWHtZxYzlpLbIq4o1FnVIaVud3RMYF/E7d4l0r37aepMY16qZBNfniw/o02BP9LAqCYCKjFTy+Q==", "license": "MIT" }, - "node_modules/fast-xml-parser": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", - "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastintcompression": { "version": "0.0.4", + "resolved": "https://registry.npmjs.org/fastintcompression/-/fastintcompression-0.0.4.tgz", + "integrity": "sha512-sRtejNHcWjJl0Fr+gsbybbgOKKj0sQYvv7rA787EbeFu1fMpmzjHuBW8fLBImSZeqDuR++TZU2n60mRrpAUR9g==", "license": "Apache-2.0" }, "node_modules/fastpriorityqueue": { "version": "0.7.5", + "resolved": "https://registry.npmjs.org/fastpriorityqueue/-/fastpriorityqueue-0.7.5.tgz", + "integrity": "sha512-3Pa0n9gwy8yIbEsT3m2j/E9DXgWvvjfiZjjqcJ+AdNKTAlVMIuFYrYG5Y3RHEM8O6cwv9hOpOWY/NaMfywoQVA==", "license": "Apache-2.0" }, "node_modules/file-type": { "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", "license": "MIT", "dependencies": { "readable-web-to-node-stream": "^3.0.0", @@ -4925,108 +2772,10 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT", - "optional": true - }, - "node_modules/filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/filename-reserved-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", - "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/filenamify": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", - "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", - "license": "MIT", - "dependencies": { - "filename-reserved-regex": "^3.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "license": "MIT", - "dependencies": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/flatbuffers": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", - "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==", - "license": "SEE LICENSE IN LICENSE.txt" - }, "node_modules/flumecodec": { "version": "0.0.0", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.0.tgz", + "integrity": "sha512-1UhNtCE8Al1mMrEqvRGxKU2GoZHXPKLWmAm6Oe2J9x+oQeMinCW9jpegJaEuGgUtro94MogbObFjEBFJHc36fA==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -5034,27 +2783,14 @@ }, "node_modules/flumecodec/node_modules/level-codec": { "version": "6.2.0", - "license": "MIT" - }, - "node_modules/flumedb": { - "version": "2.1.8", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "explain-error": "^1.0.3", - "obz": "1.0.2", - "pull-abortable": "^4.1.1", - "pull-cont": "^0.1.1", - "pull-looper": "^1.0.0", - "pull-stream": "^3.6.14" - } - }, - "node_modules/flumedb/node_modules/obz": { - "version": "1.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, "node_modules/flumelog-offset": { "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flumelog-offset/-/flumelog-offset-3.4.4.tgz", + "integrity": "sha512-sakCD+dVx541h3VeVq3Ti2lWPRrJf8PBRmnbm9EMBVLJnZkS3UD2lAlClZROxgKbh/JkMPyffvhDGv4VHNCVbA==", "license": "MIT", "dependencies": { "aligned-block-file": "^1.2.0", @@ -5069,96 +2805,10 @@ "uint48be": "^2.0.1" } }, - "node_modules/flumeview-hashtable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flumeview-hashtable/-/flumeview-hashtable-1.1.1.tgz", - "integrity": "sha512-73LAN0qF4zVMHMExYhK0z1swDo6FEh/bt1HF5/UnWkgI5JsECXOK2bTokWVU1levtr9bd+IxYChnJKJNnEzD0A==", - "license": "MIT", - "dependencies": { - "async-single": "^1.0.5", - "atomic-file": "^1.1.3", - "obv": "0.0.1", - "pull-stream": "^3.6.0" - } - }, - "node_modules/flumeview-hashtable/node_modules/atomic-file": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/atomic-file/-/atomic-file-1.1.5.tgz", - "integrity": "sha512-TG+5YFiaKQ6CZiSQsosGMJ/IJzwMZ4V/rSdEXlD6+DwKyv8OyeUcprq34kp4yuS6bfQYXhxBC2Vm8PWo+iKBGQ==", - "license": "MIT" - }, - "node_modules/flumeview-level": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "charwise": "^3.0.1", - "explain-error": "^1.0.4", - "level": "^6.0.1", - "ltgt": "^2.1.3", - "mkdirp": "^1.0.4", - "obz": "^1.0.2", - "pull-level": "^2.0.3", - "pull-paramap": "^1.2.1", - "pull-stream": "^3.6.14", - "pull-write": "^1.1.1" - } - }, - "node_modules/flumeview-links": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "deep-equal": "^2.0.3", - "flumeview-level": "^4.0.3", - "map-filter-reduce": "^3.2.2", - "pull-flatmap": "0.0.1", - "pull-stream": "^3.6.14" - } - }, - "node_modules/flumeview-links/node_modules/deep-equal": { - "version": "2.2.3", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/flumeview-query": { - "version": "8.0.0", - "license": "MIT", - "dependencies": { - "deep-equal": "^1.0.1", - "flumeview-level": "^4.0.3", - "map-filter-reduce": "^3.2.0", - "pull-flatmap": "0.0.1", - "pull-paramap": "^1.1.3", - "pull-sink-through": "0.0.0", - "pull-stream": "^3.4.0" - } - }, "node_modules/flumeview-reduce": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/flumeview-reduce/-/flumeview-reduce-1.4.0.tgz", + "integrity": "sha512-ShCMtY5YZl3icnUAWTfwzRXSGN1AUo4Isj1wTRLpMMNgM9p1T0gc6N+r1ajampf1GO19SkqrgZHdPPOaEitvhg==", "license": "MIT", "dependencies": { "async-single": "^1.0.5", @@ -5170,16 +2820,6 @@ "pull-stream": "^3.5.0" } }, - "node_modules/flumeview-search": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "flumeview-level": "^4.0.3", - "pull-paramap": "^1.2.2", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.0.1" - } - }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -5202,6 +2842,8 @@ }, "node_modules/for-each": { "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -5213,29 +2855,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", - "license": "MIT", - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/foreground-child": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -5250,6 +2873,8 @@ }, "node_modules/form-data": { "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5264,6 +2889,8 @@ }, "node_modules/formidable": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", "license": "MIT", "dependencies": { "@paralleldrive/cuid2": "^2.2.2", @@ -5275,72 +2902,25 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "license": "MIT", - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5348,6 +2928,8 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -5366,6 +2948,8 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5373,6 +2957,8 @@ }, "node_modules/futoin-hkdf": { "version": "1.5.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", + "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5380,6 +2966,8 @@ }, "node_modules/generate-function": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", "license": "MIT", "dependencies": { "is-property": "^1.0.2" @@ -5387,6 +2975,8 @@ }, "node_modules/generate-object-property": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", "license": "MIT", "dependencies": { "is-property": "^1.0.0" @@ -5394,25 +2984,17 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5435,6 +3017,8 @@ }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5444,18 +3028,10 @@ "node": ">= 0.4" } }, - "node_modules/get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -5469,23 +3045,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -5502,28 +3065,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==", - "license": "MIT", - "dependencies": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", - "license": "ISC", - "dependencies": { - "is-glob": "^2.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -5539,18 +3080,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/globalthis": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -5563,51 +3096,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-4.1.0.tgz", - "integrity": "sha512-JPDtMSr0bt25W64q792rvlrSwIaZwqUAhqdYKSr57Wh/xBcQ5JDWLM85ndn+Q1WdBQXLb9YGCl0QN/T0HpqU0A==", - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^6.0.1", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/globby/node_modules/glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5616,56 +3108,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gossip-query": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/gossip-query/-/gossip-query-2.0.2.tgz", - "integrity": "sha512-17MflOq79BYcK01RrqHhtG5Dl88ztSu7gfD+4i1GrpzkbvLDKkJ7LoKiFKzBWzzd+Jd5VhIiLtQRvjRiKa6PUg==", - "license": "MIT", - "dependencies": { - "obv": "0.0.1", - "pull-stream": "^3.6.0" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/guid-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", - "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", - "license": "ISC" - }, - "node_modules/has": { - "version": "1.0.4", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-bigints": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5674,26 +3126,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-network": { - "version": "0.0.1", - "license": "MIT" - }, "node_modules/has-network2": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/has-network2/-/has-network2-0.0.3.tgz", + "integrity": "sha512-EvEZguA+LkyiS8G/Qks5I6imKnM2Z3NPN3eoQhviUQ7O6/d8nyZ7sDozBk6kTIA+Qj/S/V8ubRA1rqJcxc3qBQ==", "license": "MIT" }, "node_modules/has-property-descriptors": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -5704,6 +3146,8 @@ }, "node_modules/has-proto": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" @@ -5717,6 +3161,8 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5727,6 +3173,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5738,90 +3186,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "license": "MIT", - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-value/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" - }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hashlru": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hashlru/-/hashlru-2.3.0.tgz", + "integrity": "sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==", "license": "MIT" }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5830,31 +3204,16 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/he/-/he-0.5.0.tgz", - "integrity": "sha512-DoufbNNOFzwRPy8uecq+j+VCPQ+JyDelHTmSgygrA5TsR8Cbw4Qcir5sGtWiusB4BdT89nmlaVDhSJOqC/33vw==", - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/heap": { "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", "license": "MIT" }, - "node_modules/hexpp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexpp/-/hexpp-2.0.0.tgz", - "integrity": "sha512-5qB1Uar+ka6iQm++gOs3rZDH7q3d7gIxgwYRz00KF5OI4saTKEnGMsv5ebqW9XRkBXnkMUghd9b9b5gJpVROtA==", - "dev": true, - "license": "MIT", - "bin": { - "hexpp": "bin.js" - } - }, "node_modules/highlight.js": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.0.0.tgz", + "integrity": "sha512-ByaTMfsSuoqerTwemOgpIhfULEIaK52JJYhky/sK7/Yqc0+t7Uh5DHay9vIC94YXSupnQ1Vqfc9VXrYP4eXW3Q==", "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" @@ -5862,17 +3221,14 @@ }, "node_modules/hoox": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/hoox/-/hoox-0.0.1.tgz", + "integrity": "sha512-4tKFjXcp8AWuw5lLTL7Xnixj1w88r+y1j9HKE8GoSeqDfsv6fLNMLjnrkB/H9tH+LqLp4+7eLss5IFS3Qra4lw==", "license": "MIT" }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true, - "license": "ISC" - }, "node_modules/html-element": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/html-element/-/html-element-2.3.1.tgz", + "integrity": "sha512-xnFt2ZkbFcjc+JoAtg3Hl89VeEZDjododu4VCPkRvFmBTHHA9U1Nt6hLUWfW2O+6Sl/rT1hHK/PivleX3PdBJQ==", "license": "MIT", "dependencies": { "class-list": "~0.1.1" @@ -5895,6 +3251,8 @@ }, "node_modules/html-tags": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "license": "MIT", "engines": { "node": ">=8" @@ -5903,8 +3261,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-assert": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", "license": "MIT", "dependencies": { "deep-equal": "~1.0.1", @@ -5916,6 +3304,8 @@ }, "node_modules/http-assert/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -5923,6 +3313,8 @@ }, "node_modules/http-assert/node_modules/http-errors": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -5937,6 +3329,8 @@ }, "node_modules/http-assert/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -5944,6 +3338,8 @@ }, "node_modules/http-errors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -5984,6 +3380,8 @@ }, "node_modules/hyperaxe": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hyperaxe/-/hyperaxe-2.0.1.tgz", + "integrity": "sha512-vp/x8kZnGoCQ5fwHtqQEFsv7sGoWFENNqSCKk46XjottHTL0feSW2LHSr6uK/JkeA8QydjoYPRjAl15mVHSM3g==", "license": "ISC", "dependencies": { "html-tags": "^3.0.0", @@ -5995,6 +3393,8 @@ }, "node_modules/hyperscript": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hyperscript/-/hyperscript-2.0.2.tgz", + "integrity": "sha512-uggBAYfHFC5WyZQXlJ61BNZbPmJbschcvfYNhYdZWCp+0J8KYb5Du8nQuk8Ru+ThoCNb01B0tPtnTRqnrFBkVg==", "license": "MIT", "dependencies": { "browser-split": "0.0.0", @@ -6002,20 +3402,10 @@ "html-element": "^2.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/idb-kv-store": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/idb-kv-store/-/idb-kv-store-4.5.0.tgz", + "integrity": "sha512-snvtAQRforYUI+C2+45L2LBJy/0/uQUffxv8/uwiS98fSUoXHVrFPClgzWZWxT0drwkLHJRm9inZcYzTR42GLA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -6024,6 +3414,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -6040,43 +3432,36 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, "node_modules/immediate": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", "license": "MIT" }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/increment-buffer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/increment-buffer/-/increment-buffer-1.0.1.tgz", + "integrity": "sha512-djREfebnCm+rsaMKhxQ3iQI0uUicnKoWUt3eHBLAMqqh9PRJYb+gc2Sty7HYpXrE0JdgdND/wNZIqOyUcMdejw==", "license": "MIT" }, - "node_modules/indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/indexof": { - "version": "0.0.1" - }, - "node_modules/infer-partial-order": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/infer-partial-order/-/infer-partial-order-0.1.1.tgz", - "integrity": "sha512-ahwueME5/IpvDbTWjG+ZawATtmvp7p04/ctMBPhwpvq8myV2FXXbJTJhvB8lgU2+1E2Nzipk+jbOmqZ+VZ3Dpw==", - "dev": true, - "license": "MIT" + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==" }, "node_modules/inflation": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -6084,6 +3469,8 @@ }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -6092,30 +3479,26 @@ }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/int53": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/int53/-/int53-1.0.0.tgz", + "integrity": "sha512-u8BMiMa05OPBgd32CKTead0CVTsFVgwFk23nNXo1teKPF6Sxcu0lXxEzP//zTcaKzXbGgPDXGmj/woyv+I4C5w==", "license": "BSD-3-Clause" }, - "node_modules/interleavings": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/interleavings/-/interleavings-1.0.0.tgz", - "integrity": "sha512-DAzMdwFFmBZcVvi3cXiEXDY3SMZvSedK6qFICOk0A6ietIUMKyZSgXlyvssakingGkSOLN6SaGnQzqrI6KWIYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "infer-partial-order": "~0.1.1", - "rng": "~0.2.2" - } - }, "node_modules/internal-slot": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6135,6 +3518,8 @@ }, "node_modules/ip-address": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "license": "MIT", "dependencies": { "jsbn": "1.1.0", @@ -6146,205 +3531,14 @@ }, "node_modules/ip-address/node_modules/sprintf-js": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ipull": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/ipull/-/ipull-3.9.5.tgz", - "integrity": "sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA==", - "license": "MIT", - "dependencies": { - "@tinyhttp/content-disposition": "^2.2.0", - "async-retry": "^1.3.3", - "chalk": "^5.3.0", - "ci-info": "^4.0.0", - "cli-spinners": "^2.9.2", - "commander": "^10.0.0", - "eventemitter3": "^5.0.1", - "filenamify": "^6.0.0", - "fs-extra": "^11.1.1", - "is-unicode-supported": "^2.0.0", - "lifecycle-utils": "^2.0.1", - "lodash.debounce": "^4.0.8", - "lowdb": "^7.0.1", - "pretty-bytes": "^6.1.0", - "pretty-ms": "^8.0.0", - "sleep-promise": "^9.1.0", - "slice-ansi": "^7.1.0", - "stdout-update": "^4.0.1", - "strip-ansi": "^7.1.0" - }, - "bin": { - "ipull": "dist/cli/cli.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/ido-pluto/ipull?sponsor=1" - }, - "optionalDependencies": { - "@reflink/reflink": "^0.1.16" - } - }, - "node_modules/ipull/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ipull/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ipull/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/lifecycle-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz", - "integrity": "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==", - "license": "MIT" - }, - "node_modules/ipull/node_modules/parse-ms": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", - "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/pretty-ms": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", - "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==", - "license": "MIT", - "dependencies": { - "parse-ms": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/ipull/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/irregular-plurals": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz", - "integrity": "sha512-kniTIJmaZYiwa17eTtWIfm0K342seyugl6vuC8DiiyiRAJWAVlLkqGCI0Im0neo0TkXw+pRcKaBPRdcKHnQJ6Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-arguments": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6359,6 +3553,8 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -6372,15 +3568,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "license": "MIT", "dependencies": { "async-function": "^1.0.0", @@ -6398,6 +3589,8 @@ }, "node_modules/is-bigint": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" @@ -6409,20 +3602,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6435,33 +3618,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/is-buffer-zero": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-buffer-zero/-/is-buffer-zero-1.0.0.tgz", + "integrity": "sha512-eqgpqrTMGaAd5dQxg0dcZ79C8wTlDVYrM+zvB8kUXXSBzOqG5JeKWne1Zv9LDV3ePovx06fHLbp372OFbp/cIA==", "license": "MIT" }, "node_modules/is-callable": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6472,10 +3638,14 @@ }, "node_modules/is-canonical-base64": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-canonical-base64/-/is-canonical-base64-1.1.1.tgz", + "integrity": "sha512-o6t/DwgEapC0bsloqtegAQyZzQXaQ5+8fzsyf2KmLqupC2ifLFq/lMQiFCJeGpdSrK1o6GL+WW2lRU050lLlFg==", "license": "MIT" }, "node_modules/is-core-module": { "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6487,20 +3657,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-data-view": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6516,6 +3676,8 @@ }, "node_modules/is-date-object": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6528,31 +3690,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-docker": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "license": "MIT", "bin": { "is-docker": "cli.js" @@ -6564,47 +3705,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA==", - "license": "MIT", - "dependencies": { - "is-primitive": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-finalizationregistry": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6616,21 +3720,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -6638,6 +3731,8 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6652,42 +3747,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6698,10 +3761,14 @@ }, "node_modules/is-my-ip-valid": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", + "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", "license": "MIT" }, "node_modules/is-my-json-valid": { "version": "2.20.6", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", + "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", "license": "MIT", "dependencies": { "generate-function": "^2.0.0", @@ -6713,6 +3780,8 @@ }, "node_modules/is-my-ssb-valid": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-my-ssb-valid/-/is-my-ssb-valid-1.2.2.tgz", + "integrity": "sha512-13B1KdYuXwfVASWWWAKcbtKHtXhFNzLtg16oKh/3UTAWVh9GBdf/zjvY9BFY3xYsYFQYC1WhZEuifbYRzeiz7w==", "license": "LGPL-3.0-only", "dependencies": { "is-my-json-valid": "^2.20.0", @@ -6721,6 +3790,8 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6729,20 +3800,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6757,76 +3818,29 @@ }, "node_modules/is-options": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz", + "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==", "license": "MIT", "dependencies": { "b4a": "^1.1.1" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, - "node_modules/is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "license": "MIT" - }, "node_modules/is-property": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6843,6 +3857,8 @@ }, "node_modules/is-set": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6853,6 +3869,8 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6866,6 +3884,8 @@ }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", "engines": { "node": ">=8" @@ -6876,6 +3896,8 @@ }, "node_modules/is-string": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6888,21 +3910,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-svg": { - "version": "4.4.0", - "license": "MIT", - "dependencies": { - "fast-xml-parser": "^4.1.3" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-symbol": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6918,6 +3929,8 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -6931,29 +3944,14 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true, + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, "node_modules/is-weakmap": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6964,6 +3962,8 @@ }, "node_modules/is-weakref": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6977,6 +3977,8 @@ }, "node_modules/is-weakset": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6989,17 +3991,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-wsl": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", "dependencies": { "is-docker": "^2.0.0" @@ -7010,86 +4005,20 @@ }, "node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isobject/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/isomorphic-ws": { - "version": "5.0.0", - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/iterate-iterator": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.2.tgz", + "integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7097,6 +4026,8 @@ }, "node_modules/iterate-value": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", "license": "MIT", "dependencies": { "es-get-iterator": "^1.0.2", @@ -7108,6 +4039,8 @@ }, "node_modules/jackspeak": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7121,6 +4054,8 @@ }, "node_modules/jitdb": { "version": "7.0.7", + "resolved": "https://registry.npmjs.org/jitdb/-/jitdb-7.0.7.tgz", + "integrity": "sha512-D/b1uyZzEvQewg9gDamgkCjvCji3ZYnirG86p3VHm/UzCohASd6ojLZrRPZHlsDRWsO5qhZxBzWoH7fSRtztdA==", "license": "LGPL-3.0", "dependencies": { "atomic-file-rw": "^0.3.0", @@ -7152,15 +4087,10 @@ "async-append-only-log": "^4.3.2" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/jsbn": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, "node_modules/jsdom": { @@ -7239,6 +4169,8 @@ }, "node_modules/jsesc": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -7247,27 +4179,10 @@ "node": ">=6" } }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonpointer": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7275,6 +4190,8 @@ }, "node_modules/key-value-file-store": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/key-value-file-store/-/key-value-file-store-1.1.1.tgz", + "integrity": "sha512-j2Vq2ftRSt99InoUUvMBgLX6BLrHZSa8nedntZyhqj2njzBaeL3IiUpT3DYx2p2+Fbf4vcmn74rJcds0uGpc0g==", "license": "MIT", "dependencies": { "atomic-file-rw": "^0.3.0" @@ -7282,6 +4199,8 @@ }, "node_modules/keygrip": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", "license": "MIT", "dependencies": { "tsscmp": "1.0.6" @@ -7290,24 +4209,6 @@ "node": ">= 0.6" } }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kind-of/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" - }, "node_modules/koa": { "version": "2.16.4", "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.4.tgz", @@ -7344,6 +4245,8 @@ }, "node_modules/koa-body": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/koa-body/-/koa-body-6.0.1.tgz", + "integrity": "sha512-M8ZvMD8r+kPHy28aWP9VxL7kY8oPWA+C7ZgCljrCMeaU7uX6wsIQgDHskyrAr9sw+jqnIXyv4Mlxri5R4InIJg==", "license": "MIT", "dependencies": { "@types/co-body": "^6.1.0", @@ -7356,10 +4259,14 @@ }, "node_modules/koa-compose": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", "license": "MIT" }, "node_modules/koa-convert": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", "license": "MIT", "dependencies": { "co": "^4.6.0", @@ -7371,6 +4278,8 @@ }, "node_modules/koa-mount": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.2.0.tgz", + "integrity": "sha512-2iHQc7vbA9qLeVq5gKAYh3m5DOMMlMfIKjW/REPAS18Mf63daCJHHVXY9nbu7ivrnYn5PiPC4CE523Tf5qvjeQ==", "license": "MIT", "dependencies": { "debug": "^4.0.1", @@ -7382,6 +4291,8 @@ }, "node_modules/koa-send": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -7394,6 +4305,8 @@ }, "node_modules/koa-send/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7401,6 +4314,8 @@ }, "node_modules/koa-send/node_modules/http-errors": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -7415,6 +4330,8 @@ }, "node_modules/koa-send/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7422,6 +4339,8 @@ }, "node_modules/koa-static": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", "license": "MIT", "dependencies": { "debug": "^3.1.0", @@ -7433,6 +4352,8 @@ }, "node_modules/koa/node_modules/http-errors": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -7447,6 +4368,8 @@ }, "node_modules/koa/node_modules/http-errors/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7454,6 +4377,8 @@ }, "node_modules/koa/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7461,6 +4386,8 @@ }, "node_modules/layered-graph": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/layered-graph/-/layered-graph-1.2.0.tgz", + "integrity": "sha512-VnjZHUyaXXsOR6KZ8JjTbkyjtq8VaU5ncGJ8cKBvg2bSLqVHE4bcGJ+20dDE7T84sss9d/BCHRj34Yh2fz9sbg==", "license": "MIT", "dependencies": { "dynamic-dijkstra": "^1.0.0", @@ -7471,6 +4398,8 @@ }, "node_modules/lazystream": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "license": "MIT", "dependencies": { "readable-stream": "^2.0.5" @@ -7481,10 +4410,14 @@ }, "node_modules/lazystream/node_modules/isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/lazystream/node_modules/readable-stream": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -7498,10 +4431,14 @@ }, "node_modules/lazystream/node_modules/safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -7509,6 +4446,8 @@ }, "node_modules/level": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", + "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", "license": "MIT", "dependencies": { "level-js": "^5.0.0", @@ -7525,6 +4464,8 @@ }, "node_modules/level-codec": { "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", "license": "MIT", "dependencies": { "buffer": "^5.6.0" @@ -7535,6 +4476,8 @@ }, "node_modules/level-codec/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -7557,6 +4500,8 @@ }, "node_modules/level-concat-iterator": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", "license": "MIT", "engines": { "node": ">=6" @@ -7564,6 +4509,8 @@ }, "node_modules/level-errors": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", "license": "MIT", "dependencies": { "errno": "~0.1.1" @@ -7574,6 +4521,8 @@ }, "node_modules/level-iterator-stream": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", "license": "MIT", "dependencies": { "inherits": "^2.0.4", @@ -7586,6 +4535,8 @@ }, "node_modules/level-iterator-stream/node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -7598,6 +4549,8 @@ }, "node_modules/level-js": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", + "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", "license": "MIT", "dependencies": { "abstract-leveldown": "~6.2.3", @@ -7608,6 +4561,8 @@ }, "node_modules/level-js/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -7630,6 +4585,8 @@ }, "node_modules/level-packager": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", "license": "MIT", "dependencies": { "encoding-down": "^6.3.0", @@ -7641,31 +4598,17 @@ }, "node_modules/level-post": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/level-post/-/level-post-1.0.7.tgz", + "integrity": "sha512-PWYqG4Q00asOrLhX7BejSajByB4EmG2GaKHfj3h5UmmZ2duciXLPGYWIjBzLECFWUGOZWlm5B20h/n3Gs3HKew==", "license": "MIT", "dependencies": { "ltgt": "^2.1.2" } }, - "node_modules/level-supports": { - "version": "6.2.0", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/level-transcoder": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "module-error": "^1.0.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/leveldown": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", + "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -7679,6 +4622,8 @@ }, "node_modules/leveldown/node_modules/node-gyp-build": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -7688,6 +4633,8 @@ }, "node_modules/levelup": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", "license": "MIT", "dependencies": { "deferred-leveldown": "~5.3.0", @@ -7702,6 +4649,8 @@ }, "node_modules/levelup/node_modules/level-supports": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", "license": "MIT", "dependencies": { "xtend": "^4.0.2" @@ -7710,67 +4659,21 @@ "node": ">=6" } }, - "node_modules/libnested": { - "version": "1.5.2", - "license": "MIT" - }, "node_modules/libsodium": { "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", + "integrity": "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==", "license": "ISC" }, "node_modules/libsodium-wrappers": { "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz", + "integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==", "license": "ISC", "dependencies": { "libsodium": "^0.7.15" } }, - "node_modules/lifecycle-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-3.1.1.tgz", - "integrity": "sha512-gNd3OvhFNjHykJE3uGntz7UuPzWlK9phrIdXxU9Adis0+ExkwnZibfxCJWiWWZ+a6VbKiZrb+9D9hCQWd4vjTg==", - "license": "MIT" - }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -7779,30 +4682,38 @@ }, "node_modules/lodash.chunk": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", + "integrity": "sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==", "license": "MIT" }, "node_modules/lodash.clone": { "version": "4.5.0", - "license": "MIT" - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, "node_modules/lodash.find": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", + "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==", "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, "node_modules/lodash.set": { @@ -7812,248 +4723,39 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", - "integrity": "sha512-4vSow8gbiGnwdDNrpy1dyNaXWKSCIPop0EHdE8GrnngHoJujM3QhvHUN/igsYCgPoHo7pFOezlJ61Hlln0KHyA==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^1.0.0", - "cli-cursor": "^1.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-update/node_modules/onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==", - "license": "MIT", - "dependencies": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "license": "Apache-2.0" - }, - "node_modules/longest-streak": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-1.0.0.tgz", - "integrity": "sha512-84jGpz/1j02Xm/L4y4uEXGxFFPHFabKjMHQ+rEPi0gPQbD5p0J3aZomvk0ZpUPpTtcVqhtSEq+4WNQbJjWiZ1Q==", - "license": "MIT" - }, "node_modules/looper": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-4.0.0.tgz", + "integrity": "sha512-NjGRcX4vCwyfbujv03omakGfAYh6St5kVsZFKfU23MFO1Z9/mZT8ypTZMEnvVC7nJeYtbqkRPFV4GoJBPdJgYw==", "license": "MIT" }, - "node_modules/lossy-store": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lossy-store/-/lossy-store-1.2.4.tgz", - "integrity": "sha512-rvqTCHZxWLvHAJv2w5vdCr5xICAW/FiXQ9wZGaAfBlNGG2FuLX0URNcj/zXC3qr5zQ8lBCbW8EEmDCKi7Dsp1g==", - "license": "MIT", - "dependencies": { - "mkdirp": "^0.5.1", - "tape": "^4.6.3" - } - }, - "node_modules/lossy-store/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loud-rejection/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/lowdb": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", - "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", - "license": "MIT", - "dependencies": { - "steno": "^4.0.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/lru-cache": { "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, "node_modules/ltgt": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", "license": "MIT" }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-filter-reduce": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "binary-search": "^1.2.0", - "compare-at-paths": "^1.0.0", - "pull-sink-through": "0.0.0", - "pull-sort": "^1.0.1", - "pull-stream": "^3.4.3", - "typewiselite": "^1.0.0" - } - }, "node_modules/map-merge": { "version": "1.1.0", - "license": "MIT" - }, - "node_modules/map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "license": "MIT", - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/markdown-table": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-0.4.0.tgz", - "integrity": "sha512-9i/E3ZtVAoaDulRQjoPseX2X5pBNdeR8MInQb57JFvCAq4glz/w2q31eL0NHMKOntzy2D6X3plZDH4+OGuz5Fw==", + "resolved": "https://registry.npmjs.org/map-merge/-/map-merge-1.1.0.tgz", + "integrity": "sha512-TGNNg3WqoLsS5HnlK6GHIXFvM/0wYMtlyflc1nAQUhgptr9wIu7JwQ2YsqnpFSqbSYFv2U6rgXQTHJmWJOtlzQ==", "license": "MIT" }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", - "license": "MIT" - }, - "node_modules/maybe-combine-errors": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/mdmanifest": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/mdmanifest/-/mdmanifest-1.0.8.tgz", - "integrity": "sha512-x7QEK3/RbANozWnUhmSPzT01yL/PsO2zBDWzDRr+tMRs0o/HQxbDzGb34JiQ2bGnSf88eKuMgrAzMR1+hyRBLw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0", - "remark": "^3.2.2", - "remark-html": "^2.0.2", - "word-wrap": "^1.1.0" - }, - "bin": { - "mdm": "mdm.js" - } - }, "node_modules/mdn-data": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", @@ -8062,90 +4764,29 @@ }, "node_modules/media-typer": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/meow": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", - "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", - "dev": true, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "license": "MIT", - "dependencies": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0", - "yargs-parser": "^10.0.0" + "bin": { + "mime": "cli.js" }, "engines": { - "node": ">=6" - } - }, - "node_modules/meow/node_modules/yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^4.1.0" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", - "license": "MIT", - "dependencies": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=10.0.0" } }, "node_modules/mime-db": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8153,6 +4794,8 @@ }, "node_modules/mime-types": { "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -8161,30 +4804,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -8209,71 +4828,26 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - }, - "engines": { - "node": ">= 4" - } - }, "node_modules/minipass": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "license": "MIT", - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mkdirp": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -8284,39 +4858,20 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/mock-property": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.1", - "functions-have-names": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "hasown": "^2.0.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/module-alias": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", "license": "MIT" }, - "node_modules/module-error": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/moment": { "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", "engines": { "node": "*" @@ -8324,18 +4879,26 @@ }, "node_modules/monotonic-timestamp": { "version": "0.0.9", + "resolved": "https://registry.npmjs.org/monotonic-timestamp/-/monotonic-timestamp-0.0.9.tgz", + "integrity": "sha512-PQcys7iTcXiMyW8cgK5B/fhKGteB/WLqWN1f+s7IJHyyRd80JnJYoNo03kTQcGZhVtsKz5kEWiS7W94gfY6qcQ==", "license": "MIT" }, "node_modules/moo": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", "license": "BSD-3-Clause" }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/multiblob": { "version": "1.13.8", + "resolved": "https://registry.npmjs.org/multiblob/-/multiblob-1.13.8.tgz", + "integrity": "sha512-YXNUflHc7I7iUiLctHMTvwDQOjMbCv+Pdbf+1lScFOsjP/CSZgwblRknPntcD+KEPGFudXPBtJR10TIrun3zmQ==", "license": "MIT", "dependencies": { "blake2s": "^1.0.1", @@ -8355,24 +4918,15 @@ "stream-to-pull-stream": "^1.7.2" } }, - "node_modules/multiblob-http": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "pull-many": "^1.0.9", - "pull-stream": "^3.6.14", - "range-parser": "^1.2.1", - "stream-to-pull-stream": "^1.7.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/multicb": { - "version": "1.2.2" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/multicb/-/multicb-1.2.2.tgz", + "integrity": "sha512-PZM4dhYFmCF6uZGWpEmoPMUqJBywS9IcAgybT2GmSpYI1BvGvoWSdbio+ik+q/YD2vodhvslESWIS3NnkKYdqQ==" }, "node_modules/multiserver": { "version": "3.8.2", + "resolved": "https://registry.npmjs.org/multiserver/-/multiserver-3.8.2.tgz", + "integrity": "sha512-gmR/5dY+N81EN0yDaziSJXAJhJjG+3Rv3lJc74OSA1ySRKOMqZwEuGJjqG6ZcZiaqcHvhQZkZavDJsAwtmnvoA==", "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -8391,6 +4945,8 @@ }, "node_modules/multiserver-address": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/multiserver-address/-/multiserver-address-1.0.1.tgz", + "integrity": "sha512-IfZMAGs9onCLkYNSnNBri3JxuvhQYllMyh3W9ry86iEDcfW9uPVsHTHDsjDxQtL+dPq3byshmA+Y4LN2wLHwNw==", "license": "MIT", "dependencies": { "nearley": "^2.15.1" @@ -8398,6 +4954,8 @@ }, "node_modules/multiserver-scopes": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/multiserver-scopes/-/multiserver-scopes-2.0.0.tgz", + "integrity": "sha512-XWv9J617i3mWtZIZQNTpYI9iq4goUpsKy3GdUEDls23z1VaMzuRp2rL3S3IKrheVdgmrf0zHbErcXokGxqQfzw==", "license": "MIT", "dependencies": { "non-private-ip": "^2.0.0" @@ -8405,197 +4963,45 @@ }, "node_modules/mutexify": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz", + "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==", "license": "MIT", "dependencies": { "queue-tick": "^1.0.0" } }, - "node_modules/muxrpc": { - "version": "8.0.0", - "license": "MIT", - "dependencies": { - "clarify-error": "^1.0.0", - "debug": "^4.3.3", - "packet-stream": "~2.0.0", - "packet-stream-codec": "^1.2.0", - "pull-goodbye": "0.0.3", - "pull-stream": "^3.6.10" - } - }, - "node_modules/muxrpc-usage": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "right-pad": "^1.0.1" - } - }, "node_modules/muxrpc-validation": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/muxrpc-validation/-/muxrpc-validation-3.0.2.tgz", + "integrity": "sha512-iWo/23xFnl+IGeX+LlfwoVKtyY4volPSodf3nwPScPgxjws4k2ZUozPG98OouMA0yn0JamqApjRw7eqLrzyV2A==", "license": "MIT", "dependencies": { "pull-stream": "^3.6.11", "zerr": "^1.0.4" } }, - "node_modules/muxrpcli": { - "version": "3.1.2", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "minimist": "^1.2.0", - "muxrpc-usage": "^2.0.1", - "pull-stream": "^3.6.9", - "stream-to-pull-stream": "^1.7.3", - "word-wrap": "^1.2.3" - } - }, - "node_modules/mv": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/mv/node_modules/glob": { - "version": "6.0.4", - "license": "ISC", - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mv/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mv/node_modules/rimraf": { - "version": "2.4.5", - "license": "ISC", - "dependencies": { - "glob": "^6.0.1" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/nan": { - "version": "2.26.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", - "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", - "license": "MIT", - "optional": true - }, "node_modules/nano-equal": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/nano-equal/-/nano-equal-2.0.2.tgz", + "integrity": "sha512-RtAioZtSSppEWRruYU8oz2AKPJWTW74TpzcqP79n7Kh4nHW+gmrJGcTuELmid9tvGR4Xp2cCoTtYIIDy9iWO4g==", "license": "MIT" }, "node_modules/nanoassert": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", + "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", "license": "ISC" }, - "node_modules/nanoid": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", - "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/napi-macros": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", "license": "MIT" }, - "node_modules/ncp": { - "version": "2.0.0", - "license": "MIT", - "bin": { - "ncp": "bin/ncp" - } - }, "node_modules/nearley": { "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", "license": "MIT", "dependencies": { "commander": "^2.19.0", @@ -8616,10 +5022,14 @@ }, "node_modules/nearley/node_modules/commander": { "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8627,57 +5037,14 @@ }, "node_modules/next-tick": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "license": "ISC" }, - "node_modules/nice-try": { - "version": "1.0.5", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/node-api-headers": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.8.0.tgz", - "integrity": "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -8685,409 +5052,22 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-llama-cpp": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/node-llama-cpp/-/node-llama-cpp-3.16.2.tgz", - "integrity": "sha512-ovhuTaXSWfcoyfI8ljWxO2Rg63mNxqQQAbDGkXRhlgsL7UjPqm2Nsy1bTNa0ZaQRg3vezG4agnCJTImrICY/0A==", - "hasInstallScript": true, + "node_modules/node-localstorage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", + "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", "license": "MIT", "dependencies": { - "@huggingface/jinja": "^0.5.5", - "async-retry": "^1.3.3", - "bytes": "^3.1.2", - "chalk": "^5.6.2", - "chmodrp": "^1.0.2", - "cmake-js": "^8.0.0", - "cross-spawn": "^7.0.6", - "env-var": "^7.5.0", - "filenamify": "^6.0.0", - "fs-extra": "^11.3.0", - "ignore": "^7.0.4", - "ipull": "^3.9.5", - "is-unicode-supported": "^2.1.0", - "lifecycle-utils": "^3.1.1", - "log-symbols": "^7.0.1", - "nanoid": "^5.1.6", - "node-addon-api": "^8.5.0", - "octokit": "^5.0.5", - "ora": "^9.3.0", - "pretty-ms": "^9.3.0", - "proper-lockfile": "^4.1.2", - "semver": "^7.7.1", - "simple-git": "^3.31.1", - "slice-ansi": "^8.0.0", - "stdout-update": "^4.0.1", - "strip-ansi": "^7.1.2", - "validate-npm-package-name": "^7.0.2", - "which": "^6.0.1", - "yargs": "^17.7.2" - }, - "bin": { - "nlc": "dist/cli/cli.js", - "node-llama-cpp": "dist/cli/cli.js" + "write-file-atomic": "^1.1.4" }, "engines": { - "node": ">=20.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/giladgd" - }, - "optionalDependencies": { - "@node-llama-cpp/linux-arm64": "3.16.2", - "@node-llama-cpp/linux-armv7l": "3.16.2", - "@node-llama-cpp/linux-x64": "3.16.2", - "@node-llama-cpp/linux-x64-cuda": "3.16.2", - "@node-llama-cpp/linux-x64-cuda-ext": "3.16.2", - "@node-llama-cpp/linux-x64-vulkan": "3.16.2", - "@node-llama-cpp/mac-arm64-metal": "3.16.2", - "@node-llama-cpp/mac-x64": "3.16.2", - "@node-llama-cpp/win-arm64": "3.16.2", - "@node-llama-cpp/win-x64": "3.16.2", - "@node-llama-cpp/win-x64-cuda": "3.16.2", - "@node-llama-cpp/win-x64-cuda-ext": "3.16.2", - "@node-llama-cpp/win-x64-vulkan": "3.16.2" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/node-llama-cpp/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/node-llama-cpp/node_modules/ignore": { - "version": "7.0.5", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/node-llama-cpp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-llama-cpp/node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/node-llama-cpp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nodemon": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", - "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^10.2.1", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/nodemon/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/nodemon/node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/nodemon/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nodemon/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/nodemon/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nodemon/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/nodemon/node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nodemon/node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nodemon/node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nodemon/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nodemon/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" + "node": ">=0.12" } }, "node_modules/non-private-ip": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/non-private-ip/-/non-private-ip-2.2.0.tgz", + "integrity": "sha512-NZ3Upr3K2whD6vdZ9k8gHsijsrQl5O6IARLIUDyvQwSuO/owM1kOMu8wDMMsIR8ujlLvhPNjlTZC2SXzWwWByQ==", "license": "MIT", "dependencies": { "ip": "^1.1.5" @@ -9096,1417 +5076,19 @@ "non-private-ip": "bin.js" } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/normalize-uri": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/normalize-uri/-/normalize-uri-1.1.3.tgz", - "integrity": "sha512-ECNgiM5IAeZNuXYu5kF4JV8t+MSFixHsvjexQtf/bLTOsL+KycDv3pod1a88N8uHtzsktYLRX6cAawg4aHeLVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/npm-install-package": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/npm-install-package/-/npm-install-package-2.1.0.tgz", - "integrity": "sha512-F37OfFN2Fk6ixI3bn8CNllSGMjpRxTqPcnN5j2zFlqNiaf/LGG6fCq3DeBGKRinyVMR5ROS34nWKQmn7tABuJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/npm-prefix": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/npm-prefix/-/npm-prefix-1.2.0.tgz", - "integrity": "sha512-EkGZ7jtA2onsULFpnZ/P5S0DPy8w9qH1TVytPhY54s+dmtLXBmp1evt8W9nfg5JEay24K3bX9WWTIHR8WQcOJA==", - "license": "MIT", - "dependencies": { - "rc": "^1.1.0", - "shellsubstitute": "^1.1.0", - "untildify": "^2.1.0" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-13.3.0.tgz", - "integrity": "sha512-P+FwIuro2aFG6B0Esd9ZDWUd51uZrAEoGutqZxzrVmYl3qSfkLgcQpBPBjtDFsUQLFY1dvTQJPOyeqr8S9GF8w==", - "bundleDependencies": [ - "archy", - "arrify", - "caching-transform", - "convert-source-map", - "find-cache-dir", - "find-up", - "foreground-child", - "glob", - "istanbul-lib-coverage", - "istanbul-lib-hook", - "istanbul-lib-report", - "istanbul-lib-source-maps", - "istanbul-reports", - "make-dir", - "merge-source-map", - "resolve-from", - "rimraf", - "signal-exit", - "spawn-wrap", - "test-exclude", - "uuid", - "yargs", - "yargs-parser" - ], - "dev": true, - "license": "ISC", - "dependencies": { - "archy": "^1.0.0", - "arrify": "^1.0.1", - "caching-transform": "^3.0.1", - "convert-source-map": "^1.6.0", - "find-cache-dir": "^2.0.0", - "find-up": "^3.0.0", - "foreground-child": "^1.5.6", - "glob": "^7.1.3", - "istanbul-lib-coverage": "^2.0.3", - "istanbul-lib-hook": "^2.0.3", - "istanbul-lib-instrument": "^3.1.0", - "istanbul-lib-report": "^2.0.4", - "istanbul-lib-source-maps": "^3.0.2", - "istanbul-reports": "^2.1.1", - "make-dir": "^1.3.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.3", - "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^5.1.0", - "uuid": "^3.3.2", - "yargs": "^12.0.5", - "yargs-parser": "^11.1.1" - }, - "bin": { - "nyc": "bin/nyc.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/ansi-regex": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/append-transform": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "default-require-extensions": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/archy": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/arrify": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/async": { - "version": "2.6.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.11" - } - }, - "node_modules/nyc/node_modules/balanced-match": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/brace-expansion": { - "version": "1.1.11", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nyc/node_modules/caching-transform": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "hasha": "^3.0.0", - "make-dir": "^1.3.0", - "package-hash": "^3.0.0", - "write-file-atomic": "^2.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/camelcase": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/cliui": { - "version": "4.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/nyc/node_modules/code-point-at": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/commander": { - "version": "2.17.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/commondir": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/concat-map": { - "version": "0.0.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/convert-source-map": { - "version": "1.6.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/nyc/node_modules/cross-spawn": { - "version": "4.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "node_modules/nyc/node_modules/debug": { - "version": "4.1.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/nyc/node_modules/decamelize": { - "version": "1.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/default-require-extensions": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/end-of-stream": { - "version": "1.4.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/nyc/node_modules/error-ex": { - "version": "1.3.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/nyc/node_modules/es6-error": { - "version": "4.1.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/execa": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/nyc/node_modules/find-cache-dir": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/find-up": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/foreground-child": { - "version": "1.5.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" - } - }, - "node_modules/nyc/node_modules/fs.realpath": { - "version": "1.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/get-caller-file": { - "version": "1.0.3", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/get-stream": { - "version": "4.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/glob": { - "version": "7.1.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nyc/node_modules/graceful-fs": { - "version": "4.1.15", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/handlebars": { - "version": "4.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "async": "^2.5.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/nyc/node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/has-flag": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/hasha": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "is-stream": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/hosted-git-info": { - "version": "2.7.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/imurmurhash": { - "version": "0.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/nyc/node_modules/inflight": { - "version": "1.0.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/nyc/node_modules/inherits": { - "version": "2.0.3", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/invert-kv": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/is-arrayish": { - "version": "0.2.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/is-stream": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/isexe": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/istanbul-lib-coverage": { - "version": "2.0.3", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-hook": { - "version": "2.0.3", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "append-transform": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-report": { - "version": "2.0.4", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^2.0.3", - "make-dir": "^1.3.0", - "supports-color": "^6.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-source-maps": { - "version": "3.0.2", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.3", - "make-dir": "^1.3.0", - "rimraf": "^2.6.2", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/istanbul-reports": { - "version": "2.1.1", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "handlebars": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/json-parse-better-errors": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/lcid": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "invert-kv": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/load-json-file": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/locate-path": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/lodash": { - "version": "4.17.11", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/lodash.flattendeep": { - "version": "4.4.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/lru-cache": { - "version": "4.1.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/nyc/node_modules/make-dir": { - "version": "1.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/map-age-cleaner": { - "version": "0.1.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/mem": { - "version": "4.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/merge-source-map": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/nyc/node_modules/merge-source-map/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/mimic-fn": { - "version": "1.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/minimatch": { - "version": "3.0.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nyc/node_modules/minimist": { - "version": "0.0.10", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/mkdirp": { - "version": "0.5.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minimist": "0.0.8" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/nyc/node_modules/mkdirp/node_modules/minimist": { - "version": "0.0.8", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/ms": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/nice-try": { - "version": "1.0.5", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/normalize-package-data": { - "version": "2.5.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/nyc/node_modules/npm-run-path": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/number-is-nan": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/once": { - "version": "1.4.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/nyc/node_modules/optimist": { - "version": "0.6.1", - "inBundle": true, - "license": "MIT/X11", - "dependencies": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "node_modules/nyc/node_modules/os-homedir": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/os-locale": { - "version": "3.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/p-defer": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/p-finally": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/p-is-promise": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/p-limit": { - "version": "2.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/p-locate": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/p-try": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/package-hash": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.15", - "hasha": "^3.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/parse-json": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/path-exists": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/path-is-absolute": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/path-key": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/path-parse": { - "version": "1.0.6", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/path-type": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/pify": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/pkg-dir": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/pseudomap": { - "version": "1.0.2", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/pump": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/nyc/node_modules/read-pkg": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/read-pkg-up": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/release-zalgo": { - "version": "1.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "es6-error": "^4.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/require-directory": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/require-main-filename": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/resolve": { - "version": "1.10.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-parse": "^1.0.6" - } - }, - "node_modules/nyc/node_modules/resolve-from": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/rimraf": { - "version": "2.6.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/nyc/node_modules/safe-buffer": { - "version": "5.1.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/semver": { - "version": "5.6.0", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/nyc/node_modules/set-blocking": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/shebang-command": { - "version": "1.2.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/shebang-regex": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/signal-exit": { - "version": "3.0.2", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/spawn-wrap": { - "version": "1.4.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^1.5.6", - "mkdirp": "^0.5.0", - "os-homedir": "^1.0.1", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "which": "^1.3.0" - } - }, - "node_modules/nyc/node_modules/spdx-correct": { - "version": "3.1.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/nyc/node_modules/spdx-exceptions": { - "version": "2.2.0", - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/nyc/node_modules/spdx-expression-parse": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/nyc/node_modules/spdx-license-ids": { - "version": "3.0.3", - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/nyc/node_modules/string-width": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/strip-ansi": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/strip-bom": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/strip-eof": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/test-exclude": { - "version": "5.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "arrify": "^1.0.1", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^1.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/uglify-js": { - "version": "3.4.9", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/nyc/node_modules/uglify-js/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/uuid": { - "version": "3.3.2", - "inBundle": true, - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/nyc/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/nyc/node_modules/which": { - "version": "1.3.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/nyc/node_modules/which-module": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/wordwrap": { - "version": "0.0.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi": { - "version": "2.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi/node_modules/string-width": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrappy": { - "version": "1.0.2", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/write-file-atomic": { - "version": "2.4.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "node_modules/nyc/node_modules/y18n": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/yallist": { - "version": "2.1.2", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/yargs": { - "version": "12.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "node_modules/nyc/node_modules/yargs-parser": { - "version": "11.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "license": "MIT", - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/object-inspect": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10515,50 +5097,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-visit/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.assign": { "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10577,6 +5128,8 @@ }, "node_modules/object.entries": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10588,92 +5141,28 @@ "node": ">= 0.4" } }, - "node_modules/object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA==", - "license": "MIT", - "dependencies": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/observ": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/observ/-/observ-0.2.0.tgz", - "integrity": "sha512-7SKbYTiFXMvqbeinO/GwTGtXSvbRq5AHKszmq5cx3fV7bKEjNCK7AdkhGzsuQAoT+IPf3mlxDmPrVW8wgshpcQ==" - }, - "node_modules/observ-debounce": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/observ-debounce/-/observ-debounce-1.1.1.tgz", - "integrity": "sha512-vP3EdCDE0+6tftznrvmmfI66qiOEkSV+zJ/TLo/3x5BZjm0a3I9A12V16iPck1v2cwAgGRPtWVF3vUmFN9vtPw==", - "license": "MIT", - "dependencies": { - "observ": "~0.2.0" - } - }, "node_modules/obv": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/obv/-/obv-0.0.1.tgz", + "integrity": "sha512-Iq/Q3TXAfHgABGL7mKlG3ILJnT8jgetOt0sH9a6SGLLCvjh58bsH7RzixLJWkVO6aQK6hI4rxVYHF0rk9KmIwA==", "license": "MIT" }, "node_modules/obz": { "version": "1.1.1", - "license": "MIT" - }, - "node_modules/octokit": { - "version": "5.0.5", - "license": "MIT", - "dependencies": { - "@octokit/app": "^16.1.2", - "@octokit/core": "^7.0.6", - "@octokit/oauth-app": "^8.0.3", - "@octokit/plugin-paginate-graphql": "^6.0.0", - "@octokit/plugin-paginate-rest": "^14.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0", - "@octokit/plugin-retry": "^8.0.3", - "@octokit/plugin-throttling": "^11.0.3", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "@octokit/webhooks": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/on-change-network": { - "version": "0.0.2", + "resolved": "https://registry.npmjs.org/obz/-/obz-1.1.1.tgz", + "integrity": "sha512-eUiAX663dASOUIEWuRzUycf2ERZJFLbdjfzYRtju9z2eBfDPywcWjfyHV5JNbVoBjuHGVodyfeyy8Qu09+JCXw==", "license": "MIT" }, "node_modules/on-change-network-strict": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/on-change-network-strict/-/on-change-network-strict-1.0.0.tgz", + "integrity": "sha512-ldHCpTJWgr5KUJy3/TVoSGNwBUA8BP9UFmd0iQqe4aGaXY4PJyzQPiVBIo8VBSlSoKyaJY3vcpW0hixZb6gPaA==", "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -10684,79 +5173,28 @@ }, "node_modules/on-wakeup": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-wakeup/-/on-wakeup-1.0.1.tgz", + "integrity": "sha512-3ufOvnTvh39ah2/TT++HpLailHVmEVVrKtzKLKifAUyWbulKLGGJGOF7ywKC4k/iQGmn9KooV6WmQl/6BVwklA==", "license": "MIT" }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/only": { - "version": "0.0.2" - }, - "node_modules/onnx-proto": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz", - "integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==", - "license": "MIT", - "dependencies": { - "protobufjs": "^6.8.8" - } - }, - "node_modules/onnxruntime-common": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz", - "integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==", - "license": "MIT" - }, - "node_modules/onnxruntime-node": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz", - "integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==", - "license": "MIT", - "optional": true, - "os": [ - "win32", - "darwin", - "linux" - ], - "dependencies": { - "onnxruntime-common": "~1.14.0" - } - }, - "node_modules/onnxruntime-web": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz", - "integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==", - "license": "MIT", - "dependencies": { - "flatbuffers": "^1.12.0", - "guid-typescript": "^1.0.9", - "long": "^4.0.0", - "onnx-proto": "^4.0.4", - "onnxruntime-common": "~1.14.0", - "platform": "^1.3.6" - } + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==" }, "node_modules/open": { "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", @@ -10770,13 +5208,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/opencollective-postinstall": { - "version": "2.0.3", - "license": "MIT", - "bin": { - "opencollective-postinstall": "index.js" - } - }, "node_modules/openpgp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-6.3.0.tgz", @@ -10786,107 +5217,25 @@ "node": ">= 18.0.0" } }, - "node_modules/ora": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", - "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.1", - "string-width": "^8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ora/node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/string-width": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", - "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "node_modules/opusscript": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.1.1.tgz", + "integrity": "sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==", + "license": "MIT" }, "node_modules/os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "license": "ISC", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "node_modules/own-keys": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", @@ -10902,49 +5251,17 @@ }, "node_modules/p-defer": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/p-try": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { "node": ">=6" @@ -10952,10 +5269,14 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/packet-stream": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/packet-stream/-/packet-stream-2.0.6.tgz", + "integrity": "sha512-kSxHpoTqlgNEetMp77snCTVILwLw4dJX6pB/z1g1PRG5xylH8cf9upIPygt+epBC3l14XrcZH4/kQYSrzp2Ijg==", "license": "MIT", "engines": { "node": ">=8" @@ -10963,6 +5284,8 @@ }, "node_modules/packet-stream-codec": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/packet-stream-codec/-/packet-stream-codec-1.2.0.tgz", + "integrity": "sha512-3xoTsSVqCPd+0mPsQGlfYm2ecvJK9tS1HOxrjnKEiB1Ynq0fOJHEXcZV/hxW6BkOSGBsGX7dTN8bjdNTU3nKBA==", "license": "MIT", "dependencies": { "pull-reader": "^1.3.1", @@ -10972,51 +5295,26 @@ "node": ">=12" } }, - "node_modules/parse-entities": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", - "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", - "license": "MIT", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "node_modules/parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==", - "license": "MIT", - "dependencies": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" }, "node_modules/parse-ms": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", "license": "MIT", "engines": { "node": ">=6" @@ -11036,22 +5334,23 @@ }, "node_modules/parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" @@ -11059,6 +5358,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11066,6 +5367,8 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -11073,10 +5376,14 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -11091,33 +5398,14 @@ }, "node_modules/path-to-regexp": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, - "node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pdfjs-dist": { - "version": "5.3.31", - "license": "Apache-2.0", - "engines": { - "node": ">=20.16.0 || >=22.3.0" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^0.1.67" - } - }, "node_modules/peek-readable": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", "license": "MIT", "engines": { "node": ">=8" @@ -11127,77 +5415,10 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/platform": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", - "license": "MIT" - }, - "node_modules/plur": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", - "integrity": "sha512-WhcHk576xg9y/iv6RWOuroZgsqvCbJN+XGvAypCJwLAYs2iWDp5LUmvaCdV6JR2O0SMBf8l6p7A94AyLCFVMlQ==", - "license": "MIT", - "dependencies": { - "irregular-plurals": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pngjs": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -11205,126 +5426,27 @@ }, "node_modules/polyraf": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/polyraf/-/polyraf-1.1.0.tgz", + "integrity": "sha512-wKRyhZQE6AC70nJyMCWbwd/dX4S6UsFz+58qAF8HTpCn6C7pvr63FiH4vR9vT6VrhZrJKuT7A81ea/lIslS0bA==", "license": "MIT", "dependencies": { "random-access-file": "^2.1.0", "random-access-web": "^2.0.1" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/prebuild-install/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/prebuild-install/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pretty-ms": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", "license": "MIT", "dependencies": { "parse-ms": "^2.1.0" @@ -11338,10 +5460,14 @@ }, "node_modules/print-tree": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/print-tree/-/print-tree-0.1.5.tgz", + "integrity": "sha512-t5eFtuheMgWGR3kj1vtxsvfWrrecEEWow3IgEtbW0EPbYRufHAvB+ShK1DgRjWxEYYEqO4B6DNlkoPmh7E3LGQ==", "license": "MIT" }, "node_modules/private-box": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/private-box/-/private-box-0.3.1.tgz", + "integrity": "sha512-abAuk3ZDyQvPLY6MygtwaDTUBIZ0C5wMMuX1jXa0svazV+keTwn7cPobRv4WYA9ctsDUztm/9CYu4y2TPL08xw==", "license": "MIT", "dependencies": { "chloride": "^2.2.9" @@ -11349,6 +5475,8 @@ }, "node_modules/private-group-spec": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-1.2.0.tgz", + "integrity": "sha512-O7SfG+vZIZgqDXy/wjsuTRI5LaozW4rxaZBpGmwlcDfjIvxvYWNboyNm1PoQUU6j4dQ02V1tOQVLDq9u2RzolA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -11356,6 +5484,8 @@ }, "node_modules/process": { "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -11363,10 +5493,14 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, "node_modules/promisify-4loc": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/promisify-4loc/-/promisify-4loc-1.0.0.tgz", + "integrity": "sha512-u/XtndUyqqDXAuhFEgFgkpjHG8IizREoj80j5dL4t41eE9yH0gzFPyOD21/VnikdPJtRziuqf6ryTu1HoTjyog==", "license": "MIT", "engines": { "node": ">=6" @@ -11374,6 +5508,8 @@ }, "node_modules/promisify-tuple": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/promisify-tuple/-/promisify-tuple-1.2.0.tgz", + "integrity": "sha512-DRI8QrLUzbQxgwLiwKhtVCpSqtAUnnyPaCi3cad2+0avb2o5UzobLWHkXUOAYQB8e4fSJVef22eVm77c/8n//g==", "license": "MIT", "engines": { "node": ">=6" @@ -11381,58 +5517,10 @@ }, "node_modules/promisize": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promisize/-/promisize-1.1.2.tgz", + "integrity": "sha512-6/X05CD1iri6YyLy6TW7a23HY0igsrb/qetltYKfJznLfzmspWtN/cY/UR0By3M5i13hBDWfmM2P42ovKl3GAw==", "license": "MIT" }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/signal-exit": { - "version": "3.0.7", - "license": "ISC" - }, - "node_modules/protobufjs": { - "version": "6.11.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.6.tgz", - "integrity": "sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -11444,42 +5532,35 @@ }, "node_modules/prr": { "version": "1.0.1", - "license": "MIT" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "license": "MIT" }, "node_modules/pull-abortable": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/pull-abortable/-/pull-abortable-4.1.1.tgz", + "integrity": "sha512-/zz8tW+T5tvtDc4NQVZTp3lJZoGEpUb2VDV3R5Q2/9lavNTwHK+hC5yF/3Q3LMmdn5AluFy23RFoqXIoySPl1w==", "license": "MIT" }, "node_modules/pull-async": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-async/-/pull-async-1.0.0.tgz", + "integrity": "sha512-mm0mZe9tb3yjIhfXt384c8EXGFAtMbau20At2yNqZDPXHevjrfom5b82sk8BMQfGhj6gXimnOde1lhUKkF/v9w==", "license": "Apache-2.0" }, "node_modules/pull-awaitable": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-awaitable/-/pull-awaitable-1.0.0.tgz", + "integrity": "sha512-tfufzU9wMd1rM38tcgunaKMUNGXMj1Qms1ygEw+ZerkMmLAwilEtMsyzrDSLhNlnfhSzHXovOarieSNrTD1ovQ==", "license": "MIT", "dependencies": { "pull-thenable": "~1.0.0" } }, - "node_modules/pull-bitflipper": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pull-bitflipper/-/pull-bitflipper-0.1.1.tgz", - "integrity": "sha512-MloQIUQkooVrlc4qom1rDeUsnx9Fol4ieMhlJEjpT/v2x37+ydL164jbRkvDFCpWBp+9qLKPgWoQum/zh8iHkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pull-stream": "^3.6.11" - } - }, "node_modules/pull-box-stream": { "version": "1.0.13", + "resolved": "https://registry.npmjs.org/pull-box-stream/-/pull-box-stream-1.0.13.tgz", + "integrity": "sha512-OPg5cTnBCH9jpTCNZfm+BrtrAzmu5NsWDyDadaWiB06oWQujSMfRJp58VqhuJOCEgxVkeKFhXTaSKE+StNKRHw==", "license": "MIT", "dependencies": { "chloride": "^2.2.7", @@ -11492,18 +5573,26 @@ }, "node_modules/pull-cat": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/pull-cat/-/pull-cat-1.1.11.tgz", + "integrity": "sha512-i3w+xZ3DCtTVz8S62hBOuNLRHqVDsHMNZmgrZsjPnsxXUgbWtXEee84lo1XswE7W2a3WHyqsNuDJTjVLAQR8xg==", "license": "MIT" }, "node_modules/pull-catch": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pull-catch/-/pull-catch-1.1.0.tgz", + "integrity": "sha512-x6DkWZfBVvYrjUIcFroe3GtBkZcRINi8PsYgx3qBYluIeawLn4Z5oJI++WE5AJPbIZBmxHFx+zIvQmhRYaSeFA==", "license": "ISC" }, "node_modules/pull-cont": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pull-cont/-/pull-cont-0.1.1.tgz", + "integrity": "sha512-OAJRFBoEW00F0++qkeNuIWdaUwqrlZGj/6JAm+094iWKDnuzBXUWi2IipDqNa+yzIJrOE8BjwugiyGtLvPAk4w==", "license": "MIT" }, "node_modules/pull-cursor": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pull-cursor/-/pull-cursor-3.0.0.tgz", + "integrity": "sha512-95lZVSF2eSEdOmUtlOBaD9p5YOvlYeCr5FBv2ySqcj/4rpaXI6d8OH+zPHHjKAf58R8QXJRZuyfHkcCX8TZbAg==", "license": "MIT", "dependencies": { "looper": "^4.0.0", @@ -11513,10 +5602,14 @@ }, "node_modules/pull-defer": { "version": "0.2.3", + "resolved": "https://registry.npmjs.org/pull-defer/-/pull-defer-0.2.3.tgz", + "integrity": "sha512-/An3KE7mVjZCqNhZsr22k1Tx8MACnUnHZZNPSJ0S62td8JtYr/AiRG42Vz7Syu31SoTLUzVIe61jtT/pNdjVYA==", "license": "MIT" }, "node_modules/pull-drain-gently": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pull-drain-gently/-/pull-drain-gently-1.1.0.tgz", + "integrity": "sha512-ZUPsNrn8jkU6Y2B4w8Jz3gXAmjSpb+qn4AQhAL8qTWUHULglH16ANr+6qnfOEa1kUoUGVCQZaORTd2NSQFAnhA==", "license": "MIT", "dependencies": { "cpu-percentage": "~1.0.3", @@ -11525,6 +5618,8 @@ }, "node_modules/pull-file": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pull-file/-/pull-file-1.1.0.tgz", + "integrity": "sha512-yP3hNN3C0PrxA5isqyzHvsvFxd7Ohjvp4aLreeHm8mhnipxUfbY08Mk7o/KDsnR8N4WT9W3CqMDysU+ZufMr6w==", "license": "MIT", "dependencies": { "pull-utf8-decoder": "^1.0.2" @@ -11532,6 +5627,8 @@ }, "node_modules/pull-flat-merge": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pull-flat-merge/-/pull-flat-merge-2.1.0.tgz", + "integrity": "sha512-V/HzK88m+N867KfDkDA0u9lHrQIcLMBsSllVReO4mU3hJ2PaPOPxtTXQuQc+eIWMHavWqaWSkim8O8s2P4dTLg==", "license": "ISC", "dependencies": { "pull-many": "^1.0.8", @@ -11540,10 +5637,14 @@ }, "node_modules/pull-flatmap": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pull-flatmap/-/pull-flatmap-0.0.1.tgz", + "integrity": "sha512-9BgwZPZ6J22kPf9ExoVI3m2yMHdCK8uPf58p6L63t36IgH7NrCX+p3QV8cQ4JmYjwvXDZzimVuJ7IW7iLxm7cA==", "license": "MIT" }, "node_modules/pull-fs": { "version": "1.1.6", + "resolved": "https://registry.npmjs.org/pull-fs/-/pull-fs-1.1.6.tgz", + "integrity": "sha512-xO+d44h/FdlSW61RpxUdqLhNz5vHKnjqEyGaXIVvZa5TxkOvIzhkWd7BXqH8oakXCxb8NozN6F/w1BtfP8Z64Q==", "license": "MIT", "dependencies": { "pull-file": "^0.5.0", @@ -11554,6 +5655,8 @@ }, "node_modules/pull-fs/node_modules/pull-file": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pull-file/-/pull-file-0.5.0.tgz", + "integrity": "sha512-4s/IfBLebH/eWUlhp3F3M29mk+B1cryp3W99t/k9J/c033f+HyLLVB2EAMGoN02j8ff7ObkLF1tMaHok3nGS6Q==", "license": "MIT", "dependencies": { "pull-utf8-decoder": "^1.0.2" @@ -11561,6 +5664,8 @@ }, "node_modules/pull-glob": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/pull-glob/-/pull-glob-1.0.7.tgz", + "integrity": "sha512-nQrhcraiOUfMYiFoJyGLICb5AzlmWm5Vzwhqt+wNuimCG1kqrjdWycw5Dk9xQbHYbq5U55hs3Hee0qZgpjZ3bA==", "license": "MIT", "dependencies": { "pull-fs": "~1.1.6", @@ -11572,6 +5677,8 @@ }, "node_modules/pull-goodbye": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/pull-goodbye/-/pull-goodbye-0.0.3.tgz", + "integrity": "sha512-fl3RcIHKsxFaygdU3dcwSznLr73HYGOEU9IshpiatYSV+PW3TOUEjtfdu1L8uIsUoDajJz3HM/+mG0mFD4+v5A==", "license": "MIT", "dependencies": { "pull-stream": "^3.6.14" @@ -11579,6 +5686,8 @@ }, "node_modules/pull-handshake": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/pull-handshake/-/pull-handshake-1.1.4.tgz", + "integrity": "sha512-+dT8auWatbSNt1o43GKygECvOM2znXdyWwhEQaxztJSvxrNtEqo/wgZttknLfyxxbkbS6qUten6LsXTOwYj0yg==", "license": "MIT", "dependencies": { "pull-cat": "^1.1.9", @@ -11589,10 +5698,14 @@ }, "node_modules/pull-hash": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-hash/-/pull-hash-1.0.0.tgz", + "integrity": "sha512-m+6RcvwRdpYLxWgSmviXo3tmZHdpniWDJj3ryC39RYH4oc6AXhkoBHkQuA4yhmkM0WQh+OP+vD9F5oCozhhxIg==", "license": "Fair" }, "node_modules/pull-inactivity": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/pull-inactivity/-/pull-inactivity-2.1.4.tgz", + "integrity": "sha512-W2Q+6Jk0oRMICYXBXom3/ipz2U5YPUSQUfLvVgUqL/daHop7QQB3Jz5XpnnxsekCPoM61lGvXc7kFDZt0uWzMg==", "license": "MIT", "dependencies": { "pull-abortable": "^4.1.1", @@ -11601,6 +5714,8 @@ }, "node_modules/pull-level": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pull-level/-/pull-level-2.0.4.tgz", + "integrity": "sha512-fW6pljDeUThpq5KXwKbRG3X7Ogk3vc75d5OQU/TvXXui65ykm+Bn+fiktg+MOx2jJ85cd+sheufPL+rw9QSVZg==", "license": "MIT", "dependencies": { "level-post": "^1.0.7", @@ -11614,6 +5729,8 @@ }, "node_modules/pull-live": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pull-live/-/pull-live-1.0.1.tgz", + "integrity": "sha512-tkNz1QT5gId8aPhV5+dmwoIiA1nmfDOzJDlOOUpU5DNusj6neNd3EePybJ5+sITr2FwyCs/FVpx74YMCfc8YeA==", "license": "MIT", "dependencies": { "pull-cat": "^1.1.9", @@ -11622,6 +5739,8 @@ }, "node_modules/pull-looper": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-looper/-/pull-looper-1.0.0.tgz", + "integrity": "sha512-djlD60A6NGe5goLdP5pgbqzMEiWmk1bInuAzBp0QOH4vDrVwh05YDz6UP8+pOXveKEk8wHVP+rB2jBrK31QMPA==", "license": "Fair", "dependencies": { "looper": "^4.0.0" @@ -11629,19 +5748,17 @@ }, "node_modules/pull-many": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/pull-many/-/pull-many-1.0.9.tgz", + "integrity": "sha512-+jUydDVlj/HsvtDqxWMSsiRq3B0HVo7RhBV4C0p2nZRS3mFTUEu9SPEBN+B5PMaW8KTnblYhTIaKg7oXgGnj4Q==", "license": "MIT", "dependencies": { "pull-stream": "^3.4.5" } }, - "node_modules/pull-next": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pull-next/-/pull-next-1.0.1.tgz", - "integrity": "sha512-CAOxYSfmFmQ+jxuwnmaXmovYOmVD+XC7X+5uyZh4hVg06DUK7O9uccsIQkGu493eGkAqASlmB5zekEH7oxS0AQ==", - "license": "MIT" - }, "node_modules/pull-notify": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pull-notify/-/pull-notify-0.1.2.tgz", + "integrity": "sha512-oooAxYEUGNbOVsUrmqqTWWsAUMRIs4sYglnxgleiVcWyvrWgOuk/WUoZDajPTsYix2/rd+z5xSclzHLA7QygcQ==", "license": "MIT", "dependencies": { "pull-pushable": "^2.0.0" @@ -11649,10 +5766,14 @@ }, "node_modules/pull-pair": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pull-pair/-/pull-pair-1.1.0.tgz", + "integrity": "sha512-7VEFLxWnj2AKaN3AvtyaM508d1+56/ulSjDLu4j6kcd9DnEM6LfG8b1R9gCLqzIjJo5stFrWynVc6e451OW4LQ==", "license": "MIT" }, "node_modules/pull-paramap": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/pull-paramap/-/pull-paramap-1.2.2.tgz", + "integrity": "sha512-0ALwLcASif3KmKCXO4liySCKsTfC/mGYhRFch+xLvfJJrYOIfXPthrykDUZpeZtbjEXglPySzfUmYcnBqzTj5w==", "license": "MIT", "dependencies": { "looper": "^4.0.0" @@ -11660,10 +5781,14 @@ }, "node_modules/pull-pause": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/pull-pause/-/pull-pause-0.0.2.tgz", + "integrity": "sha512-yQs63NDgD/FeAsdF7INpy4uDolkstkS4Gx1Z3BvMR2gQS8RREJd2tsy6d/K6T+/CDStGnCQI7JLrkjz10YNiSA==", "license": "MIT" }, "node_modules/pull-ping": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pull-ping/-/pull-ping-2.0.3.tgz", + "integrity": "sha512-nbY4yHnMesJBrvkbhMim4VXUC9k1VCkgrkQu49pf8mxFbmb/U2KQrsuePvSmLjRL+VgkBVRSUXUoOY7DtSvhKw==", "license": "MIT", "dependencies": { "pull-pushable": "^2.0.0", @@ -11673,10 +5798,14 @@ }, "node_modules/pull-pushable": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pull-pushable/-/pull-pushable-2.2.0.tgz", + "integrity": "sha512-M7dp95enQ2kaHvfCt2+DJfyzgCSpWVR2h2kWYnVsW6ZpxQBx5wOu0QWOvQPVoPnBLUZYitYP2y7HyHkLQNeGXg==", "license": "MIT" }, "node_modules/pull-rate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pull-rate/-/pull-rate-1.0.2.tgz", + "integrity": "sha512-HH4fXHM7gu2uCO6u2SqTuGGDqPZNVHKXjTdyP9igg8jAROnvSph9plMAQqmv4elYFCm+Bfah2ASGWWe/+NdzMQ==", "license": "MIT", "dependencies": { "pull-stream": "^3.6.0" @@ -11684,14 +5813,14 @@ }, "node_modules/pull-reader": { "version": "1.3.1", - "license": "MIT" - }, - "node_modules/pull-sink-through": { - "version": "0.0.0", + "resolved": "https://registry.npmjs.org/pull-reader/-/pull-reader-1.3.1.tgz", + "integrity": "sha512-CBkejkE5nX50SiSEzu0Qoz4POTJMS/mw8G6aj3h3M/RJoKgggLxyF0IyTZ0mmpXFlXRcLmLmIEW4xeYn7AeDYw==", "license": "MIT" }, "node_modules/pull-sort": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pull-sort/-/pull-sort-1.0.2.tgz", + "integrity": "sha512-jGcAHMP+0Le+bEIhSODlbNNd3jW+S6XrXOlhVzfcKU5HQZjP92OzQSgHHSlwvWRsiTWi+UGgbFpL/5gGgmFoVQ==", "license": "MIT", "dependencies": { "pull-defer": "^0.2.3", @@ -11700,10 +5829,14 @@ }, "node_modules/pull-stream": { "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.7.0.tgz", + "integrity": "sha512-Eco+/R004UaCK2qEDE8vGklcTG2OeZSVm1kTUQNrykEjDwcFXDZhygFDsW49DbXyJMEhHeRL3z5cRVqPAhXlIw==", "license": "MIT" }, "node_modules/pull-thenable": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-thenable/-/pull-thenable-1.0.0.tgz", + "integrity": "sha512-gio2Yanuj5lcN8X+VXtx6F2iWxnhxfaHSKaSJHUzJMNNboEz48/+udzZFDaya8kAwj3DbuFFt1pbb8m1tIS6PQ==", "license": "MIT", "dependencies": { "quicktask": "~1.0.0" @@ -11711,6 +5844,8 @@ }, "node_modules/pull-through": { "version": "1.0.18", + "resolved": "https://registry.npmjs.org/pull-through/-/pull-through-1.0.18.tgz", + "integrity": "sha512-t8BBRdKstI/JoWS3FAXRnBvo1ahoLcKo8nm8ZUzr4OUgIf47RodD6HjnPIpw/d6HQZaMu+TotLAYCNwLTnvcNw==", "license": "MIT", "dependencies": { "looper": "~3.0.0" @@ -11718,18 +5853,26 @@ }, "node_modules/pull-through/node_modules/looper": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-3.0.0.tgz", + "integrity": "sha512-LJ9wplN/uSn72oJRsXTx+snxPet5c8XiZmOKCm906NVYu+ag6SB6vUcnJcWxgnl2NfbIyeobAn7Bwv6xRj2XJg==", "license": "MIT" }, "node_modules/pull-traverse": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pull-traverse/-/pull-traverse-1.0.3.tgz", + "integrity": "sha512-tdvTsDPh7PwuBEJ3o7DI6h/bQ5ocm9W0TYWNWEQ37ZS9Mt9aRfJqfvmAIdrpg3DgahU9PSITX0fYg8xTbqEdjQ==", "license": "MIT" }, "node_modules/pull-utf8-decoder": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pull-utf8-decoder/-/pull-utf8-decoder-1.0.2.tgz", + "integrity": "sha512-hpGheDlDMMEOkiBwXpiUqaFVR0SlGb23vZyJuHe1s8CWGJVcDEzhy9HHUEIuPjTEajYb90ikKsDmt6lkmTtk5g==", "license": "MIT" }, "node_modules/pull-websocket": { "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pull-websocket/-/pull-websocket-3.4.2.tgz", + "integrity": "sha512-hGFWC4/fzRdO2FEsyR9woVzgv/yG4PIk3RXPN4azBpomGzGQFRUORwKQDS3j7RAIy8tjvN2W+qjU8jNn2NWeNQ==", "license": "ISC", "dependencies": { "relative-url": "^1.0.2", @@ -11739,6 +5882,8 @@ }, "node_modules/pull-window": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/pull-window/-/pull-window-2.1.4.tgz", + "integrity": "sha512-cbDzN76BMlcGG46OImrgpkMf/VkCnupj8JhsrpBw3aWBM9ye345aYnqitmZCgauBkc0HbbRRn9hCnsa3k2FNUg==", "license": "MIT", "dependencies": { "looper": "^2.0.0" @@ -11746,40 +5891,20 @@ }, "node_modules/pull-window/node_modules/looper": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-2.0.0.tgz", + "integrity": "sha512-6DzMHJcjbQX/UPHc1rRCBfKlLwDkvuGZ715cIR36wSdYqWXFT35uLXq5P/2orl3tz+t+VOVPxw4yPinQlUDGDQ==", "license": "MIT" }, - "node_modules/pull-write": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "looper": "^4.0.0", - "pull-cat": "^1.1.11", - "pull-stream": "^3.4.5" - } - }, "node_modules/pull-write-file": { "version": "0.2.4", + "resolved": "https://registry.npmjs.org/pull-write-file/-/pull-write-file-0.2.4.tgz", + "integrity": "sha512-XqtjwmjObiJYj/ojJskRayIzlmP6ufzG6Ec5oSd/qhlUWr4zMsAcJoiSPjUi079QFqvlb54HaWCBkbpg2zZiKQ==", "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { "node": ">=6" @@ -11787,10 +5912,14 @@ }, "node_modules/push-stream": { "version": "11.2.0", + "resolved": "https://registry.npmjs.org/push-stream/-/push-stream-11.2.0.tgz", + "integrity": "sha512-MbiU+tFKDFv6IRwxfU2pltftFiIsW6mhLT3q91EJQwZYd2A9aaytdKAnRGYCx9KAuWQ11/cClK/FvHY996WwxA==", "license": "MIT" }, "node_modules/push-stream-to-pull-stream": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/push-stream-to-pull-stream/-/push-stream-to-pull-stream-1.0.5.tgz", + "integrity": "sha512-oQfzDroAv+SySQIXFiBVkShIh8Vgpr+hd7TrwyUna1kVrbv3i6D+QQC+31QdI7D6Jow61QLQW+uWToxv4cXI2w==", "license": "MIT", "dependencies": { "pull-looper": "^1.0.0", @@ -11799,6 +5928,8 @@ }, "node_modules/qrcode": { "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", "dependencies": { "dijkstrajs": "^1.0.1", @@ -11814,6 +5945,8 @@ }, "node_modules/qrcode/node_modules/camelcase": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { "node": ">=6" @@ -11821,6 +5954,8 @@ }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -11830,10 +5965,14 @@ }, "node_modules/qrcode/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/qrcode/node_modules/find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -11845,6 +5984,8 @@ }, "node_modules/qrcode/node_modules/locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -11855,6 +5996,8 @@ }, "node_modules/qrcode/node_modules/p-limit": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -11868,6 +6011,8 @@ }, "node_modules/qrcode/node_modules/p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -11878,6 +6023,8 @@ }, "node_modules/qrcode/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11890,6 +6037,8 @@ }, "node_modules/qrcode/node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -11902,10 +6051,14 @@ }, "node_modules/qrcode/node_modules/y18n": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, "node_modules/qrcode/node_modules/yargs": { "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "license": "MIT", "dependencies": { "cliui": "^6.0.0", @@ -11926,6 +6079,8 @@ }, "node_modules/qrcode/node_modules/yargs-parser": { "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -11952,28 +6107,26 @@ }, "node_modules/queue-tick": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", "license": "MIT" }, - "node_modules/quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/quicktask": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/quicktask/-/quicktask-1.0.1.tgz", + "integrity": "sha512-+jhR01aSsi6Iw9wbYumKusRK72QK1ub2oE0kOnRyMkMdD/rXMMGW/TVl5edjcLjuuIKP1ezkr+xQzUMD5/4JHw==", "license": "MIT" }, "node_modules/railroad-diagrams": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", "license": "CC0-1.0" }, "node_modules/randexp": { "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", "license": "MIT", "dependencies": { "discontinuous-range": "1.0.0", @@ -11985,6 +6138,8 @@ }, "node_modules/random-access-chrome-file": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/random-access-chrome-file/-/random-access-chrome-file-1.2.0.tgz", + "integrity": "sha512-M1NOdkHEcjRB+acKrdQkwf8aMTnZUIGboiH6i2PMNkjfChBIJiB4j4MuhpOn+u+XU2n7GqpocPN4bzfv0jrBsg==", "license": "MIT", "dependencies": { "random-access-storage": "^1.3.0" @@ -11992,6 +6147,8 @@ }, "node_modules/random-access-file": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-2.2.1.tgz", + "integrity": "sha512-RGU0xmDqdOyEiynob1KYSeh8+9c9Td1MJ74GT1viMEYAn8SJ9oBtWCXLsYZukCF46yududHOdM449uRYbzBrZQ==", "license": "MIT", "dependencies": { "mkdirp-classic": "^0.5.2", @@ -12000,6 +6157,8 @@ }, "node_modules/random-access-idb": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/random-access-idb/-/random-access-idb-1.2.2.tgz", + "integrity": "sha512-NroFuBNVh5wVIHKN/jEYrgkkffppkfxNWFX9OEwC2VP7dYc3sa+Qxv7tMa1Gi9Jp/ObVfLeCZBt/8Sbn1WU1Xg==", "license": "BSD", "dependencies": { "buffer-alloc": "^1.1.0", @@ -12012,6 +6171,8 @@ }, "node_modules/random-access-idb-mutable-file": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/random-access-idb-mutable-file/-/random-access-idb-mutable-file-0.3.0.tgz", + "integrity": "sha512-CdVAoFNNDn5uAgYOJ8J3ICSaFzaMOa95XnYcX+taj4jirJuRASiTyQSOGR+Z0K8ZkBGuj0A8ivyeRAWuxRCgQA==", "license": "MIT", "dependencies": { "buffer": "5.1.0", @@ -12020,6 +6181,8 @@ }, "node_modules/random-access-idb-mutable-file/node_modules/buffer": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", + "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", "license": "MIT", "dependencies": { "base64-js": "^1.0.2", @@ -12028,6 +6191,8 @@ }, "node_modules/random-access-idb-mutable-file/node_modules/random-access-storage": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-1.3.0.tgz", + "integrity": "sha512-pdS9Mcb9TB7oICypPRALlheaSuszuAKmLVEPKJMuYor7R/zDuHh5ALuQoS+ox31XRwQUL+tDwWH2GPdyspwelA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3" @@ -12035,10 +6200,14 @@ }, "node_modules/random-access-idb/node_modules/buffer-from": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", + "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==", "license": "MIT" }, "node_modules/random-access-memory": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/random-access-memory/-/random-access-memory-3.1.4.tgz", + "integrity": "sha512-rqgqd/8ec65gbpKaYHnDOW391OR39d+eXn8NI87G+f3sUKrtGib9jC+/5/9MBFBwwHAZIS8RLJ8yyB4etzbYTA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -12048,6 +6217,8 @@ }, "node_modules/random-access-storage": { "version": "1.4.3", + "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-1.4.3.tgz", + "integrity": "sha512-D5e2iIC5dNENWyBxsjhEnNOMCwZZ64TARK6dyMN+3g4OTC4MJxyjh9hKLjTGoNhDOPrgjI+YlFEHFnrp/cSnzQ==", "license": "MIT", "dependencies": { "events": "^3.3.0", @@ -12057,6 +6228,8 @@ }, "node_modules/random-access-web": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/random-access-web/-/random-access-web-2.0.3.tgz", + "integrity": "sha512-nN3AAgl4/lTOYMk5Qm44SzFsglOmaG2d0Kh0603umh35+rk9QXYLFf0nFJ0GOv9INBsP9iT1lub24r8PjyCtvA==", "license": "MIT", "dependencies": { "@sammacbeth/random-access-idb-mutable-file": "^0.1.1", @@ -12067,47 +6240,10 @@ "random-access-storage": "^1.3.0" } }, - "node_modules/randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "license": "MIT", - "dependencies": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/randomatic/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/randomatic/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/raw-body": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -12121,6 +6257,8 @@ }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -12131,6 +6269,8 @@ }, "node_modules/rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -12144,42 +6284,17 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/readable-stream": { "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -12194,6 +6309,8 @@ }, "node_modules/readable-web-to-node-stream": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", "license": "MIT", "dependencies": { "readable-stream": "^4.7.0" @@ -12208,6 +6325,8 @@ }, "node_modules/readdir-glob": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "license": "Apache-2.0", "dependencies": { "minimatch": "^5.1.0" @@ -12225,320 +6344,16 @@ "node": ">=10" } }, - "node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/readdirp/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", - "license": "MIT", - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/readdirp/node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "license": "MIT", - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "node_modules/real-cancellable-promise": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.3.tgz", + "integrity": "sha512-hBI5Gy/55VEeeMtImMgEirD7eq5UmqJf1J8dFZtbJZA/3rB0pYFZ7PayMGueb6v4UtUtpKpP+05L0VwyE1hI9Q==", "license": "MIT" }, - "node_modules/readdirp/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/readdirp/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/readdirp/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -12557,33 +6372,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "license": "MIT", - "dependencies": { - "is-equal-shallow": "^0.1.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -12602,182 +6394,14 @@ }, "node_modules/relative-url": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/relative-url/-/relative-url-1.0.2.tgz", + "integrity": "sha512-jI1AmBVFFMXwQ3I6tIYVmVOjy8f+ogHbqkeb8/LL9tszEQiTV8I0l8XT4oEomUOoxfm698f92gYlNKeA/9LJVQ==", "license": "MIT" }, - "node_modules/remark": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/remark/-/remark-3.2.3.tgz", - "integrity": "sha512-E+WibagMmLjs7y5d6qWRiwbL2/z0zbkLwCqvmNlxBTx2Y1ufZ06sir+ZdyQXTxDiEbkbsrX6kG883ItjrEkaSQ==", - "license": "MIT", - "dependencies": { - "camelcase": "^2.0.0", - "ccount": "^1.0.0", - "chalk": "^1.0.0", - "chokidar": "^1.0.5", - "collapse-white-space": "^1.0.0", - "commander": "^2.0.0", - "concat-stream": "^1.0.0", - "debug": "^2.0.0", - "elegant-spinner": "^1.0.0", - "extend.js": "0.0.2", - "glob": "^6.0.1", - "globby": "^4.0.0", - "he": "^0.5.0", - "log-update": "^1.0.1", - "longest-streak": "^1.0.0", - "markdown-table": "^0.4.0", - "minimatch": "^3.0.0", - "npm-prefix": "^1.0.1", - "parse-entities": "^1.0.0", - "repeat-string": "^1.5.0", - "stringify-entities": "^1.0.0", - "to-vfile": "^1.0.0", - "trim": "^0.0.1", - "trim-trailing-lines": "^1.0.0", - "unified": "^2.0.0", - "user-home": "^2.0.0", - "vfile": "^1.1.0", - "vfile-find-down": "^1.0.0", - "vfile-find-up": "^1.0.0", - "vfile-reporter": "^1.5.0", - "ware": "^1.3.0" - }, - "bin": { - "remark": "bin/remark" - } - }, - "node_modules/remark-html": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-2.0.2.tgz", - "integrity": "sha512-2yj8Oad44uo/IWOssZs3Y4AvfrA9nGZqiHsL7ePyZuntPKjtljHZDcroljlQQ+sJryO3Bjnnj30e7wi8aTp/hQ==", - "license": "MIT", - "dependencies": { - "collapse-white-space": "^1.0.0", - "detab": "^1.0.0", - "normalize-uri": "^1.0.0", - "object-assign": "^4.0.1", - "trim": "0.0.1", - "trim-lines": "^1.0.0", - "unist-util-visit": "^1.0.0" - } - }, - "node_modules/remark/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remark/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remark/node_modules/camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remark/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remark/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/remark/node_modules/glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/remark/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "license": "ISC" - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-finite": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12794,10 +6418,14 @@ }, "node_modules/require-main-filename": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, "node_modules/resolve": { "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -12816,6 +6444,8 @@ }, "node_modules/resolve-path": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", "license": "MIT", "dependencies": { "http-errors": "~1.6.2", @@ -12827,6 +6457,8 @@ }, "node_modules/resolve-path/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -12834,6 +6466,8 @@ }, "node_modules/resolve-path/node_modules/http-errors": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -12847,65 +6481,38 @@ }, "node_modules/resolve-path/node_modules/inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "license": "ISC" }, "node_modules/resolve-path/node_modules/setprototypeof": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", "license": "ISC" }, "node_modules/resolve-path/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "license": "MIT" - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ret": { "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "license": "MIT", "engines": { "node": ">=0.12" } }, - "node_modules/retry": { - "version": "0.12.0", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/right-pad": { - "version": "1.1.1", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/rimraf": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -12919,6 +6526,8 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -12937,59 +6546,20 @@ }, "node_modules/rng": { "version": "0.2.2", - "license": "MIT" - }, - "node_modules/router": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/run-series": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", - "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "resolved": "https://registry.npmjs.org/rng/-/rng-0.2.2.tgz", + "integrity": "sha512-3F/A3swXdqb4JLpF22zwlCllyXlUWEE3T35GzJ+Vvtx4HnUYVAXovaK2TyUC04UlpxdVxB+E7HaKdDrGeOcJuA==", "license": "MIT" }, "node_modules/rwlock": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rwlock/-/rwlock-5.0.0.tgz", + "integrity": "sha512-XgzRqLMfCcm9QfZuPav9cV3Xin5TRcIlp4X/SH3CvB+x5D2AakdlEepfJKDd8ByncvfpcxNWdRZVUl38PS6ZJg==", "license": "MIT" }, "node_modules/safe-array-concat": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -13007,6 +6577,8 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -13025,6 +6597,8 @@ }, "node_modules/safe-push-apply": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13037,17 +6611,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "license": "MIT", - "dependencies": { - "ret": "~0.1.10" - } - }, "node_modules/safe-regex-test": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13063,10 +6630,14 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", "license": "WTFPL OR ISC", "dependencies": { "truncate-utf8-bytes": "^1.0.0" @@ -13086,6 +6657,8 @@ }, "node_modules/secret-handshake": { "version": "1.1.21", + "resolved": "https://registry.npmjs.org/secret-handshake/-/secret-handshake-1.1.21.tgz", + "integrity": "sha512-e02+IddZv40tNJmaRZNZ6aYje95h9MvmpmbOg2PVjLKRA5p9QnrQypJlVE2dU516TyYc9jikBjqhXV1IlkwNUw==", "license": "MIT", "dependencies": { "chloride": "^2.2.8", @@ -13097,6 +6670,8 @@ }, "node_modules/secret-stack": { "version": "6.4.2", + "resolved": "https://registry.npmjs.org/secret-stack/-/secret-stack-6.4.2.tgz", + "integrity": "sha512-aUJNQ1DmZo0loxcnHOU7pvu9N2aOE0H63a1hpd77+I1GbmDBkK5hI9vO16IFf4OYDgPBiBIY3NGAHF23sdJC8g==", "license": "MIT", "dependencies": { "debug": "^4.1.0", @@ -13115,10 +6690,14 @@ }, "node_modules/secret-stack-decorators": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/secret-stack-decorators/-/secret-stack-decorators-1.1.0.tgz", + "integrity": "sha512-wYl0Mcul/fuEbZwn9tN62c+W4LP2RPus/ilt3wdBNQqfBFSMlDXTLaIsrA4SEElb7JEBH4xzdQbOCnTvYHeWCA==", "license": "MIT" }, "node_modules/secret-stack/node_modules/muxrpc": { "version": "6.7.3", + "resolved": "https://registry.npmjs.org/muxrpc/-/muxrpc-6.7.3.tgz", + "integrity": "sha512-yVr66BigJxHIvHOLsdJzuvrHt46Vq9DiDCBTZiS6q+49mXh4mkDjRiXo9FnGXQv1WJOqnRJk5fcfwV6TaSgNvA==", "license": "MIT", "dependencies": { "clarify-error": "^1.0.0", @@ -13141,87 +6720,22 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/fresh": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/separator-escape": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/separator-escape/-/separator-escape-0.0.1.tgz", + "integrity": "sha512-daCzTzZVoowYzjW7x9xMH6zr+lt/zsGxV1rtXaoTnlues7ZDx6Qu0l5W3jCdgnXGE1ONAGL+XPWY+IRDxnJ9EQ==", "license": "MIT" }, - "node_modules/serve-static": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static/node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, "node_modules/set-function-length": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -13237,6 +6751,8 @@ }, "node_modules/set-function-name": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -13250,6 +6766,8 @@ }, "node_modules/set-proto": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -13260,35 +6778,10 @@ "node": ">= 0.4" } }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/set.prototype.difference": { "version": "1.1.7", + "resolved": "https://registry.npmjs.org/set.prototype.difference/-/set.prototype.difference-1.1.7.tgz", + "integrity": "sha512-u6NOoxMovg0cqKd9wa5JoZ9F/5SF5wCdO0w3zKNjKps3VJ594mCQJDGXYtKaJUpwlIZFiZGEbtsNzkWlcr6GLA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -13309,6 +6802,8 @@ }, "node_modules/set.prototype.issubsetof": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/set.prototype.issubsetof/-/set.prototype.issubsetof-1.1.4.tgz", + "integrity": "sha512-m7paZD4cLQAXj5TLpdvpyX6gQ62onRV65zxjUH4qpZX5BouTBcL89X8BQ3QDhcFKwO2eZql8yTtNKsEmCOb1Tw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -13328,10 +6823,14 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/sha.js": { "version": "2.4.5", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.5.tgz", + "integrity": "sha512-459AP5kwdIhtMLLE1+h2q5a6E7hyY7Ri88GUIhFsyrQinKkm+7qj1ARHELow7GkFS7oimWmIwhXTSxBsuNuOsw==", "license": "MIT", "dependencies": { "inherits": "^2.0.1" @@ -13342,6 +6841,8 @@ }, "node_modules/sha256-universal": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sha256-universal/-/sha256-universal-1.2.1.tgz", + "integrity": "sha512-ghn3muhdn1ailCQqqceNxRgkOeZSVfSE13RQWEg6njB+itsFzGVSJv+O//2hvNXZuxVIRyNzrgsZ37SPDdGJJw==", "license": "ISC", "dependencies": { "b4a": "^1.0.1", @@ -13350,6 +6851,8 @@ }, "node_modules/sha256-wasm": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/sha256-wasm/-/sha256-wasm-2.2.2.tgz", + "integrity": "sha512-qKSGARvao+JQlFiA+sjJZhJ/61gmW/3aNLblB2rsgIxDlDxsJPHo8a1seXj12oKtuHVgJSJJ7QEGBUYQN741lQ==", "license": "ISC", "dependencies": { "b4a": "^1.0.1", @@ -13358,6 +6861,8 @@ }, "node_modules/sha512-universal": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sha512-universal/-/sha512-universal-1.2.1.tgz", + "integrity": "sha512-kehYuigMoRkIngCv7rhgruLJNNHDnitGTBdkcYbCbooL8Cidj/bS78MDxByIjcc69M915WxcQTgZetZ1JbeQTQ==", "license": "ISC", "dependencies": { "b4a": "^1.0.1", @@ -13366,6 +6871,8 @@ }, "node_modules/sha512-wasm": { "version": "2.3.4", + "resolved": "https://registry.npmjs.org/sha512-wasm/-/sha512-wasm-2.3.4.tgz", + "integrity": "sha512-akWoxJPGCB3aZCrZ+fm6VIFhJ/p8idBv7AWGFng/CZIrQo51oQNsvDbTSRXWAzIiZJvpy16oIDiCCPqTe21sKg==", "license": "ISC", "dependencies": { "b4a": "^1.0.1", @@ -13374,9 +6881,10 @@ }, "node_modules/sharp": { "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", @@ -13412,6 +6920,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -13422,19 +6932,17 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/shellsubstitute": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shellsubstitute/-/shellsubstitute-1.2.0.tgz", - "integrity": "sha512-CI1ViFC5a3ub86aaBmBVQ7kqg8eFypZLgBh+Bmq+ehHy9g7vu9kqCj5hS82cPzLwfdJRgiPB2hNHnd6oetiakQ==", - "license": "ISC" - }, "node_modules/side-channel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13452,6 +6960,8 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13466,6 +6976,8 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13482,6 +6994,8 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13499,6 +7013,8 @@ }, "node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -13507,70 +7023,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-git": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", - "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", - "license": "MIT", - "dependencies": { - "@kwsites/file-exists": "^1.1.1", - "@kwsites/promise-deferred": "^1.1.1", - "@simple-git/args-pathspec": "^1.0.3", - "@simple-git/argv-parser": "^1.1.0", - "debug": "^4.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/git-js?sponsor=1" - } - }, "node_modules/simple-swizzle": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" @@ -13578,190 +7034,42 @@ }, "node_modules/simple-swizzle/node_modules/is-arrayish": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/siphash24": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/siphash24/-/siphash24-1.3.1.tgz", + "integrity": "sha512-moemC3ZKiTzH29nbFo3Iw8fbemWWod4vNs/WgKbQ54oEs6mE6XVlguxvinYjB+UmaE0PThgyED9fUkWvirT8hA==", "license": "MIT", "dependencies": { "nanoassert": "^2.0.0" } }, - "node_modules/sleep-promise": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz", - "integrity": "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==", - "license": "MIT" - }, - "node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "license": "ISC", "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "*" } }, "node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "license": "MIT", - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "license": "MIT", - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/socks": { "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "license": "MIT", "dependencies": { "ip-address": "^9.0.5", @@ -13774,6 +7082,8 @@ }, "node_modules/sodium-browserify": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sodium-browserify/-/sodium-browserify-1.3.0.tgz", + "integrity": "sha512-1KRS6Oew3X13AIZhbmGF0YBdt2pQdafJMfv83OZHWbzxG92YBBnN8HYx/VKmYB4xCe90eidNaDJWBEFw/o3ahw==", "license": "MIT", "dependencies": { "libsodium-wrappers": "^0.7.4", @@ -13784,6 +7094,8 @@ }, "node_modules/sodium-browserify-tweetnacl": { "version": "0.2.6", + "resolved": "https://registry.npmjs.org/sodium-browserify-tweetnacl/-/sodium-browserify-tweetnacl-0.2.6.tgz", + "integrity": "sha512-ZnEI26hdluilpYY28Xc4rc1ALfmEp2TWihkJX6Mdtw0z9RfHfpZJU7P8DoKbN1HcBdU9aJmguFZs7igE8nLJPg==", "license": "MIT", "dependencies": { "chloride-test": "^1.1.0", @@ -13795,6 +7107,8 @@ }, "node_modules/sodium-browserify-tweetnacl/node_modules/sha.js": { "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.4", @@ -13813,14 +7127,20 @@ }, "node_modules/sodium-browserify-tweetnacl/node_modules/tweetnacl": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "license": "Unlicense" }, "node_modules/sodium-chloride": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sodium-chloride/-/sodium-chloride-1.1.2.tgz", + "integrity": "sha512-8AVzr9VHueXqfzfkzUA0aXe/Q4XG3UTmhlP6Pt+HQc5bbAPIJFo7ZIMh9tvn+99QuiMcyDJdYumegGAczl0N+g==", "license": "MIT" }, "node_modules/sodium-javascript": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/sodium-javascript/-/sodium-javascript-0.8.0.tgz", + "integrity": "sha512-rEBzR5mPxPES+UjyMDvKPIXy9ImF17KOJ32nJNi9uIquWpS/nfj+h6m05J5yLJaGXjgM72LmQoUbWZVxh/rmGg==", "license": "MIT", "dependencies": { "blake2b": "^2.1.1", @@ -13834,6 +7154,8 @@ }, "node_modules/sodium-native": { "version": "3.4.1", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.4.1.tgz", + "integrity": "sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -13842,6 +7164,8 @@ }, "node_modules/sodium-universal": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-3.1.0.tgz", + "integrity": "sha512-N2gxk68Kg2qZLSJ4h0NffEhp4BjgWHCHXVlDi1aG1hA3y+ZeWEmHqnpml8Hy47QzfL1xLy5nwr9LcsWAg2Ep0A==", "license": "MIT", "dependencies": { "blake2b": "^2.1.1", @@ -13856,15 +7180,6 @@ "xsalsa20": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -13874,88 +7189,16 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "license": "MIT", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "license": "MIT" - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/split-buffer": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/split-buffer/-/split-buffer-1.0.0.tgz", + "integrity": "sha512-cbHOF413jAsBL7JLpU1RWX07Xs+dPb25DbeCvwDzSN3+4bNmRPJa2QHpDdtREgGKgIfx6Tzj/jiqyOS3kNLPnQ==", "license": "MIT" }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ssb-about": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "ssb-social-index": "^1.0.0" - } - }, "node_modules/ssb-about-self": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-about-self/-/ssb-about-self-1.1.0.tgz", + "integrity": "sha512-AG07Dg2XG9CJvK52qrpwz54cXu+G6sPKwPiOb2LX5K+ORGhA+4MJUVViwuMEcOCI1a1wmXvOKFJbeTaFtosSAw==", "license": "LGPL-3.0", "dependencies": { "bipf": "^1.6.3", @@ -13974,24 +7217,18 @@ }, "node_modules/ssb-autofollow": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-autofollow/-/ssb-autofollow-1.1.0.tgz", + "integrity": "sha512-2YBiFhVB9vKSN4pxGg11xwGpP4eVRWf5H3YthKodIfP84/IB/sUNFa+Z7VuL7IAczB3v3DQSWSTc+nK4WOc7ww==", "license": "MIT", "dependencies": { "debug": "^3.1.0", "pull-stream": "^3.6.9" } }, - "node_modules/ssb-backlinks": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "base64-url": "^2.3.3", - "flumeview-links": "^2.1.0", - "pull-stream": "^3.6.14", - "ssb-ref": "^2.14.0" - } - }, "node_modules/ssb-bendy-butt": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ssb-bendy-butt/-/ssb-bendy-butt-1.0.2.tgz", + "integrity": "sha512-PGIfYKLcpyKAGO6AS3BHctjxL58xK/DN6qmDBYVlxxqT+DI+482XYBCNKGtYfnCUlyBK+qO7lmbJp/DQhR8jmQ==", "license": "LGPL-3.0", "dependencies": { "bencode": "^2.0.1", @@ -14002,6 +7239,8 @@ }, "node_modules/ssb-bfe": { "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ssb-bfe/-/ssb-bfe-3.7.0.tgz", + "integrity": "sha512-uJVMPAQhnm8Vz+5H7mP36kGhu0ZIloiDkNrkiO/xGWcr8gMv+GXX2DyNNPD7j0xZmL7HfFqgTvPXFeuni2w4IQ==", "license": "LGPL-3.0", "dependencies": { "is-canonical-base64": "^1.1.1", @@ -14012,10 +7251,14 @@ }, "node_modules/ssb-bfe-spec": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/ssb-bfe-spec/-/ssb-bfe-spec-0.8.0.tgz", + "integrity": "sha512-0NuyOS0CK1LAoJY+c3d9VmhGFuT1ylFKp+3T7Cttgq7LHhh0zMYZdzeJCUtTYgjsW/grvwRzUuNtYffJSYT0aA==", "license": "CC0-1.0" }, "node_modules/ssb-blobs": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ssb-blobs/-/ssb-blobs-2.0.1.tgz", + "integrity": "sha512-WuF96Yc8bd0l62wGxpQNT2OGf2XEhdMLEYsWJeXilYtEJLW+IZoO4YUjPanmp4fkhU2KunN+a/cQr6vqDqLphA==", "license": "MIT", "dependencies": { "level": "^6.0.0", @@ -14030,6 +7273,8 @@ }, "node_modules/ssb-box": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssb-box/-/ssb-box-1.0.1.tgz", + "integrity": "sha512-/lZOqFr5glmzKTmpXZ8/QbvYA6/mVQ6dDPT/+zsYQkKKEHaloMkMPRRXlK86xG+/Bsuw2HGjcm78W09aWshS+g==", "license": "LGPL-3.0", "dependencies": { "chloride": "^2.4.1", @@ -14043,6 +7288,8 @@ }, "node_modules/ssb-box2": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ssb-box2/-/ssb-box2-3.0.1.tgz", + "integrity": "sha512-jQhrsEyrpqnUciEb1qzqc/SJCAx3hTm48BMzMy1bE//xQthWRahigTffflOM7pdRieGlxwGlHc8PpagBBZOkhA==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.0", @@ -14056,6 +7303,8 @@ }, "node_modules/ssb-buttwoo": { "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ssb-buttwoo/-/ssb-buttwoo-0.3.3.tgz", + "integrity": "sha512-TA4xIs+4abz8Zn40LfrM2qM/hkpn5gYmy76yFGo9KkbDYoiVc5+Wk589yUf1ahcCzfDZN/lltRgBUzyeEY36uw==", "license": "LGPL-3.0", "dependencies": { "base64-url": "^2.3.3", @@ -14070,10 +7319,14 @@ }, "node_modules/ssb-caps": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-caps/-/ssb-caps-1.1.0.tgz", + "integrity": "sha512-qe3qpvchJ+gnH8M/ge4rpL+7eRbSmsEAzNwHkDdrW06OBcziQ6/KuAdmcR6joxCbNeoAXAZF+inkefgE16okXA==", "license": "CC0-1.0" }, "node_modules/ssb-classic": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-classic/-/ssb-classic-1.1.0.tgz", + "integrity": "sha512-aR1nifEXIpyNP+tRmQ8Sw2ra9nhPGrJnhH6xGTmSn4GXSwy5Gm4JCrpzcKvzUKzKMsfmqw5HVug+2/IVkvEVXQ==", "license": "LGPL-3.0", "dependencies": { "bipf": "^1.9.0", @@ -14087,6 +7340,8 @@ }, "node_modules/ssb-client": { "version": "4.9.0", + "resolved": "https://registry.npmjs.org/ssb-client/-/ssb-client-4.9.0.tgz", + "integrity": "sha512-5wPrVA82Uk3deV2oiCZEbm1kMp9r/5xTT2VvwwMLZS3fDD9FnLOi0kjUUh3OTP+jGBK7bVHRmnvXIVy6ETnSpA==", "license": "MIT", "dependencies": { "explain-error": "^1.0.1", @@ -14101,6 +7356,8 @@ }, "node_modules/ssb-client/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -14111,6 +7368,8 @@ }, "node_modules/ssb-client/node_modules/muxrpc": { "version": "6.7.3", + "resolved": "https://registry.npmjs.org/muxrpc/-/muxrpc-6.7.3.tgz", + "integrity": "sha512-yVr66BigJxHIvHOLsdJzuvrHt46Vq9DiDCBTZiS6q+49mXh4mkDjRiXo9FnGXQv1WJOqnRJk5fcfwV6TaSgNvA==", "license": "MIT", "dependencies": { "clarify-error": "^1.0.0", @@ -14123,6 +7382,8 @@ }, "node_modules/ssb-client/node_modules/ssb-keys": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-7.2.2.tgz", + "integrity": "sha512-FPeyYU/3LpxcagnbmVWE+Q/qzg6keqeOBPbD7sEH9UKixUASeufPKiORDgh8nVX7J9Z+0vUaHt/WG999kGjvVQ==", "license": "MIT", "dependencies": { "chloride": "^2.2.8", @@ -14132,6 +7393,8 @@ }, "node_modules/ssb-config": { "version": "3.4.6", + "resolved": "https://registry.npmjs.org/ssb-config/-/ssb-config-3.4.6.tgz", + "integrity": "sha512-arKSuP3S5qlsqY7w5g0VGCidi7oullM5KBEqy/0M9OgedN1uW5SKyIgEjCE85PbkHBfCpAcCNps4exZzP317FA==", "license": "MIT", "dependencies": { "deep-extend": "^0.6.0", @@ -14145,6 +7408,8 @@ }, "node_modules/ssb-conn": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ssb-conn/-/ssb-conn-6.0.3.tgz", + "integrity": "sha512-XPZgTlLlfetsYqRtJlO0+tUE2OBGFf7BrdUkNPmSbqXHNZK1NDFPgohMYevywBt08xdTWfbSyljIsZs1buId3g==", "license": "MIT", "dependencies": { "debug": "^4.3.1", @@ -14172,6 +7437,8 @@ }, "node_modules/ssb-conn-db": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ssb-conn-db/-/ssb-conn-db-1.0.5.tgz", + "integrity": "sha512-2rbqjvlDMTke2/qgauA415QSQ4hUJziXHqcXuC2Yh1uPrCDXHXrp0dhCchn0b/taEeL0Ppqo/Tsy6szQdqlEDw==", "license": "MIT", "dependencies": { "atomic-file-rw": "^0.3.0", @@ -14183,6 +7450,8 @@ }, "node_modules/ssb-conn-hub": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ssb-conn-hub/-/ssb-conn-hub-1.2.1.tgz", + "integrity": "sha512-EazMSgPJSOZTp/NdhUOq4kduAliT9k0GNJvao3Muvqk4Y17V4yIwCKt8SENHI5BMz2ATrDmFLj9uf5z/83Txww==", "license": "MIT", "dependencies": { "debug": "^4.3.1", @@ -14198,6 +7467,8 @@ }, "node_modules/ssb-conn-query": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ssb-conn-query/-/ssb-conn-query-1.2.2.tgz", + "integrity": "sha512-pvCU8oxpX9sBap6oBrLUwDGAYnHSxkhYAYnj6RxGNotPKDIRZUBZB04yQ4nA1rHm7oHOnBgcuK0GRWls78pnkQ==", "license": "MIT", "dependencies": { "ssb-conn-db": "~1.0.3", @@ -14207,6 +7478,8 @@ }, "node_modules/ssb-conn-staging": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-conn-staging/-/ssb-conn-staging-1.0.0.tgz", + "integrity": "sha512-NOy1qZoBkhH0XNzLteaaKKePXigEJSCntD4RPRU6vWLMlQ10+SzlIz4TWKgvEVx0LfCe6tEx/vhQf6vBWO/Ylw==", "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -14216,54 +7489,10 @@ "pull-stream": "^3.6.9" } }, - "node_modules/ssb-db": { - "version": "20.4.1", - "license": "MIT", - "dependencies": { - "flumedb": "^2.1.8", - "flumelog-offset": "^3.4.2", - "flumeview-level": "^4.0.4", - "flumeview-reduce": "^1.4.0", - "hashlru": "^2.3.0", - "lodash.clonedeep": "^4.5.0", - "ltgt": "^2.2.0", - "mkdirp": "^1.0.4", - "monotonic-timestamp": "~0.0.8", - "muxrpc-validation": "^3.0.0", - "obv": "0.0.1", - "pull-cat": "^1.1.11", - "pull-cont": "^0.1.1", - "pull-notify": "^0.1.1", - "pull-stream": "^3.4.0", - "rimraf": "^3.0.0", - "ssb-keys": "^7.1.3", - "ssb-msgs": "^5.0.0", - "ssb-ref": "^2.14.0", - "ssb-validate": "^4.0.0", - "zerr": "^1.0.0" - } - }, - "node_modules/ssb-db/node_modules/ssb-keys": { - "version": "7.2.2", - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "node_modules/ssb-db/node_modules/ssb-keys/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ssb-db2": { "version": "6.3.3", + "resolved": "https://registry.npmjs.org/ssb-db2/-/ssb-db2-6.3.3.tgz", + "integrity": "sha512-H5WztPw1lJmUR1e12McRABT4OStiwygDutirHhx1P520ssu5pTmuKrcUM/uSaEPbEPsLif17HpdKKd1fK2uxYg==", "license": "LGPL-3.0", "dependencies": { "async-append-only-log": "^4.3.7", @@ -14306,6 +7535,8 @@ }, "node_modules/ssb-db2/node_modules/flumecodec": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", + "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -14313,18 +7544,14 @@ }, "node_modules/ssb-db2/node_modules/flumecodec/node_modules/level-codec": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, - "node_modules/ssb-device-address": { - "version": "1.1.6", - "license": "MIT", - "dependencies": { - "pull-stream": "^3.6.9", - "ssb-ref": "^2.13.3" - } - }, "node_modules/ssb-ebt": { "version": "9.1.2", + "resolved": "https://registry.npmjs.org/ssb-ebt/-/ssb-ebt-9.1.2.tgz", + "integrity": "sha512-nNR26/3da8e+39fARLFDhrNdbkycxvmXqoK7QxrHGJaY/opBGtZ7GaHjNKkaXcF4Fl04ee+NhkYaiq18pGI0RQ==", "license": "MIT", "dependencies": { "base64-url": "^2.2.0", @@ -14343,56 +7570,10 @@ "node": ">=10" } }, - "node_modules/ssb-feed": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ssb-feed/-/ssb-feed-2.3.0.tgz", - "integrity": "sha512-PKdrDl+KpqpLmIAJA/l7+8njHsDkJ8siDnMDxcGJhr63uwdib74H9BMaV9qbW9kRI7S0UJHvuPcre0pB3tcxgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cont": "~1.0.3", - "monotonic-timestamp": "~0.0.9", - "pull-stream": "^3.4.2", - "ssb-keys": "^7.0.0", - "ssb-ref": "^2.0.0" - } - }, - "node_modules/ssb-feed/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-feed/node_modules/ssb-keys": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-7.2.2.tgz", - "integrity": "sha512-FPeyYU/3LpxcagnbmVWE+Q/qzg6keqeOBPbD7sEH9UKixUASeufPKiORDgh8nVX7J9Z+0vUaHt/WG999kGjvVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "node_modules/ssb-friend-pub": { - "version": "1.0.7", - "license": "Beerware", - "dependencies": { - "pull-stream": "^3.6.9", - "ssb-ref": "^2.13.9", - "ssb-sort": "^1.1.3" - } - }, "node_modules/ssb-friends": { "version": "5.1.7", + "resolved": "https://registry.npmjs.org/ssb-friends/-/ssb-friends-5.1.7.tgz", + "integrity": "sha512-oBoMmcw5URa16CXM7LV1YG+YAGdwsQHkHevkh1VVKqhxxi8UAQ1qseb0iQ9Xdzwgadh9lRaW/aGz+175xrcxig==", "license": "MIT", "dependencies": { "bipf": "^1.5.1", @@ -14414,6 +7595,8 @@ }, "node_modules/ssb-friends/node_modules/flumecodec": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", + "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -14421,43 +7604,14 @@ }, "node_modules/ssb-friends/node_modules/level-codec": { "version": "6.2.0", - "license": "MIT" - }, - "node_modules/ssb-generate": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ssb-generate/-/ssb-generate-1.0.1.tgz", - "integrity": "sha512-Ube4EVI2QCHLwtxPwi3julUGW1j45oZFCA/keST5JtWLAmt7Tegq7VWwlTHxXRInTZKcT8Q6AJF+KtipR+FC1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "pull-paramap": "^1.2.2", - "pull-stream": "^3.6.0" - } - }, - "node_modules/ssb-gossip": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "atomic-file": "^1.1.5", - "deep-equal": "^1.0.1", - "has-network": "0.0.1", - "ip": "^1.1.5", - "muxrpc-validation": "^3.0.0", - "on-change-network": "0.0.2", - "on-wakeup": "^1.0.1", - "pull-notify": "^0.1.1", - "pull-ping": "^2.0.2", - "pull-stream": "^3.6.9", - "ssb-ref": "^2.13.9", - "statistics": "^3.3.0" - } - }, - "node_modules/ssb-gossip/node_modules/atomic-file": { - "version": "1.1.5", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, "node_modules/ssb-index-feeds": { "version": "0.10.2", + "resolved": "https://registry.npmjs.org/ssb-index-feeds/-/ssb-index-feeds-0.10.2.tgz", + "integrity": "sha512-j4j24OeD5CMGWcfjlk9ZUiQda6Obx/1z4X/SjYmBJBsX6mjVLJ0Kz0MbO607oVCls7AXdlgBCs6UsCGoO/VJiA==", "license": "LGPL-3.0", "dependencies": { "debug": "^4.3.2", @@ -14474,6 +7628,8 @@ }, "node_modules/ssb-invite": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ssb-invite/-/ssb-invite-3.0.3.tgz", + "integrity": "sha512-zf0i9SL1phZJjXQE0poeguw+WOl3YdHV+Ajqnwka2AbDXaounA9RpoSl/SsxgLnCF/p8lOhNGMGU5Nkm/t7gVA==", "license": "MIT", "dependencies": { "cont": "^1.0.3", @@ -14488,6 +7644,8 @@ }, "node_modules/ssb-invite-client": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/ssb-invite-client/-/ssb-invite-client-1.3.3.tgz", + "integrity": "sha512-JBj5GxtWAquFp9L4TISSmCq1bsTEwpp5KUeALFuRBWuG+y6Nib55qaLJz8Q6r+HbxiVz3Soq68RaG5uW9FCcrQ==", "license": "MIT", "dependencies": { "clarify-error": "^1.0.0", @@ -14501,6 +7659,8 @@ }, "node_modules/ssb-keyring": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ssb-keyring/-/ssb-keyring-2.2.0.tgz", + "integrity": "sha512-pXNzYsQbtdWoWAjEPT5lVrZs/Px4ypJU2m4S34tTN6KivZukHPcflj1/IbZSDj7kHJGQhJRcyqGQ3MxgQdFBrA==", "license": "LGPL-3.0", "dependencies": { "charwise": "^3.0.1", @@ -14516,6 +7676,8 @@ }, "node_modules/ssb-keyring/node_modules/ssb-private-group-keys": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-1.1.2.tgz", + "integrity": "sha512-0UPPmxy61qmbDmP71J2vhX6UPfCtXa/CNehxYTgk2+AaLXsnA0perGZAiOWm9niGEU50TYYC5/jsIfjz4IiD9A==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -14527,6 +7689,8 @@ }, "node_modules/ssb-keys": { "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-8.5.0.tgz", + "integrity": "sha512-Fdgnz5QQ/oK/bMf5Hkaqss/INqiKHFHG4RKDk3StWrQC4fUKMSL/GfnkxFlcLFGhVomzxUmhtAnhJQx2WecEKQ==", "license": "MIT", "dependencies": { "chloride": "~2.4.1", @@ -14540,6 +7704,8 @@ }, "node_modules/ssb-keys/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -14550,6 +7716,8 @@ }, "node_modules/ssb-lan": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/ssb-lan/-/ssb-lan-1.2.3.tgz", + "integrity": "sha512-HJdc9oLIYO4u2eE9F3w8aXDP+idUiJ+ODcJKCKY8VKAlyn8psP4TC+Ol9VpT8NzFdOKQ5LnaAW9yThawfi2VUg==", "license": "MIT", "dependencies": { "debug": "^4.3.1", @@ -14565,237 +7733,19 @@ "secret-stack": ">=6.2.0" } }, - "node_modules/ssb-legacy-conn": { - "version": "1.0.25", - "license": "MIT", - "dependencies": { - "debug": "~4.1.1", - "has-network": "0.0.1", - "ip": "^1.1.5", - "multiserver-address": "~1.0.1", - "on-change-network": "0.0.2", - "on-wakeup": "^1.0.1", - "pull-notify": "^0.1.1", - "pull-ping": "^2.0.2", - "pull-stream": "^3.6.9", - "ssb-conn-db": "0.1.0", - "ssb-conn-hub": "0.0.3", - "ssb-conn-staging": "0.0.1", - "ssb-ref": "^2.13.9", - "statistics": "^3.3.0" - } - }, - "node_modules/ssb-legacy-conn/node_modules/atomic-file": { - "version": "1.1.5", - "license": "MIT" - }, - "node_modules/ssb-legacy-conn/node_modules/ssb-conn-db": { - "version": "0.1.0", - "license": "MIT", - "dependencies": { - "atomic-file": "^1.1.5", - "debug": "~4.1.1", - "multiserver-address": "~1.0.1", - "pull-notify": "~0.1.1", - "ssb-ref": "~2.13.9" - } - }, - "node_modules/ssb-legacy-conn/node_modules/ssb-conn-hub": { - "version": "0.0.3", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "multiserver-address": "~1.0.1", - "promisify-tuple": "^1.0.0", - "pull-notify": "~0.1.1", - "ssb-ref": "~2.13.9" - } - }, - "node_modules/ssb-legacy-conn/node_modules/ssb-conn-staging": { - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "multiserver-address": "~1.0.1", - "pull-notify": "~0.1.1", - "pull-stream": "^3.6.9", - "ssb-conn-hub": "~0.0.3" - } - }, - "node_modules/ssb-links": { - "version": "3.0.10", - "license": "MIT", - "dependencies": { - "flumeview-links": "^1.0.1", - "map-filter-reduce": "^2.0.0", - "ssb-msgs": "^5.2.0" - } - }, - "node_modules/ssb-links/node_modules/abstract-leveldown": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "level-concat-iterator": "~2.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ssb-links/node_modules/deep-equal": { - "version": "2.2.3", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ssb-links/node_modules/flumeview-level": { - "version": "3.0.14", - "license": "MIT", - "dependencies": { - "charwise": "^3.0.1", - "explain-error": "^1.0.4", - "level": "^5.0.0", - "ltgt": "^2.1.3", - "mkdirp": "^0.5.1", - "obv": "0.0.1", - "pull-level": "^2.0.3", - "pull-paramap": "^1.2.1", - "pull-stream": "^3.5.0", - "pull-write": "^1.1.1" - } - }, - "node_modules/ssb-links/node_modules/flumeview-links": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "deep-equal": "^2.0.3", - "flumeview-level": "^3.0.14", - "map-filter-reduce": "^3.2.2", - "pull-flatmap": "0.0.1", - "pull-stream": "^3.6.14" - } - }, - "node_modules/ssb-links/node_modules/flumeview-links/node_modules/map-filter-reduce": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "binary-search": "^1.2.0", - "compare-at-paths": "^1.0.0", - "pull-sink-through": "0.0.0", - "pull-sort": "^1.0.1", - "pull-stream": "^3.4.3", - "typewiselite": "^1.0.0" - } - }, - "node_modules/ssb-links/node_modules/immediate": { - "version": "3.2.3", - "license": "MIT" - }, - "node_modules/ssb-links/node_modules/level": { - "version": "5.0.1", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "level-js": "^4.0.0", - "level-packager": "^5.0.0", - "leveldown": "^5.0.0", - "opencollective-postinstall": "^2.0.0" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/ssb-links/node_modules/level-js": { - "version": "4.0.2", - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~6.0.1", - "immediate": "~3.2.3", - "inherits": "^2.0.3", - "ltgt": "^2.1.2", - "typedarray-to-buffer": "~3.1.5" - } - }, - "node_modules/ssb-links/node_modules/map-filter-reduce": { - "version": "2.2.1", - "license": "MIT", - "dependencies": { - "binary-search": "^1.2.0", - "pull-sink-through": "0.0.0", - "pull-stream": "^3.3.0", - "typewiselite": "^1.0.0" - } - }, - "node_modules/ssb-links/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-links/node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/ssb-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ssb-local/-/ssb-local-1.0.0.tgz", - "integrity": "sha512-aIkz+tDsAQFLbcN5d9itipHG71EG62UdIwojz78BQBxD7kGTwMezZofkMplURp9/njwdqMiqxYEUMvOh8Emh/w==", - "license": "MIT", - "dependencies": { - "broadcast-stream": "^0.2.2", - "ssb-ref": "^2.13.9" - } - }, "node_modules/ssb-logging": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-logging/-/ssb-logging-1.0.0.tgz", + "integrity": "sha512-6apTG47+VgLAD3MYkpRTbO27DMDh0YLJIvWfcJUEo5FdrjnLsqDl1kkzQ4B5MbH08z5Vklx5907t4by9rhlROQ==", "license": "MIT", "dependencies": { "bash-color": "0.0.4" } }, - "node_modules/ssb-markdown": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "emoji-named-characters": "^1.0.2", - "ssb-marked": "^0.7.3", - "ssb-msgs": "^5.2.0", - "ssb-ref": "^2.3.0" - } - }, "node_modules/ssb-marked": { "version": "0.7.4", + "resolved": "https://registry.npmjs.org/ssb-marked/-/ssb-marked-0.7.4.tgz", + "integrity": "sha512-7Dnl4iTmugLXchTXN5mVi1t9in/9GsKYMD9Pm0o1JKGftyWP9ztSTl6Lukix6FoNRfLJRhjGzfXj+Aw27TN4pQ==", "license": "MIT", "bin": { "ssb-marked": "bin/marked" @@ -14803,19 +7753,14 @@ }, "node_modules/ssb-master": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ssb-master/-/ssb-master-1.0.3.tgz", + "integrity": "sha512-N1Cxm9WscGD9VEZrWbF2amyQai2U2g9gtq57W5zTqbhlQTLUUvl84U9A6fg6GPkECnUXadulnTw+mMYVkLLHjQ==", "license": "MIT" }, - "node_modules/ssb-meme": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "flumeview-search": "^2.0.0", - "is-my-json-valid": "^2.17.2", - "ssb-ref": "^2.11.1" - } - }, "node_modules/ssb-mentions": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/ssb-mentions/-/ssb-mentions-0.5.2.tgz", + "integrity": "sha512-+1lslVCkfnmJOlswJluia+70K5tTLzwRxbLmluvV0rf2KW5T4DIXBPuAhB3vBsP26emAcTHEOj7QtD8iQ9VWtA==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0", @@ -14825,6 +7770,8 @@ }, "node_modules/ssb-meta-feeds": { "version": "0.39.0", + "resolved": "https://registry.npmjs.org/ssb-meta-feeds/-/ssb-meta-feeds-0.39.0.tgz", + "integrity": "sha512-vnfGA5FT0iHVMTE3Ore10qEi2Bpi2TqIiD4FvHbU+rdBOJyKKE0E3KwDFWhlqGDB5YwSt9YP364WbEjn1VTLQw==", "license": "LGPL-3.0", "dependencies": { "bencode": "^2.0.2", @@ -14850,10 +7797,14 @@ }, "node_modules/ssb-msg-content": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssb-msg-content/-/ssb-msg-content-1.0.1.tgz", + "integrity": "sha512-M6W0Ef+jif829USmGvh6XeS4lYb/F2lgFhfEoCE/md7ESILNOGidp8frJE2uVOzSr2wVRA265tPrnVb7rYHkug==", "license": "MIT" }, "node_modules/ssb-msgs": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ssb-msgs/-/ssb-msgs-5.2.0.tgz", + "integrity": "sha512-a5SF3fnXhXhcEIT406QNPftf+9YbPWFYFABNudreB19RrE0N3JsJL7bm/eKWgVd0fXNpARZJF3f5BYYMUv6lFw==", "license": "MIT", "dependencies": { "ssb-ref": "^2.0.0" @@ -14861,10 +7812,14 @@ }, "node_modules/ssb-network-errors": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssb-network-errors/-/ssb-network-errors-1.0.1.tgz", + "integrity": "sha512-Re6q7yZL4GreJXbfa7QWk24Nr/I7bs3cIszV2usiSqVYyDphkcwT8iOXFyqjWxG9gVA3Lfjj+6uwX2WI7SorPw==", "license": "LGPL-3.0" }, "node_modules/ssb-no-auth": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-no-auth/-/ssb-no-auth-1.0.0.tgz", + "integrity": "sha512-+o9WnY566shLyiJp6l21+SHZfgv40SEr35PtoF3uuFYI24vFhgcfv77pXvWpDINVyX2uQ152o2eQHg1amlp6lQ==", "license": "MIT", "dependencies": { "multiserver": "^3.3.3" @@ -14872,183 +7827,17 @@ }, "node_modules/ssb-onion": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-onion/-/ssb-onion-1.0.0.tgz", + "integrity": "sha512-iRXjeI0sbRbgMwaKg+poqhHSH7GvUVgJ+8ysi8q7Hjj4wHkczLGAOyVjnyhjF+ErPDFn9kXN+46joneKqSgEDg==", "license": "MIT", "dependencies": { "multiserver": "^3.3.3" } }, - "node_modules/ssb-ooo": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/ssb-ooo/-/ssb-ooo-1.3.3.tgz", - "integrity": "sha512-6yI5XBpBHkwFdAELpKwlf1C1V32Z0/AKEJYsoU7lk+Eh88blv8LKydo4a1DnjMiPy4ywn4lRU5oayQaPE5MRtQ==", - "license": "MIT", - "dependencies": { - "flumecodec": "0.0.1", - "flumelog-offset": "^3.4.3", - "flumeview-hashtable": "^1.1.1", - "gossip-query": "^2.0.2", - "mkdirp": "^0.5.1", - "pull-stream": "^3.6.14", - "ssb-keys": "^7.2.0", - "ssb-ref": "^2.13.3", - "ssb-validate": "^4.1.0" - } - }, - "node_modules/ssb-ooo/node_modules/flumecodec": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", - "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", - "license": "MIT", - "dependencies": { - "level-codec": "^6.2.0" - } - }, - "node_modules/ssb-ooo/node_modules/level-codec": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", - "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", - "deprecated": "Superseded by level-transcoder (https://github.com/Level/community#faq)", - "license": "MIT" - }, - "node_modules/ssb-ooo/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-ooo/node_modules/ssb-keys": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-7.2.2.tgz", - "integrity": "sha512-FPeyYU/3LpxcagnbmVWE+Q/qzg6keqeOBPbD7sEH9UKixUASeufPKiORDgh8nVX7J9Z+0vUaHt/WG999kGjvVQ==", - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "node_modules/ssb-partial-replication": { - "version": "3.0.1", - "license": "Beerware", - "dependencies": { - "pull-cont": "^0.1.1", - "pull-stream": "^3.6.14", - "ssb-sort": "^1.1.3" - } - }, - "node_modules/ssb-plugins": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.5", - "explain-error": "^1.0.4", - "mkdirp": "^0.5.1", - "muxrpc": "^6.4.6", - "muxrpc-validation": "^3.0.0", - "mv": "^2.1.1", - "osenv": "^0.1.5", - "pull-cat": "^1.1.11", - "pull-many": "^1.0.8", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.6.9", - "stream-to-pull-stream": "^1.7.2" - } - }, - "node_modules/ssb-plugins/node_modules/cross-spawn": { - "version": "6.0.6", - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/ssb-plugins/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-plugins/node_modules/muxrpc": { - "version": "6.7.3", - "license": "MIT", - "dependencies": { - "clarify-error": "^1.0.0", - "debug": "^4.3.3", - "packet-stream": "~2.0.0", - "packet-stream-codec": "^1.2.0", - "pull-goodbye": "0.0.3", - "pull-stream": "^3.6.10" - } - }, - "node_modules/ssb-plugins/node_modules/path-key": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ssb-plugins/node_modules/semver": { - "version": "5.7.2", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/ssb-plugins/node_modules/shebang-command": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ssb-plugins/node_modules/shebang-regex": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ssb-plugins/node_modules/which": { - "version": "1.3.1", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/ssb-private": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "base64-url": "^2.2.0", - "explain-error": "^1.0.4", - "flumeview-links": "^2.0.1", - "ssb-keys": "^7.0.14" - } - }, "node_modules/ssb-private-group-keys": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-0.4.1.tgz", + "integrity": "sha512-Npl9NpUZvHho10h18/tphMfuyMlj+6lLW0mm+u8ooIRgVh5+hyoOtgJL22VmTEyD8KQVJG6SOjiv6MiGXzIL3g==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.1.2", @@ -15058,36 +7847,10 @@ "ssb-bfe": "^3.1.1" } }, - "node_modules/ssb-private/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-private/node_modules/ssb-keys": { - "version": "7.2.2", - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "node_modules/ssb-query": { - "version": "2.4.5", - "license": "MIT", - "dependencies": { - "explain-error": "^1.0.1", - "flumeview-query": "^8.0.0", - "pull-stream": "^3.6.2" - } - }, "node_modules/ssb-ref": { "version": "2.16.0", + "resolved": "https://registry.npmjs.org/ssb-ref/-/ssb-ref-2.16.0.tgz", + "integrity": "sha512-ylyrfz9NLxwTCbeDDAdLo++O3elhNs6/gUqMhZ22F+gSOIjwXy2X7dpg5Q1YTH7uALOSu307Rpo1UfK9sj7Sjw==", "license": "MIT", "dependencies": { "ip": "^1.1.3", @@ -15098,62 +7861,17 @@ }, "node_modules/ssb-ref/node_modules/is-valid-domain": { "version": "0.0.20", + "resolved": "https://registry.npmjs.org/is-valid-domain/-/is-valid-domain-0.0.20.tgz", + "integrity": "sha512-Yd9oD7sgCycVvH8CHy5U4fLXibPwxVw2+diudYbT8ZfAiQDtW1H9WvPRR4+rtN9qOll+r+KAfO4SjO28OPpitA==", "license": "MIT", "dependencies": { "punycode": "^1.4.1" } }, - "node_modules/ssb-replicate": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ssb-replicate/-/ssb-replicate-1.3.5.tgz", - "integrity": "sha512-XJcsI8xMEpkEF4VF3eUd0edkakBEokakmIPLk2kR6ryNNYo72M+WPB6q+t2JSNkpGIaKFBJTCEMTkNJnLrM9EQ==", - "license": "MIT", - "dependencies": { - "deep-equal": "^2.2.3", - "observ-debounce": "^1.1.1", - "pull-cat": "^1.1.11", - "pull-next": "^1.0.1", - "pull-notify": "^0.1.2", - "pull-paramap": "^1.2.2", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.7.0", - "ssb-ref": "^2.16.0" - } - }, - "node_modules/ssb-replicate/node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ssb-replication-scheduler": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/ssb-replication-scheduler/-/ssb-replication-scheduler-3.1.1.tgz", + "integrity": "sha512-mLGSs8RjY9LpyVa4uJ+FIFvlqv6A4aBb3YAKHziAY8IRhkbIQmS/HbHf5p2IKBSf9wo0/Ac+EzPDQdeO8hCCpA==", "license": "LGPL-3.0", "dependencies": { "debug": "^4.3.2", @@ -15174,6 +7892,8 @@ }, "node_modules/ssb-replication-scheduler/node_modules/ssb-box2": { "version": "7.5.0", + "resolved": "https://registry.npmjs.org/ssb-box2/-/ssb-box2-7.5.0.tgz", + "integrity": "sha512-cYhq4CdjHXlQCQZqqzbkBygD9Hyf5C86i4xHHNqoMW1FPS20m0uEjwTTpMFJOZfhXs/wgeCuAJ1h4UG1Es+slg==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -15188,6 +7908,8 @@ }, "node_modules/ssb-replication-scheduler/node_modules/ssb-keyring": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ssb-keyring/-/ssb-keyring-7.0.0.tgz", + "integrity": "sha512-HBeAFCdjqSLCiors10s+AHfaFm78ac97oqeWgDg9Es4gK2LUs/ZQ1DjoYH+BS4EnTh5yE+Y9E8ska4WWc0p4Dw==", "license": "LGPL-3.0", "dependencies": { "charwise": "^3.0.1", @@ -15204,6 +7926,8 @@ }, "node_modules/ssb-replication-scheduler/node_modules/ssb-keyring/node_modules/private-group-spec": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-8.1.0.tgz", + "integrity": "sha512-vxChFtmkWW85VJjh3VmByB1zeOp40JZmJ+0YBCibxlxBHuaW5aFa6BZ/zamkH2sBtXAjwZnou4f5Ty7CUj3NvA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -15211,6 +7935,8 @@ }, "node_modules/ssb-replication-scheduler/node_modules/ssb-private-group-keys": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-1.1.2.tgz", + "integrity": "sha512-0UPPmxy61qmbDmP71J2vhX6UPfCtXa/CNehxYTgk2+AaLXsnA0perGZAiOWm9niGEU50TYYC5/jsIfjz4IiD9A==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -15222,37 +7948,17 @@ }, "node_modules/ssb-schema-definitions": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ssb-schema-definitions/-/ssb-schema-definitions-1.3.0.tgz", + "integrity": "sha512-UUNBnXpaISaM3djBDH8HB5RZtXBS+/ThehppCLQ6LJru7IMubu/hOPbD3EYahfidw13pvdCQXSuGkIeKIcOv8g==", "license": "MIT", "dependencies": { "ssb-ref": "^2.13.6" } }, - "node_modules/ssb-search": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "flumeview-search": "^2.0.0", - "pull-cont": "^0.1.1", - "pull-stream": "^3.6.7", - "ssb-msgs": "^5.2.0" - } - }, - "node_modules/ssb-server": { - "resolved": "packages/ssb-server", - "link": true - }, - "node_modules/ssb-social-index": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "bipf": "^1.4.0", - "pull-defer": "^0.2.3", - "pull-stream": "^3.6.9", - "ssb-ref": "^2.7.1" - } - }, "node_modules/ssb-sort": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ssb-sort/-/ssb-sort-1.1.3.tgz", + "integrity": "sha512-oPsF8lGgcHcIb4F1GddV3CbZTJZ0OzxI9fHXH0Zc7ZjqjFlYdqMDxFSuvqJnmtDydJcswyGANiziP1ghd69jOw==", "license": "MIT", "dependencies": { "ssb-ref": "^2.3.0" @@ -15260,6 +7966,8 @@ }, "node_modules/ssb-subset-ql": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssb-subset-ql/-/ssb-subset-ql-1.0.1.tgz", + "integrity": "sha512-msZA4Oc1rgEEAVLyzMiQNxJf85+XBrw7IEQLHQpMDv+o/p4GGX9Ygijc2RtItnWSu1H5fFcZChL8pf/q+j3q+w==", "license": "LGPL-3.0", "dependencies": { "nano-equal": "^2.0.2", @@ -15267,18 +7975,10 @@ "ssb-ref": "^2.13.0" } }, - "node_modules/ssb-tangle": { - "version": "1.0.1", - "license": "AGPL-3.0", - "dependencies": { - "is-my-json-valid": "^2.20.0", - "pull-stream": "^3.6.11", - "ssb-ref": "^2.13.9", - "ssb-sort": "^1.1.3" - } - }, "node_modules/ssb-thread-schema": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ssb-thread-schema/-/ssb-thread-schema-1.1.1.tgz", + "integrity": "sha512-93RcHfnev5vj7J5UZI47lu7l71bCevUdO80xUDZSgIPGQw+FrHo0LGSxDTfzNngJaJTlat5mEkRwrrY+wYoahg==", "license": "AGPL-3.0", "dependencies": { "is-my-json-valid": "^2.20.0", @@ -15286,26 +7986,10 @@ "ssb-schema-definitions": "^1.1.5" } }, - "node_modules/ssb-threads": { - "version": "10.8.0", - "license": "MIT", - "dependencies": { - "bipf": "^1.9.0", - "pull-cat": "^1.1.11", - "pull-level": "^2.0.4", - "pull-stream": "^3.6.2", - "secret-stack-decorators": "1.1.0", - "ssb-db2": ">=3.4.1", - "ssb-ref": "^2.13.0", - "ssb-sort": "1.1.x", - "ssb-typescript": "^2.5.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ssb-tribes2": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ssb-tribes2/-/ssb-tribes2-1.4.0.tgz", + "integrity": "sha512-3Cv1HNUheBtTgfi4CobjJLd5s9xcz4d1pgNcyEUPK+mQOnD185ZkMHa/+RIbOjTSBDTlGgjt50Ymixbi+LpJtQ==", "license": "LGPL-3.0-only", "dependencies": { "@tangle/overwrite-fields": "^2.0.3", @@ -15345,6 +8029,8 @@ }, "node_modules/ssb-tribes2/node_modules/flumecodec": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", + "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -15352,10 +8038,14 @@ }, "node_modules/ssb-tribes2/node_modules/flumecodec/node_modules/level-codec": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, "node_modules/ssb-tribes2/node_modules/private-group-spec": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-8.1.0.tgz", + "integrity": "sha512-vxChFtmkWW85VJjh3VmByB1zeOp40JZmJ+0YBCibxlxBHuaW5aFa6BZ/zamkH2sBtXAjwZnou4f5Ty7CUj3NvA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -15363,6 +8053,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-box2": { "version": "7.5.0", + "resolved": "https://registry.npmjs.org/ssb-box2/-/ssb-box2-7.5.0.tgz", + "integrity": "sha512-cYhq4CdjHXlQCQZqqzbkBygD9Hyf5C86i4xHHNqoMW1FPS20m0uEjwTTpMFJOZfhXs/wgeCuAJ1h4UG1Es+slg==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -15377,6 +8069,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ssb-db2/-/ssb-db2-7.2.0.tgz", + "integrity": "sha512-XxqzmccNgE2YmUZ+lXhw08BVlLEGA8LnIXBeeem4+084/fOPdBdGbXy8rb1G3UoiKVYFLVBc64LhbRQ/JfaS6w==", "license": "LGPL-3.0-only", "dependencies": { "async-append-only-log": "^4.3.7", @@ -15421,6 +8115,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2/node_modules/private-group-spec": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-1.2.0.tgz", + "integrity": "sha512-O7SfG+vZIZgqDXy/wjsuTRI5LaozW4rxaZBpGmwlcDfjIvxvYWNboyNm1PoQUU6j4dQ02V1tOQVLDq9u2RzolA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -15428,6 +8124,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2/node_modules/ssb-box2": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ssb-box2/-/ssb-box2-3.0.1.tgz", + "integrity": "sha512-jQhrsEyrpqnUciEb1qzqc/SJCAx3hTm48BMzMy1bE//xQthWRahigTffflOM7pdRieGlxwGlHc8PpagBBZOkhA==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.0", @@ -15441,6 +8139,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2/node_modules/ssb-box2/node_modules/ssb-private-group-keys": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-0.4.1.tgz", + "integrity": "sha512-Npl9NpUZvHho10h18/tphMfuyMlj+6lLW0mm+u8ooIRgVh5+hyoOtgJL22VmTEyD8KQVJG6SOjiv6MiGXzIL3g==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.1.2", @@ -15452,6 +8152,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2/node_modules/ssb-keyring": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ssb-keyring/-/ssb-keyring-2.2.0.tgz", + "integrity": "sha512-pXNzYsQbtdWoWAjEPT5lVrZs/Px4ypJU2m4S34tTN6KivZukHPcflj1/IbZSDj7kHJGQhJRcyqGQ3MxgQdFBrA==", "license": "LGPL-3.0", "dependencies": { "charwise": "^3.0.1", @@ -15467,6 +8169,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-keyring": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ssb-keyring/-/ssb-keyring-7.0.0.tgz", + "integrity": "sha512-HBeAFCdjqSLCiors10s+AHfaFm78ac97oqeWgDg9Es4gK2LUs/ZQ1DjoYH+BS4EnTh5yE+Y9E8ska4WWc0p4Dw==", "license": "LGPL-3.0", "dependencies": { "charwise": "^3.0.1", @@ -15483,6 +8187,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-private-group-keys": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-1.1.2.tgz", + "integrity": "sha512-0UPPmxy61qmbDmP71J2vhX6UPfCtXa/CNehxYTgk2+AaLXsnA0perGZAiOWm9niGEU50TYYC5/jsIfjz4IiD9A==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -15494,6 +8200,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-private-group-keys/node_modules/private-group-spec": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-1.2.0.tgz", + "integrity": "sha512-O7SfG+vZIZgqDXy/wjsuTRI5LaozW4rxaZBpGmwlcDfjIvxvYWNboyNm1PoQUU6j4dQ02V1tOQVLDq9u2RzolA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -15501,10 +8209,14 @@ }, "node_modules/ssb-typescript": { "version": "2.8.0", + "resolved": "https://registry.npmjs.org/ssb-typescript/-/ssb-typescript-2.8.0.tgz", + "integrity": "sha512-akqVsc2HNM0x5q4iN8v/t0jDuNOyU8E8UfyObqU4qWALIXLUptUQd1n03bWOcDn/+sjb2CLpjq584gDoQ4gEjw==", "license": "MIT" }, "node_modules/ssb-unix-socket": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-unix-socket/-/ssb-unix-socket-1.0.0.tgz", + "integrity": "sha512-Z4jBj917W+dKAiDglwxCpWm8vINOMtkpHQIgk50NQTb5jHqHI5Rcyiy7EO0uRcWwRWqXi1ZwOTEFVyLyyuittA==", "license": "MIT", "dependencies": { "multiserver": "^3.3.3" @@ -15512,6 +8224,8 @@ }, "node_modules/ssb-uri2": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/ssb-uri2/-/ssb-uri2-2.4.1.tgz", + "integrity": "sha512-3Oia/V8Q7kWj2/Wcw67yKSkMcxQIAIHGDZ4CsQxqVrEjqApYkQ5Drj0nlyFCA/qX8HF0ILFwIrHuQesk0s2PkQ==", "license": "LGPL-3.0", "dependencies": { "ssb-typescript": "^2.5.0" @@ -15522,6 +8236,8 @@ }, "node_modules/ssb-validate": { "version": "4.1.4", + "resolved": "https://registry.npmjs.org/ssb-validate/-/ssb-validate-4.1.4.tgz", + "integrity": "sha512-nzj5EQnhm5fBGXgtzuuWgxv45dW+CJJm4eCLZKiOxyG1NE/WJZwju2DmqZfiE9zr9bC2T2hPHkckDP0CCP8v8w==", "license": "MIT", "dependencies": { "is-canonical-base64": "^1.1.1", @@ -15530,176 +8246,24 @@ "ssb-ref": "^2.6.2" } }, - "node_modules/ssb-ws": { - "version": "6.2.3", - "license": "MIT", - "dependencies": { - "emoji-server": "^1.0.0", - "multiblob-http": "^1.0.0", - "multiserver": "^3.0.2", - "pull-box-stream": "^1.0.13", - "pull-stream": "^3.6.9", - "ssb-ref": "^2.3.0", - "stack": "^0.1.0" - } - }, - "node_modules/stack": { - "version": "0.1.0", - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "license": "MIT", - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/statistics": { - "version": "3.3.0" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/statistics/-/statistics-3.3.0.tgz", + "integrity": "sha512-9+dmo0XcSK1AU6/uNuSUV/9/KnGUT+7ZKeL+J4IWcjcczflFTWwYQaInBBSo2zyEFVkFd59bGVxzZ8SSBWC66g==" }, "node_modules/statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/stdin-discarder": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", - "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stdout-update": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz", - "integrity": "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^6.2.0", - "ansi-styles": "^6.2.1", - "string-width": "^7.1.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/stdout-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/stdout-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/stdout-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/stdout-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stdout-update/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/steno": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", - "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -15709,8 +8273,16 @@ "node": ">= 0.4" } }, + "node_modules/store2": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", + "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "license": "MIT" + }, "node_modules/stream-to-pull-stream": { "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz", + "integrity": "sha512-6sNyqJpr5dIOQdgNy/xcDWwDuzAsAwVzhzrWlAPAQ7Lkjx/rv0wgvxEyKwTq6FmNd5rjTrELt/CLmaSw7crMGg==", "license": "MIT", "dependencies": { "looper": "^3.0.0", @@ -15719,6 +8291,8 @@ }, "node_modules/stream-to-pull-stream/node_modules/looper": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-3.0.0.tgz", + "integrity": "sha512-LJ9wplN/uSn72oJRsXTx+snxPet5c8XiZmOKCm906NVYu+ag6SB6vUcnJcWxgnl2NfbIyeobAn7Bwv6xRj2XJg==", "license": "MIT" }, "node_modules/streamx": { @@ -15732,19 +8306,10 @@ "text-decoder": "^1.1.0" } }, - "node_modules/strict-event-emitter-types": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -15759,8 +8324,10 @@ } }, "node_modules/string-width-cjs": { - "name": "string-width", "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "name": "string-width", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -15773,10 +8340,14 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -15787,6 +8358,8 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -15800,6 +8373,8 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -15819,6 +8394,8 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -15835,6 +8412,8 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -15848,20 +8427,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/stringify-entities": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", - "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-hexadecimal": "^1.0.0" + "safe-buffer": "~5.2.0" } }, "node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15871,8 +8449,10 @@ } }, "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "name": "strip-ansi", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15881,38 +8461,10 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strnum": { - "version": "1.1.2", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/strtok3": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", @@ -15926,17 +8478,10 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -15951,123 +8496,10 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, - "node_modules/tape": { - "version": "4.17.0", - "license": "MIT", - "dependencies": { - "@ljharb/resumer": "~0.0.1", - "@ljharb/through": "~2.3.9", - "call-bind": "~1.0.2", - "deep-equal": "~1.1.1", - "defined": "~1.0.1", - "dotignore": "~0.1.2", - "for-each": "~0.3.3", - "glob": "~7.2.3", - "has": "~1.0.3", - "inherits": "~2.0.4", - "is-regex": "~1.1.4", - "minimist": "~1.2.8", - "mock-property": "~1.0.0", - "object-inspect": "~1.12.3", - "resolve": "~1.22.6", - "string.prototype.trim": "~1.2.8" - }, - "bin": { - "tape": "bin/tape" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tape/node_modules/deep-equal": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "is-arguments": "^1.1.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.5.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tape/node_modules/glob": { - "version": "7.2.3", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tape/node_modules/is-regex": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tape/node_modules/object-inspect": { - "version": "1.12.3", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, "node_modules/tar-stream": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -16075,29 +8507,42 @@ "streamx": "^2.15.0" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "node_modules/telegram": { + "version": "2.26.22", + "resolved": "https://registry.npmjs.org/telegram/-/telegram-2.26.22.tgz", + "integrity": "sha512-EIj7Yrjiu0Yosa3FZ/7EyPg9s6UiTi/zDQrFmR/2Mg7pIUU+XjAit1n1u9OU9h2oRnRM5M+67/fxzQluZpaJJg==", + "deprecated": "This package is archived and no longer maintained. Development continues in teleproto (https://npmjs.com/package/teleproto), a largely compatible, actively maintained fork. See the migration guide at https://docs.teleproto.dev/migrating-from-gramjs", "license": "MIT", - "optional": true, "dependencies": { - "streamx": "^2.12.5" + "@cryptography/aes": "^0.1.1", + "async-mutex": "^0.3.0", + "big-integer": "^1.6.48", + "buffer": "^6.0.3", + "htmlparser2": "^6.1.0", + "mime": "^3.0.0", + "node-localstorage": "^2.2.1", + "pako": "^2.0.3", + "path-browserify": "^1.0.1", + "real-cancellable-promise": "^1.1.1", + "socks": "^2.6.2", + "store2": "^2.13.0", + "ts-custom-error": "^3.2.0", + "websocket": "^1.0.34" + }, + "optionalDependencies": { + "bufferutil": "^4.0.3", + "utf-8-validate": "^5.0.5" } }, "node_modules/text-decoder": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, "node_modules/tldts": { "version": "7.0.23", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", @@ -16118,6 +8563,8 @@ }, "node_modules/to-buffer": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", "license": "MIT", "dependencies": { "isarray": "^2.0.5", @@ -16130,6 +8577,8 @@ }, "node_modules/to-camel-case": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", + "integrity": "sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==", "license": "MIT", "dependencies": { "to-space-case": "^1.0.0" @@ -16137,85 +8586,23 @@ }, "node_modules/to-no-case": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", + "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==", "license": "MIT" }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "license": "MIT", - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-space-case": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", + "integrity": "sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==", "license": "MIT", "dependencies": { "to-no-case": "^1.0.0" } }, - "node_modules/to-vfile": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-1.0.0.tgz", - "integrity": "sha512-BHc+hdHwULe8x6xmQhSuTsiiPHyTCCf7dtH7l6WkBoYBR2rDfYtoJufKLDDAYGMfA+1XoRq44HfyjoB9vMBr1w==", - "license": "MIT", - "dependencies": { - "vfile": "^1.0.0" - } - }, - "node_modules/toad-cache": { - "version": "3.7.0", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -16223,6 +8610,8 @@ }, "node_modules/token-types": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", @@ -16238,21 +8627,13 @@ }, "node_modules/too-hot": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/too-hot/-/too-hot-1.0.1.tgz", + "integrity": "sha512-ymyiJ8bM8e2wKfJpxbq4Fi7GwLy9w7P8PKHnsWlDgbRIampbMtjv8eFrvpMIC0bK5dKybYrYqvbLUVu5/sDQbw==", "license": "MIT", "dependencies": { "cpu-percentage": "~1.0.3" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -16265,12 +8646,10 @@ "node": ">=16" } }, - "node_modules/tr46": { - "version": "0.0.3", - "license": "MIT" - }, "node_modules/traverse": { "version": "0.6.11", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz", + "integrity": "sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==", "license": "MIT", "dependencies": { "gopd": "^1.2.0", @@ -16284,81 +8663,64 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/trim": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.3.tgz", - "integrity": "sha512-h82ywcYhHK7veeelXrCScdH7HkWfbIT1D/CgYO+nmDarz3SGNssVBMws6jU16Ga60AJCRAvPV6w6RLuNerQqjg==", - "deprecated": "Use String.prototype.trim() instead" - }, - "node_modules/trim-lines": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-1.1.3.tgz", - "integrity": "sha512-E0ZosSWYK2mkSu+KEtQ9/KqarVjA9HztOSX+9FDdNacRAq29RRV6ZQNgob3iuW8Htar9vAfEa6yyt5qBAHZDBA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/trim-trailing-lines": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", - "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "license": "WTFPL", "dependencies": { "utf8-byte-length": "^1.0.1" } }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsscmp": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", "license": "MIT", "engines": { "node": ">=0.6.x" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/tweetnacl": { "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, "node_modules/tweetnacl-auth": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tweetnacl-auth/-/tweetnacl-auth-0.3.1.tgz", + "integrity": "sha512-9/c8c6qRMTfWuv54ETFhihgYoofi0M9HUovMSGJ1rLRUj6O5A0vuCg2L/qKfvmcjLVhaTgAJCLy2EqCLJK2QLw==", "license": "Unlicense", "dependencies": { "tweetnacl": "0.x.x" } }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-is": { "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -16370,6 +8732,8 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -16382,6 +8746,8 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -16399,6 +8765,8 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -16418,6 +8786,8 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -16434,14 +8804,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typedarray-to-buffer": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-4.0.0.tgz", + "integrity": "sha512-6dOYeZfS3O9RtRD1caom0sMxgK59b27+IwoNy8RDPsmslSGOyU+mpTamlaIW7aNKi90ZQZ9DFaZL3YRoiSCULQ==", "funding": [ { "type": "github", @@ -16460,6 +8826,8 @@ }, "node_modules/typedarray.prototype.slice": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.5.tgz", + "integrity": "sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -16480,31 +8848,20 @@ }, "node_modules/typedfastbitset": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/typedfastbitset/-/typedfastbitset-0.2.1.tgz", + "integrity": "sha512-B2B+wo5gC7VRAqcFEiUCjS6CJ1vmeYZ7uzY3Jsu6UNStHiF+W0vkhZAmQaj5m9sC2KVrpyHGRzGuhz3M6+n/8A==", "license": "Apache-2.0" }, - "node_modules/typescript": { - "version": "5.8.3", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typewiselite": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/uint48be": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uint48be/-/uint48be-2.0.1.tgz", + "integrity": "sha512-LQvWofTo3RCz+XaQR3VNch+dDFwpIvWr/98imhQne++vFhpQP16YAC/a8w9N00Heqqra00ACjHT18cgvn5H+bg==", "license": "MIT" }, "node_modules/unbox-primitive": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -16519,13 +8876,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.2.tgz", @@ -16537,408 +8887,53 @@ }, "node_modules/undici-types": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, - "node_modules/unherit": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/unified": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/unified/-/unified-2.1.4.tgz", - "integrity": "sha512-qa4nA26ms49OczPueTt7G46r89TOlwAJ4pEk2U4mwkV1wNhjttItF03SE/YnfkgWg14tzmAHXGhJp2GhDYwn1A==", - "license": "MIT", - "dependencies": { - "attach-ware": "^1.0.0", - "bail": "^1.0.0", - "extend": "^3.0.0", - "unherit": "^1.0.4", - "vfile": "^1.0.0", - "ware": "^1.3.0" - } - }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unique-random": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-random/-/unique-random-1.0.0.tgz", - "integrity": "sha512-K1sUkPf9EXCZFNIlMCoX4icAqcvkR4FMPH4Z61HbyiWhQl1ZGo0zYeV2bJmocK8Cp6tnKYrCnpkeKGebXZoRTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unique-random-array": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unique-random-array/-/unique-random-array-1.0.1.tgz", - "integrity": "sha512-z9J/SV8CUIhIRROcHe9YUoAT6XthUJt0oUyLGgobiXJprDP9O9dsErNevvSaAv5BkhwFEVPn6nIEOKeNE6Ck1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unique-random": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", - "license": "MIT" - }, - "node_modules/unist-util-visit": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", - "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", - "license": "MIT", - "dependencies": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", - "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", - "license": "MIT", - "dependencies": { - "unist-util-is": "^3.0.0" - } - }, - "node_modules/universal-github-app-jwt": { - "version": "2.2.2", - "license": "MIT" - }, - "node_modules/universal-user-agent": { - "version": "7.0.3", - "license": "ISC" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, "license": "MIT", "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "node-gyp-build": "^4.3.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/unset-value/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/untildify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", - "integrity": "sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==", - "license": "MIT", - "dependencies": { - "os-homedir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "license": "MIT" - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "license": "MIT" - }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==", - "license": "MIT", - "dependencies": { - "os-homedir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=6.14.2" } }, "node_modules/utf8-byte-length": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "license": "(WTFPL OR MIT)" }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-name": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/vfile": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-1.4.0.tgz", - "integrity": "sha512-7Fz639rwERslMqQCuf1/0H4Tqe2q484Xl6X/jsKqrP7IjFcDODFURhv0GekMnImpbj9pTOojtqL7r39LJJkjGA==", - "license": "MIT" - }, - "node_modules/vfile-find-down": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vfile-find-down/-/vfile-find-down-1.0.0.tgz", - "integrity": "sha512-AOXiJrVKizToYfRosXd1p9Fq8b0u0qchvSwVF1/ue3JE7o7KuQ/UH24bNAPLDUG/RIM1DZ6zWtDsiLShcma4WA==", - "license": "MIT", - "dependencies": { - "to-vfile": "^1.0.0" - } - }, - "node_modules/vfile-find-up": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vfile-find-up/-/vfile-find-up-1.0.0.tgz", - "integrity": "sha512-t97P/jQswvX0n//+RB74Wj43VOg3tel2InzaJYryaBewd4uN4pNXuoH/F00PkI3U1fBp+w/SIyrTjzIzPwDODg==", - "license": "MIT", - "dependencies": { - "to-vfile": "^1.0.0" - } - }, - "node_modules/vfile-reporter": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-1.5.0.tgz", - "integrity": "sha512-VFF1LK0O8/nLmrPcc+5VMEnyP21BTzdVoq1rbxTaVt6cmSVk5MQs1POhkfY/cctndmZheNgirTcAMoiKj3aJYA==", - "license": "MIT", - "dependencies": { - "chalk": "^1.1.0", - "log-symbols": "^1.0.2", - "plur": "^2.0.0", - "repeat-string": "^1.5.0", - "string-width": "^1.0.0", - "text-table": "^0.2.0", - "vfile-sort": "^1.0.0" - } - }, - "node_modules/vfile-reporter/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==", - "license": "MIT", - "dependencies": { - "chalk": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-1.0.0.tgz", - "integrity": "sha512-6qIalNEKUt2YyVFzyJptdEo9sm/pMrSKvOJ35lH4us9YeW08zRs3E9VbdJ0O0n2Thxc1TWINP5QVhucy/YiGPA==", - "license": "MIT" - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -16951,22 +8946,31 @@ "node": ">=18" } }, - "node_modules/ware": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz", - "integrity": "sha512-Y2HUDMktriUm+SR2gZWxlrszcgtXExlhQYZ8QJNYbl22jum00KIUcHJ/h/sdAXhWTJcbSkiMYN9Z2tWbWYSrrw==", - "license": "MIT", + "node_modules/websocket": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", + "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", + "license": "Apache-2.0", "dependencies": { - "wrap-fn": "^0.1.0" + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.63", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "license": "MIT" + "node_modules/websocket/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } }, "node_modules/whatwg-mimetype": { "version": "5.0.0", @@ -16977,16 +8981,10 @@ "node": ">=20" } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -17000,6 +8998,8 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", @@ -17017,6 +9017,8 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -17042,6 +9044,8 @@ }, "node_modules/which-collection": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -17058,10 +9062,14 @@ }, "node_modules/which-module": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, "node_modules/which-typed-array": { "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -17079,15 +9087,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -17102,8 +9105,10 @@ } }, "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "name": "wrap-ansi", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -17119,10 +9124,14 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17135,6 +9144,8 @@ }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -17145,6 +9156,8 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -17155,6 +9168,8 @@ }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -17166,27 +9181,27 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrap-fn": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz", - "integrity": "sha512-xDLdGx0M8JQw9QDAC9s5NUxtg9MI09F6Vbxa2LYoSoCvzJnx2n81YMIfykmXEGsUvuLaxnblJTzhSOjUOX37ag==", - "license": "MIT", - "dependencies": { - "co": "3.1.0" - } - }, - "node_modules/wrap-fn/node_modules/co": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", - "integrity": "sha512-CQsjCRiNObI8AtTsNIBDRMQ4oMR83CzEswHYahClvul7gKk+lDQiOKv+5qh7LQWf5sh6jkZNispz/QlsZxyNgA==", - "license": "MIT" - }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, "node_modules/ws": { "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -17221,10 +9236,14 @@ }, "node_modules/xsalsa20": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/xsalsa20/-/xsalsa20-1.2.0.tgz", + "integrity": "sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w==", "license": "MIT" }, "node_modules/xtend": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", "engines": { "node": ">=0.4" @@ -17232,22 +9251,27 @@ }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=0.10.32" } }, "node_modules/yargs": { "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -17264,6 +9288,8 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { "node": ">=12" @@ -17271,10 +9297,14 @@ }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17287,31 +9317,29 @@ }, "node_modules/ylru": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", "license": "MIT", "engines": { "node": ">= 4.0.0" } }, - "node_modules/yoctocolors": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zerr": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/zerr/-/zerr-1.0.4.tgz", + "integrity": "sha512-u3WW9JA9xNmhy3cK6iSi3uWe4XmuqPCZ+jOrngEIwYz//ZUhY6bexVFrWLcxrIDUbJeXnyJORqVYPi2YU6gA+A==", "license": "MIT" }, "node_modules/ziii": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ziii/-/ziii-1.0.2.tgz", + "integrity": "sha512-q1FogtBIchy1W0fkxUpe6A4n4WUvAM+hAHN1J6LjBNCV42ZegeC5JSz0mcNv4qxnI0V4cL4FNeEhPMm97Ed0kA==", "license": "MIT" }, "node_modules/zip-stream": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "license": "MIT", "dependencies": { "archiver-utils": "^5.0.0", @@ -17324,490 +9352,12 @@ }, "node_modules/zod": { "version": "3.25.56", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.56.tgz", + "integrity": "sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "packages/ssb-server": { - "version": "15.3.0", - "license": "MIT", - "dependencies": { - "bash-color": "~0.0.3", - "broadcast-stream": "^0.2.1", - "cont": "~1.0.3", - "cross-spawn": "^6.0.5", - "debug": "^4.1.1", - "deep-equal": "^1.0.1", - "explain-error": "^1.0.3", - "has-network": "0.0.1", - "ip": "^1.1.5", - "mdmanifest": "^1.0.8", - "minimist": "^1.1.3", - "mkdirp": "~0.5.0", - "multiblob": "^1.13.0", - "multiserver": "^3.3.1", - "multiserver-address": "^1.0.1", - "muxrpc-validation": "^3", - "muxrpcli": "3", - "mv": "^2.1.1", - "osenv": "^0.1.5", - "pull-cat": "~1.1.5", - "pull-file": "^1.0.0", - "pull-many": "~1.0.6", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.6.2", - "rimraf": "^2.4.2", - "secret-stack": "^6.3.1", - "ssb-blobs": "1.2.2", - "ssb-caps": "^1.0.1", - "ssb-client": "^4.7.9", - "ssb-config": "^3.2.5", - "ssb-db": "19.2.0", - "ssb-ebt": "^5.6.1", - "ssb-friends": "^4.1.0", - "ssb-gossip": "^1.1.0", - "ssb-invite": "^2.1.2", - "ssb-keys": "^7.1.1", - "ssb-links": "^3.0.10", - "ssb-local": "^1.0.0", - "ssb-logging": "^1.0.0", - "ssb-master": "^1.0.2", - "ssb-no-auth": "^1.0.0", - "ssb-onion": "^1.0.0", - "ssb-ooo": "^1.3.0", - "ssb-plugins": "1.0.0", - "ssb-query": "^2.4.0", - "ssb-ref": "^2.13.9", - "ssb-replicate": "^1.3.0", - "ssb-unix-socket": "^1.0.0", - "ssb-ws": "^6.2.1", - "stream-to-pull-stream": "^1.6.10", - "zerr": "^1.0.0" - }, - "bin": { - "sbot": "bin.js", - "ssb-server": "bin.js" - }, - "devDependencies": { - "cat-names": "^2.0.0", - "compatibility": "^1.0.0", - "dog-names": "~1.0.2", - "hexpp": "^2.0.0", - "interleavings": "^1.0.0", - "npm-install-package": "^2.1.0", - "nyc": "^13.2.0", - "pull-abortable": "^4.1.1", - "pull-bitflipper": "^0.1.0", - "pull-paramap": "^1.2.2", - "rng": "^0.2.2", - "run-series": "^1.1.8", - "semver": "^5.6.0", - "ssb-feed": "^2.3.0", - "ssb-generate": "^1.0.1", - "ssb-validate": "^4", - "tape": "^4.9.1", - "typewiselite": "^1.0.0" - } - }, - "packages/ssb-server/node_modules/abstract-leveldown": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.0.3.tgz", - "integrity": "sha512-jzewKKpZbaYUa6HTThnrl+GrJhzjEAeuc7hTVpZdzg7kupXZFoqQDFwyOwLNbmJKJlmzw8yiipMPkDiuKkT06Q==", - "deprecated": "Superseded by abstract-level (https://github.com/Level/community#faq)", - "license": "MIT", - "dependencies": { - "level-concat-iterator": "~2.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "packages/ssb-server/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "packages/ssb-server/node_modules/epidemic-broadcast-trees": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/epidemic-broadcast-trees/-/epidemic-broadcast-trees-7.0.0.tgz", - "integrity": "sha512-nm1o1ncxcJvXtLJVoPl4AJFX7Bh0BhPsTC9XhCRWwdRJXp1Ud1DcGpi/6c3sUA/URmyoIc1WONNdub+/AE5vTQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "push-stream": "^10.0.0" - } - }, - "packages/ssb-server/node_modules/flumecodec": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", - "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", - "license": "MIT", - "dependencies": { - "level-codec": "^6.2.0" - } - }, - "packages/ssb-server/node_modules/flumedb": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/flumedb/-/flumedb-1.1.0.tgz", - "integrity": "sha512-Bwol+72GU5z2DxZlnaxUA9A8qaRcQcdTprmRcgfqn2ldn147ByVh9Zyp90hVGPlo/oEN/yjOBUXcNkK3SYjbgA==", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "explain-error": "^1.0.3", - "obv": "0.0.1", - "pull-cont": "^0.1.1", - "pull-looper": "^1.0.0", - "pull-stream": "^3.5.0" - } - }, - "packages/ssb-server/node_modules/flumeview-level": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/flumeview-level/-/flumeview-level-3.0.14.tgz", - "integrity": "sha512-Nl0gJOgrDGpJGZDkP6gvo6s1Q9WmRynbHUvI/JY3eQ81YgzUUa2FKLlfu6OHV5ho5NeXP+00F+0K1yBVaEgJOQ==", - "license": "MIT", - "dependencies": { - "charwise": "^3.0.1", - "explain-error": "^1.0.4", - "level": "^5.0.0", - "ltgt": "^2.1.3", - "mkdirp": "^0.5.1", - "obv": "0.0.1", - "pull-level": "^2.0.3", - "pull-paramap": "^1.2.1", - "pull-stream": "^3.5.0", - "pull-write": "^1.1.1" - } - }, - "packages/ssb-server/node_modules/flumeview-level/node_modules/level": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/level/-/level-5.0.1.tgz", - "integrity": "sha512-wcak5OQeA4rURGacqS62R/xNHjCYnJSQDBOlm4KNUGJVE9bWv2B04TclqReYejN+oD65PzD4FsqeWoI5wNC5Lg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "level-js": "^4.0.0", - "level-packager": "^5.0.0", - "leveldown": "^5.0.0", - "opencollective-postinstall": "^2.0.0" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "packages/ssb-server/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "packages/ssb-server/node_modules/immediate": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", - "integrity": "sha512-RrGCXRm/fRVqMIhqXrGEX9rRADavPiDFSoMb/k64i9XMk8uH4r/Omi5Ctierj6XzNecwDbO4WuFbDD1zmpl3Tg==", - "license": "MIT" - }, - "packages/ssb-server/node_modules/ip": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", - "license": "MIT" - }, - "packages/ssb-server/node_modules/level-codec": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", - "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", - "deprecated": "Superseded by level-transcoder (https://github.com/Level/community#faq)", - "license": "MIT" - }, - "packages/ssb-server/node_modules/level-js": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/level-js/-/level-js-4.0.2.tgz", - "integrity": "sha512-PeGjZsyMG4O89KHiez1zoMJxStnkM+oBIqgACjoo5PJqFiSUUm3GNod/KcbqN5ktyZa8jkG7I1T0P2u6HN9lIg==", - "deprecated": "Superseded by browser-level (https://github.com/Level/community#faq)", - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~6.0.1", - "immediate": "~3.2.3", - "inherits": "^2.0.3", - "ltgt": "^2.1.2", - "typedarray-to-buffer": "~3.1.5" - } - }, - "packages/ssb-server/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "packages/ssb-server/node_modules/muxrpc": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/muxrpc/-/muxrpc-6.7.3.tgz", - "integrity": "sha512-yVr66BigJxHIvHOLsdJzuvrHt46Vq9DiDCBTZiS6q+49mXh4mkDjRiXo9FnGXQv1WJOqnRJk5fcfwV6TaSgNvA==", - "license": "MIT", - "dependencies": { - "clarify-error": "^1.0.0", - "debug": "^4.3.3", - "packet-stream": "~2.0.0", - "packet-stream-codec": "^1.2.0", - "pull-goodbye": "0.0.3", - "pull-stream": "^3.6.10" - } - }, - "packages/ssb-server/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "packages/ssb-server/node_modules/push-stream": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/push-stream/-/push-stream-10.1.2.tgz", - "integrity": "sha512-wCDN1KkAWOMXsiV0XxH/CggHqOYeXvyn00t9Zjp5RKsLZ2rzg/lLJIMAVxYLyh79T168W3fBYcG5TRRJAlQr6g==", - "license": "MIT" - }, - "packages/ssb-server/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "packages/ssb-server/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "packages/ssb-server/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "packages/ssb-server/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "packages/ssb-server/node_modules/ssb-blobs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ssb-blobs/-/ssb-blobs-1.2.2.tgz", - "integrity": "sha512-N+X46lE/KaIH9y1w3LQ9pPnt2tQr5VCSj1dAo/pJGYnSwnHz8GhIiboq7UGzrCZwCWgVUs22/2YiytCXSC9ASg==", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "debug": "^4.1.1", - "level": "^5.0.1", - "multiblob": "^1.12.0", - "pull-level": "^2.0.4", - "pull-notify": "^0.1.0", - "pull-stream": "^3.3.0", - "ssb-ref": "^2.3.0" - } - }, - "packages/ssb-server/node_modules/ssb-blobs/node_modules/level": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/level/-/level-5.0.1.tgz", - "integrity": "sha512-wcak5OQeA4rURGacqS62R/xNHjCYnJSQDBOlm4KNUGJVE9bWv2B04TclqReYejN+oD65PzD4FsqeWoI5wNC5Lg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "level-js": "^4.0.0", - "level-packager": "^5.0.0", - "leveldown": "^5.0.0", - "opencollective-postinstall": "^2.0.0" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "packages/ssb-server/node_modules/ssb-db": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/ssb-db/-/ssb-db-19.2.0.tgz", - "integrity": "sha512-pJAFizB6OcuJLX4RJJuU9HWyPwM2CqLi/vs08lhVIR3TGxacxpavvK5LzbxT+Y3iWkBchOTKS5hHCigA5aaung==", - "license": "MIT", - "dependencies": { - "async-write": "^2.1.0", - "cont": "~1.0.0", - "explain-error": "~1.0.1", - "flumedb": "^1.0.0", - "flumelog-offset": "^3.3.1", - "flumeview-hashtable": "^1.0.3", - "flumeview-level": "^3.0.5", - "flumeview-reduce": "^1.3.9", - "ltgt": "^2.2.0", - "mdmanifest": "^1.0.8", - "mkdirp": "^0.5.1", - "monotonic-timestamp": "~0.0.8", - "muxrpc-validation": "^3.0.0", - "obv": "0.0.1", - "osenv": "^0.1.5", - "pull-cont": "^0.1.1", - "pull-stream": "^3.4.0", - "rimraf": "^2.6.2", - "ssb-keys": "^7.1.3", - "ssb-msgs": "^5.0.0", - "ssb-ref": "^2.12.0", - "ssb-validate": "^4.0.0", - "typewiselite": "^1.0.0", - "zerr": "^1.0.0" - } - }, - "packages/ssb-server/node_modules/ssb-ebt": { - "version": "5.6.7", - "resolved": "https://registry.npmjs.org/ssb-ebt/-/ssb-ebt-5.6.7.tgz", - "integrity": "sha512-ifPgPNmDE8EKuuoqtXibwgYNtDZNry7sJL1epSUb3XgQr62bUV31N9R5LHKDsI2kx96OgWRwWY2PfZ7vf/hU8Q==", - "license": "MIT", - "dependencies": { - "base64-url": "^2.2.0", - "epidemic-broadcast-trees": "7", - "lossy-store": "^1.2.3", - "pull-stream": "^3.5.0", - "push-stream-to-pull-stream": "^1.0.0", - "ssb-ref": "^2.9.1" - } - }, - "packages/ssb-server/node_modules/ssb-friends": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/ssb-friends/-/ssb-friends-4.4.10.tgz", - "integrity": "sha512-APTTt47PRVv5nzo+8VLWymAxSZPiAA3p5wPu9VlCqr4qQVaDkCF7Xn5Lk9j8VkjEnclNt7aeE/YgnXCld/BPNA==", - "license": "MIT", - "dependencies": { - "bipf": "^1.9.0", - "flumecodec": "0.0.1", - "flumeview-reduce": "^1.4.0", - "layered-graph": "^1.2.0", - "pull-flatmap": "0.0.1", - "pull-level": "^2.0.4", - "pull-notify": "^0.1.2", - "pull-stream": "^3.7.0", - "ssb-ref": "^2.16.0" - } - }, - "packages/ssb-server/node_modules/ssb-invite": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/ssb-invite/-/ssb-invite-2.1.8.tgz", - "integrity": "sha512-fuBu0jtZzfsejLWUQXVzNq6s6IekJGZqVpY6EZLx3J2mM0+3rXuwDI/1LtqL2VOfzrPbC4jRJi6I21PosBGveg==", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "explain-error": "^1.0.4", - "ip": "^1.1.8", - "level": "^6.0.1", - "muxrpc-validation": "^3.0.2", - "ssb-client": "^4.9.0", - "ssb-keys": "^7.2.2", - "ssb-ref": "^2.16.0" - } - }, - "packages/ssb-server/node_modules/ssb-keys": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-7.2.2.tgz", - "integrity": "sha512-FPeyYU/3LpxcagnbmVWE+Q/qzg6keqeOBPbD7sEH9UKixUASeufPKiORDgh8nVX7J9Z+0vUaHt/WG999kGjvVQ==", - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "packages/ssb-server/node_modules/ssb-plugins": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ssb-plugins/-/ssb-plugins-1.0.0.tgz", - "integrity": "sha512-eM8vid+K8MhwZwzk/CDUhSNhUoS6wYgq9clJrrKaP0/Otdd3zZzcBQw54Xvm0olMcOgpTSlY3m2rT4iqjZPIBw==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.5", - "explain-error": "^1.0.4", - "mdmanifest": "^1.0.8", - "mkdirp": "^0.5.1", - "muxrpc": "^6.4.0", - "muxrpc-validation": "^3.0.0", - "mv": "^2.1.1", - "osenv": "^0.1.5", - "pull-cat": "^1.1.11", - "pull-many": "^1.0.8", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.6.9", - "stream-to-pull-stream": "^1.7.2" - } - }, - "packages/ssb-server/node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "packages/ssb-server/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } } } } diff --git a/src/server/package.json b/src/server/package.json index 00bb761..3acea84 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -1,6 +1,6 @@ { "name": "@krakenslab/oasis", - "version": "0.8.7", + "version": "1.3.3", "description": "Oasis - Social Networking Utopia", "repository": { "type": "git", @@ -17,27 +17,22 @@ "start:backend": "node ../backend/backend.js", "start:ssb": "node SSB_server.js start &", "dev": "npm run dev:backend", - "dev:backend": "nodemon", + "dev:backend": "npx --yes nodemon@3.1.14", "postinstall": "node ../../scripts/patch-node-modules.js" }, "dependencies": { "@koa/router": "^13.1.0", - "@open-rpc/client-js": "^1.8.1", - "@xenova/transformers": "^2.17.2", - "abstract-level": "^2.0.1", "archiver": "^7.0.1", "axios": "^1.10.0", - "cors": "^2.8.5", + "bencode": "^2.0.3", + "bipf": "^1.9.0", + "chloride": "^2.4.1", "debug": "^4.3.1", "dompurify": "^3.3.1", "env-paths": "^2.2.1", - "epidemic-broadcast-trees": "^9.0.4", - "express": "^5.1.0", "file-type": "^16.5.4", "highlight.js": "11.0.0", "hyperaxe": "^2.0.1", - "ip": "https://registry.npmjs.org/neoip/-/neoip-3.0.0.tgz", - "is-svg": "^4.4.0", "jsdom": "^28.0.0", "koa": "^2.7.0", "koa-body": "^6.0.1", @@ -47,77 +42,49 @@ "minimist": "^1.2.8", "module-alias": "^2.2.3", "moment": "^2.30.1", - "multiblob": "^1.13.0", - "multiserver": "^3.3.1", - "multiserver-address": "^1.0.1", - "muxrpc": "^8.0.0", - "muxrpc-validation": "^3.0.2", - "muxrpcli": "^3.1.2", - "node-llama-cpp": "^3.10.0", - "non-private-ip": "^2.2.0", "open": "^8.4.2", "openpgp": "^6.3.0", - "packet-stream": "^2.0.6", - "packet-stream-codec": "^1.2.0", - "pdfjs-dist": "^5.2.133", + "opusscript": "^0.1.1", "pretty-ms": "^7.0.1", "pull-abortable": "^4.1.1", "pull-cat": "~1.1.5", - "pull-file": "^1.0.0", - "pull-many": "~1.0.6", + "pull-defer": "^0.2.3", + "pull-level": "^2.0.4", "pull-paramap": "^1.2.2", "pull-pushable": "^2.2.0", "pull-sort": "^1.0.2", "pull-stream": "^3.7.0", - "punycode.js": "^2.3.1", "qrcode": "^1.5.4", "secret-stack": "^6.3.1", - "ssb-about": "^2.0.1", "ssb-autofollow": "^1.1.0", - "ssb-backlinks": "^2.1.1", "ssb-blobs": "^2.0.1", "ssb-box": "^1.0.1", "ssb-caps": "^1.0.1", + "ssb-classic": "^1.1.0", "ssb-client": "^4.9.0", "ssb-config": "^3.4.4", "ssb-conn": "6.0.3", - "ssb-conn-db": "^1.0.5", - "ssb-conn-hub": "^1.2.0", - "ssb-conn-staging": "^1.0.0", - "ssb-db": "^20.4.1", - "ssb-device-address": "^1.1.6", + "ssb-db2": "^6.3.3", "ssb-ebt": "^9.1.2", - "ssb-friend-pub": "^1.0.7", "ssb-friends": "^5.0.0", - "ssb-gossip": "^1.1.1", "ssb-invite": "^3.0.3", "ssb-invite-client": "^1.3.3", "ssb-keys": "^8.0.0", "ssb-lan": "^1.0.0", - "ssb-legacy-conn": "^1.0.17", - "ssb-links": "^3.0.10", "ssb-logging": "^1.0.0", - "ssb-markdown": "^3.6.0", "ssb-master": "^1.0.3", - "ssb-meme": "^1.1.0", "ssb-mentions": "^0.5.2", "ssb-msgs": "^5.2.0", "ssb-no-auth": "^1.0.0", "ssb-onion": "^1.0.0", - "ssb-partial-replication": "^3.0.1", - "ssb-plugins": "^1.0.2", - "ssb-private": "^1.1.0", - "ssb-query": "^2.4.5", "ssb-ref": "^2.16.0", "ssb-replication-scheduler": "^3.0.0", - "ssb-search": "^1.3.0", - "ssb-server": "file:packages/ssb-server", - "ssb-tangle": "^1.0.1", + "ssb-sort": "^1.1.3", "ssb-thread-schema": "^1.1.1", - "ssb-threads": "^10.0.4", "ssb-unix-socket": "^1.0.0", - "ssb-ws": "^6.2.3", - "util": "^0.12.5", + "ssb-validate": "^4.1.4", + "stream-to-pull-stream": "^1.7.3", + "telegram": "^2.26.22", "yargs": "^17.7.2" }, "overrides": { @@ -135,15 +102,14 @@ "ssb-ref": "^2.16.0", "secret-stack": "^6.3.1", "ip": "https://registry.npmjs.org/neoip/-/neoip-3.0.0.tgz", - "lodash.set": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + "lodash.set": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "onnxruntime-node": "1.19.2", + "onnxruntime-common": "1.19.2", + "onnxruntime-web": "1.19.2" }, "optionalDependencies": { - "fsevents": "^2.3.2", "sharp": "^0.33.5" }, - "devDependencies": { - "nodemon": "^3.1.0" - }, "bugs": { "url": "https://github.com/epsylon/oasis/issues" }, @@ -153,7 +119,7 @@ }, "keywords": [], "engines": { - "node": "^10.0.0 || >=12.0.0" + "node": ">=22.0.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/server/phone_audio_android.js b/src/server/phone_audio_android.js new file mode 100644 index 0000000..eb45a2e --- /dev/null +++ b/src/server/phone_audio_android.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const net = require('net'); +const path = require('path'); + +const MARKER = path.join(__dirname, '..', '..', '.audio_bridge'); +const MAX_BACKLOG = 4000; + +const bridgePort = () => { + try { + const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim()); + return Number.isFinite(port) && port > 0 ? port : 0; + } catch (_) { + return 0; + } +}; + +module.exports = () => { + const port = bridgePort(); + if (!port) return null; + const open = (mode) => { + const sock = net.connect(port, '127.0.0.1'); + sock.on('error', () => {}); + sock.setNoDelay(true); + sock.write(mode); + return sock; + }; + return { + capture(onPcm) { + const sock = open('R'); + sock.on('data', onPcm); + return { stop() { try { sock.destroy(); } catch (_) {} } }; + }, + player() { + const sock = open('P'); + return { + write(buf) { + if (sock.destroyed || sock.writableLength > MAX_BACKLOG) return false; + sock.write(buf); + return true; + }, + stop() { try { sock.end(); sock.destroy(); } catch (_) {} } + }; + } + }; +}; diff --git a/src/server/phone_module.js b/src/server/phone_module.js new file mode 100644 index 0000000..32a6f41 --- /dev/null +++ b/src/server/phone_module.js @@ -0,0 +1,2124 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawn } = require('child_process'); +const nodeCrypto = require('crypto'); +const pull = require('./node_modules/pull-stream'); +const Pushable = require('./node_modules/pull-pushable'); +const sodium = require('./node_modules/chloride'); +const ssbKeys = require('./node_modules/ssb-keys'); + +const RING_MS = 20000; +const PAM_MIN_MS = 500; +const dbg = process.env.OASIS_DEBUG ? (...a) => console.log('[phone]', ...a) : () => {}; +const sid = (id) => String(id || '').slice(0, 9); +const PAM_WARN_MS = 10000; +const VOICEMAIL_MAX_MS = 60000; +const RATE = 8000; +const FRAME_BYTES = 320; +const FRAME_MS = 20; +const MAX_BACKLOG = 4000; +const CLOCK_SKEW_MS = 5 * 60 * 1000; +const RING_WINDOW_MS = 60 * 1000; +const RING_PER_WINDOW = 20; +const SEEN_MAX = 5000; +const HUB_ROOMS_MAX = 256; +const HUB_RIDS_PER_ROOM = 4; +const GROUP_MAX = 10; +const MIX_QUEUE = 10; +const ROOM_MAX = 50; +const ROOM_QUIET_AT = 10; +const ROOM_JOIN_MS = 10000; +const ROOM_PEEK_MS = 1500; +const ROOM_FRAME_MAX = 2048; +const ROOM_RATE_MAX = 120; +const HUB_LAG_MAX = 2000; +const ROOM_ACK_EVERY = 100; +const ROUTES_MAX = 4096; +const MUTE_BEACON_MS = 2000; +const MUTE_TTL_MS = 6000; +const ROOM_EVENTS_MAX = 50; +const REC_TOGGLE_MIN_MS = 250; +const CHIMES_AT_ONCE = 3; +const ROOM_REC_MAX_BYTES = 4 * 3600 * 8000 * 2; +const CHIMES = { + join: [[660, 90], [0, 20], [990, 140]], + leave: [[990, 90], [0, 20], [660, 190]], + recStart: [[880, 70], [0, 60], [880, 70], [0, 60], [1320, 160]], + recStop: [[1320, 70], [0, 60], [660, 200]], + mute: [[523, 80], [0, 30], [392, 160]], + unmute: [[392, 80], [0, 30], [523, 180]], + hand: [[784, 60], [0, 40], [988, 60], [0, 40], [1175, 110]] +}; +const chimeMs = (kind) => (CHIMES[kind] || []).reduce((n, [, ms]) => n + ms, 0); +const K_CTRL = 0; +const K_AUDIO = 1; +const K_DATA = 2; +const K_KEY = 3; +const K_VIDEO = 4; +const VIDEO_CHUNK = 8 * 1024; +const VIDEO_PAD = 1024; +const VIDEO_HDR = 6; +const VIDEO_FILM = 0x80; +const VIDEO_FRAME_MAX = 256 * 1024; +const VIDEO_FPS = 8; +const VIDEO_SIZE = { w: 320, h: 240 }; +const STREAM_SIZE = { w: 640, h: 480 }; +const GRID_TILE_LARGE = { w: 640, h: 480 }; +const GRID_LARGE_MAX = 4; +const ROOM_VIDEO_CHUNK_EXTRA = 45; +const VIDEO_HOLD_MS = 3000; +const ROOM_VIDEO_FRAME_MAX = 10 * 1024; +const ROOM_VIDEO_BYTES_PER_S = 200 * 1024; +const ROOM_VIDEO_SEND_MAX = Math.floor(ROOM_VIDEO_BYTES_PER_S * 0.9); +const VAD_MIN = 200; +const VAD_FLOOR_MAX = 1500; +const VAD_HANG = 15; +const RID = /^[0-9a-f]{32}$/; + +const BIAS = 0x84; +const CLIP = 32635; +const ulawEncodeSample = (sample) => { + let s = sample; + const sign = (s >> 8) & 0x80; + if (sign) s = -s; + if (s > CLIP) s = CLIP; + s += BIAS; + let exponent = 7; + for (let mask = 0x4000; (s & mask) === 0 && exponent > 0; exponent--, mask >>= 1); + const mantissa = (s >> (exponent + 3)) & 0x0f; + return ~(sign | (exponent << 4) | mantissa) & 0xff; +}; +const ULAW_DECODE = new Int16Array(256); +for (let i = 0; i < 256; i++) { + const u = ~i & 0xff; + const t = (((u & 0x0f) << 3) + BIAS) << ((u & 0x70) >> 4); + ULAW_DECODE[i] = (u & 0x80) ? BIAS - t : t - BIAS; +} +const ulawEncode = (pcm) => { + const out = Buffer.alloc(pcm.length >> 1); + for (let i = 0; i < out.length; i++) out[i] = ulawEncodeSample(pcm.readInt16LE(i * 2)); + return out; +}; +const ulawDecode = (ulaw) => { + const out = Buffer.alloc(ulaw.length * 2); + for (let i = 0; i < ulaw.length; i++) out.writeInt16LE(ULAW_DECODE[ulaw[i]], i * 2); + return out; +}; + +const wavFile = (pcm) => { + const h = Buffer.alloc(44); + h.write('RIFF', 0); h.writeUInt32LE(36 + pcm.length, 4); h.write('WAVE', 8); + h.write('fmt ', 12); h.writeUInt32LE(16, 16); h.writeUInt16LE(1, 20); h.writeUInt16LE(1, 22); + h.writeUInt32LE(RATE, 24); h.writeUInt32LE(RATE * 2, 28); h.writeUInt16LE(2, 32); h.writeUInt16LE(16, 34); + h.write('data', 36); h.writeUInt32LE(pcm.length, 40); + return Buffer.concat([h, pcm]); +}; + +const connectingCycle = () => { + const n = RATE * 2; + const buf = Buffer.alloc(n * 2); + let seed = 0x2545f491; + let lp = 0; + for (let i = 0; i < n; i++) { + seed = (seed * 1103515245 + 12345) >>> 0; + lp += (((seed >>> 8) / 0x800000) - 1 - lp) * 0.08; + const env = Math.sin(Math.PI * i / n); + buf.writeInt16LE(Math.round(lp * 9000 * env * env), i * 2); + } + return buf; +}; +const toneCycle = (kind) => { + if (kind === 'connecting') return connectingCycle(); + const segs = kind === 'ring' + ? [{ ms: 1000, freq: (k) => (Math.floor(k / (RATE / 20)) % 2 ? 1250 : 1000) }, { ms: 2000 }] + : [{ ms: 1500, freq: () => 425 }, { ms: 3000 }]; + const total = segs.reduce((a, s) => a + s.ms, 0) * RATE / 1000; + const buf = Buffer.alloc(total * 2); + let i = 0; + let phase = 0; + for (const s of segs) { + const n = s.ms * RATE / 1000; + for (let k = 0; k < n; k++, i++) { + let v = 0; + if (s.freq) { phase += 2 * Math.PI * s.freq(k) / RATE; v = Math.round(Math.sin(phase) * 6000); } + buf.writeInt16LE(v, i * 2); + } + } + return buf; +}; + +const onPath = (bin) => (process.env.PATH || '').split(path.delimiter).some(d => d && fs.existsSync(path.join(d, bin))); +const BACKENDS = [ + { rec: ['parec', ['--raw', `--rate=${RATE}`, '--channels=1', '--format=s16le', '--latency-msec=20']], play: ['pacat', ['--playback', '--raw', `--rate=${RATE}`, '--channels=1', '--format=s16le', '--latency-msec=60']] }, + { rec: ['pw-record', ['--rate', String(RATE), '--channels', '1', '--format', 's16', '-']], play: ['pw-play', ['--rate', String(RATE), '--channels', '1', '--format', 's16', '-']] }, + { rec: ['arecord', ['-q', '-t', 'raw', '-f', 'S16_LE', '-r', String(RATE), '-c', '1']], play: ['aplay', ['-q', '-t', 'raw', '-f', 'S16_LE', '-r', String(RATE), '-c', '1']] } +]; +const systemAudio = () => { + const backend = BACKENDS.find(b => onPath(b.rec[0]) && onPath(b.play[0])); + if (!backend) return null; + return { + capture(onPcm) { + const p = spawn(backend.rec[0], backend.rec[1], { stdio: ['ignore', 'pipe', 'ignore'] }); + p.on('error', () => {}); + p.stdout.on('data', onPcm); + return { stop() { try { p.kill('SIGTERM'); } catch (_) {} } }; + }, + player() { + const p = spawn(backend.play[0], backend.play[1], { stdio: ['pipe', 'ignore', 'ignore'] }); + p.on('error', () => {}); + p.stdin.on('error', () => {}); + return { + write(buf) { + if (!p.stdin.writable || p.stdin.writableLength > MAX_BACKLOG) return false; + p.stdin.write(buf); + return true; + }, + end() { try { p.stdin.end(); } catch (_) {} }, + stop() { try { p.stdin.end(); p.kill('SIGTERM'); } catch (_) {} } + }; + } + }; +}; +let audioFactory = systemAudio; + +const JPEG_SOI = Buffer.from([0xff, 0xd8, 0xff]); +const JPEG_EOI = Buffer.from([0xff, 0xd9]); +const jpegSplitter = (onJpeg) => { + let pending = Buffer.alloc(0); + return (chunk) => { + pending = pending.length ? Buffer.concat([pending, chunk]) : Buffer.from(chunk); + for (;;) { + const start = pending.indexOf(JPEG_SOI); + if (start < 0) { pending = Buffer.alloc(0); return; } + const end = pending.indexOf(JPEG_EOI, start + 3); + if (end < 0) { pending = pending.length - start > VIDEO_FRAME_MAX ? Buffer.alloc(0) : pending.subarray(start); return; } + onJpeg(Buffer.from(pending.subarray(start, end + 2))); + pending = pending.subarray(end + 2); + } + }; +}; +const VIDEO_BACKENDS = [ + { bin: 'v4l2-ctl', args: (dev) => ['--device', dev, `--set-fmt-video=width=${VIDEO_SIZE.w},height=${VIDEO_SIZE.h},pixelformat=MJPG`, `--set-parm=${VIDEO_FPS}`, '--stream-mmap', '--stream-to=-'] }, + { bin: 'gst-launch-1.0', args: (dev) => ['-q', 'v4l2src', `device=${dev}`, '!', `video/x-raw,width=${VIDEO_SIZE.w},height=${VIDEO_SIZE.h}`, '!', 'videorate', '!', `video/x-raw,framerate=${VIDEO_FPS}/1`, '!', 'videoconvert', '!', 'jpegenc', 'quality=60', '!', 'fdsink', 'fd=1'] }, + { bin: 'ffmpeg', args: (dev) => ['-loglevel', 'quiet', '-f', 'v4l2', '-video_size', `${VIDEO_SIZE.w}x${VIDEO_SIZE.h}`, '-i', dev, '-vf', `fps=${VIDEO_FPS}`, '-c:v', 'mjpeg', '-q:v', '8', '-f', 'image2pipe', '-'] } +]; +const videoDevices = () => { + try { return fs.readdirSync('/dev').filter(n => /^video\d+$/.test(n)).sort((a, b) => Number(a.slice(5)) - Number(b.slice(5))).map(n => `/dev/${n}`); } catch (_) { return []; } +}; +const systemVideo = (server, device) => { + const backend = VIDEO_BACKENDS.find(b => onPath(b.bin)); + const dev = device || videoDevices()[0]; + if (!backend || !dev) return null; + return { + capture(onJpeg) { + const p = spawn(backend.bin, backend.args(dev), { stdio: ['ignore', 'pipe', 'ignore'] }); + p.on('error', () => {}); + p.stdout.on('data', jpegSplitter(onJpeg)); + return { stop() { try { p.kill('SIGTERM'); } catch (_) {} } }; + } + }; +}; +let videoFactory = systemVideo; + +const hasBin = (bin) => (path.isAbsolute(String(bin || '')) ? fs.existsSync(bin) : onPath(bin)); +const gstQuote = (s) => `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; +const STREAM_BACKENDS = [ + { + bin: () => process.env.OASIS_FFMPEG || 'ffmpeg', + retryWithoutAudio: true, + args: (file, withAudio) => [ + '-loglevel', 'quiet', '-protocol_whitelist', 'file,crypto', '-re', '-i', file, + '-map', '0:v:0', '-vf', `fps=${VIDEO_FPS},scale=${STREAM_SIZE.w}:${STREAM_SIZE.h}:force_original_aspect_ratio=decrease:flags=lanczos,pad=${STREAM_SIZE.w}:${STREAM_SIZE.h}:(ow-iw)/2:(oh-ih)/2`, + '-c:v', 'mjpeg', '-q:v', '9', '-f', 'image2pipe', 'pipe:1', + ...(withAudio ? ['-map', '0:a:0', '-ac', '1', '-ar', String(RATE), '-f', 's16le', 'pipe:3'] : []) + ] + }, + { + bin: () => process.env.OASIS_GST_LAUNCH || 'gst-launch-1.0', + args: (file) => [ + '-q', 'filesrc', `location=${gstQuote(file)}`, '!', 'decodebin', 'name=d', + 'd.', '!', 'queue', '!', 'videoconvert', '!', 'videoscale', 'add-borders=true', '!', `video/x-raw,width=${STREAM_SIZE.w},height=${STREAM_SIZE.h},pixel-aspect-ratio=1/1`, '!', 'videorate', '!', `video/x-raw,framerate=${VIDEO_FPS}/1`, '!', 'jpegenc', 'quality=50', '!', 'fdsink', 'fd=1', 'sync=true', + 'd.', '!', 'queue', '!', 'audioconvert', '!', 'audioresample', '!', `audio/x-raw,format=S16LE,rate=${RATE},channels=1`, '!', 'fdsink', 'fd=3', 'sync=true', 'async=false' + ] + } +]; +const STREAM_STALL_MS = 5000; +const STREAM_START_MS = 20000; +const systemStream = () => { + const backend = STREAM_BACKENDS.find(b => hasBin(b.bin())); + if (!backend) return null; + return { + open(file, { onJpeg, onPcm, onEnd }) { + let proc = null; + let frames = 0; + let lastAt = 0; + let ended = false; + const startedAt = Date.now(); + const watch = setInterval(() => { + const now = Date.now(); + if (frames ? now - lastAt > STREAM_STALL_MS : now - startedAt > STREAM_START_MS) finish(); + }, 1000); + if (watch.unref) watch.unref(); + const halt = () => { ended = true; clearInterval(watch); try { if (proc) proc.kill('SIGTERM'); } catch (_) {} }; + const finish = () => { if (ended) return; halt(); onEnd(); }; + const run = (withAudio) => { + const p = spawn(backend.bin(), backend.args(file, withAudio), { stdio: ['ignore', 'pipe', 'ignore', 'pipe'] }); + proc = p; + p.on('error', () => {}); + p.stdout.on('error', () => {}); + p.stdout.on('data', jpegSplitter((jpeg) => { if (ended || proc !== p) return; frames++; lastAt = Date.now(); onJpeg(jpeg); })); + if (p.stdio[3]) { + p.stdio[3].on('error', () => {}); + p.stdio[3].on('data', (pcm) => { if (!ended && proc === p) onPcm(pcm); }); + } + p.on('close', () => { + if (ended || proc !== p) return; + if (withAudio && !frames && backend.retryWithoutAudio) return run(false); + finish(); + }); + }; + run(true); + return { stop() { if (!ended) halt(); } }; + } + }; +}; +let streamFactory = systemStream; + +const videoChunks = (seq, jpeg, track = 0) => { + const n = Math.ceil(jpeg.length / VIDEO_CHUNK); + const out = []; + for (let i = 0; i < n; i++) { + const data = jpeg.subarray(i * VIDEO_CHUNK, (i + 1) * VIDEO_CHUNK); + const plain = Buffer.alloc(Math.ceil((VIDEO_HDR + data.length) / VIDEO_PAD) * VIDEO_PAD); + plain.writeUInt16BE(seq & 0xffff, 0); + plain[2] = i; + plain[3] = n | (track ? VIDEO_FILM : 0); + plain.writeUInt16BE(data.length, 4); + data.copy(plain, VIDEO_HDR); + nodeCrypto.randomFillSync(plain, VIDEO_HDR + data.length); + out.push(plain); + } + return out; +}; +const videoOff = (track = 0) => { const plain = Buffer.alloc(VIDEO_PAD); plain[3] = track ? VIDEO_FILM : 0; nodeCrypto.randomFillSync(plain, VIDEO_HDR); return plain; }; +const videoTrack = (plain) => (Buffer.isBuffer(plain) && plain.length >= VIDEO_HDR && (plain[3] & VIDEO_FILM) ? 1 : 0); +const videoAssembler = () => { + let cur = null; + return (plain) => { + if (!Buffer.isBuffer(plain) || plain.length < VIDEO_HDR) return null; + const seq = plain.readUInt16BE(0), idx = plain[2], n = plain[3] & ~VIDEO_FILM, len = plain.readUInt16BE(4); + if (!n) { cur = null; return 'off'; } + if (idx >= n || VIDEO_HDR + len > plain.length || n * VIDEO_CHUNK > VIDEO_FRAME_MAX) return null; + if (!cur || cur.seq !== seq) cur = { seq, n, parts: new Array(n), got: 0 }; + if (cur.parts[idx]) return null; + cur.parts[idx] = Buffer.from(plain.subarray(VIDEO_HDR, VIDEO_HDR + len)); + cur.got++; + if (cur.got < n) return null; + const jpeg = Buffer.concat(cur.parts); + cur = null; + return jpeg; + }; +}; + +const BEEP_MS = 700; +const playBeep = (audio, count = 1) => { + const on = (count > 1 ? 150 : BEEP_MS) * RATE / 1000; + const gap = 150 * RATE / 1000; + const pcm = Buffer.alloc((on * count + gap * (count - 1)) * 2); + let phase = 0; + for (let b = 0; b < count; b++) { + const start = b * (on + gap); + for (let k = 0; k < on; k++) { + phase += 2 * Math.PI * 1000 / RATE; + pcm.writeInt16LE(Math.round(Math.sin(phase) * 9000), (start + k) * 2); + } + } + const player = audio.player(); + player.write(pcm); + if (typeof player.end === 'function') player.end(); + else setTimeout(() => player.stop(), pcm.length / (RATE * 2) * 1000 + 300); +}; + +const chimeBusy = new WeakMap(); +const playChime = (audio, kind) => { + const notes = CHIMES[kind]; + if (!audio || !notes) return; + const now = Date.now(); + const playing = (chimeBusy.get(audio) || []).filter(t => t > now); + if (playing.length >= CHIMES_AT_ONCE) return; + playing.push(now + chimeMs(kind)); + chimeBusy.set(audio, playing); + const pcm = Buffer.alloc(Math.round(chimeMs(kind) * RATE / 1000) * 2); + let at = 0, phase = 0; + for (const [freq, ms] of notes) { + const n = Math.round(ms * RATE / 1000); + for (let k = 0; k < n && (at + k) * 2 + 1 < pcm.length; k++) { + if (freq > 0) { + phase += 2 * Math.PI * freq / RATE; + const env = Math.min(1, k / 40, (n - k) / 40); + pcm.writeInt16LE(Math.round(Math.sin(phase) * 7000 * env), (at + k) * 2); + } + } + at += n; + } + const player = audio.player(); + player.write(pcm); + if (typeof player.end === 'function') player.end(); + else setTimeout(() => player.stop(), pcm.length / (RATE * 2) * 1000 + 300); +}; +const recordingPrefix = (ref) => nodeCrypto.createHash('sha256').update(String(ref || '')).digest('hex').slice(0, 16); + +const startTone = (audio, kind) => { + const cycle = toneCycle(kind); + const player = audio.player(); + const chunk = RATE * 2 / 10; + let pos = 0; + const tick = () => { + const end = Math.min(pos + chunk, cycle.length); + player.write(cycle.subarray(pos, end)); + pos = end >= cycle.length ? 0 : end; + }; + tick(); + const t = setInterval(tick, 100); + return { stop() { clearInterval(t); player.stop(); } }; +}; + +const mixInto = (acc, pcm) => { for (let i = 0; i < acc.length && i * 2 + 1 < pcm.length; i++) acc[i] += pcm.readInt16LE(i * 2); }; +const clampPcm = (acc) => { + const out = Buffer.alloc(acc.length * 2); + for (let i = 0; i < acc.length; i++) out.writeInt16LE(Math.max(-32768, Math.min(32767, acc[i])), i * 2); + return out; +}; + +let OpusScript = null; +try { OpusScript = require('opusscript'); } catch (_) {} +let sharpLib = null; +try { sharpLib = require('./node_modules/sharp'); } catch (_) {} +const OPUS_TAG = 0xf0; +const OPUS_BITRATE = 16000; +const makeEncoder = (codec) => { + if (codec !== 'opus' || !OpusScript) return { encode: ulawEncode, free() {} }; + let enc = null; + return { + encode(pcm) { + try { + if (!enc) { enc = new OpusScript(RATE, 1, OpusScript.Application.VOIP); try { enc.encoderCTL(4002, OPUS_BITRATE); } catch (_) {} } + return Buffer.concat([Buffer.from([OPUS_TAG]), enc.encode(pcm, pcm.length >> 1)]); + } catch (_) { return ulawEncode(pcm); } + }, + free() { if (enc) { try { enc.delete(); } catch (_) {} enc = null; } } + }; +}; +const makeDecoder = () => { + let dec = null; + return { + decode(buf) { + if (buf.length === FRAME_BYTES / 2 || buf[0] !== OPUS_TAG || !OpusScript) return ulawDecode(buf); + try { + if (!dec) dec = new OpusScript(RATE, 1, OpusScript.Application.VOIP); + return Buffer.from(dec.decode(buf.subarray(1))); + } catch (_) { return Buffer.alloc(FRAME_BYTES); } + }, + free() { if (dec) { try { dec.delete(); } catch (_) {} dec = null; } } + }; +}; +const ctrlFrame = (obj) => Buffer.concat([Buffer.from([K_CTRL]), Buffer.from(JSON.stringify(obj))]); +const levelOf = (pcm) => { + const n = pcm.length >> 1; + let sum = 0; + for (let i = 0; i < n; i++) sum += Math.abs(pcm.readInt16LE(i * 2)); + return n ? sum / n : 0; +}; +const voiceGate = () => { + let floor = 0; + let hang = 0; + return (pcm) => { + const lv = levelOf(pcm); + floor = lv < floor ? lv : floor + (lv - floor) * 0.002; + if (lv > Math.max(VAD_MIN, Math.min(floor, VAD_FLOOR_MAX) * 3)) hang = VAD_HANG; + else if (hang > 0) hang--; + return hang > 0; + }; +}; + +const b64 = (buf) => Buffer.from(buf).toString('base64'); +const isFeedId = (id) => typeof id === 'string' && /^@[A-Za-z0-9+/]{43}=\.ed25519$/.test(id); +const pkOf = (id) => Buffer.from(String(id).slice(1, -8), 'base64'); +const signParts = (keys, parts) => b64(sodium.crypto_sign_detached(Buffer.from(parts.join('|')), Buffer.from(String(keys.private).replace(/\.ed25519$/, ''), 'base64'))); +const verifyParts = (id, parts, sig) => { + try { return !!sodium.crypto_sign_verify_detached(Buffer.from(String(sig), 'base64'), Buffer.from(parts.join('|')), pkOf(id)); } catch (_) { return false; } +}; +const ephKey = (v) => { + try { const b = Buffer.from(String(v), 'base64'); return b.length === 32 ? b : null; } catch (_) { return null; } +}; +const deriveKeys = (mySecret, theirPublic, callId, callerPk, calleePk) => { + const shared = sodium.crypto_scalarmult(mySecret, theirPublic); + const label = (l) => sodium.crypto_hash_sha256(Buffer.concat([Buffer.from(shared), Buffer.from(l), Buffer.from(callId, 'hex'), callerPk, calleePk])); + return { a2b: label('oasis-voices-a2b'), b2a: label('oasis-voices-b2a'), va2b: label('oasis-video-a2b'), vb2a: label('oasis-video-b2a') }; +}; +const sealer = (key) => { + const prefix = nodeCrypto.randomBytes(16); + let counter = 0n; + return (plain) => { + const nonce = Buffer.alloc(24); + prefix.copy(nonce, 0); + nonce.writeBigUInt64BE(++counter, 16); + return Buffer.concat([nonce, Buffer.from(sodium.crypto_secretbox_easy(plain, nonce, key))]); + }; +}; +const opener = (key) => { + let prefix = null; + let last = 0n; + return (frame) => { + if (!Buffer.isBuffer(frame) || frame.length < 40) return null; + const nonce = frame.subarray(0, 24); + const p = nonce.subarray(0, 16).toString('hex'); + const counter = nonce.readBigUInt64BE(16); + if ((prefix && p !== prefix) || counter <= last) return null; + const plain = sodium.crypto_secretbox_open_easy(frame.subarray(24), nonce, key); + if (!plain) return null; + prefix = p; + last = counter; + return Buffer.from(plain); + }; +}; + +module.exports = { + name: 'phone', + version: '1.0.0', + manifest: { + ring: 'async', answer: 'async', hangup: 'async', audio: 'duplex', relay: 'async', relayAudio: 'duplex', video: 'duplex', relayVideo: 'duplex', + call: 'async', accept: 'async', reject: 'async', end: 'async', mute: 'async', silence: 'async', dismiss: 'async', camera: 'async', stream: 'async', videoFrames: 'source', + viewerJoin: 'async', viewerLeave: 'async', viewerRooms: 'sync', publicFeed: 'source', + recordStop: 'async', recordCancel: 'async', + roomHub: 'duplex', roomInfo: 'async', roomAdmits: 'async', roomJoin: 'async', roomLeave: 'async', roomMute: 'async', roomHand: 'async', roomCount: 'async', + roomRecStart: 'async', roomRecStop: 'async', roomNotify: 'async', roomClearEvents: 'async', + roomState: 'sync', roomToken: 'sync', roomHubFor: 'async', + state: 'sync', available: 'sync', pubs: 'sync', events: 'source' + }, + permissions: { anonymous: { allow: ['ring', 'answer', 'hangup', 'audio', 'relay', 'relayAudio', 'video', 'relayVideo', 'roomHub', 'roomInfo', 'roomAdmits'] } }, + init(server, config) { + const keys = config.keys; + const audio = audioFactory(server); + const recBase = String((config && config.path) || os.tmpdir()); + const recDir = path.join(recBase, 'oasis', 'rooms'); + try { if (!fs.existsSync(recDir) && fs.existsSync(path.join(recBase, 'rooms-recordings'))) { fs.mkdirSync(path.dirname(recDir), { recursive: true }); fs.renameSync(path.join(recBase, 'rooms-recordings'), recDir); } } catch (_) {} + const subscribers = new Set(); + const seenCalls = new Map(); + const routes = new Map(); + const hubRooms = new Map(); + let call = null; + let room = null; + const silenced = new Set(); + + const policy = () => { + if (config.phone && typeof config.phone === 'object') return config.phone; + try { + const cfg = require('../configs/config-manager.js').getConfig(); + return { ...(cfg.phone || {}), off: !!(cfg.modules && cfg.modules.phoneMod === 'off') }; + } catch (_) { return {}; } + }; + const ringMs = Number(policy().ringMs) > 0 ? Number(policy().ringMs) : RING_MS; + const voicemailMaxMs = Number(policy().voicemailMaxMs) > 0 ? Number(policy().voicemailMaxMs) : VOICEMAIL_MAX_MS; + const pamWarnMs = Number(policy().pamWarnMs) > 0 ? Number(policy().pamWarnMs) : PAM_WARN_MS; + const roomMax = Number(policy().roomMax) > 0 ? Math.min(ROOM_MAX, Number(policy().roomMax)) : ROOM_MAX; + const roomQuietAt = Number(policy().roomQuietAt) > 0 ? Number(policy().roomQuietAt) : ROOM_QUIET_AT; + const video = videoFactory(server, policy().videoDevice); + const streamer = streamFactory(server); + const videoAllowed = () => policy().video !== false; + const videoOn = () => !!video && videoAllowed(); + const codecsOffered = () => [...(OpusScript && policy().opus !== false ? ['opus'] : []), ...(videoOn() ? ['mjpeg'] : [])]; + const pickCodec = (offer) => (codecsOffered().includes('opus') && Array.isArray(offer) && offer.includes('opus') ? 'opus' : 'ulaw'); + const pickVideo = (offer, mine = false) => (videoAllowed() && ((Array.isArray(offer) && offer.includes('mjpeg')) || mine) ? 'mjpeg' : null); + const frameWatchers = new Map(); + const publishFrame = (target, jpeg) => { const set = frameWatchers.get(target); if (set) for (const p of set) p.push(jpeg); }; + const endFrames = (target) => { + const set = frameWatchers.get(target); + if (!set) return; + frameWatchers.delete(target); + for (const p of set) { try { p.end(); } catch (_) {} } + }; + let cam = null; + let camSeq = 0; + const startCam = (onJpeg) => { + if (cam || !videoOn()) return false; + const cap = video.capture((jpeg) => { + if (!cam || jpeg.length > VIDEO_FRAME_MAX) return; + camSeq = (camSeq + 1) & 0xffff; + publishFrame('me', jpeg); + onJpeg(jpeg, camSeq); + }); + cam = { stop() { cap.stop(); } }; + return true; + }; + const stopCam = () => { if (!cam) return; const c = cam; cam = null; c.stop(); endFrames('me'); }; + const relation = (method, source, dest, done) => { + const friends = server.friends; + if (!friends || typeof friends[method] !== 'function') return done(false); + friends[method]({ source, dest }, (err, yes) => done(!err && !!yes)); + }; + const allowedCaller = (from, done) => { + const p = policy(); + if (p.off || p.dnd === true) return done(false); + relation('isBlocking', server.id, from, (blocked) => { + if (blocked) return done(false); + if (p.visibility !== 'mutuals') return done(true); + relation('isFollowing', server.id, from, (a) => { + if (!a) return done(false); + relation('isFollowing', from, server.id, (b) => done(!!b)); + }); + }); + }; + const relayAllowed = (a, b, done) => { + const p = policy(); + if (p.relay === false) return done(false); + if (p.relayOpen === true) return done(true); + relation('isFollowing', server.id, a, (x) => x ? done(true) : relation('isFollowing', server.id, b, (y) => done(!!y))); + }; + const seal = (payload, to) => { try { return ssbKeys.box(payload, [to]); } catch (_) { return null; } }; + const unseal = (sealed) => { try { return ssbKeys.unbox(String(sealed), keys) || null; } catch (_) { return null; } }; + const opened = (req) => (req && typeof req.sealed === 'string' ? unseal(req.sealed) : req); + const pubKeys = () => { + const set = new Set(); + const add = (list) => { for (const e of list || []) if (Array.isArray(e) && e[1] && e[1].type === 'pub' && e[1].key) set.add(e[1].key); }; + try { add(server.conn.dbPeers()); } catch (_) {} + try { add(server.conn.query().peersAll()); } catch (_) {} + return set; + }; + const knownPubs = () => { + const connected = Object.keys(server.peers || {}).filter(id => id !== server.id && liveRpc(id)); + if (!server.conn || typeof server.conn.query !== 'function') return connected; + const set = pubKeys(); + return connected.filter(id => set.has(id)); + }; + let routesPrunedAt = 0; + const learn = (cid, target, from, forwarded) => { + const now = Date.now(); + if (now - routesPrunedAt > 1000) { + routesPrunedAt = now; + for (const [k, v] of routes) if (now - v.ts > CLOCK_SKEW_MS * 2) routes.delete(k); + } + if (!routes.has(cid) && routes.size >= ROUTES_MAX) return { id: target, via: liveRpc(target) ? target : null }; + const r = routes.get(cid) || { sides: [], ts: now }; + let t = r.sides.find(x => x.id === target) || r.sides.find(x => x.id === null); + if (!t) { t = { id: target, via: null }; r.sides.push(t); } + t.id = target; + if (liveRpc(target)) t.via = target; + let o = r.sides.find(x => x !== t); + if (!o) { o = { id: forwarded ? null : from, via: from }; r.sides.push(o); } + else if (!o.via) o.via = from; + r.ts = now; + routes.set(cid, r); + return t; + }; + let seenPrunedAt = 0; + const directRings = new Map(); + const ringBudget = new Map(); + const trimOldest = (map, max) => { while (map.size > max) map.delete(map.keys().next().value); }; + const firstSeen = (callId) => { + const now = Date.now(); + if (now - seenPrunedAt > 1000) { + seenPrunedAt = now; + for (const [k, t] of seenCalls) if (now - t > CLOCK_SKEW_MS * 2) seenCalls.delete(k); + for (const [k, t] of directRings) if (now - t > CLOCK_SKEW_MS * 2) directRings.delete(k); + for (const [k, list] of ringBudget) { const live = list.filter(t => now - t < RING_WINDOW_MS); if (live.length) ringBudget.set(k, live); else ringBudget.delete(k); } + } + if (seenCalls.has(callId)) return false; + seenCalls.set(callId, now); + trimOldest(seenCalls, SEEN_MAX); + trimOldest(directRings, SEEN_MAX); + return true; + }; + const callerAllowed = (from) => { + const now = Date.now(); + const list = (ringBudget.get(from) || []).filter(t => now - t < RING_WINDOW_MS); + if (list.length >= RING_PER_WINDOW) { ringBudget.set(from, list); return false; } + list.push(now); + ringBudget.set(from, list); + trimOldest(ringBudget, SEEN_MAX); + return true; + }; + + const MOBILE_STATE = path.join(__dirname, '..', '..', '.phone_state.json'); + let mobileCall = null; + let mobileRoom = null; + const writeMobileState = () => { + if (process.env.OASIS_MOBILE !== '1') return; + const s = mobileCall || mobileRoom ? { ...(mobileCall || {}), room: mobileRoom ? { phase: mobileRoom.phase, cam: !!mobileRoom.cam } : null } : null; + try { fs.writeFileSync(MOBILE_STATE, JSON.stringify(s)); } catch (_) {} + }; + writeMobileState(); + const emit = (ev) => { for (const p of subscribers) p.push(ev); }; + const snapshot = () => call ? { + id: call.id, peer: call.peer, dir: call.dir, phase: call.phase, + startedAt: call.startedAt, answeredAt: call.answeredAt || 0, ringUntil: call.ringUntil || 0, + muted: !!call.muted, silenced: silenced.has(call.peer), recordingStartedAt: call.recordingStartedAt || 0, + reached: call.group ? call.legs.some(l => l.reached) : !!call.reached, why: call.why || '', codec: call.codec || '', + video: !!call.video, cam: !!call.cam, peerCam: !!call.peerCam, videoAvailable: videoOn(), + ...(call.group ? { group: true, peers: call.legs.map(l => ({ id: l.peer, phase: l.phase, reached: !!l.reached, silenced: silenced.has(l.peer) })) } : {}) + } : null; + const changed = () => { const s = snapshot(); mobileCall = s; writeMobileState(); emit({ type: 'state', state: s }); }; + const stopTone = (c) => { if (c.tone) { c.tone.stop(); c.tone = null; } }; + const stopRecorder = (c) => { clearTimeout(c.recDelay); clearTimeout(c.recWarn); if (c.recorder) { c.recorder.stop(); c.recorder = null; } clearTimeout(c.recTimer); }; + const startRecording = (c) => { + const max = voicemailMaxMs * RATE * 2 / 1000; + c.rec = { chunks: [], bytes: 0 }; + c.phase = 'recording'; + c.recordingStartedAt = Date.now() + BEEP_MS + 400; + playBeep(audio); + c.recDelay = setTimeout(() => { + if (call !== c || c.phase !== 'recording') return; + c.recordingStartedAt = Date.now(); + c.recorder = audio.capture((pcm) => { + if (!c.rec || c.rec.bytes >= max) return; + const take = Buffer.from(pcm.subarray(0, Math.max(0, max - c.rec.bytes))); + c.rec.chunks.push(take); + c.rec.bytes += take.length; + }); + if (voicemailMaxMs > pamWarnMs) c.recWarn = setTimeout(() => { if (call === c && c.phase === 'recording') playBeep(audio, 2); }, voicemailMaxMs - pamWarnMs); + c.recTimer = setTimeout(() => { + if (call !== c || c.phase !== 'recording') return; + stopRecorder(c); + c.phase = 'recorded'; + changed(); + emit({ type: 'recordLimit', id: c.id }); + }, voicemailMaxMs); + changed(); + }, BEEP_MS + 400); + }; + const finish = (outcome) => { + const c = call; + if (!c) return; + call = null; + clearTimeout(c.timer); + stopTone(c); + stopRecorder(c); + if (c.media) c.media.stop(); + stopCallVideo(c); + if (c.group) { + for (const leg of c.legs) { clearTimeout(leg.timer); if (leg.out) { try { leg.out.end(); } catch (_) {} } } + if (c.mixer) c.mixer.stop(); + for (const leg of c.legs) { if (leg.enc) leg.enc.free(); if (leg.dec) leg.dec.free(); } + } + emit({ type: 'ended', call: { id: c.id, peer: c.peer, ...(c.group ? { peers: c.legs.map(l => l.peer) } : {}), dir: c.dir, startedAt: c.startedAt, answeredAt: c.answeredAt || 0, endedAt: Date.now(), outcome } }); + changed(); + }; + const liveRpc = (id) => { + const list = server.peers && server.peers[id]; + return Array.isArray(list) ? list.find(r => r && !r.closed && r.phone) || null : null; + }; + const sendVia = (via, to, method, payload, cb = () => {}) => { + try { + if (via) { + const r = liveRpc(via); + const sealed = seal(payload, to); + if (!r || !sealed) return cb(new Error('no-connection')); + return r.phone.relay({ to, method, cid: payload.callId, sealed }, cb); + } + const r = liveRpc(to); + if (!r) return cb(new Error('no-connection')); + return r.phone[method](payload, cb); + } catch (err) { cb(err); } + }; + const notifyHangup = (c) => { + const ts = Date.now(); + const payload = { callId: c.id, from: server.id, ts, sig: signParts(keys, ['hangup', c.id, c.peer, ts]) }; + if (c.phase === 'calling' && c.dir === 'out') { + for (const via of c.paths || []) sendVia(via, c.peer, 'hangup', payload, (err) => dbg('hangup sent', sid(c.peer), via ? 'via ' + sid(via) : 'direct', err ? 'error ' + err.message : 'ok')); + } else { + sendVia(c.via || null, c.peer, 'hangup', payload, (err) => dbg('hangup sent', sid(c.peer), c.via ? 'via ' + sid(c.via) : 'direct', err ? 'error ' + err.message : 'ok')); + } + }; + const senderOf = (rpcCtx, payload) => { + const direct = rpcCtx && rpcCtx.id; + const claimed = payload && isFeedId(payload.from) ? payload.from : direct; + return { sender: claimed, via: claimed && direct && claimed !== direct ? direct : null }; + }; + const netFirst = (entries) => entries.slice().sort((x, y) => (/^onion:/.test(String(x && x[0])) ? 1 : 0) - (/^onion:/.test(String(y && y[0])) ? 1 : 0)); + const reach = (id, done) => { + const now = liveRpc(id); + if (now) return done(now); + let entries = []; + try { entries = netFirst(server.conn.query().peersAll()); } catch (_) {} + const hit = entries.find(e => Array.isArray(e) && e[0] && e[1] && e[1].key === id); + if (!hit) return done(null); + try { + server.conn.connect(hit[0], (err, rpc) => done(!err && rpc && rpc.phone ? rpc : liveRpc(id))); + } catch (_) { done(null); } + }; + const relayCandidates = (target) => knownPubs().filter(id => id !== target); + + const calleePubs = (target, done) => { + if (!server.friends || typeof server.friends.hops !== 'function' || !server.conn || typeof server.conn.query !== 'function') return done([]); + try { + server.friends.hops({ start: target, max: 1 }, (err, hops) => { + if (err || !hops) return done([]); + let entries = []; + try { entries = netFirst(server.conn.dbPeers().concat(server.conn.query().peersAll())); } catch (_) {} + const seen = new Set(); + done(entries + .filter(e => Array.isArray(e) && e[0] && e[1] && e[1].type === 'pub' && hops[e[1].key] === 1 && !liveRpc(e[1].key) && !seen.has(e[1].key) && seen.add(e[1].key)) + .slice(0, 5) + .map(e => ({ address: e[0], key: e[1].key }))); + }); + } catch (_) { done([]); } + }; + + const ringPaths = (holder, target, req, alive, onReached, legacy = false) => { + dbg('ring', sid(target), 'legacy=' + legacy, 'live-pubs=' + knownPubs().map(sid).join(',')); + reach(target, (rpc) => { + dbg('direct', sid(target), rpc ? 'reachable' : 'unreachable'); + if (!alive() || !rpc) return; + holder.paths.push(null); + if (legacy) onReached(); + sendVia(null, target, 'ring', req, (err) => { dbg('direct ring', sid(target), err ? 'error ' + err.message : 'delivered'); if (!err && alive()) onReached(); }); + }); + for (const via of relayCandidates(target)) { + holder.paths.push(via); + sendVia(via, target, 'ring', req, (err) => dbg('relay via', sid(via), err ? 'error ' + err.message : 'accepted')); + } + calleePubs(target, (pubs) => { + dbg('callee pubs', pubs.map(p => sid(p.key) + '@' + p.address).join(' ') || 'none'); + for (const entry of pubs) { + try { + server.conn.connect(entry.address, (err) => { + dbg('connect callee pub', sid(entry.key), err ? 'failed ' + err.message : 'ok'); + if (err || !alive()) return; + holder.paths.push(entry.key); + sendVia(entry.key, target, 'ring', req, (e2) => dbg('relay via callee pub', sid(entry.key), e2 ? 'error ' + e2.message : 'accepted')); + }); + } catch (_) {} + } + }); + }; + const ringRequest = (callId, eph, target) => { + const ts = Date.now(); + const ephPk = b64(eph.publicKey); + return { callId, from: server.id, to: target, ephPk, ts, sig: signParts(keys, ['ring', callId, ephPk, target, ts]), codecs: codecsOffered() }; + }; + + const legOf = (c, callId) => (c && c.group ? c.legs.find(l => l.id === callId) || null : null); + const toVoicemail = (c, why) => { + if (call !== c || c.phase !== 'calling') return; + dbg('voicemail', sid(c.peer), why, 'reached=' + !!c.reached); + clearTimeout(c.timer); + stopTone(c); + if (why !== 'busy') notifyHangup(c); + c.why = why; + startRecording(c); + changed(); + }; + const reachedCall = (c) => { + if (call !== c || c.phase !== 'calling' || c.reached) return; + dbg('reached', sid(c.peer), 'now ringing there'); + c.reached = true; + c.ringUntil = Date.now() + ringMs; + stopTone(c); + c.tone = startTone(audio, 'ringback'); + clearTimeout(c.timer); + c.timer = setTimeout(() => toVoicemail(c, 'noanswer'), ringMs); + changed(); + }; + const reachedLeg = (c, leg) => { + if (call !== c || leg.phase !== 'calling' || leg.reached) return; + leg.reached = true; + clearTimeout(leg.timer); + leg.timer = setTimeout(() => { if (call === c && leg.phase === 'calling') dropLeg(c, leg, true); }, ringMs); + if (c.phase === 'calling' && !c.ringing) { stopTone(c); c.ringing = true; c.tone = startTone(audio, 'ringback'); c.ringUntil = Date.now() + ringMs; } + changed(); + }; + const ringingAck = (c, req, from, cb) => { + const target = c && c.group ? legOf(c, req.callId) : c; + if (!target || c.dir !== 'out' || target.phase !== 'calling' || from !== target.peer || req.callId !== target.id) return cb(new Error('no-call')); + if (Math.abs(Date.now() - Number(req.ts)) > CLOCK_SKEW_MS || !verifyParts(from, ['ringing', req.callId, req.ringing, server.id, req.ts], req.sig)) return cb(new Error('rejected')); + cb(null, true); + if (req.ringing === 'busy') { + if (c.group) { target.reached = true; return dropLeg(c, target, false); } + c.reached = true; + return toVoicemail(c, 'busy'); + } + if (c.group) reachedLeg(c, target); + else reachedCall(c); + }; + const hangupLeg = (leg) => { + const ts = Date.now(); + const payload = { callId: leg.id, from: server.id, ts, sig: signParts(keys, ['hangup', leg.id, leg.peer, ts]) }; + if (leg.phase === 'calling') for (const via of leg.paths) sendVia(via, leg.peer, 'hangup', payload); + else sendVia(leg.via || null, leg.peer, 'hangup', payload); + }; + const settleGroup = (c) => { + if (call !== c) return; + if (c.legs.some(l => l.phase === 'connected' || l.phase === 'calling')) return changed(); + finish(c.answeredAt ? 'ended' : 'noanswer'); + }; + const dropLeg = (c, leg, notify) => { + if (leg.phase === 'gone') return; + if (notify) hangupLeg(leg); + clearTimeout(leg.timer); + leg.phase = 'gone'; + if (leg.out) { try { leg.out.end(); } catch (_) {} leg.out = null; } + if (leg.enc) leg.enc.free(); + if (leg.dec) leg.dec.free(); + leg.queue = []; + settleGroup(c); + }; + const startMixer = (c) => { + if (c.mixer) return; + const player = audio.player(); + const samples = FRAME_BYTES / 2; + let pending = Buffer.alloc(0); + const capture = audio.capture((pcm) => { + if (call !== c) return; + pending = pending.length ? Buffer.concat([pending, pcm]) : Buffer.from(pcm); + while (pending.length >= FRAME_BYTES) { + const mic = pending.subarray(0, FRAME_BYTES); + pending = pending.subarray(FRAME_BYTES); + const legs = c.legs.filter(l => l.phase === 'connected' && l.out); + const heard = legs.map(l => l.queue.shift() || null); + const all = new Int32Array(samples); + heard.forEach((f, i) => { if (f && !silenced.has(legs[i].peer)) mixInto(all, f); }); + player.write(clampPcm(all)); + legs.forEach((leg, i) => { + const acc = new Int32Array(samples); + if (!c.muted) mixInto(acc, mic); + heard.forEach((f, j) => { if (f && j !== i) mixInto(acc, f); }); + leg.out.push(leg.seal(leg.enc.encode(clampPcm(acc)))); + }); + } + }); + c.mixer = { stop() { capture.stop(); player.stop(); } }; + }; + const startLegMedia = (c, leg, duplex) => { + const open = opener(leg.keys.b2a); + leg.seal = sealer(leg.keys.a2b); + leg.enc = makeEncoder(leg.codec); + leg.dec = makeDecoder(); + leg.out = Pushable(() => {}); + leg.queue = []; + pull(leg.out, duplex.sink); + pull(duplex.source, pull.drain((frame) => { + const plain = open(Buffer.isBuffer(frame) ? frame : Buffer.from(frame || [])); + if (!plain || leg.phase !== 'connected') return; + leg.queue.push(leg.dec.decode(plain)); + if (leg.queue.length > MIX_QUEUE) leg.queue.shift(); + }, () => { if (call === c) dropLeg(c, leg, false); })); + startMixer(c); + }; + const groupCall = (targets, legacy, cb) => { + const startedAt = Date.now(); + const c = { id: nodeCrypto.randomBytes(16).toString('hex'), peer: targets[0], dir: 'out', group: true, phase: 'calling', startedAt, ringUntil: startedAt + ringMs, legs: [] }; + call = c; + c.tone = startTone(audio, 'connecting'); + for (const peer of targets) { + const leg = { id: nodeCrypto.randomBytes(16).toString('hex'), peer, phase: 'calling', eph: sodium.crypto_box_keypair(), paths: [], via: null, queue: [] }; + c.legs.push(leg); + leg.timer = setTimeout(() => { if (call === c && leg.phase === 'calling') dropLeg(c, leg, true); }, ringMs); + ringPaths(leg, peer, ringRequest(leg.id, leg.eph, peer), () => call === c && leg.phase === 'calling', () => reachedLeg(c, leg), legacy.has(peer)); + } + changed(); + cb(null, snapshot()); + }; + const groupAnswer = (c, req, from, via, cb) => { + const leg = legOf(c, req && req.callId); + if (!leg || leg.phase !== 'calling' || from !== leg.peer) return cb(new Error('no-call')); + const calleePk = ephKey(req.ephPk); + if (!calleePk || !verifyParts(from, ['answer', req.callId, req.ephPk, server.id, req.ts], req.sig)) return cb(new Error('rejected')); + const path = via ? liveRpc(via) : liveRpc(from); + if (!path) return cb(new Error('no-connection')); + leg.keys = deriveKeys(leg.eph.secretKey, calleePk, leg.id, Buffer.from(leg.eph.publicKey), calleePk); + leg.codec = pickCodec([req.codec]); + clearTimeout(leg.timer); + leg.via = via; + leg.phase = 'connected'; + leg.answeredAt = Date.now(); + if (c.phase === 'calling') { stopTone(c); c.phase = 'connected'; c.answeredAt = leg.answeredAt; } + cb(null, true); + changed(); + const quiet = () => {}; + startLegMedia(c, leg, via ? path.phone.relayAudio({ to: leg.peer, callId: leg.id }, quiet) : path.phone.audio({ callId: leg.id }, quiet)); + }; + + const startMedia = (c, duplex, role) => { + const seal = sealer(role === 'out' ? c.keys.a2b : c.keys.b2a); + const open = opener(role === 'out' ? c.keys.b2a : c.keys.a2b); + const out = Pushable(() => {}); + const player = audio.player(); + const enc = makeEncoder(c.codec); + const dec = makeDecoder(); + let pending = Buffer.alloc(0); + const capture = audio.capture((pcm) => { + if (call !== c || c.phase !== 'connected') return; + pending = pending.length ? Buffer.concat([pending, pcm]) : Buffer.from(pcm); + while (pending.length >= FRAME_BYTES) { + const frame = pending.subarray(0, FRAME_BYTES); + pending = pending.subarray(FRAME_BYTES); + if (!c.muted) out.push(seal(enc.encode(frame))); + } + }); + const incoming = []; + c.audioLag = () => incoming.length * FRAME_MS; + const pace = setInterval(() => { + if (call !== c) return; + if (incoming.length > MIX_QUEUE) incoming.splice(0, incoming.length - MIX_QUEUE); + const next = incoming.shift(); + if (next) player.write(next); + }, FRAME_MS); + if (pace.unref) pace.unref(); + const sink = pull.drain((frame) => { + const plain = open(Buffer.isBuffer(frame) ? frame : Buffer.from(frame || [])); + if (plain && !silenced.has(c.peer)) incoming.push(dec.decode(plain)); + }, () => { if (call === c) finish('ended'); }); + c.media = { stop() { clearInterval(pace); try { out.end(); } catch (_) {} capture.stop(); player.stop(); enc.free(); dec.free(); } }; + if (!duplex) return { source: out, sink }; + pull(out, duplex.sink); + pull(duplex.source, sink); + return null; + }; + + const peerFrameSeen = (c) => { + clearTimeout(c.peerTimer); + c.peerTimer = setTimeout(() => { if (call === c && c.peerCam) { c.peerCam = false; endFrames(c.peer); changed(); } }, VIDEO_HOLD_MS); + if (c.peerTimer.unref) c.peerTimer.unref(); + if (!c.peerCam) { c.peerCam = true; changed(); } + }; + const startVideo = (c, duplex, role) => { + const seal = sealer(role === 'out' ? c.keys.va2b : c.keys.vb2a); + const open = opener(role === 'out' ? c.keys.vb2a : c.keys.va2b); + const out = Pushable(() => {}); + const assemble = videoAssembler(); + const sink = pull.drain((frame) => { + const plain = open(Buffer.isBuffer(frame) ? frame : Buffer.from(frame || [])); + if (!plain || call !== c) return; + const got = assemble(plain); + if (got === 'off') { clearTimeout(c.peerTimer); if (c.peerCam) { c.peerCam = false; endFrames(c.peer); changed(); } return; } + if (!got) return; + peerFrameSeen(c); + const hold = setTimeout(() => { if (call === c && c.peerCam) publishFrame(c.peer, got); }, typeof c.audioLag === 'function' ? c.audioLag() : 0); + if (hold.unref) hold.unref(); + }, () => { if (call === c) stopCallVideo(c); }); + c.vmedia = { out, seal, stop() { try { out.end(); } catch (_) {} } }; + if (!duplex) return { source: out, sink }; + pull(out, duplex.sink); + pull(duplex.source, sink); + return null; + }; + const stopCallVideo = (c) => { + clearTimeout(c.peerTimer); + if (c.cam) { c.cam = false; stopCam(); } + if (c.vmedia) { c.vmedia.stop(); c.vmedia = null; } + endFrames(c.peer); + c.peerCam = false; + }; + const sendCallVideo = (c, plain) => { if (c.vmedia) c.vmedia.out.push(c.vmedia.seal(plain)); }; + + const deliver = (m, frame, droppable) => { + if (droppable && m.sent - m.acked > HUB_LAG_MAX) return; + m.sent++; + m.out.push(frame); + }; + const hubBroadcast = (r, from, frame, droppable) => { + for (const [slot, m] of r.members) { + if (slot !== from) deliver(m, frame, droppable); + } + }; + const hubHumans = (h) => [...h.members.values()].filter(m => !m.viewer).length; + const hubAttach = (rid, id, out, scope = null, viewer = false) => { + let r = hubRooms.get(rid); + if (!r) { + if (hubRooms.size >= HUB_ROOMS_MAX) return null; + if (scope && [...hubRooms.values()].filter(h => h.scope === scope).length >= HUB_RIDS_PER_ROOM) return null; + r = { members: new Map(), scope }; + hubRooms.set(rid, r); + } + if (scope && r.scope !== scope) return null; + for (const old of [...r.members.values()]) if (old.id === id) old.kick(); + if (!hubRooms.has(rid)) hubRooms.set(rid, r); + if (r.members.size >= roomMax) return null; + let slot = 0; + while (r.members.has(slot)) slot++; + const peers = [...r.members].map(([s, m]) => (m.viewer ? { slot: s, id: m.id, viewer: true } : { slot: s, id: m.id })); + const m = { id, out, sent: 0, acked: 0, viewer: !!viewer }; + m.leave = () => { + if (r.members.get(slot) !== m) return; + r.members.delete(slot); + if (r.members.size) hubBroadcast(r, slot, ctrlFrame({ t: 'leave', slot }), false); + else if (hubRooms.get(rid) === r) hubRooms.delete(rid); + }; + m.kick = () => { m.leave(); try { out.end(); } catch (_) {} }; + m.handle = (frame) => { + if (r.members.get(slot) !== m || frame.length < 2) return; + const kind = frame[0]; + if (kind === K_VIDEO) { + if (frame.length > ROOM_VIDEO_FRAME_MAX || policy().video === false) return; + const at = Date.now(); + if (at - (m.vwindowAt || 0) >= 1000) { m.vwindowAt = at; m.vbytes = 0; } + m.vbytes = (m.vbytes || 0) + frame.length; + if (m.vbytes > ROOM_VIDEO_BYTES_PER_S) return; + hubBroadcast(r, slot, Buffer.concat([Buffer.from([K_VIDEO, slot]), frame.subarray(1)]), true); + return; + } + if (frame.length > ROOM_FRAME_MAX) return; + if (kind === K_CTRL) { + let msg = null; + try { msg = JSON.parse(frame.subarray(1).toString('utf8')); } catch (_) {} + if (msg && msg.t === 'bye') return m.kick(); + const n = msg && msg.t === 'ack' ? Number(msg.n) : NaN; + if (Number.isFinite(n) && n > m.acked && n <= m.sent) m.acked = n; + return; + } + const now = Date.now(); + if (now - (m.windowAt || 0) >= 1000) { m.windowAt = now; m.windowCount = 0; } + if (++m.windowCount > ROOM_RATE_MAX) return; + if (kind === K_AUDIO || kind === K_DATA) { + hubBroadcast(r, slot, Buffer.concat([Buffer.from([kind, slot]), frame.subarray(1)]), true); + } else if (kind === K_KEY && frame.length > 2 && frame[1] !== slot) { + const target = r.members.get(frame[1]); + if (target) deliver(target, Buffer.concat([Buffer.from([K_KEY, slot]), frame.subarray(2)]), false); + } + }; + r.members.set(slot, m); + deliver(m, ctrlFrame({ t: 'hello', slot, peers, max: roomMax, video: policy().video !== false }), false); + hubBroadcast(r, slot, ctrlFrame(viewer ? { t: 'join', slot, id, viewer: true } : { t: 'join', slot, id }), false); + return m; + }; + const hubDuplex = (from, opts) => { + let member = null; + let closed = false; + const out = Pushable(() => { closed = true; if (member) member.leave(); }); + const sink = pull.drain((frame) => { + if (member) member.handle(Buffer.isBuffer(frame) ? frame : Buffer.from(frame || [])); + }, () => { closed = true; if (member) member.leave(); try { out.end(); } catch (_) {} }); + const refuse = (t) => { out.push(ctrlFrame({ t })); out.end(); }; + const rid = String((opts && opts.rid) || ''); + const roomId = String((opts && opts.roomId) || ''); + const tokenOk = roomId ? (RID.test(roomId) && verifyParts(opts.owner, ['room', roomId], opts.token)) : verifyParts(opts.owner, ['room', rid], opts.token); + if (!from || !RID.test(rid) || !isFeedId(opts.owner) || !tokenOk) { + refuse('refused'); + return { source: out, sink }; + } + const admit = () => { + if (closed) return; + member = hubAttach(rid, from, out, roomId ? `${opts.owner}|${roomId}` : null, opts.viewer === true); + if (!member) refuse('full'); + }; + if (opts.owner === server.id || from === server.id) admit(); + else relayAllowed(opts.owner, from, (ok) => (ok ? admit() : refuse('refused'))); + return { source: out, sink }; + }; + + const viewers = new Map(); + const active = (r) => !!r && (r === room || viewers.get(r.rid) === r); + const touched = (r) => { if (r === room) roomChanged(); }; + const humansIn = (r) => [...r.peers.values()].filter(p => !p.viewer).length; + const roomSnapshot = () => { + const r = room; + if (!r) return null; + const now = Date.now(); + return { + rid: r.rid, ref: r.ref, title: r.title, phase: r.phase, joinedAt: r.joinedAt, muted: !!r.muted, hand: !!r.hand, handSince: r.hand ? r.handAt : 0, + secure: !!r.kid, count: humansIn(r) + 1, max: r.max, public: [...r.peers.values()].some(p => p.viewer), speaking: now - r.sentAt < 1000, + cam: !!r.cam, videoAvailable: videoOn() && r.hubVideo === true, grid: !!sharpLib, gridOrder: gridIds(r), + streaming: r.stream ? r.stream.name : null, streamAvailable: fileStreamOn() && r.hubVideo === true, + notify: !!r.notify, recording: !!r.rec, recordingSince: r.rec ? r.rec.at : 0, recordingBy: [...r.recPeers], + events: r.notify ? r.events.slice(-ROOM_EVENTS_MAX) : [], + peers: [...r.peers.values()].map(p => ({ id: p.id, speaking: now - p.heardAt < 1000, muted: now - p.mutedAt < MUTE_TTL_MS, hand: now - (p.handBeat || 0) < MUTE_TTL_MS, handSince: p.handSince || 0, silenced: silenced.has(p.id), recording: r.recPeers.has(p.id), cam: now - (p.frameAt || 0) < VIDEO_HOLD_MS, film: now - (p.filmAt || 0) < VIDEO_HOLD_MS, viewer: !!p.viewer })) + }; + }; + const roomChanged = () => { const r = roomSnapshot(); mobileRoom = r; writeMobileState(); emit({ type: 'room', room: r }); }; + const pushEvent = (r, t, id) => { + r.events.push({ t, id, ts: Date.now() }); + if (r.events.length > ROOM_EVENTS_MAX) r.events.splice(0, r.events.length - ROOM_EVENTS_MAX); + touched(r); + }; + const startRoomRec = (r) => { + if (!r || r.rec || r.phase !== 'live') return false; + try { + fs.mkdirSync(recDir, { recursive: true, mode: 0o700 }); + try { fs.chmodSync(recDir, 0o700); } catch (_) {} + const file = path.join(recDir, `${recordingPrefix(r.ref)}-${Date.now()}.wav`); + const fd = fs.openSync(file, 'w', 0o600); + fs.writeSync(fd, wavFile(Buffer.alloc(0))); + r.rec = { fd, file, bytes: 0, at: Date.now() }; + } catch (_) { return false; } + sendRoomData(r, { t: 'rec', on: true }); + playChime(audio, 'recStart'); + pushEvent(r, 'recStart', server.id); + return true; + }; + const stopRoomRec = (r) => { + if (!r || !r.rec) return false; + const rec = r.rec; + r.rec = null; + try { + const h = Buffer.alloc(4); + h.writeUInt32LE(36 + rec.bytes, 0); fs.writeSync(rec.fd, h, 0, 4, 4); + h.writeUInt32LE(rec.bytes, 0); fs.writeSync(rec.fd, h, 0, 4, 40); + fs.closeSync(rec.fd); + if (!rec.bytes) fs.unlinkSync(rec.file); + } catch (_) {} + if (room === r) { + sendRoomData(r, { t: 'rec', on: false }); + playChime(audio, 'recStop'); + pushEvent(r, 'recStop', server.id); + } + if (rec.bytes) emit({ type: 'roomRecorded', room: { ref: r.ref, title: r.title }, file: rec.file, bytes: rec.bytes, durationMs: Math.round(rec.bytes / (RATE * 2) * 1000) }); + return true; + }; + const keeperSlot = (r) => r.order[0]; + const trimKeys = (r) => { while (r.keys.size > 3) r.keys.delete(r.keys.keys().next().value); }; + const useKey = (r, kid, key) => { + if (r.kid === kid && r.sealer && r.keys.get(kid) && r.keys.get(kid).equals(key)) return; + r.keys.set(kid, key); + trimKeys(r); + r.kid = kid; + r.sealer = sealer(key); + sendCodecs(r); + }; + const newRoomKey = (r) => useKey(r, nodeCrypto.randomBytes(4).toString('hex'), nodeCrypto.randomBytes(32)); + const sendRoomKey = (r, slot) => { + const p = r.peers.get(slot); + const key = r.kid && r.keys.get(r.kid); + if (!p || !key) return; + const ts = Date.now(); + const sealed = seal({ rid: r.rid, kid: r.kid, key: b64(key), from: server.id, ts, sig: signParts(keys, ['roomkey', r.rid, r.kid, p.id, ts]) }, p.id); + if (sealed) r.out.push(Buffer.concat([Buffer.from([K_KEY, slot]), Buffer.from(sealed)])); + }; + const peerEntry = (id, viewer = false) => ({ id, viewer: !!viewer, queue: [], openers: new Map(), heardAt: 0, mutedAt: 0, dec: null, opus: false, greeted: false }); + const dropPeer = (r, slot) => { const p = r.peers.get(slot); if (p) { if (p.dec) p.dec.free(); clearTimeout(p.vtimer); clearTimeout(p.ftimer); endFrames(r.tag + p.id); endFrames(r.tag + filmKey(p.id)); gridNote(r, p.id, null); gridNote(r, filmKey(p.id), null); } r.peers.delete(slot); }; + const sendCodecs = (r) => { if (r.out && r.sealer && codecsOffered().length) sendRoomData(r, { t: 'codecs', codecs: codecsOffered() }); }; + const onRoomCtrl = (r, msg) => { + if (msg.t === 'hello' && r.slot === null) { + r.slot = Number(msg.slot); + r.max = Number(msg.max) > 0 ? Number(msg.max) : ROOM_MAX; + r.hubVideo = msg.video === true; + for (const p of Array.isArray(msg.peers) ? msg.peers : []) { + if (!p || !isFeedId(p.id)) continue; + r.peers.set(Number(p.slot), peerEntry(p.id, p.viewer === true)); + r.order.push(Number(p.slot)); + } + r.order.push(r.slot); + if (r.peers.size >= roomQuietAt) r.muted = true; + if (!r.static && keeperSlot(r) === r.slot) newRoomKey(r); + r.phase = 'live'; + if (r.ready) return r.ready(null); + } else if (msg.t === 'join' && isFeedId(msg.id)) { + const slot = Number(msg.slot); + dropPeer(r, slot); + r.peers.set(slot, peerEntry(msg.id, msg.viewer === true)); + r.order = r.order.filter(s => s !== slot).concat(slot); + if (!r.static && keeperSlot(r) === r.slot) sendRoomKey(r, slot); + if (r.notify && msg.viewer !== true) playChime(audio, 'join'); + pushEvent(r, 'join', msg.id); + } else if (msg.t === 'leave') { + const slot = Number(msg.slot); + const wasKeeper = keeperSlot(r) === slot; + const gone = r.peers.get(slot); + dropPeer(r, slot); + r.order = r.order.filter(s => s !== slot); + if (gone) { + if (r.recPeers.delete(gone.id)) pushEvent(r, 'recStop', gone.id); + if (r.notify) playChime(audio, 'leave'); + pushEvent(r, 'leave', gone.id); + } + if (!r.static && keeperSlot(r) === r.slot && (wasKeeper || !r.kid)) { + newRoomKey(r); + for (const s of r.peers.keys()) sendRoomKey(r, s); + } + } else if (msg.t === 'full' || msg.t === 'refused') { + if (r.ready) return r.ready(new Error(msg.t)); + } + touched(r); + }; + const onRoomKey = (r, slot, payload) => { + const p = r.peers.get(slot); + if (r.static || !p || keeperSlot(r) !== slot) return; + const msg = unseal(payload.toString('utf8')); + if (!msg || msg.rid !== r.rid || msg.from !== p.id || !/^[0-9a-f]{8}$/.test(String(msg.kid))) return; + if (!verifyParts(p.id, ['roomkey', r.rid, msg.kid, server.id, msg.ts], msg.sig)) return; + const key = Buffer.from(String(msg.key || ''), 'base64'); + if (key.length !== 32) return; + useKey(r, msg.kid, key); + touched(r); + }; + const openRoomFrame = (r, p, payload) => { + if (!p || payload.length < 44) return null; + const kid = payload.subarray(0, 4).toString('hex'); + const key = r.keys.get(kid); + if (!key) return null; + let open = p.openers.get(kid); + if (!open) { open = opener(key); p.openers.set(kid, open); } + return open(payload.subarray(4)); + }; + const sendRoomData = (r, msg) => { + if (!r.sealer) return; + r.out.push(Buffer.concat([Buffer.from([K_DATA]), Buffer.from(r.kid, 'hex'), r.sealer(Buffer.from(JSON.stringify(msg)))])); + r.beaconAt = Date.now(); + }; + const onRoomAudio = (r, slot, payload) => { + const p = r.peers.get(slot); + const plain = openRoomFrame(r, p, payload); + if (!plain) return; + if (!p.dec) p.dec = makeDecoder(); + p.queue.push(p.dec.decode(plain)); + if (p.queue.length > MIX_QUEUE) p.queue.shift(); + p.heardAt = Date.now(); + p.mutedAt = 0; + noteMute(r, p, false); + }; + const noteMute = (r, p, on, quiet = false) => { + if (!!p.muted === on) return; + p.muted = on; + if (quiet) return touched(r); + const now = Date.now(); + const announce = now - (p.muteToggleAt || 0) >= REC_TOGGLE_MIN_MS; + p.muteToggleAt = now; + if (!announce) return touched(r); + if (r.notify) playChime(audio, on ? 'mute' : 'unmute'); + pushEvent(r, on ? 'mute' : 'unmute', p.id); + }; + const noteHand = (r, p, on, at, quiet = false) => { + if (!!p.hand === on) return; + p.hand = on; + const now = Date.now(); + p.handSince = on ? (quiet && Number.isFinite(at) && at > 0 ? Math.min(at, now) : now) : 0; + if (!on || quiet) return touched(r); + const announce = now - (p.handToggleAt || 0) >= REC_TOGGLE_MIN_MS; + p.handToggleAt = now; + if (!announce) return touched(r); + if (r.notify) playChime(audio, 'hand'); + pushEvent(r, 'hand', p.id); + }; + const onRoomData = (r, slot, payload) => { + const p = r.peers.get(slot); + const plain = openRoomFrame(r, p, payload); + if (!plain) return; + let msg = null; + try { msg = JSON.parse(plain.toString('utf8')); } catch (_) {} + if (msg && msg.t === 'codecs') { + p.opus = Array.isArray(msg.codecs) && msg.codecs.includes('opus'); + if (!p.greeted) { p.greeted = true; sendCodecs(r); } + return; + } + if (msg && msg.t === 'rec') { + const was = r.recPeers.has(p.id); + if (!!msg.on === was) return; + const now = Date.now(); + const announce = now - (p.recToggleAt || 0) >= REC_TOGGLE_MIN_MS; + p.recToggleAt = now; + if (msg.on) r.recPeers.add(p.id); else r.recPeers.delete(p.id); + if (announce) { playChime(audio, msg.on ? 'recStart' : 'recStop'); pushEvent(r, msg.on ? 'recStart' : 'recStop', p.id); } + else touched(r); + return; + } + if (msg && msg.t === 'hand') { + p.handBeat = msg.on ? Date.now() : 0; + noteHand(r, p, !!msg.on, Number(msg.at), msg.beacon === true); + return; + } + if (!msg || msg.t !== 'mute') return; + p.mutedAt = msg.on ? Date.now() : 0; + noteMute(r, p, !!msg.on, msg.beacon === true); + }; + const gridIds = (r) => { + const now = Date.now(); + const ids = []; + if (r.cam) ids.push(server.id); + if (r.stream) ids.push(filmKey(server.id)); + for (const slot of r.order) { + const p = r.peers.get(slot); + if (!p) continue; + if (now - (p.frameAt || 0) < VIDEO_HOLD_MS) ids.push(p.id); + if (now - (p.filmAt || 0) < VIDEO_HOLD_MS) ids.push(filmKey(p.id)); + } + return ids; + }; + const newGrid = () => ({ frames: new Map(), busy: false, dirty: false, timer: null }); + const composeGrid = async (r) => { + const g = r.grid; + const ids = gridIds(r).filter(id => g.frames.has(id)); + if (!ids.length) { + if (r.tag && !g.empty) { g.empty = true; const b = await blankFrame(); if (b && active(r)) publishFrame(r.tag + 'grid', b); } + return; + } + g.empty = false; + if (ids.length === 1 && ids[0] !== server.id) { if (active(r)) publishFrame(r.tag + 'grid', g.frames.get(ids[0])); return; } + const size = ids.length <= GRID_LARGE_MAX ? GRID_TILE_LARGE : VIDEO_SIZE; + const cols = Math.ceil(Math.sqrt(ids.length)); + const rows = Math.ceil(ids.length / cols); + const tiles = []; + for (let i = 0; i < ids.length; i++) { + try { + let tile = sharpLib(g.frames.get(ids[i])).resize(size.w, size.h, { fit: 'contain', background: '#000000' }); + if (ids[i] === server.id) tile = tile.flop(); + tiles.push({ input: await tile.toBuffer(), left: (i % cols) * size.w, top: Math.floor(i / cols) * size.h }); + } catch (_) {} + } + if (!tiles.length) return; + const jpeg = await sharpLib({ create: { width: cols * size.w, height: rows * size.h, channels: 3, background: '#000000' } }).composite(tiles).jpeg({ quality: 80 }).toBuffer(); + if (active(r)) publishFrame(r.tag + 'grid', jpeg); + }; + const gridTick = (r) => { + const g = r && r.grid; + if (!g || !active(r) || !sharpLib || g.busy || !g.dirty || !frameWatchers.has(r.tag + 'grid')) return; + g.dirty = false; + g.busy = true; + composeGrid(r).catch(() => {}).then(() => { g.busy = false; }); + }; + const gridNote = (r, id, jpeg) => { if (!r || !r.grid) return; if (jpeg) r.grid.frames.set(id, jpeg); else r.grid.frames.delete(id); r.grid.dirty = true; }; + const startGrid = (r) => { if (!sharpLib || !r.grid || r.grid.timer) return; r.grid.timer = setInterval(() => gridTick(r), Math.round(1000 / VIDEO_FPS)); if (r.grid.timer.unref) r.grid.timer.unref(); }; + const stopGrid = (r) => { if (!r || !r.grid) return; clearInterval(r.grid.timer); r.grid.timer = null; r.grid.frames.clear(); r.grid.dirty = false; endFrames(r.tag + 'grid'); }; + const onRoomVideo = (r, slot, payload) => { + const p = r.peers.get(slot); + if (!videoAllowed()) return; + const plain = openRoomFrame(r, p, payload); + if (!plain) return; + const film = videoTrack(plain) === 1; + const key = film ? filmKey(p.id) : p.id; + const at = film ? 'filmAt' : 'frameAt'; + const timer = film ? 'ftimer' : 'vtimer'; + const asm = film ? 'fasm' : 'vasm'; + if (!p[asm]) p[asm] = videoAssembler(); + const got = p[asm](plain); + if (got === 'off') { clearTimeout(p[timer]); if (p[at]) { p[at] = 0; endFrames(r.tag + key); gridNote(r, key, null); touched(r); } return; } + if (!got) return; + const fresh = !(Date.now() - (p[at] || 0) < VIDEO_HOLD_MS); + p[at] = Date.now(); + clearTimeout(p[timer]); + p[timer] = setTimeout(() => { if (active(r)) { endFrames(r.tag + key); gridNote(r, key, null); touched(r); } }, VIDEO_HOLD_MS); + if (p[timer].unref) p[timer].unref(); + const hold = setTimeout(() => { if (!active(r) || !(Date.now() - (p[at] || 0) < VIDEO_HOLD_MS)) return; publishFrame(r.tag + key, got); gridNote(r, key, got); }, p.queue.length * FRAME_MS); + if (hold.unref) hold.unref(); + if (fresh) touched(r); + }; + const sendRoomVideo = (r, plain) => { + if (!r.sealer || !r.hubVideo) return; + r.out.push(Buffer.concat([Buffer.from([K_VIDEO]), Buffer.from(r.kid, 'hex'), r.sealer(plain)])); + }; + const fileStreamOn = () => videoAllowed() && !!streamer; + const streamFile = (file) => { + const raw = String(file || ''); + if (!path.isAbsolute(raw)) return null; + try { const full = fs.realpathSync(raw); return fs.statSync(full).isFile() ? full : null; } catch (_) { return null; } + }; + const filmKey = (id) => `film:${id}`; + const videoBudget = (r, chunks) => { + const bytes = chunks.reduce((n, c) => n + c.length + ROOM_VIDEO_CHUNK_EXTRA, 0); + const now = Date.now(); + const sent = r.vsent || (r.vsent = []); + while (sent.length && now - sent[0][0] >= 1000) sent.shift(); + if (sent.reduce((n, e) => n + e[1], 0) + bytes > ROOM_VIDEO_SEND_MAX) return false; + sent.push([now, bytes]); + return true; + }; + const sendRoomJpeg = (r, seq, jpeg, track = 0) => { + const chunks = videoChunks(seq, jpeg, track); + if (!videoBudget(r, chunks)) return; + for (const plain of chunks) sendRoomVideo(r, plain); + }; + const stopRoomStream = (r, notify = true) => { + const s = r && r.stream; + if (!s) return false; + r.stream = null; + try { if (s.handle) s.handle.stop(); } catch (_) {} + endFrames('film:me'); + gridNote(r, filmKey(server.id), null); + if (room === r) sendRoomVideo(r, videoOff(1)); + if (notify) roomChanged(); + return true; + }; + const startRoomStream = (r, file, name) => { + if (!r || r.phase !== 'live' || !r.hubVideo || !fileStreamOn()) return false; + stopRoomStream(r, false); + const s = { name: String(name || '').slice(0, 120), pcm: Buffer.alloc(0), seq: 0, handle: null }; + r.stream = s; + try { + s.handle = streamer.open(file, { + onJpeg: (jpeg) => { + if (r.stream !== s || room !== r || !Buffer.isBuffer(jpeg) || jpeg.length > VIDEO_FRAME_MAX) return; + s.seq = (s.seq + 1) & 0xffff; + publishFrame('film:me', jpeg); + gridNote(r, filmKey(server.id), jpeg); + sendRoomJpeg(r, s.seq, jpeg, 1); + }, + onPcm: (pcm) => { + if (r.stream !== s || !pcm || !pcm.length) return; + s.pcm = s.pcm.length ? Buffer.concat([s.pcm, pcm]) : Buffer.from(pcm); + const drop = (s.pcm.length - RATE * 2) & ~1; + if (drop > 0) s.pcm = s.pcm.subarray(drop); + }, + onEnd: () => { if (r.stream === s) stopRoomStream(r); } + }); + } catch (_) { s.handle = null; } + if (!s.handle || r.stream !== s) { if (r.stream === s) r.stream = null; return false; } + return true; + }; + const streamFrame = (r) => { + const s = r.stream; + if (!s || s.pcm.length < FRAME_BYTES) return null; + const f = s.pcm.subarray(0, FRAME_BYTES); + s.pcm = s.pcm.subarray(FRAME_BYTES); + return f; + }; + const onRoomFrame = (r, frame) => { + if (!active(r)) return; + const buf = Buffer.isBuffer(frame) ? frame : Buffer.from(frame || []); + if (!buf.length) return; + r.received = (r.received || 0) + 1; + if (r.received - (r.ackedAt || 0) >= ROOM_ACK_EVERY) { r.ackedAt = r.received; r.out.push(ctrlFrame({ t: 'ack', n: r.received })); } + if (buf[0] === K_CTRL) { + let msg = null; + try { msg = JSON.parse(buf.subarray(1).toString('utf8')); } catch (_) {} + if (msg && typeof msg === 'object') onRoomCtrl(r, msg); + return; + } + if (buf.length < 3) return; + if (buf[0] === K_AUDIO) onRoomAudio(r, buf[1], buf.subarray(2)); + else if (buf[0] === K_VIDEO) onRoomVideo(r, buf[1], buf.subarray(2)); + else if (buf[0] === K_DATA) onRoomData(r, buf[1], buf.subarray(2)); + else if (buf[0] === K_KEY) onRoomKey(r, buf[1], buf.subarray(2)); + }; + const startRoomMedia = (r) => { + const player = audio.player(); + const gate = voiceGate(); + const samples = FRAME_BYTES / 2; + let pending = Buffer.alloc(0); + const capture = audio.capture((pcm) => { + if (room !== r) return; + pending = pending.length ? Buffer.concat([pending, pcm]) : Buffer.from(pcm); + while (pending.length >= FRAME_BYTES) { + const mic = pending.subarray(0, FRAME_BYTES); + pending = pending.subarray(FRAME_BYTES); + const acc = new Int32Array(samples); + for (const p of r.peers.values()) { const f = p.queue.shift(); if (f && !silenced.has(p.id)) mixInto(acc, f); } + const film = streamFrame(r); + if (film) mixInto(acc, film); + player.write(clampPcm(acc)); + if (r.rec) { + const both = Int32Array.from(acc); + if (!r.muted) mixInto(both, mic); + const take = clampPcm(both); + try { fs.writeSync(r.rec.fd, take); r.rec.bytes += take.length; } catch (_) { stopRoomRec(r); } + if (r.rec && r.rec.bytes >= ROOM_REC_MAX_BYTES) stopRoomRec(r); + if (r.rec && r.sealer && Date.now() - (r.recBeaconAt || 0) >= MUTE_BEACON_MS) { sendRoomData(r, { t: 'rec', on: true }); r.recBeaconAt = Date.now(); } + } + const voiced = gate(mic); + if (r.muted && r.sealer && Date.now() - (r.beaconAt || 0) >= MUTE_BEACON_MS) sendRoomData(r, { t: 'mute', on: true, beacon: true }); + if (r.hand && r.sealer && Date.now() - (r.handBeaconAt || 0) >= MUTE_BEACON_MS) { sendRoomData(r, { t: 'hand', on: true, at: r.handAt, beacon: true }); r.handBeaconAt = Date.now(); } + const talk = !r.muted && voiced; + if (!r.sealer || (!talk && !film)) continue; + r.sentAt = Date.now(); + let send = mic; + if (film) { const mix = new Int32Array(samples); mixInto(mix, film); if (talk) mixInto(mix, mic); send = clampPcm(mix); } + const opus = codecsOffered().includes('opus') && r.peers.size > 0 && [...r.peers.values()].every(p => p.opus); + if (opus && !r.enc) r.enc = makeEncoder('opus'); + r.out.push(Buffer.concat([Buffer.from([K_AUDIO]), Buffer.from(r.kid, 'hex'), r.sealer(opus ? r.enc.encode(send) : ulawEncode(send))])); + } + }); + r.media = { stop() { capture.stop(); player.stop(); } }; + }; + const closeDuplex = (duplex) => { + if (!duplex) return; + try { duplex.source(true, () => {}); } catch (_) {} + try { pull(pull.empty(), duplex.sink); } catch (_) {} + }; + const leaveRoom = (r, outcome) => { + if (!r || room !== r) return; + if (r.ready) { r.ready(new Error('left')); return; } + stopRoomRec(r); + room = null; + clearTimeout(r.timer); + stopRoomStream(r, false); + if (r.cam) { r.cam = false; stopCam(); } + for (const p of r.peers.values()) { clearTimeout(p.vtimer); clearTimeout(p.ftimer); endFrames(p.id); endFrames(filmKey(p.id)); } + stopGrid(r); + if (r.media) r.media.stop(); + if (r.enc) { r.enc.free(); r.enc = null; } + for (const p of r.peers.values()) if (p.dec) p.dec.free(); + try { r.out.push(ctrlFrame({ t: 'bye' })); } catch (_) {} + try { r.out.end(); } catch (_) {} + if (outcome) emit({ type: 'roomEnded', room: { rid: r.rid, ref: r.ref, title: r.title, joinedAt: r.joinedAt, endedAt: Date.now(), outcome } }); + roomChanged(); + }; + const openHub = (opts, req, attach) => { + if (opts.hub === server.id) return attach(hubDuplex(server.id, req)); + const viaRpc = (rpc) => attach(rpc && rpc.phone && typeof rpc.phone.roomHub === 'function' ? rpc.phone.roomHub(req, () => {}) : null); + const live = liveRpc(opts.hub); + if (live) return viaRpc(live); + if (typeof opts.address === 'string' && opts.address) { + try { + return server.conn.connect(opts.address, (err, rpc) => { + const got = !err && rpc && rpc.phone ? rpc : liveRpc(opts.hub); + return got ? viaRpc(got) : reach(opts.hub, viaRpc); + }); + } catch (_) {} + } + reach(opts.hub, viaRpc); + }; + let blank = null; + const blankFrame = async () => { + if (blank || !sharpLib) return blank; + try { blank = await sharpLib({ create: { width: GRID_TILE_LARGE.w, height: GRID_TILE_LARGE.h, channels: 3, background: '#000000' } }).jpeg().toBuffer(); } catch (_) { blank = null; } + return blank; + }; + const soundKey = (r) => r.tag + 'sound'; + const dropViewer = (r) => { + if (!r || viewers.get(r.rid) !== r) return; + viewers.delete(r.rid); + clearTimeout(r.timer); + clearInterval(r.mixTimer); + stopGrid(r); + endFrames(soundKey(r)); + for (const p of r.peers.values()) { if (p.dec) p.dec.free(); clearTimeout(p.vtimer); clearTimeout(p.ftimer); } + try { r.out.push(ctrlFrame({ t: 'bye' })); } catch (_) {} + try { r.out.end(); } catch (_) {} + }; + const startViewerMedia = (r) => { + const samples = FRAME_BYTES / 2; + r.mixTimer = setInterval(() => { + if (viewers.get(r.rid) !== r) return; + const acc = new Int32Array(samples); + for (const p of r.peers.values()) { const f = p.queue.shift(); if (f) mixInto(acc, f); } + if (frameWatchers.has(soundKey(r))) publishFrame(soundKey(r), clampPcm(acc)); + if (r.sealer && Date.now() - (r.beaconAt || 0) >= MUTE_BEACON_MS) sendRoomData(r, { t: 'mute', on: true, beacon: true }); + }, FRAME_MS); + if (r.mixTimer.unref) r.mixTimer.unref(); + }; + const staticKey = (rid, secret) => { + const key = Buffer.from(sodium.crypto_hash_sha256(Buffer.concat([Buffer.from('oasis-room|'), Buffer.from(rid, 'hex'), Buffer.from(String(secret))]))); + return { kid: Buffer.from(sodium.crypto_hash_sha256(key)).subarray(0, 4).toString('hex'), key }; + }; + + if (server.close && typeof server.close.hook === 'function') { + server.close.hook(function (fn, args) { finish('ended'); leaveRoom(room, 'ended'); stopCam(); for (const v of [...viewers.values()]) dropViewer(v); return fn.apply(this, args); }); + } + + return { + ring(raw, cb) { + const reply = typeof cb === 'function' ? cb : () => {}; + const req = opened(raw); + const { sender: from, via } = senderOf(this, req); + reply(null, true); + if (!from || from === server.id || !req || typeof req !== 'object') return; + const { callId, to, ephPk, ts, sig } = req; + dbg('incoming ring from', sid(from), via ? 'via ' + sid(via) : 'direct'); + if (!/^[0-9a-f]{32}$/.test(String(callId)) || to !== server.id || Math.abs(Date.now() - Number(ts)) > CLOCK_SKEW_MS) return; + const remotePk = ephKey(ephPk); + if (!remotePk || !verifyParts(from, ['ring', callId, ephPk, to, ts], sig)) return; + if (!seenCalls.has(callId) && !callerAllowed(from)) return; + if (!via) directRings.set(callId, Date.now()); + if (call && call.id === callId && call.dir === 'in') { + if (!via && call.phase === 'incoming' && call.peer === from) call.via = null; + return; + } + if (!firstSeen(callId)) return; + const ack = (state) => { + const at = Date.now(); + sendVia(directRings.has(callId) ? null : via, from, 'answer', { callId, from: server.id, ringing: state, ts: at, sig: signParts(keys, ['ringing', callId, state, from, at]) }); + }; + allowedCaller(from, (ok) => { + if (!ok || !audio) return ack('ringing'); + if (call || room) { + ack('busy'); + const at = Date.now(); + emit({ type: 'ended', call: { id: callId, peer: from, dir: 'in', startedAt: at, answeredAt: 0, endedAt: at, outcome: 'missed' } }); + return; + } + const startedAt = Date.now(); + call = { id: callId, peer: from, dir: 'in', phase: 'incoming', startedAt, ringUntil: startedAt + ringMs, remotePk, via: directRings.has(callId) ? null : via, codec: pickCodec(req.codecs), video: pickVideo(req.codecs, videoOn()) }; + call.tone = startTone(audio, 'ring'); + call.timer = setTimeout(() => { if (call && call.id === callId && call.phase === 'incoming') finish('missed'); }, ringMs); + changed(); + emit({ type: 'incoming', id: callId, peer: from }); + ack('ringing'); + }); + }, + answer(raw, cb) { + const req = opened(raw); + const { sender: from, via } = senderOf(this, req); + const c = call; + if (req && (req.ringing === 'ringing' || req.ringing === 'busy')) return ringingAck(c, req, from, cb); + if (c && c.group) return groupAnswer(c, req, from, via, cb); + if (!c || c.dir !== 'out' || c.phase !== 'calling' || from !== c.peer || !req || req.callId !== c.id) return cb(new Error('no-call')); + const calleePk = ephKey(req.ephPk); + if (!calleePk || !verifyParts(from, ['answer', req.callId, req.ephPk, server.id, req.ts], req.sig)) return cb(new Error('rejected')); + const path = via ? liveRpc(via) : liveRpc(from); + if (!path) return cb(new Error('no-connection')); + c.keys = deriveKeys(c.eph.secretKey, calleePk, c.id, Buffer.from(c.eph.publicKey), calleePk); + c.codec = pickCodec([req.codec]); + c.video = pickVideo([req.video]); + clearTimeout(c.timer); + stopTone(c); + c.via = via; + c.phase = 'connected'; + c.answeredAt = Date.now(); + cb(null, true); + changed(); + const quiet = () => {}; + startMedia(c, via ? path.phone.relayAudio({ to: c.peer, callId: c.id }, quiet) : path.phone.audio({ callId: c.id }, quiet), 'out'); + if (c.video) startVideo(c, via ? path.phone.relayVideo({ to: c.peer, callId: c.id }, quiet) : path.phone.video({ callId: c.id }, quiet), 'out'); + }, + hangup(raw, cb) { + const reply = typeof cb === 'function' ? cb : () => {}; + const req = opened(raw); + const { sender: from } = senderOf(this, req); + const c = call; + dbg('hangup received from', sid(from), c ? 'call ' + sid(c.peer) + ' ' + c.phase : 'no call'); + if (c && c.group) { + const leg = legOf(c, req && req.callId); + if (leg && from === leg.peer && verifyParts(from, ['hangup', req.callId, server.id, req.ts], req.sig)) dropLeg(c, leg, false); + return reply(null, true); + } + if (c && req && req.callId === c.id && from === c.peer && verifyParts(from, ['hangup', req.callId, server.id, req.ts], req.sig)) { + if (c.dir === 'in') finish(c.phase === 'incoming' ? 'missed' : 'ended'); + else if (c.phase === 'calling' || c.phase === 'connected') finish(c.phase === 'connected' ? 'ended' : 'noanswer'); + } + reply(null, true); + }, + audio(opts) { + const direct = this && this.id; + const from = opts && isFeedId(opts.from) ? opts.from : direct; + const c = call; + const viaOk = direct === c?.peer || (c?.via && direct === c.via); + if (!c || c.dir !== 'in' || !c.keys || c.media || from !== c.peer || !viaOk || !opts || opts.callId !== c.id || (c.phase !== 'connecting' && c.phase !== 'connected')) { + return { source: pull.error(new Error('no-call')), sink: pull.drain(null, () => {}) }; + } + c.phase = 'connected'; + return startMedia(c, null, 'in'); + }, + relay(req, cb) { + const reply = typeof cb === 'function' ? cb : () => {}; + const from = this && this.id; + reply(null, true); + if (!from || policy().relay === false || !req || typeof req !== 'object' || !isFeedId(req.to) || req.to === from) return; + if (!['ring', 'answer', 'hangup'].includes(req.method) || typeof req.sealed !== 'string' || !/^[0-9a-f]{32}$/.test(String(req.cid))) return; + const forwarded = req.hop === 1; + dbg('relay', req.method, 'from', sid(from), 'to', sid(req.to), forwarded ? 'forwarded' : 'origin', 'target-live=' + !!liveRpc(req.to)); + if (forwarded && !knownPubs().includes(from)) return; + const msg = { to: req.to, method: req.method, cid: req.cid, sealed: req.sealed, hop: 1 }; + const side = learn(req.cid, req.to, from, forwarded); + if (side.via === req.to) { + relayAllowed(req.to, forwarded ? req.to : from, (ok) => { + const target = ok ? liveRpc(req.to) : null; + dbg('relay deliver', sid(req.to), ok ? (target ? 'delivering' : 'not connected here') : 'policy refuses'); + if (!target) return; + try { target.phone[req.method]({ sealed: req.sealed }, () => {}); } catch (_) {} + }); + return; + } + if (side.via && side.via !== from && liveRpc(side.via)) { + try { liveRpc(side.via).phone.relay(msg, () => {}); } catch (_) {} + return; + } + if (forwarded || req.method !== 'ring') return; + relayAllowed(from, from, (ok) => { + if (!ok) return; + for (const pub of knownPubs().filter(id => id !== from)) { + try { liveRpc(pub).phone.relay(msg, () => {}); } catch (_) {} + } + }); + }, + relayAudio(opts) { + const from = this && this.id; + const refuse = { source: pull.error(new Error('no-route')), sink: pull.drain(null, () => {}) }; + if (!from || !opts || !isFeedId(opts.to) || opts.to === from || policy().relay === false) return refuse; + const forwarded = knownPubs().includes(from) && isFeedId(opts.from); + const caller = forwarded ? opts.from : from; + const r = routes.get(String(opts.callId || '')); + const side = r && r.sides.find(x => x.id === opts.to); + const direct = liveRpc(opts.to); + const next = direct || (side && side.via && side.via !== from ? liveRpc(side.via) : null); + if (!next) return refuse; + const remote = direct + ? next.phone.audio({ callId: opts.callId, from: caller }, () => {}) + : next.phone.relayAudio({ to: opts.to, callId: opts.callId, from: caller }, () => {}); + return { source: remote.source, sink: remote.sink }; + }, + video(opts) { + const direct = this && this.id; + const from = opts && isFeedId(opts.from) ? opts.from : direct; + const c = call; + const viaOk = direct === c?.peer || (c?.via && direct === c.via); + if (!c || c.dir !== 'in' || !c.keys || !c.video || c.vmedia || from !== c.peer || !viaOk || !opts || opts.callId !== c.id || (c.phase !== 'connecting' && c.phase !== 'connected')) { + return { source: pull.error(new Error('no-call')), sink: pull.drain(null, () => {}) }; + } + return startVideo(c, null, 'in'); + }, + relayVideo(opts) { + const from = this && this.id; + const refuse = { source: pull.error(new Error('no-route')), sink: pull.drain(null, () => {}) }; + if (!from || !opts || !isFeedId(opts.to) || opts.to === from || policy().relay === false || policy().video === false) return refuse; + const forwarded = knownPubs().includes(from) && isFeedId(opts.from); + const caller = forwarded ? opts.from : from; + const r = routes.get(String(opts.callId || '')); + const side = r && r.sides.find(x => x.id === opts.to); + const direct = liveRpc(opts.to); + const next = direct || (side && side.via && side.via !== from ? liveRpc(side.via) : null); + if (!next) return refuse; + const remote = direct + ? next.phone.video({ callId: opts.callId, from: caller }, () => {}) + : next.phone.relayVideo({ to: opts.to, callId: opts.callId, from: caller }, () => {}); + return { source: remote.source, sink: remote.sink }; + }, + camera(flag, cb) { + const on = !!flag; + const c = call; + if (c && c.vmedia && !c.group) { + if (on === !!c.cam) return cb(null, snapshot()); + if (on) { + if (!startCam((jpeg, seq) => { if (call === c) for (const plain of videoChunks(seq, jpeg)) sendCallVideo(c, plain); })) return cb(new Error('unavailable')); + c.cam = true; + } else { + c.cam = false; + stopCam(); + sendCallVideo(c, videoOff()); + } + changed(); + return cb(null, snapshot()); + } + const r = room; + if (r && r.phase === 'live') { + if (on === !!r.cam) return cb(null, roomSnapshot()); + if (on) { + if (!r.hubVideo || !startCam((jpeg, seq) => { if (room !== r) return; gridNote(r, server.id, jpeg); sendRoomJpeg(r, seq, jpeg, 0); })) return cb(new Error('unavailable')); + r.cam = true; + } else { + r.cam = false; + stopCam(); + gridNote(r, server.id, null); + sendRoomVideo(r, videoOff()); + } + roomChanged(); + return cb(null, roomSnapshot()); + } + cb(new Error('no-call')); + }, + stream(opts, cb) { + const o = opts && typeof opts === 'object' ? opts : {}; + const r = room; + if (!r || r.phase !== 'live') return cb(new Error('no-room')); + if (o.stop) { stopRoomStream(r); return cb(null, roomSnapshot()); } + const file = streamFile(o.file); + if (!file) return cb(new Error('invalid')); + if (!startRoomStream(r, file, o.name || path.basename(file))) return cb(new Error('unavailable')); + roomChanged(); + cb(null, roomSnapshot()); + }, + viewerJoin(opts, cb) { + const o = opts && typeof opts === 'object' ? opts : {}; + const rid = String(o.rid || ''); + if (!RID.test(rid) || !isFeedId(o.owner) || !isFeedId(o.hub) || typeof o.token !== 'string') return cb(new Error('invalid')); + if (viewers.has(rid)) return cb(null, true); + if (room && room.rid === rid) return cb(new Error('busy')); + const r = { + rid, ref: String(o.ref || ''), title: '', hub: o.hub, phase: 'joining', joinedAt: Date.now(), muted: true, slot: null, max: ROOM_MAX, + peers: new Map(), order: [], keys: new Map(), kid: null, sealer: null, static: false, sentAt: 0, + events: [], notify: false, rec: null, recPeers: new Set(), hand: false, handAt: 0, tag: `pub:${rid}:`, grid: newGrid() + }; + viewers.set(rid, r); + r.out = Pushable(() => {}); + r.ready = (err) => { + r.ready = null; + clearTimeout(r.timer); + if (err) { dropViewer(r); return cb(err); } + startViewerMedia(r); + startGrid(r); + cb(null, true); + }; + r.timer = setTimeout(() => { if (r.ready) r.ready(new Error('unreachable')); }, ROOM_JOIN_MS); + const attach = (duplex) => { + if (viewers.get(rid) !== r || !r.ready) return closeDuplex(duplex); + if (!duplex) return r.ready(new Error('unreachable')); + pull(r.out, duplex.sink); + pull(duplex.source, pull.drain((frame) => onRoomFrame(r, frame), () => { + if (viewers.get(rid) !== r) return; + if (r.ready) r.ready(new Error('unreachable')); + else dropViewer(r); + })); + }; + openHub(o, { rid, owner: o.owner, token: o.token, viewer: true, ...(RID.test(String(o.roomId || '')) ? { roomId: String(o.roomId) } : {}) }, attach); + }, + viewerLeave(opts, cb) { + dropViewer(viewers.get(String((opts && opts.rid) || ''))); + cb(null, true); + }, + viewerRooms() { return [...viewers.values()].filter(r => r.phase === 'live').map(r => r.rid); }, + publicFeed(opts) { + const rid = String((opts && opts.rid) || ''); + const kind = opts && opts.kind === 'sound' ? 'sound' : 'grid'; + const r = viewers.get(rid); + const target = r ? r.tag + kind : ''; + const p = Pushable(() => { const set = frameWatchers.get(target); if (set) { set.delete(p); if (!set.size) frameWatchers.delete(target); } }); + if (!r || r.phase !== 'live' || (kind === 'grid' && !sharpLib)) { p.end(); return p; } + if (!frameWatchers.has(target)) frameWatchers.set(target, new Set()); + frameWatchers.get(target).add(p); + if (kind === 'grid') { r.grid.dirty = true; r.grid.empty = false; } + return p; + }, + videoFrames(opts) { + const target = String((opts && opts.peer) || ''); + const p = Pushable(() => { const set = frameWatchers.get(target); if (set) { set.delete(p); if (!set.size) frameWatchers.delete(target); } }); + const now = Date.now(); + const filmOf = target.startsWith('film:') && target !== 'film:me' ? target.slice(5) : null; + const live = target === 'me' + ? !!cam + : target === 'film:me' ? !!(room && room.stream) + : target === 'grid' ? !!(room && sharpLib && gridIds(room).length) + : filmOf ? !!room && [...room.peers.values()].some(x => x.id === filmOf && now - (x.filmAt || 0) < VIDEO_HOLD_MS) + : call ? (call.peer === target && !!call.peerCam) + : room ? [...room.peers.values()].some(x => x.id === target && now - (x.frameAt || 0) < VIDEO_HOLD_MS) + : false; + if (!live) { p.end(); return p; } + if (!frameWatchers.has(target)) frameWatchers.set(target, new Set()); + frameWatchers.get(target).add(p); + return p; + }, + + call(target, opts, cb) { + if (typeof opts === 'function') { cb = opts; opts = {}; } + const legacy = new Set(Array.isArray(opts && opts.legacy) ? opts.legacy : []); + if (!audio) return cb(new Error('unavailable')); + const targets = [...new Set(Array.isArray(target) ? target : [target])]; + if (!targets.length || targets.length > GROUP_MAX || targets.some(t => !isFeedId(t) || t === server.id)) return cb(new Error('invalid')); + if (call || room) return cb(new Error('busy')); + if (targets.length > 1) return groupCall(targets, legacy, cb); + target = targets[0]; + const id = nodeCrypto.randomBytes(16).toString('hex'); + const eph = sodium.crypto_box_keypair(); + const startedAt = Date.now(); + const c = { id, peer: target, dir: 'out', phase: 'calling', startedAt, ringUntil: startedAt + ringMs, eph, paths: [], via: null }; + call = c; + c.tone = startTone(audio, 'connecting'); + c.timer = setTimeout(() => toVoicemail(c, 'unreachable'), ringMs); + changed(); + ringPaths(c, target, ringRequest(id, eph, target), () => call === c && c.phase === 'calling', () => reachedCall(c), legacy.has(target)); + cb(null, snapshot()); + }, + accept(cb) { + const c = call; + if (!c || c.dir !== 'in' || c.phase !== 'incoming') return cb(new Error('no-call')); + const eph = sodium.crypto_box_keypair(); + c.keys = deriveKeys(eph.secretKey, c.remotePk, c.id, c.remotePk, Buffer.from(eph.publicKey)); + stopTone(c); + clearTimeout(c.timer); + c.phase = 'connecting'; + c.answeredAt = Date.now(); + changed(); + const ts = Date.now(); + const ephPk = b64(eph.publicKey); + sendVia(c.via || null, c.peer, 'answer', { callId: c.id, from: server.id, ephPk, ts, sig: signParts(keys, ['answer', c.id, ephPk, c.peer, ts]), codec: c.codec, video: c.video }, (err) => { + if (call !== c) return; + if (err) return finish('failed'); + if (c.phase === 'connecting') { c.phase = 'connected'; changed(); } + }); + cb(null, snapshot()); + }, + reject(cb) { + const c = call; + if (c && c.dir === 'in' && c.phase === 'incoming') finish('rejected'); + cb(null, snapshot()); + }, + end(cb) { + const c = call; + if (!c) return cb(null, null); + if (c.group) { + for (const leg of c.legs) if (leg.phase === 'calling' || leg.phase === 'connected') hangupLeg(leg); + finish(c.answeredAt ? 'ended' : 'cancelled'); + return cb(null, null); + } + if (c.dir === 'in' && c.phase === 'incoming') { finish('rejected'); return cb(null, null); } + if (c.phase === 'calling' || c.phase === 'connecting' || c.phase === 'connected') notifyHangup(c); + finish(c.phase === 'connected' || c.phase === 'connecting' ? 'ended' : (c.phase === 'calling' ? 'cancelled' : 'noanswer')); + cb(null, null); + }, + mute(flag, cb) { + if (call) { call.muted = !!flag; changed(); } + cb(null, snapshot()); + }, + silence(opts, cb) { + const id = opts && opts.id; + if (!isFeedId(id) || id === server.id) return cb(new Error('invalid')); + if (opts.on) silenced.add(id); else silenced.delete(id); + if (call) changed(); + if (room) roomChanged(); + cb(null, { id, on: silenced.has(id) }); + }, + dismiss(cb) { + const c = call; + if (c && c.dir === 'out' && (c.phase === 'noanswer' || c.phase === 'recorded')) finish('noanswer'); + cb(null, snapshot()); + }, + recordStop(cb) { + const c = call; + if (!c || c.dir !== 'out' || (c.phase !== 'recording' && c.phase !== 'recorded') || !c.rec) return cb(new Error('no-recording')); + stopRecorder(c); + const pcm = Buffer.concat(c.rec.chunks); + c.rec = null; + if (pcm.length < RATE * 2 * PAM_MIN_MS / 1000) { finish('noanswer'); return cb(new Error('empty')); } + const file = path.join(os.tmpdir(), `oasis-voice-${c.id}.wav`); + try { fs.writeFileSync(file, wavFile(pcm), { mode: 0o600 }); } catch (err) { return cb(err); } + const out = { file, durationMs: Math.round(pcm.length / (RATE * 2) * 1000), peer: c.peer, callId: c.id }; + finish('pam'); + cb(null, out); + }, + recordCancel(cb) { + const c = call; + if (c && (c.phase === 'recording' || c.phase === 'recorded')) { + stopRecorder(c); + c.rec = null; + finish('noanswer'); + } + cb(null, snapshot()); + }, + roomHub(opts) { return hubDuplex(this && this.id, opts); }, + roomInfo(opts, cb) { + const r = hubRooms.get(String((opts && opts.rid) || '')); + cb(null, { count: r ? hubHumans(r) : 0, max: roomMax, video: policy().video !== false }); + }, + roomAdmits(opts, cb) { + const from = this && this.id; + if (!from) return cb(null, false); + relayAllowed(from, from, (ok) => cb(null, !!ok)); + }, + roomJoin(opts, cb) { + if (!audio) return cb(new Error('unavailable')); + if (call || room) return cb(new Error('busy')); + const rid = String((opts && opts.rid) || ''); + if (viewers.has(rid)) dropViewer(viewers.get(rid)); + if (!RID.test(rid) || !isFeedId(opts.owner) || !isFeedId(opts.hub) || typeof opts.token !== 'string') return cb(new Error('invalid')); + const secrets = (Array.isArray(opts.secrets) ? opts.secrets : []).filter(x => typeof x === 'string' && x); + const r = { + rid, ref: String(opts.ref || ''), title: String(opts.title || '').slice(0, 200), hub: opts.hub, + phase: 'joining', joinedAt: Date.now(), muted: false, slot: null, max: ROOM_MAX, peers: new Map(), order: [], + keys: new Map(), kid: null, sealer: null, static: secrets.length > 0, sentAt: 0, + events: [], notify: true, rec: null, recPeers: new Set(), hand: false, handAt: 0, tag: '', grid: newGrid() + }; + if (r.static) { + for (const secret of secrets.slice().reverse()) { const k = staticKey(rid, secret); r.keys.set(k.kid, k.key); } + const first = staticKey(rid, secrets[0]); + r.kid = first.kid; + r.sealer = sealer(first.key); + } + room = r; + r.out = Pushable(() => {}); + r.ready = (err) => { + r.ready = null; + clearTimeout(r.timer); + if (err) { leaveRoom(r, null); return cb(err); } + startRoomMedia(r); + startGrid(r); + roomChanged(); + cb(null, roomSnapshot()); + }; + r.timer = setTimeout(() => { if (r.ready) r.ready(new Error('unreachable')); }, ROOM_JOIN_MS); + const attach = (duplex) => { + if (room !== r || !r.ready) return closeDuplex(duplex); + if (!duplex) return r.ready(new Error('unreachable')); + pull(r.out, duplex.sink); + pull(duplex.source, pull.drain((frame) => onRoomFrame(r, frame), () => { + if (room !== r) return; + if (r.ready) r.ready(new Error('unreachable')); + else leaveRoom(r, 'lost'); + })); + }; + openHub(opts, { rid, owner: opts.owner, token: opts.token, ...(RID.test(String(opts.roomId || '')) ? { roomId: String(opts.roomId) } : {}) }, attach); + }, + roomLeave(cb) { + leaveRoom(room, 'left'); + cb(null, null); + }, + roomRecStart(cb) { cb(null, startRoomRec(room) ? roomSnapshot() : null); }, + roomRecStop(cb) { cb(null, stopRoomRec(room) ? roomSnapshot() : null); }, + roomNotify(flag, cb) { + if (room) { room.notify = !!flag; roomChanged(); } + cb(null, roomSnapshot()); + }, + roomClearEvents(cb) { + if (room) { room.events = []; roomChanged(); } + cb(null, roomSnapshot()); + }, + roomHand(flag, cb) { + if (room) { + room.hand = !!flag; + room.handAt = room.hand ? Date.now() : 0; + sendRoomData(room, { t: 'hand', on: room.hand, at: room.handAt }); + room.handBeaconAt = Date.now(); + roomChanged(); + } + cb(null, roomSnapshot()); + }, + roomMute(flag, cb) { + if (room) { room.muted = !!flag; sendRoomData(room, { t: 'mute', on: room.muted }); roomChanged(); } + cb(null, roomSnapshot()); + }, + roomCount(opts, cb) { + const rid = String((opts && opts.rid) || ''); + if (!RID.test(rid) || !isFeedId(opts.hub)) return cb(null, null); + if (room && room.rid === rid && room.phase === 'live') return cb(null, { count: humansIn(room) + 1, max: room.max }); + if (opts.hub === server.id) { const h = hubRooms.get(rid); return cb(null, { count: h ? hubHumans(h) : 0, max: roomMax }); } + const rpc = liveRpc(opts.hub); + if (!rpc || typeof rpc.phone.roomInfo !== 'function') return cb(null, null); + let done = false; + const finishPeek = (v) => { if (done) return; done = true; clearTimeout(t); cb(null, v); }; + const t = setTimeout(() => finishPeek(null), ROOM_PEEK_MS); + try { + rpc.phone.roomInfo({ rid }, (err, info) => finishPeek(!err && info && Number.isFinite(Number(info.count)) ? { count: Number(info.count), max: Number(info.max) > 0 ? Math.min(ROOM_MAX, Number(info.max)) : ROOM_MAX } : null)); + } catch (_) { finishPeek(null); } + }, + roomState() { return roomSnapshot(); }, + roomToken(rid) { return RID.test(String(rid || '')) ? signParts(keys, ['room', String(rid)]) : null; }, + roomHubFor(cb) { + const done = typeof cb === 'function' ? cb : () => {}; + let entries = []; + try { entries = netFirst(server.conn.query().peersAll()); } catch (_) {} + const ranked = typeof server.oasisPeerRank === 'function' ? (id) => server.oasisPeerRank([null, { key: id }]) : () => 0; + const candidates = knownPubs().filter(id => { const rpc = liveRpc(id); return rpc && rpc.phone && typeof rpc.phone.roomHub === 'function'; }).sort((x, y) => ranked(y) - ranked(x)); + const admits = (id, next) => { + const rpc = liveRpc(id); + const byGraph = () => relation('isFollowing', id, server.id, next); + if (!rpc || typeof rpc.phone.roomAdmits !== 'function') return byGraph(); + let settled = false; + const finishAsk = (fn) => { if (settled) return; settled = true; clearTimeout(t); fn(); }; + const t = setTimeout(() => finishAsk(() => next(false)), ROOM_PEEK_MS); + try { rpc.phone.roomAdmits({}, (err, ok) => finishAsk(err ? byGraph : () => next(!!ok))); } catch (_) { finishAsk(byGraph); } + }; + const pick = (i) => { + if (i >= candidates.length) return done(null, { key: server.id, address: '' }); + admits(candidates[i], (ok) => { + if (!ok) return pick(i + 1); + const hit = entries.find(e => Array.isArray(e) && e[0] && e[1] && e[1].key === candidates[i]); + done(null, { key: candidates[i], address: hit ? hit[0] : '' }); + }); + }; + pick(0); + }, + state() { return snapshot(); }, + available() { return !!audio; }, + pubs() { return [...pubKeys()]; }, + events() { + const p = Pushable(() => subscribers.delete(p)); + subscribers.add(p); + p.push({ type: 'state', state: snapshot() }); + return p; + } + }; + } +}; + +module.exports.setAudioFactory = (factory) => { audioFactory = typeof factory === 'function' ? factory : systemAudio; }; +module.exports.setVideoFactory = (factory) => { videoFactory = typeof factory === 'function' ? factory : systemVideo; }; +module.exports.setStreamFactory = (factory) => { streamFactory = typeof factory === 'function' ? factory : systemStream; }; +module.exports.jpegSplitter = jpegSplitter; +module.exports.VIDEO_CHUNK = VIDEO_CHUNK; +module.exports.VIDEO_HOLD_MS = VIDEO_HOLD_MS; +module.exports.ROOM_VIDEO_BYTES_PER_S = ROOM_VIDEO_BYTES_PER_S; +module.exports.ulawEncode = ulawEncode; +module.exports.ulawDecode = ulawDecode; +module.exports.RING_MS = RING_MS; +module.exports.GROUP_MAX = GROUP_MAX; +module.exports.ROOM_MAX = ROOM_MAX; +module.exports.RING_PER_WINDOW = RING_PER_WINDOW; +module.exports.chimeMs = chimeMs; +module.exports.recordingPrefix = recordingPrefix; +module.exports.VOICEMAIL_MAX_MS = VOICEMAIL_MAX_MS; +module.exports.SOUND_RATE = RATE; diff --git a/src/server/phone_stream_android.js b/src/server/phone_stream_android.js new file mode 100644 index 0000000..f2c525c --- /dev/null +++ b/src/server/phone_stream_android.js @@ -0,0 +1,53 @@ +const fs = require('fs'); +const net = require('net'); +const path = require('path'); + +const MARKER = path.join(__dirname, '..', '..', '.audio_bridge'); +const FRAME_MAX = 4 * 1024 * 1024; + +const bridgePort = () => { + try { + const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim()); + return Number.isFinite(port) && port > 0 ? port : 0; + } catch (_) { + return 0; + } +}; + +module.exports = () => ({ + open(file, { onJpeg, onPcm, onEnd }) { + let ended = false; + const finish = () => { + if (ended) return; + ended = true; + try { onEnd(); } catch (_) {} + }; + const port = bridgePort(); + if (!port) { + setImmediate(finish); + return { stop() { ended = true; } }; + } + const sock = net.connect(port, '127.0.0.1'); + let buf = Buffer.alloc(0); + sock.setNoDelay(true); + sock.on('error', finish); + sock.on('close', finish); + sock.on('connect', () => sock.write(Buffer.concat([Buffer.from('S'), Buffer.from(`${file}\n`, 'utf8')]))); + sock.on('data', (chunk) => { + buf = buf.length ? Buffer.concat([buf, chunk]) : chunk; + while (buf.length >= 5) { + const len = buf.readUInt32BE(1); + if (len > FRAME_MAX) { sock.destroy(); return; } + if (buf.length < 5 + len) return; + const type = buf[0]; + const payload = Buffer.from(buf.subarray(5, 5 + len)); + buf = buf.subarray(5 + len); + if (ended) continue; + if (type === 0x4a) onJpeg(payload); + else if (type === 0x41) onPcm(payload); + else if (type === 0x45) { finish(); sock.destroy(); return; } + } + }); + return { stop() { ended = true; try { sock.destroy(); } catch (_) {} } }; + } +}); diff --git a/src/server/phone_video_android.js b/src/server/phone_video_android.js new file mode 100644 index 0000000..deb6354 --- /dev/null +++ b/src/server/phone_video_android.js @@ -0,0 +1,30 @@ +const fs = require('fs'); +const net = require('net'); +const path = require('path'); +const { jpegSplitter } = require('./phone_module'); + +const MARKER = path.join(__dirname, '..', '..', '.audio_bridge'); + +const bridgePort = () => { + try { + const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim()); + return Number.isFinite(port) && port > 0 ? port : 0; + } catch (_) { + return 0; + } +}; + +module.exports = () => { + const port = bridgePort(); + if (!port) return null; + return { + capture(onJpeg) { + const sock = net.connect(port, '127.0.0.1'); + sock.on('error', () => {}); + sock.setNoDelay(true); + sock.write('V'); + sock.on('data', jpegSplitter(onJpeg)); + return { stop() { try { sock.destroy(); } catch (_) {} } }; + } + }; +}; diff --git a/src/server/snapshot_plugin.js b/src/server/snapshot_plugin.js new file mode 100644 index 0000000..9c865ef --- /dev/null +++ b/src/server/snapshot_plugin.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const path = require('path'); +const pull = require('./node_modules/pull-stream'); +const defer = require('./node_modules/pull-defer'); +const toPull = require('./node_modules/stream-to-pull-stream'); + +module.exports = { + name: 'snapshot', + version: '1.0.0', + manifest: { info: 'async', get: 'source' }, + permissions: { anonymous: { allow: ['info', 'get'] } }, + init(server, config) { + const file = (opts) => path.join(config.path, 'oasis', 'content', opts && opts.tier === 'recent' ? 'snapshot-recent.oasissn' : 'snapshot.oasissn'); + const stat = (opts) => { + try { const st = fs.statSync(file(opts)); return { tier: opts && opts.tier === 'recent' ? 'recent' : 'full', bytes: st.size, createdAt: new Date(st.mtimeMs).toISOString() }; } catch (_) { return null; } + }; + const allowed = (rpc, cb) => { + const id = rpc && rpc.id; + if (!id) return cb(null, false); + if (id === server.id) return cb(null, true); + if (!server.friends || typeof server.friends.isFollowing !== 'function') return cb(null, false); + server.friends.isFollowing({ source: server.id, dest: id }, (err, yes) => cb(null, !err && !!yes)); + }; + return { + info(opts, cb) { + if (typeof opts === 'function') { cb = opts; opts = {}; } + allowed(this, (_, ok) => { + if (!ok) return cb(new Error('snapshot: not allowed')); + const s = stat(opts); + if (!s) return cb(new Error('snapshot: not available')); + cb(null, s); + }); + }, + get(opts) { + const source = defer.source(); + allowed(this, (_, ok) => { + if (!ok) return source.resolve(pull.error(new Error('snapshot: not allowed'))); + if (!stat(opts)) return source.resolve(pull.error(new Error('snapshot: not available'))); + source.resolve(toPull.source(fs.createReadStream(file(opts)))); + }); + return source; + } + }; + } +}; diff --git a/src/server/ssb_config.js b/src/server/ssb_config.js index 0299f68..881a94d 100644 --- a/src/server/ssb_config.js +++ b/src/server/ssb_config.js @@ -3,22 +3,38 @@ const path = require('path'); const Config = require('ssb-config/inject'); const minimist = require('minimist'); -const configPath = path.resolve(__dirname, '../configs', 'server-config.json'); -const configData = JSON.parse(fs.readFileSync(configPath, 'utf8')); +const configData = require('../configs/config-manager').readServerConfig(); const argv = process.argv.slice(2); const i = argv.indexOf('--'); const conf = argv.slice(i + 1); const cliArgs = ~i ? argv.slice(0, i) : argv; -const override = minimist(conf); -if (global.__OASIS_KEYS__) override.keys = global.__OASIS_KEYS__; +// Nombre de la cuenta: define donde vive todo (~/.) y de ahi cuelgan el +// secret, la base de datos, los blobs y el socket. Sin la variable se usa 'ssb', +// que es lo de siempre, asi que las instalaciones existentes no notan nada. +// +// Se valida a proposito: este nombre acaba en rutas de fichero y hay operaciones +// destructivas (el modo panico borra el directorio de la cuenta) que no deben poder +// apuntar fuera de sitio. +// +// Manda la variable de entorno; si no esta, la cuenta que dejo anotada el selector +// de identidades de los ajustes; y si tampoco, 'ssb'. +const ACCOUNT_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/; +let annotated = null; +try { + const f = path.resolve(__dirname, '../configs', 'active-account'); + annotated = fs.readFileSync(f, 'utf8').trim() || null; +} catch (_) {} +const rawAccount = process.env.OASIS_ACCOUNT || annotated || 'ssb'; +const account = ACCOUNT_RE.test(rawAccount) ? rawAccount : 'ssb'; +if (account !== rawAccount) { + console.error(`[oasis] cuenta no valida (${rawAccount}); se usa 'ssb'`); +} -let config = Config('ssb', override); +let config = Config(account, minimist(conf)); config = { ...config, ...configData }; -if (global.__OASIS_KEYS__) config.keys = global.__OASIS_KEYS__; - const debug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; if (debug) { config.logging = { ...(config.logging || {}), level: 'debug' }; @@ -26,6 +42,23 @@ if (debug) { const megabyte = Math.pow(2, 20); config.blobs = config.blobs || {}; -config.blobs.max = 50 * megabyte; +config.blobs.max = 75 * megabyte; + +config.db2 = { automigrate: false, dangerouslyKillFlumeWhenMigrated: false, ...(config.db2 || {}) }; + +const lanOn = (() => { try { return require('../configs/config-manager').getConfig().lanBroadcasting !== false; } catch (_) { return true; } })(); +const bindHost = config.pub ? '0.0.0.0' : lanOn ? null : '127.0.0.1'; +for (const entry of (config.connections && config.connections.incoming && config.connections.incoming.net) || []) { + if (entry && typeof entry === 'object' && !entry.host && bindHost) entry.host = bindHost; +} + +config.account = account; + +config.statePath = (name) => { + // El directorio externo se separa por cuenta: sin esto dos identidades + // compartirian estado, que es justo lo que hay que evitar. + if (process.env.OASIS_STATE_DIR) return path.join(process.env.OASIS_STATE_DIR, account, name); + return config.path ? path.join(config.path, name) : null; +}; module.exports = config; diff --git a/src/server/ssb_metadata.js b/src/server/ssb_metadata.js index 37ca6c1..1646ebd 100644 --- a/src/server/ssb_metadata.js +++ b/src/server/ssb_metadata.js @@ -5,18 +5,28 @@ const pkg = require('./package.json'); const config = require('./ssb_config'); const updater = require('../backend/updater.js'); +const formatMB = (bytes, decimals = 0) => `${(bytes / (1024 * 1024)).toFixed(decimals)} MB`; + +const blobUsageOnDisk = (ssbPath) => { + const out = { bytes: 0, files: 0 }; + const walk = (dir) => { + let entries = []; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; } + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isDirectory()) walk(full); + else if (e.isFile()) { try { out.bytes += fs.statSync(full).size; out.files += 1; } catch (_) {} } + } + }; + if (ssbPath) walk(path.join(ssbPath, 'blobs', 'sha256')); + return out; +}; + let printed = false; let checkedForUpdate = false; -let pendingClearnetModules = null; -let clearnetReady = false; let pendingWarmup = null; let warmupReady = false; -function setClearnetModules(modules) { - pendingClearnetModules = Array.isArray(modules) ? modules : []; - clearnetReady = true; -} - function setWarmupTime(str) { pendingWarmup = str; warmupReady = true; @@ -64,19 +74,6 @@ async function checkForUpdate() { await updater.getRemoteVersion(); } -function waitForClearnet(timeoutMs = 25000) { - return new Promise((resolve) => { - if (clearnetReady) return resolve(pendingClearnetModules || []); - const started = Date.now(); - const tick = () => { - if (clearnetReady) return resolve(pendingClearnetModules || []); - if (Date.now() - started >= timeoutMs) return resolve(null); - setTimeout(tick, 250); - }; - tick(); - }); -} - async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, httpHost = 'localhost', offline = false, isPublic = false) { if (printed) return; printed = true; @@ -90,40 +87,86 @@ async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, htt const httpUrl = hasHttp ? `http://${httpHost}:${httpPort}` : ''; const oscLink = hasHttp ? `\x1b]8;;${httpUrl}\x07${httpUrl}\x1b]8;;\x07` : ''; const ssbPort = config.connections?.incoming?.net?.[0]?.port || config.port || 8008; - const localDiscovery = config.local === true; + const localDiscovery = (() => { try { return require('../configs/config-manager.js').getConfig().lanBroadcasting !== false; } catch (_) { return false; } })(); const hops = config.conn?.hops ?? config.friends?.hops ?? 2; console.log("========================="); console.log(`Running mode: ${modeColor}${mode}${colors.reset}`); console.log("========================="); + const walletId = (() => { + try { + const w = (require('../configs/config-manager.js').getConfig() || {}).wallet || {}; + if (!(String(w.url || '').trim() && String(w.user || '').trim() && String(w.pass || '').trim())) return ''; + const file = process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, 'wallet-addresses.json') : require('../configs/state-manager').statePath('wallet-addresses.json'); + const map = JSON.parse(fs.readFileSync(file, 'utf8')); + const v = map[`@${publicKey}`]; + const addr = typeof v === 'string' ? v : (v && v.address) || ''; + return /^E[1-9A-HJ-NP-Za-km-z]{32,34}$/.test(addr) ? addr : ''; + } catch (_) { return ''; } + })(); + console.log(`- OASIS ID: [ ${colors.orange}@${publicKey}${colors.reset} ]`); console.log(`- Package: ${colors.blue}${name} ${colors.yellow}[Version: ${version}]${colors.reset}`); - if (hasHttp) console.log(`- URL: ${colors.cyan}${oscLink}${colors.reset}`); - console.log(`- Oasis ID: [ ${colors.orange}@${publicKey}${colors.reset} ]`); + if (hasHttp) console.log(`- GUI: ${colors.cyan}${oscLink}${colors.reset}`); + const phoneEnabled = (() => { + try { return ((require('../configs/config-manager.js').getConfig() || {}).modules || {}).phoneMod !== 'off'; } catch (_) { return false; } + })(); + const phoneNumber = phoneEnabled ? (() => { try { return require('../models/phone_number').phoneNumberOf(`@${publicKey}`); } catch (_) { return null; } })() : null; + console.log(`- VoIP ID: ${phoneEnabled ? (phoneNumber ? `[ ${colors.orange}${phoneNumber}${colors.reset} ]` : 'enabled') : 'disabled'}`); + console.log(walletId ? `- ECOin ID: [ ${colors.orange}${walletId}${colors.reset} ]` : `- ECOin ID: disabled`); console.log("- Logging Level:", logLevel); + console.log("- Engine: db2-legacy"); const ifaces = os.networkInterfaces(); const isOnline = Object.values(ifaces).some(list => list && list.some(i => !i.internal && i.family === 'IPv4') ); console.log(`- Protocol (port): ${ssbPort}`); - console.log(`- Mode: ${isOnline ? 'online' : 'offline'}`); + const networkPaused = (() => { try { return require('../configs/config-manager.js').getConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1'; } catch (_) { return false; } })(); + console.log(`- Mode: ${networkPaused ? 'paused' : (isOnline ? 'online' : 'offline')}`); console.log(`- Replication (hops): ${hops}`); + const oasisCfg = (() => { try { return require('../configs/config-manager.js').getConfig() || {}; } catch (_) { return {}; } })(); + console.log(`- Blockchain backlog: ${Number(oasisCfg.ssbLogStream && oasisCfg.ssbLogStream.limit) || 1000} blocks`); + console.log(`- Blob size limit: ${formatMB(Number(config.blobs && config.blobs.max) || 0)}`); + const cacheMb = Number(oasisCfg.blobCache && (isPublic ? oasisCfg.blobCache.pubMaxMB : oasisCfg.blobCache.maxMB)) || 0; + const cacheUsage = blobUsageOnDisk(config.path); + console.log(`- Media cache limit: ${cacheMb > 0 ? formatMB(cacheMb * 1024 * 1024) : 'Unlimited'}`); + console.log(`- Current usage: ${formatMB(cacheUsage.bytes, 1)} · ${cacheUsage.files} files`); console.log(`- LAN Broadcasting (UDP): ${localDiscovery ? 'enabled' : 'disabled'}`); - const clearnetModules = await waitForClearnet(); - const clearnetStatus = (clearnetModules && clearnetModules.length > 0) ? clearnetModules.join(', ') : 'disabled'; - let fediverseConnected = false; - try { - const acc = JSON.parse(fs.readFileSync(path.join(__dirname, '../configs/fediverse-accounts.json'), 'utf8')); - fediverseConnected = !!(acc && acc.mastodon); - } catch (_) {} - console.log(`- Internet Broadcasting:`); - console.log(` - Clearnet: ${clearnetStatus}`); - console.log(` - Fediverse: ${fediverseConnected ? 'enabled' : 'disabled'}`); + const multiverse = (() => { + if ((oasisCfg.modules || {}).fediverseMod === 'off') return []; + try { + const acc = JSON.parse(fs.readFileSync(require('../configs/state-manager').statePath('fediverse-accounts.json'), 'utf8')) || {}; + return [ + acc.mastodon && acc.mastodon.token ? 'Mastodon' : null, + acc.peertube && acc.peertube.token ? 'PeerTube' : null, + acc.telegram && acc.telegram.session ? 'Telegram' : null + ].filter(Boolean); + } catch (_) { return []; } + })(); + console.log(`- Multiverse: ${multiverse.length ? multiverse.join(', ') : 'disabled'}`); + if (hasHttp) { + const en = (() => { try { return require('../client/assets/translations/i18n').en || {}; } catch (_) { return {}; } })(); + const uxNames = { blocks: en.uxModeMenus, ainav: en.uxModeAINav, chats: en.uxModeChats, feed: en.uxModeFeed, phone: en.phoneTitle, rooms: en.roomsTitle }; + const wishNames = { whole: en.settingsWishWhole, mutuals: en.settingsWishMutuals, 'only-lan': en.settingsWishOnlyLan, local: en.settingsWishLocal }; + const workflow = (() => { + try { + const wf = require('../models/workflows_model'); + const key = wf.currentWorkflow(oasisCfg); + if (key) return en[`workflow_${key}`] || key; + const mods = oasisCfg.modules || {}; + return wf.ALL_MODULES.every(m => mods[`${m}Mod`] === 'on') ? (en.welcomeWorkflow_full || 'Default') : 'custom'; + } catch (_) { return 'custom'; } + })(); + const ux = (oasisCfg.ux && oasisCfg.ux.current) || 'blocks'; + const wish = oasisCfg.wish || 'whole'; + console.log(`- Workflow: ${workflow}`); + console.log(`- UX mode: ${uxNames[ux] || ux}`); + console.log(`- Wish: ${wishNames[wish] || wish}`); + } console.log(""); console.log("========================="); console.log("Modules loaded: [", modules.length, "]"); console.log("========================="); - // Check for updates await checkForUpdate(); console.log("========================="); @@ -135,7 +178,6 @@ async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, htt module.exports = { printMetadata, - setClearnetModules, setWarmupTime, colors }; diff --git a/src/views/AI_view.js b/src/views/AI_view.js index e7a0d14..3d0bb6f 100644 --- a/src/views/AI_view.js +++ b/src/views/AI_view.js @@ -1,147 +1,102 @@ -const { div, h2, p, section, button, form, textarea, br, span, input, label, select, option } = require("../server/node_modules/hyperaxe"); -const { template, i18n } = require('./main_views'); -const { renderUrl } = require('../backend/renderUrl'); +const { div, h2, p, section, button, form, textarea, span, input, br } = require("../server/node_modules/hyperaxe"); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLinkLabel } = require('./main_views'); +const config = require('../server/ssb_config'); +const { renderStyledText } = require('../backend/renderStyledText'); -exports.aiView = (history = [], userPrompt = '') => { +const statusBanner = (status) => { + if (!status) return null; + if (status.installed === false || /model_missing/.test(String(status.error || ''))) { + return div({ class: 'ai-status ai-status-missing' }, i18n.aiStatusMissingModel); + } + if (status.error && !/model_missing/.test(String(status.error))) { + return div({ class: 'ai-status ai-status-loading' }, `${i18n.aiServerError} (${status.error})`); + } + return null; +}; + +const renderAnswerText = (text) => { + const lines = String(text || '').split('\n').map(l => l.trim()).filter(Boolean); + return lines.flatMap((line, i) => (i > 0 ? [br()] : []).concat(renderStyledText(line))); +}; + +const myNick = () => { try { return userLinkLabel(config.keys.id); } catch (_) { return '@me'; } }; + +const ratingRow = (entry) => { + if (entry.source !== 'model') return null; + const rating = Math.max(0, Math.min(5, Number(entry.rating) || 0)); + if (entry.trainStatus === 'approved') { + return div({ class: 'ai-rating ai-rating-done', title: i18n.aiTrainApproved }, + span({ class: 'ai-stars-static' }, '★'.repeat(rating || 5) + '☆'.repeat(5 - (rating || 5))), + span({ class: 'ai-rating-label' }, i18n.aiTrainApproved) + ); + } + return form({ method: 'POST', action: '/ai/approve', class: 'ai-rating ai-stars', title: i18n.aiApproveTrain }, + input({ type: 'hidden', name: 'ts', value: String(entry.timestamp) }), + ...[5, 4, 3, 2, 1].map(n => button({ type: 'submit', name: 'rating', value: String(n), class: 'ai-star-btn', title: `${n} ★` }, '★')) + ); +}; + +const SPLIT_MARKER = ''; + +const stateChip = (kind) => span({ class: `pm-exposition-chip ai-chip ai-chip-${kind}` }, + span({ class: 'pm-exposition-icon' }, kind === 'thinking' ? '⏳' : '🤖'), + span({ class: 'pm-exposition-text' }, kind === 'thinking' ? i18n.aiThinking : i18n.aiWaiting) +); + + +const renderEntry = (entry, { split = false } = {}) => { + const time = entry.timestamp ? moment(entry.timestamp).format("YYYY/MM/DD HH:mm") : ''; + const thinking = entry.trainStatus === 'thinking'; + return [ + div({ class: 'chat-bubble-row ai-row' }, + div({ class: 'chat-message ai-bubble-user' }, + span({ class: 'chat-bubble-sender' }, myNick()), + div({ class: 'chat-message-text' }, ...renderStyledText(String(entry.question || ''))), + span({ class: 'chat-bubble-time' }, time) + ) + ), + thinking ? stateChip('thinking') : null, + split ? span({ id: 'oasis-ai-split' }) : null, + thinking ? null : div({ class: 'chat-bubble-row chat-bubble-row-self ai-row' }, + div({ class: `chat-message chat-message-self ai-bubble-42 ai-bubble-${entry.source || 'model'}` }, + span({ class: 'chat-bubble-sender chat-bubble-sender-owner' }, '🤖 42'), + div({ class: 'chat-message-text' }, ...renderAnswerText(entry.answer)), + span({ class: 'chat-bubble-time' }, time), + ratingRow(entry) + ) + ) + ]; +}; + +exports.aiView = (history = [], userPrompt = '', { status = null, split = false } = {}) => { + const ordered = [...history].reverse(); + const thinking = ordered.length ? ordered[ordered.length - 1].trainStatus === 'thinking' : false; return template( i18n.aiTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.aiTitle), - p(i18n.aiDescription), - userPrompt ? div({ class: 'user-prompt', style: 'margin-bottom: 2em; font-size: 0.95em; color: #888;' }, - `${i18n.aiPromptUsed || 'System Prompt'}: `, - span({ style: 'font-style: italic;' }, `"${userPrompt}"`) - ) : null, - form({ method: 'POST', action: '/ai', style: "margin-bottom: 0;" }, - textarea({ name: 'input', rows: 4, placeholder: i18n.aiInputPlaceholder, required: true }), - br(), - div({ style: "display: flex; gap: 1.5em; justify-content: flex-end; align-items: center; margin-top: 0.7em;" }, - button({ type: 'submit' }, i18n.aiSubmitButton) - ) - ), - div({ style: "display: flex; justify-content: flex-end; margin-bottom: 2em;" }, - form({ method: 'POST', action: '/ai/clear', style: "display: inline;" }, - button({ - type: 'submit', - style: ` - background: #b80c09; - color: #fff; - border: none; - padding: 0.4em 1.2em; - border-radius: 6px; - cursor: pointer; - font-size: 1em; - margin-left: 1em; - ` - }, i18n.aiClearHistory) - ) - ), - br(), - ...history.map(entry => - div({ - class: 'chat-entry', - style: ` - margin-bottom: 2em; - position: relative; - background: #191919; - border-radius: 10px; - box-shadow: 0 0 8px #0004; - padding-top: 1.8em; - ` - }, - entry.timestamp ? span({ - style: ` - position: absolute; - top: 0.5em; - right: 1.3em; - font-size: 0.92em; - color: #888; - ` - }, new Date(entry.timestamp).toLocaleString()) : null, - br(), br(), - div({ class: 'user-question', style: 'margin-bottom: 0.75em;' }, - h2(`${i18n.aiUserQuestion}:`), - p(...renderUrl(entry.question)) - ), - div({ - class: 'ai-response', - style: ` - max-width: 800px; - margin: auto; - background: #111; - padding: 1.25em; - border-radius: 6px; - font-family: sans-serif; - line-height: 1.6; - color: #ffcc00; - ` - }, - h2(`${i18n.aiResponseTitle}:`), - ...String(entry.answer || '') - .split('\n\n') - .flatMap(paragraph => - paragraph - .split('\n') - .map(line => - p({ style: "margin-bottom: 1.2em;" }, ...renderUrl(line.trim())) - ) - ) - ), - div({ - class: 'ai-train-bar', - style: ` - display:flex; - align-items:center; - gap:12px; - margin: 12px auto 8px auto; - max-width: 800px; - padding: 8px 0; - border-top: 1px solid #2a2a2a; - flex-wrap: wrap; - ` - }, - Array.isArray(entry.snippets) && entry.snippets.length - ? span({ style: 'color:#9aa; font-size:0.95em;' }, `${i18n.aiSnippetsUsed}: ${entry.snippets.length}`) - : null, - h2(`${i18n.statsAITraining}:`), - entry.trainStatus === 'approved' - ? span({ style: 'color:#5ad25a; font-weight:600;' }, i18n.aiTrainApproved) - : entry.trainStatus === 'rejected' - ? span({ style: 'color:#ff6b6b; font-weight:600;' }, i18n.aiTrainRejected) - : null, - entry.trainStatus === 'approved' || entry.trainStatus === 'rejected' - ? null - : div({ class: 'ai-approve-block' }, - form({ method: 'POST', action: '/ai/approve', class: 'ai-approve-form' }, - input({ type: 'hidden', name: 'ts', value: String(entry.timestamp) }), - div({ class: 'ai-approve-meta' }, - label({ class: 'ai-approve-meta-label' }, i18n.aiApproveTagsLabel || 'Tags (comma-separated)'), - input({ type: 'text', name: 'tags', placeholder: i18n.aiApproveTagsPlaceholder || 'e.g. oasis, governance, ecology', maxlength: '160' }), - label({ class: 'ai-approve-meta-label' }, i18n.aiApproveRatingLabel || 'Rating'), - select({ name: 'rating' }, - option({ value: '0' }, '—'), - option({ value: '1' }, '★'), - option({ value: '2' }, '★★'), - option({ value: '3' }, '★★★'), - option({ value: '4' }, '★★★★'), - option({ value: '5' }, '★★★★★') - ) - ), - textarea({ name: 'custom', rows: 3, placeholder: i18n.aiCustomAnswerPlaceholder, class: 'ai-approve-custom' }), - div({ class: 'ai-approve-actions' }, - button({ type: 'submit', class: 'approve-btn' }, i18n.aiApproveTrain) - ) - ), - form({ method: 'POST', action: '/ai/reject', class: 'ai-approve-reject' }, - input({ type: 'hidden', name: 'ts', value: String(entry.timestamp) }), - button({ type: 'submit', class: 'reject-btn' }, i18n.aiRejectTrain) - ) - ) - ) + p(i18n.aiDescription) + ), + statusBanner(status), + div({ class: 'chat-workspace ai-workspace' }, + ordered.length ? div({ class: 'chat-messages-list ai-messages-list' }, ...ordered.flatMap((e, i) => renderEntry(e, { split: split && i === ordered.length - 1 }))) : null, + thinking ? null : div({ class: 'chat-message-form ai-message-form' }, + form({ id: 'ai-prompt-form', method: 'POST', action: '/ai', class: 'ai-prompt-form' }, + textarea({ maxlength: "5000", name: 'input', rows: 3, placeholder: i18n.aiInputPlaceholder, required: true }) + ), + div({ class: 'ai-submit-row' }, + stateChip('waiting'), + button({ type: 'submit', form: 'ai-prompt-form' }, i18n.aiSubmitButton), + ordered.length ? form({ method: 'POST', action: '/ai/clear', class: 'ai-clear-form' }, + button({ type: 'submit', class: 'ai-clear-btn' }, i18n.aiClearHistory) + ) : null ) ) ) ) ); }; + +exports.SPLIT_MARKER = SPLIT_MARKER; diff --git a/src/views/activity_view.js b/src/views/activity_view.js index f594a71..842979a 100644 --- a/src/views/activity_view.js +++ b/src/views/activity_view.js @@ -1,35 +1,52 @@ -const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary, ul, li, label } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions, emptyState, renderHiveNav } = require('./main_views'); +const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { template, i18n, paged, userLink, userLinkLabel, renderStateChip, renderPhoneChip, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, contentDeleteAction } = require('./main_views'); const opinionCategories = require('../backend/opinion_categories'); +const { roomNumberOf } = require('../models/phone_number'); -const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent','transfer']); +const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent','file']); +const TORRENTABLE_TYPES = CONTENT_TORRENTABLE; const OPINION_ROUTES = { feed: (id) => `/feed/opinions/${encodeURIComponent(id)}`, bookmark: (id) => `/bookmarks/opinions/${encodeURIComponent(id)}`, image: (id) => `/images/opinions/${encodeURIComponent(id)}`, audio: (id) => `/audios/opinions/${encodeURIComponent(id)}`, torrent: (id) => `/torrents/opinions/${encodeURIComponent(id)}`, + file: (id) => `/files/opinions/${encodeURIComponent(id)}`, video: (id) => `/videos/opinions/${encodeURIComponent(id)}`, document: (id) => `/documents/opinions/${encodeURIComponent(id)}`, - votes: (id) => `/votes/opinions/${encodeURIComponent(id)}`, - transfer: (id) => `/transfers/opinions/${encodeURIComponent(id)}` + votes: (id) => `/votes/opinions/${encodeURIComponent(id)}` }; const moment = require("../server/node_modules/moment"); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText, escapeHtml, safeExternalHref } = require('../backend/renderStyledText'); +const { letterOf } = require('./polls_view'); const { getConfig } = require("../configs/config-manager.js"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { renderZoomableImage } = require('./gallery_view'); +const { renderTribeOriginChip } = require('./clearnet_view'); + +const MEDIA_MD_RE = /!?\[(?:image|video|audio|pdf|torrent)[^\]]*\]\(\s*&[^)\s]+\s*\)/g +const stripMediaMarkdown = (text) => String(text || '').replace(MEDIA_MD_RE, '').replace(/\n{3,}/g, '\n\n').trim() +const renderMediaObject = (m, href = null) => { + if (!m || typeof m !== 'object' || !m.blobId) return null + const src = `/blob/${encodeURIComponent(String(m.blobId))}` + if (href && m.kind !== 'video' && m.kind !== 'audio') return div({ class: 'card-field image-container media-left' }, a({ href, class: 'podcast-cover-link' }, img({ src, class: 'post-image', alt: '' }))) + if (m.kind === 'video') return div({ class: 'card-field video-container' }, videoHyperaxe({ controls: true, class: 'post-video', src, preload: 'metadata' })) + if (m.kind === 'audio') return div({ class: 'card-field audio-container' }, audioHyperaxe({ controls: true, class: 'post-audio', src, preload: 'metadata' })) + return div({ class: 'card-field image-container media-left' }, renderZoomableImage(src, { imgClass: 'post-image' })) +} const renderMediaBlob = (value, fallbackSrc = null) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, class: 'post-image' }) + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: 'post-image' }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return videoHyperaxe({ controls: true, class: 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return videoHyperaxe({ controls: true, class: 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audioHyperaxe({ controls: true, class: 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: 'post-image' }) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: 'post-image' }) return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null } @@ -41,7 +58,7 @@ function cleanFeedText(t) { } function isValidFeedText(t) { - const s = cleanFeedText(t); + const s = cleanFeedText(t).replace(/!?\[[^\]\n]*\]\(\s*&[^)\s]+\.sha256\s*\)/g, '').trim(); return s.length >= FEED_TEXT_MIN && s.length <= FEED_TEXT_MAX; } @@ -109,25 +126,7 @@ const decodeMaybe = (s) => { try { return decodeURIComponent(String(s || '')); } catch { return String(s || ''); } }; -const rewriteHashtagLinks = (html) => { - const s = String(html || ''); - return s.replace(/href=(["'])(?:https?:\/\/[^"']+)?\/hashtag\/([^"'?#]+)([^"']*)\1/g, (m, q, tag, rest) => { - const t = decodeMaybe(tag).replace(/^#/, '').trim().toLowerCase(); - const href = `/search?query=%23${encodeURIComponent(t)}`; - return `href=${q}${href}${q}`; - }); -}; -function renderUrlPreserveNewlines(text) { - const s = String(text || ''); - const lines = s.split(/\r\n|\r|\n/); - const out = []; - for (let i = 0; i < lines.length; i++) { - if (i) out.push(br()); - out.push(...renderUrl(lines[i])); - } - return out; -} function getThreadIdFromPost(action) { const c = action.value?.content || action.content || {}; @@ -199,6 +198,8 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { .slice() .sort((a, b) => (a.ts || 0) - (b.ts || 0)); + if (!rootAction) continue; + const rootType = rootAction.type || 'post'; out.push({ id: `thread:${threadId}`, type: 'postThread', @@ -206,18 +207,21 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { ts: latest.ts, content: { threadId, + rootType, + rootHref: rootType === 'post' ? `/blogs/${encodeURIComponent(threadId)}` : getViewDetailsAction(rootType, rootAction), root: rootAction ? { id: safeMsgId(rootAction), author: rootAction.author, - text: excerptPostText(rootAction.value?.content || rootAction.content || {}, 240) + subject: String((rootAction.value?.content || rootAction.content || {}).contentWarning || '').trim(), + text: excerptPostText(rootAction.value?.content || rootAction.content || {}, 600) } : null, replies: replies.map(p => ({ id: safeMsgId(p), author: p.author, ts: p.ts, - text: excerptPostText(p.value?.content || p.content || {}, 200) + text: excerptPostText(p.value?.content || p.content || {}, 600) })) } }); @@ -228,7 +232,50 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { } exports.renderActionCards = renderActionCards; -function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { +const SPREADABLE_TYPES = CONTENT_SPREADABLE; + +const TRIBE_OPENED_TYPE = { event: 'typeEvent', task: 'typeTask', votation: 'typeVotes', forum: 'typeForum', feed: 'typeFeed', report: 'typeReport', image: 'typeImage', audio: 'typeAudio', video: 'typeVideo', document: 'typeDocument', bookmark: 'typeBookmark', torrent: 'typeTorrent' }; +const TRIBE_OPENED_FILTER = { event: 'event', task: 'task', votation: 'votes', forum: 'forum', feed: 'feed', report: 'report', image: 'image', audio: 'audio', video: 'video', document: 'document', bookmark: 'bookmark', torrent: 'torrent' }; +const tribeOpenedKey = (action) => { + const c = (action && action.content) || {}; + return c.contentType === 'media' ? String(c.mediaType || '') : String(c.contentType || ''); +}; +const openedIn = (action, filter) => !!action && action.type === 'tribeOpened' && TRIBE_OPENED_FILTER[tribeOpenedKey(action)] === filter; + +const renderTribeOpenedCard = (action, date, authorName) => { + const c = action.content || {}; + const origin = c.tribeOrigin || {}; + const key = tribeOpenedKey(action); + const label = String(i18n[TRIBE_OPENED_TYPE[key]] || key || '').toUpperCase(); + const blob = typeof c.image === 'string' && c.image.startsWith('&') ? `/blob/${encodeURIComponent(c.image)}` : null; + const media = !blob ? null + : c.mediaType === 'video' ? videoHyperaxe({ controls: true, class: 'post-video', src: blob, preload: 'metadata' }) + : c.mediaType === 'audio' ? audioHyperaxe({ controls: true, class: 'post-audio', src: blob }) + : c.mediaType === 'image' ? renderZoomableImage(blob, { imgClass: 'post-image' }) + : null; + const bookmarkHref = c.mediaType === 'bookmark' ? safeExternalHref(c.url) : ''; + return div({ class: 'trending-card' }, + div({ class: 'card-header activity-card-header' }, + div({ class: 'card-chips-row' }, + span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, label)), + renderTribeOriginChip(origin) + ) + ), + div({ class: 'card-section' }, + c.title ? h2(origin.href ? a({ href: origin.href }, c.title) : c.title) : null, + c.description ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(c.description).slice(0, 600))) : null, + c.date || c.location ? p({ class: 'post-text' }, [c.date, c.location].filter(Boolean).join(' · ')) : null, + media, + bookmarkHref ? a({ href: bookmarkHref, target: '_blank', rel: 'noopener noreferrer' }, c.url) : null + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${date}`), + userLink(action.author, authorName) + ) + ); +}; + +function renderActionCards(actions, userId, allActions, spreadMap = new Map(), extras = {}) { const all = Array.isArray(allActions) ? allActions : actions; const byIdAll = new Map(); for (const a0 of all) { @@ -285,11 +332,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { : validActions; if (!deduped.length) { - return emptyState({ icon: "📭", title: i18n.noActions, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta }); + return div({ class: "no-actions" }, p(i18n.noActions)); } const seenDocumentTitles = new Set(); - const items = buildActivityItemsWithPostThreads(deduped, all); + const grouped = buildActivityItemsWithPostThreads(deduped, all); + const items = extras && extras.paged ? paged(grouped) : grouped; const spreadOrdinalById = new Map(); const spreadsByLink = new Map(); @@ -323,8 +371,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } const cards = items.map(action => { - const date = action.ts ? new Date(action.ts).toLocaleString() : ""; + const date = action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ""; const type = action.type || 'unknown'; + if (type === 'tribeOpened') return renderTribeOpenedCard(action, date, (action.authorNames && action.authorNames[action.author]) || getProfile(action.author).name); let skip = false; let headerText; if (type.startsWith('parliament')) { @@ -342,14 +391,18 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { headerText = `[${String(i18n.typeTask || 'TASK').toUpperCase()} · ASSIGNMENT]`; } else if (type === 'shopProduct') { headerText = `[SHOP · PRODUCT]`; - } else if (type === 'chat') { - headerText = `[CHAT \u00b7 NEW]`; } else if (type === 'pad') { headerText = `[PAD · ${String(i18n.padNew || 'NEW').toUpperCase()}]`; - } else if (type === 'ubiClaim') { - headerText = `[UBI · CLAIM]`; - } else if (type === 'ubiclaimresult') { - headerText = `[UBI · RESULT]`; + } else if (type === 'wikiPage') { + headerText = `[${String(i18n.typeWiki || 'WIKI').toUpperCase()}]`; + } else if (type === 'emergencyUpdate') { + headerText = `[${String(i18n.typeEmergency || 'EMERGENCY').toUpperCase()} · ${String(i18n.emergencyLastUpdate || 'LAST UPDATE').toUpperCase()}]`; + } else if (type === 'campaignUpdate') { + headerText = `[${String(i18n.typeCampaign || 'CAMPAIGN').toUpperCase()} · ${String(i18n.emergencyLastUpdate || 'LAST UPDATE').toUpperCase()}]`; + } else if (type === 'podcastEpisode') { + headerText = `[${String(i18n.typePodcast || 'PODCAST').toUpperCase()} · ${String(i18n.typePodcastEpisode || 'EPISODE').toUpperCase()}]`; + } else if (type === 'larpHousePost') { + headerText = `[${String(i18n.typeLarp || 'L.A.R.P.').toUpperCase()}]`; } else { const typeLabel = i18n[`type${capitalize(type)}`] || type; headerText = `[${String(typeLabel).toUpperCase()}]`; @@ -371,7 +424,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const voteOutcome = (() => { const totalNum = Number(totalVotes || 0); - const noResult = { text: i18n.voteNoQuorum || 'NO QUORUM', color: '#ffcc00' }; + const noResult = { text: i18n.voteNoQuorum || 'NO QUORUM', tone: 'none' }; if (totalNum < 2) return noResult; const entries = Object.entries(votes || {}).filter(([o]) => o !== 'FOLLOW_MAJORITY'); const maxCount = entries.reduce((m, [, c]) => Math.max(m, Number(c) || 0), 0); @@ -379,7 +432,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { if (maxCount === 0 || top.length > 1) return noResult; const w = top[0][0]; const label = i18n['vote' + w.split('_').map(x => x.charAt(0) + x.slice(1).toLowerCase()).join('')] || w; - return { text: label, color: w === 'YES' ? '#4caf50' : w === 'NO' ? '#e53935' : '#ffcc00' }; + return { text: label, tone: w === 'YES' ? 'yes' : w === 'NO' ? 'no' : 'none' }; })(); cardBody.push( @@ -392,7 +445,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), - span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '') + span({ class: 'card-value' }, deadline ? moment(deadline).format("YYYY/MM/DD HH:mm") : '') ), div( { class: 'card-field' }, @@ -402,7 +455,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div( { class: 'card-field' }, span({ class: 'card-label' }, (i18n.voteResults || 'Results') + ':'), - span({ class: 'card-value', style: `color:${voteOutcome.color};font-weight:bold;` }, voteOutcome.text) + span({ class: `card-value vote-outcome vote-outcome-${voteOutcome.tone}` }, voteOutcome.text) ), div( { class: 'card-field' }, @@ -423,7 +476,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-section transfer' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.concept + ':'), span({ class: 'card-value' }, concept)), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.amount + ':'), span({ class: 'card-value' }, amount)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')), + deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, moment(deadline).format("YYYY/MM/DD HH:mm"))) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) ) ); @@ -441,90 +494,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); } - if (type === 'bankClaim') { - const { amount, epochId, allocationId, txid } = content; - const amt = Number(amount || 0); - cardBody.push( - div({ class: 'card-section banking-claim' }, - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiReceived + ':' ), - span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`) - ), - epochId ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankEpochShort + ':' ), - span({ class: 'card-value' }, epochId) - ) : "", - allocationId ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankAllocId + ':' ), - span({ class: 'card-value' }, allocationId) - ) : "", - txid ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankTx + ':' ), - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${txid}`, target: '_blank' }, txid) - ) : "" - ) - ); - } - if (type === 'ubiClaim') { - const { pubId, amount, epochId, claimedAt } = content; - const amt = Number(amount || 0); - const inhabitantId = action.author || ''; - cardBody.push( - div({ class: 'card-section banking-ubi' }, - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiInhabitant + ':'), - span({ class: 'card-value' }, userLink(inhabitantId)) - ), - pubId ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiPub + ':'), - span({ class: 'card-value' }, userLink(pubId)) - ) : "", - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiClaimedAmount + ':'), - span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`) - ), - epochId ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankEpochShort + ':'), - span({ class: 'card-value' }, epochId) - ) : "", - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.status + ':'), - span({ class: 'card-value' }, 'UNCONFIRMED') - ), - claimedAt ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.date + ':'), - span({ class: 'card-value' }, moment(claimedAt).format('YYYY-MM-DD HH:mm:ss')) - ) : "" - ) - ); - } - if (type === 'ubiclaimresult') { - const { txid, userId: inhabitantId, amount, epochId } = content; - const pubAuthor = action.author || action.value?.author || ''; - const amt = Number(amount || 0); - cardBody.push( - div({ class: 'card-section banking-ubi' }, - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiPub + ':'), - span({ class: 'card-value' }, pubAuthor) - ), - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiInhabitant + ':'), - span({ class: 'card-value' }, inhabitantId || '') - ), - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiClaimedAmount + ':'), - span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`) - ), - txid ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankTx + ':'), - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${txid}`, target: '_blank' }, txid) - ) : "" - ) - ); - } if (type === 'pixelia') { const { author } = content; @@ -545,35 +516,25 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { img({ src: `/assets/larp/images/${encodeURIComponent(String(house).toLowerCase())}.jpg`, alt: house, class: 'larp-house-mini' }), span(`L.A.R.P · ${String(house).toUpperCase()}`) ) : null, - text ? p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(String(text))) : null + text ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(text))) : null ) ); } if (type === 'tribe') { - const { title, image, description, location, tags, inviteMode, isAnonymous } = content; + const { title, image, description, isAnonymous } = content; if (isAnonymous === true) { skip = true; } - const validTags = Array.isArray(tags) ? tags : []; cardBody.push( div({ class: 'card-section tribe' }, h2({ class: 'tribe-title' }, a({ href: `/tribe/${encodeURIComponent(action.id)}`, class: "user-link" }, title) ), - div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' }, - location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "", - typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "", - inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "" - ), image ? (/^(\/|https?:)/.test(String(image)) - ? img({ src: image, class: 'feed-image tribe-image' }) + ? renderZoomableImage(image, { imgClass: 'feed-image tribe-image' }) : renderMediaBlob(image, null)) : null, - p({ class: 'tribe-description' }, ...renderUrl(description || '')), - validTags.length - ? div({ class: 'card-tags' }, validTags.map(tag => - a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) - : "" + p({ class: 'tribe-description' }, ...renderStyledText(description || '')) ) ); } @@ -605,11 +566,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { photo ? [ br(), - img({ class: "cv-photo", src: `/blob/${encodeURIComponent(photo)}` }), + renderZoomableImage(`/blob/${encodeURIComponent(photo)}`, { imgClass: 'cv-photo', linkClass: 'zoom-inline' }), br() ] : "", - p(...renderUrl(description || "")), + p(...renderStyledText(description || "")), personalSkills && personalSkills.length ? div({ class: 'card-tags' }, personalSkills.map(skill => a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`) @@ -634,7 +595,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const { url } = content; cardBody.push( div({ class: 'card-section image' }, - img({ src: `/blob/${encodeURIComponent(url)}`, class: 'post-image' }) + renderZoomableImage(`/blob/${encodeURIComponent(url)}`, { imgClass: 'post-image' }) ) ); } @@ -644,7 +605,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const mapKey = action.id || action.key || ''; cardBody.push( div({ class: 'card-section map' }, - title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.mapTitle || 'Map') + ':'), span({ class: 'card-value' }, mapKey ? a({ href: `/maps/${encodeURIComponent(mapKey)}`, class: 'user-link' }, title) : title)) : "", + title ? div({ class: 'card-field' }, span({ class: 'card-value' }, mapKey ? a({ href: `/maps/${encodeURIComponent(mapKey)}`, class: 'user-link' }, title) : title)) : "", div({ class: 'map-card-info' }, span({ class: 'map-type-badge' }, mapType || 'SINGLE'), span({ class: 'map-coords' }, `${(parseFloat(lat) || 0).toFixed(4)}, ${(parseFloat(lng) || 0).toFixed(4)}`) @@ -705,17 +666,28 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'torrent') { - const { title } = content; + const { title, url, source, sourceName } = content; cardBody.push( div({ class: 'card-section' }, - title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.torrentTitleLabel || 'Title') + ':'), - span({ class: 'card-value' }, title) + title?.trim() ? h2({ class: 'torrent-title' }, title) : "", + url ? div({ class: 'torrent-card-actions' }, + renderTorrentSourceDownload(action.tipId || action.id || action.key, source), + renderTorrentDownload(torrentDownloadHref(url, title)) ) : null ) ); } + if (type === 'file') { + const { title, url, fileName, torrentUrl } = content; + cardBody.push( + div({ class: 'card-section' }, + title?.trim() ? h2({ class: 'torrent-title' }, title) : "", + url ? renderFileDownloads(action.tipId || action.id || action.key, torrentUrl, fileName || title) : null + ) + ); + } + if (type === 'document') { const { url, title, key } = content; const docKey = title ? `${action.author || ''}::${title.trim()}` : null; @@ -739,22 +711,21 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const { url } = content; cardBody.push( div({ class: 'card-section bookmark' }, - h2(url ? p(a({ href: url, target: '_blank', class: "bookmark-url" }, url)) : "") + url ? div({ class: 'card-field' }, a({ href: safeExternalHref(url), target: '_blank', rel: 'noreferrer noopener', class: 'bookmark-url' }, url)) : '' ) ); } if (type === 'event') { - const { title, description, date, location, price, attendees, organizer, isPublic } = content; + const { title, description, date, location, price, attendees, isPublic } = content; cardBody.push( div({ class: 'card-section event' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)), - date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.date + ':'), span({ class: 'card-value' }, new Date(date).toLocaleString())) : "", + date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.date + ':'), span({ class: 'card-value' }, moment(date).format("YYYY/MM/DD HH:mm"))) : "", location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.location || 'Location') + ':'), span({ class: 'card-value' }, location)) : "", typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "", - price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "", + Number(price) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "", br(), - organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), userLink(organizer)) : "", Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : "" ) ); @@ -766,8 +737,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-section task' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)), priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.priority || 'Priority') + ':'), span({ class: 'card-value' }, priority)) : "", - startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskStartTimeLabel || 'Start') + ':'), span({ class: 'card-value' }, new Date(startTime).toLocaleString())) : "", - endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskEndTimeLabel || 'End') + ':'), span({ class: 'card-value' }, new Date(endTime).toLocaleString())) : "", + startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskStartTimeLabel || 'Start') + ':'), span({ class: 'card-value' }, moment(startTime).format("YYYY/MM/DD HH:mm"))) : "", + endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskEndTimeLabel || 'End') + ':'), span({ class: 'card-value' }, moment(endTime).format("YYYY/MM/DD HH:mm"))) : "", status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : "" ) ); @@ -802,15 +773,15 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'feed') { - const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); + const { renderStyledHtml } = require('../backend/renderStyledText'); const { text, refeeds } = content; if (!isValidFeedText(text)) return null; const safeText = cleanFeedText(text); - const htmlText = safeText ? rewriteHashtagLinks(renderTextWithStyles(safeText)) : ''; + const textNodes = safeText ? renderStyledText(safeText, { zoomImages: true }) : []; const refeedsNum = Number(refeeds || 0) || 0; cardBody.push( div({ class: 'card-section feed' }, - div({ class: 'feed-text', innerHTML: sanitizeHtml(htmlText) }), + div({ class: 'feed-text' }, ...textNodes), refeedsNum > 0 ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), @@ -824,7 +795,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { if (type === 'post') { const { contentWarning, text } = content || {}; const rawText = text || ''; - const POST_TRUNCATE_LEN = 300; + const POST_TRUNCATE_LEN = 1000; const isTruncated = rawText.length > POST_TRUNCATE_LEN; const displayText = isTruncated ? rawText.slice(0, POST_TRUNCATE_LEN) + '…' : rawText; const isHtml = typeof displayText === 'string' && /<\/?[a-z][\s\S]*>/i.test(displayText); @@ -838,9 +809,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { (url) => `${url}` ); - bodyNode = div({ class: 'post-text', style: 'max-height:180px;overflow:hidden;', innerHTML: sanitizeHtml(linkified) }); + bodyNode = div({ class: 'feed-text post-text post-text-clamped', [RAW_HTML]: sanitizeHtml(linkified) }); } else { - bodyNode = p({ class: 'post-text post-text-pre', style: 'max-height:180px;overflow:hidden;' }, ...renderUrlPreserveNewlines(displayText)); + bodyNode = div({ class: 'feed-text' }, + p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderStyledText(displayText)) + ); } const threadId = getThreadIdFromPost(action); const replyToId = getReplyToIdFromPost(action, byIdAll); @@ -855,20 +828,19 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-section post' }, isReply ? div( - { class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' }, - span({ style: 'font-size:0.85em;' }, + { class: 'reply-context' }, + span({ class: 'reply-context-label' }, a({ href: ctxHref, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : '' ), - parentText ? p({ class: 'post-text reply-context-text post-text-pre', style: 'font-size:0.85em;max-height:80px;overflow:hidden;margin-top:4px;' }, ...renderUrlPreserveNewlines(parentText)) : '' + parentText ? p({ class: 'post-text reply-context-text post-text-pre' }, ...renderStyledText(parentText)) : '' ) : '', contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '', bodyNode, - isTruncated && threadId - ? div({ style: 'margin-top:6px;' }, - a({ href: `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(action.id || threadId)}`, class: 'filter-btn' }, i18n.keepReading || 'Keep reading...') - ) + Array.isArray(content?.tags) && content.tags.filter(Boolean).length + ? div({ class: 'card-tags' }, content.tags.filter(Boolean).map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) : '' ) ); @@ -877,49 +849,125 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { if (type === 'postThread') { const c = action.content || {}; const threadId = c.threadId; - const href = `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(threadId)}`; + const rootType = c.rootType || 'post'; + const isBlogThread = rootType === 'post'; + const baseHref = c.rootHref || `/blogs/${encodeURIComponent(threadId)}`; + const href = isBlogThread ? baseHref : `${baseHref}${baseHref.includes('?') ? '&' : '?'}comments=open#comments-latest`; + const rootTypeLabel = isBlogThread + ? (i18n.typePost || 'BLOG') + : (i18n[`type${rootType.charAt(0).toUpperCase()}${rootType.slice(1)}`] || rootType); const root = c.root; const replies = Array.isArray(c.replies) ? c.replies : []; const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0)); - const limit = 5; - const overflow = repliesAsc.length > limit; - const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit)); - const lastId = repliesAsc.length ? repliesAsc[repliesAsc.length - 1].id : threadId; - const viewMoreHref = `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(lastId)}`; + const latest = repliesAsc.length ? repliesAsc[repliesAsc.length - 1] : null; + const titleSource = root ? (root.subject || root.text || '') : ''; + const titleText = titleSource + ? (titleSource.length > 90 ? `${titleSource.slice(0, 90)}…` : titleSource) + : threadId; return div({ class: 'trending-card post-thread' + (String(action.author) === String(userId) ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, `${String(i18n.typePost || 'POST').toUpperCase()} · THREAD`) + span({ class: 'pm-exposition-text' }, `${String(rootTypeLabel).toUpperCase()} · ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`) ) ), - renderContentActions(threadId, href) + renderContentActions(threadId, baseHref, { + author: c.rootAuthor || (root && root.author) || action.author, + reportTitle: titleText, + spread: spreadMap.get(threadId) || null, + ...favOptsFor(rootType, threadId, extras) + }) ), div({ class: 'card-body' }, - root && root.text - ? div({ class: 'card-section' }, - p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(root.text)) - ) - : '', - div({ class: 'card-section' }, - show.map(r => { - const rDate = r.ts ? new Date(r.ts).toLocaleString() : ''; - return div({ class: 'thread-reply-item' }, - div({ class: 'thread-reply' }, - r.text ? p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(r.text)) : '' - ), - div({ class: 'card-footer thread-reply-footer' }, - span({ class: 'date-link' }, rDate), - userLink(r.author, action.authorNames && action.authorNames[r.author]) - ) - ); - }), + div({ class: 'card-section' }, + div({ class: 'card-field activity-update-title' }, + a({ href, class: 'card-value user-link' }, titleText), + span({ class: 'card-label activity-update-counts' }, `💬 ${replies.length}`) + ), + latest && latest.text + ? div({ class: 'feed-text activity-update-msg' }, + p({ class: 'post-text post-text-pre' }, ...renderStyledText(latest.text)) + ) + : '' + ) + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), + userLink(action.author, action.authorNames && action.authorNames[action.author]) + ) + ); + } + + if (type === 'chatThread') { + const c = action.content || {}; + const chatRoot = c.chatRoot; + const href = `/chats/${encodeURIComponent(chatRoot)}`; + const latestHref = `${href}#chat-latest`; + const chatTitle = c.title || chatRoot; + const replies = Array.isArray(c.replies) ? c.replies : []; + const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0)); + const limit = 2; + const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit)); + return div({ class: 'trending-card post-thread chat-thread' + (String(action.author) === String(userId) ? ' own-content' : '') }, + div({ class: 'card-header activity-card-header' }, + div({ class: 'card-chips-row' }, + span({ class: 'pm-exposition-chip pm-exposition-whole' }, + span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`) + ) + ), + renderContentActions(chatRoot, href, { + author: c.chatAuthor || action.author, + reportTitle: chatTitle, + spread: spreadMap.get(chatRoot) || null, + ...favOptsFor('chat', chatRoot, extras) + }) + ), + div({ class: 'card-body' }, + div({ class: 'card-section chat' }, + div({ class: 'card-field activity-update-title' }, + a({ href: latestHref, class: 'card-value user-link' }, chatTitle), + span({ class: 'card-label activity-update-counts' }, `👥: ${c.members || 0} · 💬 ${c.messageCount || show.length}`) + ), + (() => { + const latest = show.length ? show[show.length - 1] : null; + if (!latest) return ''; + const rawBlob = String(latest.image || '').trim(); + const attSrc = rawBlob.startsWith('&') ? `/blob/${encodeURIComponent(rawBlob)}` : null; + const mime = String(latest.mimeType || ''); + const attNode = attSrc + ? (mime.startsWith('video/') + ? videoHyperaxe({ controls: true, class: 'post-video', src: attSrc, type: mime, preload: 'metadata' }) + : mime.startsWith('audio/') + ? audioHyperaxe({ controls: true, class: 'post-audio', src: attSrc, type: mime, preload: 'metadata' }) + : mime === 'application/pdf' + ? a({ href: attSrc, target: '_blank', rel: 'noopener', class: 'filter-btn' }, '📄 PDF') + : mime.includes('bittorrent') || mime === 'application/x-torrent' + ? renderTorrentDownload(attSrc) + : renderZoomableImage(attSrc, { imgClass: 'post-image' })) + : renderMediaBlob(latest.image); + const textNode = latest.text + ? p({ class: 'post-text post-text-pre' }, ...(latest.poll ? [span({ class: 'card-label' }, `${String(i18n.pollInChat || 'Poll').toUpperCase()}: `)] : []), ...renderStyledText(latest.text)) + : null; + const quoted = latest.reply || null; + const replyNode = quoted + ? div({ class: 'reply-context' }, + span({ class: 'reply-context-meta' }, + a({ href: `${href}#msg-${String(quoted.id || '').replace(/[^a-zA-Z0-9]/g, '')}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), + quoted.author ? span(' ', userLink(quoted.author, action.authorNames && action.authorNames[quoted.author])) : '' + ), + quoted.text ? p({ class: 'post-text reply-context-text' }, quoted.text) : '' + ) + : null; + return (textNode || attNode || replyNode) + ? div({ class: 'feed-text activity-update-msg' }, ...[replyNode, textNode, attNode].filter(Boolean)) + : ''; + })() + ) + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), + userLink(action.author, action.authorNames && action.authorNames[action.author]) ) - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `), - userLink(action.author, action.authorNames && action.authorNames[action.author]) - ) ); } @@ -930,9 +978,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const linkText = (title && String(title).trim()) ? title : ''; cardBody.push( div({ class: 'card-section forum' }, - div({ class: 'card-field', style: "font-size:1.12em; margin-bottom:5px;" }, - span({ class: 'card-label', style: "font-weight:800;color:#ff9800;" }, i18n.title + ': '), - a({ href: `/forum/${encodeURIComponent(linkKey)}`, style: "font-weight:800;color:#4fc3f7;" }, linkText) + div({ class: 'card-field' }, + linkKey + ? a({ href: `/forum/${encodeURIComponent(linkKey)}`, class: 'card-value user-link' }, linkText || linkKey) + : span({ class: 'card-value' }, linkText) ) ) ); @@ -941,21 +990,23 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const parentForum = byIdAll.get(rootId) || actions.find(a => a.type === 'forum' && !a.content?.root && (a.id === rootId || a.content?.key === rootId)); const parentContent = parentForum ? (parentForum.value?.content || parentForum.content || {}) : {}; const parentTitle = (parentContent?.title && String(parentContent.title).trim()) ? parentContent.title : ((rootTitle && String(rootTitle).trim()) ? rootTitle : ''); - const parentAuthor = parentForum?.author || ''; - const parentName = parentForum?.authorName || ''; + const branchId = typeof content.branch === 'string' && content.branch !== rootId && content.branch !== rootKey ? content.branch : ''; + const branchMsg = branchId ? (byIdAll.get(branchId) || actions.find(a => a.type === 'forum' && (a.id === branchId || a.content?.key === branchId))) : null; + const repliedAuthor = branchMsg ? (branchMsg.author || '') : ''; + const repliedName = branchMsg ? (branchMsg.authorName || '') : ''; const hrefKey = rootKey || rootId; cardBody.push( div({ class: 'card-section forum' }, div( - { class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' }, - span({ style: 'font-size:0.85em;' }, - a({ href: `/forum/${encodeURIComponent(hrefKey)}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), - parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : '' + { class: 'reply-context' }, + span({ class: 'reply-context-meta' }, + a({ href: `/forum/${encodeURIComponent(hrefKey)}#msg-${String(action.tipId || action.id || '').replace(/[^a-zA-Z0-9]/g, '')}`, class: 'tag-link' }, repliedAuthor ? i18n.inReplyTo : i18n.forumInThread), + repliedAuthor ? span(' ', userLink(repliedAuthor, repliedName)) : '' ), - parentTitle ? p({ class: 'post-text reply-context-text', style: 'font-size:0.85em;max-height:60px;overflow:hidden;margin-top:4px;font-weight:bold;color:#4fc3f7;' }, parentTitle) : '' + parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : '' ), - div({ class: 'card-field', style: 'margin-bottom:12px;' }, - p({ style: "margin:0 0 8px 0; word-break:break-all;" }, ...renderUrl(text)) + div({ class: 'card-field forum-reply-body' }, + p({ class: 'forum-reply-text' }, ...renderStyledText(text)) ) ) ); @@ -998,11 +1049,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '', spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '', spreadExcerpt - ? div({ class: 'post-text activity-spread-text post-text-pre', style: 'max-height:200px;overflow:hidden;' }, ...renderUrlPreserveNewlines(spreadExcerpt)) - : div({ class: 'post-text activity-spread-text', style: 'opacity:0.6;font-style:italic;' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), + ? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderStyledText(spreadExcerpt)) + : div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), spreadOriginalAuthor ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.spreadBy || 'By') + ': '), span({ class: 'card-value' }, userLink(spreadOriginalAuthor)) ) : '', @@ -1013,7 +1063,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ) : '', link - ? div({ style: 'margin-top:6px;' }, + ? div({ class: 'card-section-action' }, a({ href: `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}`, class: 'filter-btn' }, i18n.viewDetails || 'View details') ) : '' @@ -1037,30 +1087,23 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const imgId = image ? (typeof image === 'string' ? image : (image.link || '')) : ''; cardBody.push( div({ class: 'card-section about' }, - imgId - ? img({ src: `/blob/${encodeURIComponent(imgId)}`, alt: name, class: 'activity-avatar' }) - : img({ src: '/assets/images/default-avatar.png', alt: name, class: 'activity-avatar' }), - h2(userLink(about, name)), - description ? p({ class: 'tribe-side-description' }, ...renderUrlPreserveNewlines(String(description))) : null + div({ class: 'about-card-cols' }, + div({ class: 'about-card-col-img' }, + imgId + ? renderZoomableImage(`/blob/${encodeURIComponent(imgId)}`, { imgClass: 'activity-avatar', alt: name, linkClass: 'zoom-inline' }) + : img({ src: '/assets/images/default-avatar.png', alt: name, class: 'activity-avatar' }) + ), + div({ class: 'about-card-col-desc' }, + h2({ class: 'about-card-header' }, userLink(about, name)), + description + ? p({ class: 'tribe-side-description' }, ...renderStyledText(String(description))) + : null + ) + ) ) ); } - if (type === 'pub') { - const { address } = content || {}; - const { key } = address || {}; - const pr = getProfile(key || ''); - const src = pr.image ? `/blob/${encodeURIComponent(pr.image)}` : '/assets/images/default-avatar.png'; - cardBody.push( - div({ class: 'card-section pub activity-pub' }, - br(), - userLink(pr.id, pr.name), - br(), - img({ src, alt: pr.name || pr.id, class: 'activity-avatar' }) - ) - ); - } - if (type === 'market') { const { item_type, title, price, status, deadline, stock, image, auctions_poll, seller } = content; const isSeller = seller && userId && seller === userId; @@ -1069,16 +1112,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemTitle + ':'), span({ class: 'card-value' }, title)), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, item_type.toUpperCase())), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStatus + ": " ), span({ class: 'card-value' }, status.toUpperCase())), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : "")), + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? moment(deadline).format("YYYY/MM/DD HH:mm") : "")), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, stock)), + div({ class: "price-chip" }, `${price} ECO`), br(), - image - ? renderMediaBlob(image, '/assets/images/default-market.png') - : img({ src: '/assets/images/default-market.png', alt: title, class: 'post-image' }), - br(), - div({ class: "market-card price" }, - p(`${i18n.marketItemPrice}: ${price} ECO`) - ), + image ? renderMediaBlob(image, '/assets/images/default-market.png') : null, item_type === 'auction' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller ? div({ class: "auction-info" }, auctions_poll && auctions_poll.length > 0 @@ -1098,16 +1136,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); }) ) - : p(i18n.marketNoBids), - form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` }, - input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }), - br(), - button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton) - ) - ) : "", - item_type === 'exchange' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller - ? form({ method: "POST", action: `/market/buy/${encodeURIComponent(action.id)}` }, - button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy) + : p(i18n.marketNoBids) ) : "" ) ); @@ -1119,9 +1148,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const displayDesc = shortDescription || (description ? (description.length > 140 ? description.slice(0, 140) + "\u2026" : description) : ""); cardBody.push( div({ class: 'card-section shop' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopTitle || 'Shop') + ':'), span({ class: 'card-value' }, shopKey ? a({ href: `/shops/${encodeURIComponent(shopKey)}`, class: 'user-link' }, title || shopKey) : (title || ''))), - displayDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.description || 'Description') + ':'), span({ class: 'card-value' }, displayDesc)) : "", - visibility ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopVisibility || 'Visibility') + ':'), span({ class: 'card-value' }, visibility)) : "", + div({ class: 'card-field' }, span({ class: 'card-value' }, shopKey ? a({ href: `/shops/${encodeURIComponent(shopKey)}`, class: 'user-link' }, title || shopKey) : (title || ''))), + displayDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, displayDesc)) : "", location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopLocation || 'Location') + ':'), span({ class: 'card-value' }, location)) : "" ) ); @@ -1134,15 +1162,109 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const prodImageNode = renderMediaBlob(prodImage); cardBody.push( div({ class: 'card-section shop' }, - shopId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopTitle || 'Shop') + ':'), span({ class: 'card-value' }, a({ href: `/shops/${encodeURIComponent(shopId)}`, class: 'user-link' }, shopLabel))) : '', - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductTitle || 'Product') + ':'), span({ class: 'card-value' }, title || '')), + shopId ? div({ class: 'card-field' }, span({ class: 'card-value' }, a({ href: `/shops/${encodeURIComponent(shopId)}`, class: 'user-link' }, shopLabel))) : '', + div({ class: 'card-field' }, span({ class: 'card-value' }, title || '')), + div({ class: 'price-chip' }, `${Number(price || 0).toFixed(6)} ECO`), prodImageNode ? div({ class: 'card-field' }, prodImageNode) : '', - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductPrice || 'Price') + ':'), span({ class: 'card-value' }, `${Number(price || 0).toFixed(6)} ECO`)), div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductStock || 'Stock') + ':'), span({ class: 'card-value' }, String(stock || 0))) ) ); } + if (type === 'industry') { + const { name, sector, description, image, tags, membershipPolicy, status } = content; + const facKey = action.id || action.key || ''; + const validTags = Array.isArray(tags) ? tags : []; + const statusValue = (() => { + const u = String(status || 'ACTIVE').toUpperCase(); + if (u === 'PAUSED') return i18n.industryStatusPaused || 'PAUSED'; + if (u === 'DISSOLVED') return i18n.industryStatusDissolved || 'DISSOLVED'; + return i18n.industryStatusActive || 'WORKING'; + })(); + const policyValue = String(i18n[`industryPolicy_${membershipPolicy}`] || membershipPolicy || '').toUpperCase(); + cardBody.push( + div({ class: 'card-section tribe industry' }, + h2({ class: 'tribe-title' }, + a({ href: `/industry/${encodeURIComponent(facKey)}`, class: "user-link" }, name || (i18n.industryTitle || 'Industry')) + ), + div({ class: 'card-field-row' }, + sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industrySector || 'Sector').toUpperCase() + ':'), span({ class: 'card-value' }, String(sector).toUpperCase())) : "", + status ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, statusValue)) : "", + membershipPolicy ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryMembershipPolicy || 'Membership policy').toUpperCase() + ':'), span({ class: 'card-value' }, policyValue)) : "" + ), + image + ? (/^(\/|https?:)/.test(String(image)) + ? renderZoomableImage(image, { imgClass: 'feed-image tribe-image' }) + : renderMediaBlob(image, null)) + : null, + p({ class: 'tribe-description' }, ...renderStyledText(description || '')), + validTags.length + ? div({ class: 'card-tags' }, validTags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : "" + ) + ); + } + + if (type === 'industryBuild') { + const { title, notes, image, startDate, endDate, buildStatus } = content; + const buildKey = action.id || action.key || ''; + const daysLeft = endDate ? Math.max(0, Math.ceil((new Date(endDate) - Date.now()) / 86400000)) : null; + cardBody.push( + div({ class: 'card-section tribe industry' }, + h2({ class: 'tribe-title' }, + a({ href: `/industry/build/${encodeURIComponent(buildKey)}`, class: "user-link" }, title || (i18n.industryBuild || 'Build')) + ), + (buildStatus || startDate || endDate) ? div({ class: 'card-field-row' }, + buildStatus ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryBuildStatus_' + buildStatus] || buildStatus).toUpperCase())) : "", + startDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildStart || 'Start date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(startDate).format("YYYY/MM/DD"))) : "", + endDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildEnd || 'End date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(endDate).format('YYYY-MM-DD'))) : "", + daysLeft != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryTimeLeft || 'Time left').toUpperCase() + ':'), span({ class: 'card-value' }, `${daysLeft}d`)) : "" + ) : null, + image + ? (/^(\/|https?:)/.test(String(image)) + ? renderZoomableImage(image, { imgClass: 'feed-image tribe-image' }) + : renderMediaBlob(image, null)) + : null, + notes ? p({ class: 'tribe-description' }, ...renderStyledText(notes)) : "" + ) + ); + } + + if (type === 'industryBlueprint') { + const { name, facility, description, outKind, laborHours, image, estTotal } = content; + cardBody.push( + div({ class: 'card-section tribe industry' }, + h2({ class: 'tribe-title' }, + facility + ? a({ href: `/industry/${encodeURIComponent(facility)}`, class: "user-link" }, name || (i18n.industryBlueprint || 'Blueprint')) + : (name || (i18n.industryBlueprint || 'Blueprint')) + ), + div({ class: 'card-field-row' }, + outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryOutputKind || 'Product type').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + outKind] || outKind).toUpperCase())) : "", + laborHours ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryLaborHours || 'Labor hours').toUpperCase() + ':'), span({ class: 'card-value' }, String(laborHours))) : "", + estTotal != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryEstTotal || 'Estimated price').toUpperCase() + ':'), span({ class: 'card-value' }, `${Number(estTotal).toFixed(2)} ECO`)) : "" + ), + image + ? (/^(\/|https?:)/.test(String(image)) + ? renderZoomableImage(image, { imgClass: 'feed-image tribe-image' }) + : renderMediaBlob(image, null)) + : null, + description ? p({ class: 'tribe-description' }, ...renderStyledText(description)) : "" + ) + ); + } + + if (type === 'industryAllocation') { + const { target, pot } = content; + cardBody.push( + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.industryDistribution || 'Distribution') + ':'), span({ class: 'card-value' }, target ? a({ href: `/industry/build/${encodeURIComponent(target)}`, class: 'user-link' }, i18n.industryBuild || 'Build') : '')), + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.industryPot || 'Pot') + ':'), span({ class: 'card-value' }, `${Number(pot || 0).toFixed(6)} ECO`)) + ) + ); + } + if (type === 'chat') { const { title, description, image, category, status } = content; const chatKey = action.id || action.key || ''; @@ -1150,10 +1272,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const chatImageNode = renderMediaBlob(image); cardBody.push( div({ class: 'card-section chat' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, 'Chat:'), span({ class: 'card-value' }, chatKey ? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'user-link' }, title || chatKey) : (title || ''))), - displayDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatDescription || 'Description') + ':'), span({ class: 'card-value' }, displayDesc)) : '', - category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '', - status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatStatus || 'Status') + ':'), span({ class: 'card-value' }, status)) : '' + div({ class: 'card-field' }, + chatKey + ? a({ href: `/chats/${encodeURIComponent(chatKey)}#chat-latest`, class: 'card-value user-link' }, title || chatKey) + : span({ class: 'card-value' }, title || '')), + displayDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, displayDesc)) : '', + category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '' ) ); } @@ -1163,19 +1287,185 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const padTitle = content.title || action.title || ''; cardBody.push( div({ class: 'card-section' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padTitle || 'Pad') + ':'), span({ class: 'card-value' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'user-link' }, padTitle || padKey) : '')), + div({ class: 'card-field' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'card-value user-link' }, padTitle || padKey) : span({ class: 'card-value' }, padTitle || '')), content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padDeadlineLabel || 'Deadline') + ':'), span({ class: 'card-value' }, content.deadline)) : '' ) ); } + if (type === 'room') { + const roomKey = action.id || action.key || ''; + const roomTitle = content.title || action.title || ''; + const roomNumber = roomNumberOf(action.rootId || roomKey); + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field activity-room-title' }, + roomKey ? a({ href: `/rooms/${encodeURIComponent(roomKey)}`, class: 'card-value user-link' }, roomTitle || roomKey) : span({ class: 'card-value' }, roomTitle || ''), + roomNumber ? renderPhoneChip(roomNumber, content.line === 'SWITCHBOARD' ? i18n.roomNumberHint : i18n.roomLineDndError) : '' + ), + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, String(content.description).slice(0, 200))) : '' + ) + ); + } + + if (type === 'emergency') { + const emergencyKey = action.id || action.key || ''; + const emergencyTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, emergencyKey ? a({ href: `/emergencies/${encodeURIComponent(emergencyKey)}`, class: 'card-value user-link' }, emergencyTitle || emergencyKey) : span({ class: 'card-value' }, emergencyTitle || '')), + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.statusLabel || 'Status') + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : '', + content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.emergencyCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '', + renderMediaBlob(content.text, null), + stripMediaMarkdown(content.text) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.text))) : '' + ) + ); + } + + if (type === 'mailingList') { + const listKey = action.id || action.key || ''; + const listTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, listKey ? a({ href: `/mailing/${encodeURIComponent(listKey)}`, class: 'card-value user-link' }, listTitle || listKey) : span({ class: 'card-value' }, listTitle || '')) + ) + ); + } + + if (type === 'logisticsRoute') { + const routeKey = action.id || action.key || ''; + const routeTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, routeKey ? a({ href: `/logistics/${encodeURIComponent(routeKey)}`, class: 'card-value user-link' }, routeTitle || routeKey) : span({ class: 'card-value' }, routeTitle || '')), + content.origin || content.destination ? div({ class: 'card-field' }, span({ class: 'card-value' }, `${content.origin || ''} → ${content.destination || ''}`)) : '', + content.kind ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.logisticsKindLabel || 'Type') + ':'), span({ class: 'card-value' }, `${String(content.kind).toUpperCase()} · ${String(content.mode || '').toUpperCase()}`)) : '', + content.date ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.logisticsDateLabel || 'Date') + ':'), span({ class: 'card-value' }, moment(content.date).format('YYYY/MM/DD HH:mm'))) : '', + renderMediaBlob(content.description, null), + stripMediaMarkdown(content.description) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.description))) : '' + ) + ); + } + + if (type === 'podcast' || type === 'podcastEpisode') { + const pKey = action.id || action.key || ''; + const pTitle = content.title || action.title || ''; + const pHref = type === 'podcast' ? `/podcasts/${encodeURIComponent(pKey)}` : `/podcasts/episode/${encodeURIComponent(pKey)}`; + const pDesc = stripMediaMarkdown(content.description); + const pShortDesc = pDesc ? (pDesc.length > 140 ? pDesc.slice(0, 140) + "\u2026" : pDesc) : ''; + cardBody.push( + div({ class: 'card-section' }, + type === 'podcastEpisode' ? '' : div({ class: 'card-field' }, pKey ? a({ href: pHref, class: 'card-value user-link' }, pTitle || pKey) : span({ class: 'card-value' }, pTitle || '')), + pShortDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, pShortDesc)) : '', + type === 'podcast' && content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.podcastCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '', + renderMediaObject(type === 'podcast' ? content.cover : content.media, type === 'podcast' ? pHref : null) + ) + ); + } + + if (type === 'campaign') { + const cpKey = action.id || action.key || ''; + const cpTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, cpKey ? a({ href: `/campaigns/${encodeURIComponent(cpKey)}`, class: 'card-value user-link' }, cpTitle || cpKey) : span({ class: 'card-value' }, cpTitle || '')), + content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.campaignCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '', + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.statusLabel || 'Status') + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : '', + renderMediaBlob(content.text, null), + stripMediaMarkdown(content.text) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.text))) : '', + content.goal ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.campaignSignaturesLabel || 'Signatures').toUpperCase() + ':'), span({ class: 'card-value' }, `${new Set(all.filter(x => x && x.type === 'campaignSignature' && x.content && (x.content.target === cpKey || x.content.target === (action.rootId || cpKey))).map(x => x.author)).size} / ${content.goal}`)) : '' + ) + ); + } + + if (type === 'campaignUpdate') { + const targetId = String(content.target || ''); + const parent = all.find(x => x && (x.id === targetId || x.key === targetId || x.rootId === targetId || x.tipId === targetId)) || null; + const parentContent = parent ? (parent.value?.content || parent.content || {}) : {}; + const parentTitle = parentContent.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'reply-context' }, + span({ class: 'reply-context-meta' }, + a({ href: `/campaigns/${encodeURIComponent(targetId)}`, class: 'tag-link' }, i18n.emergencyLastUpdate || 'LAST UPDATE'), + parentTitle ? span(' ', a({ href: `/campaigns/${encodeURIComponent(targetId)}`, class: 'user-link' }, parentTitle)) : '' + ) + ), + renderMediaBlob(content.text, null), + stripMediaMarkdown(content.text) ? div({ class: 'card-field' }, p({ class: 'forum-reply-text' }, ...renderStyledText(stripMediaMarkdown(content.text)))) : '' + ) + ); + } + + if (type === 'emergencyUpdate') { + const targetId = String(content.target || ''); + const parent = all.find(x => x && (x.id === targetId || x.key === targetId || x.rootId === targetId || x.tipId === targetId)) || null; + const parentContent = parent ? (parent.value?.content || parent.content || {}) : {}; + const parentTitle = parentContent.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'reply-context' }, + span({ class: 'reply-context-meta' }, + a({ href: `/emergencies/${encodeURIComponent(targetId)}`, class: 'tag-link' }, i18n.emergencyLastUpdate || 'LAST UPDATE'), + parentTitle ? span(' ', a({ href: `/emergencies/${encodeURIComponent(targetId)}`, class: 'user-link' }, parentTitle)) : '' + ) + ), + renderMediaBlob(content.text, null), + stripMediaMarkdown(content.text) ? div({ class: 'card-field' }, p({ class: 'forum-reply-text' }, ...renderStyledText(stripMediaMarkdown(content.text)))) : '' + ) + ); + } + + if (type === 'wikiPage') { + const wikiKey = action.id || action.key || ''; + const wikiTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, wikiKey ? a({ href: `/wiki/${encodeURIComponent(wikiKey)}`, class: 'card-value user-link' }, wikiTitle || wikiKey) : span({ class: 'card-value' }, wikiTitle || '')) + ) + ); + } + if (type === 'calendar') { const calKey = action.id || action.key || ''; const calTitle = content.title || action.title || ''; cardBody.push( div({ class: 'card-section' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarTitle || 'Calendar') + ':'), span({ class: 'card-value' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'user-link' }, calTitle || calKey) : '')), - content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarStatusLabel || 'Status') + ':'), span({ class: 'card-value' }, content.status)) : '' + div({ class: 'card-field' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'card-value user-link' }, calTitle || calKey) : span({ class: 'card-value' }, calTitle || '')) + ) + ); + } + + if (type === 'poll') { + const options = Array.isArray(content.options) ? content.options : []; + const counts = (action.pollCounts && typeof action.pollCounts === 'object') ? action.pollCounts : {}; + const voters = Number(action.pollVoters) || 0; + const pctOf = (opt) => (voters > 0 ? Math.round(((counts[opt] || 0) / voters) * 100) : 0); + const step = (opt) => Math.round(pctOf(opt) / 5) * 5; + cardBody.push( + div({ class: 'card-section poll' }, + div({ class: 'card-field' }, + span({ class: 'card-value' }, content.question || '') + ), + options.length + ? table({ class: 'poll-results' }, + ...options.map((opt, i) => + tr( + td({ class: 'poll-result-option' }, `${letterOf(i)})`), + td({ class: 'poll-result-bar' }, + div({ class: 'poll-bar' }, + div({ class: `poll-bar-fill poll-bar-fill-${step(opt)} poll-hue-${i % 8}` }) + ) + ), + td({ class: 'poll-result-count' }, `${counts[opt] || 0} (${pctOf(opt)}%)`) + ) + ) + ) + : null, + div({ class: 'card-field' }, + span({ class: 'card-label' }, `${i18n.pollVoters}: `), + span({ class: 'card-value' }, String(voters)) + ) ) ); } @@ -1227,10 +1517,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { : 'ACTION'; const actorId = activity.activityActor || ''; + const actorLink = `${escapeHtml(userLinkLabel(actorId))}`; + const projectLink = `${escapeHtml(title || '')}`; const msgHtml = tmpl - .replace('%OASIS%', `${userLinkLabel(actorId)}`) - .replace('%PROJECT%', `${title || ''}`) - .replace('%ACTION%', `${actionWord}`); + .replace('%OASIS%', () => actorLink) + .replace('%PROJECT%', () => projectLink) + .replace('%ACTION%', () => `${escapeHtml(actionWord)}`); return div({ class: 'card card-rpg' }, div({ class: 'card-header' }, @@ -1240,10 +1532,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ) ), div( - p({ innerHTML: sanitizeHtml(msgHtml) }) + p({ [RAW_HTML]: sanitizeHtml(msgHtml) }) ), p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `), + span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), userLink(action.author, action.authorNames && action.authorNames[action.author]) ) ); @@ -1299,38 +1591,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); } - if (type === 'aiExchange') { - const { ctx, lang, tags, rating } = content; - const helpful = Number(action.helpfulVotes || 0); - cardBody.push( - div({ class: 'card-section ai-exchange' }, - Array.isArray(ctx) && ctx.length - ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiSnippetsLearned || 'Snippets learned') + ':'), span({ class: 'card-value' }, String(ctx.length))) - : null, - lang - ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeLang || 'Language') + ':'), span({ class: 'card-value' }, String(lang).toUpperCase())) - : null, - Array.isArray(tags) && tags.length - ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeTags || 'Tags') + ':'), - span({ class: 'card-value' }, tags.map(t => span({ class: 'ai-exchange-tag' }, '#' + t)))) - : null, - rating > 0 - ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeRating || 'Rating') + ':'), span({ class: 'card-value' }, '★'.repeat(rating) + '☆'.repeat(Math.max(0, 5 - rating)))) - : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.aiExchangeHelpful || 'Helpful') + ':'), - span({ class: 'card-value' }, String(helpful)), - form({ method: 'POST', action: '/ai/exchange/vote', class: 'ai-exchange-vote-form' }, - input({ type: 'hidden', name: 'target', value: action.id }), - input({ type: 'hidden', name: 'helpful', value: 'yes' }), - input({ type: 'hidden', name: 'returnTo', value: '/activity' }), - button({ type: 'submit', class: 'filter-btn' }, i18n.aiExchangeMarkHelpful || '+1 helpful') - ) - ) - ) - ); - } - if (type === 'karmaScore') { const { karmaScore } = content; cardBody.push( @@ -1343,6 +1603,75 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); } + if (type === 'housing') { + const { title, housing_type, property_type, price, place, status, rooms, size, capacity } = content; + const isFree = String(housing_type || '').toLowerCase() === 'couchsurfing'; + cardBody.push( + div({ class: 'card-section report' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, title) + ), + housing_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingType + ':'), + span({ class: 'card-value' }, String(housing_type).toUpperCase()) + ) : null, + property_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingProperty + ':'), + span({ class: 'card-value' }, String(property_type).toUpperCase()) + ) : null, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPrice + ':'), + span({ class: 'card-value' }, isFree ? (i18n.housingFree || 'FREE') : `${Number(price || 0).toFixed(2)} ECO`) + ), + place ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPlace + ':'), + span({ class: 'card-value' }, place) + ) : null, + Number(rooms) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingRooms + ':'), + span({ class: 'card-value' }, String(rooms)) + ) : null, + Number(size) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingSize + ':'), + span({ class: 'card-value' }, `${size} m²`) + ) : null, + Number(capacity) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingCapacity + ':'), + span({ class: 'card-value' }, String(capacity)) + ) : null, + status ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingStatus + ':'), + span({ class: 'card-value' }, String(status).toUpperCase()) + ) : null + ) + ); + } + + if (type === 'schoolCourse') { + const { title, description, price, visibility, startDate } = content; + const coursePaid = Number(price) > 0; + cardBody.push( + div({ class: 'card-section report' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, title) + ), + description ? div({ class: 'card-field' }, + span({ class: 'card-value' }, String(description).length > 220 ? String(description).slice(0, 220) + '…' : description) + ) : null, + coursePaid ? div({ class: 'card-field' }, + span({ class: 'card-label' }, (i18n.schoolPrice || 'Price (ECO)') + ':'), + span({ class: 'card-value' }, `${Number(price).toFixed(2)} ECO`) + ) : null, + startDate ? div({ class: 'card-field' }, + span({ class: 'card-label' }, (i18n.schoolStartDate || 'Start date') + ':'), + span({ class: 'card-value' }, moment(startDate).format("YYYY/MM/DD HH:mm")) + ) : null + ) + ); + } + if (type === 'job') { const { title, job_type, tasks, location, vacants, salary, status, subscribers } = content; cardBody.push( @@ -1351,26 +1680,26 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title) ), - salary && div({ class: 'card-field' }, + salary ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSalary + ':'), span({ class: 'card-value' }, salary + ' ECO') - ), - status && div({ class: 'card-field' }, + ) : null, + status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobStatus + ':'), - span({ class: 'card-value' }, status.toUpperCase()) - ), - job_type && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(status).toUpperCase()) + ) : null, + job_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobType + ':'), - span({ class: 'card-value' }, job_type.toUpperCase()) - ), - location && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(job_type).toUpperCase()) + ) : null, + location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobLocation + ':'), - span({ class: 'card-value' }, location.toUpperCase()) - ), - vacants && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(location).toUpperCase()) + ) : null, + Number(vacants) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobVacants + ':'), - span({ class: 'card-value' }, vacants) - ), + span({ class: 'card-value' }, String(vacants)) + ) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSubscribers + ':'), span({ class: 'card-value' }, @@ -1402,7 +1731,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'parliamentTerm') { - const { method, powerType, powerId, powerTitle, winnerVotes, totalVotes, startAt, endAt } = content; + const { method, powerType, powerId, powerTitle, startAt, endAt } = content; const powerTypeNorm = String(powerType || '').toLowerCase(); const winnerLink = powerTypeNorm === 'tribe' @@ -1411,23 +1740,23 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ? a({ href: `/parliament?filter=government`, class: 'user-link' }, (i18n.parliamentAnarchy || 'ANARCHY')) : userLink(powerId, powerTitle); - const methodUpper = String( - i18n['parliamentMethod' + String(method || '').toUpperCase()] || method - ).toUpperCase(); - cardBody.push( div({ class: 'card-section parliament' }, - startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, new Date(startAt).toLocaleString())) : '', - endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, new Date(endAt).toLocaleString())) : '', - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentCurrentLeader.toUpperCase() || 'Winning candidature') + ':'), span({ class: 'card-value' }, winnerLink)), - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod.toUpperCase() || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived.toUpperCase() || 'Votes received') + ':'), span({ class: 'card-value' }, `${Number(winnerVotes || 0)} (${Number(totalVotes || 0)})`)) + startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, moment(startAt).format("YYYY/MM/DD HH:mm"))) : '', + endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, moment(endAt).format("YYYY/MM/DD HH:mm"))) : '', + div({ class: 'card-field' }, span({ class: 'card-value' }, winnerLink)) ) ); } if (type === 'parliamentProposal') { - const { title, description, method, status, voteId, createdAt } = content; + const { title, method, status, voteId, createdAt } = content; + const legacy = !content.campaignId ? String(content.description || '').match(/\n*\s*(\d+) signatures: \/campaigns\/(%[^\s]+\.sha256)\s*$/) : null; + const description = legacy ? String(content.description || '').slice(0, legacy.index).trim() : content.description; + const campaignId = content.campaignId || (legacy ? legacy[2] : ''); + const liveSignatures = campaignId ? new Set(all.filter(x => x && x.type === 'campaignSignature' && x.content && x.content.target === campaignId).map(x => x.author)).size : 0; + const signatures = Math.max(liveSignatures, content.campaignId ? (Number(content.signatures) || 0) : (legacy ? Number(legacy[1]) || 0 : 0)); + const goal = Number(content.goal) || 0; const methodUpper = String( i18n['parliamentMethod' + String(method || '').toUpperCase()] || method @@ -1436,9 +1765,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '', - description ? p({ style: 'margin:.4rem 0' }, description) : '', + renderMediaBlob(description, null), + stripMediaMarkdown(description) ? p({ class: 'card-section-text' }, ...renderStyledText(stripMediaMarkdown(description))) : '', + campaignId ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.campaignSignaturesLabel || 'Signatures').toUpperCase() + ':'), span({ class: 'card-value' }, `${signatures}${goal > 0 ? ` / ${goal}` : ''}`)) : '', + campaignId ? div({ class: 'card-field' }, a({ href: `/campaigns/${encodeURIComponent(campaignId)}`, class: 'card-value user-link' }, title || campaignId)) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), - createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '', + createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, moment(createdAt).format("YYYY/MM/DD HH:mm"))) : '', voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '', status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : '' ) @@ -1454,9 +1786,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '', - reasons ? p({ style: 'margin:.4rem 0' }, reasons) : '', + reasons ? p({ class: 'card-section-text' }, reasons) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), - createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '', + createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, moment(createdAt).format("YYYY/MM/DD HH:mm"))) : '', voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '', status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : '' ) @@ -1475,10 +1807,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, question ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawQuestion || 'Question') + ':'), span({ class: 'card-value' }, question)) : '', - description ? p({ style: 'margin:.4rem 0' }, description) : '', + description ? p({ class: 'card-section-text' }, description) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), proposer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawProposer || 'Proposer') + ':'), span({ class: 'card-value' }, userLink(proposer))) : '', - enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, new Date(enactedAt).toLocaleString())) : '', + enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, moment(enactedAt).format("YYYY/MM/DD HH:mm"))) : '', (total || yes) ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawVotes || 'Votes') + ':'), span({ class: 'card-value' }, `${yes}/${total}`)) : '' ) ); @@ -1492,9 +1824,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsCaseTitle.toUpperCase() + ':'), span({ class: 'card-value' }, title)) : '', status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus.toUpperCase() + ':'), span({ class: 'card-value' }, status)) : '', method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsMethod.toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['courtsMethod' + String(method).toUpperCase()] || method).toUpperCase())) : '', - answerBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThAnswerBy + ':'), span({ class: 'card-value' }, new Date(answerBy).toLocaleString())) : '', - evidenceBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ':'), span({ class: 'card-value' }, new Date(evidenceBy).toLocaleString())) : '', - decisionBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThDecisionBy + ':'), span({ class: 'card-value' }, new Date(decisionBy).toLocaleString())) : '', + answerBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThAnswerBy + ':'), span({ class: 'card-value' }, moment(answerBy).format("YYYY/MM/DD HH:mm"))) : '', + evidenceBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ':'), span({ class: 'card-value' }, moment(evidenceBy).format("YYYY/MM/DD HH:mm"))) : '', + decisionBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThDecisionBy + ':'), span({ class: 'card-value' }, moment(decisionBy).format("YYYY/MM/DD HH:mm"))) : '', accuser ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsAccuser + ':'), span({ class: 'card-value' }, userLink(accuser))) : '', typeof needed !== 'undefined' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesNeeded + ':'), span({ class: 'card-value' }, String(needed))) : '', (typeof yes !== 'undefined' || typeof total !== 'undefined') ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ':'), span({ class: 'card-value' }, `${Number(yes || 0)}/${Number(total || 0)}`)) : '', @@ -1537,7 +1869,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { return null; } - const detailHref = (type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet') + const detailHref = (type !== 'bankWallet') ? (isParliamentTarget ? `/parliament?filter=${encodeURIComponent(parliamentFilter)}` : isCourtsTarget @@ -1550,57 +1882,97 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-header activity-card-header' }, div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(type || '').toUpperCase()) + span({ class: 'pm-exposition-text' }, String(headerText || type || '').replace(/^\[|\]$/g, '')) ) ), - renderContentActions(msgId, detailHref) + renderContentActions(msgId, detailHref, { + author: action.author, + contact: type === 'about', + reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '', + spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined, + ...(type === 'torrent' || type === 'file' ? { spreadTitle: i18n.seedAction } : {}), + ...(TORRENTABLE_TYPES.has(type) && content && typeof content.url === 'string' && content.url.startsWith('&') + ? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } } + : {}), + ...favOptsFor(type, msgId, extras), + deleteAction: isOwn ? activityDeleteAction(type, action, content, msgId, userId) : undefined + }) ), ...cardBody, (() => { - if (!OPINION_TYPES.has(type)) return null; - const routeFn = OPINION_ROUTES[type]; - if (!routeFn) return null; + const footerAuthorId = action.author || (content && content.proposer) || ''; + const footerNode = p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${date}`), + userLink(footerAuthorId, (action.authorNames && action.authorNames[footerAuthorId]) || getProfile(footerAuthorId).name) + ); + const commentTotal = Number(action.commentCount) || 0; + const commentsHref = detailHref + ? `${detailHref}${detailHref.includes('?') ? '&' : '?'}comments=open${commentTotal ? '#comments-latest' : ''}` + : ''; + const commentsNode = detailHref && type !== 'about' + ? a({ + href: commentsHref, + class: commentTotal ? 'comments-summary-link engage-on' : 'comments-summary-link', + title: i18n.commentsGoToLast || i18n.voteNewCommentLabel || 'Comments' + }, + span({ class: 'comments-summary-icon' }, '✑'), + span({ class: 'comments-summary-count' }, `(${commentTotal})`) + ) + : null; + const routeFn = OPINION_TYPES.has(type) ? OPINION_ROUTES[type] : null; + if (!routeFn) return commentsNode ? [renderCardMetaRow(commentsNode, footerNode)] : footerNode; const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {}; const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); - return details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${opsTotal})`), + const votingNode = details({ class: 'opinions-voting-collapse' }, + summary({ class: 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${opsTotal})`)), div({ class: 'voting-buttons' }, opinionCategories.map(cat => form({ method: 'POST', action: `${routeFn(action.id)}/${cat}` }, - button({ class: 'vote-btn' }, `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat} [${ops[cat] || 0}]`) + button({ class: 'vote-btn' }, `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${ops[cat] || 0}]`) ) ) ) ); - })(), - (() => { - const SPREADABLE = new Set([ - 'post','audio','video','image','document','torrent','bookmark', - 'event','calendar','task','votes','vote','market','shop','shopProduct', - 'project','transfer','job','report', - 'chat','chatMessage','pad','padEntry','forum','map' - ]); - if (!SPREADABLE.has(type)) return null; - const btn = renderSpreadButton(action.id, spreadMap.get(action.id)); - return btn ? div({ class: 'card-spread-left' }, btn) : null; - })(), - (() => { - const footerAuthorId = action.author || (content && content.proposer) || ''; - return p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${date} ${i18n.performed} `), - userLink(footerAuthorId, (action.authorNames && action.authorNames[footerAuthorId]) || getProfile(footerAuthorId).name) - ); + return [renderVotesSummary(ops), renderCardMetaRow(votingNode, commentsNode, footerNode)]; })() ); }); const filteredCards = cards.filter(Boolean); if (!filteredCards.length) { - return emptyState({ icon: "📭", title: i18n.noActions, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta }); + return div({ class: "no-actions" }, p(i18n.noActions)); } return filteredCards; } +const FAV_KIND_BY_TYPE = CONTENT_FAV_KIND; + +const activityDeleteAction = (type, action, content, msgId, userId) => { + const c = content || {}; + if (c.author && String(c.author) !== String(userId)) return undefined; + if (type === 'transfer') { + const required = c.from === c.to ? 1 : 2; + const confirmed = Array.isArray(c.confirmedBy) ? c.confirmedBy.length : 0; + const dl = c.deadline ? moment(c.deadline) : null; + const expired = dl && dl.isValid() ? dl.isBefore(moment()) : false; + if (String(c.status || '').toUpperCase() !== 'UNCONFIRMED' || expired || confirmed >= required) return undefined; + } + return contentDeleteAction(type, type === 'forum' ? safeMsgId(c.key || action.tipId || action.id) : msgId); +}; + +const favOptsFor = (type, id, extras = {}) => { + const favKind = FAV_KIND_BY_TYPE[type]; + if (!favKind) return {}; + const index = extras.favIndex instanceof Map ? extras.favIndex : null; + return { + favKind, + isFavorite: index ? index.get(String(id)) === favKind : false, + returnTo: extras.returnTo || '/activity' + }; +}; + function getViewDetailsAction(type, action) { const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action)); switch (type) { @@ -1622,11 +1994,18 @@ function getViewDetailsAction(type, action) { const link = normalizeSpreadLink(action.content?.spreadTargetId || action.content?.vote?.link || ''); return link ? `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}` : `/activity`; } - case 'post': return `/thread/${id}#${id}`; + case 'post': { + const root = action.content && action.content.root; + return root + ? `/blogs/${encodeURIComponent(root)}#${id}` + : `/blogs/${id}`; + } + case 'feed': return `/feed/${id}`; case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`; case 'votes': return `/votes/${id}`; case 'transfer': return `/transfers/${id}`; case 'pixelia': return `/pixelia`; + case 'poll': return `/polls/${id}`; case 'larpHousePost': return action.content?.house ? `/larp/${encodeURIComponent(action.content.house)}` : `/activity`; case 'tribe': return `/tribe/${id}`; case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`; @@ -1637,39 +2016,61 @@ function getViewDetailsAction(type, action) { case 'audio': return `/audios/${id}`; case 'video': return `/videos/${id}`; case 'torrent': return `/torrents/${id}`; - case 'forum': return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`; + case 'file': return `/files/${id}`; + case 'forum': { + const root = action.content?.root; + const rootId = typeof root === 'string' ? root : (root && (root.key || root.id)) || ''; + if (rootId) return `/forum/${encodeURIComponent(action.content?.rootKey || rootId)}#msg-${String(action.tipId || action.id || '').replace(/[^a-zA-Z0-9]/g, '')}`; + return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`; + } case 'document': return `/documents/${id}`; case 'bookmark': return `/bookmarks/${id}`; case 'event': return `/events/${id}`; case 'task': return `/tasks/${id}`; case 'taskAssignment': return `/tasks/${encodeURIComponent(action.content?.taskId || action.tipId || action.id)}`; case 'about': return `/author/${encodeURIComponent(action.author)}`; - case 'pub': return `/invites`; case 'market': return `/market/${id}`; case 'shop': return `/shops/${id}`; case 'shopProduct': return `/shops/product/${id}`; case 'chat': return `/chats/${id}`; case 'pad': return `/pads/${id}`; + case 'room': return `/rooms/${id}`; + case 'wikiPage': return `/wiki/${id}`; + case 'emergency': return `/emergencies/${id}`; + case 'emergencyUpdate': return action.content && action.content.target ? `/emergencies/${encodeURIComponent(action.content.target)}` : '/emergencies'; + case 'campaignUpdate': return action.content && action.content.target ? `/campaigns/${encodeURIComponent(action.content.target)}` : '/campaigns'; + case 'mailingList': return `/mailing/${id}`; + case 'logisticsRoute': return `/logistics/${id}`; + case 'podcast': return `/podcasts/${id}`; + case 'podcastEpisode': return `/podcasts/episode/${id}`; + case 'campaign': return `/campaigns/${id}`; case 'calendar': return `/calendars/${id}`; + case 'housing': return `/housing/${id}`; case 'job': return `/jobs/${id}`; case 'project': return `/projects/${id}`; + case 'schoolCourse': return `/school/course/${id}`; + case 'industry': return `/industry/${id}`; + case 'industryBuild': return `/industry/build/${id}`; + case 'industryBlueprint': return `/industry/blueprint/${id}`; + case 'industryAllocation': return action.content?.target ? `/industry/build/${encodeURIComponent(action.content.target)}` : '/industry?filter=BUILDS'; case 'report': return `/reports/${id}`; case 'bankWallet': return `/wallet`; - case 'bankClaim': return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`; - case 'ubiClaim': return action.content?.transferId ? `/transfers/${encodeURIComponent(action.content.transferId)}` : `/transfers?filter=ubi`; default: return `/activity`; } } +exports.getViewDetailsAction = getViewDetailsAction; + exports.activityView = (actions, filter, userId, q = '', extras = {}) => { const spreadMap = (extras && extras.spreadMap) || new Map(); const title = filter === 'mine' ? i18n.yourActivity : i18n.globalActivity; const desc = i18n.activityDesc; const activityTypes = [ - { type: 'recent', label: i18n.typeRecent }, { type: 'all', label: i18n.allButton }, + { type: 'recent', label: i18n.typeRecent }, { type: 'mine', label: i18n.mineButton }, + { type: 'top', label: i18n.typeTop }, { type: 'inhabitants', label: i18n.typeInhabitants }, { type: 'tribe', label: i18n.typeTribe }, { type: 'larp', label: i18n.typeLarp }, @@ -1684,39 +2085,56 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { { type: 'feed', label: i18n.typeFeed }, { type: 'chat', label: i18n.typeChat }, { type: 'pad', label: i18n.typePad }, + { type: 'room', label: i18n.typeRoom }, + { type: 'wiki', label: i18n.typeWiki }, + { type: 'emergency', label: i18n.typeEmergency }, + { type: 'mailing', label: i18n.typeMailingList }, + { type: 'logistics', label: i18n.typeLogisticsRoute }, + { type: 'podcast', label: i18n.typePodcast }, + { type: 'campaign', label: i18n.typeCampaign }, { type: 'forum', label: i18n.typeForum }, { type: 'map', label: i18n.typeMap }, - { type: 'banking', label: i18n.typeBanking }, { type: 'market', label: i18n.typeMarket }, + { type: 'schoolCourse', label: i18n.typeSchool }, { type: 'project', label: i18n.typeProject }, + { type: 'industry', label: i18n.typeIndustry }, + { type: 'housing', label: i18n.typeHousing }, { type: 'job', label: i18n.typeJob }, { type: 'shop', label: i18n.typeShop }, - { type: 'transfer', label: i18n.typeTransfer }, - { type: 'curriculum',label: i18n.typeCurriculum }, { type: 'audio', label: i18n.typeAudio }, { type: 'bookmark', label: i18n.typeBookmark }, { type: 'document', label: i18n.typeDocument }, { type: 'image', label: i18n.typeImage }, { type: 'torrent', label: i18n.typeTorrent }, + { type: 'file', label: i18n.typeFile }, { type: 'video', label: i18n.typeVideo } ]; - const FILTER_META = new Set(['recent', 'all', 'mine']); + const FILTER_META = new Set(['all', 'mine', 'recent']); const GROUP_SUBTYPES = { parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'], courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'], - banking: ['bankWallet', 'bankClaim', 'ubiClaim'], task: ['task', 'taskAssignment'], + votes: ['votes', 'poll'], shop: ['shop', 'shopProduct'], larp: ['larpHousePost'], - inhabitants:['about'] + inhabitants:['about'], + chat: ['chat', 'chatThread'], + wiki: ['wikiPage'], + mailing: ['mailingList'], + emergency: ['emergency', 'emergencyUpdate'], + logistics: ['logisticsRoute'], + podcast: ['podcast', 'podcastEpisode'], + campaign: ['campaign', 'campaignUpdate'], + industry: ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'] }; - const ALLOWED_TYPES = new Set(); + const ALLOWED_TYPES = new Set(['tribeOpened']); for (const { type } of activityTypes) { if (FILTER_META.has(type)) continue; if (GROUP_SUBTYPES[type]) GROUP_SUBTYPES[type].forEach(t => ALLOWED_TYPES.add(t)); else ALLOWED_TYPES.add(type); } + const rawActions = actions; actions = actions.filter(action => ALLOWED_TYPES.has(action.type)); let filteredActions; @@ -1726,9 +2144,9 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { const now = Date.now(); filteredActions = actions.filter(action => action.type !== 'tombstone' && action.ts && now - action.ts < 24 * 60 * 60 * 1000); } else if (filter === 'banking') { - filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'bankWallet' || action.type === 'bankClaim' || action.type === 'ubiClaim')); + filteredActions = []; } else if (filter === 'tribe') { - filteredActions = actions.filter(action => action.type === 'tribe'); + filteredActions = actions.filter(action => action.type === 'tribe' || action.type === 'tribeOpened'); } else if (filter === 'larp') { filteredActions = actions.filter(action => action.type === 'larpHousePost'); } else if (filter === 'inhabitants') { @@ -1741,11 +2159,37 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { return t === 'courtscase' || t === 'courtsnomination' || t === 'courtsnominationvote'; }); } else if (filter === 'task') { - filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'task' || action.type === 'taskAssignment')); + filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'task' || action.type === 'taskAssignment' || openedIn(action, 'task'))); } else if (filter === 'torrent') { - filteredActions = actions.filter(action => action.type === 'torrent'); + filteredActions = actions.filter(action => action.type === 'torrent' || openedIn(action, 'torrent')); + } else if (filter === 'file') { + filteredActions = actions.filter(action => action.type === 'file'); + } else if (filter === 'chat') { + filteredActions = actions.filter(action => (action.type === 'chat' || action.type === 'chatThread') && action.type !== 'tombstone'); + } else if (filter === 'wiki') { + filteredActions = actions.filter(action => action.type === 'wikiPage'); + } else if (filter === 'emergency') { + filteredActions = actions.filter(action => action.type === 'emergency' || action.type === 'emergencyUpdate'); + } else if (filter === 'mailing') { + filteredActions = actions.filter(action => action.type === 'mailingList'); + } else if (filter === 'room') { + filteredActions = actions.filter(action => action.type === 'room'); + } else if (filter === 'logistics') { + filteredActions = actions.filter(action => action.type === 'logisticsRoute'); + } else if (filter === 'podcast') { + filteredActions = actions.filter(action => action.type === 'podcast' || action.type === 'podcastEpisode'); + } else if (filter === 'campaign') { + filteredActions = actions.filter(action => action.type === 'campaign' || action.type === 'campaignUpdate'); + } else if (filter === 'industry') { + filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone'); } else { - filteredActions = actions.filter(action => (action.type === filter || filter === 'all' || (filter === 'shop' && action.type === 'shopProduct')) && action.type !== 'tombstone'); + filteredActions = actions.filter(action => + (action.type === filter + || filter === 'all' + || openedIn(action, filter) + || (filter === 'shop' && action.type === 'shopProduct') + || (filter === 'votes' && action.type === 'poll')) + && action.type !== 'tombstone'); } const larpEarliest = new Map(); @@ -1809,75 +2253,88 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { } const MODULE_SUB_FILTERS = { - audio: { url: '/audios', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - video: { url: '/videos', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - image: { url: '/images', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - document:{ url: '/documents', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - bookmark:{ url: '/bookmarks', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - torrent: { url: '/torrents', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - map: { url: '/maps', filters: ['all', 'mine', 'recent'] }, - forum: { url: '/forum', filters: ['all', 'mine', 'recent', 'top'] }, - event: { url: '/events', filters: ['all', 'mine', 'recent', 'top'] }, + audio: { url: '/audios', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + video: { url: '/videos', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + image: { url: '/images', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + document:{ url: '/documents', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + bookmark:{ url: '/bookmarks', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + torrent: { url: '/torrents', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + file: { url: '/files', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + map: { url: '/maps', filters: ['all', 'recent', 'mine'] }, + forum: { url: '/forum', filters: ['all', 'recent', 'mine', 'top'] }, + event: { url: '/events', filters: ['all', 'recent', 'mine', 'top'] }, task: { url: '/tasks', filters: ['all', 'mine', 'assigned', 'open', 'closed'] }, - votes: { url: '/votes', filters: ['all', 'mine', 'recent', 'top'] }, + votes: { url: '/votes', filters: ['all', 'recent', 'mine', 'top'] }, transfer:{ url: '/transfers', filters: ['all', 'mine', 'pending', 'unconfirmed', 'closed'] }, market: { url: '/market', filters: ['all', 'mine', 'exchange', 'auctions', 'for sale', 'sold'] }, - shop: { url: '/shops', filters: ['all', 'mine', 'recent'] }, + shop: { url: '/shops', filters: ['all', 'recent', 'mine'] }, job: { url: '/jobs', filters: ['ALL', 'MINE', 'REMOTE', 'PRESENCIAL', 'OPEN', 'CLOSED'] }, project: { url: '/projects', filters: ['all', 'mine', 'active', 'completed'] }, + industry:{ url: '/industry', filters: ['ALL', 'MINE', 'ACTIVE', 'PAUSED', 'DISSOLVED', 'BLUEPRINTS', 'BUILDS', 'MEMBER', 'RULES'] }, chat: { url: '/chats', filters: ['all', 'mine'] }, pad: { url: '/pads', filters: ['all', 'mine'] }, calendar:{ url: '/calendars', filters: ['all', 'mine'] }, - report: { url: '/reports', filters: ['all', 'mine', 'recent'] }, + report: { url: '/reports', filters: ['all', 'recent', 'mine'] }, curriculum:{ url: '/cv', filters: ['view', 'edit'] } }; const sub = MODULE_SUB_FILTERS[filter]; + const emptyAct = actions.length === 0 && !qs; let html = template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header activity-header-line' }, h2(i18n.activityList), p(desc) ), - renderHiveNav(), - (() => { - const labelOf = {}; - activityTypes.forEach(t => { labelOf[t.type] = t.label; }); - const mkBtn = (type) => form({ method: 'GET', action: '/activity' }, - input({ type: 'hidden', name: 'filter', value: type }), - button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, labelOf[type] || type) - ); - const QUICK = ['recent', 'all', 'mine']; - const isTypeFilter = filter && !QUICK.includes(filter) && labelOf[filter]; - if (isTypeFilter) { - return div({ class: 'activity-filter-collapsed' }, - a({ class: 'activity-back filter-btn', href: '/activity' }, - span({ class: 'activity-back-arrow' }, '‹'), - span(i18n.activityChangeFilter || 'Change filter') - ), - span({ class: 'activity-active-filter' }, labelOf[filter] || filter) + emptyAct ? null : div({ class: 'activity-filter-chips' }, + (() => { + const ORDER = [ + 'all', 'recent', 'mine', 'top', + 'inhabitants', 'tribe', 'larp', 'schoolCourse', 'parliament', 'courts', 'emergency', + 'votes', 'event', 'calendar', 'task', 'report', 'campaign', + 'banking', 'market', 'housing', 'project', 'industry', 'job', 'shop', 'logistics', + 'post', 'feed', 'chat', 'room', 'pad', 'wiki', 'mailing', 'forum', 'map', + 'audio', 'bookmark', 'document', 'file', 'image', 'torrent', 'video', 'podcast' + ]; + const byType = new Map(activityTypes.map(t => [t.type, t])); + const placed = new Set(ORDER); + const presentActionTypes = new Set(actions.map(a2 => a2.type)); + const metaKeep = new Set(['all', 'mine', 'recent', 'top']); + const hasContentFor = (type) => + metaKeep.has(type) || (GROUP_SUBTYPES[type] || [type]).some(t2 => presentActionTypes.has(t2)); + const ordered = ORDER.map(type => byType.get(type)).filter(Boolean) + .concat(activityTypes.filter(t => !placed.has(t.type))) + .filter(({ type }) => hasContentFor(type)); + return ordered.map(({ type, label }) => + a({ + href: `/activity?filter=${encodeURIComponent(type)}`, + class: filter === type ? 'activity-chip active' : 'activity-chip' + }, label) ); - } - return div({ class: 'activity-filter-acc' }, - div({ class: 'activity-quick-modes' }, QUICK.map(mkBtn)) - ); - })(), - sub - ? div({ class: 'activity-sub-filter' }, - sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase())) + })() + ), + emptyAct ? null : div({ class: 'activity-filter-chips activity-toolbar-row' }, + renderModuleStats(filteredActions.length, [], null, { showInhabitants: true, showTribes: true }), + sub ? span({ class: 'activity-subchip-label' }, '\u21b3') : '', + sub ? sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'activity-chip' }, String(f).toUpperCase())) : '', + form({ method: 'GET', action: '/activity', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: qs, placeholder: i18n.activitySearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) ) - : null, - section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap)) + ) + ), + section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, rawActions, spreadMap, { ...extras, paged: true })) ) ); const hasDocument = actions.some(a => a && a.type === 'document'); if (hasDocument) { html += ` - - + `; } return html; diff --git a/src/views/agenda_view.js b/src/views/agenda_view.js index c13b8e2..e7e5512 100644 --- a/src/views/agenda_view.js +++ b/src/views/agenda_view.js @@ -1,19 +1,11 @@ -const { div, h2, p, section, button, form, img, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions } = require('./main_views'); +const { div, h2, p, section, button, form, img, input, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe"); +const { safeExternalHref } = require("../backend/renderStyledText"); +const { template, i18n, userLink, renderContentActions, renderModuleStats, renderStateChip, CONTENT_FAV_KIND, CONTENT_SPREADABLE, contentDeleteAction, paged } = require('./main_views'); const moment = require('../server/node_modules/moment'); const { config } = require('../server/SSB_server.js'); const userId = config.keys.id; -const renderCardField = (labelText, value) => - div({ class: 'card-field' }, - span({ class: 'card-label' }, labelText), - span( - { class: 'card-value' }, - ...(Array.isArray(value) ? value : [value ?? '']) - ) - ); - function getViewDetailsAction(item) { switch (item.type) { case 'transfer': return `/transfers/${encodeURIComponent(item.id)}`; @@ -24,129 +16,116 @@ function getViewDetailsAction(item) { case 'report': return `/reports/${encodeURIComponent(item.id)}`; case 'job': return `/jobs/${encodeURIComponent(item.id)}`; case 'project': return `/projects/${encodeURIComponent(item.id)}`; + case 'industry': return `/industry/build/${encodeURIComponent(item.id)}`; + case 'housing': return `/housing/${encodeURIComponent(item.id)}`; + case 'schoolCourse': return `/school/course/${encodeURIComponent(item.id)}`; + case 'campaign': return `/campaigns/${encodeURIComponent(item.id)}`; + case 'logisticsRoute': return `/logistics/${encodeURIComponent(item.id)}`; case 'calendar': return `/calendars/${encodeURIComponent(item.id)}`; + case 'calendarDate': return `/calendars/${encodeURIComponent(item.calendarId)}`; default: return `/messages/${encodeURIComponent(item.id)}`; } } -const renderAgendaItem = (item, userId, filter) => { - const fmt = d => moment(d).format('YYYY/MM/DD HH:mm:ss'); +const agendaDeleteAction = (item) => { + if (item.type === 'transfer') { + const required = item.from === item.to ? 1 : 2; + const confirmed = Array.isArray(item.confirmedBy) ? item.confirmedBy.length : 0; + const dl = item.deadline ? moment(item.deadline) : null; + const expired = dl && dl.isValid() ? dl.isBefore(moment()) : false; + return String(item.status || '').toUpperCase() === 'UNCONFIRMED' && !expired && confirmed < required ? contentDeleteAction('transfer', item.id) : undefined; + } + return contentDeleteAction(item.type, item.id); +}; + +const chip = (text, kind = "whole", icon = null) => (text === null || text === undefined || text === "") ? null : renderStateChip(kind, icon, String(text).toUpperCase()); +const timeChip = (value, fmt = "YYYY/MM/DD HH:mm") => value ? p({ class: "time-chip" }, moment(value).format(fmt)) : null; +const metaLine = (text) => text && String(text).trim() ? p({ class: "job-meta-line" }, String(text)) : null; +const priceChip = (text) => text ? div({ class: "price-chip" }, text) : null; +const countLine = (labelText, value) => div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${labelText}: ${value}`)); +const statusKind = (status) => String(status || "").toUpperCase() === "CLOSED" ? "closed" : "mutuals"; + +const renderAgendaItem = (item, userId, filter, extras = {}) => { const author = item.seller || item.organizer || item.from || item.author || ''; - - const commonFields = [ - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${item.createdAt ? moment(item.createdAt).format('YYYY/MM/DD HH:mm:ss') : ''} ${i18n.performed} `), - author ? userLink(author) : '' - ) - ]; - - let details = []; + const chips = []; + const body = []; let actionButton = null; if (filter === 'discarded') { - actionButton = form({ method: 'POST', action: `/agenda/restore/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'restore-btn' }, i18n.agendaRestoreButton) + actionButton = form({ method: 'POST', action: `/agenda/restore/${encodeURIComponent(item.id)}`, class: 'phone-action-form' }, + button({ type: 'submit', class: 'tribe-action-btn' }, String(i18n.agendaRestoreButton).toUpperCase()) ); } else { - actionButton = form({ method: 'POST', action: `/agenda/discard/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'discard-btn' }, i18n.agendaDiscardButton) + actionButton = form({ method: 'POST', action: `/agenda/discard/${encodeURIComponent(item.id)}`, class: 'phone-action-form' }, + button({ type: 'submit', class: 'tribe-action-btn' }, String(i18n.agendaDiscardButton).toUpperCase()) ); } + const extraActions = []; + if (filter === 'discarded') extraActions.push(form({ method: 'POST', action: `/agenda/remove/${encodeURIComponent(item.id)}`, class: 'phone-action-form' }, + button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, String(i18n.agendaRemoveButton).toUpperCase()) + )); if (item.type === 'market') { - details = [ - renderCardField(i18n.marketItemType + ":", String(item.item_type || '').toUpperCase()), - renderCardField(i18n.marketItemStatus + ":", item.status), - renderCardField(i18n.marketItemStock + ":", item.stock), - renderCardField(i18n.marketItemPrice + ":", `${item.price} ECO`), - renderCardField(i18n.marketItemIncludesShipping + ":", item.includesShipping ? i18n.agendaYes : i18n.agendaNo), - renderCardField(i18n.deadline + ":", item.deadline ? new Date(item.deadline).toLocaleString() : '') - ]; + chips.push(chip(item.item_type), chip(item.status, statusKind(item.status)), chip(`${i18n.marketItemStock}: ${item.stock}`), item.includesShipping ? chip(i18n.marketItemIncludesShipping, 'mutuals', '✓') : null); if (String(item.item_type || '').toLowerCase() === 'auction') { const bids = Array.isArray(item.auctions_poll) ? item.auctions_poll.map(bid => parseFloat(String(bid).split(':')[1])).filter(n => !isNaN(n)) : []; const maxBid = bids.length ? Math.max(...bids) : 0; - details.push(renderCardField(i18n.marketItemHighestBid + ":", `${maxBid} ECO`)); + chips.push(chip(`${i18n.marketItemHighestBid}: ${maxBid} ECO`, 'whole', '▲')); } - const seller = author ? p(userLink(author)) : ''; - details.push(br(), div({ class: 'members-list' }, i18n.marketItemSeller + ': ', seller)); + body.push(priceChip(`${item.price} ECO`), timeChip(item.deadline)); } if (item.type === 'tribe') { - details = [ - renderCardField(i18n.agendaAnonymousLabel + ":", item.isAnonymous ? i18n.agendaYes : i18n.agendaNo), - renderCardField(i18n.agendaInviteModeLabel + ":", (item.inviteMode ? String(item.inviteMode).toUpperCase() : i18n.noInviteMode)), - renderCardField(i18n.agendaLocationLabel + ":", item.location || i18n.noLocation), - renderCardField(i18n.agendaMembersCount + ":", Array.isArray(item.members) ? item.members.length : 0), - br() - ]; - const membersList = Array.isArray(item.members) ? item.members.map(member => p(userLink(member))) : []; - details.push(div({ class: 'members-list' }, `${i18n.agendaMembersLabel}:`, membersList)); + chips.push(item.isAnonymous ? chip(i18n.agendaAnonymousLabel, 'hidden') : null, chip(item.inviteMode ? item.inviteMode : i18n.noInviteMode)); + body.push(metaLine(item.location || i18n.noLocation), countLine(i18n.agendaMembersLabel, Array.isArray(item.members) ? item.members.length : 0)); } if (item.type === 'report') { - details = [ - renderCardField(i18n.agendareportStatus + ":", item.status || i18n.noStatus), - renderCardField(i18n.agendareportCategory + ":", item.category || i18n.noCategory), - renderCardField(i18n.agendareportSeverity + ":", (item.severity ? String(item.severity).toUpperCase() : i18n.noSeverity)) - ]; + chips.push(chip(item.status || i18n.noStatus, statusKind(item.status)), chip(item.category || i18n.noCategory), chip(item.severity || i18n.noSeverity, 'closed')); } if (item.type === 'event') { - details = [ - renderCardField(i18n.eventDateLabel + ":", item.date ? fmt(item.date) : ''), - renderCardField(i18n.eventLocationLabel + ":", item.location || ''), - renderCardField(i18n.eventPriceLabel + ":", `${item.price} ECO`), - renderCardField( - i18n.eventUrlLabel + ":", - item.url ? p(a({ href: item.url, target: "_blank" }, item.url)) : p(i18n.noUrl) - ) - ]; - actionButton = actionButton || form({ method: 'POST', action: `/events/attend/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'assign-btn' }, `${i18n.eventAttendButton}`) - ); + body.push(timeChip(item.date), metaLine(item.location), parseFloat(item.price || 0) > 0 ? priceChip(`${item.price} ECO`) : null, + item.url ? p({ class: 'job-meta-line' }, a({ href: safeExternalHref(item.url), target: "_blank", rel: "noopener noreferrer" }, item.url)) : null); } if (item.type === 'task') { - details = [ - renderCardField(i18n.taskStatus + ":", item.status), - renderCardField(i18n.taskPriorityLabel + ":", item.priority), - renderCardField(i18n.taskStartTimeLabel + ":", item.startTime ? new Date(item.startTime).toLocaleString() : ''), - renderCardField(i18n.taskEndTimeLabel + ":", item.endTime ? new Date(item.endTime).toLocaleString() : ''), - renderCardField(i18n.taskLocationLabel + ":", item.location || '') - ]; - const assigned = Array.isArray(item.assignees) && item.assignees.includes(userId); - actionButton = actionButton || form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'assign-btn' }, assigned ? i18n.taskUnassignButton : i18n.taskAssignButton) - ); + chips.push(chip(item.status, statusKind(item.status)), chip(item.priority, 'closed')); + body.push(timeChip(item.startTime), timeChip(item.endTime), metaLine(item.location)); } if (item.type === 'transfer') { - details = [ - renderCardField(i18n.agendaTransferConcept + ":", item.concept), - renderCardField(i18n.agendaTransferAmount + ":", item.amount), - renderCardField(i18n.agendaTransferDeadline + ":", item.deadline ? fmt(item.deadline) : ''), - br() - ]; - const membersList = item.to ? p(userLink(item.to)) : ''; - details.push(div({ class: 'members-list' }, i18n.to + ': ', membersList)); + body.push(metaLine(item.concept), priceChip(`${item.amount} ECO`), timeChip(item.deadline), + item.to && process.env.OASIS_MOBILE !== '1' ? p({ class: 'job-meta-line' }, `${i18n.to}: `, userLink(item.to)) : null); } - + if (item.type === 'project') { - details = [ - renderCardField(i18n.projectStatus + ":", item.status || i18n.noStatus), - renderCardField(i18n.projectProgress + ":", `${item.progress || 0}%`), - renderCardField(i18n.projectGoal + ":", `${item.goal} ECO`), - renderCardField(i18n.projectPledged + ":", `${item.pledged || 0} ECO`), - renderCardField(i18n.projectDeadline + ":", item.deadline ? new Date(item.deadline).toLocaleString() : i18n.noDeadline) - ]; + chips.push(chip(item.status || i18n.noStatus, statusKind(item.status)), chip(`${item.progress || 0}%`, 'mutuals')); + body.push(priceChip(`${item.pledged || 0} / ${item.goal} ECO`), item.deadline ? timeChip(item.deadline) : metaLine(i18n.noDeadline)); } if (item.type === 'calendar') { - details = [ - renderCardField((i18n.calendarStatusLabel || 'Status') + ':', item.isClosed ? (i18n.calendarStatusClosed || 'CLOSED') : (i18n.calendarStatusOpen || 'OPEN')), - renderCardField((i18n.calendarDeadlineLabel || 'Deadline') + ':', item.deadline ? moment(item.deadline).format('YYYY/MM/DD HH:mm') : ''), - renderCardField((i18n.calendarParticipantsLabel || 'Participants') + ':', Array.isArray(item.participants) ? item.participants.length : 0) - ]; + chips.push(chip(item.isClosed ? (i18n.calendarStatusClosed || 'CLOSED') : (i18n.calendarStatusOpen || 'OPEN'), item.isClosed ? 'closed' : 'mutuals')); + body.push(timeChip(item.deadline), countLine(i18n.calendarParticipantsLabel || 'Participants', Array.isArray(item.participants) ? item.participants.length : 0)); + } + + if (item.type === 'campaign') { + chips.push(chip(`${i18n.campaignSignaturesLabel}: ${item.signatureCount || 0} / ${item.goal || 0}`, 'mutuals', '✍')); + body.push(timeChip(item.deadline)); + } + + if (item.type === 'logisticsRoute') { + chips.push(chip(item.kind), chip(item.mode)); + body.push(metaLine(`${item.origin || ''} → ${item.destination || ''}`), timeChip(item.date)); + } + + if (item.type === 'housing') { + const isOwner = String(item.author) === String(userId); + const requestCount = Number(item.requestCount) || 0; + chips.push(chip(i18n["housingType" + String(item.housing_type || '').toUpperCase()] || item.housing_type), + chip(String(item.status || '').toUpperCase() === 'CLOSED' ? i18n.housingStatusCLOSED : i18n.housingStatusOPEN, statusKind(item.status)), + isOwner ? chip(`${i18n.housingRequests}: ${requestCount}`) : chip(i18n.housingRequestedBadge || 'REQUESTED', 'mutuals', '✓')); + body.push(metaLine(item.place), priceChip(String(item.housing_type) === 'couchsurfing' ? (i18n.housingFree || 'FREE') : `${item.price} ECO`), timeChip(item.availableFrom, 'YYYY/MM/DD')); } if (item.type === 'job') { @@ -154,106 +133,106 @@ const renderAgendaItem = (item, userId, filter) => { ? item.subscribers : (typeof item.subscribers === 'string' ? item.subscribers.split(',').map(s => s.trim()).filter(Boolean) - : (item.subscribers && typeof item.subscribers.length === 'number' - ? Array.from(item.subscribers) - : [])); + : (item.subscribers && typeof item.subscribers.length === 'number' ? Array.from(item.subscribers) : [])); + chips.push(chip(item.status, statusKind(item.status)), chip(item.job_type), chip(item.languages), chip(`${i18n.jobVacants}: ${item.vacants}`)); + body.push(metaLine(item.location), priceChip(`${item.salary} ECO`), countLine(i18n.jobSubscribers, subs.length)); + } - const subsInterleaved = subs - .map((id, i) => [i > 0 ? ', ' : '', userLink(id)]) - .flat(); - - details = [ - renderCardField(i18n.jobStatus + ":", item.status), - renderCardField(i18n.jobLocation + ":", (item.location || '').toUpperCase()), - renderCardField(i18n.jobType + ":", (item.job_type || '').toUpperCase()), - renderCardField(i18n.jobSalary + ":", `${item.salary} ECO`), - renderCardField(i18n.jobVacants + ":", item.vacants), - renderCardField(i18n.jobLanguages + ":", (item.languages || '').toUpperCase()), - br(), - div( - { class: 'members-list' }, - i18n.jobSubscribers + ': ',br(),br(), - ...(subs.length ? subsInterleaved : [i18n.noSubscribers.toUpperCase()]) - ), - ]; - - const subscribed = subs.includes(userId); - if (!subscribed && String(item.status).toUpperCase() !== 'CLOSED' && item.author !== userId) { - actionButton = form({ method: 'POST', action: `/jobs/subscribe/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'subscribe-btn' }, i18n.jobSubscribeButton) - ); - } + if (item.type === 'industry') { + const st = String(item.status || 'PROPOSED').toUpperCase(); + chips.push(chip(i18n['industryBuildStatus_' + st] || st, statusKind(st))); + body.push(metaLine(item.facilityName)); } const isOwn = author && String(author) === String(userId); + const favKind = CONTENT_FAV_KIND[item.type]; + const favIndex = extras.favIndex instanceof Map ? extras.favIndex : null; + const spreadMap = extras.spreadMap instanceof Map ? extras.spreadMap : null; + const href = getViewDetailsAction(item); + const title = item.title || item.name || item.concept || ''; return div({ class: 'trending-card agenda-card' + (isOwn ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, String(item.type || '').toUpperCase()) ), - renderContentActions(item.id, getViewDetailsAction(item)) + renderContentActions(item.id, href, { + author, + reportTitle: title, + spread: CONTENT_SPREADABLE.has(item.type) ? ((spreadMap && spreadMap.get(item.id)) || null) : undefined, + ...(favKind ? { favKind, isFavorite: item.isFavorite === true || (!!favIndex && [item.rootId, item.id].some(id => id && favIndex.get(String(id)) === favKind)) } : {}), + returnTo: `/agenda?filter=${encodeURIComponent(filter || 'all')}`, + deleteAction: isOwn ? agendaDeleteAction(item) : undefined + }) ), div({ class: 'card-section agenda-card-body' }, - actionButton, - br(), - h2(item.title || item.name || item.concept || ''), - ...details, - ...commonFields + div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href }, title))), + chips.filter(Boolean).length ? div({ class: 'card-chips-row' }, ...chips.filter(Boolean)) : null, + ...body.filter(Boolean), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${item.createdAt ? moment(item.createdAt).format('YYYY/MM/DD HH:mm') : ''}`), + author ? userLink(author) : '' + ), + div({ class: 'agenda-card-actions' }, actionButton, ...extraActions) ) ); }; -exports.agendaView = async (data, filter) => { +exports.agendaView = async (data, filter, q = '', extras = {}) => { const { items = [], counts: _c = {} } = data || {}; - const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, discarded: 0, ..._c }; + const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, housing: 0, discarded: 0, ..._c }; + const emptyAgenda = Number(counts.all || 0) === 0 && !String(q || '').trim(); return template( i18n.agendaTitle, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.agendaTitle), p(i18n.agendaDescription) ), - div({ class: 'filters' }, - form({ method: 'GET', action: '/agenda' }, - button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterAll} (${counts.all})`), - button({ type: 'submit', name: 'filter', value: 'today', class: filter === 'today' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterToday || 'TODAY'} (${counts.today || 0})`), - button({ type: 'submit', name: 'filter', value: 'upcoming', class: filter === 'upcoming' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterUpcoming || 'UPCOMING'} (${counts.upcoming || 0})`), - button({ type: 'submit', name: 'filter', value: 'overdue', class: filter === 'overdue' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterOverdue || 'OVERDUE'} (${counts.overdue || 0})`), - button({ type: 'submit', name: 'filter', value: 'open', class: filter === 'open' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterOpen} (${counts.open})`), - button({ type: 'submit', name: 'filter', value: 'closed', class: filter === 'closed' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterClosed} (${counts.closed})`), - button({ type: 'submit', name: 'filter', value: 'events', class: filter === 'events' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterEvents} (${counts.events})`), - button({ type: 'submit', name: 'filter', value: 'tasks', class: filter === 'tasks' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterTasks} (${counts.tasks})`), - button({ type: 'submit', name: 'filter', value: 'reports', class: filter === 'reports' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterReports} (${counts.reports})`), - button({ type: 'submit', name: 'filter', value: 'tribes', class: filter === 'tribes' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterTribes} (${counts.tribes})`), - button({ type: 'submit', name: 'filter', value: 'jobs', class: filter === 'jobs' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterJobs} (${counts.jobs})`), - button({ type: 'submit', name: 'filter', value: 'market', class: filter === 'market' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterMarket} (${counts.market})`), - button({ type: 'submit', name: 'filter', value: 'projects', class: filter === 'projects' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterProjects} (${counts.projects})`), - button({ type: 'submit', name: 'filter', value: 'calendars', class: filter === 'calendars' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterCalendars || 'CALENDARS'} (${counts.calendars})`), - button({ type: 'submit', name: 'filter', value: 'transfers', class: filter === 'transfers' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterTransfers} (${counts.transfers})`), - button({ type: 'submit', name: 'filter', value: 'discarded', class: filter === 'discarded' ? 'filter-btn active' : 'filter-btn' }, - `DISCARDED (${counts.discarded})`) + emptyAgenda ? null : div({ class: 'mode-buttons-row' }, + ...[ + ['all', i18n.agendaFilterAll], + ['today', i18n.agendaFilterToday || 'TODAY'], + ['upcoming', i18n.agendaFilterUpcoming || 'UPCOMING'], + ['overdue', i18n.agendaFilterOverdue || 'OVERDUE'], + ['open', i18n.agendaFilterOpen], + ['closed', i18n.agendaFilterClosed], + ['events', i18n.agendaFilterEvents], + ['tasks', i18n.agendaFilterTasks], + ['reports', i18n.agendaFilterReports], + ['tribes', i18n.agendaFilterTribes], + ['jobs', i18n.agendaFilterJobs], + ['market', i18n.agendaFilterMarket], + ['projects', i18n.agendaFilterProjects], + ['industry', i18n.agendaFilterIndustry || 'INDUSTRY'], + ['housing', i18n.agendaFilterHousing || 'HOUSING'], + ['school', i18n.agendaFilterSchool || 'SCHOOL'], + ['campaigns', i18n.agendaFilterCampaigns || 'CAMPAIGNS'], + ['logistics', i18n.agendaFilterLogistics || 'LOGISTICS'], + ['calendars', i18n.agendaFilterCalendars || 'CALENDARS'], + ['transfers', i18n.agendaFilterTransfers], + ['discarded', 'DISCARDED'] + ].filter(([value]) => value === 'all' || filter === value || Number(counts[value] || 0) > 0) + .map(([value, labelText]) => + form({ method: 'GET', action: '/agenda' }, + input({ type: 'hidden', name: 'filter', value }), + button({ type: 'submit', class: filter === value ? 'filter-btn active' : 'filter-btn' }, String(labelText).toUpperCase()))) + ), + emptyAgenda ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(items.length, [ + ['today', i18n.agendaFilterToday || 'TODAY'], ['upcoming', i18n.agendaFilterUpcoming || 'UPCOMING'], ['overdue', i18n.agendaFilterOverdue || 'OVERDUE'], + ['open', i18n.agendaFilterOpen], ['closed', i18n.agendaFilterClosed], ['discarded', 'DISCARDED'] + ].map(([value, label]) => ({ label: String(label).toUpperCase(), count: Number(counts[value] || 0) }))), + form({ method: 'GET', action: '/agenda', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: q, placeholder: i18n.agendaSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ), - div({ class: 'agenda-list' }, - items.length - ? items.map(item => renderAgendaItem(item, userId, filter)) - : p(i18n.agendaNoItems) - ) + items.length + ? div({ class: 'jobs-grid agenda-list' }, ...paged(items).map(item => renderAgendaItem(item, userId, filter, extras || {}))) + : div({ class: 'no-content-box' }, p({ class: 'no-content' }, i18n.agendaNoItems)) ) ); }; diff --git a/src/views/audio_view.js b/src/views/audio_view.js index 7b4f57c..83e9b6b 100644 --- a/src/views/audio_view.js +++ b/src/views/audio_view.js @@ -15,12 +15,14 @@ const { label, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl") -const { renderMapLocationVisitLabel } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText") +const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); const userId = config.keys.id; @@ -47,21 +49,6 @@ const renderTags = (tags) => { : null; }; -const renderAudioFavoriteToggle = (audioObj, returnTo = "") => - form( - { - method: "POST", - action: audioObj.isFavorite - ? `/audios/favorites/remove/${encodeURIComponent(audioObj.key)}` - : `/audios/favorites/add/${encodeURIComponent(audioObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - audioObj.isFavorite ? i18n.audioRemoveFavoriteButton : i18n.audioAddFavoriteButton - ) - ); - const renderTranscodeButton = (audioObj) => audioObj.isBcs ? form( @@ -74,7 +61,7 @@ const renderAudioPlayer = (audioObj, opts = {}) => audioObj?.url ? div( { class: "audio-container" }, - audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(audioObj.url)}`, preload: "metadata" }), + audioHyperaxe({ controls: true, src: blobUrlOf(audioObj.url), preload: "metadata" }), opts.skipTranscode ? null : renderTranscodeButton(audioObj) ) : p(i18n.audioNoFile); @@ -96,78 +83,16 @@ const renderAudioOwnerActions = (filter, audioObj, params = {}) => { ) ); } - items.push( - form( - { method: "POST", action: `/audios/delete/${encodeURIComponent(audioObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.audioDeleteButton) - ) - ); return items; }; const renderAudioCommentsSection = (audioId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/audios/${encodeURIComponent(audioId)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/audios/${encodeURIComponent(audioId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - const content = c?.value?.content || {}; - const rootId = content.fork || content.root || null; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) => { @@ -177,63 +102,34 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) ? audios.map((audioObj) => { const commentCount = typeof audioObj.commentCount === "number" ? audioObj.commentCount : 0; const title = safeText(audioObj.title); - const ownerActions = renderAudioOwnerActions(filter, audioObj, params); const isOwn = audioObj.author && String(audioObj.author) === String(userId); + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(audioObj) + : audioObj.tribeOrigin + ? renderContentActions(null, audioObj.tribeOrigin.href) + : renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads || null, author: audioObj.author, favKind: 'audios', torrentFrom: { blobId: audioObj.url, name: audioObj.title }, isFavorite: audioObj.isFavorite, reportTitle: audioObj.title, deleteAction: isOwn ? contentDeleteAction('audio', audioObj.key) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : audioObj.tribeOrigin ? audioObj.tribeOrigin.href : `/audios/${encodeURIComponent(audioObj.key)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(audioObj) + : audioObj.tribeOrigin ? null : renderEngagement(audioObj.key, + renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), + renderCommentsLink({ href: `/audios/${encodeURIComponent(audioObj.key)}`, count: commentCount }) + ); return div( { class: "trending-card audio-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`) + headerActions ), div( { class: "card-section audio-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderAudioFavoriteToggle(audioObj, returnTo), - audioObj.author && String(audioObj.author) !== String(userId) - ? form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: audioObj.author }), - button({ type: "submit", class: "filter-btn" }, i18n.audioMessageAuthorButton) - ) - : null - ), - ownerActions.length - ? div({ class: "bookmark-topbar-right" }, div({ class: "bookmark-actions" }, ...ownerActions)) - : null - ), - title ? h2(title) : null, + div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, audioObj.tribeOrigin ? renderTribeOriginChip(audioObj.tribeOrigin) : audioObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('audios', audioObj.title, audioObj.key)) : null, renderLicenseChip(audioObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(audioObj) : [])), audioObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(audioObj.lifetime, i18n)) : null, renderAudioPlayer(audioObj), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/audios/${encodeURIComponent(audioObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(audioObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(audioObj.key, (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads)), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(audioObj) : []), + engagement, renderMapLocationVisitLabel(audioObj.mapUrl), br(), (() => { @@ -243,12 +139,12 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(audioObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -263,6 +159,7 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo("all", params); return div( { class: "div-center audio-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/audios/update/${encodeURIComponent(audioId)}` : "/audios/create", @@ -272,16 +169,16 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), span(i18n.audioFileLabel), br(), - input({ type: "file", name: "audio", required: filter !== "edit" }), + input({ type: "file", name: "audio", accept: "audio/*", required: filter !== "edit" }), br(), br(), span(i18n.audioTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.audioTitlePlaceholder, value: audioToEdit?.title || "" }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.audioTitlePlaceholder, value: audioToEdit?.title || "" }), br(), span(i18n.audioDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.audioDescriptionPlaceholder, rows: "4" }, audioToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.audioDescriptionPlaceholder, rows: "4" }, audioToEdit?.description || ""), br(), span(i18n.mapLocationTitle || "Map Location"), br(), @@ -296,62 +193,61 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { value: safeArr(audioToEdit?.tags).join(", ") }), br(), + ...renderLicenseSelect(audioToEdit?.license, i18n), + br(), + renderClearnetSelector(filter === "edit" ? !!audioToEdit?.clearnet : false, i18n), br(), button({ type: "submit" }, filter === "edit" ? i18n.audioUpdateButton : i18n.audioCreateButton) ) ); }; +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => !x.tribeOrigin && String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.audioView = async (audios, filter = "all", audioId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.audioMineSectionTitle - : filter === "create" - ? i18n.audioCreateSectionTitle - : filter === "edit" - ? i18n.audioUpdateSectionTitle - : filter === "recent" - ? i18n.audioRecentSectionTitle - : filter === "top" - ? i18n.audioTopSectionTitle - : filter === "favorites" - ? i18n.audioFavoritesSectionTitle - : i18n.audioAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(audioId) }; + const title = i18n.audioTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(audios); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const audioToEdit = audioId ? list.find((a) => a.key === audioId) : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.audioDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetAudios); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterTop), - button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.audioFilterFavorites - ), + String(i18n.audioFilterFavorites).toUpperCase() + )] : []), + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterTop).toUpperCase())] : []), + ...(mediaChip("bcs") ? [button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS")] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.audioCreateButton) ) ) @@ -360,8 +256,9 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {}) filter === "create" || filter === "edit" ? renderAudioForm(filter, audioId, audioToEdit, { ...params, filter }) : section( - div( - { class: "audios-search" }, + emptyMod ? null : div( + { class: "audios-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/audios", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -376,29 +273,29 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {}) { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.audioSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.audioSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.audioSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.audioSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.audioSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.audioSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.audioSearchButton) ) ) ), - div({ class: "audios-list" }, renderAudioList(list, filter, { q, sort })) + div({ class: "audios-list" }, renderAudioList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); }; exports.singleAudioView = async (audioObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(audioObj.title); const isAuthor = String(audioObj.author) === String(userId); - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetAudios); + const isClearnet = !!audioObj.clearnet; const chips = [ renderLifespanChip(audioObj.lifetime, i18n), @@ -407,17 +304,9 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params const ownerActions = renderAudioOwnerActions(filter, audioObj, { q, sort }); const sideActions = []; - sideActions.push(renderAudioFavoriteToggle(audioObj, returnTo)); - if (audioObj.author && String(audioObj.author) !== String(userId)) { - sideActions.push(form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: audioObj.author }), - button({ type: "submit", class: "filter-btn" }, i18n.audioMessageAuthorButton) - )); - } if (audioObj.isBcs) { sideActions.push(form( - { method: "GET", action: `/melody/transcode/${encodeURIComponent(audioObj.key)}` }, + { method: "GET", action: `/melody/transcode/${encodeURIComponent(audioObj.key)}`, class: "audio-transcode-form" }, button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") )); } @@ -425,22 +314,36 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params const tagsNode = renderTags(audioObj.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`, { + author: audioObj.author, + favKind: 'audios', torrentFrom: { blobId: audioObj.url, name: audioObj.title }, + isFavorite: audioObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: audioObj.title, + deleteAction: isAuthor ? contentDeleteAction('audio', audioObj.key) : undefined + }) + ); + const audioSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('audios', audioObj.title, audioObj.key) : null), + isAuthor ? renderClearnetSwitch('audios', audioObj.rootId || audioObj.key, isClearnet) : null, + renderLicenseChip(audioObj.license) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(audioObj.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(audioObj.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(audioObj.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(audioObj.key, params.spreads)), - renderMapLocationVisitLabel(audioObj.mapUrl), + renderMapEmbed(params.mapData, audioObj.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const audioMain = div({ class: "tribe-main" }, + detailActions, renderAudioPlayer(audioObj), (() => { const createdTs = audioObj.createdAt ? new Date(audioObj.createdAt).getTime() : NaN; @@ -449,24 +352,26 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(audioObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), - renderAudioCommentsSection(audioObj.key, comments, returnTo) + renderEngagement(audioObj.key, + renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), + renderAudioCommentsSection(audioObj.key, comments, returnTo) + ) ); return template( i18n.audioTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.audioAllSectionTitle || i18n.audioTitle), p(i18n.audioDescription) ), @@ -474,17 +379,16 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params { class: "filters" }, form( { method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterTop), - button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.audioFilterFavorites - ), + String(i18n.audioFilterFavorites).toUpperCase() + )] : []), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterTop).toUpperCase()), + ...(mediaChip("bcs") ? [button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS")] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.audioCreateButton) ) ), @@ -499,12 +403,12 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload const title = i18n.audioTranscodeDetailTitle || "Transcode"; const composition = Array.isArray(audio.bcsComposition) ? audio.bcsComposition : []; const hasStego = decoded && stegoPayload && (stegoPayload.id || stegoPayload.ts || stegoPayload.msg); - const stegoDate = hasStego && Number.isFinite(stegoPayload.ts) ? moment(stegoPayload.ts).format("YYYY/MM/DD HH:mm:ss") : null; + const stegoDate = hasStego && Number.isFinite(stegoPayload.ts) ? moment(stegoPayload.ts).format("YYYY/MM/DD HH:mm") : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.audioTranscodeDetailDescription || "Decode the embedded payload and the original blockchain composition map.") ), @@ -520,15 +424,14 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload p({ class: "transcode-meta card-footer" }, userLink(audio.author), span({ class: "melody-meta-sep" }, " · "), - span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm:ss")), + span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm")), itemSize ? span({ class: "melody-meta-sep" }, " · ") : null, itemSize ? renderEcoTax(itemSize, audio.key) : null ), safeText(audio.description) ? p({ class: "melody-bcs-desc" }, audio.description) : null, renderTags(audio.tags), - br(), - form({ method: "POST", action: `/melody/transcode/${encodeURIComponent(audio.key)}`, class: "audio-transcode-run-form" }, - button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") + form({ method: "POST", action: `/melody/transcode/${encodeURIComponent(audio.key)}`, class: "audio-transcode-run-form audio-transcode-form" }, + button({ type: "submit", class: "create-button" }, i18n.audioTranscodeButton || "TRANSCODE") ), br(), decoded @@ -540,7 +443,6 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload span({ class: "card-value" }, stegoDate || (i18n.audioTranscodeStegoUnknown || "—")) ), div({ class: "transcode-stego-field" }, - span({ class: "card-label" }, (i18n.audioTranscodeStegoOasisId || "By") + ": "), stegoPayload.id ? userLink(stegoPayload.id) : span({ class: "card-value" }, i18n.audioTranscodeStegoUnknown || "—") ), div({ class: "transcode-stego-field transcode-stego-msg" }, diff --git a/src/views/backup_view.js b/src/views/backup_view.js new file mode 100644 index 0000000..f3e401c --- /dev/null +++ b/src/views/backup_view.js @@ -0,0 +1,197 @@ +const { div, h2, h3, p, section, button, form, a, span, br, hr, input, label, select, option, pre, img, ul, li, strong, progress } = require("../server/node_modules/hyperaxe"); +const { template, i18n } = require("./main_views"); +const moment = require("../server/node_modules/moment"); +const crypto = require("crypto"); + +const generateRandomPassword = (length = 32) => crypto.randomBytes(length).toString("hex").slice(0, length); +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); +const humanBytes = (n) => { + const b = Number(n) || 0; + if (b < 1024) return `${b} B`; + if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`; + if (b < 1024 * 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(1)} MB`; + return `${(b / (1024 * 1024 * 1024)).toFixed(2)} GB`; +}; + +const BACKUP_TYPES = ["RECOVERY", "KEYS", "INSTANT", "FULL", "RESTORE"]; +const typeLabel = (t) => String(i18n[`backupType${t.charAt(0) + t.slice(1).toLowerCase()}`] || t).toUpperCase(); +const normalizeType = (t) => (BACKUP_TYPES.includes(String(t || "").toUpperCase()) ? String(t).toUpperCase() : "RECOVERY"); + +const renderTypeFilters = (type) => + div({ class: "filters" }, + form({ method: "GET", action: "/backup", class: "ui-toolbar ui-toolbar--filters" }, + ...BACKUP_TYPES.map(t => button({ type: "submit", name: "type", value: t, class: type === t ? "filter-btn active" : "filter-btn" }, typeLabel(t))) + ) + ); + +const renderRandomPassword = () => p({ id: "randomPassword" }, h2({ class: "generated-password" }, generateRandomPassword())); + +const renderRecovery = (kit) => kit + ? div({ class: "backup-kit" }, + p({ class: "backup-kit-line" }, strong(`${i18n.backupKitId}: `), span({ class: "backup-kit-id" }, kit.id || "—")), + p({ class: "backup-kit-line" }, strong(`${i18n.backupKitDate}: `), fmt(kit.createdAt)), + pre({ class: "backup-kit-secret" }, kit.secret), + div({ class: "backup-kit-qr" }, img({ src: "/backup/recovery-kit/qr.png", alt: i18n.backupKitQrAlt, class: "backup-kit-qr-img" })), + ul({ class: "backup-kit-steps" }, + li(i18n.backupKitStep1), + li(i18n.backupKitStep2), + li(strong(i18n.backupKitStep3)) + ), + div({ class: "doc-export-actions doc-export-left" }, + form({ method: "GET", action: "/backup/recovery-kit/pdf" }, button({ type: "submit", class: "filter-btn" }, i18n.backupKitPdfButton)) + ) + ) + : p({ class: "backup-warning" }, i18n.backupKitPageDescription); + +const renderKeysExport = () => + div({ class: "backup-section" }, + renderRandomPassword(), + p(i18n.exportDescription), + form({ action: "/backup/keys/export", method: process.env.OASIS_MOBILE === '1' ? "GET" : "POST", id: "exportForm" }, + label(i18n.exportPasswordLabel), br(), + input({ type: "password", name: "password", id: "password", required: true, placeholder: i18n.exportPasswordPlaceholder, minlength: 32 }), br(), + p({ class: "file-info" }, i18n.fileInfo), + button({ type: "submit" }, i18n.backupKeysExportButton) + ) + ); + +const renderFullBackup = (options) => { + const opt = options || {}; + const sinceValue = opt.sinceTs ? moment(opt.sinceTs).format("YYYY-MM-DD") : ""; + return div({ class: "backup-section" }, + renderRandomPassword(), + form({ method: process.env.OASIS_MOBILE === '1' ? "GET" : "POST", action: "/backup/export" }, + label(i18n.backupScopeLabel), br(), + select({ name: "scope" }, + option({ value: "all", ...(opt.scope !== "mine" ? { selected: true } : {}) }, i18n.backupScopeAll), + option({ value: "mine", ...(opt.scope === "mine" ? { selected: true } : {}) }, i18n.backupScopeMine) + ), br(), br(), + label(i18n.backupSinceLabel), br(), + input({ type: "date", name: "since", value: sinceValue }), br(), br(), + label(i18n.exportPasswordLabel), br(), + input({ type: "password", name: "password", required: true, placeholder: i18n.exportPasswordPlaceholder, minlength: 32 }), br(), + p({ class: "file-info" }, i18n.backupFileInfo), + button({ type: "submit" }, i18n.backupExportButton) + ) + ); +}; + +const restoreSummary = (r) => + `${i18n.backupRestoredMessages}: ${r.messages} · ${i18n.backupRestoredSkipped}: ${r.skipped} · ${i18n.backupRestoredBlobs}: ${r.blobs}${r.files ? ` · ${i18n.backupRestoredState}: ${r.files}` : ""}${r.forked ? ` · ${i18n.backupRestoredForked}: ${r.forked}` : ""}${r.failed ? ` · ${i18n.backupRestoredFailed}: ${r.failed}` : ""}`; + +const renderRestoreStatus = (job) => { + if (!job) return null; + if (job.running) { + const elapsed = Math.max(0, Math.round((Date.now() - Date.parse(job.startedAt)) / 1000)); + const pct = Math.max(0, Math.min(100, Number((job.progress || {}).percent) || 0)); + return div({ class: "backup-restored" }, + p({ class: "backup-warning" }, `⏳ ${i18n.backupRestoreRunning}`), + div({ class: "indexing-progress-block" }, + progress({ value: String(pct), max: "100", class: "indexing-progress" }), + p({ class: "indexing-percent" }, strong(`${pct.toFixed(1)} %`)) + ), + p(`${restoreSummary(job.progress || {})} · ${i18n.backupRestoreElapsed}: ${elapsed}s`) + ); + } + if (job.error) return div({ class: "backup-restored" }, p({ class: "backup-warning" }, `✗ ${i18n.backupRestoreFailedLine} ${job.error}`)); + const r = job.result || {}; + const mine = (r.forks || []).some(f => f.mine); + return div({ class: "backup-restored" }, + p(`✓ ${i18n.backupRestoredLine} ${restoreSummary(r)}`), + mine ? p({ class: "backup-warning" }, `⚠ ${i18n.backupRestoreForkMine}`) : null + ); +}; + +const renderRestore = (job) => + div({ class: "backup-section" }, + renderRestoreStatus(job), + job && job.running ? null : form({ method: "POST", action: "/backup/import", enctype: "multipart/form-data" }, + input({ type: "file", name: "uploadedFile", required: true, accept: ".oasisbk" }), br(), br(), + p(i18n.backupRestorePassword), + input({ type: "password", name: "importPassword", required: true, placeholder: i18n.importPasswordPlaceholder, minlength: 32 }), br(), + button({ type: "submit" }, i18n.backupRestoreButton) + ), + br(), + p(i18n.importDescription), + form({ action: "/backup/keys/import", method: "POST", enctype: "multipart/form-data" }, + input({ type: "file", name: "uploadedFile", required: true }), br(), br(), + p(i18n.passwordImport), + input({ type: "password", name: "importPassword", required: true, placeholder: i18n.importPasswordPlaceholder, minlength: 32 }), br(), + button({ type: "submit" }, i18n.backupKeysImportButton) + ) + ); + +const renderInstantBackup = () => + div({ class: "backup-form" }, + p({ class: "backup-hint" }, i18n.backupInstantDescription, strong(i18n.backupInstantRisk)), + form({ action: "/export/create", method: "POST" }, + button({ type: "submit" }, i18n.exportDataButton) + ) + ); + +exports.backupView = async ({ type = "RECOVERY", options = null, restoreJob = null, kit = null } = {}) => { + const t = normalizeType(type); + const body = t === "RECOVERY" ? renderRecovery(kit) + : t === "KEYS" ? renderKeysExport() + : t === "FULL" ? renderFullBackup(options) + : t === "INSTANT" ? renderInstantBackup() + : renderRestore(restoreJob); + const html = template( + i18n.backupTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.backupTitle), p(i18n.backupDescription)), + renderTypeFilters(t) + ), + section( + div({ class: "div-center backup-wrap" }, body) + ) + ); + return restoreJob && restoreJob.running ? html.replace('', '') : html; +}; + +exports.renderRestoreStatus = renderRestoreStatus; +exports.recoveryKitView = async (kit) => exports.backupView({ type: "RECOVERY", kit }); + +exports.renderRebuildReport = (report) => { + if (!report) return null; + const idx = report.indexes || {}; + const line = (ok, text) => li({ class: ok ? "backup-check-ok" : "backup-check-bad" }, `${ok ? "✓" : "✗"} ${text}`); + return div({ class: "backup-verification" }, + p({ class: "backup-hint" }, `${fmt(report.checkedAt)} · ${report.tookMs} ms · ${i18n.verificationTotalMessages}: ${report.totalMessages}`), + ul( + report.ok + ? line(true, `${i18n.indexesRebuilt}`) + : line(false, `${i18n.indexesRebuildFailed}: ${report.error || ''}`), + li({ class: "backup-check-ok" }, `· ${i18n.indexesFiles}: ${idx.files || 0} (${humanBytes(idx.bytes || 0)})`) + ) + ); +}; + +exports.renderVerificationReport = (report) => { + if (!report) return null; + const feed = report.feed || {}; + const blobs = report.blobs || {}; + const line = (ok, text) => li({ class: ok ? "backup-check-ok" : "backup-check-bad" }, `${ok ? "✓" : "✗"} ${text}`); + return div({ class: "backup-verification" }, + p({ class: "backup-hint" }, `${fmt(report.checkedAt)} · ${report.tookMs} ms · ${i18n.verificationTotalMessages}: ${report.totalMessages}`), + ul( + line(feed.ok, `${i18n.verificationFeed}: ${feed.messages} ${i18n.verificationMessages}, ${i18n.verificationLastSequence} ${feed.lastSequence}`), + feed.gaps && feed.gaps.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationGaps}: ${feed.gaps.map(g => `${g.expected}→${g.found}`).join(", ")}`) : null, + feed.brokenLinks && feed.brokenLinks.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationBrokenLinks}: ${feed.brokenLinks.join(", ")}`) : null, + feed.badHashes && feed.badHashes.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationBadHashes}: ${feed.badHashes.join(", ")}`) : null, + feed.badSignatures && feed.badSignatures.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationBadSignatures}: ${feed.badSignatures.join(", ")}`) : null, + feed.duplicates && feed.duplicates.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationDuplicates}: ${feed.duplicates.join(", ")}`) : null, + line(!report.forkCount, `${i18n.verificationForks}: ${report.forkCount}`), + ...(report.forks || []).map(f => li({ class: "backup-check-bad backup-fork" }, `${f.mine ? "⚠ " : ""}${f.author} · ${f.points.map(pt => `#${pt.sequence} (${pt.keys.length})`).join(", ")}`)), + blobs.listed + ? line(!blobs.missingOwn, `${i18n.verificationBlobs}: ${blobs.present} ${i18n.verificationBlobsPresent}, ${blobs.referenced} ${i18n.verificationBlobsReferenced}, ${blobs.orphan} ${i18n.verificationBlobsOrphan} (${humanBytes(blobs.orphanBytes)})`) + : li(`· ${i18n.verificationBlobsUnavailable}`), + blobs.listed && blobs.missingOwn + ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationBlobsMissingOwn}: ${blobs.missingOwn}`) + : null, + blobs.listed && (blobs.missing - (blobs.missingOwn || 0)) > 0 + ? li({ class: "backup-check-warn" }, `⚠ ${i18n.verificationBlobsMissingOthers}: ${blobs.missing - (blobs.missingOwn || 0)}`) + : null + ) + ); +}; diff --git a/src/views/banking_views.js b/src/views/banking_views.js index 5928d3e..0db52a8 100644 --- a/src/views/banking_views.js +++ b/src/views/banking_views.js @@ -1,9 +1,11 @@ -const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, formatCarbon } = require("../views/main_views"); +const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label, ul, li } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { template, i18n, userLink, formatCarbon, renderStateChip, renderModuleStats, renderWalletChip, paged, renderIcon } = require("../views/main_views"); const moment = require("../server/node_modules/moment"); const FILTER_LABELS = { overview: i18n.bankOverview, + ubi: i18n.bankUbiTab, exchange: i18n.bankExchange, taxes: i18n.bankTaxes || "Taxes", mine: i18n.mine, @@ -63,79 +65,254 @@ const escAttr = (s) => String(s) .replace(/"/g, '"') .replace(/'/g, '''); -const buildEcoValueChartSvg = (history, labels) => { - const arr = Array.isArray(history) ? history.slice(-120) : []; - const W = 720, H = 320; - const padL = 56, padR = 16, padT = 16, padB = 70; +const CHART_LINE_CLASSES = ["value", "supply", "inflation"]; +const legendLayout = (labels, x0, maxX) => { + const rows = [[]]; + let x = x0; + for (const label of labels) { + const width = 18 + String(label).length * 6.6 + 24; + if (x + width > maxX && rows[rows.length - 1].length) { rows.push([]); x = x0; } + rows[rows.length - 1].push({ label, x }); + x += width; + } + return rows; +}; + +const axisDecimals = (max, fallback) => { + const m = Math.abs(Number(max) || 0); + if (m >= 10000) return 0; + if (m >= 100) return 2; + return fallback; +}; + +const buildSeriesChartSvg = ({ series = [], xLabels = [], empty = "", yDecimals = 4, yDomain = null }) => { + const valid = series.filter(s => Array.isArray(s.points) && s.points.length >= 2); + const W = 720; + const padR = 16, padT = 16; + const first = valid[0] ? valid[0].points.map(v => Number(v) || 0) : [0]; + const yMax = yDomain ? Number(yDomain.max) : Math.max(...first); + const yMin = yDomain ? Number(yDomain.min) : Math.min(...first); + const decimals = axisDecimals(Math.max(Math.abs(yMax), Math.abs(yMin)), yDecimals); + const widestLabel = Math.max(...[yMax, yMin].map(v => v.toFixed(decimals).length)); + const padL = Math.max(56, Math.round(widestLabel * 6.2) + 14); + const legendRows = legendLayout(valid.map(s => s.label || ""), padL, W - padR); + const padB = 54 + 16 * Math.max(1, legendRows.length); + const H = 250 + padB; const plotW = W - padL - padR; const plotH = H - padT - padB; - if (arr.length < 2) { + if (!valid.length) { return `` + `` - + `${escAttr(labels.empty || 'Not enough samples yet')}` + + `${escAttr(empty || 'Not enough samples yet')}` + ``; } - const values = arr.map(s => Number(s.ecoValue || 0)); - const supplies = arr.map(s => Number(s.currentSupply || 0)); - const inflations = arr.map(s => Number(s.inflationFactor || 0)); - const minV = Math.min(...values); - const maxV = Math.max(...values); - const rangeV = maxV - minV || 1; - const minS = Math.min(...supplies); - const maxS = Math.max(...supplies); - const rangeS = maxS - minS || 1; - const minI = Math.min(...inflations); - const maxI = Math.max(...inflations); - const rangeI = maxI - minI || 1; - const stepX = arr.length > 1 ? plotW / (arr.length - 1) : plotW; - const xy = (i, v, minR, rangeR) => { - const x = padL + i * stepX; - const y = padT + plotH - ((v - minR) / rangeR) * plotH; - return `${x.toFixed(2)},${y.toFixed(2)}`; - }; - const pointsValue = values.map((v, i) => xy(i, v, minV, rangeV)).join(' '); - const pointsSupply = supplies.map((v, i) => xy(i, v, minS, rangeS)).join(' '); - const pointsInfl = inflations.map((v, i) => xy(i, v, minI, rangeI)).join(' '); - const tsStart = moment(arr[0].ts).format('YYYY-MM-DD HH:mm'); - const tsEnd = moment(arr[arr.length - 1].ts).format('YYYY-MM-DD HH:mm'); - const tsMid = moment(arr[Math.floor(arr.length / 2)].ts).format('YYYY-MM-DD HH:mm'); + const lines = valid.map((s, idx) => { + const vals = s.points.map(v => Number(v) || 0); + const min = yDomain ? yMin : Math.min(...vals); + const max = yDomain ? yMax : Math.max(...vals); + const range = max - min || 1; + const stepX = plotW / (vals.length - 1); + const pts = vals.map((v, i) => `${(padL + i * stepX).toFixed(2)},${(padT + plotH - ((v - min) / range) * plotH).toFixed(2)}`).join(" "); + return { cls: CHART_LINE_CLASSES[idx % CHART_LINE_CLASSES.length], label: s.label || "", pts, min, max, range }; + }); const yTicks = 4; const grid = []; for (let i = 0; i <= yTicks; i++) { const y = padT + (plotH / yTicks) * i; grid.push(``); - const val = maxV - (rangeV / yTicks) * i; - grid.push(`${val.toFixed(4)}`); + const val = lines[0].max - (lines[0].range / yTicks) * i; + grid.push(`${val.toFixed(decimals)}`); } const xLabelY = padT + plotH + 16; - const xLabels = `${escAttr(tsStart)}` - + `${escAttr(tsMid)}` - + `${escAttr(tsEnd)}`; - const legendY = padT + plotH + 44; - const legendBaseX = padL; - const legend = `` - + `` - + `${escAttr(labels.value || 'Value')}` - + `` - + `${escAttr(labels.supply || 'Supply')}` - + `` - + `${escAttr(labels.inflation || 'Inflation')}` - + ``; + const [xs = "", xm = "", xe = ""] = xLabels; + const xLabelsSvg = `${escAttr(xs)}` + + `${escAttr(xm)}` + + `${escAttr(xe)}`; + const legendY0 = padT + plotH + 44; + const legend = `` + legendRows.map((row, r) => row.map((item, i) => { + const idx = legendRows.slice(0, r).reduce((acc, rr) => acc + rr.length, 0) + i; + const l = lines[idx]; + const y = legendY0 + r * 16; + return `` + + `${escAttr(l.label)}`; + }).join("")).join("") + ``; return `` + `` - + grid.join('') - + `` - + `` - + `` - + xLabels + + grid.join("") + + lines.slice().reverse().map(l => ``).join("") + + xLabelsSvg + legend + ``; }; -const renderExchange = (ex, history, taxStats) => { +const tsLabels = (arr, key) => { + if (!arr.length) return ["", "", ""]; + const f = (x) => moment(x[key]).format("YYYY-MM-DD HH:mm"); + return [f(arr[0]), f(arr[Math.floor(arr.length / 2)]), f(arr[arr.length - 1])]; +}; + +const buildEcoValueChartSvg = (history, labels) => { + const arr = Array.isArray(history) ? history.slice(-120) : []; + return buildSeriesChartSvg({ + series: [ + { label: labels.value || "Value", points: arr.map(s => Number(s.ecoValue || 0)) }, + { label: labels.supply || "Supply", points: arr.map(s => Number(s.currentSupply || 0)) }, + { label: labels.inflation || "Inflation", points: arr.map(s => Number(s.inflationFactor || 0)) } + ], + xLabels: tsLabels(arr, "ts"), + empty: labels.empty + }); +}; + +const renderChartBlock = (id, title, svg) => + div({ class: "bank-eco-chart-block" }, + h2({ class: "bank-eco-chart-title" }, title), + a({ href: `#${id}`, id: `${id}-src`, class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint }, + div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg }) + ), + div({ id, class: "lightbox bank-eco-chart-lightbox" }, + a({ href: `#${id}-src`, class: "lightbox-close" }, "\u00d7"), + div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg }) + ) + ); + +const renderRangedChart = (id, title, filterName, range, hasAnyData, svg) => { + if (!hasAnyData) return null; + return div({ class: "bank-eco-chart-block", id: `${id}-block` }, + h2({ class: "bank-eco-chart-title" }, title), + renderRangeTabs(filterName, range, `${id}-block`), + svg ? a({ href: `#${id}`, id: `${id}-src`, class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint }, div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg })) : p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataYet), + svg ? div({ id, class: "lightbox bank-eco-chart-lightbox" }, a({ href: `#${id}-src`, class: "lightbox-close" }, "\u00d7"), div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg })) : null + ); +}; + +const bucketLabel = (bucket) => bucket === "month" ? i18n.bankChartPerMonth : bucket === "day" ? i18n.bankChartPerDay : i18n.bankChartPerHour; +const bucketXLabels = (pts, bucket) => { + const fmt = (x) => bucket === "month" ? moment(x.ts).format("YYYY-MM") : bucket === "day" ? moment(x.ts).format("YYYY-MM-DD") : moment(x.ts).format("YYYY-MM-DD HH:mm"); + return pts.length ? [fmt(pts[0]), fmt(pts[Math.floor(pts.length / 2)]), fmt(pts[pts.length - 1])] : ["", "", ""]; +}; + +const renderBucketChart = (id, title, filterName, data, field, label, decimals = 2) => { + const pts = Array.isArray(data && data.points) ? data.points : []; + const svg = pts.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: `${label} ${bucketLabel(data.bucket)}`, points: pts.map(p => Number(p[field]) || 0) }], + xLabels: bucketXLabels(pts, data.bucket), + empty: i18n.bankExchangeChartEmpty, yDecimals: decimals + }) : null; + return renderRangedChart(id, title, filterName, (data && data.range) || "today", !!(data && data.hasAnyData), svg); +}; + +const renderSupplyChart = (history, range, hasAnyData, totalSupply = 0) => { + const arr = Array.isArray(history) ? history.slice(-240) : []; + const total = Number(totalSupply) || 0; + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: i18n.bankCurrentSupply, points: arr.map(s => Number(s.currentSupply) || 0) }], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty, yDecimals: 0 + }) : null; + return renderRangedChart("eco-supply-chart", i18n.bankChartSupplyTitle, "exchange", range, !!hasAnyData, svg); +}; + +const renderInflationChart = (history, range, hasAnyData) => { + const arr = Array.isArray(history) ? history.slice(-240) : []; + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: i18n.bankChartInflationSeries, points: arr.map(s => Number(s.inflationFactor) || 0) }], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty, yDecimals: 2 + }) : null; + return renderRangedChart("eco-inflation-chart", i18n.bankChartInflationTitle, "exchange", range, !!hasAnyData, svg); +}; + +const renderUbiCharts = (charts) => { + const pool = Array.isArray(charts && charts.pool) ? charts.pool : []; + const empty = i18n.bankExchangeChartEmpty; + const poolSvg = pool.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: i18n.bankChartPool, points: pool.map(x => x.balance) }], + xLabels: tsLabels(pool, "ts"), + empty, yDecimals: 2 + }) : null; + const out = [ + renderBucketChart("ubi-chart-network", i18n.bankChartNetworkUbiTitle, "ubi", charts && charts.payments, "distributed", i18n.bankChartDistributed, 6), + renderRangedChart("ubi-chart-pool", i18n.bankChartPoolTitle, "ubi", (charts && charts.poolRange) || "today", !!(charts && charts.hasPool), poolSvg), + renderBucketChart("ubi-chart-rebalance", i18n.bankChartRebalanceTitle, "ubi", charts && charts.rebalances, "rebalanced", i18n.bankChartRebalanced, 6) + ].filter(Boolean); + return out.length ? div(...out) : null; +}; + +const RANGES = ["today", "week", "month", "year", "5y", "all"]; +const rangeLabel = (r) => ({ today: i18n.bankRangeToday, week: i18n.bankRangeWeek, month: i18n.bankRangeMonth, year: i18n.bankRangeYear, "5y": i18n.bankRangeFiveYears, all: i18n.bankRangeAll })[r] || r; +const renderRangeTabs = (filter, current, anchor) => + div({ class: "bank-range-tabs" }, + ...RANGES.map(r => a({ href: `/banking?filter=${filter}&range=${r}#${anchor}`, class: r === current ? "activity-chip active" : "activity-chip" }, String(rangeLabel(r)).toUpperCase())) + ); + +const renderValueChart = (history, range, hasAnyData) => { + const arr = Array.isArray(history) ? history.slice(-240) : []; + if (!hasAnyData) return null; + const hasTime = arr.some(s => Number(s.ecoTimeMs) > 0); + const hasInflation = arr.some(s => Number(s.inflationFactor) > 0); + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [ + { label: i18n.bankExchangeChartValue || "Value (ECO/h)", points: arr.map(s => Number(s.ecoValue || 0)) }, + ...(hasTime ? [{ label: i18n.bankChartValueHours, points: arr.map(s => Number(s.ecoTimeMs || 0) / 3600000) }] : []), + ...(hasInflation ? [{ label: i18n.bankExchangeChartInflation || "Inflation %", points: arr.map(s => Number(s.inflationFactor || 0)) }] : []) + ], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty + }) : null; + return renderRangedChart("eco-value-chart", i18n.bankChartValueTitle, "exchange", range, !!hasAnyData, svg); +}; + +const renderWealthChart = (wealth, range) => { + const pts = Array.isArray(wealth && wealth.points) ? wealth.points : []; + if (!(wealth && wealth.hasAnyData)) return null; + if (pts.length < 2) return div({ class: "bank-eco-chart-block", id: "wealth-chart-block" }, + h2({ class: "bank-eco-chart-title" }, i18n.bankChartWealthTitle), + renderRangeTabs("exchange", range, "wealth-chart-block"), + p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataYet) + ); + const fmtBucket = (x) => wealth.bucket === "month" ? moment(x.ts).format("YYYY-MM") : wealth.bucket === "day" ? moment(x.ts).format("YYYY-MM-DD") : moment(x.ts).format("YYYY-MM-DD HH:mm"); + const per = wealth.bucket === "month" ? i18n.bankChartPerMonth : wealth.bucket === "day" ? i18n.bankChartPerDay : i18n.bankChartPerHour; + const svg = buildSeriesChartSvg({ + series: [ + { label: `${i18n.bankChartDistributed} ${per}`, points: pts.map(p => p.distributed) }, + { label: `${i18n.bankChartTaxes} ${per}`, points: pts.map(p => p.taxes) } + ], + xLabels: [fmtBucket(pts[0]), fmtBucket(pts[Math.floor(pts.length / 2)]), fmtBucket(pts[pts.length - 1])], + empty: i18n.bankExchangeChartEmpty, yDecimals: 2 + }); + const enough = pts.length >= 2; + return div({ class: "bank-eco-chart-block", id: "wealth-chart-block" }, + h2({ class: "bank-eco-chart-title" }, i18n.bankChartWealthTitle), + renderRangeTabs("exchange", range, "wealth-chart-block"), + enough ? a({ href: "#wealth-chart", id: "wealth-chart-src", class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint }, div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg })) : p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataRange), + enough ? div({ id: "wealth-chart", class: "lightbox bank-eco-chart-lightbox" }, a({ href: "#wealth-chart-src", class: "lightbox-close" }, "\u00d7"), div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg })) : null + ); +}; + +const renderFundsChart = (history) => { + const arr = Array.isArray(history && history.points) ? history.points : []; + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: "ECO", points: arr.map(x => x.balance) }], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty, yDecimals: 2 + }) : null; + return renderRangedChart("funds-chart", i18n.bankChartFundsTitle, "overview", (history && history.range) || "today", !!(history && history.hasAny), svg); +}; + +const renderKarmaChart = (history) => { + const arr = Array.isArray(history && history.points) ? history.points : []; + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: i18n.bankChartKarma, points: arr.map(x => x.score) }], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty, yDecimals: 0 + }) : null; + return renderRangedChart("karma-chart", i18n.bankChartKarmaTitle, "overview", (history && history.range) || "today", !!(history && history.hasAny), svg); +}; + +const renderExchange = (ex, history, taxStats, wealth, range, valueRange = range, valueHasAnyData = false) => { if (!ex) return div(p(i18n.bankExchangeNoData)); const syncStatus = ex.isSynced ? i18n.bankingSyncStatusSynced : i18n.bankingSyncStatusOutdated; - const syncStatusClass = ex.isSynced ? 'synced' : 'outdated'; const ecoTimeLabel = ex.isSynced ? fmtEcoTime(ex.ecoTimeMs) : fmtEcoTime(0); const chartLabels = { value: i18n.bankExchangeChartValue || 'Value (ECO/h)', @@ -154,24 +331,30 @@ const renderExchange = (ex, history, taxStats) => { table({ class: "bank-info-table" }, tbody( kvRow(i18n.bankingSyncStatus, - span({ class: syncStatusClass }, syncStatus) + span({ class: ex.isSynced ? "ubi-tick-ok" : "ubi-tick-bad", title: syncStatus }, ex.isSynced ? "✓" : "✗") ), - kvRow(i18n.bankExchangeCurrentValue, `${fmtIndex(ex.ecoValue)} ECO`), - kvRow(i18n.bankCurrentSupply, `${Number(ex.currentSupply || 0).toFixed(6)} ECO`), - kvRow(i18n.bankTotalSupply, `${Number(ex.totalSupply || 0).toFixed(6)} ECO`), - kvRow(i18n.bankEcoinHours, ecoTimeLabel), - kvRow(i18n.bankInflation, `${ex.inflationFactor.toFixed(2)}%`), - kvRow(i18n.bankInflationMonthly, `${Number(ex.inflationMonthly || 0).toFixed(2)}%`), - ...taxRows + kvRow(strong(i18n.bankTotalSupply), strong(`${Number(ex.totalSupply || 0).toFixed(6)} ECO`)), + ...(ex.isSynced ? [kvRow(strong(i18n.bankCurrentSupply), strong(`${Number(ex.currentSupply || 0).toFixed(6)} ECO`))] : []), + ...(ex.holdingSupply != null ? [kvRow(strong(i18n.bankHoldingSupply), strong(`${Number(ex.holdingSupply).toFixed(6)} ECO`))] : []), + kvRow(strong(i18n.bankPubsSupply), strong(`${Number(ex.pubsSupply || 0).toFixed(6)} ECO`)), + ...(ex.isSynced ? [ + kvRow(strong(i18n.bankInflation), strong(`${Number(ex.inflationFactor || 0).toFixed(2)}%`)), + kvRow(i18n.bankInflationMonthly, `${Number(ex.inflationMonthly || 0).toFixed(2)}%`) + ] : []), + kvRow(strong(i18n.bankTotalUbiDistributed), strong(`${Number((wealth && wealth.totals && wealth.totals.distributed) || 0).toFixed(6)} ECO`)), + kvRow(strong(i18n.bankTotalTaxesWithheld), strong(`${Number((wealth && wealth.totals && wealth.totals.taxes) || 0).toFixed(6)} ECO`)), + ...taxRows, + ...(ex.isSynced ? [ + kvRow(strong(i18n.bankExchangeCurrentValue), strong(`${fmtIndex(ex.ecoValue)} ECO`)), + kvRow(strong(i18n.bankEcoinHours), strong(ecoTimeLabel)) + ] : []) ) ) ), - hasEnoughSamples - ? div({ class: "bank-eco-chart-block" }, - h2({ class: "bank-eco-chart-title" }, i18n.bankExchangeChartTitle || 'ECOin value over time'), - div({ class: "bank-eco-chart-canvas", innerHTML: buildEcoValueChartSvg(history, chartLabels) }) - ) - : null + ex.isSynced ? renderValueChart(history, valueRange, !!valueHasAnyData) : null, + ex.isSynced ? renderWealthChart(wealth, (wealth && wealth.range) || range) : null, + renderSupplyChart(history, range, hasEnoughSamples, ex.totalSupply), + renderInflationChart(history, range, hasEnoughSamples) ); }; @@ -248,7 +431,7 @@ const renderTaxes = (data, lookup) => { (i18n.bankTaxesUserNoteOutro || ', and their ongoing contribution may be required by networking consensus.') ), p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChips || 'The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.'), - p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChipsClick || 'Click any chip to inspect its block in the blockexplorer.'), + process.env.OASIS_MOBILE === '1' ? null : p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChipsClick || 'Click any chip to inspect its block in the blockexplorer.'), (() => { const firstBlock = data.firstBlock || null; const sampleSize = Number(data.firstBlockSize || 0); @@ -258,17 +441,17 @@ const renderTaxes = (data, lookup) => { const sampleValue = sampleSize > 0 ? formatCarbon(sampleSize) : '— CO₂'; const label = i18n.ecoTaxLabel || 'ECO Tax'; return p({ class: "bank-taxes-example-chips" }, - a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-low', title: label + ' · low' }, + (process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-low', title: label + ' · low' }, span({ class: 'eco-tax-chip-label' }, label + ': '), span({ class: 'eco-tax-chip-value' }, sampleValue) ), ' ', - a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-mid', title: label + ' · mid' }, + (process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-mid', title: label + ' · mid' }, span({ class: 'eco-tax-chip-label' }, label + ': '), span({ class: 'eco-tax-chip-value' }, sampleValue) ), ' ', - a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-high', title: label + ' · high' }, + (process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-high', title: label + ' · high' }, span({ class: 'eco-tax-chip-label' }, label + ': '), span({ class: 'eco-tax-chip-value' }, sampleValue) ) @@ -297,7 +480,35 @@ const renderTaxes = (data, lookup) => { ); }; -const renderOverviewSummaryTable = (s, rules, userEcoinTax) => { +const renderUbiPubs = (pubs, hasValidWallet) => { + const fmt = (ts) => Number(ts) > 0 ? moment(Number(ts)).format("YYYY/MM/DD HH:mm") : "—"; + if (!pubs.length) return div({ class: "bank-summary" }, p(i18n.bankUbiPubNone)); + return div({ class: "addr-cards ubi-pub-cards" }, + ...pubs.map(p => div({ class: "addr-card ubi-pub-card" + (p.isThisNode || p.isConnected ? " own-content" : "") }, + div({ class: "addr-card-head" }, + userLink(p.pubId), + p.isThisNode || p.isConnected ? span({ class: "status you" }, p.isThisNode ? i18n.bankUbiPubThisNode : i18n.bankUbiPubConnected) : null, + hasValidWallet && p.address + ? div({ class: "content-actions" }, form({ method: "GET", action: "/banking/fund", class: "content-action-form" }, input({ type: "hidden", name: "pub", value: p.pubId }), button({ type: "submit", class: "btn-singleview btn-donate", title: i18n.bankDonateEco }, renderIcon("donate")))) + : null + ), + div({ class: "bank-summary ubi-pub-summary" }, + table({ class: "bank-info-table" }, + tbody( + kvRow(i18n.bankUbiAvailability, span({ class: p.available ? "ubi-tick-ok" : "ubi-tick-bad" }, p.available ? "✓" : "✗")), + kvRow(i18n.bankUbiPool, strong(`${Number(p.balance || 0).toFixed(6)} ECO`)), + kvRow(i18n.bankUbiLastSeen, fmt(p.timestamp)), + kvRow(i18n.bankUbiLastPayout, fmt(p.lastPayoutAt)), + kvRow(i18n.bankUbiPaidOut, strong(`${Number(p.paidOut || 0).toFixed(6)} ECO`)) + ) + ) + ) + )) + ); +}; + +const renderOverviewSummaryTable = (s, rules, userEcoinTax, isPub = false) => { + const ubiWired = !!s.pubId || isPub; const score = Number(s.userEngagementScore || 0); const pool = Number(s.pool || 0); const W = Math.max(1, Number(s.weightsSum || 1)); @@ -307,36 +518,48 @@ const renderOverviewSummaryTable = (s, rules, userEcoinTax) => { const gross = Math.max(floor, Math.min(pool * (w / W), cap)); const surplus = Math.max(0, gross - floor); const tax = Number(userEcoinTax || 0); - const future = floor + Math.max(0, surplus - tax); - const availClass = s.ubiAvailability === "OK" ? "ubi-available" : "ubi-unavailable"; - const availLabel = s.ubiAvailability === "OK" ? i18n.bankUbiAvailableOk : i18n.bankUbiAvailableNo; + const ubiOn = ubiWired && s.ubiAvailability === "OK"; + const future = ubiOn ? floor + Math.max(0, surplus - tax) : 0; return div({ class: "bank-summary" }, table({ class: "bank-info-table" }, tbody( - kvRow(i18n.bankUserBalance, `${Number(s.userBalance || 0).toFixed(6)} ECO`), - kvRow(i18n.bankUbiAvailability, span({ class: availClass }, availLabel)), - s.pubId ? kvRow(i18n.pubIdLabel, userLink(s.pubId)) : null, + kvRow(strong(i18n.bankUserBalance), strong(`${Number(s.userBalance || 0).toFixed(6)} ECO`)), + Number(s.industryNetworkTotal || 0) > 0 ? kvRow(strong(i18n.bankIndustryBalance || "Industry Production"), a({ href: '/industry' }, strong(`${Number(s.industryNetworkTotal || 0).toFixed(6)} ECO`))) : null, kvRow(i18n.bankEpoch, String(s.epochId || "-")), kvRow(i18n.bankPool, `${pool.toFixed(6)} ECO`), kvRow(i18n.bankWeightsSum, String(W.toFixed(6))), - kvRow(i18n.bankingUserEngagementScore, String(score)), - kvRow(i18n.bankOverviewYourTaxes || 'Your taxes', a({ href: '/banking?filter=taxes' }, `${tax.toFixed(6)} ECO`)), - kvRow(i18n.bankUbiThisMonth, `${future.toFixed(6)} ECO`) + kvRow(strong(i18n.bankingUserEngagementScore), strong(String(score))), + ubiWired ? kvRow(i18n.bankYourUbiMonth || 'Your UBI (this month)', `${future.toFixed(6)} ECO`) : null, + Number(s.industryBalance || 0) > 0 ? kvRow(i18n.bankYourIndustryBalance || 'Your Industry Share', a({ href: '/industry?filter=MEMBER' }, `${Number(s.industryBalance || 0).toFixed(6)} ECO`)) : null, + Number(s.schoolBalance || 0) > 0 ? kvRow(i18n.bankYourSchoolBalance || 'Your School Earnings', a({ href: '/school?filter=mine' }, `${Number(s.schoolBalance || 0).toFixed(6)} ECO`)) : null, + kvRow(i18n.bankTotalUbiDistributed, `${Number((s.wealthTotals && s.wealthTotals.distributed) || 0).toFixed(6)} ECO`), + kvRow(i18n.bankTotalTaxesWithheld, `${Number((s.wealthTotals && s.wealthTotals.taxes) || 0).toFixed(6)} ECO`), + kvRow(strong(i18n.bankOverviewYourTaxes || 'Total Taxes'), a({ href: '/banking?filter=taxes' }, strong(`${tax.toFixed(6)} ECO`))), + kvRow(strong(i18n.bankYourFundsMonth || 'Total Funds'), strong(`${((ubiWired ? future : 0) + Number(s.industryBalance || 0) + Number(s.schoolBalance || 0)).toFixed(6)} ECO`)) ) ) ); }; -const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, hasValidWallet, ubiAvailability) => { +const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, hasValidWallet, ubiAvailability, alreadyRefused = false, addressPublished = false, ecoinSynced = false) => { + if (!ecoinSynced) return ""; if (alreadyClaimed) return ""; + if (alreadyRefused) return div({ class: "bank-claim-ubi" }, div({ class: "bank-claim-card" }, p(i18n.bankUbiRefusedThisMonth))); if (!pubId && !isPub) return ""; - if (!isPub && !hasValidWallet) return ""; + if (!isPub && !(hasValidWallet && addressPublished)) return ""; if (!isPub && ubiAvailability !== "OK") return ""; - if (!pendingAllocation && !isPub) { + if (!isPub) { return div({ class: "bank-claim-ubi" }, div({ class: "bank-claim-card" }, - form({ method: "POST", action: "/banking/claim-ubi" }, - button({ type: "submit", class: "create-button bank-claim-btn" }, i18n.bankClaimUBI) + pendingAllocation ? p(`${i18n.bankUbiThisMonth}: `, span({ class: "accent" }, `${Number(pendingAllocation.amount || 0).toFixed(6)} ECO`)) : null, + pendingAllocation ? p(`${i18n.bankEpoch}: `, span(pendingAllocation.concept || "")) : null, + div({ class: "bank-claim-actions" }, + form({ method: "POST", action: "/banking/claim-ubi" }, + button({ type: "submit", class: "create-button bank-claim-btn" }, i18n.bankClaimUBI) + ), + form({ method: "POST", action: "/banking/refuse-ubi" }, + button({ type: "submit", class: "delete-btn bank-claim-btn" }, i18n.bankRefuseUBI) + ) ) ) ); @@ -355,7 +578,7 @@ const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, ha const filterAllocations = (allocs, filter, userId) => { if (filter === "mine") return allocs.filter(a => a.to === userId && (a.status === "UNCLAIMED" || a.status === "UNCONFIRMED")); - if (filter === "pending") return allocs.filter(a => a.status === "UNCLAIMED" || a.status === "UNCONFIRMED"); + if (filter === "pending") return allocs.filter(a => a.status !== "EXPIRED"); if (filter === "closed") return allocs.filter(a => a.status === "CLOSED"); if (filter === "claimed") return allocs.filter(a => a.status === "CLAIMED"); if (filter === "expired") return allocs.filter(a => a.status === "EXPIRED"); @@ -374,27 +597,23 @@ const allocationsTable = (rows = [], userId) => th(i18n.bankAllocFrom), th(i18n.bankAllocTo), th(i18n.bankAllocAmount), - th(i18n.bankAllocStatus), - th("") + th(i18n.bankAllocStatus) ) ), tbody( ...rows.map(r => tr( - td(new Date(r.createdAt).toLocaleString()), + td(moment(r.createdAt).format("YYYY/MM/DD HH:mm")), td(r.concept || ""), td(userLink(r.from)), td(userLink(r.to)), td(String(Number(r.amount || 0).toFixed(6))), - td(r.status), td( - (r.status === "UNCLAIMED" || r.status === "UNCONFIRMED") && r.to === userId - ? form({ method: "POST", action: `/banking/claim/${encodeURIComponent(r.id)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.bankClaimNow) - ) - : r.status === "CLOSED" && r.txid - ? a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(r.txid)}`, target: "_blank", class: "btn-singleview" }, i18n.bankViewTx) - : null + r.status === "CLOSED" || r.status === "CLAIMED" + ? span({ class: "ubi-tick-ok", title: r.status }, "✓") + : r.status === "UNCLAIMED" || r.status === "UNCONFIRMED" + ? span({ class: "ubi-tick-bad", title: r.status }, "✗") + : r.status ) ) ) @@ -406,16 +625,15 @@ const renderEpochList = (epochs = []) => ? div(p(i18n.bankNoEpochs)) : table( { class: "bank-epochs" }, - thead(tr(th(i18n.bankEpochId), th(i18n.bankPool), th(i18n.bankWeightsSum), th(i18n.bankRuleHash), th(""))), + thead(tr(th(i18n.bankEpochId), th(i18n.bankPool), th(i18n.bankEpochAllocations), th(""))), tbody( - ...epochs - .sort((a, b) => String(b.id).localeCompare(String(a.id))) + ...paged(epochs + .sort((a, b) => String(b.id).localeCompare(String(a.id)))) .map(e => tr( td(e.id), td(String(Number(e.pool || 0).toFixed(6))), - td(String(Number(e.weightsSum || 0).toFixed(6))), - td(e.hash || "-"), + td(String(Number(e.recipients || 0))), td( form({ method: "GET", action: `/banking/epoch/${encodeURIComponent(e.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.bankViewEpoch) @@ -432,6 +650,9 @@ const rulesBlock = (rules, taxRules) => { const tr = taxRules || {}; const fmt = (n, d = 4) => (Number.isFinite(Number(n)) ? Number(n).toFixed(d) : "—"); return div({ class: "bank-rules" }, + h2(i18n.bankRulesEligibilityTitle), + ul({ class: "bank-rules-list" }, ...[i18n.bankRulesEligibility1, i18n.bankRulesEligibility2, i18n.bankRulesEligibility3, i18n.bankRulesEligibility4, i18n.bankRulesEligibility5, i18n.bankRulesEligibility6].map(x => li(x))), + h2(i18n.bankRulesPoolTitle || "Monthly pool"), table({ class: "bank-info-table" }, tbody( @@ -512,118 +733,100 @@ const flashText = (key) => { if (key === "claimed_pending") return i18n.bankClaimedPending; if (key === "already_claimed") return i18n.bankAlreadyClaimedThisMonth; if (key === "no_pub_configured") return i18n.bankNoPubConfigured; + if (key === "no_pub_address") return i18n.bankNoPubAddress; + if (key === "refused") return i18n.bankUbiRefusedThisMonth; + if (key === "already_refused") return i18n.bankUbiRefusedThisMonth; if (key === "no_funds") return i18n.bankUbiAvailableNo; if (key === "forbidden") return i18n.bankAddressForbidden; return ""; }; -const flashBanner = (msgKey) => - !msgKey ? null : div({ class: "flash-banner" }, p(flashText(msgKey) || msgKey)); - -const addressesToolbar = (rows = [], search = "") => - div({ class: "addr-toolbar" }, - div({ class: "addr-counter accent-pill" }, - span({ class: "acc-title accent" }, i18n.bankAddressTotal + ":"), - span({ class: "acc-badge" }, String(rows.length)) - ), - form({ method: "GET", action: "/banking", class: "addr-search" }, - input({ type: "hidden", name: "filter", value: "addresses" }), - input({ type: "text", name: "q", placeholder: i18n.bankAddressSearch, value: search || "" }), - br(), - button({ type: "submit", class: "filter-btn" }, i18n.search) - ) - ); - const renderAddresses = (data, userId) => { const rows = data.addresses || []; const search = data.search || ""; + const hasContent = rows.length > 0 || !!search; return div( - data.flash ? flashBanner(data.flash) : null, - addressesToolbar(rows, search), - div({ class: "bank-addresses-stack" }, - div({ class: "addr-form-card wide" }, - h2(i18n.bankAddAddressTitle), - form({ method: "POST", action: "/banking/addresses", class: "addr-form" }, - div({ class: "form-row" }, - span({ class: "form-label accent" }, i18n.bankAddAddressUser + ":"), - input({ - class: "form-input xl", - type: "text", - name: "userId", - required: true, - pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$", - placeholder: "@...=.ed25519", - id: "addr-user-id" - }) - ), - div({ class: "form-row" }, - span({ class: "form-label accent" }, i18n.bankAddAddressAddress + ":"), - input({ - class: "form-input xl", - type: "text", - name: "address", - required: true, - pattern: "^[A-Za-z0-9]{20,64}$", - placeholder: "ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX" - }) - ), - div({ class: "form-actions" }, - button({ type: "submit", class: "filter-btn" }, i18n.bankAddAddressSave) + div({ class: "bank-summary" }, + h2(i18n.bankAddAddressTitle), + form({ method: "POST", action: "/banking/addresses", class: "bank-form" }, + label({ for: "addr-label" }, i18n.bankAddAddressLabel), br(), + input({ type: "text", id: "addr-label", name: "label", maxlength: "80", placeholder: i18n.bankAddAddressLabelPlaceholder }), br(), + label({ for: "addr-user-id" }, i18n.bankAddAddressUserOptional), br(), + input({ type: "text", id: "addr-user-id", name: "userId", pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$", placeholder: "@...=.ed25519" }), br(), + label({ for: "addr-address" }, i18n.bankAddAddressAddress), br(), + input({ type: "text", id: "addr-address", name: "address", required: true, pattern: "^[A-Za-z0-9]{20,64}$", placeholder: "ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX" }), br(), + button({ type: "submit", class: "create-button" }, i18n.bankAddAddressSave) + ) + ), + hasContent + ? form({ method: "GET", action: "/banking", class: "filter-box bank-addr-search" }, + input({ type: "hidden", name: "filter", value: "addresses" }), + input({ type: "text", name: "q", placeholder: i18n.bankAddressSearch, value: search || "", class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + : null, + rows.length === 0 + ? div({ class: "no-content-box" }, p(i18n.bankNoAddresses)) + : div({ class: "bank-summary" }, + renderModuleStats(rows.length, [ + { label: i18n.bankLocal, count: rows.filter(r => r.source !== "ssb").length }, + { label: i18n.bankFromOasis, count: rows.filter(r => r.source === "ssb").length } + ]), + table({ class: "bank-addresses" }, + thead(tr(th(i18n.bankAddAddressLabel.replace(/\s*\(.*\)\s*$/, "")), th(i18n.bankAddAddressUser), th(i18n.bankAddress), th(i18n.bankAddressSource), th(""))), + tbody( + ...paged(rows).map(r => tr( + td(r.label || (r.id ? "" : i18n.bankAddressUnnamed)), + td(r.id ? userLink(r.id) : ""), + td(strong({ class: "bank-address-code" }, r.address)), + td(r.source === "ssb" ? i18n.bankFromOasis : i18n.bankLocal), + td(r.source === "ssb" && String(r.id) !== String(userId) + ? "" + : form({ method: "POST", action: "/banking/addresses/delete", class: "addr-del" }, + input({ type: "hidden", name: "userId", value: r.id || "" }), + input({ type: "hidden", name: "source", value: r.source || "local" }), + input({ type: "hidden", name: "entryId", value: r.entryId || "" }), + button({ type: "submit", class: "delete-btn" }, i18n.bankAddressDelete) + )) + )) + ) ) ) - ), - div({ class: "addr-list-card" }, - rows.length === 0 - ? div(p(i18n.bankNoAddresses)) - : table( - { class: "bank-addresses" }, - thead( - tr( - th(i18n.bankUser), - th(i18n.bankAddress), - th(i18n.bankAddressSource), - th(i18n.bankAddressActions) - ) - ), - tbody( - ...rows.map(r => - tr( - td(userLink(r.id)), - td(r.address), - td(r.source === "local" ? i18n.bankLocal : i18n.bankFromOasis), - td( - div({ class: "row-actions" }, - form({ method: "POST", action: "/banking/addresses/delete", class: "addr-del" }, - input({ type: "hidden", name: "userId", value: r.id }), - input({ type: "hidden", name: "source", value: r.source || "local" }), - button({ type: "submit", class: "delete-btn", onclick: `return confirm(${JSON.stringify(i18n.bankAddressDeleteConfirm)})` }, i18n.bankAddressDelete) - ) - ) - ) - ) - ) - ) - ) - ) - ) ); }; +const bankingFilters = (data, filter, userId) => { + const allocs = data.allocations || []; + const subsets = ["mine", "pending", "closed", "claimed", "expired"].filter(f => filterAllocations(allocs, f, userId).length > 0 || filter === f); + return [ + "overview", "exchange", "ubi", "taxes", + ...subsets, + ...((data.epochs || []).length || filter === "epochs" || String(filter || "").startsWith("epoch:") ? ["epochs"] : []), + "addresses", "rules" + ]; +}; + const renderBankingView = (data, filter, userId, isPub) => template( i18n.banking, section( - div({ class: "tags-header" }, h2(i18n.banking), p(i18n.bankingDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.banking), p(i18n.bankingDescription), renderWalletChip()), data.flash ? div({ class: "flash-banner" }, p(flashText(data.flash) || data.flash)) : null, - generateFilterButtons(["overview","exchange","taxes","mine","pending","closed","claimed","expired","epochs","rules","addresses"], filter, "/banking"), + generateFilterButtons(bankingFilters(data, filter, userId), filter, "/banking"), filter === "overview" ? div( - renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax), - renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability), - allocationsTable((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), userId) + renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax, isPub), + renderKarmaChart(data.karmaHistory), + renderFundsChart(data.fundsHistory) + ) + : filter === "ubi" + ? div( + renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability, (data.summary || {}).alreadyRefused, (data.summary || {}).addressPublished, !!(data.exchange && data.exchange.isSynced)), + renderUbiPubs(data.ubiPubs || [], !!(data.summary || {}).hasValidWallet), + renderUbiCharts({ ...(data.ubiCharts || {}), payments: data.ubiPayments, rebalances: data.ubiRebalances }) ) : filter === "exchange" - ? renderExchange(data.exchange, data.exchangeHistory, data.taxStats) + ? renderExchange(data.exchange, data.exchangeHistory, data.taxStats, data.wealth, data.range, data.valueRange || data.range, !!data.valueHasAnyData) : filter === "taxes" ? renderTaxes(data, data.lookup || null) : filter === "epochs" @@ -633,7 +836,7 @@ const renderBankingView = (data, filter, userId, isPub) => : filter === "addresses" ? renderAddresses(data, userId) : allocationsTable( - filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId), + paged(filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId)), userId ) ) @@ -655,7 +858,6 @@ const renderSingleAllocationView = (alloc, userId) => { kvRow(i18n.bankAllocAmount, `${Number(alloc.amount || 0).toFixed(6)} ECO`), kvRow(i18n.bankAllocStatus, alloc.status || "-"), kvRow(i18n.bankAllocDate, alloc.createdAt ? fmtDate(alloc.createdAt) : "-"), - alloc.txid ? kvRow("TxID", a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(alloc.txid)}`, target: "_blank" }, alloc.txid)) : null ) ) ), @@ -669,26 +871,36 @@ const renderSingleAllocationView = (alloc, userId) => { ); }; -const renderEpochView = (epoch, allocations) => { - if (!epoch) return template(i18n.banking, section(div(p(i18n.bankNoEpochs)))); +const bankingHeader = () => + div({ class: "tags-header module-header-line" }, h2(i18n.banking), p(i18n.bankingDescription), renderWalletChip()); + +const renderEpochView = (epoch, allocations, userId = "", data = {}) => { + const rows = allocations || []; return template( i18n.banking, section( - div({ class: "tags-header" }, h2(`${i18n.bankEpoch}: ${epoch.id}`)), - div({ class: "bank-summary" }, - table({ class: "bank-info-table" }, - tbody( - kvRow(i18n.bankEpochId, epoch.id || "-"), - kvRow(i18n.bankPool, `${Number(epoch.pool || 0).toFixed(6)} ECO`), - kvRow(i18n.bankWeightsSum, String(Number(epoch.weightsSum || 0).toFixed(6))), - kvRow(i18n.bankRuleHash, epoch.hash || "-") + bankingHeader(), + generateFilterButtons(bankingFilters({ ...data, allocations: rows, epochs: data.epochs || [{}] }, "epochs", userId), "epochs", "/banking"), + !epoch + ? div({ class: "no-content-box" }, p(i18n.bankNoEpochs)) + : div( + h2(`${i18n.bankEpoch}: ${epoch.id}`), + div({ class: "bank-summary" }, + table({ class: "bank-info-table" }, + tbody( + kvRow(i18n.bankEpochId, epoch.id || "-"), + kvRow(i18n.bankPool, `${Number(epoch.pool || 0).toFixed(6)} ECO`), + Number.isFinite(Number(epoch.weightsSum)) && Number(epoch.weightsSum) > 0 ? kvRow(i18n.bankWeightsSum, String(Number(epoch.weightsSum).toFixed(6))) : null, + kvRow(i18n.bankEpochAllocations, String(rows.length)), + epoch.hash && epoch.hash !== "-" ? kvRow(i18n.bankRuleHash, epoch.hash) : null + ) + ) + ), + h2(i18n.bankEpochAllocations), + allocationsTable(paged(rows), userId) ) - ) - ), - h2(i18n.bankEpochAllocations), - allocationsTable(allocations || [], "") ) ); }; -module.exports = { renderBankingView, renderSingleAllocationView, renderEpochView }; +module.exports = { renderBankingView, renderSingleAllocationView, renderEpochView, bankingFlashText: flashText }; diff --git a/src/views/blockchain_view.js b/src/views/blockchain_view.js index a70d752..2c9ae2e 100644 --- a/src/views/blockchain_view.js +++ b/src/views/blockchain_view.js @@ -1,5 +1,5 @@ const { div, h2, h3, p, section, button, form, a, input, span, pre, table, tr, td, strong } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink } = require("../views/main_views"); +const { template, i18n, userLink, paged, renderIcon } = require("../views/main_views"); const moment = require("../server/node_modules/moment"); const FILTER_LABELS = { @@ -9,19 +9,20 @@ const FILTER_LABELS = { feed: i18n.typeFeed, event: i18n.typeEvent, task: i18n.typeTask, report: i18n.typeReport, image: i18n.typeImage, audio: i18n.typeAudio, video: i18n.typeVideo, post: i18n.typePost, forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub, - transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, tribe: i18n.typeTribe, - project: i18n.typeProject, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim, + transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, housing: i18n.typeHousing, tribe: i18n.typeTribe, + project: i18n.typeProject, industry: i18n.typeIndustry, industryBlueprint: i18n.industryBlueprints, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim, aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament, courts: i18n.typeCourts, map: i18n.typeMap, shop: i18n.typeShop, shopProduct: i18n.typeShopProduct || 'Shop Product', - pad: i18n.typePad || 'PAD', chat: i18n.typeChat || 'CHAT', gameScore: i18n.typeGameScore || 'GAME SCORE', - calendar: i18n.typeCalendar || 'CALENDAR', torrent: i18n.typeTorrent + pad: i18n.typePad || 'PAD', room: i18n.typeRoom || 'ROOM', roomMember: i18n.typeRoomMember || 'ROOM PARTICIPANT', wikiPage: i18n.typeWiki || 'WIKI', emergency: i18n.typeEmergency || 'EMERGENCY', emergencyConfirm: i18n.typeEmergencyConfirm || 'EMERGENCY CONFIRM', emergencyUpdate: i18n.typeEmergencyUpdate || 'EMERGENCY UPDATE', mailingList: i18n.typeMailingList || 'MAILING LIST', logisticsRoute: i18n.typeLogisticsRoute || 'ROUTE', logisticsRating: i18n.typeLogisticsRating || 'ROUTE RATING', podcast: i18n.typePodcast || 'PODCAST', podcastEpisode: i18n.typePodcastEpisode || 'EPISODE', podcastOpinion: i18n.typePodcastOpinion || 'EPISODE OPINION', podcastPlay: i18n.typePodcastPlay || 'EPISODE PLAY', campaign: i18n.typeCampaign || 'CAMPAIGN', campaignSignature: i18n.typeCampaignSignature || 'SIGNATURE', campaignUpdate: i18n.typeCampaignUpdate || 'CAMPAIGN UPDATE', chat: i18n.typeChat || 'CHAT', gameScore: i18n.typeGameScore || 'GAME SCORE', + calendar: i18n.typeCalendar || 'CALENDAR', torrent: i18n.typeTorrent, file: i18n.typeFile, + school: i18n.typeSchool || 'SCHOOL' }; -const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone', 'logs']; -const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'parliament', 'courts']; +const BASE_FILTERS = ['all', 'recent', 'mine', 'tombstone', 'logs']; +const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'school', 'parliament', 'courts']; const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange']; -const CAT_BLOCK3 = ['banking', 'job', 'market', 'project', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore']; -const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent']; +const CAT_BLOCK3 = ['banking', 'job', 'housing', 'market', 'project', 'industry', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore']; +const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'room', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent', 'file']; const SEARCH_FIELDS = ['author','id','from','to']; @@ -75,6 +76,7 @@ const filterBlocks = (blocks, filter, userId) => { return blocks.filter(b => cset.has(b.type)); } if (filter === 'shop') return blocks.filter(b => b.type === 'shop' || b.type === 'shopProduct'); + if (filter === 'school') return blocks.filter(b => ['schoolCourse', 'schoolLesson', 'schoolEnroll', 'schoolCertificate'].includes(b.type)); if (filter === 'logs') return blocks.filter(b => b.type === 'log' && b.author === userId); return blocks.filter(b => b.type === filter); }; @@ -100,7 +102,7 @@ const computeStats = (blocks) => { const renderStatsPanel = (stats, currentFilter, search) => { if (!stats || stats.total === 0) return null; - const topTypes = stats.typeBreakdown.slice(0, 10); + const topTypes = stats.typeBreakdown.filter(t => t.type !== 'industryBlueprint').slice(0, 10); return div({ class: 'blockchain-stats' }, div({ class: 'tags-header' }, h3(`${i18n.blockchainStatsTitle || 'Stats'} (${stats.total})`) @@ -173,13 +175,19 @@ const getViewDetailsAction = (type, block) => { case 'event': return `/events/${encodeURIComponent(block.id)}`; case 'task': return `/tasks/${encodeURIComponent(block.id)}`; case 'about': return `/author/${encodeURIComponent(block.author)}`; - case 'post': return `/thread/${encodeURIComponent(block.id)}#${encodeURIComponent(block.id)}`; + case 'post': return block.content && typeof block.content.root === 'string' + ? `/thread/${encodeURIComponent(block.content.root)}#${encodeURIComponent(block.id)}` + : `/blogs/${encodeURIComponent(block.id)}`; case 'vote': return `/thread/${encodeURIComponent(block.content.vote.link)}#${encodeURIComponent(block.content.vote.link)}`; case 'contact': return `/inhabitants`; case 'pub': return `/invites`; case 'market': return `/market/${encodeURIComponent(block.id)}`; case 'job': return `/jobs/${encodeURIComponent(block.id)}`; + case 'housing': return `/housing/${encodeURIComponent(block.id)}`; case 'project': return `/projects/${encodeURIComponent(block.id)}`; + case 'schoolCourse': return `/school/course/${encodeURIComponent(block.id)}`; + case 'industry': return `/industry/${encodeURIComponent(block.id)}`; + case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(block.id)}`; case 'report': return `/reports/${encodeURIComponent(block.id)}`; case 'calendar': return `/calendars/${encodeURIComponent(block.id)}`; case 'bankWallet': return `/wallet`; @@ -200,13 +208,17 @@ const getViewDetailsAction = (type, block) => { case 'courtsNominationVote': return `/courts`; case 'map': return `/maps/${encodeURIComponent(block.id)}`; case 'torrent': return `/torrents/${encodeURIComponent(block.id)}`; + case 'file': return `/files/${encodeURIComponent(block.id)}`; case 'mapMarker': return block.content?.mapId ? `/maps/${encodeURIComponent(block.content.mapId)}` : `/maps`; case 'shop': return `/shops/${encodeURIComponent(block.id)}`; case 'shopProduct': return `/shops/product/${encodeURIComponent(block.id)}`; case 'pad': return `/pads/${encodeURIComponent(block.id)}`; + case 'room': return `/rooms/${encodeURIComponent(block.id)}`; + case 'roomMember': return block.content?.target ? `/rooms/${encodeURIComponent(block.content.target)}` : `/rooms`; case 'chat': return `/chats/${encodeURIComponent(block.id)}`; case 'gameScore': return `/games?filter=scoring`; case 'log': return `/logs/view/${encodeURIComponent(block.id)}`; + case 'poll': return `/polls/${encodeURIComponent(block.id)}`; case 'calendarDate': case 'calendarNote': return block.content?.calendarId ? `/calendars/${encodeURIComponent(block.content.calendarId)}` : `/calendars`; case 'padEntry': return block.content?.padId ? `/pads/${encodeURIComponent(block.content.padId)}` : `/pads`; @@ -219,8 +231,8 @@ const TYPE_COLORS = { post:'#3498db', vote:'#9b59b6', votes:'#9b59b6', about:'#1abc9c', contact:'#16a085', pub:'#2ecc71', tribe:'#e67e22', event:'#e74c3c', task:'#f39c12', report:'#c0392b', image:'#2980b9', audio:'#8e44ad', video:'#d35400', document:'#27ae60', bookmark:'#f1c40f', - forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db', - project:'#2ecc71', banking:'#f39c12', bankWallet:'#f39c12', bankClaim:'#f39c12', + forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db', housing:'#16a085', + project:'#2ecc71', schoolCourse:'#1abc9c', schoolLesson:'#1abc9c', schoolEnroll:'#1abc9c', schoolCertificate:'#1abc9c', banking:'#f39c12', bankWallet:'#f39c12', bankClaim:'#f39c12', pixelia:'#9b59b6', curriculum:'#1abc9c', aiExchange:'#3498db', tombstone:'#7f8c8d', parliamentTerm:'#8e44ad', parliamentProposal:'#8e44ad', parliamentLaw:'#8e44ad', parliamentCandidature:'#8e44ad', parliamentRevocation:'#8e44ad', @@ -228,7 +240,7 @@ const TYPE_COLORS = { courtsVerdict:'#c0392b', courtsSettlement:'#c0392b', courtsNomination:'#c0392b', map:'#27ae60', mapMarker:'#27ae60', shop:'#e67e22', shopProduct:'#e67e22', - pad:'#2ecc71', chat:'#3498db', gameScore:'#f39c12', + pad:'#2ecc71', room:'#27ae60', roomMember:'#27ae60', wikiPage:'#1abc9c', emergency:'#e74c3c', emergencyConfirm:'#e74c3c', emergencyUpdate:'#e74c3c', mailingList:'#8e44ad', logisticsRoute:'#16a085', logisticsRating:'#16a085', podcast:'#d35400', podcastEpisode:'#d35400', podcastOpinion:'#d35400', podcastPlay:'#d35400', campaign:'#c0392b', campaignSignature:'#c0392b', campaignUpdate:'#c0392b', chat:'#3498db', gameScore:'#f39c12', calendar:'#e74c3c' }; @@ -303,7 +315,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi return template( i18n.blockchain, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.blockchain), p(i18n.blockchainDescription) ), @@ -389,7 +401,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi return template( i18n.blockchain, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.blockchain), p(i18n.blockchainDescription) ), @@ -414,9 +426,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`) ), !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ? - form({ method:'GET', action:getViewDetailsAction(block.type, block) }, - button({ type:'submit', class:'filter-btn' }, i18n.visitContent) - ) + a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, renderIcon('open')) : (block.isTombstoned || block.isReplaced) ? div({ class: 'deleted-label' }, i18n.blockchainContentDeleted || "This content has been deleted." @@ -438,25 +448,28 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) const shown = filterBlocks(blocks, filter, userId); const qs = toQueryString(filter, s); + const censusBlocks = Array.isArray(extras && extras.censusBlocks) ? extras.censusBlocks : blocks; + const chipVisible = (mode) => mode === filter || filterBlocks(censusBlocks, mode, userId).length > 0; + const baseChips = ['all', 'recent', 'mine', ...(chipVisible('tombstone') ? ['tombstone'] : []), ...(chipVisible('logs') ? ['logs'] : [])]; return template( i18n.blockchain, section( - div({ class:'tags-header' }, + div({ class:'tags-header module-header-line' }, h2(i18n.blockchain), p(i18n.blockchainDescription) ), div({ class:'mode-buttons-row' }, div({ class: 'filter-column' }, - generateFilterButtons(BASE_FILTERS, filter, '/blockexplorer', s) + generateFilterButtons(baseChips, filter, '/blockexplorer', s) ), div({ class: 'filter-column' }, - generateFilterButtons(CAT_BLOCK1, filter, '/blockexplorer', s), - generateFilterButtons(CAT_BLOCK2, filter, '/blockexplorer', s) + generateFilterButtons(CAT_BLOCK1.filter(chipVisible), filter, '/blockexplorer', s), + generateFilterButtons(CAT_BLOCK2.filter(chipVisible), filter, '/blockexplorer', s) ), div({ class: 'filter-column' }, - generateFilterButtons(CAT_BLOCK3, filter, '/blockexplorer', s), - generateFilterButtons(CAT_BLOCK4, filter, '/blockexplorer', s) + generateFilterButtons(CAT_BLOCK3.filter(chipVisible), filter, '/blockexplorer', s), + generateFilterButtons(CAT_BLOCK4.filter(chipVisible), filter, '/blockexplorer', s) ) ), div({ class: 'blockexplorer-search' }, @@ -472,7 +485,7 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) input({ type: 'datetime-local', name: 'to', value: toVal, class: 'blockexplorer-search-input' }) ), div({ class: 'blockexplorer-search-actions' }, - button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit) + div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit)) ) ) ) @@ -510,7 +523,7 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) h2({ class: 'block-diagram-title' }, 'Blockchain Blocks'), shown.length === 0 ? div(p(i18n.blockchainNoBlocks)) - : shown + : paged(shown .sort((a,b)=>{ const ta = a.type==='market'&&a.content.updatedAt ? new Date(a.content.updatedAt).getTime() @@ -519,16 +532,14 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) ? new Date(b.content.updatedAt).getTime() : b.ts; return tb - ta; - }) + })) .map(block=> div({ class:'block' }, div({ class:'block-buttons' }, - block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, '⦿'), - block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram || 'Datagram' }, '⊞'), + block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, renderIcon('block')), + block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, renderIcon('datagram')), !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ? - form({ method:'GET', action:getViewDetailsAction(block.type, block) }, - button({ type:'submit', class:'filter-btn' }, i18n.visitContent) - ) + a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, renderIcon('open')) : (block.isTombstoned || block.isReplaced) ? div({ class: 'deleted-label' }, i18n.blockchainContentDeleted || "This content has been deleted." diff --git a/src/views/blog_view.js b/src/views/blog_view.js new file mode 100644 index 0000000..d9bbfe1 --- /dev/null +++ b/src/views/blog_view.js @@ -0,0 +1,236 @@ +const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { TEXT_CAP } = require('../backend/long_text'); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions, contentDeleteAction, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledHtml } = require("../backend/renderStyledText"); +const { sanitizeHtml } = require("../backend/sanitizeHtml"); + +const userId = config.keys.id; + +const FILTERS = [ + { key: "ALL", i18n: "blogFilterAll" }, + { key: "RECENT", i18n: "blogFilterRecent" }, + { key: "MINE", i18n: "blogFilterMine" }, + { key: "FAVORITES", i18n: "blogFilterFavorites" }, + { key: "TOP", i18n: "blogFilterTop" } +]; + +const safeText = (v) => String(v == null ? "" : v).trim(); + +const renderBlogTags = (tags) => { + const list = (Array.isArray(tags) ? tags : []).map(t => String(t || "").trim().replace(/^#+/, "")).filter(Boolean); + return list.length + ? div({ class: "card-tags" }, list.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : null; +}; + +const excerpt = (text, max = 420) => { + const s = safeText(text); + return s.length > max ? s.slice(0, max).replace(/\s+$/, "") + "…" : s; +}; + +const renderFilterBar = (filter, q, showSearch = true, total = null, censusList = null) => { + const emptyMod = total !== null && moduleIsEmpty({ length: Number(total) || 0 }, filter, "ALL", q); + const censusB = Array.isArray(censusList) ? censusList : []; + const blogChip = (x) => { + const m = x.key; + if (m === filter || m === "ALL") return true; + if (m === "TOP") return censusB.length > 0; + if (m === "MINE") return censusB.some(b => String(b.author) === String(userId)); + if (m === "RECENT") return censusB.length > 0; + if (m === "FAVORITES") return censusB.some(b => b.isFavorite); + return true; + }; + return section( + div({ class: "filters" }, + form({ method: "GET", action: "/blogs", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : FILTERS.filter(blogChip).map(f => + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + )), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.blogCreateButton) + ) + ), + showSearch && !emptyMod + ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + total != null ? renderModuleStats(total) : null, + form({ method: "GET", action: "/blogs", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q || "", placeholder: i18n.blogSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + : null + ); +}; + +const renderBlogCard = (blog, filter, spreadInfo) => { + const href = `/blogs/${encodeURIComponent(blog.id)}`; + const isOwn = String(blog.author) === String(userId); + return div({ class: "trending-card blog-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + renderContentActions(blog.id, href, { spread: spreadInfo || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text, deleteAction: isOwn ? contentDeleteAction('blog', blog.id) : undefined }) + ), + div({ class: "card-section blog-card-body" }, + blog.subject + ? div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href }, blog.subject)) + ) + : null, + blog.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("blog", blog.subject || blog.text, blog.id))) : null, + div({ class: "blog-card-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(excerpt(blog.text))) }), + renderBlogTags(blog.tags), + p({ class: "card-footer" }, + span({ class: "date-link" }, `${moment(blog.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(blog.author) + ) + ) + ); +}; + +const renderCreateForm = (draft = null) => { + const subjectValue = draft ? safeText(draft.subject) : ""; + const textValue = draft ? String(draft.text || "") : ""; + const allowComments = draft ? draft.allowComments !== false : true; + const tagsValue = draft ? (Array.isArray(draft.tags) ? draft.tags.join(", ") : safeText(draft.tags)) : ""; + return section( + draft + ? section({ class: "post-preview blog-preview" }, + div({ class: "preview-content" }, + h2(i18n.messagePreview), + subjectValue ? h2({ class: "tribe-card-title" }, subjectValue) : null, + div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(textValue)) }), + renderBlogTags(draft.tags) + ) + ) + : null, + div({ class: "publish-form" }, + form({ action: "/blogs/create", method: "POST", enctype: "multipart/form-data" }, + label({ for: "subject" }, i18n.blogSubject), + br(), + input({ type: "text", name: "subject", id: "subject", maxlength: "150", placeholder: i18n.contentWarningPlaceholder, value: subjectValue }), + br(), + label({ for: "text" }, i18n.blogMessage), + br(), + textarea({ required: true, name: "text", id: "text", rows: "8", class: "publish-textarea", maxlength: String(TEXT_CAP), placeholder: i18n.publishWarningPlaceholder }, textValue), + br(), + label({ for: "blob" }, i18n.blogMedia), + br(), + input({ type: "file", id: "blob", name: "blob", multiple: true, accept: "image/*,video/*,audio/*,application/pdf" }), + br(), + br(), + label({ for: "tags" }, i18n.tagsLabel || "Tags"), + br(), + input({ type: "text", name: "tags", id: "tags", placeholder: i18n.tagsPlaceholder || "", value: tagsValue }), + br(), + div({ class: "blog-allow-comments" }, + input({ type: "hidden", name: "allowComments", value: "0" }), + label( + input({ type: "checkbox", name: "allowComments", value: "1", ...(allowComments ? { checked: true } : {}) }), + " ", + i18n.blogAllowComments + ) + ), + renderClearnetSelector(!!(draft && draft.clearnet === true), i18n), + br(), + button({ type: "submit", class: "filter-btn", formaction: "/blogs/preview", attrs: { formmethod: "POST" } }, i18n.preview), + " ", + button({ type: "submit", class: "create-button" }, i18n.blogPublish) + ) + ) + ); +}; + +const renderCommentsSection = (blog, comments = []) => { + const href = `/blogs/${encodeURIComponent(blog.id)}`; + return renderSharedCommentsSection({ + action: `${href}/comments`, + comments: comments, + returnTo: href, + closedNote: blog.allowComments ? null : i18n.blogCommentsClosed + }); +}; + +exports.blogView = async (blogs = [], filter = "ALL", params = {}) => { + const showForm = String(filter).toUpperCase() === "CREATE"; + const spreadMap = params.spreadMap instanceof Map ? params.spreadMap : new Map(); + return template( + i18n.blogTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.blogTitle), + p(i18n.blogDescription) + ) + ), + renderFilterBar(showForm ? "CREATE" : filter, params.q, !showForm, Array.isArray(blogs) ? blogs.length : 0, params.censusList), + showForm + ? renderCreateForm(params.draft || null) + : section( + blogs.length + ? div({ class: "blogs-grid" }, ...paged(blogs).map(b => renderBlogCard(b, filter, spreadMap.get(b.id)))) + : div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.blogNoItems)) + ) + ); +}; + +exports.singleBlogView = async (blog, comments = [], params = {}) => { + const href = `/blogs/${encodeURIComponent(blog.id)}`; + const isAuthor = String(blog.author) === String(userId); + const isClearnet = !!blog.clearnet; + + const blogSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(blog.id, href, { spread: params.spreads || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text, returnTo: href, deleteAction: isAuthor ? contentDeleteAction('blog', blog.id) : undefined }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, blog.subject || i18n.blogTitle), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("blog", blog.subject || blog.text, blog.id) : null) + ), + isAuthor ? renderClearnetSwitch("posts", blog.rootId || blog.id, isClearnet) : null, + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, moment(blog.createdAt).format("YYYY/MM/DD HH:mm")) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(blog.author)) + ) + ), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.blogComments}: ${blog.commentCount || 0}`) + ), + params.subscription + ? renderSubscriptionBox({ + target: blog.author, + scope: "blogs", + subscribed: params.subscription.subscribed === true, + count: params.subscription.count, + isOwner: isAuthor, + returnTo: href + }) + : null, + ); + + const blogMain = div({ class: "tribe-main" }, + div({ class: "job-section" }, + div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(blog.text || "")) }), + renderBlogTags(blog.tags) + ), + renderEngagement(blog.id, + renderOpinionsVoting('/blogs/opinions', blog.id, blog.opinions, href, blog.opinions_inhabitants), + renderCommentsSection(blog, comments) + ) + ); + + return template( + blog.subject || i18n.blogTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.blogTitle), p(i18n.blogDescription))), + renderFilterBar("ALL", params.q, false, null, params.censusList), + section(div({ class: "tribe-details" }, blogSide, blogMain)) + ); +}; diff --git a/src/views/bookmark_view.js b/src/views/bookmark_view.js index 24381ba..8b6d2eb 100644 --- a/src/views/bookmark_view.js +++ b/src/views/bookmark_view.js @@ -1,10 +1,12 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); const userId = config.keys.id; @@ -33,95 +35,12 @@ const renderPMButton = (recipient, className = "filter-btn") => { ); }; -const renderBookmarkActions = (filter, bookmark, params = {}) => { - const returnTo = buildReturnTo(filter, params); - const isAuthor = String(bookmark.author) === String(userId); - const hasOpinions = Object.keys(bookmark.opinions || {}).length > 0; - - return isAuthor - ? div( - { class: "bookmark-actions" }, - !hasOpinions - ? form( - { method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.bookmarkUpdateButton) - ) - : null, - form( - { method: "POST", action: `/bookmarks/delete/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.bookmarkDeleteButton) - ) - ) - : null; -}; - const renderBookmarkCommentsSection = (bookmarkId, rootId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/bookmarks/${encodeURIComponent(bookmarkId)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/bookmarks/${encodeURIComponent(bookmarkId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - rootId ? input({ type: "hidden", name: "rootId", value: rootId }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c?.value?.content || {}; - const text = content.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot - ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) - : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderCardField = (labelText, value) => @@ -131,22 +50,6 @@ const renderCardField = (labelText, value) => span({ class: "card-value" }, value) ); -const renderFavoriteToggle = (bookmark, returnTo) => - form( - { - method: "POST", - action: bookmark.isFavorite - ? `/bookmarks/favorites/remove/${encodeURIComponent(bookmark.id)}` - : `/bookmarks/favorites/add/${encodeURIComponent(bookmark.id)}`, - class: "bookmark-favorite-form" - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - bookmark.isFavorite ? i18n.bookmarkRemoveFavoriteButton : i18n.bookmarkAddFavoriteButton - ) - ); - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -157,7 +60,7 @@ const renderTags = (tags) => { : null; }; -const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { +const renderBookmarkList = exports.renderBookmarkList = (filteredBookmarks, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); return filteredBookmarks.length @@ -167,65 +70,44 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; const lastVisitTxt = lastVisit && lastVisit.isValid() - ? `${lastVisit.format("YYYY/MM/DD HH:mm:ss")} (${lastVisit.fromNow()})` + ? `${lastVisit.format("YYYY/MM/DD HH:mm")} (${lastVisit.fromNow()})` : i18n.noLastVisit; const urlLink = bookmark.url - ? a({ href: bookmark.url, target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, bookmark.url) + ? a({ href: safeExternalHref(bookmark.url), target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, bookmark.url) : i18n.noUrl; const isOwn = bookmark.author && String(bookmark.author) === String(userId); + const reachChip = bookmark.tribeOrigin ? renderTribeOriginChip(bookmark.tribeOrigin) : bookmark.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref("bookmarks", bookmark.title || bookmark.url, bookmark.id)) : null; + const extraChips = typeof params.titleChips === "function" ? params.titleChips(bookmark).filter(Boolean) : []; + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(bookmark) + : bookmark.tribeOrigin + ? renderContentActions(null, bookmark.tribeOrigin.href) + : renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`, { spread: (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads || null, author: bookmark.author, favKind: 'bookmarks', isFavorite: bookmark.isFavorite, reportTitle: bookmark.title, deleteAction: isOwn ? contentDeleteAction('bookmark', bookmark.id) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : bookmark.tribeOrigin ? bookmark.tribeOrigin.href : `/bookmarks/${encodeURIComponent(bookmark.id)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(bookmark) + : bookmark.tribeOrigin ? null : renderEngagement(bookmark.id, + renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), + renderCommentsLink({ href: `/bookmarks/${encodeURIComponent(bookmark.id)}`, count: commentCount }) + ); return div( { class: "trending-card bookmark-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`) + headerActions ), div( { class: "card-section bookmark-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderPMButton(bookmark.author), - renderFavoriteToggle(bookmark, returnTo) - ), - div( - { class: "bookmark-topbar-right" }, - renderBookmarkActions(filter, bookmark, params) - ) - ), - h2({ class: "bookmark-title" }, bookmark.category || bookmark.url || ""), - bookmark.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(bookmark.lifetime, i18n)) : null, - renderCardField(i18n.bookmarkUrlLabel + ":", urlLink), - renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), + h2({ class: "bookmark-title" }, titleHref ? a({ href: titleHref, class: "card-title-link" }, bookmark.title || bookmark.url || "") : (bookmark.url ? urlLink : (bookmark.title || ""))), + bookmark.lifetime || reachChip || extraChips.length ? div({ class: "card-chips-row" }, renderLifespanChip(bookmark.lifetime, i18n), reachChip, ...extraChips) : null, + titleHref ? (bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, urlLink) : null) : (bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null), + params.hideLastVisit ? null : renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(bookmark) : []), br, - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/bookmarks/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(bookmark.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(bookmark.id, (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads)), + engagement, (() => { const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; const updatedTs = bookmark.updatedAt ? new Date(bookmark.updatedAt).getTime() : NaN; @@ -233,12 +115,12 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(bookmark.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -262,6 +144,7 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { return div( { class: "div-center bookmark-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/bookmarks/update/${encodeURIComponent(bookmarkId)}` : "/bookmarks/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: returnTo }), @@ -294,16 +177,6 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { }), br(), br(), - label(i18n.bookmarkCategoryLabel), - br(), - input({ - type: "text", - name: "category", - id: "category", - placeholder: i18n.bookmarkCategoryPlaceholder, - value: filter === "edit" ? bookmarkToEdit.category || "" : "" - }), - br(), label(i18n.bookmarkTagsLabel), br(), input({ @@ -315,65 +188,64 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { }), br(), br(), + renderClearnetSelector(filter === "edit" ? !!bookmarkToEdit.clearnet : false, i18n), + br(), button({ type: "submit" }, filter === "edit" ? i18n.bookmarkUpdateButton : i18n.bookmarkCreateButton) ) ); }; +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.bookmarkMineSectionTitle - : filter === "create" - ? i18n.bookmarkCreateSectionTitle - : filter === "edit" - ? i18n.bookmarkUpdateSectionTitle - : filter === "recent" - ? i18n.bookmarkRecentSectionTitle - : filter === "top" - ? i18n.bookmarkTopSectionTitle - : filter === "favorites" - ? i18n.bookmarkFavoritesSectionTitle - : i18n.bookmarkAllSectionTitle; + const bookmarkEditWarning = filter === "edit" ? await renderSpreadEditWarning(bookmarkId) : null; + const title = i18n.bookmarkTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(bookmarks); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const bookmarkToEdit = bookmarkId ? list.find((b) => b.id === bookmarkId) : null; const tags = bookmarkToEdit && Array.isArray(bookmarkToEdit.tags) ? bookmarkToEdit.tags : []; return template( title, section( - div({ class: "tags-header" }, h2(title), p(i18n.bookmarkDescription)), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetBookmarks); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.bookmarkDescription)), div( { class: "filters" }, form( { method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterTop), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterFavorites), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterRecent), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterFavorites).toUpperCase())] : []), + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterTop).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton) ) ) ), section( filter === "edit" || filter === "create" - ? renderBookmarkForm(filter, bookmarkId, bookmarkToEdit || {}, tags, { ...params, filter }) + ? renderBookmarkForm(filter, bookmarkId, bookmarkToEdit || {}, tags, { ...params, filter, spreadWarning: bookmarkEditWarning }) : section( - div( - { class: "bookmarks-search" }, + emptyMod ? null : div( + { class: "bookmarks-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/bookmarks", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -382,38 +254,38 @@ exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, para { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.bookmarkSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.bookmarkSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.bookmarkSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.bookmarkSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.bookmarkSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.bookmarkSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.bookmarkSearchButton) ) ) ), - div({ class: "bookmark-list" }, renderBookmarkList(list, filter, { q, sort })) + div({ class: "bookmark-list" }, renderBookmarkList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); }; exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = params.returnTo || buildReturnTo(filter, { q, sort }); const isAuthor = String(bookmark.author) === String(userId); const hasOpinions = Object.keys(bookmark.opinions || {}).length > 0; - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetBookmarks); + const isClearnet = !!bookmark.clearnet; const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; const lastVisitTxt = lastVisit && lastVisit.isValid() - ? `${lastVisit.format("YYYY/MM/DD HH:mm:ss")} (${lastVisit.fromNow()})` + ? `${lastVisit.format("YYYY/MM/DD HH:mm")} (${lastVisit.fromNow()})` : i18n.noLastVisit; const urlLink = bookmark.url - ? a({ href: bookmark.url, target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, bookmark.url) + ? a({ href: safeExternalHref(bookmark.url), target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, bookmark.url) : i18n.noUrl; const chips = [ @@ -422,10 +294,7 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par ].filter(Boolean); const sideActions = []; - sideActions.push(renderFavoriteToggle(bookmark, returnTo)); - if (bookmark.author && String(bookmark.author) !== String(userId)) { - sideActions.push(renderPMButton(bookmark.author)); - } + sideActions.push(renderPMButton(bookmark.author)); if (isAuthor && !hasOpinions) { sideActions.push(form( { method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` }, @@ -433,35 +302,38 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par button({ class: "update-btn", type: "submit" }, i18n.bookmarkUpdateButton) )); } - if (isAuthor) { - sideActions.push(form( - { method: "POST", action: `/bookmarks/delete/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.bookmarkDeleteButton) - )); - } const tagsNode = renderTags(bookmark.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`, { + author: bookmark.author, + favKind: 'bookmarks', + isFavorite: bookmark.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: bookmark.title, + deleteAction: isAuthor ? contentDeleteAction('bookmark', bookmark.id) : undefined + }) + ); + const bookmarkSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, bookmark.category || bookmark.url || ""), - renderReachChip(isClearnet, i18n) + h2({ class: "tribe-card-title" }, bookmark.url ? urlLink : (bookmark.title || "")), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("bookmarks", bookmark.title || bookmark.url, bookmark.id) : null) ), + isAuthor ? renderClearnetSwitch("bookmarks", bookmark.rootId || bookmark.id, isClearnet) : null, + bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null, chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(bookmark.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(bookmark.description)) - : null, - safeText(bookmark.category) - ? renderCardField(i18n.bookmarkCategoryLabel + ":", safeText(bookmark.category)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(bookmark.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(bookmark.id, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const bookmarkMain = div({ class: "tribe-main" }, - renderCardField(i18n.bookmarkUrlLabel + ":", urlLink), + detailActions, renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), (() => { const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; @@ -469,24 +341,26 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(bookmark.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), - renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) + renderEngagement(bookmark.id, + renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), + renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) + ) ); return template( i18n.bookmarkTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.bookmarkAllSectionTitle || i18n.bookmarkTitle), p(i18n.bookmarkDescription) ), @@ -494,13 +368,12 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par { class: "filters" }, form( { method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterTop), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterFavorites), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterRecent), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterFavorites).toUpperCase())] : []), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton) ) ), diff --git a/src/views/calendars_view.js b/src/views/calendars_view.js index 2ae1127..f1990c4 100644 --- a/src/views/calendars_view.js +++ b/src/views/calendars_view.js @@ -1,41 +1,31 @@ -const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views") -const { renderEncryptedChip } = require("./clearnet_view") +const { div, h2, h3, h4, p, section, button, form, a, span, br, hr, textarea, input, label, select, option, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") +const { renderStyledText } = require("../backend/renderStyledText") +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views") +const { renderMapEmbed } = require("./maps_view") +const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const userId = config.keys.id +const CAL_REACH = ["OPEN", "CLOSED"] +const calReachLabel = (s) => s === "CLOSED" ? (i18n.calendarStatusClosed || "CLOSED") : (i18n.calendarStatusOpen || "OPEN") +const hasPublicInvite = (cal) => Array.isArray(cal.invites) && cal.invites.some(i => i && typeof i === "object" && i.public === true) +const clearnetEligible = (cal) => !!cal && !cal.tribeId && !cal.encrypted && String(cal.status || "").toUpperCase() === "OPEN" && !cal.isClosed && (!cal.contentEncrypted || hasPublicInvite(cal)) +const calClearnetHref = (cal) => clearnetItemHref("calendars", cal.title, cal.rootId) -const renderNoteText = (text) => { - if (!text) return [] - const urlRegex = /(https?:\/\/[^\s]+)/g - const result = [] - let last = 0 - text.replace(urlRegex, (match, _g, offset) => { - if (offset > last) result.push(text.slice(last, offset)) - result.push(a({ href: match, target: "_blank" }, match)) - last = offset + match.length - }) - if (last < text.length) result.push(text.slice(last)) - return result -} +const renderNoteText = (text) => renderStyledText(String(text || "")) -const renderCalendarFavoriteToggle = (cal, returnTo) => - form( - { method: "POST", action: cal.isFavorite ? `/calendars/favorites/remove/${encodeURIComponent(cal.rootId)}` : `/calendars/favorites/add/${encodeURIComponent(cal.rootId)}` }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "tribe-action-btn" }, cal.isFavorite ? (i18n.calendarRemoveFavorite || "Remove Favorite") : (i18n.calendarAddFavorite || "Add Favorite")) - ) - -const renderModeButtons = (currentFilter) => +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => div({ class: "tribe-mode-buttons" }, - ["all", "mine", "recent", "favorites", "open", "closed"].map(f => + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "favorites", "open", "closed"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => form({ method: "GET", action: "/calendars" }, input({ type: "hidden", name: "filter", value: f }), button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`calendarFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) ) ), + ]), form({ method: "GET", action: "/calendars" }, input({ type: "hidden", name: "filter", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.calendarCreate || "Create Calendar") @@ -55,104 +45,138 @@ const renderCalendarStatusChip = (cal) => { return renderStateChip(variant, icon, label) } -const renderCalendarCard = (cal, spreadInfo) => { - const href = `/calendars/${encodeURIComponent(cal.rootId)}` +const renderCalendarCard = exports.renderCalendarCard = (cal, spreadInfo) => { + const origin = cal.tribeOrigin || null + const href = origin ? origin.href : `/calendars/${encodeURIComponent(cal.rootId)}` const chips = [ renderCalendarStatusChip(cal), - renderEncryptedChip(i18n), - renderLifespanChip(cal.lifetime, i18n) + origin ? renderTribeOriginChip(origin) : renderEncryptedChip(i18n), + renderLifespanChip(cal.lifetime, i18n), + !origin && cal.clearnet === true && clearnetEligible(cal) ? renderReachChip(true, i18n, calClearnetHref(cal)) : null, + cal.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (cal.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ].filter(Boolean) return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(cal.rootId, href, origin + ? { author: cal.author, reportTitle: cal.title } + : { spread: spreadInfo || null, author: cal.author, favKind: 'calendars', isFavorite: cal.isFavorite, reportTitle: cal.title, deleteAction: String(cal.author) === String(userId) ? contentDeleteAction('calendar', cal.rootId) : undefined }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, cal.title || "\u2014")) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, cal.deadline - ? p({ class: "job-meta-line" }, `${i18n.calendarDeadlineLabel || "Deadline"}: ${moment(cal.deadline).format("YYYY-MM-DD HH:mm")}`) + ? div({ class: "card-field card-field-mb" }, + span({ class: "card-label" }, `${String(i18n.calendarDeadlineLabel).toUpperCase()}: `), + span({ class: "card-value" }, moment(cal.deadline).format("YYYY/MM/DD HH:mm")) + ) : null, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${cal.participants.length}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(cal.rootId, spreadInfo)), - div({ class: "card-visit-btn-centered" }, - a({ href, class: "filter-btn" }, i18n.calendarVisitCalendar || "Visit Calendar") - ) + origin && Array.isArray(cal.dates) && cal.dates.length + ? div(...cal.dates.map(d => p({ class: "job-meta-line" }, `📅 ${moment(d.date).format("YYYY/MM/DD HH:mm")}${d.label ? ` · ${d.label}` : ""}`))) + : null, + origin + ? null + : div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${cal.participants.length}`) + ) ) ) } -const renderIntervalBlock = () => - div({ class: "calendar-interval-block" }, +const renderIntervalBlock = (bounds = {}, current = "", until = "") => { + const sel = (v) => (String(current || "") === v ? { selected: true } : {}) + return div({ class: "calendar-interval-block" }, span({ class: "calendar-interval-label" }, i18n.calendarIntervalLabel || "Interval"), - div({ class: "calendar-interval-row" }, - input({ type: "hidden", name: "intervalWeekly", value: "0" }), - label({ class: "calendar-interval-option" }, - input({ type: "checkbox", name: "intervalWeekly", value: "1" }), - " ", i18n.calendarIntervalWeekly || "Weekly" - ), - input({ type: "hidden", name: "intervalMonthly", value: "0" }), - label({ class: "calendar-interval-option" }, - input({ type: "checkbox", name: "intervalMonthly", value: "1" }), - " ", i18n.calendarIntervalMonthly || "Monthly" - ), - input({ type: "hidden", name: "intervalYearly", value: "0" }), - label({ class: "calendar-interval-option" }, - input({ type: "checkbox", name: "intervalYearly", value: "1" }), - " ", i18n.calendarIntervalYearly || "Yearly" - ) + select({ name: "interval", class: "calendar-interval-select" }, + option({ value: "", ...sel("") }, i18n.calendarIntervalNone), + option({ value: "weekly", ...sel("weekly") }, i18n.calendarIntervalWeekly || "Weekly"), + option({ value: "monthly", ...sel("monthly") }, i18n.calendarIntervalMonthly || "Monthly"), + option({ value: "yearly", ...sel("yearly") }, i18n.calendarIntervalYearly || "Yearly") ), span({ class: "calendar-interval-label calendar-interval-until" }, i18n.calendarIntervalUntil || "Until"), - input({ type: "datetime-local", name: "intervalDeadline" }), + input({ + type: "datetime-local", + name: "intervalDeadline", + ...(until ? { value: until } : {}), + ...(bounds.min ? { min: bounds.min } : {}), + ...(bounds.max ? { max: bounds.max } : {}) + }), br() ) +} const renderCreateForm = (calendarToEdit, params) => { const isEdit = !!calendarToEdit const tribeId = (params && params.tribeId) || "" + const draft = isEdit ? calendarToEdit : ((params && params.draft) || {}) + const reachRaw = String((params && params.reach) || draft.status || "").toUpperCase() + const reach = CAL_REACH.includes(reachRaw) ? reachRaw : "OPEN" + const showClearnet = reach === "OPEN" && !tribeId && clearnetEligible({ ...(isEdit ? calendarToEdit : {}), status: reach, isClosed: false }) + const asLocal = (v) => v ? moment(v).format("YYYY-MM-DDTHH:mm") : "" const now = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm") + const deadlineMax = calendarToEdit && calendarToEdit.deadline + ? moment(calendarToEdit.deadline).format("YYYY-MM-DDTHH:mm") + : "" const action = isEdit ? `/calendars/update/${encodeURIComponent(calendarToEdit.rootId)}` : "/calendars/create" const sectionTitle = isEdit ? (i18n.calendarUpdateSectionTitle || "Update Calendar") : (i18n.calendarCreateSectionTitle || "Create New Calendar") return div({ class: "div-center audio-form" }, h2(sectionTitle), + (params && params.spreadWarning) || null, + form({ method: "GET", action: "/calendars" }, + input({ type: "hidden", name: "filter", value: isEdit ? "edit" : "create" }), + isEdit ? input({ type: "hidden", name: "id", value: calendarToEdit.rootId }) : null, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + label(i18n.calendarTypeLabel), br(), + div({ class: "apply-row" }, + select({ name: "status", class: "report-category-select" }, + ...CAL_REACH.map(s => option({ value: s, ...(reach === s ? { selected: true } : {}) }, calReachLabel(s))) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, calReachLabel(reach)), form({ method: "POST", action }, tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + input({ type: "hidden", name: "status", value: reach }), span(i18n.calendarTitleLabel || "Title"), br(), - input({ type: "text", name: "title", required: true, placeholder: i18n.calendarTitlePlaceholder || "Calendar title...", value: calendarToEdit ? calendarToEdit.title : "" }), - br(), br(), - span(i18n.calendarStatusLabel || "Status"), br(), - select({ name: "status", required: true }, - option({ value: "OPEN", ...((!calendarToEdit || calendarToEdit.status === "OPEN") ? { selected: true } : {}) }, i18n.calendarStatusOpen || "OPEN"), - option({ value: "CLOSED", ...((calendarToEdit && calendarToEdit.status === "CLOSED") ? { selected: true } : {}) }, i18n.calendarStatusClosed || "CLOSED") - ), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.calendarTitlePlaceholder || "Calendar title...", value: draft.title || "" }), br(), br(), span(i18n.calendarDeadlineLabel || "Deadline"), br(), - input({ type: "datetime-local", name: "deadline", required: true, min: now, value: calendarToEdit && calendarToEdit.deadline ? moment(calendarToEdit.deadline).format("YYYY-MM-DDTHH:mm") : "" }), + input({ type: "datetime-local", name: "deadline", required: true, min: now, value: asLocal(draft.deadline) }), br(), br(), span(i18n.calendarTagsLabel || "Tags"), br(), - input({ type: "text", name: "tags", placeholder: i18n.calendarTagsPlaceholder || "tag1, tag2...", value: calendarToEdit && Array.isArray(calendarToEdit.tags) ? calendarToEdit.tags.join(", ") : "" }), + input({ type: "text", name: "tags", placeholder: i18n.calendarTagsPlaceholder || "Enter tags separated by commas", value: Array.isArray(draft.tags) ? draft.tags.join(", ") : "" }), + br(), br(), + span(i18n.mapLocationTitle || "Map Location"), br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: draft.mapUrl || "" }), br(), br(), !isEdit ? [ span(i18n.calendarFirstDateLabel || "Date"), br(), - input({ type: "datetime-local", name: "firstDate", required: true, min: now }), + input({ type: "datetime-local", name: "firstDate", required: true, min: now, max: deadlineMax || undefined, ...(draft.firstDate ? { value: asLocal(draft.firstDate) } : {}) }), br(), br(), span(i18n.calendarFormDescription || "Description"), br(), - input({ type: "text", name: "firstDateLabel", placeholder: i18n.calendarDatePlaceholder || "Describe this date..." }), + input({ type: "text", name: "firstDateLabel", placeholder: i18n.calendarDatePlaceholder || "Describe this date...", value: draft.firstDateLabel || "" }), br(), br(), - renderIntervalBlock(), span(i18n.calendarFirstNoteLabel || "Notes"), br(), - textarea({ name: "firstNote", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }), + textarea({ maxlength: "5000", name: "firstNote", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }, draft.firstNote || ""), + br(), br(), + renderIntervalBlock({ min: now, max: deadlineMax || undefined }, draft.interval || "", asLocal(draft.intervalDeadline)), br(), br() ] : null, + showClearnet ? renderClearnetSelector(draft.clearnet === true || String(draft.clearnet || "") === "1", i18n) : null, button({ type: "submit", class: "create-button" }, isEdit ? (i18n.calendarUpdate || "Update") : (i18n.calendarCreate || "Create Calendar")) ) ) } const renderMonthGrid = (year, month, datesMap, calendarId) => { - const DAY_NAMES = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"] + const DAY_NAMES = String(i18n.calendarDayInitials || "M,T,W,T,F,S,S").split(",") const firstDay = new Date(year, month, 1) const daysInMonth = new Date(year, month + 1, 0).getDate() const startPad = (firstDay.getDay() + 6) % 7 @@ -192,41 +216,39 @@ exports.renderCalendarInvitePage = (code) => { } exports.calendarsView = async (calendars, filter, calendarToEdit, params) => { + if (calendarToEdit) params = { ...(params || {}), spreadWarning: await renderSpreadEditWarning(calendarToEdit.id || calendarToEdit.key || calendarToEdit.rootId) }; const q = (params && params.q) || "" const showForm = filter === "create" || filter === "edit" || !!calendarToEdit - const headerMap = { - all: i18n.calendarAllSectionTitle || "Calendars", - mine: i18n.calendarMineSectionTitle || "Your Calendars", - recent: i18n.calendarRecentSectionTitle || "Recent Calendars", - favorites: i18n.calendarFavoritesSectionTitle || "Favorites", - open: i18n.calendarOpenSectionTitle || "Open Calendars", - closed: i18n.calendarClosedSectionTitle || "Closed Calendars" - } - const headerText = headerMap[filter] || headerMap.all + const emptyMod = moduleIsEmpty(Array.isArray(calendars) ? calendars : [], filter || "all", "all", q) + const headerText = i18n.calendarsTitle || "Calendars" return template( i18n.calendarsTitle || "Calendars", section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(headerText), p(i18n.calendarsDescription || "Discover and manage calendars.") ), - renderModeButtons(filter), - q - ? div({ class: "filters" }, - form({ method: "GET", action: "/calendars" }, - input({ type: "text", name: "q", value: q, placeholder: i18n.calendarSearchPlaceholder || "Search calendars..." }), - button({ type: "submit", class: "filter-btn" }, i18n.searchButton || "Search") + renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null), + showForm || emptyMod + ? null + : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(calendars, c => c.isClosed ? 'CLOSED' : String(c.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.calendarStatusOpen }, { value: 'CLOSED', label: i18n.calendarStatusClosed }]), + form({ method: "GET", action: "/calendars", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.calendarSearchPlaceholder || "Search calendars...", class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) ) ) - : null ), section( showForm ? renderCreateForm(calendarToEdit, params) : (calendars.length > 0 - ? div({ class: "tribe-grid" }, ...calendars.map(c => renderCalendarCard(c, params && params.spreadMap && params.spreadMap.get(c.rootId)))) - : p({ class: "no-content" }, i18n.calendarsNoItems || "No calendars found.")) + ? div({ class: "tribe-grid" }, ...paged(calendars).map(c => renderCalendarCard(c, params && params.spreadMap && params.spreadMap.get(c.rootId)))) + : div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.calendarsNoItems || "No calendars found."))) ) ) } @@ -253,39 +275,39 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { } const tags = Array.isArray(calendar.tags) && calendar.tags.length > 0 - ? div({ class: "tribe-side-tags" }, ...calendar.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}` }, `#${t} `))) + ? div({ class: "tribe-side-tags" }, ...calendar.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) : null + const subscriptionIn = isAuthor || (calendar.subscription && calendar.subscription.subscribed === true) + const canClearnet = clearnetEligible(calendar) + const isClearnet = canClearnet && calendar.clearnet === true const detailChips = [ renderCalendarStatusChip(calendar), renderEncryptedChip(i18n), - renderLifespanChip(calendar.lifetime, i18n) + renderLifespanChip(calendar.lifetime, i18n), + canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? calClearnetHref(calendar) : null) : null, + isAuthor && canClearnet ? renderClearnetSwitch("calendars", calendar.rootId, isClearnet) : null, + (isAuthor || isParticipant) + ? renderStateChip(subscriptionIn ? "mutuals" : "closed", "✉", subscriptionIn ? i18n.subscriptionOn : i18n.subscriptionOff) + : null ].filter(Boolean) const calSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, calendar.title || "\u2014") ), detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(calendar.rootId, params && params.spreads)), - div({ class: "shop-share" }, - span({ class: "tribe-info-label" }, i18n.calendarsShareUrl || "Share URL"), - input({ type: "text", readonly: true, value: shareUrl, class: "shop-share-input" }) - ), table({ class: "tribe-info-table" }, - tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY-MM-DD"))), + tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY/MM/DD HH:mm"))), + calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY/MM/DD HH:mm"))) : null, tr(td({ class: "tribe-info-label" }, i18n.calendarStatusLabel || "Status"), td({ class: "tribe-info-value", colspan: "3" }, renderStatus(calendar))), - tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author))), - calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY-MM-DD HH:mm"))) : null + tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author))) ), - div({ class: "tribe-side-actions" }, - renderCalendarFavoriteToggle(calendar, shareUrl), - isAuthor - ? form({ method: "GET", action: "/calendars" }, - input({ type: "hidden", name: "filter", value: "edit" }), - input({ type: "hidden", name: "id", value: calendar.rootId }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.calendarUpdate || "Update") - ) - : null, + tags, + renderMapEmbed(params && params.mapData, calendar.mapUrl), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${calendar.participants.length}`) + ), + div({ class: "tribe-side-actions calendar-invite-actions" }, isAuthor && calendar.status !== "OPEN" ? form({ method: "POST", action: `/calendars/generate-invite/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) @@ -295,34 +317,27 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { if (!(isAuthor && !calendar.tribeId)) return null const invs = Array.isArray(calendar.invites) ? calendar.invites : [] const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) - if (openInvite) return [ + if (openInvite) return div({ class: "calendar-open-invite-block" }, div({ class: "tribe-open-invite" }, span({ class: "card-label" }, i18n.tribeInviteCodeText), - span({ class: "tribe-open-invite-code" }, openInvite.code) + span({ class: "tribe-open-invite-code" }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/calendars/${encodeURIComponent(openInvite.code)}` }) ), form({ method: "POST", action: `/calendars/open-invite/remove/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) ) - ] + ) return form({ method: "POST", action: `/calendars/open-invite/create/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) ) })(), - isAuthor - ? form({ method: "POST", action: `/calendars/delete/${encodeURIComponent(calendar.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.calendarDelete || "Delete") - ) - : null, - !isAuthor - ? a({ href: `/pm?to=${encodeURIComponent(calendar.author)}`, class: "tribe-action-btn" }, "PM") - : null, !isAuthor && !isParticipant && calendar.status === "OPEN" ? form({ method: "POST", action: `/calendars/join/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "create-button" }, i18n.calendarJoin || "Join Calendar") ) : null, !isAuthor && !isParticipant && calendar.status !== "OPEN" - ? a({ class: "tribe-action-btn", href: "/invites#invites-calendars" }, i18n.tribeEnterInvite) + ? a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite) : null, !isAuthor && isParticipant ? form({ method: "POST", action: `/calendars/leave/${encodeURIComponent(calendar.rootId)}` }, @@ -330,44 +345,61 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { ) : null ), - tags, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${calendar.participants.length}`) - ) + (calendar.subscription && (isAuthor || isParticipant)) + ? renderSubscriptionBox({ + target: calendar.rootId || calendar.key, + scope: "calendars", + subscribed: calendar.subscription.subscribed === true, + count: calendar.subscription.count, + isOwner: isAuthor, + returnTo: shareUrl + }) + : null, + isAuthor + ? div({ class: "tribe-side-actions calendar-owner-actions" }, + form({ method: "GET", action: "/calendars" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: calendar.rootId }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.calendarUpdate || "Update") + ) + ) + : null, + renderDocumentActions('calendars', calendar.rootId) ) const minDate = now.add(1, "minute").format("YYYY-MM-DDTHH:mm") + const calMax = calendar.deadline ? moment(calendar.deadline).format("YYYY-MM-DDTHH:mm") : "" const canAddDate = !calClosed && (calendar.status === "OPEN" || isAuthor) const unifiedForm = canAddDate ? div({ class: "div-center audio-form" }, - h4(i18n.calendarAddEntry || "Add Entry"), form({ method: "POST", action: `/calendars/add-date/${encodeURIComponent(calendar.rootId)}` }, span(i18n.calendarDateLabel || "Date"), br(), - input({ type: "datetime-local", name: "date", required: true, min: minDate }), + input({ type: "datetime-local", name: "date", required: true, min: minDate, max: calMax || undefined }), br(), br(), span(i18n.calendarFormDescription || "Description"), br(), input({ type: "text", name: "label", placeholder: i18n.calendarDatePlaceholder || "Describe this date..." }), br(), br(), - renderIntervalBlock(), - br(), isParticipant ? [ - span(i18n.calendarNoteLabel || "Note (optional)"), br(), - textarea({ name: "text", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }), + span(i18n.calendarNoteLabel), br(), + textarea({ maxlength: "5000", name: "text", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }), br(), br() ] : null, + renderIntervalBlock({ min: minDate, max: calMax || undefined }), + br(), button({ type: "submit", class: "create-button" }, i18n.calendarAddEntry || "Add Entry") ) ) : null - const monthLabel = currentMonth.format("MMMM YYYY") + const monthNames = String(i18n.calendarMonthNames || "").split(",") + const monthLabel = `${monthNames[currentMonth.month()] || currentMonth.format("MMMM")} ${currentMonth.year()}` const calNav = div({ class: "calendar-nav" }, - a({ href: `${shareUrl}?month=${prevMonth}`, class: "filter-btn" }, i18n.calendarMonthPrev || "\u2190 Prev"), + a({ href: `${shareUrl}?month=${prevMonth}`, class: "filter-btn", title: i18n.calendarMonthPrev || "Prev" }, "\u2190"), span({ class: "tribe-info-label" }, monthLabel), - a({ href: `${shareUrl}?month=${nextMonth}`, class: "filter-btn" }, i18n.calendarMonthNext || "Next \u2192") + a({ href: `${shareUrl}?month=${nextMonth}`, class: "filter-btn", title: i18n.calendarMonthNext || "Next" }, "\u2192") ) const grid = renderMonthGrid(year, month, datesMap, calendar.rootId) @@ -391,7 +423,7 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { ) : null, div({ class: "calendar-date-item-header" }, - `${moment(d.date).format("YYYY-MM-DD HH:mm")}${d.label ? " \u2014 " + d.label : ""}` + `${moment(d.date).format("YYYY/MM/DD HH:mm")}${d.label ? " \u2014 " + d.label : ""}` ), (() => { const visibleNotes = notes.filter(n => n.text && String(n.text).trim()) @@ -424,6 +456,18 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { : null const calMain = div({ class: "tribe-main" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(calendar.rootId, shareUrl, { + author: calendar.author, + favKind: 'calendars', + isFavorite: calendar.isFavorite, + spread: (params && params.spreads) || null, + returnTo: shareUrl, + reportTitle: calendar.title, + deleteAction: isAuthor ? contentDeleteAction('calendar', calendar.rootId) : undefined + }) + ), + calNav, grid, dayNotesSection, @@ -433,12 +477,65 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { return template( calendar.title || i18n.calendarsTitle || "Calendar", section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.calendarsTitle || "Calendars"), p(i18n.calendarsDescription || "Discover and manage calendars.") ), - renderModeButtons("all") + renderModeButtons("all", false, (params && params.modesAvail) || null) ), - section(div({ class: "tribe-details" }, calSide, calMain)) + section(div({ class: "tribe-details calendar-details" }, calSide, calMain)) ) } + +exports.clearnetCalendarView = async (calendar, dates, notesByDate = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, renderClearnetPage } = require("./clearnet_view") + const name = calendar.title || i18n.calendarTitle + const fmt = (v) => esc(moment(v).format("YYYY/MM/DD HH:mm")) + const entries = [] + const byKey = new Map() + for (const d of Array.isArray(dates) ? dates : []) { + if (!byKey.has(d.key)) { + const entry = { key: d.key, label: d.label || "", when: [] } + byKey.set(d.key, entry) + entries.push(entry) + } + byKey.get(d.key).when.push(d.date) + } + const entriesHtml = entries.map(e => { + const notes = (notesByDate[e.key] || []).filter(n => n && String(n.text || "").trim()) + return `
+ ${e.label ? `` : ""} +
${e.when.map(w => `📅 ${fmt(w)}`).join("")}
+ ${notes.map(n => `

${renderRichText(n.text)}

`).join("")} +
` + }).join("") + const extraCss = ` +.cn-cal-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word} +.cn-cal-meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:16px} +.cn-cal-entry{border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin:12px 0;background:var(--bg-elev)} +.cn-cal-entry-label{color:var(--fg);font-weight:600;font-size:16px;margin-bottom:8px;word-break:break-word} +.cn-cal-dates{display:flex;flex-wrap:wrap;gap:6px} +.cn-cal-note{color:var(--fg-soft);line-height:1.5;margin:10px 0 0 0;word-break:break-word} +.cn-cal-empty{color:var(--fg-dim)} +` + const body = ` +

${esc(name)}

+
+ ${renderKindTag("calendar")} + ${calendar.deadline ? `⏳ ${esc(i18n.calendarDeadlineLabel || "Deadline")}: ${fmt(calendar.deadline)}` : ""} +
+ ${renderTagChips(calendar.tags)} +
+ ${entriesHtml || `

${esc(i18n.calendarNoDates || "No dates added yet.")}

`} +` + return renderClearnetPage({ + title: `${name} | Oasis`, + ogTitle: name, + ogDescription: "", + extraCss, + body, + hubFeedId: calendar.author || null + }) +} + +exports.renderIntervalBlock = renderIntervalBlock diff --git a/src/views/campaigns_view.js b/src/views/campaigns_view.js new file mode 100644 index 0000000..d3758fb --- /dev/null +++ b/src/views/campaigns_view.js @@ -0,0 +1,335 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, progress, table, tr, td, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); +const { renderCommentsSection } = require("./comments_view"); +const { renderMapEmbed } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const sharedState = require("../configs/shared-state"); + +const userId = config.keys.id; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "SIGNED", "TOP"]; +const STATUSES = ["OPEN", "ACHIEVED", "CLOSED"]; +const CATEGORIES = ["ENVIRONMENT", "RIGHTS", "HEALTH", "EDUCATION", "INFRASTRUCTURE", "CULTURE", "ECONOMY", "OTHER"]; + +const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); +const filterLabel = (f) => String(i18n[`campaignFilter${cap(f)}`] || f).toUpperCase(); +const statusLabel = (s) => String(i18n[`campaignStatus${cap(s)}`] || s).toUpperCase(); +const catLabel = (c) => String(i18n[`campaignCategory${cap(c)}`] || c).toUpperCase(); +const campaignHref = (cp) => `/campaigns/${encodeURIComponent(cp.id)}`; +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); + +const statusChip = (cp) => renderStateChip(cp.closed ? "closed" : "mutuals", cp.closed ? "✗" : "✓", statusLabel(cp.closed ? "CLOSED" : "OPEN")); +const outcomeChip = (cp) => cp.closed ? renderStateChip(cp.achieved ? "mutuals" : "closed", cp.achieved ? "✓" : "✗", cp.achieved ? statusLabel("ACHIEVED") : String(i18n.campaignStatusUnachieved || "UNACHIEVED").toUpperCase()) : null; +const categoryChip = (cp) => renderStateChip("neutral", "", catLabel(cp.category)); +const clearnetEligible = (cp) => String(cp.status || "OPEN").toUpperCase() !== "CLOSED" && !cp.tribeId && !cp.encrypted; + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const renderProgress = (cp) => + div({ class: "tribe-card-members campaign-progress" }, + span({ class: "tribe-members-count" }, `${i18n.campaignSignaturesLabel}: ${cp.signatureCount} / ${cp.goal} (${cp.progress}%)`), + progress({ class: "confirmations-progress", value: String(cp.signatureCount), max: String(cp.goal || 1) }) + ); + +const renderCover = (cp) => { + if (!cp.media || !cp.media.blobId) return null; + if (cp.media.kind === "video") return videoHyperaxe({ class: "campaign-card-cover", src: `/blob/${encodeURIComponent(cp.media.blobId)}`, controls: true, preload: "metadata" }); + return a({ href: campaignHref(cp) }, img({ loading: 'lazy', class: "campaign-card-cover", src: `/blob/${encodeURIComponent(cp.media.blobId)}`, alt: cp.title || "" })); +}; + +const campaignChipFor = (mode, census) => { + if (mode === "ALL") return true; + if (!Array.isArray(census)) return true; + if (mode === "MINE") return census.some(c => String(c.author) === String(userId)); + if (mode === "SIGNED") return census.some(c => Array.isArray(c.signers) && c.signers.includes(userId)); + if (mode === "RECENT") return census.length > 0; + if (mode === "TOP") return census.some(c => c.signatureCount > 0); + if (STATUSES.includes(mode)) return census.some(c => c.status === mode); + if (CATEGORIES.includes(mode)) return census.some(c => c.category === mode); + return census.length > 0; +}; +exports.campaignChipFor = campaignChipFor; + +const renderCampaignCard = (cp, params = {}) => + div({ class: `tribe-card campaign-card campaign-${cp.status.toLowerCase()}` }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(cp.id, campaignHref(cp), { author: cp.author, favKind: "campaigns", isFavorite: cp.isFavorite, reportTitle: cp.title, spread: (params.spreadMap && params.spreadMap.get(cp.id)) || null, deleteAction: cp.isOwner ? contentDeleteAction("campaign", cp.id) : undefined }) + ), + div({ class: "tribe-card-body" }, + renderCover(cp), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: campaignHref(cp) }, cp.title || "—"))), + div({ class: "card-chips-row" }, statusChip(cp), outcomeChip(cp), categoryChip(cp), cp.clearnet === true && clearnetEligible(cp) ? renderReachChip(true, i18n, clearnetItemHref("campaigns", cp.title, cp.id)) : null), + renderProgress(cp), + cp.deadline ? p({ class: "job-meta-line" }, `${i18n.campaignDeadlineLabel}: ${fmt(cp.deadline)}`) : null + ) + ); + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => m === filter || campaignChipFor(m, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const renderFilters = (filter, q, census, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/campaigns", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, filterLabel), + ...chipButtons(STATUSES, filter, census, statusLabel), + ...chipButtons(CATEGORIES, filter, census, catLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.campaignCreate) + ) + ) + ); + +const renderForm = (cp) => + div({ class: "report-form" }, + h2(cp ? i18n.campaignUpdateSectionTitle : i18n.campaignCreateSectionTitle), + form({ method: "POST", action: cp ? `/campaigns/update/${encodeURIComponent(cp.id)}` : "/campaigns/create", enctype: "multipart/form-data" }, + label(i18n.campaignTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.campaignTitlePlaceholder, value: cp ? cp.title : "" }), br(), + label(i18n.campaignTextLabel), br(), + textarea({ name: "text", rows: 10, maxlength: "8000", placeholder: i18n.campaignTextPlaceholder }, cp ? cp.text : ""), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + label(i18n.campaignCategoryLabel), br(), + select({ name: "category" }, ...CATEGORIES.map(c => option({ value: c, ...((cp ? cp.category : "OTHER") === c ? { selected: true } : {}) }, catLabel(c)))), br(), br(), + label(i18n.campaignGoalLabel), br(), + input({ type: "number", name: "goal", min: "1", step: "1", required: true, placeholder: String((sharedState.getInhabitantCount && sharedState.getInhabitantCount()) || 100), value: cp ? String(cp.goal) : "" }), br(), br(), + label(i18n.campaignDeadlineLabel), br(), + input({ type: "datetime-local", name: "deadline", min: moment().format("YYYY-MM-DDTHH:mm"), value: cp && cp.deadline ? moment(cp.deadline).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), + label(i18n.mapLocationTitle || "Map Location"), br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: cp ? cp.mapUrl : "" }), br(), + label(i18n.campaignTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.campaignTagsPlaceholder, value: cp ? cp.tags.join(", ") : "" }), br(), br(), + ...(!cp || clearnetEligible(cp) ? [renderClearnetSelector(!!cp && cp.clearnet === true, i18n), br()] : []), + button({ type: "submit", class: "create-button" }, cp ? i18n.campaignUpdate : i18n.campaignCreate) + ) + ); + +exports.campaignsView = async (campaigns, filter = "ALL", params = {}) => { + const list = Array.isArray(campaigns) ? campaigns : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q); + return template( + i18n.campaignsTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.campaignsTitle), p(i18n.campaignsDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? [] : census, emptyMod) + ), + section( + isForm + ? renderForm(f === "EDIT" ? params.campaign : null) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, STATUSES.map(s => ({ label: statusLabel(s), count: list.filter(c => c.status === s).length }))), + form({ method: "GET", action: "/campaigns", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: q, placeholder: i18n.campaignSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, list.length ? paged(list).map(c => renderCampaignCard(c, params)) : p(i18n.campaignNoItems)) + ] + ) + ); +}; + +const renderSignatures = (cp) => + div({ class: "card-section campaign-signatures" }, + h3(`${i18n.campaignSignaturesTitle} (${cp.signatureCount})`), + cp.signatureCount + ? div({ class: "campaign-signature-list" }, ...cp.signatures.slice().reverse().map(s => div({ class: "campaign-signature" }, + div({ class: "campaign-signature-head" }, userLink(s.author), span({ class: "date-link" }, fmt(s.createdAt))), + s.text ? p({ class: "campaign-signature-text" }, s.text) : null + ))) + : p(i18n.campaignNoSignatures) + ); + +const renderUpdate = (cp, u, href, editing) => + div({ class: "campaign-update", id: `update-${encodeURIComponent(u.id)}` }, + div({ class: "emergency-update-head campaign-update-head" }, + div({ class: "card-chips-row" }, + u.edited ? renderStateChip("neutral", "", i18n.campaignUpdateEdited) : null + ), + cp.isOwner + ? div({ class: "tribe-side-actions emergency-update-actions" }, + editing ? null : form({ method: "GET", action: `${href}#update-${encodeURIComponent(u.id)}` }, input({ type: "hidden", name: "editUpdate", value: u.id }), button({ type: "submit", class: "update-btn" }, i18n.campaignUpdate)), + form({ method: "POST", action: `/campaigns/updates/${encodeURIComponent(u.id)}/delete` }, button({ type: "submit", class: "delete-btn" }, i18n.campaignDelete)) + ) + : null + ), + editing + ? form({ method: "POST", action: `/campaigns/updates/${encodeURIComponent(u.id)}/edit`, enctype: "multipart/form-data", class: "campaign-update-form" }, + textarea({ name: "text", rows: 3, maxlength: "3000", required: true, autofocus: true }, u.text), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.campaignUpdate) + ) + : div({ class: "campaign-update-text" }, ...renderStyledText(u.text)), + p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(u.createdAt)}`), userLink(u.author)) + ); + +const renderUpdates = (cp, href, editUpdate) => + div({ class: "card-section campaign-updates" }, + h3(i18n.campaignUpdatesTitle), + cp.updates.length + ? div({ class: "campaign-updates-list" }, ...cp.updates.slice().reverse().map(u => renderUpdate(cp, u, href, editUpdate === u.id))) + : p(i18n.campaignNoUpdates), + cp.isOwner + ? form({ method: "POST", action: `/campaigns/updates/${encodeURIComponent(cp.id)}`, enctype: "multipart/form-data", class: "campaign-update-form" }, + textarea({ name: "text", rows: 3, maxlength: "3000", required: true, placeholder: i18n.campaignUpdatePlaceholder }), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.campaignAddUpdate) + ) + : null + ); + +exports.singleCampaignView = async (cp, params = {}) => { + const href = campaignHref(cp); + const canClearnet = clearnetEligible(cp); + const isClearnet = canClearnet && !!cp.clearnet; + const census = Array.isArray(params.censusList) ? params.censusList : []; + const sideActions = []; + const ownerActions = cp.isOwner + ? [ + form({ method: "GET", action: "/campaigns" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: cp.id }), button({ type: "submit", class: "update-btn" }, i18n.campaignUpdate)) + ].filter(Boolean) + : []; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(cp.id, href, { author: cp.author, favKind: "campaigns", isFavorite: cp.isFavorite, reportTitle: cp.title, spread: params.spread || null, returnTo: href, deleteAction: cp.isOwner ? contentDeleteAction("campaign", cp.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, cp.title)), + div({ class: "card-chips-row" }, + statusChip(cp), outcomeChip(cp), categoryChip(cp), cp.proposalId ? renderStateChip("neutral", "⚖", String(i18n.campaignElevated).toUpperCase()) : null, + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("campaigns", cp.title, cp.id) : null), + cp.isOwner && canClearnet ? renderClearnetSwitch("campaigns", cp.rootId || cp.id, isClearnet) : null + ), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, fmt(cp.createdAt)) + ), + cp.deadline + ? tr( + td({ class: "tribe-info-label" }, i18n.campaignDeadlineLabel), + td({ class: "tribe-info-value", colspan: "3" }, fmt(cp.deadline)) + ) + : null, + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(cp.author)) + ) + ), + div({ class: "campaign-qr" }, + img({ src: `${href}/qr.png`, alt: i18n.campaignQrAlt, class: "campaign-qr-img" }), + p({ class: "campaign-qr-caption" }, i18n.campaignQrCaption) + ), + renderProgress(cp), + renderMapEmbed(params.mapData, cp.mapUrl), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + div({ class: "campaign-signed" }, + cp.signed ? renderStateChip("mutuals", "✓", i18n.campaignSigned) : renderStateChip("closed", "✗", i18n.campaignNotSigned) + ), + cp.canElevate + ? div({ class: "tribe-side-actions campaign-elevate" }, form({ method: "POST", action: `/campaigns/elevate/${encodeURIComponent(cp.id)}` }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.campaignElevate).toUpperCase()))) + : null, + cp.isOwner && !cp.expired + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + statusChip(cp), + form({ method: "POST", action: `/campaigns/status/${encodeURIComponent(cp.id)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "status", value: cp.status === "CLOSED" ? "OPEN" : "CLOSED" }, + String(cp.status === "CLOSED" ? i18n.campaignReopen : i18n.campaignClose).toUpperCase()) + ) + ) + : null, + div({ class: "doc-export-actions" }, + form({ method: "GET", action: `${href}/pdf` }, button({ type: "submit", class: "filter-btn" }, i18n.campaignGenerateReport)), + form({ method: "POST", action: `${href}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) + ), + renderTags(cp.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + params.subscription + ? renderSubscriptionBox({ target: cp.id, scope: "campaigns", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: cp.isOwner, returnTo: href, canWrite: cp.isOwner }) + : null + ); + const main = div({ class: "tribe-main" }, + cp.text ? div({ class: "campaign-body" }, ...renderStyledText(cp.text)) : null, + cp.canSign + ? div({ class: "card-section campaign-sign" }, + h3(i18n.campaignSignTitle), + form({ method: "POST", action: `/campaigns/sign/${encodeURIComponent(cp.id)}`, class: "campaign-sign-form" }, + textarea({ name: "text", rows: 3, maxlength: "500", placeholder: i18n.campaignSignPlaceholder }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.campaignSign) + ) + ) + : null, + renderUpdates(cp, href, String(params.editUpdate || "")), + renderSignatures(cp), + renderEngagement(cp.id, + renderOpinionsVoting("/campaigns/opinions", cp.id, cp.opinions, href, cp.opinions_inhabitants), + renderCommentsSection({ action: `/campaigns/${encodeURIComponent(cp.id)}/comments`, comments: params.comments || [], returnTo: href }) + ) + ); + return template( + cp.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.campaignsTitle), p(i18n.campaignsDescription)), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; + +exports.clearnetCampaignView = async (cp, params = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, blobUrl: cnBlob, renderClearnetPage } = require("./clearnet_view"); + const title = esc(cp.title || i18n.campaignLabel); + const desc = renderRichText(cp.text || ""); + const point = params.mapPoint || null; + const cover = cp.media && cp.media.kind === "image" ? cnBlob(cp.media.blobId) : null; + const goal = Number(cp.goal) || 0; + const signed = Number(cp.signatureCount) || 0; + const pct = Math.max(0, Math.min(100, Number(cp.progress) || 0)); + const updates = (Array.isArray(cp.updates) ? cp.updates : []).filter(u => String(u.author) === String(cp.author)); + const extraCss = ` +.cn-campaign-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} +.cn-campaign-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} +.cn-campaign-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} +.cn-campaign-progress{margin:0 0 20px 0} +.cn-campaign-progress-label{color:var(--fg);font-weight:700;font-size:15px;margin-bottom:8px} +.cn-campaign-progress progress{width:100%;height:14px;accent-color:var(--fg)} +.cn-campaign-desc{color:var(--fg-soft);line-height:1.6;font-size:15px;margin:0 0 20px 0;word-break:break-word} +.cn-campaign-update{border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin:12px 0;background:var(--bg-elev)} +.cn-campaign-update-date{color:var(--fg-dim);font-size:12px;margin-bottom:6px} +.cn-campaign-update-text{color:var(--fg-soft);line-height:1.5;word-break:break-word} +`; + const body = ` +

${title}

+
+ ${renderKindTag("campaign")} + ${esc(statusLabel(cp.achieved ? "ACHIEVED" : "OPEN"))} + ${esc(catLabel(String(cp.category || "OTHER")))} + ${cp.createdAt ? `📅 ${esc(fmt(cp.createdAt))}` : ""} + ${cp.deadline ? `⏳ ${esc(i18n.campaignDeadlineLabel)}: ${esc(fmt(cp.deadline))}` : ""} + ${point ? `📍 ${point.title ? `${esc(point.title)} · ` : ""}${point.lat}, ${point.lng}` : ""} +
+ ${goal > 0 ? `
${esc(i18n.campaignSignaturesLabel)}: ${signed} / ${goal} (${pct}%)
` : ""} +
+ ${desc ? `
${desc}
` : ""} + ${updates.length ? `

${esc(i18n.campaignUpdatesTitle)}

${updates.slice().reverse().map(u => `
${esc(fmt(u.updatedAt || u.createdAt))}
${renderRichText(u.text || "")}
`).join("")}` : ""} + ${renderTagChips(cp.tags)} +`; + return renderClearnetPage({ + title: `${cp.title || i18n.campaignLabel} | Oasis`, + ogTitle: cp.title || i18n.campaignLabel, + ogDescription: String(cp.text || "").replace(/!?\[[^\]]*\]\(\s*&[^)\s]+\s*\)/g, "").trim(), + ogImage: cover, + extraCss, + body, + hubFeedId: cp.author || null + }); +}; diff --git a/src/views/chats_view.js b/src/views/chats_view.js index a3c2c00..bb8202f 100644 --- a/src/views/chats_view.js +++ b/src/views/chats_view.js @@ -1,9 +1,11 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views") +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li, details, summary, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe") +const { template, i18n, userLink, userLinkLabel, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, paged } = require("./main_views") const { renderEncryptedChip } = require("./clearnet_view") +const { renderResults, renderBallot, outcomeOf } = require("./polls_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") +const { renderZoomableImage } = require("./gallery_view") const userId = config.keys.id const safeArr = (v) => (Array.isArray(v) ? v : []) @@ -17,13 +19,22 @@ const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3] const catKey = (c) => "forumCat" + String(c || "").replace(/\./g, "").replace(/[\s-]/g, "").toUpperCase() const catLabel = (c) => i18n[catKey(c)] || c +const blobSrcOf = (value) => { + const s = String(value || "").trim() + if (s.startsWith("/blob/sealed/")) return s + if (s.startsWith("&")) return `/blob/${encodeURIComponent(s)}` + const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + return mImg ? `/blob/${encodeURIComponent(mImg[1])}` : null +} + const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith("&")) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) - const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) + if (s.startsWith("/blob/sealed/")) return img({ loading: 'lazy', src: s, ...attrs }) + if (s.startsWith("&")) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/) + if (mImg) return img({ loading: 'lazy', src: mImg[1].startsWith("/blob/sealed/") ? mImg[1] : `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } @@ -35,14 +46,16 @@ const buildReturnTo = (filter, params = {}) => { return `/chats?${parts.join("&")}` } -const renderModeButtons = (currentFilter) => +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => div({ class: "tribe-mode-buttons" }, - ["all", "mine", "recent", "favorites", "open", "closed"].map(f => + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "favorites", "open", "closed"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => form({ method: "GET", action: "/chats" }, input({ type: "hidden", name: "filter", value: f }), button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`chatFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) ) ), + ]), form({ method: "GET", action: "/chats" }, input({ type: "hidden", name: "filter", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.chatCreate) @@ -59,36 +72,44 @@ const renderChatStatusChip = (status) => { return renderStateChip(variant, icon, label) } -const renderChatCard = (chat, filter, params = {}) => { +const renderChatCard = exports.renderChatCard = (chat, filter, params = {}) => { const chips = [ renderChatStatusChip(chat.status), renderEncryptedChip(i18n), - renderLifespanChip(chat.lifetime, i18n) + renderLifespanChip(chat.lifetime, i18n), + chat.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (chat.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ].filter(Boolean) + const href = `/chats/${encodeURIComponent(chat.key)}` return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(chat.key, href, { + spread: params.spread || null, + author: chat.author, + favKind: 'chats', + isFavorite: chat.isFavorite, + returnTo: buildReturnTo(filter, params), + reportTitle: chat.title, + deleteAction: String(chat.author) === String(userId) ? contentDeleteAction('chat', chat.key) : undefined + }) + ), div({ class: "tribe-card-image-wrapper" }, - a({ href: `/chats/${encodeURIComponent(chat.key)}` }, + a({ href: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }, renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-card-hero-image" }) ) ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/chats/${encodeURIComponent(chat.key)}` }, chat.title || i18n.chatUntitled) + a({ href: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }, chat.title || i18n.chatUntitled) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, chat.description ? p({ class: "tribe-card-description" }, chat.description) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`) - ), - (() => { - const btn = renderSpreadButton(chat.key, (params && params.spreadMap && params.spreadMap.get(chat.key)) || (params && params.spreads)); - return btn ? div({ class: "card-spread-left" }, btn) : null; - })(), - div({ class: "visit-btn-centered" }, - a({ href: `/chats/${encodeURIComponent(chat.key)}`, class: "filter-btn" }, i18n.chatVisitChat) ) ) ) @@ -104,19 +125,18 @@ const renderChatForm = (filter, chat = {}, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, span(i18n.title || "Title"), br(), - input({ type: "text", name: "title", required: true, placeholder: i18n.chatTitlePlaceholder, value: chat.title || "" }), br(), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.chatTitlePlaceholder, value: chat.title || "" }), br(), br(), span(i18n.chatDescription), br(), - textarea({ name: "description", rows: 4, placeholder: i18n.chatDescriptionPlaceholder }, chat.description || ""), br(), br(), - span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(), + textarea({ name: "description", rows: 4, maxlength: "1000", placeholder: i18n.chatDescriptionPlaceholder }, chat.description || ""), br(), br(), + span(i18n.uploadMedia), br(), input({ type: "file", name: "image", accept: "image/*" }), br(), br(), span(i18n.chatCategory), br(), select({ name: "category" }, - option({ value: "" }, "\u2014"), ALL_CATS.map(cat => - option({ value: cat, ...(chat.category === cat ? { selected: true } : {}) }, catLabel(cat)) + option({ value: cat, ...((chat.category || "GENERAL") === cat ? { selected: true } : {}) }, catLabel(cat)) ) ), br(), br(), - span(i18n.chatStatusLabel || "Status"), br(), + span(i18n.chatTypeLabel), br(), select({ name: "status" }, option({ value: "OPEN", ...((!chat.status || chat.status === "OPEN") ? { selected: true } : {}) }, i18n.chatStatusOpen), option({ value: "INVITE-ONLY", ...(chat.status === "INVITE-ONLY" ? { selected: true } : {}) }, i18n.chatStatusInviteOnly) @@ -128,34 +148,169 @@ const renderChatForm = (filter, chat = {}, params = {}) => { ) } -const renderMessageText = (text) => { - if (!text) return span({ class: "chat-message-text" }, "") - const lines = String(text).split("\n") - const nodes = [] - lines.forEach((line, idx) => { - const rendered = renderUrl(line) - nodes.push(...rendered) - if (idx < lines.length - 1) nodes.push(br()) - }) - return span({ class: "chat-message-text" }, ...nodes) +const renderMessageText = (text) => span({ class: "chat-message-text" }, ...renderStyledText(String(text || ""))) + +const chatActivityTs = (c) => Math.max( + Number(c.lastMsgAt || 0), + Date.parse(c.updatedAt || "") || 0, + Date.parse(c.createdAt || "") || 0 +) + +const renderChatTopics = (chats, activeKey) => + div({ class: "chat-topics" }, + div({ class: "chat-topics-head" }, + a({ href: "/chats", class: "chat-topics-title-link" }, i18n.chatsTitle), + a({ href: "/chats?filter=create", class: "filter-btn chat-topics-new" }, "+") + ), + ...safeArr(chats).slice().sort((a, b) => chatActivityTs(b) - chatActivityTs(a)).map(c => { + const key = c.rootId || c.key + const active = activeKey && String(key) === String(activeKey) + return a({ href: `/chats/${encodeURIComponent(key)}#chat-latest`, class: active ? "chat-topic chat-topic-active" : "chat-topic" }, + renderMediaBlob(c.image, "/assets/images/default-avatar.png", { class: "chat-topic-thumb" }), + span({ class: "chat-topic-title" }, c.title || i18n.chatUntitled), + span({ class: "chat-topic-meta" }, `\u2689 ${safeArr(c.members).length}`), + c.lastMsg && (c.lastMsg.text || c.lastMsg.author) ? (() => { + const who = userLinkLabel(c.lastMsg.author) || '' + const shortWho = who.length > 20 ? who.slice(0, 10) + '\u2026' : who + return span({ class: "chat-topic-last" }, + span({ class: "chat-topic-last-author" }, shortWho + ': '), + String(c.lastMsg.text || '').replace(/\s+/g, ' ').slice(0, 80) + ) + })() : null + ) + }) + ) + +const renderSenderLink = (author, isOwner) => + author + ? a({ href: `/author/${encodeURIComponent(author)}`, class: isOwner ? "chat-bubble-sender chat-bubble-sender-owner" : "chat-bubble-sender" }, userLinkLabel(author)) + : null + +const renderChatPoll = (poll, chat) => { + const isSelf = String(poll.author) === String(userId) + const isOwner = String(poll.author) === String(chat.author) + const showResults = poll.hasVoted || poll.status === "CLOSED" + return div({ class: isSelf ? "chat-bubble-row chat-bubble-row-self" : "chat-bubble-row" }, + div({ class: isSelf ? "chat-message chat-message-self chat-poll" : "chat-message chat-poll" }, + isSelf ? null : renderSenderLink(poll.author, isOwner), + div({ class: "chat-poll-head" }, + span({ class: "chat-poll-tag" }, i18n.pollInChat), + poll.anonymous ? span({ class: "chat-poll-tag" }, i18n.pollAnonymous) : null, + poll.multiple ? span({ class: "chat-poll-tag" }, i18n.pollMultiple) : null, + poll.status === "CLOSED" ? span({ class: "chat-poll-tag" }, i18n.pollStatusClosed) : null + ), + poll.undecryptable + ? p({ class: "chat-poll-question" }, i18n.chatAccessDenied) + : [ + p({ class: "chat-poll-question" }, poll.question), + showResults ? renderResults(poll) : null, + (() => { + const canClose = String(poll.author) === String(userId) && poll.status === "OPEN"; + const closeFormId = `poll-close-${String(poll.id).replace(/[^a-zA-Z0-9]/g, "")}`; + const ballot = renderBallot(poll, `/chats/${encodeURIComponent(chat.key)}`, "/polls", + canClose ? [button({ type: "submit", form: closeFormId, class: "danger-btn" }, i18n.pollCloseButton)] : null); + return div({ class: "chat-poll-actions" }, + ballot, + canClose + ? form({ id: closeFormId, method: "POST", action: `/polls/close/${encodeURIComponent(poll.id)}`, class: "chat-poll-close-form" }, + input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), + ballot ? null : button({ type: "submit", class: "danger-btn" }, i18n.pollCloseButton) + ) + : null + ); + })(), + div({ class: "chat-poll-meta" }, + span({ class: "card-label" }, `${i18n.pollVoters}: `), + span({ class: "card-value" }, String(poll.totalVoters)), + span({ class: "card-label" }, ` · ${i18n.pollOutcome}: `), + span({ class: "card-value" }, outcomeOf(poll)) + ) + ], + span({ class: "chat-bubble-time" }, moment(poll.createdAt).format("HH:mm")) + ) + ) } -const renderMessage = (msg, chatAuthor) => { - const isAuthor = String(msg.author) === String(chatAuthor) +const REACTIONS = [ + ["up", "👍"], + ["heart", "❤️"], + ["laugh", "😂"], + ["down", "👎"] +] + +const anchorIdOf = (key) => "msg-" + String(key || "").replace(/[^a-zA-Z0-9]/g, "") + +const renderMessage = (msg, chat, opts = {}) => { + const isAuthor = String(msg.author) === String(chat.author) const isSelf = String(msg.author) === String(userId) - const dateStr = moment(msg.createdAt).format("YYYY/MM/DD HH:mm") - const shortId = msg.author ? "@" + msg.author.slice(1, 9) + "\u2026" : "?" - const authorLink = msg.author ? userLink(msg.author) : span("?") + const imageSrc = blobSrcOf(msg.image) + const mime = String(msg.mimeType || "") + const imageNode = imageSrc + ? (mime.startsWith("video/") + ? videoHyperaxe({ controls: true, class: "post-video chat-message-media", src: imageSrc, type: mime, preload: "metadata" }) + : mime.startsWith("audio/") + ? audioHyperaxe({ controls: true, class: "post-audio chat-message-media", src: imageSrc, type: mime, preload: "metadata" }) + : mime === "application/pdf" + ? a({ href: imageSrc, target: "_blank", rel: "noopener", class: "filter-btn chat-message-file" }, "📄 PDF") + : mime.includes("bittorrent") || mime === "application/x-torrent" + ? renderTorrentDownload(imageSrc, { class: "filter-btn chat-message-file" }) + : renderZoomableImage(imageSrc, { imgClass: "chat-message-image" })) + : (msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null) - const imageNode = msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null + const counts = (msg.reactions && msg.reactions.counts) || {} + const mine = (msg.reactions && msg.reactions.mine) || {} + const reactionNodes = REACTIONS.map(([code, icon]) => { + const n = Number(counts[code] || 0) + const label = n > 0 ? `${icon} ${n}` : icon + if (!opts.canWrite) return n > 0 ? span({ class: "chat-react-btn" }, label) : null + return form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/react`, class: "chat-react-form" }, + input({ type: "hidden", name: "target", value: msg.key }), + input({ type: "hidden", name: "emoji", value: code }), + button({ type: "submit", class: mine[code] ? "chat-react-btn chat-react-active" : "chat-react-btn" }, label) + ) + }).filter(Boolean) - return div({ class: isSelf ? "chat-message chat-message-self" : isAuthor ? "chat-message chat-message-author" : "chat-message" }, - div({ class: "chat-message-meta" }, - span({ class: "chat-message-sender" }, authorLink), - span({ class: "chat-message-date" }, ` [ ${dateStr} ]`) - ), - imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null, - renderMessageText(msg.text || "") + const replyLink = opts.canWrite + ? a({ href: `/chats/${encodeURIComponent(chat.key)}?reply=${encodeURIComponent(msg.key)}#chat-message-form`, class: "chat-react-btn chat-reply-btn", title: i18n.chatReply }, "↩") + : null + + const pinBtn = opts.isChatAuthor + ? form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/pin`, class: "chat-react-form chat-pin-toggle" }, + input({ type: "hidden", name: "target", value: msg.key }), + button({ type: "submit", class: msg.pinned ? "chat-react-btn chat-react-active" : "chat-react-btn", title: msg.pinned ? i18n.chatUnpin : i18n.chatPin }, "📌") + ) + : null + + const quoteText = String((msg.reply && msg.reply.text) || "").trim() + const quote = msg.reply + ? a({ href: `#${anchorIdOf(msg.replyTo)}`, class: "chat-reply-quote" }, + span({ class: "chat-reply-arrow" }, "↩ "), + span({ class: "chat-reply-text" }, quoteText ? `${quoteText}${quoteText.length >= 120 ? "…" : ""}` : userLinkLabel(msg.reply.author)) + ) + : null + + const timeNode = span({ class: "chat-bubble-time" }, moment(msg.createdAt).format("HH:mm")) + const actionsRow = (reactionNodes.length || replyLink) + ? div({ class: "chat-msg-actions" }, ...reactionNodes, replyLink, + span({ class: replyLink ? "chat-bubble-time chat-time-inline" : "chat-bubble-time chat-time-inline chat-time-push" }, moment(msg.createdAt).format("HH:mm"))) + : null + + const bubbleClass = [ + "chat-message", + isSelf ? "chat-message-self" : (isAuthor ? "chat-message-author" : ""), + msg.pinned ? "chat-message-pinned" : "", + pinBtn ? "chat-has-pin-toggle" : "" + ].filter(Boolean).join(" ") + + return div({ id: anchorIdOf(msg.key), class: isSelf ? "chat-bubble-row chat-bubble-row-self" : "chat-bubble-row" }, + div({ class: bubbleClass }, + pinBtn, + isSelf ? null : renderSenderLink(msg.author, isAuthor), + quote, + renderMessageText(msg.text || ""), + imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null, + actionsRow || timeNode + ) ) } @@ -174,37 +329,45 @@ exports.renderChatInvitePage = (code) => { exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => { const q = safeText(params.q || "") const list = safeArr(chats) + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q) const isForm = filter === "create" || filter === "edit" - const chatHeaderMap = { - all: i18n.chatsTitle, - mine: i18n.chatMineSectionTitle || "Your Chats", - recent: i18n.chatRecentTitle || "Recent Chats", - favorites: i18n.chatFavoritesTitle || "Favorites", - open: i18n.chatOpenTitle || "Open Chats", - closed: i18n.chatClosedTitle || "Closed Chats" + const headerText = i18n.chatsTitle + + if (params.workspace && !isForm) { + return template( + i18n.chatsTitle, + section({ class: "chat-workspace-section chat-workspace-list" }, + div({ class: "chat-workspace" }, + renderChatTopics(list, null), + div({ class: "chat-workspace-main chat-workspace-empty" }, + p({ class: "chat-no-messages" }, list.length ? i18n.chatPickChat : i18n.chatNoneYet) + ) + ) + ) + ) } - const headerText = chatHeaderMap[filter] || i18n.chatsTitle return template( i18n.chatsTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(headerText), p(i18n.modulesChatsDescription) ) ), - section(renderModeButtons(filter)), - !isForm + section(renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null)), + !isForm && !emptyMod ? section( - div({ class: "filters" }, - form({ method: "GET", action: "/chats" }, + div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(list, c => String(c.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.chatStatusOpen }, { value: 'INVITE-ONLY', label: i18n.chatStatusInviteOnly }, { value: 'CLOSED', label: i18n.chatStatusClosed }]), + form({ method: "GET", action: "/chats", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", placeholder: i18n.chatSearchPlaceholder, value: q }), - br(), - button({ type: "submit" }, i18n.search), - br() + input({ type: "text", name: "q", placeholder: i18n.chatSearchPlaceholder, value: q, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) ) ) ) @@ -214,7 +377,7 @@ exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => { ? renderChatForm(filter, filter === "edit" ? (chatToEdit || {}) : {}, params) : div({ class: "tribe-grid" }, list.length - ? list.map(chat => renderChatCard(chat, filter, { q })) + ? paged(list).map(chat => renderChatCard(chat, filter, { q, spread: params.spreadMap && params.spreadMap.get(chat.key) })) : p(i18n.chatNoItems) ) ) @@ -226,50 +389,62 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q }) const isAuthor = String(chat.author) === String(userId) const isMember = safeArr(chat.members).includes(userId) || (!!chat.tribeId && !!chat.isTribeMember) - const fullShareUrl = `/chats/${encodeURIComponent(chat.key)}` const isRestrictedInviteOnly = !isMember && !isAuthor && chat.status === "INVITE-ONLY" const statusLabel = chat.status === "CLOSED" ? i18n.chatStatusClosed : chat.status === "INVITE-ONLY" ? i18n.chatStatusInviteOnly : i18n.chatStatusOpen + const chatShares = isAuthor || isMember const detailChips = [ renderChatStatusChip(chat.status), renderEncryptedChip(i18n), - renderLifespanChip(chat.lifetime, i18n) + renderLifespanChip(chat.lifetime, i18n), + chat.subscription && chatShares + ? ((isAuthor || chat.subscription.subscribed === true) + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : renderStateChip("closed", "✉", i18n.subscriptionOff)) + : null ].filter(Boolean) const chatSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(chat.key, `/chats/${encodeURIComponent(chat.key)}`, { spread: params.spreads || null, author: chat.author, favKind: 'chats', isFavorite: chat.isFavorite, returnTo: params.returnTo, reportTitle: chat.title, deleteAction: isAuthor ? contentDeleteAction('chat', chat.key) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, chat.title || i18n.chatUntitled) ), + chat.description ? p({ class: "tribe-card-description" }, chat.description) : null, detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-detail-image" }), - div({ class: "shop-share" }, - span({ class: "tribe-info-label" }, `${i18n.chatShareUrl}: `), - input({ type: "text", value: fullShareUrl, readonly: true, class: "shop-share-input" }) - ), - (() => { - const btn = renderSpreadButton(chat.key, params.spreads); - return btn ? div({ class: "card-spread-centered" }, btn) : null; - })(), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`) ), + chat.subscription && chatShares + ? renderSubscriptionBox({ + target: chat.rootId || chat.key, + scope: "chats", + subscribed: chat.subscription.subscribed === true, + count: chat.subscription.count, + isOwner: isAuthor, + returnTo + }) + : null, table({ class: "tribe-info-table" }, tr( td({ class: "tribe-info-label" }, i18n.chatCreatedAt), td({ class: "tribe-info-value", colspan: "3" }, moment(chat.createdAt).format("YYYY/MM/DD HH:mm")) ), + !isRestrictedInviteOnly && chat.category ? tr( + td({ class: "tribe-info-label" }, i18n.chatCategoryLabel), + td({ class: "tribe-info-value", colspan: "3" }, catLabel(chat.category)) + ) : null, isRestrictedInviteOnly ? null : tr( td({ class: "tribe-info-value", colspan: "4" }, userLink(chat.author) ) - ), - !isRestrictedInviteOnly && chat.category ? tr( - td({ class: "tribe-info-label" }, i18n.chatCategoryLabel), - td({ class: "tribe-info-value", colspan: "3" }, catLabel(chat.category)) - ) : null + ) ), - isRestrictedInviteOnly ? null : div({ class: "tribe-side-actions" }, + isRestrictedInviteOnly ? null : (() => { + const sideActionNodes = [ isAuthor && chat.status === "INVITE-ONLY" ? form({ method: "POST", action: `/chats/generate-invite` }, input({ type: "hidden", name: "chatId", value: chat.key }), @@ -283,7 +458,8 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { if (openInvite) return [ div({ class: "tribe-open-invite" }, span({ class: "card-label" }, i18n.tribeInviteCodeText), - span({ class: "tribe-open-invite-code" }, openInvite.code) + span({ class: "tribe-open-invite-code" }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/chats/${encodeURIComponent(openInvite.code)}` }) ), form({ method: "POST", action: `/chats/open-invite/remove` }, input({ type: "hidden", name: "chatId", value: chat.key }), @@ -295,43 +471,35 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) ) })(), - form( - { method: "POST", action: chat.isFavorite ? `/chats/favorites/remove/${encodeURIComponent(chat.key)}` : `/chats/favorites/add/${encodeURIComponent(chat.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "tribe-action-btn" }, chat.isFavorite ? i18n.chatRemoveFavorite : i18n.chatAddFavorite) - ), - chat.author && String(chat.author) !== String(userId) - ? form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: chat.author }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.chatPM || i18n.privateMessage) - ) - : null, - isAuthor - ? form({ method: "GET", action: `/chats/edit/${encodeURIComponent(chat.key)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) - ) - : null, - isAuthor && chat.status !== "CLOSED" - ? form({ method: "POST", action: `/chats/close/${encodeURIComponent(chat.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.chatClose) - ) - : null, - isAuthor - ? form({ method: "POST", action: `/chats/delete/${encodeURIComponent(chat.key)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete) - ) - : null, !isAuthor && isMember ? form({ method: "POST", action: `/chats/leave/${encodeURIComponent(chat.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeLeaveButton) ) : null + ].flat().filter(Boolean) + return sideActionNodes.length ? div({ class: "tribe-side-actions" }, ...sideActionNodes) : null + })(), + isAuthor + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.chatStatusLabel}: `), + renderStateChip(chat.status === "CLOSED" ? "hidden" : "mutuals", chat.status === "CLOSED" ? "🔒" : "👁", String(chat.status || "OPEN").toUpperCase()), + chat.status !== "CLOSED" + ? form({ method: "POST", action: `/chats/close/${encodeURIComponent(chat.key)}`, class: "inline-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatClose) + ) + : null + ) + : null, + !isAuthor ? null : div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: `/chats/edit/${encodeURIComponent(chat.key)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) + ) ), !isMember && chat.status === "INVITE-ONLY" ? div({ class: "chat-join-section" }, - a({ class: "tribe-action-btn", href: "/invites#invites-chats" }, i18n.tribeEnterInvite) + a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite) ) : null, !isRestrictedInviteOnly && safeArr(chat.tags).length @@ -344,38 +512,112 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { const msgList = safeArr(messages) const canWrite = (isMember || chat.status === "OPEN") && chat.status !== "CLOSED" + const pinnedMsgs = msgList.filter(m => m.pinned).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + const replyMsg = params.reply ? msgList.find(m => String(m.key) === String(params.reply)) : null + const chatMain = isRestrictedInviteOnly ? div({ class: "tribe-main chat-full-width" }, p({ class: "access-denied-msg" }, i18n.chatAccessDenied)) : div({ class: "tribe-main chat-full-width" }, + div({ class: "chat-jump-row" }, + a({ href: "/chats", class: "filter-btn chat-back-btn" }, "\u00ab " + (i18n.chatsTitle || "Chats")), + msgList.length && pinnedMsgs.length + ? a({ href: `#${anchorIdOf(pinnedMsgs[0].key)}`, class: "filter-btn chat-pinned-link" }, `${i18n.chatPinned} (${pinnedMsgs.length})`) + : null, + msgList.length ? a({ href: "#chat-latest", class: "filter-btn chat-jump-latest" }, i18n.chatJumpLatest) : null + ), + div({ class: "chat-messages-list" }, + (() => { + const visible = msgList + .filter(msg => (msg.text && String(msg.text).trim()) || msg.image) + .map(msg => ({ kind: 'message', ts: Date.parse(msg.createdAt) || 0, msg })) + const pollItems = safeArr(params.polls).map(poll => ({ + kind: 'poll', ts: Date.parse(poll.createdAt) || 0, poll + })) + const stream = [...visible, ...pollItems].sort((a, b) => a.ts - b.ts) + if (!stream.length) return p({ class: "chat-no-messages" }, i18n.chatNoMessages) + const nodes = [] + let prevDay = null + stream.forEach((entry, i) => { + const day = moment(entry.ts).format("YYYY/MM/DD") + if (day !== prevDay) { + nodes.push(div({ class: "chat-day-separator" }, span({ class: "chat-day-chip" }, day))) + prevDay = day + } + const last = i === stream.length - 1 + const node = entry.kind === 'poll' + ? renderChatPoll(entry.poll, chat) + : renderMessage(entry.msg, chat, { canWrite, isChatAuthor: isAuthor }) + nodes.push(last ? div({ id: "chat-latest", class: "chat-latest-anchor" }, node) : node) + }) + return nodes + })() + ), canWrite - ? div({ class: "chat-message-form" }, + ? div({ id: "chat-message-form", class: "chat-message-form" }, + replyMsg + ? div({ class: "chat-replying-banner" }, + span({ class: "chat-reply-arrow" }, "↩ "), + span({ class: "chat-reply-text" }, String(replyMsg.text || "").trim() ? ` ${String(replyMsg.text).trim().slice(0, 120)}` : userLinkLabel(replyMsg.author)), + a({ href: `/chats/${encodeURIComponent(chat.key)}#chat-message-form`, class: "chat-reply-cancel", title: i18n.chatReplyCancel || "✕" }, "✕") + ) + : null, form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/message`, enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), - textarea({ name: "text", rows: 3, placeholder: i18n.chatMessagePlaceholder }), br(), - span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(), - input({ type: "file", name: "image", accept: "image/*" }), br(), br(), + input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }), + replyMsg ? input({ type: "hidden", name: "replyTo", value: replyMsg.key }) : null, + textarea({ name: "text", rows: 3, maxlength: "3000", placeholder: i18n.chatMessagePlaceholder }), br(), + span(i18n.uploadMedia), br(), + input({ type: "file", name: "image", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), button({ type: "submit", class: "filter-btn" }, i18n.chatSendMessage) ) ) : null, - div({ class: "chat-messages-list" }, - (() => { - const visible = msgList.filter(msg => (msg.text && String(msg.text).trim()) || msg.image) - return visible.length - ? visible.map(msg => renderMessage(msg, chat.author)) - : p({ class: "chat-no-messages" }, i18n.chatNoMessages) - })() - ) + canWrite && params.pollsEnabled + ? details({ class: "chat-poll-form" }, + summary({ class: "chat-poll-summary" }, i18n.pollChatCreate), + form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/polls/create` }, + input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), + span(i18n.pollQuestion), br(), + input({ type: "text", name: "question", required: true, maxlength: "300", placeholder: i18n.pollQuestionPlaceholder }), br(), br(), + span(i18n.pollOptions), br(), + textarea({ maxlength: "1000", name: "options", rows: 4, required: true, placeholder: i18n.pollOptionsPlaceholder }), br(), br(), + div({ class: "poll-switch" }, + input({ type: "hidden", name: "anonymous", value: "0" }), + label(input({ type: "checkbox", name: "anonymous", value: "1" }), " ", i18n.pollAnonymousLabel) + ), + div({ class: "poll-switch" }, + input({ type: "hidden", name: "multiple", value: "0" }), + label(input({ type: "checkbox", name: "multiple", value: "1" }), " ", i18n.pollMultipleLabel) + ), + button({ type: "submit", class: "filter-btn" }, i18n.pollPublishButton) + ) + ) + : null ) + if (params.workspace) { + return template( + chat.title || i18n.chatUntitled, + section({ class: "chat-workspace-section" }, + div({ class: "chat-workspace" }, + div({ class: "chat-workspace-main" }, + div({ class: "tribe-details chat-workspace-details" }, + chatSide, + chatMain + ) + ) + ) + ) + ) + } + return template( chat.title || i18n.chatUntitled, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.chatsTitle), p(i18n.modulesChatsDescription) ), - renderModeButtons(filter || "all") + renderModeButtons(filter || "all", false, (params && params.modesAvail) || null) ), section( div({ class: "tribe-details" }, diff --git a/src/views/cipher_view.js b/src/views/cipher_view.js index 6e3ecb0..b79b48d 100644 --- a/src/views/cipher_view.js +++ b/src/views/cipher_view.js @@ -9,7 +9,7 @@ function generateRandomPassword(length = 32) { const cipherView = async (encryptedText = "", decryptedText = "", iv = "", password = "") => { const randomPassword = generateRandomPassword(); - const header = div({ class: "tags-header" }, + const header = div({ class: "tags-header module-header-line" }, h2(i18n.cipherTitle), p(i18n.cipherDescription) ); @@ -83,9 +83,7 @@ const cipherView = async (encryptedText = "", decryptedText = "", iv = "", passw i18n.cipherTitle, section( header, - div({ id: "randomPassword" }, - h2({ class: "generated-password" }, randomPassword) - ), + p({ id: "randomPassword" }, h2({ class: "generated-password" }, randomPassword)), div({ class: "div-center" }, encryptForm, br(), diff --git a/src/views/clearnet_view.js b/src/views/clearnet_view.js index a863462..a54687d 100644 --- a/src/views/clearnet_view.js +++ b/src/views/clearnet_view.js @@ -1,4 +1,31 @@ -const { a, br, div, input, span, strong } = require("../server/node_modules/hyperaxe"); +const { a, br, div, input, option, select, span, strong, form, button, label } = require("../server/node_modules/hyperaxe"); +const { renderStyledHtml } = require('../backend/renderStyledText'); +const sharedState = require('../configs/shared-state'); +const cnPkg = (() => { try { return require('../server/package.json'); } catch (_) { return {}; } })(); +const mv = () => require('./main_views'); +const cnScope = () => { try { return require('../models/typed_log').requestScope.getStore() || {}; } catch (_) { return {}; } }; +const cnWithLang = (href, lang) => { + if (!lang || !/^\/c(?:[/?#]|$)/.test(href) || /[?&]lang=/.test(href)) return href; + const [pathAndQuery, hash] = href.split('#'); + const joined = `${pathAndQuery}${pathAndQuery.includes('?') ? '&' : '?'}lang=${lang}`; + return hash != null ? `${joined}#${hash}` : joined; +}; +const propagateLang = (html, lang) => !lang ? html : String(html) + .replace(/href="(\/c(?:[/?#][^"]*)?)"/g, (m, href) => `href="${cnWithLang(href, lang)}"`) + .replace(/(]*action="\/c(?:\/[^"]*)?"[^>]*>)/g, (m) => `${m}`); +const renderLangSelector = (current) => { + const langs = Object.keys(require('../client/assets/translations/i18n')); + const scope = cnScope(); + const base = String(scope.path || '/c'); + const params = new URLSearchParams(String(scope.query || '')); + params.delete('lang'); + const others = langs.filter(l => l !== current).map(l => { + const q = new URLSearchParams(params); q.set('lang', l); + return `${l.toUpperCase()}`; + }).join(''); + return `
${escapeHtml(String(current).toUpperCase())}
${others}
`; +}; +const cnText = (key, fallback) => { const v = mv().i18n[key]; return typeof v === 'string' && v ? v : fallback; }; const STAT_TYPE_KEYS = { post:'statsPost', event:'statsEvent', task:'statsTask', forum:'statsForum', tribe:'statsTribe', market:'statsMarket', job:'statsJob', project:'statsProject', shop:'statsShop', image:'statsImage', video:'statsVideo', audio:'statsAudio', document:'statsDocument', bookmark:'statsBookmark', transfer:'statsTransfer', map:'statsMap' }; const STAT_ORDER = ['post','event','task','forum','tribe','market','job','project','shop','image','video','audio','document','bookmark','transfer','map']; @@ -6,7 +33,7 @@ const renderContentStats = (stats, i18nObj = {}) => { if (!stats || typeof stats !== 'object') return null; const chips = STAT_ORDER .filter(t => (stats[t] || 0) > 0) - .map(t => span({ class: 'inhabitant-stat' }, + .map(t => span({ class: 'inhabitant-stat', title: `${i18nObj[STAT_TYPE_KEYS[t]] || t}: ${stats[t]}` }, span({ class: 'inhabitant-stat-label' }, i18nObj[STAT_TYPE_KEYS[t]] || t), strong({ class: 'inhabitant-stat-value' }, String(stats[t])) )); @@ -23,6 +50,31 @@ const escapeHtml = (s) => String(s || '') .replace(/"/g, '"') .replace(/'/g, '''); +const renderTagChips = (tags) => { + const list = (Array.isArray(tags) ? tags : []) + .map(t => String(t || '').trim()) + .filter(Boolean) + .slice(0, 12); + if (!list.length) return ''; + const chips = list.map(raw => String(raw).replace(/^#+/, '')).filter(Boolean).map(t => `#${escapeHtml(t)}`).join(''); + return `
${chips}
`; +}; + +const KIND_KEYS = { audio: 'cnKindAudio', blog: 'cnKindBlog', bookmark: 'cnKindBookmark', document: 'cnKindDocument', event: 'cnKindEvent', feed: 'cnKindFeed', image: 'cnKindImage', job: 'cnKindJob', market: 'cnKindMarket', podcast: 'cnKindPodcast', project: 'cnKindProject', course: 'cnKindCourse', shop: 'cnKindShop', torrent: 'cnKindTorrent', file: 'cnKindFile', video: 'cnKindVideo', wiki: 'cnKindWiki', emergency: 'emergencyLabel', campaign: 'campaignLabel', housing: 'housingTitle', room: 'cnKindRoom', map: 'cnKindMap', calendar: 'calendarTitle' }; +const kindLabel = (kind) => cnText(KIND_KEYS[String(kind || '').toLowerCase()], String(kind || '')); +const renderKindTag = (kind) => `[${escapeHtml(kindLabel(kind).toUpperCase())}]`; + +const renderRichText = (s, { links = true, wikiLinks = null } = {}) => renderStyledHtml(s, { + blobPrefix: '/c/blob/', + internalLinks: false, + links, + plainUrlClass: 'cn-url', + hashtagHref: links ? (tag) => `/c?q=%23${encodeURIComponent(tag)}` : null, + wikiLink: wikiLinks ? (target, slug, label) => wikiLinks.has(slug) + ? a({ href: wikiLinks.get(slug), class: 'cn-wiki-link' }, label) + : label : null +}).replace(/\n/g, '
'); + const blobIdOf = (v) => { if (!v) return null; const s = String(v).trim(); @@ -38,6 +90,22 @@ const blobUrl = (v) => { return id ? `/c/blob/${encodeURIComponent(id)}` : null; }; +const renderClearnetSelector = (on = false, i18nObj = {}) => + div({ class: 'clearnet-choice' }, + div({ class: 'clearnet-choice-options' }, + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'clearnet', value: '0', ...(on ? {} : { checked: true }) }), renderReachChip(false, i18nObj)), + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'clearnet', value: '1', ...(on ? { checked: true } : {}) }), renderReachChip(true, i18nObj)) + ) + ); + +const renderClearnetSwitch = (kind, id, on = false) => id + ? form({ method: 'POST', action: `/clearnet/item/${encodeURIComponent(id)}`, class: 'clearnet-switch' }, + input({ type: 'hidden', name: 'kind', value: kind }), + button({ type: 'submit', name: 'on', value: '0', class: on ? 'clearnet-switch-btn' : 'clearnet-switch-btn active', ...(on ? {} : { disabled: true }) }, 'OASIS'), + button({ type: 'submit', name: 'on', value: '1', class: on ? 'clearnet-switch-btn active' : 'clearnet-switch-btn', ...(on ? { disabled: true } : {}) }, 'CLEARNET') + ) + : null; + const renderReachChip = (isClearnet, i18nObj = {}, href = null) => { const icon = isClearnet ? '🌐' : '🏝'; const label = isClearnet @@ -53,6 +121,15 @@ const renderReachChip = (isClearnet, i18nObj = {}, href = null) => { return chip; }; +const renderTribeOriginChip = (origin) => origin && origin.title + ? a({ href: origin.tribeHref || origin.href, class: 'pm-exposition-chip-link' }, + span({ class: 'pm-exposition-chip pm-exposition-tribe' }, + span({ class: 'pm-exposition-icon' }, 'ꖥ'), + span({ class: 'pm-exposition-text' }, origin.title) + ) + ) + : null; + const fediverseProfileUrl = (handle) => { const h = String(handle || '').trim().replace(/^@/, ''); const at = h.indexOf('@'); @@ -71,13 +148,59 @@ const renderFediverseReach = (prefs, i18nObj = {}) => { a({ href: url, target: '_blank', rel: 'noopener noreferrer', class: 'pm-exposition-chip-link' }, span({ class: 'pm-exposition-chip pm-exposition-fediverse' }, span({ class: 'pm-exposition-icon' }, '🐘'), - span({ class: 'pm-exposition-text' }, i18nObj.fediverse || 'Fediverse') + span({ class: 'pm-exposition-text' }, i18nObj.fediverse || 'Multiverse') ) ), a({ href: url, target: '_blank', rel: 'noopener noreferrer', class: 'fediverse-reach-url' }, handle) ); }; +const LICENSES = [ + { id: 'CC0-1.0', label: 'Public Domain', name: 'Public Domain' }, + { id: 'CC-BY-4.0', label: 'CC BY 4.0', name: 'Creative Commons Attribution 4.0' }, + { id: 'CC-BY-SA-4.0', label: 'CC BY-SA 4.0', name: 'Creative Commons Attribution-ShareAlike 4.0' }, + { id: 'GFDL-1.3-or-later', label: 'GNU FDL 1.3', name: 'GNU Free Documentation License 1.3' }, + { id: 'LAL-1.3', label: 'Free Art License 1.3', name: 'Free Art License 1.3' } +]; +const DEFAULT_LICENSE = LICENSES[0].id; +const LICENSED_KINDS = new Set(['wiki', 'audio', 'video', 'image', 'document']); +const licenseInfo = (id) => { + const v = String(id || '').trim() || DEFAULT_LICENSE; + return LICENSES.find(l => l.id === v) || null; +}; +const normalizeLicense = (id) => (LICENSES.find(l => l.id === String(id || '').trim()) || LICENSES[0]).id; + +const renderLicenseChip = (id) => { + const l = licenseInfo(id); + if (!l) return null; + return span({ class: 'pm-exposition-chip pm-exposition-license', title: l.name }, + span({ class: 'pm-exposition-icon' }, '\u2696'), + span({ class: 'pm-exposition-text' }, l.label) + ); +}; + +const renderLicenseSelect = (current, i18nObj = {}) => { + const selectedId = normalizeLicense(current); + return [ + span(i18nObj.licenseLabel || 'License'), + br(), + select({ name: 'license' }, ...LICENSES.map(l => option({ value: l.id, ...(l.id === selectedId ? { selected: true } : {}) }, l.label))), + br() + ]; +}; + +const renderClearnetLicense = (id) => { + const l = licenseInfo(id); + return l ? `\u2696 ${escapeHtml(l.label)}` : ''; +}; + +const renderTransportChip = (i18nObj = {}) => { + return span({ class: 'pm-exposition-chip pm-exposition-transport', title: 'Secret Handshake + boxstream · SHA-256' }, + span({ class: 'pm-exposition-icon' }, '\u21C4'), + span({ class: 'pm-exposition-text' }, i18nObj.transportChipLabel || 'Encrypted in transit') + ); +}; + const renderEncryptedChip = (i18nObj = {}) => { return span({ class: 'pm-exposition-chip pm-exposition-encrypted' }, span({ class: 'pm-exposition-icon' }, '🔒'), @@ -92,16 +215,7 @@ const renderDoubleEncryptionChip = (i18nObj = {}) => { ); }; -const INTERNAL_OASIS_PATHS = [ - 'author','thread','hashtag','inbox','pm','profile','settings','banking','wallet', - 'jobs','events','projects','shops','audios','videos','images','documents','torrents', - 'tribes','tribe','forum','votes','votations','reports','tasks','maps','chats','pads', - 'calendars','trending','opinions','feed','pixelia','cv','invites','peers','stats', - 'blockexplorer','modules','publish','search','tags','mentions','popular','threads', - 'topics','latest','summaries','multiverse','legacy','cipher','graphos','agenda', - 'favorites','logs','games','parliament','courts','market','ai','public','spread', - 'follow','unfollow','block','like','unlike' -]; +const { INTERNAL_PATHS: INTERNAL_OASIS_PATHS } = require('../backend/renderStyledText'); const stripInternalAnchors = (html) => { if (typeof html !== 'string' || !html) return html; @@ -111,10 +225,10 @@ const stripInternalAnchors = (html) => { return html.replace(hrefedClosed, '$1').replace(hrefedBare, '$1'); }; -const renderClearnetSearchForm = ({ authorFeedId = '', query = '', placeholder = 'Search…' }) => { +const renderClearnetSearchForm = ({ authorFeedId = '', query = '', placeholder = '' }) => { if (!authorFeedId) return ''; const safeQuery = escapeHtml(query || ''); - const safePh = escapeHtml(placeholder); + const safePh = escapeHtml(placeholder || cnText('cnSearchPlaceholder', 'Search…')); return ``; }; @@ -124,6 +238,84 @@ const renderClearnetUrlBlock = ({ baseUrl = '', path, i18nObj = {} }) => { ); }; +const CLEARNET_ZOOM_CSS = ` +.cn-zoom{display:contents} +.cn-zoom img{cursor:zoom-in} +.cn-lightbox{display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.9);align-items:center;justify-content:center} +.cn-lightbox:target{display:flex} +.cn-lightbox-link{display:flex;align-items:center;justify-content:center;width:100%;height:100%;cursor:zoom-out} +.cn-lightbox-img{display:block;max-width:92vw;max-height:88vh;object-fit:contain} +.cn-lightbox-full{background:#000} +.cn-lightbox-full .cn-lightbox-img{width:100vw;height:100vh;max-width:100vw;max-height:100vh} +.cn-lightbox-close,.cn-lightbox-fs{position:absolute;top:14px;z-index:1;width:42px;height:42px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(0,0,0,.6);color:#fff;text-decoration:none;font-size:24px;line-height:1} +.cn-lightbox-close{right:16px} +.cn-lightbox-fs{right:66px} +.cn-lightbox-fs svg{display:block;width:20px;height:20px} +`; +const CN_FULLSCREEN_ICON = ''; +const cnZoom = (id, src, alt = '', { full = false, imgClass = '' } = {}) => { + const zid = `cnz-${String(id).replace(/[^A-Za-z0-9_-]/g, '')}`; + const a = escapeHtml(alt); + const u = escapeHtml(src); + const cls = imgClass ? ` class="${escapeHtml(imgClass)}"` : ''; + const fs = full ? `${CN_FULLSCREEN_ICON}` : ''; + return `` + + `
×${fs}${a}
` + + (full ? `` : ''); +}; +const CN_ZOOM_SKIP = /\bcn-(lightbox-img|footer-logo)\b/; +const zoomClearnetImages = (html) => { + let depth = 0; + let n = 0; + return String(html || '').replace(/]*>|<\/a\s*>|]*>/gi, (tag) => { + if (/^ 0) return tag; + const cls = (tag.match(/\sclass="([^"]*)"/i) || [])[1] || ''; + const src = (tag.match(/\ssrc="([^"]*)"/i) || [])[1] || ''; + if (!src || CN_ZOOM_SKIP.test(cls)) return tag; + const alt = (tag.match(/\salt="([^"]*)"/i) || [])[1] || ''; + const zid = `cnz-i${++n}`; + return `${tag}×${alt}`; + }); +}; +const CLEARNET_TEXT_CSS = ` +.cn-tags{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0} +.cn-hub-priceline{margin:10px 0 0 0} +.cn-hub-details{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0 0 0} +.cn-detail{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:5px;padding:3px 8px;font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--fg-soft);background:var(--bg-sub);white-space:nowrap} +.cn-tag{border:1px solid var(--border);border-radius:5px;padding:3px 8px;font-size:11px;color:var(--fg-soft);background:var(--bg-sub);text-decoration:none;white-space:nowrap} +.cn-tag:hover{color:var(--fg);border-color:var(--fg)} +.cn-license{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--border);border-radius:5px;padding:3px 8px;font-size:11px;letter-spacing:1px;color:var(--fg-soft);background:var(--bg-sub);text-decoration:none;white-space:nowrap} +a.tag-link{color:var(--accent);text-decoration:none} +a.cn-wiki-link{color:var(--accent);text-decoration:underline;text-underline-offset:2px} +a.cn-wiki-link:hover{color:var(--fg)} +.cn-price{display:inline-flex;align-items:center;color:var(--fg);background:var(--bg-sub);border:1px solid var(--fg);border-radius:4px;padding:4px 10px;font-weight:bold;font-size:14px} +a.tag-link:hover{text-decoration:underline} +.rt-header{display:block;font-weight:bold;margin:10px 0 4px;line-height:1.3} +.rt-header-1{font-size:1.5em} +.rt-header-2{font-size:1.3em} +.rt-header-3{font-size:1.15em} +.rt-item{display:block;padding-left:1.4em;text-indent:-0.7em} +.rt-item::before{content:"\u2022 ";opacity:.7} +.rt-item-2{padding-left:3em} +.rt-item-3{padding-left:4.6em} +.rt-item-4{padding-left:6.2em} +.rt-item-number{text-indent:-1.2em} +.rt-item-number::before{content:none} +.rt-quote{display:block;border-left:3px solid currentColor;padding-left:10px;margin:6px 0;opacity:.85} +.rt-rule{display:block;border-top:1px solid currentColor;opacity:.4;margin:10px 0} +.rt-code{font-family:monospace;background:rgba(128,128,128,.18);padding:1px 4px;border-radius:3px;word-break:break-word} +.rt-code-block{display:block;font-family:monospace;white-space:pre-wrap;background:rgba(128,128,128,.18);padding:10px;border-radius:5px;margin:8px 0;overflow-x:auto;word-break:break-word} +.post-image{display:block;width:auto;max-width:100%;max-height:480px;height:auto;margin:12px auto;border:1px solid var(--border);border-radius:6px} +.post-video{display:block;width:100%;max-width:100%;max-height:480px;margin:12px auto;border-radius:6px;background:#000} +.post-audio{display:block;width:100%;max-width:100%;margin:12px 0} +.cn-media-frame img{max-height:480px;width:auto;margin:0 auto} +.cn-media-frame video{max-height:480px} +.post-pdf{display:inline-block;margin:8px 0;padding:8px 14px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);text-decoration:none} +.post-pdf:hover{border-color:var(--fg)} +`; + const CLEARNET_SEARCH_CSS = ` .cn-search{margin:0} .cn-search input[type=text]{width:240px;max-width:100%;background:var(--bg-sub);color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:14px;font-family:inherit} @@ -189,24 +381,30 @@ header.cn-header{display:flex;align-items:center;gap:16px;padding-bottom:16px;ma .cn-brand{font-size:20px;font-weight:700;color:var(--fg);letter-spacing:1px} .cn-brand-sub{color:var(--fg-dim);font-size:12px;text-transform:uppercase;letter-spacing:2px;margin-top:2px} .cn-header-extra{flex:1 1 auto;display:flex;justify-content:flex-end;align-items:center;min-width:0} +.cn-kind-tag{font-weight:700;color:var(--fg);letter-spacing:1px} +.cn-sep{border:0;border-top:1px solid var(--border);margin:24px 0} h2.cn-section{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:32px 0 16px 0;padding-bottom:8px;border-bottom:1px solid var(--border)} footer.cn-footer{margin-top:48px;padding-top:20px;border-top:1px solid var(--border);font-size:12px;color:var(--fg-dim);text-align:center;letter-spacing:0.5px} footer.cn-footer a{color:var(--fg-soft)} footer.cn-footer .cn-footer-logo{width:56px;height:auto;display:block;margin:0 auto 10px auto;border-radius:6px} +footer.cn-footer .cn-footer-line{margin:3px 0} `; -const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = '', hubFeedId = null }) => { +const renderClearnetHubSearch = ({ type = '', query = '' } = {}) => + ``; + +const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = null, hubFeedId = null }) => { const safeTitle = escapeHtml(title || 'Oasis'); const safeOgTitle = escapeHtml(ogTitle || title || 'Oasis'); const safeOgDesc = escapeHtml(ogDescription || ''); const palette = getCurrentPalette(); const baseCss = buildBaseCss(palette); - const brandInner = `
⛱ Oasis HUB
Libre · P2P · Federated
`; - const brandBlock = hubFeedId - ? `${brandInner}` - : `
${brandInner}
`; + const brandInner = `
⛱ Oasis HUB
${escapeHtml(cnText('cnBrandSub', 'Libre · P2P · Federated'))}
`; + const lang = escapeHtml(mv().getLanguage ? mv().getLanguage() : 'en'); + const langOverride = cnScope().cnLang || ''; + const brandBlock = `${brandInner}`; return ` - + @@ -216,21 +414,33 @@ const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null ${ogImage ? `` : ''} -
${brandBlock} - ${headerExtra ? `
${headerExtra}
` : ''} +
${renderLangSelector(lang)}${propagateLang(headerExtra == null ? renderClearnetHubSearch() : headerExtra, langOverride)}
- ${stripInternalAnchors(body)} + ${propagateLang(stripInternalAnchors(zoomClearnetImages(body)), langOverride)}
- - Powered by Oasis + + + +
`; @@ -242,14 +452,44 @@ const renderClearnetNotFound = () => { ogTitle: 'Oasis', ogDescription: '', extraCss: `.cn-notfound{color:var(--fg-soft);font-size:16px;max-width:480px;margin:80px auto 40px auto;text-align:center;line-height:1.5}`, - body: `

The content is not accessible at this moment.

` + body: `

${escapeHtml(cnText('cnNotAccessible', 'The content is not accessible at this moment.'))}

` }); }; +const CLEARNET_PAGER_CSS = ` +.cn-pager{display:flex;justify-content:flex-end;align-items:center;flex-wrap:wrap;gap:8px;margin:24px 0 8px 0} +.cn-pager-info{color:var(--fg-dim);font-size:12px;margin-right:4px} +.cn-pager-sizes{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-right:auto} +.cn-pager-btn{display:inline-block;padding:6px 14px;background:var(--bg-elev);color:var(--fg-soft);border:1px solid var(--border);border-radius:14px;font-size:13px;text-decoration:none;transition:border-color .15s ease,color .15s ease,background .15s ease} +.cn-pager-btn:hover,.cn-pager-btn.active{border-color:var(--fg);color:var(--fg);text-decoration:none} +`; +const paginateClearnet = (list, page) => mv().slicePage(list, page, mv().listPerPage(cnScope().query)); +const renderClearnetPager = ({ base, params = {}, page, pages, per, total, pageParam = 'page' }) => { + const query = String(cnScope().query || ''); + const sizes = !!per && mv().showPageSizes(total, query); + if (!(pages > 1) && !sizes) return ''; + const chosen = new URLSearchParams(query).has('perPage'); + const href = (n, size) => { + const q = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) if (v) q.set(k, String(v)); + if (size || chosen) q.set('perPage', size || per); + if (n > 1) q.set(pageParam, String(n)); + const s = q.toString(); + return escapeHtml(s ? `${base}?${s}` : base); + }; + const sizeLinks = sizes + ? `${escapeHtml(cnText('searchPerPageLabel', 'Results per page'))}${mv().LIST_PAGE_SIZES.map(s => `${escapeHtml(mv().pageSizeLabel(s))}`).join('')}` + : ''; + const info = pages > 1 ? `${escapeHtml(cnText('cnPageOf', 'Page {page} of {pages}').replace('{page}', String(page)).replace('{pages}', String(pages)))}` : ''; + const prev = page > 1 ? `${escapeHtml(cnText('cnPrevPage', '← Previous'))}` : ''; + const next = page < pages ? `${escapeHtml(cnText('cnNextPage', 'Next →'))}` : ''; + return `
${sizeLinks}${info}${prev}${next}
`; +}; + const renderClearnetMediaView = ({ kind, item }) => { const blob = blobUrl(item.url); - const title = escapeHtml(item.title || 'Untitled'); - const desc = escapeHtml(item.description || ''); + const title = escapeHtml(item.title || cnText('cnUntitled', 'Untitled')); + const desc = renderRichText(item.description || '', { wikiLinks: item.wikiLinks instanceof Map ? item.wikiLinks : null }); const dateStr = item.createdAt ? escapeHtml(new Date(item.createdAt).toISOString().slice(0, 10)) : ''; const extraCss = ` .cn-media-meta{color:var(--fg-dim);font-size:13px;margin-bottom:16px;display:flex;gap:14px;flex-wrap:wrap;align-items:baseline} @@ -259,9 +499,11 @@ const renderClearnetMediaView = ({ kind, item }) => { .cn-media-frame{margin:16px 0} .cn-media-frame img{max-width:100%;height:auto;border-radius:6px;border:1px solid var(--border);display:block} .cn-media-frame audio,.cn-media-frame video{width:100%;max-width:100%;display:block;border-radius:6px;background:#000} -.cn-media-frame .cn-media-doc{display:inline-block;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:10px 18px;color:var(--fg);text-decoration:none} +.cn-media-frame .cn-media-doc{display:inline-block;max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:10px 18px;color:var(--fg);text-decoration:none} .cn-media-frame .cn-media-doc:hover{border-color:var(--fg)} +.cn-media-frame .cn-media-docs{display:flex;flex-direction:column;align-items:flex-start;gap:10px} `; + const fileKind = kind === 'document' || kind === 'torrent' || kind === 'file'; let mediaHtml = ''; if (blob) { if (kind === 'image') { @@ -270,21 +512,38 @@ const renderClearnetMediaView = ({ kind, item }) => { mediaHtml = ``; } else if (kind === 'video') { mediaHtml = ``; - } else if (kind === 'document' || kind === 'torrent') { + } else if (kind === 'file' || kind === 'torrent') { + const dlName = (v) => String(v || '').replace(/[\\/:*?"<>|\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 120); + const dlLink = (href, name) => `⇩ ${escapeHtml(name)}`; + const original = kind === 'file' ? blob : blobUrl(item.source); + const originalName = dlName(kind === 'file' ? item.fileName : item.sourceName) || dlName(item.title) || 'file'; + const torrentBlob = kind === 'file' ? blobUrl(item.torrentUrl) : blob; + const torrentName = `${kind === 'file' ? originalName : (dlName(item.title) || 'download').replace(/\.torrent$/i, '')}.torrent`; + mediaHtml = `
${[original ? dlLink(original, originalName) : '', torrentBlob ? dlLink(torrentBlob, torrentName) : ''].filter(Boolean).join('')}
`; + } else if (kind === 'document') { mediaHtml = `⇩ ${title}`; + } else { + mediaHtml = `${title}`; } } const body = `
+ ${renderKindTag(kind)} ${dateStr ? `📅 ${dateStr}` : ''} + ${(Array.isArray(item.details) ? item.details : []).map(d => `${escapeHtml(String(d))}`).join('')} + ${item.price ? `${escapeHtml(String(item.price))} ECO` : ''} + ${LICENSED_KINDS.has(kind) ? renderClearnetLicense(item.license) : ''}
-

${title}

- ${mediaHtml ? `
${mediaHtml}
` : ''} + ${item.title ? `

${title}

` : ''} +
+ ${mediaHtml && !fileKind ? `
${mediaHtml}
` : ''} ${desc ? `

${desc}

` : ''} + ${mediaHtml && fileKind ? `
${mediaHtml}
` : ''} + ${renderTagChips(item.tags)} `; return renderClearnetPage({ - title: `${title} — Oasis`, - ogTitle: item.title || 'Oasis', + title: `${item.title || (kind ? kindLabel(kind) : 'Oasis')} | Oasis`, + ogTitle: item.title || (kind ? kindLabel(kind) : 'Oasis'), ogDescription: item.description || '', ogImage: (kind === 'image') ? blob : null, extraCss, @@ -293,18 +552,82 @@ const renderClearnetMediaView = ({ kind, item }) => { }); }; +const renderClearnetPodcastView = ({ channel }) => { + const title = escapeHtml(channel.title || cnText('cnUntitled', 'Untitled')); + const desc = renderRichText(channel.description || ''); + const cover = channel.cover && channel.cover.blobId ? blobUrl(channel.cover.blobId) : null; + const coverIsVideo = !!(channel.cover && channel.cover.kind === 'video'); + const dateStr = channel.createdAt ? escapeHtml(new Date(channel.createdAt).toISOString().slice(0, 10)) : ''; + const extraCss = ` +.cn-media-meta{color:var(--fg-dim);font-size:13px;margin-bottom:16px;display:flex;gap:14px;flex-wrap:wrap;align-items:baseline} +.cn-media-title{color:var(--fg);font-size:26px;font-weight:700;margin:0 0 12px 0} +.cn-media-desc{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;margin:16px 0} +.cn-podcast-cover{max-width:240px;width:100%;border-radius:6px;border:1px solid var(--border);display:block;margin:12px 0} +.cn-episode{border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin:12px 0;background:var(--bg-elev)} +.cn-episode h2{margin:0 0 8px 0;font-size:17px;color:var(--fg)} +.cn-episode .cn-episode-meta{color:var(--fg-dim);font-size:12px;margin-bottom:8px} +.cn-episode audio,.cn-episode video{width:100%;max-width:100%;display:block;border-radius:6px;background:#000} +.cn-episode p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.5;margin:8px 0 0 0} +`; + const episodes = Array.isArray(channel.episodes) ? channel.episodes.slice().reverse() : []; + const epHtml = episodes.map(ep => { + const src = ep.media && ep.media.blobId ? blobUrl(ep.media.blobId) : null; + const player = !src ? '' : (ep.media.kind === 'video' + ? `` + : ``); + const epDate = ep.createdAt ? escapeHtml(new Date(ep.createdAt).toISOString().slice(0, 10)) : ''; + return `

#${Number(ep.number) || 0} · ${escapeHtml(ep.title || '')}

${epDate}
${player}${ep.description ? `

${renderRichText(ep.description)}

` : ''}
`; + }).join(''); + const body = ` +
${renderKindTag('podcast')}${dateStr ? `📅 ${dateStr}` : ''}🎧 ${episodes.length}
+

${title}

+ ${cover ? (coverIsVideo ? `` : `${title}`) : ''} + ${desc ? `

${desc}

` : ''} + ${epHtml ? `
${epHtml}` : ''} +`; + return renderClearnetPage({ + title: `${channel.title || cnText('cnUntitled', 'Untitled')} | Oasis`, + ogTitle: channel.title || 'Oasis', + ogDescription: channel.description || '', + ogImage: cover && !coverIsVideo ? cover : null, + extraCss, + body, + hubFeedId: channel.author || null + }); +}; + module.exports = { + cnZoom, + kindLabel, + renderTagChips, + renderClearnetPodcastView, escapeHtml, + renderRichText, + renderKindTag, blobIdOf, blobUrl, renderReachChip, + renderTribeOriginChip, + renderClearnetSelector, + renderClearnetSwitch, renderFediverseReach, renderContentStats, renderEncryptedChip, + renderTransportChip, + LICENSES, + licenseInfo, + normalizeLicense, + renderLicenseChip, + renderLicenseSelect, + renderClearnetLicense, renderDoubleEncryptionChip, renderClearnetUrlBlock, renderClearnetSearchForm, + renderClearnetHubSearch, renderClearnetPage, renderClearnetNotFound, + CLEARNET_PAGER_CSS, + paginateClearnet, + renderClearnetPager, renderClearnetMediaView }; diff --git a/src/views/comments_view.js b/src/views/comments_view.js new file mode 100644 index 0000000..b71f73f --- /dev/null +++ b/src/views/comments_view.js @@ -0,0 +1,98 @@ +const { div, p, h2, span, a, form, input, textarea, button, label, br, details, summary } = require("../server/node_modules/hyperaxe"); +const { TEXT_CAP } = require('../backend/long_text'); +const moment = require("../server/node_modules/moment"); +const { i18n, userLink, renderHideIcon, renderIcon } = require("./main_views"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledText } = require("../backend/renderStyledText"); + +const COMMENT_ICON = "✑"; + +let commentsOpenByDefault = false; +const setCommentsOpen = (value) => { commentsOpenByDefault = !!value; }; + +const visibleComments = (comments) => (Array.isArray(comments) ? comments : []).filter(c => { + const t = c && c.value && c.value.content && c.value.content.text; + return t && String(t).trim(); +}); + +const renderCommentCard = (c, extra = null, returnTo = null) => { + const author = (c && c.value && c.value.author) || ""; + const ts = (c && c.value && c.value.timestamp) || (c && c.timestamp); + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; + const relDate = ts ? moment(ts).fromNow() : ""; + const content = (c && c.value && c.value.content) || {}; + const rootId = content.fork || content.root || null; + const text = content.text || ""; + const own = !!author && !!(config && config.keys) && author === config.keys.id; + + return div({ class: own ? "votations-comment-card own-comment" : "votations-comment-card", id: c && c.key ? c.key : undefined }, + own && c.key + ? form({ method: "POST", action: `/comments/delete/${encodeURIComponent(c.key)}`, class: "comment-delete-form" }, + button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.chatDelete }, renderIcon("close"))) + : null, + !own && c.key + ? form({ method: "POST", action: `/hide/${encodeURIComponent(c.key)}`, class: "comment-delete-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo || (rootId ? `/thread/${encodeURIComponent(rootId)}` : "/activity") }), + input({ type: "hidden", name: "label", value: String(text).replace(/\s+/g, " ").trim().slice(0, 80) }), + button({ type: "submit", class: "btn-singleview btn-hide", title: i18n.hideContent }, renderHideIcon())) + : null, + span({ class: "created-at" }, + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" + ), + p({ class: "votations-comment-text" }, ...renderStyledText(String(text), { zoomImages: true })), + extra + ); +}; + +const renderCommentsSection = ({ action, comments = [], returnTo = null, closedNote = null, extraClass = "", open = false, commentExtra = null } = {}) => { + const list = visibleComments(comments); + return details({ class: "comments-collapse" + (extraClass ? ` ${extraClass}` : ""), ...((open || commentsOpenByDefault) ? { open: true } : {}), ...(process.env.OASIS_MOBILE === '1' ? { id: "comments" } : {}) }, + summary({ class: list.length > 0 ? "comments-summary engage-on" : "comments-summary" }, + span({ class: "comments-summary-icon" }, COMMENT_ICON), + span({ class: "comments-summary-count" }, `(${list.length})`) + ), + div({ class: "vote-comments-section" }, + list.length + ? div({ class: "comments-list" }, ...list.map((c, i) => { + const card = renderCommentCard(c, typeof commentExtra === "function" ? commentExtra(c) : null, returnTo); + return i === list.length - 1 ? div({ id: "comments-latest", class: "comments-latest-anchor" }, card) : card; + })) + : (closedNote ? null : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)), + closedNote + ? p({ class: "muted" }, closedNote) + : div({ class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), + form({ method: "POST", action, class: "comment-form", enctype: "multipart/form-data" }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + textarea({ name: "text", rows: 4, maxlength: String(TEXT_CAP), class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder, required: true }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) + ) + ) + ) + ); +}; + +const renderCommentsLink = ({ href, count = 0 } = {}) => { + if (!href) return null; + const total = Number(count) || 0; + const target = process.env.OASIS_MOBILE === '1' ? `${href}#comments` : href; + return a({ href: target, class: total > 0 ? "comments-summary engage-on comments-link" : "comments-summary comments-link" }, + span({ class: "comments-summary-icon" }, COMMENT_ICON), + span({ class: "comments-summary-count" }, `(${total})`) + ); +}; + +module.exports = { + COMMENT_ICON, + renderCommentsLink, + visibleComments, + renderCommentCard, + renderCommentsSection, + setCommentsOpen +}; diff --git a/src/views/courts_view.js b/src/views/courts_view.js index ef8d681..5f9fa9d 100644 --- a/src/views/courts_view.js +++ b/src/views/courts_view.js @@ -1,6 +1,7 @@ const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe'); const moment = require('../server/node_modules/moment'); -const { template, i18n, userLink, renderStateChip } = require('./main_views'); +const { template, i18n, userLink, renderStateChip, renderModuleStatsBy, paged } = require('./main_views'); +const { renderStyledText, safeExternalHref } = require('../backend/renderStyledText'); const CourtsE2EChip = () => renderStateChip('encrypted', '🔒', i18n.encryptedChipLabel || 'E2E'); @@ -31,6 +32,19 @@ const CASE_TITLE_PRESETS = [ 'Governance dispute' ]; +const PRESET_KEYS = { + 'Minor conflict': 'courtsPresetMinorConflict', + 'Moderate conflict': 'courtsPresetModerateConflict', + 'Severe conflict': 'courtsPresetSevereConflict', + 'Harassment or abuse': 'courtsPresetHarassment', + 'Content moderation': 'courtsPresetContentModeration', + 'Ban or restriction': 'courtsPresetBanRestriction', + 'Governance dispute': 'courtsPresetGovernanceDispute' +}; +const presetLabel = (t) => i18n[PRESET_KEYS[t]] || t; +const caseTitle = (title) => CASE_TITLE_PRESETS.reduce((acc, t) => acc.split(t).join(presetLabel(t)), String(title || '')); +const statusLabel = (status) => String(i18n[`courtsStatus${String(status || '').toUpperCase()}`] || status || '').toUpperCase(); + const FILTERS = [ { value: 'cases', key: 'courtsFilterCases' }, { value: 'mycases', key: 'courtsFilterMyCases' }, @@ -41,12 +55,12 @@ const FILTERS = [ { value: 'open', key: 'courtsFilterOpenCase' } ]; -const Tabs = (active) => +const Tabs = (active, emptyCourts = false) => div( { class: 'filters' }, form( { method: 'GET', action: '/courts' }, - FILTERS.map((f) => { + FILTERS.filter((f) => !emptyCourts || f.value === 'open' || f.value === 'rules' || (f.value === active && active !== 'cases')).map((f) => { const isOpen = f.value === 'open'; const cls = isOpen @@ -67,7 +81,7 @@ const Tabs = (active) => ) ); -const CaseForm = () => +const CaseForm = (prefill = {}) => div( { class: 'div-center' }, h2(i18n.courtsCaseFormTitle), @@ -82,14 +96,15 @@ const CaseForm = () => type: 'text', name: 'titleSuffix', required: true, - placeholder: 'Subject or short description' + placeholder: i18n.courtsCaseSubjectPlaceholder, + value: prefill.titleSuffix || '' }), br(), - label('Case type'), + label(i18n.courtsCaseType), br(), select( { name: 'titlePreset', required: true }, - CASE_TITLE_PRESETS.map((t) => option({ value: t }, t)) + CASE_TITLE_PRESETS.map((t) => option({ value: t, ...(prefill.titlePreset === t ? { selected: true } : {}) }, presetLabel(t))) ), br(), br(), @@ -101,7 +116,8 @@ const CaseForm = () => placeholder: i18n.courtsCaseRespondentPh, required: true, pattern: '^@[A-Za-z0-9+/]+=*\\.ed25519$', - title: i18n.courtsRespondentInvalid || 'Must be a valid SSB ID (@...ed25519)' + title: i18n.courtsRespondentInvalid || 'Must be a valid SSB ID (@...ed25519)', + value: prefill.respondentId || '' }), br(), label(i18n.courtsCaseMethod), @@ -109,7 +125,7 @@ const CaseForm = () => select( { name: 'method', required: true }, ['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY'].map((m) => - option({ value: m }, i18n[`courtsMethod${m}`]) + option({ value: m, ...(prefill.method === m ? { selected: true } : {}) }, i18n[`courtsMethod${m}`]) ) ), br(), @@ -162,7 +178,7 @@ const EvidenceForm = (caseId) => }), br(), br(), - label(i18n.uploadMedia || 'Upload media (max-size: 50MB)'), + label(i18n.uploadMedia || 'Upload media (max-size: 75MB)'), br(), input({ type: 'file', name: 'image' }), br(), @@ -323,31 +339,6 @@ const AcceptSettlementForm = (caseId) => button({ type: 'submit', class: 'create-button' }, i18n.courtsSettlementAcceptBtn) ); -const VerdictVoteForm = (caseId) => - div( - { class: 'div-center' }, - h2(i18n.courtsVerdictVoteTitle), - form( - { - method: 'POST', - action: `/courts/cases/${encodeURIComponent(caseId)}/verdict/vote` - }, - label(i18n.courtsVerdictVoteLabel), - br(), - select( - { name: 'decision' }, - option({ value: 'ACCEPT' }, i18n.courtsVerdictVoteAccept), - option({ value: 'REJECT' }, i18n.courtsVerdictVoteReject) - ), - br(), - br(), - button( - { type: 'submit', class: 'create-button' }, - i18n.courtsVerdictVoteSubmit - ) - ) - ); - const shortId = (id) => { const s = String(id || ''); if (!s) return ''; @@ -359,60 +350,9 @@ const UserLinkCompact = (id) => id ? userLink(id) : span(''); const UserLinkFull = (id) => id ? userLink(id) : span(''); const renderRichTextNodes = (raw) => { - const text = String(raw || ''); + const text = String(raw || '').trim(); if (!text) return []; - const nodes = []; - let remaining = text; - const imgRegex = /!\[[^\]]*]\(([^)]+)\)/; - const linkRegex = /\[([^\]]+)]\((https?:\/\/[^)]+)\)/; - while (remaining.length) { - const imgMatch = imgRegex.exec(remaining); - const linkMatch = linkRegex.exec(remaining); - let next = null; - let type = null; - if (imgMatch && (!linkMatch || imgMatch.index < linkMatch.index)) { - next = imgMatch; - type = 'img'; - } else if (linkMatch) { - next = linkMatch; - type = 'link'; - } - if (!next) { - if (remaining.trim()) nodes.push(p(remaining)); - break; - } - const idx = next.index; - if (idx > 0) { - const before = remaining.slice(0, idx); - if (before.trim()) nodes.push(p(before)); - } - if (type === 'img') { - const ref = next[1]; - nodes.push( - img({ - class: 'evidence-image', - src: `/blob/${encodeURIComponent(ref)}`, - alt: 'evidence' - }) - ); - } else { - const labelText = next[1]; - const url = next[2]; - nodes.push( - a( - { - class: 'evidence-link', - href: url, - target: '_blank', - rel: 'noopener noreferrer' - }, - labelText - ) - ); - } - remaining = remaining.slice(idx + next[0].length); - } - return nodes; + return [p(...renderStyledText(text))]; }; const RichTextBlock = (raw) => { @@ -468,11 +408,11 @@ const CaseCard = (c) => { select( { name: 'preference' }, option( - { value: 'YES', selected: c.myPublicPreference === true }, + { value: 'YES', ...(c.myPublicPreference === true ? { selected: true } : {})}, i18n.courtsPublicPrefYes ), option( - { value: 'NO', selected: c.myPublicPreference === false }, + { value: 'NO', ...(c.myPublicPreference === false ? { selected: true } : {})}, i18n.courtsPublicPrefNo ) ), @@ -527,7 +467,7 @@ const CaseCard = (c) => { { class: 'card-header' }, div( { class: 'card-header__meta' }, - h2(c.title || ''), + h2(caseTitle(c.title)), div({ class: 'card-chips-row' }, CourtsE2EChip()), div( { class: 'card-field' }, @@ -537,7 +477,7 @@ const CaseCard = (c) => { div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus + ': '), - span({ class: 'card-value' }, String(c.status || '')) + span({ class: 'card-value' }, statusLabel(c.status)) ), c.answerBy ? div( @@ -683,11 +623,11 @@ const MyCaseCard = (c) => { select( { name: 'preference' }, option( - { value: 'YES', selected: c.myPublicPreference === true }, + { value: 'YES', ...(c.myPublicPreference === true ? { selected: true } : {})}, i18n.courtsPublicPrefYes ), option( - { value: 'NO', selected: c.myPublicPreference === false }, + { value: 'NO', ...(c.myPublicPreference === false ? { selected: true } : {})}, i18n.courtsPublicPrefNo ) ), @@ -745,13 +685,13 @@ const MyCaseCard = (c) => { div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThCase + ': '), - span({ class: 'card-value' }, c.title || '') + span({ class: 'card-value' }, caseTitle(c.title)) ), div({ class: 'card-chips-row' }, CourtsE2EChip()), div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus + ': '), - span({ class: 'card-value' }, String(c.status || '')) + span({ class: 'card-value' }, statusLabel(c.status)) ), div( { class: 'card-field' }, @@ -899,8 +839,8 @@ const CasesTable = (rows = [], opts = {}) => { const bodyRows = rows.map((c) => { const role = showRole ? roleTextForCase(c) : ''; return tr( - td(c.title || ''), - td( + td({ 'data-label': i18n.courtsThCase }, caseTitle(c.title)), + td({ 'data-label': i18n.courtsAccuser }, c.accuser ? a( { @@ -911,10 +851,10 @@ const CasesTable = (rows = [], opts = {}) => { ) : '' ), - td(methodLabel(c.method)), - td(c.createdAt ? fmt(c.createdAt) : ''), - showRole ? td(role) : null, - td( + td({ 'data-label': i18n.courtsCaseMethod }, methodLabel(c.method)), + td({ 'data-label': i18n.courtsThCreatedAt }, c.createdAt ? fmt(c.createdAt) : ''), + showRole ? td({ 'data-label': i18n.courtsThRole || 'Role' }, role) : null, + td({ class: 'table-row-action' }, c.id ? form( { @@ -958,10 +898,10 @@ const NominationsTable = (nominations = [], currentUserId = '') => { currentUserId && String(n.judgeId || '') === String(currentUserId || ''); return tr( - td(userLink(n.judgeId)), - td(String(n.supports || 0)), - td(fmt(n.createdAt)), - td( + td({ 'data-label': i18n.courtsThJudge }, userLink(n.judgeId)), + td({ 'data-label': i18n.courtsThSupports }, String(n.supports || 0)), + td({ 'data-label': i18n.courtsThDate }, fmt(n.createdAt)), + td({ class: 'table-row-action' }, isSelf ? span('') : form( @@ -1022,7 +962,7 @@ const HistoryList = (rows = []) => { span({ class: 'card-label' }, i18n.courtsThStatus + ': '), span({ class: 'card-value' }, String(hh.status || '')) ), - h2(hh.title || ''), + h2(caseTitle(hh.title)), canShowDescription ? RichTextBlock(hh.description || '') : null, form( { @@ -1063,21 +1003,23 @@ const RulesContent = () => { ); }; -const CaseSearch = (filter, search = '') => +const CaseSearch = (filter, search = '', items = null) => div( - { class: 'filters' }, + { class: 'filters activity-filter-chips activity-toolbar-row' }, + items ? renderModuleStatsBy(items, c => c.status === 'OPEN' ? 'OPEN' : c.status === 'IN_PROGRESS' ? 'IN_PROGRESS' : 'DONE', [{ value: 'OPEN', label: i18n.taskStatusOpen }, { value: 'IN_PROGRESS', label: i18n.taskStatusInProgress }, { value: 'DONE', label: i18n.taskStatusClosed }]) : null, form( - { method: 'GET', action: '/courts' }, + { method: 'GET', action: '/courts', class: 'filter-box' }, input({ type: 'hidden', name: 'filter', value: filter }), input({ type: 'text', name: 'search', placeholder: i18n.searchCasesPlaceholder, - value: search || '' + value: search || '', + class: 'filter-box__input' }), - br(), - button({ type: 'submit' }, i18n.applyFilters), - br() + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ); @@ -1133,7 +1075,7 @@ const CaseDetailsBlock = (c) => { { class: 'card-header' }, div( { class: 'card-header__meta' }, - h2(c.title || ''), + h2(caseTitle(c.title)), div({ class: 'card-chips-row' }, CourtsE2EChip()), c.method ? div( @@ -1150,7 +1092,7 @@ const CaseDetailsBlock = (c) => { div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus + ': '), - span({ class: 'card-value' }, String(c.status || '')) + span({ class: 'card-value' }, statusLabel(c.status)) ), c.createdAt ? div( @@ -1241,8 +1183,8 @@ const CaseDetailsBlock = (c) => { a( { class: 'evidence-link', - href: e.link, - target: '_blank', + href: safeExternalHref(e.link), + target: '_blank', rel: 'noopener noreferrer', rel: 'noopener noreferrer' }, e.link @@ -1360,29 +1302,29 @@ const courtsView = async (state) => { i18n.courtsTitle, section( div( - { class: 'tags-header' }, + { class: 'tags-header module-header-line' }, h2(i18n.courtsTitle), p(i18n.courtsDescription) ), - Tabs(filter), - filter === 'cases' ? CaseSearch(filter, search) : null + Tabs(filter, cases.length === 0 && myCases.length === 0 && history.length === 0 && nominations.length === 0 && !String(search || '').trim()), + filter === 'cases' && ((Array.isArray(cases) && cases.length > 0) || String(search || '').trim()) ? CaseSearch(filter, search, cases) : null ), section( - filter === 'cases' ? CasesTable(cases) : null, + filter === 'cases' ? CasesTable(paged(cases)) : null, filter === 'mycases' ? myCases.length - ? CasesTable(myCases, { showRole: true }) + ? CasesTable(paged(myCases), { showRole: true }) : div({ class: 'empty' }, p(i18n.courtsNoMyCases)) : null, filter === 'actions' ? myCases.length - ? MyCasesList(myCases) + ? MyCasesList(paged(myCases)) : div({ class: 'empty' }, p(i18n.courtsNoMyCases)) : null, - filter === 'judges' ? JudgesSection(nominations, userId) : null, - filter === 'history' ? HistoryList(history) : null, + filter === 'judges' ? JudgesSection(paged(nominations), userId) : null, + filter === 'history' ? HistoryList(paged(history)) : null, filter === 'rules' ? RulesContent() : null, - filter === 'open' ? CaseForm() : null + filter === 'open' ? CaseForm(state.prefill || {}) : null ) ); }; @@ -1393,7 +1335,7 @@ const courtsCaseView = async (state) => { i18n.courtsTitle, section( div( - { class: 'tags-header' }, + { class: 'tags-header module-header-line' }, h2(i18n.courtsTitle), p(i18n.courtsDescription) ), diff --git a/src/views/cv_view.js b/src/views/cv_view.js index 9725175..b6c8ff5 100644 --- a/src/views/cv_view.js +++ b/src/views/cv_view.js @@ -1,6 +1,7 @@ -const { form, button, div, h2, p, section, textarea, label, input, br, img, a, select, option, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderVisibilityChip } = require('./main_views'); -const { renderUrl } = require('../backend/renderUrl'); +const { form, button, div, h2, p, section, textarea, label, input, br, img, a, select, option, span, table, tr, td } = require("../server/node_modules/hyperaxe"); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLink, renderVisibilityChip, renderDocumentActions } = require('./main_views'); +const { renderStyledText } = require('../backend/renderStyledText'); const generateCVBox = (label, content, className) => { return div({ class: `cv-box ${className}` }, @@ -9,27 +10,13 @@ const generateCVBox = (label, content, className) => { ); }; -const generateTags = (tags) => { - return tags && tags.length - ? div( - tags.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null; -}; - exports.createCVView = async (cv = {}, editMode = false) => { - const title = editMode ? i18n.cvEditSectionTitle : i18n.cvCreateSectionTitle; + const title = i18n.cvTitle; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.cvDescription) ), @@ -40,38 +27,54 @@ exports.createCVView = async (cv = {}, editMode = false) => { enctype: "multipart/form-data" }, + div({ class: "cv-box cv-ai-box" }, + div({ class: "poll-switch" }, + input({ type: "hidden", name: "aiManaged", value: "0" }), + label( + input({ type: "checkbox", name: "aiManaged", value: "1", ...(cv.aiManaged === false ? {} : { checked: true }) }), + " ", i18n.cvAiManaged + ) + ), + div({ class: "cv-threshold-row" }, + label({ for: "cv_match_threshold" }, `${i18n.cvMatchThreshold}: `), + input({ type: "number", id: "cv_match_threshold", name: "matchThreshold", min: "0", max: "100", step: "5", value: String(cv.matchThreshold != null ? cv.matchThreshold : 80) }) + ) + ), + generateCVBox(i18n.cvPersonal, [ label(i18n.cvNameLabel), br(), input({ type: "text", name: "name", required: true, value: cv.name || "" }), br(), label(i18n.cvDescriptionLabel), br(), - textarea({ name: "description", required: true, rows: 4 }, cv.description || ""), br(), + textarea({ maxlength: "5000", name: "description", required: true, rows: 4 }, cv.description || ""), br(), label(i18n.cvLanguagesLabel), br(), input({ type: "text", name: "languages", value: cv.languages || "" }), br(), label(i18n.cvPhotoLabel), br(), input({ type: "file", name: "image" }), br(), br(), + label(i18n.cvPdfLabel), br(), + input({ type: "file", name: "cvPdf", accept: "application/pdf" }), br(), br(), label(i18n.cvPersonalExperiencesLabel), br(), - textarea({ name: "personalExperiences", rows: 4 }, cv.personalExperiences || ""), br(), + textarea({ maxlength: "5000", name: "personalExperiences", rows: 4 }, cv.personalExperiences || ""), br(), label(i18n.cvPersonalSkillsLabel), br(), input({ type: "text", name: "personalSkills", required: true, value: (cv.personalSkills || []).join(", ") }), br() ], "personal"), generateCVBox(i18n.cvOasis, [ label(i18n.cvOasisExperiencesLabel), br(), - textarea({ name: "oasisExperiences", rows: 4 }, cv.oasisExperiences || ""), br(), + textarea({ maxlength: "5000", name: "oasisExperiences", rows: 4 }, cv.oasisExperiences || ""), br(), label(i18n.cvOasisSkillsLabel), br(), input({ type: "text", name: "oasisSkills", value: (cv.oasisSkills || []).join(", ") }), br() ], "oasis"), generateCVBox(i18n.cvEducational, [ label(i18n.cvEducationExperiencesLabel), br(), - textarea({ name: "educationExperiences", rows: 4 }, cv.educationExperiences || ""), br(), + textarea({ maxlength: "5000", name: "educationExperiences", rows: 4 }, cv.educationExperiences || ""), br(), label(i18n.cvEducationalSkillsLabel), br(), input({ type: "text", name: "educationalSkills", value: (cv.educationalSkills || []).join(", ") }), br() ], "education"), generateCVBox(i18n.cvProfessional, [ label(i18n.cvProfessionalExperiencesLabel), br(), - textarea({ name: "professionalExperiences", rows: 4 }, cv.professionalExperiences || ""), br(), + textarea({ maxlength: "5000", name: "professionalExperiences", rows: 4 }, cv.professionalExperiences || ""), br(), label(i18n.cvProfessionalSkillsLabel), br(), input({ type: "text", name: "professionalSkills", value: (cv.professionalSkills || []).join(", ") }), br() ], "professional"), @@ -81,19 +84,19 @@ exports.createCVView = async (cv = {}, editMode = false) => { input({ type: "text", name: "location", required: true, value: cv.location || "UNKNOWN" }), br(), label(i18n.cvStatusLabel), br(), select({ name: "status", required: true }, - option({ value: "AVAILABLE", selected: cv.status === "AVAILABLE FOR COLLABORATION" }, "AVAILABLE FOR COLLABORATION"), - option({ value: "UNAVAILABLE", selected: cv.status === "NOT CURRENTLY AVAILABLE" }, "NOT CURRENTLY AVAILABLE"), - option({ value: "LOOKING FOR WORK", selected: !cv.status || cv.status === "LOOKING FOR WORK" }, "LOOKING FOR WORK") + option({ value: "AVAILABLE", ...(cv.status === "AVAILABLE FOR COLLABORATION" ? { selected: true } : {})}, "AVAILABLE FOR COLLABORATION"), + option({ value: "UNAVAILABLE", ...(cv.status === "NOT CURRENTLY AVAILABLE" ? { selected: true } : {})}, "NOT CURRENTLY AVAILABLE"), + option({ value: "LOOKING FOR WORK", ...(!cv.status || cv.status === "LOOKING FOR WORK" ? { selected: true } : {})}, "LOOKING FOR WORK") ), br(), br(), label(i18n.cvPreferencesLabel), br(), select({ name: "preferences", required: true }, - option({ value: "IN PERSON", selected: cv.preferences === "IN-PERSON ONLY" }, "IN-PERSON ONLY"), - option({ value: "REMOTE WORKING", selected: !cv.preferences || cv.preferences === "REMOTE WORKING" }, "REMOTE-WORKING") + option({ value: "IN PERSON", ...(cv.preferences === "IN-PERSON ONLY" ? { selected: true } : {})}, "IN-PERSON ONLY"), + option({ value: "REMOTE WORKING", ...(!cv.preferences || cv.preferences === "REMOTE WORKING" ? { selected: true } : {})}, "REMOTE-WORKING") ), br(), br(), label(i18n.visibilityLabel || "Visibility"), br(), select({ name: "visibility" }, - option({ value: "PUBLIC", selected: (cv.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", selected: cv.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") + option({ value: "PUBLIC", ...((cv.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(cv.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") ), br() ], "availability"), @@ -104,154 +107,162 @@ exports.createCVView = async (cv = {}, editMode = false) => { ) }; -exports.cvView = async (cv) => { +exports.cvView = async (cv, certificates = []) => { const title = i18n.cvTitle; if (!cv) { return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.cvDescription) ), - div({ class: "no-cv" }, - p(i18n.cvNoCV), + div({ class: "filters" }, form({ method: "GET", action: "/cv/create" }, - button({ type: "submit" }, i18n.cvCreateButton) + button({ type: "submit", class: "create-button" }, i18n.cvCreateButton) ) - ) + ), + div({ class: "no-content-box" }, p(i18n.cvNoCV)), + Array.isArray(certificates) && certificates.length + ? div({ class: "cv-section-block school-certificates" }, + h2(i18n.schoolCertificates), + certificates.map(cert => + div({ class: "school-certificate" }, + span("🎓 "), + a({ href: `/school/course/${encodeURIComponent(cert.courseId)}` }, cert.courseTitle || cert.courseId), + span(" — "), + userLink(cert.author), + span({ class: "school-certificate-date" }, ` (${moment(cert.createdAt).format("YYYY/MM/DD")})`), + a({ href: `/school/certificate/pdf/${encodeURIComponent(cert.courseId)}/${encodeURIComponent(cert.id)}`, class: "filter-btn school-cert-pdf" }, `⬇ ${i18n.schoolCertificatePdf || "PDF"}`) + ) + ) + ) + : null ) ) } - const hasPersonal = cv.contact || cv.name || cv.description || cv.photo || typeof cv.oasisContributor === "boolean" || (cv.personalSkills && cv.personalSkills.length); - const hasPersonalExp = cv.personalExperiences; - const hasOasis = cv.oasisExperiences || (cv.oasisSkills && cv.oasisSkills.length); - const hasEducational = cv.educationExperiences || cv.languages || (cv.educationalSkills && cv.educationalSkills.length); - const hasProfessional = cv.professionalExperiences || (cv.professionalSkills && cv.professionalSkills.length); - const hasAvailability = cv.location || cv.status || cv.preferences; + const skills = (list) => (Array.isArray(list) ? list.filter(Boolean) : []); + const allSkills = [ + ...skills(cv.personalSkills), + ...skills(cv.educationalSkills), + ...skills(cv.professionalSkills), + ...skills(cv.oasisSkills) + ]; + const uniqueSkills = Array.from(new Set(allSkills)); + + const renderSkillTags = (list) => skills(list).length + ? div({ class: "card-tags" }, + skills(list).map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) + ) + : null; + + const renderBlock = (heading, body, list) => (body || skills(list).length) + ? div({ class: "cv-box" }, + h2({ class: "job-section-title" }, heading), + body ? p({ class: "tribe-side-description" }, ...renderStyledText(String(body))) : null, + renderSkillTags(list) + ) + : null; + + const vis = (cv.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; + const nextVis = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC'; + + const infoRows = []; + const pushRow = (labelText, valueNode) => + infoRows.push(tr( + td({ class: "tribe-info-label" }, labelText), + td({ class: "tribe-info-value" }, valueNode) + )); + if (cv.location) pushRow(i18n.cvLocationLabel, cv.location); + if (cv.status) pushRow(i18n.cvStatusLabel, cv.status); + if (cv.preferences) pushRow(i18n.cvPreferencesLabel, cv.preferences); + if (cv.languages) pushRow(i18n.cvLanguagesLabel, String(cv.languages).toUpperCase()); + pushRow(i18n.cvAiManaged, cv.aiManaged === false ? i18n.switchOff : `${i18n.switchOn} · ${cv.matchThreshold != null ? cv.matchThreshold : 80}%`); + pushRow(i18n.cvCreatedAt, moment(cv.createdAt).format("YYYY/MM/DD HH:mm")); + if (cv.updatedAt) pushRow(i18n.cvUpdatedAt, moment(cv.updatedAt).format("YYYY/MM/DD HH:mm")); + + const cvSide = div({ class: "tribe-side" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, cv.name || i18n.unnamed || 'Anonymous') + ), + cv.photo + ? img({ loading: 'lazy', src: `/blob/${encodeURIComponent(cv.photo)}`, class: "cv-photo tribe-detail-image" }) + : null, + (cv.contact || cv.author) + ? div({ class: 'profile-qr' }, + a({ href: `/author/${encodeURIComponent(cv.contact || cv.author)}` }, + img({ class: 'profile-qr-img', src: `/qr/${encodeURIComponent(cv.contact || cv.author)}?size=240`, alt: 'QR' }))) + : null, + cv.contact ? p(userLink(cv.contact)) : null, + cv.pdf + ? p(a({ href: `/blob/${encodeURIComponent(cv.pdf)}`, target: "_blank", rel: "noopener", class: "filter-btn" }, "📄 " + i18n.cvPdfLabel)) + : null, + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + uniqueSkills.length + ? div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.cvSkillsCount}: ${uniqueSkills.length}`) + ) + : null, + div({ class: "tribe-side-actions cv-visibility-row housing-status-row" }, + span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), + renderVisibilityChip(vis, i18n), + form({ method: "POST", action: `/cv/visibility/${encodeURIComponent(cv.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "visibility", value: nextVis }), + button({ type: "submit", class: "filter-btn" }, + nextVis === 'PUBLIC' ? (i18n.visibilityMakePublic || 'Make public') : (i18n.visibilityMakeHidden || 'Make hidden') + ) + ) + ), + div({ class: "tribe-side-actions" }, + form({ method: "GET", action: `/cv/edit/${encodeURIComponent(cv.id)}` }, + button({ type: "submit", class: "update-btn" }, i18n.cvEditButton) + ), + form({ method: "POST", action: `/cv/delete/${encodeURIComponent(cv.id)}` }, + button({ type: "submit", class: "delete-btn" }, i18n.cvDeleteButton) + ) + ), + renderDocumentActions('cv', null) + ); + + const cvMain = div({ class: "tribe-main" }, + cv.description + ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.cvDescriptionLabel), + p({ class: "tribe-side-description" }, ...renderStyledText(String(cv.description))) + ) + : null, + renderBlock(i18n.cvPersonal, cv.personalExperiences, cv.personalSkills), + renderBlock(i18n.cvEducationalView, cv.educationExperiences, cv.educationalSkills), + renderBlock(i18n.cvProfessionalView, cv.professionalExperiences, cv.professionalSkills), + renderBlock(i18n.cvOasisContributorView, cv.oasisExperiences, cv.oasisSkills), + Array.isArray(certificates) && certificates.length + ? div({ class: "cv-section-block school-certificates" }, + h2(i18n.schoolCertificates), + certificates.map(cert => + div({ class: "school-certificate" }, + span("🎓 "), + a({ href: `/school/course/${encodeURIComponent(cert.courseId)}` }, cert.courseTitle || cert.courseId), + span(" — "), + userLink(cert.author), + span({ class: "school-certificate-date" }, ` (${moment(cert.createdAt).format("YYYY/MM/DD")})`), + a({ href: `/school/certificate/pdf/${encodeURIComponent(cert.courseId)}/${encodeURIComponent(cert.id)}`, class: "filter-btn school-cert-pdf" }, `⬇ ${i18n.schoolCertificatePdf || "PDF"}`) + ) + ) + ) + : null + ); return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.cvDescription) ), - div({ class: "cv-section" }, - div({ class: "cv-item" }, ...[ - (() => { - const vis = (cv.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; - const next = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC'; - return div({ class: "cv-visibility-row" }, - span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), - renderVisibilityChip(vis, i18n), - " ", - form({ method: "POST", action: `/cv/visibility/${encodeURIComponent(cv.id)}`, class: "inline-form" }, - input({ type: "hidden", name: "visibility", value: next }), - button({ type: "submit", class: "filter-btn" }, - next === 'PUBLIC' ? (i18n.visibilityMakePublic || 'Make public') : (i18n.visibilityMakeHidden || 'Make hidden') - ) - ) - ); - })(), - div({ class: "cv-actions" }, - form({ method: "GET", action: `/cv/edit/${encodeURIComponent(cv.id)}` }, - button({ type: "submit" }, i18n.cvEditButton) - ), - form({ method: "POST", action: `/cv/delete/${encodeURIComponent(cv.id)}` }, - button({ type: "submit", class: "danger-btn" }, i18n.cvDeleteButton) - ) - ), - div({ class: "cv-meta" }, - p(`${i18n.cvCreatedAt}: ${new Date(cv.createdAt).toLocaleString()}`), - cv.updatedAt ? p(`${i18n.cvUpdatedAt}: ${new Date(cv.updatedAt).toLocaleString()}`) : null - ), - hasPersonal ? div({ class: "cv-box personal" }, ...[ - cv.photo - ? img({ - src: `/blob/${encodeURIComponent(cv.photo)}`, - class: "cv-photo" - }) - : null, - cv.name ? h2(`${cv.name}`) : null, - (cv.contact || cv.author) - ? a({ href: `/author/${encodeURIComponent(cv.contact || cv.author)}`, class: 'inhabitant-qr-link' }, - img({ class: 'inhabitant-qr-small', src: `/qr/${encodeURIComponent(cv.contact || cv.author)}?size=120`, alt: 'QR' })) - : null, - cv.contact ? p(userLink(cv.contact)) : null, - cv.description ? p(...renderUrl(`${cv.description}`)) : null, - (cv.personalSkills && cv.personalSkills.length) - ? div( - cv.personalSkills.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null - ]) : null, - hasPersonal ? div({ class: "cv-box personal" }, ...[ - h2(i18n.cvLanguagesLabel), - cv.languages ? p(`${cv.languages.toUpperCase()}`) : null - ]) : null, - hasEducational ? div({ class: "cv-box education" }, ...[ - h2(i18n.cvEducationalView), - cv.educationExperiences ? p(...renderUrl(`${cv.educationExperiences}`)) : null, - (cv.educationalSkills && cv.educationalSkills.length) - ? div( - cv.educationalSkills.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null - ]) : null, - hasProfessional ? div({ class: "cv-box professional" }, ...[ - h2(i18n.cvProfessionalView), - cv.professionalExperiences ? p(...renderUrl(`${cv.professionalExperiences}`)) : null, - (cv.professionalSkills && cv.professionalSkills.length) - ? div( - cv.professionalSkills.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null - ]) : null, - hasOasis ? div({ class: "cv-box oasis" }, ...[ - h2(i18n.cvOasisContributorView), - cv.oasisExperiences ? p(...renderUrl(`${cv.oasisExperiences}`)) : null, - (cv.oasisSkills && cv.oasisSkills.length) - ? div( - cv.oasisSkills.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null - ]) : null, - hasAvailability ? div({ class: "cv-box availability" }, ...[ - h2(i18n.cvAvailabilityView), - cv.location ? p(`${i18n.cvLocationLabel}: ${cv.location}`) : null, - cv.status ? p(`${i18n.cvStatusLabel}: ${cv.status}`) : null, - cv.preferences ? p(`${i18n.cvPreferencesLabel}: ${cv.preferences}`) : null - ]) : null - ]) - ) + div({ class: "tribe-details" }, cvSide, cvMain) ) ); }; - diff --git a/src/views/data_view.js b/src/views/data_view.js new file mode 100644 index 0000000..4462bf4 --- /dev/null +++ b/src/views/data_view.js @@ -0,0 +1,252 @@ +const { div, h2, p, section, button, form, a, input, span, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, renderModuleStats, paged, renderIcon } = require("./main_views"); + +const KIND_LABEL = { + inhabitants: () => i18n.dataKindInhabitants, + jobs: () => i18n.dataKindJobs, + projects: () => i18n.dataKindProjects, + events: () => i18n.dataKindEvents, + tribes: () => i18n.dataKindTribes, + market: () => i18n.dataKindMarket, + housing: () => i18n.dataKindHousing, + industry: () => i18n.dataKindIndustry, + tasks: () => i18n.dataKindTasks, + reports: () => i18n.dataKindReports, + votes: () => i18n.dataKindVotes, + audios: () => i18n.audioTitle, + videos: () => i18n.videoTitle, + images: () => i18n.imageTitle, + documents: () => i18n.documentTitle, + bookmarks: () => i18n.bookmarkTitle, + torrents: () => i18n.typeTorrent, + files: () => i18n.typeFile, + chats: () => i18n.chatsTitle, + pads: () => i18n.padsTitle, + rooms: () => i18n.roomsTitle, + maps: () => i18n.mapTitle, + calendars: () => i18n.calendarsTitle, + forum: () => i18n.forumTitle, + school: () => i18n.schoolTitle, + wiki: () => i18n.wikiTitle, + emergencies: () => i18n.emergenciesTitle, + mailing: () => i18n.mailingTitle, + logistics: () => i18n.logisticsTitle, + podcasts: () => i18n.podcastsTitle, + campaigns: () => i18n.campaignsTitle, + blogs: () => i18n.blogTitle +}; + +const REASON_LABEL = { + mutual: () => i18n.dataReasonMutual, + following: () => i18n.dataReasonFollowing, + supportsYou: () => i18n.dataReasonSupportsYou, + tribe: () => i18n.dataReasonTribe, + alike: () => i18n.dataReasonAlike, + cv: () => i18n.dataReasonCv, + content: () => i18n.dataReasonContent, + pinned: () => i18n.dataReasonPinned, + related: () => i18n.dataReasonRelated, + rated: () => i18n.dataReasonRated, + near: () => i18n.dataReasonNear +}; + +const FILTER_COLUMNS = [ + ["ALL", "RECENT", "TOP"], + ["INHABITANTS", "TRIBES"], + ["VOTES", "EVENTS", "CALENDARS", "TASKS", "REPORTS", "EMERGENCIES", "MAILING", "CAMPAIGNS"], + ["MARKET", "HOUSING", "JOBS", "PROJECTS", "INDUSTRY", "LOGISTICS"], + ["BLOGS", "FORUM", "CHATS", "ROOMS", "PADS", "WIKI", "MAPS", "SCHOOL"], + ["AUDIOS", "BOOKMARKS", "DOCUMENTS", "FILES", "IMAGES", "TORRENTS", "VIDEOS", "PODCASTS"] +]; + +const kindLabel = (kind) => (KIND_LABEL[kind] ? KIND_LABEL[kind]() : String(kind || '').toUpperCase()); + +const reasonButton = (value, label, current, filter) => + form({ method: "GET", action: "/data" }, + filter && filter !== "ALL" ? input({ type: "hidden", name: "filter", value: filter }) : null, + value ? input({ type: "hidden", name: "reason", value }) : null, + button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase()) + ); + +const filterButton = (value, label, current, reason) => + form({ method: "GET", action: "/data" }, + input({ type: "hidden", name: "filter", value }), + reason ? input({ type: "hidden", name: "reason", value: reason }) : null, + button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase()) + ); + +const sectionId = (kind) => `data-${String(kind || '').toLowerCase()}`; + +const renderFilters = (q, payload = {}, kindsPresent = []) => { + const matches = Array.isArray(payload.matches) ? payload.matches : []; + const reason = String(payload.reason || ""); + const filter = String(payload.filter || "ALL").toUpperCase(); + const isKindFilter = !FILTER_COLUMNS[0].includes(filter); + if (!matches.length && !String(q || "").trim() && !reason && !isKindFilter) return null; + const avail = (Array.isArray(payload.reasonsAvail) ? payload.reasonsAvail : []).filter(r => REASON_LABEL[r]); + const present = new Set(kindsPresent); + const columns = [FILTER_COLUMNS[0]].concat( + FILTER_COLUMNS.slice(1) + .map(col => col.filter(k => present.has(k.toLowerCase()))) + .filter(col => col.length) + ); + return section( + div({ class: "activity-sub-filter" }, + reasonButton("", i18n.dataFilterAll, reason, filter), + ...avail.map(r => reasonButton(r, REASON_LABEL[r](), reason, filter)) + ), + div({ class: "activity-filter-grid" }, + ...columns.map(col => + div({ class: "activity-filter-col" }, + ...col.map(k => filterButton(k, FILTER_COLUMNS[0].includes(k) ? (i18n[k + "Button"] || k) : kindLabel(k.toLowerCase()), filter, reason)) + ) + ) + ), + div({ class: "data-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(matches.length), + form({ method: "GET", action: "/data", class: "filter-box" }, + reason ? input({ type: "hidden", name: "reason", value: reason }) : null, + filter !== "ALL" ? input({ type: "hidden", name: "filter", value: filter }) : null, + input({ type: "text", name: "q", value: q || "", placeholder: i18n.dataSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + ); +}; + +const scorePct = (score) => ((Number(score) || 0) * 100).toFixed(2).replace(/\.?0+$/, ''); +const scaleOf = (pct) => Math.min(4, Math.floor((Number(pct) || 0) / 20)); + +const renderCohesion = (c) => { + const measurable = c.entities >= 2; + const pct = measurable ? Number(c.percent) || 0 : 0; + return section( + div({ class: "tags-header module-header-line" }, + h2(i18n.dataCohesionTitle), + p(i18n.dataCohesionHint) + ), + div({ class: "data-cohesion" }, + p({ class: "data-cohesion-value" }, + span({ class: "card-label data-cohesion-label" }, `${i18n.dataCcShort}: `), + measurable ? `${c.percent}%` : "—"), + div({ class: "poll-bar data-cohesion-bar" }, + div({ class: `poll-bar-fill poll-bar-fill-${Math.round(pct / 5) * 5} data-scale-${scaleOf(pct)}` })), + div({ class: "data-cohesion-figures" }, + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.entities)), + span({ class: "data-figure-label" }, i18n.dataStatEntities) + ), + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.distinctTerms)), + span({ class: "data-figure-label" }, i18n.dataStatTerms) + ), + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.pairs)), + span({ class: "data-figure-label" }, i18n.dataStatPairs) + ), + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.isolated || 0)), + span({ class: "data-figure-label" }, i18n.dataStatIsolated) + ), + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.connected || 0)), + span({ class: "data-figure-label" }, i18n.dataStatConnected) + ) + ) + ) + ); +}; + +const TITLE_MAX = 120; +const shortTitle = (title) => { + const t = String(title || "").replace(/\s+/g, " ").trim(); + return t.length > TITLE_MAX ? `${t.slice(0, TITLE_MAX - 1)}…` : t; +}; + +const shortId = (id) => { + const value = String(id || ""); + return value.length > 14 ? `${value.slice(0, 12)}…` : value; +}; + +const renderMatchRow = (m, isBest) => { + const common = Array.isArray(m.common) ? m.common : []; + const reasons = (Array.isArray(m.reasons) ? m.reasons : []).filter(r => REASON_LABEL[r]); + return div({ class: isBest ? "data-card data-best-card" : "data-card" }, + div({ class: "data-side-top" }, + span({ class: "data-kind-chip" }, String(kindLabel(m.kind)).toUpperCase()), + span({ class: "card-label data-score-label" }, `${isBest ? i18n.dataBestMatch : (i18n.dataCcShort || "CC")}: `), + span({ class: isBest ? "data-score-value data-best-score" : "data-score-value" }, `${scorePct(m.score)}%`), + a({ href: m.href, class: "btn-singleview btn-content data-side-visit", title: i18n.visitContent }, renderIcon("open")) + ), + a({ href: m.href, class: "data-card-title" }, shortTitle(m.title) || shortId(m.id)), + span({ class: "data-card-author" }, userLink(m.author)), + common.length + ? div({ class: "data-common-row" }, + span({ class: "card-label" }, `${i18n.dataCommonTerms}: `), + ...common.slice(0, 8).map(t => + a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`)) + ) + : null, + reasons.length + ? div({ class: "data-reasons-row" }, + ...reasons.map(r => span({ class: "data-kind-chip data-reason-chip" }, REASON_LABEL[r]())) + ) + : null + ); +}; + +const groupByKind = (matches) => { + const groups = new Map(); + for (const m of matches) { + if (!groups.has(m.kind)) groups.set(m.kind, []); + groups.get(m.kind).push(m); + } + const ordered = [...groups.entries()].map(([kind, items]) => { + items.sort((x, y) => (y.score || 0) - (x.score || 0) || (y.ts || 0) - (x.ts || 0)); + return { kind, items }; + }); + ordered.sort((x, y) => (y.items[0].score || 0) - (x.items[0].score || 0)); + return ordered; +}; + +exports.dataView = async (payload = {}) => { + const q = payload.q || ''; + const reason = String(payload.reason || ''); + + const matches = Array.isArray(payload.matches) ? payload.matches : []; + const emptyData = matches.length === 0 && payload.hasProfile === false && !String(q || '').trim() && !reason; + const groups = groupByKind(matches); + const best = groups.length ? groups[0].items[0] : null; + + return template( + i18n.dataTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.dataTitle), p(i18n.dataDescription))), + renderFilters(q, { ...payload, matches }, payload.kindsMatched && typeof payload.kindsMatched === "object" ? Object.keys(payload.kindsMatched) : groups.map(g => g.kind)), + emptyData ? null : (payload.cohesion ? renderCohesion(payload.cohesion) : null), + section( + emptyData ? null : div({ class: "tags-header" }, + h2(i18n.dataMatchesTitle), + p(i18n.dataMatchesHint) + ), + best + ? div({ class: "data-section data-top-best" }, + h2({ class: "data-section-title" }, String(i18n.dataBestMatch).toUpperCase()), + div({ class: "data-list" }, renderMatchRow(best, true)) + ) + : null, + ...groupByKind(paged(groups.flatMap(g => g.items))).map(g => + div({ id: sectionId(g.kind), class: "data-section" }, + h2({ class: "data-section-title" }, `${String(kindLabel(g.kind)).toUpperCase()} (${(groups.find(x => x.kind === g.kind) || g).items.length})`), + div({ class: "data-list" }, ...g.items.map((m, idx) => renderMatchRow(m, idx === 0))) + ) + ), + matches.length + ? null + : div({ class: "no-content-box" }, p({ class: "no-content" }, payload.hasProfile === false + ? i18n.dataNoProfile + : i18n.dataNoMatches)) + ) + ); +}; diff --git a/src/views/dev_view.js b/src/views/dev_view.js new file mode 100644 index 0000000..ccc1468 --- /dev/null +++ b/src/views/dev_view.js @@ -0,0 +1,271 @@ +const { form, button, div, h2, p, section, input, a, span, strong, ul, li, table, thead, tbody, tr, td, th, pre, code, select, option } = require("../server/node_modules/hyperaxe") +const { template, i18n } = require("./main_views") + +const FILTERS = [ + { key: "tree", href: "/dev", i18n: "devFilterFiles", fallback: "FILES" }, + { key: "map", href: "/dev/map", i18n: "devFilterModules", fallback: "MODULES" }, + { key: "translations", href: "/dev?group=translations", i18n: "devFilterTranslations", fallback: "TRANSLATIONS" }, + { key: "styles", href: "/dev?group=styles", i18n: "devFilterStyles", fallback: "STYLES" }, + { key: "tests", href: "/dev?group=tests", i18n: "devFilterTests", fallback: "TESTS" } +] + +const ICONS = { + folder: "📁", + parent: "↰", + ".js": "📜", + ".mjs": "📜", + ".json": "🧾", + ".css": "🎨", + ".md": "📖", + ".sh": "⌨", + ".html": "🌐", + ".txt": "📄", + ".yml": "🧾", + ".yaml": "🧾" +} + +const safeText = (v) => String(v == null ? "" : v) + +const iconFor = (file) => (file.viewable ? (ICONS[file.ext] || "📄") : "📦") + +const fmtSize = (bytes) => { + const n = Number(bytes) || 0 + if (n < 1024) return `${n} B` + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB` + return `${(n / (1024 * 1024)).toFixed(1)} MB` +} + +const fmtCount = (n) => String(Number(n) || 0) + +const fileHref = (path, from) => `/dev/file?path=${encodeURIComponent(path)}${from ? `&from=${from}` : ""}` +const dirHref = (path) => (path ? `/dev?path=${encodeURIComponent(path)}` : "/dev") + +const renderHeader = (active) => [ + div({ class: "tags-header module-header-line" }, + h2(i18n.devTitle || "Developer"), + p(i18n.devDescription || "Explore the source code running on this node.") + ), + div({ class: "filters" }, + div({ class: "ui-toolbar ui-toolbar--filters dev-toolbar" }, + FILTERS.map(f => a({ + class: active === f.key ? "filter-btn active" : "filter-btn", + href: f.href + }, i18n[f.i18n] || f.fallback)) + ) + ) +] + +const groupLabel = (key) => { + const f = FILTERS.find(x => x.key === key) + return f ? (i18n[f.i18n] || f.fallback) : String(key || "") +} + +const renderPathRow = (parents, currentIsFile, group) => (group + ? div({ class: "dev-breadcrumb" }, + a({ href: "/dev" }, i18n.devRoot || "root"), + span({ class: "dev-breadcrumb-sep" }, " / "), + span({ class: "dev-breadcrumb-current" }, groupLabel(group))) + : renderBreadcrumb(parents, currentIsFile)) + +const renderStats = (stats) => { + const bit = (labelKey, fallback, value) => + span(`${i18n[labelKey] || fallback}: `, strong(String(value))) + return div({ class: "module-stats-line" }, + bit("devVersion", "Version", stats.version || "-"), + bit("devNodeVersion", "Node", stats.node || "-"), + bit("devStatsFiles", "Files", fmtCount(stats.files)), + bit("devStatsLines", "Lines", fmtCount(stats.lines)), + bit("devStatsModules", "Modules", fmtCount(stats.modules)), + bit("devStatsLanguages", "Languages", fmtCount(stats.languages)), + bit("devStatsTests", "Tests", fmtCount(stats.testMods)) + ) +} + +const renderBreadcrumb = (parents, currentIsFile) => div({ class: "dev-breadcrumb" }, + a({ href: "/dev" }, i18n.devRoot || "root"), + (parents || []).map((seg, idx) => [ + span({ class: "dev-breadcrumb-sep" }, " / "), + (currentIsFile && idx === parents.length - 1) + ? span({ class: "dev-breadcrumb-current" }, safeText(seg.name)) + : a({ href: dirHref(seg.path) }, safeText(seg.name)) + ]) +) + +const renderSearchForm = (query, ext, stats = null) => div({ class: "dev-search activity-filter-chips activity-toolbar-row" }, + stats ? renderStats(stats) : null, + form({ method: "GET", action: "/dev/search", class: "filter-box" }, + input({ type: "text", name: "q", value: safeText(query), placeholder: i18n.devSearchPlaceholder || "Text to find in the code", minlength: "2", required: true, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + select({ name: "ext", class: "filter-box__select" }, + option({ value: "", ...(ext ? {} : { selected: true }) }, i18n.devAnyExtension || "Any file"), + [".js", ".json", ".css", ".md", ".sh"].map(x => option({ value: x, ...(ext === x ? { selected: true } : {}) }, x)) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton || "Search") + ) + ) +) + +const renderDesktop = (tree) => { + const dirs = (tree && tree.dirs) || [] + const files = (tree && tree.files) || [] + const parents = (tree && tree.parents) || [] + const items = [] + if (tree.path) { + const up = parents.length > 1 ? parents[parents.length - 2].path : "" + items.push(a({ class: "dev-icon dev-icon-up", href: dirHref(up) }, + span({ class: "dev-icon-glyph" }, ICONS.parent), + span({ class: "dev-icon-name" }, ".."), + span({ class: "dev-icon-meta" }, i18n.devParentFolder || "Parent folder") + )) + } + for (const d of dirs) { + items.push(a({ class: "dev-icon", href: dirHref(d.path) }, + span({ class: "dev-icon-glyph" }, ICONS.folder), + span({ class: "dev-icon-name" }, safeText(d.name)), + span({ class: "dev-icon-meta" }, i18n.devOpenFolder || "open") + )) + } + for (const f of files) { + const glyph = span({ class: "dev-icon-glyph" }, iconFor(f)) + const name = span({ class: "dev-icon-name" }, safeText(f.name)) + const meta = span({ class: "dev-icon-meta" }, f.viewable ? fmtSize(f.size) : (i18n.devNotViewable || "not viewable")) + items.push(f.viewable + ? a({ class: "dev-icon", href: fileHref(f.path) }, glyph, name, meta) + : div({ class: "dev-icon dev-icon-off" }, glyph, name, meta)) + } + if (!items.length) return p({ class: "dev-hint" }, i18n.devEmptyFolder || "This folder is empty.") + return div({ class: "dev-desktop" }, ...items) +} + +const renderPager = (file) => div({ class: "dev-pager" }, + file.hasPrev + ? a({ class: "filter-btn", href: fileHref(file.path, Math.max(1, file.from - file.pageLines)) }, i18n.devPrevPage || "Previous lines") + : null, + file.hasNext + ? a({ class: "filter-btn", href: fileHref(file.path, file.to + 1) }, i18n.devNextPage || "Next lines") + : null +) + +exports.devTreeView = async (tree, stats, active) => { + return template( + i18n.devTitle || "Developer", + section( + ...renderHeader(active || (tree.group ? tree.group : "tree")), + renderSearchForm("", "", stats), + renderPathRow(tree.parents, false, tree.group), + renderDesktop(tree) + ) + ) +} + +exports.devFileView = async (file) => { + const reportTitle = `${file.path}:${file.from}` + const reportDescription = `${i18n.devReportContext || "Found while reading the Oasis source code"}: /${file.path} (${i18n.devLine || "line"} ${file.from})` + const taskTitle = `${i18n.devTaskPrefix || "Review"} ${file.path}` + return template( + i18n.devTitle || "Developer", + section( + ...renderHeader("tree"), + renderPathRow(file.parents, true, null), + div({ class: "dev-file-head" }, + span({ class: "dev-icon-glyph dev-file-glyph" }, iconFor({ ext: `.${String(file.name).split('.').pop()}`, viewable: true })), + h2({ class: "dev-file-name" }, safeText(file.name)) + ), + div({ class: "dev-file-meta" }, + p( + span({ class: "dev-label" }, `${i18n.devStatsLines || "Lines"}: `), fmtCount(file.totalLines), + span({ class: "dev-sep" }, " · "), + span({ class: "dev-label" }, `${i18n.devSize || "Size"}: `), fmtSize(file.size), + span({ class: "dev-sep" }, " · "), + span({ class: "dev-label" }, `${i18n.devShowing || "Showing"}: `), `${fmtCount(file.from)}-${fmtCount(file.to)}` + ) + ), + div({ class: "dev-actions" }, + form({ method: "GET", action: "/reports" }, + input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "category", value: "BUGS" }), + input({ type: "hidden", name: "title", maxlength: "100", value: reportTitle }), + input({ type: "hidden", name: "description", value: reportDescription }), + button({ type: "submit", class: "filter-btn" }, i18n.devReportBug || "Report Bug") + ), + form({ method: "GET", action: "/tasks" }, + input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "title", maxlength: "100", value: taskTitle }), + input({ type: "hidden", name: "description", value: reportDescription }), + button({ type: "submit", class: "filter-btn" }, i18n.devCreateTask || "Create Task") + ), + form({ method: "GET", action: "/dev/raw" }, + input({ type: "hidden", name: "path", value: file.path }), + button({ type: "submit", class: "filter-btn" }, i18n.devRaw || "RAW") + ) + ), + div({ class: "dev-code" }, + table({ class: "dev-code-table" }, + tbody( + file.lines.map(l => tr({ id: `L${l.n}`, class: "dev-code-row" }, + td({ class: "dev-code-num" }, a({ href: `${fileHref(file.path, file.from)}#L${l.n}` }, String(l.n))), + td({ class: "dev-code-line" }, pre(code(safeText(l.text)))) + )) + ) + ) + ), + renderPager(file) + ) + ) +} + +exports.devSearchView = async (result, ext) => { + const results = (result && result.results) || [] + return template( + i18n.devTitle || "Developer", + section( + ...renderHeader("search"), + renderSearchForm(result.query, ext), + result.query + ? p({ class: "dev-hint" }, `${fmtCount(results.length)}${result.truncated ? "+" : ""} ${i18n.devMatches || "matches"} · ${fmtCount(result.scanned)} ${i18n.devFilesScanned || "files scanned"}`) + : null, + results.length + ? ul({ class: "dev-list dev-results" }, + results.map(r => li({ class: "dev-entry" }, + div({ class: "dev-result-head" }, + span({ class: "dev-icon-glyph dev-result-glyph" }, ICONS[`.${r.path.split('.').pop()}`] || "📄"), + a({ href: `${fileHref(r.path, Math.max(1, r.line - 20))}#L${r.line}` }, `${safeText(r.path)}:${r.line}`) + ), + div({ class: "dev-result-text" }, pre(code(safeText(r.text)))) + )) + ) + : (result.query ? p({ class: "dev-hint" }, i18n.devNoResults || "No matches, yet.") : null) + ) + ) +} + +exports.devMapView = async (modules) => { + return template( + i18n.devTitle || "Developer", + section( + ...renderHeader("map"), + div({ class: "dev-listing" }, + table({ class: "dev-table" }, + thead(tr( + th(i18n.devColModule || "Module"), + th(i18n.devColState || "State"), + th(i18n.devColModel || "Model"), + th(i18n.devColView || "View"), + th(i18n.devColRoutes || "Routes"), + th(i18n.devColTests || "Tests") + )), + tbody( + (modules || []).map(m => tr( + td(safeText(m.name)), + td(m.enabled ? (i18n.devOn || "on") : (i18n.devOff || "off")), + td(m.model ? a({ href: fileHref(m.model) }, safeText(m.model)) : span({ class: "dev-hint" }, "-")), + td(m.view ? a({ href: fileHref(m.view) }, safeText(m.view)) : span({ class: "dev-hint" }, "-")), + td(String(m.routes || 0)), + td(m.tests ? a({ href: dirHref(m.tests) }, safeText(m.tests)) : span({ class: "dev-hint" }, "-")) + )) + ) + ) + ) + ) + ) +} diff --git a/src/views/document_view.js b/src/views/document_view.js index e1e98f2..e609e1a 100644 --- a/src/views/document_view.js +++ b/src/views/document_view.js @@ -1,10 +1,12 @@ const { form, button, div, h2, p, section, input, label, br, a, span, textarea, select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const userId = config.keys.id; @@ -27,116 +29,18 @@ const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length ? div( - { class: "card-tags" }, + { class: "tribe-side-tags" }, list.map((tag) => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) ) : null; }; -const renderFavoriteToggle = (doc, returnTo) => - form( - { - method: "POST", - action: doc.isFavorite - ? `/documents/favorites/remove/${encodeURIComponent(doc.key)}` - : `/documents/favorites/add/${encodeURIComponent(doc.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - doc.isFavorite ? i18n.documentRemoveFavoriteButton : i18n.documentAddFavoriteButton - ) - ); - -const renderDocumentActions = (filter, doc, params = {}) => { - const returnTo = buildReturnTo(filter, params); - const isAuthor = String(doc.author) === String(userId); - const hasOpinions = Object.keys(doc.opinions || {}).length > 0; - - return isAuthor - ? div( - { class: "bookmark-actions" }, - !hasOpinions - ? form( - { method: "GET", action: `/documents/edit/${encodeURIComponent(doc.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.documentUpdateButton) - ) - : null, - form( - { method: "POST", action: `/documents/delete/${encodeURIComponent(doc.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.documentDeleteButton) - ) - ) - : null; -}; - const renderDocumentCommentsSection = (documentKey, rootId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/documents/${encodeURIComponent(documentKey)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/documents/${encodeURIComponent(documentKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - rootId ? input({ type: "hidden", name: "rootId", value: rootId }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c?.value?.content || {}; - const text = content.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot - ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) - : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderDocumentList = exports.renderDocumentList = (documents, filter, params = {}) => { @@ -148,64 +52,35 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para const title = safeText(doc.title); const pdfId = safeDomId("pdf-container-", doc.key); - const topbarLeft = - doc.author && String(doc.author) !== String(userId) - ? form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: doc.author }), - button({ type: "submit", class: "filter-btn" }, i18n.documentMessageAuthorButton) - ) - : null; - const isOwn = doc.author && String(doc.author) === String(userId); + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(doc) + : doc.tribeOrigin + ? renderContentActions(null, doc.tribeOrigin.href) + : renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`, { spread: (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads || null, author: doc.author, favKind: 'documents', torrentFrom: { blobId: doc.url, name: doc.title }, isFavorite: doc.isFavorite, reportTitle: doc.title, deleteAction: isOwn ? contentDeleteAction('document', doc.key) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : doc.tribeOrigin ? doc.tribeOrigin.href : `/documents/${encodeURIComponent(doc.key)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(doc) + : doc.tribeOrigin ? null : renderEngagement(doc.key, + renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), + renderCommentsLink({ href: `/documents/${encodeURIComponent(doc.key)}`, count: commentCount }) + ); return div( { class: "trending-card document-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`) + headerActions ), div( { class: "card-section document-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderFavoriteToggle(doc, returnTo), - topbarLeft - ), - renderDocumentActions(filter, doc, params) - ), - title ? h2(title) : null, + div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, doc.tribeOrigin ? renderTribeOriginChip(doc.tribeOrigin) : doc.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('documents', doc.title, doc.key)) : null, renderLicenseChip(doc.license), ...(typeof params.titleChips === "function" ? params.titleChips(doc) : [])), doc.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(doc.lifetime, i18n)) : null, doc?.url - ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` }) + ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) }) : p(i18n.documentNoFile), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/documents/${encodeURIComponent(doc.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(doc.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(doc.key, (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads)), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(doc) : []), + engagement, br(), (() => { const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN; @@ -214,12 +89,12 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(doc.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -237,6 +112,7 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => { return div( { class: "div-center document-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/documents/update/${encodeURIComponent(documentId)}` : "/documents/create", @@ -251,71 +127,70 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => { br(), label(i18n.documentTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || "" }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || "" }), br(), label(i18n.documentDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.documentDescriptionPlaceholder, rows: "4" }, docToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.documentDescriptionPlaceholder, rows: "4" }, docToEdit?.description || ""), br(), label(i18n.documentTagsLabel), br(), input({ type: "text", name: "tags", placeholder: i18n.documentTagsPlaceholder, value: tagsValue }), br(), + ...renderLicenseSelect(docToEdit?.license, i18n), + br(), + renderClearnetSelector(filter === "edit" ? !!docToEdit?.clearnet : false, i18n), br(), button({ type: "submit" }, filter === "edit" ? i18n.documentUpdateButton : i18n.documentCreateButton) ) ); }; +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.documentView = async (documents, filter = "all", documentId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.documentMineSectionTitle - : filter === "create" - ? i18n.documentCreateSectionTitle - : filter === "edit" - ? i18n.documentUpdateSectionTitle - : filter === "recent" - ? i18n.documentRecentSectionTitle - : filter === "top" - ? i18n.documentTopSectionTitle - : filter === "favorites" - ? i18n.documentFavoritesSectionTitle - : i18n.documentAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(documentId) }; + const title = i18n.documentTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(documents); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const docToEdit = documentId ? list.find((d) => d.key === documentId) : null; const tpl = template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.documentDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetDocuments); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterTop), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.documentFilterFavorites - ), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterRecent), + String(i18n.documentFilterFavorites).toUpperCase() + )] : []), + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterTop).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton) ) ) @@ -324,8 +199,9 @@ exports.documentView = async (documents, filter = "all", documentId = null, para filter === "create" || filter === "edit" ? renderDocumentForm(filter, documentId, docToEdit || {}, { ...params, filter }) : section( - div( - { class: "documents-search" }, + emptyMod ? null : div( + { class: "documents-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/documents", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -334,23 +210,24 @@ exports.documentView = async (documents, filter = "all", documentId = null, para { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.documentSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.documentSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.documentSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.documentSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.documentSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.documentSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.documentSearchButton) ) ) ), - div({ class: "documents-list" }, renderDocumentList(list, filter, { q, sort })) + div({ class: "documents-list" }, renderDocumentList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); - return `${tpl}`; + return `${tpl}`; }; exports.singleDocumentView = async (doc, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); @@ -359,8 +236,7 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = const pdfId = safeDomId("pdf-container-", doc.key); const isAuthor = String(doc.author) === String(userId); const hasOpinions = Object.keys(doc.opinions || {}).length > 0; - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetDocuments); + const isClearnet = !!doc.clearnet; const chips = [ renderLifespanChip(doc.lifetime, i18n), @@ -368,14 +244,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ].filter(Boolean); const sideActions = []; - sideActions.push(renderFavoriteToggle(doc, returnTo)); - if (doc.author && String(doc.author) !== String(userId)) { - sideActions.push(form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: doc.author }), - button({ type: "submit", class: "filter-btn" }, i18n.documentMessageAuthorButton) - )); - } if (isAuthor && !hasOpinions) { sideActions.push(form( { method: "GET", action: `/documents/edit/${encodeURIComponent(doc.key)}` }, @@ -383,33 +251,40 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = button({ class: "update-btn", type: "submit" }, i18n.documentUpdateButton) )); } - if (isAuthor) { - sideActions.push(form( - { method: "POST", action: `/documents/delete/${encodeURIComponent(doc.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.documentDeleteButton) - )); - } const tagsNode = renderTags(doc.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`, { + author: doc.author, + favKind: 'documents', torrentFrom: { blobId: doc.url, name: doc.title }, + isFavorite: doc.isFavorite, + spread: params.spreads || null, + returnTo: buildReturnTo(filter, { q, sort }), + reportTitle: doc.title, + deleteAction: isAuthor ? contentDeleteAction('document', doc.key) : undefined + }) + ); + const docSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('documents', doc.title, doc.key) : null), + isAuthor ? renderClearnetSwitch('documents', doc.rootId || doc.key, isClearnet) : null, + renderLicenseChip(doc.license) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(doc.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(doc.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(doc.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(doc.key, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const docMain = div({ class: "tribe-main" }, + detailActions, doc?.url - ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` }) + ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) }) : p(i18n.documentNoFile), (() => { const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN; @@ -418,24 +293,26 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(doc.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), - renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo) + renderEngagement(doc.key, + renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), + renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo) + ) ); const tpl = template( i18n.documentTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.documentAllSectionTitle || i18n.documentTitle), p(i18n.documentDescription) ), @@ -443,16 +320,15 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = { class: "filters" }, form( { method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterTop), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.documentFilterFavorites - ), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterRecent), + String(i18n.documentFilterFavorites).toUpperCase() + )] : []), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton) ) ), @@ -460,6 +336,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ) ); - return `${tpl}`; + return `${tpl}`; }; diff --git a/src/views/emergencies_view.js b/src/views/emergencies_view.js new file mode 100644 index 0000000..b5c00e2 --- /dev/null +++ b/src/views/emergencies_view.js @@ -0,0 +1,307 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, video: videoHyperaxe, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); +const { renderCommentsSection } = require("./comments_view"); +const { renderMapEmbed } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; +const CATEGORIES = ["WEATHER", "INFRASTRUCTURE", "HEALTH", "SECURITY", "LOST", "NEIGHBORHOOD"]; +const SEVERITIES = ["UNVERIFIED", "LOW", "MEDIUM", "HIGH"]; +const STATUSES = ["ACTIVE", "RESOLVED", "EXPIRED"]; +const BASE_FILTERS = ["ALL", "RECENT", "MINE"]; +const EXPIRES = ["1d", "3d", "7d", "30d"]; + +const catLabel = (c) => String(i18n[`emergencyCategory${c.charAt(0) + c.slice(1).toLowerCase()}`] || c).toUpperCase(); +const sevLabel = (s) => String(i18n[`emergencySeverity${s.charAt(0) + s.slice(1).toLowerCase()}`] || s).toUpperCase(); +const statusLabel = (s) => String(i18n[`emergencyStatus${s.charAt(0) + s.slice(1).toLowerCase()}`] || s).toUpperCase(); +const baseLabel = (f) => String(i18n[`emergencyFilter${f.charAt(0) + f.slice(1).toLowerCase()}`] || f).toUpperCase(); + +const severityChip = (emergency) => span({ class: `emergency-severity emergency-severity-${String(emergency.severity || "UNVERIFIED").toLowerCase()}` }, `${sevLabel(emergency.severity)} · ${emergency.confirmationCount || 0}`); +const statusChip = (emergency) => renderStateChip(emergency.status === "ACTIVE" ? "mutuals" : "closed", "", statusLabel(emergency.status)); +const categoryChip = (emergency) => renderStateChip("neutral", "", catLabel(emergency.category)); +const clearnetEligible = (emergency) => String(emergency.status || "ACTIVE").toUpperCase() === "ACTIVE" && !emergency.tribeId && !emergency.encrypted; + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const emergencyHref = (emergency) => `/emergencies/${encodeURIComponent(emergency.id)}`; + +const renderEmergencyCard = (emergency, params = {}) => + div({ class: `tribe-card emergency-card emergency-card-${String(emergency.severity || "UNVERIFIED").toLowerCase()}` }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(emergency.id, emergencyHref(emergency), { author: emergency.author, favKind: "emergencies", isFavorite: emergency.isFavorite, reportTitle: emergency.title, spread: (params.spreadMap && params.spreadMap.get(emergency.id)) || null, deleteAction: String(emergency.author) === String(userId) ? contentDeleteAction("emergency", emergency.id) : undefined }) + ), + div({ class: "tribe-card-body" }, + emergency.media && emergency.media.kind === "image" ? a({ href: emergencyHref(emergency) }, img({ loading: 'lazy', class: "emergency-card-cover", src: `/blob/${encodeURIComponent(emergency.media.blobId)}`, alt: emergency.title || "" })) : null, + emergency.media && emergency.media.kind === "video" ? videoHyperaxe({ class: "emergency-card-cover", src: `/blob/${encodeURIComponent(emergency.media.blobId)}`, controls: true, preload: "metadata" }) : null, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: emergencyHref(emergency) }, emergency.title || "—"))), + div({ class: "card-chips-row" }, severityChip(emergency), statusChip(emergency), categoryChip(emergency), emergency.clearnet === true && clearnetEligible(emergency) ? renderReachChip(true, i18n, clearnetItemHref("emergencies", emergency.title, emergency.id)) : null), + emergency.expiresAt ? p({ class: "job-meta-line" }, `${i18n.emergencyExpiresAt}: ${moment(emergency.expiresAt).format("YYYY/MM/DD HH:mm")}`) : null + ) + ); + +const chipVisible = (mode, filter, census) => { + if (mode === filter || mode === "ALL") return true; + if (mode === "MINE") return census.some(al => String(al.author) === String(userId)); + if (mode === "RECENT") return census.length > 0; + if (STATUSES.includes(mode)) return census.some(al => al.status === mode); + if (SEVERITIES.includes(mode)) return census.some(al => al.severity === mode); + if (CATEGORIES.includes(mode)) return census.some(al => al.category === mode); + return census.length > 0; +}; + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => chipVisible(m, filter, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const renderFilters = (filter, q, census, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/emergencies", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, baseLabel), + ...chipButtons(SEVERITIES, filter, census, sevLabel), + ...chipButtons(STATUSES, filter, census, statusLabel), + ...chipButtons(CATEGORIES, filter, census, catLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.emergencyCreate) + ) + ) + ); + +const renderForm = (emergency, params = {}) => + div({ class: "div-center audio-form" }, + h2(emergency ? i18n.emergencyUpdateSectionTitle : i18n.emergencyCreateSectionTitle), + form({ method: "POST", action: emergency ? `/emergencies/update/${encodeURIComponent(emergency.id)}` : "/emergencies/create", enctype: "multipart/form-data" }, + label(i18n.emergencyTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.emergencyTitlePlaceholder, value: emergency ? emergency.title : "" }), br(), br(), + label(i18n.emergencyCategoryLabel), br(), + select({ name: "category" }, ...CATEGORIES.map(c => option({ value: c, ...((emergency ? emergency.category : "NEIGHBORHOOD") === c ? { selected: true } : {}) }, catLabel(c)))), br(), br(), + label(i18n.emergencyTextLabel), br(), + textarea({ name: "text", rows: 8, maxlength: "5000", placeholder: i18n.emergencyTextPlaceholder }, emergency ? emergency.text : ""), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + label(i18n.mapLocationTitle || "Map Location"), br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: emergency ? emergency.mapUrl : "" }), br(), + label(i18n.emergencyExpiresLabel), br(), + select({ name: "expiresIn" }, + emergency && emergency.expiresAt ? option({ value: "keep", selected: true }, i18n.emergencyExpiresKeep) : null, + option({ value: "", ...(!(emergency && emergency.expiresAt) ? { selected: true } : {}) }, i18n.emergencyExpiresNone), + ...EXPIRES.map(e => option({ value: e }, i18n[`emergencyExpires${e}`] || e)) + ), br(), br(), + label(i18n.emergencyTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.emergencyTagsPlaceholder, value: emergency ? emergency.tags.join(", ") : "" }), br(), br(), + ...(!emergency || clearnetEligible(emergency) ? [renderClearnetSelector(!!emergency && emergency.clearnet === true, i18n), br()] : []), + button({ type: "submit", class: "create-button" }, emergency ? i18n.emergencyUpdate : i18n.emergencyCreate) + ) + ); + +const renderHeader = () => div({ class: "tags-header module-header-line" }, h2(i18n.emergenciesTitle), p(i18n.emergenciesDescription)); + +exports.emergenciesView = async (emergencies, filter = "ALL", params = {}) => { + const list = Array.isArray(emergencies) ? emergencies : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q); + return template( + i18n.emergenciesTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.emergenciesTitle), p(i18n.emergenciesDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? [] : census, emptyMod) + ), + section( + isForm + ? renderForm(f === "EDIT" ? params.emergency : null, params) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, SEVERITIES.map(s => ({ label: sevLabel(s), count: list.filter(al => al.severity === s).length }))), + form({ method: "GET", action: "/emergencies", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: q, placeholder: i18n.emergencySearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, list.length ? paged(list).map(al => renderEmergencyCard(al, params)) : p(i18n.emergencyNoItems)) + ] + ) + ); +}; + +exports.singleEmergencyView = async (emergency, params = {}) => { + const isAuthor = String(emergency.author) === String(userId); + const canClearnet = clearnetEligible(emergency); + const isClearnet = canClearnet && !!emergency.clearnet; + const confirmed = Array.isArray(emergency.confirmations) && emergency.confirmations.includes(userId); + const href = emergencyHref(emergency); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const sideActions = [ + !isAuthor && !confirmed && emergency.status === "ACTIVE" + ? form({ method: "POST", action: `/emergencies/confirm/${encodeURIComponent(emergency.id)}` }, button({ type: "submit", class: "tribe-action-btn" }, `✓ ${String(i18n.emergencyConfirmButton).toUpperCase()}`)) + : null, + confirmed ? span({ class: "status supporting" }, i18n.emergencyConfirmed) : null + ].filter(Boolean); + const ownerActions = isAuthor + ? [ + (emergency.confirmationCount || 0) > 0 + ? null + : form({ method: "GET", action: "/emergencies" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: emergency.id }), button({ type: "submit", class: "update-btn" }, i18n.emergencyUpdate)) + ].filter(Boolean) + : []; + const updates = Array.isArray(emergency.updates) ? emergency.updates : []; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(emergency.id, href, { author: emergency.author, favKind: "emergencies", isFavorite: emergency.isFavorite, reportTitle: emergency.title, spread: params.spread || null, returnTo: href, deleteAction: isAuthor ? contentDeleteAction("emergency", emergency.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, emergency.title)), + div({ class: "card-chips-row" }, + severityChip(emergency), statusChip(emergency), categoryChip(emergency), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("emergencies", emergency.title, emergency.id) : null), + isAuthor && canClearnet ? renderClearnetSwitch("emergencies", emergency.rootId || emergency.id, isClearnet) : null + ), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, moment(emergency.createdAt).format("YYYY/MM/DD HH:mm")) + ), + emergency.expiresAt + ? tr( + td({ class: "tribe-info-label" }, i18n.emergencyExpiresAt), + td({ class: "tribe-info-value", colspan: "3" }, moment(emergency.expiresAt).format("YYYY/MM/DD HH:mm")) + ) + : null, + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(emergency.author)) + ) + ), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.emergencyConfirmations}: ${emergency.confirmationCount || 0}`) + ), + renderMapEmbed(params.mapData, emergency.mapUrl), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + isAuthor && emergency.status !== "EXPIRED" + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + statusChip(emergency), + form({ method: "POST", action: `/emergencies/status/${encodeURIComponent(emergency.id)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "status", value: emergency.status === "ACTIVE" ? "RESOLVED" : "ACTIVE" }, + String(emergency.status === "ACTIVE" ? i18n.emergencyResolveButton : i18n.emergencyReopenButton).toUpperCase()) + ) + ) + : null, + div({ class: "doc-export-actions" }, + form({ method: "GET", action: `/emergencies/${encodeURIComponent(emergency.id)}/pdf` }, button({ type: "submit", class: "filter-btn" }, i18n.emergencyGenerateReport)), + form({ method: "POST", action: `/emergencies/${encodeURIComponent(emergency.id)}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) + ), + renderTags(emergency.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + params.subscription + ? renderSubscriptionBox({ target: emergency.id, scope: "emergencies", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: isAuthor, returnTo: href }) + : null + ); + const main = div({ class: "tribe-main" }, + emergency.text ? div({ class: "emergency-body" }, ...renderStyledText(emergency.text)) : null, + div({ class: "card-section emergency-updates" }, + h3(i18n.emergencyUpdatesTitle), + updates.length + ? div({ class: "emergency-updates-list" }, ...updates.map(u => { + const mineUpdate = String(u.author) === String(userId); + const confirmedUpdate = Array.isArray(u.confirmations) && u.confirmations.includes(userId); + const editing = params.editUpdate && params.editUpdate === u.id; + return div({ class: "emergency-update", id: `update-${encodeURIComponent(u.id)}` }, + div({ class: "emergency-update-head" }, + div({ class: "card-chips-row" }, + renderStateChip(u.confirmationCount > 0 ? "mutuals" : "neutral", "✓", `${i18n.emergencyConfirmations} · ${u.confirmationCount || 0}`), + u.edited ? renderStateChip("neutral", "", i18n.emergencyUpdateEdited) : null + ), + div({ class: "tribe-side-actions emergency-update-actions" }, + !mineUpdate && !confirmedUpdate && emergency.status === "ACTIVE" + ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(u.id)}/confirm` }, input({ type: "hidden", name: "emergency", value: emergency.id }), button({ type: "submit", class: "tribe-action-btn" }, `✓ ${String(i18n.emergencyConfirmButton).toUpperCase()}`)) + : null, + confirmedUpdate ? span({ class: "status supporting" }, i18n.emergencyConfirmed) : null, + mineUpdate && !editing && u.confirmationCount === 0 + ? form({ method: "GET", action: `${href}#update-${encodeURIComponent(u.id)}` }, input({ type: "hidden", name: "editUpdate", value: u.id }), button({ type: "submit", class: "update-btn" }, i18n.emergencyUpdate)) + : null, + mineUpdate + ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(u.id)}/delete` }, button({ type: "submit", class: "delete-btn" }, i18n.emergencyDelete)) + : null + ), + ), + editing + ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(u.id)}/edit`, enctype: "multipart/form-data", class: "emergency-update-form" }, + textarea({ name: "text", rows: 3, maxlength: "2000", required: true, autofocus: true }, u.text), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.emergencyUpdate) + ) + : div({ class: "emergency-update-text" }, ...renderStyledText(u.text)), + p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(u.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(u.author)) + ); + })) + : p(i18n.emergencyNoUpdates), + isAuthor && emergency.status === "ACTIVE" + ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(emergency.id)}`, enctype: "multipart/form-data", class: "emergency-update-form" }, + textarea({ name: "text", rows: 3, maxlength: "2000", required: true, placeholder: i18n.emergencyUpdatePlaceholder }), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.emergencyAddUpdate) + ) + : null + ), + renderCommentsSection({ action: `/emergencies/${encodeURIComponent(emergency.id)}/comments`, comments: params.comments || [], returnTo: href }) + ); + return template( + emergency.title, + section(renderHeader(), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; + +exports.clearnetEmergencyView = async (emergency, params = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, blobUrl: cnBlob, renderClearnetPage } = require("./clearnet_view"); + const title = esc(emergency.title || i18n.emergencyLabel); + const desc = renderRichText(emergency.text || ""); + const severity = String(emergency.severity || "UNVERIFIED").toUpperCase(); + const point = params.mapPoint || null; + const cover = emergency.media && emergency.media.kind === "image" ? cnBlob(emergency.media.blobId) : null; + const updates = (Array.isArray(emergency.updates) ? emergency.updates : []).filter(u => String(u.author) === String(emergency.author)); + const fmtDate = (v) => esc(moment(v).format("YYYY/MM/DD HH:mm")); + const extraCss = ` +.cn-emergency-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} +.cn-emergency-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} +.cn-emergency-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} +.cn-emergency-severity-medium,.cn-emergency-severity-high{border-color:var(--fg);color:var(--fg);font-weight:700} +.cn-emergency-desc{color:var(--fg-soft);line-height:1.6;font-size:15px;margin:0 0 20px 0;word-break:break-word} +.cn-emergency-update{border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin:12px 0;background:var(--bg-elev)} +.cn-emergency-update-date{color:var(--fg-dim);font-size:12px;margin-bottom:6px} +.cn-emergency-update-text{color:var(--fg-soft);line-height:1.5;word-break:break-word} +`; + const body = ` +

${title}

+
+ ${renderKindTag("emergency")} + ⚠ ${esc(sevLabel(severity))} + ✓ ${esc(i18n.emergencyConfirmations)}: ${Number(emergency.confirmationCount) || 0} + ${esc(catLabel(String(emergency.category || "NEIGHBORHOOD")))} + ${emergency.createdAt ? `📅 ${fmtDate(emergency.createdAt)}` : ""} + ${emergency.expiresAt ? `⏳ ${esc(i18n.emergencyExpiresAt)}: ${fmtDate(emergency.expiresAt)}` : ""} + ${point ? `📍 ${point.title ? `${esc(point.title)} · ` : ""}${point.lat}, ${point.lng}` : ""} +
+
+ ${desc ? `
${desc}
` : ""} + ${updates.length ? `

${esc(String(i18n.emergencyUpdatesTitle || "").replace(/[::]\s*$/, ""))}

${updates.map(u => `
${fmtDate(u.updatedAt || u.createdAt)}
${renderRichText(u.text || "")}
`).join("")}` : ""} + ${renderTagChips(emergency.tags)} +`; + return renderClearnetPage({ + title: `${emergency.title || i18n.emergencyLabel} | Oasis`, + ogTitle: emergency.title || i18n.emergencyLabel, + ogDescription: String(emergency.text || "").replace(/!?\[[^\]]*\]\(\s*&[^)\s]+\s*\)/g, "").trim(), + ogImage: cover, + extraCss, + body, + hubFeedId: emergency.author || null + }); +}; diff --git a/src/views/event_view.js b/src/views/event_view.js index c3b29fa..220277c 100644 --- a/src/views/event_view.js +++ b/src/views/event_view.js @@ -1,9 +1,13 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views"); +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td, details, summary, ul, li, hr } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { renderPhotoGallery, renderGalleryFields } = require("./gallery_view"); +const { renderIntervalBlock } = require("./calendars_view"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view"); +const { renderReachChip, renderEncryptedChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const userId = config.keys.id; @@ -18,7 +22,7 @@ exports.renderEventInvitePage = (code) => { }; const opt = (value, isSelected, text) => - option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text); + option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text); const safeArray = (v) => (Array.isArray(v) ? v : []); @@ -29,12 +33,6 @@ const normalizePrivacy = (v) => { const privacyLabel = (v) => (normalizePrivacy(v) === "private" ? i18n.eventPrivate : i18n.eventPublic); -const safeExternalHref = (url) => { - const s = String(url || "").trim(); - const lower = s.toLowerCase(); - if (lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:")) return s; - return ""; -}; const normalizeEventStatus = (v) => { const up = String(v || "").toUpperCase(); @@ -42,6 +40,8 @@ const normalizeEventStatus = (v) => { return up || "OPEN"; }; +const clearnetEligible = (e) => normalizeEventStatus(e.status) !== "CLOSED" && normalizePrivacy(e.isPublic) === "public" && !e.encrypted; + const eventStatusLabel = (v) => { const st = normalizeEventStatus(v); if (st === "OPEN") return i18n.eventStatusOpen; @@ -59,11 +59,6 @@ const renderEventOwnerActions = (e, returnTo) => { { method: "GET", action: `/events/edit/${encodeURIComponent(e.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "update-btn" }, i18n.eventUpdateButton) - ), - form( - { method: "POST", action: `/events/delete/${encodeURIComponent(e.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "delete-btn" }, i18n.eventDeleteButton) ) ]; if (normalizePrivacy(e.isPublic) === "private") { @@ -74,7 +69,8 @@ const renderEventOwnerActions = (e, returnTo) => { if (e.openInviteCode) { actions.push(div({ class: "tribe-open-invite" }, span({ class: "card-label" }, i18n.tribeInviteCodeText), - span({ class: "tribe-open-invite-code" }, e.openInviteCode) + span({ class: "tribe-open-invite-code" }, e.openInviteCode), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/events/${encodeURIComponent(e.openInviteCode)}` }) )); actions.push(form( { method: "POST", action: `/events/open-invite/remove/${encodeURIComponent(e.id)}` }, @@ -95,7 +91,7 @@ const renderEventAttendAction = (e, isAttending, returnTo) => { if (st !== "OPEN") return null; if (e.organizer === userId) return null; if (normalizePrivacy(e.isPublic) === "private" && !isAttending) { - return a({ class: "tribe-action-btn", href: "/invites#invites-events" }, i18n.tribeEnterInvite); + return a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite); } return form( { method: "POST", action: `/events/attend/${encodeURIComponent(e.id)}` }, @@ -105,70 +101,12 @@ const renderEventAttendAction = (e, isAttending, returnTo) => { }; const renderEventCommentsSection = (eventId, comments = [], currentFilter = "all") => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; const returnTo = `/events/${encodeURIComponent(eventId)}?filter=${encodeURIComponent(currentFilter || "all")}`; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/events/${encodeURIComponent(eventId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); - }); - return visibleComments.length - ? div( - { class: "comments-list" }, - visibleComments.map((c) => { - const author = c.value && c.value.author ? c.value.author : ""; - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c.value && c.value.content ? c.value.content : {}; - const root = content.fork || content.root || ""; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(String(text))) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet); - })() - ); + return renderSharedCommentsSection({ + action: `/events/${encodeURIComponent(eventId)}/comments`, + comments: comments, + returnTo: returnTo + }); }; const renderEventStatusChip = (status) => { @@ -176,66 +114,90 @@ const renderEventStatusChip = (status) => { return renderOpenClosedChip(status, { statusChipOPEN: localized, statusChipCLOSED: localized }); }; -const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo) => { +const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo, opts = {}) => { const currentFilter = filter || "all"; const attendees = safeArray(e.attendees); const isPrivate = normalizePrivacy(e.isPublic) === "private"; const isAttending = attendees.includes(userId); const price = parseFloat(e.price || 0); + const origin = e.tribeOrigin || null; + const href = opts.href !== undefined ? opts.href : (origin ? origin.href : `/events/${encodeURIComponent(e.id)}`); const chips = [ - renderEventStatusChip(e.status), renderPrivacyChip(isPrivate, i18n), + renderEventStatusChip(e.status), e.encrypted ? renderStateChip("encrypted", "🔒", i18n.encryptedChipLabel || "E2E") : null, isAttending ? renderStateChip("whole", "★", i18n.eventAttended) : null, - renderLifespanChip(e.lifetime, i18n) + renderLifespanChip(e.lifetime, i18n), + !origin && e.clearnet === true && clearnetEligible(e) ? renderReachChip(true, i18n, clearnetItemHref('events', e.title, e.id)) : null, + origin ? null : e.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (e.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null), + origin ? renderTribeOriginChip(origin) : null, + ...(Array.isArray(opts.extraChips) ? opts.extraChips : []) ].filter(Boolean); const dateText = e.date ? moment(e.date).format("YYYY/MM/DD HH:mm") : ""; const isOwn = e.organizer && String(e.organizer) === String(userId); + const headerActions = opts.headerActions !== undefined + ? opts.headerActions + : origin ? null : renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`, { spread: spreadInfo || null, author: e.organizer || e.author, favKind: 'events', isFavorite: e.isFavorite, reportTitle: e.title, deleteAction: isOwn ? contentDeleteAction('event', e.id) : undefined, returnTo: `/events?filter=${encodeURIComponent(currentFilter)}` }); return div({ class: "trending-card event-card" + (isOwn ? " own-content" : "") }, - div( + headerActions ? div( { class: "card-header activity-card-header" }, span(), - renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`) - ), + headerActions + ) : null, div({ class: "card-section event-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/events/${encodeURIComponent(e.id)}` }, e.title || i18n.eventsTitle) + href ? a({ href }, e.title || i18n.eventsTitle) : (e.title || i18n.eventsTitle) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - dateText ? p({ class: "card-date-highlight" }, dateText) : null, e.location && String(e.location).trim() - ? p({ class: "job-meta-line" }, String(e.location)) + ? div({ class: "card-field card-field-mb" }, + span({ class: "card-label" }, `${String(i18n.eventLocation).toUpperCase()}: `), + span({ class: "card-value" }, String(e.location)) + ) : null, + dateText ? p({ class: "time-chip" }, dateText) : null, price > 0 - ? div({ class: "job-price-line card-salary" }, `${price.toFixed(6)} ECO`) + ? div({ class: "price-chip" }, `${price.toFixed(6)} ECO`) : null, - div({ class: "tribe-card-members" }, + origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`) ), - div({ class: "card-spread-centered" }, renderSpreadButton(e.id, spreadInfo)) + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) ) ); }; +const eventChipFor = (currentFilter, censusEvents) => (mode) => { + if (mode === currentFilter) return true; + if (!Array.isArray(censusEvents)) return true; + const pubE = (e) => normalizePrivacy(e.isPublic) === "public"; + if (mode === "mine") return censusEvents.some((e) => e.organizer === userId); + if (mode === "recent") return censusEvents.length > 0; + if (mode === "today") return censusEvents.some((e) => pubE(e) && moment(e.date).isSame(moment(), "day")); + if (mode === "week") return censusEvents.some((e) => pubE(e) && moment(e.date).isBetween(moment(), moment().add(7, "days"), null, "[]")); + if (mode === "month") return censusEvents.some((e) => pubE(e) && moment(e.date).isBetween(moment(), moment().add(1, "month"), null, "[]")); + if (mode === "year") return censusEvents.some((e) => pubE(e) && moment(e.date).isBetween(moment(), moment().add(1, "year"), null, "[]")); + if (mode === "archived") return censusEvents.some((e) => pubE(e) && normalizeEventStatus(e.status) === "CLOSED"); + return true; +}; + exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { const list = Array.isArray(events) ? events : [events]; const currentFilter = filter || "all"; - const { renderReachChip: renderReachChipEvents } = require('./clearnet_view'); - const viewerClearnetEvents = !!(params.viewerPrefs && params.viewerPrefs.clearnetEvents); - const title = - currentFilter === "mine" ? i18n.eventMineSectionTitle : - currentFilter === "create" ? i18n.eventCreateSectionTitle : - currentFilter === "edit" ? i18n.eventUpdateSectionTitle : - i18n.eventAllSectionTitle; + const title = i18n.eventsTitle; const eventToEdit = list.find((e) => e.id === eventId) || {}; - const editTags = Array.isArray(eventToEdit.tags) ? eventToEdit.tags.filter(Boolean) : []; + const formData = currentFilter === "edit" ? eventToEdit : (params.draft || {}); + const spreadWarning = currentFilter === "edit" ? await renderSpreadEditWarning(eventToEdit && eventToEdit.id) : null; + const editTags = Array.isArray(formData.tags) ? formData.tags.filter(Boolean) : (typeof formData.tags === "string" ? formData.tags.split(",").map(t => t.trim()).filter(Boolean) : []); const canSee = (e) => { const isPub = normalizePrivacy(e.isPublic) === "public"; @@ -245,10 +207,13 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { }; const visible = list.filter(canSee); + const censusEvents = (Array.isArray(params.censusList) ? params.censusList : list).filter(canSee); let filtered; if (currentFilter === "all") { filtered = visible.filter((e) => normalizePrivacy(e.isPublic) === "public"); + } else if (currentFilter === "recent") { + filtered = visible.filter((e) => normalizePrivacy(e.isPublic) === "public" && (Date.parse(e.createdAt || "") || 0) >= Date.now() - 86400000); } else if (currentFilter === "mine") { filtered = visible.filter((e) => e.organizer === userId); } else if (currentFilter === "today") { @@ -270,36 +235,77 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { const minCreate = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm"); const ret = typeof returnTo === "string" && returnTo.startsWith("/events") ? returnTo : "/events?filter=mine"; - const editPrivacy = normalizePrivacy(eventToEdit.isPublic); + const emptyMod = moduleIsEmpty(censusEvents, currentFilter, "all", params.q); + const pubE = (e) => normalizePrivacy(e.isPublic) === "public"; + const eventChipVisible = eventChipFor(currentFilter, censusEvents); + const reachRaw = String(params.reach || "").toLowerCase(); + const editPrivacy = reachRaw === "public" || reachRaw === "private" ? reachRaw : normalizePrivacy(formData.isPublic); + const showClearnet = editPrivacy === "public" && clearnetEligible({ ...formData, isPublic: editPrivacy }); + const editInterval = formData.interval + || (formData.intervalWeekly ? "weekly" : formData.intervalMonthly ? "monthly" : formData.intervalYearly ? "yearly" : ""); + const editIntervalUntil = (formData.recurrenceUntil || formData.intervalDeadline) + ? moment(formData.recurrenceUntil || formData.intervalDeadline).format("YYYY-MM-DDTHH:mm") + : ""; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.eventsTitle), p(i18n.eventsDescription) ), - div({ class: "shop-title-row" }, renderReachChipEvents(viewerClearnetEvents, i18n)), - br(), div( { class: "filters" }, form( { method: "GET", action: "/events" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMine), - button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterToday), - button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterWeek), - button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMonth), - button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterYear), - button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterArchived), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterAll).toUpperCase()), + ...(eventChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: currentFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterRecent).toUpperCase())] : []), + ...(eventChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMine).toUpperCase())] : []), + ...(eventChipVisible("today") ? [button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterToday).toUpperCase())] : []), + ...(eventChipVisible("week") ? [button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterWeek).toUpperCase())] : []), + ...(eventChipVisible("month") ? [button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMonth).toUpperCase())] : []), + ...(eventChipVisible("year") ? [button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterYear).toUpperCase())] : []), + ...(eventChipVisible("archived") ? [button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterArchived).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.eventCreateButton) ) - ) + ), + currentFilter === "edit" || currentFilter === "create" + ? null + : emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, e => normalizeEventStatus(e.status), [{ value: 'OPEN', label: i18n.eventStatusOpen }, { value: 'CLOSED', label: i18n.eventStatusClosed }]), + form({ method: "GET", action: "/events", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: currentFilter }), + input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.eventSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) ), section( currentFilter === "edit" || currentFilter === "create" ? div( { class: "event-form" }, + spreadWarning, + form( + { method: "GET", action: currentFilter === "edit" ? `/events/edit/${encodeURIComponent(eventId)}` : "/events" }, + currentFilter === "edit" ? null : input({ type: "hidden", name: "filter", value: "create" }), + typeof returnTo === "string" && returnTo.startsWith("/events") ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + label(i18n.eventPrivacyLabel), + br(), + div({ class: "apply-row" }, + select( + { name: "isPublic", class: "report-category-select" }, + opt("public", editPrivacy !== "private", i18n.eventPublic), + opt("private", editPrivacy === "private", i18n.eventPrivate) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, privacyLabel(editPrivacy)), form( { action: currentFilter === "edit" ? `/events/update/${encodeURIComponent(eventId)}` : "/events/create", @@ -307,27 +313,27 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: ret }), + input({ type: "hidden", name: "isPublic", value: editPrivacy }), label(i18n.eventTitleLabel), br(), input({ type: "text", - name: "title", + name: "title", maxlength: "100", id: "title", required: true, - value: currentFilter === "edit" ? eventToEdit.title || "" : "" + value: formData.title || "" }), br(), label(i18n.eventDescriptionLabel), br(), textarea( { name: "description", id: "description", placeholder: i18n.eventDescriptionPlaceholder, rows: "4" }, - currentFilter === "edit" ? eventToEdit.description || "" : "" + formData.description || "" ), br(), - label(i18n.uploadMedia), - br(), - input({ type: "file", name: "image", accept: "image/*" }), - br(), + ...renderGalleryFields(formData, currentFilter === "edit"), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob" }), br(), br(), br(), label(i18n.eventDateLabel), br(), @@ -336,20 +342,11 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { name: "date", id: "date", required: true, - min: currentFilter === "create" ? minCreate : undefined, - value: currentFilter === "edit" && eventToEdit.date ? moment(eventToEdit.date).format("YYYY-MM-DDTHH:mm") : "" + min: minCreate, + value: formData.date ? moment(formData.date).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), - label(i18n.eventPrivacyLabel), - br(), - select( - { name: "isPublic", id: "isPublic" }, - opt("public", editPrivacy !== "private", i18n.eventPublic), - opt("private", editPrivacy === "private", i18n.eventPrivate) - ), - br(), - br(), label(i18n.eventLocationLabel), br(), input({ @@ -357,17 +354,17 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { name: "location", id: "location", required: true, - value: currentFilter === "edit" ? eventToEdit.location || "" : "" + value: formData.location || "" }), br(), label(i18n.mapLocationTitle || "Map Location"), br(), - input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: eventToEdit?.mapUrl || "" }), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: formData.mapUrl || "" }), br(), br(), label(i18n.eventUrlLabel), br(), - input({ type: "url", name: "url", id: "url", value: currentFilter === "edit" ? eventToEdit.url || "" : "" }), + input({ type: "url", name: "url", id: "url", value: formData.url || "" }), br(), br(), label(i18n.eventPriceLabel), @@ -378,15 +375,23 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { id: "price", min: "0.000000", step: "0.000001", - value: currentFilter === "edit" ? parseFloat(eventToEdit.price || 0).toFixed(6) : (0).toFixed(6) + value: parseFloat(formData.price || 0).toFixed(6) }), + renderEcoValueChip(), br(), br(), label(i18n.eventTagsLabel), br(), - input({ type: "text", name: "tags", id: "tags", value: currentFilter === "edit" ? editTags.join(", ") : "" }), + input({ type: "text", name: "tags", id: "tags", placeholder: i18n.tagsPlaceholder, value: editTags.join(", ") }), br(), br(), + div({ class: "event-recurrence-block" }, + label(i18n.eventRecurrenceLabel), + br(), + renderIntervalBlock({ min: minCreate }, editInterval, editIntervalUntil) + ), + br(), + showClearnet ? renderClearnetSelector(formData.clearnet === true || String(formData.clearnet || "") === "1", i18n) : null, ...(currentFilter === "create" ? [ input({ type: "hidden", name: "addToCalendar", value: "0" }), label(i18n.eventAddToCalendar || "Add to Calendar"), @@ -399,13 +404,14 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { ) ) : filtered.length > 0 - ? div({ class: "jobs-grid" }, filtered.map((e) => renderEventItem(e, currentFilter, params.spreadMap && params.spreadMap.get(e.id)))) - : p(i18n.noevents) + ? div({ class: "jobs-grid" }, paged(filtered).map((e) => renderEventItem(e, currentFilter, params.spreadMap && params.spreadMap.get(e.id)))) + : div({ class: "no-content-box" }, p(i18n.noevents)) ) ); }; exports.singleEventView = async (event, filter, comments = [], params = {}) => { + const eventChipVisible = (mode) => eventChipFor(currentFilter, Array.isArray(params.censusList) ? params.censusList : null)(mode); const currentFilter = filter || "all"; const commentCount = typeof event.commentCount === "number" ? event.commentCount : 0; const attendees = safeArray(event.attendees); @@ -419,13 +425,14 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { { class: "filters" }, form( { method: "GET", action: "/events" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMine), - button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterToday), - button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterWeek), - button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMonth), - button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterYear), - button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterArchived), + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterAll).toUpperCase()), + ...(eventChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: currentFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterRecent).toUpperCase())] : []), + ...(eventChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMine).toUpperCase())] : []), + ...(eventChipVisible("today") ? [button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterToday).toUpperCase())] : []), + ...(eventChipVisible("week") ? [button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterWeek).toUpperCase())] : []), + ...(eventChipVisible("month") ? [button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMonth).toUpperCase())] : []), + ...(eventChipVisible("year") ? [button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterYear).toUpperCase())] : []), + ...(eventChipVisible("archived") ? [button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterArchived).toUpperCase())] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.eventCreateButton) ) ); @@ -437,38 +444,39 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { ); } - const { renderReachChip, renderEncryptedChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetEvents && normalizeEventStatus(event.status) !== 'CLOSED' && normalizePrivacy(event.isPublic) === 'public'); + const canClearnet = clearnetEligible(event); + const isClearnet = canClearnet && !!event.clearnet; const isPrivate = normalizePrivacy(event.isPublic) === 'private'; const isEncrypted = !!event.encrypted || isPrivate; const isAttending = attendees.includes(userId); const isOrganizer = String(event.organizer) === String(userId); + const isEventOwner = String(event.organizer || event.author) === String(userId); + const sharesEvent = isEventOwner || attendees.includes(userId); + const eventSubIn = isEventOwner || (event.subscription && event.subscription.subscribed === true); const returnToSelf = `/events/${encodeURIComponent(event.id)}?filter=${encodeURIComponent(currentFilter)}`; const chips = [ - renderEventStatusChip(event.status), renderPrivacyChip(isPrivate, i18n), - isEncrypted ? renderEncryptedChip(i18n) : renderReachChip(isClearnet, i18n), + renderEventStatusChip(event.status), + isEncrypted ? renderEncryptedChip(i18n) : renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('events', event.title, event.id) : null), + isOrganizer && canClearnet ? renderClearnetSwitch('events', event.rootId || event.id, isClearnet) : null, isAttending ? renderStateChip("whole", "★", i18n.eventAttended) : null, renderLifespanChip(event.lifetime, i18n), - renderEcoTax(event.msgSize, event.id) + renderEcoTax(event.msgSize, event.id), + (event.subscription && sharesEvent) + ? renderStateChip(eventSubIn ? "mutuals" : "closed", "✉", eventSubIn ? i18n.subscriptionOn : i18n.subscriptionOff) + : null ].filter(Boolean); const sideActions = []; - if (event.organizer && event.organizer !== userId) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: event.organizer }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } const attendNode = renderEventAttendAction(event, isAttending, returnToSelf); if (attendNode) sideActions.push(attendNode); if (params.linkedCalendarId) { sideActions.push(form({ method: "GET", action: `/calendars/${encodeURIComponent(params.linkedCalendarId)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.eventVisitCalendar || "Visit calendar") + button({ type: "submit", class: "tribe-action-btn" }, i18n.eventVisitCalendar || "Visit calendar") )); } - if (isOrganizer) sideActions.push(...renderEventOwnerActions(event, returnToSelf)); + const ownerActions = isOrganizer ? renderEventOwnerActions(event, returnToSelf) : []; const tagsNode = event.tags && event.tags.filter(Boolean).length ? div({ class: "card-tags" }, @@ -482,7 +490,16 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { td({ class: "tribe-info-label" }, labelText), td({ class: "tribe-info-value" }, valueNode) )); - if (event.date) pushRow(i18n.eventDateLabel, moment(event.date).format("YYYY/MM/DD HH:mm")); + if (event.date) pushRow(i18n.eventDateLabel, moment(event.recurring && event.nextDate ? event.nextDate : event.date).format("YYYY/MM/DD HH:mm")); + if (event.recurring) { + const parts = [ + event.intervalWeekly ? (i18n.calendarIntervalWeekly || "Weekly") : null, + event.intervalMonthly ? (i18n.calendarIntervalMonthly || "Monthly") : null, + event.intervalYearly ? (i18n.calendarIntervalYearly || "Yearly") : null + ].filter(Boolean); + pushRow(i18n.eventRecurrenceLabel, parts.join(" · ")); + if (event.recurrenceUntil) pushRow(i18n.eventRecurrenceUntil, moment(event.recurrenceUntil).format("YYYY/MM/DD HH:mm")); + } pushRow(i18n.eventStatus, eventStatusLabel(event.status)); pushRow(i18n.eventPrivacyLabel, privacyLabel(event.isPublic)); if (event.location && String(event.location).trim()) pushRow(i18n.eventLocationLabel, event.location); @@ -497,18 +514,41 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { : null; const eventSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(event.id, returnToSelf, { spread: params.spreads || null, author: event.organizer || event.author, favKind: 'events', isFavorite: event.isFavorite, reportTitle: event.title, deleteAction: isOrganizer ? contentDeleteAction('event', event.id) : undefined, returnTo: `/events?filter=${encodeURIComponent(currentFilter)}` }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, event.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(event.id, params.spreads)), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + renderPhotoGallery(event, 'event'), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(event.createdAt, event.organizer)), + event.recurring && Array.isArray(event.occurrences) && event.occurrences.length > 1 + ? details({ class: "event-occurrences" }, + summary({ class: "chat-thread-summary" }, `${i18n.eventUpcomingDates} (${event.occurrences.length})`), + ul({ class: "event-occurrence-list" }, + ...event.occurrences.map(d => li(moment(d).format("YYYY/MM/DD HH:mm"))) + ) + ) + : null, tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`) ), attendeesListNode, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + (event.subscription && sharesEvent) + ? renderSubscriptionBox({ + target: event.id, + scope: "events", + subscribed: event.subscription.subscribed === true, + count: event.subscription.count, + isOwner: isEventOwner, + returnTo: returnToSelf + }) + : null, + renderDocumentActions('events', event.id) ); const returnToOpinions = `/events/${encodeURIComponent(event.id)}?filter=${encodeURIComponent(currentFilter)}`; @@ -518,21 +558,17 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { event.description ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.eventDescriptionLabel), - p({ class: "tribe-side-description" }, ...renderUrl(event.description)) + p({ class: "tribe-side-description" }, ...renderStyledText(event.description)) ) : null, event.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, event.mapUrl)) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(event.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(event.organizer) - ), - opinionsBar, - renderEventCommentsSection(event.id, comments, currentFilter) + renderEngagement(event.id, opinionsBar, renderEventCommentsSection(event.id, comments, currentFilter)) ); return template( event.title, section( + div({ class: "tags-header module-header-line" }, h2(i18n.eventsTitle), p(i18n.eventsDescription)), filterBar, div({ class: "tribe-details" }, eventSide, eventMain) ) @@ -540,10 +576,10 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { }; exports.clearnetEventView = async (event) => { - const { escapeHtml: esc, renderClearnetPage } = require('./clearnet_view'); - const title = esc(event.title || 'Event'); - const desc = esc(event.description || ''); - const dateStr = event.date ? esc(moment(event.date).format("YYYY-MM-DD HH:mm")) : ''; + const { escapeHtml: esc, renderRichText, renderKindTag, renderClearnetPage } = require('./clearnet_view'); + const title = esc(event.title || i18n.cnKindEvent); + const desc = renderRichText(event.description || ''); + const dateStr = event.date ? esc(moment(event.date).format("YYYY/MM/DD HH:mm")) : ''; const loc = esc(event.location || ''); const price = parseFloat(event.price || 0); const priceStr = price > 0 ? `${price.toFixed(2)} ECO` : ''; @@ -559,15 +595,17 @@ exports.clearnetEventView = async (event) => { const body = `

${title}

+ ${renderKindTag('event')} ${dateStr ? `📅 ${dateStr}` : ''} ${loc ? `📍 ${loc}` : ''} ${priceStr ? `💰 ${priceStr}` : ''}
+
${desc ? `

${desc}

` : ''} - ${urlHref ? `More info →` : ''} + ${urlHref ? `${esc(i18n.cnMoreInfo)}` : ''} `; return renderClearnetPage({ - title: `${event.title || 'Event'} — Oasis`, + title: `${event.title || 'Event'} | Oasis`, ogTitle: event.title || 'Event', ogDescription: event.description || '', extraCss, diff --git a/src/views/favorites_view.js b/src/views/favorites_view.js index a5e3106..4642562 100644 --- a/src/views/favorites_view.js +++ b/src/views/favorites_view.js @@ -1,15 +1,43 @@ const { form, button, div, h2, p, section, input, a, span, img } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions } = require("./main_views"); +const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); const userId = config.keys.id; const safeArr = (v) => (Array.isArray(v) ? v : []); const safeText = (v) => String(v || "").trim(); +const TORRENTABLE_KINDS = new Set(Object.keys(CONTENT_FAV_KIND).filter((t) => CONTENT_TORRENTABLE.has(t)).map((t) => CONTENT_FAV_KIND[t])); + +const FILTER_KINDS = [ + { value: "audios", label: () => i18n.favoritesFilterAudios }, + { value: "bookmarks", label: () => i18n.favoritesFilterBookmarks }, + { value: "calendars", label: () => i18n.favoritesFilterCalendars }, + { value: "chats", label: () => i18n.favoritesFilterChats }, + { value: "documents", label: () => i18n.favoritesFilterDocuments }, + { value: "events", label: () => i18n.favoritesFilterEvents }, + { value: "files", label: () => i18n.favoritesFilterFiles }, + { value: "forum", label: () => i18n.favoritesFilterForum }, + { value: "housing", label: () => i18n.favoritesFilterHousing }, + { value: "images", label: () => i18n.favoritesFilterImages }, + { value: "jobs", label: () => i18n.favoritesFilterJobs }, + { value: "maps", label: () => i18n.favoritesFilterMaps }, + { value: "market", label: () => i18n.favoritesFilterMarket }, + { value: "pads", label: () => i18n.favoritesFilterPads }, + { value: "projects", label: () => i18n.favoritesFilterProjects }, + { value: "reports", label: () => i18n.favoritesFilterReports }, + { value: "rooms", label: () => i18n.favoritesFilterRooms }, + { value: "shopProducts", label: () => i18n.favoritesFilterShopProducts }, + { value: "tasks", label: () => i18n.favoritesFilterTasks }, + { value: "torrents", label: () => i18n.favoritesFilterTorrents }, + { value: "transfers", label: () => i18n.favoritesFilterTransfers }, + { value: "videos", label: () => i18n.favoritesFilterVideos }, + { value: "votes", label: () => i18n.favoritesFilterVotes } +]; + const buildReturnTo = (filter) => { const f = safeText(filter || "all"); return `/favorites?filter=${encodeURIComponent(f)}`; @@ -25,12 +53,53 @@ const renderTags = (tags) => { : null; }; +const DETAIL_KEYS = [ + "status", "category", "severity", "priority", "location", "place", + "date", "deadline", "startTime", "endTime", "price", "salary", "amount", + "concept", "question", "housing_type", "property_type", "item_type", "sector" +]; + +const fieldLabel = (key) => { + const camel = key.replace(/_(.)/g, (_, c) => c.toUpperCase()); + return i18n[camel + "Label"] || i18n["search" + camel.charAt(0).toUpperCase() + camel.slice(1)] || camel.toUpperCase(); +}; + +const fieldValue = (v) => { + if (v === null || v === undefined) return ""; + if (Array.isArray(v)) return v.filter(Boolean).join(", "); + if (typeof v === "object") return ""; + return String(v).trim(); +}; + +const renderDetailFields = (item) => { + const content = item && item.content; + if (!content || typeof content !== "object") return null; + const rows = DETAIL_KEYS + .map((k) => [k, fieldValue(content[k])]) + .filter(([, v]) => v) + .map(([k, v]) => + div({ class: "card-field" }, + span({ class: "card-label" }, `${fieldLabel(k)}: `), + span({ class: "card-value" }, v) + ) + ); + const members = ["members", "participants", "attendees", "assignees", "confirmedBy"] + .map((k) => [k, Array.isArray(content[k]) ? content[k].length : null]) + .find(([, n]) => typeof n === "number" && n > 0); + if (members) { + rows.push(div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${fieldLabel(members[0])}: ${members[1]}`) + )); + } + return rows.length ? div({ class: "cv-card-fields" }, ...rows) : null; +}; + const renderBookmarkUrl = (item) => { if (item.kind !== "bookmarks") return null; if (!item.url) return null; return p( a( - { href: item.url, target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, + { href: safeExternalHref(item.url), target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, item.url ) ); @@ -54,13 +123,13 @@ const renderImagePreview = (item) => { ); }; -const renderFavoriteCard = (item, filter) => { +const renderFavoriteCard = (item, filter, spreadMap = null) => { const returnTo = buildReturnTo(filter); const title = safeText(item.title) || safeText(item.name) || safeText(item.category) || safeText(item.url) || ""; const ts = item.updatedAt || item.createdAt; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; const isOwn = item.author && String(item.author) === String(userId); return div( @@ -72,48 +141,48 @@ const renderFavoriteCard = (item, filter) => { span({ class: 'pm-exposition-text' }, String(item.kind || '').toUpperCase()) ) ), - renderContentActions(item.favId, item.viewHref) + renderContentActions(item.favId, item.viewHref, { + author: item.author, + spread: (spreadMap && spreadMap.get(item.favId)) || null, + reportTitle: title, + ...(TORRENTABLE_KINDS.has(item.kind) && typeof item.url === "string" && item.url.startsWith("&") ? { torrentFrom: { blobId: item.url, name: item.title } } : {}) + }) ), div( { class: "card-section favorites-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - form( - { - method: "POST", - action: `/favorites/remove/${encodeURIComponent(item.kind)}/${encodeURIComponent(item.favId)}`, - class: "bookmark-favorite-form" - }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "filter-btn" }, i18n.favoritesRemoveButton) - ) - ) - ), - title ? h2(title) : null, + title ? div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, item.viewHref ? a({ href: item.viewHref }, title) : title)) : null, renderImagePreview(item), renderBookmarkUrl(item), - safeText(item.description) ? p(...renderUrl(item.description)) : null, + safeText(item.description) ? p(...renderStyledText(item.description)) : null, + renderDetailFields(item), renderTags(item.tags), p( - { class: "card-footer" }, - absDate ? span({ class: "date-link" }, `${absDate} ${i18n.performed} `) : "", + { class: "card-footer favorites-meta" }, + absDate ? span({ class: "date-link" }, `${absDate}`) : "", item.author ? userLink(item.author) : "" + ), + p( + { class: "card-footer" }, + form( + { method: "POST", action: `/favorites/remove/${encodeURIComponent(item.kind)}/${encodeURIComponent(item.favId)}`, class: "phone-action-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.agendaDiscardButton).toUpperCase()) + ) ) ) ); }; -exports.favoritesView = async (items, filter = "all", counts = {}) => { +exports.favoritesView = async (items, filter = "all", counts = {}, q = "", params = {}) => { const c = counts || {}; const total = typeof c.all === "number" ? c.all : safeArr(items).length; + const emptyFav = total === 0 && !String(q || "").trim(); return template( i18n.favoritesTitle, section( - div({ class: "tags-header" }, h2(i18n.favoritesTitle), p(i18n.favoritesDescription)), - div( + div({ class: "tags-header module-header-line" }, h2(i18n.favoritesTitle), p(i18n.favoritesDescription)), + emptyFav ? null : div( { class: "filters" }, form( { method: "GET", action: "/favorites", class: "ui-toolbar ui-toolbar--filters" }, @@ -121,61 +190,33 @@ exports.favoritesView = async (items, filter = "all", counts = {}) => { { type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, `${i18n.favoritesFilterAll} (${total})` ), - button( - { type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterRecent} (${total})` - ), - button( - { type: "submit", name: "filter", value: "audios", class: filter === "audios" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterAudios} (${c.audios || 0})` - ), - button( - { type: "submit", name: "filter", value: "bookmarks", class: filter === "bookmarks" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterBookmarks} (${c.bookmarks || 0})` - ), - button( - { type: "submit", name: "filter", value: "documents", class: filter === "documents" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterDocuments} (${c.documents || 0})` - ), - button( - { type: "submit", name: "filter", value: "images", class: filter === "images" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterImages} (${c.images || 0})` - ), - button( - { type: "submit", name: "filter", value: "maps", class: filter === "maps" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterMaps} (${c.maps || 0})` - ), - button( - { type: "submit", name: "filter", value: "pads", class: filter === "pads" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterPads || "PADS"} (${c.pads || 0})` - ), - button( - { type: "submit", name: "filter", value: "chats", class: filter === "chats" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterChats || "CHATS"} (${c.chats || 0})` - ), - button( - { type: "submit", name: "filter", value: "calendars", class: filter === "calendars" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterCalendars || "CALENDARS"} (${c.calendars || 0})` - ), - button( - { type: "submit", name: "filter", value: "videos", class: filter === "videos" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterVideos} (${c.videos || 0})` - ), - button( - { type: "submit", name: "filter", value: "torrents", class: filter === "torrents" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterTorrents || "TORRENTS"} (${c.torrents || 0})` - ), - button( - { type: "submit", name: "filter", value: "market", class: filter === "market" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterMarket || "MARKET"} (${c.market || 0})` - ), - button( - { type: "submit", name: "filter", value: "shopProducts", class: filter === "shopProducts" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterShopProducts || "SHOP ITEMS"} (${c.shopProducts || 0})` + (c.recent || 0) > 0 || filter === "recent" + ? button( + { type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterRecent} (${c.recent || 0})` + ) + : null, + ...FILTER_KINDS.filter((k) => (c[k.value] || 0) > 0).map((k) => + button( + { type: "submit", name: "filter", value: k.value, class: filter === k.value ? "filter-btn active" : "filter-btn" }, + `${k.label()} (${c[k.value] || 0})` + ) ) ) ), - div({ class: "bookmark-list" }, safeArr(items).length ? safeArr(items).map((it) => renderFavoriteCard(it, filter)) : p(i18n.favoritesNoItems)) + emptyFav ? null : div( + { class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(items.length), + form( + { method: "GET", action: "/favorites", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.favoritesSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ), + div({ class: "bookmark-list" }, safeArr(items).length ? paged(safeArr(items)).map((it) => renderFavoriteCard(it, filter, params.spreadMap)) : p(i18n.favoritesNoItems)) ) ); }; diff --git a/src/views/fediverse_view.js b/src/views/fediverse_view.js index c651941..580eab7 100644 --- a/src/views/fediverse_view.js +++ b/src/views/fediverse_view.js @@ -1,12 +1,14 @@ -const { div, h2, h3, p, section, button, form, a, span, strong, input, label, img, textarea, br, hr, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); +const { div, h2, h3, h4, p, section, button, form, a, span, strong, input, label, img, textarea, br, hr, select, option, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); const { template, i18n } = require("./main_views"); +const { renderZoomableImage } = require("./gallery_view"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); const moment = require("../server/node_modules/moment"); const fmtDate = (ts) => { if (!ts) return ""; const m = moment(ts); - return m.isValid() ? m.format("YYYY-MM-DD HH:mm") : ""; + return m.isValid() ? m.format("YYYY/MM/DD HH:mm") : ""; }; const renderMedia = (m) => { @@ -17,7 +19,7 @@ const renderMedia = (m) => { if (m.type === 'audio') { return audioHyperaxe({ class: "post-audio", src: m.url, controls: true }); } - return img({ class: "post-image", src: m.url, alt: m.description || "" }); + return renderZoomableImage(m.url, { imgClass: "post-image", alt: m.description || "" }); }; const renderComposeForm = (opts = {}) => { @@ -26,7 +28,7 @@ const renderComposeForm = (opts = {}) => { form( { method: "POST", action, enctype: "multipart/form-data" }, replyToId ? input({ type: "hidden", name: "inReplyToId", value: replyToId }) : "", - textarea({ name: "text", id: "text", rows: "6", class: "publish-textarea", placeholder: placeholder || i18n.fediverseComposePlaceholder }), + textarea({ maxlength: "5000", name: "text", id: "text", rows: "6", class: "publish-textarea", placeholder: placeholder || i18n.fediverseComposePlaceholder }), br(), label({ for: "fediverse_media" }, i18n.fediverseAttach), br(), @@ -67,7 +69,7 @@ const renderPost = (post, returnTo, opts = {}) => { ), span({ class: "fediverse-date" }, fmtDate(post.createdAt)) ), - div({ class: "feed-text", innerHTML: sanitizeHtml(post.html) }), + div({ class: "feed-text", [RAW_HTML]: sanitizeHtml(post.html) }), post.media && post.media.length ? div({ class: "fediverse-media" }, post.media.map(renderMedia)) : "", opts.noActions ? "" : renderActions(post, returnTo) ); @@ -90,7 +92,7 @@ const mastodonBox = (account, stats, actions, showLabel) => { ), actions && actions.length ? div({ class: "fediverse-compact-actions" }, actions) : "" ), - (stats && stats.bio) ? div({ class: "fediverse-bio", innerHTML: sanitizeHtml(stats.bio) }) : "", + (stats && stats.bio) ? div({ class: "fediverse-bio", [RAW_HTML]: sanitizeHtml(stats.bio) }) : "", stats ? div({ class: "fediverse-stats" }, stat(i18n.fediverseFollowers, stats.followers), @@ -102,7 +104,7 @@ const mastodonBox = (account, stats, actions, showLabel) => { (stats && Array.isArray(stats.fields) && stats.fields.length) ? div({ class: "fediverse-fields" }, stats.fields.map(f => div({ class: "fediverse-field" }, span({ class: "fediverse-field-name" }, f.name, f.verified ? span({ class: "fediverse-field-verified", title: "verified" }, " ✓") : ""), - span({ class: "fediverse-field-value", innerHTML: sanitizeHtml(f.value) }) + span({ class: "fediverse-field-value", [RAW_HTML]: sanitizeHtml(f.value) }) ))) : "" ) @@ -111,7 +113,7 @@ const mastodonBox = (account, stats, actions, showLabel) => { const disconnectedBlock = () => { const text = String(i18n.fediverseDisconnected || ""); - const settingsLink = a({ href: "/settings#fediverse" }, i18n.fediverseEmptyLink || "settings"); + const settingsLink = a({ href: "/settings?filter=multiverse" }, i18n.fediverseEmptyLink || "settings"); const parts = text.split("%LINK%"); const para = parts.length > 1 ? p(parts[0], settingsLink, parts.slice(1).join("%LINK%")) @@ -119,16 +121,322 @@ const disconnectedBlock = () => { return div({ class: "fediverse-disconnected" }, h2(i18n.fediverseStatus), para); }; -exports.fediverseOverviewView = ({ account, stats } = {}) => { - const box = account - ? mastodonBox(account, stats, [ - form({ method: "GET", action: "/fediverse/mastodon" }, button({ type: "submit", class: "filter-btn" }, i18n.fediverseManage)) - ], true) - : null; + +const telegramBox = (account, stats, actions, showLabel) => { + const stat = (lbl, val) => div({ class: "fediverse-stat" }, span({ class: "fediverse-stat-label" }, lbl), strong(String(val))); + const handle = account.username ? `@${account.username}` : (account.phone || ""); + return div({ class: "fediverse-network" }, + showLabel ? h3("Telegram") : "", + div({ class: "fediverse-profile" }, + div({ class: "fediverse-namerow" }, + account.avatar ? img({ class: "fediverse-avatar", src: account.avatar, alt: account.displayName }) : "", + div({ class: "fediverse-profile-id" }, + div({ class: "fediverse-name" }, account.displayName || handle), + span({ class: "fediverse-acct" }, handle) + ), + actions && actions.length ? div({ class: "fediverse-compact-actions" }, actions) : "" + ), + stats + ? div({ class: "fediverse-stats" }, + stat(i18n.telegramChats, stats.chats), + stat(i18n.telegramUnread, stats.unread) + ) + : "" + ) + ); +}; + +const telegramKindLabel = (kind) => kind === 'channel' ? i18n.telegramChannel : kind === 'group' ? i18n.telegramGroup : i18n.telegramPrivate; + +const renderTelegramMedia = (m) => { + if (!m || !m.url) return ""; + if (m.kind === 'photo') return renderZoomableImage(m.url, { imgClass: "post-image", alt: m.name || "" }); + if (m.kind === 'video') return videoHyperaxe({ class: "post-video", src: m.url, controls: true, preload: "metadata" }); + if (m.kind === 'audio') return audioHyperaxe({ class: "post-audio", src: m.url, controls: true }); + return a({ href: m.url, class: "filter-btn" }, `📎 ${m.name || m.mime || "file"}`); +}; + +const renderTelegramDialog = (d) => { + const href = `/fediverse/telegram/chat/${encodeURIComponent(d.id)}#telegram-latest`; + const preview = d.last ? ((d.last.out ? "→ " : "") + (d.last.text || (d.last.hasMedia ? "📎" : ""))) : ""; + return div({ class: "feed-card fediverse-card telegram-dialog" + (d.unread > 0 ? " telegram-unread" : "") }, + div({ class: "fediverse-head" }, + img({ class: "fediverse-avatar", src: d.avatar, alt: "" }), + div({ class: "fediverse-author" }, + a({ class: "fediverse-name", href }, d.title), + span({ class: "fediverse-acct" }, telegramKindLabel(d.kind)) + ), + d.unread > 0 ? span({ class: "telegram-unread-badge" }, String(d.unread)) : "", + span({ class: "fediverse-date" }, fmtDate(d.date)) + ), + preview ? p({ class: "telegram-preview" }, preview.length > 160 ? `${preview.slice(0, 160)}…` : preview) : "", + div({ class: "fediverse-actions" }, + form({ method: "GET", action: href }, button({ type: "submit", class: "filter-btn" }, i18n.telegramOpen)) + ) + ); +}; + +const renderTelegramMessage = (m) => div({ class: "feed-card fediverse-card telegram-message" + (m.out ? " telegram-out" : "") }, + div({ class: "fediverse-head" }, + span({ class: "fediverse-name" }, m.out ? i18n.telegramYou : (m.senderName || "")), + span({ class: "fediverse-date" }, fmtDate(m.date)) + ), + m.text ? div({ class: "feed-text telegram-text" }, m.text) : "", + m.media ? div({ class: "fediverse-media" }, renderTelegramMedia(m.media)) : "" +); + +exports.telegramDialogsView = ({ account, stats, dialogs, error } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const body = []; + body.push(div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription))); + body.push(telegramBox(account, stats, [ + a({ href: "/fediverse", class: "filter-btn" }, `← ${i18n.fediverse}`), + a({ href: "/fediverse/telegram?refresh=1", class: "filter-btn" }, i18n.fediverseRefresh) + ], false)); + if (error) body.push(div({ class: "fediverse-error" }, i18n[error] || i18n.telegramErrConnect)); + const list = Array.isArray(dialogs) ? dialogs : []; + if (!list.length && !error) body.push(p({ class: "muted" }, i18n.telegramNoChats)); + else body.push(section({ class: "feed-container" }, list.map(renderTelegramDialog))); + return template(i18n.fediverse, section(...body)); +}; + +exports.telegramChatView = ({ account, stats, chat, error } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const header = section( + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + telegramBox(account, stats, [ + a({ href: "/fediverse/telegram", class: "filter-btn" }, `← ${i18n.telegramChats}`) + ], false) + ); + if (!chat) return template(i18n.fediverse, header, section(div({ class: "fediverse-error" }, i18n[error] || i18n.telegramErrFetch))); + const body = []; + body.push(div({ class: "telegram-chat-title" }, + h3(chat.title), + span({ class: "fediverse-acct" }, telegramKindLabel(chat.kind)), + a({ href: "#telegram-latest", class: "filter-btn chat-jump-latest" }, i18n.chatJumpLatest) + )); + if (error) body.push(div({ class: "fediverse-error" }, i18n[error] || i18n.telegramErrConnect)); + const list = Array.isArray(chat.messages) ? chat.messages : []; + if (!list.length) body.push(p({ class: "muted" }, i18n.telegramNoMessages)); + else body.push(section({ class: "feed-container" }, + list.map((m, idx) => { + const node = renderTelegramMessage(m); + return idx === list.length - 1 ? div({ id: "telegram-latest", class: "chat-latest-anchor" }, node) : node; + }) + )); + body.push(br(), hr(), br()); + body.push(div({ class: "publish-form" }, + form({ method: "POST", action: `/fediverse/telegram/chat/${encodeURIComponent(chat.id)}/send`, enctype: "multipart/form-data" }, + textarea({ maxlength: "4096", name: "text", rows: "4", class: "publish-textarea", placeholder: i18n.telegramComposePlaceholder }), + br(), + label({ for: "telegram_media" }, i18n.fediverseAttach), br(), + input({ type: "file", id: "telegram_media", name: "media", accept: "image/*,video/*,audio/*,application/pdf" }), + br(), br(), + button({ type: "submit", class: "filter-btn" }, i18n.telegramSend) + ) + )); + return template(i18n.fediverse, header, section(...body)); +}; + + +const fmtDuration = (sec) => { + const n = Math.max(0, Math.floor(Number(sec) || 0)); + const h = Math.floor(n / 3600), m = Math.floor((n % 3600) / 60), sInt = n % 60; + const mm = String(m).padStart(h ? 2 : 1, "0"), ss = String(sInt).padStart(2, "0"); + return h ? `${h}:${mm}:${ss}` : `${mm}:${ss}`; +}; + +const fmtBytes = (n) => { + const b = Number(n) || 0; + if (b >= 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB`; + if (b >= 1024 ** 2) return `${(b / 1024 ** 2).toFixed(1)} MB`; + if (b >= 1024) return `${(b / 1024).toFixed(0)} KB`; + return `${b} B`; +}; + +const peertubeBox = (account, stats, actions, showLabel) => { + const stat = (lbl, val) => div({ class: "fediverse-stat" }, span({ class: "fediverse-stat-label" }, lbl), strong(String(val))); + return div({ class: "fediverse-network" }, + showLabel ? h3("PeerTube") : "", + div({ class: "fediverse-profile" }, + div({ class: "fediverse-namerow" }, + account.avatar ? img({ class: "fediverse-avatar", src: account.avatar, alt: account.displayName }) : "", + div({ class: "fediverse-profile-id" }, + div({ class: "fediverse-name" }, account.displayName || account.handle), + a({ class: "fediverse-acct", href: account.channelUrl || account.profileUrl, target: "_blank", rel: "noopener noreferrer" }, account.handle) + ), + actions && actions.length ? div({ class: "fediverse-compact-actions" }, actions) : "" + ), + stats + ? div({ class: "fediverse-stats" }, + stat(i18n.peertubeVideos, stats.videos), + stat(i18n.peertubeSubscriptions, stats.subscriptions), + stat(i18n.peertubeFollowers, stats.followers), + stats.quota > 0 ? stat(i18n.peertubeQuota, `${fmtBytes(stats.quotaUsed)} / ${fmtBytes(stats.quota)}`) : "" + ) + : "" + ) + ); +}; + +const peertubeHeader = (account, stats, actions) => section( + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + peertubeBox(account, stats, actions, false) +); + +const renderPeertubeVideo = (v) => { + const href = `/fediverse/peertube/video/${encodeURIComponent(v.id)}`; + const ch = v.channel || {}; + const chips = []; + if (v.privacyId && v.privacyId !== 1 && v.privacy) chips.push(span({ class: "card-label" }, v.privacy)); + if (v.state && !/published/i.test(v.state)) chips.push(span({ class: "card-label" }, v.state)); + return div({ class: "feed-card fediverse-card peertube-card" }, + a({ href, class: "peertube-thumb" }, + v.thumbnail ? img({ src: v.thumbnail, alt: v.name, loading: "lazy" }) : div({ class: "peertube-thumb-empty" }), + v.duration ? span({ class: "peertube-duration" }, fmtDuration(v.duration)) : "" + ), + div({ class: "peertube-card-body" }, + a({ href, class: "peertube-title" }, v.name || v.id), + div({ class: "peertube-meta" }, + ch.avatar ? img({ class: "peertube-channel-avatar", src: ch.avatar, alt: "" }) : "", + span({ class: "fediverse-acct" }, ch.displayName || ch.handle || ""), + span({ class: "fediverse-date" }, fmtDate(v.publishedAt)), + span(`${v.views} ${i18n.peertubeViews} · ${v.likes} ${i18n.peertubeLikes}`) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : "" + ) + ); +}; + +exports.peertubeFeedView = ({ account, stats, videos, kind, error, notice } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const current = kind === "mine" ? "mine" : "subscriptions"; + const body = []; + if (error) body.push(div({ class: "fediverse-error" }, i18n[error] || i18n.peertubeErrConnect)); + if (notice) body.push(div({ class: "feed-success-msg" }, p(i18n[notice] || String(notice)))); + body.push(div({ class: "mode-buttons-row" }, + form({ method: "GET", action: "/fediverse/peertube" }, + button({ type: "submit", name: "filter", value: "subscriptions", class: current === "subscriptions" ? "filter-btn active" : "filter-btn" }, String(i18n.peertubeSubscriptions).toUpperCase()), + button({ type: "submit", name: "filter", value: "mine", class: current === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.peertubeMyVideos).toUpperCase()) + ) + )); + const list = Array.isArray(videos) ? videos : []; + if (!list.length && !error) body.push(p({ class: "muted" }, i18n.peertubeNoVideos)); + else body.push(section({ class: "peertube-grid" }, list.map(renderPeertubeVideo))); + return template(i18n.fediverse, peertubeHeader(account, stats, [ + a({ href: "/fediverse", class: "filter-btn" }, `← ${i18n.fediverse}`), + a({ href: `/fediverse/peertube?filter=${current}&refresh=1`, class: "filter-btn" }, i18n.fediverseRefresh), + a({ href: "/fediverse/peertube/upload", class: "filter-btn" }, i18n.peertubeUpload) + ]), section(...body)); +}; + +const renderPeertubeComment = (c) => { + const acc = c.account || {}; + return div({ class: "feed-card fediverse-card" }, + div({ class: "fediverse-head" }, + acc.avatar ? img({ class: "fediverse-avatar", src: acc.avatar, alt: acc.displayName }) : "", + div({ class: "fediverse-author" }, + span({ class: "fediverse-name" }, acc.displayName || acc.name), + span({ class: "fediverse-acct" }, acc.name ? `@${acc.name}${acc.host ? "@" + acc.host : ""}` : "") + ), + span({ class: "fediverse-date" }, fmtDate(c.createdAt)) + ), + div({ class: "feed-text", [RAW_HTML]: sanitizeHtml(c.html) }) + ); +}; + +exports.peertubeVideoView = ({ account, stats, video, comments, myRating, error } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const header = peertubeHeader(account, stats, [a({ href: "/fediverse/peertube", class: "filter-btn" }, `← ${i18n.peertubeVideos}`)]); + if (!video) return template(i18n.fediverse, header, section(div({ class: "fediverse-error" }, i18n[error] || i18n.peertubeErrFetch))); + const id = encodeURIComponent(video.id); + const ch = video.channel || {}; + const liked = myRating === "like"; + const body = []; + if (error) body.push(div({ class: "fediverse-error" }, i18n[error] || i18n.peertubeErrConnect)); + body.push(div({ class: "feed-card fediverse-card peertube-watch" }, + video.stream + ? videoHyperaxe({ class: "peertube-player", src: video.stream, controls: true, preload: "metadata", poster: video.thumbnail || undefined }) + : div({ class: "peertube-nostream" }, video.thumbnail ? img({ src: video.thumbnail, alt: video.name, class: "post-image" }) : "", p(i18n.peertubeNoStream)), + h3({ class: "peertube-title" }, video.name), + div({ class: "peertube-meta" }, + ch.avatar ? img({ class: "peertube-channel-avatar", src: ch.avatar, alt: "" }) : "", + ch.url ? a({ href: ch.url, target: "_blank", rel: "noopener noreferrer", class: "fediverse-acct" }, ch.displayName || ch.handle) : span({ class: "fediverse-acct" }, ch.displayName || ""), + span({ class: "fediverse-date" }, fmtDate(video.publishedAt)), + span(`${video.views} ${i18n.peertubeViews}`), + video.duration ? span(fmtDuration(video.duration)) : "" + ), + div({ class: "fediverse-actions" }, + form({ method: "POST", action: `/fediverse/peertube/video/${id}/rate` }, + input({ type: "hidden", name: "rating", value: liked ? "none" : "like" }), + button({ type: "submit", class: liked ? "filter-btn active" : "filter-btn" }, `${liked ? "★" : "☆"} ${video.likes}`) + ), + a({ href: video.url, target: "_blank", rel: "noopener noreferrer", class: "filter-btn" }, i18n.peertubeOpen) + ), + video.description ? div({ class: "feed-text peertube-description" }, video.description) : "", + Array.isArray(video.tags) && video.tags.length ? div({ class: "card-chips-row" }, video.tags.map(t => span({ class: "tag-link" }, `#${t}`))) : "" + )); + body.push(h4(`${i18n.peertubeComments} · ${Array.isArray(comments) ? comments.length : 0}`)); + const list = Array.isArray(comments) ? comments : []; + if (!list.length) body.push(p({ class: "muted" }, i18n.peertubeNoComments)); + else body.push(section({ class: "feed-container" }, list.map(renderPeertubeComment))); + body.push(div({ class: "publish-form" }, + form({ method: "POST", action: `/fediverse/peertube/video/${id}/comment` }, + textarea({ maxlength: "4000", name: "text", rows: "4", class: "publish-textarea", placeholder: i18n.peertubeCommentPlaceholder, required: true }), + br(), + button({ type: "submit", class: "filter-btn" }, i18n.peertubeComment) + ) + )); + return template(i18n.fediverse, header, section(...body)); +}; + +exports.peertubeUploadView = ({ account, stats, error } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const header = peertubeHeader(account, stats, [a({ href: "/fediverse/peertube", class: "filter-btn" }, `← ${i18n.peertubeVideos}`)]); + return template(i18n.fediverse, header, section( + error ? div({ class: "fediverse-error" }, i18n[error] || i18n.peertubeErrUpload) : "", + div({ class: "publish-form" }, + form({ method: "POST", action: "/fediverse/peertube/upload", enctype: "multipart/form-data" }, + p(i18n.peertubeUploadHelp), + label({ for: "peertube_video" }, i18n.fediverseAttach), br(), + input({ type: "file", id: "peertube_video", name: "video", accept: "video/*", required: true }), br(), br(), + label({ for: "peertube_name" }, i18n.peertubeTitleLabel), br(), + input({ type: "text", id: "peertube_name", name: "name", maxlength: "120", required: true }), br(), + label({ for: "peertube_description" }, i18n.peertubeDescriptionLabel), br(), + textarea({ id: "peertube_description", name: "description", rows: "5", maxlength: "10000", class: "publish-textarea" }), br(), + label({ for: "peertube_privacy" }, i18n.peertubePrivacyLabel), br(), + select({ id: "peertube_privacy", name: "privacy" }, + option({ value: "public" }, i18n.peertubePrivacyPublic), + option({ value: "unlisted" }, i18n.peertubePrivacyUnlisted), + option({ value: "private" }, i18n.peertubePrivacyPrivate) + ), br(), br(), + button({ type: "submit", class: "filter-btn" }, i18n.peertubeUpload) + ) + ) + )); +}; + +exports.fediverseOverviewView = ({ account, stats, telegram, telegramStats, peertube, peertubeStats } = {}) => { + const boxes = []; + if (account) { + boxes.push(mastodonBox(account, stats, [ + form({ method: "GET", action: "/fediverse/mastodon" }, button({ type: "submit", class: "filter-btn" }, i18n.fediverseManage)) + ], true)); + } + if (telegram) { + boxes.push(telegramBox(telegram, telegramStats, [ + form({ method: "GET", action: "/fediverse/telegram" }, button({ type: "submit", class: "filter-btn" }, i18n.fediverseManage)) + ], true)); + } + if (peertube) { + boxes.push(peertubeBox(peertube, peertubeStats, [ + form({ method: "GET", action: "/fediverse/peertube" }, button({ type: "submit", class: "filter-btn" }, i18n.fediverseManage)) + ], true)); + } return template(i18n.fediverse, section( - div({ class: "tags-header" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), - account ? box : disconnectedBlock() + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + boxes.length ? boxes : disconnectedBlock() )); }; @@ -136,7 +444,7 @@ exports.fediverseView = ({ account, posts, error, stats } = {}) => { if (!account) return exports.fediverseOverviewView({ account: null }); const body = []; - body.push(div({ class: "tags-header" }, h2(i18n.fediverse), p(i18n.fediverseDescription))); + body.push(div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription))); body.push(mastodonBox(account, stats, [ a({ href: "/fediverse", class: "filter-btn" }, `← ${i18n.fediverse}`), a({ href: "/fediverse/mastodon?refresh=1", class: "filter-btn" }, i18n.fediverseRefresh), @@ -156,7 +464,7 @@ exports.fediverseView = ({ account, posts, error, stats } = {}) => { exports.fediverseThreadView = ({ account, stats, thread, error } = {}) => { const header = section( - div({ class: "tags-header" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), account ? mastodonBox(account, stats, [ a({ href: "/fediverse/mastodon", class: "filter-btn" }, `← ${i18n.fediverse}`) ], true) : "" @@ -198,7 +506,7 @@ exports.fediversePreviewView = ({ account, stats, text, media, error, replyToId, return template(i18n.fediverse, section( - div({ class: "tags-header" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), account ? mastodonBox(account, stats, [ a({ href: backHref, class: "filter-btn" }, `← ${i18n.fediverse}`) ], true) : "" @@ -209,7 +517,7 @@ exports.fediversePreviewView = ({ account, stats, text, media, error, replyToId, div({ class: "publish-form" }, form( { action: postAction, method: "POST", enctype: "multipart/form-data" }, - textarea({ name: "text", id: "text", rows: "6", class: "publish-textarea", placeholder: isReply ? i18n.fediverseReplyPlaceholder : i18n.fediverseComposePlaceholder }, text || ""), + textarea({ maxlength: "5000", name: "text", id: "text", rows: "6", class: "publish-textarea", placeholder: isReply ? i18n.fediverseReplyPlaceholder : i18n.fediverseComposePlaceholder }, text || ""), ...mediaList.map(m => input({ type: "hidden", name: "tmp", value: m.name })), br(), label({ for: "fediverse_media" }, i18n.fediverseAttach), diff --git a/src/views/feed_view.js b/src/views/feed_view.js index 8e1cf97..dc561ce 100644 --- a/src/views/feed_view.js +++ b/src/views/feed_view.js @@ -1,10 +1,11 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, h1, label } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderContentActions } = require("./main_views"); +const { div, h2, p, section, button, form, a, span, textarea, br, input, h1, label, img } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, userLink, renderContentActions, renderEngagement, renderModuleStats, moduleIsEmpty, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const { renderTextWithStyles } = require("../backend/renderTextWithStyles"); +const { renderStyledHtml, renderStyledText } = require("../backend/renderStyledText"); const moment = require("../server/node_modules/moment"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); -const { renderUrl } = require("../backend/renderUrl"); const FEED_TEXT_MIN = Number(config?.feed?.minLength ?? 1); const FEED_TEXT_MAX = Number(config?.feed?.maxLength ?? 280); @@ -22,7 +23,7 @@ const normalizeOptions = (opts) => { const formatDate = (feed) => { const ts = feed?.value?.timestamp || Date.parse(feed?.value?.content?.createdAt || "") || 0; - return ts ? new Date(ts).toLocaleString() : ""; + return ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; }; const extractTags = (text) => { @@ -30,19 +31,6 @@ const extractTags = (text) => { return Array.from(new Set(list)); }; -const rewriteHashtagLinks = (html) => { - return String(html || '').replace( - /href=(["'])\/hashtag\/([^"'?#\s<]+)\1/gi, - (m, q, rawTag) => { - let t = String(rawTag || ''); - try { t = decodeURIComponent(t); } catch {} - t = t.replace(/[^A-Za-z0-9_]/g, ''); - const tag = t.toLowerCase(); - const query = encodeURIComponent(`#${tag}`); - return `href=${q}/search?query=${query}${q}`; - } - ); -}; const generateFilterButtons = (filters, currentFilter, action, extra = {}) => { const cur = String(currentFilter || "").toUpperCase(); @@ -56,21 +44,11 @@ const generateFilterButtons = (filters, currentFilter, action, extra = {}) => { { method: "GET", action }, input({ type: "hidden", name: "filter", value: mode }), ...hiddenInputs(extra), - button({ type: "submit", class: cur === mode ? "filter-btn active" : "filter-btn" }, i18n[mode + "Button"] || mode) + button({ type: "submit", class: cur === mode ? "filter-btn active" : "filter-btn" }, String(i18n[mode + "Button"] || mode).toUpperCase()) ) ); }; -const renderVotesSummary = (opinions = {}) => { - const entries = Object.entries(opinions).filter(([, v]) => Number(v) > 0); - if (!entries.length) return null; - entries.sort((a, b) => Number(b[1]) - Number(a[1]) || String(a[0]).localeCompare(String(b[0]))); - return div( - { class: "votes" }, - entries.map(([category, count]) => span({ class: "vote-category" }, `${category}: ${count}`)) - ); -}; - const renderCardField = (labelText, value) => div( { class: "card-field" }, @@ -79,68 +57,59 @@ const renderCardField = (labelText, value) => ); const renderFeedCommentsSection = (feedKey, comments = []) => { - const list = Array.isArray(comments) ? comments : []; - const commentsCount = list.length; + return renderSharedCommentsSection({ + action: `/feed/${encodeURIComponent(feedKey)}/comments`, + comments: comments, + returnTo: null, + open: true + }); +}; - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel || i18n.feedPostComment || "Post a comment"), - form( - { method: "POST", action: `/feed/${encodeURIComponent(feedKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder || "" - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia || "Upload media"), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton || i18n.feedPostComment || "Send") + +const FEED_MEDIA_RE = /^\n?!?\[[^\]\n]{0,200}\]\(&[A-Za-z0-9+/=]{44}\.sha256\)$/; +const renderMastodonToggle = (on = false) => + div({ class: "clearnet-choice" }, + div({ class: "clearnet-choice-options" }, + label({ class: "clearnet-choice-option" }, input({ type: "checkbox", name: "mastodon", value: "1", ...(on ? { checked: true } : {}) }), + span({ class: "pm-exposition-chip pm-exposition-whole" }, span({ class: "pm-exposition-icon" }, "⌬"), span({ class: "pm-exposition-text" }, "Mastodon")) ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; + ) + ); - const content = c?.value?.content || {}; - const text = content.text || c?.value?.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot - ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) - : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) +const renderFeedComposer = ({ text = "", media = "", rows = 5, clearnet = false, mastodon = false, mastodonOn = false } = {}) => { + const cleanMedia = FEED_MEDIA_RE.test(String(media || "")) ? String(media).trim() : ""; + const draft = String(text || ""); + const hasPreview = !!(draft.trim() || cleanMedia); + const previewNodes = hasPreview ? renderStyledText(cleanMedia ? `${draft.trim()}\n${cleanMedia}` : draft.trim(), { zoomImages: true }) : []; + return form( + { method: "POST", action: "/feed/create", enctype: "multipart/form-data" }, + textarea({ + name: "text", + required: true, + minlength: String(FEED_TEXT_MIN), + maxlength: String(FEED_TEXT_MAX), + rows: String(rows), + cols: 50, + placeholder: i18n.feedPlaceholder + }, draft), + cleanMedia ? input({ type: "hidden", name: "media", value: cleanMedia }) : null, + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + hasPreview + ? div({ class: "feed-preview" }, + h2({ class: "feed-preview-title" }, i18n.messagePreview || "Preview"), + div({ class: "trending-card feed-card" }, div({ class: "feed-text" }, ...previewNodes)) ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet || i18n.noComments || "") + : null, + renderClearnetSelector(clearnet, i18n), + mastodon ? renderMastodonToggle(mastodonOn) : null, + div({ class: "feed-compose-actions" }, + button({ type: "submit", class: "filter-btn", formaction: "/feed/preview" }, i18n.preview || "Preview"), + button({ type: "submit", class: "create-button" }, i18n.createFeedButton || "Send Feed!") + ) ); }; -const renderFeedCard = (feed) => { +const renderFeedCard = exports.renderFeedCard = (feed, spreadMap = null, opts = {}) => { const content = feed.value.content || {}; const rawText = typeof content.text === "string" ? content.text : ""; const safeText = rawText.trim(); @@ -153,87 +122,146 @@ const renderFeedCard = (feed) => { const alreadyRefeeded = Array.isArray(content.refeeds_inhabitants) && me ? content.refeeds_inhabitants.includes(me) : false; - const authorId = feed.value.author || content.author || ""; + const authorId = content.author || feed.value.author || ""; + const signerId = feed.value.author || ""; const refeedsNum = Number(content.refeeds || 0) || 0; const commentCount = Number(content.commentCount || 0); - const styledHtml = rewriteHashtagLinks(renderTextWithStyles(safeText)); + const styledNodes = renderStyledText(safeText, { zoomImages: true, ...(typeof opts.hashtagHref === 'function' ? { hashtagHref: opts.hashtagHref } : {}) }); + const refeedAction = opts.refeedAction || `/feed/refeed/${encodeURIComponent(feed.key)}`; + const canRefeed = opts.canRefeed !== false && !(authorId && String(authorId) === String(me)); + const headerActions = opts.headerActions !== undefined + ? opts.headerActions + : feed.tribeOrigin + ? renderContentActions(null, feed.tribeOrigin.href) + : renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`, { + author: authorId, + spread: (spreadMap && spreadMap.get(feed.key)) || null, + reportTitle: safeText, + ...(((signerId && String(signerId) === String(me)) || (authorId && String(authorId) === String(me))) ? { deleteAction: `/feed/delete/${encodeURIComponent(feed.key)}` } : {}) + }); + const chipsRow = opts.chips !== undefined + ? opts.chips + : feed.tribeOrigin ? div({ class: "card-chips-row" }, renderTribeOriginChip(feed.tribeOrigin)) : feed.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("feed", "", feed.key))) : null; + const engagement = opts.engagement !== undefined + ? opts.engagement + : feed.tribeOrigin ? null : renderEngagement(feed.key, + renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants), + renderCommentsLink({ href: `/feed/${encodeURIComponent(feed.key)}`, count: commentCount }) + ); + + if (process.env.OASIS_MOBILE === '1') { + return div( + { class: "trending-card feed-card" + (authorId && String(authorId) === String(me) ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, span(), headerActions), + div( + { class: "card-section feed-card-body" }, + div({ class: "feed-text" }, ...styledNodes), + chipsRow, + feed.tribeOrigin ? null : div( + { class: "refeed-column" }, + h1(String(refeedsNum)), + canRefeed + ? form( + { method: "POST", action: refeedAction }, + button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", title: i18n.refeedButton, "aria-label": i18n.refeedButton, ...(alreadyRefeeded ? { disabled: true } : {}) }, "\u25b2") + ) + : null, + alreadyRefeeded ? p({ class: "muted" }, i18n.alreadyRefeeded) : null + ), + engagement, + p( + { class: "card-footer" }, + span({ class: "date-link" }, `${createdAt}`), + userLink(authorId), + content._textEdited ? span({ class: "edited-badge" }, ` \u00b7 ${i18n.edited || "edited"}`) : null + ) + ) + ); + } return div( { class: "trending-card feed-card" + (authorId && String(authorId) === String(me) ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`) + headerActions ), div( { class: "card-section feed-card-body" }, div( { class: "feed-row" }, - div( + feed.tribeOrigin ? null : div( { class: "refeed-column" }, h1(String(refeedsNum)), - form( - { method: "POST", action: `/feed/refeed/${encodeURIComponent(feed.key)}` }, - button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", ...(alreadyRefeeded ? { disabled: true } : {}) }, i18n.refeedButton) - ), - alreadyRefeeded ? p({ class: "muted" }, i18n.alreadyRefeeded) : null + canRefeed + ? form( + { method: "POST", action: refeedAction }, + button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", title: i18n.refeedButton, "aria-label": i18n.refeedButton, ...(alreadyRefeeded ? { disabled: true } : {}) }, "▲") + ) + : null, ), div( { class: "feed-main" }, - div({ class: "feed-text", innerHTML: sanitizeHtml(styledHtml) }), - h2( - `${i18n.totalOpinions}: ${totalCount}`, - ...(() => { - const entries = voteEntries.filter(([, v]) => Number(v) > 0); - if (!entries.length) return []; - const maxVal = Math.max(...entries.map(([, v]) => Number(v))); - const dominant = entries.filter(([, v]) => Number(v) === maxVal).map(([k]) => i18n['vote' + k.charAt(0).toUpperCase() + k.slice(1)] || k); - return [ - span({ style: 'margin:0 8px;opacity:0.5;' }, '|'), - span({ style: 'font-weight:700;' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`) - ]; - })() - ), + div({ class: "feed-text" }, ...styledNodes), + chipsRow, p( { class: "card-footer" }, - span({ class: "date-link" }, `${createdAt} ${i18n.performed} `), + span({ class: "date-link" }, `${createdAt}`), userLink(authorId), content._textEdited ? span({ class: "edited-badge" }, ` · ${i18n.edited || "edited"}`) : null ) ) ), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, `${i18n.voteCommentsLabel || "Comments"}:`), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/feed/${encodeURIComponent(feed.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton || i18n.feedOpenDiscussion || "Open Discussion") - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(totalCount)) - ), - renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants) - ) + engagement ) ); }; +const renderFeedSideTags = (trendingTags) => + div({ class: "feed-side feed-side-right" }, + div({ class: "feed-side-box" }, + h2({ class: "feed-side-title" }, i18n.feedTrendingTitle || "Trending Tags"), + trendingTags.length + ? div({ class: "feed-side-tags" }, + trendingTags.map(t => a({ href: `/feed?tag=${encodeURIComponent(t.name || t)}`, class: "tag-link" }, `#${t.name || t}`))) + : p({ class: "muted" }, "—") + ) + ); + +const renderFeedSideUsers = (activeUsers) => + div({ class: "feed-side feed-side-left" }, + div({ class: "feed-side-box" }, + h2({ class: "feed-side-title" }, i18n.feedActiveTitle || "Active Inhabitants"), + activeUsers.length + ? div({ class: "feed-side-users" }, + activeUsers.map(u => div({ class: "feed-side-user" }, + img({ src: u.avatarUrl || "/assets/images/default-avatar.png", class: "feed-side-avatar" }), + userLink(u.id) + ))) + : p({ class: "muted" }, "—") + ) + ); + +const feedChipFor = (filter, censusF) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusF)) return true; + if (mode === "MINE") return censusF.some((f) => String(f && f.value && f.value.author) === String(config.keys.id)); + if (mode === "RECENT") return censusF.length > 0; + return true; +}; + exports.feedView = (feeds, opts = "ALL") => { const { filter, q, tag, msg } = normalizeOptions(opts); + const workspace = !!(opts && typeof opts === "object" && opts.workspace) && require("../configs/config-manager.js").getConfig().ux?.current === "feed"; + const spreadMap = (opts && typeof opts === "object" && opts.spreadMap instanceof Map) ? opts.spreadMap : null; + const trendingTags = (opts && typeof opts === "object" && Array.isArray(opts.trendingTags)) ? opts.trendingTags : []; + const activeUsers = (opts && typeof opts === "object" && Array.isArray(opts.activeUsers)) ? opts.activeUsers : []; const title = filter === "MINE" ? i18n.MINEButton - : filter === "TODAY" - ? i18n.TODAYButton + : filter === "RECENT" + ? i18n.RECENTButton : filter === "TOP" ? i18n.TOPButton : filter === "CREATE" @@ -244,55 +272,67 @@ exports.feedView = (feeds, opts = "ALL") => { ? `${i18n.searchTitle || "Search"}: “${q}”` : i18n.feedTitle; - const header = div({ class: "tags-header" }, h2(title), p(i18n.FeedshareYourOpinions)); + const header = div({ class: "tags-header module-header-line" }, h2(title), p(i18n.FeedshareYourOpinions)); const successBanner = msg === 'feedPublished' ? div({ class: 'feed-success-msg' }, p('✓ ' + (i18n.feedPublishedSuccess || 'Feed published successfully!'))) : null; const extra = { q, tag }; + const emptyMod = moduleIsEmpty(feeds, filter, "ALL", q || tag); + const censusF = Array.isArray(opts.censusList) ? opts.censusList : feeds; + const feedChip = feedChipFor(filter, censusF); - return template( - title, - section( - header, - successBanner, - div( - { class: "mode-buttons-row" }, - ...generateFilterButtons(["ALL", "MINE", "TODAY", "TOP"], filter, "/feed", extra), - form({ method: "GET", action: "/feed/create" }, button({ type: "submit", class: "create-button filter-btn" }, i18n.createFeedTitle || "Create Feed")) - ), - div( - { class: "feed-tools-row" }, - form( - { method: "GET", action: "/feed", class: "feed-search-form" }, - input({ type: "hidden", name: "filter", value: filter }), - tag ? input({ type: "hidden", name: "tag", value: tag }) : null, - input({ type: "text", name: "q", value: q, placeholder: i18n.searchPlaceholder || "Search", class: "feed-search-input" }), - button({ type: "submit", class: "filter-btn feed-search-btn" }, i18n.searchButton || "Search") + const centerContent = section( + header, + successBanner, + div( + { class: "mode-buttons-row" }, + ...(emptyMod ? [] : generateFilterButtons(["ALL", "RECENT", "MINE", "TOP"].filter(feedChip), filter, "/feed", extra)), + form({ method: "GET", action: "/feed/create" }, button({ type: "submit", class: "create-button filter-btn" }, i18n.createFeedTitle || "Create Feed")) + ), + emptyMod ? null : div( + { class: "feed-tools-row activity-filter-chips activity-toolbar-row" }, + renderModuleStats(feeds.length), + form( + { method: "GET", action: "/feed", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + tag ? input({ type: "hidden", name: "tag", value: tag }) : null, + input({ type: "text", name: "q", value: q, placeholder: i18n.feedSearchPlaceholder || i18n.searchPlaceholder || "Search", class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton || "Search") ) - ), - section( - filter === "CREATE" - ? form( - { method: "POST", action: "/feed/create" }, - textarea({ - name: "text", - placeholder: i18n.feedPlaceholder, - required: true, - minlength: String(FEED_TEXT_MIN), - maxlength: String(FEED_TEXT_MAX), - rows: 4, - cols: 50 - }), - br(), - button({ type: "submit", class: "create-button" }, i18n.createFeedButton) - ) - : feeds && feeds.length > 0 - ? div({ class: "feed-container" }, feeds.map((feed) => renderFeedCard(feed)).filter(Boolean)) - : div({ class: "no-results" }, p(i18n.noFeedsFound)) ) + ), + (process.env.OASIS_MOBILE === '1' && workspace) ? div({ class: "feed-sides-row" }, renderFeedSideUsers(activeUsers), renderFeedSideTags(trendingTags)) : null, + section( + filter === "CREATE" + ? renderFeedComposer({ rows: 4, mastodon: opts.mastodon === true }) + : feeds && feeds.length > 0 + ? div({ class: "feed-container" }, paged(feeds.filter((feed) => String((feed.value.content || {}).text || "").trim())).map((feed) => renderFeedCard(feed, spreadMap)).filter(Boolean)) + : div({ class: "no-results" }, p(i18n.noFeedsFound)) ) ); + + if (workspace) { + if (process.env.OASIS_MOBILE === '1') { + return template( + title, + div({ class: "feed-workspace" }, + div({ class: "feed-workspace-center" }, centerContent) + ) + ); + } + return template( + title, + div({ class: "feed-workspace" }, + renderFeedSideUsers(activeUsers), + div({ class: "feed-workspace-center" }, centerContent), + renderFeedSideTags(trendingTags) + ) + ); + } + + return template(title, centerContent); }; exports.feedCreateView = (opts = {}) => { @@ -301,94 +341,91 @@ exports.feedCreateView = (opts = {}) => { return template( i18n.createFeedTitle, section( - div({ class: "tags-header" }, h2(i18n.createFeedTitle), p(i18n.FeedshareYourOpinions)), - div({ class: "mode-buttons-row" }, ...generateFilterButtons(["ALL", "MINE", "TODAY", "TOP"], "CREATE", "/feed", { q, tag })), - form( - { method: "POST", action: "/feed/create" }, - textarea({ - name: "text", - required: true, - minlength: String(FEED_TEXT_MIN), - maxlength: String(FEED_TEXT_MAX), - rows: 5, - cols: 50, - placeholder: i18n.feedPlaceholder - }), - br(), - button({ type: "submit", class: "create-button" }, i18n.createFeedButton || "Send Feed!") - ) + div({ class: "tags-header module-header-line" }, h2(i18n.createFeedTitle), p(i18n.FeedshareYourOpinions)), + div({ class: "mode-buttons-row" }, ...generateFilterButtons(["ALL"], "CREATE", "/feed", { q, tag })), + renderFeedComposer({ text: opts.text, media: opts.media, clearnet: opts.clearnet === true, mastodon: opts.mastodon === true, mastodonOn: opts.mastodonOn === true }) ) ); }; -exports.singleFeedView = (feed, comments = []) => { +exports.singleFeedView = (feed, comments = [], params = {}) => { + const feedChip = feedChipFor("ALL", Array.isArray(params.censusList) ? params.censusList : null); const content = feed.value?.content || {}; const rawText = typeof content.text === "string" ? content.text : ""; const safeText = rawText.trim(); - const authorId = feed.value?.author || content.author || ""; + const authorId = content.author || feed.value?.author || ""; + const signerId = feed.value?.author || ""; const createdAt = formatDate(feed); - const styledHtml = rewriteHashtagLinks(renderTextWithStyles(safeText)); + const styledNodes = renderStyledText(safeText, { zoomImages: true }); const me = config?.keys?.id; const alreadyRefeeded = Array.isArray(content.refeeds_inhabitants) && me ? content.refeeds_inhabitants.includes(me) : false; const refeedsNum = Number(content.refeeds || 0) || 0; const tags = extractTags(safeText); + const isClearnet = !!feed.clearnet; return template( i18n.feedDetailTitle || "Feed", + section(div({ class: "tags-header module-header-line" }, h2(i18n.feedTitle), p(i18n.FeedshareYourOpinions))), section( div( { class: "filters" }, form( { method: "GET", action: "/feed", class: "ui-toolbar ui-toolbar--filters" }, button({ type: "submit", name: "filter", value: "ALL", class: "filter-btn" }, i18n.ALLButton || "ALL"), - button({ type: "submit", name: "filter", value: "MINE", class: "filter-btn" }, i18n.MINEButton || "MINE"), - button({ type: "submit", name: "filter", value: "TODAY", class: "filter-btn" }, i18n.TODAYButton || "TODAY"), + ...(feedChip("RECENT") ? [button({ type: "submit", name: "filter", value: "RECENT", class: "filter-btn" }, i18n.RECENTButton || "RECENT")] : []), + ...(feedChip("MINE") ? [button({ type: "submit", name: "filter", value: "MINE", class: "filter-btn" }, i18n.MINEButton || "MINE")] : []), button({ type: "submit", name: "filter", value: "TOP", class: "filter-btn" }, i18n.TOPButton || "TOP"), form({ method: "GET", action: "/feed/create" }, button({ type: "submit", class: "create-button" }, i18n.createFeedTitle || "Create Feed")) ) ), div( { class: "bookmark-item card feed-detail-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`, { + spread: params.spreads || null, + author: authorId, + reportTitle: safeText, + ...(((signerId && String(signerId) === String(me)) || (authorId && String(authorId) === String(me))) ? { deleteAction: `/feed/delete/${encodeURIComponent(feed.key)}` } : {}) + }) + ), br, div( { class: "feed-row" }, div( { class: "refeed-column" }, h1(String(refeedsNum)), - form( - { method: "POST", action: `/feed/refeed/${encodeURIComponent(feed.key)}` }, - button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", ...(alreadyRefeeded ? { disabled: true } : {}) }, i18n.refeedButton) - ), - alreadyRefeeded ? p({ class: "muted" }, i18n.alreadyRefeeded) : null + (authorId && String(authorId) === String(me)) + ? null + : form( + { method: "POST", action: `/feed/refeed/${encodeURIComponent(feed.key)}` }, + button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", title: i18n.refeedButton, "aria-label": i18n.refeedButton, ...(alreadyRefeeded ? { disabled: true } : {}) }, "▲") + ), ), div( { class: "feed-main" }, - div({ class: "feed-text", innerHTML: sanitizeHtml(styledHtml) }), + div({ class: "feed-text" }, ...styledNodes), tags.length ? div( { class: "card-tags" }, tags.map((tag) => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) ) : null, + div({ class: "card-chips-row" }, renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("feed", "", feed.key) : null)), + authorId && String(authorId) === String(me) ? renderClearnetSwitch("feed", feed.rootId || feed.key, isClearnet) : null, br, p( { class: "card-footer" }, - span({ class: "date-link" }, `${createdAt} ${i18n.performed} `), + span({ class: "date-link" }, `${createdAt}`), userLink(authorId), content._textEdited ? span({ class: "edited-badge" }, ` · ${i18n.edited || "edited"}`) : null ) ) ) ), - renderFeedCommentsSection(feed.key, comments), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(content.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants) + renderEngagement(feed.key, + renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants), + renderFeedCommentsSection(feed.key, comments) ) ) ); diff --git a/src/views/files_view.js b/src/views/files_view.js new file mode 100644 index 0000000..6fc9773 --- /dev/null +++ b/src/views/files_view.js @@ -0,0 +1,421 @@ +const { form, button, div, h2, h3, p, section, input, br, a, span, textarea, select, label, option, table, tr, th, td, progress, strong } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require('./clearnet_view'); + +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip , renderSpreadEditWarning, renderContentActions, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); + +const userId = config.keys.id; + +const safeArr = (v) => (Array.isArray(v) ? v : []); +const safeText = (v) => String(v || "").trim(); + +const buildReturnTo = (filter, params = {}) => { + const f = safeText(filter || "all"); + const q = safeText(params.q || ""); + const sort = safeText(params.sort || "recent"); + const parts = [`filter=${encodeURIComponent(f)}`]; + if (q) parts.push(`q=${encodeURIComponent(q)}`); + if (sort) parts.push(`sort=${encodeURIComponent(sort)}`); + return `/files?${parts.join("&")}`; +}; + +const renderTags = (tags) => { + const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); + return list.length + ? div( + { class: "card-tags" }, + list.map((tag) => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) + ) + : null; +}; + +const renderFileOwnerActions = (filter, fileObj, params = {}) => { + const returnTo = buildReturnTo(filter, params); + const isAuthor = String(fileObj.author) === String(userId); + const hasOpinions = Object.keys(fileObj.opinions || {}).length > 0; + + if (!isAuthor || hasOpinions) return []; + + return [ + form( + { method: "GET", action: `/files/edit/${encodeURIComponent(fileObj.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "update-btn", type: "submit" }, i18n.fileUpdateButton) + ) + ]; +}; + +const renderFileCommentsSection = (fileId, comments = [], returnTo = null) => { + return renderSharedCommentsSection({ + action: `/files/${encodeURIComponent(fileId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; + +const blobSha256Hex = (blobId) => { + const m = /^&([A-Za-z0-9+/=]+)\.sha256$/.exec(String(blobId || "")); + if (!m) return ""; + try { return Buffer.from(m[1], "base64").toString("hex"); } catch (_) { return ""; } +}; + +const formatSize = (bytes) => { + const n = Number(bytes) || 0; + if (n === 0) return "—"; + if (n < 1024) return n + " B"; + if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB"; + return (n / (1024 * 1024)).toFixed(1) + " MB"; +}; + +const spreadInfoOf = (t, spreadMap) => (spreadMap instanceof Map && spreadMap.get(t.key)) || null; +const renderFileSeeds = (t, spreadMap) => { + const info = spreadInfoOf(t, spreadMap); + return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0)); +}; +const renderFileSpread = (t, spreadMap) => String(t.author) === String(userId) ? "—" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false); + +const renderFileTable = exports.renderFileTable = (files, filter, params = {}) => { + const returnTo = buildReturnTo(filter, params); + + if (!files.length) return p(params.q ? i18n.fileNoMatch : i18n.noFiles); + + return table( + { border: "1", class: "file-table" }, + tr( + th(i18n.createdAt || "DATE"), + th(i18n.fileTitleLabel || "TITLE"), + th(i18n.fileSizeLabel || "SIZE"), + th({ class: "torrent-spread-cell" }, i18n.seedsLabel), + th({ class: "torrent-spread-cell" }, i18n.seedAction), + th("") + ), + files.map((t) => + tr( + td({ class: "row-chips" }, t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('files', t.title, t.key)) : null), + td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")), + td({ class: "row-title", "data-label": i18n.fileTitleLabel || "TITLE" }, t.title || ""), + td({ "data-label": i18n.fileSizeLabel || "SIZE" }, formatSize(t.size)), + td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, renderFileSeeds(t, params.spreadMap)), + td({ class: "torrent-spread-cell", "data-label": i18n.seedAction }, renderFileSpread(t, params.spreadMap)), + td( + form( + { method: "GET", action: `/files/${encodeURIComponent(t.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "filter", value: filter || "all" }), + params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, + params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, + button({ type: "submit", class: "filter-btn" }, i18n.fileDetailsButton) + ) + ) + ) + ) + ); +}; + +const isExternalUrl = (u) => /^https?:\/\//i.test(String(u || "")); + +const renderNextcloudForm = (returnTo) => div( + { class: "div-center audio-form" }, + h2("Nextcloud"), + form( + { action: "/files/nextcloud", method: "POST", enctype: "multipart/form-data" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + span(i18n.nextcloudFileLabel), + br(), + input({ type: "file", name: "file", required: true }), + br(), + br(), + span(i18n.fileTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.fileTitlePlaceholder, required: true }), + br(), + span(i18n.fileDescriptionLabel), + br(), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.fileDescriptionPlaceholder, rows: "4" }), + br(), + span(i18n.fileTagsLabel), + br(), + input({ type: "text", name: "tags", placeholder: i18n.fileTagsPlaceholder }), + br(), + br(), + renderClearnetSelector(false, i18n), br(), + button({ type: "submit" }, i18n.nextcloudUploadButton) + ) +); + +const renderFileForm = (filter, fileId, fileToEdit, params = {}) => { + const returnTo = safeText(params.returnTo) || buildReturnTo("all", params); + const tribeId = safeText(params.tribeId || ""); + const clearnetAllowed = !tribeId && !fileToEdit?.tribeId && !fileToEdit?.encrypted && !fileToEdit?.cipher; + return div( + { class: "div-center audio-form" }, + params.spreadWarning || null, + form( + { + action: filter === "edit" ? `/files/update/${encodeURIComponent(fileId)}` : "/files/create", + method: "POST", + enctype: "multipart/form-data" + }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + span(i18n.fileFileLabel), + br(), + input({ type: "file", name: "file", required: filter !== "edit" }), + br(), + br(), + span(i18n.fileTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.fileTitlePlaceholder, value: fileToEdit?.title || "", required: true }), + br(), + span(i18n.fileDescriptionLabel), + br(), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.fileDescriptionPlaceholder, rows: "4" }, fileToEdit?.description || ""), + br(), + span(i18n.fileTagsLabel), + br(), + input({ + type: "text", + name: "tags", + placeholder: i18n.fileTagsPlaceholder, + value: safeArr(fileToEdit?.tags).join(", ") + }), + br(), + br(), + ...(clearnetAllowed ? [renderClearnetSelector(filter === "edit" ? !!fileToEdit?.clearnet : false, i18n), br()] : []), + button({ type: "submit" }, filter === "edit" ? i18n.fileUpdateButton : i18n.fileCreateButton) + ) + ); +}; + +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.length > 0; + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + +exports.filesView = async (files, filter = "all", fileId = null, params = {}) => { + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(fileId) }; + const title = i18n.filesTitle; + + const q = safeText(params.q || ""); + const sort = safeText(params.sort || "recent"); + + const list = safeArr(files); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); + const fileToEdit = fileId ? list.find((t) => t.key === fileId) : null; + + return template( + title, + section( + div({ class: "tags-header module-header-line" }, + h2(title), + p(i18n.filesDescription) + ), + div( + { class: "filters" }, + form( + { method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "sort", value: sort }), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( + { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, + String(i18n.fileFilterFavorites).toUpperCase() + )] : []), + + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterTop).toUpperCase())] : []), + ]), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.fileCreateButton) + ) + ) + ), + section( + filter === "create" || filter === "edit" + ? [renderFileForm(filter, fileId, fileToEdit, { ...params, filter }), filter !== "edit" && params.nextcloud && !params.tribeId ? renderNextcloudForm(safeText(params.returnTo) || buildReturnTo("all", params)) : null] + : section( + emptyMod ? null : div( + { class: "audios-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), + form( + { method: "GET", action: "/files", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ + type: "text", + name: "q", + value: q, + placeholder: i18n.fileSearchPlaceholder, + class: "filter-box__input" + }), + div( + { class: "filter-box__controls" }, + select( + { name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.fileSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.fileSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.fileSortTop) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.fileSearchButton) + ) + ) + ), + div({ class: "audios-list" }, renderFileTable(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) + ) + ) + ); +}; + +exports.singleFileView = async (fileObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); + const q = safeText(params.q || ""); + const sort = safeText(params.sort || "recent"); + const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); + + const title = safeText(fileObj.title); + const { renderEncryptedChip, renderTransportChip } = require('./clearnet_view'); + const isAuthor = String(fileObj.author) === String(userId); + const clearnetAllowed = !fileObj.tribeId && !fileObj.encrypted && !fileObj.cipher; + const isClearnet = !!fileObj.clearnet; + + const chips = [ + renderLifespanChip(fileObj.lifetime, i18n), + fileObj.sizeBytes ? renderEcoTax(fileObj.sizeBytes, fileObj.key) : null + ].filter(Boolean); + + const ownerActions = renderFileOwnerActions(filter, fileObj, { q, sort }); + const sideActions = []; + for (const a of ownerActions) sideActions.push(a); + + const tagsNode = renderTags(fileObj.tags); + + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(fileObj.key, `/files/${encodeURIComponent(fileObj.key)}`, { + spreadTitle: i18n.seedAction, + author: fileObj.author, + deleteAction: String(fileObj.author) === String(userId) ? contentDeleteAction("file", fileObj.key) : null, + favKind: 'files', + ...(fileObj.url && fileObj.url.startsWith("&") ? { torrentFrom: { blobId: fileObj.url, name: fileObj.fileName || fileObj.title } } : {}), + isFavorite: fileObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: fileObj.title + }) + ); + + const fileSide = div({ class: "tribe-side" }, + div({ class: "shop-title-row" }, + title ? h2({ class: "tribe-card-title" }, title) : null, + fileObj.tribeId && fileObj.cipher ? renderEncryptedChip(i18n) : renderTransportChip(i18n), + clearnetAllowed ? renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('files', fileObj.title, fileObj.key) : null) : null, + clearnetAllowed && isAuthor ? renderClearnetSwitch('files', fileObj.rootId || fileObj.key, isClearnet) : null + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + safeText(fileObj.description) + ? p({ class: "tribe-side-description" }, ...renderStyledText(fileObj.description)) + : null, + tagsNode, + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + ); + + const fileMain = div({ class: "tribe-main" }, + detailActions, + fileObj.url && fileObj.url.startsWith("&") + ? div({ class: "torrent-download" }, + div({ class: "torrent-oasis torrent-detail-section" }, + h3({ class: "torrent-section-title" }, i18n.torrentOriginalSection || "Original"), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, fileObj.fileName || fileObj.title || "download")), + tr(td({ class: "tribe-info-label" }, i18n.fileSizeLabel), td({ class: "tribe-info-value" }, formatSize(fileObj.size))), + fileObj.mime ? tr(td({ class: "tribe-info-label" }, i18n.fileTypeLabel), td({ class: "tribe-info-value" }, fileObj.mime)) : null, + fileObj.cipher + ? tr(td({ class: "tribe-info-label" }, i18n.encryptedChipLabel || "E2E"), td({ class: "tribe-info-value" }, "AES-256-GCM")) + : tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(fileObj.url)))) + ), + a({ href: `/files/${encodeURIComponent(fileObj.key)}/get`, class: "filter-btn" }, `\u2B07 ${i18n.torrentOasisButton || "OASIS"}`) + ), + fileObj.torrentUrl + ? div({ class: "torrent-detail-section" }, + h3({ class: "torrent-section-title" }, "Torrent"), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, `${String(fileObj.fileName || fileObj.title || "download").replace(/\.torrent$/i, "")}.torrent`)), + tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(fileObj.torrentUrl)))) + ), + a({ href: `/blob/${encodeURIComponent(fileObj.torrentUrl)}?name=${encodeURIComponent(`${String(fileObj.fileName || fileObj.title || "download").replace(/\.torrent$/i, "")}.torrent`)}`, class: "filter-btn" }, `\u2B07 ${i18n.torrentExternalButton || "EXTERNAL"}`) + ) + : null + ) + : isExternalUrl(fileObj.url) + ? div({ class: "torrent-download" }, + div({ class: "torrent-oasis torrent-detail-section" }, + h3({ class: "torrent-section-title" }, i18n.fileExternalLink), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, fileObj.fileName || fileObj.title || "download")), + tr(td({ class: "tribe-info-label" }, i18n.fileSizeLabel), td({ class: "tribe-info-value" }, formatSize(fileObj.size))), + fileObj.mime ? tr(td({ class: "tribe-info-label" }, i18n.fileTypeLabel), td({ class: "tribe-info-value" }, fileObj.mime)) : null, + tr(td({ class: "tribe-info-label" }, "URL"), td({ class: "tribe-info-value" }, a({ href: safeExternalHref(fileObj.url), target: "_blank", rel: "noopener noreferrer" }, fileObj.url))) + ), + a({ href: safeExternalHref(fileObj.url), target: "_blank", rel: "noopener noreferrer", class: "filter-btn" }, `\u2B07 ${i18n.fileExternalLink}`) + ) + ) + : p(i18n.fileNoFile), + (() => { + const createdTs = fileObj.createdAt ? new Date(fileObj.createdAt).getTime() : NaN; + const updatedTs = fileObj.updatedAt ? new Date(fileObj.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + + return p( + { class: "card-footer" }, + span({ class: "date-link" }, `${moment(fileObj.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(fileObj.author), + showUpdated + ? span( + { class: "votations-comment-date" }, + ` | ${i18n.fileUpdatedAt}: ${moment(fileObj.updatedAt).format("YYYY/MM/DD HH:mm")}` + ) + : null + ); + })(), + fileObj.tribeId ? null : renderEngagement(fileObj.key, + renderOpinionsVoting('/files/opinions', fileObj.key, fileObj.opinions, returnTo, fileObj.opinions_inhabitants), + renderFileCommentsSection(fileObj.key, comments, returnTo) + ) + ); + + return template( + i18n.filesTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.fileAllSectionTitle || i18n.filesTitle), + p(i18n.filesDescription) + ), + div( + { class: "filters" }, + form( + { method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "sort", value: sort }), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( + { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, + String(i18n.fileFilterFavorites).toUpperCase() + )] : []), + + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterTop).toUpperCase()), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.fileCreateButton) + ) + ), + div({ class: "tribe-details" }, fileSide, fileMain) + ) + ); +}; diff --git a/src/views/forum_view.js b/src/views/forum_view.js index 4b201a1..0f7be4b 100644 --- a/src/views/forum_view.js +++ b/src/views/forum_view.js @@ -1,14 +1,16 @@ const { - div, a, span, form, button, section, p, - input, label, br, select, option, h2, textarea + div, a, span, form, button, section, p, ul, li, + input, label, br, select, option, h2, textarea, details, summary } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip } = require('./main_views'); -const { renderEncryptedChip: renderForumEncryptedChip } = require('./clearnet_view'); +const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderStateChip, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged } = require('./main_views'); +const { renderEncryptedChip: renderForumEncryptedChip, renderTribeOriginChip } = require('./clearnet_view'); const { config } = require('../server/SSB_server.js'); -const { renderUrl } = require('../backend/renderUrl'); -const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); +const { renderStyledText } = require('../backend/renderStyledText'); +const { renderStyledHtml } = require('../backend/renderStyledText'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { TEXT_CAP } = require('../backend/long_text'); const userId = config.keys.id; @@ -22,14 +24,15 @@ exports.renderForumInvitePage = (code) => { return template(i18n.invitesForumsTitle || 'Forums', section(pageContent)); }; -const BASE_FILTERS = ['hot','all','mine','recent','top']; -const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.','POLITICS','TECH']; -const CAT_BLOCK2 = ['SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS']; +const BASE_FILTERS = ['hot','all','recent','mine','top']; +const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.']; +const CAT_BLOCK2 = ['POLITICS','TECH','SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS']; const CAT_BLOCK3 = ['PHILOSOPHY','SOCIETY','PRIVACY','CYBERWARFARE','SURVIVALISM']; const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3]; const catKey = (c) => 'forumCat' + String(c || '').replace(/\./g,'').replace(/[\s-]/g,'').toUpperCase(); const catLabel = (c) => i18n[catKey(c)] || c; +const tribeCatLabel = (c) => i18n['tribeForumCat' + String(c || '').charAt(0) + String(c || '').slice(1).toLowerCase()] || c; const Z = 1.96; function wilsonScore(pos, neg) { @@ -43,9 +46,9 @@ function getFilteredForums(filter, forums) { const now = Date.now(); if (filter === 'mine') return forums.filter(f => f.author === userId); if (filter === 'recent') return forums.filter(f => new Date(f.createdAt).getTime() >= now - 86400000); - if (filter === 'top') return forums.slice().sort((a,b) => b.score - a.score); + if (filter === 'top') return forums.filter(f => !f.tribeOrigin).sort((a,b) => b.score - a.score); if (filter === 'hot') return forums - .filter(f => new Date(f.createdAt).getTime() >= now - 86400000) + .filter(f => !f.tribeOrigin && new Date(f.createdAt).getTime() >= now - 86400000) .sort((a,b) => b.score - a.score); if (ALL_CATS.includes(filter)) return forums.filter(f => f.category === filter); @@ -84,61 +87,75 @@ const renderVotes = (target, score, forumId) => ) ); -const renderForumForm = () => +const renderForumForm = (draft = null) => div({ class: 'forum-form' }, - form({ action: '/forum/create', method: 'POST' }, + form({ action: '/forum/create', method: 'POST', enctype: 'multipart/form-data' }, label(i18n.forumCategoryLabel), br(), select({ name: 'category', required: true }, - ALL_CATS.map(cat => option({ value: cat }, catLabel(cat))) + ALL_CATS.map(cat => option({ value: cat, ...(draft && draft.category === cat ? { selected: true } : {}) }, catLabel(cat))) ), br(), br(), label(i18n.eventPrivacyLabel), br(), select({ name: 'isPublic', id: 'isPublic' }, - option({ value: 'public', selected: true }, i18n.eventPublic), - option({ value: 'private' }, i18n.eventPrivate) + option({ value: 'public', ...(draft && draft.isPrivate ? {} : { selected: true }) }, i18n.eventPublic), + option({ value: 'private', ...(draft && draft.isPrivate ? { selected: true } : {}) }, i18n.eventPrivate) ), br(), br(), label(i18n.forumTitleLabel), br(), input({ type: 'text', - name: 'title', + name: 'title', maxlength: '100', required: true, - placeholder: i18n.forumTitlePlaceholder + placeholder: i18n.forumTitlePlaceholder, + value: draft ? String(draft.title || '') : '' }), br(), br(), label(i18n.forumMessageLabel), br(), - textarea({ + textarea({ maxlength: String(TEXT_CAP), name: 'text', required: true, - rows: 4, + rows: 8, placeholder: i18n.forumMessagePlaceholder - }), br(), br(), + }, draft ? String(draft.text || '') : ''), br(), br(), + label(i18n.uploadMedia), br(), + input({ type: 'file', name: 'blob' }), br(), br(), button({ type: 'submit' }, i18n.forumCreateButton) ) ); -const renderThread = (nodes, level = 0, forumId) => { +const bySupport = (a, b) => + wilsonScore(b.positiveVotes, b.negativeVotes) - wilsonScore(a.positiveVotes, a.negativeVotes); + +const bestReplyKey = (nodes) => { + const flat = []; + const walk = (list) => { for (const m of Array.isArray(list) ? list : []) { if (m && m.text && String(m.text).trim()) flat.push(m); walk(m && m.children); } }; + walk(nodes); + if (!flat.length) return null; + const sorted = flat.slice().sort(bySupport); + const top = sorted[0]; + if (wilsonScore(top.positiveVotes, top.negativeVotes) > 0) return top.key; + const first = (Array.isArray(nodes) ? nodes : []).filter(m => m && m.text && String(m.text).trim()).sort(bySupport)[0]; + return first ? first.key : null; +}; + +const renderThread = (nodes, level = 0, forumId, bestKey = null) => { if (!Array.isArray(nodes)) return []; return [...nodes] .filter(m => (m && m.text && String(m.text).trim()) || (m && Array.isArray(m.children) && m.children.length)) - .sort((a, b) => - wilsonScore(b.positiveVotes, b.negativeVotes) - - wilsonScore(a.positiveVotes, a.negativeVotes) - ) - .flatMap((m, i) => { - const isTopLevelWinner = level === 0 && i === 0; + .sort(bySupport) + .flatMap((m) => { const classList = [ 'forum-comment', `level-${level}`, - isTopLevelWinner ? 'highlighted-reply' : '' + bestKey && m.key === bestKey ? 'highlighted-reply' : '' ].filter(Boolean).join(' '); const commentBox = div( - { class: classList }, + { class: classList, id: `msg-${String(m.key || '').replace(/[^a-zA-Z0-9]/g, '')}` }, div({ class: 'comment-header' }, span({ class: 'date-link' }, - `${moment(m.timestamp).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), + `${moment(m.timestamp).format('YYYY/MM/DD HH:mm')}`), userLink(m.author), div({ class: 'comment-votes' }, span({ class: 'votes-count' }, `▲: ${m.positiveVotes || 0}`), - span({ class: 'votes-count', style: 'margin-left:12px;' }, + span({ class: 'votes-count votes-count-negative' }, `▼: ${m.negativeVotes || 0}`) ) ), @@ -151,128 +168,129 @@ const renderThread = (nodes, level = 0, forumId) => { ...(m.text || '').split('\n') .map(l => l.trim()) .filter(l => l) - .map(l => p(...renderUrl(l))) + .map(l => p(...renderStyledText(l, { zoomImages: true }))) ) ) ), - div({ class: 'new-reply' }, + details({ class: 'new-reply forum-inline-reply', ...(draftFor(m.key) ? { open: true } : {}) }, + summary({ class: 'forum-inline-reply-toggle' }, i18n.tribeForumReply), form({ method: 'POST', action: `/forum/${encodeURIComponent(forumId)}/message`, - class: 'comment-form' + class: 'comment-form', + enctype: 'multipart/form-data' }, input({ type: 'hidden', name: 'parentId', value: m.key }), - textarea({ + textarea({ maxlength: String(TEXT_CAP), name: 'message', - rows: 2, + rows: 4, required: true, placeholder: i18n.forumMessagePlaceholder, class: 'comment-textarea' - }), - button({ type: 'submit', class: 'forum-send-btn' }, 'Reply') + }, draftFor(m.key)), + div({ class: 'comment-file-upload' }, input({ type: 'file', name: 'blob' })), + button({ type: 'submit', class: 'forum-send-btn' }, i18n.tribeForumReply) ) ) ); - return [ commentBox, ...renderThread(m.children || [], level + 1, forumId) ]; + return [ commentBox, ...renderThread(m.children || [], level + 1, forumId, bestKey) ]; }); }; +const forumHref = (f) => `/forum/${encodeURIComponent(f.key)}`; + +const forumCounts = (f) => span({ class: 'card-label activity-update-counts mailing-counts' }, + `👥: ${f.participants?.length || 1} · 💬 ${Math.max(0, (f.messagesCount || 1) - 1)} · ▲ ${f.positiveVotes || 0} · ▼ ${f.negativeVotes || 0}`); + const renderForumList = (forums, currentFilter, spreadMap = new Map()) => { const visibleForums = (Array.isArray(forums) ? forums : []).filter(f => (f && f.title && String(f.title).trim()) || (f && f.text && String(f.text).trim()) ) - return div({ class: 'forum-list' }, - visibleForums.length - ? visibleForums.map(f => - div({ class: 'forum-card' }, - div({ class: 'forum-score-col' }, - renderVotes(f.key, f.score, f.key) - ), - div({ class: 'forum-main-col' }, - div({ class: 'forum-header-row' }, - a({ - class: 'forum-category', - href: `/forum?filter=${encodeURIComponent(f.category)}` - }, `[${catLabel(f.category)}]`), - a({ - class: 'forum-title', - href: `/forum/${encodeURIComponent(f.key)}` - }, f.title), - f.isPrivate ? renderPrivacyChip(true, i18n) : null, - f.isPrivate ? renderForumEncryptedChip(i18n) : null, - renderLifespanChip(f.lifetime, i18n) - ), - div({ - class: 'forum-body', - innerHTML: sanitizeHtml(renderTextWithStyles(f.text || '')) - }), - div({ class: 'forum-meta' }, - span({ class: 'forum-positive-votes' }, - `▲: ${f.positiveVotes || 0}`), - span({ class: 'forum-negative-votes', style: 'margin-left:12px;' }, - `▼: ${f.negativeVotes || 0}`), - span({ class: 'forum-participants' }, - `${i18n.forumParticipants.toUpperCase()}: ${f.participants?.length || 1}`), - span({ class: 'forum-messages' }, - `${i18n.forumMessages.toUpperCase()}: ${(f.messagesCount || 1) - 1}`), - form({ method: 'GET', action: `/forum/${encodeURIComponent(f.key)}`, class: 'visit-forum-form' }, - button({ type: 'submit', class: 'filter-btn' }, i18n.forumVisitButton) - ) - ), - (() => { - const btn = renderSpreadButton(f.key, spreadMap.get(f.key)); - return btn ? div({ class: 'card-spread-left' }, btn) : null; - })(), - div({ class: 'forum-footer' }, - span({ class: 'date-link' }, - `${moment(f.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), - userLink(f.author) - ), - currentFilter === 'mine' && f.author === userId - ? div({ class: 'forum-owner-actions' }, - form({ - method: 'POST', - action: `/forum/delete/${encodeURIComponent(f.key)}`, - class: 'forum-delete-form' - }, - button({ type: 'submit', class: 'delete-btn' }, - i18n.forumDeleteButton) - ) - ) - : null + if (!visibleForums.length) return div({ class: 'tribe-grid' }, p(i18n.noForums)); + return ul({ class: 'mailing-archive' }, + ...paged(visibleForums).map(f => { + if (f.tribeOrigin) { + return li({ class: 'mailing-archive-item' }, + div({ class: 'emergency-update-head mailing-archive-head' }, + div({ class: 'mailing-archive-meta' }, + a({ href: f.tribeOrigin.href, class: 'user-link' }, f.title || '—'), + ALL_CATS.includes(f.category) + ? a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${catLabel(f.category)}]`) + : (f.category ? span({ class: 'forum-category' }, `[${tribeCatLabel(f.category)}]`) : null), + span({ class: 'card-label activity-update-counts mailing-counts' }, `👥: ${f.participants?.length || 1} · 💬 ${Math.max(0, (f.messagesCount || 1) - 1)}`), + renderLifespanChip(f.lifetime, i18n), + renderTribeOriginChip(f.tribeOrigin) + ) ) + ); + } + return li({ class: 'mailing-archive-item' }, + div({ class: 'emergency-update-head mailing-archive-head' }, + div({ class: 'mailing-archive-meta' }, + a({ href: forumHref(f), class: 'user-link' }, f.title || '—'), + a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${catLabel(f.category)}]`), + forumCounts(f), + f.isPrivate ? renderPrivacyChip(true, i18n) : null, + f.isPrivate ? renderForumEncryptedChip(i18n) : null, + renderLifespanChip(f.lifetime, i18n), + f.subscriptionIn === true + ? renderStateChip('mutuals', '✉', i18n.subscriptionOn) + : (f.subscriptionIn === false ? renderStateChip('closed', '✉', i18n.subscriptionOff) : null) + ), + renderContentActions(f.key, forumHref(f), { spread: spreadMap.get(f.key) || null, author: f.author, favKind: 'forum', isFavorite: f.isFavorite, reportTitle: f.title, deleteAction: f.author === userId ? `/forum/delete/${encodeURIComponent(f.key)}` : null }) ) - ) - : p(i18n.noForums) + ); + }) ); } exports.forumView = async (forums, currentFilter, params = {}) => { const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{}); + const emptyModForum = moduleIsEmpty(getFilteredForums(currentFilter || 'all', forums), currentFilter || 'all', 'all', params.q); + const censusForums = Array.isArray(params.censusList) ? params.censusList : (Array.isArray(forums) ? forums : []); + const presentCats = new Set(censusForums.map(f => f && f.category).filter(Boolean)); + const baseChipVisible = (mode) => { + if (mode === currentFilter || mode === 'all') return true; + if (mode === 'mine') return censusForums.some(f => String(f.author) === String(userId)); + if (mode === 'recent') return censusForums.length > 0; + return censusForums.length > 0; + }; return template(i18n.forumTitle, section( - div({ class: 'tags-header' }, - h2(currentFilter === 'create' - ? i18n.forumCreateSectionTitle - : i18n.forumTitle), + div({ class: 'tags-header module-header-line' }, + h2(i18n.forumTitle), p(i18n.forumDescription) ), - div({ class: 'mode-buttons-cols' }, - generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', { + div({ class: 'mode-buttons-row' }, + ...(emptyModForum ? [] : [ + generateFilterButtons(BASE_FILTERS.filter(baseChipVisible), currentFilter, '/forum', { hot: i18n.forumFilterHot, all: i18n.forumFilterAll, mine: i18n.forumFilterMine, recent: i18n.forumFilterRecent, top: i18n.forumFilterTop }), - generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum', CAT_I18N_MAP_UP), - generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum', CAT_I18N_MAP_UP), - generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum', CAT_I18N_MAP_UP), + generateFilterButtons(CAT_BLOCK1.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP), + generateFilterButtons(CAT_BLOCK2.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP), + generateFilterButtons(CAT_BLOCK3.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP) + ]), renderCreateForumButton() ), currentFilter === 'create' - ? renderForumForm() + ? null + : emptyModForum ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(getFilteredForums(currentFilter || 'all', forums).length), + form({ method: 'GET', action: '/forum', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: currentFilter || 'all' }), + input({ type: 'text', name: 'q', value: params.q || '', placeholder: i18n.forumSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) + ) + ), + currentFilter === 'create' + ? renderForumForm(params.draft || null) : renderForumList( getFilteredForums(currentFilter || 'all', forums), currentFilter, @@ -282,15 +300,22 @@ exports.forumView = async (forums, currentFilter, params = {}) => { ); }; -exports.singleForumView = async (forum, messagesData, currentFilter) => { +let replyDraft = null; +const draftFor = (parentId) => replyDraft && String(replyDraft.parentId || '') === String(parentId || '') ? String(replyDraft.message || '') : ''; + +exports.singleForumView = async (forum, messagesData, currentFilter, replyId = null, params = {}) => { + replyDraft = params.draft || null; const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{}); + const isForumOwner = String(forum.author) === String(userId); + const sharesForum = isForumOwner || (forum.participants || []).includes(userId); + const forumSubIn = isForumOwner || (forum.subscription && forum.subscription.subscribed === true); return template(forum.title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.forumTitle), p(i18n.forumDescription) ), - div({ class: 'mode-buttons-cols' }, + div({ class: 'mode-buttons-row' }, generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', { hot: i18n.forumFilterHot, all: i18n.forumFilterAll, @@ -305,22 +330,23 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { ) ), div({ class: 'forum-thread-container' }, - div({ - class: 'forum-card forum-thread-header', - style: 'display:flex;align-items:flex-start;' - }, - div({ - class: 'root-vote-col', - style: 'width:60px;text-align:center;' - }, renderVotes( + div({ class: 'forum-card forum-thread-header' }, + div({ class: 'root-vote-col' }, renderVotes( forum.key, messagesData.totalScore, forum.key )), - div({ - class: 'forum-main-col', - style: 'flex:1;padding-left:10px;' - }, + div({ class: 'forum-main-col' }, + div({ class: 'card-header activity-card-header' }, + renderContentActions(forum.key, forumHref(forum), { + author: forum.author, + favKind: 'forum', + isFavorite: forum.isFavorite, + spread: params.spreads || null, + reportTitle: forum.title, + deleteAction: isForumOwner ? `/forum/delete/${encodeURIComponent(forum.key)}` : null + }) + ), div({ class: 'forum-header-row' }, a({ class: 'forum-category', @@ -333,6 +359,9 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { forum.isPrivate ? renderPrivacyChip(true, i18n) : null, forum.isPrivate ? renderForumEncryptedChip(i18n) : null, renderLifespanChip(forum.lifetime, i18n), + (forum.subscription && sharesForum) + ? renderStateChip(forumSubIn ? 'mutuals' : 'closed', '✉', forumSubIn ? i18n.subscriptionOn : i18n.subscriptionOff) + : null, (forum.isPrivate && forum.author === userId) ? form({ method: 'POST', action: `/forum/generate-invite/${encodeURIComponent(forum.key)}`, class: 'forum-invite-form' }, button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeGenerateInvite)) @@ -342,73 +371,70 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeOpenInvitation)) : null, (forum.isPrivate && forum.author === userId && forum.openInviteCode) - ? span({ class: 'tribe-open-invite' }, + ? div({ class: 'tribe-open-invite' }, span({ class: 'card-label' }, i18n.tribeInviteCodeText), - span({ class: 'tribe-open-invite-code' }, forum.openInviteCode)) + span({ class: 'tribe-open-invite-code' }, forum.openInviteCode), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/forum/${encodeURIComponent(forum.openInviteCode)}` })) : null, (forum.isPrivate && forum.author === userId && forum.openInviteCode) ? form({ method: 'POST', action: `/forum/open-invite/remove/${encodeURIComponent(forum.key)}`, class: 'forum-invite-form' }, button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeRemoveInvitation)) : null, (forum.isPrivate && forum.author !== userId) - ? a({ class: 'tribe-action-btn', href: '/invites#invites-forums' }, i18n.tribeEnterInvite) - : null, - (forum.author === userId) - ? form({ method: 'POST', action: `/forum/delete/${encodeURIComponent(forum.key)}`, class: 'forum-delete-form' }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.forumDeleteButton)) + ? a({ class: 'tribe-action-btn', href: '/invites?filter=others' }, i18n.tribeEnterInvite) : null ), + (forum.subscription && sharesForum) + ? renderSubscriptionBox({ + target: forum.rootId || forum.key, + scope: 'forum', + subscribed: forum.subscription.subscribed === true, + count: forum.subscription.count, + isOwner: isForumOwner, + returnTo: `/forum/${encodeURIComponent(forum.key)}` + }) + : null, div({ class: 'forum-body', - innerHTML: sanitizeHtml(renderTextWithStyles(forum.text || '')) + [RAW_HTML]: sanitizeHtml(renderStyledHtml(forum.text || '')) }), div({ class: 'forum-meta' }, - span({ class: 'votes-count' }, + span({ class: 'forum-positive-votes' }, `▲: ${messagesData.positiveVotes}`), - span({ - class: 'votes-count', - style: 'margin-left:12px;' - }, `▼: ${messagesData.negativeVotes}`), + span({ class: 'forum-negative-votes' }, + `▼: ${messagesData.negativeVotes}`), span({ class: 'forum-participants' }, `${i18n.forumParticipants.toUpperCase()}: ${forum.participants?.length || 1}`), span({ class: 'forum-messages' }, `${i18n.forumMessages.toUpperCase()}: ${messagesData.total}`) ), - (() => { - const btn = renderSpreadButton(forum.key); - return btn ? div({ class: 'card-spread-left' }, btn) : null; - })(), div({ class: 'forum-footer' }, span({ class: 'date-link' }, - `${moment(forum.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), + `${moment(forum.createdAt).format('YYYY/MM/DD HH:mm')}`), userLink(forum.author) ) ) ), - div({ - class: 'new-message-wrapper', - style: 'margin-top:12px;' - }, + div({ class: 'new-message-wrapper' }, form({ method: 'POST', action: `/forum/${encodeURIComponent(forum.key)}/message`, - class: 'new-message-form' + class: 'new-message-form', + enctype: 'multipart/form-data' }, - textarea({ + textarea({ maxlength: String(TEXT_CAP), name: 'message', - rows: 4, + rows: 8, required: true, placeholder: i18n.forumMessagePlaceholder, - style: 'width:100%;' - }), br(), - button({ - type: 'submit', - class: 'forum-send-btn', - style: 'margin-top:4px;' - }, i18n.forumSendButton) + class: 'new-message-textarea' + }, draftFor(null)), br(), + label(i18n.uploadMedia), br(), + input({ type: 'file', name: 'blob' }), br(), + button({ type: 'submit', class: 'forum-send-btn' }, i18n.forumSendButton) ) ), - ...renderThread(messagesData.messages, 0, forum.key) + ...renderThread(messagesData.messages, 0, forum.key, bestReplyKey(messagesData.messages)) ) ); }; diff --git a/src/views/gallery_view.js b/src/views/gallery_view.js new file mode 100644 index 0000000..741661c --- /dev/null +++ b/src/views/gallery_view.js @@ -0,0 +1,121 @@ +const { div, a, span, img, video, input, button, label, br } = require("../server/node_modules/hyperaxe") +const { i18n } = require("./main_views") +const { MAX_IMAGES } = require("../models/media_gallery") + +const safeArr = (v) => (Array.isArray(v) ? v : []) +const safeText = (v) => String(v == null ? "" : v).trim() +const downloadHref = (src) => `${src}${String(src).includes("?") ? "&" : "?"}download=1` + +const blobIdOf = (entry) => { + const value = safeText(entry) + const m = value.match(/\(([^)\s]+)\)/) + return m ? m[1] : value +} + +const blobUrl = (id, size) => size + ? `/image/${size}/${encodeURIComponent(id)}` + : `/blob/${encodeURIComponent(id)}` + +const isVideoEntry = (entry) => /\[video:[^\]]*\]\(/.test(String(entry || "")) + +const imagesOf = (item) => { + const list = safeArr(item && item.images).filter(Boolean) + if (list.length) return list + return item && item.image ? [item.image] : [] +} + +const videoOf = (item) => safeText(item && item.video) + +const renderMediaThumb = (entry, alt = "") => isVideoEntry(entry) + ? video({ controls: true, class: "gallery-image", src: blobUrl(blobIdOf(entry)), preload: 'metadata' }) + : img({ src: blobUrl(blobIdOf(entry), 256), class: "gallery-image", alt }) + +const lightboxId = (scope, itemId, index) => `${scope}-photo-${encodeURIComponent(itemId)}-${index}` + +let zoomSeq = 0 +const renderZoomableImage = (src, { alt = "", imgClass = "", linkClass = "" } = {}) => { + if (!src) return null + zoomSeq = (zoomSeq + 1) % 1000000 + const id = `zoom-${zoomSeq}-${String(src).replace(/[^a-zA-Z0-9]/g, "").slice(-10)}` + return [ + a({ href: `#${id}`, id: `${id}-src`, class: linkClass ? `zoom-link ${linkClass}` : "zoom-link" }, img({ src, class: imgClass, alt })), + div({ id, class: "lightbox" }, + a({ href: downloadHref(src), class: "lightbox-download", title: i18n.fileShareDownload || "Download" }, "⤓"), + a({ href: `#${id}-src`, class: "lightbox-close" }, "×"), + img({ src, class: "lightbox-image", alt }) + ) + ] +} + +const renderPhotoGallery = (item, scope = "media") => { + const list = imagesOf(item) + const clip = videoOf(item) + if (!list.length && !clip) return null + const id = (item && (item.id || item.key || item.rootId)) || "" + return div({ class: "media-gallery" }, + list.length + ? div({ class: "gallery" }, + list.map((entry, i) => + isVideoEntry(entry) + ? span({ class: "gallery-item" }, renderMediaThumb(entry)) + : a({ href: `#${lightboxId(scope, id, i)}`, id: `${lightboxId(scope, id, i)}-src`, class: "gallery-item" }, renderMediaThumb(entry)) + ) + ) + : null, + list.filter(entry => !isVideoEntry(entry)).map((entry) => + div({ id: lightboxId(scope, id, list.indexOf(entry)), class: "lightbox" }, + a({ href: downloadHref(blobUrl(blobIdOf(entry))), class: "lightbox-download", title: i18n.fileShareDownload || "Download" }, "⤓"), + a({ href: `#${lightboxId(scope, id, list.indexOf(entry))}-src`, class: "lightbox-close" }, "×"), + img({ src: blobUrl(blobIdOf(entry)), class: "lightbox-image", alt: "" }) + ) + ), + clip + ? div({ class: "media-video media-video-centered" }, + video({ controls: true, class: "media-video-player", src: blobUrl(blobIdOf(clip)), preload: 'metadata' }) + ) + : null + ) +} + +const renderGalleryFields = (item = {}, isEdit = false, maxImages = MAX_IMAGES) => { + const list = imagesOf(item) + return [ + label(i18n.galleryImages), + br(), + ...list.map(entry => input({ type: "hidden", name: "keepImages", value: entry })), + list.length < maxImages + ? div({ class: "media-file-row" }, + input({ type: "file", name: "images", accept: "image/*", multiple: true }), + button({ type: "submit", name: "action", value: "addPhoto", attrs: { formnovalidate: "formnovalidate" }, class: "filter-btn media-add-btn" }, i18n.galleryAddPhoto) + ) + : null, + br(), + list.length + ? [ + div({ class: "gallery media-form-gallery" }, + list.map((entry, i) => + span({ class: "gallery-item" }, + renderMediaThumb(entry), + button({ type: "submit", name: "removePhoto", value: String(i), attrs: { formnovalidate: "formnovalidate" }, class: "media-remove-btn" }, i18n.galleryRemovePhoto) + ) + ) + ), + br() + ] + : null, + br() + ] +} + +module.exports = { + MAX_IMAGES, + blobIdOf, + blobUrl, + isVideoEntry, + imagesOf, + videoOf, + renderMediaThumb, + renderPhotoGallery, + renderGalleryFields, + renderZoomableImage +} diff --git a/src/views/games_view.js b/src/views/games_view.js index 5619f82..88bbb0a 100644 --- a/src/views/games_view.js +++ b/src/views/games_view.js @@ -1,5 +1,5 @@ const { div, h2, h3, p, section, form, input, button, a, img, table, tr, td, th, span, iframe } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink} = require('./main_views'); +const { template, i18n, userLink, renderModuleStats } = require('./main_views'); const moment = require("../server/node_modules/moment"); const getGames = () => [ @@ -7,6 +7,7 @@ const getGames = () => [ { id: 'ecoinflow', title: () => i18n.gamesTheFlowTitle, desc: () => i18n.gamesTheFlowDesc }, { id: 'neoninfiltrator', title: () => i18n.gamesNeonInfiltratorTitle, desc: () => i18n.gamesNeonInfiltratorDesc }, { id: 'audiopendulum', title: () => i18n.gamesAudioPendulumTitle, desc: () => i18n.gamesAudioPendulumDesc }, + { id: 'visualdj', title: () => i18n.gamesVisualDjTitle, desc: () => i18n.gamesVisualDjDesc }, { id: 'spaceinvaders', title: () => i18n.gamesSpaceInvadersTitle, desc: () => i18n.gamesSpaceInvadersDesc }, { id: 'arkanoid', title: () => i18n.gamesArkanoidTitle, desc: () => i18n.gamesArkanoidDesc }, { id: 'pingpong', title: () => i18n.gamesPingPongTitle, desc: () => i18n.gamesPingPongDesc }, @@ -18,16 +19,16 @@ const getGames = () => [ { id: 'artillery', title: () => i18n.gamesArtilleryTitle, desc: () => i18n.gamesArtilleryDesc }, { id: 'labyrinth', title: () => i18n.gamesLabyrinthTitle, desc: () => i18n.gamesLabyrinthDesc }, { id: 'cocoman', title: () => i18n.gamesCocomanTitle, desc: () => i18n.gamesCocomanDesc }, - { id: 'tetris', title: () => i18n.gamesTetrisTitle, desc: () => i18n.gamesTetrisDesc } + { id: 'tetris', title: () => i18n.gamesTetrisTitle, desc: () => i18n.gamesTetrisDesc }, + { id: 'minesweeper', title: () => i18n.gamesMinesweeperTitle, desc: () => i18n.gamesMinesweeperDesc } ]; const shortId = (feedId) => feedId ? '@' + feedId.slice(1, 9) + '...' : '?'; -const renderHallOfFame = (hall) => { - const games = getGames(); +const renderHallOfFame = (hall, games) => { const gamesWithScores = games.filter(g => hall[g.id] && hall[g.id].length > 0); if (gamesWithScores.length === 0) { - return p({ class: 'no-content' }, i18n.gamesNoScores || 'No scores yet.'); + return div({ class: 'no-content-box' }, p({ class: 'no-content' }, i18n.gamesNoScores || 'No scores yet.')); } return div({ class: 'games-scoring-list' }, gamesWithScores.map(game => @@ -55,7 +56,7 @@ const renderHallOfFame = (hall) => { ); }; -const VALID_GAME_IDS = new Set(['cocoland','ecoinflow','neoninfiltrator','audiopendulum','spaceinvaders','arkanoid','pingpong','asteroids','rockpaperscissors','tiktaktoe','flipflop','8ball','artillery','labyrinth','cocoman','tetris']); +const VALID_GAME_IDS = new Set(['cocoland','ecoinflow','neoninfiltrator','audiopendulum','visualdj','spaceinvaders','arkanoid','pingpong','asteroids','rockpaperscissors','tiktaktoe','flipflop','8ball','artillery','labyrinth','cocoman','tetris','minesweeper']); exports.gameShellView = (name) => { if (!VALID_GAME_IDS.has(name)) { @@ -75,7 +76,7 @@ exports.gameShellView = (name) => { return template( game ? game.title() : name, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.gamesTitle), p(i18n.gamesDescription || 'Discover and play some mini-games in your network.') ), @@ -92,26 +93,38 @@ exports.gameShellView = (name) => { ); }; -exports.gamesView = (filter = 'all', hall = null) => { - const games = getGames(); +exports.gamesView = (filter = 'all', hall = null, q = '') => { + const ql = String(q || '').trim().toLowerCase(); + const games = getGames().filter(g => !ql || `${g.title()} ${g.desc()}`.toLowerCase().includes(ql)); const filterBar = div({ class: 'filter-group' }, form({ method: 'GET', action: '/games' }, input({ type: 'hidden', name: 'filter', value: 'all' }), button({ type: 'submit', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, - i18n.gamesFilterAll + String(i18n.gamesFilterAll).toUpperCase() ) ), form({ method: 'GET', action: '/games' }, input({ type: 'hidden', name: 'filter', value: 'scoring' }), button({ type: 'submit', class: filter === 'scoring' ? 'filter-btn active' : 'filter-btn' }, - i18n.gamesFilterScoring + String(i18n.gamesFilterScoring).toUpperCase() + ) + ) + ); + + const searchBox = div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(games.length), + form({ method: 'GET', action: '/games', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: q, placeholder: i18n.gamesSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) ) ) ); const content = filter === 'scoring' && hall - ? renderHallOfFame(hall) + ? renderHallOfFame(hall, games) : div({ class: 'games-single-col' }, games.map(game => { const topScore = hall && hall[game.id] && hall[game.id].length > 0 ? hall[game.id][0] : null; @@ -140,12 +153,13 @@ exports.gamesView = (filter = 'all', hall = null) => { return template( i18n.gamesTitle, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.gamesTitle), p(filter === 'scoring' ? i18n.gamesHallOfFame : (i18n.gamesDescription || 'Discover and play some mini-games in your network.')) ), filterBar ), + section(searchBox), section(content) ); }; diff --git a/src/views/graphos_view.js b/src/views/graphos_view.js index ea2332e..e291f6b 100644 --- a/src/views/graphos_view.js +++ b/src/views/graphos_view.js @@ -1,6 +1,7 @@ const h = require("../server/node_modules/hyperaxe"); -const { div, h2, p, section, button, form, input, span, a } = h; -const { template, i18n } = require('./main_views'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { div, h2, p, section, button, form, input, span, a, table, tr, td } = h; +const { template, i18n, renderStateChip } = require('./main_views'); const TAU = Math.PI * 2; @@ -26,7 +27,7 @@ const adaptiveLayout = (n) => { return { rRatio: 0.44, nodeR: 14, labelGap: 12 }; }; -const buildGraphSvg = (me, peers) => { +const buildGraphSvg = (me, peers, links = []) => { const W = 900; const H = 600; const cx = W / 2; @@ -48,6 +49,11 @@ const buildGraphSvg = (me, peers) => { const edges = positions.map(({ peer, x, y }) => `` ).join(''); + const at = new Map(positions.map(({ peer, x, y }) => [peer.key, { x, y }])); + const chords = links.filter(l => at.has(l.a) && at.has(l.b)).map(l => { + const A = at.get(l.a), B = at.get(l.b); + return ``; + }).join(''); const nodes = positions.map(({ peer, x, y }) => { const xs = x.toFixed(2); @@ -84,13 +90,30 @@ const buildGraphSvg = (me, peers) => { + ``; return `` - + edges + nodes + center + + chords + edges + nodes + center + ``; }; -const kpi = (label, value) => div({ class: 'stats-kpi' }, - div({ class: 'stats-kpi-label' }, label), - div({ class: 'stats-kpi-value' }, String(value)) +const zoomableGraph = (id, svg, stats = []) => [ + div({ class: 'graphos-legend graphos-statsbar' }, ...stats.filter(Boolean)), + div({ class: 'graphos-canvas' }, + a({ href: process.env.OASIS_MOBILE === '1' ? `#${id}-focus` : `#${id}`, id: `${id}-src`, class: 'tribe-action-btn graphos-zoom-btn' }, `\u2922 ${i18n.graphosZoom || 'Enlarge'}`), + div({ class: 'graphos-canvas-svg', [RAW_HTML]: svg }) + ), + div({ id, class: 'lightbox graphos-lightbox' }, + a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'), + div({ class: 'graphos-canvas-zoom' }, + div({ class: 'graphos-zoom-stage' }, + div({ class: 'graphos-zoom-svg', [RAW_HTML]: svg }), + span({ id: `${id}-focus`, class: 'graphos-zoom-focus' }) + ) + ) + ) +]; + +const kpi = (label, value) => span({ class: 'graphos-legend-item graphos-kpi' }, + span({ class: 'stats-kpi-label' }, String(label).toUpperCase()), + span({ class: 'stats-kpi-value' }, String(value)) ); const legendItem = (kind, label) => @@ -99,16 +122,16 @@ const legendItem = (kind, label) => span(label) ); -exports.graphosView = ({ filter, me, peers, kpis, focus = null, focusId = null, shown = null, total = null }) => { +exports.graphosView = ({ filter, me, peers, links = [], kpis, focus = null, focusId = null, shown = null, total = null }) => { const title = i18n.graphos || 'Graphos'; const description = i18n.graphosDescription || 'Interactive map of the network around you.'; - const modes = ['ALL', 'MINE']; + const modes = ['ALL', 'MINE', 'FEDERATION']; const capped = Number.isFinite(shown) && Number.isFinite(total) && total > shown; return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(description) ), @@ -126,28 +149,160 @@ exports.graphosView = ({ filter, me, peers, kpis, focus = null, focusId = null, modes.map(m => form({ method: 'GET', action: '/graphos' }, input({ type: 'hidden', name: 'filter', value: m }), - button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, i18n[m + 'Button']) + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(m === 'FEDERATION' ? i18n.graphosFederation : i18n[m + 'Button']).toUpperCase()) ) ) ), capped ? p({ class: 'graphos-cap-note' }, `${i18n.graphosShowing || 'Showing'} ${shown} / ${total}`) : null, + ...zoomableGraph('graphos-zoom', buildGraphSvg(me, peers, links), [ + kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total), + kpi(i18n.online || 'Online', kpis.online), + filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null, + filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null + ]), div({ class: 'graphos-legend' }, legendItem('me', i18n.graphosYou || 'You'), legendItem('online', i18n.online || 'Online'), filter !== 'MINE' ? legendItem('discovered', i18n.discovered || 'Discovered') : null, - filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null - ), - div({ class: 'graphos-canvas', innerHTML: buildGraphSvg(me, peers) }), - div({ class: 'stats-block' }, - div({ class: 'stats-grid' }, - kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total), - kpi(i18n.online || 'Online', kpis.online), - filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null, - filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null - ) + filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null, + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-mutual' }), span(i18n.graphosFedMutual)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-oneway' }), span(i18n.graphosFedOneWay)) ) ) ); }; + +const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0); +const olderThan = (v, own) => { + if (!v || !own) return false; + const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(own); + return a1 < a2 || (a1 === a2 && b1 < b2); +}; +const NET_REASON_KEYS = require('../models/peer_health').NET_REASON_KEYS; + +const pubLabel = (row) => { + const n = String(row.name || '').trim(); + if (n && n !== String(row.key).replace(/^@/, '').slice(0, 8)) return n.length > 22 ? n.slice(0, 21) + '…' : n; + if (row.host) return row.host.length > 22 ? row.host.slice(0, 21) + '…' : row.host; + return '@' + String(row.key).replace(/^@/, '').slice(0, 8) + '…'; +}; + +const buildFederationSvg = (myName, rows, links, ownVersion) => { + const W = 900; + const H = 640; + const cx = W / 2; + const cy = H / 2; + const N = Math.max(1, rows.length); + const r = Math.min(W, H) * (N <= 6 ? 0.34 : 0.42); + const pos = new Map(rows.map((row, i) => { + const angle = (i / N) * TAU - Math.PI / 2; + return [row.key, { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) }]; + })); + const radius = (row) => 12 + Math.min(14, Math.sqrt(row.inhabitants || 0) * 1.6); + const fed = links.map(l => { + const A = pos.get(l.a), B = pos.get(l.b); + return ``; + }).join(''); + const mine = rows.filter(row => row.state === 'connected' || row.following).map(row => { + const P = pos.get(row.key); + return ``; + }).join(''); + const nodes = rows.map(row => { + const P = pos.get(row.key); + const rr = radius(row); + const old = olderThan(row.version, ownVersion) || (!row.version && row.known); + const name = escText(pubLabel(row)); + const enc = escAttr(encodeURIComponent(row.key)); + const below = (P.y + rr + 14).toFixed(2); + return `` + + `${escText(row.name || row.host || row.key)}${row.version ? ' · ' + escText(row.version) : ''}` + + `` + + (old ? `` : '') + + (row.isolated ? `` : '') + + `` + + `${name}` + + (row.transport === 'tor' ? `TOR` : '') + + ``; + }).join(''); + const me = `${escText(myName)}` + + `` + + `${escText(myName)}`; + return `` + + fed + mine + nodes + me + + ``; +}; + +exports.graphosFederationView = ({ myName, rows = [], links = [], ownVersion = '' }) => { + const title = i18n.graphos || 'Graphos'; + const ORDER = { connected: 0, available: 1, unreachable: 2, unknown: 3 }; + const sorted = rows.slice().sort((x, y) => (ORDER[x.state] - ORDER[y.state]) || (y.inhabitants - x.inhabitants) || pubLabel(x).localeCompare(pubLabel(y))); + const outdated = (row) => olderThan(row.version, ownVersion) || (!row.version && row.known); + const count = (fn) => sorted.filter(fn).length; + const stateChip = (row) => { + if (row.state === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase()); + if (row.state === 'available') return renderStateChip(outdated(row) ? 'whole' : 'mutuals', null, String(i18n.peerStateStaged).toUpperCase()); + if (row.state === 'unreachable') return renderStateChip('hidden', null, String(i18n[NET_REASON_KEYS[row.reason] || 'peerErrSilent'] || i18n.peerErrOther).toUpperCase()); + return renderStateChip('hidden', null, String(i18n.graphosFedNoData).toUpperCase()); + }; + const versionChip = (row) => row.version + ? renderStateChip(outdated(row) ? 'closed' : 'mutuals', null, row.version) + : (row.known ? span({ title: i18n.peerVersionUnannounced }, renderStateChip('closed', null, String(i18n.peerOutdated).toUpperCase())) : '—'); + const transportChip = (row) => row.transport === 'unknown' ? '—' + : renderStateChip(row.transport === 'tor' ? 'fediverse' : 'transport', null, String(row.transport === 'tor' ? i18n.peerErrTor : row.transport === 'both' ? i18n.graphosFedBoth : i18n.graphosFedClearnet).toUpperCase()); + const linksCell = (row) => !row.known ? '—' : row.isolated ? renderStateChip('closed', null, String(i18n.graphosFedIsolated).toUpperCase()) : String(row.links); + return template( + title, + section( + div({ class: 'tags-header module-header-line' }, + h2(title), + p(i18n.graphosDescription || '') + ), + div({ class: 'mode-buttons stats-mode-row' }, + ['ALL', 'MINE', 'FEDERATION'].map(m => + form({ method: 'GET', action: '/graphos' }, + input({ type: 'hidden', name: 'filter', value: m }), + button({ type: 'submit', class: m === 'FEDERATION' ? 'filter-btn active' : 'filter-btn' }, String(m === 'FEDERATION' ? i18n.graphosFederation : i18n[m + 'Button']).toUpperCase()) + ) + ) + ), + ...(sorted.length ? zoomableGraph('graphos-fed-zoom', buildFederationSvg(myName || '', sorted, links, ownVersion), [ + kpi(i18n.graphosFedPubs, sorted.length), + kpi(i18n.peerStateConnected, count(r => r.state === 'connected')), + kpi(i18n.graphosFedUnreachable, count(r => r.state === 'unreachable')), + kpi(i18n.peerOutdated, count(outdated)), + kpi(i18n.peerErrTor, count(r => r.transport === 'tor')), + kpi(i18n.graphosFedLinks, links.filter(l => l.mutual).length), + kpi(i18n.graphosFedIsolated, count(r => r.isolated)) + ]) : [p(i18n.graphosFedEmpty)]), + div({ class: 'graphos-legend' }, + legendItem('me', i18n.graphosYou || 'You'), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-connected' }), span(i18n.peerStateConnected)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-available' }), span(i18n.peerStateStaged)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-unreachable' }), span(i18n.graphosFedUnreachable)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-outdated' }), span(i18n.peerOutdated)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-mutual' }), span(i18n.graphosFedMutual)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-oneway' }), span(i18n.graphosFedOneWay)) + ), + sorted.length ? table({ class: 'block-info-table graphos-fed-table' }, + tr( + td({ class: 'card-label' }, 'PUB'), + td({ class: 'card-label' }, i18n.peersOasisVersion), + td({ class: 'card-label' }, i18n.graphosFedTransport), + td({ class: 'card-label' }, i18n.peerStateLabel), + td({ class: 'card-label' }, i18n.graphosFedWith), + td({ class: 'card-label' }, i18n.graphosFedInhabitants) + ), + ...sorted.map(row => tr( + td({ 'data-label': 'PUB' }, a({ href: `/author/${encodeURIComponent(row.key)}`, class: 'user-link', title: row.host || row.key }, pubLabel(row))), + td({ 'data-label': i18n.peersOasisVersion }, versionChip(row)), + td({ 'data-label': i18n.graphosFedTransport }, transportChip(row)), + td({ 'data-label': i18n.peerStateLabel }, stateChip(row)), + td({ 'data-label': i18n.graphosFedWith }, linksCell(row)), + td({ 'data-label': i18n.graphosFedInhabitants }, row.known ? String(row.inhabitants) : '—') + )) + ) : null + ) + ); +}; diff --git a/src/views/hive_views.js b/src/views/hive_views.js new file mode 100644 index 0000000..6a5421e --- /dev/null +++ b/src/views/hive_views.js @@ -0,0 +1,302 @@ +"use strict"; +// Navegacion propia de la rama experimental para movil: topbar, barra inferior +// personalizable y lanzador de modulos. +// +// Vive fuera de main_views.js a proposito: upstream reescribe ese fichero en cada +// release (de 0.9.1 a 0.9.5 movio +256/-502 lineas) y esta interfaz se perdia en +// cada integracion. Aqui solo depende de i18n, que se inyecta porque main_views +// lo muta en cada peticion sin reasignarlo. + +const { a, button, div, form, h2, h3, img, input, label, nav, p, script, section, span } = require("../server/node_modules/hyperaxe"); +const { getConfig } = require("../configs/config-manager.js"); +const sharedState = require("../configs/shared-state"); +const { iconNode, iconKeyOf } = require("./mobile_icons"); + +// con pocos modulos activos (el workflow activistas) todo cabe en una pantalla: +// sobran el filtro Personal/Social y los accesos de usuario avanzado +const COMPACT_MAX = 16; + +module.exports = ({ i18n }) => { + // === Oasis UX (portado de 0.8.7_UX): topbar + bottombar + lanzador === + // topbar fija arriba: [logo] [Personal] [Community] [avatar] en una línea (sin título OASIS) + const renderMobileTopbar = () => { + // solo en movil: en escritorio manda la cabecera de upstream + if (process.env.OASIS_MOBILE !== '1') return ""; + // Personal/Social ya no van aqui: filtraban el lanzador, que ahora esta en la hoja + // del menu, y con muchos modulos (workflow activistas) no cabian a 360px y tapaban + // el logo. Queda el hueco para el boton de No molestar del telefono + return div( + { class: "oasis-mobile-topbar" }, + a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })), + div({ class: "omt-menus" }), + a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" })) + ); + }; + + const PINNABLE_MODULES = [ + { key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null }, + { key: 'inbox', href: '/inbox', glyph: '☂', labelKey: 'inbox', mod: null }, + { key: 'publish', href: '/publish', glyph: '❂', labelKey: 'publish', mod: null }, + { key: 'activity', href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, + { key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, + { key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, + { key: 'rooms', href: '/rooms', glyph: 'ꕢ', labelKey: 'roomsTitle', mod: 'roomsMod' }, + { key: 'wiki', href: '/wiki', glyph: 'ꖿ', labelKey: 'wikiTitle', mod: 'wikiMod' }, + { key: 'polls', href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' }, + { key: 'blogs', href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' }, + { key: 'data', href: '/data', glyph: 'ꘑ', labelKey: 'dataTitle', mod: 'dataMod' }, + { key: 'market', href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, + { key: 'events', href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, + { key: 'calendars', href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, + { key: 'banking', href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, + { key: 'forum', href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, + { key: 'videos', href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' }, + { key: 'images', href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, + { key: 'audios', href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, + { key: 'documents', href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, + { key: 'bookmarks', href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, + { key: 'maps', href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, + { key: 'trending', href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, + { key: 'jobs', href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, + { key: 'agenda', href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, + { key: 'wallet', href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, + { key: 'tasks', href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, + { key: 'torrents', href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, + { key: 'tags', href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, + { key: 'pads', href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, + { key: 'projects', href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, + { key: 'shops', href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, + { key: 'transfers', href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' }, + { key: 'feed', href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, + { key: 'pixelia', href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, + { key: 'melody', href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' }, + { key: 'games', href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, + { key: 'votes', href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, + { key: 'reports', href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' }, + { key: 'opinions', href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, + { key: 'parliament',href: '/parliament',glyph: 'ꗞ', labelKey: 'parliamentTitle',mod: 'parliamentMod' }, + { key: 'courts', href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' }, + { key: 'larp', href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, + { key: 'ai', href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, + { key: 'cipher', href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, + { key: 'fediverse', href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' }, + { key: 'inhabitants', href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, + { key: 'cv', href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null }, + { key: 'stats', href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null }, + { key: 'blockexplorer', href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null }, + ]; + + const renderBottomBar = () => { + if (process.env.OASIS_MOBILE !== '1') return ""; + const inboxN = sharedState.getInboxCount(); + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; + // la pestaña de la seccion abierta se marca (pastilla de color tras el icono) + let here = ''; + try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {} + const isHere = (href) => here === href || (href !== '/' && here.startsWith(href + '/')) || (href === '/activity' && here === '/'); + const bbLink = (href, ico, label, badge) => a( + { class: isHere(href) ? "bb-item current" : "bb-item", href, ...(isHere(href) ? { 'aria-current': 'page' } : {}) }, + iconNode("bb-ico", iconKeyOf(href), ico), + (badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "", + span({ class: "bb-lbl" }, label) + ); + // barra PERSONALIZABLE: solo los pins (max 4). Editarla y Peers estan en el + // lanzador (Herramientas): 7 botones abajo eran demasiados para un pulgar. + const MAX = 4; + const pins = (cfg.bottomBarPins || []) + .map(key => PINNABLE_MODULES.find(m => m.key === key)) + .filter(m => m && modOn(m.mod)) + .slice(0, MAX); + const slots = pins.map(m => bbLink( + m.href, m.glyph, i18n[m.labelKey] || m.key, m.key === 'inbox' ? inboxN : 0 + )); + // quinta pestaña fija: Menu abre desde abajo una hoja con todos los modulos (el + // lanzador). Sin JS: una casilla y CSS. autocomplete=off para que al volver atras + // el navegador no la deje abierta. Ver docs/ESTUDIO_NAVEGACION.md + const menuLabel = i18n.bbMenu || 'Menu'; + return nav( + { class: "oasis-bottombar", attrs: { "aria-label": i18n.bbQuickActions || "Quick actions" } }, + input({ type: 'checkbox', id: 'oasis-menu-toggle', class: 'menu-toggle', autocomplete: 'off', 'aria-label': menuLabel }), + ...slots, + label({ class: 'bb-item bb-menu', for: 'oasis-menu-toggle' }, + iconNode('bb-ico', 'modules', '▦'), + span({ class: 'bb-lbl' }, menuLabel) + ), + label({ class: 'menu-sheet-backdrop', for: 'oasis-menu-toggle', 'aria-hidden': 'true' }), + div({ class: 'menu-sheet', role: 'dialog', 'aria-label': menuLabel }, + div({ class: 'menu-sheet-head' }, + span({ class: 'menu-sheet-title' }, menuLabel), + label({ class: 'menu-sheet-close', for: 'oasis-menu-toggle', 'aria-label': i18n.bbMenuClose || 'Close' }, '✕') + ), + renderHiveNav() + ), + // atras cierra la hoja (sin este script el menu funciona igual) + script({ src: '/js/mobile_menu.js', defer: 'defer' }) + ); + }; + + // Lanzador de modulos de la home: secciones con una rejilla de iconos, como el + // cajon de apps del movil. Solo salen los modulos activos y las secciones que + // tienen alguno, asi que con un workflow corto (activistas) cabe en una pantalla. + const GROUPS = [ + { id: 'community', titleKey: 'menuNetwork', side: 'community', mods: [ + { href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, + { href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, + { href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, + { href: '/rooms', glyph: 'ꕢ', labelKey: 'roomsTitle', mod: 'roomsMod' }, + { href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, + { href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, + { href: '/wiki', glyph: 'ꖿ', labelKey: 'wikiTitle', mod: 'wikiMod' }, + { href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' }, + { href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, + { href: '/mentions', glyph: '✺', labelKey: 'mentions', mod: null, extra: true }, + { href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, + { href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, + { href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, + { href: '/graphos', glyph: '⌬', labelKey: 'modulesGraphosLabel', mod: 'graphosMod' }, + { href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null, extra: true }, + { href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' } ] }, + { id: 'media', titleKey: 'menuMedia', side: 'community', mods: [ + { href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, + { href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' }, + { href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, + { href: '/podcasts', glyph: '◉', labelKey: 'podcastsTitle', mod: 'podcastsMod' }, + { href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, + { href: '/files', glyph: 'ꖃ', labelKey: 'filesTitle', mod: 'filesMod' }, + { href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, + { href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' } ] }, + { id: 'creative', titleKey: 'menuCreative', side: 'community', mods: [ + { href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, + { href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, + { href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' } ] }, + { id: 'office', titleKey: 'menuOffice', side: 'personal', mods: [ + { href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, + { href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, + { href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, + { href: '/campaigns', glyph: '⚑', labelKey: 'campaignsTitle', mod: 'campaignsMod' }, + { href: '/mailing', glyph: '✉', labelKey: 'mailingTitle', mod: 'mailingMod' }, + { href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, + { href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' }, + { href: '/reports', glyph: 'ꕤ', labelKey: 'reportsTitle', mod: 'reportsMod' } ] }, + { id: 'governance', titleKey: 'menuGovernance', side: 'personal', mods: [ + { href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, + { href: '/emergencies', glyph: '✚', labelKey: 'emergenciesTitle', mod: 'emergenciesMod' }, + { href: '/school', glyph: 'ꗳ', labelKey: 'schoolTitle', mod: 'schoolMod' }, + { href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, + { href: '/parliament', glyph: 'ꗞ', labelKey: 'parliamentTitle', mod: 'parliamentMod' }, + { href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' } ] }, + { id: 'economy', titleKey: 'menuEconomy', side: 'personal', mods: [ + { href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, + { href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, + { href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, + { href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, + { href: '/housing', glyph: '⌂', labelKey: 'housingTitle', mod: 'housingMod' }, + { href: '/logistics', glyph: '⇄', labelKey: 'logisticsTitle', mod: 'logisticsMod' }, + { href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, + { href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' } ] }, + { id: 'personal', titleKey: 'menuPersonal', side: 'personal', mods: [ + { href: '/profile', glyph: '⚉', labelKey: 'profile', mod: null, extra: true }, + { href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, + { href: '/favorites', glyph: 'ꘝ', labelKey: 'favoritesTitle', mod: 'favoritesMod' }, + { href: '/phone', glyph: '☏', labelKey: 'phoneTitle', mod: 'phoneMod' }, + { href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null, extra: true }, + { href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, + { href: '/logs', glyph: 'ꗯ', labelKey: 'logsTitle', mod: 'logsMod' } ] }, + { id: 'tools', titleKey: 'menuTools', side: 'personal', mods: [ + { href: '/invites', glyph: 'ꔹ', labelKey: 'invites', mod: 'invitesMod' }, + { href: '/peers', glyph: '⧖', labelKey: 'peers', mod: null }, + { href: '/settings/bottombar', glyph: '✎', labelKey: 'bbYourBar', mod: null }, + { href: '/backup', glyph: 'ꖤ', labelKey: 'backupTitle', mod: 'backupMod' }, + { href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, + { href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, + { href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null, extra: true }, + { href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null, extra: true }, + { href: '/modules', glyph: 'ꗣ', labelKey: 'modules', mod: null, extra: true }, + { href: '/settings', glyph: '⚙', labelKey: 'settings', mod: null } ] }, + ]; + + const isCompact = () => { + const cfg = getConfig(); + const on = (m) => m && cfg.modules[m] === 'on'; + return GROUPS.reduce((n, g) => n + g.mods.filter(x => on(x.mod)).length, 0) <= COMPACT_MAX; + }; + + const renderHiveNav = () => { + if (process.env.OASIS_MOBILE !== '1') return ""; + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; + const compact = isCompact(); + // Personal/Social de la topbar (lo pone el middleware desde ?hive=) parte el lanzador en dos + const hiveFilter = compact ? "" : ((typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || ""); + const app = (m) => a({ class: 'ln-app', href: m.href }, + iconNode('ln-ico', iconKeyOf(m.href), m.glyph), + span({ class: 'ln-lbl' }, i18n[m.labelKey] || m.href.slice(1))); + const groups = GROUPS + .filter(g => !hiveFilter || g.side === hiveFilter) + .map(g => ({ g, mods: g.mods.filter(m => modOn(m.mod) && !(compact && m.extra)) })) + .filter(x => x.mods.length); + return nav({ class: 'launcher', 'aria-label': i18n.modules || 'Modules' }, + ...groups.map(({ g, mods }) => div({ class: 'ln-group' }, + h3({ class: 'ln-title' }, i18n[g.titleKey] || g.id), + div({ class: 'ln-grid' }, ...mods.map(app)) + )) + ); + }; + + + // pantalla para personalizar la barra inferior (/settings/bottombar) + const bottomBarPickerView = (template) => { + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; + const MAX = 4; + const pinnedKeys = cfg.bottomBarPins || []; + const pinned = pinnedKeys + .map(key => PINNABLE_MODULES.find(m => m.key === key)) + .filter(Boolean); + const full = pinned.length >= MAX; + const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls }, + input({ type: 'hidden', name: 'action', value: action }), + input({ type: 'hidden', name: 'key', value: key }), + node + ); + const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form', + button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' }, + iconNode('bb-manage-ico', iconKeyOf(m.href), m.glyph), + span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key), + span({ class: 'bb-manage-x' }, '✕') + ) + ); + const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form', + button({ type: 'submit', class: 'bb-pick-btn' }, + iconNode('bb-pick-ico', iconKeyOf(m.href), m.glyph), + span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key) + ) + ); + const addable = PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key)); + return template( + i18n.bbManage || 'Edit bar', + section( + div({ class: 'tags-header' }, + h2(i18n.bbPickTitle || 'Customize the bar'), + p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.') + ), + div({ class: 'bb-manage' }, + h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`), + pinned.length + ? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow)) + : p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None') + ), + div({ class: 'bb-manage-add' }, + h3(i18n.bbAdd || 'Add'), + full + ? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached') + : div({ class: 'bb-pick-grid' }, ...addable.map(addBtn)) + ), + a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done') + ) + ); + }; + + return { renderMobileTopbar, renderBottomBar, renderHiveNav, PINNABLE_MODULES, bottomBarPickerView, GROUPS }; +}; diff --git a/src/views/housing_view.js b/src/views/housing_view.js new file mode 100644 index 0000000..a0b5d45 --- /dev/null +++ b/src/views/housing_view.js @@ -0,0 +1,579 @@ +const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, video, table, tr, td, hr } = require("../server/node_modules/hyperaxe") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const { clearnetItemHref, template, i18n, renderOpenClosedChip, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderOpinionsVoting, renderEngagement, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") +const { blobUrl, blobIdOf, isVideoEntry, imagesOf, renderMediaThumb, renderPhotoGallery, renderGalleryFields } = require("./gallery_view") +const moment = require("../server/node_modules/moment") +const { config } = require("../server/SSB_server.js") +const { renderStyledText } = require("../backend/renderStyledText") +const opinionCategories = require("../backend/opinion_categories") +const { renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") + +const userId = config.keys.id + +const FILTERS = [ + { key: "ALL", i18n: "housingFilterAll" }, + { key: "RECENT", i18n: "housingFilterRecent" }, + { key: "MINE", i18n: "housingFilterMine" }, + { key: "TOP", i18n: "housingFilterTop" }, + { key: "REQUESTED", i18n: "housingFilterRequested" }, + { key: "SALE", i18n: "housingFilterSale" }, + { key: "RENT", i18n: "housingFilterRent" }, + { key: "COUCHSURFING", i18n: "housingFilterCouchsurfing" }, + { key: "OPEN", i18n: "housingFilterOpen" }, + { key: "CLOSED", i18n: "housingFilterClosed" } +] + +const MAX_IMAGES = 8 + +const safeArr = (v) => (Array.isArray(v) ? v : []) +const safeText = (v) => String(v || "").trim() +const clearnetEligible = (item) => String(item.status || "OPEN").toUpperCase() === "OPEN" && String(item.visibility || "PUBLIC").toUpperCase() === "PUBLIC" && !item.encrypted && !item.tribeId +const housingClearnetHref = (item) => clearnetItemHref("housing", item.title, item.rootId || item.id) +const HOUSING_REACH = ["PUBLIC", "HIDDEN"] +const housingReachLabel = (v) => v === "HIDDEN" ? (i18n.visibilityHidden || "Hidden") : (i18n.visibilityPublic || "Public") + +const parseNum = (v) => { + const n = parseFloat(String(v ?? "").replace(",", ".")) + return Number.isFinite(n) ? n : NaN +} + +const fmtPrice = (v) => { + const n = parseNum(v) + return Number.isFinite(n) ? n.toFixed(2) : String(v ?? "") +} + + + +const priceLabel = (item) => { + if (item.housing_type === "couchsurfing") return i18n.housingFree || "FREE" + const suffix = item.housing_type === "rent" ? ` ${i18n.housingPerMonth || "/month"}` : "" + return `${fmtPrice(item.price)} ECO${suffix}` +} + +const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((s, c) => s + (Number((opinions || {})[c]) || 0), 0) + +const renderStarRating = (opinions, voterCount) => { + const pos = sumCats(opinions, opinionCategories.positive) + const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation) + const total = pos + neg + const full = total > 0 ? Math.round((pos / total) * 5) : 0 + const stars = "★".repeat(full) + "☆".repeat(5 - full) + return span({ class: "housing-stars" }, `${stars} (${voterCount})`) +} + +const buildReturnTo = (filter, params = {}) => { + const parts = [`filter=${encodeURIComponent(safeText(filter || "ALL"))}`] + const q = safeText(params.search || "") + if (q) parts.push(`search=${encodeURIComponent(q)}`) + if (String(params.minPrice ?? "") !== "") parts.push(`minPrice=${encodeURIComponent(String(params.minPrice))}`) + if (String(params.maxPrice ?? "") !== "") parts.push(`maxPrice=${encodeURIComponent(String(params.maxPrice))}`) + if (safeText(params.place)) parts.push(`place=${encodeURIComponent(safeText(params.place))}`) + if (safeText(params.sort)) parts.push(`sort=${encodeURIComponent(safeText(params.sort))}`) + return `/housing?${parts.join("&")}` +} + +const renderTags = (tags = []) => { + const arr = safeArr(tags).map(t => String(t || "").trim()).filter(Boolean) + return arr.length + ? div({ class: "card-tags" }, arr.map(tag => a({ class: "tag-link", href: `/search?query=%23${encodeURIComponent(tag)}` }, `#${tag}`))) + : null +} + +const renderTypeChip = (item) => { + const t = String(item.housing_type || "").toUpperCase() + const emoji = item.housing_type === "sale" ? "🏷" : item.housing_type === "rent" ? "🔑" : "🛋" + return renderStateChip("whole", emoji, i18n["housingType" + t] || t) +} + +const renderStatusChip = (status) => { + const s = String(status || "").toUpperCase() + return renderOpenClosedChip(s, { + statusChipOPEN: i18n.housingStatusOPEN || "OPEN", + statusChipCLOSED: i18n.housingStatusCLOSED || "CLOSED" + }) +} + +const renderRequestedChip = () => renderStateChip("whole", "★", i18n.housingRequestedBadge || "REQUESTED") + +const renderInfoTable = (item) => { + const rows = [] + const pushRow = (labelText, valueNode, valueClass) => + rows.push(tr( + td({ class: "tribe-info-label" }, labelText), + td({ class: `tribe-info-value${valueClass ? " " + valueClass : ""}` }, valueNode) + )) + pushRow(i18n.housingProperty, i18n["housingProperty" + String(item.property_type || "").toUpperCase()] || "—") + pushRow(i18n.housingPrice, priceLabel(item), "card-salary") + if (item.rooms > 0) pushRow(i18n.housingRooms, String(item.rooms)) + if (item.size > 0) pushRow(i18n.housingSize, `${item.size} m²`) + if (item.capacity > 0) pushRow(i18n.housingCapacity, String(item.capacity)) + if (item.availableFrom) pushRow(i18n.housingAvailableFrom, moment(item.availableFrom).format("YYYY/MM/DD")) + if (item.availableTo) pushRow(i18n.housingAvailableTo, moment(item.availableTo).format("YYYY/MM/DD")) + if (safeText(item.place)) pushRow(i18n.housingPlace, safeText(item.place)) + if (item.mapUrl) { + const mapNode = renderMapLocationVisitLabel(item.mapUrl) + if (mapNode) pushRow(i18n.mapLocationTitle || "Map", mapNode) + } + return table({ class: "tribe-info-table housing-info-table" }, ...rows, ...renderCreatedRows(item.createdAt, item.author)) +} + +const today = () => moment().format("YYYY-MM-DD") + +const renderStatusRow = (item, returnTo) => { + if (String(item.author) !== String(userId)) return null + const isOpen = item.status === "OPEN" + return div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.housingStatus}: `), + renderStatusChip(item.status), + form({ method: "POST", action: `/housing/status/${encodeURIComponent(item.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "tribe-action-btn", type: "submit", name: "status", value: isOpen ? "CLOSED" : "OPEN" }, + isOpen ? (i18n.housingSetClosed || "Close") : (i18n.housingSetOpen || "Reopen")) + ) + ) +} + +const renderOwnerActions = (item, returnTo) => { + if (String(item.author) !== String(userId)) return [] + return [ + form({ method: "GET", action: `/housing/edit/${encodeURIComponent(item.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "update-btn", type: "submit" }, i18n.housingUpdateButton) + ) + ] +} + +const renderRequestToggle = (item, returnTo) => { + if (String(item.author) === String(userId)) return null + if (item.status !== "OPEN") return null + const requested = safeArr(item.requests).includes(userId) + return requested + ? form({ method: "POST", action: `/housing/cancel/${encodeURIComponent(item.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "filter-btn" }, i18n.housingCancelButton) + ) + : form({ method: "POST", action: `/housing/request/${encodeURIComponent(item.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "filter-btn" }, i18n.housingRequestButton) + ) +} + +const renderHousingList = (items, filter, params = {}) => { + const list = safeArr(items) + if (!list.length) return p(i18n.housingNoItems) + const returnTo = buildReturnTo(filter, params) + + return div({ class: "housing-grid" }, + list.map((item) => { + const clip = safeText(item.video) + const cover = imagesOf(item)[0] + const isOwn = item.author && String(item.author) === String(userId) + const requested = safeArr(item.requests).includes(userId) + const chips = [ + item.visibility === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, + renderTypeChip(item), + renderStatusChip(item.status), + requested ? renderRequestedChip() : null, + renderLifespanChip(item.lifetime, i18n), + item.clearnet === true && clearnetEligible(item) ? renderReachChip(true, i18n, housingClearnetHref(item)) : null + ].filter(Boolean) + + return div({ class: "trending-card housing-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(item.id, `/housing/${encodeURIComponent(item.id)}`, { spread: params.spreadMap && params.spreadMap.get(item.id) || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isOwn ? contentDeleteAction("housing", item.id) : null }) + ), + div({ class: "card-section housing-card-body" }, + clip || (cover && isVideoEntry(cover)) + ? div({ class: "tribe-card-image-wrapper housing-card-video" }, + video({ controls: true, class: "housing-card-hero-video", src: blobUrl(blobIdOf(clip || cover)), preload: 'metadata' }) + ) + : cover + ? div({ class: "tribe-card-image-wrapper" }, + a({ href: `/housing/${encodeURIComponent(item.id)}` }, + img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-card-hero-image", alt: "" }) + ) + ) + : null, + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, + a({ href: `/housing/${encodeURIComponent(item.id)}` }, safeText(item.title) || i18n.housingTitle) + ) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderStarRating(item.opinions, safeArr(item.opinions_inhabitants).length), + div({ class: "price-chip" }, priceLabel(item)), + safeText(item.place) + ? div({ class: "card-field card-field-mb" }, + span({ class: "card-label" }, `${String(i18n.housingPlace).toUpperCase()}: `), + span({ class: "card-value" }, safeText(item.place)) + ) + : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`) + ), + imagesOf(item).length > 1 + ? div({ class: "card-field" }, + span({ class: "card-label" }, `${i18n.galleryPhotos}: `), + span({ class: "card-value" }, String(imagesOf(item).length)) + ) + : null + ) + ) + ) + }) + ) +} + +const renderHousingForm = (item = {}, mode = "create", maxImages = MAX_IMAGES, spreadWarning = null, params = {}) => { + const isEdit = mode === "edit" + const type = String(item.housing_type || "").toLowerCase() + const reachRaw = String(params.reach || "").toUpperCase() + const ownReach = String(item.visibility || "").toUpperCase() + const reach = HOUSING_REACH.includes(reachRaw) ? reachRaw : (HOUSING_REACH.includes(ownReach) ? ownReach : "PUBLIC") + const showClearnet = reach === "PUBLIC" && clearnetEligible({ ...item, visibility: reach }) + return div({ class: "div-center housing-form" }, + isEdit ? spreadWarning : null, + form({ method: "GET", action: isEdit ? `/housing/edit/${encodeURIComponent(item.id)}` : "/housing" }, + isEdit ? null : input({ type: "hidden", name: "filter", value: "CREATE" }), + label(i18n.visibilityLabel || "Visibility"), + br(), + div({ class: "apply-row" }, + select({ name: "visibility", class: "report-category-select" }, + option({ value: "PUBLIC", ...(reach === "PUBLIC" ? { selected: true } : {}) }, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(reach === "HIDDEN" ? { selected: true } : {}) }, i18n.visibilityHidden || "Hidden") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, housingReachLabel(reach)), + form({ + action: isEdit ? `/housing/update/${encodeURIComponent(item.id)}` : "/housing/create", + method: "POST", + enctype: "multipart/form-data" + }, + input({ type: "hidden", name: "returnTo", value: "/housing?filter=MINE" }), + input({ type: "hidden", name: "visibility", value: reach }), + label(i18n.housingType), + br(), + select({ name: "housing_type", required: true }, + option({ value: "sale", selected: type === "sale" ? "selected" : undefined }, i18n.housingTypeSALE), + option({ value: "rent", selected: type === "rent" ? "selected" : undefined }, i18n.housingTypeRENT), + option({ value: "couchsurfing", selected: type === "couchsurfing" ? "selected" : undefined }, i18n.housingTypeCOUCHSURFING) + ), + br(), + br(), + label(i18n.housingProperty), + br(), + select({ name: "property_type" }, + option({ value: "apartment", ...(item.property_type === "apartment" ? { selected: true } : {})}, i18n.housingPropertyAPARTMENT), + option({ value: "house", ...(item.property_type === "house" ? { selected: true } : {})}, i18n.housingPropertyHOUSE), + option({ value: "room", ...(item.property_type === "room" ? { selected: true } : {})}, i18n.housingPropertyROOM), + option({ value: "land", ...(item.property_type === "land" ? { selected: true } : {})}, i18n.housingPropertyLAND), + option({ value: "other", ...(item.property_type === "other" ? { selected: true } : {})}, i18n.housingPropertyOTHER) + ), + br(), + br(), + label(i18n.housingTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.housingTitlePlaceholder, value: item.title || "" }), + br(), + br(), + ...renderGalleryFields(item, isEdit, maxImages), + label(i18n.housingDescription), + br(), + textarea({ maxlength: "5000", name: "description", rows: "6", required: true, placeholder: i18n.housingDescriptionPlaceholder }, item.description || ""), + br(), + br(), + label(i18n.housingRules), + br(), + textarea({ maxlength: "5000", name: "rules", rows: "4", placeholder: i18n.housingRulesPlaceholder }, item.rules || ""), + br(), + br(), + label(i18n.housingPlace), + br(), + input({ type: "text", name: "place", placeholder: i18n.housingPlacePlaceholder, value: item.place || "" }), + br(), + br(), + label(i18n.mapLocationTitle || "Map Location"), + br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: item.mapUrl || "" }), + br(), + br(), + label(i18n.housingPriceLabel), + br(), + input({ type: "number", name: "price", step: "0.01", min: "0", value: item.price || "" }), + renderEcoValueChip(), + br(), + br(), + label(i18n.housingRooms), + br(), + input({ type: "number", name: "rooms", min: "0", value: item.rooms || "" }), + br(), + br(), + label(i18n.housingSizeLabel), + br(), + input({ type: "number", name: "size", step: "0.01", min: "0", value: item.size || "" }), + br(), + br(), + label(i18n.housingCapacity), + br(), + input({ type: "number", name: "capacity", min: "0", value: item.capacity || "" }), + br(), + br(), + label(i18n.housingAvailableFrom), + br(), + input({ type: "date", name: "availableFrom", required: true, min: today(), value: item.availableFrom ? String(item.availableFrom).slice(0, 10) : "" }), + br(), + br(), + label(i18n.housingAvailableTo), + br(), + input({ type: "date", name: "availableTo", min: item.availableFrom ? String(item.availableFrom).slice(0, 10) : today(), value: item.availableTo ? String(item.availableTo).slice(0, 10) : "" }), + br(), + br(), + label(i18n.housingTags), + br(), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: Array.isArray(item.tags) ? item.tags.join(", ") : (item.tags || "") }), + br(), + br(), + ...(showClearnet ? [renderClearnetSelector(item.clearnet === true || String(item.clearnet || "") === "1", i18n), br()] : []), + button({ type: "submit" }, isEdit ? i18n.housingUpdateButton : i18n.housingCreateButton) + ) + ) +} + +const housingChip = (censusH, filter, x) => { + const m = x.key + if (m === filter) return true + if (m === "TOP") return censusH.length > 0 + if (m === "MINE") return censusH.some(h => String(h.author) === String(userId)) + if (m === "RECENT") return censusH.length > 0 + if (m === "REQUESTED") return censusH.some(h => safeArr(h.requests).includes(userId)) + if (m === "SALE" || m === "RENT" || m === "COUCHSURFING") return censusH.some(h => String(h.housing_type || "").toUpperCase() === m) + if (m === "OPEN" || m === "CLOSED") return censusH.some(h => String(h.status || "OPEN").toUpperCase() === m) + return true +} + +const renderFiltersBar = (filter, params = {}, emptyMod = false, censusH = null) => + div({ class: "filters" }, + form({ method: "GET", action: "/housing", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "search", value: safeText(params.search || "") }), + input({ type: "hidden", name: "minPrice", value: String(params.minPrice ?? "") }), + input({ type: "hidden", name: "maxPrice", value: String(params.maxPrice ?? "") }), + input({ type: "hidden", name: "place", value: safeText(params.place || "") }), + input({ type: "hidden", name: "sort", value: safeText(params.sort || "") }), + ...(emptyMod ? [] : (censusH ? FILTERS.filter(x => housingChip(censusH, filter, x)) : FILTERS).map(f => + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + )), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.housingCreateButton) + ) + ) + +exports.housingView = async (items, filter = "ALL", params = {}) => { + const search = safeText(params.search || "") + const minPrice = params.minPrice ?? "" + const maxPrice = params.maxPrice ?? "" + const place = safeText(params.place || "") + const sort = safeText(params.sort || "recent") + + const isForm = filter === "CREATE" || filter === "EDIT" + const emptyMod = moduleIsEmpty(Array.isArray(items) ? items : [], filter, "ALL", search || place || String(minPrice || "") || String(maxPrice || "")) + + return template( + i18n.housingTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.housingTitle), + p(i18n.housingDescriptionText) + ), + renderFiltersBar(filter, { search, minPrice, maxPrice, place, sort }, emptyMod, Array.isArray(params.censusList) ? params.censusList : (Array.isArray(items) ? items : [])) + ), + section( + isForm + ? renderHousingForm(filter === "EDIT" ? (Array.isArray(items) ? items[0] : items) || {} : (params.draft || {}), filter === "EDIT" ? "edit" : "create", Number(params.maxImages) > 0 ? Number(params.maxImages) : MAX_IMAGES, await renderSpreadEditWarning(filter === "EDIT" ? ((Array.isArray(items) ? items[0] : items) || {}).id : null), params) + : section( + emptyMod ? null : div({ class: "housing-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(items, it => String(it.status || '').toUpperCase(), [{ value: 'OPEN', label: i18n.housingFilterOpen }, { value: 'CLOSED', label: i18n.housingFilterClosed }]), + form({ method: "GET", action: "/housing", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter || "ALL" }), + input({ type: "text", name: "search", value: search, placeholder: i18n.housingSearchPlaceholder, class: "filter-box__input" }), + input({ type: "text", name: "place", value: place, placeholder: i18n.housingPlacePlaceholder, class: "filter-box__input housing-place-input" }), + input({ type: "number", name: "minPrice", step: "0.01", min: "0", value: String(minPrice ?? ""), placeholder: i18n.housingMinPrice, class: "filter-box__number transfer-amount-input" }), + input({ type: "number", name: "maxPrice", step: "0.01", min: "0", value: String(maxPrice ?? ""), placeholder: i18n.housingMaxPrice, class: "filter-box__number transfer-amount-input" }), + select({ name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.housingSortRecent), + option({ value: "price", ...(sort === "price" ? { selected: true } : {})}, i18n.housingSortPrice), + option({ value: "requests", ...(sort === "requests" ? { selected: true } : {})}, i18n.housingSortRequests), + option({ value: "rating", ...(sort === "rating" ? { selected: true } : {})}, i18n.housingSortRating) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.housingSearchButton) + ) + ), + div({ class: "housing-list" }, renderHousingList(paged(items), filter, params)) + ) + ) + ) +} + +const renderCommentsSection = (itemId, returnTo, comments = []) => { + return renderSharedCommentsSection({ + action: `/housing/${encodeURIComponent(itemId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; + +exports.singleHousingView = async (item, filter = "ALL", comments = [], params = {}) => { + const returnTo = safeText(params.returnTo) || buildReturnTo(filter, params) + const isAuthor = String(item.author) === String(userId) + const requested = safeArr(item.requests).includes(userId) + const voters = safeArr(item.opinions_inhabitants) + const canClearnet = clearnetEligible(item) + const isClearnet = canClearnet && !!item.clearnet + + const chips = [ + item.visibility === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, + renderTypeChip(item), + renderStatusChip(item.status), + requested ? renderRequestedChip() : null, + renderLifespanChip(item.lifetime, i18n), + renderEcoTax(item.msgSize, item.id), + renderReachChip(isClearnet, i18n, isClearnet ? housingClearnetHref(item) : null), + isAuthor && canClearnet ? renderClearnetSwitch("housing", item.rootId || item.id, isClearnet) : null + ].filter(Boolean) + + const sideActions = [] + const requestToggle = renderRequestToggle(item, returnTo) + if (requestToggle) sideActions.push(requestToggle) + const ownerActions = renderOwnerActions(item, returnTo) + + const nextVisibility = item.visibility === "PUBLIC" ? "HIDDEN" : "PUBLIC" + const isHidden = item.visibility === "HIDDEN" + const visibilityRow = isAuthor + ? div({ class: "tribe-side-actions shop-visibility-row housing-visibility-row" }, + span({ class: "card-label" }, `${i18n.visibilityLabel || "Visibility"}: `), + isHidden + ? renderStateChip("encrypted", "🔒", i18n.encryptedChipLabel || "E2E") + : renderStateChip("mutuals", "👁", i18n.visibilityPublic || "PUBLIC"), + form({ method: "POST", action: `/housing/visibility/${encodeURIComponent(item.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "visibility", value: nextVisibility }), + button({ type: "submit", class: "tribe-action-btn" }, + nextVisibility === "PUBLIC" ? (i18n.visibilityMakePublic || "Make public") : (i18n.visibilityMakeHidden || "Make hidden")) + ) + ) + : null + + const cover = imagesOf(item)[0] + const housingSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(item.id, `/housing/${encodeURIComponent(item.id)}`, { spread: params.spreads || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isAuthor ? contentDeleteAction("housing", item.id) : null }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, safeText(item.title) || i18n.housingTitle) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderStarRating(item.opinions, voters.length), + cover && !isVideoEntry(cover) ? img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-detail-image", alt: "" }) : null, + renderInfoTable(item), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`) + ), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + renderStatusRow(item, returnTo), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + visibilityRow, + renderTags(item.tags) + ) + + const renderSection = (titleText, bodyText) => + safeText(bodyText) + ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, titleText), + p({ class: "tribe-side-description" }, ...renderStyledText(bodyText)) + ) + : null + + const housingMain = div({ class: "tribe-main" }, + renderPhotoGallery(item), + renderSection(i18n.housingDescription, item.description), + renderSection(i18n.housingRules, item.rules), + item.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, item.mapUrl)) : null, + renderEngagement(item.id, + !isAuthor && item.everRequestedByViewer + ? renderOpinionsVoting("/housing/opinions", item.id, item.opinions, returnTo, voters) + : null, + renderCommentsSection(item.id, returnTo, comments) + ) + ) + + return template( + i18n.housingTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.housingTitle), p(i18n.housingDescriptionText)), + renderFiltersBar(filter, params), + div({ class: "tribe-details" }, housingSide, housingMain) + ) + ) +} + +exports.clearnetHousingView = async (item, params = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, blobUrl: cnBlob, renderClearnetPage } = require("./clearnet_view") + const title = esc(item.title || i18n.housingTitle) + const point = params.mapPoint || null + const media = imagesOf(item).map(entry => ({ src: cnBlob(entry), video: isVideoEntry(entry) })).filter(m => m.src) + const clip = cnBlob(item.video) + if (clip && !media.some(m => m.src === clip)) media.push({ src: clip, video: true }) + const cover = (media.find(m => !m.video) || {}).src || null + const day = (v) => moment(v).format("YYYY/MM/DD") + const rows = [ + [i18n.housingProperty, i18n["housingProperty" + String(item.property_type || "").toUpperCase()] || ""], + [i18n.housingRooms, item.rooms > 0 ? String(item.rooms) : ""], + [i18n.housingSize, item.size > 0 ? `${item.size} m²` : ""], + [i18n.housingCapacity, item.capacity > 0 ? String(item.capacity) : ""], + [i18n.housingAvailableFrom, item.availableFrom ? day(item.availableFrom) : ""], + [i18n.housingAvailableTo, item.availableTo ? day(item.availableTo) : ""] + ].filter(([, v]) => v) + const textSection = (heading, text) => safeText(text) ? `

${esc(heading)}

${renderRichText(text)}
` : "" + const extraCss = ` +.cn-housing-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} +.cn-housing-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} +.cn-housing-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} +.cn-housing-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;margin:0 0 20px 0} +.cn-housing-gallery img,.cn-housing-gallery video{width:100%;height:auto;display:block;border:1px solid var(--border);border-radius:6px;background:#000} +.cn-housing-info{border-collapse:collapse;margin:0 0 20px 0} +.cn-housing-info td{padding:6px 14px 6px 0;font-size:14px;color:var(--fg-soft);border-bottom:1px solid var(--border)} +.cn-housing-info td:first-child{color:var(--fg-dim);text-transform:uppercase;letter-spacing:1px;font-size:12px} +.cn-housing-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:24px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)} +.cn-housing-text{color:var(--fg-soft);line-height:1.6;font-size:15px;word-break:break-word} +` + const body = ` +

${title}

+
+ ${renderKindTag("housing")} + ${esc(i18n["housingType" + String(item.housing_type || "").toUpperCase()] || item.housing_type || "")} + ${safeText(item.place) ? `📍 ${esc(item.place)}` : ""} + ${point ? `⌖ ${point.title ? `${esc(point.title)} · ` : ""}${point.lat}, ${point.lng}` : ""} + ${item.createdAt ? `📅 ${esc(day(item.createdAt))}` : ""} + ${esc(priceLabel(item))} +
+
+ ${media.length ? `` : ""} + ${rows.length ? `${rows.map(([k, v]) => ``).join("")}
${esc(k)}${esc(v)}
` : ""} + ${textSection(i18n.housingDescription, item.description)} + ${textSection(i18n.housingRules, item.rules)} + ${renderTagChips(item.tags)} +` + return renderClearnetPage({ + title: `${item.title || i18n.housingTitle} | Oasis`, + ogTitle: item.title || i18n.housingTitle, + ogDescription: item.description || "", + ogImage: cover, + extraCss, + body, + hubFeedId: item.author || null + }) +} diff --git a/src/views/identities_view.js b/src/views/identities_view.js new file mode 100644 index 0000000..956b54f --- /dev/null +++ b/src/views/identities_view.js @@ -0,0 +1,74 @@ +/** + * Selector de identidades para los ajustes. + * + * Vive aparte de settings_view.js para que las reescrituras de upstream sobre ese + * fichero no arrastren esto: alli solo hay una linea que llama a renderIdentities(). + * + * No cambia de identidad en caliente. Anota cual sera la del proximo arranque y lo + * dice; el porque esta en src/backend/accounts.js. + */ + +module.exports = ({ i18n, html }) => { + const { form, button, div, h2, p, section, input, span, ul, li, a } = html; + const accounts = require('../backend/accounts.js'); + + const renderIdentities = (activeAccount) => { + const names = accounts.list(); + const feeds = new Map(names.map((n) => [n, accounts.feedIdOf(n)])); + + // dos carpetas con el mismo feed son la misma identidad copiada: publicar desde + // las dos parte el registro y no tiene arreglo, asi que se avisa en la lista + const seen = new Map(); + for (const [n, id] of feeds) { + if (!id) continue; + seen.set(id, (seen.get(id) || 0) + 1); + } + + const row = (name) => { + const id = feeds.get(name); + const isActive = name === activeAccount; + const duplicated = id && seen.get(id) > 1; + return li({ class: "identity-row" }, + div({ class: "identity-info" }, + span({ class: "identity-name" }, name), + span({ class: "identity-feed" }, id ? `${id.slice(0, 18)}…` : "—"), + duplicated ? span({ class: "identity-warn" }, i18n.identitiesSameFeed) : null + ), + isActive + ? span({ class: "identity-active" }, i18n.identitiesActive) + : form({ method: "POST", action: "/settings/identity/use" }, + input({ type: "hidden", name: "account", value: name }), + button({ type: "submit" }, i18n.identitiesUse) + ) + ); + }; + + // en movil el backend lo mantiene vivo un servicio en primer plano, asi que + // cerrar la app no basta para reiniciarlo: hace falta pedirlo + const restart = process.env.OASIS_MOBILE === '1' + ? form({ method: "POST", action: "/settings/identity/restart" }, + button({ type: "submit" }, i18n.identitiesRestart)) + : null; + + return section({ class: "identities" }, + div({ class: "tags-header" }, + h2(i18n.identitiesTitle), + p(i18n.identitiesDescription) + ), + ul({ class: "identity-list" }, names.map(row)), + form({ method: "POST", action: "/settings/identity/create", class: "identity-create" }, + input({ type: "text", name: "account", placeholder: i18n.identitiesNewName, + maxlength: "32", autocomplete: "off" }), + button({ type: "submit" }, i18n.identitiesCreate) + ), + div({ class: "identity-actions" }, + restart, + // sacar o meter una identidad ya existente es cosa del modulo legacy, que + // es el que sabe cifrar el secret con una contraseña + a({ href: "/legacy", class: "identity-legacy" }, i18n.identitiesBackup) + ) + ); + }; + + return { renderIdentities }; +}; diff --git a/src/views/image_view.js b/src/views/image_view.js index 36e74e8..9547b7f 100644 --- a/src/views/image_view.js +++ b/src/views/image_view.js @@ -1,11 +1,14 @@ const { form, button, div, h2, p, section, input, label, br, a, img, span, textarea, select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions } = require("./main_views"); +const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl") -const { renderMapLocationVisitLabel } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText") +const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); +const { renderZoomableImage } = require("./gallery_view"); const userId = config.keys.id; @@ -22,18 +25,6 @@ const buildReturnTo = (filter, params = {}) => { return `/images?${parts.join("&")}`; }; -const renderPMButton = (recipient, className = "filter-btn") => { - const r = safeText(recipient); - if (!r) return null; - if (String(r) === String(userId)) return null; - - return form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: r }), - button({ type: "submit", class: className }, i18n.privateMessage) - ); -}; - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -44,35 +35,13 @@ const renderTags = (tags) => { : null; }; -const renderImageFavoriteToggle = (imgObj, returnTo = "") => - form( - { - method: "POST", - action: imgObj.isFavorite - ? `/images/favorites/remove/${encodeURIComponent(imgObj.key)}` - : `/images/favorites/add/${encodeURIComponent(imgObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - imgObj.isFavorite ? i18n.imageRemoveFavoriteButton : i18n.imageAddFavoriteButton - ) - ); - const renderImageMedia = (imgObj, filter, params = {}) => { - const src = imgObj?.url ? `/blob/${encodeURIComponent(imgObj.url)}` : ""; + const src = imgObj?.url ? blobUrlOf(imgObj.url) : ""; return imgObj?.url ? div( - { class: "image-container", style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" }, - a( - { - href: `/images/${encodeURIComponent(imgObj.key)}?filter=${encodeURIComponent(filter || "all")}${ - params.q ? `&q=${encodeURIComponent(params.q)}` : "" - }${params.sort ? `&sort=${encodeURIComponent(params.sort)}` : ""}` - }, - img({ src, alt: imgObj.title || "", class: "media-preview", loading: "lazy" }) - ) + { class: "image-container image-container-row" }, + renderZoomableImage(src, { alt: imgObj.title || "", imgClass: "post-image" }) ) : p(i18n.imageNoFile); }; @@ -82,27 +51,15 @@ const renderImageOwnerActions = (filter, imgObj, params = {}) => { const isAuthor = String(imgObj.author) === String(userId); const hasOpinions = Object.keys(imgObj.opinions || {}).length > 0; - if (!isAuthor) return []; + if (!isAuthor || hasOpinions) return []; - const items = []; - if (!hasOpinions) { - items.push( - form( - { method: "GET", action: `/images/edit/${encodeURIComponent(imgObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.imageUpdateButton) - ) - ); - } - items.push( + return [ form( - { method: "POST", action: `/images/delete/${encodeURIComponent(imgObj.key)}` }, + { method: "GET", action: `/images/edit/${encodeURIComponent(imgObj.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.imageDeleteButton) + button({ class: "update-btn", type: "submit" }, i18n.imageUpdateButton) ) - ); - - return items; + ]; }; const renderImageList = exports.renderImageList = (images, filter, params = {}) => { @@ -111,58 +68,36 @@ const renderImageList = exports.renderImageList = (images, filter, params = {}) ? images.map((imgObj) => { const commentCount = typeof imgObj.commentCount === "number" ? imgObj.commentCount : 0; const title = safeText(imgObj.title); - const ownerActions = renderImageOwnerActions(filter, imgObj, params); const isOwn = imgObj.author && String(imgObj.author) === String(userId); + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(imgObj) + : imgObj.tribeOrigin + ? renderContentActions(null, imgObj.tribeOrigin.href) + : renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads || null, author: imgObj.author, favKind: 'images', torrentFrom: { blobId: imgObj.url, name: imgObj.title }, isFavorite: imgObj.isFavorite, reportTitle: imgObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction("image", imgObj.key) : null }); + const titleHref = typeof params.headerActions === "function" ? null : imgObj.tribeOrigin ? imgObj.tribeOrigin.href : `/images/${encodeURIComponent(imgObj.key)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(imgObj) + : imgObj.tribeOrigin ? null : renderEngagement(imgObj.key, + renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants), + renderCommentsLink({ href: `/images/${encodeURIComponent(imgObj.key)}`, count: commentCount }) + ); return div( { class: "trending-card image-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`) + headerActions ), div( { class: "card-section image-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderImageFavoriteToggle(imgObj, returnTo), - renderPMButton(imgObj.author) - ), - ownerActions.length ? div({ class: "bookmark-actions" }, ...ownerActions) : null - ), - title ? h2(title) : null, - (imgObj.meme || imgObj.lifetime) ? div({ class: "card-chips-row" }, - imgObj.meme ? a({ href: "/images?filter=meme", class: "chip-link" }, renderStateChip("mutuals", null, i18n.imageFilterMeme || "MEME")) : null, + div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, imgObj.tribeOrigin ? renderTribeOriginChip(imgObj.tribeOrigin) : imgObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('images', imgObj.title, imgObj.key)) : null, renderLicenseChip(imgObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(imgObj) : [])), + imgObj.lifetime ? div({ class: "card-chips-row" }, imgObj.lifetime ? renderLifespanChip(imgObj.lifetime, i18n) : null ) : null, renderImageMedia(imgObj, filter, params), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/images/${encodeURIComponent(imgObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(imgObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(imgObj.key, (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads)), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(imgObj) : []), + engagement, renderMapLocationVisitLabel(imgObj.mapUrl), br(), (() => { @@ -172,12 +107,12 @@ const renderImageList = exports.renderImageList = (images, filter, params = {}) return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(imgObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -195,6 +130,7 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => { return div( { class: "div-center image-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/images/update/${encodeURIComponent(imageId)}` : "/images/create", @@ -208,35 +144,27 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => { br(), br(), imageToEdit?.url - ? img({ src: `/blob/${encodeURIComponent(imageToEdit.url)}`, class: "media-preview", alt: imageToEdit?.title || "" }) + ? img({ loading: 'lazy', src: blobUrlOf(imageToEdit.url), class: "media-preview", alt: imageToEdit?.title || "" }) : null, label(i18n.imageTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || "" }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || "" }), br(), label(i18n.imageDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.imageDescriptionPlaceholder, rows: "4" }, imageToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.imageDescriptionPlaceholder, rows: "4" }, imageToEdit?.description || ""), br(), label(i18n.mapLocationTitle || "Map Location"), br(), input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: imageToEdit?.mapUrl || "" }), br(), - input({ type: "hidden", name: "meme", value: "0" }), label(i18n.imageTagsLabel), br(), input({ type: "text", name: "tags", placeholder: i18n.imageTagsPlaceholder, value: tagsValue }), br(), - label(i18n.imageMemeLabel), - input({ - id: "meme-checkbox", - type: "checkbox", - name: "meme", - value: "1", - class: "meme-checkbox", - ...(imageToEdit?.meme ? { checked: true } : {}) - }), + ...renderLicenseSelect(imageToEdit?.license, i18n), br(), + renderClearnetSelector(filter === "edit" ? !!imageToEdit?.clearnet : false, i18n), br(), button({ type: "submit" }, filter === "edit" ? i18n.imageUpdateButton : i18n.imageCreateButton) ) @@ -251,7 +179,7 @@ const renderGallery = (images) => { images.map((imgObj) => { const src = imgObj.url ? `/image/256/${encodeURIComponent(imgObj.url)}` : ""; return a( - { href: `#img-${encodeURIComponent(imgObj.key)}`, class: "gallery-item" }, + { href: `#img-${encodeURIComponent(imgObj.key)}`, id: `img-${encodeURIComponent(imgObj.key)}-src`, class: "gallery-item" }, img({ src, alt: imgObj.title || "", class: "gallery-image", loading: "lazy" }) ); }) @@ -260,136 +188,73 @@ const renderGallery = (images) => { const renderLightbox = (images) => images.map((imgObj) => { - const src = imgObj.url ? `/blob/${encodeURIComponent(imgObj.url)}` : ""; + const src = imgObj.url ? blobUrlOf(imgObj.url) : ""; return div( { id: `img-${encodeURIComponent(imgObj.key)}`, class: "lightbox" }, - a({ href: "#", class: "lightbox-close" }, "×"), + a({ href: `#img-${encodeURIComponent(imgObj.key)}-src`, class: "lightbox-close" }, "×"), img({ src, class: "lightbox-image", alt: imgObj.title || "" }) ); }); const renderImageCommentsSection = (imageKey, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/images/${encodeURIComponent(imageKey)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; +}; - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/images/${encodeURIComponent(imageKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c?.value?.content || {}; - const text = content.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "gallery") return censusM.length > 0; + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; }; exports.imageView = async (images, filter = "all", imageId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.imageMineSectionTitle - : filter === "create" - ? i18n.imageCreateSectionTitle - : filter === "edit" - ? i18n.imageUpdateSectionTitle - : filter === "gallery" - ? i18n.imageGallerySectionTitle - : filter === "meme" - ? i18n.imageMemeSectionTitle - : filter === "recent" - ? i18n.imageRecentSectionTitle - : filter === "top" - ? i18n.imageTopSectionTitle - : filter === "favorites" - ? i18n.imageFavoritesSectionTitle - : i18n.imageAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(imageId) }; + const title = i18n.imageTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(images); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const imageToEdit = imageId ? list.find((im) => im.key === imageId) : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.imageDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetImages); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterTop), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.imageFilterFavorites - ), - button( + String(i18n.imageFilterFavorites).toUpperCase() + )] : []), + + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterTop).toUpperCase())] : []), + ...(mediaChip("gallery") ? [button( { type: "submit", name: "filter", value: "gallery", class: filter === "gallery" ? "filter-btn active" : "filter-btn" }, - i18n.imageFilterGallery - ), - button({ type: "submit", name: "filter", value: "meme", class: filter === "meme" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMeme), + String(i18n.imageFilterGallery).toUpperCase() + )] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton) ) ) @@ -398,8 +263,9 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {}) filter === "create" || filter === "edit" ? renderImageForm(filter, imageId, imageToEdit, { ...params, filter }) : section( - div( - { class: "images-search" }, + emptyMod ? null : div( + { class: "images-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/images", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -414,70 +280,77 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {}) { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.imageSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.imageSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.imageSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.imageSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.imageSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.imageSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.imageSearchButton) ) ) ), - filter === "gallery" ? renderGallery(list) : div({ class: "images-list" }, renderImageList(list, filter, { q, sort })) + filter === "gallery" ? renderGallery(paged(list)) : div({ class: "images-list" }, renderImageList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ), - ...(filter === "gallery" ? renderLightbox(list) : []) + ...(filter === "gallery" ? renderLightbox(paged(list)) : []) ); }; exports.singleImageView = async (imageObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(imageObj.title); - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetImages); + const isAuthor = String(imageObj.author) === String(userId); + const isClearnet = !!imageObj.clearnet; - const memeChip = imageObj.meme - ? a({ href: "/images?filter=meme", class: "chip-link" }, renderStateChip("mutuals", null, i18n.imageFilterMeme || "MEME")) - : null; const chips = [ - memeChip, renderLifespanChip(imageObj.lifetime, i18n), imageObj.sizeBytes ? renderEcoTax(imageObj.sizeBytes, imageObj.key) : null ].filter(Boolean); const ownerActions = renderImageOwnerActions(filter, imageObj, { q, sort }); const sideActions = []; - sideActions.push(renderImageFavoriteToggle(imageObj, returnTo)); - if (imageObj.author && String(imageObj.author) !== String(userId)) { - sideActions.push(renderPMButton(imageObj.author)); - } for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(imageObj.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(imageObj.key, `/images/${encodeURIComponent(imageObj.key)}`, { + author: imageObj.author, + deleteAction: String(imageObj.author) === String(userId) ? contentDeleteAction("image", imageObj.key) : null, + favKind: 'images', torrentFrom: { blobId: imageObj.url, name: imageObj.title }, + isFavorite: imageObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: imageObj.title + }) + ); + const imageSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('images', imageObj.title, imageObj.key) : null), + isAuthor ? renderClearnetSwitch('images', imageObj.rootId || imageObj.key, isClearnet) : null, + renderLicenseChip(imageObj.license) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(imageObj.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(imageObj.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(imageObj.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(imageObj.key, params.spreads)), - renderMapLocationVisitLabel(imageObj.mapUrl), + renderMapEmbed(params.mapData, imageObj.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const imageMain = div({ class: "tribe-main" }, + detailActions, imageObj?.url ? div( { class: "image-container" }, img({ - src: `/blob/${encodeURIComponent(imageObj.url)}`, + src: blobUrlOf(imageObj.url), alt: imageObj.title || "", class: "media-preview", loading: "lazy" @@ -491,24 +364,26 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(imageObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants), - renderImageCommentsSection(imageObj.key, comments, returnTo) + renderEngagement(imageObj.key, + renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants), + renderImageCommentsSection(imageObj.key, comments, returnTo) + ) ); return template( i18n.imageTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.imageAllSectionTitle || i18n.imageTitle), p(i18n.imageDescription) ), @@ -516,21 +391,19 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params { class: "filters" }, form( { method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterTop), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase())] : []), { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.imageFilterFavorites - ), - button( + String(i18n.imageFilterFavorites).toUpperCase() + )] : []), + + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterTop).toUpperCase()), + ...(mediaChip("gallery") ? [button( { type: "submit", name: "filter", value: "gallery", class: filter === "gallery" ? "filter-btn active" : "filter-btn" }, - i18n.imageFilterGallery - ), - button({ type: "submit", name: "filter", value: "meme", class: filter === "meme" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMeme), + String(i18n.imageFilterGallery).toUpperCase() + )] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton) ) ), diff --git a/src/views/indexing_view.js b/src/views/indexing_view.js index c2d758b..56d3d65 100644 --- a/src/views/indexing_view.js +++ b/src/views/indexing_view.js @@ -4,7 +4,7 @@ const { getConfig } = require('../configs/config-manager.js'); const doctypeString = ''; -exports.indexingView = ({ percent }) => { +exports.indexingView = ({ percent, refresh = 10 }) => { let metaRefresh; try { const cfg = getConfig() || {}; @@ -12,28 +12,23 @@ exports.indexingView = ({ percent }) => { void theme; } catch (_) {} + const known = percent !== null && percent !== undefined && Number.isFinite(Number(percent)); const pct = Math.max(0, Math.min(100, Number(percent) || 0)); const headingText = i18n.indexingTitle || 'Synchronizing'; - const message = i18n.indexingMessage || 'Oasis is trying to syncronize a huge network of inhabitants. Just wait!'; const refreshNote = i18n.indexingRefreshNote || 'This page refreshes every 10 seconds.'; - const editProfileLabel = i18n.indexingEditProfileLink || 'Set up my avatar'; return template( headingText, section( - div({ class: 'tags-header' }, - h2(`❤ ${headingText}`), - p(message) + div({ class: 'tags-header module-header-line' }, + h2(`❤ ${headingText}`) ), div({ class: 'indexing-progress-block' }, - progress({ value: String(pct), max: '100', class: 'indexing-progress' }), - p({ class: 'indexing-percent' }, strong(`${pct.toFixed(1)} %`)), - p({ class: 'indexing-note' }, refreshNote) - ), - div({ class: 'indexing-actions' }, - a({ href: '/profile/edit', class: 'filter-btn welcome-action-primary' }, editProfileLabel), - a({ href: '/modules', class: 'filter-btn' }, i18n.modulesTitle || 'Modules') + p({ class: 'indexing-message' }, `\ud83c\udf34 ${i18n.indexingMessage || 'Oasis is synchronizing the distributed network of nodes. Please wait.'}`), + known ? progress({ value: String(pct), max: '100', class: 'indexing-progress' }) : progress({ max: '100', class: 'indexing-progress' }), + known ? p({ class: 'indexing-percent' }, strong(`${pct.toFixed(1)} %`)) : null, + p({ class: 'indexing-note' }, String(refreshNote).replace('10', String(Number(refresh) || 10))) ) ) - ).replace('', ''); + ).replace('', ``); }; diff --git a/src/views/industry_view.js b/src/views/industry_view.js new file mode 100644 index 0000000..13c7a65 --- /dev/null +++ b/src/views/industry_view.js @@ -0,0 +1,1012 @@ +const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, ul, li, img, video, audio, table, thead, tbody, tr, td, th } = require("../server/node_modules/hyperaxe") +const { renderZoomableImage } = require("./gallery_view") +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderContentActions , renderSpreadEditWarning, renderSubscriptionBox, renderModuleStatsBy, renderCardMetaRow, moduleIsEmpty, paged } = require("./main_views") +const moment = require("../server/node_modules/moment") +const { config } = require("../server/SSB_server.js") +const { renderMapEmbedWithZoom } = require("./maps_view") +const { renderStyledText } = require("../backend/renderStyledText") + +const userId = config.keys.id + +const renderMediaBlob = (value, attrs = {}) => { + if (!value) return null + const s = String(value).trim() + if (!s) return null + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) + const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) + const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) + const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) + return null +} + +const SECTORS = ["software", "hardware", "agriculture", "textile", "energy", "food", "construction", "media", "services", "other"] +const POLICIES = ["open", "vote", "invite"] + +const FILTERS = [ + { key: "ALL", i18n: "industryFilterAll", title: "industryAllTitle" }, + { key: "RECENT", i18n: "industryFilterRecent", title: "industryRecentTitle" }, + { key: "MINE", i18n: "industryFilterMine", title: "industryMineTitle" }, + { key: "ACTIVE", i18n: "industryFilterActive", title: "industryActiveTitle" }, + { key: "PAUSED", i18n: "industryFilterPaused", title: "industryPausedTitle" }, + { key: "DISSOLVED", i18n: "industryFilterDissolved", title: "industryDissolvedTitle" }, + { key: "BLUEPRINTS", i18n: "industryFilterBlueprints", title: "industryBlueprints" }, + { key: "BUILDS", i18n: "industryFilterBuilds", title: "industryBuilds" }, + { key: "MEMBER", i18n: "industryFilterMember", title: "industryMemberTitle" }, + { key: "RULES", i18n: "industryFilterRules", title: "industryRulesTitle" } +] + +const renderIndustryRules = () => { + const points = [ + i18n.industryRulesIntro, + i18n.industryRulesSteward, + i18n.industryRulesMembership, + i18n.industryRulesQuorum, + i18n.industryRulesMajority, + i18n.industryRulesDecisions, + i18n.industryRulesBlueprints, + i18n.industryRulesBuilds, + i18n.industryRulesContributions, + i18n.industryRulesLaborRate, + i18n.industryRulesShares, + i18n.industryRulesDistribution, + i18n.industryRulesTreasury + ] + return div({ class: "card" }, + h2(i18n.industryRulesTitle || "Rules"), + div({ class: "rules-points" }, + points.filter(Boolean).map((t, i) => + div({ class: "rules-point" }, + span({ class: "rules-point-num" }, String(i + 1)), + span({ class: "rules-point-text" }, t) + ) + ) + ) + ) +} + +const safeArr = (v) => (Array.isArray(v) ? v : []) +const safeText = (v) => String(v || "").trim() + +const sectorLabel = (key) => i18n[`industrySector_${key}`] || key +const policyLabel = (key) => i18n[`industryPolicy_${key}`] || key + +const renderStatusChip = (status) => { + const s = String(status || "ACTIVE").toUpperCase() + const map = { + ACTIVE: ["whole", "🟢", i18n.industryStatusActive || "ACTIVE"], + PAUSED: ["half", "⏸", i18n.industryStatusPaused || "PAUSED"], + DISSOLVED: ["tombstoned", "⚰", i18n.industryStatusDissolved || "DISSOLVED"] + } + const [variant, icon, text] = map[s] || map.ACTIVE + return renderStateChip(variant, icon, text) +} + +const renderStatusBlock = (fc, returnTo) => { + const isMember = safeArr(fc.members).includes(userId) + const status = String(fc.status || "ACTIVE").toUpperCase() + const myPause = safeArr(fc.pauseVoters).includes(userId) + return div({ class: "industry-section industry-status-block" }, + div({ class: "card-chips-row" }, renderStatusChip(status)), + (isMember && status !== "DISSOLVED") + ? [ + p({ class: "industry-hint" }, `${i18n.industryPauseVotes || "Pause votes"}: ${fc.pauseYes || 0}`), + form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: "pause" }), + input({ type: "hidden", name: "ref", value: "" }), + button({ type: "submit", name: "choice", value: myPause ? "no" : "yes", class: "filter-btn" }, myPause ? (i18n.industryResumeButton || "Vote to resume") : (i18n.industryPauseButton || "Vote to pause")) + ) + ] + : null + ) +} + +const renderFacilityList = exports.renderFacilityList = (facilities, filter, spreadMap = new Map()) => { + const list = safeArr(facilities) + if (!list.length) return p(i18n.industryNoFacilitiesFound || "No facilities found.") + return div({ class: "tribe-grid" }, + list.map((fc) => { + const isOwn = fc.steward && String(fc.steward) === String(userId) + const isMember = safeArr(fc.members).includes(userId) + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length + const href = `/industry/${encodeURIComponent(fc.id)}` + const chips = [ + renderStatusChip(fc.status), + renderStateChip("whole", "🏭", sectorLabel(fc.sector)), + renderStateChip("half", "⚖", policyLabel(fc.membershipPolicy)), + isMember ? renderStateChip("whole", "★", i18n.industryMemberBadge || "MEMBER") : null, + renderLifespanChip(fc.lifetime, i18n), + fc.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (fc.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) + ].filter(Boolean) + return div({ class: "trending-card tribes-card industry-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(fc.id || fc.key, href, { spread: spreadMap.get(fc.id || fc.key) || null, author: fc.steward, reportTitle: fc.name, deleteAction: isOwn && memberCount <= 1 ? `/industry/delete/${encodeURIComponent(fc.id)}` : null }) + ), + div({ class: "card-section tribes-card-body" }, + div({ class: "tribe-card-image-wrapper" }, + a({ href }, renderMediaBlob(fc.image, { class: "tribe-card-hero-image" }) || div({ class: "industry-card-noimage" }, span("🏭"))), + isMember + ? form({ method: "GET", action: href, class: "tribe-visit-btn-wrapper" }, + button({ type: "submit", class: "filter-btn" }, String(i18n.industryVisitButton || "VISIT").toUpperCase()) + ) + : null + ), + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href }, safeText(fc.name) || (i18n.industryTitle || "Industry"))) + ), + div({ class: "card-chips-row" }, ...chips), + fc.description ? p({ class: "tribe-card-description" }, safeText(fc.description).slice(0, 220)) : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.industryMembers || "Members"}: ${memberCount}`) + ), + ) + ) + ) + }) + ) +} + +const renderFacilityForm = (facility, mode, spreadWarning = null) => { + const fc = facility || {} + const isEdit = mode === "edit" + const returnTo = "/industry?filter=MINE" + const curPolicy = fc.membershipPolicy || "vote" + const curSector = fc.sector || "other" + return div({ class: "div-center industry-form" }, + isEdit ? spreadWarning : null, + form({ + action: isEdit ? `/industry/update/${encodeURIComponent(fc.id)}` : "/industry/create", + method: "POST", + enctype: "multipart/form-data" + }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + label(i18n.industryName || "Name"), + br(), + input({ type: "text", name: "name", required: true, maxlength: "80", placeholder: i18n.industryNamePlaceholder || "Facility name", value: fc.name || "" }), + br(), + label(i18n.industryDescriptionLabel || "Description"), + br(), + textarea({ name: "description", rows: "5", maxlength: "2000", placeholder: i18n.industryDescriptionPlaceholder || "What does this facility produce?" }, fc.description || ""), + br(), + label(i18n.uploadMedia || "Upload media (max-size: 75MB)"), + br(), + input({ type: "file", name: "image" }), + br(), + label(i18n.uploadMedia), + br(), + input({ type: "file", name: "blob" }), + fc.image ? div({ class: "industry-form-media" }, renderMediaBlob(fc.image, { class: "industry-hero-image" })) : null, + br(), + label(i18n.industrySector || "Sector"), + br(), + select({ name: "sector" }, + SECTORS.map((s) => option({ value: s, ...(s === curSector ? { selected: true } : {}) }, sectorLabel(s))) + ), + br(), + label(i18n.mapLocationTitle || "Map Location"), + br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: fc.mapUrl || "" }), + br(), + label(i18n.shopTags || "Tags"), + br(), + input({ type: "text", name: "tags", placeholder: i18n.shopTagsPlaceholder || "Enter tags separated by commas", value: safeArr(fc.tags).join(", ") }), + br(), + label(i18n.industryMembershipPolicy || "Membership policy"), + br(), + select({ name: "membershipPolicy" }, + POLICIES.map((pol) => option({ value: pol, ...(pol === curPolicy ? { selected: true } : {}) }, policyLabel(pol))) + ), + br(), + label(i18n.industryQuorum || "Quorum"), + br(), + input({ type: "number", name: "quorum", min: "1", step: "1", value: fc.quorum != null ? fc.quorum : 1 }), + br(), + label(i18n.industryMajority || "Majority"), + br(), + input({ type: "number", name: "majority", min: "0.5", max: "1", step: "0.01", value: fc.majority != null ? fc.majority : 0.5 }), + br(), + label(i18n.industryLaborRate || "Labor rate"), + br(), + input({ type: "number", name: "laborRate", min: "0", step: "0.01", value: fc.laborRate != null ? fc.laborRate : 0 }), + br(), + button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update facility") : (i18n.industryCreateButton || "Create facility")) + ) + ) +} + +const renderGlobalBlueprints = (blueprints, spreadMap = new Map()) => { + const list = safeArr(blueprints) + if (!list.length) return p(i18n.industryNoBlueprints || "No blueprints yet.") + return div({ class: "industry-blueprints" }, + list.map((bp) => div({ class: "industry-card-wrap" }, + div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, "/industry/blueprint/" + encodeURIComponent(bp.id), { spread: spreadMap.get(bp.id) || null, author: bp.author, reportTitle: bp.name })), + div({ class: "industry-blueprint-card" + (bp.author === userId ? " own-content" : "") }, + div({ class: "card-chips-row" }, + renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) + ), + p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: `/industry/${encodeURIComponent(bp.facilityId)}` }, safeText(bp.facilityName))), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/blueprint/${encodeURIComponent(bp.id)}` }, safeText(bp.name)))), + bp.image ? renderMediaBlob(bp.image, { class: "post-image" }) : null, + bp.description ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(bp.description))) : null, + renderBlueprintLaborHours(bp), + safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, + renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), + safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, + (() => { + const acts = renderBlueprintGovernActions(bp.facilityId, bp, safeArr(bp.members).includes(userId) || bp.author === userId, "/industry?filter=BLUEPRINTS") + return acts.length ? div({ class: "industry-actions" }, ...acts) : null + })() + ) + )) + ) +} + +const renderGovernVoteButton = (facilityId, subject, ref, voters, yes, need, labelKey, fallback, returnTo) => { + const mine = safeArr(voters).includes(userId) + return form({ method: "POST", action: `/industry/govern/${encodeURIComponent(facilityId)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: subject }), + input({ type: "hidden", name: "ref", value: ref }), + button({ type: "submit", name: "choice", value: mine ? "no" : "yes", class: "filter-btn" }, + `${mine ? (i18n.industryRevokeVote || "Revoke") : (i18n[labelKey] || fallback)} (${yes || 0}/${need || 1})`) + ) +} + +const renderBuildGovernActions = (b, isMember, returnTo) => { + const isProposer = b.proposer === userId + const multi = (b.facilityMembers || 1) > 1 + const facilityId = b.facilityId || "" + return [ + (isMember && isProposer && b.updateApproved !== false) + ? form({ method: "GET", action: `/industry/builds/${encodeURIComponent(b.id)}/edit` }, button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update")) + : null, + (isMember && isProposer && b.deleteApproved !== false) + ? form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/delete` }, button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete")) + : null, + (isMember && multi) ? renderGovernVoteButton(facilityId, "buildUpdate", b.id, b.updateVoters, b.updateYes, b.voteNeed, "industryVoteUpdateButton", "Vote update", returnTo) : null, + (isMember && multi) ? renderGovernVoteButton(facilityId, "buildDelete", b.id, b.deleteVoters, b.deleteYes, b.voteNeed, "industryVoteDeleteButton", "Vote delete", returnTo) : null + ].filter(Boolean) +} + +const renderBuildCard = (b, opts = {}) => { + const href = `/industry/build/${encodeURIComponent(b.id)}` + const card = div({ class: "industry-blueprint-card" + (b.proposer === userId ? " own-content" : "") }, + div({ class: "card-chips-row" }, + renderBuildStatusChip(b.status), + ...((b.blueprintKind && opts.withBlueprintChips !== false) ? [ + renderStateChip("half", b.blueprintKind === "digital" ? "💾" : "📦", i18n["industryKind_" + b.blueprintKind] || b.blueprintKind), + renderStateChip("half", "⚖", String(b.blueprintLicense || "copyleft").toUpperCase()) + ] : []) + ), + opts.withFacility ? p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: `/industry/${encodeURIComponent(b.facilityId)}` }, safeText(b.facilityName))) : null, + opts.withProposer !== false ? p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)) : null, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, safeText(b.title)))), + b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null, + b.notes ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(b.notes))) : null, + div({ class: "industry-blueprint-meta industry-dates-row" }, + b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null, + b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY/MM/DD")) : null, + ), + renderBlueprintEstimate(b, i18n.industryFinalPrice || "Final price"), + p(span({ class: "industry-meta-label" }, `${i18n.industryContributions || "Contributions"}: `), String(safeArr(b.contributions).length)), + (opts.actions && opts.actions.length) ? div({ class: "industry-actions" }, ...opts.actions) : null + ) + if (!opts.withActions) return card + return div({ class: "industry-card-wrap" }, + div({ class: "card-header activity-card-header" }, span(), renderContentActions(b.id, href, { spread: opts.spread || null, author: b.proposer, reportTitle: b.title })), + card + ) +} + +const renderGlobalBuilds = (builds, spreadMap = new Map()) => { + const list = safeArr(builds) + if (!list.length) return p(i18n.industryNoBuilds || "No builds yet.") + return div({ class: "industry-blueprints" }, + list.map((b) => renderBuildCard(b, { withFacility: true, withActions: true, spread: spreadMap.get(b.id), actions: renderBuildGovernActions(b, safeArr(b.members).includes(userId) || b.proposer === userId, "/industry?filter=BUILDS") })) + ) +} + +exports.industryView = async (facilitiesOrForm, filter, params = {}) => { + const f = String(filter || "ALL").toUpperCase() + const facilityEditWarning = f === "EDIT" ? await renderSpreadEditWarning((safeArr(facilitiesOrForm)[0] || {}).id) : null + const search = safeText(params.search) + const sectorSel = safeText(params.sector) + const isForm = f === "CREATE" || f === "EDIT" + const isRules = f === "RULES" + const emptyMod = moduleIsEmpty(safeArr(facilitiesOrForm), f, "ALL", search || sectorSel) + const censusI = Array.isArray(params.censusList) ? params.censusList : safeArr(facilitiesOrForm) + const indChip = (x) => { + const m = x.key + if (m === f) return true + if (m === "MINE") return censusI.some(fc => String(fc.steward) === String(userId) || safeArr(fc.members).includes(userId)) + if (m === "ACTIVE" || m === "PAUSED" || m === "DISSOLVED") return censusI.some(fc => String(fc.status || "ACTIVE").toUpperCase() === m) + if (m === "MEMBER") return censusI.some(fc => safeArr(fc.members).includes(userId)) + if (m === "RECENT") return censusI.length > 0 + if (m === "BLUEPRINTS" || m === "BUILDS") return censusI.length > 0 + return true + } + return template( + i18n.industryTitle || "Industry", + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.industryTitle || "Industry"), + p(i18n.industryDescription || "Network-owned production facilities.") + ), + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "search", value: search }), + input({ type: "hidden", name: "sector", value: sectorSel }), + (emptyMod ? [] : FILTERS.filter(indChip).map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase()))) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ), + isRules + ? renderIndustryRules() + : isForm + ? renderFacilityForm(f === "EDIT" ? (safeArr(facilitiesOrForm)[0] || {}) : {}, f === "EDIT" ? "edit" : "create", facilityEditWarning) + : f === "BLUEPRINTS" + ? div({ class: "industry-list" }, renderGlobalBlueprints(paged(facilitiesOrForm), params.spreadMap || new Map())) + : f === "BUILDS" + ? div({ class: "industry-list" }, renderGlobalBuilds(paged(facilitiesOrForm), params.spreadMap || new Map())) + : section( + emptyMod ? null : div({ class: "industry-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(facilitiesOrForm, fc => String(fc.status || 'ACTIVE').toUpperCase(), [{ value: 'ACTIVE', label: i18n.industryStatusActive }, { value: 'PAUSED', label: i18n.industryStatusPaused }, { value: 'DISSOLVED', label: i18n.industryStatusDissolved }]), + form({ method: "GET", action: "/industry", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f || "ALL" }), + input({ type: "text", name: "search", value: search, placeholder: i18n.industrySearchPlaceholder || "Search facilities…", class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + select({ name: "sector", class: "filter-box__number" }, + [option({ value: "", ...(sectorSel ? {} : { selected: true }) }, i18n.industryAllSectors || "All sectors")] + .concat(SECTORS.map((s) => option({ value: s, ...(s === sectorSel ? { selected: true } : {}) }, sectorLabel(s)))) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ), + div({ class: "industry-list" }, renderFacilityList(paged(facilitiesOrForm), f, params.spreadMap)) + ) + ) + ) +} + +const membershipActions = (fc, returnTo) => { + const isSteward = fc.steward === userId + const isMember = safeArr(fc.members).includes(userId) + const status = String(fc.status || "ACTIVE").toUpperCase() + const policy = fc.membershipPolicy + const invited = safeArr(fc.invites).includes(userId) + const rows = [] + if (!isMember && status !== "DISSOLVED") { + if (policy === "vote") { + rows.push(form({ method: "POST", action: `/industry/apply/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "subscribe-btn" }, i18n.industryApplyButton || "Request to join") + )) + } else if (policy === "invite") { + rows.push(invited + ? form({ method: "POST", action: `/industry/join/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "subscribe-btn" }, i18n.industryJoinButton || "Join") + ) + : p({ class: "industry-hint" }, i18n.industryInviteNeeded || "You need an invitation to join.") + ) + } else { + rows.push(form({ method: "POST", action: `/industry/join/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "subscribe-btn" }, i18n.industryJoinButton || "Join") + )) + } + } + if (isMember && !isSteward) { + rows.push(form({ method: "POST", action: `/industry/leave/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "unsubscribe-btn" }, i18n.industryLeaveButton || "Leave") + )) + } + return rows +} + +const renderInviteSection = (fc, returnTo) => { + if (!safeArr(fc.members).includes(userId) || String(fc.status || "ACTIVE").toUpperCase() === "DISSOLVED") return null + if (fc.membershipPolicy !== "invite") return null + return div({ class: "industry-section" }, + h2(i18n.industryInviteButton || "Invite"), + form({ method: "POST", action: `/industry/invite/${encodeURIComponent(fc.id)}`, class: "industry-invite-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "text", name: "invitee", required: true, placeholder: i18n.industryInvitePlaceholder || "Oasis ID (@...ed25519)" }), + button({ type: "submit", class: "create-button" }, i18n.industryCreateInvite || "Generate invite") + ) + ) +} + +const renderPendingApplicants = (fc, returnTo) => { + const isMember = safeArr(fc.members).includes(userId) + const pending = safeArr(fc.pendingApplicants) + if (!isMember || !pending.length) return null + return div({ class: "industry-section" }, + h2(i18n.industryPendingApplicants || "Pending applicants"), + ul({ class: "industry-applicants" }, + pending.map((ap) => li( + userLink(ap.id), + span({ class: "industry-vote-count" }, ` (${i18n.industryVotesYes || "yes"}: ${ap.yes} / ${i18n.industryVotesNo || "no"}: ${ap.no})`), + ap.voters.includes(userId) + ? span({ class: "industry-hint" }, ` — ${i18n.industryAlreadyVoted || "voted"}`) + : form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}`, class: "industry-vote-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: "admit" }), + input({ type: "hidden", name: "ref", value: ap.id }), + button({ type: "submit", name: "choice", value: "yes", class: "subscribe-btn" }, i18n.industryAdmit || "Admit"), + button({ type: "submit", name: "choice", value: "no", class: "unsubscribe-btn" }, i18n.industryReject || "Reject") + ) + )) + ) + ) +} + +const renderDissolveBlock = (fc, returnTo) => { + const isMember = safeArr(fc.members).includes(userId) + const status = String(fc.status || "ACTIVE").toUpperCase() + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length + if (!isMember || status === "DISSOLVED" || memberCount <= 1) return null + return div({ class: "industry-section" }, + h2(i18n.industryDissolveTitle || "Dissolve facility"), + form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: "dissolve" }), + input({ type: "hidden", name: "ref", value: "" }), + button({ type: "submit", name: "choice", value: "yes", class: "unsubscribe-btn danger-btn" }, i18n.industryDissolveVote || "Vote to dissolve") + ) + ) +} + +const BUILD_STATUSES = ["PROPOSED", "APPROVED", "STOCKING", "IN_PRODUCTION", "COMPLETED", "FAILED"] +const BUILD_TRANSITIONS = { APPROVED: ["STOCKING", "FAILED"], STOCKING: ["IN_PRODUCTION", "FAILED"], IN_PRODUCTION: ["COMPLETED", "FAILED"], FAILED: ["APPROVED", "STOCKING", "IN_PRODUCTION"], COMPLETED: ["IN_PRODUCTION"] } +const CONTRIBUTABLE_VIEW = new Set(["APPROVED", "STOCKING", "IN_PRODUCTION"]) +const buildStatusLabel = (s) => i18n["industryBuildStatus_" + String(s || "").toUpperCase()] || s + +const renderBuildStatusChip = (status) => { + const s = String(status || "PROPOSED").toUpperCase() + const map = { PROPOSED: ["half", "📋"], REJECTED: ["tombstoned", "🚫"], APPROVED: ["whole", "✅"], STOCKING: ["half", "📦"], IN_PRODUCTION: ["half", "⚙"], COMPLETED: ["whole", "🏁"], FAILED: ["tombstoned", "✗"] } + const [v, ic] = map[s] || map.PROPOSED + return renderStateChip(v, ic, buildStatusLabel(s)) +} + +const fmtEco = (n) => `${Number(n || 0).toFixed(2)} ECO` + +const renderBlueprintLaborHours = (bp) => { + if (bp.laborHours == null) return null + return p({ class: "industry-labor-hours" }, span({ class: "industry-meta-label" }, `${i18n.industryLaborHours || "Labor (hours)"}: `), `${bp.laborHours || 0}h`) +} + +const renderBlueprintEstimate = (bp, totalLabel) => { + if (bp.estTotal == null) return null + return div({ class: "industry-estimate" }, + p(span({ class: "industry-meta-label" }, `${i18n.industryEstMaterials || "Total Materials"}: `), fmtEco(bp.estMaterialsCost)), + p(span({ class: "industry-meta-label" }, `${i18n.industryEstLabor || "Total Labor"}: `), fmtEco(bp.estLaborCost)), + br(), + p({ class: "industry-estimate-total" }, span({ class: "industry-meta-label" }, `${totalLabel || i18n.industryEstTotal || "Estimated price"}: `), fmtEco(bp.estTotal)) + ) +} + +const renderBlueprintGovernActions = (facilityId, bp, isMember, returnTo) => { + const isAuthor = bp.author === userId + const multi = (bp.facilityMembers || 1) > 1 + const ref = bp.root || bp.id + const voteBtn = (subject, voters, yes, labelKey, fallback) => { + const mine = safeArr(voters).includes(userId) + return form({ method: "POST", action: `/industry/govern/${encodeURIComponent(facilityId)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: subject }), + input({ type: "hidden", name: "ref", value: ref }), + button({ type: "submit", name: "choice", value: mine ? "no" : "yes", class: "filter-btn" }, + `${mine ? (i18n.industryRevokeVote || "Revoke") : (i18n[labelKey] || fallback)} (${yes || 0}/${bp.voteNeed || 1})`) + ) + } + return [ + (isMember && isAuthor && bp.updateApproved !== false) + ? form({ method: "GET", action: `/industry/blueprints/${encodeURIComponent(bp.id)}/edit` }, button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update")) + : null, + (isMember && isAuthor && bp.deleteApproved !== false) + ? form({ method: "POST", action: `/industry/blueprints/${encodeURIComponent(bp.id)}/delete` }, button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete")) + : null, + (isMember && multi) ? voteBtn("bpUpdate", bp.updateVoters, bp.updateYes, "industryVoteUpdateButton", "Vote update") : null, + (isMember && multi) ? voteBtn("bpDelete", bp.deleteVoters, bp.deleteYes, "industryVoteDeleteButton", "Vote delete") : null + ].filter(Boolean) +} + +const renderBlueprintsSection = (fc, blueprints, isMember, spreadMap = new Map()) => { + const list = safeArr(blueprints) + const cards = list.map((bp) => { + const actions = renderBlueprintGovernActions(fc.id, bp, isMember, `/industry/${encodeURIComponent(fc.id)}`) + return div({ class: "industry-card-wrap" }, + div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { spread: spreadMap.get(bp.id) || null, author: bp.author, reportTitle: bp.name })), + div({ class: "industry-blueprint-card" }, + div({ class: "card-chips-row" }, + renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(bp.name))), + bp.image ? renderMediaBlob(bp.image, { class: "post-image" }) : null, + bp.description ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(bp.description))) : (bp.outItem ? p(span({ class: "industry-meta-label" }, `${i18n.industryOutput || "Output"}: `), `${bp.outQty || 0} × ${safeText(bp.outItem)}`) : null), + renderBlueprintLaborHours(bp), + safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, + renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), + safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, + actions.length ? div({ class: "industry-actions" }, ...actions) : null + ) + ) + }) + return div({ class: "industry-section" }, + h2(i18n.industryBlueprints || "Blueprints"), + list.length ? div({ class: "industry-blueprints" }, ...cards) : p({ class: "industry-hint" }, i18n.industryNoBlueprints || "No blueprints yet."), + isMember ? renderBlueprintForm(fc, {}, "create") : null + ) +} + +const renderBlueprintForm = (fc, bp, mode, spreadWarning = null) => { + const isEdit = mode === "edit" + const curKind = bp.outKind || "physical" + const materialsText = safeArr(bp.materials).map(m => `${m.item}:${m.qty}:${m.price != null ? m.price : 0}`).join("\n") + return div({ class: "industry-form industry-blueprint-form" }, + h2(isEdit ? (i18n.industryEditBlueprint || "Edit blueprint") : (i18n.industryNewBlueprint || "New blueprint")), + isEdit ? spreadWarning : null, + form({ method: "POST", action: isEdit ? `/industry/blueprints/${encodeURIComponent(bp.id)}/update` : `/industry/${encodeURIComponent(fc.id)}/blueprints`, enctype: "multipart/form-data" }, + label(i18n.industryName || "Name"), + br(), input({ type: "text", name: "name", required: true, maxlength: "80", value: bp.name || "" }), br(), + label(i18n.industryDescriptionLabel || "Description"), + br(), textarea({ name: "description", rows: "4", maxlength: "2000", placeholder: i18n.industryBlueprintDescPlaceholder || "Specs, dimensions, weight, docs…" }, bp.description || ""), br(), + label(i18n.uploadMedia || "Upload media (max-size: 75MB)"), + br(), input({ type: "file", name: "image" }), + bp.image ? div({ class: "industry-form-media" }, renderMediaBlob(bp.image, { class: "post-image" })) : null, + br(), + label(i18n.industryOutputKind || "Product type"), + br(), select({ name: "outKind" }, ["physical", "digital"].map(k => option({ value: k, ...(k === curKind ? { selected: true } : {}) }, i18n["industryKind_" + k] || k))), br(), + label(i18n.industryMaterials || "Materials"), + br(), textarea({ maxlength: "5000", name: "materialsText", rows: "3", placeholder: i18n.industryMaterialsPlaceholder || "solar-panel:2:120\nbattery:1:80\nkit:1:15" }, materialsText), br(), + label(i18n.industryLaborHours || "Labor hours"), + br(), input({ type: "number", name: "laborHours", min: "0", step: "0.01", value: bp.laborHours != null ? bp.laborHours : "" }), br(), + button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update") : (i18n.industryCreateBlueprintButton || "Create blueprint")) + ) + ) +} + +const renderBuildForm = (fc, blueprints, build, mode, spreadWarning = null) => { + const b = build || {} + const isEdit = mode === "edit" + const today = moment().format("YYYY-MM-DD") + return div({ class: "industry-form industry-build-form" }, + h2(isEdit ? (i18n.industryEditBuild || "Edit build") : (i18n.industryNewBuild || "Propose a build")), + isEdit ? spreadWarning : null, + form({ method: "POST", action: isEdit ? `/industry/builds/${encodeURIComponent(b.id)}/update` : `/industry/${encodeURIComponent(fc.id)}/builds`, enctype: "multipart/form-data" }, + label(i18n.industryBuildTitle || "Title"), + br(), input({ type: "text", name: "title", required: true, maxlength: "120", value: safeText(b.title) }), br(), + label(i18n.industryBlueprint || "Blueprint"), + br(), isEdit + ? input({ type: "text", disabled: true, value: safeText(b.blueprintName) }) + : select({ name: "blueprintId", required: true }, safeArr(blueprints).map(bp => option({ value: bp.id }, safeText(bp.name)))), br(), + label(i18n.industryBuildStart || "Start date"), + br(), input({ type: "date", name: "startDate", required: true, value: b.startDate || "", min: today }), br(), + label(i18n.industryBuildEnd || "End date"), + br(), input({ type: "date", name: "endDate", required: true, value: b.endDate || "", min: today }), br(), + label(i18n.industryBuildNotes || "Notes"), + br(), textarea({ name: "notes", rows: "3", maxlength: "1000" }, safeText(b.notes)), br(), + button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update") : (i18n.industryProposeBuildButton || "Propose build")) + ) + ) +} + +const renderBuildsSection = (fc, builds, blueprints, isMember, spreadMap = new Map()) => { + const list = safeArr(builds) + return div({ class: "industry-section" }, + h2(i18n.industryBuilds || "Builds"), + list.length ? div({ class: "industry-blueprints" }, ...list.map((b) => renderBuildCard(b, { withActions: true, withBlueprintChips: false, withProposer: false, spread: spreadMap.get(b.id), actions: renderBuildGovernActions(b, isMember, `/industry/${encodeURIComponent(fc.id)}`) }))) : p({ class: "industry-hint" }, i18n.industryNoBuilds || "No builds yet."), + (isMember && fc.status === "ACTIVE" && !safeArr(blueprints).length) + ? p({ class: "industry-hint" }, i18n.industryNeedBlueprint || "Create a blueprint first: every build produces one.") + : null, + (isMember && fc.status === "ACTIVE" && safeArr(blueprints).length) ? renderBuildForm(fc, blueprints, {}, "create") : null + ) +} + +const renderFacilityJobsSection = (fc, jobs) => { + const list = safeArr(jobs) + if (!list.length) return null + const rows = list.map((j) => li( + a({ href: `/jobs/${encodeURIComponent(j.id || j.key)}` }, safeText(j.title) || (i18n.jobsTitle || "Job")), + j.salary ? span({ class: "industry-hint" }, ` · ${j.salary} ECO · `) : span({ class: "industry-hint" }, " · "), + renderStateChip(String(j.status || "OPEN").toUpperCase() === "OPEN" ? "whole" : "tombstoned", "", String(j.status || "OPEN").toUpperCase()) + )) + return div({ class: "industry-section" }, + h2(i18n.industryJobs || "Jobs"), + ul({ class: "industry-jobs-list" }, ...rows) + ) +} + +const renderFacilitySide = (fc, returnTo, params = {}) => { + const isSteward = fc.steward === userId + const isMember = safeArr(fc.members).includes(userId) + const isOwner = String(fc.steward || fc.author) === String(userId) + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length + + const chips = [ + renderStateChip("whole", "🏭", sectorLabel(fc.sector)), + renderStateChip("half", "⚖", policyLabel(fc.membershipPolicy)), + renderLifespanChip(fc.lifetime, i18n), + renderEcoTax(fc.msgSize, fc.id || fc.key), + (isSteward || isMember) + ? ((isOwner || (fc.subscription && fc.subscription.subscribed === true)) + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : renderStateChip("closed", "✉", i18n.subscriptionOff)) + : null + ].filter(Boolean) + + const sideActions = [] + for (const act of membershipActions(fc, returnTo)) sideActions.push(act) + if (!isSteward && fc.steward) { + sideActions.push(a({ href: `/reports?filter=create&category=ABUSE&title=${encodeURIComponent(`${i18n.industryTitle || "Industry"}: ${safeText(fc.name)}`)}`, class: "filter-btn" }, i18n.industryReportButton || "Report")) + } + if (isSteward) { + sideActions.push(form({ method: "GET", action: `/industry/edit/${encodeURIComponent(fc.id)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.industryUpdateButton || "Update") + )) + } + + return div({ class: "tribe-side" }, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/${encodeURIComponent(fc.id)}` }, safeText(fc.name) || (i18n.industryTitle || "Industry")))), + div({ class: "card-chips-row" }, ...chips), + fc.image ? renderMediaBlob(fc.image, { class: "tribe-detail-image" }) : null, + fc.description ? p({ class: "tribe-side-description" }, ...renderStyledText(safeText(fc.description))) : null, + table({ class: "tribe-info-table" }, + tr(td({ class: "tribe-info-label" }, i18n.industryLaborRate || "Labor rate"), td({ class: "tribe-info-value", colspan: "3" }, `${fc.laborRate || 0} ECO/h`)), + tr(td({ class: "tribe-info-label" }, i18n.industryQuorum || "Quorum"), td({ class: "tribe-info-value", colspan: "3" }, String(fc.quorum))), + tr(td({ class: "tribe-info-label" }, i18n.industryMajority || "Majority"), td({ class: "tribe-info-value", colspan: "3" }, `${Math.round((fc.majority || 0.5) * 100)}%`)), + tr(td({ class: "tribe-info-value tribe-author-cell", colspan: "4" }, userLink(fc.steward))) + ), + fc.mapUrl ? renderMapEmbedWithZoom(params.mapData, fc.mapUrl, returnTo, params.zoom) : null, + safeArr(fc.tags).length + ? div({ class: "tribe-side-tags" }, safeArr(fc.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : null, + renderStatusBlock(fc, returnTo), + h2({ class: "tribe-members-count" }, `${i18n.industryMembers || "Members"}: ${memberCount}`), + renderInviteSection(fc, returnTo), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + (fc.subscription && (isSteward || isMember)) + ? renderSubscriptionBox({ + target: fc.rootId || fc.id, + scope: "industry", + subscribed: fc.subscription.subscribed === true, + count: fc.subscription.count, + isOwner: isOwner, + returnTo: returnTo + }) + : null, + renderDissolveBlock(fc, returnTo) + ) +} + +exports.singleFacilityView = async (facility, filter, params = {}) => { + const fc = facility || {} + const f = String(filter || "ALL").toUpperCase() + const returnTo = `/industry/${encodeURIComponent(fc.id)}?filter=${encodeURIComponent(f)}` + const isMember = safeArr(fc.members).includes(userId) + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length + const facilitySide = renderFacilitySide(fc, returnTo, params) + + const facilityMain = div({ class: "tribe-main" }, + renderBlueprintsSection(fc, params.blueprints, isMember, params.childSpreadMap || new Map()), + renderBuildsSection(fc, params.builds, params.blueprints, isMember, params.childSpreadMap || new Map()), + renderFacilityJobsSection(fc, params.facilityJobs), + renderCardMetaRow(renderOpinionsVoting('/industry/opinions', fc.id || fc.key, fc.opinions, null, fc.opinions_inhabitants)), + div({ class: "industry-section" }, + h2(i18n.industryMembers || "Members"), + ul({ class: "industry-members-list" }, safeArr(fc.members).map((mid) => li( + userLink(mid), + span({ class: "industry-role" }, ` · ${mid === fc.steward ? (i18n.industrySteward || "Steward") : (i18n.industryMember || "Member")}`) + ))) + ), + renderPendingApplicants(fc, returnTo) + ) + + return template( + i18n.industryTitle || "Industry", + section( + div({ class: "card-header activity-card-header" }, + renderContentActions(fc.id || fc.key, `/industry/${encodeURIComponent(fc.id)}`, { spread: params.spreads || null, author: fc.steward, reportTitle: fc.name, deleteAction: fc.steward === userId && memberCount <= 1 ? `/industry/delete/${encodeURIComponent(fc.id)}` : null }) + ), + div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ), + div({ class: "tribe-details" }, facilitySide, facilityMain) + ) + ) +} + +exports.blueprintEditView = async (bp, fc) => template( + i18n.industryTitle || "Industry", + section( + div({ class: "card-header activity-card-header" }, + renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { author: bp.author, reportTitle: bp.name }) + ), + div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ), + renderBlueprintForm(fc, bp, "edit", await renderSpreadEditWarning(bp && bp.id)) + ) +) + +exports.buildEditView = async (b, fc) => template( + i18n.industryTitle || "Industry", + section( + div({ class: "card-header activity-card-header" }, + renderContentActions(b.id, `/industry/build/${encodeURIComponent(b.id)}`, { author: b.proposer, reportTitle: b.title }) + ), + div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ), + renderBuildForm(fc, [], b, "edit", await renderSpreadEditWarning(b && b.id)) + ) +) + +exports.singleBlueprintView = async (blueprint, params = {}) => { + const bp = blueprint || {} + const isMember = safeArr(bp.members).includes(userId) + const facilityHref = `/industry/${encodeURIComponent(bp.facilityId || "")}` + const returnTo = `/industry/blueprint/${encodeURIComponent(bp.id)}` + const actions = renderBlueprintGovernActions(bp.facilityId, bp, isMember, returnTo) + + return template( + i18n.industryTitle || "Industry", + section(div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle), p(i18n.industryDescription))), + section( + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ) + ), + section( + div({ class: "shop-detail" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(bp.id, returnTo, { spread: params.spreads || null, author: bp.author, reportTitle: bp.name || bp.title }) + ), + div({ class: "card-chips-row" }, + renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) + ), + p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: facilityHref }, safeText(bp.facilityName))), + h2(safeText(bp.name)), + bp.image ? div({ class: "shop-detail-media" }, renderMediaBlob(bp.image, { class: "post-image" })) : null, + bp.description ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(bp.description))) : null, + div({ class: "industry-meta" }, + p(span({ class: "industry-meta-label" }, `${i18n.industryAuthor || "Author"}: `), userLink(bp.author)) + ), + renderBlueprintLaborHours(bp), + safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, + renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), + safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, + actions.length ? div({ class: "industry-actions" }, ...actions) : null, + safeArr(bp.builds).length + ? div({ class: "industry-section" }, + h2(i18n.industryBuilds || "Builds"), + ul({ class: "industry-builds-list" }, bp.builds.map(b => li( + a({ href: `/industry/build/${encodeURIComponent(b.id)}` }, safeText(b.title)), + " ", renderBuildStatusChip(b.status) + ))) + ) + : null + ) + ) + ) +} + +exports.singleBuildView = async (build, params = {}) => { + const b = build || {} + const isMember = safeArr(b.members).includes(userId) + const isSteward = b.steward === userId + const isProposer = b.proposer === userId + const status = String(b.status || "PROPOSED").toUpperCase() + const returnTo = `/industry/build/${encodeURIComponent(b.id)}` + const facilityHref = `/industry/${encodeURIComponent(b.facilityId)}` + + const contribRows = safeArr(b.contributions).map(c => tr( + td(userLink(c.author)), + td(i18n["industryKind_" + c.kind] || c.kind), + td(c.kind === "labor" ? `${c.hours} h` : c.kind === "material" ? `${c.value} ECO · ${safeText(c.item)}` : `${c.eco} ECO`), + td(safeText(c.note)) + )) + + const canUpdateBuild = isMember && (isProposer || isSteward) && !b.distributed && b.updateApproved !== false + const govRows = [] + if ((status === "PROPOSED" || status === "REJECTED") && isMember) { + govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/vote`, class: "industry-build-vote" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", name: "choice", value: "yes", class: "subscribe-btn" }, i18n.industryApproveBuild || "Approve"), + button({ type: "submit", name: "choice", value: "no", class: "unsubscribe-btn" }, i18n.industryReject || "Reject") + )) + } + const isReopen = status === "COMPLETED" || status === "FAILED" + const lockedByDistribution = status === "COMPLETED" && b.distributed + const transitions = ((!isMember || lockedByDistribution) ? [] : (BUILD_TRANSITIONS[status] || [])).filter(nextS => { + const stewardOnly = nextS === "COMPLETED" || nextS === "FAILED" || isReopen + return isSteward || (isProposer && !stewardOnly) + }) + if (transitions.length) { + govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/status`, class: "project-control-form project-control-form--status" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + select({ name: "status", class: "project-control-select" }, + transitions.map(nextS => option({ value: nextS }, buildStatusLabel(nextS))) + ), + button({ type: "submit", class: "status-btn project-control-btn" }, i18n.industryAdvanceTo || "Set to") + )) + } + if (canUpdateBuild) { + govRows.push(form({ method: "GET", action: `/industry/builds/${encodeURIComponent(b.id)}/edit` }, + button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update") + )) + } + if (isMember && isProposer && b.deleteApproved !== false) { + govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/delete` }, + button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete") + )) + } + if (isMember && (b.facilityMembers || 1) > 1) { + govRows.push(renderGovernVoteButton(b.facilityId, "buildUpdate", b.id, b.updateVoters, b.updateYes, b.voteNeed, "industryVoteUpdateButton", "Vote update", returnTo)) + govRows.push(renderGovernVoteButton(b.facilityId, "buildDelete", b.id, b.deleteVoters, b.deleteYes, b.voteNeed, "industryVoteDeleteButton", "Vote delete", returnTo)) + } + + const kind = ["labor", "material", "eco"].includes(String(params.kind)) ? String(params.kind) : "labor" + + const contributeAction = `/industry/builds/${encodeURIComponent(b.id)}/contribute` + const contributeBlock = (CONTRIBUTABLE_VIEW.has(status) && isMember) + ? div({ class: "industry-section", id: "contribute" }, + h2(i18n.industryContribute || "Contribute"), + form({ method: "GET", action: `/industry/build/${encodeURIComponent(b.id)}#contribute`, class: "industry-form industry-contrib-kind" }, + label(i18n.industryKindLabel || "Kind"), + select({ name: "kind", class: "project-control-select" }, + option({ value: "labor", ...(kind === "labor" ? { selected: true } : {}) }, i18n.industryKind_labor || "Labor"), + option({ value: "material", ...(kind === "material" ? { selected: true } : {}) }, i18n.industryKind_material || "Material"), + option({ value: "eco", ...(kind === "eco" ? { selected: true } : {}) }, i18n.industryKind_eco || "Funds") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ), + form({ method: "POST", action: contributeAction, class: "industry-form industry-contrib-form-single" }, + input({ type: "hidden", name: "kind", value: kind }), + kind === "labor" ? [ + label(i18n.industryHours || "Hours"), + input({ type: "number", name: "hours", min: "0.01", step: "0.01", required: true, class: "industry-input-num" }) + ] : null, + kind === "material" ? [ + label(i18n.industryMaterialItem || "Material"), + textarea({ name: "item", rows: "2", maxlength: "80", required: true }), + label(i18n.industryMaterialValue || "Value (ECO)"), + input({ type: "number", name: "value", min: "0.01", step: "0.01", required: true, class: "industry-input-num" }) + ] : null, + kind === "eco" ? [ + label(i18n.industryEcoAmount || "Amount (ECO)"), + input({ type: "number", name: "eco", min: "0.000001", step: "0.000001", required: true, class: "industry-input-num" }) + ] : null, + label(i18n.industryNote || "Note"), + textarea({ name: "note", rows: "2", maxlength: "140" }), + button({ type: "submit", class: "filter-btn" }, i18n.industryContributeButton || "Contribute") + ) + ) + : null + + const distributeBlock = (status === "COMPLETED" && isSteward && !b.distributed) + ? div({ class: "industry-section" }, + h2(i18n.industryDistribute || "Distribute"), + form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/distribute` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + label(i18n.industryOutputValue || "Output value (ECO, e.g. from sale)"), + br(), input({ type: "number", name: "outputValue", min: "0", step: "0.000001", value: "0" }), br(), + button({ type: "submit", class: "subscribe-btn" }, i18n.industryDistributeButton || "Distribute by shares") + ), + p({ class: "industry-hint" }, `${i18n.industryTreasury || "Treasury"}: ${b.treasury || 0} ECO`) + ) + : null + + const allocationBlock = b.distributed && b.allocation + ? div({ class: "industry-section" }, + h2(i18n.industryDistribution || "Distribution"), + p(`${i18n.industryPot || "Pot"}: ${(b.allocation.pot || 0).toFixed(2)} ECO`) + ) + : null + + const buildLabel = `${i18n.industryBuild || "Build"}: ${safeText(b.title)}` + const facilityName = safeText(b.facilityName) + const productName = safeText(b.blueprintName) || safeText(b.title) + const crossTags = [facilityName, productName].filter(Boolean).map(x => x.toLowerCase().replace(/\s+/g, "-")).join(", ") + const crossDates = [ + b.startDate ? `${i18n.industryBuildStart || "Start date"}: ${moment(b.startDate).format("YYYY/MM/DD")}` : null, + b.endDate ? `${i18n.industryBuildEnd || "End date"}: ${moment(b.endDate).format("YYYY/MM/DD")}` : null + ].filter(Boolean).join(" · ") + const crossContext = [ + `${i18n.industryFacility || "Facility"}: ${facilityName}`, + b.blueprintName ? `${i18n.industryBlueprint || "Blueprint"}: ${safeText(b.blueprintName)}` : null, + crossDates || null, + b.estTotal != null ? `${i18n.industryFinalPrice || "Final price"}: ${fmtEco(b.estTotal)}` : null + ].filter(Boolean).join("\n") + const crossDescription = [safeText(b.notes), crossContext].filter(Boolean).join("\n\n") + const crossLinks = isMember + ? div({ class: "industry-actions" }, + a({ href: `/tasks?filter=create&returnTo=${encodeURIComponent(returnTo)}&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}`, class: "filter-btn" }, i18n.industryCreateTask || "Create Task"), + a({ href: `/jobs?filter=CREATE&industry=${encodeURIComponent(b.facilityId || "")}&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}&tasks=${encodeURIComponent(safeText(b.notes))}&salary=${encodeURIComponent(b.estLaborCost != null ? b.estLaborCost : "")}`, class: "filter-btn" }, i18n.industryPostJob || "Create Job"), + a({ href: `/projects?filter=CREATE&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}&goal=${encodeURIComponent(b.estTotal != null ? b.estTotal : "")}`, class: "filter-btn" }, i18n.industrySendToProjects || "Create Project"), + a({ href: `/market?filter=create&industry=${encodeURIComponent(b.facilityId || "")}&title=${encodeURIComponent(productName)}&description=${encodeURIComponent(crossDescription)}&price=${encodeURIComponent(b.estTotal != null ? b.estTotal : "")}&tags=${encodeURIComponent(crossTags)}`, class: "filter-btn" }, i18n.industrySellOnMarket || "Send to Market"), + (!isSteward && b.steward) ? a({ href: `/courts?filter=open&method=MEDIATION&respondentId=${encodeURIComponent(b.steward)}&titleSuffix=${encodeURIComponent(buildLabel)}`, class: "filter-btn" }, i18n.industryOpenDispute || "Open dispute") : null + ) + : null + + return template( + i18n.industryTitle || "Industry", + section(div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle), p(i18n.industryDescription))), + section( + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ) + ), + section( + div({ class: "shop-detail" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(b.id, returnTo, { spread: params.spreads || null, author: b.proposer, reportTitle: b.title }) + ), + b.image ? div({ class: "shop-detail-media" }, renderMediaBlob(b.image, { class: "post-image" })) : null, + h2(safeText(b.title) || (i18n.industryBuild || "Build")), + div({ class: "card-chips-row" }, renderBuildStatusChip(status)), + div({ class: "industry-meta" }, + b.blueprintName ? p(span({ class: "industry-meta-label" }, `${i18n.industryBlueprint || "Blueprint"}: `), safeText(b.blueprintName)) : null, + b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null, + p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)), + b.notes ? p({ class: "industry-build-notes" }, span({ class: "industry-meta-label" }, `${i18n.industryBuildNotes || "Notes"}: `), safeText(b.notes)) : null, + div({ class: "industry-dates-row" }, + b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null, + b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY/MM/DD")) : null, + (b.endDate && status !== "COMPLETED" && status !== "FAILED") + ? p(span({ class: "industry-meta-label" }, `${i18n.industryTimeLeft || "Time left"}: `), `${Math.max(0, Math.ceil((new Date(b.endDate) - Date.now()) / 86400000))}d`) + : null + ) + ), + renderBlueprintEstimate(b, i18n.industryFinalPrice || "Final price"), + govRows.length ? div({ class: "industry-actions" }, ...govRows) : null, + crossLinks, + safeArr(b.contributions).length + ? div({ class: "industry-section" }, + h2(i18n.industryContributions || "Contributions"), + table({ class: "industry-table" }, + thead(tr(th(i18n.industryMember || "Member"), th(i18n.industryKindLabel || "Kind"), th(i18n.industryAmount || "Amount"), th(i18n.industryNote || "Note"))), + tbody(...contribRows) + ) + ) + : null, + contributeBlock, + distributeBlock, + allocationBlock + ) + ) + ) +} diff --git a/src/views/inhabitants_view.js b/src/views/inhabitants_view.js index 58f0ba0..399059e 100644 --- a/src/views/inhabitants_view.js +++ b/src/views/inhabitants_view.js @@ -1,7 +1,9 @@ -const { div, h2, p, section, button, form, img, a, textarea, input, br, span, strong } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderUserSensors, renderContentActions } = require('./main_views'); +const { div, h2, p, section, button, form, img, a, textarea, input, span, strong } = require("../server/node_modules/hyperaxe"); +const moment = require("../server/node_modules/moment"); +const { renderCallButton, template, i18n, userLink, renderUserSensors, renderContentActions, renderRelationshipBlock, renderModuleStats, paged } = require('./main_views'); +const { renderZoomableImage } = require('./gallery_view'); const { renderContentStats } = require('./clearnet_view'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText } = require('../backend/renderStyledText'); const { getConfig } = require('../configs/config-manager'); const DEFAULT_HASH_ENC = "%260000000000000000000000000000000000000000000%3D.sha256"; @@ -48,20 +50,44 @@ function resolvePhoto(photoField, size = 256) { if (photoField.startsWith('/blob/')) return photoField; if (photoField.startsWith('/image/')) { if (isDefaultImageId(photoField)) return '/assets/images/default-avatar.png'; - return photoField.replace('/image/256/','/image/'+size+'/').replace('/image/512/','/image/'+size+'/'); + return photoField.replace('/image/256/','/image/'+size+'/').replace('/image/512/','/image/'+size+'/') + '?fallback=avatar'; } } - return toImageUrl(photoField, size); + return toImageUrl(photoField, size) + '?fallback=avatar'; } -const generateFilterButtons = (filters, currentFilter) => +const filterLabel = (mode) => + String(i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode).toUpperCase(); + +const generateFilterButtons = (filters, currentFilter, labelOf = filterLabel) => filters.map(mode => form({ method: 'GET', action: '/inhabitants' }, input({ type: 'hidden', name: 'filter', value: mode }), button({ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' - }, i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode) + }, labelOf(mode)) + ) + ); + +const MAIN_FILTERS = ['all', 'TOP', 'contacts', 'blocked', 'CVs', 'SUGGESTED', 'GALLERY']; + +const renderMainFilters = (filter, isTop, census = {}) => + div({ class: 'inhabitant-action' }, + ...MAIN_FILTERS.filter(mode => { + if (mode === 'all' || mode === 'TOP') return true; + if (mode === 'TOP' ? isTop : filter === mode) return true; + return census[mode] !== false; + }).map(mode => + form({ method: 'GET', action: '/inhabitants' }, + input({ type: 'hidden', name: 'filter', value: mode === 'TOP' ? 'TOP ACTIVITY' : mode }), + button({ + type: 'submit', + class: (mode === 'TOP' ? isTop : filter === mode) ? 'filter-btn active' : 'filter-btn' + }, mode === 'TOP' + ? String(i18n.topSectionTitle || 'TOP').toUpperCase() + : filterLabel(mode)) + ) ) ); @@ -89,10 +115,34 @@ function lastActivityBadge(user, isMe) { const lightboxId = (id) => 'inhabitant_' + String(id || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_'); +const cvField = (labelText, value) => value + ? div({ class: 'card-field' }, + span({ class: 'card-label' }, `${labelText}: `), + span({ class: 'card-value' }, value) + ) + : null; + +const renderCvFields = (user) => { + const languages = Array.isArray(user.languages) ? user.languages.filter(Boolean) : []; + const skills = Array.isArray(user.skills) ? user.skills.filter(Boolean) : []; + const fields = [ + cvField(i18n.locationLabel, user.location), + cvField(i18n.languagesLabel, languages.length ? languages.join(', ').toUpperCase() : ''), + cvField(i18n.skillsLabel, skills.length ? skills.join(', ') : ''), + cvField(i18n.statusLabel || 'Status', user.status), + cvField(i18n.preferencesLabel || 'Preferences', user.preferences), + cvField(i18n.createdAtLabel || 'Created at', user.createdAt ? moment(user.createdAt).format("YYYY/MM/DD HH:mm") : ''), + user.pdf + ? div({ class: 'card-field' }, a({ href: `/blob/${encodeURIComponent(user.pdf)}`, target: '_blank', rel: 'noopener', class: 'filter-btn' }, '📄 ' + i18n.cvPdfLabel)) + : null + ].filter(Boolean); + return fields.length ? div({ class: 'cv-card-fields' }, ...fields) : null; +}; + const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) => { const isMe = user.id === currentUserId; const raw = user.visibilityPrefs || {}; - const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetBookmarks']; + const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetFiles','clearnetBookmarks','clearnetPodcasts']; const hasClearnet = raw.clearnet === true || clearnetSubKeys.some(k => raw[k] === true); const prefs = { activity: raw.activity === true, @@ -102,17 +152,14 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) wallet: raw.wallet === true, ecoTax: raw.ecoTax !== false, larpSign: raw.larpSign === true, - gpg: raw.gpg !== false, + gpg: raw.gpg === true, + phone: raw.phone === 'mutuals' || raw.phone === 'off' ? raw.phone : 'whole', + phoneDnd: raw.phoneDnd === true, clearnet: hasClearnet, fediverse: raw.fediverse === true, fediverseHandle: typeof raw.fediverseHandle === 'string' ? raw.fediverseHandle : '' }; return div({ class: 'trending-card inhabitants-card' + (isMe ? ' own-content' : '') }, - div( - { class: 'card-header activity-card-header' }, - span(), - renderContentActions(user.id, `/inhabitant/${encodeURIComponent(user.id)}`) - ), div({ class: 'card-section inhabitants-card-body' }, div({ class: 'inhabitant-card' }, div({ class: 'inhabitant-left' }, @@ -120,73 +167,44 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) { href: `/author/${encodeURIComponent(user.id)}` }, img({ class: 'inhabitant-photo-details', src: resolvePhoto(user.photo, 256), alt: user.name || 'Anonymous' }) ), + user.name ? p({ class: 'inhabitant-left-name' }, userLink(user.id, user.name)) : null, ...renderUserSensors({ isMe, fediverseConfigured, prefs, id: user.id, - karmaScore: user.karmaScore, carbonGrams: user.carbonGrams, + karmaScore: user.karmaScore, karmaReceived: user.karmaReceived || null, carbonGrams: user.carbonGrams, deviceSource: user.deviceSource, activityBucket: user.lastActivityBucket, gpgFingerprint: user.gpgFingerprint, ecoAddress: user.ecoAddress, estimatedUBI: user.estimatedUBI, lastClaimedDate: user.lastClaimedDate, totalClaimed: user.totalClaimed, - larpHouse: user.larpHouse, stats: user.stats - }, { relationshipNode: isMe ? span({ class: 'status you' }, i18n.relationshipYou) : null, excludeContent: true }), - !isMe + larpHouse: user.larpHouse, stats: user.stats, + mutual: !!(user.relationship && user.relationship.following && user.relationship.followsMe) + }, { excludeContent: true, sensorsOnly: true, dndToggle: false }), + isMe ? p({ class: 'inhabitant-left-name' }, span({ class: 'status you' }, i18n.relationshipYou)) : null, + filter === 'CVs' ? div( - { class: 'cv-actions' }, + { class: 'cv-actions doc-export-actions' }, form( - { method: 'GET', action: '/pm' }, - input({ type: 'hidden', name: 'recipients', value: user.id }), - button({ type: 'submit', class: 'btn' }, i18n.pmCreateButton) + { method: 'GET', action: `/cv/pdf/${encodeURIComponent(user.id)}` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.generatePdf) + ), + form( + { method: 'POST', action: `/cv/share/${encodeURIComponent(user.id)}` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.sharePm) ) ) : null ), - div({ class: 'inhabitant-details' }, - h2(user.name || 'Anonymous'), - user.description ? p(...renderUrl(user.description)) : null, - filter === 'MATCHSKILLS' && user.commonSkills?.length - ? div({ class: 'matchskills' }, - p(`${i18n.commonSkills}: ${user.commonSkills.join(', ')}`), - p(`${i18n.matchScore}: ${Math.round((user.matchScore || 0) * 100)}%`) - ) - : null, - filter === 'SUGGESTED' && (user.followsYou || user.commonSkills?.length || user.mutualCount) - ? div({ class: 'suggested-meta' }, - user.followsYou ? span({ class: 'suggested-badge' }, i18n.suggestedFollowsYou || 'Follows you') : null, - user.commonSkills?.length - ? p(`${i18n.commonSkills || 'Common skills'}: ${user.commonSkills.join(', ')}`) - : null, - user.mutualCount ? p(`${i18n.mutualFollowers}: ${user.mutualCount}`) : null - ) - : null, - filter === 'blocked' && user.isBlocked - ? p(i18n.blockedLabel) : null, - p(userLink(user.id)), - !isMe ? (() => { - const rel = user.relationship || {} - const blockedBoth = rel.blocking && rel.blockedBy - const mutual = rel.following && rel.followsMe - const supportAction = rel.following ? 'unfollow' : (rel.blocking ? 'unblock' : 'follow') - return div({ class: 'relationship-status inhabitant-relationship' }, - blockedBoth - ? span({ class: 'status blocked' }, i18n.relationshipMutualBlock) - : [ - rel.blocking ? span({ class: 'status blocked' }, i18n.relationshipBlocking) : null, - rel.blockedBy ? span({ class: 'status blocked-by' }, i18n.relationshipBlockedBy) : null, - mutual - ? span({ class: 'status mutual' }, i18n.relationshipMutuals) - : [ - span({ class: 'status supporting' }, rel.following ? i18n.relationshipFollowing : i18n.relationshipNone), - span({ class: 'status supported-by' }, rel.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) - ] - ], - div({ class: 'relationship-actions' }, - form({ method: 'POST', action: `/${supportAction}/${encodeURIComponent(user.id)}` }, - button({ type: 'submit', class: 'btn' }, i18n[supportAction]) + (() => { + const detailNodes = [ + filter === 'CVs' ? renderCvFields(user) : null, + filter === 'SUGGESTED' && user.commonSkills?.length + ? div({ class: 'suggested-meta' }, + p(`${i18n.commonSkills || 'Common skills'}: ${user.commonSkills.slice(0, 8).join(', ')}`) ) - ) - ) - })() : null, - renderContentStats(user.stats, i18n) - ) + : null, + filter === 'blocked' && user.isBlocked + ? p(i18n.blockedLabel) : null + ].filter(Boolean); + return detailNodes.length ? div({ class: 'inhabitant-details' }, ...detailNodes) : null; + })() ) ) ); @@ -197,7 +215,7 @@ const renderGalleryInhabitants = inhabitants => { class: "gallery" }, inhabitants.length ? inhabitants.map(u => - a({ href: `#${lightboxId(u.id)}`, class: "gallery-item" }, + a({ href: `#${lightboxId(u.id)}`, id: `${lightboxId(u.id)}-src`, class: "gallery-item" }, img({ src: resolvePhoto(u.photo, 256), alt: u.name || "Anonymous", class: "gallery-image" }) ) ) @@ -208,7 +226,7 @@ const renderLightbox = inhabitants => inhabitants.map(u => div( { id: lightboxId(u.id), class: "lightbox" }, - a({ href: "#", class: "lightbox-close" }, "×"), + a({ href: `#${lightboxId(u.id)}-src`, class: "lightbox-close" }, "×"), img({ src: resolvePhoto(u.photo, 256), class: "lightbox-image", alt: u.name || "Anonymous" }) ) ); @@ -228,59 +246,66 @@ function msgIdOf(m) { return m && (m.key || m.value?.key || m.value?.content?.root || m.value?.content?.branch || null); } -exports.inhabitantsView = (inhabitants, filter, query, currentUserId, fediverseConfigured) => { - const title = filter === 'contacts' ? i18n.yourContacts - : filter === 'CVs' ? i18n.allCVs - : filter === 'MATCHSKILLS' ? i18n.matchSkills - : filter === 'SUGGESTED' ? i18n.suggestedSectionTitle - : filter === 'blocked' ? i18n.blockedSectionTitle - : filter === 'GALLERY' ? i18n.gallerySectionTitle - : filter === 'TOP KARMA' ? i18n.topkarmaSectionTitle - : filter === 'TOP ECO' ? (i18n.topecoSectionTitle || 'Top Eco') - : filter === 'TOP ACTIVITY' ? i18n.topactivitySectionTitle - : i18n.allInhabitants; +exports.inhabitantsView = (inhabitants, filter, query, currentUserId, fediverseConfigured, filterCensus = {}) => { + const title = i18n.allInhabitants; - const showCVFilters = filter === 'CVs' || filter === 'MATCHSKILLS'; - const filters = ['all', 'TOP ACTIVITY', 'TOP KARMA', 'TOP ECO', 'contacts', 'SUGGESTED', 'blocked', 'CVs', 'MATCHSKILLS', 'GALLERY']; + const showCVFilters = filter === 'CVs'; + const bucketCounts = inhabitants.reduce((acc, u) => { + const b = u && u.lastActivityBucket; + if (b === 'green' || b === 'orange' || b === 'red') acc[b]++; + return acc; + }, { green: 0, orange: 0, red: 0 }); + const TOP_FILTERS = ['TOP ACTIVITY', 'TOP INACTIVITY', 'TOP KARMA', 'TOP ECO']; + const isTop = TOP_FILTERS.includes(filter); return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.discoverPeople) ), - div({ class: 'filters' }, - form({ method: 'GET', action: '/inhabitants' }, + renderMainFilters(filter, isTop, filterCensus), + isTop + ? div({ class: 'inhabitant-action inhabitant-subfilters' }, + ...generateFilterButtons(TOP_FILTERS, filter, (mode) => filterLabel(mode).replace(/^TOP\s+/, '')) + ) + : null, + div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(inhabitants.length, [ + { label: '<2w', count: bucketCounts.green, labelClass: 'stat-label-green' }, + { label: '2w–6m', count: bucketCounts.orange, labelClass: 'stat-label-orange' }, + { label: '>6m', count: bucketCounts.red, labelClass: 'stat-label-red' } + ]), + form({ method: 'GET', action: '/inhabitants', class: 'filter-box' }, input({ type: 'hidden', name: 'filter', value: filter }), input({ type: 'text', name: 'search', placeholder: i18n.searchInhabitantsPlaceholder, - value: (query && query.search) || '' + value: (query && query.search) || '', + class: 'filter-box__input' }), - showCVFilters - ? [ - input({ type: 'text', name: 'location', placeholder: i18n.filterLocation, value: (query && query.location) || '' }), - input({ type: 'text', name: 'language', placeholder: i18n.filterLanguage, value: (query && query.language) || '' }), - input({ type: 'text', name: 'skills', placeholder: i18n.filterSkills, value: (query && query.skills) || '' }) - ] - : null, - br(), - button({ type: 'submit' }, i18n.applyFilters) + div({ class: 'filter-box__controls' }, + showCVFilters + ? [ + input({ type: 'text', name: 'location', placeholder: i18n.filterLocation, value: (query && query.location) || '', class: 'filter-box__number' }), + input({ type: 'text', name: 'language', placeholder: i18n.filterLanguage, value: (query && query.language) || '', class: 'filter-box__number' }), + input({ type: 'text', name: 'skills', placeholder: i18n.filterSkills, value: (query && query.skills) || '', class: 'filter-box__number' }) + ] + : null, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ), - div({ class: 'inhabitant-action' }, - ...generateFilterButtons(filters, filter) - ), filter === 'GALLERY' - ? renderGalleryInhabitants(inhabitants) - : div({ class: 'inhabitants-list' }, + ? renderGalleryInhabitants(paged(inhabitants)) + : div({ class: filter === 'CVs' ? 'inhabitants-list' : 'inhabitants-list inhabitants-grid' }, inhabitants.length - ? inhabitants.map(user => renderInhabitantCard(user, filter, currentUserId, fediverseConfigured)) + ? paged(inhabitants).map(user => renderInhabitantCard(user, filter, currentUserId, fediverseConfigured)) : p({ class: 'no-results' }, i18n.noInhabitantsFound) ), - ...renderLightbox(inhabitants) + ...renderLightbox(paged(inhabitants)) ) ); }; @@ -320,7 +345,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = ]; const status = (cv && cv.status) || ''; const preferences = (cv && cv.preferences) || ''; - const createdAt = (cv && cv.createdAt) ? new Date(cv.createdAt).toLocaleString() : ''; + const createdAt = (cv && cv.createdAt) ? moment(cv.createdAt).format("YYYY/MM/DD HH:mm") : ''; const isMe = id && id === currentUserId; const title = i18n.inhabitantProfileTitle || i18n.inhabitantviewDetails; const karmaScore = typeof safe.karmaScore === 'number' ? safe.karmaScore : 0; @@ -329,7 +354,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = const totalClaimed = typeof safe.totalClaimed === 'number' ? safe.totalClaimed : 0; const ecoAddress = typeof safe.ecoAddress === 'string' ? safe.ecoAddress : null; const rawPrefs = safe.visibilityPrefs || {}; - const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetBookmarks']; + const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetFiles','clearnetBookmarks','clearnetPodcasts']; const prefs = { activity: rawPrefs.activity === true, device: rawPrefs.device === true, @@ -338,7 +363,9 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = wallet: rawPrefs.wallet === true, ecoTax: rawPrefs.ecoTax !== false, larpSign: rawPrefs.larpSign === true, - gpg: rawPrefs.gpg !== false, + gpg: rawPrefs.gpg === true, + phone: rawPrefs.phone === 'mutuals' || rawPrefs.phone === 'off' ? rawPrefs.phone : 'whole', + phoneDnd: rawPrefs.phoneDnd === true, clearnet: rawPrefs.clearnet === true || clearnetSubKeys.some(k => rawPrefs[k] === true), fediverse: rawPrefs.fediverse === true, fediverseHandle: typeof rawPrefs.fediverseHandle === 'string' ? rawPrefs.fediverseHandle : '' @@ -350,26 +377,28 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = const providedBucket = typeof safe.lastActivityBucket === 'string' ? safe.lastActivityBucket : null; const dotClass = providedBucket === 'green' ? 'green' : providedBucket === 'orange' ? 'orange' : 'red'; + const fieldNodes = [ + cvField(i18n.locationLabel, location), + cvField(i18n.languagesLabel, languages.length ? languages.join(', ').toUpperCase() : ''), + cvField(i18n.skillsLabel, skills.length ? skills.join(', ') : ''), + cvField(i18n.statusLabel || 'Status', status), + cvField(i18n.preferencesLabel || 'Preferences', preferences), + cvField(i18n.createdAtLabel || 'Created at', createdAt) + ].filter(Boolean); + const detailNodes = [ - description ? p(...renderUrl(description)) : null, - location ? p(`${i18n.locationLabel}: ${location}`) : null, - languages.length ? p(`${i18n.languagesLabel}: ${languages.join(', ').toUpperCase()}`) : null, - skills.length ? p(`${i18n.skillsLabel}: ${skills.join(', ')}`) : null, - status ? p(`${i18n.statusLabel || 'Status'}: ${status}`) : null, - preferences ? p(`${i18n.preferencesLabel || 'Preferences'}: ${preferences}`) : null, - createdAt ? p(`${i18n.createdAtLabel || 'Created at'}: ${createdAt}`) : null + description ? p(...renderStyledText(description)) : null, + fieldNodes.length ? div({ class: 'cv-card-fields' }, ...fieldNodes) : null ].filter(Boolean); return template( name, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.discoverPeople) ), - div({ class: 'mode-buttons' }, - ...generateFilterButtons(['all', 'TOP ACTIVITY', 'TOP KARMA', 'TOP ECO', 'contacts', 'SUGGESTED', 'blocked', 'CVs', 'MATCHSKILLS', 'GALLERY'], 'all') - ), + renderMainFilters('all', false), div({ class: 'inhabitant-card' }, div({ class: 'inhabitant-left' }, img({ class: 'inhabitant-photo-details', src: image, alt: name || 'Anonymous' }), @@ -377,16 +406,23 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = safe.oasisVersion ? div({ class: 'profile-side-version' }, '🌴 ' + (i18n.oasisVersionLabel || 'Oasis Version') + ': ', strong(safe.oasisVersion)) : null, ...renderUserSensors({ isMe, fediverseConfigured, prefs, id: id || viewedId, - karmaScore, carbonGrams, deviceSource: safe.deviceSource, activityBucket: providedBucket, + karmaScore, karmaReceived: safe.karmaReceived || null, carbonGrams, deviceSource: safe.deviceSource, activityBucket: providedBucket, gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed, - larpHouse, stats: safe.stats + larpHouse, stats: safe.stats, + mutual: !!(safe.relationship && safe.relationship.following && safe.relationship.followsMe) }, { excludeContent: true }), (!isMe && (id || viewedId)) - ? form( - { method: 'GET', action: '/pm' }, - input({ type: 'hidden', name: 'recipients', value: id || viewedId }), - button({ type: 'submit', class: 'btn' }, i18n.pmCreateButton) + ? div({ class: 'cv-actions doc-export-actions' }, + form( + { method: 'GET', action: '/pm' }, + input({ type: 'hidden', name: 'recipients', value: id || viewedId }), + button({ type: 'submit', class: 'filter-btn' }, `✉ ${i18n.pmCreateButton}`) + ), + renderCallButton(id || viewedId, { pam: true }) ) + : null, + (!isMe && (id || viewedId) && safe.relationship) + ? div({ class: 'inhabitant-relationship' }, renderRelationshipBlock(safe.relationship, id || viewedId)) : null ), (() => { @@ -397,15 +433,16 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = const raw = (m.value?.content?.text || '').replace(//g, ''); const parts = stripAndCollectImgs(raw); const tid = msgIdOf(m); - const visitBtn = tid - ? form({ method: 'GET', action: `/thread/${encodeURIComponent(tid)}#${encodeURIComponent(tid)}` }, - button({ type:'submit', class:'filter-btn' }, i18n.visitContent) + const actions = tid + ? div({ class: 'card-header activity-card-header' }, + span(), + renderContentActions(tid, `/thread/${encodeURIComponent(tid)}#${encodeURIComponent(tid)}`, { author: m.value?.author, reportTitle: parts.clean }) ) : null; return div({ class: 'post' }, - visitBtn, - parts.clean && parts.clean.trim() ? p(...renderUrl(parts.clean)) : null, - ...(parts.imgs || []).map(src => img({ src, class: 'post-image', alt: 'image' })) + actions, + parts.clean && parts.clean.trim() ? p(...renderStyledText(parts.clean)) : null, + ...(parts.imgs || []).map(src => renderZoomableImage(src, { imgClass: 'post-image', alt: 'image' })) ); }) ) ] @@ -420,3 +457,5 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = }; exports.lastActivityBadge = lastActivityBadge; +exports.resolvePhoto = resolvePhoto; +exports.renderInhabitantCard = renderInhabitantCard; diff --git a/src/views/invites_view.js b/src/views/invites_view.js index f58703a..8f64cb2 100644 --- a/src/views/invites_view.js +++ b/src/views/invites_view.js @@ -1,12 +1,13 @@ -const { form, button, div, h2, h3, p, section, ul, li, a, br, hr, input, label, span, table, tr, td, textarea } = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, h3, p, section, a, br, input, label, table, tr, td, textarea } = require("../server/node_modules/hyperaxe"); +const moment = require("../server/node_modules/moment"); const path = require("path"); const fs = require('fs'); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const { template, i18n } = require('./main_views'); -const homedir = require('os').homedir(); -const gossipPath = path.join(homedir, ".ssb", "gossip.json"); -const unfollowedPath = path.join(homedir, ".ssb", "gossip_unfollowed.json"); +const gossipPath = path.join(require("../server/ssb_config").path, "gossip.json"); +const unfollowedPath = require("../configs/state-manager").statePath("gossip_unfollowed.json"); +const peerHealth = require("../models/peer_health"); const encodePubLink = (key) => { let core = String(key).replace(/^@/, '').replace(/\.ed25519$/, '').replace(/-/g, '+').replace(/_/g, '/'); @@ -14,12 +15,21 @@ const encodePubLink = (key) => { return `/author/${encodeURIComponent('@' + core)}.ed25519`; }; -const snhInvitePath = path.join(__dirname, '..', 'configs', 'snh-invite-code.json'); - -let snhInvite = null; -try { - snhInvite = JSON.parse(fs.readFileSync(snhInvitePath, 'utf8')); -} catch {} +const INVITE_KIND_LABEL = { + tribes: 'invitesTribesTitle', + houses: 'invitesHousesTitle', + chats: 'invitesChatsTitle', + pads: 'invitesPadsTitle', + rooms: 'invitesRoomsTitle', + calendars: 'invitesCalendarsTitle', + events: 'invitesEventsTitle', + forum: 'invitesForumsTitle', + school: 'invitesSchoolTitle', + maps: 'invitesMapsTitle', + shops: 'invitesShopsTitle', + inhabitants: 'invitesInhabitantsTitle', + pubs: 'invitesPubsTitle' +}; const deduplicateByHost = (list) => { const seen = new Set(); @@ -31,7 +41,25 @@ const deduplicateByHost = (list) => { }); }; -const invitesView = ({ invitesEnabled, flash }) => { +const renderSuggestedPub = (entry, canJoin) => div({ class: 'snh-invite-box' }, + h3({ class: 'snh-invite-name' }, entry.name), + p({ class: 'snh-invite-name' }, entry.description), + p({ class: 'snh-invite-name' }, renderStyledText(entry.url)), + entry.createdAt ? p({ class: 'snh-invite-date' }, `${i18n.statsCreatedAt || 'Created'}: ${moment(entry.createdAt).format("YYYY/MM/DD")}`) : null, + p({ class: 'snh-invite-name snh-invite-code' }, entry.code), + entry.invites ? p({ class: 'snh-invite-date' }, `${i18n.invitesPubUses}: ${entry.invites.used}/${entry.invites.total}`) : null, + entry.exhausted + ? p({ class: 'snh-invite-name' }, i18n.invitesPubExhausted) + : canJoin + ? form({ action: '/settings/invite/accept', method: 'post' }, + input({ type: 'hidden', name: 'invite', value: entry.code }), + button({ type: 'submit' }, i18n.invitesAcceptInvite) + ) + : null +); + +const invitesView = (opts = {}) => { + const canJoin = opts.canJoin !== false; let pubs = []; let pubsValue = "false"; let unfollowed = []; @@ -60,15 +88,8 @@ const invitesView = ({ invitesEnabled, flash }) => { ? deduplicateByHost(pubs.filter(pubItem => !unfollowed.find(u => u.key === pubItem.key))) : []; - const hasError = (pubItem) => pubItem && (pubItem.error || (typeof pubItem.failure === 'number' && pubItem.failure > 0)); - - const sanitizeError = (err) => { - if (!err) return i18n.genericError || 'Unknown error'; - const firstLine = String(err).split('\n')[0].replace(/^Error:\s*/, '').trim(); - return firstLine || (i18n.genericError || 'Unknown error'); - }; - - const unreachableLabel = i18n.currentlyUnreachable || i18n.currentlyUnrecheable || 'ERROR!'; + const deadKeys = opts.deadKeys instanceof Set ? opts.deadKeys : new Set(); + const hasError = (pubItem) => !!(pubItem && deadKeys.has(peerHealth.canonicalKey(pubItem.key))); const pubTableHeader = () => tr( td({ class: 'card-label' }, 'PUB'), @@ -78,7 +99,9 @@ const invitesView = ({ invitesEnabled, flash }) => { ); const activePubs = filteredPubs.filter(pubItem => !hasError(pubItem)); - const unreachablePubs = pubs.filter(hasError); + const filter = opts.filter === 'others' ? 'others' : 'pubs'; + const suggested = Array.isArray(opts.inviteCodes) ? opts.inviteCodes : []; + const used = (Array.isArray(opts.invitesUsed) ? opts.invitesUsed : []).filter((e) => e && typeof e.href === 'string' && e.href.startsWith('/')); const renderPubTable = (items, actionFn) => table({ class: 'block-info-table invites-pubs-table' }, pubTableHeader(), @@ -96,54 +119,44 @@ const invitesView = ({ invitesEnabled, flash }) => { return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(description) + ), + div({ class: 'mode-buttons-row' }, + a({ class: filter === 'pubs' ? 'filter-btn active' : 'filter-btn', href: '/invites?filter=pubs' }, String(i18n.invitesPubsTitle).toUpperCase()), + a({ class: filter === 'others' ? 'filter-btn active' : 'filter-btn', href: '/invites?filter=others' }, String(i18n.invitesFilterOthers).toUpperCase()) ) ), - flash === 'alreadyFederated' - ? section(div({ class: 'message-banner' }, p(i18n.invitesAlreadyFederated || 'You are already federated with this pub.'))) - : null, - section( + filter === 'pubs' && suggested.length ? section( + div({ class: 'pubs-section' }, + h2(`${i18n.invitesSuggestedPubs} (${suggested.length})`), + ...suggested.map((entry) => renderSuggestedPub(entry, canJoin)) + ) + ) : null, + filter === 'pubs' ? section( div({ class: 'pubs-section' }, h2(i18n.invitesPubsTitle), - form( - { action: '/settings/invite/accept', method: 'post' }, - input({ name: 'invite', type: 'text', placeholder: i18n.invitesPubInviteCodePlaceholder, autofocus: true, required: true }), - br(), - button({ type: 'submit' }, i18n.invitesAcceptInvite) - ), - br(), - snhInvite ? div({ class: 'snh-invite-box' }, - h3({ class: 'snh-invite-name' }, snhInvite.name), - p({ class: 'snh-invite-name' }, snhInvite.description), - p({ class: 'snh-invite-name' }, renderUrl(snhInvite.url)), - snhInvite.createdAt ? p({ class: 'snh-invite-date' }, `${i18n.statsCreatedAt || 'Created'}: ${new Date(snhInvite.createdAt).toLocaleDateString()}`) : null, - form({ action: '/settings/invite/accept', method: 'post' }, - input({ type: 'hidden', name: 'invite', value: snhInvite.code }), - button({ type: 'submit', class: 'filter-btn' }, snhInvite.code) - ) - ) : null + p(i18n.invitesPubsHint), + canJoin + ? form( + { action: '/settings/invite/accept', method: 'post' }, + input({ name: 'invite', type: 'text', placeholder: i18n.invitesPubInviteCodePlaceholder, autofocus: true, required: true }), + br(), + button({ type: 'submit' }, i18n.invitesAcceptInvite) + ) + : p({ class: 'invites-join-note' }, opts.publicMode === true ? i18n.invitesJoinPubMode : i18n.invitesJoinLocalOnly) ) - ), - section( + ) : null, + filter === 'pubs' ? section( div({ class: 'federations-section' }, h2(i18n.invitesFederationsTitle || 'Federations'), + p(i18n.invitesFederationsHint), div({ class: 'conn-actions invites-pubs-actions' }, form({ action: '/invites/refresh-pubs', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsRefresh || 'Refresh')), - form({ action: '/invites/clear-unreachable', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsClearUnreachable || 'Remove unreachable')), + form({ action: '/invites/clear-unreachable', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsClearUnreachable || 'Clean')), form({ action: '/invites/export-pubs', method: 'get' }, button({ type: 'submit' }, i18n.invitesPubsExport || 'Export')) ), - form( - { action: '/invites/import-pubs', method: 'post', enctype: 'multipart/form-data', class: 'peers-import-form' }, - label({ class: 'peers-import-label' }, i18n.invitesPubsImportTitle || 'Import pubs'), - br(), - textarea({ name: 'peerList', rows: '4', placeholder: i18n.invitesPubsImportPlaceholder || 'Paste one multiserver address or invite code per line…' }), - br(), - input({ type: 'file', name: 'peerFile', accept: '.txt,text/plain' }), - br(), - button({ type: 'submit', class: 'filter-btn' }, i18n.invitesPubsImport || 'Import') - ), h2(`${i18n.invitesAcceptedInvites} (${activePubs.length})`), activePubs.length ? renderPubTable(activePubs, pubItem => @@ -153,7 +166,6 @@ const invitesView = ({ invitesEnabled, flash }) => { ) ) : p(i18n.invitesNoFederatedPubs), - hr(), h2(`${i18n.invitesUnfollowedInvites} (${unfollowed.length})`), unfollowed.length ? renderPubTable(unfollowed, pubItem => @@ -165,135 +177,51 @@ const invitesView = ({ invitesEnabled, flash }) => { ) ) : p(i18n.invitesNoUnfollowed), - hr(), - h2(`${i18n.invitesUnreachablePubs} (${unreachablePubs.length})`), - unreachablePubs.length - ? renderPubTable(unreachablePubs, pubItem => - div({ class: 'error-box' }, - p({ class: 'error-title' }, i18n.errorDetails), - p({ class: 'error-pre' }, sanitizeError(pubItem.error)) - ) + form( + { action: '/invites/import-pubs', method: 'post', enctype: 'multipart/form-data', class: 'peers-import-form' }, + label({ class: 'peers-import-label' }, i18n.invitesPubsImportTitle || 'Import pubs'), + br(), + textarea({ name: 'peerList', rows: '4', placeholder: i18n.invitesPubsImportPlaceholder || 'Paste one multiserver address or invite code per line…' }), + br(), + input({ type: 'file', name: 'peerFile', accept: '.txt,text/plain' }), + br(), + button({ type: 'submit' }, i18n.invitesPubsImport || 'Import') + ) + ) + ) : null, + filter === 'others' ? section( + div( + h2(i18n.invitesJoinTitle), + p(i18n.invitesJoinHint), + form( + { action: '/invites/join', method: 'post' }, + input({ name: 'code', type: 'text', placeholder: i18n.invitesJoinPlaceholder, required: true }), + br(), + button({ type: 'submit' }, i18n.invitesJoinButton) + ), + h2(`${i18n.invitesUsedTitle} (${used.length})`), + used.length + ? table({ class: 'block-info-table invites-used-table' }, + tr( + td({ class: 'card-label' }, i18n.invitesUsedType), + td({ class: 'card-label' }, i18n.invitesUsedName), + td({ class: 'card-label' }, i18n.invitesUsedDate) + ), + used.map((entry) => tr( + td(INVITE_KIND_LABEL[entry.kind] ? i18n[INVITE_KIND_LABEL[entry.kind]] : entry.kind), + td(a({ href: entry.href }, entry.title || entry.href)), + td(moment(entry.at).format('YYYY/MM/DD HH:mm')) + )) ) - : p(i18n.invitesNoUnreachablePubs) + : p(i18n.invitesUsedEmpty) ) - ), - section( - div({ class: 'invites-houses', id: 'invites-houses' }, - h2(i18n.invitesHousesTitle || 'Houses'), - form( - { action: '/larp/invite/redeem', method: 'post' }, - input({ type: 'hidden', name: 'returnTo', value: '/invites' }), - input({ name: 'code', type: 'text', placeholder: i18n.invitesHouseInviteCodePlaceholder || 'House invitation code', required: true, maxlength: 32 }), - br(), - button({ type: 'submit' }, i18n.invitesHouseJoinButton || 'Join House') - ) - ) - ), - section( - div({ class: 'invites-tribes', id: 'invites-tribes' }, - h2(i18n.invitesTribesTitle), - form( - { action: '/tribes/join-code', method: 'post' }, - input({ name: 'inviteCode', type: 'text', placeholder: i18n.invitesTribeInviteCodePlaceholder, required: true }), - br(), - button({ type: 'submit' }, i18n.invitesTribeJoinButton) - ) - ) - ), - section( - div({ class: 'invites-chats', id: 'invites-chats' }, - h2(i18n.invitesChatsTitle || 'Chats'), - form( - { action: '/chats/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesChatInviteCodePlaceholder || 'Enter chat invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesChatJoinButton || 'Join Chat') - ) - ) - ), - section( - div({ class: 'invites-pads', id: 'invites-pads' }, - h2(i18n.invitesPadsTitle || 'Pads'), - form( - { action: '/pads/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesPadInviteCodePlaceholder || 'Enter pad invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesPadJoinButton || 'Join Pad') - ) - ) - ), - section( - div({ class: 'invites-calendars', id: 'invites-calendars' }, - h2(i18n.invitesCalendarsTitle || 'Calendars'), - form( - { action: '/calendars/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesCalendarInviteCodePlaceholder || 'Enter calendar invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesCalendarJoinButton || 'Join Calendar') - ) - ) - ), - section( - div({ class: 'invites-events', id: 'invites-events' }, - h2(i18n.invitesEventsTitle || 'Events'), - form( - { action: '/events/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesEventInviteCodePlaceholder || 'Enter event invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesEventJoinButton || 'Join Event') - ) - ) - ), - section( - div({ class: 'invites-forums', id: 'invites-forums' }, - h2(i18n.invitesForumsTitle || 'Forums'), - form( - { action: '/forum/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesForumInviteCodePlaceholder || 'Enter forum invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesForumJoinButton || 'Join Forum') - ) - ) - ), - section( - div({ class: 'invites-maps', id: 'invites-maps' }, - h2(i18n.invitesMapsTitle || 'Maps'), - form( - { action: '/maps/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesMapInviteCodePlaceholder || 'Enter map invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesMapJoinButton || 'Join Map') - ) - ) - ), - section( - div({ class: 'invites-shops', id: 'invites-shops' }, - h2(i18n.invitesShopsTitle || 'Shops'), - form( - { action: '/shops/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesShopInviteCodePlaceholder || 'Enter shop invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesShopJoinButton || 'Join Shop') - ) - ) - ), - section( - div({ class: 'invites-inhabitants', id: 'invites-inhabitants' }, - h2(i18n.invitesInhabitantsTitle || 'Inhabitants'), - form( - { action: '/invites/inhabitant/follow', method: 'post' }, - input({ name: 'feedId', id: 'inh_oasis_id', type: 'text', placeholder: '@...=.ed25519', pattern: '@[A-Za-z0-9+/_\\-]{43}=\\.ed25519', required: true, maxlength: 56 }), - br(), - button({ type: 'submit' }, i18n.invitesInhabitantsFollow || 'Give Support') - ) - ) - ), - section( - div({ class: 'invites-peers', id: 'invites-peers' }, + ) : null, + filter === 'others' ? section( + div( h2(i18n.peers || 'Peers'), p(i18n.directConnectDescription), form({ action: '/peers/connect', method: 'post' }, - input({ type: 'text', id: 'peer_host', name: 'host', required: true, placeholder: '192.168.1.100', pattern: '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*)', title: i18n.peerHostValidation || 'Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.example.com)', maxlength: 253 }), + input({ type: 'text', id: 'peer_host', name: 'host', required: true, placeholder: '192.168.1.100', pattern: '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*)', title: i18n.peerHostValidation || 'Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.solarnethub.com)', maxlength: 253 }), br(), label({ for: 'peer_port' }, i18n.peerPort), br(), @@ -306,9 +234,10 @@ const invitesView = ({ invitesEnabled, flash }) => { button({ type: 'submit' }, i18n.connectAndFollow) ) ) - ) + ) : null ); }; exports.invitesView = invitesView; +exports.renderSuggestedPub = renderSuggestedPub; diff --git a/src/views/jobs_view.js b/src/views/jobs_view.js index 8c6694b..9d89c07 100644 --- a/src/views/jobs_view.js +++ b/src/views/jobs_view.js @@ -1,21 +1,24 @@ -const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, progress, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views") -const moment = require("../server/node_modules/moment") +const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, progress, video, audio, table, tr, td, hr } = require("../server/node_modules/hyperaxe") +const { renderZoomableImage } = require("./gallery_view") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderCallButton, clearnetItemHref, template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") +const { resolvePhoto } = require("./inhabitants_view") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") const renderMediaBlob = (value, attrs = {}) => { if (!value) return null const s = String(value).trim() if (!s) return null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) return null } @@ -23,7 +26,9 @@ const userId = config.keys.id const FILTERS = [ { key: "ALL", i18n: "jobsFilterAll", title: "jobsAllTitle" }, + { key: "RECENT", i18n: "jobsFilterRecent", title: "jobsRecentTitle" }, { key: "MINE", i18n: "jobsFilterMine", title: "jobsMineTitle" }, + { key: "TOP", i18n: "jobsFilterTop", title: "jobsTopTitle" }, { key: "APPLIED", i18n: "jobsFilterApplied", title: "jobsAppliedTitle" }, { key: "REMOTE", i18n: "jobsFilterRemote", title: "jobsRemoteTitle" }, { key: "PRESENCIAL", i18n: "jobsFilterPresencial", title: "jobsPresencialTitle" }, @@ -32,19 +37,14 @@ const FILTERS = [ { key: "EXCHANGE", i18n: "jobsFilterExchange", title: "jobsExchangeTitle" }, { key: "OPEN", i18n: "jobsFilterOpen", title: "jobsOpenTitle" }, { key: "CLOSED", i18n: "jobsFilterClosed", title: "jobsClosedTitle" }, - { key: "RECENT", i18n: "jobsFilterRecent", title: "jobsRecentTitle" }, - { key: "TOP", i18n: "jobsFilterTop", title: "jobsTopTitle" }, { key: "CV", i18n: "jobsCV", title: "jobsCVTitle" } ] -function resolvePhoto(photoField, size = 256) { - if (typeof photoField === "string" && photoField.startsWith("/image/")) return photoField - if (typeof photoField === "string" && /^&[A-Za-z0-9+/=]+\.sha256$/.test(photoField)) return `/image/${size}/${encodeURIComponent(photoField)}` - return "/assets/images/default-avatar.png" -} - const safeArr = (v) => (Array.isArray(v) ? v : []) const safeText = (v) => String(v || "").trim() +const clearnetEligible = (job) => String(job.status || "").toUpperCase() !== "CLOSED" && String(job.visibility || "PUBLIC").toUpperCase() !== "HIDDEN" +const JOB_REACH = ["PUBLIC", "HIDDEN"] +const jobReachLabel = (v) => v === "HIDDEN" ? (i18n.visibilityHidden || "Hidden") : (i18n.visibilityPublic || "Public") const parseNum = (v) => { const n = parseFloat(String(v ?? "").replace(",", ".")) @@ -72,11 +72,11 @@ const buildReturnTo = (filter, params = {}) => { const renderPmButton = (recipientId) => recipientId && String(recipientId) !== String(userId) - ? form( + ? [form( { method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: recipientId }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - ) + ), renderCallButton(recipientId, { pam: true })] : null const renderTags = (tags = []) => { @@ -104,15 +104,6 @@ const renderApplicantsProgress = (subsCount, vacants) => { ) } -const renderUpdatedLabel = (createdAt, updatedAt) => { - const createdTs = createdAt ? new Date(createdAt).getTime() : NaN - const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs) - return showUpdated - ? span({ class: "votations-comment-date" }, ` | ${i18n.jobsUpdatedAt}: ${moment(updatedAt).format("YYYY/MM/DD HH:mm:ss")}`) - : null -} - const renderJobOwnerActions = (job, returnTo) => { const isAuthor = String(job.author) === String(userId) if (!isAuthor) return [] @@ -127,11 +118,6 @@ const renderJobOwnerActions = (job, returnTo) => { { method: "GET", action: `/jobs/edit/${encodeURIComponent(job.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ class: "update-btn", type: "submit" }, i18n.jobsUpdateButton) - ), - form( - { method: "POST", action: `/jobs/delete/${encodeURIComponent(job.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.jobsDeleteButton) ) ] } @@ -208,13 +194,14 @@ const renderJobExtraDetails = (job) => { pushRow(i18n.jobsHoursRequested || "Hours requested", `${Number(job.hoursRequested) || 0} h`) if (skill) pushRow(i18n.jobsExchangeSkill || "Skill", skill) } - return table({ class: "tribe-info-table jobs-info-table" }, ...rows) + return table({ class: "tribe-info-table jobs-info-table" }, ...rows, ...(opts.includeCreated ? renderCreatedRows(job.createdAt, job.author, job.updatedAt) : [])) } const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { const list = safeArr(jobs) - if (!list.length) return p(i18n.noJobsMatch || i18n.noJobsFound) + if (!list.length) return p((params.search || params.q || params.minSalary || params.maxSalary) ? (i18n.noJobsMatch || i18n.noJobsFound) : (i18n.noJobsFound || i18n.noJobsMatch)) + const returnTo = buildReturnTo(filter, params) return div({ class: "jobs-grid" }, list.map((job) => { @@ -232,7 +219,9 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { renderJobStatusChip(job.status), isHidden ? renderJobHiddenChip() : null, isSubscribed ? renderJobAppliedChip() : null, - renderLifespanChip(job.lifetime, i18n) + job.industry ? a({ href: `/industry/${encodeURIComponent(job.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, + renderLifespanChip(job.lifetime, i18n), + job.clearnet === true && clearnetEligible(job) ? renderReachChip(true, i18n, clearnetItemHref('jobs', job.title, job.id)) : null ].filter(Boolean) const isExchange = String(job.job_type || "").toLowerCase() === "exchange" const compensationText = isExchange @@ -244,7 +233,7 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { div( { class: "card-header activity-card-header" }, span(), - renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`) + renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`, { spread: params.spreadMap && params.spreadMap.get(job.id) || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title, returnTo, deleteAction: isOwn ? contentDeleteAction("job", job.id) : null }) ), div({ class: "card-section job-card-body" }, heroNode, @@ -255,11 +244,10 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-date-highlight" }, compensationText), + div({ class: "price-chip" }, compensationText), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.jobSubscribers}: ${subs.length}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(job.id, params.spreadMap && params.spreadMap.get(job.id))) + ) ) ) ) @@ -267,10 +255,32 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { ) } -const renderJobForm = (job = {}, mode = "create") => { +const renderJobForm = (job = {}, mode = "create", spreadWarning = null, params = {}) => { const isEdit = mode === "edit" + const reachRaw = String(params.reach || "").toUpperCase() + const ownReach = String(job.visibility || "").toUpperCase() + const reach = JOB_REACH.includes(reachRaw) ? reachRaw : (JOB_REACH.includes(ownReach) ? ownReach : "PUBLIC") + const prefillKeys = ["industry", "title", "description", "tasks", "salary"] return div( { class: "div-center job-form" }, + isEdit ? spreadWarning : null, + form( + { method: "GET", action: isEdit ? `/jobs/edit/${encodeURIComponent(job.id)}` : "/jobs" }, + isEdit ? null : input({ type: "hidden", name: "filter", value: "CREATE" }), + ...(isEdit ? [] : prefillKeys.filter((k) => params[k]).map((k) => input({ type: "hidden", name: k, value: params[k] }))), + label(i18n.visibilityLabel || "Visibility"), + br(), + div({ class: "apply-row" }, + select( + { name: "visibility", class: "report-category-select" }, + option({ value: "PUBLIC", ...(reach === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(reach === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, jobReachLabel(reach)), form( { action: isEdit ? `/jobs/update/${encodeURIComponent(job.id)}` : "/jobs/create", @@ -278,6 +288,8 @@ const renderJobForm = (job = {}, mode = "create") => { enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: "/jobs?filter=MINE" }), + input({ type: "hidden", name: "visibility", value: reach }), + job.industry ? input({ type: "hidden", name: "industry", value: job.industry }) : null, label(i18n.jobType), br(), select( @@ -290,7 +302,7 @@ const renderJobForm = (job = {}, mode = "create") => { br(), label(i18n.jobTitle), br(), - input({ type: "text", name: "title", required: true, placeholder: i18n.jobTitlePlaceholder, value: job.title || "" }), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.jobTitlePlaceholder, value: job.title || "" }), br(), br(), label(i18n.jobImage), @@ -301,17 +313,17 @@ const renderJobForm = (job = {}, mode = "create") => { br(), label(i18n.jobDescription), br(), - textarea({ name: "description", rows: "6", required: true, placeholder: i18n.jobDescriptionPlaceholder }, job.description || ""), + textarea({ maxlength: "5000", name: "description", rows: "6", required: true, placeholder: i18n.jobDescriptionPlaceholder }, job.description || ""), br(), br(), label(i18n.jobRequirements), br(), - textarea({ name: "requirements", rows: "6", placeholder: i18n.jobRequirementsPlaceholder }, job.requirements || ""), + textarea({ maxlength: "5000", name: "requirements", rows: "6", placeholder: i18n.jobRequirementsPlaceholder }, job.requirements || ""), br(), br(), label(i18n.jobsTagsLabel), br(), - input({ type: "text", name: "tags", value: Array.isArray(job.tags) ? job.tags.join(", ") : (job.tags || "") }), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: Array.isArray(job.tags) ? job.tags.join(", ") : (job.tags || "") }), br(), br(), label(i18n.jobLanguages), @@ -323,22 +335,22 @@ const renderJobForm = (job = {}, mode = "create") => { br(), select( { name: "job_time", required: true }, - option({ value: "partial", selected: job.job_time === "partial" }, i18n.jobTimePartial), - option({ value: "complete", selected: job.job_time === "complete" }, i18n.jobTimeComplete) + option({ value: "partial", ...(job.job_time === "partial" ? { selected: true } : {})}, i18n.jobTimePartial), + option({ value: "complete", ...(job.job_time === "complete" ? { selected: true } : {})}, i18n.jobTimeComplete) ), br(), br(), label(i18n.jobTasks), br(), - textarea({ name: "tasks", rows: "6", placeholder: i18n.jobTasksPlaceholder }, job.tasks || ""), + textarea({ maxlength: "5000", name: "tasks", rows: "6", placeholder: i18n.jobTasksPlaceholder }, job.tasks || ""), br(), br(), label(i18n.jobLocation), br(), select( { name: "location", required: true }, - option({ value: "remote", selected: job.location === "remote" }, i18n.jobLocationRemote), - option({ value: "presencial", selected: job.location === "presencial" }, i18n.jobLocationPresencial) + option({ value: "remote", ...(job.location === "remote" ? { selected: true } : {})}, i18n.jobLocationRemote), + option({ value: "presencial", ...(job.location === "presencial" ? { selected: true } : {})}, i18n.jobLocationPresencial) ), br(), br(), @@ -347,15 +359,7 @@ const renderJobForm = (job = {}, mode = "create") => { input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: job.mapUrl || "" }), br(), br(), - label(i18n.visibilityLabel || "Visibility"), - br(), - select( - { name: "visibility" }, - option({ value: "PUBLIC", selected: (job.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", selected: job.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") - ), - br(), - br(), + reach === "PUBLIC" && clearnetEligible({ ...job, visibility: reach }) ? renderClearnetSelector(job.clearnet === true || String(job.clearnet || "") === "1", i18n) : null, label(i18n.jobVacants), br(), input({ type: "number", name: "vacants", min: "1", placeholder: i18n.jobVacantsPlaceholder, value: job.vacants || 1, required: true }), @@ -366,6 +370,7 @@ const renderJobForm = (job = {}, mode = "create") => { label(i18n.jobSalary), br(), input({ type: "number", name: "salary", step: "0.000001", min: "0", value: job.salary || "" }), + renderEcoValueChip(), br(), br() ] @@ -415,11 +420,26 @@ const renderCVList = (inhabitants) => ), div( { class: "inhabitant-details" }, - user.description ? p(...renderUrl(user.description)) : null, + user.description ? p(...renderStyledText(user.description)) : null, p(userLink(user.id)), + user.skills && user.skills.length + ? div({ class: "card-tags" }, + user.skills.slice(0, 12).map((skill) => + a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)) + ) + : null, + typeof user.matchScore === "number" + ? div({ class: "cv-match-row" }, + span({ class: "card-label" }, `${i18n.matchScore}: `), + span({ class: "card-value" }, `${Math.round(user.matchScore * 100)}%`) + ) + : null, div( { class: "cv-actions" }, - form({ method: "GET", action: `/inhabitant/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.inhabitantviewDetails)), + form({ method: "GET", action: `/inhabitant/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.cvVisitButton)), + user.pdf ? a({ href: `/blob/${encodeURIComponent(user.pdf)}`, target: "_blank", rel: "noopener", class: "filter-btn" }, "📄 " + i18n.cvPdfLabel) : null, + form({ method: "GET", action: `/cv/pdf/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.generatePdf)), + form({ method: "POST", action: `/cv/share/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)), !isMe ? renderPmButton(user.id) : null ) ) @@ -429,25 +449,41 @@ const renderCVList = (inhabitants) => ) exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { + if (filter === "EDIT") { + const editing = Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : jobsOrCVs + params = { ...params, spreadWarning: await renderSpreadEditWarning(editing && editing.id) } + } const search = safeText(params.search || "") const minSalary = params.minSalary ?? "" const maxSalary = params.maxSalary ?? "" const sort = safeText(params.sort || "recent") + const emptyMod = moduleIsEmpty(Array.isArray(jobsOrCVs) ? jobsOrCVs : [], filter, "ALL", search || String(minSalary || "") || String(maxSalary || "")); + const censusJ = Array.isArray(params.censusList) ? params.censusList : (Array.isArray(jobsOrCVs) ? jobsOrCVs : []); + const jobChip = (x) => { + const m = x.key; + if (m === filter) return true; + if (m === "TOP") return censusJ.length > 0; + if (m === "MINE") return censusJ.some(j => String(j.author) === String(userId)); + if (m === "RECENT") return censusJ.length > 0; + if (m === "APPLIED") return censusJ.some(j => safeArr(j.subscribers).includes(userId)); + if (m === "REMOTE") return censusJ.some(j => String(j.location || "").toLowerCase() === "remote"); + if (m === "PRESENCIAL") return censusJ.some(j => String(j.location || "").toLowerCase() === "presencial"); + if (m === "FREELANCER" || m === "EMPLOYEE" || m === "EXCHANGE") return censusJ.some(j => String(j.job_type || "").toUpperCase() === m); + if (m === "OPEN" || m === "CLOSED") return censusJ.some(j => String(j.status || "OPEN").toUpperCase() === m); + if (m === "CV") return params.anyCVs === true; + return true; + }; const filterObj = FILTERS.find((f) => f.key === filter) || FILTERS[0] const sectionTitle = i18n[filterObj.title] || i18n.jobsTitle - const { renderReachChip: renderReachChipJobs } = require('./clearnet_view'); - const viewerClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetJobs) return template( i18n.jobsTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(sectionTitle), p(i18n.jobsDescription) ), - div({ class: "shop-title-row" }, renderReachChipJobs(viewerClearnet, i18n)), - br(), div( { class: "filters" }, form( @@ -456,9 +492,9 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { input({ type: "hidden", name: "minSalary", value: String(minSalary ?? "") }), input({ type: "hidden", name: "maxSalary", value: String(maxSalary ?? "") }), input({ type: "hidden", name: "sort", value: sort }), - ...FILTERS.map((f) => - button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, i18n[f.i18n]) - ), + ...(emptyMod ? [] : FILTERS.filter(jobChip).map((f) => + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + )), button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob) ) ) @@ -477,100 +513,54 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { ) ), br(), - renderCVList(jobsOrCVs) + renderCVList(paged(jobsOrCVs)) ) : filter === "CREATE" || filter === "EDIT" ? (() => { - const jobToEdit = filter === "EDIT" ? (Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : {}) : {} - return renderJobForm(jobToEdit, filter === "EDIT" ? "edit" : "create") + const jobToEdit = filter === "EDIT" + ? (Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : {}) + : { + ...(params.industry ? { industry: params.industry } : {}), + ...(params.title ? { title: params.title } : {}), + ...(params.description ? { description: params.description } : {}), + ...(params.tasks ? { tasks: params.tasks } : {}), + ...(params.salary ? { salary: params.salary } : {}), + ...(params.draft || {}) + } + return renderJobForm(jobToEdit, filter === "EDIT" ? "edit" : "create", params.spreadWarning, params) })() : section( - div( - { class: "jobs-search" }, + emptyMod ? null : div( + { class: "jobs-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(jobsOrCVs, j => String(j.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.jobsFilterOpen }, { value: 'CLOSED', label: i18n.jobsFilterClosed }]), form( { method: "GET", action: "/jobs", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter || "ALL" }), input({ type: "text", name: "search", value: search, placeholder: i18n.jobsSearchPlaceholder, class: "filter-box__input" }), - div( - { class: "filter-box__controls" }, - div( - { class: "transfer-range" }, - input({ type: "number", name: "minSalary", step: "0.000001", min: "0", value: String(minSalary ?? ""), placeholder: i18n.jobsMinSalaryLabel, class: "filter-box__number transfer-amount-input" }), - input({ type: "number", name: "maxSalary", step: "0.000001", min: "0", value: String(maxSalary ?? ""), placeholder: i18n.jobsMaxSalaryLabel, class: "filter-box__number transfer-amount-input" }) - ), - select( - { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.jobsSortRecent), - option({ value: "salary", selected: sort === "salary" }, i18n.jobsSortSalary), - option({ value: "subscribers", selected: sort === "subscribers" }, i18n.jobsSortSubscribers) - ), - button({ type: "submit", class: "filter-box__button" }, i18n.jobsSearchButton) - ) + input({ type: "number", name: "minSalary", step: "0.000001", min: "0", value: String(minSalary ?? ""), placeholder: i18n.jobsMinSalaryLabel, class: "filter-box__number transfer-amount-input" }), + input({ type: "number", name: "maxSalary", step: "0.000001", min: "0", value: String(maxSalary ?? ""), placeholder: i18n.jobsMaxSalaryLabel, class: "filter-box__number transfer-amount-input" }), + select( + { name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.jobsSortRecent), + option({ value: "salary", ...(sort === "salary" ? { selected: true } : {})}, i18n.jobsSortSalary), + option({ value: "subscribers", ...(sort === "subscribers" ? { selected: true } : {})}, i18n.jobsSortSubscribers) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.jobsSearchButton) ) ), - br(), - div({ class: "jobs-list" }, renderJobList(jobsOrCVs, filter, { ...params, search, minSalary, maxSalary, sort })) + div({ class: "jobs-list" }, renderJobList(paged(jobsOrCVs), filter, { ...params, search, minSalary, maxSalary, sort })) ) ) ) } const renderJobCommentsSection = (jobId, returnTo, comments = []) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text - return t && String(t).trim() - }) - const commentsCount = list.length - - return div( - { id: "comments", class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/jobs/${encodeURIComponent(jobId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || "" - const ts = c?.value?.timestamp || c?.timestamp - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "" - const relDate = ts ? moment(ts).fromNow() : "" - const userName = author && author.includes("@") ? author.split("@")[1] : author - const rootId = c?.value?.content ? (c.value.content.fork || c.value.content.root) : null - const text = c?.value?.content?.text || "" - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ) - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ) -} + return renderSharedCommentsSection({ + action: `/jobs/${encodeURIComponent(jobId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; const renderCandidates = (candidates, jobId) => { if (!Array.isArray(candidates) || candidates.length === 0) return null; @@ -595,7 +585,7 @@ const renderCandidates = (candidates, jobId) => { ), div( { class: "inhabitant-details" }, - c.description ? p(...renderUrl(c.description)) : null, + c.description ? p(...renderStyledText(c.description)) : null, p(userLink(c.id)), div({ class: "matchskills" }, p(`${i18n.matchScore || 'Match score'}: ${Math.round(c.matchScore * 100)}%`), @@ -613,7 +603,8 @@ const renderCandidates = (candidates, jobId) => { input({ type: 'hidden', name: 'subject', value: `${i18n.jobsTitle || 'Job'}: ${job.title || ''}`.slice(0, 150) }), input({ type: 'hidden', name: 'text', value: `${i18n.jobsCandidatesPmBody || 'Hi, I think your profile matches my job opening'}: /jobs/${jobId}` }), button({ type: 'submit', class: 'filter-btn' }, i18n.pmCreateButton || 'Send PM') - ) + ), + renderCallButton(c.id, { pam: true }) ) ) )) @@ -627,21 +618,22 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) const tagsNode = renderTags(job.tags) const isAuthor = String(job.author) === String(userId) const isSubscribed = subs.includes(userId) - const { renderReachChip } = require('./clearnet_view') - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetJobs && String(job.status || '').toUpperCase() !== 'CLOSED' && String(job.visibility || 'PUBLIC').toUpperCase() !== 'HIDDEN') + const canClearnet = clearnetEligible(job) + const isClearnet = canClearnet && !!job.clearnet const candidatesBlock = isAuthor ? renderCandidates(params.candidates || [], job.id) : null const ownerActions = renderJobOwnerActions(job, returnTo) const applyToggle = renderJobApplyToggle(job, returnTo) - const pmBtn = renderPmButton(job.author) const visibility = (job.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC' const chips = [ renderJobStatusChip(job.status), visibility === 'HIDDEN' ? renderJobHiddenChip() : null, isSubscribed ? renderJobAppliedChip() : null, + job.industry ? a({ href: `/industry/${encodeURIComponent(job.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(job.lifetime, i18n), renderEcoTax(job.msgSize, job.id), - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('jobs', job.title, job.id) : null), + isAuthor && canClearnet ? renderClearnetSwitch('jobs', job.rootId || job.id, isClearnet) : null ].filter(Boolean) const nextVisibility = visibility === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC' @@ -660,19 +652,20 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) const sideActions = [] if (applyToggle) sideActions.push(applyToggle) - if (pmBtn) sideActions.push(pmBtn) for (const a of ownerActions) sideActions.push(a) const jobSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`, { spread: params.spreads || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title, returnTo, deleteAction: isAuthor ? contentDeleteAction("job", job.id) : null }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(job.title) || i18n.jobsTitle) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(job.id, params.spreads)), job.image ? renderMediaBlob(job.image, { class: "tribe-detail-image" }) : null, - renderJobInfoTable(job, { includeVacants: true, includeSubscribers: false }), + renderJobInfoTable(job, { includeVacants: true, includeSubscribers: false, includeCreated: true }), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.jobSubscribers}: ${subs.length}`) ), @@ -687,7 +680,7 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) safeText(bodyText) ? div({ class: "job-section" }, h2({ class: "job-section-title" }, titleText), - p({ class: "tribe-side-description" }, ...renderUrl(bodyText)) + p({ class: "tribe-side-description" }, ...renderStyledText(bodyText)) ) : null @@ -698,17 +691,13 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) extraDetailsTable ? div({ class: "job-section" }, extraDetailsTable) : null, renderJobSection(i18n.jobTasks, job.tasks), job.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, job.mapUrl)) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(job.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(job.author), - renderUpdatedLabel(job.createdAt, job.updatedAt) - ), candidatesBlock ? candidatesBlock : null, renderJobCommentsSection(job.id, returnTo, comments) ) return template( i18n.jobsTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.jobsTitle), p(i18n.jobsDescription))), section( div( { class: "filters" }, @@ -719,7 +708,7 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) input({ type: "hidden", name: "maxSalary", value: String(params.maxSalary ?? "") }), input({ type: "hidden", name: "sort", value: safeText(params.sort || "recent") }), ...FILTERS.map((f) => - button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, i18n[f.i18n]) + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) ), button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob) ) @@ -730,17 +719,19 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) } exports.clearnetJobView = async (job) => { - const { escapeHtml: esc, blobUrl: cnBlob, renderClearnetPage } = require('./clearnet_view'); - const title = esc(job.title || 'Job'); - const desc = esc(job.description || ''); + const { escapeHtml: esc, renderRichText, renderKindTag, blobUrl: cnBlob, renderClearnetPage } = require('./clearnet_view'); + const title = esc(job.title || i18n.cnKindJob); + const desc = renderRichText(job.description || ''); const req = esc(job.requirements || ''); const lang = esc(String(job.languages || '').toUpperCase()); const loc = esc(String(job.location || '').toUpperCase()); const jobType = String(job.job_type || '').toLowerCase(); - const jobTypeLabel = jobType === 'exchange' ? 'Hour exchange' : jobType === 'employee' ? 'Employee' : 'Freelancer'; + const jobTime = String(job.job_time || '').toLowerCase(); + const jobTimeLabel = jobTime === 'partial' ? i18n.jobTimePartial : jobTime === 'complete' ? i18n.jobTimeComplete : ''; + const jobTypeLabel = jobType === 'exchange' ? i18n.jobTypeExchange : jobType === 'employee' ? i18n.jobTypeSalary : i18n.jobTypeFreelance; let compensation = ''; if (jobType === 'exchange') { - compensation = `${Number(job.hoursOffered || 0)}h offered · ${Number(job.hoursRequested || 0)}h requested`; + compensation = `${esc(i18n.jobsHoursOffered)}: ${Number(job.hoursOffered || 0)}h · ${esc(i18n.jobsHoursRequested)}: ${Number(job.hoursRequested || 0)}h`; if (job.exchangeSkill) compensation += ` · ${esc(job.exchangeSkill)}`; } else { compensation = `${parseFloat(job.salary || 0).toFixed(2)} ECO`; @@ -750,7 +741,6 @@ exports.clearnetJobView = async (job) => { .cn-job-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} .cn-job-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} .cn-job-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} -.cn-job-comp{background:var(--bg-sub);border:1px solid var(--fg);color:var(--fg);padding:8px 16px;border-radius:6px;font-weight:600;display:inline-block;margin-bottom:20px} .cn-job-img{display:block;max-width:100%;border:1px solid var(--border);border-radius:8px;margin-bottom:20px} .cn-job-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:24px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)} .cn-job-section p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:15px} @@ -758,18 +748,21 @@ exports.clearnetJobView = async (job) => { const body = `

${title}

- 💼 ${jobTypeLabel} + ${renderKindTag('job')} + 💼 ${esc(jobTypeLabel)} + ${jobTimeLabel ? `⏱ ${esc(jobTimeLabel)}` : ''} ${job.createdAt ? `📅 ${esc(new Date(job.createdAt).toISOString().slice(0,10))}` : ''} ${loc ? `📍 ${loc}` : ''} ${lang ? `🗣 ${lang}` : ''} + ${compensation ? `${compensation}` : ''}
-
${compensation}
+
${jobImg ? `${title}` : ''} - ${desc ? `

Description

${desc}

` : ''} - ${req ? `

Requirements

${req}

` : ''} + ${desc ? `

${esc(i18n.description)}

${desc}

` : ''} + ${req ? `

${esc(i18n.jobRequirements)}

${req}

` : ''} `; return renderClearnetPage({ - title: `${job.title || 'Job'} — Oasis`, + title: `${job.title || 'Job'} | Oasis`, ogTitle: job.title || 'Job', ogDescription: job.description || '', ogImage: jobImg, diff --git a/src/views/larp_view.js b/src/views/larp_view.js index b9de0b5..fe69c1f 100644 --- a/src/views/larp_view.js +++ b/src/views/larp_view.js @@ -1,5 +1,9 @@ -const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink } = require("./main_views"); +const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong, details, summary } = require("../server/node_modules/hyperaxe"); +const { renderStyledText } = require("../backend/renderStyledText"); +const { template, i18n, userLink, paged, renderIcon } = require("./main_views"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; const moment = require("../server/node_modules/moment"); const fmtCycle = (c) => c && c.formatted ? c.formatted : ''; @@ -11,16 +15,24 @@ const renderCycleBanner = (cycle) => div({ class: 'larp-cycle-banner' }, span({ class: 'larp-cycle-value' }, fmtCycle(cycle)) ); -const renderHouseBadges = ({ myHouse, governingHouse }) => div({ class: 'larp-house-badges' }, - myHouse ? div({ class: 'larp-my-house' }, - span({ class: 'larp-my-house-label' }, i18n.larpMyHouse || 'My House'), - a({ href: `/larp/${myHouse.key}`, class: 'larp-my-house-link' }, myHouse.name) - ) : null, - governingHouse ? div({ class: 'larp-my-house' }, - span({ class: 'larp-my-house-label' }, i18n.larpGoverning || 'Governing'), - a({ href: `/larp/${governingHouse.key}`, class: 'larp-my-house-link' }, governingHouse.name) - ) : null -); +const renderHouseBadges = ({ myHouse, governingHouse, houses }) => { + const academia = Array.isArray(houses) ? houses.find(h => h.key === 'academia') : null; + const isNewcomer = !myHouse; + return div({ class: 'larp-house-badges' }, + myHouse ? div({ class: 'larp-my-house' }, + span({ class: 'larp-my-house-label' }, i18n.larpMyHouse || 'My House'), + a({ href: `/larp/${myHouse.key}`, class: 'larp-my-house-link' }, myHouse.name) + ) : null, + governingHouse ? div({ class: 'larp-my-house' }, + span({ class: 'larp-my-house-label' }, i18n.larpGoverning || 'Ruling:'), + a({ href: `/larp/${governingHouse.key}`, class: 'larp-my-house-link' }, governingHouse.name) + ) : null, + isNewcomer && academia ? div({ class: 'larp-my-house larp-academia-hint' }, + span({ class: 'larp-my-house-label' }, i18n.larpStartHere), + a({ href: `/larp/${academia.key}`, class: 'larp-my-house-link' }, academia.name) + ) : null + ); +}; const renderHouseNav = (houses, currentKey) => div({ class: 'larp-house-nav' }, span({ class: 'larp-house-nav-label' }, i18n.larpAllHouses || 'Houses:'), @@ -31,7 +43,7 @@ const renderHouseNav = (houses, currentKey) => div({ class: 'larp-house-nav' }, }, h.name)) ); -const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling }) => { +const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling, canTakeTest = false }) => { const { renderEncryptedChip } = require('./clearnet_view'); const isInHouse = viewerHouseKey === house.key; const isInLarp = viewerHouseKey !== null && viewerHouseKey !== undefined; @@ -66,6 +78,9 @@ const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cycl ), p({ class: 'larp-detail-description' }, house.description), div({ class: 'larp-actions' }, + isAcademia && isInHouse && canTakeTest + ? a({ href: '/larp/test', class: 'filter-btn larp-start-journey' }, i18n.larpStartJourney) + : null, isInHouse ? a({ href: `/larp/tribe/${encodeURIComponent(house.key)}`, class: 'filter-btn' }, i18n.larpVisitTribe || 'Visit Tribe') : null, @@ -162,53 +177,56 @@ const renderAcademiaJoinPanel = (allHouses, testStatus, housesById, questions, m div({ class: 'larp-academia-grid' }, ordered.map(h => a({ href: `/larp/${h.key}`, class: h.key === myKey ? 'larp-academia-thumb larp-academia-thumb-mine' : 'larp-academia-thumb' }, img({ src: houseImageSrc(h), alt: h.name, class: 'larp-academia-thumb-image' }), - span({ class: 'larp-academia-thumb-name' }, h.name) + span({ class: 'larp-academia-thumb-name' }, h.name), + table({ class: 'larp-info-table larp-academia-thumb-table' }, + tr(td({ class: 'card-label' }, i18n.larpRolesLabel || 'Roles'), td({ class: 'card-value' }, h.roles)), + tr(td({ class: 'card-label' }, i18n.larpFunctionLabel || 'Function'), td({ class: 'card-value' }, h.function)), + tr(td({ class: 'card-label' }, i18n.larpMembersCount || 'Members'), td({ class: 'card-value' }, String(h.memberCount || 0))) + ) )) ), - canTake && qs.length - ? [ - h2(i18n.larpWillTestTitle || 'WILL test'), - p({ class: 'larp-will-test-hint' }, i18n.larpWillTestHint || 'Once completed, you will be assigned the house that best matches your answers. Your individual answers are processed locally and never stored — only the resulting house assignment is published to your feed.'), - form({ method: 'POST', action: '/larp/test', class: 'larp-test-form larp-test-form-compact' }, - qs.map((q, idx) => div({ class: 'larp-test-question' }, - p({ class: 'larp-test-q-text' }, strong(`${idx + 1}. `), i18n[q.key] || q.question), - div({ class: 'larp-test-options' }, - q.options.map((opt, oi) => label({ class: 'larp-test-option' }, - input({ type: 'radio', name: `q${idx}`, value: String(oi), required: 'required' }), - ' ', i18n[opt.key] || opt.text - )) - ) - )), - div({ class: 'larp-actions' }, - button({ type: 'submit', class: 'filter-btn' }, i18n.larpTestSubmit || 'Join House') - ) - ) - ] - : null ); }; -const renderPostsBlock = (posts, house, canPost) => div({ class: 'larp-posts-block' }, - h2(i18n.larpPostsTitle || 'Wall'), - canPost - ? form({ method: 'POST', action: '/larp/post', class: 'larp-post-form' }, - input({ type: 'hidden', name: 'house', value: house.key }), - textarea({ id: 'larp_post_text', name: 'text', rows: '3', maxlength: '4000', placeholder: i18n.larpPostPlaceholder || 'What does this house need to say?' }), - button({ type: 'submit', class: 'filter-btn larp-post-submit' }, i18n.larpPostSubmit || 'Publish') - ) - : null, - posts.length === 0 - ? p({ class: 'empty' }, i18n.larpPostsEmpty || 'No posts yet.') - : div({ class: 'larp-posts-list' }, - posts.map(post => div({ class: 'larp-post' }, - div({ class: 'larp-post-head' }, - userLink(post.author), - span({ class: 'larp-post-time' }, moment(post.createdAt).format('YYYY-MM-DD HH:mm')) - ), - p({ class: 'larp-post-text' }, post.text) - )) - ) -); + +const renderPostsBlock = (posts, house, canPost) => { + const stream = (Array.isArray(posts) ? posts : []) + .map(post => ({ ts: Date.parse(post.createdAt) || post.ts || 0, post })) + .sort((a, b) => b.ts - a.ts); + + return div({ class: 'larp-posts-block' }, + h2(i18n.larpPostsTitle || 'Wall'), + canPost + ? form({ method: 'POST', action: '/larp/post', class: 'larp-post-form' }, + input({ type: 'hidden', name: 'house', value: house.key }), + textarea({ id: 'larp_post_text', name: 'text', rows: '3', maxlength: '4000', placeholder: i18n.larpPostPlaceholder || 'What does this house need to say?' }), + button({ type: 'submit', class: 'filter-btn larp-post-submit' }, i18n.larpPostSubmit || 'Publish') + ) + : null, + stream.length === 0 + ? p({ class: 'empty' }, i18n.larpPostsEmpty || 'No posts yet.') + : div({ class: 'larp-posts-list' }, + paged(stream).map(entry => div({ class: entry.post.author === userId ? 'larp-post own-comment' : 'larp-post' }, + entry.post.author === userId && entry.post.id + ? form({ method: 'POST', action: `/larp/post/delete/${encodeURIComponent(entry.post.id)}`, class: 'comment-delete-form' }, + button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close'))) + : null, + div({ class: 'larp-post-head' }, + userLink(entry.post.author), + span({ class: 'larp-post-time' }, moment(entry.post.createdAt).format('YYYY/MM/DD HH:mm')) + ), + p({ class: 'larp-post-text' }, ...renderStyledText(String(entry.post.text || ''))) + )) + ) + ); +}; + +const matchesHouse = (house, q) => { + const needle = String(q || '').trim().toLowerCase(); + if (!needle) return true; + return ['name', 'motto', 'roles', 'function', 'description'] + .some(f => String(house[f] || '').toLowerCase().includes(needle)); +}; const renderModeButtons = (filter) => div({ class: 'mode-buttons stats-mode-row' }, ['ruling', 'houses', 'rules'].map(m => @@ -226,33 +244,29 @@ const renderModeButtons = (filter) => div({ class: 'mode-buttons stats-mode-row' ) ); -const renderRules = () => div({ class: 'larp-rules' }, - h2(i18n.larpRulesTitle || 'F.A.Q.'), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesEntryTitle || 'Starting house'), - p(i18n.larpRulesEntryText || 'Every inhabitant begins in ACADEMIA by default.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesOneHouseTitle || 'One house at a time'), - p(i18n.larpRulesOneHouseText || 'You can belong to at most one house at any given moment. If you belong to none, you are in ACADEMIA. Every non-ACADEMIA house page shows a "Leave House" button to its members that resets membership back to ACADEMIA. Leaving does NOT waive the test cooldown.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesTestEntry || 'WILL test'), - p(i18n.larpRulesTestEntryText || 'Each option weights one or more houses; when you submit, the system tallies the scores and auto-admits you into the highest-scoring house.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesInviteEntry || 'Invitation code'), - p(i18n.larpRulesInviteEntryText || 'House members can generate invitation codes that grant direct access to the house.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesGovernanceTitle || 'Governance Wall'), - p(i18n.larpRulesGovernanceText || 'During its cycle, the governing house wears the "Ruling" badge and is the only one whose Wall is surfaced publicly under the RULING tab.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesSignTitle || 'L.A.R.P. Emblem'), - p(i18n.larpRulesSignText || 'Inhabitants can choose whether to add a sensor on their avatar with the emblem of their current house.') - ) -); +const renderRules = () => { + const points = [ + i18n.larpRulesEntryText, + i18n.larpRulesOneHouseText, + i18n.larpRulesTestEntryText, + i18n.larpRulesInviteEntryText, + i18n.larpRulesGovernanceText, + i18n.larpRulesWallText, + i18n.larpRulesSignText + ]; + return div( + { class: 'card' }, + h2(i18n.larpRulesTitle || 'F.A.Q.'), + div({ class: 'rules-points' }, + points.filter(Boolean).map((text, i) => + div({ class: 'rules-point' }, + span({ class: 'rules-point-num' }, String(i + 1)), + span({ class: 'rules-point-text' }, text) + ) + ) + ) + ); +}; const renderHousesGrid = (houses, myHouseKey, governingKey) => { const inLarp = !!myHouseKey; @@ -277,7 +291,9 @@ const renderHousesGrid = (houses, myHouseKey, governingKey) => { ), p({ class: 'larp-card-motto' }, '“' + h.motto + '”'), p({ class: 'larp-card-roles' }, h.roles), - p({ class: 'larp-card-count' }, `${i18n.larpMembersCount || 'Members'}: ${h.memberCount || 0}`), + div({ class: 'tribe-card-members' }, + span({ class: 'tribe-members-count' }, `${i18n.larpMembersCount || 'Members'}: ${h.memberCount || 0}`) + ), h.openInviteCode ? p({ class: 'larp-card-count' }, i18n.tribeInviteCodeText, span({ class: 'tribe-open-invite-code' }, h.openInviteCode)) : null ) ); @@ -285,24 +301,28 @@ const renderHousesGrid = (houses, myHouseKey, governingKey) => { ); }; -exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost }) => { +exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, q }) => { const title = i18n.larpTitle || 'L.A.R.P.'; const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.'; const myHouse = houses.find(h => h.key === myHouseKey) || null; - const mode = filter === 'houses' ? 'houses' : filter === 'rules' ? 'rules' : 'ruling'; + const search = String(q || '').trim(); + const mode = search ? 'houses' : filter === 'houses' ? 'houses' : filter === 'rules' ? 'rules' : 'ruling'; + const matched = search ? houses.filter(h => matchesHouse(h, search)) : houses; return template( title, section( - div({ class: 'tags-header' }, - h1(title), + div({ class: 'tags-header module-header-line' }, + h2(title), p(description) ), - renderCycleBanner(cycle), - renderHouseBadges({ myHouse, governingHouse }), renderModeButtons(mode), + renderCycleBanner(cycle), + renderHouseBadges({ myHouse, governingHouse, houses }), mode === 'houses' - ? renderHousesGrid(houses, myHouseKey, governingKey) + ? (matched.length + ? renderHousesGrid(matched, myHouseKey, governingKey) + : div({ class: 'no-content-box' }, p({ class: 'no-content' }, i18n.larpNoHousesMatch))) : mode === 'rules' ? renderRules() : [ @@ -330,7 +350,8 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous const isAcademia = house.key === 'academia'; const viewerInAcademia = myHouseKey === 'academia'; const viewerIsMember = myHouseKey === house.key && house.key !== 'academia'; - const showWall = viewerIsMember || house.key === governingKey; + const canWriteWall = myHouseKey === house.key; + const showWall = viewerIsMember || house.key === governingKey || house.key === 'academia'; const cycles = computeCyclesUntilRuling(house, governingKey); const housesById = Object.fromEntries(houses.map(h => [h.key, h])); @@ -342,9 +363,14 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous p({ class: 'larp-detail-motto' }, '“' + house.motto + '”') ), renderCycleBanner(cycle), - renderHouseBadges({ myHouse, governingHouse }), + renderHouseBadges({ myHouse, governingHouse, houses }), renderHouseNav(houses, house.key), - renderHousePanel(house, members, posts, { canPost, viewerHouseKey: myHouseKey, cyclesUntilRuling: cycles }), + renderHousePanel(house, members, posts, { + canPost, + viewerHouseKey: myHouseKey, + cyclesUntilRuling: cycles, + canTakeTest: isAcademia && viewerInAcademia && !!(testStatus && testStatus.allowed) + }), inviteCode ? div({ class: 'larp-invite-banner' }, p({ class: 'larp-invite-banner-title' }, strong(i18n.larpInviteBannerTitle || 'New invitation code')), @@ -352,10 +378,10 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous p({ class: 'larp-invite-banner-code' }, inviteCode) ) : null, + null, showWall - ? renderPostsBlock(posts, house, viewerIsMember) - : null, - isAcademia && viewerInAcademia ? renderAcademiaJoinPanel(houses, testStatus, housesById, questions, myHouseKey) : null + ? renderPostsBlock(posts, house, canWriteWall) + : null ) ); }; @@ -363,17 +389,19 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous exports.larpTestView = ({ questions, cycle, houses, myHouseKey, governingKey, testStatus }) => { const myHouse = houses.find(h => h.key === myHouseKey) || null; const governingHouse = houses.find(h => h.key === governingKey) || null; - const titleText = i18n.larpTestTitle || 'Profile test'; + const title = i18n.larpTitle || 'L.A.R.P.'; + const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.'; return template( - titleText, + title, section( div({ class: 'tags-header' }, - h1(titleText), - p(i18n.larpTestIntro || 'Answer the psychological questions. The system will pick the house that best fits your answers. You can attempt one test every 30 cycles.') + h1(title), + p(description) ), renderCycleBanner(cycle), - renderHouseBadges({ myHouse, governingHouse }), + renderHouseBadges({ myHouse, governingHouse, houses }), + renderModeButtons(''), testStatus && !testStatus.allowed ? div({ class: 'larp-test-cooldown' }, p((i18n.larpTestCooldownActive || 'Next test available in') + ' ' + formatCooldown(testStatus.nextAt - Date.now()) + ' ' + (i18n.larpTestCooldownDays || 'cycles')), @@ -409,7 +437,7 @@ exports.larpTestResultView = ({ house, result, cycle, houses, myHouseKey, govern section( div({ class: 'tags-header' }, h1(titleText)), renderCycleBanner(cycle), - renderHouseBadges({ myHouse, governingHouse }), + renderHouseBadges({ myHouse, governingHouse, houses }), div({ class: 'larp-test-result' }, ranking.length ? div({ class: 'larp-test-ranking' }, diff --git a/src/views/legacy_view.js b/src/views/legacy_view.js deleted file mode 100644 index 2a8964c..0000000 --- a/src/views/legacy_view.js +++ /dev/null @@ -1,65 +0,0 @@ -const { form, button, div, h2, p, section, input, label, br } = require("../server/node_modules/hyperaxe"); -const { template, i18n } = require('./main_views'); - -const crypto = require('crypto'); - -function generateRandomPassword(length = 32) { - return crypto.randomBytes(length).toString('hex').slice(0, length); -} - -const legacyView = async () => { - const randomPassword = generateRandomPassword(); - - const header = div({ class: 'tags-header' }, - h2(i18n.legacyTitle), - p(i18n.legacyDescription) - ); - - return template( - i18n.legacyTitle, - section( - header, - p({ id: "randomPassword" }, h2({ class: "generated-password" }, randomPassword)), - div({ class: "div-center legacy-section" }, - p(i18n.exportDescription), - form( - { - action: "/legacy/export", - method: "POST", - id: "exportForm" - }, - label(i18n.exportPasswordLabel), - input({ - type: "password", - name: "password", - id: "password", - required: true, - placeholder: i18n.exportPasswordPlaceholder, - minlength: 32 - }), - p({ class: "file-info" }, i18n.fileInfo), - button({ type: "submit" }, i18n.legacyExportButton) - ), - br(), - p(i18n.importDescription), - form( - { action: "/legacy/import", method: "POST", enctype: "multipart/form-data" }, - input({ type: "file", name: "uploadedFile", required: true }), - br(), - p(i18n.passwordImport), - input({ - type: "password", - name: "importPassword", - required: true, - placeholder: i18n.importPasswordPlaceholder, - minlength: 32 - }), - br(), - button({ type: "submit" }, i18n.legacyImportButton) - ) - ) - ) - ); -}; - -exports.legacyView = legacyView; diff --git a/src/views/logistics_view.js b/src/views/logistics_view.js new file mode 100644 index 0000000..4153b26 --- /dev/null +++ b/src/views/logistics_view.js @@ -0,0 +1,399 @@ +const { hr, div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, table, tr, td, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, renderStateChip, renderContentActions, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged } = require("./main_views"); +const { renderCommentsSection } = require("./comments_view"); +const { renderMapEmbed } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "BOOKED", "HISTORY", "TOP"]; +const KIND_FILTERS = ["TRIPS", "SHIPMENTS"]; +const MODE_FILTERS = ["OFFERS", "REQUESTS"]; +const DATE_FILTERS = ["UPCOMING", "PAST", "CLOSED"]; +const PRICE_FILTERS = ["FREE", "ECO", "TIME"]; +const KINDS = ["TRIP", "SHIPMENT"]; +const MODES = ["OFFER", "REQUEST"]; +const RECURRENCES = ["NONE", "DAILY", "WEEKLY", "MONTHLY"]; + +const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); +const lbl = (prefix, v) => String(i18n[`logistics${prefix}${cap(v)}`] || v).toUpperCase(); +const filterLabel = (f) => lbl("Filter", f); +const kindLabel = (k) => lbl("Kind", k); +const modeLabel = (m) => lbl("Mode", m); +const statusLabel = (s) => lbl("Status", s); +const priceLabel = (t) => lbl("Price", t); +const recurrenceLabel = (r) => lbl("Recurrence", r); +const bookingLabel = (s) => lbl("Booking", s); + +const routeHref = (route) => `/logistics/${encodeURIComponent(route.id)}`; +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); +const stars = (n) => "★".repeat(Math.round(n)) + "☆".repeat(5 - Math.round(n)); + +const priceText = (route) => { + if (route.priceType === "ECO") return `${route.price} ECO`; + if (route.priceType === "TIME") return `${route.price} ${i18n.logisticsHours}`; + return priceLabel("FREE"); +}; + +const kindChip = (route) => renderStateChip("neutral", route.kind === "TRIP" ? "🚗" : "📦", kindLabel(route.kind)); +const modeChip = (route) => renderStateChip(route.mode === "OFFER" ? "mutuals" : "whole", "", modeLabel(route.mode)); +const statusChip = (route) => renderStateChip(route.status === "OPEN" ? "mutuals" : (route.status === "PAST" ? "whole" : "closed"), route.status === "OPEN" ? "✓" : (route.status === "CLOSED" ? "✗" : ""), statusLabel(route.status)); +const priceChip = (route) => renderStateChip("neutral", "", priceText(route)); + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const routeLine = (route) => p({ class: "logistics-route-line" }, span({ class: "logistics-zone" }, route.origin), " → ", span({ class: "logistics-zone" }, route.destination)); +const whenLine = (route) => p({ class: "job-meta-line" }, `📅 ${fmt(route.date)}${route.recurrence !== "NONE" ? ` · ${recurrenceLabel(route.recurrence)}` : ""}`); +const capacityLine = (route) => { + if (route.kind === "TRIP") return route.seats ? p({ class: "job-meta-line" }, `💺 ${route.seatsLeft} / ${route.seats}`) : null; + const parts = [route.size ? `${i18n.logisticsSizeLabel}: ${route.size}` : null, route.weight ? `${i18n.logisticsWeightLabel}: ${route.weight}` : null].filter(Boolean); + return parts.length ? p({ class: "job-meta-line" }, `📦 ${parts.join(" · ")}`) : null; +}; +const countersBlock = (route) => + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.logisticsRatingsTitle}: ${route.ratingCount || 0}${route.ratingCount ? ` · ${stars(route.ratingAvg)} ${route.ratingAvg}` : ""}`) + ); +const bookingsBlock = (route) => route.isOwner + ? div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.logisticsBookingsLabel}: ${route.bookingCount || 0}`)) + : null; +const renderCover = (route) => { + if (!route.media || !route.media.blobId) return null; + if (route.media.kind === "video") return videoHyperaxe({ class: "logistics-card-cover", src: `/blob/${encodeURIComponent(route.media.blobId)}`, controls: true, preload: "metadata" }); + return a({ href: routeHref(route) }, img({ loading: 'lazy', class: "logistics-card-cover", src: `/blob/${encodeURIComponent(route.media.blobId)}`, alt: route.title || "" })); +}; + +const logisticsChipFor = (mode, census) => { + if (mode === "ALL") return true; + if (!Array.isArray(census)) return true; + if (mode === "MINE") return census.some(r => String(r.author) === String(userId)); + if (mode === "BOOKED") return census.some(r => !!r.myBooking); + if (mode === "RECENT") return census.length > 0; + if (mode === "HISTORY") return census.some(r => r.closed && r.participated); + if (mode === "TOP") return census.some(r => r.ratingCount > 0); + if (mode === "TRIPS") return census.some(r => r.kind === "TRIP"); + if (mode === "SHIPMENTS") return census.some(r => r.kind === "SHIPMENT"); + if (mode === "OFFERS") return census.some(r => r.mode === "OFFER"); + if (mode === "REQUESTS") return census.some(r => r.mode === "REQUEST"); + if (mode === "UPCOMING") return census.some(r => r.status === "OPEN"); + if (mode === "PAST") return census.some(r => r.status === "PAST"); + if (mode === "CLOSED") return census.some(r => r.status === "CLOSED"); + if (PRICE_FILTERS.includes(mode)) return census.some(r => r.priceType === mode); + return census.length > 0; +}; +exports.logisticsChipFor = logisticsChipFor; + +const renderRouteCard = (route, params = {}) => + div({ class: `tribe-card logistics-card logistics-${route.kind.toLowerCase()}` }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(route.id, routeHref(route), { author: route.author, favKind: "logistics", isFavorite: route.isFavorite, reportTitle: route.title, spread: (params.spreadMap && params.spreadMap.get(route.id)) || null, deleteAction: route.isOwner ? contentDeleteAction("logisticsRoute", route.id) : null }) + ), + div({ class: "tribe-card-body" }, + renderCover(route), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: routeHref(route) }, route.title || "—"))), + div({ class: "card-chips-row" }, kindChip(route), modeChip(route), statusChip(route), priceChip(route)), + routeLine(route), + whenLine(route), + capacityLine(route), + countersBlock(route) + ) + ); + +const chipForm = (mode, filter, q, zone, labelText, extra = {}) => + form({ class: "sub-filter-form", method: "GET", action: "/logistics" }, + input({ type: "hidden", name: "q", value: q || "" }), + input({ type: "hidden", name: "filter", value: extra.filter !== undefined ? extra.filter : mode }), + input({ type: "hidden", name: "zone", value: extra.zone !== undefined ? extra.zone : (zone || "") }), + button({ type: "submit", class: extra.active ? "filter-btn active" : "filter-btn" }, labelText) + ); + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => m === filter || logisticsChipFor(m, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const zoneRow = (zones, filter, q, zone) => { + if (!Array.isArray(zones) || !zones.length) return null; + const current = String(zone || "").toLowerCase(); + return div({ class: "activity-sub-filter logistics-zones" }, + span({ class: "inbox-filters-label" }, `${i18n.logisticsZoneLabel}:`), + ...zones.map(z => chipForm(filter, filter, q, zone, z.label, { filter, zone: current === z.label.toLowerCase() ? "" : z.label, active: current === z.label.toLowerCase() })) + ); +}; + +const renderFilters = (filter, q, zone, census, zones, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/logistics", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "zone", value: zone || "" }), + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, filterLabel), + ...chipButtons(KIND_FILTERS, filter, census, kindLabel), + ...chipButtons(MODE_FILTERS, filter, census, modeLabel), + ...chipButtons(DATE_FILTERS, filter, census, statusLabel), + ...chipButtons(PRICE_FILTERS, filter, census, priceLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.logisticsCreate) + ) + ), + emptyMod ? null : zoneRow(zones, filter, q, zone) + ); + +const renderForm = (route, params = {}) => { + const kind = route ? route.kind : (KINDS.includes(String(params.kind || "").toUpperCase()) ? String(params.kind).toUpperCase() : "TRIP"); + const isTrip = kind === "TRIP"; + const formId = route ? "logistics-edit-form" : "logistics-create-form"; + return div({ class: "report-form" }, + route + ? null + : form({ id: "logistics-kind-form", method: "GET", action: "/logistics" }, + input({ type: "hidden", name: "filter", value: "CREATE" }), + label(i18n.logisticsKindLabel), br(), + div({ class: "apply-row" }, + select({ name: "kind", class: "report-category-select" }, ...KINDS.map(k => option({ value: k, ...(k === kind ? { selected: true } : {}) }, kindLabel(k)))), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + route ? null : hr({ class: "form-sep" }), + route ? null : h2({ class: "report-category-fixed" }, kindLabel(kind)), + form({ id: formId, method: "POST", action: route ? `/logistics/update/${encodeURIComponent(route.id)}` : "/logistics/create", enctype: "multipart/form-data" }, + input({ type: "hidden", name: "kind", value: kind }), + label(i18n.logisticsTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.logisticsTitlePlaceholder, value: route ? route.title : "" }), br(), + label(i18n.logisticsModeLabel), br(), + select({ name: "mode" }, ...MODES.map(m => option({ value: m, ...((route ? route.mode : "OFFER") === m ? { selected: true } : {}) }, modeLabel(m)))), br(), br(), + label(i18n.logisticsDescriptionLabel), br(), + textarea({ name: "description", rows: 5, maxlength: "3000", placeholder: i18n.logisticsDescriptionPlaceholder }, route ? route.description : ""), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + label(i18n.logisticsOriginLabel), br(), + input({ type: "text", name: "origin", maxlength: "100", required: true, placeholder: i18n.logisticsOriginPlaceholder, value: route ? route.origin : "" }), br(), + label(i18n.logisticsDestinationLabel), br(), + input({ type: "text", name: "destination", maxlength: "100", required: true, placeholder: i18n.logisticsDestinationPlaceholder, value: route ? route.destination : "" }), br(), + label(i18n.mapLocationTitle || "Map Location"), br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: route ? route.mapUrl : "" }), br(), + label(i18n.logisticsDateLabel), br(), + input({ type: "datetime-local", name: "date", required: true, min: moment().format("YYYY-MM-DDTHH:mm"), value: route && route.date ? moment(route.date).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), + label(i18n.logisticsRecurrenceLabel), br(), + select({ name: "recurrence" }, ...RECURRENCES.map(r => option({ value: r, ...((route ? route.recurrence : "NONE") === r ? { selected: true } : {}) }, recurrenceLabel(r)))), br(), br(), + isTrip + ? [ + label(i18n.logisticsSeatsLabel), br(), + input({ type: "number", name: "seats", min: "1", max: "99", step: "1", required: true, class: "logistics-short", value: route ? String(route.seats || 1) : "1" }), br(), br() + ] + : [ + label(i18n.logisticsSizeLabel), br(), + input({ type: "text", name: "size", maxlength: "60", placeholder: i18n.logisticsSizePlaceholder, value: route ? route.size : "" }), br(), + label(i18n.logisticsWeightLabel), br(), + input({ type: "text", name: "weight", maxlength: "60", placeholder: i18n.logisticsWeightPlaceholder, value: route ? route.weight : "" }), br() + ], + label(i18n.logisticsPriceLabel), br(), + input({ type: "number", name: "price", min: "0", step: "0.01", placeholder: "0", class: "logistics-short", value: route && route.price ? String(route.price) : "" }), + select({ name: "priceType", class: "logistics-price-unit" }, ...["ECO", "TIME"].map(t => option({ value: t, ...((route && route.priceType === "TIME" ? "TIME" : "ECO") === t ? { selected: true } : {}) }, priceLabel(t)))), renderEcoValueChip(), br(), br(), + label(i18n.logisticsOrderRefLabel), br(), + input({ type: "text", name: "orderRef", maxlength: "200", placeholder: i18n.logisticsOrderRefPlaceholder, value: route ? route.orderRef : "" }), br(), + label(i18n.logisticsTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.logisticsTagsPlaceholder, value: route ? route.tags.join(", ") : "" }), br(), br(), + button({ type: "submit", class: "create-button" }, route ? i18n.logisticsUpdate : i18n.logisticsCreate) + ) + ); +}; + +const renderHeader = () => div({ class: "tags-header module-header-line" }, h2(i18n.logisticsTitle), p(i18n.logisticsDescription)); + +exports.logisticsView = async (routes, filter = "ALL", params = {}) => { + const list = Array.isArray(routes) ? routes : []; + const q = String(params.q || "").trim(); + const zone = String(params.zone || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const zones = Array.isArray(params.zones) ? params.zones : []; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q) && !zone; + return template( + i18n.logisticsTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.logisticsTitle), p(i18n.logisticsDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? "" : zone, isForm ? [] : census, isForm ? [] : zones, emptyMod) + ), + section( + isForm + ? renderForm(f === "EDIT" ? params.route : null, params) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, [ + { label: kindLabel("TRIP"), count: list.filter(r => r.kind === "TRIP").length }, + { label: kindLabel("SHIPMENT"), count: list.filter(r => r.kind === "SHIPMENT").length }, + { label: statusLabel("OPEN"), count: list.filter(r => r.status === "OPEN").length } + ]), + form({ method: "GET", action: "/logistics", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "hidden", name: "zone", value: zone }), + input({ type: "text", name: "q", value: q, placeholder: i18n.logisticsSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, list.length ? paged(list).map(r => renderRouteCard(r, params)) : p(i18n.logisticsNoItems)) + ] + ) + ); +}; + +const bookingStatusChip = (b) => renderStateChip(b.status === "CONFIRMED" || b.status === "DELIVERED" ? "mutuals" : (b.status === "PENDING" ? "whole" : "closed"), "", bookingLabel(b.status)); + +const bookingStatusForm = (route, b, status, cls, text) => + form({ method: "POST", action: `/logistics/booking/${encodeURIComponent(b.id)}/status` }, + input({ type: "hidden", name: "status", value: status }), + input({ type: "hidden", name: "route", value: route.id }), + button({ type: "submit", class: cls }, String(text).toUpperCase()) + ); + +const renderBooking = (route, b) => + div({ class: "logistics-booking" }, + div({ class: "emergency-update-head logistics-booking-head" }, + div({ class: "card-chips-row" }, bookingStatusChip(b), route.kind === "TRIP" ? renderStateChip("neutral", "💺", String(b.seats)) : null), + div({ class: "tribe-side-actions emergency-update-actions" }, + route.isOwner && b.status === "PENDING" ? bookingStatusForm(route, b, "CONFIRMED", "tribe-action-btn", i18n.logisticsConfirm) : null, + route.isOwner && b.status === "PENDING" ? bookingStatusForm(route, b, "REJECTED", "delete-btn", i18n.logisticsReject) : null, + b.mine && (b.status === "PENDING" || b.status === "CONFIRMED") ? bookingStatusForm(route, b, "CANCELLED", "delete-btn", i18n.logisticsCancelBooking) : null + ) + ), + b.notes ? p({ class: "logistics-booking-notes" }, b.notes) : null, + b.orderRef ? p({ class: "job-meta-line" }, a({ href: b.orderRef, class: "user-link" }, `🛍 ${i18n.logisticsOrderLink}`)) : null, + b.receipt ? p({ class: "logistics-receipt" }, `🧾 ${i18n.logisticsReceiptLabel}: ${b.receipt}`) : null, + route.isOwner && b.status === "CONFIRMED" + ? form({ method: "POST", action: `/logistics/booking/${encodeURIComponent(b.id)}/status`, class: "logistics-receipt-form" }, + input({ type: "hidden", name: "status", value: "DELIVERED" }), + input({ type: "hidden", name: "route", value: route.id }), + input({ type: "text", name: "receipt", maxlength: "200", placeholder: i18n.logisticsReceiptPlaceholder }), + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.logisticsDeliver).toUpperCase()) + ) + : null, + p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(b.createdAt)}`), userLink(b.booker)) + ); + +const renderBookForm = (route) => + form({ method: "POST", action: `/logistics/${encodeURIComponent(route.id)}/book`, class: "logistics-book-form" }, + h3(i18n.logisticsBookTitle), + route.kind === "TRIP" ? [label(i18n.logisticsSeatsLabel), br(), input({ type: "number", name: "seats", min: "1", max: String(route.seatsLeft || 99), step: "1", value: "1" }), br(), br()] : null, + label(i18n.logisticsNotesLabel), br(), + textarea({ name: "notes", rows: 3, maxlength: "1000", placeholder: i18n.logisticsNotesPlaceholder }), br(), br(), + label(i18n.logisticsOrderRefLabel), br(), + input({ type: "text", name: "orderRef", maxlength: "200", placeholder: i18n.logisticsOrderRefPlaceholder }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.logisticsBook) + ); + +const renderRatings = (route) => + div({ class: "card-section logistics-ratings" }, + h3(i18n.logisticsRatingsTitle), + route.ratings.length + ? div({ class: "logistics-ratings-list" }, ...route.ratings.map(r => div({ class: "logistics-rating" }, + div({ class: "emergency-update-head logistics-rating-head" }, div({ class: "card-chips-row" }, renderStateChip("neutral", "", stars(r.score)))), + r.text ? p(r.text) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(r.createdAt)}`), userLink(r.author)) + ))) + : p(route.closed ? i18n.logisticsNoRatings : i18n.logisticsRatingsWhenClosed), + route.canRate + ? form({ method: "POST", action: `/logistics/${encodeURIComponent(route.id)}/rate`, class: "logistics-rate-form" }, + label(i18n.logisticsScoreLabel), br(), + select({ name: "score" }, ...[5, 4, 3, 2, 1].map(n => option({ value: String(n) }, stars(n)))), br(), br(), + textarea({ name: "text", rows: 3, maxlength: "1000", placeholder: i18n.logisticsRatingPlaceholder }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.logisticsRate) + ) + : null + ); + +exports.singleLogisticsView = async (route, params = {}) => { + const href = routeHref(route); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const zones = Array.isArray(params.zones) ? params.zones : []; + const ownerActions = route.isOwner + ? [ + form({ method: "GET", action: "/logistics" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: route.id }), button({ type: "submit", class: "update-btn" }, i18n.logisticsUpdate)) + ] + : []; + const capacity = route.kind === "TRIP" + ? (route.seats ? [{ label: i18n.logisticsSeatsLabel, value: `${route.seatsLeft} / ${route.seats}` }] : []) + : [route.size ? { label: i18n.logisticsSizeLabel, value: route.size } : null, route.weight ? { label: i18n.logisticsWeightLabel, value: route.weight } : null].filter(Boolean); + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(route.id, href, { author: route.author, favKind: "logistics", isFavorite: route.isFavorite, reportTitle: route.title, spread: params.spread || null, deleteAction: route.isOwner ? contentDeleteAction("logisticsRoute", route.id) : null }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, route.title)), + div({ class: "card-chips-row" }, kindChip(route), modeChip(route), statusChip(route), priceChip(route)), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, fmt(route.createdAt)) + ), + tr( + td({ class: "tribe-info-label" }, i18n.logisticsDateLabel), + td({ class: "tribe-info-value", colspan: "3" }, fmt(route.date)) + ), + route.recurrence !== "NONE" + ? tr( + td({ class: "tribe-info-label" }, i18n.logisticsFrequencyLabel), + td({ class: "tribe-info-value", colspan: "3" }, recurrenceLabel(route.recurrence)) + ) + : null, + tr( + td({ class: "tribe-info-label" }, i18n.logisticsOriginLabel), + td({ class: "tribe-info-value", colspan: "3" }, route.origin) + ), + tr( + td({ class: "tribe-info-label" }, i18n.logisticsDestinationLabel), + td({ class: "tribe-info-value", colspan: "3" }, route.destination) + ), + ...capacity.map(c => tr( + td({ class: "tribe-info-label" }, c.label), + td({ class: "tribe-info-value", colspan: "3" }, c.value) + )), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(route.author)) + ) + ), + countersBlock(route), + renderMapEmbed(params.mapData, route.mapUrl), + route.orderRef + ? div({ class: "tribe-side-actions" }, form({ method: "GET", action: route.orderRef }, button({ type: "submit", class: "tribe-action-btn" }, `🛍 ${String(i18n.logisticsOrderLink).toUpperCase()}`))) + : null, + route.isOwner + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + statusChip(route), + form({ method: "POST", action: `/logistics/status/${encodeURIComponent(route.id)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "status", value: route.status === "CLOSED" ? "OPEN" : "CLOSED" }, + String(route.status === "CLOSED" ? i18n.logisticsReopen : i18n.logisticsClose).toUpperCase()) + ) + ) + : null, + bookingsBlock(route), + div({ class: "doc-export-actions" }, + form({ method: "GET", action: `${href}/pdf` }, button({ type: "submit", class: "filter-btn" }, i18n.logisticsGenerateReport)), + form({ method: "POST", action: `${href}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) + ), + renderTags(route.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ); + const main = div({ class: "tribe-main" }, + route.description ? div({ class: "logistics-body" }, ...renderStyledText(route.description)) : null, + route.canBook ? div({ class: "card-section" }, renderBookForm(route)) : null, + route.bookings.length + ? div({ class: "card-section logistics-bookings" }, + h3(route.isOwner ? `${i18n.logisticsBookingsTitle} (${route.bookings.length})` : i18n.logisticsMyBookingTitle), + ...route.bookings.map(b => renderBooking(route, b)) + ) + : null, + renderRatings(route), + renderEngagement(route.id, + renderOpinionsVoting("/logistics/opinions", route.id, route.opinions, href, route.opinions_inhabitants), + renderCommentsSection({ action: `/logistics/${encodeURIComponent(route.id)}/comments`, comments: params.comments || [], returnTo: href }) + ) + ); + return template( + route.title, + section(renderHeader(), renderFilters("ALL", "", "", census, zones, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; diff --git a/src/views/logs_view.js b/src/views/logs_view.js index 60d0149..b43b790 100644 --- a/src/views/logs_view.js +++ b/src/views/logs_view.js @@ -1,7 +1,10 @@ -const { div, h2, p, section, button, form, span, table, thead, tbody, tr, th, td, input, textarea, br, option, select } = require("../server/node_modules/hyperaxe"); -const { template, i18n } = require("./main_views"); +const { div, h2, p, section, button, form, span, table, thead, tbody, tr, th, td, input, textarea, br, option, select, a, label } = require("../server/node_modules/hyperaxe"); +const { template, i18n, renderModuleStatsBy, paged } = require("./main_views"); const moment = require("../server/node_modules/moment"); -const { renderUrl } = require("../backend/renderUrl"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledText } = require("../backend/renderStyledText"); + +const userId = config.keys.id; const safeArr = v => Array.isArray(v) ? v : []; @@ -18,30 +21,31 @@ const filterLabel = (f) => { return map[f] || f.toUpperCase(); }; -const renderFilterBar = (current, hasItems = false) => - div({ class: "logs-toolbar" }, - form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" }, - FILTERS.map(f => +const renderFilterBar = (current, hasItems = true, emptyLogs = false, avail = null) => + div({ class: "activity-sub-filter" }, + emptyLogs ? null : form({ method: "GET", action: "/logs", class: "sub-filter-form" }, + FILTERS.filter(f => f === current || f === "always" || !avail || avail[f] !== false).map(f => button({ type: "submit", name: "filter", value: f, class: current === f ? "filter-btn active" : "filter-btn" - }, filterLabel(f)) + }, String(filterLabel(f)).toUpperCase()) ) ), - form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" }, + form({ method: "GET", action: "/logs", class: "sub-filter-form" }, input({ type: "hidden", name: "view", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.logsCreate || 'Create Log') ), hasItems - ? form({ method: "GET", action: "/logs/export", class: "logs-toolbar-inline" }, + ? form({ method: "GET", action: "/logs/export", class: "sub-filter-form" }, button({ type: "submit", class: "create-button" }, i18n.logsExport || 'Export Logs') ) : null ); -const renderSearchBox = (current, search) => { +const renderSearchBox = (current, search, items = null) => { const q = search || {}; - return div({ class: "logs-search" }, + return div({ class: "logs-search activity-filter-chips activity-toolbar-row" }, + items ? renderModuleStatsBy(items, e => (e && e.mode === 'ai' ? 'ai' : 'manual'), [{ value: 'manual', label: i18n.logsModeManual }, { value: 'ai', label: i18n.logsModeAI }]) : null, form({ method: "GET", action: "/logs", class: "filter-box" }, input({ type: "hidden", name: "filter", value: current || 'today' }), input({ @@ -65,23 +69,27 @@ const renderSearchBox = (current, search) => { ); }; -const renderToolbar = (current, search, hasItems) => - div({ class: "logs-toolbar-wrap" }, - renderFilterBar(current, hasItems), - renderSearchBox(current, search) - ); +const renderToolbar = (current, search, hasItems, items = null, emptyLogs = false, avail = null) => + emptyLogs + ? div({ class: "filters" }, + form({ method: "GET", action: "/logs", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "view", value: "create" }), + button({ type: "submit", class: "create-button" }, i18n.logsCreate || 'Create Log') + ) + ) + : div({ class: "logs-toolbar-wrap" }, + renderFilterBar(current, hasItems, emptyLogs, avail), + renderSearchBox(current, search, items) + ); -const MAX_PREVIEW = 140; - -const truncate = (str) => { - const s = String(str || ''); - if (s.length <= MAX_PREVIEW) return s; - return s.slice(0, MAX_PREVIEW).replace(/\s+\S*$/, '') + '…'; +const truncate = (value, max = 160) => { + const text = String(value == null ? '' : value).trim(); + return text.length > max ? `${text.slice(0, max)}\u2026` : text; }; const renderLogPreview = (item) => { const text = truncate(item.text); - return div({ class: "logs-entry-text" }, ...renderUrl(text)); + return div({ class: "logs-entry-text" }, ...renderStyledText(text)); }; const renderTable = (items) => { @@ -91,9 +99,8 @@ const renderTable = (items) => { tr( th(i18n.logsColumnDate || 'Date'), th(i18n.logsColumnType || 'Type'), - th(i18n.logsColumnLog || 'Log'), - th(''), - th('') + th({ class: "logs-col-log" }, i18n.logsColumnLog || 'Log'), + th({ class: "logs-col-actions" }, '') ) ), tbody( @@ -114,13 +121,13 @@ const renderTable = (items) => { renderLogPreview(item) ), td({ class: "logs-col-actions" }, - form({ method: "GET", action: `/logs/view/${encodeURIComponent(item.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.logsViewDetails || 'View Details') - ) - ), - td({ class: "logs-col-actions" }, - form({ method: "GET", action: `/logs/export/${encodeURIComponent(item.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export') + div({ class: "logs-row-actions" }, + form({ method: "GET", action: `/logs/view/${encodeURIComponent(item.key)}` }, + button({ type: "submit", class: "filter-btn" }, i18n.logsViewDetails || 'View Details') + ), + form({ method: "GET", action: `/logs/export/${encodeURIComponent(item.key)}` }, + button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export') + ) ) ) ) @@ -161,8 +168,7 @@ const renderCreateForm = (mode, aiModOn) => { : div({ class: "div-center audio-form" }, form({ method: "POST", action: "/logs/create" }, input({ type: "hidden", name: "mode", value: "manual" }), - span(i18n.logsManualPrompt || 'Write your log'), br(), - textarea({ name: "text", rows: "8", required: true, placeholder: i18n.logsTextPlaceholder || 'Describe your experiences...' }), + textarea({ maxlength: "5000", name: "text", rows: "8", required: true, placeholder: i18n.logsTextPlaceholder || 'Describe your experiences...' }), br(), br(), button({ type: "submit", class: "create-button" }, i18n.logsWriteButton || 'Write') ) @@ -172,10 +178,9 @@ const renderCreateForm = (mode, aiModOn) => { const renderEditForm = (entry) => { return div({ class: "div-center audio-form" }, - h2(i18n.logsEditTitle || 'Edit Log'), - form({ method: "POST", action: `/logs/edit/${encodeURIComponent(entry.key)}` }, - span(i18n.logsManualPrompt || 'Write your log...'), br(), - textarea({ name: "text", rows: "8", required: true }, entry.text || ''), + h2(i18n.logsEditTitle || 'Update Log'), + form({ method: "POST", action: `/logs/update/${encodeURIComponent(entry.key)}` }, + textarea({ maxlength: "5000", name: "text", rows: "8", required: true }, entry.text || ''), input({ type: "hidden", name: "label", value: entry.label || '' }), br(), br(), button({ type: "submit", class: "create-button" }, i18n.logsUpdateButton || 'Update') @@ -188,26 +193,25 @@ const renderDetail = (entry) => { return div({ class: "div-center audio-form logs-detail" }, h2(headerLine), entry.label ? div({ class: "logs-entry-label" }, entry.label) : null, - div({ class: "logs-detail-text" }, ...renderUrl(String(entry.text || ''))), - div({ class: "logs-detail-actions" }, + div({ class: "logs-detail-text" }, ...renderStyledText(String(entry.text || ''))), + div({ class: "tribe-side-actions logs-detail-actions" }, form({ method: "GET", action: "/logs" }, - button({ type: "submit", class: "filter-btn" }, i18n.walletBack || 'Back') + button({ type: "submit", class: "tribe-action-btn" }, i18n.walletBack || 'Back') ), form({ method: "GET", action: `/logs/export/${encodeURIComponent(entry.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export') + button({ type: "submit", class: "tribe-action-btn" }, i18n.generatePdf) ), - form({ method: "GET", action: "/logs" }, - input({ type: "hidden", name: "view", value: "edit" }), - input({ type: "hidden", name: "id", value: entry.key }), - button({ type: "submit", class: "filter-btn" }, i18n.logsEdit || 'Edit') + form({ method: "GET", action: `/logs/edit/${encodeURIComponent(entry.key)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.logsEdit || 'Update') ), form({ method: "POST", action: `/logs/delete/${encodeURIComponent(entry.key)}` }, - button({ type: "submit", class: "filter-btn danger-btn" }, i18n.logsDelete || 'Delete') + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.logsDelete || 'Delete') ) ) ); }; + exports.logsView = (items, filter, mode, opts = {}) => { const listTitle = i18n.logsTitle || 'Logs'; const description = i18n.logsDescription || 'Record your experience in the network.'; @@ -215,35 +219,34 @@ exports.logsView = (items, filter, mode, opts = {}) => { const aiModOn = !!opts.aiModOn; const hasItems = Array.isArray(items) && items.length > 0; + const screen = (...blocks) => template( + listTitle, + section( + div({ class: "tags-header module-header-line" }, h2(listTitle), p(description)), + div({ class: "activity-sub-filter" }, + form({ method: "GET", action: "/logs", class: "sub-filter-form" }, + button({ type: "submit", name: "filter", value: filter || "today", class: "filter-btn active" }, String(filterLabel(filter || "today")).toUpperCase()) + ) + ), + ...blocks + ) + ); + if (view === 'create') { - const h = i18n.logsCreateTitle || 'Create Log'; - const body = section( - div({ class: "tags-header" }, h2(h), p(description)), - renderFilterBar(filter, hasItems), - renderCreateForm(mode, aiModOn) - ); - return template(h, body); + return screen(renderCreateForm(mode, aiModOn)); } if (view === 'edit' && opts.entry) { - const h = i18n.logsEditTitle || 'Edit Log'; - const body = section( - div({ class: "tags-header" }, h2(h), p(description)), - renderEditForm(opts.entry) - ); - return template(h, body); + return screen(renderEditForm(opts.entry)); } if (view === 'detail' && opts.entry) { - const h = i18n.logsViewTitle || 'Log'; - const body = section( - div({ class: "tags-header" }, h2(h), p(description)), - renderDetail(opts.entry) - ); - return template(h, body); + return screen(renderDetail(opts.entry)); } + const sr = opts.search || {}; + const emptyLogs = Number(opts.total ?? items.length) === 0 && !String(sr.q || '').trim() && !String(sr.type || '').trim() && !String(sr.date || '').trim(); const body = section( - div({ class: "tags-header" }, h2(listTitle), p(description)), - renderToolbar(filter, opts.search || {}, hasItems), - div({ class: "logs-list" }, renderTable(items)) + div({ class: "tags-header module-header-line" }, h2(listTitle), p(description)), + renderToolbar(filter, opts.search || {}, hasItems, items, emptyLogs, opts.avail || null), + div({ class: "logs-list" }, renderTable(paged(items))) ); return template(listTitle, body); }; diff --git a/src/views/mailing_view.js b/src/views/mailing_view.js new file mode 100644 index 0000000..071b884 --- /dev/null +++ b/src/views/mailing_view.js @@ -0,0 +1,321 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, details, summary, ul, li, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { TEXT_CAP } = require('../backend/long_text'); +const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderContentActions, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderStyledText } = require("../backend/renderStyledText"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "SUBSCRIBED"]; +const TYPE_FILTERS = ["OPEN", "CLOSED"]; +const STATUS_FILTERS = ["ACTIVE", "ARCHIVED"]; +const HISTORY_MODES = ["DATE", "THREADS", "INHABITANTS"]; + +const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); +const baseLabel = (f) => String(i18n[`mailingFilter${cap(f)}`] || f).toUpperCase(); +const typeLabel = (t) => String(i18n[`mailingType${cap(t)}`] || t).toUpperCase(); +const statusLabel = (s) => String(i18n[`mailingStatus${cap(s)}`] || s).toUpperCase(); +const historyLabel = (m) => String(i18n[`mailingHistory${cap(m)}`] || m).toUpperCase(); + +const listHref = (list) => `/mailing/${encodeURIComponent(list.id)}`; +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); + +const statusChip = (list) => renderStateChip(list.status === "ACTIVE" ? "mutuals" : "closed", list.status === "ACTIVE" ? "✓" : "✗", statusLabel(list.status)); +const countsLine = (list) => span({ class: "card-label activity-update-counts mailing-counts" }, `👥: ${list.participantCount || 0} · 💬 ${list.messageCount || 0}`); + +const subscribeForm = (list, returnTo) => { + if (list.isOwner) return null; + if (list.closed) { + return list.isMember + ? form({ method: "POST", action: `/mailing/leave/${encodeURIComponent(list.id)}` }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.subscriptionUnsubscribe).toUpperCase())) + : null; + } + return form({ method: "POST", action: "/subscriptions/toggle" }, + input({ type: "hidden", name: "target", value: list.id }), + input({ type: "hidden", name: "scope", value: "mailing" }), + input({ type: "hidden", name: "on", value: list.isMember ? "0" : "1" }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: list.isMember ? "tribe-action-btn danger-btn" : "tribe-action-btn" }, String(list.isMember ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe).toUpperCase()) + ); +}; + +const writeForm = (list) => (list.status === "ACTIVE" && (!list.closed || list.isMember)) + ? form({ method: "GET", action: `${listHref(list)}#compose` }, input({ type: "hidden", name: "write", value: "1" }), button({ type: "submit", class: "tribe-action-btn" }, String(i18n.mailingWriteButton).toUpperCase())) + : null; + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const mailingChipFor = (mode, census) => { + if (mode === "ALL") return true; + if (!Array.isArray(census)) return true; + if (mode === "MINE") return census.some(l => String(l.author) === String(userId)); + if (mode === "SUBSCRIBED") return census.some(l => l.isMember && String(l.author) !== String(userId)); + if (mode === "RECENT") return census.length > 0; + if (mode === "OPEN") return census.some(l => !l.closed); + if (mode === "CLOSED") return census.some(l => l.closed); + if (STATUS_FILTERS.includes(mode)) return census.some(l => l.status === mode); + return census.length > 0; +}; +exports.mailingChipFor = mailingChipFor; + +const renderArchiveItem = (list, params = {}) => + li({ class: "mailing-archive-item" }, + div({ class: "emergency-update-head mailing-archive-head" }, + div({ class: "mailing-archive-meta" }, + a({ href: listHref(list), class: "user-link" }, list.title || "—"), + countsLine(list), + renderOpenClosedChip(list.listType, { statusChipOPEN: typeLabel("OPEN"), statusChipCLOSED: typeLabel("CLOSED") }), + statusChip(list), + list.isMember && !list.isOwner ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) : null + ), + renderContentActions(list.id, listHref(list), { author: list.author, favKind: "mailing", isFavorite: list.isFavorite, reportTitle: list.title, spread: (params.spreadMap && params.spreadMap.get(list.id)) || null, deleteAction: String(list.author) === String(userId) ? contentDeleteAction("mailingList", list.id) : null }) + ) + ); + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => m === filter || mailingChipFor(m, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const renderFilters = (filter, q, census, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/mailing", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, baseLabel), + ...chipButtons(TYPE_FILTERS, filter, census, typeLabel), + ...chipButtons(STATUS_FILTERS, filter, census, statusLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.mailingCreate) + ) + ) + ); + +const renderForm = (list) => + div({ class: "report-form" }, + h2(list ? i18n.mailingUpdateSectionTitle : i18n.mailingCreateSectionTitle), + form({ method: "POST", action: list ? `/mailing/update/${encodeURIComponent(list.id)}` : "/mailing/create" }, + label(i18n.mailingTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.mailingTitlePlaceholder, value: list ? list.title : "" }), br(), + label(i18n.mailingDescriptionLabel), br(), + textarea({ name: "description", rows: 4, maxlength: "1000", placeholder: i18n.mailingDescriptionPlaceholder }, list ? list.description : ""), br(), + list + ? [ + label(i18n.mailingStatusLabel), br(), + select({ name: "status" }, ...STATUS_FILTERS.map(st => option({ value: st, ...(st === list.status ? { selected: true } : {}) }, statusLabel(st)))), br(), br() + ] + : [ + label(i18n.mailingTypeLabel), br(), + select({ name: "listType" }, ...TYPE_FILTERS.map(t => option({ value: t }, typeLabel(t)))), br(), br() + ], + (!list || list.closed) + ? [ + label(i18n.mailingMembersLabel), br(), + textarea({ name: "members", rows: 3, maxlength: "4000", placeholder: i18n.mailingMembersPlaceholder }, list ? list.members.filter(m => m !== list.author).join(", ") : ""), br() + ] + : null, + label(i18n.mailingTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.mailingTagsPlaceholder, value: list ? list.tags.join(", ") : "" }), br(), br(), + button({ type: "submit", class: "create-button" }, list ? i18n.mailingUpdate : i18n.mailingCreate) + ) + ); + +const renderHeader = () => div({ class: "tags-header module-header-line" }, h2(i18n.mailingTitle), p(i18n.mailingDescription)); + +exports.mailingView = async (lists, filter = "ALL", params = {}) => { + const list = Array.isArray(lists) ? lists : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q); + return template( + i18n.mailingTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.mailingTitle), p(i18n.mailingDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? [] : census, emptyMod) + ), + section( + isForm + ? renderForm(f === "EDIT" ? params.list : null) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, [ + { label: typeLabel("OPEN"), count: list.filter(l => !l.closed).length }, + { label: typeLabel("CLOSED"), count: list.filter(l => l.closed).length }, + { label: statusLabel("ARCHIVED"), count: list.filter(l => l.status === "ARCHIVED").length } + ]), + form({ method: "GET", action: "/mailing", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: q, placeholder: i18n.mailingSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + list.length ? ul({ class: "mailing-archive" }, ...paged(list.slice().sort((x, y) => (y.lastActivityTs || 0) - (x.lastActivityTs || 0))).map(l => renderArchiveItem(l, params))) : div({ class: "tribe-grid" }, p(i18n.mailingNoItems)) + ] + ) + ); +}; + +const renderMessage = (list, m, opts = {}) => + div({ class: "mailing-message" }, + div({ class: "emergency-update-head mailing-message-head" }, + span({ class: "mailing-message-subject" }, m.subject || i18n.pmNoSubject), + list.canWrite && !opts.noReply + ? div({ class: "tribe-side-actions emergency-update-actions" }, + form({ method: "GET", action: `${listHref(list)}#compose`, class: "mailing-reply-form" }, + input({ type: "hidden", name: "write", value: "1" }), + input({ type: "hidden", name: "reply", value: m.mid }), + input({ type: "hidden", name: "history", value: opts.mode || "DATE" }), + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.pmReply || "Reply").toUpperCase()) + ) + ) + : null + ), + div({ class: "mailing-message-text" }, ...renderStyledText(m.text || "")), + p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(m.sentAt)}`), userLink(m.author)) + ); + +const renderHistory = (list, history, mode) => { + if (!history.length) return p(i18n.mailingNoMessages); + if (mode === "THREADS") { + const groups = new Map(); + for (const m of history) { + if (!groups.has(m.thread)) groups.set(m.thread, []); + groups.get(m.thread).push(m); + } + const ordered = Array.from(groups.values()).sort((x, y) => y[y.length - 1].ts - x[x.length - 1].ts); + return div({ class: "mailing-threads" }, ...ordered.map(msgs => { + const root = msgs[0]; + const replies = msgs.slice(1); + return div({ class: "mailing-thread" }, + renderMessage(list, root, { mode }), + replies.length + ? details({ class: "mailing-thread-details" }, + summary(`${replies.length} ${replies.length === 1 ? (i18n.pmReply || "reply") : (i18n.pmReplies || "replies")}`), + ...replies.map(r => renderMessage(list, r, { mode })) + ) + : null + ); + })); + } + if (mode === "INHABITANTS") { + const groups = new Map(); + for (const m of history) { + if (!groups.has(m.author)) groups.set(m.author, []); + groups.get(m.author).push(m); + } + const ordered = Array.from(groups.entries()).sort((x, y) => y[1].length - x[1].length); + return div({ class: "mailing-by-author" }, ...ordered.map(([author, msgs]) => + div({ class: "mailing-author-group" }, + h3({ class: "mailing-author-title" }, userLink(author), span({ class: "mailing-author-count" }, ` · ${msgs.length}`)), + ...msgs.slice().sort((x, y) => y.ts - x.ts).map(m => renderMessage(list, m, { mode })) + ) + )); + } + return div({ class: "mailing-by-date" }, ...history.slice().sort((x, y) => y.ts - x.ts).map(m => renderMessage(list, m, { mode }))); +}; + +const renderCompose = (list, params) => { + const parent = params.replyTo ? (list.history || []).find(m => m.mid === params.replyTo || m.key === params.replyTo) : null; + const draft = params.draft || null; + const subject = draft ? String(draft.subject || "") : parent ? (/^\s*RE:/i.test(parent.subject) ? parent.subject : `RE: ${parent.subject}`) : ""; + const thread = parent ? parent.thread : draft ? String(draft.thread || "") : ""; + return div({ class: "div-center audio-form mailing-compose", id: "compose" }, + h2(parent ? i18n.mailingReplyTitle : i18n.mailingWriteTitle), + form({ method: "POST", action: `/mailing/${encodeURIComponent(list.id)}/message` }, + thread ? input({ type: "hidden", name: "thread", value: thread }) : null, + params.returnTo ? input({ type: "hidden", name: "returnTo", value: params.returnTo }) : null, + label(i18n.pmSubject), br(), + input({ type: "text", name: "subject", maxlength: "150", placeholder: i18n.pmSubjectHint, value: subject }), br(), br(), + label(i18n.pmText), br(), + textarea({ name: "text", rows: 8, maxlength: String(TEXT_CAP), required: true, autofocus: true, placeholder: parent ? parent.text.split("\n").map(l => `> ${l}`).join("\n") : i18n.mailingTextPlaceholder }, draft ? String(draft.text || "") : ""), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.mailingSend) + ) + ); +}; + +exports.singleMailingView = async (list, params = {}) => { + const isAuthor = String(list.author) === String(userId); + const href = listHref(list); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const history = Array.isArray(list.history) ? list.history : []; + const mode = HISTORY_MODES.includes(String(params.history || "").toUpperCase()) ? String(params.history).toUpperCase() : "THREADS"; + const q = String(params.q || "").trim(); + const shown = q ? history.filter(m => [m.subject, m.text, m.author].some(v => String(v || "").toLowerCase().includes(q.toLowerCase()))) : history; + const historyChips = history.length + ? div({ class: "activity-sub-filter" }, + ...HISTORY_MODES.map(m => form({ class: "sub-filter-form", method: "GET", action: `${href}#history` }, + input({ type: "hidden", name: "history", value: m }), + input({ type: "hidden", name: "q", value: q }), + button({ type: "submit", class: mode === m ? "filter-btn active" : "filter-btn" }, historyLabel(m)) + )) + ) + : null; + const sideActions = [subscribeForm(list, href)].filter(Boolean); + const ownerActions = isAuthor + ? [ + form({ method: "GET", action: "/mailing" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: list.id }), button({ type: "submit", class: "update-btn" }, i18n.mailingUpdate)) + ] + : []; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(list.id, href, { author: list.author, favKind: "mailing", isFavorite: list.isFavorite, reportTitle: list.title, spread: params.spread || null, deleteAction: isAuthor ? contentDeleteAction("mailingList", list.id) : null }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, list.title)), + div({ class: "card-chips-row" }, renderOpenClosedChip(list.listType, { statusChipOPEN: typeLabel("OPEN"), statusChipCLOSED: typeLabel("CLOSED") }), statusChip(list)), + list.description ? p({ class: "tribe-side-description" }, ...renderStyledText(list.description)) : null, + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, fmt(list.createdAt)) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(list.author)) + ) + ), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.mailingSubscribers}: ${list.participantCount || 0}`) + ), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + isAuthor + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + statusChip(list), + form({ method: "POST", action: `/mailing/status/${encodeURIComponent(list.id)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "status", value: list.status === "ACTIVE" ? "ARCHIVED" : "ACTIVE" }, + String(list.status === "ACTIVE" ? i18n.mailingArchive : i18n.mailingActivate).toUpperCase()) + ) + ) + : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.mailingMessagesLabel}: ${list.messageCount || 0}`) + ), + writeForm(list) ? div({ class: "tribe-side-actions" }, writeForm(list)) : null, + div({ class: "doc-export-actions" }, + form({ method: "GET", action: `${href}/pdf` }, button({ type: "submit", class: "filter-btn" }, i18n.mailingGenerateReport)), + form({ method: "POST", action: `${href}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) + ), + renderTags(list.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ); + const main = div({ class: "tribe-main" }, + params.write && list.status === "ACTIVE" && (!list.closed || list.isMember) ? renderCompose(list, params) : null, + div({ class: "card-section mailing-history", id: "history" }, + historyChips, + history.length + ? form({ method: "GET", action: `${href}#history`, class: "filter-box" }, + input({ type: "hidden", name: "history", value: mode }), + input({ type: "text", name: "q", value: q, placeholder: i18n.mailingHistorySearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + : null, + renderHistory(list, shown, mode) + ) + ); + return template( + list.title, + section(renderHeader(), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; diff --git a/src/views/main_views.js b/src/views/main_views.js index 8fb458e..8ee372e 100644 --- a/src/views/main_views.js +++ b/src/views/main_views.js @@ -1,15 +1,16 @@ "use strict"; const path = require("path"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); const fs = require("fs"); const envPaths = require("../server/node_modules/env-paths"); const debug = require("../server/node_modules/debug")("oasis"); -const highlightJs = require("../server/node_modules/highlight.js"); +const highlightJs = require("../server/node_modules/highlight.js/lib/core"); +highlightJs.registerLanguage("json", require("../server/node_modules/highlight.js/lib/languages/json")); const prettyMs = require("../server/node_modules/pretty-ms"); const moment = require('../server/node_modules/moment'); -const QRCode = require('../server/node_modules/qrcode'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText, renderStyledHtml, plainText, safeExternalHref } = require('../backend/renderStyledText'); const ssbClientGUI = require("../client/gui"); const config = require("../server/ssb_config"); const cooler = ssbClientGUI({ offline: config.offline }); @@ -18,15 +19,17 @@ const sharedState = require('../configs/shared-state'); let ssb, userId; const getUserId = async () => { + if (userId) return userId; + if (config && config.keys && config.keys.id) { userId = config.keys.id; return userId; } if (!ssb) ssb = await cooler.open(); - if (!userId) userId = ssb.id; + userId = ssb.id; return userId; }; +exports.getUserId = getUserId; -const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, table, td, textarea, title, tr, ul, strong, script, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); +const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, table, td, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); const lodash = require("../server/node_modules/lodash"); -const markdown = require("./markdown"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); const nameCache = require('../backend/nameCache'); @@ -46,18 +49,137 @@ const userLink = (feedId, knownName) => { }; exports.userLink = userLink; + +const renderCreatedRows = (createdAt, author, updatedAt) => { + const createdTs = createdAt ? new Date(createdAt).getTime() : NaN; + const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + return [ + createdAt ? tr(td({ class: 'tribe-info-label' }, i18n.createdAtLabel), td({ class: 'tribe-info-value', colspan: '3' }, moment(createdAt).format('YYYY/MM/DD HH:mm'))) : null, + showUpdated ? tr(td({ class: 'tribe-info-label' }, i18n.jobsUpdatedAt), td({ class: 'tribe-info-value', colspan: '3' }, moment(updatedAt).format('YYYY/MM/DD HH:mm'))) : null, + author ? tr(td({ class: 'tribe-info-value', colspan: '4' }, userLink(author))) : null + ]; +}; +exports.renderCreatedRows = renderCreatedRows; + +const inboxBadges = () => { + const inboxCount = Number(sharedState.getInboxCount()) || 0; + return [inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : '']; +}; +const phoneOn = () => !config.public && getConfig().modules.phoneMod !== 'off'; +const PHONE_PAM_MAX_MS = require('../server/phone_module').VOICEMAIL_MAX_MS; +const renderCallButton = (feedId, { upper = false, cls = null, pam = false } = {}) => { + if (!phoneOn() || !feedId || String(feedId) === String(config.keys.id) || sharedState.isPubId(feedId)) return null; + if (pam) return a({ href: `/phone?filter=create&to=${encodeURIComponent(feedId)}`, class: cls || "filter-btn" }, `✆ ${i18n.phoneCallTooltip}`); + const label = upper ? String(i18n.phoneCallButton).toUpperCase() : i18n.phoneCallButton; + return form({ method: "POST", action: "/phone/call", class: "phone-action-form" }, + input({ type: "hidden", name: "to", value: feedId }), + button({ type: "submit", class: cls || "filter-btn", title: i18n.phoneCallButton }, `✆ ${label}`) + ); +}; +exports.renderCallButton = renderCallButton; +const pad2 = (n) => String(n).padStart(2, '0'); +const liveClock = (fromMs, { max = 0 } = {}) => { + const from = Number(fromMs) || Date.now(); + const e = Math.max(0, Math.floor((Date.now() - from) / 1000)); + const roll = (cls, labels) => span({ class: 'live-clock-win' }, span({ class: `live-clock-roll ${cls}` }, ...labels.map(l => span(l)))); + if (max > 0) { + const cap = Math.floor(max / 1000); + return span({ class: 'live-clock' }, roll('live-clock-once', Array.from({ length: 60 }, (_, i) => { const v = Math.min(cap, e + i); return `${pad2(Math.floor(v / 60))}:${pad2(v % 60)}`; }))); + } + const m = Math.floor(e / 60), sec = e % 60; + return span({ class: 'live-clock' }, + roll(`live-clock-min lc-d${sec}`, Array.from({ length: 60 }, (_, i) => `${pad2(m + i)}:`)), + roll('live-clock-sec', Array.from({ length: 60 }, (_, i) => pad2((sec + i) % 60))) + ); +}; +exports.liveClock = liveClock; +const renderPhoneTopLink = () => { + if (!phoneOn()) return null; + const n = Number(sharedState.getPhoneCount()) || 0; + return li(a({ href: "/phone" }, span({ class: "emoji" }, "✆"), nbsp, i18n.phoneTitle, n > 0 ? span({ class: 'inbox-badge' }, String(n)) : '')); +}; exports.userLinkLabel = userLinkLabel; const renderInviteQrCard = ({ qrDataUrl }) => qrDataUrl ? div({ class: 'invite-qr-card' }, img({ src: qrDataUrl, alt: 'QR', class: 'invite-qr-img' })) : null; exports.renderInviteQrCard = renderInviteQrCard; +const renderLiveFeed = (src, imgClass) => { + const id = `live-${String(src).replace(/[^a-zA-Z0-9]/g, '').slice(-24)}`; + return [ + a({ href: `#${id}`, id: `${id}-src`, class: 'zoom-link zoom-inline' }, img({ src, alt: '', class: imgClass })), + div({ id, class: 'lightbox live-lightbox' }, + a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'), + a({ href: `#${id}-full`, class: 'live-fullscreen-btn', title: i18n.liveFullscreen }, renderIcon('fullscreen')), + a({ href: `#${id}-src`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' })) + ), + div({ id: `${id}-full`, class: 'lightbox live-lightbox live-fullscreen' }, + a({ href: `#${id}`, class: 'lightbox-close' }, '\u00d7'), + a({ href: `#${id}`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' })) + ) + ]; +}; +exports.renderLiveFeed = renderLiveFeed; + +const renderVideoGrid = (peers, me, { film = false } = {}) => { + const list = (Array.isArray(peers) ? peers : []).filter(p => p && p.id); + const filmName = (...label) => span({ class: 'video-tile-name video-tile-film' }, '▶ ', ...label); + const tiles = [ + me ? div({ class: 'video-tile video-tile-me' }, ...renderLiveFeed('/phone/video/me', 'video-feed'), span({ class: 'video-tile-name' }, i18n.roomYou)) : null, + film ? div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent('film:me')}`, 'video-feed'), filmName(i18n.roomYou)) : null, + ...list.filter(p => p.on).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(p.id)}`, 'video-feed'), userLink(p.id))), + ...list.filter(p => p.film).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(`film:${p.id}`)}`, 'video-feed'), filmName(userLink(p.id)))) + ].filter(Boolean); + return tiles.length ? div({ class: tiles.length === 1 ? 'video-grid video-grid-solo' : 'video-grid' }, ...tiles) : null; +}; +exports.renderVideoGrid = renderVideoGrid; + +const renderSubscriptionBox = ({ target, scope, subscribed, count, isOwner, returnTo, canWrite, inline, compact }) => { + if (!target) return null; + if (isOwner && (Number(count) || 0) <= 1) return null; + const showPm = canWrite !== undefined ? canWrite : (isOwner || subscribed); + const total = Number(count) || 0; + const countSuffix = total ? ` (${total})` : ''; + const parts = [ + total && !compact ? span({ class: 'card-label' }, `${i18n.subscriptionTitle} (${total})${isOwner ? '' : ':'}`) : null, + compact && isOwner && total ? renderStateChip('mutuals', '🔔', `${i18n.subscriptionTitle}${countSuffix}`) : null, + span({ class: 'subscription-actions' }, + isOwner + ? null + : form({ method: 'POST', action: '/subscriptions/toggle', class: 'subscription-form' }, + input({ type: 'hidden', name: 'target', value: target }), + input({ type: 'hidden', name: 'scope', value: String(scope || '') }), + input({ type: 'hidden', name: 'on', value: subscribed ? '0' : '1' }), + returnTo ? input({ type: 'hidden', name: 'returnTo', value: returnTo }) : null, + compact + ? button({ type: 'submit', class: subscribed ? 'delete-btn' : 'update-btn' }, `${subscribed ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe}${countSuffix}`) + : button({ type: 'submit', class: subscribed ? 'tribe-action-btn danger-btn' : 'tribe-action-btn' }, `${subscribed ? '🔕' : '🔔'} ${String(subscribed ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe).toUpperCase()}`) + ), + showPm && total + ? a({ href: `/pm?list=${encodeURIComponent(target)}`, class: 'btn-singleview btn-pm', title: i18n.pmCreateButton || 'Write a PM' }, renderIcon('mail')) + : null + ) + ]; + const nodes = parts.filter(Boolean); + return inline ? nodes : div({ class: 'tribe-side-actions shop-visibility-row stacked-action-row subscription-box-block' }, ...nodes); +}; +exports.renderSubscriptionBox = renderSubscriptionBox; + const renderStateChip = (variant, icon, text) => span({ class: `pm-exposition-chip pm-exposition-${variant}` }, icon ? span({ class: "pm-exposition-icon" }, icon) : null, span({ class: "pm-exposition-text" }, String(text || "")) ); +const renderPhoneCallForm = (to, inner, title) => !phoneOn() || !to + ? inner + : form({ method: 'POST', action: '/phone/call', class: 'phone-chip-form' }, + input({ type: 'hidden', name: 'to', value: String(to) }), + button({ type: 'submit', class: 'phone-chip-btn', title: title || i18n.phoneCallButton }, inner)); +const renderPhoneChip = (number, title) => number ? renderPhoneCallForm(number, renderStateChip('whole', '✆', number), title) : null; +exports.renderPhoneChip = renderPhoneChip; + const renderOpenClosedChip = (status, i18nObj) => { const s = String(status || "").toUpperCase(); const isOpen = s === "OPEN"; @@ -87,9 +209,10 @@ const renderModeChip = (mode, i18nObj) => { const renderLifespanChip = (lifetime, i18nObj) => { const lt = lifetime || null; if (!lt || !lt.bucket) return null; - const range = lt.range || ""; + const range = lt.contentRange || lt.range || ""; + const bucket = lt.contentBucket || lt.bucket; if (range) { - return span({ class: `pm-exposition-chip pm-exposition-lifespan-${lt.bucket}` }, + return span({ class: `pm-exposition-chip pm-exposition-lifespan-${bucket}` }, span({ class: "pm-exposition-text" }, range) ); } @@ -99,79 +222,284 @@ const renderLifespanChip = (lifetime, i18nObj) => { ); }; -const renderContentActions = (msgId, viewHref) => { +const DATE_TIME_FORMAT = "YYYY/MM/DD HH:mm"; +const DATE_FORMAT = "YYYY/MM/DD"; +const fmtDateTime = (v) => (v ? moment(v).format(DATE_TIME_FORMAT) : ""); +const fmtDay = (v) => (v ? moment(v).format(DATE_FORMAT) : ""); +exports.DATE_TIME_FORMAT = DATE_TIME_FORMAT; +exports.DATE_FORMAT = DATE_FORMAT; +exports.fmtDateTime = fmtDateTime; +exports.fmtDay = fmtDay; + +const clearnetShortId = (id) => String(id || '').replace(/^[%&@]/, '').replace(/[^A-Za-z0-9]/g, '').slice(0, 8).toLowerCase(); +const clearnetSlugFor = (title, id) => { + const base = String(title || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60).replace(/-+$/, ''); + const short = clearnetShortId(id); + return base ? `${base}-${short}` : short; +}; +exports.clearnetShortId = clearnetShortId; +exports.clearnetSlugFor = clearnetSlugFor; +const clearnetItemHref = (modulePath, title, id) => `/c/${modulePath}/${encodeURIComponent(clearnetSlugFor(title, id))}`; +exports.clearnetItemHref = clearnetItemHref; +const CLEARNET_PATHS = { blogs: 'blog', wiki: 'wiki', market: 'market', audios: 'audios', videos: 'videos', images: 'images', documents: 'documents', bookmarks: 'bookmarks', events: 'events', feed: 'feed', jobs: 'jobs', podcasts: 'podcasts', projects: 'projects', torrents: 'torrents', files: 'files', shops: 'shops', emergencies: 'emergencies', campaigns: 'campaigns', housing: 'housing' }; +let clearnetBaseCache = null; +const clearnetBase = () => { + if (clearnetBaseCache !== null) return clearnetBaseCache; + clearnetBaseCache = String((require('../configs/config-manager.js').defaultInvite() || {}).url || '').replace(/\/+$/, ''); + return clearnetBaseCache; +}; +const clearnetHrefFor = (viewHref, blockId, title) => { + const m = String(viewHref || '').match(/^\/(school\/course|[a-z]+)\/([^/?#]+)/); + if (!m) return null; + const seg = m[1] === 'school/course' ? 'school' : CLEARNET_PATHS[m[1]]; + if (!seg) return null; + const base = clearnetBase(); + const id = blockId ? encodeURIComponent(clearnetSlugFor(title, blockId)) : m[2]; + return base ? `${base}/c/${seg}/${id}` : null; +}; +let linkBoxSeq = 0; +const renderLinkBox = (blockId, viewHref, title) => { + linkBoxSeq = (linkBoxSeq + 1) % 1000000; + const id = `linkbox-${linkBoxSeq}-${String(blockId).replace(/[^a-zA-Z0-9]/g, '').slice(-10)}`; + const clearnetHref = clearnetHrefFor(viewHref, blockId, title); + return [ + a({ href: `#${id}`, id: `${id}-src`, class: 'btn-singleview btn-link', title: i18n.copyLinkTitle }, renderIcon('link')), + span({ id, class: 'lightbox link-lightbox' }, + a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'), + span({ class: 'link-box' }, + span({ class: 'link-box-row' }, span({ class: 'link-box-label' }, i18n.linkBoxOasis), input({ type: 'text', readonly: true, class: 'link-box-input', value: viewHref })), + clearnetHref ? span({ class: 'link-box-row' }, span({ class: 'link-box-label' }, i18n.linkBoxClearnet), input({ type: 'text', readonly: true, class: 'link-box-input', value: clearnetHref })) : null + ) + ) + ]; +}; + +const renderWalletChip = () => { + try { if (getConfig().modules.walletMod !== 'on') return null; } catch (_) { return null; } + const ready = sharedState.getWalletReady ? sharedState.getWalletReady() : false; + const chip = renderStateChip(ready ? 'mutuals' : 'closed', '❄', ready ? i18n.bankWalletReady : i18n.bankWalletNotConfigured); + return ready ? chip : a({ href: '/wallet', class: 'wallet-chip-link' }, chip); +}; +exports.renderWalletChip = renderWalletChip; + +const torrentFileName = (title) => `${String(title || 'download').replace(/\.torrent$/i, '')}.torrent`; +const torrentDownloadHref = (blobId, title) => `/blob/${encodeURIComponent(blobId)}?name=${encodeURIComponent(torrentFileName(title))}`; +const renderTorrentDownload = (href, opts = {}) => + a({ href, class: opts.class || 'filter-btn' }, `\u2B07 ${i18n.torrentExternalButton || 'EXTERNAL'}`); +exports.torrentDownloadHref = torrentDownloadHref; +exports.renderTorrentDownload = renderTorrentDownload; +const renderTorrentSourceDownload = (torrentKey, source, opts = {}) => { + if (typeof source !== 'string' || !source.startsWith('&') || !torrentKey) return null; + return a({ href: `/torrents/${encodeURIComponent(torrentKey)}/get`, class: opts.class || 'filter-btn' }, `\u2B07 ${i18n.torrentOasisButton || 'OASIS'}`); +}; +exports.renderTorrentSourceDownload = renderTorrentSourceDownload; +const renderFileDownloads = (fileKey, torrentUrl, name, opts = {}) => { + if (!fileKey) return null; + const btnClass = opts.class || 'filter-btn'; + const torrentName = `${String(name || 'download').replace(/\.torrent$/i, '')}.torrent`; + return div({ class: 'torrent-card-actions' }, + a({ href: `/files/${encodeURIComponent(fileKey)}/get`, class: btnClass }, `\u2B07 ${i18n.torrentOasisButton || 'OASIS'}`), + typeof torrentUrl === 'string' && torrentUrl.startsWith('&') + ? a({ href: `/blob/${encodeURIComponent(torrentUrl)}?name=${encodeURIComponent(torrentName)}`, class: btnClass }, `\u2B07 ${i18n.torrentExternalButton || 'EXTERNAL'}`) + : null + ); +}; +exports.renderFileDownloads = renderFileDownloads; + +const blobUrlOf = (v) => (String(v || '').startsWith('/blob/sealed/') ? String(v) : `/blob/${encodeURIComponent(v)}`); +exports.blobUrlOf = blobUrlOf; +const ACTION_ICONS = { + hide: '', + spread: '', + pin: '', + report: '', + mail: '', + call: '', + link: '', + block: '', + datagram: '', + open: '', + donate: '', + torrent: '', + close: '', + film: '', + fullscreen: '' +}; +const renderIcon = (name) => span({ class: 'btn-icon', [RAW_HTML]: `` }); +const renderHideIcon = () => renderIcon('hide'); +exports.renderIcon = renderIcon; +exports.renderHideIcon = renderHideIcon; +const renderContentActions = (msgId, viewHref, opts = {}) => { + const o = (opts && typeof opts === 'object') ? opts : {}; const blockId = (typeof msgId === 'string' && msgId.startsWith('%')) ? msgId : null; - const chainBtn = blockId - ? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer || 'View blockexplorer' }, '⦿') + const myId = (config.keys && config.keys.id) ? config.keys.id : ''; + const linkBtn = blockId && viewHref ? renderLinkBox(blockId, viewHref, o.reportTitle) : null; + + const pinBtn = o.favKind && blockId + ? form({ method: 'POST', action: `/${o.favKind}/favorites/${o.isFavorite ? 'remove' : 'add'}/${encodeURIComponent(blockId)}`, class: 'content-action-form' }, + o.returnTo ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null, + button({ type: 'submit', class: o.isFavorite ? 'btn-singleview btn-pin-on' : 'btn-singleview', title: o.isFavorite ? i18n.favoriteRemove : i18n.favoriteAdd }, renderIcon('pin')) + ) : null; - const contentBtn = viewHref - ? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent || 'Visit content' }, '↗') + + const spreadBtn = o.spread !== undefined && blockId && !(o.author && String(o.author) === String(myId)) ? renderSpreadButton(blockId, o.spread, o.spreadTitle || null) : null; + + const torrentBtn = (() => { + if (process.env.OASIS_MOBILE === '1') return null; + const src = o.torrentFrom && typeof o.torrentFrom === 'object' ? o.torrentFrom : null; + if (!src || typeof src.blobId !== 'string' || !src.blobId.startsWith('&')) return null; + try { if (getConfig().modules.torrentsMod !== 'on') return null; } catch (_) { return null; } + const name = String(src.name || o.reportTitle || '').slice(0, 100); + return a({ href: `/torrents?filter=create&fromBlob=${encodeURIComponent(src.blobId)}&name=${encodeURIComponent(name)}`, class: 'btn-singleview btn-torrent', title: i18n.torrentFromContent }, renderIcon('torrent')); + })(); + + const chainBtn = blockId && process.env.OASIS_MOBILE !== '1' + ? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer }, renderIcon('block')) : null; - if (!chainBtn && !contentBtn) return null; - return div({ class: 'content-actions' }, chainBtn, contentBtn); + + const herePath = (listScope() || {}).path; + const onItself = (() => { try { return !!herePath && decodeURIComponent(String(viewHref || '').split(/[?#]/)[0]) === decodeURIComponent(String(herePath)); } catch (_) { return false; } })(); + const contentBtn = viewHref && !onItself + ? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent }, renderIcon('open')) + : null; + + const donateBtn = (() => { + try { if (getConfig().modules.walletMod !== 'on') return null; } catch (_) { return null; } + if (!o.author || String(o.author) === String(myId)) return null; + const donatable = sharedState.getDonatableAuthors ? sharedState.getDonatableAuthors() : null; + if (!donatable || !donatable.has(String(o.author))) return null; + const ref = viewHref ? `?returnTo=${encodeURIComponent(viewHref)}` : ''; + return a({ href: `/wallet/donate/${encodeURIComponent(o.author)}${ref}`, class: 'btn-singleview btn-donate', title: i18n.donateTooltip }, renderIcon('donate')); + })(); + + const reportHref = blockId + ? `/reports?filter=create&category=CONTENT&title=${encodeURIComponent(String(o.reportTitle || blockId).slice(0, 120))}&description=${encodeURIComponent(`${viewHref || blockId}`)}` + : null; + const reportBtn = o.report !== false && reportHref && !(o.author && String(o.author) === String(myId)) + ? a({ href: reportHref, class: 'btn-singleview btn-report', title: i18n.reportContent }, renderIcon('report')) + : null; + + const pmBtn = o.contact === true && o.author && String(o.author) !== String(myId) + ? a({ href: `/pm?recipients=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-pm', title: i18n.pmContentTooltip }, renderIcon('mail')) + : null; + const callBtn = o.contact === true && o.author && String(o.author) !== String(myId) && phoneOn() && !sharedState.isPubId(o.author) + ? a({ href: `/phone?filter=create&to=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-call', title: i18n.phoneCallTooltip }, renderIcon('call')) + : null; + + const deleteBtn = o.deleteAction && process.env.OASIS_MOBILE === '1' + ? require('./mobile_ui').deleteConfirm(o.deleteAction, o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null) + : o.deleteAction + ? form({ method: 'POST', action: o.deleteAction, class: 'content-action-form content-action-delete' }, + o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null, + button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close')) + ) + : null; + + const hideBtn = blockId && o.hide !== false && !(o.author && String(o.author) === String(myId)) + ? form({ method: 'POST', action: `/hide/${encodeURIComponent(blockId)}`, class: 'content-action-form' }, + input({ type: 'hidden', name: 'returnTo', value: o.returnTo || viewHref || '/' }), + viewHref ? input({ type: 'hidden', name: 'href', value: viewHref }) : null, + o.reportTitle ? input({ type: 'hidden', name: 'label', value: String(o.reportTitle).slice(0, 120) }) : null, + button({ type: 'submit', class: 'btn-singleview btn-hide', title: i18n.hideContent }, renderHideIcon()) + ) + : null; + + if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn && !hideBtn) return null; + // movil: borrar al final, lejos del pulgar + if (process.env.OASIS_MOBILE === '1') return div({ class: 'content-actions' }, contentBtn, linkBtn, pinBtn, spreadBtn, pmBtn, callBtn, donateBtn, reportBtn, hideBtn, deleteBtn); + return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn); }; exports.renderContentActions = renderContentActions; +const CONTENT_FAV_KIND = { + image: 'images', audio: 'audios', video: 'videos', document: 'documents', + bookmark: 'bookmarks', torrent: 'torrents', file: 'files', event: 'events', task: 'tasks', + report: 'reports', votes: 'votes', poll: 'polls', market: 'market', + housing: 'housing', job: 'jobs', project: 'projects', shop: 'shops', shopProduct: 'shopProducts', + chat: 'chats', chatThread: 'chats', pad: 'pads', room: 'rooms', calendar: 'calendars', + map: 'maps', forum: 'forum', transfer: 'transfers', post: 'blogs', schoolCourse: 'school', + wikiPage: 'wiki', emergency: 'emergencies', mailingList: 'mailing', logisticsRoute: 'logistics', + podcast: 'podcasts', podcastEpisode: 'podcasts', campaign: 'campaigns' +}; +const CONTENT_SPREADABLE = new Set([ + 'post', 'audio', 'video', 'image', 'document', 'torrent', 'file', 'bookmark', + 'event', 'calendar', 'task', 'votes', 'vote', 'market', 'shop', 'shopProduct', + 'project', 'transfer', 'housing', 'job', 'report', 'industry', 'industryBuild', 'industryBlueprint', + 'chat', 'chatMessage', 'pad', 'padEntry', 'room', 'wikiPage', 'emergency', 'mailingList', 'logisticsRoute', 'podcast', 'podcastEpisode', 'campaign', 'forum', 'map', 'poll', 'blog', 'schoolCourse', 'feed' +]); +const CONTENT_TORRENTABLE = new Set(['audio', 'video', 'image', 'document', 'file']); +const CONTENT_DELETE_ROUTE = { + audio: 'audios', video: 'videos', image: 'images', document: 'documents', bookmark: 'bookmarks', + torrent: 'torrents', file: 'files', event: 'events', task: 'tasks', report: 'reports', votes: 'votes', + poll: 'polls', market: 'market', housing: 'housing', job: 'jobs', project: 'projects', shop: 'shops', + shopProduct: 'shops/product', chat: 'chats', pad: 'pads', room: 'rooms', calendar: 'calendars', map: 'maps', mapMarker: 'maps/marker', + forum: 'forum', transfer: 'transfers', emergency: 'emergencies', mailingList: 'mailing', + logisticsRoute: 'logistics', podcast: 'podcasts', podcastEpisode: 'podcasts/episode', campaign: 'campaigns', + feed: 'feed', schoolCourse: 'school', tribe: 'tribes', blog: 'blogs' +}; +const contentDeleteAction = (type, id) => CONTENT_DELETE_ROUTE[type] && id ? `/${CONTENT_DELETE_ROUTE[type]}/delete/${encodeURIComponent(id)}` : undefined; +exports.CONTENT_FAV_KIND = CONTENT_FAV_KIND; +exports.CONTENT_SPREADABLE = CONTENT_SPREADABLE; +exports.CONTENT_TORRENTABLE = CONTENT_TORRENTABLE; +exports.contentDeleteAction = contentDeleteAction; +exports.THEMES = [['Acampada', 'Acampada'], ['Dark-SNH', 'Dark-SNH'], ['Clear-SNH', 'Clear-SNH'], ['Purple-SNH', 'Purple-SNH'], ['Matrix-SNH', 'Matrix-SNH'], ['OasisMobile', 'Oasis-Mobile']]; +// los dos skins de movil; los de escritorio no estan pensados para la maqueta del movil +exports.MOBILE_THEMES = [['Acampada', 'Acampada'], ['OasisMobile', 'Oasis Mobile']]; +exports.WIZARD_THEMES = process.env.OASIS_MOBILE === '1' + ? exports.MOBILE_THEMES + : exports.THEMES.filter(([value]) => value !== 'OasisMobile' && value !== 'Acampada'); +// en el movil la maqueta de la rama (styles/mobile_base.css) va antes de cualquier skin; +// encima, Acampada.css o el OasisMobile.css del dev. hive_acampada.css va detras del +// tema Acampada: pinta lanzador, barras y tarjetas. +const acampadaBase = () => process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_base.css?v=${assetVersion()}` }) : null; +const acampadaLink = (theme) => theme === 'Acampada' ? link({ rel: "stylesheet", href: `/assets/styles/hive_acampada.css?v=${assetVersion()}` }) : null; -const emptyState = ({ icon, title, text, ctaHref, ctaLabel }) => div( - { class: "empty-state" }, - icon ? span({ class: "empty-state-icon" }, icon) : "", - title ? p({ class: "empty-state-title" }, title) : "", - text ? p({ class: "empty-state-text" }, text) : "", - (ctaHref && ctaLabel) ? a({ class: "empty-state-cta filter-btn", href: ctaHref }, ctaLabel) : "" -); -exports.emptyState = emptyState; - -exports.bottomBarPickerView = () => { - const cfg = getConfig(); - const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; - const MAX = 4; - const pinnedKeys = cfg.bottomBarPins || []; - const pinned = pinnedKeys - .map(key => exports.PINNABLE_MODULES.find(m => m.key === key)) - .filter(Boolean); - const full = pinned.length >= MAX; - const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls }, - input({ type: 'hidden', name: 'action', value: action }), - input({ type: 'hidden', name: 'key', value: key }), - node - ); - const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form', - button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' }, - span({ class: 'bb-manage-ico' }, m.glyph), - span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key), - span({ class: 'bb-manage-x' }, '✕') - ) - ); - const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form', - button({ type: 'submit', class: 'bb-pick-btn' }, - span({ class: 'bb-pick-ico' }, m.glyph), - span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key) - ) - ); - const addable = exports.PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key)); - return template( - i18n.bbManage || 'Edit bar', - section( - div({ class: 'tags-header' }, - h2(i18n.bbPickTitle || 'Customize the bar'), - p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.') - ), - div({ class: 'bb-manage' }, - h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`), - pinned.length - ? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow)) - : p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None') - ), - div({ class: 'bb-manage-add' }, - h3(i18n.bbAdd || 'Add'), - full - ? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached') - : div({ class: 'bb-pick-grid' }, ...addable.map(addBtn)) - ), - a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done') +const renderDocumentActions = (kind, id, leadingNodes = []) => { + const base = id ? `/${kind}/${encodeURIComponent(id)}` : `/${kind}`; + return div({ class: 'doc-export-actions' }, + ...leadingNodes, + form({ method: 'GET', action: `${base}/pdf` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.generatePdf) + ), + form({ method: 'POST', action: `${base}/share` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.sharePm) ) ); }; +exports.renderDocumentActions = renderDocumentActions; + +const renderRelationshipBlock = (relationship, feedId) => { + const rel = relationship || {}; + if (rel.me) return span({ class: 'status you' }, i18n.relationshipYou); + const muted = (() => { try { return require('../models/viewer_filters').isMuted(feedId); } catch (_) { return false; } })(); + const actions = []; + const icons = { follow: '♥', unfollow: '✕', block: '⊘', unblock: '↺', mute: '◌', unmute: '●' }; + const addAction = (action) => actions.push( + form({ action: `/${action}/${encodeURIComponent(feedId)}`, method: 'post' }, + button({ type: 'submit', class: 'filter-btn' }, `${icons[action]} ${i18n[action]}`) + ) + ); + if (rel.following) addAction('unfollow'); + else if (rel.blocking) addAction('unblock'); + else { addAction('follow'); addAction('block'); } + if (!rel.blocking) addAction(muted ? 'unmute' : 'mute'); + return div({ class: 'relationship-status' }, + muted ? span({ class: 'status blocked' }, i18n.relationshipMuted) : null, + rel.blocking && rel.blockedBy + ? span({ class: 'status blocked' }, i18n.relationshipMutualBlock) + : [ + rel.blocking ? span({ class: 'status blocked' }, i18n.relationshipBlocking) : null, + rel.blockedBy ? span({ class: 'status blocked-by' }, i18n.relationshipBlockedBy) : null, + rel.following && rel.followsMe + ? span({ class: 'status mutual' }, i18n.relationshipMutuals) + : [ + span({ class: 'status supporting' }, rel.following ? i18n.relationshipFollowing : i18n.relationshipNone), + span({ class: 'status supported-by' }, rel.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) + ] + ], + actions.length ? div({ class: 'relationship-actions' }, ...actions) : null + ); +}; +exports.renderRelationshipBlock = renderRelationshipBlock; exports.renderStateChip = renderStateChip; exports.renderOpenClosedChip = renderOpenClosedChip; @@ -217,7 +545,7 @@ const renderEcoTax = (sizeBytes, blockId) => { ? ` · reducer ×${reducer.toFixed(2)} (${inhabitants} inhabitants)` : ''; const title = `${label} · ${formatCarbon(sizeBytes)}${reducerNote}`; - return a({ href, class: `eco-tax-chip eco-tax-chip-${band}`, title }, + return (process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href }), class: `eco-tax-chip eco-tax-chip-${band}`, title }, span({ class: 'eco-tax-chip-label' }, label + ': '), span({ class: 'eco-tax-chip-value' }, formatCarbon(sizeBytes)) ); @@ -226,6 +554,101 @@ const renderEcoTax = (sizeBytes, blockId) => { exports.formatCarbon = formatCarbon; exports.renderEcoTax = renderEcoTax; +const renderEcoValueChip = () => { + const v = Number(sharedState.getEcoValue ? sharedState.getEcoValue() : NaN); + if (!Number.isFinite(v) || v <= 0) return null; + const title = (i18n && i18n.bankExchangeCurrentValue) ? i18n.bankExchangeCurrentValue : 'ECOin Value (1h)'; + return a({ href: '/banking?filter=exchange', class: 'eco-tax-chip eco-tax-chip-mid eco-value-chip', title }, + span({ class: 'eco-tax-chip-label' }, 'ECO/h: '), + span({ class: 'eco-tax-chip-value' }, v.toFixed(4)) + ); +}; +exports.renderEcoValueChip = renderEcoValueChip; + +const renderMobileCounters = () => { + if (process.env.OASIS_MOBILE === '1') return null; + try { + const theme = (getConfig().themes || {}).current; + if (theme !== 'OasisMobile' && process.env.OASIS_MOBILE !== '1') return null; + } catch (_) { return null; } + const inbox = Number(sharedState.getInboxCount()) || 0; + const mentions = Number(sharedState.getMentionsCount()) || 0; + const phone = phoneOn() ? (Number(sharedState.getPhoneCount()) || 0) : 0; + if (inbox <= 0 && mentions <= 0 && phone <= 0) return null; + return div({ class: 'mobile-counters' }, + inbox > 0 ? a({ href: '/inbox', class: 'mobile-counter', title: i18n.inbox }, '☂ ', String(inbox)) : null, + phone > 0 ? a({ href: '/phone', class: 'mobile-counter', title: i18n.phoneTitle }, '✆ ', String(phone)) : null, + mentions > 0 ? a({ href: '/mentions', class: 'mobile-counter', title: i18n.mentions }, '✺ ', String(mentions)) : null + ); +}; + +const LIST_PAGE_SIZE = 50; +const LIST_PAGE_SIZES = ['10', '50', '100', 'all']; +const listScope = () => { try { return require('../models/typed_log').requestScope.getStore() || null; } catch (_) { return null; } }; +const listPerPage = (query) => { + const v = String(new URLSearchParams(String(query || '')).get('perPage') || '').toLowerCase(); + return LIST_PAGE_SIZES.includes(v) ? v : String(LIST_PAGE_SIZE); +}; +const slicePage = (list, page, per) => { + const all = Array.isArray(list) ? list : []; + const size = per === 'all' ? Math.max(1, all.length) : Number(per) || LIST_PAGE_SIZE; + const pages = Math.max(1, Math.ceil(all.length / size)); + const current = Math.min(pages, Math.max(1, parseInt(page, 10) || 1)); + return { items: all.slice((current - 1) * size, current * size), page: current, pages, total: all.length, per: String(per) }; +}; +const showPageSizes = (total, query) => total > Number(LIST_PAGE_SIZES[0]) && (total > LIST_PAGE_SIZE || new URLSearchParams(String(query || '')).has('perPage')); +const pageSizeLabel = (s) => s === 'all' ? String(i18n.all || 'All').toUpperCase() : s; +exports.LIST_PAGE_SIZE = LIST_PAGE_SIZE; +exports.LIST_PAGE_SIZES = LIST_PAGE_SIZES; +exports.listPerPage = listPerPage; +exports.slicePage = slicePage; +exports.showPageSizes = showPageSizes; +exports.pageSizeLabel = pageSizeLabel; +const paged = (list) => { + const store = listScope(); + const query = store ? store.query : ''; + const { items, ...pager } = slicePage(list, new URLSearchParams(String(query || '')).get('page'), listPerPage(query)); + if (store) { store.pager = pager; store.pageItems = items; } + return items; +}; +exports.paged = paged; +exports.currentPerPage = () => { const store = listScope(); return listPerPage(store ? store.query : ''); }; +const renderListPager = () => { + const store = listScope(); + const pg = store && store.pager; + if (!pg) return null; + const sizes = showPageSizes(pg.total, store.query); + if (!(pg.pages > 1) && !sizes) return null; + const href = (n, per) => { + const q = new URLSearchParams(String(store.query || '')); + for (const k of ['page', 'error', 'errsig']) q.delete(k); + if (per) q.set('perPage', per); + if (n > 1) q.set('page', String(n)); + const s = q.toString(); + return s ? `${store.path}?${s}` : store.path; + }; + return nav({ class: 'oasis-pager' }, + sizes ? span({ class: 'oasis-pager-sizes' }, + span({ class: 'oasis-pager-info' }, i18n.searchPerPageLabel || 'Results per page'), + ...LIST_PAGE_SIZES.map(s => a({ href: href(1, s), class: s === pg.per ? 'oasis-pager-btn active' : 'oasis-pager-btn' }, pageSizeLabel(s))) + ) : null, + pg.pages > 1 ? span({ class: 'oasis-pager-info' }, String(i18n.cnPageOf || 'Page {page} of {pages}').replace('{page}', String(pg.page)).replace('{pages}', String(pg.pages))) : null, + pg.page > 1 ? a({ href: href(pg.page - 1), class: 'oasis-pager-btn' }, i18n.cnPrevPage || '← Previous') : null, + pg.page < pg.pages ? a({ href: href(pg.page + 1), class: 'oasis-pager-btn' }, i18n.cnNextPage || 'Next →') : null + ); +}; + +const renderLogWindowNote = () => { + let store = null; + try { store = require('../models/typed_log').requestScope.getStore(); } catch (_) { store = null; } + if (!store || !store.capped || !(store.limit > 0)) return null; + return div({ class: 'log-window-note' }, + a({ href: '/settings#logstream', class: 'eco-tax-chip eco-tax-chip-low log-window-chip', title: i18n.ssbLogStreamDescription }, + span({ class: 'eco-tax-chip-value' }, String(i18n.logWindowChip).replace('{n}', String(store.limit))) + ) + ); +}; + const errorView = ({ title, message, backHref }) => { const heading = title || i18n.errorPageTitle || 'Error'; return exports.template( @@ -242,27 +665,85 @@ const errorView = ({ title, message, backHref }) => { }; exports.errorView = errorView; -const renderSpreadButton = (msgKey, opts) => { +const keepTextView = ({ title, action, fields = [], hidden = [], enctype = null, backHref = null }) => { + const formAttrs = { method: 'POST', action }; + if (enctype) formAttrs.enctype = enctype; + return exports.template( + title || i18n.errorPageTitle || 'Error', + section( + div({ class: 'tags-header' }, + h2(title || ''), + form(formAttrs, + ...hidden.map(h => input({ type: 'hidden', name: h.name, value: String(h.value == null ? '' : h.value) })), + ...fields.map(f => { + const attrs = { name: f.name, id: f.name }; + if (f.maxlength) attrs.maxlength = String(f.maxlength); + if (f.required) attrs.required = true; + const value = String(f.value == null ? '' : f.value); + const control = f.type === 'textarea' + ? textarea({ ...attrs, rows: f.rows || 10 }, value) + : input({ ...attrs, type: 'text', value }); + return [label({ for: f.name }, f.label || f.name), br(), control, br(), br()]; + }).flat(), + button({ type: 'submit', class: 'create-button' }, i18n.keepTextRetry), + ' ', + backHref ? a({ href: backHref, class: 'filter-btn' }, i18n.goBack || 'Go back') : null + ) + ) + ) + ); +}; +exports.keepTextView = keepTextView; + +const confirmView = ({ title, message, action, hidden = [], backHref = null }) => exports.template( + title || i18n.confirmActionTitle, + section( + div({ class: 'tags-header' }, + h2(title || i18n.confirmActionTitle), + message ? p({ class: 'error-page-message' }, String(message)) : null, + form({ method: 'POST', action, class: 'confirm-actions' }, + ...hidden.map(h => input({ type: 'hidden', name: h.name, value: String(h.value == null ? '' : h.value) })), + button({ type: 'submit', class: 'create-button' }, i18n.confirmActionButton), + ' ', + a({ href: backHref || '/', class: 'filter-btn' }, i18n.goBack || 'Go back') + ) + ) + ) +); +exports.confirmView = confirmView; + +exports.renderInlineError = (message, dismissHref) => section({ class: 'inline-error' }, + div({ class: 'tags-header inline-error-box' }, + p({ class: 'error-page-message' }, String(message || '')), + dismissHref ? a({ href: dismissHref, class: 'inline-error-dismiss' }, i18n.errorDismiss || 'OK') : null + ) +).outerHTML; + +const renderVotesSummary = (opinions = {}) => { + const entries = Object.entries(opinions).filter(([, v]) => Number(v) > 0); + if (!entries.length) return null; + entries.sort((a, b) => Number(b[1]) - Number(a[1]) || String(a[0]).localeCompare(String(b[0]))); + return div( + { class: "votes feed-votes-summary" }, + entries.map(([category, count]) => { + const label = i18n['vote' + category.charAt(0).toUpperCase() + category.slice(1)] || category; + return span({ class: "vote-category" }, `${label} ${count}`); + }) + ); +}; +exports.renderVotesSummary = renderVotesSummary; + +const renderSpreadButton = (msgKey, opts, title = null, showCount = true) => { if (!msgKey || typeof msgKey !== 'string' || !msgKey.startsWith('%') || !/\.sha256$/.test(msgKey)) return null; const o = (opts && typeof opts === 'object') ? opts : {}; const voters = Array.isArray(o.voters) ? o.voters : []; const count = typeof o.count === 'number' ? o.count : voters.length; const alreadySpread = o.alreadySpread === true; - const maxNames = 5; - const maxLen = 16; - const lastVoters = voters.slice(-maxNames); - const tooltipNames = lastVoters - .map(v => (v && typeof v === 'object' ? (v.name || v.key || '') : String(v || ''))) - .filter(Boolean) - .map(n => n.slice(0, maxLen)) - .join(', '); - const extra = count > maxNames ? ` +${count - maxNames} ${i18n.spreadMore || 'more'}` : ''; - const tooltip = count > 0 ? `${tooltipNames}${extra}` : (i18n.spreadHint || 'Spread this to your supporters (replicates via your feed).'); return form( { method: 'POST', action: `/spread/${encodeURIComponent(msgKey)}`, class: 'spread-form' }, button( - { type: 'submit', class: alreadySpread ? 'spread-btn spread-btn-on' : 'spread-btn', title: tooltip }, - `🔁 ${count}` + { type: 'submit', class: alreadySpread ? 'btn-singleview btn-spread-on' : 'btn-singleview', title: title || i18n.spreadContent }, + renderIcon('spread'), showCount ? span({ class: 'btn-count' }, String(count)) : null ) ); }; @@ -274,7 +755,16 @@ const aiNavResultsView = ({ query, results }) => { const safeResults = Array.isArray(results) ? results : []; const fmtScore = (s) => { const n = Number(s); - return Number.isFinite(n) ? n.toFixed(2) : '—'; + return Number.isFinite(n) ? `${Math.round(n * 100)}%` : '—'; + }; + const routeLabel = (p) => { + const [base, qs] = String(p || '').split('?'); + let label = decodeURIComponent(base).replace(/^\//, '').replace(/\//g, ' · '); + try { + const filter = new URLSearchParams(qs || '').get('filter'); + if (filter) label += ` · ${filter}`; + } catch (_) {} + return label.toUpperCase(); }; const splitTerms = (desc) => String(desc || '') .split(/[,;]/) @@ -292,7 +782,7 @@ const aiNavResultsView = ({ query, results }) => { safeResults.map(r => div({ class: 'ai-nav-result-card card-section' }, div({ class: 'card-field' }, span({ class: 'card-label' }, `${(i18n.aiNavResultMatch || 'Match').toUpperCase()}: ${fmtScore(r.score)}`), - span({ class: 'card-value' }, a({ href: r.path, class: 'filter-btn' }, r.path)) + span({ class: 'card-value' }, a({ href: r.path, class: 'filter-btn' }, routeLabel(r.path))) ) )) ) @@ -306,39 +796,112 @@ const i18nBase = require("../client/assets/translations/i18n"); let selectedLanguage = "en"; let i18n = {}; Object.assign(i18n, i18nBase[selectedLanguage]); +exports.getLanguage = () => (i18nBase[selectedLanguage] ? selectedLanguage : 'en'); exports.setLanguage = (language) => { + if (language === selectedLanguage) return; selectedLanguage = language; const newLang = i18nBase[selectedLanguage] || i18nBase['en']; Object.keys(i18n).forEach(k => delete i18n[k]); Object.assign(i18n, newLang); + const momentLocale = moment.locale(selectedLanguage === 'zh' ? 'zh-cn' : (i18nBase[selectedLanguage] ? selectedLanguage : 'en')); + if (moment.localeData(momentLocale).postformat('1') !== '1') moment.updateLocale(momentLocale, { preparse: (str) => str, postformat: (str) => str }); }; exports.i18n = i18n; Object.defineProperty(exports, 'selectedLanguage', { get: () => selectedLanguage }); const opinionCategoriesList = require('../backend/opinion_categories'); -const renderOpinionsVoting = (basePath, id, opinions, returnTo, voters) => { +const renderEngagement = (id, opinionsNode, commentsNode) => { + const parts = [opinionsNode, commentsNode].filter(Boolean); + if (!parts.length) return null; + return div({ class: 'card-engage' }, ...parts); +}; +exports.renderEngagement = renderEngagement; + +const renderModuleStats = (total, segments = [], totalLabel = null, opts = {}) => { + if (!(Number(total) > 0)) return null; + const inh = opts.showInhabitants && sharedState.getInhabitantCount ? sharedState.getInhabitantCount() : 0; + const trb = opts.showTribes && sharedState.getTribesCount ? sharedState.getTribesCount() : 0; + const network = [ + inh > 0 ? { label: i18n.inhabitants, count: inh } : null, + trb > 0 ? { label: i18n.tribesTitle, count: trb } : null + ]; + const toNode = s => span(s.labelClass ? span({ class: s.labelClass }, `${s.label}: `) : `${s.label}: `, strong(String(s.count))); + return div({ class: 'module-stats-line' }, + ...network.filter(Boolean).map(toNode), + span(`${totalLabel || i18n.statsTotalLabel}: `, strong(String(total))), + ...segments.filter(s => s && Number(s.count) > 0).map(toNode) + ); +}; +exports.renderModuleStats = renderModuleStats; + +const renderModuleStatsBy = (items, field, specs, totalLabel = null) => { + const arr = Array.isArray(items) ? items : []; + const counts = {}; + for (const it of arr) { const v = typeof field === 'function' ? field(it) : (it && it[field]); counts[v] = (counts[v] || 0) + 1; } + return renderModuleStats(arr.length, specs.map(s => ({ label: s.label, count: counts[s.value] || 0 })), totalLabel); +}; +exports.renderModuleStatsBy = renderModuleStatsBy; + +const moduleIsEmpty = (items, filter, defaultFilter = 'all', q = '') => + (!items || items.length === 0) && + String(filter || defaultFilter).toUpperCase() === String(defaultFilter).toUpperCase() && + !String(q || '').trim(); +exports.moduleIsEmpty = moduleIsEmpty; + +const renderCardMetaRow = (...nodes) => { + const flat = nodes.flat(Infinity).filter(Boolean); + if (!flat.length) return null; + return div({ class: 'card-meta-row' }, ...flat); +}; +exports.renderCardMetaRow = renderCardMetaRow; + +const renderOpinionsVoting = (basePath, id, opinions, returnTo, voters, pathFor = null) => { const ops = opinions || {}; const total = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); const myId = (config.keys && config.keys.id) ? config.keys.id : ''; const alreadyVoted = Array.isArray(voters) && myId ? voters.includes(myId) : false; - return details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, - `${i18n.opinionsTitle || 'Opinions'} (${total})`), + const actionFor = (category) => typeof pathFor === 'function' ? pathFor(id, category) : `${basePath}/${encodeURIComponent(id)}/${category}`; + const votingDetails = details({ class: 'opinions-voting-collapse' }, + summary({ class: total > 0 ? 'opinions-summary engage-on' : 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${total})`)), div({ class: 'voting-buttons' }, opinionCategoriesList.map((category) => - form({ method: 'POST', action: `${basePath}/${encodeURIComponent(id)}/${category}` }, + form({ method: 'POST', action: actionFor(category) }, returnTo ? input({ type: 'hidden', name: 'returnTo', value: returnTo }) : null, button({ class: alreadyVoted ? 'vote-btn disabled' : 'vote-btn', type: 'submit', ...(alreadyVoted ? { disabled: true } : {}) }, - `${i18n['vote' + category.charAt(0).toUpperCase() + category.slice(1)] || category} [${ops[category] || 0}]`) + `${String(i18n['vote' + category.charAt(0).toUpperCase() + category.slice(1)] || category).toUpperCase()} [${ops[category] || 0}]`) ) ) ), alreadyVoted ? p({ class: 'muted' }, i18n.alreadyVoted) : null ); + return [renderVotesSummary(ops), votingDetails].filter(Boolean); }; exports.renderOpinionsVoting = renderOpinionsVoting; -// markdown +let spreadsModel = null; +const spreadsFor = async (msgId) => { + if (!msgId || typeof msgId !== 'string' || !msgId.startsWith('%')) return 0; + try { + if (!spreadsModel) spreadsModel = require('../models/main_models')({ cooler, isPublic: config.public }).spreads; + const info = await spreadsModel.forMessage(msgId); + return info && Number(info.count) > 0 ? Number(info.count) : 0; + } catch (_) { + return 0; + } +}; + +const renderSpreadEditWarning = async (msgId) => { + const count = await spreadsFor(msgId); + if (!count) return null; + return div({ class: 'error-box spread-edit-warning' }, + p(`${i18n.spreadEditWarning} (${count})`) + ); +}; +exports.renderSpreadEditWarning = renderSpreadEditWarning; + + const markdownUrl = "https://commonmark.org/help/"; const doctypeString = ""; @@ -353,8 +916,37 @@ const nbsp = "\xa0"; const { getConfig } = require('../configs/config-manager.js'); -// menu INIT -const readPkg = () => { +const shortCache = (ttl, fn) => { + let at = 0; + let value; + return () => { + const now = Date.now(); + if (!at || now - at > ttl) { value = fn(); at = now; } + return value; + }; +}; + +const computeAssetVersion = () => { + try { + const base = path.resolve(__dirname, "..", "client", "assets"); + let latest = 0; + for (const rel of ["styles", "themes"]) { + const dir = path.join(base, rel); + for (const f of fs.readdirSync(dir)) { + const t = fs.statSync(path.join(dir, f)).mtimeMs; + if (t > latest) latest = t; + } + } + const v = String(readPkg()?.version || '0'); + return encodeURIComponent(latest ? `${v}-${Math.floor(latest / 1000).toString(36)}` : v); + } catch (_) { + try { return encodeURIComponent(String(readPkg()?.version || '0')); } catch (_) { return '0'; } + } +}; + +const assetVersion = shortCache(5000, () => computeAssetVersion()); + +const readPkgFile = () => { const file = path.resolve(__dirname, "..", "server", "package.json"); try { const txt = fs.readFileSync(file, "utf8"); @@ -364,6 +956,7 @@ const readPkg = () => { return {}; } }; +const readPkg = shortCache(30000, readPkgFile); const renderFooter = () => { const pkg = readPkg(); @@ -375,11 +968,13 @@ const renderFooter = () => { const hcH = sharedState.getCarbonHcH(); const peersOnline = sharedState.getOnlinePeerCount ? sharedState.getOnlinePeerCount() : null; + const syncedPeers = sharedState.getSyncedPeerCount ? sharedState.getSyncedPeerCount() : null; const inboxUnread = sharedState.getInboxUnreadCount ? sharedState.getInboxUnreadCount() : null; const lastSyncTs = sharedState.getLastSyncTs ? sharedState.getLastSyncTs() : null; const lastSyncLabel = lastSyncTs ? moment(lastSyncTs).fromNow() : '–'; const lastActivity = sharedState.getLastActivity ? sharedState.getLastActivity() : null; + if (process.env.OASIS_MOBILE === '1') return require('./mobile_ui').footerLine(syncedPeers, pkgVersion); return div( { class: "oasis-footer" }, div( @@ -407,11 +1002,17 @@ const renderFooter = () => { ]; })(), br(), + span({ class: "oasis-footer-peers" }, + span("Synced-peers: [ "), + a({ href: "/peers" }, String(syncedPeers || 0)), + span(" ]") + ), + br(), span({ class: "oasis-footer-carbon" }, span("HcT: "), - a({ href: "/stats?filter=ALL" }, hcT != null ? String(hcT) : '–'), + a({ href: "/stats?filter=ALL#carbon" }, hcT != null ? String(hcT) : '–'), span(" | HcH: "), - a({ href: "/stats?filter=MINE" }, hcH != null ? String(hcH) : '–') + a({ href: "/stats?filter=MINE#carbon" }, hcH != null ? String(hcH) : '–') ), br(), a( @@ -447,30 +1048,43 @@ const navLink = ({ href, emoji, text, current, class: extraClass }) => ) ); -const customCSS = (filename) => { - const customStyleFile = path.join( - envPaths("oasis", { suffix: "" }).config, - filename - ); - try { - if (fs.existsSync(customStyleFile)) { - return link({ rel: "stylesheet", href: filename }); - } - } catch (error) { - return ""; +const currentNavPath = () => { + let store = null; + try { store = require('../models/typed_log').requestScope.getStore(); } catch (_) { store = null; } + return store && typeof store.path === 'string' ? store.path : ''; +}; + +const firstHref = (node) => { + if (!node || typeof node !== 'object') return ''; + const own = typeof node.href === 'string' && node.href ? node.href : (typeof node.getAttribute === 'function' ? node.getAttribute('href') : null); + if (own) return String(own); + for (const child of node.childNodes || []) { + const found = firstHref(child); + if (found) return found; } + return ''; +}; + +const navGroupHasPath = (items, path) => { + if (!path) return false; + return items.flat(Infinity).some(item => { + const href = firstHref(item).split(/[?#]/)[0]; + if (!href || href === '/') return false; + return path === href || path.startsWith(href + '/'); + }); }; const navGroup = ({ id, emoji, title, defaultOpen = false }, ...items) => { const active = items.filter(Boolean); if (!active.length) return null; + const open = process.env.OASIS_MOBILE !== '1' && (defaultOpen || navGroupHasPath(active, currentNavPath())); return li( { class: "oasis-nav-group" }, input({ type: "checkbox", id: `oasis-nav-group-${id}`, class: "oasis-nav-toggle", - ...(defaultOpen ? { checked: true } : {}) + ...(open ? { checked: true } : {}) }), label( { for: `oasis-nav-group-${id}`, class: "oasis-nav-header" }, @@ -482,72 +1096,36 @@ const navGroup = ({ id, emoji, title, defaultOpen = false }, ...items) => { ); }; -const renderPopularLink = () => { - const popularMod = getConfig().modules.popularMod === "on"; - return popularMod + + + + + + +const renderPollsLink = () => { + const pollsMod = getConfig().modules.pollsMod === "on"; + return pollsMod ? navLink({ - href: "/public/popular/day", - emoji: "⌘", - text: i18n.popular, - class: "popular-link enabled" + href: "/polls", + emoji: "☑", + text: i18n.pollsTitle, + class: "polls-link enabled" }) : ""; }; -const renderTopicsLink = () => { - const topicsMod = getConfig().modules.topicsMod === "on"; - return topicsMod +const renderBlogsLink = () => { + const blogsMod = getConfig().modules.blogsMod === "on"; + return blogsMod ? navLink({ - href: "/public/latest/topics", - emoji: "ϟ", - text: i18n.topics, - class: "topics-link enabled" + href: "/blogs", + emoji: "✦", + text: i18n.blogTitle, + class: "blogs-link enabled" }) : ""; }; -const renderSummariesLink = () => { - const summariesMod = getConfig().modules.summariesMod === "on"; - if (summariesMod) { - return [ - navLink({ - href: "/public/latest/summaries", - emoji: "※", - text: i18n.summaries, - class: "summaries-link enabled" - }) - ]; - } - return ""; -}; - -const renderLatestLink = () => { - const latestMod = getConfig().modules.latestMod === "on"; - return latestMod - ? navLink({ - href: "/public/latest", - emoji: "☄", - text: i18n.latest, - class: "latest-link enabled" - }) - : ""; -}; - -const renderThreadsLink = () => { - const threadsMod = getConfig().modules.threadsMod === "on"; - if (threadsMod) { - return [ - navLink({ - href: "/public/latest/threads", - emoji: "♺", - text: i18n.threads, - class: "threads-link enabled" - }) - ]; - } - return ""; -}; - const renderInvitesLink = () => { const invitesMod = getConfig().modules.invitesMod === "on"; return invitesMod @@ -575,15 +1153,30 @@ const renderWalletLink = () => { return ""; }; -const renderLegacyLink = () => { - const legacyMod = getConfig().modules.legacyMod === "on"; - if (legacyMod) { +const renderDevLink = () => { + const devMod = getConfig().modules.devMod === "on"; + if (devMod) { return [ navLink({ - href: "/legacy", + href: "/dev", + emoji: "⌘", + text: i18n.developer, + class: "dev-link enabled" + }) + ]; + } + return ""; +}; + +const renderBackupLink = () => { + const backupMod = getConfig().modules.backupMod === "on"; + if (backupMod) { + return [ + navLink({ + href: "/backup", emoji: "ꖤ", - text: i18n.legacy, - class: "legacy-link enabled" + text: i18n.backupTitle, + class: "backup-link enabled" }) ]; } @@ -638,7 +1231,7 @@ const renderImagesLink = () => { return [ navLink({ href: "/images", - emoji: "ꕥ", + emoji: "▦", text: i18n.imagesLabel, class: "images-link enabled" }) @@ -647,6 +1240,21 @@ const renderImagesLink = () => { return ""; }; +const renderFilesLink = () => { + const filesMod = getConfig().modules.filesMod === "on"; + if (filesMod) { + return [ + navLink({ + href: "/files", + emoji: "▣", + text: i18n.filesLabel, + class: "files-link enabled" + }) + ]; + } + return ""; +}; + const renderTorrentsLink = () => { const torrentsMod = getConfig().modules.torrentsMod === "on"; if (torrentsMod) { @@ -751,21 +1359,19 @@ const renderTagsLink = () => { : ""; }; -const renderMultiverseLink = () => { - const multiverseMod = getConfig().modules.multiverseMod === "on"; - return multiverseMod - ? navLink({ - href: "/public/latest/extended", - emoji: "∞", - text: i18n.multiverse, - class: "multiverse-link enabled" - }) - : ""; + +const hasMultiverseAccount = () => { + try { + const store = JSON.parse(fs.readFileSync(require('../configs/state-manager').statePath('fediverse-accounts.json'), 'utf8')); + return !!(store && ((store.mastodon && store.mastodon.token) || (store.peertube && store.peertube.token) || (store.telegram && store.telegram.session))); + } catch (_) { + return false; + } }; const renderMastodonLink = () => { const fediverseMod = getConfig().modules.fediverseMod === "on"; - return fediverseMod + return fediverseMod && hasMultiverseAccount() ? navLink({ href: "/fediverse", emoji: "ꗵ", @@ -787,6 +1393,19 @@ const renderMarketLink = () => { : ""; }; +const renderHousingLink = () => { + const housingMod = getConfig().modules.housingMod === "on"; + return housingMod + ? [ + navLink({ + href: "/housing", + emoji: "⌂", + text: i18n.housingTitle + }) + ] + : ""; +}; + const renderJobsLink = () => { const jobsMod = getConfig().modules.jobsMod === "on"; return jobsMod @@ -826,6 +1445,19 @@ const renderProjectsLink = () => { : ""; }; +const renderIndustryLink = () => { + const industryMod = getConfig().modules.industryMod === "on"; + return industryMod + ? [ + navLink({ + href: "/industry", + emoji: "ꖴ", + text: i18n.industryTitle + }) + ] + : ""; +}; + const renderBankingLink = () => { const bankingMod = getConfig().modules.bankingMod === "on"; return bankingMod @@ -857,7 +1489,7 @@ const renderLarpLink = () => { ? [ navLink({ href: "/larp", - emoji: "✦", + emoji: "♜", text: i18n.larpTitle || "L.A.R.P.", class: "larp-link enabled" }) @@ -879,6 +1511,18 @@ const renderParliamentLink = () => { : ""; }; +const renderSchoolLink = () => { + const schoolMod = getConfig().modules.schoolMod === "on"; + return schoolMod + ? navLink({ + href: "/school", + emoji: "ꖂ", + text: i18n.schoolTitle, + class: "school-link enabled" + }) + : ""; +}; + const renderCourtsLink = () => { const courtsMod = getConfig().modules.courtsMod === "on"; return courtsMod @@ -891,6 +1535,78 @@ const renderCourtsLink = () => { : ""; }; +const renderEmergenciesLink = () => { + const emergenciesMod = getConfig().modules.emergenciesMod === "on"; + return emergenciesMod + ? navLink({ + href: "/emergencies", + emoji: "ꕥ", + text: i18n.emergenciesTitle, + class: "emergencies-link enabled" + }) + : ""; +}; + +const renderCampaignsLink = () => { + const campaignsMod = getConfig().modules.campaignsMod === "on"; + return campaignsMod + ? navLink({ + href: "/campaigns", + emoji: "✍\ufe0e", + text: i18n.campaignsTitle, + class: "campaigns-link enabled" + }) + : ""; +}; + +const renderPodcastsLink = () => { + const podcastsMod = getConfig().modules.podcastsMod === "on"; + return podcastsMod + ? navLink({ + href: "/podcasts", + emoji: "ꖜ", + text: i18n.podcastsTitle, + class: "podcasts-link enabled" + }) + : ""; +}; + +const renderLogisticsLink = () => { + const logisticsMod = getConfig().modules.logisticsMod === "on"; + return logisticsMod + ? navLink({ + href: "/logistics", + emoji: "⇄", + text: i18n.logisticsTitle, + class: "logistics-link enabled" + }) + : ""; +}; + +const renderRoomsLink = () => { + const roomsMod = getConfig().modules.roomsMod === "on"; + return roomsMod + ? navLink({ + href: "/rooms", + emoji: "ꘒ", + text: i18n.roomsTitle, + class: "rooms-link enabled" + }) + : ""; +}; + +const renderMailingLink = () => { + const mailingMod = getConfig().modules.mailingMod === "on"; + return mailingMod + ? navLink({ + href: "/mailing", + emoji: "✉", + text: i18n.mailingTitle, + class: "mailing-link enabled" + }) + : ""; +}; + const renderVotationsLink = () => { const votesMod = getConfig().modules.votesMod === "on"; return votesMod @@ -925,7 +1641,7 @@ const renderReportsLink = () => { ? [ navLink({ href: "/reports", - emoji: "ꕥ", + emoji: "⚐", text: i18n.reportsTitle, class: "reports-link enabled" }) @@ -961,6 +1677,20 @@ const renderPadsLink = () => { : ""; }; +const renderWikiLink = () => { + const wikiMod = getConfig().modules.wikiMod === "on"; + return wikiMod + ? [ + navLink({ + href: "/wiki", + emoji: "¶", + text: i18n.wikiTitle, + class: "wiki-link enabled" + }) + ] + : ""; +}; + const renderTransfersLink = () => { const transfersMod = getConfig().modules.transfersMod === "on"; return transfersMod @@ -1134,183 +1864,152 @@ const renderTasksLink = () => { : ""; }; -const renderMobileTopbar = () => div( - { class: "oasis-mobile-topbar" }, - a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })), - a({ class: "omt-title", href: "/" }, "OASIS"), - a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" })) -); - -const PINNABLE_MODULES = [ - { key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null }, - { key: 'inbox', href: '/inbox', glyph: '☂', labelKey: 'inbox', mod: null }, - { key: 'publish', href: '/publish', glyph: '❂', labelKey: 'publish', mod: null }, - { key: 'activity', href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, - { key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, - { key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, - { key: 'market', href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, - { key: 'events', href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, - { key: 'calendars', href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, - { key: 'banking', href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, - { key: 'forum', href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, - { key: 'videos', href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' }, - { key: 'images', href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, - { key: 'audios', href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, - { key: 'documents', href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, - { key: 'bookmarks', href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, - { key: 'maps', href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, - { key: 'trending', href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, - { key: 'jobs', href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, - { key: 'agenda', href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, - { key: 'wallet', href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, - { key: 'tasks', href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, - { key: 'torrents', href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, - { key: 'tags', href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, - { key: 'pads', href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, - { key: 'projects', href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, - { key: 'shops', href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, - { key: 'transfers', href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' }, - { key: 'feed', href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, - { key: 'pixelia', href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, - { key: 'melody', href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' }, - { key: 'games', href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, - { key: 'votes', href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, - { key: 'reports', href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' }, - { key: 'opinions', href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, - { key: 'parliament',href: '/parliament',glyph: 'ꗞ', labelKey: 'parliamentTitle',mod: 'parliamentMod' }, - { key: 'courts', href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' }, - { key: 'larp', href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, - { key: 'ai', href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, - { key: 'cipher', href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, - { key: 'fediverse', href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' }, - { key: 'inhabitants', href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, - { key: 'cv', href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null }, - { key: 'stats', href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null }, - { key: 'blockexplorer', href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null }, -]; -exports.PINNABLE_MODULES = PINNABLE_MODULES; - -const renderBottomBar = () => { - const inboxN = sharedState.getInboxCount(); - const cfg = getConfig(); - const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; - const bbLink = (href, ico, label, badge) => a( - { class: "bb-item", href }, - span({ class: "bb-ico" }, ico), - (badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "", - span({ class: "bb-lbl" }, label) - ); - const MAX = 4; - const pins = (cfg.bottomBarPins || []) - .map(key => PINNABLE_MODULES.find(m => m.key === key)) - .filter(m => m && modOn(m.mod)) - .slice(0, MAX); - const slots = pins.map(m => bbLink( - m.href, m.glyph, i18n[m.labelKey] || m.key, m.key === 'inbox' ? inboxN : 0 - )); - const editSlot = a( - { class: "bb-item bb-add", href: "/settings/bottombar" }, - span({ class: "bb-ico" }, pins.length < MAX ? "+" : "✎"), - span({ class: "bb-lbl" }, pins.length < MAX ? (i18n.bbAdd || "Add") : (i18n.bbManage || "Edit")) - ); - const pair = [ bbLink("/peers", "⧖", i18n.peers || "Peers") ]; - if (modOn('invitesMod')) pair.push(bbLink("/invites", "ꔮ", i18n.invites || "Invites")); - return nav( - { class: "oasis-bottombar", style: "display:none", "aria-label": i18n.bbQuickActions || "Quick actions" }, - ...slots, - editSlot, - div({ class: "bb-pair" }, ...pair) - ); -}; - -const RADIAL_ITEMS = [ - { key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null }, - { key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, - { key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, - { key: 'market', href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, - { key: 'events', href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, - { key: 'forum', href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, - { key: 'agenda', href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, -]; - -const renderHiveNav = () => { - if (process.env.OASIS_MOBILE !== '1') return ""; - const cfg = getConfig(); - const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; - const CATS = [ - { id: 'personal', emoji: '⚉', title: i18n.menuPersonal, mods: [ - { href: '/profile', glyph: '⚉', labelKey: 'profile', mod: null }, { href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null }, - { href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, { href: '/favorites', glyph: 'ꘝ', labelKey: 'favoritesTitle', mod: 'favoritesMod' }, - { href: '/logs', glyph: 'ꗯ', labelKey: 'logsTitle', mod: 'logsMod' }, { href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, - { href: '/modules', glyph: 'ꗣ', labelKey: 'modules', mod: null }, { href: '/settings', glyph: '⚙', labelKey: 'settings', mod: null } ] }, - { id: 'governance', emoji: '⚖', title: i18n.menuGovernance, mods: [ - { href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, { href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, - { href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, { href: '/parliament', glyph: 'ꗞ', labelKey: 'parliamentTitle', mod: 'parliamentMod' }, - { href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' } ] }, - { id: 'office', emoji: '⌂', title: i18n.menuOffice, mods: [ - { href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, { href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, - { href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, { href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, - { href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' } ] }, - { id: 'economy', emoji: '¤', title: i18n.menuEconomy, mods: [ - { href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, { href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, - { href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, { href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, - { href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, { href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' } ] }, - { id: 'tools', emoji: '⚒', title: i18n.menuTools, mods: [ - { href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, { href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null }, - { href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, { href: '/invites', glyph: 'ꔹ', labelKey: 'invites', mod: 'invitesMod' }, - { href: '/legacy', glyph: 'ꖤ', labelKey: 'legacy', mod: 'legacyMod' }, { href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null } ] }, - { id: 'network', emoji: '☍', title: i18n.menuNetwork, mods: [ - { href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, { href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, - { href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, { href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, - { href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, { href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, - { href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, { href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' } ] }, - { id: 'blogs', emoji: '✦', title: i18n.menuBlogs, mods: [ - { href: '/mentions', glyph: '✺', labelKey: 'mentions', mod: null }, { href: '/public/latest', glyph: '☄', labelKey: 'latest', mod: 'latestMod' }, - { href: '/public/latest/threads', glyph: '♺', labelKey: 'threads', mod: 'threadsMod' }, { href: '/public/latest/topics', glyph: 'ϟ', labelKey: 'topics', mod: 'topicsMod' }, - { href: '/public/latest/summaries', glyph: '※', labelKey: 'summaries', mod: 'summariesMod' }, { href: '/public/popular/day', glyph: '⌘', labelKey: 'popular', mod: 'popularMod' }, - { href: '/public/latest/extended', glyph: '∞', labelKey: 'multiverse', mod: 'multiverseMod' } ] }, - { id: 'creative', emoji: '✎', title: i18n.menuCreative, mods: [ - { href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, { href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, - { href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, { href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' } ] }, - { id: 'media', emoji: '▤', title: i18n.menuMedia, mods: [ - { href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, { href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, - { href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, { href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, - { href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, { href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' } ] }, - { id: 'fediverse', emoji: '⌬', title: i18n.fediverse, mods: [ - { href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' } ] }, - ]; - const catHex = (c) => label({ for: `hive-${c.id}`, class: 'hex-cat' }, - span({ class: 'hx-ico' }, c.emoji), span({ class: 'hx-lbl' }, c.title)); - const modHex = (m) => a({ class: 'hex-mod', href: m.href }, - span({ class: 'hx-ico' }, m.glyph), span({ class: 'hx-lbl' }, i18n[m.labelKey] || '')); - const radios = CATS.map(c => input({ type: 'radio', name: 'hive-cat', id: `hive-${c.id}`, class: 'hive-radio' })); - const catRows = []; - let idx = 0; - for (const size of [3, 4, 3]) { catRows.push(div({ class: 'hive-row' }, ...CATS.slice(idx, idx + size).map(catHex))); idx += size; } - const grid = div({ class: 'hive-grid hive-cats' }, ...catRows); - const panels = CATS.map(c => div({ class: 'hive-panel', id: `hive-panel-${c.id}` }, - div({ class: 'hive-mods' }, ...c.mods.filter(m => modOn(m.mod)).map(modHex)) - )); - return div({ class: 'hive-nav' }, ...radios, grid, ...panels); -}; +// === Oasis UX: topbar + bottombar + hive === +// La implementacion vive en ./hive_views.js para que las reescrituras de +// upstream sobre este fichero no arrastren la interfaz de la rama. +const forkNav = require('./hive_views')({ i18n }); +const renderMobileTopbar = forkNav.renderMobileTopbar; +const renderBottomBar = forkNav.renderBottomBar; +const renderHiveNav = forkNav.renderHiveNav; +exports.PINNABLE_MODULES = forkNav.PINNABLE_MODULES; +exports.LAUNCHER_GROUPS = forkNav.GROUPS; exports.renderHiveNav = renderHiveNav; +// se resuelve al llamarse, cuando template ya esta definido mas abajo +exports.bottomBarPickerView = () => forkNav.bottomBarPickerView(template); +const renderEmergencyBanner = (featured, opts = {}) => { + if (!featured || !featured.href) return null; + const compact = !!opts.compact; + const cap = compact ? 42 : 80; + const sev = String(featured.severity || "UNVERIFIED"); + const sevLabel = String(i18n[`emergencySeverity${sev.charAt(0) + sev.slice(1).toLowerCase()}`] || sev).toUpperCase(); + const t = String(featured.title || "").trim(); + const label = `${t.length > cap ? t.slice(0, cap) + "…" : t} (${sevLabel} · ${featured.confirmationCount || 0})`; + return div( + { class: (compact ? "ai-suggestion-banner ai-suggestion-inline" : "update-banner ai-suggestion-banner") + ` emergency-banner emergency-banner-${sev.toLowerCase()}` }, + span({ class: "update-banner-icon" }, "🚨"), + compact ? null : span({ class: "update-banner-text" }, i18n.emergencyBannerLabel), + a({ href: featured.href, class: "update-banner-link" }, label), + form({ method: "POST", action: "/emergencies/banner/dismiss", class: "welcome-banner-close" }, + button({ type: "submit", class: "welcome-banner-close-btn" }, "✕")) + ); +}; const template = (titlePrefix, ...elements) => { + const scope = listScope(); + if (scope && scope.dryRun) return ''; const currentConfig = getConfig(); const theme = currentConfig.themes.current || "Dark-SNH"; - const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : "blocks"; + const buildAiSuggestion = (compact) => { + try { + const { getConfig } = require('../configs/config-manager.js'); + if (getConfig().ai?.suggestions === false) return null; + } catch (_) {} + let here = ''; + try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {} + const own = sharedState.getSectionMatch ? sharedState.getSectionMatch(here.split('/')[1] || '') : null; + const suggestion = (own && own.href !== here) ? own : (sharedState.getBestMatch ? sharedState.getBestMatch() : null); + if (!suggestion || !suggestion.href) return null; + if (sharedState.isSuggestionDismissed && sharedState.isSuggestionDismissed(suggestion.href)) return null; + if (sharedState.areSuggestionsQuiet && sharedState.areSuggestionsQuiet()) return null; + const cap = compact ? 52 : 96; + const t = String(suggestion.title || '').trim(); + const label = t.length > cap ? t.slice(0, cap) + '…' : t; + return div( + { class: compact ? "ai-suggestion-banner ai-suggestion-inline" : "update-banner ai-suggestion-banner" }, + span({ class: "update-banner-icon" }, "🤖"), + a({ href: suggestion.href, class: "update-banner-link" }, label), + form( + { method: "POST", action: "/ai/suggestion/dismiss", class: "welcome-banner-close" }, + input({ type: "hidden", name: "href", value: suggestion.href }), + button({ type: "submit", class: "welcome-banner-close-btn" }, "✕") + ) + ); + }; + const bannerAct = (action, label, kls, fields = {}) => form({ method: "POST", action, class: "phone-action-form" }, + ...Object.entries(fields).map(([k, v]) => input({ type: "hidden", name: k, value: String(v) })), + button({ type: "submit", class: kls }, String(label).toUpperCase())); + const bannerCls = () => "update-banner phone-banner"; + const bannerChip = (icon, value) => span({ class: "pm-exposition-chip pm-exposition-whole phone-banner-chip" }, + span({ class: "pm-exposition-icon" }, icon), + span({ class: "pm-exposition-text" }, value)); + const buildRoomBanner = (here) => { + if (config.public || getConfig().modules.roomsMod !== 'on') return null; + const room = sharedState.getPhoneRoom(); + if (room && room.ref) { + const href = `/rooms/${encodeURIComponent(room.ref)}`; + if (here === href) return null; + return div({ class: bannerCls() }, + span({ class: "phone-banner-dot" }), + a({ href, class: "update-banner-link" }, room.title), + bannerChip("👥", `${room.count}/${room.max}`), + room.joinedAt ? bannerChip("◷", liveClock(room.joinedAt)) : null, + room.hand ? bannerChip("✋", String((Array.isArray(room.peers) ? room.peers : []).filter(p => p && p.hand && (Number(p.handSince) || 0) < (Number(room.handSince) || 0)).length + 1)) : null, + bannerAct("/rooms/mute", room.muted ? i18n.phoneUnmute : i18n.phoneMute, "tribe-action-btn", { mute: room.muted ? 0 : 1 }), + bannerAct("/rooms/hand", `✋ ${room.hand ? i18n.roomLowerHand : i18n.roomRaiseHand}`, room.hand ? "tribe-action-btn room-hand-btn-on" : "tribe-action-btn", { hand: room.hand ? 0 : 1 }), + bannerAct("/rooms/leave", i18n.roomLeave, "tribe-action-btn danger-btn") + ); + } + return null; + }; + const buildPhoneBanner = () => { + let here = ''; + try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {} + const st = phoneOn() ? sharedState.getPhoneCall() : null; + if (!st) return buildRoomBanner(here); + if (here === '/phone') return null; + const nameOf = (id) => nameCache.get(id) || String(id).slice(0, 10); + const name = st.group && Array.isArray(st.peers) ? st.peers.filter(p => p.phase !== 'gone').map(p => nameOf(p.id)).join(', ') : nameOf(st.peer); + const shell = (text, ...rest) => div({ class: bannerCls() }, + span({ class: "phone-banner-dot" }), + a({ href: "/phone", class: "update-banner-link" }, text), + ...rest + ); + if (st.dir === 'in' && st.phase === 'incoming') { + return shell(`${name} ${i18n.phoneIsCalling}`, + bannerAct("/phone/accept", i18n.phoneAnswer, "tribe-action-btn"), + bannerAct("/phone/reject", i18n.phoneReject, "tribe-action-btn danger-btn")); + } + if (st.phase === 'calling') { + return shell(`${st.reached ? i18n.phoneCalling : i18n.phoneConnecting} ${name}`, + bannerChip("◷", liveClock(st.startedAt)), + bannerAct("/phone/hangup", i18n.phoneHangup, "tribe-action-btn danger-btn")); + } + if (st.phase === 'recording' || st.phase === 'recorded') { + return shell(`${i18n.phoneRecording} ${name}`, + st.phase === 'recorded' ? null : bannerChip("●", liveClock(st.recordingStartedAt, { max: PHONE_PAM_MAX_MS })), + bannerAct("/phone/pam/send", i18n.phoneSendPam, "tribe-action-btn"), + bannerAct("/phone/pam/cancel", i18n.phoneCancel, "tribe-action-btn danger-btn")); + } + return shell(name, + bannerChip("◷", liveClock(st.answeredAt || st.startedAt)), + bannerAct("/phone/mute", st.muted ? i18n.phoneUnmute : i18n.phoneMute, "tribe-action-btn", { mute: st.muted ? 0 : 1 }), + bannerAct("/phone/hangup", i18n.phoneHangup, "tribe-action-btn danger-btn")); + }; + const buildEmergencyBanner = (compact) => { + try { + if (getConfig().modules.emergenciesMod !== "on") return null; + const featured = sharedState.getFeaturedEmergency ? sharedState.getFeaturedEmergency() : null; + if (!featured || !featured.id) return null; + if (sharedState.getDismissedEmergency && sharedState.getDismissedEmergency() === featured.id) return null; + return renderEmergencyBanner(featured, { compact }); + } catch (_) { return null; } + }; + const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : currentConfig.ux?.current === "rooms" ? "rooms" : "blocks"; const themeLink = link({ rel: "stylesheet", - href: `/assets/themes/${theme}.css` + href: `/assets/themes/${theme}.css?v=${assetVersion()}` }); const nodes = html( - { lang: "en" }, + { lang: i18nBase[selectedLanguage] ? selectedLanguage : "en" }, head( title(titlePrefix, " | Oasis"), - link({ rel: "stylesheet", href: "/assets/styles/style.css" }), - themeLink, - link({ rel: "stylesheet", href: "/assets/styles/mobile.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), - link({ rel: "stylesheet", href: "/assets/themes/OasisMobileUX.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), + link({ rel: "stylesheet", href: `/assets/styles/style.css?v=${assetVersion()}` }), + // movil: el tema va DESPUES de mobile.css para ganar por cascada sin !important; + // escritorio: orden original de epsylon + ...(process.env.OASIS_MOBILE === '1' + ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), acampadaBase(theme), themeLink, acampadaLink(theme) ] + : [ themeLink, link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]), + process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_ux.css?v=${assetVersion()}` }) : null, link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }), @@ -1318,17 +2017,21 @@ const template = (titlePrefix, ...elements) => { name: "viewport", content: toAttributes({ width: "device-width", - "initial-scale": 1, - "viewport-fit": "cover" + "initial-scale": 1 }) }) ), body( - input({ type: "checkbox", id: "nav-left-toggle", class: "oasis-nav-side-toggle" }), - input({ type: "checkbox", id: "nav-right-toggle", class: "oasis-nav-side-toggle" }), + ...(uxMode === "blocks" ? [ + input({ type: "radio", name: "oasis-nav-side", id: "nav-none-toggle", class: "oasis-nav-side-toggle", checked: true }), + input({ type: "radio", name: "oasis-nav-side", id: "nav-left-toggle", class: "oasis-nav-side-toggle" }), + input({ type: "radio", name: "oasis-nav-side", id: "nav-right-toggle", class: "oasis-nav-side-toggle" }) + ] : []), div( { class: uxMode === "ainav" ? "header ainav-only" : "header" }, renderMobileTopbar(), + // hive (hexagonos) SOLO en la home; en el resto de paginas el top queda limpio + ((typeof global !== "undefined" && global.__OASIS_SHOW_HIVE__) ? renderHiveNav() : ""), div( { class: "top-bar-left" }, a( @@ -1339,52 +2042,75 @@ const template = (titlePrefix, ...elements) => { alt: "Oasis Logo" }) ), + (process.env.OASIS_MOBILE === '1' && uxMode === "blocks" && phoneOn()) ? (() => { + const dnd = !!(getConfig().phone || {}).dnd; + return form({ method: 'POST', action: '/phone/dnd', class: 'oasis-phone-state' }, + input({ type: 'hidden', name: 'dnd', value: dnd ? '0' : '1' }), + button({ type: 'submit', class: `oasis-phone-state-btn pm-exposition-chip pm-exposition-${dnd ? 'hidden' : 'mutuals'}`, title: String(dnd ? i18n.phoneDnd : i18n.phoneOpen), 'aria-label': String(dnd ? i18n.phoneDnd : i18n.phoneOpen) }, + span({ class: 'pm-exposition-icon' }, dnd ? '⊘' : '✆') + ) + ); + })() : null, uxMode === "ainav" ? nav( ul( (() => { - const inboxCount = sharedState.getInboxCount(); - const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : ''; return li( a({ href: "/inbox" }, - span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge + span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, ...inboxBadges() ) ); })(), + renderPhoneTopLink(), navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }), navLink({ href: "/invites", emoji: "ꔮ", text: i18n.invites }) ) ) : nav( ul( (() => { - const inboxCount = sharedState.getInboxCount(); - const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : ''; + const mentionsTotal = sharedState.getMentionsTotal ? sharedState.getMentionsTotal() : 0; + if (!(mentionsTotal > 0) && process.env.OASIS_MOBILE !== '1') return null; + const mentionsCount = sharedState.getMentionsCount(); + const badge = mentionsCount > 0 ? span({ class: 'inbox-badge' }, String(mentionsCount)) : ''; return li( - a({ href: "/inbox" }, - span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge + a({ href: "/mentions" }, + span({ class: "emoji" }, "✺"), nbsp, i18n.mentions, badge ) ); })(), - navLink({ - href: "/pm", - emoji: "ꕕ", - text: i18n.privateMessage - }), - navLink({ href: "/publish", emoji: "❂", text: i18n.publish }) + (() => { + return li( + a({ href: "/inbox" }, + span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, ...inboxBadges() + ) + ); + })(), + renderPhoneTopLink(), + (uxMode === "chats" || uxMode === "feed" || uxMode === "phone" || uxMode === "rooms") + ? navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }) + : null ) ) ), - div( + uxMode === "blocks" ? div( { class: "oasis-mobile-menus" }, - label({ for: "nav-left-toggle", class: "oasis-menu-btn" }, - span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuPersonal || "Personal")), - label({ for: "nav-right-toggle", class: "oasis-menu-btn" }, - span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuCommunity || "Community")) - ), + span({ class: "oasis-menu-slot oasis-menu-slot-left" }, + label({ for: "nav-left-toggle", class: "oasis-menu-btn oasis-menu-open" }, + span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuPersonal || "Personal")), + label({ for: "nav-none-toggle", class: "oasis-menu-btn oasis-menu-close" }, + span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuPersonal || "Personal"))), + span({ class: "oasis-menu-slot oasis-menu-slot-right" }, + label({ for: "nav-right-toggle", class: "oasis-menu-btn oasis-menu-open" }, + span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuNetwork || "Community")), + label({ for: "nav-none-toggle", class: "oasis-menu-btn oasis-menu-close" }, + span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuNetwork || "Community"))) + ) : null, (() => { const aiNavOn = getConfig().modules.aiNavMod === 'on'; if (!aiNavOn && uxMode !== 'ainav') return null; return div( { class: uxMode === 'ainav' ? "top-bar-center top-bar-center-ainav" : "top-bar-center" }, + div({ class: 'ai-ask-row' }, + nav({ class: 'ai-ask-nav' }, ul(...(renderAILink() || []))), form( { method: 'POST', action: '/ai/ask', class: 'ai-ask-form' }, input({ @@ -1397,7 +2123,10 @@ const template = (titlePrefix, ...elements) => { autofocus: uxMode === 'ainav' ? 'autofocus' : undefined }), button({ type: 'submit', class: 'ai-ask-btn' }, '➤') - ) + ), + buildAiSuggestion(true) + ), + buildEmergencyBanner(true) ); })(), uxMode === "ainav" ? div( @@ -1409,32 +2138,69 @@ const template = (titlePrefix, ...elements) => { navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }) ) ) + ) : (uxMode === "chats" || uxMode === "feed" || uxMode === "phone" || uxMode === "rooms") ? div( + { class: "top-bar-right" }, + nav( + ul( + navLink({ href: "/activity", emoji: "ꔙ", text: i18n.activityTitle }), + renderTrendingLink(), + navLink({ href: "/search", emoji: "⌕", text: i18n.searchTitle }) + ) + ) ) : div( { class: "top-bar-right" }, nav( ul( - navLink({ href: "/search", emoji: "ꔅ", text: i18n.searchTitle }), - renderGraphosLink(), - navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }) + ...((getConfig().modules.aiNavMod === 'on' || uxMode === 'ainav') ? [] : (renderAILink() || [])), + navLink({ href: "/data", emoji: "⚯", text: i18n.dataTitle }), + renderTagsLink(), + navLink({ href: "/search", emoji: "⌕", text: i18n.searchTitle }), + process.env.OASIS_MOBILE === '1' ? navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }) : null ) ) ) ), (() => { - const updateFlagPath = path.join(__dirname, '../server/.update_required'); - if (fs.existsSync(updateFlagPath)) { + const updateInfo = (() => { try { return require('../backend/updater').readUpdateInfo(); } catch (_) { return null; } })(); + if (updateInfo) { return div( { class: "update-banner" }, - span({ class: "update-banner-icon" }, "⟳"), - span({ class: "update-banner-text" }, i18n.updateBannerText), + span({ class: "update-banner-icon" }, "🛠️"), + span({ class: "update-banner-text" }, updateInfo.version ? `${String(i18n.updateBannerText).replace(/[.。।!]\s*$/, '')}: v${updateInfo.version}` : i18n.updateBannerText), a({ href: "/settings", class: "update-banner-link" }, i18n.updateBannerAction) ); } return null; })(), + (() => { + try { + const onboarding = require('../models/onboarding_model'); + if (!onboarding.bannerVisible(config && config.path)) return null; + let here = ''; + try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {} + if (here === '/welcome' || here.startsWith('/welcome/')) return null; + const mobileUi = require('./mobile_ui'); + if (mobileUi.isMobile() && !mobileUi.welcomeBannerHere(here)) return null; + return div( + { class: "update-banner welcome-banner" }, + span({ class: "update-banner-icon" }, "🌴"), + span({ class: "update-banner-text" }, mobileUi.isMobile() ? mobileUi.welcomeBannerText() : i18n.welcomeBannerText), + a({ href: "/welcome", class: "create-button welcome-banner-start" }, mobileUi.isMobile() ? mobileUi.welcomeBannerAction() : i18n.welcomeBannerAction), + form( + { method: "POST", action: "/welcome/dismiss", class: "welcome-banner-close" }, + button({ type: "submit", class: "welcome-banner-close-btn" }, "✕") + ) + ); + } catch (_) { + return null; + } + })(), + (getConfig().modules.aiNavMod === 'on' || uxMode === 'ainav') ? null : buildAiSuggestion(false), + buildPhoneBanner(), + buildEmergencyBanner(false), div( - { class: uxMode === "ainav" ? "main-content ainav-only" : "main-content" }, - uxMode === "ainav" ? null : div( + { class: uxMode === "ainav" ? "main-content ainav-only" : uxMode === "chats" ? "main-content chatsux-only" : uxMode === "feed" || uxMode === "phone" || uxMode === "rooms" ? "main-content chatsux-only" : "main-content" }, + uxMode !== "blocks" ? null : div( { class: "sidebar-left" }, nav( ul( @@ -1457,7 +2223,7 @@ const template = (titlePrefix, ...elements) => { renderAgendaLink(), renderFavoritesLink(), renderLogsLink(), - renderWalletLink(), + renderInvitesLink(), navLink({ href: "/modules", emoji: "ꗣ", @@ -1480,10 +2246,29 @@ const template = (titlePrefix, ...elements) => { emoji: "ꖘ", text: i18n.inhabitantsLabel }), - renderTribesLink(), - renderLarpLink(), + renderEmergenciesLink(), + renderSchoolLink(), renderParliamentLink(), - renderCourtsLink() + renderCourtsLink(), + renderLarpLink(), + renderTribesLink() + ), + navGroup( + { + id: "economy", + emoji: "¤", + title: i18n.menuEconomy + }, + renderWalletLink(), + renderBankingLink(), + renderMarketLink(), + renderShopsLink(), + renderIndustryLink(), + renderLogisticsLink(), + renderProjectsLink(), + renderJobsLink(), + renderHousingLink(), + renderTransfersLink() ), navGroup( { @@ -1492,23 +2277,13 @@ const template = (titlePrefix, ...elements) => { title: i18n.menuOffice }, renderVotationsLink(), + renderPollsLink(), + renderCampaignsLink(), renderEventsLink(), renderCalendarsLink(), renderTasksLink(), - renderReportsLink() - ), - navGroup( - { - id: "economy", - emoji: "¤", - title: i18n.menuEconomy - }, - renderBankingLink(), - renderMarketLink(), - renderProjectsLink(), - renderJobsLink(), - renderShopsLink(), - renderTransfersLink() + renderReportsLink(), + renderMailingLink() ), navGroup( { @@ -1516,26 +2291,26 @@ const template = (titlePrefix, ...elements) => { emoji: "⚒", title: i18n.menuTools }, - renderAILink(), - navLink({ + process.env.OASIS_MOBILE === '1' ? null : navLink({ href: "/blockexplorer", emoji: "ꖸ", text: i18n.blockchain }), + renderGraphosLink(), + navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }), + renderDevLink(), renderCipherLink(), - renderInvitesLink(), - renderLegacyLink(), navLink({ href: "/stats", emoji: "ꕷ", text: i18n.statistics - }) + }), + renderBackupLink() ) ) ) ), - main({ id: "content", class: "main-column" }, elements), - uxMode === "ainav" ? null : div( + uxMode !== "blocks" ? null : div( { class: "sidebar-right" }, nav( ul( @@ -1550,42 +2325,16 @@ const template = (titlePrefix, ...elements) => { emoji: "ꔙ", text: i18n.activityTitle }), - renderTagsLink(), renderTrendingLink(), renderOpinionsLink(), - renderPadsLink(), - renderForumLink(), - renderMapsLink(), - renderChatsLink() - ), - navGroup( - { - id: "blogs", - emoji: "✦", - title: i18n.menuBlogs - }, - navLink({ - href: "/mentions", - emoji: "✺", - text: i18n.mentions - }), - renderLatestLink(), - renderThreadsLink(), - renderTopicsLink(), - renderSummariesLink(), - renderPopularLink(), - renderMultiverseLink() - ), - navGroup( - { - id: "creative", - emoji: "✎", - title: i18n.menuCreative - }, renderFeedLink(), - renderGamesLink(), - renderPixeliaLink(), - renderMelodyLink() + renderBlogsLink(), + renderForumLink(), + renderPadsLink(), + renderWikiLink(), + renderMapsLink(), + renderRoomsLink(), + renderChatsLink() ), navGroup( { @@ -1597,8 +2346,20 @@ const template = (titlePrefix, ...elements) => { renderBookmarksLink(), renderDocsLink(), renderImagesLink(), + renderFilesLink(), renderTorrentsLink(), - renderVideosLink() + renderVideosLink(), + renderPodcastsLink() + ), + navGroup( + { + id: "creative", + emoji: "✎", + title: i18n.menuCreative + }, + renderGamesLink(), + renderPixeliaLink(), + renderMelodyLink() ), navGroup( { @@ -1610,16 +2371,15 @@ const template = (titlePrefix, ...elements) => { ) ) ) - ) + ), + main({ id: "content", class: "main-column" }, elements, renderListPager(), renderLogWindowNote(), renderMobileCounters()) ), renderBottomBar(), - script({ src: "/js/oasis-mobile-nav.js", defer: "defer" }), renderFooter() ) ); return doctypeString + nodes.outerHTML; }; -// menu END exports.template = template; @@ -1628,13 +2388,16 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => { const theme = currentConfig.themes.current || "Dark-SNH"; const placeholder = i18n.aiNavPlaceholder || 'Where do you want to go?'; const nodes = html( - { lang: "en" }, + { lang: i18nBase[selectedLanguage] ? selectedLanguage : "en" }, head( title(placeholder, " | Oasis"), - link({ rel: "stylesheet", href: "/assets/styles/style.css" }), - link({ rel: "stylesheet", href: `/assets/themes/${theme}.css` }), - link({ rel: "stylesheet", href: "/assets/styles/mobile.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), - link({ rel: "stylesheet", href: "/assets/themes/OasisMobileUX.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), + link({ rel: "stylesheet", href: `/assets/styles/style.css?v=${assetVersion()}` }), + // movil: el tema va DESPUES de mobile.css para ganar por cascada sin !important; + // escritorio: orden original de epsylon + ...(process.env.OASIS_MOBILE === '1' + ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), acampadaBase(theme), link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), acampadaLink(theme) ] + : [ link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]), + process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_ux.css?v=${assetVersion()}` }) : null, link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }), @@ -1647,14 +2410,13 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => { nav( ul( (() => { - const inboxCount = sharedState.getInboxCount(); - const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : ''; return li( a({ href: "/inbox" }, - span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge + span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, ...inboxBadges() ) ); })(), + renderPhoneTopLink(), navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }), navLink({ href: "/invites", emoji: "ꔮ", text: i18n.invites }) ) @@ -2034,7 +2796,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const u = safeStr(c.url); if (u) { nodes.push( - renderField((i18n.url || 'URL') + ':', a({ href: u, target: '_blank', rel: 'noopener noreferrer' }, u)) + renderField((i18n.url || 'URL') + ':', a({ href: safeExternalHref(u), target: '_blank', rel: 'noopener noreferrer' }, u)) ); } } else if (t === 'image') { @@ -2042,7 +2804,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { if (u && isMsgId(u)) { nodes.push( div({ class: 'card-field card-field-mt' }, - img({ src: `/blob/${encodeURIComponent(u)}`, class: 'feed-image img-content' }) + img({ loading: 'lazy', src: `/blob/${encodeURIComponent(u)}`, class: 'feed-image img-content' }) ) ); } @@ -2060,7 +2822,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { if (u && isMsgId(u)) { nodes.push( div({ class: 'card-field card-field-mt' }, - videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(u)}` }) + videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(u)}`, preload: 'metadata' }) ) ); } @@ -2129,7 +2891,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { details( summary(i18n.viewJson || 'View JSON'), pre({ - innerHTML: highlightJs.highlight( + [RAW_HTML]: highlightJs.highlight( JSON.stringify(msg, null, 2), { language: "json", ignoreIllegals: true } ).value, @@ -2144,11 +2906,20 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { (u) => `${u}` ); } - articleElement = article({ class: "content", innerHTML: sanitizeHtml(html) }); + articleElement = article({ class: "content", [RAW_HTML]: sanitizeHtml(html) }); } else { articleElement = article( { class: "content" }, - p({ class: "post-text" }, ...renderUrl(rawText)) + p({ class: "post-text" }, ...renderStyledText(rawText)) + ); + } + const postTags = String(content.type || '') === 'post' && Array.isArray(content.tags) ? content.tags.filter(Boolean) : []; + if (postTags.length) { + articleElement = div( + articleElement, + div({ class: 'card-tags card-tags-mt' }, + ...postTags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)) + ) ); } @@ -2177,10 +2948,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const fallbackVoted = !!msg.value?.meta?.voted; const fallbackVoteCount = msg.value?.meta?.votes?.length || 0; - const fallbackVoteNames = (msg.value?.meta?.votes || []) - .map((person) => person.name) - .filter(Boolean); - const spreadInfoObj = (spreadInfo && typeof spreadInfo === 'object') ? spreadInfo : null; const spreadCount = spreadInfoObj && typeof spreadInfoObj.count === 'number' ? spreadInfoObj.count @@ -2188,22 +2955,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const alreadySpread = spreadInfoObj && typeof spreadInfoObj.alreadySpread === 'boolean' ? spreadInfoObj.alreadySpread : fallbackVoted; - const spreadVoters = (spreadInfoObj && Array.isArray(spreadInfoObj.voters)) - ? spreadInfoObj.voters - .map(v => (v && typeof v === 'object') ? (v.name || v.key || '') : String(v || '')) - .filter(Boolean) - : fallbackVoteNames; - - const maxSpreadNameLength = 16; - const maxSpreadNames = 16; - const spreadByNames = spreadVoters - .slice(0, maxSpreadNames) - .map((n) => String(n).slice(0, maxSpreadNameLength)) - .join(", "); - const additionalSpreadsMessage = - spreadCount > maxSpreadNames ? `+${spreadCount - maxSpreadNames} more` : ``; - const spreadByMessage = - spreadCount > 0 ? `${spreadByNames} ${additionalSpreadsMessage}`.trim() : (i18n.spreadHint || 'Spread this to your supporters (replicates via your feed).'); const spreadButtonClass = alreadySpread ? 'liked' : null; const messageClasses = ["post"]; @@ -2260,7 +3011,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { ), span( { class: "created-at" }, - `${i18n.createdBy} `, userLink(authorIdForName, msg.value?.meta?.author?.name), ` | ${timeAbsolute} | ${i18n.sendTime} `, a({ href: url.link }, timeAgo) @@ -2278,7 +3028,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { { type: "submit", class: spreadButtonClass, - title: spreadByMessage, + title: i18n.spreadContent, }, `☉ ${spreadCount}` ) @@ -2309,6 +3059,7 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' ubi: visibilityPrefs.ubi === true, wallet: visibilityPrefs.wallet === true, clearnetShops: visibilityPrefs.clearnetShops === true, + clearnetSchool: visibilityPrefs.clearnetSchool === true, clearnetJobs: visibilityPrefs.clearnetJobs === true, clearnetEvents: visibilityPrefs.clearnetEvents === true, clearnetProjects: visibilityPrefs.clearnetProjects === true, @@ -2318,7 +3069,12 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' clearnetImages: visibilityPrefs.clearnetImages === true, clearnetDocuments: visibilityPrefs.clearnetDocuments === true, clearnetTorrents: visibilityPrefs.clearnetTorrents === true, + clearnetFiles: visibilityPrefs.clearnetFiles === true, clearnetBookmarks: visibilityPrefs.clearnetBookmarks === true, + clearnetPodcasts: visibilityPrefs.clearnetPodcasts === true, + clearnetMarket: visibilityPrefs.clearnetMarket === true, + clearnetFeed: visibilityPrefs.clearnetFeed === true, + clearnetWiki: visibilityPrefs.clearnetWiki === true, profileShops: visibilityPrefs.profileShops === true, profileJobs: visibilityPrefs.profileJobs === true, profileEvents: visibilityPrefs.profileEvents === true, @@ -2329,14 +3085,23 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' profileImages: visibilityPrefs.profileImages === true, profileDocuments: visibilityPrefs.profileDocuments === true, profileTorrents: visibilityPrefs.profileTorrents === true, + profileFiles: visibilityPrefs.profileFiles === true, profileBookmarks: visibilityPrefs.profileBookmarks === true, + profilePodcasts: visibilityPrefs.profilePodcasts === true, + profileSchool: visibilityPrefs.profileSchool === true, + profileMarket: visibilityPrefs.profileMarket === true, + profileFeed: visibilityPrefs.profileFeed === true, + profileWiki: visibilityPrefs.profileWiki === true, ecoTax: visibilityPrefs.ecoTax !== false, larpSign: visibilityPrefs.larpSign === true, fediverse: visibilityPrefs.fediverse === true, - gpg: visibilityPrefs.gpg !== false + gpg: visibilityPrefs.gpg === true }; const fediverseHandleValue = typeof visibilityPrefs.fediverseHandle === 'string' ? visibilityPrefs.fediverseHandle : ''; - prefs.clearnet = prefs.clearnetShops || prefs.clearnetJobs || prefs.clearnetEvents || prefs.clearnetProjects || prefs.clearnetPosts || prefs.clearnetAudios || prefs.clearnetVideos || prefs.clearnetImages || prefs.clearnetDocuments || prefs.clearnetTorrents || prefs.clearnetBookmarks; + prefs.clearnet = prefs.clearnetShops || prefs.clearnetSchool || prefs.clearnetJobs || prefs.clearnetEvents || prefs.clearnetProjects || prefs.clearnetPosts || prefs.clearnetAudios || prefs.clearnetVideos || prefs.clearnetImages || prefs.clearnetDocuments || prefs.clearnetTorrents || prefs.clearnetFiles || prefs.clearnetBookmarks || prefs.clearnetPodcasts || prefs.clearnetMarket || prefs.clearnetFeed || prefs.clearnetWiki; + const pillRows = (entries) => div({ class: "pref-pill-row" }, + ...entries.slice().sort((a, b) => String(a[1]).localeCompare(String(b[1]))).map(([key, labelText]) => togglePill(key, labelText)) + ); const togglePill = (key, labelText, forced = false) => label( { class: forced ? "pref-pill pref-pill-forced" : "pref-pill", for: forced ? undefined : `vis_${key}`, title: forced ? (i18n.profileDeviceLockedHint || 'On mobile devices this sensor is mandatory and cannot be disabled.') : undefined }, forced ? input({ type: "hidden", name: `vis_${key}`, value: "1" }) : null, @@ -2383,7 +3148,7 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' div({ class: "gpg-edit-row" }, input({ type: "file", name: "gpgKey", accept: ".asc,.gpg,.pgp,application/pgp-keys,text/plain" }), gpgFingerprint - ? button({ type: "submit", formaction: "/profile/gpg/remove", formenctype: "application/x-www-form-urlencoded", formnovalidate: true, class: "gpg-remove-btn" }, + ? button({ type: "submit", formaction: "/profile/gpg/remove", attrs: { formenctype: "application/x-www-form-urlencoded", formnovalidate: "formnovalidate" }, class: "gpg-remove-btn" }, (i18n.profileGpgRemove || 'Remove') + ' (' + String(gpgFingerprint).slice(-8).toUpperCase() + ')' ) : null @@ -2391,64 +3156,51 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' ), br(), br(), div({ class: "prefs-card" }, - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.profileContentSectionTitle || 'Avatar Content'), p({ class: "prefs-help" }, i18n.profileContentHelp || 'Choose which of your modules will be displayed on your profile.') ), - div({ class: "pref-pill-row" }, - togglePill('profileShops', i18n.profileClearnetShopsLabel || 'Shops'), - togglePill('profileJobs', i18n.profileClearnetJobsLabel || 'Jobs'), - togglePill('profileEvents', i18n.profileClearnetEventsLabel || 'Events'), - togglePill('profileProjects', i18n.profileClearnetProjectsLabel || 'Projects'), - togglePill('profilePosts', i18n.profileClearnetPostsLabel || 'Blogs'), - togglePill('profileAudios', i18n.profileClearnetAudiosLabel || 'Audios'), - togglePill('profileVideos', i18n.profileClearnetVideosLabel || 'Videos'), - togglePill('profileImages', i18n.profileClearnetImagesLabel || 'Images'), - togglePill('profileDocuments', i18n.profileClearnetDocumentsLabel || 'Documents'), - togglePill('profileTorrents', i18n.profileClearnetTorrentsLabel || 'Torrents'), - togglePill('profileBookmarks', i18n.profileClearnetBookmarksLabel || 'Bookmarks') - ) + pillRows([ + ['profileAudios', i18n.profileClearnetAudiosLabel || 'Audios'], + ['profilePosts', i18n.profileClearnetPostsLabel || 'Blogs'], + ['profileBookmarks', i18n.profileClearnetBookmarksLabel || 'Bookmarks'], + ['profileDocuments', i18n.profileClearnetDocumentsLabel || 'Documents'], + ['profileEvents', i18n.profileClearnetEventsLabel || 'Events'], + ['profileFeed', i18n.profileClearnetFeedLabel || 'Feed'], + ['profileImages', i18n.profileClearnetImagesLabel || 'Images'], + ['profileJobs', i18n.profileClearnetJobsLabel || 'Jobs'], + ['profileMarket', i18n.profileClearnetMarketLabel || 'Market'], + ['profilePodcasts', i18n.profileClearnetPodcastsLabel || 'Podcasts'], + ['profileProjects', i18n.profileClearnetProjectsLabel || 'Projects'], + ['profileSchool', i18n.profileClearnetSchoolLabel || 'School'], + ['profileShops', i18n.profileClearnetShopsLabel || 'Shops'], + ['profileTorrents', i18n.profileClearnetTorrentsLabel || 'Torrents'], + ['profileFiles', i18n.profileClearnetFilesLabel || 'Files'], + ['profileVideos', i18n.profileClearnetVideosLabel || 'Videos'], + ['profileWiki', i18n.profileClearnetWikiLabel || 'Wikis'] + ]) ), br(), div({ class: "prefs-card" }, - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.profileSensorsSectionTitle || 'Sensors'), p({ class: "prefs-help" }, i18n.profileSensorsHelp || 'Optional metrics shown on your profile.') ), - div({ class: "pref-pill-row" }, - togglePill('karma', i18n.profileVisibilityKarma || 'KARMA Scoring'), - togglePill('activity', i18n.profileVisibilityActivity || 'Activity Level'), - togglePill('fediverse', i18n.profileVisibilityFediverse || 'Fediverse'), - togglePill('device', i18n.profileVisibilityDevice || 'Device', process.env.OASIS_MOBILE === '1'), - togglePill('larpSign', i18n.profileVisibilityLarpSign || 'L.A.R.P. Sign'), - togglePill('gpg', i18n.profileVisibilityGpg || 'GPG Key'), - togglePill('wallet', i18n.profileVisibilityWallet || 'ECOIN Wallet'), - togglePill('ubi', i18n.profileVisibilityUbi || 'UBI'), - togglePill('ecoTax', i18n.profileVisibilityEcoTax || 'ECO Tax') - ), + pillRows([ + ['activity', i18n.profileVisibilityActivity || 'Activity Level'], + ['ecoTax', i18n.profileVisibilityEcoTax || 'ECO Tax'], + ['wallet', i18n.profileVisibilityWallet || 'ECOIN Wallet'], + ['gpg', i18n.profileVisibilityGpg || 'GPG Key'], + ['karma', i18n.profileVisibilityKarma || 'KARMA Scoring'], + ['larpSign', i18n.profileVisibilityLarpSign || 'L.A.R.P. Sign'], + ['fediverse', i18n.profileVisibilityFediverse || 'Multiverse'] + ]), + process.env.OASIS_MOBILE === '1' + ? div({ class: "pref-pill-row" }, togglePill('device', i18n.profileVisibilityDevice || 'Device', true)) + : null, input({ type: "hidden", name: "fediverseHandle", value: fediverseHandleValue }) ), br(), - div({ class: "prefs-card" }, - div({ class: "tags-header" }, - h2(i18n.clearnetSectionTitle || 'Clearnet'), - p({ class: "prefs-help" }, i18n.profileClearnetHelp || 'Modules that can be accessed from outside Oasis.') - ), - div({ class: "pref-pill-row" }, - togglePill('clearnetShops', i18n.profileClearnetShopsLabel || 'Shops'), - togglePill('clearnetJobs', i18n.profileClearnetJobsLabel || 'Jobs'), - togglePill('clearnetEvents', i18n.profileClearnetEventsLabel || 'Events'), - togglePill('clearnetProjects', i18n.profileClearnetProjectsLabel || 'Projects'), - togglePill('clearnetPosts', i18n.profileClearnetPostsLabel || 'Blogs'), - togglePill('clearnetAudios', i18n.profileClearnetAudiosLabel || 'Audios'), - togglePill('clearnetVideos', i18n.profileClearnetVideosLabel || 'Videos'), - togglePill('clearnetImages', i18n.profileClearnetImagesLabel || 'Images'), - togglePill('clearnetDocuments', i18n.profileClearnetDocumentsLabel || 'Documents'), - togglePill('clearnetTorrents', i18n.profileClearnetTorrentsLabel || 'Torrents'), - togglePill('clearnetBookmarks', i18n.profileClearnetBookmarksLabel || 'Bookmarks') - ) - ), - br(), button( { type: "submit", @@ -2460,41 +3212,41 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' ); }; -exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWarning, sentAt }) => { - const { escapeHtml: esc, renderClearnetPage } = require('./clearnet_view'); +exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWarning, sentAt, tags = [] }) => { + const { escapeHtml: esc, renderKindTag, renderClearnetPage, renderRichText, renderTagChips } = require('./clearnet_view'); const rawText = String(text || ''); - const renderedHtml = sanitizeHtml(markdown(rawText)) - .replace(/(["'])\/blob\//g, '$1/c/blob/'); + const renderedHtml = sanitizeHtml(renderRichText(rawText)); const plainPreview = rawText.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/<[^>]+>/g, '').slice(0, 200); const authorEsc = esc(authorName || (author || '').slice(1, 9)); const dateStr = sentAt ? esc(new Date(sentAt).toISOString().slice(0, 10)) : ''; const cw = esc(contentWarning || ''); const firstLine = rawText.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/<[^>]+>/g, '').split('\n').map(s => s.trim()).find(Boolean) || ''; - const titleText = cw || firstLine.slice(0, 100) || 'Post'; + const titleText = cw || firstLine.slice(0, 100) || i18n.cnKindBlog; const extraCss = ` .cn-blog-meta{color:var(--fg-dim);font-size:13px;margin-bottom:16px;display:flex;gap:14px;flex-wrap:wrap} .cn-blog-cw{background:#663d00;color:#ffd700;border:1px solid #ff7300;padding:8px 14px;border-radius:6px;margin-bottom:16px;font-weight:600} .cn-blog-body{color:var(--fg-soft);line-height:1.7;font-size:16px;margin:0;word-wrap:break-word} -.cn-blog-body p{margin:0 0 14px 0} .cn-blog-body img{max-width:100%;height:auto;border-radius:6px;border:1px solid var(--border);display:block;margin:10px 0} .cn-blog-body a{color:var(--fg);text-decoration:underline} .cn-blog-body a:hover{color:var(--accent)} -.cn-blog-body pre,.cn-blog-body code{background:var(--bg-sub);color:var(--fg);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-family:monospace;font-size:13px} -.cn-blog-body pre{padding:10px 14px;overflow-x:auto;white-space:pre-wrap;word-break:break-word} -.cn-blog-body blockquote{margin:10px 0;padding:6px 14px;border-left:3px solid var(--fg);color:var(--fg-soft);background:var(--bg-sub);border-radius:0 4px 4px 0} -.cn-blog-body h1,.cn-blog-body h2,.cn-blog-body h3{color:var(--fg);margin:18px 0 10px 0} -.cn-blog-body ul,.cn-blog-body ol{padding-left:24px;margin:8px 0} +.cn-blog-body .rt-code{background:var(--bg-sub);color:var(--fg);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-family:monospace;font-size:13px} +.cn-blog-body .rt-code-block{background:var(--bg-sub);color:var(--fg);border:1px solid var(--border);border-radius:4px;padding:10px 14px;font-family:monospace;font-size:13px} +.cn-blog-body .rt-quote{padding:6px 14px;border-left:3px solid var(--fg);color:var(--fg-soft);background:var(--bg-sub);border-radius:0 4px 4px 0} +.cn-blog-body .rt-header{color:var(--fg);margin:18px 0 10px 0} .cn-blog-body video,.cn-blog-body audio{max-width:100%;display:block;margin:10px 0} `; const body = `
+ ${renderKindTag('blog')} ${dateStr ? `📅 ${dateStr}` : ''}
${cw ? `
${cw}
` : ''} +
${renderedHtml}
+ ${renderTagChips(tags)} `; return renderClearnetPage({ - title: `${esc(titleText)} — Oasis`, + title: `${titleText} | Oasis`, ogTitle: titleText, ogDescription: plainPreview, extraCss, @@ -2503,48 +3255,216 @@ exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWar }); }; -exports.clearnetInhabitantView = async ({ feedId, name, description, image, prefs, items = {}, query = '', filterType = '' }) => { - const { blobUrl: cnBlob, escapeHtml: esc, renderClearnetPage } = require('./clearnet_view'); - const blobAvatarUrl = cnBlob(image); - const avatarSrc = blobAvatarUrl || '/assets/images/default-avatar.png'; - const qrSrc = feedId ? `/qr/${encodeURIComponent(feedId)}` : null; - const displayName = esc(name || 'Anonymous'); - const desc = esc(description || ''); +const cnModuleLabel = (m) => (m && i18n[m.labelKey]) || (m && m.label) || ''; +exports.cnModuleLabel = cnModuleLabel; +const CLEARNET_MODULES = [ + { key: 'audios', label: 'Audios', kind: 'audio', labelKey: 'audiosLabel', prefKey: 'clearnetAudios' }, + { key: 'posts', label: 'Blogs', kind: 'blog', labelKey: 'blogTitle', prefKey: 'clearnetPosts', modulePath: 'blog' }, + { key: 'bookmarks', label: 'Bookmarks', kind: 'bookmark', labelKey: 'bookmarksLabel', prefKey: 'clearnetBookmarks' }, + { key: 'calendars', label: 'Calendars', kind: 'calendar', labelKey: 'calendarsTitle', prefKey: 'clearnetCalendars' }, + { key: 'campaigns', label: 'Campaigns', kind: 'campaign', labelKey: 'campaignsTitle', prefKey: 'clearnetCampaigns' }, + { key: 'documents', label: 'Documents', kind: 'document', labelKey: 'docsLabel', prefKey: 'clearnetDocuments' }, + { key: 'emergencies', label: 'Emergencies', kind: 'emergency', labelKey: 'emergenciesTitle', prefKey: 'clearnetEmergencies' }, + { key: 'events', label: 'Events', kind: 'event', labelKey: 'eventsLabel', prefKey: 'clearnetEvents' }, + { key: 'feed', label: 'Feed', kind: 'feed', labelKey: 'feedTitle', prefKey: 'clearnetFeed' }, + { key: 'housing', label: 'Housing', kind: 'housing', labelKey: 'housingTitle', prefKey: 'clearnetHousing' }, + { key: 'images', label: 'Images', kind: 'image', labelKey: 'imagesLabel', prefKey: 'clearnetImages' }, + { key: 'jobs', label: 'Jobs', kind: 'job', labelKey: 'modulesJobsLabel', prefKey: 'clearnetJobs' }, + { key: 'maps', label: 'Maps', kind: 'map', labelKey: 'mapTitle', prefKey: 'clearnetMaps' }, + { key: 'market', label: 'Market', kind: 'market', labelKey: 'marketTitle', prefKey: 'clearnetMarket' }, + { key: 'podcasts', label: 'Podcasts', kind: 'podcast', labelKey: 'podcastsTitle', prefKey: 'clearnetPodcasts' }, + { key: 'projects', label: 'Projects', kind: 'project', labelKey: 'projectsTitle', prefKey: 'clearnetProjects' }, + { key: 'rooms', label: 'Rooms', kind: 'room', labelKey: 'roomsTitle', prefKey: 'clearnetRooms' }, + { key: 'school', label: 'School', kind: 'course', labelKey: 'schoolTitle', prefKey: 'clearnetSchool' }, + { key: 'shops', label: 'Shops', kind: 'shop', labelKey: 'shopsTitle', prefKey: 'clearnetShops' }, + { key: 'torrents', label: 'Torrents', kind: 'torrent', labelKey: 'torrentsLabel', prefKey: 'clearnetTorrents' }, + { key: 'files', label: 'Files', kind: 'file', labelKey: 'filesLabel', prefKey: 'clearnetFiles' }, + { key: 'videos', label: 'Videos', kind: 'video', labelKey: 'videosLabel', prefKey: 'clearnetVideos' }, + { key: 'wiki', label: 'Wikis', kind: 'wiki', labelKey: 'wikiTitle', prefKey: 'clearnetWiki' } +]; + +exports.CLEARNET_MODULES = CLEARNET_MODULES; + +const CLEARNET_HUB_CSS = ` +.cn-filter-row{display:flex;flex-wrap:wrap;gap:8px;margin:24px 0 12px 0} +.cn-filter-btn{display:inline-block;padding:6px 14px;background:var(--bg-elev);color:var(--fg-soft);border:1px solid var(--border);border-radius:14px;font-size:13px;text-decoration:none;transition:border-color .15s ease,color .15s ease,background .15s ease} +.cn-filter-btn:hover{border-color:var(--fg);color:var(--fg);text-decoration:none} +.cn-filter-btn.active{background:var(--bg-sub);border-color:var(--fg);color:var(--fg);font-weight:600} +.cn-hub-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-top:16px} +.cn-hub-kind{color:var(--fg-dim);font-size:10px;text-transform:uppercase;letter-spacing:2px;font-weight:600} +.cn-hub-card{display:flex;flex-direction:column;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;overflow:hidden;transition:border-color .15s ease;color:var(--fg);text-decoration:none} +.cn-hub-card:hover{border-color:var(--fg);text-decoration:none} +.cn-hub-card a{color:var(--fg);text-decoration:none} +.cn-hub-card a:hover{text-decoration:underline} +.cn-hub-player{width:100%;display:block;margin:6px 0;border-radius:6px;background:#000} +video.cn-hub-player{max-height:160px;object-fit:cover} +img.cn-hub-player{max-height:200px;object-fit:cover} +audio.cn-hub-player{background:transparent;height:36px} +.cn-hub-thumb{width:100%;height:140px;object-fit:cover;background:#000;border-bottom:1px solid var(--border)} +.cn-pop-row{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:14px;margin-top:16px} +.cn-pop-thumb{width:100%;height:90px;object-fit:cover;background:#000;border-bottom:1px solid var(--border)} +.cn-pop-card .cn-hub-body{padding:8px 10px;gap:4px} +.cn-pop-card .cn-hub-title{font-size:13px} +@media (max-width:700px){.cn-pop-row{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:42%;overflow-x:auto}} +.cn-hub-body{padding:12px 14px;display:flex;flex-direction:column;gap:6px;min-width:0} +.cn-hub-title{color:var(--fg);font-weight:600;font-size:15px;word-break:break-word} +.cn-hub-snippet{color:var(--fg-soft);font-size:13px;line-height:1.4;word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} +.cn-hub-snippet .cn-url{text-decoration:underline} +.cn-hub-card{position:relative} +.cn-hub-cover{position:absolute;inset:0;z-index:1} +.cn-hub-player,.cn-hub-card a:not(.cn-hub-cover){position:relative;z-index:2} +.cn-hub-meta{color:var(--fg-dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-top:auto} +.cn-hub-author{color:var(--fg-dim);font-size:11px;word-break:break-all} +.cn-empty-content{background:var(--bg-elev);border:1px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--fg-dim);font-size:14px} +`; + +const buildClearnetHub = ({ items = {}, filterBase, filterType = '', query = '', showAuthor = false, page = 1 }) => { + const { blobUrl: cnBlob, escapeHtml: esc, renderRichText, renderTagChips, kindLabel, paginateClearnet, renderClearnetPager } = require('./clearnet_view'); const renderHubItem = (modulePath, it) => { const blob = cnBlob(it.image); - const title = esc(it.title || 'Untitled'); - const snippet = esc((it.snippet || '').slice(0, 160)); + const href = clearnetItemHref(modulePath, it.title, it.id); + const title = esc(it.title || i18n.cnUntitled); + const raw = String(it.snippet || ''); + const snippet = renderRichText(raw.slice(0, 300)); const meta = esc(it.meta || ''); const kind = esc(it.kind || ''); - return ` + const mediaSrc = it.media && it.media.blobId ? cnBlob(it.media.blobId) : null; + const player = mediaSrc + ? (it.media.kind === 'video' + ? `` + : it.media.kind === 'image' + ? `` + : ``) + : ''; + const chipRow = [ + meta ? `📅 ${meta}` : '', + ...(Array.isArray(it.details) ? it.details : []).map(d => `${esc(String(d))}`), + it.price ? `${esc(String(it.price))} ECO` : '' + ].filter(Boolean).join(''); + const author = showAuthor && it.feedId + ? `${esc(it.authorName || String(it.feedId).slice(1, 9))}` + : ''; + return `
+ ${blob ? `` : ''}
${kind ? `
${kind}
` : ''} -
${title}
- ${snippet ? `
${snippet}${(it.snippet || '').length > 160 ? '…' : ''}
` : ''} - ${meta ? `
${meta}
` : ''} + ${it.title ? `
${title}
` : ''} + ${author} + ${player} + ${snippet ? `
${snippet}${raw.length > 300 ? '…' : ''}
` : ''} + ${chipRow ? `
${chipRow}
` : ''} + ${renderTagChips(it.tags)}
- `; +
`; }; - const moduleDef = [ - { key: 'shops', label: 'Shops', kind: 'Shop', prefKey: 'clearnetShops' }, - { key: 'jobs', label: 'Jobs', kind: 'Job', prefKey: 'clearnetJobs' }, - { key: 'events', label: 'Events', kind: 'Event', prefKey: 'clearnetEvents' }, - { key: 'projects', label: 'Projects', kind: 'Project', prefKey: 'clearnetProjects' }, - { key: 'posts', label: 'Blogs', kind: 'Blog', prefKey: 'clearnetPosts', modulePath: 'blog' }, - { key: 'audios', label: 'Audios', kind: 'Audio', prefKey: 'clearnetAudios' }, - { key: 'videos', label: 'Videos', kind: 'Video', prefKey: 'clearnetVideos' }, - { key: 'images', label: 'Images', kind: 'Image', prefKey: 'clearnetImages' }, - { key: 'documents', label: 'Documents', kind: 'Document', prefKey: 'clearnetDocuments' }, - { key: 'torrents', label: 'Torrents', kind: 'Torrent', prefKey: 'clearnetTorrents' }, - { key: 'bookmarks', label: 'Bookmarks', kind: 'Bookmark', prefKey: 'clearnetBookmarks' } - ]; + const q = String(query || '').trim().toLowerCase(); + const matches = (it) => !q || [it.title, it.snippet, it.meta, ...(Array.isArray(it.tags) ? it.tags.map(t => `#${t}`) : [])].some(v => String(v || '').toLowerCase().includes(q)); + const allItems = []; + for (const m of CLEARNET_MODULES) { + for (const it of (items[m.key] || [])) { + if (!matches(it)) continue; + allItems.push({ ...it, modulePath: m.modulePath || m.key, kind: kindLabel(m.kind), _moduleKey: m.key }); + } + } + allItems.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0)); + const activeFilter = String(filterType || '').toLowerCase(); + const visibleItems = activeFilter ? allItems.filter(it => it._moduleKey === activeFilter) : allItems; + const qs = q ? `&q=${encodeURIComponent(query)}` : ''; + const filterButtons = `
+ ${esc(i18n.all)} (${allItems.length}) + ${CLEARNET_MODULES.filter(m => allItems.some(it => it._moduleKey === m.key)).map(m => { + const count = allItems.filter(it => it._moduleKey === m.key).length; + return `${esc(cnModuleLabel(m))} (${count})`; + }).join('')} +
`; + const paged = paginateClearnet(visibleItems, page); + const pager = renderClearnetPager({ base: filterBase, params: { type: activeFilter, q: query }, page: paged.page, pages: paged.pages, per: paged.per, total: paged.total }); + const sections = visibleItems.length + ? `${filterButtons}

${esc(i18n.cnNetworkContent)} (${visibleItems.length})

${paged.items.map(it => renderHubItem(it.modulePath, it)).join('')}
${pager}` + : (allItems.length ? `${filterButtons}
${esc(i18n.cnCategoryEmpty)}
` : ''); + return { sections, total: allItems.length, visible: visibleItems.length, page: paged.page, pages: paged.pages }; +}; + +exports.CLEARNET_HUB_CSS = CLEARNET_HUB_CSS; + +exports.clearnetHubView = async ({ authors = [], items = {}, tribes = [], filterType = '', query = '', page = 1, tribesPage = 1 }) => { + const { escapeHtml: esc, renderClearnetPage, renderClearnetHubSearch, blobUrl: cnBlob, CLEARNET_PAGER_CSS, renderClearnetPager } = require('./clearnet_view'); + const hub = buildClearnetHub({ items, filterBase: '/c', filterType, query, page }); + const extraCss = CLEARNET_HUB_CSS + CLEARNET_PAGER_CSS + ` +`; + const q = String(query || '').trim().toLowerCase(); + const shownTribes = (filterType || hub.page > 1) ? [] : (tribes || []).filter(t => !q || [t.title, t.description].some(v => String(v || '').toLowerCase().includes(q))); + const ranked = shownTribes.slice().sort((x, y) => (Number(y.content) || 0) - (Number(x.content) || 0)); + const tribePage = slicePage(ranked, tribesPage, 10); + const tribeCard = (t) => { + const img = cnBlob(t.image) || '/c/assets/images/default-tribe.png'; + const href = `/c/tribe/${encodeURIComponent(clearnetSlugFor(t.title, t.id))}`; + return `
${esc(t.title || i18n.cnUntitled)}
${esc(i18n.cnPublicContent)}: ${Number(t.content) || 0}
`; + }; + const tribesMore = renderClearnetPager({ base: '/c', params: { q: query }, page: tribePage.page, pages: tribePage.pages, total: tribePage.total, pageParam: 'tpage' }); + const tribesBlock = ranked.length + ? `

${esc(i18n.cnTribesContent)} (${ranked.length})

${tribePage.items.map(tribeCard).join('')}
${tribesMore}` + : ''; + const body = ` + ${tribesBlock} + ${hub.total ? hub.sections : (tribesBlock ? '' : `
${esc(i18n.cnHubEmpty)}
`)} +`; + return renderClearnetPage({ + title: `${i18n.cnHubTitle} | Oasis`, + ogTitle: `${i18n.cnHubTitle} | Oasis`, + ogDescription: i18n.cnHubDescription, + extraCss, + body, + headerExtra: renderClearnetHubSearch({ type: filterType, query }) + }); +}; + +exports.clearnetInhabitantView = async ({ feedId, name, description, image, prefs, items = {}, query = '', filterType = '', page = 1 }) => { + const { blobUrl: cnBlob, escapeHtml: esc, renderRichText, renderTagChips, renderClearnetPage, kindLabel, paginateClearnet, renderClearnetPager, CLEARNET_PAGER_CSS } = require('./clearnet_view'); + const blobAvatarUrl = cnBlob(image); + const avatarSrc = blobAvatarUrl || '/assets/images/default-avatar.png'; + const qrSrc = feedId ? `/c/qr/${encodeURIComponent(feedId)}` : null; + const displayName = esc(name && name !== 'Redacted' ? name : feedId); + const desc = renderRichText(description && description !== 'Redacted' ? description : ''); + const renderHubItem = (modulePath, it) => { + const blob = cnBlob(it.image); + const title = esc(it.title || i18n.cnUntitled); + const raw = String(it.snippet || ''); + const snippet = renderRichText(raw.slice(0, 300)); + const meta = esc(it.meta || ''); + const kind = esc(it.kind || ''); + const mediaSrc = it.media && it.media.blobId ? cnBlob(it.media.blobId) : null; + const preview = mediaSrc && it.media.kind === 'image' + ? `` + : ''; + const href = clearnetItemHref(modulePath, it.title, it.id); + const chipRow = [ + meta ? `📅 ${meta}` : '', + ...(Array.isArray(it.details) ? it.details : []).map(d => `${esc(String(d))}`), + it.price ? `${esc(String(it.price))} ECO` : '' + ].filter(Boolean).join(''); + return `
+ + ${blob ? `` : ''} +
+ ${kind ? `
${kind}
` : ''} + ${it.title ? `
${title}
` : ''} + ${preview} + ${snippet ? `
${snippet}${raw.length > 300 ? '…' : ''}
` : ''} + ${chipRow ? `
${chipRow}
` : ''} + ${renderTagChips(it.tags)} +
+
`; + }; + const moduleDef = CLEARNET_MODULES; const allItems = []; for (const m of moduleDef) { for (const it of (items[m.key] || [])) { - allItems.push({ ...it, modulePath: m.modulePath || m.key, kind: m.kind, _moduleKey: m.key }); + allItems.push({ ...it, modulePath: m.modulePath || m.key, kind: kindLabel(m.kind), _moduleKey: m.key }); } } + allItems.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0)); const activeFilter = (filterType || '').toLowerCase(); const visibleItems = activeFilter ? allItems.filter(it => it._moduleKey === activeFilter) @@ -2552,18 +3472,20 @@ exports.clearnetInhabitantView = async ({ feedId, name, description, image, pref const totalCount = visibleItems.length; const filterBase = `/c/inhabitant/${encodeURIComponent(feedId)}`; const filterButtons = `
- All (${allItems.length}) - ${moduleDef.filter(m => prefs && prefs[m.prefKey] && (items[m.key] || []).length).map(m => { + ${esc(i18n.all)} (${allItems.length}) + ${moduleDef.filter(m => (items[m.key] || []).length).map(m => { const isActive = activeFilter === m.key; const count = (items[m.key] || []).length; - return `${esc(m.label)} (${count})`; + return `${esc(cnModuleLabel(m))} (${count})`; }).join('')}
`; + const paged = paginateClearnet(visibleItems, page); + const pager = renderClearnetPager({ base: filterBase, params: { type: activeFilter }, page: paged.page, pages: paged.pages, per: paged.per, total: paged.total }); const sections = totalCount - ? `${filterButtons}

Public Content (${totalCount})

${visibleItems.map(it => renderHubItem(it.modulePath, it)).join('')}
` - : (allItems.length ? `${filterButtons}
No content in this category.
` : ''); - const noResults = '
This inhabitant has not published content to Clearnet yet.
'; - const extraCss = ` + ? `${filterButtons}

${esc(i18n.cnPublicContent)} (${totalCount})

${paged.items.map(it => renderHubItem(it.modulePath, it)).join('')}
${pager}` + : (allItems.length ? `${filterButtons}
${esc(i18n.cnCategoryEmpty)}
` : ''); + const noResults = `
${esc(i18n.cnInhabitantEmpty)}
`; + const extraCss = CLEARNET_PAGER_CSS + ` .cn-profile{display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start;margin-bottom:24px} .cn-avatar{width:160px;height:160px;border-radius:8px;border:3px solid var(--fg);object-fit:cover;background:#000;flex:0 0 auto} .cn-profile-body{flex:1 1 280px;min-width:0} @@ -2580,16 +3502,26 @@ exports.clearnetInhabitantView = async ({ feedId, name, description, image, pref .cn-hub-kind{color:var(--fg-dim);font-size:10px;text-transform:uppercase;letter-spacing:2px;font-weight:600} .cn-hub-card{display:flex;flex-direction:column;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;overflow:hidden;transition:border-color .15s ease;color:var(--fg);text-decoration:none} .cn-hub-card:hover{border-color:var(--fg);text-decoration:none} +.cn-hub-card a{color:var(--fg);text-decoration:none} +.cn-hub-card a:hover{text-decoration:underline} +.cn-hub-player{width:100%;display:block;margin:6px 0;border-radius:6px;background:#000} +video.cn-hub-player{max-height:160px;object-fit:cover} +img.cn-hub-player{max-height:200px;object-fit:cover} +audio.cn-hub-player{background:transparent;height:36px} .cn-hub-thumb{width:100%;height:140px;object-fit:cover;background:#000;border-bottom:1px solid var(--border)} .cn-hub-body{padding:12px 14px;display:flex;flex-direction:column;gap:6px;min-width:0} .cn-hub-title{color:var(--fg);font-weight:600;font-size:15px;word-break:break-word} .cn-hub-snippet{color:var(--fg-soft);font-size:13px;line-height:1.4;word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} +.cn-hub-snippet .cn-url{text-decoration:underline} +.cn-hub-card{position:relative} +.cn-hub-cover{position:absolute;inset:0;z-index:1} +.cn-hub-player,.cn-hub-card a:not(.cn-hub-cover){position:relative;z-index:2} .cn-hub-meta{color:var(--fg-dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-top:auto} .cn-empty-content{background:var(--bg-elev);border:1px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--fg-dim);font-size:14px} `; const body = `
- ${displayName} + ${displayName}

${displayName}

${esc(feedId)}
@@ -2600,7 +3532,7 @@ exports.clearnetInhabitantView = async ({ feedId, name, description, image, pref ${totalCount > 0 ? sections : noResults} `; return renderClearnetPage({ - title: `${name || 'Inhabitant'} — Oasis`, + title: `${name || 'Inhabitant'} | Oasis`, ogTitle: name || 'Oasis', ogDescription: description || '', ogImage: blobAvatarUrl, @@ -2639,7 +3571,17 @@ const renderUserSensors = (u, opts = {}) => { })() : null; const items = []; - if (show('karma')) items.push(span({ class: 'karma-line' }, `${i18n.bankingUserEngagementScore}: `, strong(String(u.karmaScore !== undefined ? u.karmaScore : 0)))); + const kr = u.karmaReceived && typeof u.karmaReceived === 'object' ? u.karmaReceived : null; + const krPos = kr ? Number(kr.positive) || 0 : 0; + const krNeg = kr ? Number(kr.negative) || 0 : 0; + const krTone = !kr ? '' : krPos > krNeg ? ' karma-value-positive' : krPos < krNeg ? ' karma-value-negative' : ' karma-value-neutral'; + if (show('karma')) items.push(span({ class: kr ? 'karma-line karma-line-stacked' : 'karma-line' }, + span({ class: 'karma-line-main' }, `${i18n.bankingUserEngagementScore}: `, strong({ class: 'karma-value' + krTone }, String(u.karmaScore !== undefined ? u.karmaScore : 0))), + kr ? span({ class: 'karma-received-row' }, + span({ class: 'karma-received karma-received-up' }, span({ class: 'karma-arrow' }, '▲'), String(krPos)), + span({ class: 'karma-received karma-received-down' }, span({ class: 'karma-arrow' }, '▼'), String(krNeg)) + ) : null + )); if (show('ecoTax')) items.push(span({ class: 'karma-line eco-tax-line' }, `${i18n.profileVisibilityEcoTax || 'ECO Tax'}: `, strong(fmtCarbon(u.carbonGrams)))); if (activityChip) items.push(activityChip); if (deviceChip) items.push(deviceChip); @@ -2655,16 +3597,32 @@ const renderUserSensors = (u, opts = {}) => { items.push(span({ class: 'gpg-line' }, `${i18n.profileGpgChip || 'GPG'}: `, gpgNode)); } if (show('wallet') && (u.ecoAddress || isMe)) { - const wt = u.ecoAddress || (i18n.statsEcoWalletNotConfigured || 'Not configured!'); - const wn = isMe ? a({ href: '/wallet' }, strong(wt)) : strong(wt); - items.push(span({ class: 'ubi-line' }, `${i18n.statsEcoWalletLabel || 'ECOin Wallet'}: `, wn)); + const addressNode = u.ecoAddress + ? span({ class: 'wallet-address', title: i18n.statsEcoWalletLabel }, u.ecoAddress) + : (isMe ? a({ href: '/wallet' }, strong(i18n.statsEcoWalletNotConfigured || 'Not configured!')) : strong(i18n.statsEcoWalletNotConfigured || 'Not configured!')); + items.push(div({ class: 'wallet-line' }, + span({ class: 'wallet-line-head', title: i18n.statsEcoWalletLabel }, span({ class: 'wallet-icon' }, '❄')), + isMe && u.ecoAddress ? a({ href: '/wallet', class: 'wallet-line-link' }, addressNode) : addressNode + )); } - if (show('ubi')) { - items.push(span({ class: 'ubi-line' }, `${i18n.bankUbiThisMonth || 'UBI'}: `, strong(`${Number(u.estimatedUBI || 0).toFixed(6)} ECO`))); - items.push(span({ class: 'ubi-line' }, `${i18n.bankUbiLastClaimed || 'Last claimed'}: `, u.lastClaimedDate ? a({ href: '/transfers?filter=ubi', class: 'user-link' }, new Date(u.lastClaimedDate).toLocaleDateString()) : strong(i18n.bankUbiNeverClaimed || 'Never claimed'))); - items.push(span({ class: 'ubi-line' }, `${i18n.bankUbiTotalClaimed || 'Total claimed'}: `, strong(`${Number(u.totalClaimed || 0).toFixed(6)} ECO`))); + const phoneVisible = isMe || prefs.phone === 'whole' || prefs.phone === undefined || (prefs.phone === 'mutuals' && u.mutual === true); + if (phoneOn() && prefs.phone !== 'off' && phoneVisible && u.id && !sharedState.isPubId(u.id)) { + const number = require('../models/phone_number').phoneNumberOf(u.id); + const dnd = isMe ? !!(getConfig().phone || {}).dnd : prefs.phoneDnd === true; + if (number) items.push(div({ class: 'wallet-line phone-number-line' }, + span({ class: 'wallet-line-head', title: i18n.profileVisibilityPhone }, span({ class: 'phone-number-icon' }, '✆')), + isMe || dnd + ? span({ class: 'wallet-address phone-number-value', title: isMe ? i18n.profileVisibilityPhone : i18n.phoneRecipientRefuses }, number) + : renderPhoneCallForm(u.id, span({ class: 'wallet-address phone-number-value' }, number), i18n.phoneCallButton), + renderStateChip(dnd ? 'hidden' : 'mutuals', null, String(dnd ? i18n.phoneDnd : i18n.phoneOpen).toUpperCase()), + isMe && opts.dndToggle !== false && process.env.OASIS_MOBILE !== '1' ? form({ method: 'POST', action: '/phone/dnd', class: 'phone-dnd-form' }, + input({ type: 'hidden', name: 'dnd', value: dnd ? '0' : '1' }), + button({ type: 'submit', class: 'tribe-action-btn' }, String(dnd ? i18n.phoneOpen : i18n.phoneDnd).toUpperCase()) + ) : null + )); } const sensorsBox = items.length ? div({ class: 'profile-sensors-box' }, ...items) : null; + if (opts.sensorsOnly) return sensorsBox ? [sensorsBox] : []; const larpNode = (show('larpSign') && u.larpHouse && u.larpHouse.key) ? a({ href: `/larp/${u.larpHouse.key}`, class: 'larp-sign-block', title: u.larpHouse.name }, img({ src: u.larpHouse.image || '/assets/larp/images/default.jpg', alt: u.larpHouse.name, class: 'larp-sign-large' })) : null; @@ -2673,13 +3631,12 @@ const renderUserSensors = (u, opts = {}) => { const path = `/c/inhabitant/${encodeURIComponent(u.id)}`; return div({ class: 'profile-reach' }, renderReachChip(true, i18n, path), - renderInviteQrCard({ qrDataUrl: `/qr-clearnet/${encodeURIComponent(u.id)}` }), - isMe ? form({ method: 'POST', action: '/profile/clearnet-toggle', class: 'profile-reach-toggle' }, button({ type: 'submit', class: 'btn' }, i18n.profileSwitchToOasis || 'Return to Oasis')) : null + renderInviteQrCard({ qrDataUrl: `/qr-clearnet/${encodeURIComponent(u.id)}` }) ); })() : null; const contentNode = opts.excludeContent ? null : renderContentStats(u.stats, i18n); - return [fediverseNode, opts.relationshipNode || null, sensorsBox, larpNode, reachNode, contentNode].filter(Boolean); + return [opts.relationshipNode || null, sensorsBox, fediverseNode, larpNode, reachNode, contentNode].filter(Boolean); }; exports.renderUserSensors = renderUserSensors; @@ -2695,6 +3652,7 @@ exports.authorView = async ({ relationship, ecoAddress, karmaScore = 0, + karmaReceived = null, estimatedUBI = 0, lastClaimedDate = null, totalClaimed = 0, @@ -2717,7 +3675,7 @@ exports.authorView = async ({ const rawPrefs = visibilityPrefs || {}; const prefs = { activity: rawPrefs.activity === true, - device: rawPrefs.device !== false, + device: rawPrefs.device === true, karma: rawPrefs.karma !== false, ubi: rawPrefs.ubi === true, wallet: rawPrefs.wallet === true, @@ -2726,9 +3684,11 @@ exports.authorView = async ({ clearnet: rawPrefs.clearnet === true, fediverse: rawPrefs.fediverse === true, fediverseHandle: typeof rawPrefs.fediverseHandle === 'string' ? rawPrefs.fediverseHandle : '', - gpg: rawPrefs.gpg !== false + gpg: rawPrefs.gpg === true, + phone: rawPrefs.phone === 'mutuals' || rawPrefs.phone === 'off' ? rawPrefs.phone : 'whole', + phoneDnd: rawPrefs.phoneDnd === true }; - const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetBookmarks']; + const clearnetSubKeys = CLEARNET_MODULES.map(m => m.prefKey); const anySubClearnet = clearnetSubKeys.some(k => rawPrefs[k] === true); prefs.clearnet = prefs.clearnet || anySubClearnet; const showField = (key) => prefs[key]; @@ -2738,57 +3698,14 @@ exports.authorView = async ({ const escHtml = s => String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); const markdownMention = `[@${escHtml(name)}](${escHtml(feedId)})`; - const contactForms = []; - const addForm = ({ action }) => - contactForms.push( - form( - { action: `/${action}/${encodeURIComponent(feedId)}`, method: "post" }, - button({ type: "submit", class: "btn" }, i18n[action]) - ) - ); - - if (relationship.me === false) { - if (relationship.following) addForm({ action: "unfollow" }); - else if (relationship.blocking) addForm({ action: "unblock" }); - else { addForm({ action: "follow" }); addForm({ action: "block" }) } - } - - const relationshipMessage = (() => { - if (relationship.me) return i18n.relationshipYou; - const following = relationship.following === true; - const followsMe = relationship.followsMe === true; - if (following && followsMe) return i18n.relationshipMutuals; - const messagesArr = []; - messagesArr.push(following ? i18n.relationshipFollowing : i18n.relationshipNone); - messagesArr.push(followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing); - return messagesArr.join(". ") + "."; - })(); - - const relationshipBlock = relationship.me - ? span({ class: "status you" }, i18n.relationshipYou) - : div({ class: "relationship-status" }, - relationship.blocking && relationship.blockedBy - ? span({ class: "status blocked" }, i18n.relationshipMutualBlock) - : [ - relationship.blocking ? span({ class: "status blocked" }, i18n.relationshipBlocking) : null, - relationship.blockedBy ? span({ class: "status blocked-by" }, i18n.relationshipBlockedBy) : null, - relationship.following && relationship.followsMe - ? span({ class: "status mutual" }, i18n.relationshipMutuals) - : [ - span({ class: "status supporting" }, relationship.following ? i18n.relationshipFollowing : i18n.relationshipNone), - span({ class: "status supported-by" }, relationship.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) - ] - ], - contactForms.length - ? div({ class: "relationship-actions" }, ...contactForms) - : null - ); + const relationshipBlock = renderRelationshipBlock(relationship, feedId); const userSensors = renderUserSensors({ isMe: isOwnProfile, fediverseConfigured, prefs, id: feedId, - karmaScore, carbonGrams, deviceSource, activityBucket: lastActivityBucket, + karmaScore, karmaReceived, carbonGrams, deviceSource, activityBucket: lastActivityBucket, gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed, - larpHouse, stats + larpHouse, stats, + mutual: !!(relationship && relationship.following && relationship.followsMe) }, { relationshipNode: div({ class: "profile-side-relationship" }, relationshipBlock) }); const sideColumn = div({ class: "tribe-side profile-side" }, @@ -2803,13 +3720,15 @@ exports.authorView = async ({ ) : null, feedId ? renderInviteQrCard({ qrDataUrl: `/qr/${encodeURIComponent(feedId)}` }) : null, description !== "" - ? div({ class: "profile-side-description", innerHTML: sanitizeHtml(markdown(description)) }) + ? div({ class: "profile-side-description", [RAW_HTML]: sanitizeHtml(renderStyledHtml(description)) }) : null, ...userSensors, div({ class: "profile-side-actions" }, - isOwnProfile ? a({ href: `/profile/edit`, class: "btn" }, i18n.editProfile) : null, - a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "btn" }, i18n.viewLikes), - !isOwnProfile ? a({ href: `/pm?recipients=${encodeURIComponent(feedId)}`, class: "btn" }, i18n.pmCreateButton) : null + isOwnProfile ? a({ href: `/profile/edit`, class: "filter-btn" }, `✎ ${i18n.editProfile}`) : null, + isOwnProfile ? a({ href: "/supporters", class: "filter-btn" }, `♥ ${i18n.viewSupporters}`) : null, + a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "filter-btn" }, `⟳ ${i18n.viewLikes}`), + !isOwnProfile ? a({ href: `/pm?recipients=${encodeURIComponent(feedId)}`, class: "filter-btn" }, `✉ ${i18n.pmCreateButton}`) : null, + !isOwnProfile ? renderCallButton(feedId, { pam: true }) : null ) ); @@ -2826,25 +3745,40 @@ exports.authorView = async ({ videos: new Set(['video']), images: new Set(['image']), documents: new Set(['document']), - torrents: new Set(['torrent']) + torrents: new Set(['torrent']), + files: new Set(['file']), + bookmarks: new Set(['bookmark']), + podcasts: new Set(['podcast']), + school: new Set(['schoolCourse']), + market: new Set(['market']), + feed: new Set(['feed']), + wiki: new Set(['wikiPage']) }; const moduleDef = [ - { key: 'shops', label: i18n.profileClearnetShopsLabel || 'Shops' }, - { key: 'jobs', label: i18n.profileClearnetJobsLabel || 'Jobs' }, - { key: 'events', label: i18n.profileClearnetEventsLabel || 'Events' }, - { key: 'projects', label: i18n.profileClearnetProjectsLabel || 'Projects' }, - { key: 'posts', label: i18n.profileClearnetPostsLabel || 'Blogs' }, { key: 'audios', label: i18n.profileClearnetAudiosLabel || 'Audios' }, - { key: 'videos', label: i18n.profileClearnetVideosLabel || 'Videos' }, - { key: 'images', label: i18n.profileClearnetImagesLabel || 'Images' }, + { key: 'posts', label: i18n.profileClearnetPostsLabel || 'Blogs' }, + { key: 'bookmarks', label: i18n.profileClearnetBookmarksLabel || 'Bookmarks' }, { key: 'documents', label: i18n.profileClearnetDocumentsLabel || 'Documents' }, - { key: 'torrents', label: i18n.profileClearnetTorrentsLabel || 'Torrents' } + { key: 'events', label: i18n.profileClearnetEventsLabel || 'Events' }, + { key: 'feed', label: i18n.profileClearnetFeedLabel || 'Feed' }, + { key: 'images', label: i18n.profileClearnetImagesLabel || 'Images' }, + { key: 'jobs', label: i18n.profileClearnetJobsLabel || 'Jobs' }, + { key: 'market', label: i18n.profileClearnetMarketLabel || 'Market' }, + { key: 'podcasts', label: i18n.profileClearnetPodcastsLabel || 'Podcasts' }, + { key: 'projects', label: i18n.profileClearnetProjectsLabel || 'Projects' }, + { key: 'school', label: i18n.profileClearnetSchoolLabel || 'School' }, + { key: 'shops', label: i18n.profileClearnetShopsLabel || 'Shops' }, + { key: 'torrents', label: i18n.profileClearnetTorrentsLabel || 'Torrents' }, + { key: 'files', label: i18n.profileClearnetFilesLabel || 'Files' }, + { key: 'videos', label: i18n.profileClearnetVideosLabel || 'Videos' }, + { key: 'wiki', label: i18n.profileClearnetWikiLabel || 'Wikis' } ]; const enabledKeys = moduleDef.filter(m => rawPrefs[`profile${m.key.charAt(0).toUpperCase() + m.key.slice(1)}`] === true).map(m => m.key); if (enabledKeys.length > 0) { const allowedTypes = new Set(); for (const k of enabledKeys) for (const t of keyToTypes[k]) allowedTypes.add(t); - const authorActions = allActions.filter(a => a && a.author === feedId && allowedTypes.has(a.type)); + const channelsWithEpisodes = new Set(allActions.filter(a => a && a.type === 'podcastEpisode' && a.content && a.content.channel).map(a => String(a.content.channel))); + const authorActions = allActions.filter(a => a && a.author === feedId && allowedTypes.has(a.type) && (a.type !== 'podcast' || channelsWithEpisodes.has(String(a.rootId || a.id)) || channelsWithEpisodes.has(String(a.id)))); const counts = {}; for (const k of enabledKeys) counts[k] = 0; for (const a of authorActions) { @@ -2869,11 +3803,10 @@ exports.authorView = async ({ ? authorActions.filter(a => keyToTypes[activeFilter].has(a.type)) : authorActions; visible.sort((a, b) => (b.ts || 0) - (a.ts || 0)); - const limited = visible.slice(0, 50); const { renderActionCards } = require('./activity_view'); mainColumnContent.push(filterRow); mainColumnContent.push(div({ class: 'feed-container profile-module-section' }, - renderActionCards(limited, feedId, allActions || limited, spreadMap instanceof Map ? spreadMap : new Map()) + renderActionCards(visible, (config.keys && config.keys.id) ? config.keys.id : '', allActions || visible, spreadMap instanceof Map ? spreadMap : new Map(), { paged: true }) )); } } @@ -2891,8 +3824,7 @@ exports.authorView = async ({ const hasDocument = Array.isArray(allActions) && allActions.some(a => a && a.author === feedId && a.type === 'document'); if (hasDocument) { html += ` - - + `; } return html; @@ -3023,7 +3955,7 @@ exports.commentView = async ( br(), label( { for: "blob" }, - i18n.blogImage || "Upload media (max-size: 50MB)" + i18n.blogImage || "Upload media (max-size: 75MB)" ), input({ type: "file", id: "blob", name: "blob" }), br(), @@ -3037,7 +3969,7 @@ exports.commentView = async ( const renderMessage = (msg) => { const content = lodash.get(msg, "value.content", {}); const authorId = msg.value.author || "Anonymous"; - const createdAt = new Date(msg.value.timestamp).toLocaleString(); + const createdAt = moment(msg.value.timestamp).format("YYYY/MM/DD HH:mm"); const mentionsText = content.text || ''; const isTribe = content.type === 'tribe-content'; const visitUrl = isTribe @@ -3064,116 +3996,85 @@ const renderMessage = (msg) => { div({ class: 'card-body' }, div({ class: 'card-section' }, badge, - p({ class: 'post-text' }, ...renderUrl(mentionsText || '[No content]')) + p({ class: 'post-text' }, ...renderStyledText(mentionsText || '[No content]')) ) ), p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${createdAt} ${i18n.performed || ''} `), + span({ class: 'date-link' }, `${createdAt}`), userLink(authorId) ) ); }; -const hasMention = (msg, feedId) => { - const content = lodash.get(msg, "value.content", {}); - const mentions = content.mentions; - if (mentions) { - if (Array.isArray(mentions)) { - if (mentions.some(m => m.link === feedId || m.feed === feedId)) return true; - } else if (typeof mentions === 'object') { - for (const arr of Object.values(mentions)) { - if (Array.isArray(arr) && arr.some(m => m.link === feedId || m.feed === feedId)) return true; - if (arr && (arr.link === feedId || arr.feed === feedId)) return true; - } - } - } - const text = content.text || ''; - if (text.includes(feedId) || text.includes(feedId.slice(1))) return true; - return false; -}; -exports.mentionsView = ({ messages, myFeedId }) => { - const title = i18n.mentions; - const description = i18n.mentionsDescription; - if (!Array.isArray(messages) || messages.length === 0) { - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - p({ class: "empty" }, i18n.noMentions) - ) - ) - ); - } - const filteredMessages = messages - .filter(msg => hasMention(msg, myFeedId)) - .sort((a, b) => (b.value.timestamp || 0) - (a.value.timestamp || 0)); - if (filteredMessages.length === 0) { - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - p({ class: "empty" }, i18n.noMentions) - ) - ) - ); - } - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - filteredMessages.map(renderMessage) - ) - ) - ); -}; -exports.privateView = async (messagesInput, filter, decrypted = null) => { +const { botOf: inboxBotOf, INBOX_BOTS } = require('../models/pm_model'); +const INBOX_BOT_ORDER = Object.keys(INBOX_BOTS).concat(['reminders']); +const inboxBotLabel = (bot) => ({ + blogs: i18n.pmBotBlogs, jobs: i18n.pmBotJobs, projects: i18n.pmBotProjects, market: i18n.pmBotMarket, shops: i18n.pmBotShops, + political: i18n.pmBotPolitical, banking: i18n.pmBotBanking, school: i18n.pmBotSchool, industry: i18n.pmBotIndustry, + housing: i18n.pmBotHousing, wiki: i18n.pmBotWiki, emergencies: i18n.pmBotEmergencies, podcasts: i18n.pmBotPodcasts, + campaigns: i18n.pmBotCampaigns, logistics: i18n.pmBotLogistics, reminders: i18n.privateReminders +})[bot] || bot; +exports.INBOX_BOT_ORDER = INBOX_BOT_ORDER; +exports.inboxBotLabel = inboxBotLabel; + +exports.privateView = async (messagesInput, filter, decrypted = null, notice = '', q = '') => { const messagesRaw = Array.isArray(messagesInput) ? messagesInput : messagesInput.messages - const messages = (messagesRaw || []).filter(m => m && m.key && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.private === true) + const listTitles = (!Array.isArray(messagesInput) && messagesInput.listTitles && typeof messagesInput.listTitles === 'object') ? messagesInput.listTitles : {} + const readKeys = new Set((!Array.isArray(messagesInput) && Array.isArray(messagesInput.readKeys)) ? messagesInput.readKeys.map(String) : []) + const archivedKeys = new Set((!Array.isArray(messagesInput) && Array.isArray(messagesInput.archivedKeys)) ? messagesInput.archivedKeys.map(String) : []) + const mutedBots = new Set((!Array.isArray(messagesInput) && Array.isArray(messagesInput.mutedBots)) ? messagesInput.mutedBots.map(String) : []) + const botFilter = (!Array.isArray(messagesInput) && typeof messagesInput.bot === 'string' && INBOX_BOT_ORDER.includes(messagesInput.bot)) ? messagesInput.bot : '' + const sortMode = (!Array.isArray(messagesInput) && messagesInput.sort === 'recent') ? 'recent' : 'thread' + const isMutedMsg = m => { const b = inboxBotOf(m?.value?.content); return !!(b && mutedBots.has(b)) } + const messages = (messagesRaw || []).filter(m => m && m.key && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.private === true).filter(m => !isMutedMsg(m)) + const qNorm = String(q || '').trim().toLowerCase() + const qMatch = (m) => { + if (!qNorm) return true + const c = m.value.content || {} + const ids = [c.from, m.value.author, ...(Array.isArray(c.to) ? c.to : [])].filter(Boolean) + return [c.subject, c.text, ...ids, ...ids.map(id => userLinkLabel(id))] + .some(v => String(v || '').toLowerCase().includes(qNorm)) + } const userId = await getUserId() const isSent = m => (m?.value?.author === userId) || (m?.value?.content?.from === userId) + const isList = m => !!(m?.value?.content?.list) const isToUser = m => Array.isArray(m?.value?.content?.to) && m.value.content.to.includes(userId) + const isReminder = m => { + const s = String(m?.value?.content?.subject || '') + return /^(Task Reminder:|Calendar Reminder:)/i.test(s) + } + const isNotification = m => !!inboxBotOf(m?.value?.content) + const isSelfNotice = m => isSent(m) && isToUser(m) && isNotification(m) + const isArchived = m => archivedKeys.has(String(m?.key)) + const toUserKeys = new Set(messages.filter(isToUser).map(m => String(m.key))) + const isUnreadKey = (key) => toUserKeys.has(String(key)) && !readKeys.has(String(key)) && !archivedKeys.has(String(key)) + const firstHref = (str) => { const m = String(str || '').match(/\]\((\/[^)\s]+)\)/); return m ? m[1] : null } const linkAuthor = (id) => userLink(id) const hrefFor = { job: (id) => `/jobs/${encodeURIComponent(id)}`, project: (id) => `/projects/${encodeURIComponent(id)}`, - market: (id) => `/market/${encodeURIComponent(id)}` + market: (id) => `/market/${encodeURIComponent(id)}`, + shopProduct: (id) => `/shops/product/${encodeURIComponent(id)}` } - const clickableCardProps = (href, extraClass = '') => { - const props = { class: `pm-card ${extraClass}` } - if (href) { - props.onclick = `window.location='${href}'` - props.tabindex = 0 - props.onkeypress = `if(event.key==='Enter') window.location='${href}'` - } - return props - } + const titleLink = (href, text) => href ? a({ href, class: 'pm-title-link' }, text) : text const chip = (txt) => span({ class: 'chip' }, txt) + const fmtBytes = (n) => { + const b = Number(n) || 0 + if (b < 1024) return `${b} B` + const units = ['KB', 'MB', 'GB', 'TB'] + let v = b / 1024, i = 0 + while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ } + return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)} ${units[i]}` + } + const e2eChip = () => span({ class: 'pm-exposition-chip pm-exposition-encrypted' }, span({ class: 'pm-exposition-icon' }, '🔒'), span({ class: 'pm-exposition-text' }, i18n.encryptedChipLabel || 'E2E') @@ -3200,7 +4101,10 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { ), tr( td({ class: 'card-label' }, i18n.pmSubjectLabel || 'Subject:'), - td({ class: 'card-value' }, subject || i18n.pmNoSubject || '(no subject)') + td({ class: 'card-value pm-subject-cell' }, + span(subject || i18n.pmNoSubject || '(no subject)'), + isUnreadKey(msgKey) ? span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.inboxUnreadChip)) : null + ) ), ecoChip ? tr( @@ -3219,28 +4123,25 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { try { return Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) { return 0; } } - function bubbleMeta({ sentAt, subjectRaw, crypter, msgSize, msgKey }) { - const ecoChip = msgSize ? renderEcoTax(msgSize, msgKey) : null - return div({ class: 'pm-bubble-meta' }, - subjectRaw ? span({ class: 'pm-bubble-subject' }, subjectRaw) : null, - span({ class: 'pm-bubble-time' }, moment(sentAt).format('DD/MM HH:mm')), - span({ class: 'pm-bubble-lock', title: crypter ? '2xE2E' : 'E2E' }, crypter ? '🔒🔒' : '🔒'), - ecoChip ? span({ class: 'pm-bubble-eco' }, ecoChip) : null - ) - } - - function actions({ key, replyId, subjectRaw, text }) { - const stop = { onclick: 'event.stopPropagation()' } + function actions({ key, replyId, subjectRaw, text, extra = null }) { const subjectReply = /^(\s*RE:\s*)/i.test(subjectRaw || '') ? (subjectRaw || '') : `RE: ${subjectRaw || ''}` const isSelf = replyId === userId return div({ class: 'pm-actions' }, - isSelf ? null : form({ method: 'GET', action: '/pm', class: 'pm-action-form', ...stop }, + isSelf ? null : form({ method: 'GET', action: '/pm', class: 'pm-action-form' }, input({ type: 'hidden', name: 'recipients', value: replyId }), input({ type: 'hidden', name: 'subject', value: subjectReply }), input({ type: 'hidden', name: 'quote', value: text || '' }), button({ type: 'submit', class: 'pm-btn reply-btn' }, i18n.pmReply.toUpperCase()) ), - form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'pm-action-form', ...stop }, + isSelf ? null : renderCallButton(replyId, { upper: true, cls: 'pm-btn' }), + extra || null, + toUserKeys.has(String(key)) && !archivedKeys.has(String(key)) ? form({ method: 'POST', action: `/inbox/${readKeys.has(String(key)) ? 'unread' : 'read'}/${encodeURIComponent(key)}`, class: 'pm-action-form' }, + button({ type: 'submit', class: 'pm-btn read-btn' }, String(readKeys.has(String(key)) ? i18n.inboxMarkUnread : i18n.inboxMarkRead).toUpperCase()) + ) : null, + toUserKeys.has(String(key)) ? form({ method: 'POST', action: `/inbox/${archivedKeys.has(String(key)) ? 'unarchive' : 'archive'}/${encodeURIComponent(key)}`, class: 'pm-action-form' }, + button({ type: 'submit', class: 'pm-btn archive-btn' }, String(archivedKeys.has(String(key)) ? i18n.inboxUnarchive : i18n.inboxArchive).toUpperCase()) + ) : null, + form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'pm-action-form' }, button({ type: 'submit', class: 'pm-btn delete-btn danger-btn' }, i18n.privateDelete.toUpperCase()) ) ) @@ -3257,7 +4158,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { } function threadId(m) { - return canonicalSubject(m?.value?.content?.subject || '') + '||' + participantsKey(m) + const c = m?.value?.content || {} + if (c.list) return canonicalSubject(c.subject || '') + '||list:' + String(c.thread || c.list) + return canonicalSubject(c.subject || '') + '||' + participantsKey(m) } function threadLevel(s) { @@ -3283,6 +4186,10 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const m = str.match(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/) return m ? m[1] : '' } + if (kind === 'shopProduct') { + const m = str.match(/\/shops\/product\/([%A-Za-z0-9/+._=-]+\.sha256)/) + return m ? m[1] : '' + } return '' } @@ -3305,15 +4212,27 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { } } flushQuote() - return parts.join('
') - .replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g, (match, id) => `${userLinkLabel(id)}`) + const mdLinks = [] + const masked = parts.join('
') + .replace(/\[([^\]\n]{1,120})\]\((https?:\/\/[^)\s]+|\/(?![\/\\])[^)\s]*)\)/g, (match, label, href) => { + const idx = mdLinks.length + const safeHref = String(href).replace(/"/g, '%22') + const ext = /^https?:/.test(href) ? ' target="_blank" rel="noopener noreferrer"' : '' + mdLinks.push(`${label}`) + return `\u0000MD${idx}\u0000` + }) + return masked + .replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g, (match, id) => `${escapeHtml(userLinkLabel(id))}`) .replace(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `${match}`) .replace(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `${match}`) .replace(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `${match}`) .replace(/\/calendars\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `${match}`) .replace(/\/ai\/ask\?[^\s<"]+/g, (match) => `${match}`) - .replace(/(? `${match}`) + .replace(/\/tribe\/([%A-Za-z0-9/+._=-]+\.sha256)(\?section=[a-zA-Z]+)?/g, (match) => `${match}`) + .replace(/\/larp\/([a-zA-Z]+)/g, (match) => `${match}`) + .replace(/(? `${match}`) .replace(/(https?:\/\/[^\s<"]+)/g, (match) => `${match}`) + .replace(/\u0000MD(\d+)\u0000/g, (m, i) => mdLinks[Number(i)] !== undefined ? mdLinks[Number(i)] : m) } const threads = {} @@ -3326,24 +4245,39 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const inboxSet = new Set() for (const arr of Object.values(threads)) { for (const m of arr) { - if (!isSent(m) && isToUser(m)) inboxSet.add(m) + if ((!isSent(m) || isSelfNotice(m)) && isToUser(m) && !isArchived(m)) inboxSet.add(m) } } - const isReminder = m => { - const s = String(m?.value?.content?.subject || '') - return /^(Task Reminder:|Calendar Reminder:)/i.test(s) - } + const received = Array.from(inboxSet) + const archivedMsgs = messages.filter(isArchived) + const notifications = messages.filter(m => isNotification(m) && !isArchived(m) && (!isSent(m) || isReminder(m) || isSelfNotice(m))) + const mailing = messages.filter(m => isList(m) && !isArchived(m)) + const botCounts = {} + for (const m of notifications) { const b = inboxBotOf(m.value.content); if (b) botCounts[b] = (botCounts[b] || 0) + 1 } + const botsPresent = INBOX_BOT_ORDER.filter(b => botCounts[b] > 0) - const data = - filter === 'sent' ? messages.filter(m => isSent(m) && !isReminder(m)) : - filter === 'reminders' ? messages.filter(isReminder) : - filter === 'inbox' ? Array.from(inboxSet).filter(m => !isReminder(m)) : - messages + const data = ( + filter === 'sent' ? messages.filter(m => isSent(m) && !isReminder(m) && !isSelfNotice(m) && !isArchived(m)) : + filter === 'reminders' ? messages.filter(m => isReminder(m) && !isArchived(m)) : + filter === 'pms' ? received.filter(m => !isNotification(m) && !isList(m)) : + filter === 'notifications' ? (botFilter ? notifications.filter(m => inboxBotOf(m.value.content) === botFilter) : notifications) : + filter === 'mailing' ? mailing : + filter === 'archived' ? archivedMsgs : + filter === 'inbox' ? received.filter(m => !isReminder(m)) : + messages.filter(m => !isArchived(m)) + ).filter(qMatch) - const inboxCount = Array.from(inboxSet).filter(m => !isReminder(m)).length - const sentCount = messages.filter(m => isSent(m) && !isReminder(m)).length - const reminderCount = messages.filter(isReminder).length + const inboxCount = received.filter(m => !isReminder(m)).length + const sentCount = messages.filter(m => isSent(m) && !isReminder(m) && !isSelfNotice(m) && !isArchived(m)).length + const pmCount = received.filter(m => !isNotification(m) && !isList(m)).length + const notifCount = notifications.length + const mailingCount = mailing.length + const archivedCount = archivedMsgs.length + const emptyInbox = messages.length === 0 && !qNorm + const shownKeys = data.map(m => String(m.key)) + const unreadShown = shownKeys.filter(k => isUnreadKey(k)) + const bulkDeletable = filter === 'notifications' || filter === 'reminders' || filter === 'archived' const sorted = [...data].sort((a, b) => { const ta = threadId(a) @@ -3363,9 +4297,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const jobId = pickLink(text, 'job') const href = jobId ? hrefFor.job(jobId) : null return div( - clickableCardProps(href, `job-notification thread-level-0`), - headerLine({ sentAt, from, toLinks, subject: type, msgKey: key, msgSize }), - h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotJobs} · ${titleH}`), + { class: 'pm-card job-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: titleH, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotJobs} · `, titleLink(href, titleH)), p( i18n.pmInhabitantWithId, ' ', linkAuthor(from), ' ', @@ -3387,9 +4321,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const projectId = pickLink(text, 'project') const href = projectId ? hrefFor.project(projectId) : null return div( - clickableCardProps(href, `project-${isFollow ? 'follow' : 'unfollow'}-notification thread-level-0`), - headerLine({ sentAt, from, toLinks, subject: type, msgKey: key, msgSize }), - h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotProjects} · ${titleH}`), + { class: `pm-card project-${isFollow ? 'follow' : 'unfollow'}-notification thread-level-0` }, + headerLine({ sentAt, from, toLinks, subject: titleH, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotProjects} · `, titleLink(href, titleH)), p( i18n.pmInhabitantWithId, ' ', userLink(from), @@ -3402,23 +4336,28 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { ) } - function MarketSoldCard({ sentAt, from, toLinks, subject, text, key, msgSize }) { + function MarketSoldCard({ sentAt, from, toLinks, subject, text, key, msgSize, variant = 'market' }) { + const isShop = variant === 'shop' const itemTitle = quoted(subject) || quoted(text) || 'item' const buyerId = (text.match(/OASIS ID:\s*([\w=/+.-]+)/) || [])[1] || from - const price = (text.match(/for:\s*\$([\d.]+)/) || [])[1] || '' + const price = (text.match(/for:\s*\$?([\d.]+)/) || [])[1] || '' const marketId = pickLink(text, 'market') - const href = marketId ? hrefFor.market(marketId) : null + const shopProductId = pickLink(text, 'shopProduct') + const href = isShop ? (shopProductId ? hrefFor.shopProduct(shopProductId) : null) : (marketId ? hrefFor.market(marketId) : null) + const icon = isShop ? '🛍️' : '💰' + const botLabel = isShop ? i18n.pmBotShops : i18n.pmBotMarket + const soldTitle = isShop ? (i18n.inboxShopSoldTitle || 'Product Sold') : i18n.inboxMarketItemSoldTitle return div( - clickableCardProps(href, 'market-sold-notification thread-level-0'), - headerLine({ sentAt, from, toLinks, subject, msgKey: key, msgSize }), - h2({ class: 'pm-title' }, `💰 ${i18n.pmBotMarket} · ${i18n.inboxMarketItemSoldTitle}`), + { class: `pm-card ${isShop ? 'shop-sold-notification' : 'market-sold-notification'} thread-level-0` }, + headerLine({ sentAt, from, toLinks, subject: soldTitle, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${icon} ${botLabel} · `, titleLink(href, soldTitle)), p( i18n.pmYourItem, ' ', - href ? a({ class: 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`, + href ? a({ class: isShop ? 'shop-link' : 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`, ' ', i18n.pmHasBeenSoldTo, ' ', linkAuthor(buyerId), - price ? ` ${i18n.pmFor} $${price}.` : '.' + price ? ` ${i18n.pmFor} ${price} ECO.` : '.' ), actions({ key, replyId: buyerId, subjectRaw: itemTitle, text }) ) @@ -3430,9 +4369,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const projectId = content.meta?.projectId ?? pickLink(text, 'project') const href = projectId ? hrefFor.project(projectId) : null return div( - clickableCardProps(href, 'project-pledge-notification thread-level-0'), - headerLine({ sentAt, from, toLinks, subject: 'PROJECT_PLEDGE', msgKey: key, msgSize }), - h2({ class: 'pm-title' }, `💚 ${i18n.pmBotProjects} · ${i18n.inboxProjectPledgedTitle}`), + { class: 'pm-card project-pledge-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: i18n.inboxProjectPledgedTitle, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `💚 ${i18n.pmBotProjects} · `, titleLink(href, i18n.inboxProjectPledgedTitle)), p( i18n.pmInhabitantWithId, ' ', linkAuthor(from), ' ', @@ -3445,64 +4384,323 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { ) } + function PoliticalBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + LARP_RULING: i18n.politicalBotLarpTitle || 'The ruling house of the L.A.R.P has changed.', + PARLIAMENT_GOV: i18n.politicalBotParliamentTitle || 'A new government cycle has begun in the Parliament.', + TRIBE_GOV: i18n.politicalBotTribeTitle || 'A new government cycle has begun in one of your Tribes.' + } + const title = titleMap[subjectU] || (i18n.pmBotPolitical || 'PoliticalBot') + const href = firstHref(text) + return div( + { class: 'pm-card political-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🏛️ ${i18n.pmBotPolitical} · `, href ? a({ href, class: 'pm-title-link' }, title) : title), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function IndustryBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + INDUSTRY_ADMITTED: i18n.industryBotAdmittedTitle || 'You have been admitted to a facility.', + INDUSTRY_APPLICATION: i18n.industryBotApplicationTitle || 'New membership application in your facility.', + INDUSTRY_INVITED: i18n.industryBotInvitedTitle || 'You have been invited to a facility.', + INDUSTRY_DISSOLVED: i18n.industryBotDissolvedTitle || 'A facility you belong to has been dissolved.', + INDUSTRY_BUILD_APPROVED: i18n.industryBotBuildApprovedTitle || 'A build has been approved.', + INDUSTRY_DISTRIBUTED: i18n.industryBotDistributedTitle || 'A build output has been distributed.' + } + const title = titleMap[subjectU] || (i18n.pmBotIndustry || 'IndustryBot') + return div( + { class: 'pm-card industry-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🏭 ${i18n.pmBotIndustry || 'IndustryBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function BankingBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const title = subjectU === 'BANKING_UBI_PAID' + ? (i18n.bankingBotUbiPaidTitle || 'You have received your UBI.') + : subjectU === 'BANKING_UBI_AVAILABLE' + ? (i18n.bankingBotUbiAvailableTitle || 'You can claim your UBI.') + : subjectU === 'BANKING_CONFIRM_PENDING' + ? (i18n.bankingBotConfirmTitle || 'You have transfers awaiting your confirmation.') + : (i18n.bankingBotPaymentTitle || 'You have received a payment.') + const href = firstHref(text) + return div( + { class: 'pm-card banking-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `💰 ${i18n.pmBotBanking || 'BankingBot'} · `, href ? a({ href, class: 'pm-title-link' }, title) : title), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function SchoolBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + SCHOOL_ENROLLED: i18n.schoolBotEnrolledTitle || 'A new student has enrolled in your course.', + SCHOOL_INVITED: i18n.schoolBotInvitedTitle || 'You have been invited to a course.', + SCHOOL_ADMITTED: i18n.schoolBotAdmittedTitle || 'You have been admitted to a course.', + SCHOOL_CERTIFICATE: i18n.schoolBotCertificateTitle || 'You have received a certificate.', + SCHOOL_PASSED: i18n.schoolBotPassedTitle || 'A student has passed your course.', + SCHOOL_LESSON_NEW: i18n.schoolBotLessonTitle || 'A new lesson has been published.' + } + const title = titleMap[subjectU] || (i18n.pmBotSchool || 'EducaBot') + return div( + { class: 'pm-card school-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🎓 ${i18n.pmBotSchool || 'EducaBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function EmergencyBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + EMERGENCY_UPDATED: i18n.emergencyBotUpdatedTitle || 'An emergency you follow has an update.', + EMERGENCY_RESOLVED: i18n.emergencyBotResolvedTitle || 'An emergency you follow has been resolved.' + } + const title = titleMap[subjectU] || (i18n.pmBotEmergencies || 'EmergencyBot') + return div( + { class: 'pm-card emergency-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🚨 ${i18n.pmBotEmergencies || 'EmergencyBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function MailingCard({ sentAt, from, toLinks, subject, text, key, msgSize, content }) { + const listId = String(content.list || '') + const info = listTitles[listId] || null + const listHref = `/mailing/${encodeURIComponent(listId)}` + const listName = info && info.title ? info.title : (i18n.mailingTitle || 'Mailing list') + const subjectReply = /^(\s*RE:\s*)/i.test(subject || '') ? (subject || '') : `RE: ${subject || ''}` + const canReply = !!info && info.status === 'ACTIVE' + const replyForm = canReply + ? form({ method: 'POST', action: `${listHref}/message`, class: 'pm-action-form mailing-inbox-reply' }, + input({ type: 'hidden', name: 'thread', value: String(content.thread || content.mid || '') }), + input({ type: 'hidden', name: 'subject', value: subjectReply }), + input({ type: 'hidden', name: 'returnTo', value: '/inbox?filter=mailing' }), + textarea({ name: 'text', rows: 2, maxlength: String(require('../backend/long_text').TEXT_CAP), required: true, placeholder: i18n.mailingReplyPlaceholder || 'Reply to the list...', class: 'mailing-inbox-reply-text' }), + button({ type: 'submit', class: 'pm-btn reply-btn' }, String(i18n.mailingReplyToList || 'Reply to list').toUpperCase()) + ) + : null + return div( + { class: 'pm-card mailing-pm' }, + headerLine({ sentAt, from, toLinks, subject, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, '✉ ', a({ href: listHref, class: 'mailing-inbox-list' }, listName)), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: userId, subjectRaw: subject, text, extra: replyForm }) + ) + } + + function CampaignBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + CAMPAIGN_UPDATED: i18n.campaignBotUpdatedTitle || 'A campaign you follow has an update.', + CAMPAIGN_ACHIEVED: i18n.campaignBotAchievedTitle || 'A campaign you follow has reached its goal.', + CAMPAIGN_RAISED: i18n.campaignBotRaisedTitle || 'A campaign you signed has been raised to Parliament.' + } + const title = titleMap[subjectU] || (i18n.pmBotCampaigns || 'CampaignBot') + return div( + { class: 'pm-card campaign-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `✍ ${i18n.pmBotCampaigns || 'CampaignBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function LogisticsBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + LOGISTICS_UPDATED: i18n.logisticsBotUpdatedTitle || 'A route you booked has changed.', + LOGISTICS_CLOSED: i18n.logisticsBotClosedTitle || 'A route you booked has been closed.', + LOGISTICS_BOOKED: i18n.logisticsBotBookedTitle || 'Someone booked your route.', + LOGISTICS_CANCELLED: i18n.logisticsBotCancelledTitle || 'A booking on your route was cancelled.', + LOGISTICS_CONFIRMED: i18n.logisticsBotConfirmedTitle || 'Your booking was confirmed: the package is on its way.', + LOGISTICS_REJECTED: i18n.logisticsBotRejectedTitle || 'Your booking was rejected.', + LOGISTICS_DELIVERED: i18n.logisticsBotDeliveredTitle || 'Your package has arrived.' + } + const title = titleMap[subjectU] || (i18n.pmBotLogistics || 'LogisticsBot') + return div( + { class: 'pm-card logistics-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${i18n.pmBotLogistics || 'LogisticsBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function BlogBotCard({ sentAt, from, toLinks, text, key, msgSize }) { + const title = i18n.blogBotNewTitle + return div( + { class: 'pm-card blog-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `📝 ${i18n.pmBotBlogs} · `, titleLink(firstHref(text), title)), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function PodcastBotCard({ sentAt, from, toLinks, text, key, msgSize }) { + const title = i18n.podcastBotEpisodeTitle || 'A podcast you follow has a new episode.' + return div( + { class: 'pm-card podcast-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${i18n.pmBotPodcasts || 'PodcastBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function WikiBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + WIKI_EDITED: i18n.wikiBotEditedTitle || 'A wiki page you follow has been edited.', + WIKI_RESTORED: i18n.wikiBotRestoredTitle || 'An earlier version of a wiki page you follow has been restored.' + } + const title = titleMap[subjectU] || (i18n.pmBotWiki || 'WikiBot') + return div( + { class: 'pm-card wiki-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `📖 ${i18n.pmBotWiki || 'WikiBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function JobsBotCard({ sentAt, from, toLinks, text, key, msgSize }) { + const title = i18n.jobsBotMatchTitle + return div( + { class: 'pm-card jobs-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `💼 ${i18n.pmBotJobs} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function HousingBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const myId = (config.keys && config.keys.id) ? config.keys.id : '' + const mine = String(from) === String(myId) + const titleMap = mine + ? { + HOUSING_REQUESTED: i18n.housingBotYouRequestedTitle || 'You have requested a place.', + HOUSING_CANCELLED: i18n.housingBotYouCancelledTitle || 'You have cancelled a request.', + HOUSING_UNAVAILABLE: i18n.housingBotUnavailableSentTitle || 'You have told the people who requested this place.' + } + : { + HOUSING_REQUESTED: i18n.housingBotRequestedTitle || 'Somebody has requested one of your places.', + HOUSING_CANCELLED: i18n.housingBotCancelledTitle || 'A request on one of your places was cancelled.', + HOUSING_UNAVAILABLE: i18n.housingBotUnavailableTitle || 'A place you requested is no longer available.' + } + const title = titleMap[subjectU] || (i18n.pmBotHousing || 'HousingBot') + return div( + { class: 'pm-card housing-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🏠 ${i18n.pmBotHousing || 'HousingBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + const { renderEncryptedChip } = require('./clearnet_view'); return template( i18n.private, section( - div({ class: 'tags-header' }, - div({ class: 'title-with-chip' }, h2(i18n.private), renderEncryptedChip(i18n)), - p(i18n.privateDescription) + div({ class: 'tags-header module-header-line' }, + h2(i18n.private), + p(i18n.privateDescription), + renderEncryptedChip(i18n) ), - (() => { - const pmVis = getConfig().pmVisibility === 'mutuals' ? 'mutuals' : 'whole' - const pmVisLabel = pmVis === 'mutuals' ? i18n.settingsPmVisibilityMutuals : i18n.settingsPmVisibilityWhole - const pmVisIcon = pmVis === 'mutuals' ? '🤝' : '🌐' - const nextVis = pmVis === 'mutuals' ? 'whole' : 'mutuals' - const nextLabel = nextVis === 'mutuals' - ? (i18n.inboxToggleToMutuals || 'Switch to mutuals') - : (i18n.inboxToggleToWhole || 'Switch to whole') - return div({ class: 'pm-exposition inbox-exposition' }, - span({ class: 'inbox-filters-label' }, i18n.inboxFiltersLabel || 'Filters:'), - span({ class: `pm-exposition-chip pm-exposition-${pmVis}` }, - span({ class: 'pm-exposition-icon' }, pmVisIcon), - span({ class: 'pm-exposition-text' }, pmVisLabel) - ), - form({ method: 'POST', action: '/settings/pm-visibility?returnTo=/inbox', class: 'inbox-vis-toggle' }, - input({ type: 'hidden', name: 'pmVisibility', value: nextVis }), - button({ type: 'submit', class: 'btn' }, nextLabel) - ) - ) - })(), div({ class: 'filters' }, form({ method: 'GET', action: '/inbox' }, [ + ...(emptyInbox ? [] : [ button({ type: 'submit', name: 'filter', value: 'inbox', class: filter === 'inbox' ? 'filter-btn active' : 'filter-btn' - }, `${i18n.privateInbox} (${inboxCount})`), - button({ + }, `${String(i18n.allButton || 'ALL').toUpperCase()} (${inboxCount})`), + (pmCount > 0 || filter === 'pms') ? button({ type: 'submit', name: 'filter', - value: 'reminders', - class: filter === 'reminders' ? 'filter-btn active' : 'filter-btn' - }, `${i18n.privateReminders || 'Reminders'} (${reminderCount})`), - button({ + value: 'pms', + class: filter === 'pms' ? 'filter-btn active' : 'filter-btn' + }, `${String(i18n.inboxFilterPms || 'PMs').toUpperCase()} (${pmCount})`) : null, + (mailingCount > 0 || filter === 'mailing') ? button({ + type: 'submit', + name: 'filter', + value: 'mailing', + class: filter === 'mailing' ? 'filter-btn active' : 'filter-btn' + }, `${String(i18n.inboxFilterMailing || 'Mailing lists').toUpperCase()} (${mailingCount})`) : null, + (sentCount > 0 || filter === 'sent') ? button({ type: 'submit', name: 'filter', value: 'sent', class: filter === 'sent' ? 'filter-btn active' : 'filter-btn' - }, `${i18n.privateSent} (${sentCount})`), + }, `${String(i18n.privateSent || 'SENT').toUpperCase()} (${sentCount})`) : null, + (notifCount > 0 || filter === 'notifications' || filter === 'reminders') ? button({ + type: 'submit', + name: 'filter', + value: 'notifications', + class: (filter === 'notifications' || filter === 'reminders') ? 'filter-btn active' : 'filter-btn' + }, `${String(i18n.inboxFilterNotifications || 'Notifications').toUpperCase()} (${notifCount})`) : null, + (archivedCount > 0 || filter === 'archived') ? button({ + type: 'submit', + name: 'filter', + value: 'archived', + class: filter === 'archived' ? 'filter-btn active' : 'filter-btn' + }, `${String(i18n.inboxFilterArchived).toUpperCase()} (${archivedCount})`) : null, + ]), button({ type: 'submit', name: 'filter', value: 'create', class: 'create-button', formaction: '/pm', - formmethod: 'GET' + attrs: { formmethod: 'GET' } }, i18n.pmCreateButton) ]) ), + (filter === 'notifications' && botsPresent.length) ? div({ class: 'filters inbox-bot-filters' }, + form({ method: 'GET', action: '/inbox' }, + input({ type: 'hidden', name: 'filter', value: 'notifications' }), + sortMode === 'recent' ? input({ type: 'hidden', name: 'sort', value: 'recent' }) : null, + button({ type: 'submit', name: 'bot', value: '', class: botFilter ? 'filter-btn' : 'filter-btn active' }, `${String(i18n.inboxAllBots).toUpperCase()} (${notifCount})`), + ...botsPresent.map(b => button({ type: 'submit', name: 'bot', value: b, class: botFilter === b ? 'filter-btn active' : 'filter-btn' }, `${String(inboxBotLabel(b)).toUpperCase()} (${botCounts[b]})`)) + ) + ) : null, + emptyInbox ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + (() => { + return div({ class: 'pm-exposition inbox-exposition' }, + span({ class: 'inbox-filters-label' }, i18n.inboxFiltersLabel || 'Filters:'), + form({ method: 'GET', action: '/inbox', class: 'inbox-vis-toggle' }, + input({ type: 'hidden', name: 'filter', value: filter || 'inbox' }), + botFilter ? input({ type: 'hidden', name: 'bot', value: botFilter }) : null, + qNorm ? input({ type: 'hidden', name: 'q', value: String(q || '') }) : null, + sortMode === 'recent' ? null : input({ type: 'hidden', name: 'sort', value: 'recent' }), + button({ type: 'submit', class: 'btn' }, sortMode === 'recent' ? i18n.inboxSortThread : i18n.inboxSortRecent) + ), + unreadShown.length ? form({ method: 'POST', action: '/inbox/read-all', class: 'inbox-vis-toggle' }, + ...unreadShown.map(k => input({ type: 'hidden', name: 'keys', value: k })), + button({ type: 'submit', class: 'btn' }, `${i18n.inboxMarkAllRead} (${unreadShown.length})`) + ) : null, + (bulkDeletable && shownKeys.length) ? form({ method: 'POST', action: '/inbox/delete-many', class: 'inbox-vis-toggle inbox-bulk-delete' }, + ...shownKeys.map(k => input({ type: 'hidden', name: 'keys', value: k })), + button({ type: 'submit', class: 'btn delete-btn' }, `${String(i18n.inboxDeleteShown).toUpperCase()} (${shownKeys.length})`) + ) : null + ) + })(), + form({ method: 'GET', action: '/inbox', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter || 'inbox' }), + botFilter ? input({ type: 'hidden', name: 'bot', value: botFilter }) : null, + sortMode === 'recent' ? input({ type: 'hidden', name: 'sort', value: 'recent' }) : null, + input({ type: 'text', name: 'q', value: String(q || ''), placeholder: i18n.inboxSearchPlaceholder || 'Search in Inbox...', class: 'filter-box__input' }), + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) + ), div({ class: 'message-list' }, (() => { function renderMsg(msg) { @@ -3512,12 +4710,43 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const subjectU = subjectRaw.toUpperCase() const text = content.text || '' const sentAt = new Date(content.sentAt || msg.timestamp) - const fromResolved = content.from || author + const fromResolved = author const toLinks = Array.isArray(content.to) ? content.to.map(addr => linkAuthor(addr)) : [] const level = threadLevel(subjectRaw) const msgSize = msgSizeBytes(msg) - const isMob = process.env.OASIS_MOBILE === '1' + if (content.fileShare && typeof content.fileShare === 'object') { + const fsp = content.fileShare + const dblKey = (decrypted && decrypted.key === msg.key) ? decrypted : null + return div( + { class: 'pm-card normal-pm pm-fileshare-card' }, + headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, msgKey: msg.key, msgSize, crypter: !!fsp.crypter }), + div({ class: 'pm-fileshare-info' }, + span({ class: 'pm-fileshare-icon' }, '📎'), + span({ class: 'pm-fileshare-name' }, String(fsp.filename || 'file')), + span({ class: 'pm-fileshare-meta' }, `${fmtBytes(fsp.size)} · ${String(fsp.mime || 'application/octet-stream')}`) + ), + fsp.crypter + ? form({ method: 'POST', action: `/inbox/file/${encodeURIComponent(msg.key)}`, class: 'pm-fileshare-download-form' }, + dblKey && dblKey.error ? div({ class: 'pm-form-error-msg' }, p('✗ ' + (i18n.pmCrypterBadKey || 'Your shared key is incorrect!'))) : null, + input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }), + button({ type: 'submit', class: 'pm-btn pm-fileshare-download' }, (i18n.fileShareDownload || 'Download').toUpperCase()) + ) + : null, + actions({ + key: msg.key, replyId: fromResolved, subjectRaw, text: '', + extra: fsp.crypter + ? null + : form({ method: 'GET', action: `/inbox/file/${encodeURIComponent(msg.key)}`, class: 'pm-action-form' }, + button({ type: 'submit', class: 'pm-btn pm-fileshare-download' }, (i18n.fileShareDownload || 'Download').toUpperCase()) + ) + }) + ) + } + + if (subjectU === 'JOB_MATCH') { + return JobsBotCard({ sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } if (subjectU === 'JOB_SUBSCRIBED' || subjectU === 'JOB_UNSUBSCRIBED') { return JobCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) } @@ -3525,68 +4754,86 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { return ProjectFollowCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) } if (subjectU === 'MARKET_SOLD') { - return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize }) + return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize, variant: 'market' }) + } + if (subjectU === 'SHOP_SOLD') { + return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize, variant: 'shop' }) + } + if (subjectU === 'LARP_RULING' || subjectU === 'PARLIAMENT_GOV' || subjectU === 'TRIBE_GOV') { + return PoliticalBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'SCHOOL_ENROLLED' || subjectU === 'SCHOOL_INVITED' || subjectU === 'SCHOOL_ADMITTED' || subjectU === 'SCHOOL_CERTIFICATE' || subjectU === 'SCHOOL_PASSED' || subjectU === 'SCHOOL_LESSON_NEW') { + return SchoolBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'BANKING_UBI_PAID' || subjectU === 'BANKING_UBI_AVAILABLE' || subjectU === 'BANKING_CONFIRM_PENDING' || subjectU === 'WALLET_PAYMENT') { + return BankingBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'INDUSTRY_ADMITTED' || subjectU === 'INDUSTRY_APPLICATION' || subjectU === 'INDUSTRY_INVITED' || subjectU === 'INDUSTRY_DISSOLVED' || subjectU === 'INDUSTRY_BUILD_APPROVED' || subjectU === 'INDUSTRY_DISTRIBUTED') { + return IndustryBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'HOUSING_REQUESTED' || subjectU === 'HOUSING_CANCELLED' || subjectU === 'HOUSING_UNAVAILABLE') { + return HousingBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'WIKI_EDITED' || subjectU === 'WIKI_RESTORED') { + return WikiBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'EMERGENCY_UPDATED' || subjectU === 'EMERGENCY_RESOLVED') { + return EmergencyBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'BLOG_NEW') { + return BlogBotCard({ sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'PODCAST_EPISODE') { + return PodcastBotCard({ sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'CAMPAIGN_UPDATED' || subjectU === 'CAMPAIGN_ACHIEVED' || subjectU === 'CAMPAIGN_RAISED') { + return CampaignBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU.startsWith('LOGISTICS_')) { + return LogisticsBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) } if (subjectU === 'PROJECT_PLEDGE' || content.meta?.type === 'project-pledge') { return ProjectPledgeCard({ sentAt, from: fromResolved, toLinks, content, text, key: msg.key, msgSize }) } + if (content.list) { + return MailingCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize, content }) + } if (content.crypter === true) { const dec = (decrypted && decrypted.key === msg.key) ? decrypted : null const shownText = dec && typeof dec.text === 'string' ? dec.text : '' - const cbody = dec && typeof dec.text === 'string' - ? div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(dec.text)) }) - : dec && dec.error - ? div({ class: 'pm-form-error-msg' }, p('✗ ' + i18n.pmCrypterBadKey)) - : null - const cform = dec && typeof dec.text === 'string' - ? null - : form({ method: 'POST', action: '/inbox/decrypt', class: 'pm-crypter-decrypt-form' }, - input({ type: 'hidden', name: 'id', value: msg.key }), - input({ type: 'hidden', name: 'returnFilter', value: filter || 'all' }), - input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }), - button({ type: 'submit', class: 'pm-btn pm-crypter-decrypt-btn' }, (i18n.pmCrypterDecryptButton || 'Decrypt').toUpperCase()) - ) - const cacts = actions({ key: msg.key, replyId: fromResolved, subjectRaw, text: shownText }) - if (isMob) { - const mine = isSent(msg) - return div( - { class: `pm-card normal-pm pm-crypter-card pm-bubble ${mine ? 'pm-sent' : 'pm-recv'} thread-level-${level}` }, - mine ? null : div({ class: 'pm-bubble-sender' }, linkAuthor(fromResolved)), - cbody, - cform, - bubbleMeta({ sentAt, subjectRaw, crypter: true, msgSize, msgKey: msg.key }), - cacts - ) - } return div( { class: 'pm-card normal-pm pm-crypter-card' }, headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, msgKey: msg.key, msgSize, crypter: true }), - cbody, - cform, - cacts - ) - } - - if (isMob) { - const mine = isSent(msg) - return div( - { class: `pm-card normal-pm pm-bubble ${mine ? 'pm-sent' : 'pm-recv'} thread-level-${level}` }, - mine ? null : div({ class: 'pm-bubble-sender' }, linkAuthor(fromResolved)), - div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text)) }), - bubbleMeta({ sentAt, subjectRaw, crypter: false, msgSize, msgKey: msg.key }), - actions({ key: msg.key, replyId: fromResolved, subjectRaw, text }) + dec && typeof dec.text === 'string' + ? div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(dec.text)) }) + : null, + dec && typeof dec.text === 'string' + ? null + : form({ method: 'POST', action: '/inbox/decrypt', class: 'pm-crypter-decrypt-form' }, + input({ type: 'hidden', name: 'id', value: msg.key }), + input({ type: 'hidden', name: 'returnFilter', value: filter || 'all' }), + input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }), + button({ type: 'submit', class: 'pm-btn pm-crypter-decrypt-btn' }, (i18n.pmCrypterDecryptButton || 'Decrypt').toUpperCase()) + ), + actions({ key: msg.key, replyId: fromResolved, subjectRaw, text: shownText }) ) } return div( { class: 'pm-card normal-pm' }, headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, msgKey: msg.key, msgSize }), - div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text)) }), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text)) }), actions({ key: msg.key, replyId: fromResolved, subjectRaw, text }) ) } + const msgTs = (m) => new Date(m?.value?.content?.sentAt || m.timestamp || 0).getTime() + if (sortMode === 'recent') { + if (!sorted.length) return p({ class: 'empty' }, i18n.noPrivateMessages) + return paged([...sorted].sort((a, b) => msgTs(b) - msgTs(a))).map(renderMsg) + } + const threadGroups = {} const threadOrder = [] for (const msg of sorted) { @@ -3598,28 +4845,34 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { threadGroups[tid].push(msg) } - if (!threadOrder.length) return emptyState({ icon: "✉", title: i18n.noPrivateMessages, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta }) + if (!threadOrder.length) return p({ class: 'empty' }, i18n.noPrivateMessages) - return threadOrder.map(tid => { + const threadTs = (tid) => (threadGroups[tid] || []).reduce((max, m) => { + const t = new Date(m?.value?.content?.sentAt || m.timestamp || 0).getTime() + return t > max ? t : max + }, 0) + threadOrder.sort((a, b) => threadTs(b) - threadTs(a)) + + return paged(threadOrder).map(tid => { const msgs = threadGroups[tid] - const original = msgs[0] - const replies = msgs.slice(1) + const latest = msgs[msgs.length - 1] + const earlier = msgs.slice(0, -1) - if (!replies.length) { - return renderMsg(original) + if (!earlier.length) { + return renderMsg(latest) } - const replyLabel = `${replies.length} ${replies.length === 1 ? (i18n.pmReply || 'reply') : (i18n.pmReplies || 'replies')}` + const earlierLabel = `${earlier.length} ${i18n.inboxEarlierMessages}` return div({ class: 'pm-thread' }, - renderMsg(original), + renderMsg(latest), details({ class: 'pm-thread-details' }, summary({ class: 'pm-thread-toggle' }, span({ class: 'pm-thread-icon' }, '▶'), - span(replyLabel) + span(earlierLabel) ), div({ class: 'pm-thread-replies' }, - ...replies.map(renderMsg) + ...earlier.map(renderMsg) ) ) ) @@ -3630,46 +4883,11 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { ) } -exports.publishCustomView = async () => { - const action = "/publish/custom"; - const method = "post"; - return template( - i18n.publishCustom, - section( - div({ class: "tags-header" }, - h2(i18n.publishCustom), - p(i18n.publishCustomDescription) - ), - form( - { action, method }, - textarea( - { - autofocus: true, - required: true, - name: "text", - rows: 10, - class: "textarea-full" - }, - "{\n", - ' "type": "feed",\n', - ' "hello": "world"\n', - "}" - ), - br(), - br(), - button({ type: "submit" }, i18n.submit) - ) - ), - section( - div({ class: "tags-header" }, - p(i18n.publishBasicInfo({ href: "/publish" })) - ) - ) - ); -}; - -exports.threadView = ({ messages, spreadMap = null }) => { +exports.threadView = ({ messages, spreadMap = null, hiddenRoot = false }) => { + if (!Array.isArray(messages) || !messages.length) { + return template(i18n.hiddenByYou, section(p({ class: 'muted hidden-note' }, i18n.hiddenByYou))); + } const rootMessage = messages[0]; const rootAuthorName = rootMessage.value.meta.author.name; @@ -3680,81 +4898,50 @@ exports.threadView = ({ messages, spreadMap = null }) => { const tpl = template( [`@${rootAuthorName}`], + hiddenRoot ? p({ class: 'muted hidden-note' }, i18n.hiddenByYou) : null, div(thread(messages, spreadMap)) ); return `${tpl}${ needsPdfViewer - ? ` - ` + ? `` : "" }`; }; -exports.publishView = (preview, text, contentWarning) => { +exports.supportersView = async ({ supporters, feed, name }) => { + const list = Array.isArray(supporters) ? supporters : []; return template( - i18n.publish, + i18n.viewSupporters, section( - div({ class: "tags-header" }, - h2(i18n.publishBlog), - p(i18n.publishLabel({ markdownUrl, linkTarget: "_blank" })) + div({ class: "tags-header module-header-line" }, + h2(`${i18n.viewSupporters} (${list.length})`), + p(userLink(feed, name)) ) ), - section( - div({ class: "publish-form" }, - form( - { - action: "/publish/preview", - method: "post", - enctype: "multipart/form-data", - }, - [ - label({ for: "contentWarning" }, i18n.blogSubject), - br(), - input({ - name: "contentWarning", - id: "contentWarning", - type: "text", - class: "contentWarning", - value: contentWarning || "", - placeholder: i18n.contentWarningPlaceholder - }), - br(), - label({ for: "text" }, i18n.blogMessage), - br(), - textarea( - { - required: true, - name: "text", - id: "text", - rows: "6", - cols: "50", - placeholder: i18n.publishWarningPlaceholder, - class: "publish-textarea", - maxlength: "8096" - }, - text || "" - ), - br(), - label({ for: "blob" }, i18n.blogImage || "Upload media (max-size: 50MB)"), - br(), - input({ type: "file", id: "blob", name: "blob" }), - br(), br(), - button({ type: "submit" }, i18n.blogPublish) - ] - ) - ) - ), - preview || "", - section( - div({ class: "tags-header" }, - p(i18n.publishCustomInfo({ href: "/publish/custom" })) - ) - ) + list.length + ? section(div({ class: "supporters-list" }, ...list.map(id => span({ class: "pad-member-chip" }, userLink(id))))) + : p({ class: "no-content" }, i18n.no_results) ); }; -//generate preview +exports.likesView = async ({ messages, feed, name, spreadMap = null }) => { + const list = Array.isArray(messages) ? messages : []; + return template( + i18n.viewLikes, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.viewLikes), + p(userLink(feed, name)) + ) + ), + list.length + ? div(thread(list, spreadMap)) + : p({ class: "no-content" }, i18n.no_results) + ); +}; + + const ensureAt = (id) => { const s = String(id || "").trim() if (!s) return "" @@ -3812,51 +4999,6 @@ const injectResolvedMentions = (text, mentions) => { return out } -const markdownMentionsToHtml = (markdownText) => { - const escaped = escapeHtml(String(markdownText || "")) - const withBr = escaped.replace(/\r\n|\r|\n/g, "
") - - const unescapeBlob = (b) => b.replace(/&/g, '&') - - const escAttr = (s) => String(s).replace(/"/g, '"').replace(/'/g, ''') - - const withImages = withBr.replace( - /!\[([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, - (_m, alt, blob) => `${escAttr(alt)}` - ) - - const withVideos = withImages.replace( - /\[video:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, - (_m, _name, blob) => `` - ) - - const withAudios = withVideos.replace( - /\[audio:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, - (_m, _name, blob) => `` - ) - - const withPdfs = withAudios.replace( - /\[pdf:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, - (_m, name, blob) => `${escapeHtml(name || i18n.pdfFallbackLabel || 'PDF')}` - ) - - const withMentions = withPdfs.replace( - /\[@([^\]]+)\]\(\s*@?([^) \t\r\n]+\.ed25519)\s*\)/g, - (_m, label, feed) => { - const href = authorHref(feed) - const shown = `@${String(label || "").replace(/^@+/, "")}` - return `${escapeHtml(shown)}` - } - ) - - const withLinks = withMentions.replace( - /(https?:\/\/[^\s"'<>]+)/g, - (u) => `${escAttr(u)}` - ) - - return withLinks -} - const generatePreview = ({ previewData, contentWarning, action }) => { const mentions = previewData && previewData.mentions && typeof previewData.mentions === "object" @@ -3868,7 +5010,7 @@ const generatePreview = ({ previewData, contentWarning, action }) => { const injected = injectResolvedMentions(normalized, mentions) const publishText = normalizeMentionLinks(injected) - const previewHtml = markdownMentionsToHtml(publishText) + const previewHtml = renderStyledHtml(publishText) const mentionCards = Object.entries(mentions) .map(([_token, matches]) => { @@ -3934,7 +5076,7 @@ const generatePreview = ({ previewData, contentWarning, action }) => { { class: "preview-content" }, h2(i18n.messagePreview), contentWarning ? div({ class: "content-warning-preview" }, escapeHtml(contentWarning)) : null, - div({ class: "preview-rendered", innerHTML: previewHtml }) + div({ class: "preview-rendered", [RAW_HTML]: previewHtml }) ) ), section( @@ -3954,159 +5096,6 @@ const generatePreview = ({ previewData, contentWarning, action }) => { ) } -exports.previewView = ({ previewData, contentWarning }) => { - const publishAction = "/publish" - const preview = generatePreview({ - previewData, - contentWarning, - action: publishAction, - }) - return exports.publishView(preview, (previewData && previewData.text) || "", contentWarning) -} - -const viewInfoBox = ({ viewTitle = null, viewDescription = null }) => { - if (!viewTitle && !viewDescription) { - return null - } - return section( - { class: "viewInfo" }, - viewTitle ? h1(viewTitle) : null, - viewDescription ? em(viewDescription) : null - ) -} -//generate preview - -exports.likesView = async ({ messages, feed, name, spreadMap = null }) => { - const authorLink = a( - { href: `/author/${encodeURIComponent(feed)}` }, - "@" + name - ); - const getSpread = (key) => (spreadMap instanceof Map ? spreadMap.get(key) : null) || null; - - return template( - ["@", name], - viewInfoBox({ - viewTitle: span(authorLink), - viewDescription: span(i18n.spreadedDescription) - }), - messages.map((msg) => post({ msg, spreadInfo: getSpread(msg.key) })) - ); -}; - -const messageListView = ({ - messages, - viewTitle = null, - viewDescription = null, - viewElements = null, - aside = null, - spreadMap = null, -}) => { - const hasHeader = !!viewElements; - const titleBlock = hasHeader - ? viewElements - : div({ class: "tags-header" }, - h2(viewTitle), - p(viewDescription) - ); - const getSpread = (key) => (spreadMap instanceof Map ? spreadMap.get(key) : null) || null; - return template( - viewTitle, - section(titleBlock), - messages.map((msg) => post({ msg, aside, spreadInfo: getSpread(msg.key) })) - ); -}; - -exports.popularView = ({ messages, prefix, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.popular), - p(i18n.popularDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.popular, - viewElements: [header, prefix], - spreadMap - }); -}; - -exports.extendedView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.extended), - p(i18n.extendedDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.extended, - viewElements: header, - spreadMap - }); -}; - -exports.latestView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.latest), - p(i18n.latestDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.latest, - viewElements: header, - spreadMap - }); -}; - -exports.topicsView = ({ messages, prefix, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.topics), - p(i18n.topicsDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.topics, - viewElements: [header, prefix], - spreadMap - }); -}; - -exports.summaryView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.summaries), - p(i18n.summariesDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.summaries, - viewElements: header, - aside: true, - spreadMap - }); -}; - -exports.spreadedView = ({ messages }) => { - const header = div({ class: "tags-header" }, - h2(i18n.spreaded), - p(i18n.spreadedDescription) - ); - return spreadedListView({ - messages, - viewTitle: i18n.spreaded, - viewElements: header - }); -}; - -exports.threadsView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.threads), - p(i18n.threadsDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.threads, - viewElements: header, - aside: true, - spreadMap - }); -}; exports.previewSubtopicView = async ({ previewData, @@ -4192,7 +5181,7 @@ exports.subtopicView = async ( br(), label( { for: "blob" }, - i18n.blogImage || "Upload media (max-size: 50MB)" + i18n.blogImage || "Upload media (max-size: 75MB)" ), input({ type: "file", id: "blob", name: "blob" }), br(), diff --git a/src/views/maps_view.js b/src/views/maps_view.js index 997555f..ff17622 100644 --- a/src/views/maps_view.js +++ b/src/views/maps_view.js @@ -1,16 +1,23 @@ -const { form, button, div, h2, h3, p, section, input, label, br, a, span, textarea, select, option, img, strong } = +const { form, button, div, h2, h3, p, section, input, label, br, hr, a, span, textarea, select, option, img, strong, table, tr, td } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { renderStyledText, renderStyledHtml, escapeHtml } = require("../backend/renderStyledText"); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderLifespanChip, renderSpreadButton, renderContentActions } = require("./main_views"); -const { renderEncryptedChip } = require("./clearnet_view"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderIcon } = require("./main_views"); +const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const { renderMapWithPins, renderZoomedMapWithPins, getViewportBounds, latLngToPx, pxToLatLng, MAP_W, MAP_H, getMaxTileZoom } = require("../maps/map_renderer"); -const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { renderMapHtml, resolveView, makeView, fitView, parseView, viewParam, parsePick, MIN_ZOOM, MAX_ZOOM } = require("../maps/map_renderer"); +const { searchPlaces, nearestPlace } = require("../maps/map_data"); const userId = config.keys.id; const safeArr = (v) => (Array.isArray(v) ? v : []); +const localHref = (u) => (typeof u === 'string' && /^\/(?![\/\\])\S*$/.test(u) ? u : null); const safeText = (v) => String(v || "").trim(); +const MAP_REACH = ["SINGLE", "OPEN", "CLOSED"]; +const mapReachLabel = (t) => t === "OPEN" ? i18n.mapTypeOpen : t === "CLOSED" ? i18n.mapTypeClosed : i18n.mapTypeSingle; +const clearnetEligible = (m) => !!m && !m.tribeId && !m.encrypted && !m.contentEncrypted && String(m.mapType || "").toUpperCase() === "OPEN"; +const mapClearnetHref = (m) => clearnetItemHref("maps", m.title, m.rootId || m.key); const buildReturnTo = (filter, params = {}) => { const f = safeText(filter || "all"); @@ -20,14 +27,6 @@ const buildReturnTo = (filter, params = {}) => { return `/maps?${parts.join("&")}`; }; -const renderPMButton = (recipient) => { - const r = safeText(recipient); - if (!r || String(r) === String(userId)) return null; - return form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: r }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)); -}; - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -35,126 +34,105 @@ const renderTags = (tags) => { : null; }; -const renderMapFavoriteToggle = (mapObj, returnTo = "") => - form({ - method: "POST", - action: mapObj.isFavorite ? `/maps/favorites/remove/${encodeURIComponent(mapObj.key)}` : `/maps/favorites/add/${encodeURIComponent(mapObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "filter-btn" }, mapObj.isFavorite ? i18n.mapRemoveFavoriteButton : i18n.mapAddFavoriteButton)); - let areaCounter = 0; +const lastOf = (v) => (Array.isArray(v) ? v[v.length - 1] : v); +const numOrNull = (v) => { const n = parseFloat(lastOf(v)); return isFinite(n) ? n : null; }; +const isSealed = (v) => !!v && String(v).startsWith("/blob/sealed/"); +const isBlob = (v) => !!v && (String(v).startsWith("&") || isSealed(v)); +const blobSrc = (image, base) => (isSealed(image) ? (base ? "" : String(image)) : (isBlob(image) ? `${base}/blob/${encodeURIComponent(image)}` : "")); -const buildAreas = (clickUrl, latParam = "lat", lngParam = "lng", viewport = null) => { - const GRID = 16; - const cellW = MAP_W / GRID; - const cellH = MAP_H / GRID; - const areas = []; - for (let gy = 0; gy < GRID; gy++) { - for (let gx = 0; gx < GRID; gx++) { - let c; - if (viewport) { - const lat = viewport.latMax - (gy + 0.5) / GRID * (viewport.latMax - viewport.latMin); - const lng = viewport.lngMin + (gx + 0.5) / GRID * (viewport.lngMax - viewport.lngMin); - c = { lat: Math.round(lat * 10000) / 10000, lng: Math.round(lng * 10000) / 10000 }; - } else { - const cx = Math.round(gx * cellW + cellW / 2); - const cy = Math.round(gy * cellH + cellH / 2); - c = pxToLatLng(cx, cy); - } - const x1 = Math.round(gx * cellW); - const y1 = Math.round(gy * cellH); - const x2 = Math.round((gx + 1) * cellW); - const y2 = Math.round((gy + 1) * cellH); - areas.push(`${c.lat},${c.lng}`); - } +const buildQuery = (params) => Object.entries(params || {}) + .filter(([, v]) => v !== undefined && v !== null && String(v) !== "") + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) + .join("&"); + +const viewHrefFor = (basePath, baseParams, anchor = "#map") => (target, extra = {}) => { + const q = buildQuery({ ...baseParams, ...extra, view: target ? viewParam(target) : undefined }); + return `${basePath}${q ? "?" + q : ""}${anchor}`; +}; + +const navText = () => ({ zoomIn: i18n.mapZoomIn, zoomOut: i18n.mapZoomOut, fit: i18n.mapFitMarkers, pan: i18n.mapPanLabel }); + +const viewForPlace = (hit) => { + if (!hit) return null; + if (hit.kind === "country" && Array.isArray(hit.bbox)) { + const b = hit.bbox; + const f = fitView([{ lat: b[1], lng: b[0] }, { lat: b[3], lng: b[2] }], { maxZoom: 7, pad: 40 }); + return makeView(f.lat, f.lng, f.zoom); } - return areas; + return makeView(hit.lat, hit.lng, MAX_ZOOM); +}; + +const placeLabel = (hit) => hit.country ? `${hit.name}, ${hit.country}` : hit.name; + +const nearText = (lat, lng) => { + const near = nearestPlace(lat, lng); + return near ? `${i18n.mapNear} ${near.name}, ${near.country}` : ""; +}; + +const pickedSpot = (lat, lng) => { + const la = numOrNull(lat), lo = numOrNull(lng); + if (la === null || lo === null) return p({ class: "map-pick-hint" }, i18n.mapPickHint); + const near = nearText(la, lo); + return div({ class: "map-picked" }, + span({ class: "map-coord-pin" }, "📍"), + strong(`${la.toFixed(4)}, ${lo.toFixed(4)}`), + near ? span({ class: "map-picked-near" }, near) : null); }; const renderMap = (markers, clickUrl, mainIdx, opts = {}) => { areaCounter++; - const mapName = `m${areaCounter}`; - const latParam = opts.latParam || "lat"; - const lngParam = opts.lngParam || "lng"; const pinLabels = opts.pinLabels || []; const pinImages = opts.pinImages || []; - const pfx = opts.pinPrefix || `pin${areaCounter}`; - const zoom = parseInt(opts.zoom) || 2; - const centerLat = typeof opts.centerLat === "number" ? opts.centerLat : 0; - const centerLng = typeof opts.centerLng === "number" ? opts.centerLng : 0; - - const pinList = safeArr(markers).filter((m) => m && typeof m.lat === "number" && typeof m.lng === "number"); - const useZoom = zoom > 2; - const mapFile = useZoom - ? renderZoomedMapWithPins(centerLat, centerLng, zoom, pinList, mainIdx) - : (pinList.length > 0 ? renderMapWithPins(pinList, mainIdx) : null); - const imgSrc = mapFile ? `/mapcache/${mapFile}` : "/assets/images/worldmap-z2.png"; - const viewport = useZoom && clickUrl ? getViewportBounds(centerLat, centerLng, zoom) : null; - - const useMap = clickUrl || pinLabels.length > 0; - const mapTag = useMap ? mapName : ""; - - let gridAreasHtml = ""; - if (clickUrl) { - const clickUrlWithZoom = zoom > 2 ? `${clickUrl}zoom=${zoom}&` : clickUrl; - gridAreasHtml = buildAreas(clickUrlWithZoom, latParam, lngParam, viewport).join(""); + const pinHrefs = opts.pinHrefs || []; + const base = opts.publicBase || ""; + const focus = Number.isInteger(opts.focus) ? opts.focus : -1; + const main = mainIdx || 0; + const pins = safeArr(markers) + .filter((m) => m && numOrNull(m.lat) !== null && numOrNull(m.lng) !== null) + .map((m, i) => ({ + lat: numOrNull(m.lat), + lng: numOrNull(m.lng), + label: pinLabels[i] !== undefined ? pinLabels[i] : (m.label || ""), + image: blobSrc(pinImages[i] !== undefined ? pinImages[i] : m.image, base), + href: pinHrefs[i] || m.href || null, + title: m.title || "", + main: i === main, + focus: i === focus + })); + if (opts.pick && numOrNull(opts.pick.lat) !== null && numOrNull(opts.pick.lng) !== null) { + pins.push({ lat: numOrNull(opts.pick.lat), lng: numOrNull(opts.pick.lng), pick: true }); } - let popupAreasHtml = ""; - let popupsHtml = ""; - if (pinLabels.length > 0) { - const vp = useZoom ? getViewportBounds(centerLat, centerLng, zoom) : null; - pinList.forEach((m, i) => { - const lbl = pinLabels[i] || ""; - let px; - if (vp) { - px = { - x: ((m.lng - vp.lngMin) / (vp.lngMax - vp.lngMin)) * MAP_W, - y: ((vp.latMax - m.lat) / (vp.latMax - vp.latMin)) * MAP_H - }; - } else { - px = latLngToPx(m.lat, m.lng); - } - const escaped = lbl.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); - const withLinks = escaped.replace(/https?:\/\/[^\s&"<>]+/g, (url) => { - const clean = url.replace(/&/g, "&"); - return `${url}`; - }).replace(/\n/g, "
"); - const sz = 20 * Math.pow(2, Math.max(0, zoom - getMaxTileZoom())); - const x1 = Math.max(0, px.x - sz); - const y1 = Math.max(0, px.y - sz); - const x2 = Math.min(MAP_W, px.x + sz); - const y2 = Math.min(MAP_H, px.y + sz); - const popupId = `${pfx}_${i}`; - const latStr = typeof m.lat === "number" ? m.lat.toFixed(4) : ""; - const lngStr = typeof m.lng === "number" ? m.lng.toFixed(4) : ""; - const imgBlobId = pinImages[i] && String(pinImages[i]).startsWith("&") ? pinImages[i] : ""; - const imgHtml = imgBlobId ? `` : ""; - popupAreasHtml += `${escaped}`; - popupsHtml += `
✕${imgHtml}
${sanitizeHtml(withLinks)}
${latStr}, ${lngStr}
`; - }); - } - const mapHtml = useMap ? `${popupAreasHtml}${gridAreasHtml}` : ""; - const useAttr = useMap ? ` usemap="#${mapTag}"` : ""; - - const mapWrapHtml = `
map${mapHtml}
`; - const viewerEl = div({ class: "map-viewer" }, { innerHTML: mapWrapHtml }); - if (!popupsHtml) return viewerEl; - return div({ class: "map-zone" }, viewerEl, div({ class: "map-popup-container", innerHTML: popupsHtml })); + const view = opts.view || resolveView({ zoom: opts.zoom, clat: opts.centerLat, clng: opts.centerLng }, pins.filter((x) => !x.pick), { singleZoom: opts.singleZoom || 8 }); + const html = renderMapHtml(view, pins, { + id: opts.id || `map${areaCounter}`, + thumb: !!opts.thumb, + publicBase: base, + nav: opts.nav || null, + pick: opts.pickForm ? { form: opts.pickForm, action: opts.pickAction, name: opts.pickName || "pick" } : null, + clickUrl: clickUrl || null, + latParam: opts.latParam, + lngParam: opts.lngParam, + anchor: typeof opts.anchor === "string" ? opts.anchor : "", + viewHref: opts.viewHref || null, + popup: opts.popup || null, + clusterTitle: i18n.mapClusterTitle, + text: navText() + }); + return div({ class: opts.thumb ? "map-viewer map-viewer-thumb" : "map-viewer", [RAW_HTML]: html }); }; -const renderCoordPreview = (lat, lng) => { - if (!lat && !lng) return null; - return span({ class: "map-coord-inline" }, - span({ class: "map-coord-pin" }, "📍"), - strong(`${lat}, ${lng}`)); +const renderPlaceResults = (hits, toEl) => { + if (!hits) return null; + if (!hits.length) return span({ class: "map-place-none" }, i18n.mapNoPlaces); + return div({ class: "map-place-results" }, hits.map((h) => toEl(h))); }; const renderLocalEmbed = (lat, lng) => { const la = parseFloat(lat) || 0; const lo = parseFloat(lng) || 0; if (!la && !lo) return null; - return renderMap([{ lat: la, lng: lo }], null, 0); + return renderMap([{ lat: la, lng: lo }], null, 0, { zoom: MIN_ZOOM, centerLat: la, centerLng: lo }); }; const renderMapUrl = (mapObj) => @@ -169,22 +147,20 @@ const renderMapOwnerActions = (filter, mapObj, params = {}) => { const actions = [ form({ method: "GET", action: `/maps/edit/${encodeURIComponent(mapObj.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.mapUpdateButton)), - form({ method: "POST", action: `/maps/delete/${encodeURIComponent(mapObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.mapDeleteButton)) + button({ class: "update-btn", type: "submit" }, i18n.mapUpdateButton)) ]; - const isPrivateMap = !mapObj.tribeId && mapObj.mapType !== "OPEN"; - if (isPrivateMap) { + const invitable = !mapObj.tribeId && mapObj.mapType === "CLOSED"; + if (invitable) { actions.push(form({ method: "POST", action: `/maps/generate-invite/${encodeURIComponent(mapObj.key)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite))); } const openInvite = Array.isArray(mapObj.invites) ? mapObj.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code) : null; - if (!mapObj.tribeId) { + if (invitable) { if (openInvite) { actions.push(div({ class: 'tribe-open-invite' }, span({ class: 'card-label' }, i18n.tribeInviteCodeText), - span({ class: 'tribe-open-invite-code' }, openInvite.code) + span({ class: 'tribe-open-invite-code' }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/maps/${encodeURIComponent(openInvite.code)}` }) )); actions.push(form({ method: "POST", action: `/maps/open-invite/remove/${encodeURIComponent(mapObj.key)}` }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation))); @@ -196,195 +172,270 @@ const renderMapOwnerActions = (filter, mapObj, params = {}) => { return actions; }; -const renderFilters = (filter, q) => - div({ class: "filters" }, +const renderFilters = (filter, q, emptyMod = false, chip = null, search = null) => + div({ class: search ? "filters activity-filter-chips activity-toolbar-row" : "filters" }, form({ method: "GET", action: "/maps", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q || "" }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterRecent), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterFavorites), - button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.mapUploadButton))); + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterAll).toUpperCase()), + ...(!chip || chip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterRecent).toUpperCase())] : []), + ...(!chip || chip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterMine).toUpperCase())] : []), + ...(!chip || chip("favorites") ? [button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterFavorites).toUpperCase())] : []), + ]), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.mapCreateButton)), + search); const renderMapForm = (filter, mapId, mapToEdit, params = {}) => { const returnFilter = filter === "create" ? "all" : params.filter || "all"; const returnTo = safeText(params.returnTo) || buildReturnTo(returnFilter, params); - const latVal = params.lat !== undefined ? String(params.lat) : String(mapToEdit?.lat || ""); - const lngVal = params.lng !== undefined ? String(params.lng) : String(mapToEdit?.lng || ""); + const picked = parsePick(params.pick); + const latVal = picked ? String(picked.lat) : (params.lat !== undefined ? String(lastOf(params.lat)) : String(mapToEdit?.lat || "")); + const lngVal = picked ? String(picked.lng) : (params.lng !== undefined ? String(lastOf(params.lng)) : String(mapToEdit?.lng || "")); const titleVal = params.title || mapToEdit?.title || ""; const descVal = params.description || mapToEdit?.description || ""; const markerLabelVal = params.markerLabel !== undefined ? params.markerLabel : (mapToEdit?.markerLabel || ""); const tagsValue = params.tags !== undefined ? params.tags : safeArr(mapToEdit?.tags).join(", "); - const mapTypeVal = params.mapType || mapToEdit?.mapType || "SINGLE"; - const maxTileZoom = getMaxTileZoom(); - const zoomVal = parseInt(params.zoom) || 2; - const cleanUrl = `/maps?filter=create${params.tribeId ? '&tribeId=' + encodeURIComponent(params.tribeId) : ''}`; + const isEdit = filter === "edit"; + const reachRaw = String(params.reach || params.mapType || "").toUpperCase(); + const mapTypeVal = MAP_REACH.includes(reachRaw) ? reachRaw : (mapToEdit?.mapType || "SINGLE"); + const showClearnet = mapTypeVal === "OPEN" && !params.tribeId && clearnetEligible({ ...(mapToEdit || {}), mapType: mapTypeVal }); + const clearnetOn = params.clearnet !== undefined ? String(params.clearnet) === "1" : mapToEdit?.clearnet === true; + const stepAction = isEdit ? `/maps/edit/${encodeURIComponent(mapId)}` : "/maps"; + const cleanUrl = isEdit + ? `${stepAction}?mapType=${encodeURIComponent(mapTypeVal)}` + : `/maps?filter=create&mapType=${encodeURIComponent(mapTypeVal)}${params.tribeId ? '&tribeId=' + encodeURIComponent(params.tribeId) : ''}`; const pickerMarkers = latVal && lngVal ? [{ lat: parseFloat(latVal), lng: parseFloat(lngVal) }] : []; + const placeQ = safeText(lastOf(params.place)); + const placeHits = placeQ ? searchPlaces(placeQ, 5) : null; + const explicitView = parseView(params.view); + const prevView = Array.isArray(params.view) ? parseView(params.view[0]) : null; + const view = explicitView + ? makeView(explicitView.lat, explicitView.lng, explicitView.zoom) + : (placeHits && placeHits.length ? viewForPlace(placeHits[0]) : resolveView({ zoom: params.zoom, clat: prevView ? prevView.lat : undefined, clng: prevView ? prevView.lng : undefined }, pickerMarkers, { singleZoom: 8 })); + const zoomVal = view.zoom; + const formId = "map-main-form"; + const nav = { form: formId, action: stepAction }; + const keptKeys = ["lat", "lng", "zoom", "view", "title", "description", "markerLabel", "tags"]; - return div({ class: "map-create-layout" }, - div({ class: "map-form map-form-full" }, - form({ - action: filter === "edit" ? `/maps/update/${encodeURIComponent(mapId)}` : "/maps/create", + return div({ class: "div-center audio-form" }, + params.spreadWarning || null, + h2(isEdit ? i18n.mapUpdateButton : i18n.mapCreateButton), + form({ method: "GET", action: stepAction }, + isEdit ? null : input({ type: "hidden", name: "filter", value: "create" }), + params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null, + isEdit && safeText(params.returnTo) ? input({ type: "hidden", name: "returnTo", value: safeText(params.returnTo) }) : null, + ...keptKeys.filter((k) => params[k] !== undefined && String(lastOf(params[k])) !== "").map((k) => input({ type: "hidden", name: k, value: String(lastOf(params[k])) })), + label(i18n.mapTypeLabel), br(), + div({ class: "apply-row" }, + select({ name: "mapType", class: "report-category-select" }, + ...MAP_REACH.map((t) => option({ value: t, ...(mapTypeVal === t ? { selected: true } : {}) }, mapReachLabel(t)))), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply"))), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, mapReachLabel(mapTypeVal)), + form({ + id: formId, + action: isEdit ? `/maps/update/${encodeURIComponent(mapId)}` : "/maps/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: "create" }), + isEdit ? null : input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "mapType", value: mapTypeVal }), + input({ type: "hidden", name: "view", value: viewParam(view) }), params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null, - label(i18n.title || "Title"), - input({ type: "text", name: "title", placeholder: i18n.mapTitlePlaceholder || "Map title", value: titleVal }), - label(i18n.mapDescriptionLabel), - textarea({ name: "description", placeholder: i18n.mapDescriptionPlaceholder, rows: "3" }, descVal), - label(i18n.mapTagsLabel), + label(i18n.title || "Title"), br(), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.mapTitlePlaceholder || "Map title", value: titleVal }), + br(), br(), + label(i18n.mapDescriptionLabel), br(), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.mapDescriptionPlaceholder, rows: "3" }, descVal), + br(), br(), + label(i18n.mapTagsLabel), br(), input({ type: "text", name: "tags", placeholder: i18n.mapTagsPlaceholder, value: tagsValue }), - label(i18n.mapTypeLabel), - select({ name: "mapType" }, - option({ value: "SINGLE", ...(mapTypeVal === "SINGLE" ? { selected: true } : {}) }, "SINGLE"), - option({ value: "OPEN", ...(mapTypeVal === "OPEN" ? { selected: true } : {}) }, "OPEN"), - option({ value: "CLOSED", ...(mapTypeVal === "CLOSED" ? { selected: true } : {}) }, "CLOSED")), - br(),br(), - label(i18n.mapMarkerLabelField), - textarea({ name: "markerLabel", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, markerLabelVal), - label(i18n.markerImageLabel || "Marker Image"), + br(), br(), + showClearnet ? renderClearnetSelector(clearnetOn, i18n) : null, + label(i18n.mapMarkerLabelField), br(), + textarea({ maxlength: "5000", name: "markerLabel", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, markerLabelVal), + br(), br(), + label(i18n.markerImageLabel || "Marker Image"), br(), input({ type: "file", name: "image", accept: "image/*" }), br(), br(), - label(i18n.mapLatLabel), + label(i18n.mapLatLabel), br(), input({ type: "text", name: "lat", placeholder: i18n.mapLatPlaceholder, value: latVal }), - label(i18n.mapLngLabel), + br(), br(), + label(i18n.mapLngLabel), br(), input({ type: "text", name: "lng", placeholder: i18n.mapLngPlaceholder, value: lngVal }), + br(), br(), div({ class: "map-form-row" }, - button({ type: "submit", attrs: { formmethod: "GET" }, formaction: "/maps", class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"), + button({ type: "submit", attrs: { formmethod: "GET" }, formaction: stepAction, class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"), a({ href: cleanUrl, class: "filter-btn" }, i18n.mapCleanMarkerButton || "Clean Marker")), - renderCoordPreview(latVal, lngVal), - label(i18n.mapZoomLabel || "Zoom"), - select({ name: "zoom" }, - [2, 3, 4, 5, 6, 7, 8].map(z => - option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), - br(),br(), - button({ type: "submit", attrs: { formmethod: "GET" }, formaction: "/maps", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom"), + pickedSpot(latVal, lngVal), + div({ class: "map-toolbar" }, + div({ class: "map-place-search" }, + input({ type: "text", name: "place", value: placeQ, placeholder: i18n.mapPlaceSearchPlaceholder, class: "filter-box__input" }), + button({ type: "submit", attrs: { formmethod: "GET" }, formaction: stepAction, name: "view", value: "", class: "filter-btn" }, i18n.mapSearchButton))), + renderPlaceResults(placeHits, (h) => { + const v = viewForPlace(h); + return button({ type: "submit", attrs: { formmethod: "GET" }, formaction: stepAction, name: "view", value: viewParam(v), class: "map-place-result" }, + span(h.name), h.country ? span({ class: "map-place-result-country" }, h.country) : null); + }), div({ class: "map-form-map-slot" }, - renderMap(pickerMarkers, null, 0, { zoom: zoomVal, centerLat: parseFloat(latVal) || 0, centerLng: parseFloat(lngVal) || 0 })), - button({ type: "submit", class: "create-button" }, filter === "edit" ? i18n.mapUpdateButton : i18n.mapCreateButton)))); + renderMap(pickerMarkers, null, 0, { view, id: "map", nav, pickForm: formId, pickAction: stepAction })), + button({ type: "submit", class: "create-button" }, isEdit ? i18n.mapUpdateButton : i18n.mapCreateButton))); }; -const renderMarkerForm = (mapObj, returnTo, params = {}, tribeMembers = []) => { - if (mapObj.mapType === "SINGLE") return null; - if (mapObj.mapType === "CLOSED" && String(mapObj.author) !== String(userId)) return null; - if (mapObj.mapType === "OPEN" && mapObj.tribeId && !tribeMembers.includes(userId)) return null; - const mkLat = params.mkLat || ""; - const mkLng = params.mkLng || ""; - const zoomVal = parseInt(params.zoom) || 2; +const MARKER_FORM_ID = "add-marker-form"; - const existingMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( - safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); - if (mkLat && mkLng) existingMarkers.push({ lat: parseFloat(mkLat), lng: parseFloat(mkLng) }); +const canAddMarkers = (mapObj, tribeMembers = []) => { + if (mapObj.mapType === "SINGLE") return false; + if (mapObj.mapType === "CLOSED" && String(mapObj.author) !== String(userId)) return false; + if (mapObj.mapType === "OPEN" && mapObj.tribeId && !safeArr(tribeMembers).includes(userId)) return false; + return true; +}; - const pinLabels = [mapObj.markerLabel || mapObj.description || mapObj.title || ""].concat( - safeArr(mapObj.markers).map((m) => m.label || "")); +const allMarkersOf = (mapObj) => [{ + key: mapObj.key, + lat: mapObj.lat, + lng: mapObj.lng, + label: mapObj.markerLabel || mapObj.description || mapObj.title || i18n.mapMarkerDefault, + image: mapObj.image || "", + author: mapObj.author, + createdAt: mapObj.createdAt, + root: true +}].concat(safeArr(mapObj.markers).map((m) => ({ ...m, root: false }))); - const mkCleanUrl = `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(params.filter || "all")}`; - const clickUrl = `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(params.filter || "all")}&zoom=${zoomVal}&`; +const firstLine = (s, n = 140) => { + const line = String(s || "").split(/\r?\n/)[0].trim(); + return line.length > n ? line.slice(0, n - 1) + "…" : line; +}; + +const renderMarkerForm = (mapObj, returnTo, params = {}, tribeMembers = [], ctx = {}) => { + if (!canAddMarkers(mapObj, tribeMembers)) return null; + const mkLat = ctx.mkLat || ""; + const mkLng = ctx.mkLng || ""; + const base = `/maps/${encodeURIComponent(mapObj.key)}`; + const filter = params.filter || "all"; + const viewVal = ctx.view ? viewParam(ctx.view) : ""; + const mkCleanUrl = `${base}?${buildQuery({ filter, view: viewVal, focus: ctx.focus >= 0 ? ctx.focus : undefined })}#add-marker`; return div({ class: "map-marker-form", id: "add-marker" }, h3(i18n.mapAddMarkerTitle), - form({ method: "POST", action: `/maps/${encodeURIComponent(mapObj.key)}/marker`, class: "map-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + form({ id: MARKER_FORM_ID, method: "POST", action: `${base}/marker`, class: "map-form", enctype: "multipart/form-data" }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + input({ type: "hidden", name: "filter", value: filter }), + viewVal ? input({ type: "hidden", name: "view", value: viewVal }) : null, + ctx.focus >= 0 ? input({ type: "hidden", name: "focus", value: String(ctx.focus) }) : null, + pickedSpot(mkLat, mkLng), label(i18n.mapMarkerLabelField), - textarea({ name: "label", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, params.mkMarkerLabel || ""), + textarea({ maxlength: "5000", name: "label", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, params.mkMarkerLabel || ""), label(i18n.markerImageLabel || "Marker Image"), input({ type: "file", name: "image", accept: "image/*" }), - br(),br(), + br(), br(), label(i18n.mapMarkerLatLabel), input({ type: "text", name: "mkLat", placeholder: i18n.mapLatPlaceholder, value: String(mkLat) }), label(i18n.mapMarkerLngLabel), input({ type: "text", name: "mkLng", placeholder: i18n.mapLngPlaceholder, value: String(mkLng) }), - div({ class: "map-form-row" }, - button({ type: "submit", attrs: { formmethod: "GET" }, formaction: `/maps/${encodeURIComponent(mapObj.key)}`, class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"), - a({ href: mkCleanUrl, class: "filter-btn" }, i18n.mapCleanMarkerButton || "Clean Marker")), - renderCoordPreview(mkLat, mkLng), - label(i18n.mapZoomLabel || "Zoom"), - select({ name: "zoom" }, - [2, 3, 4, 5, 6, 7, 8].map(z => - option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), - br(),br(), - button({ type: "submit", attrs: { formmethod: "GET" }, formaction: `/maps/${encodeURIComponent(mapObj.key)}`, class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom"), - div({ class: "map-form-map-slot" }, - renderMap(existingMarkers, clickUrl, 0, { latParam: "mkLat", lngParam: "mkLng", pinLabels, pinPrefix: `mk${areaCounter}`, zoom: zoomVal, centerLat: parseFloat(mkLat) || parseFloat(mapObj.lat) || 0, centerLng: parseFloat(mkLng) || parseFloat(mapObj.lng) || 0 })), - button({ type: "submit", class: "create-button" }, i18n.mapAddMarkerButton))); + div({ class: "map-form-row map-form-actions" }, + button({ type: "submit", class: "create-button" }, i18n.mapAddMarkerButton), + a({ href: mkCleanUrl, class: "filter-btn" }, i18n.mapClearPick)))); }; -const renderMarkersList = (markers, mapObj) => { - const allMarkers = []; - if (mapObj) { - allMarkers.push({ - lat: mapObj.lat, - lng: mapObj.lng, - label: mapObj.markerLabel || mapObj.description || mapObj.title || i18n.mapMarkerDefault, - author: mapObj.author, - createdAt: mapObj.createdAt - }); - } - allMarkers.push(...safeArr(markers)); +const popupNotes = (s, n = 600) => { + const text = String(s || "").trim(); + if (text.length <= n) return text; + const cut = text.slice(0, n); + return cut.slice(0, Math.max(cut.lastIndexOf(" "), n - 40)) + "…"; +}; + +const renderPinPopup = (mk, i, ctx = {}) => { + const lat = numOrNull(mk.lat) === null ? 0 : numOrNull(mk.lat); + const lng = numOrNull(mk.lng) === null ? 0 : numOrNull(mk.lng); + const base = ctx.publicBase || ""; + const notes = ctx.notesHtml ? ctx.notesHtml(popupNotes(mk.label)) : renderStyledHtml(popupNotes(mk.label)); + const own = !ctx.readOnly && !mk.root && mk.key && String(mk.author) === String(userId); + const parts = [ + `
${mk.root ? "★" : i}` + + (ctx.closeHref ? `×` : "") + `
`, + isBlob(mk.image) ? `` : "", + notes ? `
${notes}
` : "", + `${lat.toFixed(4)}, ${lng.toFixed(4)}`, + `
` + (ctx.readOnly ? "" : userLink(mk.author).outerHTML) + + (mk.createdAt ? `${escapeHtml(moment(mk.createdAt).fromNow())}` : "") + `
`, + `
` + + (ctx.centerHref ? `${escapeHtml(i18n.mapCenterHere)}` : "") + + (own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) }, + ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null, + button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, renderIcon("close"))).outerHTML : "") + `
` + ]; + return parts.join(""); +}; + +const renderMarkersList = (markers, mapObj, ctx = {}) => { + const allMarkers = mapObj ? allMarkersOf({ ...mapObj, markers }) : safeArr(markers); if (!allMarkers.length) return null; + const focus = Number.isInteger(ctx.focus) ? ctx.focus : -1; + const base = ctx.publicBase || ""; return div({ class: "map-markers-list" }, - h3(i18n.mapMarkersTitle), - br(), - div(allMarkers.flatMap((mk, i) => [ - ...(i > 0 ? [br()] : []), - div({ class: "map-marker-info" }, - span({ class: "map-marker-dot" }, "ꔌ"), - span({ class: "map-marker-coords" }, `${(typeof mk.lat === 'number' ? mk.lat : 0).toFixed(4)}, ${(typeof mk.lng === 'number' ? mk.lng : 0).toFixed(4)}`), - span({ class: "map-marker-meta" }, - userLink(mk.author), - ` · ${moment(mk.createdAt).fromNow()}`)) - ]))); + h3(`${i18n.mapMarkersTitle} (${allMarkers.length})`), + div(allMarkers.map((mk, i) => { + const lat = numOrNull(mk.lat) === null ? 0 : numOrNull(mk.lat); + const lng = numOrNull(mk.lng) === null ? 0 : numOrNull(mk.lng); + const centerHref = ctx.viewHref ? ctx.viewHref({ zoom: MAX_ZOOM, lat, lng }, { focus: i }) : null; + const own = !ctx.readOnly && !mk.root && mk.key && String(mk.author) === String(userId); + return div({ class: "map-marker-row" + (i === focus ? " map-marker-row--focus" : ""), id: `marker-${i}` }, + span({ class: "map-marker-idx" + (mk.root ? " map-marker-idx--main" : "") }, mk.root ? "★" : String(i)), + div({ class: "map-marker-body" }, + div({ class: "map-marker-text" }, + firstLine(mk.label) ? span({ class: "map-marker-label" }, firstLine(mk.label)) : null, + span({ class: "map-marker-coords" }, `${lat.toFixed(4)}, ${lng.toFixed(4)}`), + span({ class: "map-marker-meta" }, + ctx.readOnly ? null : userLink(mk.author), + mk.createdAt ? span({ class: "map-marker-date" }, moment(mk.createdAt).fromNow()) : null)), + isBlob(mk.image) ? img({ src: blobSrc(mk.image, base), class: "map-marker-thumb", alt: "" }) : null), + div({ class: "map-marker-actions" }, + centerHref ? a({ href: centerHref, class: "map-center-link" }, i18n.mapCenterHere) : null, + own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) }, + ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null, + button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, renderIcon("close"))) : null)); + }))); }; -const renderMapCard = (mapObj, filter, params = {}) => { - const returnTo = buildReturnTo(filter, params); - const ownerActions = renderMapOwnerActions(filter, mapObj, params); - const markerCount = safeArr(mapObj.markers).length; +const renderMapCard = exports.renderMapCard = (mapObj, filter, params = {}) => { + const returnTo = params.returnTo || buildReturnTo(filter, params); + const markerCount = safeArr(mapObj.markers).length + 1; const thumbMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); - const thumbFile = renderMapWithPins(thumbMarkers, 0); - const thumbSrc = thumbFile ? `/mapcache/${thumbFile}` : "/assets/images/worldmap-z2.png"; + const origin = mapObj.tribeOrigin || null; const chips = [ - renderEncryptedChip(i18n), - renderLifespanChip(mapObj.lifetime, i18n) + renderStateChip("whole", "🗺", String(mapObj.mapType || "").toUpperCase()), + origin ? renderTribeOriginChip(origin) : renderEncryptedChip(i18n), + renderLifespanChip(mapObj.lifetime, i18n), + !origin && mapObj.clearnet === true && clearnetEligible(mapObj) ? renderReachChip(true, i18n, mapClearnetHref(mapObj)) : null ].filter(Boolean); const isOwn = mapObj.author && String(mapObj.author) === String(userId); - return div({ class: "trending-card map-card" + (isOwn ? " own-content" : "") }, + const href = origin ? origin.href : `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`; + return div({ class: "tribe-card" + (isOwn ? " own-content" : "") }, div({ class: "card-header activity-card-header" }, - span({ class: "map-type-badge" }, mapObj.mapType), - renderContentActions(mapObj.key, `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`)), - div({ class: "card-section maps-card-body" }, - a({ href: `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`, class: "map-card-thumb-link" }, - { innerHTML: `map` }), - div({ class: "map-card-body" }, - div({ class: "shop-title-row" }, - mapObj.title ? h2({ class: "tribe-card-title" }, a({ href: `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}` }, mapObj.title)) : null - ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "map-card-header" }, - div({ class: "map-card-info" }, - span({ class: "map-coords" }, `📍 ${mapObj.lat.toFixed(4)}, ${mapObj.lng.toFixed(4)}`), - markerCount > 0 ? span({ class: "map-marker-count" }, `▾ ${markerCount}`) : null, - mapObj.key ? renderMapUrl(mapObj) : null), - div({ class: "map-card-actions" }, - renderMapFavoriteToggle(mapObj, returnTo), - renderPMButton(mapObj.author), - ...ownerActions)), - safeText(mapObj.description) ? p({ class: "map-description" }, mapObj.description) : null, - (() => { - const btn = renderSpreadButton(mapObj.key, params.spreadMap && params.spreadMap.get(mapObj.key)); - return btn ? div({ class: "card-spread-left" }, btn) : null; - })(), - p({ class: "card-footer" }, - span({ class: "date-link" }, moment(mapObj.createdAt).fromNow()), - span(" · "), - userLink(mapObj.author))))); + span(), + renderContentActions(mapObj.key, href, origin + ? { author: mapObj.author, reportTitle: mapObj.title } + : { spread: params.spreadMap && params.spreadMap.get(mapObj.key) || null, author: mapObj.author, favKind: 'maps', isFavorite: mapObj.isFavorite, reportTitle: mapObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction('map', mapObj.key) : undefined }) + ), + div({ class: "tribe-card-image-wrapper map-card-thumb" }, + a({ href }, renderMap(thumbMarkers, null, 0, { thumb: true, singleZoom: 5 })) + ), + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href }, mapObj.title || i18n.mapAllSectionTitle)) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + p({ class: "job-meta-line" }, `📍 ${mapObj.lat.toFixed(4)}, ${mapObj.lng.toFixed(4)}`), + safeText(mapObj.description) ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(mapObj.description))) : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.mapMarkersTitle || "Markers"}: ${markerCount}`) + ) + ) + ); }; const renderMapList = (maps, filter, params = {}) => @@ -392,33 +443,50 @@ const renderMapList = (maps, filter, params = {}) => ? maps.map((mapObj) => renderMapCard(mapObj, filter, params)) : p(params.q ? i18n.mapNoMatch : i18n.noMaps); +exports.renderMapInvitePage = (code) => { + const pageContent = div({ class: "invite-page" }, + h2(i18n.tribeInviteCodeText, code), + form({ method: "GET", action: "/maps" }, + input({ type: "hidden", name: "filter", value: "all" }), + button({ type: "submit", class: "filter-btn" }, i18n.walletBack) + ) + ); + return template(i18n.mapInviteMode || i18n.tribeInviteCodeText || "Invite", section(pageContent)); +}; + exports.mapsView = async (maps, filter = "all", mapId = null, params = {}) => { - const title = filter === "mine" ? i18n.mapMineSectionTitle - : filter === "create" ? i18n.mapCreateSectionTitle - : filter === "edit" ? i18n.mapUpdateSectionTitle - : filter === "recent" ? i18n.mapRecentSectionTitle - : filter === "favorites" ? i18n.mapFavoritesSectionTitle - : i18n.mapAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(mapId) }; + const title = i18n.mapTitle; const q = safeText(params.q || ""); const list = safeArr(maps); const mapToEdit = mapId ? list.find((m) => m.key === mapId) : null; const allMarkers = list.map((m) => ({ lat: m.lat, lng: m.lng, href: `/maps/${encodeURIComponent(m.key)}` })); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const censusMp = Array.isArray(params.censusList) ? params.censusList : list; + const mapsChip = (mode) => { + if (mode === filter) return true; + if (mode === "mine") return censusMp.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusMp.length > 0; + if (mode === "favorites") return censusMp.some((x) => x.isFavorite); + return true; + }; return template(title, section( - div({ class: "tags-header" }, h2(title), p(i18n.mapDescription)), - renderFilters(filter, q)), + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.mapDescription)), + renderFilters(filter, q, emptyMod, mapsChip)), section( filter === "create" || filter === "edit" ? renderMapForm(filter, mapId, mapToEdit, { ...params, filter }) : section( - div({ class: "maps-search" }, + emptyMod ? null : div({ class: "maps-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form({ method: "GET", action: "/maps", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), input({ type: "text", name: "q", value: q, placeholder: i18n.mapSearchPlaceholder, class: "filter-box__input" }), div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.mapSearchButton)))), - div({ class: "maps-list" }, renderMapList(list, filter, { q }))))); + div({ class: "jobs-grid" }, renderMapList(paged(list), filter, { q, spreadMap: params.spreadMap }))))); }; exports.singleMapView = async (mapObj, filter = "all", params = {}) => { @@ -426,74 +494,277 @@ exports.singleMapView = async (mapObj, filter = "all", params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q }); const ownerActions = renderMapOwnerActions(filter, mapObj, { q }); const tribeMembers = safeArr(params.tribeMembers); - const zoomVal = parseInt(params.zoom) || 2; + const canClearnet = clearnetEligible(mapObj); + const isClearnet = canClearnet && mapObj.clearnet === true; - const allMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( - safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); + const base = `/maps/${encodeURIComponent(mapObj.key)}`; + const allMarkers = allMarkersOf(mapObj); + const focusRaw = parseInt(lastOf(params.focus)); + const focus = Number.isInteger(focusRaw) && focusRaw >= 0 && focusRaw < allMarkers.length ? focusRaw : -1; + const picked = parsePick(params.pick); + const mkLat = picked ? String(picked.lat) : safeText(lastOf(params.mkLat)); + const mkLng = picked ? String(picked.lng) : safeText(lastOf(params.mkLng)); + const placeQ = safeText(lastOf(params.place)); + const placeHits = placeQ ? searchPlaces(placeQ, 5) : null; + const explicitView = parseView(params.view); + const view = explicitView + ? makeView(explicitView.lat, explicitView.lng, explicitView.zoom) + : (placeHits && placeHits.length ? viewForPlace(placeHits[0]) : resolveView({ zoom: params.zoom, clat: params.clat, clng: params.clng }, allMarkers, { singleZoom: 8 })); + const baseParams = { filter, q, mkLat: mkLat || undefined, mkLng: mkLng || undefined, focus: focus >= 0 ? focus : undefined }; + const viewHref = viewHrefFor(base, baseParams, "#map"); + const pinHrefs = allMarkers.map((m, i) => viewHref({ zoom: view.zoom, lat: m.lat, lng: m.lng }, { focus: i })); + const canAdd = canAddMarkers(mapObj, tribeMembers); + const pageReturnTo = viewHref(view); + const popup = focus >= 0 ? renderPinPopup(allMarkers[focus], focus, { + closeHref: viewHrefFor(base, { ...baseParams, focus: undefined }, "#map")(view), + centerHref: viewHref({ zoom: MAX_ZOOM, lat: allMarkers[focus].lat, lng: allMarkers[focus].lng }, { focus }), + returnTo: pageReturnTo + }) : null; - const pinLabels = [mapObj.markerLabel || mapObj.description || mapObj.title || ""].concat( - safeArr(mapObj.markers).map((m) => m.label || "")); - const pinImages = [mapObj.image || ""].concat(safeArr(mapObj.markers).map((m) => m.image || "")); + const mapSide = div({ class: "tribe-side map-detail-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(mapObj.key, `/maps/${encodeURIComponent(mapObj.key)}`, { + author: mapObj.author, + favKind: 'maps', + isFavorite: mapObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: mapObj.title, + deleteAction: String(mapObj.author) === String(userId) ? contentDeleteAction('map', mapObj.key) : undefined + }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, mapObj.title || i18n.mapAllSectionTitle) + ), + div({ class: "card-chips-row" }, + renderStateChip("whole", "🗺", String(mapObj.mapType || "").toUpperCase()), + renderEncryptedChip(i18n), + renderLifespanChip(mapObj.lifetime, i18n), + canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? mapClearnetHref(mapObj) : null) : null, + canClearnet && String(mapObj.author) === String(userId) ? renderClearnetSwitch("maps", mapObj.rootId || mapObj.key, isClearnet) : null + ), + safeText(mapObj.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(safeText(mapObj.description))) : null, + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.mapLatLabel), + td({ class: "tribe-info-value", colspan: "3" }, mapObj.lat.toFixed(6)) + ), + tr( + td({ class: "tribe-info-label" }, i18n.mapLngLabel), + td({ class: "tribe-info-value", colspan: "3" }, mapObj.lng.toFixed(6)) + ), + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm")) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(mapObj.author)) + ) + ), + renderTags(mapObj.tags), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.mapMarkersTitle || "Markers"}: ${safeArr(mapObj.markers).length + 1}`) + ), + (String(mapObj.author) !== String(userId) && !mapObj.tribeId && mapObj.mapType !== "OPEN") + ? div({ class: "tribe-side-actions" }, + a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite) + ) + : null, + renderDocumentActions('maps', mapObj.key), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ); + + const hiddenState = (withView) => [ + input({ type: "hidden", name: "filter", value: filter }), + q ? input({ type: "hidden", name: "q", value: q }) : null, + mkLat && mkLng ? input({ type: "hidden", name: "mkLat", value: mkLat }) : null, + mkLat && mkLng ? input({ type: "hidden", name: "mkLng", value: mkLng }) : null, + focus >= 0 ? input({ type: "hidden", name: "focus", value: String(focus) }) : null, + withView ? input({ type: "hidden", name: "clat", value: String(view.lat) }) : null, + withView ? input({ type: "hidden", name: "clng", value: String(view.lng) }) : null + ]; + + const placeSearch = form({ method: "GET", action: base, class: "filter-box" }, + ...hiddenState(false), + input({ type: "text", name: "place", value: placeQ, placeholder: i18n.mapPlaceSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.mapSearchButton))); + + const mapHero = div({ class: "map-hero" }, + renderPlaceResults(placeHits, (h) => a({ href: viewHref(viewForPlace(h)), class: "map-place-result" }, + span(h.name), h.country ? span({ class: "map-place-result-country" }, h.country) : null)), + renderMapUrl(mapObj), + renderMap(allMarkers, null, 0, { + id: "map", + view, + focus, + pinHrefs, + popup, + nav: { href: viewHref }, + viewHref, + pick: mkLat && mkLng ? { lat: mkLat, lng: mkLng } : null, + pickForm: canAdd ? MARKER_FORM_ID : null, + pickAction: base + })); + + const mapMain = div({ class: "tribe-main map-detail-main" }, + renderMarkersList(mapObj.markers, mapObj, { focus, view, viewHref, returnTo: pageReturnTo }), + p({ class: "card-footer" }, + span({ class: "date-link" }, `${moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(mapObj.author), + mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt + ? span({ class: "votations-comment-date" }, ` · ${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null), + renderMarkerForm(mapObj, returnTo, { ...params, filter }, tribeMembers, { mkLat, mkLng, view, focus }) + ); return template(mapObj.title || i18n.mapTitle, - section(renderFilters(filter, q)), - section( - div({ class: "map-detail" }, - mapObj.title ? div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, mapObj.title) - ) : null, - mapObj.title ? div({ class: "card-chips-row" }, - renderEncryptedChip(i18n), - renderLifespanChip(mapObj.lifetime, i18n) - ) : null, - safeText(mapObj.description) ? p({ class: "map-description" }, mapObj.description) : null, - div({ class: "map-detail-header" }, - div({ class: "map-detail-info" }, - span({ class: "map-type-badge" }, mapObj.mapType), - span({ class: "map-coords-detail" }, `📍 ${mapObj.lat.toFixed(6)}, ${mapObj.lng.toFixed(6)}`)), - div({ class: "map-detail-actions" }, - renderMapFavoriteToggle(mapObj, returnTo), - renderPMButton(mapObj.author), - (String(mapObj.author) !== String(userId) && !mapObj.tribeId && mapObj.mapType !== "OPEN") - ? a({ class: "tribe-action-btn", href: "/invites#invites-maps" }, i18n.tribeEnterInvite) - : null, - ...ownerActions)), - renderMapUrl(mapObj), - br(), - form({ method: "GET", action: `/maps/${encodeURIComponent(mapObj.key)}` }, - label(i18n.mapZoomLabel || "Zoom"), - br(), - select({ name: "zoom" }, - [2, 3, 4, 5, 6, 7, 8].map(z => - option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), - br(), br(), - button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")), - br(), - renderMap(allMarkers, null, 0, { pinLabels, pinImages, pinPrefix: `detail${areaCounter}`, zoom: zoomVal, centerLat: parseFloat(mapObj.lat) || 0, centerLng: parseFloat(mapObj.lng) || 0 }), - renderMarkersList(mapObj.markers, mapObj), - renderTags(mapObj.tags), - (() => { - const btn = renderSpreadButton(mapObj.key); - return btn ? div({ class: "card-spread-centered" }, btn) : null; - })(), - br(), - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), - userLink(mapObj.author), - mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt - ? span({ class: "votations-comment-date" }, ` · ${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`) - : null), - renderMarkerForm(mapObj, returnTo, params, tribeMembers)))); + section(div({ class: "tags-header module-header-line" }, h2(i18n.mapTitle), p(i18n.mapDescription))), + section(renderFilters(filter, q, false, null, placeSearch)), + section(mapHero, div({ class: "tribe-details map-detail-grid" }, mapSide, mapMain))); }; -exports.renderMapLocationUrl = (mapUrl) => { +exports.clearnetMapView = async (mapObj, params = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, renderClearnetPage } = require("./clearnet_view"); + const base = clearnetItemHref("maps", mapObj.title, mapObj.rootId || mapObj.key); + const allMarkers = allMarkersOf(mapObj); + const focusRaw = parseInt(lastOf(params.focus)); + const focus = Number.isInteger(focusRaw) && focusRaw >= 0 && focusRaw < allMarkers.length ? focusRaw : -1; + const explicitView = parseView(params.view); + const view = explicitView + ? makeView(explicitView.lat, explicitView.lng, explicitView.zoom) + : resolveView({ zoom: params.zoom, clat: params.clat, clng: params.clng }, allMarkers, { singleZoom: 8 }); + const viewHref = viewHrefFor(base, { focus: focus >= 0 ? focus : undefined }, "#map"); + const pinHrefs = allMarkers.map((m, i) => viewHref({ zoom: view.zoom, lat: m.lat, lng: m.lng }, { focus: i })); + const popup = focus >= 0 ? renderPinPopup(allMarkers[focus], focus, { + readOnly: true, + publicBase: "/c", + notesHtml: renderRichText, + closeHref: viewHrefFor(base, {}, "#map")(view), + centerHref: viewHref({ zoom: MAX_ZOOM, lat: allMarkers[focus].lat, lng: allMarkers[focus].lng }, { focus }) + }) : null; + const mapHtml = renderMap(allMarkers, null, 0, { id: "map", view, focus, pinHrefs, popup, nav: { href: viewHref }, viewHref, publicBase: "/c" }).outerHTML; + const name = mapObj.title || i18n.cnKindMap; + const desc = renderRichText(mapObj.description || ""); + const coord = (v) => (typeof v === "number" ? v : parseFloat(v) || 0).toFixed(4); + const extraCss = ` +.cn-map-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word} +.cn-map-meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:16px} +.cn-map-desc{color:var(--fg-soft);line-height:1.6;margin:16px 0} +.map-viewer{width:100%} +.map-stage{position:relative;width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;overflow:hidden;line-height:0} +.map-svg{display:block;width:100%;height:auto;font-family:inherit} +.map-pins-layer{position:absolute;top:0;left:0;width:100%;height:100%;z-index:4;pointer-events:none} +.map-pins-layer a{pointer-events:auto} +.map-ocean{fill:#a7c6de} +.map-tile{image-rendering:auto} +.map-grat{fill:none;stroke:rgba(255,255,255,.4);stroke-width:.6} +.map-border{fill:none;stroke:rgba(72,52,32,.65);stroke-width:.8;stroke-linejoin:round} +.map-lake{fill:none;stroke:rgba(36,86,128,.7);stroke-width:.7;stroke-linejoin:round} +.map-state{fill:none;stroke:rgba(72,52,32,.5);stroke-width:.6;stroke-dasharray:3 2} +.map-coast{fill:none;stroke:rgba(36,86,128,.75);stroke-width:.8;stroke-linejoin:round} +.map-label-country{fill:#262c31;font-size:14px;font-weight:600;letter-spacing:.6px;text-anchor:middle;paint-order:stroke;stroke:rgba(255,255,255,.85);stroke-width:3px;stroke-linejoin:round} +.map-place-dot{fill:#1f252a;stroke:#fff;stroke-width:1} +.map-place-capital{fill:#d35400} +.map-place-label{fill:#1f252a;font-size:12px;paint-order:stroke;stroke:rgba(255,255,255,.85);stroke-width:3px;stroke-linejoin:round} +.map-place-label-capital{font-weight:700} +.map-pin-body{fill:#3498db;stroke:#fff;stroke-width:1.5} +.map-pin-main .map-pin-body{fill:#e74c3c} +.map-pin-focus .map-pin-body{fill:var(--accent);stroke-width:2.2} +.map-pin-eye{fill:#fff} +.map-pin-label-bg{fill:var(--bg-elev);stroke:var(--border);stroke-width:.8} +.map-pin-focus .map-pin-label-bg{stroke:var(--accent);stroke-width:1.2} +.map-pin-label{fill:var(--fg);font-size:12px;font-weight:700;text-anchor:middle} +.map-pin-img-frame{fill:#fff} +.map-pins-layer a:hover .map-pin-body,.map-pins a:hover .map-pin-body{fill:var(--accent)} +.map-cluster-ring{fill:var(--bg-elev);opacity:.7} +.map-cluster-dot{fill:var(--accent);stroke:#fff;stroke-width:1.6} +.map-cluster-count{fill:#000;font-size:13px;font-weight:700;text-anchor:middle} +.map-scale-line{fill:none;stroke:#1f252a;stroke-width:1.6} +.map-scale-text{fill:#1f252a;font-size:11px;font-weight:600;paint-order:stroke;stroke:rgba(255,255,255,.85);stroke-width:3px} +.map-attrib{fill:#3b434a;font-size:9px;text-anchor:end;paint-order:stroke;stroke:rgba(255,255,255,.75);stroke-width:2px} +.map-controls{position:absolute;z-index:3;display:flex;flex-direction:column;gap:4px;line-height:1} +.map-controls-zoom{top:10px;left:10px} +.map-controls-pan{top:10px;right:10px;display:grid;grid-template-columns:repeat(3,30px);grid-template-rows:repeat(3,30px);gap:2px} +.map-ctrl{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;background:var(--bg-elev);color:var(--accent);border:1px solid var(--border);border-radius:6px;text-decoration:none;font-size:17px;font-weight:700;font-family:inherit;box-sizing:border-box} +.map-ctrl:hover{background:var(--accent);color:var(--bg);border-color:var(--accent);text-decoration:none} +.map-ctrl-off{opacity:.35} +.map-ctrl-off:hover{background:var(--bg-elev);color:var(--accent);border-color:var(--border)} +.cn-map-marker{display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:8px 6px;border-bottom:1px solid var(--border);color:var(--fg-soft)} +.cn-map-marker--focus{background:var(--bg-sub);box-shadow:inset 3px 0 0 var(--accent)} +.cn-map-marker-idx{min-width:24px;height:24px;border-radius:50%;background:#3498db;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700} +.cn-map-marker-idx--main{background:#e74c3c} +.cn-map-marker--focus .cn-map-marker-idx{background:var(--accent);color:#000} +.cn-map-marker-thumb{width:48px;height:48px;object-fit:cover;border-radius:4px} +.cn-map-marker-text{flex:1;min-width:160px;display:flex;flex-direction:column;gap:2px} +.cn-map-marker-coords{font-family:monospace;color:var(--fg);font-size:13px} +.cn-map-marker-label{word-break:break-word;color:var(--fg)} +.cn-map-marker-date{font-size:12px;color:var(--fg-dim)} +.cn-map-center{color:var(--accent);text-decoration:none;font-size:13px;border:1px solid var(--border);border-radius:6px;padding:4px 8px;white-space:nowrap;margin-left:auto} +.cn-map-center:hover{background:var(--accent);color:var(--bg);text-decoration:none} +.map-popup{pointer-events:auto} +.map-popup-pointer{fill:var(--bg-elev);stroke:var(--border);stroke-width:1} +.map-popup-wrap{height:100%;display:flex;flex-direction:column;justify-content:flex-end;line-height:1.35} +.map-popup--below .map-popup-wrap{justify-content:flex-start} +.map-popup-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--fg);font-size:13px;box-sizing:border-box;max-height:100%;overflow:hidden;display:flex;flex-direction:column;gap:6px;text-align:left} +.map-popup-card>*{flex-shrink:0} +.map-popup-head{display:flex;justify-content:space-between;align-items:center;gap:8px} +.map-popup-head .map-marker-idx{min-width:24px;height:24px;border-radius:50%;background:#3498db;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700} +.map-popup-head .map-marker-idx--main{background:#e74c3c} +.map-popup-close{color:var(--fg-dim);text-decoration:none;font-size:20px;line-height:1} +.map-popup-close:hover{color:var(--accent);text-decoration:none} +.map-popup-img{width:100%;max-height:120px;object-fit:cover;border-radius:4px;display:block} +.map-popup-text{color:var(--fg);word-break:break-word;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden} +.map-popup-text img{max-width:100%;max-height:100px} +.map-popup-coords{font-family:monospace;font-size:12px;color:var(--fg-soft)} +.map-popup-meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:12px;color:var(--fg-dim)} +.map-popup-actions{display:flex;gap:6px;align-items:center} +.map-popup-actions .cn-map-center,.map-popup-actions .map-center-link{margin-left:0;color:var(--accent);text-decoration:none;font-size:13px;border:1px solid var(--border);border-radius:6px;padding:4px 8px} +`; + const markerRow = (mk, i) => { + const centerHref = viewHref({ zoom: MAX_ZOOM, lat: numOrNull(mk.lat) || 0, lng: numOrNull(mk.lng) || 0 }, { focus: i }); + const label = firstLine(mk.label); + return `
` + + `${mk.root ? "★" : i}` + + (isBlob(mk.image) ? `` : "") + + `${label ? `${esc(label)}` : ""}` + + `${coord(mk.lat)}, ${coord(mk.lng)}` + + (mk.createdAt ? `${esc(moment(mk.createdAt).format("YYYY/MM/DD"))}` : "") + + `${esc(i18n.mapCenterHere)}
`; + }; + const body = ` +

${esc(name)}

+
+ ${renderKindTag("map")} + ${mapObj.createdAt ? `📅 ${esc(moment(mapObj.createdAt).format("YYYY/MM/DD"))}` : ""} + 📍 ${coord(mapObj.lat)}, ${coord(mapObj.lng)} +
+ ${desc ? `

${desc}

` : ""} + ${renderTagChips(mapObj.tags)} +
+ ${mapHtml} +

${esc(i18n.mapMarkersTitle || "Markers")} (${allMarkers.length})

+ ${allMarkers.map(markerRow).join("")} +`; + return renderClearnetPage({ + title: `${name} | Oasis`, + ogTitle: name, + ogDescription: mapObj.description || "", + extraCss, + body, + hubFeedId: mapObj.author || null + }); +}; + +exports.renderMapLocationUrl = (rawMapUrl) => { + const mapUrl = localHref(rawMapUrl); if (!mapUrl) return null; return span({ class: "map-location-inline" }, span({ class: "map-location-icon" }, "ꔌ"), a({ href: mapUrl, class: "map-location-link" }, mapUrl)); }; -exports.renderMapLocationVisitLabel = (mapUrl) => { +exports.renderMapLocationVisitLabel = (rawMapUrl) => { + const mapUrl = localHref(rawMapUrl); if (!mapUrl) return null; return div({ class: "card-field" }, span({ class: "card-label" }, (i18n.mapLocationTitle || "Map Location") + ":"), @@ -501,7 +772,8 @@ exports.renderMapLocationVisitLabel = (mapUrl) => { a({ href: mapUrl, class: "map-location-link" }, i18n.mapVisitLabel || "Visit map"))); }; -exports.renderMapEmbed = (mapData, mapUrl) => { +exports.renderMapEmbed = (mapData, rawMapUrl) => { + const mapUrl = localHref(rawMapUrl); if (!mapData || (parseFloat(mapData.lat) === 0 && parseFloat(mapData.lng) === 0)) return exports.renderMapLocationVisitLabel(mapUrl); return div({ class: "map-embed-section" }, @@ -512,7 +784,8 @@ exports.renderMapEmbed = (mapData, mapUrl) => { a({ href: mapUrl, class: "map-location-link" }, mapUrl)) : null); }; -exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => { +exports.renderMapEmbedWithZoom = (mapData, rawMapUrl, detailUrl, zoom) => { + const mapUrl = localHref(rawMapUrl); if (!mapData || (parseFloat(mapData.lat) === 0 && parseFloat(mapData.lng) === 0)) return exports.renderMapLocationVisitLabel(mapUrl); const zoomVal = parseInt(zoom) || 2; @@ -520,15 +793,6 @@ exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => { const lo = parseFloat(mapData.lng) || 0; return div({ class: "map-embed-section" }, span({ class: "card-label" }, (i18n.mapLocationTitle || "Map Location") + ":"), - form({ method: "GET", action: detailUrl }, - label(i18n.mapZoomLabel || "Zoom"), - br(), - select({ name: "zoom" }, - [2, 3, 4, 5, 6, 7, 8].map(z => - option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), - br(), br(), - button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")), - br(), renderMap([{ lat: la, lng: lo }], null, 0, { zoom: zoomVal, centerLat: la, centerLng: lo }), mapUrl ? div({ class: "map-embed-url" }, a({ href: mapUrl, class: "map-location-link" }, mapUrl)) : null); @@ -537,5 +801,5 @@ exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => { exports.renderMapLocationGrid = (lat, lng) => { if (lat === undefined || lng === undefined) return null; return div({ class: "map-location-embed" }, - renderMap([{ lat: parseFloat(lat) || 0, lng: parseFloat(lng) || 0 }], null, 0)); + renderMap([{ lat: parseFloat(lat) || 0, lng: parseFloat(lng) || 0 }], null, 0, { zoom: MIN_ZOOM, centerLat: parseFloat(lat) || 0, centerLng: parseFloat(lng) || 0 })); }; diff --git a/src/views/markdown.js b/src/views/markdown.js deleted file mode 100644 index 4e5d040..0000000 --- a/src/views/markdown.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; - -const md = require("../server/node_modules/ssb-markdown"); -const ssbMessages = require("../server/node_modules/ssb-msgs"); -const ssbRef = require("../server/node_modules/ssb-ref"); -const { span } = require("../server/node_modules/hyperaxe"); - -const toUrl = (mentions) => { - const mentionNames = []; - - const handleLink = ({ name, link }) => { - if (typeof name === "string") { - const atName = name.charAt(0) === "@" ? name : `@${name}`; - mentionNames.push({ name: atName, link }); - } - }; - - ssbMessages.links(mentions, "feed").forEach(handleLink); - - const urlHandler = (ref) => { - const found = mentionNames.find(({ name }) => name === ref); - if (found) return `/author/${encodeURIComponent(found.link)}`; - - if (ssbRef.isFeedId(ref)) return `/author/${encodeURIComponent(ref)}`; - if (ssbRef.isMsgId(ref)) return `/thread/${encodeURIComponent(ref)}`; - - const splitIndex = ref.indexOf("?"); - const blobRef = splitIndex === -1 ? ref : ref.slice(0, splitIndex); - - if (ssbRef.isBlobId(blobRef)) return `/blob/${encodeURIComponent(blobRef)}`; - if (ref && ref[0] === "#") return `/hashtag/${encodeURIComponent(ref.substr(1))}`; - - return ""; - }; - - return urlHandler; -}; - -module.exports = (input, mentions = []) => - md.block(input, { - toUrl: toUrl(mentions), - emoji: (character) => span({ class: "emoji" }, character).outerHTML, - }); diff --git a/src/views/market_view.js b/src/views/market_view.js index 019d4f3..99098c2 100644 --- a/src/views/market_view.js +++ b/src/views/market_view.js @@ -1,25 +1,31 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, th, td, progress, video, audio } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views") +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, th, td, progress, video, audio, hr } = require("../server/node_modules/hyperaxe") +const { renderZoomableImage } = require("./gallery_view") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderOpinionsVoting, renderEngagement , renderContentActions, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const opinionCategories = require("../backend/opinion_categories") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel, renderMapEmbedWithZoom } = require("./maps_view") const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } const userId = config.keys.id +const MARKET_REACH = ["PUBLIC", "HIDDEN"] +const marketReachLabel = (v) => v === "HIDDEN" ? (i18n.visibilityHidden || "Hidden") : (i18n.visibilityPublic || "Public") const parseBidEntry = (raw) => { const s = String(raw || "").trim() @@ -95,17 +101,23 @@ const buildReturnTo = (filter, q, minPrice, maxPrice, sort) => { return `/market${params.length ? `?${params.join("&")}` : ""}` } + +const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((acc, c) => acc + (Number((opinions || {})[c]) || 0), 0) + +const renderStarRating = (opinions, voterCount) => { + const pos = sumCats(opinions, opinionCategories.positive) + const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation) + const total = pos + neg + const full = total > 0 ? Math.round((pos / total) * 5) : 0 + return span({ class: "shop-product-stars" }, "★".repeat(full) + "☆".repeat(5 - full) + ` (${voterCount})`) +} + const renderCardField = (labelText, value = "") => - div({ class: "card-field" }, span({ class: "card-label" }, labelText), span({ class: "card-value" }, ...renderUrl(String(value)))) + div({ class: "card-field" }, span({ class: "card-label" }, labelText), span({ class: "card-value" }, ...renderStyledText(String(value)))) const renderCardFieldRich = (labelText, parts) => div({ class: "card-field" }, span({ class: "card-label" }, labelText), span({ class: "card-value" }, ...(Array.isArray(parts) ? parts : [parts]))) -const renderPmButton = (recipientId) => - recipientId && String(recipientId) !== String(userId) - ? form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: recipientId }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)) - : null - const renderStockBar = (stockValue, maxValue) => { const s = Math.max(0, Number(stockValue || 0)) const m = Math.max(1, Number(maxValue || s || 1)) @@ -121,59 +133,12 @@ const renderStockBar = (stockValue, maxValue) => { } const renderMarketCommentsSection = (itemId, returnTo, comments = []) => { - const commentsCount = Array.isArray(comments) ? comments.length : 0 - - return div( - { class: "vote-comments-section market-comments" }, - div({ class: "comments-count" }, span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), span({ class: "card-value" }, String(commentsCount))), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/market/${encodeURIComponent(itemId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text - return t && String(t).trim() - }) - return visibleComments.length - ? div( - { class: "comments-list" }, - visibleComments.map((c) => { - const author = c.value && c.value.author ? c.value.author : "" - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "" - const relDate = ts ? moment(ts).fromNow() : "" - const userName = author && author.includes("@") ? author.split("@")[1] : author - const rootId = c.value && c.value.content ? c.value.content.fork || c.value.content.root : null - const text = c.value && c.value.content && c.value.content.text ? c.value.content.text : "" - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ) - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - })() - ) -} + return renderSharedCommentsSection({ + action: `/market/${encodeURIComponent(itemId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; const isMyBidItem = (item) => { const polls = Array.isArray(item.auctions_poll) ? item.auctions_poll : [] @@ -197,7 +162,7 @@ const auctionCountdownParts = (deadline) => { const renderCountdownField = (item) => { const cd = item && (item.item_type === "auction" || item.item_type === "exchange") ? auctionCountdownParts(item.deadline) : null if (!cd) return null - return renderCardFieldRich(`${cd.label}:`, [span({ class: "countdown-strong" }, cd.rel)]) + return renderCardFieldRich(`${cd.label}:`, [span({ class: "time-chip" }, cd.rel)]) } const normStatus = (s) => String(s || "").toUpperCase().replace(/_/g, " ").replace(/\s+/g, " ").trim() @@ -217,13 +182,6 @@ const renderMarketOwnerActions = (item, returnTo) => { ) ) } - out.push( - form( - { method: "POST", action: `/market/delete/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.marketActionsDelete) - ) - ) if (canChange) { out.push( form( @@ -231,9 +189,9 @@ const renderMarketOwnerActions = (item, returnTo) => { input({ type: "hidden", name: "returnTo", value: returnTo }), select( { name: "status", class: "project-control-select" }, - option({ value: "FOR SALE", selected: cur === "FOR SALE" }, i18n.marketFilterForSale), - option({ value: "SOLD", selected: cur === "SOLD" }, i18n.marketFilterSold), - option({ value: "DISCARDED", selected: cur === "DISCARDED" }, i18n.marketFilterDiscarded) + option({ value: "FOR SALE", ...(cur === "FOR SALE" ? { selected: true } : {})}, i18n.marketFilterForSale), + option({ value: "SOLD", ...(cur === "SOLD" ? { selected: true } : {})}, i18n.marketFilterSold), + option({ value: "DISCARDED", ...(cur === "DISCARDED" ? { selected: true } : {})}, i18n.marketFilterDiscarded) ), button({ class: "status-btn project-control-btn", type: "submit" }, i18n.marketActionsChangeStatus) ) @@ -242,16 +200,22 @@ const renderMarketOwnerActions = (item, returnTo) => { return out } -const renderMarketTopbar = (item, returnTo) => { - const left = [renderPmButton(item && item.seller)].filter(Boolean) - const right = item && String(item.seller) === String(userId) ? renderMarketOwnerActions(item, returnTo) : [] - const leftNode = left.length ? div({ class: "bookmark-topbar-left transfer-topbar-left" }, ...left) : null - const rightNode = right.length ? div({ class: "bookmark-actions transfer-actions" }, ...right) : null - const children = [] - if (leftNode) children.push(leftNode) - if (rightNode) children.push(rightNode) - return children.length ? div({ class: "bookmark-topbar transfer-topbar-single" }, ...children) : null -} +const marketChipFor = (filter, list) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(list)) return true; + if (mode === "recent") return list.length > 0; + if (mode === "mine") return list.some((e) => String(e.seller) === String(userId)); + if (mode === "exchange") return list.some((e) => e.item_type === "exchange" && e.status === "FOR SALE"); + if (mode === "auctions") return list.some((e) => e.item_type === "auction" && e.status === "FOR SALE"); + if (mode === "mybids") return list.some(isMyBidItem); + if (mode === "new") return list.some((e) => e.item_status === "NEW" && e.status === "FOR SALE"); + if (mode === "used") return list.some((e) => e.item_status === "USED" && e.status === "FOR SALE"); + if (mode === "broken") return list.some((e) => e.item_status === "BROKEN" && e.status === "FOR SALE"); + if (mode === "for sale") return list.some((e) => normStatus(e.status || "FOR SALE") === "FOR SALE"); + if (mode === "sold") return list.some((e) => normStatus(e.status || "") === "SOLD"); + if (mode === "discarded") return list.some((e) => normStatus(e.status || "") === "DISCARDED"); + return true; +}; exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { const list = Array.isArray(items) ? items : [] @@ -260,21 +224,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { const maxPrice = params.maxPrice const sort = params.sort || "recent" - let title = i18n.marketAllSectionTitle - switch (filter) { - case "mine": - title = i18n.marketMineSectionTitle - break - case "create": - title = i18n.marketCreateSectionTitle - break - case "edit": - title = i18n.marketUpdateSectionTitle - break - case "mybids": - title = i18n.marketFilterMyBids - break - } + const title = i18n.marketTitle let filtered = [] switch (filter) { @@ -326,6 +276,8 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { filtered = filtered.filter((it) => matchesQuery(it, q)).filter((it) => withinPrice(it, minP, maxP)) filtered = sortItems(filtered, sort) + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q || minPrice || maxPrice); + const marketChipVisible = marketChipFor(filter, list); const returnTo = buildReturnTo(filter, q, minPrice, maxPrice, sort) const itemEdit = itemToEdit || {} @@ -337,43 +289,47 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { ] const isFormMode = filter === "create" || filter === "edit" + const spreadWarning = filter === "edit" ? await renderSpreadEditWarning(itemEdit && (itemEdit.id || itemEdit.key)) : null + const reachRaw = String(params.reach || "").toUpperCase() + const ownReach = String(itemEdit.visibility || "").toUpperCase() + const reach = MARKET_REACH.includes(reachRaw) ? reachRaw : (MARKET_REACH.includes(ownReach) ? ownReach : "PUBLIC") + const prefillKeys = ["industry", "title", "description", "price", "tags", "stock"] return template( title, section( - div({ class: "tags-header" }, h2(i18n.marketTitle), p(i18n.marketDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.marketTitle), p(i18n.marketDescription), renderWalletChip()), div( { class: "filters" }, form( { method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" }, ...hiddenCtx, - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMine), - button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterItems), - button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAuctions), - button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMyBids), - button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterNew), - button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterUsed), - button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterBroken), - button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterForSale), - button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterSold), - button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterDiscarded), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterRecent), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAll).toUpperCase()), + ...(marketChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterRecent).toUpperCase())] : []), + ...(marketChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMine).toUpperCase())] : []), + ...(marketChipVisible("exchange") ? [button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterItems).toUpperCase())] : []), + ...(marketChipVisible("auctions") ? [button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAuctions).toUpperCase())] : []), + ...(marketChipVisible("mybids") ? [button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMyBids).toUpperCase())] : []), + ...(marketChipVisible("new") ? [button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterNew).toUpperCase())] : []), + ...(marketChipVisible("used") ? [button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterUsed).toUpperCase())] : []), + ...(marketChipVisible("broken") ? [button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterBroken).toUpperCase())] : []), + ...(marketChipVisible("for sale") ? [button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterForSale).toUpperCase())] : []), + ...(marketChipVisible("sold") ? [button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterSold).toUpperCase())] : []), + ...(marketChipVisible("discarded") ? [button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterDiscarded).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton) ) ), - !isFormMode + !isFormMode && !emptyMod ? div( - { class: "market-search" }, + { class: "market-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, e => normStatus(e.status || 'FOR SALE'), [{ value: 'FOR SALE', label: i18n.marketFilterForSale }, { value: 'SOLD', label: i18n.marketFilterSold }, { value: 'DISCARDED', label: i18n.marketFilterDiscarded }]), form( { method: "GET", action: "/market", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter || "all" }), input({ type: "text", name: "q", value: q, placeholder: i18n.marketSearchPlaceholder, class: "filter-box__input" }), - div( - { class: "filter-box__controls" }, - div( - { class: "transfer-range" }, - input({ + input({ type: "number", name: "minPrice", step: "0.000001", @@ -390,16 +346,16 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { value: String(maxPrice ?? ""), placeholder: i18n.marketMaxPriceLabel, class: "filter-box__number transfer-amount-input" - }) - ), + }), + select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.marketSortRecent), - option({ value: "price", selected: sort === "price" }, i18n.marketSortPrice), - option({ value: "deadline", selected: sort === "deadline" }, i18n.marketSortDeadline) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.marketSortRecent), + option({ value: "price", ...(sort === "price" ? { selected: true } : {})}, i18n.marketSortPrice), + option({ value: "deadline", ...(sort === "deadline" ? { selected: true } : {})}, i18n.marketSortDeadline) ), button({ type: "submit", class: "filter-box__button" }, i18n.marketSearchButton) - ) + ) ) : null @@ -408,26 +364,46 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { isFormMode ? div( { class: "market-form" }, + spreadWarning, + form( + { method: "GET", action: filter === "edit" ? `/market/edit/${encodeURIComponent(itemEdit.id)}` : "/market" }, + filter === "edit" ? null : input({ type: "hidden", name: "filter", value: "create" }), + ...(filter === "edit" ? [] : prefillKeys.filter((k) => params[k]).map((k) => input({ type: "hidden", name: k, value: params[k] }))), + label(i18n.visibilityLabel || "Visibility"), + br(), + div({ class: "apply-row" }, + select( + { name: "visibility", class: "report-category-select" }, + option({ value: "PUBLIC", ...(reach === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(reach === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, marketReachLabel(reach)), form( { action: filter === "edit" ? `/market/update/${encodeURIComponent(itemEdit.id)}` : "/market/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: "/market?filter=mine" }), + input({ type: "hidden", name: "visibility", value: reach }), + ((itemEdit && itemEdit.industry) || params.industry) ? input({ type: "hidden", name: "industry", value: (itemEdit && itemEdit.industry) || params.industry }) : null, label(i18n.marketItemType), br(), select( { name: "item_type", id: "item_type", required: true }, - option({ value: "auction", selected: itemEdit && itemEdit.item_type === "auction" }, "Auction"), - option({ value: "exchange", selected: itemEdit && itemEdit.item_type === "exchange" }, "Exchange") + option({ value: "auction", ...(itemEdit && itemEdit.item_type === "auction" ? { selected: true } : {})}, "Auction"), + option({ value: "exchange", ...(itemEdit && itemEdit.item_type === "exchange" ? { selected: true } : {})}, "Exchange") ), br(), br(), label(i18n.marketItemTitle), br(), - input({ type: "text", name: "title", id: "title", value: (itemEdit && itemEdit.title) || "", required: true }), + input({ type: "text", name: "title", maxlength: "100", id: "title", value: (itemEdit && itemEdit.title) || params.title || "", required: true }), br(), br(), label(i18n.marketItemDescription), br(), - textarea({ name: "description", id: "description", placeholder: i18n.marketItemDescriptionPlaceholder, rows: "6", required: true }, (itemEdit && itemEdit.description) || ""), + textarea({ maxlength: "5000", name: "description", id: "description", placeholder: i18n.marketItemDescriptionPlaceholder, rows: "6", required: true }, (itemEdit && itemEdit.description) || params.description || ""), br(), br(), label(i18n.marketCreateFormImageLabel), @@ -439,15 +415,15 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { br(), select( { name: "item_status", id: "item_status" }, - option({ value: "BROKEN", selected: itemEdit && itemEdit.item_status === "BROKEN" }, "BROKEN"), - option({ value: "USED", selected: itemEdit && itemEdit.item_status === "USED" }, "USED"), - option({ value: "NEW", selected: itemEdit && itemEdit.item_status === "NEW" }, "NEW") + option({ value: "BROKEN", ...(itemEdit && itemEdit.item_status === "BROKEN" ? { selected: true } : {})}, "BROKEN"), + option({ value: "USED", ...(itemEdit && itemEdit.item_status === "USED" ? { selected: true } : {})}, "USED"), + option({ value: "NEW", ...(itemEdit && itemEdit.item_status === "NEW" ? { selected: true } : {})}, "NEW") ), br(), br(), label(i18n.marketItemStock), br(), - input({ type: "number", name: "stock", id: "stock", value: (itemEdit && itemEdit.stock) || 1, required: true, min: "1", step: "1" }), + input({ type: "number", name: "stock", id: "stock", value: (itemEdit && itemEdit.stock) || params.stock || 1, required: true, min: "1", step: "1" }), br(), br(), label(i18n.mapLocationTitle || "Map Location"), @@ -455,23 +431,16 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: itemEdit?.mapUrl || "" }), br(), br(), - label(i18n.visibilityLabel || "Visibility"), - br(), - select( - { name: "visibility" }, - option({ value: "PUBLIC", selected: (itemEdit?.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", selected: itemEdit?.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") - ), - br(), - br(), + ...(reach === "PUBLIC" ? [renderClearnetSelector(itemEdit.clearnet === true || String(itemEdit.clearnet || "") === "1", i18n), br()] : []), label(i18n.marketItemPrice), br(), - input({ type: "number", name: "price", id: "price", value: (itemEdit && itemEdit.price) || "", required: true, step: "0.000001", min: "0.000001" }), + input({ type: "number", name: "price", id: "price", value: (itemEdit && itemEdit.price) || params.price || "", required: true, step: "0.000001", min: "0.000001" }), + renderEcoValueChip(), br(), br(), label(i18n.marketItemTags), br(), - input({ type: "text", name: "tags", id: "tags", placeholder: i18n.marketItemTagsPlaceholder, value: (itemEdit && itemEdit.tags && itemEdit.tags.join(", ")) || "" }), + input({ type: "text", name: "tags", id: "tags", placeholder: i18n.marketItemTagsPlaceholder, value: (itemEdit && itemEdit.tags && itemEdit.tags.join(", ")) || params.tags || "" }), br(), br(), label(i18n.marketItemDeadline), @@ -495,7 +464,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { name: "includesShipping", value: "1", class: "meme-checkbox", - ...(itemEdit && itemEdit.includesShipping ? { checked: true } : {}) + ...(itemEdit && [true, "1", "on", "true"].includes([].concat(itemEdit.includesShipping).pop()) ? { checked: true } : {}) }), br(),br(), button({ type: "submit" }, filter === "edit" ? i18n.marketUpdateButton : i18n.marketCreateButton) @@ -504,75 +473,45 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { : div( { class: "market-grid" }, filtered.length > 0 - ? filtered.map((item) => { - const polls = Array.isArray(item.auctions_poll) ? item.auctions_poll : [] - const parsedBids = polls.map(parseBidEntry).filter(Boolean).sort((a, b) => new Date(b.time) - new Date(a.time)) - const myBid = item.item_type === "auction" ? parsedBids.some((b) => b.bidder === userId) : false - const maxStock = item.initialStock || item.stockMax || item.stock || 1 - const stockLeft = Number(item.stock || 0) + ? paged(filtered).map((item) => { const isOwner = String(item.seller) === String(userId) - const actionNodesRaw = isOwner - ? renderMarketOwnerActions(item, "/market?filter=mine") - : [ - item.status !== "SOLD" && item.status !== "DISCARDED" && item.item_type === "auction" - ? form( - { method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }), - br(), - button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton) - ) - : null, - item.status === "FOR SALE" && item.item_type !== "auction" && !isOwner && stockLeft > 0 - ? form( - { method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: "/inbox?filter=sent" }), - input({ type: "hidden", name: "buyerId", value: userId }), - button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy) - ) - : null - ].filter(Boolean) - return div({ class: "tribe-card market-tribe-card" }, div({ class: "tribe-card-image-wrapper" }, a({ href: `/market/${encodeURIComponent(item.id)}` }, renderMediaBlob(item.image, '/assets/images/default-market.png', { class: 'tribe-card-hero-image' }) ) ), + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(item.id, `/market/${encodeURIComponent(item.id)}`, { spread: (params.spreadMap && params.spreadMap.get(item.id)) || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isOwner ? contentDeleteAction('market', item.id) : undefined }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/market/${encodeURIComponent(item.id)}` }, item.title)) ), + renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), div({ class: "card-chips-row" }, + String(item.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, renderStateChip("encrypted", "", String(item.item_type || "").toUpperCase()), item.item_status ? renderStateChip("whole", "", String(item.item_status).toUpperCase()) : null, - String(item.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, item.includesShipping ? renderStateChip("mutuals", "📦", String(i18n.marketItemIncludesShipping || "Shipping").replace(/\?$/, "").toUpperCase()) : null, - renderLifespanChip(item.lifetime, i18n) + item.industry ? a({ href: `/industry/${encodeURIComponent(item.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, + renderLifespanChip(item.lifetime, i18n), + item.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref("market", item.title, item.id)) : null ), - div({ class: "market-card-price card-date-highlight" }, `${item.price} ECO`), - div({ class: "card-visit-btn-centered" }, - form({ method: 'GET', action: `/market/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - input({ type: "hidden", name: "q", value: q }), - input({ type: "hidden", name: "minPrice", value: String(minPrice ?? "") }), - input({ type: "hidden", name: "maxPrice", value: String(maxPrice ?? "") }), - input({ type: "hidden", name: "sort", value: sort }), - button({ type: 'submit', class: 'filter-btn' }, i18n.viewItem || i18n.marketVisitItem || 'View Item') - ) - ) + div({ class: "price-chip" }, `${item.price} ECO`) ) ) }) - : p(i18n.marketNoItemsMatch || i18n.marketNoItems) + : p(q || String(minPrice ?? "") || String(maxPrice ?? "") ? (i18n.marketNoItemsMatch || i18n.marketNoItems) : (i18n.marketNoItems || i18n.marketNoItemsMatch)) ) ) ) } exports.singleMarketView = async (item, filter, comments = [], params = {}) => { + const marketChipVisible = (mode) => marketChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null)(mode); const polls = Array.isArray(item.auctions_poll) ? item.auctions_poll : [] const parsedBids = polls.map(parseBidEntry).filter(Boolean).sort((a, b) => new Date(b.time) - new Date(a.time)) const q = params.q || "" @@ -580,44 +519,46 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { const maxPrice = params.maxPrice const sort = params.sort || "recent" const returnTo = params.returnTo || buildReturnTo(filter, q, minPrice, maxPrice, sort) - const topbar = renderMarketTopbar(item, returnTo) const stockLeft = Number(item.stock || 0) const showBuy = item.status === "FOR SALE" && item.item_type !== "auction" && String(item.seller) !== String(userId) && stockLeft > 0 const maxStock = item.initialStock || item.stockMax || item.stock || 1 return template( item.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.marketTitle), p(i18n.marketDescription), renderWalletChip())), section( div( { class: "filters" }, form( { method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "minPrice", value: minPrice ?? "" }), input({ type: "hidden", name: "maxPrice", value: maxPrice ?? "" }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMine), - button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterItems), - button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAuctions), - button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMyBids), - button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterNew), - button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterUsed), - button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterBroken), - button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterForSale), - button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterSold), - button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterDiscarded), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterRecent), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAll).toUpperCase()), + ...(marketChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterRecent).toUpperCase())] : []), + ...(marketChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMine).toUpperCase())] : []), + ...(marketChipVisible("exchange") ? [button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterItems).toUpperCase())] : []), + ...(marketChipVisible("auctions") ? [button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAuctions).toUpperCase())] : []), + ...(marketChipVisible("mybids") ? [button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMyBids).toUpperCase())] : []), + ...(marketChipVisible("new") ? [button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterNew).toUpperCase())] : []), + ...(marketChipVisible("used") ? [button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterUsed).toUpperCase())] : []), + ...(marketChipVisible("broken") ? [button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterBroken).toUpperCase())] : []), + ...(marketChipVisible("for sale") ? [button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterForSale).toUpperCase())] : []), + ...(marketChipVisible("sold") ? [button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterSold).toUpperCase())] : []), + ...(marketChipVisible("discarded") ? [button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterDiscarded).toUpperCase())] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton) ) ), (() => { const isHidden = String(item.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' + const isClearnet = !!item.clearnet const chips = [ + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("market", item.title, item.id) : null), + isHidden ? renderVisibilityChip("HIDDEN", i18n) : null, renderStateChip("encrypted", "", String(item.item_type || "").toUpperCase()), item.item_status ? renderStateChip("whole", "", String(item.item_status).toUpperCase()) : null, item.includesShipping ? renderStateChip("mutuals", "📦", String(i18n.marketItemIncludesShipping || "Shipping").replace(/\?$/, "").toUpperCase()) : null, - isHidden ? renderVisibilityChip("HIDDEN", i18n) : null, + item.industry ? a({ href: `/industry/${encodeURIComponent(item.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(item.lifetime, i18n), renderEcoTax(item.msgSize, item.id) ].filter(Boolean) @@ -634,26 +575,13 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { )) pushRow(i18n.marketItemSeller, userLink(item.seller)) if (item.shopId && item.shopTitle) pushRow(i18n.marketShopLabel || "Shop", a({ href: `/shops/${encodeURIComponent(item.shopId)}`, class: "user-link" }, item.shopTitle)) - if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm:ss")) - - const itemSide = div({ class: "tribe-side" }, - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, item.title) - ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(item.id, params.spreads)), - renderMediaBlob(item.image, "/assets/images/default-market.png"), - div({ class: "card-date-highlight" }, `${item.price} ECO`), - renderStockBar(item.stock, maxStock), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), - tagsNode - ) + if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm")) const visibilityRow = String(item.seller) === String(userId) ? (() => { const vis = isHidden ? 'HIDDEN' : 'PUBLIC' const next = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC' - return div({ class: "tribe-side-actions" }, + return div({ class: "tribe-side-actions housing-status-row" }, span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), renderVisibilityChip(vis, i18n), form({ method: "POST", action: `/market/visibility/${encodeURIComponent(item.id)}`, class: "inline-form" }, @@ -666,28 +594,32 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { })() : null - const marketActions = [] - if (item.seller && String(item.seller) !== String(userId)) { - marketActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: item.seller }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )) - } - if (String(item.seller) === String(userId)) { - marketActions.push(form({ method: "GET", action: `/market/edit/${encodeURIComponent(item.id)}` }, - button({ type: "submit", class: "update-btn" }, i18n.marketUpdateButton || "Update") - )) - marketActions.push(form({ method: "POST", action: `/market/delete/${encodeURIComponent(item.id)}` }, - button({ type: "submit", class: "delete-btn" }, i18n.marketDeleteButton || "Delete") - )) - } - const itemMain = div({ class: "tribe-main" }, - marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null, + const marketActions = String(item.seller) === String(userId) ? renderMarketOwnerActions(item, `/market/${encodeURIComponent(item.id)}`) : [] + + const itemSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(item.id, `/market/${encodeURIComponent(item.id)}`, { spread: params.spreads || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title, returnTo: `/market/${encodeURIComponent(item.id)}`, deleteAction: String(item.seller) === String(userId) ? contentDeleteAction('market', item.id) : undefined }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, item.title) + ), + renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + String(item.seller) === String(userId) ? renderClearnetSwitch("market", item.rootId || item.id, isClearnet) : null, + renderMediaBlob(item.image, "/assets/images/default-market.png"), + div({ class: "price-chip" }, `${item.price} ECO`), + renderStockBar(item.stock, maxStock), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + tagsNode, visibilityRow, + marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null + ) + + const itemMain = div({ class: "tribe-main" }, item.description ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.marketItemDescription), - p({ class: "tribe-side-description" }, ...renderUrl(item.description)) + p({ class: "tribe-side-description" }, ...renderStyledText(item.description)) ) : null, renderCountdownField(item), @@ -701,13 +633,13 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { { class: "auction-bid-table" }, tr(th(i18n.marketAuctionBidTime), th(i18n.marketAuctionUser), th(i18n.marketAuctionBidAmount)), parsedBids.map((bid) => - tr(td(moment(bid.time).format("YYYY-MM-DD HH:mm:ss")), td(a({ href: `/author/${encodeURIComponent(bid.bidder)}` }, bid.bidder)), td(`${parseFloat(bid.amount).toFixed(6)} ECO`)) + tr(td(moment(bid.time).format("YYYY/MM/DD HH:mm")), td(a({ href: `/author/${encodeURIComponent(bid.bidder)}` }, bid.bidder)), td(`${parseFloat(bid.amount).toFixed(6)} ECO`)) ) ) : p({ class: "tribe-side-description" }, i18n.marketNoBids || "No bids yet"), item.status !== "SOLD" && item.status !== "DISCARDED" && String(item.seller) !== String(userId) ? form( - { method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}` }, + { method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}`, class: "market-bid-form" }, input({ type: "hidden", name: "returnTo", value: returnTo }), input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }), br(), @@ -720,13 +652,18 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { ? div( { class: "market-item actions" }, form( - { method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}` }, + { method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}`, class: "market-buy-form" }, input({ type: "hidden", name: "returnTo", value: "/inbox?filter=sent" }), button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy) ) ) : null, - renderMarketCommentsSection(item.id, returnTo, comments) + renderEngagement(item.id, + String(item.seller) !== String(userId) && item.purchasedByViewer + ? renderOpinionsVoting("/market/opinions", item.id, item.opinions, returnTo, item.opinions_inhabitants) + : null, + renderMarketCommentsSection(item.id, returnTo, comments) + ) ) return div({ class: "tribe-details" }, itemSide, itemMain) diff --git a/src/views/melody_view.js b/src/views/melody_view.js index 8129272..fc82391 100644 --- a/src/views/melody_view.js +++ b/src/views/melody_view.js @@ -2,7 +2,7 @@ const { div, h2, p, section, audio, source, span, a, form, input, button, br, textarea, label } = require("../server/node_modules/hyperaxe"); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderEcoTax } = require("./main_views"); +const { template, i18n, userLink, renderEcoTax, renderContentActions, paged } = require("./main_views"); const renderSequenceList = (sequence, availableIds = null) => { if (!sequence || sequence.length === 0) { @@ -26,7 +26,7 @@ const renderSequenceList = (sequence, availableIds = null) => { span({ class: "melody-note-name" }, noteName), span({ class: "melody-note-type" }, noteType) ]; - if (reachable) { + if (reachable && process.env.OASIS_MOBILE !== '1') { return a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: "melody-note-chip melody-note-chip-link", title: titleAttr }, ...inner); } const cls = checkAvailability && blockId && !reachable @@ -97,27 +97,31 @@ const renderBcsList = (bcsAudios) => { return p({ class: "empty" }, i18n.melodyAllEmpty || "No BCS compositions from other inhabitants yet."); } return div({ class: "melody-bcs-list" }, - bcsAudios.map(a => div({ class: "melody-bcs-card" }, + bcsAudios.map(item => div({ class: "melody-bcs-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(item.key, `/audios/${encodeURIComponent(item.key)}`, { author: item.author, favKind: 'audios', isFavorite: item.isFavorite, torrentFrom: { blobId: item.url, name: item.title }, reportTitle: item.title }) + ), div({ class: "melody-bcs-head" }, - a.title ? h2({ class: "melody-bcs-title" }, a.title) : null, + item.title ? h2({ class: "melody-bcs-title" }, item.title) : null, div({ class: "melody-bcs-meta" }, - span({ class: "card-label" }, `${i18n.melodyByLabel || "By"}: `), - userLink(a.author), - span({ class: "melody-meta-sep" }, " · "), - span({ class: "card-value" }, moment(a.createdAt).format("YYYY/MM/DD HH:mm:ss")) + userLink(item.author), + span({ class: "melody-bcs-date" }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")) ) ), - a.url + item.url ? div({ class: "audio-container melody-bcs-player" }, - audio({ controls: true, preload: "metadata", src: `/blob/${encodeURIComponent(a.url)}` }) + audio({ controls: true, preload: "metadata", src: `/blob/${encodeURIComponent(item.url)}` }), + form({ method: "GET", action: `/melody/transcode/${encodeURIComponent(item.key)}`, class: "audio-transcode-form" }, + button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") + ) ) - : null, - a.description ? p({ class: "melody-bcs-desc" }, a.description) : null, - div({ class: "melody-bcs-actions" }, - form({ method: "GET", action: `/melody/transcode/${encodeURIComponent(a.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") - ) - ) + : div({ class: "melody-bcs-actions" }, + form({ method: "GET", action: `/melody/transcode/${encodeURIComponent(item.key)}`, class: "audio-transcode-form" }, + button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") + ) + ), + item.description ? p({ class: "melody-bcs-desc" }, item.description) : null )) ); }; @@ -127,13 +131,13 @@ exports.melodyView = ({ feedId, total, sequence, filter, bcsAudios }) => { const description = i18n.melodyDescription || "Play the melody of your blockchain — each block becomes a note."; const activeFilter = filter === "all" ? "all" : "mine"; const body = activeFilter === "all" - ? renderBcsList(bcsAudios) + ? renderBcsList(paged(bcsAudios || [])) : renderOwnPanel({ feedId, total, sequence }); return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(description) ), diff --git a/src/views/mentions_view.js b/src/views/mentions_view.js new file mode 100644 index 0000000..3ef4313 --- /dev/null +++ b/src/views/mentions_view.js @@ -0,0 +1,103 @@ +const { div, h2, p, section, button, form, input, span } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged } = require("./main_views"); +const { getViewDetailsAction } = require("./activity_view"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledText } = require("../backend/renderStyledText"); + +const userId = config.keys.id; + +const excerpt = (text, max = 320) => { + const s = String(text == null ? "" : text).trim(); + return s.length > max ? s.slice(0, max) + "…" : s; +}; + +const typeLabel = (type) => i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || String(type || '').toUpperCase(); + +const renderMentionCard = (item, readKeys, params = {}) => { + const href = getViewDetailsAction(item.type, { id: item.id, key: item.id, author: item.author, content: item.content }); + const isOwn = String(item.author) === String(userId); + const read = readKeys.has(String(item.key)); + return div({ class: "trending-card mention-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + div({ class: 'card-chips-row' }, + span({ class: 'pm-exposition-chip pm-exposition-whole' }, + span({ class: 'pm-exposition-text' }, typeLabel(item.type)) + ), + read ? null : span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.inboxUnreadChip)) + ), + renderContentActions(item.id, href, { + author: item.author, + reportTitle: item.title || item.text, + spread: CONTENT_SPREADABLE.has(item.type) ? ((params.spreadMap && params.spreadMap.get(item.id)) || null) : undefined, + ...(item.type === 'torrent' || item.type === 'file' ? { spreadTitle: i18n.seedAction } : {}), + ...(CONTENT_TORRENTABLE.has(item.type) && item.content && typeof item.content.url === 'string' && item.content.url.startsWith('&') + ? { torrentFrom: { blobId: item.content.url, name: item.content.title || item.content.fileName || '' } } + : {}) + }) + ), + div({ class: "card-section mention-card-body" }, + item.title + ? div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, item.title)) + : null, + item.text ? p({ class: "mention-text" }, ...renderStyledText(excerpt(item.text))) : null, + p({ class: "card-footer" }, + span({ class: "date-link" }, `${moment(item.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(item.author) + ), + div({ class: "pm-actions mention-actions" }, + form({ method: "POST", action: `/mentions/${read ? 'unread' : 'read'}/${encodeURIComponent(item.key)}`, class: "pm-action-form" }, + button({ type: "submit", class: "pm-btn read-btn" }, String(read ? i18n.inboxMarkUnread : i18n.inboxMarkRead).toUpperCase()) + ) + ) + ) + ); +}; + +exports.mentionsView = async (items = [], filter = 'ALL', params = {}) => { + const counts = params.counts || {}; + const types = Object.keys(counts).sort((a, b) => (counts[b] - counts[a]) || a.localeCompare(b)); + const q = params.q || ''; + const readKeys = new Set(Array.isArray(params.readKeys) ? params.readKeys.map(String) : []); + const unreadShown = items.filter(x => x && x.key && !readKeys.has(String(x.key))).map(x => String(x.key)); + const emptyMentions = (!Array.isArray(items) || items.length === 0) && String(filter || 'ALL').toUpperCase() === 'ALL' && !q.trim(); + + return template( + i18n.mentions, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.mentions), + p(i18n.mentionsDescription) + ), + types.length + ? div({ class: "filters" }, + form({ method: "GET", action: "/mentions", class: "ui-toolbar ui-toolbar--filters" }, + ...types.map(t => + button({ type: "submit", name: "filter", value: t, class: filter === t ? "filter-btn active" : "filter-btn" }, + `${typeLabel(t)} (${counts[t]})`) + ) + ) + ) + : null, + emptyMentions ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(items.length), + unreadShown.length ? form({ method: "POST", action: "/mentions/read-all", class: "inbox-vis-toggle" }, + ...unreadShown.map(k => input({ type: "hidden", name: "keys", value: k })), + button({ type: "submit", class: "btn" }, `${i18n.inboxMarkAllRead} (${unreadShown.length})`) + ) : null, + form({ method: "GET", action: "/mentions", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.mentionsSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + ), + section( + items.length + ? div({ class: "mentions-list" }, ...paged(items).map(item => renderMentionCard(item, readKeys, params))) + : div({ class: "no-content-box" }, p({ class: "empty" }, i18n.noMentions)) + ) + ); +}; diff --git a/src/views/mobile_icons.js b/src/views/mobile_icons.js new file mode 100644 index 0000000..ce067ac --- /dev/null +++ b/src/views/mobile_icons.js @@ -0,0 +1,106 @@ +"use strict"; +// Iconos del lanzador y de la barra de abajo (rama movil). +// +// Antes eran letras del alfabeto vai (ꔙ ꕿ ꖒ...): no decian nada de lo que abren, +// dependian de que el movil trajera la fuente y Karvan y Nodos llevaban casi el +// mismo reloj de arena. Estos son SVG de trazo en rejilla de 24, el mismo estilo +// que los ACTION_ICONS de las tarjetas (main_views.js), y se pintan con +// currentColor, asi que cada tema les pone su color. + +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { span } = require("../server/node_modules/hyperaxe"); + +const ICONS = { + activity: '', + feed: '', + chats: '', + rooms: '', + forum: '', + pads: '', + wiki: '', + blogs: '', + opinions: '', + mentions: '', + tags: '', + trending: '', + maps: '', + graphos: '', + inhabitants: '', + fediverse: '', + images: '', + videos: '', + audios: '', + podcasts: '', + documents: '', + files: '', + bookmarks: '', + torrents: '', + games: '', + pixelia: '', + melody: '', + events: '', + calendars: '', + tasks: '', + campaigns: '', + mailing: '', + votes: '', + polls: '', + reports: '', + tribes: '', + emergencies: '', + school: '', + larp: '', + parliament: '', + courts: '', + market: '', + shops: '', + jobs: '', + projects: '', + industry: '', + housing: '', + logistics: '', + banking: '', + transfers: '', + profile: '', + agenda: '', + favorites: '', + phone: '', + cv: '', + wallet: '', + logs: '', + invites: '', + peers: '', + bar: '', + backup: '', + cipher: '', + ai: '', + stats: '', + blockexplorer: '', + modules: '', + settings: '', + search: '', + inbox: '', + publish: '', + data: '', + mic: '', + camera: '', + hangup: '' +}; + +// la clave sale de la ruta: /settings/bottombar es "bar" +const iconKeyOf = (href) => { + const h = String(href || '').replace(/^\//, '').split(/[?#]/)[0]; + if (h === 'settings/bottombar') return 'bar'; + return h.split('/')[0]; +}; + +const svg = (key) => ICONS[key] + ? `` + : ''; + +// span con el SVG dentro; si no hay icono para esa clave, el caracter de antes +const iconNode = (cls, key, fallback) => ICONS[key] + ? span({ class: cls + ' has-svg', 'aria-hidden': 'true', [RAW_HTML]: svg(key) }) + : span({ class: cls, 'aria-hidden': 'true' }, fallback || ''); + +module.exports = { ICONS, iconKeyOf, svg, iconNode }; diff --git a/src/views/mobile_modules_view.js b/src/views/mobile_modules_view.js new file mode 100644 index 0000000..ed51f39 --- /dev/null +++ b/src/views/mobile_modules_view.js @@ -0,0 +1,103 @@ +"use strict"; +// Pantalla de Modulos del movil (la sirve el router propio, fork_routes.js). +// +// Los modulos agrupados como en el lanzador, cada uno una fila que se toca entera con +// un interruptor, y Guardar siempre a la vista. La tabla de upstream eran 52 filas +// con casillas de 16px, unas 11 pantallas. Va en un fichero propio para no tocar +// modules_view.js, que es del dev (docs/FRONTERA_UPSTREAM.md). +// +// El formulario envia a la misma ruta de upstream (/save-modules) con los mismos +// nombres de campo (Form), asi que guardar sigue siendo cosa suya. + +const { button, div, form, h2, h3, input, label, p, section, span } = require("../server/node_modules/hyperaxe"); +const { getConfig } = require('../configs/config-manager.js'); +const { iconNode } = require('./mobile_icons'); + +// clave de i18n de cada modulo (modulesLabel / modulesDescription) +const STEMS = { + agenda: 'Agenda', ai: 'AI', aiNav: 'AINav', emergencies: 'Emergencies', mailing: 'Mailing', rooms: 'Rooms', + phone: 'Phone', logistics: 'Logistics', podcasts: 'Podcasts', campaigns: 'Campaigns', audios: 'Audios', banking: 'Banking', + bookmarks: 'Bookmarks', calendars: 'Calendars', chats: 'Chats', cipher: 'Cipher', courts: 'Courts', dev: 'Dev', + docs: 'Docs', events: 'Events', favorites: 'Favorites', blogs: 'Blogs', fediverse: 'Fediverse', feed: 'Feed', + files: 'Files', forum: 'Forum', games: 'Games', graphos: 'Graphos', housing: 'Housing', images: 'Images', + industry: 'Industry', invites: 'Invites', jobs: 'Jobs', larp: 'Larp', backup: 'Backup', + logs: 'Logs', maps: 'Map', market: 'Market', melody: 'Melody', opinions: 'Opinions', pads: 'Pads', + parliament: 'Parliament', pixelia: 'Pixelia', polls: 'Polls', projects: 'Projects', reports: 'Reports', school: 'School', + shops: 'Shops', tags: 'Tags', tasks: 'Tasks', torrents: 'Torrents', transfers: 'Transfers', trending: 'Trending', + tribes: 'Tribes', videos: 'Videos', votes: 'Votations', wallet: 'Wallet', wiki: 'Wiki' +}; +// los que no van en el movil (los apaga tambien /save-modules) +const MOBILE_UNAVAILABLE = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia']; + +const moduleLabel = (i18n, name) => i18n[`modules${STEMS[name]}Label`] || name; + +const mobileModulesView = (opts = {}) => { + const { template, i18n, LAUNCHER_GROUPS } = require('./main_views'); + const config = getConfig().modules || {}; + const visible = Object.keys(STEMS) + .filter(name => !MOBILE_UNAVAILABLE.includes(name)) + .map(name => ({ name, label: moduleLabel(i18n, name), description: i18n[`modules${STEMS[name]}Description`] || '' })) + .sort((x, y) => String(x.label).localeCompare(String(y.label), undefined, { sensitivity: 'base' })); + const isOn = (m) => config[`${m.name}Mod`] === 'on'; + + // grupos del lanzador; Industria va con la economia y lo demas, al final + const groupOf = {}; + (LAUNCHER_GROUPS || []).forEach(g => g.mods.forEach(m => { if (m.mod) groupOf[m.mod.replace(/Mod$/, '')] = g; })); + const economy = (LAUNCHER_GROUPS || []).find(g => g.id === 'economy'); + if (economy && !groupOf.industry) groupOf.industry = economy; + const groups = []; + const byId = {}; + visible.forEach(mod => { + const g = groupOf[mod.name] || { id: 'other', titleKey: 'menuTools' }; + if (!byId[g.id]) { byId[g.id] = { g, mods: [] }; groups.push(byId[g.id]); } + byId[g.id].mods.push(mod); + }); + const order = (LAUNCHER_GROUPS || []).map(g => g.id); + const rank = (id) => { const i = order.indexOf(id); return i === -1 ? order.length : i; }; + groups.sort((x, y) => rank(x.g.id) - rank(y.g.id)); + + const blocked = opts.blocked && visible.find(m => m.name === opts.blocked.mod); + const blockedBox = blocked ? div({ class: 'module-blocked' }, + iconNode('module-blocked-ico', blocked.name), + p({ class: 'module-blocked-text' }, String(i18n.moduleBlockedText || '{name} is turned off.').replace('{name}', blocked.label)), + form({ method: 'POST', action: '/modules/enable' }, + input({ type: 'hidden', name: 'mod', value: blocked.name }), + input({ type: 'hidden', name: 'returnTo', value: opts.blocked.path || `/${blocked.name}` }), + button({ type: 'submit', class: 'create-button' }, String(i18n.moduleBlockedAction || 'Turn on {name}').replace('{name}', blocked.label)) + ) + ) : null; + + return template( + i18n.modules, + section(div({ class: 'tags-header module-header-line' }, + h2(i18n.modulesViewTitle), + p(String(i18n.modulesOnCount || '{on} of {total} sections on') + .replace('{on}', visible.filter(isOn).length).replace('{total}', visible.length)) + )), + blockedBox, + opts.saved ? p({ class: 'module-saved', role: 'status' }, i18n.modulesSaved || 'Saved.') : null, + section( + form({ action: '/save-modules', method: 'post', class: 'module-switches' }, + ...groups.map(({ g, mods }) => div({ class: 'module-group' }, + h3({ class: 'module-group-title' }, i18n[g.titleKey] || g.id), + ...mods.map(mod => label({ class: 'module-switch', for: `${mod.name}Mod` }, + iconNode('module-switch-ico', mod.name), + span({ class: 'module-switch-text' }, + span({ class: 'module-switch-name' }, mod.label), + span({ class: 'module-switch-desc' }, mod.description) + ), + input({ + type: 'checkbox', role: 'switch', id: `${mod.name}Mod`, name: `${mod.name}Form`, value: 'on', class: 'switch-input', + checked: isOn(mod) ? true : undefined + }) + )) + )), + div({ class: 'module-save-bar' }, + button({ type: 'submit', class: 'create-button' }, i18n.saveSettings) + ) + ) + ) + ); +}; + +module.exports = { mobileModulesView, MOBILE_UNAVAILABLE, STEMS }; diff --git a/src/views/mobile_ui.js b/src/views/mobile_ui.js new file mode 100644 index 0000000..c3448e3 --- /dev/null +++ b/src/views/mobile_ui.js @@ -0,0 +1,235 @@ +"use strict"; +// Piezas de interfaz de la rama movil que se enchufan en vistas de upstream. +// +// Las vistas (vote_view, tribes_view, settings_view, main_views, welcome_view) son del +// dev y se reescriben a menudo (tribes_view en 23 de 39 releases, main_views en 31). +// Aqui vive la logica; en cada vista queda una llamada con la bandera de movil, que es +// lo unico que hay que volver a poner si el dev la reescribe. +// Ver docs/FRONTERA_UPSTREAM.md. +// +// i18n se lee al llamar (main_views lo cambia en cada peticion y este fichero se carga +// antes que el). + +const { a, button, details, div, form, img, input, label, nav, p, progress, span, summary } = require("../server/node_modules/hyperaxe"); +const { iconNode } = require("./mobile_icons"); + +const isMobile = () => process.env.OASIS_MOBILE === '1'; +const t = () => require('./main_views').i18n; + +// ---------- votaciones ---------- + +// tipos de votacion: el servidor ya guarda las opciones que se le pasen (campo +// options de /votes/create); aqui solo se ofrecen las que usan las asambleas +const VOTE_KINDS = [ + { key: 'classic', options: null }, + { key: 'traffic', options: ['GREEN', 'YELLOW', 'RED'] }, + { key: 'scale', options: ['1', '2', '3', '4', '5'] } +]; + +const voteKindPicker = (current) => { + const i18n = t(); + const cur = String(current || ''); + return div({ class: 'vote-kind' }, + label({ class: 'vote-kind-title' }, i18n.voteKindLabel || 'Type of vote'), + ...VOTE_KINDS.map((k, i) => { + const value = k.options ? k.options.join(',') : ''; + const checked = cur ? cur === value : i === 0; + return label({ class: 'vote-kind-option' }, + input(Object.assign({ type: 'radio', name: 'options', value }, checked ? { checked: 'checked' } : {})), + span({ class: 'vote-kind-name' }, i18n['voteKind_' + k.key] || k.key), + span({ class: 'vote-kind-hint' }, i18n['voteKindHint_' + k.key] || '') + ); + }) + ); +}; + +// resultados como barras, una por opcion: la tabla de 6 columnas no cabia y partia +// las palabras por silabas +const voteBars = (voteOptions, baseCounts, voteLabel) => { + const total = voteOptions.reduce((s, o) => s + (Number(baseCounts[o]) || 0), 0); + return div({ class: 'vote-bars' }, + ...voteOptions.map((opt) => { + const n = Number(baseCounts[opt]) || 0; + return div({ class: `vote-bar vote-bar-${String(opt).toLowerCase()}` }, + span({ class: 'vote-bar-label' }, voteLabel(opt)), + span({ class: 'vote-bar-count' }, String(n)), + progress({ class: 'vote-bar-meter', max: String(Math.max(total, 1)), value: String(n) }) + ); + }) + ); +}; + +// quien crea una votacion no vota (regla de votes_model.js, no se toca): se explica +const voteOwnNote = () => p({ class: 'vote-own-note' }, t().voteOwnNote || 'You created this vote, so you cannot vote in it.'); + +// ---------- tribus ---------- + +// una fila por tribu: con 12 comisiones la lista de tarjetas con la imagen generica +// eran 10 pantallas. La imagen solo sale si la tribu tiene una propia +const tribeRow = (tr, { own, imageNode }) => { + const i18n = t(); + const href = `/tribe/${encodeURIComponent(tr.id)}`; + const members = tr.memberCount != null ? tr.memberCount : (tr.members || []).length; + const facts = [ + tr.isAnonymous ? i18n.tribePrivate : i18n.tribePublic, + tr.inviteMode === 'open' ? i18n.tribeOpen : i18n.tribeStrict, + `${members} ${String(i18n.tribeMembersCount || '').toLowerCase()}` + ].filter(Boolean).join(' · '); + return a({ href, class: 'tribe-row' + (own ? ' own-content' : '') }, + tr.image && imageNode ? imageNode : iconNode('tribe-row-thumb tribe-row-icon', 'tribes'), + span({ class: 'tribe-row-text' }, + span({ class: 'tribe-row-title' }, tr.title), + span({ class: 'tribe-row-facts' }, facts), + tr.description ? span({ class: 'tribe-row-desc' }, String(tr.description).slice(0, 90)) : null + ), + span({ class: 'tribe-row-go', 'aria-hidden': 'true' }, '›') + ); +}; + +// secciones de una tribu: 25 botones a todo lo ancho eran varias pantallas antes del +// contenido. Tira de fichas con lo que mas usa una comision y el resto en "Mas" +const TRIBE_MAIN_TABS = ['activity', 'inhabitants', 'governance', 'votations', 'forum', 'chats', 'events', 'pads']; +const tribeTabs = (tribe, section, sections) => { + const i18n = t(); + const all = sections.flatMap(g => g.items); + const tab = (s) => a({ href: `/tribe/${encodeURIComponent(tribe.id)}?section=${encodeURIComponent(s.key)}`, class: section === s.key ? 'tribe-tab is-current' : 'tribe-tab', ...(section === s.key ? { 'aria-current': 'page' } : {}) }, s.label); + const main = all.filter(s => TRIBE_MAIN_TABS.includes(s.key)); + const rest = all.filter(s => !TRIBE_MAIN_TABS.includes(s.key)); + const currentRest = rest.find(s => s.key === section); + return nav({ class: 'tribe-tabs', 'aria-label': i18n.tribeSectionsLabel || 'Sections' }, + div({ class: 'tribe-tabs-strip' }, ...(currentRest ? [tab(currentRest)] : []), ...main.map(tab)), + details({ class: 'tribe-tabs-more' }, + summary(i18n.tribeSectionsMore || 'More'), + div({ class: 'tribe-tabs-grid' }, ...rest.filter(s => s !== currentRest).map(tab)) + ) + ); +}; + +// ---------- ajustes ---------- + +// primero lo que se toca (idioma, aspecto, tipo de uso, portada) y despues lo tecnico; +// se reordena por id de seccion, sin tocar las secciones +const SETTINGS_FIRST = ['update', 'language', 'ux', 'theme', 'workflows', 'home-page']; +const settingsOrder = (parts) => { + const list = parts.filter(Boolean); + const rank = (el) => { const i = SETTINGS_FIRST.indexOf(el && el.id); return i === -1 ? SETTINGS_FIRST.length : i; }; + return list.map((el, i) => ({ el, i })).sort((x, y) => rank(x.el) - rank(y.el) || x.i - y.i).map(x => x.el); +}; + +// cada idioma un boton que se aplica al tocarlo (antes desplegable y otro boton) +const LANGS = [['Español', 'es'], ['Català', 'ca'], ['Euskara', 'eu'], ['Galego', 'gl'], ['English', 'en'], ['Français', 'fr'], + ['Português', 'pt'], ['Italiano', 'it'], ['Deutsch', 'de'], ['Русский', 'ru'], ['العربية', 'ar'], ['हिन्दी', 'hi'], ['中文', 'zh']]; +// el idioma se lee al pintar: settings_view desestructura selectedLanguage al cargar y +// se queda con el valor de entonces (siempre "en") +const languagePick = () => { + const selected = require('./main_views').getLanguage(); + return form({ action: '/language', method: 'post', class: 'lang-pick' }, + ...LANGS.map(([name, code]) => button({ type: 'submit', name: 'language', value: code, class: code === selected ? 'lang-chip is-current' : 'lang-chip', lang: code }, name)) + ); +}; + +// tipo de portada (ux): cada tarjeta es un boton que guarda al tocarlo. Antes eran +// radios y un boton de guardar debajo de seis tarjetas grandes, fuera de pantalla. +// Mismos modos y mismas condiciones que settings_view y welcome_view; las rutas +// (/settings/ux, /welcome/ux) son las del dev +const uxPick = (action, cfg, withHint) => { + const i18n = t(); + const mods = (cfg && cfg.modules) || {}; + const ux = cfg && cfg.ux && cfg.ux.current; + const modes = [ + ['blocks', i18n.uxModeMenus || 'Blocks', true], + ['ainav', i18n.uxModeAINav || 'AI', mods.aiNavMod === 'on'], + ['chats', i18n.uxModeChats || 'Conversations', mods.chatsMod === 'on'], + ['feed', i18n.uxModeFeed || 'Microblogging', true], + ['phone', i18n.phoneTitle || 'Phone', mods.phoneMod !== 'off'], + ['rooms', i18n.roomsTitle || 'Rooms', mods.roomsMod !== 'off'] + ].filter(m => m[2]); + const cur = modes.some(m => m[0] === ux) ? ux : 'blocks'; + return form({ method: 'POST', action, class: 'ux-pick' }, + withHint ? p({ class: 'ux-pick-hint' }, i18n.uxPickHint || 'Tap one and it is saved. You can change it any time in Settings.') : null, + div({ class: 'ux-pick-grid' }, + ...modes.map(([value, title]) => button(Object.assign({ type: 'submit', name: 'ux', value, class: value === cur ? 'ux-pick-card is-current' : 'ux-pick-card' }, value === cur ? { 'aria-current': 'true' } : {}), + img({ src: `/assets/images/ux-${value}.png`, class: 'ux-pick-shot', alt: '' }), + span({ class: 'ux-pick-label' }, title), + value === cur ? span({ class: 'ux-pick-now' }, i18n.uxPickCurrent || 'In use') : null + )) + ) + ); +}; + +// perfil de uso (workflow): igual, un boton por opcion con una linea de para que +// sirve. Activismo y Acampada primero; el resto en el orden del dev +const WORKFLOW_FIRST = ['activists', 'acampada']; +const workflowPick = (action, current, keys, withHint) => { + const i18n = t(); + const list = WORKFLOW_FIRST.filter(k => keys.includes(k)).concat(keys.filter(k => !WORKFLOW_FIRST.includes(k))); + const name = (k) => k === 'full' ? (i18n.welcomeWorkflow_full || k) : (i18n[`workflow_${k}`] || k); + return form({ method: 'POST', action, class: 'ux-pick wf-pick' }, + withHint ? p({ class: 'ux-pick-hint' }, i18n.uxPickHint || 'Tap one and it is saved. You can change it any time in Settings.') : null, + div({ class: 'ux-pick-grid' }, + ...list.map(k => button(Object.assign({ type: 'submit', name: 'workflow', value: k, class: k === current ? 'ux-pick-card is-current' : 'ux-pick-card' }, k === current ? { 'aria-current': 'true' } : {}), + span({ class: 'ux-pick-text' }, + span({ class: 'ux-pick-label' }, name(k)), + i18n[`wfHint_${k}`] ? span({ class: 'ux-pick-sub' }, i18n[`wfHint_${k}`]) : null), + k === current ? span({ class: 'ux-pick-now' }, i18n.uxPickCurrent || 'In use') : null + )) + ) + ); +}; + +// pasos de la bienvenida en el movil: el backend del dev quita el de uso en el movil; +// aqui vuelve, el segundo, justo despues de la identidad (/welcome/workflow es suyo) +const welcomeSteps = (usable) => { + if (usable.includes('workflow')) return usable; + const out = usable.slice(); + const at = out.indexOf('profile'); + out.splice(at === -1 ? 0 : at + 1, 0, 'workflow'); + return out; +}; + +// ---------- acciones de tarjeta ---------- + +// borrar pide un segundo toque: la X abre "¿Borrar? Si, borrar" y solo ese boton +// envia. Sin JS, con
. hidden: los que lleve el formulario +const deleteConfirm = (action, hidden) => { + const i18n = t(); + return details({ class: 'content-action-confirm' }, + summary({ class: 'btn-singleview btn-delete', title: i18n.chatDelete, 'aria-label': i18n.chatDelete }, require('./main_views').renderIcon('close')), + div({ class: 'confirm-bar' }, + span({ class: 'confirm-question' }, i18n.deleteConfirmQuestion || 'Delete it for good?'), + form({ method: 'POST', action, class: 'content-action-form' }, hidden || null, + button({ type: 'submit', class: 'confirm-yes' }, i18n.deleteConfirmYes || 'Yes, delete')) + ) + ); +}; + +// ---------- pie y aviso de bienvenida ---------- + +// una linea: con cuanta gente estas sincronizando, dicho en claro, y la version. La +// clave, HcT/HcH y la licencia estan en Perfil, Estadisticas y Ajustes +const footerLine = (syncedPeers, version) => { + const i18n = t(); + const n = Number(syncedPeers) || 0; + const text = n > 0 + ? String(i18n.footerPeersSome || '{n} peers in sync').replace('{n}', String(n)) + : (i18n.footerPeersNone || 'Not syncing with anyone yet'); + return div({ class: 'oasis-footer oasis-footer-mobile' }, + a({ href: '/peers', class: n > 0 ? 'footer-sync is-on' : 'footer-sync' }, + span({ class: 'footer-sync-dot', 'aria-hidden': 'true' }), + span(text) + ), + span({ class: 'footer-version' }, `Oasis ${version} · GPLv3`) + ); +}; + +// el aviso de "pon tu avatar" solo en la portada (en cada pagina se comia una quinta +// parte de la pantalla) y diciendo para que es +const welcomeBannerHere = (here) => ['', '/', '/activity'].includes(here); +const welcomeBannerText = () => t().welcomeBannerTextMobile || t().welcomeBannerText; +const welcomeBannerAction = () => t().welcomeBannerActionMobile || t().welcomeBannerAction; + +module.exports = { + isMobile, VOTE_KINDS, voteKindPicker, voteBars, voteOwnNote, tribeRow, tribeTabs, + settingsOrder, languagePick, uxPick, workflowPick, welcomeSteps, deleteConfirm, footerLine, + welcomeBannerHere, welcomeBannerText, welcomeBannerAction +}; diff --git a/src/views/modules_view.js b/src/views/modules_view.js index 03be08b..c558559 100644 --- a/src/views/modules_view.js +++ b/src/views/modules_view.js @@ -1,6 +1,7 @@ const { form, button, div, h2, p, section, table, tr, td, input, a, br, ul, li, span, strong } = require("../server/node_modules/hyperaxe"); const { template, i18n } = require('./main_views'); const { getConfig } = require('../configs/config-manager.js'); +const { PRESETS: PRESET_LIST } = require('../models/workflows_model'); const modulesView = () => { const config = getConfig().modules; @@ -8,6 +9,13 @@ const modulesView = () => { { name: 'agenda', label: i18n.modulesAgendaLabel, description: i18n.modulesAgendaDescription }, { name: 'ai', label: i18n.modulesAILabel, description: i18n.modulesAIDescription }, { name: 'aiNav', label: i18n.modulesAINavLabel, description: i18n.modulesAINavDescription }, + { name: 'emergencies', label: i18n.modulesEmergenciesLabel, description: i18n.modulesEmergenciesDescription }, + { name: 'mailing', label: i18n.modulesMailingLabel, description: i18n.modulesMailingDescription }, + { name: 'rooms', label: i18n.modulesRoomsLabel, description: i18n.modulesRoomsDescription }, + { name: 'phone', label: i18n.modulesPhoneLabel, description: i18n.modulesPhoneDescription }, + { name: 'logistics', label: i18n.modulesLogisticsLabel, description: i18n.modulesLogisticsDescription }, + { name: 'podcasts', label: i18n.modulesPodcastsLabel, description: i18n.modulesPodcastsDescription }, + { name: 'campaigns', label: i18n.modulesCampaignsLabel, description: i18n.modulesCampaignsDescription }, { name: 'audios', label: i18n.modulesAudiosLabel, description: i18n.modulesAudiosDescription }, { name: 'banking', label: i18n.modulesBankingLabel, description: i18n.modulesBankingDescription }, { name: 'bookmarks', label: i18n.modulesBookmarksLabel, description: i18n.modulesBookmarksDescription }, @@ -15,87 +23,89 @@ const modulesView = () => { { name: 'chats', label: i18n.modulesChatsLabel, description: i18n.modulesChatsDescription }, { name: 'cipher', label: i18n.modulesCipherLabel, description: i18n.modulesCipherDescription }, { name: 'courts', label: i18n.modulesCourtsLabel, description: i18n.modulesCourtsDescription }, + { name: 'dev', label: i18n.modulesDevLabel, description: i18n.modulesDevDescription }, { name: 'docs', label: i18n.modulesDocsLabel, description: i18n.modulesDocsDescription }, { name: 'events', label: i18n.modulesEventsLabel, description: i18n.modulesEventsDescription }, { name: 'favorites', label: i18n.modulesFavoritesLabel, description: i18n.modulesFavoritesDescription }, + { name: 'blogs', label: i18n.modulesBlogsLabel, description: i18n.modulesBlogsDescription }, { name: 'fediverse', label: i18n.modulesFediverseLabel, description: i18n.modulesFediverseDescription }, { name: 'feed', label: i18n.modulesFeedLabel, description: i18n.modulesFeedDescription }, + { name: 'files', label: i18n.modulesFilesLabel, description: i18n.modulesFilesDescription }, { name: 'forum', label: i18n.modulesForumLabel, description: i18n.modulesForumDescription }, { name: 'games', label: i18n.modulesGamesLabel, description: i18n.modulesGamesDescription }, { name: 'graphos', label: i18n.modulesGraphosLabel, description: i18n.modulesGraphosDescription }, + { name: 'housing', label: i18n.modulesHousingLabel, description: i18n.modulesHousingDescription }, { name: 'images', label: i18n.modulesImagesLabel, description: i18n.modulesImagesDescription }, + { name: 'industry', label: i18n.modulesIndustryLabel, description: i18n.modulesIndustryDescription }, { name: 'invites', label: i18n.modulesInvitesLabel, description: i18n.modulesInvitesDescription }, { name: 'jobs', label: i18n.modulesJobsLabel, description: i18n.modulesJobsDescription }, - { name: 'legacy', label: i18n.modulesLegacyLabel, description: i18n.modulesLegacyDescription }, - { name: 'latest', label: i18n.modulesLatestLabel, description: i18n.modulesLatestDescription }, + { name: 'larp', label: i18n.modulesLarpLabel, description: i18n.modulesLarpDescription }, + { name: 'backup', label: i18n.modulesBackupLabel, description: i18n.modulesBackupDescription }, { name: 'logs', label: i18n.modulesLogsLabel, description: i18n.modulesLogsDescription }, { name: 'maps', label: i18n.modulesMapLabel, description: i18n.modulesMapDescription }, { name: 'market', label: i18n.modulesMarketLabel, description: i18n.modulesMarketDescription }, { name: 'melody', label: i18n.modulesMelodyLabel, description: i18n.modulesMelodyDescription }, - { name: 'multiverse', label: i18n.modulesMultiverseLabel, description: i18n.modulesMultiverseDescription }, { name: 'opinions', label: i18n.modulesOpinionsLabel, description: i18n.modulesOpinionsDescription }, { name: 'pads', label: i18n.modulesPadsLabel, description: i18n.modulesPadsDescription }, { name: 'parliament', label: i18n.modulesParliamentLabel, description: i18n.modulesParliamentDescription }, { name: 'pixelia', label: i18n.modulesPixeliaLabel, description: i18n.modulesPixeliaDescription }, + { name: 'polls', label: i18n.modulesPollsLabel, description: i18n.modulesPollsDescription }, { name: 'projects', label: i18n.modulesProjectsLabel, description: i18n.modulesProjectsDescription }, - { name: 'popular', label: i18n.modulesPopularLabel, description: i18n.modulesPopularDescription }, { name: 'reports', label: i18n.modulesReportsLabel, description: i18n.modulesReportsDescription }, + { name: 'school', label: i18n.modulesSchoolLabel, description: i18n.modulesSchoolDescription }, { name: 'shops', label: i18n.modulesShopsLabel, description: i18n.modulesShopsDescription }, - { name: 'summaries', label: i18n.modulesSummariesLabel, description: i18n.modulesSummariesDescription }, { name: 'tags', label: i18n.modulesTagsLabel, description: i18n.modulesTagsDescription }, { name: 'tasks', label: i18n.modulesTasksLabel, description: i18n.modulesTasksDescription }, - { name: 'threads', label: i18n.modulesThreadsLabel, description: i18n.modulesThreadsDescription }, { name: 'torrents', label: i18n.modulesTorrentsLabel, description: i18n.modulesTorrentsDescription }, { name: 'transfers', label: i18n.modulesTransfersLabel, description: i18n.modulesTransfersDescription }, { name: 'trending', label: i18n.modulesTrendingLabel, description: i18n.modulesTrendingDescription }, { name: 'tribes', label: i18n.modulesTribesLabel, description: i18n.modulesTribesDescription }, - { name: 'larp', label: i18n.modulesLarpLabel, description: i18n.modulesLarpDescription }, { name: 'videos', label: i18n.modulesVideosLabel, description: i18n.modulesVideosDescription }, { name: 'votes', label: i18n.modulesVotationsLabel, description: i18n.modulesVotationsDescription }, { name: 'wallet', label: i18n.modulesWalletLabel, description: i18n.modulesWalletDescription }, - { name: 'topics', label: i18n.modulesTopicsLabel, description: i18n.modulesTopicsDescription } - ]; + { name: 'wiki', label: i18n.modulesWikiLabel, description: i18n.modulesWikiDescription } + ].sort((x, y) => String(x.label || x.name).localeCompare(String(y.label || y.name), undefined, { sensitivity: 'base' })); - const moduleStates = modules.reduce((acc, mod) => { + const MOBILE_UNAVAILABLE = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia']; + const isMobile = process.env.OASIS_MOBILE === '1'; + const visibleModules = isMobile ? modules.filter(mod => !MOBILE_UNAVAILABLE.includes(mod.name)) : modules; + + const moduleStates = visibleModules.reduce((acc, mod) => { acc[`${mod.name}Mod`] = config[`${mod.name}Mod`] === 'on' ? 'on' : 'off'; return acc; }, {}); - const activeModulesCount = modules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length; - const totalModulesCount = modules.length; + const activeModulesCount = visibleModules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length; + const totalModulesCount = visibleModules.length; const disabledModulesCount = totalModulesCount - activeModulesCount; - const header = div({ class: 'tags-header' }, + const header = [div({ class: 'tags-header module-header-line' }, h2(i18n.modulesViewTitle), - p(i18n.modulesViewDescription), - div({ style: 'margin-bottom:16px;' }, - `${i18n.modulesTotalModulesLabel}: `, - strong({ style: 'color:#888;' }, totalModulesCount), - ul({ style: 'list-style-type:none; padding:0; margin:0;' }, - li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, - `${i18n.modulesEnabledModulesLabel}: `, - span({ style: 'color:#888;' }, activeModulesCount) - ), - li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, - `${i18n.modulesDisabledModulesLabel}: `, - span({ style: 'color:#888;' }, disabledModulesCount) - ) + p(i18n.modulesViewDescription) + )]; + + const counts = div({ class: 'modules-counts' }, + span({ class: 'modules-count-item' }, + `${i18n.modulesTotalModulesLabel}: `, + strong({ class: 'modules-count-value' }, totalModulesCount) + ), + span({ class: 'modules-count-item' }, + `${i18n.modulesEnabledModulesLabel}: `, + strong({ class: 'modules-count-value' }, activeModulesCount) + ), + span({ class: 'modules-count-item' }, + `${i18n.modulesDisabledModulesLabel}: `, + strong({ class: 'modules-count-value' }, disabledModulesCount) ) - ) - ); + ); - const PRESETS = { - minimal: ['feed', 'forum', 'games', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'popular', 'latest', 'threads', 'opinions', 'cipher', 'legacy'], - social: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'multiverse', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes'], - economy: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'multiverse', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'jobs', 'shops'], - full: modules.map(m => m.name) - }; + const PRESETS = PRESET_LIST; - const presetButtons = div({ class: 'preset-group', style: 'display:flex;gap:8px;flex-wrap:nowrap;margin-bottom:16px;' }, + const presetButtons = div({ class: 'preset-group' }, Object.entries(PRESETS).map(([key, mods]) => { const presetLabel = (i18n[`modulesPreset_${key}`] || key).toUpperCase(); - const isActive = modules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on')); - return form({ action: "/modules/preset", method: "post", style: "display:inline;margin:0;" }, + const isActive = visibleModules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on')); + return form({ action: "/modules/preset", method: "post", class: "modules-preset-form" }, input({ type: "hidden", name: "preset", value: key }), button({ type: 'submit', @@ -109,8 +119,8 @@ const modulesView = () => { i18n.modules, section(header), section( - h2(i18n.modulesPresetTitle || "Common Configurations"), - presetButtons, + ...(isMobile ? [] : [h2(i18n.modulesPresetTitle || "Common Configurations"), presetButtons]), + counts, form( { action: "/save-modules", method: "post" }, table( @@ -118,9 +128,9 @@ const modulesView = () => { tr( td(i18n.modulesModuleName), td(i18n.modulesModuleDescription), - td({ style: 'text-align: center;' }, i18n.modulesModuleStatus) + td({ class: 'module-status-cell' }, i18n.modulesModuleStatus) ), - modules.map(mod => + visibleModules.map(mod => tr( td(a({ href: `/${mod.name}` }, mod.label)), td(p(mod.description)), @@ -138,7 +148,7 @@ const modulesView = () => { ) ), div( - { class: "save-button-container", style: "margin-top: 20px; text-align: center;" }, + { class: "save-button-container" }, button({ type: "submit", class: "submit-button" }, i18n.saveSettings) ) ) diff --git a/src/views/opinions_view.js b/src/views/opinions_view.js index c742fd0..cb020b5 100644 --- a/src/views/opinions_view.js +++ b/src/views/opinions_view.js @@ -1,12 +1,13 @@ const { div, h2, p, section, button, form, a, img, video: videoHyperaxe, audio: audioHyperaxe, input, table, tr, th, td, br, span, details, summary } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderSpreadButton, renderContentActions } = require('./main_views'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLink, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require('./main_views'); +const { renderZoomableImage } = require('./gallery_view'); const { config } = require('../server/SSB_server.js'); -const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText, renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); const opinionCategories = require('../backend/opinion_categories'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); -const seenDocumentTitles = new Set(); const detailHref = (type, key) => { switch (type) { @@ -15,30 +16,152 @@ const detailHref = (type, key) => { case 'video': return `/videos/${encodeURIComponent(key)}`; case 'audio': return `/audios/${encodeURIComponent(key)}`; case 'torrent': return `/torrents/${encodeURIComponent(key)}`; + case 'file': return `/files/${encodeURIComponent(key)}`; case 'document': return `/documents/${encodeURIComponent(key)}`; case 'feed': return `/feed/${encodeURIComponent(key)}`; + case 'post': return `/blogs/${encodeURIComponent(key)}`; case 'votes': return `/votes/${encodeURIComponent(key)}`; case 'transfer': return `/transfers/${encodeURIComponent(key)}`; + case 'industry': return `/industry/${encodeURIComponent(key)}`; + case 'project': return `/projects/${encodeURIComponent(key)}`; + case 'report': return `/reports/${encodeURIComponent(key)}`; + case 'task': return `/tasks/${encodeURIComponent(key)}`; + case 'event': return `/events/${encodeURIComponent(key)}`; + case 'shopProduct': return `/shops/product/${encodeURIComponent(key)}`; + case 'housing': return `/housing/${encodeURIComponent(key)}`; + case 'market': return `/market/${encodeURIComponent(key)}`; + case 'schoolCourse': return `/school/course/${encodeURIComponent(key)}`; + case 'podcast': return `/podcasts/${encodeURIComponent(key)}`; + case 'podcastEpisode': return `/podcasts/episode/${encodeURIComponent(key)}`; + case 'campaign': return `/campaigns/${encodeURIComponent(key)}`; + case 'logisticsRoute': return `/logistics/${encodeURIComponent(key)}`; default: return null; } }; const renderContentHtml = (content, key) => { switch (content.type) { + case 'industry': + return div({ class: 'opinion-industry' }, + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryName + ':'), + span({ class: 'card-value' }, content.name || '') + ), + content.sector ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industrySector + ':'), + span({ class: 'card-value' }, String(content.sector).toUpperCase()) + ) : "", + content.membershipPolicy ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryMembershipPolicy + ':'), + span({ class: 'card-value' }, String(i18n['industryPolicy_' + content.membershipPolicy] || content.membershipPolicy).toUpperCase()) + ) : "", + content.description ? p(...renderStyledText(content.description)) : null, + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + case 'industryBlueprint': + return div({ class: 'opinion-industry' }, + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryBlueprint + ':'), + span({ class: 'card-value' }, content.name || '') + ), + content.outKind ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryOutputKind + ':'), + span({ class: 'card-value' }, String(i18n['industryKind_' + content.outKind] || content.outKind).toUpperCase()) + ) : "", + content.laborHours ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryLaborHours + ':'), + span({ class: 'card-value' }, String(content.laborHours)) + ) : "", + content.description ? p(...renderStyledText(content.description)) : null + ) + ); + case 'housing': { + const free = String(content.housing_type || '').toLowerCase() === 'couchsurfing'; + return div({ class: 'opinion-housing' }, + div({ class: 'card-section housing' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, content.title || '') + ), + content.housing_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingType + ':'), + span({ class: 'card-value' }, String(i18n['housingType' + String(content.housing_type).toUpperCase()] || content.housing_type).toUpperCase()) + ) : null, + content.property_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingProperty + ':'), + span({ class: 'card-value' }, String(i18n['housingProperty' + String(content.property_type).toUpperCase()] || content.property_type).toUpperCase()) + ) : null, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPrice + ':'), + span({ class: 'card-value' }, free ? (i18n.housingFree || 'FREE') : `${Number(content.price || 0).toFixed(2)} ECO`) + ), + content.place ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPlace + ':'), + span({ class: 'card-value' }, content.place) + ) : null, + Number(content.rooms) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingRooms + ':'), + span({ class: 'card-value' }, String(content.rooms)) + ) : null, + Number(content.size) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingSize + ':'), + span({ class: 'card-value' }, `${content.size} m²`) + ) : null, + Number(content.capacity) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingCapacity + ':'), + span({ class: 'card-value' }, String(content.capacity)) + ) : null, + content.description ? p(...renderStyledText(content.description)) : null, + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + } + case 'market': + return div({ class: 'opinion-market' }, + div({ class: 'card-section market' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, content.title || '') + ), + content.item_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.marketItemType + ':'), + span({ class: 'card-value' }, String(content.item_type).toUpperCase()) + ) : null, + content.item_status ? div({ class: 'card-field' }, + span({ class: 'card-label' }, (i18n.marketItemCondition || i18n.status) + ':'), + span({ class: 'card-value' }, String(content.item_status).toUpperCase()) + ) : null, + div({ class: 'price-chip' }, `${content.price} ECO`), + content.description ? p(...renderStyledText(content.description)) : null, + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); case 'bookmark': return div({ class: 'opinion-bookmark' }, div({ class: 'card-section bookmark' }, h2(content.url ? div({ class: 'card-field' }, - span({ class: 'card-label' }, p(a({ href: content.url, target: '_blank', class: "bookmark-url" }, content.url))) + span({ class: 'card-label' }, p(a({ href: safeExternalHref(content.url), target: '_blank', rel: 'noopener noreferrer', class: "bookmark-url" }, content.url))) ) : ""), content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisitLabel + ':'), - span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString()) + span({ class: 'card-value' }, moment(content.lastVisit).format("YYYY/MM/DD HH:mm")) ) : "", content.description ? [ span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null ) @@ -53,16 +176,12 @@ const renderContentHtml = (content, key) => { content.description ? [ span({ class: 'card-label' }, i18n.imageDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null, - content.meme ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.trendingCategory + ':'), - span({ class: 'card-value' }, i18n.meme) - ) : "", br(), - div({ class: 'card-field' }, - img({ src: `/blob/${encodeURIComponent(content.url)}`, class: 'feed-image' }) + div({ class: 'card-field image-container' }, + renderZoomableImage(`/blob/${encodeURIComponent(content.url)}`, { imgClass: 'post-image' }) ) ) ); @@ -76,16 +195,16 @@ const renderContentHtml = (content, key) => { content.description ? [ span({ class: 'card-label' }, i18n.videoDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null, div({ class: 'card-field' }, videoHyperaxe({ controls: true, + class: 'post-video', src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', - width: '640', - height: '360' + preload: 'metadata' }) ) ) @@ -100,12 +219,13 @@ const renderContentHtml = (content, key) => { content.description ? [ span({ class: 'card-label' }, i18n.audioDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null, - div({ class: 'card-field' }, + div({ class: 'card-field audio-container' }, audioHyperaxe({ controls: true, + class: 'post-audio', src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType, preload: 'metadata' @@ -113,6 +233,12 @@ const renderContentHtml = (content, key) => { ) ) ); + case 'file': + return div({ class: 'opinion-file' }, + div({ class: 'card-section' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.fileTitleLabel || 'Title') + ':'), span({ class: 'card-value' }, content.title)) : "" + ) + ); case 'torrent': return div({ class: 'opinion-torrent' }, div({ class: 'card-section' }, @@ -121,8 +247,6 @@ const renderContentHtml = (content, key) => { ); case 'document': { const t = content.title?.trim(); - if (t && seenDocumentTitles.has(t)) return null; - if (t) seenDocumentTitles.add(t); return div({ class: 'opinion-document' }, div({ class: 'card-section document' }, t ? div({ class: 'card-field' }, @@ -132,7 +256,7 @@ const renderContentHtml = (content, key) => { content.description ? [ span({ class: 'card-label' }, i18n.documentDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null, div({ class: 'card-field' }, @@ -141,10 +265,20 @@ const renderContentHtml = (content, key) => { ) ); } + case 'post': + return div({ class: 'opinion-blog' }, + div({ class: 'card-section blog' }, + content.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href: `/blogs/${encodeURIComponent(key)}` }, content.contentWarning))) : "", + div({ class: 'blog-card-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(content.text || '').trim().length > 420 ? String(content.text).trim().slice(0, 420).replace(/\s+$/, '') + '…' : String(content.text || '').trim())) }), + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); case 'feed': return div({ class: 'opinion-feed' }, div({ class: 'card-section feed' }, - div({ class: 'feed-text', innerHTML: sanitizeHtml(renderTextWithStyles(content.text)) }), + div({ class: 'feed-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(content.text)) }), content.refeeds ? h2({ class: 'card-field' }, span({ class: 'card-label' }, `${i18n.tribeFeedRefeeds}: `), span({ class: 'card-value' }, content.refeeds)) : "" @@ -162,7 +296,7 @@ const renderContentHtml = (content, key) => { ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':'), - span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '') + span({ class: 'card-value' }, content.deadline ? moment(content.deadline).format("YYYY/MM/DD HH:mm") : '') ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteTotalVotes + ':'), @@ -184,7 +318,7 @@ const renderContentHtml = (content, key) => { ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), - span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '') + span({ class: 'card-value' }, content.deadline ? moment(content.deadline).format("YYYY/MM/DD HH:mm") : '') ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), @@ -198,7 +332,7 @@ const renderContentHtml = (content, key) => { span({ class: 'card-label' }, i18n.from + ':'), span({ class: 'card-value' }, userLink(content.from)) ), - div({ class: 'card-field' }, + process.env.OASIS_MOBILE === '1' ? null : div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.to + ':'), span({ class: 'card-value' }, userLink(content.to)) ), @@ -212,15 +346,19 @@ const renderContentHtml = (content, key) => { return div({ class: 'styled-text' }, div({ class: 'card-section styled-text-content' }, div({ class: 'card-field' }, - span({ class: 'card-value', innerHTML: sanitizeHtml(content.text || content.description || content.title || '[no content]') }) + span({ class: 'card-value', [RAW_HTML]: sanitizeHtml(renderStyledHtml(content.title || content.name || content.text || content.description || '[no content]')) }) ) ) ); } }; -exports.opinionsView = (items, filter, spreadMap = new Map()) => { - seenDocumentTitles.clear(); +exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = null, params = {}) => { + const presentCats = new Set(); + for (const it of (Array.isArray(allItems) ? allItems : items)) { + const ops = (it.value?.content || {}).opinions || {}; + for (const [cat, n] of Object.entries(ops)) if (Number(n) > 0) presentCats.add(cat); + } items = items .filter(item => { const c = item.value?.content || item.content; @@ -236,9 +374,19 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { }); const title = i18n.opinionsTitle; - const baseFilters = ['TOP', 'ALL', 'MINE', 'RECENT']; + const baseFilters = ['ALL', 'RECENT', 'MINE', 'TOP']; + const emptyOps = (Array.isArray(allItems) ? allItems : items).length === 0 && !String(q || '').trim(); - const cards = items + const seenDocs = new Set(); + const shownItems = items.filter(item => { + const c = item.value.content || {}; + const t = c.type === 'document' ? String(c.title || '').trim() : ''; + if (!t) return true; + if (seenDocs.has(t)) return false; + seenDocs.add(t); + return true; + }); + const cards = paged(shownItems) .map(item => { const c = item.value.content; const key = item.key; @@ -247,7 +395,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { const voteEntries = Object.entries(c.opinions || {}); const total = voteEntries.reduce((sum, [, v]) => sum + v, 0); const voted = c.opinions_inhabitants?.includes(config.keys.id); - const created = new Date(item.value.timestamp).toLocaleString(); + const created = moment(item.value.timestamp).format("YYYY/MM/DD HH:mm"); const allCats = opinionCategories; const isOwn = item.value.author && String(item.value.author) === String(config.keys.id); @@ -255,40 +403,33 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { { class: 'trending-card opinions-card' + (isOwn ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(c.type || '').toUpperCase()) + span({ class: 'pm-exposition-text' }, String(c.type === 'post' ? (i18n.typePost || 'BLOG') : (c.type || '')).toUpperCase()) ), - renderContentActions(key, detailHref(c.type, key)) + renderContentActions(key, detailHref(c.type, key), { + spread: spreadMap.get(key) || null, + author: item.value.author, + reportTitle: c.title || c.question || c.concept || c.name || c.contentWarning || '', + ...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&') + ? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } } + : {}), + ...(CONTENT_FAV_KIND[c.type] + ? { favKind: CONTENT_FAV_KIND[c.type], isFavorite: params.favIndex instanceof Map ? params.favIndex.get(String(key)) === CONTENT_FAV_KIND[c.type] : false, returnTo: `/opinions?filter=${encodeURIComponent(filter)}` } + : {}) + }) ), div( { class: 'card-section opinions-card-body' }, - contentHtml, - div({ class: 'card-spread-left' }, renderSpreadButton(key, spreadMap.get(key))), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${created} ${i18n.performed} `), - userLink(item.value.author) - ), - (() => { - const entries = voteEntries.filter(([, v]) => v > 0); - const dominantPart = (() => { - if (!entries.length) return null; - const maxVal = Math.max(...entries.map(([, v]) => v)); - const dominant = entries.filter(([, v]) => v === maxVal).map(([k]) => i18n['vote' + k.charAt(0).toUpperCase() + k.slice(1)] || k); - return [ - span({ class: 'trending-dominant-sep' }, '|'), - span({ class: 'trending-dominant-text' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`) - ]; - })(); - return h2( - `${i18n.totalOpinions || i18n.opinionsTotalCount}: `, - span({ class: 'trending-total-count' }, String(total)), - ...(dominantPart || []) - ); - })(), + contentHtml + ), + renderVotesSummary(c.opinions), + renderCardMetaRow( details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`), + summary({ class: 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`)), div({ class: 'voting-buttons' }, allCats.map(cat => { - const label = `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat} [${c.opinions?.[cat] || 0}]`; + const label = `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${c.opinions?.[cat] || 0}]`; if (voted) { return button({ class: 'vote-btn', type: 'button' }, label); } @@ -297,6 +438,10 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { ); }) ) + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${created}`), + userLink(item.value.author) ) ) ); @@ -304,7 +449,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { .filter(Boolean); const hasDocuments = items.some(item => item.value.content?.type === 'document'); - const header = div({ class: 'tags-header' }, + const header = div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.shareYourOpinions) ); @@ -313,71 +458,80 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { title, section( header, - div({ class: 'mode-buttons' }, + emptyOps ? null : div({ class: 'mode-buttons' }, div({ class: 'column' }, baseFilters.map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) ) ) ), div({ class: 'column' }, - opinionCategories.positive.slice(0, 5).map(mode => + opinionCategories.positive.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) ) ) ), div({ class: 'column' }, - opinionCategories.positive.slice(5, 10).map(mode => + opinionCategories.positive.slice(5, 10).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) ) ) ), div({ class: 'column' }, - opinionCategories.positive.slice(10, 15).map(mode => + opinionCategories.positive.slice(10, 15).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + ) + ) + ) + , + div({ class: 'column' }, + opinionCategories.constructive.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => + form({ method: 'GET', action: '/opinions' }, + input({ type: 'hidden', name: 'filter', value: mode }), + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + ) + ) + ), + div({ class: 'column' }, + opinionCategories.constructive.slice(5, 11).filter(mode => presentCats.has(mode)).map(mode => + form({ method: 'GET', action: '/opinions' }, + input({ type: 'hidden', name: 'filter', value: mode }), + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + ) + ) + ), + div({ class: 'column' }, + opinionCategories.moderation.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => + form({ method: 'GET', action: '/opinions' }, + input({ type: 'hidden', name: 'filter', value: mode }), + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + ) + ) + ), + div({ class: 'column' }, + opinionCategories.moderation.slice(5, 10).filter(mode => presentCats.has(mode)).map(mode => + form({ method: 'GET', action: '/opinions' }, + input({ type: 'hidden', name: 'filter', value: mode }), + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) ) ) ) ), - div({ class: 'mode-buttons' }, - div({ class: 'column' }, - opinionCategories.constructive.slice(0, 5).map(mode => - form({ method: 'GET', action: '/opinions' }, - input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) - ) - ) - ), - div({ class: 'column' }, - opinionCategories.constructive.slice(5, 11).map(mode => - form({ method: 'GET', action: '/opinions' }, - input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) - ) - ) - ), - div({ class: 'column' }, - opinionCategories.moderation.slice(0, 5).map(mode => - form({ method: 'GET', action: '/opinions' }, - input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) - ) - ) - ), - div({ class: 'column' }, - opinionCategories.moderation.slice(5, 10).map(mode => - form({ method: 'GET', action: '/opinions' }, - input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) - ) + emptyOps ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(shownItems.length), + form({ method: 'GET', action: '/opinions', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: q, placeholder: i18n.opinionsSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) ) ) ), @@ -390,8 +544,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { ); return `${html}${hasDocuments - ? ` - ` + ? `` : ''}`; }; diff --git a/src/views/pads_view.js b/src/views/pads_view.js index 25c9ec6..26eae79 100644 --- a/src/views/pads_view.js +++ b/src/views/pads_view.js @@ -1,5 +1,5 @@ -const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views") +const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td, details, summary } = require("../server/node_modules/hyperaxe") +const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderContentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views") const { renderEncryptedChip } = require("./clearnet_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") @@ -82,15 +82,17 @@ const renderPadStatusChip = (status, isClosed) => { return renderStateChip(variant, icon, label) } -const renderModeButtons = (currentFilter) => +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => div({ class: "tribe-mode-buttons" }, - ["all", "mine", "recent", "open", "closed"].map(f => + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "open", "closed"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => form({ method: "GET", action: "/pads" }, input({ type: "hidden", name: "filter", value: f }), button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`padFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) ) ), + ]), form({ method: "GET", action: "/pads" }, input({ type: "hidden", name: "filter", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.padCreate || "Create Pad") @@ -98,13 +100,20 @@ const renderModeButtons = (currentFilter) => ) -const renderPadCard = (pad, filter, spreadInfo) => { +const renderPadCard = exports.renderPadCard = (pad, filter, spreadInfo) => { const chips = [ renderPadStatusChip(pad.status, pad.isClosed), renderEncryptedChip(i18n), - renderLifespanChip(pad.lifetime, i18n) + renderLifespanChip(pad.lifetime, i18n), + pad.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (pad.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ].filter(Boolean) return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(pad.rootId || pad.key, `/pads/${encodeURIComponent(pad.rootId)}`, { spread: spreadInfo || null, author: pad.author, favKind: 'pads', isFavorite: pad.isFavorite, reportTitle: pad.title, deleteAction: String(pad.author) === String(userId) ? contentDeleteAction('pad', pad.rootId || pad.key) : undefined }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, @@ -113,17 +122,10 @@ const renderPadCard = (pad, filter, spreadInfo) => { ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, pad.deadline - ? p({ class: "job-meta-line" }, `${i18n.padDeadlineLabel || "Deadline"}: ${moment(pad.deadline).format("YYYY-MM-DD HH:mm")}`) + ? p({ class: "job-meta-line" }, `${i18n.padDeadlineLabel || "Deadline"}: ${moment(pad.deadline).format("YYYY/MM/DD HH:mm")}`) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`) - ), - (() => { - const btn = renderSpreadButton(pad.rootId, spreadInfo); - return btn ? div({ class: "card-spread-left" }, btn) : null; - })(), - div({ class: "visit-btn-centered" }, - a({ href: `/pads/${encodeURIComponent(pad.rootId)}`, class: "filter-btn" }, i18n.padVisitPad || "Visit Pad") ) ) ) @@ -139,9 +141,9 @@ const renderCreateForm = (padToEdit, params) => { }, tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, span(i18n.padTitleLabel || "Title"), require("../server/node_modules/hyperaxe").br(), - input({ type: "text", name: "title", value: padToEdit ? padToEdit.title : "", placeholder: i18n.padTitlePlaceholder || "Enter pad title...", required: true }), + input({ type: "text", name: "title", maxlength: "100", value: padToEdit ? padToEdit.title : "", placeholder: i18n.padTitlePlaceholder || "Enter pad title...", required: true }), require("../server/node_modules/hyperaxe").br(), require("../server/node_modules/hyperaxe").br(), - span(i18n.padStatusLabel || "Status"), require("../server/node_modules/hyperaxe").br(), + span(i18n.padTypeLabel), require("../server/node_modules/hyperaxe").br(), select({ name: "status" }, ["OPEN", "INVITE-ONLY"].map(s => option({ value: s, ...(padToEdit && padToEdit.status === s ? { selected: true } : {}) }, s) @@ -157,7 +159,7 @@ const renderCreateForm = (padToEdit, params) => { }), require("../server/node_modules/hyperaxe").br(), require("../server/node_modules/hyperaxe").br(), span(i18n.padTagsLabel || "Tags"), require("../server/node_modules/hyperaxe").br(), - input({ type: "text", name: "tags", value: padToEdit ? padToEdit.tags.join(", ") : "", placeholder: i18n.padTagsPlaceholder || "tag1, tag2, ..." }), + input({ type: "text", name: "tags", value: padToEdit ? padToEdit.tags.join(", ") : "", placeholder: i18n.padTagsPlaceholder || "Enter tags separated by commas" }), require("../server/node_modules/hyperaxe").br(), require("../server/node_modules/hyperaxe").br(), button({ type: "submit", class: "create-button" }, padToEdit ? (i18n.padUpdate || "Update Pad") : (i18n.padCreate || "Create Pad")) ) @@ -178,33 +180,28 @@ exports.renderPadInvitePage = (code) => { exports.padsView = async (pads, filter, padToEdit, params) => { const q = String((params && params.q) || "").trim() const isForm = filter === "create" || filter === "edit" - const headerMap = { - all: i18n.padAllSectionTitle || "Pads", - mine: i18n.padMineSectionTitle || "Your Pads", - recent: i18n.padRecentSectionTitle || "Recent Pads", - open: i18n.padOpenSectionTitle || "Open Pads", - closed: i18n.padClosedSectionTitle || "Closed Pads" - } - const headerText = headerMap[filter] || headerMap.all + const headerText = i18n.padsTitle + const emptyMod = moduleIsEmpty(Array.isArray(pads) ? pads : [], filter || "all", "all", q) const filteredPads = q ? pads.filter(pd => String(pd.title || "").toLowerCase().includes(q.toLowerCase())) : pads - const body = div({ class: "main-column" }, - div({ class: "tags-header" }, + const body = section( + div({ class: "tags-header module-header-line" }, h2(headerText), p(i18n.padsDescription || "Manage collaborative encrypted text editors in your network.") ), - renderModeButtons(filter), - !isForm - ? div({ class: "filters" }, - form({ method: "GET", action: "/pads" }, + renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null), + !isForm && !emptyMod + ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filteredPads, pd => pd.isClosed ? 'CLOSED' : String(pd.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.padStatusOpen }, { value: 'INVITE-ONLY', label: i18n.padStatusInviteOnly }, { value: 'CLOSED', label: i18n.padStatusClosed }]), + form({ method: "GET", action: "/pads", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", placeholder: i18n.padSearchPlaceholder || "Search pads...", value: q }), - br(), - button({ type: "submit" }, i18n.search), - br() + input({ type: "text", name: "q", placeholder: i18n.padSearchPlaceholder || "Search pads...", value: q, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) ) ) : null, @@ -213,7 +210,7 @@ exports.padsView = async (pads, filter, padToEdit, params) => { : div( filteredPads.length === 0 ? p({ class: "no-content" }, i18n.padsNoItems || "No pads found.") - : div({ class: "tribe-grid" }, ...filteredPads.map(pd => renderPadCard(pd, filter, params && params.spreadMap && params.spreadMap.get(pd.rootId)))) + : div({ class: "tribe-grid pads-grid" }, ...paged(filteredPads).map(pd => renderPadCard(pd, filter, params && params.spreadMap && params.spreadMap.get(pd.rootId)))) ) ) @@ -225,99 +222,122 @@ exports.singlePadView = async (pad, entries, params) => { const isMember = pad.members.includes(userId) || (!!pad.tribeId && !!pad.isTribeMember) const padClosed = pad.isClosed const returnTo = `/pads/${encodeURIComponent(pad.rootId)}` - const shareUrl = `/pads/${encodeURIComponent(pad.rootId)}` const isRestrictedInviteOnly = !isMember && !isAuthor && pad.status === "INVITE-ONLY" const tags = !isRestrictedInviteOnly && Array.isArray(pad.tags) && pad.tags.length > 0 - ? div({ class: "tribe-side-tags" }, ...pad.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}` }, `#${t}`))) + ? div({ class: "tribe-side-tags" }, ...pad.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) : null + const sharesPad = isAuthor || (pad.members || []).includes(userId) + const subscriptionIn = isAuthor || (pad.subscription && pad.subscription.subscribed === true) const detailChips = [ renderPadStatusChip(pad.status, padClosed), renderEncryptedChip(i18n), - renderLifespanChip(pad.lifetime, i18n) + renderLifespanChip(pad.lifetime, i18n), + sharesPad + ? renderStateChip(subscriptionIn ? "mutuals" : "closed", "✉", subscriptionIn ? i18n.subscriptionOn : i18n.subscriptionOff) + : null ].filter(Boolean) + const inviteActions = [ + isAuthor && pad.status === "INVITE-ONLY" + ? form({ method: "POST", action: `/pads/generate-invite/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) + ) + : null, + (() => { + if (!(isAuthor && pad.status === "INVITE-ONLY")) return null + const invs = Array.isArray(pad.invites) ? pad.invites : [] + const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) + if (openInvite) return [ + div({ class: "tribe-open-invite" }, + span({ class: "card-label" }, i18n.tribeInviteCodeText), + span({ class: "tribe-open-invite-code" }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/pads/${encodeURIComponent(openInvite.code)}` }) + ), + form({ method: "POST", action: `/pads/open-invite/remove/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) + ) + ] + return form({ method: "POST", action: `/pads/open-invite/create/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) + ) + })() + ].filter(Boolean) + + const topButtons = isRestrictedInviteOnly ? [] : [ + !isAuthor && !isMember && pad.status === "OPEN" && !padClosed + ? form({ method: "POST", action: `/pads/join/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "update-btn" }, i18n.padStartEditing || "EDIT") + ) + : null, + form({ method: "GET", action: `/pads/${encodeURIComponent(pad.rootId)}/pdf` }, + button({ type: "submit", class: "update-btn" }, i18n.generatePdf || "Generate PDF") + ), + isAuthor && pad.status !== "CLOSED" && !padClosed + ? form({ method: "POST", action: `/pads/close/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "update-btn" }, i18n.padClose || "Close") + ) + : null, + isAuthor + ? form({ method: "GET", action: "/pads" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: pad.rootId }), + button({ type: "submit", class: "update-btn" }, i18n.padUpdate || "Update") + ) + : null + ].filter(Boolean) + const padSide = div({ class: "tribe-side" }, + div({ class: "tribe-side-actions wiki-actions-top pad-actions-top" }, + span({ class: "wiki-actions-left" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(pad.rootId, returnTo, { + author: pad.author, + favKind: 'pads', + isFavorite: pad.isFavorite, + spread: (params && params.spreads) || null, + returnTo, + reportTitle: pad.title, + deleteAction: isAuthor ? contentDeleteAction('pad', pad.rootId) : undefined + }) + ) + ), + ...topButtons + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, pad.title || "\u2014") ), detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, - div({ class: "shop-share" }, - span({ class: "tribe-info-label" }, i18n.padShareUrl || "Share URL"), - input({ type: "text", readonly: true, value: shareUrl, class: "shop-share-input" }) - ), - (() => { - const btn = renderSpreadButton(pad.rootId); - return btn ? div({ class: "card-spread-centered" }, btn) : null; - })(), - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`) - ), - table({ class: "tribe-info-table" }, - tr(td({ class: "tribe-info-label" }, i18n.padCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(pad.createdAt).format("YYYY-MM-DD"))), - isRestrictedInviteOnly ? null : tr(td({ class: "tribe-info-value pad-author-cell", colspan: "4" }, userLink(pad.author))), - isRestrictedInviteOnly ? null : tr(td({ class: "tribe-info-label" }, i18n.padDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, pad.deadline ? moment(pad.deadline).format("YYYY-MM-DD HH:mm") : "\u2014")) - ), - isRestrictedInviteOnly ? null : div({ class: "tribe-side-actions" }, - isAuthor && pad.status === "INVITE-ONLY" - ? form({ method: "POST", action: `/pads/generate-invite/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) - ) - : null, - (() => { - if (!(isAuthor && pad.status === "INVITE-ONLY")) return null - const invs = Array.isArray(pad.invites) ? pad.invites : [] - const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) - if (openInvite) return [ - div({ class: "tribe-open-invite" }, - span({ class: "card-label" }, i18n.tribeInviteCodeText), - span({ class: "tribe-open-invite-code" }, openInvite.code) - ), - form({ method: "POST", action: `/pads/open-invite/remove/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) - ) - ] - return form({ method: "POST", action: `/pads/open-invite/create/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) - ) - })(), - form( - { method: "POST", action: pad.isFavorite ? `/pads/favorites/remove/${encodeURIComponent(pad.key)}` : `/pads/favorites/add/${encodeURIComponent(pad.key)}` }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "tribe-action-btn" }, pad.isFavorite ? (i18n.padRemoveFavorite || "Remove Favorite") : (i18n.padAddFavorite || "Add Favorite")) - ), - !isAuthor - ? a({ href: `/pm?to=${encodeURIComponent(pad.author)}`, class: "tribe-action-btn" }, "PM") - : null, - isAuthor - ? form({ method: "GET", action: "/pads" }, - input({ type: "hidden", name: "filter", value: "edit" }), - input({ type: "hidden", name: "id", value: pad.rootId }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.padUpdate || "Update") - ) - : null, - isAuthor && pad.status !== "CLOSED" && !padClosed - ? form({ method: "POST", action: `/pads/close/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.padClose || "Close Pad") - ) - : null, - isAuthor - ? form({ method: "POST", action: `/pads/delete/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.padDelete || "Delete") - ) - : null + div({ class: "pad-meta" }, + span({ class: "card-label" }, `${i18n.padCreated || "Created"}: `), + span(moment(pad.createdAt).format("YYYY/MM/DD HH:mm")), + (isRestrictedInviteOnly || !pad.deadline) ? null : span({ class: "card-label" }, `${i18n.padDeadlineLabel || "Deadline"}: `), + (isRestrictedInviteOnly || !pad.deadline) ? null : span(moment(pad.deadline).format("YYYY/MM/DD HH:mm")), + isRestrictedInviteOnly ? null : userLink(pad.author) ), + tags, + (isRestrictedInviteOnly || !inviteActions.length) ? null : div({ class: "tribe-side-actions" }, ...inviteActions), + (pad.subscription && sharesPad) + ? renderSubscriptionBox({ + target: pad.rootId || pad.key, + scope: "pads", + subscribed: pad.subscription.subscribed === true, + count: pad.subscription.count, + isOwner: isAuthor, + returnTo + }) + : null, !isAuthor && pad.status === "INVITE-ONLY" && !isMember ? div({ class: "pad-invite-section" }, - a({ class: "tribe-action-btn", href: "/invites#invites-pads" }, i18n.tribeEnterInvite) + a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite) ) : null, - !isRestrictedInviteOnly && !isAuthor && !isMember && pad.status === "OPEN" && !padClosed - ? form({ method: "POST", action: `/pads/join/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "create-button" }, i18n.padStartEditing || "START EDITING!") - ) - : null, - tags + details({ class: "pad-members-details", ...(pad.members.length <= 8 ? { open: true } : {}) }, + summary({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`), + div({ class: "pad-members-chips" }, + ...pad.members.map(m => span({ class: "pad-member-chip" }, span({ class: "pad-author-swatch " + memberColorClass(pad.members, m) }), userLink(m))) + ) + ) ) let canonicalEntries = entries @@ -329,49 +349,83 @@ exports.singlePadView = async (pad, entries, params) => { const lastEntry = canonicalEntries.length > 0 ? canonicalEntries[canonicalEntries.length - 1] : null const currentText = lastEntry ? lastEntry.text : "" - const coloredView = chunks.length > 0 - ? div({ class: "pad-readonly-colored" }, - ...chunks.map(c => - span({ class: "pad-author-span " + memberColorClass(pad.members, c.author) }, c.text) - ) + const linesOfChunks = (list) => { + const rows = [[]] + for (const c of list) { + const parts = String(c.text || "").split("\n") + parts.forEach((part, i) => { + if (i > 0) rows.push([]) + if (part) rows[rows.length - 1].push({ text: part, author: c.author }) + }) + } + return rows + } + const renderNumbered = (list, minRows = 0) => { + const rows = linesOfChunks(list) + while (rows.length < minRows) rows.push([]) + return table({ class: "pad-lines" }, + ...rows.map((segs, i) => tr({ id: `L${i + 1}` }, + td({ class: "pad-line-num" }, a({ href: `#L${i + 1}` }, String(i + 1))), + td({ class: "pad-line-text" }, segs.length + ? segs.map(sg => span({ class: "pad-author-span " + memberColorClass(pad.members, sg.author) }, sg.text)) + : (i === 0 && !list.length ? span({ class: "pad-line-placeholder" }, i18n.padNoEntries || "No entries yet.") : " ")) + )) + ) + } + const documentView = div({ class: "pad-document" }, renderNumbered(chunks, 24)) + const attributionView = chunks.length > 0 + ? details({ class: "pad-attribution" }, + summary(i18n.padAttributionView || "By author"), + div({ class: "pad-readonly-colored" }, renderNumbered(chunks)) ) - : p(i18n.padNoEntries || "No entries yet.") + : null + const lineCount = String(currentText || "").split("\n").length + const editorRows = Math.max(24, lineCount + 12) + const gutter = div({ class: "pad-gutter" }, Array.from({ length: editorRows }, (_, i) => String(i + 1)).join("\n")) const visibleEntries = entries.filter(e => e.text && String(e.text).trim()) - const versionList = visibleEntries.length > 0 - ? div({ class: "pad-version-list" }, + const selectedKey = params.selectedVersion ? params.selectedVersion.key : null + const historyPanel = visibleEntries.length > 0 + ? div({ class: "pad-history" }, h4(i18n.padVersionHistory || "Version History"), ...visibleEntries.slice().reverse().map((e, idx) => - div({ class: "pad-version-item" }, - span({ class: "pad-version-date" }, moment(e.createdAt).format("YYYY-MM-DD HH:mm")), - span({ class: "pad-version-author" }, - span({ class: "pad-author-swatch " + memberColorClass(pad.members, e.author) }), - userLink(e.author) - ), - a({ href: `/pads/${encodeURIComponent(pad.rootId)}?version=${encodeURIComponent(e.key || idx)}`, class: "pad-version-link" }, i18n.padVersionView || "View") + div({ class: "pad-history-item" }, + a({ href: `/pads/${encodeURIComponent(pad.rootId)}?version=${encodeURIComponent(e.key || idx)}`, class: "pad-version-link" + (selectedKey && e.key === selectedKey ? " active" : ""), title: i18n.padVersionView || "View" }, moment(e.createdAt).format("YYYY/MM/DD HH:mm")), + span({ class: "pad-author-swatch " + memberColorClass(pad.members, e.author) }), + userLink(e.author) ) ) ) : null + const sidePanels = [historyPanel].filter(Boolean) const editorArea = isMember && !padClosed && !params.selectedVersion - ? div({ class: "pad-editor-area" }, - coloredView, - form({ method: "POST", action: `/pads/entry/${encodeURIComponent(pad.rootId)}` }, - textarea({ name: "text", rows: "12", class: "pad-editor-white", placeholder: i18n.padEditorPlaceholder || "Start writing..." }, currentText), - button({ type: "submit", class: "create-button" }, i18n.padSubmitEntry || "Submit") - ), - versionList ? div({ class: "pad-version-section" }, versionList) : null - ) - : div({ class: "pad-editor-area" }, - params.selectedVersion - ? div({ class: "pad-viewer-back" }, - a({ href: `/pads/${encodeURIComponent(pad.rootId)}`, class: "filter-btn" }, - "\u2190 " + (i18n.padBackToEditor || "Back to editor")) + ? div({ class: "pad-workspace" }, + div({ class: "pad-workspace-main" }, + form({ method: "POST", action: `/pads/entry/${encodeURIComponent(pad.rootId)}` }, + div({ class: "pad-editor-wrap" }, + gutter, + textarea({ maxlength: "3000", name: "text", rows: String(editorRows), class: "pad-editor", placeholder: i18n.padEditorPlaceholder || "Start writing...", wrap: "off" }, currentText) + ), + div({ class: "pad-editor-bar" }, + button({ type: "submit", class: "create-button" }, i18n.padSubmitEntry || "Submit") ) - : null, - coloredView, - versionList ? div({ class: "pad-version-section" }, versionList) : null + ), + attributionView + ), + sidePanels.length ? div({ class: "pad-workspace-side" }, ...sidePanels) : null + ) + : div({ class: "pad-workspace" }, + div({ class: "pad-workspace-main" }, + params.selectedVersion + ? div({ class: "pad-viewer-back" }, + a({ href: `/pads/${encodeURIComponent(pad.rootId)}`, class: "filter-btn" }, + "\u2190 " + (i18n.padBackToEditor || "Back to editor")) + ) + : null, + documentView + ), + sidePanels.length ? div({ class: "pad-workspace-side" }, ...sidePanels) : null ) const padMain = isRestrictedInviteOnly @@ -381,12 +435,12 @@ exports.singlePadView = async (pad, entries, params) => { return template( pad.title || i18n.padsTitle || "Pad", section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.padsTitle || "Pads"), p(i18n.padsDescription || "Manage collaborative encrypted text editors in your network.") ), - renderModeButtons("all") + renderModeButtons("all", false, (params && params.modesAvail) || null) ), - section(div({ class: "tribe-details" }, padSide, padMain)) + section(div({ class: "tribe-details pad-page" }, padSide, padMain)) ) } diff --git a/src/views/parliament_view.js b/src/views/parliament_view.js index 7a9a9b1..0558668 100644 --- a/src/views/parliament_view.js +++ b/src/views/parliament_view.js @@ -1,11 +1,159 @@ const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink} = require('./main_views'); +const { template, i18n, userLink, paged } = require('./main_views'); +const { renderStyledText } = require('../backend/renderStyledText'); + +const { seatPositions } = require('../models/parliament_model').hemicycle; + +const HEMI_W = 720; +const HEMI_H = 380; + +const hemiEsc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + +const blocClass = (key) => { + const m = /^opp(\d+)$/.exec(String(key || '')); + return m ? `hemi-bloc-opp${Number(m[1]) % 8}` : `hemi-bloc-${key}`; +}; + +const seatClass = (seat, mode) => { + const cls = ['hemi-seat']; + if (mode === 'houses') cls.push(seat.house ? `hemi-house-${seat.house}` : 'hemi-bloc-none'); + else cls.push(blocClass(seat.bloc)); + if (seat.leader) cls.push('hemi-leader'); + if (seat.inPower) cls.push('hemi-power'); + return cls.join(' '); +}; + +const thresholdMarker = (positions, count, cls, label) => { + if (!count || count <= 0 || count > positions.length) return null; + const p = positions[count - 1]; + const q = positions[Math.min(positions.length - 1, count)]; + const angle = (p.angle + q.angle) / 2; + const cx = HEMI_W / 2; + const cy = HEMI_H - 20; + const inner = 40; + const outer = Math.min(HEMI_W / 2 - 16, HEMI_H - 60) + 14; + return { + angle, cls, text: `${label}: ${count}`, + x1: cx + inner * Math.cos(angle), y1: cy - inner * Math.sin(angle), + x2: cx + outer * Math.cos(angle), y2: cy - outer * Math.sin(angle), + tx: cx + (outer + 12) * Math.cos(angle), ty: cy - (outer + 12) * Math.sin(angle), + anchor: angle > Math.PI / 2 + 0.2 ? 'end' : (angle < Math.PI / 2 - 0.2 ? 'start' : 'middle') + }; +}; + +const LABEL_LINE = 14; +const renderMarkers = (markers) => { + const list = markers.filter(Boolean); + const placed = []; + for (const m of list.slice().sort((a, b) => a.ty - b.ty)) { + let ty = m.ty; + while (placed.some(o => Math.abs(o.ty - ty) < LABEL_LINE && Math.abs(o.tx - m.tx) < 160)) ty += LABEL_LINE; + placed.push({ ...m, ty }); + } + const drawn = new Set(); + return placed.map(m => { + const key = `${m.x2.toFixed(1)},${m.y2.toFixed(1)}`; + const line = drawn.has(key) ? '' : ``; + drawn.add(key); + return `${line}${hemiEsc(m.text)}`; + }).join(''); +}; + +const hemicycleSvg = (data, mode) => { + const seats = Array.isArray(data.seats) ? data.seats : []; + const unit = Math.max(1, Number(data.seatUnit) || 1); + const shown = unit > 1 ? seats.filter((_, i) => i % unit === 0) : seats; + const positions = seatPositions(shown.length, { width: HEMI_W, height: HEMI_H }); + const circles = shown.map((seat, i) => { + const pos = positions[i]; + if (!pos) return ''; + const title = hemiEsc(seat.name ? `${seat.name} (${seat.id})` : seat.id); + const circle = `${title}`; + return unit > 1 ? circle : `${circle}`; + }).join(''); + const t = data.thresholds || {}; + const scale = (n) => Math.ceil(Number(n || 0) / unit); + const markers = mode === 'houses' ? '' : renderMarkers([ + thresholdMarker(positions, scale(t.quorum), 'hemi-threshold-quorum', i18n.parliamentSeatsQuorum), + thresholdMarker(positions, scale(t.pass), 'hemi-threshold-pass', i18n.parliamentSeatsPass) + ]); + return `${circles}${markers}`; +}; + +const pct = (n, total) => total > 0 ? `${((n / total) * 100).toFixed(1)}%` : '0%'; + +const blocLabel = (b) => { + if (b.kind === 'government') return `${i18n.parliamentSeatsGovernment} · ${b.label}`; + if (b.kind === 'opposition') return `${i18n.parliamentSeatsOpposition} · ${i18n.parliamentSeatsVotesFor} ${b.label}`; + return i18n.parliamentSeatsAbstention; +}; + +const legendElection = (data) => ul({ class: 'hemi-legend' }, + ...(data.blocs || []).map(b => li({ class: 'hemi-legend-item' }, + span({ class: `hemi-swatch ${blocClass(b.key)}` }), + b.href ? a({ href: b.href, class: 'hemi-legend-link' }, blocLabel(b)) : span(blocLabel(b)), + span({ class: 'hemi-legend-count' }, `${b.count} ${i18n.parliamentSeatsSeats} · ${pct(b.count, data.population)}`) + )) +); + +const legendHouses = (data, houseNames = {}) => { + const entries = Object.entries(data.houses || {}).sort((a, b) => b[1] - a[1]); + const noHouse = data.population - entries.reduce((s, [, n]) => s + n, 0); + return ul({ class: 'hemi-legend' }, + ...entries.map(([key, n]) => li({ class: 'hemi-legend-item' }, + span({ class: `hemi-swatch hemi-house-${key}` }), + a({ href: `/larp/${encodeURIComponent(key)}`, class: 'hemi-legend-link' }, houseNames[key] || key), + span({ class: 'hemi-legend-count' }, `${n} ${i18n.parliamentSeatsSeats} · ${pct(n, data.population)}`) + )), + noHouse > 0 ? li({ class: 'hemi-legend-item' }, span({ class: 'hemi-swatch hemi-bloc-none' }), span(i18n.parliamentSeatsNoHouse), span({ class: 'hemi-legend-count' }, `${noHouse} ${i18n.parliamentSeatsSeats} · ${pct(noHouse, data.population)}`)) : null + ); +}; + +const renderHemicycle = (data, { mode = 'election', baseHref = '/parliament?filter=government', houseNames = {}, showHouses = true } = {}) => { + if (!data || !Array.isArray(data.seats) || !data.seats.length) return null; + const seatsMode = mode === 'houses' ? 'houses' : 'election'; + const join = baseHref.includes('?') ? '&' : '?'; + const toggle = showHouses ? div({ class: 'filters hemi-modes' }, + a({ href: baseHref, class: seatsMode === 'election' ? 'filter-btn active' : 'filter-btn' }, i18n.parliamentSeatsByElection), + a({ href: `${baseHref}${join}seats=houses`, class: seatsMode === 'houses' ? 'filter-btn active' : 'filter-btn' }, i18n.parliamentSeatsByHouses) + ) : null; + const unit = Math.max(1, Number(data.seatUnit) || 1); + return div({ class: 'hemicycle' }, + toggle, + (() => { + const svg = hemicycleSvg(data, seatsMode); + return div({ class: 'hemicycle-zoom-wrap' }, + div({ class: 'hemicycle-canvas' }, + div({ [RAW_HTML]: svg }), + a({ href: '#hemi-zoom', id: 'hemi-zoom-src', class: 'hemi-zoom-link', title: i18n.bankChartZoomHint }) + ), + div({ id: 'hemi-zoom', class: 'lightbox bank-eco-chart-lightbox' }, + a({ href: '#hemi-zoom-src', class: 'lightbox-close' }, '\u00d7'), + div({ class: 'bank-eco-chart-canvas-zoom hemi-zoom-canvas', [RAW_HTML]: svg }) + ) + ); + })(), + unit > 1 ? p({ class: 'hemi-note' }, `1 ${i18n.parliamentSeatsSeat} = ${unit}`) : null, + seatsMode === 'houses' ? legendHouses(data, houseNames) : legendElection(data), + data.leaderId ? p({ class: 'hemi-note' }, `${i18n.parliamentSeatsLeader}: `, a({ href: `/author/${encodeURIComponent(data.leaderId)}`, class: 'hemi-legend-link' }, (data.seats.find(s => s.id === data.leaderId) || {}).name || data.leaderId)) : null + ); +}; + + +const campaignBlock = (pItem) => pItem && pItem.campaignId + ? [ + div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.campaignSignaturesLabel || 'Signatures').toUpperCase() + ': '), span({ class: 'card-value' }, `${Number(pItem.signatures) || 0}${Number(pItem.goal) > 0 ? ` / ${Number(pItem.goal)}` : ''}`)), + div({ class: 'doc-export-actions doc-export-left' }, form({ method: 'GET', action: `/campaigns/${encodeURIComponent(pItem.campaignId)}/pdf` }, button({ type: 'submit', class: 'filter-btn' }, i18n.parliamentViewReport))) + ] + : []; const TERM_DAYS = 60; -const fmt = (d) => moment(d).format('YYYY-MM-DD HH:mm:ss'); +const fmt = (d) => d ? moment(d).format('YYYY-MM-DD HH:mm:ss') : '—'; const timeLeft = (end) => { + if (!end) return '—'; const diff = moment(end).diff(moment()); if (diff <= 0) return '0d 00:00:00'; const dur = moment.duration(diff); @@ -28,26 +176,6 @@ const showVoteMetrics = (method) => { }; const applyEl = (fn, attrs, kids) => fn.apply(null, [attrs || {}].concat(kids || [])); const methodImageSrc = (method) => `assets/images/${String(method || '').toUpperCase().toLowerCase()}.png`; -const MethodBadge = (method) => { - const m = String(method || '').toUpperCase(); - const labelTxt = String(i18n[`parliamentMethod${m}`] || m).toUpperCase(); - return span( - { class: 'method-badge' }, - labelTxt, - br(),br(), - img({ src: methodImageSrc(m), alt: labelTxt, class: 'method-badge__icon' }) - ); -}; -const MethodHero = (method) => { - const m = String(method || '').toUpperCase(); - const labelTxt = String(i18n[`parliamentMethod${m}`] || m).toUpperCase(); - return span( - { class: 'method-hero' }, - labelTxt, - br(),br(), - img({ src: methodImageSrc(m), alt: labelTxt, class: 'method-hero__icon' }) - ); -}; const KPI = (labelTxt, value) => div({ class: 'kpi' }, span({ class: 'kpi__label' }, labelTxt), @@ -63,20 +191,22 @@ const CycleInfo = (start, end, labels = { KPI((labels.end + ': ').toUpperCase(), fmt(end)), KPI((labels.remaining + ': ').toUpperCase(), timeLeft(end)) ); -const Tabs = (active) => +const Tabs = (active, avail = null) => div( { class: 'filters' }, form( { method: 'GET', action: '/parliament' }, - ['government', 'candidatures', 'proposals', 'laws', 'revocations', 'historical', 'leaders', 'rules'].map(f => + ['government', 'candidatures', 'proposals', 'laws', 'revocations', 'historical', 'leaders', 'rules'] + .filter(f => !avail || f === 'government' || f === 'rules' || f === active || avail[f]) + .map(f => button({ type: 'submit', name: 'filter', value: f, class: active === f ? 'filter-btn active' : 'filter-btn' }, i18n[`parliamentFilter${f.charAt(0).toUpperCase()+f.slice(1)}`]) ) ) ); const GovHeader = (g) => { - const termStart = g && g.since ? g.since : moment().toISOString(); - const termEnd = g && g.end ? g.end : moment(termStart).add(TERM_DAYS, 'days').toISOString(); + const termStart = g && g.since ? g.since : null; + const termEnd = g && g.end ? g.end : null; const methodKeyRaw = g && g.method ? String(g.method) : 'ANARCHY'; const methodKey = methodKeyRaw.toUpperCase(); const i18nMeth = i18n[`parliamentMethod${methodKey}`]; @@ -117,9 +247,9 @@ const GovHeader = (g) => { ); }; -const GovernmentCard = (g, meta) => { - const termStart = g && g.since ? g.since : moment().toISOString(); - const termEnd = g && g.end ? g.end : moment(termStart).add(TERM_DAYS, 'days').toISOString(); +const GovernmentCard = (g, meta, hemicycle = null, seatsMode = 'election', houseNames = {}) => { + const termStart = g && g.since ? g.since : null; + const termEnd = g && g.end ? g.end : null; const actorLabel = g.powerType === 'tribe' ? (i18n.parliamentActorInPowerTribe || i18n.parliamentActorInPower || 'TRIBE RULING') @@ -153,7 +283,9 @@ const GovernmentCard = (g, meta) => { { class: 'card' }, h2(i18n.parliamentGovernmentCard), GovHeader(g), - div({ class: 'method-image-centered' }, img({ src: methodImageSrc(methodKey), alt: methodLabel })), + hemicycle + ? renderHemicycle(hemicycle, { mode: seatsMode, baseHref: '/parliament?filter=government', houseNames }) + : div({ class: 'method-image-centered' }, img({ src: methodImageSrc(methodKey), alt: methodLabel })), div( { class: 'table-wrap' }, applyEl(table, { class: 'table table--centered gov-overview' }, [ @@ -263,7 +395,7 @@ const CandidatureStats = (cands, govCard, leaderMeta, electionQuorum = 2) => { h2(i18n.parliamentElectionsStatusTitle), div({ class: 'card-field card-field--spaced' }, span({ class: 'card-label' }, winLbl + ': '), - span({ class: 'card-value' }, hasQuorum ? idLink : span({ style: 'color:#ffcc00;font-weight:bold;' }, i18n.voteNoQuorum || 'NO QUORUM')) + span({ class: 'card-value' }, hasQuorum ? idLink : span({ class: 'vote-no-quorum' }, i18n.voteNoQuorum || 'NO QUORUM')) ), hasQuorum ? div({ class: 'card-field card-field--spaced' }, @@ -274,14 +406,14 @@ const CandidatureStats = (cands, govCard, leaderMeta, electionQuorum = 2) => { hasQuorum ? div( { class: 'table-wrap mt-2' }, - applyEl(table, { class: 'parliament-gov-table' }, [ + applyEl(table, { class: 'parliament-gov-table parliament-rows-table' }, [ thead(tr( th(i18n.parliamentThLeader), th({ class: 'parliament-method-col' }, i18n.parliamentGovMethod), th({ class: 'parliament-votes-col' }, i18n.parliamentVotesReceived) )), tbody(tr( - td( + td({ 'data-label': i18n.parliamentThLeader }, div({ class: 'parliament-avatar-cell' }, avatarHref ? a({ href: avatarHref }, img({ src: avatarSrc, class: 'parliament-leader-avatar' })) @@ -289,10 +421,10 @@ const CandidatureStats = (cands, govCard, leaderMeta, electionQuorum = 2) => { idLink ? div({ class: 'parliament-avatar-name' }, idLink) : null ) ), - td({ class: 'parliament-method-col' }, + td({ class: 'parliament-method-col', 'data-label': i18n.parliamentGovMethod }, img({ src: methodImageSrc(methodKey), alt: methodLabel, class: 'parliament-method-img' }) ), - td({ class: 'parliament-votes-col' }, span({ class: 'votes-value' }, votes)) + td({ class: 'parliament-votes-col', 'data-label': i18n.parliamentVotesReceived }, span({ class: 'votes-value' }, votes)) )) ]) ) @@ -306,23 +438,19 @@ const CandidaturesTable = (candidatures) => { c.targetType === 'inhabitant' ? p(userLink(c.targetId)) : p(a({ class: 'tag-link', href: `/tribe/${encodeURIComponent(c.targetId)}?` }, c.targetTitle || c.targetId)); - const voters = Array.isArray(c.voters) ? c.voters : []; return tr( - td(idLink), - td(fmt(c.createdAt)), - td({ class: 'nowrap' }, c.method), - td(c.targetType === 'inhabitant' ? String(c.karma || 0) : '-'), - td( - span({ class: 'candidature-votes-count' }, String(c.votes || 0)), - voters.length ? div({ class: 'candidature-voters' }, ...voters.map(v => div({ class: 'candidature-voter' }, userLink(v)))) : null - ), - td(form({ method: 'POST', action: `/parliament/candidatures/${encodeURIComponent(c.id)}/vote` }, button({ class: 'vote-btn' }, i18n.parliamentVoteBtn))) + td({ 'data-label': i18n.parliamentThId }, idLink), + td({ 'data-label': i18n.parliamentThProposalDate }, fmt(c.createdAt)), + td({ class: 'nowrap', 'data-label': i18n.parliamentThMethod }, c.method), + td({ 'data-label': i18n.parliamentThKarma }, c.targetType === 'inhabitant' ? String(c.karma || 0) : '-'), + td({ 'data-label': i18n.parliamentThSupports }, span({ class: 'candidature-votes-count' }, String(c.votes || 0))), + td({ 'data-label': '' }, form({ method: 'POST', action: `/parliament/candidatures/${encodeURIComponent(c.id)}/vote` }, button({ class: 'vote-btn' }, i18n.parliamentVoteBtn))) ); }); return div( { class: 'table-wrap' }, h2(i18n.parliamentCandidaturesListTitle), - applyEl(table, { class: 'table table--centered' }, [ + applyEl(table, { class: 'table table--centered parliament-rows-table' }, [ thead(tr( th(i18n.parliamentThId), th(i18n.parliamentThProposalDate), @@ -343,7 +471,7 @@ const ProposalForm = () => form( { method: 'POST', action: '/parliament/proposals/create' }, label(i18n.parliamentProposalTitle), br(), - input({ type: 'text', name: 'title', required: true }), br(), br(), + input({ type: 'text', name: 'title', maxlength: '100', required: true }), br(), br(), label(i18n.parliamentProposalDescription), br(), textarea({ name: 'description', rows: 5, maxlength: 1000 }), br(), br(), button({ type: 'submit', class: 'create-button' }, i18n.parliamentProposalPublish) @@ -377,11 +505,11 @@ const ProposalsList = (proposals) => { span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method) ), - br(), div( h2(titleNode), - p(pItem.description || '') + p(...renderStyledText(pItem.description || '')) ), + ...campaignBlock(pItem), pItem.deadline ? div( { class: 'card-field' }, @@ -420,7 +548,7 @@ const ProposalsList = (proposals) => { pItem && pItem.voteId ? form( { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` }, - button({ type: 'submit', class: 'vote-btn' }, i18n.parliamentVoteAction) + button({ type: 'submit', class: 'buy-btn' }, i18n.parliamentVoteAction) ) : null ); @@ -442,7 +570,8 @@ const FutureLawsList = (rows) => { div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, userLink(pItem.proposer))), h2(pItem.title || ''), - p(pItem.description || '') + p(...renderStyledText(pItem.description || '')), + ...campaignBlock(pItem) ) ); return div( @@ -552,7 +681,7 @@ const RevocationsList = (revocations) => { pItem && pItem.voteId ? form( { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` }, - button({ type: 'submit', class: 'vote-btn' }, i18n.parliamentVoteAction) + button({ type: 'submit', class: 'buy-btn' }, i18n.parliamentVoteAction) ) : null ); @@ -593,7 +722,7 @@ const LawsStats = (laws = [], revocatedCount = 0) => { return div( { class: 'table-wrap' }, h2(i18n.parliamentPoliciesTitle || 'POLICIES'), - applyEl(table, { class: 'table table--centered' }, [ + applyEl(table, { class: 'table table--centered parliament-rows-table' }, [ thead(tr( th(i18n.parliamentThProposed), th(i18n.parliamentThApproved), @@ -603,11 +732,11 @@ const LawsStats = (laws = [], revocatedCount = 0) => { )), tbody( tr( - td(String(proposed)), - td(String(approved)), - td(String(declined)), - td(String(discarded)), - td(String(revocated)) + td({ 'data-label': i18n.parliamentThProposed }, String(proposed)), + td({ 'data-label': i18n.parliamentThApproved }, String(approved)), + td({ 'data-label': i18n.parliamentThDeclined }, String(declined)), + td({ 'data-label': i18n.parliamentThDiscarded }, String(discarded)), + td({ 'data-label': i18n.parliamentPoliciesRevocated }, String(revocated)) ) ) ]) @@ -656,19 +785,19 @@ const HistoricalGovsSummary = (rows = []) => { const entries = Array.from(byMethod.entries()).sort((a,b) => String(a[0]).localeCompare(String(b[0]))); const lines = entries.map(([method, rec]) => tr( - td(method), - td(String(rec.proposed)), - td(String(rec.approved)), - td(String(rec.declined)), - td(String(rec.discarded)), - td(String(rec.revocated)), - td(String(rec.cycles)) + td({ 'data-label': String(i18n.parliamentGovMethod).toUpperCase() }, method), + td({ 'data-label': i18n.parliamentPoliciesProposal }, String(rec.proposed)), + td({ 'data-label': i18n.parliamentPoliciesApproved }, String(rec.approved)), + td({ 'data-label': i18n.parliamentPoliciesDeclined }, String(rec.declined)), + td({ 'data-label': i18n.parliamentPoliciesDiscarded }, String(rec.discarded)), + td({ 'data-label': i18n.parliamentPoliciesRevocated }, String(rec.revocated)), + td({ 'data-label': i18n.parliamentThCycles }, String(rec.cycles)) ) ); return div( { class: 'table-wrap' }, h2(i18n.parliamentHistoricalGovernmentsTitle || 'Governments'), - applyEl(table, { class: 'table table--centered' }, [ + applyEl(table, { class: 'table table--centered parliament-rows-table' }, [ thead(tr( th(String(i18n.parliamentGovMethod).toUpperCase()), th(i18n.parliamentPoliciesProposal), @@ -781,7 +910,7 @@ const LeadersSummary = (leaders = [], candidatures = [], allGovCards = [], total return div( { class: 'table-wrap' }, h2(i18n.parliamentHistoricalLawsTitle || 'Actions'), - applyEl(table, { class: 'table table--centered' }, [ + applyEl(table, { class: 'table table--centered parliament-rows-table' }, [ thead(tr( th(i18n.parliamentThTotalCandidatures), th(i18n.parliamentThTimesInPower), @@ -794,14 +923,14 @@ const LeadersSummary = (leaders = [], candidatures = [], allGovCards = [], total )), tbody( tr( - td(String(presented)), - td(String(timesInPower)), - td(String(totals.proposed)), - td(String(totals.approved)), - td(String(totals.declined)), - td(String(totals.discarded)), - td(String(totals.revocated)), - td(`${efficiencyPct} %`) + td({ 'data-label': i18n.parliamentThTotalCandidatures }, String(presented)), + td({ 'data-label': i18n.parliamentThTimesInPower }, String(timesInPower)), + td({ 'data-label': i18n.parliamentThProposed }, String(totals.proposed)), + td({ 'data-label': i18n.parliamentThApproved }, String(totals.approved)), + td({ 'data-label': i18n.parliamentThDeclined }, String(totals.declined)), + td({ 'data-label': i18n.parliamentThDiscarded }, String(totals.discarded)), + td({ 'data-label': i18n.parliamentPoliciesRevocated }, String(totals.revocated)), + td({ 'data-label': i18n.parliamentEfficiency }, `${efficiencyPct} %`) ) ) ]) @@ -824,13 +953,13 @@ const LeadersList = (leaders, metas = {}, candidatures = []) => { : userLink(l.powerId, l.powerTitle); const leaderCell = div({ class: 'leader-cell parliament-avatar-cell' }, avatar, div({ class: 'parliament-avatar-name' }, link)); return tr( - td(leaderCell), - td(String(l.proposed || 0)), - td(String(l.approved || 0)), - td(String(l.declined || 0)), - td(String(l.discarded || 0)), - td(String(l.revocated || 0)), - td(`${(l.efficiency != null ? Math.round(l.efficiency * 100) : (l.proposed > 0 ? Math.round((l.approved / l.proposed) * 100) : 0))} %`) + td({ 'data-label': i18n.parliamentThLeader }, leaderCell), + td({ 'data-label': i18n.parliamentPoliciesProposal }, String(l.proposed || 0)), + td({ 'data-label': i18n.parliamentPoliciesApproved }, String(l.approved || 0)), + td({ 'data-label': i18n.parliamentPoliciesDeclined }, String(l.declined || 0)), + td({ 'data-label': i18n.parliamentPoliciesDiscarded }, String(l.discarded || 0)), + td({ 'data-label': i18n.parliamentPoliciesRevocated }, String(l.revocated || 0)), + td({ 'data-label': i18n.parliamentEfficiency }, `${(l.efficiency != null ? Math.round(l.efficiency * 100) : (l.proposed > 0 ? Math.round((l.approved / l.proposed) * 100) : 0))} %`) ); }); return div( @@ -855,6 +984,7 @@ const RulesContent = () => { const points = [ i18n.parliamentRulesIntro, i18n.parliamentRulesTerm, + i18n.parliamentRulesHemicycle, i18n.parliamentRulesMethods, i18n.parliamentRulesAnarchy, i18n.parliamentRulesCandidates, @@ -887,7 +1017,7 @@ const CandidaturesSection = (governmentCard, candidatures, leaderMeta, electionQ GovHeader(governmentCard || {}), CandidatureStats(candidatures || [], governmentCard || null, leaderMeta || null, electionQuorum), CandidatureForm(), - candidatures && candidatures.length ? CandidaturesTable(candidatures) : null + candidatures && candidatures.length ? CandidaturesTable(paged(candidatures)) : null ); }; @@ -896,7 +1026,7 @@ const ProposalsSection = (governmentCard, proposals, futureLaws, canPropose) => const fl = FutureLawsList(futureLaws || []); if (!has && canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), fl); if (!has && !canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), NoProposals(), fl); - return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), ProposalsList(proposals), fl); + return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), ProposalsList(paged(proposals)), fl); }; const RevocationsSection = (governmentCard, laws, revocations, futureRevocations) => @@ -904,7 +1034,7 @@ const RevocationsSection = (governmentCard, laws, revocations, futureRevocations h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), RevocationForm(laws || []), - RevocationsList(revocations || []) || '', + RevocationsList(paged(revocations || [])) || '', FutureRevocationsList(futureRevocations || []) || '' ); @@ -929,6 +1059,9 @@ const parliamentView = async (state) => { leaders, leaderMeta, powerMeta, + hemicycle, + seatsMode, + houseNames, revocations, futureRevocations, revocationsEnactedCount, @@ -950,8 +1083,8 @@ const parliamentView = async (state) => { powerType: 'none', powerId: null, powerTitle: 'ANARCHY', - since: moment().toISOString(), - end: moment().add(TERM_DAYS, 'days').toISOString(), + since: null, + end: null, inhabitantsTotal: Number(inhabitantsTotal ?? 0) || 0 }; @@ -963,24 +1096,30 @@ const parliamentView = async (state) => { const LawsSectionWrap = () => div( LawsStats(laws || [], revocationsEnactedCount || 0), - LawsList(laws || []) + LawsList(paged(laws || [])) ); return template( i18n.parliamentTitle, - section(div({ class: 'tags-header' }, h2(i18n.parliamentTitle), p(i18n.parliamentDescription)), Tabs(filter)), + section(div({ class: 'tags-header module-header-line' }, h2(i18n.parliamentTitle), p(i18n.parliamentDescription)), Tabs(filter, { + candidatures: true, + proposals: true, + laws: (laws || []).length > 0 || (futureLaws || []).length > 0, + revocations: (revocations || []).length > 0 || (futureRevocations || []).length > 0, + historical: (historical || []).length > 0, + leaders: (leaders || []).length > 0 + })), section( - filter === 'government' ? GovernmentCard(gov, powerMeta) : null, + filter === 'government' ? GovernmentCard(gov, powerMeta, hemicycle || null, seatsMode || 'election', houseNames || {}) : null, filter === 'candidatures' ? CandidaturesSection(gov, candidatures, leaderMeta, electionQuorum) : null, filter === 'proposals' ? ProposalsSection(gov, proposals, futureLaws, canPropose) : null, filter === 'laws' ? LawsSectionWrap() : null, filter === 'revocations' ? RevocationsSection(gov, laws, revocations, futureRevocations) : null, - filter === 'historical' ? div(HistoricalGovsSummary(historical || []), HistoricalList(historical || [], historicalMetas)) : null, - filter === 'leaders' ? div(LeadersSummary(leaders || [], candidatures || [], leadersGovCards, leadersTotalCandidatures), LeadersList(leaders || [], leadersMetas, candidatures || [])) : null, + filter === 'historical' ? div(HistoricalGovsSummary(historical || []), HistoricalList(paged(historical || []), historicalMetas)) : null, + filter === 'leaders' ? div(LeadersSummary(leaders || [], candidatures || [], leadersGovCards, leadersTotalCandidatures), LeadersList(paged(leaders || []), leadersMetas, candidatures || [])) : null, filter === 'rules' ? RulesContent() : null ) ); }; -module.exports = { parliamentView, pickLeader }; - +module.exports = { parliamentView, pickLeader, renderHemicycle }; diff --git a/src/views/peers_view.js b/src/views/peers_view.js index 4f9e793..f2bd5b1 100644 --- a/src/views/peers_view.js +++ b/src/views/peers_view.js @@ -1,12 +1,35 @@ -const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [] }) => { - const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea } = require("../server/node_modules/hyperaxe"); - const { template, i18n } = require('./main_views'); +const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [], versions = {}, staleKeys = [], onionKeys = [], ownVersion = '', lastErrors = {}, paused = false, filter = 'status', advanced = false, health = null, lan = null }) => { + const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea, wbr } = require("../server/node_modules/hyperaxe"); + const { template, i18n, renderStateChip, renderModuleStats } = require('./main_views'); + const { NET_REASON_KEYS, canonicalKey } = require('../models/peer_health'); + const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0); + const isOutdated = (v) => { + if (!v || !ownVersion) return false; + const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(ownVersion); + return a1 < a2 || (a1 === a2 && b1 < b2); + }; + const stale = new Set(staleKeys || []); + const outdatedKey = (k) => isOutdated((versions || {})[k]) || (!(versions || {})[k] && stale.has(k)); + const knowsVersion = (keys) => keys.some(k => (versions || {})[k] || stale.has(k)); + const unspecifiedHost = (h) => /^(0\.0\.0\.0|::|\[::\])$/.test(String(h || '')); + const versionCell = (k) => { + const v = (versions || {})[k]; + if (!v) return [stale.has(k) ? span({ title: i18n.peerVersionUnannounced }, renderStateChip('closed', null, String(i18n.peerOutdated).toUpperCase())) : '—']; + const old = isOutdated(v); + return [span({ title: old ? i18n.peerOutdated : '' }, renderStateChip(old ? 'closed' : 'mutuals', null, String(v)))]; + }; + const stateCell = (tp) => { + const st = String(tp.state || ''); + if (st === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase()); + if (st === 'connecting') return renderStateChip('whole', null, String(i18n.peerStateConnecting).toUpperCase()); + if (st === 'staged') return renderStateChip(outdatedKey(tp.key) ? 'whole' : 'mutuals', null, String(i18n.peerStateStaged).toUpperCase()); + const err = (lastErrors || {})[canonicalKey(tp.key)]; + return renderStateChip('hidden', null, String(i18n[err ? NET_REASON_KEYS[err.reason] : 'peerErrSilent'] || i18n.peerErrOther).toUpperCase()); + }; - const startButton = form({ action: "/settings/conn/start", method: "post" }, button({ type: "submit" }, i18n.startNetworking)); - const restartButton = form({ action: "/settings/conn/restart", method: "post" }, button({ type: "submit" }, i18n.restartNetworking)); - const stopButton = form({ action: "/settings/conn/stop", method: "post" }, button({ type: "submit" }, i18n.stopNetworking)); - const syncButton = form({ action: "/settings/conn/sync", method: "post" }, button({ type: "submit" }, i18n.sync)); - const connButtons = [startButton, restartButton, stopButton, syncButton]; + const pauseButton = paused + ? form({ action: "/peers/resume", method: "post" }, button({ type: "submit", class: "tribe-action-btn success-btn" }, String(i18n.peersResume || 'Resume').toUpperCase())) + : form({ action: "/peers/pause", method: "post" }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.peersPause || 'Pause').toUpperCase())); const deduplicatePeers = (peers) => { const seen = new Set(); @@ -28,16 +51,28 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc return null; }; - const renderPeerRow = (peerData) => { + const shortHost = (h) => { + const s = String(h || ''); + const m = s.match(/^(?:onion:)?([a-z2-7]{16,56})\.onion(.*)$/i); + return m ? `${m[1].slice(0, 10)}….onion${m[2]}` : s; + }; + const onion = new Set(onionKeys || []); + const overTor = (peerData) => /^onion:/i.test(String(peerData[0] || '')) || /\.onion$/i.test(String((peerData[1] || {}).host || '')) || onion.has(canonicalKey((peerData[1] || {}).key)); + const renderPeerRow = (peerData, showVersion) => { const peer = peerData[1]; const { name, users, key } = peer; + const tor = overTor(peerData); const peerUrl = `/author/${encodeURIComponent(key)}`; const filteredUsers = (users || []).filter(u => u.id !== key); - const userCount = filteredUsers.length; + const userCount = Array.isArray(users) ? filteredUsers.length : null; return tr( - td({ 'data-label': i18n.peerHost || 'Pub' }, a({ href: peerUrl, class: "user-link" }, name || key.slice(0, 20) + '…')), - td({ 'data-label': 'Key' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)), - td({ 'data-label': i18n.peersReplicatedFeeds || 'Feeds' }, String(userCount)) + td({ 'data-label': i18n.peerHost || 'Pub' }, unspecifiedHost(name) ? '—' : a({ href: peerUrl, class: "user-link", title: name || key }, name ? shortHost(name) : key.slice(0, 20) + '…')), + td({ 'data-label': i18n.peersOasisId || 'Oasis ID' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)), + showVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(key)) : null, + td({ 'data-label': i18n.peersTorColumn }, tor + ? span({ class: 'ubi-tick-ok', title: i18n.peersTorYes }, '✓') + : span({ class: 'peer-tor-no', title: i18n.peersTorNo }, '—')), + td({ 'data-label': i18n.peersReplicatedFeeds || 'Replicated' }, userCount == null ? '—' : String(userCount)) ); }; @@ -51,28 +86,35 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc const renderPeerTable = (peers, emptyKey) => { if (peers.length === 0) return p(i18n[emptyKey] || i18n.noConnections); + const showVersion = knowsVersion(peers.map(pd => pd[1].key)); return table({ class: 'block-info-table' }, tr( td({ class: 'card-label' }, i18n.peerHost || 'Pub'), - td({ class: 'card-label' }, 'Key'), - td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated feeds') + td({ class: 'card-label' }, i18n.peersOasisId || 'Oasis ID'), + showVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null, + td({ class: 'card-label' }, i18n.peersTorColumn), + td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated') ), - ...peers.map(renderPeerRow) + ...peers.map(pd => renderPeerRow(pd, showVersion)) ); }; + const techVersion = knowsVersion((technicalPeers || []).map(tp => tp.key)); const technicalRows = (technicalPeers || []).map(tp => { const k = tp.key || ''; const connected = tp.state === 'connected'; const action = connected ? 'disconnect' : 'connect'; const btnLabel = connected ? (i18n.peerDisconnect || 'Disconnect') : (i18n.peerConnect || 'Connect'); return tr( - td({ 'data-label': 'Key' }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')), - td({ 'data-label': 'Host' }, String(tp.host || '—')), - td({ 'data-label': i18n.peerPort || 'Port' }, String(tp.port || '—')), - td({ 'data-label': 'State' }, String(tp.state || tp.source || '—')), - td({ 'data-label': i18n.peerLastChange || 'Last change' }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')), - td({ 'data-label': '' }, + td({ 'data-label': i18n.peersOasisId }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')), + onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || '')) + ? td({ 'data-label': i18n.peerAddressLabel, class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR')) + : td({ 'data-label': i18n.peerAddressLabel, title: unspecifiedHost(tp.host) ? '' : String(tp.host || ''), class: 'peer-host' }, tp.host && !unspecifiedHost(tp.host) ? shortHost(tp.host).split(/(?<=\.)/).reduce((acc, part, i) => i ? [...acc, wbr(), part] : [part], []) : '—'), + td({ 'data-label': i18n.peerPort, class: 'peer-port' }, String(tp.port || '—')), + techVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(k)) : null, + td({ 'data-label': i18n.peerStateLabel }, stateCell(tp)), + td({ 'data-label': i18n.peerLastChangeLabel }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')), + td( form({ method: "POST", action: `/peers/${action}`, class: "inline-form" }, input({ type: "hidden", name: "key", value: k }), input({ type: "hidden", name: "host", value: String(tp.host || '') }), @@ -82,31 +124,46 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc ) ); }); - const refreshButton = form({ action: "/peers/refresh", method: "post" }, button({ type: "submit" }, i18n.peerRefresh || 'Refresh')); - const pruneButton = form({ action: "/peers/prune", method: "post" }, button({ type: "submit" }, i18n.peerPruneIdle || 'Remove idle')); - const exportButton = form({ action: "/peers/export", method: "get" }, button({ type: "submit" }, i18n.peerExport || 'Export')); + const withCount = (label, n) => Number(n) > 0 ? `${label} (${n})` : label; + const peersToolbar = (tabName, total, segments) => { + if (!(Number(total) > 0)) return null; + const back = input({ type: "hidden", name: "returnTo", value: `/peers?filter=${tabName}` }); + return div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(total, segments), + div({ class: "conn-actions peers-conn-actions filter-box" }, + form({ action: "/peers/refresh", method: "post" }, back, button({ type: "submit", class: "filter-btn" }, i18n.peerRefresh || 'Refresh')), + form({ action: "/peers/prune", method: "post" }, back, tabName === 'lan' ? input({ type: "hidden", name: "scope", value: "lan" }) : null, button({ type: "submit", class: "filter-btn" }, i18n.peerPruneIdle || 'Remove idle')), + form({ action: "/peers/export", method: "get" }, button({ type: "submit", class: "filter-btn" }, i18n.peerExport || 'Export')) + ) + ); + }; const importForm = form( { action: "/peers/import", method: "post", enctype: "multipart/form-data", class: "peers-import-form" }, label({ class: 'peers-import-label' }, i18n.peerImportTitle || 'Import peer list'), br(), textarea({ name: "peerList", rows: "4", placeholder: i18n.peerImportPlaceholder || 'Paste one multiserver address per line…' }), - br(), input({ type: "file", name: "peerFile", accept: ".txt,text/plain" }), br(), button({ type: "submit", class: "filter-btn" }, i18n.peerImport || 'Import') ); - const peersTechnicalBlock = div({ class: 'tags-header peers-technical-block' }, - h2(i18n.peerConnectionsTitle || 'Connections'), - div({ class: "conn-actions peers-conn-actions" }, refreshButton, pruneButton, exportButton), + const peersTechnicalBlock = div({ class: 'peers-technical-block' }, + peersToolbar('connections', technicalPeers.length, [ + { label: i18n.peerStateConnected, count: technicalPeers.filter(tp => tp.state === 'connected').length }, + { label: i18n.peerOutdated, count: technicalPeers.filter(tp => outdatedKey(tp.key)).length }, + { label: 'TOR', count: technicalPeers.filter(tp => onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))).length } + ]), + div({ class: "tags-header" }, h2(withCount(i18n.peerConnectionsTitle || 'Connections', technicalPeers.length)), p(i18n.peersConnectionsHint)), + div({ class: "peers-pause-row" }, pauseButton, p({ class: paused ? "peers-paused-note" : "peers-pause-hint" }, paused ? i18n.peersPausedNote : i18n.peersPauseHint)), technicalPeers.length ? table({ class: 'block-info-table' }, tr( - td({ class: 'card-label' }, 'Key'), - td({ class: 'card-label' }, 'Host'), - td({ class: 'card-label' }, i18n.peerPort || 'Port'), - td({ class: 'card-label' }, 'State'), - td({ class: 'card-label' }, 'Last change'), + td({ class: 'card-label' }, i18n.peersOasisId), + td({ class: 'card-label' }, i18n.peerAddressLabel), + td({ class: 'card-label peer-port' }, i18n.peerPort), + techVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null, + td({ class: 'card-label' }, i18n.peerStateLabel), + td({ class: 'card-label' }, i18n.peerLastChangeLabel), td({ class: 'card-label' }, '') ), ...technicalRows @@ -115,28 +172,136 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc importForm ); + const fmtDate = (ts) => ts ? new Date(ts).toISOString().slice(0, 16).replace('T', ' ') : '—'; + const healthRows = health && Array.isArray(health.rows) ? health.rows : []; + const healthKind = (r) => { + if (r.state === 'connected') return 'connected'; + if (r.dead) return 'unreachable'; + if ((Number(r.failures) || 0) > 0 || r.reason) return 'failing'; + return 'reachable'; + }; + const healthChip = (r) => { + const k = healthKind(r); + if (k === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase()); + if (k === 'unreachable') return renderStateChip('closed', null, String(i18n.graphosFedUnreachable).toUpperCase()); + if (k === 'failing') return renderStateChip('hidden', null, String(i18n[NET_REASON_KEYS[r.reason] || 'peerErrSilent'] || i18n.peerErrOther).toUpperCase()); + return renderStateChip(outdatedKey(r.key) ? 'whole' : 'mutuals', null, String(i18n.peersHealthReachable).toUpperCase()); + }; + const healthCount = (k) => healthRows.filter(r => healthKind(r) === k).length; + const deadCount = healthRows.filter(r => r.dead).length; + const healthVersion = knowsVersion(healthRows.map(r => r.key)); + const healthBlock = !healthRows.length ? div({ class: 'peers-list' }, p(i18n.noConnections)) : div({ class: 'peers-technical-block peers-health-block' }, + peersToolbar('health', healthRows.length, [ + { label: i18n.peersHealthReachable, count: healthCount('connected') + healthCount('reachable') }, + { label: i18n.peersHealthFailing, count: healthCount('failing') }, + { label: i18n.graphosFedUnreachable, count: deadCount }, + { label: i18n.peerOutdated, count: healthRows.filter(r => outdatedKey(r.key)).length }, + { label: 'TOR', count: healthRows.filter(r => r.tor).length } + ]), + div({ class: "tags-header" }, h2(withCount(i18n.peersTabHealth, healthRows.length)), p(i18n.peersHealthHint)), + deadCount + ? div({ class: "conn-actions peers-conn-actions filters" }, + form({ action: "/peers/clear-unreachable", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peersHealthForget))) + : null, + healthRows.length + ? table({ class: 'block-info-table' }, + tr( + td({ class: 'card-label' }, i18n.peerHost || 'IP'), + td({ class: 'card-label' }, i18n.peersOasisId || 'ID'), + td({ class: 'card-label' }, i18n.graphosFedTransport), + td({ class: 'card-label' }, i18n.peerStateLabel), + td({ class: 'card-label' }, i18n.peersHealthFailures), + td({ class: 'card-label' }, i18n.peersFailingSince), + healthVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null + ), + ...healthRows.map(r => tr( + r.tor + ? td({ 'data-label': i18n.peerHost || 'IP', class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR')) + : td({ 'data-label': i18n.peerHost || 'IP', title: unspecifiedHost(r.host) ? '' : String(r.host || ''), class: 'peer-host' }, r.host && !unspecifiedHost(r.host) ? shortHost(r.host) : '—'), + td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)), + td({ 'data-label': i18n.graphosFedTransport }, r.tor ? renderStateChip('fediverse', null, 'TOR') : renderStateChip('transport', null, String(i18n.graphosFedClearnet).toUpperCase())), + td({ 'data-label': i18n.peerStateLabel }, healthChip(r)), + td({ 'data-label': i18n.peersHealthFailures }, String(Number(r.failures) || 0)), + td({ 'data-label': i18n.peersFailingSince }, r.failingSince ? fmtDate(r.failingSince) : '—'), + healthVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null + )) + ) + : p(i18n.noConnections) + ); + + const lanRows = lan && Array.isArray(lan.rows) ? lan.rows : []; + const lanOn = !!(lan && lan.broadcasting); + const lanVersion = knowsVersion(lanRows.map(r => r.key)); + const lanBlock = div({ class: 'peers-technical-block peers-lan-block' }, + peersToolbar('lan', lanRows.length, [ + { label: i18n.online, count: lanRows.filter(r => r.online).length }, + { label: i18n.peerOutdated, count: lanRows.filter(r => outdatedKey(r.key)).length } + ]), + div({ class: "tags-header" }, h2(withCount(i18n.peersTabLan, lanRows.length)), p(i18n.peersLanHint)), + div({ class: "peers-pause-row" }, + p({ class: "peers-pause-hint" }, `${i18n.settingsLanTitle}: `, renderStateChip(lanOn ? 'mutuals' : 'hidden', null, String(lanOn ? i18n.modulesEnabledModulesLabel : i18n.modulesDisabledModulesLabel).toUpperCase())) + ), + lanRows.length + ? table({ class: 'block-info-table' }, + tr( + td({ class: 'card-label' }, i18n.peersOasisId || 'ID'), + td({ class: 'card-label' }, i18n.peerStateLabel), + td({ class: 'card-label' }, i18n.peersLanLastSeen), + lanVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null, + td({ class: 'card-label' }, '') + ), + ...lanRows.map(r => tr( + td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)), + td({ 'data-label': i18n.peerStateLabel }, r.online ? renderStateChip('mutuals', null, String(i18n.online).toUpperCase()) : renderStateChip('hidden', null, String(i18n.offline).toUpperCase())), + td({ 'data-label': i18n.peersLanLastSeen }, fmtDate(r.seenAt)), + lanVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null, + td(r.online ? null : form({ method: "POST", action: "/peers/lan/forget", class: "inline-form" }, + input({ type: "hidden", name: "key", value: r.key }), + button({ type: "submit", class: "filter-btn" }, i18n.delete) + )) + )) + ) + : p(i18n.peersLanEmpty) + ); + + const tabs = ['status', 'connections'].concat(advanced ? ['lan', 'health'] : []); + const tab = tabs.includes(filter) ? filter : 'status'; + const tabLabel = { status: i18n.status || 'Status', connections: i18n.peerConnectionsTitle || 'Connections', lan: i18n.peersTabLan, health: i18n.peersTabHealth }; + const statusToolbar = peersToolbar('status', onlineCount + discoveredCount + unknownCount, [ + { label: i18n.online, count: onlineCount }, + { label: i18n.discovered, count: discoveredCount }, + { label: i18n.unknown, count: unknownCount } + ]); + const statusBlock = (onlineCount + discoveredCount + unknownCount) > 0 + ? div({ class: "peers-list" }, + statusToolbar, + ...[ + [i18n.online, onlineCount, dedupOnline, 'noConnections', i18n.peersOnlineHint], + [i18n.discovered, discoveredCount, dedupDiscovered, 'noDiscovered', i18n.peersDiscoveredHint], + [i18n.unknown, unknownCount, dedupUnknown, 'noUnknownPeers', i18n.peersUnknownHint] + ].filter(([, count]) => count > 0).flatMap(([title, count, list, emptyKey, hint], idx) => [ + idx ? hr() : null, + div({ class: "tags-header" }, h2(withCount(title, count)), p(hint)), + renderPeerTable(list, emptyKey) + ]) + ) + : div({ class: "peers-list" }, statusToolbar, p(i18n.noConnections)); + return template( i18n.peers, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.peers), p(i18n.peerConnectionsIntro) ), - div({ class: "conn-actions" }, ...connButtons), - (onlineCount + discoveredCount + unknownCount) > 0 - ? div({ class: "peers-list" }, - div({ class: "tags-header" }, h2(`${i18n.online} (${onlineCount})`)), - renderPeerTable(dedupOnline, 'noConnections'), - hr(), - div({ class: "tags-header" }, h2(`${i18n.discovered} (${discoveredCount})`)), - renderPeerTable(dedupDiscovered, 'noDiscovered'), - hr(), - div({ class: "tags-header" }, h2(`${i18n.unknown} (${unknownCount})`)), - renderPeerTable(dedupUnknown, 'noUnknownPeers') - ) - : null, - peersTechnicalBlock, - p(i18n.connectionActionIntro) + div({ class: 'mode-buttons-row' }, + ...tabs.map(t => a({ class: tab === t ? 'filter-btn active' : 'filter-btn', href: `/peers?filter=${t}` }, String(tabLabel[t]).toUpperCase())) + ), + tab === 'status' ? statusBlock : null, + tab === 'connections' ? peersTechnicalBlock : null, + tab === 'connections' ? p(i18n.connectionActionIntro) : null, + tab === 'lan' ? lanBlock : null, + tab === 'health' ? healthBlock : null ) ); }; diff --git a/src/views/phone_view.js b/src/views/phone_view.js new file mode 100644 index 0000000..96acdfd --- /dev/null +++ b/src/views/phone_view.js @@ -0,0 +1,299 @@ +const { div, h2, p, section, button, form, input, span, audio, table, tr, td, th, br, a } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, userLinkLabel, renderCallButton, liveClock, renderStateChip, renderVideoGrid, renderIcon } = require("./main_views"); +const { renderEncryptedChip } = require("./clearnet_view"); +const { phoneNumberOf } = require("../models/phone_number"); +const moment = require("../server/node_modules/moment"); + +const PAM_MAX_MS = require('../server/phone_module').VOICEMAIL_MAX_MS; +const GROUP_MAX = require('../server/phone_module').GROUP_MAX; + +const clock = (ms) => { + const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000)); + return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; +}; + +const postButton = (action, label, danger = false, fields = {}) => + form({ method: 'POST', action, class: 'phone-action-form' }, + ...Object.entries(fields).map(([name, value]) => input({ type: 'hidden', name, value: String(value) })), + button({ type: 'submit', class: danger ? 'tribe-action-btn danger-btn' : 'tribe-action-btn' }, String(label).toUpperCase()) + ); + +const linkList = (ids) => ids.reduce((acc, id, i) => i > 0 ? [...acc, ', ', userLink(id)] : [userLink(id)], []); +const peersOf = (h) => (Array.isArray(h.peers) && h.peers.length ? h.peers : [h.peer]).filter(Boolean); + +const OUTCOME_LABEL = { + ended: () => i18n.phoneOutcomeEnded, + missed: () => i18n.phoneOutcomeMissed, + rejected: () => i18n.phoneOutcomeRejected, + noanswer: () => i18n.phoneOutcomeNoanswer, + cancelled: () => i18n.phoneOutcomeCancelled, + pam: () => i18n.phoneOutcomePam, + failed: () => i18n.phoneOutcomeFailed +}; + +const renderCallPanel = (st, now) => { + if (!st) return null; + const who = st.group ? linkList(st.peers.map(x => x.id)) : [userLink(st.peer)]; + const line = (...parts) => div({ class: 'phone-call-who' }, span({ class: 'phone-call-icon' }, '✆'), ...parts); + const actions = (...btns) => div({ class: 'phone-call-actions' }, ...btns); + let body; + if (st.dir === 'in' && st.phase === 'incoming') { + body = [line(...who, ' ', i18n.phoneIsCalling), actions(process.env.OASIS_MOBILE === '1' ? postButton('/phone/accept', '\u2706 ' + i18n.phoneEarpiece) : postButton('/phone/accept', i18n.phoneAnswer), process.env.OASIS_MOBILE === '1' ? a({ href: '/phone/accept-speaker', class: 'tribe-action-btn phone-speaker-btn' }, span({ class: 'phone-ico' }, '\ud83d\udd0a'), ' ' + String(i18n.phoneSpeaker).toUpperCase()) : null, postButton('/phone/reject', i18n.phoneReject, true))]; + } else if (st.phase === 'calling') { + body = [line(st.reached ? i18n.phoneCalling : i18n.phoneConnecting, ' ', ...who, ' ', span({ class: 'phone-call-clock' }, liveClock(st.startedAt))), actions(postButton('/phone/hangup', i18n.phoneHangup, true))]; + } else if (st.phase === 'connecting' || st.phase === 'connected') { + const inCall = st.group ? linkList(st.peers.filter(x => x.phase === 'connected').map(x => x.id)) : who; + const ringing = st.group ? st.peers.filter(x => x.phase === 'calling').map(x => x.id) : []; + const silenceButton = (id, on) => on + ? postButton('/phone/silence', i18n.phoneUnsilence, true, { id, on: 0 }) + : postButton('/phone/silence', i18n.phoneSilence, false, { id, on: 1 }); + const peerRow = (pr) => div({ class: 'phone-contact room-peer' }, + span({ class: 'phone-call-icon' }, '✆'), + userLink(pr.id), + silenceButton(pr.id, pr.silenced) + ); + body = [ + line(...inCall, ' ', span({ class: 'phone-call-clock' }, liveClock(st.answeredAt || st.startedAt))), + ringing.length ? div({ class: 'phone-call-ringing' }, i18n.phoneCalling, ' ', ...linkList(ringing)) : null, + st.group ? div({ class: 'phone-contacts phone-call-peers' }, ...st.peers.filter(x => x.phase === 'connected').map(peerRow)) : null, + st.video ? renderVideoGrid([{ id: st.peer, on: st.peerCam }], st.cam) : null, + actions( + st.muted ? postButton('/phone/mute', i18n.phoneUnmute, false, { mute: 0 }) : postButton('/phone/mute', i18n.phoneMute, false, { mute: 1 }), + st.video ? (st.cam ? postButton('/phone/camera', i18n.phoneCameraOff, false, { cam: 0 }) : postButton('/phone/camera', i18n.phoneCameraOn, false, { cam: 1 })) : null, + st.group ? null : silenceButton(st.peer, st.silenced), + process.env.OASIS_MOBILE === '1' ? (() => { let on = false; try { on = require('fs').readFileSync(require('path').join(__dirname, '..', '..', '.audio_speaker'), 'utf8').trim() === '1'; } catch (_) {} return on ? a({ href: '/phone/speaker?on=0', class: 'tribe-action-btn phone-speaker-btn' }, '\u2706 ' + String(i18n.phoneEarpiece).toUpperCase()) : a({ href: '/phone/speaker?on=1', class: 'tribe-action-btn phone-speaker-btn' }, span({ class: 'phone-ico' }, '\ud83d\udd0a'), ' ' + String(i18n.phoneSpeaker).toUpperCase()); })() : null, + postButton('/phone/hangup', i18n.phoneHangup, true) + ) + ]; + } else if (st.phase === 'recording' || st.phase === 'recorded') { + const elapsed = st.phase === 'recorded' ? clock(PAM_MAX_MS) : liveClock(st.recordingStartedAt || now, { max: PAM_MAX_MS }); + body = [line(i18n.phoneRecording, ' ', ...who, ' ', span({ class: 'phone-call-clock' }, elapsed, ` / ${clock(PAM_MAX_MS)}`)), p({ class: 'phone-pam-hint' }, st.why === 'busy' ? i18n.phoneLeaveMessageBusy : st.why === 'unreachable' ? i18n.phoneLeaveMessageUnreachable : i18n.phoneLeaveMessage), actions(postButton('/phone/pam/send', i18n.phoneSendPam), postButton('/phone/pam/cancel', i18n.phoneCancel, true))]; + } else { + return null; + } + return div({ class: 'phone-call-panel' }, + div({ class: 'room-live-head' }, + div({ class: 'room-live-head-main' }, + div({ class: 'card-chips-row' }, renderEncryptedChip(i18n), st.group ? span({ class: 'pm-exposition-chip phone-joint-chip' }, span({ class: 'pm-exposition-text' }, i18n.phoneJointCall)) : null) + ), + (process.env.OASIS_MOBILE === '1' && (st.phase === 'incoming' || st.phase === 'connecting' || st.phase === 'connected')) ? null : a({ href: '/phone', class: 'tribe-action-btn room-refresh-btn' }, String(i18n.liveRefresh).toUpperCase()) + ), + ...body + ); +}; + +const dndChip = () => renderStateChip('hidden', null, String(i18n.phoneDnd).toUpperCase()); + +const renderNumberChooser = (compose) => + div({ class: 'phone-number-chooser' }, + p(String(i18n.phoneNumberChoose || '').replace('{number}', compose.number || '')), + div({ class: 'phone-contacts' }, + ...(compose.candidates || []).map(c => div({ class: 'phone-contact' }, + userLink(c.id), + span({ class: 'date-link' }, c.id), + c.dnd ? dndChip() : postButton('/phone/call', `✆ ${i18n.phoneCallButton}`, false, { to: c.to, compose: 1 }) + )), + ...(compose.rooms || []).map(r => div({ class: 'phone-contact' }, + a({ href: `/rooms/${encodeURIComponent(r.id)}`, class: 'user-link' }, r.title || i18n.roomsTitle), + span({ class: 'date-link' }, i18n.roomsTitle), + postButton(`/rooms/join/${encodeURIComponent(r.id)}`, `ꘒ ${i18n.roomJoin}`) + )) + ) + ); + +const DIAL_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; +const NUMBER_TOKEN = /^\d{3}-\d{3}$/; + +const renderCompose = (compose) => { + const recipients = Array.isArray(compose.recipients) ? compose.recipients : []; + const max = Number(compose.max) || GROUP_MAX; + const dial = String(compose.dial || ''); + const full = recipients.length >= max; + const keep = () => [input({ type: 'hidden', name: 'filter', value: 'create' }), input({ type: 'hidden', name: 'to', value: compose.to || '' })]; + const key = (value, label, cls = 'phone-key', extra = {}) => button({ type: 'submit', name: 'key', value, class: cls, ...extra }, label); + return section( + section({ class: 'phone-flash-anchor' }), + div({ class: 'pm-form phone-compose' }, + compose.choose && ((compose.candidates || []).length || (compose.rooms || []).length) ? renderNumberChooser(compose) : null, + form({ method: 'GET', action: '/phone', class: 'phone-dialer', id: 'phone-dialer' }, + ...keep(), + key('add', `+ ${i18n.phoneDialAdd}`, 'phone-dial-add', full ? { disabled: true } : {}), + div({ class: 'phone-screen' }, + input({ type: 'text', name: 'dial', value: dial, placeholder: i18n.phoneDialPlaceholder, maxlength: '64', autocomplete: 'off', ...(dial ? {} : { autofocus: true }) }) + ), + div({ class: 'phone-dialpad' }, + ...DIAL_KEYS.map(d => key(d, d)), + key('back', '⌫', 'phone-key phone-key-fn', { title: i18n.phoneDialBack }), + key('0', '0'), + key('clear', '✕', 'phone-key phone-key-fn', { title: i18n.phoneDialClear }) + ) + ), + p({ class: 'phone-empty phone-dial-hint' }, String(i18n.phoneLimitsHint).replace('{n}', String(max))), + div({ class: 'phone-recipients' }, + span({ class: 'card-label' }, `${i18n.phoneDialInhabitants}: ${recipients.length}/${max}`), + recipients.length ? div({ class: 'phone-recipient-list' }, + ...recipients.map((r, i) => span({ class: 'phone-recipient' }, + NUMBER_TOKEN.test(r) ? span(`✆ ${r}`) : [userLink(r), span(`✆ ${phoneNumberOf(r)}`)], + form({ method: 'GET', action: '/phone' }, + ...keep(), + input({ type: 'hidden', name: 'dial', value: dial }), + button({ type: 'submit', name: 'key', value: `remove:${i}`, class: 'phone-recipient-remove', title: i18n.phoneDialRemove }, '✕') + ) + )) + ) : null + ), + div({ class: 'phone-dial-call' }, + button({ type: 'submit', form: 'phone-dialer', formaction: '/phone/call', attrs: { formmethod: 'post' }, name: 'compose', value: '1', class: 'pm-btn' }, `✆ ${String(i18n.phoneCallButton).toUpperCase()}`) + ) + ) + ); +}; + +const renderRecords = (pams) => + div({ class: 'phone-block' }, + h2(i18n.phonePamTitle), + pams.length + ? div({ class: 'message-list' }, + ...pams.map(m => div({ class: 'pm-card normal-pm phone-record' }, + table({ class: 'pm-info-table' }, + tr(td({ class: 'card-label' }, i18n.pmFromLabel || 'From:'), td({ class: 'card-value' }, userLink(m.from))), + tr(td({ class: 'card-label' }, i18n.privateDate || 'Date'), td({ class: 'card-value' }, moment(m.sentAt).format('YYYY/MM/DD HH:mm:ss'))), + tr( + td({ class: 'card-label' }, i18n.phoneDuration), + td({ class: 'card-value pm-subject-cell' }, + span(clock(m.durationSec * 1000)), + m.heard ? null : span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.phoneNew)) + ) + ), + tr(td({ class: 'card-label' }, i18n.pmEncryptionLabel || 'Encryption'), td({ class: 'card-value pm-encryption-cell' }, renderEncryptedChip(i18n))) + ), + m.ready === false + ? div({ class: 'phone-record-pending' }, span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.torrentStateDownloading))) + : audio({ controls: true, preload: 'metadata', src: `/phone/pam/${encodeURIComponent(m.key)}/audio` }), + div({ class: 'pm-actions' }, + renderCallButton(m.from, { upper: true, cls: 'pm-btn' }), + form({ method: 'POST', action: `/phone/pam/${encodeURIComponent(m.key)}/delete`, class: 'pm-action-form' }, + button({ type: 'submit', class: 'pm-btn delete-btn danger-btn' }, String(i18n.phoneDelete).toUpperCase()) + ) + ) + )) + ) + : p({ class: 'phone-empty' }, i18n.phoneNoPams) + ); + +const deleteCalls = (ids) => ids.length + ? form({ method: 'POST', action: '/phone/delete-shown', class: 'content-action-form phone-row-delete' }, + ...ids.map(id => input({ type: 'hidden', name: 'calls', value: id })), + button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.phoneDelete }, renderIcon('close')) + ) + : null; + +const renderLatest = (latest, canCall, history = []) => + div({ class: 'phone-block' }, + h2(i18n.phoneLatestTitle), + latest.length + ? div({ class: 'phone-contacts' }, + ...latest.map(c => div({ class: 'phone-contact phone-latest-row' }, + span({ class: 'phone-dir', title: c.dir === 'out' ? i18n.phoneOutgoing : i18n.phoneIncoming }, c.dir === 'out' ? '↗' : '↙'), + userLink(c.id), + span({ class: 'date-link' }, moment(c.startedAt).format('YYYY/MM/DD HH:mm')), + c.dnd ? dndChip() : canCall ? renderCallButton(c.id) : null, + deleteCalls(history.filter(h => peersOf(h).includes(c.id)).map(h => h.id)) + )) + ) + : p({ class: 'phone-empty' }, i18n.phoneNoHistory) + ); + +const renderHistory = (items) => + div({ class: 'phone-block' }, + h2(i18n.phoneHistoryTitle), + items.length + ? table({ class: 'phone-history' }, + tr(th(''), th(i18n.phoneWith), th(i18n.phoneResult), th(i18n.phoneWhen), th(i18n.phoneDuration), th('')), + ...items.map(h => tr( + td({ class: 'phone-dir', title: h.dir === 'out' ? i18n.phoneOutgoing : i18n.phoneIncoming }, h.dir === 'out' ? '↗' : '↙'), + td({ 'data-label': i18n.phoneWith }, ...linkList(peersOf(h))), + td({ 'data-label': i18n.phoneResult }, (OUTCOME_LABEL[h.outcome] || OUTCOME_LABEL.failed)()), + td({ 'data-label': i18n.phoneWhen }, moment(h.startedAt).format('YYYY/MM/DD HH:mm')), + td({ 'data-label': i18n.phoneDuration }, h.answeredAt ? clock(h.endedAt - h.answeredAt) : '—'), + td({ class: 'phone-row-actions' }, deleteCalls([h.id])) + )) + ) + : p({ class: 'phone-empty' }, i18n.phoneNoHistory) + ); + +exports.phoneView = ({ available = false, state = null, pams = [], history = [], latest = [], q = '', filter = 'all', compose = {}, now = Date.now(), refused = [] } = {}) => { + const qNorm = String(q || '').trim().toLowerCase(); + const matchId = (id) => !qNorm || [id, userLinkLabel(id)].some(v => String(v || '').toLowerCase().includes(qNorm)); + const isMissed = (h) => h.dir === 'in' && h.outcome === 'missed'; + const counts = { + all: pams.length + history.length, + records: pams.length, + missed: history.filter(isMissed).length, + incoming: history.filter(h => h.dir === 'in').length, + outgoing: history.filter(h => h.dir === 'out').length + }; + const records = pams.filter(m => matchId(m.from)); + const calls = history.filter(h => peersOf(h).some(matchId)); + const shownCalls = filter === 'missed' ? calls.filter(isMissed) + : filter === 'incoming' ? calls.filter(h => h.dir === 'in') + : filter === 'outgoing' ? calls.filter(h => h.dir === 'out') + : calls; + const showRecords = filter === 'all' || filter === 'records'; + const showCalls = filter === 'all' || filter === 'missed' || filter === 'incoming' || filter === 'outgoing'; + const unheard = showRecords ? records.filter(m => !m.heard).map(m => m.key) : []; + const deletable = [...(showRecords ? records.map(m => m.key) : []), ...(showCalls ? shownCalls.map(h => h.id) : [])]; + const canCall = available && !state; + const empty = counts.all === 0; + const filterBtn = (value, label, count) => + !empty && (value === 'all' || count > 0 || filter === value) + ? button({ type: 'submit', name: 'filter', value, class: filter === value ? 'filter-btn active' : 'filter-btn' }, `${String(label).toUpperCase()} (${count})`) + : null; + + return template( + i18n.phoneTitle, + section( + div({ class: 'tags-header module-header-line' }, + h2(i18n.phoneTitle), + p(i18n.phoneDescription), + renderEncryptedChip(i18n) + ), + available ? null : div({ class: 'pm-form-error-msg' }, p('✗ ' + i18n.phoneUnavailable)), + refused.length ? div({ class: 'phone-refused' }, span(i18n.phoneRefusedNotice), ' ', ...linkList(refused)) : null, + state ? renderCallPanel(state, now) : null, + div({ class: 'filters' }, + form({ method: 'GET', action: '/phone' }, + filterBtn('all', i18n.allButton || 'ALL', counts.all), + filterBtn('records', i18n.phonePamTitle, counts.records), + filterBtn('missed', i18n.phoneOutcomeMissed, counts.missed), + filterBtn('incoming', i18n.phoneIncoming, counts.incoming), + filterBtn('outgoing', i18n.phoneOutgoing, counts.outgoing), + canCall ? button({ type: 'submit', name: 'filter', value: 'create', class: 'create-button' }, i18n.phoneCreateButton) : null + ) + ), + (filter === 'create' || empty) ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + (unheard.length || deletable.length) ? div({ class: 'pm-exposition inbox-exposition' }, + span({ class: 'inbox-filters-label' }, i18n.inboxFiltersLabel || 'Filters:'), + unheard.length ? form({ method: 'POST', action: '/phone/heard-all', class: 'inbox-vis-toggle' }, + ...unheard.map(k => input({ type: 'hidden', name: 'keys', value: k })), + button({ type: 'submit', class: 'btn' }, `${i18n.inboxMarkAllRead} (${unheard.length})`) + ) : null, + deletable.length ? form({ method: 'POST', action: '/phone/delete-shown', class: 'inbox-vis-toggle inbox-bulk-delete' }, + ...(showRecords ? records.map(m => input({ type: 'hidden', name: 'keys', value: m.key })) : []), + ...(showCalls ? shownCalls.map(h => input({ type: 'hidden', name: 'calls', value: h.id })) : []), + button({ type: 'submit', class: 'btn delete-btn' }, `${String(i18n.inboxDeleteShown).toUpperCase()} (${deletable.length})`) + ) : null + ) : null, + form({ method: 'GET', action: '/phone', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: String(q || ''), placeholder: i18n.phoneSearchPlaceholder, class: 'filter-box__input' }), + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) + ), + filter === 'create' && canCall ? renderCompose(compose) : null, + showRecords ? renderRecords(records) : null, + filter === 'all' ? renderLatest(latest.filter(c => matchId(c.id)), canCall, history) : null, + showCalls ? renderHistory(shownCalls) : null + ) + ); +}; diff --git a/src/views/pixelia_view.js b/src/views/pixelia_view.js index 8cc81ae..d42de5e 100644 --- a/src/views/pixelia_view.js +++ b/src/views/pixelia_view.js @@ -1,7 +1,7 @@ const { div, h2, p, section, form, input, label, select, option, button, table, tr, td, hr, ul, li, a, br } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink} = require('./main_views'); +const { template, i18n, userLink, renderDocumentActions } = require('./main_views'); -exports.pixeliaView = (pixelArt, errorMessage) => { +exports.pixeliaView = (pixelArt) => { const title = i18n.pixeliaTitle; const description = i18n.pixeliaDescription; const gridWidth = 50; @@ -18,7 +18,7 @@ exports.pixeliaView = (pixelArt, errorMessage) => { return td( { id: cellId, - title: pixel ? `By: ${pixel.author}` : "", + title: pixel ? `${pixel.author}` : "", class: `pixel-cell ${colorClass}` }, "" @@ -33,7 +33,7 @@ exports.pixeliaView = (pixelArt, errorMessage) => { return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(description) ) @@ -47,27 +47,26 @@ exports.pixeliaView = (pixelArt, errorMessage) => { input({ type: "number", id: "y", name: "y", min: 1, max: gridHeight, required: true }), label({ for: "color" }, i18n.colorLabel), select({ id: "color", name: "color", required: true }, - option({ value: "#000000", style: "background-color:#000000;" }, "Black"), - option({ value: "#ffffff", style: "background-color:#ffffff;" }, "White"), - option({ value: "#17f018", style: "background-color:#17f018;" }, "Green"), - option({ value: "#ffbb00", style: "background-color:#ffbb00;" }, "Yellow"), - option({ value: "#ff0000", style: "background-color:#ff0000;" }, "Red"), - option({ value: "#0000ff", style: "background-color:#0000ff;" }, "Blue"), - option({ value: "#ffff00", style: "background-color:#ffff00;" }, "Lime"), - option({ value: "#00ff00", style: "background-color:#00ff00;" }, "Spring Green"), - option({ value: "#00ffff", style: "background-color:#00ffff;" }, "Aqua"), - option({ value: "#ff00ff", style: "background-color:#ff00ff;" }, "Fuchsia"), - option({ value: "#a52a2a", style: "background-color:#a52a2a;" }, "Brown"), - option({ value: "#800080", style: "background-color:#800080;" }, "Purple"), - option({ value: "#808000", style: "background-color:#808000;" }, "Olive"), - option({ value: "#00bfff", style: "background-color:#00bfff;" }, "Deep Sky Blue"), - option({ value: "#d3d3d3", style: "background-color:#d3d3d3;" }, "Light Grey"), - option({ value: "#ff6347", style: "background-color:#ff6347;" }, "Tomato") + option({ value: "#000000", class: "pixelia-swatch pixelia-swatch-000000" }, "Black"), + option({ value: "#ffffff", class: "pixelia-swatch pixelia-swatch-ffffff" }, "White"), + option({ value: "#17f018", class: "pixelia-swatch pixelia-swatch-17f018" }, "Green"), + option({ value: "#ffbb00", class: "pixelia-swatch pixelia-swatch-ffbb00" }, "Yellow"), + option({ value: "#ff0000", class: "pixelia-swatch pixelia-swatch-ff0000" }, "Red"), + option({ value: "#0000ff", class: "pixelia-swatch pixelia-swatch-0000ff" }, "Blue"), + option({ value: "#ffff00", class: "pixelia-swatch pixelia-swatch-ffff00" }, "Lime"), + option({ value: "#00ff00", class: "pixelia-swatch pixelia-swatch-00ff00" }, "Spring Green"), + option({ value: "#00ffff", class: "pixelia-swatch pixelia-swatch-00ffff" }, "Aqua"), + option({ value: "#ff00ff", class: "pixelia-swatch pixelia-swatch-ff00ff" }, "Fuchsia"), + option({ value: "#a52a2a", class: "pixelia-swatch pixelia-swatch-a52a2a" }, "Brown"), + option({ value: "#800080", class: "pixelia-swatch pixelia-swatch-800080" }, "Purple"), + option({ value: "#808000", class: "pixelia-swatch pixelia-swatch-808000" }, "Olive"), + option({ value: "#00bfff", class: "pixelia-swatch pixelia-swatch-00bfff" }, "Deep Sky Blue"), + option({ value: "#d3d3d3", class: "pixelia-swatch pixelia-swatch-d3d3d3" }, "Light Grey"), + option({ value: "#ff6347", class: "pixelia-swatch pixelia-swatch-ff6347" }, "Tomato") ), - button({ type: "submit" }, i18n.paintButton) + button({ type: "submit", class: "filter-btn" }, String(i18n.paintButton).toUpperCase()) ) ), - errorMessage ? div({ class: "error-message" }, errorMessage) : null, div({ class: "total-pixels" }, h2(`${i18n.totalPixels}: ${pixelArt.length}`) ) @@ -76,6 +75,7 @@ exports.pixeliaView = (pixelArt, errorMessage) => { section( div({ class: "main_content" }, div({ class: "pixelia-grid-wrap" }, grid), + pixelArt.length > 0 ? renderDocumentActions('pixelia', null) : null, pixelArt.length > 0 ? div({ class: "contributors" }, h2(i18n.contributorsTitle), diff --git a/src/views/pm_view.js b/src/views/pm_view.js index 3612627..e31a20c 100644 --- a/src/views/pm_view.js +++ b/src/views/pm_view.js @@ -1,26 +1,25 @@ -const { div, h2, p, section, button, form, input, textarea, br, label, pre, span, strong } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, input, textarea, br, label, pre, span, strong, a, select, option, datalist } = require("../server/node_modules/hyperaxe"); +const { TEXT_CAP } = require('../backend/long_text'); const { template, i18n } = require('./main_views'); const { getConfig } = require('../configs/config-manager.js'); -exports.pmView = async (initialRecipients = '', initialSubject = '', initialText = '', showPreview = false, sentKey = '', crypterError = false, crypterPreview = null, recipientError = false) => { +exports.pmView = async (initialRecipients = '', initialSubject = '', initialText = '', showPreview = false, sentKey = '', crypterError = false, crypterPreview = null, recipientError = false, fileError = '', fileSharePreview = null, opts = {}) => { + const mailingLists = Array.isArray(opts.lists) ? opts.lists : []; + const selectedList = String(opts.selectedList || ''); const title = i18n.pmSendTitle; const description = i18n.pmDescription; const textLen = (initialText || '').length; + const { renderEncryptedChip, renderDoubleEncryptionChip } = require('./clearnet_view'); return template( title, section( - div({ class: "tags-header" }, - div({ class: "title-with-chip" }, h2(title), renderEncryptedChip(i18n)), - p(description) + div({ class: "tags-header module-header-line" }, + h2(title), + p(description), + renderEncryptedChip(i18n) ), - crypterError - ? div({ class: "pm-form-error-msg" }, p('✗ ' + i18n.pmCrypterTooLong)) - : null, - recipientError - ? div({ class: "pm-form-error-msg" }, p('✗ ' + i18n.pmInvalidRecipients)) - : null, sentKey ? div({ class: "pm-sent-key" }, input({ type: "text", readonly: true, value: sentKey, class: "pm-sent-key-value" }) @@ -28,35 +27,54 @@ exports.pmView = async (initialRecipients = '', initialSubject = '', initialText : null, section( div({ class: "pm-form" }, - form({ method: "POST", action: "/pm", id: "pm-form" }, - label({ for: "recipients" }, i18n.pmLimitsHint), - br(), + h2({ class: "pm-section-title" }, i18n.pmComposeTitle || 'Send a message'), + form({ method: "POST", action: "/pm/preview", id: "pm-form" }, input({ type: "text", name: "recipients", - placeholder: i18n.pmRecipientsHint, - required: true, + placeholder: `${i18n.pmRecipientsHint}. ${i18n.pmLimitsHint}`, + ...(mailingLists.length ? {} : { required: true }), value: initialRecipients, maxlength: "511" }), br(), + mailingLists.length + ? div({ class: "pm-list-row" }, + label({ for: "pm-list" }, i18n.pmListLabel), + br(), + mailingLists.length <= 12 + ? select({ name: "list", id: "pm-list" }, + option({ value: "" }, i18n.pmListNone), + mailingLists.map(l => + option({ value: l.target, ...(selectedList === l.target ? { selected: true } : {}) }, `${l.label} (${l.count})`) + ) + ) + : div( + input({ type: "text", name: "list", id: "pm-list", list: "pm-list-options", value: (mailingLists.find(l => l.target === selectedList) || {}).label || selectedList, placeholder: i18n.pmListNone }), + datalist({ id: "pm-list-options" }, + mailingLists.map(l => option({ value: l.label }, `(${l.count})`)) + ) + ), + br() + ) + : null, label({ for: "subject" }, i18n.pmSubject), br(), input({ type: "text", name: "subject", placeholder: i18n.pmSubjectHint, value: initialSubject, maxlength: "150" }), br(), label({ for: "text" }, i18n.pmText), br(), - textarea({ name: "text", rows: "6", cols: "50", id: "pm-text", maxlength: "8096" }, initialText), + textarea({ name: "text", rows: "6", cols: "50", id: "pm-text", maxlength: String(TEXT_CAP), placeholder: i18n.pmTextPlaceholder || '' }, initialText), div({ class: "pm-crypter-row" }, label({ for: "pm-crypter" }, - input({ type: "checkbox", name: "crypter", value: "1", id: "pm-crypter" }), + input({ type: "checkbox", name: "crypter", value: "1", id: "pm-crypter", ...(crypterPreview ? { checked: true } : {}) }), renderDoubleEncryptionChip(i18n) ) ), div({ class: "pm-actions-block" }, div({ class: "pm-actions" }, - button({ type: "submit", formaction: "/pm/preview", formmethod: "POST" }, i18n.pmPreview), - button({ type: "submit", class: "btn-compact" }, i18n.pmSend) + button({ type: "submit", class: "pm-btn" }, i18n.pmPreview), + button({ type: "reset", class: "pm-btn danger-btn" }, i18n.pmReset || 'Reset') ) ) ), @@ -66,31 +84,112 @@ exports.pmView = async (initialRecipients = '', initialSubject = '', initialText div({ class: "title-with-chip" }, h2(i18n.pmPreviewTitle), renderEncryptedChip(i18n)), p({ class: "pm-preview-count" }, `${(initialText || '').length} ${i18n.pmCrypterCharsLabel}`), div({ class: "pm-preview-content" }, pre({ class: "pm-pre" }, initialText || '')), + div({ class: "title-with-chip" }, h2(i18n.pmCrypterCipherLabel), renderDoubleEncryptionChip(i18n)), + p({ class: "pm-key-hint" }, i18n.pmSharedKeyHint), div({ class: "pm-sent-key" }, input({ type: "text", readonly: true, value: crypterPreview.key, class: "pm-sent-key-value" }) ), - div({ class: "title-with-chip" }, h2(i18n.pmCrypterCipherLabel), renderDoubleEncryptionChip(i18n)), p({ class: "pm-preview-count" }, `${(crypterPreview.cipher || '').length} ${i18n.pmCrypterCharsLabel}`), div({ class: "pm-preview-content" }, pre({ class: "pm-pre" }, crypterPreview.cipher || '')), form({ method: "POST", action: "/pm", class: "pm-crypter-send-form" }, input({ type: "hidden", name: "recipients", value: initialRecipients }), + input({ type: "hidden", name: "list", value: selectedList }), input({ type: "hidden", name: "subject", value: initialSubject }), input({ type: "hidden", name: "text", value: initialText }), input({ type: "hidden", name: "crypter", value: "1" }), input({ type: "hidden", name: "crypterKey", value: crypterPreview.key }), input({ type: "hidden", name: "precomputed", value: crypterPreview.cipher }), - button({ type: "submit", class: "btn-compact" }, i18n.pmSend) + div({ class: "pm-actions" }, + button({ type: "submit", class: "pm-btn" }, i18n.pmSend), + a({ href: "/pm", class: "pm-btn danger-btn" }, i18n.pmCancel || 'Cancel') + ) ) ) : div({ id: "pm-preview-area", class: "pm-preview" }, div({ class: "title-with-chip" }, h2(i18n.pmPreviewTitle), renderEncryptedChip(i18n)), - p({ id: "pm-preview-count", class: "pm-preview-count" }, `${textLen}/8096`), + p({ id: "pm-preview-count", class: "pm-preview-count" }, `${textLen}/7000`), div({ id: "pm-preview-content", class: "pm-preview-content" }, pre({ class: "pm-pre" }, initialText || '') + ), + form({ method: "POST", action: "/pm", class: "pm-send-form" }, + input({ type: "hidden", name: "recipients", value: initialRecipients }), + input({ type: "hidden", name: "list", value: selectedList }), + input({ type: "hidden", name: "subject", value: initialSubject }), + input({ type: "hidden", name: "text", value: initialText }), + div({ class: "pm-actions" }, + button({ type: "submit", class: "pm-btn" }, i18n.pmSend), + a({ href: "/pm", class: "pm-btn danger-btn" }, i18n.pmCancel || 'Cancel') + ) ) )) : null ) + ), + section({ id: "fileshare" }, + div({ class: "pm-form pm-fileshare-form-wrap" }, + h2({ class: "pm-section-title" }, i18n.fileShareTitle || 'Share a file'), + form({ method: "POST", action: "/pm/file/preview#fileshare", enctype: "multipart/form-data", class: "pm-fileshare-form" }, + input({ id: "fs-recipient", type: "text", name: "recipient", placeholder: i18n.fileShareRecipientPlaceholder || 'Enter Oasis ID (@....ed25519)', required: true, value: initialRecipients, maxlength: "120" }), + br(), + label({ for: "fs-subject" }, i18n.pmSubject), + br(), + input({ id: "fs-subject", type: "text", name: "subject", placeholder: i18n.pmSubjectHint, value: initialSubject, maxlength: "150" }), + br(), + label({ for: "fs-file" }, i18n.fileShareFileLabel || 'File'), + br(), + input({ id: "fs-file", type: "file", name: "file", required: true }), + div({ class: "pm-crypter-row" }, + label({ for: "fs-crypter" }, + input({ type: "checkbox", name: "crypter", value: "1", id: "fs-crypter", ...(fileSharePreview && fileSharePreview.crypter ? { checked: true } : {}) }), + renderDoubleEncryptionChip(i18n) + ) + ), + div({ class: "pm-actions-block" }, + div({ class: "pm-actions" }, + button({ type: "submit", class: "pm-btn" }, i18n.pmPreview), + button({ type: "reset", class: "pm-btn danger-btn" }, i18n.pmReset || 'Reset') + ) + ) + ), + fileSharePreview + ? div({ class: "pm-preview pm-fileshare-preview" }, + div({ class: "title-with-chip" }, h2(i18n.pmPreviewTitle), renderEncryptedChip(i18n)), + fileSharePreview.crypter + ? div( + div({ class: "title-with-chip" }, h2(i18n.pmCrypterCipherLabel), renderDoubleEncryptionChip(i18n)), + p({ class: "pm-key-hint" }, i18n.pmSharedKeyHint), + div({ class: "pm-sent-key" }, input({ type: "text", readonly: true, value: fileSharePreview.sharedKey, class: "pm-sent-key-value" })) + ) + : null, + div({ class: "pm-fileshare-info" }, + span({ class: "pm-fileshare-icon" }, '📎'), + span({ class: "pm-fileshare-name" }, String(fileSharePreview.filename || 'file')), + span({ class: "pm-fileshare-meta" }, `${fileSharePreview.sizeLabel || ''} · ${String(fileSharePreview.mime || 'application/octet-stream')}`) + ), + form({ method: "POST", action: "/pm/file", class: "pm-fileshare-send-form" }, + fileSharePreview.recipient + ? input({ type: "hidden", name: "recipient", value: fileSharePreview.recipient }) + : div({ class: "pm-fileshare-recipient" }, + label({ for: "fs-preview-recipient" }, i18n.pmRecipients), + br(), + input({ id: "fs-preview-recipient", type: "text", name: "recipient", placeholder: i18n.fileShareRecipientPlaceholder || 'Enter Oasis ID (@....ed25519)', required: true, maxlength: "120" }) + ), + input({ type: "hidden", name: "subject", value: fileSharePreview.subject || '' }), + input({ type: "hidden", name: "manifestBlobId", value: fileSharePreview.manifestBlobId }), + input({ type: "hidden", name: "keyHex", value: fileSharePreview.keyHex }), + input({ type: "hidden", name: "filename", value: fileSharePreview.filename || 'file' }), + input({ type: "hidden", name: "mime", value: fileSharePreview.mime || 'application/octet-stream' }), + input({ type: "hidden", name: "size", value: String(fileSharePreview.size || 0) }), + fileSharePreview.crypter ? input({ type: "hidden", name: "crypter", value: "1" }) : null, + fileSharePreview.crypter ? input({ type: "hidden", name: "sharedKey", value: fileSharePreview.sharedKey }) : null, + div({ class: "pm-actions" }, + button({ type: "submit", class: "pm-btn" }, i18n.pmSend), + a({ href: "/pm", class: "pm-btn danger-btn" }, i18n.pmCancel || 'Cancel') + ) + ) + ) + : null + ) ) ) ); diff --git a/src/views/podcasts_view.js b/src/views/podcasts_view.js new file mode 100644 index 0000000..4c7141f --- /dev/null +++ b/src/views/podcasts_view.js @@ -0,0 +1,321 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, table, tr, td, audio: audioHyperaxe, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderCommentsSection } = require("./comments_view"); +const { renderStyledText } = require("../backend/renderStyledText"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + + +const userId = config.keys.id; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "TOP", "VIEWERS"]; +const CATEGORIES = ["NEWS", "MUSIC", "TALK", "EDUCATION", "TECH", "CULTURE", "COMMUNITY", "OASIS", "OTHER"]; + +const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); +const filterLabel = (f) => String(i18n[`podcastFilter${cap(f)}`] || f).toUpperCase(); +const catLabel = (c) => String(i18n[`podcastCategory${cap(c)}`] || c).toUpperCase(); +const channelHref = (ch) => `/podcasts/${encodeURIComponent(ch.id)}`; +const episodeHref = (ep) => `/podcasts/episode/${encodeURIComponent(ep.id)}`; +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); +const blobSrc = (m) => `/blob/${encodeURIComponent(m.blobId)}`; + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const renderCover = (ch, cls = "podcast-cover") => { + if (!ch.cover || !ch.cover.blobId) return null; + if (ch.cover.kind === "video") return videoHyperaxe({ class: cls, src: blobSrc(ch.cover), controls: true, preload: "metadata", muted: true }); + return img({ class: cls, src: blobSrc(ch.cover), alt: ch.title || "" }); +}; + +const countsLine = (ch) => div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.podcastPlaysLabel}: ${ch.playCount || 0}`) +); +const infoTable = (item) => table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, fmt(item.createdAt)) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(item.author)) + ) +); + +const podcastChipFor = (mode, census) => { + if (mode === "ALL") return true; + if (!Array.isArray(census)) return true; + if (mode === "MINE") return census.some(c => String(c.author) === String(userId)); + if (mode === "RECENT") return census.length > 0; + if (mode === "TOP") return census.some(c => (c.opinionCount || 0) > 0 || (c.spreadCount || 0) > 0); + if (mode === "VIEWERS") return census.some(c => (c.playCount || 0) > 0); + if (CATEGORIES.includes(mode)) return census.some(c => c.category === mode); + return census.length > 0; +}; +exports.podcastChipFor = podcastChipFor; + +const renderChannelCard = (ch, params = {}) => + div({ class: "tribe-card podcast-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(ch.id, channelHref(ch), { author: ch.author, favKind: "podcasts", isFavorite: ch.isFavorite, reportTitle: ch.title, spread: (params.spreadMap && params.spreadMap.get(ch.id)) || null, deleteAction: String(ch.author) === String(userId) ? contentDeleteAction("podcast", ch.id) : undefined }) + ), + div({ class: "tribe-card-body" }, + ch.cover && ch.cover.blobId ? a({ href: channelHref(ch) }, renderCover(ch, "podcast-card-cover")) : null, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: channelHref(ch) }, ch.title || "—"))), + div({ class: "card-chips-row" }, renderStateChip("neutral", "", catLabel(ch.category)), ch.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('podcasts', ch.title, ch.id)) : null), + countsLine(ch) + ) + ); + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => m === filter || podcastChipFor(m, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const renderFilters = (filter, q, census, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/podcasts", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, filterLabel), + ...chipButtons(CATEGORIES, filter, census, catLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.podcastCreate) + ) + ) + ); + +const renderChannelForm = (ch) => + div({ class: "div-center audio-form" }, + h2(ch ? i18n.podcastUpdateSectionTitle : i18n.podcastCreateSectionTitle), + form({ method: "POST", action: ch ? `/podcasts/update/${encodeURIComponent(ch.id)}` : "/podcasts/create", enctype: "multipart/form-data" }, + label(i18n.podcastTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.podcastTitlePlaceholder, value: ch ? ch.title : "" }), br(), + label(i18n.podcastDescriptionLabel), br(), + textarea({ name: "description", rows: 5, maxlength: "3000", placeholder: i18n.podcastDescriptionPlaceholder }, ch ? ch.description : ""), br(), + label(i18n.podcastCoverLabel), br(), + input({ type: "file", name: "cover", accept: "image/*,video/*" }), br(), br(), + label(i18n.podcastCategoryLabel), br(), + select({ name: "category" }, ...CATEGORIES.map(c => option({ value: c, ...((ch ? ch.category : "TALK") === c ? { selected: true } : {}) }, catLabel(c)))), br(), br(), + label(i18n.podcastTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.podcastTagsPlaceholder, value: ch ? ch.tags.join(", ") : "" }), br(), br(), + renderClearnetSelector(ch ? !!ch.clearnet : false, i18n), br(), + button({ type: "submit", class: "create-button" }, ch ? i18n.podcastUpdate : i18n.podcastCreate) + ) + ); + +const renderEpisodeForm = (ch, ep) => + div({ class: "div-center audio-form" }, + h2(ep ? i18n.podcastEpisodeUpdateTitle : i18n.podcastEpisodeCreateTitle), + form({ method: "POST", action: ep ? `/podcasts/episode/update/${encodeURIComponent(ep.id)}` : `/podcasts/${encodeURIComponent(ch.id)}/episodes`, enctype: "multipart/form-data" }, + label(i18n.podcastEpisodeTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.podcastEpisodeTitlePlaceholder, value: ep ? ep.title : "" }), br(), + label(i18n.podcastEpisodeDescriptionLabel), br(), + textarea({ name: "description", rows: 6, maxlength: "5000", placeholder: i18n.podcastEpisodeDescriptionPlaceholder }, ep ? ep.description : ""), br(), + label(i18n.podcastEpisodeAudioLabel), br(), + input({ type: "file", name: "media", accept: "audio/*,video/*", ...(ep ? {} : { required: true }) }), br(), br(), + label(i18n.podcastTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.podcastTagsPlaceholder, value: ep ? ep.tags.join(", ") : "" }), br(), br(), + button({ type: "submit", class: "create-button" }, ep ? i18n.podcastUpdate : i18n.podcastEpisodePublish) + ) + ); + +exports.podcastsView = async (channels, filter = "ALL", params = {}) => { + const list = Array.isArray(channels) ? channels : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q); + return template( + i18n.podcastsTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.podcastsTitle), p(i18n.podcastsDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? [] : census, emptyMod) + ), + section( + isForm + ? renderChannelForm(f === "EDIT" ? params.channel : null) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, [ + { label: i18n.podcastEpisodesLabel, count: list.reduce((s, c) => s + (c.episodeCount || 0), 0) }, + { label: i18n.podcastPlaysLabel, count: list.reduce((s, c) => s + (c.playCount || 0), 0) } + ]), + form({ method: "GET", action: "/podcasts", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: q, placeholder: i18n.podcastSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, list.length ? paged(list).map(c => renderChannelCard(c, params)) : p(i18n.podcastNoItems)) + ] + ) + ); +}; + +const episodeChips = (ep) => + div({ class: "card-chips-row" }, + renderStateChip("neutral", "", `${i18n.podcastPlaysLabel}: ${ep.playCount || 0}`), + ep.opinionCount > 0 ? renderStateChip("mutuals", "ꔍ", String(ep.opinionCount)) : null + ); + +const episodeActions = (ep, params = {}, returnTo) => + renderContentActions(ep.id, episodeHref(ep), { + author: ep.author, + favKind: "podcasts", + isFavorite: ep.isFavorite, + reportTitle: ep.title, + spread: (params.spreadMap && params.spreadMap.get(ep.id)) || null, + returnTo, + torrentFrom: { blobId: ep.media && ep.media.blobId, name: ep.title }, + deleteAction: String(ep.author) === String(userId) ? contentDeleteAction("podcastEpisode", ep.id) : undefined + }); + +const episodeRow = (ch, ep, params = {}, returnTo) => + div({ class: "podcast-episode-row" }, + span({ class: "podcast-episode-number" }, `#${ep.number}`), + div({ class: "podcast-episode-main" }, + div({ class: "emergency-update-head podcast-episode-head" }, + div({ class: "podcast-episode-head-left" }, + a({ href: episodeHref(ep), class: "podcast-episode-title" }, ep.title), + episodeChips(ep) + ), + episodeActions(ep, params, returnTo) + ), + ep.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ep.description)) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, fmt(ep.createdAt))) + ) + ); + +exports.singleChannelView = async (ch, params = {}) => { + const isAuthor = String(ch.author) === String(userId); + const href = channelHref(ch); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const mode = String(params.mode || "").toUpperCase(); + const ownerActions = isAuthor + ? [ + form({ method: "GET", action: "/podcasts" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: ch.id }), button({ type: "submit", class: "update-btn" }, i18n.podcastUpdate)) + ] + : []; + const allEpisodes = Array.isArray(ch.episodes) ? ch.episodes.slice().reverse() : []; + const episodeFilter = String(params.episodes || "").toUpperCase() === "VIEWERS" && allEpisodes.some(e => (e.playCount || 0) > 0) ? "VIEWERS" : "RECENT"; + const episodes = episodeFilter === "VIEWERS" ? allEpisodes.slice().sort((x, y) => (y.playCount || 0) - (x.playCount || 0) || y.publishedTs - x.publishedTs) : allEpisodes; + const episodeModes = ["RECENT", ...(allEpisodes.some(e => (e.playCount || 0) > 0) ? ["VIEWERS"] : [])]; + const episodeChipsRow = allEpisodes.length + ? div({ class: "activity-sub-filter" }, + ...episodeModes.map(m => form({ class: "sub-filter-form", method: "GET", action: `${href}#episodes` }, + input({ type: "hidden", name: "episodes", value: m }), + button({ type: "submit", class: episodeFilter === m ? "filter-btn active" : "filter-btn" }, filterLabel(m)) + )) + ) + : null; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(ch.id, href, { author: ch.author, favKind: "podcasts", isFavorite: ch.isFavorite, reportTitle: ch.title, spread: params.spread || null, returnTo: href, deleteAction: isAuthor ? contentDeleteAction("podcast", ch.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, ch.title), renderReachChip(!!ch.clearnet, i18n, ch.clearnet ? clearnetItemHref('podcasts', ch.title, ch.id) : null)), + isAuthor ? renderClearnetSwitch("podcasts", ch.rootId || ch.id, !!ch.clearnet) : null, + div({ class: "card-chips-row" }, renderStateChip("neutral", "", catLabel(ch.category))), + ch.cover && ch.cover.blobId ? a({ href: channelHref(ch), class: "podcast-cover-link" }, renderCover(ch, "podcast-cover podcast-cover-large")) : null, + ch.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ch.description)) : null, + infoTable(ch), + countsLine(ch), + isAuthor + ? div({ class: "tribe-side-actions podcast-episode-add" }, + form({ method: "GET", action: href }, input({ type: "hidden", name: "mode", value: "episode" }), button({ type: "submit", class: "tribe-action-btn" }, String(i18n.podcastEpisodeAdd).toUpperCase())) + ) + : null, + renderTags(ch.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + params.subscription + ? renderSubscriptionBox({ target: ch.id, scope: "podcasts", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: isAuthor, returnTo: href, canWrite: isAuthor }) + : null + ); + const main = div({ class: "tribe-main" }, + isAuthor && mode === "EPISODE" ? renderEpisodeForm(ch, null) : null, + allEpisodes.length + ? div({ class: "podcast-featured" }, + div({ class: "emergency-update-head podcast-episode-head" }, + div({ class: "podcast-episode-head-left" }, + h2({ class: "tribe-card-title" }, a({ href: episodeHref(allEpisodes[0]) }, `#${allEpisodes[0].number} · ${allEpisodes[0].title}`)), + episodeChips(allEpisodes[0]) + ), + episodeActions(allEpisodes[0], params, href) + ), + renderPlayer(allEpisodes[0]), + allEpisodes[0].description ? p({ class: "tribe-side-description" }, ...renderStyledText(allEpisodes[0].description)) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, fmt(allEpisodes[0].createdAt))) + ) + : null, + div({ class: "card-section podcast-episodes", id: "episodes" }, + h3(`${i18n.podcastEpisodesTitle} (${episodes.length})`), + episodeChipsRow, + episodes.length ? div({ class: "podcast-episode-list" }, ...episodes.map(ep => episodeRow(ch, ep, params, href))) : p(i18n.podcastNoEpisodes) + ), + renderEngagement(ch.id, + renderOpinionsVoting("/podcasts/opinions", ch.id, ch.opinions, href, ch.opinions_inhabitants), + renderCommentsSection({ action: `/podcasts/${encodeURIComponent(ch.id)}/comments`, comments: params.comments || [], returnTo: href }) + ) + ); + return template( + ch.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.podcastsTitle), p(i18n.podcastsDescription)), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; + +const renderPlayer = (ep) => { + if (!ep.media || !ep.media.blobId) return p(i18n.audioNoFile); + if (ep.media.kind === "video") return div({ class: "podcast-player video-container video-container-row" }, videoHyperaxe({ controls: true, src: blobSrc(ep.media), preload: "metadata", class: "podcast-video" })); + return div({ class: "podcast-player audio-container" }, audioHyperaxe({ controls: true, src: blobSrc(ep.media), preload: "metadata" })); +}; + +exports.singleEpisodeView = async (ep, params = {}) => { + const ch = ep.channel || { id: ep.channelId, title: "", author: ep.author, episodes: [] }; + const isAuthor = String(ep.author) === String(userId); + const href = episodeHref(ep); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const played = Array.isArray(ep.listeners) && ep.listeners.includes(userId); + const mode = String(params.mode || "").toUpperCase(); + const headActions = [ + played + ? span({ class: "status supporting" }, `✓ ${i18n.podcastPlayed}`) + : form({ method: "POST", action: `/podcasts/episode/${encodeURIComponent(ep.id)}/play` }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.podcastMarkPlayed).toUpperCase())) + ].filter(Boolean); + const ownerActions = isAuthor + ? [ + form({ method: "GET", action: href }, input({ type: "hidden", name: "mode", value: "edit" }), button({ type: "submit", class: "update-btn" }, i18n.podcastUpdate)) + ] + : []; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(ep.id, href, { author: ep.author, favKind: "podcasts", isFavorite: ep.isFavorite, reportTitle: ep.title, spread: params.spread || null, returnTo: href, torrentFrom: { blobId: ep.media && ep.media.blobId, name: ep.title }, deleteAction: isAuthor ? contentDeleteAction("podcastEpisode", ep.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: channelHref(ch), class: "user-link" }, ch.title || i18n.podcastsTitle)), renderReachChip(!!ch.clearnet, i18n, ch.clearnet ? clearnetItemHref('podcasts', ch.title, ch.id) : null)), + ch.cover && ch.cover.blobId ? a({ href: channelHref(ch), class: "podcast-cover-link" }, renderCover(ch, "podcast-cover podcast-cover-large")) : null, + ep.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ep.description)) : null, + infoTable(ep), + renderTags(ep.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ); + const main = div({ class: "tribe-main" }, + div({ class: "emergency-update-head podcast-episode-head" }, + div({ class: "podcast-episode-head-left" }, + h2({ class: "tribe-card-title" }, `#${ep.number} · ${ep.title}`), + episodeChips(ep) + ), + headActions.length ? div({ class: "tribe-side-actions emergency-update-actions" }, ...headActions) : null + ), + renderPlayer(ep), + renderEngagement(ep.id, + renderOpinionsVoting("/podcasts/opinions", ep.id, ep.opinions, href, ep.opinions_inhabitants), + renderCommentsSection({ action: `/podcasts/episode/${encodeURIComponent(ep.id)}/comments`, comments: params.comments || [], returnTo: href }) + ), + isAuthor && mode === "EDIT" ? renderEpisodeForm(ch, ep) : null + ); + return template( + ep.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.podcastsTitle), p(i18n.podcastsDescription)), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; diff --git a/src/views/polls_view.js b/src/views/polls_view.js new file mode 100644 index 0000000..61461fd --- /dev/null +++ b/src/views/polls_view.js @@ -0,0 +1,329 @@ +const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions, renderStateChip, renderLifespanChip, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH } = require("../models/polls_model_limits"); + +const userId = config.keys.id; + +const FILTERS = [ + { key: "ALL", i18n: "pollFilterAll" }, + { key: "RECENT", i18n: "pollFilterRecent" }, + { key: "MINE", i18n: "pollFilterMine" }, + { key: "TOP", i18n: "pollFilterTop" }, + { key: "VOTED", i18n: "pollFilterVoted" }, + { key: "OPEN", i18n: "pollFilterOpen" }, + { key: "CLOSED", i18n: "pollFilterClosed" } +]; + +const safeArr = (v) => (Array.isArray(v) ? v : []); + +const pct = (n, total) => (total > 0 ? Math.round((Number(n) || 0) / total * 100) : 0); +const letterOf = (i) => { + let n = Number(i) || 0; + let out = ""; + do { + out = String.fromCharCode(65 + (n % 26)) + out; + n = Math.floor(n / 26) - 1; + } while (n >= 0); + return out; +}; +const pctStep = (n, total) => Math.round(pct(n, total) / 5) * 5; + +const outcomeOf = (poll) => { + if (!poll.totalVoters) return i18n.pollNoVotesYet; + const max = Math.max(...poll.options.map(o => poll.counts[o] || 0)); + if (!max) return i18n.pollNoVotesYet; + const winners = poll.options + .map((o, i) => ({ o, letter: letterOf(i) })) + .filter(({ o }) => (poll.counts[o] || 0) === max); + if (winners.length > 1) return `${i18n.pollTie}: ${winners.map(w => `${w.letter})`).join(", ")}`; + return `${winners[0].letter}) ${pct(max, poll.totalVoters)}%`; +}; + +const renderStatusChip = (poll) => + renderStateChip(poll.status === "OPEN" ? "whole" : "mutuals", "◆", + poll.status === "OPEN" ? i18n.pollStatusOpen : i18n.pollStatusClosed); + +const renderModeChips = (poll) => [ + poll.anonymous ? renderStateChip("mutuals", "◈", i18n.pollAnonymous) : null, + poll.multiple ? renderStateChip("whole", "☰", i18n.pollMultiple) : null +].filter(Boolean); + +const renderResults = (poll) => + table({ class: "poll-results" }, + ...poll.options.map((opt, i) => + tr({ class: poll.myChoices.includes(opt) ? "poll-result-row poll-result-mine" : "poll-result-row" }, + td({ class: "poll-result-option" }, `${letterOf(i)})`), + td({ class: "poll-result-bar" }, + div({ class: "poll-bar" }, + div({ class: `poll-bar-fill poll-bar-fill-${pctStep(poll.counts[opt], poll.totalVoters)} poll-hue-${i % 8}` }) + ) + ), + td({ class: "poll-result-count" }, + `${poll.counts[opt] || 0} (${pct(poll.counts[opt], poll.totalVoters)}%)`) + ) + ) + ); + +const renderBallot = (poll, returnTo, basePath, extra = null) => { + if (poll.status === "CLOSED" || poll.hasVoted) return null; + const inputType = poll.multiple ? "checkbox" : "radio"; + return form({ method: "POST", action: `${basePath}/vote/${encodeURIComponent(poll.id)}`, class: "poll-ballot" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + ...poll.options.map((opt, i) => + div({ class: "poll-option-row" }, + label({ for: `opt-${i}-${poll.id}` }, + input({ + type: inputType, name: "choices", value: opt, id: `opt-${i}-${poll.id}`, + ...(poll.myChoices.includes(opt) ? { checked: true } : {}) + }), + " ", + `${letterOf(i)}) ${opt}` + ) + ) + ), + div({ class: "poll-ballot-actions" }, + button({ type: "submit", class: "filter-btn" }, i18n.pollVoteButton), + ...(Array.isArray(extra) ? extra.filter(Boolean) : []) + ) + ); +}; + +const renderPollCard = (poll, filter, spreadInfo, basePath = "/polls") => { + const href = `${basePath}/${encodeURIComponent(poll.id)}`; + const isOwn = String(poll.author) === String(userId); + const chips = [ + renderStatusChip(poll), + ...renderModeChips(poll), + poll.deadline + ? renderStateChip(poll.status === "OPEN" ? "whole" : "mutuals", "◷", `${i18n.pollDeadline}: ${moment(poll.deadline).format("YYYY/MM/DD HH:mm")}`) + : null, + renderLifespanChip(poll.lifetime, i18n) + ].filter(Boolean); + return div({ class: "tribe-card poll-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(poll.id, href, { spread: spreadInfo || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, reportTitle: poll.question, deleteAction: isOwn ? contentDeleteAction('poll', poll.id) : undefined }) + ), + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href }, poll.question)) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.pollVoters}: ${poll.totalVoters}`) + ) + ) + ); +}; + +const renderCreateForm = (poll = null, params = {}) => { + const isEdit = !!poll; + const action = isEdit ? `/polls/update/${encodeURIComponent(poll.id)}` : "/polls/create"; + return section( + params.spreadWarning || null, + div({ class: "div-center audio-form" }, + h2(isEdit ? i18n.pollEditTitle : i18n.pollCreateTitle), + form({ method: "POST", action }, + label(i18n.pollQuestion), + br(), + input({ type: "text", name: "question", required: true, maxlength: "300", value: isEdit ? poll.question : "", placeholder: i18n.pollQuestionPlaceholder }), + br(), br(), + label(i18n.pollOptions), + br(), + textarea({ + name: "options", + rows: String(MAX_OPTIONS), + required: true, + maxlength: String(MAX_OPTIONS * (MAX_OPTION_LENGTH + 1)) + }, isEdit ? poll.options.join("\n") : ""), + br(), + div({ class: "poll-switch" }, + input({ type: "hidden", name: "anonymous", value: "0" }), + label( + input({ type: "checkbox", name: "anonymous", value: "1", ...(isEdit && poll.anonymous ? { checked: true } : {}) }), + " ", i18n.pollAnonymousLabel + ) + ), + div({ class: "poll-switch" }, + input({ type: "hidden", name: "multiple", value: "0" }), + label( + input({ type: "checkbox", name: "multiple", value: "1", ...(isEdit && poll.multiple ? { checked: true } : {}) }), + " ", i18n.pollMultipleLabel + ) + ), + br(), + label(i18n.pollDeadline), + br(), + input({ + type: "datetime-local", name: "deadline", + min: moment().format("YYYY-MM-DDTHH:mm"), + value: isEdit && poll.deadline ? moment(poll.deadline).format("YYYY-MM-DDTHH:mm") : "" + }), + br(), br(), + label(i18n.pollTags), + br(), + input({ type: "text", name: "tags", value: isEdit ? poll.tags.join(", ") : "" }), + br(), br(), + button({ type: "submit", class: "create-button" }, isEdit ? i18n.pollUpdateButton : i18n.pollPublishButton) + ) + ) + ); +}; + +const renderComments = (poll, comments, basePath) => { + const href = `${basePath}/${encodeURIComponent(poll.id)}`; + return renderSharedCommentsSection({ + action: `${href}/comments`, + comments: comments, + returnTo: href + }); +}; + +const renderFilterBar = (filter, q, showSearch = true, items = null, censusList = null) => { + const emptyMod = items !== null && moduleIsEmpty(items, filter, "ALL", q); + const censusPl = Array.isArray(censusList) ? censusList : (Array.isArray(items) ? items : []); + const pollChip = (x) => { + const m = x.key; + if (m === filter) return true; + if (m === "TOP") return censusPl.length > 0; + if (m === "MINE") return censusPl.some(pl => String(pl.author || pl.createdBy) === String(userId)); + if (m === "RECENT") return censusPl.length > 0; + if (m === "VOTED") return censusPl.some(pl => pl.hasVoted || safeArr(pl.myChoices).length > 0); + if (m === "OPEN" || m === "CLOSED") return censusPl.some(pl => String(pl.status || "OPEN").toUpperCase() === m); + return true; + }; + return section( + div({ class: "filters" }, + form({ method: "GET", action: "/polls", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : FILTERS.filter(pollChip).map(f => + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + )), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.pollCreateButton) + ) + ), + showSearch && !emptyMod ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + items ? renderModuleStatsBy(items, pl => String(pl.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.pollStatusOpen }, { value: 'CLOSED', label: i18n.pollStatusClosed }]) : null, + form({ method: "GET", action: "/polls", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q || "", placeholder: i18n.pollSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) : null + ); +}; + +exports.pollsView = async (polls = [], filter = "ALL", params = {}) => { + const mode = String(filter).toUpperCase(); + const spreadMap = params.spreadMap instanceof Map ? params.spreadMap : new Map(); + if (mode === "CREATE" || mode === "EDIT") { + const editing = params.poll || null; + if (editing) params = { ...params, spreadWarning: await renderSpreadEditWarning(editing.id) }; + return template( + i18n.pollsTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))), + renderFilterBar(mode, params.q, false), + renderCreateForm(editing, params) + ); + } + return template( + i18n.pollsTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))), + renderFilterBar(mode, params.q, true, polls, params.censusList), + section( + polls.length + ? div({ class: "jobs-grid" }, ...paged(polls).map(pl => renderPollCard(pl, mode, spreadMap.get(pl.id)))) + : div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.pollsNoItems)) + ) + ); +}; + +exports.singlePollView = async (poll, comments = [], params = {}) => { + const basePath = "/polls"; + const href = `${basePath}/${encodeURIComponent(poll.id)}`; + const isOwn = String(poll.author) === String(userId); + const chips = [renderStatusChip(poll), ...renderModeChips(poll), renderLifespanChip(poll.lifetime, i18n)].filter(Boolean); + + const infoRows = []; + const pushRow = (labelText, valueNode) => + infoRows.push(tr( + td({ class: "tribe-info-label" }, labelText), + td({ class: "tribe-info-value" }, valueNode) + )); + if (poll.deadline) pushRow(i18n.pollDeadline, moment(poll.deadline).format("YYYY/MM/DD HH:mm")); + pushRow(i18n.pollOutcome, outcomeOf(poll)); + + const tagsNode = poll.tags.length + ? div({ class: "card-tags" }, poll.tags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : null; + + const ownerActions = isOwn && poll.totalVoters === 0 && poll.status === "OPEN" + ? div({ class: "tribe-side-actions" }, + form({ method: "GET", action: "/polls" }, + input({ type: "hidden", name: "filter", value: "EDIT" }), + input({ type: "hidden", name: "id", value: poll.id }), + button({ type: "submit", class: "update-btn" }, i18n.pollUpdateButton) + ) + ) + : null; + + const pollSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(poll.id, href, { spread: params.spreads || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, returnTo: href, reportTitle: poll.question, deleteAction: isOwn ? contentDeleteAction('poll', poll.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, poll.question)), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(poll.createdAt, poll.author)), + tagsNode, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.pollVoters}: ${poll.totalVoters}`) + ), + isOwn + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.pollStatusLabel}: `), + renderStatusChip(poll), + poll.status === "OPEN" + ? form({ method: "POST", action: `/polls/close/${encodeURIComponent(poll.id)}` }, + button({ type: "submit", class: "status-btn project-control-btn" }, i18n.pollCloseButton) + ) + : null + ) + : null, + ownerActions + ); + + const pollMain = div({ class: "tribe-main" }, + div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.pollOutcome), + renderBallot(poll, href, basePath), + renderResults(poll) + ), + poll.votersHidden + ? null + : (poll.voters.length + ? div({ class: "card-assigned-list" }, ...poll.voters.map(v => userLink(v))) + : null), + poll.tribeId ? null : renderEngagement(poll.id, + renderOpinionsVoting('/polls/opinions', poll.id, poll.opinions, href, poll.opinions_inhabitants), + renderComments(poll, comments, basePath) + ) + ); + + return template( + poll.question, + section(div({ class: "tags-header module-header-line" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))), + renderFilterBar(String(params.filter || "ALL").toUpperCase(), params.q, false), + section(div({ class: "tribe-details" }, pollSide, pollMain)) + ); +}; + +exports.renderPollCard = renderPollCard; +exports.renderResults = renderResults; +exports.renderBallot = renderBallot; +exports.outcomeOf = outcomeOf; +exports.letterOf = letterOf; diff --git a/src/views/projects_view.js b/src/views/projects_view.js index 8c17d80..df75a33 100644 --- a/src/views/projects_view.js +++ b/src/views/projects_view.js @@ -1,21 +1,24 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, ul, li, table, thead, tbody, tr, th, td, progress, video, audio } = require("../server/node_modules/hyperaxe") -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views") +const { renderZoomableImage } = require("./gallery_view") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel, renderMapEmbedWithZoom } = require("./maps_view") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") const renderMediaBlob = (value, attrs = {}) => { if (!value) return null const s = String(value).trim() if (!s) return null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) return null } @@ -23,19 +26,20 @@ const userId = config.keys.id const FILTERS = [ { key: "ALL", i18n: "projectFilterAll", title: "projectAllTitle" }, + { key: "RECENT", i18n: "projectFilterRecent", title: "projectRecentTitle" }, { key: "MINE", i18n: "projectFilterMine", title: "projectMineTitle" }, { key: "APPLIED", i18n: "projectFilterApplied", title: "projectAppliedTitle" }, { key: "ACTIVE", i18n: "projectFilterActive", title: "projectActiveTitle" }, { key: "PAUSED", i18n: "projectFilterPaused", title: "projectPausedTitle" }, { key: "COMPLETED", i18n: "projectFilterCompleted", title: "projectCompletedTitle" }, { key: "FOLLOWING", i18n: "projectFilterFollowing", title: "projectFollowingTitle" }, - { key: "RECENT", i18n: "projectFilterRecent", title: "projectRecentTitle" }, { key: "TOP", i18n: "projectFilterTop", title: "projectTopTitle" }, { key: "BACKERS", i18n: "projectFilterBackers", title: "projectBackersLeaderboardTitle" } ] const safeArr = (v) => (Array.isArray(v) ? v : []) const safeText = (v) => String(v || "").trim() +const clearnetEligible = (pr) => String(pr.status || "").toUpperCase() !== "CANCELLED" const toNum = (v) => { const s = v === null || v === undefined ? "" : String(v) @@ -77,7 +81,7 @@ const renderCardFieldRich = (labelText, children) => const renderProgressBlock = (labelText, valueText, value, maxValue) => div( - { class: "confirmations-block progress-block" }, + { class: "confirmations-block progress-block project-progress-block" }, div( { class: "card-field" }, span({ class: "card-label" }, labelText), @@ -214,6 +218,7 @@ const renderPledgeBox = (project, filter, isAuthor) => { { method: "POST", action: `/projects/pledge/${encodeURIComponent(project.id)}` }, input({ type: "hidden", name: "returnTo", value: rt }), input({ type: "number", name: "amount", min: "0.01", step: "0.01", required: true, placeholder: i18n.projectPledgePlaceholder }), + renderEcoValueChip(), select( { name: "milestoneOrBounty" }, option({ value: "" }, i18n.projectSelectMilestoneOrBounty), @@ -259,7 +264,7 @@ const renderMilestonesAndBounties = (project, filter, editable) => { { class: "milestone-head" }, span({ class: "milestone-title" }, m.title), m.dueDate ? span({ class: "chip chip-due" }, `${i18n.projectMilestoneDue}: ${moment(m.dueDate).format("YYYY/MM/DD HH:mm")}`) : null, - safeText(m.description) ? p(...renderUrl(m.description)) : null, + safeText(m.description) ? p(...renderStyledText(m.description)) : null, editable && !m.done ? form( { method: "POST", action: `/projects/milestones/complete/${encodeURIComponent(project.id)}/${idx}` }, @@ -283,7 +288,7 @@ const renderMilestonesAndBounties = (project, filter, editable) => { span({ class: "bounty-title" }, b.title), span({ class: "bounty-amount" }, `${b.amount} ECO`) ), - safeText(b.description) ? p(...renderUrl(b.description)) : null, + safeText(b.description) ? p(...renderStyledText(b.description)) : null, renderCardField(i18n.projectBountyStatus + ":", statusText), b.claimedBy ? renderCardField(i18n.projectBountyClaimedBy + ":", userLink(b.claimedBy)) : null, !editable && !b.done && !b.claimedBy && project.author !== userId @@ -308,8 +313,8 @@ const renderMilestonesAndBounties = (project, filter, editable) => { br(), select( { name: "milestoneIndex" }, - option({ value: "", selected: b.milestoneIndex == null }, "-"), - ...milestones.map((m2, idx2) => option({ value: String(idx2), selected: b.milestoneIndex === idx2 }, m2.title)) + option({ value: "", ...(b.milestoneIndex == null ? { selected: true } : {})}, "-"), + ...milestones.map((m2, idx2) => option({ value: String(idx2), ...(b.milestoneIndex === idx2 ? { selected: true } : {})}, m2.title)) ), br(), br(), @@ -342,7 +347,7 @@ const renderMilestonesAndBounties = (project, filter, editable) => { span({ class: "bounty-title" }, b.title), span({ class: "bounty-amount" }, `${b.amount} ECO`) ), - safeText(b.description) ? p(...renderUrl(b.description)) : null, + safeText(b.description) ? p(...renderStyledText(b.description)) : null, renderCardField(i18n.projectBountyStatus + ":", statusText), b.claimedBy ? renderCardField(i18n.projectBountyClaimedBy + ":", userLink(b.claimedBy)) : null, !editable && !b.done && !b.claimedBy && project.author !== userId @@ -400,7 +405,11 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM : null const chips = [ renderProjectStatusChip(pr.status), - renderLifespanChip(pr.lifetime, i18n) + renderLifespanChip(pr.lifetime, i18n), + pr.clearnet === true && clearnetEligible(pr) ? renderReachChip(true, i18n, clearnetItemHref('projects', pr.title, pr.id || pr.key)) : null, + pr.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (pr.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ].filter(Boolean) const isOwn = pr.author && String(pr.author) === String(userId) @@ -408,7 +417,7 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM div( { class: "card-header activity-card-header" }, span(), - renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`) + renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { spread: spreadMap.get(pr.id || pr.key) || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title, deleteAction: isOwn ? contentDeleteAction('project', pr.id || pr.key) : undefined }) ), div({ class: "card-section project-card-body" }, heroNode, @@ -419,22 +428,21 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, goal > 0 - ? div({ class: "card-date-highlight" }, `${pr.goal} ECO`) + ? div({ class: "price-chip" }, `${pr.goal} ECO`) : null, goal > 0 ? renderProgressBlock(i18n.projectFunding + ":", `${fundingPct}%`, fundingPct, 100) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.projectFollowers}: ${followersCount(pr)}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(pr.id || pr.key, spreadMap.get(pr.id || pr.key))) + ) ) ) }) ) } -const renderProjectForm = (project, mode) => { +const renderProjectForm = (project, mode, spreadWarning = null) => { const pr = project || {} const isEdit = mode === "edit" const nowLocal = moment().format("YYYY-MM-DDTHH:mm") @@ -444,6 +452,7 @@ const renderProjectForm = (project, mode) => { return div( { class: "div-center project-form" }, + isEdit ? spreadWarning : null, form( { action: isEdit ? `/projects/update/${encodeURIComponent(pr.id)}` : "/projects/create", @@ -453,21 +462,25 @@ const renderProjectForm = (project, mode) => { input({ type: "hidden", name: "returnTo", value: returnTo }), label(i18n.projectTitle), br(), - input({ type: "text", name: "title", required: true, placeholder: i18n.projectTitlePlaceholder, value: pr.title || "" }), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.projectTitlePlaceholder, value: pr.title || "" }), br(), label(i18n.projectDescription), br(), - textarea({ name: "description", rows: "6", required: true, placeholder: i18n.projectDescriptionPlaceholder }, pr.description || ""), + textarea({ maxlength: "5000", name: "description", rows: "6", required: true, placeholder: i18n.projectDescriptionPlaceholder }, pr.description || ""), br(), label(i18n.projectImage), br(), input({ type: "file", name: "image" }), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob" }), + br(), pr.image ? renderMediaBlob(pr.image) : null, br(), label(i18n.projectGoal), br(), input({ type: "number", step: "0.01", min: "0.01", name: "goal", required: true, placeholder: i18n.projectGoalPlaceholder, value: pr.goal || "" }), + renderEcoValueChip(), br(), br(), label(i18n.mapLocationTitle || "Map Location"), @@ -487,7 +500,7 @@ const renderProjectForm = (project, mode) => { br(), label(i18n.projectMilestoneDescription), br(), - textarea({ name: "milestoneDescription", rows: "3", placeholder: i18n.projectMilestoneDescriptionPlaceholder }), + textarea({ maxlength: "5000", name: "milestoneDescription", rows: "3", placeholder: i18n.projectMilestoneDescriptionPlaceholder }), br(), label(i18n.projectMilestoneTargetPercent), br(), @@ -499,43 +512,70 @@ const renderProjectForm = (project, mode) => { input({ type: "datetime-local", name: "milestoneDueDate", min: nowLocal, max: milestoneMax }), br(), br(), + !isEdit || clearnetEligible(pr) ? renderClearnetSelector(isEdit ? !!pr.clearnet : false, i18n) : null, button({ type: "submit" }, isEdit ? i18n.projectUpdateButton : i18n.projectCreateButton) ) ) } exports.projectsView = async (projectsOrForm, filter, _unused, params = {}) => { + if (String(filter).toUpperCase() === "EDIT") { + const editing = Array.isArray(projectsOrForm) ? projectsOrForm[0] : projectsOrForm + params = { ...params, spreadWarning: await renderSpreadEditWarning(editing && (editing.id || editing.key)) } + } const f = String(filter || "ALL").toUpperCase() const filterObj = FILTERS.find((x) => x.key === f) || FILTERS[0] const sectionTitle = i18n[filterObj.title] || i18n.projectAllTitle - const { renderReachChip: renderReachChipProjects } = require('./clearnet_view'); - const viewerClearnetProjects = !!(params.viewerPrefs && params.viewerPrefs.clearnetProjects); + const emptyMod = moduleIsEmpty(Array.isArray(projectsOrForm) ? projectsOrForm : [], f, "ALL", params.q); + const censusP = Array.isArray(params.censusList) ? params.censusList : (Array.isArray(projectsOrForm) ? projectsOrForm : []); + const projChip = (x) => { + const m = x.key; + if (m === f) return true; + if (m === "TOP") return censusP.length > 0; + if (m === "MINE") return censusP.some(pr => String(pr.author) === String(userId)); + if (m === "APPLIED") return censusP.some(pr => String(pr.author) !== String(userId) && [pr.members, pr.participants, pr.applicants].some(l => Array.isArray(l) && l.includes(userId))); + if (m === "ACTIVE" || m === "PAUSED" || m === "COMPLETED") return censusP.some(pr => String(pr.status || "ACTIVE").toUpperCase() === m); + if (m === "FOLLOWING") return censusP.some(pr => Array.isArray(pr.followers) && pr.followers.includes(userId)); + if (m === "RECENT") return censusP.length > 0; + if (m === "BACKERS") return censusP.some(pr => safeArr(pr.backers).length > 0); + return true; + }; return template( i18n.projectsTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(sectionTitle), p(i18n.projectsDescription) ), - div({ class: "shop-title-row" }, renderReachChipProjects(viewerClearnetProjects, i18n)), - br(), div( { class: "filters" }, form( { method: "GET", action: "/projects", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, i18n[x.i18n])) + (emptyMod ? [] : FILTERS.filter(projChip).map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n]).toUpperCase()))) .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.projectCreateProject)) ) ), + f === "CREATE" || f === "EDIT" + ? null + : emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(projectsOrForm, pr => String(pr.status || 'ACTIVE').toUpperCase(), [{ value: 'ACTIVE', label: i18n.projectStatusACTIVE }, { value: 'PAUSED', label: i18n.projectStatusPAUSED }, { value: 'COMPLETED', label: i18n.projectStatusCOMPLETED }, { value: 'CANCELLED', label: i18n.projectStatusCANCELLED }]), + form({ method: "GET", action: "/projects", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: safeText(params.q), placeholder: i18n.projectSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ), f === "CREATE" || f === "EDIT" ? (() => { - const prToEdit = f === "EDIT" ? (safeArr(projectsOrForm)[0] || {}) : {} - return renderProjectForm(prToEdit, f === "EDIT" ? "edit" : "create") + const prToEdit = f === "EDIT" ? (safeArr(projectsOrForm)[0] || {}) : (params.prefill || {}) + return renderProjectForm(prToEdit, f === "EDIT" ? "edit" : "create", params.spreadWarning) })() : (f === "BACKERS" ? renderBackersLeaderboard(projectsOrForm) - : div({ class: "projects-list" }, renderProjectList(projectsOrForm, f, params.spreadMap)) + : div({ class: "projects-list" }, renderProjectList(paged(projectsOrForm), f, params.spreadMap)) ) ) ) @@ -548,6 +588,8 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { const isFollower = safeArr(pr.followers).includes(userId) const statusUpper = String(pr.status || "ACTIVE").toUpperCase() + const canClearnet = clearnetEligible(pr) + const isClearnet = canClearnet && !!pr.clearnet const pctRaw = toNum(pr.progress) const pct = clamp(Math.round(Number.isFinite(pctRaw) ? pctRaw : 0), 0, 100) const goal = Math.max(0, toNum(pr.goal) || 0) @@ -560,16 +602,17 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { renderProjectStatusChip(pr.status), isFollower ? renderStateChip("whole", "★", i18n.projectFollowing || "FOLLOWING") : null, renderLifespanChip(pr.lifetime, i18n), - renderEcoTax(pr.msgSize, pr.id || pr.key) + renderEcoTax(pr.msgSize, pr.id || pr.key), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('projects', pr.title, pr.id || pr.key) : null), + isAuthor && canClearnet ? renderClearnetSwitch('projects', pr.rootId || pr.id || pr.key, isClearnet) : null, + pr.subscription + ? ((isAuthor || pr.subscription.subscribed === true) + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : renderStateChip("closed", "✉", i18n.subscriptionOff)) + : null ].filter(Boolean) const sideActions = [] - if (!isAuthor && pr.author) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: pr.author }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )) - } if (!isAuthor && statusUpper === "ACTIVE") { sideActions.push(isFollower ? form({ method: "POST", action: `/projects/unfollow/${encodeURIComponent(pr.id)}` }, @@ -584,21 +627,17 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { } if (isAuthor) { sideActions.push(form({ method: "GET", action: `/projects/edit/${encodeURIComponent(pr.id)}` }, - button({ class: "update-btn", type: "submit" }, i18n.projectUpdateButton) - )) - sideActions.push(form({ method: "POST", action: `/projects/delete/${encodeURIComponent(pr.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.projectDeleteButton) + button({ class: "tribe-action-btn", type: "submit" }, i18n.projectUpdateButton) )) sideActions.push(form( { method: "POST", action: `/projects/status/${encodeURIComponent(pr.id)}`, class: "project-control-form project-control-form--status" }, input({ type: "hidden", name: "returnTo", value: returnTo }), select( { name: "status", class: "project-control-select" }, - option({ value: "ACTIVE", selected: statusUpper === "ACTIVE" }, i18n.projectStatusACTIVE), - option({ value: "PAUSED", selected: statusUpper === "PAUSED" }, i18n.projectStatusPAUSED), - option({ value: "COMPLETED", selected: statusUpper === "COMPLETED" }, i18n.projectStatusCOMPLETED), - option({ value: "CANCELLED", selected: statusUpper === "CANCELLED" }, i18n.projectStatusCANCELLED) + option({ value: "ACTIVE", ...(statusUpper === "ACTIVE" ? { selected: true } : {})}, i18n.projectStatusACTIVE), + option({ value: "PAUSED", ...(statusUpper === "PAUSED" ? { selected: true } : {})}, i18n.projectStatusPAUSED), + option({ value: "COMPLETED", ...(statusUpper === "COMPLETED" ? { selected: true } : {})}, i18n.projectStatusCOMPLETED), + option({ value: "CANCELLED", ...(statusUpper === "CANCELLED" ? { selected: true } : {})}, i18n.projectStatusCANCELLED) ), button({ class: "status-btn project-control-btn", type: "submit" }, i18n.projectSetStatus) )) @@ -611,17 +650,31 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { } const projectSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { spread: params.spreads || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title, returnTo: `/projects/${encodeURIComponent(pr.id)}`, deleteAction: isAuthor ? contentDeleteAction('project', pr.id || pr.key) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, pr.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(pr.id || pr.key, params.spreads)), pr.image ? renderMediaBlob(pr.image, { class: "tribe-detail-image" }) : null, - div({ class: "job-price-line card-salary" }, `${pr.goal || 0} ECO`), + div({ class: "price-chip" }, `${pr.goal || 0} ECO`), div({ class: "job-price-line card-salary" }, `${i18n.projectFollowers}: ${followersCount(pr)}`), renderProgressBlock(i18n.projectProgress + ":", `${pct}%`, pct, 100), goal > 0 ? renderProgressBlock(i18n.projectFunding + ":", `${fundingPct}%`, fundingPct, 100) : null, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + table({ class: "tribe-info-table" }, ...renderCreatedRows(pr.createdAt, pr.author)), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + pr.subscription + ? renderSubscriptionBox({ + target: pr.rootId || pr.id, + scope: "projects", + subscribed: pr.subscription.subscribed === true, + count: pr.subscription.count, + isOwner: isAuthor, + canWrite: isAuthor, + returnTo + }) + : null ) const projectMain = div({ class: "tribe-main" }, @@ -629,65 +682,34 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { safeText(pr.description) ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.projectDescription), - p({ class: "tribe-side-description" }, ...renderUrl(pr.description)) + p({ class: "tribe-side-description" }, ...renderStyledText(pr.description)) ) : null, pr.mapUrl ? div({ class: "job-section" }, renderMapEmbedWithZoom(params.mapData, pr.mapUrl, `/projects/${encodeURIComponent(pr.id || pr.key)}`, params.zoom)) : null, renderBudget(pr), renderBackers(pr, f), renderMilestonesAndBounties(pr, f, isAuthor), - renderFollowers(pr), renderPledgeBox(pr, f, isAuthor), - div({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(pr.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(pr.author) + renderFollowers(pr), + renderEngagement(pr.id || pr.key, + renderOpinionsVoting('/projects/opinions', pr.id || pr.key, pr.opinions, null, pr.opinions_inhabitants), + renderSharedCommentsSection({ + action: `/projects/${encodeURIComponent(pr.id || pr.key)}/comments`, + comments, + returnTo: `/projects/${encodeURIComponent(pr.id || pr.key)}` + }) ), - renderOpinionsVoting('/projects/opinions', pr.id || pr.key, pr.opinions, null, pr.opinions_inhabitants), - div( - { id: "comments", class: "vote-comments-section" }, - div({ class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/projects/${encodeURIComponent(pr.id || pr.key)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), br(), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text - return t && String(t).trim() - }) - return visibleComments.length - ? div({ class: "comments-list" }, - visibleComments.map((c) => { - const author = c?.value?.author || "" - const ts = c?.value?.timestamp || c?.timestamp - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "" - const relDate = ts ? moment(ts).fromNow() : "" - return div({ class: "comment-card" }, - div({ class: "comment-header" }, userLink(author)), - div({ class: "comment-date" }, span({ title: absDate }, relDate)), - div({ class: "comment-body" }, ...renderUrl(c?.value?.content?.text || "")) - ) - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - })() - ) ) return template( i18n.projectsTitle, section( - div({ class: "tags-header" }, h2(i18n.projectsTitle), p(i18n.projectsDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.projectsTitle), p(i18n.projectsDescription)), div( { class: "filters" }, form( { method: "GET", action: "/projects", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, i18n[x.i18n])) + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n]).toUpperCase())) .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.projectCreateProject)) ) ), @@ -697,10 +719,10 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { } exports.clearnetProjectView = async (project) => { - const { escapeHtml: esc, blobUrl: cnBlob, renderClearnetPage } = require('./clearnet_view'); + const { escapeHtml: esc, renderRichText, renderKindTag, blobUrl: cnBlob, renderClearnetPage } = require('./clearnet_view'); const pr = project || {}; - const title = esc(pr.title || 'Project'); - const desc = esc(pr.description || ''); + const title = esc(pr.title || i18n.cnKindProject); + const desc = renderRichText(pr.description || ''); const goal = Math.max(0, toNum(pr.goal) || 0); const pledged = Math.max(0, toNum(pr.pledged) || 0); const fundingPct = goal > 0 ? Math.min(100, Math.round((pledged / goal) * 100)) : 0; @@ -709,17 +731,18 @@ exports.clearnetProjectView = async (project) => { const deadline = pr.deadline ? new Date(pr.deadline).toISOString().slice(0, 10) : ''; const milestones = Array.isArray(pr.milestones) ? pr.milestones.slice(0, 10) : []; const milestonesBlock = milestones.length - ? `

Milestones

    ${milestones.map(m => `
  1. ${esc(m.title || '')}${m.targetPercent ? ` — ${m.targetPercent}%` : ''}
  2. `).join('')}
` + ? `

${esc(i18n.projectMilestones)}

    ${milestones.map(m => `
  1. ${esc(m.title || '')}${m.targetPercent ? ` — ${m.targetPercent}%` : ''}
  2. `).join('')}
` : ''; const extraCss = ` .cn-prj-title{color:var(--fg);margin:0 0 12px 0;font-size:32px;font-weight:700} -.cn-prj-status{display:inline-block;background:var(--bg-sub);border:1px solid var(--fg);color:var(--fg);padding:6px 12px;border-radius:6px;font-weight:600;text-transform:uppercase;letter-spacing:1px;font-size:12px;margin-bottom:16px} +.cn-prj-status{display:inline-flex;align-items:center;background:var(--bg-sub);border:1px solid var(--fg);color:var(--fg);padding:6px 12px;border-radius:6px;font-weight:600;text-transform:uppercase;letter-spacing:1px;font-size:12px;margin:0} .cn-prj-img{display:block;max-width:100%;border:1px solid var(--border);border-radius:8px;margin-bottom:20px} .cn-prj-funding{background:var(--bg-sub);border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:20px} .cn-prj-funding-label{color:var(--fg-dim);font-size:12px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px} .cn-prj-funding-amount{color:var(--fg);font-size:18px;font-weight:700;margin-bottom:8px} .cn-prj-bar{height:8px;background:#000;border-radius:4px;overflow:hidden} .cn-prj-bar-fill{height:100%;background:var(--fg);border-radius:4px} +${Array.from({ length: 21 }, (_, i) => `.cn-prj-bar-fill-${i * 5}{width:${i * 5}%}`).join('\n')} .cn-prj-section{margin-top:24px} .cn-prj-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:0 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)} .cn-prj-section p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:15px;margin:0} @@ -733,22 +756,24 @@ exports.clearnetProjectView = async (project) => { const body = `

${title}

- ${esc(status)} + ${renderKindTag('project')} + ${esc(i18n['projectStatus' + status] || status)} ${pr.createdAt ? `📅 ${esc(new Date(pr.createdAt).toISOString().slice(0,10))}` : ''}
+
${projectImg ? `${title}` : ''} ${goal > 0 ? `
-
Funding
+
${esc(i18n.projectFunding)}
${pledged.toFixed(2)} / ${goal.toFixed(2)} ECO · ${fundingPct}%
-
- ${deadline ? `
Deadline: ${deadline}
` : ''} +
+ ${deadline ? `
${esc(i18n.deadline)}: ${deadline}
` : ''}
` : ''} - ${desc ? `

Description

${desc}

` : ''} + ${desc ? `

${esc(i18n.description)}

${desc}

` : ''} ${milestonesBlock} `; return renderClearnetPage({ - title: `${pr.title || 'Project'} — Oasis`, + title: `${pr.title || 'Project'} | Oasis`, ogTitle: pr.title || 'Project', ogDescription: pr.description || '', ogImage: projectImg, diff --git a/src/views/report_view.js b/src/views/report_view.js index c3f261c..3dc904f 100644 --- a/src/views/report_view.js +++ b/src/views/report_view.js @@ -1,20 +1,22 @@ -const { div, h2, p, section, button, form, a, textarea, br, input, img, span, label, select, option, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views"); +const { hr, div, h2, p, section, button, form, a, textarea, br, input, img, span, label, select, option, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { template, i18n, renderOpinionsVoting, renderEngagement, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { renderPhotoGallery, renderGalleryFields, renderZoomableImage } = require("./gallery_view"); +const { renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const moment = require("../server/node_modules/moment"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const renderMediaBlob = (value, attrs = {}) => { if (!value) return null; const s = String(value).trim(); if (!s) return null; - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }); + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }); const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }); + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }); const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }); const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }); + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }); return null; }; @@ -38,8 +40,15 @@ const STATUS_BY_FILTER = { closed: "CLOSED" }; +const SEVERITY_BY_FILTER = { + sev_low: "LOW", + sev_medium: "MEDIUM", + sev_high: "HIGH", + sev_critical: "CRITICAL" +}; + const opt = (value, isSelected, text) => - option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text); + option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text); const hasAnyTemplateValue = (t) => { if (!t || typeof t !== "object") return false; @@ -50,7 +59,7 @@ const renderCardField = (labelText, value = "") => div( { class: "card-field" }, span({ class: "card-label" }, labelText), - span({ class: "card-value" }, ...renderUrl(String(value ?? ""))) + span({ class: "card-value" }, ...renderStyledText(String(value ?? ""))) ); const renderStackedTextField = (lbl, val) => @@ -58,16 +67,7 @@ const renderStackedTextField = (lbl, val) => ? div( { class: "card-field card-field-stacked" }, span({ class: "card-label" }, lbl), - span({ class: "card-value" }, ...renderUrl(String(val))) - ) - : null; - -const renderPmButton = (recipientId) => - recipientId && String(recipientId) !== String(userId) - ? form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: recipientId }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) + span({ class: "card-value" }, ...renderStyledText(String(val))) ) : null; @@ -135,7 +135,6 @@ const renderTemplateDetails = (report) => { h2({ class: "report-template-title" }, i18n.reportsContentTemplateTitle), renderStackedTextField(i18n.reportsContentLocationLabel + ":", t.contentLocation), renderStackedTextField(i18n.reportsWhyInappropriateLabel + ":", t.whyInappropriate), - renderStackedTextField(i18n.reportsRequestedActionLabel + ":", t.requestedAction), renderStackedTextField(i18n.reportsEvidenceLinksLabel + ":", t.evidenceLinks) ); } @@ -144,73 +143,11 @@ const renderTemplateDetails = (report) => { }; const renderReportCommentsSection = (reportId, comments = []) => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { - method: "POST", - action: `/reports/${encodeURIComponent(reportId)}/comments`, - class: "comment-form", - enctype: "multipart/form-data" - }, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); - }); - return visibleComments.length - ? div( - { class: "comments-list" }, - visibleComments.map((c) => { - const author = c.value && c.value.author ? c.value.author : ""; - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c.value && c.value.content ? c.value.content : {}; - const root = content.fork || content.root || ""; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet); - })() - ); + return renderSharedCommentsSection({ + action: `/reports/${encodeURIComponent(reportId)}/comments`, + comments: comments, + returnTo: null + }); }; const renderTemplateForCategory = (category, templateData = {}) => { @@ -225,23 +162,19 @@ const renderTemplateForCategory = (category, templateData = {}) => { h2({ class: "report-template-title" }, i18n.reportsBugTemplateTitle), label(i18n.reportsStepsToReproduceLabel), br(), - textarea({ name: "stepsToReproduce", rows: "4", placeholder: i18n.reportsStepsToReproducePlaceholder }, tval("stepsToReproduce")), - br(), + textarea({ maxlength: "5000", name: "stepsToReproduce", rows: "4", placeholder: i18n.reportsStepsToReproducePlaceholder }, tval("stepsToReproduce")), br(), label(i18n.reportsExpectedBehaviorLabel), br(), - textarea({ name: "expectedBehavior", rows: "3", placeholder: i18n.reportsExpectedBehaviorPlaceholder }, tval("expectedBehavior")), - br(), + textarea({ maxlength: "5000", name: "expectedBehavior", rows: "3", placeholder: i18n.reportsExpectedBehaviorPlaceholder }, tval("expectedBehavior")), br(), label(i18n.reportsActualBehaviorLabel), br(), - textarea({ name: "actualBehavior", rows: "3", placeholder: i18n.reportsActualBehaviorPlaceholder }, tval("actualBehavior")), - br(), + textarea({ maxlength: "5000", name: "actualBehavior", rows: "3", placeholder: i18n.reportsActualBehaviorPlaceholder }, tval("actualBehavior")), br(), label(i18n.reportsEnvironmentLabel), br(), - textarea({ name: "environment", rows: "3", placeholder: i18n.reportsEnvironmentPlaceholder }, tval("environment")), - br(), + textarea({ maxlength: "5000", name: "environment", rows: "3", placeholder: i18n.reportsEnvironmentPlaceholder }, tval("environment")), br(), label(i18n.reportsReproduceRateLabel), br(), @@ -257,48 +190,21 @@ const renderTemplateForCategory = (category, templateData = {}) => { ); } - if (cat === "ABUSE") { - return div( - { class: "report-template-block" }, - h2({ class: "report-template-title" }, i18n.reportsAbuseTemplateTitle), - label(i18n.reportsWhatHappenedLabel), - br(), - textarea({ name: "whatHappened", rows: "4", placeholder: i18n.reportsWhatHappenedPlaceholder }, tval("whatHappened")), - br(), - br(), - label(i18n.reportsReportedUserLabel), - br(), - textarea({ name: "reportedUser", rows: "2", placeholder: i18n.reportsReportedUserPlaceholder }, tval("reportedUser")), - br(), - br(), - label(i18n.reportsEvidenceLinksLabel), - br(), - textarea({ name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks")) - ); - } - if (cat === "CONTENT") { return div( { class: "report-template-block" }, h2({ class: "report-template-title" }, i18n.reportsContentTemplateTitle), label(i18n.reportsContentLocationLabel), br(), - textarea({ name: "contentLocation", rows: "3", placeholder: i18n.reportsContentLocationPlaceholder }, tval("contentLocation")), - br(), + textarea({ maxlength: "5000", name: "contentLocation", rows: "3", placeholder: i18n.reportsContentLocationPlaceholder }, tval("contentLocation")), br(), label(i18n.reportsWhyInappropriateLabel), br(), - textarea({ name: "whyInappropriate", rows: "4", placeholder: i18n.reportsWhyInappropriatePlaceholder }, tval("whyInappropriate")), - br(), - br(), - label(i18n.reportsRequestedActionLabel), - br(), - textarea({ name: "requestedAction", rows: "3", placeholder: i18n.reportsRequestedActionPlaceholder }, tval("requestedAction")), - br(), + textarea({ maxlength: "5000", name: "whyInappropriate", rows: "4", placeholder: i18n.reportsWhyInappropriatePlaceholder }, tval("whyInappropriate")), br(), label(i18n.reportsEvidenceLinksLabel), br(), - textarea({ name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks")) + textarea({ maxlength: "5000", name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks")) ); } @@ -307,17 +213,15 @@ const renderTemplateForCategory = (category, templateData = {}) => { h2({ class: "report-template-title" }, i18n.reportsFeatureTemplateTitle), label(i18n.reportsProblemStatementLabel), br(), - textarea({ name: "problemStatement", rows: "4", placeholder: i18n.reportsProblemStatementPlaceholder }, tval("problemStatement")), - br(), + textarea({ maxlength: "5000", name: "problemStatement", rows: "4", placeholder: i18n.reportsProblemStatementPlaceholder }, tval("problemStatement")), br(), label(i18n.reportsUserStoryLabel), br(), - textarea({ name: "userStory", rows: "3", placeholder: i18n.reportsUserStoryPlaceholder }, tval("userStory")), - br(), + textarea({ maxlength: "5000", name: "userStory", rows: "3", placeholder: i18n.reportsUserStoryPlaceholder }, tval("userStory")), br(), label(i18n.reportsAcceptanceCriteriaLabel), br(), - textarea({ name: "acceptanceCriteria", rows: "4", placeholder: i18n.reportsAcceptanceCriteriaPlaceholder }, tval("acceptanceCriteria")) + textarea({ maxlength: "5000", name: "acceptanceCriteria", rows: "4", placeholder: i18n.reportsAcceptanceCriteriaPlaceholder }, tval("acceptanceCriteria")) ); }; @@ -368,146 +272,191 @@ const renderReportCategoryChip = (category) => { const renderReportCard = (report, userId, currentFilter = "all", spreadInfo) => { const confirmations = Array.isArray(report.confirmations) ? report.confirmations : []; + const origin = report.tribeOrigin || null; const chips = [ renderReportStatusChip(report.status), renderReportSeverityChip(report.severity), - renderReportCategoryChip(report.category), - renderLifespanChip(report.lifetime, i18n) + origin && !report.category ? null : renderReportCategoryChip(report.category), + renderLifespanChip(report.lifetime, i18n), + origin ? renderTribeOriginChip(origin) : null ].filter(Boolean); return div({ class: "tribe-card report-card" }, + origin ? null : div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(report.id, `/reports/${encodeURIComponent(report.id)}`, { spread: spreadInfo || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false, deleteAction: String(report.author) === String(userId) ? contentDeleteAction('report', report.id) : undefined }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/reports/${encodeURIComponent(report.id)}` }, report.title || i18n.reportsTitle) + a({ href: origin ? origin.href : `/reports/${encodeURIComponent(report.id)}` }, report.title || i18n.reportsTitle) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "tribe-card-members" }, + origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.reportsConfirmations}: ${confirmations.length}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(report.id, spreadInfo)), - div({ class: "card-visit-btn-centered" }, - form({ method: "GET", action: `/reports/${encodeURIComponent(report.id)}` }, - input({ type: "hidden", name: "filter", value: currentFilter }), - button({ type: "submit", class: "filter-btn" }, i18n.viewReport || "View Report") - ) ) ) ); }; +const reportChipFor = (filter, censusReports) => (mode) => { + if (mode === filter || mode === "all") return true; + if (!Array.isArray(censusReports)) return true; + if (mode === "top") return censusReports.length > 0; + if (mode === "mine") return censusReports.some((r) => r.author === userId); + if (mode === "recent") return censusReports.length > 0; + if (mode === "confirmed") return censusReports.some((r) => Array.isArray(r.confirmations) && r.confirmations.includes(userId)); + if (CATEGORY_BY_FILTER[mode]) return censusReports.some((r) => normU(r.category) === CATEGORY_BY_FILTER[mode]); + if (STATUS_BY_FILTER[mode]) return censusReports.some((r) => normalizeStatus(r.status) === STATUS_BY_FILTER[mode]); + if (SEVERITY_BY_FILTER[mode]) return censusReports.some((r) => normU(r.severity) === SEVERITY_BY_FILTER[mode]); + return true; +}; + exports.reportView = async (reports, filter, reportId, createCategory, params = {}) => { - const title = - filter === "create" ? i18n.reportsCreateButton : - filter === "edit" ? i18n.reportsUpdateButton : - filter === "mine" ? i18n.reportsMineSectionTitle : - filter === "features" ? i18n.reportsFeaturesSectionTitle : - filter === "bugs" ? i18n.reportsBugsSectionTitle : - filter === "abuse" ? i18n.reportsAbuseSectionTitle : - filter === "content" ? i18n.reportsContentSectionTitle : - filter === "confirmed" ? i18n.reportsConfirmedSectionTitle : - filter === "open" ? i18n.reportsOpenSectionTitle : - filter === "under_review" ? i18n.reportsUnderReviewSectionTitle : - filter === "resolved" ? i18n.reportsResolvedSectionTitle : - filter === "invalid" ? i18n.reportsInvalidSectionTitle : - i18n.reportsAllSectionTitle; + const title = i18n.reportsTitle; let filtered = Array.isArray(reports) ? reports : []; + const dayAgoReports = Date.now() - 86400000; + const confirmationsOf = (r) => (Array.isArray(r.confirmations) ? r.confirmations.length : 0); if (filter === "mine") { filtered = filtered.filter((r) => r.author === userId); + } else if (filter === "recent") { + filtered = filtered.filter((r) => new Date(r.createdAt).getTime() >= dayAgoReports); } else if (filter === "confirmed") { filtered = filtered.filter((r) => Array.isArray(r.confirmations) && r.confirmations.includes(userId)); + } else if (filter === "top") { + filtered = filtered.filter((r) => !r.tribeOrigin); } else if (CATEGORY_BY_FILTER[filter]) { const wanted = CATEGORY_BY_FILTER[filter]; filtered = filtered.filter((r) => normU(r.category) === wanted); } else if (STATUS_BY_FILTER[filter]) { const wanted = STATUS_BY_FILTER[filter]; filtered = filtered.filter((r) => normalizeStatus(r.status) === wanted); + } else if (SEVERITY_BY_FILTER[filter]) { + const wanted = SEVERITY_BY_FILTER[filter]; + filtered = filtered.filter((r) => normU(r.severity) === wanted); } - filtered = filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + filtered = filter === "top" + ? filtered.sort((a, b) => confirmationsOf(b) - confirmationsOf(a) || new Date(b.createdAt) - new Date(a.createdAt)) + : filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + const emptyMod = moduleIsEmpty(filtered, filter, "all", params.q); + const censusReports = Array.isArray(reports) ? reports : []; + const reportChip = reportChipFor(filter, censusReports); + const chipBtn = (mode, label) => reportChip(mode) + ? [button({ type: "submit", name: "filter", value: mode, class: btnClass(mode) }, String(label).toUpperCase())] + : []; const reportToEdit = filter === "edit" ? (Array.isArray(reports) ? reports.find((r) => r.id === reportId) : null) : null; + const formData = reportToEdit || params.draft || {}; const btnClass = (v) => (filter === v ? "filter-btn active" : "filter-btn"); - const selectedCategory = normU( + const rawCategory = normU( filter === "create" ? (createCategory || "FEATURES") : (reportToEdit?.category || "FEATURES") ); + const selectedCategory = ["FEATURES", "BUGS", "CONTENT"].includes(rawCategory) ? rawCategory : "FEATURES"; const selectedTemplate = reportToEdit?.template && typeof reportToEdit.template === "object" ? reportToEdit.template : {}; const applyLabel = i18n.apply || "Apply"; - const sev = String(reportToEdit?.severity || "low"); + const sev = String(formData.severity || "low"); const hiddenDescription = String(reportToEdit?.description || ""); return template( title, section( div( - { class: "tags-header" }, + { class: "tags-header module-header-line" }, h2(i18n.reportsTitle), p(i18n.reportsDescription) ), div( { class: "filters" }, + ...(emptyMod ? [] : [form( + { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters" }, + ...chipBtn("all", i18n.reportsFilterAll), + ...chipBtn("recent", i18n.reportsFilterRecent), + ...chipBtn("mine", i18n.reportsFilterMine), + ...chipBtn("top", i18n.reportsFilterTop), + ...chipBtn("features", i18n.reportsFilterFeatures), + ...chipBtn("bugs", i18n.reportsFilterBugs), + ...chipBtn("content", i18n.reportsFilterContent), + ...chipBtn("confirmed", i18n.reportsFilterConfirmed), + )]), form( - { method: "GET", action: "/reports" }, - button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, i18n.reportsFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, i18n.reportsFilterMine), - button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, i18n.reportsFilterFeatures), - button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, i18n.reportsFilterBugs), - button({ type: "submit", name: "filter", value: "abuse", class: btnClass("abuse") }, i18n.reportsFilterAbuse), - button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, i18n.reportsFilterContent), - button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, i18n.reportsFilterConfirmed), - button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, i18n.reportsFilterOpen), - button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, i18n.reportsFilterUnderReview), - button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, i18n.reportsFilterResolved), - button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, i18n.reportsFilterInvalid), + { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters reports-subfilters" }, + ...(emptyMod ? [] : [ + ...chipBtn("open", i18n.reportsFilterOpen), + ...chipBtn("under_review", i18n.reportsFilterUnderReview), + ...chipBtn("resolved", i18n.reportsFilterResolved), + ...chipBtn("invalid", i18n.reportsFilterInvalid), + ...chipBtn("sev_low", i18n.reportsSeverityLow), + ...chipBtn("sev_medium", i18n.reportsSeverityMedium), + ...chipBtn("sev_high", i18n.reportsSeverityHigh), + ...chipBtn("sev_critical", i18n.reportsSeverityCritical), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.reportsCreateButton) ) - ) + ), + filter === "edit" || filter === "create" + ? null + : emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, r => normalizeStatus(r.status || 'OPEN'), [{ value: 'OPEN', label: i18n.reportsStatusOpen }, { value: 'UNDER_REVIEW', label: i18n.reportsStatusUnderReview }, { value: 'RESOLVED', label: i18n.reportsStatusResolved }, { value: 'CLOSED', label: i18n.reportsStatusClosed }, { value: 'INVALID', label: i18n.reportsStatusInvalid }]), + form({ method: "GET", action: "/reports", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.reportsSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) ), section( filter === "edit" || filter === "create" ? div( { class: "report-form" }, + filter === "edit" ? await renderSpreadEditWarning(reportToEdit && (reportToEdit.id || reportToEdit.key)) : null, filter === "create" - ? div( - label(i18n.reportsTitleLabel), - br(), - input({ type: "text", name: "title", required: true, value: "", form: "report-create-form" }), - br(), - br(), + ? [ form( { id: "report-category-form", method: "GET", action: "/reports" }, input({ type: "hidden", name: "filter", value: "create" }), - label(i18n.reportsCategory), - br(), - select( - { name: "category" }, - opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures), - opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs), - opt("ABUSE", selectedCategory === "ABUSE", i18n.reportsCategoryAbuse), - opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) - ), - br(), - br(), - button({ type: "submit", class: "create-button" }, applyLabel) + div({ class: "apply-row" }, + select( + { name: "category", class: "report-category-select" }, + opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures), + opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs), + opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) + ), + button({ type: "submit", class: "create-button" }, applyLabel) + ) ), - br(), + hr({ class: "form-sep" }), h2({ class: "report-category-fixed" }, selectedCategory), - br(), form( { id: "report-create-form", action: "/reports/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "category", value: selectedCategory }), input({ type: "hidden", name: "description", value: "" }), + label(i18n.reportsTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", required: true, value: params.prefillTitle || "" }), + br(), + renderTemplateForCategory(selectedCategory, params.draft || {}), + br(), + ...renderGalleryFields(formData, false, 8), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob" }), br(), br(), + label("Tags"), + br(), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: Array.isArray(formData.tags) ? formData.tags.join(", ") : (formData.tags || "") }), + br(), label(i18n.reportsSeverity), br(), select( @@ -519,28 +468,15 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = ), br(), br(), - h2({ class: "report-template-main-title" }, i18n.reportsTemplateSectionTitle), - renderTemplateForCategory(selectedCategory, {}), - label(i18n.reportsUploadFile), - br(), - input({ type: "file", name: "image" }), - br(), - br(), - label("Tags"), - br(), - input({ type: "text", name: "tags", value: "" }), - br(), - br(), button({ type: "submit", class: "create-button" }, i18n.reportsCreateButton) ) - ) - : div( + ] + : [ form( { id: "report-edit-form", action: `/reports/update/${encodeURIComponent(reportId)}`, method: "POST", enctype: "multipart/form-data" }, label(i18n.reportsTitleLabel), br(), - input({ type: "text", name: "title", required: true, value: reportToEdit?.title || "" }), - br(), + input({ type: "text", name: "title", maxlength: "100", required: true, value: reportToEdit?.title || "" }), br(), input({ type: "hidden", name: "description", value: hiddenDescription }), label(i18n.reportsCategory), @@ -549,10 +485,15 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = { name: "category", required: true }, opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures), opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs), - opt("ABUSE", selectedCategory === "ABUSE", i18n.reportsCategoryAbuse), opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) ), br(), + renderTemplateForCategory(selectedCategory, selectedTemplate), + br(), + ...renderGalleryFields(reportToEdit || {}, true, 8), + label("Tags"), + br(), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: reportToEdit?.tags?.join(", ") || "" }), br(), label(i18n.reportsSeverity), br(), @@ -565,32 +506,19 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = ), br(), br(), - h2({ class: "report-template-main-title" }, i18n.reportsTemplateSectionTitle), - renderTemplateForCategory(selectedCategory, selectedTemplate), - br(), - br(), - label(i18n.reportsUploadFile), - br(), - input({ type: "file", name: "image" }), - br(), - br(), - label("Tags"), - br(), - input({ type: "text", name: "tags", value: reportToEdit?.tags?.join(", ") || "" }), - br(), - br(), button({ type: "submit" }, i18n.reportsUpdateButton) ) - ) + ] ) : filtered.length > 0 - ? div({ class: "jobs-grid" }, filtered.map((r) => renderReportCard(r, userId, filter, params.spreadMap && params.spreadMap.get(r.id)))) - : p(i18n.reportsNoItems) + ? div({ class: "jobs-grid" }, paged(filtered).map((r) => renderReportCard(r, userId, filter, params.spreadMap && params.spreadMap.get(r.id)))) + : div({ class: "no-content-box" }, p(i18n.reportsNoItems)) ) ); }; exports.singleReportView = async (report, filter, comments = [], params = {}) => { + const reportChip = (mode) => reportChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null)(mode); const btnClass = (v) => (filter === v ? "filter-btn active" : "filter-btn"); const confirmations = Array.isArray(report.confirmations) ? report.confirmations : []; const isAuthor = String(report.author) === String(userId); @@ -605,21 +533,13 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => ].filter(Boolean); const sideActions = []; - const pm = renderPmButton(report.author); - if (pm) sideActions.push(pm); sideActions.push(form({ method: "POST", action: `/reports/confirm/${encodeURIComponent(report.id)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.reportsConfirmButton) - )); - sideActions.push(a({ href: "/tasks?filter=create", target: "_blank" }, - button({ type: "button", class: "filter-btn" }, i18n.reportsCreateTaskButton) + button({ type: "submit", class: "tribe-action-btn" }, i18n.reportsConfirmButton) )); + const ownerActions = []; if (isAuthor) { - sideActions.push(renderReportStatusSetter(report)); - sideActions.push(form({ method: "GET", action: `/reports/edit/${encodeURIComponent(report.id)}` }, - button({ type: "submit", class: "update-btn" }, i18n.reportsUpdateButton) - )); - sideActions.push(form({ method: "POST", action: `/reports/delete/${encodeURIComponent(report.id)}` }, - button({ type: "submit", class: "delete-btn" }, i18n.reportsDeleteButton) + ownerActions.push(form({ method: "GET", action: `/reports/edit/${encodeURIComponent(report.id)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.reportsUpdateButton) )); } @@ -640,53 +560,68 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => pushRow(i18n.reportsCategory, report.category); const reportSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(report.id, `/reports/${encodeURIComponent(report.id)}`, { spread: params.spreads || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false, returnTo: `/reports/${encodeURIComponent(report.id)}`, deleteAction: isAuthor ? contentDeleteAction('report', report.id) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, report.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(report.id, params.spreads)), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(report.createdAt, report.author)), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.reportsConfirmations}: ${confirmations.length}`) ), - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + isAuthor + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.reportsStatus}: `), + renderStateChip(normalizeStatus(report.status) === "RESOLVED" ? "mutuals" : "whole", "◆", String(report.status || "OPEN").toUpperCase()), + renderReportStatusSetter(report) + ) + : null, + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + renderDocumentActions('reports', report.id, [ + a({ href: "/tasks?filter=create", class: "filter-btn" }, i18n.reportsCreateTaskButton) + ]) ); const opinionsBar = renderOpinionsVoting('/reports/opinions', report.id, report.opinions, null, report.opinions_inhabitants); const reportMain = div({ class: "tribe-main" }, - (details || report.image) ? div({ class: "job-section" }, - details || null, - report.image ? renderMediaBlob(report.image, { class: "report-detail-image" }) : null - ) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(report.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(report.author) - ), - opinionsBar, - renderReportCommentsSection(report.id, comments) + details ? div({ class: "job-section" }, details) : null, + renderPhotoGallery(report, 'report'), + renderEngagement(report.id, opinionsBar, renderReportCommentsSection(report.id, comments)) ); return template( report.title, section( + div({ class: "tags-header module-header-line" }, h2(i18n.reportsTitle), p(i18n.reportsDescription)), div( { class: "filters" }, form( - { method: "GET", action: "/reports" }, - button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, i18n.reportsFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, i18n.reportsFilterMine), - button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, i18n.reportsFilterFeatures), - button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, i18n.reportsFilterBugs), - button({ type: "submit", name: "filter", value: "abuse", class: btnClass("abuse") }, i18n.reportsFilterAbuse), - button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, i18n.reportsFilterContent), - button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, i18n.reportsFilterConfirmed), - button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, i18n.reportsFilterOpen), - button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, i18n.reportsFilterUnderReview), - button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, i18n.reportsFilterResolved), - button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, i18n.reportsFilterInvalid), + { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters" }, + button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, String(i18n.reportsFilterAll).toUpperCase()), + ...(reportChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: btnClass("recent") }, String(i18n.reportsFilterRecent).toUpperCase())] : []), + ...(reportChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, String(i18n.reportsFilterMine).toUpperCase())] : []), + button({ type: "submit", name: "filter", value: "top", class: btnClass("top") }, String(i18n.reportsFilterTop).toUpperCase()), + ...(reportChip("features") ? [button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, String(i18n.reportsFilterFeatures).toUpperCase())] : []), + ...(reportChip("bugs") ? [button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, String(i18n.reportsFilterBugs).toUpperCase())] : []), + ...(reportChip("content") ? [button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, String(i18n.reportsFilterContent).toUpperCase())] : []), + ...(reportChip("confirmed") ? [button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, String(i18n.reportsFilterConfirmed).toUpperCase())] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.reportsCreateButton) + ), + form( + { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters reports-subfilters" }, + ...(reportChip("open") ? [button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, String(i18n.reportsFilterOpen).toUpperCase())] : []), + ...(reportChip("under_review") ? [button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, String(i18n.reportsFilterUnderReview).toUpperCase())] : []), + ...(reportChip("resolved") ? [button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, String(i18n.reportsFilterResolved).toUpperCase())] : []), + ...(reportChip("invalid") ? [button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, String(i18n.reportsFilterInvalid).toUpperCase())] : []), + ...(reportChip("sev_low") ? [button({ type: "submit", name: "filter", value: "sev_low", class: btnClass("sev_low") }, String(i18n.reportsSeverityLow).toUpperCase())] : []), + ...(reportChip("sev_medium") ? [button({ type: "submit", name: "filter", value: "sev_medium", class: btnClass("sev_medium") }, String(i18n.reportsSeverityMedium).toUpperCase())] : []), + ...(reportChip("sev_high") ? [button({ type: "submit", name: "filter", value: "sev_high", class: btnClass("sev_high") }, String(i18n.reportsSeverityHigh).toUpperCase())] : []), + ...(reportChip("sev_critical") ? [button({ type: "submit", name: "filter", value: "sev_critical", class: btnClass("sev_critical") }, String(i18n.reportsSeverityCritical).toUpperCase())] : []) ) ), div({ class: "tribe-details" }, reportSide, reportMain) diff --git a/src/views/rooms_view.js b/src/views/rooms_view.js new file mode 100644 index 0000000..ba9efc7 --- /dev/null +++ b/src/views/rooms_view.js @@ -0,0 +1,526 @@ +const { div, h2, p, section, button, form, a, span, br, hr, label, textarea, input, select, option, img, table, tr, td, details, summary, ul, li } = require("../server/node_modules/hyperaxe") +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderModuleStatsBy, moduleIsEmpty, liveClock, renderPhoneChip, contentDeleteAction, paged, renderVideoGrid, renderLiveFeed, renderIcon } = require("./main_views") +const { renderEncryptedChip, renderTransportChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const { renderStyledText } = require("../backend/renderStyledText") +const moment = require("../server/node_modules/moment") +const { config } = require("../server/SSB_server.js") + +const userId = config.keys.id +const ROOM_MAX = 50 +const MODES = ["all", "recent", "mine", "live", "open", "invite", "closed"] + +const safeArr = (v) => (Array.isArray(v) ? v : []) +const roomHref = (room) => `/rooms/${encodeURIComponent(room.rootId)}` +const occOf = (params, room) => (params && params.occupancy && params.occupancy.get(room.rootId)) || null +const modeLabel = (f) => String(i18n[`roomFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f).toUpperCase() +const ROOM_REACH = ["OPEN", "INVITE-ONLY"] +const roomReachLabel = (s) => s === "INVITE-ONLY" ? i18n.roomStatusInviteOnly : i18n.roomStatusOpen +const clearnetEligible = (room) => !!room && !room.tribeId && !room.encrypted && room.type === "OPEN" && String(room.status || "").toUpperCase() === "OPEN" && !room.isClosed +const roomClearnetHref = (room) => clearnetItemHref("rooms", room.title, room.rootId) + +const renderRoomStatusChip = (room) => { + const s = room.isClosed ? "CLOSED" : room.type + const variant = s === "CLOSED" ? "closed" : s === "INVITE-ONLY" ? "whole" : "mutuals" + const icon = s === "CLOSED" ? "✗" : s === "INVITE-ONLY" ? "🔑" : "✓" + const label = s === "CLOSED" ? i18n.roomStatusClosed : s === "INVITE-ONLY" ? i18n.roomStatusInviteOnly : i18n.roomStatusOpen + return renderStateChip(variant, icon, label) +} + +const renderRoomNumberChip = (room, occ, inside) => { + if (!room.number) return null + if (room.line === "SWITCHBOARD" && !inside && occ && Number(occ.count) > 0) return renderPhoneChip(room.number, i18n.roomNumberHint) + return span({ title: room.line !== "SWITCHBOARD" ? i18n.roomLineDndError : inside ? i18n.roomYouAreIn : i18n.roomNobodyInside }, renderStateChip("whole", "✆", room.number)) +} +const renderRoomLineChip = (room) => room.isClosed || String(room.author) === String(userId) + ? null + : renderStateChip(room.line === "SWITCHBOARD" ? "mutuals" : "hidden", null, String(room.line === "SWITCHBOARD" ? i18n.roomLineSwitchboard : i18n.phoneDnd).toUpperCase()) + +const renderLiveChip = (occ) => { + if (!occ) return null + const count = Number(occ.count) || 0 + const max = occ.max || ROOM_MAX + return count > 0 ? renderStateChip("mutuals", "●", `${i18n.roomLiveChip} ${count}/${max}`) : renderStateChip("neutral", "●", `${count}/${max}`) +} + +const roomChips = (room, occ, inside = false) => [ + renderRoomStatusChip(room), + renderEncryptedChip(i18n), + room.type === "OPEN" && !room.tribeId ? renderTransportChip(i18n) : null, + renderRoomNumberChip(room, occ, inside), + renderRoomLineChip(room), + renderLiveChip(occ), + renderLifespanChip(room.lifetime, i18n) +].filter(Boolean) + +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => + div({ class: "tribe-mode-buttons" }, + ...(emptyMod ? [] : [ + MODES.filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] === true)).map(f => + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: f }), + button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, modeLabel(f)) + ) + ) + ]), + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "create" }), + button({ type: "submit", class: "create-button" }, i18n.roomCreate) + ) + ) + +const renderCover = (room, cls) => room.image ? img({ loading: "lazy", src: String(room.image).startsWith("/blob/sealed/") ? room.image : `/blob/${encodeURIComponent(room.image)}`, class: cls, alt: "" }) : null + +const roomIsFull = (occ) => !!occ && occ.count >= ((occ && occ.max) || ROOM_MAX) + +const joinForm = (room, occ, cls = "tribe-action-btn") => { + const max = (occ && occ.max) || ROOM_MAX + const count = occ ? occ.count : null + const full = count !== null && count >= max + return form({ method: "POST", action: `/rooms/join/${encodeURIComponent(room.rootId)}`, class: "room-join-form" }, + button({ type: "submit", class: cls, ...(full ? { disabled: true } : {}) }, + String(full ? i18n.roomFull : i18n.roomJoin).toUpperCase()) + ) +} + +const renderRoomItem = (room, params = {}) => { + const inside = params.live && params.live.ref === room.rootId + const occ = inside && Number.isFinite(Number(params.live.count)) ? { count: Number(params.live.count), max: params.live.max } : occOf(params, room) + const canJoin = !room.isClosed && !inside && (room.type === "OPEN" || room.tribeId || String(room.author) === String(userId) || safeArr(room.members).includes(userId)) + return li({ class: "mailing-archive-item room-item" }, + div({ class: "emergency-update-head mailing-archive-head" }, + div({ class: "mailing-archive-meta" }, + a({ href: roomHref(room), class: "user-link" }, room.title || "—"), + span({ class: "card-label activity-update-counts mailing-counts" }, `👥: ${safeArr(room.members).length}`), + ...roomChips(room, occ, inside), + room.clearnet === true && clearnetEligible(room) ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, + inside ? renderStateChip("whole", "ꘒ", String(i18n.roomYouAreIn).toUpperCase()) : null + ), + renderContentActions(room.rootId, roomHref(room), { spread: (params.spreadMap && params.spreadMap.get(room.rootId)) || null, author: room.author, favKind: "rooms", isFavorite: room.isFavorite, reportTitle: room.title, deleteAction: String(room.author) === String(userId) ? contentDeleteAction("room", room.rootId) : undefined }) + ) + ) +} + +const renderRoomForm = (room, params = {}) => { + const tribeId = String(params.tribeId || "") + const isEdit = !!room + const draft = isEdit ? room : (params.draft || {}) + const reachRaw = String(params.reach || draft.status || "").toUpperCase() + const reach = ROOM_REACH.includes(reachRaw) ? reachRaw : "OPEN" + const withReach = !isEdit && !tribeId + const showClearnet = isEdit ? clearnetEligible(room) : (withReach && reach === "OPEN") + return div({ class: "div-center audio-form" }, + h2(isEdit ? i18n.roomUpdateSectionTitle : i18n.roomCreateSectionTitle), + withReach + ? [ + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "create" }), + label(i18n.roomTypeLabel), br(), + div({ class: "apply-row" }, + select({ name: "status", class: "report-category-select" }, + ...ROOM_REACH.map(s => option({ value: s, ...(reach === s ? { selected: true } : {}) }, roomReachLabel(s))) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, roomReachLabel(reach)) + ] + : null, + form({ method: "POST", action: isEdit ? `/rooms/update/${encodeURIComponent(room.rootId)}` : "/rooms/create", enctype: "multipart/form-data" }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + withReach ? input({ type: "hidden", name: "status", value: reach }) : null, + span(i18n.title || "Title"), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.roomTitlePlaceholder, value: draft.title || "" }), br(), + span(i18n.roomDescriptionLabel), br(), + textarea({ name: "description", rows: 4, maxlength: "1000", placeholder: i18n.roomDescriptionPlaceholder }, draft.description || ""), br(), + span(i18n.uploadMedia), br(), + input({ type: "file", name: "image", accept: "image/*" }), br(), br(), + span(i18n.roomTagsLabel), br(), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: safeArr(draft.tags).join(", ") }), br(), + showClearnet ? renderClearnetSelector(draft.clearnet === true || String(draft.clearnet || "") === "1", i18n) : null, + button({ type: "submit", class: "create-button" }, isEdit ? i18n.roomUpdate : i18n.roomCreate) + ) + ) +} + +exports.renderRoomInvitePage = (code) => + template(i18n.roomInviteMode || i18n.tribeInviteCodeText, + section(div({ class: "invite-page" }, + h2(i18n.tribeInviteCodeText, code), + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "all" }), + button({ type: "submit", class: "filter-btn" }, i18n.walletBack) + ) + )) + ) + +exports.roomsView = async (rooms, filter, roomToEdit, params = {}) => { + const list = safeArr(rooms) + const q = String(params.q || "").trim() + const isForm = filter === "create" || filter === "edit" + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q) + const shown = q + ? list.filter(r => [r.title, r.description, r.number, ...safeArr(r.tags)].some(v => String(v || "").toLowerCase().includes(q.toLowerCase()))) + : list + return template(i18n.roomsTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.roomsTitle), p(i18n.roomsDescription)), + renderModeButtons(filter, emptyMod, params.modesAvail || null), + !isForm && !emptyMod + ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(shown, r => r.isClosed ? "CLOSED" : r.type, [{ value: "OPEN", label: i18n.roomStatusOpen }, { value: "INVITE-ONLY", label: i18n.roomStatusInviteOnly }, { value: "CLOSED", label: i18n.roomStatusClosed }]), + form({ method: "GET", action: "/rooms", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", placeholder: i18n.roomSearchPlaceholder, value: q, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ) + : null, + isForm + ? renderRoomForm(filter === "edit" ? roomToEdit : null, params) + : (shown.length + ? ul({ class: "mailing-archive" }, ...paged(shown).map(r => renderRoomItem(r, params))) + : div({ class: "tribe-grid" }, p(i18n.roomsNoItems))) + ) + ) +} + +const eventLine = (ev) => { + const key = ev.t === "join" ? "roomEventJoined" : ev.t === "leave" ? "roomEventLeft" : ev.t === "recStart" ? "roomEventRecStart" : ev.t === "mute" ? "roomEventMuted" : ev.t === "unmute" ? "roomEventUnmuted" : ev.t === "hand" ? "roomEventHand" : "roomEventRecStop" + const [before, after] = String(i18n[key] || "{name}").split("{name}") + return [before, userLink(ev.id), after] +} +const renderRecChip = () => span({ class: "room-rec-chip" }, "● " + String(i18n.roomRecording).toUpperCase()) +const fmtDuration = (ms) => { const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000)); return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}` } +const refreshLink = (room) => a({ href: roomHref(room), class: "tribe-action-btn room-refresh-btn" }, String(i18n.liveRefresh).toUpperCase()) +const renderLiveControls = (room, live) => div({ class: "room-live-controls" }, + form({ method: "POST", action: "/rooms/mute", class: "phone-action-form" }, + input({ type: "hidden", name: "mute", value: live.muted ? "0" : "1" }), + input({ type: "hidden", name: "returnTo", value: roomHref(room) }), + button({ type: "submit", class: "tribe-action-btn" }, String(live.muted ? i18n.phoneUnmute : i18n.phoneMute).toUpperCase()) + ), + live.videoAvailable + ? form({ method: "POST", action: "/rooms/camera", class: "phone-action-form" }, + input({ type: "hidden", name: "cam", value: live.cam ? "0" : "1" }), + input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }), + button({ type: "submit", class: live.cam ? "tribe-action-btn room-cam-btn-on" : "tribe-action-btn" }, String(live.cam ? i18n.phoneCameraOff : i18n.phoneCameraOn).toUpperCase()) + ) + : null, + live.streaming != null + ? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" }, + input({ type: "hidden", name: "stop", value: "1" }), + input({ type: "hidden", name: "returnTo", value: roomHref(room) }), + button({ type: "submit", class: "tribe-action-btn room-cam-btn-on" }, String(i18n.roomStreamStop).toUpperCase()) + ) + : live.streamAvailable + ? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form room-stream-form" }, + input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }), + input({ type: "file", id: "room-stream-file", name: "video", accept: "video/*", required: true, class: "room-stream-input" }), + label({ for: "room-stream-file", class: "tribe-action-btn room-stream-pick" }, String(i18n.roomStreamFile).toUpperCase()), + button({ type: "submit", class: "tribe-action-btn room-cam-btn-on room-stream-go", title: i18n.roomStreamGo }, "▶") + ) + : null, + form({ method: "POST", action: "/rooms/hand", class: "phone-action-form" }, + input({ type: "hidden", name: "hand", value: live.hand ? "0" : "1" }), + input({ type: "hidden", name: "returnTo", value: roomHref(room) }), + button({ type: "submit", class: live.hand ? "tribe-action-btn room-hand-btn-on" : "tribe-action-btn" }, "✋ " + String(live.hand ? i18n.roomLowerHand : i18n.roomRaiseHand).toUpperCase()) + ), + live.recording + ? form({ method: "POST", action: "/rooms/rec/stop", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.roomRecStop).toUpperCase())) + : form({ method: "POST", action: "/rooms/rec/start", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomRec).toUpperCase())), + live.recording || safeArr(live.recordingBy).length ? renderRecChip() : null, + form({ method: "POST", action: "/rooms/notify", class: "phone-action-form" }, + input({ type: "hidden", name: "on", value: live.notify ? "0" : "1" }), + button({ type: "submit", class: live.notify ? "tribe-action-btn" : "tribe-action-btn room-notify-off" }, "🔔 " + String(live.notify ? i18n.roomNotificationsOn : i18n.roomNotificationsOff).toUpperCase()) + ), + live.notify && safeArr(live.events).length + ? form({ method: "POST", action: "/rooms/notify/clear", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomClearNotices).toUpperCase())) + : null, + refreshLink(room), + form({ method: "POST", action: "/rooms/leave", class: "phone-action-form" }, + input({ type: "hidden", name: "returnTo", value: roomHref(room) }), + button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.roomLeave).toUpperCase()) + ) +) +const handOrder = (live) => { + const all = [{ you: true, hand: !!live.hand, since: Number(live.handSince) || 0 }, ...safeArr(live.peers).map(peer => ({ peer, hand: !!peer.hand, since: Number(peer.handSince) || 0 }))] + const raised = all.filter(e => e.hand).sort((a, b) => a.since - b.since) + raised.forEach((e, i) => { e.turn = i + 1 }) + return [...raised, ...all.filter(e => !e.hand)] +} +const renderRoomEvents = (live) => live.notify && safeArr(live.events).length + ? ul({ class: "room-events" }, ...safeArr(live.events).slice().reverse().map(ev => li({ class: "room-event" }, span({ class: "date-link" }, moment(ev.ts).format("HH:mm:ss")), span({ class: "room-event-text" }, ...eventLine(ev))))) + : null +const renderRecordings = (recordings) => safeArr(recordings).length + ? div({ class: "phone-block room-recordings-block" }, + h2(i18n.roomRecordings), + ul({ class: "room-recordings" }, ...recordings.map(r => li({ class: "room-recording" }, + span({ class: "date-link" }, moment(r.at).format("YYYY/MM/DD HH:mm")), + span({ class: "phone-call-clock" }, fmtDuration(r.durationMs)), + a({ href: `/rooms/recordings/${encodeURIComponent(r.name)}`, class: "tribe-action-btn", download: r.name }, String(i18n.fileDownload).toUpperCase()), + form({ method: "POST", action: `/rooms/recordings/${encodeURIComponent(r.name)}/delete`, class: "phone-action-form" }, + button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.delete }, renderIcon("close"))) + ))) + ) + : null +const renderLivePanel = (room, live, occ, canJoin = false) => { + const head = (left, controls = null) => div({ class: "room-live-head" }, + div({ class: "room-live-head-main" }, left), + controls || refreshLink(room) + ) + if (live) { + const silenceForm = (id, on) => form({ method: "POST", action: "/rooms/silence", class: "phone-action-form" }, + input({ type: "hidden", name: "id", value: id }), + input({ type: "hidden", name: "on", value: on ? "0" : "1" }), + button({ type: "submit", class: on ? "tribe-action-btn danger-btn" : "tribe-action-btn" }, String(on ? i18n.phoneUnsilence : i18n.phoneSilence).toUpperCase()) + ) + const isPublic = !!live.public || (room.clearnet === true && clearnetEligible(room)) + const who = (id, muted, you, silenced = false, recording = false, turn = 0, cam = false, film = false, viewer = false) => viewer + ? div({ class: "phone-contact room-peer" }, userLink(id), renderReachChip(true, i18n)) + : div({ class: "phone-contact room-peer" + (turn ? " room-peer-raised" : "") }, + turn ? span({ class: "room-peer-hand" }, `✋ ${turn}`) : null, + userLink(id), + you ? span({ class: "room-peer-you" }, i18n.roomYou) : null, + muted ? span({ class: "room-peer-muted" }, String(i18n.roomMuted).toUpperCase()) : null, + cam ? span({ class: "room-peer-cam" }, "CAM") : null, + film ? span({ class: "room-peer-cam" }, "▶") : null, + recording ? renderRecChip() : null, + you ? null : silenceForm(id, silenced) + ) + return div({ class: "phone-call-panel room-live-panel" }, + head(div({ class: "card-chips-row" }, renderEncryptedChip(i18n), isPublic ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, renderLiveChip({ count: live.count, max: live.max }), live.joinedAt ? span({ class: "phone-call-clock" }, liveClock(live.joinedAt)) : null, live.streaming != null ? span({ class: "room-stream-chip", title: live.streaming || "" }, "▶ " + String(live.streaming || i18n.roomStreaming)) : null), renderLiveControls(room, live)), + live.grid + ? (safeArr(live.gridOrder).length + ? div({ id: "room-video", class: safeArr(live.gridOrder).length === 1 ? "video-grid video-grid-one video-grid-solo" : "video-grid video-grid-one" }, + ...renderLiveFeed("/phone/video/grid", "video-feed video-feed-grid"), + div({ class: "video-grid-names" }, ...safeArr(live.gridOrder).map(key => { + const film = String(key).startsWith("film:") + const id = film ? String(key).slice(5) : key + const name = id === userId ? i18n.roomYou : userLink(id) + return film ? span({ class: "video-tile-name video-tile-film" }, "▶ ", name) : (id === userId ? span({ class: "video-tile-name" }, name) : name) + })) + ) + : null) + : div({ id: "room-video" }, renderVideoGrid(safeArr(live.peers).map(p => ({ id: p.id, on: !!p.cam, film: !!p.film })), !!live.cam, { film: live.streaming != null })), + div({ class: "phone-contacts" }, ...handOrder(live).map(e => e.you + ? who(userId, live.muted, true, false, !!live.recording, e.turn, !!live.cam, live.streaming != null) + : who(e.peer.id, e.peer.muted, false, !!e.peer.silenced, !!e.peer.recording, e.turn, !!e.peer.cam, !!e.peer.film, !!e.peer.viewer))), + renderRoomEvents(live) + ) + } + const count = occ ? occ.count : 0 + return div({ class: "phone-block room-live-panel" }, + head(p({ class: "phone-empty" }, count > 0 ? String(i18n.roomSomeone).replace("{n}", String(count)) : i18n.roomNobody), canJoin ? div({ class: "room-live-controls" }, refreshLink(room), joinForm(room, occ)) : null), + canJoin && room.clearnet === true && clearnetEligible(room) ? p({ class: "room-public-warning" }, i18n.roomPublicWarning) : null + ) +} + +exports.singleRoomView = async (room, params = {}) => { + const isAuthor = String(room.author) === String(userId) + const isMember = safeArr(room.members).includes(userId) || (!!room.tribeId && !!room.isTribeMember) + const restricted = !isMember && !isAuthor && room.type === "INVITE-ONLY" + const live = params.live && params.live.ref === room.rootId ? params.live : null + const occ = params.occ || null + const canJoin = !restricted && !room.isClosed && params.available !== false + const canClearnet = clearnetEligible(room) + const isClearnet = canClearnet && room.clearnet === true + + const inviteActions = isAuthor && room.type === "INVITE-ONLY" && !room.tribeId && !room.isClosed + ? [ + form({ method: "POST", action: `/rooms/generate-invite/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) + ), + (() => { + const openInvite = safeArr(room.invites).find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) + if (openInvite) return [ + div({ class: "tribe-open-invite" }, + span({ class: "card-label" }, i18n.tribeInviteCodeText), + span({ class: "tribe-open-invite-code" }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/rooms/${encodeURIComponent(openInvite.code)}` }) + ), + form({ method: "POST", action: `/rooms/open-invite/remove/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) + ) + ] + return form({ method: "POST", action: `/rooms/open-invite/create/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) + ) + })() + ].flat().filter(Boolean) + : [] + + const ownerActions = isAuthor + ? [ + room.isClosed + ? form({ method: "POST", action: `/rooms/reopen/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "update-btn" }, i18n.roomReopen) + ) + : form({ method: "POST", action: `/rooms/close/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "update-btn delete-btn" }, i18n.roomClose) + ), + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: room.rootId }), + button({ type: "submit", class: "update-btn" }, i18n.roomUpdate) + ) + ].filter(Boolean) + : [] + + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(room.rootId, roomHref(room), { spread: params.spreads || null, author: room.author, favKind: "rooms", isFavorite: room.isFavorite, returnTo: roomHref(room), reportTitle: room.title, deleteAction: isAuthor ? contentDeleteAction("room", room.rootId) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, restricted ? i18n.roomStatusInviteOnly : (room.title || "—"))), + div({ class: "card-chips-row" }, + ...roomChips(room, live ? { count: live.count, max: live.max } : occ, !!live), + canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? roomClearnetHref(room) : null) : null, + isAuthor && canClearnet ? renderClearnetSwitch("rooms", room.rootId, isClearnet) : null + ), + restricted ? null : renderCover(room, "tribe-detail-image"), + restricted || !room.description ? null : p({ class: "tribe-side-description" }, ...renderStyledText(room.description)), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, moment(room.createdAt).format("YYYY/MM/DD HH:mm")) + ), + restricted ? null : tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(room.author))) + ), + live || restricted + ? div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.roomParticipants}: ${safeArr(room.members).length}`) + ) + : null, + inviteActions.length ? div({ class: "tribe-side-actions" }, ...inviteActions) : null, + isAuthor && !room.isClosed + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + renderStateChip(room.line === "SWITCHBOARD" ? "mutuals" : "hidden", "✆", String(room.line === "SWITCHBOARD" ? i18n.roomLineSwitchboard : i18n.phoneDnd).toUpperCase()), + form({ method: "POST", action: `/rooms/line/${encodeURIComponent(room.rootId)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "line", value: room.line === "SWITCHBOARD" ? "DND" : "SWITCHBOARD" }, + String(room.line === "SWITCHBOARD" ? i18n.phoneDnd : i18n.roomLineSwitchboard).toUpperCase()) + ) + ) + : null, + restricted + ? div({ class: "tribe-side-actions" }, a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite)) + : null, + restricted || !safeArr(room.tags).length + ? null + : div({ class: "card-tags" }, ...room.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ) + + const main = div({ class: "tribe-main" }, + restricted + ? p({ class: "access-denied-msg" }, i18n.roomAccessDenied) + : [ + params.available === false ? div({ class: "pm-form-error-msg" }, p("✗ " + i18n.phoneUnavailable)) : null, + live + ? renderLivePanel(room, live, occ) + : [ + renderLivePanel(room, null, occ, canJoin), + details({ class: "pad-members-details", ...(safeArr(room.members).length <= 8 ? { open: true } : {}) }, + summary({ class: "tribe-members-count" }, `${i18n.roomParticipants}: ${safeArr(room.members).length}`), + div({ class: "pad-members-chips" }, ...safeArr(room.members).map(m => span({ class: "pad-member-chip" }, userLink(m)))) + ) + ], + renderRecordings(params.recordings) + ] + ) + + return template(room.title || i18n.roomsTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.roomsTitle), p(i18n.roomsDescription)), renderModeButtons("all", false, params.modesAvail || null)), + section(div({ class: "tribe-details" }, side, main)) + ) +} + +exports.renderTribeRoomsSection = (tribe, rooms, occupancy, live, toolbar = null) => { + const items = safeArr(rooms) + const createBtn = form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "tribeId", value: tribe.id }), + button({ type: "submit", class: "create-button" }, i18n.tribeRoomCreate)) + const head = toolbar || div({ class: "tribe-content-header" }, h2(i18n.tribeSectionRooms), createBtn) + if (!items.length) return div({ class: "tribe-content-list" }, head, div({ class: "no-content-box" }, p(i18n.tribeRoomsEmpty))) + return div({ class: "tribe-content-list" }, + head, + ul({ class: "mailing-archive" }, ...paged(items).map(r => renderRoomItem(r, { occupancy, live }))) + ) +} + +exports.renderRoomItem = renderRoomItem +exports.roomStatsBy = (rooms) => renderModuleStatsBy(safeArr(rooms), r => r.isClosed ? "CLOSED" : r.type, [{ value: "OPEN", label: i18n.roomStatusOpen }, { value: "INVITE-ONLY", label: i18n.roomStatusInviteOnly }, { value: "CLOSED", label: i18n.roomStatusClosed }]) + +exports.clearnetRoomView = async (room, { live = false, sound = true, base = "" } = {}) => { + const { escapeHtml: esc, renderKindTag, renderClearnetPage, cnZoom } = require("./clearnet_view") + const name = room.title || i18n.cnKindRoom + const extraCss = ` +.cn-room-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word} +.cn-room-meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center} +.cn-room-number{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--fg);border-radius:10px;padding:4px 12px;font-size:16px;font-weight:600;color:var(--fg);background:var(--bg-sub)} +.cn-room-number .pm-exposition-icon{font-size:18px;line-height:1} +.cn-room-number .pm-exposition-text{line-height:1;letter-spacing:1px;font-family:monospace} +.cn-room-live{display:flex;flex-direction:column;align-items:center;gap:12px;margin-top:24px} +.cn-room-live-chip{align-self:flex-start;border:1px solid #2ecc71;border-radius:10px;padding:3px 10px;color:#2ecc71;font-size:13px;font-weight:700;letter-spacing:1px} +.cn-room-live .cn-zoom{display:block;width:100%;max-width:960px} +.cn-room-grid{width:100%;aspect-ratio:4/3;object-fit:contain;background:#000;border-radius:8px} +.cn-room-sound{width:100%;max-width:960px} +.cn-room-live .cn-lightbox-img{width:92vw;height:88vh;object-fit:contain} +.cn-room-live .cn-lightbox-full .cn-lightbox-img{width:100vw;height:100vh} +` + const body = ` +

${esc(name)}

+
+ ${renderKindTag("room")} + ${room.number ? `✆${esc(room.number)}` : ""} +
+ ${live && base ? `
+ ● ${esc(String(i18n.roomLiveChip || "LIVE").toUpperCase())} + ${cnZoom("room-live", `${base}/live`, name, { full: true, imgClass: "cn-room-grid" })} + ${sound ? `` : ""} +
` : ""} +` + return renderClearnetPage({ + title: `${name} | Oasis`, + ogTitle: name, + ogDescription: "", + extraCss, + body, + hubFeedId: room.author || null + }) +} + +exports.roomModesFromCensus = (census, me, occupancy) => { + const list = safeArr(census) + return { + mine: list.some(x => String(x.author) === String(me)), + recent: list.length > 0, + live: list.some(x => { const o = occupancy && occupancy.get(x.rootId); return !!(o && o.count > 0) }), + open: list.some(x => x.type === "OPEN" && !x.isClosed), + invite: list.some(x => x.type === "INVITE-ONLY" && !x.isClosed), + closed: list.some(x => x.isClosed) + } +} + +const pickerSize = (bytes) => { + const n = Number(bytes) || 0 + if (n >= 1073741824) return `${(n / 1073741824).toFixed(1)} GB` + if (n >= 1048576) return `${(n / 1048576).toFixed(1)} MB` + return `${Math.max(1, Math.round(n / 1024))} KB` +} + +exports.roomStreamMatchesView = ({ files, returnTo }) => template( + i18n.roomStreamPickTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.roomStreamPickTitle), p(i18n.roomStreamPickHint)), + div({ class: "mode-buttons-row" }, a({ href: returnTo, class: "filter-btn" }, String(i18n.phoneCancel).toUpperCase())), + ul({ class: "stream-picker-list" }, ...safeArr(files).map(f => li({ class: "stream-picker-item" }, + span({ class: "stream-picker-link" }, renderIcon("film"), span({ class: "stream-picker-name" }, f.path), span({ class: "stream-picker-size" }, pickerSize(f.size))), + form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" }, + input({ type: "hidden", name: "file", value: f.path }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomStreamFile).toUpperCase()) + ) + ))) + ) +) diff --git a/src/views/school_view.js b/src/views/school_view.js new file mode 100644 index 0000000..eb6c031 --- /dev/null +++ b/src/views/school_view.js @@ -0,0 +1,945 @@ +const { div, h2, h3, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td, th, details, summary, datalist, progress, hr } = require("../server/node_modules/hyperaxe") +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const moment = require("../server/node_modules/moment"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderOpinionsVoting, renderEngagement, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") +const opinionCategories = require("../backend/opinion_categories") +const { config } = require("../server/SSB_server.js") +const { renderStyledText, renderStyledHtml } = require("../backend/renderStyledText") +const nameCache = require("../backend/nameCache") +const { sanitizeHtml } = require("../backend/sanitizeHtml") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const renderMd = (text) => div({ class: "styled-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(text || ""))) }) + +const userId = config.keys.id +const safeArr = (v) => (Array.isArray(v) ? v : []) +const safeText = (v) => String(v || "").trim() +const isFree = (course) => !(Number(course.price) > 0) +const isProtected = (course) => !isFree(course) || course.visibility === "INVITE" +const clearnetEligible = (course) => course.visibility === "PUBLIC" && isFree(course) && !course.encrypted +const COURSE_TYPES = ["OPEN", "PAID", "INVITE"] +const courseTypeOf = (course) => COURSE_TYPES.includes(String(course.courseType || "").toUpperCase()) ? String(course.courseType).toUpperCase() : (course.visibility === "INVITE" ? "INVITE" : (Number(course.price) > 0 ? "PAID" : "OPEN")) +const courseTypeLabel = (v) => v === "INVITE" ? i18n.schoolTypeInvite : (v === "PAID" ? i18n.schoolTypePaid : i18n.schoolTypeOpen) +const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((sum, cat) => sum + (Number((opinions || {})[cat]) || 0), 0) +const renderStarRating = (opinions, voterCount) => { + const pos = sumCats(opinions, opinionCategories.positive) + const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation) + const totalVotes = pos + neg + const full = totalVotes > 0 ? Math.round((pos / totalVotes) * 5) : 0 + const stars = "\u2605".repeat(full) + "\u2606".repeat(5 - full) + return span({ class: "shop-product-stars" }, `${stars} (${voterCount})`) +} + +const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { + if (!value) return fallbackSrc ? require("../server/node_modules/hyperaxe").img({ src: fallbackSrc, ...attrs }) : null + const { img } = require("../server/node_modules/hyperaxe") + const s = String(value).trim() + if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null +} + +const buildReturnTo = (filter, params = {}) => { + const f = safeText(filter || "all") + const q = safeText(params.q || "") + const sort = safeText(params.sort || "recent") + const parts = [`filter=${encodeURIComponent(f)}`] + if (q) parts.push(`q=${encodeURIComponent(q)}`) + if (sort) parts.push(`sort=${encodeURIComponent(sort)}`) + return `/school?${parts.join("&")}` +} + +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => + div({ class: "tribe-mode-buttons" }, + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "top", "applied", "open", "favorites"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => + form({ method: "GET", action: "/school" }, + input({ type: "hidden", name: "filter", value: f }), + button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`schoolFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) + ) + ), + ]), + form({ method: "GET", action: "/school" }, + input({ type: "hidden", name: "filter", value: "create" }), + button({ type: "submit", class: "create-button" }, i18n.schoolCreateButton) + ) + ) + +const renderPriceChip = (course) => + isFree(course) + ? renderStateChip("mutuals", "✓", i18n.schoolFree) + : span({ class: "price-chip" }, `${Number(course.price).toFixed(2)} ECO`) + +const renderCourseChips = (course, subscription = null) => + div({ class: "card-chips-row" }, + course.visibility === "INVITE" ? renderStateChip("whole", "\uD83D\uDD11", i18n.chatStatusInviteOnly) : null, + course.status === "CLOSED" + ? renderStateChip("closed", "✗", i18n.schoolClosed) + : renderStateChip("mutuals", "✓", i18n.schoolOngoing), + renderPriceChip(course), + course.clearnet === true && clearnetEligible(course) ? renderReachChip(true, i18n, clearnetItemHref('school', course.title, course.id)) : null, + subscription + ? (subscription.in + ? renderStateChip("mutuals", "\u2709", i18n.subscriptionOn) + : renderStateChip("closed", "\u2709", i18n.subscriptionOff)) + : null + ) + +const renderCourseCard = (course, filter, params = {}) => { + const url = `/school/course/${encodeURIComponent(course.id)}` + const isOwn = String(course.author) === String(userId) + return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(course.id, url, { + spread: (params.spreadMap && params.spreadMap.get(course.id)) || null, + author: course.author, + favKind: 'school', + isFavorite: course.isFavorite, + reportTitle: course.title, + returnTo: buildReturnTo(filter, params), + deleteAction: isOwn ? contentDeleteAction('schoolCourse', course.id) : undefined + }) + ), + div({ class: "tribe-card-image-wrapper" }, + a({ href: url }, + renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-card-hero-image' }) + ) + ), + div({ class: "tribe-card-body" }, + h2({ class: "tribe-card-title" }, a({ href: url }, course.title)), + renderStarRating(course.opinions, safeArr(course.opinions_inhabitants).length), + renderCourseChips(course, params.subscriptions && (String(course.author) === String(userId) || safeArr(course.students).includes(userId)) ? { in: params.subscriptions.mine.has(course.rootId || course.id) || String(course.author) === String(userId) } : null), + course.description ? p({ class: "tribe-card-description" }, ...renderStyledText(course.description)) : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.schoolStudents}: ${course.students.length}`) + ), + course.startDate ? div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.schoolStartDate}: ${moment(course.startDate).format("YYYY/MM/DD HH:mm")}`) + ) : null + ) + ) +} + +const renderCourseForm = (filter, course = {}, params = {}) => { + const isEdit = filter === "edit" + const reachRaw = String(params.reach || "").toUpperCase() + const reach = COURSE_TYPES.includes(reachRaw) ? reachRaw : courseTypeOf(course) + return div({ class: "create-tribe-form" }, + h2(isEdit ? i18n.schoolUpdateSectionTitle : i18n.schoolCreateSectionTitle), + form({ method: "GET", action: "/school" }, + input({ type: "hidden", name: "filter", value: isEdit ? "edit" : "create" }), + isEdit ? input({ type: "hidden", name: "courseId", value: course.id || "" }) : null, + label(i18n.schoolCourseType), br(), + div({ class: "apply-row" }, + select({ name: "courseType", class: "report-category-select" }, + option({ value: "OPEN", ...(reach === "OPEN" ? { selected: true } : {}) }, i18n.schoolTypeOpen), + option({ value: "PAID", ...(reach === "PAID" ? { selected: true } : {}) }, i18n.schoolTypePaid), + option({ value: "INVITE", ...(reach === "INVITE" ? { selected: true } : {}) }, i18n.schoolTypeInvite) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, courseTypeLabel(reach)), + form({ action: isEdit ? `/school/update/${encodeURIComponent(course.id || "")}` : "/school/create", method: "POST", enctype: "multipart/form-data" }, + input({ type: "hidden", name: "courseType", value: reach }), + label(i18n.title), br, + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.schoolCourseTitlePlaceholder, value: course.title || "" }), br(), + label(i18n.description), br, + textarea({ name: "description", rows: 5, required: true, maxlength: "2000", placeholder: i18n.schoolCourseDescriptionPlaceholder }, course.description || ""), br, + label(i18n.blogImage), br, + input({ type: "file", name: "image", accept: "image/*" }), br(), br(), + label(i18n.schoolTags), br, + input({ type: "text", name: "tags", placeholder: i18n.schoolTagsPlaceholder, value: Array.isArray(course.tags) ? course.tags.join(", ") : (course.tags || "") }), br, + ...(reach === "PAID" + ? [ + label(i18n.schoolPrice), br, + input({ type: "number", name: "price", step: "0.000001", min: "0.000001", required: true, value: course.price && Number(course.price) > 0 ? course.price : "" }), renderEcoValueChip(), br(), br() + ] + : reach === "INVITE" + ? [ + label(i18n.schoolPrice), br, + input({ type: "number", name: "price", step: "0.000001", min: "0", value: course.price && Number(course.price) > 0 ? course.price : "0" }), renderEcoValueChip(), br(), br() + ] + : [input({ type: "hidden", name: "price", value: "0" })]), + label(i18n.schoolStartDate), br, + input({ type: "datetime-local", name: "startDate", min: moment().format("YYYY-MM-DDTHH:mm"), value: course.startDate ? moment(course.startDate).format("YYYY-MM-DDTHH:mm") : "" }), br(), + isEdit + ? div( + label(i18n.schoolStatus), br, + select({ name: "status" }, + option({ value: "ONGOING", ...((course.status || "ONGOING") === "ONGOING" ? { selected: true } : {}) }, i18n.schoolOngoing), + option({ value: "CLOSED", ...(course.status === "CLOSED" ? { selected: true } : {}) }, i18n.schoolClosed) + ), br() + ) + : null, + reach === "OPEN" && clearnetEligible({ ...course, visibility: "PUBLIC", price: "0" }) ? renderClearnetSelector(course.clearnet === true || String(course.clearnet || "") === "1", i18n) : null, + br(), + button({ type: "submit" }, isEdit ? i18n.schoolUpdate : i18n.schoolCreate) + ) + ) +} + +exports.schoolView = async (courses, filter, courseToEdit = null, params = {}) => { + const q = safeText(params.q || "") + const sort = safeText(params.sort || "recent") + const list = safeArr(courses) + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q) + const title = i18n.schoolTitle + const isForm = filter === "create" || filter === "edit" + + return template( + title, + section(div({ class: "tags-header module-header-line" }, h2(title), p(i18n.schoolDescription), renderWalletChip())), + section(renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null)), + !isForm && !emptyMod + ? section( + div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(list, c => String(c.status || 'ONGOING').toUpperCase(), [{ value: 'ONGOING', label: i18n.schoolOngoing }, { value: 'CLOSED', label: i18n.schoolClosed }]), + form({ method: "GET", action: "/school", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.schoolSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + select({ name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {}) }, i18n.documentSortRecent), + option({ value: "students", ...(sort === "students" ? { selected: true } : {}) }, i18n.documentSortTop), + option({ value: "title", ...(sort === "title" ? { selected: true } : {}) }, i18n.schoolSortTitle) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + ) + : null, + section( + isForm + ? renderCourseForm(filter, filter === "edit" ? (courseToEdit || {}) : (params.draft || {}), params) + : div({ class: "tribe-grid" }, + list.length + ? paged(list).map(course => renderCourseCard(course, filter, params)) + : p(i18n.schoolNoCourses) + ) + ) + ) +} + +const renderLesson = (lesson, course, isTeacher, returnTo, isStudent = false, isNext = false) => + lesson.locked + ? div({ class: "school-lesson school-lesson-locked" }, + h3("🔒"), + p(i18n.schoolLessonLocked), + div({ class: "school-lesson-meta" }, span(moment(lesson.createdAt).format("YYYY/MM/DD"))) + ) + : div({ class: isNext ? "school-lesson school-lesson-next" : "school-lesson" }, + details({ class: "school-lesson-details", ...(isNext ? { open: true } : {}) }, + summary({ class: "school-lesson-summary" }, + span({ class: "school-lesson-summary-title" }, (lesson.completed ? "✓ " : isNext ? "▶ " : "") + lesson.title), + span({ class: "school-lesson-summary-meta" }, + lesson.sessionDate ? `${moment(lesson.sessionDate).format("YYYY/MM/DD HH:mm")} · ` : "", + moment(lesson.createdAt).format("YYYY/MM/DD") + ) + ), + div({ class: "school-lesson-body" }, + renderMd(lesson.text), + div({ class: "school-lesson-actions" }, + a({ href: `/school/lesson/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}`, class: "filter-btn" }, i18n.schoolLessonOpen || "Open lesson"), + isTeacher + ? a({ href: `/school/lesson/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}?edit=1`, class: "filter-btn" }, i18n.chatUpdate) + : null, + isStudent + ? form({ method: "POST", action: `/school/lesson/complete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "value", value: lesson.completed ? "false" : "true" }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: lesson.completed ? "tribe-action-btn school-complete-btn school-complete-btn--done" : "tribe-action-btn school-complete-btn" }, lesson.completed ? `✓ ${i18n.schoolProgressDone}` : i18n.schoolProgressMark) + ) + : null + ) + ) + ) + ) + +const renderTeacherPanel = (course, certificates, returnTo, lessons = [], exams = [], progress = {}, certStudent = "") => + div({ class: "create-tribe-form school-teacher-panel" }, + h2(i18n.schoolTeacherPanel), + div({ class: "school-panel-box" }, + h3(i18n.schoolAddLesson), + form({ method: "POST", action: `/school/lesson/add/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.schoolLessonTitlePlaceholder }), br(), + textarea({ name: "text", rows: 5, required: true, maxlength: "4000", placeholder: i18n.schoolLessonTextPlaceholder }), br, + label(i18n.schoolLessonUnit), br, + input({ type: "text", name: "unit", maxlength: "60", placeholder: i18n.schoolLessonUnitPlaceholder }), br(), + label(i18n.schoolLessonOrder), br, + input({ type: "number", name: "order", min: "0", step: "1" }), br(), br(), + label(i18n.schoolSessionDate), br, + input({ type: "datetime-local", name: "sessionDate", min: moment().format("YYYY-MM-DDTHH:mm") }), br(), br(), + button({ type: "submit" }, i18n.schoolAddLesson) + ) + ), + isProtected(course) && safeArr(lessons).length + ? div({ class: "school-panel-box" }, + h3(i18n.schoolExamCreate), + form({ method: "POST", action: `/school/exam/create/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.schoolExamTitlePlaceholder }), br(), + label(i18n.schoolLessons), br, + select({ name: "lessonId" }, + safeArr(lessons).map(lesson => option({ value: lesson.id }, lesson.title || lesson.id)) + ), br(), br(), + button({ type: "submit" }, i18n.schoolExamCreate) + ) + ) + : null, + (() => { + const openExams = safeArr(exams).filter(x => !x.locked) + return openExams.length + ? div({ class: "school-panel-box" }, + h3(i18n.schoolExamCreateQuestion), + form({ method: "POST", action: `/school/exam/question/add/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + select({ name: "examId" }, + openExams.map(x => option({ value: x.id }, x.title || x.id)) + ), br(), br(), + label(i18n.schoolExamAddQuestion), br, + input({ type: "text", name: "q", required: true, maxlength: "300" }), br(), + input({ type: "text", name: "o1", required: true, maxlength: "200", placeholder: `${i18n.schoolExamOption} 1` }), br(), + input({ type: "text", name: "o2", required: true, maxlength: "200", placeholder: `${i18n.schoolExamOption} 2` }), br(), + input({ type: "text", name: "o3", required: true, maxlength: "200", placeholder: `${i18n.schoolExamOption} 3` }), br(), + input({ type: "text", name: "o4", required: true, maxlength: "200", placeholder: `${i18n.schoolExamOption} 4` }), br(), + label(i18n.schoolExamCorrect), br, + select({ name: "correct" }, + option({ value: "0" }, `${i18n.schoolExamOption} 1`), + option({ value: "1" }, `${i18n.schoolExamOption} 2`), + option({ value: "2" }, `${i18n.schoolExamOption} 3`), + option({ value: "3" }, `${i18n.schoolExamOption} 4`) + ), br(), br(), + button({ type: "submit" }, i18n.schoolExamAddQuestion) + ) + ) + : null + })(), + course.students.length + ? div({ class: "school-panel-box", id: "school-cert" }, + h3(i18n.schoolIssueCertificate), + form({ method: "POST", action: `/school/certificate/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "text", name: "student", required: true, list: "school-cert-students", placeholder: "@…", maxlength: "120", value: (() => { + const id = safeText(certStudent) + if (!id) return "" + const nick = String(nameCache.get(id) || "").trim() + return nick ? `${id} (${nick})` : id + })() }), + datalist({ id: "school-cert-students" }, + (() => { + const readyExams = safeArr(exams).filter(x => !x.locked && safeArr(x.questions).length) + return course.students + .filter(s => !safeArr(certificates).some(cert => cert.student === s)) + .filter(s => (Number((progress || {})[s]) || 0) >= safeArr(lessons).length) + .filter(s => readyExams.filter(x => safeArr(x.results).some(r => r.author === s && r.passed)).length >= readyExams.length) + .map(s => { + const nick = String(nameCache.get(s) || "").trim() + return option({ value: nick ? `${s} (${nick})` : s }) + }) + })() + ), br(), + textarea({ name: "text", rows: 3, maxlength: "500", placeholder: i18n.schoolCertificateTextPlaceholder }), br, + button({ type: "submit" }, i18n.schoolIssueCertificate) + ) + ) + : null, + safeArr(course.pending).length + ? div({ class: "school-pending-list" }, + p(`${i18n.schoolPendingStudents}: `), + course.pending.map(pen => + p( + userLink(pen.author), + pen.transferId ? span(" — ") : null, + pen.transferId ? a({ href: `/transfers/${encodeURIComponent(pen.transferId)}` }, i18n.schoolViewBill) : null + ) + ), + br() + ) + : null, + !isFree(course) || course.visibility === "INVITE" + ? (() => { + const granted = safeArr(course.granted) + const ungrantedStudents = safeArr(course.students).filter(st => !granted.includes(st)) + return ungrantedStudents.length + ? div({ class: "school-grant-list" }, + p(`${i18n.schoolGrantAccess}: `), + ungrantedStudents.map(st => + form({ method: "POST", action: `/school/grant/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "student", value: st }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "filter-btn" }, `🔑 ${st.slice(0, 10)}...`) + ) + ), + br() + ) + : null + })() + : null, + null + ) + +exports.singleCourseView = async (course, lessons = [], certificates = [], params = {}) => { + const courseApproved = params.approved === true + const returnTo = `/school/course/${encodeURIComponent(course.id)}` + const isTeacher = String(course.author) === String(userId) + const isStudent = course.students.includes(userId) + const myPending = safeArr(course.pending).find(p => p.author === userId) || null + const isPaid = !isFree(course) + const canClearnet = clearnetEligible(course) + const isClearnet = canClearnet && !!course.clearnet + lessons = safeArr(lessons) + certificates = safeArr(certificates) + const myCompleted = lessons.filter(lesson => lesson.completed).length + const nextLesson = isStudent ? lessons.find(l => !l.locked && !l.completed) : null + + const courseSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(course.id, returnTo, { spread: params.spreads || null, author: course.author, favKind: 'school', isFavorite: course.isFavorite, returnTo, reportTitle: course.title, deleteAction: isTeacher ? contentDeleteAction('schoolCourse', course.id) : undefined }) + ), + h2({ class: "tribe-card-title" }, course.title), + renderStarRating(course.opinions, safeArr(course.opinions_inhabitants).length), + div({ class: "card-chips-row" }, + course.visibility === "INVITE" ? renderStateChip("whole", "\uD83D\uDD11", i18n.chatStatusInviteOnly) : null, + course.status === "CLOSED" + ? renderStateChip("closed", "✗", i18n.schoolClosed) + : renderStateChip("mutuals", "✓", i18n.schoolOngoing), + renderPriceChip(course), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('school', course.title, course.id) : null), + isTeacher && canClearnet ? renderClearnetSwitch('school', course.rootId || course.id, isClearnet) : null, + courseApproved ? renderStateChip("mutuals", "🎓", i18n.schoolApproved) : null, + params.subscription && (isTeacher || isStudent) + ? ((isTeacher || params.subscription.subscribed === true) + ? renderStateChip("mutuals", "\u2709", i18n.subscriptionOn) + : renderStateChip("closed", "\u2709", i18n.subscriptionOff)) + : null + ), + renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), + course.description ? p({ class: "tribe-side-description" }, ...renderStyledText(course.description)) : null, + table({ class: "tribe-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.schoolCreatedAt), + td({ class: "tribe-info-value", colspan: "3" }, moment(course.createdAt).format("YYYY/MM/DD HH:mm")) + ), + course.startDate ? tr( + td({ class: "tribe-info-label" }, i18n.schoolStartDate), + td({ class: "tribe-info-value", colspan: "3" }, moment(course.startDate).format("YYYY/MM/DD HH:mm")) + ) : null, + tr( + td({ class: "tribe-info-label" }, i18n.schoolTeacher), + td({ class: "tribe-info-value", colspan: "3" }, userLink(course.author)) + ) + ), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.schoolStudents}: ${course.students.length}`) + ), + course.chatId && (isTeacher || isStudent || (course.visibility === "PUBLIC" && isFree(course))) + ? div({ class: "tribe-side-actions shop-visibility-row stacked-action-row" }, + span({ class: "card-label" }, `${i18n.schoolCourseChat}:`), + a({ href: `/chats/${encodeURIComponent(course.chatId)}#chat-latest`, class: "tribe-action-btn" }, i18n.chatVisitChat) + ) + : null, + isStudent && lessons.length + ? div({ class: "tribe-side-actions shop-visibility-row school-progress-side" }, + span({ class: "card-label" }, `${i18n.schoolProgress}:`), + progress({ class: "school-progress-bar", value: String(myCompleted), max: String(lessons.length) }), + span({ class: "school-progress-count" }, `${myCompleted}/${lessons.length}`) + ) + : null, + !isTeacher + ? (myPending + ? div({ class: "school-pending-block" }, + div({ class: "tribe-side-actions school-pending-row" }, + renderStateChip("closed", "⧖", i18n.schoolPendingPayment) + ), + myPending.transferId + ? div({ class: "doc-export-actions" }, + form({ method: "GET", action: `/transfers/contract/${encodeURIComponent(myPending.transferId)}`, class: "transfer-contract-form" }, + button({ type: "submit", class: "filter-btn" }, i18n.transfersExportContract) + ), + form({ method: "POST", action: `/transfers/${encodeURIComponent(myPending.transferId)}/share` }, + button({ type: "submit", class: "filter-btn" }, i18n.sharePm) + ) + ) + : null + ) + : !isStudent && course.status === "ONGOING" && (course.visibility === "PUBLIC" || course.invited.includes(userId)) + ? div({ class: "tribe-side-actions shop-visibility-row stacked-action-row" }, + span({ class: "card-label" }, `${i18n.schoolEnrollLabel}:`), + form({ method: "POST", action: `/school/enroll/${encodeURIComponent(course.id)}` }, + button({ type: "submit", class: "tribe-action-btn" }, isPaid ? `${i18n.schoolEnrollButton} (${Number(course.price).toFixed(2)} ECO)` : i18n.schoolEnrollButton) + ) + ) + : !isStudent + ? p(i18n.schoolInviteRequired) + : null) + : null, + isTeacher && course.visibility === "INVITE" && !course.inviteCode + ? div({ class: "tribe-side-actions shop-visibility-row" }, + span({ class: "card-label" }, `${i18n.schoolVisibilityInvite}: `), + form({ method: "POST", action: `/school/generate-invite/${encodeURIComponent(course.id)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) + ) + ) + : null, + isTeacher && course.visibility === "INVITE" && course.inviteCode + ? div({ class: "tribe-side-actions school-invite-code-block" }, + div({ class: "tribe-open-invite" }, + span({ class: "card-label" }, i18n.tribeInviteCodeText), + span({ class: "tribe-open-invite-code" }, course.inviteCode), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/school/${encodeURIComponent(course.inviteCode)}` }) + ) + ) + : null, + isTeacher && course.visibility === "INVITE" + ? div({ class: "tribe-side-actions stacked-action-row school-invite-students" }, + form({ method: "POST", action: `/school/invite/${encodeURIComponent(course.id)}` }, + label({ class: "card-label" }, `${i18n.schoolInviteStudents}:`), br, + input({ type: "text", name: "students", required: true, placeholder: i18n.schoolInvitePlaceholder }), br(), + button({ type: "submit", class: "tribe-action-btn" }, i18n.schoolInviteButton) + ), + course.invited.length + ? div({ class: "school-invited-list" }, + p(`${i18n.schoolInvited}: `), + course.invited.map(s => p(userLink(s))) + ) + : null + ) + : null, + params.subscription && (isTeacher || isStudent) + ? renderSubscriptionBox({ + target: course.rootId || course.id, + scope: "school", + subscribed: params.subscription.subscribed === true, + count: params.subscription.count, + isOwner: isTeacher, + returnTo + }) + : null, + safeArr(course.tags).length + ? div({ class: "tribe-side-tags" }, safeArr(course.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : null, + isTeacher + ? div({ class: "tribe-side-actions shop-visibility-row" }, + span({ class: "card-label" }, `${i18n.schoolStatus}: `), + course.status === "CLOSED" + ? renderStateChip("closed", "✗", i18n.schoolClosed) + : renderStateChip("mutuals", "✓", i18n.schoolOngoing), + form({ method: "POST", action: `/school/status/${encodeURIComponent(course.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "status", value: course.status === "CLOSED" ? "ONGOING" : "CLOSED" }), + button({ type: "submit", class: "tribe-action-btn" }, course.status === "CLOSED" ? i18n.schoolReopenCourse : i18n.schoolCloseCourse) + ) + ) + : null, + isTeacher + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: "/school" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "courseId", value: course.id }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) + ) + ) + : null, + !isTeacher && (isStudent || myPending) + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "POST", action: `/school/unenroll/${encodeURIComponent(course.id)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.schoolUnenrollButton) + ) + ) + : null + ) + + const exams = safeArr(params.exams) + const readyExams = exams.filter(x => !x.locked && safeArr(x.questions).length) + + const teacherProgress = isTeacher && safeArr(course.students).length + ? div({ class: "school-progress-list" }, + h2(`${i18n.schoolStudents} (${course.students.length})`), + table({ class: "school-students-table" }, + tr( + th(i18n.schoolStudents), + th(i18n.schoolLessons), + th(i18n.schoolExams), + th(i18n.schoolCertified) + ), + course.students.map(student => { + const done = Number((params.progress || {})[student]) || 0 + const passedExams = readyExams.filter(x => safeArr(x.results).some(r => r.author === student && r.passed)).length + const failedExams = readyExams.filter(x => safeArr(x.results).some(r => r.author === student && !r.passed) && !safeArr(x.results).some(r => r.author === student && r.passed)) + const cert = safeArr(certificates).find(c => c.student === student) || null + const eligible = done >= lessons.length && passedExams >= readyExams.length + return tr({ class: eligible ? "school-row-passed" : "" }, + td(userLink(student)), + td(`${done}/${lessons.length}`), + td( + readyExams.length ? `${passedExams}/${readyExams.length}` : "—", + failedExams.length + ? span({ class: "school-progress-failed", title: failedExams.map(x => x.title).join(", ") }, ` ✗${failedExams.length}`) + : null + ), + td(cert + ? a({ href: `/school/certificate/pdf/${encodeURIComponent(course.id)}/${encodeURIComponent(cert.id)}`, class: "filter-btn school-cert-pdf" }, `⬇ ${i18n.schoolCertificatePdf || "PDF"}`) + : eligible + ? a({ href: `${returnTo}?cert=${encodeURIComponent(student)}#school-cert`, class: "filter-btn school-cert-pdf" }, i18n.schoolIssueCertificate) + : span({ class: "school-progress-failed" }, "✗")) + ) + }) + ) + ) + : null + + const studentPanel = isStudent && !isTeacher + ? (() => { + const myPassed = readyExams.filter(x => x.myResult && x.myResult.passed).length + const myFailed = readyExams.filter(x => x.myResult && !x.myResult.passed) + return div({ class: "school-progress-list" }, + h2(i18n.schoolStudentPanel), + table({ class: "school-students-table" }, + tr( + th(i18n.schoolLessons), + th(i18n.schoolExams), + th(i18n.schoolCertified) + ), + tr( + td(`${myCompleted}/${lessons.length}`), + td( + readyExams.length ? `${myPassed}/${readyExams.length}` : "—", + myFailed.length + ? span({ class: "school-progress-failed", title: myFailed.map(x => x.title).join(", ") }, ` ✗${myFailed.length}`) + : null + ), + td((() => { + const cert = safeArr(certificates).find(c => c.student === userId) || null + return cert + ? a({ href: `/school/certificate/pdf/${encodeURIComponent(course.id)}/${encodeURIComponent(cert.id)}`, class: "filter-btn school-cert-pdf" }, `⬇ ${i18n.schoolCertificatePdf || "PDF"}`) + : span({ class: "school-progress-failed" }, "✗") + })()) + ) + ) + ) + })() + : null + + const examsBlock = (isTeacher || isStudent) && exams.length + ? div({ class: "school-exams" }, + h2(`${i18n.schoolExams} (${isTeacher ? exams.length : readyExams.length})`), + (isTeacher ? exams : readyExams).map(exam => + exam.locked + ? null + : div({ class: "school-lesson school-exam" }, + isTeacher + ? details({ class: "school-lesson-details" }, + summary({ class: "school-lesson-summary" }, + span({ class: "school-lesson-summary-title" }, `${exam.title} (${safeArr(exam.questions).length})`) + ), + div({ class: "school-lesson-body" }, + exam.questions.map((question, qi) => + div({ class: "school-exam-question" }, + p(`${qi + 1}. ${question.q}`), + question.options.map((opt, oi) => + p({ class: oi === question.correct ? "school-exam-correct" : "school-exam-option-row" }, `${oi === question.correct ? "✓" : "·"} ${opt}`) + ), + form({ method: "POST", action: `/school/exam/question/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(exam.id)}/${encodeURIComponent(question.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete) + ) + ) + ), + form({ method: "POST", action: `/school/exam/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(exam.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete) + ) + ) + ) + : (() => { + const res = exam.myResult + const blocked = res && (res.passed || Date.now() - Number(res.at || 0) < 24 * 60 * 60 * 1000) + const chip = res && res.passed + ? renderStateChip("mutuals", "✓", i18n.schoolExamPassed) + : blocked + ? renderStateChip("closed", "✗", i18n.schoolExamFailed) + : span({ class: "state-chip" }, i18n.schoolExamPending) + return details({ class: "school-lesson-details" }, + summary({ class: "school-lesson-summary" }, + span({ class: "school-lesson-summary-title" }, `${exam.title} (${safeArr(exam.questions).length})`), + chip + ), + div({ class: "school-lesson-body" }, + blocked + ? div( + p(`${i18n.schoolExamScore}: ${res.score}/10 — ${res.passed ? i18n.schoolExamPassed : i18n.schoolExamFailed}`), + !res.passed ? p({ class: "school-exam-retry" }, i18n.schoolExamRetryLater) : null + ) + : form({ method: "POST", action: `/school/exam/take/${encodeURIComponent(course.id)}/${encodeURIComponent(exam.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + exam.questions.map((question, qi) => + div({ class: "school-exam-question" }, + p(question.q), + question.options.map((opt, oi) => + label({ class: "school-exam-option" }, + input({ type: "radio", name: `q${qi}`, value: String(oi), required: true }), + ` ${opt}` + ) + ) + ) + ), + button({ type: "submit" }, i18n.schoolExamSubmit) + ) + ) + ) + })() + ) + ), + null + ) + : null + + const lessonItem = (lesson) => renderLesson(lesson, course, isTeacher, returnTo, isStudent, nextLesson && lesson.id === nextLesson.id) + const unitGroups = (() => { + const groups = new Map() + for (const lesson of lessons) { + const unit = safeText(lesson.unit) + if (!groups.has(unit)) groups.set(unit, []) + groups.get(unit).push(lesson) + } + return [...groups.entries()] + })() + + const lessonsBlock = div({ class: "school-lessons" }, + h2(`${i18n.schoolLessons} (${lessons.length})`), + isTeacher || isStudent || isFree(course) + ? (lessons.length + ? unitGroups.map(([unit, items]) => + unit + ? details({ class: "school-unit", ...(isTeacher ? {} : { open: true }) }, + summary({ class: "school-unit-summary" }, `${unit} (${items.length})`), + items.map(lessonItem) + ) + : items.map(lessonItem) + ) + : p(i18n.schoolNoLessons)) + : p(i18n.schoolEnrollToSee) + ) + + return template( + course.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.schoolTitle), p(i18n.schoolDescription), renderWalletChip())), + section(renderModeButtons("all", false, (params && params.modesAvail) || null)), + section( + div({ class: "tribe-details" }, + courseSide, + div({ class: "tribe-main" }, + lessonsBlock, + examsBlock, + teacherProgress, + studentPanel, + isTeacher ? renderTeacherPanel(course, certificates, returnTo, lessons, exams, params.progress, params.certStudent) : null, + renderEngagement(course.id, + isStudent && !isTeacher + ? renderOpinionsVoting('/school/opinions', course.id, course.opinions, returnTo, course.opinions_inhabitants) + : null, + null + ) + ) + ) + ) + ) +} + +exports.clearnetCourseView = async (course, lessons = []) => { + const { escapeHtml: esc, blobUrl: cnBlob, renderRichText, renderKindTag, renderClearnetPage } = require("./clearnet_view") + const title = esc(course.title || i18n.cnKindCourse) + const desc = renderRichText(course.description || "") + const courseImg = cnBlob(course.image) + const visibleLessons = safeArr(lessons).filter(lesson => !lesson.locked) + const extraCss = ` +.cn-course-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} +.cn-course-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} +.cn-course-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} +.cn-course-img{display:block;max-width:100%;border:1px solid var(--border);border-radius:8px;margin-bottom:20px} +.cn-course-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:24px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)} +.cn-course-section p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:15px} +.cn-lesson{background:var(--bg-sub);border:1px solid var(--border);border-radius:8px;padding:14px 18px;margin-bottom:12px} +.cn-lesson h3{color:var(--fg);margin:0 0 8px 0;font-size:16px} +.cn-lesson p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:14px;margin:0} +.cn-lesson-date{color:var(--fg-dim);font-size:12px;margin-top:8px} +` + const body = ` +

${title}

+
+ ${renderKindTag('course')} + 🎓 ${esc(i18n.cnOpenCourse)} + ${course.startDate ? `📅 ${esc(new Date(course.startDate).toISOString().slice(0, 10))}` : ""} + 👥 ${safeArr(course.students).length} ${esc(i18n.schoolStudents)} +
+
+ ${courseImg ? `${title}` : ""} + ${desc ? `

${esc(i18n.description)}

${desc}

` : ""} + ${visibleLessons.length ? `

${esc(i18n.schoolLessons)} (${visibleLessons.length})

${visibleLessons.map(lesson => ` +
+

${esc(lesson.title || "")}

+

${esc(lesson.text || "")}

+ ${lesson.sessionDate ? `
📅 ${esc(new Date(lesson.sessionDate).toISOString().slice(0, 10))}
` : ""} +
`).join("")}
` : ""} +` + return renderClearnetPage({ + title: `${course.title || "Course"} | Oasis`, + ogTitle: course.title || "Course", + ogDescription: course.description || "", + ogImage: courseImg, + extraCss, + body, + hubFeedId: course.author || null + }) +} + +const renderMaterial = (material, course, isTeacher) => { + const { img, video, audio } = require("../server/node_modules/hyperaxe") + const m = String(material.media || "") + const ref = m.match(/\[(image|audio|video|pdf|torrent):([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/) || m.match(/^!?\[([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)$/m) + let body + if (material.locked) body = p(i18n.schoolLessonLocked) + else if (ref && ref.length === 4) { + const kind = ref[1], name = ref[2], blobId = ref[3] + const url = `/blob/${encodeURIComponent(blobId)}` + if (kind === "pdf") body = div({ class: "pdf-viewer-container", id: `pdf-${material.id.slice(1, 9)}`, "data-pdf-url": url }, a({ href: url }, `📎 ${name || "PDF"}`)) + else if (kind === "image") body = img({ src: url, alt: name, class: "school-material-image" }) + else if (kind === "video") body = video({ controls: true, src: url, class: "post-video", preload: 'metadata' }) + else if (kind === "audio") body = audio({ controls: true, src: url }) + else body = p(a({ href: url }, `📎 ${name || blobId}`)) + } else if (ref) { + body = p(a({ href: `/blob/${encodeURIComponent(ref[2])}` }, `📎 ${ref[1] || ref[2]}`)) + } else { + body = renderMd(m) + } + return div({ class: "school-material" }, + body, + material.caption ? p({ class: "school-material-caption" }, material.caption) : null, + isTeacher + ? form({ method: "POST", action: `/school/lesson/media/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(material.lessonId)}/${encodeURIComponent(material.id)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, "DELETE") + ) + : null + ) +} + +exports.singleLessonView = async (course, lesson, materials = [], params = {}) => { + const isTeacher = String(course.author) === String(userId) + const isStudent = course.students.includes(userId) + const courseUrl = `/school/course/${encodeURIComponent(course.id)}` + const lessonUrl = `/school/lesson/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` + + const lessonSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(lesson.id, lessonUrl, { + author: lesson.author || course.author, + reportTitle: lesson.title || course.title, + returnTo: courseUrl, + deleteAction: String(lesson.author || course.author) === String(userId) ? `/school/lesson/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` : undefined + }) + ), + h2({ class: "tribe-card-title" }, a({ href: courseUrl }, course.title)), + renderCourseChips(course), + renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), + table({ class: "tribe-info-table" }, + lesson.unit ? tr( + td({ class: "tribe-info-label" }, i18n.schoolLessonUnit), + td({ class: "tribe-info-value", colspan: "3" }, lesson.unit) + ) : null, + lesson.sessionDate ? tr( + td({ class: "tribe-info-label" }, i18n.schoolSessionDate), + td({ class: "tribe-info-value", colspan: "3" }, moment(lesson.sessionDate).format("YYYY/MM/DD HH:mm")) + ) : null, + tr( + td({ class: "tribe-info-label" }, i18n.schoolTeacher), + td({ class: "tribe-info-value", colspan: "3" }, userLink(course.author)) + ) + ), + isStudent + ? div({ class: "tribe-side-actions" }, + form({ method: "POST", action: `/school/lesson/complete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` }, + input({ type: "hidden", name: "value", value: lesson.completed ? "false" : "true" }), + input({ type: "hidden", name: "returnTo", value: lessonUrl }), + button({ type: "submit", class: "tribe-action-btn" }, lesson.completed ? `✓ ${i18n.schoolProgressDone}` : i18n.schoolProgressMark) + ) + ) + : null, + isTeacher + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: lessonUrl }, + input({ type: "hidden", name: "edit", value: "1" }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) + ) + ) + : null + ) + + const lessonMain = div({ class: "tribe-main" }, + params.edit && isTeacher + ? div({ class: "create-tribe-form" }, + h2(i18n.chatUpdate), + form({ method: "POST", action: `/school/lesson/update/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` }, + label(i18n.title), br, + input({ type: "text", name: "title", maxlength: "120", required: true, value: lesson.title || "" }), br(), + textarea({ name: "text", rows: 5, required: true, maxlength: "4000" }, lesson.text || ""), br, + label(i18n.schoolLessonUnit), br, + input({ type: "text", name: "unit", maxlength: "60", value: lesson.unit || "" }), br(), + label(i18n.schoolLessonOrder), br, + input({ type: "number", name: "order", min: "0", step: "1", value: lesson.order != null ? String(lesson.order) : "" }), br(), br(), + label(i18n.schoolSessionDate), br, + input({ type: "datetime-local", name: "sessionDate", min: moment().format("YYYY-MM-DDTHH:mm"), value: lesson.sessionDate ? moment(lesson.sessionDate).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), + button({ type: "submit" }, i18n.chatUpdate) + ) + ) + : div({ class: "school-lesson" }, + h2(lesson.title), + renderMd(lesson.text) + ), + div({ class: "school-materials" }, + h2(`${i18n.schoolMaterials} (${safeArr(materials).length})`), + safeArr(materials).length + ? materials.map(material => renderMaterial(material, course, isTeacher)) + : p(i18n.schoolNoMaterials) + ), + isTeacher + ? div({ class: "create-tribe-form" }, + h2(i18n.schoolAddMaterial), + form({ method: "POST", action: `/school/lesson/media/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}`, enctype: "multipart/form-data" }, + label(i18n.blogImage), br, + input({ type: "file", name: "files", multiple: true, accept: "image/*,video/*,audio/*,application/pdf,.md,.txt,.epub,.zip" }), br(), br(), + label(i18n.title), br, + input({ type: "text", name: "caption", maxlength: "120" }), br(), br(), + label(i18n.description), br, + textarea({ name: "text", rows: 3, maxlength: "2000", placeholder: i18n.schoolMaterialTextPlaceholder }), br, + button({ type: "submit" }, i18n.schoolAddMaterial) + ) + ) + : null + ) + + const tpl = template( + lesson.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.schoolTitle), p(i18n.schoolDescription), renderWalletChip())), + section(renderModeButtons("all", false, (params && params.modesAvail) || null)), + section(div({ class: "tribe-details" }, lessonSide, lessonMain)) + ) + const hasPdf = safeArr(materials).some(m => !m.locked && /\[pdf:/.test(String(m.media || ""))) + return hasPdf ? `${tpl}` : tpl +} + +exports.schoolInvitePage = (code) => { + return template( + i18n.schoolTitle, + section( + div({ class: "invite-page" }, + h2(i18n.tribeInviteCodeText, " ", code), + form({ method: "GET", action: "/school" }, + button({ type: "submit", class: "filter-btn" }, i18n.walletBack) + ) + ) + ) + ) +} diff --git a/src/views/search_view.js b/src/views/search_view.js index a8a583c..e494b3d 100644 --- a/src/views/search_view.js +++ b/src/views/search_view.js @@ -1,77 +1,99 @@ const { form, button, div, h2, p, section, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions, emptyState } = require('./main_views'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged, currentPerPage, LIST_PAGE_SIZES, pageSizeLabel } = require('./main_views'); const moment = require("../server/node_modules/moment"); -const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); +const { renderStyledText } = require('../backend/renderStyledText'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { renderZoomableImage } = require('./gallery_view'); const { config } = require("../server/SSB_server.js"); const userId = config.keys.id; -const decodeMaybe = (s) => { - try { return decodeURIComponent(String(s || '')); } catch { return String(s || ''); } + +const industryStatusLabel = (status) => { + const s = String(status || 'ACTIVE').toUpperCase(); + if (s === 'PAUSED') return i18n.industryStatusPaused || 'PAUSED'; + if (s === 'DISSOLVED') return i18n.industryStatusDissolved || 'DISSOLVED'; + return i18n.industryStatusActive || 'WORKING'; }; -const rewriteHashtagLinks = (html) => { - const s = String(html || ''); - return s.replace(/href=(["'])(?:https?:\/\/[^"']+)?\/hashtag\/([^"'?#]+)([^"']*)\1/g, (m, q, tag, rest) => { - const t = decodeMaybe(tag).replace(/^#/, '').trim().toLowerCase(); - const href = `/search?query=%23${encodeURIComponent(t)}`; - return `href=${q}${href}${q}`; - }); -}; -const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = [], hashtag = null, results = {}, resultCount = "10" }) => { - const searchInput = input({ - id: "search_query", - name: "query", - required: false, - type: "search", - value: query, - placeholder: i18n.searchPlaceholder - }); +const renderSearchForm = ({ action, query = "", queryName = "query", hidden = [] } = {}) => { + const searchInput = input({ id: "search_query", name: queryName, required: false, type: "search", value: query, placeholder: i18n.searchPlaceholder }); searchInput.setAttribute("minlength", 3); + const resultCount = currentPerPage(); + const resultsPerPageSelect = select( + { id: "results-per-page", name: "perPage", class: "input-select search-select" }, + LIST_PAGE_SIZES.map(s => option({ value: s, selected: resultCount === s ? "selected" : undefined }, pageSizeLabel(s))) + ); + return form( + { action, method: "GET", class: "search-form" }, + ...hidden.map(([name, value]) => input({ type: "hidden", name, value })), + div({ class: "search-filters-row" }, + table({ class: "search-filters-table" }, + tr( + td({ class: 'card-label' }, label({ for: "search_from" }, i18n.searchFromLabel || "From")), + td(input({ id: "search_from", type: "datetime-local", name: "from" })) + ), + tr( + td({ class: 'card-label' }, label({ for: "search_to" }, i18n.searchToLabel || "To")), + td(input({ id: "search_to", type: "datetime-local", name: "to" })) + ), + tr( + td({ class: 'card-label' }, label({ for: "search_inhabitant" }, "Oasis ID")), + td(input({ id: "search_inhabitant", type: "text", name: "inhabitant", placeholder: "@...=.ed25519", pattern: "@[A-Za-z0-9+/_\\-]{43}=\\.ed25519", maxlength: 56, class: "search-oasis-id" })) + ) + ), + table({ class: "search-filters-table" }, + tr( + td({ class: 'card-label' }, label({ for: "search_query" }, i18n.searchQueryLabel || "Query")), + td(searchInput) + ), + tr( + td({ class: 'card-label' }, label({ for: "results-per-page" }, i18n.searchPerPageLabel || "Results per page")), + td(resultsPerPageSelect) + ) + ) + ), + div({ class: "search-submit-row" }, + button({ type: "submit" }, i18n.searchSubmit) + ) + ); +}; +exports.renderSearchForm = renderSearchForm; + +const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = [], hashtag = null, results = {}, spreadMap = null, favIndex = null }) => { const contentTypes = [ "post", "about", "curriculum", "tribe", "market", "transfer", "feed", "votes", - "report", "task", "event", "bookmark", "image", "audio", "video", "document", "torrent", - "bankWallet", "bankClaim", "project", "job", "forum", "vote", "contact", "pub", "map", "shop", "shopProduct", "chat", "pad", "all" + "report", "task", "event", "bookmark", "image", "audio", "video", "document", "torrent", "file", + "bankWallet", "bankClaim", "project", "job", "industry", "industryBlueprint", "forum", "vote", "contact", "map", "shop", "shopProduct", "chat", "pad", "room", "schoolCourse", "wikiPage", "emergency", "mailingList", "logisticsRoute", "podcast", "podcastEpisode", "campaign", "all" ]; const filterSelect = select( { id: "search-type", name: "type", - class: "input-select", - style: "position:relative; z-index:10;" + class: "input-select search-select" }, contentTypes.map(type => option({ value: type === 'all' ? "" : type, - selected: (types.length === 0 && type === 'all') || types.includes(type) + ...((types.length === 0 && type === 'all') || types.includes(type) ? { selected: true } : {}) }, i18n[type + "Label"] || type.toUpperCase()) ) ); - const resultsPerPageSelect = select( - { - id: "results-per-page", - name: "resultsPerPage", - class: "input-select", - style: "position:relative; z-index:10;margin-left:10px;" - }, - option({ value: "100", selected: resultCount === "100" ? "selected" : undefined }, "100"), - option({ value: "50", selected: resultCount === "50" ? "selected" : undefined }, "50"), - option({ value: "10", selected: resultCount === "10" ? "selected" : undefined }, "10"), - option({ value: "all", selected: resultCount === "all" ? "selected" : undefined }, i18n.allTypesLabel) - ); const getViewDetailsActionForSearch = (type, contentId, content) => { switch (type) { case 'votes': return `/votes/${encodeURIComponent(contentId)}`; case 'transfer': return `/transfers/${encodeURIComponent(contentId)}`; case 'tribe': return `/tribe/${encodeURIComponent(contentId)}`; - case 'about': return content && (content.about || content.author) ? `/inhabitant/${encodeURIComponent(content.about || content.author)}` : '#'; - case 'curriculum': return content && content.author ? `/inhabitant/${encodeURIComponent(content.author)}` : '#'; + case 'about': return content && (content.about || content.author) ? `/inhabitant/${encodeURIComponent(content.about || content.author)}` : null; + case 'curriculum': return content && content.author ? `/inhabitant/${encodeURIComponent(content.author)}` : null; + case 'feed': return `/feed/${encodeURIComponent(contentId)}`; + case 'poll': return `/polls/${encodeURIComponent(contentId)}`; case 'image': return `/images/${encodeURIComponent(contentId)}`; case 'audio': return `/audios/${encodeURIComponent(contentId)}`; case 'video': return `/videos/${encodeURIComponent(contentId)}`; @@ -79,15 +101,21 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'bookmark': return `/bookmarks/${encodeURIComponent(contentId)}`; case 'event': return `/events/${encodeURIComponent(contentId)}`; case 'task': return `/tasks/${encodeURIComponent(contentId)}`; - case 'post': return `/thread/${encodeURIComponent(contentId)}#${encodeURIComponent(contentId)}`; + case 'post': return content && typeof content.root === 'string' + ? `/thread/${encodeURIComponent(content.root)}#${encodeURIComponent(contentId)}` + : `/blogs/${encodeURIComponent(contentId)}`; case 'market': return `/market/${encodeURIComponent(contentId)}`; case 'report': return `/reports/${encodeURIComponent(contentId)}`; case 'project': return `/projects/${encodeURIComponent(contentId)}`; + case 'schoolCourse': return `/school/course/${encodeURIComponent(contentId)}`; + case 'housing': return `/housing/${encodeURIComponent(contentId)}`; case 'job': return `/jobs/${encodeURIComponent(contentId)}`; - case 'forum': return `/forum/${encodeURIComponent(contentId)}`; - case 'vote': return content && content.vote && content.vote.link ? `/thread/${encodeURIComponent(content.vote.link)}#${encodeURIComponent(content.vote.link)}` : '#'; - case 'contact': return content && content.contact ? `/author/${encodeURIComponent(content.contact)}` : '#'; - case 'pub': return '#'; + case 'industry': return `/industry/${encodeURIComponent(contentId)}`; + case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(contentId)}`; + case 'forum': return content && content.root ? `/forum/${encodeURIComponent(typeof content.root === 'string' ? content.root : (content.root.key || content.root.id || ''))}#msg-${String(contentId || '').replace(/[^a-zA-Z0-9]/g, '')}` : `/forum/${encodeURIComponent(contentId)}`; + case 'vote': return content && content.vote && content.vote.link ? `/thread/${encodeURIComponent(content.vote.link)}#${encodeURIComponent(content.vote.link)}` : null; + case 'contact': return content && content.contact ? `/author/${encodeURIComponent(content.contact)}` : null; + case 'pub': return null; case 'bankWallet': return `/banking`; case 'bankClaim': return `/banking`; case 'map': return `/maps/${encodeURIComponent(contentId)}`; @@ -95,9 +123,18 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'shopProduct': return `/shops/product/${encodeURIComponent(contentId)}`; case 'chat': return `/chats/${encodeURIComponent(contentId)}`; case 'pad': return `/pads/${encodeURIComponent(contentId)}`; + case 'room': return `/rooms/${encodeURIComponent(contentId)}`; + case 'wikiPage': return `/wiki/${encodeURIComponent(contentId)}`; + case 'emergency': return `/emergencies/${encodeURIComponent(contentId)}`; + case 'mailingList': return `/mailing/${encodeURIComponent(contentId)}`; + case 'logisticsRoute': return `/logistics/${encodeURIComponent(contentId)}`; + case 'podcast': return `/podcasts/${encodeURIComponent(contentId)}`; + case 'podcastEpisode': return `/podcasts/episode/${encodeURIComponent(contentId)}`; + case 'campaign': return `/campaigns/${encodeURIComponent(contentId)}`; case 'torrent': return `/torrents/${encodeURIComponent(contentId)}`; + case 'file': return `/files/${encodeURIComponent(contentId)}`; case 'gameScore': return content && content.game ? `/games/${encodeURIComponent(content.game)}` : '/games'; - default: return '#'; + default: return null; } }; @@ -108,28 +145,39 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = const s = String(value).trim().replace(/&/g, '&'); const m = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); const src = m ? m[1] : s; - return src.startsWith('&') ? img({ src: `/blob/${encodeURIComponent(src)}`, class: 'search-result-image' }) : null; + return src.startsWith('&') ? renderZoomableImage(`/blob/${encodeURIComponent(src)}`, { imgClass: 'search-result-image' }) : null; }; const renderContentHtml = (content) => { switch (content.type) { case 'post': return div({ class: 'search-post' }, - content.contentWarning ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.contentWarning)) : null, - content.text ? div({ class: 'card-field' }, span({ class: 'card-value', innerHTML: sanitizeHtml(content.text) })) : null + content.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, content.contentWarning)) : null, + content.text ? div({ class: 'blog-card-text post-text-clamped', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(content.text).trim().length > 1000 ? String(content.text).trim().slice(0, 1000).replace(/\s+$/, '') + '…' : String(content.text).trim())) }) : null, + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null ); case 'about': - return div({ class: 'search-about' }, - content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.name + ':'), span({ class: 'card-value' }, content.name)) : null, - content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.description + ':'), span({ class: 'card-value' }, content.description)) : null, - content.image ? img({ src: `/image/64/${encodeURIComponent(content.image)}` }) : null + return div({ class: 'card-section about' }, + div({ class: 'about-card-cols' }, + div({ class: 'about-card-col-img' }, + content.image + ? renderZoomableImage(`/blob/${encodeURIComponent(content.image)}`, { imgClass: 'activity-avatar', alt: content.name || '', linkClass: 'zoom-inline' }) + : img({ src: '/assets/images/default-avatar.png', alt: content.name || '', class: 'activity-avatar' }) + ), + div({ class: 'about-card-col-desc' }, + h2({ class: 'about-card-header' }, userLink(content.about, content.name)), + content.description ? p({ class: 'tribe-side-description' }, ...renderStyledText(String(content.description))) : null + ) + ) ); case 'feed': { const rawText = typeof content.text === 'string' ? content.text.trim() : ''; - const htmlText = rawText ? rewriteHashtagLinks(renderTextWithStyles(rawText)) : ''; + const htmlText = rawText ? renderStyledHtml(rawText) : ''; const refeedsNum = Number(content.refeeds || 0) || 0; return div({ class: 'search-feed' }, - rawText ? div({ class: 'card-field' }, span({ class: 'card-value', innerHTML: sanitizeHtml(htmlText) })) : null, + rawText ? div({ class: 'card-field' }, span({ class: 'card-value', [RAW_HTML]: sanitizeHtml(htmlText) })) : null, refeedsNum > 0 ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ':'), @@ -144,13 +192,13 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = const cleanEvDesc = rawEvDesc.replace(/!\[[^\]]*\]\(&[^)]+\.sha256\)/g, '').trim(); return div({ class: 'search-event' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - cleanEvDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, cleanEvDesc)) : null, + cleanEvDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, cleanEvDesc)) : null, blobImg(content.image || blobInEvDesc), - content.date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventDate + ':'), span({ class: 'card-value' }, new Date(content.date).toLocaleString())) : null, + content.date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventDate + ':'), span({ class: 'card-value' }, moment(content.date).format("YYYY/MM/DD HH:mm"))) : null, content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventLocation + ':'), span({ class: 'card-value' }, content.location)) : null, content.isPublic ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventPrivacyLabel + ':'), span({ class: 'card-value' }, content.isPublic)) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventStatus + ':'), span({ class: 'card-value' }, content.status)) : null, - content.eventUrl ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventUrlLabel + ':'), span({ class: 'card-value' }, a({ href: content.eventUrl, target: '_blank' }, content.eventUrl))) : null, + content.eventUrl ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventUrlLabel + ':'), span({ class: 'card-value' }, a({ href: safeExternalHref(content.eventUrl), target: '_blank', rel: 'noopener noreferrer' }, content.eventUrl))) : null, content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventPrice + ':'), span({ class: 'card-value' }, content.price)) : null, content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -175,7 +223,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ) : null, content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':' ), - span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '') + span({ class: 'card-value' }, content.deadline ? moment(content.deadline).format("YYYY/MM/DD HH:mm") : '') ) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteTotalVotes + ':' ), @@ -192,9 +240,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'tribe': return div({ class: 'search-tribe' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, - (() => { const s = String(content.image || '').trim().replace(/&/g, '&'); const m = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); const src = m ? m[1] : s; return src.startsWith('&') ? img({ src: `/blob/${encodeURIComponent(src)}`, class: 'feed-image' }) : img({ src: '/assets/images/default-tribe.png', class: 'feed-image' }); })(), - content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, ...renderUrl(content.description))) : null, - content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLocationLabel + ':'), span({ class: 'card-value' }, ...renderUrl(content.location))) : null, + (() => { const s = String(content.image || '').trim().replace(/&/g, '&'); const m = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); const src = m ? m[1] : s; return src.startsWith('&') ? img({ loading: 'lazy', src: `/blob/${encodeURIComponent(src)}`, class: 'feed-image' }) : img({ src: '/assets/images/default-tribe.png', class: 'feed-image' }); })(), + content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, ...renderStyledText(content.description))) : null, + content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLocationLabel + ':'), span({ class: 'card-value' }, ...renderStyledText(content.location))) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel + ':'), span({ class: 'card-value' }, content.isAnonymous ? i18n.tribePrivate : i18n.tribePublic)), content.inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeModeLabel + ':'), span({ class: 'card-value' }, String(content.inviteMode).toUpperCase())) : null, Array.isArray(content.members) @@ -209,7 +257,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'audio': return content.url ? div({ class: 'search-audio' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, br(), audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType, preload: 'metadata' }), br(), @@ -222,10 +270,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'image': return content.url ? div({ class: 'search-image' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, - content.meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, br(), - img({ src: `/blob/${encodeURIComponent(content.url)}` }), + renderZoomableImage(`/blob/${encodeURIComponent(content.url)}`, { imgClass: 'search-result-image' }), br(), content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -236,10 +283,10 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'video': return content.url ? div({ class: 'search-video' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, - br(), - videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', width: '640', height: '360' }), - br(), + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + div({ class: 'video-container' }, + videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', width: '640', height: '360', preload: 'metadata' }) + ), content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`) @@ -264,10 +311,16 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = )) : null ); + case 'file': + return div({ class: 'search-file' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.fileTitleLabel || i18n.title) + ':'), span({ class: 'card-value' }, content.title)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + content.size ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.fileSizeLabel || 'Size') + ':'), span({ class: 'card-value' }, String(content.size))) : null + ); case 'torrent': return div({ class: 'search-torrent' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentTitleLabel || i18n.title) + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentDescriptionLabel || i18n.searchDescription) + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, content.size ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentSizeLabel || 'Size') + ':'), span({ class: 'card-value' }, String(content.size))) : null, content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -278,17 +331,17 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'market': return div({ class: 'search-market' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, content.item_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, content.item_type.toUpperCase())) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemCondition + ':'), span({ class: 'card-value' }, content.status)) : null, - content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemDeadline + ':'), span({ class: 'card-value' }, new Date(content.deadline).toLocaleString())) : null, + content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemDeadline + ':'), span({ class: 'card-value' }, moment(content.deadline).format("YYYY/MM/DD HH:mm"))) : null, br(), blobImg(content.image), br(), content.seller ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemSeller + ':'), span({ class: 'card-value' }, userLink(content.seller))) : null, content.stock ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, content.stock || 'N/A')) : null, - content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchPriceLabel + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null, - content.condition ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.condition)) : null, + content.price ? div({ class: 'price-chip' }, `${content.price} ECO`) : null, + content.condition ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.condition)) : null, content.includesShipping ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemIncludesShipping + ':'), span({ class: 'card-value' }, `${content.includesShipping ? i18n.YESLabel : i18n.NOLabel}`)) : null, content.auctions_poll && content.auctions_poll.length > 0 ? div({ class: 'auction-info' }, @@ -318,10 +371,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ); case 'bookmark': return div({ class: 'search-bookmark' }, - content.url ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkUrlLabel + ':'), span({ class: 'card-value' }, a({ href: content.url, target: '_blank' }, content.url))) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null, - content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkCategory + ':'), span({ class: 'card-value' }, content.category)) : null, - content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString())) : null, + content.url ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkUrlLabel + ':'), span({ class: 'card-value' }, a({ href: safeExternalHref(content.url), target: '_blank', rel: 'noopener noreferrer' }, content.url))) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, moment(content.lastVisit).format("YYYY/MM/DD HH:mm"))) : null, content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`) @@ -331,13 +383,13 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'task': return div({ class: 'search-task' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field', style: 'display:flex;flex-direction:column;' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), p({ class: 'card-value', style: 'white-space:pre-wrap;margin-top:4px;' }, content.description)) : null, + content.description ? div({ class: 'card-field card-field-stacked' }, p({ class: 'card-value card-value-pre' }, content.description)) : null, content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null, content.priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchPriorityLabel + ':'), span({ class: 'card-value' }, content.priority)) : null, typeof content.isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchIsPublicLabel + ':'), span({ class: 'card-value' }, content.isPublic ? i18n.YESLabel : i18n.NOLabel)) : null, - content.startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskStartTimeLabel + ':'), span({ class: 'card-value' }, new Date(content.startTime).toLocaleString())) : null, - content.endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskEndTimeLabel + ':'), span({ class: 'card-value' }, new Date(content.endTime).toLocaleString())) : null, + content.startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskStartTimeLabel + ':'), span({ class: 'card-value' }, moment(content.startTime).format("YYYY/MM/DD HH:mm"))) : null, + content.endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskEndTimeLabel + ':'), span({ class: 'card-value' }, moment(content.endTime).format("YYYY/MM/DD HH:mm"))) : null, Array.isArray(content.assignees) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskAssignees + ':'), span({ class: 'card-value' }, content.assignees.length)) : null, content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -351,7 +403,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null, content.severity ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.reportsSeverity + ':'), span({ class: 'card-value' }, content.severity)) : null, content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchCategoryLabel + ':'), span({ class: 'card-value' }, content.category)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, br(), blobImg(content.image), br(), @@ -370,7 +422,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = content.amount ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersAmount + ':'), span({ class: 'card-value' }, content.amount)) : null, br(), content.from ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersFrom + ':'), span({ class: 'card-value' }, userLink(content.from))) : null, - content.to ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersTo + ':'), span({ class: 'card-value' }, userLink(content.to))) : null, + content.to && process.env.OASIS_MOBILE !== '1' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersTo + ':'), span({ class: 'card-value' }, userLink(content.to))) : null, br(), content.confirmedBy && content.confirmedBy.length ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersConfirmations + ':'), span({ class: 'card-value' }, content.confirmedBy.length)) @@ -384,8 +436,8 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'curriculum': return div({ class: 'search-curriculum' }, content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvNameLabel + ':'), span({ class: 'card-value' }, content.name)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, - content.photo ? img({ src: `/blob/${encodeURIComponent(content.photo)}`, class: 'curriculum-photo' }) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + content.photo ? renderZoomableImage(`/blob/${encodeURIComponent(content.photo)}`, { imgClass: 'curriculum-photo' }) : null, content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null, content.preferences ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvPreferencesLabel + ':'), span({ class: 'card-value' }, content.preferences)) : null, @@ -435,9 +487,28 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ) : null, content.txid ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bankTx + ':' ), - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${content.txid}`, target: '_blank' }, content.txid) + span({ class: 'card-value bank-address-code' }, content.txid) ) : null ); + case 'schoolCourse': + return div({ class: 'search-school' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.schoolCourseType || 'Course type') + ':'), span({ class: 'card-value' }, String(content.visibility || '').toUpperCase() === 'INVITE' ? 'INVITE-ONLY' : (Number(content.price) > 0 ? 'PAID' : 'OPEN'))), + Number(content.price) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.schoolPrice || 'Price (ECO)') + ':'), span({ class: 'card-value' }, `${Number(content.price).toFixed(2)} ECO`)) : null, + content.startDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.schoolStartDate || 'Start date') + ':'), span({ class: 'card-value' }, moment(content.startDate).format("YYYY/MM/DD HH:mm"))) : null, + content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, String(content.description).length > 220 ? String(content.description).slice(0, 220) + '…' : content.description)) : null, + Array.isArray(content.tags) && content.tags.length ? div({ class: 'card-tags' }, + content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)) + ) : null + ); + case 'housing': + return div({ class: 'search-housing' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, + content.housing_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingType + ':'), span({ class: 'card-value' }, String(content.housing_type).toUpperCase())) : null, + content.place ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPlace + ':'), span({ class: 'card-value' }, content.place)) : null, + String(content.housing_type || '').toLowerCase() !== 'couchsurfing' && content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPrice + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null, + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingStatus + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : null + ); case 'job': return div({ class: 'search-job' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, @@ -448,11 +519,24 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = content.vacants ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobVacants + ':'), span({ class: 'card-value' }, content.vacants)) : null, Array.isArray(content.subscribers) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSubscribers + ':'), span({ class: 'card-value' }, `${content.subscribers.length}`)) : null ); + case 'industry': + return div({ class: 'search-industry' }, + content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryName + ':'), span({ class: 'card-value' }, content.name)) : null, + content.sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industrySector + ':'), span({ class: 'card-value' }, String(content.sector).toUpperCase())) : null, + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryStatusLabel + ':'), span({ class: 'card-value' }, industryStatusLabel(content.status))) : null, + content.description ? div({ class: "card-field card-field-stacked" }, span({ class: "card-value" }, content.description)) : null + ); + case "industryBlueprint": + return div({ class: 'search-industry' }, + content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryBlueprint + ':'), span({ class: 'card-value' }, content.name)) : null, + content.outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryOutputKind + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + content.outKind] || content.outKind).toUpperCase())) : null, + content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, content.description)) : null + ); case 'forum': return div({ class: 'search-forum' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchCategoryLabel + ':'), span({ class: 'card-value' }, content.category)) : null, - content.text ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.text)) : null + content.text ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, content.text)) : null ); case 'vote': return div({ class: 'search-vote-link' }, @@ -470,7 +554,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = return div({ class: 'search-shop' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, blobImg(content.image), - content.shortDescription ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.shortDescription)) : null, + content.shortDescription ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.shortDescription)) : null, content.visibility ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopStatus || 'STATUS') + ':'), span({ class: 'card-value' }, content.visibility)) : null, content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.searchLocationLabel || 'LOCATION') + ':'), span({ class: 'card-value' }, content.location)) : null, content.tags && content.tags.length @@ -481,8 +565,8 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = return div({ class: 'search-shop-product' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, blobImg(content.image), - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null, - content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.searchPriceLabel || 'PRICE') + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + content.price ? div({ class: 'price-chip' }, `${content.price} ECO`) : null, content.stock !== undefined ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, content.stock)) : null ); case 'chat': { @@ -493,7 +577,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ); return div({ class: 'search-chat' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatsTitle || 'Chat') + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatDescription || 'Description') + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, content.category)) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatStatus || 'STATUS') + ':'), span({ class: 'card-value' }, content.status)) : null ); @@ -510,6 +594,11 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padDeadlineLabel || 'Deadline') + ':'), span({ class: 'card-value' }, content.deadline)) : null ); } + case 'room': + return div({ class: 'search-room' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.roomsTitle || 'Rooms') + ':'), span({ class: 'card-value' }, content.title)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null + ); case 'gameScore': return div({ class: 'search-game' }, content.game ? div({ class: 'game-row' }, @@ -524,7 +613,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'map': return div({ class: 'search-map' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.mapDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, content.lat && content.lng ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.mapLocationTitle + ':'), span({ class: 'card-value' }, `${content.lat}, ${content.lng}`)) : null, content.mapType ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.mapTypeLabel + ':'), span({ class: 'card-value' }, content.mapType)) : null, content.tags && content.tags.length @@ -534,17 +623,25 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = : null ); default: - return div({ class: 'styled-text', innerHTML: sanitizeHtml(renderTextWithStyles(content.text || content.description || content.title || '[no content]')) }); + return div({ class: 'styled-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(content.text || content.description || content.title || '[no content]')) }); } }; + const typeLabel = (type) => String(i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || type || '').toUpperCase(); + const totalResults = Object.values(results).reduce((n, msgs) => n + (Array.isArray(msgs) ? msgs.length : 0), 0); + const resultStats = renderModuleStats(totalResults, + Object.entries(results) + .map(([type, msgs]) => ({ label: typeLabel(type), count: Array.isArray(msgs) ? msgs.length : 0 })) + .sort((x, y) => y.count - x.count || x.label.localeCompare(y.label))); + const pageGroups = paged(Object.entries(results).flatMap(([key, msgs]) => msgs.map((msg) => [key, msg]))) + .reduce((acc, [key, msg]) => { (acc[key] = acc[key] || []).push(msg); return acc; }, {}); const resultSection = Object.entries(results).length > 0 - ? Object.entries(results).map(([key, msgs]) => + ? Object.entries(pageGroups).map(([key, msgs]) => div( { class: "search-result-group" }, ...msgs.map((msg) => { const content = msg.value.content || {}; - const created = new Date(msg.timestamp).toLocaleString(); + const created = moment(msg.timestamp).format("YYYY/MM/DD HH:mm"); if (content.type === 'document') hasDocument = true; const contentHtml = renderContentHtml(content); let author; @@ -582,16 +679,26 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = div({ class: 'card-header activity-card-header' }, div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(content.type || '').toUpperCase()) + span({ class: 'pm-exposition-text' }, String(i18n['type' + String(content.type || '').charAt(0).toUpperCase() + String(content.type || '').slice(1)] || content.type || '').toUpperCase()) ) ), - renderContentActions(contentId, getViewDetailsActionForSearch(content.type, contentId, content)) + renderContentActions(contentId, getViewDetailsActionForSearch(content.type, contentId, content), { + author: msg.value.author, + reportTitle: content.title || content.question || content.concept || '', + spread: CONTENT_SPREADABLE.has(content.type) ? ((spreadMap instanceof Map && spreadMap.get(contentId)) || null) : undefined, + ...(CONTENT_TORRENTABLE.has(content.type) && typeof content.url === 'string' && content.url.startsWith('&') + ? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } } + : {}), + ...(CONTENT_FAV_KIND[content.type] + ? { favKind: CONTENT_FAV_KIND[content.type], isFavorite: favIndex instanceof Map ? favIndex.get(String(contentId)) === CONTENT_FAV_KIND[content.type] : false, returnTo: `/search?query=${encodeURIComponent(hashtag ? `#${hashtag}` : String(query || ''))}` } + : {}) + }) ), div({ class: 'card-section search-card-body' }, contentHtml, author ? p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${created} ${i18n.performed} `), + span({ class: 'date-link' }, `${created}`), (authorUrl && authorUrl !== '#' && authorUrl.startsWith('/author/')) ? userLink(decodeURIComponent(authorUrl.replace(/^\/author\//, ''))) : a({ href: authorUrl, class: 'user-link' }, `${author}`) @@ -601,55 +708,28 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = }) ) ) - : emptyState({ icon: "🔍", title: i18n.noResultsFound }); + : (String(query || '').trim() || hashtag + ? div({ class: 'no-results' }, p(i18n.noResultsFound)) + : null); let html = template( - hashtag ? `#${hashtag}` : i18n.searchTitle, + i18n.searchTitle, section( - div({ class: "tags-header" }, - h2(hashtag ? `#${hashtag}` : i18n.searchTitle), + div({ class: "tags-header module-header-line" }, + h2(i18n.searchTitle), p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel) ), - form( - { action: "/search", method: "POST", class: "search-form" }, - div({ class: "search-filters-row" }, - table({ class: "search-filters-table" }, - tr( - td({ class: 'card-label' }, label({ for: "search_from" }, i18n.searchFromLabel || "From")), - td(input({ id: "search_from", type: "datetime-local", name: "from" })) - ), - tr( - td({ class: 'card-label' }, label({ for: "search_to" }, i18n.searchToLabel || "To")), - td(input({ id: "search_to", type: "datetime-local", name: "to" })) - ), - tr( - td({ class: 'card-label' }, label({ for: "search_inhabitant" }, "Oasis ID")), - td(input({ id: "search_inhabitant", type: "text", name: "inhabitant", placeholder: "@...=.ed25519", pattern: "@[A-Za-z0-9+/_\\-]{43}=\\.ed25519", maxlength: 56, class: "search-oasis-id" })) - ) - ), - table({ class: "search-filters-table" }, - tr( - td({ class: 'card-label' }, label({ for: "search_query" }, i18n.searchQueryLabel || "Query")), - td(searchInput) - ), - tr( - td({ class: 'card-label' }, label({ for: "results-per-page" }, i18n.searchPerPageLabel || "Results per page")), - td(resultsPerPageSelect) - ) - ) - ), - div({ class: "search-submit-row" }, - button({ type: "submit" }, i18n.searchSubmit) - ) - ) + renderSearchForm({ action: "/search", query }) ), - section(resultSection) + section( + resultStats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, resultStats) : null, + resultSection + ) ); if (hasDocument) { html += ` - - + `; } diff --git a/src/views/settings_view.js b/src/views/settings_view.js index 0969d08..6ce4758 100644 --- a/src/views/settings_view.js +++ b/src/views/settings_view.js @@ -1,13 +1,31 @@ -const { form, button, div, h2, h3, p, section, select, option, input, br, a, label, span } = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, h3, p, section, select, option, input, br, a, label, span, img, strong, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe"); const fs = require('fs'); const path = require('path'); const { getConfig } = require('../configs/config-manager.js'); -const { template, selectedLanguage, i18n, setLanguage } = require('./main_views'); +const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES, MOBILE_THEMES, userLink } = require('./main_views'); const i18nBase = require("../client/assets/translations/i18n"); +const moment = require("../server/node_modules/moment"); +const { WORKFLOWS, currentWorkflow } = require('../models/workflows_model'); +const mobileUi = require('./mobile_ui'); +const html = require("../server/node_modules/hyperaxe"); +// selector de identidades: la implementacion vive en ./identities_view.js +const { renderIdentities } = require('./identities_view')({ i18n, html }); +const { renderVerificationReport, renderRebuildReport } = require('./backup_view'); const snhUrl = "https://wiki.solarnethub.com/socialnet/overview"; +const BLOB_CACHE_OPTIONS = [512, 1024, 2048, 5120, 10240, 0]; +const plainLabel = (s) => String(s || '').replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/\s+/g, ' ').trim(); +const hiddenAt = (at) => at ? moment(at).format("YYYY/MM/DD HH:mm") : "—"; +const sizeLabel = (bytes) => { + let value = Number(bytes) || 0; + const units = ['B', 'KB', 'MB', 'GB']; + let unit = 0; + while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } + return `${unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`; +}; +const blobCacheOptionLabel = (mb) => mb === 0 ? i18n.settingsBlobCacheUnlimited : (mb >= 1024 ? `${mb / 1024} GB` : `${mb} MB`); -const themeFilePath = path.join(__dirname, '../configs/oasis-config.json'); +const themeFilePath = require('../configs/config-manager').configFilePath; const getThemeConfig = () => { try { const configData = fs.readFileSync(themeFilePath); @@ -18,27 +36,61 @@ const getThemeConfig = () => { } }; -const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) => { +const settingsView = (params) => { + const { version, aiPrompt, aiExportCount = 0, blobCache = null, fediverseAccount, telegramAccount = null, telegramLogin = null, peertubeAccount = null, nextcloudAccount = null, verification = null, rebuild = null } = params; const currentThemeConfig = getThemeConfig(); const theme = currentThemeConfig.themes?.current || "Dark-SNH"; const currentConfig = getConfig(); let serverConfig = {}; - try { serverConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '../configs/server-config.json'), 'utf8')); } catch (_) {} + try { serverConfig = require('../configs/config-manager.js').readServerConfig(); } catch (_) {} const currentHops = (serverConfig.friends && Number.isFinite(serverConfig.friends.hops)) ? serverConfig.friends.hops : 2; const walletUrl = currentConfig.wallet.url; const walletUser = currentConfig.wallet.user; const walletFee = currentConfig.wallet.fee; - const pubId = currentConfig.walletPub?.pubId || ''; - const currentWish = currentConfig.wish === 'mutuals' ? 'mutuals' : 'whole'; - const currentPmVisibility = currentConfig.pmVisibility === 'mutuals' ? 'mutuals' : 'whole'; + const currentWish = ['mutuals', 'only-lan', 'local'].includes(currentConfig.wish) ? currentConfig.wish : 'whole'; + const currentPmVisibility = ['mutuals', 'following'].includes(currentConfig.pmVisibility) ? currentConfig.pmVisibility : 'whole'; + const currentMentionsVisibility = ['mutuals', 'following'].includes(currentConfig.mentionsVisibility) ? currentConfig.mentionsVisibility : 'whole'; + const viewerHidden = (() => { try { const vf = require('../models/viewer_filters'); return { muted: vf.mutedList(), hidden: vf.hiddenList() }; } catch (_) { return { muted: [], hidden: [] }; } })(); + const currentPhone = currentConfig.phone && typeof currentConfig.phone === 'object' ? currentConfig.phone : {}; - const themeElements = [ - option({ value: "Dark-SNH", selected: theme === "Dark-SNH" ? true : undefined }, "Dark-SNH"), - option({ value: "Clear-SNH", selected: theme === "Clear-SNH" ? true : undefined }, "Clear-SNH"), - option({ value: "Purple-SNH", selected: theme === "Purple-SNH" ? true : undefined }, "Purple-SNH"), - option({ value: "Matrix-SNH", selected: theme === "Matrix-SNH" ? true : undefined }, "Matrix-SNH"), - option({ value: "OasisMobile", selected: theme === "OasisMobile" ? true : undefined }, "Oasis-Mobile") - ]; + const themeElements = THEMES.map(([value, name]) => option({ value, ...(theme === value ? { selected: true } : {}) }, name)); + + const activeWorkflow = currentWorkflow(currentConfig) || ''; + const modOn = (name) => (currentConfig.modules || {})[`${name}Mod`] === 'on'; + const isMobile = process.env.OASIS_MOBILE === '1'; + + // movil: skins y workflows como tarjetas que se aplican al tocarlas (un boton + // de envio por opcion); el desplegable de escritorio quedaba enterrado y dejaba + // poner temas de escritorio, que en el movil se quedan sin maqueta + const skinPicker = () => section({ id: "theme" }, + div({ class: "tags-header" }, h2(i18n.theme), p(i18n.skinIntroMobile)), + form({ action: "/settings/theme", method: "post", class: "skin-pick" }, + ...MOBILE_THEMES.map(([value, name]) => button({ type: "submit", name: "theme", value, class: theme === value ? "skin-card is-current" : "skin-card" }, + img({ src: `/assets/images/theme-${value.toLowerCase()}.png`, alt: "", class: "skin-shot" }), + span({ class: "skin-name" }, name))) + ) + ); + const workflowPicker = () => section({ id: "workflows" }, + div({ class: "tags-header" }, h2(i18n.workflowsTitle), p(i18n.workflowsDescriptionMobile)), + mobileUi.workflowPick("/settings/workflow", activeWorkflow, WORKFLOWS.map(w => w.key)) + ); + + const workflowSection = isMobile ? workflowPicker() : section({ id: "workflows" }, + div({ class: "tags-header" }, + h2(i18n.workflowsTitle), + p(i18n.workflowsDescription), + form( + { action: "/settings/workflow", method: "POST" }, + select({ name: "workflow" }, + ...WORKFLOWS.map(w => activeWorkflow === w.key + ? option({ value: w.key, selected: true }, i18n[`workflow_${w.key}`]) + : option({ value: w.key }, i18n[`workflow_${w.key}`])) + ), + br(), br(), + button({ type: "submit" }, i18n.workflowsSet) + ) + ) + ); const languageOption = (longName, shortName) => { return shortName === selectedLanguage @@ -51,48 +103,57 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) = button({ type: "submit" }, i18n.rebuildName) ); - const updateFlagPath = path.join(__dirname, '../server/.update_required'); - let updateButton = null; - if (fs.existsSync(updateFlagPath)) { - updateButton = form( - { action: "/update", method: "post" }, - button({ type: "submit" }, i18n.updateit) + const updateInfo = (() => { try { return require('../backend/updater').readUpdateInfo(); } catch (_) { return null; } })(); + let updatePanel = null; + if (updateInfo) { + const d = updateInfo.details || {}; + const day = (iso) => String(iso || '').slice(0, 10); + const versionLine = (v, c) => `${v || '?'}${c ? ` · ${c.hash} · ${day(c.date)}` : ''}`; + const row = (name, ...value) => tr(td({ class: "tribe-info-label" }, name), td({ class: "tribe-info-value" }, ...value)); + updatePanel = section({ id: "update" }, + div({ class: "torrent-download update-panel" }, + div({ class: "torrent-oasis torrent-detail-section" }, + h3({ class: "torrent-section-title" }, i18n.notifyUpdateLabel), + table({ class: "tribe-info-table torrent-file-info" }, + row(i18n.updateInstalledLabel, versionLine(updateInfo.installedVersion, d.installed)), + row(i18n.updateAvailableLabel, versionLine(updateInfo.version, d.available)), + Array.isArray(d.commits) && d.commits.length + ? row(i18n.updateChangesLabel, ul({ class: "update-changes" }, + ...d.commits.map(c => li(`${day(c.date)} · ${c.subject}`)), + d.count > d.commits.length ? li(`+${d.count - d.commits.length}`) : null)) + : null, + d.files ? row(i18n.updateFilesLabel, `${d.files} · +${d.insertions} −${d.deletions}`) : null, + d.origin ? row(i18n.updateOriginLabel, span({ class: "bank-address-code" }, d.origin)) : null + ), + isMobile + ? ((updateInfo.apk || updateInfo.page) + ? a({ href: updateInfo.apk || updateInfo.page, class: "filter-btn update-download-link" }, `\u2B07 OASIS${updateInfo.version ? ` ${updateInfo.version}` : ''}`) + : null) + : form({ action: "/update", method: "post" }, + button({ type: "submit", class: "filter-btn" }, `\u2B07 OASIS${updateInfo.version ? ` ${updateInfo.version}` : ''}`) + ) + ) + ) ); } - return template( - i18n.settings, - section( - div({ class: "tags-header" }, - h2(i18n.settings), - p(a({ href: snhUrl, target: "_blank" }, i18n.settingsIntro({ version }))), - updateButton - ) - ), - section( - div({ class: "tags-header" }, - h2(i18n.theme), - p(i18n.themeIntro), - form( - { action: "/settings/theme", method: "post" }, - select({ name: "theme" }, ...themeElements), - br(), - br(), - button({ type: "submit" }, i18n.setTheme) - ) - ) - ), - section( + const filter = params.filter === 'multiverse' && modOn('fediverse') ? 'multiverse' : params.filter === 'content' ? 'content' : 'oasis'; + const oasisParts = [ + updatePanel, + renderIdentities(require('../server/ssb_config').account), + section({ id: "language" }, div({ class: "tags-header" }, h2(i18n.language), p(i18n.languageDescription), - form( + isMobile ? mobileUi.languagePick() : form( { action: "/language", method: "post" }, select({ name: "language" }, [ languageOption("English", "en"), languageOption("Español", "es"), languageOption("Français", "fr"), languageOption("Euskara", "eu"), + languageOption("Català", "ca"), + languageOption("Galego", "gl"), languageOption("Deutsch", "de"), languageOption("Italiano", "it"), languageOption("Português", "pt"), @@ -107,53 +168,111 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) = ) ) ), - section( + section({ id: "ux" }, div({ class: "tags-header" }, h2(i18n.uxModeTitle || "UX"), p(i18n.uxModeDescription || "Select which UX navigation mode you want for your GUI."), - form( + isMobile ? mobileUi.uxPick("/settings/ux", currentConfig) : form( { action: "/settings/ux", method: "POST" }, (() => { const aiNavEnabled = currentConfig.modules && currentConfig.modules.aiNavMod === 'on'; - const opts = [ - option({ value: "blocks", selected: (currentConfig.ux?.current !== "ainav") ? true : undefined }, i18n.uxModeMenus || "Blocks") - ]; - if (aiNavEnabled) { - opts.push(option({ value: "ainav", selected: currentConfig.ux?.current === "ainav" ? true : undefined }, i18n.uxModeAINav || "AI")); - } - return select({ name: "ux" }, ...opts); + const chatsEnabled = currentConfig.modules && currentConfig.modules.chatsMod === 'on'; + const phoneEnabled = !!(currentConfig.modules && currentConfig.modules.phoneMod !== 'off'); + const roomsEnabled = !!(currentConfig.modules && currentConfig.modules.roomsMod !== 'off'); + const cur = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : currentConfig.ux?.current === "rooms" ? "rooms" : "blocks"; + const uxCard = (value, title, image) => label({ class: "welcome-ux-option" }, + input({ type: "radio", name: "ux", value, ...(cur === value ? { checked: true } : {}) }), + img({ src: image, class: "welcome-ux-shot", alt: title }), + span({ class: "welcome-ux-label" }, title) + ); + return div({ class: "welcome-ux-grid" }, + uxCard("blocks", i18n.uxModeMenus || "Blocks", "/assets/images/ux-blocks.png"), + aiNavEnabled ? uxCard("ainav", i18n.uxModeAINav || "AI", "/assets/images/ux-ainav.png") : null, + chatsEnabled ? uxCard("chats", i18n.uxModeChats || "Conversations", "/assets/images/ux-chats.png") : null, + uxCard("feed", i18n.uxModeFeed || "Microblogging", "/assets/images/ux-feed.png"), + phoneEnabled ? uxCard("phone", i18n.phoneTitle || "Phone", "/assets/images/ux-phone.png") : null, + roomsEnabled ? uxCard("rooms", i18n.roomsTitle || "Rooms", "/assets/images/ux-rooms.png") : null + ); })(), - br(), br(), button({ type: "submit" }, i18n.saveSettings) ) ) ), - section( + isMobile ? skinPicker() : section({ id: "theme" }, + div({ class: "tags-header" }, + h2(i18n.theme), + p(i18n.themeIntro), + form( + { action: "/settings/theme", method: "post" }, + select({ name: "theme" }, ...themeElements), + br(), + br(), + button({ type: "submit" }, i18n.setTheme) + ) + ) + ), + workflowSection, + section({ id: "home-page" }, div({ class: "tags-header" }, h2(i18n.homePageTitle), p(i18n.homePageDescription), form( { action: "/settings/home-page", method: "POST" }, select({ name: "homePage" }, - option({ value: "activity", selected: currentConfig.homePage === "activity" ? true : undefined }, i18n.activityTitle), - option({ value: "ai", selected: currentConfig.homePage === "ai" ? true : undefined }, i18n.aiTitle), - option({ value: "trending", selected: currentConfig.homePage === "trending" ? true : undefined }, i18n.trendingTitle), - option({ value: "opinions", selected: currentConfig.homePage === "opinions" ? true : undefined }, i18n.opinionsTitle), - option({ value: "forum", selected: currentConfig.homePage === "forum" ? true : undefined }, i18n.forumTitle), - option({ value: "feed", selected: currentConfig.homePage === "feed" ? true : undefined }, i18n.feedTitle), - option({ value: "mentions", selected: currentConfig.homePage === "mentions" ? true : undefined }, i18n.mentions), - option({ value: "inbox", selected: currentConfig.homePage === "inbox" ? true : undefined }, i18n.inbox), - option({ value: "agenda", selected: currentConfig.homePage === "agenda" ? true : undefined }, i18n.agendaTitle), - option({ value: "favorites", selected: currentConfig.homePage === "favorites" ? true : undefined }, i18n.favoritesTitle), - option({ value: "stats", selected: currentConfig.homePage === "stats" ? true : undefined }, i18n.statsTitle), - option({ value: "blockexplorer", selected: currentConfig.homePage === "blockexplorer" ? true : undefined }, i18n.blockchain) + ...[ + { value: "activity", label: i18n.activityTitle }, + { value: "ai", label: i18n.aiTitle, mod: "ai" }, + { value: "trending", label: i18n.trendingTitle, mod: "trending" }, + { value: "forum", label: i18n.forumTitle, mod: "forum" }, + { value: "feed", label: i18n.feedTitle, mod: "feed" }, + { value: "chats", label: i18n.chatsTitle, mod: "chats" }, + { value: "inbox", label: i18n.inbox }, + { value: "mentions", label: i18n.mentions }, + { value: "agenda", label: i18n.agendaTitle, mod: "agenda" }, + { value: "market", label: i18n.marketTitle, mod: "market" }, + { value: "favorites", label: i18n.favoritesTitle, mod: "favorites" } + ].filter(o => !o.mod || modOn(o.mod)).map(o => currentConfig.homePage === o.value + ? option({ value: o.value, selected: true }, o.label) + : option({ value: o.value }, o.label)) ), br(), br(), button({ type: "submit" }, i18n.saveHomePage) ) ) ), - section( + modOn('ai') ? section({ id: "ai" }, + div({ class: "tags-header" }, + h2(i18n.aiTitle), + p(i18n.aiSettingsDescription), + form( + { action: "/settings/ai", method: "POST" }, + input({ + type: "text", + id: "ai_prompt", + name: "ai_prompt", + placeholder: aiPrompt, + value: aiPrompt, + maxlength: "128", + required: true + }), br(), + label({ for: "aiSuggestions", class: "lan-checkbox-label" }, + input({ + type: "checkbox", + id: "aiSuggestions", + name: "ai_suggestions", + value: "on", + class: "lan-checkbox-input", + checked: currentConfig.ai?.suggestions !== false ? true : undefined + }), + span({ class: "lan-checkbox-text" }, i18n.aiSuggestionsEnable) + ), + br(), + button({ type: "submit" }, i18n.saveSettings), + Number(aiExportCount) > 0 ? button({ type: "submit", formaction: "/ai/export", attrs: { formmethod: "GET" }, class: "ai-export-btn" }, `${i18n.aiExportFineTuning} (${aiExportCount})`) : null + ) + ) + ) : null, + section({ id: "logstream" }, div({ class: "tags-header" }, h2(i18n.ssbLogStream), p(i18n.ssbLogStreamDescription), @@ -171,7 +290,127 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) = ) ) ), - section( + section({ id: "lan" }, + div({ class: "tags-header" }, + h2(i18n.settingsLanTitle || 'LAN Broadcasting'), + p(i18n.settingsLanDesc || 'Periodically announce this peer to other Oasis instances on the same local network.'), + form( + { action: "/settings/lan-broadcasting", method: "POST" }, + label({ for: "lanBroadcasting", class: "lan-checkbox-label" }, + input({ + type: "checkbox", + id: "lanBroadcasting", + name: "lanBroadcasting", + value: "on", + class: "lan-checkbox-input", + checked: currentConfig.lanBroadcasting !== false ? true : undefined + }), + span({ class: "lan-checkbox-text" }, i18n.settingsLanEnable || 'Enable LAN broadcasting') + ), + br(), + button({ type: "submit" }, i18n.saveSettings) + ) + ) + ), + modOn('phone') ? section({ id: "phone" }, + div({ class: "tags-header" }, + h2(i18n.phoneTitle), + p(i18n.phoneSettingsDesc), + form( + { action: "/settings/phone", method: "POST" }, + select({ name: "visibility" }, + option({ value: "whole", ...(!currentPhone.dnd && currentPhone.visibility !== "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole), + option({ value: "mutuals", ...(!currentPhone.dnd && currentPhone.visibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals), + option({ value: "dnd", ...(currentPhone.dnd ? { selected: true } : {})}, i18n.phoneDnd) + ), br(), br(), + label(i18n.settingsPhoneVideo), br(), + select({ name: "video" }, + option({ value: "on", ...(currentPhone.video !== false ? { selected: true } : {}) }, i18n.settingsPhoneVideoOn), + option({ value: "off", ...(currentPhone.video === false ? { selected: true } : {}) }, i18n.settingsPhoneVideoOff) + ), br(), br(), + button({ type: "submit" }, i18n.saveSettings) + ) + ) + ) : null, + modOn('wallet') ? section( + { id: "wallet" }, + div({ class: "tags-header" }, + h2(i18n.wallet), + p( + isMobile ? i18n.walletSettingsMobileDescription : i18n.walletSettingsDescription + ), + isMobile ? a({ href: require('../backend/updater.js').ecoinWalletDownloadUrl(), class: "update-download-link" }, i18n.walletDownloadEcoin) : null, + form( + { action: "/settings/wallet", method: "POST" }, + label({ for: "wallet_url" }, i18n.walletRpcUrl), br(), + input({ type: "text", id: "wallet_url", name: "wallet_url", placeholder: "http://localhost:7474", value: walletUrl || "http://localhost:7474" }), br(), + label({ for: "wallet_user" }, i18n.walletUser), br(), + input({ type: "text", id: "wallet_user", name: "wallet_user", placeholder: "ecoinrpc", value: walletUser }), br(), + label({ for: "wallet_pass" }, i18n.walletPass), br(), + input({ type: "password", id: "wallet_pass", name: "wallet_pass" }), br(), + label({ for: "wallet_fee" }, i18n.walletFee), br(), + input({ type: "text", id: "wallet_fee", name: "wallet_fee", placeholder: "5", value: walletFee || "5" }), br(), + button({ type: "submit" }, i18n.walletConfiguration), + walletUser ? " " : null, + walletUser ? button({ type: "submit", class: "delete-btn", formaction: "/settings/wallet/disconnect", attrs: { formmethod: "POST" } }, i18n.walletDisconnectButton) : null + ) + ) + ) : null, + section({ id: "verification" }, + div({ class: "tags-header" }, + h2(i18n.verificationTitle), + p(i18n.verificationDescription), + form({ action: "/settings/verify", method: "post" }, button({ type: "submit" }, i18n.verificationRun)), + verification && verification.error ? p({ class: "backup-check-bad" }, verification.error) : renderVerificationReport(verification) + ) + ), + section({ id: "indexes" }, + div({ class: "tags-header" }, + h2(i18n.indexes), + p(i18n.indexesDescription), + rebuildButton, + renderRebuildReport(rebuild) + ) + ), + section({ id: "export" }, + div({ class: "tags-header" }, + h2(i18n.exportDataTitle), + p(i18n.exportDataDescription), + form( + { action: "/export/create", method: "POST", id: "exportForm" }, + button({ type: "submit" }, i18n.exportDataButton) + ) + ) + ), + section({ id: "panic" }, + div({ class: "tags-header" }, + h2(i18n.panicMode), + p(i18n.removeDataDescription), + form( + { action: "/panic/remove", method: "POST", id: "removeForm" }, + button({ type: "submit" }, i18n.removePanicButton) + ) + ) + ) + ]; + const contentParts = [ + section({ id: "wish" }, + div({ class: "tags-header" }, + h2(i18n.settingsWishTitle), + p(i18n.settingsWishDesc), + form( + { action: "/settings/wish", method: "POST" }, + select({ name: "wish" }, + option({ value: "whole", ...(currentWish === "whole" ? { selected: true } : {})}, i18n.settingsWishWhole), + option({ value: "mutuals", ...(currentWish === "mutuals" ? { selected: true } : {})}, i18n.settingsWishMutuals), + option({ value: "only-lan", ...(currentWish === "only-lan" ? { selected: true } : {})}, i18n.settingsWishOnlyLan || "Only LAN"), + option({ value: "local", ...(currentWish === "local" ? { selected: true } : {})}, i18n.settingsWishLocal || "Local") + ), br(), br(), + button({ type: "submit" }, i18n.saveSettings) + ) + ) + ), + section({ id: "replication" }, div({ class: "tags-header" }, h2(i18n.settingsReplicationTitle || 'Replication'), p(i18n.settingsReplicationDesc || 'Configure the number of hops your peer follows out from your own feed when replicating content.'), @@ -192,175 +431,248 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) = ) ) ), - section( + blobCache ? section({ id: "blobcache" }, div({ class: "tags-header" }, - h2(i18n.settingsLanTitle || 'LAN Broadcasting'), - p(i18n.settingsLanDesc || 'Periodically announce this peer to other Oasis instances on the same local network. Disable to stop UDP broadcasts.'), + h2(i18n.settingsBlobCacheTitle), + p(i18n.settingsBlobCacheDesc), + blobCache.usage ? p({ class: "blob-cache-usage" }, strong(`${i18n.settingsBlobCacheUsage}: `), `${sizeLabel(blobCache.usage.bytes)} · ${blobCache.usage.count} ${i18n.settingsBlobCacheFiles}`) : null, + blobCache.cleaned !== null ? p({ class: "blob-cache-notice" }, String(i18n.settingsBlobCacheCleaned).replace('{n}', String(blobCache.cleaned)).replace('{size}', sizeLabel(blobCache.freed))) : null, form( - { action: "/settings/lan-broadcasting", method: "POST" }, - label({ for: "lanBroadcasting", class: "lan-checkbox-label" }, - input({ - type: "checkbox", - id: "lanBroadcasting", - name: "lanBroadcasting", - value: "on", - class: "lan-checkbox-input", - checked: currentConfig.lanBroadcasting !== false ? true : undefined - }), - span({ class: "lan-checkbox-text" }, i18n.settingsLanEnable || 'Enable LAN broadcasting') - ), + { action: "/settings/blob-cache", method: "POST" }, + label({ for: "blob_cache_mb" }, i18n.settingsBlobCacheLimit), br(), - button({ type: "submit" }, i18n.saveSettings) + select({ id: "blob_cache_mb", name: "blob_cache_mb" }, ...BLOB_CACHE_OPTIONS.map(mb => option({ value: String(mb), ...(mb === blobCache.maxMB ? { selected: true } : {}) }, blobCacheOptionLabel(mb)))), + br(), br(), + button({ type: "submit" }, i18n.saveSettings), + blobCache.maxMB > 0 ? button({ type: "submit", formaction: "/settings/blob-cache/collect", class: "blob-cache-clean-btn" }, i18n.settingsBlobCacheCleanNow) : null ) ) - ), - section( - div({ class: "tags-header" }, - h2(i18n.settingsWishTitle), - p(i18n.settingsWishDesc), - form( - { action: "/settings/wish", method: "POST" }, - select({ name: "wish" }, - option({ value: "whole", selected: currentWish === "whole" ? true : undefined }, i18n.settingsWishWhole), - option({ value: "mutuals", selected: currentWish === "mutuals" ? true : undefined }, i18n.settingsWishMutuals), - option({ value: "only-lan", selected: currentWish === "only-lan" ? true : undefined }, i18n.settingsWishOnlyLan || "Only LAN") - ), br(), br(), - button({ type: "submit" }, i18n.saveSettings) - ) - ) - ), - section( + ) : null, + section({ id: "pm-visibility" }, div({ class: "tags-header" }, h2(i18n.settingsPmVisibilityTitle), p(i18n.settingsPmVisibilityDesc), form( { action: "/settings/pm-visibility", method: "POST" }, select({ name: "pmVisibility" }, - option({ value: "whole", selected: currentPmVisibility === "whole" ? true : undefined }, i18n.settingsPmVisibilityWhole), - option({ value: "mutuals", selected: currentPmVisibility === "mutuals" ? true : undefined }, i18n.settingsPmVisibilityMutuals) + option({ value: "whole", ...(currentPmVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole), + option({ value: "following", ...(currentPmVisibility === "following" ? { selected: true } : {})}, i18n.settingsPmVisibilityFollowing), + option({ value: "mutuals", ...(currentPmVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals) ), br(), br(), button({ type: "submit" }, i18n.saveSettings) ) ) ), - section( - { id: "wallet" }, + section({ id: "mentions-visibility" }, div({ class: "tags-header" }, - h2(i18n.wallet), - p( - i18n.walletSettingsDescription, " ", - a({ href: "docs/ecoin.md", target: "_blank", rel: "noopener" }, `[${i18n.walletSettingsDocLink}]`) - ), + h2(i18n.settingsMentionsVisibilityTitle), + p(i18n.settingsMentionsVisibilityDesc), form( - { action: "/settings/wallet", method: "POST" }, - label({ for: "wallet_url" }, i18n.walletAddress), br(), - input({ type: "text", id: "wallet_url", name: "wallet_url", placeholder: walletUrl, value: walletUrl }), br(), - label({ for: "wallet_user" }, i18n.walletUser), br(), - input({ type: "text", id: "wallet_user", name: "wallet_user", placeholder: walletUser, value: walletUser }), br(), - label({ for: "wallet_pass" }, i18n.walletPass), br(), - input({ type: "password", id: "wallet_pass", name: "wallet_pass" }), br(), - label({ for: "wallet_fee" }, i18n.walletFee), br(), - input({ type: "text", id: "wallet_fee", name: "wallet_fee", placeholder: walletFee, value: walletFee }), br(), - button({ type: "submit" }, i18n.walletConfiguration) + { action: "/settings/mentions-visibility", method: "POST" }, + select({ name: "mentionsVisibility" }, + option({ value: "whole", ...(currentMentionsVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole), + option({ value: "following", ...(currentMentionsVisibility === "following" ? { selected: true } : {})}, i18n.settingsPmVisibilityFollowing), + option({ value: "mutuals", ...(currentMentionsVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals) + ), br(), br(), + button({ type: "submit" }, i18n.saveSettings) ) ) ), - section( + modOn('inbox') ? section({ id: "inbox-bots" }, div({ class: "tags-header" }, - h2(i18n.pubIdTitle || "PUB Wallet"), - p(i18n.pubIdDescription || "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI)."), + h2(i18n.settingsInboxBotsTitle), + p(i18n.settingsInboxBotsDesc), form( - { action: "/settings/pub-id", method: "POST" }, - input({ - type: "text", - id: "pub_id", - name: "pub_id", - value: pubId, - placeholder: i18n.pubIdPlaceholder || "@example.ed25519" - }), br(), - button({ type: "submit" }, i18n.pubIdSave || "Save configuration") + { action: "/settings/inbox-bots", method: "POST" }, + ...INBOX_BOT_ORDER.map(bot => + label({ for: `inbox-bot-${bot}`, class: "lan-checkbox-label inbox-bot-label" }, + input({ + type: "checkbox", + id: `inbox-bot-${bot}`, + name: "bots", + value: bot, + class: "lan-checkbox-input", + checked: (Array.isArray(currentConfig.inboxMutedBots) && currentConfig.inboxMutedBots.includes(bot)) ? undefined : true + }), + span({ class: "lan-checkbox-text" }, inboxBotLabel(bot)) + ) + ), + br(), + button({ type: "submit" }, i18n.saveSettings) ) ) - ), - section( + ) : null, + section({ id: "hidden" }, div({ class: "tags-header" }, - h2(i18n.aiTitle), - p(i18n.aiSettingsDescription), - form( - { action: "/settings/ai", method: "POST" }, - input({ - type: "text", - id: "ai_prompt", - name: "ai_prompt", - placeholder: aiPrompt, - value: aiPrompt, - maxlength: "128", - required: true - }), br(), - button({ type: "submit" }, i18n.aiConfiguration) - ) + h2(i18n.settingsHiddenTitle), + p(i18n.settingsHiddenDesc), + p(strong(`${i18n.settingsMutedLabel} (${viewerHidden.muted.length})`)), + viewerHidden.muted.length + ? div({ class: "viewer-hidden-block" }, table({ class: "block-info-table" }, + tr( + td({ class: "card-label" }, i18n.inhabitantProfileTitle), + td({ class: "card-label" }, i18n.date), + td({ class: "card-label" }, "") + ), + ...viewerHidden.muted.map(m => tr( + td({ "data-label": i18n.inhabitantProfileTitle }, userLink(m.id)), + td({ "data-label": i18n.date }, hiddenAt(m.at)), + td({ "data-label": "" }, form({ action: `/unmute/${encodeURIComponent(m.id)}`, method: "POST" }, + input({ type: "hidden", name: "returnTo", value: "/settings?filter=content#hidden" }), + button({ type: "submit", class: "filter-btn" }, i18n.unmute))) + )) + )) + : p({ class: "muted" }, i18n.settingsMutedEmpty), + p(strong(`${i18n.settingsHiddenLabel} (${viewerHidden.hidden.length})`)), + viewerHidden.hidden.length + ? div({ class: "viewer-hidden-block" }, table({ class: "block-info-table" }, + tr( + td({ class: "card-label" }, i18n.settingsTabContent), + td({ class: "card-label" }, i18n.date), + td({ class: "card-label" }, "") + ), + ...viewerHidden.hidden.map(h => tr( + td({ "data-label": i18n.settingsTabContent }, h.href ? a({ href: h.href }, plainLabel(h.label) || h.id) : span(plainLabel(h.label) || h.id)), + td({ "data-label": i18n.date }, hiddenAt(h.at)), + td({ "data-label": "" }, form({ action: `/unhide/${encodeURIComponent(h.id)}`, method: "POST" }, + input({ type: "hidden", name: "returnTo", value: "/settings?filter=content#hidden" }), + button({ type: "submit", class: "filter-btn" }, i18n.unhideContent))) + )) + )) + : p({ class: "muted" }, i18n.settingsHiddenEmpty) ) ), - section( - { id: "fediverse" }, + ]; + const multiverseParts = modOn('fediverse') ? [ + section({ id: "mastodon" }, div({ class: "tags-header" }, - h2(i18n.fediverseSettingsTitle), - div({ class: "fediverse-network" }, - h3("Mastodon"), - p(i18n.fediverseTokenHelp), - fediverseError ? p({ class: "fediverse-error" }, i18n[fediverseError] || i18n.fediverseError) : "", - fediverseAccount - ? (() => { - const host = String(fediverseAccount.instance || "").replace(/^https?:\/\//, ""); - const profileUrl = `${fediverseAccount.instance}/@${fediverseAccount.acct}`; - const link = (txt) => a({ href: profileUrl, target: "_blank", rel: "noopener noreferrer" }, txt); - return form( - { action: "/settings/fediverse/disconnect", method: "POST" }, - p( - `${i18n.fediverseConnectedAs}: `, - link(`${fediverseAccount.acct}@${host}`) - ), - button({ type: "submit" }, i18n.fediverseDisconnect) - ); - })() - : form( - { action: "/settings/fediverse", method: "POST" }, - label({ for: "fediverse_instance" }, i18n.fediverseInstanceLabel), br(), - input({ type: "text", id: "fediverse_instance", name: "instance", placeholder: "mastodon.social", required: true }), br(), - label({ for: "fediverse_token" }, i18n.fediverseTokenLabel), br(), - input({ type: "password", id: "fediverse_token", name: "token", autocomplete: "off", required: true }), br(), - button({ type: "submit" }, i18n.fediverseConnect) + h2("Mastodon"), + fediverseAccount + ? (() => { + const host = String(fediverseAccount.instance || "").replace(/^https?:\/\//, ""); + const profileUrl = `${fediverseAccount.instance}/@${fediverseAccount.acct}`; + const link = (txt) => a({ href: profileUrl, target: "_blank", rel: "noopener noreferrer" }, txt); + return form( + { action: "/settings/fediverse/disconnect", method: "POST" }, + p( + `${i18n.fediverseConnectedAs}: `, + link(`${fediverseAccount.acct}@${host}`) + ), + button({ type: "submit" }, i18n.fediverseDisconnect) + ); + })() + : form( + { action: "/settings/fediverse", method: "POST" }, + label({ for: "fediverse_instance" }, i18n.fediverseInstanceLabel), br(), + input({ type: "text", id: "fediverse_instance", name: "instance", placeholder: "mastodon.social", required: true }), br(), + label({ for: "fediverse_token" }, i18n.fediverseTokenLabel), br(), + input({ type: "password", id: "fediverse_token", name: "token", autocomplete: "off", required: true }), br(), + button({ type: "submit" }, i18n.fediverseConnect) + ) + ) + ), + section({ id: "telegram" }, + div({ class: "tags-header" }, + h2("Telegram"), + telegramAccount + ? form( + { action: "/settings/telegram/disconnect", method: "POST" }, + p(`${i18n.fediverseConnectedAs}: `, strong(telegramAccount.displayName || ""), telegramAccount.username ? span(` (@${telegramAccount.username})`) : ""), + br(), + button({ type: "submit" }, i18n.fediverseDisconnect) + ) + : telegramLogin && telegramLogin.step === "code" + ? form( + { action: "/settings/telegram/code", method: "POST" }, + br(), + p(i18n.telegramCodeHelp), + input({ type: "text", id: "telegram_code", name: "code", autocomplete: "off", inputmode: "numeric", placeholder: "12345", required: true }), br(), br(), + button({ type: "submit" }, i18n.telegramVerify), + " ", + button({ type: "submit", formaction: "/settings/telegram/cancel", class: "delete-btn" }, i18n.telegramCancel) ) - ) + : telegramLogin && telegramLogin.step === "password" + ? form( + { action: "/settings/telegram/password", method: "POST" }, + br(), + p(i18n.telegramPasswordHelp), + input({ type: "password", id: "telegram_password", name: "password", autocomplete: "off", required: true }), br(), br(), + button({ type: "submit" }, i18n.telegramVerify), + " ", + button({ type: "submit", formaction: "/settings/telegram/cancel", class: "delete-btn" }, i18n.telegramCancel) + ) + : form( + { action: "/settings/telegram/start", method: "POST" }, + label({ for: "telegram_api_id" }, i18n.telegramApiIdLabel), br(), + input({ type: "text", id: "telegram_api_id", name: "apiId", inputmode: "numeric", placeholder: "12345678", required: true }), br(), + label({ for: "telegram_api_hash" }, i18n.fediverseTokenLabel), br(), + input({ type: "password", id: "telegram_api_hash", name: "apiHash", autocomplete: "off", placeholder: "0123456789abcdef0123456789abcdef", required: true }), br(), + label({ for: "telegram_phone" }, i18n.telegramPhoneLabel), br(), + input({ type: "tel", id: "telegram_phone", name: "phone", placeholder: "+34 60000000", required: true }), br(), br(), + button({ type: "submit" }, i18n.fediverseConnect) + ) ) ), - section( + section({ id: "peertube" }, div({ class: "tags-header" }, - h2(i18n.indexes), - p(i18n.indexesDescription), - rebuildButton - ) - ), - section( - div({ class: "tags-header" }, - h2(i18n.exportDataTitle), - p(i18n.exportDataDescription), - form( - { action: "/export/create", method: "POST", id: "exportForm" }, - button({ type: "submit" }, i18n.exportDataButton) - ) - ) - ), - section( - div({ class: "tags-header" }, - h2(i18n.panicMode), - p(i18n.removeDataDescription), - form( - { action: "/panic/remove", method: "POST", id: "removeForm" }, - button({ type: "submit" }, i18n.removePanicButton) - ) + h2("PeerTube"), + peertubeAccount + ? form( + { action: "/settings/peertube/disconnect", method: "POST" }, + p(`${i18n.fediverseConnectedAs}: `, a({ href: peertubeAccount.channelUrl || peertubeAccount.profileUrl, target: "_blank", rel: "noopener noreferrer" }, peertubeAccount.handle)), + button({ type: "submit" }, i18n.fediverseDisconnect) + ) + : form( + { action: "/settings/peertube", method: "POST" }, + label({ for: "peertube_instance" }, i18n.fediverseInstanceLabel), br(), + input({ type: "text", id: "peertube_instance", name: "instance", placeholder: "peertube.example.org", required: true }), br(), + label({ for: "peertube_username" }, i18n.peertubeUsernameLabel), br(), + input({ type: "text", id: "peertube_username", name: "username", autocomplete: "off", required: true }), br(), + label({ for: "peertube_password" }, i18n.peertubePasswordLabel), br(), + input({ type: "password", id: "peertube_password", name: "password", autocomplete: "off", required: true }), br(), + button({ type: "submit" }, i18n.fediverseConnect) + ) ) ) + , + section({ id: "nextcloud" }, + div({ class: "tags-header" }, + h2("Nextcloud"), + nextcloudAccount + ? form( + { action: "/settings/nextcloud/disconnect", method: "POST" }, + p(`${i18n.fediverseConnectedAs}: `, a({ href: nextcloudAccount.server, target: "_blank", rel: "noopener noreferrer" }, `${nextcloudAccount.user}@${String(nextcloudAccount.server).replace(/^https?:\/\//, "")}`), ` (${nextcloudAccount.folder})`), + button({ type: "submit" }, i18n.fediverseDisconnect) + ) + : form( + { action: "/settings/nextcloud", method: "POST" }, + label({ for: "nextcloud_server" }, i18n.nextcloudServerLabel), br(), + input({ type: "text", id: "nextcloud_server", name: "server", placeholder: "cloud.example.org", required: true }), br(), + label({ for: "nextcloud_user" }, i18n.nextcloudUserLabel), br(), + input({ type: "text", id: "nextcloud_user", name: "user", autocomplete: "off", required: true }), br(), + label({ for: "nextcloud_password" }, i18n.peertubePasswordLabel), br(), + input({ type: "password", id: "nextcloud_password", name: "password", autocomplete: "off", required: true }), br(), + label({ for: "nextcloud_folder" }, i18n.nextcloudFolderLabel), br(), + input({ type: "text", id: "nextcloud_folder", name: "folder", placeholder: "Oasis" }), br(), + button({ type: "submit" }, i18n.fediverseConnect) + ) + ) + ) + ] : []; + return template( + i18n.settings, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.settings), + p(i18n.settingsDescription) + ), + div({ class: "mode-buttons-row" }, + a({ class: filter === 'oasis' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=oasis' }, String(i18n.linkBoxOasis).toUpperCase()), + a({ class: filter === 'content' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=content' }, String(i18n.settingsTabContent).toUpperCase()), + modOn('fediverse') ? a({ class: filter === 'multiverse' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=multiverse' }, String(i18n.multiverse).toUpperCase()) : null + ) + ), + ...(filter === 'multiverse' ? multiverseParts : filter === 'content' ? contentParts : (isMobile ? mobileUi.settingsOrder(oasisParts) : oasisParts)) ); }; diff --git a/src/views/shops_view.js b/src/views/shops_view.js index 7f50ab0..ce324ef 100644 --- a/src/views/shops_view.js +++ b/src/views/shops_view.js @@ -1,15 +1,19 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, progress, video, table, tr, td } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderOpinionsVoting, renderInviteQrCard } = require("./main_views") +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, progress, video, audio, table, tr, td, hr } = require("../server/node_modules/hyperaxe") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderCallButton, clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderOpinionsVoting, renderEngagement, renderInviteQrCard , renderSpreadEditWarning, renderContentActions, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") const opinionCategories = require("../backend/opinion_categories") -const { renderReachChip, renderClearnetUrlBlock, renderClearnetPage, renderClearnetSearchForm, renderEncryptedChip, blobUrl: cnBlobUrl, escapeHtml: cnEscapeHtml } = require("./clearnet_view") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderClearnetUrlBlock, renderClearnetPage, renderClearnetSearchForm, renderEncryptedChip, blobUrl: cnBlobUrl, escapeHtml: cnEscapeHtml, renderRichText: cnRichText, renderKindTag: cnKindTag } = require("./clearnet_view") const userId = config.keys.id const safeArr = (v) => (Array.isArray(v) ? v : []) const safeText = (v) => String(v || "").trim() +const clearnetEligible = (shop) => String(shop.visibility || "").toUpperCase() !== "CLOSED" && !shop.encrypted +const SHOP_REACH = ["OPEN", "CLOSED"] +const shopReachLabel = (v) => v === "CLOSED" ? i18n.shopClosed : i18n.shopOpen const voteSum = (opinions = {}) => Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0) const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((s, c) => s + (Number((opinions || {})[c]) || 0), 0) const renderStarRating = (opinions, voterCount) => { @@ -25,11 +29,13 @@ const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) + const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}`, preload: 'metadata' }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) + if (mImg) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } @@ -43,35 +49,33 @@ const buildReturnTo = (filter, params = {}) => { return `/shops?${parts.join("&")}` } -const renderModeButtons = (currentFilter) => +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => div({ class: "tribe-mode-buttons" }, - ["all", "recent", "mine", "top", "products", "prices", "favorites"].map(f => + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "top", "products", "prices", "favorites"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => form({ method: "GET", action: "/shops" }, input({ type: "hidden", name: "filter", value: f }), button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`shopFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) ) ), - form({ method: "GET", action: "/shops/purchases" }, + ...((currentFilter === "purchases" || !modesAvail || modesAvail.purchases !== false) ? [form({ method: "GET", action: "/shops/purchases" }, button({ type: "submit", class: currentFilter === "purchases" ? "filter-btn active" : "filter-btn" }, (i18n.shopPurchasesButton || "Purchases").toUpperCase()) - ), + )] : []), + ]), form({ method: "GET", action: "/shops" }, input({ type: "hidden", name: "filter", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.shopUpload) ) ) -const renderFavoriteToggle = (shop, returnTo) => - form( - { method: "POST", action: shop.isFavorite ? `/shops/favorites/remove/${encodeURIComponent(shop.key)}` : `/shops/favorites/add/${encodeURIComponent(shop.key)}` }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "filter-btn" }, shop.isFavorite ? i18n.shopRemoveFavorite : i18n.shopAddFavorite) - ) - const renderShopCard = exports.renderShopCard = (shop, filter, params = {}) => { const returnTo = buildReturnTo(filter, params) const isAuthor = String(shop.author) === String(userId) return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(shop.key, `/shops/${encodeURIComponent(shop.key)}`, { spread: params.spreadMap && params.spreadMap.get(shop.key) || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title, returnTo, deleteAction: isAuthor ? contentDeleteAction('shop', shop.key) : undefined }) + ), div({ class: "tribe-card-image-wrapper" }, a({ href: `/shops/${encodeURIComponent(shop.key)}` }, renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-card-hero-image' }) @@ -85,35 +89,36 @@ const renderShopCard = exports.renderShopCard = (shop, filter, params = {}) => { shop.visibility === "CLOSED" ? renderStateChip("closed", "✗", i18n.shopClosed) : renderStateChip("mutuals", "✓", i18n.shopOpen), - renderLifespanChip(shop.lifetime, i18n) + renderLifespanChip(shop.lifetime, i18n), + shop.clearnet === true && clearnetEligible(shop) ? renderReachChip(true, i18n, clearnetItemHref('shops', shop.title, shop.key)) : null, + shop.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (shop.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.shopProducts}: ${shop.productCount || 0}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(shop.key, params.spreadMap && params.spreadMap.get(shop.key))), - div({ class: "card-visit-btn-centered" }, - form({ method: 'GET', action: `/shops/${encodeURIComponent(shop.key)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewShop || i18n.shopVisitShop || 'View Shop') - ) ) ) ) } -const renderProductCard = (product, shopId, returnTo) => { +const renderProductCard = (product, shopId, returnTo, params = {}) => { const isAuthor = String(product.author) === String(userId) const stock = Number(product.stock) || 0 const voterCount = safeArr(product.opinions_inhabitants).length const productUrl = `/shops/product/${encodeURIComponent(product.key)}?shopId=${encodeURIComponent(shopId)}` return div({ class: "shop-product-card" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(product.key, `/shops/product/${encodeURIComponent(product.key)}`, { spread: params.spreadMap && params.spreadMap.get(product.key) || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, reportTitle: product.title, returnTo, deleteAction: isAuthor ? contentDeleteAction('shopProduct', product.key) : undefined }) + ), product.image ? div({ class: "shop-product-media" }, a({ href: productUrl }, renderMediaBlob(product.image))) : null, div({ class: "shop-product-body" }, product.shopTitle ? p(a({ href: `/shops/${encodeURIComponent(shopId)}`, class: "user-link" }, product.shopTitle)) : null, h2(a({ href: productUrl }, product.title || i18n.shopProductUntitled)), renderStarRating(product.opinions, voterCount), - product.description ? p(...renderUrl(product.description)) : null, - div({ class: "shop-product-price" }, `${Number(product.price || 0).toFixed(6)} ECO`), + product.description ? p(...renderStyledText(product.description)) : null, + div({ class: "price-chip" }, `${Number(product.price || 0).toFixed(6)} ECO`), div({ class: "confirmations-block stock-block" }, div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopProductStock}: `), @@ -126,9 +131,6 @@ const renderProductCard = (product, shopId, returnTo) => { if (!isAuthor && stock > 0) { actions.push(a({ href: productUrl, class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy)); } - actions.push(form({ method: "POST", action: product.isFavorite ? `/shops/favorites/remove/${encodeURIComponent(product.key)}` : `/shops/favorites/add/${encodeURIComponent(product.key)}` }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "filter-btn" }, product.isFavorite ? i18n.shopRemoveFavorite : i18n.shopAddFavorite))); return actions.length ? div({ class: "shop-product-actions" }, ...actions) : null; })() ) @@ -136,55 +138,48 @@ const renderProductCard = (product, shopId, returnTo) => { } const renderCommentsSection = (parentId, returnTo, comments = []) => { - const count = Array.isArray(comments) ? comments.length : 0 - return div({ class: "vote-comments-section market-comments" }, - div({ class: "comments-count" }, span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), span({ class: "card-value" }, String(count))), - div({ class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form({ method: "POST", action: `/shops/${encodeURIComponent(parentId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - count - ? div({ class: "comments-list" }, - comments.map(c => { - const author = c.value?.author || "" - const ts = c.value?.timestamp || c.timestamp - const text = c.value?.content?.text || "" - const rootId = c.value?.content?.fork || c.value?.content?.root || null - return div({ class: "votations-comment-card" }, - span({ class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - ts ? span(" | ", span({ class: "votations-comment-date" }, moment(ts).format("YYYY/MM/DD HH:mm:ss"))) : "", - ts && rootId ? span(" | ", a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, moment(ts).fromNow())) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ) - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ) -} + return renderSharedCommentsSection({ + action: `/shops/${encodeURIComponent(parentId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; const renderShopForm = (filter, shop = {}, params = {}) => { const isEdit = filter === "edit" const returnTo = safeText(params.returnTo) || buildReturnTo("all") + const reachRaw = String(params.reach || "").toUpperCase() + const ownReach = String(shop.visibility || "").toUpperCase() + const reach = SHOP_REACH.includes(reachRaw) ? reachRaw : (SHOP_REACH.includes(ownReach) ? ownReach : "OPEN") + const showClearnet = isEdit ? clearnetEligible(shop) : reach === "OPEN" && clearnetEligible({ ...shop, visibility: reach }) return div({ class: "create-tribe-form" }, h2(isEdit ? i18n.shopUpdateSectionTitle : i18n.shopCreateSectionTitle), + isEdit ? (params.spreadWarning || null) : null, + ...(isEdit ? [] : [ + form({ method: "GET", action: "/shops" }, + input({ type: "hidden", name: "filter", value: "create" }), + label(i18n.shopVisibility), br(), + div({ class: "apply-row" }, + select({ name: "visibility", class: "report-category-select" }, + option({ value: "OPEN", ...(reach === "OPEN" ? { selected: true } : {}) }, i18n.shopOpen), + option({ value: "CLOSED", ...(reach === "CLOSED" ? { selected: true } : {}) }, i18n.shopClosed) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, shopReachLabel(reach)) + ]), form({ action: isEdit ? `/shops/update/${encodeURIComponent(shop.key || "")}` : "/shops/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: returnTo }), + isEdit ? null : input({ type: "hidden", name: "visibility", value: reach }), label(i18n.title || "Title"), br, - input({ type: "text", name: "title", required: true, placeholder: i18n.shopTitlePlaceholder || "Name of your shop", value: shop.title || "" }), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.shopTitlePlaceholder || "Name of your shop", value: shop.title || "" }), br(), label(i18n.shopShortDescription), br, input({ type: "text", name: "shortDescription", required: true, maxlength: 160, placeholder: i18n.shopShortDescriptionPlaceholder || "Brief description of your shop", value: shop.shortDescription || "" }), br(), label(i18n.description || "Description"), br, - textarea({ name: "description", rows: 4, placeholder: i18n.shopDescriptionPlaceholder || "Detailed description of your shop" }, shop.description || ""), br, - label(i18n.blogImage || "Upload media (max-size: 50MB)"), br, + textarea({ maxlength: "5000", name: "description", rows: 4, placeholder: i18n.shopDescriptionPlaceholder || "Detailed description of your shop" }, shop.description || ""), br, + label(i18n.blogImage || "Upload media (max-size: 75MB)"), br, input({ type: "file", name: "image", accept: "image/*,video/*" }), br(), br(), label(i18n.shopUrl), br, input({ type: "text", name: "url", placeholder: "https://", value: shop.url || "" }), br, @@ -193,35 +188,30 @@ const renderShopForm = (filter, shop = {}, params = {}) => { label(i18n.mapLocationTitle || "Map Location"), br, input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: shop.mapUrl || "" }), br, label(i18n.shopTags), br, - input({ type: "text", name: "tags", placeholder: i18n.shopTagsPlaceholder || "tag1, tag2, tag3", value: safeArr(shop.tags).join(", ") }), br, - isEdit ? null : label(i18n.shopVisibility), - isEdit ? null : br, - isEdit ? null : select({ name: "visibility" }, - option({ value: "OPEN", selected: (shop.visibility || "OPEN") === "OPEN" }, i18n.shopOpen), - option({ value: "CLOSED", selected: shop.visibility === "CLOSED" }, i18n.shopClosed) - ), - isEdit ? null : br(), + input({ type: "text", name: "tags", placeholder: i18n.shopTagsPlaceholder || "Enter tags separated by commas", value: Array.isArray(shop.tags) ? shop.tags.join(", ") : (shop.tags || "") }), br, + showClearnet ? renderClearnetSelector(shop.clearnet === true || String(shop.clearnet || "") === "1", i18n) : null, br(), button({ type: "submit" }, isEdit ? i18n.shopUpdate : i18n.shopCreate) ) ) } -const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "") => { +const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "", spreadWarning = null) => { return div({ class: "create-tribe-form" }, h2(isEdit ? i18n.shopProductUpdate : i18n.shopProductAdd), + isEdit ? spreadWarning : null, form({ action: isEdit ? `/shops/product/update/${encodeURIComponent(product.key || "")}` : "/shops/product/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "shopId", value: shopId }), input({ type: "hidden", name: "returnTo", value: returnTo }), label(i18n.title || "Title"), br, - input({ type: "text", name: "title", required: true, value: product.title || "" }), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, value: product.title || "" }), br(), label(i18n.description || "Description"), br, - textarea({ name: "description", rows: 4 }, product.description || ""), br, + textarea({ maxlength: "5000", name: "description", rows: 4 }, product.description || ""), br, label(i18n.shopProductPrice), br, - input({ type: "number", name: "price", step: "0.000001", min: "0.000001", required: true, value: product.price || "" }), br(), br(), + input({ type: "number", name: "price", step: "0.000001", min: "0.000001", required: true, value: product.price || "" }), renderEcoValueChip(), br(), br(), label(i18n.shopProductStock), br, input({ type: "number", name: "stock", min: "0", value: product.stock !== undefined ? product.stock : 1 }), br(), br(), - label(i18n.blogImage || "Upload media (max-size: 50MB)"), br, + label(i18n.blogImage || "Upload media (max-size: 75MB)"), br, input({ type: "file", name: "image", accept: "image/*,video/*" }), br(), br(), input({ type: "hidden", name: "featured", value: "0" }), label(i18n.shopProductFeatured), @@ -241,66 +231,49 @@ const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "") } exports.shopsView = async (shops, filter, shopToEdit = null, params = {}) => { + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(shopToEdit && (shopToEdit.key || shopToEdit.id)) }; const q = safeText(params.q || "") const sort = safeText(params.sort || "recent") const list = safeArr(shops) - - const title = - filter === "mine" ? i18n.shopMineSectionTitle : - filter === "recent" ? i18n.shopRecentSectionTitle : - filter === "top" ? i18n.shopTopSectionTitle : - filter === "products" ? i18n.shopProductsSectionTitle : - filter === "prices" ? (i18n.shopPricesSectionTitle || "Products by Price") : - filter === "favorites" ? i18n.shopFavoritesSectionTitle : - filter === "create" ? i18n.shopCreateSectionTitle : - filter === "edit" ? i18n.shopUpdateSectionTitle : - i18n.shopAllSectionTitle - + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q) + const title = i18n.shopsTitle const isForm = filter === "create" || filter === "edit" - - const viewerClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetShops) - const header = [ - div({ class: "tags-header" }, - h2(title), - p(i18n.shopDescription) - ), - div({ class: "shop-title-row" }, renderReachChip(viewerClearnet, i18n)), - br() - ] - - const searchBar = div({ class: "filters" }, - form({ method: "GET", action: "/shops" }, - input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", placeholder: i18n.shopSearchPlaceholder, value: q }), - br(), - button({ type: "submit" }, i18n.search), - br() - ) - ) - - const sortedProducts = filter === "products" - ? [...list].sort((a, b) => safeArr(b.opinions_inhabitants).length - safeArr(a.opinions_inhabitants).length) - : filter === "prices" - ? [...list].sort((a, b) => Number(b.price || 0) - Number(a.price || 0)) - : list + const isProducts = filter === "products" || filter === "prices" return template( title, - section(...header), - section(renderModeButtons(filter)), - !isForm ? section(searchBar) : null, + section(div({ class: "tags-header module-header-line" }, h2(title), p(i18n.shopDescription), renderWalletChip())), + section(renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null)), + !isForm && !emptyMod + ? section( + div({ class: "filters activity-filter-chips activity-toolbar-row" }, + isProducts ? renderModuleStats(list.length) : renderModuleStatsBy(list, s => String(s.visibility || '').toUpperCase(), [{ value: 'OPEN', label: i18n.shopOpen }, { value: 'CLOSED', label: i18n.shopClosed }]), + form({ method: "GET", action: "/shops", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.shopSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + select({ name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {}) }, i18n.documentSortRecent), + option({ value: "top", ...(sort === "top" ? { selected: true } : {}) }, i18n.documentSortTop) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + ) + : null, section( isForm - ? renderShopForm(filter, filter === "edit" ? (shopToEdit || {}) : {}, params) - : filter === "products" || filter === "prices" + ? renderShopForm(filter, filter === "edit" ? (shopToEdit || {}) : (params.draft || {}), params) + : isProducts ? div({ class: "shop-products-grid" }, - sortedProducts.length - ? sortedProducts.map(prod => renderProductCard(prod, prod.shopId, buildReturnTo(filter, params))) - : p(i18n.shopNoItems) + list.length + ? paged(list).map(prod => renderProductCard(prod, prod.shopId, buildReturnTo(filter, { q }), params)) + : p(i18n.shopNoProducts) ) : div({ class: "tribe-grid" }, list.length - ? list.map(shop => renderShopCard(shop, filter, { q, sort, spreadMap: params.spreadMap })) + ? paged(list).map(shopItem => renderShopCard(shopItem, filter, params)) : p(i18n.shopNoItems) ) ) @@ -309,13 +282,16 @@ exports.shopsView = async (shops, filter, shopToEdit = null, params = {}) => { exports.singleShopView = async (shop, filter, products = [], comments = [], params = {}) => { const q = safeText(params.q || "") - const sort = safeText(params.sort || "recent") - const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }) + const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q }) const isAuthor = String(shop.author) === String(userId) - const fullShareUrl = `/shops/${encodeURIComponent(shop.key)}` + const canClearnet = clearnetEligible(shop) + const isClearnet = canClearnet && !!shop.clearnet + products = safeArr(products) - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetShops && shop.visibility !== 'CLOSED'); const shopSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(shop.key, `/shops/${encodeURIComponent(shop.key)}`, { spread: params.spreads || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title, deleteAction: isAuthor ? contentDeleteAction('shop', shop.key) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, shop.title || i18n.shopUntitled) ), @@ -325,26 +301,24 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para : renderStateChip("mutuals", "✓", i18n.shopOpen), shop.encrypted ? renderStateChip("encrypted", "🔒", i18n.encryptedChipLabel || "E2E") : null, renderLifespanChip(shop.lifetime, i18n), - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('shops', shop.title, shop.key) : null), + isAuthor && canClearnet ? renderClearnetSwitch('shops', shop.rootId || shop.key, isClearnet) : null, + shop.subscription + ? ((isAuthor || shop.subscription.subscribed === true) + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : renderStateChip("closed", "✉", i18n.subscriptionOff)) + : null ), - div({ class: "card-spread-centered" }, renderSpreadButton(shop.key, params.spreads)), renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), - shop.description ? p({ class: "tribe-side-description" }, ...renderUrl(shop.description)) : null, - div({ class: "shop-share" }, - span({ class: "tribe-info-label" }, `${i18n.shopShareUrl}: `), - input({ type: "text", value: fullShareUrl, readonly: true, class: "shop-share-input" }) - ), + shop.description ? p({ class: "tribe-side-description" }, ...renderStyledText(shop.description)) : null, table({ class: "tribe-info-table" }, tr( td({ class: "tribe-info-label" }, i18n.shopCreatedAt || "CREATED"), - td({ class: "tribe-info-value", colspan: "3" }, new Date(shop.createdAt).toLocaleString()) - ), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(shop.author)) + td({ class: "tribe-info-value", colspan: "3" }, moment(shop.createdAt).format("YYYY/MM/DD HH:mm")) ), shop.location ? tr( td({ class: "tribe-info-label" }, i18n.shopLocation), - td({ class: "tribe-info-value", colspan: "3" }, ...renderUrl(shop.location)) + td({ class: "tribe-info-value", colspan: "3" }, ...renderStyledText(shop.location)) ) : null, tr( td({ class: "tribe-info-label" }, i18n.shopStatus || "STATUS"), @@ -352,37 +326,16 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para ), shop.url ? tr( td({ class: "tribe-info-label" }, i18n.shopUrl), - td({ class: "tribe-info-value", colspan: "3" }, ...renderUrl(shop.url)) - ) : null + td({ class: "tribe-info-value", colspan: "3" }, ...renderStyledText(shop.url)) + ) : null, + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(shop.author)) + ) ), renderMapEmbed(params.mapData, shop.mapUrl), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.shopProducts}: ${shop.productCount || 0}`) ), - div({ class: "tribe-side-actions" }, - renderFavoriteToggle(shop, returnTo), - shop.author && String(shop.author) !== String(userId) - ? form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: shop.author }), button({ type: "submit", class: "tribe-action-btn" }, i18n.privateMessage)) - : null, - isAuthor - ? form({ method: "GET", action: `/shops/edit/${encodeURIComponent(shop.key)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.shopUpdate) - ) - : null, - isAuthor - ? form({ method: "GET", action: `/shops/${encodeURIComponent(shop.key)}/orders` }, - button({ type: "submit", class: "tribe-action-btn" }, - i18n.shopOrdersTitle || "Orders", - Number(shop.pendingOrders || 0) > 0 ? span({ class: "shop-orders-badge", title: i18n.shopOrdersPending || "Pending orders" }, ` ● ${shop.pendingOrders}`) : null - ) - ) - : null, - isAuthor - ? form({ method: "POST", action: `/shops/delete/${encodeURIComponent(shop.key)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.shopDelete) - ) - : null - ), isAuthor ? div({ class: "tribe-side-actions shop-visibility-row" }, span({ class: "card-label" }, `${i18n.visibilityLabel || "Visibility"}: `), @@ -416,6 +369,34 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para shop.encrypted ? i18n.shopMakePublic : i18n.shopMakePrivate)) ) : null, + shop.subscription + ? renderSubscriptionBox({ + target: shop.rootId || shop.key, + scope: "shops", + subscribed: shop.subscription.subscribed === true, + count: shop.subscription.count, + isOwner: isAuthor, + canWrite: isAuthor, + returnTo + }) + : null, + isAuthor + ? div({ class: "tribe-side-actions" }, + form({ method: "GET", action: `/shops/${encodeURIComponent(shop.key)}/orders` }, + button({ type: "submit", class: "tribe-action-btn" }, + i18n.shopOrdersTitle || "Orders", + Number(shop.pendingOrders || 0) > 0 ? span({ class: "shop-orders-badge", title: i18n.shopOrdersPending || "Pending orders" }, ` ● ${shop.pendingOrders}`) : null + ) + ) + ) + : null, + isAuthor + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: `/shops/edit/${encodeURIComponent(shop.key)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.shopUpdate) + ) + ) + : null, safeArr(shop.tags).length ? div({ class: "tribe-side-tags" }, safeArr(shop.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) : null @@ -429,14 +410,15 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para : null, div({ class: "shop-products-grid" }, products.length - ? products.map(prod => renderProductCard(prod, shop.rootId || shop.key, returnTo)) + ? paged(products).map(prod => renderProductCard(prod, shop.rootId || shop.key, `/shops/${encodeURIComponent(shop.key)}`, params)) : p(i18n.shopNoProducts) ) ) return template( shop.title || i18n.shopTitle, - section(renderModeButtons(filter)), + section(div({ class: "tags-header module-header-line" }, h2(i18n.shopsTitle), p(i18n.shopDescription), renderWalletChip())), + section(renderModeButtons(filter, false, (params && params.modesAvail) || null)), section( div({ class: "tribe-details" }, shopSide, @@ -451,89 +433,103 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) => const isAuthor = String(product.author) === String(userId) const stock = Number(product.stock) || 0 - return template( - product.title || i18n.shopProductTitle, - section(renderModeButtons("products")), - section( - div({ class: "shop-detail" }, - div({ class: "bookmark-topbar transfer-topbar-single" }, - div({ class: "bookmark-topbar-left" }, - product.author && String(product.author) !== String(userId) - ? form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: product.author }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)) - : null, - form({ method: "GET", action: `/shops/${encodeURIComponent(product.shopId)}` }, - button({ type: "submit", class: "filter-btn" }, `← ${i18n.shopBackToShop}`)) - ), - isAuthor - ? div({ class: "bookmark-actions transfer-actions" }, - form({ method: "GET", action: `/shops/product/edit/${encodeURIComponent(product.key)}` }, - input({ type: "hidden", name: "shopId", value: product.shopId }), - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.shopUpdate) - ), - form({ method: "POST", action: `/shops/product/delete/${encodeURIComponent(product.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.shopDelete) - ) - ) - : null - ), - product.image ? div({ class: "shop-detail-media" }, renderMediaBlob(product.image)) : null, - h2(product.title), - renderStarRating(product.opinions, safeArr(product.opinions_inhabitants).length), - product.description ? div({ class: "shop-detail-desc" }, ...renderUrl(product.description)) : null, - div({ class: "shop-product-price" }, `${Number(product.price || 0).toFixed(6)} ECO`), - div({ class: "confirmations-block stock-block" }, - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.shopProductStock}: `), - span({ class: "card-value" }, stock > 0 ? String(stock) : i18n.shopOutOfStock) - ), - progress({ class: "confirmations-progress stock-progress", value: Math.min(stock, 100), max: 100 }) - ), - !isAuthor && stock > 0 - ? form({ method: "POST", action: `/shops/product/buy/${encodeURIComponent(product.key)}`, class: "shop-buy-form" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - p({ class: "shop-buy-form-note" }, i18n.shopBuyEncryptedNote || "Your delivery details are sent encrypted only to the shop owner."), - label(i18n.shopBuyDeliveryAddress || "Delivery address"), - br(), - textarea({ name: "deliveryAddress", required: true, rows: 3, placeholder: i18n.shopBuyDeliveryAddressPlaceholder || "" }), - br(), - br(), - label(i18n.shopBuyContact || "Contact"), - br(), - input({ type: "text", name: "contact", placeholder: i18n.shopBuyContactPlaceholder || "email, phone, etc." }), - br(), - br(), - label(i18n.shopBuyNotes || "Notes"), - br(), - textarea({ name: "notes", rows: 2, placeholder: i18n.shopBuyNotesPlaceholder || "" }), - br(), - br(), - button({ type: "submit", class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy) - ) - : null, - br(), - p({ class: "card-footer" }, - span({ class: "date-link" }, moment(product.createdAt).format("YYYY-MM-DD HH:mm")), - " ", - userLink(product.author) - ), - !isAuthor && params.canRate - ? renderOpinionsVoting('/shops/product/opinions', product.key, product.opinions, returnTo, product.opinions_inhabitants) - : null + const productSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(product.key, `/shops/product/${encodeURIComponent(product.key)}`, { spread: params.spreads || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, returnTo, reportTitle: product.title, deleteAction: isAuthor ? contentDeleteAction('shopProduct', product.key) : undefined }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, product.title || i18n.shopProductUntitled) + ), + renderStarRating(product.opinions, safeArr(product.opinions_inhabitants).length), + product.image ? renderMediaBlob(product.image, null, { class: "tribe-detail-image" }) : null, + div({ class: "price-chip" }, `${Number(product.price || 0).toFixed(6)} ECO`), + div({ class: "confirmations-block stock-block" }, + div({ class: "card-field" }, + span({ class: "card-label" }, `${i18n.shopProductStock}: `), + span({ class: "card-value" }, stock > 0 ? String(stock) : i18n.shopOutOfStock) ), + progress({ class: "confirmations-progress stock-progress", value: Math.min(stock, 100), max: 100 }) + ), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.shopCreatedAt || "CREATED"), + td({ class: "tribe-info-value", colspan: "3" }, moment(product.createdAt).format("YYYY/MM/DD HH:mm")) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(product.author)) + ) + ), + div({ class: "tribe-side-actions" }, + form({ method: "GET", action: `/shops/${encodeURIComponent(product.shopId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, `← ${i18n.shopBackToShop}`) + ) + ), + isAuthor + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: `/shops/product/edit/${encodeURIComponent(product.key)}` }, + input({ type: "hidden", name: "shopId", value: product.shopId }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "tribe-action-btn", type: "submit" }, i18n.shopUpdate) + ) + ) + : null + ) + + const productMain = div({ class: "tribe-main" }, + product.description + ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.marketItemDescription || i18n.shopDescription), + p({ class: "tribe-side-description" }, ...renderStyledText(product.description)) + ) + : null, + !isAuthor && stock > 0 + ? div({ class: "job-section" }, + form({ method: "POST", action: `/shops/product/buy/${encodeURIComponent(product.key)}`, class: "shop-buy-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + p({ class: "shop-buy-form-note" }, i18n.shopBuyEncryptedNote || "Your delivery details are sent encrypted only to the shop owner."), + label(i18n.shopBuyDeliveryAddress || "Delivery address"), + br(), + textarea({ maxlength: "5000", name: "deliveryAddress", required: true, rows: 3, placeholder: i18n.shopBuyDeliveryAddressPlaceholder || "" }), + br(), + br(), + label(i18n.shopBuyContact || "Contact"), + br(), + input({ type: "text", name: "contact", placeholder: i18n.shopBuyContactPlaceholder || "email, phone, etc." }), + br(), + br(), + label(i18n.shopBuyNotes || "Notes"), + br(), + textarea({ maxlength: "5000", name: "notes", rows: 2, placeholder: i18n.shopBuyNotesPlaceholder || "" }), + br(), + br(), + button({ type: "submit", class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy) + ) + ) + : null, + renderEngagement(product.key, + !isAuthor && params.canRate + ? renderOpinionsVoting('/shops/product/opinions', product.key, product.opinions, returnTo, product.opinions_inhabitants) + : null, renderCommentsSection(product.key, returnTo, comments) ) ) + + return template( + product.title || i18n.shopProductTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.shopsTitle), p(i18n.shopDescription), renderWalletChip())), + section(renderModeButtons("products", false, (params && params.modesAvail) || null)), + section(div({ class: "tribe-details" }, productSide, productMain)) + ) } + exports.editProductView = async (product, shopId, params = {}) => { const returnTo = safeText(params.returnTo) || `/shops/${encodeURIComponent(shopId)}` return template( i18n.shopProductUpdate, section( div({ class: "tags-header" }, h2(i18n.shopProductUpdate)), - renderProductForm(shopId, product, true, returnTo) + renderProductForm(shopId, product, true, returnTo, await renderSpreadEditWarning(product && (product.key || product.id))) ) ) } @@ -576,9 +572,10 @@ exports.shopOrdersView = async (shop, orders) => { acts.push(statusForm(o.id, "SHIPPED", i18n.shopOrderMarkShipped || "Mark shipped")) } acts.push(a({ href: `/pm?recipients=${encodeURIComponent(o.buyer)}`, class: "tribe-action-btn" }, i18n.shopOrderPmBuyer || i18n.privateMessage || "PM")) + { const cb = renderCallButton(o.buyer, { cls: "tribe-action-btn", pam: true }); if (cb) acts.push(cb); } return div({ class: "tribe-side-actions" }, ...acts) } - const rows = (orders || []).map(o => div({ class: "shop-order-card card-section" }, + const rows = paged(orders || []).map(o => div({ class: "shop-order-card card-section" }, div({ class: "card-chips-row" }, renderEncryptedChip(i18n), orderStatusChip(o.status)), div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderProduct || "Product"}:`), span({ class: "card-value" }, String(o.title || o.productId || ""))), div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderPrice || "Price"}:`), span({ class: "card-value" }, `${Number(o.price || 0).toFixed(6)} ECO`)), @@ -586,13 +583,13 @@ exports.shopOrdersView = async (shop, orders) => { div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyDeliveryAddress || "Delivery address"}:`), span({ class: "card-value" }, String(o.deliveryAddress || ""))), o.contact ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyContact || "Contact"}:`), span({ class: "card-value" }, String(o.contact))) : null, o.notes ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyNotes || "Notes"}:`), span({ class: "card-value" }, String(o.notes))) : null, - p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY-MM-DD HH:mm"))), + p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY/MM/DD HH:mm"))), orderActions(o) )) return template( title, section( - div({ class: "tags-header" }, h2(title), p(i18n.shopOrdersDescription || "Encrypted purchase orders received by this shop.")), + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.shopOrdersDescription || "Encrypted purchase orders received by this shop.")), a({ href: `/shops/${encodeURIComponent(shop.key || shop.id || "")}`, class: "filter-btn" }, i18n.goBack || "Go back") ), section( @@ -601,11 +598,12 @@ exports.shopOrdersView = async (shop, orders) => { ) } -exports.myPurchasesView = async (purchases) => { - const title = i18n.shopMyOrdersTitle || "My orders" +exports.myPurchasesView = async (purchases, params = {}) => { + const title = i18n.shopsTitle const buyerActions = (o) => { const acts = [] if (o.seller) acts.push(a({ href: `/pm?recipients=${encodeURIComponent(o.seller)}`, class: "tribe-action-btn" }, i18n.shopOrderPmSeller || i18n.privateMessage || "PM")) + if (o.seller) { const cb = renderCallButton(o.seller, { cls: "tribe-action-btn", pam: true }); if (cb) acts.push(cb); } if (String(o.status || "PENDING").toUpperCase() === "SHIPPED") { acts.push(form({ method: "POST", action: `/shops/orders/${encodeURIComponent(o.id)}/status`, class: "inline-form" }, input({ type: "hidden", name: "status", value: "RECEIVED" }), @@ -615,7 +613,7 @@ exports.myPurchasesView = async (purchases) => { } return acts.length ? div({ class: "tribe-side-actions" }, ...acts) : null } - const rows = (purchases || []).map(o => div({ class: "shop-order-card card-section" }, + const rows = paged(purchases || []).map(o => div({ class: "shop-order-card card-section" }, div({ class: "card-chips-row" }, renderEncryptedChip(i18n), orderStatusChip(o.status)), div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderProduct || "Product"}:`), span({ class: "card-value" }, String(o.title || o.productId || ""))), div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderPrice || "Price"}:`), span({ class: "card-value" }, `${Number(o.price || 0).toFixed(6)} ECO`)), @@ -623,17 +621,17 @@ exports.myPurchasesView = async (purchases) => { div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyDeliveryAddress || "Delivery address"}:`), span({ class: "card-value" }, String(o.deliveryAddress || ""))), o.contact ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyContact || "Contact"}:`), span({ class: "card-value" }, String(o.contact))) : null, o.notes ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyNotes || "Notes"}:`), span({ class: "card-value" }, String(o.notes))) : null, - p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY-MM-DD HH:mm"))), + p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY/MM/DD HH:mm"))), buyerActions(o) )) return template( title, section( - div({ class: "tags-header" }, h2(title), p(i18n.shopMyOrdersDescription || "Your encrypted purchase orders.")) + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.shopMyOrdersDescription || "Your encrypted purchase orders.")) ), - section(renderModeButtons("purchases")), + section(renderModeButtons("purchases", false, params.modesAvail || null)), section( - rows.length ? div({ class: "shop-orders-list" }, ...rows) : p(i18n.shopMyOrdersEmpty || "You have no purchases yet.") + rows.length ? div({ class: "shop-orders-list" }, ...rows) : div({ class: "tribe-grid" }, p(i18n.shopMyOrdersEmpty || "You have no purchases yet.")) ) ) } @@ -645,17 +643,17 @@ exports.clearnetShopView = async (shop, products = []) => { }; const productCards = (products || []).filter(p => Number(p.stock) > 0 || p.featured).map(prod => { const pImg = cnBlobUrl(prod.image); + const pKind = /^\[video:/.test(String(prod.image || '')) ? 'video' : /^\[audio:/.test(String(prod.image || '')) ? 'audio' : 'img'; return `
- ${pImg ? `` : ''} + ${pImg ? (pKind === 'img' ? `` : `<${pKind} class="cn-product-img" src="${pImg}" controls preload="metadata">`) : ''}

${cnEscapeHtml(prod.title || '')}

- ${prod.description ? `

${cnEscapeHtml(prod.description)}

` : ''} + ${prod.description ? `

${cnRichText(prod.description)}

` : ''}

${fmtPrice(prod.price)} ECO

- ${Number(prod.stock) > 0 ? `

Stock: ${prod.stock}

` : ''}
`; }).join('\n'); const shopBlobUrl = cnBlobUrl(shop.image); const shopImg = shopBlobUrl ? `${cnEscapeHtml(shop.title || '')}` : ''; - const desc = cnEscapeHtml(shop.shortDescription || shop.description || ''); + const desc = cnRichText(shop.shortDescription || shop.description || ''); const extraCss = ` .cn-hero{display:flex;gap:24px;margin-bottom:24px;flex-wrap:wrap;align-items:flex-start} .cn-shop-img{display:block;max-width:280px;width:100%;border:3px solid var(--fg);border-radius:8px;background:#000} @@ -681,16 +679,17 @@ exports.clearnetShopView = async (shop, products = []) => {

${cnEscapeHtml(shop.title || '')}

${desc ? `

${desc}

` : ''}
+ ${cnKindTag('shop')} ${shop.createdAt ? `📅 ${new Date(shop.createdAt).toISOString().slice(0,10)}` : ''} - ${shop.location ? `📍 ${cnEscapeHtml(shop.location)}` : ''}
-

Products

- ${productCards ? `
${productCards}
` : '
No products available.
'} +
+

${cnEscapeHtml(i18n.shopProducts)}

+ ${productCards ? `
${productCards}
` : `
${cnEscapeHtml(i18n.shopNoProducts)}
`} `; return renderClearnetPage({ - title: `${shop.title || 'Shop'} — Oasis`, + title: `${shop.title || 'Shop'} | Oasis`, ogTitle: shop.title || 'Oasis', ogDescription: shop.shortDescription || shop.description || '', ogImage: shopBlobUrl, diff --git a/src/views/stats_view.js b/src/views/stats_view.js index a2cd700..337b0ce 100644 --- a/src/views/stats_view.js +++ b/src/views/stats_view.js @@ -1,5 +1,6 @@ const { div, h2, p, section, button, form, input, ul, li, a, h3, span, strong, table, thead, tbody, tr, td, th } = require("../server/node_modules/hyperaxe"); const { template, i18n, userLink } = require('./main_views'); +const sharedState = require('../configs/shared-state'); Object.assign(i18n, { statsChat: "Chats", @@ -36,10 +37,11 @@ exports.statsView = (stats, filter) => { const description = i18n.statsDescription; const modes = ['ALL', 'MINE', 'TOMBSTONE']; const types = [ - 'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project', - 'image', 'torrent', 'audio', 'video', 'document', 'transfer', 'post', 'tribe', + 'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project', 'industry', 'industryBlueprint', + 'image', 'torrent', 'file', 'audio', 'video', 'document', 'transfer', 'post', 'tribe', 'market', 'forum', 'job', 'aiExchange', 'map', 'shop', 'shopProduct', - 'chat', 'chatMessage', 'pad', 'padEntry', 'gameScore', 'calendar', 'calendarDate', 'calendarNote', + 'chat', 'chatMessage', 'pad', 'padEntry', 'room', 'roomMember', 'wikiPage', 'emergency', 'emergencyConfirm', 'emergencyUpdate', 'mailingList', 'logisticsRoute', 'logisticsRating', 'podcast', 'podcastEpisode', 'podcastPlay', 'campaign', 'campaignSignature', 'campaignUpdate', 'gameScore', 'calendar', 'calendarDate', 'calendarNote', + 'schoolCourse', 'schoolLesson', 'schoolEnroll', 'schoolCertificate', 'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw', 'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote' ]; @@ -51,8 +53,11 @@ exports.statsView = (stats, filter) => { report: i18n.statsReport, feed: i18n.statsFeed, project: i18n.statsProject, + industry: i18n.industryTitle, + industryBlueprint: i18n.industryBlueprints, image: i18n.statsImage, torrent: i18n.statsTorrent, + file: i18n.statsFile, audio: i18n.statsAudio, video: i18n.statsVideo, document: i18n.statsDocument, @@ -70,7 +75,26 @@ exports.statsView = (stats, filter) => { chatMessage: i18n.statsChatMessage, pad: i18n.statsPad, padEntry: i18n.statsPadEntry, + room: i18n.statsRoom, + roomMember: i18n.statsRoomMember, + wikiPage: i18n.statsWiki, + emergency: i18n.statsEmergency, + emergencyConfirm: i18n.statsEmergencyConfirm, + emergencyUpdate: i18n.statsEmergencyUpdate, + mailingList: i18n.statsMailingList, + logisticsRoute: i18n.statsLogisticsRoute, + logisticsRating: i18n.statsLogisticsRating, + podcast: i18n.statsPodcast, + podcastEpisode: i18n.statsPodcastEpisode, + podcastPlay: i18n.statsPodcastPlay, + campaign: i18n.statsCampaign, + campaignSignature: i18n.statsCampaignSignature, + campaignUpdate: i18n.statsCampaignUpdate, gameScore: i18n.statsGameScore, + schoolCourse: i18n.statsSchoolCourse, + schoolLesson: i18n.statsSchoolLesson, + schoolEnroll: i18n.statsSchoolEnroll, + schoolCertificate: i18n.statsSchoolCertificate, calendar: i18n.statsCalendar, calendarDate: i18n.statsCalendarDate, calendarNote: i18n.statsCalendarNote, @@ -169,25 +193,26 @@ exports.statsView = (stats, filter) => { const networkCO2 = parseFloat((totalMB * kWhPerMB * gCO2PerKWh).toFixed(2)); const inhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 1; const userCO2 = parseFloat((networkCO2 / Math.max(1, inhabitants)).toFixed(2)); - const maxAnnualCO2 = 500; + const gCO2PerInhabitantYear = 50; + const maxAnnualCO2 = Math.max(500, Math.max(1, inhabitants) * gCO2PerInhabitantYear); if (filter === 'MINE') { const pct = networkCO2 > 0 ? Math.min(100, (userCO2 / networkCO2) * 100) : 0; return div({ class: 'carbon-chart' }, div({ class: 'carbon-bar-label' }, - span(i18n.statsCarbonUser || 'Your footprint'), - span(`${userCO2} g CO₂`) - ), - div({ class: 'carbon-bar-track' }, - div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(pct)}` }) - ), - div({ class: 'carbon-bar-label' }, - span(i18n.statsCarbonNetwork || 'Network total'), + span(`HcT — ${i18n.statsCarbonNetwork || 'Network total'}`), span(`${networkCO2} g CO₂`) ), div({ class: 'carbon-bar-track' }, div({ class: 'carbon-bar-fill carbon-bar-network stats-w-100' }) ), + div({ class: 'carbon-bar-label' }, + span(`HcH — ${i18n.statsCarbonUser || 'Your footprint'}`), + span(`${userCO2} g CO₂`) + ), + div({ class: 'carbon-bar-track' }, + div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(pct)}` }) + ), p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfNetwork || 'of network total'}`), p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)')) ); @@ -199,13 +224,6 @@ exports.statsView = (stats, filter) => { const tombCO2 = parseFloat((tombMB * kWhPerMB * gCO2PerKWh).toFixed(4)); const tombPct = networkCO2 > 0 ? Math.min(100, (tombCO2 / networkCO2) * 100) : 0; return div({ class: 'carbon-chart' }, - div({ class: 'carbon-bar-label' }, - span(i18n.statsCarbonTombstone || 'Tombstoning footprint'), - span(`${tombCO2} g CO₂`) - ), - div({ class: 'carbon-bar-track' }, - div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(tombPct)}` }) - ), div({ class: 'carbon-bar-label' }, span(i18n.statsCarbonNetwork || 'Network total'), span(`${networkCO2} g CO₂`) @@ -213,62 +231,59 @@ exports.statsView = (stats, filter) => { div({ class: 'carbon-bar-track' }, div({ class: 'carbon-bar-fill carbon-bar-network stats-w-100' }) ), + div({ class: 'carbon-bar-label' }, + span(i18n.statsCarbonTombstone || 'Tombstoning footprint'), + span(`${tombCO2} g CO₂`) + ), + div({ class: 'carbon-bar-track' }, + div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(tombPct)}` }) + ), p({ class: 'carbon-bar-note' }, strong(`${tombPct.toFixed(1)}%`), ` ${i18n.statsCarbonOfNetwork || 'of network total'} (${tombCount} tombstones × ~${avgTombBytes} bytes)`), p({ class: 'carbon-bar-formula' }, 'Based on estimated tombstone message size ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)')) ); } const pct = Math.min(100, (networkCO2 / maxAnnualCO2) * 100); + const now = new Date(); + const yearStart = new Date(now.getFullYear(), 0, 1).getTime(); + const yearEnd = new Date(now.getFullYear() + 1, 0, 1).getTime(); + const yearPct = Math.min(100, ((now.getTime() - yearStart) / (yearEnd - yearStart)) * 100); return div({ class: 'carbon-chart' }, - div({ class: 'carbon-bar-label' }, - span(i18n.statsCarbonNetwork || 'Network footprint'), - span(`${networkCO2} g CO₂`) - ), - div({ class: 'carbon-bar-track' }, - div({ class: `carbon-bar-fill carbon-bar-network ${wClass(pct)}` }) - ), div({ class: 'carbon-bar-label' }, span(i18n.statsCarbonMaxAnnual || 'Annual max estimate'), span(`${maxAnnualCO2} g CO₂`) ), div({ class: 'carbon-bar-track' }, - div({ class: 'carbon-bar-fill carbon-bar-max stats-w-100' }) + div({ class: `carbon-bar-fill carbon-bar-max ${wClass(yearPct)}` }) ), - p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfEstMax || 'of estimated max capacity'}`), - p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)')) + div({ class: 'carbon-bar-label' }, + span(`HcT — ${i18n.statsCarbonNetwork || 'Network footprint'}`), + span(`${networkCO2} g CO₂`) + ), + div({ class: 'carbon-bar-track' }, + div({ class: `carbon-bar-fill carbon-bar-network ${wClass(pct)}` }) + ), + p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfEstMax || 'of estimated max capacity'} · `, strong(`${yearPct.toFixed(1)}%`), ` ${i18n.statsCarbonYearProgress || 'of the year elapsed'}`), + p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)'), ` · max: ${gCO2PerInhabitantYear} g CO₂ × ${Math.max(1, inhabitants)} inhabitants`) ); })(); - const headerCard = div({ class: 'stats-card' }, + const storageCard = div({ class: 'stats-card' }, + h3({ class: 'stats-section-h' }, i18n.statsStorageTitle || 'Storage'), table({ class: 'block-info-table' }, - tr(td({ class: 'card-label' }, i18n.statsCreatedAt), td({ class: 'card-value' }, stats.createdAt)), - tr(td({ class: 'card-label' }, 'ID'), td({ class: 'card-value' }, userLink(stats.id))), tr(td({ class: 'card-label' }, i18n.statsBlobsSize), td({ class: 'card-value' }, stats.statsBlobsSize)), tr(td({ class: 'card-label' }, i18n.statsBlockchainSize), td({ class: 'card-value' }, stats.statsBlockchainSize)), tr(td({ class: 'card-label' }, i18n.statsSize), td({ class: 'card-value' }, stats.folderSize)) ) ); - const totalInhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 0; - const networkKPIs = stats.networkKPIs || {}; - - const topStrip = div({ class: 'stats-block' }, - kpiGrid( - kpi(i18n.bankingUserEngagementScore, C(stats, 'karmaScore')), - kpi(i18n.statsUsersTitle, totalInhabitants), - kpi(i18n.statsTotalMsgs || 'Total messages', networkKPIs.totalMsgs || 0), - kpi(i18n.statsLogsTitle || 'Logs', stats?.logsCount || 0), - kpi(i18n.statsAITraining, C(stats, 'aiExchange') || 0), - kpi(i18n.statsPUBs, stats.pubsCount || 0) - ) - ); - - const carbonCard = div({ class: 'stats-card' }, - h3({ class: 'stats-section-h' }, i18n.statsCarbonFootprintTitle || 'Carbon Footprint'), - carbonChart - ); - - const bankingCard = div({ class: 'stats-card' }, + const accountCard = div({ class: 'stats-card' }, + h3({ class: 'stats-section-h' }, i18n.statsAccountTitle || 'Account'), table({ class: 'block-info-table' }, + tr(td({ class: 'card-label' }, 'ID'), td({ class: 'card-value' }, userLink(stats.id))), + tr(td({ class: 'card-label' }, i18n.statsCreatedAt), td({ class: 'card-value' }, stats.createdAt)), + C(stats, 'karmaScore') > 0 + ? tr(td({ class: 'card-label' }, i18n.bankingUserEngagementScore), td({ class: 'card-value' }, String(C(stats, 'karmaScore')))) + : null, tr(td({ class: 'card-label' }, i18n.statsEcoWalletLabel), td({ class: 'card-value' }, a({ href: '/wallet', class: 'stats-link-break' }, stats?.banking?.myAddress || i18n.statsEcoWalletNotConfigured))), tr(td({ class: 'card-label' }, i18n.profileGpgChip || 'GPG'), td({ class: 'card-value' }, stats?.gpgFingerprint @@ -278,23 +293,67 @@ exports.statsView = (stats, filter) => { ) ); - const networkBlock = div({ class: 'stats-block' }, - kpiGrid( - filter === 'MINE' - ? kpi(i18n.statsMyShare || 'Your share of the network', `${fmtNum(networkKPIs.myShare || 0)}%`) - : null, - kpi(i18n.statsAvgPerInhabitant || 'Avg per inhabitant', fmtNum(networkKPIs.avgMsgsPerInhabitant || 0)), - kpi(i18n.statsMsgsPerDay || 'Messages/day (lifetime)', fmtNum(networkKPIs.networkMsgsPerDay || 0)), - kpi(i18n.statsNetworkSpan || 'Network span', `${fmtNum(networkKPIs.networkSpanDays || 0)} d`), - kpi(i18n.statsTombstoneRatioLabel || 'Tombstone ratio', `${fmtNum(stats.tombstoneKPIs?.ratio || 0)}%`) - ) + const totalInhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 0; + const networkKPIs = stats.networkKPIs || {}; + + const kvTable = (rows) => { + const shown = rows.filter(([, v]) => !isZero(v)); + if (!shown.length) return p({ class: 'no-content' }, i18n.no_results || 'No data'); + return table({ class: 'tag-table' }, + thead(tr( + th(i18n.statsContentTypeColumn || 'Type'), + th(i18n.statsContentCountColumn || 'Count') + )), + tbody(...shown.map(([label, value]) => tr(td(label), td(String(value))))) + ); + }; + + const blockWithTiles = (cls, title, tiles) => { + const shown = (Array.isArray(tiles) ? tiles : []).filter(Boolean); + if (!shown.length) return null; + return div({ class: cls }, h3({ class: 'stats-section-h' }, title), kpiGrid(...shown)); + }; + + const networkRows = [ + [i18n.statsUsersTitle, totalInhabitants], + [i18n.statsTotalMsgs || 'Total messages', networkKPIs.totalMsgs || 0], + [i18n.statsLogsTitle || 'Logs', stats?.logsCount || 0], + [i18n.statsAITraining, C(stats, 'aiExchange') || 0], + [i18n.statsPUBs, stats.pubsCount || 0], + [i18n.statsSyncedPeers, sharedState.getSyncedPeerCount() || 0] + ]; + const networkStrip = networkRows.some(([, v]) => !isZero(v)) + ? div({ class: 'stats-block' }, + h3({ class: 'stats-section-h' }, i18n.statsNetworkTitle || 'Network'), + kvTable(networkRows) + ) + : null; + + const carbonCard = div({ id: 'carbon', class: 'stats-card' }, + h3({ class: 'stats-section-h' }, i18n.statsCarbonFootprintTitle || 'Carbon Footprint'), + carbonChart ); + const networkBlock = blockWithTiles('stats-block stats-block-boxed', i18n.statsAveragesTitle || 'Averages', [ + filter === 'MINE' + ? kpi(i18n.statsMyShare || 'Your share of the network', `${fmtNum(networkKPIs.myShare || 0)}%`) + : null, + kpi(i18n.statsAvgPerInhabitant || 'Avg per inhabitant', fmtNum(networkKPIs.avgMsgsPerInhabitant || 0)), + kpi(i18n.statsMsgsPerDay || 'Messages/day (lifetime)', fmtNum(networkKPIs.networkMsgsPerDay || 0)), + kpi(i18n.statsNetworkSpan || 'Network span', `${fmtNum(networkKPIs.networkSpanDays || 0)} d`), + kpi(i18n.statsTombstoneRatioLabel || 'Tombstone ratio', `${fmtNum(stats.tombstoneKPIs?.ratio || 0)}%`) + ]); + const activityBlock = (() => { const rows = Array.isArray(stats.activity?.daily7) ? stats.activity.daily7 : []; const max = Math.max(1, ...rows.map(r => Number(r.count) || 0)); + if (!rows.length && isZero(stats.activity?.daily7Total || 0) && isZero(stats.activity?.daily30Total || 0)) return null; return div({ class: 'stats-block' }, h2(i18n.statsActivity7d), + kpiGrid( + kpi(i18n.statsActivity7dTotal, stats.activity?.daily7Total || 0), + kpi(i18n.statsActivity30dTotal, stats.activity?.daily30Total || 0) + ), rows.length ? ul({ class: 'stats-toplist' }, ...rows.map(row => { @@ -309,11 +368,7 @@ exports.statsView = (stats, filter) => { ); }) ) - : p({ class: 'no-content' }, i18n.no_results || 'No data'), - div({ class: 'stats-activity-totals' }, - span(`${i18n.statsActivity7dTotal}: `, strong(String(stats.activity?.daily7Total || 0))), - span(`${i18n.statsActivity30dTotal}: `, strong(String(stats.activity?.daily30Total || 0))) - ) + : p({ class: 'no-content' }, i18n.no_results || 'No data') ); })(); @@ -389,8 +444,17 @@ exports.statsView = (stats, filter) => { ); }; - const buildOpinionTiles = () => - types.map(t => O(stats, t) > 0 ? kpi(labels[t], O(stats, t)) : null).filter(Boolean); + const buildOpinionTable = () => { + const rows = types.map(t => [labels[t], O(stats, t)]).filter(([, n]) => n > 0).sort((a, b) => b[1] - a[1]); + if (!rows.length) return p({ class: 'no-content' }, i18n.no_results || 'No data'); + return table({ class: 'tag-table' }, + thead(tr( + th(i18n.statsContentTypeColumn || 'Type'), + th(i18n.statsContentCountColumn || 'Count') + )), + tbody(...rows.map(([label, count]) => tr(td(label), td(String(count))))) + ); + }; const tribeListBlock = (label, list) => div({ class: 'stats-block' }, h2(`${label}: ${list.length}`), @@ -403,16 +467,15 @@ exports.statsView = (stats, filter) => { const allMode = filter === 'ALL' ? div({ class: 'stats-container' }, [ - networkBlock, activityBlock, totalOpinions > 0 - ? div({ class: 'stats-block' }, + ? div({ class: 'stats-block stats-block-boxed' }, h2(`${i18n.statsNetworkOpinions}: ${totalOpinions}`), - kpiGrid(...buildOpinionTiles()) + buildOpinionTable() ) : null, totalContent > 0 - ? div({ class: 'stats-block' }, + ? div({ class: 'stats-block stats-block-boxed' }, h2(`${i18n.statsNetworkContent}: ${totalContent}`), buildContentTable() ) @@ -422,16 +485,15 @@ exports.statsView = (stats, filter) => { const mineMode = filter === 'MINE' ? div({ class: 'stats-container' }, [ - networkBlock, activityBlock, totalOpinions > 0 - ? div({ class: 'stats-block' }, + ? div({ class: 'stats-block stats-block-boxed' }, h2(`${i18n.statsYourOpinions}: ${totalOpinions}`), - kpiGrid(...buildOpinionTiles()) + buildOpinionTable() ) : null, totalContent > 0 - ? div({ class: 'stats-block' }, + ? div({ class: 'stats-block stats-block-boxed' }, h2(`${i18n.statsYourContent}: ${totalContent}`), buildContentTable() ) @@ -444,26 +506,23 @@ exports.statsView = (stats, filter) => { const userTomb = Number(stats.userTombstoneCount || 0); const netTomb = Number(stats.tombstoneKPIs?.networkTombstoneCount || 0); if (userTomb === 0 && netTomb === 0) { - return div({ class: 'stats-container' }, - div({ class: 'stats-block' }, - p({ class: 'no-content' }, i18n.statsTombstoneEmpty || 'No tombstones in the network yet.') - ) + return div({ class: 'stats-block' }, + p({ class: 'no-content' }, i18n.statsTombstoneEmpty || 'No tombstones in the network yet.') ); } - return div({ class: 'stats-container' }, [ - div({ class: 'stats-block' }, - kpiGrid( - kpi(i18n.TOMBSTONEButton, userTomb), - kpi(i18n.statsTombstoneRatio, `${(stats.tombstoneKPIs?.ratio || 0).toFixed(2)}%`) - ) + return div({ class: 'stats-block stats-block-boxed' }, + kpiGrid( + kpi(i18n.TOMBSTONEButton, userTomb), + kpi(i18n.statsTombstoneRatio, `${(stats.tombstoneKPIs?.ratio || 0).toFixed(2)}%`) ) - ]); + ); })() : null; const ets = stats.ecoTaxStats || null; const userTax = Number(stats.userEcoinTax || 0); const ecoTaxBlock = ets ? div({ class: 'stats-card' }, + h3({ class: 'stats-section-h' }, i18n.statsEcoTaxTitle || 'ECO Tax'), table({ class: 'block-info-table' }, filter === 'MINE' ? tr(td({ class: 'card-label' }, i18n.bankTaxesUserAmount || 'Your ECO tax'), td({ class: 'card-value' }, `${userTax.toFixed(6)} ECO`)) @@ -479,7 +538,7 @@ exports.statsView = (stats, filter) => { return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(description) ), @@ -487,18 +546,20 @@ exports.statsView = (stats, filter) => { modes.map(m => form({ method: 'GET', action: '/stats' }, input({ type: 'hidden', name: 'filter', value: m }), - button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, i18n[m + 'Button']) + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(i18n[m + 'Button']).toUpperCase()) ) ) ), section( - topStrip, - headerCard, + filter === 'MINE' ? accountCard : null, + filter === 'ALL' ? storageCard : null, + filter === 'ALL' ? networkStrip : null, + tombMode, + filter === 'TOMBSTONE' ? null : networkBlock, + filter === 'TOMBSTONE' && !(Number(stats.userTombstoneCount || 0) || Number(stats.tombstoneKPIs?.networkTombstoneCount || 0)) ? null : carbonCard, filter !== 'TOMBSTONE' ? ecoTaxBlock : null, - bankingCard, allMode, - mineMode, - tombMode + mineMode ) ) ); diff --git a/src/views/tags_view.js b/src/views/tags_view.js index 2976f5b..eb7069d 100644 --- a/src/views/tags_view.js +++ b/src/views/tags_view.js @@ -1,14 +1,12 @@ -const { form, button, div, h2, p, section, table, thead, tr, th, td, a, tbody } = require("../server/node_modules/hyperaxe"); -const { template, i18n } = require('./main_views'); +const { form, button, div, h2, p, section, table, thead, tr, th, td, a, tbody, input } = require("../server/node_modules/hyperaxe"); +const { template, i18n, renderModuleStats, paged } = require('./main_views'); const getFilteredTags = (filter, tags) => { - let filteredTags = Array.isArray(tags) ? tags : []; - if (filter === 'top') { - filteredTags = filteredTags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); - } else { - filteredTags = filteredTags.sort((a, b) => a.name.localeCompare(b.name)); - } - return filteredTags; + const filteredTags = Array.isArray(tags) ? [...tags] : []; + if (filter === 'top') return filteredTags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); + if (filter === 'mine') return filteredTags.sort((a, b) => (b.mine || 0) - (a.mine || 0) || a.name.localeCompare(b.name)); + if (filter === 'recent') return filteredTags.sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0)); + return filteredTags.sort((a, b) => a.name.localeCompare(b.name)); }; const renderTagsTable = (filteredTags) => { @@ -33,56 +31,51 @@ const renderTagsTable = (filteredTags) => { const renderTagsCloud = (mergedTags) => { const maxCount = Math.max(...mergedTags.map(t => t.count), 1); return div({ class: 'tag-cloud-wrap' }, - mergedTags.map(tag => { - const angle = Math.random() * 2 * Math.PI; - const radius = 10 + Math.random() * 40; - const x = 50 + Math.cos(angle) * radius; - const y = 50 + Math.sin(angle) * radius; - const weight = tag.count / maxCount; - const fontSize = 12 + Math.round(weight * 32); - const hue = 200 + Math.round(weight * 120); - - return a({ - href: `/search?query=%23${encodeURIComponent(tag.name)}`, - class: 'tag-cloud-item', - style: ` - left: ${x}%; - top: ${y}%; - font-size: ${fontSize}px; - color: hsl(${hue},70%,60%); - ` - }, tag.name); - }) + mergedTags.map(tag => a({ + href: `/search?query=%23${encodeURIComponent(tag.name)}`, + class: 'tag-cloud-item', + 'data-weight': String(1 + Math.round((tag.count / maxCount) * 4)) + }, tag.name)) ); }; -exports.tagsView = async (tags, filter) => { +exports.tagsView = async (tags, filter, search = '') => { const filteredTags = getFilteredTags(filter, tags); + const query = String(search || '').trim(); - const title = - filter === 'top' ? i18n.tagsTopSectionTitle : - filter === 'cloud' ? i18n.tagsCloudSectionTitle : - i18n.tagsAllSectionTitle; + const title = i18n.tagsTitle; + const emptyTags = filteredTags.length === 0 && ['top', 'all'].includes(String(filter || 'top')) && !query; return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.tagsTitle), p(i18n.tagsDescription) ), - div({ class: 'filters' }, - form({ method: 'GET', action: '/tags' }, - button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterAll), - button({ type: 'submit', name: 'filter', value: 'top', class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterTop), - button({ type: 'submit', name: 'filter', value: 'cloud', class: filter === 'cloud' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterCloud) + emptyTags ? null : div({ class: 'filters' }, + form({ method: 'GET', action: '/tags', class: 'ui-toolbar ui-toolbar--filters' }, + input({ type: 'hidden', name: 'search', value: query }), + button({ type: 'submit', name: 'filter', value: 'top', class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterTop).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterAll).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'recent', class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterRecent).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'mine', class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterMine).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'cloud', class: filter === 'cloud' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterCloud).toUpperCase()) + ) + ), + emptyTags ? null : div({ class: 'tags-search activity-filter-chips activity-toolbar-row' }, + renderModuleStats(filteredTags.length), + form({ method: 'GET', action: '/tags', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter || 'top' }), + input({ type: 'text', name: 'search', value: query, placeholder: i18n.tagsSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)) ) ), div({ class: 'tags-list' }, filteredTags.length === 0 - ? p(i18n.tagsNoItems) + ? p(query ? i18n.noResultsFound : i18n.tagsNoItems) : filter !== 'cloud' - ? renderTagsTable(filteredTags) + ? renderTagsTable(paged(filteredTags)) : renderTagsCloud(filteredTags) ) ) diff --git a/src/views/task_view.js b/src/views/task_view.js index 40a2624..089e534 100644 --- a/src/views/task_view.js +++ b/src/views/task_view.js @@ -1,25 +1,28 @@ const { div, h2, p, section, button, form, input, select, option, a, br, textarea, label, span, table, tr, td, img, video } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { renderPhotoGallery, renderGalleryFields, imagesOf, renderZoomableImage } = require("./gallery_view"); +const { renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const renderTaskMediaBlob = (value, attrs = {}) => { if (!value) return null; const s = String(value).trim(); if (!s) return null; - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }); + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }); const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }); + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }); const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }); + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }); return null; }; const userId = config.keys.id; const opt = (value, isSelected, text) => - option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text); + option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text); const safeArray = (v) => Array.isArray(v) ? v : []; @@ -52,100 +55,50 @@ const renderTaskOwnerActions = (task, returnTo) => { input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "update-btn" }, i18n.taskUpdateButton) ), + ]; +}; + +const renderTaskStatusRow = exports.renderTaskStatusRow = (task, returnTo, action = null) => { + const st = normalizeStatus(task.status || "OPEN"); + return div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.taskStatus}: `), + renderStateChip(st === "CLOSED" ? "hidden" : "mutuals", st === "CLOSED" ? "🔒" : "👁", statusLabel(task.status)), form( - { method: "POST", action: `/tasks/delete/${encodeURIComponent(task.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "delete-btn" }, i18n.taskDeleteButton) - ), - form( - { method: "POST", action: `/tasks/status/${encodeURIComponent(task.id)}`, class: "project-control-form project-control-form--status" }, + { method: "POST", action: action || `/tasks/status/${encodeURIComponent(task.id)}`, class: "project-control-form project-control-form--status" }, input({ type: "hidden", name: "returnTo", value: returnTo }), select( { name: "status", class: "project-control-select" }, - option({ value: "OPEN", selected: st === "OPEN" }, i18n.taskStatusOpen), - option({ value: "IN-PROGRESS", selected: st === "IN-PROGRESS" }, i18n.taskStatusInProgress), - option({ value: "CLOSED", selected: st === "CLOSED" }, i18n.taskStatusClosed) + option({ value: "OPEN", ...(st === "OPEN" ? { selected: true } : {})}, i18n.taskStatusOpen), + option({ value: "IN-PROGRESS", ...(st === "IN-PROGRESS" ? { selected: true } : {})}, i18n.taskStatusInProgress), + option({ value: "CLOSED", ...(st === "CLOSED" ? { selected: true } : {})}, i18n.taskStatusClosed) ), - button({ class: "status-btn project-control-btn", type: "submit" }, setStatusLabel) + button({ class: "status-btn project-control-btn", type: "submit" }, i18n.taskSetStatus) + ) + ); +}; + +const renderTaskAssignAction = exports.renderTaskAssignAction = (task, isAssignedToMe, returnTo, action = null) => { + const st = normalizeStatus(task.status || "OPEN"); + if (st === "CLOSED") return null; + return [ + span({ class: "card-label" }, `${i18n.taskAssignedTo}: `), + renderStateChip(isAssignedToMe ? "mutuals" : "whole", isAssignedToMe ? "👤" : "○", + String(isAssignedToMe ? i18n.taskAssignedChip : i18n.taskUnassignedChip).toUpperCase()), + form( + { method: "POST", action: action || `/tasks/assign/${encodeURIComponent(task.id)}`, class: "project-control-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "status-btn project-control-btn" }, isAssignedToMe ? i18n.taskUnassignButton : i18n.taskAssignButton) ) ]; }; -const renderTaskAssignAction = (task, isAssignedToMe, returnTo) => { - const st = normalizeStatus(task.status || "OPEN"); - if (st === "CLOSED") return null; - return form( - { method: "POST", action: `/tasks/assign/${encodeURIComponent(task.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "filter-btn" }, isAssignedToMe ? i18n.taskUnassignButton : i18n.taskAssignButton) - ); -}; - const renderTaskCommentsSection = (taskId, comments = [], currentFilter = "all") => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; const returnTo = `/tasks/${encodeURIComponent(taskId)}?filter=${encodeURIComponent(currentFilter || "all")}`; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/tasks/${encodeURIComponent(taskId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); - }); - return visibleComments.length - ? div( - { class: "comments-list" }, - visibleComments.map((c) => { - const author = c.value && c.value.author ? c.value.author : ""; - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c.value && c.value.content ? c.value.content : {}; - const root = content.fork || content.root || ""; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(String(text))) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet); - })() - ); + return renderSharedCommentsSection({ + action: `/tasks/${encodeURIComponent(taskId)}/comments`, + comments: comments, + returnTo: returnTo + }); }; const renderTaskStatusChip = (status) => { @@ -159,11 +112,12 @@ const renderTaskPriorityChip = (priority) => { const p = String(priority || "").toUpperCase(); if (!p) return null; const variant = - p === "URGENT" ? "closed" : + p === "URGENT" || p === "CRITICAL" ? "closed" : p === "HIGH" ? "lifespan-orange" : p === "MEDIUM" ? "whole" : "mutuals"; const localized = + p === "CRITICAL" ? i18n.tribePriorityCritical : p === "URGENT" ? i18n.taskPriorityUrgent : p === "HIGH" ? i18n.taskPriorityHigh : p === "MEDIUM" ? i18n.taskPriorityMedium : @@ -171,15 +125,19 @@ const renderTaskPriorityChip = (priority) => { return renderStateChip(variant, "⚑", localized || p); }; -const renderTaskItem = (task, filter, spreadInfo) => { +const renderTaskItem = exports.renderTaskItem = (task, filter, spreadInfo, opts = {}) => { const currentFilter = filter || "all"; const assignees = safeArray(task.assignees); const isPrivate = String(task.isPublic || "").toUpperCase() === "PRIVATE"; + const isAuthor = String(task.author) === String(userId); + const origin = task.tribeOrigin || null; + const href = opts.href !== undefined ? opts.href : (origin ? origin.href : `/tasks/${encodeURIComponent(task.id)}`); - const heroNode = task.image + const cover = origin ? null : imagesOf(task)[0] + const heroNode = cover ? div({ class: "tribe-card-image-wrapper" }, a({ href: `/tasks/${encodeURIComponent(task.id)}` }, - renderTaskMediaBlob(task.image, { class: "tribe-card-hero-image" }) + renderTaskMediaBlob(cover, { class: "tribe-card-hero-image" }) ), div({ class: "tribe-visit-btn-wrapper" }, form({ method: "GET", action: `/tasks/${encodeURIComponent(task.id)}` }, @@ -191,58 +149,54 @@ const renderTaskItem = (task, filter, spreadInfo) => { : null; const chips = [ - renderTaskStatusChip(task.status), isPrivate ? renderPrivacyChip(true, i18n) : null, + renderTaskStatusChip(task.status), renderTaskPriorityChip(task.priority), - renderLifespanChip(task.lifetime, i18n) + renderLifespanChip(task.lifetime, i18n), + origin ? renderTribeOriginChip(origin) : null, + ...(Array.isArray(opts.extraChips) ? opts.extraChips : []) ].filter(Boolean); - const start = task.startTime ? moment(task.startTime).format("YYYY/MM/DD HH:mm") : ""; - const end = task.endTime ? moment(task.endTime).format("YYYY/MM/DD HH:mm") : ""; + const headerActions = opts.headerActions !== undefined + ? opts.headerActions + : origin ? null : renderContentActions(task.id, `/tasks/${encodeURIComponent(task.id)}`, { spread: spreadInfo || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title, returnTo: `/tasks?filter=${encodeURIComponent(currentFilter)}`, deleteAction: isAuthor ? contentDeleteAction('task', task.id) : undefined }); - return div({ class: "tribe-card task-card" }, + return div({ class: "tribe-card task-card" + (isAuthor ? " own-content" : "") }, + headerActions ? div({ class: "card-header activity-card-header" }, span(), headerActions) : null, heroNode, div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/tasks/${encodeURIComponent(task.id)}` }, task.title || i18n.tasksTitle) + href ? a({ href }, task.title || i18n.tasksTitle) : (task.title || i18n.tasksTitle) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - (start || end) ? p({ class: "card-date-highlight" }, start && end ? `${start} → ${end}` : (start || end)) : null, - div({ class: "tribe-card-members" }, + origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.taskAssignedTo}: ${assignees.length}`) ), - div({ class: "card-spread-centered" }, renderSpreadButton(task.id, spreadInfo)), - !heroNode - ? div({ class: "card-visit-btn-centered" }, - form({ method: "GET", action: `/tasks/${encodeURIComponent(task.id)}` }, - input({ type: "hidden", name: "filter", value: currentFilter }), - button({ type: "submit", class: "filter-btn" }, i18n.viewTask || "View Task") - ) - ) - : null + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) ) ); }; +const taskChipFor = (currentFilter, visible) => (mode) => { + if (mode === currentFilter) return true; + if (!Array.isArray(visible)) return true; + if (mode === "mine") return visible.some((t) => t.author === userId); + if (mode === "recent") return visible.length > 0; + if (mode === "assigned") return visible.some((t) => safeArray(t.assignees).includes(userId)); + if (mode === "open") return visible.some((t) => normalizeStatus(t.status) === "OPEN"); + if (mode === "in-progress") return visible.some((t) => normalizeStatus(t.status) === "IN-PROGRESS"); + if (mode === "closed") return visible.some((t) => normalizeStatus(t.status) === "CLOSED"); + if (mode.indexOf("priority-") === 0) { const pr = mode.slice(9).toUpperCase(); return visible.some((t) => String(t.priority).toUpperCase() === pr); } + return true; +}; + exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => { const list = Array.isArray(tasks) ? tasks : [tasks]; const currentFilter = filter || "all"; - const title = - currentFilter === "mine" ? i18n.taskMineSectionTitle : - currentFilter === "create" ? i18n.taskCreateSectionTitle : - currentFilter === "edit" ? i18n.taskUpdateSectionTitle : - currentFilter === "open" ? i18n.taskOpenTitle : - currentFilter === "in-progress" ? i18n.taskInProgressTitle : - currentFilter === "closed" ? i18n.taskClosedTitle : - currentFilter === "assigned" ? i18n.taskAssignedTitle : - currentFilter === "priority-urgent" ? i18n.taskFilterUrgent : - currentFilter === "priority-high" ? i18n.taskFilterHigh : - currentFilter === "priority-medium" ? i18n.taskFilterMedium : - currentFilter === "priority-low" ? i18n.taskFilterLow : - i18n.taskAllSectionTitle; + const title = i18n.tasksTitle; const canSee = (t) => { const vis = String(t.isPublic || "").toUpperCase(); @@ -254,7 +208,8 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => { const visible = list.filter(canSee); let filtered; - if (currentFilter === "mine") filtered = visible.filter((t) => t.author === userId); + if (currentFilter === "recent") filtered = visible.filter((t) => (Date.parse(t.createdAt || "") || 0) >= Date.now() - 86400000); + else if (currentFilter === "mine") filtered = visible.filter((t) => t.author === userId); else if (currentFilter === "assigned") filtered = visible.filter((t) => safeArray(t.assignees).includes(userId)); else if (currentFilter === "open") filtered = visible.filter((t) => normalizeStatus(t.status) === "OPEN"); else if (currentFilter === "in-progress") filtered = visible.filter((t) => normalizeStatus(t.status) === "IN-PROGRESS"); @@ -268,18 +223,21 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => { filtered = filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); const editTask = list.find((t) => t.id === taskId) || {}; - const editTags = Array.isArray(editTask.tags) ? editTask.tags : []; + const formData = currentFilter === "edit" ? editTask : (params.draft || {}); + const editTags = Array.isArray(formData.tags) ? formData.tags : (typeof formData.tags === "string" ? formData.tags.split(",").map(t => t.trim()).filter(Boolean) : []); const minCreate = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm"); const ret = typeof returnTo === "string" && returnTo.startsWith("/tasks") ? returnTo : "/tasks?filter=mine"; + const emptyMod = moduleIsEmpty(filtered, currentFilter, "all", params.q); + const taskChipVisible = taskChipFor(currentFilter, visible); return template( title, section( div( - { class: "tags-header" }, + { class: "tags-header module-header-line" }, h2(i18n.tasksTitle), p(i18n.tasksDescription) ), @@ -287,78 +245,95 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => { { class: "filters" }, form( { method: "GET", action: "/tasks" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMine), - button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAssigned), - button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterOpen), - button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterInProgress), - button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterClosed), - button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterLow), - button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMedium), - button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterHigh), - button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterUrgent), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAll).toUpperCase()), + ...(taskChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: currentFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterRecent).toUpperCase())] : []), + ...(taskChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMine).toUpperCase())] : []), + ...(taskChipVisible("assigned") ? [button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAssigned).toUpperCase())] : []), + ...(taskChipVisible("open") ? [button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterOpen).toUpperCase())] : []), + ...(taskChipVisible("in-progress") ? [button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterInProgress).toUpperCase())] : []), + ...(taskChipVisible("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterClosed).toUpperCase())] : []), + ...(taskChipVisible("priority-low") ? [button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterLow).toUpperCase())] : []), + ...(taskChipVisible("priority-medium") ? [button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMedium).toUpperCase())] : []), + ...(taskChipVisible("priority-high") ? [button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterHigh).toUpperCase())] : []), + ...(taskChipVisible("priority-urgent") ? [button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterUrgent).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.taskCreateButton) ) - ) + ), + currentFilter === "edit" || currentFilter === "create" + ? null + : emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, t => normalizeStatus(t.status || 'OPEN'), [{ value: 'OPEN', label: i18n.taskStatusOpen }, { value: 'IN-PROGRESS', label: i18n.taskStatusInProgress }, { value: 'CLOSED', label: i18n.taskStatusClosed }]), + form({ method: "GET", action: "/tasks", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: currentFilter }), + input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.taskSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) ), section( currentFilter === "edit" || currentFilter === "create" ? div( { class: "task-form" }, + currentFilter === "edit" ? await renderSpreadEditWarning(taskId) : null, form( { action: currentFilter === "edit" ? `/tasks/update/${encodeURIComponent(taskId)}` : "/tasks/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: ret }), label(i18n.taskTitleLabel), br(), - input({ type: "text", name: "title", required: true, value: currentFilter === "edit" ? (editTask.title || "") : "" }), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, value: formData.title || params.prefillTitle || "" }), br(), label(i18n.taskDescriptionLabel), br(), - textarea({ name: "description", required: true, placeholder: i18n.taskDescriptionPlaceholder, rows: "4" }, currentFilter === "edit" ? (editTask.description || "") : ""), br(), - label(i18n.uploadMedia), br(), - input({ type: "file", name: "image", accept: "image/*" }), br(),br(), + textarea({ maxlength: "5000", name: "description", required: true, placeholder: i18n.taskDescriptionPlaceholder, rows: "4" }, formData.description || params.prefillDescription || ""), br(), + ...renderGalleryFields(formData, currentFilter === "edit"), + br(), label(i18n.taskStartTimeLabel), br(), input({ type: "datetime-local", name: "startTime", required: true, - min: currentFilter === "create" ? minCreate : undefined, - value: currentFilter === "edit" && editTask.startTime ? moment(editTask.startTime).format("YYYY-MM-DDTHH:mm") : "" + min: minCreate, + value: formData.startTime ? moment(formData.startTime).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), label(i18n.taskEndTimeLabel), br(), input({ type: "datetime-local", name: "endTime", required: true, - min: currentFilter === "create" ? minCreate : undefined, - value: currentFilter === "edit" && editTask.endTime ? moment(editTask.endTime).format("YYYY-MM-DDTHH:mm") : "" + min: minCreate, + value: formData.endTime ? moment(formData.endTime).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), label(i18n.taskPriorityLabel), br(), select( { name: "priority", required: true }, - opt("URGENT", String(editTask.priority || "").toUpperCase() === "URGENT", i18n.taskPriorityUrgent), - opt("HIGH", String(editTask.priority || "").toUpperCase() === "HIGH", i18n.taskPriorityHigh), - opt("MEDIUM", String(editTask.priority || "").toUpperCase() === "MEDIUM", i18n.taskPriorityMedium), - opt("LOW", !editTask.priority || String(editTask.priority || "").toUpperCase() === "LOW", i18n.taskPriorityLow) + opt("URGENT", String(formData.priority || "").toUpperCase() === "URGENT", i18n.taskPriorityUrgent), + opt("HIGH", String(formData.priority || "").toUpperCase() === "HIGH", i18n.taskPriorityHigh), + opt("MEDIUM", String(formData.priority || "").toUpperCase() === "MEDIUM", i18n.taskPriorityMedium), + opt("LOW", !formData.priority || String(formData.priority || "").toUpperCase() === "LOW", i18n.taskPriorityLow) ), br(), br(), label(i18n.taskLocationLabel), br(), - input({ type: "text", name: "location", value: editTask.location || "" }), br(), + input({ type: "text", name: "location", value: formData.location || "" }), br(), label(i18n.taskTagsLabel), br(), input({ type: "text", name: "tags", value: editTags.join(", ") }), br(), label(i18n.taskVisibilityLabel), br(), select( { name: "isPublic", id: "isPublic" }, - opt("PUBLIC", String(editTask.isPublic || "PUBLIC").toUpperCase() === "PUBLIC", i18n.taskPublic), - opt("PRIVATE", String(editTask.isPublic || "").toUpperCase() === "PRIVATE", i18n.taskPrivate) + opt("PUBLIC", String(formData.isPublic || "PUBLIC").toUpperCase() === "PUBLIC", i18n.taskPublic), + opt("PRIVATE", String(formData.isPublic || "").toUpperCase() === "PRIVATE", i18n.taskPrivate) ), br(), br(), button({ type: "submit" }, currentFilter === "edit" ? i18n.taskUpdateButton : i18n.taskCreateButton) ) ) : filtered.length > 0 - ? div({ class: "jobs-grid" }, filtered.map((t) => renderTaskItem(t, currentFilter, params.spreadMap && params.spreadMap.get(t.id)))) - : p(i18n.notasks) + ? div({ class: "jobs-grid" }, paged(filtered).map((t) => renderTaskItem(t, currentFilter, params.spreadMap && params.spreadMap.get(t.id)))) + : div({ class: "no-content-box" }, p(i18n.notasks)) ) ); }; exports.singleTaskView = async (task, filter, comments = [], params = {}) => { + const taskChipVisible = (mode) => taskChipFor(currentFilter, Array.isArray(params.censusList) ? params.censusList : null)(mode); const currentFilter = filter || "all"; const assignees = safeArray(task.assignees); const commentCount = typeof task.commentCount === "number" ? task.commentCount : 0; @@ -371,16 +346,17 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { { class: "filters" }, form( { method: "GET", action: "/tasks" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMine), - button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAssigned), - button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterOpen), - button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterInProgress), - button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterClosed), - button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterLow), - button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMedium), - button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterHigh), - button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterUrgent), + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAll).toUpperCase()), + ...(taskChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: currentFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterRecent).toUpperCase())] : []), + ...(taskChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMine).toUpperCase())] : []), + ...(taskChipVisible("assigned") ? [button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAssigned).toUpperCase())] : []), + ...(taskChipVisible("open") ? [button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterOpen).toUpperCase())] : []), + ...(taskChipVisible("in-progress") ? [button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterInProgress).toUpperCase())] : []), + ...(taskChipVisible("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterClosed).toUpperCase())] : []), + ...(taskChipVisible("priority-low") ? [button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterLow).toUpperCase())] : []), + ...(taskChipVisible("priority-medium") ? [button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMedium).toUpperCase())] : []), + ...(taskChipVisible("priority-high") ? [button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterHigh).toUpperCase())] : []), + ...(taskChipVisible("priority-urgent") ? [button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterUrgent).toUpperCase())] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.taskCreateButton) ) ); @@ -401,8 +377,8 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { const ecoTaxChipNode = renderEcoTax(task.msgSize, task.id); const chips = [ - renderTaskStatusChip(task.status), renderPrivacyChip(isPrivate, i18n), + renderTaskStatusChip(task.status), renderTaskPriorityChip(task.priority), lifespanChipNode, ecoTaxChipNode @@ -421,15 +397,8 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { : null; const sideActions = []; - if (task.author && task.author !== userId) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: task.author }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } const assignNode = renderTaskAssignAction(task, isAssignedToMe, returnToSelf); - if (assignNode) sideActions.push(assignNode); - if (isAuthor) sideActions.push(...renderTaskOwnerActions(task, returnToSelf)); + const ownerActions = isAuthor ? renderTaskOwnerActions(task, returnToSelf) : []; const infoRows = []; const pushRow = (labelText, valueNode) => @@ -444,19 +413,25 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { if (task.location && String(task.location).trim()) pushRow(i18n.taskLocationLabel, task.location); const taskSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(task.id, `/tasks/${encodeURIComponent(task.id)}`, { spread: params.spreads || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title, deleteAction: isAuthor ? contentDeleteAction('task', task.id) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, task.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(task.id, params.spreads)), - task.image ? renderTaskMediaBlob(task.image, { class: "tribe-detail-image" }) : null, - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + renderPhotoGallery(task, 'task'), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(task.createdAt, task.author)), + assignNode ? div({ class: "tribe-side-actions housing-status-row task-assign-row" }, ...assignNode) : null, + isAuthor ? renderTaskStatusRow(task, returnToSelf) : null, tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.taskAssignedTo}: ${assignees.length}`) ), assigneesListNode, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + renderDocumentActions('tasks', task.id) ); const returnToOpinions = `/tasks/${encodeURIComponent(task.id)}?filter=${encodeURIComponent(currentFilter)}`; @@ -466,20 +441,16 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { task.description ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.taskDescriptionLabel), - p({ class: "tribe-side-description" }, ...renderUrl(task.description)) + p({ class: "tribe-side-description" }, ...renderStyledText(task.description)) ) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(task.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(task.author) - ), - opinionsBar, - renderTaskCommentsSection(task.id, comments, currentFilter) + renderEngagement(task.id, opinionsBar, renderTaskCommentsSection(task.id, comments, currentFilter)) ); return template( task.title, section( + div({ class: "tags-header module-header-line" }, h2(i18n.tasksTitle), p(i18n.tasksDescription)), filterBar, div({ class: "tribe-details" }, taskSide, taskMain) ) diff --git a/src/views/torrents_view.js b/src/views/torrents_view.js index dad95bc..7f50d31 100644 --- a/src/views/torrents_view.js +++ b/src/views/torrents_view.js @@ -1,28 +1,11 @@ -const { - form, - button, - div, - h2, - p, - section, - input, - br, - a, - span, - textarea, - select, - label, - option, - table, - tr, - th, - td -} = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, h3, p, section, input, br, a, span, textarea, select, label, option, table, tr, th, td, progress, strong } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip , renderSpreadEditWarning, renderContentActions, renderModuleStats, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, renderTorrentSourceDownload, contentDeleteAction, paged } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const userId = config.keys.id; @@ -49,21 +32,6 @@ const renderTags = (tags) => { : null; }; -const renderTorrentFavoriteToggle = (torrentObj, returnTo = "") => - form( - { - method: "POST", - action: torrentObj.isFavorite - ? `/torrents/favorites/remove/${encodeURIComponent(torrentObj.key)}` - : `/torrents/favorites/add/${encodeURIComponent(torrentObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - torrentObj.isFavorite ? i18n.torrentRemoveFavoriteButton : i18n.torrentAddFavoriteButton - ) - ); - const renderTorrentOwnerActions = (filter, torrentObj, params = {}) => { const returnTo = buildReturnTo(filter, params); const isAuthor = String(torrentObj.author) === String(userId); @@ -81,75 +49,22 @@ const renderTorrentOwnerActions = (filter, torrentObj, params = {}) => { ) ); } - items.push( - form( - { method: "POST", action: `/torrents/delete/${encodeURIComponent(torrentObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.torrentDeleteButton) - ) - ); return items; }; const renderTorrentCommentsSection = (torrentId, comments = [], returnTo = null) => { - const list = safeArr(comments); - const commentsCount = list.length; + return renderSharedCommentsSection({ + action: `/torrents/${encodeURIComponent(torrentId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/torrents/${encodeURIComponent(torrentId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - const content = c?.value?.content || {}; - const rootId = content.fork || content.root || null; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); +const blobSha256Hex = (blobId) => { + const m = /^&([A-Za-z0-9+/=]+)\.sha256$/.exec(String(blobId || "")); + if (!m) return ""; + try { return Buffer.from(m[1], "base64").toString("hex"); } catch (_) { return ""; } }; const formatSize = (bytes) => { @@ -160,8 +75,16 @@ const formatSize = (bytes) => { return (n / (1024 * 1024)).toFixed(1) + " MB"; }; +const spreadInfoOf = (t, spreadMap) => (spreadMap instanceof Map && spreadMap.get(t.key)) || null; +const renderTorrentSeeds = (t, spreadMap) => { + const info = spreadInfoOf(t, spreadMap); + return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0)); +}; +const renderTorrentSpread = (t, spreadMap) => String(t.author) === String(userId) ? "—" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false); + const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); + const extraColumns = typeof params.rowFor === "function"; if (!torrents.length) return p(params.q ? i18n.torrentNoMatch : i18n.noTorrents); @@ -169,43 +92,51 @@ const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, param { border: "1", class: "torrent-table" }, tr( th(i18n.createdAt || "DATE"), - th(i18n.authorLabel || "AUTHOR"), th(i18n.torrentTitleLabel || "TITLE"), th(i18n.torrentSizeLabel || "SIZE"), + th({ class: "torrent-spread-cell" }, i18n.seedsLabel), + th({ class: "torrent-spread-cell" }, i18n.seedAction), th(""), - th("") + ...(extraColumns ? [th(""), th("")] : []) ), - torrents.map((t) => - tr( - td(moment(t.createdAt).format("YYYY/MM/DD HH:mm")), - td(userLink(t.author)), - td(t.title || ""), - td(formatSize(t.size)), + torrents.map((t) => { + const row = typeof params.rowFor === "function" ? params.rowFor(t) : null; + return tr( + td({ class: "row-chips" }, t.tribeOrigin ? renderTribeOriginChip(t.tribeOrigin) : t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('torrents', t.title, t.key)) : null, ...(row && Array.isArray(row.titleChips) ? row.titleChips.filter(Boolean) : [])), + td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")), + td({ class: "row-title", "data-label": i18n.torrentTitleLabel || "TITLE" }, t.title || ""), + td({ "data-label": i18n.torrentSizeLabel || "SIZE" }, formatSize(t.size)), + td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, t.tribeOrigin || row ? "" : renderTorrentSeeds(t, params.spreadMap)), + td({ class: "torrent-spread-cell", "data-label": i18n.seedAction }, t.tribeOrigin || row ? "" : renderTorrentSpread(t, params.spreadMap)), td( - form( - { method: "GET", action: `/torrents/${encodeURIComponent(t.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.viewDetails) - ) + row + ? (row.details || "") + : t.tribeOrigin + ? a({ href: t.tribeOrigin.href, class: "filter-btn" }, i18n.torrentDetailsButton) + : form( + { method: "GET", action: `/torrents/${encodeURIComponent(t.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "filter", value: filter || "all" }), + params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, + params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, + button({ type: "submit", class: "filter-btn" }, i18n.torrentDetailsButton) + ) ), - td( - t.url && t.url.startsWith("&") - ? a({ href: `/blob/${encodeURIComponent(t.url)}`, class: "filter-btn" }, i18n.torrentDownloadButton || "DOWNLOAD IT!") - : "" - ) - ) - ) + ...(extraColumns ? [td(row ? (row.download || "") : ""), td({ class: "row-actions" }, row ? (row.actions || "") : "")] : []) + ); + }) ); }; const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo("all", params); const tribeId = safeText(params.tribeId || ""); + const fromBlob = filter === "edit" ? "" : safeText(params.fromBlob || ""); + const fromName = safeText(params.fromName || ""); + const clearnetAllowed = !tribeId && !torrentToEdit?.tribeId && !torrentToEdit?.encrypted && !torrentToEdit?.cipher; return div( { class: "div-center audio-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/torrents/update/${encodeURIComponent(torrentId)}` : "/torrents/create", @@ -214,18 +145,29 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => { }, input({ type: "hidden", name: "returnTo", value: returnTo }), tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, - span(i18n.torrentFileLabel), - br(), - input({ type: "file", name: "torrent", accept: ".torrent", required: filter !== "edit" }), - br(), - br(), + fromBlob + ? div({ class: "torrent-from-notice" }, + input({ type: "hidden", name: "fromBlob", value: fromBlob }), + input({ type: "hidden", name: "fromName", value: fromName }), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, fromName || fromBlob.slice(0, 16))), + tr(td({ class: "tribe-info-label" }, i18n.torrentSizeLabel), td({ class: "tribe-info-value" }, formatSize(Number(params.fromSize) || 0))), + tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(fromBlob)))) + ) + ) + : null, + fromBlob ? null : span(i18n.torrentFileLabel), + fromBlob ? null : br(), + fromBlob ? null : input({ type: "file", name: "torrent", accept: ".torrent", required: filter !== "edit" }), + fromBlob ? null : br(), + fromBlob ? null : br(), span(i18n.torrentTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.torrentTitlePlaceholder, value: torrentToEdit?.title || "", required: true }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.torrentTitlePlaceholder, value: torrentToEdit?.title || fromName || "", required: true }), br(), span(i18n.torrentDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.torrentDescriptionPlaceholder, rows: "4" }, torrentToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.torrentDescriptionPlaceholder, rows: "4" }, torrentToEdit?.description || ""), br(), span(i18n.torrentTagsLabel), br(), @@ -237,60 +179,128 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => { }), br(), br(), + ...(clearnetAllowed ? [renderClearnetSelector(filter === "edit" ? !!torrentToEdit?.clearnet : false, i18n), br()] : []), button({ type: "submit" }, filter === "edit" ? i18n.torrentUpdateButton : i18n.torrentCreateButton) ) ); }; +const torrentStateLabel = (state) => { + const map = { queued: i18n.torrentStateQueued, probing: i18n.torrentStateProbing, downloading: i18n.torrentStateDownloading, done: i18n.torrentStateDone, failed: i18n.torrentStateFailed, nosource: i18n.torrentStateNoSource, cancelled: i18n.torrentStateCancelled }; + return map[state] || String(state || ""); +}; + +const sourceSaveHref = (blobId, name) => `/blob/${encodeURIComponent(blobId)}?download=1&name=${encodeURIComponent(String(name || "download"))}`; + +const renderDownloadRow = (d) => { + const pct = d.size > 0 ? Math.min(100, Math.floor((d.received / d.size) * 100)) : (d.state === "done" ? 100 : 0); + const titleNode = d.torrentKey ? a({ href: `/torrents/${encodeURIComponent(d.torrentKey)}` }, d.name || d.blobId.slice(0, 12)) : span(d.name || d.blobId.slice(0, 12)); + const actions = d.state === "done" + ? a({ href: sourceSaveHref(d.blobId, d.name), class: "filter-btn" }, i18n.torrentSaveFile) + : d.active + ? form({ method: "POST", action: `/torrents/downloads/${encodeURIComponent(d.blobId)}/cancel` }, button({ type: "submit", class: "filter-btn" }, i18n.torrentCancel)) + : form({ method: "POST", action: `/torrents/downloads/${encodeURIComponent(d.blobId)}/remove` }, button({ type: "submit", class: "filter-btn" }, "✕")); + return tr({ class: "torrent-download-row torrent-state-" + d.state }, + td(titleNode), + td(d.size ? formatSize(d.size) : "—"), + td(String(d.seeds || 0)), + td({ class: "torrent-progress-cell" }, progress({ value: String(pct), max: "100", class: "torrent-progress" }), span({ class: "torrent-progress-text" }, ` ${pct}%`)), + td(torrentStateLabel(d.state)), + td(actions) + ); +}; + +const renderDownloads = (downloads) => { + const list = safeArr(downloads); + if (!list.length) return p(i18n.torrentNoDownloads); + return table( + { border: "1", class: "torrent-table torrent-downloads" }, + tr( + th(i18n.torrentSourceFile), + th(i18n.torrentSizeLabel), + th(i18n.torrentSeeds), + th(""), + th(""), + th("") + ), + ...list.map(renderDownloadRow) + ); +}; + +const renderOasisDownload = (torrentObj, params = {}) => { + if (!torrentObj.source || params.isPublic) return null; + const d = params.download || null; + const rows = [ + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, torrentObj.sourceName || torrentObj.source.slice(0, 16))), + tr(td({ class: "tribe-info-label" }, i18n.torrentSizeLabel), td({ class: "tribe-info-value" }, formatSize(torrentObj.size))), + tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(torrentObj.source)))) + ]; + let action; + if (d && d.active) { + const pct = d.size > 0 ? Math.min(100, Math.floor((d.received / d.size) * 100)) : 0; + action = div({ class: "torrent-progress-cell" }, + progress({ value: String(pct), max: "100", class: "torrent-progress" }), + span({ class: "torrent-progress-text" }, ` ${pct}% · ${torrentStateLabel(d.state)} · ${d.seeds || 0} ${i18n.torrentSeeds}`), + br(), + a({ href: "/torrents?filter=downloads", class: "filter-btn" }, i18n.torrentDownloads) + ); + } else { + action = renderTorrentSourceDownload(torrentObj.key, torrentObj.source); + } + return div({ class: "torrent-oasis torrent-detail-section" }, + h3({ class: "torrent-section-title" }, i18n.torrentOriginalSection || "Original"), + table({ class: "tribe-info-table torrent-file-info" }, ...rows), + action + ); +}; + +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.torrentsView = async (torrents, filter = "all", torrentId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.torrentMineSectionTitle - : filter === "create" - ? i18n.torrentCreateSectionTitle - : filter === "edit" - ? i18n.torrentUpdateSectionTitle - : filter === "recent" - ? i18n.torrentRecentSectionTitle - : filter === "top" - ? i18n.torrentTopSectionTitle - : filter === "favorites" - ? i18n.torrentFavoritesSectionTitle - : i18n.torrentAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(torrentId) }; + const title = i18n.torrentsTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(torrents); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const torrentToEdit = torrentId ? list.find((t) => t.key === torrentId) : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.torrentsDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetTorrents); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterTop), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.torrentFilterFavorites - ), + String(i18n.torrentFilterFavorites).toUpperCase() + )] : []), + + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterTop).toUpperCase())] : []), + ...(params.isPublic || filter === "create" ? [] : [button({ type: "submit", name: "filter", value: "downloads", class: filter === "downloads" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentDownloads).toUpperCase())]), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.torrentCreateButton) ) ) @@ -298,9 +308,12 @@ exports.torrentsView = async (torrents, filter = "all", torrentId = null, params section( filter === "create" || filter === "edit" ? renderTorrentForm(filter, torrentId, torrentToEdit, { ...params, filter }) + : filter === "downloads" + ? section(renderDownloads(params.downloads)) : section( - div( - { class: "audios-search" }, + emptyMod ? null : div( + { class: "audios-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/torrents", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -315,28 +328,31 @@ exports.torrentsView = async (torrents, filter = "all", torrentId = null, params { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.torrentSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.torrentSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.torrentSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.torrentSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.torrentSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.torrentSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.torrentSearchButton) ) ) ), - div({ class: "audios-list" }, renderTorrentTable(list, filter, { q, sort })) + div({ class: "audios-list" }, renderTorrentTable(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); }; exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(torrentObj.title); - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetTorrents); + const { renderEncryptedChip, renderTransportChip } = require('./clearnet_view'); + const isAuthor = String(torrentObj.author) === String(userId); + const clearnetAllowed = !torrentObj.tribeId && !torrentObj.encrypted && !torrentObj.cipher; + const isClearnet = !!torrentObj.clearnet; const chips = [ renderLifespanChip(torrentObj.lifetime, i18n), @@ -345,36 +361,54 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa const ownerActions = renderTorrentOwnerActions(filter, torrentObj, { q, sort }); const sideActions = []; - sideActions.push(renderTorrentFavoriteToggle(torrentObj, returnTo)); - if (torrentObj.author && String(torrentObj.author) !== String(userId)) { - sideActions.push(form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: torrentObj.author }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(torrentObj.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(torrentObj.key, `/torrents/${encodeURIComponent(torrentObj.key)}`, { + spreadTitle: i18n.seedAction, + author: torrentObj.author, + favKind: 'torrents', + isFavorite: torrentObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: torrentObj.title, + deleteAction: String(torrentObj.author) === String(userId) ? contentDeleteAction('torrent', torrentObj.key) : undefined + }) + ); + const torrentSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + torrentObj.tribeId && torrentObj.cipher ? renderEncryptedChip(i18n) : renderTransportChip(i18n), + clearnetAllowed ? renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('torrents', torrentObj.title, torrentObj.key) : null) : null, + clearnetAllowed && isAuthor ? renderClearnetSwitch('torrents', torrentObj.rootId || torrentObj.key, isClearnet) : null ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(torrentObj.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(torrentObj.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(torrentObj.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(torrentObj.key, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const torrentMain = div({ class: "tribe-main" }, + detailActions, torrentObj.url && torrentObj.url.startsWith("&") ? div({ class: "torrent-download" }, - a({ href: `/blob/${encodeURIComponent(torrentObj.url)}?name=${encodeURIComponent((torrentObj.title || 'download').replace(/\.torrent$/i, '') + '.torrent')}` , class: "filter-btn" }, i18n.torrentDownloadButton || "DOWNLOAD IT!") + renderOasisDownload(torrentObj, params), + div({ class: "torrent-detail-section" }, + h3({ class: "torrent-section-title" }, "Torrent"), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, `${String(torrentObj.title || "download").replace(/\.torrent$/i, "")}.torrent`)), + torrentObj.source ? null : tr(td({ class: "tribe-info-label" }, i18n.torrentSizeLabel), td({ class: "tribe-info-value" }, formatSize(torrentObj.size))), + torrentObj.cipher + ? tr(td({ class: "tribe-info-label" }, i18n.encryptedChipLabel || "E2E"), td({ class: "tribe-info-value" }, "AES-256-GCM")) + : tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(torrentObj.url)))) + ), + renderTorrentDownload(torrentObj.cipher ? `/torrents/${encodeURIComponent(torrentObj.key)}/file` : torrentDownloadHref(torrentObj.url, torrentObj.title)) + ) ) : p(i18n.torrentNoFile), (() => { @@ -384,41 +418,42 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(torrentObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(torrentObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(torrentObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.torrentUpdatedAt}: ${moment(torrentObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.torrentUpdatedAt}: ${moment(torrentObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants), - renderTorrentCommentsSection(torrentObj.key, comments, returnTo) + torrentObj.tribeId ? null : renderEngagement(torrentObj.key, + renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants), + renderTorrentCommentsSection(torrentObj.key, comments, returnTo) + ) ); return template( i18n.torrentsTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.torrentAllSectionTitle || i18n.torrentsTitle), - p(i18n.torrentDescription) + p(i18n.torrentsDescription) ), div( { class: "filters" }, form( { method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterTop), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterMine).toUpperCase())] : []), { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.torrentFilterFavorites - ), + String(i18n.torrentFilterFavorites).toUpperCase() + )] : []), + + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.torrentCreateButton) ) ), diff --git a/src/views/transfer_view.js b/src/views/transfer_view.js index e3c3de5..f712fe6 100644 --- a/src/views/transfer_view.js +++ b/src/views/transfer_view.js @@ -1,5 +1,7 @@ -const { div, h2, p, section, button, form, a, input, br, span, label, select, option, progress, table, tr, td } = require("../server/node_modules/hyperaxe") -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views") +const { hr, div, h2, p, section, button, form, a, input, br, span, label, select, option, progress, table, tr, td } = require("../server/node_modules/hyperaxe") +const { getConfig } = require('../configs/config-manager.js'); +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") @@ -89,7 +91,7 @@ const renderBlockInfoCard = (block) => { div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.blockchainBlockID || "Block ID"}: `), span({ class: "card-value" }, - a({ href: `/blockexplorer/block/${encodeURIComponent(block.id)}` }, block.id) + process.env.OASIS_MOBILE === '1' ? block.id : a({ href: `/blockexplorer/block/${encodeURIComponent(block.id)}` }, block.id) ) ) ) @@ -116,25 +118,10 @@ const renderOwnerActions = (transfer, returnTo) => { { method: "GET", action: `/transfers/edit/${encodeURIComponent(transfer.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "update-btn" }, i18n.transfersUpdateButton) - ), - form( - { method: "POST", action: `/transfers/delete/${encodeURIComponent(transfer.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "delete-btn" }, i18n.transfersDeleteButton) ) ] } -const renderUpdatedLabel = (createdAt, updatedAt) => { - const createdTs = createdAt ? new Date(createdAt).getTime() : NaN - const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs) - return showUpdated - ? span({ class: "votations-comment-date" }, ` | ${i18n.transfersUpdatedAt}: ${moment(updatedAt).format("YYYY-MM-DD HH:mm")}`) - : null -} - - const renderTransferStatusChip = (status) => { const up = String(status || "").toUpperCase() const variant = @@ -145,12 +132,25 @@ const renderTransferStatusChip = (status) => { return renderStateChip(variant, icon, i18n[statusKey(up)] || up) } +const ubiEpochLabel = (t) => { + const tags = safeArr(t.tags).map(x => String(x)) + const fromTag = tags.find(x => /^epoch:\d{4}-\d{2}$/i.test(x)) + const epoch = fromTag ? fromTag.slice(6) : ((String(t.concept || "").match(/(\d{4}-\d{2})/) || [])[1] || "") + return epoch ? `UBI - ${epoch}` : "UBI" +} +const transferTitle = (t) => { + const tags = safeArr(t.tags).map(x => String(x).toUpperCase()) + if (tags.includes("UBI") && /UBI Payment|^UBI\b/.test(String(t.concept || ""))) return ubiEpochLabel(t) + return t.concept || i18n.transfersTitle +} + const renderTransferCategoryChip = (cat) => renderStateChip("encrypted", "", categoryLabel(cat)) const generateTransferCard = (transfer, filter, params = {}) => { const confirmedBy = safeArr(transfer.confirmedBy) - const required = transfer.from === transfer.to ? 1 : 2 + const settledUbiTx = (Array.isArray(transfer.tags) ? transfer.tags : []).some(t => String(t).toUpperCase() === "UBI") && /^[0-9a-f]{64}$/i.test(String(transfer.txid || "")) + const required = (transfer.from === transfer.to || settledUbiTx) ? Math.max(1, Math.min(confirmedBy.length, 2)) : 2 const confirmedCount = confirmedBy.length const dl = transfer.deadline ? moment(transfer.deadline) : null const isExpired = dl && dl.isValid() ? dl.isBefore(moment()) : false @@ -160,7 +160,7 @@ const generateTransferCard = (transfer, filter, params = {}) => { const chips = [ renderTransferStatusChip(transfer.status), - isUbi ? renderStateChip("mutuals", "🎁", "UBI") : null, + isUbi ? renderStateChip("mutuals", "", "UBI") : null, isExpired ? renderStateChip("closed", "⏰", i18n.transfersExpiredBadge) : null, renderLifespanChip(transfer.lifetime, i18n) ].filter(Boolean) @@ -170,59 +170,56 @@ const generateTransferCard = (transfer, filter, params = {}) => { const returnTo = buildReturnTo(filter, params) const isOwn = transfer.from && String(transfer.from) === String(userId) + const canDelete = isOwn && String(transfer.status || "").toUpperCase() === "UNCONFIRMED" && !isExpired && confirmedCount < required return div({ class: "trending-card transfer-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`) + renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, { spread: params.spreadMap && params.spreadMap.get(transfer.id) || null, author: transfer.from, favKind: 'transfers', isFavorite: transfer.isFavorite, reportTitle: transfer.concept, returnTo, deleteAction: canDelete ? contentDeleteAction('transfer', transfer.id) : undefined }) ), div({ class: "card-section transfer-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/transfers/${encodeURIComponent(transfer.id)}` }, transfer.concept || i18n.transfersTitle) + a({ href: `/transfers/${encodeURIComponent(transfer.id)}` }, transferTitle(transfer)) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, !isUbi && dl && dl.isValid() - ? p({ class: "card-date-highlight" }, dl.format("YYYY-MM-DD HH:mm")) + ? p({ class: "time-chip" }, dl.format("YYYY/MM/DD HH:mm")) : null, cat !== "TRUST" - ? div({ class: "job-price-line card-salary" }, fmtAmountWithUnit(transfer)) - : null, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.transfersConfirmations}: ${confirmedCount}/${required}`) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(transfer.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(transfer.id, params.spreadMap && params.spreadMap.get(transfer.id))) + ? div({ class: "tribe-card-members" }, span({ class: "price-chip" }, fmtAmountWithUnit(transfer))) + : null ) ) } +const isUbiTagged = (t) => safeArr(t.tags).some(tag => String(tag).toUpperCase() === "UBI") + +const trChipFor = (normalizedFilter, censusT) => (mode) => { + if (mode === normalizedFilter) return true + if (!Array.isArray(censusT)) return true; + const stT = (t) => String(t.status || "").toUpperCase() + if (mode === "mine") return censusT.some(t => t.from === userId || t.to === userId) + if (mode === "recent") return censusT.length > 0 + if (mode === "mine-pending") return censusT.some(t => (t.from === userId || t.to === userId) && String(t.status || "").toUpperCase() === "UNCONFIRMED") + if (mode === "mine-confirmed") return censusT.some(t => (t.from === userId || t.to === userId) && String(t.status || "").toUpperCase() === "CLOSED") + if (mode === "ubi") return censusT.some(t => safeArr(t.tags).some(tag => String(tag).toUpperCase() === "UBI")) + if (mode === "economic") return censusT.some(t => categoryOf(t) === "ECONOMIC") + if (mode === "time") return censusT.some(t => categoryOf(t) === "TIME") + if (mode === "trust") return censusT.some(t => categoryOf(t) === "TRUST") + if (mode === "market") return censusT.some(t => !isUbiTagged(t)) + if (mode === "pending") return censusT.some(t => stT(t) === "UNCONFIRMED" && t.to === userId && !safeArr(t.confirmedBy).includes(userId)) + if (mode === "unconfirmed") return censusT.some(t => stT(t) === "UNCONFIRMED") + if (mode === "closed" || mode === "top") return censusT.some(t => stT(t) === "CLOSED") + if (mode === "discarded") return censusT.some(t => stT(t) === "DISCARDED") + return true +} + exports.transferView = async (transfers, filter, transferId, params = {}) => { const normalizedFilter = filter === "favs" ? "all" : (filter || "all") - const title = - normalizedFilter === "mine" ? i18n.transfersMineSectionTitle : - normalizedFilter === "ubi" ? i18n.transfersUBISectionTitle : - normalizedFilter === "pending" ? i18n.transfersPendingSectionTitle : - normalizedFilter === "top" ? i18n.transfersTopSectionTitle : - normalizedFilter === "unconfirmed" ? i18n.transfersUnconfirmedSectionTitle : - normalizedFilter === "closed" ? i18n.transfersClosedSectionTitle : - normalizedFilter === "discarded" ? i18n.transfersDiscardedSectionTitle : - normalizedFilter === "create" ? i18n.transfersCreateSectionTitle : - normalizedFilter === "edit" ? i18n.transfersUpdateSectionTitle : - normalizedFilter === "economic" ? (i18n.transfersEconomicSectionTitle || (i18n.transfersCategoryEconomic + " " + i18n.transfersTitle)) : - normalizedFilter === "time" ? (i18n.transfersTimeSectionTitle || (i18n.transfersCategoryTime + " " + i18n.transfersTitle)) : - normalizedFilter === "trust" ? (i18n.transfersTrustSectionTitle || (i18n.transfersCategoryTrust + " " + i18n.transfersTitle)) : - i18n.transfersAllSectionTitle + const title = i18n.transfersTitle const q = safeText(params.q || "") const minAmountRaw = params.minAmount ?? "" @@ -234,7 +231,10 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { const list = safeArr(transfers) let filtered = + normalizedFilter === "recent" ? list.filter(t => (Date.parse(t.createdAt || "") || 0) >= Date.now() - 86400000) : normalizedFilter === "mine" ? list.filter(t => t.from === userId || t.to === userId) : + normalizedFilter === "mine-pending" ? list.filter(t => (t.from === userId || t.to === userId) && String(t.status || "").toUpperCase() === "UNCONFIRMED") : + normalizedFilter === "mine-confirmed" ? list.filter(t => (t.from === userId || t.to === userId) && String(t.status || "").toUpperCase() === "CLOSED") : normalizedFilter === "ubi" ? list.filter(t => safeArr(t.tags).some(tag => String(tag).toUpperCase() === "UBI")) : normalizedFilter === "pending" ? list.filter(t => String(t.status || "").toUpperCase() === "UNCONFIRMED" && t.to === userId && !safeArr(t.confirmedBy).includes(userId)) : normalizedFilter === "top" ? list.filter(t => String(t.status || "").toUpperCase() === "CLOSED") : @@ -244,7 +244,7 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { normalizedFilter === "economic" ? list.filter(t => categoryOf(t) === "ECONOMIC") : normalizedFilter === "time" ? list.filter(t => categoryOf(t) === "TIME") : normalizedFilter === "trust" ? list.filter(t => categoryOf(t) === "TRUST") : - normalizedFilter === "market" ? list : + normalizedFilter === "market" ? list.filter(t => !isUbiTagged(t)) : list if (q) { @@ -283,32 +283,45 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { selectedCategory === "TRUST" ? (i18n.transfersUnitTrust || "trust") : (i18n.transfersUnitEco || "ECO") + const emptyMod = moduleIsEmpty(list, normalizedFilter, "all", q || String(minAmountRaw || "") || String(maxAmountRaw || "")); + const censusT = Array.isArray(params.censusList) ? params.censusList : list + const stT = (t) => String(t.status || "").toUpperCase() + const trChip = trChipFor(normalizedFilter, censusT) return template( title, section( - div({ class: "tags-header" }, h2(title), p(i18n.transfersDescription)), + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.transfersDescription), renderWalletChip()), div( { class: "filters" }, form( { method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "minAmount", value: String(minAmountRaw ?? "") }), input({ type: "hidden", name: "maxAmount", value: String(maxAmountRaw ?? "") }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMine), - button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUBI), - button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC")), - button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME")), - button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST")), - button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMarket), - button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterPending), - button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUnconfirmed), - button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterClosed), - button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterDiscarded), - button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTop), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterAll).toUpperCase()), + ...(trChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: normalizedFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterRecent).toUpperCase())] : []), + ...(trChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMine).toUpperCase())] : []), + ...(trChip("ubi") ? [button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUBI).toUpperCase())] : []), + ...(trChip("economic") ? [button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC"))] : []), + ...(trChip("time") ? [button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME"))] : []), + ...(trChip("trust") ? [button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST"))] : []), + ...(trChip("market") ? [button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMarket).toUpperCase())] : []), + ...(trChip("pending") ? [button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterPending).toUpperCase())] : []), + ...(trChip("unconfirmed") ? [button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUnconfirmed).toUpperCase())] : []), + ...(trChip("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterClosed).toUpperCase())] : []), + ...(trChip("discarded") ? [button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterDiscarded).toUpperCase())] : []), + ...(trChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterTop).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.transfersCreateButton) - ) + ), + ["mine", "mine-pending", "mine-confirmed"].includes(normalizedFilter) + ? div({ class: "transfers-subfilters" }, + span({ class: "activity-subchip-label" }, "\u21b3"), + ...[["mine", i18n.transfersFilterAll], ["mine-pending", i18n.transfersFilterMinePending], ["mine-confirmed", i18n.transfersFilterMineConfirmed]] + .map(([mode, label]) => a({ href: `/transfers?filter=${mode}`, class: normalizedFilter === mode ? "activity-chip active" : "activity-chip" }, String(label).toUpperCase())) + ) + : null ) ), section( @@ -321,15 +334,16 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { input({ type: "hidden", name: "filter", value: "create" }), label(i18n.transfersCategory || "Category"), br(), - select( - { name: "category", class: "transfer-category-select" }, - option({ value: "ECONOMIC", selected: selectedCategory === "ECONOMIC" ? "selected" : undefined }, i18n.transfersCategoryEconomic || "Economic"), - option({ value: "TIME", selected: selectedCategory === "TIME" ? "selected" : undefined }, i18n.transfersCategoryTime || "Time"), - option({ value: "TRUST", selected: selectedCategory === "TRUST" ? "selected" : undefined }, i18n.transfersCategoryTrust || "Trust") + div({ class: "apply-row" }, + select( + { name: "category", class: "transfer-category-select" }, + option({ value: "ECONOMIC", selected: selectedCategory === "ECONOMIC" ? "selected" : undefined }, i18n.transfersCategoryEconomic || "Economic"), + option({ value: "TIME", selected: selectedCategory === "TIME" ? "selected" : undefined }, i18n.transfersCategoryTime || "Time"), + option({ value: "TRUST", selected: selectedCategory === "TRUST" ? "selected" : undefined }, i18n.transfersCategoryTrust || "Trust") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") ), - " ", - button({ type: "submit", class: "filter-btn" }, i18n.transfersCategoryApply || "Apply"), - br(), br() + hr({ class: "form-sep" }) ) : null, form( @@ -363,6 +377,7 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { label(`${i18n.transfersAmount} (${amountUnitLabel})`), br(), input({ type: "number", name: "amount", step: "0.000001", required: true, min: "0.000001", value: transferToEdit.amount || "" }), + renderEcoValueChip(), br(), br() ], label(i18n.transfersDeadline), @@ -379,19 +394,16 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { ) ) : section( - div( - { class: "transfers-search" }, + emptyMod ? null : div( + { class: "transfers-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, t => String(t.status || '').toUpperCase(), [{ value: 'UNCONFIRMED', label: i18n.transfersFilterUnconfirmed }, { value: 'CLOSED', label: i18n.transfersFilterClosed }, { value: 'DISCARDED', label: i18n.transfersFilterDiscarded }]), form( { method: "GET", action: "/transfers", class: "filter-box" }, input({ type: "hidden", name: "filter", value: normalizedFilter || "all" }), input({ type: "text", name: "q", value: q, placeholder: i18n.transfersSearchPlaceholder, class: "filter-box__input" }), - div( - { class: "filter-box__controls" }, - div( - { class: "transfer-range" }, - input({ type: "number", name: "minAmount", step: "0.000001", min: "0", value: String(minAmountRaw ?? ""), placeholder: i18n.transfersMinAmountLabel, class: "filter-box__number transfer-amount-input" }), - input({ type: "number", name: "maxAmount", step: "0.000001", min: "0", value: String(maxAmountRaw ?? ""), placeholder: i18n.transfersMaxAmountLabel, class: "filter-box__number transfer-amount-input" }) - ), + input({ type: "number", name: "minAmount", step: "0.000001", min: "0", value: String(minAmountRaw ?? ""), placeholder: i18n.transfersMinAmountLabel, class: "filter-box__number transfer-amount-input" }), + input({ type: "number", name: "maxAmount", step: "0.000001", min: "0", value: String(maxAmountRaw ?? ""), placeholder: i18n.transfersMaxAmountLabel, class: "filter-box__number transfer-amount-input" }), + select( { name: "sort", class: "filter-box__select" }, option({ value: "recent", selected: sort === "recent" ? "selected" : undefined }, i18n.transfersSortRecent), @@ -399,52 +411,52 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { option({ value: "deadline", selected: sort === "deadline" ? "selected" : undefined }, i18n.transfersSortDeadline) ), button({ type: "submit", class: "filter-box__button" }, i18n.transfersSearchButton) - ) + ) ), - br(), filtered.length - ? div({ class: "jobs-grid" }, - filtered.map(t => generateTransferCard(t, normalizedFilter, { q, minAmount: minAmountRaw, maxAmount: maxAmountRaw, sort, spreadMap: params.spreadMap })) + ? div({ class: "jobs-grid transfers-grid" }, + paged(filtered).map(t => generateTransferCard(t, normalizedFilter, { q, minAmount: minAmountRaw, maxAmount: maxAmountRaw, sort, spreadMap: params.spreadMap })) ) - : p(q || String(minAmountRaw) || String(maxAmountRaw) ? i18n.transfersNoMatch : i18n.transfersNoItems) + : div({ class: "no-content-box" }, p(q || String(minAmountRaw) || String(maxAmountRaw) ? i18n.transfersNoMatch : i18n.transfersNoItems)) ) ) ) } exports.singleTransferView = async (transfer, filter, params = {}) => { + const trChip = (mode) => trChipFor(normalizedFilter, Array.isArray(params.censusList) ? params.censusList : null)(mode) const normalizedFilter = filter === "favs" ? "all" : (filter || "all") const q = safeText(params.q || "") const sort = safeText(params.sort || "recent") const returnTo = safeText(params.returnTo) || buildReturnTo(normalizedFilter, { ...params, q, sort }) const confirmedBy = safeArr(transfer.confirmedBy) - const required = transfer.from === transfer.to ? 1 : 2 + const settledUbiTx = (Array.isArray(transfer.tags) ? transfer.tags : []).some(t => String(t).toUpperCase() === "UBI") && /^[0-9a-f]{64}$/i.test(String(transfer.txid || "")) + const required = (transfer.from === transfer.to || settledUbiTx) ? Math.max(1, Math.min(confirmedBy.length, 2)) : 2 const confirmedCount = confirmedBy.length const isUnconfirmed = String(transfer.status || "").toUpperCase() === "UNCONFIRMED" const dl = transfer.deadline ? moment(transfer.deadline) : null const isExpired = dl && dl.isValid() ? dl.isBefore(moment()) : false const tags = Array.isArray(transfer.tags) ? transfer.tags.map(t => String(t).toUpperCase()) : [] const isUbi = tags.includes("UBI") - const showConfirm = isUnconfirmed && transfer.to === userId && !confirmedBy.includes(userId) && !isExpired && !tags.includes("PENDING") + const settledUbi = isUbi && /^[0-9a-f]{64}$/i.test(String(transfer.txid || "")) + const showConfirm = isUnconfirmed && transfer.to === userId && !confirmedBy.includes(userId) && !isExpired && !tags.includes("PENDING") && !settledUbi const tagsNode = renderTags(transfer.tags) const cat = categoryOf(transfer) const otherParty = transfer.from === userId ? transfer.to : transfer.from const chips = [ renderTransferStatusChip(transfer.status), - isUbi ? renderStateChip("mutuals", "🎁", "UBI") : null, + isUbi ? renderStateChip("mutuals", "", "UBI") : null, isExpired ? renderStateChip("closed", "⏰", i18n.transfersExpiredBadge) : null, renderLifespanChip(transfer.lifetime, i18n) ].filter(Boolean) const sideActions = [] - if (otherParty && String(otherParty) !== String(userId)) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: otherParty }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )) + const isPayer = String(transfer.from) === String(userId) + if (isPayer && String(transfer.status || "").toUpperCase() === "UNCONFIRMED" && getConfig().modules.walletMod === "on") { + sideActions.push(a({ href: `/wallet/send?transfer=${encodeURIComponent(transfer.id)}`, class: "filter-btn" }, i18n.transfersPayWithWallet)) } if (showConfirm) { sideActions.push(form({ method: "POST", action: `/transfers/confirm/${encodeURIComponent(transfer.id)}` }, @@ -458,14 +470,16 @@ exports.singleTransferView = async (transfer, filter, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "update-btn" }, i18n.transfersUpdateButton) )) - sideActions.push(form({ method: "POST", action: `/transfers/delete/${encodeURIComponent(transfer.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "delete-btn" }, i18n.transfersDeleteButton) - )) } - sideActions.push(form({ method: "GET", action: `/transfers/contract/${encodeURIComponent(transfer.id)}`, class: "transfer-contract-form" }, - button({ type: "submit", class: "filter-btn" }, i18n.transfersExportContract || 'Create Contract') - )) + const canDelete = canEdit && !isExpired && confirmedCount < required + const exportActions = div({ class: "doc-export-actions" }, + form({ method: "GET", action: `/transfers/contract/${encodeURIComponent(transfer.id)}`, class: "transfer-contract-form" }, + button({ type: "submit", class: "filter-btn" }, i18n.transfersExportContract) + ), + form({ method: "POST", action: `/transfers/${encodeURIComponent(transfer.id)}/share` }, + button({ type: "submit", class: "filter-btn" }, i18n.sharePm) + ) + ) const infoRows = [] const pushRow = (labelText, valueNode) => @@ -477,15 +491,26 @@ exports.singleTransferView = async (transfer, filter, params = {}) => { pushRow(i18n.transfersTo, userLink(transfer.to)) if (cat !== "TRUST") pushRow(i18n.transfersAmount, fmtAmountWithUnit(transfer)) pushRow(i18n.transfersCategory || "Category", categoryLabel(cat)) - if (!isUbi) pushRow(i18n.transfersDeadline, dl && dl.isValid() ? dl.format("YYYY-MM-DD HH:mm") : "") + if (!isUbi) pushRow(i18n.transfersDeadline, dl && dl.isValid() ? dl.format("YYYY/MM/DD HH:mm") : "") pushRow(i18n.transfersStatus, i18n[statusKey(transfer.status)] || String(transfer.status || "")) const transferSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, { + spread: params.spreads || null, + author: transfer.from, + favKind: 'transfers', + isFavorite: transfer.isFavorite, + returnTo, + reportTitle: transfer.concept, + deleteAction: canDelete ? contentDeleteAction('transfer', transfer.id) : undefined + }) + ), div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, transfer.concept || i18n.transfersTitle) + h2({ class: "tribe-card-title" }, transferTitle(transfer)) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(transfer.id, params.spreads)), + table({ class: "tribe-info-table" }, ...renderCreatedRows(transfer.createdAt, transfer.from, transfer.updatedAt)), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.transfersConfirmations}: ${confirmedCount}/${required}`) @@ -493,74 +518,61 @@ exports.singleTransferView = async (transfer, filter, params = {}) => { params.block && params.block.id ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.blockchainBlockID || "Block ID"}: `), - span({ class: "card-value" }, a({ class: "user-link", href: `/blockexplorer/block/${encodeURIComponent(params.block.id)}` }, params.block.id)) + span({ class: "card-value" }, process.env.OASIS_MOBILE === '1' ? params.block.id : a({ class: "user-link", href: `/blockexplorer/block/${encodeURIComponent(params.block.id)}` }, params.block.id)) ) : null, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + exportActions ) + const confirmedList = Array.isArray(transfer.confirmedBy) ? transfer.confirmedBy.filter(Boolean) : [] + const transferMain = div({ class: "tribe-main" }, div({ class: "job-section" }, - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersFrom}: `), - span({ class: "card-value" }, userLink(transfer.from)) - ), - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersTo}: `), - span({ class: "card-value" }, userLink(transfer.to)) - ), - cat !== "TRUST" ? div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersAmount}: `), - span({ class: "card-value card-salary" }, fmtAmountWithUnit(transfer)) - ) : null, - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersCategory || "Category"}: `), - span({ class: "card-value" }, categoryLabel(cat)) - ), - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersConcept}: `), - span({ class: "card-value" }, transfer.concept || "") - ), - !isUbi && dl && dl.isValid() ? div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersDeadline}: `), - span({ class: "card-value" }, dl.format("YYYY-MM-DD HH:mm")) - ) : null, - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersStatus}: `), - span({ class: "card-value" }, i18n[statusKey(transfer.status)] || String(transfer.status || "")) - ) + h2({ class: "job-section-title" }, i18n.transfersConcept), + p({ class: "tribe-side-description" }, transfer.concept || "") ), - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(transfer.createdAt).format("YYYY-MM-DD HH:mm")} ${i18n.performed} `), - userLink(transfer.from), - renderUpdatedLabel(transfer.createdAt, transfer.updatedAt) + div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.transfersConfirmations), + p({ class: "tribe-side-description" }, `${confirmedCount}/${required}`), + confirmedList.length + ? div({ class: "card-assigned-list" }, ...confirmedList.map(id => userLink(id))) + : null ), - renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants) + renderEngagement(transfer.id, + renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants), + renderSharedCommentsSection({ + action: `/transfers/${encodeURIComponent(transfer.id)}/comments`, + comments: params.comments || [], + returnTo + }) + ) ) return template( transfer.concept, + section(div({ class: "tags-header module-header-line" }, h2(i18n.transfersTitle), p(i18n.transfersDescription), renderWalletChip())), section( div( { class: "filters" }, form( { method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "minAmount", value: String(params.minAmount ?? "") }), input({ type: "hidden", name: "maxAmount", value: String(params.maxAmount ?? "") }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMine), - button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUBI), - button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC")), - button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME")), - button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST")), - button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMarket), - button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterPending), - button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUnconfirmed), - button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterClosed), - button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterDiscarded), - button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTop), + button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterAll).toUpperCase()), + ...(trChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: normalizedFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterRecent).toUpperCase())] : []), + ...(trChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMine).toUpperCase())] : []), + ...(trChip("ubi") ? [button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUBI).toUpperCase())] : []), + ...(trChip("economic") ? [button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC"))] : []), + ...(trChip("time") ? [button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME"))] : []), + ...(trChip("trust") ? [button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST"))] : []), + ...(trChip("market") ? [button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMarket).toUpperCase())] : []), + ...(trChip("pending") ? [button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterPending).toUpperCase())] : []), + ...(trChip("unconfirmed") ? [button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUnconfirmed).toUpperCase())] : []), + ...(trChip("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterClosed).toUpperCase())] : []), + ...(trChip("discarded") ? [button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterDiscarded).toUpperCase())] : []), + button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.transfersCreateButton) ) ), diff --git a/src/views/trending_view.js b/src/views/trending_view.js index cbdb142..9a6a518 100644 --- a/src/views/trending_view.js +++ b/src/views/trending_view.js @@ -1,28 +1,34 @@ const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, img, video: videoHyperaxe, audio: audioHyperaxe, span, details, summary} = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderSpreadButton, renderContentActions} = require('./main_views'); -const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLink, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require('./main_views'); +const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); +const { renderZoomableImage } = require('./gallery_view'); const { config } = require('../server/SSB_server.js'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText } = require('../backend/renderStyledText'); const opinionCategories = require('../backend/opinion_categories'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); const userId = config.keys.id; +const capFirst = (s) => String(s || '').charAt(0).toUpperCase() + String(s || '').slice(1); +const typeLabel = (type) => i18n['type' + capFirst(type)] || type; + const filterButton = (mode, currentFilter) => form({ method: 'GET', action: '/trending' }, input({ type: 'hidden', name: 'filter', value: mode }), button( { type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' }, - i18n[mode + 'Button'] || mode + String(i18n[mode + 'Button'] || typeLabel(mode)).toUpperCase() ) ); const voteLabelFor = (cat) => i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat; -const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new Map()) => { +const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new Map(), params = {}) => { const c = item.value.content; - const created = new Date(item.value.timestamp).toLocaleString(); + const created = moment(item.value.timestamp).format("YYYY/MM/DD HH:mm"); let contentHtml; @@ -30,25 +36,24 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new const { url, description, lastVisit } = c; contentHtml = div({ class: 'trending-bookmark' }, div({ class: 'card-section bookmark' }, - url ? h2(p(a({ href: url, target: '_blank', class: "bookmark-url" }, url))) : "", + url ? h2(p(a({ href: safeExternalHref(url), target: '_blank', rel: 'noopener noreferrer', class: "bookmark-url" }, url))) : "", lastVisit ? div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisitLabel + ':'), - span({ class: 'card-value' }, new Date(lastVisit).toLocaleString()) + span({ class: 'card-value' }, moment(lastVisit).format("YYYY/MM/DD HH:mm")) ) : "", - description ? [span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ":"), p(...renderUrl(description))] : null + description ? [span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ":"), p(...renderStyledText(description))] : null ) ); } else if (c.type === 'image') { - const { url, title, description, meme } = c; + const { url, title, description } = c; contentHtml = div({ class: 'trending-image' }, div({ class: 'card-section image' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, title)) : "", - description ? [span({ class: 'card-label' }, i18n.imageDescriptionLabel + ":"), p(...renderUrl(description))] : null, - meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : "", - div({ class: 'card-field' }, img({ src: `/blob/${encodeURIComponent(url)}`, class: 'feed-image' })) + description ? [span({ class: 'card-label' }, i18n.imageDescriptionLabel + ":"), p(...renderStyledText(description))] : null, + div({ class: 'card-field image-container' }, renderZoomableImage(`/blob/${encodeURIComponent(url)}`, { imgClass: 'post-image' })) ) ); } else if (c.type === 'audio') { @@ -56,9 +61,9 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-audio' }, div({ class: 'card-section audio' }, title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioTitleLabel + ':'), span({ class: 'card-value' }, title)) : "", - description ? [span({ class: 'card-label' }, i18n.audioDescriptionLabel + ":"), p(...renderUrl(description))] : null, + description ? [span({ class: 'card-label' }, i18n.audioDescriptionLabel + ":"), p(...renderStyledText(description))] : null, url - ? div({ class: 'card-field audio-container' }, audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(url)}`, type: mimeType })) + ? div({ class: 'card-field audio-container' }, audioHyperaxe({ controls: true, class: 'post-audio', src: `/blob/${encodeURIComponent(url)}`, type: mimeType })) : div({ class: 'card-field' }, p(i18n.audioNoFile)) ) ); @@ -67,21 +72,32 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-video' }, div({ class: 'card-section video' }, title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoTitleLabel + ':'), span({ class: 'card-value' }, title)) : "", - description ? [span({ class: 'card-label' }, i18n.videoDescriptionLabel + ":"), p(...renderUrl(description))] : null, + description ? [span({ class: 'card-label' }, i18n.videoDescriptionLabel + ":"), p(...renderStyledText(description))] : null, br(), url - ? div({ class: 'card-field video-container' }, videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(url)}`, type: mimeType, preload: 'metadata', width: '640', height: '360' })) + ? div({ class: 'card-field video-container' }, videoHyperaxe({ controls: true, class: 'post-video', src: `/blob/${encodeURIComponent(url)}`, type: mimeType, preload: 'metadata' })) : div({ class: 'card-field' }, p(i18n.videoNoFile)) ) ); + } else if (c.type === 'file') { + const { url, title, description, fileName, torrentUrl } = c; + contentHtml = div({ class: 'trending-file' }, + div({ class: 'card-section file' }, + title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.fileTitleLabel || 'Title') + ':'), span({ class: 'card-value' }, title)) : "", + description ? [span({ class: 'card-label' }, (i18n.fileDescriptionLabel || 'Description') + ":"), p(...renderStyledText(description))] : null, + url && url.startsWith("&") + ? div({ class: 'card-field' }, renderFileDownloads(item.key, torrentUrl, fileName || title)) + : div({ class: 'card-field' }, p(i18n.fileNoFile || 'No file')) + ) + ); } else if (c.type === 'torrent') { - const { url, title, description } = c; + const { url, title, description, source, sourceName } = c; contentHtml = div({ class: 'trending-torrent' }, div({ class: 'card-section torrent' }, title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentTitleLabel || 'Title') + ':'), span({ class: 'card-value' }, title)) : "", - description ? [span({ class: 'card-label' }, (i18n.torrentDescriptionLabel || 'Description') + ":"), p(...renderUrl(description))] : null, + description ? [span({ class: 'card-label' }, (i18n.torrentDescriptionLabel || 'Description') + ":"), p(...renderStyledText(description))] : null, url && url.startsWith("&") - ? div({ class: 'card-field' }, a({ href: `/blob/${encodeURIComponent(url)}`, class: 'filter-btn' }, i18n.torrentDownload || 'Download')) + ? div({ class: 'card-field torrent-card-actions' }, renderTorrentSourceDownload(item.key, source), renderTorrentDownload(torrentDownloadHref(url, title))) : div({ class: 'card-field' }, p(i18n.torrentNoFile || 'No file')) ) ); @@ -94,21 +110,82 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-document' }, div({ class: 'card-section document' }, t ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.documentTitleLabel + ':'), span({ class: 'card-value' }, t)) : "", - description ? [span({ class: 'card-label' }, i18n.documentDescriptionLabel + ":"), p(...renderUrl(description))] : null, + description ? [span({ class: 'card-label' }, i18n.documentDescriptionLabel + ":"), p(...renderStyledText(description))] : null, div({ id: `pdf-container-${item.key}`, class: 'pdf-viewer-container', 'data-pdf-url': `/blob/${encodeURIComponent(url)}` }) ) ); + } else if (c.type === 'industry') { + contentHtml = div({ class: 'trending-industry' }, + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryName + ':'), span({ class: 'card-value' }, c.name || '')), + c.sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industrySector + ':'), span({ class: 'card-value' }, String(c.sector).toUpperCase())) : "", + c.membershipPolicy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryMembershipPolicy + ':'), span({ class: 'card-value' }, String(i18n['industryPolicy_' + c.membershipPolicy] || c.membershipPolicy).toUpperCase())) : "", + c.description ? p(...renderStyledText(c.description)) : null, + Array.isArray(c.tags) && c.tags.length + ? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + } else if (c.type === 'industryBlueprint') { + contentHtml = div({ class: 'trending-industry' }, + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryBlueprint + ':'), span({ class: 'card-value' }, c.name || '')), + c.outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryOutputKind + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + c.outKind] || c.outKind).toUpperCase())) : "", + c.laborHours ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryLaborHours + ':'), span({ class: 'card-value' }, String(c.laborHours))) : "", + c.description ? p(...renderStyledText(c.description)) : null + ) + ); + } else if (c.type === 'housing') { + const free = String(c.housing_type || '').toLowerCase() === 'couchsurfing'; + contentHtml = div({ class: 'trending-housing' }, + div({ class: 'card-section housing' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, c.title || '')), + c.housing_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingType + ':'), span({ class: 'card-value' }, String(i18n['housingType' + String(c.housing_type).toUpperCase()] || c.housing_type).toUpperCase())) : "", + c.property_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingProperty + ':'), span({ class: 'card-value' }, String(i18n['housingProperty' + String(c.property_type).toUpperCase()] || c.property_type).toUpperCase())) : "", + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPrice + ':'), span({ class: 'card-value' }, free ? (i18n.housingFree || 'FREE') : `${Number(c.price || 0).toFixed(2)} ECO`)), + c.place ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPlace + ':'), span({ class: 'card-value' }, c.place)) : "", + Number(c.rooms) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingRooms + ':'), span({ class: 'card-value' }, String(c.rooms))) : "", + Number(c.size) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingSize + ':'), span({ class: 'card-value' }, `${c.size} m²`)) : "", + c.description ? p(...renderStyledText(c.description)) : null, + Array.isArray(c.tags) && c.tags.length + ? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + } else if (c.type === 'market') { + contentHtml = div({ class: 'trending-market' }, + div({ class: 'card-section market' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, c.title || '')), + c.item_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, String(c.item_type).toUpperCase())) : "", + c.item_status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.marketItemCondition || i18n.status) + ':'), span({ class: 'card-value' }, String(c.item_status).toUpperCase())) : "", + div({ class: 'price-chip' }, `${c.price} ECO`), + c.description ? p(...renderStyledText(c.description)) : null, + Array.isArray(c.tags) && c.tags.length + ? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + } else if (c.type === 'post') { + contentHtml = div({ class: 'trending-blog' }, + div({ class: 'card-section blog' }, + c.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href: `/blogs/${encodeURIComponent(item.key)}` }, c.contentWarning))) : "", + div({ class: 'blog-card-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(c.text || '').trim().length > 420 ? String(c.text).trim().slice(0, 420).replace(/\s+$/, '') + '…' : String(c.text || '').trim())) }), + Array.isArray(c.tags) && c.tags.length + ? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); } else if (c.type === 'feed') { const { text, refeeds } = c; contentHtml = div({ class: 'trending-feed' }, div({ class: 'card-section feed' }, - div({ class: 'feed-text', innerHTML: sanitizeHtml(renderTextWithStyles(text)) }), + div({ class: 'feed-text' }, ...renderStyledText(String(text || ''), { zoomImages: true })), refeeds ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), span({ class: 'card-value' }, refeeds)) : "" ) ); - } else if (c.type === 'votes') { + } else if (c.type === 'votes' || c.type === 'poll') { const { question, deadline, votes: vmap, totalVotes } = c; const votesList = vmap && typeof vmap === 'object' ? Object.entries(vmap).map(([o, cnt]) => ({ option: o, count: cnt })) @@ -116,7 +193,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-votes' }, div({ class: 'card-section votes' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteQuestionLabel + ':'), span({ class: 'card-value' }, question)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')), + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':'), span({ class: 'card-value' }, deadline ? moment(deadline).format("YYYY/MM/DD HH:mm") : '')), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteTotalVotes + ':'), span({ class: 'card-value' }, totalVotes)), table( tr(...votesList.map(v => th(i18n[v.option] || v.option))), @@ -129,11 +206,11 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-transfer' }, div({ class: 'card-section transfer' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.concept + ':'), span({ class: 'card-value' }, concept)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')), + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? moment(deadline).format("YYYY/MM/DD HH:mm") : '')), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.amount + ':'), span({ class: 'card-value' }, amount)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.from + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(from)}`, target: '_blank' }, from))), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.to + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(to)}`, target: '_blank' }, to))), + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.from + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(from)}`, target: '_blank', rel: 'noopener noreferrer' }, from))), + process.env.OASIS_MOBILE === '1' ? null : div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.to + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(to)}`, target: '_blank', rel: 'noopener noreferrer' }, to))), h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersConfirmations + ': '), span({ class: 'card-value' }, `${confirmedBy.length}/2`)) ) ); @@ -142,7 +219,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new div({ class: 'card-section styled-text-content' }, div( { class: 'card-field' }, - span({ class: 'card-value', innerHTML: sanitizeHtml(renderTextWithStyles(c.text || c.description || c.title || '[no content]')) }) + span({ class: 'card-value', [RAW_HTML]: sanitizeHtml(renderStyledHtml(c.title || c.name || c.text || c.description || '[no content]')) }) ) ) ); @@ -154,10 +231,27 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new audio: 'audios', video: 'videos', torrent: 'torrents', + file: 'files', document: 'documents', feed: 'feed', + post: 'blogs', votes: 'votes', - transfer: 'transfers' + transfer: 'transfers', + industry: 'industry', + project: 'projects', + report: 'reports', + task: 'tasks', + event: 'events', + shopProduct: 'shops/product', + housing: 'housing', + market: 'market', + podcast: 'podcasts', + podcastEpisode: 'podcasts/episode', + campaign: 'campaigns', + logisticsRoute: 'logistics', + poll: 'polls', + schoolCourse: 'school/course', + industryBlueprint: 'industry/blueprint' }; const detailHref = detailPaths[c.type] ? `/${detailPaths[c.type]}/${encodeURIComponent(item.key)}` @@ -169,69 +263,70 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new div( { class: 'card-header activity-card-header' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(c.type || '').toUpperCase()) + span({ class: 'pm-exposition-text' }, String(typeLabel(c.type) || '').toUpperCase()) ), - renderContentActions(item.key, detailHref) + renderContentActions(item.key, detailHref, { + spread: spreadMap.get(item.key) || null, + author: item.value.author, + reportTitle: c.title || c.question || c.concept || c.contentWarning || '', + ...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&') + ? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } } + : {}), + ...(CONTENT_FAV_KIND[c.type] + ? { favKind: CONTENT_FAV_KIND[c.type], isFavorite: params.favIndex instanceof Map ? params.favIndex.get(String(item.key)) === CONTENT_FAV_KIND[c.type] : false, returnTo: params.returnTo } + : {}) + }) ), div( { class: 'card-section trending-card-body' }, - contentHtml, - div({ class: 'card-spread-left' }, renderSpreadButton(item.key, spreadMap.get(item.key))), - p( - { class: 'card-footer' }, - span({ class: 'date-link' }, `${created} ${i18n.performed} `), - userLink(item.value.author) - ), - (() => { - const ops = c.opinions || {}; - const entries = Object.entries(ops).filter(([, v]) => v > 0); - const dominantPart = (() => { - if (!entries.length) return null; - const maxVal = Math.max(...entries.map(([, v]) => v)); - const dominant = entries.filter(([, v]) => v === maxVal).map(([k]) => voteLabelFor(k)); - return [ - span({ class: 'trending-dominant-sep' }, '|'), - span({ class: 'trending-dominant-text' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`) - ]; - })(); - return h2( - `${i18n.trendingTotalOpinions || i18n.trendingTotalCount}: `, - span({ class: 'trending-total-count' }, String(votes)), - ...(dominantPart || []) - ); - })(), + contentHtml + ), + renderVotesSummary(c.opinions), + renderCardMetaRow( details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`), + summary({ class: 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`)), div( { class: 'voting-buttons' }, categories.map(cat => form({ method: 'POST', action: `/trending/${encodeURIComponent(item.key)}/${cat}` }, button( { class: 'vote-btn' }, - `${voteLabelFor(cat)} [${c.opinions?.[cat] || 0}]` + `${String(voteLabelFor(cat)).toUpperCase()} [${c.opinions?.[cat] || 0}]` ) ) ) ) + ), + p( + { class: 'card-footer' }, + span({ class: 'date-link' }, `${created}`), + userLink(item.value.author) ) ) ); }; -exports.trendingView = (items, filter, categories = opinionCategories, spreadMap = new Map()) => { +exports.trendingView = (items, filter, categories = opinionCategories, spreadMap = new Map(), q = '', allItems = null, params = {}) => { const seenDocumentTitles = new Set(); const title = i18n.trendingTitle; - const baseFilters = ['TOP', 'ALL', 'MINE', 'RECENT']; + const baseFilters = ['ALL', 'RECENT', 'MINE', 'TOP']; const contentFilters = [ - ['votes', 'feed', 'transfer'], - ['bookmark', 'image', 'video', 'audio', 'document', 'torrent'] + ['votes', 'event', 'task', 'report'], + ['feed', 'post', 'project', 'industry', 'shopProduct', 'transfer'], + ['audio', 'bookmark', 'document', 'file', 'image', 'torrent', 'video'], + ['podcast', 'podcastEpisode', 'campaign', 'logisticsRoute'] ]; let filteredItems = items.filter(item => { const c = item.value?.content || item.content; return c && typeof c === 'object' && c.type !== 'tombstone'; }); + const censusItems = Array.isArray(allItems) ? allItems : filteredItems; + const presentTypes = new Set(censusItems.map(item => (item.value?.content || item.content || {}).type).filter(Boolean)); + const emptyTrend = censusItems.length === 0 && !String(q || '').trim(); if (filter === 'MINE') { filteredItems = filteredItems.filter(item => item.value.author === userId); @@ -245,7 +340,9 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap return bVotes !== aVotes ? bVotes - aVotes : b.value.timestamp - a.value.timestamp; }); } else if (contentFilters.flat().includes(filter)) { - filteredItems = filteredItems.filter(item => item.value.content.type === filter); + filteredItems = filteredItems.filter(item => + item.value.content.type === filter || + (filter === 'votes' && item.value.content.type === 'poll')); } else if (filter !== 'ALL') { filteredItems = filteredItems.filter(item => (item.value.content.opinions_inhabitants || []).length > 0); } @@ -254,15 +351,25 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap filteredItems.sort((a, b) => b.value.timestamp - a.value.timestamp); } - const header = div({ class: 'tags-header' }, h2(title), p(i18n.exploreTrending)); - const cards = filteredItems + const header = div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.exploreTrending)); + const seenDocs = new Set(); + const shownItems = filteredItems.filter(item => { + const c = item.value.content || {}; + const t = c.type === 'document' ? String(c.title || '').trim() : ''; + if (!t) return true; + if (seenDocs.has(t)) return false; + seenDocs.add(t); + return true; + }); + const cards = paged(shownItems) .map(item => renderTrendingCard( item, Object.values(item.value.content.opinions || {}).reduce((s, n) => s + (n || 0), 0), categories, seenDocumentTitles, - spreadMap + spreadMap, + { favIndex: params.favIndex, returnTo: `/trending?filter=${encodeURIComponent(filter)}` } ) ) .filter(Boolean); @@ -273,16 +380,27 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap title, section( header, - div( + emptyTrend ? null : div( { class: 'mode-buttons' }, div({ class: 'column' }, baseFilters.map(mode => filterButton(mode, filter))), - ...contentFilters.map(row => - div({ class: 'column' }, row.map(mode => filterButton(mode, filter))) + ...contentFilters.map(row => { + const visible = row.filter(mode => presentTypes.has(mode) || (mode === 'votes' && presentTypes.has('poll'))); + return visible.length ? div({ class: 'column' }, visible.map(mode => filterButton(mode, filter))) : null; + }) + ), + emptyTrend ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(shownItems.length), + form({ method: 'GET', action: '/trending', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: q, placeholder: i18n.trendingSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ), section( cards.length - ? div({ class: 'trending-container', style: 'display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;' }, ...cards) + ? div({ class: 'trending-container' }, ...cards) : div({ class: 'no-results' }, p(i18n.trendingNoContentMessage)) ) ) @@ -290,8 +408,7 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap if (hasDocument) { html += ` - - + `; } diff --git a/src/views/tribes_view.js b/src/views/tribes_view.js index 13cf41e..a0f8087 100644 --- a/src/views/tribes_view.js +++ b/src/views/tribes_view.js @@ -1,14 +1,84 @@ const { div, h2, h3, p, section, button, form, a, input, img, label, select, option, br, textarea, h1, span, nav, ul, li, video, audio, table, tr, td, thead, tbody, th } = require("../server/node_modules/hyperaxe"); +const { TEXT_CAP } = require('../backend/long_text'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions } = require('./main_views'); -const { renderEncryptedChip: renderTribeEncryptedChip } = require('./clearnet_view'); +const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, clearnetSlugFor, renderEngagement, renderOpinionsVoting, renderVotesSummary, paged, CLEARNET_HUB_CSS } = require('./main_views'); +const { renderTribeWikiSection } = require('./wiki_view'); +const mobileUi = require('./mobile_ui'); +const { renderEncryptedChip: renderTribeEncryptedChip, renderLicenseSelect, renderReachChip, renderClearnetPage, escapeHtml, renderRichText, blobUrl: cnBlobUrl, paginateClearnet, renderClearnetPager, CLEARNET_PAGER_CSS } = require('./clearnet_view'); +const { renderPollCard } = require('./polls_view'); +const { renderFeedCard } = require('./feed_view'); +const { renderEventItem } = require('./event_view'); +const { renderTaskItem, renderTaskAssignAction, renderTaskStatusRow } = require('./task_view'); +const { renderVoteListItem, renderVoteOptionsBlock } = require('./vote_view'); +const { renderImageList } = require('./image_view'); +const { renderAudioList } = require('./audio_view'); +const { renderVideoList } = require('./video_view'); +const { renderDocumentList } = require('./document_view'); +const { renderBookmarkList } = require('./bookmark_view'); +const { renderTorrentTable } = require('./torrents_view'); +const { renderFileTable } = require('./files_view'); +const { renderPadCard } = require('./pads_view'); +const { renderChatCard } = require('./chats_view'); +const { renderCalendarCard } = require('./calendars_view'); +const { renderInhabitantCard } = require('./inhabitants_view'); const { config } = require('../server/SSB_server.js'); -const { renderUrl } = require('../backend/renderUrl'); -const { renderMapLocationUrl, renderMapLocationGrid, renderMapLocationVisitLabel } = require("./maps_view"); -const opinion_categories = require('../backend/opinion_categories.js'); +const { renderStyledText, safeExternalHref } = require('../backend/renderStyledText'); +const { renderSearchForm } = require('./search_view'); +const { renderMapCard, renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); +const { renderTribeRoomsSection, roomStatsBy } = require('./rooms_view'); const userId = config.keys.id; const isLarpHouseTribe = (t) => Array.isArray(t && t.tags) && t.tags.some(x => String(x).startsWith('larp-')); +const isTribeMember = (t) => !!t && (t.author === userId || (Array.isArray(t.members) && t.members.includes(userId))); + +const REACH_LEVELS = ['tribe', 'oasis', 'clearnet']; +const REACH_CONTENT_TYPES = new Set(['event', 'task', 'votation', 'forum', 'forum-reply', 'media', 'feed']); +const reachLevelOf = (item) => REACH_LEVELS.includes(item && item.reach) ? item.reach : 'tribe'; +const tribeReachTribeLabel = () => String(i18n.tribeReachTribe || 'TRIBE').toUpperCase(); +const tribeClearnetHref = (t) => `/c/tribe/${encodeURIComponent(clearnetSlugFor(t.title, t.rootId || t.id))}`; + +const renderTribeReachChip = (level, tribe) => level === 'oasis' || level === 'clearnet' + ? renderReachChip(level === 'clearnet', i18n, level === 'clearnet' && tribe ? tribeClearnetHref(tribe) : null) + : renderStateChip('encrypted', '🔒', i18n.tribeReachTribe || 'TRIBE'); + +const renderTribeReachSwitch = (tribe, item, returnTo) => { + const current = reachLevelOf(item); + const levelBtn = (level, text) => button({ type: 'submit', name: 'reach', value: level, class: current === level ? 'clearnet-switch-btn active' : 'clearnet-switch-btn', ...(current === level ? { disabled: true } : {}) }, text); + return form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/content/${encodeURIComponent(item.id)}/reach`, class: 'clearnet-switch', title: i18n.tribeReachChange }, + input({ type: 'hidden', name: 'returnTo', value: returnTo }), + levelBtn('tribe', tribeReachTribeLabel()), + levelBtn('oasis', 'OASIS'), + levelBtn('clearnet', 'CLEARNET') + ); +}; + +const renderTribeReachSelector = () => + div({ class: 'clearnet-choice' }, + div({ class: 'clearnet-choice-options' }, + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'reach', value: 'tribe', checked: true }), renderTribeReachChip('tribe')), + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'reach', value: 'oasis' }), renderTribeReachChip('oasis')), + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'reach', value: 'clearnet' }), renderTribeReachChip('clearnet')) + ), + p({ class: 'tribe-meta-label' }, i18n.tribeReachNotice) + ); + +const reachIndexOf = (items) => { + const byId = new Map(); + (Array.isArray(items) ? items : []).forEach(i => { + if (!i || i.contentType !== 'forum') return; + if (i.id) byId.set(i.id, i); + if (i.originId) byId.set(i.originId, i); + }); + return byId; +}; + +const renderTribeReachListChip = (tribe, item, threadsById) => { + if (!item || !REACH_CONTENT_TYPES.has(item.contentType)) return null; + const parent = item.contentType === 'forum-reply' && threadsById ? threadsById.get(item.parentId) : null; + const level = reachLevelOf(parent || item); + if (!tribe.reachAllowed && level === 'tribe') return null; + return div({ class: 'card-chips-row' }, renderTribeReachChip(level, item.tribeName ? null : tribe)); +}; const DEFAULT_HASH_ENC = "%260000000000000000000000000000000000000000000%3D.sha256"; const DEFAULT_HASH_PATH_RE = /\/image\/\d+\/%260000000000000000000000000000000000000000000%3D\.sha256$/; @@ -34,27 +104,27 @@ const resolvePhoto = (photoField) => { }; const MS_PER_DAY = 86400000; -const FEED_ITEMS_PER_PAGE = 5; const MAX_MESSAGE_LENGTH = 280; const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith('/assets/') || s.startsWith('/blob/') || s.startsWith('/image/')) return img({ src: s, ...attrs }) - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) - const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) - const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) - const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) + if (s.startsWith('/assets/') || s.startsWith('/blob/') || s.startsWith('/image/')) return img({ loading: 'lazy', src: s, ...attrs }) + if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: toBlobUrl(mVideo[1]), preload: 'metadata' }) + const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/) + if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: toBlobUrl(mAudio[1]) }) + const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/) + if (mImg) return img({ loading: 'lazy', src: toBlobUrl(mImg[1]), ...attrs }) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } const toBlobUrl = (raw) => { if (!raw) return null const s = String(raw).trim() + if (s.startsWith('/blob/sealed/')) return s if (s.startsWith('&')) return `/blob/${encodeURIComponent(s)}` const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/) return m ? `/blob/${encodeURIComponent(m[1])}` : null @@ -69,23 +139,11 @@ const toImageUrl = (raw, fallback) => { return url || fallback } -const filterAndSortFeed = (feed, feedFilter) => { - const parseDate = (d) => typeof d === 'string' ? Date.parse(d) : d; - if (feedFilter === 'MINE') return feed.filter(m => m.author === userId); - if (feedFilter === 'RECENT') { - const last24h = Date.now() - MS_PER_DAY; - return [...feed].filter(m => parseDate(m.createdAt) >= last24h).sort((a, b) => parseDate(b.createdAt) - parseDate(a.createdAt)); - } - if (feedFilter === 'ALL') return [...feed].sort((a, b) => parseDate(b.createdAt) - parseDate(a.createdAt)); - if (feedFilter === 'TOP') return [...feed].sort((a, b) => (b.refeeds || 0) - (a.refeeds || 0)); - return feed; -}; - const renderGallery = (sortedTribes) => { return div({ class: "gallery" }, sortedTribes.length ? sortedTribes.map(t => - a({ href: `#tribe-${encodeURIComponent(t.id)}`, class: "gallery-item" }, + a({ href: `#tribe-${encodeURIComponent(t.id)}`, id: `tribe-${encodeURIComponent(t.id)}-src`, class: "gallery-item" }, img({ src: toImageUrl(t.image, '/assets/images/default-tribe.png'), alt: t.title || "", class: "gallery-image" }) ) ) @@ -97,7 +155,7 @@ const renderLightbox = (sortedTribes) => { return sortedTribes.map(t => div( { id: `tribe-${encodeURIComponent(t.id)}`, class: "lightbox" }, - a({ href: "#", class: "lightbox-close" }, "×"), + a({ href: `#tribe-${encodeURIComponent(t.id)}-src`, class: "lightbox-close" }, "×"), img({ src: toImageUrl(t.image, '/assets/images/default-tribe.png'), class: "lightbox-image", alt: t.title || "" }) ) ); @@ -110,7 +168,7 @@ exports.renderInvitePage = (inviteCode) => { input({ type: 'text', value: inviteCode, readonly: 'readonly', class: 'tribe-invite-code-input' }), br(), form({ method: "GET", action: `/tribes` }, - button({ type: "submit", class: "filter-btn" }, i18n.walletBack) + button({ type: "submit", class: "tribe-action-btn" }, i18n.walletBack) ), ); return template(i18n.tribeInviteMode, section(pageContent)); @@ -166,30 +224,34 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul return cb - ca; }); - const title = - filter === 'recent' ? i18n.tribeRecentSectionTitle : - filter === 'mine' ? i18n.tribeMineSectionTitle : - filter === 'create' ? i18n.tribeCreateSectionTitle : - filter === 'edit' ? i18n.tribeUpdateSectionTitle : - filter === 'gallery' ? i18n.tribeGallerySectionTitle : - filter === 'top' ? i18n.tribeTopSectionTitle : - filter === 'subtribes' ? (i18n.tribeSubTribes || 'SUB-TRIBES') : - i18n.tribeAllSectionTitle; + const title = i18n.tribesTitle; - const header = div({ class: 'tags-header' }, h2(title), p(i18n.tribeDescription)); + const header = div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.tribeDescription)); - const filters = div({ class: 'filters' }, - form({ method: 'GET', action: '/tribes' }, + const emptyMod = moduleIsEmpty(Array.isArray(tribes) ? tribes : [], filter || 'all', 'all', query.search); + const tribeChip = (m) => { + if (m === filter || m === 'all') return true; + if (m === 'top' || m === 'gallery') return tribes.length > 0; + if (m === 'mine') return tribes.some(t => t.author === userId); + if (m === 'membership') return tribes.some(t => Array.isArray(t.members) && t.members.includes(userId)); + if (m === 'subtribes') return tribes.some(t => !!t.parentTribeId); + if (m === 'recent') return tribes.length > 0; + return true; + }; + const filters = emptyMod ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStatsBy(sorted, t => t.isAnonymous ? 'PRIVATE' : 'PUBLIC', [{ value: 'PUBLIC', label: i18n.tribePublic }, { value: 'PRIVATE', label: i18n.tribePrivate }]), + form({ method: 'GET', action: '/tribes', class: 'filter-box' }, input({ type: 'hidden', name: 'filter', value: filter }), - input({ type: 'text', name: 'search', placeholder: i18n.searchTribesPlaceholder, value: query.search || '' }), - br(), - button({ type: 'submit' }, i18n.applyFilters), - br() + input({ type: 'text', name: 'search', placeholder: i18n.searchTribesPlaceholder, value: query.search || '', class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ); const modeButtons = div({ class: 'tribe-mode-buttons' }, - ['all','recent','mine','membership','subtribes','top','gallery'].map(f => + ...(emptyMod ? [] : [ + ['all','recent','mine','membership','subtribes','top','gallery'].filter(tribeChip).map(f => form({ method: 'GET', action: '/tribes' }, input({ type: 'hidden', name: 'filter', value: f }), button({ type: 'submit', class: filter === f ? 'filter-btn active' : 'filter-btn' }, @@ -197,6 +259,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul ) ) ), + ]), form({ method: 'GET', action: '/tribes/create' }, button({ type: 'submit', class: 'create-button' }, i18n.tribeCreateButton) ) @@ -214,17 +277,17 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul }, label({ for: 'title' }, i18n.tribeTitleLabel), br, - input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeTitlePlaceholder, value: tribeToEdit.title || '' }), + input({ type: 'text', name: 'title', maxlength: '100', id: 'title', required: true, placeholder: i18n.tribeTitlePlaceholder, value: tribeToEdit.title || '' }), br(), label({ for: 'description' }, i18n.tribeDescriptionLabel), br, - textarea({ name: 'description', id: 'description', required: true, rows: 4, cols: 50, placeholder: i18n.tribeDescriptionPlaceholder }, tribeToEdit.description || ''), + textarea({ maxlength: "3000", name: 'description', id: 'description', required: true, rows: 4, cols: 50, placeholder: i18n.tribeDescriptionPlaceholder }, tribeToEdit.description || ''), br, label({ for: 'location' }, i18n.tribeLocationLabel), br, input({ type: 'text', name: 'location', id: 'location', placeholder: i18n.tribeLocationPlaceholder, value: tribeToEdit.location || '' }), br, - label(i18n.mapLocationTitle || 'Map Location'), + label(process.env.OASIS_MOBILE === '1' ? (i18n.tribeMapUrlLabel || 'Map link (optional)') : (i18n.mapLocationTitle || 'Map Location')), br, input({ type: 'text', name: 'mapUrl', placeholder: i18n.mapUrlPlaceholder || '/maps/MAP_ID', value: tribeToEdit.mapUrl || '' }), br, @@ -242,6 +305,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul option({ value: 'true', selected: tribeToEdit.isAnonymous === true ? 'selected' : undefined }, i18n.tribePrivate), option({ value: 'false', selected: tribeToEdit.isAnonymous === false ? 'selected' : undefined }, i18n.tribePublic) ), + !isSubEdit && mobileUi.isMobile() ? p({ class: 'form-hint' }, i18n.tribeHintPrivacy) : null, isSubEdit ? null : br(), isSubEdit ? null : br(), label({ for: 'inviteMode' }, i18n.tribeModeLabel), @@ -250,6 +314,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul option({ value: 'strict', selected: tribeToEdit.inviteMode === 'strict' ? 'selected' : undefined }, i18n.tribeStrict), option({ value: 'open', selected: tribeToEdit.inviteMode === 'open' ? 'selected' : undefined }, i18n.tribeOpen) ), + mobileUi.isMobile() ? p({ class: 'form-hint' }, i18n.tribeHintMode) : null, br(), br(), button({ type: 'submit' }, isEdit ? i18n.tribeUpdateButton : i18n.tribeCreateButton) ) @@ -257,26 +322,23 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul const allT = allTribes || tribes; - const tribeCards = sorted.map(t => { - const isMember = t.members.includes(userId); + const shownTribes = filter === 'create' || filter === 'edit' ? [] : paged(sorted); + + const tribeCards = shownTribes.map(t => { + if (mobileUi.isMobile()) return mobileUi.tribeRow(t, { own: String(t.author) === String(userId), imageNode: t.image ? renderMediaBlob(t.image, null, { class: 'tribe-row-thumb' }) : null }); const parentTribe = t.parentTribeId ? allT.find(p => p.id === t.parentTribeId) : null; const isOwn = String(t.author) === String(userId); return div({ class: 'trending-card tribes-card' + (isOwn ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, span(), - renderContentActions(t.id, `/tribe/${encodeURIComponent(t.id)}`) + renderContentActions(t.id, `/tribe/${encodeURIComponent(t.id)}`, { author: t.author, reportTitle: t.title, deleteAction: isOwn && !isLarpHouseTribe(t) ? contentDeleteAction('tribe', t.id) : undefined }) ), div({ class: 'card-section tribes-card-body' }, div({ class: 'tribe-card-image-wrapper' }, a({ href: `/tribe/${encodeURIComponent(t.id)}` }, renderMediaBlob(t.image, '/assets/images/default-tribe.png', { class: 'tribe-card-hero-image' }) - ), - isMember - ? form({ method: 'GET', action: `/tribe/${encodeURIComponent(t.id)}`, class: 'tribe-visit-btn-wrapper' }, - button({ type: 'submit', class: 'filter-btn' }, t.parentTribeId ? String(i18n.tribeviewSubTribeButton || 'VISIT SUB-TRIBE').toUpperCase() : String(i18n.tribeviewTribeButton || '').toUpperCase()) - ) - : null + ) ), div({ class: 'tribe-card-body' }, div({ class: 'shop-title-row' }, @@ -286,45 +348,22 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul t.isAnonymous ? renderPrivacyChip(true, i18n) : renderPrivacyChip(false, i18n), t.isAnonymous ? renderTribeEncryptedChip(i18n) : null, renderModeChip(t.inviteMode, i18n), - renderLifespanChip(t.lifetime, i18n) + renderLifespanChip(t.lifetime, i18n), + t.subscriptionIn === true + ? renderStateChip('mutuals', '\u2709', i18n.subscriptionOn) + : (t.subscriptionIn === false ? renderStateChip('closed', '\u2709', i18n.subscriptionOff) : null) ), - t.description ? p({ class: 'tribe-card-description' }, ...renderUrl(t.description)) : null, + t.description ? p({ class: 'tribe-card-description' }, ...renderStyledText(t.description)) : null, renderMapLocationVisitLabel(t.mapUrl), - (parentTribe || t.location) ? table({ class: 'tribe-info-table' }, - parentTribe ? tr( + parentTribe ? table({ class: 'tribe-info-table' }, + tr( td({ class: 'tribe-info-label' }, i18n.tribeRootLabel || 'ROOT'), td({ class: 'tribe-info-value', colspan: '3' }, a({ href: `/tribe/${encodeURIComponent(parentTribe.id)}` }, parentTribe.title)) - ) : null, - t.location ? tr( - td({ class: 'tribe-info-label' }, i18n.tribeLocationLabel || 'LOCATION'), - td({ class: 'tribe-info-value', colspan: '3' }, ...renderUrl(t.location)) - ) : null + ) ) : null, div({ class: 'tribe-card-members' }, span({ class: 'tribe-members-count' }, `${i18n.tribeMembersCount}: ${t.memberCount != null ? t.memberCount : t.members.length}`) - ), - (t.openInviteCode && !isMember && String(t.author) !== String(userId)) ? div({ class: 'tribe-card-join' }, - form({ method: 'POST', action: '/tribes/open-invite/join' }, - input({ type: 'hidden', name: 'tribeId', value: t.id }), - button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeJoinOpen) - ), - renderInviteQrCard({ qrDataUrl: t.openInviteQr, name: t.title }) - ) : null, - isMember ? div({ class: 'tribe-card-actions' }, - !isLarpHouseTribe(t) - ? form({ method: 'POST', action: '/tribes/generate-invite' }, - input({ type: 'hidden', name: 'tribeId', value: t.id }), - button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeGenerateInvite) - ) - : null, - form({ method: 'POST', action: `/tribes/leave/${encodeURIComponent(t.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeLeaveButton) - ) - ) : null, - filter === 'mine' ? div({ class: 'tribe-actions' }, - form({ method: 'GET', action: `/tribes/edit/${encodeURIComponent(t.id)}` }, button({ type: 'submit' }, i18n.tribeUpdateButton)), - form({ method: 'POST', action: `/tribes/delete/${encodeURIComponent(t.id)}` }, button({ type: 'submit', class: 'danger-btn' }, i18n.tribeDeleteButton)) - ) : null + ) ) ) ); @@ -333,83 +372,54 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul return template( title, section(header), - section(filters), section(modeButtons), + filters ? section(filters) : null, section( (filter === 'create' || filter === 'edit') ? createForm : filter === 'gallery' - ? renderGallery(sorted) - : div({ class: 'tribe-grid' }, - tribeCards.length > 0 ? tribeCards : p(i18n.noTribes) - ) + ? renderGallery(shownTribes) + : tribeCards.length > 0 + ? div({ class: mobileUi.isMobile() ? 'tribe-list' : 'tribe-grid' }, tribeCards) + : div({ class: "no-content-box" }, p(i18n.noTribes)) ), - ...renderLightbox(sorted) - ); -}; - -const renderFeedTribeView = async (feedItems, tribe, query = {}, filter) => { - const feed = Array.isArray(feedItems) ? feedItems : []; - const feedFilter = (query.feedFilter || 'RECENT').toUpperCase(); - const filteredFeed = filterAndSortFeed(feed, feedFilter); - return div({ class: 'tribe-feed-full' }, - div({ class: 'feed-actions' }, - ['TOP', 'MINE', 'ALL', 'RECENT'].map(f => - form({ method: 'GET', action: `/tribe/${encodeURIComponent(tribe.id)}` }, - input({ type: 'hidden', name: 'section', value: 'feed' }), - input({ type: 'hidden', name: 'feedFilter', value: f }), - button({ type: 'submit', class: feedFilter === f ? 'filter-btn active' : 'filter-btn' }, i18n[`tribeFeedFilter${f}`]) - ) - ) - ), - filteredFeed.length === 0 - ? p(i18n.tribeFeedEmpty) - : div({ class: 'feed-list' }, - filteredFeed.map(m => div({ class: 'feed-item' }, - div({ class: 'feed-row' }, - div({ class: 'refeed-column' }, - h1(`${m.refeeds || 0}`), - !(m.refeeds_inhabitants || []).includes(userId) - ? form( - { method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/refeed/${encodeURIComponent(m.id)}` }, - button({ class: 'refeed-btn' }, i18n.tribeFeedRefeed) - ) - : null - ), - div({ class: 'feed-main' }, - p(`${new Date(m.createdAt).toLocaleString()} — `, userLink(m.author)), - br, - p(...renderUrl(m.description)) - ) - ) - )) - ) + ...renderLightbox(shownTribes) ); }; const sectionLink = (tribe, sectionKey, label, currentSection) => form({ method: 'GET', action: `/tribe/${encodeURIComponent(tribe.id)}` }, input({ type: 'hidden', name: 'section', value: sectionKey }), - button({ type: 'submit', class: currentSection === sectionKey ? 'filter-btn active' : 'filter-btn' }, label) + button({ type: 'submit', class: currentSection === sectionKey ? 'filter-btn active' : 'filter-btn' }, String(label).toUpperCase()) ); +const tribeTagHref = (tribe) => (tag) => `/tribe/${encodeURIComponent(tribe.id)}?section=search&q=${encodeURIComponent('#' + String(tag).toLowerCase().replace(/^#+/, ''))}`; +const tribeText = (tribe, text, opts = {}) => renderStyledText(String(text || ''), { ...opts, ...(tribe && tribe.id ? { hashtagHref: tribeTagHref(tribe) } : {}) }); +const tribeExcerpt = (tribe, text, max = 200) => { + const full = String(text || '').trim(); + if (!full) return null; + const cut = full.length > max ? full.slice(0, max).replace(/\s+$/, '') + '…' : full; + return p({ class: 'post-text post-text-pre' }, ...tribeText(tribe, cut)); +}; + const renderSectionNav = (tribe, section) => { const firstGroup = [{ key: 'activity', label: i18n.tribeSectionActivity }, { key: 'inhabitants', label: i18n.tribeSectionInhabitants }]; if (!tribe.parentTribeId) firstGroup.push({ key: 'subtribes', label: i18n.tribeSubTribes }); if (!tribe.parentTribeId) firstGroup.push({ key: 'governance', label: i18n.tribeSectionGovernance || 'GOVERNANCE' }); const sections = [ { items: firstGroup }, - { items: [{ key: 'votations', label: i18n.tribeSectionVotations }, { key: 'events', label: i18n.tribeSectionEvents }, { key: 'tasks', label: i18n.tribeSectionTasks }] }, - { items: [{ key: 'feed', label: i18n.tribeSectionFeed }, { key: 'forum', label: i18n.tribeSectionForum }, { key: 'maps', label: i18n.tribeSectionMaps || 'MAPS' }, { key: 'torrents', label: i18n.tribeSectionTorrents || 'TORRENTS' }, { key: 'pads', label: i18n.tribeSectionPads || 'PADS' }, { key: 'chats', label: i18n.tribeSectionChats || 'CHATS' }, { key: 'calendars', label: i18n.tribeSectionCalendars || 'CALENDARS' }] }, - { items: [{ key: 'images', label: i18n.tribeSectionImages || 'IMAGES' }, { key: 'audios', label: i18n.tribeSectionAudios || 'AUDIOS' }, { key: 'videos', label: i18n.tribeSectionVideos || 'VIDEOS' }, { key: 'documents', label: i18n.tribeSectionDocuments || 'DOCUMENTS' }, { key: 'bookmarks', label: i18n.tribeSectionBookmarks || 'BOOKMARKS' }] }, + { items: [{ key: 'votations', label: i18n.tribeSectionVotations }, { key: 'polls', label: i18n.pollsTitle }, { key: 'events', label: i18n.tribeSectionEvents }, { key: 'tasks', label: i18n.tribeSectionTasks }] }, + { items: [{ key: 'feed', label: i18n.tribeSectionFeed }, { key: 'forum', label: i18n.tribeSectionForum }, { key: 'maps', label: i18n.tribeSectionMaps || 'MAPS' }, { key: 'torrents', label: i18n.tribeSectionTorrents || 'TORRENTS' }, { key: 'pads', label: i18n.tribeSectionPads || 'PADS' }, { key: 'wiki', label: i18n.tribeSectionWiki || 'WIKI' }, { key: 'chats', label: i18n.tribeSectionChats || 'CHATS' }, { key: 'rooms', label: i18n.tribeSectionRooms || 'ROOMS' }, { key: 'calendars', label: i18n.tribeSectionCalendars || 'CALENDARS' }] }, + { items: [{ key: 'images', label: i18n.tribeSectionImages || 'IMAGES' }, { key: 'audios', label: i18n.tribeSectionAudios || 'AUDIOS' }, { key: 'videos', label: i18n.tribeSectionVideos || 'VIDEOS' }, { key: 'documents', label: i18n.tribeSectionDocuments || 'DOCUMENTS' }, { key: 'files', label: i18n.tribeSectionFiles || 'FILES' }, { key: 'bookmarks', label: i18n.tribeSectionBookmarks || 'BOOKMARKS' }] }, { items: [{ key: 'tags', label: i18n.tribeSectionTags || 'TAGS' }, { key: 'search', label: i18n.tribeSectionSearch }] }, ]; + if (mobileUi.isMobile()) return mobileUi.tribeTabs(tribe, section, sections); return div({ class: 'tribe-section-nav no-border' }, sections.map(g => div({ class: 'tribe-section-group no-border' }, g.items.map(s => s.href ? form({ method: 'GET', action: s.href }, - button({ type: 'submit', class: 'filter-btn' }, s.label) + button({ type: 'submit', class: 'filter-btn' }, String(s.label).toUpperCase()) ) : sectionLink(tribe, s.key, s.label, section)) ) @@ -430,11 +440,6 @@ const priorityI18n = () => ({ 'CRITICAL': i18n.tribePriorityCritical, }); -const inviteModeI18n = () => ({ - 'strict': i18n.tribeStrict, - 'open': i18n.tribeOpen, -}); - const forumCatI18n = () => ({ 'GENERAL': i18n.tribeForumCatGeneral, 'PROPOSAL': i18n.tribeForumCatProposal, @@ -442,22 +447,6 @@ const forumCatI18n = () => ({ 'ANNOUNCEMENT': i18n.tribeForumCatAnnouncement, }); -const mediaTypeI18n = () => ({ - 'all': i18n.tribeMediaFilterAll, - 'image': i18n.tribeMediaTypeImage, - 'video': i18n.tribeMediaTypeVideo, - 'audio': i18n.tribeMediaTypeAudio, - 'document': i18n.tribeMediaTypeDocument, - 'bookmark': i18n.tribeMediaTypeBookmark, -}); - -const taskFilterI18n = () => ({ - 'all': i18n.tribeTaskFilterAll, - 'open': i18n.tribeStatusOpen, - 'in-progress': i18n.tribeStatusInProgress, - 'closed': i18n.tribeStatusClosed, -}); - const statusBadge = (status) => { const cls = status === 'OPEN' ? 'tribe-content-status-open' : status === 'CLOSED' ? 'tribe-content-status-closed' @@ -476,27 +465,27 @@ const contentTypeVerb = (ct) => { }; const contentTypeName = (ct) => { - const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS' }; + const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, poll: String(i18n.pollsTitle || 'POLLS').toUpperCase(), forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, blog: String(i18n.blogTitle || 'BLOGS').toUpperCase(), media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS', wiki: i18n.tribeSectionWiki || 'WIKI' }; return map[ct] || ct; }; const activitySectionMap = { - event: 'events', task: 'tasks', votation: 'votations', + event: 'events', task: 'tasks', votation: 'votations', poll: 'polls', forum: 'forum', 'forum-reply': 'forum', feed: 'feed', - pad: 'pads', chat: 'chats', calendar: 'calendars', map: 'maps', torrent: 'torrents' + pad: 'pads', chat: 'chats', calendar: 'calendars', map: 'maps', wiki: 'wiki', torrent: 'torrents', file: 'files' }; const activitySectionForItem = (item) => { if (item.contentType === 'media' && item.mediaType) { - const map = { image: 'images', audio: 'audios', video: 'videos', document: 'documents', bookmark: 'bookmarks', torrent: 'torrents' }; + const map = { image: 'images', audio: 'audios', video: 'videos', document: 'documents', bookmark: 'bookmarks', torrent: 'torrents', file: 'files' }; return map[item.mediaType] || 'media'; } return activitySectionMap[item.contentType] || 'activity'; }; const activityMediaTypeName = (mt) => { - const map = { image: i18n.tribeSectionImages, audio: i18n.tribeSectionAudios, video: i18n.tribeSectionVideos, document: i18n.tribeSectionDocuments, bookmark: i18n.tribeSectionBookmarks, torrent: i18n.tribeSectionTorrents }; + const map = { image: i18n.tribeSectionImages, audio: i18n.tribeSectionAudios, video: i18n.tribeSectionVideos, document: i18n.tribeSectionDocuments, bookmark: i18n.tribeSectionBookmarks, torrent: i18n.tribeSectionTorrents, file: i18n.tribeSectionFiles }; return map[mt] || i18n.tribeSectionMedia || 'MEDIA'; }; @@ -504,10 +493,39 @@ const renderTribeActivitySection = (tribe, sectionData) => { const { activities } = sectionData || { activities: [] }; if (activities.length === 0) return div({ class: 'tribe-content-list' }, p(i18n.tribeActivityEmpty)); const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; + const threadsById = reachIndexOf(activities); return div({ class: 'tribe-content-list tribe-content-list-spaced' }, - activities.slice(0, 50).map(item => { + paged(activities).map(item => { if (item.encrypted) return div({ class: 'card card-rpg' }, div({ class: 'tribe-card-body' }, p({ class: 'tribe-meta-label' }, i18n.tribeContentEncrypted || 'Encrypted content'))); - const date = item.timestamp ? new Date(item.timestamp).toLocaleString() : ''; + if (item.contentType === 'chatThread') { + const replies = Array.isArray(item.replies) ? item.replies : []; + const tDate = item.timestamp ? moment(item.timestamp).format("YYYY/MM/DD HH:mm") : ''; + const headerText = item.tribeName + ? `[${String(i18n.typeChat || 'CHAT').toUpperCase()} · THREAD · ${item.tribeName}]` + : `[${String(i18n.typeChat || 'CHAT').toUpperCase()} · THREAD]`; + return div({ class: 'card card-rpg tribe-card-padded chat-thread' }, + div({ class: 'card-header' }, + h2({ class: 'card-label' }, headerText), + a({ href: item.directUrl, class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') + ), + div({ class: 'tribe-card-body' }, + item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null, + tribeExcerpt(tribe, item.description), + replies.map(r => div({ class: 'thread-reply-item' }, + r.text ? p({ class: 'post-text post-text-pre' }, String(r.text)) : null, + div({ class: 'card-footer thread-reply-footer' }, + span({ class: 'date-link' }, r.createdAt ? moment(r.createdAt).format("YYYY/MM/DD HH:mm") : ''), + userLink(r.author, '') + ) + )) + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${tDate}`), + userLink(item.author, item.authorName) + ) + ); + } + const date = item.timestamp ? moment(item.timestamp).format("YYYY/MM/DD HH:mm") : ''; const typeLabel = item.contentType === 'media' && item.mediaType ? activityMediaTypeName(item.mediaType) : contentTypeName(item.contentType); @@ -518,35 +536,36 @@ const renderTribeActivitySection = (tribe, sectionData) => { const blobUrl = item.contentType === 'media' ? toBlobUrl(item.image) : null; const mediaContent = item.contentType === 'media' && item.mediaType === 'image' && blobUrl - ? a({ href: blobUrl, target: '_blank' }, img({ src: blobUrl, alt: item.title || '', class: 'tribe-media-thumb' })) + ? a({ href: blobUrl, target: '_blank', rel: 'noopener noreferrer' }, img({ src: blobUrl, alt: item.title || '', class: 'tribe-media-thumb' })) : item.contentType === 'media' && item.mediaType === 'audio' && blobUrl ? audio({ src: blobUrl, controls: true, class: 'tribe-media-audio' }) : item.contentType === 'media' && item.mediaType === 'video' && blobUrl - ? video({ src: blobUrl, controls: true, class: 'tribe-media-thumb' }) + ? video({ src: blobUrl, controls: true, class: 'tribe-media-thumb', preload: 'metadata' }) : item.contentType === 'media' && item.mediaType === 'document' && blobUrl - ? a({ href: blobUrl, target: '_blank', class: 'tribe-action-btn' }, i18n.readDocument || 'Read Document') + ? a({ href: blobUrl, target: '_blank', rel: 'noopener noreferrer', class: 'tribe-action-btn' }, i18n.readDocument || 'Read Document') : item.contentType === 'media' && item.mediaType === 'bookmark' && (item.url || item.description) - ? a({ href: item.url || item.description, target: '_blank', class: 'tribe-action-btn' }, item.url || item.description) + ? a({ href: safeExternalHref(item.url || item.description), target: '_blank', rel: 'noopener noreferrer', class: 'tribe-action-btn' }, item.url || item.description) : null; return div({ class: 'card card-rpg tribe-card-padded' }, div({ class: 'card-header' }, h2({ class: 'card-label' }, headerText), item.directUrl - ? a({ href: item.directUrl, class: 'filter-btn' }, i18n.viewDetails || 'View Details') + ? a({ href: item.directUrl, class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') : form({ method: 'GET', action: tribeUrl }, input({ type: 'hidden', name: 'section', value: targetSection }), - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details') + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') ) ), div({ class: 'tribe-card-body' }, + renderTribeReachListChip(tribe, item, threadsById), item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title) ) : null, mediaContent, - item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? p(item.description.substring(0, 200)) : null + item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? tribeExcerpt(tribe, item.description) : null ), p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${date} ${i18n.performed || ''} `), + span({ class: 'date-link' }, `${date}`), userLink(item.author, item.authorName) ) ); @@ -564,7 +583,7 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => { div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionTrending)), div({ class: 'tribe-filter-bar' }, periodBtn('day', i18n.tribeTrendingPeriodDay), periodBtn('week', i18n.tribeTrendingPeriodWeek), periodBtn('all', i18n.tribeTrendingPeriodAll)), items.length === 0 ? p(i18n.tribeTrendingEmpty) : - items.slice(0, 30).map((item, idx) => { + paged(items.map((item, idx) => [item, idx])).map(([item, idx]) => { if (item.encrypted) return div({ class: 'tribe-content-card' }, div({ class: 'tribe-content-meta' }, span(`#${idx + 1}`)), p({ class: 'tribe-meta-label' }, i18n.tribeContentEncrypted || 'Encrypted content')); return div({ class: 'tribe-content-card' }, div({ class: 'tribe-content-meta' }, @@ -573,6 +592,7 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => { span(`${i18n.tribeTrendingEngagement}: ${engagementScore(item)}`), (() => { const entries = Object.entries(item.opinions || {}); if (!entries.length) return null; const top = entries.reduce((a, b) => b[1] > a[1] ? b : a); return span(`| ${i18n.tribeTopCategory}: ${i18n['opinionCat' + top[0].charAt(0).toUpperCase() + top[0].slice(1)] || top[0]}`); })() ), + renderTribeReachListChip(tribe, item, null), item.title ? h2(item.title) : item.description ? h2(item.description) : null, div({ class: 'tribe-content-meta' }, item.refeeds ? span(`${i18n.tribeActivityRefeed}: ${item.refeeds}`) : null, @@ -585,12 +605,10 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => { const renderTribeTagsSection = (tribe, sectionData, query) => { const tagsList = Array.isArray(sectionData?.tags) ? sectionData.tags : []; - const selectedTag = sectionData?.selectedTag || ''; - const filteredItems = Array.isArray(sectionData?.filteredItems) ? sectionData.filteredItems : []; - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; const sortedTags = tagsList.slice().sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag)); + const tagHref = tribeTagHref(tribe); return section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionTags || i18n.tagsTitle || 'TAGS'), p(i18n.tagsDescription || '') ), @@ -603,28 +621,13 @@ const renderTribeTagsSection = (tribe, sectionData, query) => { th(i18n.tagsTableHeaderCount || 'Count') )), tbody( - sortedTags.map(t => tr( - td(a({ href: `${tribeUrl}?section=tags&tag=${encodeURIComponent(t.tag)}` }, t.tag)), + paged(sortedTags).map(t => tr( + td(a({ href: tagHref(t.tag) }, t.tag)), td(`${t.count}`) )) ) ) - ), - selectedTag ? div({ class: 'tribe-content-list' }, - h2(`#${selectedTag} (${filteredItems.length})`), - filteredItems.length === 0 ? p(i18n.tribeTagsEmpty || 'No items') : - filteredItems.slice(0, 50).map(item => div({ class: 'card card-rpg' }, - div({ class: 'card-header' }, h2({ class: 'card-label' }, `[${String(contentTypeName(item.contentType)).toUpperCase()}]`)), - div({ class: 'card-body' }, - item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null, - item.description ? p(item.description.substring(0, 200)) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(item.createdAt).toLocaleString()), - userLink(item.author) - ) - )) - ) : null + ) ); }; @@ -632,30 +635,35 @@ const renderTribeSearchSection = (tribe, sectionData, query) => { const { results } = sectionData || { results: [] }; const sq = sectionData?.query || ''; const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; + const threadsById = reachIndexOf(results); + const hashtag = sq.trim().startsWith('#') ? sq.trim().replace(/^#+/, '').toLowerCase() : ''; + const typeCounts = new Map(); + for (const item of results) { + const label = String(contentTypeName(item.contentType)).toUpperCase(); + typeCounts.set(label, (typeCounts.get(label) || 0) + 1); + } + const stats = renderModuleStats(results.length, [...typeCounts].map(([label, count]) => ({ label, count })).sort((x, y) => y.count - x.count || x.label.localeCompare(y.label))); return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionSearch)), - form({ method: 'GET', action: tribeUrl, class: 'tribe-search-form' }, - input({ type: 'hidden', name: 'section', value: 'search' }), - input({ type: 'text', name: 'q', value: sq, placeholder: i18n.tribeSearchPlaceholder, minlength: 2 }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeSectionSearch) - ), + div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionSearch), p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel)), + renderSearchForm({ action: tribeUrl, query: sq, queryName: 'q', hidden: [['section', 'search']] }), sq.length > 0 && sq.length < 2 ? p(i18n.tribeSearchMinChars) : null, sq.length >= 2 ? div( - h2(`${i18n.tribeSearchResults}: ${results.length}`), + stats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, stats) : null, results.length === 0 ? p(i18n.tribeSearchEmpty) : - results.map(item => div({ class: 'card card-rpg' }, + paged(results).map(item => div({ class: 'card card-rpg' }, div({ class: 'card-header' }, h2({ class: 'card-label' }, `[${String(contentTypeName(item.contentType)).toUpperCase()}]`) ), div({ class: 'card-body' }, + renderTribeReachListChip(tribe, item, threadsById), item.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), span({ class: 'card-value' }, item.title) ) : null, - item.description ? p(item.description.substring(0, 200)) : null + tribeExcerpt(tribe, item.description) ), p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(item.createdAt).toLocaleDateString()), + span({ class: 'date-link' }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")), userLink(item.author) ) )) @@ -677,8 +685,8 @@ const renderOverviewSection = (tribe, query, sectionData) => { recentFeed.length === 0 ? p(i18n.tribeFeedEmpty) : recentFeed.map(m => div({ class: 'feed-item' }, - p(`${new Date(m.createdAt).toLocaleString()} — `, userLink(m.author)), - p(...renderUrl(m.description)) + p(`${moment(m.createdAt).format("YYYY/MM/DD HH:mm")} — `, userLink(m.author)), + p(...tribeText(tribe, m.description)) ) ) ), @@ -719,23 +727,831 @@ const renderOverviewSection = (tribe, query, sectionData) => { ); }; -const renderInhabitantsSection = (tribe, members) => { - const resolved = Array.isArray(members) ? members : []; - if (resolved.length === 0) return p(i18n.tribeInhabitantsEmpty); - return div({ class: 'tribe-thumb-grid' }, - resolved.map(m => - a({ href: `/author/${encodeURIComponent(m.id)}`, class: 'tribe-thumb-link', title: m.name || m.id }, - img({ src: resolvePhoto(m.photo), class: 'tribe-thumb-img', alt: m.name || m.id }) +const tribeUrlOf = (tribe) => `/tribe/${encodeURIComponent(tribe.id)}`; +const sectionHref = (tribe, sectionKey, params = {}) => { + const qs = Object.entries({ section: sectionKey, ...params }) + .filter(([, v]) => v !== undefined && v !== null && String(v) !== '') + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&'); + return `${tribeUrlOf(tribe)}?${qs}`; +}; +const asList = (v) => Array.isArray(v) ? v : []; +const tsOf = (item) => Date.parse(item.createdAt) || item._ts || 0; +const isRecentItem = (item) => tsOf(item) >= Date.now() - MS_PER_DAY; +const isMineItem = (item) => String(item.author) === String(userId); +const tribeStatusOf = (item) => String(item.status || 'OPEN').toUpperCase(); +const capFirst = (s) => String(s).charAt(0).toUpperCase() + String(s).slice(1); +const blobIdOf = (raw) => { + const s = String(raw || '').trim(); + if (s.startsWith('&') || s.startsWith('/blob/sealed/')) return s; + const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/); + return m ? m[1] : null; +}; +const matchesTribeQuery = (item, q) => { + if (!q) return true; + const needle = q.toLowerCase(); + return [item.title, item.description, item.question, item.url, item.location, item.number, ...asList(item.tags)] + .some(v => String(v || '').toLowerCase().includes(needle)); +}; +const sortRecentFirst = (items) => [...items].sort((a, b) => tsOf(b) - tsOf(a)); +const BASE_MODES = { recent: isRecentItem, mine: isMineItem, all: () => true, top: () => true }; +const applyTribeFilter = (items, filter, q, modes = BASE_MODES, score = engagementScore) => { + const pred = modes[filter] || modes.all || (() => true); + const kept = asList(items).filter(it => pred(it) && matchesTribeQuery(it, q)); + return filter === 'top' ? [...kept].sort((a, b) => score(b) - score(a) || tsOf(b) - tsOf(a)) : sortRecentFirst(kept); +}; +const tribeChipVisible = (items, filter, modes) => (mode) => + mode === filter || mode === 'all' || mode === 'ALL' || asList(items).some(it => (modes[mode] || (() => true))(it)); +const openClosedModes = (isClosed) => ({ ...BASE_MODES, open: it => !isClosed(it), closed: isClosed }); +const openClosedStats = (items, isClosed, openLabel, closedLabel) => + renderModuleStatsBy(items, it => isClosed(it) ? 'CLOSED' : 'OPEN', [{ value: 'OPEN', label: openLabel }, { value: 'CLOSED', label: closedLabel }]); + +const renderTribeSectionHeader = (title, description) => + div({ class: 'tags-header module-header-line' }, h2(title), description ? p(description) : null); + +const renderTribeModeChips = (tribe, sectionKey, { modes = [], filter = 'all', labelOf = (m) => m, visible = null, emptyMod = false, create = null }) => + div({ class: 'mode-buttons-row' }, + ...(emptyMod ? [] : modes.filter(m => !visible || visible(m)).map(m => + form({ method: 'GET', action: tribeUrlOf(tribe) }, + input({ type: 'hidden', name: 'section', value: sectionKey }), + input({ type: 'hidden', name: 'filter', value: m }), + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(labelOf(m) || m).toUpperCase()) + ) + )), + create + ? form({ method: 'GET', action: create.action || tribeUrlOf(tribe) }, + ...Object.entries(create.hidden || { section: sectionKey, action: 'create' }).map(([k, v]) => input({ type: 'hidden', name: k, value: v })), + button({ type: 'submit', class: 'create-button' }, create.label) + ) + : null + ); + +const renderTribeSearchRow = (tribe, sectionKey, filter, q, statsNode, placeholder, paramName = 'q') => + div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + statsNode, + form({ method: 'GET', action: tribeUrlOf(tribe), class: 'filter-box' }, + input({ type: 'hidden', name: 'section', value: sectionKey }), + filter ? input({ type: 'hidden', name: 'filter', value: filter }) : null, + input({ type: 'text', name: paramName, value: q || '', placeholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)) + ) + ); + +const renderTribeEmpty = (text) => div({ class: 'no-content-box' }, p(text)); + +const tribeDescription = (text) => text ? p({ class: 'tribe-card-description' }, ...renderStyledText(String(text))) : null; + +const tribeReachChipFor = (tribe, item) => { + const level = reachLevelOf(item); + return tribe.reachAllowed || level !== 'tribe' ? renderTribeReachChip(level, tribe) : null; +}; + +const renderTribeReachControls = (tribe, item, returnTo) => { + const levelSwitch = returnTo && tribe.reachAllowed && isTribeMember(tribe) && item.contentType !== 'forum-reply' && item.id ? renderTribeReachSwitch(tribe, item, returnTo) : null; + const openedBy = item.exposedBy && item.exposedBy !== item.author + ? span({ class: 'tribe-meta-label' }, `${i18n.tribeReachOpenedBy || 'Opened by'}: `, userLink(item.exposedBy)) + : null; + return levelSwitch || openedBy ? div({ class: 'card-chips-row' }, levelSwitch, openedBy) : null; +}; + +const tribeItemActions = (tribe, item, member, title) => + renderContentActions(item.id, null, { + author: item.author, + reportTitle: title || item.title || item.description, + deleteAction: member && isMineItem(item) && item.id ? `${tribeUrlOf(tribe)}/content/delete/${encodeURIComponent(item.id)}` : undefined + }); + +const tribeEngagement = (tribe, item, member, returnTo) => { + if (member) { + return renderEngagement(item.id, renderOpinionsVoting(null, item.id, item.opinions, returnTo, item.opinions_inhabitants, + (id, category) => `${tribeUrlOf(tribe)}/content/${encodeURIComponent(id)}/opinion/${category}`)); + } + const summary = renderVotesSummary(item.opinions); + return summary ? renderEngagement(item.id, summary) : null; +}; + +const toFeedShape = (m) => ({ + key: m.id, + value: { author: m.author, timestamp: tsOf(m), content: { text: m.description, author: m.author, refeeds: m.refeeds, refeeds_inhabitants: m.refeeds_inhabitants, opinions: m.opinions, opinions_inhabitants: m.opinions_inhabitants } } +}); + +const renderFeedComposeForm = (tribe) => + form({ method: 'POST', action: `${tribeUrlOf(tribe)}/message` }, + textarea({ name: 'message', required: true, minlength: '1', maxlength: String(MAX_MESSAGE_LENGTH), rows: '4', cols: 50, placeholder: i18n.feedPlaceholder }), + tribe.reachAllowed ? renderTribeReachSelector() : null, + div({ class: 'feed-compose-actions' }, + button({ type: 'submit', class: 'create-button' }, i18n.createFeedButton || 'Send Feed!') + ) + ); + +const FEED_MODES = ['ALL', 'RECENT', 'MINE', 'TOP']; +const renderFeedTribeView = (feedItems, tribe, query = {}) => { + const feed = asList(feedItems); + const member = isTribeMember(tribe); + const filter = String(query.filter || query.feedFilter || 'ALL').toUpperCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(isCreate ? i18n.createFeedTitle : i18n.tribeSectionFeed, i18n.FeedshareYourOpinions); + const emptyMod = !isCreate && moduleIsEmpty(feed, filter, 'ALL', q); + const visible = tribeChipVisible(feed, filter, { RECENT: isRecentItem, MINE: isMineItem, ALL: () => true, TOP: () => true }); + const chips = renderTribeModeChips(tribe, 'feed', { modes: FEED_MODES, filter: isCreate ? 'CREATE' : filter, labelOf: m => i18n[`${m}Button`], visible, emptyMod, create: member ? { label: i18n.createFeedTitle || 'Create Feed' } : null }); + if (isCreate) return div({ class: 'tribe-content-list' }, header, chips, renderFeedComposeForm(tribe)); + const items = applyTribeFilter(feed, filter.toLowerCase(), q, BASE_MODES, it => Number(it.refeeds || 0)); + const returnTo = sectionHref(tribe, 'feed', { filter, q }); + const banner = query.sent ? div({ class: 'feed-success-msg' }, p('✓ ' + (i18n.feedPublishedSuccess || i18n.tribeFeedSent))) : null; + const cards = paged(items).map(m => { + const chip = tribeReachChipFor(tribe, m); + const controls = renderTribeReachControls(tribe, m, returnTo); + return renderFeedCard(toFeedShape(m), null, { + hashtagHref: tribeTagHref(tribe), + refeedAction: `${tribeUrl}/refeed/${encodeURIComponent(m.id)}`, + canRefeed: member, + headerActions: tribeItemActions(tribe, m, member, m.description), + chips: chip || controls ? [chip ? div({ class: 'card-chips-row' }, chip) : null, controls] : null, + engagement: tribeEngagement(tribe, m, member, returnTo) + }); + }).filter(Boolean); + return div({ class: 'tribe-content-list' }, + header, + banner, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'feed', filter, q, renderModuleStats(items.length), i18n.feedSearchPlaceholder), + cards.length ? div({ class: 'feed-container' }, ...cards) : renderTribeEmpty(i18n.noFeedsFound || i18n.tribeFeedEmpty) + ); +}; + +const EVENT_MODES = { + ...BASE_MODES, + today: e => moment(e.date).isSame(moment(), 'day'), + week: e => moment(e.date).isBetween(moment(), moment().add(7, 'days'), null, '[]'), + month: e => moment(e.date).isBetween(moment(), moment().add(1, 'month'), null, '[]'), + year: e => moment(e.date).isBetween(moment(), moment().add(1, 'year'), null, '[]'), + archived: e => tribeStatusOf(e) === 'CLOSED' +}; +const EVENT_MODE_LIST = ['all', 'recent', 'mine', 'today', 'week', 'month', 'year', 'archived']; + +const renderTribeEventCard = (tribe, e, member, returnTo) => { + const tribeUrl = tribeUrlOf(tribe); + const isOpen = tribeStatusOf(e) === 'OPEN'; + const attending = asList(e.attendees).includes(userId); + return renderEventItem({ ...e, organizer: e.author, isPublic: reachLevelOf(e) === 'tribe' ? 'private' : 'public' }, 'all', null, { + href: null, + headerActions: tribeItemActions(tribe, e, member, e.title), + extraChips: [tribeReachChipFor(tribe, e)], + bodyExtra: [ + tribeDescription(e.description), + renderTribeReachControls(tribe, e, returnTo), + member && isOpen + ? div({ class: 'tribe-side-actions' }, + form({ method: 'POST', action: `${tribeUrl}/events/attend/${encodeURIComponent(e.id)}` }, + button({ type: 'submit', class: 'tribe-action-btn' }, attending ? i18n.tribeEventUnattend : i18n.tribeEventAttend) + ) + ) + : null, + tribeEngagement(tribe, e, member, returnTo) + ] + }); +}; + +const renderEventsSection = (tribe, items, query = {}) => { + const events = asList(items); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.tribeSectionEvents, i18n.eventsDescription); + const emptyMod = !isCreate && moduleIsEmpty(events, filter, 'all', q); + const chips = renderTribeModeChips(tribe, 'events', { modes: EVENT_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: m => i18n[`eventFilter${capFirst(m)}`], visible: tribeChipVisible(events, filter, EVENT_MODES), emptyMod, create: member ? { label: i18n.eventCreateButton } : null }); + if (isCreate) { + const today = new Date().toISOString().split('T')[0]; + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'event-form' }, + form({ method: 'POST', action: `${tribeUrl}/events/create` }, + label(i18n.eventTitleLabel), br(), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), + label(i18n.eventDescriptionLabel), br(), + textarea({ name: 'description', id: 'description', rows: '4', maxlength: String(TEXT_CAP), required: true, placeholder: i18n.eventDescriptionPlaceholder }), br(), + label(i18n.eventDateLabel), br(), + input({ type: 'date', name: 'date', id: 'date', required: true, min: today }), br(), br(), + label(i18n.eventLocationLabel), br(), + input({ type: 'text', name: 'location', id: 'location' }), br(), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, + button({ type: 'submit' }, i18n.eventCreateButton) + ) + ) + ); + } + const list = applyTribeFilter(events, filter, q, EVENT_MODES); + const returnTo = sectionHref(tribe, 'events', { filter, q }); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'events', filter, q, openClosedStats(list, e => tribeStatusOf(e) === 'CLOSED', i18n.eventStatusOpen, i18n.eventStatusClosed), i18n.eventSearchPlaceholder), + list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(e => renderTribeEventCard(tribe, e, member, returnTo))) : renderTribeEmpty(i18n.noevents || i18n.tribeEventsEmpty) + ); +}; + +const TASK_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']; +const TASK_MODES = { + ...BASE_MODES, + assigned: t => asList(t.assignees).includes(userId), + open: t => tribeStatusOf(t) === 'OPEN', + 'in-progress': t => tribeStatusOf(t) === 'IN-PROGRESS', + closed: t => tribeStatusOf(t) === 'CLOSED', + ...Object.fromEntries(TASK_PRIORITIES.map(pr => [`priority-${pr.toLowerCase()}`, t => String(t.priority || '').toUpperCase() === pr])) +}; +const TASK_MODE_LIST = ['all', 'recent', 'mine', 'assigned', 'open', 'in-progress', 'closed', ...TASK_PRIORITIES.map(pr => `priority-${pr.toLowerCase()}`)]; +const taskModeLabel = (m) => { + if (m === 'in-progress') return i18n.taskFilterInProgress; + if (m === 'priority-critical') return i18n.tribePriorityCritical; + if (m.startsWith('priority-')) return i18n[`taskFilter${capFirst(m.slice(9))}`]; + return i18n[`taskFilter${capFirst(m)}`]; +}; + +const renderTribeTaskCard = (tribe, t, member, returnTo) => { + const tribeUrl = tribeUrlOf(tribe); + const assigned = asList(t.assignees).includes(userId); + const assignRow = member ? renderTaskAssignAction(t, assigned, returnTo, `${tribeUrl}/tasks/assign/${encodeURIComponent(t.id)}`) : null; + return renderTaskItem({ ...t, isPublic: reachLevelOf(t) === 'tribe' ? 'PRIVATE' : 'PUBLIC' }, 'all', null, { + href: null, + headerActions: tribeItemActions(tribe, t, member, t.title), + extraChips: [tribeReachChipFor(tribe, t)], + bodyExtra: [ + tribeDescription(t.description), + t.deadline ? p({ class: 'job-meta-line' }, `${i18n.tribeTaskDeadline}: ${t.deadline}`) : null, + renderTribeReachControls(tribe, t, returnTo), + assignRow ? div({ class: 'tribe-side-actions housing-status-row task-assign-row' }, ...assignRow) : null, + member && isMineItem(t) ? renderTaskStatusRow(t, returnTo, `${tribeUrl}/tasks/status/${encodeURIComponent(t.id)}`) : null, + tribeEngagement(tribe, t, member, returnTo) + ] + }); +}; + +const renderTasksSection = (tribe, items, query = {}) => { + const tasks = asList(items); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.tribeSectionTasks, i18n.tasksDescription); + const emptyMod = !isCreate && moduleIsEmpty(tasks, filter, 'all', q); + const chips = renderTribeModeChips(tribe, 'tasks', { modes: TASK_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: taskModeLabel, visible: tribeChipVisible(tasks, filter, TASK_MODES), emptyMod, create: member ? { label: i18n.taskCreateButton } : null }); + if (isCreate) { + const today = new Date().toISOString().split('T')[0]; + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'task-form' }, + form({ method: 'POST', action: `${tribeUrl}/tasks/create` }, + label(i18n.taskTitleLabel), br(), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), + label(i18n.taskDescriptionLabel), br(), + textarea({ name: 'description', id: 'description', rows: '4', maxlength: String(TEXT_CAP), required: true, placeholder: i18n.taskDescriptionPlaceholder }), br(), + label(i18n.taskPriorityLabel), br(), + select({ name: 'priority', id: 'priority', required: true }, + ...TASK_PRIORITIES.map(pr => option({ value: pr }, i18n[`tribePriority${capFirst(pr.toLowerCase())}`])) + ), br(), br(), + label(i18n.tribeTaskDeadline), br(), + input({ type: 'date', name: 'deadline', id: 'deadline', min: today }), br(), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, + button({ type: 'submit' }, i18n.taskCreateButton) + ) + ) + ); + } + const list = applyTribeFilter(tasks, filter, q, TASK_MODES); + const returnTo = sectionHref(tribe, 'tasks', { filter, q }); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'tasks', filter, q, renderModuleStatsBy(list, tribeStatusOf, [{ value: 'OPEN', label: i18n.taskStatusOpen }, { value: 'IN-PROGRESS', label: i18n.taskStatusInProgress }, { value: 'CLOSED', label: i18n.taskStatusClosed }]), i18n.taskSearchPlaceholder), + list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(t => renderTribeTaskCard(tribe, t, member, returnTo))) : renderTribeEmpty(i18n.notasks || i18n.tribeTasksEmpty) + ); +}; + +const toVoteShape = (v) => { + const options = asList(v.options).map(o => String(o)); + const votes = v.votes || {}; + const counts = {}; + options.forEach((o, i) => { counts[o] = (counts[o] || 0) + asList(votes[String(i)]).length; }); + const totalVotes = Object.values(counts).reduce((s, n) => s + n, 0); + return { ...v, question: v.title, createdBy: v.author, options, votes: counts, totalVotes }; +}; +const VOTE_MODES = openClosedModes(v => tribeStatusOf(v) === 'CLOSED'); +const VOTE_MODE_LIST = ['all', 'recent', 'mine', 'open', 'closed']; + +const renderTribeVotationCard = (tribe, v, member, returnTo) => { + const tribeUrl = tribeUrlOf(tribe); + const shaped = toVoteShape(v); + const isOpen = tribeStatusOf(v) === 'OPEN'; + const hasVoted = Object.values(v.votes || {}).some(arr => asList(arr).includes(userId)); + return renderVoteListItem(shaped, shaped.options, 'all', null, { + href: null, + headerActions: tribeItemActions(tribe, v, member, v.title), + extraChips: [tribeReachChipFor(tribe, v)], + bodyExtra: [ + tribeDescription(v.description), + renderTribeReachControls(tribe, v, returnTo), + ...renderVoteOptionsBlock(shaped, shaped.options, `${tribeUrl}/votations/${encodeURIComponent(v.id)}/vote`, returnTo, member && isOpen && !hasVoted), + member && isMineItem(v) && isOpen + ? div({ class: 'tribe-side-actions' }, + form({ method: 'POST', action: `${tribeUrl}/votations/close/${encodeURIComponent(v.id)}` }, + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeVotationClose) + ) + ) + : null, + tribeEngagement(tribe, v, member, returnTo) + ] + }); +}; + +const renderVotationsSection = (tribe, items, query = {}) => { + const votations = asList(items); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.tribeSectionVotations, i18n.votationsDescription); + const emptyMod = !isCreate && moduleIsEmpty(votations, filter, 'all', q); + const chips = renderTribeModeChips(tribe, 'votations', { modes: VOTE_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: m => i18n[`voteFilter${capFirst(m)}`], visible: tribeChipVisible(votations, filter, VOTE_MODES), emptyMod, create: member ? { label: i18n.voteCreateButton } : null }); + if (isCreate) { + const today = new Date().toISOString().split('T')[0]; + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'vote-form' }, + form({ method: 'POST', action: `${tribeUrl}/votations/create` }, + h2(i18n.voteQuestionLabel), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), br(), + label(i18n.tribeVotationDescription), br(), + textarea({ name: 'description', id: 'description', rows: '3', maxlength: String(TEXT_CAP) }), br(), br(), + label(i18n.voteDeadlineLabel), br(), + input({ type: 'date', name: 'deadline', id: 'deadline', min: today }), br(), br(), + label(i18n.tribeVotationOptions), br(), + input({ type: 'text', name: 'option1', placeholder: `${i18n.tribeVotationOptionPlaceholder} 1`, required: true }), br(), + input({ type: 'text', name: 'option2', placeholder: `${i18n.tribeVotationOptionPlaceholder} 2`, required: true }), br(), + input({ type: 'text', name: 'option3', placeholder: `${i18n.tribeVotationOptionPlaceholder} 3` }), br(), + input({ type: 'text', name: 'option4', placeholder: `${i18n.tribeVotationOptionPlaceholder} 4` }), br(), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, + button({ type: 'submit' }, i18n.voteCreateButton) + ) + ) + ); + } + const list = applyTribeFilter(votations, filter, q, VOTE_MODES); + const returnTo = sectionHref(tribe, 'votations', { filter, q }); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'votations', filter, q, openClosedStats(list, v => tribeStatusOf(v) === 'CLOSED', i18n.voteStatusOpen, i18n.voteStatusClosed), i18n.votesSearchPlaceholder), + list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(v => renderTribeVotationCard(tribe, v, member, returnTo))) : renderTribeEmpty(i18n.novotes || i18n.tribeVotationsEmpty) + ); +}; + +const POLL_MODES = { + RECENT: isRecentItem, MINE: isMineItem, ALL: () => true, TOP: () => true, + VOTED: pl => !!pl.hasVoted || asList(pl.myChoices).length > 0, + OPEN: pl => tribeStatusOf(pl) === 'OPEN', CLOSED: pl => tribeStatusOf(pl) === 'CLOSED' +}; +const POLL_MODE_LIST = ['ALL', 'RECENT', 'MINE', 'TOP', 'VOTED', 'OPEN', 'CLOSED']; +const pollModeLabel = (m) => i18n[`pollFilter${capFirst(m.toLowerCase())}`]; + +const renderTribePollsSection = (tribe, items, query = {}) => { + const polls = asList(items).filter(pl => !pl.undecryptable); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'ALL').toUpperCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.pollsTitle, i18n.pollsDescription); + const emptyMod = !isCreate && moduleIsEmpty(polls, filter, 'ALL', q); + const chips = renderTribeModeChips(tribe, 'polls', { modes: POLL_MODE_LIST, filter: isCreate ? 'CREATE' : filter, labelOf: pollModeLabel, visible: tribeChipVisible(polls, filter, POLL_MODES), emptyMod, create: member ? { label: i18n.pollCreateButton } : null }); + if (isCreate) { + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'div-center audio-form' }, + h2(i18n.pollCreateTitle), + form({ method: 'POST', action: `${tribeUrl}/polls/create` }, + label(i18n.pollQuestion), br(), + input({ type: 'text', name: 'question', required: true, maxlength: '300', placeholder: i18n.pollQuestionPlaceholder }), br(), br(), + label(i18n.pollOptions), br(), + textarea({ name: 'options', rows: '5', required: true, maxlength: '3000', placeholder: i18n.pollOptionsPlaceholder }, ''), br(), + div({ class: 'poll-switch' }, + input({ type: 'hidden', name: 'anonymous', value: '0' }), + label(input({ type: 'checkbox', name: 'anonymous', value: '1' }), ' ', i18n.pollAnonymousLabel) + ), + div({ class: 'poll-switch' }, + input({ type: 'hidden', name: 'multiple', value: '0' }), + label(input({ type: 'checkbox', name: 'multiple', value: '1' }), ' ', i18n.pollMultipleLabel) + ), + br(), + button({ type: 'submit', class: 'create-button' }, i18n.pollPublishButton) + ) + ) + ); + } + const pred = POLL_MODES[filter] || (() => true); + const kept = polls.filter(pl => pred(pl) && matchesTribeQuery(pl, q)); + const list = filter === 'TOP' ? [...kept].sort((a, b) => (b.totalVoters || 0) - (a.totalVoters || 0) || tsOf(b) - tsOf(a)) : sortRecentFirst(kept); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'polls', filter, q, openClosedStats(list, pl => tribeStatusOf(pl) === 'CLOSED', i18n.pollStatusOpen, i18n.pollStatusClosed), i18n.pollSearchPlaceholder), + list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(pl => renderPollCard(pl, filter, null))) : renderTribeEmpty(i18n.pollsNoItems) + ); +}; + +const FORUM_CATEGORIES = ['GENERAL', 'PROPOSAL', 'QUESTION', 'ANNOUNCEMENT']; +const forumCatLabel = (c) => forumCatI18n()[c] || c; +const forumScoreBox = (tribe, item, member, thread) => + div({ class: 'forum-score-box' }, + member + ? form({ method: 'POST', action: `${tribeUrlOf(tribe)}/forum/${encodeURIComponent(item.id)}/refeed${thread ? `?thread=${encodeURIComponent(thread)}` : ''}`, class: 'forum-score-form' }, + button({ type: 'submit', class: 'score-btn', title: i18n.refeedButton }, '▲'), + div({ class: 'score-total' }, String(item.refeeds || 0)) + ) + : div({ class: 'score-total' }, String(item.refeeds || 0)) + ); + +const renderForumThreadDetail = (tribe, allItems, threadId, member) => { + const tribeUrl = tribeUrlOf(tribe); + const thread = allItems.find(i => i.id === threadId); + if (!thread) return renderTribeEmpty(i18n.tribeForumEmpty); + const replies = allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === threadId) + .sort((a, b) => (b.refeeds || 0) - (a.refeeds || 0) || tsOf(a) - tsOf(b)); + const participants = new Set([thread.author, ...replies.map(r => r.author)].filter(Boolean)); + const returnTo = sectionHref(tribe, 'forum', { thread: threadId }); + const reachChip = tribeReachChipFor(tribe, thread); + return div({ class: 'forum-thread-container' }, + div({ class: 'forum-card forum-thread-header' }, + div({ class: 'root-vote-col' }, forumScoreBox(tribe, thread, member, threadId)), + div({ class: 'forum-main-col' }, + div({ class: 'card-header activity-card-header' }, + renderContentActions(thread.id, null, { author: thread.author, reportTitle: thread.title, deleteAction: member && isMineItem(thread) ? `${tribeUrl}/content/delete/${encodeURIComponent(thread.id)}` : undefined }) + ), + div({ class: 'forum-header-row' }, + thread.category ? span({ class: 'forum-category' }, `[${forumCatLabel(thread.category)}]`) : null, + span({ class: 'forum-title' }, thread.title), + reachChip + ), + renderTribeReachControls(tribe, thread, returnTo), + div({ class: 'forum-body' }, ...tribeText(tribe, thread.description || '')), + div({ class: 'forum-meta' }, + span({ class: 'forum-positive-votes' }, `▲: ${thread.refeeds || 0}`), + span({ class: 'forum-participants' }, `${String(i18n.forumParticipants).toUpperCase()}: ${participants.size}`), + span({ class: 'forum-messages' }, `${String(i18n.forumMessages).toUpperCase()}: ${replies.length}`) + ), + div({ class: 'forum-footer' }, + span({ class: 'date-link' }, `${moment(thread.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(thread.author) + ), + tribeEngagement(tribe, thread, member, returnTo) + ) + ), + member + ? div({ class: 'new-message-wrapper' }, + form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(threadId)}/reply`, class: 'new-message-form' }, + textarea({ maxlength: String(TEXT_CAP), name: 'description', rows: '4', required: true, placeholder: i18n.forumMessagePlaceholder, class: 'new-message-textarea' }), br(), + button({ type: 'submit', class: 'forum-send-btn' }, i18n.forumSendButton) + ) + ) + : null, + ...replies.map((r, idx) => + div({ class: `forum-comment level-0${idx === 0 ? ' highlighted-reply' : ''}` }, + div({ class: 'comment-header' }, + span({ class: 'date-link' }, `${moment(r.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(r.author), + div({ class: 'comment-votes' }, + span({ class: 'votes-count' }, `▲: ${r.refeeds || 0}`), + tribeItemActions(tribe, r, member, r.description) + ) + ), + div({ class: 'comment-body-row' }, + div({ class: 'comment-vote-col' }, forumScoreBox(tribe, r, member, threadId)), + div({ class: 'comment-text-col' }, + div(...String(r.description || '').split('\n').map(l => l.trim()).filter(Boolean).map(l => p(...tribeText(tribe, l, { zoomImages: true })))) + ) + ) ) ) ); }; +const renderForumSection = (tribe, items, query = {}) => { + const allItems = asList(items); + const threads = allItems.filter(i => i.contentType === 'forum'); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all'); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.tribeSectionForum, i18n.forumDescription); + const replyCount = new Map(); + allItems.filter(i => i.contentType === 'forum-reply').forEach(r => replyCount.set(r.parentId, (replyCount.get(r.parentId) || 0) + 1)); + const forumScore = (t) => (t.refeeds || 0) + (replyCount.get(t.id) || 0); + const modes = { ...BASE_MODES, ...Object.fromEntries(FORUM_CATEGORIES.map(c => [c, t => t.category === c])) }; + const emptyMod = !isCreate && !query.thread && moduleIsEmpty(threads, filter, 'all', q); + const present = new Set(threads.map(t => t.category).filter(Boolean)); + const visible = (m) => FORUM_CATEGORIES.includes(m) ? (present.has(m) || m === filter) : tribeChipVisible(threads, filter, modes)(m); + const labelOf = (m) => FORUM_CATEGORIES.includes(m) ? forumCatLabel(m) : i18n[`forumFilter${capFirst(m)}`]; + const chips = renderTribeModeChips(tribe, 'forum', { modes: ['all', 'recent', 'mine', 'top', ...FORUM_CATEGORIES], filter: isCreate ? 'create' : filter, labelOf, visible, emptyMod, create: member ? { label: i18n.forumCreateButton } : null }); + + if (isCreate) { + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'forum-form' }, + form({ method: 'POST', action: `${tribeUrl}/forum/create` }, + label(i18n.forumCategoryLabel), br(), + select({ name: 'category', required: true }, ...FORUM_CATEGORIES.map(c => option({ value: c }, forumCatLabel(c)))), br(), br(), + label(i18n.forumTitleLabel), br(), + input({ type: 'text', name: 'title', maxlength: '100', required: true, placeholder: i18n.forumTitlePlaceholder }), br(), br(), + label(i18n.forumMessageLabel), br(), + textarea({ maxlength: String(TEXT_CAP), name: 'description', required: true, rows: '4', placeholder: i18n.forumMessagePlaceholder }), br(), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, + button({ type: 'submit' }, i18n.forumCreateButton) + ) + ) + ); + } + + if (query.thread) { + return div({ class: 'tribe-content-list' }, header, chips, + form({ method: 'GET', action: tribeUrl, class: 'forum-back-form' }, + input({ type: 'hidden', name: 'section', value: 'forum' }), + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.walletBack) + ), + renderForumThreadDetail(tribe, allItems, query.thread, member) + ); + } + + const list = applyTribeFilter(threads, filter, q, modes, forumScore); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'forum', filter, q, renderModuleStats(list.length), i18n.forumSearchPlaceholder), + list.length + ? ul({ class: 'mailing-archive' }, + ...paged(list).map(t => { + const participants = new Set([t.author, ...allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === t.id).map(r => r.author)].filter(Boolean)); + return li({ class: 'mailing-archive-item' }, + div({ class: 'emergency-update-head mailing-archive-head' }, + div({ class: 'mailing-archive-meta' }, + a({ href: sectionHref(tribe, 'forum', { thread: t.id }), class: 'user-link' }, t.title || '—'), + t.category ? span({ class: 'forum-category' }, `[${forumCatLabel(t.category)}]`) : null, + span({ class: 'card-label activity-update-counts mailing-counts' }, `👥: ${participants.size} · 💬 ${replyCount.get(t.id) || 0} · ▲ ${t.refeeds || 0}`), + tribeReachChipFor(tribe, t) + ), + tribeItemActions(tribe, t, member, t.title) + ) + ); + }) + ) + : renderTribeEmpty(i18n.noForums || i18n.tribeForumEmpty) + ); +}; + +const MEDIA_SECTIONS = { + image: { key: 'images', list: renderImageList, wrapper: 'images-list', formClass: 'image-form', title: 'tribeSectionImages', description: 'imageDescription', create: 'imageCreateButton', search: 'imageSearchPlaceholder', empty: 'noImages', file: 'imageFileLabel', titleLabel: 'imageTitleLabel', descLabel: 'imageDescriptionLabel', filter: 'imageFilter', accept: 'image/*' }, + audio: { key: 'audios', list: renderAudioList, wrapper: 'audios-list', formClass: 'audio-form', title: 'tribeSectionAudios', description: 'audioDescription', create: 'audioCreateButton', search: 'audioSearchPlaceholder', empty: 'noAudios', file: 'audioFileLabel', titleLabel: 'audioTitleLabel', descLabel: 'audioDescriptionLabel', filter: 'audioFilter', accept: 'audio/*' }, + video: { key: 'videos', list: renderVideoList, wrapper: 'videos-list', formClass: 'video-form', title: 'tribeSectionVideos', description: 'videoDescription', create: 'videoCreateButton', search: 'videoSearchPlaceholder', empty: 'noVideos', file: 'videoFileLabel', titleLabel: 'videoTitleLabel', descLabel: 'videoDescriptionLabel', filter: 'videoFilter', accept: 'video/*' }, + document: { key: 'documents', list: renderDocumentList, wrapper: 'documents-list', formClass: 'document-form', title: 'tribeSectionDocuments', description: 'documentDescription', create: 'documentCreateButton', search: 'documentSearchPlaceholder', empty: 'noDocuments', file: 'documentFileLabel', titleLabel: 'documentTitleLabel', descLabel: 'documentDescriptionLabel', filter: 'documentFilter', accept: 'application/pdf,.pdf,.doc,.docx,.txt,.odt' }, + bookmark: { key: 'bookmarks', list: renderBookmarkList, wrapper: 'bookmark-list', formClass: 'bookmark-form', title: 'tribeSectionBookmarks', description: 'bookmarkDescription', create: 'bookmarkCreateButton', search: 'bookmarkSearchPlaceholder', empty: 'noBookmarks', filter: 'bookmarkFilter' } +}; +const toMediaShape = (m) => ({ ...m, key: m.id, url: m.mediaType === 'bookmark' ? (m.url || m.description || '') : blobIdOf(m.image), updatedAt: undefined }); + +const renderTribeMediaTypeSection = (tribe, items, query = {}, mediaType) => { + const cfg = MEDIA_SECTIONS[mediaType]; + const media = asList(items).filter(m => m.mediaType === mediaType); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n[cfg.title], i18n[cfg.description]); + const emptyMod = !isCreate && moduleIsEmpty(media, filter, 'all', q); + const chips = renderTribeModeChips(tribe, cfg.key, { modes: ['all', 'recent', 'mine', 'top'], filter: isCreate ? 'create' : filter, labelOf: m => i18n[`${cfg.filter}${capFirst(m)}`], visible: tribeChipVisible(media, filter, BASE_MODES), emptyMod, create: member ? { label: i18n[cfg.create] } : null }); + + if (isCreate) { + const common = [ + input({ type: 'hidden', name: 'mediaType', value: mediaType }), + input({ type: 'hidden', name: 'returnSection', value: cfg.key }) + ]; + const formNode = mediaType === 'bookmark' + ? form({ method: 'POST', action: `${tribeUrl}/media/upload` }, + ...common, + label(i18n.bookmarkUrlLabel), br(), + input({ type: 'url', name: 'url', id: 'url', required: true, placeholder: i18n.bookmarkUrlPlaceholder }), br(), br(), + label(i18n.tribeMediaTitle), br(), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), br(), + label(i18n.bookmarkDescriptionLabel), br(), + textarea({ maxlength: String(TEXT_CAP), name: 'description', id: 'description', rows: '4', placeholder: i18n.bookmarkDescriptionPlaceholder }, ''), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, br(), + button({ type: 'submit' }, i18n[cfg.create]) + ) + : form({ method: 'POST', action: `${tribeUrl}/media/upload`, enctype: 'multipart/form-data' }, + ...common, + label(i18n[cfg.file]), br(), + input({ type: 'file', name: 'media', id: 'media', accept: cfg.accept, required: true }), br(), br(), + label(i18n[cfg.titleLabel]), br(), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), + label(i18n[cfg.descLabel]), br(), + textarea({ maxlength: String(TEXT_CAP), name: 'description', id: 'description', rows: '4' }, ''), br(), + ...renderLicenseSelect('', i18n), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, br(), + button({ type: 'submit' }, i18n[cfg.create]) + ); + return div({ class: 'tribe-content-list' }, header, chips, div({ class: `div-center ${cfg.formClass}` }, formNode)); + } + + const list = applyTribeFilter(media, filter, q, BASE_MODES).map(toMediaShape); + const returnTo = sectionHref(tribe, cfg.key, { filter, q }); + const params = { + q, + hideLastVisit: true, + headerActions: (m) => tribeItemActions(tribe, m, member, m.title), + titleChips: (m) => [tribeReachChipFor(tribe, m)], + bodyExtra: (m) => [tribeDescription(m.description && m.description !== m.url ? m.description : null), renderTribeReachControls(tribe, m, returnTo)], + engagement: (m) => tribeEngagement(tribe, m, member, returnTo) + }; + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, cfg.key, filter, q, renderModuleStats(list.length), i18n[cfg.search]), + list.length ? div({ class: cfg.wrapper }, cfg.list(paged(list), filter, params)) : renderTribeEmpty(i18n[cfg.empty] || i18n.tribeMediaEmpty) + ); +}; + +const renderStandaloneSection = (tribe, sectionKey, items, query, cfg) => { + const list = asList(items); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const modes = cfg.modes || BASE_MODES; + const modeList = cfg.modeList || ['all', 'recent', 'mine']; + const header = renderTribeSectionHeader(cfg.title, cfg.description); + const emptyMod = moduleIsEmpty(list, filter, 'all', q); + const chips = renderTribeModeChips(tribe, sectionKey, { + modes: modeList, filter, labelOf: cfg.labelOf, visible: tribeChipVisible(list, filter, modes), emptyMod, + create: member && cfg.createAction ? { action: cfg.createAction, hidden: { filter: 'create', tribeId: tribe.id }, label: cfg.createLabel } : null + }); + const shown = applyTribeFilter(list, filter, q, modes, cfg.score || (() => 0)); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, sectionKey, filter, q, cfg.stats ? cfg.stats(shown) : renderModuleStats(shown.length), cfg.placeholder), + shown.length ? cfg.render(paged(shown), member) : renderTribeEmpty(cfg.empty) + ); +}; + +const withModuleReach = (tribe, item, contentType, card, returnTo) => { + if (item.tribeOrigin) return card; + const it = { ...item, id: item.rootId || item.id || item.key, contentType }; + const chip = tribeReachChipFor(tribe, it); + return div(card, chip ? div({ class: 'card-chips-row' }, chip) : null, renderTribeReachControls(tribe, it, returnTo)); +}; + +const renderTribeMapsSection = (tribe, maps, query = {}) => + renderStandaloneSection(tribe, 'maps', maps, query, { + title: i18n.tribeSectionMaps || 'MAPS', description: i18n.mapDescription, createAction: '/maps', createLabel: i18n.mapCreateButton || 'Create Map', + labelOf: m => i18n[`mapFilter${capFirst(m)}`], placeholder: i18n.mapSearchPlaceholder, empty: i18n.noMaps, + render: (list) => { + const returnTo = sectionHref(tribe, 'maps', { filter: query.filter, q: query.q }); + return div({ class: 'jobs-grid' }, ...list.map(m => withModuleReach(tribe, m, 'map', renderMapCard(m, 'all', { q: query.q || '', returnTo }), returnTo))); + } + }); + +const renderTribeTorrentsSection = (tribe, torrents, query = {}) => + renderStandaloneSection(tribe, 'torrents', torrents, query, { + title: i18n.tribeSectionTorrents || 'TORRENTS', description: i18n.torrentsDescription, createAction: '/torrents', createLabel: i18n.tribeCreateTorrent || 'Upload Torrent', + labelOf: m => i18n[`torrentFilter${capFirst(m)}`], placeholder: i18n.torrentSearchPlaceholder, empty: i18n.tribeTorrentsEmpty || i18n.noTorrents, + render: (list, member) => { + const returnTo = sectionHref(tribe, 'torrents', { filter: query.filter, q: query.q }); + return div({ class: 'audios-list' }, renderTorrentTable(list, 'all', { + q: query.q || '', + rowFor: (t) => t._isMedia + ? { + titleChips: [tribeReachChipFor(tribe, t)], + details: renderTribeReachControls(tribe, { ...t, id: t.rootId || t.key, contentType: 'media' }, returnTo), + download: t.url ? renderTorrentDownload(torrentDownloadHref(t.url, t.title)) : null, + actions: tribeItemActions(tribe, { ...t, id: t.rootId || t.key }, member, t.title) + } + : null + })); + } + }); + +const renderTribeFilesSection = (tribe, files, query = {}) => + renderStandaloneSection(tribe, 'files', files, query, { + title: i18n.tribeSectionFiles || 'FILES', description: i18n.filesDescription, createAction: '/files', createLabel: i18n.tribeCreateFile || 'Upload File', + labelOf: m => i18n[`fileFilter${capFirst(m)}`], placeholder: i18n.fileSearchPlaceholder, empty: i18n.tribeFilesEmpty || i18n.noFiles, + render: (list) => div({ class: 'audios-list' }, renderFileTable(list, 'all', { q: query.q || '' })) + }); + +const closedByFlag = (it) => !!it.isClosed || String(it.status || '').toUpperCase() === 'CLOSED'; + +const renderTribePadsSection = (tribe, pads, query = {}) => + renderStandaloneSection(tribe, 'pads', pads, query, { + title: i18n.tribeSectionPads || 'PADS', description: i18n.padsDescription, createAction: '/pads', createLabel: i18n.tribePadCreate || 'Create Pad', + modes: openClosedModes(closedByFlag), modeList: ['all', 'recent', 'mine', 'open', 'closed'], + labelOf: m => i18n[`padFilter${capFirst(m)}`], placeholder: i18n.padSearchPlaceholder, empty: i18n.tribePadsEmpty || i18n.padsNoItems, + stats: (list) => renderModuleStatsBy(list, pd => pd.isClosed ? 'CLOSED' : String(pd.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.padStatusOpen }, { value: 'INVITE-ONLY', label: i18n.padStatusInviteOnly }, { value: 'CLOSED', label: i18n.padStatusClosed }]), + render: (list) => div({ class: 'tribe-grid pads-grid' }, ...list.map(pd => renderPadCard(pd, 'all', null))) + }); + +const renderTribeChatsSection = (tribe, chats, query = {}) => + renderStandaloneSection(tribe, 'chats', chats, query, { + title: i18n.tribeSectionChats || 'CHATS', description: i18n.modulesChatsDescription, createAction: '/chats', createLabel: i18n.tribeChatCreate || 'Create Chat', + modes: openClosedModes(closedByFlag), modeList: ['all', 'recent', 'mine', 'open', 'closed'], + labelOf: m => i18n[`chatFilter${capFirst(m)}`], placeholder: i18n.chatSearchPlaceholder, empty: i18n.tribeChatsEmpty || i18n.chatNoItems, + stats: (list) => renderModuleStatsBy(list, c => String(c.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.chatStatusOpen }, { value: 'INVITE-ONLY', label: i18n.chatStatusInviteOnly }, { value: 'CLOSED', label: i18n.chatStatusClosed }]), + render: (list) => div({ class: 'tribe-grid' }, ...list.map(c => renderChatCard(c, 'all', { q: query.q || '' }))) + }); + +const renderTribeCalendarsSection = (tribe, calendars, query = {}) => + renderStandaloneSection(tribe, 'calendars', calendars, query, { + title: i18n.tribeSectionCalendars || 'CALENDARS', description: i18n.calendarsDescription, createAction: '/calendars', createLabel: i18n.tribeCalendarCreate || 'Create Calendar', + modes: openClosedModes(closedByFlag), modeList: ['all', 'recent', 'mine', 'open', 'closed'], + labelOf: m => i18n[`calendarFilter${capFirst(m)}`], placeholder: i18n.calendarSearchPlaceholder, empty: i18n.tribeCalendarsEmpty || i18n.calendarsNoItems, + stats: (list) => openClosedStats(list, closedByFlag, i18n.calendarStatusOpen, i18n.calendarStatusClosed), + render: (list) => { + const returnTo = sectionHref(tribe, 'calendars', { filter: query.filter, q: query.q }); + return div({ class: 'tribe-grid' }, ...list.map(c => withModuleReach(tribe, c, 'calendar', renderCalendarCard(c, null), returnTo))); + } + }); + +const renderTribeWikiSectionPage = (tribe, pages, query = {}) => { + const list = asList(pages); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const emptyMod = moduleIsEmpty(list, filter, 'all', q); + const shown = applyTribeFilter(list, filter, q, BASE_MODES, () => 0); + const toolbar = div({ class: 'tribe-content-list' }, + renderTribeSectionHeader(i18n.tribeSectionWiki || 'WIKI', i18n.wikiDescription), + div({ class: 'mode-buttons-row' }, + ...(emptyMod ? [] : [ + ...['all', 'recent', 'mine'].filter(tribeChipVisible(list, filter, BASE_MODES)).map(m => + form({ method: 'GET', action: tribeUrlOf(tribe) }, + input({ type: 'hidden', name: 'section', value: 'wiki' }), + input({ type: 'hidden', name: 'filter', value: m }), + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(i18n[`wikiFilter${capFirst(m)}`] || m).toUpperCase()) + )), + form({ method: 'GET', action: '/wiki' }, + input({ type: 'hidden', name: 'filter', value: 'changes' }), + input({ type: 'hidden', name: 'tribeId', value: tribe.id }), + button({ type: 'submit', class: 'filter-btn' }, String(i18n.wikiChanges).toUpperCase()) + ) + ]), + member + ? form({ method: 'GET', action: '/wiki' }, + input({ type: 'hidden', name: 'filter', value: 'create' }), + input({ type: 'hidden', name: 'tribeId', value: tribe.id }), + button({ type: 'submit', class: 'create-button' }, i18n.tribeWikiCreate) + ) + : null + ), + emptyMod ? null : renderTribeSearchRow(tribe, 'wiki', filter, q, renderModuleStats(shown.length), i18n.wikiSearchPlaceholder) + ); + const returnTo = sectionHref(tribe, 'wiki', { filter: query.filter, q: query.q }); + return renderTribeWikiSection(tribe, shown, toolbar, (pg, card) => withModuleReach(tribe, pg, 'wiki', card, returnTo)); +}; + +const ROOM_MODES = { ...openClosedModes(r => !!r.isClosed), live: () => false }; +const renderTribeRoomsSectionPage = (tribe, sectionData, query = {}) => { + const rooms = asList(sectionData && sectionData.rooms); + const occupancy = sectionData && sectionData.occupancy; + const live = sectionData && sectionData.live; + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const modes = { ...ROOM_MODES, live: r => !!(occupancy && occupancy.get && occupancy.get(r.rootId) && occupancy.get(r.rootId).count > 0) }; + const emptyMod = moduleIsEmpty(rooms, filter, 'all', q); + const shown = applyTribeFilter(rooms, filter, q, modes, () => 0); + const toolbar = div({ class: 'tribe-content-list' }, + renderTribeSectionHeader(i18n.tribeSectionRooms || 'ROOMS', i18n.roomsDescription), + renderTribeModeChips(tribe, 'rooms', { + modes: ['all', 'recent', 'mine', 'live', 'open', 'closed'], filter, labelOf: m => i18n[`roomFilter${capFirst(m)}`], visible: tribeChipVisible(rooms, filter, modes), emptyMod, + create: member ? { action: '/rooms', hidden: { filter: 'create', tribeId: tribe.id }, label: i18n.tribeRoomCreate } : null + }), + emptyMod ? null : renderTribeSearchRow(tribe, 'rooms', filter, q, roomStatsBy(shown), i18n.roomSearchPlaceholder) + ); + return renderTribeRoomsSection(tribe, shown, occupancy, live, toolbar); +}; + +const renderInhabitantsSection = (tribe, members, query = {}) => { + const resolved = asList(members); + const search = String(query.search || '').trim().toLowerCase(); + const shown = search + ? resolved.filter(u => [u.name, u.id, u.description].some(v => String(v || '').toLowerCase().includes(search))) + : resolved; + return div({ class: 'tribe-content-list' }, + renderTribeSectionHeader(i18n.tribeSectionInhabitants, i18n.discoverPeople), + resolved.length ? renderTribeSearchRow(tribe, 'inhabitants', null, search, renderModuleStats(shown.length), i18n.searchInhabitantsPlaceholder, 'search') : null, + shown.length + ? div({ class: 'inhabitants-list inhabitants-grid' }, ...paged(shown).map(u => renderInhabitantCard(u, 'all', userId, false))) + : renderTribeEmpty(i18n.tribeInhabitantsEmpty || i18n.noInhabitantsFound) + ); +}; + const createButtonI18n = { - events: () => i18n.tribeEventCreate || 'Create Event', - tasks: () => i18n.tribeTaskCreate || 'Create Task', - votations: () => i18n.tribeVotationCreate || 'Create Votation', - forum: () => i18n.tribeForumCreate || 'Create Forum', subtribes: () => i18n.tribeSubTribesCreate || 'Create Sub-Tribe', }; @@ -749,10 +1565,11 @@ const renderCreateForm = (tribe, contentType, fields) => { form(formAttrs, ...fields.map(f => { const prefix = f.spaceBefore ? [br()] : []; - if (f.type === 'textarea') return [...prefix, label({ for: f.name }, f.label), br, textarea({ name: f.name, id: f.name, rows: f.rows || 4, required: f.required, placeholder: f.placeholder }, ''), br()]; + if (f.type === 'textarea') return [...prefix, label({ for: f.name }, f.label), br, textarea({ maxlength: String(TEXT_CAP), name: f.name, id: f.name, rows: f.rows || 4, required: f.required, placeholder: f.placeholder }, ''), br()]; if (f.type === 'select') return [...prefix, label({ for: f.name }, f.label), br, select({ name: f.name, id: f.name }, ...f.options.map(o => option({ value: o.value }, o.label))), br()]; if (f.type === 'file') return [...prefix, label({ for: f.name }, f.label), br, input({ type: 'file', name: f.name, id: f.name, accept: f.accept || '*/*' }), br()]; const attrs = { type: f.type || 'text', name: f.name, id: f.name, required: f.required, placeholder: f.placeholder }; + if (f.maxlength) attrs.maxlength = f.maxlength; if (f.min) attrs.min = f.min; return [...prefix, br, label({ for: f.name }, f.label), br, input(attrs), br()]; }).flat(),br(), @@ -761,514 +1578,6 @@ const renderCreateForm = (tribe, contentType, fields) => { ); }; -const renderEventsSection = (tribe, items, query) => { - const events = Array.isArray(items) ? items : []; - const action = query.action; - const today = new Date().toISOString().split('T')[0]; - if (action === 'create') { - return renderCreateForm(tribe, 'events', [ - { name: 'title', label: i18n.tribeEventTitle, required: true, placeholder: i18n.tribeEventTitle }, - { name: 'description', type: 'textarea', label: i18n.tribeEventDescription, required: true, placeholder: i18n.tribeEventDescription }, - { name: 'date', type: 'date', label: i18n.tribeEventDate, required: true, min: today }, - { name: 'location', label: i18n.tribeEventLocation, placeholder: i18n.tribeEventLocation }, - ]); - } - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(i18n.tribeSectionEvents), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'events' }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeEventCreate) - ) - ), - events.length === 0 ? p(i18n.tribeEventsEmpty) : - events.map(e => div({ class: 'tribe-content-card' }, - h2(e.title), - e.description ? p(...renderUrl(e.description)) : null, - e.date ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeEventDate || 'Date') + ':'), - span({ class: 'card-value' }, e.date) - ) : null, - e.location ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeEventLocation || 'Location') + ':'), - span({ class: 'card-value' }, ...renderUrl(e.location)) - ) : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeEventAttendees || 'Attendees') + ':'), - span({ class: 'card-value' }, String((e.attendees || []).length)) - ), - statusBadge(e.status), - div({ class: 'tribe-content-actions' }, - form({ method: 'POST', action: `${tribeUrl}/events/attend/${encodeURIComponent(e.id)}` }, - button({ type: 'submit', class: 'filter-btn' }, - (e.attendees || []).includes(userId) ? i18n.tribeEventUnattend : i18n.tribeEventAttend - ) - ), - e.author === userId ? form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(e.id)}` }, - button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.tribeContentDelete) - ) : null - ) - )) - ); -}; - -const renderTasksSection = (tribe, items, query) => { - const tasks = Array.isArray(items) ? items : []; - const action = query.action; - const today = new Date().toISOString().split('T')[0]; - if (action === 'create') { - return renderCreateForm(tribe, 'tasks', [ - { name: 'title', label: i18n.tribeTaskTitle, required: true, placeholder: i18n.tribeTaskTitle }, - { name: 'description', type: 'textarea', label: i18n.tribeTaskDescription, required: true, placeholder: i18n.tribeTaskDescription }, - { name: 'priority', type: 'select', label: i18n.tribeTaskPriority, options: [ - { value: 'LOW', label: i18n.tribePriorityLow }, { value: 'MEDIUM', label: i18n.tribePriorityMedium }, - { value: 'HIGH', label: i18n.tribePriorityHigh }, { value: 'CRITICAL', label: i18n.tribePriorityCritical } - ]}, - { name: 'deadline', type: 'date', label: i18n.tribeTaskDeadline, min: today }, - ]); - } - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(i18n.tribeSectionTasks), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'tasks' }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeTaskCreate) - ) - ), - tasks.length === 0 ? p(i18n.tribeTasksEmpty) : - tasks.map(t => div({ class: 'tribe-content-card' }, - h2(t.title), - t.description ? p(...renderUrl(t.description)) : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeTaskPriority || 'Priority') + ':'), - priorityLabel(t.priority) - ), - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeStatusLabel || 'Status') + ':'), - statusBadge(t.status) - ), - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeTaskAssignees || 'Assignees') + ':'), - span({ class: 'card-value' }, String((t.assignees || []).length)) - ), - br(), - t.deadline ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeTaskDeadline || 'Deadline') + ':'), - span({ class: 'card-value' }, t.deadline) - ) : null, - div({ class: 'tribe-content-actions' }, - form({ method: 'POST', action: `${tribeUrl}/tasks/assign/${encodeURIComponent(t.id)}` }, - button({ type: 'submit', class: 'filter-btn' }, - (t.assignees || []).includes(userId) ? i18n.tribeTaskUnassign : i18n.tribeTaskAssign - ) - ), - t.status !== 'IN-PROGRESS' && t.author === userId ? form({ method: 'POST', action: `${tribeUrl}/tasks/status/${encodeURIComponent(t.id)}` }, - input({ type: 'hidden', name: 'status', value: 'IN-PROGRESS' }), - button({ type: 'submit', class: 'filter-btn' }, i18n.tribeTaskStatusInProgress) - ) : null, - t.status !== 'CLOSED' && t.author === userId ? form({ method: 'POST', action: `${tribeUrl}/tasks/status/${encodeURIComponent(t.id)}` }, - input({ type: 'hidden', name: 'status', value: 'CLOSED' }), - button({ type: 'submit', class: 'filter-btn' }, i18n.tribeTaskStatusClosed) - ) : null, - t.author === userId ? form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(t.id)}` }, - button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.tribeContentDelete) - ) : null - ) - )) - ); -}; - -const renderVotationsSection = (tribe, items, query) => { - const votations = Array.isArray(items) ? items : []; - const action = query.action; - const today = new Date().toISOString().split('T')[0]; - if (action === 'create') { - return div({ class: 'create-tribe-form' }, - form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/votations/create` }, - label({ for: 'title' }, i18n.tribeVotationTitle), br, - input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeVotationTitle }), br(), - label({ for: 'description' }, i18n.tribeVotationDescription), br, - textarea({ name: 'description', id: 'description', rows: 3, placeholder: i18n.tribeVotationDescription }, ''), br(), - label({ for: 'deadline' }, i18n.tribeVotationDeadline), br, - input({ type: 'date', name: 'deadline', id: 'deadline', min: today }), br(), - br(), - label(i18n.tribeVotationOptions), br, - input({ type: 'text', name: 'option1', placeholder: `${i18n.tribeVotationOptionPlaceholder} 1`, required: true }), br(), - input({ type: 'text', name: 'option2', placeholder: `${i18n.tribeVotationOptionPlaceholder} 2`, required: true }), br(), - input({ type: 'text', name: 'option3', placeholder: `${i18n.tribeVotationOptionPlaceholder} 3` }), br(), - input({ type: 'text', name: 'option4', placeholder: `${i18n.tribeVotationOptionPlaceholder} 4` }), br(), - button({ type: 'submit', class: 'create-button' }, i18n.tribeVotationCreate) - ) - ); - } - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(i18n.tribeSectionVotations), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'votations' }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeVotationCreate) - ) - ), - votations.length === 0 ? p(i18n.tribeVotationsEmpty) : - votations.map(v => { - const opts = Array.isArray(v.options) ? v.options : []; - const votes = v.votes || {}; - const totalVotes = Object.values(votes).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0); - const hasVoted = Object.values(votes).some(arr => Array.isArray(arr) && arr.includes(userId)); - const isOpen = v.status === 'OPEN'; - - return div({ class: 'tribe-content-card' }, - h2(v.title), - v.description ? p(...renderUrl(v.description)) : null, - statusBadge(v.status), - v.deadline ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeVotationDeadline || 'Deadline') + ':'), - span({ class: 'card-value' }, v.deadline) - ) : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeVotationResults || 'Votes') + ':'), - span({ class: 'card-value' }, String(totalVotes)) - ), - br(), - div({ class: 'tribe-votation-options' }, - opts.map((opt, idx) => { - const count = Array.isArray(votes[String(idx)]) ? votes[String(idx)].length : 0; - const pct = totalVotes > 0 ? Math.round((count / totalVotes) * 100) : 0; - const roundedPct = Math.round(pct / 5) * 5; - return div({ class: 'tribe-votation-option' }, - span({ class: 'tribe-votation-label' }, opt), - div({ class: 'tribe-votation-bar' }, - div({ class: `tribe-votation-fill tribe-fill-${roundedPct}` }) - ), - span({ class: 'tribe-votation-count' }, `${count} (${pct}%)`), - isOpen && !hasVoted ? form({ method: 'POST', action: `${tribeUrl}/votations/${encodeURIComponent(v.id)}/vote` }, - input({ type: 'hidden', name: 'optionIndex', value: String(idx) }), - button({ type: 'submit', class: 'filter-btn' }, i18n.tribeVotationVote) - ) : null - ); - }) - ), - v.author === userId ? div({ class: 'tribe-content-actions' }, - isOpen ? form({ method: 'POST', action: `${tribeUrl}/votations/close/${encodeURIComponent(v.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeVotationClose) - ) : null, - form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(v.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeContentDelete) - ) - ) : null - ); - }) - ); -}; - -const renderForumSection = (tribe, items, query) => { - const allItems = Array.isArray(items) ? items : []; - const threads = allItems.filter(i => i.contentType === 'forum'); - const action = query.action; - const threadId = query.thread; - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - - if (action === 'create') { - return renderCreateForm(tribe, 'forum', [ - { name: 'title', label: i18n.tribeForumTitle, required: true, placeholder: i18n.tribeForumTitle }, - { name: 'description', type: 'textarea', label: i18n.tribeForumText, required: true, placeholder: i18n.tribeForumText, rows: 6 }, - { name: 'category', type: 'select', label: i18n.tribeForumCategory, options: [ - { value: 'GENERAL', label: i18n.tribeForumCatGeneral }, { value: 'PROPOSAL', label: i18n.tribeForumCatProposal }, - { value: 'QUESTION', label: i18n.tribeForumCatQuestion }, { value: 'ANNOUNCEMENT', label: i18n.tribeForumCatAnnouncement } - ]} - ]); - } - - if (threadId) { - const thread = allItems.find(i => i.id === threadId); - const replies = allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === threadId) - .sort((a, b) => (a.refeeds || 0) - (b.refeeds || 0) !== 0 ? (b.refeeds || 0) - (a.refeeds || 0) : (Date.parse(a.createdAt) || 0) - (Date.parse(b.createdAt) || 0)); - if (!thread) return p(i18n.tribeForumEmpty); - const replyCount = replies.length; - return div({ class: 'tribe-content-list' }, - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'forum' }), - button({ type: 'submit', class: 'filter-btn' }, i18n.walletBack) - ), - div({ class: 'forum-card forum-thread-header' }, - div({ class: 'forum-score-col' }, - div({ class: 'forum-score-box' }, - form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(thread.id)}/refeed` }, - button({ type: 'submit', class: 'score-btn' }, '▲') - ), - div({ class: 'score-total' }, String(thread.refeeds || 0)), - ) - ), - div({ class: 'forum-main-col' }, - div({ class: 'forum-header-row' }, - thread.category ? span({ class: 'forum-category' }, `[${forumCatI18n()[thread.category] || thread.category}]`) : null, - span({ class: 'forum-title' }, thread.title) - ), - div({ class: 'forum-footer' }, - span({ class: 'date-link' }, `${new Date(thread.createdAt).toLocaleString()} ${i18n.performed || ''}`), - userLink(thread.author) - ), - div({ class: 'forum-body' }, ...renderUrl(thread.description || '')), - div({ class: 'forum-meta' }, - span({ class: 'forum-positive-votes' }, `▲: ${thread.refeeds || 0}`), - span({ class: 'forum-messages' }, `${(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase()}: ${replyCount}`) - ) - ) - ), - div({ class: 'tribe-forum-reply-form' }, - form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(threadId)}/reply` }, - textarea({ name: 'description', rows: 3, required: true, placeholder: i18n.tribeForumReply }), - br(), - button({ type: 'submit', class: 'forum-send-btn' }, i18n.tribeForumReply) - ) - ), - replies.length > 0 - ? replies.map((r, idx) => - div({ class: `forum-comment${idx === 0 ? ' highlighted-reply' : ''}` }, - div({ class: 'comment-header' }, - span({ class: 'date-link' }, `${new Date(r.createdAt).toLocaleString()} ${i18n.performed || ''}`), - userLink(r.author), - div({ class: 'comment-votes' }, - span({ class: 'forum-positive-votes' }, `▲: ${r.refeeds || 0}`) - ) - ), - div({ class: 'comment-body-row' }, - div({ class: 'comment-vote-col' }, - div({ class: 'forum-score-box' }, - form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(r.id)}/refeed` }, - button({ type: 'submit', class: 'score-btn' }, '▲') - ), - div({ class: 'score-total' }, String(r.refeeds || 0)) - ) - ), - div({ class: 'comment-text-col' }, - ...(r.description || '').split('\n').map(l => l.trim()).filter(l => l).map(l => p(...renderUrl(l))) - ) - ), - r.author === userId ? div({ class: 'tribe-content-actions' }, - form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(r.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeContentDelete) - ) - ) : null - ) - ) - : null - ); - } - - const replyMap = new Map(); - allItems.filter(i => i.contentType === 'forum-reply').forEach(r => { replyMap.set(r.parentId, (replyMap.get(r.parentId) || 0) + 1); }); - const sortedThreads = [...threads].sort((a, b) => ((b.refeeds || 0) + (replyMap.get(b.id) || 0)) - ((a.refeeds || 0) + (replyMap.get(a.id) || 0))); - - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(i18n.tribeSectionForum), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'forum' }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeForumCreate) - ) - ), - sortedThreads.length === 0 ? p(i18n.tribeForumEmpty) : - div({ class: 'forum-list' }, - sortedThreads.map(t => { - const replyCount = allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === t.id).length; - return div({ class: 'forum-card' }, - div({ class: 'forum-score-col' }, - div({ class: 'forum-score-box' }, - form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(t.id)}/refeed` }, - button({ type: 'submit', class: 'score-btn' }, '▲') - ), - div({ class: 'score-total' }, String(t.refeeds || 0)) - ) - ), - div({ class: 'forum-main-col' }, - div({ class: 'forum-header-row' }, - t.category ? span({ class: 'forum-category' }, `[${forumCatI18n()[t.category] || t.category}]`) : null, - form({ method: 'GET', action: tribeUrl, class: 'forum-title-form' }, - input({ type: 'hidden', name: 'section', value: 'forum' }), - input({ type: 'hidden', name: 'thread', value: t.id }), - button({ type: 'submit', class: 'forum-title' }, t.title) - ) - ), - t.description ? div({ class: 'forum-body' }, ...renderUrl((t.description || '').substring(0, 200))) : null, - div({ class: 'forum-meta' }, - span({ class: 'forum-positive-votes' }, `▲: ${t.refeeds || 0}`), - span({ class: 'forum-messages' }, `${(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase()}: ${replyCount}`), - form({ method: 'GET', action: tribeUrl, class: 'visit-forum-form' }, - input({ type: 'hidden', name: 'section', value: 'forum' }), - input({ type: 'hidden', name: 'thread', value: t.id }), - button({ type: 'submit', class: 'filter-btn' }, i18n.forumVisitButton || 'VISIT') - ) - ), - div({ class: 'forum-footer' }, - span({ class: 'date-link' }, `${new Date(t.createdAt).toLocaleString()} ${i18n.performed || ''}`), - userLink(t.author) - ), - t.author === userId ? div({ class: 'forum-owner-actions' }, - form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(t.id)}`, class: 'forum-delete-form' }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeContentDelete) - ) - ) : null - ) - ); - }) - ) - ); -}; - -const sectionKeyForMediaType = { image: 'images', audio: 'audios', video: 'videos', document: 'documents', bookmark: 'bookmarks', torrent: 'torrents' }; -const acceptForMediaType = { image: 'image/*', audio: 'audio/*', video: 'video/*', document: 'application/pdf,.pdf,.doc,.docx,.txt,.odt', bookmark: null, torrent: '.torrent' }; -const sectionTitleForMediaType = (mt) => { - const map = { image: i18n.tribeSectionImages, audio: i18n.tribeSectionAudios, video: i18n.tribeSectionVideos, document: i18n.tribeSectionDocuments, bookmark: i18n.tribeSectionBookmarks, torrent: i18n.tribeSectionTorrents }; - return map[mt] || mt; -}; - -const renderTribeMediaTypeSection = (tribe, items, query, mediaType) => { - const allMedia = Array.isArray(items) ? items : []; - const media = allMedia.filter(m => m.mediaType === mediaType); - const action = query.action; - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - const sectionKey = sectionKeyForMediaType[mediaType] || 'media'; - const sTitle = sectionTitleForMediaType(mediaType); - const createMediaLabel = { - image: () => i18n.tribeCreateImage || 'Create Image', - audio: () => i18n.tribeCreateAudio || 'Create Audio', - video: () => i18n.tribeCreateVideo || 'Create Video', - document: () => i18n.tribeCreateDocument || 'Create Document', - bookmark: () => i18n.tribeCreateBookmark || 'Create Bookmark', - torrent: () => i18n.tribeCreateTorrent || 'Upload Torrent', - }; - const mediaBtnLabel = createMediaLabel[mediaType] ? createMediaLabel[mediaType]() : i18n.tribeCreateButton; - - if (action === 'create') { - if (mediaType === 'bookmark') { - return div({ class: 'create-tribe-form' }, - form({ method: 'POST', action: `${tribeUrl}/media/upload` }, - input({ type: 'hidden', name: 'mediaType', value: 'bookmark' }), - input({ type: 'hidden', name: 'returnSection', value: sectionKey }), - label({ for: 'title' }, i18n.tribeMediaTitle), br, - input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeMediaTitle }), br(), - label({ for: 'url' }, i18n.bookmarkUrlLabel || 'URL'), br, - input({ type: 'url', name: 'url', id: 'url', required: true, placeholder: 'https://' }), br(),br(), - label({ for: 'description' }, i18n.tribeMediaDescription), br, - textarea({ name: 'description', id: 'description', rows: 3, placeholder: i18n.tribeMediaDescription }, ''), br(), - button({ type: 'submit', class: 'create-button' }, mediaBtnLabel) - ) - ); - } - return div({ class: 'create-tribe-form' }, - form({ method: 'POST', action: `${tribeUrl}/media/upload`, enctype: 'multipart/form-data' }, - input({ type: 'hidden', name: 'mediaType', value: mediaType }), - input({ type: 'hidden', name: 'returnSection', value: sectionKey }), - label({ for: 'title' }, i18n.tribeMediaTitle), br, - input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeMediaTitle }), br(), - label({ for: 'description' }, i18n.tribeMediaDescription), br, - textarea({ name: 'description', id: 'description', rows: 3, placeholder: i18n.tribeMediaDescription }, ''), br(), - label({ for: 'media' }, i18n.tribeMediaUpload), br, - input({ type: 'file', name: 'media', id: 'media', accept: acceptForMediaType[mediaType] || '*/*', required: true }), br(), br(), - button({ type: 'submit', class: 'create-button' }, mediaBtnLabel) - ) - ); - } - - const mediaFooter = (m) => [ - p({ class: 'tribe-media-date' }, span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString())), - p({ class: 'tribe-media-author' }, userLink(m.author)), - m.author === userId ? form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(m.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeContentDelete) - ) : null - ]; - - const renderMediaItem = (m) => { - const blobUrl = toBlobUrl(m.image); - if (mediaType === 'image') { - return div({ class: 'tribe-media-item' }, - blobUrl ? a({ href: blobUrl, target: '_blank' }, img({ src: blobUrl, alt: m.title || '', class: 'tribe-media-thumb' })) : null, - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'audio') { - return div({ class: 'tribe-media-item' }, - blobUrl ? audio({ src: blobUrl, controls: true, class: 'tribe-media-audio' }) : p(i18n.tribeMediaEmpty), - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'video') { - return div({ class: 'tribe-media-item' }, - blobUrl ? video({ src: blobUrl, controls: true, class: 'tribe-media-thumb' }) : p(i18n.tribeMediaEmpty), - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'document') { - return div({ class: 'tribe-media-item' }, - blobUrl ? a({ href: blobUrl, target: '_blank', class: 'tribe-action-btn' }, i18n.readDocument || 'Read Document') : p(i18n.tribeMediaEmpty), - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'bookmark') { - const url = m.url || m.description || ''; - return div({ class: 'tribe-media-item' }, - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - url ? div({ class: 'card-field' }, - span({ class: 'card-label' }, 'URL:'), - a({ href: url, target: '_blank', class: 'card-value' }, url) - ) : null, - m.description && m.description !== url ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'torrent') { - return div({ class: 'tribe-media-item' }, - blobUrl ? a({ href: blobUrl, class: 'tribe-action-btn' }, i18n.torrentDownloadButton || 'DOWNLOAD IT!') : p(i18n.tribeMediaEmpty), - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - return null; - }; - - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(sTitle), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: sectionKey }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, mediaBtnLabel) - ) - ), - media.length === 0 ? p(i18n.tribeMediaEmpty) : - div({ class: 'tribe-media-grid' }, media.map(renderMediaItem)) - ); -}; - const renderSubTribesSection = (tribe, items, query) => { const action = query.action; const isMemberSub = Array.isArray(tribe.members) && tribe.members.includes(userId); @@ -1277,7 +1586,7 @@ const renderSubTribesSection = (tribe, items, query) => { if (action === 'create' && canCreate) { return renderCreateForm(tribe, 'subtribes', [ - { name: 'title', label: i18n.tribeTitleLabel, required: true, placeholder: 'Name of the sub-tribe' }, + { name: 'title', label: i18n.tribeTitleLabel, maxlength: '100', required: true, placeholder: 'Name of the sub-tribe' }, { name: 'description', type: 'textarea', label: i18n.tribeDescriptionLabel, required: true, placeholder: 'Description of the sub-tribe' }, { name: 'location', label: i18n.tribeLocationLabel, placeholder: 'Where is this sub-tribe located?' }, { name: 'mapUrl', label: i18n.mapLocationTitle || 'Map Location', placeholder: i18n.mapUrlPlaceholder || '/maps/MAP_ID', spaceBefore: true }, @@ -1304,7 +1613,7 @@ const renderSubTribesSection = (tribe, items, query) => { subTribes.length === 0 ? null : div({ class: 'tribe-thumb-grid' }, - subTribes.map(st => { + paged(subTribes).map(st => { return a({ href: `/tribe/${encodeURIComponent(st.id)}`, class: 'tribe-thumb-link', title: st.title }, img({ src: toImageUrl(st.image, '/assets/images/default-tribe.png'), class: 'tribe-thumb-img', alt: st.title }) ); @@ -1313,176 +1622,6 @@ const renderSubTribesSection = (tribe, items, query) => { ); }; -const renderTribeMapsSection = (tribe, maps) => { - const items = Array.isArray(maps) ? maps : []; - const createBtn = form({ method: 'GET', action: '/maps' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.mapUploadButton || 'Create Map')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionMaps || 'MAPS'), createBtn), p(i18n.noMaps || 'No maps yet')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionMaps || 'MAPS'), createBtn), - items.map(m => - div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeMap || 'MAP').toUpperCase()}]`), - form({ method: 'GET', action: `/maps/${encodeURIComponent(m.key)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, a({ href: `/maps/${encodeURIComponent(m.key)}` }, m.title)) - ) : null, - m.description ? p(m.description.substring(0, 200)) : null, - m.lat && m.lng ? span({ class: 'map-coords' }, `📍 ${m.lat.toFixed(4)}, ${m.lng.toFixed(4)}`) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ) - ) - ); -}; - -const renderTribeTorrentsSection = (tribe, torrents) => { - const items = Array.isArray(torrents) ? torrents : []; - const createBtn = form({ method: 'GET', action: '/torrents' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeCreateTorrent || 'Upload Torrent')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionTorrents || 'TORRENTS'), createBtn), p(i18n.tribeTorrentsEmpty || 'No torrents, yet.')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionTorrents || 'TORRENTS'), createBtn), - items.map(m => { - const blobName = encodeURIComponent((m.title || 'download').replace(/\.torrent$/i, '') + '.torrent'); - const blobUrl = m.url ? `/blob/${encodeURIComponent(m.url)}?name=${blobName}` : null; - return div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeTorrent || 'TORRENT').toUpperCase()}]`), - m._isMedia - ? (blobUrl ? a({ href: blobUrl, class: 'filter-btn' }, i18n.torrentDownload || 'Download') : null) - : form({ method: 'GET', action: `/torrents/${encodeURIComponent(m.rootId || m.key)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, m._isMedia - ? (blobUrl ? a({ href: blobUrl }, m.title) : m.title) - : a({ href: `/torrents/${encodeURIComponent(m.rootId || m.key)}` }, m.title)) - ) : null, - m.description ? p(String(m.description).substring(0, 200)) : null, - blobUrl && !m._isMedia ? div({ class: 'card-field' }, a({ href: blobUrl, class: 'filter-btn' }, i18n.torrentDownload || 'Download')) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ); - }) - ); -}; - -const renderTribePadsSection = (tribe, pads) => { - const items = Array.isArray(pads) ? pads : []; - const createBtn = form({ method: 'GET', action: '/pads' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.tribePadCreate || 'Create Pad')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionPads || 'PADS'), createBtn), p(i18n.tribePadsEmpty || 'No pads, yet.')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionPads || 'PADS'), createBtn), - items.map(m => - div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typePad || 'PAD').toUpperCase()}]`), - form({ method: 'GET', action: `/pads/${encodeURIComponent(m.rootId)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, a({ href: `/pads/${encodeURIComponent(m.rootId)}` }, m.title)) - ) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ) - ) - ); -}; - -const renderTribeChatsSection = (tribe, chats) => { - const items = Array.isArray(chats) ? chats : []; - const createBtn = form({ method: 'GET', action: '/chats' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeChatCreate || 'Create Chat')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionChats || 'CHATS'), createBtn), p(i18n.tribeChatsEmpty || 'No chats, yet.')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionChats || 'CHATS'), createBtn), - items.map(m => - div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeChat || 'CHAT').toUpperCase()}]`), - form({ method: 'GET', action: `/chats/${encodeURIComponent(m.key)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, a({ href: `/chats/${encodeURIComponent(m.key)}` }, m.title)) - ) : null, - m.description ? p(m.description.substring(0, 200)) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ) - ) - ); -}; - -const renderTribeCalendarsSection = (tribe, calendars) => { - const items = Array.isArray(calendars) ? calendars : []; - const createBtn = form({ method: 'GET', action: '/calendars' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeCalendarCreate || 'Create Calendar')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionCalendars || 'CALENDARS'), createBtn), p(i18n.tribeCalendarsEmpty || 'No calendars, yet.')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionCalendars || 'CALENDARS'), createBtn), - items.map(m => - div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeCalendar || 'CALENDAR').toUpperCase()}]`), - form({ method: 'GET', action: `/calendars/${encodeURIComponent(m.rootId)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, a({ href: `/calendars/${encodeURIComponent(m.rootId)}` }, m.title)) - ) : null, - m.deadline ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.calendarDeadlineLabel || 'Deadline') + ':'), - span({ class: 'card-value' }, new Date(m.deadline).toLocaleDateString()) - ) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ) - ) - ); -}; - exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { if (!tribe) { return div({ class: 'error' }, i18n.tribeNotFound); @@ -1496,24 +1635,13 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { let sectionContent; switch (section) { - case 'inhabitants': sectionContent = renderInhabitantsSection(tribe, sectionData); break; - case 'feed': - sectionContent = div( - query.sent ? div({ class: 'card card-rpg tribe-banner' }, - p({ class: 'bold' }, i18n.tribeFeedSent || 'Message sent successfully!') - ) : null, - tribe.members.includes(config.keys.id) - ? form({ class: 'tribe-feed-compose', method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/message` }, - textarea({ name: 'message', rows: 4, maxlength: MAX_MESSAGE_LENGTH, placeholder: i18n.tribeFeedMessagePlaceholder }), - button({ type: 'submit', class: 'tribe-feed-send' }, i18n.tribeFeedSend) - ) - : null, - await renderFeedTribeView(sectionData, tribe, query, query.filter) - ); - break; + case 'inhabitants': sectionContent = renderInhabitantsSection(tribe, sectionData, query); break; + case 'overview': sectionContent = renderOverviewSection(tribe, query, sectionData); break; + case 'feed': sectionContent = renderFeedTribeView(sectionData, tribe, query); break; case 'events': sectionContent = renderEventsSection(tribe, sectionData, query); break; case 'tasks': sectionContent = renderTasksSection(tribe, sectionData, query); break; case 'votations': sectionContent = renderVotationsSection(tribe, sectionData, query); break; + case 'polls': sectionContent = renderTribePollsSection(tribe, sectionData, query); break; case 'forum': sectionContent = renderForumSection(tribe, sectionData, query); break; case 'subtribes': sectionContent = renderSubTribesSection(tribe, sectionData, query); break; case 'search': sectionContent = renderTribeSearchSection(tribe, sectionData, query); break; @@ -1522,11 +1650,14 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { case 'videos': sectionContent = renderTribeMediaTypeSection(tribe, sectionData, query, 'video'); break; case 'documents': sectionContent = renderTribeMediaTypeSection(tribe, sectionData, query, 'document'); break; case 'bookmarks': sectionContent = renderTribeMediaTypeSection(tribe, sectionData, query, 'bookmark'); break; - case 'torrents': sectionContent = renderTribeTorrentsSection(tribe, sectionData); break; - case 'maps': sectionContent = renderTribeMapsSection(tribe, sectionData); break; - case 'pads': sectionContent = renderTribePadsSection(tribe, sectionData); break; - case 'chats': sectionContent = renderTribeChatsSection(tribe, sectionData); break; - case 'calendars': sectionContent = renderTribeCalendarsSection(tribe, sectionData); break; + case 'torrents': sectionContent = renderTribeTorrentsSection(tribe, sectionData, query); break; + case 'files': sectionContent = renderTribeFilesSection(tribe, sectionData, query); break; + case 'maps': sectionContent = renderTribeMapsSection(tribe, sectionData, query); break; + case 'pads': sectionContent = renderTribePadsSection(tribe, sectionData, query); break; + case 'wiki': sectionContent = renderTribeWikiSectionPage(tribe, sectionData, query); break; + case 'chats': sectionContent = renderTribeChatsSection(tribe, sectionData, query); break; + case 'rooms': sectionContent = renderTribeRoomsSectionPage(tribe, sectionData, query); break; + case 'calendars': sectionContent = renderTribeCalendarsSection(tribe, sectionData, query); break; case 'governance': sectionContent = renderGovernance(tribe, sectionData); break; case 'trending': sectionContent = renderTribeTrendingSection(tribe, sectionData, query); break; case 'tags': sectionContent = renderTribeTagsSection(tribe, sectionData, query); break; @@ -1546,7 +1677,7 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { : (isAuthorOfTribe || (tribe.inviteMode === 'open' && isMemberOfTribe)); const canLeave = isMemberOfTribe && (isLarpHouse || !isAuthorOfTribe); const showOwnerControls = !isLarpHouse && isAuthorOfTribe; - const inviteHref = isLarpHouse ? '/invites#invites-houses' : '/invites#invites-tribes'; + const inviteHref = '/invites?filter=others'; const showInviteTop = isOutsider && !tribe.parentTribeId; const hasOpenInvite = !!tribe.openInviteCode; const canManageOpenInvite = hasOpenInvite && (tribe.openInviteBy === userId || isAuthorOfTribe); @@ -1554,6 +1685,9 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { const tribeDetails = div({ class: 'tribe-details' }, div({ class: 'tribe-side' }, + div({ class: 'card-header activity-card-header' }, + renderContentActions(tribe.id, `/tribe/${encodeURIComponent(tribe.id)}`, { author: tribe.author, reportTitle: tribe.title, deleteAction: showOwnerControls ? contentDeleteAction('tribe', tribe.id) : undefined }) + ), tribe.parentTribe ? div({ class: 'tribe-parent-box' }, h2({ class: 'tribe-info-label' }, i18n.tribeMainTribeLabel || 'MAIN TRIBE'), @@ -1575,18 +1709,27 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { table({ class: 'tribe-info-table' }, tr( td({ class: 'tribe-info-label' }, i18n.tribeCreatedAt || 'CREATED'), - td({ class: 'tribe-info-value', colspan: '3' }, new Date(tribe.createdAt).toLocaleString()) + td({ class: 'tribe-info-value', colspan: '3' }, moment(tribe.createdAt).format("YYYY/MM/DD HH:mm")) ), tr( td({ class: 'tribe-info-value tribe-author-cell', colspan: '4' }, userLink(tribe.author)) ), tribe.location ? tr( td({ class: 'tribe-info-label' }, i18n.tribeLocationLabel || 'LOCATION'), - td({ class: 'tribe-info-value', colspan: '3' }, ...renderUrl(tribe.location)) + td({ class: 'tribe-info-value', colspan: '3' }, ...renderStyledText(tribe.location)) ) : null ), h2({ class: 'tribe-members-count' }, `${i18n.tribeMembersCount}: ${tribe.memberCount != null ? tribe.memberCount : tribe.members.length}`), - (canGenerateInvite || (isOutsider && !showInviteTop)) ? div({ class: 'tribe-side-actions' }, + (isOutsider && hasOpenInvite) + ? div({ class: 'tribe-side-actions tribe-side-center' }, + renderInviteQrCard({ qrDataUrl: tribe.openInviteQr, name: tribe.title }), + form({ method: 'POST', action: '/tribes/open-invite/join' }, + input({ type: 'hidden', name: 'tribeId', value: tribe.id }), + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeJoinOpen) + ) + ) + : null, + (canGenerateInvite || (isOutsider && !showInviteTop)) ? div({ class: canGenerateInvite ? 'tribe-side-actions' : 'tribe-side-actions tribe-side-center tribe-enter-invite' }, canGenerateInvite ? (isLarpHouse ? form({ method: 'POST', action: '/larp/invite/create' }, @@ -1608,6 +1751,25 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { ? a({ class: 'tribe-action-btn', href: inviteHref }, i18n.tribeEnterInvite) : null ) : null, + (tribe.subscription && !isOutsider) + ? renderSubscriptionBox({ + target: tribe.id, + scope: 'tribes', + subscribed: tribe.subscription.subscribed === true, + count: tribe.subscription.count, + isOwner: String(tribe.author) === String(userIdParam), + returnTo: `/tribe/${encodeURIComponent(tribe.id)}` + }) + : null, + (isLarpHouse && larpHouseKey === 'academia' && isOutsider && String(tribe.viewerHouse || '') !== 'academia') + ? div({ class: 'tribe-side-actions' }, + form({ method: 'POST', action: '/larp/join' }, + input({ type: 'hidden', name: 'house', value: 'academia' }), + input({ type: 'hidden', name: 'returnTo', value: `/tribe/${encodeURIComponent(tribe.id)}` }), + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.larpInviteRedeem || 'Join House') + ) + ) + : null, (hasOpenInvite && canManageOpenInvite) ? div({ class: 'tribe-open-invite-panel' }, div({ class: 'tribe-open-invite' }, span({ class: 'tribe-members-count' }, i18n.tribeInviteCodeText), @@ -1619,9 +1781,12 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeRemoveInvitation) ) ) : null, - tribe.description ? p({ class: 'tribe-side-description' }, ...renderUrl(tribe.description)) : null, - renderMapLocationVisitLabel(tribe.mapUrl), - (!tribe.parentTribeId && (canCreateSub || subTribes.length > 0 || showInviteTop)) ? div({ class: 'tribe-side-subtribes' }, + tribe.description ? p({ class: 'tribe-side-description' }, ...renderStyledText(tribe.description)) : null, + renderMapEmbed(tribe.mapData, tribe.mapUrl), + showInviteTop + ? div({ class: 'tribe-side-actions tribe-side-center tribe-enter-invite' }, a({ class: 'tribe-action-btn', href: inviteHref }, i18n.tribeEnterInvite)) + : null, + (!tribe.parentTribeId && (canCreateSub || subTribes.length > 0)) ? div({ class: 'tribe-side-subtribes' }, canCreateSub ? form({ method: 'GET', action: `/tribe/${encodeURIComponent(tribe.id)}` }, input({ type: 'hidden', name: 'section', value: 'subtribes' }), @@ -1629,9 +1794,6 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeSubTribesCreate) ) : null, - showInviteTop - ? a({ class: 'tribe-action-btn', href: inviteHref }, i18n.tribeEnterInvite) - : null, subTribes.length > 0 ? div({ class: 'tribe-subtribes-list' }, subTribes.map(st => @@ -1651,11 +1813,6 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeUpdateButton) ) : null, - showOwnerControls - ? form({ method: 'POST', action: `/tribes/delete/${encodeURIComponent(tribe.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeDeleteButton) - ) - : null, canLeave ? (isLarpHouse ? form({ method: 'POST', action: '/larp/leave' }, @@ -1676,10 +1833,132 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { ) ); - return template( + const page = template( pageTitle, tribeDetails ); + return section === 'documents' ? `${page}` : page; +}; + +exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, filterType = '', filterAuthor = '' }) => { + const t = tribe || {}; + const all = (Array.isArray(items) ? items : []).filter(it => it && it.contentType !== 'forum-reply'); + const kindKey = (it) => it.contentType === 'media' ? `media-${String(it.mediaType || '')}` : String(it.contentType || ''); + const kindName = (it) => it.contentType === 'media' ? activityMediaTypeName(String(it.mediaType || '')) : contentTypeName(it.contentType); + const activeType = String(filterType || '').toLowerCase(); + const activeAuthor = String(filterAuthor || ''); + const byAuthor = activeAuthor ? all.filter(it => it.author === activeAuthor) : all; + const list = activeType ? byAuthor.filter(it => kindKey(it) === activeType) : byAuthor; + const base = `/c/tribe/${encodeURIComponent(slug || '')}`; + const hrefFor = (type, author) => { + const q = new URLSearchParams(); + if (type) q.set('type', type); + if (author) q.set('author', author); + const qs = q.toString(); + return escapeHtml(qs ? `${base}?${qs}` : base); + }; + const paged = paginateClearnet(list, page); + const pager = renderClearnetPager({ base, params: { type: activeType, author: activeAuthor }, page: paged.page, pages: paged.pages, per: paged.per, total: paged.total }); + const name = t.title || i18n.cnUntitled || 'Untitled'; + const cover = cnBlobUrl(t.image); + const desc = renderRichText(t.description || ''); + const day = (v) => { const d = new Date(v); return v && !isNaN(d) ? d.toISOString().slice(0, 10) : ''; }; + const who = (id) => (names && names[id]) || (id ? `${String(id).slice(0, 10)}…` : ''); + const countBy = (arr, fn) => { const out = new Map(); for (const it of arr) { const k = fn(it); if (k) out.set(k, (out.get(k) || 0) + 1); } return out; }; + const typeCounts = countBy(byAuthor, kindKey); + const authorCounts = countBy(activeType ? all.filter(it => kindKey(it) === activeType) : all, it => it.author); + const btn = (href, label, count, active) => `${escapeHtml(label)} (${count})`; + const typeRow = typeCounts.size > 1 + ? `
${btn(hrefFor('', activeAuthor), i18n.all, byAuthor.length, !activeType)}${[...typeCounts.entries()].map(([k, n]) => btn(hrefFor(k, activeAuthor), kindName(byAuthor.find(it => kindKey(it) === k)), n, activeType === k)).join('')}
` + : ''; + const authorRow = authorCounts.size > 1 + ? `
${[...authorCounts.entries()].map(([id, n]) => btn(hrefFor(activeType, activeAuthor === id ? '' : id), who(id), n, activeAuthor === id)).join('')}
` + : ''; + const renderItem = (it) => { + const mt = it.contentType === 'media' ? String(it.mediaType || '') : ''; + const kind = kindName(it); + const title = escapeHtml(it.title || ''); + const blob = cnBlobUrl(it.image); + const media = !blob ? '' + : mt === 'image' ? `${title}` + : mt === 'audio' ? `` + : mt === 'video' ? `` + : ['document', 'file', 'torrent'].includes(mt) ? `⇩ ${title || escapeHtml(kind)}` + : ''; + const url = mt === 'bookmark' ? String(it.url || '') : ''; + const href = url ? safeExternalHref(url) : ''; + const bookmark = !url ? '' + : href ? `${escapeHtml(url)}` + : `${escapeHtml(url)}`; + const details = [ + it.date ? `${i18n.tribeEventDate || 'Date'}: ${it.date}` : '', + it.deadline ? `${i18n.tribeTaskDeadline || 'Deadline'}: ${it.deadline}` : '', + it.location ? `${i18n.tribeEventLocation || 'Location'}: ${it.location}` : '', + it.status ? (statusI18n()[it.status] || it.status) : '', + it.contentType === 'map' && Number.isFinite(Number(it.lat)) ? `📍 ${Number(it.lat).toFixed(4)}, ${Number(it.lng).toFixed(4)}` : '', + ...(it.contentType === 'map' && Array.isArray(it.markers) ? it.markers.map(mk => `📍 ${Number(mk.lat).toFixed(4)}, ${Number(mk.lng).toFixed(4)}${mk.label ? ` · ${mk.label}` : ''}`) : []), + ...(it.contentType === 'calendar' && Array.isArray(it.dates) ? it.dates.map(d => `📅 ${day(d.date)}${d.label ? ` · ${d.label}` : ''}`) : []) + ].filter(Boolean).map(d => `${escapeHtml(String(d))}`).join(''); + const options = it.contentType === 'votation' && Array.isArray(it.options) && it.options.length + ? `
    ${it.options.map(o => `
  • ${escapeHtml(String(o))}
  • `).join('')}
` + : ''; + const replies = it.contentType === 'forum' && Array.isArray(it.replies) ? it.replies : []; + const repliesHtml = replies.length + ? `
${escapeHtml(String(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase())}: ${replies.length}
${replies.map(r => `
${escapeHtml(who(r.author))}${day(r.createdAt) ? ` · ${escapeHtml(day(r.createdAt))}` : ''}

${renderRichText(r.description || '')}

`).join('')}
` + : ''; + const text = it.description && it.description !== url ? renderRichText(it.description) : ''; + const when = day(it.createdAt); + return `
+
[${escapeHtml(String(kind).toUpperCase())}]${when ? `📅 ${escapeHtml(when)}` : ''}${it.author ? `${escapeHtml(who(it.author))}` : ''}
+ ${title ? `

${it.href ? `${title}` : title}

` : ''} + ${media} + ${text ? `

${text}

` : ''} + ${bookmark} + ${details ? `
${details}
` : ''} + ${options} + ${repliesHtml} +
`; + }; + const extraCss = ` +.cn-tribe-head{display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start;margin-bottom:24px} +.cn-tribe-cover{width:160px;height:160px;border-radius:8px;border:3px solid var(--fg);object-fit:cover;background:#000;flex:0 0 auto} +.cn-tribe-head-body{flex:1 1 280px;min-width:0} +.cn-tribe-name{color:var(--fg);margin:0 0 8px 0;font-size:28px;font-weight:700;word-break:break-word} +.cn-tribe-desc{color:var(--fg-soft);white-space:pre-wrap;margin:0} +.cn-tribe-list{display:flex;flex-direction:column;gap:14px;margin-top:16px} +.cn-tribe-item{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:14px 16px;min-width:0} +.cn-tribe-meta{display:flex;gap:12px;flex-wrap:wrap;align-items:baseline;color:var(--fg-dim);font-size:12px;word-break:break-all} +.cn-tribe-title{color:var(--fg);font-size:18px;font-weight:700;margin:8px 0;word-break:break-word} +.cn-tribe-text{color:var(--fg-soft);line-height:1.5;margin:8px 0;word-break:break-word} +.cn-tribe-link{display:inline-block;margin:8px 0;padding:6px 12px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);word-break:break-all} +.cn-tribe-options{margin:8px 0;padding-left:20px;color:var(--fg-soft)} +.cn-tribe-replies{margin-top:12px;border-top:1px solid var(--border);padding-top:8px} +.cn-tribe-reply{border-left:2px solid var(--border);padding:4px 0 4px 12px;margin:8px 0} +.cn-tribe-reply-meta{color:var(--fg-dim);font-size:12px;word-break:break-all} +.cn-tribe-reply p{margin:4px 0 0 0;color:var(--fg-soft);line-height:1.5;word-break:break-word} +.cn-empty-content{background:var(--bg-elev);border:1px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--fg-dim);font-size:14px} +` + CLEARNET_HUB_CSS + CLEARNET_PAGER_CSS; + const body = ` +
+ ${escapeHtml(name)} +
+

${escapeHtml(name)}

+ ${desc ? `

${desc}

` : ''} +
+
+ ${typeRow}${authorRow} + ${list.length + ? `

${escapeHtml(i18n.cnPublicContent || 'Content')} (${list.length})

${paged.items.map(renderItem).join('')}
${pager}` + : `
${escapeHtml(i18n.cnCategoryEmpty || '')}
`} +`; + return renderClearnetPage({ + title: `${name} | Oasis`, + ogTitle: name, + ogDescription: t.description || '', + ogImage: cover, + extraCss, + body + }); }; const GOVERNANCE_METHODS = ['DEMOCRACY', 'MAJORITY', 'MINORITY', 'DICTATORSHIP', 'KARMATOCRACY']; @@ -1719,8 +1998,9 @@ const governanceFilterBar = (tribeId, currentFilter, showPublish) => { ); }; -const fmtTermDate = (d) => moment(d).format('YYYY-MM-DD HH:mm:ss'); +const fmtTermDate = (d) => d ? moment(d).format('YYYY-MM-DD HH:mm:ss') : '—'; const fmtTimeLeft = (end) => { + if (!end) return '—'; const diff = moment(end).diff(moment()); if (diff <= 0) return '0d 00:00:00'; const dur = moment.duration(diff); @@ -1743,9 +2023,9 @@ const governmentCard = (tribe, term, leaders) => { const isAnarchy = method === 'ANARCHY'; const i18nMeth = i18n[`parliamentMethod${method}`]; const methodLabel = (i18nMeth && String(i18nMeth).trim() ? String(i18nMeth) : method).toUpperCase(); - const termStart = term.startAt || moment().toISOString(); - const termEnd = term.endAt || moment(termStart).add(60, 'days').toISOString(); - const population = Array.isArray(tribe.members) ? tribe.members.length : 0; + const termStart = term.startAt || null; + const termEnd = term.endAt || null; + const population = tribe.memberCount != null ? tribe.memberCount : (Array.isArray(tribe.members) ? tribe.members.length : 0); const winnerVotes = Number(term.winnerVotes || 0); const totalVotes = Number(term.totalVotes || 0); const votesDisplay = `${winnerVotes} (${totalVotes})`; @@ -1826,7 +2106,7 @@ const candidaturesBlock = (tribe, candidatures, alreadyPublishedThisGlobalCycle) ' ', form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/governance/candidature/vote`, class: 'inline-form' }, input({ type: 'hidden', name: 'candidatureId', value: c.id }), - button({ type: 'submit', class: 'filter-btn' }, i18n.vote || 'Vote') + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.vote || 'Vote') ) ) )) @@ -1839,9 +2119,9 @@ const rulesBlock = (tribe, rules, isCreator) => div({}, h3(i18n.tribeGovernanceAddRule || 'Add rule'), form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/governance/rule/add` }, label({}, i18n.tribeGovRuleTitle || 'Title'), br(), - input({ type: 'text', name: 'title', required: true }), br(), br(), + input({ type: 'text', name: 'title', maxlength: '100', required: true }), br(), br(), label({}, i18n.tribeGovRuleBody || 'Body'), br(), - textarea({ name: 'body', rows: 4 }), br(), br(), + textarea({ maxlength: "3000", name: 'body', rows: 4 }), br(), br(), button({ type: 'submit', class: 'create-button' }, i18n.save || 'Save') ) ) @@ -1856,7 +2136,7 @@ const rulesBlock = (tribe, rules, isCreator) => div({}, isCreator ? form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/governance/rule/delete`, class: 'inline-form' }, input({ type: 'hidden', name: 'ruleId', value: r.id }), - button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.delete || 'Delete') + button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.delete || 'Delete') ) : null ) @@ -1866,7 +2146,7 @@ const rulesBlock = (tribe, rules, isCreator) => div({}, const renderGovernance = (tribe, data) => { const { filter, term, candidatures, rules, leaders, isCreator, canPublishToGlobal, alreadyPublishedThisGlobalCycle, hasElectedCandidate } = data || {}; const f = filter || 'government'; - const header = div({ class: 'tags-header' }, + const header = div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionGovernance || 'GOVERNANCE'), p(i18n.tribeGovernanceDesc || 'Internal governance for this tribe. Propose candidatures, debate rules, elect leaders — mirrors the global Parliament.') ); diff --git a/src/views/video_view.js b/src/views/video_view.js index 5dbfe9c..8c64b43 100644 --- a/src/views/video_view.js +++ b/src/views/video_view.js @@ -15,12 +15,14 @@ const { select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl") -const { renderMapLocationVisitLabel } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText") +const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); const userId = config.keys.id; @@ -37,18 +39,6 @@ const buildReturnTo = (filter, params = {}) => { return `/videos?${parts.join("&")}`; }; -const renderPMButton = (recipient, className = "filter-btn") => { - const r = safeText(recipient); - if (!r) return null; - if (String(r) === String(userId)) return null; - - return form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: r }), - button({ type: "submit", class: className }, i18n.privateMessage) - ); -}; - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -59,28 +49,13 @@ const renderTags = (tags) => { : null; }; -const renderVideoFavoriteToggle = (videoObj, returnTo = "") => - form( - { - method: "POST", - action: videoObj.isFavorite - ? `/videos/favorites/remove/${encodeURIComponent(videoObj.key)}` - : `/videos/favorites/add/${encodeURIComponent(videoObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - videoObj.isFavorite ? i18n.videoRemoveFavoriteButton : i18n.videoAddFavoriteButton - ) - ); - const renderVideoPlayer = (videoObj) => videoObj?.url ? div( - { class: "video-container", style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" }, + { class: "video-container video-container-row" }, videoHyperaxe({ controls: true, - src: `/blob/${encodeURIComponent(videoObj.url)}`, + src: blobUrlOf(videoObj.url), preload: "metadata" }) ) @@ -103,78 +78,16 @@ const renderVideoOwnerActions = (filter, videoObj, params = {}) => { ) ); } - items.push( - form( - { method: "POST", action: `/videos/delete/${encodeURIComponent(videoObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.videoDeleteButton) - ) - ); return items; }; const renderVideoCommentsSection = (videoId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/videos/${encodeURIComponent(videoId)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/videos/${encodeURIComponent(videoId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - const content = c?.value?.content || {}; - const rootId = content.fork || content.root || null; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) => { @@ -184,57 +97,34 @@ const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) ? videos.map((videoObj) => { const commentCount = typeof videoObj.commentCount === "number" ? videoObj.commentCount : 0; const title = safeText(videoObj.title); - const ownerActions = renderVideoOwnerActions(filter, videoObj, params); const isOwn = videoObj.author && String(videoObj.author) === String(userId); + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(videoObj) + : videoObj.tribeOrigin + ? renderContentActions(null, videoObj.tribeOrigin.href) + : renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(videoObj.key)) || params.spreads || null, author: videoObj.author, favKind: 'videos', torrentFrom: { blobId: videoObj.url, name: videoObj.title }, isFavorite: videoObj.isFavorite, reportTitle: videoObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction('video', videoObj.key) : undefined }); + const titleHref = typeof params.headerActions === "function" ? null : videoObj.tribeOrigin ? videoObj.tribeOrigin.href : `/videos/${encodeURIComponent(videoObj.key)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(videoObj) + : videoObj.tribeOrigin ? null : renderEngagement(videoObj.key, + renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants), + renderCommentsLink({ href: `/videos/${encodeURIComponent(videoObj.key)}`, count: commentCount }) + ); return div( { class: "trending-card video-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`) + headerActions ), div( { class: "card-section video-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderVideoFavoriteToggle(videoObj, returnTo), - renderPMButton(videoObj.author) - ), - ownerActions.length - ? div({ class: "bookmark-topbar-right" }, div({ class: "bookmark-actions" }, ...ownerActions)) - : null - ), - title ? h2(title) : null, + div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, videoObj.tribeOrigin ? renderTribeOriginChip(videoObj.tribeOrigin) : videoObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('videos', videoObj.title, videoObj.key)) : null, renderLicenseChip(videoObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(videoObj) : [])), videoObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(videoObj.lifetime, i18n)) : null, renderVideoPlayer(videoObj), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/videos/${encodeURIComponent(videoObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(videoObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(videoObj.key, (params.spreadMap && params.spreadMap.get(videoObj.key)) || params.spreads)), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(videoObj) : []), + engagement, renderMapLocationVisitLabel(videoObj.mapUrl), br(), (() => { @@ -244,12 +134,12 @@ const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(videoObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -265,6 +155,7 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => { return div( { class: "div-center video-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/videos/update/${encodeURIComponent(videoId)}` : "/videos/create", @@ -279,11 +170,11 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => { br(), span(i18n.videoTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.videoTitlePlaceholder, value: videoToEdit?.title || "" }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.videoTitlePlaceholder, value: videoToEdit?.title || "" }), br(), span(i18n.videoDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.videoDescriptionPlaceholder, rows: "4" }, videoToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.videoDescriptionPlaceholder, rows: "4" }, videoToEdit?.description || ""), br(), span(i18n.mapLocationTitle || "Map Location"), br(), @@ -298,61 +189,61 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => { value: safeArr(videoToEdit?.tags).join(", ") }), br(), + ...renderLicenseSelect(videoToEdit?.license, i18n), + br(), + renderClearnetSelector(filter === "edit" ? !!videoToEdit?.clearnet : false, i18n), br(), button({ type: "submit" }, filter === "edit" ? i18n.videoUpdateButton : i18n.videoCreateButton) ) ); }; +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.videoView = async (videos, filter = "all", videoId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.videoMineSectionTitle - : filter === "create" - ? i18n.videoCreateSectionTitle - : filter === "edit" - ? i18n.videoUpdateSectionTitle - : filter === "recent" - ? i18n.videoRecentSectionTitle - : filter === "top" - ? i18n.videoTopSectionTitle - : filter === "favorites" - ? i18n.videoFavoritesSectionTitle - : i18n.videoAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(videoId) }; + const title = i18n.videoTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(videos); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const videoToEdit = videoId ? list.find((v) => v.key === videoId) : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.videoDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetVideos); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterTop), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.videoFilterFavorites - ), + String(i18n.videoFilterFavorites).toUpperCase() + )] : []), + + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterTop).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.videoCreateButton) ) ) @@ -361,8 +252,9 @@ exports.videoView = async (videos, filter = "all", videoId = null, params = {}) filter === "create" || filter === "edit" ? renderVideoForm(filter, videoId, videoToEdit, { ...params, filter }) : section( - div( - { class: "videos-search" }, + emptyMod ? null : div( + { class: "videos-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/videos", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -377,28 +269,29 @@ exports.videoView = async (videos, filter = "all", videoId = null, params = {}) { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.videoSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.videoSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.videoSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.videoSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.videoSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.videoSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.videoSearchButton) ) ) ), - div({ class: "videos-list" }, renderVideoList(list, filter, { q, sort })) + div({ class: "videos-list" }, renderVideoList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); }; exports.singleVideoView = async (videoObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(videoObj.title); - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetVideos); + const isAuthor = String(videoObj.author) === String(userId); + const isClearnet = !!videoObj.clearnet; const chips = [ renderLifespanChip(videoObj.lifetime, i18n), @@ -407,30 +300,40 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params const ownerActions = renderVideoOwnerActions(filter, videoObj, { q, sort }); const sideActions = []; - sideActions.push(renderVideoFavoriteToggle(videoObj, returnTo)); - if (videoObj.author && String(videoObj.author) !== String(userId)) { - sideActions.push(renderPMButton(videoObj.author)); - } for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(videoObj.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`, { + author: videoObj.author, + favKind: 'videos', torrentFrom: { blobId: videoObj.url, name: videoObj.title }, + isFavorite: videoObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: videoObj.title, + deleteAction: String(videoObj.author) === String(userId) ? contentDeleteAction('video', videoObj.key) : undefined + }) + ); + const videoSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('videos', videoObj.title, videoObj.key) : null), + isAuthor ? renderClearnetSwitch('videos', videoObj.rootId || videoObj.key, isClearnet) : null, + renderLicenseChip(videoObj.license) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(videoObj.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(videoObj.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(videoObj.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(videoObj.key, params.spreads)), - renderMapLocationVisitLabel(videoObj.mapUrl), + renderMapEmbed(params.mapData, videoObj.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const videoMain = div({ class: "tribe-main" }, + detailActions, renderVideoPlayer(videoObj), (() => { const createdTs = videoObj.createdAt ? new Date(videoObj.createdAt).getTime() : NaN; @@ -439,24 +342,26 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(videoObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants), - renderVideoCommentsSection(videoObj.key, comments, returnTo) + renderEngagement(videoObj.key, + renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants), + renderVideoCommentsSection(videoObj.key, comments, returnTo) + ) ); return template( i18n.videoTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.videoAllSectionTitle || i18n.videoTitle), p(i18n.videoDescription) ), @@ -464,16 +369,15 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params { class: "filters" }, form( { method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterTop), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterMine).toUpperCase())] : []), { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.videoFilterFavorites - ), + String(i18n.videoFilterFavorites).toUpperCase() + )] : []), + + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.videoCreateButton) ) ), diff --git a/src/views/vote_view.js b/src/views/vote_view.js index 557c64f..003882a 100644 --- a/src/views/vote_view.js +++ b/src/views/vote_view.js @@ -1,8 +1,10 @@ const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { template, i18n, renderOpinionsVoting, renderEngagement, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderTribeOriginChip } = require("./clearnet_view"); +const mobileUi = require("./mobile_ui"); const userId = config.keys.id; const VOTE_QUORUM = 2; @@ -13,15 +15,15 @@ const voteLabel = (opt) => i18n["vote" + opt.split("_").map(w => w.charAt(0) + w.slice(1).toLowerCase()).join("")] || opt; const computeVoteOutcome = (baseCounts, voteOptions, totalVotesNum) => { - const noResult = { text: i18n.voteNoQuorum || "NO QUORUM", color: "#ffcc00" }; + const noResult = { text: i18n.voteNoQuorum || "NO QUORUM", variant: "pending" }; if (totalVotesNum < VOTE_QUORUM) return noResult; const opts = voteOptions.filter((o) => o !== "FOLLOW_MAJORITY"); const maxCount = opts.reduce((m, o) => Math.max(m, baseCounts[o] || 0), 0); const topOpts = opts.filter((o) => (baseCounts[o] || 0) === maxCount); if (maxCount === 0 || topOpts.length > 1) return noResult; const winner = topOpts[0]; - const color = winner === "YES" ? "#4caf50" : winner === "NO" ? "#e53935" : "#ffcc00"; - return { text: voteLabel(winner), color }; + const variant = (winner === "YES" || winner === "GREEN") ? "yes" : (winner === "NO" || winner === "RED") ? "no" : "pending"; + return { text: voteLabel(winner), variant }; }; const normalizeStatus = (v) => { @@ -37,9 +39,12 @@ const statusLabel = (s) => { return up; }; -const renderVoteOwnerActions = (v, returnTo, mode) => { - const showUpdateButton = mode === "mine" && !Object.keys(v.opinions || {}).length; - const showDeleteButton = mode === "mine"; +const { isVoteLocked } = require("../models/votes_model"); +const isVoteAuthor = (v) => !!(v && v.createdBy && String(v.createdBy) === String(userId)); +const canModifyVote = (v) => isVoteAuthor(v) && !isVoteLocked(v); + +const renderVoteOwnerActions = (v, returnTo) => { + const showUpdateButton = canModifyVote(v); const actions = []; if (showUpdateButton) { @@ -51,15 +56,6 @@ const renderVoteOwnerActions = (v, returnTo, mode) => { ) ); } - if (showDeleteButton) { - actions.push( - form( - { method: "POST", action: `/votes/delete/${encodeURIComponent(v.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.voteDeleteButton) - ) - ); - } return actions; }; @@ -91,7 +87,7 @@ const renderVoteStatusChip = (status) => { return renderOpenClosedChip(status, { statusChipOPEN: localized, statusChipCLOSED: localized }); }; -const renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo) => { +const renderVoteListItem = exports.renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo, opts = {}) => { const voteOptions = Array.isArray(v.options) && v.options.length ? v.options : voteOptionsDefault; const baseCounts = voteOptions.reduce((acc, opt) => { acc[opt] = (v.votes && v.votes[opt]) ? v.votes[opt] : 0; @@ -99,36 +95,69 @@ const renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo) => }, {}); const totalVotesNum = typeof v.totalVotes === "number" ? v.totalVotes : parseInt(String(v.totalVotes || "0"), 10) || 0; const outcome = computeVoteOutcome(baseCounts, voteOptions, totalVotesNum); + const origin = v.tribeOrigin || null; + const href = opts.href !== undefined ? opts.href : (origin ? origin.href : `/votes/${encodeURIComponent(v.id)}`); const chips = [ renderVoteStatusChip(v.status), - renderLifespanChip(v.lifetime, i18n) + renderLifespanChip(v.lifetime, i18n), + origin ? renderTribeOriginChip(origin) : null, + ...(Array.isArray(opts.extraChips) ? opts.extraChips : []) ].filter(Boolean); const returnTo = `/votes?filter=${encodeURIComponent(activeFilter || "all")}`; const totalOpinions = Object.values(v.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0); - const isOwn = v.createdBy && String(v.createdBy) === String(userId); + const isOwn = isVoteAuthor(v); + const headerActions = opts.headerActions !== undefined + ? opts.headerActions + : origin ? null : renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`, { spread: spreadInfo || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question, returnTo, deleteAction: canModifyVote(v) ? contentDeleteAction('votes', v.id) : undefined }); return div({ class: "trending-card vote-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, + headerActions ? div({ class: "card-header activity-card-header" }, span(), - renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`) - ), + headerActions + ) : null, div({ class: "card-section vote-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/votes/${encodeURIComponent(v.id)}` }, v.question || i18n.votationsTitle) + href ? a({ href }, v.question || i18n.votationsTitle) : (v.question || i18n.votationsTitle) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - v.deadline ? p({ class: "card-date-highlight" }, moment(v.deadline).format("YYYY/MM/DD HH:mm")) : null, - div({ class: "tribe-card-members" }, + v.deadline ? p({ class: "time-chip" }, moment(v.deadline).format("YYYY/MM/DD HH:mm")) : null, + origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`) ), - div({ class: "job-meta-line", style: `color:${outcome.color};font-weight:bold;` }, `${i18n.voteResults || "Results"}: ${outcome.text}`), - div({ class: "card-spread-centered" }, renderSpreadButton(v.id, spreadInfo)) + origin ? null : div({ class: `job-meta-line vote-outcome vote-outcome-${outcome.variant}` }, `${i18n.voteResults || "Results"}: ${outcome.text}`), + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) ) ); }; +exports.renderVoteOptionsBlock = (v, voteOptions, action, returnTo, canVote) => { + const baseCounts = voteOptions.reduce((acc, opt) => { acc[opt] = (v.votes && v.votes[opt]) ? v.votes[opt] : 0; return acc; }, {}); + return [ + canVote ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.voteCastTitle || "Cast Vote"), + div({ class: "vote-buttons-block" }, + div({ class: "vote-buttons-row-single" }, + ...voteOptions.map((opt, idx) => + form({ method: "POST", action }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + input({ type: "hidden", name: "optionIndex", value: String(idx) }), + button({ type: "submit", name: "choice", value: opt }, voteLabel(opt)) + ) + ) + ) + ) + ) : null, + div({ class: "vote-table" }, + table( + tr(...voteOptions.map((opt) => th(voteLabel(opt)))), + tr(...voteOptions.map((opt) => td(String(baseCounts[opt] || 0)))) + ) + ) + ]; +}; + const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, activeFilter, params = {}) => { const voteOptions = Array.isArray(v.options) && v.options.length ? v.options : voteOptionsDefault; const baseCounts = voteOptions.reduce((acc, opt) => { @@ -147,13 +176,7 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti ].filter(Boolean); const sideActions = []; - if (v.createdBy && v.createdBy !== userId) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: v.createdBy }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } - for (const a of renderVoteOwnerActions(v, returnTo, mode || "")) sideActions.push(a); + for (const a of renderVoteOwnerActions(v, returnTo)) sideActions.push(a); const cleanTags = (Array.isArray(v.tags) ? v.tags : []).filter((t) => t && !String(t).includes(":")); const tagsNode = cleanTags.length @@ -172,22 +195,26 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti pushRow(i18n.voteStatus, statusLabel(v.status)); const voteSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`, { spread: params.spreads || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question, deleteAction: canModifyVote(v) ? contentDeleteAction('votes', v.id) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, v.question) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(v.id, params.spreads)), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(v.createdAt, v.createdBy)), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`) ), - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + renderDocumentActions('votes', v.id) ); const voteButtonsNode = renderVoteButtons(v, voteOptions, firstRow, secondRow, returnTo); const voteMain = div({ class: "tribe-main" }, + mobileUi.isMobile() && !voteButtonsNode && isVoteAuthor(v) && normalizeStatus(v.status) === "OPEN" ? mobileUi.voteOwnNote() : null, voteButtonsNode ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.voteCastTitle || "Cast Vote"), @@ -197,84 +224,29 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti div({ class: "job-section" }, h2({ class: "job-section-title" }, `${i18n.voteResults || "Results"}: `, - span({ style: `color:${outcome.color} !important;font-weight:bold;` }, outcome.text) + span({ class: `vote-outcome vote-outcome-${outcome.variant}` }, outcome.text) ), - div({ class: "vote-table" }, + mobileUi.isMobile() ? mobileUi.voteBars(voteOptions, baseCounts, voteLabel) : div({ class: "vote-table" }, table( tr(...voteOptions.map((opt) => th(voteLabel(opt)))), tr(...voteOptions.map((opt) => td(baseCounts[opt]))) ) ) ), - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(v.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(v.createdBy) - ), - renderOpinionsBar(v, returnTo) + renderEngagement(v.id, renderOpinionsBar(v, returnTo), + mode === "detail" ? renderCommentsSection(v.id, params.comments || [], activeFilter) : null) ); return div({ class: "tribe-details" }, voteSide, voteMain); }; const renderCommentsSection = (voteId, comments, activeFilter) => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; const returnTo = `/votes/${encodeURIComponent(voteId)}?filter=${encodeURIComponent(activeFilter || "all")}`; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/votes/${encodeURIComponent(voteId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - comments && comments.length - ? div( - { class: "comments-list" }, - comments.map((c) => { - const author = c.value && c.value.author ? c.value.author : ""; - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c.value && c.value.content ? c.value.content : {}; - const root = content.fork || content.root || ""; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(String(text))) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); + return renderSharedCommentsSection({ + action: `/votes/${encodeURIComponent(voteId)}/comments`, + comments: comments, + returnTo: returnTo + }); }; exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, params = {}) => { @@ -285,14 +257,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, ? activeFilterParam : (standardFilters.includes(mode) ? mode : "all"); - const title = - mode === "mine" ? i18n.voteMineSectionTitle : - mode === "create" ? i18n.voteCreateSectionTitle : - mode === "edit" ? i18n.voteUpdateSectionTitle : - mode === "open" ? i18n.voteOpenTitle : - mode === "closed" ? i18n.voteClosedTitle : - mode === "detail" ? (i18n.voteDetailSectionTitle || i18n.voteAllSectionTitle) : - i18n.voteAllSectionTitle; + const title = i18n.votationsTitle; const voteToEdit = list.find((v) => v.id === voteId) || {}; const editTags = Array.isArray(voteToEdit.tags) ? voteToEdit.tags.filter(Boolean) : []; @@ -310,15 +275,31 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, const secondRow = ["CONFUSED", "FOLLOW_MAJORITY", "NOT_INTERESTED"]; const header = div( - { class: "tags-header" }, + { class: "tags-header module-header-line" }, h2(i18n.votationsTitle), p(i18n.votationsDescription) ); const listReturnTo = standardFilters.includes(activeFilter) ? `/votes?filter=${encodeURIComponent(activeFilter)}` : "/votes"; + const emptyMod = moduleIsEmpty(filtered, mode, "all", (params && params.q) || ""); + const censusV = Array.isArray(params && params.censusList) ? params.censusList : list; + const voteChip = (m) => { + if (m === mode) return true; + if (m === "mine") return censusV.some((v) => v.createdBy === userId); + if (m === "recent") return censusV.length > 0; + if (m === "open") return censusV.some((v) => normalizeStatus(v.status) === "OPEN"); + if (m === "closed") return censusV.some((v) => normalizeStatus(v.status) === "CLOSED"); + return true; + }; - const deadlineMin = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm"); - const deadlineValue = voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : ""; + const minVoteDays = Number(params.minVoteDays) > 0 ? Number(params.minVoteDays) : 7; + const deadlineMin = moment().add(minVoteDays, "days").format("YYYY-MM-DDTHH:mm"); + const draft = params.draft || {}; + const deadlineValue = draft.deadline + ? String(draft.deadline) + : (voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : ""); + const questionValue = draft.question !== undefined && draft.question !== "" ? String(draft.question) : (voteToEdit.question || ""); + const tagsValue = draft.tags ? String(draft.tags) : editTags.join(", "); return template( title, @@ -328,43 +309,59 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, { class: "filters" }, form( { method: "GET", action: "/votes" }, - button({ type: "submit", name: "filter", value: "all", class: mode === "all" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: mode === "mine" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterMine), - button({ type: "submit", name: "filter", value: "open", class: mode === "open" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterOpen), - button({ type: "submit", name: "filter", value: "closed", class: mode === "closed" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterClosed), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: mode === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterAll).toUpperCase()), + ...(voteChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: mode === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterRecent).toUpperCase())] : []), + ...(voteChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: mode === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterMine).toUpperCase())] : []), + ...(voteChip("open") ? [button({ type: "submit", name: "filter", value: "open", class: mode === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterOpen).toUpperCase())] : []), + ...(voteChip("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: mode === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterClosed).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: mode === "create" ? "create-button active" : "create-button" }, i18n.voteCreateButton) ) - ) + ), + mode !== "create" && mode !== "edit" && !emptyMod + ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, v => normalizeStatus(v.status || 'OPEN'), [{ value: 'OPEN', label: i18n.voteStatusOpen }, { value: 'CLOSED', label: i18n.voteStatusClosed }]), + form({ method: "GET", action: "/votes", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: mode }), + input({ type: "text", name: "q", value: (params && params.q) || "", placeholder: i18n.votesSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + : null ), section( (mode === "edit" || mode === "create") ? div( { class: "vote-form" }, + mode === "edit" ? await renderSpreadEditWarning(voteId) : null, form( { action: mode === "edit" ? `/votes/update/${encodeURIComponent(voteId)}` : "/votes/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: listReturnTo }), h2(i18n.voteQuestionLabel), - input({ type: "text", name: "question", id: "question", required: true, value: voteToEdit.question || "" }), br(), br(), + input({ type: "text", name: "question", id: "question", required: true, value: questionValue }), br(), br(), label(i18n.voteDeadlineLabel), br(), input({ type: "datetime-local", name: "deadline", id: "deadline", required: true, - min: mode === "create" ? deadlineMin : undefined, + min: deadlineMin, value: deadlineValue }), br(), br(), label(i18n.voteTagsLabel), br(), - input({ type: "text", name: "tags", id: "tags", value: editTags.join(", ") }), br(), br(), + input({ type: "text", name: "tags", id: "tags", value: tagsValue }), br(), br(), + mobileUi.isMobile() && mode === "create" ? mobileUi.voteKindPicker(draft.options) : null, button({ type: "submit" }, mode === "edit" ? i18n.voteUpdateButton : i18n.voteCreateButton) ) ) : mode === "detail" && voteId - ? renderVoteDetail(filtered[0] || list.find(v => v.id === voteId) || {}, voteOptions, firstRow, secondRow, mode, activeFilter, params) + ? renderVoteDetail(filtered[0] || list.find(v => v.id === voteId) || {}, voteOptions, firstRow, secondRow, mode, activeFilter, { ...params, comments }) : filtered.length > 0 - ? div({ class: "jobs-grid" }, filtered.map((v) => renderVoteListItem(v, voteOptions, activeFilter, params.spreadMap && params.spreadMap.get(v.id)))) - : p(i18n.novotes), - (mode === "detail" && voteId) ? renderCommentsSection(voteId, comments, activeFilter) : null + ? div({ class: "jobs-grid" }, paged(filtered).map((v) => renderVoteListItem(v, voteOptions, activeFilter, params.spreadMap && params.spreadMap.get(v.id)))) + : div({ class: "no-content-box" }, p(i18n.novotes)), ) ); }; diff --git a/src/views/wallet_view.js b/src/views/wallet_view.js index ee8ca73..6c99485 100644 --- a/src/views/wallet_view.js +++ b/src/views/wallet_view.js @@ -1,67 +1,86 @@ const { form, button, div, h2, p, section, input, span, table, thead, tbody, tr, td, th, ul, li, a, br, label, img } = require("../server/node_modules/hyperaxe"); -const QRCode = require('../server/node_modules/qrcode'); -const { template, i18n } = require('./main_views'); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLink, renderWalletChip, renderStateChip, paged } = require('./main_views'); -const walletViewRender = (balance, address, ...elements) => { - const header = div({ class: 'tags-header' }, h2(i18n.walletTitle), p(i18n.walletDescription)); +const TX_CATEGORY_CHIPS = { + receive: ["mutuals", "\u2B07"], + send: ["whole", "\u2B06"], + generate: ["mutuals", "\u2713"], + stake: ["mutuals", "\u2713"], + immature: ["lifespan", "\u23F3"], + orphan: ["closed", "\u2717"] +}; +const renderTxCategory = (category) => { + const key = String(category || "").toLowerCase(); + const [variant, icon] = TX_CATEGORY_CHIPS[key] || ["encrypted", ""]; + return renderStateChip(variant, icon, (key || "-").toUpperCase()); +}; + +const WALLET_MODES = [ + { key: "send", href: "/wallet/send", label: () => i18n.walletSend }, + { key: "receive", href: "/wallet/receive", label: () => i18n.walletReceive }, + { key: "history", href: "/wallet/history", label: () => i18n.walletHistory } +]; + +const walletModeButtons = (current) => + div({ class: "mode-buttons-row" }, + ...WALLET_MODES.map(mode => + form({ method: "GET", action: mode.href }, + button({ type: "submit", class: current === mode.key ? "filter-btn active" : "filter-btn" }, String(mode.label()).toUpperCase()) + ) + ) + ); + +const walletViewRender = (balance, address, current, ...elements) => { + const header = div({ class: 'tags-header module-header-line' }, h2(i18n.walletTitle), p(i18n.walletDescription), renderWalletChip()); return template( i18n.walletTitle, section( header, + walletModeButtons(current), div({ class: "wallet-section" }, h2(i18n.walletAddress), div({ class: "wallet-address" }, h2({ class: "element" }, address || "-")), h2(i18n.walletBalanceTitle), - div({ class: "div-center" }, h2(i18n.walletBalanceLine({ balance: Number(balance || 0).toFixed(6) }))), - div({ class: "div-center" }, - span({ class: "wallet-form-button-group-center" }, - form({ action: "/wallet/send", method: "get" }, button({ type: 'submit' }, i18n.walletSend)), - form({ action: "/wallet/receive", method: "get" }, button({ type: 'submit' }, i18n.walletReceive)), - form({ action: "/wallet/history", method: "get" }, button({ type: 'submit' }, i18n.walletHistory)) - ) - ) + div({ class: "div-center" }, h2(i18n.walletBalanceLine({ balance: Number(balance || 0).toFixed(6) }))) ) ), - elements.length > 0 ? section(...elements) : null + elements.length > 0 ? section({ id: `wallet-${current || 'main'}` }, ...elements) : null ); }; exports.walletView = async (balance, address) => { - return walletViewRender(balance, address); + return walletViewRender(balance, address, null); }; exports.walletHistoryView = async (balance, transactions, address) => { - const rows = Array.isArray(transactions) ? transactions : []; + const rows = (Array.isArray(transactions) ? transactions.slice() : []).sort((a, b) => ((b.time || b.timereceived || 0) - (a.time || a.timereceived || 0))); return walletViewRender( balance, address, - h2(i18n.walletHistoryTitle), + "history", table( { class: "wallet-history" }, thead( tr( { class: "full-center" }, - th({ class: "col-10" }, i18n.walletCnfrs), - th(i18n.walletDate), + th({ class: "wallet-col-confirmations" }, i18n.walletCnfrs), + th({ class: "wallet-col-date" }, i18n.walletDate), th(i18n.walletType), - th(i18n.walletAmount), - th({ class: "col-30" }, i18n.walletTxId) + th(i18n.walletAmount) ) ), tbody( - ...rows.map(tx => { + ...paged(rows).map(tx => { const date = new Date((tx.time || tx.timereceived || 0) * 1000); const amount = Number(tx.amount) || 0; const fee = Number(tx.fee) || 0; const totalAmount = amount + fee; return tr( td({ class: "full-center" }, String(tx.confirmations || 0)), - td(date.toLocaleDateString(), br(), date.toLocaleTimeString()), - td(tx.category || "-"), - td(totalAmount.toFixed(6)), - td({ width: "30%", class: "tcell-ellipsis" }, - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(tx.txid || '')}`, target: "_blank" }, tx.txid || "-") - ) + td({ class: "wallet-col-date" }, moment(date).format("YYYY/MM/DD HH:mm")), + td(renderTxCategory(tx.category)), + td(totalAmount.toFixed(6)) ); }) ) @@ -70,42 +89,63 @@ exports.walletHistoryView = async (balance, transactions, address) => { }; exports.walletReceiveView = async (balance, address) => { - const qrDataUrl = await QRCode.toDataURL(address || '', { type: 'image/png', width: 320, margin: 1 }); return walletViewRender( balance, address, - h2(i18n.walletReceiveTitle), - div({ class: 'div-center qr-code' }, img({ src: qrDataUrl, alt: 'QR', class: 'wallet-qr-img' })) + "receive", + address + ? div({ class: 'div-center qr-code' }, img({ src: `/wallet/qr/${encodeURIComponent(address)}`, alt: 'QR', class: 'wallet-qr-img' })) + : null ); }; -exports.walletSendFormView = async (balance, destination, amount, fee, statusMessages, address) => { +exports.walletSendFormView = async (balance, destination, amount, fee, statusMessages, address, options = {}) => { + const transferCtx = options.transfer || null; + const paymentRef = options.payment || null; const type = statusMessages?.type || 'info'; const titleKey = statusMessages?.title || ''; const messages = statusMessages?.messages || []; - const statusBlock = messages.length > 0 - ? div( - { class: `wallet-status-${type}` }, - span(i18n.walletStatusMessages[titleKey] || titleKey || ''), - ul(...messages.map(error => li(i18n.walletStatusMessages[error] || error))) - ) + const statusText = [i18n.walletStatusMessages[titleKey] || titleKey || '', ...messages.map(error => i18n.walletStatusMessages[error] || error)].filter(Boolean).join(': '); + const statusBlock = messages.length > 0 && type !== 'error' + ? div({ class: 'flash-banner wallet-status' }, p(statusText)) : null; return walletViewRender( balance, address, - h2(i18n.walletWalletSendTitle), + "send", div( { class: "div-center" }, statusBlock, form( { action: '/wallet/send', method: 'POST' }, label({ for: 'destination' }, i18n.walletAddress), br(), - input({ type: 'text', id: 'destination', name: 'destination', placeholder: 'ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX', value: destination || '' }), br(), + input({ type: 'text', id: 'destination', name: 'destination', placeholder: 'ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX', value: destination || '', required: true, pattern: 'E[1-9A-HJ-NP-Za-km-z]{32,34}', maxlength: '35' }), br(), label({ for: 'amount' }, i18n.walletAmount), br(), - input({ type: 'text', id: 'amount', name: 'amount', placeholder: '0.25', value: amount || '' }), br(), + input({ type: 'number', id: 'amount', name: 'amount', placeholder: '0.25', value: amount || '', required: true, min: '0.000001', step: '0.000001', inputmode: 'decimal' }), br(), br(), label({ for: 'fee' }, i18n.walletFee), br(), - input({ type: 'text', id: 'fee', name: 'fee', placeholder: '0.01', value: fee || '' }), br(), + input({ type: 'number', id: 'fee', name: 'fee', placeholder: '5', value: fee || '', required: true, min: '0', step: '0.000001', inputmode: 'decimal' }), br(), br(), + paymentRef + ? div({ class: 'wallet-transfer-ctx' }, + div({ class: 'wallet-transfer-ctx-line' }, span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletPayeeLabel}: `), userLink(paymentRef.payeeId)), + paymentRef.concept ? div({ class: 'wallet-transfer-ctx-line' }, span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletConceptLabel}: `), span(paymentRef.concept)) : null, + input({ type: 'hidden', name: 'payeeId', value: paymentRef.payeeId }), + input({ type: 'hidden', name: 'concept', value: paymentRef.concept || '' }), + input({ type: 'hidden', name: 'refHref', value: paymentRef.href || '' }), + input({ type: 'hidden', name: 'refTag', value: paymentRef.tag || 'WALLET' }) + ) + : null, + transferCtx + ? div({ class: 'wallet-transfer-ctx' }, + span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletPayingTransfer}: `), + a({ href: `/transfers/${encodeURIComponent(transferCtx.id)}` }, transferCtx.concept || transferCtx.id), + input({ type: 'hidden', name: 'transferId', value: transferCtx.id }) + ) + : label({ class: 'wallet-create-transfer' }, + input({ type: 'checkbox', name: 'createTransfer', value: '1', ...(options.createTransfer ? { checked: true } : {}) }), + ` ${i18n.walletCreateTransferLabel}` + ), + br(), input({ type: 'hidden', name: 'action', value: 'confirm' }), div({ class: 'wallet-form-button-group-center' }, button({ type: 'submit' }, i18n.walletSend), @@ -116,25 +156,37 @@ exports.walletSendFormView = async (balance, destination, amount, fee, statusMes ); }; -exports.walletSendConfirmView = async (balance, destination, amount, fee) => { - const a = Number(amount || 0); - const f = Number(fee || 0); - const totalCost = a + f; +exports.walletSendConfirmView = async (balance, destination, amount, fee, options = {}) => { + const amountNum = Number(amount || 0); + const feeNum = Number(fee || 0); + const totalCost = amountNum + feeNum; return walletViewRender( balance, destination, + "send", p( i18n.walletAddressLine({ address: destination || '-' }), br(), - i18n.walletAmountLine({ amount: a.toFixed(6) }), br(), - i18n.walletFeeLine({ fee: f.toFixed(6) }), br(), + i18n.walletAmountLine({ amount: amountNum.toFixed(6) }), br(), + i18n.walletFeeLine({ fee: feeNum.toFixed(6) }), br(), i18n.walletTotalCostLine({ totalCost: totalCost.toFixed(6) }) ), form( { action: '/wallet/send', method: 'POST' }, input({ type: 'hidden', name: 'action', value: 'send' }), input({ type: 'hidden', name: 'destination', value: destination || '' }), - input({ type: 'hidden', name: 'amount', value: String(a) }), - input({ type: 'hidden', name: 'fee', value: String(f) }), + input({ type: 'hidden', name: 'amount', value: String(amountNum) }), + input({ type: 'hidden', name: 'fee', value: String(feeNum) }), + options.transfer ? input({ type: 'hidden', name: 'transferId', value: options.transfer.id }) : null, + options.createTransfer ? input({ type: 'hidden', name: 'createTransfer', value: '1' }) : null, + options.transfer ? p({ class: 'wallet-transfer-ctx' }, `${i18n.walletPayingTransfer}: ${options.transfer.concept || options.transfer.id}`) : null, + options.payment ? input({ type: 'hidden', name: 'payeeId', value: options.payment.payeeId }) : null, + options.payment ? input({ type: 'hidden', name: 'concept', value: options.payment.concept || '' }) : null, + options.payment ? input({ type: 'hidden', name: 'refHref', value: options.payment.href || '' }) : null, + options.payment ? input({ type: 'hidden', name: 'refTag', value: options.payment.tag || 'WALLET' }) : null, + options.payment ? div({ class: 'wallet-transfer-ctx' }, + div({ class: 'wallet-transfer-ctx-line' }, span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletPayeeLabel}: `), userLink(options.payment.payeeId)), + div({ class: 'wallet-transfer-ctx-line' }, span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletConceptLabel}: `), options.payment.concept || options.payment.href) + ) : null, div({ class: 'form-button-group-center' }, button({ type: 'submit' }, i18n.walletConfirm), a({ href: `/wallet/send`, class: "button-like-link" }, i18n.walletBack) @@ -143,20 +195,26 @@ exports.walletSendConfirmView = async (balance, destination, amount, fee) => { ); }; -exports.walletSendResultView = async (balance, destination, amount, txId) => { +exports.walletSendResultView = async (balance, destination, amount, txId, note = null) => { return walletViewRender( balance, destination, + "send", p( i18n.walletSentToLine({ destination: destination || '-', amount: Number(amount || 0).toFixed(6) }), br(), `${i18n.walletTransactionId}: `, - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(txId || '')}`, target: "_blank" }, txId || '-') - ) + span({ class: "bank-address-code" }, txId || '-') + ), + note ? p({ class: 'wallet-transfer-ctx' }, note) : null ); }; exports.walletErrorView = async (error) => { - const header = div({ class: 'tags-header' }, h2(i18n.walletTitle), p(i18n.walletDescription)); + const header = div({ class: 'tags-header module-header-line' }, h2(i18n.walletTitle), p(i18n.walletDescription), renderWalletChip()); + const w = (require('../configs/config-manager.js').getConfig() || {}).wallet || {}; + if (![w.url, w.user, w.pass].every(v => String(v || '').trim())) { + return template(i18n.walletTitle, section(header, div({ class: "wallet-error" }, h2(i18n.walletStatus), p(i18n.walletNotConfigured, ' ', a({ href: '/settings#wallet' }, i18n.walletConfiguration)), process.env.OASIS_MOBILE === '1' ? div({ class: "wallet-error-actions" }, a({ href: require('../backend/updater.js').ecoinWalletDownloadUrl(), class: "update-download-link" }, i18n.walletDownloadEcoin)) : null))); + } return template( i18n.walletTitle, section( diff --git a/src/views/welcome_view.js b/src/views/welcome_view.js new file mode 100644 index 0000000..7b39341 --- /dev/null +++ b/src/views/welcome_view.js @@ -0,0 +1,151 @@ +const { form, button, div, h2, p, section, a, span, select, option, label, input, textarea, img, details, summary } = require("../server/node_modules/hyperaxe") +const { template, i18n, WIZARD_THEMES } = require("./main_views") +const { renderRestoreStatus } = require("./backup_view") +const { renderSuggestedPub } = require("./invites_view") +const { config } = require("../server/SSB_server.js") +const mobileUi = require("./mobile_ui") +const { WIZARD_WORKFLOWS } = require("../models/workflows_model") + +const FEED_TEXT_MIN = Number(config?.feed?.minLength ?? 1) +const FEED_TEXT_MAX = Number(config?.feed?.maxLength ?? 280) + +const SECTIONS = { + profile: ["welcomeStepIdentity", "welcomeHintIdentity"], + ux: ["welcomeStepType", "welcomeHintType"], + workflow: ["welcomeStepUse", "welcomeHintUse"], + theme: ["welcomeStepLook", "welcomeHintLook"], + community: ["welcomeStepCommunity", "welcomeHintCommunity"], + greeting: ["welcomeStepIntro", "welcomeHintIntro"] +} +if (mobileUi.isMobile()) SECTIONS.workflow = ["welcomeStepUseMobile", "welcomeHintUseMobile"] + +const submitRow = (text) => div({ class: "welcome-action" }, button({ type: "submit" }, text)) + +const pickCard = (name, value, current, title, image) => label({ class: "welcome-ux-option" }, + input({ type: "radio", name, value, ...(value === current ? { checked: true } : {}) }), + img({ src: image, class: "welcome-ux-shot", alt: title }), + span({ class: "welcome-ux-label" }, title) +) + +const stepAction = (key, profile, status) => { + if (key === "profile") return form({ method: "POST", action: "/welcome/profile", enctype: "multipart/form-data", class: "welcome-profile-form" }, + label(i18n.profileName), + input({ type: "text", name: "name", maxlength: "80", value: profile.name || "", placeholder: profile.name || String(profile.id || "").slice(1, 9) }), + label(i18n.profileImage), + profile.image ? img({ loading: "lazy", class: "welcome-profile-avatar", src: `/image/256/${encodeURIComponent(profile.image)}` }) : null, + input({ type: "file", name: "image", accept: "image/*" }), + submitRow(i18n.welcomeStepProfileAction) + ) + if (key === "ux") { + let cfg = {} + try { cfg = require("../configs/config-manager.js").getConfig() || {} } catch (_) {} + const cur = ["ainav", "chats", "feed", "phone", "rooms"].includes(cfg.ux?.current) ? cfg.ux.current : "blocks" + if (mobileUi.isMobile()) return mobileUi.uxPick("/welcome/ux", cfg, true) + return form({ method: "POST", action: "/welcome/ux", class: "welcome-ux-form" }, + div({ class: "welcome-ux-grid" }, + pickCard("ux", "blocks", cur, i18n.uxModeMenus, "/assets/images/ux-blocks.png"), + cfg.modules?.aiNavMod === "on" ? pickCard("ux", "ainav", cur, i18n.uxModeAINav, "/assets/images/ux-ainav.png") : null, + cfg.modules?.chatsMod === "on" ? pickCard("ux", "chats", cur, i18n.uxModeChats, "/assets/images/ux-chats.png") : null, + pickCard("ux", "feed", cur, i18n.uxModeFeed, "/assets/images/ux-feed.png"), + cfg.modules?.phoneMod !== "off" ? pickCard("ux", "phone", cur, i18n.phoneTitle, "/assets/images/ux-phone.png") : null, + cfg.modules?.roomsMod !== "off" ? pickCard("ux", "rooms", cur, i18n.roomsTitle, "/assets/images/ux-rooms.png") : null + ), + submitRow(i18n.welcomeStepUxAction) + ) + } + if (key === "workflow") { + const selected = WIZARD_WORKFLOWS.includes(status.workflow) ? status.workflow : "full" + if (mobileUi.isMobile()) return mobileUi.workflowPick("/welcome/workflow", WIZARD_WORKFLOWS.includes(status.workflow) ? status.workflow : null, WIZARD_WORKFLOWS, true) + return form({ method: "POST", action: "/welcome/workflow", class: "welcome-select-form" }, + select({ name: "workflow" }, + WIZARD_WORKFLOWS.map(k => option({ value: k, ...(k === selected ? { selected: true } : {}) }, k === "full" ? i18n.welcomeWorkflow_full : (i18n[`workflow_${k}`] || k))) + ), + submitRow(i18n.welcomeStepWorkflowAction) + ) + } + if (key === "theme") return form({ method: "POST", action: "/welcome/theme", class: "welcome-ux-form" }, + div({ class: "welcome-ux-grid" }, + WIZARD_THEMES.map(([value, name]) => pickCard("theme", value, status.theme, name, `/assets/images/theme-${value.toLowerCase()}.png`)) + ), + submitRow(i18n.welcomeStepThemeAction) + ) + if (key === "community") { + const pubs = Array.isArray(status.pubs) ? status.pubs : [] + const bootstrap = status.bootstrap + const canJoin = status.canJoin !== false + return form({ method: "POST", action: "/welcome/community", class: "welcome-community-form" }, + bootstrap && bootstrap.running ? p(i18n.welcomeBootstrapRunning) : null, + bootstrap ? renderRestoreStatus(bootstrap) : null, + pubs.length && !canJoin + ? p({ class: "invites-join-note" }, status.publicMode === true ? i18n.invitesJoinPubMode : i18n.invitesJoinLocalOnly) + : null, + ...pubs.map(entry => label({ class: "welcome-pick" }, + canJoin && !entry.exhausted ? input({ type: "checkbox", name: "pub", value: entry.code }) : null, + renderSuggestedPub(entry, false) + )), + !pubs.length ? div({ class: "welcome-action" }, a({ href: "/invites", class: "filter-btn" }, i18n.invites)) : null, + div({ class: "welcome-action" }, + canJoin && pubs.some(e => !e.exhausted) ? button({ type: "submit", name: "action", value: "join" }, i18n.welcomeJoinPubs) : null, + button({ type: "submit", name: "action", value: "skip" }, i18n.welcomeNoFederation) + ) + ) + } + return form({ method: "POST", action: "/welcome/greeting", class: "welcome-greeting-form" }, + textarea({ + name: "text", + required: true, + minlength: String(FEED_TEXT_MIN), + maxlength: String(FEED_TEXT_MAX), + rows: 4, + cols: 50, + placeholder: i18n.welcomePmSubject + }), + submitRow(i18n.welcomeStepGreetingAction) + ) +} + +exports.welcomeView = async (status, profile = {}) => { + const steps = status.steps || {} + const usable = mobileUi.isMobile() ? mobileUi.welcomeSteps(status.usable || []) : (status.usable || []) + const opened = usable.find(key => !steps[key]) + const html = template( + i18n.welcomeTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.welcomeTitle), + p(i18n.welcomeDescription) + ), + status.pending && !mobileUi.isMobile() + ? form({ method: "POST", action: "/welcome/dismiss", class: "welcome-action" }, + input({ type: "hidden", name: "returnTo", value: "/" }), + button({ type: "submit" }, i18n.welcomeSkip) + ) + : null + ), + status.complete + ? section( + div({ class: "welcome-complete" }, + h2(i18n.welcomeCompleteTitle), + p(i18n.welcomeCompleteText), + div({ class: "welcome-action" }, a({ href: "/inhabitants", class: "filter-btn" }, i18n.welcomeFindPeople)) + ) + ) + : null, + ...usable.map(key => section( + details({ id: `step-${key}`, class: "welcome-step", ...(key === opened ? { open: true } : {}) }, + summary( + h2(`${steps[key] ? "✓ " : ""}${i18n[SECTIONS[key][0]]}`), + span({ class: "welcome-step-text" }, i18n[SECTIONS[key][1]]) + ), + stepAction(key, profile || {}, status) + ) + )), + // movil: "Ahora no" al final y discreto (arriba era lo primero que se tocaba) + status.pending && mobileUi.isMobile() + ? section(form({ method: "POST", action: "/welcome/dismiss", class: "welcome-action welcome-skip-end" }, + input({ type: "hidden", name: "returnTo", value: "/" }), + button({ type: "submit", class: "welcome-skip-btn" }, i18n.welcomeSkip))) + : null + ) + return status.bootstrap && status.bootstrap.running ? html.replace('', '') : html +} diff --git a/src/views/wiki_view.js b/src/views/wiki_view.js new file mode 100644 index 0000000..4caba70 --- /dev/null +++ b/src/views/wiki_view.js @@ -0,0 +1,382 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, ul, li, img } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, renderCardMetaRow, paged } = require("./main_views"); +const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderLicenseChip, renderLicenseSelect, renderTribeOriginChip } = require("./clearnet_view"); +const { renderStyledText, richTextarea } = require("../backend/renderStyledText"); +const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; +const FILTERS = ["all", "recent", "mine", "linked"]; + +const pageHref = (idOrSlug, tribeId = null) => + `/wiki/${encodeURIComponent(idOrSlug)}${tribeId ? `?tribeId=${encodeURIComponent(tribeId)}` : ""}`; + +const listHref = (filter, q, tribeId = null) => { + const parts = [`filter=${encodeURIComponent(filter || "all")}`]; + if (q) parts.push(`q=${encodeURIComponent(q)}`); + if (tribeId) parts.push(`tribeId=${encodeURIComponent(tribeId)}`); + return `/wiki?${parts.join("&")}`; +}; + +const renderWikiBody = (body, opts = {}) => { + const missing = new Set(opts.missing || []); + const tribeId = opts.tribeId || null; + const wikiLink = (target, slug, label) => { + const isMissing = missing.has(slug); + return a({ + href: isMissing ? `/wiki?filter=create&title=${encodeURIComponent(target)}${tribeId ? `&tribeId=${encodeURIComponent(tribeId)}` : ""}` : pageHref(slug, tribeId), + class: isMissing ? "wiki-link wiki-link-missing" : "wiki-link", + title: isMissing ? (i18n.wikiMissingLink || "Page does not exist yet") : label + }, label); + }; + const paragraphs = String(body || "").replace(/\r\n/g, "\n").split(/\n{2,}/); + return div({ class: "wiki-body" }, + ...paragraphs.filter(par => par.trim()).map(par => + p({ class: "wiki-paragraph" }, ...renderStyledText(par, { wikiLink, zoomImages: true })) + ) + ); +}; + +const diffLines = (oldText, newText) => { + const a = String(oldText || "").replace(/\r\n/g, "\n").split("\n"); + const b = String(newText || "").replace(/\r\n/g, "\n").split("\n"); + const n = a.length, m = b.length; + const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); + for (let i = n - 1; i >= 0; i--) { + for (let j = m - 1; j >= 0; j--) { + lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]); + } + } + const out = []; + let i = 0, j = 0; + while (i < n && j < m) { + if (a[i] === b[j]) { out.push({ type: "ctx", text: a[i] }); i++; j++; } + else if (lcs[i + 1][j] >= lcs[i][j + 1]) { out.push({ type: "del", text: a[i] }); i++; } + else { out.push({ type: "add", text: b[j] }); j++; } + } + while (i < n) { out.push({ type: "del", text: a[i++] }); } + while (j < m) { out.push({ type: "add", text: b[j++] }); } + return out; +}; + +const renderDiff = (oldText, newText) => + div({ class: "wiki-diff" }, + ...diffLines(oldText, newText).map(l => + div({ class: `wiki-diff-line wiki-diff-${l.type}` }, span({ class: "wiki-diff-sign" }, l.type === "add" ? "+" : l.type === "del" ? "−" : " "), span({ class: "wiki-diff-text" }, l.text || " ")) + ) + ); + +const reachChip = (page) => + page.tribeId || page.encrypted + ? null + : renderReachChip(!!page.clearnet, i18n, page.clearnet ? clearnetItemHref('wiki', page.title, page.id) : null); + +const statusChip = (page) => + renderStateChip(page.editPolicy === "author" ? "closed" : "open", "", page.editPolicy === "author" ? i18n.wikiStatusClosed : i18n.wikiStatusOpen); + +const pageChips = (page, params = {}, { withStatus = true } = {}) => [ + page.encrypted ? renderEncryptedChip(i18n) : null, + withStatus ? statusChip(page) : null, + page.versionCount > 1 ? renderStateChip("neutral", "", `${page.versionCount} ${i18n.wikiVersions || "versions"}`) : null, + page.isLinked ? renderStateChip("open", "", i18n.wikiLinkedChip || "Linked") : null +].filter(Boolean); + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const renderPageCard = (page, params = {}) => { + const origin = page.tribeOrigin || null; + const href = origin ? origin.href : pageHref(page.tribeId ? page.id : page.slug, page.tribeId); + return div({ class: "tribe-card wiki-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(page.id, href, origin + ? { author: page.author, reportTitle: page.title } + : { author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: (params.spreadMap && params.spreadMap.get(page.id)) || null, deleteAction: !page.tribeId && String(page.author) === String(userId) ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined }) + ), + div({ class: "tribe-card-body" }, + page.image ? a({ href }, img({ loading: 'lazy', class: "wiki-card-cover", src: `/blob/${encodeURIComponent(page.image)}`, alt: page.title || "" })) : null, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, page.title || "—"))), + div({ class: "card-chips-row" }, ...(origin ? [renderTribeOriginChip(origin)] : pageChips(page)), !origin && page.clearnet === true ? reachChip(page) : null, renderLicenseChip(page.license)), + origin && params.withBody && page.body ? renderWikiBody(page.body) : null + ) + ); +}; + +const renderFilters = (filter, q, params = {}, census = []) => { + const tribeId = params.tribeId || null; + const chipVisible = (mode) => { + if (mode === filter || mode === "all") return true; + if (mode === "mine") return census.some(p => String(p.author) === String(userId) || (p.versions || []).some(v => String(v.author) === String(userId))); + if (mode === "recent") return census.length > 0; + if (mode === "linked") return census.some(p => p.isLinked); + return true; + }; + const labelOf = (mode) => String(i18n[`wikiFilter${mode.charAt(0).toUpperCase() + mode.slice(1)}`] || mode).toUpperCase(); + return div({ class: "filters" }, + form({ method: "GET", action: "/wiki", class: "ui-toolbar ui-toolbar--filters" }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + ...(params.emptyMod ? [] : [ + ...FILTERS.filter(chipVisible).map(mode => button({ type: "submit", name: "filter", value: mode, class: filter === mode ? "filter-btn active" : "filter-btn" }, labelOf(mode))), + ...((census.length > 0 || filter === "changes") ? [button({ type: "submit", name: "filter", value: "changes", class: filter === "changes" ? "filter-btn active" : "filter-btn" }, String(i18n.wikiChanges || "Recent changes").toUpperCase())] : []) + ]), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.wikiCreate) + ) + ); +}; + +const renderForm = (page, params = {}) => { + const tribeId = (page && page.tribeId) || params.tribeId || ""; + const draft = params.draft || null; + const status = draft ? (String(draft.status || "OPEN").toUpperCase() === "CLOSED" ? "CLOSED" : "OPEN") : (page && page.editPolicy === "author" ? "CLOSED" : "OPEN"); + const titleValue = draft ? draft.title : (page ? page.title : (params.title || "")); + const bodyValue = draft ? draft.body : (page ? page.body : ""); + const tagsValue = draft ? draft.tags : (page ? page.tags.join(", ") : ""); + const summaryValue = draft ? draft.summary : ""; + const licenseValue = draft && draft.license ? draft.license : (page ? page.license : ""); + const canLicense = !page || String(page.author) === String(userId); + const statusOptions = [ + { value: "OPEN", label: i18n.wikiStatusOpen }, + { value: "CLOSED", label: i18n.wikiStatusClosed } + ]; + return div({ class: "div-center audio-form" }, + h2(page ? i18n.wikiUpdateSectionTitle : i18n.wikiCreateSectionTitle), + draft + ? section({ class: "post-preview wiki-preview" }, + div({ class: "preview-content" }, + h2(i18n.messagePreview), + h3({ class: "wiki-page-title" }, titleValue || "—"), + renderWikiBody(bodyValue, { missing: [], tribeId: tribeId || null }) + ) + ) + : null, + form({ method: "POST", action: page ? `/wiki/update/${encodeURIComponent(page.id)}` : "/wiki/create", enctype: "multipart/form-data" }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + page ? input({ type: "hidden", name: "pageId", value: page.id }) : null, + params.returnTo ? input({ type: "hidden", name: "returnTo", value: params.returnTo }) : null, + label(i18n.wikiTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.wikiTitlePlaceholder, value: titleValue }), br(), + label(i18n.wikiBodyLabel), br(), + richTextarea({ name: "body", rows: 14, maxlength: "20000", placeholder: i18n.wikiBodyPlaceholder }, bodyValue), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + page ? [label(i18n.wikiSummaryLabel), br(), input({ type: "text", name: "summary", maxlength: "200", placeholder: i18n.wikiSummaryPlaceholder, value: summaryValue }), br()] : null, + page + ? null + : [label(i18n.wikiStatusLabel), br(), select({ name: "status" }, ...statusOptions.map(o => option({ value: o.value, ...(o.value === status ? { selected: true } : {}) }, o.label))), br(), br()], + label(i18n.wikiTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.wikiTagsPlaceholder, value: tagsValue }), br(), + canLicense ? renderLicenseSelect(licenseValue, i18n) : null, + !tribeId && canLicense ? renderClearnetSelector(draft ? draft.clearnet === true : !!(page && page.clearnet), i18n) : null, + br(), + button({ type: "submit", class: "filter-btn", formaction: "/wiki/preview", attrs: { formmethod: "POST" } }, i18n.wikiPreview), + " ", + button({ type: "submit", class: "create-button" }, page ? i18n.wikiUpdate : i18n.wikiCreate) + ) + ); +}; + +const renderHeader = (tribe = null) => + div({ class: "tags-header module-header-line" }, + h2(tribe ? `${i18n.wikiTitle} · ${tribe.title}` : i18n.wikiTitle), + p(i18n.wikiDescription) + ); + +exports.wikiView = async (pages, filter = "all", params = {}) => { + const list = Array.isArray(pages) ? pages : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const isForm = filter === "create" || filter === "edit"; + const emptyMod = !isForm && census.length === 0 && !q; + return template( + i18n.wikiTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(params.tribe ? `${i18n.wikiTitle} · ${params.tribe.title}` : i18n.wikiTitle), + p(i18n.wikiDescription) + ), + renderFilters(isForm ? "all" : filter, q, { ...params, emptyMod }, census) + ), + section( + isForm + ? renderForm(filter === "edit" ? params.page : null, params) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), + form({ method: "GET", action: "/wiki", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null, + input({ type: "text", name: "q", value: q, placeholder: i18n.wikiSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, + list.length ? paged(list).map(pg => renderPageCard(pg, params)) : p(i18n.wikiNoPages) + ) + ] + ) + ); +}; + +exports.wikiChangesView = async (changes, params = {}) => { + const list = Array.isArray(changes) ? changes : []; + const census = Array.isArray(params.censusList) ? params.censusList : []; + return template( + i18n.wikiTitle, + section(renderHeader(params.tribe || null), renderFilters("changes", "", { ...params, emptyMod: census.length === 0 }, census)), + section( + div({ class: "tags-header" }, h2(i18n.wikiChanges)), + list.length + ? div({ class: "wiki-changes" }, ...paged(list).map(c => + div({ class: "card-section wiki-change" }, + div({ class: "card-field" }, + span({ class: "card-label" }, `#${c.index}`), + span({ class: "card-value" }, a({ href: `${pageHref(c.pageId, c.tribeId)}${c.tribeId ? "&" : "?"}version=${encodeURIComponent(c.versionKey)}` }, c.title || c.slug)) + ), + c.summary ? p({ class: "wiki-change-summary" }, c.summary.startsWith("restore:") ? i18n.wikiRestoredSummary : c.summary) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(c.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(c.author)) + ) + )) + : div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.wikiNoChanges)) + ) + ); +}; + +const versionRow = (page, v, idx, current, tribeId) => { + const isCurrent = v.key === page.tipId; + const base = pageHref(page.tribeId ? page.id : page.slug, tribeId); + const sep = tribeId ? "&" : "?"; + return div({ class: "card-section wiki-version" + (isCurrent ? " wiki-version-current" : "") }, + div({ class: "card-field" }, + span({ class: "card-label" }, `${i18n.wikiVersion} ${idx + 1}`), + isCurrent ? span({ class: "card-value" }, renderStateChip("mutuals", "", i18n.wikiCurrentVersion)) : span({ class: "card-value" }) + ), + v.summary ? [br(), p({ class: "wiki-change-summary" }, v.summary.startsWith("restore:") ? i18n.wikiRestoredSummary : v.summary), br()] : null, + div({ class: "tribe-side-actions wiki-version-actions" }, + form({ method: "GET", action: base }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + input({ type: "hidden", name: "version", value: v.key }), + button({ type: "submit", class: "update-btn" }, i18n.wikiViewVersion)), + idx > 0 + ? form({ method: "GET", action: base }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + input({ type: "hidden", name: "diff", value: v.key }), + button({ type: "submit", class: "update-btn" }, i18n.wikiDiff)) + : null, + !isCurrent && page.canEdit + ? form({ method: "POST", action: `/wiki/restore/${encodeURIComponent(page.id)}` }, + input({ type: "hidden", name: "version", value: v.key }), + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + button({ type: "submit", class: "update-btn" }, i18n.wikiRestore)) + : null + ), + p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(v.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(v.author)) + ); +}; + +exports.wikiHistoryView = async (page, params = {}) => { + const tribeId = page.tribeId || null; + const versions = page.versions.slice().reverse(); + const census = Array.isArray(params.censusList) ? params.censusList : []; + return template( + page.title, + section(renderHeader(params.tribe || null), renderFilters("all", "", { ...params, emptyMod: census.length === 0 }, census)), + section( + div({ class: "tags-header" }, h2(i18n.wikiHistory)), + div({ class: "wiki-history" }, ...versions.map((v, i) => versionRow(page, v, page.versions.length - 1 - i, page.tipId, tribeId))) + ) + ); +}; + +exports.wikiPageView = async (page, params = {}) => { + const tribeId = page.tribeId || null; + const base = pageHref(tribeId ? page.id : page.slug, tribeId); + const sep = tribeId ? "&" : "?"; + const version = params.version || null; + const diffVersion = params.diff || null; + const shownTitle = version ? version.title : page.title; + const shownBody = version ? version.body : page.body; + const census = Array.isArray(params.censusList) ? params.censusList : []; + const subscriptionNodes = params.subscription + ? renderSubscriptionBox({ target: page.id, scope: "wiki", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: page.isOwner, returnTo: base, inline: true, compact: true }) + : null; + const barChips = [reachChip(page), statusChip(page)].filter(Boolean); + const actions = div({ class: "tribe-side-actions wiki-actions-top" }, + span({ class: "wiki-actions-left" }, ...barChips), + page.isOwner && !tribeId && !page.encrypted ? renderClearnetSwitch("wiki", page.rootId || page.id, !!page.clearnet) : null, + ...(subscriptionNodes && subscriptionNodes.length ? subscriptionNodes : []), + page.canEdit && !version + ? form({ method: "GET", action: "/wiki" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: page.id }), + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + button({ type: "submit", class: "update-btn" }, i18n.wikiEdit)) + : null, + form({ method: "GET", action: `/wiki/${encodeURIComponent(page.id)}/history` }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + button({ type: "submit", class: "update-btn" }, `${i18n.wikiHistory} (${page.versionCount})`)), + form({ method: "GET", action: `/wiki/${encodeURIComponent(page.id)}/pdf` }, + button({ type: "submit", class: "update-btn" }, i18n.generatePdf)), + page.isOwner && tribeId + ? form({ method: "POST", action: `/wiki/delete/${encodeURIComponent(page.id)}` }, input({ type: "hidden", name: "tribeId", value: tribeId }), button({ type: "submit", class: "delete-btn" }, i18n.wikiDelete)) + : null + ); + let diffBlock = null; + if (diffVersion) { + const idx = page.versions.findIndex(v => v.key === diffVersion.key); + const prev = idx > 0 ? page.versions[idx - 1] : null; + diffBlock = div({ class: "card-section" }, + h3(`${i18n.wikiDiff}: ${i18n.wikiVersion} ${idx} → ${idx + 1}`), + renderDiff(prev ? prev.body : "", diffVersion.body) + ); + } + return template( + page.title, + section(renderHeader(params.tribe || null), renderFilters("all", "", { ...params, emptyMod: census.length === 0 }, census)), + section( + div({ class: "trending-card wiki-page" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(page.id, base, { author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: params.spread || null, deleteAction: page.isOwner && !tribeId ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined }) + ), + div({ class: "card-section" }, + actions, + div({ class: "shop-title-row" }, h2({ class: "wiki-page-title" }, shownTitle), renderLicenseChip(page.license)), + (() => { const rest = pageChips(page, params, { withStatus: false }); return rest.length ? div({ class: "card-chips-row" }, ...rest) : null; })(), + version ? p({ class: "wiki-version-notice" }, `${i18n.wikiViewingVersion} (${moment(version.createdAt).format("YYYY/MM/DD HH:mm")}) — `, a({ href: base }, i18n.wikiBackToPage)) : null, + diffBlock || renderWikiBody(shownBody, { missing: page.missingLinks, tribeId }), + renderTags(page.tags), + renderCardMetaRow(p({ class: "card-footer" }, + span({ class: "date-link" }, `${moment(page.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(page.author) + )) + ), + ), + div({ class: "card-section wiki-backlinks" }, + h3(i18n.wikiLinkedPages), + page.linkedPages && page.linkedPages.length + ? ul(...page.linkedPages.map(b => li(a({ href: pageHref(b.tribeId ? b.id : b.slug, b.tribeId) }, b.title)))) + : p(i18n.wikiNoLinkedPages) + ) + ) + ); +}; + +exports.renderTribeWikiSection = (tribe, pages, toolbar = null, wrap = (pg, card) => card) => { + const items = Array.isArray(pages) ? pages : []; + const createBtn = form({ method: "GET", action: "/wiki" }, + input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "tribeId", value: tribe.id }), + button({ type: "submit", class: "create-button" }, i18n.tribeWikiCreate)); + const header = toolbar || div({ class: "tribe-content-header" }, h2(i18n.tribeSectionWiki), createBtn); + if (!items.length) return div({ class: "tribe-content-list" }, header, div({ class: "no-content-box" }, p(i18n.tribeWikiEmpty))); + return div({ class: "tribe-content-list" }, + header, + div({ class: "tribe-grid" }, ...paged(items).map(pg => wrap(pg, renderPageCard(pg, { tribeId: tribe.id, withBody: true })))) + ); +}; + +exports.renderWikiBody = renderWikiBody; +exports.diffLines = diffLines;